目录
信号的处理方式
默认操作
介绍
每个信号都有一个默认的处理方式,即系统默认定义的操作
- 如果进程没有设置特定信号的处理方式,系统会执行该信号的默认操作
- 大多数信号的默认操作都是结束进程
- 以宏的形式被调用 -- SIG_DFL(在handler结构中是通过整数0强转来的)
示例
- eg : 收到外卖送达的消息后,你默认去取外卖
- SIGINT默认操作是终止进程
- SIGTERM的默认操作是终止进程并清理资源
自定义操作
- 要求os在处理该信号时,切换到[用户态]来执行这个处理函数
- 通过注册[信号处理函数]捕获特定信号,来完成对信号的特定响应
- 当进程接收到捕获的信号时,会调用预先注册的信号处理函数,来执行相应的操作
- eg: 外卖送达后,你本应该吃掉,但你选择将它送给外卖员
处理期间再次收到信号
引入
- 如果我们使用自定义函数处理信号,那么处理期间我们会在用户态下执行代码
- 并且我们使用的一般操作(比如io),底层可能会使用系统调用,那我们就会陷入到内核态去完成任务
- 那当我们完成任务返回到原先调用位置时,是不是又是处于内核态转为用户态的时候,也就是可以处理信号的时机
如果在转换之前,我们又收到了信号,会在这个时候再次处理信号吗?
- 不会的,如果此时再去处理信号,就会形成递归
- linux不允许这样的行为,处理信号时只能有一层操作
如何实现 -- block信号集
- 还记得block信号集吗,用来屏蔽信号的(即使信号产生了,也不会递达,处于未决状态)
- 它其实还有一个作用 -- 用来保证信号不会递归式处理
- 当某个信号正在处理时,block会自动添加当前信号的屏蔽字
示例
void show_sigset() { sigset_t pending; sigpending(&pending); for (int i = 1; i <= 31; ++i) { if (sigismember(&pending, i)) { cout << 1; } else { cout << 0; } } cout << endl; } void show_block() { sigset_t oldset; sigprocmask(SIG_BLOCK, NULL, &oldset); for (int i = 1; i <= 31; ++i) { if (sigismember(&oldset, i)) { cout << 1; } else { cout << 0; } } cout << endl; } void func1(int signum) { cout << "im " << getpid() << "i got a signal : " << signum << endl; while (true) { show_sigset(); show_block(); cout<<endl; sleep(1); } } void test7() { cout << "im " << getpid() << endl; signal(2, func1); while (true) { show_sigset(); show_block(); cout<<endl; sleep(1); } }
- 当我们发送第一个2号信号后,进程自动屏蔽了2号信号:
- 当我们继续发送2号信号,进程的pending设置成1:
- 但因为被屏蔽了,所以无法被递达
signal函数
介绍
- 用于注册信号处理函数,以定义进程在接收到特定信号时应采取的操作
- 相当于修改了进程收到signum信号时做出的行为
本质
- 结合底层结构来分析:
- 修改处理方法,就是去修改该进程pcb中的[handler结构]中存放的指针
函数原型
signum
要处理的信号的编号
handler
- 一个指向特定类型函数的指针,该函数用于处理信号
除了传入自己定义的函数之外,也可以传入signal.h文件中定义的宏(也就是处理方式中的默认操作和忽略操作)
示例
#include<iostream> #include<unistd.h> #include<signal.h> using namespace std; void function(int signum){ cout<<"im " << getpid() <<"i got a signal : "<<signum<<endl; } void test1(){ signal(2,function); while(1){ cout<<"im here : "<<getpid()<<endl; sleep(1); } }
可以看到,当我们输入ctrl c时,就会调用我们注册的那个函数
(也就验证了,键入ctrl c等于向当前进程发送2号信号(SIGINT)):
sigaction函数
介绍
和signal一样的功能,只是使用方法不一样
函数原型
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
signum
要处理的信号编号
act
- 输入型参数
- 是一个[内核为我们提供的sigaction结构体]的指针(素的,它和函数同名)
- 里面保存着我们控制信号处理的变量
sigaction结构体的定义
struct sigaction { void (*sa_handler)(int); // 指定信号处理函数的地址 void (*sa_sigaction)(int, siginfo_t *, void *); // 与 sa_handler 二选一,指定信号处理函数的地址 sigset_t sa_mask; // 在信号处理函数执行期间将阻塞的信号集 int sa_flags; // 控制信号处理的行为 void (*sa_restorer)(void); // 不再使用,一般设置为 NULL };
oldact
输出型参数,用于保存修改之前的结构体
示例
void func(int signum) { cout << "im " << getpid() << "i got a signal : " << signum << endl; } void test6(){ struct sigaction sa; sigset_t block; sigemptyset(&block); sa.sa_handler=func; sa.sa_mask=block; //没有屏蔽任何信号 sigaction(2,&sa,NULL); while(1){ sleep(1); } }
可以看到,我们的进程成功修改了2号信号的处理方法:
忽略信号
介绍
进程可以选择忽略某个特定的信号
- 如果设置了忽略信号的处理方式,当进程接收到该信号时,不会采取任何操作,信号被丢弃
- 以宏的形式被调用 -- SIG_IGN(在handler结构中,是将整数1强转得来的)
- eg: 设置的闹钟响了后,你选择继续睡觉,忽略这个闹钟
SIGCHLD信号
- 用于通知父进程子进程状态变化的信号
- 当一个子进程终止或暂停时,内核会向其父进程发送SIGCHLD信号
- 可以看到,它默认被父进程忽略:
示例 -- 是否收到信号
void func(int signum) { cout << "im " << getpid() << "i got a signal : " << signum << endl; } void test2(){ signal(SIGCHLD,func); if(fork()==0){ cout<<"im child : "<<getpid()<<endl; sleep(2); exit(0); } while(true){ ; } }
可以看到,我们的父进程是收到了信号的:
但是,这个信号有什么用呢?
父进程虽然收到了信号,但是并不会做任何处理啊,子进程还是变成了僵尸进程:
void test1(){ if(fork()==0){ cout<<"im child : "<<getpid()<<endl; sleep(2); exit(0); } while(true){ ; } }
示例 -- 手动设置忽略操作
但素,如果我们手动设置父进程对SIGCHLD的忽略操作,情况就不一样了:
void test3(){ signal(SIGCHLD,SIG_IGN); if(fork()==0){ cout<<"im child : "<<getpid()<<endl; sleep(2); exit(0); } while(true){ ; } }
当子进程退出后,竟然没有成为僵尸进程!
这是为什么呢?
原理
(只是我的理解哈)
我们不是有wait函数吗,专门用于父进程回收子进程的,也就是说,这个信号在系统中其实是用不到的
- 当子进程退出,os认为父进程会去等待它,还需要拿退出信息等等,所以自己就先没有管
- 但如果手动设置了忽略操作,就是明确告诉os,父进程不需要管这个进程,所以os就直接释放掉子进程了
我们也可以通过自定义处理函数,让父进程收到信号时,自动回收子进程:
示例 -- 修改信号处理函数
我们使用wait函数,手动回收子进程:
void func_wait(int signum) { cout << "im " << getpid() << "i got a signal : " << signum << endl; wait(nullptr); cout << "success waited" << endl; } void test3() { signal(SIGCHLD, func_wait); if (fork() == 0) { cout << "im child : " << getpid() << endl; sleep(2); exit(0); } while (true) { ; } }
示例 -- 多个子进程
如果有多个子进程在严格意义上的同一时间退出呢?
- 但是我们的pending信号集只能表示是否收到,而不能表示数量
- 所以,为了可以回收每一个子进程,可以循环n次wait()
那如果有部分子进程并没有退出怎么办?
- 我们无法知道哪个进程退出了,只能也是循环n次进行回收
- 那这样就不能进行阻塞等待了
- 如果当前检测的进程并没有退出,那执行流就会卡在这里,直到该进程退出
- 所以我们必须使用非阻塞等待,也就是使用waitpid+非阻塞等待选项(可以将子进程pid保存起来进行遍历 / 传入-1)
不能被忽略的信号
但是有一些信号是不能被忽略的
- 也就是我们之前验证过的,他们都不能被忽略/修改行为/阻塞
手动设置和系统默认的区别
像上面的SIGCHLD信号那样,有时候信号的默认动作和用户手动设置,其功能可能会不一样