DragonOS/docs/kernel/process_management/de_thread.md

160 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DragonOS 多线程 Exec (De-thread) 机制原理
:::{note}
Author: longjin <longjin@dragonos.org>
:::
## 1. 概述
在 POSIX 标准中,`execve` 系统调用用于执行一个新的程序替换当前进程的镜像。对于多线程程序Thread GroupPOSIX 要求 `exec` 成功后:
1. **PID 保持不变**:进程的 PID在内核中通常指 TGID即 Thread Group ID必须保持不变。
2. **单线程化**:原进程组中的所有其他线程都必须被终止,仅保留执行 `exec` 的那个线程(它将成为新的单线程进程)。
如果执行 `exec` 的是线程组的 Leader主线程事情相对简单它只需杀掉其他线程即可。但如果执行 `exec` 的是一个普通线程(非 Leader情况就变得复杂**它必须“变身”为 Leader接管原 Leader 的 PID并清理掉原 Leader**。这个过程被称为“去线程化”De-thread
本文档详细描述 DragonOS 内核中 `de_thread` 的实现原理、流程及并发控制机制。
## 2. 核心挑战
1. **身份窃取 (Identity Theft)**:执行 `exec` 的非 Leader 线程(以下简称 **Exec-Thread**)必须在内核层面交换身份,使得它在用户态和父进程看来,拥有原 Leader 的 PID。
2. **并发互斥**
- 同一进程组内可能有两个线程同时调用 `exec`,或者一个调 `exec` 一个调 `exit_group`。必须保证只有一个能成功开始。
- 在身份交换的临界区Critical Section进程的状态是不稳定的必须防止父进程通过 `wait` 系统调用看到并回收处于中间状态的旧 Leader。
3. **资源继承与清理**Exec-Thread 需要继承原 Leader 的父子进程关系、子进程列表等,同时负责回收旧 Leader 占用的资源。
## 3. 实现机制
DragonOS 参考了 Linux 内核的设计,但在具体实现上结合了 Rust 的所有权和并发安全特性。
### 3.1 关键数据结构
`SigHand`(信号处理结构,由线程组共享)中,引入了以下字段来管理状态:
- `SignalFlags::GROUP_EXEC`:标志位。表示当前线程组正在进行去线程化操作。
- `group_exec_task`: `Weak<ProcessControlBlock>`。指向正在执行 `exec` 的那个线程。
- `group_exec_wait_queue`: `WaitQueue`。用于等待组内其他线程退出。
- `group_exec_notify_count`: `isize`。需要等待退出的线程计数(用于唤醒等待队列)。
### 3.2 核心流程 (`de_thread`)
`de_thread` 函数位于 `kernel/src/process/exec.rs`,是实现该逻辑的核心。
#### 阶段一:发起与互斥
1. **加锁并互斥**:调用 `sighand.start_group_exec()`。若 `GROUP_EXEC``GROUP_EXIT` 已被设置,则返回 `EAGAIN`,保证与并发 `exec`/`exit_group` 互斥。
2. **设置执行者**:记录当前线程到 `group_exec_task`,并清空 `group_exec_notify_count`
3. **单线程快路径**:若线程组为空(仅自己),直接设置 `exit_signal = SIGCHLD` 并结束去线程化流程。
#### 阶段二:终止兄弟线程
1. **构建清理列表**:遍历线程组,收集所有仍存活且未处于退出路径的线程(包括旧 Leader
2. **发送信号**:向清理列表中的线程逐个发送 `SIGKILL`
3. **设置计数**:将清理列表长度写入 `group_exec_notify_count`,用于线程退出时唤醒等待队列。
#### 阶段三:等待同步
1. **进入等待**:在 `group_exec_wait_queue` 上以 killable 方式睡眠。
2. **唤醒条件**
- 遍历 `group_tasks`,确认除当前线程以外已无存活线程;
- 若当前不是 Leader还需确保旧 Leader 已经进入 `Zombie``Dead` 状态;
- 若收到 fatal signal 或等待被打断,则返回 `EAGAIN`
- *注:其他线程在退出路径(`exit_notify`)中会递减计数并唤醒等待队列;若执行者异常退出,也会在该路径中清理 `GROUP_EXEC` 标志。*
#### 阶段四:身份交换 (Identity Theft)
*仅当 Exec-Thread 不是 Leader 时执行*
1. **PID/TID 交换**:调用 `ProcessManager::exchange_tid_and_raw_pids`
- 在全局进程表中交换两者的 PID 映射关系,并交换 PCB 内部的 TID/PID 字段。
- **结果**Exec-Thread 获得原 TGIDOld-Leader 获得临时 PID。
2. **信号语义调整**
- 新 LeaderExec-Thread`exit_signal` 设为 `SIGCHLD`
- Old-Leader 的 `exit_signal` 设为 `INVALID`,避免被当作普通子进程处理。
3. **结构调整**:将 Exec-Thread 设为新的 `group_leader`,并清空两侧 `group_tasks`(去线程化后仅剩一个线程)。
4. **关系继承**
- **子进程**:将 Old-Leader 的 `children` 列表整体迁移到 Exec-Thread 下;
- **父进程**Exec-Thread 继承 Old-Leader 的 `parent`/`real_parent`,并更新 `fork_parent` 为自身。
#### 阶段五:资源清理
1. **回收旧 Leader**:若 Old-Leader 已是 Zombie 或 DeadExec-Thread 直接将其标记为 Dead 并释放 PID 资源。
2. **完成**:无论成功/失败,最终都会清除 `GROUP_EXEC` 标志并唤醒等待者。
### 3.3 并发保护:防止父进程误回收
这是实现中最微妙的部分。
**问题场景**
在阶段三和阶段四之间Old-Leader 已经收到 SIGKILL 并退出变成 Zombie 状态。此时,如果父进程调用 `wait()`,它可能会发现 Old-Leader (PID=TGID) 是 Zombie于是将其回收。
如果父进程在 Exec-Thread 完成 PID 交换**之前**回收了 Old-Leader那么 Exec-Thread 就无法窃取该 PID因为该 PID 对应的进程已经消失了),或者导致逻辑混乱。
**解决方案**
`kernel/src/process/exit.rs``do_wait` 逻辑中,增加了一个检查函数 `reap_blocked_by_group_exec`
```rust
fn reap_blocked_by_group_exec(child_pcb: &Arc<ProcessControlBlock>) -> bool {
// 如果子进程是 Leader且标记了 GROUP_EXEC
// 且执行 exec 的不是它自己,那么说明它正在等待被“篡位”,父进程不能回收它。
if !child_pcb.is_thread_group_leader() {
return false;
}
if !child_pcb.sighand().flags_contains(SignalFlags::GROUP_EXEC) {
return false;
}
let exec_task = child_pcb.sighand().group_exec_task();
exec_task
.as_ref()
.map(|t| !Arc::ptr_eq(t, child_pcb))
.unwrap_or(true)
}
```
这个检查确保了即使 Old-Leader 变成 Zombie只要 `GROUP_EXEC` 标志还在且 `exec_task` 不是它,父进程就无法回收它。这为 Exec-Thread 安全地进行 PID/TID 交换提供了保护伞。
## 4. 流程图
```mermaid
sequenceDiagram
participant P as Parent Process
participant OL as Old Leader (PID=100)
participant ET as Exec Thread (PID=101)
participant S as Sibling (PID=102)
Note over P, S: 初始状态TGID=100. Exec Thread 想要执行 exec
ET->>ET: start_group_exec()<br/>Set GROUP_EXEC flag
ET->>S: Send SIGKILL
ET->>OL: Send SIGKILL
ET->>ET: Wait for siblings & leader death
S->>S: Exit -> Zombie
S-->>ET: Notify (count--)
OL->>OL: Exit -> Zombie
OL-->>ET: Notify (count--)
Note right of P: Parent 调用 wait(PID=100)
P->>OL: Check PID 100 (Zombie)
P->>P: reap_blocked_by_group_exec?<br/>Yes! (Blocked by GROUP_EXEC)
Note right of P: Parent 无法回收 Old Leader继续等待
ET->>ET: Woken up (All dead)
Note over ET, OL: 开始身份交换 (Identity Theft)
ET->>OL: exchange_tid_and_raw_pids()
Note over ET, OL: ET 变成 PID=100<br/>OL 变成 PID=101
ET->>ET: Inherit Parent/Children
ET->>OL: Release/Reap Old Leader
ET->>OL: OL 彻底销毁
ET->>ET: Finish GROUP_EXEC
Note over ET: exec 成功ET 以 PID=100 运行新程序
```
## 5. 参考资料
- **Linux Kernel Source**: `fs/exec.c` (`de_thread` function)
- **DragonOS Source**:
- `kernel/src/process/exec.rs`
- `kernel/src/process/exit.rs`
- `kernel/src/ipc/sighand.rs`