目录
1.信号的概念:信号是一个软中断
- 问题引入:
- 信号灯
- 看到绿灯,可以不走吗?可以走,也可以不走。
- 看到红灯,可以走吗?可以走,也可以不走。
- 信号灯只是给我传递了一个消息,走不走,取决于我们自己。(软性行为)
- 信号灯
- 1.1 只是告诉有这样一个信号,但是具体这个信号怎么处理,什么时候处理由进程决定的。所以是软中断
2.信号的种类
- kill -l 命令可以罗列信号
- 信号的种类
- 非实时信号(非可靠信号):
- 特点:信号可能会丢失
- 1~31
- 实时信号(可靠信号):
- 特点:信号不会丢失
- 33~ 64
- 总共定义了62个信号(没有32和33)
- 非实时信号(非可靠信号):
3.信号的产生:
- 硬件产生: .
- ctrl+c : 2号信号 SIGINT (signal interrupt)
- 键盘当中按下ctr1+c结束一个进程的时候,其实是进程收到了2号信号。2号信号导致了进程的退出。
- ctrl+z : 20号信号 SIGTSTP
- 在Windows操作系统下 ,按下ctrl+z,让进程收到EOF,让进程退出。
- ctr1+| : 3号信号SIGQUIT
- 也可以通过命令 kill -[信号值] [pid]
- kill命令:向进程发送信号
- ctrl+c : 2号信号 SIGINT (signal interrupt)
- 软件产生:
- kill函数
- 头文件 :#include<signal. h>
- int kill(pid_ t pid, int sig);
- 参数:
- pid :进程号, 要给那个进程发送信号,则填写那个进程的进程号
- sig :要发送信号的值
-
#include <stdio.h> #include <unistd.h> #include <signal.h> int main(){ while(1){ kill(getpid(),2); printf("signal process...\n"); sleep(1); } return 0; }
- raise函数
- int raise(int sig);
- 谁调用给谁发送信号。
- sig :要发送的信号值
- 成功时返回0,失败时返回非零。
- 该函数的实现当中是调用kill函数
- kill函数
- 扩展:
- 崩溃程序收到的信号(结合gdb)
- gdb调试coredump文件来验证,进程在崩溃的时候收到的是什么信号
- coredump文件,修改corefile size(一般情况下不需要考虑磁盘空间大小):ulimit -a unlimited
- 1.解引用空指针(11号信号 SIGSEGV)
- Segmentation fault:段错误
- 2.内存访问越界(11号信号 SIGSEGV)
- 这种越界场景并没有让进程崩溃,原因:操作系统容忍进程访问不属于自己的内存,但是有个前提条件,越界访问的内存,没有分配给其他进程所使用。
- 当操作系统发现进程在越界访问其他进程的内存时,操作系统就会给当前进程发送一个11号信号,让当前进程退出。
- 3.除0(8号信号 SIGFPE)
- 4.double free (收到6号信号)
- 崩溃程序收到的信号(结合gdb)
4.信号的处理方式
- 操作系统对信号的处理方式 (man 7 signal)
- 默认处理方式:
- SIG_ DFD,操作系统当中已经定义号信号的处理方式了
- 2 号信号->终止进程
- 11号信号 -> 终止进程,并且产生核心转储文件
- SIG_ DFD,操作系统当中已经定义号信号的处理方式了
- 忽略处理方式:
- SIG_ IGN, 该信号为忽略处理(僵尸进程),进程收到忽略处理方式的信号后,是不进行处理的。
- SIGCHLD信号
- 子进程先于父进程退出,子进程退出的时候会给父进程发送SIGCHLD信号,而父进程接收到这个信号之后,是忽略处理的,导致了父进程并没有回收子进程的退出状态信息,从而子进程变成了僵尸进程。
- 自定义处理方式:
- 程序员可以更改信号的处理方式, 定义一个函数, 当进程收到该信号的时候, 调用程序员自己写的函数。
5.信号的注册
-
- 基础概念了解:
- 一个进程收到一个信号,这个过程称之为注册,信号的注册和注销并不是一个过程,是两个独立的过程。
- 内核中信号注册
- 位图以及sigqueue队列的的了解
- task_ struct结构体内部
- struct sigpending pending;
- sigset_ t
- struct sigpending pending;
- 三者之间的联系
- 信号的注册:
- 位图更改为1,添加sigqueue节点到sigqueue队列
- 信号注册的时候,会将信号对应的比特位从0修改为1,表示当前进程收到了该信号。还需要在sigqueue队列当中添加一个sigqueue节点,队列在操作系统内核当中本质上就是一个双向链表(先进先出的特性)。
- 区别:
- 非实时信号的注册
- 第一次注册:
- 修改sig位图(0->1),修改sigqueue队列
- 第二次注册相同信号值的信号:
- 修改sig位图(1->1),并不会添加sigqueue节点,因此,对于非实时信号,相同信号的sigqueue节点在sigqueue队列当中有且只有一个
- 第一次注册:
- 实时信号的注册
- 第一次注册:
- 修改sig位图(0->1),修改sigqueue队列
- 第二次注册相同信号值的信号:
- 修改sig位图(1->1),添加sigqueue节点到sigqueue队列当中
- 第一次注册:
- 非实时信号的注册
6.信号的注销:
- 非可靠信号
- 1.将信号对应的sig位图当中的比特位置为0(1->0)
- 2.将对应的信号的sigqueue节点进行出队操作
- 可靠信号
- 1.将对应的信号的sigqueue节点进行出队操作
- 2.判断sigqueue队列当中还有相同信号的sigqueue节点吗
- 如果有:则比特位不变
- 如果没有:则比特位改变位0
7.信号的自定义处理方式
- 7.1 自定义处理方式, 就是让程序员自己定义某一个信号的处理方式.
- 7.2函数
- sighandler_t signal(int signum, sighandler_t handler);
- signum:信号值
- handler:更改为哪一个函数处理,接受一个函数地址,函数指针
- typedef void (*sighandler_t) (int) ;
- 注意回调
- 在调用signal函数的时候,给第二个参数传递函数地址的时候,并没有调用传递的函数。而是,等到进程收到了某个信号之后,才回调刚刚注册的函数
- 验证:
- 如果2号和3号信号被同时自定义了,可以用9号信号终止进程
- 9号信号(强杀)是不能被程序员自定义处理的信号。
- sighandler_t signal(int signum, sighandler_t handler);
- 7.4 sigaction
- int sigaction(int signum, const struct sigaction*act, struct sigaction *oldact);
- signum:信号值
- act:将信号的处理方式更改为act(输入型参数)
- oldact:原来信号的处理方式(输出型参数)
- struct sigaction {
- void (*sa_handler) (int) ;//保在信号处理方式(默认)的函数指针
- void (*sa_ sigaction) (int,siginfo_t *,void *);//也是保存信号的处理方式的函数指针,但是没有使用。当要使用的时候,配合sa_flags一起使用。当sa_falgs的值为SA_SIGINFO的时候,信号按照sa_sigaction保存的函数地址进行处理
- (sigset_ t) sa_mask;//当进程在处理信号的时候,如果还在收到信号,则放到该信号位图当中,后续在放到进程的信号位图当中(假设进程正在处理信号A的时候,又来了一个信号B,则信号B会被放到sa_mask这各位图当中,等到进程及那个信号A处理完成后,再把信号B放到PCB的位图中去)
- int sa_ flags;
- void (*sa restorer) (void);//保留字段
- };
- 测试1:自定义处理方式
- 测试2:先以自定义方式处理,再以默认方式处理
- int sigaction(int signum, const struct sigaction*act, struct sigaction *oldact);
- 7.5内核理解自定义原理
8.信号的捕捉流程
- 8.1信号的处理时机:当从内核态切换会用户态的时候,会调用do signal函数处理信号(判断进程是否收到信号)
- 有,就处理信号(信号的处理方式(默认, 忽略, 自定义) )
- 没有,就直接返回会用户态
- 8.2处理信号的时候, 不同的处理方式,
- 默认,忽略:直接在内核就处理结束。
- 自定义处理:调用程序员自己定义的处理函数进行处理
- 执行用户自定义的处理函数(用户空间)
- 调用sigreturn()再次回到操作系统内核(内核空间)
- 再次调用会调用do_ signal 函数处理信号
- 调用sys_ sigreturn函数回到用户空间,继续执行代码
- 8.4常见的进入到内核的方式:
- 一调用系统调用函数,
- 内存访问越界,访问空指针
- 调用库函数
9.信号的阻塞.
- 9.1要理解的点:. 信号的注册是信号注册,信号阻塞是信号阻塞。信号的阻害并不会干扰信号的注册,而是说进程收到这个信号之后,由于阻寒, 暂时不处理该信号。
- 阻塞并不是说不处理该信号,而是等该信号不阻塞了之后,在进 行处理
- 9.2从内核理解:
- 9.3加上信号阻塞之后, 理解信号的处理;
- 进入内核, 返回之前, 会调用do signal函数处理信号,有信号要处理, 则先判断该信号是否阻塞, 如果没阻塞, 再处理信号。如果阻塞, 则不处理。
- 9.4接口:
- int sigprocmask(int how, const sigset_t *set,sigset_ t *oldset);
- how:想让sigprocmask做在么事情
- SIG_BLOCK: 设置某个信号为阻塞状态
- SIG_UNBLOCK :设置某个信号为非阻塞状态
- SIG_ SETMASK :用第二个参数“set”,替换原来的阻塞位图。
- set:新设置的阻塞位图
- oldset :原来老的阻塞位图
- set:传递进去的是一个位图类型的变量,根据传递进函数的变量,计算新的阻塞位图
- 可能出现的结果:
- 1.阻塞单个信号/阻塞多个信号
- 2.解除阻塞单个信号/解除阻塞多个信号
- how:想让sigprocmask做在么事情
- 原理解析:
- 当how为SIG_BLOCK时,函数会根据set,计算新的阻塞位图,方式为:block(new) = block(old) | set;
- 验证:
- 结论:9号信号和19号信号是不能被阻塞的
- 当how为为SIG_UNBLOCK时,函数会根据set,计算新的阻塞位图,方式为:block(new) = block(old) & (~ set) ;
- 验证:
- 结论:相同非实时信号的注册和注销只处理一次
- 当how为为SIG_SETMASK时,函数会根据set,计算新的阻塞位图, 方 式为:block (new) = set;
- 当how为SIG_BLOCK时,函数会根据set,计算新的阻塞位图,方式为:block(new) = block(old) | set;
- int sigprocmask(int how, const sigset_t *set,sigset_ t *oldset);
10.扩展
- 1.父子进程+进程等得+自定义信号处理方式
- 2.volatile关键字
- 作用:保证内存可见性
- 每次CPU要计算的数据都是从内存当中获取,拒绝编译时优化的方案(从寄存器当中获取)
- gcc/g++的编译选项“-O0,-O1, -O2, -O3年 ,优化级别时越来越高。( 理解优化级别越高,程序 可能执行的越快)