Linux进程控制(五)之做一个简易的shell

发布于:2025-04-08 ⋅ 阅读:(17) ⋅ 点赞:(0)

做一个简易的shell

重谈Shell

shell是操作系统的一层外壳程序,帮我们用户执行指令,

获取到指令后,交给操作系统,操作系统执行完后,把执行结果通过shell交给用户。

shell大部分执行命令时,要创建子进程(fork)。

shell/bash本身也是一个进程,执行指令的时候,本质就是自己创建子进程执行的。

考虑下面这个与shell典型的互动:

[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
 PID TTY TIME CMD
 3451 pts/0 00:00:00 bash
 3514 pts/0 00:00:00 ps

用下图的时间轴来表示事件的发生次序。

其中时间从左向右。

shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。

shell从用户读入字符串"ls"。

shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

image-20250312144828753

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。

所以要写一个shell,需要循环以下过程:

  1. 获取命令行

  2. 解析命令行

  3. 建立一个子进程(fork)

  4. 替换子进程(execvp)

  5. 父进程等待子进程退出(wait)

根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。


在继续学习新知识前,我们来思考函数和进程之间的相似性

exec/exit就像call/return

一个C程序有很多函数组成。

一个函数可以调用另外一个函数,同时传递给它一些参数。

被调用的函数执行一定的操作,然后返回一个值。

每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。

这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。

Linux鼓励将这种应用于程序之内的模式扩展到程序之间。

image-20250312144958240

一个C程序可以fork/exec另一个程序,并传给它一些参数。

这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。

调用它的进程可以通过wait(&ret)来获取exit的返回值。


预备知识

命令函的本质就是字符串。先输出后输入。

主机名、用户名和路径都是可以通过系统调用获取的。

但是,我们也可以使用环境变量获得。使用getenv()。

image-20250320201242755

image-20250320201413053

读取一行

image-20250320203406396

Linux系统会打开三个输入输出流(所以我们可以从stdin里面读取输入)

image-20250320203929427

分割字符串strtok

image-20250320215012841

子进程执行普通命令exec*,因为有argv,所以带v、带p。

image-20250320221358808

编写cd指令时,需要用到chdir和getcwd。

chdir是改变当前的工作目录

image-20250320230829763

getcwd是获取当前的工作目录

image-20250320230858527

把一个字符串格式化到指定处

image-20250320231224528

导入环境变量的时候要注意的问题!

image-20250321164750259

代码实现

  #include <stdio.h>
  #include <stdlib.h>
  #include <unistd.h>
  #include <sys/wait.h>
  #include <assert.h>
  #include <string.h>
  #include <sys/types.h>
  
  #define LEFT "["
  #define RIGHT "]"
  #define LALEL "$"
  #define DELIM " \t"
  #define LINE_SIZE 1024
  #define ARGC_MAX 32
  #define EXIT_CODE 33
  
  int last_code=0;
  int quit=0;
  char commandline[LINE_SIZE];
  char* argv[ARGC_MAX];
  char pwd[LINE_SIZE];
  
  //在shell需要维护
  //自定义环境变量表
  //自定义本地变量表(一段缓冲区)
  
  char myenv[LINE_SIZE];
  //也可以malloc空间
  //或者弄成一个二维数组
  const char*getusername()
  {
      return getenv("USER");
  }
  
  const char*gethostname()
  {
      return getenv("HOSTNAME");
  }
  
  void getpwd()
  {
      getcwd(pwd,sizeof(pwd));
  }
  
  void interact(char*cline,int size)
  {
      getpwd();
      printf(LEFT"%s@%s %s"RIGHT""LALEL" ",getusername(),gethostname(),pwd);
      //输入最好不要用scanf,因为它是以空格结尾
      //fgets可以读取一行,读取成功就为读取字符串的地址,失败返回NULL
      char*s=fgets(cline,size,stdin);
      //sizeof是否需要-1呢
      //不需要,-1是防御性编程,防止\0不够放了。后面要用到系统级别的io就要考虑是否-1
      //fgets不可能为null,不输入,按了回车之后,也算是有字符。
      //输入了命令,一敲回车就会换行,但是我们不想要换行。
      assert(s!=NULL);
      //assert在debug的情况下起效果
      (void)s;//抵消编译器的报警
      //"abcd\n\0"
      cline[strlen(cline)-1]='\0';
  }
  int splitstring(char*_argv[],char*cline)
  {
      int i=0;
      _argv[i++]=strtok(cline,DELIM);
      while(_argv[i++]=strtok(NULL,DELIM));
      return i-1;
  }
  
  void normalexcute(char*_argv[])
  {
       pid_t id=fork();
       if(id<0)
       {
           perror("fork fail!");
           return;
       }
       else if(id==0)
       {
          execvp(_argv[0],_argv);
          exit(EXIT_CODE);
          //替换失败我们就可以拿到子进程的退出信息。
          //cd echo等内建命令不能由子进程执行
          //子进程cd 与父进程无关!
       }
       else
       {
           int status=0;
           pid_t rid=waitpid(id,&status,0);
           if(rid==id)
           {
               last_code=WEXITSTATUS(status);                
           }
       }
  }
  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;
       }
       //export也是内建命令
       //如果让子进程执行export,那么子进程的环境变量会添加,
       //但是与父进程无关,export导环境变量是给调用的进程导入。                                                                                                                             
       else if(_argc==2&&strcmp(_argv[0],"export")==0)
       {
           strcpy(myenv,_argv[1]);
           putenv(myenv);
           //只是修改了一个环境变量表的指针,让该指针指向了环境变量。
           //但是环境变量是在argv里面的,所以,下一次argv的就会改变,
           //所以环境变量指针指向的内容也会改变
           //没有直接拷贝到系统的环境变量表中
           //而是把argv里面的信息导入了,argv指向的内容一直都会变化。
           //所以我们需要一个不变的区域。
           return 1;
       }
 	   else if(_argc==2&&strcmp(_argv[0],"echo")==0)
       {
           if(strcmp(_argv[1],"$?")==0)
           {
               printf("%d\n",last_code);
               last_code=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";
           _argv[_argc]=NULL;
       }
       return 0;
  }
  int main()
  {
      while(!quit)
      {
          //1.
          //2.交互问题,获取命令行
          interact(commandline,sizeof(commandline));
  
          //printf("echo :%s\n",commandline);
          //字符串分割 要有每个字串的地址 "ls -a -l" -> "ls" "-a" "-l"
          //保存在agrv(字符串数组里)
          //分割字串strtok 调用一次只能切割一个子串
          
          //3.字符串分割问题,解析命令行
          int argc=splitstring(argv,commandline);
          if(argc==0)continue;
          // for(int i=0;argv[i];i++)
          // {
          //     printf("[%d]:%s\n",i,argv[i]);
          // }
          
          //4.指令的判断
          //判断是否是内建命令,如果是就要让父进程执行相应的指令
          //内建命令本质就是shell内部的一个函数
          int n=buildcommand(argv,argc);
          
          //5.普通命令的执行
          if(!n)normalexcute(argv);
      }
      return 0;
  }

运行结果

image-20250321172332907

image-20250321172400214

image-20250321173002520

结论:

所以,当我们进行登陆的时候,系统就是要启动一个shell进程

我们shell本身的环境变量是从哪里来的?

image-20250321170235622

.bash_profile

image-20250321170525423

.bashrc

image-20250321170617366

/etc/bashrc
我们所用的环境变量都是从配置文件读的。

image-20250321170739023

当用户登陆的时候,shell会读取用户目录下的 .bash_profile 文件,里面保存了导入环境变量的方式!

shell的环境变量最终是从环境里来的。