写这篇文章的原因是看到了一段与 Linux 下进程复制有关的代码,感觉很神奇,不甚理解,所以找了一些相关的资料想要弄明白 Linux 的 fork、进程复制到底是怎么工作的,于是有了这篇文章。
那段代码是这样的:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
int main() {
int pid, status;
pid = fork();
if (pid == 0) {
// /bin/echo是 Linux 自带的 echo 命令的源代码所在的位置
char *argv[] = {"/bin/echo是 Linux 自带的 echo 命令的源代码所在的位置", "THIS","MESSAGE", "IS", "FROM", "ECHO", NULL};
execve("/bin/echo", argv, NULL);
printf("exec failed!\n");
exit(1);
} else {
printf("parent wating!\n");
wait(&status);
printf("the child exited with status %d \n", status);
}
exit(0);
}
(base) root@iZuf65cax8rsfekcyp3ytyZ:~/testfork# gcc tfork.c -o test
(base) root@iZuf65cax8rsfekcyp3ytyZ:~/testfork# ./test
parent wating!
THIS MESSAGE IS FROM ECHO
the child exited with status 0
如果上面的代码是调用了系统的 echo 命令,如果我们把命令换成一个不存在的命令,那么就会报错,我们来修改一下代码生成 errorfork.c :
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
int main() {
int pid, status;
pid = fork();
if (pid == 0) {
// 当前目录下没有名为 echo 的文件,所以 execve 会执行失败
char *argv[] = {"echo", "THIS","MESSAGE", "IS", "FROM", "ECHO", NULL};
execve("echo", argv, NULL);
printf("exec failed!\n");
exit(1);
} else {
printf("parent wating!\n");
wait(&status);
printf("the child exited with status %d \n", status);
}
exit(0);
}
(base) root@iZuf65cax8rsfekcyp3ytyZ:~/testfork# gcc errorfork.c -o errorfork
(base) root@iZuf65cax8rsfekcyp3ytyZ:~/testfork# ./errorfork
parent wating!
exec failed!
the child exited with status 256
大家可以用在线C语言编译器 来做一下这个实验试试看。
我当时看到这个实验觉得有两点很不能理解:
- fork() 函数调用返回了两个值,if 和 else 子句都会被执行,超出认知了,感觉很神奇。
- 如果 execve 函数执行成功了,这个函数后面的代码就不会再被执行了,只有失败了就会继续执行下去,超出认知了,为什么一个函数调用,如果成功了它就不会回到它的原始调用点了?
基于这两个疑惑我找了一些资料,了解了 fork 的运行机制,总结成了这篇文章。
初识 fork
fork 是叉子的意思,进程复制函数为什么要交 fork 呢,这就要从 fork 的起源说起了
fork 的思想最初是 Conway 作为一种 多处理器并行的方案提出来的,这个想法非常有意思。简而言之,fork 思想来源于流程图。看上面这张流程图,从 fork 点开始产生分叉,产生了一个新进程,可以和原来的进程并行执行,就像叉子一样。
先来解决第一个疑惑,为什么 fork 函数会有两个返回值, if 和 else 语句都会被执行到?因为 fork 函数是把父进程完全复制了一遍,所以复制出来的子进程它下一步应该执行的就是 if 语句, 父进程要执行的代码一样,只是他俩的 pid 不一样,所以进入了不同的选择分支
再来解决第二个疑惑,为什么执行 exec 函数成功之后就不回到原来的函数调用点了?因为 exec 把所要执行的新的函数代码加载到了这个子进程里,子进程原始的代码就无了,自然也不会回到调用 exec 的地方了。