Linux 学习-模拟实现【简易版bash】

news2025/6/3 15:11:02

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

使用说明:

  1. 第一次调用:传入待分割的字符串。

  2. 后续调用:每次调用时,传入 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)
{
  //直接执行程序替换,这里使用 execvp
  execvp(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//stderr


char 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);
  
    //正常含回子进程的PID
  
    if(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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2395444.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【中国・珠海】2025 物联网与边缘计算国际研讨会(IoTEC2025)盛大来袭!

2025 物联网与边缘计算国际研讨会&#xff08;IoTEC2025&#xff09;盛大来袭&#xff01; 科技浪潮奔涌向前&#xff0c;物联网与边缘计算已成为驱动各行业变革的核心力量。在此背景下&#xff0c;2025 物联网与边缘计算国际研讨会&#xff08;IoTEC2025&#xff09;即将震撼…

中国高分辨率高质量地面CO数据集(2013-2023)

时间分辨率&#xff1a;日空间分辨率&#xff1a;1km - 10km共享方式&#xff1a;开放获取数据大小&#xff1a;9.83 GB数据时间范围&#xff1a;2013-01-01 — 2023-12-31元数据更新时间&#xff1a;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触觉鼠标通过三维交互和触觉反馈技术&#xff0c;彻底颠覆了传统二维鼠标的操作方式。用户在操作虚拟物体时&#xff0c;可以真实感知表面质感、重量和阻力。这种技术不仅为数字环境注入了深度与临场感&#xff0c;还在3D设计、游戏开发和工程仿真等领域展现了广泛潜…

乾元通渠道商中标青海省自然灾害应急能力提升工程基层防灾项目

近日&#xff0c;乾元通渠道商中标青海省自然灾害应急能力提升工程基层防灾项目&#xff0c;乾元通作为设备厂家&#xff0c;为项目提供通信指挥类装备&#xff08;多链路聚合设备&#xff09;QYT-X1。 青岛乾元通数码科技有限公司作为国家应急产业企业&#xff0c;深耕于数据调…

openssl-aes-ctr使用openmp加速

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

PHP+MySQL开发语言 在线下单订水送水小程序源码及搭建指南

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

计算机网络第1章(上):网络组成与三种交换方式全解析

目录 一、计算机网络的概念二、计算机网络的组成和功能2.1 计算机网络的组成2.2 计算机网络的功能 三、电路交换、报文交换、分组交换3.1 电路交换&#xff08;Circuit Switching&#xff09;3.2 报文交换&#xff08;Message Switching&#xff09;3.3 分组交换&#xff08;Pa…

Android studio进阶开发(七)---做一个完整的登录系统(前后端连接)

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

计算机网络第1章(下):网络性能指标与分层模型全面解析

目录 一、计算机网络的性能指标1.1 性能指标1&#xff1a;速率1.2 性能指标2&#xff1a;带宽1.3 性能指标3&#xff1a;吞吐量1.4 性能指标4&#xff1a;时延1.5 性能指标5&#xff1a;时延带宽积1.6 性能指标6&#xff1a;往返时延1.7 性能指标7&#xff1a;信道利用率 二、计…

恶意软件清理工具,让Mac电脑安全更简单

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

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 及以上串行链路的有效链路均衡

通道均衡已成为当今高速串行链路的关键机制。目前有许多均衡方案&#xff0c;例如发射机加重均衡、接收机CTLE&#xff08;连续时间线性均衡器&#xff09;、FFE&#xff08;前馈均衡器&#xff09;、DFE&#xff08;判决反馈均衡器&#xff09;和FEC&#xff08;前向纠错&…

Python-13(永久存储)

创建并打开文件 open(file,mode)函数 该函数用于打开一个文件并返回对应的文件对象。 file参数指定的是文件路径和文件名&#xff0c;如果没有添加路径&#xff0c;那么默认将文件创建在python的主文件夹里面。mode参数指定的是打开的模式&#xff0c;r表示读取&#xff08;…

记录一次session安装应用recyclerview更新数据的bug

首先抛出异常日志&#xff0c;在 先说结论&#xff1a;因为session安装监听是在点击事件里面&#xff0c;所以会保留旧的对象数据 直接上代码&#xff0c;原有的逻辑是点击时执行session安装&#xff0c;并注册监听回调 private fun installApk(position: Int) {val packageIns…

大数据-274 Spark MLib - 基础介绍 机器学习算法 剪枝 后剪枝 ID3 C4.5 CART

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大模型篇章已经开始&#xff01; 目前已经更新到了第 22 篇&#xff1a;大语言模型 22 - MCP 自动操作 FigmaCursor 自动设计原型 Java篇开…

力扣面试150题--二叉树的锯齿形层序遍历

Day 56 题目描述 思路 锯齿形就是一层是从左向右&#xff0c;一层是从右向左&#xff0c;那么我们可以分析样例&#xff0c;对于第奇数层是从左向右&#xff0c;第偶数层是从右向左&#xff0c;于是可以采取一个计数器&#xff0c;采取链表方式&#xff0c;从左向右就是正常插…

如何在 CentOS / RHEL 上修改 MySQL 默认数据目录 ?

MySQL 是一个广泛使用的开源关系数据库管理系统(RDBMS)&#xff0c;为无数的 web 应用程序和服务提供支持。默认情况下&#xff0c;MySQL 将其数据存储在预定义的目录中&#xff0c;这可能并不总是适合您的需求。您可能希望将数据目录移动到另一个位置以获得更好的性能和安全性…

简历制作要精而不简

不得不说&#xff0c;不管是春招&#xff0c;还是秋招&#xff0c;我们在求职时&#xff0c;第一步便是制作一份简历。不得不承认&#xff0c;好的简历&#xff0c;就像一块敲门砖&#xff0c;能让面试官眼前一亮&#xff0c;让应聘成功的概率增添一分。 对于一个初次求职者来…

SPA-RL:通过Stepwise Progress Attribution训练LLM智能体

SPA-RL&#xff1a;通过Stepwise Progress Attribution训练LLM智能体 在大语言模型&#xff08;LLM&#xff09;驱动智能体发展的浪潮中&#xff0c;强化学习&#xff08;RL&#xff09;面临着延迟奖励这一关键挑战。本文提出的SPA-RL框架&#xff0c;通过创新的分步进度归因机…