Sealessland logo Sealessland
OS

进程管理与地址空间

深入剖析 Linux 进程创建(fork)、加载(execve)以及虚拟内存映射(mmap)的底层机制

OS

进程的创建

graph TD

    subgraph Storage ["磁盘阶段 (Static Program)"]
        ELF["ELF 可执行文件<br/>(.text, .data, .rodata)"]
    end

    subgraph Kernel_Action ["内核加载 (Execve System Call)"]
        Load["读取 ELF Header"] --> Alloc["分配虚拟内存空间 (VMA)"]
        Alloc --> Map["建立页表映射 (Paging)"]
    end

    subgraph OS_Abstraction ["进程实体 (Process Entity)"]
        PCB["<b>PCB (Process Control Block)</b><br/>PID, 优先级, 寄存器快照"]
        ADDR["<b>虚拟地址空间</b><br/>Stack, Heap, Shared Libs"]
        FD["<b>资源清单</b><br/>文件描述符, 信号量"]
    end

    ELF --> Load
    Map --> ADDR
    Kernel_Action --> PCB
    PCB --- ADDR
    PCB --- FD

    CPU((CPU))
    PCB -- 上下文切换 --> CPU
    style PCB fill:#f96,stroke:#333,stroke-width:2px
    style CPU fill:#bbf,stroke:#333,stroke-width:2px

操作系统通过进程抽象为应用程序提供了独立的执行环境。进程是操作系统中最基本的资源分配单位,它包含程序本身的状态和操作系统内部的状态。操作系统提供了一系列系统调用 (如 Linux 中的 fork、exec、wait 和 exit,Windows 中的 CreateProcess 和TerminateProcess) 来创建、管理和终止进程。

fork()

pid_t pid = fork(void);
//get the pid of child process in parent process
//return 0 in child process
if(pid==0){
    //task for child process
}else if(pid>0){
    //task for parent process
    int status;
    wait(&status); // 等待子进程结束,回收资源,防止僵尸进程
    printf("Child finished.\n");
}
sequenceDiagram
    participant Parent as 父进程
    participant Kernel as 操作系统内核
    participant Child as 子进程

    Note over Parent, Child: 阶段 1: 正常运行
    Parent->>Kernel: fork()
    Kernel-->>Child: 创建进程
  
    Note over Parent: 父进程调用 wait(&status)
    Parent->>Kernel: wait()
  
    alt 子进程还在运行
        Kernel->>Parent: 将父进程置为 SLEEP 状态
        Note right of Parent: 阻塞中...
    end

    Note over Child: 子进程结束
    Child->>Kernel: exit(42)
    Note right of Child: 变身僵尸进程 (Zombie)<br>保留 task_struct<br>保存 exit_code=42

    Kernel->>Parent: 发送 SIGCHLD 信号 (唤醒)
  
    Note over Parent: 父进程被唤醒
    Kernel-->>Parent: wait 返回 Child_PID<br>写入 status = 0x2A00 (高位是42)
  
    Note over Kernel: 内核清理子进程尸体 (Reaping)
    Note right of Child: task_struct 被释放<br>彻底消失
  
    Parent->>Parent: WEXITSTATUS(status) 解析出 42

fork 在应用层表现为执行流的分叉,并且立即完成了所有状态的复制(COW)。但是父子进程在资源继承上仍然具有差异。每个进程的都有独立的PID,对应的/proc/[pid]/暴露了该进程的核心数据结构:

/proc 文件对应内核结构Fork 后状态说明
status (Pid)task_struct->pid❌ 不同身份证号必须唯一
status (Uid/Gid)cred✅ 相同继承父进程权限
mapsmm_struct✅ 相同*虚拟地址一致,物理地址 COW
fd/files_struct✅ 相同指向同一组 struct file
fdinfo (pos)file->f_pos共享这是一个动态同步的值!
cwd / exefs_struct✅ 相同工作环境继承
stat (utime/stime)task_struct❌ 不同子进程计时器归零
pid_t x = fork(); 
pid_t y = fork();
printf("%d %d\n", x, y); 
  
graph TD
    Start((start)) --> P1[父进程 P1]
  
    %% 第一次 Fork
    P1 -- "fork() #1" --> F1{x = ?}
  
    F1 -- "父进程 (P1)" --> P1_A["P1 继续运行<br/>x = 1001"]
    F1 -- "子进程 (P2)" --> P2_A["P2 开始运行<br/>x = 0"]
  
    %% 第二次 Fork (P1 分裂)
    P1_A -- "fork() #2" --> F2_1{y = ?}
    F2_1 -- "父进程 (P1)" --> P1_End[("P1 输出<br/>x=1001, y=1002")]
    F2_1 -- "子进程 (P3)" --> P3_End[("P3 输出<br/>x=1001, y=0")]
  
    %% 第二次 Fork (P2 分裂)
    P2_A -- "fork() #2" --> F2_2{y = ?}
    F2_2 -- "父进程 (P2)" --> P2_End[("P2 输出<br/>x=0, y=1003")]
    F2_2 -- "子进程 (P4)" --> P4_End[("P4 输出<br/>x=0, y=0")]

    style P1 fill:#f9f,stroke:#333,stroke-width:2px
    style P1_End fill:#e1f5fe,stroke:#01579b
    style P2_End fill:#e8f5e9,stroke:#2e7d32
    style P3_End fill:#fff3e0,stroke:#ef6c00
    style P4_End fill:#f3e5f5,stroke:#7b1fa2

execve()

execve 是程序加载与执行的唯一入口。 它将当前进程的执行上下文替换为新的程序。除 init 进程外,所有用户态程序的执行最终都源于某次 execve 调用。”

将当前进程重置成一个可执行文件描述状态机的初始状态

  • 操作系统维护的状态不变:进程号、目录、打开的文件…
#include<unistd.h>
#include<stdio.h>
int main() {
    char *args[] = {"ls", "-l", NULL}; 
    char *const envp[] = {NULL};
    execve("/bin/ls", args, envp);

    return 0;
}
$ strace ./a.out
execve("./a.out", ["./a.out"], 0x7ffe2839cf20 /* 90 vars */) = 0
    //一旦 execve 成功,控制权将移交给新程序的入口点(_start),原程序后续的代码永远不会被执行。只有调用失败时,它才会返回 -1 并设置 errno。
brk(NULL)                               = 0x1d4d1000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffb20e75000
......

execve会解除旧进程所有用户态内存区域的映射(Unmap),并根据新程序的 ELF 头重新加载。此时,进程回到了可执行文件描述的初始状态:

  • 指令指针 (RIP/EIP): 重置为 ELF Header 中定义的 Entry Point(通常是 _start)。

  • 内存布局: 堆 (Heap) 和 栈 (Stack) 被清空并重新初始化。

  • 数据段: 全局变量和静态变量被重置为新程序代码中定义的初始值。

  • 身份标识: PIDPPID、进程组 ID、Session ID 保持不变。

  • 环境上下文: 当前工作目录 (CWD)、根目录 (Root)、以及控制终端 (TTY)。

  • 资源句柄: 打开的文件描述符 (File Descriptors)

    默认继承打开的文件描述符。除非 该文件描述符被设置了 FD_CLOEXEC 标志(Close-on-Exec),这种情况下它会在 execve 成功后被内核自动关闭。

Addr Value Region
High"SHELL=..."Strings
"ls\0"Strings
...Ptr to "SHELL"envp
...Ptr to "ls"argv
Lowargc (2)Initial Stack Layout

地址空间

ELF header不能预测程序的动态行为,需要处理运行时程序内存区域的映射变化

// 映射
void *mmap(void *addr, size_t length, int prot, int flags,           int fd, off_t offset); int munmap(void *addr, size_t length); 

// 修改映射权限 
int mprotect(void *addr, size_t length, int prot);

mmap 的本质是在进程的虚拟地址空间中注册一个VMA,而非交付物理内存。此时,真实的物理内存对程序是不可见且未分配的。

程序在运行通过 Load/Store 指令直接发起访存,此过程不经过 System Call,而是完全受控于 MMU的硬件级管辖。

程序的每一次访存行为都受到 VMA页表 的约束。当程序访问到MMU没有加载的虚拟地址时,硬件触发 缺页异常 强行陷入内核。内核随即查阅 VMA ,若行为符合约束,则分配物理页并填充页表;若违约,则通过信号(SIGSEGV)终结进程。

mprotect 修改指定虚拟地址范围的页面保护属性(RWX),即修改 vm_area_struct 中的 vm_flags,进而驱动内核重新计算对应的物理页保护位(vm_page_prot),并更新页表项(PTE)及刷新转换检测缓冲(TLB),以确保 CPU 的 MMU 能够即时执行新的权限约束;若修改的地址范围仅为当前 VMA 的子集,内核还会自动触发 VMA 的分裂,将原本连续的内存区域切割为多个独立的结构体,以分别记录不同的权限状态。

graph TD
    %% ================= 样式定义 =================
    classDef cpu fill:#1a237e,stroke:#534bae,stroke-width:2px,color:#fff;
    classDef hw_logic fill:#332b00,stroke:#fbc02d,stroke-width:2px,color:#fff59d;
    classDef ram fill:#0f2e13,stroke:#2e7d32,stroke-width:2px,color:#a5d6a7;
    classDef kernel_soft fill:#2b0b0b,stroke:#b71c1c,stroke-width:2px,color:#ef9a9a;
    classDef decision fill:#3e2723,stroke:#8d6e63,stroke-width:2px,stroke-dasharray: 5 5,color:#d7ccc8;
    classDef action fill:#004d40,stroke:#009688,stroke-width:2px,color:#e0f2f1;

    %% ================= 1. 硬件层 (Fast Path) =================
    subgraph Hardware_Layer [CPU & MMU 硬件层]
        direction TB
        Instr(["指令: MOV R1, ADDR"]):::cpu
        TLB{TLB 查缓存}:::hw_logic
        PageWalker{{"硬件 Page Walker<br>查询 RAM 中的 PTE"}}:::hw_logic
        Exception["触发 #PF 异常<br>将 ADDR 存入 CR2"]:::kernel_soft
    end

    %% ================= 2. 物理内存 (Storage) =================
    subgraph RAM_Layer [物理内存 RAM]
        PTE_RAM[("页表项 (PTE)<br>Flags: P=0/1, RW, NX")]:::ram
        Data_RAM[("物理数据页<br>(Data Frame)")]:::ram
    end

    %% ================= 3. 内核层 (Slow Path) =================
    subgraph Kernel_Layer [内核 Page Fault Handler]
        direction TB
        DoPageFault["do_page_fault()"]:::kernel_soft
      
        %% VMA 查找逻辑
        FindVMA{{"查红黑树<br>find_vma"}}:::decision
        CheckRange{{"检查边界<br>vm_start <= addr < vm_end"}}:::decision
        CheckPerms{{"检查权限<br>Op vs vm_flags"}}:::decision
      
        %% 判决结果
        SigSegv["发送 SIGSEGV<br>(段错误)"]:::action
      
        %% 具体的分配逻辑
        HandleFault{{处理类型?}}:::decision
        AllocAnon["匿名页分配<br>分配零页 / alloc_page"]:::action
        FileMap["文件映射<br>vm_ops->fault 读取磁盘"]:::action
        CoW["写时复制 CoW<br>拷贝物理页"]:::action
      
        %% 硬件同步
        UpdatePTE["更新 PTE<br>Set P=1, Set RW"]:::action
        FlushTLB["刷新 TLB<br>invlpg"]:::action
    end

    %% ================= 连线逻辑 =================

    %% 1. 硬件指令流
    Instr --> TLB
    TLB --"Hit (命中)"--> Data_RAM
    TLB --"Miss (未命中)"--> PageWalker
  
    %% 2. 硬件查表流
    PageWalker --"读取"--> PTE_RAM
    PTE_RAM --"P=1 & 权限OK"--> TLB
    PTE_RAM --"P=0 (缺页) 或 权限违规"--> Exception

    %% 3. 陷入内核
    Exception --> DoPageFault
    DoPageFault -->|读取 CR2| FindVMA
  
    %% 4. VMA 合法性校验 (软件契约)
    FindVMA --"未找到 VMA"--> SigSegv
    FindVMA --"找到 VMA"--> CheckRange
    CheckRange --"越界"--> SigSegv
    CheckRange --"范围内"--> CheckPerms
  
    CheckPerms --"权限不符 (如写只读)"--> SigSegv
    CheckPerms --"合法"--> HandleFault

    %% 5. 内存分配策略
    HandleFault --"首次访问 (Lazy)"--> AllocAnon
    HandleFault --"文件缺页"--> FileMap
    HandleFault --"写共享页"--> CoW

    %% 6. 闭环:更新硬件
    AllocAnon --> UpdatePTE
    FileMap --> UpdatePTE
    CoW --> UpdatePTE
  
    UpdatePTE --"写入"--> PTE_RAM
    UpdatePTE --> FlushTLB
    FlushTLB --"指令重试 (IRET)"--> Instr