目录
Part3.3. 命令字符串分割 CommandlineParse
Part3.5. Execute (),fork + execvp + dup2 重定向
前言
前面学了这么多知识,我们可以写一个简易的自定义myshell来复习一波,接下来来跟随小编的视角来看看吧。
let's go!!!!!!!!
Part1. Shell 核心原理前置知识
Shell 本质就是运行在用户态的命令行解释程序,它的核心工作流程固定:
- 打印提示符,读取用户输入一行命令;
- 解析命令字符串,切割成 argv 参数数组;
- 识别特殊语法:重定向
> >> <;- 识别别名 alias,替换原始命令;
- 判断是否为内建命令:cd/echo/export/alias 这类,不能 fork 子进程执行,必须在当前 shell 进程内执行,因为它们要修改 shell 自身的进程环境(当前工作目录、环境变量);
- 外部命令:调用
fork()创建子进程,子进程通过execvp加载程序,父进程waitpid等待子进程结束,回收子进程,保存退出码$?;- 循环回到打印提示符,等待下一条输入。
重点区分:内建命令 vs 外部命令 外部命令(ls、cat)是磁盘上的可执行文件,必须 fork+exec; cd 为什么不能 fork 执行?如果 fork 后子进程 chdir,只会修改子进程的工作目录,父 shell 当前目录完全不变,所以 cd 必须在本进程执行。
Part2. 整体代码架构总览
全局变量说明
#define COMMAND_SIZE 1024 char* argv[MAX_ARGV_SIZE]; // 存放解析后的命令参数数组,execvp要求char*[]格式 int argc=0; // 参数个数 int lastcode=0; // 保存上一条命令退出码,对应shell里$? std::unordered_map<std::string,std::string> alias_table; // 别名表 // 重定向标记 #define NO_REDIR 0 #define INPUT_REDIR 1 #define OUTPUT_REDIR 2 #define APPEND_REDIR 3 int redir=0; std::string file_name; // 重定向目标文件名
Part3. 模块逐段代码解析
Part3.1. 提示符模块
const char* GetPwd() { const char* pwd=getcwd(cwd_buf,sizeof(cwd_buf)); if(pwd!=NULL) { snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd_buf); putenv(cwdenv); } return pwd==NULL?"None":pwd; }
getcwd获取当前进程工作目录;- 同步更新环境变量
PWD,其他程序读取 PWD 时拿到最新路径;snprintf拼接提示符字符串;fflush(stdout):关键,printf 默认行缓冲,如果输出不带换行,缓冲区不会自动刷出,提示符会卡住不显示,必须手动 fflush。
Part3.2. 读取命令行
GetCommandlinebool GetCommandline(char* out,int size) { char* comline=fgets(out,size,stdin); if(comline==NULL) return false; out[strlen(out)-1]=0; // 把fgets读到的末尾'\n'替换成'\0' if(strlen(out)==0) return false; return true; }
fgets读取一行输入,会保留回车换行\n;- 手动覆盖掉
\n,方便后续字符串解析;- 返回 false 代表空输入,直接跳过本轮循环。
缺陷:fgets 遇到 Ctrl+D(EOF)返回 NULL,代码仅 return false,没有退出 shell,真实 bash 收到 EOF 直接退出。
Part3.3. 命令字符串分割
CommandlineParsebool CommandlineParse(char* out) { #define SEP " " argc=0; argv[argc++]=strtok(out,SEP); while((bool)(argv[argc++]=strtok(NULL,SEP))); argc--; return argc>0?true:false; }
strtok字符串分割,按空格切分,填充 argv 数组。注意:strtok 会原地修改原字符串,把分隔符替换成
\0,适合这里解析场景。 致命短板:不支持引号。原生 bash 支持echo "hello world"作为单个参数,strtok 直接按空格切,引号失效;同时连续多个空格可以分割,但 tab 制表符无法识别。Part3.4. 重定向解析模块
void CheckRedir(char* out) { redir=NO_REDIR; std::string s=out; if(s.find(">>")!=s.npos) { redir=OUTPUT_REDIR; size_t pos=s.find(">>"); out[pos]=0; // 在>>位置截断原命令字符串,后面留给重定向文件 file_name=s.substr(fpos); } else if(s.find('>')!=s.npos) { redir=APPEND_REDIR; // ...逻辑类似 } }核心思路:
- 在原始命令行字符串找到
>>/>/<;- 在符号位置写入
\0截断命令,前面部分交给命令解析;- 提取符号后的文件名保存到
file_name;- 设置全局
redir标记,fork 子进程后执行 dup2 修改文件描述符。代码里这里有一个 BUG:
>>识别后标记为 OUTPUT_REDIR,>识别标记 APPEND_REDIR,宏定义写反了,后续执行逻辑会错乱! 宏定义:OUTPUT_REDIR=2,APPEND_REDIR=3,代码逻辑写反:>>追加 应该绑定 APPEND_REDIR;>覆盖绑定 OUTPUT_REDIR,原代码写反。Part3.5. Execute (),fork + execvp + dup2 重定向
int Execute() { pid_t id=fork(); if(id==0) { int fd=-1; if(redir==INPUT_REDIR) { fd=open(file_name.c_str(),O_RDONLY); dup2(fd,0); // fd复制到标准输入0 close(fd); } // > >> 的输出重定向同理dup2(fd,1)标准输出 execvp(argv[0],argv); exit(1); } int status=0; pid_t rid=waitpid(id,&status,0); if(rid>0) { lastcode=WEXITSTATUS(status); } return 0; }系统调用拆解:
fork():复制父进程,得到子进程,子进程复制父进程文件描述符表;- 子进程内部:
open打开目标文件,dup2(oldfd, newfd)把文件描述符重定向到,dup2 会关闭 newfd 原有文件;execvp:在子进程加载新程序,替换子进程地址空间;execvp 成功不返回;失败才继续往下执行 exit (1);- 父进程调用
waitpid阻塞等待子进程结束,拿到子进程退出状态,WEXITSTATUS提取退出码存入lastcode(对应$?)。注意:重定向逻辑只写在外部命令分支,原代码 echo 内建命令没有处理重定向,
echo hello > a.txt不会生效。Part3.6. 内建命令实现
Part3.6.1. cd
bool Cd() { if(argc==1) // cd不带参数,切home { PREVPATH=getcwd(prevpath_buf,sizeof(prevpath_buf)); chdir(home.c_str()); } else { std::string where=argv[1]; if(where=="-") // cd - 切回上一次目录 { chdir(PREVPATH); } } }核心:
chdir修改当前进程工作目录,同时保存上一次路径 PREVPATH,实现cd -。
Part3.6.2. echo
支持
echo $?打印上一条退出码,echo $PATH读取环境变量。限制:仅支持单个参数,
echo a b c只会打印第一个参数。
Part3.6.3. export、alias
- export:维护自定义环境变量数组 envs,调用 putenv 注册环境变量;
- alias:unordered_map 存储别名映射;
CheckAlias在解析完命令后,查找别名,如果命中,重新解析别名对应的字符串执行。Part3.7. 主循环 main
while(true) { PrintCommandPrompt(); char commandline[COMMANDLINE_SIZE]; if(!GetCommandline(commandline,sizeof(commandline))) continue; CheckRedir(commandline); if(!CommandlineParse(commandline)) continue; if(CheckAndExecBuiltin()) continue; if(CheckAlias(commandline)) continue; Execute(); }标准事件循环:打印提示符→读命令→解析重定向→分割参数→优先执行内建命令→别名替换→fork 执行外部命令。
Part4. 结语
这篇文章我们知道了自定义myshell的实现,接下来,小编还会带来更多Linux相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!
最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~


403

被折叠的 条评论
为什么被折叠?



