硬件层面的中断1. 外设准备就绪当按下键盘、网卡收到数据包、磁盘读写完成时外设硬件会产生一个电信号通过专用线路发给中断控制器。每个设备都有自己的 “专属线路”不会和别的设备信号混在一起。这个信号就是硬件中断请求意思是“我这边有数据 / 事件了CPU 快来处理我”2. 中断控制器“我来当中间调度员”中断控制器收到一堆设备的中断请求后会做两件关键的硬件事优先级仲裁如果多个设备同时发请求它会按预设优先级比如时钟中断 网卡 键盘选出 “最急的那个”。通知 CPU 传编号向 CPU 的INTR 引脚发一个电信号告诉 CPU “有设备找你”。同时把这个设备对应的 ** 中断号IRQ 编号** 放到数据总线上让 CPU 知道是 “哪个设备” 在喊它。3. CPU“先暂停手头工作存好现场”CPU 在执行完当前指令后检测到 INTR 引脚的信号就会立刻暂停正在跑的程序不管是用户进程还是内核代码然后硬件自动完成 “保护现场”把当前的程序计数器PC、标志寄存器EFLAGS、通用寄存器这些关键状态压入内核栈里存起来。这一步是纯硬件行为不需要软件插手目的是等会儿处理完中断能精确回到刚才被打断的地方继续执行就像没被打断过一样。同时 CPU 会暂时关闭可屏蔽中断防止新的中断干扰这次处理。4. CPU“查中断向量表找到处理函数入口”CPU 拿到中断号后会用这个编号当索引去查内存里的中断向量表IDTIDT 是内核启动时就预先建好的硬件数据结构每个中断号都对应一个表项里面存着这个中断对应的 ** 中断服务程序ISR** 的内存地址权限、段信息等硬件执行需要的参数比如中断号 1 → 键盘中断 → 指向 “处理键盘输入” 的 ISR 地址中断号 14 → 硬盘中断 → 指向 “处理磁盘读写完成” 的 ISR 地址。这一步是硬件寻址逻辑CPU 直接跳转到对应的 ISR 代码开始执行。5. 执行中断服务程序ISR“快速搞定硬件操作”ISR 是内核里写给对应设备的硬件处理代码在中断上下文里执行有严格限制必须非常快不能睡眠、不能调用阻塞函数比如不能等待磁盘 IO因为 CPU 还 “占着” 中断别的设备没法发请求。典型操作读外设寄存器从设备 I/O 端口或内存映射地址MMIO读数据比如键盘扫描码、网卡数据包头。清中断标志向设备寄存器写一个值告诉设备 “我已经收到你的请求了别再发信号了”。延后耗时操作把复杂处理比如解析完整网络包、保存文件交给软中断 / 工作队列让内核稍后在进程上下文里慢慢处理。6. 恢复现场“回到原来的工作流”ISR 执行完后CPU 硬件会自动从内核栈里恢复之前保存的寄存器、PC、标志位然后重新打开可屏蔽中断整个状态和中断发生前一模一样CPU 继续执行刚才被打断的程序。外设和中断控制器也会重置中断请求状态等待下一次事件触发。一句话总结外设喊一声 → 控制器排好队 → CPU 存好现场 → 查表找处理函数 → 快速处理硬件 → 恢复原状继续干活闹钟中断电脑主板上存在一颗独立的硬件实时时钟与可编程定时器它依靠主板电池独立供电即便电脑断网、拔掉电源数周后重新开机系统仍能显示准确时间这正是该硬件持续工作的直接证明。闹钟中断 由硬件定时器周期性、自动触发的同步中断。内核启动时配置好触发周期后硬件会以固定时间间隔自动向 CPU 发送中断信号强制打断当前运行的进程使操作系统或其他进程重新获取 CPU 资源避免单一进程长期独占 CPU 导致系统卡死或失效让操作系统始终保有对 CPU 的控制权。两次闹钟中断触发的间隔时间段内CPU 完全交付给某一进程执行操作系统内核处于 “休眠” 状态不主动干预进程的指令流。所以CPU 的频率是性能的关键体现。频率越高单位时间内时钟周期就越多能执行的指令越多、进程切换与任务调度的轮转次数也越频繁整体处理能力就越强性能也就越好。图中的 2.6GHz 是 CPU 常规状态下的运行频率而操作系统真正的调度周期是由内核在这个硬件频率的基础上通过时钟中断重新分配、管理和控制的。闹钟中断也属于硬件中断。软件中断除了上面的硬件中断也能由软件程序触发。软件中断不是硬件发的而是软件程序自己主动执行指令主动触发的中断。硬件中断是外部事件打断CPU而软件中断则是进程主动进入CPU请求资源。在Linux系统中所有的系统调用函数都会被保存在一张数组中假设我们执行下面的代码ssize_t n read(fd, buf, count);首先我们要明白read函数最终是需要调用Linux系统的sys_read系统函数的。在这之前C语言标准库会做出调用前的准备mov ebx, fd ; 放第1个参数 mov ecx, buf ; 放第2个参数 mov edx, count ; 放第3个参数 mov eax, 3 ; 放read的系统调用号告诉内核要调用sys_read当执行到下面的汇编代码时软件中断会立刻产生CPU会从用户态转到内核态并保护现场跳转到内核的中断处理函数int 0x80 ; 老式x86 ; 或 syscall ; 现代x86_64之后内核从eax里拿到3去sys_call_table[3]找到sys_read执行内核逻辑完成读文件操作。最后内核把结果放到eax恢复现场回到用户态read函数把eax的值返回。调用系统函数本质是选取对应的数组下标。从中断来解释操作系统从上面的结论可以看出OS是跑在中断上面的。OS本质上就是一个死循环如果OS需要每时每刻去询问外设或进程其的轮询状况那么OS的效率会变得极低。但是在中断的帮助下OS可以一直处于工作状态当其接受到中断信号后OS会暂停并保存当前的工作进度转而去处理中断带来的工作当插入的任务完成后OS会恢复之前的进度继续执行未完成的工作。总的来说中断信号是喊OS干活的闹钟。用户态和内核态操作系统运行时除了管理用户应用程序自身也需要运行内核代码、维护内核数据结构。如果允许用户态程序直接访问内核资源、使用内核级权限系统极易被异常操作破坏甚至崩溃。同时每个进程拥有独立的地址空间和私有数据必须严格隔离、互不干扰与各进程都拥有自己的用户页表相比内核页表是全局唯一、所有进程共享的核心组件因此需要通过内核页表与用户页表分离实现权限隔离、地址隔离和系统稳定运行。所以操作系统需独立运行内核代码与管理内核数据必须实现用户态与内核态分离。32位下Linux的虚拟地址空间为4G从低到高其中0~3G属于用户态3~4G属于内核态。用户态和内核态各自拥有一份页表结构用户页表和内核页表。页表中专门有区分用户态和内核态的变量DPL如果DPL为000那么就是内核态如果为113则为用户态。用户态只管 “算逻辑”内核态只管 “碰硬件、管资源”两者靠系统调用切换一起把程序跑完。例如假如程序要执行scanf函数scanf(%d, a);1. 一开始全程 用户态程序的代码、变量、栈、堆都在0~3G 用户空间用的是用户页表只能碰自己的数据不能碰键盘、硬盘、网卡调用scanf它是用户态库函数它自己拿不到键盘输入。2. 必须求助内核进入 内核态用户态做不了硬件操作于是库函数准备好系统调用号执行int 0x80/syscall指令CPU 从用户态 → 内核态权限从 Ring3 → Ring0同时可以用用户页表 内核页表3. 内核态真正干活内核做三件事用内核页表访问键盘驱动、读取按键数据用用户页表把数据写到你程序的变量a里做完清理现场这里是关键内核必须同时用两张表才能完成一次读写读硬件 → 内核页表写回用户变量 → 用户页表4. 回到用户态继续跑内核把结果给程序切回用户态只能用用户页表继续执行后面的代码sigaction函数#include signal.h int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);参数作用signum要处理的信号比如SIGINT2、SIGTERM15、SIGSEGV11act新的信号处理规则告诉内核 “怎么处理这个信号”oldact保存原来的处理规则可选传 NULL 表示不保存成功就返回0失败返回-1并设error。struct sigaction结构体struct sigaction { void (*sa_handler)(int); // 简单信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 带详细信息的处理函数 sigset_t sa_mask; // 额外要屏蔽的信号集合 int sa_flags; // 控制行为的标志位 void (*sa_restorer)(void); // 已废弃无需关注 };sa_mask是sigaction函数第二个参数指向的struct sigaction结构体核心字段核心解决信号处理函数执行时被其他信号打断导致递归调用、逻辑混乱甚至程序崩溃的问题。sa_mask专门解决“一个信号的处理函数正在执行时被另一个信号强行打断导致逻辑乱掉甚至程序崩溃”的问题比如给SIGINT按 CtrlC 触发注册处理函数时把SIGQUIT按 Ctrl\ 触发加到sa_mask里当程序收到SIGINT并开始执行它的处理函数比如要跑 5 秒的业务逻辑内核会自动把SIGINT当前正在处理的信号和SIGQUITsa_mask里加的信号都放进进程的信号屏蔽字中。这时候就算一直按 Ctrl\ 发SIGQUIT这个信号也会被内核拦住 —— 不会出现 “SIGINT处理到一半突然暂停去执行SIGQUIT处理函数” 的打断问题。等SIGINT处理函数跑完内核会自动去掉这两个临时屏蔽的信号恢复原本的规则之前被拦住的SIGQUIT这时候才会生效处理这样就能保证SIGINT的处理逻辑从头到尾完整执行不会被打断。#include stdio.h #include signal.h #include unistd.h // SIGINT处理函数 void sigint_handler(int signum) { printf(\n 开始处理SIGINTCtrlC接下来5秒屏蔽SIGQUITCtrl\\\n); sleep(5); // 核心业务逻辑5秒内若按Ctrl\SIGQUIT会被阻塞 printf( SIGINT处理完成解除SIGQUIT屏蔽此时按过的Ctrl\\会立即生效\n); } int main() { struct sigaction act; // 1. 配置信号处理规则 act.sa_handler sigint_handler; // 指定SIGINT的处理函数 sigemptyset(act.sa_mask); // 清空屏蔽集合 sigaddset(act.sa_mask, SIGQUIT); // 关键在sa_mask中添加SIGQUIT act.sa_flags 0; // 默认行为 // 2. 注册规则第二个参数act传入包含sa_mask的配置 if (sigaction(SIGINT, act, NULL) -1) { perror(sigaction注册失败); return 1; } printf(程序运行中\n1. 先按CtrlC触发SIGINT\n2. 触发后5秒内按Ctrl\\测试屏蔽效果\n); while (1) sleep(1); // 保持程序运行 return 0; }可重入函数1. p-next head; // 步骤1新节点指向原头节点 4. head p; // 步骤4头指针更新为新节点在插入函数中步骤1、4的全部完成才能视为成功插入主函数main插入node1执行步骤 1(node1-next head)node1指向了原来的头节点。此时CPU 发生时钟中断切换到内核态。内核态处理中断内核发现有信号比如SIGINT准备切回用户态执行信号处理函数。信号处理函数sighandler插入node2执行步骤 1(node2-next head)node2-next指向了当前的head也就是node1。执行步骤 4(head node2)head指针指向node2。此时信号处理函数执行完毕返回。回到主函数main继续执行步骤 4(head node1)Bug 发生head被重新赋值回node1导致刚刚插入的node2彻底丢失只剩下node1指向原来链表内存泄漏如果node2还没来得及赋值head就被中断可能会导致链表成环死循环。可重入函数是指函数在执行过程中被中断允许另一个线程 / 信号处理函数调用它且执行完毕后原函数继续执行依然能得到正确结果不会造成数据错乱。上面的insert函数就不是可重入函数。可重入函数的标准不使用全局 / 静态变量只使用栈上变量或参数。不调用不可重入函数比如 malloc、printf标准库可能有全局锁、strtok内部有静态缓冲区。原子操作指令序列不被中断即执行过程不能被切分。对于是否可重入我们要将其看成函数的一个特性而不是一个BUG。volatile变量类型修饰符#include stdio.h #include signal.h // 未加volatile编译器将flag缓存到寄存器不读内存 int flag 0; // 信号处理函数修改内存中的flag为1 void handler(int sig) { flag 1; // 仅修改内存值寄存器中的值仍为0 } int main() { signal(SIGINT, handler); // 注册CtrlC信号 // 编译器优化循环内只读寄存器的旧值0永远不退出 while (flag 0); printf(循环退出\n); return 0; }在上面代码中如果编译器的优化程度很高就会导致flag变量被存入寄存器的隐形问题这时虽然能提高读取效率但就会导致程序一致读取的是寄存器中的值导致内存中的变量改变无效造成死循环。而加volatile后编译器禁止寄存器缓存while每次读取内存中flag的真实值信号修改后循环立即退出。#include stdio.h #include signal.h // 加volatile强制每次读内存而非寄存器 volatile int flag 0; void handler(int sig) { flag 1; } int main() { signal(SIGINT, handler); while (flag 0); // 每次读内存中flag的真实值 printf(循环退出\n); return 0; }
中断与信号
硬件层面的中断1. 外设准备就绪当按下键盘、网卡收到数据包、磁盘读写完成时外设硬件会产生一个电信号通过专用线路发给中断控制器。每个设备都有自己的 “专属线路”不会和别的设备信号混在一起。这个信号就是硬件中断请求意思是“我这边有数据 / 事件了CPU 快来处理我”2. 中断控制器“我来当中间调度员”中断控制器收到一堆设备的中断请求后会做两件关键的硬件事优先级仲裁如果多个设备同时发请求它会按预设优先级比如时钟中断 网卡 键盘选出 “最急的那个”。通知 CPU 传编号向 CPU 的INTR 引脚发一个电信号告诉 CPU “有设备找你”。同时把这个设备对应的 ** 中断号IRQ 编号** 放到数据总线上让 CPU 知道是 “哪个设备” 在喊它。3. CPU“先暂停手头工作存好现场”CPU 在执行完当前指令后检测到 INTR 引脚的信号就会立刻暂停正在跑的程序不管是用户进程还是内核代码然后硬件自动完成 “保护现场”把当前的程序计数器PC、标志寄存器EFLAGS、通用寄存器这些关键状态压入内核栈里存起来。这一步是纯硬件行为不需要软件插手目的是等会儿处理完中断能精确回到刚才被打断的地方继续执行就像没被打断过一样。同时 CPU 会暂时关闭可屏蔽中断防止新的中断干扰这次处理。4. CPU“查中断向量表找到处理函数入口”CPU 拿到中断号后会用这个编号当索引去查内存里的中断向量表IDTIDT 是内核启动时就预先建好的硬件数据结构每个中断号都对应一个表项里面存着这个中断对应的 ** 中断服务程序ISR** 的内存地址权限、段信息等硬件执行需要的参数比如中断号 1 → 键盘中断 → 指向 “处理键盘输入” 的 ISR 地址中断号 14 → 硬盘中断 → 指向 “处理磁盘读写完成” 的 ISR 地址。这一步是硬件寻址逻辑CPU 直接跳转到对应的 ISR 代码开始执行。5. 执行中断服务程序ISR“快速搞定硬件操作”ISR 是内核里写给对应设备的硬件处理代码在中断上下文里执行有严格限制必须非常快不能睡眠、不能调用阻塞函数比如不能等待磁盘 IO因为 CPU 还 “占着” 中断别的设备没法发请求。典型操作读外设寄存器从设备 I/O 端口或内存映射地址MMIO读数据比如键盘扫描码、网卡数据包头。清中断标志向设备寄存器写一个值告诉设备 “我已经收到你的请求了别再发信号了”。延后耗时操作把复杂处理比如解析完整网络包、保存文件交给软中断 / 工作队列让内核稍后在进程上下文里慢慢处理。6. 恢复现场“回到原来的工作流”ISR 执行完后CPU 硬件会自动从内核栈里恢复之前保存的寄存器、PC、标志位然后重新打开可屏蔽中断整个状态和中断发生前一模一样CPU 继续执行刚才被打断的程序。外设和中断控制器也会重置中断请求状态等待下一次事件触发。一句话总结外设喊一声 → 控制器排好队 → CPU 存好现场 → 查表找处理函数 → 快速处理硬件 → 恢复原状继续干活闹钟中断电脑主板上存在一颗独立的硬件实时时钟与可编程定时器它依靠主板电池独立供电即便电脑断网、拔掉电源数周后重新开机系统仍能显示准确时间这正是该硬件持续工作的直接证明。闹钟中断 由硬件定时器周期性、自动触发的同步中断。内核启动时配置好触发周期后硬件会以固定时间间隔自动向 CPU 发送中断信号强制打断当前运行的进程使操作系统或其他进程重新获取 CPU 资源避免单一进程长期独占 CPU 导致系统卡死或失效让操作系统始终保有对 CPU 的控制权。两次闹钟中断触发的间隔时间段内CPU 完全交付给某一进程执行操作系统内核处于 “休眠” 状态不主动干预进程的指令流。所以CPU 的频率是性能的关键体现。频率越高单位时间内时钟周期就越多能执行的指令越多、进程切换与任务调度的轮转次数也越频繁整体处理能力就越强性能也就越好。图中的 2.6GHz 是 CPU 常规状态下的运行频率而操作系统真正的调度周期是由内核在这个硬件频率的基础上通过时钟中断重新分配、管理和控制的。闹钟中断也属于硬件中断。软件中断除了上面的硬件中断也能由软件程序触发。软件中断不是硬件发的而是软件程序自己主动执行指令主动触发的中断。硬件中断是外部事件打断CPU而软件中断则是进程主动进入CPU请求资源。在Linux系统中所有的系统调用函数都会被保存在一张数组中假设我们执行下面的代码ssize_t n read(fd, buf, count);首先我们要明白read函数最终是需要调用Linux系统的sys_read系统函数的。在这之前C语言标准库会做出调用前的准备mov ebx, fd ; 放第1个参数 mov ecx, buf ; 放第2个参数 mov edx, count ; 放第3个参数 mov eax, 3 ; 放read的系统调用号告诉内核要调用sys_read当执行到下面的汇编代码时软件中断会立刻产生CPU会从用户态转到内核态并保护现场跳转到内核的中断处理函数int 0x80 ; 老式x86 ; 或 syscall ; 现代x86_64之后内核从eax里拿到3去sys_call_table[3]找到sys_read执行内核逻辑完成读文件操作。最后内核把结果放到eax恢复现场回到用户态read函数把eax的值返回。调用系统函数本质是选取对应的数组下标。从中断来解释操作系统从上面的结论可以看出OS是跑在中断上面的。OS本质上就是一个死循环如果OS需要每时每刻去询问外设或进程其的轮询状况那么OS的效率会变得极低。但是在中断的帮助下OS可以一直处于工作状态当其接受到中断信号后OS会暂停并保存当前的工作进度转而去处理中断带来的工作当插入的任务完成后OS会恢复之前的进度继续执行未完成的工作。总的来说中断信号是喊OS干活的闹钟。用户态和内核态操作系统运行时除了管理用户应用程序自身也需要运行内核代码、维护内核数据结构。如果允许用户态程序直接访问内核资源、使用内核级权限系统极易被异常操作破坏甚至崩溃。同时每个进程拥有独立的地址空间和私有数据必须严格隔离、互不干扰与各进程都拥有自己的用户页表相比内核页表是全局唯一、所有进程共享的核心组件因此需要通过内核页表与用户页表分离实现权限隔离、地址隔离和系统稳定运行。所以操作系统需独立运行内核代码与管理内核数据必须实现用户态与内核态分离。32位下Linux的虚拟地址空间为4G从低到高其中0~3G属于用户态3~4G属于内核态。用户态和内核态各自拥有一份页表结构用户页表和内核页表。页表中专门有区分用户态和内核态的变量DPL如果DPL为000那么就是内核态如果为113则为用户态。用户态只管 “算逻辑”内核态只管 “碰硬件、管资源”两者靠系统调用切换一起把程序跑完。例如假如程序要执行scanf函数scanf(%d, a);1. 一开始全程 用户态程序的代码、变量、栈、堆都在0~3G 用户空间用的是用户页表只能碰自己的数据不能碰键盘、硬盘、网卡调用scanf它是用户态库函数它自己拿不到键盘输入。2. 必须求助内核进入 内核态用户态做不了硬件操作于是库函数准备好系统调用号执行int 0x80/syscall指令CPU 从用户态 → 内核态权限从 Ring3 → Ring0同时可以用用户页表 内核页表3. 内核态真正干活内核做三件事用内核页表访问键盘驱动、读取按键数据用用户页表把数据写到你程序的变量a里做完清理现场这里是关键内核必须同时用两张表才能完成一次读写读硬件 → 内核页表写回用户变量 → 用户页表4. 回到用户态继续跑内核把结果给程序切回用户态只能用用户页表继续执行后面的代码sigaction函数#include signal.h int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);参数作用signum要处理的信号比如SIGINT2、SIGTERM15、SIGSEGV11act新的信号处理规则告诉内核 “怎么处理这个信号”oldact保存原来的处理规则可选传 NULL 表示不保存成功就返回0失败返回-1并设error。struct sigaction结构体struct sigaction { void (*sa_handler)(int); // 简单信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 带详细信息的处理函数 sigset_t sa_mask; // 额外要屏蔽的信号集合 int sa_flags; // 控制行为的标志位 void (*sa_restorer)(void); // 已废弃无需关注 };sa_mask是sigaction函数第二个参数指向的struct sigaction结构体核心字段核心解决信号处理函数执行时被其他信号打断导致递归调用、逻辑混乱甚至程序崩溃的问题。sa_mask专门解决“一个信号的处理函数正在执行时被另一个信号强行打断导致逻辑乱掉甚至程序崩溃”的问题比如给SIGINT按 CtrlC 触发注册处理函数时把SIGQUIT按 Ctrl\ 触发加到sa_mask里当程序收到SIGINT并开始执行它的处理函数比如要跑 5 秒的业务逻辑内核会自动把SIGINT当前正在处理的信号和SIGQUITsa_mask里加的信号都放进进程的信号屏蔽字中。这时候就算一直按 Ctrl\ 发SIGQUIT这个信号也会被内核拦住 —— 不会出现 “SIGINT处理到一半突然暂停去执行SIGQUIT处理函数” 的打断问题。等SIGINT处理函数跑完内核会自动去掉这两个临时屏蔽的信号恢复原本的规则之前被拦住的SIGQUIT这时候才会生效处理这样就能保证SIGINT的处理逻辑从头到尾完整执行不会被打断。#include stdio.h #include signal.h #include unistd.h // SIGINT处理函数 void sigint_handler(int signum) { printf(\n 开始处理SIGINTCtrlC接下来5秒屏蔽SIGQUITCtrl\\\n); sleep(5); // 核心业务逻辑5秒内若按Ctrl\SIGQUIT会被阻塞 printf( SIGINT处理完成解除SIGQUIT屏蔽此时按过的Ctrl\\会立即生效\n); } int main() { struct sigaction act; // 1. 配置信号处理规则 act.sa_handler sigint_handler; // 指定SIGINT的处理函数 sigemptyset(act.sa_mask); // 清空屏蔽集合 sigaddset(act.sa_mask, SIGQUIT); // 关键在sa_mask中添加SIGQUIT act.sa_flags 0; // 默认行为 // 2. 注册规则第二个参数act传入包含sa_mask的配置 if (sigaction(SIGINT, act, NULL) -1) { perror(sigaction注册失败); return 1; } printf(程序运行中\n1. 先按CtrlC触发SIGINT\n2. 触发后5秒内按Ctrl\\测试屏蔽效果\n); while (1) sleep(1); // 保持程序运行 return 0; }可重入函数1. p-next head; // 步骤1新节点指向原头节点 4. head p; // 步骤4头指针更新为新节点在插入函数中步骤1、4的全部完成才能视为成功插入主函数main插入node1执行步骤 1(node1-next head)node1指向了原来的头节点。此时CPU 发生时钟中断切换到内核态。内核态处理中断内核发现有信号比如SIGINT准备切回用户态执行信号处理函数。信号处理函数sighandler插入node2执行步骤 1(node2-next head)node2-next指向了当前的head也就是node1。执行步骤 4(head node2)head指针指向node2。此时信号处理函数执行完毕返回。回到主函数main继续执行步骤 4(head node1)Bug 发生head被重新赋值回node1导致刚刚插入的node2彻底丢失只剩下node1指向原来链表内存泄漏如果node2还没来得及赋值head就被中断可能会导致链表成环死循环。可重入函数是指函数在执行过程中被中断允许另一个线程 / 信号处理函数调用它且执行完毕后原函数继续执行依然能得到正确结果不会造成数据错乱。上面的insert函数就不是可重入函数。可重入函数的标准不使用全局 / 静态变量只使用栈上变量或参数。不调用不可重入函数比如 malloc、printf标准库可能有全局锁、strtok内部有静态缓冲区。原子操作指令序列不被中断即执行过程不能被切分。对于是否可重入我们要将其看成函数的一个特性而不是一个BUG。volatile变量类型修饰符#include stdio.h #include signal.h // 未加volatile编译器将flag缓存到寄存器不读内存 int flag 0; // 信号处理函数修改内存中的flag为1 void handler(int sig) { flag 1; // 仅修改内存值寄存器中的值仍为0 } int main() { signal(SIGINT, handler); // 注册CtrlC信号 // 编译器优化循环内只读寄存器的旧值0永远不退出 while (flag 0); printf(循环退出\n); return 0; }在上面代码中如果编译器的优化程度很高就会导致flag变量被存入寄存器的隐形问题这时虽然能提高读取效率但就会导致程序一致读取的是寄存器中的值导致内存中的变量改变无效造成死循环。而加volatile后编译器禁止寄存器缓存while每次读取内存中flag的真实值信号修改后循环立即退出。#include stdio.h #include signal.h // 加volatile强制每次读内存而非寄存器 volatile int flag 0; void handler(int sig) { flag 1; } int main() { signal(SIGINT, handler); while (flag 0); // 每次读内存中flag的真实值 printf(循环退出\n); return 0; }