《简易制作 Linux Shell:详细分析原理、设计与实践》

**前引:**你是否好奇 Bash 是如何将你输入的命令变成操作系统的实际动作?本项目将一步步教你实现一个支持基本命令执行、管道、重定向和后台运行的 Linux Shell。通过亲手编写代码,你将加深对 Linux 进程模型、文件描述符、信号机制和系统调用的理解,同时提升你的系统编程能力!

目录

简易版:

【一】打印命令行

【二】输入命令行

【三】解析命令行

【四】父子进程创建

【五】进程替换

【六】子进程回收

【七】封装整理

挑战版:

【一】解决cd指令

【二】解决echo命令


简易版:

【一】打印命令行

在原本的shell中,我们每次都可以看见如下的打印,等待你输入指令:

所以可以看到需要获取一些用户当前的环境变量信息,因为这里属于实现,所以我们选择函数调用的方式(getenv(),头文件#include<stdlib.h>)来获取环境变量(获取内容自己个性化设置!):

【二】输入命令行

这里我们要开始输入命令行,在之前我们已经学了命令行的输入其实是一个个字符串,例如:

"ls" "pwd" "touch"等,我们每次输入都是输入的字符串,再根据空格去分割,我们使用 fgets():

函数原型:

cpp 复制代码
char *fgets(char *str, int n, FILE *stream);

第一个参数:一个字符串指针,用来存放从流中读取到的字符串

第二个参数:最多读取的字符数

第三个参数:输入的文件流(stdin:标准输入流,通常是键盘输入)

例如:

cpp 复制代码
#define MAX 32

char str[MAX];

//命令获取
fgets(str,sizeof(str)-1,stdin); 

//注意去除用户输入的换行符
str[strlen(str)-1]='\0';

效果:将键盘输入的字符串存储到 str 里面,str存储的类似:**"pwd ls rm"**这样的一整个字符串

【三】解析命令行

现在我们已经利用C语言的库函数 fgets()函数将命令行存储到了 str 数组里面,现在我们通过字符串分割来根据空格和\0将每个命令解析出来,放在一个字符串数组里面:

这里采用的是strtok()库函数,头文件:<string.h>,分隔符会被替换成 \0

函数原型:

cpp 复制代码
char *strtok(char *str, const char *delim);

第一个参数:从 str 里面拿要分割的字符串

第二个参数:根据delim里面每个字符来进行分割,比如"./-+"

(注意:如果想多次分割,第一次传字符串,后续传 NULL否则会从头重新分割)

返回值:

成功:返回指向当前分割出的子串(token)的指针

没有更多子串:返回 NULL

例如:

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


char* argv[MAX]={NULL};      
const char* delim=" \0";


int i = 0;
//命令行提取
argv[i++]=strtok(str,delim);
while(argv[i-1])
{
   argv[i++]=strtok(NULL,delim);
}

效果:将刚才的一整个字符串,根据空格截取每段到一个字符串数组里面

【四】父子进程创建

既然我们现在获取了命令行参数,调用就很简单了,可以先分割父子进程:

cpp 复制代码
pid_t d =fork();
if(d==0)
{
  .... 
}
else
{
  ....
}

【五】进程替换

将当前子进程的代码数据采用 execvp()进行替换,它的第一次参数只需要是路径就行

注意:如果用户输入的是换行符,需要判断一下!

cpp 复制代码
//argv就是提取的字符串数组

//进程替换
if(argv[0]==NULL)
{
   return 0;
}
int count =  execvp(argv[0],argv);
if(count<0)perror("execvp failed");
exit(0);

【六】子进程回收

这里我采用的是阻塞等待,也可以采用非阻塞等待,自定义!

cpp 复制代码
//回收子进程
int count = waitpid(-1,NULL,0);
if(count<0)
{
   printf("子进程回收失败\n");
}

【七】封装整理

现在我们用函数来封装一下,更加的美观!(注意:拷贝传参自动带清零的效果!)

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>

#define LEFT "["
#define RIGHT "]"
#define MAX 32

int argc=0;
char str[MAX];
char* argv[MAX]={NULL};
const char* delim=" ";

//命令行打印
void command_printf()
{
  printf(LEFT"%s"":""%s"" ""#"RIGHT" ",getenv("USER"),getenv("HOME"));
}
//命令行获取
char* command_get(char str[MAX],int size)
{
  char* pc=fgets(str,size,stdin);
  //去除换行符
  str[strlen(str)-1]='\0';

  //测试
  //printf("命令行获取测试:\n");
  //int i=0;
  //while(str[i])
  //{
  //  printf("%c",str[i++]);
  //}
  //printf("\n");
  return pc;
}
//命令行提取
void command_extraction(char str[MAX],const char* delim,char* argv[MAX])
{
  int i=0;
  //printf("命令行提取测试:\n");
  argv[i++]=strtok(str,delim);
  while(argv[i-1])
  {
    argc++;
    //printf("argv[%d]=%s\n",i-1,argv[i-1]);
    argv[i++]=strtok(NULL,delim);
  }
  //printf("\n");
  return;
}

//命令行参数调用与回收
void command_use(char* argv[MAX])
{
  pid_t d = fork();
  if(d==0)
  {
      //进程替换
      if(argv[0]==NULL)
      { 
          return;
      }
      int count =  execvp(argv[0],argv);
      if(count<0)perror("execvp failed");
      //子进程退出
      exit(EXIT_FAILURE);
      }
  else
  {
      //回收子进程
      int count = waitpid(-1,NULL,0);                                                                                       
      if(count<0)
      {
          printf("子进程回收失败\n");
      }
   }
  return;
}


int main()
{
  while(1)
  {   //命令行打印
      command_printf();
      //命令获取
      int size=sizeof(str)-1;
      char* pc=command_get(str,size);
      if(pc==NULL)
      {
        printf("读取失败\n");
      }
      //命令行提取
      command_extraction(str,delim,argv);
      //命令行参数调用
      command_use(argv);
  }
  return 0;
}

效果展示:

挑战版:

现在我们已经完成了基本的shell功能,但是像 echo $PATH、cd ../ 这些内置命令,例如:

**原因:**在 Linux/Unix 下,shell 命令分为两类:

(1)外部命令 例如 lscatps 等,它们是磁盘上的可执行文件。当 shell 执行它们会 fork() 一个子进程,然后 execvp() 加载对应的程序

(2)内置命令(built-in) 例如 cdecho(部分实现)、exportsourceexit 等。这些命令必须由 shell 自己直接执行 ,不能用 fork() 子进程执行,因为它们会影响 shell 自身的运行环境

【一】解决cd指令

cd 指令的效果就是改变当前的工作目录,而实现的 shell 每轮输出一次的指令效果,然后自己就挂掉了,因此不会影响到下一个子进程,所以 cd 命令不应该给子进程完成,而交给父进程,而父进程本身又是系统shell的子进程,所以我们需要父进程调用 chdir()函数:

补充知识:C语言字符串比较用strcmp()【狗头】continue只能用在循环里面【狗头】

cpp 复制代码
int chdir(const char *path);

//参数为目标路径

执行逻辑:

cpp 复制代码
//cd命令判断                                                                                                                                
if(strcmp(argv[0],"cd")==0 && argc==2)
{
  /如果是跳到当前目录
  if(strcmp(argv[1],"./")==0)
  {
    return;
  }
  else
  {
    //剩余可以交给chdir函数
    const char*path=argv[1];
    int count = chdir(path);
    if(count==-1)
    {
       printf("路径执行错误\n");
       return;
    }
  }
}

效果展示:

【二】解决echo命令

shell 不是直接把 $PATH 传给 echo 程序,而是先替/usr/local/sbin:/usr/local/bin:... 这样的真实值(命令展开 )因此我们需要先判断第二个参数的开头是不是 $ 符号(是则getenv()替换)

原理:先用getenv()获取展开的环境变量,再替换argv[1],就可以直接打印出来

cpp 复制代码
//echo命令判断                                                                                                              
if(strcmp(argv[0],"echo")==0 && argc==2)
{
   //取第二个参数
   char* pc=argv[1];
   //防止只有一个¥
   if(pc[0]=='$' && strlen(pc)>1)
   {
      //去除¥
      char* var_name = pc + 1;     
      //获取展开的环境变量         
      char* value=getenv(var_name);
      //替换                  
      if(value)               
      {                       
        argv[1]=value;        
      }                       
   }                         
}

效果展示:

其它的也可以增加 export 指令,这里就不展示了!正确处理环境变量即可!

相关推荐
weixin_307779134 小时前
在Linux服务器上使用Jenkins和Poetry实现Python项目自动化
linux·开发语言·python·自动化·jenkins
爱宇阳4 小时前
Linux 教程:如何查看服务器当前目录中的文件
linux·运维·github
天才奇男子4 小时前
用户管理,权限管理
linux·云原生
wheeldown4 小时前
【Linux】Linux 进程通信:System V 共享内存(最快方案)C++ 封装实战 + 通信案例,4 类经典 Bug 快速修复
linux·运维·服务器·开发语言
Ching·5 小时前
linux系统编程(十②)RK3568 socket之 TCP 客户端的实现
linux·tcp/ip·rk3568
NiKo_W5 小时前
Linux 线程控制
linux·数据结构·内核·线程·进程·线程控制
迎風吹頭髮5 小时前
Linux内核架构浅谈44-Linux slab分配器:通用缓存与专用缓存的创建与使用
linux·spring·架构
AORO20255 小时前
防爆手机与普通手机有什么区别?防爆手机哪个牌子好?
运维·服务器·网络·5g·智能手机·信息与通信
爱倒腾的老唐5 小时前
29、构建可视化日志管理服务器
运维·服务器