
http://hutaow.com/blog/2013/10/19/linux-signal/
在Linux中,信号是进程间通讯的一种方式,它采用的是异步机制。当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。
需要说明的是,信号只是用于通知进程发生了某个事件,除了信号本身的信息之外,并不具备传递用户数据的功能。
1 信号的响应动作
每个信号都有自己的响应动作,当接收到信号时,进程会根据信号的响应动作执行相应的操作,信号的响应动作有以下几种:
- 中止进程(Term)
- 忽略信号(Ign)
- 中止进程并保存内存信息(Core)
- 停止进程(Stop)
- 继续运行进程(Cont)
用户可以通过signal或sigaction函数修改信号的响应动作(也就是常说的“注册信号”,在文章的后面会举例说明)。另外,在多线程中,各线程的信号响应动作都是相同的,不能对某个线程设置独立的响应动作。
2 信号类型
Linux支持的信号类型可以参考下面给出的列表。
2.1 在POSIX.1-1990标准中的信号列表
信号 | 值 | 动作 | 说明 |
SIGHUP | 1 | Term | 终端控制进程结束(终端连接断开) |
SIGINT | 2 | Term | 用户发送INTR字符(Ctrl+C)触发 |
SIGQUIT | 3 | Core | 用户发送QUIT字符(Ctrl+/)触发 |
SIGILL | 4 | Core | 非法指令(程序错误、试图执行数据段、栈溢出等) |
SIGABRT | 6 | Core | 调用abort函数触发 |
SIGFPE | 8 | Core | 算术运行错误(浮点运算错误、除数为零等) |
SIGKILL | 9 | Term | 无条件结束程序(不能被捕获、阻塞或忽略) |
SIGSEGV | 11 | Core | 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作) |
SIGPIPE | 13 | Term | 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作) |
SIGALRM | 14 | Term | 时钟定时信号 |
SIGTERM | 15 | Term | 结束程序(可以被捕获、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 用户保留 |
SIGUSR2 | 31,12,17 | Term | 用户保留 |
SIGCHLD | 20,17,18 | Ign | 子进程结束(由父进程接收) |
SIGCONT | 19,18,25 | Cont | 继续执行已经停止的进程(不能被阻塞) |
SIGSTOP | 17,19,23 | Stop | 停止进程(不能被捕获、阻塞或忽略) |
SIGTSTP | 18,20,24 | Stop | 停止进程(可以被捕获、阻塞或忽略) |
SIGTTIN | 21,21,26 | Stop | 后台程序从终端中读取数据时触发 |
SIGTTOU | 22,22,27 | Stop | 后台程序向终端中写数据时触发 |
注:其中SIGKILL和SIGSTOP信号不能被捕获、阻塞或忽略。
2.2 在SUSv2和POSIX.1-2001标准中的信号列表
信号 | 值 | 动作 | 说明 |
SIGTRAP | 5 | Core | Trap指令触发(如断点,在调试器中使用) |
SIGBUS | 0,7,10 | Core | 非法地址(内存地址对齐错误) |
SIGPOLL | Term | Pollable event (Sys V). Synonym for SIGIO | |
SIGPROF | 27,27,29 | Term | 性能时钟信号(包含系统调用时间和进程占用CPU的时间) |
SIGSYS | 12,31,12 | Core | 无效的系统调用(SVr4) |
SIGURG | 16,23,21 | Ign | 有紧急数据到达Socket(4.2BSD) |
SIGVTALRM | 26,26,28 | Term | 虚拟时钟信号(进程占用CPU的时间)(4.2BSD) |
SIGXCPU | 24,24,30 | Core | 超过CPU时间资源限制(4.2BSD) |
SIGXFSZ | 25,25,31 | Core | 超过文件大小资源限制(4.2BSD) |
注:在Linux 2.2版本之前,SIGSYS、SIGXCPU、SIGXFSZ以及SIGBUS的默认响应动作为Term,Linux 2.4版本之后这三个信号的默认响应动作改为Core。
2.3 其它信号
信号 | 值 | 动作 | 说明 |
SIGIOT | 6 | Core | IOT捕获信号(同SIGABRT信号) |
SIGEMT | 7,-,7 | Term | 实时硬件发生错误 |
SIGSTKFLT | -,16,- | Term | 协同处理器栈错误(未使用) |
SIGIO | 23,29,22 | Term | 文件描述符准备就绪(可以开始进行输入/输出操作)(4.2BSD) |
SIGCLD | -,-,18 | Ign | 子进程结束(由父进程接收)(同SIGCHLD信号) |
SIGPWR | 29,30,19 | Term | 电源错误(System V) |
SIGINFO | 29,-,- | 电源错误(同SIGPWR信号) | |
SIGLOST | -,-,- | Term | 文件锁丢失(未使用) |
SIGWINCH | 28,28,20 | Ign | 窗口大小改变时触发(4.3BSD, Sun) |
SIGUNUSED | -,31,- | Core | 无效的系统调用(同SIGSYS信号) |
注意:列表中有的信号有三个值,这是因为部分信号的值和CPU架构有关,这些信号的值在不同架构的CPU中是不同的,三个值的排列顺序为:1,Alpha/Sparc;2,x86/ARM/Others;3,MIPS。
例如SIGSTOP这个信号,它有三种可能的值,分别是17、19、23,其中第一个值(17)是用在Alpha和Sparc架构中,第二个值(19)用在x86、ARM等其它架构中,第三个值(23)则是用在MIPS架构中的。
3 信号机制
文章的前面提到过,信号是异步的,这就涉及信号何时接收、何时处理的问题。
我们知道,函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换,过程可以先看一下下面的示意图:
接下来围绕示意图,将信号分成接收、检测和处理三个部分,逐一讲解每一步的处理流程。
3.1 信号的接收
接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。
注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。
3.2 信号的检测
进程陷入内核态后,有两种场景会对信号进行检测:
- 进程从内核态返回到用户态前进行信号检测
- 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
当发现有新信号时,便会进入下一步,信号的处理。
3.3 信号的处理
信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。
接下来进程返回到用户态中,执行相应的信号处理函数。
信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。
至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。
4 信号的使用
4.1 发送信号
用于发送信号的函数有raise、kill、killpg、pthread_kill、tgkill、sigqueue,这几个函数的含义和用法都大同小异,这里主要介绍一下常用的raise和kill函数。
raise函数:向进程本身发送信号
函数声明如下:
#include <signal.h>
int raise(int sig);
函数功能是向当前程序(自身)发送信号,其中参数sig为信号值。
kill函数:向指定进程发送信号
函数声明如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
函数功能是向特定的进程发送信号,其中参数pid为进程号,sig为信号值。
在这里的参数pid,根据取值范围不同,含义也不同,具体说明如下:
- pid > 0 :向进程号为pid的进程发送信号
- pid = 0 :向当前进程所在的进程组发送信号
- pid = -1 :向所有进程(除PID=1外)发送信号(权限范围内)
- pid < -1 :向进程组号为-pid的所有进程发送信号
另外,当sig值为零时,实际不发送任何信号,但函数返回值依然有效,可以用于检查进程是否存在。
4.2 等待信号被捕获
等待信号的过程,其实就是将当前进程(线程)暂停,直到有信号发到当前进程(线程)上并被捕获,函数有pause和sigsuspend。
pause函数:将进程(或线程)转入睡眠状态,直到接收到信号
函数声明如下:
#include <unistd.h>
int pause(void);
该函数调用后,调用者(进程或线程)会进入睡眠(Sleep)状态,直到捕获到(任意)信号为止。该函数的返回值始终为-1,并且调用结束后,错误代码(errno)会被置为EINTR。
sigsuspend函数:将进程(或线程)转入睡眠状态,直到接收到特定信号
函数声明如下:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
该函数调用后,会将进程的信号掩码临时修改(参数mask),然后暂停进程,直到收到符合条件的信号为止,函数返回前会将调用前的信号掩码恢复。该函数的返回值始终为-1,并且调用结束后,错误代码(errno)会被置为EINTR。
4.3 修改信号的响应动作
用户可以自己重新定义某个信号的处理方式,即前面提到的修改信号的默认响应动作,也可以理解为对信号的注册,可以通过signal或sigaction函数进行,这里以signal函数举例说明。
首先看一下函数声明:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数signum是信号值,可以从前面的信号列表中查到,第二个参数handler为处理函数,通过回调方式在信号触发时调用。
下面为示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/* 信号处理函数 */
void sig_callback(int signum) {
switch (signum) {
case SIGINT:
/* SIGINT: Ctrl+C 按下时触发 */
printf("Get signal SIGINT. \r\n");
break;
/* 多个信号可以放到同一个函数中进行 通过信号值来区分 */
default:
/* 其它信号 */
printf("Unknown signal %d. \r\n", signum);
break;
}
return;
}
/* 主函数 */
int main(int argc, char *argv[]) {
printf("Register SIGINT(%u) Signal Action. \r\n", SIGINT);
/* 注册SIGINT信号的处理函数 */
signal(SIGINT, sig_callback);
printf("Waitting for Signal … \r\n");
/* 等待信号触发 */
pause();
printf("Process Continue. \r\n");
return 0;
}
源文件下载:链接
例子中,将SIGINT信号(Ctrl+C触发)的动作接管(打印提示信息),程序运行后,按下Ctrl+C,命令行输出如下:
./linux_signal_example
Register SIGINT(2) Signal Action.
Waitting for Signal …
^CGet signal SIGINT.
Process Continue.
进程收到SIGINT信号后,触发响应动作,将提示信息打印出来,然后从暂停的地方继续运行。这里需要注意的是,因为我们修改了SIGINT信号的响应动作(只打印信息,不做进程退出处理),所以我们按下Ctrl+C后,程序并没有直接退出,而是继续运行并将"Process Continue."打印出来,直至程序正常结束。
https://github.com/ahui132/ahui132.github.io/blob/master/_posts/linux-c-signal.md
layout | title | category | description |
page | linux c 信号 | blog |
Preface
信号处理的一个经典场景:
- shell 启动一个进程
- 用户按下ctrl+c 键盘产生一个硬件中断
- 如果cpu 正在执行这个进程的代码, 则暂停这个进程,cpu 从用户态切换到内核态 处理这个硬件中断
- 内核态的终端处理程序将ctrl-c 解析为一个SIGINT 信号,记录到进程的PCB 中(也可以说是向进程发送了一个SIGINT 信号)
- 然后cpu 从内核态切换到进程态前,会检查到PCB 有无待处理的信号。本例中,cpu 会发现PCB 中有一个SIGINT 信号, 这个信号默认是终止程序执行,不必再返回用户空间
关于ctrl-c 产生的int 中断: 它只影响shell 前台进程(不带&, shell 被wait waitpid 所阻塞); 不影响shell 后台进程(带&)
信号与进程的关系:
- 它们是异步的(Asynchronous)
信号定义与分类
kill -l 查看系统定义的信号列表:
- SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
- SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
- SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
- SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
- SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
- SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
- SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
- SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
- SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
这些信号的宏定义位于signal.h, 编号34 以上的是实时信号,34以下的为非实时信号.
信号的产生与处理
signal(7) 有对signal 产生条件和默认动作 的说明:
Signal Value Action Comment
<span style="font-size:13px;font-family:monospace;color:#24292e"–<———————————————————————–
SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
signal generation, 信号的产生
一般地,信号产生的主要条件有:
- 从终端按下特殊键时,终端驱动程序会发送信号给前台进程:ctrl-C 产生 SIGHUP ,Ctrl-\ 产生SIGQUIT, Ctrl-Z 产生SIGTSTP
- 硬件异常产生信号,由硬件检测到并通知内核: 除0 异常产生SIGFPE; 非法访问内存地址,MMU 产生SIGSEGV 信号(段错误)
- kill(2) 函数将信号发送给进程
- kill(1) 调用kill(2) 发送信号,默认会发送SIGTERM 终止进程信号
- 内核检测到软件设定的条件发生时,产生信号: 如闹钟超时产生SIGALRM, 向读端关闭的管道写数据时产生SIGPIPE 信号
通过键盘产生
从终端按下特殊键时,终端驱动程序会发送信号给前台进程:ctrl-C 产生 SIGHUP ,Ctrl-\ 产生SIGQUIT, Ctrl-Z 产生SIGTSTP
调用系统函数向进程发送信号
使用kill(1) 或者 kill(2)/raise(2)向进程发送信号. 比如:
➜ test ./a.out &
[1] 9973
➜ test kill -SIGSEGV 9973; # 也可以写作kill -11 9973
➜ test
[1] + 9973 segmentation fault ./a.out
signal 库提供了很多向进程发送信号的函数:成功返回0, 错误返回-1
- kill 向指定进程发送信号;
- raise 向当前进程发送信号;
#include <signal.h> int kill(pid_t pid, int signo); int raise(int signo);
c 系统库提供了abort 函数,它向当前进程发送SIGABRT 信号(当前进程终止), 它和exit 函数一样,总会成功,没有返回值。
#include <stdlib.h>
void abort(void);
软件产生信号
SIGPIPE 是一种由软件产生的管理信号。此外,还有alarm 函数产生的SIGALRM 信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//The return value of alarm() is the amount of time left on the timer from a previous call to alarm(). If no alarm is currently set, the return value is 0.
它用于告诉内核seconds 秒后给当前进程发SIGALRM 信号(信号与进程是异步的),此信号默认的动作是终止当前进程(可用来防进程超时)。 此函数返回值是0, 或者前设定时钟的剩余秒数。
例子,通过alarm() 看下进程一秒内大概能数多少个数字
#include <stdio.h>
#include <unistd.h>
int main(void) {
//1 秒后产生SIGALRM 信号
alarm(1);
long i=0;
while(1) printf("%ld ", i++);
}
信号的处理动作
信号的默认处理动作是:
- Term 终止当前的进程
- Core 终止当前的进程 并且 Core Dump(进程被终止时,用户空间的内存数据全部都会被保存到磁盘上的core 文件。事后可以用调试检查器检查core 文件以理清错误原因,这叫做Post-mortem Debug)
- Ign 忽略
- Cont 继续先前停止的进程
可以通过调用sigaction(2) 告诉内核 信号对应的处理动作,主要有三种:
- 默认处理动作
- 忽略
- 提供自定义的处理函数(信号处理时,cpu 要从内核态切换到用户态 并执行这个自定义函数), 这种方式称为捕获(Catch) 一个信号
关于Core
Core 动作默认是不允许产生core 文件的, 可以通过ulimit 允许产生core 文件; 一个进程产生的core 大小取决于进程的Resource Limit(这个信息保存在PCB 中)
用ulimit 改变shell 进程的Resource Limit 为1024K, 子进程会继承这个设置:
$ ulimit -c 1024
产生Core Dump:
$ ./a.out (按Ctrl-C) $ ./a.out (按Ctrl-\)Quit (core dumped) $ ls -l core* -rw——- 1 akaedu akaedu 147456 2008-11-05 23:40 core
信号阻塞
信号有三个状态:
- Signal Generation(信号产生),
- 信号产生后信号就处于未决状态, Signal Pending(信号未决),
- 信号阻塞, 用于保持未决状态
- 进程正式处理信号, Signal Delivery(信号递达,信号的处理动作).
信号产生后会保持在Pending 状态,进程处理信号时可以选择阻塞信号(让信号继续保持在未决状态). 信号阻塞与信号忽略是不同的,信号忽略是信号递达(Delivery) 后的一种动作,而信号阻塞使信号继续保持在Pending 状态。
信号在内核中的表示 信号是存放于内核中task_struct 中进程控制块中的一个信号集: 每个信号有block 阻塞标志位,pending 标志位, handler 指针.
信号在内核中的示意图:
信号产生时,内核在进程控制块中设置该信号的未决标志(Pending),直到信号递达(Delivery)时才清除该标志. 在上图中:
- SIGHUP 信号未阻塞,也未产生
- SIGINT 信号阻塞,且已产生,当信号递达时,处理动作是忽略(Pending 仍然保持)
- SIGQUIT 信号是阻塞,但未产生,一旦信号产生它将保持在未决状态(阻塞),它的处理动作是用户空间的函数sighandler.
如果进程解除对某个信号的阻塞之前这种信号产生多次怎么办? POSIX.1 允许信号产生多次。linux 的实现是:
- 常规信号在递达之前产生多次的话,只计一次(参考以上信号在内核中表示的示意图);
- 实时信号在递达之前产生多次可以依次放在一个队列里面(不在本文讨论范围)
上图中的常规信号中,每个信号的阻塞和产生标志位用相同的数据类型 sigset_t 来存储,sigset_t 称为信号集,用于表示“有效”和“无效”两种状态。其中阻塞信号集也叫当前进程的信号屏蔽字(Signal Mask).
信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
#include <signal.h>
int sigemptyset(sigset_t *set);//初始化set 指向的信号集,使得其中所有的bit 清0, 表示该信号集不含任何有效信号。
int sigfillset(sigset_t *set);//使set 指向的信号集中所有的位置1, 表示该信号集的有效信号支持系统所有的信号。
int sigaddset(sigset_t *set, int signo);//在信号集中添加某有效信号
int sigdelset(sigset_t *set, int signo);//在信号集中删除某有效信号 //以上函数成功返回0,失败则返回-1
int sigismember(const sigset_t *set, int signo);//判断signo 是否为set 集中的有效信号。 //包含则返回1 不包含则返回0,出错则返回-1
Note: 在使用sigset_t 信号集之前,一定要调用sigemptyset/sigfillset 做初始化, 使得信号集处于确定的状态
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//成功返回0, 失败返回-1
*oset
如果oset 为非空指针,则读取当前进程的信号屏蔽字, 并用oset 传出
*set
如果set 为非空指针,则修改当前进程的信号屏蔽字,参数how 指示如何修改。 如果oset 与 set 都为非空,则先将原来的信号屏蔽字备份到oset, 然后根据set与how 修改信号屏蔽字。
假设当前进程的信号屏蔽字为mask, 下表则说明了how 参数的可选值:
SIG_BLOCK
set包含了我们希望添加到当前信号屏蔽字的信号,相当 于mask=mask|set
SIG_UNBLOCK
set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当 于mask=mask&~set
SIG_SETMASK
设置当前信号屏蔽字为set所指向的值,相当于mask=set
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其 中一个信号递达(内核态返回用户态时,会检查并处理信号)
sigpending
sigpending读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1
#include <signal.h>
int sigpending(sigset_t *set);
例子,阻塞SIGINT:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void printsigset(const sigset_t *set) {
int i;
for (i = 1; i < 32; i++){
if (sigismember(set, i) == 1)
putchar('1');
else
putchar('0');
}
puts("");
}
int main(void) {
sigset_t s, p;
sigemptyset(&s);
sigaddset(&s, SIGINT);
//阻塞s中的有效信号:即 SIGINT
sigprocmask(SIG_BLOCK, &s, NULL);
while (1) {
//取出未决信号集
sigpending(&p);
//打印未决信号集
printsigset(&p);
sleep(1);
}
return 0;
}
执行以上代码时,每秒钟都会打印一次未决信号集;ctrl-c 不生效的原因是SIGINT 被阻塞,阻碍用SIG_UNBLOCK 解除阻塞
$ ./a.out
0000000000000000000000000000000
0000000000000000000000000000000(这时按Ctrl-C)
0100000000000000000000000000000
0100000000000000000000000000000(这时按Ctrl-\)
Quit (core dumped)
信号捕获
内核如何捕获信号
如果信号的处理动作是自定义函数,在信号递达时内核就会调用这个自定义的函数。举例如下:
- 用户注册了SIGQUIT 信号的处理函数为sighandler
- 当前正在执行main 函数,这里发生中断或异常切换到内核态 (比如按下CTRL-)
- 中断处理完毕后要返回用户态的main 之前检查到有信号SIGQUIT 递达
- 内核决定返回用户态时执行sighandler, 而不是返回main 的上下文. sighandler 和 main 函数使用不同的堆栈空间,它们不存在调用和被调用的关系,是两个独立的控制流程。
- sighandler 返回后自动执行sigreturn 系统调用,再次进入内核态
- 如果没有信号递达,就再返回用户态恢复main 函数的上下文
sigaction
sigaction 用于读取与修改指定信号关联的处理动作。成功返回0, 失败返回-1.
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
params:
signo 信号
act,oact
act 为非空时, 则根据act 修改关联动作
oact 为非空时, 输出原有的关联动作
sigaction 结构体:
struct sigaction {
void (*sa_handler)(int); /* addr of signal handler, */
/* or SIG_IGN, or SIG_DFL */
sigset_t sa_mask; /* additional signals to block */
int sa_flags; /* signal options, Figure 10.16 */
/* alternate handler */
void (*sa_sigaction)(int, siginfo_t *, void *);
};
sigaction 结构体中sa_handler 的含义是:
- sa_handler 如果为常数 SIG_IGN 表示忽略信号
- sa_handler 如果为常数 SIG_DFL 表示使用默认系统动作
- sa_handler 如果为指针, 表示向内核注册了一个信号处理函数, 可以传一个int 参数(信号编号),这样同一样函数就可以处理多个信号了
sa_mask 的含义是, 当信号处理函数被调用时:
- 该信号被自动加入信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字(这保证了处理某种信号时,如果该信号再次产生,它将被阻塞到处理函数结束)
- 如果希望在处理函数调用期间,屏蔽其它信号, 则用sa_mask 字段说明额外屏蔽的信号,处理函数结束时自动恢复屏蔽关键字。
sa_flags 包含一些选项,本文将sa_flags 设置为0;sa_sigaction 是实时处理函数。本书不详细解释这两个字段。
pause
#include <unistd.h> int pause(void);
pause 使信号挂起,直到有信号递达(这类似于sleep 使进程休眠,直到有时间到达).
- 如果信号处理动作是终止进程,则pause 没有机会返回
- 如果信号处理动作是忽略,则进程继续挂起, pause 不返回
- 如果信号处理动作是捕捉,则调用信号处理函数后,pause 返回-1, errno 设置为ENTER(表示被信号中断)。所以pause 只有错误的返回值
可以用alarm 与 sleep 实现sleep:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void sig_alarm(int a){ } //执行此函数时SIGALRM 信号被屏蔽, 函数结束后解除对该信号的屏蔽。然后调用sig_return 再次进入内核,再返回用户碰碰继续执行主程序
unsigned int mysleep(int s){
struct sigaction newact, oldact;
unsigned int unslept;
//add sigaction
newact.sa_handler = sig_alarm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
alarm(s);//s 秒之后,闹钟超时,内核发SIGALRM给这个进程。
pause();//也可能是非SIGALRM 信号触发pause 终止,导致unslept 时间大于0
//recovery
sigaction(SIGALRM, &oldact, NULL);
//return
unslept = alarm(0);
return unslept;
}
int main(){
mysleep(3);
}
可重入函数 reentrant function
信号处理函数 和 主程序的控制流程是相互独立的,二者是异步的,不存在调用与被调用的关系,并且二者使用不同的堆栈空间。引入信号处理函数后,使用进程拥有多个控制流程。这会导致访问全局资源时,出现冲突。比如下图中,两函数同时操作链表时,出现混乱:
上例中,insert 函数执行还没有返回时,就被别的控制流程再次调用,这称为重入(reentrant).insert 访问全局链表时,可能因为重入而造成混乱,这样的函数被称为不可重入函数. 反之,如果一个函数在只访问自己的局部资源,则这样的函数就叫可重入函数(Reentrant function)
不可重入函数满足:
- 调用了malloc 或 free, 因为malloc 是用全局链表来管理维护的堆的。
- 调用了标准I/O 函数。因为标准I/O 函数都是以不可重入的方式使用的全局数据结构。
SUS规定有些系统函数必须以线程安全的方式实现(暂不详述)
线程会讲到如何保证一个代码段以原子操作完成
sig_atomic_t 与 volatitle 限定符
上面的例子中,main 和 sighanler 同时调用insert 函数时,导致链表错乱。根本原因时,链表的插入操作分两步完成,而非原子操作(线程节会讲解如何保证一个代码段以原子操作完成)
考虑一个问题,如果函数只有一行赋值操作,这个操作是不是原子型的呢?
long long a; int main(){ a=5; }
使用32位机编译后的结果是
objdump -dS a.out
a=5;
8048352: c7 05 50 95 04 08 05 movl $0x5,0x8049550
8048359: 00 00 00
804835c: c7 05 54 95 04 08 00 movl $0x0,0x8049554
8048363: 00 00 00
赋值语句被分成了两条汇编语句,因为32位的系统单条语句只能处理32位的数据。这种单条语句在32位机器中不是原子操作(main, sighandler 同是操作a 会产生错乱),但是在64位机器上却是原子操作。
为了保证各平台中, 程序对变量的读写是原子性的,c 标准定义了一个类型sig_atomic_t, 在不同平台的c 语言库中选择不同的类型。例如在32位平台机上定义sig_atomic_t 为int 类型(也就是说变量可能是32位,也可能是64位)
即使使用sig_atomic_t ,编译器做优化时也不会考虑对多个控制流程对全局变量的读写。比如:
sig_atomic_t a=0;
int main(void) {
/* register a sighandler */
while(!a);
/* wait until a changes in sighandler */
/* do something after signal arrives */
return 0;
}
编译后的反汇率代码如下,当信号处理函数将a修改为1时,循环就跳出。
/* register a sighandler */
while(!a); /* wait until a changes in sighandler */
8048352: a1 3c 95 04 08 mov 0x804953c,%eax
8048357: 85 c0 test %eax,%eax //对eax和eax做AND运算,若结果为0则跳回循环开头
8048359: 74 f7 je 8048352 <main+0xe>
如果gcc 在编译过程中做的优化(默认认为a的值不可变),编译的结果如下。 先将a 同0 比较,如果相同,就在第二句死循环了
8048352: 83 3d 3c 95 04 08 00 cmpl $0x0,0x804953c
/* register a sighandler */
while(!a); /* wait until a changes in sighandler */
8048359: 74 fe je 8048359 <main+0x15>
这是因为编译器认为没有其它控制流程修改a, 误认为a 值不可变的。这不是编译器的错,sigaction, pthread, pthread_create 这些控制流程都不是c 语言的规范,不归编译器管。不过,c 语言提供了volatile 限定符,表示这个变量可能被多个执行流程修改,优化时不能将变量的读写优化掉.
volatile sig_atomic_t a=0;
下列情况也是需要加sig_atomic_t:
- 内存单元不需要写操作(只读不写),就可以自己变化(如串口的接收寄存器)
- 内存单元不需要读操作(只写不读),但会被其它的机制读取(如串口的发送寄存器)
sig_atomic_t 总是要加上volatile 限定符的。因为两者都是为了防止全局资源在多控制流程下出现冲突。
竞态条件(Race Condition)与sigsuspend 函数
现在为mysleep 设想这样一种时序
- 首先为SIGALRM 设置自定义的信号处理函数
- alarm(nsecs) 设置了nsecs 秒后进程收到 SIGALRM 信号
- 假设此时比较倒霉,这时内核调度到其它优先级比较高的进程,而且这些进程需要执行很长的时间, 超过nsecs 秒
- 经过nsecs 秒后, 内核向该进程发送SIGALRM, 信号处于未决状态
- 又经过一些时间,优先级高的进程总算执行完了,内核调度到该进程. 此时检查到未决信号SIGALRM, 开始执行信号处理函数。
- 信号处理函数结束后,再次进入内核,然后才回到主进程, 调用pause()挂起等待。
- 因为SIGALRM 信号已经被处理了,再挂起pause 有什么用呢??
出现这个问题的原因是设置信号函数alarm(nsecs)与设置信号作用点pause(), 二者不是原子性的,可能导致执行pause时,alarm 所设定的超时信号已经提前结束了。 这种由于时序问题而导致的错误叫作竞态条件(Race Condition).
考虑一下解决方案1:
- 屏蔽SIGALRM信号;
- alarm(nsecs);
- 解除对SIGALRM信号的屏蔽;
- pause();
这个方案还无法避免 解除信号 到 pause 过程中发生SIGALRM
方案2 呢?更不行了,解除信号在pause 之后,SIGALRM 信号永远被阻塞了:
- 屏蔽SIGALRM信号;
- alarm(nsecs);
- pause();
- 解除对SIGALRM信号的屏蔽;
如果将解除屏蔽信号 与 pause 合并为原子操作就发好了?那就是sigsuspend 函数了: 挂起时,指定要临时解除的屏蔽信号. 它的返回与pause 是一样的. 返回时,恢复原来的信号屏蔽字
#include <signal.h> int sigsuspend(const sigset_t *sigmask);
考虑到Race Condition 后,重新实现mysleep :
unsigned int mysleep(unsigned int nsecs) {
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
/* set our handler, save previous information */
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
/* block SIGALRM and save current signal mask */
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
alarm(nsecs);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM); /* make sure SIGALRM isn't blocked */
sigsuspend(&suspmask); /* wait for any signal to be caught */
/* some signal has been caught, SIGALRM is now blocked */
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL); /* reset previous action */
/* reset signal mask, which unblocks SIGALRM */
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return(unslept);
}
关于SIGCHLD信号
我在 进程中提及过,wait和waitpid函数清理僵尸进程.
- 父进程阻塞等待子进程结束
- 父进程非阻塞,定时轮询子进程状态,子进程一旦结束就收尸
其实子进程在结束时,会向父进程发送SIGCHLD 信号,父进程可以:
- 为SIGCHLD 绑定信号处理函数,由信号处理函数调用wait 收尸
- 由于UNIX历史原因, 父进程也可以调用sigaction 将SIGCHLD 处理动作设置为SIG_IGN, 这样子进程终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction 定义的忽略是没有区别的,但这一个SIG_IGN是特例。此方法对linux 可用,但不保证在所有的UNIX 上可用。
参考