01-信号机制.md 48 KB


title: 信号机制 tags: [

嵌入式Linux,
Linux应用编程,
进程,
信号,
signal,
sigaction,
信号集,
阻塞,
实时信号,
IMX6ULL,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第八章 信号:基础"

信号机制

💡 关联知识:[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/03-进程间通信]]、[[02-进程线程与IPC/04-线程与线程同步]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/6. 信号机制]]

信号是 Linux 应用编程里处理异步事件的核心手段:进程正常跑着,某个事件(按键、除零、定时器超时、子进程退出)突然发生,内核用一个"编号"打断进程当前流程,通知它去应对。本篇从概念、分类、常见信号、默认行为一路讲到 signal / sigaction、发送信号、信号集、阻塞与等待,最后是实战测试、跨平台对比与面试题。

本章篇幅较长,因为信号"概念简单、细节很多":同一个信号在不同状态下行为不同,阻塞与未决(pending)是两个概念,实时信号又能排队。把这些厘清,才算真正会用信号。


1. 基本概念

1.1 信号是什么

信号是事件发生时对进程的通知机制,也可以称为软件中断。它与硬件中断的相似之处在于:能够打断程序当前执行的正常流程,是软件层次上对中断机制的一种模拟。大多数情况下无法预测信号到达的准确时间,所以信号提供了一种处理异步事件的方法。

几个关键定性:

  • 信号是用来通信的:一个具有合适权限的进程能向另一个进程发送信号,这可以作为一种同步技术,甚至是进程间通信(IPC)的原始形式。
  • 信号由谁处理、怎么处理:信号通常发送给对应进程,进程收到后执行以下操作之一——忽略信号捕获信号(执行预先绑定的处理函数)、执行系统默认操作
  • 信号是异步的:产生信号的事件对进程而言是随机出现的,进程不能靠"测试一个变量"或简单系统调用来判断是否产生了信号,只能被打断后被动进入处理流程。
  • 信号本质上是 int 类型数字编号:内核为每个信号定义了唯一整数编号,从 1 开始顺序展开;每个信号都有对应名字(宏),程序里应使用符号名而非编号,因为编号可能因系统而异。

两种信号绝不能被忽略SIGKILLSIGSTOP。原因是它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,进程的运行行为是未定义的。

1.2 信号从哪里来

来源 说明 例子
硬件异常 硬件检测到错误条件通知内核,内核再发给相关进程 除数为 0、数组越界引用无法访问的内存
终端特殊字符 终端下输入产生信号的字符 Ctrl+CSIGINTCtrl+\SIGQUITCtrl+ZSIGTSTP
kill() 系统调用 进程把任意信号发给另一个进程或进程组 受限:收发双方所有者相同,或发送者是 root
kill 命令 用户在终端向其它进程发信号,内部就是 kill() kill -9 xxx 杀死 PID 为 xxx 的进程
软件事件 检测到某种软件条件已发生 定时器超时、CPU 时间超限、子进程退出
进程自身 进程可以给自己发信号 raise()abort()

绝大多数到达进程的信号来自内核。

1.3 信号处理流程图

flowchart TD
    A[事件发生] --> B{信号来源}
    B -->|硬件异常| C[内核]
    B -->|终端 Ctrl+C / Ctrl+Z| C
    B -->|kill 系统调用 / kill 命令| C
    B -->|软件事件: 定时器 / 子进程退出| C
    C --> D[内核向目标进程投递信号]
    D --> E{信号是否在进程信号掩码中?}
    E -->|是: 阻塞| F[信号置为未决 pending]
    F --> G[解除阻塞后重新投递]
    G --> H{进程设置的处理方式}
    E -->|否| H
    H -->|SIG_IGN 忽略| I[丢弃, 无任何影响]
    H -->|捕获: 自定义处理函数| J[执行信号处理函数]
    H -->|SIG_DFL 系统默认| K[执行默认操作: term/ core/ ignore/ stop/ cont]
    J --> L[返回被中断处继续执行]

1.4 程序启动与进程创建时的处理方式

  • 程序启动:应用刚启动(或代码还没执行到 signal())时,进程对所有信号的处理方式都是系统默认操作。所以不调用 signal() 的程序,Ctrl+C 会按默认操作终止进程。
  • 进程创建:父进程调用 fork() 创建子进程时,子进程继承父进程的信号处理方式(子进程复制父进程内存映像,信号捕获函数地址在子进程中有意义)。

2. 信号的分类

Linux 下从两个角度分类:可靠性(可靠信号 / 不可靠信号)与实时性(实时信号 / 非实时信号)。

2.1 可靠信号与不可靠信号

早期 UNIX 信号机制简单原始,问题主要有两个:

  • 进程每次处理信号后,就把对该信号的响应设置为系统默认操作,用户若不想这样,就得在处理函数结尾再调用一次 signal() 重新绑定。
  • 于是"不可靠信号"主要指的是:进程可能对信号做出错误反应,以及信号可能丢失(处理信号时又来了新信号,导致丢失)。

Linux 支持不可靠信号,但做了改进:调用完信号处理函数后不必重新调用 signal()。因此 Linux 下的不可靠信号问题主要就是"信号可能丢失"。

  • 信号值 小于 SIGRTMIN(34) 的(即 1~31)都是不可靠信号。
  • 新增的 SIGRTMIN~SIGRTMAX(34~64)被定义为可靠信号:支持排队、不会丢失;发送函数为 sigqueue(),绑定函数为 sigaction()

2.2 实时信号与非实时信号

实时性分类与可靠性分类相互对应:

  • 非实时信号都不支持排队,都是不可靠信号;一般也称为标准信号
  • 实时信号都支持排队,都是可靠信号;它保证了发送的多个信号都能被接收,是 POSIX 标准的一部分。

    flowchart LR
    subgraph STD["标准信号 1~31"]
        S1[不可靠]
        S2[不支持排队]
        S3[信号可能丢失]
    end
    subgraph RT["实时信号 34~64"]
        R1[可靠]
        R2[支持排队, 多次发送多次传递]
        R3[可携带伴随数据]
    end
    

使用 kill -l 可以查看系统所有信号:编号 1~31 是不可靠信号,34~64 是可靠信号,可靠信号没有具体名字,用 SIGRTMIN+N / SIGRTMAX-N 表示。


3. 常见信号与默认行为

标准信号(1~31)中,每个信号都有确定的用途、含义以及系统默认操作。

信号名称 编号 描述 系统默认操作
SIGINT 2 终端中断符(Ctrl+C term
SIGQUIT 3 终端退出符(Ctrl+\ term + core
SIGILL 4 非法硬件指令 term + core
SIGABRT 6 异常终止(abort() term + core
SIGBUS 7 内存访问错误 term + core
SIGFPE 8 算术异常 term + core
SIGKILL 9 终极终止信号(不可阻塞/忽略/捕获) term
SIGUSR1 10 用户自定义信号 1 term
SIGSEGV 11 无效的内存引用 term + core
SIGUSR2 12 用户自定义信号 2 term
SIGPIPE 13 管道关闭 term
SIGALRM 14 定时器超时(alarm) term
SIGTERM 15 终止进程(kill 默认信号) term
SIGCHLD / SIGCLD 17 子进程终止或停止 ignore
SIGCONT 18 使停止状态的进程继续运行 cont
SIGSTOP 19 停止进程(不可阻塞/忽略/捕获) stop
SIGTSTP 20 终端停止符(Ctrl+Z stop
SIGXCPU 24 超过 CPU 限制 term + core
SIGVTALRM 26 虚拟定时器超时 term
SIGWINCH 28 终端窗口尺寸发生变化 ignore
SIGPOLL / SIGIO 29 异步 I/O term / ignore
SIGSYS 31 无效系统调用 term + core

默认操作缩写term 终止进程;core 生成核心转储文件;ignore 忽略信号;cont 继续运行进程;stop 停止进程(注意停止 ≠ 终止,而是暂停)。

3.1 重点信号逐条说明

  • SIGINT:用户在终端按下中断字符(通常 Ctrl+C),内核发给前台进程组中每个进程。默认终止进程——这就是平时 Ctrl+C 能杀掉前台进程的原因。
  • SIGQUIT:按下退出字符(通常 Ctrl+\),内核发给前台进程组中每个进程。默认终止进程并生成核心转储文件。进程陷入无限循环、不再响应时用它比较合适。
  • SIGKILL必杀信号,无法被阻塞、忽略或捕获,总能终止进程。当 SIGINT / SIGQUIT 都杀不掉时才动它,kill -9 xxx-9 就是它。
  • SIGSEGV:无效内存引用时发送,常见原因是解引用了含错误地址的指针(如未初始化指针)或传了无效参数。默认终止进程。
  • SIGPIPE:向已关闭的管道、FIFO 或套接字写入时产生。默认终止进程。
  • SIGALRM:与 alarm()setitimer() 有关,定时器到时内核发它给应用。
  • SIGTERM终止进程的标准信号,也是 kill 命令的默认信号。精心设计的应用应捕获 SIGTERM 并绑定处理函数,在函数里清理临时文件、释放资源再退出。直接 kill -9 会跳过这些处理,是暴力方式,应作为最后手段。
  • SIGCHLD:父进程的某个子进程终止时内核发给父进程;子进程因信号停止或恢复时也可能发送(停止指暂停,不是终止)。默认忽略;父进程若想被告知子进程状态改变,应捕获它。
  • SIGCONT:发给已停止的进程使其恢复运行。进程不在停止态时默认忽略;在停止态时默认使其继续运行。
  • SIGSTOP必停信号,无法忽略或捕获,总能停止进程(停止只是暂停,进程没有终止)。
  • SIGTSTP:终端停止字符(通常 Ctrl+Z),内核发给前台进程组中每个进程使其停止。
  • SIGUSR1 / SIGUSR2:供程序员自定义使用,内核绝不会主动为进程产生它们,可用于进程间通知事件或同步。
  • SIGHUP:用户准备退出会话时,系统向会话发 SIGHUP,会话再发给所有子进程,子进程收到后默认终止。详见「守护进程」相关章节。

4. 设置信号处理方式

Linux 提供 signal()sigaction() 两个接口设置处理方式,推荐使用后者。

4.1 signal()

#include <signal.h>

typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
参数 含义
signum 需要设置的信号,可使用信号名(宏)或数字编号,建议用信号名
handler sig_t 类型函数指针:自定义处理函数(捕获)、SIG_IGN(忽略)、SIG_DFL(系统默认)
返回值 含义
成功 返回指向此前信号处理函数的指针
失败 返回 SIG_ERR,并设置 errno

SIG_IGNSIG_DFL 的定义:

/* Fake signal functions. */
#define SIG_ERR ((sig_t) -1)   /* Error return. */
#define SIG_DFL ((sig_t) 0)    /* Default action. */
#define SIG_IGN ((sig_t) 1)    /* Ignore signal. */

处理函数的 int 参数就是当前触发的信号,用于把多个信号绑到同一个处理函数时区分来源。

4.2 sigaction()

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数 含义
signum 需要设置的信号(SIGKILLSIGSTOP 除外)
act 指向 struct sigaction,描述新处理方式;为 NULL 表示不改变当前处理方式
oldact 不为 NULL 时,返回信号之前的处理方式;不关心可传 NULL
返回值 含义
成功 返回 0
失败 返回 -1,并设置 errno

sigaction() 允许单独获取处理函数而不设置,还可以通过标志精细控制调用处理函数时的行为,因此灵活性与可移植性更好。

4.3 struct sigaction

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
成员 含义
sa_handler 指定信号处理函数,与 signal()handler 相同
sa_sigaction 替代处理函数,能获取更多信息(通过 siginfo_t)。与 sa_handler 互斥,通过 SA_SIGINFO 标志选择
sa_mask 执行处理函数前,把这组信号加入进程信号掩码;处理函数返回后恢复。进入处理函数时,当前处理的信号会被自动加入掩码,保证同种信号不重入
sa_restorer 已过时,不要使用
sa_flags 一组控制标志,多个用 \| 组合

sa_flags 可用标志:

标志 含义
SA_NOCLDSTOP signumSIGCHLD,子进程停止(收到 SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU)或恢复(收到 SIGCONT)时不收 SIGCHLD
SA_NOCLDWAIT signumSIGCHLD,子进程终止时不转为僵尸进程
SA_NODEFER 不阻塞从自身处理函数中接收的此信号(默认会阻塞同种信号以避免竞态)
SA_RESETHAND 执行完处理函数后,把处理方式重置为系统默认操作
SA_RESTART 被信号中断的系统调用,在信号处理完成后自动重新发起
SA_SIGINFO 使用 sa_sigaction 而非 sa_handler

4.4 siginfo_t

siginfo_t {
    int       si_signo;     /* Signal number */
    int       si_errno;     /* An errno value */
    int       si_code;      /* Signal code */
    int       si_trapno;    /* Trap number that caused hardware-generated signal */
    pid_t     si_pid;       /* Sending process ID */
    uid_t     si_uid;       /* Real user ID of sending process */
    int       si_status;    /* Exit value or signal */
    clock_t   si_utime;     /* User time consumed */
    clock_t   si_stime;     /* System time consumed */
    sigval_t  si_value;     /* Signal value */
    int       si_int;       /* POSIX.1b signal */
    void     *si_ptr;       /* POSIX.1b signal */
    int       si_overrun;   /* Timer overrun count; POSIX.1b timers */
    int       si_timerid;   /* Timer ID; POSIX.1b timers */
    void     *si_addr;      /* Memory location which caused fault */
    long      si_band;      /* Band event */
    int       si_fd;        /* File descriptor */
    short     si_addr_lsb;  /* Least significant bit of address */
    void     *si_call_addr; /* Address of system call instruction */
    int       si_syscall;   /* Number of attempted system call */
    unsigned int si_arch;   /* Architecture of attempted system call */
}

实际使用时用 man sigaction 查阅即可,重点字段是 si_signosi_codesi_pidsi_value

4.5 信号处理函数说明

一般而言,信号处理函数设计得越简单越好,道理和中断处理函数一样:越快越好,不要做大量消耗 CPU 时间的事情。重要原因是:越简单,引发信号竞争条件(竞态)的风险越低。


5. 向进程发送信号

5.1 kill()

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
参数 含义
pid 目标进程/进程组,取值见下表
sig 需要发送的信号;为 0 时不发送信号,但仍执行错误检查,可用于判断进程是否存在
返回值 含义
成功 返回 0
失败 返回 -1,并设置 errno(如 ESRCH 表示进程不存在)

pid 不同取值含义:

pid 取值 含义
pid > 0 发送给 pid 指定的进程
pid == 0 发送给当前进程所在进程组中的每个进程
pid == -1 发送给当前进程有权发送信号的每个进程,但进程 1(init)除外
pid < -1 发送给 ID 为 -pid 的进程组中的每个进程

权限规则:root 进程可以把信号发给任何进程;普通用户进程的基本规则是发送者进程的实际/有效用户 ID 必须等于接收者进程的实际/有效用户 ID

killpg() 也可用于向进程组发送信号;raise() 则是向自身发送信号。

5.2 raise()

#include <signal.h>

int raise(int sig);
参数 含义
sig 需要发送的信号
返回值 含义
成功 返回 0
失败 返回非零值

raise() 等价于 kill(getpid(), sig);,其中 getpid() 用于获取进程自身 PID。

5.3 sigqueue()(实时信号)

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);
参数 含义
pid 接收信号的进程 PID
sig 需要发送的信号;为 0 时用于检查进程是否存在
value 信号的伴随数据,union sigval 类型
返回值 含义
成功 返回 0
失败 返回 -1,并设置 errno
typedef union sigval {
    int sival_int;
    void *sival_ptr;
} sigval_t;

伴随数据既可以是整型,也可以是指针。


6. alarm() 与 pause()

6.1 alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds);
参数 含义
seconds 定时秒数;为 0 表示取消之前设置的 alarm 闹钟
返回值 含义
成功 若之前已设置闹钟且未超时,返回旧闹钟的剩余秒数;否则返回 0

要点:

  • 定时时间到时,内核向进程发送 SIGALRM 信号。
  • 每个进程只能设置一个 alarm 闹钟,新闹钟会替代旧闹钟。
  • alarm 闹钟只能触发一次,不能循环;要实现循环触发,可在 SIGALRM 处理函数中再次调用 alarm()
  • SIGALRM 默认操作是终止进程,但大多数使用闹钟的进程都会捕获它。

6.2 pause()

#include <unistd.h>

int pause(void);

pause() 使进程暂停运行、进入休眠,直到进程捕获到一个信号为止;只有执行了信号处理函数并从其返回时 pause() 才返回。此时返回 -1,并把 errno 设置为 EINTR

alarm() + pause() 可以模拟一个 sleep:设置闹钟后挂起,闹钟到点触发 SIGALRM,处理函数执行完返回,pause() 随之中断。


7. 信号集

信号集(signal set)是能表示多个信号的数据类型,很多系统调用用它传参,如 sigaction()sigprocmask()sigpending()。它其实就是 sigset_t

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct {
    unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;

7.1 操作 API

#include <signal.h>

int sigemptyset(sigset_t *set);                  /* 初始化为不含任何信号 */
int sigfillset(sigset_t *set);                   /* 初始化为包含所有信号(含实时信号) */
int sigaddset(sigset_t *set, int signum);        /* 添加一个信号 */
int sigdelset(sigset_t *set, int signum);        /* 删除一个信号 */
int sigismember(const sigset_t *set, int signum);/* 测试信号是否在集合中 */
函数 参数 返回值
sigemptyset / sigfillset set 指向待初始化信号集 成功 0;失败 -1 并设 errno
sigaddset / sigdelset set 信号集;signum 目标信号 成功 0;失败 -1 并设 errno
sigismember set 信号集;signum 待测信号 在集合中返回 1;不在返回 0;失败 -1 并设 errno

典型用法:

sigset_t sig_set;

sigemptyset(&sig_set);       /* 清空 */
sigaddset(&sig_set, SIGINT); /* 加入 SIGINT */

if (1 == sigismember(&sig_set, SIGINT))
    puts("信号集中包含 SIGINT 信号");

8. 获取信号的描述信息

Linux 下每个信号都有一串对应的字符串描述信息,存放在 sys_siglist 数组中,可直接用 sys_siglist[SIGINT] 获取。更推荐使用 strsignal()

#include <string.h>

char *strsignal(int sig);

返回 sig 对应描述信息字符串的指针;若 sig 无效,返回 "Unknown signal"

#include <signal.h>

void psignal(int sig, const char *s);

psignal() 把信号的描述信息输出到标准错误,并允许调用者附加信息:整个输出由字符串 s、冒号、空格、描述信号编号的字符串和换行符组成。


9. 信号掩码(阻塞信号传递)

内核为每个进程维护一个信号掩码(就是一个信号集)。当进程收到属于掩码中的信号时,该信号被阻塞、无法投递给进程处理,内核会将它保持为未决(pending)状态,直到该信号从掩码中移除,才投递给进程。

向信号掩码中添加信号的方式:

  1. 调用 signal()sigaction() 为某信号设置处理方式时,进程自动把该信号加入掩码,保证处理期间同种信号被阻塞(sigaction() 是否如此取决于 SA_NODEFER);处理函数返回后自动移除。
  2. sigaction()sa_mask 额外指定一组信号,进入处理函数时加入、返回后移除。
  3. 使用 sigprocmask() 显式添加/移除。

9.1 sigprocmask()

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数 含义
how 操作方式,见下表
set 要加入/移除的信号集;为 NULL 表示不改动当前掩码
oldset 不为 NULL 时,在修改前获取当前信号掩码存入其中
返回值 含义
成功 返回 0
失败 返回 -1,并设置 errno

how 取值:

含义
SIG_BLOCK set 中所有信号加入信号掩码(当前值 ∪ set)
SIG_UNBLOCK set 中所有信号从信号掩码移除
SIG_SETMASK 信号掩码直接设置为 set
sigset_t sig_set;

sigemptyset(&sig_set);              /* 初始化为空 */
sigaddset(&sig_set, SIGINT);        /* 加入 SIGINT */
sigprocmask(SIG_BLOCK, &sig_set, NULL);   /* 阻塞 SIGINT */
/* ... 关键代码 ... */
sigprocmask(SIG_UNBLOCK, &sig_set, NULL); /* 解除阻塞 */

10. 阻塞等待信号:sigsuspend()

考虑这样一个场景:执行受保护的关键代码时不希望被 SIGINT 打断,于是执行前把 SIGINT 加入掩码,执行完恢复掩码,然后调用 pause() 等待信号唤醒。

问题在于:如果信号的传递恰好发生在恢复信号掩码之后、pause() 之前,信号处理函数会先执行,返回后主程序再进入 pause() 被阻塞,直到下一次信号才能唤醒——这有违本意。虽然概率不大,但确实是缺陷。

解决方案是把"恢复信号掩码"和"挂起进程"封装成一个原子操作,这正是 sigsuspend() 的目的:

#include <signal.h>

int sigsuspend(const sigset_t *mask);
参数 含义
mask 指向一个信号集
返回值 含义
总是返回 -1 设置 errno 指示错误,通常为 EINTR(被信号中断);调用失败时 errno 设为 EFAULT

sigsuspend() 把进程信号掩码替换为 mask,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号本身是 mask 集合成员,则不会唤醒、继续挂起),并从信号处理函数返回;一旦返回,sigsuspend() 会把信号掩码恢复为调用前的值。

它等价于以不可中断(原子)方式执行:

sigprocmask(SIG_SETMASK, &mask, &old_mask);
pause();
sigprocmask(SIG_SETMASK, &old_mask, NULL);

11. 实时信号与 sigpending()

11.1 sigpending()

如果进程正在执行信号处理函数,期间又收到信号,且该信号在信号掩码中,内核会阻塞它,把它加入进程的等待信号集(未决信号集)。sigpending() 用于获取当前处于等待状态的信号:

#include <signal.h>

int sigpending(sigset_t *set);
参数 含义
set 处于等待状态的信号会存放到它指向的信号集中
返回值 含义
成功 返回 0
失败 返回 -1,并设置 errno
sigset_t sig_set;

sigemptyset(&sig_set);
sigpending(&sig_set);
if (1 == sigismember(&sig_set, SIGINT))
    puts("SIGINT 信号处于等待状态");

11.2 实时信号的优势

等待信号集只是一个掩码,仅表明信号是否发生,不能表示发生次数:同一个信号在阻塞状态下产生多次,只会被记录一次,之后仅传递一次——这是标准信号的缺点之一。

实时信号相比标准信号的优势:

  • 信号范围扩大,可应用于应用自定义目的(标准信号只有 SIGUSR1SIGUSR2 两个可自定义)。
  • 内核采用队列化管理:同一实时信号多次发送会多次传递。
  • 发送实时信号时可指定伴随数据(整型或指针值),供接收进程在处理函数中获取。
  • 不同实时信号的传递顺序得到保障:多个实时信号等待时,先传编号最小的(编号越小优先级越高);同类型多个信号排队时,传递顺序与发送顺序一致。

Linux 内核定义了 31 个实时信号,编号范围 34~64;SIGRTMIN 表示编号最小的实时信号,SIGRTMAX 表示最大的。

应用使用实时信号需要两点:

  1. 发送进程用 sigqueue() 发送实时信号及伴随数据。
  2. 接收进程用 sigaction() 为该信号建立处理函数,并加入 SA_SIGINFO,用 sa_sigaction 指向的处理函数,这样才能收到伴随数据(用 sa_handler 则收不到伴随数据)。

12. 异常退出:abort()

#include <stdlib.h>

void abort(void);

abort() 通常产生 SIGABRT 信号来终止调用它的进程。SIGABRT 默认操作是终止进程并生成核心转储文件。即使在程序中捕获了 SIGABRT,程序依然会终止:无论阻塞还是忽略 SIGABRTabort() 调用都不受影响,总会成功终止进程。

对比:exit()_exit()_Exit() 用于正常退出abort() 用于异常退出


13. 信号处理函数编写注意事项(扩展)

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

13.1 为什么处理函数里不能随便调用库函数

信号处理函数可能在任意时刻打断主流程,如果主流程正持有锁、正在修改某个全局数据结构(如 malloc 的内部堆管理结构、stdio 的缓冲区),而处理函数又调用了同样的非可重入函数,就会破坏内部状态。

  • 可重入(reentrant):函数在执行过程中被打断、再次进入后仍能正确工作。多数库函数不可重入(内部使用静态/全局数据)。
  • 异步信号安全(async-signal-safe):POSIX 规定了信号处理函数中可以安全调用的函数集合,如 write()read()_exit()open()close()kill()sigaction()signal()waitpid() 等。

printf()malloc()free()strtok()exit() 不属于异步信号安全函数。所以处理函数里通常用 write(STDOUT_FILENO, ...) 而不是 printf()

13.2 处理函数通用原则

  • 尽量只设置一个 volatile sig_atomic_t 标志位,把实际工作留给主循环。
  • 使用 volatile sig_atomic_t 类型的全局标志,保证读写是原子的且不被编译器优化掉。
  • 避免在信号处理函数中做耗时、加锁、分配内存的操作。
  • 需要处理同种信号嵌套时,用 sigaction()sa_mask 明确阻塞哪些信号。
  • 需要让被中断的系统调用自动重启时,加 SA_RESTART 标志。

13.3 sigwait():同步等待信号(扩展)

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

sigwait() 提供"同步"等待信号的方式:先阻塞目标信号,再调用 sigwait() 挂起等待,它返回时把信号编号写入 *sig

#include <signal.h>

int sigwait(const sigset_t *set, int *sig);
返回值 含义
成功 返回 0
失败 返回错误编号(不设置 errno

sigsuspend() 的区别:sigsuspend() 仍会调用在 sigaction 中注册的处理函数,返回 -1 和 EINTRsigwait()不调用处理函数,直接把信号取走。多线程程序里常用 sigwait() 让某个线程专职处理信号。


14. 完整可复制源码

以下程序覆盖 signal、sigaction、kill、raise、alarm、pause、信号集、sigprocmask、sigsuspend、sigqueue 的主干用法,可直接编译运行。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

static volatile sig_atomic_t g_sig_flag = 0;

/* 基础信号处理函数:最简实现,只打印信号编号 */
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}

/* 支持伴随数据的处理函数,需配合 SA_SIGINFO 使用 */
static void sig_rt_handler(int sig, siginfo_t *info, void *context)
{
    (void)context;
    printf("接收到实时信号: %d, 伴随数据: %d, 来自进程: %d\n",
           sig, info->si_value.sival_int, info->si_pid);
}

/* 通过全局标志位通知主循环(推荐做法) */
static void sig_flag_handler(int sig)
{
    g_sig_flag = sig;
}

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "用法: %s <demo>\n", argv[0]);
        fprintf(stderr, "  signal    - signal() 捕获 SIGINT\n");
        fprintf(stderr, "  sigaction - sigaction() 捕获 SIGINT\n");
        fprintf(stderr, "  alarm     - alarm() + pause() 模拟 sleep\n");
        fprintf(stderr, "  block     - sigprocmask() 阻塞 SIGINT\n");
        fprintf(stderr, "  suspend   - sigsuspend() 原子等待信号\n");
        fprintf(stderr, "  sigqueue  - 向自身发送实时信号及其伴随数据\n");
        exit(-1);
    }

    /* ------------------------------------------------------------------
     * 1. signal():捕获 Ctrl+C,不再终止进程
     * ------------------------------------------------------------------ */
    if (0 == strcmp(argv[1], "signal")) {
        sig_t ret = signal(SIGINT, (sig_t)sig_handler);
        if (SIG_ERR == ret) {
            perror("signal error");
            exit(-1);
        }
        printf("已捕获 SIGINT,按 Ctrl+C 试试(用 kill -9 才能退出)\n");
        for ( ; ; )
            pause();

    /* ------------------------------------------------------------------
     * 2. sigaction():功能等价,但控制力更强(推荐)
     * ------------------------------------------------------------------ */
    } else if (0 == strcmp(argv[1], "sigaction")) {
        struct sigaction sig = {0};

        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (-1 == sigaction(SIGINT, &sig, NULL)) {
            perror("sigaction error");
            exit(-1);
        }
        printf("已捕获 SIGINT(sigaction 版本),按 Ctrl+C 试试\n");
        for ( ; ; )
            pause();

    /* ------------------------------------------------------------------
     * 3. alarm() + pause():模拟 sleep
     * ------------------------------------------------------------------ */
    } else if (0 == strcmp(argv[1], "alarm")) {
        struct sigaction sig = {0};

        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (-1 == sigaction(SIGALRM, &sig, NULL)) {
            perror("sigaction error");
            exit(-1);
        }
        printf("启动 3 秒闹钟\n");
        alarm(3);
        pause();
        puts("休眠结束");

    /* ------------------------------------------------------------------
     * 4. sigprocmask():信号掩码阻塞,验证 raise() 后不会立即处理
     * ------------------------------------------------------------------ */
    } else if (0 == strcmp(argv[1], "block")) {
        struct sigaction sig = {0};
        sigset_t sig_set;

        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (-1 == sigaction(SIGINT, &sig, NULL))
            exit(-1);

        sigemptyset(&sig_set);
        sigaddset(&sig_set, SIGINT);
        sigprocmask(SIG_BLOCK, &sig_set, NULL);   /* 阻塞 SIGINT */

        raise(SIGINT);                            /* 立即发送,但被阻塞 */
        printf("信号已发送,但被阻塞,先休眠 2 秒\n");
        sleep(2);
        printf("休眠结束,即将解除阻塞\n");

        sigprocmask(SIG_UNBLOCK, &sig_set, NULL); /* 解除阻塞后立即处理 */

    /* ------------------------------------------------------------------
     * 5. sigsuspend():原子地恢复掩码并挂起
     * ------------------------------------------------------------------ */
    } else if (0 == strcmp(argv[1], "suspend")) {
        struct sigaction sig = {0};
        sigset_t new_mask, old_mask, wait_mask;

        sigemptyset(&new_mask);
        sigaddset(&new_mask, SIGINT);
        sigemptyset(&wait_mask);

        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (-1 == sigaction(SIGINT, &sig, NULL))
            exit(-1);

        sigprocmask(SIG_BLOCK, &new_mask, &old_mask);
        puts("执行保护代码段");
        /* ... 受保护代码段 ... */

        /* 原子地恢复掩码并挂起,避免"恢复后、pause 前"的信号丢失窗口 */
        if (-1 != sigsuspend(&wait_mask))
            exit(-1);

        sigprocmask(SIG_SETMASK, &old_mask, NULL);

    /* ------------------------------------------------------------------
     * 6. sigqueue():发送实时信号及伴随数据
     * ------------------------------------------------------------------ */
    } else if (0 == strcmp(argv[1], "sigqueue")) {
        struct sigaction sig = {0};
        sigset_t sig_set;
        union sigval sig_val;

        sig.sa_sigaction = sig_rt_handler;
        sig.sa_flags = SA_SIGINFO;    /* 必须,才能收到伴随数据 */
        sigemptyset(&sig.sa_mask);
        if (-1 == sigaction(SIGRTMIN, &sig, NULL)) {
            perror("sigaction error");
            exit(-1);
        }

        /* 解除 SIGRTMIN 的阻塞,确保实时信号能投递 */
        sigemptyset(&sig_set);
        sigaddset(&sig_set, SIGRTMIN);
        sigprocmask(SIG_UNBLOCK, &sig_set, NULL);

        sig_val.sival_int = 10;
        if (-1 == sigqueue(getpid(), SIGRTMIN, sig_val)) {
            perror("sigqueue error");
            exit(-1);
        }
        puts("实时信号发送成功");
        sleep(1);

    } else {
        fprintf(stderr, "未知参数: %s\n", argv[1]);
        exit(-1);
    }

    exit(0);
}

14.1 源码逐段解释

  • volatile sig_atomic_t g_sig_flag:信号处理函数与主流程共享数据时的标准类型,保证读写原子且不被优化。
  • signal() 分支:演示最简捕获写法。捕获后 Ctrl+C 不再终止进程,需另开终端 kill -9
  • sigaction() 分支:与 signal() 等价功能,但设置 sa_flagssa_mask 更灵活,是推荐写法。
  • alarm() 分支alarm(3) 设置 3 秒闹钟,pause() 挂起;到点触发 SIGALRM 执行处理函数,返回后 pause() 返回。
  • block 分支:先 SIG_BLOCK 阻塞 SIGINT,然后 raise(SIGINT);由于被阻塞,处理函数不会立刻执行,直到 SIG_UNBLOCK 解除后才执行。
  • suspend 分支sigsuspend(&wait_mask) 用空集替换掩码(即不阻塞任何信号)并挂起,等价于原子地"恢复掩码 + pause"。
  • sigqueue 分支SA_SIGINFO + sa_sigaction 才能取到 si_valueSIGRTMIN 属于可靠信号,支持伴随数据。

15. 实验步骤与调试方法

15.1 编译与运行

# 本地 Ubuntu 上编译
gcc -Wall -o sig_demo sig_demo.c

./sig_demo signal      # 按 Ctrl+C,观察自定义处理函数被调用

15.2 常用调试命令

命令 用途
kill -l 列出系统所有信号及其编号
kill -9 <pid> 发送 SIGKILL,强制终止进程
kill -SIGTERM <pid> 发送 SIGTERM(kill 默认信号)
ps -aux | ps -ef 查看进程 PID 与状态
top 动态查看进程、CPU 占用,可对进程发信号
strace -e trace=signal ./app 跟踪进程收到的信号
ulimit -c unlimited 允许生成核心转储文件

15.3 实验一:捕获 SIGINT

./sig_demo signal &
# 记录后台进程 pid
kill -SIGINT <pid>     # 观察到 "Received signal: 2"
kill -9 <pid>          # 只有 SIGKILL 才能杀死它

结论:进程捕获 SIGINT 后,Ctrl+Ckill -SIGINT 都无法终止它;kill -9(SIGKILL)可以。

15.4 实验二:前台与后台的差异

把捕获了 SIGINT 的程序放到后台运行,再按 Ctrl+C,会发现进程收不到信号——因为内核只把 SIGINT 发给前台进程组。需用 kill 命令手动发送。

15.5 实验三:验证信号掩码

运行 ./sig_demo block:预期先打印"信号已发送,但被阻塞",2 秒后才打印"Received signal: 2"。说明阻塞期间信号未决、解除后才投递。

15.6 实验四:验证实时信号伴随数据

运行 ./sig_demo sigqueue:预期打印"接收到实时信号: 34, 伴随数据: 10"。若去掉 SA_SIGINFO,则收不到伴随数据。


16. 跨平台对比:IMX6ULL vs STM32 vs RK3568

维度 I.MX6ULL(本教程) STM32(裸机/HAL) RK3568(Linux)
信号机制 完整 POSIX 信号,1~64 无进程信号;有硬件中断 NVIC 完整 POSIX 信号
异步事件载体 signal / sigaction 中断服务函数(ISR) signal / sigaction
处理函数约束 异步信号安全、尽量短 ISR 中不宜耗时、注意 volatile 同 I.MX6ULL
定时事件 alarm / setitimer + SIGALRM SysTick / TIM 中断 POSIX timer / alarm
进程模型 多进程,可 fork/vfork/exec 通常单裸机主循环 + 中断 多进程、多线程
实时信号 支持排队、可带伴随数据 无对应概念 支持
调试工具 kill -lstraceps/top 调试器断点、逻辑分析仪 同 I.MX6ULL

要点:STM32 裸机没有"信号"这一层,它用 NVIC 硬件中断直接封装异步事件;而 I.MX6ULL 与 RK3568 都运行 Linux,本篇所有 API 通用。信号更适合"用户态异步通知",硬件事件仍由驱动 + 中断处理。


17. 面试精选(5 题)

Q1:信号的本质是什么?为什么说它是"软件中断"?什么是不可靠信号与可靠信号?

答案:信号本质是内核定义的 int 类型数字编号(1~64),用于在事件发生时通知进程。它像硬件中断一样能打断程序正常流程,属于软件层面对中断机制的模拟,且到达时间不可预测,因此是处理异步事件的手段。按可靠性分:信号值 1~31(小于 SIGRTMIN,即 34)的是不可靠信号,不支持排队,阻塞期间多次到达只记一次、可能丢失;34~64 是可靠信号(实时信号),支持排队,多次发送多次传递,还可携带伴随数据,且不同实时信号按编号从小到大传递。可靠信号用 sigqueue() 发送、sigaction() 绑定。

Q2:signal()sigaction() 有什么区别?为什么推荐 sigaction()

答案signal() 只接受"信号 + 处理函数指针"两个参数,用法简单,但无法设置 sa_mask(处理期间额外阻塞哪些信号)、sa_flags(如 SA_SIGINFOSA_RESTARTSA_NODEFER),也无法单独获取旧处理方式而不修改,且不同实现语义有差异,可移植性差。sigaction() 通过 struct sigaction 提供完整控制:可指定替代处理函数 sa_sigaction 获取 siginfo_t 附加信息、可设置处理期间的信号掩码避免竞态、可用标志精确控制系统调用重启与同种信号是否阻塞,并能通过 oldact 取回旧设置。因此工程上推荐 sigaction()。此外两者都不能设置 SIGKILLSIGSTOP

Q3:信号被"阻塞"和"忽略"有什么区别?"未决(pending)"又是什么?

答案忽略是处理动作——用 SIG_IGN 设置后,信号到达时被直接丢弃,进程永远不会处理它;阻塞是投递控制——信号进入进程信号掩码,内核暂时不投递给进程,但它被记在"等待信号集(未决信号集)"中,一旦解除阻塞就立即投递并处理。所以阻塞不是丢弃,而是延后。用 sigprocmask() 增删掩码,用 sigpending() 查询当前有哪些信号处于未决状态。注意:等待信号集只是掩码,对标准信号无法表示"发生了几次",这也是实时信号支持排队的价值所在。

Q4:什么是 fork 之后的竞争条件?如何用信号保证父/子进程的执行顺序?

答案fork() 之后父子进程都在 fork() 返回处继续,谁先被 CPU 调度是不确定的(多核甚至可能同时运行),因此不能假设父先或子先。若程序正确性依赖特定顺序,就构成竞争条件。可以用信号做同步:希望子进程先运行时,父进程先 sigaction 注册好 SIGUSR1 处理函数,然后父进程调用 sigsuspend(&wait_mask) 挂起;子进程完成工作后 kill(getppid(), SIGUSR1) 唤醒父进程。这样父进程必然等到子进程发信号后才继续,顺序被强制确定。

Q5:信号处理函数里为什么不能用 printf?应遵循哪些原则?

答案:信号处理函数可能在任意时刻打断主流程,若主流程此刻正在 printf(操作 stdio 缓冲区)或 malloc(操作堆管理结构),处理函数再次调用同类非可重入函数,会破坏其内部状态导致崩溃或数据错乱。printfmallocexitstrtok 等都不是异步信号安全函数。应遵循:只调用 POSIX 规定的异步信号安全函数(如 writeread_exitkillwaitpid);用 volatile sig_atomic_t 标志位记录事件、把真正工作交给主循环;用 sa_mask 控制处理期间的信号阻塞以避免竞态;处理函数尽量短小,降低竞争风险。


内容来源:《I.MX6U嵌入式Linux C应用编程指南》第八章 信号:基础