Linux信号的产生,处理
信号的概念:
在日常的学习中,当一个进程在执行时,按下crtl-C时就会产生一个硬件中断.(这也是一种信号)整个过程如下:
信号的产生:
- 通过键盘
Core Dump:
Core Dump其实是指一个文件,当一个进程因为异常终止时,可以将进程的内存数据全部保存到磁盘中,文件名通常就是core,所以这就是Core Dump.并且可以通过调试这个文件来查看我们的错误原因,这种方式叫做事后调试.注意:在Linux中,我们需要手动改变core文件的大小,因为默认该文件的大小为0; - 通过命令(向指定信号发送指定命令)
例如:下面的这个例子:
int main(int argc,char* argv[])
14 {
15 if(argc != 3)
16 {
17 printf("usage:kill [-option] [number]\n");
18 return 1;
19 }
20 kill(atoi(argv[2]),atoi(argv[1]));
21 return 0;
22 }
int kill(pid_t pid,int singo);
函数作用:是给指定的进程发送指定的信号.
3. 通过异常
信号的处理:
- 忽略信号
- 执行信号的默认处理行为
- 自己写一个函数去捕捉信号
下面我们通过一个函数来简单的检测一下我们电脑的速度:(就是上面的第三种处理信号的方式)
1 #include <stdio.h>
2 #include<stdlib.h>
3 #include<signal.h>
4 #include<unistd.h>
5
6 int count = 0;
7 void hander(int signo)
8 {
9 printf("signo:%d,count:%d\n",signo,count);
10 //printf("signo:%d\n",signo);
11 exit(1); //收到信号就终止程序
12 }
13 int main()
14 {
15 signal(14,hander);
16 alarm(1); //1秒后向进程发送闹钟信号
17 while(1)
18 {
19 count++; //只进行++,不进行I/O输出,这样效率又会提升很多
20 //printf("count = %d",count++); //如果每加一次就输出一次就会不停的进行I/O,那么速度将会非常的慢
21 }
22 }
该函数的作用:不停的在1秒钟之内不停地数数,当一秒钟后就会被SIGALARM信号终止.在该进程中设置闹钟就是向操作系统设置闹钟.
阻塞信号:
信号未决(pending):收到了信号但没有处理得过程.
信号递达(delivery):处理收到的信号.
一个进程可以选择将某个信号阻塞,某个信号一旦阻塞,不再会递达,一直处于未决状态,直到该进程被解除阻塞,才会执行递答动作.
信号在内核中的表示方式:(是通过数据结构中的位图实现的,位图的下表就是的序号,位图的内容就是信号的状态,1表示收到了信号,0表示没有收到该信号)
对于普通信号,如果同时收到多个相同的信号.那么只会处理一次.
而对于实时信号,就会收到一次处理一次.
每个信号都有两个标志位分别表示阻塞和未决,还有一个对于该信号的处理方式. |
如果处理信号的方式选择自定义的捕捉信号:那么内核中是怎么实现信号的捕捉的呢?
下面我们画一张图来模拟一下信号的处理过程:
总结:在信号的捕捉过程中,一共要进行4次的内核到用户的转变.
下面这个例子就是设置信号屏蔽字:
1 #include <stdio.h>
2 #include<stdlib.h>
3 #include<signal.h>
4 #include<unistd.h>
5
6 void hander(int signo)
7 {
8 printf("sig = %d\n",signo);
9 //exit(1);
10 }
11 void Print(sigset_t *set)
12 {
13 int i = 1;
14 for(i = 1;i < 32;++i)
15 {
16 if(sigismember(set,i))
17 {
18 printf("1");
19 }
20 else
21 {
22 printf("0");
23 }
24 }
25 printf("\n");
26 }
27 int main()
28 {
29 //1.捕捉信号
30 //2.设置信号屏蔽字
31 //3.读取信号屏蔽字
32 signal(SIGINT,hander);
33 sigset_t set,oldset;
34 sigemptyset(&set);
35 sigaddset(&set,SIGINT);
36 sigprocmask(SIG_BLOCK,&set,&oldset);
37 int count = 0;
38 while(1)
39 {
40 ++count;
41 if(count == 5)
42 {
43 count = 0;
44 printf("解除信号屏蔽字\n");
45 sigprocmask(SIG_SETMASK,&oldset,NULL);
46 sleep(1);
47 printf("重新设置信号屏蔽字\n");
48 sigprocmask(SIG_BLOCK,&set,NULL);
49 }
50 sleep(1);
51 sigset_t p;
52 sigpending(&p); //获取当前进程的未决信号集
53 Print(&p);
54 }
55 return 0;
56 }
可重入函数
当同一个函数在不同的执行流中被调用时,如果在第一次调用还没有返回时再次进入该函数时,就成为重入.
比如:在进行单链表的插入时,在插入刚进行了一部分时,出现了硬件中断然后进程切换到内核,在返回用户态之前要进行检查有没有递达信号,于是就会切换到对应的信号处理函数中执行相应的代码,如果此时在该函数中也有对应的插入函数,那么就会在执行结束由于系统调用后再次进入内核态,然后从内核切换到用户态执行上次中断的地方.即将剩余的插入函数执行完毕.当插入函数结束时,此时就会在同一个地方有两个节点,此时就出现了错乱.
所以,在上面这中情况下是不可重入的.
总结:在下面几种情况下是不建议使用重入函数的:
1.使用了非常量的全局变量/静态变量.例如:malloc或free函数,malloc是由全局链表来管理的;比如标准库I/O库函数.
2.调用了其它不可重入的函数.
在上述例子中,如果插入是原子操作,那么就不会产生错乱. |
volatile限定符
在C语言中的使用:例如:
9 const int number = 10;
10 printf("number = %d\n",number);
11 int *p = (int*)&number;
12 *p = 20;
13 printf("number = %d\n",number);
结果:
g++中执行的结果是:10,10;
当加了volatile关键字后,就变成了10,20;
因为加了const后会进行优化,将const修饰的变量直接拷贝一份到寄存器中,而在使用时直接从寄存器中访问,这样就提高了访问效率.就造成了内存的不可见性,在对该变量进行修改时,就只在内存中修改了而寄存器中的却没有改变.所以加volatile关键字就可以保证每次读取时从内存中读取.
又如下面这个例子:
7 int value = 1;
8 void Handler(int signo)
9 {
10 (void)signo;
11 value = 0;
12 }
13 int main()
14 {
15 signal(SIGINT,Handler);
16 while(value);
17 return 0;
18 }
结论:如果是上面这种情况,那么当程序执行时按下ctrl+c就会终止程序.但是,在gcc编译器中,将优化级别加到O3,那么此时程序就不会终止.当在全局变量value前加上volatile关键字,那么按下ctrl+c就会停下来.
竞态条件
在进程调度时,产生的问题.
在这里:要学习一个函数:
int sigsuspend(const sigset_t *sigmask);
该函数的功能是可以进行原子操作.将解除信号屏蔽和挂起等待信号这两部合成一个原子操作.
注意:该函数的参数不能包含需要解除的信号.
SIGCHLD信号
该信号是一种避免产生僵尸进程的有效方法.wait()和waitpid()函数在清理僵尸进程时,父进程可以阻塞式的等待子进程的结束,也可以非阻塞式(轮询)的等待子进程的结束.
那么,子进程在结束时会给父进程发送SIGCHLD信号,内核对于这个信号的默认处理动作是忽略.如果父进程自定义的实现SIGCHLD信号的处理函数,那么子进程在结束时就会发送SIGCHLD信号,从而父进程就会调用函数去清理子进程.
例如:下面的例子:
8 void Handler(int sig)
9 {
10 (void)sig;
11 while(1)
12 {
13 int ret = waitpid(-1,NULL,WNOHANG);
14 if(ret > 0)
15 {
16 printf("ret = %d\n",ret);
17 continue;
18 }
19 else if(ret == 0)
20 {
21 break;
22 }
23 else
24 {
25 break;
26 }
27 }
28 }
29
30 int main()
31 {
32 signal(SIGCHLD,Handler); //父进程可以做自己的事情,不用轮训的查看子进程是否结束
33 int i = 0;
32 for(i = 0;i < 20;++i)
35 {
36 pid_t id = fork();
37 if(id < 0)
38 {
39 perror("fork()");
40 return 1;
41 }
42 if(id == 0)
43 {
44 printf("child = %d\n",getpid());
45 sleep(3);
46 exit(0);
47 }
48 }
49 while(1)
50 {
51 printf("father\n");
52 sleep(1);
53 }
54 return 0;
55 }
在Linux中,还有一种有效简洁处理子进程的方式:就是自定义的采用忽略的处理方式.
32 signal(SIGCHLD,SIG_IGN);
33 int i = 0;
34 for(i = 0;i < 20;++i)
35 {
36 pid_t id = fork();
37 if(id < 0)
38 {
39 perror("fork()");
40 return 1;
41 }
42 if(id == 0)
43 {
44 printf("child = %d\n",getpid());
45 sleep(3);
46 exit(0);
47 }
48 }
49 while(1)
50 {
51 printf("father\n");
52 sleep(1);
53 }
上面的方法也可以处理僵尸进程.将SIGCHLD的处理动作置为SIG_IGN,这样在子进程结束时就会自动清理,不会产生僵尸进程,也不会通知父进程. |