引言
多进程编程的概念早在六十年代就被提出,但直到八十年代中期,Unix系统中才引入多进程机制。如今,由于自身的许多优点,多进程编程已经得到了广泛的应用。
fork函数是 Unix 系统最杰出的成就之一,它是七十年代 UNIX 早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果。一方面,它使操作系统在进程管理上付出了最小的代价;另一方面,又为程序员提供了一个简洁明了的多进程方法。
Linux下进程的结构
Linux 下一个进程在内存里有三部分的数据,就是代码段、堆栈段和数据段。
- 代码段:存放程序代码的数据。如果机器中有数个进程运行相同的一个程序,它们可以共享相同的代码段。
- 堆栈段:存放子程序的返回地址、子程序的参数以及程序的局部变量。
- 数据段:存放程序的全局变量、常数以及动态数据分配的空间(如用
malloc函数取得的空间)。
系统如果同时运行数个相同的程序,它们之间不能使用同一个堆栈段和数据段,但可以共享代码段。
Linux下的进程控制
fork 函数
fork()用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝。
#include<sys/types.h>#include<unistd.h>pid_tfork(void);exec 函数族
exec()函数族用来启动另外的进程以取代当前运行的进程。
fork 的返回值
理解fork最困难之处在于:调用它一次,它却返回两次。
- 在父进程中:返回新派生进程(子进程)的进程 ID 号
- 在子进程中:返回值为 0
因此,返回值本身告知当前进程是子进程还是父进程。
pid_tpid=fork();if(pid==-1){// 出错}elseif(pid==0){// 子进程}else{// 父进程,pid 是子进程的 ID}fork在子进程返回 0 而不是父进程 ID 的原因:任何子进程只有一个父进程,子进程可以通过getppid()取得父进程 ID;相反,父进程可以有许多子进程,无法直接获取各个子进程 ID。如果父进程想要跟踪所有子进程,必须记录每次调用fork的返回值。
fork 的两个典型用法
一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。这是网络服务器的典型用法。
一个进程想要执行另一个程序。既然创建新进程的唯一办法是调用
fork,该进程于是首先调用fork创建一个自身的副本,然后其中一个副本(通常为子进程)调用exec把自身替换成新的程序。这是 Shell 之类程序的典型用法。
fork 的注意事项
fork()通过复制调用进程来创建一个新进程,新进程称为子进程,是调用进程(父进程)的完全副本,但以下情况除外:
- 子进程有自己唯一的进程 ID,并且这个 PID 不匹配任何现有进程组的 ID。
- 子进程的父进程 ID 与父进程 ID 相同。
- 子进程不会继承其父进程的内存锁和信号量调整。
- 子代不会从其父代继承未完成的异步 I/O 操作,也不会从其父代继承任何异步 I/O 上下文。
父进程中调用fork之前打开的所有描述符在fork返回之后由子进程共享。网络服务器利用了这个特性:父进程调用accept之后调用fork,所接受的已连接套接字随后在父进程与子进程之间共享。通常情况下,子进程接着读写这个已连接套接字,父进程则关闭这个已连接套接字。
exec 函数族
exec 函数族的作用
exec函数族的作用是替换当前进程的代码段和数据段,将一个新程序加载到当前进程的内存空间中执行。与fork不同,exec不会创建新进程,而是在原有进程的基础上替换执行内容。
6 个 exec 函数
系统调用execve()对当前进程进行替换,替换者为一个指定的程序。在 Linux 中,exec 函数族包括 6 个函数:
| 函数名 | 路径指定方式 | 参数传递方式 | 环境变量 |
|---|---|---|---|
execl | 路径名 | 列表形式 | 使用当前环境 |
execlp | 文件名(搜索 PATH) | 列表形式 | 使用当前环境 |
execle | 路径名 | 列表形式 | 自定义环境 |
execv | 路径名 | 数组形式 | 使用当前环境 |
execvp | 文件名(搜索 PATH) | 数组形式 | 使用当前环境 |
execve | 路径名 | 数组形式 | 自定义环境 |
函数命名规律
这些函数的命名遵循一定的规律:
- l(list):参数以列表形式逐个传递
- v(vector):参数以指针数组形式传递
- p(path):使用 PATH 环境变量搜索可执行文件
- e(environment):自定义环境变量
函数原型
#include<unistd.h>intexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecv(constchar*path,char*constargv[]);intexecvp(constchar*file,char*constargv[]);intexecve(constchar*path,char*constargv[],char*constenvp[]);这些函数只在出错时才返回到调用者,否则控制将被传递给新程序的起始点(通常是main函数)。
调用示例
// execl 示例:列表形式,路径名execl("/bin/ls","ls","-l",NULL);// execlp 示例:列表形式,搜索 PATHexeclp("ls","ls","-l",NULL);// execv 示例:数组形式,路径名char*argv[]={"ls","-l",NULL};execv("/bin/ls",argv);// execvp 示例:数组形式,搜索 PATHexecvp("ls",argv);// execve 示例:自定义环境变量char*envp[]={"PATH=/usr/bin","HOME=/root",NULL};execve("/bin/ls",argv,envp);fork 和 exec 代码示例
示例 1:基本 fork 用法
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>intmain(){pid_tpid=fork();if(pid==-1){perror("fork failed");exit(EXIT_FAILURE);}elseif(pid==0){// 子进程printf("子进程: PID=%d, PPID=%d\n",getpid(),getppid());exit(EXIT_SUCCESS);}else{// 父进程printf("父进程: PID=%d, 子进程PID=%d\n",getpid(),pid);wait(NULL);// 等待子进程结束exit(EXIT_SUCCESS);}return0;}示例 2:fork + exec 组合使用
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>intmain(){pid_tpid=fork();if(pid==-1){perror("fork failed");exit(EXIT_FAILURE);}elseif(pid==0){// 子进程:执行 ls 命令printf("子进程: 执行 ls -l\n");char*argv[]={"ls","-l",NULL};execvp("ls",argv);// exec 失败才会执行到这里perror("execvp failed");exit(EXIT_FAILURE);}else{// 父进程:等待子进程结束printf("父进程: 等待子进程完成\n");intstatus;waitpid(pid,&status,0);if(WIFEXITED(status)){printf("子进程正常结束,退出码: %d\n",WEXITSTATUS(status));}elseif(WIFSIGNALED(status)){printf("子进程被信号终止,信号: %d\n",WTERMSIG(status));}exit(EXIT_SUCCESS);}return0;}示例 3:模拟 Shell 执行命令
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>#include<string.h>#defineMAX_ARGS10intmain(){charcommand[100];while(1){printf("mini-shell> ");fgets(command,sizeof(command),stdin);// 去除换行符command[strcspn(command,"\n")]=0;if(strcmp(command,"exit")==0){break;}pid_tpid=fork();if(pid==-1){perror("fork failed");continue;}elseif(pid==0){// 解析命令参数char*argv[MAX_ARGS];intargc=0;char*token=strtok(command," ");while(token!=NULL&&argc<MAX_ARGS-1){argv[argc++]=token;token=strtok(NULL," ");}argv[argc]=NULL;execvp(argv[0],argv);perror("execvp failed");exit(EXIT_FAILURE);}else{wait(NULL);}}return0;}总结
fork和exec是 Unix/Linux 系统中进程控制的核心函数:
- fork:创建一个与当前进程几乎完全相同的子进程,返回两次
- exec:用新程序替换当前进程的执行内容,不创建新进程
两者结合使用是创建新进程并执行新程序的标准模式:fork创建子进程,exec替换子进程的执行内容。这种模式被广泛应用于 Shell、网络服务器等程序中。
本文部分内容参考自《UNIX网络编程卷1:套接字联网API》。