Linux信号
信号处理机制
信号产生方式
在linux系统中,信号由以下五种方式产生
- 按键产生
- 系统调用产生
- 软件条件产生
- 硬件异常产生
- 命令产生
信号的两种状态
信号存在以下两种状态
- 递达:信号产生并且送达进程。被内核直接处理
- 未决:介于产生和递达之间的状态
信号的三种处理方式
- 执行默认处理动作
- 忽略
- 捕捉(自定义)
信号列表
使用如下命令即可查看linux支持的信号列表
1 | kill -l |
信号集
- 阻塞信号集(信号屏蔽字): 本质是位图,用来记录信号的屏蔽状态。一旦被屏蔽的信号, 在解除屏蔽前,一直处于未决态。
- 未决信号集:本质是位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生, 但尚未被处理
信号处理流程
以向屏蔽了SIGINT信号的程序发送Ctrl+C信号为例,系统在接收到硬件发来的信号后,产生中断,进入内核态处理,发现为SIGINT信号,将前台应用的PCB中的未决信号集里的2号位置为1,在处理完中断后,返回用户态之前,检查未决信号集,发现2号位为1,此时检查阻塞信号集,因为已经屏蔽了SIGINT信号,因此阻塞信号集中该位也为1,因此不对此未决信号做出处理,返回用户态继续执行。
若程序没有屏蔽SIGINT信号,那么内核发现阻塞信号集2号位为0,未决信号集2号位为1,则会直接结束进程,不会继续返回用户态执行剩余代码。
注意:
linux中由信号产生的中断为一种软件中断,严格来说并不会立即进入内核处理,而是在因为其他因素产生中断进入内核后,在返回用户态之前,会对信号集进行检查,并处理信号。
也就是说,linux并不会因为信号的产生而特意进入内核处理,而是在处理其他中断出内核的时候顺便检查一下信号,如果有的话就处理一下。但在宏观上由于中断的产生很频繁,所以也可以按照信号产生后立刻就会被处理来理解。
信号集操作方式
信号集操作函数
1 | sigset_t set; //自定义信号集。 |
1 | //设置信号屏蔽字和解除屏蔽: |
how: SIG_BLOCK: 设置阻塞
SIG_UNBLOCK: 取消阻塞
SIG_SETMASK: 用自定义 set 替换 mask。
set: 自定义 set
oldset:旧有的 mask。
1 | //查看当前的未决信号集: |
set: 传出的 未决信号集。
信号的发送与捕捉
信号的发送
kill函数
向指定进程发送指定的信号。
1 | int kill(pid_t pid,int signum) |
参数:
pid:
- 0:发送信号给指定进程
- = 0:发送信号给跟调用 kill 函数的那个进程处于同一进程组的进程。
- < -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
- = -1:发送信号给,有权限发送的所有进程。
signum:待发送的信号
返回值:成功返回0,失败返回-1
注意:kill函数虽然名字叫做kill,但其功能本质上是向指定进程发送一个信号,而并不一定是杀死该进程。只是大多数信号的默认处理动作都是结束进程,以及它也时常被用于结束进程,因此得名。
alarm函数
定时发送 SIGALRM 给当前进程。
1 | unsigned int alarm(unsigned int seconds); |
seconds为要设定的秒数
返回值:上次定时剩余的时间
alarm(0)为取消定时
注意:alarm函数采用的计时为自然时间,即所记的时间为内核空间时间加上用户空间时间。若想要单独记用户空间或内核空间时间,或者想要将单位精确到微秒,则需使用settimer函数。
其他几个发送信号的函数
1 | int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); |
信号的捕捉
信号捕捉函数
linux中每个信号都有自己对应的默认处理函数,而想修改收到信号后的处理方式,则需要我们自己定义信号的捕捉处理函数,这类函数的格式为:
1 | void function(int) |
即传入参数为int型的信号编号,而传出参数为void。
这一捕捉函数无需我们自己调用,我们只需使用signal函数或sigaction函数在linux内核中注册这一函数,即可将想捕捉的信号的处理方式指定为这一函数。当程序收到这一信号时便会自动调用我们指定的这一函数。
signal函数
该函数为ANSI定义,在不同Linux版本中可能有着不同的行为,应尽量避免使用
sigaction函数
修改信号处理动作,在内核中为指定型号注册捕捉处理函数
1 | int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); |
参数:
signum:要修改处理动作的信号
act:传入参数,新的处理方式
oldact:传出参数,旧的处理方式。如果不需要也可以传入NULL.
1 | struct sigaction { |
*sa_handler 为要指定的信号处理函数。
sa_mask 为只在信号捕捉函数调用时才起作用的信号屏蔽字
sa_flags 默认传0,表示在当前信号的捕捉函数执行时,屏蔽这一信号。
需求1:用信号集操作屏蔽ctrl+c操作
1 |
|
需求2:在不影响父进程执行的情况下回收子进程
**补充—SIGCHLD 的产生条件: **
子进程终止时
子进程接收到 SIGSTOP
子进程处于停止态,接收到 SIGCONT 后唤醒时
1 |
|
说明:
可能遇到的问题:
Q1:信号捕捉函数执行的过程中可能有多个子进程同时结束
A1:在捕捉函数中循环回收,直到没有结束的子进程
Q2:若在捕捉函数中采用wait(NULL),会使得父进程一直阻塞等待。
A2:在循环中使用waitpid(-1,&status,WNOHANG),判断没有已经退出的子进程之后就结束捕捉函数(如下图是两种不同的执行结果)
图一为采用wait(NULL)阻塞等待的方式,信号处理函数只被调用了两次,说明父进程一直在被阻塞。之所以是两次而不是一次是因为在阻塞回收的过程中,不断有子进程结束,父进程的未决信号集中SIGCHLD被置为了1,但该信号在信号处理函数期间处于被屏蔽的状态,当回收完所有子进程之后,屏蔽解除,再次进入处理函数处理这一信号。因此为两次。
图二采用非阻塞的调用方式,可以看到四次调用了捕捉函数,说明回收子进程的工作并没有让父进程一直等待,达成了我们的目的。