进程创建( fork 与 vfork)
pid_t fork (void):详细介绍fork
- fork函数是一个非常重要的函数,它从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程。创建子进程用到写时拷贝实现。子进程拷贝父进程PCB中的数据(虚拟地址空间,页表),子进程是父进程的一个复制品。
pid_t vfork(void):详细介绍vfork
- 从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。创建的子进程与父进程共用同一个虚拟地址空间。vfork 创建子进程后会阻塞父进程(阻塞父进程是为了防止调用栈混乱),直到子进程退出或程序替换。
fork 与 vofork 的区别:
不同:
-
fork(): 父子进程的执行次序不确定。
vfork():保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行。 -
fork(): 子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。
vfork():子进程共享父进程的地址空间。
相同:
- fork 与 vfork 两个接口创建子进程,都是在内核中调用 clone 函数实现的。
进程终止
进程终止场景:
- 正常退出,结果符合预期。
- 正常退出,结果不符合预期。
- 异常退出。
如何终止一个进程:
-
在main()函数中return,退出前会刷新缓冲区。
-
调用 exit(int statu) 接口(参数:status 定义了进程的终止状态,父进程通过wait来获取该值 ),该接口是一个库函数接口,退出前会刷新缓冲区。
-
调用_exit(int statu) 接口(参数:status 定义了进程的终止状态,父进程通过wait来获取该值 ),该接口是一个系统调用接口,退出时直接释放所有资源。
PS:上面所有的返回值大小都是通过一个字节来存储的,例如:exit(-1) , return -1, _exit(-1), 在终端执行echo $?(查看上一条执行命令执行之后的返回结果)发现返回值 是255。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 int main()
5 {
6 _exit(-1);
7 }
进程等待
什么是进程等待
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。为了避免僵尸进程的产生,采取进程等待:父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的方法
阻塞接口:为了完成一个功能,发起调用,若当前不具备完成条件,则一直等待!直到完成后返回。
非阻塞接口:为了完成一个功能,发起调用,若当前不具备完成条件,则立即报错返回。
-
wait
函数原型:pid_t wait(int*status),这是一个阻塞接口,等待任意一一个子进程退出,若没有子进程退出,则一直等待。其中子进程的返回值会传入到参数 statu 中。返回值: 成功返回被等待进程pid,失败返回-1。
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
-
waitpid
函数原型:pid_ t waitpid(pid_t pid, int *status, int options);参数:
pid:- ①: Pid=-1,等待任一个子进程。与 wait 等效。
- ②: Pid=指定子进程的pid。等待其进程ID与pid相等的子进程。如果没有当前指定的子进程 pid ,则会报错返回。
status:
- 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。更加详细了解status
options:
- ①:设置为 0 时为阻塞操作,如果没有子进程退出,则会一直等待,直到子进程退出父进程才会退出。
- ②:设置为 WNOHANG 时,为非阻塞操作。(对于 WNOHANG 来说。①:如果有子进程退出(子进程先于父进程退出),即正常返回,则返回值就是子进程的 pid; ②:如果有子进程,但是却还没有收集到子进程退出(父进程先于子进程退出),它也会立即返回,不会像wait那样永远等下去,则返回值 ==0;③:如果父进程中没有通过 fork()创建子进程,则会报错,返回值为 -1 )
返回值:
- ①:正常返回(即返回值>0),返回的是退出子进程的 pid。
- ②:如果设置了选项WNOHANG,而调用中 waitpid 发现没有已退出的子进程可收集,则返回0(即子进程没有退出的时候,返回值 == 0)
- ③:如果出错,则等待返回-1(即返回值 < 0)
进程程序替换
替换原理
- 一般我们在一个进程中调用 fork 创建出子进程,该子进程往往需要执行另一个程序,此时我们就需要调用 exec 函数。当进程调用一种 exec 函数时,该进程的执行程序完全替换了新进程,而新程序则从其 main 函数开始执行。注意:exec 并不创建新进程,所以前后子进程的 ID 并没有改变。exec 只是用一个新的程序替换了当前进程的正文、数据、堆和栈段。 PS:exec 函数族不仅能替换子进程,也能替换父进程,所有进程都能替换。
替换函数
- 程序替换函数是一族函数,可以通过man命令进行查看。 其中有六种以exec开头的函数,统称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[]);//系统调用
path:路径
file:环境变量PATH路径下的执行程序
arg/argv[]:从第1个是真正的参数,第0个是要执行文件本身的名称
envp[]:环境变量
对于这些函数名来说:
-
带 p 与 不带 p ( 表示path )的区别:
①: 不带字母 p 的exec函数,第一个参数必须是程序的相对路径或绝对路径,例如”/bin/ls”或”./a.out”,而不能是”ls”或”a.out”。
②:带字母 p 的exec函数,可以不用写路径,自动从PATH中寻找需要执行的程序,但是仅限于环境变量PATH路径下的程序。如果写上路径也可以正常执行。 -
l(list:表示采用列表) 和 v(vector:表示采用数组)的区别:
①: 带有字母 l 的exec函数,要求将新程序的每个命令行参数都当作一个参数传给它,最后一个参数应该是NULL。
②:带有字母 v 的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后⼀个指针也是NULL。 -
带 e 与 不带 e ( 表示env )的区别:
①:带 e 表示新程序自己来维护环境变量。
②:不带 e 表示 新程序使用当前环境变量。
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 如果调用出错则返回 -1 ,所以exec函数只有出错的返回值而没有成功的返回值。其中,只有 execve 是真正的系统调用,其它五个函数都是库函数调用,它们都调用 execve。
execl:
6 //最后一个参数必须是NULL
7 if(execl("/bin/ls","ls","-a","-l",NULL) == -1)
8 {
9 perror("execl error");
10 exit(-1);
11 }
execlp:
13 //最后一个参数必须是NULL
14 if(execlp("ls","ls","-a","-l",NULL) == -1)
15 {
16 perror("execl error");
17 exit(-1);
18 }
execle:
21 char* env[] = {"MYTEST=123","HELLO=456",NULL}; //最后一个参数必须是NULL
22 if(execle("./env","./env",NULL,env) ==-1) //最后一个参数必须是NULL
23 {
24 perror("execl error");
25 exit(-1);
26 }
- execv:
29 char* argv[]={"ls","-a","-l",NULL};//最后一个参数必须是NULL
30 if(execv("/bin/ls",argv))
31 {
32 perror("execl error");
33 exit(-1);
34 }
execvp:
29 char* argv[]={"ls","-a","-l",NULL};//最后一个参数必须是NULL
30 if(execvp("ls",argv) == -1)
31 {
32 perror("execl error");
33 exit(-1);
34 }
execvpe:
29 char* argv[]={"./env",NULL};//最后一个参数必须是NULL
30 char* env[] = {"MYTEST=123","HELLO=456",NULL};//最后一个参数必须是NULL
31 if(execve("./env",argv,env) == -1)
32 {
33 perror("execl error");
34 exit(-1);
35 }