mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2196 字
6 分钟
进程管理:fork 与 exec
2021-09-29

fork 和 exec 是 UNIX 最独特的设计之一。创建新进程为什么要分成两步?fork 复制当前进程,exec 替换进程的内存映像——这种分离给了程序员极大的灵活性:可以在 fork 之后、exec 之前做任何初始化工作(重定向、环境变量、关闭文件)。本章将实现简化版的 fork 和 exec,以及进程退出机制。

重要说明:本章实现的是简化版的进程管理,用于演示核心概念。与完整的 UNIX 进程模型相比,缺少许多高级特性,如 Copy-on-Write、ELF 加载、文件描述符管理等。

进程管理概述#

进程是操作系统中最核心的抽象之一。进程管理主要解决以下问题:

  1. 程序执行:如何加载和执行用户程序?
  2. 进程创建:如何从一个进程创建新的进程?
  3. 进程隔离:如何保证不同进程的地址空间相互独立?
  4. 进程关系:如何管理父子进程之间的关联?
  5. 资源管理:如何管理进程使用的内存、文件等资源?

在实现完整的进程模型之前,需要先理解其核心概念。本章的简化实现将帮助我们理解:

  • 进程的基本结构
  • fork 系统调用的工作原理
  • exec 系统调用的工作原理
  • 进程退出和清理机制

进程管理的核心机制#

进程结构(process_t)#

进程结构是操作系统管理进程的核心数据结构。它存储了进程的所有关键信息,包括进程 ID、状态、内存管理信息、父子关系等。没有进程结构,操作系统就无法跟踪和管理进程的生命周期。

本章实现的进程结构是一个简化版本,与完整 UNIX 进程模型相比有以下区别:

实际实现的进程结构

typedef struct process {
pid_t pid; /* 进程ID */
char name[32]; /* 进程名 */
process_state_t state; /* 进程状态 */
/* 内存管理 */
uint32_t *page_dir; /* 页目录 */
uint32_t user_stack_top; /* 用户栈顶 */
uint32_t kernel_stack_top; /* 内核栈顶 */
/* 线程管理 */
task_t *main_thread; /* 主线程 */
struct list_node thread_list; /* 线程链表 */
int thread_count; /* 线程数量 */
/* 父子进程关系 */
struct process *parent; /* 父进程 */
struct list_node children; /* 子进程链表 */
struct list_node sibling; /* 兄弟进程链表 */
/* 退出状态 */
int exit_code; /* 退出码 */
int refcount; /* 引用计数 */
} process_t;

与完整 UNIX 进程的区别

特性本章实现完整 UNIX 进程
父进程 ID (ppid)无 无(使用 parent 指针)
文件描述符表 (files[])无 无有 (MAX_FDS)
等待队列 (wait_queue)无 无
堆管理 (heap_start/heap_end)无 无
进程组 (pgid)无 无
会话 (sid)无 无
信号处理无 无

进程状态转换#

stateDiagram-v2 [*] --> 创建: process_create() 创建 --> 就绪: 分配完成 就绪 --> 运行: schedule() 运行 --> 就绪: 时间片到期 运行 --> 僵尸: exit() 僵尸 --> [*]: 资源回收 note right of 运行 进程正在CPU上执行 end note note right of 僵尸 进程已退出但未被父进程回收 注意:wait未实现,无法自动回收 end note

简化版 fork 系统调用#

fork 系统调用是 UNIX 进程模型的核心,它允许一个进程创建一个完全独立的副本。这为进程间通信、并行执行提供了基础。

完整的 fork 系统调用需要:

  1. 复制父进程的页目录和页表(创建独立地址空间)
  2. 使用 Copy-on-Write 技术优化性能
  3. 复制文件描述符表
  4. 设置父子进程关系

本章实现的 fork 是简化版本,只创建了新的内核线程,父子进程共享地址空间:

graph TD A[父进程] -->|fork| B[创建子进程] B --> C[分配 process_t 结构] C --> D[创建新线程] D --> E[共享页目录] E --> F[设置父子关系] F --> G[返回子进程 PID] style A fill:#e1f5fe style B fill:#fff3e0 style C fill:#f3e5f5 style D fill:#e8f5e9 style E fill:#ffebee style F fill:#fff9c4 style G fill:#e1bee7

内存布局对比

graph LR subgraph 完整 fork A[父进程页目录] -.-> A'[复制] --> B[子进程页目录] A -->|Copy-on-Write| C[共享物理页] B --> C end subgraph 本章简化 fork D[父进程页目录] --> E[共享页表] F[子进程页目录] --> E end style A fill:#90caf9 style B fill:#90caf9 style D fill:#ffab91 style F fill:#ffab91 style E fill:#ffe0b2 style C fill:#c5e1a5

与完整 fork 的区别

  • 不复制地址空间:父子进程共享页表
  • 无 Copy-on-Write:所有内存完全共享
  • 无文件描述符复制:未实现文件描述符
  • 创建独立的 process_t 结构
  • 设置父子进程关系
  • 返回子进程 PID(父进程)或 0(子进程)

简化版 exec 系统调用#

exec 系统调用允许进程加载和执行新的程序,这是实现 Shell、程序启动等功能的必要条件。没有 exec,我们就无法运行不同的用户程序。

完整的 exec 系统调用需要:

  1. 解析 ELF 可执行文件格式
  2. 加载代码段、数据段等多个段
  3. 处理动态链接库
  4. 传递命令行参数 (argv) 和环境变量 (envp)
  5. 重置文件描述符(某些场景)

本章实现的 exec 加载flat binary(裸二进制文件),流程如下:

sequenceDiagram participant User as 用户程序 participant Kernel as 内核 participant FS as 文件系统 participant MMU as 内存管理 User->>Kernel: sys_exec("program.bin") Kernel->>FS: fs_open("program.bin", READ) FS-->>Kernel: fd Kernel->>FS: fs_read(fd, buf, size) FS-->>Kernel: 读取的数据 Kernel->>MMU: vmm_map_page(0x08048000) MMU-->>Kernel: 映射完成 Kernel->>MMU: memcpy(0x08048000, buf, size) Kernel->>MMU: 映射用户栈 Kernel->>Kernel: switch_to_usermode() Kernel-->>User: 跳转到用户态执行

内存布局变化

graph TB subgraph exec 之前 A1[旧代码段] A2[旧数据段] A3[旧堆/栈] end subgraph exec 之后 B1[Flat Binary<br/>@ 0x08048000] B2[新用户栈<br/>@ 0x00C00000] end A1 -.->|被覆盖| B1 A2 -.->|被覆盖| B1 A3 -.->|被替换| B2 style B1 fill:#c8e6c9 style B2 fill:#ffe082

与完整 exec 的区别

  • 不支持 ELF 格式:只支持 flat binary
  • 无参数传递:不支持 argv/envp
  • 无动态链接:只加载裸代码
  • 不释放旧地址空间:覆盖式加载
  • 从 SimpleFS 读取程序
  • 加载到固定用户地址
  • 设置用户栈
  • 切换到用户态执行

进程退出机制#

当进程完成执行或遇到错误时,需要正确退出并释放资源。退出机制需要:

  1. 更新进程状态为僵尸态
  2. 保存退出码供父进程查询
  3. 释放进程资源
  4. 通知父进程(如果实现了 wait)
flowchart TD A[进程调用 exit] --> B{是否为用户进程?} B -->|是| C[设置退出码] B -->|否| F[内核线程退出] C --> D[状态设为 PROC_ZOMBIE] D --> E[调用 task_exit] F --> G[直接退出] E --> H[通知调度器] G --> H H --> I[进程变为僵尸态]

注意:wait 系统调用未实现,父进程无法等待子进程结束。这意味着僵尸进程不会被自动回收。

未实现的功能

  • 无 关闭文件描述符(未实现)
  • 无 将子进程过继给 init(未实现)
  • 无 唤醒等待的父进程(wait 未实现)

代码实现#

文件结构#

15.kernel-proc/
├── boot/
│ ├── mbr.S # MBR 引导程序
│ └── loader.S # 引导加载器
├── kernel/
│ ├── include/
│ │ ├── process.h # 进程管理头文件
│ │ ├── task.h # 任务管理头文件
│ │ └── syscall.h # 系统调用头文件
│ ├── task/
│ │ ├── process.c # 进程管理实现
│ │ ├── fork.c # fork 系统调用
│ │ ├── exec.c # exec 系统调用
│ │ ├── task.c # 任务管理
│ │ ├── scheduler.c # 调度器
│ │ └── runqueue.c # 运行队列
│ ├── interrupt/
│ │ ├── syscall.c # 系统调用处理
│ │ └── timer.c # 定时器
│ ├── mem/
│ │ ├── vmm.c # 虚拟内存管理
│ │ ├── pmm.c # 物理内存管理
│ │ └── gdt.c # 全局描述符表
│ └── arch/x86/
│ ├── context_switch.S # 上下文切换
│ └── usermode.S # 用户模式切换
├── Makefile
└── README.md

进程结构(process_t)#

/* 进程状态 */
typedef enum {
PROC_RUNNING, /* 正在运行 */
PROC_READY, /* 就绪 */
PROC_BLOCKED, /* 阻塞 */
PROC_ZOMBIE, /* 僵尸态 */
PROC_EXITED /* 已退出 */
} process_state_t;
/* 用户空间布局常量 */
#define USER_STACK_TOP 0xBFFFF000 /* 用户栈顶部 (3GB - 4KB) */
#define USER_STACK_SIZE (64 * 1024) /* 用户栈大小 64KB */
#define USER_CODE_BASE 0x08048000 /* 用户代码段起始地址 */
/* 进程结构 */
typedef struct process {
pid_t pid; /* 进程ID */
char name[32]; /* 进程名 */
process_state_t state; /* 进程状态 */
/* 内存管理 */
uint32_t *page_dir; /* 页目录 */
uint32_t user_stack_top; /* 用户栈顶 */
uint32_t kernel_stack_top; /* 内核栈顶 */
/* 线程管理 */
task_t *main_thread; /* 主线程 */
struct list_node thread_list; /* 线程链表 */
int thread_count; /* 线程数量 */
/* 父子进程关系 */
struct process *parent; /* 父进程 */
struct list_node children; /* 子进程链表 */
struct list_node sibling; /* 兄弟进程链表 */
/* 退出状态 */
int exit_code; /* 退出码 */
int refcount; /* 引用计数 */
} process_t;

fork 系统调用流程#

flowchart TD A[用户调用 fork] --> B[sys_fork_impl] B --> C[获取父进程] C --> D[创建子进程结构] D --> E[分配 process_t] E --> F[创建新页目录] F --> G[分配用户栈] G --> H[分配内核栈] H --> I[创建主线程] I --> J[设置父子关系] J --> K[添加到进程链表] K --> L[返回子进程 PID]

exec 系统调用流程#

flowchart TD A[用户调用 exec] --> B[sys_exec_impl] B --> C[从文件系统读取程序] C --> D{读取成功?} D -->|否| E[返回错误] D -->|是| F[映射用户空间页面] F --> G[复制程序到内存] G --> H[设置用户栈] H --> I[切换到用户态] I --> J[执行用户程序]

fork 子进程入口#

static void fork_child_entry(void)
{
process_t *proc = current_process();
vga_printf("[Fork-Child] pid=%u running\n", proc ? proc->pid : 0);
process_exit(0);
}

解析:这是 fork 创建的子进程的入口函数。它打印子进程 PID 后立即退出。在完整实现中,子进程应该继续执行 fork 后的代码,但由于简化实现共享地址空间,这里只是演示概念。

fork 系统调用实现#

int sys_fork_impl(void)
{
process_t *parent = current_process();
task_t *parent_task = current_task();
if (!parent_task) return -1;
/* 创建子进程(新的 process_t 结构) */
process_t *child = process_create("forked", fork_child_entry,
parent_task->priority);
if (!child) return -1;
/* 设置父进程关系 */
child->parent = parent;
vga_printf("[Fork] Created child pid=%u from parent pid=%u\n",
child->pid, parent ? parent->pid : 0);
/* 返回子进程 PID 给父进程 */
return (int)child->pid;
}

解析

  1. 获取当前父进程和任务
  2. 创建新的子进程结构,指定入口函数为 fork_child_entry
  3. 建立父子进程关系
  4. 返回子进程 PID 给父进程

注意:由于共享地址空间,子进程不会返回 0,而是直接执行入口函数。

exec 系统调用实现#

int sys_exec_impl(const char *path)
{
if (!path) return -1;
/* 打开程序文件 */
int fd = fs_open(path, FS_OPEN_READ);
if (fd < 0) {
vga_printf("[Exec] File not found: %s\n", path);
return -1;
}
/* 读取程序到临时缓冲区 */
char buf[4096];
memset(buf, 0, sizeof(buf));
ssize_t size = fs_read(fd, buf, sizeof(buf));
fs_close(fd);
if (size <= 0) {
vga_printf("[Exec] Failed to read program\n");
return -1;
}
vga_printf("[Exec] Loaded %d bytes from %s\n", (int)size, path);
/* 映射加载地址的页面 */
uint32_t load_addr = EXEC_LOAD_ADDR; // 0x08048000
for (uint32_t a = load_addr; a < load_addr + (uint32_t)size; a += 4096) {
vmm_map_page_default(a);
}
/* 复制程序到加载地址 */
memcpy((void *)load_addr, buf, size);
/* 映射用户栈 */
uint32_t stack_bottom = EXEC_STACK_TOP - EXEC_STACK_SIZE;
for (uint32_t a = stack_bottom; a < EXEC_STACK_TOP; a += 4096) {
vmm_map_page_default(a);
}
/* 跳转到用户空间执行 */
switch_to_usermode(load_addr, EXEC_STACK_TOP);
/* 不应到达 */
return -1;
}

解析

  1. 从 SimpleFS 读取 flat binary 文件
  2. 将程序加载到固定地址 0x08048000
  3. 设置用户栈空间
  4. 通过 switch_to_usermode 切换到用户态执行

关键常量

  • EXEC_LOAD_ADDR = 0x08048000:程序加载地址
  • EXEC_STACK_TOP = 0x00C00000:用户栈顶
  • EXEC_STACK_SIZE = 16KB:用户栈大小

进程退出实现#

void process_exit(int exit_code)
{
process_t *proc = current_process();
if (proc == NULL) {
/* 内核线程退出 */
task_exit(exit_code);
return;
}
/* 设置退出状态 */
proc->exit_code = exit_code;
proc->state = PROC_ZOMBIE;
vga_printf("[Process] Process %u exiting with code %d\n",
proc->pid, exit_code);
/* 退出主线程 */
task_exit(exit_code);
}

解析

  1. 检查当前是否为用户进程
  2. 设置进程的退出码和状态为僵尸态
  3. 调用 task_exit 退出主线程

注意:由于 wait 未实现,僵尸进程不会被自动回收。

进程创建实现#

process_t *process_create(const char *name, void (*entry)(void), int priority)
{
process_t *proc;
task_t *thread;
/* 分配进程结构 */
proc = (process_t *)kmalloc(sizeof(process_t));
if (proc == NULL) {
vga_printf("[Process] Failed to allocate process structure\n");
return NULL;
}
memset(proc, 0, sizeof(process_t));
/* 分配进程ID */
proc->pid = next_pid++;
/* 设置进程名 */
if (name != NULL) {
strncpy(proc->name, name, sizeof(proc->name) - 1);
proc->name[sizeof(proc->name) - 1] = '\0';
} else {
sprintf(proc->name, "process-%u", proc->pid);
}
proc->state = PROC_READY;
proc->refcount = 1;
/* 创建页目录 */
proc->page_dir = create_user_page_dir();
if (proc->page_dir == NULL) {
vga_printf("[Process] Failed to create page directory\n");
kfree(proc);
return NULL;
}
/* 分配用户栈 */
proc->user_stack_top = alloc_user_stack();
/* 分配内核栈 */
uint32_t kernel_stack = (uint32_t)kmalloc_aligned(KERNEL_STACK_SIZE);
if (kernel_stack == 0) {
vga_printf("[Process] Failed to allocate kernel stack\n");
kfree(proc);
return NULL;
}
proc->kernel_stack_top = kernel_stack + KERNEL_STACK_SIZE;
/* 设置 TSS 内核栈 */
tss_set_kernel_stack(proc->kernel_stack_top);
/* 创建主线程 */
thread = task_create_user_stub(proc->pid, proc->name, entry,
(void *)proc->user_stack_top, priority);
if (thread == NULL) {
vga_printf("[Process] Failed to create main thread\n");
kfree(proc);
return NULL;
}
proc->main_thread = thread;
proc->thread_count = 1;
thread->owner = proc;
/* 添加到进程链表 */
process_list.count++;
vga_printf("[Process] Created process '%s' (pid=%u)\n",
proc->name, proc->pid);
return proc;
}

解析

  1. 分配并初始化进程结构
  2. 分配进程 ID 和设置进程名
  3. 创建页目录(复制内核映射)
  4. 分配用户栈和内核栈
  5. 创建主线程并关联进程
  6. 添加到进程链表

运行与验证#

编译运行#

cd 15.kernel-proc
make clean && make all && make run

预期输出#

[Process] Subsystem initialized
[Task] Task subsystem initialized
[Scheduler] Round-robin scheduler initialized
[Process] Created process 'init' (pid=1)
[Fork] Created child pid=2 from parent pid=1
[Fork-Child] pid=2 running
[Process] Process 2 exiting with code 0

输出示例说明:

  1. 进程子系统初始化
  2. 创建 init 进程(PID=1)
  3. fork 创建子进程(PID=2)
  4. 子进程执行并退出

踩坑记录#

  1. 问题:fork 后子进程不返回 0

    • 原因:简化实现共享地址空间,子进程直接执行入口函数
    • 解决方案:这是设计的简化,完整实现需要独立地址空间
  2. 问题:exec 无法加载 ELF 文件

    • 原因:本章只支持 flat binary 格式
    • 解决方案:使用 NASM 生成 flat binary:nasm -f bin program.asm -o program.bin
  3. 问题:僵尸进程无法被回收

    • 原因:wait 系统调用未实现
    • 解决方案:实现 wait 系统调用(见课后练习)
  4. 问题:fork 创建的进程共享内存

    • 原因:简化实现不复制地址空间
    • 解决方案:实现完整的 fork(复制页表或使用 COW)

小结#

fork 复制当前进程创建子进程,exec 替换进程的内存映像加载新程序,exit/wait 处理进程的退出和回收——这三者构成了进程的生命周期管理。fork+exec 的分离设计源自 UNIX 哲学:两步操作给了程序员在子进程初始化阶段插入自定义逻辑的灵活性。下一章将完善系统调用框架,实现更多 POSIX 接口,为 Shell 和用户工具提供完整的基础服务。

与完整进程模型的对比

功能本章实现完整 UNIX 进程
进程创建简化版完整版
地址空间独立无 共享独立
Copy-on-Write
ELF 加载无 只有 flat binary支持 ELF
参数传递 (argv/envp)
文件描述符
wait/exit只有 exit完整实现
进程组/会话
信号机制

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

进程管理:fork 与 exec
https://blog.souloss.com/posts/os/process-management-fork-exec/
作者
Souloss
发布于
2021-09-29
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时