DIY Shell:探秘进程构建与命令解析的核心原理
个人主页:chian-ocean
文章专栏-Linux
前言:
Shell(外壳)是一个操作系统的用户界面,它提供了一种方式,使得用户能够与操作系统进行交互。Shell 是用户与操作系统之间的桥梁,允许用户通过命令行输入来执行各种操作,例如文件管理、程序执行、进程控制、系统监控等

常见的 Shell 类型:
Bash(Bourne Again Shell):- 是 Linux 和 macOS 等类 Unix 系统中常见的默认 Shell。它是 Bourne Shell 的增强版,支持丰富的特性,如命令补全、历史命令、数组等。
Zsh(Z Shell):- 是一个功能强大的 Shell,支持更丰富的自动化、命令补全、插件系统等特性。Zsh 常常被认为是最为用户友好的 Shell 之一。
Fish(Friendly Interactive Shell):- 是一个具有用户友好界面和丰富特性(如自动提示、自动补全等)的现代 Shell。其设计注重简洁和易用性。
C Shell(csh):- 基于 C 语言语法的 Shell,主要用于早期的 Unix 系统。C Shell 提供了较强的脚本编程功能。
Korn Shell(ksh):- 是一个功能强大的 Shell,结合了 Bourne Shell 和 C Shell 的特性,并且提供了很多增强的功能。
shell外壳的实现
引入头文件
#include<string>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<assert.h>
-
#include<string>:引入 C++ 的string库,用于字符串处理。 -
#include<unistd.h>:提供访问系统调用的接口,例如fork()、execvp()、getcwd()等。 -
#include<sys/wait.h>:包含等待子进程退出的函数。 -
#include<sys/types.h>:包含系统数据类型的定义,如pid_t(进程 ID 类型)。 -
#include<stdlib.h>:提供一些标准库函数,如exit()、getenv()和putenv()等。 -
#include<stdio.h>:提供输入输出函数,如printf()。 -
#include<string.h>:提供字符串操作函数,如strtok()、strcmp()等。 -
#include<assert.h>:提供调试宏assert(),用于检测程序中的错误
宏定义
#define DELIM " \t"
#define LEFT "["
#define RIGHT "]"
#define LABLE "$"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 4
-
DELIM:命令行参数的分隔符,包含空格和制表符。 -
LEFT,RIGHT,LABLE:格式化命令行提示符的符号,用于显示用户、主机和当前工作目录。 -
LINE_SIZE:最大命令行字符长度,设置为1024。 -
ARGC_SIZE:最大命令行参数数量,设置为32。 -
EXIT_CODE:用于退出的错误代码。
全局变量
int quit = 0;
int LASTCODE = 0;
char* argv[ARGC_SIZE];
char commondline[LINE_SIZE];
char pwd[ARGC_SIZE];
char myenv[ARGC_SIZE];
-
quit:控制程序是否退出的标志。 -
LASTCODE:记录上一个命令的退出状态码。 -
argv:存储命令行解析后的参数。 -
commondline:存储输入的命令行字符串。 -
pwd:存储当前工作目录路径。 -
myenv:存储环境变量。
const char* getusr()
{return getenv("USER");
}const char* gethostname()
{return getenv("HOSTNAME");
}
getusr:返回当前用户的用户名。gethostname:返回当前计算机的主机名。
获取当前工作目录
void getpwd()
{getcwd(pwd, sizeof(pwd));
}
getpwd:调用getcwd获取当前工作目录,并将结果存储在pwd中。
交互式输入处理
void ineract(char* cline, int size)
{getpwd();printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusr(), gethostname(), pwd);char* s = fgets(cline, size, stdin);assert(s);(void)s;cline[strlen(cline) - 1] = '\0';
}
该 ineract 函数是命令行交互的核心部分,用于显示提示符并获取用户输入。以下是对代码逐行的解析:
函数定义
void ineract(char* cline, int size)
cline:指向存储用户输入命令的字符数组的指针。size:输入缓冲区的大小,表示cline数组的最大容量。
获取当前工作目录并显示提示符
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusr(), gethostname(), pwd);
-
getpwd():调用getpwd函数来获取当前工作目录并存储到全局变量pwd中。 -
printf:显示命令行提示符。格式为[user@hostname pwd]$,其中:getusr():获取当前用户名(通过环境变量USER)。gethostname():获取当前主机名(通过环境变量HOSTNAME)。pwd:显示当前工作目录。
提示符通过格式化字符串显示,
LEFT和RIGHT用于添加方括号([和])包围信息,而LABLE是一个$字符,表示命令行提示符。
获取用户输入
char* s = fgets(cline, size, stdin);
assert(s);
(void)s;
fgets(cline, size, stdin):从标准输入(键盘)读取用户输入,存储在cline数组中,最多读取size-1个字符。fgets会自动在输入末尾添加一个\0来终止字符串。assert(s):如果fgets返回NULL,程序将终止并输出错误信息。assert是一种调试检查,确保输入读取成功。如果s为NULL,说明读取输入失败。(void)s:(void)s的作用是消除未使用变量s的编译器警告,实际上这里并没有做任何事情。
去除输入末尾的换行符
cline[strlen(cline) - 1] = '\0';
strlen(cline) - 1:计算输入字符串的长度,并将其最后一个字符(换行符\n)替换为字符串结束符\0。这一步去除fgets读取时可能留下的换行符。
命令行解析
int AnalyzeCommandLine(char* cline)
{int i = 0;argv[i++] = strtok(cline, DELIM);while (argv[i++] = strtok(NULL, DELIM));return i - 1;
}
AnalyzeCommandLine 函数用于解析输入的命令行字符串,并将解析出的各个命令参数存储在 argv 数组中。以下是对该函数的逐行解析:
函数定义
int AnalyzeCommandLine(char* cline)
cline:输入的命令行字符串(用户在命令行输入的完整命令)。该字符串将会被解析为多个命令和参数。
初始化参数索引
int i = 0;
i:定义一个整数变量i,用于跟踪argv数组的索引位置,表示当前解析的命令参数的位置。
使用 strtok 解析命令行
argv[i++] = strtok(cline, DELIM);
strtok(cline, DELIM):strtok 是一个字符串分割函数,它通过指定的分隔符(DELIM)将 cline 字符串分割成多个子字符串。DELIM 在此代码中定义为 " \t",即空格和制表符。
- 第一次调用
strtok()时,它会返回cline字符串中的第一个子字符串(即命令或第一个参数)。返回值会存储在argv[i]中。 - 然后
i++使得i增加 1,指向下一个位置
继续解析命令行参数
while (argv[i++] = strtok(NULL, DELIM));
strtok(NULL, DELIM):在第一次调用strtok()后,后续调用需要传入NULL作为第一个参数,表示继续从上次分割的位置开始。strtok()会继续根据分隔符分割剩余的命令行字符串,并返回下一个子字符串。- 这段代码通过
while循环逐个提取命令行中的每个子字符串,并将其存储到argv[i]中。每次调用strtok()后,i++将i指向下一个数组位置。
返回参数的数量
return i - 1;
i - 1:由于最后一次i++会多加一次,因此函数返回i - 1,即存储在argv数组中的参数个数(命令行中的参数数量)。
执行常规命令
void NormalExecl(char* _argv[])
{pid_t id = fork();if (id < 0){perror("fork");return;}else if (id == 0){execvp(_argv[0], argv);exit(EXIT_CODE);}else{int status = 0;pid_t rid = waitpid(id, &status, 0);if (id){LASTCODE = WEXITSTATUS(status);}}
}
函数定义
void NormalExecl(char* _argv[])
_argv[]:这是一个参数数组,用于传递命令及其参数。例如,_argv[0]是命令,_argv[1]是命令的第一个参数,依此类推。
创建子进程
pid_t id = fork();
fork():fork() 函数用于创建一个新进程。它将当前进程复制一份。新进程被称为子进程,原始进程是父进程。
- 如果
fork()成功,它会返回两次:- 父进程:返回子进程的进程 ID(PID)。
- 子进程:返回 0。
- 如果
fork()失败,它返回负值。
错误处理
if (id < 0)
{perror("fork");return;
}
id < 0:如果fork()返回负值,表示创建子进程失败。此时打印错误信息并返回。perror("fork"):输出错误信息,说明fork()失败的原因。
子进程执行命令
else if (id == 0)
{execvp(_argv[0], argv);exit(EXIT_CODE);
}
id == 0:这是子进程中的代码块。如果 fork() 返回 0,表示当前代码在子进程中执行。
execvp(_argv[0], argv):子进程调用 execvp() 函数来执行命令。execvp() 会用指定的命令替换当前进程的映像。具体来说:
_argv[0]是命令(例如ls)。argv是命令的参数数组,其中包含命令和它的所有参数(例如ls -l)。
exit(EXIT_CODE):如果 execvp() 失败,子进程会退出,返回 EXIT_CODE。如果 execvp() 成功,当前进程会被新的命令替代,exit() 不会被执行
父进程等待子进程结束
else
{int status = 0;pid_t rid = waitpid(id, &status, 0);if (id){LASTCODE = WEXITSTATUS(status);}
}
-
else:这是父进程中的代码块,父进程需要等待子进程结束并获取其退出状态。 -
int status = 0;:定义一个变量status用来存储子进程的退出状态。 -
waitpid(id, &status, 0):父进程使用
waitpid()函数等待子进程的结束。waitpid()会阻塞父进程,直到指定的子进程结束,并返回子进程的退出状态。id:是子进程的进程 ID,表示父进程等待这个子进程。&status:存储子进程退出时的状态信息。0:表示父进程等待子进程的退出,不对其状态做其他操作。
-
LASTCODE = WEXITSTATUS(status):获取子进程的退出状态码并存储在LASTCODE中。WEXITSTATUS(status)提取的是子进程的退出代码。
内建命令执行
int BuildExec(char* _argv[], int _argc)
{if (_argc == 2 && strcmp(_argv[0], "cd") == 0){chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 1;}else if (_argc == 2 && strcmp(_argv[0], "export") == 0){strcpy(myenv, _argv[1]);putenv(myenv);return 1;}else if (_argc == 2 && strcmp(_argv[0], "echo") == 0){if (strcmp(_argv[1], "$?")){printf("%d\n", LASTCODE);LASTCODE = 0;}else if (strcmp(_argv[1], "$")){char* val = getenv(_argv[1] + 1);printf("%s\n", val);}else{printf("%s\n", _argv[1]);}}if (strcmp(_argv[0], "ls") == 0){_argv[_argc++] = "--color";_argv[_argc] = NULL;}return 0;
}
函数定义
int BuildExec(char* _argv[], int _argc)
_argv[]:命令行解析后参数的数组,存储命令及其参数。_argc:命令行参数的数量。
处理 cd 命令
if (_argc == 2 && strcmp(_argv[0], "cd") == 0)
{chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 1;
}
strcmp(_argv[0], "cd") == 0:检查命令是否为cd。如果argv[0]是"cd",则执行以下操作。chdir(_argv[1]):改变当前工作目录到argv[1]指定的路径。getpwd():调用getpwd()获取新的工作目录并更新全局变量pwd。sprintf(getenv("PWD"), "%s", pwd):更新环境变量PWD,使其反映当前工作目录。return 1;:表示已经处理了cd命令,因此直接返回,不继续处理后面的代码。
处理 export 命令
else if (_argc == 2 && strcmp(_argv[0], "export") == 0)
{strcpy(myenv, _argv[1]);putenv(myenv);return 1;
}
strcmp(_argv[0], "export") == 0:检查命令是否为export。如果argv[0]是"export",则执行以下操作。strcpy(myenv, _argv[1]):将argv[1]的值复制到myenv字符数组中。argv[1]应该是一个环境变量的设置(例如"VAR=value")。putenv(myenv):使用putenv()将myenv中的环境变量设置添加到当前环境中。return 1;:表示已经处理了export命令,直接返回。
处理 echo 命令
else if (_argc == 2 && strcmp(_argv[0], "echo") == 0)
{if (strcmp(_argv[1], "$?")){printf("%d\n", LASTCODE);LASTCODE = 0;}else if (strcmp(_argv[1], "$")){char* val = getenv(_argv[1] + 1);printf("%s\n", val);}else{printf("%s\n", _argv[1]);}
}
strcmp(_argv[0], "echo") == 0:检查命令是否为echo。如果是,继续执行以下代码。strcmp(_argv[1], "$?"):检查是否要求输出上一个命令的退出状态码。如果argv[1]是"$?",则输出上一个命令的退出代码LASTCODE,并将LASTCODE重置为 0。strcmp(_argv[1], "$"):检查是否要求输出某个环境变量的值。如果argv[1]是以$开头(例如$HOME),则获取该环境变量的值并打印。printf("%s\n", _argv[1]);:如果既不是"$?"也不是以$开头,则直接输出argv[1],即用户传递给echo的字符串。
特殊处理 ls 命令
if (strcmp(_argv[0], "ls") == 0)
{_argv[_argc++] = "--color";_argv[_argc] = NULL;
}
strcmp(_argv[0], "ls") == 0:检查命令是否为ls。如果是ls命令,执行以下操作。_argv[_argc++] = "--color";:给ls命令添加--color参数,这样ls命令输出的文件列表会使用不同的颜色显示(通常是通过文件类型区分)。_argv[_argc] = NULL;:将数组最后一个元素设置为NULL,确保execvp()在执行时能正确处理参数数组。
返回值
return 0;
- 如果命令不是内建命令(
cd、export、echo)或者没有进行特殊处理(如ls),则返回 0,表示该命令需要外部执行。
主程序逻辑
int main()
{while (!quit){//命令行提示ineract(commondline, sizeof(commondline));//命令解析int argc = AnalyzeCommandLine(commondline);//指令解析int n = BuildExec(argv, argc);if (!n) NormalExecl(argv);}return 0;
}
main:主程序循环,不断提示用户输入命令。首先获取并解析命令行输入,然后判断是否为内建命令,若不是,则调用NormalExecl执行外部命令。直到quit被设置为 1 时,程序结束。
相关文章:
DIY Shell:探秘进程构建与命令解析的核心原理
个人主页:chian-ocean 文章专栏-Linux 前言: Shell(外壳)是一个操作系统的用户界面,它提供了一种方式,使得用户能够与操作系统进行交互。Shell 是用户与操作系统之间的桥梁,允许用户通过命令行…...
数据库备份、主从、集群等配置
数据库备份、主从、集群等配置 1 MySQL1.1 docker安装MySQL1.2 主从复制1.2.1 主节点配置1.2.2 从节点配置1.2.3 创建用于主从同步的用户1.2.4 开启主从同步1.2.4 主从同步验证 1.3 主从切换1.3.1 主节点设置只读(在192.168.1.151上操作)1.3.2 检查主从数…...
【数据采集】基于Selenium采集豆瓣电影Top250的详细数据
基于Selenium采集豆瓣电影Top250的详细数据 Selenium官网:https://www.selenium.dev/blog/ 豆瓣电影Top250官网:https://movie.douban.com/top250 写在前面 实验目标:基于Selenium框架采集豆瓣电影Top250的详细数据。 电脑系统:Windows 使用软件:PyCharm、Navicat 技术需求…...
(回溯递归dfs 电话号码的字母组合 remake)leetcode 17
只找边界条件和非边界条件,剩下的交给数学归纳法就行,考虑子问题的重复性 [class Solution {vector<string>str { "","","abc","def","ghi","jkl","mno","pqrs"…...
Redis --- 使用zset处理排行榜和计数问题
在处理计数业务时,我们一般会使用一个数据结构,既是集合又可以保证唯一性,所以我们会选择Redis中的set集合: 业务逻辑: 用户点击点赞按钮,需要再set集合内判断是否已点赞,未点赞则需要将点赞数1…...
响应式编程_04Spring 5 中的响应式编程技术栈_WebFlux 和 Spring Data Reactive
文章目录 概述响应式Web框架Spring WebFlux响应式数据访问Spring Data Reactive 概述 https://spring.io/reactive 2017 年,Spring 发布了新版本 Spring 5, Spring 5 引入了很多核心功能,这其中重要的就是全面拥抱了响应式编程的设计思想和实…...
C++ Primer 算术运算符
欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…...
中位数定理:小试牛刀> _ <2025牛客寒假1
给定数轴上的n个点,找出一个到它们的距离之和尽量小的点(即使我们可以选择不是这些点里的点,我们还是选择中位数的那个点最优) 结论:这些点的中位数就是目标点。可以自己枚举推导(很好想) (对于 点的数量为…...
一些常用的HTML结构
1. 页面基本结构 DOCTYPE 声明: 作用:告知浏览器使用哪种 HTML 版本进行解析。示例: <!DOCTYPE html><html> 标签: 作用:作为整个 HTML 文档的根元素,包含文档的头部和主体。示例࿱…...
js的 encodeURI() encodeURIComponent() decodeURI() decodeURIComponent() 笔记250205
js的 encodeURI() encodeURIComponent() decodeURI() decodeURIComponent() 在JavaScript中,处理URI编码和解码的四个关键函数为:encodeURI()、encodeURIComponent()、decodeURI()和decodeURIComponent()。它们分别用于不同的场景,具体区别和…...
安全实验作业
一 拓扑图 二 要求 1、R4为ISP,其上只能配置IP地址;R4与其他所有直连设备间均使用共有IP 2、R3-R5-R6-R7为MGRE环境,R3为中心站点; 3、整个OSPF环境IP基于172.16.0.0/16划分; 4、所有设备均可访问R4的环回&#x…...
《Python预训练视觉和大语言模型》:从DeepSeek到大模型实战的全栈指南
就是当代AI工程师的日常:* - 砸钱买算力,却卡在分布式训练的“隐形坑”里; - 跟着论文复现模型,结果连1/10的性能都达不到; - 好不容易上线应用,却因伦理问题被用户投诉…… 当所有人都在教你怎么调用…...
血压计OCR文字检测数据集VOC+YOLO格式2147张11类别
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):2147 标注数量(xml文件个数):2147 标注数量(txt文件个数):2147 …...
Java 面试合集(2024版)
种自己的花,爱自己的宇宙 目录 第一章-Java基础篇 1、你是怎样理解OOP面向对象??? 难度系数:? 2、重载与重写区别??? 难度系数:? 3、接口与抽象类的区别??? 难度系数:? 4、深拷贝与浅拷贝的理解??? 难度系数&…...
Typora免费使用
一.下载地址 https://typoraio.cn/ 二.修改配置文件 1.找到安装路径下的LicenseIndex.180dd4c7.4da8909c.chunk.js文件 文件路径为:安装路径\resources\page-dist\static\js\LicenseIndex.180dd4c7.4da8909c.chunk.js 将js中的 e.hasActivated"true"e.hasActiva…...
第一性原理:游戏开发成本的思考
利润 营收-成本 营收定价x销量x分成比例 销量 曝光量x 点击率x (购买率- 退款率) 分成比例 100%- 平台抽成- 税- 引擎费- 发行抽成 成本开发成本运营成本 开发成本 人工外包办公地点租金水电设备折旧 人工成本设计成本开发成本迭代修改成本后续内容…...
裁员潮血洗硅谷,普通人惨遭裁员的血泪教训——要随时做好失业的准备
我大学室友21年暑假在meta的某AI组实习过,压力巨大!组里大群天天消息99,年底实习结束直接就进到Google去了,听说eng组的intern十有八九都拿到了return offer,但都利用空余时间跳到了别的大厂。 离谱的时候,…...
MacBook Pro(M1芯片)Qt环境配置
MacBook Pro(M1芯片)Qt环境配置 1、准备 试图写一个跨平台的桌面应用,此时想到了使用Qt,于是开始了搭建开发环境~ 在M1芯片的电脑上安装,使用brew工具比较方便 Apple Silicon(ARM/M1…...
智能编码在前端研发的创新应用
一、前端开发实例 今天主要想分享一些关于大模型如何协助我们进行前端编码的实践。首先,让我们以一个前端开发的实例开始。通常,当需要实现一个新的前端功能时,我们会收到相关的背景和需求描述。我的期望是,大模型能够直接使用这…...
[特殊字符] ChatGPT-4与4o大比拼
🔍 ChatGPT-4与ChatGPT-4o之间有何不同?让我们一探究竟! 🚀 性能与速度方面,GPT-4-turbo以其优化设计,提供了更快的响应速度和处理性能,非常适合需要即时反馈的应用场景。相比之下,G…...
在Spring Cloud中将Redis共用到Common模块
前言 在分布式系统中,共用组件的设计可以极大地提升代码复用性和维护性。Spring Cloud中将Redis共用到一个公共模块(common模块)是一个常见的设计实践,这样可以让多个微服务共享相同的Redis配置和操作逻辑。本文将详细介绍如何在…...
基于RK3588/RK3576+MCU STM32+AI的储能电站电池簇管理系统设计与实现
伴随近年来新型储能技术的高质量规模化发展,储能电站作为新能源领域的重要载体, 旨在配合逐步迈进智能电网时代,满足电力系统能源结构与分布的创新升级,给予相应规模 电池管理系统的设计与实现以新的挑战。同时,电子系…...
Django框架丨从零开始的Django入门学习
Django 是一个用于构建 Web 应用程序的高级 Python Web 框架,Django是一个高度模块化的框架,使用 Django,只要很少的代码,Python 的程序开发人员就可以轻松地完成一个正式网站所需要的大部分内容,并进一步开发出全功能…...
稀疏混合专家架构语言模型(MoE)
注:本文为 “稀疏混合专家架构语言模型(MoE)” 相关文章合辑。 手把手教你,从零开始实现一个稀疏混合专家架构语言模型(MoE) 机器之心 2024年02月11日 12:21 河南 选自huggingface 机器之心编译 机器之心…...
spring基础总结
先修知识:依赖注入,反转控制,生命周期 IDEA快捷键 Ctrl Altm:提取方法,设置trycatch 通用快捷键: Ctrl F:在当前文件中查找文本。Ctrl R:在当前文件中替换文本。Ctrl Z:撤销…...
【C#】Process、ProcessStartInfo启动外部exe
在C#中使用 Process 和 ProcessStartInfo 类启动外部 .exe 文件,可以按照以下步骤进行: 创建 ProcessStartInfo 实例:配置进程启动信息,包括可执行文件的路径、传递给该程序的参数等。 设置启动选项:根据需要配置 Pro…...
【实用小技巧】如何不更新application.yml而更新spring的配置
大家都知道,我们在java工程中,常常在application.yml中有各种各样的运行时的配置,一般来说都是这样的结构: a:b:c: {ENV_NAME} 这样,我们在部署应用时,就可以通过在不同的局点修改ENV_NAME的值࿰…...
windows linux常用基础命令
windows基础命令 cd …/ (访问D盘 直接D: 进入目录cd…\baidudu) color 2 改变颜色 dir 浏览当前目录中有什么内容 例如 dir windows可以浏览windows中有什么文件 cls 清屏 cd windows 可以跳转到c盘目录的下面 cd…/可以返回到上一级目录 ./当前目录 cd \ 直…...
openRv1126 AI算法部署实战之——TensorFlow TFLite Pytorch ONNX等模型转换实战
Conda简介 查看当前系统的环境列表 conda env list base为基础环境 py3.6-rknn-1.7.3为模型转换环境,rknn-toolkit版本V1.7.3,python版本3.6 py3.6-tensorflow-2.5.0为tensorflow模型训练环境,tensorflow版本2.5.0,python版本…...
java进阶1——JVM
java进阶——JVM 1、JVM概述 作用 Java 虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对 应平台上的机器码指令行,每一条 java 指令,java 虚拟机中都有详细定义,如怎么取操 作数,…...
