fork 和 exec 是 UNIX 最独特的设计之一。创建新进程为什么要分成两步?fork 复制当前进程,exec 替换进程的内存映像,这种分离给了程序员极大的灵活性:可以在 fork 之后、exec 之前做任何初始化工作(重定向、环境变量、关闭文件)。本章将实现简化版的 fork 和 exec,以及进程退出机制。
重要说明:本章实现的是简化版的进程管理,用于演示核心概念。与完整的 UNIX 进程模型相比,缺少许多高级特性,如 Copy-on-Write、ELF 加载、文件描述符管理等。
进程管理概述
进程是操作系统中最核心的抽象之一。进程管理主要解决以下问题:
- 程序执行:如何加载和执行用户程序?
- 进程创建:如何从一个进程创建新的进程?
- 进程隔离:如何保证不同进程的地址空间相互独立?
- 进程关系:如何管理父子进程之间的关联?
- 资源管理:如何管理进程使用的内存、文件等资源?
在实现完整的进程模型之前,需要先理解其核心概念。本章的简化实现将帮助我们理解:
- 进程的基本结构
- 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) | 否 | 是 |
| 信号处理 | 否 | 是 |
进程状态转换
简化版 fork 系统调用
fork 系统调用是 UNIX 进程模型的核心,它允许一个进程创建一个完全独立的副本。这为进程间通信、并行执行提供了基础。
完整的 fork 系统调用需要:
- 复制父进程的页目录和页表(创建独立地址空间)
- 使用 Copy-on-Write 技术优化性能
- 复制文件描述符表
- 设置父子进程关系
本章实现的 fork 是简化版本,只创建了新的内核线程,父子进程共享地址空间:
内存布局对比:
与完整 fork 的区别:
- 不复制地址空间:父子进程共享页表
- 无 Copy-on-Write:所有内存完全共享
- 无文件描述符复制:未实现文件描述符
- 创建独立的
process_t结构 - 设置父子进程关系
- 返回子进程 PID(父进程)或 0(子进程)
简化版 exec 系统调用
exec 系统调用允许进程加载和执行新的程序,这是实现 Shell、程序启动等功能的必要条件。没有 exec,我们就无法运行不同的用户程序。
完整的 exec 系统调用需要:
- 解析 ELF 可执行文件格式
- 加载代码段、数据段等多个段
- 处理动态链接库
- 传递命令行参数 (argv) 和环境变量 (envp)
- 重置文件描述符(某些场景)
本章实现的 exec 加载flat binary(裸二进制文件),流程如下:
内存布局变化:
与完整 exec 的区别:
- 不支持 ELF 格式:只支持 flat binary
- 无参数传递:不支持 argv/envp
- 无动态链接:只加载裸代码
- 不释放旧地址空间:覆盖式加载
- 从 SimpleFS 读取程序
- 加载到固定用户地址
- 设置用户栈
- 切换到用户态执行
进程退出机制
当进程完成执行或遇到错误时,需要正确退出并释放资源。退出机制需要:
- 更新进程状态为僵尸态
- 保存退出码供父进程查询
- 释放进程资源
- 通知父进程(如果实现了 wait)
wait 系统调用未实现,父进程无法等待子进程结束。这意味着僵尸进程不会被自动回收。
未实现的功能:
- 关闭文件描述符(未实现)
- 将子进程过继给 init(未实现)
- 唤醒等待的父进程(wait 未实现)
代码实现
文件结构
boot
mbr.SMBR 引导程序
loader.S引导加载器
kernel
include
process.h进程管理头文件
task.h任务管理头文件
syscall.h系统调用头文件
task
process.c进程管理实现
fork.cfork 系统调用
exec.cexec 系统调用
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;
/* 用户空间布局常量(来自 process.h) */#define USER_STACK_TOP 0xBFFFF000 /* 用户栈顶部 (3GB - 4KB) */#define USER_STACK_SIZE (64 * 1024) /* 用户栈大小 64KB */#define USER_CODE_BASE 0x08048000 /* 用户代码段起始地址 */#define USER_HEAP_START 0x08049000 /* 用户堆起始地址 */#define USER_HEAP_END 0xA0000000 /* 用户堆结束地址 */
/* 进程结构 */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 系统调用流程
15 章的 sys_fork_impl 是内核内部函数,并未挂到系统调用表上。syscall.c 的 syscall_init() 只注册到 SYS_GETTID(8 个),sys_fork/sys_exec 要等到 16 章完善系统调用框架时才作为系统调用注册。这里先按内核内部调用流程描述。
exec 系统调用流程
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;}解析:
- 获取当前父进程和任务
- 创建新的子进程结构,指定入口函数为
fork_child_entry - 建立父子进程关系
- 返回子进程 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;}解析:
- 从 SimpleFS 读取 flat binary 文件
- 将程序加载到固定地址
0x08048000 - 设置用户栈空间
- 通过
switch_to_usermode切换到用户态执行
关键常量(定义在 exec.c):
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);}解析:
- 检查当前是否为用户进程
- 设置进程的退出码和状态为僵尸态
- 调用
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;}解析:
- 分配并初始化进程结构
- 分配进程 ID 和设置进程名
- 创建页目录(复制内核映射)
- 分配用户栈和内核栈
- 创建主线程并关联进程
- 添加到进程链表
运行与验证
编译运行
cd 15.kernel-procmake clean && make all && make run预期输出
main() 完成各子系统初始化后,创建 manager 内核任务并启动调度器。manager_task 负责整个演示流程:先写初始文件,再 fork 出 writer 进程写 /procN.txt,等写完后 fork 出 reader 进程读回,最后列出根目录。
=== Chapter 15: Process Management ===
[VMM] Initialized with fixed page tables. Page Dir @ 0x... (Virt: 0x...) Page Tables @ 0x... Page Fault handler registered for on-demand paging.[Scheduler] Initialized (idle tid=1)[Process] Subsystem initialized[Syscall] Initialized with 8 system callsStarting scheduler...[Manager] Starting process management demo
[Manager] Creating initial files...
[Manager] Creating writer processes...[Manager] Created writer 1 (tid=2)[Manager] Created writer 2 (tid=3)[Manager] Created writer 3 (tid=4)[Process 2] Creating file: /proc1.txt[Process 3] Creating file: /proc2.txt[Process 4] Creating file: /proc3.txt
[Manager] All writers done
[Manager] Creating reader processes...[Process 5] Reading file: /welcome.txt[Process 6] Reading file: /proc1.txt[Process 7] Reading file: /proc2.txt[Process 5] Read N bytes: Welcome to MyOS Process Management![Process 6] Read N bytes: Written by process 2 (id=1)[Process 7] Read N bytes: Written by process 3 (id=2)
[Manager] Final directory listing: [DIR] data [FILE] proc1.txt [FILE] proc2.txt [FILE] proc3.txt [FILE] welcome.txt
=== Process Management Test Complete ===输出示例说明:
- 各子系统按顺序初始化:VMM、调度器(含 idle 任务 tid=1)、进程子系统、系统调用(8 个)
manager_task先创建/welcome.txt和/data目录- 创建 3 个 writer 任务写
/procN.txt,全部完成后继续 - 创建 3 个 reader 任务读回刚才写的文件
- 列出根目录,确认文件都落盘
- 演示结束,
manager_task进入无限让出循环
本章 main() 走的是 manager_task 演示路径,并不调用 sys_fork_impl/sys_exec_impl。fork 与 exec 的实现代码已就绪,但要到 16 章把它们注册进系统调用表后才会真正被触发。上面输出里的 [Process N] 是 manager_task 通过 task_create_kernel 创建的内核任务打印的,tid 由任务子系统分配,与 process_t 的 pid 不是一回事。
踩坑记录
-
问题:fork 后子进程不返回 0
- 原因:简化实现共享地址空间,子进程直接执行入口函数
- 解决方案:这是设计的简化,完整实现需要独立地址空间
-
问题:exec 无法加载 ELF 文件
- 原因:本章只支持 flat binary 格式
- 解决方案:使用 NASM 生成 flat binary:
nasm -f bin program.asm -o program.bin
-
问题:僵尸进程无法被回收
- 原因:wait 系统调用未实现
- 解决方案:实现 wait 系统调用(见课后练习)
-
问题:fork 创建的进程共享内存
- 原因:简化实现不复制地址空间
- 解决方案:实现完整的 fork(复制页表或使用 COW)
小结
fork 复制当前进程创建子进程,exec 替换进程的内存映像加载新程序,exit/wait 处理进程的退出和回收,这三者构成了进程的生命周期管理。fork+exec 的分离设计源自 UNIX 哲学:两步操作给了程序员在子进程初始化阶段插入自定义逻辑的灵活性。下一章将完善系统调用框架,实现更多 POSIX 接口,为 Shell 和用户工具提供完整的基础服务。
与完整进程模型的对比:
| 功能 | 本章实现 | 完整 UNIX 进程 |
|---|---|---|
| 进程创建 | 简化版 | 完整版 |
| 地址空间独立 | 否,共享页表 | 是,独立 |
| Copy-on-Write | 否 | 是,按需复制页表 |
| ELF 加载 | 否,仅 flat binary | 是 |
| 参数传递 (argv/envp) | 否 | 完整支持 |
| 文件描述符 | 否 | 继承与重定向 |
| wait/exit | 仅 exit | 完整实现 |
| 进程组/会话 | 否 | 完整支持 |
| 信号机制 | 否 | 完整支持 |
参考
- 第 12 章:用户空间 - TSS 与 Ring 3 切换基础
- 第 14 章:文件系统 - VFS 与 SimpleFS 实现
- Copy-on-Write - 写时复制技术原理
- ELF 格式规范 - ELF 可执行文件格式官方规范
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






