news 2026/10/16 12:21:31

Linux进程的信号

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux进程的信号

概念

信号是内核发给进程的软件中断,用来通知进程发生了某个异步事件,是进程间最简单的 IPC。

信号命令和函数

kill -l :查看信号列表

killall :按进程名发送信号,给同名的进程发信号

# 先开一个sleep进程用来测试,新开终端执行 sleep 100 & sleep 100 & # 此时有2个sleep进程 # 发送默认信号SIGTERM(15),优雅终止sleep进程 killall sleep # -9 发送SIGKILL强制杀死同名全部进程 killall -9 sleep

pkill:支持模糊匹配进程名,还可以按 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 done

signal函数

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; }

核心特性(非常重要!面试常考)

  1. 默认行为:收到 SIGABRT,终止进程,并生成 core dump 核心转储文件,方便调试崩溃原因
  2. 虽然你可以用signal/sigaction捕获 SIGABRT,执行自定义处理函数✅ 但是!如果你的信号处理函数 return 返回了,abort 会再次发送 SIGABRT,强制杀死进程✅ 只有一种情况能阻止退出:信号处理函数内部不返回(longjmp 跳转出去)
  3. abort 会忽略 SIGABRT 的阻塞 / 忽略设置,就算你 sigprocmask 阻塞 SIGABRT,abort 依然生效The Open G...
  4. ❗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. 内核自身事件触发(内核主动产生)

内核检测到系统事件,主动投递信号给进程:

  1. 定时器到期:alarm()设置闹钟,时间到内核发SIGALRM(14)
  2. 子进程退出:内核给父进程发SIGCHLD(17),通知父进程回收子进程资源
  3. 管道断开:管道读端关闭,写进程继续写,内核发SIGPIPE(13)
  4. 超过资源限制:进程超出 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

函数指针数组,下标 = 信号编号,数组元素是处理方式:

  1. SIG_DFL:默认动作(Term / Core / Ignore / Stop)
  2. SIG_IGN:忽略信号
  3. 自定义函数地址:用户注册的信号处理函数(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,递达后直接丢弃,不会执行任何函数。

三张表对应的系统调用

  1. block 表:sigprocmask()— 设置 / 获取阻塞信号集
  2. pending 表:sigpending()— 获取当前未决信号集(只能读,不能直接写)
  3. handler 表:signal()/sigaction()— 注册信号处理函数

完整流程串一遍(结合前面知识点)

  1. 信号产生,内核把 pending 对应 bit 置 1
  2. 内核准备从内核态返回用户态
  3. 遍历信号:检查 pending 是否为 1,同时 block 是否为 0
  4. 满足条件 → 递达,查找 handler 表
    • SIG_DFL:执行默认动作(Term/Core)
    • SIG_IGN:忽略
    • 自定义 handler:回到用户态执行 handler 函数
  5. 处理完毕,pending 对应 bit 清零

信号捕捉

硬件中断

硬件中断是外设(硬件)主动发给 CPU 的电信号,通知 CPU:我这边有事,暂停当前任务,过来处理我;Linux 内核负责接收、分发、处理这个信号。

1. 从硬件层面(CPU + 外设)

  1. 外设(网卡、磁盘、键盘、定时器)硬件上产生事件,通过中断引脚向 CPU 发送电信号。
  2. CPU 收到中断信号:
    • 暂停正在执行的程序(保存现场:寄存器、指令地址)
    • 跳转到中断向量表,找到这个中断对应的处理入口
    • 执行中断处理程序
    • 处理完,恢复现场,回到原来被打断的代码继续运行

早期:8259A PIC 可编程中断控制器;现在 PC 用APIC(高级可编程中断控制器),支持多 CPU 分发中断。

2. 进入操作系统(Linux 内核)

CPU 捕获中断后,进入内核态,Linux 把中断处理分成上半部(top half)和下半部(bottom half),这是 Linux 中断最核心的设计:

上半部(中断上下文,快速执行)

  • 硬件触发,不可睡眠,运行在中断上下文
  • 任务:读取硬件状态、确认中断、把耗时工作丢给下半部,尽快退出

原因:中断期间要屏蔽同级中断,不能长时间占用

下半部(延迟处理,可以稍后调度)

用来处理上半部来不及做、耗时的工作,有多种实现:

  1. softirq 软中断(内核静态定义,高优先级,如网络收发、块设备)
  2. tasklet:基于 softirq 实现,简单,单个 tasklet 不能并行运行在多 CPU
  3. workqueue 工作队列:交给内核线程执行,可以睡眠,适合会阻塞的任务

注意:软中断不是硬件中断!软中断是内核软件机制,用来延迟硬件中断的后续工作。

3. 关键概念

  • 中断号 IRQ:每个硬件中断分配一个编号,内核通过 IRQ 区分是哪个设备发来的中断。
  • 中断上下文:执行中断处理函数时的环境,没有进程 task_struct,不能睡眠、不能调用会阻塞的函数。
  • /proc/interrupts:Linux 查看系统硬件中断统计(各个 IRQ 的中断次数、所属 CPU、设备名)
cat /proc/interrupts

4. 完整流程(硬件 → Linux)

  1. 硬件设备产生事件 → 发出硬件中断电信号
  2. APIC/PIC 转发中断信号给 CPU
  3. CPU 收到中断,保存现场,进入内核,触发中断异常
  4. Linux 中断入口:识别 IRQ 号,调用该 IRQ 注册的上半部中断处理函数
  5. 上半部:简单处理硬件,触发下半部(softirq/tasklet/workqueue)
  6. 退出上半部,CPU 退出中断上下文;下半部在合适时机执行剩余工作
  7. 全部完成,CPU 恢复用户态 / 内核态被打断的任务

软中断

• ⽤⼾层怎么把系统调⽤号给操作系统?-寄存器(⽐如EAX)

• 操作系统怎么把返回值给⽤⼾?-寄存器或者⽤⼾传⼊的缓冲区地址

• 系统调⽤的过程,其实就是先int0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执 ⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法

• 系统调⽤号的本质:数组下标

1. 什么是软中断

软中断是内核实现的一种软件级别的中断机制,属于中断下半部机制。硬件中断是硬件触发;软中断是内核代码主动标记、软件触发。 目的:把硬件中断上半部里非紧急、耗时一点的工作剥离出来,延后执行,缩短硬件中断关闭的时间。

一句话区分:

  • 硬件中断:外设→CPU,异步硬件事件,上半部
  • softirq:内核软件标记,软件触发,属于中断下半部

2. 软中断的特点

  1. 运行在中断上下文❗ 重要:不是进程上下文,不能睡眠
  2. 可以在多个 CPU 上并行并发执行(同一个软中断类型可以在不同 CPU 同时跑)
  3. 静态定义:内核编译期就固定好软中断类型,不能动态新增(和 tasklet 不一样)
  4. 执行时机:硬件中断退出的时候,内核检查是否有待处理软中断,有就执行

Linux 预定义的软中断(linux/interrupt.h),常见几个:

HI_SOFTIRQ // 最高优先级,tasklet就基于这个 TIMER_SOFTIRQ // 定时器软中断(时钟中断下半部) NET_TX_SOFTIRQ // 网络发包 NET_RX_SOFTIRQ // 网络收包(网卡硬件中断只做简单ack,收包大逻辑在这个软中断) BLOCK_SOFTIRQ // 块设备IO

一共就 10 种,固定写死在内核。

3. 软中断触发与执行流程

  1. 硬件中断上半部执行,快速做完硬件 ack,调用 raise_softirq () 标记某个软中断 pending
  2. 硬件中断上半部处理完毕,退出中断
  3. 内核在中断返回路径检测:有没有 pending 软中断?
  4. 有,则执行对应的 softirq 处理函数(中断上下文,关抢占,不能 sleep)

注意:软中断可以抢占进程上下文;软中断不能被硬件中断以外的东西抢占(硬件中断可以打断软中断)

补充:如果软中断执行太久,内核会把剩余工作丢给 ksoftirqd 内核线程,在进程上下文继续处理,避免长时间关抢占。

时钟中断

. 定义

时钟中断是一种硬件中断。主板上的硬件定时器(PIT / HPET / APIC Timer),到设定时间就向 CPU 发送硬件中断请求,CPU 收到中断,进入内核的中断上下文执行时钟中断处理函数。

内核参数HZ:每秒触发多少次时钟中断。 例:HZ=100→ 每 10ms 一次时钟中断,一次中断叫一个tick。 tickless(CONFIG_NO_HZ)无滴答内核:不再周期性触发,按需触发,省电。

2. 核心作用(前面讲过,精简版)

  1. 抢占式进程调度(最重要)每次时钟中断,内核检查当前进程时间片是否耗尽。
    • 时间片用完,就绪队列有其他进程 → 触发进程调度,切换进程
    • 时间片还有剩余 → 继续运行当前进程

    ⚠️ 时钟中断不一定会进程切换,两个条件同时满足才切换。

  2. 更新 jiffiesjiffies是内核全局变量,每次时钟中断 jiffies++,内核所有计时、超时都基于它。
  3. 统计进程 CPU 耗时统计进程的用户态时间、内核态 CPU 时间,top命令的 CPU 使用率来源。
  4. 处理内核定时器(TIMER_SOFTIRQ)检查内核定时器链表,执行到期定时器回调(sleep、TCP 超时、看门狗等)。

3. 完整执行流程

  1. CPU 正在运行用户态应用程序
  2. 硬件定时器到期,发出硬件中断信号
  3. CPU 保存寄存器现场,进入内核态、中断上下文
  4. 执行时钟中断上半部:
    • 更新 jiffies
    • 统计 CPU 时间
    • 判断当前进程时间片是否耗尽,标记是否需要调度
    • 标记 TIMER_SOFTIRQ 软中断 pending
  5. 硬件上半部执行完毕,退出硬件中断
  6. 内核发现 pending 软中断,执行 TIMER_SOFTIRQ(软中断,中断上下文),执行到期定时器回调
  7. 软中断处理结束,准备返回用户态
  8. 返回用户态前,内核检查进程未决信号 pending 信号,有信号就递送信号
  9. 如果前面标记了需要调度,则执行进程上下文切换,切换到其他进程;否则回到原用户进程继续运行

4. 上下文与关键限制

  • 时钟中断上半部:中断上下文,不能睡眠
  • TIMER_SOFTIRQ 软中断:依然是中断上下文,不能睡眠

5. 高频面试问题

Q1:时钟中断属于硬件中断还是软中断?

硬件中断。TIMER_SOFTIRQ 是它触发的软中断(下半部)。

Q2:HZ、tick、jiffies 分别是什么?

  • HZ:每秒时钟中断次数,内核编译配置;
  • tick:一次时钟中断;
  • jiffies:全局计数器,每 tick 自增 1,记录系统启动以来的 tick 总数。

Q3:tickless 内核是什么?

关闭周期性 tick。当 CPU 空闲或者只有一个进程运行时,不再固定产生时钟中断,直到下一个定时器到期才触发中断,减少中断开销、省电,多用于嵌入式、移动端。

Q4:时钟中断和信号有关系吗?

时钟中断本身不会直接产生信号。 时钟中断处理完,内核在准备返回用户态的时候,才去检查 pending 信号,递送信号。

Q5:如果没有时钟中断会怎么样?

  1. 没有抢占,一个死循环进程永久占住 CPU,只能主动让出 CPU(协同式多任务);
  2. jiffies 不再增长,sleep、TCP 超时全部失效;
  3. 无法统计 CPU 占用。

6. 关联知识点串联

时钟中断 → 硬件中断 → 中断上半部 / 下半部 → softirq (TIMER_SOFTIRQ) → 进程调度 → 信号检查(返回用户态)

用户态和内核态

用户态

普通权限,只能访问用户空间内存,不能直接操作硬件,不能执行特权指令。我们写的应用程序(nginx、python、浏览器)默认跑在用户态。

用户空间(用户态)

  • 每个进程独立拥有,进程之间互不可见
  • 存放:程序代码、堆、栈、库(libc)
  • 进程崩溃,只影响当前进程,不会直接挂掉整个系统

内核态

最高权限,可以访问全部物理内存、CPU 寄存器、硬件设备,执行所有指令。Linux 内核运行在这里。

  1. 内核空间(内核态)
    • 所有进程共享同一份内核空间
    • 存放:内核代码、页表、中断处理、驱动
    • 内核代码一旦崩溃,大概率导致系统 panic 宕机

⚠️ 进程在用户态时,只能访问自己的用户空间;进入内核态后,才能访问内核空间。

用户态 → 内核态 的切换(系统调用)

用户程序不能直接操作硬件,想要读写磁盘、收发网络、申请内存,必须求助内核,这个动作叫系统调用(syscall)。

切换的三种触发方式
  1. 系统调用(主动):最常见。open/read/write/malloc底层都会触发 syscall,主动陷入内核。
  2. 中断(硬件触发):键盘、网卡、硬盘等硬件向 CPU 发中断,CPU 切到内核态执行中断处理程序。
  3. 异常(CPU 出错):比如访问非法内存(缺页异常、段错误),CPU 自动陷入内核处理。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 12:20:13

AI智能体开发技术调研总结

近年来AI智能体技术发展非常之快&#xff0c;目前智能体已经大量应用于各个行业&#xff0c;与每个人息息相关&#xff1b;从去年刚开始的 function_calling、Prompt 到单智能体到最新的多智能体&#xff0c;从刚开始只能进行实现简单问答式、单一的调用外部工具、MCP、RAG 到 …

作者头像 李华
网站建设 2026/10/16 12:18:57

MATLAB调用STK卫星覆盖性能分析:COM接口联合仿真与避坑手册

简介&#xff1a;这份用户手册面向航天任务分析、遥感卫星轨道设计方向的学习者与工程人员&#xff0c;围绕MATLAB调用STK开展卫星覆盖性能分析这一主题&#xff0c;讲解如何借助STK的MATLAB双向接口完成仿真建模、数据处理与覆盖计算。内容涵盖仿真场景创建、中心天体选择、运…

作者头像 李华
网站建设 2026/10/16 12:18:25

MySQL表设计实战:从字段选型到索引优化的完整指南

在做MySQL开发的前几年&#xff0c;我一度觉得表设计就是写几条CREATE TABLE&#xff0c;字段够用就行。直到有一次接手一个线上报表系统&#xff0c;发现某张业务表有将近四十个字段&#xff0c;其中一半是冗余的统计列&#xff0c;另一半年月日时分秒拆成了六个字段&#xff…

作者头像 李华
网站建设 2026/10/16 12:16:06

智诺方AI|课程论文研究背景与引言,降重降AIGC分段优化不翻车

写好「研究背景与引言」&#xff0c;再谈降重与降 AIGC 先把研究内容写清楚&#xff0c;再处理重复率与 AI 痕迹&#xff0c;顺序不能反。研究背景与引言最容易写成「宏大叙事套话」。AI 生成引言往往从行业背景层层下铺&#xff0c;缺少与本选题绑定的问题意识&#xff0c;查重…

作者头像 李华
网站建设 2026/10/16 12:14:54

Schema、多语言继承、素材授权:三件被反复追问的技术细节

从深圳的大会回来&#xff0c;我把自家平台重新挑了一遍毛病身份说明&#xff1a;我是龙孚信息技术&#xff08;上海&#xff09;股份有限公司的售前助理。在深圳那场出海营销与 SEO 的行业大会上&#xff0c;我在交流区讲了两天的产品。回来之后&#xff0c;我把现场被问得最多…

作者头像 李华