文章目录
1 Hello signal
1.1 hello signal 程序
如同你第一次学习 hello world 一样,这里写一个简单的 hello signal 程序。我的目的是希望借助 hello signal 程序来观察它对信号的反馈。
- 代码
// hellosignal.c
#include<stdio.h>
#include<unistd.h>
int main(){
printf("hello signal! I'm %d\n", getpid());
while(1){
write(STDOUT_FILENO, ".", 1);
sleep(1);
}
return 0;
}
这段代码启动后先打印自己的进程 id 号。接下来就每隔 10 秒的往屏幕打点号,非常简单。
- 编译
$ gcc hellosignal.c -o hellosignal
- 运行
$ ./hellosignal
屏幕打印:
hello signal! I'm 14578
......
接下来,hellosignal 先在屏幕上输出它的 id 号,然后就开始十分缓慢的打点了。
1.2 向 hello signal 发信号
(1) 使用 kill 命令
再启动一个终端,然后键入 kill -11 14578。hellosignal 报告结果如下:
hello signal! I'm 14578
.....................段错误 (核心已转储)
然后 hellosignal 就退出了。。。
接下来,你可以逐一发送信号1,2,3,4,5,⋯,311,2,3,4,5,⋯,31 给你的hellosignal,看看它做出什么反映?
如果你发送了某个信号(比如信号 19)导致 hellosignal 并没有结束,而是暂停了,你可以给它发送信号 18 让它重新恢复执行。
(2)使用键盘快捷键
上面是通过 kill 向 hellosignal 发信号,你也可以使用键盘快捷键来给 hellosignal 发信号,不过这种方式只能发送给“前台进程”。
键盘快捷键主要有 3 种:
- Ctrl + C (相当于发送信号 2, SIGINT)
- Ctrl + Z (相当于发送信号 20, SIGTSTP)
- Ctrl + \ (相当于发送信号 3,SIGQUIT)
(3)使用 linux 系统调用
当然啦,你也可以通过编写程序向 hellosignal 发信号。这样的函数有很多,比如 kill
函数,除此之外还有 alarm
函数等等。
我们的 hellosignal 太弱了,几乎一收到信号就挂。
1.3 招待你的“客人”
对于进程来说,信号就像它的客人。本篇学习一个ANSI C 规定的函数 signal,可以招待指定的客人。
1.3.1 signal 函数
- 函数原型
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 参数 handler
这个函数的第二个参数是函数指针。一般来说,参数里有函数指针的函数,称之为“注册”函数,而指针指向的那个函数,称之为“回调函数”。
所以 signal 函数可以称它为信号注册函数。你注册了你指定的函数,你的函数就可以被回调了。回调的意思是指不需要你亲自调用,而是由别人(一般来说是操作系统)调用。
系统为我们事先提供好的两个宏,分别是 SIG_DFL (default) 和 SIG_IGN (ignore)。如果 handler 被指定为 SIG_DFL,系统将为该信号指定默认的信号处理函数,如果 handler 被指定为 SIG_IGN,系统将忽略该信号。实际上在程序启动时,所有信号的处理函数都被指定为默认或者忽略。
注意:如果你需要指定自己编写的信号处理函数,你的函数格式必须为 void func(int) 这种形式,函数的名字可以随便,但是参数和返回值不能随便改。
- 参数 signum
signal 的第一个参数指示了你需要捕捉哪个信号。
- 返回值
signal 的返回值表示旧的信号处理函数。如果返回值等于 SIG_ERR 说明注册失败。
1.3.2 招待你的客人
下面这个例子的功能很简单,捕捉了SIGUSR1, SIGUSR2, SIGINT, SIGTSTP, SIGQUIT 以及 SIGSEGV 信号,sighandler 函数是自己编写的信号处理函数,这个函数必须以 void 返回,并且带一个 int 参数。
主函数主要做了三件事:
- 打印自己的进程 id 号
- 注册完所有你想捕捉的信号
- 每隔 10 秒打一个点。
// catchsignal.c
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
void sighandler(int sig){
switch(sig){
case SIGUSR1:
printf("hello SIGUSR1\n");break;
case SIGUSR2:
printf("hello SIGUSR2\n");break;
case SIGINT:
printf("休想干掉我!\n");break;
case SIGTSTP:
printf("不要停止我!\n");break;
case SIGQUIT:
printf("就是不退出!\n");break;
case SIGSEGV:
printf("呃!程序出bug了!\n");break;
default:
printf("hello, who are you %d\n", sig);
}
sleep(2);
}
int main(){
printf("I'm %d\n", getpid());
if (SIG_ERR == signal(SIGUSR1, sighandler))
perror("signal SIGUSR1");
if (SIG_ERR == signal(SIGUSR2, sighandler))
perror("signal SIGUSR2");
if (SIG_ERR == signal(SIGINT, sighandler))
perror("signal SIGTSTP");
if (SIG_ERR == signal(SIGTSTP, sighandler))
perror("signal SIGTSTP");
if (SIG_ERR == signal(SIGQUIT, sighandler))
perror("signal SIGQUIT");
if (SIG_ERR == signal(SIGSEGV, sighandler))
perror("signal SIGSEGV");
while(1){
write(STDOUT_FILENO, ".", 1);
sleep(10);
}
return 0;
}
编译
$ gcc catchsignal.c -o catchsignal
运行
$ ./catchsignal
程序运行起来后,你可以试试快捷键 Ctrl + C、Ctrl + Z 以及 Ctrl + \,看看你的进程有何反应。
你也可以再打开一个终端,使用 kill 命令发送信号给你的进程。
提示:kill -9 pid 可以终结你的进程。所有信号 9 被称为——终结者。
练习:
-
在上面的实验中,如果你连续发送相同的信号,会有什么结果?
答:只有部分信号被捕获处理,1-31 号信号是不可靠的,可能会丢失 -
把信号处理函数的 sleep 语句删除,再重新编译运行。当程序捕捉到信号后,是否还要等待 10 秒才会打点?
答:直接打点,信号会打断某些正在阻塞的函数,例如sleep -
最后,请你尝试捕捉 SIGKILL 和 SIGSTOP 信号,看看能否成功。(提示:有些客人招待下就没事了,有些客人很霸道,比如城管)。
答:无法捕获
2 标准信号及其不可靠性
1-31号,被规定为 standard signals,也就是标准信号。32-64号信号,被规定为 real-time signals,也就是实时信号。目前我们只关心标准信号,而不关心实时信号。
标准信号是不可靠的,不可靠的意思是如果同时来了很多相同的信号,而且还没来得及处理,这些相同的信号就会被合并成一个信号。实时信号就没有这个问题,只要来一次,就会处理一次。
2.1 不可靠是什么样子
这段代码的功能:main 函数生成 10 个子进程,每个子进程一生出就直接退出,只有一个子进程访问非法内存不正常退出。最后 main 函数每隔 10 秒在屏幕打点。
除此之外,这段程序注册了 SIGCHLD 信号处理函数。当有子进程状态发生改变时,会执行信号处理函数。信号处理函数主要就是 wait 子进程,并打印子进程的退出码或者打印子进程被何种信号终止或停止。最后信号处理函数会 sleep 1 秒钟。
// stdsig.c
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/wait.h>
#include<sys/types.h>
void waitchild(int sig){
int status;
pid_t pid;
if ((pid = waitpid(-1, &status, WUNTRACED | WCONTINUED)) > 0){
if (WIFEXITED(status)) {
printf("child %d exited! return code = %d\n\n", pid, WEXITSTATUS(status));
}
else if (WIFSIGNALED(status)) {
printf("child %d terminated by signal %d\n\n", pid, WTERMSIG(status));
}
else if (WIFSTOPPED(status)) {
printf("child %d stopped by signal %d\n\n", pid, WSTOPSIG(status));
}
else if (WIFCONTINUED(status)) {
printf("child %d continued\n\n", pid);
}
}
sleep(1);
}
void child(int n){
if (n == 9) *((int*)0) = 0;
exit(n+1);
}
int main(){
printf("I'm %d\n", getpid());
if (SIG_ERR == signal(SIGCHLD, waitchild)){
perror("signal SIGSTOP");
}
int n = 10;
pid_t pid;
while(n--){
pid = fork();
if (pid == 0){
child(n);
}
else if(pid == -1){
perror("fork");
}
}
while(1){
write(STDOUT_FILENO, ".", 1);
sleep(10);
}
return 0;
}
编译
$ gcc stdsig.c -o stdsig
运行
$ ./stdsig
1.1 结果分析
在我机器上运行的结果如下:
I'm 15280
.child 15289 exited! return code = 3
child 15281 terminated by signal 11
.....
你会很惊讶的发现,你只处理了 2 个子进程发来的信号,还有 8 个信号去哪了?另外,再打开一个终端,执行 ps a ,你会发现有一堆僵尸在那等着吃掉你的脑子。
实际上,在信号处理函数 waitchild 中还没来得及结束,新的 SIGCHLD 信号又来了,而操作系统对此的操作是将其和前一个 SIGCHLD 信号合并。
2.2 改进方案
弄清楚原因后,我们就知道在 waitpid 函数正在执行时,可能已经有多个子进程结束了。因此只需修改一处——将waitchild 的 if 判断改为 while 循环。即下面这样:
……
- if ((pid = waitpid(-1, &status, WUNTRACED | WCONTINUED)) > 0) { // 将这一行改成下面那一行
+ while ((pid = waitpid(-1, &status, WUNTRACED | WCONTINUED)) > 0) {
……
重新编译运行,发现所有子进程正常回收。
3 可重入函数
3.1 何为可重入
不妨看下面的一个函数。
int a = 0; // 全局变量
int fun() {
++a;
return a;
}
试想一下,当你在执行 fun() 函数的 return a 的时候(假设这时候 a 的值已经为 1),你的代码突然由于信号的打断而跳转到另一段代码运行。然而十分不巧的是,那段代码把 fun 函数执行了一遍(此时 a 的值已经变成了 2),当重新回到你的代码时,你的 fun 函数的返回值已经不再是你期望的 1,而是 2.
产生这种现象的本质在于,该函数引用了全局变量 a。
除此之外,使用静态局部变量也会出现这种问题。所以,我们把所有引用了全局变量或静态变量的函数,称为不可重入函数,不可重入函数都不是信号安全的,也不是线程安全的。
有一点需要注意的是,如果一个函数使用了不可重入函数,那么该函数也会变成不可重入的。这意味着,你不能在信号处理函数中使用不可重入函数。
有很多 C 库函数和 linux 系统调用都是不可重入的,比如 malloc、getpwdnam。很多标准库的 IO 函数都是不可重入的,因为这些函数使用了缓冲区。
3.2 不可重入导致的 bug
下面以实例说明在信号处理函数中使用不可重入函数带来的危害。
该段程序使用 getpwdnam 根据用户名获取用户 uid。在 main 函数中,getpwdnam 执行完后即进入 sleep 状态。另外该段程序注册了信号 SIGINT,待会我们在键盘键入Ctrl + C 命令以及什么都不做的情况下,看看屏幕打印的 uid 是多少。
// reenterable.c
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<pwd.h>
#include<stdio.h>
void handler(int sig){
getpwnam("root");
}
int main(){
if (SIG_ERR == signal(SIGINT, handler)){
perror("signal");
return 1;
}
printf("I'm %d\n", getpid());
struct passwd *pwd = getpwnam("skx");
sleep(10);
printf("skx's pid = %d\n", pwd->pw_uid);
return 0;
}
- 编译
$ gcc reenterable.c -o reenterable
- 运行
$ ./reenterable
当你运行程序后,什么也不做,等待 10 秒后,结果显示:
I'm 15355
skx's pid = 1000
当程序在打印 uid 前,如果你按下 Ctrl + C,屏幕会打印:
I'm 15357
^Cskx's pid = 0
这不符合我们的预期,uid = 0 明明是 root 账号的 uid 才对。主要原因是在 main 函数 sleep 的时候,我们向程序发送了 SIGINT 信号,程序即转入信号处理函数 handler 又执行了一次 getpwnam。
4 发送信号
4.1 使用 kill 函数发送信号
一直以来,我们给程序发送信号都是通过快捷键(Ctrl+C, Ctrl+Z, Ctrl+) 或者使用系统命令kill来向进程发送信号(还有一种是系统给你发送的信号,比如 SIGCHLD、SIGSEGV 等等)。除此之外,也可以通过函数 int kill(pid_t pid, int sig) 来向进程发送信号。
4.1.1 kill 函数原型
int kill(pid_t pid, int sig);
-
参数 pid
-
参数 pid
-
pid > 0,表示向进程号为 pid 的进程发信号
-
pid = 0,表示向同组进程发信号(有权限才行)
-
pid < -1,向进程组 |pid||pid| 发信号(有权限才行)
-
pid = -1,向所有进程发信号(有权限才行,早期的 POSIX 并未定义此种情况)
所以,最常用的就是 pid > 0 的情况了,其它情况暂时还不会,因为你还不懂什么是进程组,就此略过,等讲到进程组的概念的时候,自然就懂了。
- 参数 sig
表示向进程发送什么信号。如果 sig 为 0 ,通常用来测试是否有权限向进程发信号。
4.1.2 kill 使用实例
// mykill.c
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<wait.h>
#include<stdio.h>
#include<stdlib.h>
void handler(int sig){
if (sig == SIGUSR1){
printf("Hello my child\n");
}
else if(sig == SIGCHLD){
wait(NULL);
}
}
void child(){
int n = 10;
while(n--){
if (kill(getppid(), SIGUSR1) == -1){
perror("kill");
}
sleep(1);
}
exit(0);
}
int main(){
if (SIG_ERR == signal(SIGUSR1, handler)){
perror("signal SIGUSR1");
}
if (SIG_ERR == signal(SIGCHLD, handler)){
perror("signal SIGUSR2");
}
pid_t pid = fork();
if (pid == 0)child();
else if(pid == -1){
perror("fork");
}
while(1){
write(STDOUT_FILENO, ".", 1);
sleep(10);
}
return 0;
}
- 编译并运行
$ gcc mykill.c -o mykill
$ ./mykill12
- 结果
.Hello my child
.Hello my child
.Hello my child
.Hello my child
.Hello my child
.Hello my child
.Hello my child
.Hello my child
.Hello my child
.Hello my child
............
- 结果分析
如果父进程收到子进程发来的信号,父进程在信号处理函数里打印 Hello my child。值得注意的是,每次父进程打印完Hello my child 后就会立即打印一个点,原因:号会中断父进程的 sleep 函数。
4.2 启动定时炸弹 alarm
除了能够使用 kill 函数发信号,再学习一个很重要的信号发送函数 alarm。该函数不像 kill 函数可以给其它进程发送信号,它只能给自己发信号。另外,它只能发送 SIGALRM 信号。
有同学会问,那要它何用?还不如用 kill 函数呢。其实,alarm 的强大之处在于它可以做到多少秒后发送消息——感觉像极了定时炸弹。
4.2.1 alarm 函数
unsigned int alarm(unsigned int seconds);
alarm 的参数用来设定多少秒后向本进程发送 SIGALRM 信号。
返回值表示上一次设定的定时炸弹还剩下多少秒的时间会爆炸(同时会取消上一次没来得及爆炸的定时炸弹)。
4.2.2 alarm 实例
下面这段程序设定了两次定时炸弹,第一次设定 5 秒后爆炸,设定后过了 2 秒,再设定了一个 3 秒后爆炸的定时炸弹。
// myalarm.c
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
void handler(int sig){
if (sig == SIGALRM)printf("Bomb!!!!!!!!\n");
}
int main(){
if (SIG_ERR == signal(SIGALRM, handler)){
perror("signal SIGALRM");
}
unsigned int remain = 0;
remain = alarm(5);
printf("the previous alarm remain %d seconds\n", remain);
sleep(3);
remain = alarm(3);
printf("the previous alarm remain %d seconds\n", remain);
while(1){
write(STDOUT_FILENO, ".", 1);
sleep(5);
}
}
- 编译和运行
$ gcc myalarm.c -o myalarm
$ ./myalarm
- 结果
the previous alarm remain 0 seconds
the previous alarm remain 2 seconds
.Bomb!!!!!!!!
.
5 装信号的容器 sigset
5.1 sigset_t
POSIX 定义一个数据结构 —— sigset_t,本质上它是一个 64 位宽度的整数。
这是一个集合,每种信号只会在这个集合里出现一次。前面你已经知道,信号的编号是从1 到 64 这个范围。而 sigset_t 正好是一个 64 位整数,这正好可以让每一个比特位表示一个信号。所以有时候 sigset_t 也被称为信号位图。
5.2 操作 sigset_t 的函数
既然 sigset_t 是一种数据结构,那就有操作这种数据结构的一系列函数了。操作 sigset_t 的函数主要有 5 个。
- sigemptyset (清空集合,所有比特位置 0)
- sigfillset (填充集合,所有比特位置 1)
- sigaddset (添加信号到集合,将某一比特位置 1)
- sigdelset (删除某个信号,将某一比特位置 0)
- sigismember (是否存在某个信号,判断某一比特位是否为 1)
这些函数的功能都非常简单易懂,后面我用例子来说明。
5.3 实例
(1)打印 sigset_t 的函数
void printsigset(const sigset_t *set)
{
for (int i = 1; i <= 64; i++) {
if (i==33) putchar(' ');
if (sigismember(set, i) == 1)
putchar('1');
else
putchar('0');
}
puts("");
}
(2)验证 sigset_t 是64位整数
unsigned int test[2] = {0xf0f0f0f0, 0xf0f0f0f0};
printsigset((sigset_t*)test);
打印后的结果为:
00001111000011110000111100001111 00001111000011110000111100001111
(3)建一个 sigset_t
sigset_t st;
printsigset(&st);
(4) 填充 sigset_t
sigfillset(&st);
printsigset(&st);
结果:
11111111111111111111111111111110 01111111111111111111111111111111
上面的结果发现有 2 个比特位为 0,他们分别是信号 32 和 33 对应的位置,你可以通过 kill -l 查看下,为什么出现这样的结果。
(5)清空 sigset_t
sigemptyset(&st);
printsigset(&st);
结果:
00000000000000000000000000000000 00000000000000000000000000000000
清空集合后,所有的比特位都被置 0 了。
(6)添加信号
sigaddset(&st, SIGHUP);
sigaddset(&st, SIGINT);
sigaddset(&st, SIGKILL);
sigaddset(&st, SIGSYS);
sigaddset(&st, SIGRTMIN);
sigaddset(&st, SIGRTMAX);
printsigset(&st);
上面这段代码一共添加了 6 个不同的信号。
结果:
11000000100000000000000000000010 01000000000000000000000000000001
可以发现信号对应的比特位都被置 1 了
可以发现信号对应的比特位都被置 1 了
(7)删除信号
sigdelset(&st, SIGKILL);
printsigset(&st);
这里只删除了信号 9(SIGKILL).
结果:
11000000000000000000000000000010 01000000000000000000000000000001
发现第 9 个比特位被置 0了。
(8)判断集合里是否存在某个信号
if (sigismember(&st, SIGKILL)) {
printf("SIGKILL is member\n");
}
if (sigismember(&st, SIGINT)) {
printf("SIGINT is member\n");
结果:
SIGINT is member
因为 SIGKILL 刚刚被删除了,所以这里只打印了 SIGINT.
(9)完整代码
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void printsigset(const sigset_t *set)
{
int i;
for (i = 1; i <= 64; i++) {
if (i==33) putchar(' ');
if (sigismember(set, i) == 1)
putchar('1');
else
putchar('0');
}
puts("");
}
int main() {
sigset_t st;
printf("1. create set\n");
printsigset(&st);
printf("\n2. vertify sigset_t is a 64-bit integer\n");
unsigned int test[2] = {0xf0f0f0f0, 0xf0f0f0f0};
printsigset((sigset_t*)test); // 这种方法不被推荐,仅供测试用。
// fill set
printf("\n3. fill set\n");
sigfillset(&st);
printsigset(&st);
// empty set
printf("\n4. empty set\n");
sigemptyset(&st);
printsigset(&st);
// add sig
printf("\n5. add SIGHUP(1), SIGINT(2), SIGKILL(9), SIGSYS(31), SIGRTMIN(34) and SIGRTMAX(64) to set\n");
sigaddset(&st, SIGHUP);
sigaddset(&st, SIGINT);
sigaddset(&st, SIGKILL);
sigaddset(&st, SIGSYS);
sigaddset(&st, SIGRTMIN);
sigaddset(&st, SIGRTMAX);
printsigset(&st);
// delete sig
printf("\n6. delete SIGKILL from set\n");
sigdelset(&st, SIGKILL);
printsigset(&st);
// is member
printf("\n");
if (sigismember(&st, SIGKILL)) {
printf("SIGKILL is member\n");
}
if (sigismember(&st, SIGINT)) {
printf("SIGINT is member\n");
}
return 0;
}
6 阻塞信号与未决信号
有时候,你并不希望你的进程处理信号。比如接收到 SIGINT 后对它置之不理。linux 提供了一个函数 sigprocmask 来帮助我们实现此功能。
在一个进程中,保存了两个信号集(在PCB中),分别是阻塞信号集,还有一个未决信号集。当你使用 sigprocmask 的时候,就会修改阻塞信号集。
如果一个信号加入阻塞信号集,该信号的信号处理函数就不会被调用。
6.1 sigprocmask 函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
该函数不仅可以阻塞你指定的信号,也可以将之前阻塞的信号撤销。具体是通过 how 参数来控制的。
参数及返回值
-
how 参数
- SIG_BLOCK 该选项表示将 set 参数指示的信号集中的信号添加到进程阻塞集中
- SIG_UNBLOCK 该选项与功能 SIG_BLOCK 相反,表示将进程阻塞信号集中指定的信号删除
- SIG_SETMASK 该选项表示将进程阻塞信号集直接设定为你指定的 set
-
set 参数
表示你指定的信号集合 -
oldset
返回旧的阻塞信号集 -
返回 int
0 表示成功,-1 失败。
6.2 未决信号
说的通俗点,未决信号,就是你的进程已经接收到了信号了,只是还没被信号处理函数处理的那些信号。
特别说明:虽然未决信号的定义是上面这样,但是这里我们需要更加具体一点,未决信号特指进程收到且被阻塞的信号。
当你的进程一收到信号且该信号被阻塞,它首先进入到未决信号集中(就是一个 sigset_t),当未决信号集中的信号被信号处理函数(你自己定义的或者系统默认的)处理,就会从未决信号集中删除。
你可以使用 sigpending 函数获取未决信号集。它的函数原型如下:
int sigpending(sigset_t *set);
使用起来也是相当简单。
6.3 实例
该程序的功能是先把 SIGINT、SIGTSTP 加入到了进程阻塞信号集中去。接下来,每隔一秒打印一次未决信号集,第 10 次的时候,又把 SIGINT 信号从阻塞信号集中删除。
// sigblock.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
void printsigset(const sigset_t *set)
{
for (int i = 1; i <= 64; i++) {
if (i==33) putchar(' ');
if (sigismember(set, i) == 1)
putchar('1');
else
putchar('0');
}
puts("");
}
void handler(int sig) {
if (sig == SIGINT) printf("hello SIGINT\n");
if (sig == SIGQUIT) printf("hello SIGQUIT\n");
}
int main() {
printf("I'm %d\n", getpid());
sigset_t st, oldst;
sigemptyset(&st);
sigaddset(&st, SIGINT);
sigaddset(&st, SIGTSTP);
sigprocmask(SIG_BLOCK, &st, &oldst);
printf("new set:");
printsigset(&st);
printf("old set:");
printsigset(&oldst);
if (SIG_ERR == signal(SIGINT, handler)) {
perror("signal SIGINT");
return 1;
}
if (SIG_ERR == signal(SIGQUIT, handler)) {
perror("signal SIGQUIT");
return 1;
}
puts("");
int n = 0;
while(1) {
sigpending(&st);
printsigset(&st);
puts("");
sleep(1);
if (n == 10) {
sigset_t tmp;
sigemptyset(&tmp);
sigaddset(&tmp, SIGINT);
sigprocmask(SIG_UNBLOCK, &tmp, NULL);
}
++n;
}
return 0;
}
编译和运行
$ gcc sigblock.c -o sigblock
$ ./sigblock
运行过程中,按下 Ctrl + C 和 Ctrl + Z 程序都没反应,但是可以看到未决信号集中这两个信号比特位被置位。10 秒后,SIGINT 从阻塞信号集中被删除,即使没有按下 Ctrl + C 发送信号,信号处理函数也被执行,这之后发现未决信号集中的 SIGINT 比特位被置 0.
运行过程中,按下 Ctrl + C 和 Ctrl + Z 程序都没反应,但是可以看到未决信号集中这两个信号比特位被置位。10 秒后,SIGINT 从阻塞信号集中被删除,即使没有按下 Ctrl + C 发送信号,信号处理函数也被执行,这之后发现未决信号集中的 SIGINT 比特位被置 0.
结果
I'm 5939
new set:01000000000000000001000000000000 00000000000000000000000000000000
old set:00000000000000000000000000000000 00000000000000000000000000000000
00000000000000000000000000000000 00000000000000000000000000000000
00000000000000000000000000000000 00000000000000000000000000000000
^C01000000000000000000000000000000 00000000000000000000000000000000 // 此处按下了 ctrl c
01000000000000000000000000000000 00000000000000000000000000000000
^Z01000000000000000001000000000000 00000000000000000000000000000000 // 此处按下了 ctrl z
^\hello SIGQUIT // 此处按下了 ctrl \
01000000000000000001000000000000 00000000000000000000000000000000
01000000000000000001000000000000 00000000000000000000000000000000
01000000000000000001000000000000 00000000000000000000000000000000
01000000000000000001000000000000 00000000000000000000000000000000
01000000000000000001000000000000 00000000000000000000000000000000
01000000000000000001000000000000 00000000000000000000000000000000
hello SIGINT // 执行到这里 SIGINT 从阻塞信号集中被删除
00000000000000000001000000000000 00000000000000000000000000000000
7 新的信号注册函数 sigaction
7.1 sigaction 函数
相比于 signal 函数,sigaction 提供了更多的功能。它可以处理带参数的信号,在信号处理的时候,可以屏蔽其它信号等等。
函数原型
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 参数 signum :要捕获的信号。
- 参数 act:struct sigaction 结构体,它保存了信号处理函数指针等等,后面具体讲解。
- 参数 oldact:返回旧的 struct sigaction 结构体。
- 返回 0 成功,-1 失败
struct sigaction 结构体
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
-
sa_handler : 不带附加参数的信号处理函数指针
-
sa_sigaction: 带有附加参数的信号处理函数指针(两个信号处理函数指针只能二选一)
-
sa_mask: 在执行信号处理函数时,应该屏蔽掉哪些信号
-
sa_flags: 用于控制信号行为,它的值可以是下面选项的组合。
- SA_NOCLDSTOP : 当捕获 SIGCHLD 时,不接收子进程停止的通知。
- SA_NOCLDWAIT:当捕获 SIGCHLD 时,收子进程在退出时不变成僵尸进程。
- SA_NODEFER:当该信号处理函数执行时,不阻塞该信号。
- SA_ONESTACK:在指定的栈(signaltstack 函数指定)上执行信号处理函数。
- SA_RESETHAND:在进入信号处理函数入口点处恢复该信号的处理函数为默认函数。
- SA_RESTART:由此信号中断的系统调用是否要再启动
- SA_SIGINFO:如果指定该选项,则向信号处理函数传递参数(这时应该使用 sa_sigaction 成员而不是 sa_handler).
-
sa_restorer:该成员在早期是用来清理函数栈的,如今已被废弃不用。
sa_flags 的选项比较多,大部分可又自己做实验验证,有些是需要额外的知识,比如 SA_ONESTACK 和 SA_RESTART,这些放到后面讲解。本节示例中,只需要把 sa_flags 设置为 0 即可。
7.2 实例
下面的程序演示了 sigaction 函数的用法,程序注册了信号 SIGINT 和 SIGTSTP。需要注意的一点是 sa_mask 被设置为 SIGINT,它表示当执行信号处理函数的时候,阻塞信 SIGINT 信号。在 handler 函数加入了一打印未决信号的功能,以验证执行到 handler 的时候发送 SIGINT 是被阻塞住的。
- 代码
// sigaction.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void printsigset(const sigset_t *set)
{
for (int i = 1; i <= 64; i++) {
if (i==33) putchar(' ');
if (sigismember(set, i) == 1)
putchar('1');
else
putchar('0');
}
puts("");
}
void handler(int sig) {
if (sig == SIGTSTP) printf("hello SIGTSTP\n");
if (sig == SIGINT) printf("hello SIGINT\n");
sleep(5);
sigset_t st;
sigpending(&st);
printsigset(&st);
}
int main() {
printf("I'm %d\n", getpid());
struct sigaction act, oldact;
act.sa_handler = handler; // 设置普通信号处理函数
// 向 sa_mask 中添加 SIGINT
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGINT);
act.sa_flags = 0; // 先置 0
sigaction(SIGTSTP, &act, &oldact);
sigaction(SIGINT, &act, &oldact);
while(1) {
write(STDOUT_FILENO, ".", 1);
pause();
}
return 0;
}
- 编译和运行
$ gcc sigaction.c -o sigaction
$ ./sigaction
-
当程序运行的时候,
Ctrl C
进入 handler,然后立即Ctrl Z
发现 handler 还未执行完就被 SIGTSTP 打断. -
当程序运行的时候,
Ctrl Z
进入 handler,然后立即Ctrl C
发现并不会被 SIGINT 打断,这是因为该 handler 注册的时候被设置了 SA_MASK = SIGINT。最后 handler 结束的时候打印了未决信号集,发现里头有 SIGINT。所以 handler 结束后,又去继续对 SIGINT 进行处理。
注意,sa_mask 的含义是 “在执行信号处理函数时,应该屏蔽掉哪些信号”。
7.3 带参数的信号
前面只注册了带一个参数的信号处理函数 void handler(int sig)。我们也可以使用带参数的的信号处理函数。sigaction 可以通过第二个参数 struct sigaction 结构体来指定带附加参数的信号处理函数。
7.3.1 如何给信号处理函数传递参数
在成员 sa_flags 上加上选项——SA_SIGINFO。
需要注意的是,sa_flags 加上选项 SA_SIGINFO 的含义仅仅是表明:在处理信号的时候,会附带一个 siginfo_t* 类型的参数。它并不表明使用该选项了就必须得 sa_sigaction 成员赋值,换句话说,即使你使用不带参数的信号处理函数,你也可以给 sa_flags 加上 SA_SIGINFO 选项。只不过这样做没什么用。
同样的,就算你不指定 SA_SIGINFO 选项,你也一样可以使用带附加参数的 sa_sigaction。
总之一句话,SA_SIGINFO 仅仅表示在处理信号的时候会附加一个 siginfo_t* 类型的参数(至于你用哪种信号处理函数,无所谓)。
7.3.2 sa_sigaction 成员
sa_sigaction 成员是一个函数指针。它指示的函数原型必须是下面这样:
void fun(int sig, siginfo_t *siginfo, void *context); // 函数是什么名字无所谓
该函数的第一个参数表示处理的是哪个信号,第二个参数是一个结构体,第三个参数实际上类型为 ucontext_t 类型的指针,使用的时候应该把 void* 转换为 ucontext_t*,它表示发送进程在发送信号时的上下文,这个参数目前来说没什么用,我们不用理会。
7.3.3 struct siginfo_t 结体体
这个结构体成员众多,不过我们仅仅关心其中的三个值。
- 简化版
struct siginfo_t {
pid_t si_pid; /* 发送信号的进程 ID */
uid_t si_uid; /* 发送信号的进程实际用户 ID */
sigval_t si_value; /* 附加参数(联合体) */
int si_int; /* 实际上这个参数的值就是 si_value,他们相等 */
void *si_ptr; /* 同上 */
}
union sigval_t {
int sival_int;
void *sival_ptr;
};
如果一来,我们需要关心的成员数量就减少很多了,便于学习。值得注意的是 si_value 成员、si_int 和 si_ptr 这几个成员,这些成员实际上由用户在发送信号的时候传递的。而且,si_value 的值,和 si_int,si_ptr 的值是完全一致的,后面的实验可以验证。
- 完整版的 siginfo_t
struct siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
int si_band; /* Band event */
int si_fd; /* Fle descriptor */
}
7.3.4 新的信号发送函数 sigqueue
可能你比较关心的是用户如何发送信号的时候传递参数到 siginfo_t 结构体中的 si_value?我们学习的 kill 函数并不支持这个功能啊?没关系,linux 系统提供了另一个信号发送函数 sigqueue 帮助我们解决这个问题,它的原型如下:
int sigqueue(pid_t pid, int sig, const union sigval value);
它的用法和 kill 函数一样,只不过多一个参数,上面这个参数类型 sigval 其实就是 sigval_t,没有任何区别。
union sigval {
int sival_int;
void *sival_ptr;
};
7.3.5 实例
下面一共有两个程序,分别是 a 和 b。
程序 a 的功能就是使用带附加参数的信号处理函数,然后打印所有附加参数的值。
程序 b 的功能就是给程序 a 发信号,同时附带一个整数。
- 程序 a
// a.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void handler(int sig, siginfo_t* siginfo, void* context) {
if (sig == SIGQUIT) printf("hello SIGQUIT\n");
if (siginfo) {
printf("si_signo = %d\n", siginfo->si_signo);
printf("si_errno = %d\n", siginfo->si_errno);
printf("si_code = %d\n", siginfo->si_code);
// printf("si_trapno = %d\n", siginfo->si_trapno); 这个成员依赖于架构
printf("si_pid = %d\n", siginfo->si_pid);
printf("si_uid = %d\n", siginfo->si_uid);
printf("si_status = %d\n", siginfo->si_status);
printf("si_utime = %ld\n", siginfo->si_utime);
printf("si_stime = %ld\n", siginfo->si_stime);
printf("si_value{\n");
printf("\tsival_int = %08x(%d)\n", siginfo->si_value.sival_int, siginfo->si_value.sival_int);
printf("\tsival_ptr = %p\n", siginfo->si_value.sival_ptr);
printf("}\n");
printf("si_int = %08x(%d)\n", siginfo->si_int, siginfo->si_value.sival_int);
printf("si_ptr = %p\n", siginfo->si_ptr);
printf("si_overrun= %d\n", siginfo->si_overrun);
printf("si_timerid= %d\n", siginfo->si_timerid);
printf("si_addr = %p\n", siginfo->si_addr);
printf("si_band = %ld\n", siginfo->si_band);
printf("si_fd = %d\n", siginfo->si_fd);
}
printf("---------------------------------------------\n");
}
int main() {
printf("I'm %d\n", getpid());
struct sigaction act;
act.sa_sigaction = handler; // 使用带附加参数的信号处理函数
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO; // 发送的信号带参数
sigaction(SIGQUIT, &act, NULL);
while(1) {
write(STDOUT_FILENO, ".", 1);
pause();
}
return 0;
}
- 程序b
// b.c
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
if (argc < 2) printf("usage: %s <pid>\n", argv[0]);
pid_t pid = atoi(argv[1]);
union sigval val;
while(1) {
scanf("%d", &val.sival_int);
if (sigqueue(pid, SIGQUIT, val) < 0) { // 发送带附加值的信号
perror("sigqueue");
}
}
}
编译与运行
$ gcc a.c -o a
$ gcc b.c -o b
先运行程序 a
$ ./a
屏幕打印:
I'm 18519
.
运行程序 b
再开启一个终端,键入:
$ ./b 18519
这时候,再键入任意数字
1234
结果
程序 a 会打印:
I'm 18519
.hello SIGQUIT
si_signo = 3
si_errno = 0
si_code = -1
si_pid = 18531
si_uid = 0
si_status = 1234
si_utime = 0
si_stime = 0
si_value{
sival_int = 000004d2(1234)
sival_ptr = 0x7ffe000004d2
}
si_int = 000004d2(1234)
si_ptr = 0x7ffe000004d2
si_overrun= 0
si_timerid= 18531
si_addr = 0x4863
si_band = 18531
si_fd = 1234
---------------------------------------------
.si_int
8 打通你的任督二脉-信号处理函数的执行期
8.1 系统调用
当你使用系统调用的时候,一般就意味着你要进入操作系统内核了。比如 write 函数,read 函数,再比如 sigaction 函数等等。
基本上你调用大多数系统调用的时候,都会发生进程或线程调度。所谓的调度就是进入某些系统调用后,因为某种原因你的进程就暂停了,CPU 转而执行另一个进程(这只是调度的一种,另外一种是因为时间片耗尽而发生调度)。
当你的系统调用请求的资源已经完成的时候(也可能还没完成,比如被信号打断),CPU 就会有机会回来再次执行你的程序了。
进程 A 在执行系统调用后比如 read,因为发现想要的资源还没到来(用户迟迟不输入数据),于是进程 A 执行调度程序,主动让出了 CPU,切换到了进程B。
8.2 系统调用如何进退内核
系统调用通常是通过 cpu 提供的指令进入内核的。这需要 cpu 硬件的支持!
在老的系统中,x86 cpu 是通过 int 中断指令进入内核态的(这个 int 不是 c 语言里那个 int 整型,而是 cpu 汇编指令)。内核态,指的是 cpu 的状态,不是操作系统的状态,处于内核态的 cpu 可以执行一切特权指令,读写任意内存(说任意有点夸张,起码要读写的地方应该被挂上物理页才行)。比如 linux 系统是通过 int 0x80 中断指令让 cpu 变成内核态。
现代操作系统都使用了 x86 的另一个指令称为 sysenter 指令进入内核,该指令的速度比中断指令更快。
介绍了进入内核,当然要谈谈如何退出内核。与 int 中断指令对应的是,必须使用 iret 指令让 cpu 退出内核态,进入用户态。与 sysenter 指令对应的退出指令是 sysexit.
8.3 信号处理函数是如何被调用的
同样假设系统里只有两个进程。该执行流程参考了 linux 0.11 内核代码(高版本的linux实现可能与此有些出入,但是大同小异。简单的掌握了,才有信心去研究高版本源码是不?)。
下面具体分析下执行流程:
-
进程 A 执行到 read 里的时候(题外话,本质上 read 函数只是个外壳而已,真正的系统调用是 sys_read,这已经位于内核了),然后执行了中断指令 int 0x80,进入到了内核态。
-
接下来执行真正的系统调用 sys_read,由于请求的资源没有到来,sys_read 调用了 schedule 函数,这是真正的进程调度函数。进程调度函数执行到某个位置后,找到了下一个要运行的进程(进程 B),然后切换进程上下文,转而执行进程 B。
-
进程 B 运行到某个位置,也发生了和进程 A 一样的遭遇,也调用了某个系统调用而阻塞而切换到了进程 A
注意:如果进程 A 的资源没有准备好,同时进程 A没有收到信号是不会被调度到的。只要进程 A 收到信号了,同时该信号未被阻塞,进程 A 就会被调度到,资源没有准备好也没关系。
-
当再次回到进程 A 的时候,进程 A 接着从 schedule 函数切到进程 B 那行代码的下一行继续执行,完成 sys_read 的功能。sys_read 完成后,准备退出内核态。
-
在退出内核态前,会执行 do_signal 函数检查是否有信号,如果有信号 do_signal 会找到信号对应的信号处理函数(你使用 signal 或者 sigaction 注册的)。
-
接下来执行 do_signal
-
如果进程没有收到信号,do_signal 函数直接返回,执行 iret 后直接返回到 read 函数。图中没有画线条,因为这种情况比较简单。
-
执行如果进程收到了信号,do_signal 函数会修改用户栈,为信号处理函数创建一个函数执行现场,这将导致系统调用在执行 iret 函数的时候不是返回到 read 函数,而是返回到信号处理函数!!!
-
do_signal 函数创建完对应的信号处理函数的执行现场后,执行 iret,返回到了信号处理函数。信号处理函数执行完后,才返回到 read 函数。
-
接下来,read 函数返回。
实际上,系统进程内核态,除了执行系统调用,也可以通过时钟中断进入内核。只不过这种方式是被动的。进入时钟中断后,会检查进程时间片是否用完,如果用完,照样会执行 schedule 调度程序转移到别的进程。
9 中断系统调用与自动重启动
经历了大量的代码实践,每每我们在 main 函数中都有这么类似的一句:
while(1) {
write(STDOUT_FILENO, ".", 1);
sleep(...); // read(...), pause(...)
}
有时候,只要发现信号一来,这后面的 sleep 或者 pause 被信号中断后都会失效。不过你还没见过 read 也失效的情况,那是因为之前我们一直用的 signal 信号注册函数。或者说,signal 默认情况下设置了自动重启动属性。
9.1 低速系统调用与其它系统调用
下面这段话引用片 man page:
read(2), readv(2), write(2), writev(2), and ioctl(2) calls on “slow” devices. A “slow” device is one where the I/O call may block for an indefinite time.
意思是说,read, readv, write, writev 和 ioctl 被称为“低速”设备,所谓的“低速”设备,是指I/O 调用可能会被永远阻塞。
for example, a terminal, pipe, or socket. If an I/O call on a slow device has already transferred some data by the time it is interrupted by a signal handler, then the call will return a success status (normally, the number of bytes transferred).
例如,终端,管道或者套接字。如果低速设备上的 I/O 调用正在传输数据的过程中被信号打断,则返回传输的字节数。
Note that a (local) disk is not a slow device according to this definition; I/O operations on disk devices are not interrupted by signals.
需要注意的是:根据定义,本地磁盘不是慢设备!磁盘设备上的 I/O 操作是不会被信号打断的!
对于上面这句,APUE 给的解释是这样的:
虽然读写磁盘文件可能会暂时阻塞调用者(磁盘将驱动程序将请求保存到队列,最后会在适当的时期执行该请求),除非发生硬件错误,否则 I/O 操作总是很快返回,并使调用者不在处于阻塞状态。
综合以上的论述,我们可以认为,只要可能导致 I/O 永远 阻塞的,就是慢速系统调用。(关键词:可能,永远)
按照定义,pause 函数是慢速的,而 sleep 不是(仔细体会)。
9.2 再谈信号处理函数执行期
按照 APUE 的说法,只有对低速设备进行操作的时候,才会被信号中断!!!
回到篇首语,其中讲到只要进程接收到了信号(未被阻塞),即使请求的资源还没到来,进程照样会被调度到,这句话就得修正为:
只要进程接收到了信号(未被阻塞),同时执行 I/O 操作位于低速设备上,即使请求的资源还没到来,进程照样会被调度到
9.3 低速系统调用被信号中断
这里有两种情况:
- 低速系统调用已经收到 n 字节的数据时被信号中断,按照 POSIX 语义,成功返回已读取的字节数 n!(System V 语义是返回错误,而 linux 是遵守 POSIX 标准的)
- 低速系统调用尚未收到数据,被信号中断,返回错误(-1),同时 errno 变量置为 EINTR (error interrupt)
有时候,只要发现信号一来,这后面的 sleep 或者 pause 被信号中断后都会失效。不过你还没见过 read 也失效的情况,那是因为之前我们一直用的 signal 信号注册函数。或者说,signal 默认情况下设置了自动重启动属性。
9.4 什么是自动重启
有些慢速系统调用,被信号中断后,本应该返回错误的,但是通过开启 struct sigaction 成员 sa_flags 的 SA_RESTART 选项,这些慢速系统调用就不会返回错误,而是重新执行一次!!!
如果你使用了 signal 信号注册函数,SA_RESTART 选项默认就是开启的(大多数时候,我们并不希望开启此选项)。
9.4.1 能够自动重启的系统调用
- read(2), readv(2), write(2), writev(2), ioctl(2).
- open(2)(在打开 FIFO 文件时).
- wait(2), wait3(2), wait4(2), waitid(2), waitpid(2).
- socket 接口 accept(2), connect(2), recv(2), recvfrom(2), recvmmsg(2), recvmsg(2), send(2), sendto(2), and sendmsg(2).(未设置超时时间的情况下)
- 文件锁接口 flock(2), 以及 fcntl(2) 在使用 F_SETLKW 和 F_OFD_SETLKW 时.
- 消息队列 mq_receive(3), mq_timedreceive(3), mq_send(3), mq_timedsend(3).
- futex(3) FUTEX_WAIT (2.6.22 内核以前不支持自动重启)
- getrandom(2).
- pthread_mutex_lock(3), pthread_cond_wait(3) 和相关 api.
- 信号量相关的函数 sem_wait(3), sem_timedwait(3) (2.6.22 内核以前不支持自动重启).
9.4.2 不能自动重启的系统调用
不能自动重启的系统调用无视 SA_RESTART 开关。
- socket 读相关的接口, 在使用了 setsockopt(2) 函数设置了 SO_RCVTIMEO 的情况下:accept(2), recv(2), recvfrom(2), recvmmsg(2) recvmsg(2).
- socket 写相关的接口,在使用了 setsockopt(2) 函数设置了 SO_RCVTIMEO 的情况下:connect(2), send(2), sendto(2), and sendmsg(2)
- 等待信号的函数:pause(2), sigsuspend(2), sigtimedwait(2), and sigwaitinfo(2)
- 多路复用:epoll_wait(2), epoll_pwait(2), poll(2), ppoll(2), select(2), pselect(2).
- System V 进程间通信接口:msgrcv(2), msgsnd(2), semop(2), semtimedop(2).
- sleep 相关接口:clock_nanosleep(2), nanosleep(2), usleep(3).
- read(2) 读取 inotify(7) 返回的描述符.
- io_getevents(2)
以上函数被信号中断都会返回失败,同时 errno 置 EINTR.
另外,还有一个比较奇葩的函数 sleep(3),要单独挑出来打一顿,它不支持自动重启,但是被信号中断了它能够成功返回剩余时间的秒数。
9.5 实例
看了如此多的概念,相信你也烦了。下面这段代码就演示 read 从终端读取数据时自动重启和不自动重启两种情况。
// restart.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void handler(int sig) {
switch(sig) {
case SIGUSR1:
printf("hello SIGUSR1\n");break;
case SIGALRM:
printf("hello SIGALRM\n");break;
}
}
int main(int argc, char* argv[]) {
char buf[16] = { 0 };
int n = 0;
printf("I'm %d\n", getpid());
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 如果进程带参数 -r,则开启自动重启选项
if (argc >= 2 && strcmp(argv[1], "-r") == 0) {
act.sa_flags |= SA_RESTART;
}
if (sigaction(SIGUSR1, &act, NULL) < 0) {
perror("signal SIGUSR1");
}
if (sigaction(SIGALRM, &act, NULL)) {
perror("signal SIGALRM");
}
while(1) {
if ((n = read(STDIN_FILENO, buf, 15)) < 0) {
if (errno == EINTR) { // 如果 read 返回错误,检查 errno,判断是否被信号中断
printf("Inuterrupted by signal\n");
}
}
else {
buf[n] = 0;
printf("%s", buf);
}
}
return 0;
}
- 编译
$ gcc restart.c -o restart
- 运行
该程序有两种运行方式:
./restart # 不带参数运行,在这种情况下,read 函数不自动重启。
启动后,再开启一个终端,发送 SIGUSR1 或者 SIGALRM 信号给进程,结果如下:
I'm 3626
hello SIGUSR1
Inuterrupted by signal
hello SIGALRM
Inuterrupted by signal
./restart -r # 带参数运行,在这种情况下,read 函数会自动重启。
启动后,再开启一个终端,发送 SIGUSR1 或者 SIGALRM 信号给进程,结果如下:
I'm 3643
hello SIGUSR1
hello SIGALRM
- 结果分析
从上面的运行结果可以看到,当开启 SA_RESTART 选项时,read 函数不会返回错误。而关闭 SA_RESTART 选项时,read 函数会返回错误(-1),同时把 errno 置为 EINTR 。
很多时候,并不希望进程再接收到 SIGALRM 信号自动重启,APUE 给的解释是:
希望对 I/O 操作可以设置时间限制。
10 信号专题总结
从理解进程能够接收信号开始,就探讨了进程对信号做出的默认动作。接下来我们开始引入ANSI C提供的 signal 信号注册函数,帮助我们自己处理信号。
掌握了信号处理函数,你就可以使用异步的方式处理子进程先行退出的情况了(SIGCHLD)。同时我们演示了信号的不可靠性,意思是说如果同时有很多相同的信号到来,会自动合并成一个交给信号处理函数处理。
接下来,又简单的介绍了一下编写信号处理函数需要注意的事项——信号处理函数必须是可重入的!并列举了 getpwnam 这个例子,如果不记得了,可以翻过去再看看。
逐渐熟练的掌握了信号处理函数的种种细节后,探讨了信号发送函数 kill 和 alarm,希望同学们能够分清楚这两个函数在发送信号上的差异。
在一步一步踩踏实后,为了给阻塞信号未决信号做铺垫,不得不提前为信号集做铺垫。但是为了防止过于突兀,一开始并未提及信号集的概念,标题取了个《装信号的容器 sigset》,其实是希望大家能够接收,在掌握了对容器的 5 种操作方法(回忆)后,就有基础去理解阻塞信号了。
讲解阻塞信号集的时候,提到进程 PCB 中其实有两个装信号的容器,分别是阻塞信号集和未决信号集,大家需要理解这两信号集的含义和作用。
基本上到这里,关于信号的基础大家相对来说已经完成了70%了。后面就是关于信号更高级的部分,为了能够更精细的控制信号处理函数和信号的行为,引入了 POSIX 标准提供的信号注册函数 sigaction。实际上 signal 函数的底层就是用 sigaction 实现的。
需要注意的是 sigaction 的第二个参数,它是一个结构体,每个成员需要大家都能够熟练理解掌握。特别是成员 sa_flags,它的选项用来控制信号处理函数的和信号的行为。比如什么时候带参数?什么时候让被打断的系统调用重启等等。
为了能讲清楚被中断的系统调用自动重启,我们深入到了内核,去理解信号处理函数的执行期,有了这些内功后,再去理解系统调用自动重启这件事就不难了。