Linux进程控制(下):进程等待和进程替换
文章目录
- 一、进程等待
- 是什么?
- 为什么?(进程等待必要性)
- 怎么做?(进程等待的方法)
- 获取子进程的status
- wait函数
- waitpid
- 具体代码实现
- 进程的阻塞等待方式
- 进程的非阻塞等待方式
- 问题
- 二、进程替换
- 替换原理
- 七个替换函数
- 函数解释
- 命令理解
一、进程等待
是什么?
通过系统调用wait/waitpid,来进行对子进程进行1状态检测与回收的功能
为什么?(进程等待必要性)
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,连“杀人不眨眼”的
kill -9
也无能为力,因为谁也没有办法杀死一个已经死去的进程。 - 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程可以通过进程等待的方式,回收子进程资源,获取子进程退出信息
注意:
- 僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题——这是必须解决的
- 通过进程等待,获取子进程的特殊情况(知道我们布置给子进程的任务,完成的怎么样了)——取决于我们的需求,不是必要的
怎么做?(进程等待的方法)
获取子进程的status
进程等待所使用的两个函数wait/waitpid,都有一个status参数:
#include<sys/types.h>
#include<sys/wait.h>pid_t wait(int*status);
pid_ t waitpid(pid_t pid, int *status, int options);
该参数是一个输出型参数,由操作系统进行填充。
如果不关心子进程的退出状态信息,就对status参数传入NULL。反之,操作系统会通过该参数将子进程的退出信息反馈给父进程。
status是一个整型变量,但不能简单的当作整型来看待,可以当作位图来看待:
一个整数有4个字节,每个字节有8个比特位,status共有32个比特位,status的不同比特位代表的信息不同,如图(只研究status低16比特
位):
所以,我们可以通过位操作,配合status得到进程的退出码和退出信号。
exit_code = (status >> 8) & 0xFF; //退出码
exit_signal = status & 0x7F; //退出信号
为此,系统提供了两个宏来获取退出码和退出信号:
WIDEXITED(status)
:若子进程正常终止退出,则为真。(用于查看进程是否是正常退出)WEXITSTATUS(status)
:若非零,则为子进程的退出码(用于查看进程的退出码)
值的注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。
wait函数
这个函数可以说是waitpid的子集,它的功能没有waitpid强大,且它的功能waitpid也都能够实现。因此,我们学会了waitpid,wait也就自然会用了,但推荐使用waitpid。
#include<sys/types.h>
#include<sys/wait.h>pid_t wait(int* status);
- 作用:等待任意子进程
- 参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
- 返回值:等待成功则返回等待进程的pid,等待失败则返回-1
waitpid
#include<sys/types.h>
#include<sys/wait.h>pid_t waitpid(pid_t pid, int* status, int options);
- 作用:等待指定子进程或任意子进程
- 参数:
- pid:
- pid = 0,等待任意一个子进程,与wait等效
- pid > 0,等待进程ID与pid相等的指定子进程
- status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
- options:选择父进程是否需要阻塞等待子进程退出
WNOHANG
:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。0
:默认阻塞等待子进程退出
- pid:
- 返回值:
- 等待成功返回被等待进程的pid
- 如果设置了参数option为
WNOHANG
,而调用中waitpid发现没有已退出的子进程可以等待,则返回0 - 如果调用中出错,返回-1
具体代码实现
进程的阻塞等待方式
将options设置为0,进程就会以阻塞的状态进行等待:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>int main()
{int id = fork();//代码创建子进程if(id < 0){printf("fork error\n");}else if(id == 0)//child{printf("I am a child process PID:%d, PPID:%d\n", getpid(), getppid());sleep(5);exit(1);//子进程退出}//fatherint status = 0;//状态int ret = waitpid(id, &status, 0);//参数3为0,默认阻塞等待,等待5Sif(ret > 0)//等待成功{if(WIFEXITED(status)) {printf("子进程正常退出了 ret:%d, exit code:%d, exit sig:%d\n", ret, WEXITSTATUS(status), status&0x7F);}else {printf("进程异常了\n");}}else if(ret == 0){printf("子进程还在运行!\n");}else //等待失败{printf("wait false\n");}return 0;
}
结果:
进程的非阻塞等待方式
如果等待的时间略长,就让进程在那傻傻的等未免有点浪费了,我们可以将options设置为WNOHANG
,让进程在等待的闲暇做点其他事:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{int pid = fork();if(pid < 0){printf("%s fork error\n",__FUNCTION__);return 1;}else if( pid == 0 )//child{ printf("child is run, pid is : %d\n",getpid());sleep(5);exit(1);} int status = 0;int ret = 0;do{ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待if( ret == 0 ){printf("Child is running.");printf("I can do other thing\n");//这里父进程可以做其他事情}sleep(1);}while(ret == 0);if( WIFEXITED(status) && ret == pid ){printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));}else{printf("wait child failed, return.\n");return 1;}return 0;
}
结果:
这里每一次循环都会检查子进程是否完成,若未完成,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程终止后读取子进程的退出信息。因此被称作非阻塞轮询。
问题
父进程要拿子进程的状态数据,任意数据,为什么必须要用wait等系统调用呢》
因为进程具有独立性,子进程与父进程相互独立,都有各自的PCB,是没有权利去其他PCB去获取数据的
在进程的PCB中,存在两个成员:
退出码和信号码,它们便是进程的退出信息
二、进程替换
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),然而子进程往往需要调用一种exec函数一执行另一个程序。
当进程调用一种exec函数时,该进程的用户空间代码和数据会完全被新的程序替换,从新程序的起始开始执行。
调用exec并不创建新进程,所以调用exec前后该进程的ID并未发生改变。
CPU如何得知新程序的入口地址呢?
Linux中形成的可执行程序是有格式的,ELF是可执行程序的表头,而可执行程序的入口地址就在表中。
子进程进行进程程序替换后,会影响父进程的代码和数据吗?
子进程刚被创建时,父子进程代码和数据共享,当子进程需要被程序替换时,也就是需要修改子进程的数据和代码了,这时就会发生写时拷贝,这样父子进程的代码和数据不再共享,因此子进程被程序替换后,父进程代码和数据不受影响。
注意:代码也可以写时拷贝
环境变量是什么时候给进程的?环境变量会被替换吗?
环境变量也是数据,创建子进程的时候,环境变量就已经被子进程继承了。
环境变量是具有全局属性的,当当是只替换程序,环境变量信息是不会被替换的。
程序替换成功后,exec后面的代码会被执行吗?替换失败呢?
程序替换成功后,exec后面的代码不会被执行!
替换失败了,才可能会执行后续的代码。
七个替换函数
进程程序替换函数共有七个,其中六个都是在调用系统调用函数execve,对其进行封装,实现不同需求的六大函数。
它们统称为exec函数。
int execl(const char *path, const char *arg, ...);
- 第一个参数是要执行程序的路径
- 第二个参数是可变参数列表,表示你要如何执行这个程序,并以
NULL
结尾。
例如,要替换的是ls程序:
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
用可变参数的原因其实很好理解,找到这个程序后,我们要知道如何执行这个程序,需要涵盖哪些选项?这是不确定的。
💡:命令行怎么写,我们就怎么传
int execlp(const char *file, const char *arg, ...);
- 第一个参数是要执行程序的名字
- 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
例如,要替换的是ls程序:
execlp("ls", "ls", "-a", "-i", "-l", NULL);
这里看到两个一样的参数,会不会冗余?
其实它们的含义的作用是不同的:前者作用为找到该程序,后者作用是如何运行这个程序。
那为什么一个凭字符串指针就可以帮助我们找到这个程序呢?
这是环境变量的功劳,PATH中有ls的路径。
如果我们的exec*能够替换系统命令,那能不能替换我们自己的程序呢?
当然可以,但是需要注意环境变量的问题。
如果想给子进程传递环境变量,该怎么传递呢?
- 新增环境变量:在父进程中直接使用putenv
- 彻底替换:下文函数
int execle(const char *path, const char *arg, ...,char *const envp[]);
- 第一个参数是要执行程序的路径
- 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
- 第三个参数是你自己设置的环境变量,覆盖子进程原有的环境变量。
例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
剩余这三个函数与前三个函数作用是对应相同的,唯一区别是将可变参数更改为了字符指针数组,我们需要预先设置好该数组,然后作为参数传入。
一样的,命令行怎么写,我们数组就怎么设置
注意:数组需要以NULL结尾
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 调用成功则没有返回值
命令理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记了,如图:
exec函数族: