进程的等待与替换
进程等待
1.进程等待的必要性
·我们之前介绍过了僵尸进程,即子进程退出时父进程没有接收到子进程的退出状态信息,这时的子进程就成为了僵尸进程,从而导致内存泄露。而进程一旦成为僵尸进程,那么任何手段都无法杀死这个进程,因为没有人能杀死一个已经死去的进程。因此,为了防止内存泄露的危害,需要进行进程等待来回收僵尸进程。
·其次,进程等待还可以让父进程获取子进程的运行结束状态。当然,这并非必须的,防止出现内存泄露才是进程等待的主要目的。
·最后从编码层面上来说,进程等待可以保证父进程晚于子进程退出,从而规范化资源回收。
2.进程等待的方法
我们已经了解了进程等待的必要性,即为什么要进程等待,那么要怎么做到进程等待呢?接下来将介绍两个进程等待的方法,即wait()
和waitpid()
两个函数。
wait()方法
首先,wait函数的功能是等待任意一个进程退出,并获取该进程的退出状态。注意是等待任意一个进程退出,因此在有多个进程需要等待时,就需要通过循环来等待所有进程退出。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int*status);
wait函数的返回值在函数运行成功时会返回所等待的子进程的pid,若函数运行失败则返回-1。wait函数的参数为输出型参数,用于获取子进程的退出状1态,我们后面再介绍,在不关心该参数时可以将其设置为NULL。
下面我们通过一段代码来理解一下wait函数:
int main()
{
int n = 3;
while(n--)
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
exit(-1);
}
if(id == 0)//child
{
int count = 3;
while(count)
{
printf("child.pid:%d,ppid:%d,count:%d\n",getpid(), getppid(), count--);
sleep(1);
}
exit(0);//退出子进程
}
}
//father
sleep(3);
int m = 3;
while(m--)
{
pid_t ret = wait(NULL);
printf("father wait done, pid:%d\n", ret);
sleep(1);
}
return 0;
}
waitpid()方法
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收 回收,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid参数
pid = -1
,等待任一个子进程。与wait等效。pid > 0
等待其进程ID与pid相等的子进程。
status参数
首先我们来深入认识一下status这个参数,这是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。虽然status是一个整型参数,但其实status的高16位是无意义的。
我们直到int型有32个bit位,而对于status,其只有低16位有意义,这16位中的高8位标识了子进程的退出码信息,低7位标识进程异常退出时的信息,第8 位为core dump标志,后面会介绍。
一个进程的退出分为正常终止和异常退出:
可以看到进程被杀死是没有为0的信号的,因此若status的低7位不为0,则说明进程被信号所杀,此时8-15位是没有意义的。而只有低7位全为0时,才表示进程正常终止,此时的status中8-15位就是进程的退出码。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
exit(-1);
}
if(id == 0)
{
//child
//int *p = 0x12334;
//*p = 100;
int count = 5;
while(count)
{
printf("I am child,pid:%d,count:%d\n",getpid(),count--);
sleep(1);
}
printf("child exit\n");
exit(10);//退出码为10
}
//father
int status = 0;//获取子进程的退出码
pid_t ret = waitpid(id, &status, 0);//等待pid为id的子进程退出
int sig = status & 0x7F;//退出信号
sleep(8);
if(sig == 0)//退出信号为0,子进程正常退出
{
int code = (status >> 8) & 0xFF;//子进程退出码
printf("wait successfully,exit code:%d,pid:%d\n", code, ret);
}
else
{
//退出信号不为0,子进程被信号终止
printf("exit unnormally,sig:%d\n", sig);
}
return 0;
}
至此,我们详细了解了status这个参数,但是在通过status参数获取退出码和终止信息时有些复杂,因此可以用宏进行替换:WIFEXITED(status)
: 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status)
: 若WIFEXITED
非零,提取子进程退出码。(查看进程的退出码)
if(WIFEXITED(status))//退出信号为0,子进程正常退出
int code = WEXITSTATUS(status);//子进程退出码
options参数
在之前的代码中,我们默认waitpid()的第三个参数为0,其代表阻塞状态,对应的还有WNOHANG这种非阻塞状态。
WNOHANG
: 若pid
指定的子进程没有结束,则waitpid()
函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
需要注意的是:
·如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
·如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
·如果不存在该子进程,则立即出错返回。
非阻塞轮询方案代码:
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
exit(-1);
}
if(id == 0)//child
{
int count = 5;
while(count)
{
printf("I am child, pid:%d, count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
//father
int status = 0;
int ret = 0;
do
{
ret = waitpid(id, &status, WNOHANG);//非阻塞轮询方案
if(ret == 0)
{
printf("child is running\n");
}
sleep(1);
}while(ret == 0);
if(WIFEXITED(status))
{
//等待成功
printf("wait successfully, exit code:%d\n",WEXITSTATUS(status));
}
else
{
printf("wait failed, sig:%d\n",status & 0x7F);
}
return 0;
}
运行结果:
进程替换
1.进程替换的原理
我们知道fork创建子进程后,子进程执行与父进程相同的程序。那么是否可以让子进程执行另一个程序呢?这便是我们接下来要介绍的进程替换了。首先,子进程在创建后会有一块自己的进程地址空间(虚拟内存),同时通过页表映射到物理内存上,而进程替换的原理就是通过改变页表的映射关系,将子进程的进程地址空间映射到要替换的程序所对应的物理内存上即可,此时子进程要执行的程序就被替换了。
而在进程替换的过程中,本质上并没有创建新的进程,因为虽然子进程执行的程序被替换了,但子进程自己的pid并没有发生变化。
2.进程替换的方法
替换函数
系统给我们提供了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 execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
通过man指令查阅手册:
函数解释
·这些函数若执行成功则程序被替换,后续代码不再执行。
·若调用出错则返回-1。
·由于exec系列函数调用成功返回值没有意义,因此函数只有调用失败的返回值,而没有成功的返回值。
命名理解
我们先来认识exec系列函数名中字母的含义
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
下面我们通过代码来熟悉上述函数:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
printf("I am child. pid: %d,ppid:%d\n",getpid(), getppid());
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
exit(1);//调用失败
}
//father
int count = 3;
while(count)
{
sleep(1);
printf("I am father. pid: %d, count: %d\n", getpid(), count--);
}
int status = 0;
int ret = waitpid(-1, &status, 0);
if(WIFEXITED(status))//子进程正常退出
{
int code = WEXITSTATUS(status);//获取子进程退出码
printf("wait successfully, exit code: %d, pid: %d\n", code, ret);
}
else
{
printf("wait failed,sig: %d\n", (status & 0x7F));
}
return 0;
}
以上代码的执行结果为:
可以看到,对于execl
函数,第一个参数是要替换的程序所在的路径,其后为可变参数列表,即为在命令行上要执行的命令及选项,这种传参方式叫做以列表方式传参,即list方法;最后需以NULL结尾,告知exec系列函数传参结束。
我们再来看看execv
函数接口:
char* const my_argv[] = { "ls", "-l", "-a", NULL};
execv("/usr/bin/ls", my_argv);
可以看到,execv
函数接口就不是以列表的形式传入参数了,而是通过数组(vector)的形式传入参数,当然两者的最终结果并无区别。
了解了execl
以及execv
两个函数接口后,我们再来认识一下execlp
函数接口:
execlp("ls", "ls", "-l", "-a", NULL);
我们知道,有p会自动搜索环境变量PATH(只有系统命令或我们导入到PATH路径下的命令可以搜索),这也就意味着,在第一个参数中对于要替换的指令我们并不需要带上路径,而后面的参数则与execl
函数接口的参数一直,并且改函数的结构与前两个函数也相同。
在熟悉了上述三个函数后,我们就不难使用execvp
函数接口,即无需路径的以数组形式传参:
execvp("ls", my_argv);
接下里我们再来认识一下execle
以及execve
这两个函数接口,首先字母e的含义为环境变量,那么我们就知道了所要传入的参数为环境变量。
这里所传的环境变量可以是我们自己定义的环境变量,当然也可以是系统自带的环境变量:
//exec_cmd.c
int main()
{
char* const my_env[] = {"MYENV=helloworld", NULL};//自己定义的环境变量或者系统自带的环境变量
char* const my_argv[] = {"mycmd", NULL};
//execl("./mycmd", "mycmd", NULL);
//execle("./mycmd", "mycmd", NULL, my_env);
execve("./mycmd", my_argv, my_env);//切换进程
return 0;
}
//cmd.c
int main()
{
for(int i = 0; i < 10; i++)
{
printf("myenv:%s,i:%d\n", getenv("MYENV"), i);
}
return 0;
}
3.小结
·通过上面的内容我们知道程序替换就是通过exec系列的函数让特定进程取加载磁盘中的其他程序,以达到运行的目的,并且期间不创建新的进程。
·如果子进程有新的程序需求就需要进程替换。
·exec系列函数只要返回了,就说明函数调用失败了。