news 2026/10/5 0:19:23

Linux进程信号机制详解:从生命周期到sigaction实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux进程信号机制详解:从生命周期到sigaction实战

1. 信号到底是什么,为什么要用它

如果你写过Linux下的服务程序,或者哪怕只是用kill命令杀过几个进程,那你其实已经和“信号”打过交道了。比如终端里按一下Ctrl+C,前台进程立刻退出;kill -9 <pid>把杀不掉的进程强制带走;程序崩溃时 shell 里冒出一句Segmentation fault (core dumped)。这些场景背后,全都是 Linux 进程信号在起作用。

信号本质上是一种进程间异步通知机制。它不需要像管道、消息队列那样建立双向通道,也不需要双方同时在线等着收发,而是由一个进程(或者内核)直接向另一个进程发送编号化的“通知”,告诉它“该停一下了”“你踩到非法内存了”“你的子进程退出了”,对方收到后按约定的方式处理。这个模型很像日常生活中的来电:你正在做手头的事,电话响了,你可以选择立刻接起来处理,也可以先挂掉忙完再说(对应阻塞),甚至干脆拔掉电话线(对应忽略)。

这篇文章主要讲信号的基础机制:信号是什么、内核怎么管理它、标准信号和实时信号有什么差别、常见的信号怎么用、以及signal和sigaction这两套处理接口的差别。适合刚学完进程管理、想进一步理解 Linux 进程协作方式的读者,也适合准备嵌入式或后端面试时想系统性梳理“进程间通信”这块知识的人。内容偏原理偏底层,但我会尽量用实操验证的方式来拆,让看完的你既能应付面试里“信号是什么”这类概念题,也能在调程序时真正用得上。

我自己刚学这块的时候最大的困惑是:信号到底存在哪里、什么时候算“发出”、什么时候算“收到”,这几个时间点总是理不清。后面用sleep + kill + ps反复做实验,加上看内核的 task_struct 结构,才算把这条链路彻底捋顺。下面我把这套东西一步步拆开讲。

2. 信号的完整生命周期:产生、未决、递送、处理

2.1 什么时候算“收到”信号

很多人以为kill -TERM <pid>敲下去那一刻,进程就算“收到”信号了。严格说不对。信号的传递分三个阶段:产生(generation)、未决(pending)、递送(delivery)。

信号产生之后,内核会在目标进程的进程控制块(task_struct)里挂一个信号记录,但此时目标进程可能正在内核态做系统调用,也可能正在用户态跑业务代码。真正要对信号做出响应,必须等到该进程被调度、从内核态返回用户态的那个检查点。换句话说,信号的“递送”发生在进程被切换回用户态之前。如果进程此刻压根没被调度(比如处于睡眠状态),那信号就一直在 pending 队列里躺着,直到进程醒来。

理解这个时序特别重要。比如你写了一个死循环程序,里面没有任何系统调用,那么它大部分时间都在用户态转悠,信号送去之后要等内核抢占、处理完调度切换,才会进入处理流程——这中间可能有几毫秒的延迟。反过来,如果进程正在做文件读取这种内核操作,信号反而会在系统调用返回的路上被快速检查到。

2.2 内核里的三张“信号账本”

Linux 内核为了管理每个进程的信号状态,在 task_struct 里维护了好几个关键字段,理解它们就理解了信号机制的一半:

  • signal_struct:描述进程组、会话等信号相关属性的共享结构,也记录了信号处理函数(action表)以及信号是否被屏蔽。
  • pending(当前未决信号集合):记录“已经产生、但还没递送”的信号位图。每个进程有一个自己的 pending,线程组还有一个共享的shared_pending。
  • blocked(信号屏蔽集合):记录当前被阻塞的信号位图,也就是进程暂时“不想处理”的信号集合。

信号处理函数的注册表是一个数组,下标就是信号编号,默认情况下每个标准信号都有对应的默认行为(终止、忽略、停止、继续或产生 core dump)。你没写任何处理代码时,内核就按默认行为帮你兜底。

这三者的协作方式用一句话总结就是:信号产生时在 pending 里置位,递送时查 blocked 判断是否放行,放行后查 action 表决定怎么处理。阻塞和忽略的区别也在这里:忽略是“处理方式”设为不理会,阻塞则是“根本不让你递送”,一旦解除阻塞,之前未决的信号会立刻递送。顺带说一句,blocked和pending都是位图结构,所以 Linux 里每个信号在任意时刻只有“有无”之分,同样类型的信号不会排队堆积,这正是标准信号最大的局限。

2.3 标准信号为什么“不排队”

标准信号(1~31号)在 pending 里只有一个 bit 位。也就是说,连续发送 5 个SIGUSR1,内核只记一次“有一个 SIGUSR1 未决”。等信号处理函数执行完,队列清零,后续那些同类型信号就丢了。实时信号(34~64号)不一样,它们走的是排队机制,不能合并,每一个信号都有独立的记录。

这个差异对普通程序影响不大,但对“靠信号计数”的场景是致命的。我以前调一个多线程下载器的进度统计模块,想用SIGUSR1通知主线程“某个分片下载完成”,结果程序跑起来之后进度一会儿快一会儿慢,原因就是连续到达的SIGUSR1被合并了,计数丢了。后来改成用管道传消息,才彻底解决。标准信号适合“通知事件发生”,不适合“传递精确信息”,这个边界一定要清楚。

3. 信号家族谱:标准信号 vs 实时信号

3.1 常用标准信号速查表

Linux 的标准信号编号是 1 到 31,每一路信号都有固定的触发场景和默认动作。下面这是我在实际开发和面试准备中反复用到的一张速查表:

信号编号信号名触发场景默认动作
2SIGINT终端按 Ctrl+C终止进程
3SIGQUIT终端按 Ctrl+\终止并 core dump
6SIGABRTabort() 调用终止并 core dump
8SIGFPE除零或浮点异常终止并 core dump
9SIGKILLkill -9强制终止,不可捕获不可阻塞
11SIGSEGV非法内存访问终止并 core dump
13SIGPIPE写管道但读端关闭终止进程
14SIGALRMalarm() 定时器到期终止进程
15SIGTERMkill 默认发送终止进程(可捕获)
17SIGCHLD子进程状态改变忽略
19SIGSTOPCtrl+Z 或 kill -STOP暂停进程
20SIGTSTP终端停止键暂停进程
18SIGCONT继续被暂停的进程恢复运行

这里重点说几个容易混的。SIGTERM和SIGKILL都是“终止”,但前者可以捕获,进程可以在退出前做资源清理、保存现场;后者直接由内核强制释放进程,连清理的机会都没有。所以服务治理规范里通常会先发SIGTERM做优雅停机,等几秒没退再补SIGKILL。SIGSTOP和SIGTSTP也都容易看混:前者是绝对的暂停信号,程序无法拦截;后者是终端发起的暂停,可以被程序捕获,用于在暂停前保存状态。

SIGPIPE是我在写网络服务时踩坑最多的一路信号。默认行为是直接终止进程,而且终端上经常看不到任何报错。比如你用curl测试一个 HTTP 服务,服务端往已关闭的连接里写数据,如果进程没处理SIGPIPE,它会在某个瞬间莫名其妙地“消失”,排查起来特别像内存泄漏导致的崩溃。所以长期跑的网络服务里,几乎都会看到signal(SIGPIPE, SIG_IGN)这一步。

3.2 实时信号带来了什么

实时信号编号范围是 34 到 64(32 和 33 被 NPTL 线程库保留用于内部机制),最核心的改进有三个:支持排队、支持携带整数或者指针数据、有优先级(编号越小优先级越高)。注意实时信号不使用“编号越小优先级越高”这个逻辑——恰好相反,编号越大反而排在队列前面,信号调度时按编号从大到小递送。这个细节我在面试中被问到过一次,印象特别深。

实时信号的数据载荷靠sigqueue()函数发送,处理函数要从siginfo_t里取数据。比如设计一个简易的用户态任务通知机制:生产者用sigqueue(pid, SIGRTMIN+5, value)给消费者发消息,消费者在信号处理函数里sigval字段就能拿到参数。这个用法在嵌入式场景里非常常见,比如设备驱动上报事件给应用层。

但说实话,现代 Linux 开发里,实时信号的实际应用场景在变少。原因很简单:进程间要传结构化数据的话,socketpair、管道、共享内存哪个都比信号方便;信号处理函数里能调用的函数非常受限(很多函数不是异步信号安全的),一不小心就会死锁或者数据竞争。实时信号如今更多是作为一种“快速事件通知”手段在使用,而不是完整的数据通道。

3.3 信号编号可移植性

这个坑我忍不住先提一下:不要直接在代码里用数字编号。虽然上面表格里写了“信号编号”,但那是 x86 上的编号,ARM、MIPS 等架构不一定完全一致。比如 x86 上SIGBUS是 7,有些架构上编号就不一样。写跨平台代码时请始终使用宏名(SIGTERM等),命令行操作时用kill -l可以查看当前系统的编号映射。每次在新的嵌入式板子上调试,我都会第一时间跑一下kill -l,防止把编号搞错。

4. 信号的默认动作和 Core Dump

4.1 为什么程序崩溃会留下 core 文件

进程收到SIGSEGV、SIGABRT、SIGFPE这一类信号时,系统默认动作除了终止进程,还会尝试把进程当前的内存映像写进磁盘,形成 core dump 文件。这个文件的名字默认是core,也可能带 pid 后缀(core.<pid>),具体看系统的kernel.core_pattern配置。

core dump 就是为了事后调试的。进程崩溃的现场太宝贵:当时的栈回溯、寄存器值、内存数据全都凝聚在这个文件里,gdb <binary> core.<pid>可以直接加载进去看崩溃线程的调用栈。很多线上偶现的段错误,排查最有效的方式就是开着 core dump,等下次崩了直接分析现场,比加日志靠谱得多。

不过这里有一个很常见的陷阱:默认情况下很多系统会限制 core dump 的大小为 0,也就是根本不产生 core 文件。如果发现程序崩了却没有 core 文件,先执行ulimit -c unlimited再看。小内存的嵌入式板子上还要留意磁盘空间,410MB 的 core 文件可能会直接把根分区塞满。

4.2 信号处理函数注册的基本姿势

要自定义某一路信号的处理方式,最基础的方法是signal()函数,原型很简单:

#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);

handler有三个取值:SIG_IGN(忽略)、SIG_DFL(恢复默认)、或者一个自定义函数指针。比如忽略管道破裂信号:

signal(SIGPIPE, SIG_IGN);

习惯上一定要检查返回值,因为signal()失败时返回SIG_ERR,如果没检查,后续逻辑很可能在错误基础上跑出更隐蔽的问题。另外网上很多老代码用signal()做信号处理,但它有两个广为人知的毛病:一是不同 UNIX 版本的行为差异很大(有的系统处理完一次后自动重置为默认动作),二是处理信号期间如果又有新信号到达,可能因为缺乏阻塞机制导致重复进入处理函数,产生竞态。所以新代码我强烈建议直接用sigaction(),它的行为在各平台下足够一致,控制粒度也细得多。

struct sigaction sa; sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGTERM, &sa, NULL);

sa_mask字段的意思是:在处理这个信号的期间,额外阻塞哪些信号。比如处理SIGTERM时还想把SIGINT也一并挡住,就把SIGINT加入sa_mask。sa_flags里我觉得最常用的标志有两个:SA_RESTART,让被信号打断的系统调用自动重启,避免read()返回EINTR;SA_SIGINFO,启用带参数的信号处理函数(配合siginfo_t使用)。

4.3 用 sigaction 验证“同一信号不排队”

写一段简单的验证程序,连续发送 10 个SIGUSR1给同一个进程,看处理函数调用了几次:

#include <stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> static int count = 0; void handler(int sig) { count++; write(STDOUT_FILENO, "sig\n", 4); } int main(void) { struct sigaction sa = {0}; sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); for (int i = 0; i < 10; i++) { kill(getpid(), SIGUSR1); } sleep(1); printf("handler called: %d\n", count); return 0; }

可能有读者看到write觉得奇怪:为什么处理函数里不直接用printf?原因很简单,printf在信号处理上下文里不是异步信号安全的,和主流程里的printf混在一起可能出问题。这里我用write做最小输出,同时避免了缓冲区问题。

编译运行后,输出结果会落在“1 次”附近,而不是 10 次。标准信号被合并,这就是最直观的证明。如果把SIGUSR1换成某个实时信号(比如SIGRTMIN+1),再跑一次,计数就会接近 10。这一个实验就能把上面两节的理论全部验证一遍。

5. 信号从哪来:常见的产生方式和发送函数

5.1 终端按键与硬件异常

信号产生来源大致分为三类。第一类是用户主动从终端按键触发:Ctrl+C产生SIGINT,Ctrl+\产生SIGQUIT,Ctrl+Z产生SIGTSTP。这类信号发往整个前台进程组,所以多条管道链上的进程都会被波及。

第二类是硬件异常:CPU 在执行指令时发现除零、非法内存访问、总线错误等异常,由内核翻译成对应的信号发给当前进程。这类信号通常以SIGFPE、SIGSEGV、SIGBUS为代表,默认行为基本都是“终止 + core dump”。需要注意,硬件异常信号产生于 CPU 内部,不是像kill那样“外人主动发”,这也是为什么它们往往来得非常突然,进程完全没有准备。

第三类就是显式调用函数。raise(sig)给当前进程发信号;kill(pid, sig)给指定进程发;abort()给自己发SIGABRT并 core dump;alarm(seconds)让内核在 seconds 秒后给你发SIGALRM;setitimer()则是更精细的定时器接口。嵌入式开发里,alarm 常用于实现超时控制,比如等待设备就绪时,超过 3 秒还没响应就触发SIGALRM清理资源,避免卡死主流程。

5.2 kill 函数里 pid 参数的深刻含义

kill()的第二个参数是信号编号,好理解;但第一个参数的坑我不止一次见人踩:

  • pid > 0:发送给指定 pid 的进程。
  • pid == 0:发送给当前进程所在进程组的所有进程。这个在批量控制子进程时非常好用,比如kill(0, SIGTERM)通知当前进程组全部退出。
  • pid == -1:发送给当前进程有权限发送的所有进程,除了 1 号进程(init/systemd)和当前进程自身。这招切莫乱用,尤其不要直接搁在带 root 权限的脚本或程序里,否则整个系统上能被杀到的进程都被你问候一遍。
  • pid < -1:发送给进程组 id 等于-pid的所有进程,用于按进程组精准管理。

很多面试题会问“kill -9 -123 是什么意思”,答案就是按进程组号发信号。如果只看过网上教程说“pid 填 -1 就能杀所有进程”,十有八九会在这里卡壳。

5.3 信号的“不可杀”特权:SIGKILL 和 SIGSTOP

这两路信号连 root 也只能说“我能发”,但收信的进程本身无法屏蔽、无法捕获、无法忽略。发出去之后,内核直接对 task_struct 执行停止或销毁的终审判决,没有复议环节。

为什么 Linux 要预留这么两路“上帝信号”?因为系统必须有最后兜底的手段:一个进程如果自己把自己所有信号都屏蔽了,又没有心跳机制,那就成了杀不死的僵尸级存在;oss 软件出故障时,kill -9是管理员手中最后一把破门锤。代价是它永远无法触发用户态逻辑,所以优雅退出协议里,SIGTERM是首选,SIGKILL永远是底线。

6. 屏蔽信号:sigprocmask 与未决队列实战

6.1 屏蔽不是忽略

很多初学者把“屏蔽信号”理解成“让信号不起作用”,其实不对。屏蔽的准确含义是“把信号挡在递送之前”,信号一旦产生,就留在 pending 集合里,等屏蔽解除后再补送。

这个机制非常有用,典型场景是“临界区保护”。假设程序维护一个数据结构,正在更新过程中,如果此时来一个信号触发处理函数、而处理函数又访问同一份数据结构,可能看到中间态而崩溃。正确做法是在更新前用sigprocmask把相关信号全部屏蔽,更新完再解除屏蔽,信号处理函数等更新结束后再执行,避免数据竞争。

6.2 一个亲手验证 pending 机制的小程序

下面这段代码演示了“屏蔽 -> 发信号 -> 查询 pending -> 解除屏蔽 -> 处理函数执行”的完整链条:

#include <stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> void handler(int sig) { printf("got SIGUSR1\n"); } int main(void) { struct sigaction sa = {0}; sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); sigset_t set, oldset, pending; sigemptyset(&set); sigaddset(&set, SIGUSR1); sigprocmask(SIG_BLOCK, &set, &oldset); kill(getpid(), SIGUSR1); sigpending(&pending); if (sigismember(&pending, SIGUSR1)) { printf("SIGUSR1 is pending\n"); } sigprocmask(SIG_SETMASK, &oldset, NULL); printf("unblocked, handler runs next\n"); sleep(1); return 0; }

编译跑一下,预期输出顺序是:先打印SIGUSR1 is pending,解除屏蔽之后立刻打印got SIGUSR1。注意这里信号处理时用的printf仅仅是演示,真实代码里建议使用 async-signal-safe 的write。

我在写这段代码时踩过一个特别低级的坑:sigprocmask第三个参数要传旧的信号集,用来恢复现场。我只传了NULL,导致后续想恢复时无从下手,程序的行为变得不确定。所以养成习惯,oldset该传就传,恢复时用SIG_SETMASK精准还原。在 PThreads 环境里,官方方案是每个线程用pthread_sigmask()而不是sigprocmask(),多线程程序里不要混用,否则信号屏蔽在线程间的传递行为会变得很诡异。

6.3 pending 状态一眼看穿

sigpending()只能告诉你某个信号是否 pending,但如果想直观看到整个未决信号集合,可以熟悉一下/proc/<pid>/status里的SigPnd、ShdPnd、SigBlk字段。它们是十六进制位图,比如SigBlk: 0000000000000200表示屏蔽了 10 号信号(0x200 = 512 = 1 << 9,对应信号编号 10)。写守护进程的调试工具时,这种信息比盲目猜好使一百倍。我上个月还在一个现场问题里靠这个字段定位到“某个模块意外把 SIGTERM 屏蔽了”,导致服务明明收到了停止指令却不退出。

7. 进程退出瞬间的信号处理:不算 bug 的竞态

7.1 SIGCHLD 与子进程善后

子进程退出时,内核会给父进程发SIGCHLD。父进程注册处理函数后,可以在里面调用waitpid()回收子进程的退出状态,防止它变成僵尸。这也是实现“异步回收子进程”的经典手段。

处理SIGCHLD时有三个细节要特别注意。第一,信号处理函数里waitpid要用WNOHANG并以循环方式调用,把能回收的子进程都回收掉,只回收一个是很多人的第一版写法,子进程一多就漏。第二,处理函数返回后如果主流程里也在wait,两边可能抢着收同一个子进程,带来混乱,所以要么只在一个地方回收,要么用sigprocmask阶段性屏蔽。第三,子进程可能因为SIGSTOP/SIGCONT暂停或恢复而触发SIGCHLD,处理时要结合status判断到底是什么事件,不能一律当“退出了”看待。

7.2 pause 与 sleep 的信号打断问题

pause()会让进程挂起直到收到信号,处理函数执行完,pause()返回 -1 并设置errno为EINTR。sleep()在收到信号后也会提前返回,返回值为剩余秒数。这些“被打断”的行为早期被视为烦人,但在设计事件循环时反而是特性:你可以用信号来唤起一个空闲的 worker 去做清理、重新读取配置、或者检查外部状态。

写网络服务时常见的EINTR处理套路是这样的:read()返回 -1 且errno == EINTR时,不要当错误处理,直接重试一次即可。这也是SA_RESTART标志为什么受欢迎——它帮你把很多系统调用自动重启了,省去手写重试逻辑。但这并不意味着要无脑加SA_RESTART,有些场景下你恰恰希望read()能被信号打断,比如让阻塞读超时返回,这时候就不应该设置这个标志。

7.3 信号处理函数里到底能干什么

这是信号编程里最“劝退”的一部分,但也是面试最爱的追问点。信号处理函数运行在被打断的上下文中,不是正常的函数调用流程,它回去之后还要继续执行原来的代码。如果处理函数里调用了一个非异步信号安全的函数,而这个函数内部又一次访问了被中断的资源,死锁和缓冲区错乱是分分钟的事。

官方认可的 async-signal-safe 函数在man signal-safety里有完整清单。基本原则是:处理函数里优先用write()做简单输出,用_exit()做立即退出,用sig_atomic_t类型变量配合信号完成“值传递”(比如置一个全局标志,主循环里轮询这个标志位再做事)。printf、malloc、lock这类函数,一律不要碰。我见过有人在信号处理函数里free一个全局指针,程序跑满一天,在某个信号到达的瞬间直接段错误。这类问题极难复现,往往线上先崩、本地怎么测都测不出来。

8. 实战排查:信号相关的常见问题和避坑清单

8.1 程序莫名其妙退出,查不到日志

服务退出了但日志里没有异常信息,第一反应应该看是不是被信号终止的。shell 里如果进程是被信号杀死的,会提示Killed或Segmentation fault,但如果用systemd托管,打印结果就不那么直观。此时echo $?并不够用,更好的方式是查看进程退出码的 shell 语义:比如 128+信号编号,137= 128 + 9,表示被SIGKILL杀掉;143= 128 + 15,表示被SIGTERM终止。这个规律值得记牢,排查时一眼就能定位到信号类别。

如果是在 Java 或 Python 服务里,进程退出的同时还会打印signal相关的提示位,但很多语言运行时会先把常用信号劫持走,间接导致你以为“信号没生效”,反而不容易定位。排查这类问题的第一步,永远是确认目标进程收到的最后一个信号到底是什么。用strace -f -e trace=signal挂上去,或者gdb跟一下信号处理流程,比瞎猜高效得多。

8.2 SIGPIPE 把网络服务干掉了

这是新写网络服务最容易中的招。服务端往一个已经关闭的 socket 上写数据,内核会发SIGPIPE,默认动作是终止进程。日志里没有任何打印,进程就“消失”了,而且经常是在高并发压测时出现,非常像资源耗尽的假象。

标准解法就是服务启动时执行一句signal(SIGPIPE, SIG_IGN),然后自己处理和EPIPE相关的错误码。注意,这里用sigaction时不要加SA_RESTART,因为一次send()触发EPIPE后,需要业务层感知连接已经断开并主动清理连接,而不是被自动重启后继续往死链上写。我实际排过这个故障,服务进程一夜之间被重启了 N 次,加监控才看出是SIGPIPE干的。

8.3 僵尸进程为什么阴魂不散

父进程没有及时wait()子进程,子进程退出后就会进入僵尸状态,ps里能看到一堆<defunct>。常规的解法确实是注册SIGCHLD处理函数,在里面用waitpid(-1, &status, WNOHANG)循环回收。但有一种情况比较尴尬:如果父进程自己也是个被托管的服务,托管方对信号有一套自己的回收逻辑,你再注册一套SIGCHLD处理,两套回收逻辑会打架。

这种情况下,最稳妥的是在服务框架层统一处理——接收SIGCHLD后只做一次全局回收,或者干脆交给托管框架内部waitpid逻辑。用 docker 跑后台任务时,1 号进程如果不是专门的 init 程序,也可能因为不回收孙进程而产生大量僵尸。我的经验是:能用 init 系统解决的就别在业务代码里玩太多信号技巧;必须自己处理时,确保整个进程里只有一个人负责“收尸”。

8.4 多线程程序里的信号屏蔽陷阱

线程库(NPTL)下,信号处理是“进程级注册、线程级屏蔽”。同一进程里某个线程用pthread_sigmask屏蔽了SIGUSR1,另一个线程不屏蔽,那么pthread_kill指定发给那个不屏蔽的线程时,信号照样会被处理。但如果用kill发给进程,信号的归属线程由内核挑一个未屏蔽它的线程,具体谁处理不可预测。

这个特性经常引出隐蔽的并发 bug。比如主线程里pthread_sigmask把SIGINT屏蔽了,子线程没屏蔽,用户按Ctrl+C时,信号可能被任意一个没有屏蔽它的线程处理,而不是你想的那个。所以多线程程序里处理信号的标准姿势是:统一在主线程里屏蔽所有想统一处理的异步信号,配合sigwait()用同步方式接收信号;或者每个线程各自处理自己关注的那部分信号,并严格控制发送对象。两条路都行,但混着用就是给自己挖坑。

9. 关于信号处理代码风格的几点个人体会

这一节不算“新知识”,但我特别想强调一下,因为我见过太多人学会了 API 却在工程里写出一堆难维护的信号代码。首先,永远不要假设信号处理函数会在哪条执行流上运行。它在哪个线程、打断了哪段代码,都不受你控制。于是,任何需要锁保护的全局状态,在信号处理函数里碰都别碰;真需要传数据,用sig_atomic_t或者精心设计的无锁队列。其次,把处理函数做得越小越好,最理想的情况是做个标记就回来,所有重活放到主循环里慢慢处理。这叫做“延迟处理”,能避开九成以上的信号相关 bug。

另外,处理函数里要避免调用longjmp或者siglongjmp。虽然标准允许在有严格约束时使用,但我实际操作中见过有人用它从信号处理函数跳出去绕过清理逻辑,最后资源泄漏得莫名其妙。

最后留一个小技巧:调试信号问题最顺手的三件套是strace、gdb、/proc/<pid>/status。strace看系统调用层面信号在哪里产生、有没有EINTR;gdb用handle SIGxxx print可以在信号送达进程前拦截它,直接看信号参数;/proc则适合短时间内快照进程的屏蔽和未决状态。这三招配起来,大多数信号问题都能顺藤摸瓜找到源头。

进程信号这块内容很多,我这篇先把整体框架和关键机制捋清楚。信号的安全函数、竞争状态、与线程的协作、以及setjmp/longjmp这类偏底层的奇技淫巧,放在下一篇继续拆。看完这篇,建议你现在就打开终端,用man signal、man sigaction、kill -l把环境和资料先翻一翻,然后用上面几个小实验亲手验证一遍。动手跑过一轮之后,信号在你心里就不再是“玄学”,而是 Linux 下最直接的异步通知机制了。

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

插件机制解析:从架构原理到failed to load plugins排查实战

写这篇东西的起因挺简单&#xff1a;前阵子帮朋友排查一个工具链启动就报错的问题&#xff0c;控制台翻来覆去就一句话——failed to load plugins&#xff0c;后面还跟着 web boot、entries did not activate 之类的提示。折腾了大半天&#xff0c;最后发现根因就是某个插件包…

作者头像 李华
网站建设 2026/10/4 23:48:51

MATLAB心音分类实战:从信号预处理到分类器训练

心音分类这个项目我断断续续做了两周多&#xff0c;最开始纯粹是被一段异常心音录音勾起了兴趣——那“咕咚、咕咚”的节律里藏着一点多余的杂音&#xff0c;人耳能听出来不对劲&#xff0c;但要说清楚到底哪类问题&#xff0c;得靠专业医生。于是我就想&#xff0c;能不能用MA…

作者头像 李华
网站建设 2026/10/4 23:42:50

如何调试matchMedia.js?官方测试页与JSLitmus性能基准完全指南

如何调试matchMedia.js&#xff1f;官方测试页与JSLitmus性能基准完全指南 【免费下载链接】matchMedia.js matchMedia polyfill for testing media queries in JS 项目地址: https://gitcode.com/gh_mirrors/ma/matchMedia.js matchMedia.js 是一个经典的 JavaScript p…

作者头像 李华
网站建设 2026/10/4 23:39:46

Qt 炫酷曲线,图表,2D/3D开源库

&#x1f7e2;QCustomPlot轻量级首选&#xff0c;文档友好上手快&#xff0c;画普通的折线图柱状图完全够用&#xff0c;不需要额外依赖&#xff0c;小项目用它效率超高 &#x1f7e1;Qwt工业级老选手了&#xff0c;性能稳定功能全&#xff0c;做工控、仪表类的界面选它准没错&…

作者头像 李华
网站建设 2026/10/4 23:32:55

华硕路由器变身AI边缘网关:提示流编排器部署实战

先说结论&#xff1a;这篇文章讲的不是把一个大模型权重塞进华硕路由器——那不可能&#xff0c;任何一台家用路由器的闪存和内存都装不下几 B 甚至几十 B 的参数。真正落地的是把AI 提示流编排器这种"大脑调度层"搬到路由器上&#xff0c;做一个轻量边缘网关&#x…

作者头像 李华