文章目录
-
目录
前言
1.进程替换概念
2.进程替换的原理
3.进程替换的接口
4.接口功能验证
①execl接口演示
②execlp接口演示
③execle接口演示
④execv接口验证
5.尝试写一个自己的shell【了解】
前言
你一定见过类似于这样的“黑框框”,这个“黑框框”其实就是我们的终端,我们可以在这个黑框框中输入相应的命令,来执行一定的功能。在博主的【Linux中的环境变量】一文中,解释了其是命令行中的命令,其实都是一个个被编写好的可执行文件,我们使用对应的命令其实就是在执行相应的程序。那么这个操作到底是怎么实现的呢,我们可不可写一个类似的“黑框框”呢?
1.进程替换概念
请注意进程的切换与替换是两个完全不同的概念,在博主的【进程的调度与替换】一节中讲解了,进程的切换,如果感兴趣的可以去阅读一下,进程替换顾名思义,就是用另一个进程替换掉当前正在执行的进程。
2.进程替换的原理
还记得我们之前谈论的【进程地址空间】吗?在这篇文章中我们讲解了一个进程所拥有的存储数据的结构mm_struct,我们在这篇文章中介绍到了虚拟地址通过页表转化到物理地址,进程替换的原理就与页表、进程地址空间、物理内存这三者有关,当一个进程使用进程替换接口时,正在按照执行流执行的进程停止向下执行,此时操作系统将需要替换的进程的数据和代码从磁盘中加载到物理内存,并将页表重新映射。进程替换接口调用结束后,原进程的代码和数据都被释放(包括寄存器信息),此时代码从新的代码数据开始执行。
3.进程替换的接口
#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[]);
说明:
在上述接口中,
①path参数:表示要传递一个文件地址,这一般指的是可执行文件的地址
②file参数:表示要传递一个文件的名字,而后该替换函数会根据环境变量中设置的路径依次递归查找该文件
③arg参数:表示可变参数列表,这个参数必须以NULL结尾(注意不是“NULL”),这个参数等同于你在命令行下想怎么执行这个可执行文件。(有示例代码)
④envp参数:表示向替换程序传递环境变量
⑤argv参数:表示以数组形式向替换程序传递的命令行参数,需要以NULL为结尾。
以上的六个函数接口,只有在失败的时候才会返回,返回值为-1,如果程序替换成功,在该进程中该接口后的代码都不会执行。
命名理解:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATHe(env) : 表示自己维护环境变量
4.接口功能验证
注意:在示例代码中,有关主函数的代码中为了追求代码的简便,没有在主函数中设置对紫禁城的等待,这样的作法在工程中是不好的,请不要模仿,这里只是为了演示接口而写的简易代码
①execl接口演示
在使用进程替换之前我们需要一份要替换的程序,在本文中使用一个已经编写好的C++程序(注意:这个被替换的文件可以是任何语言生成的可执行程序),这个示例代码意思是,如果传递的命令行参数小于等于1,那么就报错,如果超过1那么就将这些参数进行打印。
#include <iostream>
using namespace std;
int main(int argc,char *argv[])
{
if(argc<=1)
{
cout<<"未传递足够多的命令行参数"<<endl;
return 0;
}
for(int i=0;argv[i];i++)
{
cout<<argv[i]<<endl;
}
return 0;
}
#include <iostream>
#include <unistd.h>
int main()
{
pid_t id=fork();
if(id==0)
{
execl("./exec","./exec","ls",NULL);
}
return 0;
}
②execlp接口演示
在该接口的演示程序中,我们使用程序替换执行ls命令行命令,这个命令默认是被加载到环境变量中的,所以我们使用程序替换执行是有结果的,但是如果你想要让你的程序也可以让execlp直接执行,需要将你的程序路径加载的对应的环境变量中,具体操作可参考【Linux中的环境变量】
③execle接口演示
我们先将原来的示例代码稍作更改:
#include <iostream>
using namespace std;
int main(int argc,char *argv[],char *env[])
{
if(argc<=1)
{
cout<<"未传递足够多的命令行参数"<<endl;
return 0;
}
for(int i=0;argv[i];i++)
{
cout<<argv[i]<<endl;
}
for(int i=0;env[i];i++)
{
cout<<env[i]<<endl;
}
return 0;
}
可以看到,程序替换后的进程成功拿到了,传递的环境变量数据。
④execv接口验证
注意对于其他接口中带v的其实本质都一样就是将原来的一个一个分散的参数变为一个数组直接传递过去,这些数组要以NULL作为结尾。
5.尝试写一个自己的shell【了解】
一个简单的shell:
#include <iostream>
#include <string>
#include <vector>
#include <sys/wait.h>
#include <string.h>
#include <unistd.h>
#define MAXSIZE 64
void Iteractive(std::string& line)
{
line="";
std::cout<<getenv("USER")<<"@"<<getenv("PWD")<<"#: ";
std::getline(std::cin,line);
}
void GetCommandLine(char *line,char *commandline[])
{
commandline[0]=strtok(line," ");
for(int i=1;commandline[i]=strtok(NULL," ");i++);
}
int Execute(char * commandline[])
{
pid_t id=fork();
if(id==0)
{
execvp(commandline[0],commandline);
}
int status;
pid_t rid =waitpid(id,&status,0);
return status;
}
int main()
{
char * commandline[MAXSIZE]={0};
std::string line;
//打印交互窗口
while(1)
{
Iteractive(line);
//获取命令函参数并分割
GetCommandLine((char *)line.c_str(),commandline);
//处理内建命令
//执行命令
int ret=Execute(commandline);
if(ret!=0)
{
std::cout<<"进程替换出错!"<<std::endl;
//处理错误
exit(1);
}
}
return 0;
}
该程序是一个比较简单的版本,只能执行环境变量可自行推测的可执行程序,运行该程序后一些常见的指令都可以执行如:“pwd”、“ls”、“whoami”等,这些可以执行的叫做外部命令,这类命令通常是由独立的可执行文件,当执行这些程序的时候,需要加载到内存。但是有一些指令我们是无法执行的,比如“ll”、“mkdir”、“rm”等,这些不能执行的指令叫做内建命令,这类命令通常是内置在特定的操作系统版本中定制的,不需要加载到内存就可以执行的命令。我们使用exec系列接口只能替换有可执行文件的程序,但是对于内建命令这种不存在可执行程序的命令,我们就需要自己手动来编写来实现该特定的功能。
这个版本的shell,比上一个代码中,多了内建命令检测的函数,我们将内建命令统一存储在一个全局变量Foreign_key_command中,如果还想添加其它的内建命令除了要自己实现相应的逻辑外,还要在这个全局变量中添加这个命令的名字,本文添加一个内建命令“cd”,其他的命令就不做添加了,这里仅仅是以cd命令作为一个示例。该代码编译后会生成几个警告,无需进行理会,直接执行即可。同时对于该cd命令的逻辑与我们命令行中cd的逻辑有所不同,下示代码是一个“阉割版”的cd命令,没有设置错误检查和对错误地址的检查。
#include <iostream>
#include <string>
#include <vector>
#include <sys/wait.h>
#include <string.h>
#include <unistd.h>
#define MAXSIZE 64
#define FOREIGN_KEY_SIZE 64
char *Foreign_key_command[FOREIGN_KEY_SIZE]={"cd"};
void Iteractive(std::string& line)
{
line="";
std::cout<<getenv("USER")<<"@"<<getenv("PWD")<<"#: ";
std::getline(std::cin,line);
}
void GetCommandLine(char *line,char *commandline[])
{
commandline[0]=strtok(line," ");
for(int i=1;commandline[i]=strtok(NULL," ");i++);
}
int Execute(char * commandline[])
{
pid_t id=fork();
if(id==0)
{
execvp(commandline[0],commandline);
}
int status;
pid_t rid =waitpid(id,&status,0);
return status;
}
bool Is_Foreign_key(char *command)
{
for(int i=0;i<FOREIGN_KEY_SIZE;i++)
{
if(strcmp(command,Foreign_key_command[i])==0)
{
return true;
}
}
return false;
}
void Bulid_externel_command( char * commandline[])
{
//这里就以cd命令作为示例,不实现其他内建命令。
if(strcmp(commandline[0],"cd")==0)
{
char *address=commandline[1];
if(address==nullptr)
{
address=getenv("HOME");
}
chdir(address);
char current_address[1024];
getcwd(current_address,1024);
char pwd[1024];
snprintf(pwd,1024,"PWD=%s",current_address);
putenv(pwd);
}
}
int main()
{
char * commandline[MAXSIZE]={0};
std::string line;
//打印交互窗口
while(1)
{
Iteractive(line);
//获取命令函参数并分割
GetCommandLine((char *)line.c_str(),commandline);
//处理内建命令
if(Is_Foreign_key(commandline[0]))
{
Bulid_externel_command(commandline);
}
//执行命令
else
{
int ret=Execute(commandline);
if(ret!=0)
{
std::cout<<"进程替换出错!"<<std::endl;
//处理错误/释放资源....
exit(1);
}
}
}
return 0;
}