【Linux】不允许你还不会实现shell的部分功能
一、程序替换(续集)
execl("/usr/bin/ls","ls","-a","-l",NULL);//程序替换函数,此时执行的ls文件的代码
⏩️任何程序想要执行,必须要完成一下两个步骤:1、找到它、加载它——路径+程序名;2、你想怎么执行——由程序和选项决定。
⏩️在上面的代码中,第一个参数是文件路径,后面的参数除了的最后一个NULL都是程序的执行方式。当然程序替换的函数,部分参数确实可以省略,但是不要这么写。
⏩️所有的以 exec 开头的程序替换函数,替换成功的时候,都没有返回值,因为后面的数据和代码都被替换了;替换失败的时候,会返回 -1 ,我们不用直接弄个值来接收它,替换失败之后后面的代码和数据是可以使用的,所以直接弄个退出码来表示替换失败就行。
⏩️那么什么时候替换失败呢?
✅️答:要么就是路径写错了,要么就是选项写错了,总结:替换函数的参数写错了,或者说替换的代码和数据根本不存在。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>int main()
{printf("我是一个进程:%d\n",getpid());pid_t id = fork();if(id == 0)//子进程{//执行另外一个程序的代码execl("/usr/bin/xxx","ls","-a","-l",NULL);//程序替换函数,此时执行的ls文件的代码exit(1);}int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){printf("wait success,exit_code:%d\n",WEXITSTATUS(status));}return 0;
}

⏩️我们来接着看第二个程序替换函数:

⏩️第一个参数是:文件名,不用写路径因为 execlp 会自动到环境变量PATH所表明的路径下查找。
⏩️其余参数:跟 execl 的参数一样,选项 + NULL。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>int main()
{printf("我是一个进程:%d\n",getpid());pid_t id = fork();if(id == 0)//子进程{//执行另外一个程序的代码//execl("/usr/bin/xxx","ls","-a","-l",NULL);//程序替换函数,此时执行的ls文件的代码execlp("ls","ls","-a","-l",NULL);exit(1);}int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){printf("wait success,exit_code:%d\n",WEXITSTATUS(status));}return 0;
}

⏩️我们看一下第三个程序替换函数:

❌️注意:exec 开头的函数没有带 p 的就是要写路径,带 p 的就是要写文件名就行。
⏩️上面的函数中第一个参数:是文件所在的路径;第二个参数是一个指针数组,用来存储执行文件的方式 。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>int main()
{printf("我是一个进程:%d\n",getpid());pid_t id = fork();if(id == 0)//子进程{//执行另外一个程序的代码//execl("/usr/bin/xxx","ls","-a","-l",NULL);//程序替换函数,此时执行的ls文件的代码//execlp("ls","-a","-l",NULL);char* argv[] = {(char*)"ls",(char*)"-a",(char*)"-l",NULL};execv("/usr/bin/ls",argv);exit(1);}int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){printf("wait success,exit_code:%d\n",WEXITSTATUS(status));}return 0;
}

❌️注意:上面代码中的 argv 这张表是传给 ls 文件的 main 函数的命令行参数,之前学的 exec 函数是也是如此。
⏩️我们再来看一下第四个程序替换的函数:
int execvp(const char *file, char *const argv[]);
⏩️这个函数的参数就不多说了。大家都懂。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>int main()
{printf("我是一个进程:%d\n",getpid());pid_t id = fork();if(id == 0)//子进程{//执行另外一个程序的代码//execl("/usr/bin/xxx","ls","-a","-l",NULL);//程序替换函数,此时执行的ls文件的代码//execlp("ls","-a","-l",NULL);char* argv[] = {(char*)"ls",(char*)"-a",(char*)"-l",NULL};//execv("/usr/bin/ls",argv);execvp("ls",argv);exit(1);}int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){printf("wait success,exit_code:%d\n",WEXITSTATUS(status));}return 0;
}

⏩️那么问题来了:能不能替换自己写的程序呢?
✅️答:可以,请看下面代码:
❌️注意:替换函数指定的文件是一个可执行文件(二进制文件),不是我们写的C++代码文件,没有被编译的文件。所以我们替换其他语言编译的二进制文件。
⏩️我这里写了个程序:
#include<iostream>int main()
{std::cout << "我是一个C++程序" << std::endl;std::cout << "我是一个C++程序" << std::endl;std::cout << "我是一个C++程序" << std::endl;std::cout << "我是一个C++程序" << std::endl;return 0;
}
li⏩️然后让其他文件的代码来替换这个程序:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>int main()
{printf("我是一个进程:%d\n",getpid());pid_t id = fork();if(id == 0)//子进程{//执行另外一个程序的代码execl("./othercmp.exe","othercmp.exe",NULL);}int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){printf("wait success,exit_code:%d\n",WEXITSTATUS(status));}return 0;
}

⏩️我们看一下第五个程序替换函数:
int execvpe(const char *file, char *const argv[],char *const envp[]);
⏩️第一个参数是:文件名,OS会根据默认路径来找该文件。第二个参数:执行可执行文件的方式列表。第三个参数:传我们自定义的 环境表里列表过去,并且覆盖原来的环境变量,也就是说原来的环境变量(继承父进程的)没有了。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>int main()
{printf("我是一个进程:%d\n",getpid());pid_t id = fork();if(id == 0)//子进程{sleep(2);printf("下面的代码都是子进程自己在执行\n");char* myargv[] = {(char*)"othercmp",(char*)"-a",(char*)"-b",NULL};char* myenv[] = {(char*)"PATH = /home/lisi",NULL};extern char** environ;//继承父进程的环境变量execvpe("othercmp.exe",myargv,myenv);exit(3);}int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){printf("wait success,exit_code:%d\n",WEXITSTATUS(status));}return 0;
}
#include<iostream>
#include<stdio.h>int main(int argc,char* argv[],char* env[])
{for(int i = 0; i < argc; i++){printf("argc[%d]:%s\n",i,argv[i]);}std::cout << "\r\n";for(int j = 0;env[j];j++){printf("env[%d]->%s\n",j,env[j]);}//std::cout << "我是一个C++程序" << std::endl;//std::cout << "我是一个C++程序" << std::endl;//std::cout << "我是一个C++程序" << std::endl;std::cout << "我是一个C++程序" << std::endl;return 0;
}

⏩️我们可以看到原来父进程的环境变量没有被子进程继承了,而是使用我们自定义的环境变量。
⏩️当然我们也可以传父进程的环境变量过去:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>int main()
{printf("我是一个进程:%d\n",getpid());pid_t id = fork();if(id == 0)//子进程{sleep(2);printf("下面的代码都是子进程自己在执行\n");char* myargv[] = {(char*)"othercmp",(char*)"-a",(char*)"-b",NULL};char* myenv[] = {(char*)"PATH = /home/lisi",NULL};extern char** environ;//继承父进程的环境变量execvpe("othercmp.exe",myargv,environ);exit(3);}int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){printf("wait success,exit_code:%d\n",WEXITSTATUS(status));}return 0;
}

⏩️那么问题来了,我们可以既继承父进程的环境变量的继承上,加上我们自定义的环境变量吗?也就是说我们要父进程的环境变量也要我们自己定义的环境变量:
⏩️使用 putenv 函数:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>int main()
{printf("我是一个进程:%d\n",getpid());pid_t id = fork();if(id == 0)//子进程{sleep(2);printf("下面的代码都是子进程自己在执行\n");char* myargv[] = {(char*)"othercmp",(char*)"-a",(char*)"-b",NULL};char* myenv[] = {(char*)"PATH = /home/lisi",NULL};extern char** environ;//继承父进程的环境变量putenv((char*)"hhhhhh=aaaaaaaa");execvpe("othercmp.exe",myargv,environ);exit(3);}int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){printf("wait success,exit_code:%d\n",WEXITSTATUS(status));}return 0;
}

⏩️总结:程序替换函数一共由7个,在上面的其实有六个,这六个是库函数:

⏩️还剩一个是系统调用,它不在上面这张图中:

⏩️这个函数的使用通过上面的例子我们也能轻松使用。
二、自主 Shell 命令行解释器
⏩️我们写一条指令,执行,然后又写一条。然后又执行代码,bash是怎么做到的呢?这里我们实现部分 shell 的功能,用我们之前学到的知识来实现:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include<string>#define MAXSIZE 128
#define MAXARGS 32//命令行参数表
char* gargv[MAXSIZE];
int gargc = 0;//我们shell自己所处的工作路径
char cwd[1028];//最近一个进程的退出码获取
int lastcode = 0;//环境变量表
char* genv[MAXARGS];
int genvc = 0;//绝对路径的最后一个文件名获取
static std::string rfindDir(const std::string& p)
{if(p == "/") return p;const std::string psep = "/";auto pos = p.rfind(psep);if(pos == std::string::npos) return std::string();return p.substr(pos + 1);
}void LoadEnv()
{//正常情况下,环境变量表内部是从配置文件中来的//现在我们是从父进程拷贝过来extern char** environ;for(int i = 0;environ[i];i++){genv[i] = (char*)malloc(sizeof(char)*4096);strcpy(genv[i],environ[i]);genvc++;}genv[genvc] = NULL;}const char* GetUserName() {char* name = getenv("USER");if(name == NULL){return "None";}return name;
}const char* GetHostName()
{char* hostname = getenv("HOSTNAME");if(hostname == NULL)return "None";return hostname;
}const char* GetPwd()
{char* pwd = getenv("PWD");//第一种方案//char* pwd = getcwd(cwd.sizeof(cwd));//第二种方案if(pwd == NULL){return "None";}return pwd;//第一张方案:返回pwd,第二种方案:返回cwd
}void PrintCommandLine() {printf("[%s@%s %s]# ",GetUserName(),GetHostName(),rfindDir(GetPwd()).c_str());fflush(stdout);//刷新缓冲区
}int GetCommand(char commandline[],int size)
{//获取用户输入if(NULL == fgets(commandline,size,stdin))return 0;//用户在输入的时候,至少要摁一下回车\n,例如:abcd\n//我们要把最后一个字符设置成\0commandline[strlen(commandline) - 1] = '\0';return strlen(commandline);
}int ParseCommand(char commandline[])
{gargc = 0;char gsep[] = " ";memset(gargv,0,sizeof(gargv));gargv[0] = strtok(commandline,gsep);while((gargv[++gargc] = strtok(NULL,gsep)));//字符切割//printf("gargc:%d\n",gargc);int i = 0;//for(;gargv[i];i++)//printf("gargv[%d]:%s\n",i,gargv[i]);return gargc;}int ExecuteCommand()
{//进行程序替换pid_t id = fork();if(id < 0){printf("创建子进程失败\n");return -1;}else if(id == 0){//子进程//printf("我是一个子进程\n");execvpe(gargv[0],gargv,genv);//程序替换函数执行命令exit(1);//替换失败}else{//父进程int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){lastcode = WEXITSTATUS(status);//printf("wait success\n");}elseperror("waitpid");}return 0;
}//return val:
//0:不是内建命令
//1:内建命令并且执行完毕
int CheckBuiltinExecute()
{if(strcmp(gargv[0],"cd") == 0){if(gargc == 2)//内建命令有很多,这里写一个cd就行。更改路径:cd + 路径{//1、更改进程内核的路径chdir(gargv[1]);//工作路径在gargv[1]里面,chair更改进程的路径,在哪里就改谁的//2、更改环境变量char pwd[1024];getcwd(pwd,sizeof(pwd));snprintf(cwd,sizeof(cwd),"PWD=%s",pwd);putenv(cwd);lastcode = 0;}return 1;}else if(strcmp(gargv[0],"echo") == 0){if(gargc == 2){if(gargv[1][0] == '$')//获取到环境变量的指令,内建命令{if(strcmp(gargv[1]+1,"?") == 0)//打印环境变量{printf("lastcode:%d\n",lastcode);}}else if(strcmp(gargv[1]+1,"PATH") == 0){printf("%s\n",getenv("PATH"));//getenv和getenvc的本质是环境变量表}lastcode = 0;}return 1;}return 0;
}
int main()
{LoadEnv();//加载环境变量char command_line[MAXSIZE] = {0};while(1){//打印命令行字符串PrintCommandLine();//获取用户输入if(GetCommand(command_line,sizeof(command_line)) == 0)continue;//printf("%s\n",command_line);//解析字符串:ls -a -l ,我们写的这个命令行解释器,就是对用户输入的命令字符串//首先进行解析!ParseCommand(command_line);//路径切换,切换的是父进程bash的路径,而是不是子进程的路径//这个命令到底是让父进程 bash 自己执行(内建命令)还是让子进程执行if(CheckBuiltinExecute())//判断是否是内建命令{continue;}//执行子进程命令ExecuteCommand();//sleep(1);}return 0;
}
⏩️哦!对了,各位优秀的程序员觉得我博客给你带来帮助或者让你学到了知识,记得给博主一个关注哦~❤️❤️❤️
⏩️各位博友,下篇博客见🍁🍁🍁

