掌握Linux进程替换:从原理到实战(自定义shell)
目录
进程程序替换
看看效果
进程替换原理---fork
认识全部接口
execl
execlp
execv
编辑
execvp
execvpe
编辑
execve系统调用
编辑
自定义shell
1.输出命令行提示符
2.获取用户输入的命令
3. 命令行分析
4.执行命令
5.检测并处理内建命令
初始化工作
总源码
进程程序替换
在Linux中sleep,自身就是一个命令
它也是C语言写的一个可执行程序,当它跑起来时,也是一个进程
可以发现在sleep执行时,我们输入ls,pwd等命令无法执行,我们的bash在干什么?
sleep在执行时,bash在阻塞等待,因此命令会卡住
看看效果
先来看一个函数接口,execl,作用是执行一个文件
我们写一段代码来认识它
可以发现,我们的程序居然把系统的命令ls给跑起来了,所以当我们的进程先跑自己的代码,再遇到execl时,又去执行系统的二进制和代码,这种现象叫做程序替换
进程替换原理---fork
所谓的程序替换,就是自己再执行的时候,没调execl之前,就执行我的printf,它默认执行的就是我们的代码,可是执行到了execl函数时,等于这个函数给我们指向了新的一个程序,新的一个程序即新的代码和数据,而一个进程刚开始创建,先有PCB,地址空间,代码段数据段,堆区栈区,页表映射,代码数据。一旦当进程运行到execl,并成功执行,它就会把execl所指明的新的路径下的新的程序,重新覆盖式的写入我们原有进程的数据段和代码段当中,整个替换过程只替换当前进程的代码和数据。
进程 = 内核数据结构 + 代码和数据
也就是说PCB不变,页表的映射关系可能发生调整,但是整个进程基本不变,只是将新的代码和数据替换进来,这个过程叫做程序替换
在程序替换的过程中,并没有创建新的进程,只是把当前进程的代码和数据,用新的程序的代码和数据覆盖式的进行替换
为什么我们的刚才的程序,第一个printf执行了,第二个printf没有执行呢?
原因是,一旦程序替换成功,就去执行新代码了,原始代码的后半部分,已经不存在了
换句话说,1.一旦程序替换成功,那原始代码的后半部分是不会执行的
那有没有可能 执行失败呢?
我们发现它的替换就是失败的,就没有替换
我们看到execl只有错误时,才有返回值,为什么只有错误时才有返回值呢?成功时没有?
因为如果成功,那么原始代码的后半部分就被替换掉了,没有意义
2.exec*系列函数,只有失败返回值,没有成功返回值
exec*系列函数,不用做返回值判断,只要返回,就是失败
所以我们的程序一旦替换失败了,我们也不用判断,直接让程序直接结束,退出码设为1
认识全部接口
#include <unistd.h>extern char **environ;int execl(const char *pathname, const char *arg, .../* (char *) NULL */);int execlp(const char *file, const char *arg, .../* (char *) NULL */);int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);int execv(const char *pathname, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[],char *const envp[]);
我们可以看到所有的exec*接口,前缀exec都一样,而后面有不同的命名方案,我们稍后再解释
在C语言中,参数栏中...代表可变参数列表,如果参数数量不确定,就可以用可变参数列表来进行声明
execl
path表示路径+程序名的方式,表示我要用谁来替换我的原有程序
后面arg,... 命令行中怎么写,我们就怎么传
就是我们命令行中以空格分隔,然后以字符串方式传入参数
那么能用 ls -ln这样的混在一起命令吗?
可以的 ,所以命令行怎么写,我们就怎么传,就像是列表一样,将命令拆开依次传入到参数栏
execl的l就是list的意思,我们平常写的单链表,最后一个节点是空节点,
因此规定参数栏必须以NULL结尾,表明参数传递完成
有没有一种方法,我不想让程序替换把我的原有程序给替换掉,我想让其他的程序帮我执行程序替换的逻辑,不影响我原有的程序逻辑
用子进程
所以这样设计代码,就让子进程执行进程替换的逻辑,父进程正常执行原有逻辑
为什么子进程程序替换不影响父进程呢?
进程具有独立性
数据和代码都发生写实拷贝,子进程一旦程序替换成功,就和父进程彻底分离了
我们知道一个程序运行需要先加载,加载器来加载,加载器把对应的程序放到内存里,任何一个程序到内存中需要先加载成为进程,所以程序的加载本质上就是一个动态创建进程的过程,而我们学习了shell之后,就知道我们所有的进程都是bash的子进程,我们的命令都是由shell创建出子进程,然后父进程只要进行wait就可以了,而子进程去进行程序替换,加载新的进程,这样对应的命令就能被子进程跑起来了。
所以我们今天学习的exec*就属于加载器的范畴
我们的程序替换能替换代码和数据,能替换我们自己的程序吗?
可以
可以看到我们的程序用execl将我写的C++程序运行了
那么我们是否能用程序替换将python,PHP,shell脚本的东西调用起来呢?
都可以调用,所以以后任何语言包括java,我们都可以用我们的C语言程序调用,为什么能全都能调?
因为不管你是什么语言,你的脚本语言,编译型语言,在寄存器里要运行,全都是进程,只要是进程都能被程序替换,只不过在替换时,替换是代码数据不是脚本,替换的是你的解释器的代码,然后再从脚本文件依次读取。
只要你的程序未来要在系统中跑,你就必须是进程,只要是进程,就全都能替换
验证程序替换不会创建新的进程
execlp
execv
ls命令也是一个C语言写的可执行程序,它内部也有main函数,任何一个程序都有命令行参数,argc,argv,我们曾经没有说过,这两个参数是谁传的,谁传的,怎么传的?
任何一个程序,调用exec*函数时,如果有命令行参数,经过argv,把argv传递给path
其实在我们的参数里面,所有程序运行时都是bash的子进程,那么谁给我们传命令行参数?
父进程bash
怎么传?
通过exec传的
execvp
execvpe
code.c
int main(){printf("我的程序开始运行\n");if(fork() == 0){printf("I am Child,My pid is: %d\n",getpid());sleep(1);//childchar*const argv[] = {(char*const)"other",(char*const)"-a",(char*const)"-b",(char*const)"-c",(char*const)"-d",NULL};char*const env[] = {(char* const)_"MYVAL=123456789", NULL}execvpe(./other,argv,env);exit(1);}waitpid(-1,NULL,0);printf("我的程序运行完毕\n");return 0;
}
那我如何以新增的方式,给子进程传入环境变量呢?
两种做法
1.putenv函数
putenv,可以导入环境变量,假设 A->B->C,A是B的父进程,B是C的父进程
当在B中新增环境变量,那么A不会导入新的环境变量,而C会导入新的环境变量
2.我们用exec*e,带e的exec函数,传的时候先putenv()导出到我们的进程,再传入environ,把默认的环境变量地址传进去,它就拿到了新的环境变量
execve系统调用
上面的接口都是对系统调用,语言级别的封装
真正调用的还是execve
所以上面的函数不用传递env的原因,是由于系统调用execve时,会自动传入env参数
自定义shell
1.输出命令行提示符
我们可以用printf直接打印出,命令行提示符,但是这样太固定了,我们有没有方法直接获取用户名和主机名以及路径呢?
getenv,用户名主机名等都存在环境变量中,我们可以调用getenv获取对应的变量
snprintf
就是把格式化的可变内容往字符串中输出
除此之外,我们这样获得的pwd是一个绝对路径,在提示符中太长了,我们想只要显示当前目录的名称
那就只能做切割字符串,路径都是以/来分割的,要么只要一个/表示根目录,要么就是一堆路径用/分割,这种情况,我们只需要最后一个/后的内容
2.获取用户输入的命令
先从标准输入获取,获取失败就返回false
由于我们输完命令会输入回车,因此我们要把命令中的\n,给清理掉
如果用户什么都没输入,直接按回车,我们返回false
3. 命令行分析
大部分我们对应的命令," ls -a -l"这是一个字符串,未来我们要执行命令时,是无法执行一个字符串的,我们的命令行参数要么以数组传入,要么以链表传入,都不能以字符串传入,因此我们要拆分字符串,要做命令行分析
我们怎么存分割的字符串呢?
我们定义一个全局的命令行参数表
用strtok来切割字符串,填充命令行参数表
4.执行命令
直接用fork,创建子进程,并用exec*函数进程替换执行我们的命令,让父进程阻塞等待
我们应该调哪个接口呢?
应该用execvp,因为我们目前所存的命令就在vector中,需要带v,而我们的程序名又不带路径,我们需要其从环境变量中找,需要带p,而我们也不想传入环境变量,不带e
5.检测并处理内建命令
我们在执行CD目录的时候发现路径不发生变化,因为目前我们执行命令都是子进程执行的,而一旦是子进程执行,改变路径时,是子进程自己改变的,父进程没改,而cd真正改变的是父进程的路径,因为父进程改变了,以后子进程拷贝父进程才能在新的路径下工作,所以cd命令不能让子进程执行,而让父进程亲自执行,把自己的路径切掉,这种命令叫做内建命令
所以我们还可以添加一个检测是否是内建命令的函数,如果是内建命令,我们就不需要创建子进程去执行,而是让父进程执行,这个我们就做了解,因为如果真要把所有的内建命令都添加到函数中,那可就是一个大工作了,我们知道内在逻辑就可以了
除了cd是内建命令
echo也是内建命令,我们来实现一下echo
echo有 $?查看最近一个进程的退出码的功能,需要创建一个全局变量来记录退出码
并在execute调用exec函数时,将该变量传入
除此之外,echo还能通过 $环境变量,查看环境变量的值
这里就调用getenv直接查询
最后就是基本功能 回显输出
初始化工作
系统shell在启动的时候,会从系统中获取环境变量,但是我们的shell做不到,这个读是需要用shell脚本的,重写一个语言才行,我们只能从父进程拷贝一份,维护环境变量表,我们还要把环境变量表导入到环境变量空间中
所以我们知道shell有两张表,一张叫做命令行参数表,一张叫做环境变量表
总源码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "// 下面是shell定义的全局数据// 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0; // 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;// for test
char cwd[1024];
char cwdenv[1024];// last exit code
int lastcode = 0;const char *GetUserName()
{const char *name = getenv("USER");return name == NULL ? "None" : name;
}const char *GetHostName()
{const char *hostname = getenv("HOSTNAME");return hostname == NULL ? "None" : hostname;
}const char *GetPwd()
{//const char *pwd = getenv("PWD");const char *pwd = getcwd(cwd, sizeof(cwd));if(pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}const char *GetHome()
{const char *home = getenv("HOME");return home == NULL ? "" : home;
}void InitEnv()
{extern char **environ;memset(g_env, 0, sizeof(g_env));g_envs = 0;//本来要从配置文件来//1. 获取环境变量for(int i = 0; environ[i]; i++){// 1.1 申请空间g_env[i] = (char*)malloc(strlen(environ[i])+1);strcpy(g_env[i], environ[i]);g_envs++;}g_env[g_envs++] = (char*)"HAHA=for_test"; //for_testg_env[g_envs] = NULL;//2. 导成环境变量for(int i = 0; g_env[i]; i++){putenv(g_env[i]);}environ = g_env;
}//command
bool Cd()
{// cd argc = 1if(g_argc == 1){std::string home = GetHome();if(home.empty()) return true;chdir(home.c_str());}else{std::string where = g_argv[1];// cd - / cd ~if(where == "-"){// Todu}else if(where == "~"){// Todu}else{chdir(where.c_str());}}return true;
}void Echo()
{if(g_argc == 2){// echo "hello world"// echo $?// echo $PATHstd::string opt = g_argv[1];if(opt == "$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if(opt[0] == '$'){std::string env_name = opt.substr(1);const char *env_value = getenv(env_name.c_str());if(env_value)std::cout << env_value << std::endl;}else{std::cout << opt << std::endl;}}
}// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"std::string dir = pwd;if(dir == SLASH) return SLASH;auto pos = dir.rfind(SLASH);if(pos == std::string::npos) return "BUG?";return dir.substr(pos+1);
}void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());//snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}void PrintCommandPrompt()
{char prompt[COMMAND_SIZE];MakeCommandLine(prompt, sizeof(prompt));printf("%s", prompt);fflush(stdout);
}bool GetCommandLine(char *out, int size)
{// ls -a -l => "ls -a -l\n" 字符串char *c = fgets(out, size, stdin);if(c == NULL) return false;out[strlen(out)-1] = 0; // 清理\nif(strlen(out) == 0) return false;return true;
}// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "g_argc = 0;// 命令行分析 "ls -a -l" -> "ls" "-a" "-l"g_argv[g_argc++] = strtok(commandline, SEP);while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));g_argc--;return g_argc > 0 ? true:false;
}void PrintArgv()
{for(int i = 0; g_argv[i]; i++){printf("argv[%d]->%s\n", i, g_argv[i]);}printf("argc: %d\n", g_argc);
}bool CheckAndExecBuiltin()
{std::string cmd = g_argv[0];if(cmd == "cd"){Cd();return true;}else if(cmd == "echo"){Echo();return true;}else if(cmd == "export"){}else if(cmd == "alias"){// std::string nickname = g_argv[1];// alias_list.insert(k, v);}return false;
}int Execute()
{pid_t id = fork();if(id == 0){//childexecvp(g_argv[0], g_argv);exit(1);}int status = 0;// fatherpid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}int main()
{// shell 启动的时候,从系统中获取环境变量// 我们的环境变量信息应该从父shell统一来InitEnv();while(true){// 1. 输出命令行提示符PrintCommandPrompt();// 2. 获取用户输入的命令char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline, sizeof(commandline)))continue;// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"if(!CommandParse(commandline))continue;//PrintArgv();// 检测别名// 4. 检测并处理内键命令if(CheckAndExecBuiltin())continue;// 5. 执行命令Execute();}//cleanup();return 0;
}