tplink虛擬服務(wù)器做網(wǎng)站百度網(wǎng)盤網(wǎng)頁版登錄首頁
一,前言
????????創(chuàng)建子進程的目的之一就是為了代勞父進程執(zhí)行父進程的部分代碼,也就是說本質(zhì)上來說父子進程都是執(zhí)行的同一個代碼段的數(shù)據(jù),在子進程修改數(shù)據(jù)的時候進行寫時拷貝修改數(shù)據(jù)段的部分數(shù)據(jù)。
? ? ? ? 但是還有一個目的——將子進程在運行時指向一個全新的程序代碼,也就是我們的進程程序替換。
????????一般情況下,對應(yīng)的語言寫的程序只能調(diào)用對應(yīng)的語言的接口,而不能調(diào)用其他語言的接口,如C++不能調(diào)用Java或者Python或者Shell等語言的接口,那么如果我們想要調(diào)用別人寫的程序應(yīng)該怎么辦呢?
????????那么進程程序替換就能夠很好地幫助我們解決這個問題,這在很大程度上減少了我們編程的成本,學(xué)會進程程序替換,在很多時候,如果有現(xiàn)成的程序,那么我們不需要自己再去寫一個,是不是方便很多了?
二、什么是進程程序替換?
????????所謂進程程序替換,顧名思義,就是使用一個新的程序替換原有的程序,進程將執(zhí)行新程序的代碼,而不再執(zhí)行原有程序的代碼,前面我們已經(jīng)學(xué)習(xí)了如何創(chuàng)建一個進程,一般情況下,進程程序替換都不會使用父進程直接進行進程程序替換,而是讓父進程調(diào)用fork()函數(shù)創(chuàng)建一個子進程,讓子進程去執(zhí)行一個新的程序即可
????????進程程序替換是指在運行過程中將一個進程的地址空間中的代碼、數(shù)據(jù)和堆棧等內(nèi)容完全替換為另一個程序的代碼、數(shù)據(jù)和堆棧的過程。
三,進程程序替換的原理?
- ?進程替換前的效果圖
當(dāng)一個進程成功創(chuàng)建一個子進程之后,父子進程的情況如下圖所示:
????????這個時候,我們這里先針對代碼和數(shù)據(jù)進行分析,其他內(nèi)容暫不做考慮,此時父子進程都沒有修改代碼和數(shù)據(jù),因此,父子進程的代碼和數(shù)據(jù)都是指向同一塊內(nèi)容的,也就是代碼和數(shù)據(jù)是共享的,如果其中一方對數(shù)據(jù)進行修改,則這一方就會進行寫時拷貝,如果想要執(zhí)行不同的代碼,則此時就要進行進程程序替換
進程程序替換的原理:
????????假如剛開始父子進程都是執(zhí)行程序a.exe,后面,想要讓子進程執(zhí)行b.exe了,那么此時就要進行進程程序替換,替換的過程就是首先將b.exe從磁盤加載進內(nèi)存,然后重新建立子進程的頁表,更新子進程的頁表中的映射關(guān)系(注意,這里修改的是頁表中的物理地址而不是虛擬地址,此時父子進程代碼塊中虛擬地址是一樣的,但是通過頁表映射出來的物理地址是不一樣的),從而實現(xiàn)父子進程的代碼徹底分離,此時父子進程的代碼是互不干擾的,很好地滿足了進程的獨立性
- ?進程替換之后的效果圖
?3.1.進程替換注意事項
- 1.進程替換不會創(chuàng)建新進程,因為進程替換只是將該進程的數(shù)據(jù)替換為指定的可執(zhí)行程序。而進程PCB沒有改變,所以不是新的進程,進程替換后不會發(fā)生進程pid改變。
- 2.進程替換后,如果替換成功后則替換函數(shù)下的代碼不會執(zhí)行,因為進程替換是覆蓋式的替換,替換成功后進程原來的代碼就消失了。同理在進程替換失敗后會執(zhí)行替換函數(shù)后的代碼
- 3.進程替換函數(shù)在進程替換成功后不返回,函數(shù)的返回值表示替換失敗
- 4.進程替換成功后,退出碼為替換后的進程的退出碼
四,為什么要進行進程程序替換
????????在學(xué)習(xí)進程程序替換之前,我們知道當(dāng)一個父進程創(chuàng)建一個子進程之后,父子進程的代碼是共享的,子進程只能執(zhí)行父進程的代碼塊
????????但是現(xiàn)在我們的需求增加了,我們不僅要讓子進程能夠執(zhí)行父進程的代碼塊,也要能夠讓子進程能夠做一些父進程不能做的事情,也就是能夠執(zhí)行一個全新的代碼(程序),這樣就能實現(xiàn)父子進程做的事情有所差異,大大提高了辦事效率,同時也使父子進程的代碼徹底分離,維護進程的獨立性
五,怎么實現(xiàn)進程程序替換
????????進程程序替換是指在運行過程中將一個進程的地址空間中的代碼、數(shù)據(jù)和堆棧等內(nèi)容完全替換為另一個程序的代碼、數(shù)據(jù)和堆棧的過程。
這個過程通常是由操作系統(tǒng)提供的 exec 系列函數(shù)來實現(xiàn)的:
- 地址空間替換:進程的地址空間是指進程可以訪問的內(nèi)存范圍。通過地址空間替換,進程可以在運行時動態(tài)地加載并執(zhí)行不同的程序,從而實現(xiàn)靈活的程序執(zhí)行和管理。
- exec 函數(shù)族:exec 函數(shù)族是一組系統(tǒng)調(diào)用,用于執(zhí)行程序替換操作。這些函數(shù)包括 execl, execv, execle, execve 等,它們允許以不同的方式傳遞參數(shù)給新程序,并執(zhí)行地址空間替換。(我們要改變內(nèi)存,那肯定是要調(diào)用系統(tǒng)調(diào)用接口的,這些函數(shù)會封裝相應(yīng)的接口)
- 程序入口點:新程序的入口點是程序中的起始執(zhí)行位置,通常是 main 函數(shù)或其他指定的入口函數(shù)。替換完成后,控制權(quán)將轉(zhuǎn)移到程序入口點,開始執(zhí)行新程序的代碼。
5.1.原理
- 當(dāng)進程調(diào)用一種exec函數(shù)時,該進程的用戶空間代碼和數(shù)據(jù)完全被新程序替換
- 替換完成后,控制權(quán)將轉(zhuǎn)移到新程序的入口點,開始執(zhí)行新程序的代碼。
5.2.使用execl()函數(shù)
????????在學(xué)習(xí)使用進程程序替換的相關(guān)接口之前我們首先需要明確一點,這個在使用接口的時候需要做什么事情?
????????很明顯,最基本的我們首先得知道這個程序在哪里,其次,我們還需要知道怎么執(zhí)行這個程序(比如在我們前面學(xué)習(xí)一些指令的時候,有些指令是可以攜帶選項的有些指令可以不用攜帶選項)。
????????總結(jié)起來就是我們需要知道要執(zhí)行的程序的路徑和怎么執(zhí)行新程序
????????execl函數(shù)是Linux系統(tǒng)中用于執(zhí)行新程序的函數(shù)之一,它屬于exec函數(shù)族的一部分。
????????這個函數(shù)的作用是在當(dāng)前進程的上下文中啟動一個新的程序,并替換當(dāng)前進程的映像為新的程序映像。調(diào)用execl函數(shù)后,當(dāng)前進程將停止執(zhí)行,并由新的程序開始執(zhí)行.
函數(shù)原型如下
參數(shù)說明:
path
:要執(zhí)行的程序的路徑。arg0
:新程序的參數(shù)列表的開始,通常這會是新程序的名稱(盡管這不是強制的,但它通常用于錯誤消息和程序內(nèi)部)。...
:一個可變參數(shù)列表(參數(shù)的數(shù)量不固定),新程序的參數(shù)列表,必須以NULL結(jié)尾。
????????execl函數(shù)會根據(jù)提供的路徑
path
找到并執(zhí)行相應(yīng)的程序,同時將arg0
及其后面的參數(shù)作為新程序的命令行參數(shù)傳遞。注意,參數(shù)列表必須以NULL結(jié)尾,這是告訴execl參數(shù)列表結(jié)束的標(biāo)志。?
??只看上面的解釋,我相信小伙伴們對于這一塊還是不太理解,那么上代碼:
?我們來看個例子
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("I'm a process, pid: %d\n", getpid());printf("execl begin...\n");int a=execl("/usr/bin/ls", "ls", "-a", "-l", NULL);printf("execl end...\n");return 0;
}
????????如果execl函數(shù)調(diào)用成功,那么它實際上不會返回,因為當(dāng)前進程的映像已經(jīng)被新程序替換。如果調(diào)用失敗,它會返回-1,并設(shè)置全局變量errno以指示錯誤原因。常見的錯誤原因可能包括文件未找到、權(quán)限不足等。
????????execl函數(shù)和其他exec函數(shù)一樣,不會創(chuàng)建新的進程。它們只是在當(dāng)前進程的上下文中啟動另一個程序。因此,調(diào)用execl前后,進程的ID(PID)不會改變。
????????同時,由于execl會替換整個進程映像,所以在調(diào)用execl之前,通常需要確保當(dāng)前進程的所有打開的文件描述符、內(nèi)存分配等都被適當(dāng)?shù)靥幚砘蜥尫?#xff0c;因為這些資源不會被新程序繼承。
5.2.1.結(jié)論與細節(jié)
1,程序替換一旦成功,exec后面的代碼不在執(zhí)行。因為被替換掉了,這也是什么代碼沒有輸出execl end的原因了
2,exec函數(shù)調(diào)用成功,那么它實際上不會有返回值;調(diào)用失敗,它會返回-1
3,exec函數(shù)不會創(chuàng)建新的進程。它們只是在當(dāng)前進程的上下文中啟動另一個程序
4,創(chuàng)建一個進程。我們是先創(chuàng)建PCB、地址空間、頁表等再先把程序加載到內(nèi)存
????????????????????????????????如果先加載的話,頁表都沒辦法映射的
5,程序替換的本質(zhì)就是加載 (可以看成一個加載器),有替換就是替換,沒有就是程序加載
????????程序替換的本質(zhì)是程序加載,因為在執(zhí)行 exec 函數(shù)時,操作系統(tǒng)會加載新程序的可執(zhí)行文件,并將其代碼、數(shù)據(jù)和堆棧等部分加載到進程的地址空間中。這個過程涉及將新程序的內(nèi)容從磁盤加載到內(nèi)存中,為進程提供執(zhí)行所需的資源。
????????因此,雖然我們常說是“程序替換”,但實際上更準(zhǔn)確地說是將新程序加載到內(nèi)存中,替換掉原有的程序,以實現(xiàn)進程的功能切換和更新。
6,程序運行要加載到內(nèi)存;為什么?
????????馮諾依曼體系規(guī)定;
如何加載的呢?
????????就是程序替換:程序替換是操作系統(tǒng)的接口,所謂的把磁盤里的數(shù)據(jù)加載到內(nèi)存就是把磁盤設(shè)備的數(shù)據(jù)拷貝到內(nèi)存里。把數(shù)據(jù)從一個硬件搬到另一個硬件,只有操作系統(tǒng)能做
5.3.多進程實現(xiàn)使用ls?
1,我們可以創(chuàng)建一個子進程,由子進程來進行程序替換,父進程來等待結(jié)果就可以。為什么? 父進程能得到子進程的執(zhí)行結(jié)果
2,我們知道父進程與子進程映射到同一塊代碼,那么子進程進行程序替換后,不是會覆蓋嗎,替換為什么不影響父進程?
????????進程具有獨立性,在進行程序替換時要進行寫時拷貝,寫時拷貝的本質(zhì)就是開辟新的空間
3,shell是如何運行起來一個指令的?
????????首先創(chuàng)建子進程,shell會waitpid()等待進程結(jié)果,子進程會繼承shell的代碼,但是不影響。子進程進行程序替換,替換為我們輸入的指令
?上面的實驗是沒有子進程的,是一個純單進程的實驗,下面將演示一個多進程的例子執(zhí)行l(wèi)s指令
上面的實驗思路就是父進程創(chuàng)建一個子進程,然后本來子進程是要執(zhí)行父進程的代碼塊和父進程進行代碼共享的,但是我們在子進程中調(diào)用execl函數(shù)接口,因此,在子進程中會進行程序替換
6.exec系列函數(shù)
????????由我們庫封裝的exec函數(shù)常用的有上面的幾種,這些函數(shù)都有下面的特性,當(dāng)該函數(shù)成功執(zhí)行,那么進程替換成功,代碼不再返回,如果函數(shù)調(diào)用失敗,例如不正確的地址,不正確的文件等等,函數(shù)會返回一個-1,并且exec函數(shù)之后在函數(shù)調(diào)用失敗時才有返回值,成功沒有返回值。
? ? ? ? 只有失敗時有返回值其實很好理解,因為函數(shù)調(diào)用成功那就表示程序替換成功,原來的代碼都被替換了,我返回之后給誰?沒了,之后的代碼就是新代碼了
1.execl:
該函數(shù)允許通過提供可變數(shù)量的參數(shù)來執(zhí)行指定的可執(zhí)行文件。它的原型如下:
int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
path 是要執(zhí)行的可執(zhí)行文件的路徑,arg0 是第一個參數(shù),后續(xù)參數(shù)都是傳遞給可執(zhí)行文件的命令行參數(shù),以 NULL 結(jié)尾。
觀察上圖發(fā)現(xiàn)進程替換成功后,替換函數(shù)下的打印沒有執(zhí)行,原因與注意事項的第二條相同
如果替換失敗:
2.execlp:
該函數(shù)與 execl 類似,但是它會在系統(tǒng)的環(huán)境變量 PATH 指定的目錄中查找可執(zhí)行文件。它的原型如下:
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
file 是要執(zhí)行的可執(zhí)行文件的文件名,arg0 是第一個參數(shù),后續(xù)參數(shù)都是傳遞給可執(zhí)行文件的命令行參數(shù),以 NULL 結(jié)尾。
相比于execl函數(shù),execlp函數(shù)的第一個參數(shù)能直接寫文件名,系統(tǒng)會PATH環(huán)境變量里去查找
多的字母p的意思:PATH環(huán)境變量
int main() {pid_t id = fork();if (id == 0){printf("I'm a process, pid: %d\n", getpid());printf("execl begin...\n");execl("ls", "ls", "-a", "-l", NULL);printf("execl end...\n");exit(1);}pid_t rid = waitpid(id, NULL, 0);if (rid > 0){printf("wait successfully\n");}return 0; }
3.execv:
類似于 execl,但是允許傳遞一個參數(shù)數(shù)組給被執(zhí)行的程序。它的原型如下:
int execv(const char *path, char *const argv[]);
path 是要執(zhí)行的可執(zhí)行文件的路徑,argv 是一個以 NULL 結(jié)尾的參數(shù)數(shù)組,其中每個元素都是一個字符串,表示命令行參數(shù)。
相比于exec多個字母v:代表vector
int main() {pid_t id = fork();if (id == 0){printf("I'm a process, pid: %d\n", getpid());printf("execl begin...\n");char* argv[] = { "ls","-a","-l",NULL};execv("/usr/bin/ls",argv);printf("execl end...\n");exit(1);}pid_t rid = waitpid(id, NULL, 0);if (rid > 0){printf("wait successfully\n");}return 0; }
4.execvp:
類似于 execv,但是它會在系統(tǒng)的環(huán)境變量 PATH 指定的目錄中查找可執(zhí)行文件。它的原型如下:
int execvp(const char *file, char *const argv[]);
file 是要執(zhí)行的可執(zhí)行文件的文件名,argv 是一個以 NULL 結(jié)尾的參數(shù)數(shù)組,其中每個元素都是一個字符串,表示命令行參數(shù)。
既有字母p 又有v,結(jié)合上面那兩種就行
5.execle:
函數(shù)與 execl 函數(shù)類似,但允許在啟動新程序時傳遞額外的環(huán)境變量。它的原型如下:
int execle(const char *path, const char *arg, ..., char *const envp[]);
path 是要執(zhí)行的可執(zhí)行文件的路徑,arg 是要傳遞給新程序的命令行參數(shù),后面的參數(shù)是額外的環(huán)境變量,以 NULL 結(jié)尾。
進程程序替換不會替換環(huán)境變量的
想要子進程繼承全部的環(huán)境變量,不用管,直接就能拿到
單純新增環(huán)境變量,在父進程里使用putenv()函數(shù),會影響子進程
putenv 是 C 語言中的一個庫函數(shù),它定義在 <stdlib.h> 頭文件中。這個函數(shù)用于將字符串添加到環(huán)境變量中,或者修改已經(jīng)存在的環(huán)境變量的值。
int putenv(const char *string);
使用全新的環(huán)境變量,就使用execle()函數(shù),那么替換后的代碼切換后的環(huán)境變量就只是我們傳入的表里的內(nèi)容
?因為此時我們沒有環(huán)境變量MYSTR所以第一行打印為空
這里在myProc子進程中用execle函數(shù)來導(dǎo)入環(huán)境變量MYSTR
注意:
- 1.導(dǎo)環(huán)境變量的數(shù)組最后以NULL結(jié)尾
- 2.導(dǎo)入環(huán)境變量后原系統(tǒng)環(huán)境變量的值被清空,這種導(dǎo)入環(huán)境變量的方式為覆蓋式導(dǎo)入?
?6.使用方法總結(jié)
有人就說了上面那些函數(shù),我怎么記得住他們的用法啊?
?使用這些函數(shù)其實簡單,先將函數(shù)名的exec提取出來看后面的幾個字母。
l:表示用列表方式傳遞。
?? ?其中/bin/ls表示需要執(zhí)行的文件是誰,ls表示執(zhí)行方式,而-a和-l表示這個執(zhí)行的參數(shù)列表。
v:表示使用數(shù)組的方式傳遞。?
?可以看到我們用過指針數(shù)組的方式將我們的執(zhí)行和參數(shù)列表存到了一起,然后將這個指針數(shù)組作為參數(shù)傳遞給我們的execv函數(shù)就行。
p:表示自己只需要傳遞需要執(zhí)行的文件是誰,操作系統(tǒng)會從默認環(huán)境變量當(dāng)中去查找。
?e:表示可以傳遞自己的環(huán)境變量。?
?注意:當(dāng)我們傳遞自己的環(huán)境變量時會替換默認環(huán)境變量,所以如果想要添加一個環(huán)境變量,而不是替換那就需要下方的操作。
通過系統(tǒng)提供的存環(huán)境變量的environ變量,在用putenv函數(shù)添加自己的環(huán)境變量,以達到添加環(huán)境變量的操作。
? ? ? ? 上面的幾個字母通過不同的組合可以達到不同的操作方式。
7.也可以調(diào)用其他語言的程序
code.c里:
int main()
{char* const env[] = {(char*)"first",(char*)"second",NULL };pid_t id = fork();if (id == 0){printf("I'm a process, pid: %d\n", getpid());printf("execl begin...\n");execle("./mytest", "mytest", NULL, env)printf("execl end...\n");exit(1);}pid_t rid = waitpid(id, NULL, 0);if (rid > 0){printf("wait successfully\n");}return 0;
}
?test.cpp里:
#include <iostream>
#include <unistd.h>using namespace std;int main()
{for (int i = 0; environ[i]; i++){printf("env[%d]: %s\n", i, environ[i]);}cout << "This is C++" << endl;return 0;
}
當(dāng)然我們也能傳系統(tǒng)環(huán)境變量,但是沒必要,這樣的話直接默認就行
execle("./mytest", "mytest", NULL, environ)//傳入這個全局變量