当前位置: 首页 > news >正文

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

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

目录

简易版:

【一】打印命令行

【二】输入命令行

【三】解析命令行

【四】父子进程创建

【五】进程替换

【六】子进程回收

【七】封装整理

挑战版:

【一】解决cd指令

【二】解决echo命令


简易版:

【一】打印命令行

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

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

【二】输入命令行

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

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

函数原型:

char *fgets(char *str, int n, FILE *stream);

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

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

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

例如:

#define MAX 32char 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

函数原型:

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

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

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

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

返回值:

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

没有更多子串:返回 NULL

例如:

#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);
}

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

【四】父子进程创建

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

pid_t d =fork();
if(d==0)
{.... 
}
else
{....
}

【五】进程替换

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

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

//argv就是提取的字符串数组//进程替换
if(argv[0]==NULL)
{return 0;
}
int count =  execvp(argv[0],argv);
if(count<0)perror("execvp failed");
exit(0);

【六】子进程回收

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

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

【七】封装整理

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

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>#define LEFT "["
#define RIGHT "]"
#define MAX 32int 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只能用在循环里面【狗头】

int chdir(const char *path);//参数为目标路径

执行逻辑:

//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],就可以直接打印出来

//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 指令,这里就不展示了!正确处理环境变量即可!

http://www.dtcms.com/a/494290.html

相关文章:

  • 网站 营销方案怎么在网站上添加广告代码
  • 前端面试题+算法题(三)
  • 吕口*音乐多销*-程序系统方案
  • 分享一个基于Java和Spring Boot的产品售后服务跟踪平台设计与实现,源码、调试、答疑、lw、开题报告、ppt
  • 上海AiLab扩散策略赋能具身导航!NavDP:基于特权信息的仿真到现实导航扩散策略
  • iOS 发布全流程详解,从开发到上架的流程与跨平台使用 开心上架 发布实战
  • 无线充电的工作原理是什么样子的呢?
  • led高端网站建设seo外链技巧
  • Cross Product / Vector Product / 向量外积 / 叉积 / 矢量外积 可理解为一个意思
  • 如何在 Mac 上恢复已删除的文件(包括清空了垃圾箱方法)
  • JavaScript学习第二天:常量与数据类型
  • perf 子系统宏观认知
  • P14137 「SFMOI Round II」Strange Covering Game 题解
  • 进程的状态
  • macOS 基本使用
  • 前端最新Vue2+Vue3基础入门到实战项目11-13
  • 【Linux】Linux 进程通信:System V 共享内存(最快方案)C++ 封装实战 + 通信案例,4 类经典 Bug 快速修复
  • Windows进程-dllhost.exe
  • Linux小课堂: 群组管理与文件权限控制
  • 5-4〔OSCP ◈ 研记〕❘ SQL注入攻击▸基于 UNION 的SQLi
  • 黑龙江住房建设部网站qwins是哪个网站做的
  • Spring容器的refresh()方法
  • 接口测试难点总结
  • 《C++ Stack 与 Queue 完全使用指南:基础操作 + 经典场景 + 实战习题》
  • php 网站换空间网站打开慢如何优化
  • html5商城网站模板泰州网站制作工具
  • 浅谈SQL审核(一):SQL审核实现方式与常见工具的选择
  • 贪玩手游官方网站论文答辩免费ppt模板下载
  • Linux 上可以同时安装并运行 MySQL 和 PostgreSQL
  • Python Arrow库:告别datetime繁琐,优雅处理时间与时区