Linux下 进程控制(三) —— ⾃主Shell命令⾏解释器

欢迎来到我的频道 【点击跳转专栏】

码云链接 【点此转跳】

Linux进程相关的所有内容(可以直接点击转跳):

一: Linux进程概念相关:

  1. 【Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)】
  2. 【 Linux下 进程(二)(进程状态、僵尸进程和孤儿进程)】
  3. 【Linux下 进程(三)(进程的优先级)】
  4. 【Linux下 进程(四)(进程的组织、进程的切换和进程O(1)调度算法)】
  5. 【Linux下 进程(五)(命令⾏参数和环境变量)】
  6. 【Linux下 进程(六)(程序地址空间)】

二:Linux进程控制相关:

  1. 【Linux下 进程控制(一) ------ 进程的创建、终止和等待】
  2. 【Linux下 进程控制(二) ------ 进程程序替换】
  3. 【Linux下 进程控制(三) ------ ⾃主Shell命令⾏解释器】

文章目录

  • [1. 实现目标](#1. 实现目标)
  • [2. 实现效果](#2. 实现效果)
  • [3. myshell代码的实现](#3. myshell代码的实现)
    • [3.1 打印命令行](#3.1 打印命令行)
    • [3.2 获取用户输入](#3.2 获取用户输入)
      • fgets
      • [3.1 和 3.2 阶段效果展示](#3.1 和 3.2 阶段效果展示)
    • [3.3 解析字符串](#3.3 解析字符串)
    • [3.4 尝试执行这个命令](#3.4 尝试执行这个命令)
    • [3.5 内建命令的解决](#3.5 内建命令的解决)
      • [1. 交互式 vs. 批处理](#1. 交互式 vs. 批处理)
      • [2. 内建命令 vs. 外部命令](#2. 内建命令 vs. 外部命令)
      • [3. getcwd](#3. getcwd)
      • [4. chdir(系统调用)](#4. chdir(系统调用))
      • [5. snprintf](#5. snprintf)
      • [6. 代码及成果](#6. 代码及成果)
      • [7. 拓展:](#7. 拓展:)
    • [3.6 命令行的二次调整](#3.6 命令行的二次调整)
    • [3.7 从配置文件中获取环境变量填充环境变量表](#3.7 从配置文件中获取环境变量填充环境变量表)
  • [4. 完整代码和该项目意义](#4. 完整代码和该项目意义)
    • [4.1 代码](#4.1 代码)
    • [4.2 意义](#4.2 意义)

1. 实现目标

  • 要能处理普通命令
  • 要能处理内建命令
  • 要能帮助我们理解内建命令/本地变量/环境变量这些概念
  • 要能帮助我们理解shell的运行原理

2. 实现效果

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由标识为bash的⽅块代表,它随着时间的流逝从左向右移动。shell从⽤⼾读⼊字符串"ls"。shell建⽴⼀个新的进程,然后在那个进程中运⾏ls程序并等待那个进程结束。

然后shell读取新的⼀⾏输⼊ps,建⽴⼀个新的进程,在这个进程中运⾏程序 并等待这个进程结束。

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

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

3. myshell代码的实现

3.1 打印命令行

这是正常 shell 下的命令行 我们发现里面有 用户名 主机名 当前路径

所以我们 首先需要获取这三个信息 而获取这些东西有两种方法


  1. 通过环境变量 (但是实际的shell中并不是通过这种方法的 )

我们可以 通过 PWD(当前路径的环境变量) USER(当前用户名环境变量) HOSTNAME(当前主机名环境变量)

(不过需要注意: 在某些最小化的 Linux 发行版或特定的 Shell(如 dash)中,HOSTNAME可能不会被默认导出。)

(在我的机器上unbuntu 22.04上发现 并没有HOSTNAME 这个环境变量)

cpp 复制代码
//利用环境变量获取用户名
const char *GetUserName()
{
        char *name = getenv("USER");
        if(name==NULL)
                return "None";
        return name;
}
cpp 复制代码
//利用环境变量获取当前路径
const char *GetPwd()
{
        char *pwd = getenv("PWD");
        if(pwd==NULL)
                return "None";
        return pwd;
}

  1. 那么 当环境变量 没有效果的时候 是不是只能使用 别的方式了

这张表了解一下就行(函数细节不懂的地方可以自己搜一下 这里就展开说一下gethostname ):

信息项 核心函数/系统调用 头文件 说明
用户名 getlogin() (值开辟在静态区)或 getpwuid() <unistd.h>, <pwd.h> getlogin() 获取登录名;getpwuid 更准确但稍繁琐。
主机名 gethostname() <unistd.h> 获取系统网络名称。
当前路径 getcwd() <unistd.h> 获取当前工作目录的绝对路径。

gethostname()

gethostname 是 Linux/Unix 系统编程中用于获取当前主机名称 的标准函数,属于系统调用

相比于使用 getenv("HOSTNAME"),它是最可靠的方法,因为它直接向操作系统内核查询,不依赖任何环境变量设置。

c 复制代码
#include <unistd.h>

int gethostname(char *name, size_t len);
  1. name: 一个字符数组(缓冲区)的指针,用于存放获取到的主机名。
  2. len : 缓冲区的长度(字节数)。
    • 注意 :为了防止缓冲区溢出,通常建议定义为 256 字节或更大(虽然标准主机名通常较短,但为了安全起见)。
  • 成功 : 返回 0,主机名已存入 name 缓冲区。
  • 失败 : 返回 -1,并设置 errno 错误码。
    我们直接使用这个函数 来获取当前主机名字
cpp 复制代码
const char *GetHostName()
{
        static char hostname[256];
        //除了字符串 还会读取\n
       if(gethostname(hostname, sizeof(hostname)) == 0)
       {      
        return hostname;
       }
         return "None";
}

问题来了 为什么要 static char hostname[256];这么写?不能直接 char* hostname 吗? 或者 直接不要static 定义数组不行吗??


  1. 指针 vs 数组
  • (char* hostname) :声明了一个指针变量。它只占 8 字节(64位系统),里面存的是一个地址。因为没初始化,这个地址是随机的(随机地址可能只读,可能不存在或者被保护【内核空间或者别人家地址】)。
  • gethostname 试图往这个随机地址写数据,操作系统会立即终止程序(段错误)。(可以这么写 char *hostname = (char *)malloc(256);
  • 正确写法 (char hostname[256]) :声明了一个数组。系统在栈上分配了 256 字节的连续空间。gethostname 可以安全地把主机名填入这块空间。

  1. sizeof 的陷阱
  • 指针 使用 sizeof:结果是指针本身的大小(通常是 8 字节)。
  • 数组 使用 sizeof:结果是数组分配的总字节数(这里是 256)。

  1. static 关键字的重要性 你的函数返回类型是 const char*
  • 如果你定义 char hostname[256](不加 static),这个数组是局部变量,存储在栈上。当 GetHostName 函数执行结束返回时,这块栈内存会被系统回收。
  • 此时你返回的 hostname 指针指向的是一块已经被标记为"可用"的内存。主程序再去读取它时,里面的数据可能已经被覆盖或变成乱码。
  • 加上 static 后,数组存储在静态存储区,生命周期伴随整个程序运行,返回它的地址是安全的。

整合代码

cpp 复制代码
//打印命令行
void PrintCommandLine()
{
        printf("[%s@%s %s]# ",GetUserName(),GetHostName(),GetPwd());//用户名 @ 主机名 当前路径
        fflush(stdout);//刷新缓存区!
}

int main()
{
while(1)
  {
          //1.打印命令行字符串
          PrintCommandLine();
   } 
}        

3.2 获取用户输入

关于 获取用户输入 很多人第一反应是 scanf 但是使用 scanf会空格无法读取 所以这里推荐 使用 fgets

fgets

fgets 是 C 语言中用于从流中读取一行字符串 的标准库函数。它是 gets 函数的安全替代品,因为它允许你指定最大读取长度,从而有效防止缓冲区溢出。

c 复制代码
#include <stdio.h>

char *fgets(char *str, int n, FILE *stream);
  1. str: 指向字符数组(缓冲区)的指针,用于存储读取到的字符串。
  2. n : 最大读取的字符数(包括结尾的空字符 \0)。fgets 最多读取 n-1 个字符,并自动在末尾添加 \0
  3. stream : 指向 FILE 对象的指针,表示要读取的输入流。
    • 读取文件时:传入文件指针(如 fp)。
    • 读取标准输入时:传入 stdin
  • 成功 : 返回 str 指针(即缓冲区的地址)。
  • 失败或读到文件末尾 (EOF) : 返回 NULL
    ⚠️:fgets 会保留输入中的换行符 (\n) 。如果输入的行包含换行符且缓冲区足够大,它会被存储在字符串中。
    处理方法:通常需要手动去除末尾的换行符。

所以读取用户输入 可以这么写

cpp 复制代码
//获取用户输入
int GetCommand(char commandline[],int size)
{

          if(fgets(commandline,size,stdin)==NULL)
                  return 0;
          //2.1 用户在输入的时候,至少会按一下回车\n
          commandline[strlen(commandline)-1]='\0';
            return strlen(commandline);
}

int main()
{
        char command_line[MAXSIZE]={0};//`MAXSIZE`是我定义的宏 值是256
  while(1)
  {
          //1.打印命令行字符串
          PrintCommandLine();
          //2.获取用户输入
          if(GetCommand(command_line,sizeof(command_line))==0)
                  continue;
  }

  return 0;
}

3.1 和 3.2 阶段效果展示

3.3 解析字符串

类似当我们输入ls -a -l 在真正命令行解释器,就要对用户输入的命令字符串首先进行解析!

shell内部自己会维护一张表 用于存储命令行参数我们也要仿照其 构建一张表!

cpp 复制代码
#define MAXARGS 32
//shell自己内部维护的第一张表:命令行参数表
//故意设置成全局
char *gargv[MAXARGS];
int gargc = 0;//用于记录表中存储数据的个数

还是拿 ls -l -a举例子:

我们输入 ls -l -a 的时候
gargv[0]="ls"、gargv[1]="-l"、gargv[2]="-a" 空格就是他们的分割符

该如何实现?这里我们要介绍一个函数strtok!

strtok

strtok 是 C 语言标准库中用于字符串分割的核心函数。它的作用是将一个长字符串根据指定的分隔符(如空格、逗号)切分成一个个独立的子串。

虽然它非常常用,但它也是一个 "危险" 的函数,因为它会直接修改原字符串。

c 复制代码
#include <string.h>

char *strtok(char *str, const char *delim);
  • str :待分割的字符串。
    • 首次调用:传入需要分割的字符串指针。
    • 后续调用 :必须传入 NULL ,表示继续分割上一次的字符串。(因为里面的快慢指针 你可以理解成 静态的static的 是全局的!
  • delim :分隔符集合(如 " ,;" 表示空格、逗号、分号都是分隔符)。
  • 返回值
    • 成功:返回当前找到的子串的指针。
    • 失败/结束:返回 NULL

⚙️ 核心工作原理:它是如何"破坏"的?

假设字符串是 "A,B,C",分隔符是 ","

  • 调用1 :找到 ,,将其变为 \0。内存变为 "A\0B,C",返回指向 "A" 的指针。
  • 调用2 :从上次位置继续,找到下一个 ,,变为 \0。内存变为 "A\0B\0C",返回指向 "B" 的指针。
  • 调用3 :找到结尾 \0,返回指向 "C" 的指针。

最后返回 NULL


所以我们可以这么写:

cpp 复制代码
const char *gsep=" ";
int ParseCommand(char commandline[])
{
        //记得每次要把表清空
        gargc=0;
        memset(gargv,0,sizeof(gargv));
        gargv[0]=strtok(commandline,gsep);
        while(gargv[++gargc]=strtok(NULL,gsep));
       //这一段是为了可视化测试是否成功将输入的字符串读取到了我们写的命令行参数表
        printf("gargc: %d\n",gargc);
        int i=0;
        for(;gargv[i];i++)
            printf("gargv[%d]: %s\n",i,gargv[i]);
        return gargc;
}

效果展示

3.4 尝试执行这个命令

下一步 就应该通过程序替换 来执行系统命令了 当然应该该使用fork来创建子进程,然后将子进程进行程序替换 这边推荐使用execvp,因为该函数的底层是execve 会自动继承父进程的环境变量表 如果需要对表进行修改 也可以直接使用putenv,此时对应于环境变量 也不过发生写时拷贝罢了!

cpp 复制代码
int ExecuteCommand()
{
        pid_t id = fork();
        if(id<0)
        return -1;
        else if(id==0)
        {
                //子进程:如何执行
                execvp(gargv[0],gargv);
                exit(1);
        }
        else
        {
                //父进程
                int status = 0;
                pid_t rid = waitpid(id,&status,0);
        //      if(rid>0)
        //      {
        //              printf("wait child process success!\n");
        //      }
        }
return 0;

}

效果展示

疑难点

我们发现 如果使用cd .. 路径并不会改变:

这是因为 cd .. 切换的是fork子进程的路径,我们以前bash使用 cd ..切换的是父进程bash自己的路径!然后其他命令又是bash的子进程,所有子进程会继承父进程当前的工作路径!

3.5 内建命令的解决

解决该问题前 我们需要认识一些概念和函数!

1. 交互式 vs. 批处理

Shell 的工作方式从根本上取决于它的命令从哪里来。

  1. 交互模式
    这就是你每天在终端里做的事。Shell 作为一个实时对话者,等待你输入命令。
    • 流程读取 (Read) -> 执行 (Eval) -> 打印 (Print) -> 循环 (Loop)。
    • 特点:你输入一条,它执行一条,立刻给你看结果。这是最常用、最直观的模式。
  2. 批处理模式
    这就是运行 Shell 脚本。你把一堆命令事先写在一个文件里,然后让 Shell 一次性执行完。
    • 特点:自动化、无需人工干预。适合执行重复性、流程化的复杂任务。

2. 内建命令 vs. 外部命令

内建命令
  • 是什么 :这些命令是 Shell 程序自身的一部分,就像"大管家"自己会的技能,比如 cd (切换目录)、exit (退出)、alias (设置别名)。
  • 如何执行 :Shell 不会创建新进程 ,而是在当前 Shell 进程内部 直接执行。( ⚠️:内建命令的脚本文件是一个外部文件。但当 Shell 执行脚本时,它是在读取并解释这个文件里的内容。
  • 为什么
    • 效率:无需启动新程序,速度极快。
    • 必要性 :有些命令必须在当前进程执行才有效。最典型的例子就是 cd。如果 cd 是一个外部程序,它会在一个子进程中切换目录,然后子进程结束,你所在的 Shell 目录根本没变。所以 cd 必须是内建的。
外部命令
  • 是什么 :这些是独立于 Shell 的可执行程序文件,比如 lsgrepps。它们通常位于 /bin/usr/bin 等目录下。
  • 如何执行 :当 Shell 发现一个命令不是内建的,它就会去系统路径里找到这个程序,然后创建一个新的子进程来运行它。
  • 流程 :Shell 通过 fork() 系统调用创建一个自己的副本(子进程),然后在子进程里用 exec() 系统调用把这个子进程替换成你要执行的程序(比如 ls)。

3. getcwd

bash中有一个环境变量表中存储当前的工作路径,所以我们的shell内部也应该维护自己的工作路径

cpp 复制代码
#define  MAXSIZE 128
//我们shell自己所处的工作路径
char cwd[MAXSIZE];

此时我们肯定不能单纯的使用 bash的环境变量表来维护了,而是该使用getcwd()函数来获取当前的工作路径


getcwd 是 C 语言中用于获取当前工作目录(Current Working Directory)绝对路径的标准函数。

  • Linux / Unix (POSIX):

    c 复制代码
    #include <unistd.h>
    char *getcwd(char *buffer, size_t size);
参数 说明
buffer 用于存储路径字符串的字符数组指针。
size / maxlen 缓冲区的大小(字节数)。
  • 返回值
    • 成功 :返回指向 buffer 的指针。
    • 失败 :返回 NULL,并设置全局变量 errno(常见错误码 ERANGE 表示缓冲区太小)。
      ⚠️:如果将 buffer 设为 NULLsize 设为 0getcwd 会自动调用 malloc 分配刚好足够的内存来存储路径。
      注意 :这种情况下,你必须手动调用 free() 释放内存,否则会导致内存泄漏

cpp 复制代码
const char *GetPwd()
{
        //char *pwd = getenv("PWD");
        //不用环境变量的方式
        char* pwd=getcwd(cwd,sizeof(cwd));
        if(pwd==NULL)
                return "None";
        return cwd;
}

4. chdir(系统调用)

在在 C 语言编程中,chdir 是一个标准的 POSIX 系统调用,用于改变当前进程的工作目录。

  • 头文件#include <unistd.h>

  • 函数原型

    c 复制代码
    int chdir(const char *path);
  • 参数
    • path:目标目录的路径,可以是绝对路径(如 /home/user)或相对路径(如 ../src)。
  • 返回值
    • 成功 :返回 0
    • 失败 :返回 -1,并设置 errno 错误码(例如 ENOENT 表示目录不存在,EACCES 表示无权限)。
代码示例
c 复制代码
#include <stdio.h>
#include <unistd.h> // Linux 标准头文件

int main() {
    // 尝试切换到 /tmp 目录
    if (chdir("/tmp") == 0)
     {
        printf("成功切换到 /tmp 目录\n");
    } 
    else
     {
        perror("切换目录失败"); // 打印错误原因
        return 1;
    }
    return 0; 
}

5. snprintf

它的核心作用是:将各种类型的数据(整数、浮点数等)按照指定格式拼接到一个字符串中,同时严格限制写入长度,防止缓冲区溢出。

c 复制代码
#include <stdio.h>

int snprintf(char *str, size_t size, const char *format, ...);
参数 说明
str 指向目标字符数组(缓冲区)的指针,用于存储格式化后的结果。
size 缓冲区的总大小 (字节数)。这是它与 sprintf 最大的区别。
format 格式化字符串(如 "%d", "%s"),与 printf 相同。
... 可变参数列表,用于填充 format 中的占位符。

snprintf 的设计初衷是为了解决 sprintf 容易导致缓冲区溢出的问题。它的行为逻辑如下:

  1. 自动截断 :如果格式化后的字符串长度 >= size,它只会写入 size - 1 个字符。
  2. 自动补零 :无论是否截断,它始终 会在缓冲区末尾自动添加字符串结束符 \0(只要 size > 0)。
  3. 返回值玄机
    • 它返回的是 "假如缓冲区足够大,应该写入的字符数" (不包括 \0)。
    • 判断截断 :如果 返回值 >= size,说明发生了截断,数据不完整。

6. 代码及成果

通过上述方式 我们就可以创建一个函数 功能如下

判断:若为内建命令,则shell自己执行;若不为内建命令,则创建子进程然后用exec*=进行替换!同时切换路径的时候还要自动更改环境变量!

cpp 复制代码
/**
 * 检查并执行内建命令
 * 
 * 返回值说明:
 * 0: 不是内建命令 (调用者应继续执行 fork/exec 流程)
 * 1: 是内建命令且已执行完毕 (调用者应直接返回,不再创建子进程)
 */
int CheckBuiltinExecute()
{
    // 1. 判断输入的命令是否为 "cd"
    // gargv[0] 存储的是用户输入的命令字符串(例如 "cd", "ls")
    if (strcmp(gargv[0], "cd") == 0)
    {
        // 2. 进入 cd 命令的处理逻辑
        // 这是一个内建命令,必须由当前进程直接执行,不能 fork 子进程
        
        // 3. 检查参数个数
        // gargc == 2 表示命令格式为 "cd <目录路径>"
        // 例如: cd /home/user
        if (gargc == 2)
        {
            // 4. 执行目录切换
            // gargv[1] 是用户输入的目标路径参数
            // chdir 是系统调用,成功返回 0,失败返回 -1
            // 注意:这里直接修改了当前 Shell 进程的工作目录
            chdir(gargv[1]);
           //更改环境变量
           char pwd[1024];
           getcwd(pwd,sizeof(pwd));
           snprintf(cwd,sizeof(cwd),"PWD=%s",pwd);//cdw="PWD=当前的路径"
           putenv(cwd);
       //⚠️:发生了写时拷贝,改的其实是我们自己的!
          }
        
        // 5. 返回 1,告诉主循环:
        // "我已经处理了这个命令,不需要再 fork 子进程去执行了"
        return 1;
    }

    // 6. 如果不是 "cd",返回 0
    // 主循环收到 0 后,会继续执行后续的 fork() 和 execvp() 来处理外部命令
    return 0;
}

7. 拓展:

内建命令有很多 包括echo $?在内 可以看到最近一次的退出码 我们还可以稍微优化一下

cpp 复制代码
//最近一个命令执行完毕后,退出码
int lastcode=0;

//return val:
//0:不是内建命令
//1:内建命令&&执行完毕
int CheckBuiltinExecute()
{
   if(strcmp(gargv[0],"cd")==0)
   {
           //内建命令
           if(gargc==2)
           //1.更改进程内核中的路径        
           //新的目标路径:gargv[1]
           {
                   chdir(gargv[1]);
           //2.更改环境变量
           char pwd[1024];
           getcwd(pwd,sizeof(pwd));
           snprintf(cwd,sizeof(cwd),"PWD=%s",pwd);//cdw="PWD=当前的路径"
           putenv(cwd);
           lastcode=0;
           }
           return 1;
   }
   else if(strcmp(gargv[0],"echo")==0)
   {
           if(gargc==2)
           {
                   if(gargv[1][0]=='$')
                   {
                           if(strcmp(gargv[1]+1,"?")==0)
                           {
                                   printf("lastcode:%d\n",lastcode);
                           }
                  else if(strcmp(gargv[1]+1,"PATH")==0)
                  {
                   printf("%s\n",getenv("PATH"));
                  }
                   }
                 }
           lastcode=0;

             return 1;
   }
   return 0;
}

此时 对应的 waitpid 也得发生变化:

cpp 复制代码
  //父进程
                int status = 0;
                pid_t rid = waitpid(id,&status,0);
                if(rid>0)
                {
                        lastcode = WEXITSTATUS(status);
        //              printf("wait child process success!\n");
                }

3.6 命令行的二次调整

我们发现 在bash中 命令行打印的是相对路径 而我们的shell打印的却是绝对路径,那么这一块该怎么解决呢?

cpp 复制代码
static std::string rfindDir(const std::string &p)
{
    // 1. 处理根目录的特殊情况
    // 如果传入的路径就是根目录 "/",直接返回 "/"
    if(p == "/")
        return p;

    const std::string psep = "/"; // 定义路径分隔符

    // 2. 从字符串右侧(末尾)开始查找最后一个 "/" 的位置
    // 例如:"/usr/local/bin" -> 找到 "bin" 前面的那个 "/"
    auto pos = p.rfind(psep);

    // 3. 如果没有找到分隔符(说明路径中不包含 "/",或者是一个空字符串)
    if(pos == std::string::npos)
        return std::string(); // 返回一个空字符串

    // 4. 截取并返回分隔符之后的内容
    // pos + 1 表示从 "/" 的下一个字符开始截取,直到字符串末尾
    // 例如:"/home/user/file.txt" -> 返回 "file.txt"
    return p.substr(pos + 1);
}

//打印命令行
void PrintCommandLine()
{
        printf("[%s@%s %s]# ",GetUserName(),GetHostName(),rfindDir(GetPwd()).c_str());//用户名 @ 主机名 当前路径
        fflush(stdout);
}

效果:

3.7 从配置文件中获取环境变量填充环境变量表

正常情况下,bahs的环境变量表内部是从配置文件来的,这次我们自己的shell就直接用父进程继承的方式来自己手动创建并尝试维护一下!

cpp 复制代码
//环境变量表
char *genv[MAXARGS];
int genvc=0;//环境变量的个数

void LoadEnv()
{
        //正常情况下,环境变量表内部是从配置文件来的
        //今天我们从父进程拷贝
        extern char **environ;
        for(;environ[genvc];genvc++)
        {
                genv[genvc]= (char*)malloc(sizeof(char)*4096);
                strcpy(genv[genvc],environ[genvc]);
        }

        genv[genvc]=NULL;
//printf("Load env: \n");
  //  for(int i = 0; genv[i]; i++)
    //    printf("genv[%d]: %s\n", i, genv[i]);
     }

然后对应的 我们的 exec*可以改成这样

cpp 复制代码
  execvpe(gargv[0],gargv,genv);

哪怕不传 其实也不影响其自动继承维护!这里手动写只是为了模拟bash环境变量表如何产生罢了!也就是说学习意义大于实际意义

4. 完整代码和该项目意义

4.1 代码

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<string>
#define  MAXSIZE 128
#define MAXARGS 32
//shell自己内部维护的第一张表:命令行参数表
//故意设置成全局
char *gargv[MAXARGS];
int gargc = 0;
const char *gsep=" ";
//我们shell自己所处的工作路径
char cwd[MAXSIZE];
//最近一个命令执行完毕后,退出码
int lastcode=0; 
//环境变量表
char *genv[MAXARGS];
int genvc=0;//环境变量的个数

void LoadEnv()
{
        //正常情况下,环境变量表内部是从配置文件来的
        //今天我们从父进程拷贝
        extern char **environ;
        for(;environ[genvc];genvc++)
        {
                genv[genvc]= (char*)malloc(sizeof(char)*4096);
                strcpy(genv[genvc],environ[genvc]);
        }

        genv[genvc]=NULL;
//printf("Load env: \n");
  //  for(int i = 0; genv[i]; i++)
    //    printf("genv[%d]: %s\n", i, genv[i]);
     }
static std::string rfindDir(const std::string &p)
{
        if(p=="/")
                return p;
        const std::string psep="/";
        auto pos=p.rfind(psep);
        if(pos==std::string::npos)
                return std::string();
        return p.substr(pos+1);
}
//利用环境变量获取用户名
const char *GetUserName()
{
        char *name = getenv("USER");
        if(name==NULL)
                return "None";
        return name;
}

//利用系统调用获取主机名
const char *GetHostName()
{
        static char hostname[256];
        //除了字符串 还会读取\n
       if(gethostname(hostname, sizeof(hostname)) == 0)
       {      
        return hostname;
       }
         return "None";
}

//利用环境变量获取当前路径
const char *GetPwd()
{
        char *pwd = getenv("PWD");
        //不用环境变量的方式
        //char* pwd=getcwd(cwd,sizeof(cwd));
        if(pwd==NULL)
                return "None";
        return pwd;
}

//获取用户输入
int GetCommand(char commandline[],int size)
{

          if(fgets(commandline,size,stdin)==NULL)
                  return 0;
          //2.1 用户在输入的时候,至少会按一下回车\n
          commandline[strlen(commandline)-1]='\0';
            return strlen(commandline);
}



//打印命令行
void PrintCommandLine()
{
        printf("[%s@%s %s]# ",GetUserName(),GetHostName(),rfindDir(GetPwd()).c_str());//用户名 @ 主机名 当前路径
        fflush(stdout);
}


int ParseCommand(char commandline[])
{
        //记得每次要把表清空
        gargc=0;
        memset(gargv,0,sizeof(gargv));
        gargv[0]=strtok(commandline,gsep);
        while(gargv[++gargc]=strtok(NULL,gsep));

        //printf("gargc: %d\n",gargc);
        //int i=0;
        //for(;gargv[i];i++)
        //      printf("gargv[%d]: %s\n",i,gargv[i]);
        return gargc;
}

//return val:
//0:不是内建命令
//1:内建命令&&执行完毕
int CheckBuiltinExecute()
{
   if(strcmp(gargv[0],"cd")==0)
   {
           //内建命令
           if(gargc==2)
           //1.更改进程内核中的路径        
           //新的目标路径:gargv[1]
           {
                   chdir(gargv[1]);
           //2.更改环境变量
           char pwd[1024];
           getcwd(pwd,sizeof(pwd));
           snprintf(cwd,sizeof(cwd),"PWD=%s",pwd);//cdw="PWD=当前的路径"
           putenv(cwd);
           lastcode=0;
           }
           return 1;
   }
   else if(strcmp(gargv[0],"echo")==0)
   {
           if(gargc==2)
           {
                   if(gargv[1][0]=='$')
                   {
                           if(strcmp(gargv[1]+1,"?")==0)
                           {
                                   printf("lastcode:%d\n",lastcode);
                           }
                  else if(strcmp(gargv[1]+1,"PATH")==0)
                  {
                   printf("%s\n",getenv("PATH"));
                  } 
                   }
                 }
           lastcode=0;

             return 1;
   }
   return 0;
}

int ExecuteCommand()
{
        pid_t id = fork();
        if(id<0)
        return -1;
        else if(id==0)
        {
                //子进程:如何执行
                execvpe(gargv[0],gargv,genv);
                exit(1);
        }
        else
        {
                //父进程
                int status = 0;
                pid_t rid = waitpid(id,&status,0);
                if(rid>0)
                {
                        lastcode = WEXITSTATUS(status);
        //              printf("wait child process success!\n");
                }
        }
return 0;

}


int main()
{
        //0.从配置文件中获取环境变量填充环境变量表
        LoadEnv();
        char command_line[MAXSIZE]={0};
  while(1)
  {
          //1.打印命令行字符串
          PrintCommandLine();
          //2.获取用户输入
          if(GetCommand(command_line,sizeof(command_line))==0)
                  continue;
          //3. 解析字符串 -> "ls -a -l" -> 命令行解释器,就要对用户输入的命令字符串首先进行解析!
          ParseCommand(command_line);
          //4.这个命令,到底是让父进程自己执行(内建命令),还是让子进程执行
          if(CheckBuiltinExecute()==1)
          {
                  continue;
          }
          //5. 尝试执行这个命令
          ExecuteCommand();
  }

  return 0;
}

4.2 意义

这个自主shell本身实现其实并没有意义 但是我们可以通过对于它的实现,来深层次的理解bash是如何工作的!我们可以通过它 知道 普通命令 内建命令 以及环境变量表和命令行参数表对应于bash的关系等等。也就是说 该项目手动实现有很强的 教育学习意义!

至此 单纯的Linux进程部分 告以结束!

相关推荐
imc.111 小时前
linux基础IO
linux·运维·服务器
躺不平的理查德1 小时前
让Ubuntu 的第二块网卡 ens37 永久固定为 192.168.30.111
运维·服务器
bitbrowser1 小时前
Facebook 验证反复回到原页面,应该从哪里排查
运维·服务器·facebook
糖果店的幽灵2 小时前
langgraph的分支结构之 - 静态分支详解
java·服务器·数据库·langgraph
虹科网络安全2 小时前
艾体宝新闻|从 SQL 注入到服务器接管:CVE-2026-57517 暴露 Web 管理面板的供应链与安全编码风险
服务器·前端·sql
BelongPanda3 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
AC赳赳老秦3 小时前
招投标公开数据自动化采集实战:基于 OpenClaw 的定时抓取与业务关键词精准推送
运维·服务器·数据库·自动化·测试用例·deepseek·openclaw
酷可达拉斯3 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998343 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器
yunwei373 小时前
eBPF 教程:检查 exec 后真正安装的可执行镜像
linux·开源