Linux SIGTERM 信号
-
- [1. 什么是 SIGTERM](#1. 什么是 SIGTERM)
- [2. 信号编号与默认动作](#2. 信号编号与默认动作)
- [3. SIGTERM 与 SIGKILL、SIGINT 的区别](#3. SIGTERM 与 SIGKILL、SIGINT 的区别)
- [4. 如何发送 SIGTERM](#4. 如何发送 SIGTERM)
- [5. 在程序中捕获并优雅处理 SIGTERM](#5. 在程序中捕获并优雅处理 SIGTERM)
-
- [5.1 C 语言](#5.1 C 语言)
- [5.2 Python](#5.2 Python)
- [5.3 Go](#5.3 Go)
- [5.4 Node.js](#5.4 Node.js)
- [6. 优雅退出的实践清单](#6. 优雅退出的实践清单)
- [7. 交互式验证:以 sleep 为例tes 部署中的 SIGTERM](#7. 交互式验证:以 sleep 为例tes 部署中的 SIGTERM)
-
- [7.1 Docker:docker stop 的默认信号](#7.1 Docker:docker stop 的默认信号)
-
- [exec 在 Linux shell 中的作用](#exec 在 Linux shell 中的作用)
- [7.2 Kubernetes:Pod 删除的优雅终止流程](#7.2 Kubernetes:Pod 删除的优雅终止流程)
- [8. 总结](#8. 总结)
1. 什么是 SIGTERM
SIGTERM(Signal Terminate)是 Linux/Unix 系统中用于请求进程「正常终止」的信号,编号为 15 。它的名字由 Signal 与 Terminate 组合而来,含义直白:通知目标进程「请你结束运行」。
与强制杀死进程的信号不同,SIGTERM 是一种可被捕获、可被忽略 的礼貌性请求。进程收到 SIGTERM 后,有机会完成善后工作------例如保存数据、释放锁、关闭连接、记录日志------然后体面地退出。正因如此,SIGTERM 被视为系统管理员和运维人员「首选」的进程终止方式,也是 kill、docker stop、systemctl stop 等命令默认发出的信号。
2. 信号编号与默认动作
在 Linux 中,SIGTERM 的编号固定为 15。可以通过以下命令查证:
bash
kill -l
输出中可以看到:
text
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
...
SIGTERM 的默认动作是终止进程(Term)。如果进程没有显式捕获或忽略这个信号,内核会按照默认规则直接结束它。这也意味着:一个「什么都没处理」的普通进程,收到 SIGTERM 就会被干净地终止。
3. SIGTERM 与 SIGKILL、SIGINT 的区别
信号家族中,最常与 SIGTERM 混淆的是 SIGKILL(9)和 SIGINT(2),三者的差异非常关键:
| 信号 | 编号 | 能否捕获 | 常见来源 | 典型用途 |
|---|---|---|---|---|
| SIGINT | 2 | 可捕获 | 终端 Ctrl+C | 交互式打断当前前台进程 |
| SIGTERM | 15 | 可捕获 | kill 命令、关闭脚本 |
请求进程优雅终止(默认) |
| SIGKILL | 9 | 不可捕获 | kill -9 |
强制立即终止,不留给进程任何机会 |
理解要点:
- SIGTERM 是「请求」:进程可以拒绝、延迟或自定义处理。
- SIGKILL 是「命令」:由内核直接执行,进程完全无法感知、无法阻止。它只应在 SIGTERM 无效、进程僵死或资源无法释放时作为最后手段。
- SIGINT 面向交互 :Ctrl+C 发送的是 SIGINT,适合用户手动打断;SIGTERM 源自
kill等命令,适合脚本、服务管理和容器编排。
日常运维的最佳实践是:先 SIGTERM,再观察,最后才 SIGKILL。
4. 如何发送 SIGTERM
最常用的方式是 kill 命令。因为 SIGTERM 是 kill 的默认信号,下面两种写法完全等价:
bash
kill <PID>
kill -15 <PID>
kill -s TERM <PID>
kill -SIGTERM <PID>
也可以使用 pkill 按进程名发送:
bash
pkill -TERM nginx
此外,docker stop 和 systemctl stop 在停止容器/服务时,默认首先发送的正是 SIGTERM,并留出一段宽限期(grace period)等待进程优雅退出,超时后才会升级为 SIGKILL。这正是 SIGTERM 在容器时代依然重要的原因。
5. 在程序中捕获并优雅处理 SIGTERM
写服务端程序时,正确处理 SIGTERM 是生产环境的必备修养。下面以几种常见语言为例。
5.1 C 语言
C 语言使用 signal() 或 sigaction() 注册处理函数。推荐使用更可靠的 sigaction:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
volatile sig_atomic_t stop = 0;
void handle_sigterm(int signo) {
stop = 1; // 仅置位标志,不在信号处理函数中做重活
}
int main(void) {
struct sigaction sa;
sa.sa_handler = handle_sigterm;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
while (!stop) {
puts("working...");
sleep(1);
}
puts("收到 SIGTERM,开始清理并退出");
return EXIT_SUCCESS;
}
要点:信号处理函数中只做最轻量的操作(如设置标志位),真正的清理逻辑放在主循环里完成,避免重入问题。
5.2 Python
Python 通过 signal 模块注册处理函数。注意信号处理函数会在主线程执行,避免在其中做阻塞或复杂操作:
python
import signal
import time
should_exit = False
def handle_sigterm(signum, frame):
global should_exit
print("收到 SIGTERM,准备优雅退出")
should_exit = True
signal.signal(signal.SIGTERM, handle_sigterm)
while not should_exit:
print("working...")
time.sleep(1)
print("清理资源,退出")
5.3 Go
Go 通过 channel 接收信号,编写优雅关闭非常自然:
go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM)
go func() {
<-quit
fmt.Println("收到 SIGTERM,开始清理")
// 关闭连接、保存状态等
os.Exit(0)
}()
// 模拟持续运行
select {}
}
5.4 Node.js
Node.js 通过监听 process.on('SIGTERM', ...) 实现:
javascript
process.on('SIGTERM', () => {
console.log('收到 SIGTERM,优雅关闭中...');
server.close(() => {
console.log('HTTP 服务已停止监听');
process.exit(0);
});
// 兜底:10 秒后仍未退出则强制结束
setTimeout(() => process.exit(1), 10000).unref();
});
6. 优雅退出的实践清单
无论使用何种语言,处理 SIGTERM 的优雅退出逻辑通常包含以下环节,建议按顺序执行:
- 停止接收新请求:先让负载均衡摘除节点、HTTP 服务停止监听。
- 处理在途请求:等待已在处理中的请求完成,或设置合理的截止时间。
- 保存状态:落盘缓存、刷新日志、提交进行中的事务。
- 释放资源:关闭数据库连接、网络连接、文件句柄、释放分布式锁。
- 通知协作者:向注册中心注销服务、通知其他组件本实例即将下线。
- 退出进程 :完成清理后调用
exit,并设置合适的退出码。
同时要避免两个常见陷阱:
- 无限制等待:为退出流程设置超时(如 30 秒),超时后强制退出,防止「优雅退出」沦为「永远退出」。
- 在信号处理函数中做重活:信号处理函数应尽量只做标记,复杂清理放到主流程或专门的退出协程中。
7. 交互式验证:以 sleep 为例tes 部署中的 SIGTERM
在容器化与云原生环境里,SIGTERM 是应用优雅下线链路的第一环。docker stop 与 Kubernetes 的 Pod 删除流程,本质都遵循同一套模式:先发 SIGTERM 请求退出 → 等待宽限期 → 超时后再 SIGKILL 兜底。
7.1 Docker:docker stop 的默认信号
docker stop <container> 会先向容器内 PID 1 进程 发送 SIGTERM,并默认等待 10 秒;如果超时后容器仍未退出,Docker 才会发送 SIGKILL 强制结束。因此,只有容器的主进程真正捕获并处理 SIGTERM,优雅退出才可能发生。
可以使用 -t 参数调整宽限期:
bash
docker stop -t 30 <container>
还可以在 Dockerfile 中显式声明停止信号:
dockerfile
STOPSIGNAL SIGTERM
一个非常常见的坑是 shell 形式 vs exec 形式:
dockerfile
# ❌ shell 形式:SIGTERM 发给 /bin/sh,业务进程可能收不到
CMD ./server
# ✅ exec 形式:业务进程直接作为 PID 1,能够收到 SIGTERM
CMD ["./server"]
如果主进程是 shell,它未必会向业务进程转发信号,导致容器总是耗尽宽限期后被 SIGKILL 强杀。必要时也可以引入 tini 或 dumb-init 作为 PID 1,既负责信号转发,又负责回收子进程。
exec 在 Linux shell 中的作用
exec 是 shell 内建命令,它的核心作用是:用指定程序替换当前 shell 进程本身,而不是像普通命令那样先 fork 出一个子进程再执行 。执行 exec ./server 后,当前 shell 的 PID 保持不变,但进程映像已经被 ./server 覆盖;命令执行结束后也不会再"回到 shell",因为 shell 本身已经不存在了。
这种"进程替换"机制在容器场景中有两个直接意义:
- 减少中间进程层 :Dockerfile 中的 exec 形式
CMD ["./server"]让业务程序直接成为 PID 1,docker stop发出的 SIGTERM 能直接到达业务进程。 - 避免信号转发问题 :shell 形式
CMD ./server等价于/bin/sh -c "./server",此时 PID 1 是 shell,业务进程只是它的子进程。docker stop一般只向 PID 1 发送 SIGTERM,而 shell 未必会继续把信号转发给子进程,最终导致优雅退出失败。
因此,在 Docker、Kubernetes 或 systemd 这类"以主进程接收信号并完成清理"为假设的场景中,更推荐使用 exec 形式运行服务,让业务进程自己成为 PID 1、直接接收 SIGTERM。
一个必须注意的关键误区是:很多人以为 exec 只是"在脚本里运行一个程序,然后继续执行后面的代码"。这是错误的 。一旦 Shell 执行了 exec java ...,Shell 进程本身就会被 java 替换,原 Shell 直接消失,因此脚本中 exec 后面的所有代码都不会再执行。
bash
#!/bin/sh
exec java -jar app.jar
echo "这行永远不会执行"
上面脚本中,echo 这行永远没有机会运行:exec 已经把当前进程替换成 java,Java 启动后不会"返回"这个脚本继续往下走。这也是 exec 与普通命令最本质的区别------普通命令是 fork 子进程执行、父 Shell 继续往下走;而 exec 是原地替换,没有"之后"可继续。
7.2 Kubernetes:Pod 删除的优雅终止流程
当执行 kubectl delete pod、滚动更新或节点驱逐时,Kubernetes 按以下顺序终止 Pod:
- Pod 状态变为
Terminating。 - kubelet 向 Pod 内每个容器的主进程发送 SIGTERM。
- 进入
terminationGracePeriodSeconds宽限期,默认 30 秒。 - 如果容器在宽限期内仍未退出,kubelet 发送 SIGKILL 强制结束。
对应的 YAML 配置如下:
yaml
spec:
terminationGracePeriodSeconds: 30 # SIGTERM 到 SIGKILL 的宽限期
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
实践要点:
- 应用必须捕获 SIGTERM :否则 Pod 会一直悬在
Terminating状态,直到宽限期耗尽才被 SIGKILL 杀死。 - 配合
preStophook:在收到 SIGTERM 前先执行摘流量、等待负载均衡更新端点等操作,给注册中心或 Service 一点缓冲时间。 - 滚动更新时停止接收新流量:应用收到 SIGTERM 后应关闭 HTTP 监听、停止消费消息,但要继续处理已有请求。
- 合理设置宽限期 :让
terminationGracePeriodSeconds大于应用实际清理所需时间,避免还未来得及善后就被强杀。
理解 Docker 与 Kubernetes 的这层机制后,编写容器化服务时会更有意识地做到:以 PID 1 直面 SIGTERM,在限时内完成优雅关闭。
8. 总结
SIGTERM 是 Linux 进程管理中「文明终止」的基石:
- 它的编号是 15 ,默认动作是终止进程,但可被捕获处理。
- 它重在给进程一个体面收尾的机会,是
kill、docker stop、systemctl stop的默认信号。 - 正确处理 SIGTERM 是编写健壮服务、构建优雅关闭(graceful shutdown)机制的核心。
- 只有 SIGTERM 处理无效时,才应动用 SIGKILL 作为兜底。
掌握 SIGTERM,不只是记住一个信号的编号,更是理解进程生命周期、资源清理与生产可靠性之间的深刻联系。