信号快速认识从生活角度理解信号想象你在网上购物等待快递送达的场景识别信号你知道快递到来时该如何处理这是内置的能力处理时机快递到了你在打游戏可以5分钟后去取——不需要立即处理记忆机制从收到通知到拿到快递期间你记住了有一个快递要去取处理方式默认动作开心拆快递使用商品自定义动作把零食送给女朋友忽略动作扔在床头继续打游戏核心结论进程识别信号是内核程序员写好的内置特性信号的处理方法在信号产生之前就已经准备好了信号产生后不会立即处理而是在合适的时候技术角度一个简单示例#include iostream #include unistd.h #include signal.h void handler(int signumber) { std::cout 我是 getpid() 我获得了一个信号 signumber std::endl; } int main() { std::cout 我是进程 getpid() std::endl; // 对2号信号(SIGINT)设置自定义捕捉函数 signal(SIGINT, handler); while(true) { std::cout I am a process, I am waiting signal! std::endl; sleep(1); } }按CtrlC时进程不再退出而是打印信号信息——这就是信号捕捉。signal函数仅仅是设置了处理方式并没有调用handler。如果信号没有产生handler永远不会被调用信号的基本概念什么是信号信号是进程之间事件异步通知的一种方式属于软中断。类比理解硬件中断由硬件设备触发发给CPU信号由软件模拟发给进程查看系统支持的信号kill -l常用信号速查表信号编号默认动作产生条件SIGHUP1终止终端挂起或控制进程终止SIGINT2终止CtrlCSIGQUIT3终止CoreCtrl\SIGILL4终止Core非法指令SIGABRT6终止Coreabort()函数SIGFPE8终止Core浮点异常(除零)SIGKILL9终止无法捕获/阻塞SIGSEGV11终止Core无效内存访问SIGPIPE13终止向无读端的管道写SIGALRM14终止alarm()超时SIGTERM15终止终止请求SIGCHLD17忽略子进程状态改变SIGSTOP19停止无法捕获/阻塞SIGTSTP20停止CtrlZ注意SIGKILL(9)和SIGSTOP(19)无法被捕获、阻塞或忽略这是内核保留的权力。信号处理的三种方式// 1. 默认处理 signal(SIGINT, SIG_DFL); // CtrlC终止进程 // 2. 忽略信号 signal(SIGINT, SIG_IGN); // CtrlC无反应 // 3. 自定义捕捉 signal(SIGINT, handler); // 执行用户定义函数信号的产生方式通过终端按键产生按键信号效果CtrlCSIGINT(2)中断前台进程Ctrl\SIGQUIT(3)退出并产生Core DumpCtrlZSIGTSTP(20)挂起前台进程到后台调用系统命令# 向进程发送信号 kill -SIGSEGV 12345 kill -11 12345 # 使用编号使用函数产生信号kill() - 向任意进程发信号#include sys/types.h #include signal.h int kill(pid_t pid, int sig);raise() - 给自己发信号#include signal.h int raise(int sig); // 成功返回0 // 每隔1秒给自己发SIGINT signal(SIGINT, handler); while(true) { sleep(1); raise(SIGINT); }abort() - 异常终止#include stdlib.h void abort(void); // 总是成功不返回 // 发送的是SIGABRT(6号信号) // 即使捕捉了abort()仍会终止进程 signal(SIGABRT, handler); abort(); // 输出获取了一个信号6然后Aborted软件条件产生alarm() - 闹钟函数#include unistd.h unsigned int alarm(unsigned int seconds);基本用法#include iostream #include unistd.h #include signal.h int count 0; void handler(int signo) { std::cout count: count std::endl; exit(0); } int main() { signal(SIGALRM, handler); alarm(1); // 1秒后发送SIGALRM while(true) { count; } }运行结果可能显示计数达到4.9亿次有IO时可能只有10万体现出纯CPU运算非常快IO操作极其耗时重复闹钟void handler(int signo) { // 处理业务... alarm(1); // 重设闹钟实现周期性 }硬件异常产生信号除零错误int a 10; a / 0; // 产生SIGFPE(8号信号)野指针访问int* p NULL; *p 100; // 产生SIGSEGV(11号信号)为什么会持续收到信号CPU中有状态寄存器记录异常状态除零后没有清理异常标志位所以异常会一直存在OS持续发送信号。Core Dump详解Core Dump是进程异常终止时将用户空间内存数据保存到磁盘core文件的机制。# 允许生成core文件默认大小为0不允许生成 ulimit -c 1024 # 编译时加-g保留调试信息 gcc -g test.c -o test # 运行程序生成core ./test Segmentation fault (core dumped) # 使用gdb调试 gdb test coreCore Dump的作用事后调试定位程序崩溃原因。// 查看子进程退出状态 int status; waitpid(-1, status, 0); int exit_signal status 0x7F; // 终止信号 int core_dump (status 7) 1; // 是否生成core信号的保存三个关键术语递达(Delivery)实际执行信号处理动作未决(Pending)信号从产生到递达之间的状态阻塞(Block)进程选择阻塞某个信号被阻塞的信号产生后将保持在未决状态阻塞 ≠ 忽略阻塞是不让信号递达忽略是递达后什么都不做。内核中的表示struct task_struct { // ... struct sighand_struct *sighand; // 信号处理函数 sigset_t blocked; // 阻塞信号集 struct sigpending pending; // 未决信号集 // ... };每个信号在内核中有三个属性block位是否阻塞该信号pending位该信号是否处于未决状态handler指针处理动作默认/忽略/自定义示意图 信号: SIGHUP SIGINT SIGQUIT block: 0 1 0 pending: 0 1 0 handler: SIG_DFL SIG_IGN user_funcsigset_t信号集操作#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);sigprocmask - 修改信号屏蔽字int sigprocmask(int how, const sigset_t *set, sigset_t *oset);how参数含义公式SIG_BLOCK添加阻塞mask mask | setSIG_UNBLOCK解除阻塞mask mask ~setSIG_SETMASK直接设置mask set信号的捕捉什么是信号捕捉信号捕捉Signal Catch进程注册一个自定义函数当特定信号递达时内核转而执行这个用户空间的函数而不是执行信号的默认动作。// 信号捕捉的三要素 void my_handler(int signo) { // 1. 自定义函数 printf(捕获信号: %d\n, signo); } int main() { signal(SIGINT, my_handler); // 2. 注册告诉内核收到SIGINT时调用my_handler // 3. 等待信号产生... }信号捕捉与普通函数调用的区别// 普通函数调用明确的调用关系 void func_a() { func_b(); // 调用者明确知道要调用func_b } // 信号捕捉异步回调 int main() { signal(SIGINT, handler); // 注册告诉内核在合适的时候调用handler while(1); // 内核会在中断返回时检查并调用handler }特性普通函数调用信号捕捉调用时机开发者明确知道异步的由内核决定调用者用户代码内核堆栈使用调用者堆栈使用用户态堆栈独立返回方式直接返回通过sigreturn系统调用信号捕捉流程详解当信号处理函数是用户自定义函数时处理过程非常特殊用户态执行main → 中断/异常 → 内核态处理 ↓ 检查到有信号递达 ↓ 返回用户态执行sighandler不是恢复main ↓ sighandler返回 → 执行sigreturn → 内核态 ↓ 恢复main的上下文继续执行关键点sighandler和main使用不同的堆栈空间它们之间不存在调用关系是两个独立的控制流程整个过程涉及4次用户态/内核态切换宏观流程四次用户态/内核态切换微观流程七步走第1步用户程序执行到某个点int main() { // 1. 用户态执行main函数 int x 0; while(1) { x; // 执行到某个指令时... // 可能被中断打断 } }第2步发生中断进入内核态// 可能触发的原因 // 1. 硬件中断时钟中断、键盘中断 // 2. 系统调用用户主动int 0x80/syscall // 3. 异常缺页异常、除零异常 // 4. 信号产生其他进程调用kill()CPU自动完成保存当前上下文寄存器、栈指针等切换到内核栈特权级从ring3提升到ring0跳转到中断向量表对应的处理函数第3步内核处理中断检查信号// 内核中断处理伪代码 void do_IRQ(int irq) { // 处理硬件中断 handle_irq(irq); // 检查当前进程是否有待处理信号 if (current-pending.signals) { // 有未决信号 do_signal(); } } void do_signal() { sigset_t pending current-pending ~current-blocked; // 找到第一个未被阻塞的未决信号 int signo find_first_signal(pending); // 获取信号处理动作 struct k_sigaction *ka current-sighand-action[signo]; if (ka-sa_handler SIG_DFL) { // 默认处理 do_default_action(signo); } else if (ka-sa_handler SIG_IGN) { // 忽略清除pending位 clear_pending(signo); } else { // 用户自定义处理 → 信号捕捉 handle_signal(signo, ka); } }第4步准备执行用户态handler这是最关键的步骤内核需要保存main函数的上下文以便恢复构造handler的栈帧修改返回地址不返回main而是执行handler// 内核信号处理伪代码 void handle_signal(int signo, struct k_sigaction *ka) { struct pt_regs *regs current-thread.regs; // 用户态寄存器 unsigned long *stack (unsigned long *)regs-esp; // 1. 保存main函数的返回地址 unsigned long old_eip regs-eip; // 2. 在用户栈上压入返回地址sigreturn // 这样handler返回时会执行sigreturn系统调用 *(--stack) (unsigned long)stub_sigreturn; // 3. 设置新的执行地址为handler regs-eip (unsigned long)ka-sa_handler; // 4. 参数传递通过寄存器传递信号编号 regs-eax signo; // 对应handler的第一个参数 // 5. 保存原上下文到用户栈用于sigreturn恢复 // 实际实现中会构造一个sigcontext结构 // 6. 修改栈指针 regs-esp (unsigned long)stack; }第5步返回用户态执行handlerCPU自动完成从内核栈切换回用户栈特权级从ring0降回ring3开始执行handler函数void handler(int signo) { // signo来自EAX寄存器 // 用户空间代码 printf(捕获信号: %d\n, signo); // 执行完后返回 }重要handler执行时使用用户态堆栈与main函数使用不同的栈帧。第6步handler返回 → 执行sigreturn当handler执行完return语句时// 编译器生成的代码 handler: push ebp mov ebp, esp // ... 处理逻辑 mov esp, ebp pop ebp ret // 返回到栈顶保存的地址stub_sigreturn// sigreturn系统调用的简化实现 asmlinkage void sys_sigreturn(void) { struct pt_regs *regs current-thread.regs; // 从用户栈恢复之前保存的上下文 struct sigcontext *sc (struct sigcontext *)regs-esp; // 恢复所有寄存器 regs-eax sc-eax; regs-ebx sc-ebx; // ... 恢复所有通用寄存器 regs-eip sc-eip; // 恢复到main函数 regs-esp sc-esp; // 恢复到main的栈 // 恢复信号屏蔽字 current-blocked sc-oldmask; // 清除信号已递达 clear_pending(signo); }第7步恢复main函数执行sigreturn返回 → 内核态再次切换到用户态 → 继续执行main函数main函数从被中断的地方继续执行完全不知道刚才发生了信号处理sigaction - 更强大的信号设置#include signal.h int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);struct sigaction { void (*sa_handler)(int); // 信号处理函数 sigset_t sa_mask; // 处理期间额外屏蔽的信号 int sa_flags; // 选项通常设为0 void (*sa_sigaction)(int, siginfo_t*, void*); // 实时信号 };自动屏蔽特性当某个信号的处理函数被调用时内核自动将该信号加入屏蔽字处理完再恢复。这保证了在处理某个信号时如果这种信号再次产生会被阻塞到当前处理结束。操作系统运行原理信号捕捉不仅仅是注册一个函数等内核调用这么简单。它涉及硬件中断、CPU特权级切换、内核调度、用户态/内核态转换等一系列操作系统核心机制。操作系统的本质躺在中断上的代码操作系统是什么很多人以为操作系统是一个主动运行的程序但实际上操作系统本质上是躺在中断处理例程上的代码块它自己不主动做任何事情而是被各种中断事件唤醒来执行相应操作。// Linux 0.11 内核的 main.c void main(void) { // ... 初始化各种硬件、数据结构 ... // 最后进入死循环 for (;;) pause(); // 让出CPU等待中断 }操作系统启动后会进入一个无限循环等待各种事件的发生。这些事件包括硬件中断键盘、鼠标、网卡、磁盘等时钟中断定时触发软件中断/异常系统调用、缺页、除零等中断向量表操作系统的大门// 中断向量表就是操作系统所有入口函数的地址表 // 启动时加载到内存固定位置 void trap_init(void) { // 硬件中断 set_trap_gate(0x20, timer_interrupt); // 时钟中断 set_trap_gate(0x21, keyboard_interrupt); // 键盘中断 set_trap_gate(0x2E, harddisk_interrupt); // 硬盘中断 // 异常 set_trap_gate(0, divide_error); // 除零异常 set_trap_gate(14, page_fault); // 缺页异常 // 系统调用 set_system_gate(0x80, system_call); // int 0x80 }中断驱动模型------------------ | 用户程序执行 | ------------------ | | 发生了什么事件 v ------------------ | CPU保存现场 | ← 自动完成 | 查中断向量表 | ------------------ | v ------------------ | 执行中断处理例程 | ← 操作系统代码 | 处理具体事件 | ------------------ | v ------------------ | 恢复现场 | ← 自动完成 | 返回用户程序 | ------------------硬件中断操作系统的心跳硬件中断的本质硬件中断是外部设备告诉CPU我有事情需要处理的机制。外部设备键盘、鼠标、网卡、磁盘、时钟... | | 发送电信号到中断控制器8259A/PIC v 中断控制器 | | 向CPU发送中断请求IRQ v CPU | | 保存当前执行状态 | 查中断向量表 | 跳转到中断处理函数 v 操作系统中断处理Linux中断向量分配中断向量号x86 0-31: CPU异常除零、缺页、非法指令等 32-47: 硬件中断IRQ0-IRQ15 32 (0x20): 时钟中断PIT 33 (0x21): 键盘中断 46 (0x2E): 硬盘中断 ... 0x80: 系统调用int 0x80键盘中断完整流程// 1. 用户按下键盘按键 // 2. 键盘控制器发送扫描码到中断控制器 // 3. 中断控制器通知CPUIRQ1向量33 // 4. CPU保存当前进程上下文 // 5. CPU跳转到键盘中断处理函数 void keyboard_interrupt(void) { unsigned char scancode inb(0x60); // 读取扫描码 // 将扫描码转换为字符 char key scancode_to_char(scancode); // 如果是CtrlC需要给前台进程发送SIGINT if(key c ctrl_pressed) { // 获取前台进程的PID pid_t fg_pid get_foreground_pid(); // 发送信号 send_signal(fg_pid, SIGINT); } // 通知中断控制器处理完成 outb(0x20, 0x20); }系统调用用户主动进入内核系统调用的本质系统调用是用户程序主动请求内核服务的唯一方式。它本质上是一个软件中断。// 用户程序调用open函数 int fd open(/tmp/test.txt, O_RDWR); // 经过glibc封装后变成 asm volatile ( mov $2, %%eax\n // 系统调用号__NR_open 2 mov %1, %%ebx\n // 参数1文件名 mov %2, %%ecx\n // 参数2标志 int $0x80\n // 触发软中断 : a (result) : r (filename), r (flags) : memory );系统调用表// Linux 0.11 系统调用表函数指针数组 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_waitpid, // 7 sys_create, // 8 sys_link, // 9 sys_unlink, // 10 sys_execve, // 11 sys_chdir, // 12 sys_time, // 13 sys_mknod, // 14 sys_chmod, // 15 // ... 共72个 };系统调用完整流程用户态 内核态 | | | 调用库函数 | | 设置参数到寄存器 | | 触发 int $0x80 | |------------------------------| CPU保存上下文 | | 切换到内核栈 | | 查系统调用表 | | 执行 sys_open | | ↓ | | 打开文件 | | 分配文件描述符 | | 返回结果 |------------------------------| 恢复上下文 | 获取返回值 | 回到用户态 | 继续执行 |时钟中断进程调度的驱动力时钟中断的设置// Linux初始化时设置时钟中断 void sched_init(void) { // 设置时钟中断处理函数 set_intr_gate(0x20, timer_interrupt); // 允许时钟中断IRQ0 outb_p(inb_p(0x21) ~0x01, 0x21); } // 时钟中断处理函数 void timer_interrupt(void) { // 1. 更新系统时间 jiffies; // 2. 检查时间片是否用完 if (--current-counter 0) return; // 时间片还有剩余 // 3. 时间片用完进行调度 do_timer(1); } // 调度入口 void do_timer(long cpl) { // ... 更新各种统计信息 ... // 调用调度器 schedule(); } // 调度器 void schedule(void) { // 1. 在就绪队列中找到优先级最高的进程 int next find_best_task(); // 2. 切换到该进程 if (next ! current) { switch_to(next); } }时间片轮转时间轴每10ms一个时钟中断 -------------------------------- | P1 | P1 | P1 | P2 | P2 | P2 | P3 | P1 | -------------------------------- 10ms 20ms 30ms 40ms 50ms 60ms 70ms 80ms 每个进程的时间片30ms P1: 0-30ms, 70-80ms P2: 30-60ms P3: 60-70ms时钟中断与信号的关系// 时钟中断处理中也会检查信号 void timer_interrupt(void) { // ... 更新jiffies检查时间片 ... // 检查当前进程是否有信号待处理 if (current-pending.signals ~current-blocked) { // 有未决信号 // 在中断返回用户态时会处理 current-need_resched 1; } }核心认知时钟中断 ≠ 强制切换进程时钟中断是“心跳”和“夺权”硬件定时器每次发出电信号都是强制把 CPU 的控制权从用户程序手里夺过来交给操作系统内核。时间片是“额度”只是操作系统用来防止某个进程过度霸占 CPU 的防御机制兜底策略。结果触发时钟中断后操作系统夺回控制权看了一眼当前进程的“额度”还没用完于是又把控制权还给了它。这完全合法且极其必要。既然要继续跑那“夺权”的这一下时钟中断意义何在时钟中断绝不是白白中断一下操作系统在这个微秒级的瞬间干了极其重要的活处理紧急硬件事件网卡来了数据包、你按了鼠标都是靠中断机制响应的。没有时钟中断强制打断系统无法响应。维护系统时间与定时器每次中断系统时间加一点检查有没有sleep(1)到点了、TCP 重传超时了。系统监控与记账计算每个进程的真实 CPU 占用率比如top命令。防止死循环最重要的兜底如果用户程序写了个while(1)死循环没有时钟中断把 CPU 强行抢回来整个系统就会直接死机。难道所有进程都死板地按时间片跑吗绝对不是。死板地按时间片轮转是极度低效的。真实情况是如果进程卡在 I/O比如等键盘输入、读文件它会主动让出 CPU 进入休眠如果突然有高优先级任务比如你刚点了播放音乐操作系统会主动抢占当前的进程。信号捕捉中断驱动的完整流程信号捕捉发生的时机信号捕捉不是在信号产生时立即执行而是在从内核态返回用户态之前进行检查。信号捕捉的时机 1. 系统调用返回 2. 中断处理返回 3. 异常处理返回 4. 进程调度返回 → 任何从内核态回到用户态的时刻信号处理需要满足两个条件1. 必须在内核态执行修改进程栈、设置返回地址等2. 信号处理函数在用户态执行用户的代码因此最自然的时机就是刚从内核态出来还没回到用户态的时刻这样内核可以修改返回路径让程序先执行handler再回到main完整流程拆解阶段1信号产生以键盘中断为例键盘中断 → 读扫描码 → 识别CtrlC → 发送SIGINT// 键盘中断处理 void keyboard_interrupt(void) { // ... 读取扫描码识别按键 ... if (is_ctrl_c()) { // 获取前台进程 struct task_struct *p get_foreground_process(); // 发送信号 send_signal(p, SIGINT); } } // 发送信号 void send_signal(struct task_struct *p, int signo) { // 设置pending位 p-pending.signal | (1 signo); // 如果进程在可中断睡眠状态唤醒它 if (p-state TASK_INTERRUPTIBLE) { wake_up_process(p); } }阶段2时钟中断触发时钟中断 → 更新系统时间 → 检查时间片 → 可能触发调度void timer_interrupt(void) { jiffies; // 检查当前进程的时间片 if (--current-counter 0) { // 时间片还有但也要检查信号 if (current-pending.signals ~current-blocked) { // 有信号待处理标记需要调度 current-need_resched 1; } return; } // 时间片用完调度 do_timer(1); }阶段3进程调度可能发生void schedule(void) { // 选择下一个要运行的进程 int next pick_next_task(); if (next ! current) { // 上下文切换 switch_to(next); } // 如果没有切换也要处理信号 do_signal(); }阶段4中断返回前检查信号// 中断返回时的处理 void ret_from_interrupt(void) { // 检查是否需要调度 if (current-need_resched) { schedule(); } // 检查是否有信号需要处理 if (current-pending.signals ~current-blocked) { do_signal(); // 处理信号 } } // 处理信号 void do_signal(void) { // 找到第一个未阻塞的未决信号 int signo find_first_pending(); // 获取该信号的处理动作 struct k_sigaction *ka current-sighand-action[signo]; if (ka-sa_handler SIG_DFL) { // 默认动作 do_default_action(signo); } else if (ka-sa_handler SIG_IGN) { // 忽略 clear_pending(signo); } else { // 信号捕捉开始执行用户处理函数 handle_signal(signo, ka); } }阶段5信号捕捉的准备void handle_signal(int signo, struct k_sigaction *ka) { struct pt_regs *regs current-thread.regs; // 用户寄存器 unsigned long *stack (unsigned long *)regs-esp; // 1. 保存用户态上下文到栈用于sigreturn struct sigcontext *sc (struct sigcontext *)--stack; sc-eip regs-eip; sc-esp regs-esp; sc-eflags regs-eflags; sc-oldmask current-blocked; // 保存信号屏蔽字 // 2. 压入sigreturn地址 *(--stack) (unsigned long)stub_sigreturn; // 3. 设置新的执行地址 regs-eip (unsigned long)ka-sa_handler; // 4. 设置参数signo通过eax传递 regs-eax signo; // 5. 切换栈 regs-esp (unsigned long)stack; // 6. 阻塞当前信号自动 sigaddset(current-blocked, signo); // 加上sa_mask中指定的额外信号 current-blocked | ka-sa_mask; }阶段6返回用户态执行handlerCPU执行iret指令 1. 从内核栈恢复用户态上下文 2. CPL从0变为3 3. 跳转到handler的地址 4. 开始执行用户定义的信号处理函数void my_handler(int signo) { // signo从eax获取 printf(捕获信号: %d\n, signo); // 这里在用户态执行使用用户栈 // 可以调用普通函数但要小心重入问题 // 可以访问用户数据 // 处理完成后return }阶段7handler返回 → sigreturn// 当handler执行return时 // 1. ret指令弹出stub_sigreturn的地址到EIP // 2. 跳转到stub_sigreturn stub_sigreturn: mov eax, __NR_sigreturn // 系统调用号 int $0x80 // 触发系统调用 // sigreturn系统调用 asmlinkage void sys_sigreturn(void) { struct pt_regs *regs current-thread.regs; struct sigcontext *sc (struct sigcontext *)regs-esp; // 恢复所有寄存器 regs-eip sc-eip; regs-esp sc-esp; regs-eflags sc-eflags; // ... 恢复其他寄存器 ... // 恢复信号屏蔽字 current-blocked sc-oldmask; // 清除该信号的pending位 clear_pending(signo); }阶段8返回到main函数// sigreturn返回后再次执行iret // CPU从内核态回到用户态 // 从之前保存的上下文恢复main的执行 // main继续执行完全不知道刚才发生了什么用户态和内核态用户态和内核态是CPU的两种工作状态特权级用来限制不同代码对硬件资源和敏感数据的访问权限。用户态权限低内核态权限高。直观类比想象一栋大楼的安保系统 大楼 计算机系统 一楼大厅 用户态任何人都可以进入 核心机房 内核态只有授权人员可以进入 - 普通访客用户程序只能在一楼大厅活动 - 系统管理员内核可以进入核心机房 - 要进入核心机房必须通过安检通道系统调用 - 核心机房有最贵重的设备硬件和最重要的数据内核数据结构通俗理解用户态内核态普通模式特权模式权限低权限高执行用户程序执行操作系统代码不能直接访问硬件可以直接操作硬件不能访问内核空间内存可以访问所有内存受限的CPU指令集全部CPU指令集程序崩溃可恢复内核崩溃导致系统宕机CPU特权级硬件层面的支撑Intel x86的四环模型Intel x86架构设计了4个特权级Ring 0-3用于保护系统资源┌─────────────────────┐ │ Ring 0 │ ← 内核态 │ 最高权限 │ Linux使用 │ 所有指令都可执行 │ │ 访问所有内存 │ ├─────────────────────┤ │ Ring 1 │ ← 设备驱动某些OS使用 │ │ ├─────────────────────┤ │ Ring 2 │ ← 设备驱动某些OS使用 │ │ ├─────────────────────┤ │ Ring 3 │ ← 用户态 │ 最低权限 │ Linux使用 │ 受限指令集 │ │ 只能访问用户空间 │ └─────────────────────┘关键事实Linux只使用Ring 0内核态和Ring 3用户态Ring 1和Ring 2在Linux中基本不使用CPU通过CPLCurrent Privilege Level当前特权级来判断当前处于哪个环当前特权级CPL段选择子的低2位表示CPLCS寄存器代码段选择子的位0-1内核代码段CS0x10 → CPL0用户代码段CS0x1B → CPL3内核态时CS 0x10 (二进制: 10000) → 低2位00 → 特权级0用户态时CS 0x1B (二进制: 11011) → 低2位11 → 特权级3内存空间划分虚拟地址空间布局以32位Linux系统为例每个进程拥有4GB的虚拟地址空间32位系统4GB虚拟地址空间2^32 4GB 地址从低到高 ┌──────────────────────────────────────┐ 0xFFFFFFFF │ │ 内核空间 (1GB) 内核态可访问 │ (0xC0000000 - 0xFFFFFFFF) 所有进程共享 │ │ ┌──────────────────────────┐ │ │ 内核代码和数据 │ │ 中断向量表 │ │ 内核栈 │ │ task_struct │ └──────────────────────────┘ ├──────────────────────────────────────┤ 0xC0000000 │ │ 用户空间 (3GB) 用户态可访问 │ (0x00000000 - 0xBFFFFFFF) 每个进程独有 │ │ ┌──────────────────────────┐ │ │ 用户代码 (.text) │ │ 用户数据 (.data/.bss) │ │ 堆 (Heap) │ │ 共享库 │ │ 栈 (Stack) │ └──────────────────────────┘ │ └──────────────────────────────────────┘ 0x00000000地址空间的特点// 用户态视角只能访问0-3GB void user_code() { // 可以访问自己的代码段 // 可以访问自己的数据段 // 可以访问自己的堆和栈 // 不能访问3-4GB的内核空间 } // 内核态视角可以访问所有内存 void kernel_code() { // 可以访问当前进程的用户空间 // 可以访问所有进程的内核空间共享 // 可以访问硬件设备内存 // 可以访问物理内存 }用户态和内核态的切换什么时候发生切换┌─────────────────────────────────────────────────────────────────┐ │ 用户态 → 内核态的切换时机 ├─────────────────────────────────────────────────────────────────┤ │ │ 1. 系统调用 │ └── 用户主动请求内核服务read、write、fork等 │ │ 2. 硬件中断 │ └── 外部设备通知CPU键盘、鼠标、时钟等 │ │ 3. CPU异常 │ └── 指令执行出错除零、缺页、非法指令等 │ │ 4. 进程调度 │ └── 时间片用完或进程主动让出CPUschedule │ └─────────────────────────────────────────────────────────────────┘信号处理中的切换次数正常执行用户态(main)产生信号用户态 → 内核态中断/系统调用设置pending内核态返回检查内核态 → 用户态发现信号执行handler内核态 → 用户态(handler)修改返回路径handler返回用户态(handler) → 内核态sigreturn恢复main内核态 → 用户态(main)总共4次用户态↔内核态切换如果你的代码里没有用signal()去注册自定义的handler函数即执行的是系统默认行为比如直接杀死进程、忽略信号就只有两次切换。用户态和内核态是CPU为了系统安全而设计的两种特权模式用户态运行普通程序权限受限内核态运行操作系统拥有全部权限。用户程序通过系统调用、中断和异常这三种方式在需要时暂时切换到内核态获取服务完成后再返回用户态继续执行。可重入函数定义可重入函数Reentrant Function是指可以被多个执行流如信号处理函数、线程同时调用而不会产生数据错乱或未定义行为的函数。简单来说函数可以在执行过程中被中断并再次进入而不会造成任何问题。核心概念图解同一函数的多次调用 不可重入函数 ┌─────────────────────────────────────────────────────────────────┐ │ 不可重入函数 │ │ 调用1 ──► ┌─────────────┐ │ │ 修改全局 │ ← 共享数据 │ 调用2 ──► │ 变量/链表 │ ← 数据被覆盖/损坏 │ └─────────────┘ │ └─────────────────────────────────────────────────────────────────┘ 可重入函数 ┌─────────────────────────────────────────────────────────────────┐ │ 可重入函数 │ │ 调用1 ──► ┌─────────────┐ │ │ 局部变量 │ ← 栈上独立 │ 调用2 ──► │ 局部变量 │ ← 互不干扰 │ └─────────────┘ │ └─────────────────────────────────────────────────────────────────┘为什么会导致问题问题的根源共享状态函数的四种数据区域1. 参数通常安全每个调用都有自己的参数副本2. 局部变量安全存储在栈上每次调用独立3. 静态变量危险存储在数据段所有调用共享4. 全局变量危险存储在数据段所有调用共享可重入函数的判定标准三个安全条件条件1不使用静态数据条件2不修改全局数据条件3不调用不可重入函数
企业数字化 ERP 产品动态
相关推荐
打印机SNMP被锁怎么办:epson_print_conf USB模式绕过新款爱普生打印机访问限制的实战指南 【免费下载链接】epson_print_conf Epson Printer Configuration tool and waste ink counter resetter 项目地址: https://gitcode.com/gh_mirrors/ep/epson_print_conf 点击查看 免费下载 新款爱普生(Epson)打印机把 SNMP 的 EEPROM 访问锁… · 2026/11/5 7:44:49
LMMS 音频插件怎么选、扫不到怎么修?一篇实战排障手册 LMMS 音频插件怎么选、扫不到怎么修?一篇实战排障手册 【免费下载链接】lmms Cross-platform music production software 项目地址: https://gitcode.com/gh_mirrors/lm/lmms
LMMS 音频插件分 LADSPA、LV2 和 VST 三个家族。新手最常见的卡点就三个ÿ… · 2026/11/5 7:43:49
FreeTube播放列表管理终极指南:创建、编辑与同步全攻略 FreeTube播放列表管理终极指南:创建、编辑与同步全攻略
FreeTube作为一款注重隐私保护的开源YouTube应用程序,不仅提供了无广告的视频观看体验,还内置了强大的播放列表管理功能。本指南将带你掌握从基础创建到高级同步的全方位技巧ÿ… · 2026/11/5 7:42:49
免费降ai软件实测:先免费降AI 1000字,满意后整篇降,维普AI率96.46%降到0% /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 8:03:55
老房翻新报价单怎么看,拆除清运和基层处理有没有单列 老房翻新报价单要看的不是总价,而是拆除清运和基层处理有没有单列出来,这两项一旦被混进综合项或干脆不写,后面拆开墙面、铲掉旧地面时,报价就会越变越长。 深圳市星居易装饰有限公司2017年起在深圳经营,专注房龄十年以… · 2026/11/5 8:03:55
七千亿排队入场,钱的关键在于穿过项目之后 《七千亿排队入场,钱的关键在于穿过项目之后》
——资金投放决定起点,订单与收入决定内需回暖的力度先说一个让人坐不住的数字:今年八千亿元的政策性金融工具,到四季度还有约七千亿元没有投出去。换句话说,全年额度的近… · 2026/11/5 8:03:55
嘎嘎aigc降重免费入口:降重降AI一起做,新用户免费降AI 1000字 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 8:03:55
三层交换机与路由器:核心概念与定位 一、核心概念与定位三层交换机(Layer 3 Switch)和路由器(Router)是构建现代网络的关键设备,但它们的设计目标和工作层级不同。三层交换机本质上是“带路由功能的二层交换机”,它主要工作在数据链路层&#… · 2026/11/5 8:03:55
【量化实战】VeighNa (vnpy) 回测“无结果”排雷指南:从 GUI 到 CLI 的完美对齐 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 8:02:55
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38