Linux 信号权限与优雅停机:谁在杀进程,谁有权做什么
结论基线:信号语义依据 signal(7),退出码依据 POSIX 2.8.2("greater than 128",128+N 为 bash/ksh 约定),JVM 侧行为固定 OpenJDK
jdk-17.0.12+7源码摘录;权限依据 POSIX 文件权限与 capabilities(7)(查询均于 2026-10-25)。本机实测 macOS 26.7.1 + Oracle JDK 17.0.12+8(perm/signal模式);Linux 容器/Docker 行为未实跑,逐处标注推导。
问题:谁在杀你的进程,谁有权读你的文件
发布脚本 kill -9 后数据丢了、容器退出码 137 被当成 OOM、生产进程创建的文件权限和测试机不一样、Java 读配置报 AccessDeniedException——这四件事共享同一个根因:进程边界外的操作面(信号、权限)没有按契约理解。本页把"谁能杀、杀到什么程度、JVM 接住什么"与"谁能读、谁能写、谁能绑端口"讲清;进程/线程观测归进程线程页,容器内存 OOM 的两条失败路径归cgroup 正文。
信号终止与退出码:128+N、137 与 143
signal(7) 的表给三类默认动作:Term(终止)、Core(终止+转储)、Stop。三条关键事实:SIGKILL/SIGSTOP cannot be caught, blocked, or ignored(原文);SIGQUIT 的动作是 Core、注释 "Quit from keyboard";SIGTERM 是 "Termination signal"。进程被信号杀死时,父进程(shell/容器运行时)按 POSIX 报告"大于 128"的状态,bash/ksh 约定取 128+信号号:143=TERM、137=KILL、130=INT。退出码是编码不是证据:137 只说明"死于 SIGKILL",谁发的 KILL(人、OOM killer、编排器)要查 memory.events、审计日志与编排事件,见cgroup 正文。
| 动作 | 进程视角 | 数据/资源视角 |
|---|---|---|
kill -15(TERM) | 可被捕获,JVM 转入 shutdown | hook 跑完才退;有界等待是调用方责任 |
kill -9(KILL) | 不可捕获,内核直接移除 | 无 hook、无 dump、无 flush;锁文件/临时目录残留 |
kill -3(QUIT) | JVM 打线程 dump 继续运行 | 不是终止手段,是观测手段 |
| 容器 stop | 运行时先发 TERM,宽限期后 KILL | 宽限期 < hook 时长 = 等于 kill -9(推导,未实跑) |
本机实测(macOS + Oracle JDK 17.0.12+8,signal 模式四相位):TERM → hook 打印 HOOK-RAN/HOOK-DONE、退出码 143;KILL → 无 hook、退出 -9(编码 137);QUIT → stdout 出现 Full thread dump 且进程存活;-Xrs + TERM → 无 hook、退出 -15(红1/红2,见实验)。SIGQUIT 的机制在 HotSpot 源码里:
原码连续摘录(仅统一公共缩进,截取分支局部):os.cpp,jdk-17.0.12+7,L391–L400。
// Print stack traces
// Any SIGBREAK operations added here should make sure to flush
// the output stream (e.g. tty->flush()) after output. See 4803766.
// Each module also prints an extra carriage return after its output.
VM_PrintThreads op;
VMThread::execute(&op);
VM_PrintJNI jni_op;
VMThread::execute(&jni_op);
VM_FindDeadlocks op1(tty);
VMThread::execute(&op1);SIGBREAK 在 POSIX 平台定义为 SIGQUIT(同文件 L341):该分支只执行 VM_PrintThreads/VM_PrintJNI/VM_FindDeadlocks(线程 dump 与死锁检测),没有终止路径——实测"QUIT 后进程还在"与源码一致(红2)。
JVM 的信号处理:hook、SIGQUIT 转储与 -Xrs
JDK 在 Terminator 里给 HUP/INT/TERM 注册同一个处理器:
原码连续摘录(仅统一公共缩进,截取方法局部):Terminator.java,jdk-17.0.12+7,L47–L70。
static void setup() {
if (handler != null) return;
Signal.Handler sh = new Signal.Handler() {
public void handle(Signal sig) {
Shutdown.exit(sig.getNumber() + 0200);
}
};
handler = sh;
// When -Xrs is specified the user is responsible for
// ensuring that shutdown hooks are run by calling
// System.exit()
try {
Signal.handle(new Signal("HUP"), sh);
} catch (IllegalArgumentException e) {
}
try {
Signal.handle(new Signal("INT"), sh);
} catch (IllegalArgumentException e) {
}
try {
Signal.handle(new Signal("TERM"), sh);
} catch (IllegalArgumentException e) {
}
}两条可验证结论:退出码公式在源码里——sig.getNumber() + 0200(八进制 0200=128,即 128+N);-Xrs 时 handler 注册不到(Signal.handle 抛 IllegalArgumentException 被吞),JVM 不再接 TERM,hook 是否运行取决于调用方是否自己 System.exit——java(1) 手册的口径一致:"The JVM uses SIGHUP, SIGINT, and SIGTERM to initiate the running of shutdown hooks… When -Xrs is used, the signal masks for SIGINT, SIGTERM, SIGHUP, and SIGQUIT aren't changed by the JVM… SIGQUIT thread dumps aren't available. User code is responsible for causing shutdown hooks to run"(JDK 17 man,查询 2026-10-25)。实测相位 4 证实:-Xrs + TERM → 进程直接死于信号(-15),无 hook(红8)。
shutdown hook 的真实语义:顺序、异常与停止
Runtime.addShutdownHook 注册的是应用 hook,而 Shutdown.runHooks 的系统钩子是顺序执行:
原码连续摘录(仅统一公共缩进,截取方法局部):Shutdown.java,jdk-17.0.12+7,L113–L139。
private static void runHooks() {
synchronized (lock) {
/* Guard against the possibility of a daemon thread invoking exit
* after DestroyJavaVM initiates the shutdown sequence
*/
if (VM.isShutdown()) return;
}
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
synchronized (lock) {
// acquire the lock to make sure the hook registered during
// shutdown is visible here.
currentRunningHook = i;
hook = hooks[i];
}
if (hook != null) hook.run();
} catch (Throwable t) {
if (t instanceof ThreadDeath td) {
throw td;
}
}
}
// set shutdown state
VM.shutdown();三个工程边界:应用 hook 由其中一个系统槽统一"启动全部并等待完成",hook 之间没有先后保证(并发启动,先注册的登记进数组但完成顺序无契约);hook 抛出的异常被吞掉(catch Throwable 只放行 ThreadDeath),"hook 里报错就会重试/记录到日志"是错觉,必须自己在 hook 内 try-catch 落日志;hook 无限时等待——SIGTERM 不会打断卡住的 hook,超时后强杀的责任在容器运行时的宽限期(docker stop 推导,未实跑)。hook 里调用 System.exit 的行为由 Shutdown.exit 决定:
原码连续摘录(仅统一公共缩进,截取方法局部):Shutdown.java,jdk-17.0.12+7,L161–L176。
static void exit(int status) {
synchronized (lock) {
if (status != 0 && VM.isShutdown()) {
/* Halt immediately on nonzero status */
halt(status);
}
}
synchronized (Shutdown.class) {
/* Synchronize on the class object, causing any other thread
* that attempts to initiate shutdown to stall indefinitely
*/
beforeHalt();
runHooks();
halt(status);
}
}非零 status 且已在关机序列中 → 立即 halt,不再等剩余 hook——hook 里用非零 System.exit 会把兄弟 hook 拦腰截断。优雅停机协议因此是:TERM → 有界 drain(在途请求/连接池,见HTTP 池正文)→ hook 内只做释放与落盘,不做新工作 → 挂在 hook 里的慢任务由运行时宽限期兜底(预算=宽限期−drain 时间)。
文件权限、umask 与绑定端口:谁有权做什么
POSIX 权限三层检查:uid/gid 属主与 mode 位、父目录的 x 权限、挂载层(只读卷)与扩展属性;进程的 fsuid/fsgid 决定检查身份(credentials 语义按 credentials(7) 口径,查询 2026-10-25)。Java 侧 Files.createFile(attr) 显式请求的 mode 仍会被 umask 掩蔽:本机实测(umask 027)请求 rwxrwxrwx 得到 rwxr-x---(777 & ~027 = 750,红5);chmod 000 后连属主读文件都得到 AccessDeniedException(实测)。部署差异题的答案在这里:CI 与生产 umask 不同(022 vs 027/077),同一份"默认权限"代码产出不同文件模式——要稳定的组权限,显式 setPosixFilePermissions + 明确 umask,不能只靠请求值。
绑定低端口是另一类权限:Linux 传统上 <1024 需要 root 或 CAP_NET_BIND_SERVICE(capabilities(7),查询 2026-10-25;未在 Linux 实跑)。macOS 实测推翻了把它当跨平台规则:uid=501 bind(80) 成功(红1,macOS 10.14+ 允许非特权低端口绑定)。所以"非 root 起不来 80 端口"的修复(提权/加能力/换 8080+映射)只在 Linux 部署上成立,验证必须看目标平台的实测输出而不是本机。只读容器文件系统下写日志/临时目录失败(EROFS)同理属推导。
实验:信号与权限观测
公开实验入口:signal 模式四相位(TERM/KILL/QUIT/-Xrs)断言 hook 与退出码;perm 模式断言 umask 掩蔽与 000 拒读、把 bind80 作为平台观察记录;thread/fd/mem/disk 模式见进程线程页与内存文件磁盘页。信号与权限的 Linux 专属分支(OOMKilled、CAP_*、只读卷)标注推导未实跑。
架构师推演:停机窗口与权限预算(算例,非实测)
设生产服务:编排器宽限期 30s,drain 在途请求 P99 需要 8s,hook 落盘指标 2s,日志 flush 1s。预算 = 30 − 8 − 2 − 1 = 19s 余量;若某次发布把 hook 里加了"同步预热缓存"(18s),预算只剩 1s——P99 抖动一次就把服务顶过宽限期,接下来是 SIGKILL:数据落盘与否看运气。两条设计约束:hook 只做幂等释放(不做可延迟到下次启动的工作);退出路径单独演练(kill -TERM 演练 + 校验退出码 143 与日志完整性),不能只在 IDE 里点停止。权限侧:镜像内非 root 运行是默认假设,端口 >1024、数据目录属主、umask 显式化都要进部署清单;把"本机能跑"当验证等于没有验证(红1 的教训)。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
- signal(7)、credentials(7)、capabilities(7)(查询 2026-10-25);POSIX 2.8.2 退出状态 >128。
- JDK 17 java(1) 工具手册(查询 2026-10-25):-Xrs 与 shutdown hook、SIGQUIT dump 不可用的口径。
- OpenJDK
jdk-17.0.12+7:Terminator.java L47–L70、Shutdown.java L113–L139/L161–L176、os.cpp L391–L400(本页摘录);运行 JDK 为 Oracle 17.0.12+8,构建号不同分记。 - 本机实验:
labs/os-networksignal/perm 模式与证据记录;docker stop/OOMKilled/CAP_*/只读卷为文档推导,未实跑。