kill 命令向进程发送预定义
信号(如
SIGTERM),若需自定义信号处理逻辑,可用
signal() 或 `sigaction()
在 Linux 操作系统中,信号(Signal)是一种软中断机制,用于通知进程发生了特定事件(如用户输入、硬件异常或外部请求),它是进程间通信的核心手段之一,广泛应用于进程控制、调试和错误处理等场景,以下是关于“如何在 Linux 中创建信号”的完整指南,涵盖命令行操作、系统调用、编程实践及关键细节。
信号的基础概念与分类
1 什么是信号?
信号本质是操作系统内核向进程传递的异步通知,类似于硬件中断,每个信号对应唯一的整型编号(如 SIGINT=2, SIGKILL=9),并关联预定义的行为(终止进程、暂停执行等)。
| 信号名称 | 编号 | 默认动作 | 典型触发场景 |
|---|---|---|---|
| SIGHUP | 1 | 终止进程 | 终端挂断 |
| SIGINT | 2 | 终止进程 | Ctrl+C 组合键 |
| SIGQUIT | 3 | 核心转储+终止 | Ctrl+ 组合键 |
| SIGILL | 4 | 核心转储+终止 | 非规指令 |
| SIGFPE | 8 | 核心转储+终止 | 算术错误(除零溢出) |
| SIGKILL | 9 | 强制终止 | kill -9 命令 |
| SIGUSR1 | 10 | 忽略 | 用户自定义逻辑 |
| SIGUSR2 | 11 | 忽略 | 用户自定义逻辑 |
| SIGALRM | 14 | 终止进程 | alarm() 定时器到期 |
| SIGTERM | 15 | 终止进程 | kill 默认信号 |
| SIGBUS | 100+ | 核心转储+终止 | 总线错误 |
注:编号小于 32 的信号为标准信号,32~64 为实时信号(可通过
man 7 signal查看完整列表)。
2 信号的生命周期
- 生成:由键盘事件、系统调用或其它进程发起。
- 递送:内核将信号插入目标进程的信号队列。
- 处理:进程按以下顺序选择一种响应方式:
- 执行关联的处理函数(Handler);
- 忽略信号;
- 执行默认动作(如终止进程)。
创建与发送信号的方法
1 命令行工具:kill 命令
kill 命令并非仅用于终止进程,而是通用的信号发送工具,其语法为:
kill [选项] <进程ID> <信号编号>
常用场景示例:
- 向当前终端发送
SIGHUP(重启前台作业):kill -HUP $$ # $$ 表示当前Shell的PID
- 优雅终止后台进程(替代
Ctrl+C):kill -TERM $(pgrep myapp) # 根据进程名获取PID
- 强制杀死顽固进程:
kill -KILL 12345 # 直接终止PID为12345的进程
| 选项 | 含义 | 示例 |
|---|---|---|
-l |
列出所有信号名称 | kill -l → 显示编号与名称映射 |
-L |
列出所有信号编号 | kill -L |
-NAME |
按名称发送信号 | kill -SIGINT 1234 |
-sig |
同上 | kill -SIGQUIT 1234 |
-PIPE |
向同一进程组的所有成员发送 | kill -USR1 -PGRP |
注意:普通用户只能向自己拥有的进程发送非特权信号(如 SIGTERM),而 SIGKILL 可绕过此限制。
2 系统调用:kill() 与 raise()
在 C/C++ 程序中,可通过以下两种方式主动发送信号:
raise(int sig):向自身发送信号
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int signum) {
printf("Received signal %dn", signum);
}
int main() {
signal(SIGUSR1, handler); // 注册处理函数
raise(SIGUSR1); // 向自身发送信号
pause(); // 等待信号到来
return 0;
}
编译运行:gcc test.c -o test && ./test → 输出 “Received signal 10″。
kill(pid_t pid, int sig):向指定进程发送信号
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t child_pid = fork();
if (child_pid == 0) { // 子进程
while (1) {
printf("Child running... PID=%dn", getpid());
sleep(1);
}
} else { // 父进程
sleep(5); // 等待子进程启动
kill(child_pid, SIGTERM); // 发送终止信号
printf("Sent SIGTERM to child PID=%dn", child_pid);
}
return 0;
}
此示例演示了父子进程间的信号通信。
信号处理程序的开发
1 简单模型:signal() 函数
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void custom_handler(int signum) {
printf("Custom handler triggered by signal %dn", signum);
}
int main() {
if (signal(SIGTSTP, custom_handler) == SIG_ERR) { // STTY停止字符
perror("signal");
return 1;
}
while (1) pause(); // 挂起等待信号
return 0;
}
运行时按下 Ctrl+Z(生成 SIGTSTP),将触发自定义逻辑而非暂停进程。
2 可靠模型:sigaction() 结构体
相较于 signal(),sigaction() 提供更精细的控制能力:
#include <signal.h> #include <stdio.h> #include <string.h> #include <unistd.h> struct sigaction sa; memset(&sa, 0, sizeof(sa)); // 清零结构体 sa.sa_handler = handler; // 设置处理函数 sa.sa_flags = SA_RESTART; // 自动重启被中断的系统调用 sigemptyset(&sa.sa_mask); // 不屏蔽任何额外信号 sigaction(SIGHUP, &sa, NULL); // 安装新处理器
关键字段解析:
| 字段 | 作用 |
|—————-|—————————————-|
| sa_handler | 指向处理函数的指针 |
| sa_mask | 信号处理期间临时屏蔽的信号集合 |
| sa_flags | 控制标志(如 SA_RESTART, SA_NOCLDSTOP)|
| sa_sigaction | 替代 sa_handler,支持带参回调 |
优势对比:
| 特性 | signal() | sigaction() |
|——————–|———————|———————|
| 可靠性 | 低(易被覆盖) | 高(原子性操作) |
| 支持复杂行为 | | (如带参回调) |
| 可移植性 | 部分平台不一致 | POSIX标准 |
| 信号集管理 | 有限 | 完整 |
特殊场景与最佳实践
1 守护进程的信号管理
守护进程通常脱离终端运行,需显式处理以下信号:
SIGHUP:重新加载配置文件(常见于 Nginx/Apache);SIGUSR1/SIGUSR2:执行日志轮转或性能统计;SIGCHLD:回收子进程资源。
示例配置片段:
void hup_reload(int signum) {
printf("Reloading config files...n");
load_config(); // 重新读取配置文件
}
void init_daemon() {
struct sigaction sa;
sa.sa_handler = hup_reload;
sigaction(SIGHUP, &sa, NULL);
}
2 避免竞态条件
多线程环境中,若主线程正在修改共享数据时收到信号,可能导致数据不一致,解决方案:
- 在信号处理函数中仅设置标志位,主循环检查标志位后执行操作;
- 使用
pthread_sigmask()动态调整线程的信号掩码。
3 实时信号的使用
实时信号(编号≥32)允许自定义行为且不会互相干扰。
union sigval {
int sival_int; // 整数值
void sival_ptr; // 指针值
};
void realtime_handler(int signum, siginfo info, void ucontext) {
printf("Realtime signal %d received with value %dn", signum, info->si_value.sival_int);
}
int main() {
struct sigaction sa;
sa.sa_flags = SA_SIGINFO; // 启用附加信息传递
sa.sa_sigaction = realtime_handler;
sigaction(SIRTMIN+1, &sa, NULL); // 使用第一个实时信号
// 发送信号并附带数据
union sigval val;
val.sival_int = 42;
sigqueue(getpid(), SIRTMIN+1, val); // 发送带数据的实时信号
pause();
return 0;
}
常见错误与调试技巧
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 信号未被接收 | 目标进程已退出或不存在 | 检查PID有效性 |
| 处理函数未生效 | signal() 被后续调用覆盖 |
改用 sigaction() |
| 死锁(无限递归) | 处理函数内再次触发同名信号 | 在入口添加静态变量去重 |
| 多线程环境下崩溃 | 非异步信号安全函数被中断 | 标记为 volatile sigatomic_t |
SIGSEGV 频繁发生 |
野指针或栈溢出 | 使用 gdb 进行内存分析 |
调试工具推荐:
strace:跟踪进程的信号传递过程;gdb:单步调试信号处理函数;dmesg:查看内核日志中的信号相关记录。
相关问答FAQs
Q1: 我的程序收到了意外的 SIGSEGV,如何定位原因?
A: SIGSEGV(段错误)通常由非规内存访问引起,解决方法:
- 使用
gdb运行程序:gdb ./your_program→run→ 崩溃时输入bt查看调用栈; - 启用编译器的保护选项:
-fsanitize=address; - 检查数组越界、空指针解引用或堆栈溢出。
Q2: 为什么我的 SIGCHLD 处理函数没有被调用?
A: 可能原因及排查步骤:
- 子进程已自动回收:若子进程退出时父进程未阻塞,
waitpid()会立即回收它,导致SIGCHLD未被递送,可在父进程中添加wait(&status)确保同步; - 信号被忽略:默认情况下
SIGCHLD不会被忽略,但若曾显式调用过signal(SIGCHLD, SIG_IGN),需恢复默认处理; - 命名空间隔离:容器或 namespaces 可能限制了信号的传播,需检查宿主机与容器内的进程关系
