W...Y的主頁 😊
代碼倉庫分享💕?
?
前言:我們已經了解了進程的工作原理,并且學習了進程創建、進程終止、進程等待以及進程程序替換。為了更好的鞏固這些知識,我們可以創建一個簡易的shell命令行。
目錄
做一個簡易的shell
觀察shell命令行
獲取命令行
解析命令行
執行命令行
處理內建指令
完整代碼
做一個簡易的shell
觀察shell命令行
我們要做命令行就要觀察其行為,考慮下面這個與shell典型的互動:
[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
PID TTY TIME CMD
3451 pts/0 00:00:00 bash
3514 pts/0 00:00:00 ps
用下圖的時間軸來表示事件的發生次序。其中時間從左向右。shell由標識為sh的方塊代表,它隨著時間的流逝從左向右移動。shell從用戶讀入字符串"ls"。shell建立一個新的進程,然后在那個進程中運行ls程序并等待那個進程結束。
然后shell讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序 并等待這個進程結束。
所以要寫一個shell,需要循環以下過程:
1. 獲取命令行
2. 解析命令行
3. 建立一個子進程(fork)
4. 替換子進程(execvp)5. 父進程等待子進程退出(wait)?
根據這些思路,和我們前面的學的技術,就可以自己來實現一個shell了。
獲取命令行
首先我們得先創建一個命令行提示符,在Linux不同版本下命令行提示符的格式不太一樣。在這里我們使用centos os7的命令行提示符。
格式為:
[用戶名@主機名 當前路徑]$/#
$一般是普通用戶,#一般是超級用戶root
上面的用戶名、主機名、當前路徑在環境變量中都可以查詢到,我們可以使用getenv函數進行獲取:
const char* HostName()
{char *hostname = getenv("HOSTNAME");if(hostname) return hostname;else return "None";
}const char* UserName()
{char *hostname = getenv("USER");if(hostname) return hostname;else return "None";
}const char *CurrentWorkDir()
{char *hostname = getenv("PWD");if(hostname) return hostname;else return "None";
}
并且我們要獲取用戶所輸入的命令。在這里我們不能使用scanf獲取輸入,因為使用scanf遇到空格時會停止讀入。所以我們使用fgets函數獲取輸入。
char commandline[SIZE];
// 1. 打印命令行提示符,獲取用戶輸入的命令字符串
int n = Interactive(commandline, SIZE);int Interactive(char out[], int size)
{// 輸出提示符并獲取用戶輸入的命令字符串"ls -a -l"printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());fgets(out, size, stdin);out[strlen(out)-1] = 0; //'\0', commandline是空串的情況?return strlen(out);
}
解析命令行
因為我們最后要讓子進程進行進程程序替換,所以我們要使用enev*函數,這些函數需要將命令和拆開一步一步執行,下一步就是我們將獲取的命令行按照空格的方式拆開,放到argv的指針數組中進行保存。進程程序替換博客
我們推薦使用C語言提供的接口,strtok函數可以將一個字符串按照特定的字符進行切割。
void Split(char in[])
{int i = 0;argv[i++] = strtok(in, SEP); // "ls -a -l"while(argv[i++] = strtok(NULL, SEP)); if(strcmp(argv[0], "ls") ==0){argv[i-1] = (char*)"--color";argv[i] = NULL;}
}//main函數
Split(commandline);
執行命令行
執行命令行時為了不讓我們的本進程改變,所以要創建子進程進行替換。然后用進程程序替換函數進行替換。
void Execute()
{pid_t id = fork();if(id == 0){// 讓子進程執行命名execvp(argv[0], argv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id) lastcode = WEXITSTATUS(status);
}
?這個簡易的shell就已經做好了,當我們使用一些代碼時就可以進行了。但是我們要注意當我們進行回車時會出現問題,所以我們得在解析命令行之前進行判斷,如果獲取的字符串個數為0,我們進行continue即可。
處理內建指令
但是還有一個問題,我們執行不了如cd + 路徑等的內建指令。就拿cd指令做演示,因為cd命令是想改變當前路徑的,但是我們使用子進程進行程序替換時,子進程進行完時就會退出沒有實際意義,我們想要的其實是bash進程的切換,所以這種內建指令不能使用子進程進行替換,我們得進行特殊處理。
?首先我們得檢測一個命令是否是內建命令,如果是返回值為1,不是內建命令返回值為0,當返回值為1時,我們不需要讓子進程進行程序替換,我們直接continue即可。這里我們以cd命令作為例子。
我們只能使用strcmp函數與內建命令cd進行判斷是否相等,如果相等我們拿去argv中的1號位置內容進行判斷,如果cd后面沒有指令直接獲取家目錄的路徑。getenv函數即可。
char *Home()
{return getenv("HOME");
}
如果有cd命令后面有路徑,我們直接使用chdir函數將更改工作目錄的路徑。
?
int BuildinCmd()
{int ret = 0;// 1. 檢測是否是內建命令, 是 1, 否 0if(strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; //cd XXX or cdif(!target) target = Home();chdir(target);return ret;}
}
但是我們運行后發現執行后沒有問題,但是在命令行提示符中的當前工作路徑卻沒有改變,也就是說我們剛才寫的獲取當前路徑的函數中獲取環境變量中的路徑沒有改變,所以我們要實時對環境變量進行更新。
我們使用getcwd函數可以獲取當前路徑,然后再修改環境變量中的路徑即可。
?更新后的代碼:
int BuildinCmd()
{int ret = 0;// 1. 檢測是否是內建命令, 是 1, 否 0if(strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; //cd XXX or cdif(!target) target = Home();chdir(target);char temp[1024];getcwd(temp, 1024);snprintf(pwd, SIZE, "PWD=%s", temp);putenv(pwd);}return ret;
}
?我們的內建命令不止有cd,還有export,echo指令等待。所以我們也對這些命令做了特殊處理。這些代碼直接放在完整代碼中,想要了解的可以查看。
完整代碼
一下是簡易shell的完整代碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "char *argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE]; // for test
int lastcode = 0;const char* HostName()
{char *hostname = getenv("HOSTNAME");if(hostname) return hostname;else return "None";
}const char* UserName()
{char *hostname = getenv("USER");if(hostname) return hostname;else return "None";
}const char *CurrentWorkDir()
{char *hostname = getenv("PWD");if(hostname) return hostname;else return "None";
}char *Home()
{return getenv("HOME");
}int Interactive(char out[], int size)
{// 輸出提示符并獲取用戶輸入的命令字符串"ls -a -l"printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());fgets(out, size, stdin);out[strlen(out)-1] = 0; //'\0', commandline是空串的情況?return strlen(out);
}void Split(char in[])
{int i = 0;argv[i++] = strtok(in, SEP); // "ls -a -l"while(argv[i++] = strtok(NULL, SEP)); if(strcmp(argv[0], "ls") ==0){argv[i-1] = (char*)"--color";argv[i] = NULL;}
}void Execute()
{pid_t id = fork();if(id == 0){// 讓子進程執行命名execvp(argv[0], argv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id) lastcode = WEXITSTATUS(status); //printf("run done, rid: %d\n", rid);
}int BuildinCmd()
{int ret = 0;// 1. 檢測是否是內建命令, 是 1, 否 0if(strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; //cd XXX or cdif(!target) target = Home();chdir(target);char temp[1024];getcwd(temp, 1024);snprintf(pwd, SIZE, "PWD=%s", temp);putenv(pwd);}else if(strcmp("export", argv[0]) == 0){ret = 1;if(argv[1]){strcpy(env, argv[1]);putenv(env);}}else if(strcmp("echo", argv[0]) == 0){ret = 1;if(argv[1] == NULL) {printf("\n");}else{if(argv[1][0] == '$'){if(argv[1][1] == '?'){printf("%d\n", lastcode);lastcode = 0;}else{char *e = getenv(argv[1]+1);if(e) printf("%s\n", e);}}else{printf("%s\n", argv[1]);}}}return ret;
}int main()
{while(1){char commandline[SIZE];// 1. 打印命令行提示符,獲取用戶輸入的命令字符串int n = Interactive(commandline, SIZE);if(n == 0) continue;// 2. 對命令行字符串進行切割Split(commandline);// 3. 處理內建命令n = BuildinCmd();if(n) continue;// 4. 執行這個命令Execute();}return 0;
}
exec/exit就像call/return
一個C程序有很多函數組成。一個函數可以調用另外一個函數,同時傳遞給它一些參數。被調用的函數執行一定的操作,然后返回一個值。每個函數都有他的局部變量,不同的函數通過call/return系統進行通信。
這種通過參數和返回值在擁有私有數據的函數間通信的模式是結構化程序設計的基礎。Linux鼓勵將這種應用于程序之內的模式擴展到程序之間。如下圖
一個C程序可以fork/exec另一個程序,并傳給它一些參數。這個被調用的程序執行一定的操作,然后通過exit(n)來返回值。調用它的進程可以通過wait(&ret)來獲取exit的返回值。?
以上就是本次的全部內容,感謝大家觀看!?