【Linux C编程】第十二章 信号2
(5)竞态条件(时序竞态)
pause函数
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。
int pause(void); 返回值:-1 并设置errno为EINTR
返回值:
1)如果信号的默认处理动作是终止进程,则进程终止,pause函数么有机会返回。
2)如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
3)如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1】,errno设置为EINTR,表示“被信号中断”。想想我们还有哪个函数只有出错返回值。
4)pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
练习:使用pause和alarm来实现sleep函数。
mysleep.c
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4 #include <errno.h>
5
6 void sig_alarm(int signum)
7 {
8 printf("cat %d\n", signum);
9 return;
10 }
11
12 void mysleep(int seconds)
13 {
14 signal(SIGALRM, sig_alarm);
15 alarm(seconds);
16 printf("------pause out--------\n");
17 while(pause() == -1 && errno == EINTR)
18 {
19 printf("------pause in--------\n");
20 return;
21 }
22 }
23
24 int main()
25 {
26 printf("begin test mysleep...\n");
27 mysleep(5);
28 printf("end test mysleep...\n");
29
30 return 0;
31 }
(6)时序竞态
1)前导例
设想如下场景:
欲睡觉,定闹钟10分钟,希望10分钟后闹铃将自己唤醒。
正常:定时,睡觉,10分钟后被闹钟唤醒。
异常:闹钟定好后,被唤走,外出劳动,20分钟后劳动结束。回来继续睡觉计划,但劳动期间闹钟已经响过,不会再将我唤醒。
2)时序问题分析
回顾,借助pause和alarm实现的mysleep函数。设想如下时序:
a. 注册SIGALRM信号处理函数 (sigaction...)
b. 调用alarm(1) 函数设定闹钟1秒。
c. 函数调用刚结束,开始倒计时1秒。当前进程失去cpu,内核调度优先级高的进程(有多个)取代当前进程。当前进程无法获得cpu,进入就绪态等待cpu。
d. 1秒后,闹钟超时,内核向当前进程发送SIGALRM信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
e. 优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。SIGALRM信号递达,信号设置捕捉,执行处理函数sig_alarm。
f. 信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲等待alarm函数发送的SIGALRM信号将自己唤醒)
g. SIGALRM信号已经处理完毕,pause不会等到。
时序问题
1 #include <unistd.h>
2 #include <signal.h>
3 #include <stdio.h>
4
5 void handler(int sig) //信号处理函数的实现
6 {
7 printf("SIGINT sig\n");
8 }
9
10 int main()
11 {
12 sigset_t new, old;
13 struct sigaction act;
14
15 act.sa_handler = handler; //信号处理函数handler
16 sigemptyset(&act.sa_mask);
17 act.sa_flags = 0;
18 sigaction(SIGINT, &act, NULL); //准备捕捉SIGINT信号
19
20 sigemptyset(&new);
21 sigaddset(&new, SIGINT);
22 printf("Blocked before...\n");
23 sigprocmask(SIG_BLOCK, &new, &old); //将SIGINT信号阻塞,同时保存当前信号集
24
25 printf("Blocked after...\n");
26 sigprocmask(SIG_SETMASK, &old, NULL); //取消阻塞
27
28 printf("start sleep 10s...\n");
29 sleep(10);
30 printf("end sleep 10s...\n");
31
32 pause();
33 printf("main progress end...\n");
34
35 return 0;
36 }
执行结果:
[root@centos 09-linux-day07]# ./pause_bug
Blocked before...
Blocked after...
start sleep 10s...
^CSIGINT sig
end sleep 10s...
^CSIGINT sig
main progress end...
可以看出在第一次ctl+c时,pause还没有执行,此时信号已处理,执行完pause之后,程序就会一直挂起,再执行一次ctl+c,此时唤醒程序,程序执行结束。
上面实例的问题是:本来期望pause()之后,来SIGINT信号,可以结束程序;可是,如果当“取消阻塞”和“pause”之间,正好来了SIGINT信号,结果程序因为pause的原因会一直挂起。
3)解决时序问题
可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。
int sigsuspend(const sigset_t *mask); 挂起等待信号。
sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。
可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号。
sigsuspend示例
1 #include <unistd.h>
2 #include <signal.h>
3 #include <stdio.h>
4
5 void handler(int sig) //信号处理程序
6 {
7 if(sig == SIGINT)
8 printf("SIGINT sig\n");
9 else if(sig == SIGQUIT)
10 printf("SIGQUIT sig\n");
11 else
12 printf("SIGUSR1 sig\n");
13 }
14
15 int main()
16 {
17 sigset_t new,old,wait; //三个信号集
18 struct sigaction act;
19 act.sa_handler = handler;
20 sigemptyset(&act.sa_mask);
21 act.sa_flags = 0;
22 sigaction(SIGINT, &act, NULL); //可以捕捉以下三个信号:SIGINT/SIGQUIT/SIGUSR1
23 sigaction(SIGQUIT, &act, NULL);
24 sigaction(SIGUSR1, &act, NULL);
25
26 sigemptyset(&new);
27 sigaddset(&new, SIGINT); //SIGINT信号加入到new信号集中
28
29 sigemptyset(&wait);
30 sigaddset(&wait, SIGUSR1); //SIGUSR1信号加入wait
31
32 sigprocmask(SIG_BLOCK, &new, &old); //将SIGINT阻塞,保存当前信号集到old中
33 //临界区代码执行
34 if(sigsuspend(&wait) != -1) //程序在此处挂起;用wait信号集替换new信号集。即:过来SIGUSR1信号,阻塞掉,程序继续挂起;过来其他信号,例如SIGINT,则会唤醒程序。执行
35 {
36 perror("sigsuspend error:");
37 return -1;
38 }
39
40 printf("After sigsuspend\n");
41
42 sigprocmask(SIG_SETMASK, &old, NULL);
43
44 return 0;
45 }
执行结果:
窗口1:
[root@centos 09-linux-day07]# ./pause_bugfix
SIGINT sig
SIGUSR1 sig
After sigsuspend
窗口2:
[root@centos ~]# ps aux | grep pause_bugfix | grep -v grep
root 11442 0.0 0.0 4208 356 pts/0 S+ 23:19 0:00 ./pause_bugfix
[root@centos ~]# kill -SIGUSR1 11442
[root@centos ~]# kill -SIGINT 11442
可以看到,当我们程序在 sigsuspend(&wait) 处挂起;用wait信号集替换new信号集。即:过来SIGUSR1信号,阻塞掉,程序继续挂起;过来其他信号,例如SIGINT,则会唤醒程序。执行sigsuspend的原子操作。
注意:如果“sigaddset(&wait, SIGUSR1);”这句没有,则此处不会阻塞任何信号,即过来任何信号均会唤醒程序。
练习:改进版mysleep
mysleep2.c
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4 #include <errno.h>
5
6 void sig_alarm(int signum)
7 {
8 printf("cat %d\n", signum);
9 return;
10 }
11
12 void mysleep(int seconds)
13 {
14 sigset_t st;
15 struct sigaction act;
16 act.sa_handler = sig_alarm;
17 sigemptyset(&act.sa_mask);
18 act.sa_flags = 0;
19 sigaction(SIGALRM, &act, NULL);
20
21 sigdelset(&st, SIGALRM);
22 sigprocmask(SIG_BLOCK, &st, NULL);
23
24 alarm(seconds);
25
26 printf("------sigsuspend before--------\n");
27 if (sigsuspend(&st) != -1)
28 {
29 perror("sigsuspend error:");
30 return;
31 }
32 printf("------sigsuspend after--------\n");
33 }
34
35 int main()
36 {
37 printf("begin test mysleep...\n");
38 mysleep(5);
39 printf("end test mysleep...\n");
40
41 return 0;
42 }
总结:
竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。
这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。
4)全局变量异步I/O
分析如下父子进程交替数数程序。当捕捉函数里面的sleep取消,程序即会出现问题。请分析原因。
全局变量异步IO
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 #include <stdlib.h>
5
6 int n = 0, flag = 0;
7
8 void sys_err(char *str)
9 {
10 perror(str);
11 exit(1);
12 }
13
14 void do_sig_child(int num)
15 {
16 printf("I am child %d\t%d\n", getpid(), n);
17 n += 2;
18 flag = 1;
19 sleep(1);
20 }
21
22 void do_sig_parent(int num)
23 {
24 printf("I am parent %d\t%d\n", getpid(), n);
25 n += 2;
26 flag = 1;
27 sleep(1);
28 }
29
30 int main(void)
31 {
32 pid_t pid;
33 struct sigaction act;
34
35 if ((pid = fork()) < 0)
36 {
37 sys_err("fork");
38 }
39 else if (pid > 0)
40 {
41 n = 1;
42 sleep(1);
43 act.sa_handler = do_sig_parent;
44 sigemptyset(&act.sa_mask);
45 act.sa_flags = 0;
46 sigaction(SIGUSR2, &act, NULL); //注册自己的信号捕捉函数 父使用SIGUSR2信号
47 do_sig_parent(0);
48 while (1)
49 {
50 /* wait for signal */;
51 if (flag == 1) { //父进程数数完成
52 kill(pid, SIGUSR1);
53 flag = 0; //标志已经给子进程发送完信号
54 }
55 }
56 }
57 else if (pid == 0)
58 {
59 n = 2;
60 act.sa_handler = do_sig_child;
61 sigemptyset(&act.sa_mask);
62 act.sa_flags = 0;
63 sigaction(SIGUSR1, &act, NULL);
64
65 while (1)
66 {
67 /* waiting for a signal */;
68 if (flag == 1) {
69 kill(getppid(), SIGUSR2);
70 flag = 0;
71 }
72 }
73 }
74
75 return 0;
76 }
示例中,通过flag变量标记程序实行进度。flag置1表示数数完成。flag置0表示给对方发送信号完成。
问题出现的位置,在父子进程kill函数之后需要紧接着调用 flag,将其置0,标记信号已经发送。但,在这期间很有可能被kernel调度,失去执行权利,而对方获取了执行时间,通过发送信号回调捕捉函数,从而修改了全局的flag。
如何解决该问题呢?可以使用后续课程讲到的“锁”机制。当操作全局变量的时候,通过加锁、解锁来解决该问题。
现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步IO可能造成的问题。
全局变量异步IO改进版(父子进程数数)
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 #include <stdlib.h>
5
6 pid_t pid;
7 int n = 0;
8
9 void sys_err(char *str)
10 {
11 perror(str);
12 exit(1);
13 }
14
15 void do_sig_child(int num)
16 {
17 printf("I am child %d\t%d\n", getpid(), n);
18 n += 2;
19 kill(getppid(), SIGUSR2);
20 }
21
22 void do_sig_parent(int num)
23 {
24 printf("I am parent %d\t%d\n", getpid(), n);
25 n += 2;
26 kill(pid, SIGUSR1);
27 }
28
29 int main(void)
30 {
31 if ((pid = fork()) < 0)
32 {
33 sys_err("fork");
34 }
35 else if (pid > 0)
36 {
37 struct sigaction act;
38 usleep(10); //等待父进程注册完毕
39 n = 1;
40 act.sa_handler = do_sig_parent;
41 sigemptyset(&act.sa_mask);
42 act.sa_flags = 0;
43 sigaction(SIGUSR2, &act, NULL); //注册自己的信号捕捉函数 父使用SIGUSR2信号
44 do_sig_parent(0);
45
46 while (1) {
47 }
48 } else if (pid == 0) {
49 struct sigaction act;
50 n = 2;
51 act.sa_handler = do_sig_child;
52 sigemptyset(&act.sa_mask);
53 act.sa_flags = 0;
54 sigaction(SIGUSR1, &act, NULL);
55
56 while (1) {
57 }
58 }
59
60 return 0;
61 }
5)可/不可重入函数
一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。根据函数实现的方法可分为“可重入函数”和“不可重入函数”两种。看如下时序。
显然,insert函数是不可重入函数,重入调用,会导致意外结果呈现。究其原因,是该函数内部实现使用了全局变量。
注意事项:
1)定义可重入函数,函数内不能含有全局变量及static变量,不能使用malloc、free
2)信号捕捉函数应设计为可重入函数
3)信号处理程序可以调用的可重入函数可参阅man 7 signal
4)没有包含在上述列表中的函数大多是不可重入的,其原因为:
a. 使用静态数据结构
b. 调用了malloc或free
c. 是标准I/O函数
(7)SIGCHLD函数
1)SIGCHLD产生的条件
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
2)借助SIGCHLD函数回收子进程
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
SIGCHLD回收子进程示例
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/wait.h>
4 #include <signal.h>
5
6 void catch_sig(int num)
7 {
8 pid_t wpid;
9 while((wpid = waitpid(-1, NULL, WNOHANG)) > 0)
10 {
11 printf("wait child %d ok\n", wpid);
12 }
13 }
14
15 int main()
16 {
17 int i = 0;
18 pid_t pid;
19 for (i = 0; i < 10; i++)
20 {
21 pid = fork();
22 if (pid == 0)
23 {
24 break;
25 }
26 }
27
28 if (i == 10)
29 {
30 //sleep(2); //模拟注册晚于子进程死亡,此时子进程没有回收,全部成为僵尸进程
31 struct sigaction act;
32 act.sa_flags = 0;
33 sigemptyset(&act.sa_mask);
34 act.sa_handler = catch_sig;
35
36 sigaction(SIGCHLD, &act, NULL);
37 while(1)
38 {
39 sleep(1);
40 }
41 }
42 else if(pid < 10)
43 {
44 printf("I am %d child, pid = %d\n", i, getpid());
45 //sleep(i); //不加第30行代码,需要加上这行代码,目的就是注册早于子进程死亡,否则出现僵尸进程
46 }
47
48 return 0;
49 }
分析该例子。结合 17)SIGCHLD 信号默认动作,掌握父使用捕捉函数回收子进程的方式。
如果每创建一个子进程后不使用sleep可以吗?可不可以将程序中,捕捉函数内部的while替换为if?为什么?
if ((pid = waitpid(0, &status, WNOHANG)) > 0) { ... }
思考:信号不支持排队,当正在执行SIGCHLD捕捉函数时,再过来一个或多个SIGCHLD信号怎么办?
3)子进程结束status处理方式
pid_t waitpid(pid_t pid, int *status, int options)
options
WNOHANG
没有子进程结束,立即返回
WUNTRACED
如果子进程由于被停止产生的SIGCHLD,waitpid则立即返回
WCONTINUED
如果子进程由于被SIGCONT唤醒而产生的SIGCHLD,waitpid则立即返回
获取status
WIFEXITED(status)
子进程正常exit终止,返回真
WEXITSTATUS(status)返回子进程正常退出值
WIFSIGNALED(status)
子进程被信号终止,返回真
WTERMSIG(status)返回终止子进程的信号值
WIFSTOPPED(status)
子进程被停止,返回真
WSTOPSIG(status)返回停止子进程的信号值
WIFCONTINUED(status)
SIGCHLD信号注意问题:
- 子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集spending。
- 注意注册信号捕捉函数的位置。
- 应该在fork之前,阻塞SIGCHLD信号。注册完捕捉函数后解除阻塞。
改进版父进程回收子进程
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/wait.h>
4 #include <signal.h>
5
6 void catch_sig(int num)
7 {
8 pid_t wpid;
9 while((wpid = waitpid(-1, NULL, WNOHANG)) > 0)
10 {
11 printf("wait child %d ok\n", wpid);
12 }
13 }
14
15 int main()
16 {
17 int i = 0;
18 pid_t pid;
19 //在创建子进程之前屏蔽SIGCHLD信号
20 sigset_t myset, oldset;
21 sigemptyset(&myset);
22 sigaddset(&myset, SIGCHLD);
23 //oldset 保留现场,设置了SIGCHLD到阻塞信号集
24 sigprocmask(SIG_BLOCK, &myset, &oldset);
25
26 for (i = 0; i < 10; i++)
27 {
28 pid = fork();
29 if (pid == 0)
30 {
31 break;
32 }
33 }
34
35 if (i == 10)
36 {
37 //sleep(2); //模拟注册晚于子进程死亡,此时子进程会被全部回收
38 struct sigaction act;
39 act.sa_flags = 0;
40 sigemptyset(&act.sa_mask);
41 act.sa_handler = catch_sig;
42
43 sigaction(SIGCHLD, &act, NULL);
44
45 //解除屏蔽现场
46 sigprocmask(SIG_SETMASK, &oldset, NULL);
47
48 while(1)
49 {
50 sleep(1);
51 }
52 }
53 else if(pid < 10)
54 {
55 printf("I am %d child, pid = %d\n", i, getpid());
56 //sleep(i);
57 }
58
59 return 0;
60 }
(8)信号传参
1)发送信号传参
sigqueue函数对应kill函数,但可在向指定进程发送信号的同时携带参数
int sigqueue(pid_t pid, int sig, const union sigval value);
成功:0;失败:-1,设置errno
union sigval {
int sival_int;
void *sival_ptr;
};
向指定进程发送指定信号的同时,携带数据。但,如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。
2)捕捉函数传参
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
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_flags必须指定为SA_SIGINFO。siginfo_t是一个成员十分丰富的结构体类型,可以携带各种与信号相关的数据。
(9)中断系统调用
系统调用可分为两类:慢速系统调用和其他系统调用。
- 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait...
- 其他系统调用:getpid、getppid、fork...
结合pause,回顾慢速系统调用:
慢速系统调用被中断的相关行为,实际上就是pause的行为: 如,read
1)想中断pause,信号不能被屏蔽。
2)信号的处理方式必须是捕捉 (默认、忽略都不可以)
3)中断后返回-1, 设置errno为EINTR(表“被信号中断”)
可修改sa_flags参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启。 SA_RESTART重启。
扩展了解:
sa_flags还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号。
练习:
1. 使用setitimer实现每个一秒打印一次hello world
2. 使用SIGUSR1和SIGUSR2在父子进程之间进行消息传递,实现父子进程交替报数(每隔1s)
a. kill(pid, sig)发送信号
b. 父子进程捕捉信号
3. 在父子进程进程管道通信时,如果管道读端都关闭,会收到SIGPIPE信号,模拟场景,对该信号进行捕捉,并且使用捕捉函数回收子进程。
- 点赞
- 收藏
- 关注作者
评论(0)