目录
一.信号
1.介绍
2.信号概念
3.查看系统定义的信号列表
4.信号处理的方式
二.信号产生前
1.用户层产生信号的方式
三.产生信号
1.通过按键产生信号
2.调用系统函数向进程发信号
(1)kill
(2)raise
(3)abort
3.软体条件产生信号
4.硬件异常产生信号
四.阻塞信号
1.信息其它相关概念
2.信号在内核中的表示:
3.sigset_t
4.信号集操作函数
5.sigprocmask
6.sigpending
五.捕捉信号
1.如何实现信号的捕捉
2.sigaction
六.可重入函数
七.volatile
八.SIGCHLD
前言:这一篇将介绍Linux进程信号,包括产生信号前、产生信号时和产生信号后3个阶段,已经可重入函数、volatile等。
一.信号
1.介绍
信号是给进程发送的,进程要具备处理信号的能力。
① 该能力一定是预先早就已经有了的(程序员写的 ---- OS提供)
② 进程能够识别对应的信号
③ 进程能够处理对应信号
对于进程来讲,即使是信号还没有产生,进程已经具有识别和处理这个信号的能力了。
#include <iostream>
#include <unistd.h>
int main()
{
while(1)
{
printf("hello linux\n");
sleep(1);
}
return 0;
}
正常运行一个进程,直接用ctrl+c就可以终止,并且按任何命令都是没有效果的。
但是如果我们用在后面加一个&,就会将这个进程变成后台进程了:
看这张图,在后面加了&后,会发现按ctrl+c无法终止。但是按命令是有效果的,因为这是后台进程,不会影响命令。
如果想要查看该后台进程,可以按jobs,看到当前后台进程。
如果想要将后台进程变为前台进程,可以按fg [jobs中对应的号],这里对应的是1,就是fg 1。这时,按ctrl+c就可以结束进程了。
jobs
fg [jobs中对应的号]
下面我们去掉打印。
#include <iostream>
#include <unistd.h>
int main()
{
while(1)
{
// printf("hello linux\n");
sleep(1);
}
return 0;
}
这里可以通过jobs查看所有创建的后台进程,然后这里把3进程转为前台进程,并结束。
如果按了fg之后,又不想转换成后台进程了,就可以先按ctrl+z,这里通过jobs可以看到进程从Running变成了Stopped,然后再用bg [jobs中对应的号],即可变回Running。
bg [jobs中对应的号]
① Ctrl-c 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
② Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
③ 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
2.信号概念
信号是进程直接事件异步通知的一直方式,属于软中断。
3.查看系统定义的信号列表
kill -l
注意看这里31到34之间没有信号,32、33都是没有的,0号信号也没有。
因此呢,这个信号是可以分成两组的,一组是1号到31号,另一组的34号到64号。
1号到31号是普通信号,34号到64号是实时信号。
4.信号处理的方式
因为信号产生是异步的,所以当信号产生的时候,对应的进程可能正在做更重要的事情,而这时我们进程就可以暂时不处理这个信号。
进程暂时不处理这个信号,不代表这个信号不会被处理。OS是一定会记住这个信号的。
信号的捕捉,递达处理动作(信号的处理):
① 默认动作
② 忽视
③ 自定义动作
二.信号产生前
1.用户层产生信号的方式
信号是会被进程记住的,这个信号会被记录在进程的PCB中,包括了这个信号的所有属性。
信号有没有产生:比特位在位图中的内容是1代表产生,0代表没有
什么信号产生:比特位在位图中的位置
take_struct{
uint32_t sig
}
只有OS能之间修改这个take_struct内的数据位图。
OS是进程的管理者,进程的所有的属性的获取和设置,只能由OS来操作。
无论信号怎么产生,最终一定只能是OS来进行信号的设置。
三.产生信号
1.通过按键产生信号
ctrl-c其实就是一种信号,是2号信号
sighandler_t signal(int signum, sighandler_t handler)
sighandler_t:返回值为void,参数为int的一个函数指针
signum:对哪个信号设置捕捉动作
handler:是一个函数指针,这个函数允许用户自定义对信号的处理动作
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}
int main()
{
// 2号信号
// 这里不是调用hander方法,这里只是设置了一个会回调,让SIGINT(2)产生的时候,该方法才会被调用
// 如果不产生SIGINT,该方法不会被调用
// ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
// 现在更改了对2号信号的处理,设置了用户自定义处理动作
signal(SIGINT, handler);
sleep(3);
cout << "进程已经设置完了" << endl;
sleep(3);
while (true)
{
cout << "我是一个正在运行中的进程: " << getpid() << endl;
sleep(1);
}
// while(1)
// {
// // printf("hello linux\n");
// sleep(1);
// }
return 0;
}
这里的SIGINT就是2号信号:
结果为:
signal不是调用hander方法,这里只是设置了一个会回调,让SIGINT(2)产生的时候,该方法才会被调用
如果不产生SIGINT,该方法就不会被调用
ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
现在更改了对2号信号的处理,设置了用户自定义处理动作
下面再试一下3号信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}
int main()
{
// 2号信号
// 这里不是调用hander方法,这里只是设置了一个会回调,让SIGINT(2)产生的时候,该方法才会被调用
// 如果不产生SIGINT,该方法不会被调用
// ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
// 现在更改了对2号信号的处理,设置了用户自定义处理动作
signal(SIGINT, handler);
sleep(3);
cout << "进程已经设置完了" << endl;
sleep(3);
while (true)
{
cout << "我是一个正在运行中的进程: " << getpid() << endl;
sleep(1);
}
// while(1)
// {
// // printf("hello linux\n");
// sleep(1);
// }
return 0;
}
这里我们就会发现无论是按ctrl+c,还是按ctrl+\都无法结束进程,都会去调用自定义处理动作。
那么如何结束呢:
如果把9号信号也给signal自定义处理动作了,那就无法kill -9来结束了,那么把1到31的信号全部都用signal,自定义处理动作了,我们如何结束?
其实这个是无法对9号进程signal自定义处理的,OS决定的。
用户层产生信号的方式:
① 键盘产生
注意这个是键盘产生的信号,不是键盘发送的信号,是OS发送的信号。
那么OS是怎么发送信号的呢?
OS能找到每个进程的take_struct,也能找到当前显示器上前台进程的take_struct,每一个进程的take_struct内部都有一个位图,OS在拿到了对应的信号后,将这个对应的位置由0设为1,OS就完成了信号的发送(OS发送信号,也可以说成是写入信号)
2.调用系统函数向进程发信号
(1)kill
这个kill不只是一个指令,也是一个函数,下面来介绍该函数:
int kill(pid_t pid, int sig)
作用:向指定进程发送指定信号
成功了就是0,失败了就是-1
接下来实现一个我们自己的kill:
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
static void Usage(const std::string& proc)
{
cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
// 写一个kill命令,mykill 9 ...
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
if(kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
{
cerr << "kill: " << strerror(errno) << endl;
exit(2);
}
}
结果如下:
通过运行我们的进程,并且输入对应的参数,就可以实现kill指令的作用。
如果我们随便kill一个进程,是没有权限的。
(2)raise
int raise(int sig);
作用:给自己发任意信号
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}
int main()
{
signal(2, handler);// 自定义捕捉动作handler
while(1)
{
sleep(1);
raise(2);
}
}
这里可以看到,使用了raise,就一直给自己发送上面自定义捕捉动作的2号信号。
(3)abort
void abort(void);
作用:终止进程(给自己发6号信号: SIGABRT)
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}
int main()
{
signal(2, handler);// 自定义捕捉动作handler
while(1)
{
sleep(1);
abort(2);
}
}
调用abort,之间就终止了进程。
这个6号信号是可以被捕捉的,但是捕捉后依旧会退出。
3.软体条件产生信号
unsigned int alarm(unsigned, int seconds);
作用:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程。
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int cnt = 0;
int main()
{
// 统计这个进程1s,cnt++多少次
signal(SIGALRM, handler);
alarm(1);
while(1)
{
printf("hello : %d\n", cnt++);
}
}
这里可以看到,printf了3万多次后,就结束了,说明在这1s内进行了3万多次的printf
我们试试不带IO,测试一下:
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int cnt = 0;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << " cnt: " << cnt << endl;
exit(1);
}
int main()
{
// 统计这个进程1s,cnt++多少次
signal(SIGALRM, handler);
alarm(1);
while(1)
{
cnt++;
}
}
这个次数已经达到了4亿多,是带printf的1万多倍,这完全可以说明IO的慢。
4.硬件异常产生信号
我们一定见过进程崩溃的样子,那么崩溃的本质是是什么呢?
进程崩溃的本质,是该进程收到了异常信号。
下面介绍2个因为硬件异常,而导致OS向目标进程发送信号,进而导致进程终止的现象:
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
exit(1);
}
// 写一个kill命令,mykill 9 ...
int main(int argc, char *argv[])
{
// 进程崩溃的本质,是该进程收到了异常信号
for(int sig = 1; sig <= 31; sig++)
{
signal(sig, handler);
}
// int a[10];
// a[10000] = 1000;
// int *p = nullptr;
// *p = 100;
// int b = 10;
// b /= 0;
}
这里我们依次测试下面注释掉的内容:
根据测试结果,我们可以看到,第一个越界和第二个野指针错误,都是11号信号,而第三个除零错误是8号信号。
除零:
当我们除零的时候,CPU内的状态寄存器会被设置成为:有报错:浮点数越界
CPU的内部寄存器是硬件,OS就会识别到CPU内有报错了。
OS知道了是谁报的错,并且是什么报错后,就会让OS->构造信号->向目标进程发送信号->目标进程在合适的时候,处理信号->终止进程
越界&&野指针:
在语言层面使用的地址(指针),其实都是虚拟地址->转换成物理地址->访问物理内存->读取对应的数据和代码
如果虚拟地址有问题,地址转换的工作是由(MMU(硬件) + 页表(软件))做的,转化过程就会引起问题->表现在硬件MMU上->OS就发现硬件出现了问题
OSOS知道了是谁出的问题,并且是什么问题后,就会让OS->构造信号->向目标进程发送信号->目标进程在合适的时候,处理信号->终止进程
那么崩溃了,一定会导致进程终止吗?
不一定。
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
//exit(1);
}
int main(int argc, char *argv[])
{
// 进程崩溃的本质,是该进程收到了异常信号
for(int sig = 1; sig <= 31; sig++)
{
signal(sig, handler);
}
int b = 10;
b /= 0;
return 0;
}
这里我们把自定义捕捉动作中的exit去掉
根据结果可以看到,虽然崩溃了,但是进程一直在运行。
因为我们对其进行了自定义捕捉,修改了默认动作,所以进程没有退出。如果我们不对其进行捕捉,采用默认动作,那么进程还是会退出的。
因此,出现了异常,进程是否被终止,并不是完全由OS决定的,是由用户来决定的。
在C++的异常中,我们一般抛出异常后就直接调用exit等函数退出了,那么如果我们不退出会怎么样呢?
进程就不会被终止,需要我们手动对进程进行终止。
四.阻塞信号
1.信息其它相关概念
递达:实际执行信号的处理动作称为信号递达(Delivery)
未决:信号从产生到递达之间的状态,称为信号未决(Pending)。
阻塞:进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
2.信号在内核中的表示:
block:阻塞信号集,和pending都是位图,对应的比特位为1,就会拦截对应的信号去递达对应的方法,即使pending为1也没用。
pending:用来识别信号中对应信号的位置为1,就说明来了,围殴0,说明没来。
handler:用来处理信号,信号的编号就作为这个函数指针的数组下标,直接可以访问到对应的自定义的方法,或者系统默认的处理方法。
这3个应该先看pending位,如果pending为1,再去看block,如果block为0,再去看handler。
这里提到了阻塞信号,而我们之前学知道忽略信号,这两个有什么区别呢?
忽略信号:处理信号的一种,处理的方式是忽略,什么都不做,将pending位图由1置0。
阻塞信号:不是处理信号,是拦截信号,不允许去处理信号。
3.sigset_t
每个信号只有一个bit的未决标志,非0即1,不会记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略。
4.信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
① int sigemptyset(sigset_t *set);
② int sigfillset(sigset_t *set);
③ int sigaddset (sigset_t *set, int signo);
④ int sigdelset(sigset_t *set, int signo);
⑤ int sigismember(const sigset_t *set, int signo);
第1个是清空位图,第2个是将位图全置1,第3个是添加一个,第4个是删除一个,第5个是判断某个。
5.sigprocmask
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
作用:可以读取或更改进程的信号屏蔽字(阻塞信号集)
第一个参how数有3个选择:
① SIG_BLOCK:添加信号屏蔽字,相当于 mask = mask | set
② SIG_UNBLOCK:从信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set
③ SIG_SETMASK:设置当前信号屏蔽字为set所值的值,相当于mask = set
第二个参数set是输入型参数:代表的是新增、删除或覆盖式的重新添加。
第三个参数oset是输出型参数:返回老的信号屏蔽字
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
6.sigpending
int sigpending(sigset_t *set);
作用:获取进程的pending信号集,通过set参数传出。
调用成功就返回0,出错则返回-1
下面我们来看一下当前进程的pending信号集:
如果直接获取2号,会因为递达的太快,我们没等看到打印出来,就已经结束了,所以这里我们利用信号屏蔽字,把2号信号屏蔽,来看:
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
exit(1);
}
static void showPending(sigset_t *pendings)
{
for(int sig = 1; sig <= 31; ++sig)
{
if(sigismember(pendings, sig))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
// 3.屏蔽2号信号
sigset_t bsig, obsig;
sigemptyset(&bsig);
sigemptyset(&obsig);
// 3.1添加2号信号到信号屏蔽字中
sigaddset(&bsig, 2);
// 3.2设置用户级的信号屏蔽字到内核中,让当前进程屏蔽到2号信号
sigprocmask(SIG_SETMASK, &bsig, &obsig);
// 2.signal
signal(2, handler);
// 1.不断的获取当前进程的pending信号集
sigset_t pendings;
while(true)
{
// 1.1 清空信号集
sigemptyset(&pendings);
// 1.2 获取当前进程(谁调用,获取谁)的pending信号集
if(sigpending(&pendings) == 0)
{
// 1.3 打印当前进程的pending信号集
showPending(&pendings);
}
sleep(1);
}
return 0;
}
这里就可以看到,我们调用2号信号时,因为信号屏蔽字的原因,pending里对应的位置变成了1,但是却并没有去调用2号信号,因为2号信号被屏蔽了。
那么如果我们把信号全部屏蔽了会怎么样呢?
从1开始屏蔽,一直到8都可以成功,但是到9的时候,就会直接把进程killed,而不会被屏蔽。
这里我们把信号全部屏蔽了,然后后面再把2号信号解除,来看一下测试结果:
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
using namespace std;
int cnt = 0;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
// exit(1);
}
static void showPending(sigset_t *pendings)
{
for(int sig = 1; sig <= 31; ++sig)
{
if(sigismember(pendings, sig))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
cout << "pid: " << getpid() << endl;
// 3.屏蔽2号信号
sigset_t bsig, obsig;
sigemptyset(&bsig);
sigemptyset(&obsig);
// // 3.1添加2号信号到信号屏蔽字中
// sigaddset(&bsig, 2);
// 全屏蔽
for(int sig = 1; sig <= 31; ++sig)
{
sigaddset(&bsig, sig);
signal(sig, nullptr);
}
// 3.2设置用户级的信号屏蔽字到内核中,让当前进程屏蔽到2号信号
sigprocmask(SIG_SETMASK, &bsig, &obsig);
// 2.signal
// signal(2, handler);
// 1.不断的获取当前进程的pending信号集
sigset_t pendings;
while(true)
{
// 1.1 清空信号集
sigemptyset(&pendings);
// 1.2 获取当前进程(谁调用,获取谁)的pending信号集
if(sigpending(&pendings) == 0)
{
// 1.3 打印当前进程的pending信号集
showPending(&pendings);
}
sleep(1);
++cnt;
if(cnt == 20)
{
cout << "解除对2号信号的block..." << endl;
sigset_t sigs;
sigemptyset(&sigs);
sigaddset(&sigs, 2);
sigprocmask(SIG_UNBLOCK, &sigs, nullptr);
}
}
return 0;
}
根据结果可以看到,我们在前面kill 一直到了7都没有任何反应,而等到了20s之后,就解除了对2号信号的屏蔽,2号信号就可以递达了,显示器上便出现了自定义捕捉动作的2号信号。
五.捕捉信号
1.如何实现信号的捕捉
进程处理信号,不是立即处理的,是在合适的时候。
那么这个合适的时候,是什么时候呢?
当 当前进程从内核态,切换会用户态的时候,进行信号的检测与处理。
OS是在内存中被加载的。
无论进程怎么切换,都可以找到内核的代码和数据,前提是你只要能够有权利访问。
当前进程如何具备权利,访问这个内核页表,乃至访问内核数据呢?
要进行身份切换。
进程如果是用户态的--只能访问用户级页表。
进程如果是内核态的--访问内核级和用户级的页表。
那么我们怎么知道自己是用户态还是内核态的呢?
CPU内部有对应的状态寄存器CR3,有比特位标识当前进程的状态,0:内核态;3:用户态
什么时候能切换到内核态呢?
① 系统调用的时候
② 时间片到了,进程间切换
③ 等等
内核态:可以访问所有的代码和数据(不是意味着它一定要访问所有的) -- 具备更高权限
用户态:只能访问自己的。
我们的进程,会无数次直接或者间接的访问系统级软硬件资源(管理者是OS),本质上,我们并没有自己去操作这些软硬件资源,而是必须通过OS->无数次的陷入内核(1.切换身份 2.切换页表)->调用内核的代码->完成访问的动作->结果返回给用户(1.切换身份 2.切换页表)->得到结果。
即使是像while(1)这样的代码也是会进行内核态和用户态的切换的。
因为它有自己的时间片->时间片到了的时候->内核态,更换内核级页表->保护上下文,执行调度算法->选择了新的进程->恢复新进程的上下文 ->用户态,更换成用户级页表->CPU执行的就是新进程的代码。
如果有一些系统调用,就会从用户态切换到内核态。
调用完该接口后,去识别信号,如果pending表为1,block表为0,会去进行自定义处理动作,就会从内核态切换到用户态(注意:这里一定要切换,虽然内核态可以完成用户态的操作,但是如果用户写的是一段恶意代码,那么因为内核态的权限过大,无论什么代码都会执行,就会导致OS受到恶意攻击,而切换成用户态就可以因为权限小而不会去执行该代码)。
把该动作由用户态执行完,就又会从用户态切换回内核态。
都处理完之后,就要返回了,而这返回又要从内核态切换回用户态。
简而言之,上面的过程就相当于数学中的无穷大符号∞
2.sigaction
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
作用:检查并且更改一个信号的动作。(设置对特定信号的特定处理的动作)
signo:对哪个信号实施信号自定义捕捉
act:要设置成什么动作
oact:老的动作,如果不要就设为nullptr;如果要就传入一个对象
void (*sa_handler) :对信号的处理方法,充当回调
sa_mask:需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
mask作用:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段
下面我们来使用这个sigaction:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "获取到一个信号,信号的编号是: " << signo << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while(true)
{
sleep(1);
}
return 0;
}
成功对2号信号进行了自定义捕捉。
下面改一下act.sa_handler:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "获取到一个信号,信号的编号是: " << signo << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_handler = SIG_IGN;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while(true)
{
sleep(1);
}
return 0;
}
这里无论怎么产生2号信号,都会忽略,SIG_IGN就是忽略该信号。
再改一下act.sa_handler:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "获取到一个信号,信号的编号是: " << signo << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_handler = SIG_DFL;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while(true)
{
sleep(1);
}
return 0;
}
这里产生2号信号就直接退出了,SIG_DFL就是采取默认动作。
上面说到当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,这里来测试一下:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "获取到一个信号,信号的编号是: " << signo << endl;
sigset_t pending;
// 增加handler信号的时间,永远都会处理2号信号
while(true)
{
cout << "." << endl;
sigpending(&pending);
for(int i = 1; i <= 31; ++i)
{
if(sigismember(&pending, i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while(true)
{
cout << "main running" << endl;
sleep(1);
}
return 0;
}
这里虽然pending中有了,但是被屏蔽了,无法发送信号。
再把3号信号加入进去:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "获取到一个信号,信号的编号是: " << signo << endl;
sigset_t pending;
// 增加handler信号的时间,永远都会处理2号信号
while(true)
{
cout << "." << endl;
sigpending(&pending);
for(int i = 1; i <= 31; ++i)
{
if(sigismember(&pending, i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
sigaction(2, &act, &oact);
while(true)
{
cout << "main running" << endl;
sleep(1);
}
return 0;
}
可以看到ctrl+\也因为被屏蔽无法退出了。
#include <iostream>
#include <vector>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 局限性,需要对所有的信号进行捕捉
std::vector<sighandler_t> handler;
void Handler2()
{
cout << "hello 2" << endl;
}
void Handler3()
{
cout << "hello 3" << endl;
}
void Handler4()
{
cout << "hello 4" << endl;
}
void Handler5()
{
cout << "hello 5" << endl;
}
void Handler(int signo)
{
switch (signo)
{
case 2:
Handler2();
break;
case 3:
Handler3();
break;
case 4:
Handler4();
break;
case 5:
Handler5();
break;
default:
break;
}
}
int main()
{
signal(2, Handler);
signal(3, Handler);
signal(4, Handler);
signal(5, Handler);
return 0;
}
这种写法可以让耦合性更低。
六.可重入函数
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的:
① 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
② 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
七.volatile
先看下面这段代码:
#include <stdio.h>
#include <signal.h>
int flags = 0;
void handler(int signo)
{
flags = 1;
printf("更改flags: 0->1\n");
}
int main()
{
signal(2, handler);
while(!flags);
printf("进程是正常退出的!\n");
return 0;
}
这里的结果和我们的预期是相同的,因为发送了2号信号,就将flags由0置1了。
但是这是在gcc编译器中,如果在别个编译器中就不一定是这种效果了。
某些编译器会把这个flags保存到CPU的寄存器中,去调用flags时也时去调用这个寄存器里的,而这里更改的时内存中的flags,不是寄存器中的,这时就与我们的预期不同。
这里我们更改一下编译器的编译层度:
这时就无法按我们预期进行了。
volatile的作用就是保持内存的可见性:
告诉编译器,不准对flags做任何优化
每次CPU计算的时候,都必须从内存中拿数据。
这里加上了volatile:
#include <stdio.h>
#include <signal.h>
// 保持内存的可见性
volatile int flags = 0;
void handler(int signo)
{
flags = 1;
printf("更改flags: 0->1\n");
}
int main()
{
signal(2, handler);
while(!flags);
printf("进程是正常退出的!\n");
return 0;
}
结果就又与预期相同了。
volatile和const也并不冲突,可以同时使用。
八.SIGCHLD
子进程退出的时候,不是默默的退出,会自动给父进程发送SIGCHLD
下面我们来验证一下:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "子进程退出了,我收到了信号: " << signo << "我是" << getpid() << endl;
}
int main()
{
signal(SIGCHLD, handler);
pid_t id = fork();
if(id == 0)
{
while(true)
{
cout << "我是子进程: " << getpid() << endl;
sleep(1);
}
exit(0);
}
// parent
while(true)
{
cout << "我是父进程: " << getpid() << endl;
sleep(1);
}
return 0;
}
可以看到,子进程被杀掉之后,确实向父进程发送了SIGCHLD信号。
其实,不只是子进程退出的时候会发送SIGCHLD信号,暂停和唤醒都会发送:
这里的 kill -19就是暂停,kill -18就是运行,都发送了SIGCHLD信号。
之前我们所写的父子进程的进程等待,父进程是要自己主动等待的:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 10;
while(cnt)
{
cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出,进入僵尸状态" << endl;
exit(0);
}
// 父进程,是要自己等待的
if(waitpid(id, nullptr, 0) > 0)
{
cout << "父进程等待子进程成功" << endl;
}
return 0;
}
这里父进程会一直等待子进程退出。
那么我们怎么才能让父进程不只是在等待子进程,可以让父进程去做别的事呢:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>
using namespace std;
void FreeChild(int signo)
{
assert(signo == SIGCHLD);
pid_t id = waitpid(-1, nullptr, 0);
if(id > 0)
{
cout << "父进程等待成功, child pid: " << id << endl;
}
}
int main()
{
signal(SIGCHLD, FreeChild);
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 10;
while(cnt)
{
cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出,进入僵尸状态" << endl;
exit(0);
}
while(true)
{
cout << "我是父进程,我正在运行: " << getpid() << endl;
sleep(1);
}
return 0;
}
按照上面的写法,是可以做到父进程不只是在等待子进程的,但是这种写法是有bug的,如果多个进程同时运行,就容易出现:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>
using namespace std;
void FreeChild(int signo)
{
assert(signo == SIGCHLD);
pid_t id = waitpid(-1, nullptr, 0);
if(id > 0)
{
cout << "父进程等待成功, child pid: " << id << endl;
}
}
int main()
{
signal(SIGCHLD, FreeChild);
for(int i = 0; i < 10; ++i)
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 10;
while(cnt)
{
cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出,进入僵尸状态" << endl;
exit(0);
}
}
while(true)
{
cout << "我是父进程,我正在运行: " << getpid() << endl;
sleep(1);
}
return 0;
}
这时,我们子进程正常是应该已经全部退出了的,但是看右边,会发现还有好几个子进程处于僵尸状态,因为在同时退出的时候,一下发送了很多个SIGCHLD信号,当正在处理某一个信号时,其它一些信号就可能会被丢掉,导致bug。
这里又会出现子进程没有都退出,但是父进程却不再运行的bug了。
因为waitpid在等待子进程的时候,把前7个信号全部回收了,当它回收第8个进程时,第8个进程并没有退出,这时调用waitpid会被阻塞住
这里我们可以给waitpid传入WNOHANG,然后再进行修改,即可处理:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>
using namespace std;
void FreeChild(int signo)
{
assert(signo == SIGCHLD);
while(true)
{
// 如果没有子进程了,waitpid 才会调用失败
// -1: 等待任意一个子进程
pid_t id = waitpid(-1, nullptr, WNOHANG);
if(id > 0)
{
cout << "父进程等待成功, child pid: " << id << endl;
}
else if(id == 0)
{
// 还有子进程,但是现在没退出
cout << "还有子进程,但是现在还没退出,父进程要去做自己的事情了" << endl;
break;
}
else
{
cout << "父进程等待所有子进程结束" << endl;
break;
}
}
}
int main()
{
signal(SIGCHLD, FreeChild);
for(int i = 0; i < 10; ++i)
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 0;
if(i < 7)
cnt = 5;
else
cnt = 20;
while(cnt)
{
cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出,进入僵尸状态" << endl;
exit(0);
}
}
while(true)
{
cout << "我是父进程,我正在运行: " << getpid() << endl;
sleep(1);
}
return 0;
}
如果我们不想麻烦,不想获取状态,也不想自己去管,想让子进程退出时直接退出僵尸进程,就可以这么写:
signal(SIGCHLD, SIG_IGN);
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>
using namespace std;
void FreeChild(int signo)
{
assert(signo == SIGCHLD);
while(true)
{
// 如果没有子进程了,waitpid 才会调用失败
// -1: 等待任意一个子进程
pid_t id = waitpid(-1, nullptr, WNOHANG);
if(id > 0)
{
cout << "父进程等待成功, child pid: " << id << endl;
}
else if(id == 0)
{
// 还有子进程,但是现在没退出
cout << "还有子进程,但是现在还没退出,父进程要去做自己的事情了" << endl;
break;
}
else
{
cout << "父进程等待所有子进程结束" << endl;
break;
}
}
}
int main()
{
signal(SIGCHLD, SIG_IGN);
for(int i = 0; i < 10; ++i)
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 0;
if(i < 7)
cnt = 5;
else
cnt = 20;
while(cnt)
{
cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
sleep(1);
}
cout << "子进程退出,进入僵尸状态" << endl;
exit(0);
}
}
while(true)
{
cout << "我是父进程,我正在运行: " << getpid() << endl;
sleep(1);
}
return 0;
}
既然子进程退出的时候,默认的信号处理就是忽略,那么我们调用signal/sigaction SIG_IGN的意思在哪呢?
SIG_IGN手动设置还是与默认的不一样的,默认的可能还会给父进程发送信号,但是手动设置,让子进程退出时,就不会给父进程发送信号了,并且会自动释放。