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
  • 使用数组进行指令存储(缓冲区)
cpp 复制代码
char commandline[1024];//命令行

考虑什么是指令?如何读取指令?

  • Linux 中的大部分指令由 指令 [选项] 构成,在 指令[选择] 间有空格
  • 常规的 scanf 无法正常读取指令,因为空格会触发输入缓冲区刷新
  • 这里主要使用 fgets 逐行读取,可以读取到空格
cpp 复制代码
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 通常用于解析由分隔符(如空格、逗号等)分隔的字符串。

函数原型:

cpp 复制代码
char *strtok(char *str, const char *delim);

参数说明:

  • str :要分割的字符串。在第一次调用时,传入需要分割的原始字符串,之后的调用则传入 NULL,以继续分割上次 strtok 返回的部分。

  • delim :一个包含所有分隔符字符的字符串。例如,如果分隔符是空格和逗号,delim 可以是 " ,"

返回值:

  • 成功:返回指向分割出的子字符串的指针(tokens)。子字符串会从原始字符串中分割出来,并且这个分割后的子字符串是原始字符串的一部分,它们将共享内存空间。

  • 失败 :如果没有更多的子字符串可供提取,strtok 返回 NULL

使用说明:

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

  2. 后续调用 :每次调用时,传入 NULL 以继续分割上次 strtok 返回的部分,直到没有更多的子字符串为止(返回 NULL)。

cpp 复制代码
#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 表对应
  • ppath,可以根据 argv[0](指令),在 PATH 中寻找该程序并替换

当然也可以使用 execve 系统级替换函数

cpp 复制代码
//子进程进行程序替换
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 即可
cpp 复制代码
//特殊处理
//颜色高亮处理,识别是否为 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 移动至指定目录即可(不必再创建子进程进行替换)

cpp 复制代码
//目录间移动处理
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 是一个全局的数组。

cpp 复制代码
strcpy(myenv[count],_argv[1]);
putenv(myenv[count++]);
4.4、重定向

重定向的本质:关闭默认输出/输入流,打开新的文件流,从其中写入/读取数据

重定向的三种情况:

  • echo 字符串 > 文件 向文件中写入数据,写入前会先清空内容
  • echo 字符串 >> 文件 向文件中追加数据,追加前不会先清空内容
  • 可执行程序 < 文件 从文件中读取数据给可执行程序

所以实现重定向的关键在于判断指令中是否含有 >>>< 这三个字符,如果有,就具体问题具体分析,完成重定向

具体实现步骤:

  • 判断字符串中是否含有目标字符,如果有,就置当前位置为 '\0',其后半部分不参与指令分割
  • 后半部分就是文件名,在打开文件时需要使用
  • 根据不同的字符,设置不同的标记位,用于判断打开文件的方式(只写、追加、只读)
  • 判断是否需要进行重定向,如果需要,在子进程创建后,打开目标文件,并调用 dup2 函数进行标准流的替换

open 函数的打开选项

cpp 复制代码
O_RDONLY	//只读
O_WRONLY | O_CREAT | O_TRUNC	//只写
O_WRONLY | O_CREAT | O_APPEND	//追加

标准流交换函数 dup2

cpp 复制代码
//给参数1传打开文件后的文件描述符,给参数2传递待关闭的标准流
//读取:关闭0号流
//写入、追加:关闭1号流
int dup2(int oldfd, int newfd);
cpp 复制代码
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.源码:好好理解

cpp 复制代码
#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;
}
相关推荐
IC 见路不走1 小时前
LeetCode 第91题:解码方法
linux·运维·服务器
翻滚吧键盘2 小时前
查看linux中steam游戏的兼容性
linux·运维·游戏
小能喵2 小时前
Kali Linux Wifi 伪造热点
linux·安全·kali·kali linux
汀沿河2 小时前
8.1 prefix Tunning与Prompt Tunning模型微调方法
linux·运维·服务器·人工智能
zly35002 小时前
centos7 ping127.0.0.1不通
linux·运维·服务器
kikikidult3 小时前
(2025.07)解决——ubuntu20.04系统开机黑屏,左上角光标闪烁
笔记·ubuntu
小哥山水之间3 小时前
基于dropbear实现嵌入式系统ssh服务端与客户端完整交互
linux
ldj20203 小时前
2025 Centos 安装PostgreSQL
linux·postgresql·centos
翻滚吧键盘3 小时前
opensuse tumbleweed上安装显卡驱动
linux
近津薪荼3 小时前
初学者关于数据在内存中的储存的笔记
笔记