Haohao Notes

DREAM OF TECHNICAL ACHIEVEMENT

0%

Linux系统编程-信号

信号概念

信号其实我们也见过,当我们在shell上写出一个死循环退不出来的时候,只需要一个组合键,ctrl+c,就可以解决了,这就是一个信号,但是真正的过程并不是那么简单的。

  1. 当用户按下这一对组合键时,这个键盘输入会产生一个硬件中断,如果CPU正在执行这个进程的代码时,则该进程的用户代码先暂停执行,用户从用户态切换到内核态处理硬件中断

  2. 终端驱动程序将这一对组合键翻译成一个SIGINT(ctrl+c)信号记在该进程的PCB中(也就是发送了一个SIGINT信号给该进程)

  3. 当某个时刻要从内核态回到该进程的用户·空间代码继续执行之前,首先处理PCB中的信号,发现有一个SIGINT信号需要处理,而这个信号的默认处理方式是终止进程,所以直接终止进程,不再返回用户空间执行代码。

  4. shell可以同时运行一个前台进程和多个后台进程,只有前台进程才能收到ctrl+c这种组合键产生的信号

  5. 前台进程在 运行过程中用户可以随时按下ctrl+c产生一个信号也就是说前台进程的用户空间代码执行到任意一个时刻都可能接收到SIGINT信号而终止,所以信号对于进程的控制流来说是异步的。

注意:ctrl+c只能终止前台进程。一个命令可以加&可以将进程放在后台执行,这样shell就不必等待进程结束就可以接收新的命令,启动新的进程

信号介绍

在bash上执行命令kill -l便可看到系统定义的所有信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1
36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5
40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9
44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13
52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5
60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1
64) SIGRTMAX

列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

每个信号都有一个编号和一个宏定义名称,这些宏定义都可以在signal.h中找到,在man手册中还可以找到各种信号的详细信息

man 7 signal

下面我们对编号小于SIGRTMIN的信号进行讨论。

SIGHUP

1) SIGHUP
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。

登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

SIGINT

2) SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

SIGQUIT

3) SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

SIGILL

4) SIGILL
执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

SIGTRAP

5) SIGTRAP
由断点指令或其它trap指令产生. 由debugger使用。

SIGABRT

6) SIGABRT
调用abort函数生成的信号。

SIGBUS

7) SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

SIGFPE

8) SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

SIGKILL

9) SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

SIGUSR1

10) SIGUSR1
留给用户使用

SIGSEGV

11) SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

SIGUSR2

12) SIGUSR2
留给用户使用

SIGPIPE

13) SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

SIGALRM

14) SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

SIGTERM

15) SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

SIGCHLD

17) SIGCHLD
子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。

SIGCONT

18) SIGCONT
让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

SIGSTOP

19) SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

SIGTSTP

20) SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

SIGTTIN

21) SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

SIGTTOU

22) SIGTTOU
类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

SIGURG

23) SIGURG
有”紧急”数据或out-of-band数据到达socket时产生.

SIGXCPU

24) SIGXCPU
超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

SIGXFSZ

25) SIGXFSZ
当进程企图扩大文件以至于超过文件大小资源限制。

SIGVTALRM

26) SIGVTALRM
虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

SIGPROF

27) SIGPROF
类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

SIGWINCH

28) SIGWINCH
窗口大小改变时发出.

SIGIO

29) SIGIO
文件描述符准备就绪, 可以开始进行输入/输出操作.

SIGPWR

30) SIGPWR
Power failure

SIGSYS

31) SIGSYS
非法的系统调用。

总结

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

产生信号的方式

  1. 通过键盘的组合键产生,比如ctrl+c产生SIGINT信号,ctrl+\产生SIGQUIT信号,ctrl+z产生SIGTSTP信号

  2. 硬件异常产生信号,这些条件由硬件检测并通知内核,然后内核向进程发送适当的信号,比如执行了除以零的指令,进程访问了非法内存地址,cpu的运算单元都会产生异常,内核将这个异常解释成一个个信号发送给进程

  3. 一个进程调用kill(2)函数可以发送信号给另一个进程。可以用kill(1)发送信号给某一个进程kill(1)也是用kill(2)实现的如果不清楚指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程,当内核检测到软件条件发生时也可以通过信号通知进程例如闹钟超时,会产生SIGALRM信号,向读端已经关闭的管道文件写数据时产生SIGPIPE信号,如果不想按照默认动作处理信号,用户可以调用sigaction(2)函数告诉内核如何处理某种信号

  4. 软件条件产生

信号常见处理方式

  1. 忽略该信号

  2. 执行信号的默认处理动作

  3. 提供一个信号处理函数,要求内核在处理信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个异常

信号产生具体过程

通过终端按键来产生信号

SIGINT (ctrl+c)的默认处理动作是终止进程,SIGQUIT(trl+/)的默认处理动作是终止进程并Core Dump,我们在Linux环境下来验证一下,先来了解一下什么是Core Dump.

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存在磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有BUG,比如非法访问内存导致段错误,事后可以用调试器检查core文件以查清楚错误原因,这叫做事后调试,一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中),默认是不允许改变这个限制,允许产生core文件。首先用ulimit命令来改变shell进程的Resource Limit,允许core文件最大为1024k

ulimit -c 1024

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@centos-linux ~]# ulimit -c 1024
[root@centos-linux ~]# ulimit -a
core file size (blocks, -c) 1024
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7240
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7240
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

test.c 举例一个死循环程序:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int main(void){
printf("get pid = %d \n",getpid());
while (1);
return 0;
}

编译并执行程序:

1
2
3
4
5
[root@centos-linux code]# gcc -o test test.c
[root@centos-linux code]# ./test
get pid = 10427
^\Quit (core dumped)
[root@centos-linux code]#

看到的现象是先打印出pid然后一直在死循环,按下组合键ctrl+\后退出并提示core dumped

test程序也会core dump的原因是我们先修改了shell的Resource Limit值,而test进程是由shell产生的所以test进程的PCB也是由shell复制而来,所以test进程和shell就具有相同的Resource Limit值,所以就会产生core dump了。

我们使用ls目录查看一下目录多出来的文件:

1
2
[root@centos-linux code]# ls
core.10427 test test.c

我们来使用core文件,通过gdb调试.core-file core.10427

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@centos-linux code]# gdb test
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-115.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/code/test...(no debugging symbols found)...done.
(gdb) core-file core.10427
[New LWP 10427]
Core was generated by `./test'.
Program terminated with signal 3, Quit.
#0 0x0000000000400597 in main ()
Missing separate debuginfos, use: debuginfo-install glibc-2.17-292.el7.x86_64
(gdb)

调用系统函数来向进程发信号

首先在后台运行一个死循环程序,然后用kill 命令给它发信号

1
2
3
4
5
6
7
8
[root@centos-linux code]# ./test &           
[1] 10549
[root@centos-linux code]# get pid = 10549

[root@centos-linux code]# kill -SIGSEGV 10549
[root@centos-linux code]# ls
core.10427 core.10549 test test.c
[1]+ Segmentation fault (core dumped) ./test

说明:我们之所以要多按一次回车,是因为10549进程终止掉之前已经回到了shell提示符等待用户输入下一条命令,shell不希望错误信息和用户命令混在一起,所以先等用户输入后再显示

指定发送某种信号的kill命令可以有多种,上面的命令还可以写成kill -11 10549,11是信号SIGSEGV信号的编号。以往遇到的段错误都是由非法内存访问引起的,而这个程序本来也没错误,给它发送一个SIGSEGV信号也能引起段错误

kill命令是由kill函数实现的,kill函数可以给一个指定的进程发送指定的信号,raise函数可以给当前进程发送指定的信号(自己给自己发信号)

软件条件产生的信号

软件条件产生的信号我们已经见过一种,就在我们学习进程间通信的时候,信号SIGPIPE就被我们介绍过,我们在这里不再多加介绍,我们接下来要介绍一种有趣的信号和产生这种信号的函数,我们可以想想,有一种声音我们每个人最不想听到的一种声音是什么,当然是每天的闹钟声了,我们介绍的这个信号就和现实中的闹钟很像,今天要介绍的信号就是SIGALRM信号,以及产生这种信号的函数alarm.

alarm(设置信号传送闹钟)

描述 内容
表头文件 #include<unistd.h>
定义函数 unsigned int alarm(unsigned int seconds);
函数说明 alarm()用来设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程。如果参数seconds 为0,则之前设置的闹钟会被取消,并将剩下的时间返回。
返回值 返回之前闹钟的剩余秒数,如果之前未设闹钟则返回0。

范例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int main(void){
int count = 1;
alarm(1);
for (;1;count++) {
printf("count is %d\n",count);
}
return 0;
}

执行结果(由于执行结果太长我就贴最后重要部分,详细的结果可以自己试验):

1
2
3
4
count is 184693
count is 184694
count is 184695Alarm clock
[root@centos-linux code]#

代码中设置一个闹钟和一个计数器,在闹钟响前,count一直++,并输出count值直到闹钟响,接收到SIGALRM信号才结束进程

kill

描述 内容
表头文件 #include<sys/types.h> #include<signal.h>
定义函数 int kill(pid_t pid,int sig);
函数说明 kill()可以用来送参数sig指定的信号给参数pid指定的进程。参数pid有几种情况:pid>0 将信号传给进程识别码为pid 的进程。pid=0 将信号传给和目前进程相同进程组的所有进程pid=-1 将信号广播传送给系统内所有的进程pid<0 将信号传给进程组识别码为pid绝对值的所有进程,参数sig代表的信号编号可参考上面信号介绍
返回值 执行成功则返回0,如果有错误则返回-1。
错误代码 EINVAL 参数sig 不合法 ESRCH 参数pid 所指定的进程或进程组不存在 EPERM 权限不够无法传送信号给指定进程

raise

描述 内容
表头文件 #include<sys/types.h> #include<signal.h>
定义函数 int raise(int sig);
函数说明 用于向进程自身发送信号。
返回值 成功返回0,失败返回-1。

abort

描述 内容
表头文件 #include<signal.h>
定义函数 int abort(void);
函数说明 中止程序执行,直接从调用的地方跳出。
返回值 该函数不返回任何值。

阻塞信号

实际执行信号的动作叫做信号递达

信号从产生到递达过程中的状态叫做信号未决,

进程可以选择阻塞某个信号

被阻塞的信号将处于未决状态,直到进程解除对信号的阻塞,才执行递达的动作

注意:阻塞和忽略是不同的,只要信号被阻塞就不会被递达,而忽略是在递达之后所选择的一种处理动作

信号在内核中的表示示意图
上图来源: Linux C编程一站式学习 第33章信号

解释说明:每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作,信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才会清除该标志。如果进程解除对某信号的阻塞之前该信号产生过很多次,将如何处理?

OPSIX.1允许递达该信号一次或多次。Linux是这样实现的,常规信号在递达之前产生多次只记一次,而实时信号在递达之前产生多次可以依次放在一个队列里。在这里,不讨论实时信号。

sigset_t

由上图可知每个信号都只有一个bit的未决状态,不是0就是1,阻塞标志也是一样。因此阻塞和未决可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以用来表示信号的有效和无效状态,阻塞信号集也叫作当前进程的信号屏蔽字,这里的屏蔽应理解为阻塞而不是忽略。

信号集操作函数

以下就是我们常用的信号集操作函数:

sigemptyset (初始化信号集)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigemptyset(sigset_t *set);
函数说明 sigemptyset()用来将参数set信号集初始化并清空。
返回值 执行成功则返回0,如果有错误则返回-1。
错误代码 EFAULT 参数set指针地址无法存取

sigfillset(将所有信号加入至信号集)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigfillset(sigset_t * set);
函数说明 sigfillset()用来将参数set信号集初始化,然后把所有的信号加入到此信号集里。
返回值 执行成功则返回0,如果有错误则返回-1。
附加说明 EFAULT 参数set指针地址无法存取

sigaddset(增加一个信号至信号集)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigaddset(sigset_t *set,int signum);
函数说明 sigaddset()用来将参数signum 代表的信号加入至参数set 信号集里。
返回值 执行成功则返回0,如果有错误则返回-1。
错误代码 EFAULT 参数set指针地址无法存取 EINVAL 参数signum非合法的信号编号

sigdelset(从信号集里删除一个信号)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigdelset(sigset_t * set,int signum);
函数说明 sigdelset()用来将参数signum代表的信号从参数set信号集里删除。
返回值 执行成功则返回0,如果有错误则返回-1。
错误代码 EFAULT 参数set指针地址无法存取 EINVAL 参数signum非合法的信号编号

sigismember(测试某个信号是否已加入至信号集里)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigismember(const sigset_t *set,int signum);
函数说明 sigismember()用来测试参数signum 代表的信号是否已加入至参数set信号集里。如果信号集里已有该信号则返回1,否则返回0。
返回值 信号集已有该信号则返回1,没有则返回0。如果有错误则返回-1。
错误代码 EFAULT 参数set指针地址无法存取 EINVAL 参数signum 非合法的信号编号

sigprocmask(查询或设置信号遮罩)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigprocmask(int how,const sigset_t *set,sigset_t * oldset);
函数说明 sigprocmask()可以用来改变目前的信号遮罩,其操作依参数how来决定
SIG_BLOCK 新的信号遮罩由目前的信号遮罩和参数set 指定的信号遮罩作联集
SIG_UNBLOCK 将目前的信号遮罩删除掉参数set指定的信号遮罩
SIG_SETMASK 将目前的信号遮罩设成参数set指定的信号遮罩。
如果参数oldset不是NULL指针,那么目前的信号遮罩会由此指针返回。
返回值 执行成功则返回0,如果有错误则返回-1。
错误代码 EFAULT 参数set,oldset指针地址无法存取。EINTR 此调用被中断

注意:在使用sigset_t类型的变量之前一定要用sigemptyset函数和sigfillset函数初始化是信号集处于确定的状态,初始化之后就可以使用sigaddset函数和sigdelset函数在该信号集中增加或者删除有效信号。

sigpending(查询被搁置的信号)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigpending(sigset_t *set);
函数说明 sigpending()会将被搁置的信号集合由参数set指针返回。
返回值执 行成功则返回0,如果有错误则返回-1。
错误代码 EFAULT 参数set指针地址无法存取 EINTR 此调用被中断。

案例(打印当前未决信号集)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int showsigset(sigset_t *sigset){
int i = 0;
for (;i <= 32;i++) {
if (sigismember(sigset, i)){
printf("%d", 1);
}else{
printf("%d", 0);
}
}
puts("");
}
int main(void){
sigset_t s,p;
sigemptyset(&s);
sigaddset(&s,SIGINT);
sigprocmask(SIG_BLOCK,&s,NULL);
while (1){
sigpending(&p);
showsigset(&p);
sleep(2);
}
return 0;
}

结果:

1
2
3
4
5
[root@centos-linux code]# ./test            
10000000000000000000000000000000
10000000000000000000000000000000
^\Quit (core dumped)
[root@centos-linux code]#

捕捉信号

捕捉信号

内核如何实现信号的捕捉呢?

  1. 首先在用户正常执行主控制流程由于中断,异常或系统调用而直接进入内核态进行处理处理这种异常,
  2. 内核处理完异常就准备返回用户态了,在这之前会看当前进程有没有可以抵达的信号,如果有就对可递达的信号进行处理,
  3. 如果信号的处理函数是用户自定义的就返回用户态去执行用户自定义的信号处理函数
  4. 信号处理函数执行完之后,会调用一个特殊的系统调用函数sigreturn而再一次进入内核态,执行这个系统调用
  5. 这个系统调用完成之后,就会返回主控制流程被中断的地方继续执行下面的代码
  6. 执行主控制流程的时候如果再次遇到异常、中断或系统调用就继续回到1,继续执行下面的流程

捕捉信号的函数

sigaction(查询或设置信号处理方式)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);
函数说明 sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。
返回值 执行成功则返回0,如果有错误则返回-1。
错误代码 EINVAL 参数signum 不合法, 或是企图拦截SIGKILL/SIGSTOPSIGKILL信号 EFAULT 参数act,oldact指针地址无法存取。EINTR 此调用被中断

如参数结构sigaction定义如下

1
2
3
4
5
6
7
struct sigaction
{
void (*sa_handler) (int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer) (void);
}

  • sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数,其他意义请参考signal()。
  • sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号搁置。
  • sa_restorer 此参数没有使用。
  • sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。
    OR 运算(|)组合
    A_NOCLDSTOP : 如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
    SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式改为系统预设的方式。
    SA_RESTART:被信号中断的系统调用会自行重启
    SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来。
    如果参数oldact不是NULL指针,则原来的信号处理方式会由此结构sigaction 返回。

pause(让进程暂停直到信号出现)

描述 内容
表头文件 #include<unistd.h>
定义函数 int pause(void);
函数说明 pause()会令目前的进程暂停(进入睡眠状态),直到被信号(signal)所中断。
返回值 只返回-1。
错误代码 EINTR 有信号到达中断了此函数。

案例(完成自己的sleep函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void sig_alrm(int signo){
printf("signo = %d\n",signo);
}

unsigned int mysleep(unsigned int nsecs){
struct sigaction new,old;
unsigned int unslept = 0;
new.sa_handler = sig_alrm;
// 首先进程初始化,以保证信号有一个确定的状态
sigemptyset(&new.sa_mask);
new.sa_flags = 0;
// 注册信号处理函数
sigaction(SIGALRM,&new,&old);
// 设定一个闹钟
alarm(nsecs);
// 将进程挂起
pause();
// 清空闹钟
unslept = alarm(0);
// 信号继续执行默认动作
sigaction(SIGALRM,&old,NULL);
return unslept;
}

int main(void){
while (1){
mysleep(5);
fflush(stdout);
printf("已经过了5秒咯\n");
}
return 0;
}

输出结果:

1
2
3
4
5
6
[root@centos-linux code]# gcc -o test test.c
[root@centos-linux code]# ./test
signo = 14
已经过了5秒咯
signo = 14
已经过了5秒咯

竞态条件与sigsuspend函数

我们再来考虑一下以前写的mysleep函数,我们使用alarm函数设定闹钟之后调用pause函数进行等待,可是SIGALRM信号已经处理完了还在等什么呢?

出现这个问题的根本原因是系统运行的时序并不像我们写程序时想得那样,虽然alarm函数设定闹钟后,后面紧跟的是pause函数,但是不能保证pause函数一定会在nsecs秒之内被调用。由于异步事件在任何时候都可能发生(异步指出现更高优先级的进程),如果我们写程序的时候考虑不周,就有可能会产生时序问题而导致错误,这就叫做竞态条件。

解决这种问题一般有两种思路,一种是在调用pause之前屏蔽SIGALRM信号使它不能提前递达就好了

我们将代码执行过程分为四步

  1. 屏蔽SIGALRM信号
  2. alarm(nsecs)设定闹钟
  3. 解除屏蔽
  4. pause();

这样的话SIGALRM信号也可能在解除屏蔽和调用pause之间的时间间隔内递达

我们又可以设想将解除信号屏蔽放在pause()函数调用之后,执行过程就变为:

  1. 屏蔽SIGALRM信号
  2. alarm(nsecs)设置闹钟
  3. pause();
  4. 解除屏蔽

这样更不行还没有解除屏蔽就调用pause,pause根本不可能等到SIGALRM信号,经过这两步的分析我,我们最想得到的就是将解除屏蔽和等待放在一起,让他们中间不要间断的执行,也就是这两条代码的执行是原子的。sigsuspend函数的功能就是这个

对时序要求严格的都应该调用sigsuspend函数而不是pause

sigsuspend (屏蔽新的信号,原来屏蔽的信号失效)

描述 内容
表头文件 #include<signal.h>
定义函数 int sigsuspend(const sigset_t *mask);
函数说明 进程执行到sigsuspend时,sigsuspend并不会立刻返回,进程处于TASK_INTERRUPTIBLE状态并立刻放弃CPU,等待UNBLOCK(mask之外的)信号的唤醒。进程在接收到UNBLOCK(mask之外)信号后,调用处理函数,然后还原信号集,sigsuspend返回,进程恢复执行。
返回值 sigsuspend返回后将恢复调用之前的的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR.

SIGCHILD信号

前面我们知道清除僵尸进程的方法就是使用wait和waitpid函数父进程可以阻塞等待子进程结束,也可以非阻塞的查询是否有子进程需要被清理(轮询),第一种方式父进程阻塞就不能做其他事情了,第二种,父进程不断去询问,代码实现比较复杂

其实子进程在终止时会给父进程发一个SIGCHILD信号,默认处理动作是忽略,用户可以自定义SIGCHILD的处理函数,这样父进程就可以专心处理自己的事情,不用关心子进程了,子进程退出时会通知父进程,父进程在信号处理函数中调用wait来处理子进程就可以了

补充:想不产生僵尸进程还有另外一种方法:父进程调用sigaction将SIGCHILD处理动作置为SIG_IGN这样fork出的子进程在终止时会自动清理掉也不会通知父进程系统默认的忽略和用户自定义的忽略一般是没有区别的,但这是一个特例,对于Linux可以用,在其他unix系统上不一定能用。