信号的基本概念
1. ⽤户输⼊命令,在Shell下启动⼀个前台进程。
2. ⽤户按下Ctrl-C,这个键盘输⼊产⽣⼀个硬件中断。
3. 如果CPU当前正在执⾏这个进程的代码 ,则该进程的⽤户空间代码暂停执⾏ ,CPU从⽤户态 切换到内
核态处理硬件中断。
4. 终端驱动程序将Ctrl-C解释成⼀个SIGINT信号,记在该进程的PCB中(也可以说发送了⼀ 个SIGINT
信号给该进程)。
5. 当某个时刻要从内核返回到该进程的⽤户空间代码继续执⾏之前 ,⾸先处理PCB中记录的信号,发现
有⼀个SIGINT信号待处理,⽽这个信号的默认处理动作是终⽌进程 ,所以直接终⽌进程⽽不再返回它
的⽤户空间代码执⾏。
注意:
1. Ctrl-C产⽣的信号只能发给前台进程。⼀个命令 后⾯加个&可以放到后台运⾏,这样Shell不必等待进程结束就可以接受新的命令 ,启动新的进程。
2. Shell可以同时运⾏⼀个前台进程和任意多个后台进程 ,只有前台进程才能接到像 Ctrl-C这种控制键产⽣的信号。
3. 前台进程在运⾏过程中⽤户随时可能按下 Ctrl-C⽽产⽣⼀个信号,也就是说该进程的⽤户空间代码执⾏到任何地⽅都有可能收到 SIGINT信号⽽终⽌,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
⽤kill -l命令可以察看系统定义的信号列表 :
产⽣信号的⽅式概览:
1. ⽤户在终端按下某些键时 ,终端驱动程序会发送信号给前台进程 ,例如Ctrl-C产⽣SIGINT信号,Ctrl-\产⽣SIGQUIT信号,Ctrl-Z产⽣SIGTSTP信号(可使前台进程停⽌,这个信号将 在后⾯课程详细解释)。
2. 硬件异常产⽣信号,这些条件由硬件检测到并通知内核 ,然后内核向当前进程发送适当的信 号。例如当前进程执⾏了除以 0的指令,CPU的运算单元会产⽣异常 ,内核将这个异常解释 为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址 ,,MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。
3. ⼀个进程调⽤kill(2)函数可以发送信号给另⼀个进程。 可以⽤kill(1)命令发送信号给某个进程 ,kill(1)命令也是调⽤kill(2)函数实现的,如果不明确指定信号则发送 SIGTERM信号,该信号的默认处理动作
是终⽌进程。 当内核检测到某种软件条件发⽣时也可以通过信号通知进程 ,例如闹钟超时产⽣SIGALRM信号,向读端已关闭的管道写数据时产⽣ SIGPIPE信号。如果不想按默认动作处理信号 ,
⽤户程序可以调⽤sigaction(2)函数告诉内核如何处理某种信号 .
4. 软件条件产⽣。
信号处理常⻅⽅式概览 : (sigaction函数稍后详细介绍),可选的处理动作有以下三种 :
1. 忽略此信号。
2. 执⾏该信号的默认处理动作。
3. 提供⼀个信号处理函数 ,要求内核在处理该信号时切换到⽤户态执⾏这个处理函数 ,这种⽅式称为捕捉(Catch)⼀个信号。
产⽣信号
1. 通过终端按键产⽣信号
2. 调⽤系统函数向进程发信号
3. 由软件条件产⽣信号
阻塞信号
1. 信号其他相关常⻅概念
实际执⾏信号的处理动作称为信号递达 (Delivery)。
信号从产⽣到递达之间的状态 ,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产⽣时将保持在未决状态 ,直到进程解除对此信号的阻塞 ,才执⾏递达的动作.
注意:阻塞和忽略是不同的 ,只要信号被阻塞就不会递达 ,⽽忽略是在递达之后 可选的⼀种处理动作。
2. 在内核中的表⽰
每个信号都有两个标志位分别表⽰阻塞 (block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志 ,直到信号递达才清除该标志。在上图的例⼦中 ,SIGHUP信号未阻塞也未产⽣过 ,当它递达时执⾏默认处理动作。 SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略 ,但在没有解除阻塞之前不能忽略这个信号 ,因为进程仍有机会改变处理动作之后再解除阻塞。 SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤户⾃定义函数 sighandler。如果在进程解除对某信号的阻塞之前这种信号产⽣过多次 ,将如何处理?POSIX.1允许系统递送该信号⼀次或多次。 Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。
3. sigset_t
每个信号只有⼀个bit的未决标志,⾮0即1,不记录该信号产⽣了多少次 ,阻塞标志也是这样表⽰的。因此,未决和阻塞标志可以⽤相同的数据类型 sigsett来存储,sigsett称为信号集,这个类型可以表⽰每个信号的“有效”或“⽆效”状态,在阻塞信号集中“有效”和“⽆效”的含义是该信号是否被阻塞 ,⽽在未决信号集中“有效”和“⽆效”的含义是该信号是否处于未决状态。下⼀节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字 (Signal Mask),这⾥的“屏蔽”应该理解为阻塞⽽不是忽略。
4. 信号集操作函数
sigsett类型对于每种信号⽤⼀个 bit表⽰“有效”或“⽆效”状态,⾄于这个类型内部如何存储这些 bit则依赖于系统实现,从使⽤者的⾓度是不必关⼼的 ,使⽤者只能调⽤以下函数来操作 sigset t变量,⽽不应该对它的内部数据做任何解释,⽐如⽤printf直接打印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);
int sigismember(const sigset_t *set, int signo);
1、函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应 bit清零,表⽰该信号集不包含 任何有效信号。
2、函数sigfillset初始化set所指向的信号集,使其中所有信号的对应 bit置位,表⽰该信号集的有效信号包括系统⽀持的所有信号。
注意:在使⽤sigset_ t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调⽤ sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
sigprocmask
调⽤函数sigprocmask可以读取或更改进程的信号屏蔽字 (阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1。
如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过 oset参数传出。如果set是⾮空指针,则更改进程的信号屏蔽字,参数how指⽰如何更改。如果 oset和set都是⾮空指针,则先将原来的信号 屏蔽字备份到oset⾥,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask,下表说明了how参数的可选值。
如果调⽤sigprocmask解除了对当前若干个未决信号的阻塞 ,则在sigprocmask返回前,⾄少将其中⼀个信号递达。
捕捉信号
信号的捕捉
1. 内核如何实现信号的捕捉
如果信号的处理动作是⽤户⾃定义函数 ,在信号递达时就调⽤这个函数 ,这称为捕捉信号。由于信号处理函数的代码是在⽤户空间的 ,处理过程⽐较复杂,举例如下:⽤户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执⾏main函数,这时发⽣中断或异常切换到内核态。 在中断处理完毕后要返回⽤户态的 main函数之前检查到有信号SIGQUIT递达。内核决定返回⽤户态后不是恢复 main函数的上下⽂继续执⾏ ,⽽是执⾏
sighandler函数,sighandler和main函数使⽤不同的堆栈空间 ,它们之间不存在调⽤和被调⽤的关系 ,是两个独⽴的控制流程。sighandler函数返回后⾃动执⾏特殊的系统调⽤ sigreturn再次进⼊内核态。如果没有新的信号要递达,这次再返回⽤户态就是恢复 main函数的上下⽂继续执⾏了。
2. sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调⽤成功则返回 0,出错则返回- 1。signo是指定信号的编号。若 act指针⾮空,则根据act修改该信号的处理动作。若 oact指针⾮空,则通过oact传出该信号原来的处理动作。 act和oact指向sigaction结构体;
将sahandler赋值为常数SIGIGN传给sigaction表⽰忽略信号,赋值为常数SIG_DFL表⽰执⾏系统默认动作,赋值为⼀个函数指针表⽰⽤⾃定义函数捕捉信号 ,或者说向内核注册了⼀个信号处理函 数,该函数返回值为void,可以带⼀个int参数,通过参数可以得知当前信号的编号 ,这样就可以⽤同⼀个函数处理多种信号。显然 ,这也是⼀个回调函数 ,不是被main函数调⽤,⽽是被系统所调⽤。
当某个信号的处理函数被调⽤时 ,内核⾃动将当前信号加⼊进程的信号屏蔽字 ,当信号处理函数返回时⾃动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时 ,如果这种信号再次产⽣ ,那么它会被阻塞到当前处理结束
为⽌。如果在调⽤信号处理函数时 ,除了当前信号被⾃动屏蔽之外 ,还希望⾃动屏蔽另外⼀些信号 ,则⽤samask字段说明这些需要额外屏蔽的信号 ,当信号处理函数返回时⾃动恢复原来的信号屏蔽字。
3. pause
#include <unistd.h>
int pause(void);
pause函数使调⽤进程挂起直到有信号递达。如果信号的处理动作是终⽌进程 ,则进程终 ⽌,pause函数没有机会返回;如果信号的处理动作是忽略 ,则进程继续处于挂起状态 ,pause不返回;如果信号的处理动作是捕捉 ,则调
⽤了信号处理函数之后 pause返回-1,errno设置为EINTR,所以pause只有出错的返回值(想想以前还学过什么函数只有出错返回值 ?)。错误码EINTR表⽰“被信号中断”。
可重⼊函数
main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的 时候,因为硬件中断使进程切换到内核 ,再次回⽤户态之前检查到有信号待处理 ,于是切换 到sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的 两步都做完之后从sighandler返回内核态,再次回到⽤户态就从 main函数调⽤的insert函数中继续 往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是 ,main函数和sighandler先后向链表中插⼊两个节点 ,⽽最后只有⼀个节点真正插⼊链表中了。
像上例这样,insert函数被不同的控制流程调⽤ ,有可能在第⼀次调⽤还没返回时就再次进⼊该函数 ,这称为重⼊,insert函数访问⼀个全局链表 ,有可能因为重⼊⽽造成错乱 ,像这样的函数称为 不可重⼊函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数 ,则称为可重⼊(Reentrant) 函数。
如果⼀个函数符合以下条件之⼀则是不可重⼊的 :
调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。