Nuttx - Signal
从 sigaction 注册到 kill 发送,从内核态 trampoline 到用户态 handler 执行,完整解析 NuttX POSIX 信号在 CONFIG_BUILD_KERNEL 模式下的投递链路。
本文回答以下问题:NuttX 如何实现 POSIX 信号的注册、发送和投递?在 KERNEL 模式下,内核如何安全地调用用户空间的信号处理函数?信号被阻塞时如何排队、何时投递?sigprocmask 和 sigwaitinfo 的内部机制是什么?读完后,你将能够从源码级别追踪一个信号从 kill() 到用户 handler 执行的完整路径,理解 KERNEL 模式下涉及的三次特权级切换。
1. 开篇:信号解决什么问题?
信号是 POSIX 系统中异步通知的核心机制。与同步 IPC(信号量、管道、消息队列)不同,信号可以在任何时刻”打断”目标任务的执行,迫使它运行预先注册的处理函数。典型用途:
- 进程终止通知(SIGCHLD)
- 定时器到期(SIGALRM)
- 用户自定义事件(SIGUSR1/SIGUSR2)
- 异常处理(SIGSEGV、SIGBUS)
在 KERNEL 模式下,信号投递面临额外挑战:信号处理函数运行在用户空间(USR 模式),但投递决策在内核中完成。内核不能直接调用用户函数(特权级不对),必须通过信号跳板(signal trampoline)安全地跨越边界。
NuttX 的信号实现遵循 POSIX 标准(IEEE Std 1003.1 Signal章节),同时为嵌入式场景做了精简(如固定大小的预分配池、无实时信号队列深度限制等)。NuttX 官方文档(Documentation/implementation/signal_handlers.rst)描述了信号处理的设计约束:信号 handler 在任务上下文中执行,不在中断上下文中。
接下来先看信号相关的核心数据结构。
2. 核心数据结构
信号机制涉及三个维度的信息:动作(收到信号后做什么)、状态(哪些信号被阻塞)、队列(待投递的信号)。NuttX 用不同的结构体分别管理这三个维度。
2.1 struct sigaction:注册的信号动作
文件:include/signal.h:400-429
1 | struct sigaction |
每个信号最多有一个 sigaction 与之关联,存储在 task_group 的 tg_sigactionq 链表中。
2.2 siginfo_t:信号携带的信息
文件:include/signal.h:378-394
1 | struct siginfo |
siginfo_t 是信号携带的元数据——接收方的 handler 通过它知道信号来自谁(si_pid)、为什么产生(si_code)、以及附带的数据(si_value)。si_code 区分了信号来源:SI_USER 来自 kill()、SI_QUEUE 来自 sigqueue()、SI_TIMER 来自定时器到期。
2.3 struct sigactq:per-group 的动作注册表条目
文件:sched/signal/signal.h:74-80
1 | struct sigactq |
task_group_s->tg_sigactionq 是一个 sigactq 链表——每个条目对应一个已注册 handler 的信号。查找时线性遍历匹配 signo。
2.4 struct sigq_s:待投递的信号动作(per-TCB)
文件:sched/signal/signal.h:101-113
1 | struct sigq_s |
当一个信号被分发到目标 TCB 时,一个 sigq_s 被分配并加入 tcb->sigpendactionq。这是”待投递队列”——等待下次上下文切换时执行。
2.5 TCB 中的信号字段
文件:include/nuttx/sched.h:664-675
1 | struct tcb_s |
两个关键队列:
sigpendactionq:已决定要投递但还没执行的 handlersigpostedq:正在执行中的 handler(用于防止重入)
数据结构定义了”谁”和”做什么”。下面看如何通过 sigaction() 注册处理函数。
3. 信号注册:sigaction()
signal() 是 C89 接口,语义在不同 Unix 上不一致(有的自动重置为 SIG_DFL)。sigaction() 是 POSIX 标准接口,提供明确的控制:可以指定执行 handler 期间阻塞哪些额外信号(sa_mask),可以选择是否接收 siginfo_t(SA_SIGINFO),可以控制是否自动取消注册(SA_RESETHAND)。
3.1 实现
文件:sched/signal/sig_action.c:207-409(nxsig_action() 关键摘录,原函数约 200 行)
1 | int nxsig_action(int signo, FAR const struct sigaction *act, |
注册完成后,该信号的 handler 信息保存在 task_group 中——同一进程的所有线程共享信号 action。
注册只是准备工作。下面看信号如何被发送到目标任务。
4. 信号发送:kill() / sigqueue()
kill() 只发送信号编号,sigqueue() 还能附带一个 union sigval(整数或指针)。接收方通过 siginfo_t.si_value 获取附带数据。
4.1 kill() 实现
文件:sched/signal/sig_kill.c:78-160
1 | int nxsig_kill(pid_t pid, int signo) |
nxsig_dispatch() 是所有信号发送的统一入口——无论来源是 kill()、sigqueue()、timer_expire() 还是内核内部,最终都调用它。
4.2 sigqueue() 实现
文件:sched/signal/sig_queue.c:79
1 | int nxsig_queue(pid_t pid, int signo, union sigval value) |
发送触发了 nxsig_dispatch()。下面看分发逻辑如何决定信号的命运。
5. 信号分发:决策树
收到信号后,系统必须回答多个问题:
- 信号是否被目标
sigprocmask阻塞? - 目标是否正在
sigwaitinfo()等待该信号? - 该信号有没有注册 handler?
- 目标当前是什么状态(运行中?阻塞在信号量上?)
不同组合导致不同行为。
5.1 nxsig_tcbdispatch():核心决策
文件:sched/signal/sig_dispatch.c:479-747(逻辑流程摘要)
1 | nxsig_tcbdispatch(stcb, info): |
实例:TaskA 调用 kill(TaskB_pid, SIGUSR1)
假设 TaskB 注册了 SIGUSR1 的 handler 且未阻塞该信号:
nxsig_dispatch()找到 TaskB 的 TCB- 检查
TaskB->sigprocmask:SIGUSR1 不在其中 → 未阻塞 nxsig_find_action()在group->tg_sigactionq中找到 SIGUSR1 的 handlernxsig_queue_action():分配sigq_t,填入 handler 指针和 siginfo,加入TaskB->sigpendactionq- 设置
TaskB->sigdeliver = nxsig_deliver - 调用
up_schedule_sigaction(TaskB)——修改 TaskB 的保存寄存器帧,使其下次被调度时先进入arm_sigdeliver
分发决定了”谁该收到信号”。真正执行 handler 的是投递机制——这是 KERNEL 模式下最复杂的部分。
6. 信号投递:KERNEL 模式的三次特权级切换
用户注册的 handler 必须在 USR 模式执行(安全隔离要求)。但信号投递决策在内核中完成。从内核到用户 handler 再回到内核,需要三次特权级切换:
1 | (1) arm_sigdeliver: SYS mode → nxsig_deliver() → up_signal_dispatch() |
6.1 Step 1:up_schedule_sigaction()——修改目标任务的寄存器帧
文件:arch/arm/src/armv7-a/arm_schedulesigaction.c:82-118
1 | void up_schedule_sigaction(struct tcb_s *tcb) |
效果:当 TaskB 下次被调度时,CPU 不会恢复到它被中断的位置,而是进入 arm_sigdeliver()。原始寄存器帧被保存在 xcp.saved_regs 中,等信号处理完后恢复。
6.2 Step 2:arm_sigdeliver()——内核态信号投递入口
文件:arch/arm/src/armv7-a/arm_sigdeliver.c:56-167(关键摘录)
1 | void arm_sigdeliver(void) |
6.3 Step 3:nxsig_deliver()——投递每个 pending 信号
文件:sched/signal/sig_deliver.c:55-216(关键摘录)
1 | void nxsig_deliver(FAR struct tcb_s *stcb) |
6.4 Step 4:up_signal_dispatch()——触发 SYS_signal_handler
文件:arch/arm/src/common/arm_signal_dispatch.c:69-76
1 | void up_signal_dispatch(_sa_sigaction_t sighand, int signo, |
6.5 Step 5:arm_syscall() SYS_signal_handler——切换到 USR 模式
文件:arch/arm/src/armv7-a/arm_syscall.c:382-446
1 | case SYS_signal_handler: |
6.6 Step 6:sig_trampoline()——用户空间跳板
文件:arch/arm/src/common/crt0.c:82-101
1 | sig_trampoline: |
用户 handler 在 USR 模式执行完毕后,sig_trampoline 通过 SVC 请求 SYS_signal_handler_return 回到内核。
6.7 Step 7:SYS_signal_handler_return——回到内核态
文件:arch/arm/src/armv7-a/arm_syscall.c:457-485
1 | case SYS_signal_handler_return: |
之后 nxsig_deliver() 继续处理下一个 pending 信号(如果有),最终 arm_sigdeliver() 恢复原始寄存器帧,任务回到被中断的位置继续执行。
6.8 完整流程图
下图展示了 KERNEL 模式下信号从发送到用户 handler 执行再返回内核的完整时序(含三次特权级切换):

1 | kill(TaskB, SIGUSR1) |
信号投递链路展示了”handler 如何被执行”。但并非所有信号都会被立即投递——应用可以通过 sigprocmask 阻塞信号,或通过 sigwaitinfo 同步等待信号。
7. 信号阻塞与等待
有时应用不希望在任意时刻被信号打断(如正在操作共享数据结构),需要临时阻塞某些信号。阻塞期间到达的信号不会丢失——它们被排入 pending 队列,等解除阻塞后立即投递。
另一种场景是”同步等待信号”——任务主动睡眠直到特定信号到达(如等待定时器超时),这比注册 handler + flag 轮询更高效。
7.1 sigprocmask():控制哪些信号被阻塞
文件:sched/signal/sig_procmask.c:90-189
1 | int nxsig_procmask(int how, FAR const sigset_t *set, FAR sigset_t *oset) |
nxsig_unmask_pendingsignal() 是关键——每次解除阻塞后都要检查 pending 队列,将新可投递的信号立即分发。
7.2 sigwaitinfo():同步等待信号
文件:sched/signal/sig_timedwait.c:102-242
sigwaitinfo() 让任务阻塞等待指定信号集中的任何一个信号到达:
1 | int nxsig_timedwait(FAR const sigset_t *set, FAR siginfo_t *info, |
当目标信号到达时,nxsig_tcbdispatch() 检测到 sigwaitmask 匹配,将信号信息复制到 sigunbinfo,并唤醒任务——这是信号分发决策树中”目标在 sigwaitinfo 等待”的路径。
信号的阻塞和等待都依赖于正确的 handler 投递。而在 KERNEL 模式下,handler 投递还需要一个关键基础设施:用户空间的信号跳板。
8. 信号跳板的初始化
在 KERNEL 模式下,sig_trampoline 函数运行在用户空间。内核必须知道它在用户虚拟地址空间中的位置,才能在 SYS_signal_handler 中设置 PC。这个地址存储在每个进程数据段开头的保留区域中。
8.1 初始化过程
文件:arch/arm/src/common/crt0.c:163-173(用户进程的 _start() 函数)
1 | void _start(int argc, char *argv[]) |
ARCH_DATA_RESERVE 是每个进程 .data 段起始处的 4KB 保留区域(VA CONFIG_ARCH_DATA_VBASE),内核可以在该进程的地址环境中读取 ar_sigtramp 指针。
9. 对比分析
| 特性 | NuttX | Linux | FreeRTOS |
|---|---|---|---|
| 信号标准 | POSIX(sigaction/sigqueue/kill) | POSIX + RT signals | 无信号(用 task notification 替代) |
| 投递时机 | 下次上下文切换到目标时 | 返回用户态时 | — |
| handler 执行模式 | USR(KERNEL 模式)/ SYS(FLAT) | USR | — |
| 信号排队 | 每信号至多一个 pending(非 RT) | RT signals 可排多个 | — |
| 跳板机制 | sig_trampoline (crt0.c) | rt_sigreturn | — |
| sa_mask 支持 | 完整 | 完整 | — |
| siginfo 数据传递 | 内核栈→用户栈 memcpy | 同(signal frame on user stack) | — |
| 预分配策略 | 固定池(CONFIG_SIG_PREALLOC_ACTIONS) | 动态 slab | — |
| SIGKILL/SIGSTOP | 不可捕获/阻塞 | 同 | — |
NuttX 的设计取舍:
- 预分配池而非动态分配——避免信号投递路径中的 malloc(可能阻塞),代价是信号数量有上限
- 单信号排队(非实时)——同一信号多次发送只保留一个 pending,简化了 pending 队列管理
- 三次特权级切换是 KERNEL 模式的必然代价——FLAT 模式下 handler 直接调用,无需跳板
10. 关键要点
信号动作注册在 task_group 中(同一进程的所有线程共享),待投递队列在各 TCB 中(线程私有)。
信号分发的核心决策在
nxsig_tcbdispatch()中:判断阻塞/等待/忽略/处理,决定信号的最终命运。KERNEL 模式下信号投递涉及三次特权级切换:SYS→SYS(arm_sigdeliver 调用 nxsig_deliver)→ SYS→USR(SYS_signal_handler 跳到 sig_trampoline)→ USR→SYS(SYS_signal_handler_return 回到 nxsig_deliver)。
up_schedule_sigaction()通过修改目标 TCB 的 xcp.regs(PC→arm_sigdeliver),使目标下次被调度时自动进入信号处理流程——不需要显式”打断”目标。sig_trampoline 在每个进程的
_start()中注册到 ARCH_DATA_RESERVE,内核通过该地址知道用户空间跳板在哪里。**sigprocmask 的每次修改都会触发
nxsig_unmask_pendingsignal()**——确保被阻塞的信号在解除阻塞的第一时间被投递。信号使用预分配池(
g_sigfreeaction、g_sigpendingsignal等),避免在信号投递的关键路径上做内存分配。
11. 参考文件索引
| 文件路径 | 关键内容 | 引用行号 |
|---|---|---|
include/signal.h |
siginfo_t, struct sigaction, sigset_t | 378-429 |
include/nuttx/sched.h |
TCB 中的信号字段(sigprocmask, sigpendactionq 等) | 664-675, 518-524 |
include/nuttx/addrenv.h |
ARCH_DATA_RESERVE, addrenv_sigtramp_t | 292-312 |
sched/signal/signal.h |
sigactq, sigpendq, sigq_s, 全局池声明 | 74-155 |
sched/signal/sig_action.c |
nxsig_action() / sigaction() | 207-409 |
sched/signal/sig_kill.c |
nxsig_kill() / kill() | 78-160 |
sched/signal/sig_queue.c |
nxsig_queue() / sigqueue() | 79+ |
sched/signal/sig_dispatch.c |
nxsig_dispatch(), nxsig_tcbdispatch(), nxsig_queue_action() | 112-805 |
sched/signal/sig_deliver.c |
nxsig_deliver() | 55-216 |
sched/signal/sig_procmask.c |
nxsig_procmask() / sigprocmask() | 90-189 |
sched/signal/sig_timedwait.c |
nxsig_timedwait() / sigwaitinfo() | 102-242 |
sched/signal/sig_unmaskpendingsignal.c |
nxsig_unmask_pendingsignal() | 50-120 |
sched/signal/sig_findaction.c |
nxsig_find_action() | 46-72 |
sched/signal/sig_initialize.c |
预分配池初始化 | 56-57 |
arch/arm/src/armv7-a/arm_schedulesigaction.c |
up_schedule_sigaction() | 82-118 |
arch/arm/src/armv7-a/arm_sigdeliver.c |
arm_sigdeliver() | 56-167 |
arch/arm/src/armv7-a/arm_syscall.c |
SYS_signal_handler / SYS_signal_handler_return | 382-485 |
arch/arm/src/common/arm_signal_dispatch.c |
up_signal_dispatch() | 69-76 |
arch/arm/src/common/crt0.c |
sig_trampoline, _start() 注册跳板 | 82-173 |
Documentation/implementation/signal_handlers.rst |
NuttX 官方信号处理设计文档 | 全文 |
外部参考文档:
| 文档 | 说明 |
|---|---|
| IEEE Std 1003.1 (POSIX.1-2017), Section 2.4 Signal Concepts | 信号语义标准定义 |
| NuttX Documentation: https://nuttx.apache.org/docs/latest/ | 官方 API 参考和配置指南 |
| ARM Architecture Reference Manual (ARMv7-A/R) | CPSR mode bits, exception return |