深入了解linux系統—— 自定義shell

shell的原理

我們知道,我們程序啟動時創建的進程,它的父進程都是bash也就是shell命令行解釋器;

bash都做了哪些工作呢?

根據已有的知識,我們可以簡單理解為:

  1. 輸出命令行提示符
  2. 獲取并解析我們輸入的指令
  3. 執行內建命令或者創建子進程執行命令

在這里插入圖片描述

就如下圖所示,bash讀取我們輸入的命令,并進行解析;然后創建子進程執行命令(bash等待子進程退出)。

在這里插入圖片描述

自定義shell實現

根據上述bash的工作原理,我們現在實現一個簡單的自定義shell

要想實現一個自定義shell,我們就要執行以下過程:

  • 獲取命令行
  • 解析命令行
  • 創建子進程,讓子進程執行命令(使用程序替換)
  • shell等待子進程退出

當然,還存在一部分內建命令,它是由bash自主實現的;我們要進行特殊處理;

1. 輸出命令行提示符

在實現自定義shell之前,我們來看

在這里插入圖片描述

我們的bash在每次都會輸出命令行提示符,然后等待我們用戶輸入;

看這個命令行提示符,它包含以下信息:

  • 用戶名USER
  • 主機名HOSTNAME
  • 當前工作路徑PWD

這些在我們的環境變量表中都能夠找到,所以我們就可以使用getenv來獲取。

在這里插入圖片描述

所以這個就非常容易實現了,直接按照格式輸出即可;

這樣我們需要獲取環境變量USERHOSTNAMEPWD等;

但是我們會發現bash輸出的命令行提示符中的當前工作路徑只有當前文件,而我們通過環境變量PWD獲取的是當前工作目錄的絕對路徑,所以我們這里要進行一下分割;

詳細代碼如下:

//命令行提示符格式
#define CLP "[%s@%s %s]#"
//命令行提示符的最大長度
#define MAX_CLP 100
//獲取環境變量
const char* GetUser(){return getenv("USER");
}
const char* GetHostName(){return getenv("HOSTNAME");
}
const char* GetPwd()
{return getenv("PWD");
}
//分割路徑
//"/home/lxb/linux/MYSHELL" --> "MYSHELL"
string DirPwd(char s[])
{
#define SLASH "/"string str = s;if(str == SLASH) return str;                                                                       auto pos = str.rfind(SLASH);if(pos == std::string::npos) return "err";return str.substr(pos+1);
}         
//生成命令行提示符
void CommandLinePrompt(char buffer[])
{sprintf(buffer,CLP,GetUser(),GetHostName(),DirPwd(GetPwd());
}
//輸出命令行提示符
void PrintCommandPrompt()
{char buffer[100];CommandLinePrompt(buffer);printf("%s",buffer);fflush(stdout);
}

在這里插入圖片描述

2. 獲取用戶輸入的信息

輸出了命令行提示符,接下來就要獲取用戶輸入的信息了,也就是輸入的命令;

在這里插入圖片描述

在用戶輸入時,是會輸入空格的,所以這里我們不能使用scanf/cin進行輸入;我們要使用fgets進行輸入。

而也可能存在只輸入一個回車的情況,所以我們要進行特殊判斷:當只輸入一個回車時就再次輸出命令行提示符,然后等待用戶輸入。

輸入:

//命令行信息最大長度    
#define MAX_COMLINE 1024
char* GetCommandLine(char buff[]){char* c = fgets(buff,MAX_COMLINE,stdin);buff[strlen(buff)-1] = 0;//處理回車return c;
}

這里來測試一下輸出命令行提示符和獲取用戶輸入信息;

如果獲取用戶輸入信息成功,那就輸出獲取的輸入信息,如果失敗或者只輸入了一個回車就再次輸出命令行提示符,然后等待用戶輸入。

int main()
{while(1){//1. 輸出命令行提示符PrintCommandPrompt();//2. 獲取用戶輸入信息char buff[MAX_COMLINE];char* c = GetCommandLine(buff);if(c == NULL)//讀取用戶輸入信息失敗continue;if(strlen(buff) == 0)//只輸入了空格continue;printf("%s\n",buff);}return 0;
}

在這里插入圖片描述

3. 命令行解析

獲取了用戶輸入的信息,但是我們獲得的是一個字符串,而我們要想執行用戶輸入的命令,要先對這個字符串進行解析;生成對應的命令行參數表,才能夠去執行。

命令行參數個數g_argc,命令行參數表g_argv;我們可以設置成全局的,這樣每次通過修改argcargv中最后一個指針為NULL即可。

這里,我們可以使用strtok函數進行分割命令行參數;

簡單描述一下strtok,在str字符串中查找sep字符串的內容,找到并將其修改成\0并返回指向這個字符串的指針。

在這里插入圖片描述

在分割完成之后,我們直接讓g_argv命令行參數表指向對應位置即可。

在這里插入圖片描述

#define MAX_ARGC 50
//命令行參數表
int g_argc;
char* g_argv[MAX_ARGC];
//解析命令行參數                                                               
//"ls -a -l"--> "ls" "-a" "-l"                                                      
void PrasCommandLine(char buff[]){                                  g_argc = 0;                                          const char* sep = " ";      for(g_argv[g_argc] = strtok(buff,sep);g_argv[g_argc] != NULL; g_argv[g_argc] = strtok(NULL,sep))    g_argc++;    
}   

這里還是測試,命令行解析是否成功。

在這里插入圖片描述

4. 創建子進程執行命令

解析命令行,生成命令行參數表之后,現在就是去執行命令了;

我們的shell并不是自己去執行,而是創建子進程,然后讓子進程去執行命令,shell等待子進程退出。

void CreateChildExecute(){    int id = fork();    if(id < 0)    {    perror("fork");    exit(1);    }    else if (id == 0){    //child    execvp(g_argv[0],g_argv);    exit(2);    }    //parent    wait(NULL);    
}

這里我們使用的程序替換函數是execvp,我們有命令行參數表(數組),而且我們輸入的系統命令是不帶路徑的;

看一下運行效果:

在這里插入圖片描述

擴展部分

在上述描述中,簡單的shell運行就OK了;

但是上述我們沒有考慮內建命令環境變量表等這些東西;

環境變量表

bash啟動時,它的環境變量表從我們系統的配置文件中來,但是我們這里沒辦法從系統配置文件中讀;所以我們這里就只能從父進程bash獲取環境變量表;

這里即從bash中獲取環境變量;

但是拿到了環境變量表,進程中還是保存的來自父進程bash的環境變量;environ還是執行bash的環境變量表。

我們需要導出環境變量,使用putenv來導出環境變量;然后讓environ執行我們的環境遍歷表。

//環境變量表最大數量
#define MAX_GENV 500
int g_argc;
char* g_argv[MAX_GARGC];
//環境變量表
int g_envs;    
char* g_env[MAX_GENV];    
//導入環境變量    
void EnvInit(){      extern char** environ;    memset(g_env,0,sizeof(g_env));    g_envs = 0;                              //環境變量表要從系統文件中來             //這從bash中獲取    for(int i = 0;environ[i]!=NULL;i++){    g_env[i] = (char*) malloc(strlen(environ[i])+1);    if(g_env[i] == NULL){    perror("malloc");    exit(3);    }    strcpy(g_env[i], environ[i]);    g_envs++;    }    g_env[g_envs] = NULL;    //導出環境變量    for(int i = 0;i < g_envs;i++){    putenv(g_env[i]);    }    environ = g_env;                                                                                                                                                                              
}

在我們程序啟動時,從父進程bash獲取環境變量即可。

內建命令

內建命令,指bash不創建子進程去執行,而是bash自己去執行的命令;

我們現在知道內建命令有cdexportecho等。

cd

cd命令,仔細想一想,肯定不會是子進程執行的;因為子進程執行它修改的是子進程的工作路徑。

我們要讓shell去執行cd命令,肯定不能使用程序替換了,我們可以使用chdir系統調用來修改當前工作路徑;

在這里插入圖片描述

cd命令:

  1. cd:會進入用戶的家目錄
  2. cd ~:進入用戶的家目錄
  3. cd where:進入指定路徑
  4. cd -:進入上次的工作路徑
void CD(){std::string oldpwd = getenv("PWD");std::string where;if(g_argc == 1){where = GetHome();if(where.empty()) return;chdir(where.c_str());                                                                                                                                                                     }else{where = g_argv[1];if(strcmp("-", g_argv[1]) == 0){where = getenv("OLDPWD");}else if(strcmp("~", g_argv[1]) == 0){where = GetHome();if(where.empty()) return;}chdir(where.c_str());//修改環境變量}
}

當然呢,這里存在一個問題,當我們cd -進入上次各種目錄時就會發現,它進入的一直都是同一個目錄;

因為我們這里沒有修改環境變量OLDPWD

echo

echo命令也是內建命令,我們知道,echo $?可以查看最近一次進程退出時的退出碼;

但是在我們的shell中,如果讓子進程去執行echo $?,它則是直接輸出$?

在這里插入圖片描述

echo $?,查看最近一次進程退出時的退出碼;而這些退出碼在哪里呢?

肯定不會在子進程中,那就在bash中了;

所以在我們的shell中,我們可以定義一個全局變量,每次執行一次命令就對其進行一次修改。

//最近一次進程退出時的退出碼
int last_code;
void Echo(){                                                      if(g_argc == 2){    std::string str = g_argv[1];    if(str == "$?"){    std::cout<<last_code<<std::endl;    }    else if(str[1] == '$'){    std::string env_name = str.substr(1);    const char* s = getenv(env_name.c_str());    if(s)    std::cout<<s<<std::endl;    }    else{    std::cout<<str<<std::endl;    }    }    
}    

這里,設置了last_code,那在每次執行命令之后,都要進行更新last_code

除此之外呢,還有非常多的內建命令,比如exportunset等;這里就不實現了。

別名alias

如果測試我們可以發現,bash支持ll,而我們的shell是不支持的;

我們知道ll是別名,所以如果想要我們shell支持別名,我們就要在shell中新增一張別名表;

然后維護這張別名表,就可以支持ll等指令的別名了。

這里就不實現了,可以使用unordered_map或者map來存儲這張別名表。

到這里本篇文章大致內容就結束了;

本篇文章自定義實現shell,幫助理解進程,以及bash是如何工作的

附源碼:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <cstdbool>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
//命令行提示符格式
#define CLP "[%s@%s %s]# "
#define MAX_CLP 100
//命令行信息最大長度
#define MAX_COMLINE 1024
//命令行參數最大個數
#define MAX_GARGC 50
//環境變量表最大數量
#define MAX_GENV 500
int g_argc;
char* g_argv[MAX_GARGC];
//環境變量表
int g_envs;
char* g_env[MAX_GENV];
//最近一次進程退出時的退出碼
int last_code = 0;
//導入環境變量
void EnvInit(){extern char** environ;memset(g_env,0,sizeof(g_env));g_envs = 0;//環境變量表要從系統文件中來                                                                                                                                                                  //這從bash中獲取for(int i = 0;environ[i]!=NULL;i++){g_env[i] = (char*) malloc(strlen(environ[i])+1);if(g_env[i] == NULL){perror("malloc");exit(3);}strcpy(g_env[i], environ[i]);g_envs++;                                                                                                                                                                                 }g_env[g_envs] = NULL;//導出環境變量for(int i = 0;i < g_envs;i++){putenv(g_env[i]);}environ = g_env;
}
//獲取環境變量
char* GetUser(){return getenv("USER");
}
char* GetHostName(){return getenv("HOSTNAME");
}
//路徑切割
std::string DirPwd(const char s[])
{
#define SLASH "/"std::string str = s;if(str == SLASH) return str;auto pos = str.rfind(SLASH);if(pos == std::string::npos) return "err";return str.substr(pos+1);
}
const char* GetPwd()
{//return getenv("PWD");return DirPwd(getenv("PWD")).c_str();
}
const char* GetHome(){return getenv("HOME");                                                  
}
//生成命令行提示符
void CommandLinePrompt(char buffer[])
{sprintf(buffer,CLP,GetUser(),GetHostName(),GetPwd());//sprintf(buffer,CLP,GetUser(),GetHostName(),DirPwd(GetPwd()).c_str());
}
void PrintCommandPrompt()
{char buffer[100];CommandLinePrompt(buffer);printf("%s",buffer);fflush(stdout);
}
char* GetCommandLine(char buff[]){char* c = fgets(buff,MAX_COMLINE,stdin);buff[strlen(buff)-1] = 0;return c;
}
void PrasCommandLine(char* buff){g_argc = 0;const char* sep = " ";for(g_argv[g_argc] = strtok(buff,sep); g_argv[g_argc] != NULL; g_argv[g_argc] = strtok(NULL,sep)){g_argc++;}
}
void CreateChildExecute(){int id = fork();if(id < 0){perror("fork");exit(1);                                                                                                                                                                                  }else if (id == 0){//childexecvp(g_argv[0],g_argv);exit(2);}//parentint status = 0;int rid = wait(&status);if(rid > 0)last_code = WEXITSTATUS(status);
}
void Cd(){std::string oldpwd = getenv("PWD");std::string where;if(g_argc == 1){where = GetHome();if(where.empty()) return;                                                                                                                                                                 chdir(where.c_str());}else{where = g_argv[1];if(strcmp("-", g_argv[1]) == 0){where = getenv("OLDPWD");}else if(strcmp("~", g_argv[1]) == 0){where = GetHome();if(where.empty()) return;}chdir(where.c_str());//修改環境變量}//std::string old = std::string("OLDPWD=") + oldpwd;//char* arr = (char*)malloc(old.size()+1);//for(size_t i = 0;i<old.size();i++){//    arr[i] = old[i];//}//arr[old.size()] = 0;//putenv(arr);
}
void Echo(){if(g_argc == 2){                                                                                                                                                                              std::string str = g_argv[1];if(str == "$?"){std::cout<<last_code<<std::endl;}else if(str[1] == '$'){std::string env_name = str.substr(1);const char* s = getenv(env_name.c_str());if(s)std::cout<<s<<std::endl;}else{std::cout<<str<<std::endl;}}
}
//判斷內建命令
bool BinCommand(){std::string str = g_argv[0];if(str == "cd"){Cd();last_code = 0;return true;}else if(str == "echo"){Echo();last_code = 0;return true;}return false;            
}
void PrintArgv(){for(int i = 0;i < g_argc; i++){printf("g_argv[%d] : %s\n",i,g_argv[i]);}
}
void PrintEnv(){for(int i = 0; i < g_envs;i++){printf("g_env[%d] : %s\n",i,g_env[i]);}
}
int main()
{//獲取環境變量表EnvInit();//PrintEnv();while(1){                                                                                                                                                                                     //1. 輸出命令行提示符PrintCommandPrompt();//2. 獲取用戶輸入信息char buff[MAX_COMLINE];char* c = GetCommandLine(buff);if(c == NULL)//讀取用戶輸入信息失敗continue;if(strlen(buff) == 0)//只輸入了空格continue;//3. 命令行解析PrasCommandLine(buff);//4.內建命令if(BinCommand())continue;//5. 創建子進程執行命令CreateChildExecute();}return 0;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/80568.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/80568.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/80568.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Redux和Vuex

為什么React和Vue需要Redux和Vuex 狀態管理需求的演變 #mermaid-svg-GaKl3pkZ82yc1m8E {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GaKl3pkZ82yc1m8E .error-icon{fill:#552222;}#mermaid-svg-GaKl3pkZ82yc1m8E…

Kubernetes排錯(十三):Pod間偶發超時問題排查

在微服務架構中&#xff0c;Pod間偶發的通信超時是最令人頭疼的問題之一。本文將通過生產環境中的真實案例&#xff0c;手把手教你定位這類"幽靈問題"。 一、快速定位問題方向&#xff08;5分鐘縮小范圍&#xff09; 1. 基礎檢查三板斧 # 檢查Service與Endpoint映…

Nginx 源碼安裝成服務

一、環境準備 一臺裝有 CentOS 7.9 的虛擬機&#xff08;IP: 192.168.40.81&#xff09;nginx-1.21.6.tar.gz 安裝包一個&#xff08;版本隨意&#xff09; 二、安裝 1&#xff09;解壓 nginx-1.21.6.tar.gz tar -xzvf nginx-1.21.6.tar.gz -tar&#xff1a;這是一個在 Linu…

L51.【LeetCode題解】438. 找到字符串中所有字母異位詞(四種方法)

目錄 1.題目 2.分析 暴力解法 方法1:排序(超時) 方法2:哈希表(險過) ★判斷兩個哈希表是否相同算法(通用方法,必須掌握) 能相等的前提:兩個哈希表的大小相等 哈希表有迭代器,可以使用范圍for從頭到尾遍歷 提交結果 優化方法:定長滑動窗口 提交結果 使用哈希數組更快…

Qt模塊化架構設計教程 -- 輕松上手插件開發

概述 在軟件開發領域,隨著項目的增長和需求的變化,保持代碼的可維護性和擴展性變得尤為重要。一個有效的解決方案是采用模塊化架構,尤其是利用插件系統來增強應用的功能性和靈活性。Qt框架提供了一套強大的插件機制,可以幫助開發者輕松實現這種架構。 模塊化與插件系統 模…

深入理解 HashMap 的索引計算:右移與異或的作用

在 Java 中&#xff0c;HashMap 是一種高效的數據結構&#xff0c;它通過將鍵映射到數組中的索引位置來實現快速的插入和查找。但之前看源碼總是理解到它要hash之后散列到數組中某一個位置&#xff0c;但卻從未深究它究竟怎么散列的&#xff0c;如果不夠散那就意味著hash沖突增…

overleaf較高級的細節指令

換行命令 原來代碼是將三個矩陣表達式在同一行顯示&#xff0c;使用aligned環境&#xff08;需引入amsmath宏包&#xff0c;一般文檔導言區默認會引入&#xff09;&#xff0c;把三個矩陣的定義分別放在不同行&#xff0c;可通過\\換行。 對齊命令 &放在等號前&#xff0…

LiteLLM:統一API接口,讓多種LLM模型調用如臂使指

在人工智能迅猛發展的今天,各種大語言模型(LLM)層出不窮。對開發者而言,如何高效集成和管理這些模型成為一個棘手問題。LiteLLM應運而生,它提供了一個統一的API接口,讓開發者可以輕松調用包括OpenAI、Anthropic、Cohere等在內的多種LLM模型。本文將深入介紹LiteLLM的特性、…

Google語法整理

以下是從整理出的 Google 語法&#xff1a; site&#xff1a;指定域名&#xff0c;如 “apache site:bbs.xuegod.cn”&#xff0c;可查詢網站的收錄情況 。 inurl&#xff1a;限定在 url 中搜索&#xff0c;如 “inurl:qq.txt”&#xff0c;可搜索 url 中包含特定內容的頁面&a…

python 寫一個工作 簡單 番茄鐘

1、圖 2、需求 番茄鐘&#xff08;Pomodoro Technique&#xff09;是一種時間管理方法&#xff0c;由弗朗西斯科西里洛&#xff08;Francesco Cirillo&#xff09;在 20 世紀 80 年代創立。“Pomodoro”在意大利語中意為“番茄”&#xff0c;這個名字來源于西里洛最初使用的一個…

Compose Multiplatform iOS 穩定版發布:可用于生產環境,并支持 hotload

隨著 Compose Multiplatform 1.8.0 的發布&#xff0c;iOS 版本也引來的第一個穩定版本&#xff0c;按照官方的原話&#xff1a;「iOS Is Stable and Production-Ready」 &#xff0c;而 1.8.0 版本&#xff0c;也讓 Kotlin 和 Compose 在移動端有了完整的支持。 在 2023 年 4 …

Jenkins 服務器上安裝 Git

安裝 Git # 更新包列表 sudo apt update# 安裝 Git sudo apt install git 驗證安裝 # 檢查 Git 版本 git --version 查看所有全局配置 git config --global --list 查看特定配置項 # 查看用戶名配置 git config --global user.name# 查看郵箱配置 git config --global u…

OpenHarmony SystemUI開發——實現全局導航欄和狀態欄關閉

在實際生產中&#xff0c;進場遇到需要關閉導航欄和狀態欄的需求&#xff0c;現分享解決辦法&#xff1a; 開發環境 OpenHarmony 5.0.0r 代碼分析 思路&#xff1a; launcher本身可以關閉 導航欄&#xff08;實際是 公共事件&#xff0c;發送消息給systemUI來實控制&#x…

大模型微調終極方案:LoRA、QLoRA原理詳解與LLaMA-Factory、Xtuner實戰對比

文章目錄 一、微調概述1.1 微調步驟1.2 微調場景 二、微調方法2.1 三種方法2.2 方法對比2.3 關鍵結論 三、微調技術3.1 微調依據3.2 LoRA3.2.1 原理3.2.2 示例 3.3 QLoRA3.4 適用場景 四、微調框架4.1 LLaMA-Factory4.2 Xtuner4.3 對比 一、微調概述 微調&#xff08;Fine-tun…

單片機-STM32部分:10-2、邏輯分析儀

飛書文檔https://x509p6c8to.feishu.cn/wiki/VrdkwVzOnifH8xktu3Bcuc4Enie 安裝包如下&#xff1a;根據自己的系統選擇&#xff0c;目前這個工具只有window版本哦 安裝方法比較簡單&#xff0c;都按默認下一步即可&#xff0c;注意不要安裝到中文路徑哦。 其余部分參考飛書文檔…

uniapp-商城-48-后臺 分類數據添加修改彈窗bug

在第47章的操作中&#xff0c;涉及到分類的添加、刪除和更新功能&#xff0c;但發現uni-popup組件存在bug。該組件的函數接口錯誤導致在小程序中出現以下問題&#xff1a;1. 點擊修改肉類名稱時&#xff0c;回調顯示為空&#xff0c;并報錯“setVal is not defined”&#xff0…

STM32-ADC模數轉換器(7)

目錄 一、ADC簡介 二、逐次逼近型ADC 三、ADC基本結構圖 四、規則組的四種轉換模式 五、轉換時間 對GPIO來說&#xff0c;它只能讀取引腳的高低電平&#xff0c;使用了ADC模數轉化器之后&#xff0c;就可以對高電平和低電平之間的任意電壓進行量化&#xff0c;最終用一個變…

智能商品推薦系統技術路線圖

智能商品推薦系統技術路線圖 系統架構圖 --------------------------------------------------------------------------------------------------------------- | 用戶交互層 (Presentation Layer) …

【Docker系列】docker inspect查看容器部署位置

&#x1f49d;&#x1f49d;&#x1f49d;歡迎來到我的博客&#xff0c;很高興能夠在這里和您見面&#xff01;希望您在這里可以感受到一份輕松愉快的氛圍&#xff0c;不僅可以獲得有趣的內容和知識&#xff0c;也可以暢所欲言、分享您的想法和見解。 推薦:kwan 的首頁,持續學…

標量/向量/矩陣/張量/范數詳解及其在機器學習中的應用

標量&#xff08;Scalar&#xff09;、向量&#xff08;Vector&#xff09;、矩陣&#xff08;Matrix&#xff09;、張量&#xff08;Tensor&#xff09;與范數&#xff08;Norm&#xff09;詳解及其在機器學習中的應用 1. 標量&#xff08;Scalar&#xff09; 定義&#xff1…