news 2026/6/24 12:39:03

【Linux】进程控制(四)—— 手搓自主shell

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux】进程控制(四)—— 手搓自主shell

学习Linux到目前为止,我们都知道命令是由shell执行的,但是具体如何执行的我们看不到,因此我们今天来自己写一个shell来执行我们的指令,让大家对shell的底层有一个进阶的理解,文章的最后会给出完整代码喔~

目录

一、打印命令行提示符

二、获取键盘输入

三、解析字符串

四、执行指令

五、增加内建命令

1.cd 路径的改变

2.echo 退出码

六、总结与源码


一、打印命令行提示符

知识前置:shell本质上是一个死循环,因为要不断地处理一条又一条指令,因此编写的shell功能全部都要放进一个死循环中,直到我们主动退出才结束

命令行提示符的格式为 [用户名@主机名 路径],因此要打印出来就必须获取这三个数据,很显然它们都属于环境变量,分别对应USER、HOSTNAME和PWD,要获取环境变量,用getenv函数即可,为了方便管理,我们将打印命令行提示符封装成一个函数

//1.打印命令行提示符 PrintCommandLine();
void PrintCommandLine() { printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetPwd()); //用户名@主机名 当前路径 }
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"); if(pwd == NULL) { return "None"; } return pwd; }

至此命令行提示符的打印就完成了,但是shell本质是一个死循环,它还得需要一个等待我们输入指令的功能,否则就会一直打印,导致满屏的命令行提示符

二、获取键盘输入

键盘输入本质就是输入一个字符串,需要用一个字符数组来接收

#define MAXSIZE 128 //... char command_line[MAXSIZE] = {0};

对于指令的输入我们通常存在两种情况,一种是输入指令后回车,一种是啥也不输入直接回车,因此,要获取键盘输入,我们需要用到系统调用 fgets ,啥也不输入的时候返回NULL, 我们依旧对获取键盘输入的功能封装一个函数,且函数的返回值为输入的指令字符串长度,当返回0的时候,命中我们刚刚说的第二种情况,直接continue即可,否则我们尝试打印出刚刚输入的指令做一个验证测试,看看是否输出的与我们输入的一致

但同时要注意的是,无论哪一种情况,我们都要至少输入一次回车键,回车键相当于换行符\n,因此为了保证字符串和输出的正确性,我们需要将最后的换行符改成'\0'

//2.获取键盘输入 if(GetCommand(command_line, sizeof(command_line)) == 0) continue; printf("%s\n", command_line);
int GetCommand(char commandline[], int size) { if(fgets(commandline, size, stdin) == NULL) return 0; //用户输入的时候,至少会按一次回车\n,改'\0' commandline[strlen(commandline)-1] = '\0'; return strlen(commandline); }

运行结果如下:

三、解析字符串

前面我们输入进去的指令是一整个字符串,我们要把它们拆分(“ls -a -l” -> “ls” “-a” "-l" ),并放入命令行参数表中

对于字符串的拆分,C语言中有一个封装好的函数,strtok

第一个参数为要拆分的字符串,第二个参数为拆分符号,遇到该符号就进行拆分,对于同一个字符串的第二次拆分,则将第一个参数设为NULL,否则会一直拆分第一个而后面的不拆分,具体代码演示如下:

#include<stdio.h> #include<string.h> int main() { char str[] = "aaa bbb ccc ddd"; const char* sep = " "; char *p = strtok(str, sep); printf("%s\n", p); while(p) { p = strtok(NULL, sep); if(p == NULL) { break; } printf("%s\n",p); } return 0; }

拆分的字符串,我们要放到全局的环境变量表中,这是shell内部要维护的第一张表,同时设置一下切割分隔符

#define MAXARGS 32 //shell内部维护的第一张表:命令行参数表 char *gargv[MAXARGS]; int gargc = 0; const char *sep = " ";

将解析字符串封装函数

//3.解析字符串 ParseCommand(command_line);
int ParseCommand(char commandline[]) { //输入新的指令要重置命令行参数表 gargc = 0; memset(gargv, 0, sizeof(gargv)); //分割字符串 gargv[0] = strtok(commandline, sep); while((gargv[++gargc] = strtok(NULL, sep))); //打印测试 printf("gargc: %d\n", gargc); int i = 0; for(; gargv[i]; i++) printf("gargv[%d]: %s\n", i, gargv[i]); return 0; }

我们打印出分割结果测试一下效果

四、执行指令

前面我们完成了指令的输入和解析,还差一个执行,要执行指令,就需要fork子进程来进行程序替换

//4.执行指令 ExcuteCommand();
int ExcuteCommand() { pid_t id = fork(); if(id < 0) return -1; else if(id == 0) { //子进程 程序替换 execvp(gargv[0], gargv); exit(1); } else{ //父进程 int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { printf("wait child process success!\n"); } } }

有了命令行参数表gargv,我们就可以用程序替换函数execvp了,对于程序替换有问题的可以回看博主的文章《进程程序替换》

【Linux】进程控制(三)——进程程序替换-CSDN博客

我们来看看运行结果

结果如我们所料,我们把所有的测试打印全部注释再试试看

到这里为止,shell的基本框架搭成了

五、增加内建命令

刚刚我们输入的指令都如期由子进程替换执行成功了,但是我们来看下面这种情况

这里演示了两种无法成功执行的情况,echo $?是输出上一个程序执行结束的退出码,这里没有打印出来;cd ..是返回上一个目录,但是我们用pwd查看,却发现路径没有变化。

原因就是它们都属于内建命令,内建命令的特点是由Shell自身解析执行,不需要创建新进程再替换,接下来一一解决这两个问题

执行命令前先写一个函数判断是否是内建命令,如果是则直接执行并返回1,如果不是则返回0,并创建子进程替换执行命令

1.cd 路径的改变

要改变路径,可以使用chdir函数

直接将gargv[1]放入参数即可,因为gargv[0]是cd,后面一个必跟路径

//4.执行指令 if(CheckBuiltinExcute() > 0) continue; ExcuteCommand();
int CheckBuiltinExcute() { if(strcmp(gargv[0], "cd") == 0) { //内建命令 if(gargc == 2) { //新的目标路径 chdir(gargv[1]); } return 1; } return 0; }

试一下执行结果

可以发现路径改变了,但是为什么命令行提示符的路径却没有改变呢?

回看我们之前获取路径的函数

const char *GetPwd() { char *pwd = getenv("PWD"); if(pwd == NULL) { return "None"; } return pwd; }

会发现我们获取的是环境变量的PWD记录,但是环境变量的这个值是静态的,即使我们用chdir切换了目录但是没更新PWD环境变量,它依旧会返回旧路径,不准

要实时改变这个路径,就不能依赖环境变量,而需要一个能直接与内核交互的系统调用 getcwd!

这个系统调用的第一个参数属于典型的输出型参数,我们提供数组参数,它会将实时路径给我传递到数组中,那我们就创建一个接收的数组

//我们shell所处的工作路径 char cwd[MAXSIZE];

利用getcwd系统调用优化Getpwd函数

const char *GetPwd() { //char *pwd = getenv("PWD"); char *pwd = getcwd(cwd, sizeof(cwd)); if(pwd == NULL) { return "None"; } return cwd; }

试试优化后的效果

达到了我们的预期

但到这里还剩下最后一个问题,Linux命令行提示符中的路径只保留了最后一个,而我们是直接显示出了一长串的绝对路径,这是过于冗余的,我们接下来就是要解决这个问题,只取它的最后一个“/”后的路径,这里我用c++来实现

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 PrintCommandLine() { printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); //用户名@主机名 当前路径 }

记得对rfindDir的返回值还要加一个c_str(),这是C++为了兼容C语言的打印而设计的接口

来看看成果,为了区分原shell和我们自己写的shell,我们的分隔符是不一样的,前者是$,我的是#

到这里就解决了cd路径的改变问题,所以其实真正的shell还是非常复杂的,我这仅仅是极简版本,还有很多内建命令和其他各种快捷键功能没实现,我们接下来再解决一下echo问题

2.echo 退出码

echo依旧是内建命令,承接实现“cd”的代码继续扩充。

首先先解决获取退出码的问题,先定一个全局变量Last_Exitcode

//上一个进程结束的退出码 int Last_ExitCode = 0;

编写输入指令为echo的情况,并在每个指令执行结束的地方重置退出码

int CheckBuiltinExcute() { if(strcmp(gargv[0], "cd") == 0) { //内建命令 if(gargc == 2) { //新的目标路径 chdir(gargv[1]); Last_ExitCode = 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", Last_ExitCode); } Last_ExitCode = 0; } return 1; } } return 0; }
//父进程 int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { //获取退出码 Last_ExitCode = WEXITSTATUS(status); //printf("wait child process success!\n"); }

来看看实现成果

六、总结与源码

至此,一个简易的shell被我们手搓出来了,独立完成其实非常考验知识储备和代码能力,可作为一个教学意义极高的训练典例,其实除了命令行参数表以外,shell内部还管理了另一张表就是环境变量表,它也承担着非常重要的角色,我将手搓的源码放在下面供大家自行在此基础拓展

#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #include<iostream> #include<string> #define MAXSIZE 128 #define MAXARGS 32 //shell内部维护的第一张表:命令行参数表 char *gargv[MAXARGS]; int gargc = 0; const char *sep = " "; //我们shell所处的工作路径 char cwd[MAXSIZE]; //上一个进程结束的退出码 int Last_ExitCode = 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); } 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 cwd; } void PrintCommandLine() { printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); //用户名@主机名 当前路径 } int GetCommand(char commandline[], int size) { if(fgets(commandline, size, stdin) == NULL) return 0; //用户输入的时候,至少会按一次回车\n,改'\0' commandline[strlen(commandline)-1] = '\0'; return strlen(commandline); } int ParseCommand(char commandline[]) { //输入新的指令要重置命令行参数表 gargc = 0; memset(gargv, 0, sizeof(gargv)); //分割字符串 gargv[0] = strtok(commandline, sep); while((gargv[++gargc] = strtok(NULL, sep))); // printf("gargc: %d\n", gargc); // int i = 0; // for(; gargv[i]; i++) // printf("gargv[%d]: %s\n", i, gargv[i]); return 0; } int CheckBuiltinExcute() { if(strcmp(gargv[0], "cd") == 0) { //内建命令 if(gargc == 2) { //新的目标路径 chdir(gargv[1]); Last_ExitCode = 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", Last_ExitCode); } Last_ExitCode = 0; } return 1; } } return 0; } int ExcuteCommand() { pid_t id = fork(); if(id < 0) return -1; else if(id == 0) { //子进程 程序替换 execvp(gargv[0], gargv); exit(1); } else{ //父进程 int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { //获取退出码 Last_ExitCode = WEXITSTATUS(status); //printf("wait child process success!\n"); } } } int main() { char command_line[MAXSIZE] = {0}; while(1) { //1.打印命令行提示符 PrintCommandLine(); //2.获取键盘输入 if(GetCommand(command_line, sizeof(command_line)) == 0) continue; // printf("%s\n", command_line); //3.解析字符串 ParseCommand(command_line); //4.执行指令 if(CheckBuiltinExcute() > 0) continue; ExcuteCommand(); } return 0; }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/24 12:34:30

线上Java服务凌晨3点告警,我靠这张排查流程图5分钟解决了故障

前言&#xff1a;所有线上故障&#xff0c;都是提前预埋的雷 做Java后端开发的同学&#xff0c;大概率都经历过凌晨线上告警轰炸的绝望&#xff1a;手机钉钉、短信、电话轮番震动&#xff0c;睡眼惺忪打开监控面板&#xff0c;映入眼帘的是一片红通通的告警色块、飙升的CPU、堆…

作者头像 李华
网站建设 2026/6/24 12:33:42

Java 后端转 AI 大模型,这套学习路线评测帮你避坑

为什么 Java 后端是转型 AI 的“潜力股” 很多 Java 开发者在听到"AI 大模型”时&#xff0c;第一反应往往是畏难&#xff1a;数学要补到什么时候&#xff1f;Python 语法会不会和 Java 冲突&#xff1f;是不是得从头去卷算法博士的赛道&#xff1f;其实&#xff0c;这种焦…

作者头像 李华
网站建设 2026/6/24 12:26:35

temu商家端加密分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 有相关问题请第一时间头像私信联系我删…

作者头像 李华
网站建设 2026/6/24 12:24:51

北京离婚财产分割律师联系方式推荐 资深律师曹子燕执业服务指南

在当前的婚姻家事纠纷中&#xff0c;离婚财产分割是占比最高的争议类型之一&#xff0c;涉及房产、存款、股权、投资收益、债权债务等多类资产&#xff0c;不少案情还牵扯到跨区域资产、涉外资产等复杂情况&#xff0c;普通当事人很难仅凭自身能力理清相关法律关系&#xff0c;…

作者头像 李华
网站建设 2026/6/24 12:19:23

LIVE项目解析:基于图像先验与时间一致性的AI视频编辑技术

1. 项目概述&#xff1a;从“修图”到“修视频”的范式跃迁 最近在折腾一个挺有意思的项目&#xff0c;叫 LIVE。这名字起得挺妙&#xff0c;它不是一个直播工具&#xff0c;而是 “Latent Image Video Editing” 的缩写&#xff0c;直译过来是“潜在图像视频编辑”。简单来说&…

作者头像 李华