DIY Shell:探秘进程构建与命令解析的核心原理

个人主页:chian-ocean

文章专栏-Linux

前言:

Shell(外壳)是一个操作系统的用户界面,它提供了一种方式,使得用户能够与操作系统进行交互。Shell 是用户与操作系统之间的桥梁,允许用户通过命令行输入来执行各种操作,例如文件管理、程序执行、进程控制、系统监控等

在这里插入图片描述

常见的 Shell 类型:

  1. Bash(Bourne Again Shell)
    • 是 Linux 和 macOS 等类 Unix 系统中常见的默认 Shell。它是 Bourne Shell 的增强版,支持丰富的特性,如命令补全、历史命令、数组等。
  2. Zsh(Z Shell)
    • 是一个功能强大的 Shell,支持更丰富的自动化、命令补全、插件系统等特性。Zsh 常常被认为是最为用户友好的 Shell 之一。
  3. Fish(Friendly Interactive Shell
    • 是一个具有用户友好界面和丰富特性(如自动提示、自动补全等)的现代 Shell。其设计注重简洁和易用性。
  4. C Shell(csh)
    • 基于 C 语言语法的 Shell,主要用于早期的 Unix 系统。C Shell 提供了较强的脚本编程功能。
  5. 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:显示当前工作目录。

    提示符通过格式化字符串显示,LEFTRIGHT 用于添加方括号([])包围信息,而 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 是一种调试检查,确保输入读取成功。如果 sNULL,说明读取输入失败。
  • (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;
  • 如果命令不是内建命令(cdexportecho)或者没有进行特殊处理(如 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 时,程序结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/12753.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

新春贺岁,共赴AGI之旅

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 往期精彩文章推荐 季姮教授独家文字版干货 | 面向知识渊博的大语言模型 关于AI TIME AI TIME源起于2019年&#xff0c;旨在发扬科学思辨精神&#xff0c;邀请各界人士对人工智能理论、算法和场景应用的本质问题…

FastAPI之参数传递和参数校验

FastAPI之参数传递 一、请求URL传参1、URL传参2、一个参数名&#xff0c;多个值3、参数校验3.1、默认值设置&#xff0c;和参数接口描述3.2、字符串长度校验3.3、正则表达式校验3.4、数值大小校验 二、请求体传参1、请求体单个传参 一、请求URL传参 1、URL传参 url请求参数是…

Vue Dom截图插件,截图转Base64 html2canvas

安装插件 npm install html2canvas --save插件使用 <template><div style"padding: 10px;"><div ref"imageTofile" class"box">发生什么事了</div><button click"toImage" style"margin: 10px;&quo…

C语言:深入了解指针3

1.回调函数是什么&#xff1f; 基本概念 回调函数就是⼀个通过函数指针调⽤的函数。 如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另⼀个函数&#xff0c;当这个指针被⽤来调⽤其所指向的函数 时&#xff0c;被调⽤的函数就是回调函数。回调函数不是由该函…

llama.cpp GGUF 模型格式

llama.cpp GGUF 模型格式 1. Specification1.1. GGUF Naming Convention (命名规则)1.1.1. Validating Above Naming Convention 1.2. File Structure 2. Standardized key-value pairs2.1. General2.1.1. Required2.1.2. General metadata2.1.3. Source metadata 2.2. LLM2.2.…

【C++】STL——vector底层实现

目录 &#x1f495; 1.vector三个核心 &#x1f495;2.begin函数&#xff0c;end函数的实现&#xff08;简单略讲&#xff09; &#x1f495;3.size函数&#xff0c;capacity函数的实现 &#xff08;简单略讲&#xff09; &#x1f495;4.reserve函数实现 &#xff08;细节…

Pinia状态管理

1、为什么要使用Pinia&#xff1f; Pinia 是 Vue 的存储库&#xff0c;它允许跨组件/页面共享状态 Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子&#xff0c;结合了 Vuex 5 核心团队讨论中的许多想法。最终&#xff0c;我们意识到 Pinia 已经实现了我们在 Vuex 5 中想…

TCP | RFC793

注&#xff1a;本文为 “ RFC793” 相关文章合辑。 RFC793-TCP 中文翻译 编码那些事儿已于 2022-07-14 16:02:16 修改 简介 翻译自&#xff1a; RFC 793 - Transmission Control Protocol https://datatracker.ietf.org/doc/html/rfc793 TCP 是一个高可靠的主机到主机之间…

VMware Workstation Pro安装了Ubuntu 24.04实现与Windows10之间的复制粘贴

windows10安装了VMware Workstation Pro&#xff0c;虚拟机上安装Ubuntu 24.04&#xff0c;想Ubuntu和windows之间实现复制粘贴&#xff0c;便于互相执行下面命令&#xff1a; sudo apt-get autoremove open-vm-tools //卸载已有的工具 sudo apt-get install open-vm-tools …

idea分析sql性能

idea对sql进行解析&#xff0c;可有效展示sql的性能问题&#xff0c;比直接看命令好。&#xff08;专业版才有数据库功能&#xff0c;可以在淘宝买&#xff0c;10块就好了&#xff09; 如下&#xff1a; 发现一个全表扫描&#xff0c;耗时6s&#xff0c;对应sql语句可以查看&…

智慧园区系统集成解决方案提升管理效率与智能化水平的新探索

内容概要 随着科技的不断进步&#xff0c;智慧园区管理系统已成为现代园区管理的重要组成部分。在众多系统中&#xff0c;快鲸智慧园区(楼宇)管理系统凭借其独特的优势&#xff0c;获得了广泛关注。该系统通过全面整合园区内各类智能设备&#xff0c;大幅提升了管理效率和智能…

Linux 的 sysfs 伪文件系统介绍【用户可以通过文件操作与内核交互(如调用内核函数),而无需编写内核代码】

1. 什么是 sysfs伪文件系统&#xff1f; sysfs 是 Linux 内核提供的 伪文件系统&#xff0c;用于向用户空间暴露内核对象的信息和控制接口。它是 procfs 的补充&#xff0c;主要用于管理 设备、驱动、内核子系统 等信息&#xff0c;使用户可以通过文件操作&#xff08;如用户空…

TCP编程

1.socket函数 int socket(int domain, int type, int protocol); 头文件&#xff1a;include<sys/types.h>&#xff0c;include<sys/socket.h> 参数 int domain AF_INET: IPv4 Internet protocols AF_INET6: IPv6 Internet protocols AF_UNIX, AF_LOCAL : Local…

springboot+vue+uniapp的校园二手交易小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

【PyQt】使用PyQt5和Matplotlib实现的CSV数据可视化工具

使用PyQt5和Matplotlib实现的CSV数据可视化工具 界面展示 代码 import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,QHBoxLayout, QPushButton, QComboBox, QFileDialog,QLabel, QMessageBox) import pandas as pd from matplotlib.f…

软件工程导论三级项目报告--《软件工程》课程网站

《软件工程》课程网站 摘要 本文详细介绍了《软件工程》课程网站的设计与实现方案&#xff0c;包括可行性分析、需求分析、总体设计、详细设计、测试用例。首先&#xff0c;通过可行性分析从各方面确认了该工程的可实现性&#xff0c;接着需求分析明确了系统的目标用户群和功能…

数据结构-堆和PriorityQueue

1.堆&#xff08;Heap&#xff09; 1.1堆的概念 堆是一种非常重要的数据结构&#xff0c;通常被实现为一种特殊的完全二叉树 如果有一个关键码的集合K{k0,k1,k2,...,kn-1}&#xff0c;把它所有的元素按照完全二叉树的顺序存储在一个一维数组中&#xff0c;如果满足ki<k2i…

Spring @Lazy:延迟初始化,为应用减负

在Spring框架中&#xff0c;Lazy注解的作用非常直观&#xff0c;它就是用来告诉Spring容器&#xff1a;“嘿&#xff0c;这个Bean嘛&#xff0c;先别急着创建和初始化&#xff0c;等到真正需要用到的时候再弄吧&#xff01;” 默认情况下&#xff0c;Spring容器在启动时会立即创…

SynchronousQueue 与 LinkedBlockingQueue区别及应用场景

文章目录 前言认识SynchronousQueue基本对比及比较1. **基本特性**2. **内部实现**3. **性能特点**4. **使用场景**5. **总结对比** SynchronousQueue案例JDK应用案例案例1&#xff1a;SynchronousQueue的简单用例案例2&#xff1a;SynchronousQueue公平锁、非公平锁案例案例3&…

MySQL 缓存机制与架构解析

目录 一、MySQL缓存机制概述 二、MySQL整体架构 三、SQL查询执行全流程 四、MySQL 8.0为何移除查询缓存&#xff1f; 五、MySQL 8.0前的查询缓存配置 六、替代方案&#xff1a;应用层缓存与优化建议 总结 一、MySQL缓存机制概述 MySQL的缓存机制旨在提升数据访问效率&am…