WARNING这是《深入理解计算机系统》(CS
)的笔记,彼时还未上CO,OS等计算机系统基础课,存在理解不到位与错误之处.
前面说的异常是硬件、软件合作提供的低层异常机制(陷阱、终止、中断、故障)。下面来看一种更高层次的软件形式的异常,Linux 信号。通过发送不同信号,进程或内核可以中断其它进程,并携带一定的信息。
信号种类与意义
信号就是一条小消息,Linux 支持30种不同的信号,每个信号定义有号码。信号可以来自进程、内核、硬件等。
主要几种信号:
编号 | 信号名称 | 默认行为 | 触发事件 |
---|---|---|---|
2 | SIGINT | 终止 | 来自键盘的中断 Ctrl+C |
3 | SIGQUIT | 终止 | 来自键盘的退出 不知道怎么按 |
8 | SIGFPE | 终止 | 浮点异常(一般除以0) |
9 | SIGKILL | 终止 | 杀死进程。这个信号不能被捕获也不能被忽略,也就是不能重载它的处理程序。识别到SIGKILL,立刻终止。 |
14 | SIGALRM | 终止 | 来自alarm函数的定时器信号 |
17 | SIGCHLD | 忽略 | 一个子进程终止或停止 |
18 | SIGCONT | 忽略 | 如果进程停止,则继续执行 |
19 | SIGSTOP | 停止等SIGCONT | 不是来自终端的停止信号 |
20 | SIGTSTP | 停止等SIGCONT | 来自终端的停止信号,Ctrl+Z |
21 | SIGTTIN | 停止等SIGCONT | 后台进程从终端读 |
22 | SIGTTOUT | 停止等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位向量的状态。