目錄
序言
(一)替換原理
1、進程角度——見見豬跑
?1?? 認識 execl 函數
2、程序角度——看圖理解
(二)替換函數
1、命名理解
?2、函數理解
1??execlp
2??execv
3??execvp
4??execle
5??execve
6??execve
(三)自制shell
總結
序言
在前面的文章中,我已經詳細的講解了進程的創建。但是大家是否知道創建子進程的目的是什么呢?
- 其實很簡單,無非就是讓子進程幫我 (父進程) 執行特定的任務而已
此時又有一個問題被衍生出來了:那就是子進程如果指向一個全新的程序代碼時呢?
- 基于上述這樣的問題,就需要用到本節講到的 — 程序替換
(一)替換原理
1、進程角度——見見豬跑
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。
?1?? 認識 execl 函數
execl
是一個在操作系統中用于進程替換的系統調用函數,它允許將當前的進程映像替換為另一個可執行文件的映像。下面是關于 execl
函數的詳細解釋:
int execl(const char *path, const char *arg0, ... /* (char *) NULL */);
參數說明:
path
:用于指定要替換為的新程序的路徑。arg0
:新程序的名稱。該參數在新程序中被作為?argv[0]
?參數傳遞。
返回值:
- 如果調用成功,
execl
?函數不會返回,因為進程被替換為了新的程序映像; - 若發生錯誤,該函數將返回 -1,并設置全局變量?
errno
?以指示錯誤類型。
💨 當我們到 man 手冊中去查找時,查詢如下:
?接下來,我簡單的寫段代碼對 execl 函數的返回值進行介紹:
#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <sys/wait.h>int main(){pid_t id = fork();if(id == 0){//childprintf("我是子進程: %d\n", getpid());int n= execl("/bin/lssssss", "lssssss", "-a", "-ln", NULL); //lsssss: 不存在 printf("you can see me : %d\n",n);exit(0);}sleep(5);//父進程printf("我是父進程: %d\n", getpid());waitpid(id, NULL, 0);return 0 ;}
程序執行如下:
【說明】?
- ?在子進程中,我們打印出子進程的進程ID,并調用
execl()
函數來將子進程的映像替換為/bin/lssssss
這個不存在的命令; - 由于該命令不存在,所以
execl()
函數會失敗,execl()
函數將返回 -1。因此,打印出 you can see me:" 后的返回值將是 -1。
【結論】
- 如果替換成功,不會有返回值,如果替換失敗,一定有返回值 ;
- 如果失敗了,必定返回;只要有返回值,就失敗了;
- 因此不用對該函數進行返回值判斷,只要繼續向后運行一定是失敗的!
?
?有了上述對 execl 函數的認識,我相信下面這段代碼對大家來說就小菜一碟了:
#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <sys/wait.h>int main(){printf("begin...\n");printf("begin...\n");printf("begin...\n");printf("begin...\n");// 執行進程替換execl("/bin/ls", "ls", "-l", NULL);printf("end...\n");printf("end...\n");printf("end...\n");printf("end...\n"); return 0;}
- ?程序輸出執行結果如下:
?
【說明】
?
在上述示例中,execl
函數被調用來將當前進程替換為 /bin/ls
,并傳遞 -l
參數給 ls
命令。如果 execl
函數執行成功,當前進程的映像將被替換為新的 ls
程序的映像。如果 execl
函數執行失敗,將打印相應的錯誤信息。
需要注意的是,在調用 execl
函數時,需要指定新程序的完整路徑,并確保該路徑下的程序可執行。同時,還可以傳遞其他命令行參數給新程序,后續的參數通過參數列表傳遞,以 NULL
結束。
2、程序角度——看圖理解
上訴這張圖,我來給大家隆重介紹一下!!
- 那么實際上呢,我曾經講過,當你出啟動一個進程時,那你是不是就要有PCB啊,會創建一個PCB;
- 對于一個進程,也要有自己的虛擬利空間,也要有自己的列表,那么當前進程的代碼數據,它都要經過頁表映射到物理內存的特定區,所以呢,那么當我們當前的這個進程,它在執行代碼時,如果執行了你剛剛所調用的系統調用exec等這樣的接口時,它就會根據你所傳入的程序的路徑和你要執行的程序的名稱及選項,把磁盤當中的一個其他的程序加載到我們對應的內存,用新程序的代碼來替換,此時當用我們對應的當前進程去替換我們老進程的數據和代碼,把數據和代碼用新的程序全部給你重新替換一遍;
- 其中上圖中右側那部分基本不變啊,當然了,你替換的時候,如果空間要增多,那你就重新再去調整頁面就行了;
- 反正呢,我們在替換時就相當于當前我們的進程的內核數據結構不變,而把這個進程所匹配的代碼和數據用新的程序它的代碼數據來進行替換,這個行為就叫做程序替換。
💨接下來,回答一個大家可能關心的問題?那就是進程進行程序替換有沒有創建新進程呢?
- 答案是沒有創建新的進程,為什么沒有創建新的進程呢?很簡單,因為我只是把一個新的程序加載到我們當前進程所對應的代碼和數據段;
- 然后呢,我讓CPU去調度當前進程,它就可以跑起來了,在這其中,我們并沒有創建新的進程,因為當前進程的內核、PCB、地址空間,尤其是PCB的pid沒有變化,
?
而站在程序的角度,我們可以這樣去進行理解:
- 假設當前我是一個進程,我家里閑的沒事兒干,躺在那兒看電視呢,突然有一個人把我拉走了,讓我就給它辦一些它對應的事情,那么站在程序的角度呢,它是不是就相當于被動的被加載到的到了內存當中;
- 站在程序的角度,那么其中就相當于這個程序被加載了,是不是相當于這個程序就直接被加載到內存了?所以呢,我們也可以稱我們對應的exec以及之后學習到的這些程序替換函數,我們就可以稱它為叫做加載器。
?
(二)替換函數
在操作系統中,有幾個常用的進程替換函數可以使用,包括 exec
系列函數。下面是對它們的簡要介紹:
exec
系列函數用于執行一個新的程序映像,將當前進程替換為新程序。這些函數包括:
execl
:接收可變數量的參數作為命令行參數傳遞給新程序。execv
:接收參數數組,其中第一個元素是新程序的路徑,后續元素是命令行參數。execle
:與?execl
?類似,但額外接收一個環境變量數組作為參數。execve
:與?execv
?類似,但額外接收一個環境變量數組作為參數。execlp
:與?execl
?類似,但允許通過環境變量?PATH
?自動搜索可執行文件的路徑。execvp
:與?execv
?類似,但允許通過環境變量?PATH
?自動搜索可執行文件的路徑。
這些函數在調用成功時不會返回,因為進程映像已被替換為新程序。如果調用失敗,它們將返回 -1,并設置全局變量 errno
指示錯誤類型。
- ?接下來,我們通過 man手冊去對其進行查詢:
?
?
1、命名理解
這些函數原型看起來很容易混,但只要掌握了規律就很好記:
- l(list) : 表示參數采用列表
- v(vector) : 參數用數組
- p(path) : 有p自動搜索環境變量PATH
- e(env) : 表示自己維護環境變量
?
?2、函數理解
在上述我們已經對 execl 函數進行了詳解,接下來我逐個對剩余的函數進行解釋。
1??execlp
execlp 函數是 exec
系列函數之一,用于執行一個新的程序映像并替換當前進程。execlp 函數通過在系統的標準路徑(由 PATH
環境變量指定)中搜索可執行文件來確定要執行的程序。
- execlp 函數的原型如下:
int execlp(const char *file, const char *arg, ...);
- file:參數是要執行的程序文件名稱或路徑。如果在?
PATH
?中找到匹配的可執行文件,則只需提供文件名即可。 - arg:參數是要傳遞給新程序的命令行參數列表。需要以空指針結尾。
下面是一個使用 execlp 函數的示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){//childprintf("我是子進程: %d\n", getpid());char *const myargv[] = {"ls","-a","-l","-n",NULL};execlp("ls","-ls", "-a","-l","-n",NULL); exit(1);}sleep(1);int status = 0;//父進程printf("我是父進程: %d\n", getpid());waitpid(id, &status, 0);printf("child exit code: %d\n", WEXITSTATUS(status));return 0 ;
}
?輸出展示:
?
2??execv
execv 是一個系統調用函數,用于在當前進程的上下文中執行一個新的程序。
- 函數原型如下:
int execv(const char *path, char *const argv[]);
參數說明:
path
?是一個字符串,表示要執行的程序的路徑。argv
?是一個以 NULL 結尾的字符串數組,表示要傳遞給執行的程序的命令行參數。
execv 執行成功時不會返回,而是直接將當前進程替換為新的程序。如果 execv 調用失敗,它會返回 -1,并且當前進程的狀態不會改變。
下面是一個使用 execv 的示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){//childprintf("我是子進程: %d\n", getpid());char *const myargv[] = {"ls","-a","-l","-n",NULL};execv("/bin/ls", myargv); //lsssss: 不存在 exit(1);}sleep(1);int status = 0;//父進程printf("我是父進程: %d\n", getpid());waitpid(id, &status, 0);printf("child exit code: %d\n", WEXITSTATUS(status));return 0 ;
}
輸出展示:
?【說明】
- 使用
execv
執行了? ls a l?-n 命令。"/bin/ls"
指定了要執行的程序的路徑,而myargv
數組包含了命令行參數。當 execv 成功執行時,當前進程就會被ls
程序所替代,并且輸出文件列表。 - 需要注意的是,execv 函數需要提供完整的可執行文件路徑,并且命令行參數在數組
argv
中以 NULL 結尾。
?
3??execvp
execvp 是一個系統調用函數,與 execv
類似,用于在當前進程的上下文中執行一個新的程序。它的參數形式稍有不同,主要是在指定程序路徑時可以省略路徑。
- 函數原型如下:
int execvp(const char *file, char *const argv[]);
參數說明:
file
?是一個字符串,表示要執行的程序的路徑。如果?file
?不包含斜杠字符(/
),那么系統會按照標準的搜索路徑規則來查找可執行文件。argv
?是一個以 NULL 結尾的字符串數組,表示要傳遞給執行的程序的命令行參數。
與 execv
不同的是,execvp 可以在當前進程的環境變量 PATH
指定的路徑中搜索要執行的程序,而不需要提供完整的路徑。
下面是一個使用 execvp 的示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){//childprintf("我是子進程: %d\n", getpid());char *const myargv[] = {"ls","-a","-l","-n",NULL};execvp("ls", myargv); exit(1);}sleep(1);int status = 0;//父進程printf("我是父進程: %d\n", getpid());waitpid(id, &status, 0);printf("child exit code: %d\n", WEXITSTATUS(status));return 0 ;
}
輸出展示:
?
?【說明】
- 我們使用 execvp 執行了 ls a l?-n 命令。由于 "ls" 是一個簡單的命令,而不是一個具體的可執行文件路徑,所以
execvp
會在系統的PATH
環境變量中搜索 "ls" 可執行文件,并執行該文件。當 execvp 成功執行時,當前進程就會被ls
程序所替代,并且輸出文件列表。 - 與
execv
相比,execvp 更加靈活,因為它可以直接使用程序名稱而不需要指定完整路徑。
4??execle
execle 是一個系統調用函數,用于在當前進程的上下文中執行一個新的程序,并且可以指定環境變量。
- 函數原型如下:
int execle(const char *path, const char *arg0, ..., const char *argn, char *const envp[]);
參數說明:
- path:是一個字符串,表示要執行的程序的路徑。
- arg:到?
argn
?是一系列以 NULL 結尾的字符串,表示要傳遞給執行的程序的命令行參數。 - envp:是一個以 NULL 結尾的字符串數組,表示要設置給新程序的環境變量。
與 execv 和 execvp不同的是,execle 可以顯式地指定環境變量,而不是繼承當前進程的環境變量。
下面是一個使用?execle 的示例:
?
- 首先,為了跟上述的代碼區分開,我另外在創了一個文件,在里面放入了相應的信息,目的就是通過我們的【myproc】去調用other目錄下的【otherproc】:
- ?otherproc.cc代碼如下:
#include <iostream>
#include <unistd.h>
#include <stdlib.h>using namespace std;int main()
{for(int i = 0; i < 5; i++){cout << "----------------------------------------------------------------"<< endl;cout << "我是另一個程序,我的pid是: " << getpid() << endl;cout << " MYENV: " << (getenv("MYENV")==NULL?"NULL":getenv("MYENV")) << endl;cout << " PATH: " << (getenv("PATH")==NULL?"NULL":getenv("PATH")) << endl;cout << "----------------------------------------------------------------"<< endl;sleep(1);}return 0;
}
【說明】
- 用于輸出當前程序的PID和環境變量。程序會循環輸出這些信息,并每秒鐘輸出一次;
- 程序會使用
getpid()
函數獲取當前進程的PID,并使用getenv()
函數獲取環境變量的值; - 需要注意的是,
getenv()
函數用于獲取指定環境變量的值。在程序中,使用了"MYENV"
和"PATH"
作為要獲取的環境變量名,而對于 PATH來說在系統中默認是有的
輸出展示:
?
接下來,我們退出 other 目錄,對【myproc.c】進行改造:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){//childprintf("我是子進程: %d\n", getpid());char *const myenv[]={"MYENV=YouCanSeeMe",NULL};execle("./other/otherproc","otherproc",NULL,myenv); exit(1);}sleep(1);int status = 0;//父進程printf("我是父進程: %d\n", getpid());waitpid(id, &status, 0);printf("child exit code: %d\n", WEXITSTATUS(status));return 0 ;
}
- 輸出展示:
- ?上述不難看出,當我們調用 execle函數時,發生的是覆蓋式的調入。老的數據會被覆蓋掉;
- 此時即傳入了我自己定義的環境變量
但是有一天,我不想傳自己的環境變量了。此時,我想傳系統的環境變量,此時我們可以怎么做呢?
💨 此時,我們需要引入一個概念:extern char **environ;
- 在 POSIX 標準中,全局環境變量是一個字符串指針數組,其中每個指針指向一個以
key=value
格式表示的環境變量字符串; - 通過使用
extern char **environ;
的聲明,我們可以在程序中訪問這個全局環境變量數組。
接下來,我們改動一下代碼:
?
輸出展示:
?【說明】
- 我們再進行程序此時呢我們可以發現,用我們的程序去運行 other 時,此時【myenv】當前是沒有的,但是【PATH】此時還是有的。確實交給子進程了
當我不僅想傳系統的環境變量,還想把自己的環境變量都傳給子進程時,該怎么做呢?
此時我們需要在認識一個接口:putenv
putenv?是一個 C 語言標準庫函數,用于設置環境變量的值。它可以添加新的環境變量或修改已存在環境變量的值。
- 函數原型如下:
int putenv(char *string);
- 參數
string
是一個以"key=value"
格式表示的字符串; key
是要設置或修改的環境變量的名稱;value
是要將該環境變量設置為的值。
代碼展示:
?
輸出展示:
除了上述這樣的做法之外,我們還可以像下述這樣去進行操作:
- 我們在 myproc.c 中不進行 putenv 操作,我們在當前命令行中進行 export操作:
?輸出展示:?
有了上述的理解。接下來解釋一個問題:
?在之前學習環境變量時,我們知道 環境變量具有全局屬性,可以被子進程繼承下去,但是是怎么辦到的呢?
- 很簡單,因為所有的指令都是bash的子進程,而bash執行所有的指令,都可以直接通過exec去執行;
- 我們要給子進程把bash的環境變量交給子進程,只需要調用 【execle】,然后再把我們指定的環境變量,直接以最后一個參數的形式傳給子進程,此時子進程就拿到了!!!
?
5??execve
?
execve?是一個系統調用函數,它會替換當前進程的映像,將其替換為新程序的映像,并開始執行新程序。
- 函數原型如下:
int execve(const char *filename, char *const argv[], char *const envp[]);
參數說明:?
- 參數
filename
是要執行的程序的路徑; - 第二個參數
argv[]
是一個字符串數組,它包含了傳遞給新程序的命令行參數; - 而
envp[]
是一個字符串數組,它包含了傳遞給新程序的環境變量。
這個跟上述的 execle 函數是類似的。在這里就不做過多演示。
6??execve
事實上,只有execve是真正的系統調用,其它五個函數最終都調用 execve,所以execve在man手冊第2節,其它函數在man手冊第3節
?
?
?這些函數之間的關系如下圖所示:
?
(三)自制shell
我們可以用下圖的時間軸來表示事件的發生次序。其中時間從左向右。shell由標識為sh的方塊代表,它隨著時間的流逝從左向右移動。shell從用戶讀入字符串"ls"。shell建立一個新的進程,然后在那個進程中運行ls程序并等待那個進程結束。
?
然后shell讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序 并等待這個進程結束。
所以要寫一個shell,需要循環以下過程:
- 1. 獲取命令行
- 2. 解析命令行
- 3. 建立一個子進程(fork)
- 4. 替換子進程(execvp)
- 5. 父進程等待子進程退出(wait)
?
根據這些思路,和我們前面的學的技術,就可以自己來實現一個shell了
實現代碼:
?
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAX 1024
#define ARGC 64
#define SEP " "int split(char *commandstr, char *argv[])
{assert(commandstr);assert(argv);argv[0] = strtok(commandstr, SEP);if(argv[0] == NULL) return -1;int i = 1;while((argv[i++] = strtok(NULL, SEP)));return 0;
}void debugPrint(char *argv[])
{for(int i = 0; argv[i]; i++){printf("%d: %s\n", i, argv[i]);}
}int main()
{while(1){char commandstr[MAX] = {0};char *argv[ARGC] = {NULL};printf("[zhangsan@mymachine currpath]# ");fflush(stdout);char *s = fgets(commandstr, sizeof(commandstr), stdin);assert(s);// 保證在release方式發布的時候,因為去掉assert了,所以s就沒有被使用// 而帶來的編譯告警, 什么都沒做,但是充當一次使用(void)s;commandstr[strlen(commandstr)-1] = '\0';int n = split(commandstr, argv);if(n != 0) continue;//debugPrint(argv);// version 1pid_t id = fork();assert(id >= 0);(void)id;if(id == 0){//childexecvp(argv[0], argv);exit(1);}int status = 0;waitpid(id, &status, 0);}
}
?💨 整體代碼:進程替換代碼?
總結
以上便是關于進程程序替換的全部內容。接下來,簡單回顧下本文都講了什么!!
進程程序替換是指在一個正在運行的進程中,用另外一個可執行程序替換當前進程的執行內容,從而使新的程序代碼開始執行。
進程程序替換通常用于實現進程的動態更新、功能擴展或進程間通信。在 Linux 系統中,常用的進程程序替換函數是 exec
函數族,包括 execl
、execle
、execlp
、execv
、execvp
等。
這些函數可以加載新的可執行文件,并用其替換當前進程的執行內容,從而運行新的程序。替換后,新的程序將繼承原進程的一些屬性,如進程 ID、文件描述符等。
需要注意以下幾點:
- 替換后,原有的程序代碼、數據和堆棧信息都會被新的程序取代,因此原有進程的狀態將完全丟失。
- 替換的新程序需具備執行權限,并與原進程使用相同的用戶身份執行,否則可能會導致權限問題。
- 替換后,新程序的命令行參數、環境變量等可以與原進程不同,從而實現不同的功能。
到此,本文便講解完畢了。感謝大家的觀看與支持!!!
?