Linux 操做系统牢牢依赖进程建立来知足用户的需求。例如,只要用户输入一条命令,shell 进程就建立一个新进程,新进程运行 shell 的另外一个拷贝并执行用户输入的命令。Linux 系统中经过 fork/vfork 系统调用来建立新进程。本文将介绍如何使用 fork/vfork 系统调用来建立新进程并使用 exec 族函数在新进程中执行任务。linux
要建立一个进程,最基本的系统调用是 fork:shell
# include <unistd.h> pid_t fork(void); pid_t vfork(void);
调用 fork 时,系统将建立一个与当前进程相同的新进程。一般将原有的进程称为父进程,把新建立的进程称为子进程。子进程是父进程的一个拷贝,子进程得到同父进程相同的数据,可是同父进程使用不一样的数据段和堆栈段。子进程从父进程继承大多数的属性,可是也修改一些属性,下表对比了父子进程间的属性差别:编程
继承属性 | 差别 |
uid,gid,euid,egid | 进程 ID |
进程组 ID | 父进程 ID |
SESSION ID | 子进程运行时间记录 |
所打开文件及文件的偏移量 | 父进程对文件的锁定 |
控制终端 | |
设置用户 ID 和 设置组 ID 标记位 | |
根目录与当前目录 | |
文件默认建立的权限掩码 | |
可访问的内存区段 | |
环境变量及其它资源分配 |
下面是一个常见的演示 fork 工做原理的 demo(笔者的环境为 Ubuntu 16.04 desktop):数组
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; char *message; int n; pid = fork(); if(pid < 0) { perror("fork failed"); exit(1); } if(pid == 0) { printf("This is the child process. My PID is: %d. My PPID is: %d.\n", getpid(), getppid()); } else { printf("This is the parent process. My PID is %d.\n", getpid()); } return 0; }
把上面的代码保存到文件 forkdemo.c 文件中,并执行下面的命令编译:函数
$ gcc forkdemo.c -o forkdemo
而后运行编译出来的 forkdemo 程序:学习
$ ./forkdemo
fork 函数的特色是 "调用一次,返回两次":在父进程中调用一次,在父进程和子进程中各返回一次。在父进程中返回时的返回值为子进程的 PID,而在子进程中返回时的返回值为 0,而且返回后都将执行 fork 函数调用以后的语句。若是 fork 函数调用失败,则返回值为 -1。
咱们细想会发现,fork 函数的返回值设计仍是很高明的。在子进程中 fork 函数返回 0,那么子进程仍然能够调用 getpid 函数获得本身的 PID,也能够调用 getppid 函数获得父进程 PID。在父进程中用 getpid 函数能够获得本身的 PID,若是想获得子进程的PID,惟一的办法就是把 fork 函数的返回值记录下来。
注意:执行 forkdemo 程序时的输出是会发生变化的,可能先打印父进程的信息,也可能先打印子进程的信息。ui
vfork 系统调用和 fork 系统调用的功能基本相同。vfork 系统调用建立的进程共享其父进程的内存地址空间,可是并不彻底复制父进程的数据段,而是和父进程共享其数据段。为了防止父进程重写子进程须要的数据,父进程会被 vfork 调用阻塞,直到子进程退出或执行一个新的程序。因为调用 vfork 函数时父进程被挂起,因此若是咱们使用 vfork 函数替换 forkdemo 中的 fork 函数,那么执行程序时输出信息的顺序就不会变化了。this
使用 vfork 建立的子进程通常会经过 exec 族函数执行新的程序。接下来让咱们先了解下 exec 族函数。spa
使用 fork/vfork 建立子进程后执行的是和父进程相同的程序(但有可能执行不一样的代码分支),子进程每每须要调用一个 exec 族函数以执行另一个程序。当进程调用 exec 族函数时,该进程的用户空间代码和数据彻底被新程序替换,重新程序的起始处开始执行。调用 exec 族函数并不建立新进程,因此调用 exec 族函数先后该进程的 PID 并不改变。操作系统
exec 族函数一共有六个:
#include <unistd.h> int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
函数名字中带字母 "l" 的表示其参数个数不肯定,带字母 "v" 的表示使用字符串数组指针 argv 指向参数列表。
函数名字中含有字母 "p" 的表示能够自动在环境变量 PATH 指定的路径中搜索要执行的程序。
函数名字中含有字母 "e" 的函数比其它函数多一个参数 envp。该参数是字符串数组指针,用于指定环境变量。调用这样的函数时,能够由用户自行设定子进程的环境变量,存放在参数 envp 所指向的字符串数组中。
事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用 execve。这些函数之间的关系以下图所示(此图来自互联网):
exec 族函数的特征:调用 exec 族函数会把新的程序装载到当前进程中。在调用过 exec 族函数后,进程中执行的代码就与以前彻底不一样了,因此 exec 函数调用以后的代码是不会被执行的。
下面让咱们经过 vfork 和 execve 函数实如今子进程中执行 ls 命令:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; if((pid=vfork()) < 0) { printf("vfork error!\n"); exit(1); } else if(pid==0) { printf("Child process PID: %d.\n", getpid()); char *argv[ ]={"ls", "-al", "/home", NULL}; char *envp[ ]={"PATH=/bin", NULL}; if(execve("/bin/ls", argv, envp) < 0) { printf("subprocess error"); exit(1); } // 子进程要么从 ls 命令中退出,要么从上面的 exit(1) 语句退出 // 因此代码的执行路径永远也走不到这里,下面的 printf 语句不会被执行 printf("You should never see this message."); } else { printf("Parent process PID: %d.\n", getpid()); sleep(1); } return 0; }
把上面的代码保存到文件 subprocessdemo.c 文件中,并执行下面的命令编译:
$ gcc subprocessdemo.c -o subprocessdemo
而后运行编译出来的 subprocessdemo程序:
$ ./subprocessdemo
fork/vfork 函数和 exec 族函数都是 Linux 系统中很是重要的概念。本文试图经过简单的 demo 来演示这些函数的基本用法,为理解 Linux 系统中父进程与子进程的概念提供一些直观的感觉。
参考:
Linux C 编程一站式学习《Linux 环境下 C 编程指南》《深刻理解 Linux 内核》