app网站推广平台,南头网站建设,长春关键词优化,凯里市住房和城乡建设局网站目录 一、信号处理概述#xff1a;为什么需要“信号”#xff1f;
二、用户空间与内核空间#xff1a;进程的“双重人格”
三、内核态与用户态#xff1a;权限的“安全锁”
四、信号捕捉的内核级实现#xff1a;层层“安检”
五、sigaction函数#xff1a;精细控制信…目录 一、信号处理概述为什么需要“信号”
二、用户空间与内核空间进程的“双重人格”
三、内核态与用户态权限的“安全锁”
四、信号捕捉的内核级实现层层“安检”
五、sigaction函数精细控制信号行为
1. 函数原型
2. 关键结构体
3. 示例代码动态修改信号处理
六、可重入函数
1. 问题场景
2. 问题分析
3. 原因解释
4. 不可重入函数与可重入函数
5. 如何避免重入问题
七、volatile
1. 问题引入
2. 未使用volatile的情况
3. 使用volatile解决问题
4. volatile的作用总结 一、信号处理概述为什么需要“信号” 想象你在办公室工作时突然有人敲门提醒你快递到了。这里的“敲门”就像操作系统发给进程的信号。信号是操作系统通知进程某个事件发生的机制例如 CtrlC 发送 SIGINT 信号终止进程 程序崩溃时内核发送 SIGSEGV 信号 用户自定义信号处理逻辑如保存日志 但进程不会立即处理信号而是在“合适的时候”——比如从内核态切换回用户态时。这背后隐藏着操作系统的核心设计逻辑。 二、用户空间与内核空间进程的“双重人格”
每个进程的地址空间分为两部分
用户空间内核空间存储进程私有代码和数据存储操作系统全局代码和数据通过用户级页表映射物理内存通过内核级页表映射物理内存每个进程看到的内容不同所有进程看到的内容相同 // 示例用户空间的变量
int user_data 100; // 内核空间的代码进程无权直接访问
void kernel_code()
{// 管理硬件资源
}
关键点 用户态代码无法直接访问内核空间权限不足 执行系统调用如printf时进程会陷入内核切换到内核态 三、内核态与用户态权限的“安全锁”
用户态内核态权限等级低普通用户代码高操作系统代码操作限制无法直接访问硬件可执行任何指令触发场景执行普通代码系统调用、中断、异常
状态切换示例
printf(Hello); // 用户态 - 内核态执行write系统调用 - 用户态
具体步骤
1、用户态调用printf
printf是C标准库函数负责格式化字符串如将Hello转换为字符流。若输出到终端如屏幕最终会调用**系统调用write**将数据写入文件描述符如标准输出stdout。
2、触发系统调用write
系统调用是用户程序请求操作系统服务的唯一入口。
write的函数签名
ssize_t write(int fd, const void *buf, size_t count);
其中fd1表示标准输出buf指向数据缓冲区count为数据长度。
3、从用户态陷入内核态
CPU执行特殊的陷入指令如syscall或int 0x80触发软中断。硬件自动切换特权级用户态ring 3→ 内核态ring 0。跳转到内核中预定义的系统调用处理函数如sys_write。
4、内核态执行sys_write
操作系统验证参数合法性如fd是否有效。将用户空间的数据Hello从缓冲区复制到内核空间防止用户篡改。调用设备驱动将数据发送到终端如控制台、SSH会话。记录返回结果成功写入的字节数或错误码。
5、返回用户态
内核恢复用户程序的寄存器状态和堆栈。CPU特权级切换回用户态ring 3。用户程序继续执行printf之后的代码。 为什么需要切换特权态 用户态的限制 用户程序无法直接访问硬件如磁盘、网卡或修改关键数据结构如进程表。 例若允许用户程序直接写磁盘恶意程序可能覆盖系统文件。 内核态的权限 操作系统代码拥有最高权限可安全管理硬件和资源。 通过系统调用“代理”用户程序的请求确保所有操作受控。 四、信号捕捉的内核级实现层层“安检” 在计算机系统里程序运行时可能会遇到一些特殊情况比如用户按下某些按键或者系统出现了问题这时候就需要程序能够及时做出反应。这种反应机制在Linux系统中是通过“信号”来实现的。信号就像是一个信使负责把发生的事件告诉程序。 现在假设一个程序正在运行它的主函数main函数就好比一个人正在按照计划做一件大事。突然某个特定的事件发生了比如用户按下了一个特殊的按键组合这会触发SIGQUIT信号。这时候系统会暂时中断这个人的工作切换到一个专门处理这种情况的模式也就是“内核态”由操作系统来处理这个事件。 操作系统在处理完这个事件后准备回到原来的程序继续工作之前会检查有没有需要特别处理的信号。如果发现有SIGQUIT信号而且这个程序之前已经告诉过操作系统当这个信号出现时要按照它自己定义的方式来处理也就是注册了一个信号处理函数sighandler那么操作系统就会安排一个特殊的操作。 这个操作就是不是直接回到原来的主函数继续做之前的事情而是先去执行那个专门定义的处理函数sighandler。这就好比在你做一件大事的时候突然有紧急情况需要你先去处理一下处理完了再回来继续做原来的事。 需要注意的是这个处理函数sighandler和原来的主函数main函数是两个完全独立的任务它们就像两条平行的路没有直接的调用关系。sighandler有自己的工作空间不同的堆栈空间来完成它的任务。 当处理函数sighandler完成自己的任务后它会触发一个特殊的指令sigreturn系统调用再次回到操作系统那里。操作系统会检查是否还有其他的紧急情况需要处理。如果没有就会回到原来的主函数恢复之前的状态继续完成未做完的事情。 当进程从内核态返回用户态时会检查未决信号集pending 检查信号状态 若信号未被阻塞block且处理动作为默认或忽略 → 立即处理如终止进程并清除pending标志 若处理动作为自定义 → 先返回用户态执行处理函数再通过sigreturn回到内核 执行自定义处理函数的关键步骤 内核不信任用户代码必须返回用户态执行处理函数 处理函数与主流程独立不同堆栈无调用关系 五、sigaction函数精细控制信号行为
1. 函数原型
#include signal.h
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact); 参数说明 signo 指定信号的编号如SIGINT。 act 新的处理动作 oldact 保存旧的处理动作 2. 关键结构体
结构体 sigaction 的定义如下
struct sigaction
{void (*sa_handler)(int); // 信号处理函数sigset_t sa_mask; // 额外屏蔽的信号int sa_flags; // 控制选项通常设为0// 其他字段如sa_sigaction暂不讨论
};
3. 示例代码动态修改信号处理
#include stdio.h
#include string.h
#include unistd.h
#include signal.hstruct sigaction act, oact;// 自定义信号处理函数
void handler(int signo)
{printf(捕获信号: %d\n, signo);// 恢复为默认处理方式仅第一次捕获时自定义sigaction(SIGINT, oact, NULL);
}int main()
{memset(act, 0, sizeof(act));memset(oact, 0, sizeof(oact));act.sa_handler handler; // 设置自定义处理函数act.sa_flags 0; // 无特殊标志sigemptyset(act.sa_mask); // 不额外屏蔽其他信号// 注册SIGINT信号CtrlC触发sigaction(SIGINT, act, oact);while (1){printf(程序运行中...\n);sleep(1);}return 0;
}运行效果 第一次按下CtrlC → 打印“捕获信号: 2” 再次按下CtrlC → 进程终止已恢复默认行为 六、可重入函数 1. 问题场景 假设我们有一个简单的链表结构定义了两个节点 node1 和 node2以及一个头指针 head。在 main 函数中我们调用 insert 函数将 node1 插入到链表中。insert 函数的实现分为两步首先将新节点的 next 指针指向当前头节点然后更新头指针为新节点。
node_t node1, node2, *head;void insert(node_t *p) {p-next head; // 第一步将新节点的 next 指针指向当前头节点head p; // 第二步更新头指针为新节点
}int main() {// ... 其他代码 ...insert(node1); // 在 main 函数中插入 node1// ... 其他代码 ...
} 在插入 node1 的过程中假设刚执行完第一步p-next head此时发生了硬件中断导致进程切换到内核态。在内核态处理完中断后检查到有信号待处理于是切换到信号处理函数 sighandler。sighandler 同样调用 insert 函数试图将 node2 插入到同一个链表中。
void sighandler(int signo) {// ... 其他代码 ...insert(node2); // 在信号处理函数中插入 node2// ... 其他代码 ...
} 当 sighandler 完成插入 node2 的操作并返回内核态后再次回到用户态继续执行 main 函数中被中断的 insert 函数的第二步head p。
2. 问题分析 理想情况下我们希望 main 函数和 sighandler 分别将 node1 和 node2 插入到链表中最终链表包含两个节点。然而实际情况却并非如此。 main 函数插入 node1 的第一步 将 node1 的 next 指针指向当前头节点初始时 head 为 NULL此时 node1-next NULL。 中断发生切换到内核态 main 函数的 insert 操作被中断此时 head 还未更新为 node1。 sighandler 插入 node2 在信号处理函数中执行 insert(node2)。此时 head 仍为 NULL所以 node2-next NULL然后 head 被更新为 node2。 返回 main 函数继续执行 执行 insert 函数的第二步将 head 更新为 node1。 最终链表的头指针 head 指向 node1而 node1 的 next 指针为 NULL。node2 被插入后又被覆盖实际上没有真正加入链表。
3. 原因解释 这个问题的根源在于 insert 函数被不同的控制流程main 函数和 sighandler调用且在第一次调用还未完成时就再次进入该函数。这种现象称为“重入”Reentrant。insert 函数访问了一个全局链表 head由于全局变量在多个控制流程之间共享导致数据不一致。
4. 不可重入函数与可重入函数 不可重入函数 如果一个函数在被调用过程中其内部操作依赖于全局变量或共享资源并且在函数执行过程中这些资源可能被其他调用者修改那么这个函数就是不可重入的。像上面的 insert 函数因为它操作了全局链表 head所以在重入情况下容易出错。 可重入函数 如果一个函数只访问自己的局部变量或参数不依赖于全局变量或共享资源那么它就是可重入的。可重入函数在不同控制流程中被调用时不会相互干扰。
5. 如何避免重入问题 避免使用全局变量 尽量使用局部变量或者通过参数传递必要的数据。 使用互斥机制 在多线程或信号处理场景中使用互斥锁如 mutex来保护共享资源的访问。 设计可重入函数 确保函数只依赖于参数和局部变量不依赖于外部环境。 七、volatile 在C语言中volatile 是一个经常被提及但又容易被误解的关键字。今天我们通过一个具体的信号处理例子来深入理解 volatile 的作用。
1. 问题引入
考虑以下代码
#include stdio.h
#include signal.hint flag 0;void handler(int sig) {printf(change flag 0 to 1\n);flag 1;
}int main() {signal(2, handler);while (!flag);printf(process quit normal\n);return 0;
} 该程序的功能是在接收到 SIGINT 信号如用户按下 CtrlC时执行自定义信号处理函数 handler将全局变量 flag 设置为 1从而退出 while 循环程序正常结束。 2. 未使用volatile的情况 在未使用 volatile 修饰 flag 的情况下编译器可能会对代码进行优化。例如当使用 -O2大写字母O 优化选项编译时编译器可能会认为 flag 的值在 while 循环中不会被改变因为从代码的静态分析来看没有明显的修改操作于是将 flag 的值缓存到 CPU 寄存器中而不是每次都从内存中读取。 这就会导致一个问题当信号处理函数 handler 修改了 flag 的值时while 循环中的条件判断仍然使用寄存器中的旧值无法及时检测到 flag 的变化程序无法正常退出。这种现象被称为“数据不一致性”或“内存可见性”问题。 3. 使用volatile解决问题
为了解决上述问题我们需要使用 volatile 关键字修饰 flag 变量
#include stdio.h
#include signal.hvolatile int flag 0;void handler(int sig) {printf(change flag 0 to 1\n);flag 1;
}int main() {signal(2, handler);while (!flag);printf(process quit normal\n);return 0;
} volatile 告诉编译器该变量的值可能会被程序之外的其他因素如信号处理函数、硬件中断等改变因此编译器在优化时不会假设该变量的值不变。每次访问 volatile 修饰的变量时编译器都会生成代码从内存中重新读取该变量的值而不是使用寄存器中的缓存值。 这样在信号处理函数修改了 flag 的值后while 循环中的条件判断能够及时检测到变化程序可以正常退出。 4. volatile的作用总结
volatile 的主要作用是保持内存的可见性确保程序能够正确地读取和写入变量的最新值。在以下场景中使用 volatile 是必要的 信号处理 当变量可能被信号处理函数修改时需要使用 volatile 修饰以确保主程序能够及时检测到变量的变化。 多线程编程 在多线程环境中当变量可能被其他线程修改时volatile 可以防止编译器优化导致的内存可见性问题。不过需要注意的是volatile 并不能完全替代互斥锁等同步机制因为它不能保证操作的原子性。 硬件寄存器访问 当程序需要直接访问硬件寄存器时这些寄存器的值可能会被硬件异步修改因此需要使用 volatile 修饰相关的指针或变量。