Linux 学习-模拟实现【简易版bash】
1、bash本质
在模拟实现前,先得了解 bash
的本质
bash
也是一个进程,并且是不断运行中的进程
证明:常显示的命令输入提示符就是 bash
不断打印输出的结果
输入指令后,bash
会创建子进程,并进行程序替换
证明:运行自己写的程序后,可以看到当前进程的 父进程
为 bash
此时可以断定神秘的 bash
就是一个运行中的进程,因为进程间具有独立性,因此可以同时存在多个 bash
,这也是多用户登录 Linux
可以同时使用 bash
的重要原因
系统自带的 bash
是一个庞然大物,我们只需根据其本质,实现一个简易版 bash
就行了
2、需求分析
bash
需要帮我们完成命令解释+程序替换的任务,因此它至少要具备以下功能:
- 接收指令(字符串)
- 对指令进行分割,构成有效信息
- 创建子进程,执行进程替换
- 子进程运行结束后,父进程回收僵尸进程
- 输入特殊指令时的处理
3、核心内容
核心内容主要为 读取
、切割
、替换
这三部分,逐一实现,首先从指令读取开始
3.1、指令读取
读取指令前,首先要清楚待读取命令可能有多长
- 常见命令如
ls -a -l
长度不超过10
- 为了避免极端情况,这里预设命令最大长度为
1024
- 使用数组进行指令存储(缓冲区)
char commandline[1024];//命令行
考虑什么是指令?如何读取指令?
Linux
中的大部分指令由指令 [选项]
构成,在指令
和[选择]
间有空格- 常规的
scanf
无法正常读取指令,因为空格会触发输入缓冲区刷新 - 这里主要使用
fgets
逐行读取,可以读取到空格
void interact(char* cline,int size)//输出命令行{getpwd();printf("[%s@%s%s]# " ,getusrname(),gethostname(),pwd); char *s=fgets(cline,size,stdin);//输入指令,有可能什么也没有输入直接回车 assert(s);(void)s;//”abcd\n\0" cline[strlen(cline)-1]='\0';//原来\n,在输入的时候也会加入到字符串中;checkdir(cline);//检查重定向}
注意: 可能存在读取失败的情况,assert
断言解决;因为 fgets
也会把最后的 '\n'
读进去,为了避免出错,手动置为 '\0';
3.2、指令分割
获得指令后,就需要将指令进行分割
为何要分割指令?
- 程序替换时,需要使用
argv
表,这张表由指令
、选项
、NULL
构成 - 利用指令间的空格进行分割
如何分割指令?
C语言
提供了字符串分割函数strtok
,可以直接使用- 当然也可以手动实现分割
指令分割后呢?
- 将分割好的指令段,依次存入
argv
表中,供后续程序替换使用 argv
表实际为一个指针数组,可以存储字符串
如 command
一样,表 argv
也需要考虑大小,这里设置为 64
,实际使用时也就分割为四五个指令段
strtok
是 C 语言中的一个字符串处理函数,用于将一个字符串分割成多个子字符串(tokens)。该函数定义在string.h
头文件中。strtok
通常用于解析由分隔符(如空格、逗号等)分隔的字符串。函数原型:
char *strtok(char *str, const char *delim);
参数说明:
str
:要分割的字符串。在第一次调用时,传入需要分割的原始字符串,之后的调用则传入NULL
,以继续分割上次strtok
返回的部分。
delim
:一个包含所有分隔符字符的字符串。例如,如果分隔符是空格和逗号,delim
可以是" ,"
。返回值:
成功:返回指向分割出的子字符串的指针(tokens)。子字符串会从原始字符串中分割出来,并且这个分割后的子字符串是原始字符串的一部分,它们将共享内存空间。
失败:如果没有更多的子字符串可供提取,
strtok
返回NULL
。使用说明:
第一次调用:传入待分割的字符串。
后续调用:每次调用时,传入
NULL
以继续分割上次strtok
返回的部分,直到没有更多的子字符串为止(返回NULL
)。
#define DEF_CHAR " " //预设分割项,需为字符串void split(char* argv[ARGV_SIZE], char* ps)
{assert(argv && ps);//调用 C语言 中的 strtok 函数分割字符串int pos = 0;argv[pos++] = strtok(ps, DEF_CHAR); //有空格就分割while(argv[pos++] = strtok(NULL, DEF_CHAR)); //不断分割argv[pos] = NULL; //确保安全
}
注意: 指令分割结束后,需要在添加 argv
表结尾 NULL
3.3、程序替换
获得实际可用的 argv
表后,就可以开始子进程程序替换操作了
这里使用的是函数 execvp
,理由:
v
表示vector
,正好和我们的argv
表对应p
为path
,可以根据argv[0]
(指令),在PATH
中寻找该程序并替换
当然也可以使用 execve
系统级替换函数
//子进程进行程序替换
pid_t id = fork();
if(id == 0)
{//直接执行程序替换,这里使用 execvpexecvp(argv[0], argv);exit(168); //替换失败后返回
}
注意: 程序替换成功后,exit(168)
语句不会执行.
4、特殊情况处理
对特殊情况进行处理,使 myBash
更加完善
4.1、ls 显示高亮
系统中的 bash
在面对 ls
等文件显示指令时,不仅会显示内容,还会将特殊文件做颜色高亮处理,比如在我的环境下,可执行文件显示为绿色
实现原理
- 在指令结尾加上
--color=auto
语句,即可实现高亮处理这个问题很简单,在指令分割结束后,判断是否为
ls
,如果是,就在argv
表后尾插入语句--color=auto
即可
//特殊处理
//颜色高亮处理,识别是否为 ls 指令
if(strcmp(argv[0], "ls") == 0)
{int pos = 0;while(argv[pos++]); //找到尾argv[pos - 1] = (char*)"--color=auto"; //添加此字段argv[pos] = NULL; //结新尾
}
注意:
- 因为
argv
表中的元素类型为char*
,所以在尾插语句时,需要进行类型转换 - 尾插语句后,需要再次添加结尾,确保安全
4.2、内建命令
内建命令是比较特殊的命令,不同于普通命令直接进行程序替换,内建命令需要进行特殊处理,比如 cd
命令调用系统级接口 chdir
让 父进程(myBash)
进行目录间的移动
5.3、cd
首先实现不同目录间的切换
切换的本质:令当前 bash
移动至另一个目录下,不能直接使用 子进程
,因为需要移动的是 父进程(bash)
对于当前的 myBash
来说,cd
没有丝毫效果,因为此时 指令会被拆分后交给子进程处理,这个方向本身就是错误的
特殊情况特殊处理,同 ls
高亮一样,对指令进行识别,如果识别到 cd
命令,就直接调用 chdir
函数令当前进程 myBash
移动至指定目录即可(不必再创建子进程进行替换)
//目录间移动处理
if(strcmp(argv[0], "cd") == 0)
{//直接调用接口,然后 continue 不再执行后续代码if(strcmp(argv[1], "~") == 0)chdir("/home"); //回到家目录else if(strcmp(argv[1], "-") == 0)chdir(getenv("OLDPWD"));else if(argv[1])chdir(argv[1]); //argv[1] 中就是路径continue; //终止此次循环
}
4.3、export
当添加环境变量时,环境变量具有全局属性,需要持久存在,所以要定义一个全局的数组存储环境变量的值。myenv 是一个全局的数组。
strcpy(myenv[count],_argv[1]);
putenv(myenv[count++]);
4.4、重定向
重定向的本质:关闭默认输出/输入流,打开新的文件流,从其中写入/读取数据
重定向的三种情况:
echo 字符串 > 文件
向文件中写入数据,写入前会先清空内容echo 字符串 >> 文件
向文件中追加数据,追加前不会先清空内容可执行程序 < 文件
从文件中读取数据给可执行程序
所以实现重定向的关键在于判断指令中是否含有 >
、>>
、<
这三个字符,如果有,就具体问题具体分析,完成重定向
具体实现步骤:
- 判断字符串中是否含有目标字符,如果有,就置当前位置为
'\0‘
,其后半部分不参与指令分割 - 后半部分就是文件名,在打开文件时需要使用
- 根据不同的字符,设置不同的标记位,用于判断打开文件的方式(只写、追加、只读)
- 判断是否需要进行重定向,如果需要,在子进程创建后,打开目标文件,并调用
dup2
函数进行标准流的替换
open
函数的打开选项
O_RDONLY //只读
O_WRONLY | O_CREAT | O_TRUNC //只写
O_WRONLY | O_CREAT | O_APPEND //追加
标准流交换函数 dup2
//给参数1传打开文件后的文件描述符,给参数2传递待关闭的标准流
//读取:关闭0号流
//写入、追加:关闭1号流
int dup2(int oldfd, int newfd);
void checkdir(char * cmd)48 {49 char *pos =cmd;50 while(*pos)51 {52 if(*pos=='>')53 {54 if(*(pos+1)=='>')//'>>'55 {56 *(pos++)='\0';57 *(pos++)='\0';58 while(*pos==' ') pos++;59 60 rdirfilename=pos;61 rdir = APPEND_RDIR;62 break;63 }64 else //'>'65 {66 *(pos++)='\0'; 67 while(*pos==' ') pos++;68 rdirfilename=pos;69 rdir=OUT_RDIR;70 }72 }73 else if(*pos=='<')74 {75 *pos='\0';76 pos++;77 while(*pos==' ') pos++;78 79 rdirfilename=pos;80 rdir=IN_RDIR;81 break;82 }83 else{}84 85 pos++;86 } 87 88 }
5.源码:好好理解
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>#include <sys/stat.h>#include <fcntl.h>extern char** environ;#define NONE -1
#define IN_RDIR 0 //输入
#define OUT_RDIR 1//stdout
#define APPEND_RDIR 2//stderrchar commandline[1024];//命令行
char *argv[32];//参数表
char pwd[1024];//路径长
char myenv[10][10];//环境变量表
int count=0;
int lastcode=0;//退出码char * rdirfilename=NULL; //重定向的文件
int rdir =NONE;const char* getusrname()
{ const char* str=getenv("USER");return str;
}const char* gethostname()
{return getenv("HOSTNAME");
}void getpwd()
{getcwd(pwd,sizeof(pwd));//是一个接口函数,将路径写到pwd里面
}void checkdir(char * cmd)
{char *pos =cmd;while(*pos){if(*pos=='>'){if(*(pos+1)=='>')//'>>'{*(pos++)='\0';*(pos++)='\0';while(*pos==' ') pos++;rdirfilename=pos;rdir = APPEND_RDIR;break;}else //'>'{*(pos++)='\0'; while(*pos==' ') pos++;rdirfilename=pos;rdir=OUT_RDIR;}}else if(*pos=='<'){*pos='\0';pos++;while(*pos==' ') pos++;rdirfilename=pos;rdir=IN_RDIR;break;}else{}pos++;} }void interact(char* cline,int size)//输出命令行
{getpwd();printf("[%s@%s%s]# " ,getusrname(),gethostname(),pwd);char *s=fgets(cline,size,stdin);//输入指令,有可能什么也没有输入直接回车assert(s);(void)s;//”abcd\n\0" cline[strlen(cline)-1]='\0';//原来\n,在输入的时候也会加入到字符串中;checkdir(cline);//检查重定向
}int splitstring(char * cline,char *_argv[])
{int i=0;argv[i++]=strtok(cline," ");//字符串分割while(_argv[i++]=strtok(NULL," "));//如果截取失败就会返回NULL,正好是参数表尾;return i-1; //含回指令的参数个数。NULL不算
}int buildCommand(char*_argv[],int _argc)
{if(_argc==2&&strcmp(_argv[0],"cd")==0){chdir(argv[1]);//改变当前进程的路径,但是并不影响环境变量当中的路径getpwd();sprintf(getenv("PWD"),"%s",pwd);return 1;}else if(_argc==2&&strcmp(_argv[0],"export")==0){strcpy(myenv[count],_argv[1]);putenv(myenv[count++]);return 1;}else if(_argc==2&&strcmp(_argv[0],"echo")==0){if(strcmp(_argv[1],"$?")==0){printf("%d\n",lastcode);lastcode=0;//查看完后置为0;}else if(*_argv[1]=='$'){char* val=getenv(_argv[1]+1);if(val) printf("%s\n",val);}else printf("%s\n",_argv[1]);return 1;}if(strcmp(_argv[0],"ls")==0){ _argv[_argc++]="--color=auto";_argv[_argc]=NULL;// return 1;}return 0;
}void NormalExcute(char* _argv[]){pid_t id=fork();if(id<0){perror("fork");return ;}else if(id==0){int fd=0;if(rdir==IN_RDIR){fd=open(rdirfilename,O_RDONLY);dup2(fd ,0);}else if(rdir==OUT_RDIR){fd=open(rdirfilename,O_CREAT|O_WRONLY|O_TRUNC,0666);dup2(fd,1);}else if(rdir==APPEND_RDIR){fd=open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND,0666);dup2(fd,1);}execvp(_argv[0],_argv);exit(1);}else {int status=0;pid_t rid=waitpid(id,&status,0);//正常含回子进程的PIDif(rid==id){lastcode=WEXITSTATUS(status);}}
}int main()
{while(1){rdirfilename=NULL;rdir=NONE; interact(commandline,sizeof(commandline));int argc=splitstring(commandline,argv);if(argc==0) continue;//for(int i=0;i<argc;i++) printf("argv[%d]:%s\n",i,argv[i]);int n=buildCommand(argv,argc);if(!n) NormalExcute(argv);}return 0;
}
相关文章:

Linux 学习-模拟实现【简易版bash】
1、bash本质 在模拟实现前,先得了解 bash 的本质 bash 也是一个进程,并且是不断运行中的进程 证明:常显示的命令输入提示符就是 bash 不断打印输出的结果 输入指令后,bash 会创建子进程,并进行程序替换 证明&#x…...

【中国・珠海】2025 物联网与边缘计算国际研讨会(IoTEC2025)盛大来袭!
2025 物联网与边缘计算国际研讨会(IoTEC2025)盛大来袭! 科技浪潮奔涌向前,物联网与边缘计算已成为驱动各行业变革的核心力量。在此背景下,2025 物联网与边缘计算国际研讨会(IoTEC2025)即将震撼…...
企业级安全实践:SSL/TLS 加密与权限管理(二)
案例分析:成功与失败的经验教训 成功案例分析 以一家知名电商企业 ABC 为例,该企业每天处理数百万笔订单,涉及大量用户的个人信息、支付信息和商品数据。在网络安全建设方面,ABC 电商高度重视 SSL/TLS 加密与权限管理。 在 SSL…...
Java面试:从Spring Boot到分布式系统的技术探讨
场景一:电商平台的订单处理 面试官: “谢先生,假设我们在一个电商平台工作,你将如何使用Spring Boot构建一个订单处理服务?” 谢飞机: “这个简单,我会使用Spring Boot快速启动项目࿰…...
NodeJS全栈开发面试题讲解——P7 DevOps 与部署和跨域等
✅ 7.1 如何部署 Node.js 项目到生产环境?用过哪些工具? 面试官您好,我部署 Node.js 项目通常分为 构建 → 上传 → 启动服务 三步,常用工具包括 PM2、Nginx、Docker、Git Hooks、CI/CD 工具。 🛠️ 主要部署步骤&…...

中国高分辨率高质量地面CO数据集(2013-2023)
时间分辨率:日空间分辨率:1km - 10km共享方式:开放获取数据大小:9.83 GB数据时间范围:2013-01-01 — 2023-12-31元数据更新时间:2024-08-19 数据集摘要 ChinaHighCO数据集是中国高分辨率高质量近地表空气污…...

GO——内存逃逸分析
一、可能导致逃逸的5中情况 package mainimport "fmt"func main() {f1()f2()f3()f4()f5() }type animal interface {run() }type dog struct{}func (d *dog) run() {fmt.Println("狗在跑") }// 指针、map、切片为返回值的会发生内存逃逸 func f1() (*int,…...

MinVerse 3D触觉鼠标的技术原理与创新解析
MinVerse3D触觉鼠标通过三维交互和触觉反馈技术,彻底颠覆了传统二维鼠标的操作方式。用户在操作虚拟物体时,可以真实感知表面质感、重量和阻力。这种技术不仅为数字环境注入了深度与临场感,还在3D设计、游戏开发和工程仿真等领域展现了广泛潜…...
Spring Boot整活指南:从Helo World到“真香”定律
📌 一、Spring Boot的"真香"本质(不是996的福报) 你以为Spring Boot只是个简化配置的工具?Too young!它其实是程序员的摸鱼加速器。 经典场景还原: 产品经理:“这个…...
Python-Selenium报错截图
报错截图设计方案: 功能:截图层主要用来存放selenium运行时的报错截图信息 1. 截图路径管理 分层存储:在项目根目录下创建 screenshots 文件夹,并按日期进一步分类(如 20250601)。命名规范࿱…...
数论——质数和合数及求质数
质数和合数及求质数 一个大于 1 的自然数,除了 1 和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。其中,质数又称素数。有的资料用的词不同,但质数和素数其实是一回事。 规定 1 既不是质数也不是合数。 …...
nc 命令示例
nc -zv 实用示例 示例 1:测试单个 TCP 端口(最常见) 目标: 检查主机 webserver.example.com 上的 80 端口 (HTTP) 是否开放。 nc -zv webserver.example.com 80成功输出: Connection to webserver.example.com (19…...

乾元通渠道商中标青海省自然灾害应急能力提升工程基层防灾项目
近日,乾元通渠道商中标青海省自然灾害应急能力提升工程基层防灾项目,乾元通作为设备厂家,为项目提供通信指挥类装备(多链路聚合设备)QYT-X1。 青岛乾元通数码科技有限公司作为国家应急产业企业,深耕于数据调…...
Ubuntu取消开机用户自动登录
注:配置前请先设置登录密码,不同显示管理器配置方法不同,可用命令查看:cat /etc/X11/default-display-manager 一、LightDM 显示管理器,关闭 Ubuntu 系统用户自动登录 查找自动登录配置文件,可以看到类似 a…...
用 Spring Boot 静态资源映射 vs 用 Nginx 提供静态文件服务总结
【1】Spring Boot 静态资源映射 vs 用 Nginx 提供静态文件服务 ✅ 简短回答: 在性能、并发能力、缓存控制、安全性等方面,Nginx 完胜。 所以:如果你只是提供静态文件下载(如图片、PDF、Excel 等),强烈推荐…...

openssl-aes-ctr使用openmp加速
openssl-aes-ctr使用openmp加速 openssl-aes-ctropenmp omp for openssl-aes-ctr 本文采用openssl-1.1.1w进行开发验证开发;因为aes-ctr加解密模式中,不依赖与上一个模块的加/解密的内容,所以对于aes-ctr加解密模式是比较适合进行并行加速的…...

PHP+MySQL开发语言 在线下单订水送水小程序源码及搭建指南
随着互联网技术的不断发展,在线下单订水送水服务为人们所需要。分享一款 PHP 和 MySQL 搭建一个功能完善的在线订水送水小程序源码及搭建教程。这个系统将包含用户端和管理端两部分,用户可以在线下单、查询订单状态,管理员可以处理订单、管理…...

计算机网络第1章(上):网络组成与三种交换方式全解析
目录 一、计算机网络的概念二、计算机网络的组成和功能2.1 计算机网络的组成2.2 计算机网络的功能 三、电路交换、报文交换、分组交换3.1 电路交换(Circuit Switching)3.2 报文交换(Message Switching)3.3 分组交换(Pa…...

Android studio进阶开发(七)---做一个完整的登录系统(前后端连接)
我们已经讲过了okhttp和登录系统的使用,我们今天做一个完整的登录系统,后端用springmybatis去做 数据库内容 -- 创建学生信息表 CREATE TABLE student_info (id SERIAL PRIMARY KEY, -- 添加自增主键name VARCHAR(255) NOT NULL,number INT NOT NULL,…...

计算机网络第1章(下):网络性能指标与分层模型全面解析
目录 一、计算机网络的性能指标1.1 性能指标1:速率1.2 性能指标2:带宽1.3 性能指标3:吞吐量1.4 性能指标4:时延1.5 性能指标5:时延带宽积1.6 性能指标6:往返时延1.7 性能指标7:信道利用率 二、计…...

恶意软件清理工具,让Mac电脑安全更简单
你的Mac最近是不是开始表演"电子迷惑行为"?浏览器主页突然变成澳门赌场,风扇转得比直升机螺旋桨还猛......恭喜你!可能中奖获得"恶意软件大礼包"!别慌,今天就教你用恶意软件清理工具化身数字特工…...

HackMyVM-Jabita
信息搜集 主机发现 ┌──(kali㉿kali)-[~] └─$ nmap -sn 192.168.43.0/24 Starting Nmap 7.95 ( https://nmap.org ) at 2025-06-01 05:20 EDT Nmap scan report for 192.168.43.1 Host is up (0.020s latency). MAC Address: C6:45:66:05:91:88 (Unknown) Nmap scan repo…...

112 Gbps 及以上串行链路的有效链路均衡
通道均衡已成为当今高速串行链路的关键机制。目前有许多均衡方案,例如发射机加重均衡、接收机CTLE(连续时间线性均衡器)、FFE(前馈均衡器)、DFE(判决反馈均衡器)和FEC(前向纠错&…...
Mac 版不能连接华为 GaussDB 吗?我看 Windows 版可以连接?
🧑💻 GaussDB 用户 Mac 版不能连接华为 GaussDB 吗?我看Windows 版可以连接。 🧑🔧 官方技术中心 由于 GaussDB 数据库本身未支持 macOS 系统,所以在 macOS 上的 Navicat 中也未支持该数据库。 &…...

Python-13(永久存储)
创建并打开文件 open(file,mode)函数 该函数用于打开一个文件并返回对应的文件对象。 file参数指定的是文件路径和文件名,如果没有添加路径,那么默认将文件创建在python的主文件夹里面。mode参数指定的是打开的模式,r表示读取(…...
《关于有序推动绿电直连发展有关事项的通知》核心内容
《关于有序推动绿电直连发展有关事项的通知》(发改能源〔2025〕650 号)由国家发展改革委和国家能源局于 2025 年 5 月 21 日联合印发,旨在明确绿电直连项目的适用范围、规划引导、运行管理、交易与价格机制以及组织保障等内容。 适用范围与原…...
数据结构-排序(1)
一,排序的基本概念 1.排序的定义 核心概念: 给定一个包含 n 个元素的序列 (R1, R2, ..., Rn) 和一个关键码 Ki(通常是记录 Ri 的一个属性),排序的目标是找到一个排列 (p1, p2, ..., pn),使得关键码序列 (K…...

记录一次session安装应用recyclerview更新数据的bug
首先抛出异常日志,在 先说结论:因为session安装监听是在点击事件里面,所以会保留旧的对象数据 直接上代码,原有的逻辑是点击时执行session安装,并注册监听回调 private fun installApk(position: Int) {val packageIns…...

大数据-274 Spark MLib - 基础介绍 机器学习算法 剪枝 后剪枝 ID3 C4.5 CART
点一下关注吧!!!非常感谢!!持续更新!!! 大模型篇章已经开始! 目前已经更新到了第 22 篇:大语言模型 22 - MCP 自动操作 FigmaCursor 自动设计原型 Java篇开…...

力扣面试150题--二叉树的锯齿形层序遍历
Day 56 题目描述 思路 锯齿形就是一层是从左向右,一层是从右向左,那么我们可以分析样例,对于第奇数层是从左向右,第偶数层是从右向左,于是可以采取一个计数器,采取链表方式,从左向右就是正常插…...