进程控制中的进程替换,下文我们学习进程替换的意义,以及进程替换的方式
初步认识进程替换,我们先使用一个比较简单的execl函数,来实现进程替换,并从结果上,了解替换的原理。
单进程进程替换
简单实现一下进程替换,以此推理进程替换原理
#include
#include
#include
int main()
{
//实现单进程的进程替换
printf("hello linux\n");//输出
execl("/usr/bin/ls","ls","-a","-l",NULL);//输入的第一个参数为要执行的进程的绝对路径,后面的参数为命令行参数,可变序列参数以NULL结尾,表示结束
printf("hello world\n");
return 0;
}
单进程实现进程替换,我们发现进程的代码和数据会被替换,替换execl函数内参数所指定的内容。
多进程(父子进程)实现进程替换
#include
#include
#include
#include
#include
int main()
{
//实现单进程的进程替换
// printf("hello linux\n");//输出
// execl("/usr/bin/ls","ls","-a","-l",NULL);//输入的第一个参数为要执行的进程的绝对路径,后面的参数为命令行参数,可变序列参数以NULL结尾,表示结束
//
// printf("hello world\n");
//实现多进程(父子进程)进程替换
//创建子进程
pid_t id=fork();
if(id==0)
{
//表示子进程
printf("i am child process pid: %d ppid: %d\n",getpid(),getppid());//得到父进程子进程的pid
execl("/usr/bin/ls","ls","-l",NULL);//实现程序替换
exit(11);//退出码,查看是否是会覆盖该代码,如果覆盖的话,我们执行的是ls命令,执行成功,返回码exit为0,通过waitpid进行等到得到status,WEXITSTATUS(status)来判断是否是这样
}
else if(id<0)
{
//表示子进程创建失败
perror("fork failed\n");
exit(1);//表示是创建失败
}
else
{
//这是父进程要执行的内容,我们先进行等待
//waitpid等待
int status=0;
pid_t ret=waitpid(-1,&status,0);//阻塞等待
if(ret<0)
{
//表示等待失败
perror("wait failed\n");
exit(2);
}
else
{
//等待成功,我们要输出的是子进程的退出码
printf("i am parent pid: %d child pid: %d child exit_code: %d\n",getpid(),id,WEXITSTATUS(status));
}
}
return 0;
}
上述多进程进程替换后的结果,表明了进程替换的原理,为将新进程的数据和代码直接覆盖原来的进程的代码。
进程替换原理
- 首先,我们知道进程运行时,有task_struct结构体来存储进程信息,以及虚拟空间地址(mm_struct)存储数据,通过页表映射到物理内存
- 先运行原有进程,然后fork创建子进程,在子进程中使用进程替换,把子进程的数据和代码进行替换,操作系统识别到要更改子进程的代码和数据,在物理内存中开辟空间,将新进程代码和数据放入,更改页表指向该地址。
- 通过上述步骤,实现了子进程的进程替换,此时子进程的task_struct除了代码和数据地址,其他没有变化。
如下图所示:
通过上述内容,我们就能明白进程替换的原理了,起始就是更改物理空间原有进程的代码数据,如果是父子进程,那么就在物理空间上开辟新空间,存放代码和数据,更改子进程页表指向。
进程替换函数有7种,其中,库函数(man 3号手册)中有6种函数,系统调用(man 2号手册)有一种函数
//man 2 exec 库函数 6种
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[]);
下面是这6种函数的使用样例:
#include
#include
#include
#include
#include
int main()
{
//使用进程替换,为了简单的演示,我们就单纯使用单进程进程替换来实现
//1. int execl(const char *path, const char *arg, ...);
int num=0;
scanf("%d",&num);//输入参数选定不同的进程替换参数
if(num==1)
{
printf("进程替换函数execl\n");
execl("/usr/bin/ls","ls","-l",NULL);//实现完毕,可变列表的实现,以NULL为结尾
printf("进程替换完毕\n");
}
else if(num==2)
{
//int execlp(const char *file, const char *arg, ...);
//该函数的实现,第一个参数,不必要使用绝对路径,会从PATH以及当前目录下,找到可执行文件,并结合avg命令行参数实现
printf("execlp函数的实现\n");
execl("/usr/bin/pwd","pwd",NULL);//实现显示当前工作路径
}
else if(num==3)
{
//int execle(const char *path, const char *arg, ..., char * const envp[]);
//实现功能:需要绝对路径,需要可变列表,需要环境变量
printf("execle函数的实现\n");
char * const env[]={"why=123",NULL};
execle("/usr/bin/ls","ls",NULL,env);
}
else if(num==4)
{
// int execv(const char *path, char *const argv[]);
printf("execv函数的实现\n");
char * const argv[]={"top",NULL};
execv("/usr/bin/top",argv);
}
else if(num==5)
{
//int execvp(const char *file, char *const argv[]);
printf("execvp函数的实现\n");
char * const argv[]={"ls",NULL};
execvp("ls",argv);
}
else if(num==6)
{
//int execvpe(const char *file, char *const argv[], char *const envp[]);
printf("execvpe函数的实现\n");
char * const argv[]={"ls",NULL};
char * const env[]={"lll=123456",NULL};
execvpe("/usr/bin/ls",argv,env);
}
return 0;
}
解释:
- 对于环境变量以及命令函参数,无论是可变参数形式,还是字符指针数组形式,最后一个元素都要是NULL结尾,标志参数输入完毕。
- 对于这六种函数本质上没什么太大的区别,只是对于参数的形式不同。
- 关于环境变量,我们可以使用putenv和getenv函数来增加或者得到指定的环境变量,参数都是字符指针类型,比如getenv(“PATH”)
对于进程中的环境变量进行解释,以下图为准
- getenv和putenv函数可以实现得到/创建环境变量信息
- PATH环境变量,父子进程都能获得,我们在后文可知,实际上是bash对于进程默认传参environ环境变量(在Linux下就是env命令展示的内容),所以只要系统环境变量中存在的,我们在进程中都能直接getenv获得。
- 子进程得到环境变量有两种方式:新增环境变量(putenv,在fork之前),另外是通过进程替换函数,自定义环境变量参数,传参(是彻底替换,即覆盖默认environ)
通过上述图中代码结果表明,我们putenv新建的环境变量,实际上只是在当前进程中的environ有效,但是等进程结束之后,输入env命令,并不会有我们新增的环境变量,这说明,由于进程的独立性,子进程的环境变量并不会影响其他进程(系统,本身也是一个进程)。
最后,对于库函数中的6种进程替换函数,以及对于其中环境变量与进程的关系,我们已经解释完毕,最后一个系统函数调用execve,我们只需要知道,前6种库函数,最后底层都是调用该函数,对于操作系统进行操作(也只有系统调用函数才能对于操作系统操作),以此全局视角,才能操控进程,从而替换进程中数据和代码,最终实现进程替换。
- 进程替换,实际上只是对于该进程的程序和代码进行替换,子进程的其他信息如pid等都不发生变化,如果是存在父子进程的情况,对父子进程进程替换,会在物理空间中在开辟新空间来存放新进程的代码和数据。
- 掌握7个进程替换函数,其中6个是库函数,一个是系统调用函数,库函数的底层会使用这个系统调用函数,来实现对于系统中的进程进行操作。
- 进程环境变量是有默认的数值的,即一个进程默认的环境变量为操作系统中的env命令展示的内容,在C语言中,我们将environ(类型为char**)变量指向该环境变量,如果在执行进程替换中传递我们自己创建的环境变量, 如myenv,会覆盖原有的environ环境变量。