信号的基本概念
信号是一个软件中断,打断当前正在运行的进程,让该进程去处理信号的事件。当前的进程需要处理信号所带来的操作。
信号的种类
Linux操作系统当中,总共有62种信号
1~31 不可靠信号,信号有可能会丢失,非实时信号
34~64 可靠信号,信号是不可能被丢失的,实时信号
信号的产生
硬件的产生:
Ctrl+c 给前台进程发送一个SIGNT,中断当前的进程
Ctrl+z 给前台进程发送一个SIGTSTP信号,暂停进程
Ctrl+| 给当前进程发送一个SIGQUIT信号,使进程崩溃,产生coredump文件
软件的产生
kill [pid] 给前台进程发送一个SIGTERM信号
kill -[signalno] [pid] 给指定进程发送指定的信号
调用函数的产生
kill 函数 kill(pid_t pid,int signo);
abort函数 封装kill函数,谁调用给谁发送SIGABRT信号
alarm定时器函数 alarm(int sec) SIGALRM
信号的注册
前提:
- 在内核当中的task_struct结构体当中,保存了一个struct sigpending的对象pending,struct sigpending 这个结构体当中保存了两个元素。第一个元素是struct list_head list第二个元素是sigset_t signal
sigset_t类型也是一个结构体,struct sigset_t,保存了一个unsigned long sig[ ]
sig这个数组是按照比特位来使用的,我们称之为位图
- 内核当中还维护了一个sigqueue队列。队列当中的每一个元素对应信号的一个处理节点。
图解:
非可靠信号 1~31
更改sig位图当中对应的比特位为1 在sigqueue队列当中也增加对应信号所对应的节点
当多次收到同一个信号的时候,只添加一次节点,也就是意味着,第二次收到的同样的信号被丢弃掉了
可靠信号 34~64
更改sig位图当中对应比特位为1,并且在sigqueue队列当中增加对应信号所对应的节点
当多次收到同样的一个信号的时候,先判断sig位图当中的比特位是否为1,并且在sigqueue队列当中增加对应信号所对应的节点。
信号的注销过程
非可靠信号的注销
将sig位图当中对应信号的比特位置为0,并且将sigqueue队列当中的节点去除掉(即操作系统收到相应信号,去做相应处理)
可靠信号的注销
将待处理的信号在sigqueue队列当中对应的节点进行盘算,当前处理的对应信号的节点是否在sigqueue队列当中还有相同类型的
- 如果有,不改变sig位图当中该信号对应的比特位的值
- 如果没有,直接将sig位图中对应的比特位值置为0
信号的捕捉处理
- 默认处理方式:SIG_DEL 执行一个函数
- 忽略 SIG_IGN 不做任何事情
僵尸进程产生的原因:子进程退出的时候,会给父进程发送一个SIGCHID信号,而操作系统对SIGCHID信号的处理方式为忽略处理,导致父进程不去处理信号,从而子进程变成僵尸进程
- 自定义:程序员自己定义信号的处理函数
sighandler_t signal(int signum,sighandler_t handler);
signum:需要自定义哪一个信号
handler:函数指针类型的参数
typedef void(*sighandler_t)(int)
void,返回值类型
int:参数,操作系统传递的信号值
signal函数的这种方式称之为向操作系统注册一个函数,当发生某种事件的时候,会回调刚才注册的函数,我们称这个函数为回调函数。并不是我们调用signal函数的时候,回调函数就会被操作系统调用执行,而是当我们收到自定义信号的时候,操作系统才会帮我们调用该回调函数,进行处理信号
我们自己的程序启动起来,有自己的执行流,回调函数的执行并不是我们程序的执行流执行的,
而是操作系统的执行流调用执行的
自定义信号的处理过程
前提
1.在task_struct结构体当中,有一个指向sighand_struct的结构体指针,在该结构体当中有一个action数组,数组当中的每一个元素都是一个struct k_sigaction结构体,数组当中每一个元素对应一个信号的处理逻辑
2.在struct k_sigaction结构体当中有一个元素是struct sigaction sa,struct sigaction结构体中还有一个sighandler_t类型元素,这个sighandler_t是一个函数指针类型,typedef void(*sighandler_t)(int) , 保存信号默认执行的函数。
图解:
操作系统默认对信号的处理
当sig位图当中收到一个信号的时候,意味着sig位图当中的某一个比特位被置为1,操作系统处理该信号的时候,就会从PCB当中去寻找sighand_struct这个结构的指针,从而找到sa_handler,进而操作系统内核去调用sa_handler保存的函数地址,完成信号的功能。
自定义信号处理函数
sighandler_t signal(int signum,sighandler_t handler)
signal函数:相当于改掉了sa_handler保存的函数地址,意味着,当收到自定义信号的时候,操作系统内核去调用sa_handler保存的新的函数地址,而这个函数是程序员定义的,也就达到了更改信号处理函数的目的
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum:需要更改的信号
act:表示把signum信号修改成act处理方式
oldact:表示操作系统之前被signum所定义的处理方式。
struct sigaction类型
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
void (*sa_handler)(int);操作系统为每一个信号定义的默认调用函数
void (*sa_sigaction)(int, siginfo_t *, void *);函数指针,这个函数指针一般是预留的,配合sa_flags使用,当sa_flags为SA_SIGINFO的时候,操作系统就会调用该函数指针当中保存的函数地址。
sigset_t sa_mask;当前进程在处理信号的时候,也有可能收到新的信号,将新的信号放到sa_mask里面
int sa_flags;配合sa_sigaction使用
void (*sa_restorer)(void);预留信息
struct sigaction相当于action[ ]数组当中的元素类型(k_sigaction )
sigaction函数:相当于action数组当中的元素,也意味这,直接更改结构体,达到修改信号处理函数地址的目的
signal函数调用sigaction函数实现
信号的捕捉流程
图解:
什么时候会进入内核状态
- 调用系统调用
- 程序员调用库函数的时候,库函数如果底层调用可函数调用
- 程序异常
- 访问空指针 11号信号
- 内存访问越界 11号信号
- double free(同一个指针free两次) 6号信号
信号的阻塞:
前提:在task_struct结构体当中保存了一个block位图
信号阻塞,不会影响信号注册、信号更改pengding位图和增加sigqueue节点
操作系统处理信号的逻辑
当程序从用户态切换到内核态之后,处理do_signal函数的时候,发现收到某个信号,想要处理这个信号之前,先判读block位图当中对应信号的比特位是否为1
block当中对应的比特位为1:则不处理该信号,sigqueue当中对应的信号节点还是存在
block当中对应比特位为0: 处理该信号
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
更改sigset_t位图当中比特位的值
how:
SIG_BLOCK 设置某个信号为阻塞状态, 用修改位图达到目的 block(new)=block(old)| set
SIG_UNBLOCK 设置某个信号为非阻塞状态 block(new)=block(old)& (~ set)
SIG_SETMASK 设置新的阻塞的sigset_t位图 block(new)=set
set:要设置的新的阻塞位图
oldset:之前程序当中阻塞的位图,出参
注:
9号信号不能被阻塞
竞态条件:
多个执行流访问同一个资源的情况下,会对程序结果造成二义性,将这种现象称之为竞态条件
重入:多个执行流可以访问到同一资源,称之为重入
可重入:多个执行流可以访问到同一个资源,不会对程序结果产生影响
不可重入:多个执行流可以访问到同一个资源,但是会对程序结果产生影响,意味着程序会有多个结果表现
volitile关键字:
使变量保持内存可见性,每次操作变量都需要重新从内存中获取,而摒弃编译器优化。
-O0 -O1 -O2 -O3 编译选项,优化程度依次增高
僵尸进程的解决方法:
SIGCHID:当子进程退出,就会给父进程发送一个SIGCHID信号,配合signal处理机制将进程等待放到SIGCHID函数的自定义处理函数当中,解决父进程一直在等待的局面。
更改SIGCHID信号的处理方式,自定义一个处理函数,当子进程退出的时候,内核回去调用用户自定义的函数(回调函数),在回调函数当中去调用wait或者waitpid接口,让父进程来等待子进程
注:在自定义信号处理函数的时候,不要将17号信号之外的函数定义为调用wait或者waitpid的函数,否则当进程收到一个其他信号的时候,触发内核调用用户自定义函数的时候,由于没有一个子进程退出,可能导致内核执行流卡死