Li Jiaheng's blog
1931 字
10 分钟
程序控制流(3) 信号
WARNING

这是《深入理解计算机系统》(CS)的笔记,彼时还未上CO,OS等计算机系统基础课,存在理解不到位与错误之处.

前面说的异常是硬件、软件合作提供的低层异常机制(陷阱、终止、中断、故障)。下面来看一种更高层次的软件形式的异常,Linux 信号。通过发送不同信号,进程或内核可以中断其它进程,并携带一定的信息。

信号种类与意义#

信号就是一条小消息,Linux 支持30种不同的信号,每个信号定义有号码。信号可以来自进程、内核、硬件等。

主要几种信号:

编号信号名称默认行为触发事件
2SIGINT终止来自键盘的中断 Ctrl+C
3SIGQUIT终止来自键盘的退出 不知道怎么按
8SIGFPE终止浮点异常(一般除以0)
9SIGKILL终止杀死进程。这个信号不能被捕获也不能被忽略,也就是不能重载它的处理程序。识别到SIGKILL,立刻终止。
14SIGALRM终止来自alarm函数的定时器信号
17SIGCHLD忽略一个子进程终止或停止
18SIGCONT忽略如果进程停止,则继续执行
19SIGSTOP停止等SIGCONT不是来自终端的停止信号
20SIGTSTP停止等SIGCONT来自终端的停止信号,Ctrl+Z
21SIGTTIN停止等SIGCONT后台进程从终端读
22SIGTTOUT停止等SIGCONT后台进程向终端写

发出信号:内核更新目的进程的上下文中的某个状态,将信号发送给进程。
接收信号:发送来的信号处于待处理状态。等进程执行了该信号的信号处理程序后(若未人为指定重载,则为默认行为),内核消除此信号,才算进程接收了信号。
注意,多个相同的待处理信号不会重叠(这里分为实时信号和非实时信号,前32个信号是非实时信号,非实时信号不会重叠)。也就是说,如果信号a发送到进程X的时候,发现进程X已经有一个待处理信号a,那么后面的这个a会被丢弃,而不会“排到后面”。也就是,待处理信号中有个信号a,只说明至少有一个a发送到该进程中。这是很常见的bug制造点。
有多个待处理信号时,处理顺序不定。
阻塞信号:进程设置自己的阻塞信号集合,集合中的信号能够被发送到本进程,但是进程不会处理这些信号,直到进程取消阻塞。
信号状态:内核为每个进程在pending位向量中维护待处理信号集合,在block位向量中维护被阻塞的信号集合。它们以 one-hot 形式存储,如果有信号2,则第2位置1.

进程组#

Unix向进程发送信号的机制是基于进程组 的。每个进程都只属于一个进程组,默认情况下,父进程与子进程在同一个进程组。每个进程组有自己的 ID,通过getpgrp函数返回自己进程所处的进程组ID。而setpgid 函数可以修改某个进程的进程组属性。

#include<unistd.h>  
pid_t getpgrp(void);  
int setpgid(pid_t pid, pid_t pgid);  

对于setpgid(),意义为将 pid 所对应的进程的进程组修改为 ID 为pgid的进程组。如果pid=0,则修改调用这个函数的进程(自己)的进程组属性;如果pgid=0,则用pid所指定的进程的pid为其新进程组的ID。
比如:pid=15123的进程调用setpgid(0,0),则新建一个ID=15123的进程组,将调用此函数的进程(15123)的进程组修改为ID=15123的进程组。此时,这个进程组中只有它一个进程。

/bin/kill 程序发送信号
在命令行输入

linux> /bin/kill -9 15123  

意为向pid=15123的进程发送编号为9(SIGKILL)的信号。

linux> /bin/kill -9 -15123  

负号可以理解为横杠 -,表示向编号为15123的进程 发送编号为9的信号。
命令行中用路径表示程序,有的shell中kill是内建命令而有的不是。

从键盘发送信号
shell用作业 这个概念表示为对一条命令行求值而创建的进程组。只能有一个前台作业、0个或多个后台作业。进程组ID一般设置为作业中的“根进程”的pid。
用 / 可以在前台作业中创建多个子进程。比如:

linux> ls / sort  

可以在前台作业中创建两个子进程,分别运行ls和sort。
在键盘上输入Ctrl+C 向前台进程组中所有子进程发送SIGINT信号,默认终止前台进程组中每个进程。而Ctrl+Z 发送一个SIGTSTP信号,默认挂起前台作业。

用kill()函数发送信号
注意kill函数不一定杀死进程,这只是个发送信号的函数。

#include<sys/types.h>  
#include<sys/signal.h>  
int kill(pid_t pid, int sig);  

意味发送信号号码sig给进程pid。如果pid>0,发送给对应进程。如果pid=0,kill发送信号给调用Kill的进程所在进程组中的所有进程,包括调用者自己。如果pid<0,则发送给编号为 abs(pid) 的进程组中的所有进程。

用alarm函数发送信号

#include<unistd.h>  
unsigned int alarm(unsigned int secs);  

作用是在 secs 秒后向调用进程(自己)发送一个 SIGALRM 信号。它的默认行为是终止,可以重载。
调用它不会导致程序停下来,时钟在“后台”自动计时,而程序继续运行。如果这次调用alarm函数时,后台的时钟还在为上一次alarm计时,则直接将时钟重置,开始为本次alarm计时,并且在本次调用的alarm中返回上次alarm剩余未跳完的秒数。
可见alarm函数立刻有一个返回。

接收信号#

默认情况下,接收信号只有四种行为:
终止;终止并转储内存;停止等待SIGCONT信号;忽略。
但我们可以用signal函数修改对某个信号的处理行为。

#include<signal.h>  
typedef void (*sighandler_t)(int);  //定义一个void func(int) 的函数指针类型  
sighandler_t signal(int signum, sighandler_t handler);  

在此进程中,将signum对应的信号处理函数替换为handler所指向的函数。返回之前的信号处理函数指针。如果出错则返回SIG_ERR,但不设置errno。
如果handler=SIG_IGN,则行为变为忽略signum对应的信号。
如果handler=SIG_DFL,则恢复为默认行为。
比如:

#include<signal.h>  
...  
void sigint_handler(int sig)  
{  
	...  
}  
int main()  
{  
	if(signal(SIGINT,sigint_handler)==SIG_ERR){  
		error...  
	}  
	...  
}  

就将SIGINT的处理函数修改为sigint_handler函数。当接收SIGINT信号时,会调用这个函数。

阻塞信号#

隐式阻塞:内核默认阻塞当前处理程序正在处理的信号。比如一个s 信号到来的时候,程序正在处理上一个s信号并且处于s处理程序S中,则自动阻塞新来的s。(但不会舍弃)
主动阻塞:通过sigprocmask函数和其辅助函数明确地阻塞和解除阻塞信号。

#include<signal.h>  
int sigprocmask(int how, const sigset_t *set, const sigset_t *oldset);  
/* 以下是辅助函数,用来构造阻塞集合set */  
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(sigset_t *set, int signum);//判断set中是否有signum。  

sigprocmask依照 set 的值修改block 位向量。how参数定义其行为:
how=SIG_BLOCK:将set中的信号添加到blocked中。blocked |= set;
how=SIG_UNBLOCK:从blocked中删除set中的信号。block &= (~set);
how=SIG_SETMASK:block=set。
参数*oldset 用来存储修改前block位向量的状态。

程序控制流(3) 信号
https://namisntimpot.github.io/posts/computersystem/程序控制流/3信号/
作者
Li Jiaheng
发布于
2022-08-15
许可协议
CC BY-NC-SA 4.0