一、如何理解内核态和用户态
1.1 为什么总能找到操作系统?

无论进程如何调度,操作系统始终"在场"


内核页表只有一份,所有进程共享;用户页表每个进程各有一份。因此无论CPU切换到哪个进程,通过页表映射总能找到同一个内核。
1.2 用户态 vs 内核态:权限隔离机制
问题:用户和内核在同一个4GB地址空间,用户能直接访问内核吗?
答案:不能!OS通过特权级别保护自己
| 状态 | 身份标识 | 可访问范围 | 如何进入 |
|---|---|---|---|
| 用户态 | CPL=3 | 只能访问[0,3GB] | 默认执行状态 |
| 内核态 | CPL=0 | 可访问[0,4GB]全部 | 系统调用/中断/异常 |
CPL(Current Privilege Level):CPU当前特权级,0表示内核,3表示用户
CPU通过状态标志位(CPL)管控运行权限,该标志位存储在代码段寄存器(
CS)的低两位,0代表内核态,3代表用户态。用户态程序尝试访问内核区时,CPU会直接拦截并触发权限错误,保障内核安全。
2-2-1 如何切换状态?

用户态程序需通过预定义的陷入指令切换至内核态,例如
int 0x80或syscall,执行指令后CPU会自动切换CS寄存器至内核代码段,将CPL降为0,完成从用户态到内核态的切换。
1.3 中断与系统调用的执行流程


1.4 系统调用表(sys_call_table)

这是一个函数指针数组
// 系统调用号对应具体的内核函数
fn_ptr sys_call_table[] = {
sys_setup, // 0
sys_exit, // 1
sys_fork, // 2
sys_read, // 3
sys_write, // 4
sys_open, // 5
sys_close, // 6
...
sys_pause, // 暂停进程
sys_signal, // 信号处理相关
sys_sigaction,
...
};
用户程序调用 syscall 时,通过系统调用号索引到这个表,找到对应的内核函数执行。

二、可重入函数
2.1 什么是重入?
在单进程环境中,函数通常被一个控制流(主程序)调用。但是当信号发生时,进程可能正在执行某个函数,信号处理函数中也可能调用同一个函数。这就导致该函数在第一次调用尚未返回时,又被第二次调用。这种现象称为重入。
2.2 实例

// 全局链表头指针
struct node *head = NULL;
void insert(struct node *new_node) {
// 第一步:让新节点指向当前头
new_node->next = head;
// 假设在这里被信号中断
// 第二步:更新头指针指向新节点
head = new_node;
}
如果在第一步和第二步之间发生信号,信号处理函数中也调用了insert,则可能导致head的更新错乱,最终链表结构被破坏。
2.3 什么函数是不可重入的?
如果一个函数满足以下任一条件,通常就是不可重入的:
使用了静态或全局变量(如上面的
head)。调用了
malloc或free—— 因为堆管理使用全局链表。调用了标准I/O库函数(如
printf、scanf)—— 内部有缓冲区,也是全局数据结构。调用了其他不可重入函数。
2.4 可重入函数的特点
-
只访问自己的局部变量或参数。
-
不修改全局数据(或者通过锁保护,但信号处理中加锁容易死锁,不推荐)。
-
不调用不可重入的系统调用(但像
read、write等直接操作文件描述符且无缓冲区的系统调用通常是可重入的)。
📌 经验法则:在信号处理函数中,尽量只做最简单的操作(如设置一个全局标志)。如果必须做复杂处理,可以考虑在主循环中检查标志再执行。

三、volatile
volatile 是 C 语言中的一个类型修饰符,它告诉编译器:这个变量的值可能会在程序的控制流之外被意外改变。因此,编译器不能对它做任何优化(比如缓存到寄存器、省略看似多余的读写),每次访问该变量时都必须从它的原始内存地址重新读取。
【简而言之,不做优化~】
man gcc ,查看优化级别 :

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
int flag = 0;
void handler(int signu)
{
std::cout << "更改全局变量," << flag << "-> 1" << std::endl;
flag = 1;
}
int main()
{
signal(2,handler);
while(!flag);
std::cout << "process quit normal!" << std::endl;
return 0;
}

按照代码的逻辑 , 按了Ctrl + c , handler执行 , flag = 1 , 主循环退出 ; 但是 !!!我们开启优化后 。 主循环永远卡死!!!


为什么while 循环没有退出 ?
3.1 根本原因:编译器优化导致的"内存不可见"

// 源代码
while(!flag); // main函数内flag不会被修改(编译器的判断)
// 编译器优化后(等价于)
register int tmp = flag; // 将flag缓存到寄存器
while(!tmp); // 永远读取寄存器,不再访问内存!
// 即使handler把内存中的flag改为1,
// 主循环也"看不见"
编译器认为main函数内部没有对flag的修改操作,因此"安全地"将其优化到寄存器中,导致内存写入对主循环不可见。

3.2 volatile 的适用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 硬件寄存器映射 | 外设寄存器值由硬件改变 | 嵌入式开发中的GPIO状态寄存器 |
| 多线程共享变量(简单场景) | 一个线程写,另一个线程读 | 标志位、状态通知 |
| 信号处理函数 | 异步修改全局变量 | 本例中的flag标志 |
| 中断服务程序 | 中断中修改,主循环读取 | 嵌入式中断标志 |
3.3 volatile 的局限性(重要!)

四、SIGCHLD信号
4.1 SIGCHLD信号的定义与作用
信号编号:17号信号(SIGCHLD)。
触发场景:当子进程终止或暂停时,操作系统自动向父进程发送此信号。
关联知识:与进程管理中的“等待与回收”机制强相关。



可是我没看到啊!!!接下来,写一个代码,证明一下,子进程在退出的时候的的确确发送了信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
void say(int num)
{
std::cout << "father get a signal: " << num << std::endl;
}
int main()
{
// 注册SIGCHLD信号处理函数
signal(SIGCHLD, say);
pid_t id = fork();
if (id == 0)
{
// 子进程
std::cout << "I am child, exit" << std::endl;
sleep(3);
exit(3);
}
// 父进程等待子进程结束
waitpid(id, nullptr, 0);
std::cout << "I am father, exit" << std::endl;
return 0;
}

4.2 SIGCHLD 的默认处理动作

1. 用wait和waitpid函数清理僵尸进程 , 父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
2. 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

缺点就是 : 拿不到退出码 !!
所以 , 如果我们想要拿到退出码 , 可以使用 waitpid , 但是waitpid 默认是阻塞式的等待 , 所以我们要使用WNOHANG 这个参数
为什么要学习这么多的回收策略 ?
【根据需求进行选择】
- 无状态跟踪 : SIG_ING
- 需监控子进程状态 : 信号驱动 + WNOHANG
默认动作为IGN , 为什么手动还需要用IGN ?
- SIGCHLD 默认行为为SIG_DEL(忽略) , 但实际表现为父进程需要显式处理
- 本质区别
- 系统默认忽略 : 子进程保留僵尸状态
- 用户显示设置为 SIG_IGN:内核主动回收子进程资源
4.3 SIGCHLD 的常见处理策略
策略1:显式忽略(自动回收子进程)

int main() {
signal(SIGCHLD, SIG_IGN); // 在fork前设置!
pid_t id = fork();
if (id == 0) {
exit(0); // 子进程直接退出
}
// 父进程无需wait,子进程不会变僵尸
sleep(100);
}
策略2:自定义信号处理函数(异步回收)
void handler(int sig) {
// 在信号处理函数中调用waitpid
// 注意:需要使用非阻塞模式或循环等待
while (waitpid(-1, nullptr, WNOHANG) > 0);
}
int main() {
signal(SIGCHLD, handler);
// ... fork子进程 ...
}
注意:SIGCHLD是不可靠信号,多个子进程同时终止可能只触发一次信号,因此handler中要用while循环尽可能多地回收。


被折叠的 条评论
为什么被折叠?



