news 2026/9/26 17:11:21

Linux信号处理全解:从异步通知原理到EINTR排查实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux信号处理全解:从异步通知原理到EINTR排查实战

新手阶段我啃《APUE》信号那一章,啃了三遍才敢说自己入门了。但真正让我对信号机制“开窍”的,不是书本上的定义,而是线上一次诡异的服务“假死”事故——进程还在,CPU 占用为 0,就是什么活都不干。后来 strace 一挂,发现进程阻塞在一个被信号打断后没有重试的系统调用上,就这么安安静静地“躺”了大半天。那一刻我才明白,信号这东西,日常里安静得像风,但处理不好,真的会让整个服务在风中凌乱。这篇文章,聊的就是 Linux 信号处理的门道——从原理到实操到填坑,一次性讲透。

1. 信号是什么:先搞清楚这个“异步通知”到底在通知什么

1.1 从一个“Ctrl+C”讲起:信号的日常体验

你在终端里跑着一个耗时的命令,比如tar解压一个大文件,等得不耐烦了,按下Ctrl+C,命令立刻终止,提示符回来了。这个过程中发生了什么?很多人只知道“按 Ctrl+C 能中断程序”,但背后的机制就是信号:终端驱动把按键解释为一个中断信号(SIGINT),然后把这个信号发送给前台进程组里的每一个进程。进程收到 SIGINT 后,默认行为是终止运行。

这件稀松平常的小事,把信号的几个核心特征全暴露出来了:

  • 信号是异步的:你不知道进程正在执行哪条指令,按下按键的那一刻,进程可能在做解压、可能在写日志、可能在等待网络数据包,信号随时可能到达。
  • 信号是通知,不是指令:内核只是告诉进程“你该中断了”,至于进程是立刻终止、还是先保存现场再退出、甚至完全忽略这个消息,取决于进程自己设置的“态度”。
  • 信号有默认行为:如果进程没有专门处理 SIGINT,内核就按默认规则办——终止进程。

理解这一点,信号就不再神秘了。它本质上是操作系统给进程递小纸条:“注意,发生了一件事,你自己看着办。”

1.2 信号在内核里的出生和送达:从按键到进程的完整链路

既然要讲“艺术和实践”,就不能只停留在应用层。我试着把信号从产生到被处理的完整链路捋一遍,你会发现它其实是一个很精巧的状态机。

一个信号的生命周期大致分为四个阶段:

  1. 产生(Generation):事件发生的瞬间。引发信号的事件很多:键盘输入(SIGINT、SIGQUIT)、硬件异常(段错误产生 SIGSEGV、非法指令产生 SIGILL)、软件条件(kill()系统调用、alarm()定时器到期、子进程状态变化产生 SIGCHLD)、终端挂断(SIGHUP)等。
  2. 未决(Pending):信号已经生成,但还没有被目标进程接收。如果信号被进程阻塞(block),它会一直停留在未决队列里,直到解除阻塞。
  3. 递送(Delivery):信号被进程接收的时刻。对于未被阻塞的信号,递送几乎是即时的。
  4. 处理(Handling):进程对信号作出反应。三种方式:执行默认动作(通常是终止或暂停)、忽略、执行自定义的信号处理函数。

这里有个关键点:普通信号(非实时信号)是不排队的。意思是,如果进程还没来得及处理一个信号,同样的信号又来了几个,内核只记录“有一个这样的信号待处理”,不会为每个信号建立一个队列条目。这就是很多新手踩的第一个坑——以为发了 N 次信号,处理函数就会被调用 N 次,实际上可能只调用一次。

这个不排队的特性,放在后面的“常见问题”里细说,这里先记住结论。

1.3 信号与门铃的类比:为什么异步是个好东西

理解异步这件事,最有用的一个类比是门铃。

想象你在房间里专心写代码,快递员到了楼下按门铃。门铃响了(信号产生),你听到声音(递送),放下手头工作去开门(处理)。在这个模型里:

  • 你不会每写一行代码就去楼下看一眼快递到了没有(那是轮询,polling)。
  • 门铃响的时机你无法预测,可能在你写第一行时就响,也可能写完整整一章才响(异步)。
  • 你可以选择不理会门铃(忽略信号),也可以选择贴个纸条“放门口就好”(改变默认处理方式),但你不能阻止快递员按门铃(除非你拔掉门铃线——那就是阻塞信号,让信号无法递送)。

对比一下轮询模式:如果没有信号这种异步通知机制,进程想知道“有没有新数据到了”“子进程退出了没有”“定时器到期了没有”,就只能不停地主动去查。查得频繁,浪费 CPU;查得不频繁,响应延迟。信号机制把“何时去查”的判断交给内核,进程只在信号到达时才做出反应,CPU 利用率和响应及时性都得到保障。

这就是信号的底层价值:它是对“主动查询”的一种替代方案,让进程在等待外部事件时不必空转。你可以回忆一下日常排查问题时top里看到的那些 sleep 状态进程——它们中的很多,就是在等待某个信号来唤醒,而不是在忙等。

2. 认识信号家族:常用信号逐个拆解

2.1 先看看清单:那些天天打照面的信号

Linux 上执行kill -l就能列出全部信号。传统 Unix 信号编号 1~31,POSIX 实时信号编号 32~64,日常打交道最多的是前 31 个。我把最常用的整理成一张表,默认行为列也一并标注:

信号编号触发场景默认行为
SIGHUP1终端挂断或控制进程退出终止进程
SIGINT2键盘中断(Ctrl+C)终止进程
SIGQUIT3键盘退出(Ctrl+\)终止并生成 core 转储
SIGKILL9kill -9强制杀死终止,不可捕获、不可阻塞
SIGSEGV11段错误(非法内存访问)终止并生成 core 转储
SIGPIPE13写管道但读端已关闭终止进程
SIGALRM14alarm()或setitimer()定时器到期终止进程
SIGTERM15kill默认发送的终止信号终止进程
SIGCHLD17子进程停止或退出忽略
SIGCONT18让暂停的进程继续运行继续运行
SIGSTOP19暂停进程(Ctrl+Z 的底层机制)暂停,不可捕获、不可阻塞
SIGTSTP20终端挂起(Ctrl+Z)暂停进程

这张表里我标了两个“不可捕获、不可阻塞”的异类:SIGKILL 和 SIGSTOP。这是内核的刻意设计——如果所有信号都能被进程屏蔽,那系统管理员就永远没办法强制终止一个失控进程了,这相当于给操作系统留了一把“物理钥匙”。

2.2 几个容易混淆的信号:SIGTERM vs SIGKILL、SIGHUP 的经典用途

新手最常问的一个问题是:“kill和kill -9有什么区别?”这里的区别,本质上就是 SIGTERM 和 SIGKILL 的区别。

kill命令默认发送 SIGTERM(编号 15)。SIGTERM 是“有礼貌的请求”:内核通知进程“请你退出”,进程可以捕获这个信号,在退出前完成清理工作——释放数据库连接、把缓冲区的日志刷到磁盘、删除临时文件、通知上游服务“我要下线了”。这也是为什么大多数服务管理工具(systemd 的Stop、Docker 的docker stop)默认发送的都是 SIGTERM。

而 SIGKILL 是“物理消灭”:内核直接终止进程,不给任何处理的机会。进程连“临终遗言”都来不及说,所有未刷盘的缓冲区数据直接丢失,任何自定义清理代码都不执行。所以一个成熟的运维流程里,kill -9永远应该是最后手段——先用 SIGTERM 给进程一个优雅退出的机会,等几秒钟,还赖着不走再上 SIGKILL。

再说 SIGHUP,这是一个很有故事的信号。它诞生于终端时代:用户挂在终端上的进程,终端断线后,内核发送 SIGHUP(Hang Up,挂断)给该终端下的进程组。默认行为是终止——想想也是合理的,用户都走了,你还跑给谁看?后来守护进程(daemon)发扬了 SIGHUP 的另一个用途:重新读取配置文件。因为守护进程没有终端,系统管理员要让它重新加载配置,总不能重启进程(重启有服务中断的风险),于是约定俗成:给守护进程发 SIGHUP,让它重新读配置。Nginx、sshd 都有这个习惯——kill -HUP $(cat /var/run/nginx.pid)就是经典的平滑重载配置操作。

2.3 信号的“生命周期”:产生、排队、递送、处理

前面我把生命周期分成四个阶段,这里再展开说一下“排队”这个细节,因为它和实时/非实时信号的差异直接相关。

对于标准信号(编号 1~31),内核在每个进程里维护一个pending 位图:每个信号占一位,这一位一旦置 1,就表示“这个信号处于未决状态”。如果进程正在处理 SIGINT,此时又来了一个 SIGINT,内核发现 pending 位图上 SIGINT 这一位本来已经是 1 了,就直接丢弃这个新信号——这就是“不排队”的底层原因。位图只有一位,无法记录“来了多少次”,只能记录“有没有来过”。

POSIX 实时信号(编号 32~64)则不同,内核为它们维护的是队列,每个信号都可以排队,可以附带数据(借助sigqueue()发送),到达的顺序也有保证。这就是为什么实时信号在一些对可靠性要求极高的场景(比如实时工业控制)中会用到——但在日常业务开发里,99% 的场合标准信号就够用了。

这个机制直接导出一个实战经验:不能依赖发送信号的次数来传递计数信息。比如某个脚本连发 5 次 SIGUSR1,希望触发 5 次处理逻辑——这完全是错误预期。正确的做法是:收到一次信号后,去读取文件、共享内存、消息队列里的“任务清单”,有多少处理多少。

3. 两种处理方式:signal() 与 sigaction() 的选型解析

3.1 signal():老接口的便利与陷阱

写 C 语言的信号处理,第一反应往往是signal()函数:

#include <stdio.h> #include <signal.h> #include <unistd.h> void handler(int sig) { write(STDOUT_FILENO, "catch SIGINT\n", 13); } int main() { signal(SIGINT, handler); for (;;) { sleep(1); } return 0; }

这段代码能跑,也很简洁。但我要泼一盆冷水:在实际项目里,我几乎不用signal(),而是用sigaction()。原因有几个。

第一,signal()在不同 Unix 系统上的语义有历史差异。早期的 System V 版本里,信号处理函数执行完一次后,信号处置方式会被重置回默认值(SIG_DFL),这意味着处理函数执行期间再次到达的同种信号,可能会按默认行为终止进程。虽然 Linux 的signal()实现是 BSD 语义(不会被重置),但如果你写跨平台代码,这个差异就是个雷。

第二,signal()无法精细控制信号处理的行为。它不能指定“在执行处理函数期间阻塞哪些其他信号”,也不能获取当前的信号处置方式。

第三,signal()不支持实时信号的排队和带参传递。想用sigqueue()传数据?signal()无能为力,必须上sigaction()。

因此,结论很直接:新写的代码一律用sigaction()。signal()适合在快速写个小测试程序、验证信号机制时用。

3.2 sigaction():结构体里的门道

sigaction()的函数签名如下:

#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

核心是struct sigaction这个结构体:

struct sigaction { void (*sa_handler)(int); // 信号处理函数 sigset_t sa_mask; // 处理期间要阻塞的信号集 int sa_flags; // 行为标志 void (*sa_sigaction)(int, siginfo_t *, void *); // 配合 SA_SIGINFO 使用 };

注意这是一个联合体(我为了直观省略了 union 的写法),sa_handler和sa_sigaction共用一块内存。如果你想用带siginfo_t的处理函数(能拿到发送者 PID、信号编号、附加数据等),需要设置sa_flags里的SA_SIGINFO标志位,并把函数指针赋给sa_sigaction字段。

sa_mask是容易被忽略却很重要的一个字段:它指定了一个信号集,当当前信号的处理函数正在执行时,这个信号集里的信号会被自动阻塞。举个例子,你在处理 SIGINT 时,不希望又收到 SIGTERM 把事情搞乱,就把 SIGTERM 放进sa_mask,这样 SIGTERM 会等到 SIGINT 处理函数返回后才被递送。注意,被处理的信号本身也会在它自己的处理函数执行期间被阻塞,防止同一信号递归打断,这个行为是内核自动保证的,不需要手动往sa_mask里加自己。

sa_flags里还有几个常用的:

  • SA_RESTART:让被信号打断的系统调用自动重启,这个太关键了,后面专门讲 EINTR。
  • SA_SIGINFO:使用sa_sigaction作为处理函数,可以获得更多细节。
  • SA_RESETHAND:处理完一次后恢复默认行为(System V 语义)。
  • SA_NOCLDWAIT:作用于 SIGCHLD 时,子进程退出后不变成僵尸进程(但是慎用,会丢失子进程的退出状态)。

3.3 实操代码:完整实现一个优雅退出程序

理论讲完,上实操。这里我给出一个“优雅退出”的完整示例,它做了这么几件事:

  1. 捕获 SIGINT 和 SIGTERM,收到后设置一个全局标志位,让主循环自然退出。
  2. 捕获 SIGUSR1,用于触发一次配置重载(这里用日志打印模拟)。
  3. 在信号处理函数里只做异步安全的最简操作(后面会讲为什么)。
#include <stdio.h> #include <signal.h> #include <string.h> #include <unistd.h> #include <stdlib.h> static volatile sig_atomic_t g_running = 1; static volatile sig_atomic_t g_reload = 0; static void handle_signal(int sig) { if (sig == SIGINT || sig == SIGTERM) { g_running = 0; } else if (sig == SIGUSR1) { g_reload = 1; } } static void setup_signal(int sig) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = handle_signal; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(sig, &sa, NULL) == -1) { perror("sigaction"); exit(1); } } int main() { setup_signal(SIGINT); setup_signal(SIGTERM); setup_signal(SIGUSR1); printf("process started, pid=%d\n", getpid()); while (g_running) { if (g_reload) { g_reload = 0; printf("reload config...\n"); // 这里做真正的配置读取逻辑 } // 模拟业务逻辑 sleep(1); } printf("process exiting gracefully\n"); return 0; }

几个细节你可能注意到了:

  • g_running和g_reload声明成volatile sig_atomic_t而不是普通的int。sig_atomic_t是保证对信号处理函数中的读写是原子的整数类型,避免读到半更新状态。volatile是防止编译器把变量优化到寄存器里——信号处理函数和主循环之间共享这个变量,如果编译器自作聪明地缓存了副本,主循环可能永远看不到更新。
  • 信号处理函数里我没有调用printf,而是直接把标志位置位,由主循环去响应。这是一个原则性问题,下一节详细解释。

编译运行一下:

gcc -o graceful graceful.c ./graceful

另开一个终端,执行kill -USR1 <pid>,程序输出 reload config;执行kill -TERM <pid>,程序打印 exiting 信息后退出。对比一下直接kill -9,退出时没有清理日志——这就是优雅退出和强杀的区别。

4. 进阶实践:定时器信号、子进程协作与 Shell 层级的信号处理

4.1 用 SIGALRM 实现定时任务

alarm()是每个 C 程序员最早接触的定时器接口之一:

#include <stdio.h> #include <signal.h> #include <unistd.h> void on_alarm(int sig) { printf("tick\n"); alarm(1); // 再次设定期时器,实现周期性 } int main() { signal(SIGALRM, on_alarm); alarm(1); for (;;) { pause(); // 挂起直到收到信号 } return 0; }

这个程序每秒打印一次 “tick”。原理是:alarm()让内核在指定秒数后向当前进程发送 SIGALRM,默认行为是终止进程,我们把它捕获并重新设定下一次闹钟。

alarm()的粒度是秒级,只支持一个定时器。更精细的场景用setitimer()或timer_create():

  • setitimer()支持微秒级精度,且支持三种计时模式(ITIMER_REAL、ITIMER_VIRTUAL、ITIMER_PROF)。ITIMER_REAL 到期发送 SIGALRM;ITIMER_VIRTUAL 只在进程用户态执行时计时,到期发送 SIGVTALRM;ITIMER_PROF 统计用户态+内核态耗时,到期发送 SIGPROF。
  • timer_create()是 POSIX 定时器,支持高精度、多个独立定时器,甚至可以用实时信号携带超时值。

不过在 Linux 上实现高精度的定时任务调度,我的个人建议是优先考虑timerfd,这个话题稍微超纲,这里留个引子:timerfd_create()把定时器事件绑定到一个文件描述符上,配合epoll,比信号定时器干净得多,因为你完全绕开了信号处理函数里的种种限制。

4.2 SIGCHLD 与子进程回收

fork()之后,子进程退出时,内核会向父进程发送 SIGCHLD 信号。如果父进程不对这个信号做任何处理,子进程退出后就会变成僵尸进程(zombie),占用一个进程表项,直到父进程调用wait()/waitpid()来回收。

我见过不少生产事故:父进程跑成了守护进程,fork()出的子进程意外退出,但父进程没wait(),于是每个失败的子进程都变成僵尸。僵尸越积越多,进程号枯竭,最终整个服务不可用。

正确的做法之一,就是在父进程里捕获 SIGCHLD,并在处理函数里调用waitpid():

#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/wait.h> #include <unistd.h> #include <string.h> static void on_sigchld(int sig) { int status; pid_t pid; // WNOHANG 保证如果没有子进程退出就立即返回,不会阻塞 while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { printf("child %d exit, status=%d\n", pid, WEXITSTATUS(status)); } } int main() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = on_sigchld; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // 只在子进程退出时通知,停止不通知 sigaction(SIGCHLD, &sa, NULL); pid_t pid = fork(); if (pid == 0) { // 子进程 exit(42); } // 父进程持续运行 for (;;) { sleep(1); } return 0; }

注意这里用了while循环配合WNOHANG反复调用waitpid(),原因仍然是信号不排队的问题:如果瞬间多个子进程退出,可能只触发一次 SIGCHLD,如果处理函数里只waitpid()一次,就可能漏收僵尸。循环回收直到返回 0(没有更多退出的子进程)才是稳妥做法。

SA_NOCLDSTOP标志的意义是:只关心子进程退出,不关心子进程因信号被暂停(SIGSTOP/SIGTSTP)的事件,避免无谓的唤醒。

4.3 Shell 脚本中的 trap 陷阱命令

信号处理不只是 C 语言的专利。写 Shell 脚本时,trap命令是你最好的朋友。它的基本用法是:

trap 'echo "catch signal"' INT TERM

意思是:当脚本收到 SIGINT 或 SIGTERM 时,执行echo。更常用的场景是清理临时文件:

#!/bin/bash tmpfile=$(mktemp) trap 'rm -f "$tmpfile"; echo "cleanup done"; exit 1' INT TERM EXIT echo "tmpfile: $tmpfile" while true; do sleep 1 done

这里我把EXIT也放进了 trap 列表。EXIT是 Bash 扩展的伪信号,脚本无论正常结束还是异常退出,都会触发该 trap。这样就能保证临时文件一定会被清理,不会残留在 /tmp 里。

有几个 trap 的细节值得强调:

  • 如果脚本里启动了后台子进程,kill脚本主进程不会自动传递信号给子进程。要妥善处理,可以在 trap 里显式kill后台任务的 PID。
  • 如果希望忽略某个信号,trap 目标可以写空字符串:trap '' INT,此时进程收到 SIGINT 不做任何事。这在写一些“不可中断”的安装脚本时很有用。
  • trap 字符串中的变量展开时机很微妙。用单引号包裹时,变量在 trap 执行时才展开;用双引号包裹时,变量在 trap 设置时就展开。初学者在这个细节上容易踩坑,建议统一用单引号。

5. 常见问题与排查技巧实录

5.1 信号丢失:非实时信号的排队问题

这是最经典的“幽灵问题”。现象是:程序明明收到多次信号,处理函数只执行了一两次。原因我在信号生命周期里说过了——标准信号用位图维护 pending 状态,相同信号不排队。

但这背后还有一个进阶场景:处理函数还没执行完,新信号又来了。此时如果新信号被阻塞(比如正在处理该信号),它会停留在 pending 位图上;处理函数返回后,这个信号就会被递送。如果期间又来了好几个同种信号,位图还是只记录“有一个”,处理函数只会再执行一次。

所以,凡是想通过“信号次数”来驱动逻辑的设计,都要重新审视。我的经验是:信号只当“敲门砖”用,真正的信息装在文件、管道或共享内存里。收到信号后去读数据,数据有多少就处理多少,不依赖信号本身的次数。

5.2 EINTR:被信号打断的系统调用

这是一个让很多人挠头的错误。你写了一个阻塞式read(),等待客户端发送数据,然后程序收到一个信号,处理函数执行完后,read()返回了 -1,errno被设置为EINTR。

为什么?因为系统调用在等待期间被信号中断,内核会先切换到信号处理函数,等处理函数返回后,系统调用是否继续由 SA_RESTART 标志决定。如果设置了这个标志,内核会自动重新启动系统调用,对应用无感知;如果没有设置,系统调用返回EINTR,必须由应用自己决定要不要重试。

这种错误在 Java 里体现为InterruptedException,在 C 里就是EINTR。解决方式:

  1. 在sigaction()里设置sa_flags |= SA_RESTART。大多数情况下,这能让read()、write()、wait()这类系统调用自动重启。
  2. 对于某些不支持自动重启的系统调用(如poll()、epoll_wait()、nanosleep()),即便设置了 SA_RESTART 也可能返回 EINTR。这时必须手动重试:
do { ret = epoll_wait(epfd, events, maxevents, timeout); } while (ret < 0 && errno == EINTR);

我在生产环境里排查过一起“偶发请求超时”,最后定位就是epoll_wait()被周期性信号中断,返回 EINTR 后代码直接 break 出循环,导致这一轮的连接没有被及时处理。修复方式就是加了一个 EINTR 重试循环,超时问题彻底消失。

5.3 死锁与重入:在信号处理函数里别乱调函数

这条经验,我愿称之为“信号处理的铁律”。信号处理函数执行时,主流程可能正处于一个随机的位置——可能刚给某个互斥锁加了锁,可能正在malloc()内部操作堆的内存,可能正在调用某个库函数的中间步骤。如果处理函数里也调用了printf()(内部可能加锁)、malloc()、free()、pthread_mutex_lock()这类非异步安全函数,就可能发生:

  • 死锁:处理函数想拿主流程已经拿住的锁,而主流程被信号打断,永远不释放。
  • 堆损坏:malloc()是线程不安全的(且有内部锁),处理函数里再调malloc(),极可能破坏堆元数据,产生随机崩溃。

POSIX 明确规定,在信号处理函数里可以安全调用的函数是有限的,统称“异步安全函数”(async-signal-safe)。常见的有:

  • write()、read()、open()、close()
  • _exit()(注意不是exit())
  • kill()、getpid()、sigaction()
  • waitpid()(配合 WNOHANG)

而不安全的包括:printf()、malloc()、free()、pthread_*、syslog()等。

结合我自己的实践,最稳妥的模式是:处理函数里只设置标志位(sig_atomic_t),主循环里再执行真正的业务逻辑。 如果非要立即响应,也只用write()往管道里写一个字节,主循环用poll()监听管道可读,再去做重活。

5.4 排查信号问题的实用工具

最后给出一套我常用的信号问题排查工具链,遇到“疑似信号问题”时按顺序排查:

  1. kill -l:列出系统支持的全部信号及编号。记不住编号时的查表神器。
  2. /proc/<pid>/status:查看进程的信号相关信息。关键字段SigPnd(挂起的信号)、ShdPnd(进程级挂起)、SigBlk(阻塞的信号)、SigIgn(忽略的信号)、SigCgt(捕获的信号),以十六进制位图显示。我曾经用这个确认过一个进程“是不是漏了 SIGTERM 的捕获”——一看SigCgt位图就真相大白。
  3. strace -p <pid>:跟踪系统调用,能直观看到信号打断系统调用的现场。-e trace=signal可以只追踪信号相关调用。
  4. gdb调试时,用handle SIGSEGV stop设置断点,排查段错误信号。

一次排查“进程收不到 kill 信号”的过程,我很推荐新手复现一下:起一个 sleep 进程,手动编辑/proc/<pid>/status是做不到的(权限不允许),但如果进程阻塞了信号,SigBlk对应位会是 1。你会发现,kill一个阻塞了 SIGINT 的进程,进程确实不会终止——不是信号丢了,而是它被阻塞在 pending 队列里了。

写在最后的一点个人体会

开头提到的那个线上“假死”事故,最后修复其实只改了三行代码:给sigaction加上SA_RESTART,给epoll_wait加上 EINTR 重试。但这三行代码背后的理解成本,是好几本 Linux 编程书和无数次踩坑攒下来的。信号处理就是这样——平时静默无声,像风一样让人感觉不到存在,可一旦你在不合适的地方做了不合适的事,它就能让整个系统在“风中凌乱”。

我个人最大的心得是:对信号保持敬畏,但不必恐惧。把“信号只做通知、不做业务”这个原则焊死在脑子里,再熟练运用sigaction的几个关键标志位,绝大多数信号相关的问题都可以在设计阶段就规避掉。如果你正在做一些网络服务、守护进程或者容器相关的工作,强烈建议亲手写一遍这篇文章里的小例子——不是背接口,而是把信号在你脑子里的“运行模型”搭起来。模型通了,代码就通了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 17:11:16

ghcr.io镜像拉不下来?亲测有效的六种加速与搬运方案

1. 为什么每次 docker pull ghcr.io 都卡在半路&#xff1a;一次拉取背后的完整链路群里又有人喊 ghcr.io 镜像拉不动了。这种事我一年能碰上好几次&#xff1a;docker pull ghcr.io/xxx/yyy:latest敲下去&#xff0c;进度条长时间停在 0%&#xff0c;等几分钟直接弹i/o timeou…

作者头像 李华
网站建设 2026/9/26 17:11:16

校园文件管理系统源代码实战:部署、权限控制与二次开发指南

简介&#xff1a;一套面向校园网环境的文件管理系统源代码&#xff0c;覆盖学校班级文件共享、课件资源管理、内容发布与存储备份等典型场景。系统由桃源企业文件管理系统V2.4演进而来&#xff0c;在通用文件功能的基础上强化了文件发布、教育课件管理和权限控制能力&#xff0…

作者头像 李华
网站建设 2026/9/26 17:08:18

Claude Code Skills实战指南:从安装配置到API报错排查

做 AI 编程这块的朋友&#xff0c;最近应该都注意到一个事&#xff1a;Claude Code 从单纯的命令行助手&#xff0c;开始往“带技能”的方向发展了。这个 Skills 扩展机制刚出来的时候我还没太当回事&#xff0c;直到自己在两个项目里连续踩了上下文失控和 API 调用混乱的坑&am…

作者头像 李华
网站建设 2026/9/26 17:07:37

Linux Socket编程:从底层通信原理到常见错误排查

我们直接聊Socket编程&#xff0c;但聊的是写第一行代码之前&#xff0c;你必须先搞明白的那些底层的、通信层面的东西。很多教程一上来就甩给你socket()、bind()、listen()的函数签名&#xff0c;然后让你抄一个 echo server&#xff0c;跑通了就以为会了。但一旦遇到高并发、…

作者头像 李华
网站建设 2026/9/26 17:07:06

私有化部署CRM实战:Docker Compose搭建Deskcomm客户管理系统

1. 为什么我最终选择了私有化部署这条路三年前我第一次接触CRM&#xff0c;用的是某家免费SaaS产品。刚开始觉得挺香&#xff0c;注册就能用&#xff0c;客户录入、跟进记录、销售漏斗一应俱全。但用了不到半年&#xff0c;问题就来了&#xff1a;免费版限制联系人数量&#xf…

作者头像 李华