Linux实践 - 命令行解释器 简易版

在这里插入图片描述

前言

本文将根据进程创建fork()、进程替换exec系列函数、进程等待waitpid()实现一个简单的命令行解释器。


解决的问题

为什么shell要以子进程的方式执行我们的命令?

shell也是一个进程,shell会提取用户在命令行输入的内容以空格字符作为分隔符切割成一个个的子串,然后执行exec程序替换函数。如果没有子进程。shell进程本身会被替换,shell也就结束运行了,但是我们需要shell一直运行,持续解析命令行的,所以shell通过fork创建子进程,让子进程执行程序替换,父进程shell然后等待子进程退出,之后shell将再次等待命令行的输入。

为什么直接使用程序名ls,而不是路径/usr/bin/ls?

shell以fork子进程的方式,通过exec替换子进程执行其他程序。子进程继承了shell的环境变量,使用exec函数时不需要制定替换程序的路径,使用程序名即可,操作系统会在PATH包含的路径下自动寻找。
echo的问题 : 内建命令

头文件包含

#include<stdio.h>
#include<stdlib.h>// exec系列替换函数
#include<string.h>// 字符串函数
#include<assert.h>// 断言判断
#include<unistd.h>// fork创建子进程
#include<sys/types.h>// 进程等待
#include<sys/wait.h>// 进程等待

命令行提示符

首先我们登录shell时左侧会提示我们进行输入的提示符,包含了当前登录的用户名、主机名和当前所在目录。
外
我们仿照xshell的写法即可:

printf("[用户名@主机名 路径]# ");

外

接受用户命令行输入

定义接收用户输入的长度为NUM的字符数组commandLine;

#define NUM 1024    
char commandLine[NUM];

我们需要接受用户的一行输入,这里使用fgets函数。
fgets函数声明

char *fgets(char *s, int size, FILE *stream);

从标准输入stdin中读取最多数组长度-1个字符到commandLine数组中。
空出来的一个位置是为了放’\0’,防止出可能的越界问题。

char* s = fgets(commandLine, sizeof(commandLine) - 1, stdin);
assert(s);
(void)s;

使用字符指针s接收fgets的返回值,需要判断一下是否读取成功;
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png
去除commandLine多读取到的换行符\n

commandLine[strlen(commandLine) - 1] = 0;

外

解析用户的输入

读取的用户输入都在字符数组commandLine中,且以空格分隔,所以需要先把commandLine按空格分隔成多个子串。
为了保存分隔的子串,定义一个字符指针数组argv_按顺序依次指向分割的子串,且以NULL空指针结尾。
假定最多分隔的子串不超过63个;

#define OPT_NUM 64
char* argv_[OPT_NUM];

分割字符串的方法很多,这里采用库函数strtok进行commandLine的分割;
strtok函数声明

char *strtok(char *str, const char *delim);

使用strtok时,第一次分割需要指明要分割的是哪个字符串,后续我们还需要继续切割,所以第一个参数填NULL,循环切割,直到strtok函数返回NULL结束。
正巧的是,strtok返回NULL时正好也是argv_所需要的结束,所以while循环简写了。

argv_[0] = strtok(commandLine, " ");
int i = 1;
while(argv_[i++] = strtok(NULL, "  "));

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

内建命令&&特殊处理

解析完commandLine长串为多个子串之后,可以知道argv_[0]是用户期望执行的程序名,而之后的所有子串都是执行该程序的选项。

ls 时目录等文件不带高亮颜色

我们使用ls命令时,一些文件没有高亮,对此,除了我们每次显式的输入"--color=auto"之外,直接在父进程内部进行特殊处理即可:
外链图片
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

if(argv_[0] && strcmp("ls", argv_[0]) == 0){// strcmp传入的参数确保是有效的,否则结果未定义
    argv_[i - 1] = (char*)"--color=auto";
    argv_[i] = 0;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

cd时目录不变的问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
为什么我们的shell,cd的时候,路径没有变化呢?

shell以子进程的方式执行cd命令,子进程有自己的工作目录,cd更改的是子进程的目录,而子进程执行完毕就退出了,继续运行的是父进程shell,而父进程的工作目录从始至终都没有更改
所以解决方法是cd命令时特殊判断,父进程直接执行cd命令,本次循环的后续代码不再执行(称之为自建命令)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
解决方法是:特殊判断cd,直接在父进程中执行实现cd命令的效果–更改进程的当前工作目录。
我们使用chdir()函数实现:

#include <unistd.h>
int chdir(const char *path);
if(argv_[0] && strcmp("cd", argv_[0]) == 0){    
    if(argv_[1]){    
      chdir(argv_[1]);    
    }     
    continue;    
}

改变完父进程myshell的工作目录之后,已经完成了cd的功能,后续代码无须执行,直接continue开始下一次循环,继续等待用户下一次命令行输入。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

echo

echo命令能显示本地变量而env命令获取不到的原因

echo其实是bash的内建命令,不是fork创建子进程去执行的,而是bash亲自执行的,本地变量就在bash内,当然bash能够获取;
而env不是内建命令,是bash通过fork创建子进程然后进程替换(exec)为env进程,然后env进程再查找环境变量的。env是bash的子进程,继承了bash的环境变量,但是bash的本地变量(没有导入到bash环境变量中)没有被env继承,所以env当然就找不到bash的本地变量了。

echo $?显示上一次进程的退出码

既然是内建命令,那么就需要myshell父进程邵本身进行特殊判断和处理:

if(argv_[0] && strcmp("echo", argv_[0]) == 0){    
    if(argv_[1] && strcmp("$?", argv_[1]) == 0){    
        printf("sig: %d, exit code: %d\n", lastSig, lastExitCode);                  lastSig = 0;
        lastExitCode = 0;
        continue;            
    }                
} 

创建子进程

我们使用fork函数为myshell程序创建子进程,让子进程执行程序替换exec从而执行用户期望的程序。

pid_t id = fork();    
assert(id != -1);    

如果子进程创建失败,fork返回-1,后续程序不再执行。

子进程执行进程程序替换

fork函数创建子进程之后函数返回之前,就有了两个执行流:父进程myshell和子进程。
通过父子进程fork返回值的不同,让父子进程执行后续代码的不同部分。
对于子进程,fork函数返回0。
子进程需要进行程序替换,进程替换函数(或者说加载函数)exec有多个,我们选择哪一个呢?

我期望用户直接输入程序名执行而不是路径名,所以需要带p(path),系统自动在PATH中帮我找程序位置; 我期望传递字符指针数组,而不是可变参数列表,所以需要v(vector);
我期望子进程继承默认环境变量就行,即我不想显式传递环境变量,所以没有e(environ);

所以我选择的是execvp函数

int execvp(const char *file, char *const argv[]);
if(id == 0){    
  execvp(argv_[0], argv_);    
  exit(1);// 到这一步,程序替换失败,进程退出,且退出码设置为-1
}

父进程等待

父进程阻塞式等待子进程,知道子进程退出。

pid_t waitpid(pid_t pid, int *status, int options);

使用watpid函数,第一个参数表示等待的子进程id,第二个参数是输出型参数(为NULL时不接受),接收子进程退出状态,第三个参数为0表示父进程阻塞式等待子进程。
我们先不接首子进程状态,第二个参数设置为NULL

int ret = waitpid(id, NULL, 0);    
assert(ret != -1);    
(void)ret;    

waitpid函数返回如果是-1表示等待失败,需要判断一下,等待失败就不再继续执行。

现在我们想要实现xshell中echo $?显示上一次进程运行退出码,怎么实现呢?
其实很简单,定义全局变量lastSig记录子进程退出信号和lastExitCode记录子进程退出码。

int lastSig = 0;
int lastExitCode = 0;

每次父进程等待成功都根据status设置一次lastSiglastExitCode即可。

int status = 0;
int ret = waitpid(id, &status, 0);    
assert(ret != -1);    
(void)ret; 
lastSig = status & 0x7f;// 0~6位表示信号   
lastExitCode = (status >> 8) & 0xff;// 低8~15位表示退出码

myshell.c 源码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

#define NUM 1024
#define OPT_NUM 64

char commandLine[NUM];// 获取用户输入
char* argv_[OPT_NUM];// 存放按空格切割的字符串的多个子串

int status = 0;
int lastSig = 0;
int lastExitCode = 0;

int main(){
    while(1){
        // 输出命令行提示符
        printf("[用户名@主机名 路径]# ");                                 
        // 用户输入
        char* s = fgets(commandLine, sizeof(commandLine) - 1, stdin);
        assert(s);
        (void)s;
        commandLine[strlen(commandLine) - 1] = 0;// 处理用户输入的\n
#ifdef DEBUG
        printf("test: %s\n", commandLine);
#endif                                                                    
        // strtok切割字符串
        argv_[0] = strtok(commandLine, " ");
        int i = 1;
        while(argv_[i++] = strtok(NULL, "  "));
#ifdef DEBUG
        for(int i = 0; argv_[i]; i++)
        printf("argv_[%d]:%s\n", i, argv_[i]);
#endif
        // 命令行带颜色
        if(argv_[0] && strcmp("ls", argv_[0]) == 0){
          argv_[i - 1] = (char*)"--color=auto";
          argv_[i] = 0;
        }
        // cd命令父进程直接执行,改变的是父进程shell的当前工作目录,而不是  更改子进程的工作目录。如果子进程执行cd命令,更改完自己的工作目录就退出了,  父进程工作目录并没有改变。
        if(argv_[0] && strcmp("cd", argv_[0]) == 0){
          if(argv_[1]){
              chdir(argv_[1]);
          } 
          continue;
          // echo $? 查看最近一次进程运行结果信息
        if(argv_[0] && strcmp("echo", argv_[0]) == 0){
          if(argv_[1] && strcmp("$?", argv_[1]) == 0){
              printf("sig: %d, exit code: %d\n", lastSig, lastExitCode);
              lastSig = 0;
              lastExitCode = 0;
              continue;
          }
        }
        // fork子进程执行新程序
        pid_t id = fork();
        assert(id != -1);
        
        if(id == 0){
          execvp(argv_[0], argv_);
          exit(1);
        }
        // 父进程waitpid子进程
        int ret = waitpid(id, &status, 0);
        assert(ret != -1);
        (void)ret;
        lastSig = status & 0x7f;
        lastExitCode = (status >> 8) & 0xff;
    }
      return 0;
}

结语


T h e E n d TheEnd TheEnd

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-03-20 09:08:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-20 09:08:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-20 09:08:04       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-20 09:08:04       20 阅读

热门阅读

  1. PHP与Spring Boot在实现功能上的比较

    2024-03-20 09:08:04       17 阅读
  2. 关于sftp限制登录默认目录若干问题

    2024-03-20 09:08:04       17 阅读
  3. Go语言学习12-反射和Unsafe

    2024-03-20 09:08:04       20 阅读
  4. go 解决货币计算的难题:避免浮点数陷阱

    2024-03-20 09:08:04       17 阅读
  5. Rust 的 PhantomData

    2024-03-20 09:08:04       21 阅读
  6. ES进程除了kill之外,有什么优雅关闭的方式吗?

    2024-03-20 09:08:04       19 阅读
  7. R语言Scale函数与normalize.quantiles()函数的异同

    2024-03-20 09:08:04       21 阅读
  8. Linux Shell 管道基本介绍

    2024-03-20 09:08:04       18 阅读
  9. 红魔馆的馆主

    2024-03-20 09:08:04       20 阅读
  10. SQL-存储过程介绍

    2024-03-20 09:08:04       27 阅读
  11. 未来之路:Python PDF处理技术的革新

    2024-03-20 09:08:04       20 阅读
  12. 使用 pypdf 快速切分 PDF 文件

    2024-03-20 09:08:04       24 阅读