进程信号
- 信号的概念
- 信号的产生
- 信号的种类
- 信号的处理方式
- 信号的注册
- 信号的注销
- 信号的自定义处理方式
- 信号的捕捉流程
- 信号的阻塞
- 常见的程序崩溃
- 父子进程+进程等待+自定义信号处理方式
- volatile关键字
信号的概念
信号是一个软件中断,实际上是操作系统告诉进程需要进程执行说明动作,但是进程什么时候执行,什么时候处理,是由进程决定的,所以信号是一个软中断(意味着和强制执行某种指令的硬中断是有区别的)
和信号灯有些类似,实际上起着建议的作用,并没用哪个硬件强制去执行(信号灯的位置也没有人强制拉着我们不让我们走)
信号的产生
信号的产生可以分为两种:硬件产生和软件产生
硬件产生:ctrl + c, ctrl + z , ctrl + l, 它们被按下之后,分别会给进程发送的信号是 SIGINT / SIGSTOP / SIGQUIT
软件产生:kill函数,通过kill -传递的信号值 进程号
信号的种类
通过kill -l
可以查看当前所有的信号
可以发现,当前的信号可以分为1号—>31号,34号---->64号,共62个信号,而1—>31号是属于非可靠信号(也叫非实时信号),34号---->64号是属于可靠信号(也叫实时信号),至于信号的实时和非实时有什么区别,在信号的注册和注销部分会介绍。
信号的处理方式
由操作系统给进程发送信号,进程接收到信号在进行处理时,还是需要操作系统对该信号进行处理的。
处理的方式总体可以分为三类:默认处理、忽略处理、自定义处理
默认处理方式是由操作系统定义好的,而上述62种信号总结起来一共有五种:终止,忽略,终止并产生核心转储文件、停止、继续(如果当前进程处于停止状态的话)。可以通过
man 7 signal
来查看
忽略处理:该信号为忽略处理(当子进程先于父进程退出时,子进程会给父进程发送SIGCHLD信号,而父进程接收到这个信号是忽略处理的,这使得子进程的退出状态信息没有进程回收,导致子进程变成了僵尸进程)
自定义处理:在信号的自定义处理方式会介绍
信号的注册
当一个进程接收到一个信号,这个过程称为信号的注册。
而信号的注册分为两种:非实时信号的注册和实时信号的注册。
非实时信号的注册:sig位图对应位置为1,将sigqueue节点添加到sigqueue队列中,当再次有相同的信号需要注册时,就不会再添加到sigqueue队列中了。(这样会使得多次相同的信号,最终只被添加到sigqueue队列中了一次,造成了信号的丢失)
实时信号的注册:sig位图对应位置为1, 将sigqueue节点添加到sigqueue队列中,当再次有相同的信号需要注册时,依然会被添加到sigqueue队列中。(这样的话,多次相同的信号,就会被添加到sigqueue队列中多次,不会造成信号的丢失)
如果从linux内核源码的角度理解,task_struct在
linux-3.10.0-957.el7/include/linux/sched.h
这个位置的1340行,
我们可以看到在task_struct中有一个模块是signal handlers
,在1530行,其中有一个结构体叫做sigpending,
我们打开struct sigpending这个结构体后,看到有一个sigset_t这样一个类型,存放的是signal信号。
跳转到这个sigset_t 类型,发现也是一个结构体,其中存储的是unsigned long类型的一个位图
其中定义的各个信号对应的值
在源码中,是将信号放在sig位图中的,以bite位的方式存储,收到哪个信号就将哪一位bite位置为1。
信号的注销
注销也是需要分两种情况的:非可靠信号和可靠信号
非可靠信号,由于多个相同的信号只会被注册一次,那么当注销时,无需判断当前sigqueue队列中是否还存在这个类型的信号,直接将sig位图对应位置置为0即可。再将对应信号的sigqueue队列出队
可靠信号,需要先将对应信号的sigqueue出队,然后判断sigqueue队列中是否还有相同类型的信号,如果有sig位图不需要置0,没有才能置0;
信号的自定义处理方式
如果程序员自己不定义信号的处理方式,那么就会默认执行上述的处理方式
对于程序猿自己,也可以指定信号的处理方式,而不去执行默认的,需要使用的函数是:signal函数或者sigaction函数。
sighandler_t signal(int signum, sighandler_t handler);
signum:信号值
handler:更改为哪一个函数处理,接收一个函数指针(传递函数名即可,那个函数就是一个回调函数)
被传递的函数应该被定义为:void sighandler (int)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum: 信号值
act:将信号的处理方式改为act
oldact:原来的信号处理方式
signal和sigaction函数的区别是:
这幅图描述了sigaction这个结构体在内核中的位置和包含的变量,在结构体中可以看到一个sa_handler,其类型是sighandler_t,这是一个函数指针类型,需要进行信号处理时,就会调用这个对象中存储的函数指针指向的函数。而signal函数就是在修改这个函数指针的指向。对于sigaction函数而言,我们修改的是struct sigaction这个结构体本身。区别就是sigaction函数包含了signal函数。
信号的捕捉流程
当一个进程在执行时,可能会从用户态进入到内核态,当在内核处理完成后,返回用户态之前,操作系统会判断当前是否有信号需要处理,如果有则判断当前处理方式是默认处理还是用户自定义处理,若为默认处理,则直接在内核态中调用相应的处理函数,若为自定义处理则需先返回用户态,执行自定义的信号处理函数,然后再次进入内核。执行完之后才会返回用户态继续向下执行(前提是没有需要处理的信号了)
信号的阻塞
信号的注册和信号的阻塞是分开的,互不影响。
信号被阻塞后,意味着当前这个信号暂时不被处理而已。
从内核角度理解,和sig位图相似,都是task_struct结构体内的一个位图,这个位图被称为block位图。
我们在内核态进行信号处理的时候,逻辑和上述的信号捕捉流程一致,只是在调用信号处理函数前会判断当前的信号是否被阻塞,如果未被阻塞,则进行处理,若被阻塞了,则暂不处理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how:想让sigprocmask做什么事情:SIG_BLOCK(设为阻塞状态),SIG_UNBLOCK(设为非阻塞状态),SIG_SETMASK(用set替换原来的阻塞位图)
set:设置新的阻塞位图
oldset:老的阻塞位图
如果要设置为阻塞状态的话,将how设置为SIG_BLOCK,将对应位的set设置为1,就会和oldset进行按位或操作,这样就能将对应位置设为阻塞状态。
如果要设置为非阻塞状态,将how置为SIG_UNBLOCK,将对应位的set设置为0,就会和oldset进行按位与操作,这样就能将对应位置设置为非阻塞状态。
最终我们也可以得到一个结论,9号和19号信号是不能被阻塞的,9号信号也不能被重新定义信号处理方式
常见的程序崩溃
double free | 解引用空指针 | 内存访问越界 | 栈溢出 | 管道破裂 | 除0错误 |
---|---|---|---|---|---|
SIGABRT | SIGSEGV(段错误) | SIGSEGV(段错误) | SIGSEGV(段错误) | SIGPIPE | SIGFPE |
父子进程+进程等待+自定义信号处理方式
父进程如果需要回收子进程的退出状态信息,那么就要用wait进行进程等待,但是wait这个函数是阻塞属性的,那么就会导致父进程什么也不做,一直在等待子进程退出,那么就失去了创建子进程的目的(让程序的运行效率高)。
我们就可以使用父进程进行自定义信号的处理方式,这样就可以让父进程从等待中解放出来。
1 #include<stdio.h>
2 #include<sys/wait.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5 #include<signal.h>
6
7 void signalcallback(int signum)
8 {
9 printf("i catch signal : %d\n", signum);
10 }
11
12 int main()
13 {
14 pid_t pid = fork();
15 if(pid < 0)
16 {
17 perror("fork");
18 exit(-1);
19 }
20 else if(pid == 0)
21 {
22 int count = 10;
23 while(count--)
24 {
25 printf("i am child process working\n");
26 sleep(1);
27 }
28 exit(2);
29 }
30 else{
31 signal(SIGCHLD, signalcallback);
32 while(1)
33 {
34 printf("i am father process working\n");
35 sleep(1);
36 }
37 }
38 return 0;
39 }
volatile关键字
作用:保证内存可见性(让cpu需要的计算数据都从内存中读取,而不会由于编译器优化程度过高导致需要的计算数据从寄存器当中读取)