在 Linux 系统编程的面试中,信号(Signal)、信号量(Semaphore)以及信号处理函数(Signal Handler)是高频考点,也是容易暴露候选人基本功是否扎实的领域。很多候选人对基本概念对答如流,但在追问细节时却频频踩坑。本文梳理了这三类知识点中常见的面试陷阱,帮助你查漏补缺。
一、信号处理函数:异步安全是最大的坑
陷阱 1:在信号处理函数中调用非异步信号安全函数
这是面试中出现频率最高的问题之一。面试官可能会问:“在信号处理函数中调用 printf 会有什么问题?”
答案是:printf 不是异步信号安全的(async-signal-safe)。它内部使用了锁和全局缓冲区,如果信号在主程序持有 stdout 锁时到达,处理函数中再次调用 printf 就可能导致死锁或输出混乱。
POSIX 标准明确列出了一些异步信号安全的函数,如 write、read、open、close、_exit、signal、sigaction 等。而 printf、malloc、free、syslog 等都不在其中。
正确做法:在信号处理函数中只使用异步信号安全的函数,通常的做法是设置一个 volatile sig_atomic_t 类型的标志位,由主循环检测后处理。
volatile sig_atomic_t got_sigint = 0;
void handler(int sig) {
got_sigint = 1;
}
int main(void) {
struct sigaction sa = { .sa_handler = handler };
sigaction(SIGINT, &sa, NULL);
while (!got_sigint) {
/* 正常主循环 */
}
write(STDOUT_FILENO, "Caught SIGINT\n", 14);
return 0;
}
陷阱 2:volatile sig_atomic_t 不等于原子操作
很多候选人知道要用 volatile sig_atomic_t,但面试官会进一步追问:“volatile 和 sig_atomic_t 分别解决什么问题?”
volatile告诉编译器不要优化对该变量的读写,每次都必须从内存加载。sig_atomic_t保证对该类型的读写是原子的(不可分割的)。
但注意,这仅保证单次读写是原子的。如果涉及复合操作(如 counter++),即使类型是 sig_atomic_t,在多线程或信号处理场景下仍然可能出问题。
陷阱 3:信号处理函数的可重入性
面试官常问:“信号处理函数可以被同一个信号再次中断吗?”
默认情况下,当某个信号的处理函数正在执行时,该信号会被自动阻塞(除非使用了 SA_NODEFER 标志)。但其他信号仍然可能中断当前处理函数。如果处理函数访问了共享数据,就可能产生竞态条件。
解决方案是使用 sigaction 的 sa_mask 字段,明确指定在处理函数执行期间需要额外阻塞哪些信号。
陷阱 4:signal() 与 sigaction() 的差异
面试中经常被问到:“为什么推荐使用 sigaction() 而不是 signal()?”
关键区别在于:
signal()的行为在不同 Unix 系统间存在差异,尤其是信号处理函数执行后是否重置为默认行为(System V 会重置,BSD 不会)。sigaction()提供了更精细的控制,包括信号屏蔽、标志位(如SA_RESTART、SA_SIGINFO)等。sigaction()可以获取和设置更详细的信号信息,支持实时信号。
二、信号量:PV 操作与竞态条件
陷阱 5:混淆 System V 信号量与 POSIX 信号量
面试官可能会问:“Linux 中有几种信号量?它们有什么区别?”
- System V 信号量:通过
semget、semop、semctl操作,支持信号量集,但接口复杂,生命周期与内核绑定。 - POSIX 无名信号量:通过
sem_init、sem_wait、sem_post操作,位于内存中,适用于线程间同步。 - POSIX 有名信号量:通过
sem_open、sem_wait、sem_post操作,位于/dev/shm,适用于进程间同步。
候选人常犯的错误是将 sem_wait 和 sem_post 与 System V 的 semop 混为一谈,或者在需要进程间同步时错误地使用了无名信号量。
陷阱 6:sem_wait 被信号中断
这是一个非常隐蔽的坑。sem_wait 是一个慢系统调用,如果进程在等待信号量时接收到信号,sem_wait 会返回 -1 并将 errno 设置为 EINTR。
如果调用者没有处理 EINTR,就会误以为信号量操作失败。正确的做法是循环重试:
while (sem_wait(&sem) == -1) {
if (errno != EINTR) {
perror("sem_wait");
break;
}
}
面试官问这个问题,往往是在考察候选人是否真正写过健壮的多线程/多进程代码。
陷阱 7:信号量的值是“计数器”而非“锁”
面试官可能会问:“信号量初始值为 1 和初始值为 0 分别代表什么?”
- 初始值为 1:相当于互斥锁,第一个
sem_wait成功,后续阻塞。 - 初始值为 0:通常用于同步,等待某个事件发生后由另一方
sem_post唤醒。
很多候选人会忽略信号量的计数特性,将其简单等同于互斥锁。实际上,信号量可以允许多个线程同时访问(初始值 > 1),这是与互斥锁的本质区别。
三、信号与信号量的交叉陷阱
陷阱 8:用信号量实现信号处理函数的同步
有些候选人会提出:“在信号处理函数中 sem_post,在主程序中 sem_wait 来同步。”
这里有几个问题:
sem_post是异步信号安全的,这一点没问题。- 但
sem_wait在主程序中可能被其他信号中断,需要处理EINTR。 - 更关键的是,如果信号在
sem_wait之前多次到达,信号量会累积计数,可能导致主程序多次处理同一个逻辑事件。
面试官通常会追问:“如果信号在 sem_wait 之前到达了两次,会发生什么?”这考察的是候选人对信号量计数语义的理解。
陷阱 9:SIGCHLD 与信号量的配合
在处理子进程退出时,常见做法是在 SIGCHLD 处理函数中 sem_post,主程序 sem_wait 后调用 waitpid。
但这里有一个经典陷阱:多个子进程同时退出时,SIGCHLD 可能被合并。标准信号不排队,如果两个子进程几乎同时退出,可能只收到一个 SIGCHLD。因此,正确的做法是在 SIGCHLD 处理函数中循环调用 waitpid(-1, ..., WNOHANG),直到没有更多子进程可回收。
四、面试应对建议
- 不要只背概念:面试官更看重你是否理解“为什么”。比如为什么
printf不能在信号处理函数中用,为什么sem_wait要处理EINTR。 - 动手写代码:很多陷阱只有真正写过代码才会遇到。建议自己写一个信号处理的小程序,用
strace观察系统调用行为。 - 关注可重入性和原子性:这是信号相关问题的核心。任何涉及共享数据的操作,都要问自己:“如果此时来一个信号,会发生什么?”
- 区分相似概念:信号 vs 信号量、
signalvssigaction、System V vs POSIX 信号量,这些对比是面试中的常客。
总结
信号、信号量和信号处理函数的陷阱,本质上都围绕三个核心问题:异步安全、竞态条件和可重入性。掌握这些,不仅能帮你通过面试,更能让你在实际开发中写出更健壮的 Linux 系统程序。建议在复习时多问自己“如果……会怎样”,这种思维方式比死记硬背答案有效得多。
未经允许不得转载:任鹏个人博客 » Linux 面试题:信号、信号量与信号处理函数的常见陷阱

