《UNIX高级环境编程》 第十章 信号 读书笔记(一文读懂UNIX信号机制)
本文介绍了Linux系统中的信号机制,主要内容包括:1.信号的基本概念,包括31种标准信号及其产生条件,以及进程对信号的三种处理方式(忽略、捕获或执行默认动作);2.信号处理函数signal的使用方法,以及信号在进程fork和exec时的行为变化;3.低速系统调用与信号中断的关系,以及可重入函数的重要性;4.常用的信号相关函数,包括kill、raise、alarm、pause等;5.信号集的操作函
目录
一、概念
信号的名称总是以SIG开头。Linux支持31种不同的信号。除此之外,Linux还支持应用程序额外定义的信号将其作为实时扩展。这些信号被定义在<signal.h>中,都是正整数。不存在编号为0的信号,因为其具有特殊含义(后面会提到)。
很多条件可以产生信号:
- 用户按下指定按键(如DELETE键发送中断信号)
- 硬件一场产生信号(除数为0、无效内存等等)。
- 进程调用kill函数将信号发送给另一个进程或进程组(限制:接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者是超级用户)
- 用户可用kill命令发送信号给其他进程
- 当硬件检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。
当进程接收到一个信号时,有三种可选的操作:
1. 忽略此信号。除了SIGKILL和SIGSTOP信号不可被忽略,其他信号都可以忽略
2. 捕捉信号。进程为对应信号编写一个信号处理函数,当进程接收到对应信号时,停止当前的工作,跳转到信号处理函数做出对应操作。注意,不能捕获SIGKILL和SIGSTOP信号。
3. 执行系统默认动作。系统为每个信号都设计了一种默认动作。


终止+core的意思是进程终止并且生成core文件。
在下列条件不产生core文件:1.进程是设置用户ID的,而且当前用户并非程序文件的所有者 2.进程是设置组ID的,而且当前用户并非该程序的组所有者 3.用户没有些当前工作目录的权限 4. 文件已存在,而且用户对该文件设有读写权限 5.文件太大
SIGABRT:调用abort函数产生此信号,进程异常终止
SIGALRM:在用alarm函数设置的计时器超时时产生此信号。若由setitimer函数设置的间隔时间超时时也产生此信号(后面详细说明)
SIGBUS:只是一个是西安定义的硬件故障,通常被发送在出现某些类型的内存故障
SIGCANCEL:Solaris线程库内部使用的信号
SIGCHLD:一个进程终止或停止时发送此信号给其父进程。信号捕捉函数中通常调用wait函数取得子进程ID和终止状态
SIGCONT:次作业控制信号被发送给需要继续运行但当前处于停止状态的进程。
SIGEMT:只是一个实现定义的硬件故障
SIGFPE:算术运算异常(除以0、浮点溢出等等)
SIGFREEZE:此信号仅由Solaris定义,它用于通知进程在解冻系统状态之前需采取特定的动作
SIGHUP:如果终端接口检测到一个连接断开,则将此信号发送给对应的会话首进程。仅当终端的CLOCAL标志没有设置时才产生此信号(第十八章的笔记提到过),中断、退出和挂起信号通常只发送给前台进程组,而SIGHUP总是发送给会话首进程,即使它是后台进程组。会话首进程终止时向前台进程组的每一个进程发送此信号。
SIGILL:此信号指示进程已执行一条非法的硬件指令。
SIGINFO:这是一种BSD信号,当用户按状态键(一般是Ctrl+T)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程。此信号通常导致在终端上显示前台进程组中各进程的状态信息。
SIGINT:当用户按中断键(一般是DELETE或Ctrl+C),终端驱动产生此信号并送至前台进程组每个进程。
SIGIO:此信号指示一个异步IO事件(后面会说明)
SIGIOT:指示一个实现定义的硬件故障
SIGKILL:杀死进程
SIGLWP:此信号由Solaris线程库内部使用
SIGPIPE:如果卸载管道时读进程已终止,则产生此信号(后面详细说明)。当类型为SOCK_STREAM的套接字已不在连接时,进程写道该套接字也产生此信号
SIGPOLL:当在一个可轮询设备上发送一特定时间时产生此信号(后续说明)
SIGPROF:当setitimer函数设置的梗概同级间隔计时器到期时产生此信号
SIGPWR:这时一种依赖于系统的信号。主要用于具有不间断电源(UPS)的系统。如果电源失效,则UPS起作用,而且通常软件会接到通知。在这种情况下系统依靠蓄电池电源继续运行,无需任何处理。但是如果蓄电池也将不能支持工作,软件通常会再次收到通知,此时系统必须在15-30s内使各部分停止运行。大多数系统中此信号被发送给init进程由init处理停机操作
SIGQUIT:当用户在终端上按退出键(Ctrl+\),将此信号发送给前台进程组。
SIGSEGV:改进好指示进程进行了一次无效内存引用
SIGSTKFLT:此信号仅有Linux定义,旨在用于数学协处理器的栈故障
SIGSTOP:作业控制信号,停止一个进程
SIGSYS:该信号指示一个无效的系统调用。
SIGTERM:由kill命令发送的系统默认终止信号
SIGTHAW:此信号仅由Solaris定义,恢复被挂起的操作
SIGTRAP:指示一个是西安定义的硬件故障
SIGTSTP:交互式停止信号,当用户按下挂起键(Ctrl+Z)
SIGTTIN:当一个后台进程组中的进程试图读其控制终端时,终端取得程序产生此信号
SIGTTOU:当一个后台进程试图些到其控制终端时产生的信号
SIGURG:此信号通知进程已经发生一个紧急情况
SIGUSR1:用户自定义信号
SIGUSR2:用户自定义信号
SIGVTALRM:当一个由setitimer函数设置的虚拟间隔时间到期时产生此信号
SIGWINCH:内核维持与每个终端或伪终端相关联的窗口大小,如果使用ioctl更改窗口大小则产生此信号
SIGXCPU:如果进程超过其软CPU时间限制则产生此信号
SIGXFSZ:如果进程超过了其软文件长度限制,产生此信号
二、signal函数
#include <signal.h>
void (*signal(int signo,void (*func)(int)))(int);
注册一个信号处理函数,该进程接收到指定信号时跳转到信号处理函数执行相应操作。
signo的值是前面讲到的信号的信号名。func的值是信号处理函数的地址。此外,还可以指定func为SIG_IGN表示忽略此信号或指定func为SIG_DFL表示接收到此信号之后执行默认操作。
实际调用时可以这样用:
typedef void Sigfunc(int);
Signal *signal(int,Sigfunc *);
示例:
#include "apue.h"
static void sig_usr(int);
int main(void){
if(signal(SIGUSR1,sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR1");
if(signal(SIGUSR2,sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR2");
for(;;)
pasue();
}
static void sig_usr(int signo){
if(signo == SIGUSR1)
printf("received SIGUSR1\n");
else if(signo == SIGUSR2)
printf("received SIGUSR2");
else
err_dump("recived signal %d\n",signo);
}
pasue的意思是挂起进程,当接收到信号时会跳转到对应函数打印。
当进程调用fork时,子进程继承父进程的信号处理函数。因为子进程在开始时复制了父进程的存储映像。若子进程后续又调用了exec系列的函数,则所有信号的处理方式会恢复默认。
三、可中断的系统调用
如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用被中断,不再执行。
系统调用分为低速系统调用和其他系统调用。低俗系统调用是可能会使进程永远阻塞的一类系统调用,包括:
- 在读某些类型的文件(管道、终端设备以及网络设备)时,如果数据并不存在则可能会使调用者永远阻塞。
- 在写这些类型的文件时,如果不能立即接受这些数据,则也可能会使调用者永远阻塞。
- 打开某些类型文件,在某种条件发生之前也可能会使调用者阻塞(例如,打开了终端设备,它要等待直到所连接的调制解调器应答了电话)
- pasue(按照定义,它使调用进程休眠知道捕捉一个信号)和wait函数
- 某些ioctl操作
- 某些进程间通信函数

四、可重入函数
进程捕捉到信号之后,中断系统调用,转而去执行信号处理函数中的内容。等到执行完信号处理函数,程序又要跳回到被中断的地方继续执行。但在信号处理程序中,不能判断捕捉到信号时进程在何处执行,如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,信号处理函数中又调用malloc,这会发生什么?答案是可能会对进程造成破坏,因为malloc通常为它所分配的孙出去维护一个链接表,而插入执行信号处理程序时,进程可能在更改此链接表。
为了避免上述问题,系统规定了一些可以重复进入的函数:

没有在表中的函数大多是不可重入的,原因为:1.已知它们使用静态结构 2.它们调用malloc或free 3.它们是标准IO函数,printf就是一个很容易忽略的点。
每个进程都有一个屏蔽字,它规定来了当前要阻塞递送到该进程的信号集。对于每种信号该屏蔽字中都有一位与之对应,如果设置了某位,则说明对应信号是被屏蔽的。
五、kill函数和raise函数
#include <signal.h>
int kill(pid_t pid,int signo);
int raise(int signo);
//成功返回0,失败返回-1
kill用来发送信号给进程,pid参数有四种不同的情况:
- pid > 0 将该信号发送给进程ID为pid的进程
- pid == 0 将该进程发送给与发送进程属于同一进程组的所有进程,而且发送进程具有向这些进程(不包括实现定义的系统进程集,比如内核进程和init进程)发送信号的权限。
- pid < 0 将该信号发送给其进程组ID等于pid的绝对值,而且发送进程具有向其发送信号的权限(不包括系统进程集)。
- pid == -1 将该信号发送给发送进程有权限向它们发送信号的系统上的所有进程(不包括系统进程集)。
将信号发送给进程需要权限。超级用户可以将信号发送给任一进程,对于非超级用户其基本规则是发送者的实际或有效用户ID必须等于接收者的实际或有效用户ID。如果该系统支持,则检查接收者的保存的设置用户ID而不是其有效用户ID。(例外:如果信号是SIGCONT,则进程可将它发送给属于同一会话的任何其他进程)
POSIX.1标准定义编号为0的信号是空信号,使用kill将此信号发送给一个进程,kill会执行正常的错误检查但不发送信号,可以使用这个特性来检测一个进程是否存在。
六、alarm和pause函数
使用alarm函数可以设置一个定时器,再将来某个指定的时间该计时器会超时,产生SIGALRM信号,此信号对应的默认操作是终止调用alarm的进程:
#include <unistd.h>
unsigned int alarm(unisgned int seconds);
//返回0或以前设置的闹钟时间的余留秒数
seconds秒数之后产生SIGALRM信号。每个进程只能有一个闹钟时钟。如果在一个已经设置过闹钟并且其没有超时的进程再次设置闹钟,则将上次闹钟的余留值作为本次alarm函数调用的值返回,以前设置的闹钟被代替。如果在一个已经设置过闹钟并且其没有超时的情况下再次设置闹钟且seconds = 0,则取消上次设置的闹钟。
pasue函数挂起本进程直至捕捉到一个信号:
#include <unistd.h>
int pasue(void);
//返回-1并将errno设置为EINTR
我们可以使用alarm函数设置阻塞操作的时间上限值,举一个例子,程序中有一个读低俗设备的可能阻塞的操作,我们希望超过一定时间量后就停止执行该操作:
#include "apue.h"
static void sig_alrm(int);
int main(void){
int n;
char line[MAXLINE];
if(signal(SIGALRM,sig_alrm) == SIG_ERR)
err_sys("signal(SIGALRM) error\n");
alarm(10);
if((n=read(STDIN_FILENO,line,MAXLINE))<0)
err_sys("read error\n");
alarm(0);
write(STDOUT_FILENO,line,n);
exit(0);
}
static void sig_alrm(int signo){
//什么也不做,只是用来打断read操作
}
这个程序有连个问题:1.第一次调用alarm和read之间有一个竞争条件。如果alarm设置的闹钟值已经超时,但是read还没有开始执行(内核在中间这段时间将该进程挂起),那么read又可能永远阻塞 2.如果系统调用是自动重启的,则当SIGALRM信号处理程序返回时read并不被中断。这种情况下时间限制不起作用
我们使用longjmp重新实现这个功能(这种方法不用担心系统调用自动重启,因为longjmp之后程序不会再返回被中断的程序位置,而是直接跳转到setjmp的位置):
#include "apue.h"
#include <setjmp.h>
static void sig_alrm(int);
static jmp_buf env_alrm;
int main(void){
int m;
char line[MAXLINE];
if(signal(SIGALRM,sig_alrm) == SIG_ERR)
err_sys("signal (SIGALRM) error");
if(setjmp(env_alrm) != 0)
err_quit(read timeout);
alarm(10);
if((n = read(STDIN_FILENO,line,MAXLINE)) < 0)
err_sys("read error");
alarm(0);
write(STDOUT_FILENO,line,n);
exit(0);
}
static void sig_alrm(int signo){
longjmp(env_alrm,1);
}
此程序仍然会产生一个问题,试想一下,如果此程序除了需要处理SIGALRM信号之外,还需要处理其他的信号,假设该程序为捕捉SIGUSR1信号编写了一个信号处理函数。当计时器开始定时、read操作被阻塞时,程序接收到了SIGUSR1信号,于是跳转到SIGUSR1信号的信号处理函数执行相关操作。在执行SIGUSR1信号处理函数的过程中,alarm计时器超时,程序捕捉到SIGALRM信号,于是又跳转到sig_alrm信号处理函数中去执行longjmp,这个操作将导致程序直接从setjmp开始执行,这导致了一个严重的问题,SIGUSR1的信号处理函数并未执行完成,它被SIGALRM信号中断并且执行了longjmp操作,这将导致程序永远也不会再返回SIGUSR1信号处理函数中去完成被中断的操作。
七、信号集
一个信号集能够表示多个信号。POSIX.1定义了数据类型sigset_t以包含一个信号集,并且定义了下列五个信号集处理函数:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set,int signo);
int sigdelset(sigset_t *set,int signo);
//成功返回0失败返回-1
int sigismember(const sigset_t *set,int signo);
//若真则返回1,假则返回0,出错则返回-1
函数sigemptyset初始化由set指向的信号集,清除其中的所有信号;sigfillset初始化由set指向的信号集,包含所有信号;所有应用程序在使用信号集之前,必须要调用这两个函数中的一个进行初始化。
一旦已经初始化了一个信号集,sigaddset可以向其中添加一个信号,sigdelset可以删除一个信号
八、信号相关的函数
1.sigprocmask函数
调用sigprocmask函数可以检测或更改一个进程的信号屏蔽字:
#include <signal.h>
int sigprocmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);
//成功返回0,失败返回-1
若oset为非空指针,则进程的当前信号屏蔽字通过oset返回
其次,若set是一个非空信号,则how指示如何修改当前吸纳后屏蔽字(SIG_BLOCK是‘或’操作,SIG_SETMAST是赋值操作):
- SIG_BLOCK :该进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集的并集。set包含了我们希望阻塞的附加信号
- SIG_UMBLOCK :该进程新的信号屏蔽字是当前信号屏蔽字和set所指向的信号集补集的交集。set包含了我们希望解除阻塞的信号
- SIG_SETMASK:该进程新的信号屏蔽字将被set指向的信号集的值代替
如果set是空指针,则不改变进程的信号屏蔽字,how的值无意义
2.sigpending函数
sigpending函数返回信号集:
##include <signal.h>
int sigpending(sigset_t *set);
//成功则返回0,失败返回-1
3.sigsetimp和siglongjmp函数
学习这两个函数之前我们需要先了解一下,当进程捕捉到某个信号并且进入信号处理函数时,系统会自动将该信号加入信号屏蔽字,组织同类信号再次中断当前处理。当信号处理函数正常返回时,系统会自动回复信号屏蔽字到进入之前的状态。但如果在信号处理函数中调用longjmp函数跳出,则信号屏蔽字的行为是未定义的,需要引入新的、支持在信号处理函数中跳转的函数:
#insclude <setjmp.h>
int sigsetjmp(sigjmp_buf env , int savemask);
void siglongjmp(sigjmp_buf env , int val);
此函数与原函数唯一的区别是sigsetjmp增加了一个参数,若savemask非0,则在env中保存当前信号屏蔽字;若带非0savemask的sigsetjmp调用已经保存了snv,则sigsetjmp从中恢复保存的信号屏蔽字
4.sigsuspend函数
先澄清一点,信号屏蔽字是对信号的阻塞,阻塞≠忽略,如果一个捕捉到一个信号发现该信号在此进程的信号屏蔽字中,则该信号被阻塞,标记为未决信号,此信号在被解除阻塞(修改屏蔽字)之后会被递送给进程一次,进行处理
现在想象这样一个场景,一个进程需要在某个时机将一个信号从信号屏蔽字中剔除,也就是不阻塞该信号,之后再调用pasue等待此信号。但由于更改信号屏蔽字和pasue并不是一个原子操作,信号可能在修改信号屏蔽字之后、pasue之前被捕捉,并且只递送一次,那么pasue将导致该进程永远阻塞。为了解决这种问题,设计了sigsuspend函数:
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
此函数将该进程的屏蔽字设置为由sigmask指向的值,在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程会被挂起;若捕捉到一个信号且已从信号处理函数中返回,则sigsuspend返回,并将改进程的信号屏蔽字设置为调用sigsusoend之前的值
更多推荐



所有评论(0)