Linux进程信号

1.信号的概念

什么是信号,在现实世界里信号其实就是一种通知事件。信号通常和我们正在进行的事件是异步的,而且在信号来临之前我们就知道信号来了我们要干什么了。比如红绿灯,红灯亮了就不能走了,绿灯亮了就可以走了。但是红绿灯亮不亮都不影响我们正在做的事情。比如我在打游戏时红的突然亮了,这两件事情就是异步的了。当然在操作系统里也一样,但操作系统的信号是进程之间事件异步通知的一种方式,它属于软中断。在命令行下可以通过kill -l来查看信号。如下图。

31及其以前的我们称为标准型号,也叫不可靠信号。后面的信号统称为实时信号。它们的区别在于:标准信号可能会丢失且处理顺序不确定,而实时信号不会丢失信号且处理顺序确定。因为标准信号是由位图结构保存的,而实时信号是链表保存的。信号的处理又分为默认处理、忽略和自定义处理。一个信号想要达到通知事件的目的就要分为三步:信号产生、信号保存信号处理。

2.产生信号

信号产生的方式有很多种,键盘产生、kill指令、系统调用、软件条件、硬件异常。我们可以使用ctrl+c/ctrl+\来中断程序,也就是所谓的键盘产生信号。那么问题来了,OS是怎么知道键盘有数据的呢?

2.1理解OS如何得知键盘有数据

首先OS是软硬件的资源管理者,键盘上有数据它是必须要知道的。那么键盘这些硬件什么时候有数据呢,这是不确定的如果一直访问键盘查看其是否有数据确实可以解决,但是这样的效率太低了。所以真实的情况是硬件什么时候有数据了再来通知OS。以键盘为例大概描述一下:当我们按下键盘时会向cpu发送硬件中断,cpu会识别硬件中断的信息,实际上就是高低电压,cpu通过·硬件中断的信息来确定要调用OS处理键盘数据的代码,然后OS将外设的数据读取到内存中等待下一步处理。

2.2函数产生信号

所谓的函数产生信号,实际上就是调用系统调用只不过c语言封装了系统调用。产生信号的系统调用有很多,kill函数可以给指定的进行发送指定的信号,kill命令就是调用的kill函数实现的。raise函数可以向当前进程发生指定信号。abort函数使当前进程接收到信号而异常终止。

2.3软件条件产生信号

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产⽣。

2.4硬件异常产生信号

硬件异常通常是硬件检测到异常通知内核,然后内核再对当前进程发送特定的信号。比如除0操作cpu就会抛异常,内核将这个异常解释为SIGFPE信号发送给当前进程。还有非法寻址等操作。

2.4.1子进程退出core dump

子进程退出码是一个整数32位,其中我们只用低16位。其中低16位的前8位是子进程的退出的返回值,0到6位为子进程收到的信号,第7为就是core dump位,它表示子进程退出时是否创建了core dump文件。在进程出现BUG而异常终止时,可以通过调试工具加core dump文件来确定错误在哪,这种方式叫做事后调试。默认情况下是不允许生成core dump文件的,当然也可以通过ulimit命令来修改限制。

3.保存信号

进程在收到信号后并不会立即处理,因为进程收到信号时可能在做更重要的事情。所以进程在收到信号之后要先把信号保存起来。实际处理信号的动作叫做信号递达,从接收到信号到信号递达之前的状态叫信号未决,进程可以阻塞某个信号。被阻塞的信号将保持未决状态,直到解除对该信号的阻塞。注意阻塞和忽略不同的,阻塞是让信号保持未决状态而忽略是递达的处理结果。

3.1内核中的表示

在内核里不同的进程有不同的信号,而不同的信号又有不同的状态。所以操作系统要管理它们。在内核里使用三个位图来管理信号的。它们分别为block(阻塞)表它只要用来存储信号是否阻塞的,pending(未决)主要存储信号是否存在的,handler主要存储信号的处理方法的。因为信号是通过pending中的0、1来表示是否存在的,所以发送多个信号时可能会丢失。阻塞信号在解除阻塞后,pending对应的1应该变为0,这一步应该在递达完成前完成。

3.2信号保存相关的函数

信号的产生有很多种,所以对应的pending表的函数我们只看sigpending函数,它的只用是获取当前进程的pending表。block是阻塞信号集,又叫信号屏蔽字。可以通过sigprockmask函数来读取或修改信号屏蔽字。

复制代码
#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。

4.信号捕捉

4.1信号捕捉的流程

信号捕捉的流程为:1.进程在用户态正常执行指令时,信号可通过硬件异常、其他进程发送等方式产生,内核会将未被进程 blocked 集合阻塞的信号标记到其 pending(待处理)信号集中;2. 当进程因中断、异常或系统调用从用户态陷入内核态,内核先处理完当前中断 / 异常 / 系统调用;3. 内核准备返回用户态前,检查进程的 pending 集合,若无未阻塞的待处理信号,直接返回用户态从中断的指令处继续执行;若有则按信号处理动作分情况:动作是 SIG_IGN(忽略)则内核清空该信号 pending 标记后返回用户态,动作是 SIG_DFL(默认)则内核执行默认行为(如终止进程)无需返回,动作是自定义处理函数(捕捉)则进入下一步;4. 内核修改进程执行上下文(将返回地址改为自定义处理函数入口),返回用户态执行该处理函数; 处理函数执行完毕后,触发 sigreturn 系统调用再次陷入内核态; 5.内核在 sigreturn 处理中恢复进程原执行上下文、清空该信号 pending 标记,再次返回用户态; 进程回到用户态后,从之前中断指令的下一条指令处继续执行。查考下图。

4.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函数调⽤,⽽是被系统所调⽤。struct sigaction中的sa_mask字段说明想要额外屏蔽的信号。

4.3操作系统是怎么运行的

在以往的介绍中我们只知道操作系统是软硬件资源的管理者,实际上简陋来说操作系统是一个基于中断的死循环的软件集。

4.3.1硬件中断

硬件中断的步骤为:1.外设就绪。2.向中断控制器发起中断,获得中断号。3.通知CPU。4.CPU从中断控制器获取中断号。5.CPU保护中断前的指令及数据。6.CPU根据中断号查询中断向量表,执行对应的中断方法。7.执行完毕恢复现场,中断处理完成继续完成之前的工作。

4.3.2时钟中断

时钟中断就是一个时钟源以固定的频率向CPU发送中断,在之前的《linux进程概念》里有提到过时间片的概念,那么到底什么是时间片呢?实际上时钟源发送的时钟中断会在中断控制器里计数,达到一定次数后通知CPU该切换进程了。所以时间片就是发送一次时钟信号的时间乘以数量就是时间片。这也就是为什么CPU主频越快性能越好的由来。

4.3.3死循环

通过以上描述我们可以得知,只有CPU收到中断或异常时才会运行操作系统的方法。操作系统自己并没有做什么,想要什么方法往中断向量表里添加就可以了。操作系统本质上就是一个死循环。

4.3.4软中断

软中断其实就是模仿硬件中断来实现的,那么软件是怎么触发中断的呢?其实CPU有两个指令int 0x80、syscall会陷入内核,本质就是触发软中断,CPU执行系统调用的处理方法。系统调用在内核也是被封装为一个表,既然有表就有系统调用号,这是通过系统调用自动查询执行对应方法。

4.4内核态和用户态

进程空间分为用户区[0,3GB]和内核区[3,4GB],用户区和内核区的区别是内核区是所以进程共享的,不同的进程的虚拟地址可能不同,但在内存上只有一份内核区的数据,也就是说不同的内核区的虚拟地址也只能映射到相同物理地址 。这也就说明了CPU在进行进程切换时是怎么找到内核的。用户不能直接执行内核的代码,内核也不会执行用户的代码。在CPU的cs寄存器或者ss(段寄存器)的最低2位(CPL)存储的就是表示状态的,3为用户态、0为内核态。进入内核时把CPL变为0,返回用户态时变为3。在页表上有对应的特权位0、3,CPU会拿它和cs.CPL进行对比,不满足就报错,这是为了防止用户态的野指针随机访问到内核区。

5.可重入函数

重入(Reentrancy)的核心定义:一个函数在尚未执行完成的情况下(比如被中断、多线程抢占、嵌套调用等原因暂停),再次被调用并开始执行,这个函数被 "重入",该过程称为重入;如果一个函数在被重入后出现结果不正确、资源泄漏等问题就说明它是不可被重入函数,反之被重入后没有出现问题的就是可重入函数。如果一个函数满足了以下的条件之一就是不可被重入函数:1.调用malloc或free、调用了标准I/O库函数。

6.volatile

被volatile关键字修饰的叫做易变变量。CPU访问变量时一般是先从内存里提取变量到寄存器里,再从寄存器里提取。但是编译器有时会对一些特殊的变量直接提取到寄存器里,这时即使变量的值改变了CPU也不知道。volatile就是为了防止编译器将一些元素优化到寄存器中,是这个变量保持正常的从内存里提取。可以参考一下代码。

复制代码
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
相关推荐
一勺菠萝丶1 小时前
芋道框架 - API 前缀区分机制
java·linux·python
西木Qi3 小时前
Centos10及下载
linux
面对疾风叭!哈撒给3 小时前
Linux之Docker安装Mysql 8.0+
linux·mysql·docker
代码AC不AC3 小时前
【Linux】进程池
linux·主从模式·进程池
feng一样的男子3 小时前
Rocky Linux 9 配置 IPv6 完整指南
linux·网络
十五年专注C++开发3 小时前
Linux 下用 VS Code 高效调试
linux·运维·服务器·c++·vscode
Sylvia33.3 小时前
体育数据API实战:用火星数据实现NBA赛事实时比分与状态同步
java·linux·开发语言·前端·python
大胖某人3 小时前
Kali系统安装OpenClaw调用DeepSeek API部署方法详解
linux·人工智能
七夜zippoe3 小时前
OpenClaw CLI 完整命令手册
linux·服务器·网络·cli·openclaw·命令手册