ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解Linux信号机制:从kill到sigaction的完整实践指南

深入理解Linux信号机制:从kill到sigaction的完整实践指南 1. 信号究竟是什么先聊聊我对Linux进程信号的理解。很多人学Linuxkill命令用过kill -9也用过但信号这套机制真正意味着什么往往是在线上事故里才彻底搞明白的。信号是操作系统给进程发送的“异步事件通知”它的核心特点是进程不需要主动去轮询什么信号来了内核会打断进程当前的执行流让它转去处理这个通知。这个“异步”值得多说两句。我们平时写的代码函数调用是同步的——你调read()它阻塞在那里等数据到了才返回。但信号不是这样它可能在进程执行任意一条指令的时候突然到来就像你在家正常吃饭门铃突然响了你得放下筷子去看看谁来了。这种不确定性是信号这套机制最强大也最危险的地方。从解决什么问题来看信号是Linux系统里进程管理和系统监控的基础设施。进程要退出、后台任务要终止、守护进程要重新加载配置、父子进程要同步状态底层都要靠信号来传递意图。适合谁来读这篇内容呢一类是做Linux运维的同学天天和kill、nohup、systemd打交道另一类是写服务端程序的开发无论是C/C、Go还是Python只要服务跑在Linux上理解信号就能避免很多线上诡异的Bug。信号还有一个容易忽略的身份它是进程间通信IPC的一种。在Linux的IPC家族里有名管道、共享内存、消息队列、Socket但信号是最轻量、最原始的一个。它传递的信息量很小小到只有一个编号但它能完成的事情却不可替代——因为它是内核直接介入的、能打断进程执行的通知机制。很多系统调用如pause()、sigsuspend()、sleep()底层都依赖信号来唤醒进程。2. 信号的完整生命周期从产生到处理2.1 信号从哪里来三种产生方式信号不是凭空出现的它有明确的源头。最常见的三个来源第一种是终端产生的信号。你在终端里按CtrlC内核的终端驱动会向前台进程组发送SIGINT2号信号按Ctrl\发送SIGQUIT3号信号按CtrlZ发送SIGTSTP20号信号这个会把进程挂起。终端信号只发给前台进程组后台进程收不到——这是个关键特性后面讲后台任务时会用到。第二种是硬件异常产生的信号。比如进程执行了除零操作CPU会触发异常内核把这个异常转换成SIGFPE8号信号发给进程访问了非法内存地址MMU触发缺页异常且无法修复内核发SIGSEGV11号信号也就是我们常说的段错误执行了非法指令发SIGILL4号信号。这类信号的特点是“同步”的——它们总是在同一条指令上重复触发所以进程崩溃时的核心转储文件能精确指出是哪一行代码出了问题。第三种是软件条件触发的信号。这里包括kill()系统调用、raise()、alarm()、setitimer()等也包括内核内部状态变化触发的信号。比如子进程退出时内核自动给父进程发SIGCHLD17号信号进程写管道但读端已经关闭时内核发SIGPIPE13号信号定时器到期发SIGALRM14号信号。2.2 信号在内核里的旅程从发送到递送一个信号的完整旅程可以拆成三个阶段信号生成Generation、信号挂起Pending、信号递送Delivery。发送信号只是告诉内核“我想给某个进程发一个特定信号”这个操作对应kill()系统调用。但信号不是立即到达进程的它先被挂到目标进程的pending队列上。为什么要挂起而不是直接递送因为中断进程执行流是个有代价的操作内核需要等一个安全点——通常是进程从内核态回到用户态的时候或者进程当前指令可以被打断的时候。这里有个很重要的细节标准信号不排队。也就是说如果有100个SIGUSR1信号同时发给一个进程这个进程的 pending 位图里只会置一个位内核只会递送一次。如果你需要“一个都不能丢”的可靠信号那就得用实时信号SIGRTMIN到SIGRTMAX34到64号信号实时信号是排队的并且按编号从小到大递送。信号递送的时候进程对信号的处理方式有三种默认动作、忽略、捕获。默认动作又分好几种终止进程、终止并生成核心转储、停止进程、继续运行。比如SIGINT的默认动作是终止进程SIGCHLD的默认动作是忽略SIGSTOP的默认动作是停止进程SIGCONT的默认动作是继续运行。SIGKILL和SIGSTOP这两个信号很特殊它们既不能被捕获也不能被忽略因为内核必须保留“强制终止”和“强制停止”的最后一手底牌。2.3 信号系统调用全家福信号相关的系统调用就那几个但每个都有自己不可替代的位置kill(pid, sig)是最基本的发送接口pid 参数还能取特殊值0表示发给同进程组的所有进程-1表示发给所有有权限发送的进程排除 init负值表示发给指定的进程组。raise(sig)是把自己发给自己的快捷方式在单线程程序里等于kill(getpid(), sig)。alarm(seconds)和setitimer()是定时器接口内核在一段时间后给进程发SIGALRM。alarm()只支持秒级精度而且是“一次性”的setitimer()支持ITIMER_REAL真实时间发SIGALRM、ITIMER_VIRTUAL进程用户态CPU时间发SIGVTALRM、ITIMER_PROF用户态加内核态CPU时间发SIGPROF三种模式而且可以设定周期触发。性能监控里常用ITIMER_PROF因为它统计的是进程消耗CPU的总时间。sigaction()和signal()是安装处理器handler的接口。signal()是简化版跨平台行为有差异在Linux上不同的libc实现行为还不完全一致sigaction()是 POSIX 标准推荐的方式功能更细后面讲信号处理时会重点说。sigprocmask()是阻塞信号的接口它允许进程告诉内核“这些信号暂时别递送给我”但这些信号仍然会变成 pending 状态挂在队列里等解除阻塞后统一递送。注意阻塞和忽略是两码事忽略是“递送了但我什么也不做”阻塞是“压根别递送给我”。sigsuspend()则是原子地“临时换掉信号掩码 等待信号 恢复原掩码”用于解决“检查条件”和“等待信号”之间的竞态。3. 命令行实操kill 与信号控制3.1 kill 命令的正确写法与信号编号选择kill命令的价值被严重低估了。很多人只会kill -9 PID但生产环境里动不动就kill -9是运维大忌。kill命令不带信号编号时默认发SIGTERM15号信号这是“礼貌地请求终止”。进程收到SIGTERM后可以做清理工作关闭文件描述符、释放锁、通知下游服务下线、保存状态然后自己退出。一个设计良好的守护进程收到SIGTERM后能优雅地完成所有善后工作。kill -9发的是SIGKILL这个信号内核直接强制终止进程进程没有机会做任何清理。文件可能是半写的状态锁没释放共享内存没分离端口没关闭——进程没了但这些“遗物”会留在系统里。所以正确做法是先kill PID发SIGTERM观察5到10秒如果进程还没退再考虑kill -9兜底。kill -l可以列出所有信号编号和名字的对应关系。这里有几个运维高频信号要记住1号SIGHUP终端挂断但常被守护进程用作“重新加载配置”2号SIGINT终端中断对应CtrlC3号SIGQUIT终端退出会生成核心转储9号SIGKILL15号SIGTERM。还有几个信号要警惕13号SIGPIPE写已关闭的管道或Socket默认动作是终止进程——这个坑后面细说。3.2 后台任务与进程组nohup、、setsid 的信号语义说到后台任务就绕不开进程组和会话的概念。每个终端登录后shell 会创建一个新会话和一个前台进程组。你在终端里跑python server.py这个进程属于前台进程组因此能收到CtrlC发出的SIGINT。如果你在命令后面加这个进程就进入后台进程组不再接收终端键盘产生的信号。但有个隐患终端关闭时内核会给会话首进程通常是shell发SIGHUPshell 退出时会把SIGHUP转发给它的子进程。所以用启动的后台任务在终端关闭时还是会收到SIGHUP并终止。这就是nohup存在的原因nohup command 会忽略SIGHUP同时把标准输出重定向到nohup.out文件如果没手动重定向的话。setsid更进一步它让进程完全脱离当前的会话成为新会话的首进程。脱离会话的进程和终端彻底无关不会再收到终端关闭带来的信号。这也是为什么很多服务启动脚本里会看到setsid或daemon()调用的原因。更现代的替代方案是systemd的服务文件它天然把服务进程放入独立的 control group 和 session 中由 systemd 负责信号管理。还有个和信号紧密相关的坑管道和SIGPIPE。用管道连接两个命令时比如yes | head -n 1yes输出到管道head取到一行就退出了pipe 的读端关闭。yes再往下写内核检测到写管道但无读者发SIGPIPE给yes默认动作是终止进程——所以yes不会永远跑下去。反过来写网络服务程序时如果 Socket 对端已经关闭send()会触发SIGPIPE并终止整个服务进程这就是很多网络服务莫名崩溃的常见原因。生产级服务的解决方式一般是设置MSG_NOSIGNAL标志位或者捕获并忽略SIGPIPE让send()返回EPIPE错误而不是让进程直接挂掉。3.3 等待子进程退出wait 与信号的关系排查进程问题时经常需要关注子进程的退出方式。子进程先于父进程退出时会变成僵尸进程zombie等父进程调用wait()/waitpid()来回收它的退出状态码。如果父进程一直不调用僵尸进程就一直占着进程表项——进程表项是有限资源僵尸进程堆积多了会导致系统无法创建新进程。SIGCHLD信号在这里是关键。子进程状态发生变化退出、停止、被恢复运行时内核自动给父进程发送SIGCHLD信号通知父进程“你可以过来收尸了”。父进程如果在信号处理函数里调waitpid()就能及时回收子进程。这里有个经典坑如果在SIGCHLD的处理函数里只调了一次waitpid()而同一时刻有多个子进程退出SIGCHLD是不排队的标准信号父进程只会收到一次通知剩下的子进程就变成僵尸了。所以标准写法是循环调用waitpid(-1, status, WNOHANG)直到返回0或-1。一个真实的例子某家公司的后台任务调度系统父进程负责拉起一批子进程执行任务。上线初期发现系统每隔几天就出现“无法创建新线程”的错误排查后发现是僵尸进程积累了数万个。原因是调度框架的SIGCHLD处理器里只调了一次waitpid()同时有多个子进程退出时就漏回收了。改成while (waitpid(-1, status, WNOHANG) 0) {}的循环写法之后问题彻底消失。这个坑在CPU密集型、子进程并发量大的系统里特别容易踩。4. 信号处理编程从 signal 到 sigaction4.1 为什么不推荐 signal() 而推荐 sigaction()写C语言的信号处理程序网上很多教程还停留在signal(SIGINT, handler)的写法。这个接口确实简洁但它的行为在历史上是“变化”的早期Unix系统里signal()安装的处理器在触发一次之后会被重置为默认动作下一次信号到来时就不再调用你的处理器了。要在处理器里再次调signal()重新安装才能维持捕获。不同系统的signal()实现有差异有的重置有的不重置这给跨平台代码埋了坑。sigaction()则是一个行为明确、功能完备的接口。它接受一个struct sigaction结构体里面可以指定信号处理函数、信号屏蔽字、以及各种标志位#include signal.h void handler(int sig) { // 注意这里只能调用异步信号安全async-signal-safe的函数 } int main() { struct sigaction sa; sa.sa_handler handler; sigemptyset(sa.sa_mask); sa.sa_flags 0; // 或者 SA_RESTART if (sigaction(SIGINT, sa, NULL) -1) { perror(sigaction); return 1; } return 0; }这里逐个解释sa_flags里常用标志SA_RESTART让被信号打断的系统调用自动重启比如read()正在阻塞等输入时来了信号设置这个标志后read()不会被EINTR错误打断而是自动重新进入阻塞等待SA_NOCLDSTOP表示只在子进程退出时发SIGCHLD子进程被SIGSTOP/SIGTSTP暂停时不要通知父进程SA_SIGINFO让处理函数接收更多信息此时处理函数是sa_sigaction风格的三个参数版本能拿到发送者的PID和UID、信号编号等SA_NODEFER使得处理函数执行期间不自动阻塞当前信号一般不建议设因为同一个信号在处理器执行期间再次到来时如果没阻塞就可能递归进入新的处理器栈溢出风险很大。sigaction()还有一个实用能力它在替换旧处理器的时候可以把旧的处理器配置返回出来这样你在临时改处理器后可以恢复原状。signal()做不到这一点它拿不到旧的处理函数地址。4.2 信号处理函数里的“禁区”异步信号安全函数清单这是信号编程里最重要、最容易犯错的地方。信号处理函数是在进程执行流的“任意位置”被插入执行的它执行时可能正处在malloc()内部、可能在printf()内部、可能在持着某个锁。如果处理函数里又调用了malloc()、printf()、fgets()、pthread_mutex_lock()这些非可重入函数就可能出问题正在执行malloc()时被信号打断信号处理函数里又调malloc()而malloc()的内部链表正处于半更新状态二次进入就会破坏堆结构轻则内存损坏重则死锁。正在执行printf()时信号到来处理函数里又写标准输出两个输出可能交叉混杂。正在持锁时信号到来处理函数里尝试拿同一把锁直接死锁进程永久卡住。POSIX 标准规定异步信号处理函数里只能调用“异步信号安全”的函数。这个清单包括write()、read()对已打开的文件描述符、open()在某些受限条件下、close()、_exit()、sigaction()、sigprocmask()、waitpid()、kill()等。清单里最值得注意的没有printf()、没有malloc()、没有free()、没有互斥锁操作。信号处理函数里想打日志正确方式是write(STDERR_FILENO, msg, len)不是printf()。处理函数里需要记录信息的话建议用一个volatile sig_atomic_t类型的标志位。sig_atomic_t是整型类型读写是原子的不会被信号打断成半更新状态。C标准保证即使信号在读写中间到来读到值也是完整有效的。这是信号处理函数和主程序之间最简单可靠的通信方式static volatile sig_atomic_t g_flag 0; void handler(int sig) { g_flag 1; } int main() { struct sigaction sa {0}; sa.sa_handler handler; sigaction(SIGUSR1, sa, NULL); while (1) { if (g_flag) { // 主程序里做实际的清理工作 g_flag 0; } // 继续正常工作 } }这个模式背后的哲学是信号处理函数里“只标记不做事”。真正复杂的工作释放资源、写日志、关闭连接全部推迟到主程序的主循环里完成。这样既避开了可重入问题又让主程序逻辑保持可控。4.3 信号阻塞与未决sigprocmask 在竞态控制中的价值阻塞信号的操作在程序里不常用但解决竞态问题时离不开它。sigprocmask()的工作逻辑是传入SIG_BLOCK时把新信号集加到当前阻塞集合上SIG_UNBLOCK时从当前阻塞集合里移除SIG_SETMASK是整体替换。经典的应用场景是“先设置再等待避免竞态”。进程需要阻塞SIGUSR1然后设置某种条件判断再解除阻塞等待信号。如果中间不阻塞信号可能在条件判断之前就来了等真正pause()等待时信号已经过去了进程永远等不到这就是丢失信号的问题。sigprocmask()阻塞 sigsuspend()原子等待的组合能解决这个问题sigsuspend()的精妙之处在于它原子地做三件事——把进程的信号掩码替换为参数指定的新掩码、挂起进程等待信号、信号到来并处理完后恢复原来的掩码。这个“原子性”保证了不会出现“信号刚被解除阻塞但进程还没开始等待”的时间窗口。sigset_t set, oldset; sigemptyset(set); sigaddset(set, SIGINT); sigprocmask(SIG_BLOCK, set, oldset); // 先阻塞 // 检查条件做一些准备... sigemptyset(set); sigsuspend(set); // 原子解除所有信号的阻塞并等待任意信号到来 sigprocmask(SIG_SETMASK, oldset, NULL); // 恢复这个模式在主从同步、事件通知、进程间的简单握手协议里很常见。刚接触时可能会问sigprocmask()阻塞完后解除阻塞时信号就来了然后sigsuspend()就收不到了实际上sigsuspend()在“解除阻塞”的同一瞬间开始等待内核递送信号的动作被合并到了这个原子的等待过程中信号会被sigsuspend()捕获并唤醒进程。这就是它存在的意义。5. 信号与系统管理的实战组合拳5.1 会话、控制终端与 SIGHUP 的传播关系运维和进程打交道多了迟早会遇到“为什么我的服务进程退了”的疑问其中相当一部分答案都指向SIGHUP。这个信号的触发条件是“控制终端挂断”但它实际传播的路径有微妙之处。当SSH连接断开时内核会给会话首进程通常是 bash 或你启动服务的外层命令发SIGHUP。如果会话首进程不去忽略它它会在退出前把SIGHUP继续转发给当前会话里的子进程确保整个会话的进程都被清理掉。这也是为什么直接在一个SSH会话里跑python app.py断开连接服务就挂。nohup的原理就是让进程忽略SIGHUP断开连接时进程不被这个信号影响可以继续在后台运行。但在 systemd 时代服务管理方式变了systemd 把服务进程放进独立的control group不继承启动者的会话因此终端断开不会影响 systemd 管理的服务。这里就引出kill -HUP pid在服务管理里的特殊含义很多守护进程把SIGHUP当作“重新加载配置文件”的指令。比如nginx -s reload在功能上等价于给 master 进程发SIGHUPsystemctl reload sshd实际就是给 sshd 进程发SIGHUP让它重新读取配置而不是重启。理解了这层语义遇到“改了配置想不重启生效”的需求时优先查目标服务的文档看是否支持SIGHUPreload就比直接kill -9重启文明得多。5.2 僵尸进程与 SIGCHLD 的回收机制僵尸进程和SIGCHLD是一对经典组合排查系统进程状态时一定会碰到。一个进程变成僵尸状态显示为Z本身不可怕可怕的是僵尸堆积。前面提到过标准信号不排队多个子进程同时退出时父进程可能在SIGCHLD处理函数里通过waitpid(-1, WNOHANG)循环回收。这里再补充两个实际细节。第一个细节是waitpid(-1, status, WNOHANG)的返回值语义返回值大于0表示回收了一个子进程并拿到状态返回0表示有子进程活着但没有状态变化返回-1且 errno 为ECHILD表示没有子进程了。经典的循环回收写法是void sigchld_handler(int sig) { int saved_errno errno; while (waitpid(-1, NULL, WNOHANG) 0) { // 循环回收直到没有子进程退出 } errno saved_errno; // 保存和恢复 errno避免影响主程序 }这里保存errno是个容易被忽视的细节信号处理函数可能在任何系统调用之间被插入执行waitpid()本身可能改变errno如果不在处理函数里保存和恢复主程序本来要处理的errno就被悄悄篡改了导致排查问题时看到“莫名其妙”的错误码。第二个细节是SIGCHLD的处理器里不建议做太多事。绘制监控数据、发送通知、记录日志这些操作最好只是设置标志位让主进程的主循环统一处理。原因还是那个处理函数里只能调异步信号安全函数而发送网络请求、操作复杂日志库这些显然超出了安全清单范围。5.3 用 /proc 文件系统观察信号相关状态排查信号相关问题/proc文件系统是最直接的现场。/proc/pid/status里有几个和信号直接相关的字段SigQ显示该进程当前挂起信号数量和队列上限格式是0/31468前面是当前值后面是系统上限。如果看到挂起信号不断增加说明有信号被阻塞了但一直没人处理。SigPnd/ShdPnd分别是线程级和进程级挂起信号的位图显示哪些信号处于 pending 状态未递送。SigBlk当前阻塞信号的位图对应信号掩码。SigCgt进程捕获了哪些信号安装了处理器。这些位图都是十六进制数字换算对应关系时可以用位运算信号编号n对应第n-1位把十六进制数转成二进制数逐位看。举个例子SigCgt: 0000000000000003二进制是11说明进程捕获了1号SIGHUP和2号SIGINT。用这些字段排查的典型场景是一个服务进程“表现异常但没退出”先看SigBlk确认它阻塞了哪些信号再看SigPnd确认是否有正在排队等待递送的信号。如果发现服务阻塞了SIGTERM且 pending 里挂着SIGTERM那说明它收到过终止请求但选择不响应——这时候应用层排查方向就是阻塞信号的那段代码逻辑。strace工具也能辅助排查信号问题strace -e tracesignal -p pid可以实时跟踪进程收到的所有信号以及它调用的信号相关系统调用这在定位“某个信号是谁发的、什么时候发的”时非常有用。6. 信号相关的常见坑与排查技巧6.1 快查速记表信号症状、原因与对策我在实际处理问题中积累了一个信号相关的排查表说几个高频出现的症状可能原因对策进程突然消失无日志收到了SIGKILL可能是内存超限被 OOM Killer 选中查dmesg或journalctl -k中的 OOM 记录调整内存限制网络服务无故退出写Socket触发SIGPIPE默认动作是终止进程代码里忽略SIGPIPE或用MSG_NOSIGNAL*/进程无响应但状态是 S 或 D可能信号被阻塞SigBlk有对应位或在不可中断睡眠确认信号掩码对 D 状态的进程只能等或重启改了配置 kill -HUP 无变化某些进程不把 HUP 当作 reload 指令查官方文档确认支持信号类型避免误用多子进程同时退出僵尸堆积SIGCHLD处理器只调了一次waitpid改为while(waitpid(-1, NULL, WNOHANG) 0)循环回收6.2 一个典型的竞态问题子进程退出后父进程才设置 SIGCHLD有一个非常容易踩的线父进程fork()出子进程后如果先设置了SIGCHLD的处理器再让子进程运行中间可能漏掉早期的SIGCHLD。我遇到过一个双进程协同服务父进程要等子进程退出后重新拉起它但首次拉起后等很久才触发重新拉起的逻辑。排查后的原因就是竞态子进程很快退出在父进程的条件检查和pause()等待之间SIGCHLD已经到来了。因为信号是异步的父进程可能还没进入pause()信号就递送完了后续pause()等不到任何信号进程卡死。这就是前面提到sigprocmask()sigsuspend()组合的用武之地先阻塞SIGCHLD再做条件判断再sigsuspend()任务完成后再恢复。这种方式杜绝了“条件检查”和“等待信号”之间丢失信号的窗口。写多进程程序时建议默认就采用这个模式去处理SIGCHLD不要依赖“父进程先安装处理器再 fork”这种顺序上的侥幸。代码顺序看起来是对的但SIGCHLD的异步性意味着事件可能在任意间隙发生只有原子地“检查等待”才是可靠的。6.3 容器环境里的信号陷阱PID 1 的特殊性最后写一点容器场景的特殊性。现在的服务大量跑在容器里容器内PID为1的进程通常是主进程比如java -jar app.jar和其他PID有一些本质区别它对信号的处理会直接影响整个容器的生命周期。在Linux内核里PID 1 是 init 进程它有一个特殊职责收养孤儿进程。容器内所有的进程如果父进程先退出都会被PID 1接管PID 1需要负责调用wait()回收这些孤儿进程的退出状态。很多基础镜像的主进程只负责启动应用没有专门的“收养和回收孤儿进程”逻辑如果应用代码里再 fork 子进程子进程变僵尸后无人回收僵尸进程堆积导致容器内进程表耗尽整个容器就“卡死”了——表现出来的症状是docker exec都进不去。另一个容器相关的信号坑是docker stop的行为。docker stop默认向容器内PID 1发SIGTERM然后等10秒可配置超时再发SIGKILL。如果应用的Java、Go程序没有注册SIGTERM的优雅停机钩子就只能在超时后被强制杀掉事务没提交完、数据没刷盘、连接没释放都是潜在问题。这就是为什么现代服务框架都在强调“优雅退出graceful shutdown”——本质就是正确处理SIGTERM。关于容器里SIGTERM递送还有个细节如果容器内有多个进程docker stop只向PID 1发信号不会自动传播给其他子进程。所以编排多个进程的容器时要么用tini或s6这类init程序做信号转发和孤儿收养要么手动实现信号转发逻辑。直接裸跑多进程容器的坑比想象中多得多。6.4 最后再说两个实用调试技巧kill -0 pid是个低调但极其实用的命令。它的作用是“发送一个空信号”不实际递送任何信号但会返回发送权限检测的结果进程存在且你有权限返回0进程不存在或权限不足返回非0。脚本里判断进程是否还活着优先用kill -0而不是写ps -ef | grep再过滤grep 匹配自身进程、同名进程干扰是常有的问题。strace -f -e kill能跟踪所有信号的发送路径。调试“这个信号到底是谁发的”的时候直接在目标服务的启动入口挂上strace等信号发生时看栈里的调用来源通常比猜快得多。有一次排查中间件进程反复收到SIGUSR1的诡异现象用strace -e signal -p跟踪后发现是另一个监控代理在周期性探测心跳那个线程 SECURed 到中间件PID上乱发信号导致的。定位到来源之后问题就变成配置修正的简单事了。这几年的实践中Linux信号相关的坑翻来覆去就是这么几个信号处理函数里干了不该干的事、系统调用被信号打断没考虑EINTR、僵尸进程回收不彻底、容器PID 1的信号语义不清晰。理解了信号从产生、挂起、递送到处理的完整路径再遇到问题就不会慌先确认信号是哪里发的、进程的阻塞掩码是什么、处理器做了什么你基本就能找到答案了。
返回列表