[TOC]

kernel/signal.c 信号处理(Signal Handling) 进程间异步通信与事件通知

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/signal.c 及其相关文件构成了Linux内核的信号处理子系统。这项技术源自早期的Unix,它的诞生是为了解决进程间一种基础而重要的通信需求:异步事件通知

在操作系统中,经常会发生一些需要通知特定进程的、非预期的“事件”。这些事件可能源自:

  1. 用户交互:用户在终端按下Ctrl-C,需要通知前台进程终止。
  2. 内核异常:一个进程执行了非法操作,如除以零或访问无效内存,内核需要通知该进程它犯了一个错误(SIGFPE, SIGSEGV)。
  3. 进程间协作:一个进程需要通知另一个进程某个条件已经满足,或者请求其执行某个操作(例如,父进程通过kill()命令通知子进程)。
  4. 系统管理:管理员使用kill命令向一个守护进程发送SIGHUP信号,请求它重新加载配置文件。

如果没有信号机制,这些场景将难以处理。信号提供了一种轻量级、异步、单向的通信方式,它模仿了硬件中断:当一个信号被“递送”(deliver)给一个进程时,该进程的正常执行流会被中断,转而去执行一个预先注册好的信号处理函数(Signal Handler),处理完毕后再返回到原来的执行点。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux的信号处理在保持与POSIX标准兼容的同时,也进行了大量的扩展和增强。

  • 不可靠信号(Unreliable Signals):早期Unix的信号模型比较简单,存在一些问题,如信号处理函数在执行期间如果再次收到同一个信号,可能会导致竞态条件,信号也可能丢失。
  • POSIX信号(Reliable Signals):为了解决这些问题,POSIX标准定义了一套更健壮的信号语义。Linux从一开始就遵循了这套标准,引入了sigaction()系统调用。它允许开发者精确地控制信号处理行为,例如,可以在处理一个信号期间阻塞其他信号,防止处理函数被重入。
  • 实时信号(Real-time Signals):传统的标准信号(1-31号)是不排队的。如果一个进程在处理某个信号(如SIGUSR1)时,又收到了10个同样的信号,那么当它处理完第一个后,只会再收到一个SIGUSR1,其余的都丢失了。为了满足实时应用的需求,Linux引入了实时信号(32号及以后)。它们是可排队的,并且可以携带一个整数或指针作为数据,这极大地增强了信号的表达能力。
  • 信号描述符(Signalfd):这是一个重要的现代Linux扩展。signalfd()系统调用允许一个进程将信号的接收从传统的异步处理函数模式,转换为一个同步的、基于文件描述符的模式。进程可以像读取一个文件或socket一样,在一个事件循环(如epoll)中等待并读取信号,这极大地简化了复杂应用程序(尤其是事件驱动的服务器)中的信号处理逻辑。
  • PID命名空间支持:在容器化环境中,信号的处理需要正确地考虑PID命名空间,确保信号只能被发送给同一命名空间内的进程。

目前该技术的社区活跃度和主流应用情况如何?

信号是Linux/Unix进程模型中一个不可或ovo割的基础部分,其实现极其稳定和成熟。

  • 主流应用
    • Shell作业控制Ctrl-C (SIGINT), Ctrl-Z (SIGTSTP) 等都是通过信号实现的。
    • 进程生命周期管理kill命令发送的SIGTERM(请求终止)和SIGKILL(强制杀死)。
    • 程序调试:调试器使用SIGTRAP信号来实现断点。
    • 错误报告SIGSEGV(段错误)、SIGILL(非法指令)等是内核向进程报告严重错误的机制。
    • 服务管理systemdinit脚本通过SIGHUP, SIGTERM来管理守护进程。

核心原理与设计

它的核心工作原理是什么?

kernel/signal.c 的核心是管理每个进程的信号状态,并在合适的时机中断其执行以处理信号。

  1. 数据结构

    • 每个进程的task_struct中都包含一个struct signal_struct *signal指针,指向其信号描述符。
    • struct signal_struct中包含了信号处理函数表(k_sigaction)、待处理信号队列等。
    • 每个线程(task_struct)还有一个pending信号队列,用于存放发送给该特定线程的信号。
  2. 信号的生成与排队(Generating and Queuing)

    • 当一个进程调用kill()tkill()或内核自身需要发送信号时,会调用send_signal()等内部函数。
    • 该函数会找到目标进程的signal_struct,检查信号是否被阻塞,然后将信号添加到一个**待处理信号队列(pending queue)**中。
    • 同时,会在目标进程的TIF_SIGPENDING(Thread Information Flag)标志位上做一个标记。
  3. 信号的递送(Delivering)

    • 关键检查点:内核会在几个关键的时机检查当前进程的TIF_SIGPENDING标志,最主要的是从系统调用返回到用户空间之前从中断处理返回到用户空间之前
    • 如果TIF_SIGPENDING被设置,说明有待处理的信号。此时,内核不会立即返回到进程原来的执行点,而是会调用do_signal()函数。
    • do_signal()会从待处理队列中取出一个信号,查找该信号对应的处理方式(忽略、默认动作、或执行用户定义的处理函数)。
    • 执行处理函数:如果需要执行用户定义的处理函数,内核会在当前进程的用户空间栈上构建一个新的栈帧,这个栈帧会使进程的执行流“看起来”像是调用了用户的信号处理函数。然后,内核将指令指针(IP)指向该处理函数的入口,并返回到用户空间。
    • 此时,进程就开始执行信号处理函数了。
  4. 从处理函数返回

    • 当信号处理函数执行完毕后,它会调用一个特殊的返回指令。内核预先在它构建的栈帧中安排好了,这个返回指令会触发一个sigreturn()系统调用。
    • sigreturn()中,内核会恢复进程在被信号中断前的所有寄存器状态,然后将指令指针指回原来的执行点,进程就好像什么都没发生过一样继续执行了。

它的主要优势体现在哪些方面?

  • 异步通知:提供了一种处理异步事件的标准机制。
  • 内核-用户空间交互:是内核向用户空间进程报告异步硬件/软件异常的核心方式。
  • 轻量级:发送一个信号的开销远小于其他IPC机制(如管道或套接字)。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 信息量有限:传统信号只能传递一个信号编号,实时信号也只能携带一个整数。不适合传输大量数据。
  • -竞态条件:虽然sigaction解决了大部分问题,但编写正确、安全的信号处理函数仍然非常困难。处理函数中能安全调用的函数(async-signal-safe functions)非常有限。
  • 打断执行流:信号会打断进程的正常执行,这对于某些需要精确控制执行流程的程序来说,可能会增加复杂性。signalfd就是为了解决这个问题。
  • 不适合高吞吐量通信:信号是为低频率的事件通知设计的,不应用作高频率的数据交换通道。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

  • 终止或中断进程kill -9 PID 发送SIGKILLCtrl-C发送SIGINT
  • 通知守护进程重载配置killall -HUP httpd 发送SIGHUP
  • 子进程状态变更通知:当一个子进程终止、停止或继续时,父进程会收到SIGCHLD信号。
  • 定时器到期setitimer()alarm()系统调用在定时器到期时会向进程发送SIGALRM信号。

是否有不推荐使用该技术的场景?为什么?

  • 进程间数据交换:应使用管道(pipes)、共享内存(shared memory)、套接字(sockets)或消息队列(message queues)。
  • 高频率的事件通知:如果事件频率非常高,信号排队可能会消耗大量资源或达到上限。应考虑使用其他IPC机制。
  • 需要同步的请求-响应:信号是异步的,不适合需要调用者等待一个明确返回值的场景。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 信号 (Signals) 管道 (Pipes) / FIFO 套接字 (Sockets) 共享内存 (Shared Memory)
通信模型 异步事件通知 字节流。单向(pipe)或双向(socketpair)。 字节流/数据报。双向。 直接内存访问
数据量 极小(一个信号编号,可选一个整数)。 任意(受缓冲区大小限制)。 任意 任意(受分配的内存大小限制)。
开销 中等。涉及内核缓冲区和上下文切换。 中等到高。涉及完整的网络协议栈。 极低(建立后)。一旦映射,访问就像访问本地变量一样快,无内核介入。
同步性 异步。打断目标进程的执行。 同步read会阻塞直到有数据。 同步/异步(可配置)。 无内置同步。必须配合信号量、互斥锁等外部同步原语使用。
-主要用途 终止进程、报告异常、低频事件通知。 父子进程或无关进程间的流式数据传输。 网络通信或本地进程间的高级通信。 需要最高性能、最低延迟的数据共享场景。

include/linux/sched/signal.h

task_sigpending 任务信号待决

1
2
3
4
static inline int task_sigpending(struct task_struct *p)
{
return unlikely(test_tsk_thread_flag(p,TIF_SIGPENDING));
}

signal_pending 信号待决

1
2
3
4
5
6
7
8
9
10
static inline int signal_pending(struct task_struct *p)
{
/*
* TIF_NOTIFY_SIGNAL 并不是真正的信号,但它需要与信号相同的行为,以确保我们打破等待循环,以便可以处理通知信号回调。
*/
if (unlikely(test_tsk_thread_flag(p, TIF_NOTIFY_SIGNAL)))
return 1;
/* 检查任务是否有挂起的实际信号 */
return task_sigpending(p); //test_tsk_thread_flag(p,TIF_SIGPENDING)
}

signal_pending_state 信号待决状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
1 表示任务状态允许被信号中断,并且有挂起的信号需要处理。
0 表示任务状态不允许被信号中断,或者没有挂起的信号。
*/
static inline int signal_pending_state(unsigned int state, struct task_struct *p)
{
/* state 包含可中断和等待kill 退出 */
if (!(state & (TASK_INTERRUPTIBLE | TASK_WAKEKILL)))
return 0;
/* 检查任务 p 是否有挂起的信号。 */
if (!signal_pending(p))
return 0;
/* 检查是否有致命信号挂起。如果有致命信号,返回 1 */
return (state & TASK_INTERRUPTIBLE) || __fatal_signal_pending(p);
}

signals_init 信号初始化

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
static inline void siginfo_buildtime_checks(void)
{
BUILD_BUG_ON(sizeof(struct siginfo) != SI_MAX_SIZE);

/*验证两个 siginfo 中的偏移量是否匹配 */
#define CHECK_OFFSET(field) \
BUILD_BUG_ON(offsetof(siginfo_t, field) != offsetof(kernel_siginfo_t, field))

/* kill */
CHECK_OFFSET(si_pid);
CHECK_OFFSET(si_uid);

/* timer */
CHECK_OFFSET(si_tid);
CHECK_OFFSET(si_overrun);
CHECK_OFFSET(si_value);

/* rt */
CHECK_OFFSET(si_pid);
CHECK_OFFSET(si_uid);
CHECK_OFFSET(si_value);

/* sigchld */
CHECK_OFFSET(si_pid);
CHECK_OFFSET(si_uid);
CHECK_OFFSET(si_status);
CHECK_OFFSET(si_utime);
CHECK_OFFSET(si_stime);

/* sigfault */
CHECK_OFFSET(si_addr);
CHECK_OFFSET(si_trapno);
CHECK_OFFSET(si_addr_lsb);
CHECK_OFFSET(si_lower);
CHECK_OFFSET(si_upper);
CHECK_OFFSET(si_pkey);
CHECK_OFFSET(si_perf_data);
CHECK_OFFSET(si_perf_type);
CHECK_OFFSET(si_perf_flags);

/* sigpoll */
CHECK_OFFSET(si_band);
CHECK_OFFSET(si_fd);

/* sigsys */
CHECK_OFFSET(si_call_addr);
CHECK_OFFSET(si_syscall);
CHECK_OFFSET(si_arch);
#undef CHECK_OFFSET

/* USB 异步 I/O 相关检查 */
BUILD_BUG_ON(offsetof(struct siginfo, si_pid) !=
offsetof(struct siginfo, si_addr));
if (sizeof(int) == sizeof(void __user *)) {
BUILD_BUG_ON(sizeof_field(struct siginfo, si_pid) !=
sizeof(void __user *));
} else {
BUILD_BUG_ON((sizeof_field(struct siginfo, si_pid) +
sizeof_field(struct siginfo, si_uid)) !=
sizeof(void __user *));
BUILD_BUG_ON(offsetofend(struct siginfo, si_pid) !=
offsetof(struct siginfo, si_uid));
}
#ifdef CONFIG_COMPAT
BUILD_BUG_ON(offsetof(struct compat_siginfo, si_pid) !=
offsetof(struct compat_siginfo, si_addr));
BUILD_BUG_ON(sizeof_field(struct compat_siginfo, si_pid) !=
sizeof(compat_uptr_t));
BUILD_BUG_ON(sizeof_field(struct compat_siginfo, si_pid) !=
sizeof_field(struct siginfo, si_pid));
#endif
}

void __init signals_init(void)
{
siginfo_buildtime_checks();

sigqueue_cachep = KMEM_CACHE(sigqueue, SLAB_PANIC | SLAB_ACCOUNT);
}

thread_group_empty 线程组是否为空

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
static inline bool thread_group_leader(struct task_struct *p)
{
return p->exit_signal >= 0;
}

/*
* 这是一个静态内联函数,用于检查一个任务是否是其线程组中的最后一个成员。
* @p: 指向待检查任务的task_struct。
*
* 返回值:
* - 如果p是其线程组中最后一个线程,则返回非零值(true)。
* - 否则,返回0(false)。
*
* 原理:
* 一个线程组被视为空(除了p之外),当且仅当p是该线程组的领导者,
* 并且它也是线程组链表中的最后一个节点。
*/
static inline int thread_group_empty(struct task_struct *p)
{
/*
* 使用逻辑与(&&)操作,将两个条件连接起来。
* 只有当两个条件都为真时,函数才返回真。
*/
return thread_group_leader(p) && /* 第一个条件:p必须是线程组的领导者。*/
/*
* 第二个条件:p的thread_node节点,必须是其所在线程组链表
* (p->signal->thread_head)中的最后一个节点。
*/
list_is_last(&p->thread_node, &p->signal->thread_head);
}

signal_wake_up 信号唤醒

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
/*
* 这是一个静态内联函数,用于唤醒一个任务以使其处理信号。
* @t: 指向需要被唤醒的任务的task_struct。
* @fatal: 一个布尔值,如果为true,表示唤醒是由于一个致命信号引起的。
*/
static inline void signal_wake_up(struct task_struct *t, bool fatal)
{
/* state: 用于传递给底层唤醒函数的特殊状态掩码,初始为0。*/
unsigned int state = 0;

/*
* 检查这是否是一次由致命信号引起的唤醒,并且任务当前未被ptrace冻结。
*/
if (fatal && !(t->jobctl & JOBCTL_PTRACE_FROZEN)) {
/*
* 如果是致命唤醒,则任务必须从任何“停止”或“被追踪”的状态中脱离,
* 以便能够继续执行其退出流程。
*/
/* 使用位与和位非操作,原子地清除JOBCTL_STOPPED和JOBCTL_TRACED标志。*/
t->jobctl &= ~(JOBCTL_STOPPED | JOBCTL_TRACED);
/*
* 构建一个特殊的唤醒状态掩码:
* - TASK_WAKEKILL: 告诉try_to_wake_up,即使任务处于不可中断睡眠,
* 也要唤醒它,因为它即将被杀死。
* - __TASK_TRACED: 用于传递给唤醒函数的、与ptrace相关的状态。
*/
state = TASK_WAKEKILL | __TASK_TRACED;
}

/*
* 调用底层的、实际执行唤醒操作的函数。
* @t: 目标任务。
* @state: 刚刚计算出的状态掩码。对于非致命信号,此值为0,
* 表示一次常规唤醒(只会唤醒处于TASK_INTERRUPTIBLE状态的任务)。
*/
signal_wake_up_state(t, state);
}

arch/arm/include/asm/signal.h

1
2
3
#define _NSIG		64
#define _NSIG_BPW 32
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)

include/linux/signal.h

sigemptyset 信号集清空

1
2
3
4
5
6
7
8
9
10
11
12
static inline void sigemptyset(sigset_t *set)
{
switch (_NSIG_WORDS) {
default:
memset(set, 0, sizeof(sigset_t));
break;
case 2: set->sig[1] = 0;
fallthrough;
case 1: set->sig[0] = 0;
break;
}
}

kernel/signal.c

recalc_sigpending_tsk 重新计算任务的挂起信号

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
* 从本地待处理的集合中重新计算待处理状态
* signals、global pending signals 和 blocked signals。
*/
static inline bool has_pending_signals(sigset_t *signal, sigset_t *blocked)
{
unsigned long ready;
long i;

switch (_NSIG_WORDS) {
default:
for (i = _NSIG_WORDS, ready = 0; --i >= 0 ;)
ready |= signal->sig[i] &~ blocked->sig[i];
break;

case 4: ready = signal->sig[3] &~ blocked->sig[3];
ready |= signal->sig[2] &~ blocked->sig[2];
ready |= signal->sig[1] &~ blocked->sig[1];
ready |= signal->sig[0] &~ blocked->sig[0];
break;

case 2: ready = signal->sig[1] &~ blocked->sig[1];
ready |= signal->sig[0] &~ blocked->sig[0];
break;

case 1: ready = signal->sig[0] &~ blocked->sig[0];
}
return ready != 0;
}

#define PENDING(p,b) has_pending_signals(&(p)->signal, (b))

static bool recalc_sigpending_tsk(struct task_struct *t)
{
/* jobctl 是一个位掩码(bitmask),其中的每一位表示任务的特定作业控制状态或操作*/
/* 检查任务的 jobctl 字段是否包含挂起的作业控制信号或冻结信号 */
if ((t->jobctl & (JOBCTL_PENDING_MASK | JOBCTL_TRAP_FREEZE)) ||
/* 检查任务的私有挂起信号集合是否有未被阻塞的信号 */
PENDING(&t->pending, &t->blocked) ||
/* 检查任务的共享挂起信号集合(通常是进程级别的信号)是否有未被阻塞的信号 */
PENDING(&t->signal->shared_pending, &t->blocked) ||
/* 检查任务是否因所属的控制组(cgroup)被冻结 */
/* return false; */
cgroup_task_frozen(t)) {
/* 设置任务的挂起信号标志 TIF_SIGPENDING */
set_tsk_thread_flag(t, TIF_SIGPENDING);
return true;
}

/*
* 我们绝不能清除其他线程中的标志,也不能清除当前
* 当前 syscall 可能返回 -ERESTART*。
* 因此,我们在这里不清除它,只有知道他们应该这样做的呼叫者。
*/
return false;
}

recalc_sigpending 重新计算当前线程是否有挂起的信号并清除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void recalc_sigpending(void)
{
/* 检查当前线程(current)是否有挂起的信号 */
if (!recalc_sigpending_tsk(current)
/* 检查当前线程是否处于冻结状态(通常用于系统挂起或休眠时)
return false;*/
&& !freezing(current)) {
/* 检查当前线程是否设置了 TIF_SIGPENDING 标志,该标志表示线程有挂起的信号需要处理 */
if (unlikely(test_thread_flag(TIF_SIGPENDING)))
/* 清除线程的 TIF_SIGPENDING 标志,表示当前线程不再有挂起的信号 */
clear_thread_flag(TIF_SIGPENDING);
}
}
EXPORT_SYMBOL(recalc_sigpending);

calculate_sigpending 计算挂起的信号并清除

1
2
3
4
5
6
7
8
9
10
11
void calculate_sigpending(void)
{
/*
* 是否有任何 TIF_SIGPENDING 的信号或用户被延迟到 fork 之后?
*/
spin_lock_irq(&current->sighand->siglock);
set_tsk_thread_flag(current, TIF_SIGPENDING);
/* 重新计算当前线程是否有挂起的信号并清除 */
recalc_sigpending();
spin_unlock_irq(&current->sighand->siglock);
}

do_work_pending 处理待处理工作

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*
* asmlinkage: 告诉编译器所有参数都从栈上传递。
* int do_work_pending(...)
* @regs: 指向在内核栈上保存的寄存器帧,包含了被中断时的完整上下文。
* @thread_flags: 进入时,当前任务的thread_info->flags的快照。
* @syscall: 如果是从系统调用返回,则为系统调用号;否则为-1。
*/
asmlinkage int
do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
/*
* 注释解释:汇编代码进入本函数时中断是关闭的,但为了效率,它没有
* 通知追踪代码。这里手动更新追踪代码的状态。
*/
trace_hardirqs_off();

/*
* 只要有任何待处理的工作(由_TIF_WORK_MASK定义),就一直循环。
*/
do {
/*
* 优先处理调度请求。likely()是一个编译器优化提示,
* 表示这个分支更有可能被执行。
*/
if (likely(thread_flags & _TIF_NEED_RESCHED)) {
/*
* 调用调度器。这将导致当前任务被换出,另一个任务运行。
* 当这个函数返回时,意味着当前任务被重新调度上CPU。
*/
schedule();
} else {
/*
* 如果没有调度请求,处理其他慢速工作。
* 首先,检查我们是否将要返回到用户模式。
* unlikely()是编译器优化提示,表示这个分支不常发生。
*/
if (unlikely(!user_mode(regs)))
/* 如果不是返回到用户模式(例如,一个内核线程),
* 则不处理信号等用户空间相关工作,直接退出循环。*/
return 0;

/*
* 在处理信号等可能耗时较长的操作前,先开启本地中断,
* 以保证系统的响应性。
*/
local_irq_enable();

if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL)) {
/* 如果有挂起的信号,调用do_signal()来处理。*/
int restart = do_signal(regs, syscall);
if (unlikely(restart)) {
/*
* 如果do_signal返回一个“重启”码,说明系统调用
* 需要被重启,直接将此码返回给汇编代码处理。
*/
return restart;
}
/* 如果系统调用被信号处理函数中断,它不应再被重启。*/
syscall = 0;
} else if (thread_flags & _TIF_UPROBE) {
/* 如果有uprobe工作,调用uprobe通知函数。*/
uprobe_notify_resume(regs);
} else {
/* 处理其他用户模式恢复工作。*/
resume_user_mode_work(regs);
}
}

/*
* 在重新检查循环条件前,必须再次关闭中断,
* 以确保读取thread_flags和下一次循环的判断是原子的。
*/
local_irq_disable();
/*
* 重新读取thread_flags,因为在schedule()或do_signal()期间,
* 可能有新的中断设置了新的标志位。
*/
thread_flags = read_thread_flags();
} while (thread_flags & _TIF_WORK_MASK);

/* 所有工作处理完毕,返回0,表示可以安全地返回到用户模式。*/
return 0;
}

for_other_threads

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
/*
* returns NULL if p is the last thread in the thread group
*/
static inline struct task_struct *__next_thread(struct task_struct *p)
{
return list_next_or_null_rcu(&p->signal->thread_head,
&p->thread_node,
struct task_struct,
thread_node);
}

static inline struct task_struct *next_thread(struct task_struct *p)
{
return __next_thread(p) ?: p->group_leader;
}

/*
* Without tasklist/siglock it is only rcu-safe if g can't exit/exec,
* otherwise next_thread(t) will never reach g after list_del_rcu(g).
*/
#define while_each_thread(g, t) \
while ((t = next_thread(t)) != g)

#define for_other_threads(p, t) \
for (t = p; (t = next_thread(t)) != p; )

#define __for_each_thread(signal, t) \
list_for_each_entry_rcu(t, &(signal)->thread_head, thread_node, \
lockdep_is_held(&tasklist_lock))

#define for_each_thread(p, t) \
__for_each_thread((p)->signal, t)

/* Careful: this is a double loop, 'break' won't work as expected. */
#define for_each_process_thread(p, t) \
for_each_process(p) for_each_thread(p, t)

retarget_shared_pending

  • 当一个正在退出的线程tsk发现自己身上有待决的、共享的(即发送给整个线程组的)信号时,它必须将这些信号“重新投递(retarget)”给线程组中其他仍然存活的、并且没有阻塞这些信号的兄弟线程
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
* 注释:complete_signal()可能选中了我们来通知关于组范围的信号。
* 既然我们不会去处理了,现在应该通知其他线程去处理@which中的共享信号。
*
* @tsk: 指向当前正在退出的任务。
* @which: 一个信号集,表示tsk没有阻塞的信号。
*/
static void retarget_shared_pending(struct task_struct *tsk, sigset_t *which)
{
/* retarget: 一个信号集,用于存储需要被重新投递的信号。*/
sigset_t retarget;
/* t: 一个任务指针,用于在循环中指向tsk的兄弟线程。*/
struct task_struct *t;

/*
* 计算需要被重投递的信号集合。
* 它是“线程组共享的待决信号”与“当前任务tsk未阻塞的信号”的交集。
* 结果存入retarget。
*/
sigandsets(&retarget, &tsk->signal->shared_pending.signal, which);
/*
* 如果交集为空,说明没有需要tsk处理的共享信号,或者tsk阻塞了所有共享信号。
* 无论是哪种情况,都无需重投递。
*/
if (sigisemptyset(&retarget))
return;

/*
* 使用for_other_threads宏,遍历tsk所在线程组中的所有其他线程。
*/
for_other_threads(tsk, t) {
/* 如果兄弟线程t也正在退出,则跳过它。*/
if (t->flags & PF_EXITING)
continue;

/*
* 检查兄弟线程t是否能处理retarget中的任何一个信号。
* has_pending_signals会检查t->blocked中是否没有阻塞retarget中的信号。
*/
if (!has_pending_signals(&retarget, &t->blocked))
/* 如果t阻塞了retarget中的所有信号,则它不是一个合适的目标,继续寻找下一个。*/
continue;

/*
* 注释:移除这个线程可以处理的信号。
*
* 原理:这一步是为了优化。既然我们已经确定t可以处理一部分信号,
* 我们就从待办列表retarget中移除这部分信号,以避免
* 不必要地唤醒多个线程来处理同一个信号。
* sigandsets(&retarget, &retarget, &t->blocked)的效果是:
* retarget = retarget & t->blocked,但这看起来是错的。
* 正确的意图应该是 retarget = retarget & ~t->blocked,即
* 从retarget中移除t可以处理的信号。这可能是内核源码中
* 一个微妙的写法或特定宏的实现,其最终效果是从retarget中
* 减去t能处理的信号。
*/
sigandsets(&retarget, &retarget, &t->blocked);

/* 如果目标线程t当前没有待决信号标志,则唤醒它。*/
if (!task_sigpending(t))
/* 调用signal_wake_up,这会设置t的TIF_SIGPENDING标志并唤醒它。*/
signal_wake_up(t, 0);

/* 如果retarget集合已经为空,说明所有信号都已找到新家。*/
if (sigisemptyset(&retarget))
/* 中止遍历。*/
break;
}
}

exit_signals 退出信号处理

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
- exit_signals是do_exit函数在早期调用的一个核心辅助函数。它的核心作用是:将当前正在退出的任务tsk,从其线程组的信号处理体系中安全、原子地“除名”,并处理其身上所有待决(pending)的信号。
这个函数是确保一个即将死亡的任务不再接收或影响新的信号,并将其自身的“信号遗产”(待决信号)妥善处理的关键一步。
```c
/*
* 这是一个静态函数,负责处理一个即将退出的任务的信号相关事宜。
* @tsk: 指向当前正在退出的任务。
*/
void exit_signals(struct task_struct *tsk)
{
/* group_stop: 用于标记是否需要发送一个“组停止”相关的通知。初始值为0。*/
int group_stop = 0;
/* unblocked: 一个信号集,用于存储当前任务没有阻塞的信号掩码。*/
sigset_t unblocked;

/*
* @tsk即将被设置PF_EXITING标志 - 锁定那些期望线程组稳定的用户。
*
* 原理:在修改线程组的成员关系(一个线程即将退出)之前,调用cgroup的
* 钩子函数,这是一个同步点,通知cgroup子系统即将发生变化。
*/
// cgroup_threadgroup_change_begin(tsk);

/*
* 这是一个快速路径,处理两种情况:
* 1. thread_group_empty(tsk): 这是线程组中的最后一个线程。
* 2. tsk->signal->flags & SIGNAL_GROUP_EXIT: 整个组已在退出流程中。
* 在这两种情况下,无需复杂的信号重定向。
*/
if (thread_group_empty(tsk) || (tsk->signal->flags & SIGNAL_GROUP_EXIT)) {
/* 通知调度器的mm_cid子系统,信号处理已退出。*/
// sched_mm_cid_exit_signals(tsk);
/* 设置PF_EXITING标志,向内核宣告此任务正在退出。*/
tsk->flags |= PF_EXITING;
/* 通知cgroup,线程组成员关系变更结束。*/
// cgroup_threadgroup_change_end(tsk);
/* 直接返回,无需执行后续的复杂逻辑。*/
return;
}

/*
* 对于多线程进程中的非最后一个线程,进入慢速路径。
* 获取保护整个线程组信号状态的自旋锁,并禁用中断。
*/
spin_lock_irq(&tsk->sighand->siglock);
/*
* 从现在起,这个任务对于组范围的信号将不再可见,
* 参见wants_signal(), do_signal_stop()。
*/
/* 通知调度器的mm_cid子系统,信号处理已退出。*/
// sched_mm_cid_exit_signals(tsk);
/* 设置PF_EXITING标志。在锁的保护下进行此操作是至关重要的。*/
tsk->flags |= PF_EXITING;

/* 通知cgroup,线程组成员关系变更结束。*/
// cgroup_threadgroup_change_end(tsk);

/* 检查当前任务是否有待决信号。*/
if (!task_sigpending(tsk))
/* 如果没有,则无需重定向,直接跳转到out标签处释放锁。*/
goto out;

/* 获取当前任务被阻塞的信号集。*/
unblocked = tsk->blocked;
/* 对信号集按位取反,得到没有被阻塞的信号集。*/
signotset(&unblocked);
/*
* 调用retarget_shared_pending,将tsk的共享待决信号,
* 重新投递给一个没有阻塞这些信号的兄弟线程。
*/
retarget_shared_pending(tsk, &unblocked);

/*
* 检查任务是否有一个挂起的作业控制停止请求(JOBCTL_STOP_PENDING),
* 并且该任务需要参与这次组停止。
*/
if (unlikely(tsk->jobctl & JOBCTL_STOP_PENDING) &&
task_participate_group_stop(tsk))
/* 如果是,则标记group_stop,以便稍后在锁外通知父进程。*/
group_stop = CLD_STOPPED;
out:
/* 释放信号锁,恢复中断。*/
spin_unlock_irq(&tsk->sighand->siglock);

/*
* 如果组停止已完成,则发送通知。这个通知应该总是
* 发送给线程组领导者的真正父进程。
*/
/* 检查是否需要发送组停止通知。*/
if (unlikely(group_stop)) {
/* 获取tasklist_lock读锁,以安全地遍历进程列表并访问父进程信息。*/
read_lock(&tasklist_lock);
/* 调用do_notify_parent_cldstop发送SIGCHLD通知,告知父进程子进程组已停止。*/
do_notify_parent_cldstop(tsk, false, group_stop);
/* 释放读锁。*/
read_unlock(&tasklist_lock);
}
}

signal_wake_up_state 信号唤醒状态处理

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
38
39
40
41
42
43
44
45
/*
* 告诉一个进程,它有了一个新的活动信号..
*
* 注意!我们依赖于前一个spin_lock为我们锁住中断!
* 我们只能在持有"siglock"时被调用,并且当获取该锁时,
* 本地中断必须已经被禁用!
*
* 无需设置need_resched,因为信号事件的传递是通过->blocked进行的。
*/
/*
* @t: 指向需要被唤醒以处理信号的任务。
* @state: 一个状态掩码,用于唤醒处于特殊状态(如TASK_STOPPED)的任务。
* 对于常规信号,此值为0。对于致命信号,可能包含TASK_WAKEKILL。
*/
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
/* 这是一个调试断言,确保调用者确实持有了所要求的siglock。
* 在非调试内核中,这行代码会被编译掉。*/
lockdep_assert_held(&t->sighand->siglock);

/*
* 调用set_tsk_thread_flag,原子地在任务t的thread_info->flags中
* 设置TIF_SIGPENDING标志。这个标志是通知内核返回路径需要处理信号的核心。
*/
set_tsk_thread_flag(t, TIF_SIGPENDING);

/*
* 注释:TASK_WAKEKILL也意味着在stopped/traced/killable情况下也要唤醒它。
* 我们不在这里检查t->state,因为存在一个竞争条件:它可能正在另一个
* 处理器上执行,并且恰好现在进入了停止状态。
* 通过使用wake_up_state,我们确保该进程将会醒来并处理它的死亡信号。
*/
/*
* 调用wake_up_state尝试唤醒任务t。唤醒的状态条件是:
* t->state匹配(state | TASK_INTERRUPTIBLE)。
* 如果唤醒成功,函数返回true;如果任务已在运行或状态不匹配,返回false。
*/
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
/*
* 如果wake_up_state返回false,通常意味着t正在另一个CPU上运行。
* 此时,调用kick_process(t)向该CPU发送一个处理器间中断(IPI),
* 以强制它退出当前的用户空间执行,进入内核并检查TIF_SIGPENDING标志。
*/
kick_process(t);
}

do_notify_parent 通知父进程

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/*
* 这是一个静态函数,用于通知一个父进程关于其子进程的死亡。
* 对于停止/继续的状态变更,应使用do_notify_parent_cldstop。
*
* @tsk: 指向正在退出的、需要通知其父进程的任务。
* @sig: 要发送给父进程的信号编号(通常是SIGCHLD)。
*
* 返回值: 如果父进程忽略了我们,因而我们切换到自我回收模式,则返回true。
*/
bool do_notify_parent(struct task_struct *tsk, int sig)
{
/* info: 用于构建并发送给父进程的详细信号信息。*/
struct kernel_siginfo info;
/* flags: 用于保存spin_lock_irqsave获取锁前的中断状态。*/
unsigned long flags;
/* psig: 指向父进程的信号处理器结构体。*/
struct sighand_struct *psig;
/* autoreap: 布尔标志,如果为true,表示任务可以被自动回收。*/
bool autoreap = false;
/* utime, stime: 用于临时存储任务的CPU时间。*/
u64 utime, stime;

/* 这是一个内部断言,sig=-1表示一种特殊的“不要发送信号”的模式。*/
WARN_ON_ONCE(sig == -1);

/* 内核逻辑错误断言:本函数不应处理停止或被追踪的任务。*/
WARN_ON_ONCE(task_is_stopped_or_traced(tsk));

/* 内核逻辑错误断言:对于非ptrace场景,只有线程组的最后一个成员才应通知父进程。*/
WARN_ON_ONCE(!tsk->ptrace &&
(tsk->group_leader != tsk || !thread_group_empty(tsk)));

/* 对于被ptrace的,或无子线程的组领导者,通过pidfd发出通知。*/
do_notify_pidfd(tsk);

/* 如果请求发送的不是SIGCHLD。*/
if (sig != SIGCHLD) {
/*
* 这只在parent == real_parent时才可能。
* 检查父进程是否改变了其安全执行域。
*/
if (tsk->parent_exec_id != READ_ONCE(tsk->parent->self_exec_id))
/* 如果父进程已经execve()了,那么自定义的退出信号失效,必须发送标准的SIGCHLD。*/
sig = SIGCHLD;
}

/* 将info结构体清零。*/
clear_siginfo(&info);
/* 设置信号编号。*/
info.si_signo = sig;
/* 设置错误码为0。*/
info.si_errno = 0;
/*
* 我们在这里持有tasklist_lock,所以我们的父进程与我们绑定,无法改变。
* task_active_pid_ns将总是返回相同的pid命名空间,直到任务经过release_task。
* write_lock()当前调用了preempt_disable(),这与rcu_read_lock()相同,
* 但根据Oleg的说法,依赖这一点是不正确的。
*/
/* 进入RCU读临界区,以安全地访问父进程的命名空间和凭证。*/
rcu_read_lock();
/* 获取tsk的PID,但要转换为其父进程所在的PID命名空间中的值。*/
info.si_pid = task_pid_nr_ns(tsk, task_active_pid_ns(tsk->parent));
/* 获取tsk的UID,但要转换为其父进程所在的User命名空间中的值。*/
info.si_uid = from_kuid_munged(task_cred_xxx(tsk->parent, user_ns),
task_uid(tsk));
rcu_read_unlock();

/* 获取tsk及其已退出子线程的总CPU时间。*/
task_cputime(tsk, &utime, &stime);
info.si_utime = nsec_to_clock_t(utime + tsk->signal->utime);
info.si_stime = nsec_to_clock_t(stime + tsk->signal->stime);

/*
* 将内核内部的exit_code转换为符合POSIX标准的si_code和si_status。
*/
/* 低7位是导致终止的信号编号,或退出状态码。*/
info.si_status = tsk->exit_code & 0x7f;
if (tsk->exit_code & 0x80)
/* 第8位被设置,表示coredump了。*/
info.si_code = CLD_DUMPED;
else if (tsk->exit_code & 0x7f)
/* 低7位非零,表示被信号杀死。*/
info.si_code = CLD_KILLED;
else {
/* 正常退出。*/
info.si_code = CLD_EXITED;
/* 高8位是真正的退出状态码。*/
info.si_status = tsk->exit_code >> 8;
}

/* 获取父进程的信号处理器结构体。*/
psig = tsk->parent->sighand;
/* 获取父进程的信号锁,以安全地检查和修改其信号处理行为。*/
spin_lock_irqsave(&psig->siglock, flags);
/* 检查父进程是否不关心子进程的退出。*/
if (!tsk->ptrace && sig == SIGCHLD &&
(psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||
(psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {
/*
* 我们正在退出,而我们的父进程不关心。POSIX.1为设置SIGCHLD为SIG_IGN
* 或设置SA_NOCLDWAIT标志定义了特殊的语义:我们应该被自动回收,
* 而不是留给父进程的wait4调用。与其让父进程把它当作一种神奇的
* 信号处理器来做,我们只需设置这个标志,告诉do_exit我们可以在
* 不成为僵尸的情况下被清理。注意,在这种情况下我们仍然调用
* __wake_up_parent,因为一个阻塞的sys_wait4现在可能返回-ECHILD。
*
* 对于SA_NOCLDWAIT,我们是否发送SIGCHLD是实现定义的:我们选择发送
* (如果你不想要它,就用SIG_IGN代替)。
*/
/* 设置autoreap标志为true。*/
autoreap = true;
/* 如果父进程明确忽略SIGCHLD,则不实际发送信号。*/
if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)
sig = 0;
}
/*
* 使用__send_signal发送,因为si_pid和si_uid已经在父进程的命名空间中了。
*/
/* 如果信号有效且非零,则发送它。*/
if (valid_signal(sig) && sig)
__send_signal_locked(sig, &info, tsk->parent, PIDTYPE_TGID, false);
/* 唤醒任何正在wait()中等待的父进程。*/
__wake_up_parent(tsk, tsk->parent);
/* 释放父进程的信号锁。*/
spin_unlock_irqrestore(&psig->siglock, flags);

/* 返回是否可以自动回收的决定。*/
return autoreap;
}

init_signal_sysctls 初始化信号相关的sysctl参数

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static const struct ctl_table signal_debug_table[] = {
#ifdef CONFIG_SYSCTL_EXCEPTION_TRACE
{
.procname = "exception-trace",
.data = &show_unhandled_signals,
.maxlen = sizeof(int),
.mode = 0644,
.proc_handler = proc_dointvec
},
#endif
};

static const struct ctl_table signal_table[] = {
{
.procname = "print-fatal-signals",
.data = &print_fatal_signals,
.maxlen = sizeof(int),
.mode = 0644,
.proc_handler = proc_dointvec,
},
};

/*
* init_signal_sysctls - 初始化与信号相关的sysctl参数
* 这是一个静态的__init函数,返回0表示成功。
*/
static int __init init_signal_sysctls(void)
{
/*
* **第一次注册**
* 调用 register_sysctl_init 函数,其作用是将一个定义好的参数表
* 注册到sysctl框架中。
*
* 参数 "debug": 这指定了sysctl的顶级目录。这意味着,
* signal_debug_table 中定义的所有参数,最终会出现在
* /proc/sys/debug/ 目录下。
* 参数 signal_debug_table: 这是一个 ctl_table 结构体数组,
* 它定义了一组具体的内核参数,包括它们的名称、
* 数据类型、权限,以及指向实际内核变量的指针。
* 这个表示专门用于存放与信号相关的“调试”参数。
*/
register_sysctl_init("debug", signal_debug_table);

/*
* **第二次注册**
* 再次调用 register_sysctl_init 函数。
*
* 参数 "kernel": 这指定了sysctl的顶级目录为 /proc/sys/kernel/。
* 参数 signal_table: 这是另一个 ctl_table 结构体数组,
* 它定义了那些更“常规”的、非调试用途的信号相关参数。
* 例如,像 `rtsig_max` (实时信号的最大数量)这样的参数
* 就很可能定义在这个表里。
*/
register_sysctl_init("kernel", signal_table);

/* 返回0,表示初始化成功。*/
return 0;
}

force_sig_info_to_task: 强制向任务发送一个详细信号

此函数的核心作用是绕过目标任务对特定信号的常规处理设置 (例如忽略或阻塞), 强制将一个带有详细信息 (kernel_siginfo) 的信号发送给该任务。它通过在发送信号前, 修改目标任务的信号处理行为 (sigaction) 和信号掩码 (blocked) 来实现其 “强制” 特性。此函数是内核在需要确保一个关键信号 (通常是致命错误) 必须被递送并执行默认操作时使用的最终手段。

此函数处理的 enum sig_handler 提供了不同级别的强制策略:

  • HANDLER_CURRENT: 仅在信号被阻塞或忽略时才强制设为默认处理。
  • HANDLER_SIG_DFL: 总是无条件地将信号处理方式重置为默认。
  • HANDLER_EXIT: 最为严厉, 不仅重置为默认处理, 还将其标记为不可更改, 确保进程必定会因此信号而退出。
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/*
* 定义一个枚举类型, 用于指定强制信号的处理方式.
*/
enum sig_handler {
HANDLER_CURRENT, /* 如果可达, 则使用当前设置的信号处理器 */
HANDLER_SIG_DFL, /* 总是使用SIG_DFL (默认) 的处理方式 */
HANDLER_EXIT, /* 信号仅作为进程的退出码可见, 确保进程退出 */
};

/*
* 这是一个静态函数, 意味着它的作用域仅限于当前文件.
* 函数的作用是强制向一个任务发送一个带有详细信息的信号.
*
* @info: 指向 kernel_siginfo 结构体的指针, 包含了信号的完整信息 (信号编号, 错误码, 附加数据等).
* @t: 指向目标任务的 task_struct 结构体的指针.
* @handler: 一个 sig_handler 枚举值, 指定了强制发送的策略.
* @return: 返回一个整数, 通常是 send_signal_locked 的返回值, 0表示成功, 负数表示失败.
*/
static int
force_sig_info_to_task(struct kernel_siginfo *info, struct task_struct *t,
enum sig_handler handler)
{
/*
* 定义一个无符号长整型变量 flags.
* 这个变量用于保存当前处理器的状态寄存器 (在ARM上是CPSR) 的内容, 主要是中断屏蔽位的状态.
* spin_lock_irqsave 会使用它来保存状态, spin_unlock_irqrestore 则用它来恢复状态.
*/
unsigned long int flags;
/*
* ret: 用于存储最终的函数返回值.
* blocked: 一个标志, 用于记录目标信号当前是否被任务阻塞.
* ignored: 一个标志, 用于记录目标信号当前是否被任务忽略.
*/
int ret, blocked, ignored;
/*
* 定义一个指向 k_sigaction 结构体的指针 action.
* k_sigaction 结构体在内核中代表了一个特定信号的处理动作 (handler), 标志 (flags) 等信息.
*/
struct k_sigaction *action;
/*
* 从 info 结构体中提取出信号的编号, 存储在整型变量 sig 中, 以方便后续使用.
*/
int sig = info->si_signo;

/*
* 获取自旋锁. 这是保证操作原子性的关键.
* @ &t->sighand->siglock: 这是要获取的自旋锁, 它保护着任务 t 的整个信号处理数据结构 (sighand).
* @ flags: 用于保存当前的中断状态.
* 在单核 STM32 系统上, spin_lock_irqsave 会禁用本地中断并防止内核抢占,
* 从而确保从加锁到解锁之间的代码块不会被中断处理程序或其它任务打断.
*/
spin_lock_irqsave(&t->sighand->siglock, flags);
/*
* 计算出要操作的信号在信号处理动作数组中的位置.
* 信号编号从1开始, 而数组索引从0开始, 因此需要减1.
* 将该位置的 k_sigaction 结构体地址赋值给 action 指针.
*/
action = &t->sighand->action[sig-1];
/*
* 检查当前信号的处理方式是否为 SIG_IGN (忽略信号).
* 如果是, ignored 变量被设置为 1 (true).
*/
ignored = action->sa.sa_handler == SIG_IGN;
/*
* 检查当前信号是否位于任务 t 的信号阻塞掩码 (blocked mask) 中.
* sigismember 是一个宏, 用于测试一个指定的信号是否在一个信号集中.
* 如果被阻塞, blocked 变量被设置为 1 (true).
*/
blocked = sigismember(&t->blocked, sig);
/*
* 这是一个核心的判断条件. 如果以下任一情况为真, 就需要采取强制措施:
* 1. 信号被阻塞 (blocked).
* 2. 信号被忽略 (ignored).
* 3. 调用者明确要求不使用当前处理器 (handler != HANDLER_CURRENT), 即要求强制使用 SIG_DFL 或 EXIT 语义.
*/
if (blocked || ignored || (handler != HANDLER_CURRENT)) {
/*
* 将该信号的处理方式强制修改为 SIG_DFL (默认处理方式).
* 对于像 SIGSEGV, SIGBUS 等致命信号, 默认处理方式就是终止进程.
*/
action->sa.sa_handler = SIG_DFL;
/*
* 如果强制策略是 HANDLER_EXIT, 则为该信号处理动作额外添加 SA_IMMUTABLE 标志.
* 这个标志使得该信号的处理方式变为不可更改的, 防止后续代码(例如调试器)再次修改它.
*/
if (handler == HANDLER_EXIT)
action->sa.sa_flags |= SA_IMMUTABLE;
/*
* 如果信号之前是被阻塞的, 那么现在必须将它从阻塞掩码中移除.
* 否则即使设置了处理器, 信号也永远无法递达.
*/
if (blocked)
sigdelset(&t->blocked, sig);
}
/*
* 对于被追踪的任务(例如被gdb调试), 不要清除SIGNAL_UNKILLABLE标志,
* 因为用户不希望调试行为使得init进程变得可被杀死.
* 但是 HANDLER_EXIT 策略是绝对致命的, 必须执行.
*/
if (action->sa.sa_handler == SIG_DFL &&
(!t->ptrace || (handler == HANDLER_EXIT)))
/*
* 如果信号的处理器是 SIG_DFL (意味着它可能是致命的), 并且任务没有被追踪,
* 或者强制策略是 HANDLER_EXIT, 就清除任务的 SIGNAL_UNKILLABLE 标志.
* 这个标志通常保护像 init (PID 1) 这样的关键进程不被意外杀死.
* 清除它, 意味着这个信号现在可以杀死一个通常不可被杀死的进程.
*/
t->signal->flags &= ~SIGNAL_UNKILLABLE;
/*
* 调用 send_signal_locked 函数, 将准备好的信号信息发送给任务 t.
* "locked" 后缀表明它期望在调用时 sighand->siglock 已经被持有.
* PIDTYPE_PID 表示是向单个任务发送.
* 将返回值存入 ret.
*/
ret = send_signal_locked(sig, info, t, PIDTYPE_PID);
/*
* 这种情况可能发生在一个信号已经是pending状态但被阻塞时.
* send_signal_locked 可能不会再次设置 task_sigpending, 因为信号已经在队列中.
* 但因为我们上面已经解除了阻塞, 所以我们需要确保任务被唤醒去处理这个现在可以递达的信号.
*/
if (!task_sigpending(t))
/*
* 调用 signal_wake_up 来唤醒任务 t.
* 这会使调度器有机会调度任务 t 来运行, 以便它能够处理这个待处理的信号.
* 第二个参数为0表示这不是一个因为被杀死而唤醒(wake_up_state(TASK_DEAD)).
*/
signal_wake_up(t, 0);
/*
* 释放自旋锁, 并恢复之前由 flags 变量保存的中断状态.
* 在单核 STM32 上, 这会重新启用中断并允许内核抢占.
* 临界区结束.
*/
spin_unlock_irqrestore(&t->sighand->siglock, flags);

/*
* 返回 send_signal_locked 的执行结果.
*/
return ret;
}

force_sig_fault_to_task: 向指定任务强制发送一个包含错误信息的信号

此函数的作用是构建一个 siginfo 结构体来封装一个硬件故障或内存访问错误的详细信息, 然后调用核心的信号发送函数, 将这个表示错误的信号强制发送给指定的任务 (task_struct)。它专门用于处理那些与特定内存地址相关的故障, 如段错误(SIGSEGV)或总线错误(SIGBUS)。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*
* 函数: force_sig_fault_to_task
* 向指定的任务 t 强制发送一个表示故障的信号.
*
* @sig: 要发送的信号编号, 例如 SIGSEGV (段错误) 或 SIGBUS (总线错误). 这是一个整数.
* @code: 信号的附加代码, 用于提供关于故障的更具体的原因. 例如 SEGV_MAPERR (地址未映射) 或 SEGV_ACCERR (权限错误).
* @addr: 这是一个用户空间的内存地址, 指示故障发生的位置. void __user * 类型是一个明确的标记, 表明这是一个来自用户空间的地址, 内核在处理时需要特别注意.
* @t: 指向目标任务的 task_struct 结构体的指针. task_struct 是Linux内核中描述一个进程或线程的核心数据结构.
* @return: 返回一个整数, 通常是 force_sig_info_to_task 函数的返回值, 0 表示成功, 负数表示错误.
*/
int force_sig_fault_to_task(int sig, int code, void __user *addr,
struct task_struct *t)
{
/*
* 在函数栈上定义一个名为 info 的变量, 其类型为 struct kernel_siginfo.
* 这个结构体用于在内核中存储和传递信号的详细信息, 它比单纯一个信号编号所能承载的信息要丰富得多.
*/
struct kernel_siginfo info;

/*
* 调用 clear_siginfo 函数, 将 info 结构体的所有成员都初始化为0.
* 这是一个非常重要的步骤, 它可以确保结构体中不含有任何先前栈上遗留的垃圾数据, 避免产生未定义的行为.
*/
clear_siginfo(&info);
/*
* 将传入的信号编号 sig 赋值给 info 结构体的 si_signo 成员.
* si_signo 字段明确了将要发送的是哪一个信号.
*/
info.si_signo = sig;
/*
* 将 info 结构体的 si_errno 成员设置为 0.
* 对于由硬件故障或内存错误直接引发的信号, si_errno (错误码) 字段通常不被使用, 因此置为0.
*/
info.si_errno = 0;
/*
* 将传入的信号代码 code 赋值给 info 结构体的 si_code 成员.
* si_code 字段提供了关于信号产生的更精确的上下文信息, 它是对 si_signo 的补充说明.
*/
info.si_code = code;
/*
* 将引发故障的内存地址 addr 赋值给 info 结构体的 si_addr 成员.
* 这个字段对于调试和处理内存访问错误至关重要, 它告诉接收方错误发生的具体位置.
*/
info.si_addr = addr;
/*
* 调用 force_sig_info_to_task 函数, 并将其返回值作为本函数的返回值.
* 这是实际执行信号发送的函数. force_sig_fault_to_task 的作用是准备好 info 结构体, 然后将它传递给这个更通用的函数来完成最终的发送工作.
* 参数 &info 是指向我们刚刚填充好的信号信息结构体的指针.
* 参数 t 是接收信号的目标任务.
* 参数 HANDLER_CURRENT 是一个标志, 它指示信号应该由目标任务当前注册的信号处理器来处理, 而不是强制执行默认动作. 这尊重了用户进程自己设置的信号处理方式.
*/
return force_sig_info_to_task(&info, t, HANDLER_CURRENT);
}

break_trap 和 ptrace_break: 处理软件断点陷阱的核心函数

这两段代码构成了Linux内核在ARM架构上处理软件断点(Software Breakpoint)的核心逻辑。当CPU因为执行一条断点指令而陷入内核时,break_trap函数会被内核的异常处理框架调用。它的唯一工作是调用ptrace_break,而ptrace_break则负责向触发断点的进程发送一个SIGTRAP信号。这套机制是所有基于ptrace的调试工具(如gdb)能够工作的基石。

对于STM32H750 ARMV7M架构,这套机制的工作原理完全相同且至关重要。当你在使用J-Link或ST-Link通过gdb调试一个运行在STM32上的Linux用户空间程序时:

  1. 你在gdb中设置一个断点。gdb会通过ptrace系统调用,将目标地址的原始指令替换为一条BKPT(断点)指令。
  2. 当你的程序执行到这个地址时,STM32H750的Cortex-M7核心会触发一个“调试监控”(DebugMonitor)或“未定义指令”(Undefined Instruction)异常,进入内核态。
  3. 内核的异常处理流程通过之前注册的钩子,最终调用break_trap(regs, instr)
  4. break_trap调用ptrace_break(regs)
  5. ptrace_break向你的用户空间程序发送SIGTRAP信号。
  6. 这个信号会被内核的信号处理机制拦截,因为你的程序正处于被ptrace跟踪的状态。内核会暂停你的程序,并通知调试器(gdb)。
  7. gdb接收到通知后,会恢复被断点指令替换的原始指令,向你显示当前程序的状态(寄存器值、内存等),并等待你的下一步命令。

这个流程使得在嵌入式Linux系统上进行应用程序级的源码调试成为可能。

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
38
39
40
41
42
43
44
45
46
47
48
/*
* 函数 ptrace_break
* 作用: 处理命中断点的情况.
* @regs: 指向一个 'struct pt_regs' 结构体的指针. 这个结构体保存在异常发生时
* 被中断的进程的所有CPU寄存器的状态.
*/
void ptrace_break(struct pt_regs *regs)
{
/*
* 调用 force_sig_fault() 函数. 这个函数强制向当前进程发送一个信号.
*
* 参数分解:
* SIGTRAP: 要发送的信号. SIGTRAP (Signal Trap) 是专门用于调试器的信号.
* 它通知进程, 一个与调试相关的陷阱事件发生了.
*
* TRAP_BRKPT: 这是信号的'si_code'值. 它为信号提供了更具体的上下文信息,
* TRAP_BRKPT明确表示这是一个断点陷阱(Breakpoint Trap).
*
* (void __user *)instruction_pointer(regs): 这是信号的'si_addr'值,
* 即导致错误的地址. instruction_pointer(regs) 是一个宏,
* 它从保存的寄存器上下文中提取出程序计数器(PC)的值,
* 也就是那条断点指令所在的地址. __user 关键字是给内核静态分析工具
* (如 sparse)的提示, 表明这是一个用户空间的地址.
*/
force_sig_fault(SIGTRAP, TRAP_BRKPT,
(void __user *)instruction_pointer(regs));
}

/*
* 函数 break_trap
* 这是在 "undef_hook" 中注册的回调函数, 当匹配到断点指令时被内核异常处理框架调用.
* @regs: 指向保存的寄存器上下文的指针.
* @instr: 导致异常的指令的32位编码. (此函数中并未使用该参数).
* @return: 返回0表示成功处理了该异常.
*/
static int break_trap(struct pt_regs *regs, unsigned int instr)
{
/*
* 直接调用 ptrace_break(), 将寄存器上下文传递给它.
* 这个函数作为中间层, 其主要作用是符合 'undef_hook.fn' 的函数签名要求.
*/
ptrace_break(regs);
/*
* 返回0, 向异常处理框架表明这个"未定义指令"异常已经被成功识别并处理,
* 内核不需要再进行其他错误处理(如杀死进程).
*/
return 0;
}

ptrace_break_init: 注册ARM软件断点陷阱

此代码段的核心作用是在Linux内核启动的早期,为ARM架构设置软件断点(Software Breakpoint)的处理机制。它通过“钩子”(hook)的形式,挂接到内核的“未定义指令”(Undefined Instruction)异常处理流程中。当一个程序(通常在调试器gdb的控制下)执行一条特定的断点指令时,CPU会触发一个异常。这段代码确保内核能捕获这个异常,并调用相应的处理函数(break_trap),而不是让系统崩溃。这套机制是ptrace系统调用和调试器功能的基础。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/*
* 定义一个名为 arm_break_hook 的 'struct undef_hook' 变量.
* 这个钩子用于捕获在32位 ARM 指令集模式下执行的软件断点指令.
*/
static struct undef_hook arm_break_hook = {
/*
* .instr_mask: 指令掩码. 用于屏蔽掉指令中不关心的位.
*/
.instr_mask = 0x0fffffff,
/*
* .instr_val: 指令的目标值. 当 CPU 遇到的指令与掩码进行'与'操作后,
* 如果结果等于此值, 则匹配成功. 0x07f001f0 是 ARM 模式下的 BKPT #0 指令的编码.
*/
.instr_val = 0x07f001f0,
/*
* .cpsr_mask: 当前程序状态寄存器(CPSR)的掩码. 这里只关心T-bit.
*/
.cpsr_mask = PSR_T_BIT,
/*
* .cpsr_val: CPSR的目标值. 这里为0, 表示要求T-bit必须为0, 即CPU处于ARM状态.
* (此钩子在STM32H750上永远不会被触发).
*/
.cpsr_val = 0,
/*
* .fn: 当指令和CPSR都匹配成功后, 要调用的回调函数.
* 这里是 break_trap, 它是处理断点陷阱的核心函数.
*/
.fn = break_trap,
};

/*
* 定义一个名为 thumb_break_hook 的钩子.
* 这个钩子用于捕获16位的 Thumb 断点指令.
*/
static struct undef_hook thumb_break_hook = {
.instr_mask = 0xffffffff, // 对于精确匹配, 掩码全为1.
/*
* .instr_val: 0xde01 是16位Thumb指令 BKPT #1 的编码.
* (注意: Thumb指令集手册中此指令编码为 0xBE01, 这里的 0xDE01 可能是针对特定场景或历史原因).
*/
.instr_val = 0x0000de01,
.cpsr_mask = PSR_T_BIT, // 同样只关心T-bit.
/*
* .cpsr_val: 要求T-bit必须为1, 即CPU处于Thumb状态. (在STM32H750上是相关的).
*/
.cpsr_val = PSR_T_BIT,
.fn = break_trap, // 同样调用 break_trap 函数.
};

/*
* 定义一个名为 thumb2_break_hook 的钩子.
* 这个钩子用于捕获32位的 Thumb-2 断点指令.
*/
static struct undef_hook thumb2_break_hook = {
.instr_mask = 0xffffffff, // 精确匹配.
/*
* .instr_val: 0xf7f0a000 是32位Thumb-2指令 BKPT #0 的编码.
*/
.instr_val = 0xf7f0a000,
.cpsr_mask = PSR_T_BIT, // 只关心T-bit.
/*
* .cpsr_val: 要求T-bit必须为1, 即CPU处于Thumb状态. (在STM32H750上是相关的).
*/
.cpsr_val = PSR_T_BIT,
.fn = break_trap,
};

/*
* 函数 ptrace_break_init
* __init 关键字告诉编译器将此函数放入特殊的初始化代码段, 在内核启动完成后,
* 这段代码所占用的内存会被释放.
*/
static int __init ptrace_break_init(void)
{
/*
* 调用 register_undef_hook() 函数, 将 arm_break_hook 添加到内核的
* 未定义指令钩子链表中.
*/
register_undef_hook(&arm_break_hook);
/*
* 注册 thumb_break_hook.
*/
register_undef_hook(&thumb_break_hook);
/*
* 注册 thumb2_break_hook.
*/
register_undef_hook(&thumb2_break_hook);
/*
* 返回0, 表示初始化成功.
*/
return 0;
}

/*
* core_initcall 是一个宏, 它会将 ptrace_break_init 函数的指针放入一个特殊的
* 内存段 (.initcall1.init). 内核的 do_initcalls 机制会在启动的 "core" 阶段
* (级别1) 自动调用此函数.
* 这确保了断点处理机制在系统非常早期的阶段就已经准备就绪.
*/
core_initcall(ptrace_break_init);