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

一、建议Shell简介

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

  1. 模拟 Linux 终端的命令行提示符([用户名@主机名 工作目录]# );
  2. 读取用户输入的命令行指令;
  3. 解析命令参数(按空格分割);
  4. 通过创建子进程执行外部命令(如lspwdcd等);
  5. 等待子进程执行完成,循环等待下一条命令。

该 Shell 的核心逻辑遵循 Linux Shell 的经典流程:提示符输出 → 命令读取 → 命令解析 → 创建子进程执行命令 → 等待子进程退出,完整复现了标准 Shell 的核心工作链路。


二、代码核心模块解析

1.头文件与全局配置

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

cpp 复制代码
#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:提供字符串处理(strtokmemset)、内存管理等函数;
  • 其他头文件:满足基础输入输出、字符串操作需求。

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

2.命令行提示符创建

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

cpp 复制代码
// 获取当前用户名
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.读取用户命令

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

cpp 复制代码
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中:

cpp 复制代码
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执行命令,父进程等待子进程退出:

cpp 复制代码
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 的入口,实现无限循环,持续接收并处理用户命令:

cpp 复制代码
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.完整代码

cpp 复制代码
#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.编译命令

bash 复制代码
g++ simple_shell.cpp -o simple_shell

2.执行命令

bash 复制代码
./simple_shell
相关推荐
Yorlen_Zhang2 小时前
python Tkinter Frame 深度解析与实战指南
开发语言·python
lly2024062 小时前
Eclipse 关闭项目详解
开发语言
LXS_3572 小时前
C++常用容器(下)---stack、queue、list、set、map
开发语言·c++·学习方法·改行学it
愚者游世2 小时前
list Initialization各版本异同
开发语言·c++·学习·程序人生·算法
.小墨迹2 小时前
apollo中车辆的减速绕行,和加速超车实现
c++·学习·算法·ubuntu·机器学习
Poetinthedusk2 小时前
WPF应用跟随桌面切换
开发语言·wpf
Byte不洛2 小时前
Linux 多线程:生产者消费者模型、阻塞队列与条件变量详解
linux·多线程·并发编程·pthread·生产者消费者模型
小Pawn爷2 小时前
13.virtualbox安装ubuntu
linux·运维·ubuntu
Hello World . .2 小时前
数据结构:二叉树(Binary tree)
c语言·开发语言·数据结构·vim