基本概念
信号是linux系统提供的一种向指定进程发送特定事件的一种方式,信号产生式异步的;进程能识别信号,处理信号。
真正发送信号的事操作系统,本质是修改pcb内信号位图
信号产生:
通过kill命令发送信号或者键盘产生(如Ctrl+c),系统调用,软件条件(管道读端被关闭,写开着,SIGPIPE),异常产生信号。
系统调用
向目标发送信号:
向自己发送信号:
向自己发送6号信号:
软件条件:
指定时间后接受到14号SIGALRM信号。
异常产生信号:
除数为0,野指针都会出错发送信号,使进程中断。
查看信号:
kill -l
1-31 普通信号 32-64 实时信号
信号处理:
有三种:默认动作、忽略动作、自定义处理(信号的捕捉)
默认动作基本是:终止、暂停或者忽略。
查看默认动作:
man 7 signal
在后面(core和term基本是终止)
自定义捕捉(捕捉一次,后续一直有效,但是9号信号不允许自定义捕捉):
测试代码:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void hander(int sig)
{
cout << "get!" << endl;
}
int main()
{
signal(2, hander);
while(1)
{
cout << "syx 666" << endl;
sleep(1);
}
return 0;
}
发送2号信号:
kill -2 pid
使用kill系统调用发送信号:
#include<iostream>
using namespace std;
#include<sys/types.h>
#include<signal.h>
int main(int argc,char* argv[])
{
if(argc!=3)
{
cerr << "error!" << endl;
return 1;
}
int pid = stoi(argv[2]);
int flag = stoi(argv[1]);
kill(pid,flag);
}
./emitsig 2 pid就可以观察到同样的效果。
向自己发送6号信号:
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
void hander(int sig)
{
cout << "abort!" << endl;
}
int main()
{
signal(6, hander);
while(1)
{
cout << "syx 666" << endl;
sleep(1);
abort();
}
return 0;
}
结果(还是要终止):
发送alarm信号:
#include<iostream>
using namespace std;
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
int cnt = 0;
void hander(int sig)
{
cout << cnt << endl;
exit(1);
}
int main()
{
signal(14, hander);
alarm(1);
while(1)
{
cnt++;
}
return 0;
}
在系统内部也会对闹钟做管理,当然也是一个结构体,按照未来的超时时间构建一个最小堆,alarm(0)取消闹钟,返回剩余时间。闹钟设置一次就会触发一次。
当出现错误,操作系统会向进程发送信号,一般都是终止进程,为了防止死循环,本质是释放进程的上下文数据,包括溢出标志数据和其他异常数据。
core和term
都叫做终止进程,区别是term是异常终止,core也是,但会帮我们形成一个debug文件。
查看限制;
ulimit -a
把core file改一下大小,允许形成异常文件,保存进程退出时候的镜像数据(核心转储)
运行下面代码:
#include<iostream>
int main()
{
int a = 10;
a /= 0;
return 0;
}
就会发现生成了一个core文件:
可以协助我们进行调试。
阻塞信号
1.实际执行信号的处理动作称为信号递达(Delivery)
2.信号从产生到递达之间的状态,称为信号未决(Pending)。
3.进程可以选择阻塞 (Block )某个信号。(阻塞和未决没关系)
4.被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
5.注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
在PCB中有一个pending位图,表示未决信号集,对应为为1的时候,表示该信号接收到。
signal函数就是用来信号函数的。
途中handler表式一个函数指针数组,用来处理对应信号(信号递达),对应信号就是对应下标。
block和pending一样是一个32位位图,比特位对应相应信号,表示信号是否阻塞,当block对应为1,pending对应位信号不能递达,只有阻塞被解除,才能递达。
sigset_t
未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
sigset_t就是linux操作系统给用户的数据类型。
信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set); //清空
int sigfillset(sigset_t *set); //全部置1
int sigaddset (sigset_t *set, int signo); //对应位置1
int sigdelset(sigset_t *set, int signo); //对应位置0
int sigismember(const sigset_t *set, int signo); //信号是否在集合中
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
how:
第二个是输入型参数,第三个是输出型参数,修改前保存老的信号屏蔽字.
sigpending
获取pending位图,成功返回0,失败返回-1。
屏蔽2号信号代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
void PrintPending(sigset_t& pending)
{
for (int i = 31; i > 0;i--)
{
if(sigismember(&pending,i))
{
cout << "1";
}
else
cout << "0";
}
cout << endl;
}
void hander(int sig)
{
cout << "unblock" << endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
}
int main()
{
signal(2, hander);
// 屏蔽2号信号
sigset_t old_set, block_set;
sigemptyset(&old_set);
sigemptyset(&block_set);
sigaddset(&block_set, 2);//此处并没有修改pcb的block表
//修改pcb的block表
sigprocmask(SIG_BLOCK, &block_set, &old_set);
int cnt = 10;
while (1)
{
if(cnt==0)//解除屏蔽
sigprocmask(SIG_SETMASK, &old_set, &block_set);
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
cnt--;
}
return 0;
}
最终现象是发送2号信号后,对应的比特位在屏蔽时是1,解除后为0(递达之前置0);解除信号屏蔽一般会立即处理当前被解除的型号。
信号处理
忽略信号:SIG_IGN
默认动作:SIG_DEF
信号被捕捉不会立即处理,而是在进程从内核态返回到用户态的时候,进行处理。
操作系统不能直接转过去执行用户提供的的方法,而是要切换用户心态去执行。信号捕捉要经历四次状态切换(自定义处理函数)。
内核级页表只有一张,多个进程共用一张,访问操作系统和访问库函数差不多,由于操作系统不信任用户,所以用户访问操作系统只能通过系统调用。
OS中断机制:
操作系统的本质就是一个死循环,时钟中断,不断调度系统任务;在操作系统源码中会有一个系统调用的函数指针数组,我们只需要找到特定数组下标,就能使用对应的系统调用。调用封装的系统调用的函数,会把对应系统调用数组的下标保存到寄存器中。由外部形成的中断叫外部中断,外部器件发送中断信号,内部直接形成的中断叫陷阱、缺陷(0x80)。
当系统调用的时候,需要把状态切换为内核态(由3置0),否则会被拦截。
信号捕捉
除了signal捕捉信号的方式,还有一个:
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
第二个参数是输入型参数,第三个参数是输入型参数。对特定信号做捕捉,oact是记录更改之前的act,以便于回复。
捕捉2号信号例子:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int sig)
{
cout << "get " << sig << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(2, &act, &oact);
while(1)
{
cout << "hello!" << endl;
sleep(1);
}
return 0;
}
当我们正在处理特定信号的时候,特定信号会被屏蔽,处理完成会解除屏蔽,这是为了防止递归调用导致崩溃。
在sigaction中的sa_mask表示在处理特定型号时,需要对其他信号也屏蔽,如对三号信号屏蔽:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int sig)
{
cout << "get " << sig << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
act.sa_flags = 0;
sigaction(2, &act, &oact);
while(1)
{
cout << "hello!" << endl;
sleep(1);
}
return 0;
}
但是有一些信号是不能被屏蔽的,如9号信号。
可重入函数
main函数在执行insert的时候,head还没有改变就接收到信号,去头插入另一个节点,就造成了节点丢失,内存泄露,这里的insert就被重复进入了(被重入了)。因此,如果重复进入的函数会出问题了,该函数就叫做不可重入函数,我们使用的大部分函数都是不可重入函数(使用全局变量基本都是不可重入的)。
volatile
来看一个现象:
#include<iostream>
using namespace std;
#include<signal.h>
int flag = 0;
void handle(int sig)
{
cout << "get:" << sig << endl;
flag = 1;
}
int main()
{
signal(2, handle);
while (!flag);
return 0;
}
运行后发现程序竟然终止不掉了。
这是因为编译器优化,cpu发现多次访问flag,就把flag放在寄存器中了,while(!flag)访问寄存器中的数据,而handle修改的是内存,因此一直死循环。
对此,为了防止出现这种问题,我们要求保持内存可见性,每次从内存中读取,就使用volatile关键字:
volatile int flag = 0;
SIGCHLD信号
子进程在退出的时候会给父进程发送SIGCHLD信号,默认处理方式是忽略。
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void notice(int sig)
{
cout << "child quit!" << endl;
}
int main()
{
signal(SIGCHLD, notice);
pid_t id = fork();
if(id==0)
{
cout << "i am child" << endl;
sleep(1);
exit(1);
}
int cnt = 10;
while(cnt--)
{
cout << "father" << endl;
sleep(1);
}
cout << "over!" << endl;
return 0;
}
但是有个问题:多个子进程同时退出,都会向父进程发送信号,但是pending表只改了一次,因此,应该这么改:
void notice(int sig)
{
int flag = 0;
while (!flag)
{
pid_t id = waitpid(-1, nullptr, WNOHANG);
if(id>0)
{
cout <<id<< " child quit!" << endl;
}
else
flag = 1;
}
}
因此为了避免造成僵尸进程,就可以用这种方式(可以将处理函数设为SIG_IGN,这个忽略和列表中原始的忽略不同,它会自动回收资源)。