linux_api之信号
本片索引:
1、引言
2、信号
3.程序启动
4、signal函数
5、系统调用的中断和系统调用的重启(了解)
6、可再入与不可再入函数(了解)
7、kill函数和raise函数
8、alarm函数和pause函数
9、信号的发送、接收和处理的过程
10、信号集设置函数和sigprocmask函数
11、sigpending函数
12、sigaction函数
13、sigsuspend函数
14、abort函数
15、sleep函数
1、引言
信号是一种软件中断,与之相对应的就是硬件中断,区别是,软件中断是由软件代码触发的中断,而硬件中断是由硬件直接产生的电信号触发的中断,说到中断那么一定是异步的,所谓的异步就是中断信号的产生是随机的,对于中断信号的接受者来说,接受到中断信号的时刻自然也就是随机的。
当然与异步相对应的概念就是同步,所谓同步就是接收者知道在何时能够收到信号,当然本章所涉及的信号都是异步的,这里只是简单的解释一下什么是同步的概念。
2、信号
2.1、概念:所谓的信号就是通信内容受限制的一种异步的通信方式。
2.2、信号的命名方式
每个信号都有一个名字,这些名字都是以SIG开头(signal的缩写),如SIGABRT是夭折信号,当调用abort函数时会产生这个信号。当调用alarm函数时,由alarm设置的超时时间到后,会产生SIGALRM信号。
这些信号宏都被定义在了signal.h这些头文件中,每一个宏(正整数)代表一个信号,这里需要特别说明,没有编号为0(空信号)的信号,讲到kill函数时,0信号有特殊用途。
2.3、哪些情况下会产生信号
1)在终端按下按键时产生信号
例如按下crtl+c,会产生SIGINT信号,这个信号常常用于终止一个进程。实际上该信号可以被映射到终端上的任意一个字符(按键)上。
2)硬件异常产生信号
如除数为0,或者对内存无效的存储访问(段错误)等等,这些通常是由硬件先检测到并将其通知内核,然后内核会为该条件发生时正在运行的进程发送适当的信号,对于无效存储访问会产生一个SIGSEGV信号。
3)用户使用kill命令向其它的进程发送某个信号
kill命令是kill函数的命令形式,常用于向某进程发送信号。
4)当检测到某种软件条件已经发生,也会产生某个信号以通知某个进程。
例如:
a)、当网络上传来非固定波特率的数据时(如带外数据),产生SIGURG信号。
b)、写一个读端已经被关闭了的管道时,产生SIGPIPE信号。
c)、进程设置的闹钟超时(与alarm函数相关),产生SIGALRM信号。
2.4、信号的处理方式
在引言中我们已经说过,信号是异步通信机制,对于某个进程而言某个信号的产生是随机的,该进程必须告诉内核,当信号发生时需要按照什么样的方式处理。实际上处理信号的方式有如下三种:
1)忽略此信号
除了两种信号(SIGKILL和SIGSTOP)不能被忽略外,其它的的信号都可被忽略,这两个信号不能被忽略的原因是:它们提供一种终极的终止或停止进程的可靠方法,这是一种终极裁判权,如果这两个信号都被忽略了的话,某个进程跑飞后就没有办法终止或停止这个进程。
另外忽略某些硬件产生的信号被认为是不可取的,如我们如果忽略非法存储访问或除以0等硬件产生的信号的话,进程状态未定义的(无法确定的状态)。
2)捕获信号
如果某个进程被通知某个信号发生了,但是想要捕获这个信号的话,该进程就必须向内核注册一个捕获函数,当相应的信号发生时,捕获函数就会被调用并执行希望对这个事件的处理。
3)执行系统的默认操作
其实我们内核为每个信号在发生时都规定了一个默认的操作,如果我们不捕获也不忽略的话,当这个信号发生时,进程会按照默认方式去处理这些发生的信号,当然对于绝大多数信号而言,其默认的处理方式都是终止接收到该信号的进程或者忽略此信号。
2.5、内核定义的信号
1)、所有信号的清单
运行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
16) SIGSTKFLT 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
一共64个信号,编号从1~4,每个编号就是一个整形数,其中1~34是常用的32个编号(32、33没有),35~64是增加的信号,我们下面将会对1~34信号做一个较为详细的介绍。
2)、常用的32(1~34,但是没有32,33信号)个信号
信号名 字(宏名) |
信号 编号 |
说明 |
缺省动作(默认动作) |
*SIGABRT |
6 |
异常终止,调abort函数是产生 |
终止,产生w/core文件 |
*SIGALRM |
14 |
超时,调用alarm函数时产生 |
终止 |
*SIGBUS |
7 |
硬件故障 |
终止,产生w/core文件 |
*SIGCHLD |
17 |
子进程状态改变,常用于作业控制 |
忽略 |
SIGCONT |
18 |
使暂停进程继续,常用语作业控制 |
继续/忽略 |
SIGEMT |
15 |
硬件故障 |
终止,产生w/core文件 |
SIGFPE |
8 |
算数异常 |
终止,产生w/core文件 |
SIGHUP |
1 |
连接断开 |
终止 |
SIGILL |
4 |
非法硬件指令 |
终止,产生w/core文件 |
*SIGINT |
2 |
中断中断符,常用语终止失控进程 |
终止 |
*SIGIO |
29 |
异步通知信号 |
终止/忽略 |
SIGTOT |
17 |
硬件故障 |
终止 |
*SIGKILL |
9 |
无条件终止一个进程,不可以被捕获或忽略 |
继续/忽略 |
*SIGPIPE |
13 |
写读端被关闭的管道 |
终止 |
*SIGPOLL |
8 |
轮询事件,涉及POLL机制 |
终止 |
SIGPROF |
27 |
梗概时间到时,涉及setitimer函数 |
终止 |
SIGPWR |
30 |
电源失效/再启动 |
忽略 |
*SIGQUIT |
3 |
终端推出符 |
终止,产生w/core文件 |
*SIGSEGV |
11 |
无效存储访问 |
终止,产生w/core文件 |
SIGSTOP |
19 |
停止 |
暂停进程 |
SIGSYS |
31 |
无效系统调用 |
终止,产生w/core文件 |
*SIGTERM |
15 |
终止 |
终止 |
SIGTRAP |
5 |
硬件故障 |
终止,产生w/core文件 |
SIGSTP |
20 |
终端挂起,常用于作业控制 |
停止进程 |
SIGTTIN |
21 |
后台控制tty读,常用语作业控制 |
停止进程 |
SIGTTOU |
22 |
后台向TTY写,常用语作业控制 |
停止进程 |
*SIGURG |
23 |
紧急情况,如带外数据需用到此信号 |
忽略 |
*SIGUSR1 |
10 |
用户自定义信号1 |
终止 |
*SIGUSR2 |
12 |
用户自定义信号2 |
终止 |
SIGVTALRM |
26 |
虚拟闹钟时间,涉及setitimer函数 |
终止 |
SIGWINCH |
28 |
终端窗口大小改变时产生该信号 |
忽略 |
SIGXCPU |
24 |
超过CPU限制,涉及setrlimit函数 |
终止,产生w/core文件 |
SIGXFSZ |
25 |
超过文件长度时产生此信号,涉及setrlimit函数 |
终止,产生w/core文件 |
以上标注有(*)的是我们必须了解的常用信号。
3)、有关core文件
从前面默认的信号处理方式中可以看到,有写信号产生时会同时产生core文件(核心文件),这个文件被产生后,一般是被存在进程的当前路径下,core文件的权限一般是rw-r--r--。
core文件中复制了该进程终止时的内存映像,记录了进程终止时的内存状态,但存在下列条件时,core文件将不被产生。
a)对配置文件做了相应的设置,临时地或永久地取消了core文件的产生。
b)进程设置了用户ID设置位,并且当前用户并非该程序文件的所有者,换句话说就 是实际用户ID不等于有效用户ID的情况下,不会产生core文件。
c)进程设置了组ID设置位,并且当前用户并非该程序文件的组所有者,换句话说就 是实际组ID不等于有效组ID的情况下,不会产生core文件。
d)内存映像太大,core文件无法全部记录下所有的进程内存映像信息时,不会产生
core文件。
如果想对core文件有更多的了解,请查阅相关资料或者本文件所带的附件资料。
4)、32个常用信号的功能详述
• SIGABRT:调用abort函数时产生此信号。进程异常终止。
• SIGALRM:超过用alarm函数设置的时间时产生此信号。详细情况见10.10节。若由setitimer
(2)函数(此函数的讲解略)设置的间隔时间已经过时,那么也产生此信号。
• SIGBUS:指示一个实现定义的硬件故障。
• SIGCHLD:在一个进程终止或停止时,SIGCHLD信号被送给其父进程。按系统默认, 将 忽略此信号。如果父进程希望了解其子进程的这种状态改变,则应捕捉此信号。信号捕捉 函数中通常要调用w a i t函数以取得子进程I D和其终止状态。
系统V的早期版本有一个名为SIGCLD (无H )的类似信号。这一信号具有非标准的语义,SVR2的手册页警告在新的程序中尽量不要使用这种信号。应用程序应当使用标准的SIGCHLD信号。10.7节将讨论这两个信号。
•SIGCONT:此作业控制信号送给需要继续运行的处于停止状态的进程。如果接收到此信
号的进程处于停止状态,则系统默认动作是使该进程继续运行,否则默认动作是忽略此信号。
例如,vi编辑程序在捕捉到此信号后,重新绘制终端屏幕。关于进一步的情况见10.20节。
•SIGEMT:指示一个实现定义的硬件故障。EMT这一名字来自PDP-11的emulator trap 指令。
•SIGFPE:此信号表示一个算术运算异常,例如除以0,浮点溢出等。
•SIGHUP:如果终端界面检测到一个连接断开,则将此信号送给与该终端相关的控制进程(对
话期首进程)。见图9 - 11,此信号被送给session结构中sleader字段所指向的进程。仅当终
端的CLOCAL标志没有设置时,在上述条件下才产生此信号。(如果所连接的终端是本地的,才
设置该终端的CLOCAL标志。它告诉终端驱动程序忽略所有调制解调器的状态行。第11章将说
明如何设置此标志。)注意,接到此信号的对话期首进程可能在后台,作为一个例子见
图9-7。这区别于通常由终端产生的信号(中断、退出和挂起),这些信号总是传递给前台进程 组。如果对话期前进程终止,则也产生此信号。在这种情况,此信号送给前台进程组中的每 一个进程。通常用此信号通知精灵进程(见第1 3章)以再读它们的配置文件。选用SIGHUP
的理由是,因为一个精灵进程不会有一个控制终端,而且通常决不会接收到这种信号。
•SIGILL:此信号指示进程已执行一条非法硬件指令。4.3BSD由abort函数产生此信号。SIGABRT 现在被用于此。
•SIGINFO:这是一种4.3+BSD信号,当用户按状态键(一般采用Ctrl-T)时,终端驱动程序产
生此信号并送至前台进程组中的每一个进程(见图9-8)。此信号通常造成在终端上显示前台
进程组中各进程的状态信息。
•SIGINT:当用户按中断键(一般采用DELETE或Ctrl-C)时,终端驱动程序产生此信号并送至
前台进程组中的每一个进程(见图9-8)。当一个进程在运行时失控,特别是它正在屏幕上产生
大量不需要的输出时,常用此信号终止它。
•SIGIO:此信号指示一个异步I/O事件。在12.6.2节中将对此进行讨论。在表10-1中,对SIGIO 的系统默认动作是终止或忽略。不幸的是,这依赖于系统。在SVR4中,SIGIO与SIGPOLL相同, 其默认动作是终止此进程。在4.3+BSD中(此信号起源于4.2BSD),其默认动作是忽略。
•SIGIOT:这指示一个实现定义的硬件故障。IOT这个名字来自于PDP-11对于输入/输出 TRAP(input/output TRAP)指令的缩写。系统V的早期版本,由abort函数产生此信号。SIGABRT 现在被用于此。
•SIGKILL:这是两个不能被捕捉或忽略信号中的一个。它向系统管理员提供了一种可以杀死
任一进程的可靠方法。
•SIGPIPE:如果在读进程已终止时写管道,则产生此信号。14.2节将说明管道。当套接口的 一端已经终止时,若进程写该套接口也产生此信号。
•SIGPOLL:这是一种SVR4信号,当在一个可轮询设备上发生一特定事件时产生此信号。12.5.2
节将说明poll函数和此信号。它与4.3+BSD的SIGIO和SIGURG信号接近。
•SIGPROF:当setitimer ( 2 )函数设置的梗概统计间隔时间已经超过时产生此信号。
•SIGPWR:这是一种SVR4信号,它依赖于系统。它主要用于具有不间断电源(UPS)的系统上。 如果电源失效,则UPS起作用,而且通常软件会接到通知。在这种情况下,系统依靠蓄电池电 源继续运行,所以无须作任何处理。但是如果蓄电池也将不能支持工作,则软件通常会再次 接到通知,此时,它在15~3 0秒内使系统各部分都停止运行。此时应当传递SIGPWR信号。在
大多数系统中使接到蓄电池电压过低的进程将信号SIGPWR发送给init进程,然后由init处理
停机操作。很多系统V的init实现在inittab文件中提供了两个记录项用于此种目的; powerfail以及powerwait。目前已能获得低价格的UPS系统,它用RS-232串行连接能够很容易
地将蓄电池电压过低的条件通知系统,于是这种信号也就更加重要了。
•SIGQUIT:当用户在终端上按退出键(一般采用Ctrl-\)时,产生此信号,并送至前台进程
组中的所有进程(见图9-8)。此信号不仅终止前台进程组(如SIGINT所做的那样),同时产生
一个core文件。
•SIGSEGV:指示进程进行了一次无效的存储访问。名字SEGV表示“段违例(segmentation violation)”。
•SIGSTOP:这是一个作业控制信号,它停止一个进程。它类似于交互停止信号(SIGTSTP),但
是SIGSTOP不能被捕捉或忽略。
•SIGSYS:指示一个无效的系统调用。由于某种未知原因,进程执行了一条系统调用指令,但
其指示系统调用类型的参数却是无效的。
•SIGTERM:这是由kill(1)命令发送的系统默认终止信号。
•SIGTRAP:指示一个实现定义的硬件故障。此信号名来自于PDP-11的TRAP指令。
•SIGTSTP:交互停止信号,当用户在终端上按挂起键(一般采用Ctrl-Z)时,终端驱动程序
产生此信号。
•SIGTTIN 当一个后台进程组进程试图读其控制终端时,终端驱动程序产生此信号。(见9.8 节中对此问题的讨论。)在下列例外情形下不产生此信号,此时读操作返回出错,errno设置
为EIO(a)读进程忽略或阻塞此信号,或(b)读进程所属的进程组是孤儿进程组。
•SIGTTOU:当一个后台进程组进程试图写其控制终端时产生此信号。(见9.8节对此问题的讨
论。)与上面所述的SIGTTIN信号不同,一个进程可以选择为允许后台进程写控制终端。第11
章将讨论如何更改此选择项。如果不允许后台进程写,则与SIGTTIN相似也有两种特殊情况:
(a)写进程忽略或阻塞此信不幸的是,术语停止(stop)有不同的意义。在讨论作业控制和信号
时我们需提及停止和继续作业。但是终端驱动程序一直用术语停止表示用Ctrl-S和Ctrl-Q字
符停止和起动终端输出。因此,终端驱动程序将产生交互停止信号和字符称之为挂起字符而
非停止字符。号,或(b)写进程所属进程组是孤儿进程组。在这两种情况下不产生此信号,写
操作返回出错,errno设置为EIO。不论是否允许后台进程写,某些除写以外的下列终端操 作也能产生此信号:tcsetattr,tcsendbreak, tcdrain, tcflush, tcflow 以及tcsetpgrp。 第11章将说明这些终端操作。
•SIGURG:此信号通知进程已经发生一个紧急情况。在网络连接上,接到非规定波特率的数据
时,此信号可选择地产生。
•SIGUSR1:这是一个用户定义的信号,可用于应用程序。
•SIGUSR2:这是一个用户定义的信号,可用于应用程序。
•SIGVTALRM:当一个由setitimer(2)函数设置的虚拟间隔时间已经超过时产生此信号。
•SIGWINCH:SVR4和4.3+BSD内核保持与每个终端或伪终端相关联的窗口的大小。一个进程可
以用ioctl函数(见11.12节)得到或设置窗口的大小。如果一个进程用ioctl的设置-窗口-大小
命令更改了窗口大小,则内核将SIGWINCH信号送至前台进程组。
•SIGXCPU:SVR4和4.3+BSD支持资源限制的概念(见7.11节)。如果进程超过了其软CPU时间限
制,则产生此信号。
•SIGXFSZ:如果进程超过了其软文件长度限制(见7.11节),则SVR4和4.3+BSD产生此信号。
3、signal函数
3.1、signal函数说明
signal函数专门用于设置对信号的处理方式(默认,忽略和捕获)。
1)、函数原型和所需头文件
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
2)、函数功能:注册信号处理方式。
3)、函数参数
·int signum:信号编号,一般直接填写信号的宏名即可。
·sighandler_t handler:信号处理方式。
4)、函数返回值
成功则返回上一次对信号设置的处理方式(上一次的handler值),失败返回宏SIG_ERR。
5)、注意
·信号三种处理方式的设置:
a)、handler处写SIG_DFL,处理方式设置为默认
一般情况下,默认方式不需要显示的调用signal函数进行设置,因为不做任何信号处理方式设置的情况下,就是默认的。
b)、handler处写SIG_IGN,处理方式设置为忽略
设置为忽略以后,信号发给进程后,就当做没有发生一样。
c)、handler处写信号处理函数的地址,处理方式设置为捕获
捕获函数的类型是 sighandler_t,这个类型是对void (*)(int)由typedef 而来。
·宏SIG_DFL、SIG_IGN和SIG_ERR的类型
处理方式设置为捕获时,信号捕获函数的类型是void (*)(int)型的,那么为了统一参数类型,SIG_DFL、SIG_IGN和SIG_ERR的定义形式是这样的,
#define SIG_DFL ((void (*)(int))0)
#define SIG_IGN ((void (*)(int))1)
#define SIG_ERR ((void (*)(int))-1)
这几个宏定义在了<signal.h>头文件中,当然内核不会直接如此定义,而尽量的使用typedef将void (*)(int)类型转换成为一个别名,但是这里为了便于理解,我这里直接使用了void (*)(int)进行说明。当然这三个常数并不一定要是0,1,-1,只要这些常数不与任何一个函数地址冲突均可,只是由于历史原因我们用了这三个常数。
6)、测试用例
我们知道按下ctrl+c会产生SIGINT的信号,默认情况下通常用于终止一个跑飞的终端进程,那么我们就以SIGINT为例子,将这个信号设置为三种处理方式,看下signal函数是如何使用的。
·对SIGINT的处理设置为忽略
typedef void (*sighandler_t)(int); int main(void){ sighandler_t ret = (sighandler_t)-2; ret = signal(SIGINT, SIG_IGN); //忽略SIGINT信号 if(SIG_ERR == ret) perror("signal is fail"); while(1); return 0; }
该程序运行之后,当我们按下ctrl+c时没有任何的反应,这是因为SIGINT已经被忽略了,进程接收到这个信号后会当它不存在。
·对SIGINT的处理设置为默认
int main(void) { void (*ret)(int) = (void (*)(int))-2; /* 这里的设置有点多此一举,因为即使你不设置,程序 * 一开所有信号的处理方式都是默认的,除非在之前有 * 对SIGINT信号做过其它如忽略或捕获的设置后,想要 * 改回原有的默认设置时,需要调用signal函数显示的 * 设置回来 */ ret = signal(SIGINT, SIG_DFL); if(SIG_ERR == ret) perror("signal is fail"); while(1); return 0; }
该程序运行起来之后,当我们按下ctrl+c之后进程会被立即被终止,因为对SIGINT信号默认的处理方式,就是终止接收到该信号的进程。
·对SIGINT的处理设置为捕获
/* 信号捕获函数,注意捕获函数的类型是void (*)(int), * 参数用来接受信号的编号 */ void signal_fun(int signo) { /* 检测如果送过来的信号的确时SIGINT的话, * 提示SIGINT信号捕获成功了,随后调用exit * 函数立即结束整个进程 */ if(SIGINT == signo) printf(" hello, SIGINT be captured\n"); exit(-1); } int main(void) { void (*ret)(int) = (void (*)(int))-2; /* 对SIGINT的处理设置为捕获方式, * 第二个参数写捕获函数的地址 */ ret = signal(SIGINT, signal_fun); if(SIG_ERR == ret) perror("signal is fail"); while(1); return 0; }
该程序运行起来之后,当我们按下ctrl+c之后,首先打印了下面一句话,打印结果如下:
^C hello, SIGINT be captured
之后立即结束,这说明我们成功的实现了对SIGINT信号的捕获。
我们注意一点,第一个例子中的void (*)(int)类型,我们使用了typedef替换,但是后面的两个例子中,均是直接使用的void (*)(int)类型,这两种方法均可,但是鼓励多多使用typedef关键字。
3.2、signal函数的缺点
1)、signal函数的功能比较简单。
2)、想要知道某个信号的当前处理方式是什么比较麻烦,需要使用返回值的方式。
我们知道,对于signal的返回值,无非就下面两种:
·函数调用成功:返回上一次对该信号设置的处理方式。
·函数调用失败:返回SIG_ERR。
所以如果我们想知道对某个信号的当前处理方式是什么,就必须修改一次它的处理方式,通过返回值就能知道它之前的处理方式被设置为了什么,最后再将之前的处理方式设置回去,例子代码如下:
/* 信号捕获函数 */ void signal_fun(int signo) { } /* 函数功能:测试某个信号的处理方式的函数 , * 修改一次对SIGINT信号的处理方式,返回上一次SIGINT信号的处 * 理方式,随后判断是默认,忽略,捕获,又或者函数调用失败了?最 * 后再将SIGINT的处理方式设置回去,还原对SIGINT的原有处理方式*/ void test_deal_method(int signo) { void (*ret)(int) = (void (*)(int))-2; ret = signal(signo, SIG_IGN); if(SIG_DFL == ret) printf("current deal_method of SIGINT is SIG_DFL\n"); else if(SIG_IGN == ret) printf("current deal_method of SIGINT is SIG_IGN\n"); else if(SIG_ERR == ret) perror("signal is fail"); else printf("SIGINT be captured\n"); signal(signo, ret); //还原对SIGINT的原有处理方式 } int main(void) { test_deal_method(SIGINT);//测试对信号的处理方式是什么 signal(SIGINT, SIG_IGN); test_deal_method(SIGINT);//测试对信号的处理方式是什么 signal(SIGINT, signal_fun); test_deal_method(SIGINT);//测试对信号的处理方式是什么 return 0; }
运行该程序,打印结果如下:
current deal_method of SIGINT is SIG_DFL
current deal_method of SIGINT is SIG_IGN
SIGINT be captured
程序刚开始运行时,对SIGINT信号的处理方式就是默认方式,所以第一次的测试的、结果是默认处理方式。
之后对SIGINT的处理方式设置为了忽略,第二次的测试结果是忽略。
最后对SIGINT的处理方式设置为了捕获,第三次的测试的结果是捕获。
为了能够弥补signal函数的缺点,我们引入了sigaction函数,它的功能比signal更强大,在获取某个信号的当前处理方式时非常方便,实际上signal函数也只是对sigaction函数的封装。
4.程序启动
4.1、进程创建
当一个进程fork出一个子进程时,子进程会继承父进程对所有信号设置的处理方式,当处理方式是捕获时,捕获函数在子进程中仍然是有意义的,因为子进程复制了父进程的内存。
4.2、exec新程序
前面说过fork出的子进程会继承父进程对所有信号的处理方式(默认,捕获或忽略),但是一旦在子进程中exec新的程序后,所有要被捕获的信号都被修改为默认动作,这是因为执行了新的程序以后,子进程中所有的地址空间会被新程序覆盖,所以原有从父进程继承来的信号捕获处理函数地址已经没有了意义。
4.3、总结
a)、子进程一定会继承父进程设置的信号处理方式,如果设置的是捕获或者忽略,信号捕获 函数或者忽略设置在子进程中依然有效。
b)、exec执行新程序后,对信号设置的捕获处理方式将被还原为默认处理方式。
c)、exec执行新程序后,对信号设置的忽略处理方式在新程序中继续有效。
5、系统调用的中断和系统调用的重启(了解)
5.1、什么是低速系统调用
内核的所有系统调用被分为两类,一类是低速系统调用,而另一类被称为其它系统调用。
1)、低速系统调用:是可能会使进程永远阻塞的系统调用。
一、导致阻塞的原因有两种。
a)、因文件类型而阻塞,阻塞与函数本身无关,比如:
·读某些文件时,由于数据不存在会导致调用者永远阻塞
读管道:管道是进程间通信用的特殊文件,读管道时,如果管道中并无数
据,会导致读管道文件的操作永远阻塞下去
读终端设备:比如读鼠标,键盘等字符设备类的文件,以及网络设备文
件时,如果没有数据的话,读操作也会阻塞。
注意:值得强调的是,读磁盘I/O(也即普通文件)不是低速系统调用。
·写某些文件:在写某些文件时,由于文件不能立即接收写入的数据时,也可
能会导致写操作长期的阻塞下去,这种情况很少见。
·打开文件:在某些条件成立之前可能有些文件是无法打开的,这也可能会导
致打开操作长期的阻塞下去,比如:
情况1:如果想要成功打开某些终端设备,那么你就必须等到某些调制解
调器应答后才能,否者会一直阻塞下去。
情况2:如果某个管道的读端没打开时,而你又想以写的方式打开该管道,
那么以写方式打开的操作会一直阻赛,直到某个地方以读打开这个管道为止。
我们发现当read读鼠标键盘时会阻塞,而读普通文件时却不会阻塞,所以看出阻塞与否与read本身没有什么关系,是因为文件的类型导致,当然我们前面的课程讲到过如何将键盘和鼠标改为非阻塞的,请回忆一下。
b)、某些函数本身就是阻塞的
pause函数,wait函数,sleep函数,某些ioctl操作,以及某些进程间通信的函数,这些函数调用本身就是阻塞的。
二、注意
a)、值得强调的是,低速系统调用读磁盘I/O(也即普通文件)并非是阻塞的,尽管 读、写一个磁盘文件可能暂时阻塞调用者(在磁盘驱动程序将请求排入队列,然后在 适当时间执行请求期间),但是除非发生硬件错误,I/O操作总会很快返回,并使调 用者不再处于阻塞状态。
b)、从上面知道对于低速系统调用来说,我们不能简单地说因为文件而阻塞或因函数
而阻塞,实际上这两种情况都存在。
当read函数去读键盘、鼠标而没有数据输入时,默认就是阻塞的,但是当read去读普通文件时,不管有没有数据都会快速返回,所以同样是read函数,有时可能会阻塞而有时又可能不会阻塞。
但是像pause等类的函数,与文件类型什么的是没有什么关系的,该函数调用本身就是阻塞的。
c)、像前面提到的字符设备、管道以及网络文件,这些文件打开时默认都是阻塞的,
所以之后read这些文件时都会阻塞,但是我们在前面的课程中学到,可以在打开时或者打开后利用fcntl函数将其指定为非阻塞的,之后read时即使没有数据它也不会阻塞。
2)、其它系统调用:除了低速系统调用外的都是其它系统调用。
5.2、低速系统调用存在的问题
之前我们说过,低速系统调用可能会导致调用进程长期阻塞,为了避免这个问题,我们的内核提供了一种机制,那就是该系统调用可以被任何的信号唤醒(内核认为这是唤醒阻塞的绝佳机会)。但是这种机制也有一个缺点,如果我本来就希望长期阻塞,而阻塞过程不幸被信号中断,在这种情况下我们就必须重启这个系统调用,重启系统调用的方法有三种,当然如果不希望被中断的话,还有一个办法,那就是忽略或者屏蔽信号。
1)、手动重启
比如下面这个例子中,当sleep(1000)秒,中途ctrl+c产生SIGINT信号会中断sleep,如果我们不希望sleep被中断的话就需要手动重启。
/* 需要捕获一下IGINT函数,否则SIGINT的某 * 人处理方式会结束次进程 */ void signal_fun(int signo) { } int main(void) { int ret = 1000; signal(SIGINT, signal_fun);//对SIGINT信号的处理方式设置为捕获 /* 对于sleep函数来说,如果调用成功,返回0, * 如果调用失败或被中断则返回剩余未休眠的秒数 */ lab: ret = sleep(ret); if((ret>0&&ret<100) && EINTR==errno) { printf("sleep is interrupted by signal\n"); goto lab; } printf("do last thing\n"); return 0; }
程序运行时,按下ctrl+c,显示如下结果:
^C sleep is interrupted by signal
^C sleep is interrupted by signal
每次sleep被信号中断后,都会执行goto lab语句,跳到lab的位置重启sleep系统调用,休眠剩余的秒数(ret会记录下剩余的时间),直到整个休眠1000秒的时间到为止,在这之前不管按下多少次ctrl+c,进程都不会结束,因为每次系统调用都会被重启。
2)、自动重启
前面的手动重启的方法虽然简单但也是有缺点的,比如有时候我们可能并不清楚某些输入、输出设备底是不是低速系统调用,那么我们在写应用程序时就不清楚是不是要手动重启,所以我们系统引入了自动重启的概念,所谓自动重启就是不管你是不是低速系统调用,我一律不用手动重启,如果你不是就不阻塞,如果是那么再被信号打断后将会自动重启该系统调用,我们写应用代码的人无须关心。
ioctl、read、readv、write、writev、wait和waitpid这些函数以前都是需要手动重启的,但是现在已经全部改为自动重启了:
/* 需要捕获一下IGINT函数,否则SIGINT的某 * 人处理方式会结束次进程 */ void signal_fun(int signo) { printf(" signal be captured\n"); } int main(void) { int ret = 1000; char buf[300] = {0}; signal(SIGINT, signal_fun);//对SIGINT信号的处理方式设置为捕获 /* 对于sleep函数来说,如果调用成功,返回0, * 如果调用失败或被中断则返回剩余未休眠的秒数 */ lab: ret = read(0, buf, sizeof(buf)); if(ret<0 && EINTR==errno) { printf("sleep is interrupted by signal\n"); goto lab; } printf("do last thing\n"); return 0; }
运行程序,按下ctrl++c,结果如下:
^C signal be captured
^C signal be captured
每按下一次ctrl+c,则打印一次^C signal be captured,虽然我们加入了手动重启,但是sleep is interrupted by signal并没有被打印,说明手动重启没用,实际上当每次read被信号中断之后会立即自动重启,所以如下手动重启的代码:
lab:
if(ret<0 && EINTR==errno){
printf("sleep is interrupted by signal\n");
goto lab;
}
没有了意义。
实际上自动重启也有缺点,虽然这些自动重启的函数可以被某个信号中断,但是由于这些函数的自动重启会导致这些函数后面的代码没有被执行的机会,进程永远被定在read处,如何实现让后面的代码有运行的机会呢,这可以将read和poll或select等需要手动重启的函数联合使用。
3)、设置信号以实现重启
为了重启系统调用,除了上面的手动和自动两种方法外,我们还可以对信号进行设置,从而实现让被该信号中断的系统调用重启,这一方法在上一篇的末尾有提及,后面讲sigaction时也会再次提到。
7、可再入与不可再入函数(了解)
6.1、不可再入函数
如果在执行xxx函数时信号来了,该函数会被中断而立即调用信号捕获函数,如果信号捕获函数还能够再次调用xxx函数的话,我们就说xxx函数是一个可再入函数,否则就是不可再入函数。以malloc为例,实际上malloc函数就不是一个可载入函数,因为malloc函数在分配空间时,需要修改一个存储区链表,如果在malloc正在被调用时信号来了,信号捕获函数里面如果再次调用malloc的话,上一次malloc修改的存储区链表会被信号捕获函数中的malloc冲刷掉。
6.2、不可再入函数的特征
1)使用静态数据结
2)调用malloc或free
3)标准I/O函数。很多标准I/O函数都是不可再入函数,因它们都是用全局(静态)数据结构。
拥有以上三种条件之一的函数基本就是不可载入函数,但是我们在实际使用时不必完全拘泥于此,出现数据结构被冲刷的情况还是很少见的。
8、kill函数和raise函数
1)、函数原型和所需头文件
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
#include <signal.h>
int raise(int sig);
2)、函数功能
·kill函数:将信号发送给指定的进程或进程组。
·raise函数:专门用于进程向自身发送信号,同kill(gtpid(), signo)。
3)、函数参数
·kill函数
pid_t pid:接受信号的进程或进程组。
int sig:信号。
·raise函数
int sig:信号。
4)、函数返回值
·kill函数:成功返回0,失败返回-1,errno被设置。
·rasie函数:成功返回0,失败返回非0。
5)、注意
a)、kill是系统调用,但是raise是库函数(可以看做是对kill的封装)。
b)、rasie函数只能向进程自己发送信号。
c)、kill函数可以向pid指定的进程或进程组发信号,pid共有4种不同的设置情况。
·pid>0:将信号发送给进程ID= =pid的进程。
·pid= =0:将信号发送给发送进程所在进程组下的所有进程。
·pid<0:将信号发送给其进程组ID= =|pid|的进程组下的所有进程。
·pid= =-1:此情况未定义。
d)、发送信号进程与接收信号进程的实际或有效用户ID之间的关系
·超级用户进程一定可以将信号发送给其它超级用户进程或普通用户进程。
·对于非超级用户进程,其基本规则是发送者进程的实际或有效用户 ID必须等于接收者
进程的实际或有效用户I D。
e)、编号0的信号被定义为空信号。如果signo参数是0,则kill仍执行正常的错误检查,但不发
送信号。这常被用来确定一个特定进程是否仍旧存在。如果向一个并不存在的进程发送空信
号,则kill返回-1,errno则被设置为ESRCH。但是,应当了解,系统在经过一定时间后会重
新使用这个进程ID,所以一个现存的具有所给定进程ID的进程并不一定就是你所想要的进
程。
6)、测试用例
a)、rasie函数
int main(void) { printf("111111\n"); printf("222222\n"); printf("333333\n"); raise(SIGINT); printf("444444\n"); printf("555555\n"); return 0; }
程序打印结果如下:
111111
222222
333333
从结果可以看出,4和5并没有被打印出来,因为打印完3之后,raise函数就向自己发送了一个SIGINT信号,而这个信号默认的处理方式就是将接收到信号的进程终止。
b)、kill函数的使用举例:略,请自行测试。
9、alarm函数和pause函数
8.1、函数说明
1)、函数原型和所需头文件
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
int pause(void);
2)、函数功能
·alarm函数:设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过。当所设置
的时间值被超过后,会产生SIGALRM信号。如果不忽略或不捕捉此信号的话,则其默认动作
是终止调用该函数的进程。
·pause函数:使调用该函数的进程挂起(阻塞),直至捕捉到一个信号为止,换句话说该函 数的阻塞是可以被信号中断(唤醒)的。
3)、函数参数
·alarm函数
unsigned int seconds:设置闹钟时间。
·pause函数:没有参数。
4)、函数返回值
·alarm函数:如果该进程在之前没有设置过闹钟时间,并且还未超时的话则返回0,否者返
回剩余的时间值。
·pause函数:函数阻塞时,只有当被某个信号中断时才返回,其返回值为-1,并且errno 被设置为EINTR(表示被信号中断),实际上在本篇前面的内容我们就曾说过,pause函数本
身就是可以被信号中断的低速系统调用。
5)、注意
a)alarm函数
·每个进程当前时刻只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设
置过闹钟时间,而且它还没有超时,则该闹钟时间的余留值作为本次alarm函数的值
返回。以前设置的闹钟时间将会被本次新设置的闹钟时间所替换。
·如果以前登记的闹钟时间尚未超时,但是本次设置的seconds新值确实是0,则会取
消以前的闹钟时间,但是其余留值仍然作为函数的返回值被返回。
b)、pasue函数
· pause函数的阻塞可以被除了SIGKILL和SIGSTOP信号外的所有信号中断(唤醒), 但是前提是我们必须对这个信号进行设置捕获,否者很多信号的默认处理方式会终止
进程。
·pause函数是低速系统调用,可以被信号中断,因为该函数本身就是阻塞的,但是
该函数不具备自动重启功能,一般采用手动重启功能。
6)、测试用例
a)、alarm函数
int main(void) { int ret = -3; //由于之前没有设置闹钟时间,因此返回值为0 ret = alarm(10); printf("1. ret = %d\n", ret); sleep(2); //由于之前闹钟时间设置为了10s,但是中途休 //眠了2s,闹钟剩余值为8s,返回值为8s ret = alarm(10); printf("2. ret = %d\n", ret); sleep(3); //之前闹钟时间再次被设置为了10s,中途休 //眠了3s,所以闹钟剩余值为7s,返回值为7s ret = alarm(10); printf("3. ret = %d\n", ret); sleep(4); return 0; }
改成运行了9s,打印结果如下:
1. ret = 0
2. ret = 8
3. ret = 7
在第一次设置闹钟时间为10s之前,由于没有设置过闹钟所以返回值为0,因此第一次打印出来的ret值等于0。
在第二次设置闹钟时间为10s时,由于之前设置了一次闹钟时间为10s,并且中途休眠了2s,所以还剩余8s的闹钟时间未到,所以本次alarm函数的返回值为8,因此第二次打印出来的ret值等于8。
在第三次设置闹钟时间为10s时,由于之前又一次设置闹钟时间为10s,并且中途休眠了3s,所以还剩余7s的闹钟时间未到,所以本次alarm函数的返回值为7,因此第三次打印出来的ret值等于7,之后又休眠了4s。
在return进程结束之前,总共休眠了3次,一共是9s,虽然最后一次闹钟时间是10s,但是实际上4s之后进程就终止了,进程终止时闹钟时间实际上还未超时,但是倘若最后一次休眠的是20s的话,那么在休眠还未结束之前超时就到了,到时会发送SIGALRM给进程并终止进程。
a)、pause函数
void signal_fun(int signo){ printf("signal %d", signo); } int main(void) { int ret = -3; printf(" process be pasued\n"); signal(SIGINT, signal_fun); ret = pause(); if(ret<0 && EINTR==errno) printf(" interrupt pause\n"); printf(" hello\n"); return 0; }
程序运行时,按下ctrl+c后,打印结果如下:
process be pasued
^Csignal 2 interrupt pause
hello
8.2、利用alarm和pause函数模拟sleep函数
利用用alarm和pause来简单的模拟一下我们自己的sleep函数,代码如下:
/* 信号捕获函数 */ void signal_fun(int signo) { } /* 用alarm和pause模拟的sleep函数,secondes:休眠的时间 */ void my_sleep(int secondes) { //捕获SIGALRM信号,否则默认的信号处理方式会终止进程 signal(SIGALRM, signal_fun); alarm(secondes);//设置闹钟时间 pause();//挂起进程直到因休眠时间到而被SIGALRM信号唤醒为止 } int main(void) { int ret = -3; printf("sleep 3 s\n"); my_sleep(3);//调用我自己实现的sleep函数 printf("sleep over\n"); return 0; }
程序会休眠3s,并且打印结果如下:
sleep 3 s
sleep over
但是实际上这个模拟的sleep函数存在着如下三个方面问题的:
a)如果调用者之前已设置过闹钟,则它会被my_sleep的第一次alarm擦去。
更正的方法是检查第一次调用alarm的返回值,如其小于本次调用alarm的参数值,则只需等待此次设置的超时到即可。如果前次设置闹钟时间大于此次的设置,则在my_sleep函数返回之前,应再次设置回剩余闹钟时间,使得前次设置的超时时间能够在预定时间到时发生超时。
b)该程序中修改了对SIGALRM的配置。
更正这一点的方法是保存signal函数的返回值,在返回前恢复对信号的原有处理方式的设置。
c)在调用alarm和pause之间有一个竞态条件。在一个繁忙的系统中,alarm很可能在调用 pause之前超时(因为执行完alarm后在执行pause之前很可能由于系统忙于其它事情而无法执
行pause语句),如果发生了这种情况后,pause会永远的失去被SIGALRM信号唤醒的机会而导
致长期的阻塞下去。
前两种情况很好解决。但是第3的问题不好纠正,其纠正的方法有如下两种:
·第一种方法:是使用setjmp(此函数不讲,所以本方法略)。
·第二种方法:是使用sigprocmask和sigsuspend,本篇的后面将说明这种方法。
10、信号的发送、接收和处理的过程
10.1、涉及概念
1)、信号屏蔽字:
信号屏蔽字是属于进程表的一部分,被专门用于描述与信号相关的内容,typedef后的类型是sigset_t,它实际上是如下的一个类型:
typedef struct { unsigned long sig[_NSIG_WORDS]; } sigset_t;
是一个结构体(定义在了signal.h中),结构体的成员是一个有64个元素的无符号长整形的数组,每一个下标的元素对应一个信号编号等于下标号的信号,这里为了理解的方便我们可以认为igset_t只是一个64位的无符号整形数。从第1位到第64位的每一位被用来设置对应编号信号的响应权限,当这一位为0时代表可以接受该信号,为1时则代表屏蔽(或称为阻塞)该信号,信号屏蔽字的示意图如下:
起初信号屏蔽字的64位全部为0,表示所有的信号都可以被接受,一旦某个信号被接受后,该信号在屏蔽字中对应的那一位会被设置为1,在这个信号的捕获函数处理之前这一位将一直是1,也就意味着当该信号再次产生时将得不到响应直到前次信号被处理完毕为止,屏蔽字中对应这一位才被清0,当然学习到本篇的后面时,我们将学会如何人为的改变屏蔽字中各位的设置。
2)、信号的发送
当造成信号的事件发生时,会给相关进程发送一个信号,发生信号的事件可以是如下的任何一种事件:
a)、硬件异常,例如除以0,存储访问异常等。
b)、软件条件,例如,闹钟时间超过等。
c)、终端产生的信号,如ctrl+c产生SIGINT信号。
d)、其它进程利用kill函数或命令向另一个进程发送信号。
3)、信号的递送
当进程发现信号已经发送过来,进程会按照如下方式响应信号:
首先检查对信号的处理方式:
1)如果是忽略,则直接忽略此信号。
2)否则紧接着检查该信号在信号屏蔽字的对应位:
·如果为0则表示当前递送的这个信号可以响应,接着把这一位设置为1,然后紧接
着按照相应处理方式去处理信号,那么屏蔽字这一位由0变1过程我们成为“递送”。
当信号被处理完后,屏蔽字的该位又会被清0。
·如果为1则表示之前该信号应经发生过而且正在被处理中,该信号这一次的发送将
会被阻塞,那么发送和被递送之间的过程我们成为“未决”,这一次发送的信号会变
成为“未决信号”而被记录到未决信号集中。
4)、未决信号字(或称未决信号集)
在本质上它和信号屏蔽字是一样的,只是各自履行的功能不一样而已,未决信号集的每一位被用来记录信号的未决状态,如果某信号发生了多次而导致多次未决,那么未决只能被记录一次。
如果该位为0:表示该信号没有未决。
如果该位为1:表示该信号有未决。
10.2、以某信号为例,看一下具体的过程
上图为大家详细的说明了信号从发生到被进程接受再到被处理的过程,这里我们再次以SIGINT信号为例详细地讲述下信号发生后的处理过程。
1)信号发送:某事件发生,产生SIGINT信号并发送给相关进程。
2)信号递送:相关进程接收到这个信号后,首先查看处理方式。
·忽略:此信号直接被忽略。
·不是忽略:此种情况要么是捕获,要么是终止处理方式,这时需要查看该信号对应的屏蔽
字某位为0还是为1。
a)如果为0,递送成功:允许该信号马上被处理,该位立即被设置为1(递送成功),在
当前信号被处理的过程中,以阻塞下一次该信号再次产生时干扰本次信号的处理。
b)如果为1,递送失败:说明之前该信号发生过正在且正在被处理,又或被人为设置为
1以期阻塞此信号,在此种情况下会递送失败,而该信号会因变成未决信号而被设置到未
决信集中,设置之前检查该信号在未决信号集对应的为:
·该位为0,允许登记未决
·该位为1,说明之前已经登记过一次该信号的未决,当同一信号多次未决时,只允
许登记一次。
未决信号集中该位被置的1状态会被一直被保持,直到屏蔽字中该信号的对应位被清0而被响应而被清0,又或者因该信号的处理方式被改为了忽略而被清0为止(当信号处理方式被改为忽略后,该信号就没有被响应的必要了)。
3)处理信号
递送成功后,按照处理方式处理信号。
·捕获:调用捕获函数,捕获函数执行完毕后
(1)捕获函数调用exit直接结束整个进程。
(2)返回到之前被捕获函数中断的位置继续运行,并且自动的将该 信号在屏蔽字
中对应的位清0,然后检查一下未决信号集查
·如果该信号对应位为1:表示该信号有未决状态,立即回到第2步过程被重复,
需要再次检查信号的处理方式的原因是因为在信号未决的过程中该信号的处理
方式有被修改的可能(比如被改为了忽略),如果处理方式不是忽略则信号立即
被递送,与此同时未决信号集中对应的位也会被立即清0,以允许下一次该信号的
未决登记。
·如果该信号对应位为0;表示该信号没有未决发生过,此时会等待下一次该信
号的发生。
·默认处理方式是终止:终止则终止进程。
10.3、如何人为修改信号屏蔽字
从上一小节的学习中我们已经认识到,信号屏蔽字带有屏蔽(阻塞)信号的作用,但是一般情况下都是进程自动地置1和清0,但实际上我们也可以人为的去修改屏蔽字的某些位,人为的实现对某些信号的阻塞或取消阻塞,这就涉及一系列信号集合的设置函数和sigprocmask函数的使用,本篇后面会陆续地介绍到。
想要人为地修改信号屏蔽字,我们必须提前清楚如下几个问题:
1)只有在信号被成功递送后,内核才按照要求的处理方式处理信号,只要信号还是未决的(还
未被成功递送),那么我们就可以修改对信号的处理方式。
2)如果在解除对某个信号的阻塞之前,该信号再次发生多次,这些信号都是未决信号而且只
有一次被记录到未决信号集中,因为大多数UNIX并不对多次产生的信号进行排队。
3)如果有多个不同的信号要递送给一个进程那么将如何呢? 内核并没有规定这些信号的递
送顺序。但是基本上与进程当前状态有关的信号,例如SIGSEGV优先于其它信号被递送。
10.4、例子
void signal_fun(int signo) { printf("SIGINT be captured\n"); sleep(3); } int main(void) { signal(SIGINT, signal_fun); while(1); return 0; }
运行该程序,运行结果如下:
按下第一次ctrl+c时,SIGINT be captured被打印一次,随后按下的多次ctrl+c均无反应,直到3s后SIGINT be captured再次被打印一次,之后不管按下多少次ctrl+c,每3s只能响应一次,通过上面的学习,我们应该清楚产生这样结果的原因了。
当第一次按下ctrl+c时,信号屏蔽字开始对这个SIGINT信号是允许递送的,递送完后SIGINT信号在信号屏蔽字中对应的位被设置为1,由于该信号捕获函数会休眠3s,在这期间屏蔽字中SIGINT信号对应的位会被一直为1,导致后面多次按下的ctrl+c产生的SIGINT信号都得不到递送而成为未决信号而被记录在未决信号集中,虽然按下很多次但是只能有一次未决被记录,当等到3s后屏蔽字中SIGINT对应的位被清0时,此时会响应未决信号集记录的未决信号,当然前提是在未决期间该信号的处理方式并未被修改为忽略才行。
11、信号集设置函数和sigprocmask函数
通过第9小节的学习,我们知道了什么是信号屏蔽字,屏蔽字中的位除了能够被自动的置1和清0外,也能够被人为地按照需要进行置1和清0以阻塞和不要阻塞某些信号,前面也提到如果想要人为的修改屏蔽字中的位的话必须用到信号集设置函数和sigprocmask函数。
11.1、什么是信号集
实际上信号集和屏蔽字以及未决信号集本质上是一样的东西,都是sigset_t类型,我们仍然可以把它们认为是64位无符号整形数,信号集主要用于修改屏蔽字,步骤如下:
1)利用信号集设置函数,预先按要求设置好信号集(某些信号要屏蔽,某些信号要打开的)。
2)利用sigprocmask函数,将我们的信号集拿去和信号屏蔽字做相与、相或或者直接替换
等的操作,从而达到修改信号屏蔽字的目的。
11.2、设置信号集的函数
1)、函数原型和所需头文件
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
2)、函数功能
·sigemptyset函数:将信号集set的64位全部清0。
·sigfillset函数:将信号集set的64位全部置1。
·sigaddset函数:将信号集set的signum(信号编号)指定的那一位置1。
·sigdelset函数:将信号集set的signum(信号编号)指定的那一位清0。
·sigisnumber函数:判断信号集合set中signum信号对应的位是否有被设置。
3)、函数参数
·sigset_t set:需要被设置的信号集的地址。
·int signum:信号编号,信号集中的每一位的编号恰好等于每个信号的编号。
4)、函数返回值
·sigemptyset(),sigfillset(),sigaddset(),and sigdelset()调用成功返回0,失败返回
-1,并且errno被设置。
·sigismember()函数,如果检查发现集合中确实signum对应的为确实有被设置的话返回1, 没有被设置就返回0,如果函数调用失败的话返回-1。
5)、注意
a)、sigemptyset和sigfillset函数
·函数sigemptyset初始化由set指向的信号集,将集合中所有的位清0。
·函数sigfillset初始化由set指向的信号集将集合中所有的位置1。
所有应用在使用信号集前,应该调用sigemptyset或sigfillset初始化信号集合一次。
b)、sigaddset和sigfillset函数
一旦已经初始化了一个信号集,以后就可以调用这两个函数在信号集中增、删特定的信号。函数sigaddset将一个信号添加到现存集中, sigdelset则从信号集中删除一个信号。对所有以信号集作为参数的函数,都需要向其传送信号集地址。
6)、测试用例:暂略,讲完sigprocmask函数之后一起举例。
11.2、sigprocmask函数
1)、函数原型和所需头文件
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
2)、函数功能:将我们设置好的信号集set按照how的要求,去修改信号屏蔽字,当然我们 可以同oldset获取到修改前的原有信号屏蔽字。
3)、函数参数
·int how:指定把信号集拿去修改屏蔽字的方式。
how的设置有如下三种。
SIG_BLOCK:字面意思就是屏蔽某个信号,修改方式是“新屏蔽字=信号集|屏蔽字”, 其实就是对屏蔽字进行置操作,set包含了我们希望阻塞的附加信号。
SIG_UNBLOCK:字面意思就打开某个信号(不要屏蔽),修改方式是“新屏蔽字=(~信 号集)&屏蔽字”,实际就是对屏蔽字的某位进行清0操作,set包含了我们希望被解除 阻塞的信号。
SIG_SETMASK:这种方式就是直接利用信号集替换掉屏蔽字,“新屏蔽字=信号集”。
·const sigset_t *set:需要被拿去修改屏蔽字的信号集。
·sigset_t *oldset:获取被修改前的屏蔽字内容。
4)、函数返回值:函数调用成功返回0,失败返回-1。
5)、注意
a)如果set是一个空指针NULL的话,信号屏蔽字则保持不变,即便是设置了how也是没有
意义的。
b)除了SIG_SETMASK这种信号集直接替换屏蔽字外,SIG_BLOCK和SIG_UNBLOCK这两中修
改方式要求我们在利用信号集修改屏蔽字时只能修改我们关心的位,而不能改变其它不
相关的位。
6)、测试用例
9.4的例子,在信号处理函数sleep休眠的过程中产生的SIGINT信号无法被响应,因为在这期间屏蔽字中SIGINT对应的位被置为了1,之后所有产生的SIGINT信号都被屏蔽了。
如何才能做到在休眠期间,来一个SIGINT信号立马就被相应一次呢,那就需要我们人为地将屏蔽字中SIGINT对应位清0,例子代码如下:
void signal_fun(int signo) { sigset_t set;//定义一个信号集set sigemptyset(&set);//将set所有的位清0 //将信号集中SIGINT对应位置1,set被设置为了0000...00010000...0 sigaddset(&set, SIGINT); //按照SIG_BLOCK的要求用set取修改屏蔽字,新屏蔽字 = ~(set)&屏蔽字 //SIGINT在屏蔽字中对应的位被清0 sigprocmask(SIG_UNBLOCK, &set, NULL); printf("SIGINT be captured\n"); sleep(10); printf("sleep over\n"); } int main(void) { signal(SIGINT, signal_fun); while(1); return 0; }
请留意粗体部分代码,该程序运行结果如下:
立即按下ctrl+c,立即响应打印出^CSIGINT be captured,即使是在某个信号正在被处理的时候。当你不再按下ctrl+c时,10s后会一次性打印出所有的sleep over,数量与按下ctrl+c的次数相等。
该程序有一个现象是值等我们注意的,那就是第一个产生SIGINT信号正在被处理时(处理函数正在被运行),但是第二次SIGINT信号又产生了,捕获函数再次被调用,那么第一次正在执行的捕获函数会被中断,以本程序为例,按下四次ctrl+c的话,处理函数调用如下:
根据上图的描述知道,如果捕获函数的调用时间较长(里面包含休眠),而且此后产生的信号又能被及时响应(没有被屏蔽),那么此次的信号捕获函数会中断前次的正在处理的捕获函数,直到此次处理完后返回后前次的捕获函数才会继续运行。
因为上述原因,本例运行时会有一个奇怪的现象,那就是如果你一直不停ctrl+c的按下去,“sleep over”这句话永远都不会被打印出来,直到你停下并等待10s后,所有被挤压“sleep over”会被一次性打印出来。这是因为如果你永远按下去的话,所有的信号处理函数会永远处于被中断的状态,即便是某个函数的休眠时间到了,但是由于被中断了,那么“sleep over”这句话的打印语句永远得不到执行,直到停下等到最后一次信号处理函数彻底的执行完毕后才会一次次的返回,所有前面被中断的信号处理函数才得以继续执行。
12、sigpending函数
1)、函数原型和所需头文件
#include <signal.h>
int sigpending(sigset_t *set);
2)、函数功能
获取未决信号集,因为我们有时希望知道,哪些信号有未决状态,这就要求我们去查看未决信号集以了解情况。
3)、函数参数
·sigset_t *set填写一个信号集的地址,用来获取未决信号集。
4)、函数返回值:函数调用成功返回0,失败返回-1。
5)、注意:无
6)、测试用例
void signal_fun(int signo) { int ret = -1; sigset_t pendset;//定义信号集,set用于设置屏蔽字用,pendset用于获取未决信号字 printf("SIGINT be captured\n"); sleep(3); sigpending(&pendset); //获取未决信号字 ret = sigismember(&pendset, SIGINT); //判断下SIGINT是否是未决信号 if(1 == ret) printf("SIGINT is pending\n"); else if(0 == ret) printf("SIGINT is not be pended\n"); else if(ret < 0) printf("sigismember is fail\n"); printf("sleep over\n"); exit(-1); //结束进程 } int main(void) { signal(SIGINT, signal_fun); while(1); return 0; }
运行该程序,打印结果如下:
^C^CSIGINT is pending
sleep over
结果说明SIGINT是未决信号,因为默认情况下这个信号发生后,捕获函数调用的过程中,这个信号会被设置为屏蔽,再次产生的该信号会成为未决信号,而代码测试结果也说明在信号捕获函数休眠期间,多次按下的ctrl+c成为了未决信号。
13、sigaction函数
signal函数在前面讲过,说过这个函数有些缺点,也说过它的底层实现其实是sigaction函数,我们现在将对sigaction函数进行详细的讲解。
1)、函数原型和所需头文件
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
2)、函数功能:对信号signum实现act指定的动作,oldact可以用来接受以前老动作。
3)、函数参数
·int signum:指定信号编号。
·const struct sigaction *act:指定对信号的动作。
·struct sigaction *oldact:接受以前对信号设置的老的动作。
4)、函数返回值:函数调用成功返回0,失败返回-1。
5)、注意
a)、act结构体
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
·void(*sa_handler)(int):模拟signal函数,用于登记的带一个参数的捕获函数,该函
数的参数用于传递信号编号。
·void (*sa_sigaction)(int, siginfo_t *, void *):如果想要获取有关发送信号更为详
细的信息的话,需要登记带三个参数的信号处理函数,这要求将sa_flags设置为SASIGINFO,
函数参数说明如下:
第一个参数:信号编号,说明我们希望对那个信号处理。
第二个参数:siginfo_t结构体信息,当某个信号产生时,通过这个参数可以获取到与这 个信号更多的信息,比如什么原因产生的信号,产生这个信号的进程号是多少等等。
结构体参数如下:
siginfo_t { int si_signo; /* 信号值*/ int si_errno; /* errno值*/ int si_code; /* 信号产生的原因 */ //这三项对所有信号都有意义
\* 以下的这些选项都是用联合体实现的,不同成员适应不同信号 *\ pid_t si_pid; /* 发送信号的进程ID,对kill(2), *实时信号以及SIGCHLD有意义 */ uid_t si_uid; /* 发送信号进程的真实用户ID, *对kill(2),实时信号以及SIGCHLD有意义 */ int si_status; /* 退出状态,对SIGCHLD有意义*/ clock_t si_utime; /* 用户消耗的时间,对SIGCHLD有意义 */ clock_t si_stime; /* 内核消耗的时间,对SIGCHLD有意义 */ sigval_t si_value; /* 信号值,对所有实时有意义,是一个 * 联合数据结构,可以为一个整数(由si_int标示, * 也可以为一个指针,由si_ptr标示)*/ void * si_addr; /* 触发fault的内存地址,对 * SIGILL,SIGFPE,SIGSEGV,SIGBUS 信号有意义*/ int si_band; /* 对SIGPOLL信号有意义 */ int si_fd; /* 对SIGPOLL信号有意义 */ }
第三个参数:没有用上。
·sigset_t sa_mask:如果想要在执行信号捕获函数时屏蔽一些信号(不想被这些信号干
扰),可是通过sa_mask这个参数实现,避免了使用sigprocmask的麻烦。
·int sa_flags:选择对信号的各种处理的选项,一般来说当我们设置了sa_flags后就需要
登记带三个参数的信号处理函数,这样可以通过信号捕获函数第二个参数获取与信号相关的
更详细的信息。sa_flags可设置宏如下:
sa_flags的可选项 |
说明 |
SANOCLDSTOP |
若signo是SIGCHLD,当一子进程停止时(作业控制),不产生此信号。当一子进程终止时,仍旧产生此信号(但请参阅下面说明的SVR4 SA_NOCLDWA IT可选项) |
SARESTART |
由此信号中断的系统调用自动再起动(参见10.5节) |
SAONSTACK |
若若用sigaltstack(2)已说明了一替换栈,则此信号递送给 替换栈上的进程 |
SANOCLDWAIT |
若若signo是SIGCHLD,则当调用进程的子进程终止时,不创建僵死进程。若调用进程在后面调用wait,则阻塞到它所有子进程都终止,此时返回-1,errno设置为ECHILD(见10.7节) |
SANODEFER |
当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号。注意,此种类型的操作对应于早期的不可靠信号 |
SARESETHAND |
对此信号的处理方式在此信号捕捉函数的入口处复置为SIGDFL。注意,此种类型的信号对应于早期的不可靠信号 |
SASIGINFO |
当sa_flags设置为该选项时,我们就可以登记带有三个参数的信号处理函数,利用第二个参数siginfo_t结构可以获取信号发生时,与该信号相关的详细情况。 |
b)、sigaction除了比signal函数还有一个优点就是,利用sigaction获取对某个信号的当前处理方式时不用改变当前的处理方式,但是signal就不行。
6)、测试用例
a)、获取某个信号的当前处理方式。
void signal_fun1(int signo) //一个参数的信号处理函数 { } void signal_fun2(int signo, siginfo_t *siginfo, void *arg) //三个参数的信号处理函数 { } int main(void) { int ret = -1; struct sigaction oldact, act; act.sa_handler = SIG_IGN; // act.sa_sigaction = signal_fun2; sigaction(SIGINT, &act, NULL);//设置SIGINT的处理方式 /* 获取SIGINT的当前当前处理方式 */ if(sigaction(SIGINT, NULL, &oldact) < 0){ perror("sigaction is fail"); exit(-1); } /* 判断失身么处理方式 */ if(SIG_DFL == oldact.sa_handler) printf("SIG_DFL\n"); else if(SIG_IGN == oldact.sa_handler) printf("SIG_IGN\n"); else printf("SIGINT be captured\n"); return 0; }
自己尝试看下运行结果,我们发现不管是对sa_handler设置还用sa_sigaction进行设置处理方式,最后用sa_handler和sa_sigaction进行判断都行,因为这两个人会被同步设置,只是类型不同而已,所以我们约定判断对某个信号的当前处理方式时,全部用sa_handler判断某信号的当前处理方式。
前面我们用signal进行判断某信号的当前处理方式时,必须修改当前处理方式来返回上一次当前处理方式然后再设置回去,显得很麻烦,但是sigaction函数就避免了这一点。
b)、用sigaction函数模拟简单的signal
我们之前也说过siganl的底层也是用sigaction实现的,我们现在自己来实现以下如何利用sigaction实现我们自己的signal函数。
/* 利用typedef将void (*)(int)类型重命名为sighndler_t */ typedef void (*sighandler_t)(int); /* 信号处理函数 */ void signal_fun(int signo) { printf("signal be caputred\n"); } /* 用sigaction函数模拟出来的signal函数 */ sighandler_t my_signal(int signo, sighandler_t handler) { struct sigaction act, oldact; act.sa_handler = handler; if(sigaction(signo, &act, &oldact) < 0) goto lab0; else goto lab1; lab0: return SIG_ERR; lab1: return oldact.sa_handler; } /* 测试信号处理方式的函数 */ void test_signal_deal_method(int signo) { sighandler_t ret; //通过修改处理方式,返回信号的当前处理方式 ret = my_signal(signo, SIG_IGN); if(SIG_ERR == ret) { perror("my_signal is fail"); exit(-1); } else if(SIG_IGN == ret) printf("signal is SIG_IGN\n"); else if(SIG_DFL == ret) printf("signal is SIG_DFL\n"); else printf("signal be cauptured\n"); my_signal(signo, ret);//还原处理方式 } int main(void) { //将SGIINT设置为捕获 my_signal(SIGINT, signal_fun); //测试SIGINT的处理方式 test_signal_deal_method(SIGINT); //将SIGINT设置为SIG_DFL my_signal(SIGINT, SIG_DFL); //测试SIGINT的处理方式 test_signal_deal_method(SIGINT); return 0; }
c)、之前讲过sigation还有更多的功能,我们来看下
void sigaction_fun(int signo, siginfo_t *siginfo, void *arg) { if(SIGSEGV == signo) { //提示什么对什么地址访问导致了段错误 printf("SIGSEGV be send, addr %d can't be access\n", (int)siginfo->si_addr); exit(-1); } } int main(void) { struct sigaction act; //注册带三个参数的信号捕获函数 act.sa_sigaction = sigaction_fun; //将sa_flags设置为SA_SIGINFO后,可以获取该信号的详细信息 act.sa_flags = SA_SIGINFO; if(sigaction(SIGSEGV, &act, NULL) < 0) { perror("sigaction is fail"); exit(-1); } int *p = (int *)100; *p = 100; //该操作产生段错误,进程被发送SIGSEGV信号 return 0; }
运行结果如下:
SIGSEGV be send, addr 100 can't be access
因为我试图向100地址空间写数据,但是实际上这个地址空间并不存在,所以进程会被内核发送SIGEGV信号,信号处理函数的形参siginfo指向的空间存放了与该信号相关的详细信息,其中就存放了导致访问错误的地址
14. sigsuspend函数
1)、函数原型和所需头文件
#include <signal.h>
int sigsuspend(const sigset_t *mask);
2)、函数功能:让进程挂起,信号屏蔽字被替换为mask,进程只能被mask允许的信号唤醒,
唤醒后屏蔽字会从mask被还原为原来的屏蔽字。
3)、函数参数
·const sigset_t *mask:一个集合,其中设置了我们想要屏蔽和打开的信号,到时信号屏
蔽字会被这个集合替换。
4)、函数返回值:总是返回-1,因为只有被信号打断时才返回,errno被设置为EINTR。
5)、注意
1)之前的pause函数挂起后,可以被任何被捕获的信号唤醒,但是sigsusoend函数可以按照 要求被指定的信号唤醒。
2)唤醒和将屏蔽字还原为原有屏蔽字的操作是一个原子操作。
6)、测试用例
void signal_fun(int signo) { } int main(void) { sigset_t mask; //捕获SIGINT函数 signal(SIGINT, signal_fun); //捕获SIGINT函数 signal(SIGINT, signal_fun); /* 设置一个mask,只有SIGINT信号被允许,其它的全部被屏蔽 */ sigfillset(&mask); sigdelset(&mask, SIGINT); /* 挂起,信号屏蔽字被设置为mask,挂起的进程只能被SIGINT唤醒, * 唤醒后,屏蔽字又被还原为原来的屏蔽字 */ sigsuspend(&mask); printf("hello\n"); return 0; }
程序运行起来后,除了按下ctrl+c产生SIGINT信号外,按下ctrl+\没有任何作用,因为其他信号都被屏蔽了。
14、abort函数
1)、函数原型和所需头文件
#include <stdlib.h>
void abort(void);
2)、函数功能:向调用该函数的进程发送SIGABRT信号,对该信号的默认处理方式是终止进程。
3)、函数参数:无
4)、函数返回值:无返回值
5)、注意:无
6)、测试用例
int main(void) { sleep(5); abort(); printf("hello\n"); return 0; }
程序休眠3s后,abort发送SIGABRT信号终止了进程,因此“hello”没有被打印出来。
15、sleep函数
1)、函数原型和所需头文件
#include <unsitd.h>
unsigned int sleep(unsigned int seconds);
2)、函数功能:休眠seconds秒。
3)、函数参数:指定需要休眠的秒数。
4)、函数返回值
休眠完成返回0,否者如果休眠过程被信号打断,返回剩余未休眠的秒数,errno被设置为EINTR。
5)、注意:
1)sleep是一个库函数
2)该函数是一个低速系统调用,会被信号打断,不具备自动重启功能。
6)、测试用例
void signal_fun(int signo) { } int main(void) { int ret = 0; signal(SIGINT, signal_fun);//捕获信号SIGINT ret = sleep(10); if(ret>0 && ret<=10) { perror("sleep be interrupted\n"); exit(-1); } printf("hello\n"); return 0; }
在程序休眠的10s内,如果按下ctrl+c的话,sleep会被SIGINT信号打断,"sleep be interrupted\n"被打印你出来。