﻿# 线程启动全流程 —— 主线程与子线程从创建到执行第一条业务指令

> **核心问题**：shell 敲下 `./program` 后，`main()` 是怎么被调到的？`pthread_create()` 返回后，新线程是"已经在跑了"还是"还没出生"？它们的第一个内核函数和第一个用户函数分别是什么？**新线程从未被执行过，它的内核栈上有调用帧吗——调度器凭什么能"跳"过去？从内核态回到用户态时，又是怎么找到线程入口代码的？**


> 本文对比两条截然不同的启动路径——**主线程（execve → _start → main）**和**子线程（clone → ret_from_fork → start_thread → 入口函数）**——拆解每一步的调用栈、内核函数执行过程、用户态/内核态切换时机，以及调度器何时介入。**重点（§2.3.4）拆解 `copy_thread()` 如何伪造内核栈上的 pt_regs 和返回地址、`switch_to()` 如何通过 RSP 切换"跳"到新线程的 `ret_from_fork`、以及 `sysretq` 如何读取伪造的 `pt_regs.rip` 定位用户态入口代码。**


> 前置阅读：线程创建的整体框架见 [thread-creation.md](/concepts/process/thread-creation.md)；进程创建见 [process-creation.md](/concepts/process/process-creation.md)；ELF 加载细节见 [../elf/compile-link-load.md](/concepts/elf/compile-link-load.md)；上下文切换实现见 [context-switch.md](/concepts/process/context-switch.md)；调度器逻辑见 [scheduling.md](/concepts/process/scheduling.md)；段寄存器与特权级切换见 [segment-management.md](/concepts/process/segment-management.md)。

## 零、全景图：两条路径，一个调度器

主线程和子线程虽然最终都在用户态执行，但它们的"出生"路径完全不同：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<path1>> #E3F2FD
  BorderColor<<path1>> #1976D2
  BackgroundColor<<path2>> #C8E6C9
  BorderColor<<path2>> #388E3C
  BackgroundColor<<k>> #FFCDD2
  BorderColor<<k>> #C62828
}
rectangle "路径一：主线程启动\n(shell → execve → _start → main)" <<path1>> {
  rectangle "内核态" <<k>> {
    rectangle "sys_execve()\n销毁旧地址空间\n加载 ELF" as SE1
    rectangle "load_elf_binary()\nmmap 代码/数据段\n准备用户栈(argc/argv/envp)" as SE2
    rectangle "start_thread()\n在 pt_regs 上伪造\n'被中断过'的用户态现场\nCS=0x33 SS=0x2B RIP→_start" as SE3
    SE1 --> SE2
    SE2 --> SE3
  }
  rectangle "用户态" as UE {
    rectangle "_start (crt1.o)\n设置 argc/argv\n调用 __libc_start_main" as UE1
    rectangle "__libc_start_main\n初始化 TLS、调用 init\n调用 main(argc,argv)" as UE2
    rectangle "main()\n★ 第一条业务代码" as UE3
  }
  SE3 --> UE1 : iretq 伪造返回用户态
  UE1 --> UE2
  UE2 --> UE3
}
rectangle "路径二：子线程启动\n(pthread_create → clone → ret_from_fork → 线程入口)" <<path2>> {
  rectangle "内核态" <<k>> {
    rectangle "sys_clone()\ncopy_process\n拷贝/共享资源" as SC1
    rectangle "wake_up_new_task()\n新 task 入队\n→ 等待被调度" as SC2
    rectangle "ret_from_fork()\n新线程首次被调度时\n从此处开始执行" as SC3
    SC1 --> SC2
    SC2 --> SC3
  }
  rectangle "用户态" as UC {
    rectangle "start_thread (glibc)\n注册清理函数\n设置 TLS" as UC1
    rectangle "用户入口函数\n(传给 pthread_create 的 fn)\n★ 第一条业务代码" as UC2
  }
  SC3 --> UC1 : sysretq 返回用户态
  UC1 --> UC2
}
note right of SE3 : 关键：新进程从来没有"陷入内核"\n它是被内核"伪造"出来的
note right of SC3 : 关键：ret_from_fork 不是\n某个真实中断的返回路径\n它是专门为新 fork 的 task 准备的起点
@enduml
```

| 维度 | 主线程（execve 路径） | 子线程（clone 路径） |
|------|---------------------|---------------------|
| 触发者 | shell 调用 `fork()+execve()` | 任一已有线程调用 `pthread_create()` |
| 进程/线程关系 | 新建**进程**（新 PID），主线程即该进程的第一个线程 | 新建**线程**（新 tid），加入已有进程的线程组 |
| 地址空间 | **全新**——旧地址空间销毁，加载 ELF 构建新的 | **共享**——父线程的地址空间（CLONE_VM） |
| 栈 | 内核在用户态地址空间准备初始栈 | glibc 用 `mmap` 分配新栈（默认 ~8MB） |
| 内核入口 | `sys_execve()` | `sys_clone()` |
| 内核出口 | `start_thread()` → `iretq` → 用户态 `_start` | `copy_thread()` → `ret_from_fork` → `sysretq` → 用户态 `start_thread`(glibc) |
| 第一个用户函数 | `_start`（在 crt1.o 中） | `start_thread`（glibc/NPTL 内部） |
| 业务入口 | `main(argc, argv)` | `fn(arg)`（pthread_create 传入的函数指针） |
| 第一次被调度 | execve 返回时立即运行（当前 task 正在 CPU 上） | clone 返回后由调度器择机运行（可能与父线程并发） |

---

## 一、主线程启动：从 execve() 到 main()

### 1.1 概述：shell 如何启动一个程序

用户敲下 `./program` 后，shell 走的是经典的 `fork + execve` 两段式：

```bash
shell (PID 100)
  → fork() → 子进程 (PID 101)
    子进程: execve("./program", argv, envp)  ← 本文从这里开始
```

`execve()` 的特殊之处：它**不返回**（成功时）。调用它的进程镜像被整个替换——旧代码、旧数据、旧栈全部消失，取而代之的是一个全新的程序。从内核视角看，PID 没变，但 `task->mm` 指向了全新的地址空间。

### 1.2 阶段一：内核态 —— sys_execve() 的完整调用链

内核从 `sys_execve()` 进入，经过层层调用，最终在 `load_elf_binary()` 中完成所有繁重工作。以下是**内核函数调用栈**（按调用顺序从上到下）：

```bash
用户态
═══════════════════════════════════ syscall ═══════════════════════════════════
内核态
┌─ sys_execve(filename, argv, envp)            // fs/exec.c
│    ├─ do_execve(getname(filename), argv, envp)
│    │   └─ do_execveat_common(AT_FDCWD, filename, argv, envp, 0)
│    │       ├─ alloc_bprm()                    // 分配 linux_binprm 结构体
│    │       │   └─ 分配临时内核空间，拷贝 argv、envp
│    │       ├─ prepare_bprm_creds()            // 准备新凭证（若 setuid 会在此切换）
│    │       ├─ exec_binprm(bprm)
│    │       │   └─ search_binary_handler(bprm)
│    │       │       └─ load_elf_binary(bprm)   // ★ 核心：fs/binfmt_elf.c
│    │       │           ├─ elf_check_arch()     // 校验 ELF 架构匹配（x86-64）
│    │       │           ├─ flush_old_exec()     // 销毁旧地址空间：释放页表、
│    │       │           │                        //   清空 mmap、重置信号处理
│    │       │           ├─ setup_new_exec()     // 设置新进程属性
│    │       │           │   └─ arch_pick_mmap_layout()  // 决定 mmap 基址
│    │       │           ├─ 解析 ELF Program Headers
│    │       │           │   └─ elf_map()         // ★ mmap 每个 PT_LOAD 段
│    │       │           │       (代码段: PROT_READ|PROT_EXEC, MAP_PRIVATE)
│    │       │           │       (数据段: PROT_READ|PROT_WRITE, MAP_PRIVATE)
│    │       │           │       (BSS: 匿名映射，初始化为 0)
│    │       │           ├─ 处理 ELF Interpreter (PT_INTERP)
│    │       │           │   └─ load_elf_interp()  // 加载 ld-linux-x86-64.so.2
│    │       │           │       返回 ld.so 的入口地址
│    │       │           ├─ set_brk()             // 设置堆 (brk) 起始地址
│    │       │           ├─ create_elf_tables()   // ★ 构建用户态初始栈
│    │       │           │   栈底: argc
│    │       │           │   栈底+8: argv[0], argv[1], ..., NULL
│    │       │           │   栈底+N: envp[0], envp[1], ..., NULL
│    │       │           │   栈底+M: auxiliary vector (AT_PHDR, AT_ENTRY,
│    │       │           │          AT_PAGESZ, AT_RANDOM, ...)
│    │       │           ├─ 确定入口地址:
│    │       │           │   动态链接 → ld.so 的入口 (e_entry of interpreter)
│    │       │           │   静态链接 → ELF 自己的 e_entry (即 _start)
│    │       │           └─ start_thread(regs, entry, sp)
│    │       │               ★ 伪造 pt_regs，让进程"返回到"新程序入口
│    │       └─ ... (execve 成功后不返回此处)
└──────────────────────────────────────────────────────────────────────
```

**关键内核函数的工作：**

| 内核函数 | 做了什么 | 对后续的影响 |
|---------|---------|------------|
| `flush_old_exec()` | 释放旧进程的全部页表、`mm_struct`，清空 VMA（虚拟内存区域）链 | 旧代码彻底消失，execve "不可能返回"的根源 |
| `elf_map()` | 调用 `vm_mmap()` 把 ELF 的每个 `PT_LOAD` 段映射到虚拟地址空间 | 代码段 (R+X)、数据段 (R+W) 就位，缺页时从磁盘读入 |
| `create_elf_tables()` | 在用户栈顶写入 `argc`, `argv[]`, `envp[]`, `auxv[]` | `_start` 能拿到 `argc`/`argv`——它们已经在栈上 |
| `load_elf_interp()` | 把 `ld-linux-x86-64.so.2` 也 mmap 进来 | 动态链接的程序先跑 ld.so，做完重定位后才跳 `main` |
| `start_thread()` | 修改当前 task 内核栈上的 `pt_regs`，设置 `CS/SS/RIP/RSP` | `execve` "返回"时不回到旧代码，而是出现在 `_start` |

### 1.3 阶段二：start_thread() —— 伪造"被中断过"的用户态返回现场

这是整个 execve 路径里最精妙的一步。execve 本质上是当前 task 发出的**一个普通系统调用**——它和 `read`/`write` 一样，是通过 `syscall` 指令陷入内核的。系统调用返回时，CPU 会根据内核栈上保存的 `pt_regs` 恢复用户态寄存器并执行 `sysretq`（或 `iretq`），回到 `syscall` 下一条指令。

**但 execve 不打算回原来的代码了**。所以 `start_thread()` 直接**覆盖**当前 task 内核栈上的 `pt_regs`，把它改成新程序的执行环境：

```bash
当前 task 的内核栈（在 start_thread 之前）：
┌──────────────────────────────┐
│ pt_regs (由 syscall 入口保存)  │  ← RIP 指向 syscall 下一条指令
│   CS=0x33, SS=0x2B           │      RSP 指向原用户态栈
│   RIP=原代码地址              │      ...
│   RSP=原用户栈指针            │
├──────────────────────────────┤
│ ... (内核栈剩余部分) ...       │
└──────────────────────────────┘
start_thread 覆盖后：
┌──────────────────────────────┐
│ pt_regs (被"伪造"过)          │  ← RIP 改为 _start (或 ld.so 入口)
│   CS=0x33    ← 用户代码段     │     RSP 改为新用户栈顶
│   SS=0x2B    ← 用户数据/栈段   │     RFLAGS 清零（无 IF 中断也暂时不开）
│   RIP=entry  ← 新程序的入口   │
│   RSP=new_sp ← 新用户栈顶     │     其他通用寄存器全部清零
│   RFLAGS=0                    │
├──────────────────────────────┤
│ ... (内核栈剩余部分) ...       │
└──────────────────────────────┘
```

对应的内核代码（简化）：

```c
// arch/x86/kernel/process_64.c
void start_thread(struct pt_regs *regs, unsigned long new_ip,
                  unsigned long new_sp)
{
    // 设置段寄存器——关键：CS=0x33 用户代码段、SS=0x2B 用户数据段
    start_thread_common(regs, new_ip, new_sp,
                        __USER_CS, __USER_DS, 0);
    // __USER_CS = 0x33  (GDT[6], RPL=3, 用户态 64 位代码段)
    // __USER_DS = 0x2B  (GDT[5], RPL=3, 用户态数据段)
}
```

> **为什么 CS=0x33？** 这是本文最关键的数字之一。`0x33` = Index 6 + TI=0 + RPL=3 → GDT[6] = 用户代码段描述符，DPL=3。CPU 根据 `CS.Selector.RPL` 决定 CPL（Current Privilege Level），所以进程启动时 CPL=3 即用户态。这对数字的完整解释见 [segment-management.md](/concepts/process/segment-management.md) §二。

**start_thread 的调用参数来源：**

| 参数 | 值 | 来源 |
|------|-----|------|
| `new_ip` (→ RIP) | 动态链接：ld.so 的 e_entry；静态链接：ELF 的 e_entry（即 `_start`） | ELF header → `elf_entry` → 若动态链接用 interp 的入口 |
| `new_sp` (→ RSP) | 用户栈顶地址 | `create_elf_tables()` 写入 `argc/argv/envp/auxv` 后栈顶的位置 |

### 1.4 阶段三：sysretq → 用户态 _start

`start_thread()` 修改完 `pt_regs`，内核函数调用链依次返回。当返回到 `do_execveat_common()` → 系统调用出口路径时，内核发现 `pt_regs` 已经指向了新程序的入口，于是：

```bash
syscall 出口路径
  → arch_exit_to_user_mode_prepare()
    → exit_to_user_mode_loop()
      → exit_to_user_mode_prepare()
        → sysretq (或 iretq)
          ★ CPU 从 pt_regs 恢复:
             CS = 0x33 (→ CPL=3，用户态，64 位代码段)
             SS = 0x2B
             RIP = _start  ← 新程序的真正入口
             RSP = 新用户栈顶
             RFLAGS = 0 (初始状态)
          ★ 执行流跳转到 _start，从此跑在用户态
```

> **一句话**：execve 是内核"借用"当前 task 的 syscall 返回路径，把它"劫持"到了新程序的 `_start`。当前进程中原始程序的所有痕迹——代码、数据、栈、页表——已在内核中提前被 `flush_old_exec()` 彻底清除。

### 1.5 阶段四：_start → __libc_start_main → main()

进入用户态后，第一个执行的函数是 `_start`。它不在你的代码里——它在 `crt1.o`（C Runtime）中，由链接器在编译时自动加入。

#### 1.5.1 静态链接程序的路径（简化）

```bash
_start (crt1.o, AT&T 汇编示意):
    xorl   %ebp, %ebp          ; 清空帧指针（标记调用链顶端）
    movl   %edx, %r9d          ; 传递 fini 函数指针
    popq   %rsi                ; RSI = argc（栈顶第一个值）
    movq   %rsp, %rdx          ; RDX = argv（argc 弹出后 RSP 指向 argv[0]）
    andq   $-16, %rsp          ; 栈 16 字节对齐（ABI 要求）
    call   __libc_start_main   ; 调用 glibc 初始化 → 最终调 main
    hlt                        ; __libc_start_main 永远不返回这里
```

#### 1.5.2 动态链接程序的路径（先跑 ld.so）

动态链接的程序更复杂——`start_thread()` 设置的 `RIP` 不是你自己程序的 `_start`，而是 **ld.so 的入口**。

```bash
阶段 4a: ld.so 自举
────────
RIP = ld-linux-x86-64.so.2 的 _start
  → _dl_start()              // ld.so 自举：重定位自己
    → _dl_start_final()
      → _dl_sysdep_start()
        → 解析 auxiliary vector（从栈上 auxv 读取 AT_PHDR/AT_ENTRY 等）
阶段 4b: 加载依赖库
────────
  → dl_main()
    → _dl_init_paths()       // 初始化库搜索路径（LD_LIBRARY_PATH 等）
    → 遍历 ELF .dynamic 段，加载所有 DT_NEEDED 的 .so
      → _dl_map_object_from_fd()  // mmap 每个 .so
    → _dl_relocate_object()  // 重定位：填写 GOT/PLT
    → _dl_start_user:        // 跳回主程序的 _start
      → jmp *%r12             // R12 = 主程序的 _start 地址
阶段 4c: 主程序的 _start
────────
_start (crt1.o):
    → call __libc_start_main(main, argc, argv, init, fini, ...)
```

#### 1.5.3 __libc_start_main() 做了什么

```c
// glibc: csu/libc-start.c (简化)
int __libc_start_main(
    int (*main)(int, char **, char **),  // main 的函数指针
    int argc,
    char **argv,
    void (*init)(void),    // __libc_csu_init → .init_array
    void (*fini)(void),    // .fini_array
    void (*rtld_fini)(void),
    void *stack_end)
{
    // 1. 初始化 TLS: arch_prctl(ARCH_SET_FS, tcb)
    //    将 FS 段基址指到线程控制块 (TCB)
    __tls_init_tp();
    // 2. 设置 __environ (全局 environ 指针，指向栈上的 envp)
    __environ = argv + argc + 1;
    // 3. 调用初始化函数
    if (init)
        (*init)(argc, argv, __environ);  // → __libc_csu_init → .init_array
    // 4. ★ 调用你的 main
    int result = main(argc, argv, __environ);
    // 5. 调用 exit(result) —— main 返回后不直接结束，
    //    exit() 会: 调 atexit 函数 → 调 .fini_array → _exit 系统调用
    exit(result);
}
```

> **`__libc_start_main()` 永远不会返回到 `_start`**——`main()` 返回后它调用 `exit()`，而 `exit()` 最终调用 `_exit` 系统调用，整个进程在此终止。

### 1.6 主线程启动的完整调用栈总览

```bash
══════════════════ 用户态 ══════════════════
main(argc, argv)                           ★ 你的代码
  ↑ __libc_start_main() 调用
__libc_start_main(main, argc, argv, ...)   glibc 框架
  ├─ __tls_init_tp()                      设置 TLS (ARCH_SET_FS)
  └─ init() → __libc_csu_init             .init_array 初始化
  ↑ call from _start
_start (crt1.o)                            汇编入口
══════════════════ syscall ══════════════════
  ↑ sysretq —— CPU 从 pt_regs 恢复，返回到 _start
══════════════════ 内核态 ══════════════════
start_thread(regs, entry, sp)              伪造 pt_regs:
  start_thread_common(regs, ip, sp,        CS = __USER_CS  (0x33)
                      __USER_CS,           SS = __USER_DS  (0x2B)
                      __USER_DS, 0)        RIP = entry (_start 或 ld.so)
  ↑ load_elf_binary() 调用
load_elf_binary(bprm)                      加载 ELF:
  ├─ flush_old_exec()                     销毁旧地址空间
  ├─ elf_map() × N                        mmap 代码/数据段
  ├─ load_elf_interp()                    加载 ld.so（动态链接）
  ├─ create_elf_tables()                  构建用户栈: argc/argv/envp/auxv
  └─ start_thread()                       伪造返回现场
  ↑ exec_binprm() → search_binary_handler() 调用
do_execveat_common(AT_FDCWD, filename, ...)
  ↑ do_execve() → sys_execve() 调用
══════════════════ syscall ══════════════════
  ↑ 系统调用入口 (syscall 指令)
══════════════════ 用户态（原程序）══════════
execve("/bin/program", argv, envp)
```

---

## 二、子线程启动：从 pthread_create() 到线程入口函数

### 2.1 概述：父线程如何制造子线程

`pthread_create()` 是 POSIX 线程创建接口，用户代码看起来是"函数调用"——但实际上它内部走了一个完整的 `clone()` 系统调用，在内核中创建一个可独立调度的新 task。

与 execve 最根本的区别：**子线程没有"原程序被替换"的过程**——父线程和子线程共享同一地址空间，只是执行流分叉了。

### 2.2 阶段一：用户态准备 —— glibc/NPTL 分配栈和 TLS

在发起 `clone()` 系统调用之前，glibc（具体是 NPTL —— Native POSIX Thread Library）需要准备好新线程"安身立命"的资源：

```bash
用户态：pthread_create(&tid, &attr, func, arg)
  → __pthread_create_2_1()                 // nptl/pthread_create.c
      阶段 1a: 分配线程栈
      ├─ allocate_stack(iattr, &pd, ...)   // nptl/allocatestack.c
      │   ├─ mmap(NULL, stacksize,          // 默认 8MB
      │   │       PROT_READ|PROT_WRITE,
      │   │       MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK,
      │   │       -1, 0)
      │   │   → 返回地址如 0x7ffff73fd000（栈顶附近）
      │   ├─ 在栈顶下方放置 guard page (PROT_NONE)
      │   │   → 栈溢出时会触发 SIGSEGV 而非悄无声息覆盖其他数据
      │   └─ pd->stackblock = mmap 返回的地址
      │       pd->stackblock_size = stacksize
      阶段 1b: 准备 TLS
      ├─ _dl_allocate_tls_storage()          // 为 TCB 分配内存
      │   └─ pd->tcb = 分配的 TCB (Thread Control Block)
      └─ 在 TCB 中初始化 TLS 变量槽位 (errno 等)
      阶段 1c: 设置 clone 参数
      └─ pd->start_routine = func;          // 用户传入的线程入口函数
```

此时新线程的用户栈布局（`pd->stackblock` 指向的区域）：

```bash
0x7ffff73fd000  高地址
┌──────────────────────────┐
│ 栈顶 (RSP 初值指向这里)     │ ← pd->stackblock + stacksize
│ ... 给新线程用的栈空间 ...  │    (实际 RSP 由内核在 clone 时设置)
│                          │
│ 栈帧: start_thread 的参数  │ ← glibc 在这里写好 func 和 arg
├──────────────────────────┤
│ guard page (PROT_NONE)   │   栈溢出保护
└──────────────────────────┘
0x7ffff73fc000  低地址
```

### 2.3 阶段二：clone() 系统调用 —— 父线程进入内核

glibc 准备好环境后，发起 `clone()` 系统调用。

#### 2.3.1 用户态 → 内核态

```c
// glibc 内部: 通过内联汇编发起 clone
// 关键参数:
//   clone_flags = CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|
//                 CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|
//                 CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID
//   child_stack = pd->stackblock + pd->stackblock_size  (新线程栈顶)
//   parent_tid  = &pd->tid    (内核填回 tid)
//   tls         = pd->tcb     (TLS 描述符)
//   child_tid   = &pd->tid    (线程退出时内核清理用)
// 实际调用: clone(fn_wrapper, child_stack, clone_flags, arg, ...)
//   → syscall(SYS_clone, ...)           ← ★ 模式切换: 用户态 → 内核态
```

#### 2.3.2 内核态调用链

```bash
用户态
════════════════════════════════════════ syscall ════════════════════════════
内核态（运行在父线程的上下文）
┌─ sys_clone(flags, child_stack, parent_tid, child_tid, tls)
│   └─ kernel_clone(args)
│       ├─ copy_process(NULL, trace, node, args)
│       │   ├─ dup_task_struct(current)       // 分配新 task_struct
│       │   │   ├─ alloc_task_struct_node()   //   kmalloc task_struct
│       │   │   ├─ alloc_thread_stack_node()  // ★ 分配子线程的内核栈
│       │   │   │   (通常 16KB: THREAD_SIZE, 两个页)
│       │   │   └─ arch_dup_task_struct()
│       │   │       └─ 复制 parent 的 thread_struct (里面有 TLS 等)
│       │   │
│       │   ├─ 检查资源限制 (RLIMIT_NPROC, 信号数等)
│       │   │
│       │   ├─ copy_creds()             // 共享/复制凭证
│       │   │
│       │   ├─ sched_fork()             // 调度器初始化
│       │   │   └─ p->state = TASK_NEW  // 状态设为 "新生"
│       │   │
│       │   ├─ ★ 根据 flags 共享/复制各类资源:
│       │   │   ├─ copy_mm()            // CLONE_VM=1 → 共享 mm，不复制页表
│       │   │   ├─ copy_files()         // CLONE_FILES=1 → 共享 fd 表
│       │   │   ├─ copy_fs()            // CLONE_FS=1 → 共享 cwd/umask
│       │   │   ├─ copy_sighand()       // CLONE_SIGHAND=1 → 共享信号处理表
│       │   │   ├─ copy_signal()        // CLONE_THREAD=1 → 共享 signal_struct
│       │   │   └─ copy_namespaces()    // 共享 namespace
│       │   │
│       │   ├─ copy_thread(flags, child_stack, child_stack_size)
│       │   │   // ★ 关键：设置子线程的内核栈上的"初始帧"
│       │   │   //   → 让子线程首次被调度时从 ret_from_fork 开始
│       │   │   //   → 设置子线程返回用户态后的 RIP (start_thread) 和 RSP (新栈)
│       │   │
│       │   ├─ 分配 PID/tid → p->pid = 新 tid
│       │   │
│       │   └─ CLONE_SETTLS → 设置 p->thread.fsbase = tls 参数
│       │
│       ├─ wake_up_new_task(p)
│       │   ├─ p->state = TASK_RUNNING  // ★ 状态变为 "可运行"
│       │   ├─ activate_task(rq, p)     // 加入运行队列
│       │   │   └─ enqueue_task(rq, p)
│       │   │       └─ 按 vruntime 插入 CFS 红黑树
│       │   ├─ check_preempt_curr()     // 检查是否需要抢占当前 task
│       │   │   └─ 若新线程的 vruntime < 当前 task → 置 need_resched
│       │   └─ 返回
│       │
│       └─ clone 返回子线程的 tid (给父线程的返回值)
```

> **关键观察**：`clone()` 系统调用在**父线程的内核上下文**中执行上述所有步骤。子线程的 `task_struct` 在 `copy_process()` 中被创建，但在 `wake_up_new_task()` 之后才真正变为可运行。此时子线程可能还在运行队列中排队——**它的第一条指令尚未被执行**。

#### 2.3.3 copy_thread() —— 子线程内核栈的初始帧

`copy_thread()` 是新线程出生点的关键。它在子线程的**内核栈**上构建一个伪造的"被中断过"的栈帧，确保子线程首次被调度时从 `ret_from_fork` 开始执行：

```c
// arch/x86/kernel/process.c (简化)
int copy_thread(unsigned long clone_flags, unsigned long sp,
                unsigned long arg, struct task_struct *p, ...)
{
    struct inactive_task_frame *frame;
    struct pt_regs *childregs;
    // childregs 指向子线程内核栈上 pt_regs 的位置
    childregs = task_pt_regs(p);
    *childregs = *current_pt_regs();  // 先拷贝父线程的 pt_regs
    // ★ 然后根据 clone_flags 修改:
    childregs->ax = 0;                // 子线程的 fork/clone 返回值 = 0
    if (sp) {
        // ★ sp != 0 → 这是用户线程（CLONE_VM）
        childregs->sp = sp;           // RSP → 新线程的用户栈
    }
    // 设置 ret_from_fork 的返回地址（子线程首次被调度时从这里开始）
    p->thread.sp = (unsigned long)childregs;
    p->thread.ip = (unsigned long)ret_from_fork;
    // 设置 TLS
    if (clone_flags & CLONE_SETTLS)
        p->thread.fsbase = ...;       // FS 段基址 → TCB
    return 0;
}
```

子线程内核栈上的初始布局：

```bash
子线程的内核栈顶
┌──────────────────────────────────┐
│ pt_regs (从父线程拷贝 + 修改)     │  ← childregs = task_pt_regs(p)
│   CS = 0x33                      │     ★ 用户代码段（与父线程相同）
│   SS = 0x2B                      │     ★ 用户数据段
│   RSP = child_stack               │     ★ 新线程的独立用户栈顶
│   RIP = glibc start_thread 地址   │     ★ 返回用户态后第一跳
│   RAX = 0                        │     ★ clone() 返回值 = 0（子线程视角）
│   其他通用寄存器 = 父线程的快照     │
├──────────────────────────────────┤
│ inactive_task_frame              │
│   rip = ret_from_fork            │  ← p->thread.ip: 首次被调度时从此开始
├──────────────────────────────────┤
│ ... (剩余内核栈空间) ...          │
└──────────────────────────────────┘
```

> **为什么 `ret_from_fork`？** 子线程并不是从某个"真实中断/系统调用"返回——它从未跑过任何代码。但内核用同一个机制来"假扮"它曾被中断过：在内核栈上伪造 `pt_regs`，然后通过 `ret_from_fork` → 系统调用出口路径 → `sysretq` 进入用户态。这就是为什么 `copy_thread` 既要设置 `p->thread.ip`（内核态起点）也要设置 `pt_regs`（用户态终点）。

#### 2.3.4 深入：新线程内核栈到底是不是"空的"？调度器如何"凭空"找到入口？

这是理解新线程启动的核心问题。拆成三个子问题逐步解答。

##### 问题 1：新线程的内核栈上有调用帧吗？

**不是空的，但也不是"正常函数调用产生的调用帧"——是 `copy_thread()` 手工构建的一条伪造帧链。**

对比一下"正常"的内核栈长什么样：

```bash
正常线程的内核栈（比如从 syscall 陷入后）：
┌──────────────────────────────────┐
│ pt_regs                          │  ← syscall 入口硬件自动压入（SS/RSP/
│   (由 CPU 在陷入时自动压栈)       │     RFLAGS/CS/RIP/err/...）
│   包括: CS, SS, RIP, RFLAGS,     │
│         RSP, RAX, ... 等         │
├──────────────────────────────────┤
│ 函数调用帧 A (call xxx)          │  ← syscall 处理函数层层调用，每层
│ 函数调用帧 B (call yyy)          │    `call` 压一个返回地址
│ 函数调用帧 C ...                │
├──────────────────────────────────┤
│ ...                              │
└──────────────────────────────────┘
新线程的内核栈（copy_thread 伪造后）：
┌──────────────────────────────────┐ ← RSP 在这里（p->thread.sp 指向这里）
│ pt_regs                          │    不是硬件压的！是 copy_thread()
│   CS  = 0x33        ← 用户代码段 │    从父线程拷贝后逐字段修改的
│   SS  = 0x2B        ← 用户数据段 │
│   RSP = child_stack ← 新线程独立用户栈 │
│   RIP = start_thread(glibc 地址) │  ★ 这是"返回用户态后去哪里"的答案
│   RAX = 0           ← clone 返回值 │
│   ...                            │
├──────────────────────────────────┤
│ inactive_task_frame              │    这也不是 `call` 指令压的！
│   其他 callee-saved 寄存器       │    是 copy_thread() 手写在这里的
│   ...                            │
├──────────────────────────────────┤
│ 返回地址 = ret_from_fork         │  ← p->thread.ip 的值被写在栈上
│                                  │     switch_to 返回时从这里 pop
├──────────────────────────────────┤
│ 空（未使用的内核栈剩余空间）       │
└──────────────────────────────────┘
```

关键区别：正常内核栈上的帧是 `call` 指令的产物（硬件自动压返回地址、被调用函数压 `rbp` 形成链），而新线程栈上的帧是 **`copy_thread()` 手工 memset-like 写进去的值**。它们是"长得像栈帧的数据结构"，不是"函数调用的历史记录"。

> **一句话**：新线程内核栈上有数据（`pt_regs` + `inactive_task_frame`），但它们是**被构造出来骗 CPU 的**——骗 CPU 相信"这个线程曾经中断过，现在该返回了"。

---

##### 问题 2：switch_to() 如何"跳"到从未运行过的新线程？

这是上下文切换的魔法时刻。结合 `switch_to` 的实现来追踪整个栈切换过程：

```asm
; arch/x86/entry/entry_64.S → switch_to(prev, next), 简化
switch_to:
    ; ───── 步骤 ①: 保存当前线程的现场 ─────
    pushq   %rbp                    ; 压 callee-saved 寄存器
    pushq   %rbx                    ; 到当前线程的内核栈
    pushq   %r12
    pushq   %r13
    pushq   %r14
    pushq   %r15
    ; ───── 步骤 ②: 保存/切换栈指针 ★ ─────
    movq    %rsp, THREAD_SP(%rdi)   ; prev->thread.sp = 当前 RSP
                                    ;  ← "记住我停在哪了"
    movq    THREAD_SP(%rsi), %rsp   ; RSP = next->thread.sp
                                    ;  ← ★★★ 全世界在这一行换了！
                                    ;  对于新线程: next->thread.sp =
                                    ;     copy_thread 里设置的 childregs 地址
                                    ;     即 → 指向伪造的 pt_regs 顶部
    ; ───── 步骤 ③: 从新栈恢复现场 ─────
    popq    %r15                    ; 从新线程的内核栈弹出
    popq    %r14
    popq    %r13
    popq    %r12
    popq    %rbx
    popq    %rbp
    ; ───── 步骤 ④: "返回"到新线程的执行流 ─────
    ret                             ; ★ 从新线程的内核栈 pop 返回地址
                                    ;   对于新线程: 这个地址是 ret_from_fork
                                    ;   （因为 p->thread.ip 栈上是这个值）
```

逐步骤追踪：

```bash
时刻     CPU 寄存器                    新线程内核栈（栈顶在上）
──────────────────────────────────────────────────────────────────
t0       RSP = 父线程内核栈             ┌────────────────────┐
        （switch_to 尚未切栈）          │ pt_regs            │ ← next->thread.sp 指向这里
                                        │   ...              │
                                        ├────────────────────┤
                                        │ inactive_task_frame│
                                        ├────────────────────┤
                                        │ 返回地址:           │
                                        │   ret_from_fork    │ ← ★
                                        └────────────────────┘
t1       RSP = 父线程内核栈
        （push 了 6 个 callee-saved 寄存器到父线程栈上）
t2       movq %rsp, prev->thread.sp
        → 保存了父线程的栈指针
t3       movq next->thread.sp, %rsp    ┌────────────────────┐ ← ★ RSP 现在指这里！
        → ★★★ RSP 换了！              │ pt_regs            │
        现在 RSP 指向新线程内核栈       │   RIP = start_thread│
        的 pt_regs 顶部               │   RSP = child_stack │
                                        │   CS = 0x33         │
                                        │   ...              │
                                        ├────────────────────┤
                                        │ inactive_task_frame│
                                        ├────────────────────┤
                                        │ ret_from_fork      │ ← ret 指令从这里 pop
                                        └────────────────────┘
t4       popq %r15 ~ %rbp
        → 从新线程栈弹出 callee-saved 寄存器（都是初始值/0）
t5       ret
        → pop 返回地址 = ret_from_fork
        → RIP = ret_from_fork
        → ★ 新线程的第一条内核指令被执行了！
```

> **核心机制**：`switch_to` 不需要"知道"跳转目标——它只是机械地做 `mov next->thread.sp, %rsp` 然后 `pop` 和 `ret`。因为 `copy_thread()` 已经把 `ret_from_fork` 的地址写在了新内核栈正确的位置上（`p->thread.sp` 指向的栈顶区域），`ret` 指令自然就跳到了 `ret_from_fork`。

---

##### 问题 3：从 ret_from_fork 如何找到用户态入口代码？

`ret_from_fork` 执行时，RSP 恰好指向伪造的 `pt_regs`（见上一步 t5 时刻的栈图）。它走的是**标准的系统调用返回路径**：

```asm
; arch/x86/entry/entry_64.S
ret_from_fork:
    ; RSP → pt_regs（copy_thread 伪造的）
    call    schedule_tail           ; 调度后清理
    ; 检查是否需要处理信号/seccomp
    testb   $3, CS-ORIG_RAX(%rsp)
    jnz     ret_from_fork_orig
    ; ★ 走标准 syscall 返回路径
    ; GLOBAL(entry_SYSCALL_64_after_hwframe) 之后的逻辑：
    ;
    ;   从 pt_regs 恢复通用寄存器:
    ;     pop rax, pop rcx, pop rdx, pop rsi, ...
    ;
    ;   然后 sysretq:
    ;     RIP := RCX  ← pt_regs.rip = start_thread (glibc 用户态地址)
    ;     RSP := R11  ← pt_regs.rsp = child_stack
    ;     CS  := MSR_STAR[63:48] + 16 | 3 = 0x33
    ;     SS  := MSR_STAR[63:48] + 8  | 3 = 0x2B
    ;     ★ CPL 从 0 变为 3，进入用户态
    jmp     .Lsysret_return
```

**用户态入口代码的定位过程：**

```bash
copy_thread() 在创建时设置的 pt_regs:
  ┌──────────────────────────────┐
  │ pt_regs.rip = 0x7ffff7xxx... │  → glibc 的 start_thread 函数地址
  │ pt_regs.rsp = 0x7ffff73fe... │  → 新线程的用户栈顶
  │ pt_regs.cs  = 0x33           │  → 用户代码段
  │ pt_regs.ss  = 0x2B           │  → 用户数据段
  │ pt_regs.rax = 0              │  → clone() 返回值 = 0
  └──────────────────────────────┘
         │
         │  sysretq 读取这些值
         ▼
  ┌──────────────────────────────────────┐
  │  用户态: RIP = glibc start_thread     │
  │          RSP = child_stack            │
  │          CS  = 0x33 (CPL=3)           │
  │          RAX = 0  ("我是子线程")       │
  │                                      │
  │  start_thread(pd):                   │
  │    → ARCH_SET_FS(pd)  // 设 TLS      │
  │    → pd->start_routine(pd->arg)      │
  │    → 用户入口函数 func(arg) 执行 ★    │
  └──────────────────────────────────────┘
```

> **两行关键赋值**（`copy_thread` 中）：

> - `childregs->rip = start_thread`  → **决定了用户态的第一条指令**

> - `p->thread.ip = ret_from_fork`   → **决定了内核态的第一条指令**


> `p->thread.sp = childregs`（指向 `pt_regs` 顶部）确保 `switch_to` 切完栈后，`ret` 弹出的地址是 `ret_from_fork`，而 `ret_from_fork` 看到的 `pt_regs` 恰好在 RSP 位置。

##### 完整的数据流图：谁在什么时候写了什么，谁在什么时候读了什么

```bash
创建时（copy_thread，父线程上下文）：
────────────────────────────────────────
新线程
内核栈
  [pt_regs]           ← ① childregs->rip = glibc_start_thread
    .rip = 0x7ffff...      childregs->rsp = child_user_stack
    .rsp = 0x7ffff...      childregs->cs  = 0x33
    .cs  = 0x33            childregs->ax  = 0
    ...
  [inactive_frame]
  [返回地址]           ← ② 栈上写入 ret_from_fork 地址
task_struct
  thread.sp ──────────→ ③ 指向 pt_regs 顶部
  thread.ip = ret_from_fork
────────────────────────────────────────
     ↓  调度时刻（某 CPU 的 context_switch）
────────────────────────────────────────
switch_to(prev, next):
  ④ 读  next->thread.sp
  ⑤ mov next->thread.sp, %rsp     → RSP = pt_regs 顶部
  ⑥ pop %r15...%rbp               → 从新栈恢复 callee-saved 寄存器
  ⑦ ret                           → 从新栈 pop 返回地址
                                     = ret_from_fork
────────────────────────────────────────
     ↓  进入 ret_from_fork
────────────────────────────────────────
ret_from_fork:
  ⑧ RSP 正指向 pt_regs（第⑤步设置）
  ⑨ call schedule_tail
  ⑩ jmp syscall 返回路径
  ⑪ 读  pt_regs.rip  → RCX
     读  pt_regs.rsp  → R11
     读  pt_regs.cs   → 用户 CS
  ⑫ sysretq  → CPL=3, RIP=glibc_start_thread ★
```

---

### 2.4 阶段三：子线程被调度 —— ret_from_fork → 用户态

`wake_up_new_task()` 将子线程加入运行队列后，**父线程继续执行**——`clone` 系统调用返回到 glibc，`pthread_create` 返回到用户代码。而子线程的"第一条指令"何时执行，完全由调度器决定。

> 内核栈的伪造机制和 `switch_to` → `ret_from_fork` → `sysretq` 的栈切换全流程，已在上一节（§2.3.4）逐步骤拆解，本节不再重复。

#### 2.4.1 子线程首次被调度时的内核路径

```bash
调度器选中新线程（由某 CPU 核的 pick_next_task 选中）
  → context_switch(prev, next)   // next = 子线程
    ├─ switch_mm()               // 同进程线程 → 共享 mm → CR3 不变
    └─ switch_to(prev, next)     // ★ 切换寄存器现场
        → mov next->thread.sp, %rsp    // RSP → 子线程内核栈（指向 pt_regs）
        → pop callee-saved 寄存器
        → ret                      // pop 返回地址 = ret_from_fork
          → ★ 执行流"变成"了子线程，第一条内核指令: ret_from_fork
```

#### 2.4.2 ret_from_fork 的执行流程

```asm
; arch/x86/entry/entry_64.S (简化)
ret_from_fork:
    ; 此时跑在子线程的内核栈上，pt_regs 已就位
    call schedule_tail           ; 调度后清理: 释放 prev 的引用
                                 ; 若设置了 need_resched，可能会再次 schedule
    ; 检查是否有系统调用工作 (seccomp / trace)
    testb   $3, CS-ORIG_RAX(%rsp)
    jnz     ret_from_fork_orig
    ; 没有额外工作 → 直接返回用户态
    ; pt_regs 已经指向:
    ;   RIP → glibc 的 start_thread
    ;   RSP → 新线程的用户栈
    ;   CS  → 0x33 (用户态)
    ;   SS  → 0x2B
    jmp     .Lsysret_return     ; → sysretq → 用户态
```

> **schedule_tail() 的作用**：它标志着"上一个 task 已经被我们彻底抢占成功了"，做一些引用计数清理和抢占标志检查。如果有信号待处理，也会在这里处理。

#### 2.4.3 sysretq → 进入用户态

```bash
sysretq (CPU 硬件操作):
  CS.Selector := MSR_STAR[63:48] | 3  = 0x33  (用户代码段, RPL=3)
  SS.Selector := MSR_STAR[63:48] + 8 | 3 = 0x2B (用户数据段)
  RIP := RCX   → 即 glibc 的 start_thread 地址
  RSP := R11   → 即新线程的用户栈顶
  RFLAGS := R11 (来自 pt_regs)
  ★ CPL 从 0 变为 3 —— 已进入用户态
```

> 关于 sysretq 如何切换段寄存器的完整机制，见 [segment-management.md](/concepts/process/segment-management.md) §五。

### 2.5 阶段四：用户态 start_thread → 真正的线程入口函数

子线程进入用户态后，第一个运行的函数是 **glibc 内部的 `start_thread`**（注意：这不是内核的 `start_thread()`，而是 NPTL 中同名但完全不同的用户态函数）：

```c
// nptl/pthread_create.c (简化)
static void *
start_thread(void *arg)
{
    struct pthread *pd = (struct pthread *)arg;
    // arg 是 clone 时通过 CLONE_SETTLS 传递的 TCB 指针
    // 1. 完成 TLS 初始化: arch_prctl(ARCH_SET_FS, pd)
    //    确保 FS 段基址指向当前线程的 TCB
    //    (clone 时内核已设置 fsbase，这里做用户态补全)
    // 2. 注册线程退出时的清理函数
    __pthread_unwind_next(&pd->cancel_jmp_buf);
    // 3. 设置线程的亲和性（如果通过 pthread_attr 指定了）
    if (pd->cpuset)
        set_thread_affinity(pd);
    // 4. 注册 atfork 处理（如果 fork 时需要在子进程中重置锁）
    // 5. ★ 调用用户传入的线程入口函数
    void *retval = pd->start_routine(pd->arg);
    //             ↑ 这就是 pthread_create(&tid, NULL, func, arg) 里的 func
    // 6. 入口函数返回 → 线程退出
    __pthread_cleanup_upto(...);
    __do_cancel();
    // 最终: exit(0) → _exit 系统调用 → 内核回收线程
}
```

> **gdb 取证**：当你在一个线程入口函数打断点时，`bt`（backtrace）会显示：

> ```

> #0  my_thread_func(arg)         ← 你的线程入口

> #1  start_thread(arg)           ← glibc NPTL 内部

> #2  ??                          ← clone 伪造的栈帧，没有符号

> ```

> 你永远看不到内核栈帧（`ret_from_fork`、`schedule` 等），因为 gdb 只能回溯用户态栈帧。

### 2.6 阶段五：父线程视角 —— clone 返回

```bash
父线程调用栈（clone 返回后）：
═══ 内核态（clone 返回路径） ═══
kernel_clone() 返回子线程 tid
  → sys_clone() 返回
    → 系统调用出口: sysretq
═══ 用户态 ═══
glibc clone 封装:
  pd->tid = 内核返回的 tid
  (若 clone 时用了 CLONE_PARENT_SETTID，内核已自动写入)
__pthread_create_2_1() 返回
pthread_create() 返回 0 (成功) —— ★ 此时子线程可能还没跑过一条指令！
```

**父线程和子线程现在都是 TASK_RUNNING 状态，在同一个或不同 CPU 核的运行队列中，由调度器独立调度——两者是真正的并发关系。**

### 2.7 子线程启动完整调用栈

```bash
════════════════ 用户态（子线程）═════════════
my_thread_func(arg)                      ★ 你的线程函数
  ↑ pd->start_routine(pd->arg) 调用
start_thread (glibc NPTL)                glibc 内部
  ├─ ARCH_SET_FS → 设置 TLS
  └─ 调用 pd->start_routine(pd->arg)
════════════════ sysretq ═══════════════════
  ↑ CPU 从 pt_regs 恢复: RIP→start_thread, RSP→新用户栈
════════════════ 内核态（子线程首次被调度）═══
ret_from_fork                            arch/x86/entry/entry_64.S
  ├─ schedule_tail()                     释放 prev、检查信号
  └─ jmp .Lsysret_return                 → 返回用户态
  ↑ p->thread.ip 指向这里
context_switch(prev, p)                  内核调度
  ├─ switch_mm()                         CLONE_VM → CR3 不变
  └─ switch_to(prev, p)                  ★ 执行流切换点
  ↑ pick_next_task() 选中子线程
schedule()                               调度入口
  ↑ (某个触发原因: tick/阻塞/唤醒抢占)
════════════════ 内核态（父线程创建时）═══════
copy_thread()                            伪造子线程内核栈上的 pt_regs
  p->thread.ip = ret_from_fork          ★ 首次被调度时的入口
  childregs->rip = start_thread          ★ 返回用户态后的入口
  childregs->rsp = child_stack           ★ 新线程的用户栈顶
  childregs->cs = __USER_CS (0x33)      ★ 用户代码段
  childregs->ss = __USER_DS (0x2B)      ★ 用户数据段
wake_up_new_task(p)
  p->state = TASK_RUNNING
  → enqueue_task(rq, p)                加入运行队列
copy_process()                           创建 task_struct、复制资源
kernel_clone()                           clone 调度层
════════════════ syscall ═══════════════════
  ↑ 父线程执行 clone() 系统调用
════════════════ 用户态（父线程）═══════════
pthread_create(&tid, NULL, func, arg)   父线程的用户态入口
```

---

## 三、调度器何时介入 —— 三张时序图

### 3.1 主线程：调度器不介入（execve 直接在当前 task 上"换脑"）

```bash
时刻      主线程 (PID 100 → 仍是 PID 100)
─────────────────────────────────────────────
t0       用户态: execve("./program", ...)
t1       内核态: sys_execve → ... → load_elf_binary
t2       内核态: start_thread() 伪造 pt_regs
t3       内核态: execve 返回路径 → sysretq
t4       用户态: _start (新程序，PID 不变)
─────────────────────────────────────────────
★ 全程没有 schedule()，没有 context_switch()
★ PID 不变——只是"同一个人换了新的思维"
```

### 3.2 子线程：clone 的两种调度时序

#### 时序一：子线程先跑（wake_up_new_task 触发抢占）

```bash
父线程                                     子线程 (新 tid)
─────────────────────────────────────────────
用户态: pthread_create()
  → clone 系统调用
  内核态: copy_process()                              ← 尚不存在
  内核态: wake_up_new_task()                          ← 被创建，加入运行队列
    → check_preempt_curr():
      子线程 vruntime < 父线程 → 置 need_resched
  内核态: clone 返回 (tid)
  系统调用出口: 检测到 need_resched
    → schedule()
      → context_switch(父→子)        ─────→ 内核态: ret_from_fork ★
      父线程 → TASK_RUNNING (入队)              → schedule_tail()
                                                → sysretq
                                             用户态: start_thread (glibc)
                                                → my_thread_func()
  ...在运行队列中排队...                         ★ 子线程已经在跑我们的代码了！
  某时刻被重新调度:
  用户态: pthread_create 返回
```

#### 时序二：父线程先跑完 pthread_create（子线程在队列中等）

```bash
父线程                                     子线程 (新 tid)
─────────────────────────────────────────────
用户态: pthread_create()
  → clone 系统调用
  内核态: copy_process()                              ← 尚不存在
  内核态: wake_up_new_task()                          ← 被创建，加入运行队列
    → check_preempt_curr(): 不触发抢占
      (子线程 vruntime 不够小，或有其他原因)
  内核态: clone 返回
  sysretq
用户态: pthread_create 返回 ★
用户态: 父线程继续执行后续代码...                      ...在运行队列中等待...
用户态: 父线程做别的事情...                            ...仍在等待...
[某个 tick 或父线程阻塞]                              某核: pick_next_task() 选中子线程
  → schedule()                         ─────→ 内核态: ret_from_fork ★
  → context_switch(父→子)                        → sysretq
                                               用户态: start_thread
                                                  → my_thread_func()
  ...在运行队列中排队...                         ★ 子线程终于开始跑了
```

> **实际生产中两种时序都有**，glibc 的 `pthread_create` 专门不做任何保证。如果你的代码依赖于"父线程先跑完 xx 再让子线程跑 yy"，请用 `pthread_join`、条件变量、或 barrier 等同步原语。

### 3.3 子线程退出后父线程如何"回收"

```bash
子线程                                     父线程
─────────────────────────────────────────────
my_thread_func() 返回
  → start_thread 获取返回值
  → __pthread_cleanup_upto()
  → exit(0) → _exit 系统调用
  内核态: do_exit()
    → 释放资源、通知父进程 (SIGCHLD)
    → task_struct 变为 zombie
    → CLONE_CHILD_CLEARTID: 清除 child_tid
    → schedule() — 永不返回            (若 pthread_join)
                                      用户态: pthread_join(tid, &retval)
                                         → futex(FUTEX_WAIT, &tid)
                                        内核态: clone 时设置的
                                               CLONE_CHILD_CLEARTID
                                               触发 futex_wake
                                      用户态: pthread_join 返回
                                              拿到子线程的返回值
```

---

## 四、主线程 vs 子线程启动：逐阶段对比

| 阶段 | 主线程 | 子线程 |
|------|--------|--------|
| **触发系统调用** | `execve(path, argv, envp)` | `clone(flags, child_stack, ...)` |
| **内核核心函数** | `load_elf_binary()` | `copy_process()` |
| **地址空间处理** | `flush_old_exec()` 销毁旧 mm → 全新 mm | CLONE_VM → **共享**父线程的 mm |
| **内核栈** | 复用当前 task 的内核栈（原 program 的） | `alloc_thread_stack_node()` 分配新的 ~16KB |
| **用户栈** | `create_elf_tables()` 在内核中构建初始栈 | glibc `mmap` 分配 ~8MB，栈顶传给 clone |
| **pt_regs 来源** | `start_thread()` **覆盖**原 pt_regs | `copy_thread()` 先复制父 pt_regs 再**局部修改** |
| **CS/SS** | CS=0x33, SS=0x2B（都一样） | CS=0x33, SS=0x2B（都一样） |
| **RIP（用户态）** | ELF entry (`_start` 或 ld.so) | glibc 的 `start_thread` |
| **RSP（用户态）** | 内核构造的新用户栈顶 | glibc 分配的线程栈顶 |
| **RAX（返回值）** | execve 成功后不返回（返回值无意义） | **0**（clone 约定：子进程/子线程返回 0） |
| **内核态起点** | syscall 返回路径（被劫持到 entry） | `ret_from_fork` |
| **第一个用户函数** | `_start`（crt1.o） | `start_thread`（glibc NPTL） |
| **业务入口** | `main(argc, argv)` | `func(arg)` |
| **调度介入** | **不介入**——execve 全程在当前 task 上 | **介入**——`wake_up_new_task` 加入运行队列 |
| **TLS 设置** | `__libc_start_main` → `__tls_init_tp()` | clone 时 CLONE_SETTLS + `start_thread` 补全 |
| **进程/线程 ID** | PID 不变 | 新 tid，同 PID（CLONE_THREAD） |

---

## 五、关键系统调用一览

| 系统调用 | 触发位置 | 作用 | 本文出现位置 |
|---------|---------|------|------------|
| **execve** | shell / 进程内部 | 销毁当前进程镜像，加载新 ELF，伪造返回现场到 `_start` | §一 主线程启动 |
| **clone** | glibc `pthread_create` | 创建新 task，根据 flags 共享/复制资源，子 task 从 `ret_from_fork` 开始 | §二 子线程启动 |
| **mmap** | glibc NPTL `allocate_stack` | 为用户线程分配独立栈空间 (~8MB) | §2.2 阶段一 |
| **arch_prctl(ARCH_SET_FS)** | glibc `__tls_init_tp` / `start_thread` | 设置 FS 段基址 → 线程局部存储 (TLS) 的硬件支撑 | §1.5 / §2.5 |
| **futex(FUTEX_WAIT/FUTEX_WAKE)** | `pthread_join` / 子线程 `exit` | 线程退出时唤醒等待者，传递返回值 | §3.3 线程退出 |
| **_exit** | `pthread_exit` / thread return | 终止当前线程，触发 `do_exit()` → zombie → 父回收 | §2.5 / §3.3 |
| **sched_yield** | 用户主动让出 | 当前线程放弃 CPU，立即重新调度 | 未出现在主路径，但常在热点路径中 |

---

## 六、与仓库其他文档的关系

- **[thread-creation.md](/concepts/process/thread-creation.md)** —— 线程创建的整体框架（`pthread_create` → `clone`、join/detach、结论性总结），本文为其**执行流程深化版**，聚焦内核函数调用链和切换细节。
- **[segment-management.md](/concepts/process/segment-management.md)** —— `CS=0x33`/`SS=0x2B` 从哪里来、`ARCH_SET_FS` 怎么设置 `fs_base`、`syscall`/`sysret` 怎样切换段寄存器——本文多处依赖该文档的结论。
- **[scheduling.md](/concepts/process/scheduling.md)** —— 运行队列结构（CFS 红黑树）、`pick_next_task` 选人逻辑、调度类分层、CFS vruntime 公平性——本文 §三 的时序分析依赖这些概念。
- **[context-switch.md](/concepts/process/context-switch.md)** —— `schedule()` → `context_switch()` → `switch_mm()`/`switch_to()` 的完整实现，本文 §2.4.1 直接引用。
- **[process-creation.md](/concepts/process/process-creation.md)** —— `fork()+execve()` 的整体框架，本文 §一 是其中 execve 部分的执行流程深化。
- **[syscall-details.md](/concepts/process/syscall-details.md)** —— `syscall` 指令如何陷入内核、`pt_regs` 如何保存/恢复，本文在 `start_thread()` 和 `ret_from_fork` 两处涉及。
- **[../elf/compile-link-load.md](/concepts/elf/compile-link-load.md)** —— ELF 加载、`_start` → `__libc_start_main` → `main` 的静态/动态链接路径，本文 §1.5 直接引用。
- **[x86-64-registers.md](/concepts/process/x86-64-registers.md)** —— FS/GS/CS/SS 寄存器的角色、MSR_STAR 的设置，本文多处涉及。

---

> **一句话总结**：主线程的出生是"借尸还魂"——`execve` 借当前 task 的内核栈和 syscall 返回路径，通过 `start_thread()` 伪造 `pt_regs`，在 `sysretq` 后出现在 `_start`；子线程的出生是"凭空造人"——`clone` 在内核里创建全套新 `task_struct`，通过 `copy_thread()` 在新内核栈上伪造 `pt_regs` 并指向 `ret_from_fork`，由调度器选中后跳入用户态 `start_thread`(glibc)。两者的共同点是：**它们的第一条"用户态指令"都不是你的代码——中间都隔着 glibc 的启动框架。**

