【Linux學習筆記】進程替換和自定義shell
🔥個人主頁:大白的編程日記
🔥專欄:Linux學習筆記
文章目錄
- 【Linux學習筆記】進程替換和自定義shell
- 前言
- 一.進程程序替換
- 1.1 替換原理
- 1.2 替換函數
- 1.2.1函數解釋
- 1.2.2命名理解
- 二.自主Shell命令行解釋器
- 2.1 模塊框架圖
- 2.2 目標
- 2.3 實現原理
- 2.4 全局變量
- 2.5 環境變量函數
- 2.6 初始化環境變量表函數
- 2.7 輸出命令行提示符模塊
- 2.8 提取命令輸入模塊
- 2.9 填充命令行參數表模塊
- 2.10 檢測并處理內建命令模塊
- 2.11 執行命令模塊
- 2.12 源碼
- 三 總結
- 后言
前言
哈嘍,各位小伙伴大家好!上期我們講了進程地址空間 今天我們講的是進程替換和自定義shell。話不多說,我們進入正題!向大廠沖鋒!
一.進程程序替換
fork()之后,父子各自執行父進程代碼的一部分如果子進程就想執行一個全新的程序呢?進程的程序替換來完成這個功能!程序替換是通過特定的接口,加載磁盤上的一個全新的程序(代碼和數據),加載到調用進程的地址空間中!
1.1 替換原理
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變。
1.2 替換函數
其實有六種以exec開頭的函數,統稱exec函數:
#include <unistd.h>int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ...);int execle(const char *path, const char *arg, ...,char *const envp[]);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execve(const char *path, char *const argv[], char *const envp[]);
1.2.1函數解釋
- 這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回。
- 如果調用出錯則返回-1
- 所以exec函數只有出錯的返回值而沒有成功的返回值。
1.2.2命名理解
這些函數原型看起來很容易混,但只要掌握了規律就很好記。
- I(list):表示參數采用列表
- v(vector):參數用數組
- p(path):有p自動搜索環境變量PATH
- e(env):表示自己維護環境變量
函數名 | 參數格式 | 是否帶路徑 | 是否使用當前環境變量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,須自己組裝環境變量 |
execv | 數組 | 不是 | 是 |
execvp | 數組 | 是 | 是 |
execve | 數組 | 不是 | 不是,須自己組裝環境變量 |
exec調用舉例如下:
#include <unistd.h>int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 帶p的,可以使用環境變量PATH,無需寫全路徑execlp("ps", "ps", "-ef", NULL);// 帶e的,需要自己組裝環境變量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 帶p的,可以使用環境變量PATH,無需寫全路徑execvp("ps", argv);// 帶e的,需要自己組裝環境變量execve("/bin/ps", argv, envp);exit(0);
}
事實上,只有execve是真正的系統調用,其它五個函數最終都調用execve,所以execve在man手冊第2節,其它函數在man手冊第3節。這些函數之間的關系如下圖所示。下圖exec函數簇一個完整的例子:
二.自主Shell命令行解釋器
2.1 模塊框架圖
2.2 目標
- 要能處理普通命令
- 要能處理內建命令
- 要能幫助我們理解內建命令/本地變量/環境變量這些概念
- 要能幫助我們理解shell的允許原理
2.3 實現原理
考慮下面這個與shell典型的互動:
[root@localhost epoll]# lsclient.cpp readme.md server.cpp utility.h[root@localhost epoll]# psPID TTY
TIME CMD3451 pts/0
3514 pts/0
00:00:00 bash00:00:00 ps
然后shell讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序并等待這個進程結束。 所以要寫一個shell,需要循環以下過程:
- 獲取命令行
- 解析命令行
- 建立一個子進程(fork)
- 替換子進程(execvp)
- 父進程等待子進程退出(wait) 根據這些思路,和我們前面的學的技術,就可以自己來實現一個shell了。
2.4 全局變量
- 我們的shell內部有兩張表命令行參數表和環境變量表
- 同時我們還要定義一張哈希表方便處理別名
- 定義兩個數組用來方便處理記錄路徑
- lastcode記錄上一次的進程退出碼、
- 宏定義大小方便開辟數組 以及命令行輸出格式
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "// 下面是shell定義的全局數據// 1. 命令行參數表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;// 2. 環境變量表
#define MAX_ENVS 100
char* g_env[MAX_ENVS];
int g_envs = 0;// 3. 別名映射表
std::unordered_map<std::string, std::string> alias_list;// for test
char cwd[1024];
char cwdenv[1024];// last exit code
int lastcode = 0;
2.5 環境變量函數
環境標量函數直接調用getenv獲取在返回即可
注意GetPWD需要ssnprintf格式化寫入即可
DirName直接從后面查找分割符\ 然后返回之后的字符串即可
const char* GetUserName()
{const char* name = getenv("USER");return name == NULL ? "None" : name;
}const char* GetHostName()
{const char* hostname = getenv("HOSTNAME");return hostname == NULL ? "None" : hostname;
}const char* GetPwd()
{//const char *pwd = getenv("PWD");const char* pwd = getcwd(cwd, sizeof(cwd));if (pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}const char* GetHome()
{const char* home = getenv("HOME");return home == NULL ? "" : home;
}
string DirName(const char* pwd)
{
#define SEP "/"string ret = pwd;int index = ret.rfind(SEP);if (ret == SEP){return SEP;}if (index == string::npos){return "BUG?";}string t = ret.substr(index+1);return t;
}
2.6 初始化環境變量表函數
直接把enriron指向的環境變量表拷貝到我們的環境變量表里面 為了區分我們的shell和系統的我們在末尾添加上haha區分
在讓environ指向我們的環境變量表即可
void InitEnv()
{extern char** environ;memset(g_env, 0, sizeof(g_env));g_envs = 0;for (int i = 0; environ[i]; i++){g_env[i] = (char*)malloc(strlen(environ[i]) + 1);strcpy(g_env[i], environ[i]);g_envs++;}g_env[g_envs++] = "haha";g_env[g_envs] = NULL;for (int i = 0; g_env[i]; i++){putenv(g_env[i]);cout << g_env[i] << endl;}environ = g_env;
}
2.7 輸出命令行提示符模塊
先定義一個字符輸出存儲命令行提示符
然后snprintf格式化寫入字符數組中 在輸出即可
void PrintCommandPrompt()
{char cmd_prompt[COMMAND_SIZE];MakeCommandLine(cmd_prompt, sizeof(cmd_prompt));std::cout << cmd_prompt;
}
void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
const char* GetUserName()
{const char* name = getenv("USER");return name == NULL ? "None" : name;
}const char* GetHostName()
{const char* hostname = getenv("HOSTNAME");return hostname == NULL ? "None" : hostname;
}const char* GetPwd()
{//const char *pwd = getenv("PWD");const char* pwd = getcwd(cwd, sizeof(cwd));if (pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}const char* GetHome()
{const char* home = getenv("HOME");return home == NULL ? "" : home;
}
string DirName(const char* pwd)
{
#define SEP "/"string ret = pwd;int index = ret.rfind(SEP);if (ret == SEP){return SEP;}if (index == string::npos){return "BUG?";}string t = ret.substr(index+1);return t;
}
2.8 提取命令輸入模塊
這里先fgets獲取標準輸入到字符數組中
然后構造字符串刪除erase\0
然后去map中判斷是否為別名
如果是直接把value的值拷貝到數組中即可 然后return ture退出
不是別名則把用戶回車的\n字符消除 同時如果用戶只回車此時n==0
不做處理返回false 其他返回true;
bool GetCommandLine(char* commandline, int size)
{const char* ret=fgets(commandline, size, stdin);if (ret == NULL){return false;}std::string a = ret;a.erase(a.size() - 1, 1);if (alias_list.count(a)){strcpy(commandline, alias_list[a].c_str());return true;}int n = strlen(commandline);commandline[n - 1] = 0;if (n == 0){return false;}return true;
}
2.9 填充命令行參數表模塊
先strtok獲取指向第一個空格字符串 while的那個ret不為空時填充g_argv參數表 繼續分割填充 直到找不到空格 說明字符串分割完畢
最后填充在g_argv表最后填充NULL即可
根據g_argv大小判斷是否填充成功 成功返回ture 反之返回flase
bool CommandParse(char* commandline)
{
#define SEP " "g_argc = 0;char* ret = strtok(commandline, SEP);if (ret == NULL){return false;}while (ret){g_argv[g_argc++] = ret;ret = strtok(nullptr, SEP);}g_argv[g_argc] = ret;for (int i = 0; g_argv[i]; i++){cout << g_argv[i] << " ";}cout << endl;return g_argc > 0 ? true : false;
}
2.10 檢測并處理內建命令模塊
先根據g_argv表的第一個命令 判斷分流檢測處理
cd命令如果只有cd 那就直接獲取家目錄的環境變量字符串
chdir修改當前命令為家目錄即可
否則直接chdir修改當前目錄的路徑為g_argv[1]
echo命令判斷分流
echo $?直接返回lastcode退出碼 再設置wield0即可
echo $xxx 直接獲取xxx的環境變量 再輸出即可
echo xxx 直接打印xxx字符串g_argv[1]即可
export命令先判斷是否填寫了要導入的環境變量
沒有直接返回ture不做處理 否則直接putenv導入g_argv[1]環境變量即可
alias命令 這里只處理不帶命令選項的替換
strtok分割=前后字符串然后 存儲到map中即可
bool CheckAndExecBuiltion()
{std::string t = g_argv[0];if (t == "cd"){return Cd();}else if (t == "echo"){return Echo();}else if (t == "export"){return Export();}else if (t == "alias"){cout << "開始替換" << endl;return Alias();}else{return false;}
}
bool Cd()
{std::string t;if (g_argc == 1){t = GetHome();if (t == ""){return true;}chdir(t.c_str());}else{t =g_argv[1];chdir(t.c_str());}return true;
}
bool Echo()
{std::string t = g_argv[1];if (t=="$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if (t[0] == '$'){char* ret = getenv(t.substr(1).c_str());if (ret){cout << ret << endl;}}else{cout << t << endl;}return true;
}
bool Export()
{if (g_argc != 2){return true;}putenv(g_argv[1]);return true;
}
bool Alias()
{
#define SEP "="char* t = g_argv[1];char* ret = strtok(t, SEP);if (ret == NULL){return false;}string a=std::string(ret),b = std::string(strtok(NULL, SEP));alias_list[a] = b;cout << alias_list[a] <<" "<<alias_list.count(a)<<endl;cout << a<< "->" << b << endl;return true;
}
2.11 執行命令模塊
直接創建子進程 紫金陳通過execvp程序替換執行命令 執行完后exit退出 然后父進程waitpid等待子進程 同時把lastcode更新即可。
int Execute()
{pid_t id = fork();if (id == 0){execvp(g_argv[0], g_argv);exit(1);}int status = 0;pid_t rid=waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}
效果演示:
2.12 源碼
#include<iostream>
#include<cstdio>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string>
#include<cstring>
#include<unordered_map>
using namespace std;
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "// 下面是shell定義的全局數據// 1. 命令行參數表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;// 2. 環境變量表
#define MAX_ENVS 100
char* g_env[MAX_ENVS];
int g_envs = 0;// 3. 別名映射表
std::unordered_map<std::string, std::string> alias_list;// for test
char cwd[1024];
char cwdenv[1024];// last exit code
int lastcode = 0;
const char* GetUserName()
{const char* name = getenv("USER");return name == NULL ? "None" : name;
}const char* GetHostName()
{const char* hostname = getenv("HOSTNAME");return hostname == NULL ? "None" : hostname;
}const char* GetPwd()
{//const char *pwd = getenv("PWD");const char* pwd = getcwd(cwd, sizeof(cwd));if (pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}const char* GetHome()
{const char* home = getenv("HOME");return home == NULL ? "" : home;
}
void InitEnv()
{extern char** environ;memset(g_env, 0, sizeof(g_env));g_envs = 0;for (int i = 0; environ[i]; i++){g_env[i] = (char*)malloc(strlen(environ[i]) + 1);strcpy(g_env[i], environ[i]);g_envs++;}g_env[g_envs++] = "haha";g_env[g_envs] = NULL;for (int i = 0; g_env[i]; i++){putenv(g_env[i]);cout << g_env[i] << endl;}environ = g_env;
}
bool GetCommandLine(char* commandline, int size)
{const char* ret=fgets(commandline, size, stdin);if (ret == NULL){return false;}std::string a = ret;a.erase(a.size() - 1, 1);if (alias_list.count(a)){strcpy(commandline, alias_list[a].c_str());return true;}int n = strlen(commandline);commandline[n - 1] = 0;if (n == 0){return false;}return true;
}
string DirName(const char* pwd)
{
#define SEP "/"string ret = pwd;int index = ret.rfind(SEP);if (ret == SEP){return SEP;}if (index == string::npos){return "BUG?";}string t = ret.substr(index+1);return t;
}
void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), "@hcss-ecs-8ddb:", DirName(GetPwd()).c_str());
}
void PrintCommandPrompt()
{char cmd_prompt[COMMAND_SIZE];MakeCommandLine(cmd_prompt, sizeof(cmd_prompt));std::cout << cmd_prompt;
}
bool CommandParse(char* commandline)
{
#define SEP " "g_argc = 0;char* ret = strtok(commandline, SEP);if (ret == NULL){return false;}while (ret){g_argv[g_argc++] = ret;ret = strtok(nullptr, SEP);}g_argv[g_argc] = ret;for (int i = 0; g_argv[i]; i++){cout << g_argv[i] << "#";}cout << endl;return g_argc > 0 ? true : false;
}
bool Cd()
{std::string t;if (g_argc == 1){t = GetHome();if (t == ""){return true;}chdir(t.c_str());}else{t =g_argv[1];chdir(t.c_str());}return true;
}
bool Echo()
{if (g_argc != 2){return true;}std::string t = g_argv[1];if (t=="$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if (t[0] == '$'){char* ret = getenv(t.substr(1).c_str());if (ret){cout << ret << endl;}}else{cout << t << endl;}return true;
}
bool Export()
{if (g_argc != 2){return false;}putenv(g_argv[1]);
}
bool Alias()
{
#define SEP "="char* t = g_argv[1];char* ret = strtok(t, SEP);if (ret == NULL){return false;}string a=std::string(ret),b = std::string(strtok(NULL, SEP));alias_list[a] = b;cout << alias_list[a] <<" "<<alias_list.count(a)<<endl;cout << a<< "->" << b << endl;return true;
}
bool CheckAndExecBuiltion()
{std::string t = g_argv[0];if (t == "cd"){return Cd();}else if (t == "echo"){return Echo();}else if (t == "export"){return Export();}else if (t == "alias"){cout << "開始替換" << endl;return Alias();}else{return false;}
}
int Execute()
{pid_t id = fork();if (id == 0){execvp(g_argv[0], g_argv);exit(1);}int status = 0;pid_t rid=waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}
int main()
{//初始化環境變量表InitEnv();while (1){//打印命令行提示符PrintCommandPrompt();//獲取命令行輸入char commandline[COMMAND_SIZE];if (!GetCommandLine(commandline, sizeof(commandline))){continue;}//填充命令行參數表if (!CommandParse(commandline)){continue;}//處理內建命令if (CheckAndExecBuiltion()){continue;}//執行命令Execute();}return 0;
}
三 總結
在繼續學習新知識前,我們來思考函數和進程之間的相似性 exec/exit就像call/return
一個C程序有很多函數組成。一個函數可以調用另外一個函數,同時傳遞給它一些參數。被調用的函數執行一定的操作,然后返回一個值。每個函數都有他的局部變量,不同的函數通過cal/return系統進行通信。
后言
這就是=進程替換和自定義shell。大家自己好好消化!今天就分享到這! 感謝各位的耐心垂閱!咱們下期見!拜拜~