Linux 面试题:信号、信号量与信号处理函数的常见陷阱

在 Linux 系统编程的面试中,信号(Signal)、信号量(Semaphore)以及信号处理函数(Signal Handler)是高频考点,也是容易暴露候选人基本功是否扎实的领域。很多候选人对基本概念对答如流,但在追问细节时却频频踩坑。本文梳理了这三类知识点中常见的面试陷阱,帮助你查漏补缺。

一、信号处理函数:异步安全是最大的坑

陷阱 1:在信号处理函数中调用非异步信号安全函数

这是面试中出现频率最高的问题之一。面试官可能会问:“在信号处理函数中调用 printf 会有什么问题?”

答案是:printf 不是异步信号安全的(async-signal-safe)。它内部使用了锁和全局缓冲区,如果信号在主程序持有 stdout 锁时到达,处理函数中再次调用 printf 就可能导致死锁或输出混乱。

POSIX 标准明确列出了一些异步信号安全的函数,如 writereadopenclose_exitsignalsigaction 等。而 printfmallocfreesyslog 等都不在其中。

正确做法:在信号处理函数中只使用异步信号安全的函数,通常的做法是设置一个 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,但面试官会进一步追问:“volatilesig_atomic_t 分别解决什么问题?”

  • volatile 告诉编译器不要优化对该变量的读写,每次都必须从内存加载。
  • sig_atomic_t 保证对该类型的读写是原子的(不可分割的)。

但注意,这仅保证单次读写是原子的。如果涉及复合操作(如 counter++),即使类型是 sig_atomic_t,在多线程或信号处理场景下仍然可能出问题。

陷阱 3:信号处理函数的可重入性

面试官常问:“信号处理函数可以被同一个信号再次中断吗?”

默认情况下,当某个信号的处理函数正在执行时,该信号会被自动阻塞(除非使用了 SA_NODEFER 标志)。但其他信号仍然可能中断当前处理函数。如果处理函数访问了共享数据,就可能产生竞态条件。

解决方案是使用 sigactionsa_mask 字段,明确指定在处理函数执行期间需要额外阻塞哪些信号。

陷阱 4:signal()sigaction() 的差异

面试中经常被问到:“为什么推荐使用 sigaction() 而不是 signal()?”

关键区别在于:

  • signal() 的行为在不同 Unix 系统间存在差异,尤其是信号处理函数执行后是否重置为默认行为(System V 会重置,BSD 不会)。
  • sigaction() 提供了更精细的控制,包括信号屏蔽、标志位(如 SA_RESTARTSA_SIGINFO)等。
  • sigaction() 可以获取和设置更详细的信号信息,支持实时信号。

二、信号量:PV 操作与竞态条件

陷阱 5:混淆 System V 信号量与 POSIX 信号量

面试官可能会问:“Linux 中有几种信号量?它们有什么区别?”

  • System V 信号量:通过 semgetsemopsemctl 操作,支持信号量集,但接口复杂,生命周期与内核绑定。
  • POSIX 无名信号量:通过 sem_initsem_waitsem_post 操作,位于内存中,适用于线程间同步。
  • POSIX 有名信号量:通过 sem_opensem_waitsem_post 操作,位于 /dev/shm,适用于进程间同步。

候选人常犯的错误是将 sem_waitsem_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 来同步。”

这里有几个问题:

  1. sem_post 是异步信号安全的,这一点没问题。
  2. sem_wait 在主程序中可能被其他信号中断,需要处理 EINTR
  3. 更关键的是,如果信号在 sem_wait 之前多次到达,信号量会累积计数,可能导致主程序多次处理同一个逻辑事件。

面试官通常会追问:“如果信号在 sem_wait 之前到达了两次,会发生什么?”这考察的是候选人对信号量计数语义的理解。

陷阱 9:SIGCHLD 与信号量的配合

在处理子进程退出时,常见做法是在 SIGCHLD 处理函数中 sem_post,主程序 sem_wait 后调用 waitpid

但这里有一个经典陷阱:多个子进程同时退出时,SIGCHLD 可能被合并。标准信号不排队,如果两个子进程几乎同时退出,可能只收到一个 SIGCHLD。因此,正确的做法是在 SIGCHLD 处理函数中循环调用 waitpid(-1, ..., WNOHANG),直到没有更多子进程可回收。

四、面试应对建议

  1. 不要只背概念:面试官更看重你是否理解“为什么”。比如为什么 printf 不能在信号处理函数中用,为什么 sem_wait 要处理 EINTR
  2. 动手写代码:很多陷阱只有真正写过代码才会遇到。建议自己写一个信号处理的小程序,用 strace 观察系统调用行为。
  3. 关注可重入性和原子性:这是信号相关问题的核心。任何涉及共享数据的操作,都要问自己:“如果此时来一个信号,会发生什么?”
  4. 区分相似概念:信号 vs 信号量、signal vs sigaction、System V vs POSIX 信号量,这些对比是面试中的常客。

总结

信号、信号量和信号处理函数的陷阱,本质上都围绕三个核心问题:异步安全竞态条件可重入性。掌握这些,不仅能帮你通过面试,更能让你在实际开发中写出更健壮的 Linux 系统程序。建议在复习时多问自己“如果……会怎样”,这种思维方式比死记硬背答案有效得多。

未经允许不得转载:任鹏个人博客 » Linux 面试题:信号、信号量与信号处理函数的常见陷阱

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏