【Linux】信号处理(3)信号处理&&valatile关键字

目录

[一 信号处理剩余部分](#一 信号处理剩余部分)

[3 穿插话题---操作系统是怎么运行起来的](#3 穿插话题---操作系统是怎么运行起来的)

(1)硬件中断

(2)时钟中断

(3)死循环

(4)软中断

[4 重谈用户态和内核态](#4 重谈用户态和内核态)

[二 可重入函数](#二 可重入函数)

[三 valatile 关键字](#三 valatile 关键字)

[四 SIGCHLD信号](#四 SIGCHLD信号)


一 信号处理剩余部分

3 穿插话题---操作系统是怎么运行起来的

(1)硬件中断

操作系统在被编写时,就已经针对各种中断情况(键盘,网卡等等外设),有处理方法

所有的处理方法,形成表结构,叫做中断向量表IOT;把处理方法,当成函数指针数组有下标

所有的外设和中断控制器连接,中断控制器和CPU连接

中断向量表是由操作系统提供

由外部设备触发的中断,叫做硬件中断

主板上的时钟源晶振是一个硬件外设,它会以固定的时间周期(本质就是时间,通常 1ms,即1/1000HZ),向 CPU 发送硬件中断。
操作系统本身并非持续执行调度任务,而是以 "死循环" 的方式等待事件触发;所有进程调度、任务切换等核心工作,本质上都是由各类硬件中断(尤其是时钟中断)驱动 完成的。

因此,操作系统的核心工作原理,就是基于中断机制运行的软件集合。

而时钟中断触发的固定时间间隔,就是进程调度的时间片

时钟源晶振可以自己触发时钟中断;时钟中断是让操作系统调度的动力源头

计数器的本质就是时间,在软件层面上,把时间进行量化成次数

进程task_struct,都会有时间片的概念;时间片本质就是一个计数器 int counter

当代计算机系统中有一个全局变量jiffiies嘀嗒,中断一次,jiffies就++,累计中断多少次

(2)时钟中断

当代很多计算机会把晶振集成在CPU内部,就不用走外部了;晶振可以通过硬件计数器分频,实现稳定输出1m/s

问题:

• 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?

• 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?

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

时钟中断 是由主板 / CPU 上的时钟源晶振(定时器硬件) 以固定时间周期(通常为 1ms,即 1/1000Hz)主动触发的硬件中断,是操作系统的「系统心跳」,也是所有时间相关机制的硬件基础。
核心本质:
它是唯一能定期、自动触发的硬件中断,不依赖用户 / 外部设备操作,是操作系统能自主运行的根本动力。
本质是把连续的时间量化为离散的时间片,让操作系统能精准管理时间、调度进程

CPU是中断处理的核心

(3)死循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可。操作系统的本质:就是一个死循环!

cpp 复制代码
void main(void) /* 这⾥确实是void,并没错。 */ 
{ /* 在startup 程序(head.s)中就是这样假设的。 */ 
 ...
 /*
 * 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返 
 * 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任 
 * 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时), 
 * 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没 
 * 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。 
 */
 for (;;)
 pause();
} // end main

(4)软中断

仅依靠硬件触发中断的场景过于单一,因此 CPU 指令集中设计了int或syscall这类汇编指令,用于由软件主动触发中断,这类中断被称为软中断

例如:你用一个硬的东西打小孩,小孩就会自己躲,但是现在你给小孩说,听到我说躲,你就躲起来,小孩说我知道了;等你说躲时,小孩就躲起来了。硬件中断是 "被动挨打才反应",软中断是"收到指令主动触发",二者本质都是中断机制,只是触发源和时机不同

接收来自外部的中断号,触发中断,软件上也可以;在X86下,int对应中断号-->0x80,是32位下常用;systall对应0x80,是64位下常用

软件中断有什么用?

操作系统内部有很多系统调用,会形成系统调用表,这个表是一个指针数组;在表里对应的下标,叫做对应的系统调用的系统调用号

我们自己写一个方法:call_sys(int number)number是系统调用号,放到中断向量表中系统调用位置,当未来系统调用时,int 0x80就会触发软中断,就会查中断向量表,执行这个方法

而我们自己写的这个方法,在源码中,实际是:

cpp 复制代码
extern int system_call(void);  // 系统调用中断处理程序
_system_call:
    call [_sys_call_table+eax*4]
部分 含义
_sys_call_table 系统调用表 ,是内核中一个全局的函数指针数组,数组的每个元素对应一个系统调用的内核实现函数(如 sys_readsys_writesys_fork)。
eax 32 位 x86 架构下,用户态程序会把系统调用号 存入 eax 寄存器(比如 read 对应调用号 3,write 对应 4)。
*4 因为 32 位系统中,每个函数指针占 4 字节,所以用 eax*4 计算目标函数在数组中的字节偏移量
[_sys_call_table+eax*4] 计算出目标系统调用函数的内存地址,从数组中取出该函数指针。
call 跳转到该地址,执行对应的内核系统调用函数,完成用户请求的操作。

软中断完整执行流程拆分:
1 用户态发起系统调用

Linux 把系统调用封装成 C 库函数(如 fork()、read()、write()),用户无需直接操作汇编:

底层本质:C 库会帮用户执行汇编指令,把系统调用号存入 eax 寄存器,再执行 int 0x80(32 位)/syscall(64 位)软中断指令。
2. 软中断触发,CPU 切换内核态

CPU 收到 0x80 软中断后,保护现场(保存用户态寄存器、程序计数器等上下文),然后查询中断向量表 (IDT)。中断向量表中,0x80 中断号绑定了系统调用的总入口处理函数 system_call()(定义在 kernel/system_call.c 中)。

CPU 跳转到 system_call(),正式进入内核态执行。
3. 内核态查表执行对应系统调用

system_call() 会根据用户传入 eax 寄存器的系统调用号,在 sys_call_table(系统调用表)中索引对应的内核函数。

执行核心逻辑:call *sys_call_table[eax*4],根据调用号跳转到具体的内核函数,完成用户请求的操作(如读文件、创建进程)。
4. 执行完毕,恢复现场,返回用户态

系统调用执行完成后,内核恢复之前保存的用户态上下文,把结果存入对应寄存器,切换回用户态。

用户程序从 int 0x80 指令的下一条继续执行,拿到系统调用的返回结果,完成整个流程。

操作系统提供系统调用:系统调用号+软中断方式

所以我们看到的系统调用,是被C封装过的

4 重谈用户态和内核态

页表在逻辑上存在用户页表和内核页表;用户页表是每个用户都有一份,而内核页表只存在一份

页表在物理上是混在一起的,又标志位表示映射到用户还是内核空间

结论:
核心结论:Linux 中所有函数跳转(包括系统调用的内核态执行),都在当前进程的地址空间内完成。

系统调用本质是「当前进程从用户态切到内核态,在自己的地址空间里执行内核代码」,不是跳转到操作系统的独立地址空间。
1 进程切换的本质:进程切换时,只是切换了进程的地址空间上下文,内核代码(系统调用实现)会被映射到每个进程的地址空间的高地址部分,因此无论切到哪个进程,都能执行同一套内核系统调用

2 关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL 等概念,这块我们不做深究了,现在芯片为了保证兼容性,已经非常复杂了,进而导致 OS 也必须得照顾它的复杂性

CPU中,用CS寄存器中低2位比特位表示当前CPU的执行级别

CS寄存器只用两种:0表示内核态,3表示用户态

用户必须调用int 0x80(只能通过设置eax的方式调用)或systall,才能自动把执行级别由3-->0

细节:把CS寄存器中0,3这样的数字叫做CPL(Current Privilege Level)

如何进行信号操作:(1)signal (2)sigaction

cpp 复制代码
#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函数调用,而是被系统所调用。

pause函数:作用是wait for signal,否则一直阻塞

如果进程收到了大量重复信号?

操作系统处理同一个信号时,是串行处理的--->被递归调用,否则容易栈溢出

当你对应进程正在处理某种信号时,特定的信号自动被屏蔽,意味着收到大量重复循环,操作系统是串行处理的;sigaction比signal多了信号屏蔽的功能


二 可重入函数

上述图片灾难发生过程:

main 正在插 node1:它刚做完第 1 步,node1->next 已经指向 node1 了,正准备执行第 2 步把 head 换成 node1。

此时链表状态:head 指向 node1,node1 指向 node1。

神仙打断(信号触发):这时候信号来了,CPU 暂停了 main,立马跑去执行 sighandler。

sighandler 插 node2:sighandler 调用 insert 插 node2。

它非常顺利,两步都做完了!

此时链表状态:head 指向 node2,node2 指向 node1,node1 指向 node1。

回到 main 继续做:sighandler 执行完返回,CPU 回到 main,继续执行第 2 步 head = p;。

main 把 head 直接改成了 node1。

结局:node2 被覆盖了,彻底找不到了,链表上只有 node1

为什么会这样?

因为 insert 操作了全局变量 head。这个 head 是大家共有的,main 和 sighandler 两个流同时去改它,就会发生 "插队" 事故。这里的insert就是不可重入函数

可重入函数:
一个函数可以被多个执行流同时调用(比如主流程 + 信号处理函数同时调用),不会出现数据混乱、错误、崩溃,这样的函数叫可重入函数

核心特点:
不使用全局变量、静态变量
不调用 malloc /free
不调用标准 I/O 库函数(如 printf、scanf)
只使用自己栈上的局部变量
可以在信号处理函数中安全调用

信号处理函数必须使用可重入函数

可重入和不可重入只是特点不同,没有好坏之分

当一个函数只使用了局部变量,可能是可重入函数


三 valatile 关键字

这个关键字我们之前在c中接触过,是C80/C90的32个关键字的一个

在编译不同的代码时,编译器可能使用不用的编译选项,从而让编译器表现出不同的优化级别

在gcc中,如果我们想使用编译器优化,有相应的优化级别,默认是-O0(没有优化)

如果在编译代码时,采取不同的优化策略和优化级别时,可以带上优化级别

同一份代码,在不同的优化级别下,可能会有不同的表现。例如:运行结果不同

我们下边站在信号的角度重新理解一下啊volatile关键字

cpp 复制代码
#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; 
} 
bash 复制代码
[hb@localhost code_test]$ cat Makefile 
sig:sig.c 
 gcc -o sig sig.c #-O2 
.PHONY:clean 
clean: 
 rm -f sig 
 
[hb@localhost code_test]$ ./sig 
^Cchage flag 0 to 1 
process quit normal

上述第一个代码:

变量是一定在内存中存在的,while循环的本质:是对flag做逻辑判断(逻辑判断的工作是CPU完成的);CPU能做到两种最常见的计算类型:(1)数据计算 (2)逻辑运算(逻辑运算与,或,取反....)

所以flag在while循环中的判断,本质是计算

必须先把flag加载到CPU中,次啊能做判断;判断步骤:(1)载入数据(2)逻辑运算及判断

while对flag没有修改逻辑,只有检查和判断逻辑

当我们开启-O2优化时,GCC 会做一个非常 "聪明" 的优化:把flag直接缓存到 CPU 寄存器里,等价于给flag加了register关键字,后续循环只读取寄存器里的flag,再也不访问内存

这时候,汇编伪代码就变成了这样:

bash 复制代码
start:
    mov  eax, flag    ; 仅在循环开始时,把flag从内存加载到寄存器eax
loop:
    cmp  !eax         ; 每次循环只判断寄存器里的eax,不再读内存
    if(true)
        goto loop
    else
        {}

编译器就会把flag优化成:register int flag = 0;

我们按下Ctrl+C触发SIGINT信号时,内核会调用handler函数,把 内存中的flag 从 0 改成 1。

但此时的问题是:

  • while循环里,CPU 一直读取的是寄存器里缓存的旧值(0),根本不会去内存读取新值
  • 哪怕内存里的flag已经变成 1,循环里的判断永远是!0为真,程序就会永远卡死在循环里,永远无法退出

因为有寄存器的原因,会导致内存变量不可见;但是有了volatile,告诉编译器不要过度优化,不要优化,保持内存变量的可见性

所以volatile修饰的变量不要优化成内存器变量


四 SIGCHLD信号

问题:子进程退出的时候,是安安静静推出的吗?

不是!子进程会给父进程发送SIGSHLD信号(17号信号),该信号的默认处理动作是忽略

父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程。子进程终止时会主动通知父进程,父进程在信号处理函数中调用 wait 清理子进程即可。

会不会有问题?如果存在10个子进程呢?

我们使用的是普通信号,在使用这个信号时,其他的这个信号会被屏蔽

SIGCHLD+while+非阻塞:对任意个数的子进程进行回收

测试代码:

bash 复制代码
#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){//child 
 printf("child : %d\n", getpid()); 
 sleep(3); 
 exit(1); 
 } 
 while(1){ 
 printf("father proc is doing some thing!\n"); 
 sleep(1); 
 } 
 return 0; 
}

父进程必须等待子进程吗?

必须!(大多数情况)

特殊情况:在Linux中,系统允许父进程在一定场景下,可以不用等待子进程退出

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

信号本质是模拟硬件中断

信号编号就相当于中断向量表中的下标

信号和中断是完全不一样的!!!

相关推荐
郝学胜-神的一滴1 小时前
干货版《算法导论》04:渐近复杂度与序列接口实战
java·开发语言·数据结构·c++·python·算法
志栋智能1 小时前
超自动化运维:提升业务连续性的关键引擎
运维·服务器·网络·人工智能·自动化
IT研究所1 小时前
从系统选型到ITR智能服务流落地的关键一步
大数据·运维·服务器·数据库·人工智能·科技·自动化
Dylan的码园1 小时前
2026年免费远程控制软件哪个好?ToDesk向日葵UU远程免费版横评,不限次数不限时长
服务器·开发语言·php
_Rookie._1 小时前
部署python后端,以及Dockerfile 的 RUN CMD ENTRYPOINT字段
开发语言·python
念恒123061 小时前
Docker基础
运维·docker·容器
dog2501 小时前
解析几何的力量(1)
服务器·开发语言·网络·php
AI砖家1 小时前
DeepSeek TUI 保姆级安装配置全指南 -Windows||macOS双平台全覆盖
服务器·前端·人工智能·windows·macos·ai编程·策略模式
红茶要加冰1 小时前
四、流程控制之条件判断
linux·运维·服务器