個人網站 備案 廣告seo搜索引擎優(yōu)化價格
🎬?個人主頁:誰在夜里看海.
📖?個人專欄:《C++系列》《Linux系列》《算法系列》
???一念既出,萬山無阻
目錄
📖一、進程程序替換
1.替換的演示
?替換與執(zhí)行流
?程序替換≠進程替換
2.替換的原理
📚 系統(tǒng)調用exec
📚?進程控制塊 (PCB)
📚?內存管理
3. 替換的函數(shù)
📚 execl
📚 execv
📚 execp
📚 exece
🚩本質?
📖二、命令行解釋器shell
1.shell的本質
2.shell的模擬實現(xiàn)
📚頭文件
📚宏定義
📚全局變量
📚獲取信息
📚交互式命令行輸入
📚字符串分割
📚內置命令
📚普通命令
📚main函數(shù)
📖一、進程程序替換
上一篇博客我們講到了進程的誕生過程:父進程調用fork創(chuàng)建子進程,子進程執(zhí)行父進程相同的程序。但是很多時候我們希望子進程執(zhí)行另一個程序,此時就要用到exec函數(shù)調用,子進程中調用exec函數(shù)之后,該程序就會被調用的程序代替,這就是程序替換:
1.替換的演示
#include<stdio.h>
#include<unistd.h>
int main()
{int a = 0;a++;execl("/usr/bin/pwd", "pwd", NULL);printf("%d\n", a++);
}
此時程序執(zhí)行結果:
我們可以看到,原先的程序執(zhí)行結果應該是打印變量a,但是被替換成了pwd指令(指令本身也是一個可執(zhí)行程序),這就是程序替換的過程:當進程調用exec函數(shù)時,該進程的代碼和數(shù)據完全被新程序替換,從新程序的啟動例程開始執(zhí)行。
?替換與執(zhí)行流
int main()
{int a = 0;printf("Before: %d\n",a++);execl("/usr/bin/pwd", "pwd", NULL);printf("After: %d\n", a++);
}
不對呢,不是說程序替換之后原來的代碼和數(shù)據都會被替換嗎,那為什么這里還會顯示原程序的打印信息呢?下面進行分析:
?雖然進程調用exec函數(shù)后會發(fā)生程序替換,原程序的代碼和數(shù)據會被覆蓋,但在調用 exec
函數(shù)之前,執(zhí)行流還是要經過原來的步驟的,上述代碼中,在調用execl之前,執(zhí)行流先執(zhí)行printf函數(shù)代碼,由于以“\n”結尾,輸出緩沖區(qū)的數(shù)據會被刷新到終端,所以我們能看到“Before: 0”:
修改一下代碼,結尾不加“\n”,?此時數(shù)據會被保留在輸出緩沖區(qū)當中,后面又因為發(fā)生程序替換,緩沖區(qū)的內容被清除了,所以最終終端不會顯示"Before: 0"內容:
int main()
{int a = 0;printf("Before: %d",a++);execl("/usr/bin/pwd", "pwd", NULL);printf("After: %d\n", a++);
}
?程序替換≠進程替換
程序替換會改變進程的執(zhí)行內容,但它不會改變進程的進程ID,也就是說,進程還是原來的進程,程序替換并不是進程替換,且看下面示例:
先寫一個可執(zhí)行程序test2,源代碼為:
#include<stdio.h>
#include<unistd.h>int main()
{// 打印當前pid,ppidprintf("After: pid = %d, ppid = %d\n",getpid(),getppid());
}
另一個可執(zhí)行程序test源代碼為:
#include<stdio.h>
#include<unistd.h>int main()
{// \n結尾直接打印當前內容printf("Before: pid = %d, ppid = %d\n",getpid(),getppid());// 程序替換成test2execl("/home/ywh/linux_gitee/test_excel/test2", "test2", NULL);
}
test執(zhí)行結果:
我們可以看到,程序替換前后都是同一個進程,結論:exec并不創(chuàng)建新進程。
2.替換的原理
📚 系統(tǒng)調用exec
exec
系列函數(shù)(如 execl
, execv
, execve
等)是用來將當前進程的內存空間、程序代碼段、數(shù)據段等替換成一個新的程序。該系統(tǒng)調用不會創(chuàng)建新進程,而是直接用新程序替換當前進程的內容。
具體來說,exec
調用會:
①:清空當前進程的代碼段、數(shù)據段、堆棧等。
②:加載并執(zhí)行新程序的代碼段、數(shù)據段、堆棧等。
③:保留當前進程的進程 ID (PID)、父進程標識符 (PPID)、文件描述符等。
📚?進程控制塊 (PCB)
操作系統(tǒng)通過 進程控制塊 (PCB) 來管理進程,每個進程都有一個獨立的 PCB,包含了進程的各種狀態(tài)信息,比如進程的 PID、父進程 ID、程序計數(shù)器、堆棧指針等。
當調用 exec
時,進程的 PCB 中的狀態(tài)信息并沒有被改變,操作系統(tǒng)只會根據 exec
調用的參數(shù)加載新的程序內容(代碼段、數(shù)據段等),并且更新程序計數(shù)器和堆棧指針等信息。
📚?內存管理
操作系統(tǒng)中的內存管理模塊負責為進程分配內存。當進程調用 exec
時,操作系統(tǒng)會:
①:釋放原進程的內存(代碼段、數(shù)據段、堆棧)。
②:加載新程序的內存:從磁盤(例如 ELF 文件或其他可執(zhí)行文件)中加載新的程序到內存,包括新的代碼段、數(shù)據段等。
③:更新堆棧和堆的布局,準備新程序的運行環(huán)境。
3. 替換的函數(shù)
其實有六種以exec開頭的函數(shù),統(tǒng)稱exec函數(shù):
#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[]);
為了便于理解,我們可以把exec后面出現(xiàn)的?l、p、e、v?看作exec的四個選項,下面我們依次介紹這些選項:
📚 execl
l(list) : 參數(shù)采用列表?
path:表示要執(zhí)行的程序路徑;
arg:表示程序本身的參數(shù),第一個是程序本身的名稱,后續(xù)為程序的參數(shù)(傳遞系統(tǒng)指令時,參數(shù)就是指令的選項),必須以NULL結尾。
示例:
execl("/bin/ls", "ls", "-l", (char *)NULL);
📚 execv
v(vector) : 參數(shù)用數(shù)組
path:表示要執(zhí)行的程序路徑;
argv:參數(shù)列表,程序的參數(shù)以數(shù)組的形式傳遞,數(shù)組內部也必須以NULL結尾。
示例:
execv("/bin/ls", (char *[]){"ls", "-l", NULL});
📚 execp
p(path) : 自動搜索環(huán)境變量PATH
它可以通過環(huán)境變量 PATH
來查找可執(zhí)行文件,而不需要提供絕對路徑。
示例:
execlp("ls", "ls", "-l", (char *)NULL);
📚 exece
e(env) : 表示自己維護環(huán)境變量?
execle
允許顯式地傳遞一個 環(huán)境變量數(shù)組,而不是繼承當前進程的環(huán)境變量。通過 execle
,你可以自定義新進程的環(huán)境變量。
示例:
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execle("ps", "ps", "-ef", NULL, envp);
🚩本質?
事實上,只有execve才是真正的系統(tǒng)調用,而其他四個函數(shù)最后都會調用execve:
📖二、命令行解釋器shell
我們在linux學習過程中離不開shell,shell是命令行解釋工具,是用戶與內核之間的工具,提供了一個接口,通過它,我們可以執(zhí)行命令、啟動程序等與操作系統(tǒng)進行交互。shell解析用戶輸入的命令,返回執(zhí)行結果。
?shell的本質是什么呢?
1.shell的本質
?shell本質其實是一個進程:
當我們啟動一個終端或打開一個命令行窗口的時候,相當于啟動了一個shell進程(也叫bash進程),這個進程會等待用戶輸入的命令,并將命令通過系統(tǒng)調用傳遞給內核,內核執(zhí)行相應的操作后,返回給shell。
shell的工作原理就是循環(huán)以下操作:
1??獲取命令行 --> 2??解析命令行 --> 3??fork創(chuàng)建子進程?
--> 4??execve替換子進程 --> 5??wait等待子進程退出 ->1??
根據這些思路,我們可以模擬實現(xiàn)一個shell:
2.shell的模擬實現(xiàn)
實現(xiàn)一個簡化版的shell,需要執(zhí)行以下功能:
① 獲取當前工作目錄、用戶名、主機名。
② 解析用戶輸入的命令行并執(zhí)行命令。
③ 內置支持一些常見命令,如
cd
、echo
、export
等。④ 創(chuàng)建子進程來執(zhí)行普通命令,并支持基本的命令分割和管道處理。
📚頭文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
這些頭文件提供了標準輸入輸出、字符串處理、系統(tǒng)調用等功能。unistd包含與進程相關的函數(shù)(如fork,exit)
📚宏定義
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
LEFT、RIGHT、LABLE:用于命令行提示符的格式化;
DELIM:用于命令行字符串的分隔符;
LINE_SIZE、ARGC_SIZE:定義了命令行和參數(shù)的緩沖區(qū)大小;
EXIT_CODE:用于子進程異常退出的返回值。
📚全局變量
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];
lastcode:保存上一個命令的退出碼;
quit:用于控制shell是否退出;
commandline:存儲用戶輸入的命令行字符串;
argv:存儲解析后的命令和參數(shù);
pwd:保存當前工作目錄;
myenv:存儲自定義的環(huán)境變量。
📚獲取信息
const char *getusername() {return getenv("USER");
}const char *gethostname() {return getenv("HOSTNAME");
}void getpwd() {getcwd(pwd, sizeof(pwd));
}
getusername:獲取用戶名
gethostname:獲取主機名
getpwd:獲取當前工作目錄
📚交互式命令行輸入
void interact(char *cline, int size) {getpwd();printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);char *s = fgets(cline, size, stdin);assert(s);(void)s;cline[strlen(cline)-1] = '\0';
}
interact函數(shù)顯示格式化的提示符,并等待用戶輸入命令。輸入命令存儲在cline中;輸入的命令符末行換行符替換成終止符 '\0'。
📚字符串分割
int splitstring(char cline[], char *_argv[]) {int i = 0;argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM));return i - 1;
}
splitstring函數(shù)使用strtok將輸入的命令行字符串按空格和制表符分割成多個命令或參數(shù),存儲在指針數(shù)組argv中。
📚內置命令
int buildCommand(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], "$?") == 0) {printf("%d\n", lastcode);lastcode=0;}else if(*_argv[1] == '$') {char *val = getenv(_argv[1]+1);if(val) printf("%s\n", val);}else {printf("%s\n", _argv[1]);}return 1;}if(strcmp(_argv[0], "ls") == 0) {_argv[_argc++] = "--color";_argv[_argc] = NULL;}return 0;
}
提供了幾個內置命令:
cd:改變當前目錄
export:設置一個新的環(huán)境變量
enho:打印變量值或退出碼
📚普通命令
隊友普通命令的執(zhí)行,需要調用exec程序替換成目標命令的程序:
void NormalExcute(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(rid == id) {lastcode = WEXITSTATUS(status);}}
}
NormalExcute使用fork創(chuàng)建子進程,子進程調用execvp,替換當前程序,父進程等待子進程結束。
📚main函數(shù)
int main() {while(!quit) {interact(commandline, sizeof(commandline));int argc = splitstring(commandline, argv);if(argc == 0) continue;int n = buildCommand(argv, argc);if(!n) NormalExcute(argv);}return 0;
}
main函數(shù)進入循環(huán),不斷接收用戶輸入的命令并解析執(zhí)行。
如果命令是內置命令,則在當前進程中執(zhí)行;如果是普通命令,通過程序替換在子進程中執(zhí)行。
以上就是【劇幕中的靈魂更迭:探索Shell下的程序替換】的全部內容,歡迎指正~??
碼文不易,還請多多關注支持,這是我持續(xù)創(chuàng)作的最大動力!?