欢迎光临
我们一直在努力

从零实现一个简易 Shell:理解 Linux 进程与命令执行

一、建议Shell简介

本次实现的简易 Shell 具备以下核心功能:

  • 模拟 Linux 终端的命令行提示符([用户名@主机名 工作目录]# );
  • 读取用户输入的命令行指令;
  • 解析命令参数(按空格分割);
  • 通过创建子进程执行外部命令(如ls、pwd、cd等);
  • 等待子进程执行完成,循环等待下一条命令。
  • 该 Shell 的核心逻辑遵循 Linux Shell 的经典流程:提示符输出 → 命令读取 → 命令解析 → 创建子进程执行命令 → 等待子进程退出,完整复现了标准 Shell 的核心工作链路。


    二、代码核心模块解析

    1.头文件与全局配置

    首先看代码的基础配置部分,包含了实现 Shell 所需的核心头文件和全局变量:

    #include <iostream>
    #include <cstdio>
    using namespace std;
    #include <stdlib.h>
    #include <string>
    #include <cstring>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/types.h>

    // 命令缓冲区大小
    const int basesize = 1024;
    // 最大命令参数个数
    const int argvnum = 64;

    // 全局的命令行参数(存储解析后的命令参数)
    char* gargv[argvnum];
    int gargc = 0;

    头文件说明:

    • unistd.h/sys/types.h/sys/wait.h:提供进程创建(fork)、进程等待(waitpid)、环境变量获取等系统调用;
    • cstring/stdlib.h:提供字符串处理(strtok、memset)、内存管理等函数;
    • 其他头文件:满足基础输入输出、字符串操作需求。

    全局变量:gargv存储解析后的命令参数(如ls -l会被解析为gargv[0]="ls"、gargv[1]="-l"),gargc记录参数个数。

    2.命令行提示符创建

    Shell 的提示符需要显示用户名、主机名、当前工作目录,这部分通过三个函数实现:

    // 获取当前用户名
    string GetUserName()
    {
    string name = getenv("USER");
    return name;
    }

    // 获取主机名
    string GetHostName()
    {
    return getenv("HOSTNAME");
    }

    // 获取当前工作目录
    string GetPwd()
    {
    return getenv("PWD");
    }

    // 拼接命令行提示符
    string MakeCommandLine()
    {
    char command_line[basesize];
    snprintf(command_line,basesize,"[%s@%s %s]# ",\\
    GetUserName().c_str(),GetHostName().c_str(),GetPwd().c_str());
    return command_line;
    }

    // 输出提示符(刷新缓冲区确保即时显示)
    void PrintCommandLine()
    {
    printf("%s",MakeCommandLine().c_str());
    fflush(stdout);
    }

    核心逻辑:通过getenv获取系统环境变量(USER/HOSTNAME/PWD),拼接成 Linux 风格的提示符格式,最后通过fflush(stdout)强制刷新输出缓冲区,确保提示符即时显示。

    3.读取用户命令

    接收用户输入的命令,并做基础校验:

    bool GetCommmandLine(char command_buffer[], int size)
    {
    // 读取用户输入到缓冲区
    char* result = fgets(command_buffer, size, stdin);
    if(!result) // 读取失败(如EOF)
    {
    return false;
    }
    // 去除换行符(fgets会读取换行符,需替换为字符串结束符)
    command_buffer[strlen(command_buffer)-1] = 0;
    // 空命令直接返回
    if(strlen(command_buffer) == 0)
    {
    return false;
    }
    return true;
    }

    关键处理:fgets读取的内容包含用户输入的换行符,需将其替换为\\0,否则会影响后续命令解析;空命令(用户仅按回车)直接跳过,避免无效处理。

    4.解析命令参数

    将用户输入的命令按空格分割,填充到全局参数数组gargv中:

    void ParseCommandLine(char command_buffer[], int size)
    {
    (void)size; // 未使用的参数,避免编译警告
    // 清空全局参数数组
    memset(gargv, 0, sizeof(gargv));
    int gargc = 0;
    const char *sep = " "; // 分割符为空格
    // 第一次调用strtok,分割命令缓冲区
    gargv[gargc++] = strtok(command_buffer, sep);
    // 循环分割剩余参数,直到返回NULL
    while((bool)(gargv[gargc++] = strtok(nullptr,sep)));
    gargc–; // 最后一次strtok返回NULL,参数个数减1
    }

    核心函数:strtok是 C 语言字符串分割函数,第一次调用传入待分割字符串,后续调用传入nullptr即可继续分割剩余内容;

    示例:若输入ls -l /home,解析后gargv[0]="ls"、gargv[1]="-l"、gargv[2]="/home"、gargv[3]=NULL。

    5.执行命令

    通过fork创建子进程,在子进程中调用execvp执行命令,父进程等待子进程退出:

    bool ExecuteCommandLine()
    {
    // 创建子进程
    pid_t id = fork();
    if(id<0) // 进程创建失败
    {
    return false;
    }
    if(id == 0) // 子进程逻辑
    {
    // 替换子进程镜像,执行命令
    execvp(gargv[0],gargv);
    // 若execvp返回,说明执行失败(如命令不存在),退出子进程
    exit(1);
    }

    // 父进程逻辑:等待子进程退出
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0) // 成功等待到子进程
    {
    return true;
    }
    return false;
    }

    核心原理:

    • fork:创建与父进程完全相同的子进程,返回值在父进程中是子进程 PID,在子进程中是 0;
    • execvp:替换子进程的代码段、数据段,执行指定命令,若执行成功则不会返回,失败则执行exit(1);
    • waitpid:父进程阻塞等待子进程退出,避免产生僵尸进程。

    6.主函数

    主函数是 Shell 的入口,实现无限循环,持续接收并处理用户命令:

    int main()
    {
    char command_buffer[basesize];
    while(true) // 无限循环,模拟Shell持续运行
    {
    PrintCommandLine();// 1. 输出命令行提示符
    sleep(1); // 可选:延迟1秒,仅为演示

    // 2. 获取用户命令,空命令则跳过
    if(!GetCommmandLine(command_buffer,basesize))
    {
    continue;
    }

    // 3. 解析命令参数
    ParseCommandLine(command_buffer,strlen(command_buffer));

    // 4. 执行命令
    ExecuteCommandLine();
    }
    return 0;
    }

    无限循环:保证 Shell 持续运行,直到用户通过Ctrl+C终止进程;

    sleep(1):代码中添加的延迟,仅为演示,实际 Shell 可移除。

    7.完整代码

    #include <iostream>
    #include <cstdio>
    using namespace std;
    #include <stdlib.h>
    #include <string>
    #include <cstring>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/types.h>

    const int basesize = 1024;
    const int argvnum = 64;

    //全局的命令行参数
    char* gargv[argvnum];
    int gargc = 0;

    string GetUserName()
    {
    string name = getenv("USER");
    return name;
    }

    string GetHostName()
    {
    return getenv("HOSTNAME");
    }

    string GetPwd()
    {
    return getenv("PWD");
    }

    string MakeCommandLine()
    {
    char command_line[basesize];
    snprintf(command_line,basesize,"[%s@%s %s]# ",\\
    GetUserName().c_str(),GetHostName().c_str(),GetPwd().c_str());
    return command_line;
    }

    void PrintCommandLine()
    {
    printf("%s",MakeCommandLine().c_str());
    fflush(stdout);
    }

    bool GetCommmandLine(char command_buffer[], int size)
    {
    char* result = fgets(command_buffer, size, stdin);
    if(!result)
    {
    return false;
    }
    command_buffer[strlen(command_buffer)-1] = 0;
    if(strlen(command_buffer) == 0)
    {
    return false;
    }
    return true;
    }

    void ParseCommandLine(char command_buffer[], int size)
    {
    (void)size;
    memset(gargv, 0, sizeof(gargv));
    int gargc = 0;
    const char *sep = " ";
    gargv[gargc++] = strtok(command_buffer, sep);
    while((bool)(gargv[gargc++] = strtok(nullptr,sep)));
    gargc–;
    }

    bool ExecuteCommandLine()
    {
    pid_t id = fork();
    if(id<0)
    {
    return false;
    }
    if(id == 0)
    {
    execvp(gargv[0],gargv);
    exit(1);
    }

    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)
    {
    return true;
    }
    return false;
    }

    int main()
    {
    char command_buffer[basesize];
    while(true)
    {
    PrintCommandLine();//1.命令行提示符
    sleep(1);

    if(!GetCommmandLine(command_buffer,basesize))//2.获取用户命令
    {
    continue;
    }
    ParseCommandLine(command_buffer,strlen(command_buffer));//3.分析命令

    ExecuteCommandLine();//4.执行命令
    }
    return 0;
    }


    三、编译与运行

    1.编译命令

    g++ simple_shell.cpp -o simple_shell

    2.执行命令

    ./simple_shell

    赞(0)
    未经允许不得转载:171主机测评 » 从零实现一个简易 Shell:理解 Linux 进程与命令执行
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址