title: 信号机制 tags: [嵌入式Linux, Linux应用编程, 进程, 信号, signal, sigaction, 信号集, 阻塞, 实时信号, IMX6ULL] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/03-进程间通信]]、[[02-进程线程与IPC/04-线程与线程同步]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/6. 信号机制]]
信号是 Linux 应用编程里处理异步事件的核心手段:进程正常跑着,某个事件(按键、除零、定时器超时、子进程退出)突然发生,内核用一个"编号"打断进程当前流程,通知它去应对。本篇从概念、分类、常见信号、默认行为一路讲到 signal / sigaction、发送信号、信号集、阻塞与等待,最后是实战测试、跨平台对比与面试题。
本章篇幅较长,因为信号"概念简单、细节很多":同一个信号在不同状态下行为不同,阻塞与未决(pending)是两个概念,实时信号又能排队。把这些厘清,才算真正会用信号。
信号是事件发生时对进程的通知机制,也可以称为软件中断。它与硬件中断的相似之处在于:能够打断程序当前执行的正常流程,是软件层次上对中断机制的一种模拟。大多数情况下无法预测信号到达的准确时间,所以信号提供了一种处理异步事件的方法。
几个关键定性:
两种信号绝不能被忽略:
SIGKILL和SIGSTOP。原因是它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,进程的运行行为是未定义的。
| 来源 | 说明 | 例子 |
|---|---|---|
| 硬件异常 | 硬件检测到错误条件通知内核,内核再发给相关进程 | 除数为 0、数组越界引用无法访问的内存 |
| 终端特殊字符 | 终端下输入产生信号的字符 | Ctrl+C → SIGINT;Ctrl+\ → SIGQUIT;Ctrl+Z → SIGTSTP |
kill() 系统调用 |
进程把任意信号发给另一个进程或进程组 | 受限:收发双方所有者相同,或发送者是 root |
kill 命令 |
用户在终端向其它进程发信号,内部就是 kill() |
kill -9 xxx 杀死 PID 为 xxx 的进程 |
| 软件事件 | 检测到某种软件条件已发生 | 定时器超时、CPU 时间超限、子进程退出 |
| 进程自身 | 进程可以给自己发信号 | raise()、abort() |
绝大多数到达进程的信号来自内核。
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[返回被中断处继续执行]
signal())时,进程对所有信号的处理方式都是系统默认操作。所以不调用 signal() 的程序,Ctrl+C 会按默认操作终止进程。fork() 创建子进程时,子进程继承父进程的信号处理方式(子进程复制父进程内存映像,信号捕获函数地址在子进程中有意义)。Linux 下从两个角度分类:可靠性(可靠信号 / 不可靠信号)与实时性(实时信号 / 非实时信号)。
早期 UNIX 信号机制简单原始,问题主要有两个:
signal() 重新绑定。Linux 支持不可靠信号,但做了改进:调用完信号处理函数后不必重新调用 signal()。因此 Linux 下的不可靠信号问题主要就是"信号可能丢失"。
SIGRTMIN~SIGRTMAX(34~64)被定义为可靠信号:支持排队、不会丢失;发送函数为 sigqueue(),绑定函数为 sigaction()。实时性分类与可靠性分类相互对应:
实时信号都支持排队,都是可靠信号;它保证了发送的多个信号都能被接收,是 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 表示。
标准信号(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停止进程(注意停止 ≠ 终止,而是暂停)。
Ctrl+C),内核发给前台进程组中每个进程。默认终止进程——这就是平时 Ctrl+C 能杀掉前台进程的原因。Ctrl+\),内核发给前台进程组中每个进程。默认终止进程并生成核心转储文件。进程陷入无限循环、不再响应时用它比较合适。SIGINT / SIGQUIT 都杀不掉时才动它,kill -9 xxx 的 -9 就是它。alarm() 或 setitimer() 有关,定时器到时内核发它给应用。kill 命令的默认信号。精心设计的应用应捕获 SIGTERM 并绑定处理函数,在函数里清理临时文件、释放资源再退出。直接 kill -9 会跳过这些处理,是暴力方式,应作为最后手段。Ctrl+Z),内核发给前台进程组中每个进程使其停止。SIGHUP,会话再发给所有子进程,子进程收到后默认终止。详见「守护进程」相关章节。Linux 提供 signal() 与 sigaction() 两个接口设置处理方式,推荐使用后者。
#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_IGN、SIG_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 参数就是当前触发的信号,用于把多个信号绑到同一个处理函数时区分来源。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
| 参数 | 含义 |
|---|---|
signum |
需要设置的信号(SIGKILL、SIGSTOP 除外) |
act |
指向 struct sigaction,描述新处理方式;为 NULL 表示不改变当前处理方式 |
oldact |
不为 NULL 时,返回信号之前的处理方式;不关心可传 NULL |
| 返回值 | 含义 |
|---|---|
| 成功 | 返回 0 |
| 失败 | 返回 -1,并设置 errno |
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 |
若 signum 为 SIGCHLD,子进程停止(收到 SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU)或恢复(收到 SIGCONT)时不收 SIGCHLD |
SA_NOCLDWAIT |
若 signum 是 SIGCHLD,子进程终止时不转为僵尸进程 |
SA_NODEFER |
不阻塞从自身处理函数中接收的此信号(默认会阻塞同种信号以避免竞态) |
SA_RESETHAND |
执行完处理函数后,把处理方式重置为系统默认操作 |
SA_RESTART |
被信号中断的系统调用,在信号处理完成后自动重新发起 |
SA_SIGINFO |
使用 sa_sigaction 而非 sa_handler |
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_signo、si_code、si_pid、si_value。
一般而言,信号处理函数设计得越简单越好,道理和中断处理函数一样:越快越好,不要做大量消耗 CPU 时间的事情。重要原因是:越简单,引发信号竞争条件(竞态)的风险越低。
#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() 则是向自身发送信号。
#include <signal.h>
int raise(int sig);
| 参数 | 含义 |
|---|---|
sig |
需要发送的信号 |
| 返回值 | 含义 |
|---|---|
| 成功 | 返回 0 |
| 失败 | 返回非零值 |
raise() 等价于 kill(getpid(), sig);,其中 getpid() 用于获取进程自身 PID。
#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;
伴随数据既可以是整型,也可以是指针。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
| 参数 | 含义 |
|---|---|
seconds |
定时秒数;为 0 表示取消之前设置的 alarm 闹钟 |
| 返回值 | 含义 |
|---|---|
| 成功 | 若之前已设置闹钟且未超时,返回旧闹钟的剩余秒数;否则返回 0 |
要点:
SIGALRM 信号。SIGALRM 处理函数中再次调用 alarm()。SIGALRM 默认操作是终止进程,但大多数使用闹钟的进程都会捕获它。#include <unistd.h>
int pause(void);
pause() 使进程暂停运行、进入休眠,直到进程捕获到一个信号为止;只有执行了信号处理函数并从其返回时 pause() 才返回。此时返回 -1,并把 errno 设置为 EINTR。
用 alarm() + pause() 可以模拟一个 sleep:设置闹钟后挂起,闹钟到点触发 SIGALRM,处理函数执行完返回,pause() 随之中断。
信号集(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;
#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 信号");
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、冒号、空格、描述信号编号的字符串和换行符组成。
内核为每个进程维护一个信号掩码(就是一个信号集)。当进程收到属于掩码中的信号时,该信号被阻塞、无法投递给进程处理,内核会将它保持为未决(pending)状态,直到该信号从掩码中移除,才投递给进程。
向信号掩码中添加信号的方式:
signal() 或 sigaction() 为某信号设置处理方式时,进程自动把该信号加入掩码,保证处理期间同种信号被阻塞(sigaction() 是否如此取决于 SA_NODEFER);处理函数返回后自动移除。sigaction() 的 sa_mask 额外指定一组信号,进入处理函数时加入、返回后移除。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); /* 解除阻塞 */
考虑这样一个场景:执行受保护的关键代码时不希望被 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);
如果进程正在执行信号处理函数,期间又收到信号,且该信号在信号掩码中,内核会阻塞它,把它加入进程的等待信号集(未决信号集)。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 信号处于等待状态");
等待信号集只是一个掩码,仅表明信号是否发生,不能表示发生次数:同一个信号在阻塞状态下产生多次,只会被记录一次,之后仅传递一次——这是标准信号的缺点之一。
实时信号相比标准信号的优势:
SIGUSR1、SIGUSR2 两个可自定义)。Linux 内核定义了 31 个实时信号,编号范围 34~64;SIGRTMIN 表示编号最小的实时信号,SIGRTMAX 表示最大的。
应用使用实时信号需要两点:
sigqueue() 发送实时信号及伴随数据。sigaction() 为该信号建立处理函数,并加入 SA_SIGINFO,用 sa_sigaction 指向的处理函数,这样才能收到伴随数据(用 sa_handler 则收不到伴随数据)。#include <stdlib.h>
void abort(void);
abort() 通常产生 SIGABRT 信号来终止调用它的进程。SIGABRT 默认操作是终止进程并生成核心转储文件。即使在程序中捕获了 SIGABRT,程序依然会终止:无论阻塞还是忽略 SIGABRT,abort() 调用都不受影响,总会成功终止进程。
对比:exit()、_exit()、_Exit() 用于正常退出,abort() 用于异常退出。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
信号处理函数可能在任意时刻打断主流程,如果主流程正持有锁、正在修改某个全局数据结构(如 malloc 的内部堆管理结构、stdio 的缓冲区),而处理函数又调用了同样的非可重入函数,就会破坏内部状态。
write()、read()、_exit()、open()、close()、kill()、sigaction()、signal()、waitpid() 等。printf()、malloc()、free()、strtok()、exit() 不属于异步信号安全函数。所以处理函数里通常用 write(STDOUT_FILENO, ...) 而不是 printf()。
volatile sig_atomic_t 标志位,把实际工作留给主循环。volatile sig_atomic_t 类型的全局标志,保证读写是原子的且不被编译器优化掉。sigaction() 的 sa_mask 明确阻塞哪些信号。SA_RESTART 标志。⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
sigwait() 提供"同步"等待信号的方式:先阻塞目标信号,再调用 sigwait() 挂起等待,它返回时把信号编号写入 *sig。
#include <signal.h>
int sigwait(const sigset_t *set, int *sig);
| 返回值 | 含义 |
|---|---|
| 成功 | 返回 0 |
| 失败 | 返回错误编号(不设置 errno) |
与 sigsuspend() 的区别:sigsuspend() 仍会调用在 sigaction 中注册的处理函数,返回 -1 和 EINTR;sigwait() 则不调用处理函数,直接把信号取走。多线程程序里常用 sigwait() 让某个线程专职处理信号。
以下程序覆盖 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);
}
volatile sig_atomic_t g_sig_flag:信号处理函数与主流程共享数据时的标准类型,保证读写原子且不被优化。signal() 分支:演示最简捕获写法。捕获后 Ctrl+C 不再终止进程,需另开终端 kill -9。sigaction() 分支:与 signal() 等价功能,但设置 sa_flags、sa_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_value;SIGRTMIN 属于可靠信号,支持伴随数据。# 本地 Ubuntu 上编译
gcc -Wall -o sig_demo sig_demo.c
./sig_demo signal # 按 Ctrl+C,观察自定义处理函数被调用
| 命令 | 用途 |
|---|---|
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 |
允许生成核心转储文件 |
./sig_demo signal &
# 记录后台进程 pid
kill -SIGINT <pid> # 观察到 "Received signal: 2"
kill -9 <pid> # 只有 SIGKILL 才能杀死它
结论:进程捕获 SIGINT 后,Ctrl+C 与 kill -SIGINT 都无法终止它;kill -9(SIGKILL)可以。
把捕获了 SIGINT 的程序放到后台运行,再按 Ctrl+C,会发现进程收不到信号——因为内核只把 SIGINT 发给前台进程组。需用 kill 命令手动发送。
运行 ./sig_demo block:预期先打印"信号已发送,但被阻塞",2 秒后才打印"Received signal: 2"。说明阻塞期间信号未决、解除后才投递。
运行 ./sig_demo sigqueue:预期打印"接收到实时信号: 34, 伴随数据: 10"。若去掉 SA_SIGINFO,则收不到伴随数据。
| 维度 | 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 -l、strace、ps/top |
调试器断点、逻辑分析仪 | 同 I.MX6ULL |
要点:STM32 裸机没有"信号"这一层,它用 NVIC 硬件中断直接封装异步事件;而 I.MX6ULL 与 RK3568 都运行 Linux,本篇所有 API 通用。信号更适合"用户态异步通知",硬件事件仍由驱动 + 中断处理。
答案:信号本质是内核定义的 int 类型数字编号(1~64),用于在事件发生时通知进程。它像硬件中断一样能打断程序正常流程,属于软件层面对中断机制的模拟,且到达时间不可预测,因此是处理异步事件的手段。按可靠性分:信号值 1~31(小于 SIGRTMIN,即 34)的是不可靠信号,不支持排队,阻塞期间多次到达只记一次、可能丢失;34~64 是可靠信号(实时信号),支持排队,多次发送多次传递,还可携带伴随数据,且不同实时信号按编号从小到大传递。可靠信号用 sigqueue() 发送、sigaction() 绑定。
signal() 和 sigaction() 有什么区别?为什么推荐 sigaction()?答案:signal() 只接受"信号 + 处理函数指针"两个参数,用法简单,但无法设置 sa_mask(处理期间额外阻塞哪些信号)、sa_flags(如 SA_SIGINFO、SA_RESTART、SA_NODEFER),也无法单独获取旧处理方式而不修改,且不同实现语义有差异,可移植性差。sigaction() 通过 struct sigaction 提供完整控制:可指定替代处理函数 sa_sigaction 获取 siginfo_t 附加信息、可设置处理期间的信号掩码避免竞态、可用标志精确控制系统调用重启与同种信号是否阻塞,并能通过 oldact 取回旧设置。因此工程上推荐 sigaction()。此外两者都不能设置 SIGKILL、SIGSTOP。
答案:忽略是处理动作——用 SIG_IGN 设置后,信号到达时被直接丢弃,进程永远不会处理它;阻塞是投递控制——信号进入进程信号掩码,内核暂时不投递给进程,但它被记在"等待信号集(未决信号集)"中,一旦解除阻塞就立即投递并处理。所以阻塞不是丢弃,而是延后。用 sigprocmask() 增删掩码,用 sigpending() 查询当前有哪些信号处于未决状态。注意:等待信号集只是掩码,对标准信号无法表示"发生了几次",这也是实时信号支持排队的价值所在。
答案:fork() 之后父子进程都在 fork() 返回处继续,谁先被 CPU 调度是不确定的(多核甚至可能同时运行),因此不能假设父先或子先。若程序正确性依赖特定顺序,就构成竞争条件。可以用信号做同步:希望子进程先运行时,父进程先 sigaction 注册好 SIGUSR1 处理函数,然后父进程调用 sigsuspend(&wait_mask) 挂起;子进程完成工作后 kill(getppid(), SIGUSR1) 唤醒父进程。这样父进程必然等到子进程发信号后才继续,顺序被强制确定。
printf?应遵循哪些原则?答案:信号处理函数可能在任意时刻打断主流程,若主流程此刻正在 printf(操作 stdio 缓冲区)或 malloc(操作堆管理结构),处理函数再次调用同类非可重入函数,会破坏其内部状态导致崩溃或数据错乱。printf、malloc、exit、strtok 等都不是异步信号安全函数。应遵循:只调用 POSIX 规定的异步信号安全函数(如 write、read、_exit、kill、waitpid);用 volatile sig_atomic_t 标志位记录事件、把真正工作交给主循环;用 sa_mask 控制处理期间的信号阻塞以避免竞态;处理函数尽量短小,降低竞争风险。
内容来源:《I.MX6U嵌入式Linux C应用编程指南》第八章 信号:基础