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 未实现)
代码实现
文件结构
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 系统调用流程
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_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预期输出
[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输出示例说明:
- 进程子系统初始化
- 创建 init 进程(PID=1)
- fork 创建子进程(PID=2)
- 子进程执行并退出
踩坑记录
-
问题: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 | 支持 ELF |
| 参数传递 (argv/envp) | 无 | |
| 文件描述符 | 无 | |
| wait/exit | 只有 exit | 完整实现 |
| 进程组/会话 | 无 | |
| 信号机制 | 无 |
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






