Nuttx - Signal

从 sigaction 注册到 kill 发送,从内核态 trampoline 到用户态 handler 执行,完整解析 NuttX POSIX 信号在 CONFIG_BUILD_KERNEL 模式下的投递链路。

本文回答以下问题:NuttX 如何实现 POSIX 信号的注册、发送和投递?在 KERNEL 模式下,内核如何安全地调用用户空间的信号处理函数?信号被阻塞时如何排队、何时投递?sigprocmasksigwaitinfo 的内部机制是什么?读完后,你将能够从源码级别追踪一个信号从 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
2
3
4
5
6
7
8
9
10
11
12
13
struct sigaction
{
union
{
_sa_handler_t _sa_handler; /* void handler(int signo) */
_sa_sigaction_t _sa_sigaction; /* void handler(int signo, siginfo_t *info, void *ctx) */
} sa_u;
sigset_t sa_mask; /* 执行 handler 期间额外阻塞的信号集 */
int sa_flags; /* SA_SIGINFO, SA_NODEFER, SA_NOCLDWAIT 等 */
};

#define sa_handler sa_u._sa_handler
#define sa_sigaction sa_u._sa_sigaction

每个信号最多有一个 sigaction 与之关联,存储在 task_group 的 tg_sigactionq 链表中。

2.2 siginfo_t:信号携带的信息

文件:include/signal.h:378-394

1
2
3
4
5
6
7
8
struct siginfo
{
uint8_t si_signo; /* 信号编号 */
uint8_t si_code; /* 来源:SI_USER, SI_QUEUE, SI_TIMER 等 */
uint8_t si_errno; /* 关联的 errno */
union sigval si_value; /* sigqueue() 传递的附加数据 */
pid_t si_pid; /* 发送者 PID */
};

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
2
3
4
5
6
struct sigactq
{
FAR struct sigactq *flink; /* 链表指针 */
struct sigaction act; /* 注册的动作 */
uint8_t signo; /* 关联的信号编号 */
};

task_group_s->tg_sigactionq 是一个 sigactq 链表——每个条目对应一个已注册 handler 的信号。查找时线性遍历匹配 signo

2.4 struct sigq_s:待投递的信号动作(per-TCB)

文件:sched/signal/signal.h:101-113

1
2
3
4
5
6
7
8
9
10
struct sigq_s
{
FAR struct sigq_s *flink;
union {
void (*sighandler)(int signo, siginfo_t *info, void *context);
} action; /* 要执行的 handler */
sigset_t mask; /* 执行期间的额外阻塞掩码 */
siginfo_t info; /* 信号信息 */
uint8_t type; /* 分配类型标记 */
};

当一个信号被分发到目标 TCB 时,一个 sigq_s 被分配并加入 tcb->sigpendactionq。这是”待投递队列”——等待下次上下文切换时执行。

2.5 TCB 中的信号字段

文件:include/nuttx/sched.h:664-675

1
2
3
4
5
6
7
8
9
10
11
struct tcb_s
{
/* ... */
sig_deliver_t sigdeliver; /* 信号投递回调(= nxsig_deliver)*/
sq_queue_t sigpendactionq; /* 待投递的信号动作队列 */
sq_queue_t sigpostedq; /* 正在投递中的信号 */
sigset_t sigprocmask; /* 被阻塞的信号集 */
sigset_t sigwaitmask; /* sigwaitinfo 等待的信号集 */
siginfo_t *sigunbinfo; /* 解除阻塞时的信号信息指针 */
/* ... */
};

两个关键队列

  • sigpendactionq:已决定要投递但还没执行的 handler
  • sigpostedq:正在执行中的 handler(用于防止重入)

数据结构定义了”谁”和”做什么”。下面看如何通过 sigaction() 注册处理函数。


3. 信号注册:sigaction()

signal() 是 C89 接口,语义在不同 Unix 上不一致(有的自动重置为 SIG_DFL)。sigaction() 是 POSIX 标准接口,提供明确的控制:可以指定执行 handler 期间阻塞哪些额外信号(sa_mask),可以选择是否接收 siginfo_tSA_SIGINFO),可以控制是否自动取消注册(SA_RESETHAND)。

3.1 实现

文件:sched/signal/sig_action.c:207-409nxsig_action() 关键摘录,原函数约 200 行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int nxsig_action(int signo, FAR const struct sigaction *act,
FAR struct sigaction *oact, bool force)
{
FAR struct tcb_s *rtcb = this_task();
FAR struct task_group_s *group = rtcb->group;
FAR sigactq_t *sigact;

/* 在 group->tg_sigactionq 中查找该信号的现有注册 */
sigact = nxsig_find_action(group, signo);

/* 如果调用者要求获取旧的 action(oact != NULL),复制出来 */
if (oact != NULL && sigact != NULL)
{
memcpy(oact, &sigact->act, sizeof(struct sigaction));
}

if (act == NULL) return OK; /* 仅查询 */

/* 如果 handler == SIG_IGN:从链表中移除该 signal 的 action */
if (act->sa_handler == SIG_IGN)
{
/* ... remove sigact from tg_sigactionq, free ... */
}
else
{
/* 分配或复用 sigactq_t,设置 handler/mask/flags */
if (sigact == NULL)
{
sigact = nxsig_alloc_action(); /* 从预分配池中取 */
}
sigact->signo = signo;
memcpy(&sigact->act, act, sizeof(struct sigaction));
sq_addlast(&sigact->flink, &group->tg_sigactionq);
}

return OK;
}

注册完成后,该信号的 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
2
3
4
5
6
7
8
9
10
11
12
int nxsig_kill(pid_t pid, int signo)
{
siginfo_t info;

info.si_signo = signo;
info.si_code = SI_USER; /* 标记来源为用户 kill */
info.si_errno = EINTR;
info.si_value.sival_ptr = NULL;
info.si_pid = _SCHED_GETPID();

return nxsig_dispatch(pid, &info, false); /* false = group dispatch */
}

nxsig_dispatch() 是所有信号发送的统一入口——无论来源是 kill()sigqueue()timer_expire() 还是内核内部,最终都调用它。

4.2 sigqueue() 实现

文件:sched/signal/sig_queue.c:79

1
2
3
4
5
6
7
8
9
10
11
12
int nxsig_queue(pid_t pid, int signo, union sigval value)
{
siginfo_t info;

info.si_signo = signo;
info.si_code = SI_QUEUE; /* 标记来源为 sigqueue */
info.si_errno = OK;
info.si_value = value; /* 携带用户数据 */
info.si_pid = _SCHED_GETPID();

return nxsig_dispatch(pid, &info, false);
}

发送触发了 nxsig_dispatch()。下面看分发逻辑如何决定信号的命运。


5. 信号分发:决策树

收到信号后,系统必须回答多个问题:

  • 信号是否被目标 sigprocmask 阻塞?
  • 目标是否正在 sigwaitinfo() 等待该信号?
  • 该信号有没有注册 handler?
  • 目标当前是什么状态(运行中?阻塞在信号量上?)

不同组合导致不同行为。

5.1 nxsig_tcbdispatch():核心决策

文件:sched/signal/sig_dispatch.c:479-747(逻辑流程摘要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
nxsig_tcbdispatch(stcb, info):
|
+-- 信号被 sigprocmask 阻塞?
| |
| +-- YES: 目标在 sigwaitinfo 等待该信号?
| | +-- YES: 解除阻塞,copy info → sigunbinfo,唤醒任务
| | +-- NO: 加入 pending 队列(等 mask 解除时投递)
| |
| +-- NO (信号未被阻塞):
| |
| +-- 查找 sigaction:nxsig_find_action(group, signo)
| |
| +-- 有 handler?
| +-- YES: nxsig_queue_action() → 加入 sigpendactionq
| | → stcb->sigdeliver = nxsig_deliver
| | → up_schedule_sigaction(stcb) [安排下次切换时执行]
| |
| +-- NO (SIG_DFL): 执行默认动作(终止/忽略/停止)
|
+-- 如果目标阻塞在 sem/mqueue 上:以 EINTR 解除阻塞

实例:TaskA 调用 kill(TaskB_pid, SIGUSR1)

假设 TaskB 注册了 SIGUSR1 的 handler 且未阻塞该信号:

  1. nxsig_dispatch() 找到 TaskB 的 TCB
  2. 检查 TaskB->sigprocmask:SIGUSR1 不在其中 → 未阻塞
  3. nxsig_find_action()group->tg_sigactionq 中找到 SIGUSR1 的 handler
  4. nxsig_queue_action():分配 sigq_t,填入 handler 指针和 siginfo,加入 TaskB->sigpendactionq
  5. 设置 TaskB->sigdeliver = nxsig_deliver
  6. 调用 up_schedule_sigaction(TaskB)——修改 TaskB 的保存寄存器帧,使其下次被调度时先进入 arm_sigdeliver

分发决定了”谁该收到信号”。真正执行 handler 的是投递机制——这是 KERNEL 模式下最复杂的部分。


6. 信号投递:KERNEL 模式的三次特权级切换

用户注册的 handler 必须在 USR 模式执行(安全隔离要求)。但信号投递决策在内核中完成。从内核到用户 handler 再回到内核,需要三次特权级切换

1
2
3
(1) arm_sigdeliver: SYS mode → nxsig_deliver() → up_signal_dispatch()
(2) SYS_signal_handler: SYS mode → USR mode (进入 sig_trampoline)
(3) SYS_signal_handler_return: USR mode → SYS mode (回到 nxsig_deliver)

6.1 Step 1:up_schedule_sigaction()——修改目标任务的寄存器帧

文件:arch/arm/src/armv7-a/arm_schedulesigaction.c:82-118

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void up_schedule_sigaction(struct tcb_s *tcb)
{
/* 保存当前寄存器帧 */
tcb->xcp.saved_regs = tcb->xcp.regs;

/* 在栈上复制一份寄存器帧(为信号处理预留空间)*/
tcb->xcp.regs = (void *)((uint32_t)tcb->xcp.regs - XCPTCONTEXT_SIZE);
memcpy(tcb->xcp.regs, tcb->xcp.saved_regs, XCPTCONTEXT_SIZE);

/* 修改新帧的 PC 为 arm_sigdeliver */
tcb->xcp.regs[REG_PC] = (uint32_t)arm_sigdeliver;
/* 修改 CPSR 为 SYS 模式 + 禁中断 */
tcb->xcp.regs[REG_CPSR] = (PSR_MODE_SYS | PSR_I_BIT | PSR_F_BIT);
}

效果:当 TaskB 下次被调度时,CPU 不会恢复到它被中断的位置,而是进入 arm_sigdeliver()。原始寄存器帧被保存在 xcp.saved_regs 中,等信号处理完后恢复。

6.2 Step 2:arm_sigdeliver()——内核态信号投递入口

文件:arch/arm/src/armv7-a/arm_sigdeliver.c:56-167(关键摘录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void arm_sigdeliver(void)
{
struct tcb_s *rtcb = this_task();

/* 开启中断(handler 需要在可中断状态下执行)*/
up_irq_enable();

/* 调用信号投递函数(遍历 sigpendactionq 中的所有待投递信号)*/
(rtcb->sigdeliver)(rtcb); /* = nxsig_deliver(rtcb) */

/* 所有信号处理完毕,恢复原始上下文 */
rtcb->sigdeliver = NULL;
rtcb->xcp.regs = rtcb->xcp.saved_regs;

/* 完全恢复寄存器帧(回到被中断的位置)*/
arm_fullcontextrestore(); /* sys_call0(SYS_restore_context) */
}

6.3 Step 3:nxsig_deliver()——投递每个 pending 信号

文件:sched/signal/sig_deliver.c:55-216(关键摘录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void nxsig_deliver(FAR struct tcb_s *stcb)
{
while ((sigq = sq_remfirst(&stcb->sigpendactionq)) != NULL)
{
/* 保存旧 sigprocmask,设置新的(= 旧 | sa_mask | 当前信号)*/
savemask = stcb->sigprocmask;
stcb->sigprocmask |= sigq->mask;
nxsig_addset(&stcb->sigprocmask, sigq->info.si_signo);

if ((stcb->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_KERNEL)
{
/* 用户任务:通过 syscall 跳到用户空间执行 handler */
up_signal_dispatch(sigq->action.sighandler,
sigq->info.si_signo, &sigq->info, NULL);
}
else
{
/* 内核线程:直接调用 handler(已在特权态)*/
(*sigq->action.sighandler)(sigq->info.si_signo, &sigq->info, NULL);
}

/* 恢复 sigprocmask */
stcb->sigprocmask = savemask;

/* 检查是否有新解除阻塞的 pending 信号 */
nxsig_unmask_pendingsignal();
}
}

6.4 Step 4:up_signal_dispatch()——触发 SYS_signal_handler

文件:arch/arm/src/common/arm_signal_dispatch.c:69-76

1
2
3
4
5
6
7
void up_signal_dispatch(_sa_sigaction_t sighand, int signo,
siginfo_t *info, void *ucontext)
{
/* 通过 SVC 进入内核,请求切换到用户态执行 handler */
sys_call4(SYS_signal_handler, (uintptr_t)sighand,
(uintptr_t)signo, (uintptr_t)info, (uintptr_t)ucontext);
}

6.5 Step 5:arm_syscall() SYS_signal_handler——切换到 USR 模式

文件:arch/arm/src/armv7-a/arm_syscall.c:382-446

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case SYS_signal_handler:
/* 保存返回地址(nxsig_deliver 中 up_signal_dispatch 之后的位置)*/
rtcb->xcp.sigreturn = regs[REG_PC];

/* 设置 PC 为用户空间的 signal trampoline */
regs[REG_PC] = (uint32_t)(ARCH_DATA_RESERVE->ar_sigtramp);

/* 切换到用户模式 */
cpsr = regs[REG_CPSR] & ~PSR_MODE_MASK;
regs[REG_CPSR] = cpsr | PSR_MODE_USR;

/* 参数传递:R0=sighand, R1=signo, R2=info, R3=ucontext */
regs[REG_R0] = regs[REG_R1]; /* sighand */
regs[REG_R1] = regs[REG_R2]; /* signo */
regs[REG_R2] = regs[REG_R3]; /* info */
regs[REG_R3] = regs[REG_R4]; /* ucontext */

6.6 Step 6:sig_trampoline()——用户空间跳板

文件:arch/arm/src/common/crt0.c:82-101

1
2
3
4
5
6
7
8
9
10
11
sig_trampoline:
push {lr} /* 保存 LR */
mov ip, r0 /* IP = sighand(用户 handler 地址)*/
mov r0, r1 /* R0 = signo */
mov r1, r2 /* R1 = info */
mov r2, r3 /* R2 = ucontext */
blx ip /* 调用用户的信号处理函数! */
pop {r2} /* 恢复 LR */
mov lr, r2
mov r0, #SYS_signal_handler_return
svc #SYS_syscall /* 返回内核 */

用户 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
2
3
4
5
6
7
case SYS_signal_handler_return:
/* 恢复 PC 到 nxsig_deliver 中的返回点 */
regs[REG_PC] = rtcb->xcp.sigreturn;
/* 切回 SYS 模式(特权态)*/
cpsr = regs[REG_CPSR] & ~PSR_MODE_MASK;
regs[REG_CPSR] = cpsr | PSR_MODE_SYS;
rtcb->xcp.sigreturn = 0;

之后 nxsig_deliver() 继续处理下一个 pending 信号(如果有),最终 arm_sigdeliver() 恢复原始寄存器帧,任务回到被中断的位置继续执行。

6.8 完整流程图

下图展示了 KERNEL 模式下信号从发送到用户 handler 执行再返回内核的完整时序(含三次特权级切换):

Signal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
kill(TaskB, SIGUSR1)
| nxsig_dispatch -> nxsig_tcbdispatch -> nxsig_queue_action
| -> up_schedule_sigaction: set TaskB->xcp.regs[PC] = arm_sigdeliver
v
[TaskB gets scheduled]
|
v
arm_sigdeliver() [SYS mode]
| enable IRQ
| nxsig_deliver(TaskB)
| | pop sigq from sigpendactionq
| | up_signal_dispatch(handler, signo, info, NULL)
| | | sys_call4(SYS_signal_handler, ...)
| | v
| | [SVC] arm_syscall: SYS_signal_handler
| | | sigreturn = regs[PC]
| | | regs[PC] = sig_trampoline
| | | regs[CPSR] = USR mode
| | v
| | [USR mode] sig_trampoline
| | | blx handler(signo, info, ctx) <- USER HANDLER EXECUTES
| | | svc SYS_signal_handler_return
| | v
| | [SVC] arm_syscall: SYS_signal_handler_return
| | | regs[PC] = sigreturn
| | | regs[CPSR] = SYS mode
| | v
| | [back to nxsig_deliver] restore sigprocmask, next signal
| v
| nxsig_deliver returns
| sigdeliver = NULL
| xcp.regs = saved_regs
| arm_fullcontextrestore()
v
TaskB resumes at original interrupted point

信号投递链路展示了”handler 如何被执行”。但并非所有信号都会被立即投递——应用可以通过 sigprocmask 阻塞信号,或通过 sigwaitinfo 同步等待信号。


7. 信号阻塞与等待

有时应用不希望在任意时刻被信号打断(如正在操作共享数据结构),需要临时阻塞某些信号。阻塞期间到达的信号不会丢失——它们被排入 pending 队列,等解除阻塞后立即投递。

另一种场景是”同步等待信号”——任务主动睡眠直到特定信号到达(如等待定时器超时),这比注册 handler + flag 轮询更高效。

7.1 sigprocmask():控制哪些信号被阻塞

文件:sched/signal/sig_procmask.c:90-189

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int nxsig_procmask(int how, FAR const sigset_t *set, FAR sigset_t *oset)
{
FAR struct tcb_s *rtcb = this_task();

if (oset != NULL)
*oset = rtcb->sigprocmask;

if (set != NULL)
{
switch (how)
{
case SIG_BLOCK: /* 添加到阻塞集 */
sigorset(&rtcb->sigprocmask, &rtcb->sigprocmask, set);
break;
case SIG_UNBLOCK: /* 从阻塞集移除 */
sigandset(&rtcb->sigprocmask, &rtcb->sigprocmask, &~(*set));
break;
case SIG_SETMASK: /* 直接替换 */
rtcb->sigprocmask = *set;
break;
}

/* 检查是否有之前被阻塞的 pending 信号现在可以投递了 */
nxsig_unmask_pendingsignal();
}

return OK;
}

nxsig_unmask_pendingsignal() 是关键——每次解除阻塞后都要检查 pending 队列,将新可投递的信号立即分发。

7.2 sigwaitinfo():同步等待信号

文件:sched/signal/sig_timedwait.c:102-242

sigwaitinfo() 让任务阻塞等待指定信号集中的任何一个信号到达:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int nxsig_timedwait(FAR const sigset_t *set, FAR siginfo_t *info,
FAR const struct timespec *timeout)
{
/* 先检查是否已有匹配的 pending 信号 */
pendingsig = nxsig_remove_pendingsignal(rtcb, set);
if (pendingsig)
{
/* 有!直接返回,不阻塞 */
memcpy(info, &pendingsig->info, sizeof(siginfo_t));
return pendingsig->info.si_signo;
}

/* 没有匹配的 pending 信号,阻塞等待 */
rtcb->sigwaitmask = *set;
rtcb->sigunbinfo = info;

/* 如果有 timeout,启动 watchdog timer */
if (timeout) wd_start(&rtcb->waitdog, ...);

/* 将任务置为 TSTATE_WAIT_SIG,让出 CPU */
nxtask_wait(..., TSTATE_WAIT_SIG);

/* 被唤醒后:info 已被 nxsig_tcbdispatch 填充 */
return info->si_signo;
}

当目标信号到达时,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
2
3
4
5
6
7
8
void _start(int argc, char *argv[])
{
/* 将 sig_trampoline 的地址写入进程保留区域 */
ARCH_DATA_RESERVE->ar_sigtramp = (addrenv_sigtramp_t)sig_trampoline;

/* 然后调用用户的 main() */
exit(main(argc, 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. 关键要点

  1. 信号动作注册在 task_group 中(同一进程的所有线程共享),待投递队列在各 TCB 中(线程私有)。

  2. 信号分发的核心决策nxsig_tcbdispatch() 中:判断阻塞/等待/忽略/处理,决定信号的最终命运。

  3. KERNEL 模式下信号投递涉及三次特权级切换:SYS→SYS(arm_sigdeliver 调用 nxsig_deliver)→ SYS→USR(SYS_signal_handler 跳到 sig_trampoline)→ USR→SYS(SYS_signal_handler_return 回到 nxsig_deliver)。

  4. up_schedule_sigaction() 通过修改目标 TCB 的 xcp.regs(PC→arm_sigdeliver),使目标下次被调度时自动进入信号处理流程——不需要显式”打断”目标。

  5. sig_trampoline 在每个进程的 _start() 中注册到 ARCH_DATA_RESERVE,内核通过该地址知道用户空间跳板在哪里。

  6. **sigprocmask 的每次修改都会触发 nxsig_unmask_pendingsignal()**——确保被阻塞的信号在解除阻塞的第一时间被投递。

  7. 信号使用预分配池g_sigfreeactiong_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