DragonOS/docs/kernel/process_management/de_thread.md

7.9 KiB
Raw Permalink Blame History

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_EXECGROUP_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 已经进入 ZombieDead 状态;
    • 若收到 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-Threadexit_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.rsdo_wait 逻辑中,增加了一个检查函数 reap_blocked_by_group_exec

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. 流程图

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