Linux进程间通信:信号

信号是Linux进程间通信的方式之一,它的特点是简单而有效,也是我们经常使用的IPC之一。

信号的运行机制:

信号的运行机制很简单:A 给 B 发送信号,B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。

信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。

每个进程收到的所有信号,都是由内核负责发送的 ,内核处理。

信号的产生以及处理方式:

有以下5种产生信号的方式:

1. 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+
2. 系统调用产生,如:kill、raise、abort
3. 软件条件产生,如:定时器 alarm
4. 硬件异常产生,如:非法访问内存(段错误)、除 0(浮点数例外)、内存对齐出错(总线错误)
5. 命令产生,如:kill 命令

3种信号的处理方式:
1. 执行默认动作(不同信号有自己的默认动作)
2. 忽略(丢弃)
3. 捕捉(调用户处理函数)

信号编号

可以使用 kill –l 命令查看当前系统可使用的信号有哪些。
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
不存在编号为 0 的信号。其中 1-31 号信号称之为常规信号(也叫普通信号或标准信号),34-64 称之为实时信
号,驱动编程与硬件相关。名字上区别不大。而前 32 个名字各不相同。

信号最重要的是:编号  名称  产生事件  默认处理操作

信号集

信号集被定义为一种数据类型:

typedef struct {  unsigned long sig[_NSIG_WORDS];  } sigset_t;  其实我们可以直接把它当成bitmap就行

信号集用来描述信号的集合,每个信号占用一位(64位)。Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:

sigemptyset(sigset_t *set)  初始化由set指定的信号集,信号集里面的所有信号被清空,相当于64为置0;

sigfillset(sigset_t *set)  调用该函数后,set指向的信号集中将包含linux支持的64种信号,相当于64为都置1;

sigaddset(sigset_t *set, int signum)  在set指向的信号集中加入signum信号,相当于将给定信号所对应的位置1;

sigdelset(sigset_t *set, int signum)  在set指向的信号集中删除signum信号,相当于将给定信号所对应的位置0;

sigismember(const sigset_t *set, int signum)  判定信号signum是否在set指向的信号集中,相当于检查给定信号所对应的位是0还是1。

阻塞信号集与未决信号集

在PCB中,每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。 SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。 SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动 作是用户⾃自定义函数。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计⼀次,而实时信号在递达之前产生多次可以依次放在一个队列⾥。

因此,总结上面一段话可以得出,普通信号(1~31号)允许在递达之前丢失。而实时信号(34~64号)不允许丢失。

 

sigprocmask 函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);  用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字/阻塞信号集 (PCB 中)

参数:

set:传入参数,是一个位图,set 中哪位置 1,就表示当前进程屏蔽哪个信号。

oldset:传出参数,保存旧的信号屏蔽集

how 参数取值: 假设当前的信号屏蔽字为 mask
  1. SIG_BLOCK: 当 how 设置为此值,set 表示需要屏蔽的信号。相当于 mask = mask|set
  2. SIG_UNBLOCK: 当 how 设置为此,set 表示需要解除屏蔽的信号。相当于 mask = mask & ~set
  3. SIG_SETMASK: 当 how 设置为此,set 表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set。若,调用 sigprocmask 解除了对当前若干个信号的阻塞,则在 sigprocmask 返回前,至少将其中一 个信号递达。    

返回值:成功:0;失败:-1,设置 errno

注意,屏蔽信号:只是将信号处理延后执行( 延至解除屏蔽) ;而忽略表示将信号丢处理。


sigpending 函数
int sigpending(sigset_t *set); set 传出参数。  读取当前进程的 未决信号集

返回值:成功:0;失败:-1,设置 errno

 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 #include<signal.h>
 4 #include<string.h>
 5 #include<errno.h>
 6 #include<pthread.h>
 7 #include<unistd.h>
 8 
 9 void sys_err(const char *str) {
10     perror(str);
11     exit(1);
12 }
13 
14 void print_set(sigset_t *set) {
15     for (int i=1;i<32;i++) {
16         if (sigismember(set,i))
17             putchar('1');
18         else
19             putchar('0');
20     }
21     printf("
");
22 }
23 
24 int main(int argc,char *argv[])
25 {
26     sigset_t set,oldset,pedset;    //sigset_t是一个bitmap
27     int ret=0;
28 
29     //以下几行是对set的操作
30     sigemptyset(&set);    
31     sigaddset(&set,SIGINT);        //屏蔽(阻塞)ctrl+C
32     sigaddset(&set,SIGQUIT);
33     //sigaddset(&set,SIGBUS);
34     //sigaddset(&set,SIGKILL);
35 
36     ret=sigprocmask(SIG_BLOCK,&set,&oldset);    //把set设置为阻塞信号,并且把旧的阻塞信号返回给oldset
37     if (ret==-1)
38         sys_err("sigprocmask error");
39 
40     while (1) {    
41         ret=sigpending(&pedset);    //取得未决信号集
42         print_set(&pedset);        //函数处理未决信号集
43         sleep(1);
44     }
45 
46     return 0;
47 }
信号集处理

子进程继承父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集 spending。

信号捕捉

signal 函数(在不同Unix/Linux系统也许有不同表现,我们应该尽量使用sigaction而不是signal)
注册一个信号捕捉函数:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数一是信号,参数二是回调函数。(即产生信号1的时候调用参数二的回调函数)

sigaction 函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);  修改信号处理动作(通常在 Linux 用其来注册一个信号的捕捉函数)

signum:要处理的信号

act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。

返回值:成功:0;失败:-1,设置 errno

struct sigaction {    //struct sigaction 结构体
  void (*sa_handler)(int);    //重点掌握①
  void (*sa_sigaction)(int, siginfo_t *, void *);    //当 sa_flags 被指定为 SA_SIGINFO 标志时,使用该信号处理程序。(很少使用)
  sigset_t sa_mask;      //重点掌握②
  int sa_flags;        //重点掌握③
  void (*sa_restorer)(void);    //POSIX.1 标准将不指定该元素
};

① sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为 SIG_IGN 表忽略 或 SIG_DFL 表执行默认动作

② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。

③ sa_flags:通常设置为 0,表使用默认属性。

信号捕捉特性(重要)

1. 进程正常运行时,默认 PCB 中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用 sa_mask 来指定。调用完信号处理函数,再恢复为☆。

2. XXX 信号捕捉函数执行期间,XXX 信号自动被屏蔽。
3. 阻塞的常规信号不支持排队,产生多次只记录一次。(后 32 个实时信号支持排队)

SIGCHLD信号

SIGCHLD 的产生条件

①子进程终止时  ②子进程接收到 SIGSTOP 信号停止时  ③子进程处在停止态,接受到 SIGCONT 后唤醒时

 SIGCHLD 信号回收子进程

子进程结束运行,其父进程会收到 SIGCHLD 信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。

 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 #include<string.h>
 4 #include<unistd.h>
 5 #include<signal.h>
 6 #include<sys/wait.h>
 7 #include<errno.h>
 8 #include<pthread.h>
 9 
10 void sys_err(const char *str) {
11     perror(str);
12     exit(1);
13 }
14 
15 void catch_child(int signo) {
16     pid_t wpid;
17     int status;
18 
19     //这里的while非常重要,当接受到SIGCHLD信号时候,有多个子进程同时死亡,这时候就需要while来把这段时间的死亡子全部回收
20     while ((wpid=waitpid(-1,&status,0))!=-1) {
21         if (WIFEXITED(status))
22             printf("-------------catch child id %d, ret=%d 
",wpid,WEXITSTATUS(status));
23     }
24     return;
25 }
26 
27 int main(int argc,char *argv[])
28 {
29     pid_t pid;
30     int i;
31 
32     for (i=0;i<15;i++)
33         if ((pid=fork())==0)    //创建15个子进程
34             break;
35 
36     if (i==15) {    //父进程
37         
38         //信号结构体,三个参数重要
39         struct sigaction act;    
40 
41         act.sa_handler=catch_child;    //注册函数
42         sigemptyset(&act.sa_mask);    //在执行期间,sa_mask会替换原mask
43         act.sa_flags=0;        //设为0,在该信号处理函数期间,再次收到同样信号就屏蔽
44 
45         sigaction(SIGCHLD,&act,NULL);
46 
47         printf("I'm parent, pid=%d 
",getpid());
48 
49         while(1);
50 
51     } else {    //子进程
52         printf("I'm child pid= %d
",getpid());
53         return i;
54     }
55     return 0;
56 }
SIGCHLD信号回收子进程

参考资料:

https://blog.csdn.net/aaronlanni/article/details/79873296

原文地址:https://www.cnblogs.com/clno1/p/12941316.html