信号基础:信号的概念、常见信号与 signal() 函数

信号,说白了就是操作系统给进程发的一种“软中断”。

我刚开始学 Linux 编程时,总觉得信号这东西挺玄乎。后来在嵌入式项目中调试一个串口程序,发现按 Ctrl+C 杀不掉进程,才真正体会到——不懂信号,你连怎么优雅地退出程序都做不到。

今天我们就来聊聊信号的基础知识。包括信号是什么、有哪些常见信号、以及怎么用 signal() 函数去处理它们。

信号是什么?

信号是一种异步事件通知机制。内核或者某个进程,可以向另一个进程发送信号。收到信号的进程,可以选择忽略、执行默认动作、或者调用自定义的处理函数。

你想想看,进程正在跑着,突然收到一个信号,就像你正在写代码,突然有人拍你肩膀——你得决定是继续写、还是回头看一眼、还是直接关机走人。

核心要点:信号是异步的。进程不知道信号什么时候来,只能提前注册好处理函数。

常见信号一览

Linux 系统定义了数十种信号。我挑几个最常用的,也是面试和项目中经常遇到的,给你列出来:

信号名 编号 默认动作 说明
SIGINT 2 终止进程 终端中断,通常由 Ctrl+C 触发
SIGKILL 9 终止进程 强制杀死,不能被捕获或忽略
SIGSEGV 11 终止进程(core dump) 段错误,非法内存访问
SIGTERM 15 终止进程 软件终止信号,可被捕获
SIGUSR1 10 终止进程 用户自定义信号1
SIGCHLD 17 忽略 子进程状态改变时发送给父进程

这里我要特别提一下 SIGKILLSIGSTOP(编号 19)。这两个信号,你没法用 signal() 去捕获或忽略。为什么?因为系统要保证管理员总有办法杀掉一个失控的进程。我曾经在项目里遇到一个死循环的守护进程,普通信号发过去没反应,最后就是靠 kill -9 解决的。

注意:不要试图捕获 SIGKILL 和 SIGSTOP。这是内核的底线,你绕不过去。

signal() 函数的使用

处理信号,最基础的函数就是 signal()。它的原型很简单:

#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

嗯,这个声明看着有点绕。我习惯把它拆开理解:

  • signum:你要处理的信号编号或宏名
  • handler:一个函数指针,指向你的信号处理函数
  • 返回值:之前的信号处理函数指针

说白了,就是告诉内核:“嘿,当这个信号来了,你帮我调用这个函数。”

一个简单的例子

我们写一个程序,捕获 SIGINT(Ctrl+C),然后打印一条消息,而不是直接退出:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int sig) {
    printf("\n收到 SIGINT 信号!按 Ctrl+C 也没用,我偏不退出。\n");
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, sigint_handler);

    printf("程序运行中... 按 Ctrl+C 试试看。\n");

    // 让程序一直跑着
    while (1) {
        sleep(1);
    }

    return 0;
}

运行这个程序,你按 Ctrl+C,它不会退出,而是打印一条消息。嗯,这里要注意:printf() 在信号处理函数中其实不是完全安全的,但为了演示,我们先用着。

个人建议:信号处理函数里尽量做简单的事。比如设置一个全局标志位,然后在主循环里检查这个标志位。我在项目中吃过亏,在信号处理函数里调用了 malloc(),结果死锁了。信号处理函数不是普通函数,它可能打断任何代码的执行。

信号处理函数的注意事项

signal() 有几个坑,我帮你列出来:

  • 可重入性:信号处理函数必须是可重入的。不要调用非可重入函数,比如 malloc()printf()fopen() 等。
  • 全局变量:如果要在信号处理函数和主程序之间共享变量,记得用 volatile sig_atomic_t 类型。这个类型保证读写是原子的。
  • 信号屏蔽:在处理一个信号时,同类型的信号会被自动屏蔽,防止嵌套。但其他信号仍可能打断当前处理函数。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t flag = 0;

void handler(int sig) {
    flag = 1;  // 只做一件事:设置标志位
}

int main() {
    signal(SIGINT, handler);

    while (!flag) {
        // 主循环做自己的事
        sleep(1);
    }

    printf("收到信号,准备退出。\n");
    return 0;
}

这个写法就安全多了。信号处理函数只设置一个标志位,主循环去检查它。我在实际项目中,处理 SIGTERM 和 SIGINT 时,基本都是这个模式。

信号与系统接口的关系

信号不是孤立存在的。它和很多系统调用有交互。比如:

  • pause():让进程挂起,直到收到一个信号
  • alarm():定时发送 SIGALRM 信号
  • kill():向指定进程发送信号
  • raise():向自己发送信号

我记得有一次调试一个网络程序,客户端连上后一直没反应。后来发现是服务端调用了 pause() 等待信号,但信号处理函数没注册好,导致进程永远挂在那。嗯,这种问题排查起来挺费劲的。

SVG 知识结构图

下面这张图,帮你理清信号相关的核心概念:

信号基础 信号概念 • 异步事件通知机制 • 软中断 • 内核→进程 或 进程→进程 常见信号 • SIGINT (2) — Ctrl+C • SIGKILL (9) — 强制杀死 • SIGSEGV (11) — 段错误 • SIGTERM (15) — 软件终止 signal() 函数 • 注册信号处理函数 • 可捕获/忽略/默认 • 注意可重入性 避坑:SIGKILL/SIGSTOP 不可捕获 信号处理函数中避免调用非可重入函数

总结

信号是 Linux 编程中绕不开的基础知识。你不需要记住所有信号,但 SIGINT、SIGKILL、SIGSEGV、SIGTERM 这几个,必须烂熟于心。

signal() 函数虽然简单,但用起来有讲究。记住:信号处理函数里别干重活,设个标志位就够了。如果你想要更精细的控制,后面可以学 sigaction(),它比 signal() 更强大、更可靠。

嗯,今天就聊到这里。信号这东西,多写几个 demo 就熟了。


公众号:蓝海资料掘金营,微信 deep3321