在学习 Linux 系统编程时,很多初学者会对进程创建机制感到困惑。当使用 strace 跟踪程序执行时,明明代码中调用了 fork (),却在输出中找不到 fork 系统调用的踪迹,反而看到了 clone ()。这背后隐藏着 Linux 进程创建的重要机制,本文将逐步揭开这个谜团。
在 Unix/Linux 系统中,进程创建遵循一个经典模型:先复制再替换。这个模型由两个关键系统调用支撑:
fork(2)
:创建当前进程的副本execve(2)
:将当前进程替换为新程序理解这个模型前,我们需要明确一个重要概念:每个进程(除了 init 进程)都由另一个进程创建。这种父子进程关系构成了系统的进程树结构。
fork()
的核心作用是创建一个与当前进程几乎完全相同的子进程。调用 fork 后会发生:
一个关键特性是:fork()
在父进程中返回子进程的 PID,在子进程中返回 0,通过返回值可以区分父子进程。
execve()
的作用是用新程序替换当前进程的内存空间,它的原型是:
int execve(const char *pathname, char *const argv[], char *const envp[]);
调用 execve 后,当前进程的代码段、数据段、堆和栈都会被新程序替换,只有进程 ID 保持不变。注意:execve 成功时不会返回,只有出错时才会返回 - 1。
下面通过一个完整示例来演示二者的组合使用:
#include
#include
#include
int main() {
pid_t pid;
int status;
char *cmd[] = {"/bin/ls", "-l", "/etc", NULL};
// 调用fork创建子进程
if ((pid = fork()) < 0) {
perror("fork failed");
return 1;
}
// 父进程分支
else if (pid > 0) {
printf("父进程PID: %d,子进程PID: %d\n", getpid(), pid);
// 等待子进程结束
wait(&status);
printf("/bin/ls 执行完毕,退出状态: %d\n", WEXITSTATUS(status));
}
// 子进程分支
else {
printf("子进程PID: %d,开始执行%s\n", getpid(), cmd[0]);
// 执行ls命令,替换子进程内容
if (execve(cmd[0], cmd, NULL) < 0) {
perror("execve failed");
return 1;
}
}
return 0;
}
编译运行这个程序:
$ gcc -o fork_exec_demo fork_exec_demo.c
$ ./fork_exec_demo
典型输出如下:
父进程PID: 12345,子进程PID: 12346
子进程PID: 12346,开始执行/bin/ls
total 1234
drwxr-xr-x 2 root root 4096 Jan 1 00:00 bin
drwxr-xr-x 3 root root 4096 Jan 1 00:00 sbin
...(省略ls的输出)...
/bin/ls 执行完毕,退出状态: 0
代码解析:
现在让我们用 strace 跟踪上面的程序:
$ strace -f ./fork_exec_demo
关键输出片段:
execve("./fork_exec_demo", ["./fork_exec_demo"], 0x7ffc2b96d938
...
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f2b5c82a9d0) = 23456
[pid 23455] wait4(23456, 0x7ffc2b96d8d4, 0, NULL
[pid 23456] execve("/bin/ls", ["/bin/ls", "-l", "/etc"], 0x7ffc2b96d938
...
[pid 23456] exit_group(0) = ?
[pid 23455] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 23456
...
奇怪现象:
真相藏在 glibc 的实现中:Linux 上的 fork () 实际上是 clone () 的包装器。
查看 fork (2) 的手册页可以发现:
Since version 2.3.3, rather than invoking the kernel's fork() system
call, the glibc fork() wrapper that is provided as part of the NPTL threading
implementation invokes clone(2) with flags that provide the same effect as the
traditional system call.
关键要点:
clone()
是 Linux 内核提供的更底层的进程创建接口,原型为:
int clone(int (*child_func)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
与 fork () 相比,clone () 的优势在于:
使用 ltrace(跟踪库函数调用)可以看到真相:
$ ltrace -f ./fork_exec_demo
关键输出:
[pid 12345] fork() = 12346
[pid 12345] wait(0x7ffc2b96d8d4
[pid 12346] <... fork resumed> ) = 0
[pid 12346] execve("/bin/ls", ["/bin/ls", "-l", "/etc"], 0x7ffc2b96d938
...
现在清楚了:
Linux 中进程和线程的实现基于两个关键技术:
当调用 fork () 时,glibc 实际上执行的是:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID, ...);
这些标志表示:
主要原因有:
这是一个常见混淆点:
在 Linux 中:
线程创建函数 pthread_create () 本质上也是调用 clone (),但使用不同的 flags:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID, ...);
关键区别是 CLONE_THREAD 标志,它让父子进程属于同一个线程组。
如果需要直接调用系统调用,可以使用 syscall () 函数:
#include
long syscall(long number, ...);
例如,直接调用 clone 系统调用:
pid_t pid = syscall(SYS_clone, child_func, child_stack, flags, arg, &ptid, &tls, &ctid);
做一个简单的性能测试,比较 fork 和 clone 的开销:
#include
#include
#include
#include
#define ITERATIONS 100000
// 直接调用clone系统调用
pid_t my_fork(void) {
return syscall(SYS_clone, NULL, NULL, SIGCHLD, NULL, NULL, NULL, NULL);
}
int main() {
struct timeval start, end;
long msecs;
int i;
pid_t pid;
// 测试标准fork
gettimeofday(&start, NULL);
for (i = 0; i < ITERATIONS; i++) {
pid = fork();
if (pid == 0) _exit(0); // 子进程直接退出
wait(NULL); // 等待子进程
}
gettimeofday(&end, NULL);
msecs = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000;
printf("标准fork: %ld ms, 每次: %ld us\n", msecs, msecs * 1000 / ITERATIONS);
// 测试直接调用clone
gettimeofday(&start, NULL);
for (i = 0; i < ITERATIONS; i++) {
pid = my_fork();
if (pid == 0) _exit(0);
wait(NULL);
}
gettimeofday(&end, NULL);
msecs = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000;
printf("直接clone: %ld ms, 每次: %ld us\n", msecs, msecs * 1000 / ITERATIONS);
return 0;
}
典型输出(不同系统会有差异):
标准fork: 2345 ms, 每次: 23 us
直接clone: 2134 ms, 每次: 21 us
结果表明:
阅读源码:
glibc/nptl/fork.c
kernel/fork.c
深入学习:
实践建议:
命令 | 作用 |
---|---|
strace -f program |
跟踪程序及其子进程的系统调用 |
ltrace -f program |
跟踪程序及其子进程的库函数调用 |
ps -ef |
查看系统进程树 |
man fork |
查看 fork 手册页 |
man clone |
查看 clone 手册页 |
通过理解 Linux 进程创建的底层机制,我们不仅能解决 "fork 失踪" 的困惑,还能为深入学习系统编程打下坚实基础。进程模型是操作系统的核心概念,掌握它对理解 Linux 系统运行原理至关重要。