Linux系统(信号篇):信号的产生
本节重点
- 什么是信号
- 信号产生的5种情况
- 终端按键产生
- 系统命令产生
- 系统调用产生
- 硬件异常与软件条件
一、什么是信号
在 Linux 系统中,信号(Signal)是一种软件中断机制,用于在进程间传递异步事件通知。它类似于硬件中断,但由软件层面实现,可用于通知进程某个事件已经发生,进程可根据预设规则响应或忽略这些信号。
1.1 本质与作用
信号可在任何时刻发送给进程,无需进程主动查询,类似于 “紧急通知”。
每个信号都对应一个唯一编号(如SIGINT
对应编号 2),不同信号代表不同事件(如键盘中断、进程终止请求等)。在Linux系统中我们可以通过以下指令来查看不同的信号和与其对应的编号:
kill -l
在图中编号34以上的都叫做实时信号,这里我们不讨论实时信号。每个信号在什么情况下产生,默认的处理动作是什么我们可以在signal(7)中找到详细说明,下面是部分示例:
man 7 signal
Action列相关字段的解释:
动作 英文全称 中文含义 是否终止进程 能否被忽略 / 捕获 典型应用场景 示例信号 Core
Core Dump 终止并生成核心文件 ✅ 部分可 程序崩溃调试(如段错误、总线错误) SIGSEGV
(11)、SIGABRT
(6)Term
Terminate 终止进程 ✅ 部分可 正常终止进程(如用户主动请求) SIGTERM
(15)、SIGINT
(2)Ign
Ignore 忽略信号 ❌ ✅ 无关紧要的事件通知(如子进程结束) SIGCHLD
(17)、SIGHUP
(1)Cont
Continue 继续执行暂停的进程 ❌ ✅ 恢复被暂停的进程(如调试、后台任务管理) SIGCONT
(18)Stop
Stop Execution 暂停进程 ❌ SIGSTOP
不可调试、临时冻结进程(如 gdb
调试、资源限流)SIGSTOP
(19)、SIGTSTP
(20)
二、信号的产生
2.1 终端按键触发
2.1.1 原理与分类
用户在终端按下特定组合键时,终端驱动程序会向内核发送信号,内核再将信号传递给当前的前台进程,主要用于进程的交互式控制。
按键组合 | 发送信号 | 信号编号 | 核心作用 | 典型场景 |
---|---|---|---|---|
Ctrl + C | SIGINT | 2 | 终止前台进程(等价于中断请求) | 取消正在运行的命令(如 ping ) |
Ctrl + Z | SIGTSTP | 20 | 暂停前台进程(可后台恢复) | 临时暂停任务并放入后台队列 |
Ctrl + \ | SIGQUIT | 3 | 终止进程并生成核心转储 | 调试时获取进程崩溃现场 |
Ctrl + D | SIGEOF | - | 触发终端 EOF(非标准信号) | 结束交互式输入(如 cat 命令) |
Alt + PrintScreen + K | SIGKILL | 9 | 强制终止所有进程(安全机制) | 系统卡死时的紧急恢复(Magic SysRq) |
这里需要注意的是信号仅作用于前台进程,后台进程无法接收。
2.1.2 补充:前后台进程
在操作系统中,进程根据其与终端(如Shell)的交互方式分为前台进程和后台进程。两者的核心区别在于是否直接占用终端输入/输出(I/O)资源。
a> 前台进程
前台进程是当前与终端直接交互的进程,会独占终端的输入(如键盘输入)和输出(如屏幕显示)。用户必须等待其完成或主动终止后,才能继续在终端执行其他命令。
前台进程一般具有以下特点:
- 占用终端:用户无法在同一终端输入其他命令,直到进程结束。
- 信号处理:可通过终端按键(如
Ctrl+C
)发送信号(如SIGINT
)终止进程。
#include <iostream>
#include<unistd.h>
int main()
{while (1){std::cout << "hello world" << std::endl;sleep(1);}return 0;
}
我们的程序运行起来默认就是前台进程,此时该进程在被终止之前会占用终端无法执行其它命令
b> 后台进程
后台进程不直接占用终端,在后台独立运行,用户可以在同一终端继续输入其他命令。后台进程通常通过 &
符号或 bg
命令启动。
后台进程一般具有以下特点:
- 不占用终端:用户可以同时执行其他命令。
- 输出限制:默认情况下,后台进程的标准输出(stdout)和错误输出(stderr)仍会打印到终端(可能干扰用户输入),可通过重定向避免。
- 信号处理:无法直接通过终端按键终止(如
Ctrl+C
),需通过kill
命令或fg
切换到前台后终止。
c> 相关操作
将进程放到后台运行
command & # 直接在命令后加 &
Ctrl+Z # 暂停当前前台进程
bg # 将暂停的进程放到后台继续运行
查看后台进程:
jobs # 列出当前会话的后台作业
将后台进程切换到前台:
fg %1 # 将作业号为 1 的进程切换到前台
终止后台进程:
kill %1 # 终止作业号为 1 的进程
kill 1234 # 通过进程 ID 终止
2.2 系统命令触发
2.2.1 kill命令
kill
命令用于向指定进程发送信号,默认发送 SIGTERM
(信号编号 15)。
语法与参数:
kill [选项] [进程ID] # 最常用格式
kill [信号] [进程ID] # 指定信号类型
常用选项:
-s <信号名/编号>
:指定发送的信号(如-s SIGKILL
或-9
)-l
:列出所有可用信号及其编号-p
:仅打印进程 ID,不发送信号-a
:当与-g
配合时,作用于进程组内所有进程
使用场景:
按照进程名终止进程
# 查找并杀死名为"java"的进程
kill $(ps -ef | grep java | grep -v grep | awk '{print $2}')# 更简洁的方式(pgrep工具需提前安装)
kill $(pgrep java)
终止进程组中所有进程
# 杀死进程组ID为1234的所有进程
kill -g 1234# 结合pgrep查找进程组
kill -g $(pgrep -g nginx)
2.2.2 pkill命令
pkill
(process kill)是 Linux/Unix 系统中用于按条件终止进程的工具,相比kill
命令,它支持通过进程名、用户、终端等多种条件匹配进程,无需手动查询 PID,大幅提升批量管理效率。
语法与参数:
pkill [选项] [条件] # 按条件匹配并终止进程
pkill [信号选项] [条件] # 指定信号类型
选项类型:
-x
:精确匹配完整进程名(如pkill -x firefox
仅匹配进程名完全为 "firefox" 的进程)
-f
:匹配进程命令行全内容(包括参数,如pkill -f "java -jar app.jar"
)
-u <用户名/UID>
:终止指定用户的进程(如pkill -u admin
)
-U <非特权用户>
:终止非指定用户的进程
-g <进程组ID>
:终止指定进程组内的所有进程
-t <终端号>
:终止指定终端(如tty1
、pts/0
)启动的进程
-P <父进程ID>
:终止指定父进程的子进程
pkill
默认发送SIGTERM
(15)信号,可通过以下选项修改:
-s <信号名/编号>
:指定信号(如pkill -s SIGKILL chrome
)-9
:等价于-s SIGKILL
,强制终止进程-1
:等价于-s SIGHUP
,常用于重启进程
使用场景:
精确匹配进程名
# 终止所有名为"nginx"的进程(忽略参数)
pkill nginx# 仅终止进程名完全为"java"的进程(排除java程序启动的其他进程)
pkill -x java
按命令行参数匹配:
# 终止包含"python script.py"命令的进程
pkill -f "python script.py"# 终止所有使用80端口的Nginx进程(结合lsof)
pkill -f "$(lsof -i :80 | grep nginx | awk '{print $2}')"
按用户与终端筛选:
# 终止用户"test"启动的所有Firefox进程
pkill -u test firefox# 终止当前终端(pts/0)运行的所有进程
pkill -t pts/0
向进程组发送信号:
# 终止用户"test"启动的所有Firefox进程
pkill -u test firefox# 终止当前终端(pts/0)运行的所有进程
pkill -t pts/0
终止最早启动的进程:
pkill -o chrome # 终止最早启动的Chrome进程
终止最新启动的进程:
pkill -n firefox # 终止最新启动的Firefox进程
仅终止在指定时间内启动的进程:
pkill -T 300 chrome # 终止5分钟内启动的Chrome进程
2.2.3 killall命令
killall
是 Linux/Unix 系统中用于按进程名批量终止进程的命令,介于kill
(按 PID 终止)和pkill
(多条件匹配)之间,专注于通过进程名快速终止多个进程,语法简洁且执行效率高。
语法与参数:
killall [选项] 进程名 # 按进程名终止所有匹配进程
killall [信号选项] 进程名 # 指定信号类型
信号控制选项(默认发送 SIGTERM 信号)
-s <信号名/编号>
:指定信号(如killall -s SIGKILL chrome
)-9
:等价于-s SIGKILL
,强制终止进程-1
:等价于-s SIGHUP
,常用于重启进程(如killall -1 nginx
)
匹配模式选项
-e
:精确匹配进程名(严格区分大小写,如killall -e Firefox
仅匹配 "Firefox")-i
:忽略大小写匹配(如killall -i firefox
可匹配 "Firefox" 或 "firefox")-o
:仅终止最早启动的进程实例(oldest)-n
:仅终止最新启动的进程实例(newest)
用法与案例
精确匹配与模糊匹配:
# 模糊匹配:终止所有包含"java"的进程名(如java、javac)
killall java# 精确匹配:仅终止进程名完全为"java"的进程
killall -e java
指定型号组合操作:
# 先尝试优雅终止,30秒后强制终止
killall httpd
sleep 30
killall -9 httpd# 向所有sshd进程发送SIGHUP信号(重启服务)
killall -s SIGHUP sshd
按进程启动时间筛选:
# 终止最新启动的chrome进程
killall -n chrome# 终止最早启动的firefox进程
killall -o firefox
其他工具联动操作:
# 结合pgrep获取进程名后终止(支持正则表达式)
killall $(pgrep -l '^mysql.*' | awk '{print $2}')# 通过ps筛选特定用户的进程后终止
ps -u admin | grep 'tomcat' | awk '{killall -9 $NF}'
2.2.4 三者对比
工具 | 核心优势 | 匹配维度 | 适用场景 |
---|---|---|---|
kill | 精准控制单个进程 | PID | 已知 PID 时的精确操作 |
killall | 进程名批量终止,语法简洁 | 进程名(支持简单匹配) | 快速终止同名称进程群 |
pkill | 多维度条件匹配(名称 / 用户 / 终端等) | 名称 / 用户 / 时间等 | 复杂筛选场景(如按用户筛选) |
2.3 系统调用触发
通过编程语言接口主动发送信号,实现进程间通信或自我控制。
2.3.1 kill
kill
是 Unix/Linux 系统中用于向指定进程发送信号的系统调用,其核心功能是通过信号机制实现进程控制。以下是详细解析:
函数原型:
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
参数解析:
pid
:目标进程的标识符,支持以下取值:
pid > 0
:向指定 PID 的进程发送信号。pid = 0
:向当前进程所在进程组的所有进程发送信号。pid = -1
:向所有有权限发送信号的进程发送信号(除 init 进程外)。pid < -1
:向进程组 ID 为-pid
的所有进程发送信号。
sig
:要发送的信号编号或名称(如 SIGTERM
、SIGKILL
)。若 sig = 0
,则不发送信号,仅检查进程是否存在。
信号编号 | 信号名称 | 用途 |
---|---|---|
1 (SIGHUP ) | 终端断线 | 通知守护进程重新加载配置。 |
2 (SIGINT ) | 中断 | 模拟 Ctrl+C ,请求进程终止。 |
3 (SIGQUIT ) | 退出 | 模拟 Ctrl+\ ,请求进程退出并生成核心转储。 |
9 (SIGKILL ) | 强制终止 | 立即终止进程,不可捕获或忽略。 |
15 (SIGTERM ) | 终止 | 默认信号,请求进程优雅退出。 |
18 (SIGCONT ) | 继续 | 恢复被暂停的进程。 |
19 (SIGSTOP ) | 暂停 | 暂停进程,不可捕获或忽略。 |
返回值:
成功返回 0
,失败返回 -1
并设置 errno
(如 ESRCH
表示进程不存在,EPERM
表示无权限)。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid == 0) { // 子进程while (1) {printf("Child process running (PID: %d)\n", getpid());sleep(1);}} else if (pid > 0) { // 父进程sleep(5); // 等待5秒后终止子进程printf("Sending SIGTERM to child process (PID: %d)\n", pid);kill(pid, SIGTERM);} else {perror("fork failed");exit(1);}return 0;
}
2.3.2 raise
raise
是 Unix/Linux 系统中的一个系统调用函数(也属于 C 语言标准库函数),用于向调用进程自身发送信号,允许进程主动触发信号处理机制。
函数原型:
#include <signal.h>int raise(int sig);
参数解析:
sig
:要生成的信号的标识符,可以是任何有效的信号常量,例如 SIGINT
(代表 Ctrl+C
中断)、SIGTERM
(代表终止进程请求)等
返回值:
成功时返回 0
。
失败时返回非 0
值,并设置 errno
(如 EINVAL
表示无效的信号编号)。
代码示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>void sigint_handler(int sig) {printf("Received SIGINT signal!\n");
}int main() {// 注册 SIGINT 信号的处理函数if (signal(SIGINT, sigint_handler) == SIG_ERR) {perror("signal");return 1;}printf("Waiting for SIGINT signal...\n");sleep(5); // 等待 5 秒// 主动发送 SIGINT 信号给自己printf("Sending SIGINT to self...\n");if (raise(SIGINT) != 0) {perror("raise");return 1;}return 0;
}
2.3.3 alarm
alarm
是 Unix/Linux 系统中的一个系统调用函数,用于设置一个实时闹钟,在指定的时间到达时向当前进程发送 SIGALRM
信号终止进程。
alarm
函数允许进程设置一个定时器,当定时器到期时,系统会向该进程发送 SIGALRM
信号,常用于限制程序或操作的执行时间,防止程序长时间阻塞或无限等待。
函数原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数解析:
seconds
:指定定时器的时间,单位为秒。
返回值:
如果进程之前设置了闹钟且尚未超时,则返回之前闹钟的剩余时间(秒)。
如果之前没有设置闹钟,则返回 0
。
如果 seconds
为 0
,则取消当前闹钟,并返回之前闹钟的剩余时间(如果有)。
代码示例:
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
void handler(int sign)
{printf("收到了一个信号%d\n",sign);
}
int main()
{//首先将信号捕捉signal(SIGALRM,handler);//设置一个5秒的alarm闹钟,5秒后内核会给该进程发送SIGALRM信号//进程收到SIGALRM信号后会执行handler函数alarm(5);//暂停进程等待信号pause();return 0;
}
在这段代码中我们首先将SIGALRM信号进行捕捉(之后当进程收到SIGALRM后会转而执行我们的handler函数),之后我们设置一个5秒钟的alarm闹钟后暂停进程等待信号。5秒后进程收到SIGALRM信号时pause 会返回(解除暂停状态)并执行handler函数。因为之后我们没有后续代码执行,main函数会返回。
注意事项:
- 非累积性:每次调用
alarm
会覆盖之前的闹钟设置。例如,如果已经设置了一个 5 秒的闹钟,再次调用alarm(3)
会取消之前的 5 秒闹钟,并设置一个新的 3 秒闹钟。- 不阻塞进程:
alarm
调用会立即返回,不会阻塞进程的执行。进程可以继续执行其他任务,直到定时器到期并收到SIGALRM
信号。- 信号处理:当定时器到期时,系统会向进程发送
SIGALRM
信号。如果进程没有为该信号注册处理函数,默认动作是终止进程。通常,进程会注册一个信号处理函数来处理SIGALRM
信号,例如执行特定的操作或重新设置闹钟。
2.3.4 abort
abort
是 Unix/Linux 系统中的一个系统调用函数(通常通过标准库函数提供),用于强制终止当前进程,并生成一个核心转储文件(core dump,如果系统允许)。
函数原型:
#include <stdlib.h>
void abort(void);
参数:无。
返回值:无(函数不返回,直接终止进程)。
实现原理:
abort
首先向自身进程发送SIGABRT
信号(信号编号通常为6
)。- 如果进程未捕获
SIGABRT
,内核会终止进程并生成核心转储。 - 如果进程捕获了
SIGABRT
,但信号处理函数中未调用abort
或exit
,且未重新触发SIGABRT
,进程可能会继续执行(但通常不推荐这样做)。
代码示例:
当程序遇到不可恢复的错误(如内存分配失败、断言失败)时,调用 abort
终止进程。
#include <stdio.h>
#include <stdlib.h>int main() {FILE *file = fopen("nonexistent.txt", "r");if (file == NULL) {perror("Failed to open file");abort(); // 强制终止进程}// 其他代码...return 0;
}
abort与exit的区别:
特性 | abort | exit |
---|---|---|
终止方式 | 强制终止,可能生成核心转储 | 正常终止,不生成核心转储 |
信号触发 | 发送 SIGABRT 信号 | 不发送信号 |
清理行为 | 不执行 atexit 注册的函数 | 执行 atexit 注册的函数 |
返回值 | 无(直接终止) | 返回状态码给父进程 |
2.4 硬件异常触发
在 Linux 系统中,硬件异常(如 CPU 错误、内存访问故障等)会被内核捕获并转换为信号发送给相关进程,这类信号是系统底层故障处理的重要机制。
2.4.1 硬件异常对应的信号
信号名称 | 数值 | 硬件异常场景 | 典型触发原因 |
---|---|---|---|
SIGFPE | 8 | 浮点运算错误 | 除零、无效浮点操作(如 sqrt (-1)) |
SIGSEGV | 11 | 无效内存访问(段错误) | 访问空指针、越界内存、未分配内存 |
SIGBUS | 7 | 硬件总线错误 | 物理内存寻址错误(如未对齐的内存访问) |
SIGILL | 4 | 非法指令 | 执行 CPU 不支持的指令(如错误的机器码) |
SIGTRAP | 5 | 调试陷阱(由调试器触发) | 断点指令(如int 3 )或硬件调试异常 |
SIGXCPU | 24 | CPU 时间限制超出(由资源限制触发) | 进程占用 CPU 时间超过设定上限 |
SIGXFSZ | 25 | 文件大小限制超出 | 写入文件超过设定大小限制 |
2.4.2 底层识别与检测
在计算机系统中,硬件异常的识别依赖于 CPU 内部的硬件检测机制和寄存器状态的协同工作。CPU 在指令执行的各个阶段(取指、解码、执行、访存、写回)会实时检测异常条件:
- 取指阶段:检测指令地址是否有效(如越界访问)
- 解码阶段:验证指令格式合法性(如非法操作码)
- 执行阶段:检测算术运算错误(如除零、溢出)
- 访存阶段:验证内存访问权限(如写只读内存)
异常类型 | 检测时机 | 硬件检测逻辑 |
---|---|---|
非法指令(SIGILL) | 指令解码阶段 | 指令操作码不在 CPU 支持的指令集中(如 x86 架构检测到未定义的操作码) |
段错误(SIGSEGV) | 内存访问阶段 | 虚拟地址转换时发现页表项无效(如访问未映射的地址) |
总线错误(SIGBUS) | 物理内存寻址阶段 | 内存地址未对齐(如 32 位系统中访问 4 字节数据使用奇数地址)或硬件寻址失败 |
浮点异常(SIGFPE) | 浮点运算执行阶段 | 检测到除零、无效操作(如 sqrt (-1))或精度溢出 |
关键寄存器:
程序状态寄存器(EFLAGS/FLAGS)
- 作用:记录 CPU 当前状态和异常标志
- 关键标志位:
IF
(中断允许标志):控制外部中断的响应TF
(陷阱标志):单步执行模式(用于调试异常)AC
(对齐检查标志):检测未对齐内存访问(仅 Pentium 及以上)
程序计数器(EIP/RIP)
- 作用:存储下一条待执行指令的地址
- 异常时行为:异常发生时,当前 EIP/RIP 会被保存,异常处理程序的地址被载入以跳转执行
控制寄存器(CR0-CR8)
- CR2:存储导致页错误的线性地址(用于 SIGSEGV/SIGBUS)
- CR0:
PG
位(分页启用标志)控制虚拟地址转换,PE
位(保护模式标志)标识 CPU 运行模式 - CR4:
VME
位(虚拟 8086 模式扩展)、TS
位(任务切换标志)等与异常处理相关
错误码寄存器(仅部分异常)
- 作用:为某些异常提供额外错误信息
- 示例:
- 页错误时,错误码存入栈中,包含访问类型(读 / 写)和错误类型(页不存在、权限错误)
- 保护异常(如无效段选择子)的错误码包含段选择子信息
浮点状态寄存器(x87/FPU)
- 作用:记录浮点运算异常状态
- 关键字段:
- 异常标志位(如除以零、无效操作、溢出)
- 精度控制字段(影响浮点运算精度异常检测)
2.4.3 信号检测与响应流程
2.5 软件条件触发
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条 件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据 产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。
系统闹钟,其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这 样的技术。 现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:
// 定义位置:include/linux/timer.h
struct timer_list {/* 1. 内核内部使用的字段 */struct hlist_node entry; // 定时器链表节点unsigned long expires; // 到期时间(jiffies格式)struct tvec_base *base; // 指向所属的定时器基数树/* 2. 用户定义的回调函数和数据 */void (*function)(unsigned long); // 定时器到期时执行的回调函数unsigned long data; // 传递给回调函数的参数/* 3. 调试和状态字段 */int slack; // 定时器执行的松弛时间(纳秒)int start_pid; // 创建定时器的进程PID(调试用)void *start_site; // 创建位置(调试用)const char *start_comm; // 创建进程的名称(调试用)
};
这部分我们了解即可,我们也可以用系统调用alarm设置定时器通过信号捕捉来模拟这种定时器超时的现象:
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
#include<vector.h>int gcount = 0;
vector<func_t> gfuncs;
void handler(int sign)
{for(auto &f : gfuncs){f();}int n = alarm(1); // 重设闹钟,会返回上⼀次闹钟的剩余时间printf("剩余时间%d\n",n);
}
int main()
{//首先将信号捕捉signal(SIGALRM,handler);//设置一个1秒的alarm闹钟,5秒后内核会给该进程发送SIGALRM信号//进程收到SIGALRM信号后会执行handler函数alarm(1);//暂停进程等待信号pause();printf("我醒来了!\n");return 0;
}