概念
信号是内核发给进程的软件中断,用来通知进程发生了某个异步事件,是进程间最简单的 IPC。
信号命令和函数
kill -l :查看信号列表
killall :按进程名发送信号,给同名的进程发信号
# 先开一个sleep进程用来测试,新开终端执行 sleep 100 & sleep 100 & # 此时有2个sleep进程 # 发送默认信号SIGTERM(15),优雅终止sleep进程 killall sleep # -9 发送SIGKILL强制杀死同名全部进程 killall -9 sleeppkill:支持模糊匹配进程名,还可以按 UID、终端等属性筛选进程,比 killall 更灵活
# 测试:后台运行sleep sleep 100 & sleep 100 & # pkill 默认SIGTERM,杀死名字匹配sleep的进程 pkill sleep # -2 发送SIGINT信号(等价Ctrl+C) pkill -2 sleep区别:pkill 是模糊匹配,pkill sle也能匹配 sleep;killall 是精确匹配
trap用于 shell 脚本,捕获信号并执行自定义命令
#!/bin/bash # 捕获SIGINT(信号2,就是Ctrl+C),收到信号执行引号内命令 trap "echo 收到ctrl+c,程序不会退出" SIGINT echo "脚本运行中,按 Ctrl+C 测试" while true do sleep 1 donesignal函数
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);- 参数 1:信号编号
- 参数 2:信号处理函数。可以填:
SIG_IGN:忽略该信号SIG_DFL:恢复系统默认处理- 自定义函数地址:捕获信号执行回调
#include <stdio.h> #include <signal.h> #include <unistd.h> // 信号处理回调函数 void sig_handler(int sig) { printf("收到信号:%d\n", sig); if(sig == SIGINT){ printf("Ctrl+C 触发,程序不会直接退出\n"); } } int main(void) { // 注册SIGINT信号处理函数 signal(SIGINT, sig_handler); printf("程序运行中,按Ctrl+C测试\n"); while(1){ sleep(1); } return 0; }sigaction 函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);struct sigaction { void (*sa_handler)(int); // 简单信号处理函数(和signal一样) void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展版本,可以拿到发送进程信息 sigset_t sa_mask; // 信号掩码:处理当前信号时,需要阻塞的信号集合 int sa_flags; // 标志位 };#include <stdio.h> #include <signal.h> #include <unistd.h> void sig_handler(int sig) { printf("sigaction捕获信号 %d\n", sig); } int main(void) { struct sigaction act; act.sa_handler = sig_handler; sigemptyset(&act.sa_mask); // 清空掩码 act.sa_flags = 0; // 注册SIGINT sigaction(SIGINT, &act, NULL); printf("运行中,Ctrl+C\n"); while(1){ sleep(1); } return 0; }信号集操作掩码
sigemptyset(&set); // 清空信号集 sigfillset(&set); // 添加所有信号 sigaddset(&set, SIGINT);// 添加指定信号 sigdelset(&set, SIGINT);// 删除指定信号 sigismember(&set,SIGINT);// 判断信号是否在集合 // 设置进程信号掩码(阻塞/解除阻塞信号) int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); how: SIG_BLOCK(阻塞), SIG_UNBLOCK(解除), SIG_SETMASK(替换)raise /kill 函数:代码内发送信号
#include <signal.h> // 给自己发信号 int raise(int sig); // pid>0:发给pid进程;pid=0发给本组进程;pid=-1发给所有有权限进程 int kill(pid_t pid, int sig);alarm () 闹钟信号 SIGALRM
#include <unistd.h> unsigned int alarm(unsigned int seconds);seconds 秒之后,给本进程发送 SIGALRM。
#include <stdio.h> #include <signal.h> #include <unistd.h> void alarm_handler(int sig) { printf("闹钟时间到!\n"); } int main() { signal(SIGALRM, alarm_handler); alarm(3); // 3秒后触发SIGALRM pause(); // 阻塞等待信号 return 0; }abort函数
#include <stdlib.h> void abort(void);- noreturn:调用后不会返回,程序异常终止
- 底层:内部会解除 SIGABRT 阻塞,然后调用
raise(SIGABRT),给自己发送6 号信号 SIGABRTDebian Man...
kill -l查看:6) SIGABRT
#include <stdio.h> #include <stdlib.h> int main(void) { printf("程序开始\n"); abort(); // 发送SIGABRT,异常终止 printf("这里永远不会执行\n"); return 0; }核心特性(非常重要!面试常考)
- 默认行为:收到 SIGABRT,终止进程,并生成 core dump 核心转储文件,方便调试崩溃原因
- 虽然你可以用
signal/sigaction捕获 SIGABRT,执行自定义处理函数✅ 但是!如果你的信号处理函数 return 返回了,abort 会再次发送 SIGABRT,强制杀死进程✅ 只有一种情况能阻止退出:信号处理函数内部不返回(longjmp 跳转出去) - abort 会忽略 SIGABRT 的阻塞 / 忽略设置,就算你 sigprocmask 阻塞 SIGABRT,abort 依然生效The Open G...
- ❗abort不会调用 atexit 注册的清理函数,不会做正常退出的资源清理(和 exit 最大区别)
pause():阻塞进程,直到收到信号。
Shell里面的前台进程和后台进程
前台进程
直接在终端敲命令执行,独占终端,Shell 会等待它执行完毕,才能继续输入下一条命令。
Ctrl + C→SIGINT(2)Ctrl + \→ SIGQUIT(3)Ctrl + Z→ SIGTSTP(20),暂停前台进程,丢到后台(停止状态)
Ctrl+C只能终止前台进程,不能终止后台进程
后台进程
命令末尾加&,放到后台运行,不占用终端,Shell 立刻返回命令提示符,可以继续输入其他命令。
sleep 20 & # & 代表后台运行 #输出 [1] 3456 ,[1]是作业号,3456是PID信号处理
忽略信号
执行信号的默认动作(handler)
信号的产生
1. 用户终端(键盘产生)
终端驱动收到按键,触发硬件中断,内核处理键盘中断,向前台进程组发送信号
Ctrl + C→SIGINT(2)Ctrl + \→SIGQUIT(3)(默认 Core 动作,生成 core)Ctrl + Z→SIGTSTP(20),暂停进程
注意:只发给前台进程组,后台进程不受影响。
2. 硬件异常(CPU 硬件检测产生)
CPU 执行指令出错,触发异常(exception,同步),内核捕获 CPU 异常,转化为信号发给当前进程
- 访问非法内存地址 → 段错误
SIGSEGV(11) - 除零、浮点运算错误 →
SIGFPE(8) - 非法指令 →
SIGILL(4)
特点:同步信号,代码执行到错误指令就触发,信号总是发给当前出错进程。
3. 进程之间发送(系统调用产生)
进程调用系统调用,请求内核给另一个进程发信号
kill(pid, sig):最常用,可以发给进程 / 进程组;kill -信号 pid命令底层就是 kill 系统调用raise(sig):进程给自己发信号sigqueue():发送实时信号,支持携带附加数据(可靠信号)
4. 内核自身事件触发(内核主动产生)
内核检测到系统事件,主动投递信号给进程:
- 定时器到期:
alarm()设置闹钟,时间到内核发SIGALRM(14) - 子进程退出:内核给父进程发
SIGCHLD(17),通知父进程回收子进程资源 - 管道断开:管道读端关闭,写进程继续写,内核发
SIGPIPE(13) - 超过资源限制:进程超出 CPU / 文件大小限制,内核发送
SIGXCPU、SIGXFSZ
Core和Term
Linux 信号的默认处理动作中的两种:Term(Terminate)和Core(Core Dump),两者都会终止进程,区别在于是否生成 core 核心转储文件
Term
- 含义:单纯终止进程
- 行为:收到信号后直接杀掉进程,不产生 core 文件,不保存进程内存镜像
- 例子:
SIGINT(2 Ctrl+C)、SIGTERM(15)、SIGKILL(9),默认动作都是 Term - 场景:正常关闭程序,不需要崩溃调试信息
Core(Core Dump,核心转储)
- 含义:终止进程 + 生成 core 文件
- 行为:进程崩溃退出时,操作系统把进程当时整个用户空间内存、寄存器、调用栈保存到磁盘,生成
core或者core.pid文件,用来事后 gdb 调试定位崩溃原因抖音百科 - 触发信号:
SIGSEGV(11段错误)、SIGABRT(6)、SIGQUIT(3 Ctrl+\)、SIGFPE(8除零)等 - ⚠️前提:系统资源限制
ulimit -c不能是 0。默认云服务器ulimit -c 0,即使收到 Core 类型信号,也不会生成 core 文件
信号保存
Linux信号的三张表
block
位图,每一位对应一个信号编号
- bit=1:阻塞该信号,信号来了不能递达,留在 pending
- bit=0:不阻塞
作用:标记哪些信号被进程屏蔽了 操作函数:
sigprocmask()修改 block 表
pending
位图,bit 位对应信号编号
- bit=1:信号已经产生,但还没递达(没处理),处于 pending 未决
- bit=0:没有收到该信号
信号产生时,内核把 pending 对应位置 1;信号成功递达处理完,清零。 查看:
sigpending()获取 pending 集合 ⚠️普通信号 (1~31) 位图只有一位,多次收到同一个信号,只会置 1,信号丢失(不排队);实时信号用链表保存,可以排队。
handler
函数指针数组,下标 = 信号编号,数组元素是处理方式:
SIG_DFL:默认动作(Term / Core / Ignore / Stop)SIG_IGN:忽略信号- 自定义函数地址:用户注册的信号处理函数(
signal()/sigaction()写入)
signal/sigaction 本质就是修改 handler 表
核从内核态返回用户态的时候,遍历信号:
if (pending 对应位 == 1 && block 对应位 == 0) → 信号可以递达,去 handler 表找到处理动作执行 else → 不递达,保持 pending
一句话理解:pending 代表有没有收到信号;block 代表要不要拦住不让处理;handler 代表收到之后怎么办
重要区分:阻塞 vs 忽略
- 阻塞 block:信号不让递达,信号保留 pending,不处理(递达前拦截)
- 忽略 SIG_IGN:信号成功递达之后,选择什么都不做(递达之后动作)
例子:阻塞 SIGINT,发送 SIGINT,pending 置 1;解除阻塞,立刻递达。 忽略 SIGINT,发送 SIGINT,pending 置 1,递达后直接丢弃,不会执行任何函数。
三张表对应的系统调用
- block 表:
sigprocmask()— 设置 / 获取阻塞信号集 - pending 表:
sigpending()— 获取当前未决信号集(只能读,不能直接写) - handler 表:
signal()/sigaction()— 注册信号处理函数
完整流程串一遍(结合前面知识点)
- 信号产生,内核把 pending 对应 bit 置 1
- 内核准备从内核态返回用户态
- 遍历信号:检查 pending 是否为 1,同时 block 是否为 0
- 满足条件 → 递达,查找 handler 表
- SIG_DFL:执行默认动作(Term/Core)
- SIG_IGN:忽略
- 自定义 handler:回到用户态执行 handler 函数
- 处理完毕,pending 对应 bit 清零
信号捕捉
硬件中断
硬件中断是外设(硬件)主动发给 CPU 的电信号,通知 CPU:我这边有事,暂停当前任务,过来处理我;Linux 内核负责接收、分发、处理这个信号。
1. 从硬件层面(CPU + 外设)
- 外设(网卡、磁盘、键盘、定时器)硬件上产生事件,通过中断引脚向 CPU 发送电信号。
- CPU 收到中断信号:
- 暂停正在执行的程序(保存现场:寄存器、指令地址)
- 跳转到中断向量表,找到这个中断对应的处理入口
- 执行中断处理程序
- 处理完,恢复现场,回到原来被打断的代码继续运行
早期:8259A PIC 可编程中断控制器;现在 PC 用APIC(高级可编程中断控制器),支持多 CPU 分发中断。
2. 进入操作系统(Linux 内核)
CPU 捕获中断后,进入内核态,Linux 把中断处理分成上半部(top half)和下半部(bottom half),这是 Linux 中断最核心的设计:
上半部(中断上下文,快速执行)
- 硬件触发,不可睡眠,运行在中断上下文
- 任务:读取硬件状态、确认中断、把耗时工作丢给下半部,尽快退出
原因:中断期间要屏蔽同级中断,不能长时间占用
下半部(延迟处理,可以稍后调度)
用来处理上半部来不及做、耗时的工作,有多种实现:
- softirq 软中断(内核静态定义,高优先级,如网络收发、块设备)
- tasklet:基于 softirq 实现,简单,单个 tasklet 不能并行运行在多 CPU
- workqueue 工作队列:交给内核线程执行,可以睡眠,适合会阻塞的任务
注意:软中断不是硬件中断!软中断是内核软件机制,用来延迟硬件中断的后续工作。
3. 关键概念
- 中断号 IRQ:每个硬件中断分配一个编号,内核通过 IRQ 区分是哪个设备发来的中断。
- 中断上下文:执行中断处理函数时的环境,没有进程 task_struct,不能睡眠、不能调用会阻塞的函数。
/proc/interrupts:Linux 查看系统硬件中断统计(各个 IRQ 的中断次数、所属 CPU、设备名)
cat /proc/interrupts4. 完整流程(硬件 → Linux)
- 硬件设备产生事件 → 发出硬件中断电信号
- APIC/PIC 转发中断信号给 CPU
- CPU 收到中断,保存现场,进入内核,触发中断异常
- Linux 中断入口:识别 IRQ 号,调用该 IRQ 注册的上半部中断处理函数
- 上半部:简单处理硬件,触发下半部(softirq/tasklet/workqueue)
- 退出上半部,CPU 退出中断上下文;下半部在合适时机执行剩余工作
- 全部完成,CPU 恢复用户态 / 内核态被打断的任务
软中断
• ⽤⼾层怎么把系统调⽤号给操作系统?-寄存器(⽐如EAX)
• 操作系统怎么把返回值给⽤⼾?-寄存器或者⽤⼾传⼊的缓冲区地址
• 系统调⽤的过程,其实就是先int0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执 ⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法
• 系统调⽤号的本质:数组下标
1. 什么是软中断
软中断是内核实现的一种软件级别的中断机制,属于中断下半部机制。硬件中断是硬件触发;软中断是内核代码主动标记、软件触发。 目的:把硬件中断上半部里非紧急、耗时一点的工作剥离出来,延后执行,缩短硬件中断关闭的时间。
一句话区分:
- 硬件中断:外设→CPU,异步硬件事件,上半部
- softirq:内核软件标记,软件触发,属于中断下半部
2. 软中断的特点
- 运行在中断上下文❗ 重要:不是进程上下文,不能睡眠
- 可以在多个 CPU 上并行并发执行(同一个软中断类型可以在不同 CPU 同时跑)
- 静态定义:内核编译期就固定好软中断类型,不能动态新增(和 tasklet 不一样)
- 执行时机:硬件中断退出的时候,内核检查是否有待处理软中断,有就执行
Linux 预定义的软中断(linux/interrupt.h),常见几个:
HI_SOFTIRQ // 最高优先级,tasklet就基于这个 TIMER_SOFTIRQ // 定时器软中断(时钟中断下半部) NET_TX_SOFTIRQ // 网络发包 NET_RX_SOFTIRQ // 网络收包(网卡硬件中断只做简单ack,收包大逻辑在这个软中断) BLOCK_SOFTIRQ // 块设备IO一共就 10 种,固定写死在内核。
3. 软中断触发与执行流程
- 硬件中断上半部执行,快速做完硬件 ack,调用 raise_softirq () 标记某个软中断 pending
- 硬件中断上半部处理完毕,退出中断
- 内核在中断返回路径检测:有没有 pending 软中断?
- 有,则执行对应的 softirq 处理函数(中断上下文,关抢占,不能 sleep)
注意:软中断可以抢占进程上下文;软中断不能被硬件中断以外的东西抢占(硬件中断可以打断软中断)
补充:如果软中断执行太久,内核会把剩余工作丢给 ksoftirqd 内核线程,在进程上下文继续处理,避免长时间关抢占。
时钟中断
. 定义
时钟中断是一种硬件中断。主板上的硬件定时器(PIT / HPET / APIC Timer),到设定时间就向 CPU 发送硬件中断请求,CPU 收到中断,进入内核的中断上下文执行时钟中断处理函数。
内核参数
HZ:每秒触发多少次时钟中断。 例:HZ=100→ 每 10ms 一次时钟中断,一次中断叫一个tick。 tickless(CONFIG_NO_HZ)无滴答内核:不再周期性触发,按需触发,省电。
2. 核心作用(前面讲过,精简版)
- 抢占式进程调度(最重要)每次时钟中断,内核检查当前进程时间片是否耗尽。
- 时间片用完,就绪队列有其他进程 → 触发进程调度,切换进程
- 时间片还有剩余 → 继续运行当前进程
⚠️ 时钟中断不一定会进程切换,两个条件同时满足才切换。
- 更新 jiffies
jiffies是内核全局变量,每次时钟中断 jiffies++,内核所有计时、超时都基于它。 - 统计进程 CPU 耗时统计进程的用户态时间、内核态 CPU 时间,
top命令的 CPU 使用率来源。 - 处理内核定时器(TIMER_SOFTIRQ)检查内核定时器链表,执行到期定时器回调(sleep、TCP 超时、看门狗等)。
3. 完整执行流程
- CPU 正在运行用户态应用程序
- 硬件定时器到期,发出硬件中断信号
- CPU 保存寄存器现场,进入内核态、中断上下文
- 执行时钟中断上半部:
- 更新 jiffies
- 统计 CPU 时间
- 判断当前进程时间片是否耗尽,标记是否需要调度
- 标记 TIMER_SOFTIRQ 软中断 pending
- 硬件上半部执行完毕,退出硬件中断
- 内核发现 pending 软中断,执行 TIMER_SOFTIRQ(软中断,中断上下文),执行到期定时器回调
- 软中断处理结束,准备返回用户态
- 返回用户态前,内核检查进程未决信号 pending 信号,有信号就递送信号
- 如果前面标记了需要调度,则执行进程上下文切换,切换到其他进程;否则回到原用户进程继续运行
4. 上下文与关键限制
- 时钟中断上半部:中断上下文,不能睡眠
- TIMER_SOFTIRQ 软中断:依然是中断上下文,不能睡眠
5. 高频面试问题
Q1:时钟中断属于硬件中断还是软中断?
硬件中断。TIMER_SOFTIRQ 是它触发的软中断(下半部)。
Q2:HZ、tick、jiffies 分别是什么?
- HZ:每秒时钟中断次数,内核编译配置;
- tick:一次时钟中断;
- jiffies:全局计数器,每 tick 自增 1,记录系统启动以来的 tick 总数。
Q3:tickless 内核是什么?
关闭周期性 tick。当 CPU 空闲或者只有一个进程运行时,不再固定产生时钟中断,直到下一个定时器到期才触发中断,减少中断开销、省电,多用于嵌入式、移动端。
Q4:时钟中断和信号有关系吗?
时钟中断本身不会直接产生信号。 时钟中断处理完,内核在准备返回用户态的时候,才去检查 pending 信号,递送信号。
Q5:如果没有时钟中断会怎么样?
- 没有抢占,一个死循环进程永久占住 CPU,只能主动让出 CPU(协同式多任务);
- jiffies 不再增长,sleep、TCP 超时全部失效;
- 无法统计 CPU 占用。
6. 关联知识点串联
时钟中断 → 硬件中断 → 中断上半部 / 下半部 → softirq (TIMER_SOFTIRQ) → 进程调度 → 信号检查(返回用户态)
用户态和内核态
用户态
普通权限,只能访问用户空间内存,不能直接操作硬件,不能执行特权指令。我们写的应用程序(nginx、python、浏览器)默认跑在用户态。
用户空间(用户态)
- 每个进程独立拥有,进程之间互不可见
- 存放:程序代码、堆、栈、库(libc)
- 进程崩溃,只影响当前进程,不会直接挂掉整个系统
内核态
最高权限,可以访问全部物理内存、CPU 寄存器、硬件设备,执行所有指令。Linux 内核运行在这里。
- 内核空间(内核态)
- 所有进程共享同一份内核空间
- 存放:内核代码、页表、中断处理、驱动
- 内核代码一旦崩溃,大概率导致系统 panic 宕机
⚠️ 进程在用户态时,只能访问自己的用户空间;进入内核态后,才能访问内核空间。
用户态 → 内核态 的切换(系统调用)
用户程序不能直接操作硬件,想要读写磁盘、收发网络、申请内存,必须求助内核,这个动作叫系统调用(syscall)。
切换的三种触发方式
- 系统调用(主动):最常见。
open/read/write/malloc底层都会触发 syscall,主动陷入内核。 - 中断(硬件触发):键盘、网卡、硬盘等硬件向 CPU 发中断,CPU 切到内核态执行中断处理程序。
- 异常(CPU 出错):比如访问非法内存(缺页异常、段错误),CPU 自动陷入内核处理。