一 信号的基本概念
信号机制是进程间相互传递消息的一种方法,信号全称软中断信号,也有人称作软中断,从它的命名可以看出,它的使用很像中断,所以,信号是进程控制的一部分。
(1)进程之间可以通过系统调用kill发送软中断信号
(2)内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。
注:信号指示通知给进程发生了什么事,并不给进程传递数据。
为了理解信号,我们从熟悉的场景说起
- 用户输入指令,在shell下启动一个前台进程。
- 用户按下Ctrl+C,此时硬盘驱动产生一个中断给Linux内核。
- 如果cpu当前正在执行这个进程的代码,则该进程的用户空间暂停执行,CPU从用户态切换到内核态处理硬件中断。
- 终端驱动程序将Ctrl+C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送一个SIGINT信号给该进程)
- 当某个信号从内核返回到该用户空间代码继续执行之前,首先处理PCB中(也可以说发送一个SIGINT信号给该进程)
kill -l 命令可查看系统定义的信号列表
信号产生的条件主要有:
1.用户在终端下按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl+C产生SIGINT信号,Ctrl+\产生SIGQUIT信号,Ctrl+Z产生SIGSTOP信号(可使前台进程终止)
2.硬件异常产生信号,这些条件由硬件检测并通知内核,然后内核向当前进程发送适当的信号,例如当前进程执行了除以0的指令,CPU运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给该进程,再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给该进程。
3.一个进程调用kill(2)函数可以发送信号给另一个进程,可以用kill(1)給某个进程,kill(1)也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号默认处理动作是终止进程。当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。如果不想按默认动作处理信号,用户进程可以调用sigaction(2)函数告诉进程应该如何处理某种信号。
可选的信号处理有一下三个动作:
- 忽略此信号
- 执行信号的默认处理动作
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。
产生信号的方式:
- 通过终端键产生信号(Ctrl+C等)
- 调用系统函数向进程发信号
- 由软件条件产生信号(如闹钟alarm函数)
-
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在second秒之后给当前进程发送SIGALARM信号,该信号默认处理动作是终止当前进程,这个函数返回值是0或者是以前闹钟时间还余下的秒数。
二 阻塞信号
信号在内核中的表示
信号产生有各种原因,而实际信号的处理动作称为信号的递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞才执行递达动作,注意:阻塞和忽略是不同的,只要信号被阻塞就不会被递达,而忽略是递达之后可选的一种处理动作。
每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作,信号产生时,内核在进程控制块中设置信号未决标志,直到信号递达才处理这个标志。
- SIGHUP信号未阻塞也未产生过,当它递达是才默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达,虽然它的处理动作是忽略的,但没有接触阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
如果进程在解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
允许系统递送信号一次或多次,Linux是这样实现的:常规信号在递送之前产生只计一次,而实时信号在递送之前产生多次可依次放在队列里。
从上图看来,每个信号只有一个bit的未决标志,非0即1,不记录信号产生了多少次,阻塞标志也是这样表示的。因此未决和阻塞标志可用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号“有效”或“无效”状态。信号阻塞集也叫作当前进程的信号屏蔽字(Signal Mask).
#include<stdio.h> #include<signal.h> void catch() {} int my_sleep(int timeout) { signal(SIGALRM,catch); alarm(timeout); pause(); int ret = alarm(0); signal(SIGALRM,SIG_DFL); return ret; } int main() { while(1) { printf("testing...\n"); my_sleep(1); } return 0; }
设置一个闹钟,使得每一秒输出一次。
现在重新审视“mysleep”程序,设想这样的时序:
1. 注册SIGALRM信号的处理函数。
2. 调用alarm(nsecs)设定闹钟。
3. 内核调度优先级更高的进程取代当前进程执行,并且优先级更高的进程有很多个,每个
都要 执行很长时间
4. nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态。
5. 优先级更高的进程执行完了,内核要调度回这个进程执行。SIGALRM信号递达,执行处
理函 数sig_alrm之后再次进入内核。
6. 返回这个进程的主控制流程,alarm(nsecs)返回,调用pause()挂起等待。
7. 可是SIGALRM信号已经处理完了,还等待什么呢?
出现这个问题的根本原因是系统运行的时序(Timing)并不像我们写程序时所设想的那样。
虽然alarm(nsecs)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用
alarm(nsecs)之 后的nsecs秒之内被调用。由于异步事件在任何时候都有可能发生(这里
的异步事件指出现更高优 先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题
而导致错误,这叫做竞态条件 (Race Condition)。
如何解决上述问题呢?读者可能会想到,在调用pause之前屏蔽SIGALRM信号使它不能提前递
达就可 以了。看看以下方法可行吗?
- 屏蔽SIGALRM信号;
2. alarm(nsecs);
3. 解除对SIGALRM信号的屏蔽;
4. pause(); - 从解除信号屏蔽到调用pause之间存在间隙,SIGALRM仍有可能在这个间隙递达。要消除这
个间隙, 我们把解除屏蔽移到pause后面可以吗?
1. 屏蔽SIGALRM信号;
2. alarm(nsecs);
3. pause();
4. 解除对SIGALRM信号的屏蔽;
这样更不行了,还没有解除屏蔽就调用pause,pause根本不可能等到SIGALRM信号。要是
“解除信号屏蔽”和“挂起等待信号”这两步能合并成一个原子操作就好了,这正是sigsuspend
函数的功 能。sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对
时序要求严格的场合下都应该调用sigsuspend而不是pause。
三 信号捕捉
测试31种信号哪种可以捕捉?哪种不能捕捉?
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
1.