LinuxNotes

第 08 章:信号

zjc 于 2026-01-08 发布

这是《Linux 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 信号是 Linux 向进程传递异步事件的机制。优雅停机、配置重载、故障转储、脚本中断,都依赖信号。理解信号的发送、阻塞、默认行为和继承规则,才能设计可靠的服务生命周期。

8.1 信号是什么

kill / kernel / terminal
        |
        v
     signal
        |
        v
target process
        |
        +-- default action
        +-- ignore
        +-- custom handler

查看信号:

kill -l
man 7 signal

常用信号:

信号 默认行为 常见用途
SIGHUP 终止 终端断开;很多守护进程用它重载配置
SIGINT 终止 Ctrl+C
SIGQUIT 终止并 core 调试转储
SIGILL 终止并 core 非法指令
SIGABRT 终止并 core abort
SIGFPE 终止并 core 算术错误
SIGKILL 终止 强制杀死
SIGSEGV 终止并 core 非法内存访问
SIGPIPE 终止 写已关闭管道或 socket
SIGALRM 终止 定时器
SIGTERM 终止 优雅终止
SIGUSR1 终止 应用自定义
SIGUSR2 终止 应用自定义
SIGCHLD 忽略 子进程状态变化
SIGCONT 继续 恢复执行
SIGSTOP 暂停 强制暂停
SIGTSTP 暂停 Ctrl+Z
SIGWINCH 忽略 终端窗口变化

8.2 发送信号

按 PID:

kill <pid>
kill -TERM <pid>
kill -15 <pid>
kill -KILL <pid>

按名称或命令行:

pkill -TERM nginx
pkill -TERM -f 'java -jar order.jar'
killall -TERM nginx

给进程组发送:

kill -TERM -<pgid>

负号表示进程组 ID。使用前必须确认范围,避免误杀其他服务。

查看进程组:

ps -eo pid,pgid,sid,stat,cmd
ps -o pid,pgid,sid,cmd -p <pid>

8.3 默认行为与处理

进程可以选择:

1. 执行默认动作
2. 忽略信号
3. 捕获信号并执行处理函数

SIGKILL 和 SIGSTOP 不能被捕获、阻塞或忽略,这是内核保底能力。

C 示例:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

volatile sig_atomic_t running = 1;

void handle_term(int signo) {
    running = 0;
}

int main(void) {
    struct sigaction sa;
    sa.sa_handler = handle_term;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGTERM, &sa, NULL) != 0) {
        perror("sigaction");
        return 1;
    }

    while (running) {
        pause();
    }

    puts("graceful shutdown");
    return 0;
}

信号处理函数中应尽量避免调用非异步信号安全函数。复杂清理应设置标志,由主循环处理。

8.4 优雅停机

理想流程:

1. 负载均衡摘除实例
2. 停止接收新请求
3. 发送 SIGTERM
4. 等待存量请求完成
5. 关闭资源:连接、文件、锁、队列
6. 输出退出原因
7. 超时后由编排系统 SIGKILL

Nginx:

sudo nginx -s reload
sudo nginx -s quit

reload 通常重读配置并平滑拉起 worker,quit 通常等待请求结束后退出。具体行为以当前版本文档为准。

systemd 停止:

sudo systemctl stop app
sudo systemctl kill -s SIGTERM app

超时配置:

[Service]
TimeoutStopSec=30s
KillSignal=SIGTERM
KillMode=mixed

8.5 SIGPIPE 问题

进程向已关闭的管道或 socket 写数据时可能收到 SIGPIPE。很多网络服务会忽略 SIGPIPE,并在写调用处处理 EPIPE 错误。

处理原则:

  1. 不要让连接关闭直接杀死服务;
  2. 写失败要记录上下文;
  3. 关闭对应连接;
  4. 不掩盖真实 bug;
  5. 按语言和框架的推荐方式处理。

Java 中对已关闭 socket 的异常处理与 C 不同,应结合具体运行时分析。

8.6 Core Dump

Core dump 是进程异常退出时的内存镜像,用于定位崩溃。

查看限制:

ulimit -c
cat /proc/sys/kernel/core_pattern

临时开启:

ulimit -c unlimited

core 文件路径由 core_pattern 控制,可能由 systemd-coredump、apport 或 abrt 接管,发行版差异较大。

systemd:

coredumpctl list
coredumpctl info <pid>
coredumpctl gdb <pid>

生产注意:

  1. core 可能包含敏感数据;
  2. 大内存进程文件很大;
  3. 存储位置要有空间;
  4. 需要保留二进制和符号;
  5. 采集流程要受控。

8.7 信号与 Shell 脚本

捕获信号:

#!/usr/bin/env bash
set -euo pipefail

tmpfile=$(mktemp)
cleanup() {
  rm -f "$tmpfile"
}
trap cleanup EXIT INT TERM

echo data > "$tmpfile"
sleep 30

trap EXIT 会在脚本退出时执行。多个信号的处理顺序和脚本当前命令有关,退出前要保证清理逻辑幂等。

忽略 Ctrl+C:

trap '' INT

恢复默认:

trap - INT

关键任务不要随意忽略终止信号,应实现可中断的检查点。

8.8 信号常见问题

kill 没反应

ps -o pid,stat,cmd -p <pid>
cat /proc/<pid>/status | grep State
dmesg -T | tail -100

常见原因:

  1. 进程处于 D 状态,等待 IO 返回;
  2. 僵尸进程,等待父进程回收;
  3. PID 1 实现特殊;
  4. 命令拼写或目标错误;
  5. 权限不足;
  6. 内核或存储异常。

重载配置无效

nginx -t
sudo systemctl reload nginx
journalctl -u nginx -n 100

检查:

  1. 配置文件路径;
  2. 是否 reload 到主进程;
  3. 是否还有独立 worker 未退出;
  4. 配置是否实际命中当前 server 块;
  5. 是否需要重启而非 reload。

容器停止很慢

SIGTERM 未被应用处理
  -> 编排等待 termination grace period
     -> 超时后 SIGKILL

检查应用是否监听 SIGTERM,以及入口脚本是否会转发信号。

本章小结

信号是进程生命周期控制的基础。生产服务要正确处理 SIGTERM,实现摘流、停止接流、处理存量请求和释放资源。SIGKILL 是保底手段,不是日常操作。Core dump 有助于崩溃定位,但必须治理大小、安全和留存。

思考题

  1. 为什么 SIGKILL 不能被捕获?
  2. 设计一个 Java 服务的优雅停机流程。
  3. KillMode=control-groupmixed 有什么差异?
  4. 哪些信号适合用于重载配置?
  5. 脚本中如何保证临时文件被清理?