行政部建设公司网站,跨境电商开店详细步骤,网站开发毕设题目,html制作静态网站模板#x1f525;本文专栏#xff1a;Linux Linux实践项目 #x1f338;博主主页#xff1a;努力努力再努力wz 那么今天我们就要进入Linux的实践环节#xff0c;那么我们之前学习了进程控制相关的几个知识点#xff0c;比如进程的终止以及进程的等待和进程的替换#xff0c;… 本文专栏Linux Linux实践项目 博主主页努力努力再努力wz 那么今天我们就要进入Linux的实践环节那么我们之前学习了进程控制相关的几个知识点比如进程的终止以及进程的等待和进程的替换那么我们接下来就要结合前面所讲的进程控制相关的接口比如fork以及waitpid和execl等来自己实现一个命令行解释器那么废话不多说让我们进入正文 ★★★ 本文前置知识 进程的替换 进程的终止与等待 进程的概念 shell实现的框架
那么在用c语言真正上手实操我们的shell外壳程序时那么我们脑海里得有一个大体的实现框架也就是所谓的一个整体思路在有了整体思路后我们再去谈具体每个模块的细节那么我们首先就先从shell本身的工作原理作为切入口入手
那么我们的shell也就是我们的命令行解释器那么它的工作就是获取用户输入的指令然后来执行用户输入的指令那么我们知道用户输入的指令的本质上就是一个字符串所以shell首先就得读取到用户输入的字符串然后保存在一个字符数组中读取到用户输入的字符串之后那么紧接着下一步便是解析用户输入的字符串那么我们用户输入的指令无非可以分成两大部分分别是指令部分以及参数部分那么这里我们就需要定义一个字符指针数组那么数组的每一个元素就是一个指针那么指针指向的就是一个字符串那么我们用户输入的字符串的指令部分就保存在字符指针数组的第一个元素也就是下标为0的位置那么参数部分则依次保存在之后的位置比如我们用户输入的指令是ls -l -a,那么此时我们就要解析为三部分分别是是指令部分的“ls”字符串以及两个参数部分的字符串-l”和“-a”将这三个字符串则是依次保存到我们的字符指针数组下标为0和1和2的位置当中
而具体的解析这三部分字符串则需要用到我们c语言的strtok函数那么具体细节我们下文再说那么这里我们讨论的是大的框架与思路所以我们可以专门定义一个函数来完成这个字符串解析的模块它的工作就是解析用户输入的字符串将其指令部分以及参数部分的各个字符串分别保存到字符指针数组不同位置中并且返回命令行的个数比如用户输入的是ls -l,那么将其保存在字符指针数组char* argv[]并返回的个数就是2而如果是pwd将其保存在字符指针数组char* argv[]并返回的个数就是1
那么接下来解析完用户输入的字符串之后那么我们就可以来执行用户输入的指令了那么这里我们知道我们用户输入的各种指令本质上就是在特定路径下保存的一个可执行文件那么指令的执行本质上就是创建一个进程那么我们shell执行这些指令就得利用fork函数来创建一个子进程然后我们利用fork函数的返回值将父子进程分成不同的执行流那么在子进程的执行流代码片段中我们就可以利用进程的替换那么将我们的子进程的内容替换为我们要执行指令所对应的进程的上下文那么我们父进程的执行流代码片段则是等待我们子进程的退出结果那么我们就需要用waitpid函数来获取子进程的退出码
最后获取完子进程的退出码如果子进程没有正常终止那么就得将情况返回给用户也就是将错误信息打印到终端如果子进程正常终止然后下一步就是重复我们之前上文的环节那么重复也就意味着我们实现的时候最后这些逻辑的代码都要封装到一个死循环当中。
那么这就是我们实现shell外壳程序的一个大框将那么我们可以简单将其分为几个模块分别是获取用户输入-解析用户输入-创建子进程-子进程的替换-父进程等待获取子进程的退出情况-重复上述步骤
那么看到这些模块想必你一定还有一些疑问那么接下来我就会在下文补充每个模块的代码实现以及注意的一些细节和其他的模块的补充那么有了大框架之后那么接下来就让我们具体实现每一个模块了
shell各个模块的实现
1.获取用户输入
那么我们的shell首先得获取用户输入的字符串那么我们知道在c语言中我们获取用户输入的字符串常见就是使用我们的scanf函数来获取用户的输入但是scanf函数有一个缺陷就是一旦读取到空格的时候那么scanf便停止读取输入而我们用户在输入字符串的时候会手动用空格隔开指令部分与参数部分所以我们就不能采取scanf函数来获取输入所以这里我们需要用fgets函数那么fgets函数则是将从标准输入流中读取用户的输入遇到换行符停止那么我们可以指定其在输入流中读取的字符串的长度也就是fgets的第二个参数那么将其保存到一个临时字符数组中如果读取失败那么fgets则会返回NULL读取成功fgets则会返回保存数组的地址 fgets 头文件string.h 功能获取用户输入的字符串末尾自动添加\0 函数原型
char *fgets(char *str, int n, FILE *stream);而我们知道用户在输入之前我们终端都会显示一个命令行提示符会显示我们当前登录的用户名以及所处的工作目录和运行的主机名称所以我们在获取用户输入之前我们得先打印一个字符串也就是命令行提示符而切记我们的shell命令行解释器本质也是一个进程所以这命令行提示符的每一个信息就保存在我们当前进程的环境变量中我们需要通过我们的系统调用接口getenv来获取其中特定字段的环境变量这里就需要获取到我们的USER以及HOSTNMAE以及PWD这三个字段那么我们只需要向getenv函数传递这三个字符串的指针那么他会依次匹配各个字段的名称所对应的字符串并返回对应的值也就是字符串的起始地址
代码实现 printf([%s%s %s]$,getenv(USER),getenv(HOSTNAME),getenv(PWD));if(fgets(temp,sizeof(temp),stdin)NULL){perror(fgets);continue;}2.解析命令行
那么现在我们获取了我们的用户输入的字符串之后那么我们是将用户输入的字符串保存在一个临时字符数组里那么接下来我们就要将这个字符串给分割将其指令部分以及参数部分的各个字符串给分割保存到我们的字符指针数组当中那么我们这里就专门可以定义一个函数来完成字符串解析模块并返回命令行参数的个数那么我们知道我们用户输入的字符串会手动以空格分割那么这里我们就需要调用我们的字符串函数也就是strtok函数来分割我们的字符串按照空格作为分隔符。但是在分割之前我们又得注意一个细节也就是我们用户输入完一个字符串那么它会敲一个回车键来表示输入的结束而回车则是对应的一个换行符\n他会被我们的fegts给读取到那么意味着在我们的字符串的末尾可能会有一个回车换行符
而回车换行符并不是我们一个有效的字符信息所以我们在解析之前得去掉这个换行符所以我们就利用我们的strlen函数首先获取到我们这个字符串包括空格以及换行符的总长度那么如果我判断用户输入的字符串的len-1位置处的字符处是换行符那么我们就将len-1位置用\0来覆盖而\0是标记字符串结尾的标志那么这样我们就可以消去末尾的回车换行符这里是其中一个关键的实现细节
那么第二个细节就是我们的strtok函数的使用那么我们strtok函数第一次调用的时候要传递我们要分割的字符串的首元素的地址那么strtok内部会访问到一个静态的全局变量这个静态变量是用来保存下一次分割的位置那么我们每次调用strtok函数的时候会从分割的起始位置处往后扫描直到遇到分隔符然后将分隔符的位置修改为\0然后返回该分割起始位置的指针而我们知道\0是标记字符串的结尾所以返回分割起始位置的指针就达到了一个分割子串的一个效果 而下一次调用strtok函数的时候那么我们就不用传要分割的字符串的首元素的地址因为上文说过strtok内部能访问到一个记录下一次分割位置的全局变量那么之后的调用我们只需要传递一个NULL即可它内部会继续从这个全局变量记录的位置开始扫描到下一个分隔符将其修改为\0,最后如果我们开始的分割的位置是\0也就是字符串末尾没有更多的子串来分割时候那么strtok就返回一个NULL strtok 头文件string.h -功能分割字符串 函数原型 char *strtok(char *str, const char *delim);在这个函数中我们就定义一个int类型的argc变量来跟踪命令行的个数初始化为0而我们将分割的字符串保存在对应的字符数组的下标就是argc的值保存之后接着递增argc最后返回的该argc就是我们的命令行的个数
代码实现
int getString(char temp[],char* argv[])
{int lenstrlen(temp);if(len0temp[len-1]\n){temp[len-1]\0;len--;}int argc0;char* tokestrtok(temp, );while(toke!NULLargclength-1){argv[argc]toke;tokestrtok(NULL, );}argv[argc]NULL;return argc;
}3.指令判断
那么这里在我们上文介绍实现我们的shell外壳程序的框架的时候模块的时候其实我们故意漏了一个模块那就是指令的判断那么想必你一定会有所疑问那么就是我们获取解析完用户输入的指令之后我们为什么还要进行指令的判断呢直接通通交给子进程去执行不就完了吗我们父进程也就是shell外壳程序的本职工作不就是获取用户的输入吗
那么这里我们就要注意的就是我们用户其中输入的指令比如cd指令也就是更改我们进程所处的工作目录那么它针对的对象其实是我们的父进程也就是我们的shell外壳程序那么如果我们把这个指令交给了子进程去完成将子进程替换为cd指令所对应的上下文那么子进程的执行是不会影响父进程的那么子进程执行结束退出之后我们shell进程所处的工作目录没有进行更改那么所以我们对于有些指令也就是针对当前父进程shell的运行环境的指令比如cd比如PWD指令那么它就不能交给子进程来执行而是得交给父进程来自己完成那么这些指令也就是我们的内置指令
那么内置指令那么就不再是一个编写好的可执行文件那么它是通常是一个实现好的库函数或者直接嵌套在我们的shell进程所对应的代码中所以我们自己用c语言实现的时候那么我们就首先准备定义一个全局属性的字符指针数组然后该数组里面记录了我们所有的内置命令所对应的字符串那么当解析完用户的指令之后解析完保存的字符指针数组的第一个位置就是对应用户输入的指令部分的字符串所以下一步我们依次匹配保存的所有内置命令对应的字符串如果匹配成功那么意味着是内置指令就直接交给我们父进程执行匹配失败则说明该指令不是内置命令就交给子进程来执行那么我们匹配的过程以及内置命令的执行的过程都可以定义两个函数来分别实现这两个模块那么其中字符串的匹配就需要用到strcmp函数来实现
而所谓的内置命令他的底层实现的时候本质其实就是依赖用c编写的库函数或者系统调用比如cd内置命令那么它就是用chdir库函数来实现的那么这个库函数的作用就是能够访问到当前进程的环境变量中的工作目录字段然后修改当前所处的工作目录而pwd内置命令的本质其实也就是依赖getcwd库函数那么该库函数会访问到该进程中环境变量记录当前所处也就是工作目录的字段PWD将其值记录保存到一个数组当中并且返回指向该数组的指针 chdir 头文件unistd.h 函数原型 int chdir(const char *path); getcwd 头文件unistd.h 函数原型 char *getcwd(char *buf, size_t size);
那么这里我在实现的时候就只判断了cd以及pwd这两种内置命令那么我们可以下来直接去添加更多的内置命令然后查询对应实现所依赖的库函数或者系统调用接口
代码实现
bool check(char* argv[])//指令的判断
{for(int i0;order[i]!NULL;i){if(strcmp(argv[0],order[i])0)//如果该指令是内置命令就返回true{return true;}}return false;
}
void ordercomplete(int argc,char* argv[])//内置命令的执行
{if(strcmp(argv[0],cd)0){if(argc2)//cd指令最多只能两个参数其中第二个参数就是跳转的工作目录{if (chdir(argv[1]) ! 0) {perror(chdir);}}else{printf(error: expected argument for cd\n);}}if(strcmp(argv[0],pwd)0){char cwd[length]; // 定义一个字符数组错误的来保存我们的当前所处的工作目录if (getcwd(cwd, sizeof(cwd)) ! NULL) {printf(Current working directory: %s\n, cwd);} else {perror(getcwd failed); // 输出错误信息}}
}5.子进程执行指令
那么剩下几个模块的细节和实现就很简单了接下来这个模块就调用fork函数来创建一个子进程然后利用fork函数的返回值让父子进程有着不同的执行流然后我们在子进程对应的执行流代码片段中调用进程的替换的系统接口而这里我们调用的exec族函数一定是不能带有l的比如execl以及execlp等因为我们不知道用户输入的命令行个数所以不能用可变参数列表的进程替换接口这里要注意 而我们用户输入的字符串都解析在了一个字符指针数组中所以我们传的参数肯定就是一个数组所以这里我们选择进程替换的函数就是execvp那么它可以默认在环境变量的PATH中去匹配我们用户输入的指令所对应的可执行文件 那么我们用execvp函数来将子进程替换为指令所对应的进程的上下文但是我们知道我们进程替换会出现调用失败的情况那么调用失败的结果则是会执行进程替换接口之后的代码那么我们就在execvp函数后面打印一个错误信息并且返回一个特殊的退出码
6.父进程的等待
那么我们父进程对应的执行流代码片段则是等待我们子进程的退出情况所以我们需要调用waitpid函数来获取子进程的退出码那么waitpid我们的等待方式则是设置为阻塞式等待那么它的返回值就分别对应两种情况要么等待成功并且获取到子进程的退出码对应的返回值就是子进程的pid而等待失败则是返回-1我们对于等待失败则是要打印错误信息以及子进程的退出码
完整实现 那么将我们上面的6个模块所对应代码融合就是我们的shell的外壳程序那么其实我们在实现shell外壳程序的时候其实shell的整体实现难度不大主要考察你对shell的工作原理的理解程度和几个系统调用接口的熟悉程度shell实现的真正的难点其实在它各个模块实现的细节上很容易出错其中就考察我们对于一些c语言的库函数的掌握情况那么接下来我就给出完成的shell的c语言代码的实现 #includestdio.h
#includeunistd.h
#includestring.h
#includesys/types.h
#includesys/wait.h
#includestdlib.h
#includestdbool.h
#define length 1000
#define EXIT_FAIL 40
const char* order[]{cd,pwd,NULL};int getString(char temp[],char* argv[])
{int lenstrlen(temp);if(len0temp[len-1]\n){temp[len-1]\0;len--;}int argc0;char* tokestrtok(temp, );while(toke!NULLargclength-1){argv[argc]toke;tokestrtok(NULL, );}argv[argc]NULL;return argc;
}
bool check(char* argv[])
{for(int i0;order[i]!NULL;i){if(strcmp(argv[0],order[i])0){return true;}}return false;
}
void ordercomplete(int argc,char* argv[])
{if(strcmp(argv[0],cd)0){if(argc2){if (chdir(argv[1]) ! 0) {perror(chdir);}}else{printf(error: expected argument for cd\n);}}if(strcmp(argv[0],pwd)0){char cwd[length]; // 定义一个足够大的缓冲区来存储路径if (getcwd(cwd, sizeof(cwd)) ! NULL) {printf(Current working directory: %s\n, cwd);} else {perror(getcwd failed); // 输出错误信息}}
}
int main()
{int argc;char* argv[length];char temp[length];while(1){printf([%s%s %s]$,getenv(USER),getenv(HOSTNAME),getenv(PWD));if(fgets(temp,sizeof(temp),stdin)NULL){perror(fgets);continue;}argcgetString(temp,argv);if(argc0){continue;}if(check(argv)){ordercomplete(argc,argv);continue;}int idfork();if(id0){execvp(argv[0],argv);perror(execvp);exit(EXIT_FAIL);}else{int status;int mwaitpid(id,status,0);if(m0){perror(waitpid);}else{if(WIFEXITED(status)){if(WEXITSTATUS(status)40){printf(error\n);}}}}}return 0;
}在Linux上的运行截图
结语
那么这就是用c语言实现shell外壳程序的所有内容啦那么它也是我第一个学习Linux所完成的一个小项目那么它这个小项目的教学价值以及学习意义其实非常高因为它不仅可以帮组你了解shell外壳程序的工作原理更重要的是帮组你更能熟练掌握运用那几个关于进程控制十分重要的系统调用接口其中比如fork以及waitpid等那么我的下一篇Linux文章就正式进入文件系统啦我会持续更新希望大家多多关注那么如果本篇文章对你有所帮组的话那么还请多多三连加关注哦你的支持就是我最大的动力