fork是创建子进程的关键函数,常和exec系列函数一起使用。

对于代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h> // 提供 exit()

int main() {
pid_t pid = fork();

if (pid < 0) {
perror("fork 失败");
return 1;
}

if (pid == 0) {
// 子进程
printf("我是子进程 (PID: %d),准备运行 ls...\n", getpid());
execl("/bin/ls", "ls", "-l", NULL);

// 如果 execl 执行成功,下面的代码绝不会被运行
perror("execl 失败");
exit(1);
} else {
// 父进程
printf("我是父进程 (PID: %d),我创建的子进程 PID 是 %d\n", getpid(), pid);
wait(NULL); // 阻塞直到子进程结束
printf("子进程已结束,父进程退出。\n");
}

return 0;
}

执行结果如下:

fork() 调用之后,系统中同时存在了两个进程,调用一次,返回两次。

子进程得到的返回值为0,而父进程得到的返回值是子进程的PID(大于0),子进程获得了父进程数据空间、堆和栈的副本。注意是“副本”,父子进程不共享内存变量(除非使用特殊的共享内存手段)。

当子进程执行 execl 时,它会把当前进程的整个内存(代码段、数据段)全部清空,然后加载 /bin/ls 的程序代码,子进程后面的代码就不执行了。

注意执行 exec 系列函数后,原进程打开的文件描述符(如 open("/dev/scull", ...) 得到的 fd)默认保持打开状态。这意味着新程序可以直接通过原来的数字 fd 继续读写文件。除非有**D_CLOEXEC 标志**,如果在 open 驱动时设置了这个标志(Close-on-exec),那么在执行 exec 的瞬间,内核会自动关闭这个 fd,防止子进程意外操作父进程打开的敏感硬件设备。

execl参数含义:

  • "/bin/ls":可执行文件的绝对路径。
  • "ls":传给程序的 argv[0],通常是程序名。
  • "-l":传给程序的参数。
  • NULL:必须以空指针结尾,告诉内核参数列表结束了。

wait则是父进程执行到这一行会挂起(休眠),等待任何一个子进程结束后继续执行

补充:

exec系列函数:

函数名 字母含义 特点
execl l(list) 参数以列表形式列出,最后必须以 NULL 结尾。
execv v(vector) 参数放入一个字符串数组(指针数组)中传递。
execlp p (path) 会自动在环境变量 PATH 中寻找可执行文件,不需要写全路径。
exece e (env) 可以自定义环境变量,而不使用当前进程的环境变量。

如果知道所有参数的话就使用**execl**

1
2
// 路径, argv[0], argv[1], ..., NULL
execl("/bin/ls", "ls", "-l", "-a", NULL);

如果参数是动态生成的,用 execv

1
2
char *args[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", args);

根据后缀是v还是l就可以判断传入的参数,也就是说exec函数可以有很多组合,如execvp,execve,execlp,execle。

如果不想写 /bin/ls 这么长的绝对路径,只想写 ls,就使用p(path)

1
execvp("ls", args);

内核会去 /usr/bin, /bin 等目录下帮你找。

execve是 Linux 内核中真正的系统调用。其他的 execl, execvp 等底层最终都会调用 execve。不使用e的话就直接继承父进程的环境变量。函数原型:

1
int execve(const char *pathname, char *const argv[], char *const envp[]);