Linux fork()
fork()
在 Linux 中创建一个新进程的唯一方法是使用 fork() 函数。fork() 函数是 Linux 中一个非常重要的函数,和以往遇到的函数有一些区别,因为 fork() 函数看起来执行一次却返回两个值1。
fork() 函数用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。使用 fork() 函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程的 上下文、代码段、进程堆栈、内存信息、打开的文件描述符、符号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端 等,而子进程所独有的只有它的 进程号、资源使用和计时器 等。1
在父进程中执行 fork() 函数时,父进程会复制一个子进程,而且父子进程的代码从 fork() 函数的返回开始分别在两个地址空间中同时运行,从而使两个进程分别获得所属 fork() 函数的返回值,其中在父进程中的返回值是子进程的 pid ,而在子进程中返回 0 。因此,可以通过 返回值 来判断该进程的父进程还是子进程。
使用 fork()
使用 C 语言:
#include <sys/types.h> #include <unistd.h> pid_t fork(void); /* * Return: * 0 child process * > 0 the pid of child process * -1 error */
当系统的进程数达到最大, errno 为 EAGAIN ; 内存不足时, errno 为 ENOMEM 。
while((p1 = fork()) == -1)
:
- fork() 函数是在当前进程中新建立一个子进程,如果这个创建子进程失败,那么返回 -1,这个实际是把创建进程的返回值 和-1 比较看看是否创建失败
- 因为是写在 while 语句里,那么当创建失败之后,如果在 while 里面没有 break 或者跳出,当 while 执行体执行结束后又会执行
(p1 = fork()) == -1
,等于不断重复创建子进程一直
到创建成功为止
- 注意这里会返回两次,因为父进程创建子进程的时候复制了父进程的地址空间,那么父子进程地址空间的语句执行都在等待 fork() 返回的那句话里,所以返回两次是父进程返回一个,返回的是子进程的 PID,子进程返回一次,返回的是 0 。
例子
#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main(int argc, char *const *args) { pid_t result; result = fork(); if (-1 == result) { printf("Fork error\n"); } else if (0 == result) { printf("The returned value is %d\n", result); printf("In child process!\nMy PID is %d\n", getpid()); } else { printf("The returned value is %d\n", result); printf("In father process!\nMy PID is %d\n", getpid()); } for (;;) ; return 0; }
The returned value is 5787 The returned value is 0 In child process! My PID is 5787 In father process! My PID is 5786
根据 PID 可以看出父进程先运行,子进程后运行,但子进程执行可能在前。
cat /proc/5786/maps | md5sum
与 cat /proc/5787/maps | md5sum
完全一样,父子进程的内存映射具体如下:
563e9d45c000-563e9d45d000 r--p 00000000 08:01 1076422 /var/data/workspace/linux-fork/a.out 563e9d45d000-563e9d45e000 r-xp 00001000 08:01 1076422 /var/data/workspace/linux-fork/a.out 563e9d45e000-563e9d45f000 r--p 00002000 08:01 1076422 /var/data/workspace/linux-fork/a.out 563e9d45f000-563e9d460000 r--p 00002000 08:01 1076422 /var/data/workspace/linux-fork/a.out 563e9d460000-563e9d461000 rw-p 00003000 08:01 1076422 /var/data/workspace/linux-fork/a.out 563e9dec9000-563e9deea000 rw-p 00000000 00:00 0 [heap] 7fd489800000-7fd489828000 r--p 00000000 08:01 919998 /usr/lib/x86_64-linux-gnu/libc.so.6 7fd489828000-7fd489996000 r-xp 00028000 08:01 919998 /usr/lib/x86_64-linux-gnu/libc.so.6 7fd489996000-7fd4899ee000 r--p 00196000 08:01 919998 /usr/lib/x86_64-linux-gnu/libc.so.6 7fd4899ee000-7fd4899f2000 r--p 001ed000 08:01 919998 /usr/lib/x86_64-linux-gnu/libc.so.6 7fd4899f2000-7fd4899f4000 rw-p 001f1000 08:01 919998 /usr/lib/x86_64-linux-gnu/libc.so.6 7fd4899f4000-7fd489a01000 rw-p 00000000 00:00 0 7fd489b1d000-7fd489b20000 rw-p 00000000 00:00 0 7fd489b33000-7fd489b35000 rw-p 00000000 00:00 0 7fd489b35000-7fd489b36000 r--p 00000000 08:01 917934 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7fd489b36000-7fd489b5b000 r-xp 00001000 08:01 917934 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7fd489b5b000-7fd489b65000 r--p 00026000 08:01 917934 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7fd489b65000-7fd489b67000 r--p 0002f000 08:01 917934 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7fd489b67000-7fd489b69000 rw-p 00031000 08:01 917934 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffd61342000-7ffd61363000 rw-p 00000000 00:00 0 [stack] 7ffd61372000-7ffd61376000 r--p 00000000 00:00 0 [vvar] 7ffd61376000-7ffd61378000 r-xp 00000000 00:00 0 [vdso]
当父进程调用了 fork() ,父 子进程分别在两个地址空间同时运行,同时运行 fork() 后面的代码,此例中 fork() 后有 if 逻辑判断使得父 子进程表现出不同的行为。而 fork () 的返回值在父进程中返回子进程的 PID ,而在子进程中返回 0 。
vfork()
为了加快 fork() 执行速度,vfork() 不产生父进程的副本,而是允许父子进程可访问相同的物理内存,伪装了对进程空间的真实复制,当子进程需要改变内存中的数据时才复制父进程。也被称为 “写操作时复制 (copy-on-write) ”技术。
exec()
exec() 提供了一个在进程中启动另一个程序执行的方法,可以根据指定的文件名或目录名找到可执行文件(也可以是脚本文件),并用其 取代原调用进程的数据段、代码段和堆栈段 ,执行完之后,原调用进程的内容除了进程以外,其他全部被新的进程替换了。
使用的两种情况:
- 当进程认为自己不能再为系统和用户作出贡献时,调用 exec() 函数族的任意一个函数让自己重生
- 如果一个进程想执行另一个程序,可以用 fork() 函数新建一个进程,然后调用 exec() 函数族的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程
使用 exec()
#include <unistd.h> int execl(const char *path, const char *arg, ...); int execv(const char *path, char *const argv[]); int execle(const char *path, const char *arg, ... , char *const envp[]); int execve(const char *path, char *const argv[], ... , char *const envp[]); int execlp(const char *file, const char *arg, ...); int execvp(const char *file, char *const argv[]); /* Return -1: error */
说明:
- path 为完整的文件目录,而 file 会自动在 $PATH 所指定的目录下进行查找
- 第 5 个字母为 l 时,代表以 list 列表逐个列举的传参方式,而 v 代表以 vector 将所有参数整体构造指针数组传递,参数必须以 NULL 结束
- 第 6 个字母为 e 时,代表 environment ,同时 envp 指定环境变量
- 实际上最终执行的只有 execve() 这个函数
常见返回 -1 的错误原因:
- 找不到文件或路径, errno 为 ENOENT
- 数据 argv 和 envp 为用 NULL 结尾, errno 为 EFAUL
- 没用对应可执行文件的运行权限, errno 为 EACCES
例子 - 1
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char *const argv[]) { printf("father process pid is %d\n", getpid()); int ret = 0; if ((ret = execlp("ps", "ps", "-ef", NULL)) < 0) { printf("Execlp error\n"); } printf("child process pid is %d, ret = %d\n", getpid(), ret); return 0; }
father process pid is 6300 UID PID PPID C STIME TTY TIME CMD root 1 0 0 06:56 ? 00:00:01 /sbin/init splash
printf("child process pid is %d, ret = %d\n", getpid(), ret);
不会执行,因为原进程的内容被 ps 进程完全替换了。
例子 - 2
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char *const argv[]) { printf("father process pid is %d\n", getpid()); int ret = 0; if (0 == fork()) { if ((ret = execlp("ps", "ps", "-ef", NULL)) < 0) { printf("Execlp error\n"); } } printf("child process pid is %d, ret = %d\n", getpid(), ret); return 0; }
father process pid is 6438 child process pid is 6438, ret = 0
UID PID PPID C STIME TTY TIME CMD root 1 0 0 06:56 ? 00:00:01 /sbin/init splash
这里只是子进程被替换了,父进程没有影响,执行完了。但在子进程中 printf("child process pid is %d, ret = %d\n", getpid(), ret);
不会执行。
例子 - 3
int main(int argc, char *const argv[]) { char *envp[] = { "PATH=/tmp", "USER=execle", NULL, }; if (0 == fork()) { if (execle("/usr/bin/env", "env", NULL, envp) < 0) { printf("execle error\n"); } } return 0; }
PATH=/tmp USER=execle
可以看到子进程中的环境变量被修改成代码中的 envp 中的值。
Footnotes:
https://blog.csdn.net/Thanksgining/article/details/41943799
fork() 为 UNIX 中派生新进程的唯一方法。fork() 在调用进程(成为父进程)中返回一次,返回值为新派生进程(子进程)的 PID ,在子进程中又返回一次,返回值为 0 。