总的来说,创建新进程主要有两种主流模型:
- Unix/Linux 模型: 分两步走,即
fork()
+exec()
。 - Windows 模型: 一步到位,即
CreateProcess()
。
下面我们分别详细介绍这两种模型。
1. Unix/Linux 模型: fork()
和 exec()
在 Unix、Linux 以及其他类 Unix 系统(如 macOS)中,创建新进程是一个“两步走”的过程。这种设计非常优雅且灵活。
第一步: fork()
- 克隆进程
fork()
系统调用的作用是创建一个当前进程的副本(克隆)。
-
工作方式: 当一个进程(称为“父进程”)调用
fork()
时,操作系统内核会:- 为新进程(称为“子进程”)分配一个新的、唯一的进程ID(PID)。
- 创建一个新的进程控制块(PCB),并复制父进程PCB的大部分内容。
- 复制父进程的整个地址空间(包括代码、数据、堆栈)。
- 复制父进程打开的文件描述符、环境变量等。
-
关键特性 - 写时复制 (Copy-on-Write, COW): 早期的
fork()
实现会完整地复制整个内存空间,效率较低。现代操作系统普遍采用 写时复制(COW) 技术进行优化。这意味着,fork()
之后,父子进程在逻辑上拥有独立的内存空间,但物理上它们共享相同的内存页。只有当其中一个进程尝试写入某个内存页时,内核才会真正为该进程复制一份这个内存页,让它拥有自己的副本。这极大地提高了fork()
的效率,因为大多数情况下,子进程很快会调用exec()
,之前的内存复制就白费了。 -
fork()
的返回值:fork()
的返回值非常巧妙,是区分父子进程的关键:- 在父进程中,
fork()
返回新创建的子进程的PID(一个正整数)。 - 在子进程中,
fork()
返回 0。 - 如果创建失败,
fork()
在父进程中返回 -1。
- 在父进程中,
代码示例 (fork()
):
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
// fork 失败
fprintf(stderr, "Fork Failed\n");
return 1;
} else if (pid == 0) {
// 这是子进程执行的代码
printf("I am the child process, my PID is %d\n", getpid());
} else {
// 这是父进程执行的代码
printf("I am the parent process, my PID is %d, my child's PID is %d\n", getpid(), pid);
}
// 父子进程都会执行这里的代码
printf("This line is executed by both processes.\n");
return 0;
}
第二步: exec()
- 加载新程序
fork()
只是创建了一个父进程的副本,如果想让子进程执行一个全新的程序,就需要 exec()
系列函数。
-
工作方式:
exec()
系列函数会用一个新的程序来替换当前进程的内存空间(包括代码、数据和堆栈)。- 进程的 PID 保持不变。
- 一旦
exec()
调用成功,它永远不会返回。新的程序会从它的main
函数开始执行。如果exec()
返回了,那一定是出错了。
-
exec()
函数家族:exec()
不是一个函数,而是一组函数,它们的细微差别在于参数传递方式和是否使用系统PATH
环境变量来查找可执行文件。execl(path, arg0, arg1, ...)
: 参数以列表形式传入。execv(path, argv[])
: 参数以字符串数组(vector)形式传入。execlp(...)
,execvp(...)
:p
代表会自动在PATH
环境变量中搜索可执行文件。execle(...)
,execve(...)
:e
代表可以手动传入新的环境变量。
fork()
+ exec()
组合使用
这才是创建新进程并运行新程序的标准模式。典型的应用场景就是 Shell(命令行解释器)。
- Shell(父进程)调用
fork()
创建一个子进程。 - 子进程调用
execvp()
来执行用户输入的命令(例如ls -l
)。 - 父进程(Shell)通常会调用
wait()
或waitpid()
等待子进程执行结束,然后返回到命令提示符,等待下一个命令。
代码示例 (fork()
+ execvp()
)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "Fork Failed\n");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process is about to run 'ls -l'\n");
char *args[] = {"ls", "-l", NULL}; // execvp 的参数数组,必须以 NULL 结尾
execvp(args[0], args); // 加载并执行 ls 命令
// 如果 execvp 成功,下面的代码永远不会被执行
perror("execvp failed"); // 如果执行到这里,说明 execvp 出错了
return 1;
} else {
// 父进程
printf("Parent process is waiting for the child to complete...\n");
wait(NULL); // 等待子进程结束
printf("Child process has finished.\n");
}
return 0;
}
2. Windows 模型: CreateProcess()
Windows 采用了一种更“直接”或“一体化”的方式来创建进程,通过一个功能强大的 API 函数 CreateProcess()
来完成。
-
工作方式:
CreateProcess()
函数一步到位地完成了进程创建和程序加载两项任务。它不会像fork()
那样复制父进程的上下文。- 它会创建一个全新的、独立的进程。
- 然后将指定的可执行文件加载到这个新进程的地址空间中。
- 父进程会得到新进程和其主线程的句柄(Handle),以便后续进行管理和同步。
-
CreateProcess()
函数: 这个函数的参数非常多(有10个),提供了非常精细的控制,例如:- 要执行的程序名和命令行参数。
- 进程和线程的安全属性。
- 环境变量。
- 进程的启动信息(如窗口如何显示)。
- 返回新创建进程和主线程的句柄。
简化的 CreateProcess()
概念性代码示例 (C++):
#include <windows.h>
#include <stdio.h>
int main() {
STARTUPINFO si;
PROCESS_INFORMATION pi;
// 初始化结构体
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// 要执行的命令
// 注意:在 Windows 中,字符串最好使用 TCHAR 类型以支持 Unicode
TCHAR cmd[] = TEXT("C:\\Windows\\System32\\notepad.exe");
// 创建新进程
if (!CreateProcess(
NULL, // 不使用模块名
cmd, // 命令行字符串
NULL, // 进程安全属性
NULL, // 线程安全属性
FALSE, // 句柄不被继承
0, // 创建标志
NULL, // 使用父进程的环境变量
NULL, // 使用父进程的当前目录
&si, // 指向 STARTUPINFO 结构的指针
&pi // 指向 PROCESS_INFORMATION 结构的指针
)) {
printf("CreateProcess failed (%d).\n", GetLastError());
return 1;
}
printf("Process created with PID: %d\n", pi.dwProcessId);
// 等待子进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
// 关闭进程和线程句柄
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
printf("Child process has finished.\n");
return 0;
}
总结与对比
特性 | Unix/Linux (fork() + exec() ) | Windows (CreateProcess() ) |
---|---|---|
设计哲学 | 两步分离:创建(克隆)和执行(替换)是分开的。 | 一步到位:创建和执行合并在一个函数调用中。 |
灵活性 | 高。子进程可以在调用 exec() 之前修改其环境(如重定向文件描述符、更改环境变量),这对于实现Shell的I/O重定向等功能非常方便。 | 较低。虽然参数众多,但子进程在执行新程序代码前能做的事情有限。 |
效率 | 看起来低效(复制整个地址空间),但**写时复制(COW)**技术使其在大多数情况下非常高效。 | 概念上更直接高效,因为它直接创建新进程并加载程序,无需复制父进程。 |
继承性 | 子进程默认继承父进程的大部分资源(内存、文件描述符等)。 | 子进程不继承父进程的地址空间。其他资源(如句柄)是否继承可以通过参数精确控制。 |
复杂度 | 概念简单,两个函数的职责清晰。 | API 复杂,单个函数有10个参数,需要精细配置。 |
总而言之,这两种模型都有效地完成了创建新进程的任务,但它们的设计哲学反映了各自操作系统的历史和设计目标。Unix 的 fork
/exec
模型因其简洁和强大的灵活性而备受赞誉,而 Windows 的 CreateProcess
则提供了一种功能丰富、控制精细的“一站式”解决方案。