进程程序替换
进程程序替换是指将一个正在运行的进程替换为另一个可执行程序。它的本质是调用了Linux操作系统中的exec
系统调用。而exec系统调用是一个家族函数,例如execl
、execv
、execle
、execve
等。它们的共同特点是当当前进程执行到该函数时,就会直接跳转到新的程序并开始执行新的可执行文件。
exec系统调用
所以我们要想知道怎样进行进程程序替换就要先会使用exec系统调用,而且exec系统调用的家族函数只要掌握了一个,其他的也就大差不差了。
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
所以就先详细的讲一下execl函数。
execl系统调用函数
#include <unistd.h>
int execl(const char *path, const char *arg0, ..., const char *argn, (char *)0);
该函数接受可执行文件的路径(path
)以及一系列的参数(arg0
至argn
),最后以一个空指针(char *)0
结尾。arg0
代表可执行文件本身(即argv[0]),arg1
到argn
是命令行参数。
代码示例
int main()
{
cout<<"before execl"<<endl;//执行前
execl("/usr/bin/ls","ls","-l","-a",NULL);//必须以空结束
cout<<"after execl"<<endl;//执行后
return 0;
}
我们不难发现我们自己写的一个可执行程序运行的时候,在调用execl函数时会执行新的命令(可执行程序文件)而且在执行完成之后就直接结束了,并没有执行我们自己写的可执行程序的后续操作。就相当于在代码中调用其他程序。所以可以初步得出:当execl
函数成功执行时,当前进程将被替换为指定的可执行文件,并从该文件开始执行。注意,execl
函数不会创建新的进程,而是将当前进程替换为新的可执行文件。
函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
执行原理
当我们运行我们的可执行程序时,该程序就已经变成了进程了,所以自然就少不了进程PCB以及进程地址空间和页表,和代码数据所映射的物理内存。但是当我们执行到execl函数时,就要开始执行一个已有的全新程序(并不创建进程),而该已有的程序(文件)自然存在磁盘当中,所以我们运行这个新的程序时就要将程序加载进内存。但是关键的是该新程序中的代码和数据并不是另存的,而是直接以覆盖式的存放在原进程的代码和数据所在物理内存中对应的位置,并继续从新程序的启动例程开始执行。
以上就是单进程程序替换的过程
多进程的程序替换
int main()
{
pid_t i=fork();
if(i==0)//子进程
{
cout<<"before execl,"<<"mypid:"<<getpid()<<endl;
execl("/usr/bin/ls","ls","-l","-a",NULL);//必须以空 结束
cout<<"after execl,"<<"mypid:"<<getpid()<<endl;
}
else//父进程
{
pid_t ret=waitpid(-1,NULL,0);
if(ret==i)
cout<<"等待成功,"<<ret<<endl;
}
return 0;
}
我们知道开始我们就创建了一个子进程,而创建子进程的特点就是:数据独立以写时拷贝的方式存在,代码共享。而此时子进程执行execl函数,所以子进程就替换成了ls-a-l的一个程序,此时新程序的代码数据会覆盖在物理内存上。我们知道正常情况下父子进程数据发生改变时会以写时拷贝的方式另存空间,但是代码并不会。可是此时情况特殊,所以代码同样也会以写时拷贝的方式另存空间。归根结底还是:进程具有独立性
所以回到开始,为什么execl函数之后的代码不执行?其实就是程序替换,导致原进程的代码数据被新程序的代码数据覆盖,所以后续的代码自然就没有了,更不可能被执行。
其实我们的进程切换对语言是没要求的,可以自己随意地切换成其他语言的程序,不一定只切换成同语言的程序。
程序替换总结
程序替换是指将当前运行中的进程完全替换为一个新的程序。简单来说,就是将当前进程的代码、数据和堆栈等内容替换为新程序的代码、数据和堆栈。
当一个进程执行程序替换时,原来进程的代码、数据和堆栈被新程序覆盖,然后开始执行新程序的代码。这意味着原来进程的运行状态、打开的文件、socket连接等都会丢失,并且无法恢复。新程序从main函数开始执行,其运行过程与原程序无关。
而且对于需要传环境变量的exec类系统调用函数而言,环境变量属于全局的字符指针数组类型的变量environ,而且一个进程的环境变量是源于其父进程的环境变量,也就是说子进程会继承父进程的环境变量。不仅仅是创建子进程时会继承环境变量和命令行参数,而对于进程切换时也是会继承下来的。而对于进程替换而言原进程会采用覆盖环境变量内容的方式让新的进程继承下来,也就是说如果原进程调用函数接口时传入的环境变量是自己写的环境变量的话,那么切换的新进程会将原进程的环境变量里的内容继承下来。但是如果不想全部内容都覆盖式传递的话可以采用putenv(char* env_val)函数在原环境变量表里去添加新的环境变量env_val,此时就不会清空原环境变量里的内容(如果导入的环境变量名相同则会覆盖式导入,如果不同则是添加环境变量,切记我们导入的环境变量实质上是地址,环境变量表相当于char* arr[]类型)。而添加内容后的环境变量会传递给新的进程main函数的第三个参数当中。
实现一个简易的命令行解释器
#define max 1024
const char* get_username()
{
const char* p=getenv("USER");
return p;
}
const char* get_hosthome()
{
const char* p=getenv("HOSTNAME");
return p;
}
const char* get_pwd()
{
const char* p=getenv("PWD");
return p;
}
void get_command(char* command,size_t num)
{
printf("[%s@%s %s]# ",get_username(),get_hosthome(),get_pwd());
fgets(command,num,stdin);//fgets函数会读取\n
command[strlen(command)-1]=0;
}
void get_split(char* in[],char out[])//拆分
{
int argc=0;
in[argc++]=strtok(out," ");
while(in[argc++]=strtok(NULL," "));//未找到对应串时末尾会添加NULL
}
int lastcode = 0;
void my_exec(char* argv[])
{
pid_t i=fork();
if(i==0)//子进程
{
execvp(argv[0],argv);
exit(-1);//进程没有成功切换
}
else//父进程
{
int status;
int ret = waitpid(i,&status,0);
if(ret==0)
cout<<"出错"<<endl;
else
lastcode=WEXITSTATUS(status);
}
}
char mycwd[512];//添加环境变量实质是添加地址,所以应始终有效
char myenv[150][100];int sz=0;//环境变量表
int deal(char* argv[])//内建命令就是bash自己内部函数执行的命令,不是bash子进程
{
if(strcmp(argv[0],"cd")==0)
{
if(!argv[1]) return 1;
chdir(argv[1]);//改变当前工作目录(..)
//处理环境变量里的PWD
char tmp[512];//临时变量存放
getcwd(tmp,sizeof(tmp));//将当前进程相对路径转为绝对路径存入tmp中
sprintf(mycwd,"PWD=%s",tmp);//存进全局变量中(地址)
putenv(mycwd);
return 1;
}
else if(strcmp(argv[0],"export")==0)
{
if(!argv[1]) return 1;
strcpy(myenv[sz],argv[1]);
putenv(myenv[sz]);
sz++;
return 1;
}
else if(strcmp(argv[0],"echo")==0)
{
if(!argv[1]) {
cout<<endl;
return 1;
}
char* tmp=argv[1]+1;//取得$后面的数据
if(strcmp(tmp,"?")==0){
cout<<lastcode<<endl;
lastcode=0;
}
else if(argv[1][0]=='$')//打印环境变量
{
cout<<getenv(tmp)<<endl;
}
else{
cout<<argv[1]<<endl;
}
return 1;
}
return 0;
}
int main()
{
while(1)
{
char command[max];
char* argv[max];
//接收命令行内容
get_command(command,max);
if(strlen(command)==0)//此时命令行中仅仅输入回车键
continue;
//拆分命令行
get_split(argv,command);
//进程切换调用
int ret = deal(argv);//先判断是不是内建命令
if(ret==1) continue;//是内建命令就不用让子进程去执行
my_exec(argv);//创建子进程并替换执行
}
return 0;
}