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

Linux进程信号(五)之捕捉信号

文章目录

    • 捕捉信号
      • 信号是什么时候被处理的?
      • 内核如何实现信号的捕捉
      • sigaction
      • 代码应用
      • 可重入函数
      • volatile
      • SIGCHLD信号

捕捉信号

信号是什么时候被处理的?

内核态:允许进程访问操作系统的代码和数据!
用户态:只能访问(进程)自己的代码和数据!

当进程从内核态返回到用户态的时候,进行信号的检测和处理!

返回之前,还处于内核态,而且这时候更重要的事已经做完了!

CPU在跑代码时,不仅仅跑用户自己写的代码,还跑库函数的代码和操作系统的代码!

调用系统调用,除了调用相应的函数,还需要切换身份:用户身份变成内核身份(或者反过来)。

内核态-用户态

image-20250417145521767

不允许以内核态的身份来访问用户代码,

防止用户用这种方式用操作系统的权限做非法操作。

身份切换就是将ECS寄存器的低两位比特位改变0->3/3->0

image-20250423233150572

内核如何实现信号的捕捉

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

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

举例如下:

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

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

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

内核决定返回用户态后不是恢复main函数的上下文继续执行,

而是执行sighandler函数,sighandlermain函数使用不同的堆栈空间,

它们之间不存在调用和被调用的关系,是 两个独立的控制流程。

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

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

sigaction

image-20250424230021961

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 
sigaction函数可以读取和修改与指定信号相关联的处理动作。
调用成功则返回0,出错则返回-1。
signo是指定信号的编号。
若act指针非空,则根据act修改该信号的处理动作。(输入型参数)
若oact指针非空,则通过oact传出该信号原来的处理动作。(输出型参数)

actoact指向sigaction结构体:

sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,

赋值为常数SIG_DFL表示执行系统默认动作,

赋值为一个函数指针表示用自定义函数捕捉信号,

或者说向内核注册了一个信号处理函数,

该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,

这样就可以用同一个函数处理多种信号。

显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

image-20250424230120772

代码应用

使用sa_handler

#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<signal.h>
#include<cstring>using namespace std;void handler(int signo)
{cout<<"catch a signo: "<<signo<<endl;
}int main()
{struct sigaction act,oact;memset(&act,0,sizeof(act));memset(&oact,0,sizeof(oact));act.sa_handler=handler;sigaction(2,&act,&oact);while(1){cout<<"I am a process: "<<getpid()<<endl;sleep(1);}return 0;
}

image-20250424231454700


问题1:什么时候将捕捉的信号的pending位图由1->0的呢?

思路:在handler信号捕捉执行完之前打印pending位图。

如果pending位图是 0000 0000 0000 0000 0000 0000 0000 0000

那么说明位图的改变是在捕捉信号之前

否则位图的改变就是在捕捉信号之后

#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<signal.h>
#include<cstring>using namespace std;//问题1:pending位图,什么时候由1->0
//思路:在handler信号捕捉执行完之前打印pending位图。
//如果pending位图是 0000 0000 0000 0000 0000 0000 0000 0000
//那么说明位图的改变是在捕捉信号之前
//否则位图的改变就是在捕捉信号之后
void PrintPending()
{sigset_t set;sigemptyset(&set);sigpending(&set);for(int i=31;i>=1;i--){if(sigismember(&set,i)){cout<<"1";}else{cout<<"0";}}cout<<endl;
}void handler(int signo)
{PrintPending();cout<<"catch a signo: "<<signo<<endl;
}int main()
{struct sigaction act,oact;memset(&act,0,sizeof(act));memset(&oact,0,sizeof(oact));act.sa_handler=handler;sigaction(2,&act,&oact);while(1){cout<<"I am a process: "<<getpid()<<endl;sleep(1);}return 0;
}

image-20250424232724787

结论:在处理信号之前,pending位图就已经由1->0了。


问题二:信号被处理的时候,对应的信号也会被添加到block表中,防止信号被嵌套捕捉!

思路:

在信号捕捉(处理)方法里面写一个死循环,

那么就一直是在捕捉信号的过程,

再来一个2号信号,因为2号信号的block位图是1,所以只能阻塞(不能递达),

所以2号信号的pending0->1

image-20250424234244352

结论:

当某个信号的处理函数被调用时,

内核自动将当前信号加入进程的信号屏蔽字,

防止该信号被嵌套捕捉,

当信号处理函数返回时自动恢复原来的信号屏蔽字,

这样就保证了在处理某个信号时,

如果这种信号再次产生,

那么它会被阻塞到当前处理结束为止。

在阻塞期间,该信号发送了n次,但是最终也只会被记录一次。


使用sa_mask

如果在调用信号处理函数时,

除了当前信号被自动屏蔽之外,

还希望自动屏蔽另外一些信号,

则用sa_mask字段说明这些需要额外屏蔽的信号,

当信号处理函数返回时自动恢复原来的信号屏蔽字。

对比实验:

没有手动屏蔽任何信号

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>using namespace std;void PrintPending()
{sigset_t set;sigemptyset(&set);sigpending(&set);for (int i = 31; i >= 1; i--){if (sigismember(&set, i)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int signo)
{cout << "catch a signo: " << signo << endl;while (1){PrintPending();sleep(1);}//该循环就是在处理信号
}int main()
{struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(oact));// sigemptyset(&act.sa_mask);// sigaddset(&act.sa_mask,1);// sigaddset(&act.sa_mask,3);// sigaddset(&act.sa_mask,4);act.sa_handler = handler;//SIG_DFL SIG_IGNsigaction(2, &act, &oact);while (1){cout << "I am a process: " << getpid() << endl;sleep(1);}return 0;
}

image-20250424235334636

手动屏蔽1,3,4号信号

sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,1);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);

image-20250424235546122

sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,

sa_sigaction是实时信号的处理函数。

可重入函数

可重入函数

main函数调用insert函数向一个链表head中插入节点node1,

插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,

再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,

sighandler也调用insert函数向同一个链表head中插入节点node2,

插入操作的两步都做完之后从sighandler返回内核态,

再次回到用户态就从main函数调用的insert函数中继续往下执行,

先前做第一步之后被打断,现在继续做完第二步。

结果是,main函数和sighandler先后向链表中插入两个节点,

而最后只有一个节点真正插入链表中了。

image-20250417145534585

像上例这样,insert函数被不同的控制流程调用,、

有可能在第一次调用还没返回时就再次进入该函数,这称为:重入,

insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为:不可重入函数,

反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

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

调用了mallocfree,因为malloc也是用全局链表来管理堆的。

调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile

保存内存可见性!!

该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>using namespace std;
int flag=0;void handler(int signo)
{cout<<"catch a sig: "<<signo<<endl;flag=1;
}int main()
{signal(2,handler);while(!flag);cout<<"process normal quit!"<<endl;return 0;
}

image-20250425200844784

标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,

修改 flag=1while 条件不满足,退出循环,进程退出。

因为main函数和handler函数是两个不同的执行流,

flag的值在main函数中没有修改flagmain函数只对flag进行了逻辑判断(检测),

算数运算/逻辑运算都是在cpu内部进行的,

所以在优化条件下,flag 变量可能直接被优化到寄存器中。

优化

image-20250425201818311

mysignal:mysignal.ccg++ -o $@ $^ -O3 -std=c++11
.PHONY:clean
clean:rm -rf mysignal

image-20250425201949053

优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,

修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!

但是很明显flag肯定已经被修改了,但是为何循环依旧执行?

很明显, while 循环检查的flag,

并不是内存中最新的flag,这就存在了数据二异性的问题。

while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。

image-20250425203237625

如何解决呢?很明显需要 volatile

volatile int flag=0;

image-20250425203332549

volatile 作用:防止过度优化,保持内存的可见性,

告知编译器,被该关键字修饰的变量,不允许被优化,

对该变量的任何操作,都必须在真实的内存中进行操作。

SIGCHLD信号

之前讲过用waitwaitpid函数清理僵尸进程,

父进程可以阻塞等待子进程结束,

也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。

采用第一种方式,父进程阻塞了就不能处理自己的工作了;

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

image-20250425203751546

其实,子进程在终止时会给父进程发SIGCHLD信号,

该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,

这样父进程只需专心处理自己的工作,不必关心子进程了,

子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

验证:在子进程退出时,父进程会收到17号信号

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>using namespace std;void handler(int signo)
{cout<<"I am "<<getpid()<<" catch a signal: "<<signo<<endl;
}int main()
{signal(17,handler);pid_t id=fork();if(id==0){while(1){cout<<"I am child , pid: "<<getpid()<<" , ppid: "<<getppid()<<endl;sleep(1);break;}cout<<"child quit!!!!!!!!!!"<<endl;exit(1);}while(1){cout<<"I am father , pid: "<<getpid()<<endl;sleep(1);}return 0;
}

image-20250425204758043

父进程在进行等待的时候,我们可以采用基于信号的方式异步等待。

等待的好处:

1.获取子进程的退出状态,释放子进程的僵尸。

虽然我们不知道父子进程谁先运行,但是我们知道父进程一定最后退出!

还是得调用 wait/waitpid 这样的接口!

并且要保证父进程是一直在运行的。(防止子进程变孤儿)

请编写一个程序完成以下功能:父进程fork出子进程,

子进程调用exit终止,父进程自定义SIGCHLD信号的处理函数,

在其中调用wait/waitpid等待子进程退出。

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include<sys/wait.h>
#include<sys/types.h>using namespace std;void handler(int signo)
{sleep(3);pid_t rid=waitpid(-1,nullptr,0);cout<<"I am "<<getpid()<<" catch a signal: "<<signo<<" ,child quit: "<<rid<<endl;
}int main()
{signal(17,handler);pid_t id=fork();if(id==0){while(1){cout<<"I am child , pid: "<<getpid()<<" , ppid: "<<getppid()<<endl;sleep(5);break;}cout<<"child quit!!!!!!!!!!"<<endl;exit(1);}while(1){cout<<"I am father , pid: "<<getpid()<<endl;sleep(1);}return 0;
}

image-20250425205944924

如果我们有5个子进程,

同时退出?

那么在捕捉方法里设置一个循环,不断等待子进程退出即可。

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>using namespace std;void handler(int signo)
{sleep(3);pid_t rid;while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0){cout << "I am " << getpid() << " catch a signal: " << signo << " ,child quit: " << rid << endl;}
}int main()
{signal(17, handler);for (int i = 0; i < 5; i++){pid_t id = fork();if (id == 0){while (1){cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;sleep(5);break;}cout << "child quit!!!!!!!!!!" << endl;exit(1);}}while (1){cout << "I am father , pid: " << getpid() << endl;sleep(1);}return 0;
}

image-20250425211328656

退出一半?

在等待的时候,使用非阻塞等待,就不会一直卡在没有退出的子进程上了。

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include<ctime>
#include <sys/wait.h>
#include <sys/types.h>using namespace std;void handler(int signo)
{sleep(3);pid_t rid;while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0){cout << "I am " << getpid() << " catch a signal: " << signo << " ,child quit: " << rid << endl;}
}int main()
{srand(time(nullptr));signal(17, handler);for (int i = 0; i < 5; i++){pid_t id = fork();if (id == 0){while (1){cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;sleep(5);break;}cout << "child quit!!!!!!!!!!" << endl;exit(1);}sleep(rand()%5+3);}while (1){cout << "I am father , pid: " << getpid() << endl;sleep(1);}return 0;
}

image-20250425212208515

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:

父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,

这样fork出来的子进程在终止时会自动清理掉,

不会产生僵尸进程,也不会通知父进程。

系统默认的忽略动作和用户用sigaction函数自定义的忽略

通常是没有区别的,但这是一个特例。

此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

请编写程序验证这样做不会产生僵尸进程。

测试代码

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>using namespace std;int main()
{signal(17, SIG_IGN);// SIG_DFL -> IGNfor (int i = 0; i < 5; i++){pid_t id = fork();if (id == 0){while (1){cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;sleep(3);break;}cout << "child quit!!!!!!!!!!" << endl;exit(1);}sleep(1);}while (1){cout << "I am father , pid: " << getpid() << endl;sleep(1);}return 0;
}

image-20250425213317158

相关文章:

  • c++11特性——lambda对象、包装器
  • 市面上有好用的拓客系统吗?
  • 计算机发展史
  • Java中static关键字深度解析:从入门到高阶实战
  • Vaptcha 手势、轨迹验证码
  • Unity飞机大战-射击类游戏3D虚拟
  • 嵌入式STM32学习——串口USART 2.2(串口中断接收)
  • 腾讯音乐二面
  • VLLM在linux下部署
  • 2023CCPC东北四省赛题解
  • python正方形面积 2024年信息素养大赛复赛/决赛真题 小学组/初中组 python编程挑战赛 真题详细解析
  • embedding的微调
  • 有动画效果,但动画窗格里为空
  • HJ33 整数与IP地址间的转换【牛客网】
  • 让电脑不再卡,从清理系统做起
  • Python Web开发基础
  • 【Linux笔记】——网络基础
  • 小林八股Java集合笔记(8k字概要版)
  • 【题解-洛谷】P11951 [科大国创杯初中组 2023] 数数
  • 数仓-概念模型、逻辑模型、物理模型介绍
  • 网络课程系统网站建设费用/优化搜狗排名
  • 网站做外链/惠东seo公司
  • 网站建设流程ppt/如何制作公司网页
  • 做政府网站的公司/媒体软文发布平台
  • 网站怎么做才被收录快/国家再就业免费培训网
  • 厦门市建设局网站住房保障/深圳靠谱网站建设公司