--- 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 开始顺序展开;每个信号都有对应名字(宏),程序里应使用符号名而非编号,因为编号可能因系统而异。 > 两种信号**绝不能被忽略**:`SIGKILL` 和 `SIGSTOP`。原因是它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,进程的运行行为是未定义的。 ### 1.2 信号从哪里来 | 来源 | 说明 | 例子 | | ----------------- | ---------------------------------------------- | --------------------------------------------------------------- | | 硬件异常 | 硬件检测到错误条件通知内核,内核再发给相关进程 | 除数为 0、数组越界引用无法访问的内存 | | 终端特殊字符 | 终端下输入产生信号的字符 | `Ctrl+C` → `SIGINT`;`Ctrl+\` → `SIGQUIT`;`Ctrl+Z` → `SIGTSTP` | | `kill()` 系统调用 | 进程把任意信号发给另一个进程或进程组 | 受限:收发双方所有者相同,或发送者是 root | | `kill` 命令 | 用户在终端向其它进程发信号,内部就是 `kill()` | `kill -9 xxx` 杀死 PID 为 xxx 的进程 | | 软件事件 | 检测到某种软件条件已发生 | 定时器超时、CPU 时间超限、子进程退出 | | 进程自身 | 进程可以给自己发信号 | `raise()`、`abort()` | 绝大多数到达进程的信号来自内核。 ### 1.3 信号处理流程图 ```mermaid 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 标准的一部分。 ```mermaid 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() ```c #include 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` 的定义: ```c /* 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() ```c #include 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()` 允许单独获取处理函数而不设置,还可以通过标志精细控制调用处理函数时的行为,因此灵活性与可移植性更好。 ### 4.3 struct sigaction ```c 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` | ### 4.4 siginfo_t ```c 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`。 ### 4.5 信号处理函数说明 一般而言,**信号处理函数设计得越简单越好**,道理和中断处理函数一样:越快越好,不要做大量消耗 CPU 时间的事情。重要原因是:越简单,引发信号竞争条件(竞态)的风险越低。 --- ## 5. 向进程发送信号 ### 5.1 kill() ```c #include #include 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() ```c #include int raise(int sig); ``` | 参数 | 含义 | | ----- | -------------- | | `sig` | 需要发送的信号 | | 返回值 | 含义 | | ------ | ---------- | | 成功 | 返回 0 | | 失败 | 返回非零值 | `raise()` 等价于 `kill(getpid(), sig);`,其中 `getpid()` 用于获取进程自身 PID。 ### 5.3 sigqueue()(实时信号) ```c #include int sigqueue(pid_t pid, int sig, const union sigval value); ``` | 参数 | 含义 | | ------- | ------------------------------------------- | | `pid` | 接收信号的进程 PID | | `sig` | 需要发送的信号;为 0 时用于检查进程是否存在 | | `value` | 信号的伴随数据,`union sigval` 类型 | | 返回值 | 含义 | | ------ | ----------------------- | | 成功 | 返回 0 | | 失败 | 返回 -1,并设置 `errno` | ```c typedef union sigval { int sival_int; void *sival_ptr; } sigval_t; ``` 伴随数据既可以是整型,也可以是指针。 --- ## 6. alarm() 与 pause() ### 6.1 alarm() ```c #include unsigned int alarm(unsigned int seconds); ``` | 参数 | 含义 | | --------- | -------------------------------------------- | | `seconds` | 定时秒数;为 0 表示取消之前设置的 alarm 闹钟 | | 返回值 | 含义 | | ------ | ---------------------------------------------------------- | | 成功 | 若之前已设置闹钟且未超时,返回旧闹钟的剩余秒数;否则返回 0 | 要点: - 定时时间到时,内核向进程发送 `SIGALRM` 信号。 - **每个进程只能设置一个 alarm 闹钟**,新闹钟会替代旧闹钟。 - alarm 闹钟**只能触发一次**,不能循环;要实现循环触发,可在 `SIGALRM` 处理函数中再次调用 `alarm()`。 - `SIGALRM` 默认操作是终止进程,但大多数使用闹钟的进程都会捕获它。 ### 6.2 pause() ```c #include int pause(void); ``` `pause()` 使进程暂停运行、进入休眠,直到进程捕获到一个信号为止;只有执行了信号处理函数并从其返回时 `pause()` 才返回。此时返回 -1,并把 `errno` 设置为 `EINTR`。 用 `alarm()` + `pause()` 可以模拟一个 `sleep`:设置闹钟后挂起,闹钟到点触发 `SIGALRM`,处理函数执行完返回,`pause()` 随之中断。 --- ## 7. 信号集 信号集(signal set)是能表示多个信号的数据类型,很多系统调用用它传参,如 `sigaction()`、`sigprocmask()`、`sigpending()`。它其实就是 `sigset_t`: ```c #define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } sigset_t; ``` ### 7.1 操作 API ```c #include 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` | 典型用法: ```c 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()`。 ```c #include char *strsignal(int sig); ``` 返回 `sig` 对应描述信息字符串的指针;若 `sig` 无效,返回 `"Unknown signal"`。 ```c #include 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() ```c #include 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` | ```c 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()` 的目的: ```c #include int sigsuspend(const sigset_t *mask); ``` | 参数 | 含义 | | ------ | -------------- | | `mask` | 指向一个信号集 | | 返回值 | 含义 | | ----------- | ------------------------------------------------------------------------------------- | | 总是返回 -1 | 设置 `errno` 指示错误,通常为 `EINTR`(被信号中断);调用失败时 `errno` 设为 `EFAULT` | `sigsuspend()` 把进程信号掩码替换为 `mask`,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号本身是 `mask` 集合成员,则不会唤醒、继续挂起),并从信号处理函数返回;一旦返回,`sigsuspend()` 会把信号掩码恢复为调用前的值。 它等价于以不可中断(原子)方式执行: ```c sigprocmask(SIG_SETMASK, &mask, &old_mask); pause(); sigprocmask(SIG_SETMASK, &old_mask, NULL); ``` --- ## 11. 实时信号与 sigpending() ### 11.1 sigpending() 如果进程正在执行信号处理函数,期间又收到信号,且该信号在信号掩码中,内核会阻塞它,把它加入进程的**等待信号集**(未决信号集)。`sigpending()` 用于获取当前处于等待状态的信号: ```c #include int sigpending(sigset_t *set); ``` | 参数 | 含义 | | ----- | ------------------------------------------ | | `set` | 处于等待状态的信号会存放到它指向的信号集中 | | 返回值 | 含义 | | ------ | ----------------------- | | 成功 | 返回 0 | | 失败 | 返回 -1,并设置 `errno` | ```c sigset_t sig_set; sigemptyset(&sig_set); sigpending(&sig_set); if (1 == sigismember(&sig_set, SIGINT)) puts("SIGINT 信号处于等待状态"); ``` ### 11.2 实时信号的优势 等待信号集只是一个掩码,仅表明信号是否发生,不能表示发生次数:同一个信号在阻塞状态下产生多次,只会被记录一次,之后仅传递一次——这是标准信号的缺点之一。 实时信号相比标准信号的优势: - 信号范围扩大,可应用于应用自定义目的(标准信号只有 `SIGUSR1`、`SIGUSR2` 两个可自定义)。 - 内核采用**队列化管理**:同一实时信号多次发送会多次传递。 - 发送实时信号时可指定**伴随数据**(整型或指针值),供接收进程在处理函数中获取。 - **不同实时信号的传递顺序得到保障**:多个实时信号等待时,先传编号最小的(编号越小优先级越高);同类型多个信号排队时,传递顺序与发送顺序一致。 Linux 内核定义了 31 个实时信号,编号范围 34~64;`SIGRTMIN` 表示编号最小的实时信号,`SIGRTMAX` 表示最大的。 应用使用实时信号需要两点: 1. 发送进程用 `sigqueue()` 发送实时信号及伴随数据。 2. 接收进程用 `sigaction()` 为该信号建立处理函数,并加入 `SA_SIGINFO`,用 `sa_sigaction` 指向的处理函数,这样才能收到伴随数据(用 `sa_handler` 则收不到伴随数据)。 --- ## 12. 异常退出:abort() ```c #include void abort(void); ``` `abort()` 通常产生 `SIGABRT` 信号来终止调用它的进程。`SIGABRT` 默认操作是终止进程并生成核心转储文件。即使在程序中捕获了 `SIGABRT`,程序依然会终止:无论阻塞还是忽略 `SIGABRT`,`abort()` 调用都不受影响,总会成功终止进程。 对比:`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`。 ```c #include int sigwait(const sigset_t *set, int *sig); ``` | 返回值 | 含义 | | ------ | ------------------------------ | | 成功 | 返回 0 | | 失败 | 返回错误编号(不设置 `errno`) | 与 `sigsuspend()` 的区别:`sigsuspend()` 仍会调用在 `sigaction` 中注册的处理函数,返回 -1 和 `EINTR`;`sigwait()` 则**不调用**处理函数,直接把信号取走。多线程程序里常用 `sigwait()` 让某个线程专职处理信号。 --- ## 14. 完整可复制源码 以下程序覆盖 signal、sigaction、kill、raise、alarm、pause、信号集、sigprocmask、sigsuspend、sigqueue 的主干用法,可直接编译运行。 ```c #include #include #include #include #include #include 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 \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_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` 属于可靠信号,支持伴随数据。 --- ## 15. 实验步骤与调试方法 ### 15.1 编译与运行 ```bash # 本地 Ubuntu 上编译 gcc -Wall -o sig_demo sig_demo.c ./sig_demo signal # 按 Ctrl+C,观察自定义处理函数被调用 ``` ### 15.2 常用调试命令 | 命令 | 用途 | | ------------------------------ | -------------------------------------- | | `kill -l` | 列出系统所有信号及其编号 | | `kill -9 ` | 发送 SIGKILL,强制终止进程 | | `kill -SIGTERM ` | 发送 SIGTERM(kill 默认信号) | | `ps -aux` \| `ps -ef` | 查看进程 PID 与状态 | | `top` | 动态查看进程、CPU 占用,可对进程发信号 | | `strace -e trace=signal ./app` | 跟踪进程收到的信号 | | `ulimit -c unlimited` | 允许生成核心转储文件 | ### 15.3 实验一:捕获 SIGINT ```bash ./sig_demo signal & # 记录后台进程 pid kill -SIGINT # 观察到 "Received signal: 2" kill -9 # 只有 SIGKILL 才能杀死它 ``` 结论:进程捕获 `SIGINT` 后,`Ctrl+C` 与 `kill -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 -l`、`strace`、`ps/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_SIGINFO`、`SA_RESTART`、`SA_NODEFER`),也无法单独获取旧处理方式而不修改,且不同实现语义有差异,可移植性差。`sigaction()` 通过 `struct sigaction` 提供完整控制:可指定替代处理函数 `sa_sigaction` 获取 `siginfo_t` 附加信息、可设置处理期间的信号掩码避免竞态、可用标志精确控制系统调用重启与同种信号是否阻塞,并能通过 `oldact` 取回旧设置。因此工程上推荐 `sigaction()`。此外两者都不能设置 `SIGKILL`、`SIGSTOP`。 ### Q3:信号被"阻塞"和"忽略"有什么区别?"未决(pending)"又是什么? **答案**:**忽略**是处理动作——用 `SIG_IGN` 设置后,信号到达时被直接丢弃,进程永远不会处理它;**阻塞**是投递控制——信号进入进程信号掩码,内核暂时不投递给进程,但它被记在"等待信号集(未决信号集)"中,一旦解除阻塞就立即投递并处理。所以阻塞不是丢弃,而是延后。用 `sigprocmask()` 增删掩码,用 `sigpending()` 查询当前有哪些信号处于未决状态。注意:等待信号集只是掩码,对标准信号无法表示"发生了几次",这也是实时信号支持排队的价值所在。 ### Q4:什么是 fork 之后的竞争条件?如何用信号保证父/子进程的执行顺序? **答案**:`fork()` 之后父子进程都在 `fork()` 返回处继续,谁先被 CPU 调度是不确定的(多核甚至可能同时运行),因此不能假设父先或子先。若程序正确性依赖特定顺序,就构成竞争条件。可以用信号做同步:希望**子进程先运行**时,父进程先 `sigaction` 注册好 `SIGUSR1` 处理函数,然后父进程调用 `sigsuspend(&wait_mask)` 挂起;子进程完成工作后 `kill(getppid(), SIGUSR1)` 唤醒父进程。这样父进程必然等到子进程发信号后才继续,顺序被强制确定。 ### Q5:信号处理函数里为什么不能用 `printf`?应遵循哪些原则? **答案**:信号处理函数可能在任意时刻打断主流程,若主流程此刻正在 `printf`(操作 `stdio` 缓冲区)或 `malloc`(操作堆管理结构),处理函数再次调用同类**非可重入**函数,会破坏其内部状态导致崩溃或数据错乱。`printf`、`malloc`、`exit`、`strtok` 等都不是异步信号安全函数。应遵循:只调用 POSIX 规定的异步信号安全函数(如 `write`、`read`、`_exit`、`kill`、`waitpid`);用 `volatile sig_atomic_t` 标志位记录事件、把真正工作交给主循环;用 `sa_mask` 控制处理期间的信号阻塞以避免竞态;处理函数尽量短小,降低竞争风险。 --- **内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第八章 信号:基础