进程管理与地址空间
深入剖析 Linux 进程创建(fork)、加载(execve)以及虚拟内存映射(mmap)的底层机制
进程的创建
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 | ✅ 相同 | 继承父进程权限 |
| maps | mm_struct | ✅ 相同* | 虚拟地址一致,物理地址 COW |
| fd/ | files_struct | ✅ 相同 | 指向同一组 struct file |
| fdinfo (pos) | file->f_pos | ✅共享 | 这是一个动态同步的值! |
| cwd / exe | fs_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) 被清空并重新初始化。
-
数据段: 全局变量和静态变量被重置为新程序代码中定义的初始值。
-
身份标识:
PID、PPID、进程组 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 |
| Low | argc (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