当前位置: 首页 > news >正文

深入解析Linux信号处理机制

目录

一:信号保存

1.1信号其他相关常见概念

1.2在内核中的表示

1.3 sigset_t

1.4信号集操作函数

1.4.1 sigprocmask

1.4.2 sigpending

二:信号捕捉

​2.1信号捕捉的流程

2.2sigaction

2.3操作系统是怎么运行的

2.3.1硬件中断

2.3.2时钟中断

2.3.3死循环

2.3.4软中断

2.4 用户态和内核态

三:可重入函数

四:volatile

五:SIGCHLD信号


一:信号保存

上一篇文章信号的产生,有五种方式:键盘,指令,系统调用,软件条件,异常

信号的捕捉有三种方式:忽略,默认,自定义

下一个阶段:

1.1信号其他相关常见概念

实际执行信号的处理动作称为信号递达(Delivery)

信号从产生到递达之间的状态,称为信号未决(Pending)

进程可以选择阻塞 (Block )某个信号。(阻塞特定的信号之后,信号产生了,把信号进行pending保存,永远不传递,除非我们解除阻塞)

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。


1.2在内核中的表示

信号在内核中的表示意图

你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有⼀个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻 塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

SIGQUIT信号未产生过,⼀旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?

POSIX.1允许系统递送该信一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在队列。


1.3 sigset_t

 从上图来看,每个信号只有⼀个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。


1.4信号集操作函数

sigset_t类型对于每种信号用⼀个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些 bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h> 
int sigemptyset(sigset_t *set); 
int sigfillset(sigset_t *set); 
int sigaddset(sigset_t *set, int signo); 
int sigdelset(sigset_t *set, int signo); 
int sigismember(const sigset_t *set, int signo);

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。

注意:在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。

初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断⼀个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。


1.4.1 sigprocmask

调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1 

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改 进程的信号屏蔽字,参数how指示如何更改。

如果oset和set都是非空指针,则先将原来的信号屏蔽字 备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了 how参数的可选值。(把老的拷贝到上层方便后续回复信号)

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。


1.4.2 sigpending

#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1

获得对应的pending集,信号发生的五种方式都在改变pending表

pending表中的1是在信号开始处理之前都置为0,然后处理完信号之后,再将对应信号的0置为1,如果是处理完之后再处理pending表,无法确定这个1是信号处理之前就有的,还是这次信号的

使用上述函数,进行小实验:

#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>void PrintPending(sigset_t& pending)
{std::cout <<"curr process [ "<<getpid()<<" ] pending:"<<std::endl;for(int signo = 31;signo >=1;signo--){if(sigismember(&pending,signo)){std::cout << "1";}else{std::cout << "0";}}std::cout << std::endl;
}void Handler(int signo)
{std::cout <<signo<<"号信号被递达"<<std::endl;std::cout <<"#########################"<<std::endl;sigset_t pending;sigpending(&pending);PrintPending(pending);std::cout <<"#########################"<<std::endl;}int main()
{//1.捕捉2号信号signal(2,Handler);//自定义捕捉//2.屏蔽2号信号sigset_t block_set,old_set;sigemptyset(&block_set);//初始化sigemptyset(&old_set);sigaddset(&block_set,SIGINT);//添加2号信号,有没有完成内核中block表的修改???//3.向阻塞表中添加2号信号sigprocmask(SIG_BLOCK,&block_set,&old_set);//此处真正的完成对内核block表的修改,屏蔽2号信号//4.获取当前进程的pending表int cnt = 15;while(true){//获取当前pending表sigset_t pending;sigpending(&pending);//打印pending表PrintPending(pending);cnt--;if(cnt == 0){std::cout << "解除对2号信号的屏蔽!!!" <<std::endl;sigprocmask(SIG_SETMASK,&old_set,&block_set);}sleep(1);}return 0;    
}


二:信号捕捉


2.1信号捕捉的流程

处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。没有处理--->信号到来--->进程记录下来的对应的信号--->处理
进程在从内核态切换到用户态的时候,检测当前的pending和block表,决定是否处理,handler表处理信号方法。

1.信号捕捉方法执行的时候,为什么还要做权限的切换,直接内核执行完就完了吗?

如果在内核执行完,会有安全风险,因为handler是用户自己定的函数,如果让内核执行这个代码,如果里面有非法操作(删root用户,修改自己权限……),此时会有安全风险

2.为什么信号处理完,只能从内核返回

main 函数和我们写到handler函数没有调用关系,内核调用完handler函数弹栈之后,没办法返回直接返回main函数,而main函数与内核有调用关系,通过调用完内核可以弹栈返回main函数。所以内核调用handler,调用完了,返回到内核,main调用内核,内核调用完之后,弹栈返回main

3.OS怎么知道返回main函数之后,继续执行下一条代码?pc指针

PC指针保存当前正在执行的下一条指令的地址,处理异常时,OS保存PC指针,处理完后回复       PC指针

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。

当前正在执行 main 函数,这时发生中断或异常切换到内核态。

在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。

内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数, sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

sighandler 函数返回后自动执行特殊的系统调用sigreturn 再次进入内核态。

如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。


2.2sigaction

#include <signal.h> 
int sigaction(int signo, const struct sigaction *act, struct sigaction 
*oact); 

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。

signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体

将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统 默认动作,赋值为一个函数指针表示自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带⼀个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含⼀些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数。

处理2号信号的时候,屏蔽3-7号信号:

信号处理支持串行处理,处理完2号信号,再处理其他信号

但是OS不允许信号进行嵌套处理,某一个信号正在处理,OS会自动把block位置为1,信号处理完之后再将信号置为1


2.3操作系统是怎么运行的

2.3.1硬件中断

中断向量表就是操作系统的⼀部分,启动就加载到内存中了

通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询

由外部设备触发的,中断系统运行流程,叫做硬件中断


2.3.2时钟中断

进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢? 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的 设备?

进程调度 --->时钟中断,IO--->外设中断信号

这样,操作系统不就在硬件的推动下,自动调度了么!!!


2.3.3死循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可.操作系统的本质:就是⼀个死循环!操作系统就是基于中断向量表进行工作的。OS就是在硬件的调度下,自动调度的


2.3.4软中断

上述外部硬件中断,需要硬件设备触发。

有没有可能,因为软件原因,也触发上面的逻辑?有!

为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(可以写在我们的代码里)例如:int (老版使用)或 syscall(旧版本使用),可以让CPU内部触发中断逻辑。系统调用也是通过中断完成的!!!

所以:

用户层怎么把系统调用号给操作系统?寄存器(比如EAX)

操作系统怎么把返回值给用户?寄存器或者用户传入的缓冲区地址

系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执 行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法。

可是为什么我们用的系统调用,从来没有见过什么 int 0x80 或者 syscall 呢?都是直接调用上层的函数的啊?

那是因为Linux的gnuC标准库,给我们把几乎所有的系统调用全部封装了。

#define SYS_ify(syscall_name) __NR_##syscall_name :是⼀个宏定义,用于将系统调用的名称转换为对应的系统调用号。

比如: SYS_ify(open) 会被展开为__NR_open

而系统调用号,不是 glibc 提供的,是内核提供的,内核提供系统调用入口函数 man 2 syscall ,或者直接提供汇编级别软中断命令 int or syscall ,并提供对应的头文件或者开发入口,让上层语言的设计者使用系统调用号,完成系统调用过程

缺页中断(虚拟内存有对应的地址,但是物理内存没有对应地址),内存碎片处理,除零,野指针都会被转化为CPU内部的软中断。CPU内部软中断分为两类:一类是陷阱:int 0x80 / syscall

一类是异常:除零 野指针……OS就是躺在中断处理历程上的代码块。


2.4 用户态和内核态

1.内核页表只有一份,用户页表不同的进程各自持有一份

2.操作系统无论怎么切换进程,都能找到同一个操作系统!操作系统系统调用方法的执行是在进的地址空间执行的,而用户调用系统调用(函数,库)也是在自己的地址空间上调用的。即对于任何进程,无论如何调度,任何进程最终都会找到同一个OS

3.不管是通过哪一个进程的地址空间进入内核,都是通过软中断进入OS的。

在硬件方面:CPU知道自己处于用户态还是内核态,CS段寄存器中的CPL,0表示内核,3表示用户,CPU自己会决定要不要调用int 0x80

软件层面:系统调用:int 0x80 / syscall

4.用户是如何进入内核?1.时钟中断 2.异常 3.陷阱(int 0x80/ syscall)

用户态和内核态是一直来回切换的

5.OS是怎么运行的?

OS内核固定历程,OS会自己fork一堆进程,然后和普通进程一样,调度这些进程


三:可重入函数

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第⼀步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同⼀个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第⼀步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入一个节点,而最后只有⼀个节点真正插入链表中了。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数

如果一个函数符合以下条件之一则是不可重入的:

1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。

2.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。(之前匿名管道里面父子进程不能同时向执行流写入,否则打印乱序出错进程间通信:管道与共享内存-CSDN博客)

我们C++中学的STL中的接口基本都是不可重入函数,里面会使用全局资源扩容

一般在命名上面,函数名带_r的是可重入函数


四:volatile

标准情况下,CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,while条件不满足,退出循环,进程退出

优化情况下,键入CTRL-C ,2号信号被捕捉,执行自定义动作,修改flag=1 ,但是while条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据⼆异性的问题。while检测的flag其实已经因为优化(寄存器+优化 = 屏蔽内存的可见性),被放在了CPU寄存器当中, !flag 也是逻辑运算,也是CPU做。如何解决呢?很明显需要 volatile

volatile作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

register:建议把内容优化到寄存器,只检测寄存器不再看内存


五:SIGCHLD信号

进程中讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查 询是否有子进程结束等待清理(也就是轮询的方式)。

采用第⼀种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

void Handler(int signo)
{printf("I get a signal,signalnum : %d\n", signo);pid_t rid;while ((rid = waitpid(rid, nullptr, WNOHANG)) > 0){printf("Wait child sucess,id %d\n", rid);}printf("child is quit,id : %d", getpid());
}int main()
{signal(SIGCHLD, Handler);pid_t id = fork();if (id < 0){perror("fork");exit(2);}else if (id == 0){printf("I am child process ,pid : %d\n", getpid());sleep(3);exit(1);}else{while (1){printf("I am father process, I am doing something\n");sleep(1);}}
}

事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外⼀种办法:父进程调用sigaction将 SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程, 也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

子进程退出时,主动向父进程发出SIGCHLD信号,实现父子异步,发出信号之后回收,不需要父进程再阻塞回收。但是上面父进程使用信号回收子进程的代码还存在其他问题:

1.如果有n个信号,n个进程同时退出,向父进程发出信号,位图只会记录一次,即使不同时到达,最多也只能记录两个,一个正在处理,一个到达,之后的信号就会被屏蔽,所以存在风险

解决办法:循环回收,回收完n个进程之后,第n+1次时不确定进程个数,依然回收,此时判断

2.如果我们使用阻塞式回收呢?假设有10个进程,前6个退出,在进程走第7个进程时,就会阻塞等待第7个进程退出,如果第7个进程不退出,就会陷入死循环,第7个后面的进程退出,也回收不到

void Handler(int signo)
{printf("I get a signal,signalnum : %d\n", signo);while(true){pid_t rid = waitpid(-1,nullptr,WNOHANG);if(rid > 0){std::cout << "回收子进程成功,child id is :"<<rid<<std::endl;}else if(rid == 0){std::cout << "子进程已经全部回收完成"<<std::endl;break;}else{std::cout << "wait error"<<std::endl;break;}}}int main()
{::signal(SIGCHLD, Handler);for(int i = 1; i <= 10;i++){if(fork() == 0){sleep(5);printf("子进程退出,pid: %d\n",getpid());exit(0);}}while(true){sleep(1);}
}

http://www.lryc.cn/news/614742.html

相关文章:

  • DeepSeek辅助编写的带缓存检查的数据库查询缓存系统
  • 三方相机问题分析七:【datespace导致GPU异常】三方黑块和花图问题
  • Sum of Three Values(sorting and searching)
  • 基于MATLAB实现的毫米波大规模MIMO系统中继混合预编码设计
  • Python Day26 HTTP 协议相关笔记
  • Neo4j APOC插件安装教程
  • 论文阅读:AAAI 2024 ExpeL: LLM Agents Are Experiential Learners
  • 连锁店管理系统的库存跟踪功能:数字化转型下的零售运营核心
  • Nextcloud容器化部署新范式:Docker与Cpolar如何重塑私有云远程访问能力
  • 浅试A2A
  • 商品 SKU 计算,库存不足不能选择
  • SpringBoot的profile加载
  • C++ 模拟实现 map 和 set:掌握核心数据结构
  • 恒科持续低迷:新能源汽车股下跌成拖累,销量担忧加剧
  • Mac下安装Conda虚拟环境管理器
  • AI开发平台行业全景分析与战略方向建议
  • WPF 动画卡顿
  • Seaborn 数据可视化库:入门与进阶指南
  • 解决多线程安全性问题的方法
  • 无人设备遥控器之信号编码技术篇
  • 深入理解OpenGL Shader与GLSL:基础知识与优势分析
  • 【深度学习】动手深度学习PyTorch版——安装书本附带的环境和代码(Windows11)
  • list的简单介绍
  • 大厂求职 | 唯品会2026校园招聘正式启动!
  • “鱼书”深度学习进阶笔记(1)第二章
  • 微信小程序功能 表单密码强度验证
  • NOIP 2024 游记
  • [激光原理与应用-185]:光学器件 - BBO、LBO、CLBO晶体的全面比较
  • LoRA微调的代码细节
  • 2025年渗透测试面试题总结-07(题目+回答)