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

[linux仓库]信号处理及可重入函数[进程信号·陆]

🌟 各位看官好,我是

🌍 Linux == Linux is not Unix !

🚀 今天来学习Linux的信号处理及可重入函数。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!

目录

回归信号处理

sigaction

可重入函数

volatile(易变关键字)(场景+解决方案)

基于信号回收进程

总结

阅读了解

用户态和内核态

用户态与内核态的切换

什么情况会导致用户态到内核态切换?

切换时 CPU 需要做什么?

切换流程


回归信号处理

 

sigaction

int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

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);};
  • 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是实时信号的处理函数,本章不详细解释这两个字段。

void handler(int signo)
{std::cout << "我获取到一个信号: " << signo << std::endl;while (true){sigset_t pending;sigemptyset(&pending);sigpending(&pending);for (int i = 31; i > 0; i--){if (sigismember(&pending, i))std::cout << "1";elsestd::cout << "0";}std::cout << std::endl;sleep(1);}
}int main()
{struct sigaction act, old;act.sa_handler = handler;sigaddset(&(act.sa_mask), 3); // 并没有设置到内核sigaddset(&(act.sa_mask), 4);sigaddset(&(act.sa_mask), 5);sigaddset(&(act.sa_mask), 6);sigaction(2, &act, &old); // 将2号信号的捕捉方法,设置到内核中!while (true){std::cout << "我是一个进程: " << getpid() << std::endl;sleep(1);}return 0;
}

可重入函数

  • main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的两步都做完之后从sighandler返回内核态,再次回到⽤⼾态就从main函数调⽤的insert函数中继续往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后向链表中插⼊两个节点,⽽最后只有⼀个节点真正插⼊链表中了。
  • 像上例这样,insert函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函数,这称为重⼊,insert函数访问⼀个全局链表,有可能因为重⼊⽽造成错乱,像这样的函数称为不可重⼊函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数,则称为可重⼊(Reentrant) 函数。想⼀下,为什么两个不同的控制流程调⽤同⼀个函数,访问它的同⼀个局部变量或参数就不会造成错乱?

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

  • 调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
  • 调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。

volatile(易变关键字)(场景+解决方案)

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

 

  • 标准情况下,键⼊ CTRL-C ,2号信号被捕捉,执⾏⾃定义动作,修改 flag=1 , while 条件不满⾜, 退出循环,进程退出
  • 优化情况下,键⼊ CTRL-C ,2号信号被捕捉,执⾏⾃定义动作,修改 flag=1 ,但是 while 条件依旧满⾜,进程继续运⾏!但是很明显flag肯定已经被修改了,但是为何循环依旧执⾏?很明显, while 循环检查的 flag,并不是内存中最新的 flag,这就存在了数据⼆异性的问题。 while 检测的 flag 其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile

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

由于编译器的自作充满,导致cpu每次读数据都是从内存寄存器里面读,即使更改了数据,但是并没有更改寄存器里的内容,因此需要volatile关键字,禁止编译器对这部分进行优化,保持内存可见性!!!

    

基于信号回收进程

进程⼀章讲过⽤wait和waitpid函数清理僵⼫进程,⽗进程可以阻塞等待⼦进程结束,也可以⾮阻 塞地查询是否有⼦进程结束等待清理(也就是轮询的⽅式)。采⽤第⼀种⽅式,⽗进程阻塞了就不 能处理⾃⼰的⼯作了;采⽤第⼆种⽅式,⽗进程在处理⾃⼰的⼯作的同时还要记得时不时地轮询⼀ 下,程序实现复杂。

其实,⼦进程在终⽌时会给⽗进程发SIGCHLD信号,该信号的默认处理动作是忽略(父进程对子进程的处理SIGCHLD, signal,SIG DFL(上层其实是DFL,内核中默认被设置成为了忽略)),⽗进程可以⾃ 定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程 终⽌时会通知⽗进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。

请编写⼀个程序完成以下功能:⽗进程fork出⼦进程,⼦进程调⽤exit(2)终⽌,⽗进程⾃定 义SIGCHLD信号的处理函数, 在其中调⽤wait获得⼦进程的退出状态并打印。

事实上,由于UNIX 的历史原因,要想不产⽣僵⼫进程还有另外⼀种办法:⽗进程调 ⽤sigaction将SIGCHLD的处理动作置为SIG_IGN(底层:回收子进程),这样fork出来的⼦进程在终⽌时会⾃动清理掉,不 会产⽣僵⼫进程,也不会通知⽗进程。系统默认的忽略动作和⽤⼾⽤sigaction函数⾃定义的忽略 通常是没有区别的,但这是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可 ⽤。请编写程序验证这样做不会产⽣僵⼫进程。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{pid_t id;while( (id = waitpid(-1, NULL, WNOHANG)) > 0) {printf("wait child success: %d\n", id);} printf("child is quit! %d\n", getpid());
} int main()
{signal(SIGCHLD, handler);pid_t cid;if((cid = fork()) == 0){ //childprintf("child : %d\n", getpid());sleep(3);exit(1);}while(1){printf("father proc is doing some thing!\n");sleep(1);} return 0;
}

场景1: 如果我们创建了多个子进程,多个子进程几乎同时退出

此时可能只能回收一个进程呀,因此是不是需要以非阻塞轮询方式回收每一个子进程,但是此时会出现情景2.

场景2:如果我们创建了多个子进程,一部分退出,一部分不退出,回收时并不知道哪些退了.

我们要回收子进程,有没有一种全新的方案呢?

void handler(int signo)
{while (1){pid_t rid = waitpid(-1, NULL, WNOHANG); // 会被阻塞if (rid > 0)printf("%d进程, 收到了信号: %d, 回收子进程:%d\n", getpid(), signo, rid);else if (rid < 0)break;elsebreak;}
}int main()
{// signal(SIGCHLD, handler);signal(SIGCHLD, SIG_IGN);pid_t id = fork();if (id == 0){// 子进程printf("我是子进程: %d\n", getpid());sleep(3);exit(2);}// 父进程while (1){printf("我是父进程:%d\n", getpid());sleep(1);}
}

总结

本文介绍了Linux信号处理机制,重点讲解了sigaction函数的用法和原理。sigaction函数可以读取和修改指定信号的处理动作,支持自定义信号处理函数,并自动管理信号屏蔽字。文章通过示例代码演示了如何注册信号处理函数,并讨论了信号处理中的可重入函数问题。同时,讲解了volatile关键字在信号处理中的重要性,它能防止编译器优化导致的数据不一致问题。最后,文章介绍了基于SIGCHLD信号回收子进程的方法,包括非阻塞轮询方式和忽略信号自动回收的两种方案,并提供了相关代码实现。这些内容为Linux系统编程中的信号处理提供了实用指导。

阅读了解

用户态和内核态

CPU 指令集 :是 CPU 实现软件指挥硬件执⾏的媒介,具体来说每⼀条汇编语句都对应了⼀条CPU 指令 ,⽽⾮常⾮常多的 CPU 指令 在⼀起,可以组成⼀个、甚⾄多个集合,指令的集合叫 CPU 指令集 。

CPU 指令集 有权限分级,⼤家试想, CPU 指令集 可以直接操作硬件的,要是因为指令操作的不规范,造成的错误会影响整个计算机系统的。好⽐你写程序,因为对硬件操作不熟悉,导致操作系统内核、及其他所有正在运⾏的程序,都可能会因为操作失误⽽受到不可挽回的错误,最后只能重启计算机才⾏。(对开发⼈员来说是个艰巨的任务,还会增加负担,同时开发⼈员在这⽅⾯也不被信任,所以操作系统内核直接屏蔽开发⼈员对硬件操作的可能,都不让你碰到这些 CPU 指令集 .)

针对上⾯的需求,硬件设备商直接提供硬件级别的⽀持,做法就是对 CPU 指令集 设置了权限,不同级别权限能使⽤的 CPU 指令集 是有限的,以 Inter CPU 为例,Inter把 CPU 指令集 操作的权限由⾼到低划为4级:

  • ring 0:权限最⾼,可以使⽤所有 CPU 指令集
  • ring 1
  • ring 2
  • ring 3:权限最低,仅能使⽤常规 CPU 指令集 ,不能使⽤操作硬件资源的 CPU 指令集 ,⽐如 IO 读写、⽹卡访问、申请内存都不⾏

要知道的是,Linux系统仅采⽤ring 0 和 ring 3 这2个权限。CPU中有⼀个标志字段,标志着线程的运⾏状态,⽤⼾态为3,内核态为0。

  • ring 0被叫做内核态,完全在操作系统内核中运⾏(执⾏内核空间的代码,具有ring 0保护级别,有对硬件的所有操作权限,可以执⾏所有 CPU指令集 ,访问任意地址的内存,在内核模式下的任何异常都是灾难性的,将会导致整台机器停机)
  • ring 3被叫做用户态,在应⽤程序中运⾏(在用户模式下,具有ring 3保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调⽤系统接⼝(System Call APIs)来达到访问硬件和内存,在这种保护模式下,即时程序发⽣崩溃也是可以恢复的,在电脑上⼤部分程序都是在,用户模式下运⾏的)

低权限的资源范围较⼩,⾼权限的资源范围更⼤,所以⽤⼾态与内核态的概念就是CPU 指令集权限的区别。

我们通过指令集权限区分⽤⼾态和内核态,还限制了内存资源的使⽤,操作系统为⽤⼾态与内核态划分了两块内存空间,给它们对应的指令集使⽤。

在内存资源上的使⽤,操作系统对⽤⼾态与内核态也做了限制,每个进程创建都会分配虚拟空间地址,以Linux32位操作系统为例,它的寻址空间范围是 4G (2的32次⽅),⽽操作系统会把虚拟控制地址划分为两部分,⼀部分为内核空间,另⼀部分为⽤⼾空间,⾼位的 1G (从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使⽤,⽽低位的 3G (从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使⽤。

  1. 用户态:只能操作 0-3G 范围的低位虚拟空间地址
  2. 内核态: 0-4G 范围的虚拟空间地址都可以操作,尤其是对 3-4G 范围的⾼位虚拟空间地址必须由内核态去操作
  • 3G-4G 部分⼤家是共享的(指所有进程的内核态逻辑地址是共享同⼀块内存地址),是内核态的地址空间,这⾥存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。 
  • 在内核运⾏的过程中,会涉及内核栈的分配,内核的进程管理的代码会将内核栈创建在内核空间中,当然相应的⻚表也会被创建。

用户态与内核态的切换

什么情况会导致用户态到内核态切换?

  • 系统调⽤ :用户态进程主动切换到内核态的⽅式,⽤⼾态进程通过系统调⽤向操作系统申请资源完成⼯作,例如 fork()就是⼀个创建新进程的系统调⽤。(操作系统提供了中断指令int 0x80来主动进⼊内核,这是⽤⼾程序发起的调⽤访问内核代码的唯⼀⽅式。调⽤系统函数时会通过内联汇编代码插⼊int 0x80的中断指令,内核接收到int 0x80中断后,查询中断处理函数地址,随后进⼊系统调⽤。)
  • 异常 :当 CPU 在执⾏⽤⼾态的进程时,发⽣了⼀些没有预知的异常,这时当前运⾏进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺⻚异常
  • 中断 :当 CPU 在执⾏⽤⼾态的进程时,外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执⾏下⼀条即将要执⾏的指令,转到与中断信号对应的处理程序去执⾏,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执⾏后边的操作等

切换时 CPU 需要做什么?

  • 当某个进程中要读写 IO ,必然会⽤到 ring 0 级别的 CPU 指令集 。⽽此时 CPU 的指令集操作权限只有 ring 3,为了可以操作ring 0 级别的 CPU 指令集 , CPU 切换指令集操作权限级别为ring 0(可称之为提权),CPU再执⾏相应的ring 0 级别的 CPU 指令集 (内核代码)。
  • 代码发⽣提权时,CPU 是需要切换栈的!!前⾯我们提到过,内核有⾃⼰的内核栈。CPU 切换栈是需要栈段描述符(ss寄存器)和栈顶指针(esp寄存器),这两个值从哪⾥来?(CPU通过⼀个段寄存器(tr)确定 TSS(任务状态段,struct TSS) 的位置。在TSS结构中存在这么⼀个 SS0 和 ESP0。提权的时候,CPU就从这个TSS⾥把SS0和ESP0取出来,放到 ss和 esp 寄存器中。)

切换流程

  1. 从用户态切换到内核态时,⾸先用户态可以直接读写寄存器,⽤⼾态操作CPU,将寄存器的状态保存到对应的内存中,然后调⽤对应的系统函数,传⼊对应的⽤⼾栈地址和寄存器信息,⽅便后续内核⽅法调⽤完毕后,恢复用户⽅法执⾏的现场。
  2. 从用户态切换到内核态需要提权,CPU 切换指令集操作权限级别为 ring 0。
  3. 提权后,切换内核栈。然后开始执⾏内核⽅法,相应的⽅法栈帧时保存在内核栈中。
  4. 当内核⽅法执⾏完毕后,CPU切换指令集操作权限级别为 ring 3,然后利⽤之前写⼊的信息来恢复用户栈的执⾏。

从上述流程可以看出用户态切换到内核态的时候,会牵扯到⽤⼾态现场信息的保存以及恢复,还要进⾏⼀系列的安全检查,还是⽐较耗费资源的。

http://www.dtcms.com/a/519619.html

相关文章:

  • webrtc源码走读(一)-QOS-NACK-概述
  • wordpress 企业网站 免费如何注册网站免费的
  • 斗地主游戏源码,自适应手机版,带有管理后端
  • Linux桌面X11服务-XRecord方案捕获鼠标点击的应用窗口
  • 021数据结构之并查集——算法备赛
  • 网站制作售后免费在线代理网站
  • Vue组件的一些底层细节
  • 2. =>的用法 C#例子 WPF例子
  • 在C#中出现WinForm原控件Chart卡顿问题
  • Spring Boot 3零基础教程,WEB 开发 内嵌服务器底层源码分析 笔记48
  • 网站开发案例分析成都制作网页
  • 导入的 Google(Chrome)书签默认不会自动显示在「书签栏」,而是放在一个文件夹里。下面是详细步骤,帮你把 导入的全部书签添加到书签栏
  • 一小时内使用NVIDIA Nemotron创建你自己的Bash计算机使用智能体
  • Chrome开发者工具
  • 虚拟机 Ubuntu 中安装 Google Chrome 浏览器
  • Docker/K8s部署MySQL的创新实践与优化技巧大纲
  • 网站建设管理流程避免网站侵权
  • 如何在Visual Studio中配置C++环境?
  • 珠海翻译公司高效翻译服务 2025年10月
  • 网站后台管理系统怎么登陆鄂州网站建设与设计
  • 建设系统网站企业密信下载app下载官网
  • 算法面经常考题整理(1)机器学习
  • 使用java如何进行接口测试
  • 机器学习-方差与偏差
  • 甘肃省网站建设咨询seo最好的网站源码
  • 3.序列式容器-heap
  • Module JDK is not defined 警告解决
  • 柞水县住房和城乡建设局网站网站建设客户分析调查表文档
  • html`contenteditable`
  • 【语音识别】语音识别的发展历程