【Linux C编程】第十二章 信号2

Yuchuan 发表于 2021/05/31 08:48:30 2021/05/31
【摘要】  sa_flags还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号。

(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信号,模拟场景,对该信号进行捕捉,并且使用捕捉函数回收子进程。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。