久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

「Linux 底層原理」理解進(jìn)程內(nèi)存布局,掌握程序動(dòng)態(tài)

 漢無(wú)為 2019-01-31

你寫(xiě)了一個(gè)多進(jìn)程模型的服務(wù)器,,但總感覺(jué)新進(jìn)程啟動(dòng)地不干凈,,有時(shí)會(huì)有些父進(jìn)程的東西摻和到子進(jìn)程里來(lái),。

可如果讓父進(jìn)程在啟動(dòng)子進(jìn)程之前做更多的計(jì)算,,或者單純多等一會(huì),這種情況發(fā)生的概率便大大減少了,,該系統(tǒng)的行為讓人有點(diǎn)捉摸不透,,其背后的原因是什么呢?

簡(jiǎn)單來(lái)講,,進(jìn)程就是運(yùn)行中的程序,。更進(jìn)一步,,在用戶空間中,進(jìn)程是加載器根據(jù)程序頭提供的信息將程序加載到內(nèi)存并運(yùn)行的實(shí)體,。

在本文中,,我們就來(lái)深挖進(jìn)程在用戶空間內(nèi)的更多細(xì)節(jié),主要包括以下幾部分內(nèi)容:

  • 進(jìn)程的虛擬空間排布

  • 進(jìn)程的啟動(dòng)

  • 監(jiān)控子進(jìn)程的狀態(tài)

  • 進(jìn)程的終止

01

進(jìn)程的虛擬空間排布

1.1 虛擬空間及其功能

在理解虛擬空間排布之前,,先要明確虛擬空間的概念,。在《攻克 Linux 系統(tǒng)編程》中,我們解釋了的 ELF 文件頭中指定的程序入口地址,,各個(gè)節(jié)區(qū)在程序運(yùn)行時(shí)的內(nèi)存排布地址等,,指的都是在進(jìn)程虛擬空間中的地址。

虛擬空間可以認(rèn)為是操作系統(tǒng)給每個(gè)進(jìn)程準(zhǔn)備的沙盒,,就像電影《黑客帝國(guó)》中 Matrix 給每個(gè)人準(zhǔn)備的充滿營(yíng)養(yǎng)液的容器一樣,。

實(shí)際上,每個(gè)進(jìn)程只存活在自己的虛擬世界里,,卻感覺(jué)自己獨(dú)占了所有的系統(tǒng)資源(內(nèi)存),。

當(dāng)一個(gè)進(jìn)程要使用某塊內(nèi)存時(shí),它會(huì)將自己世界里的一個(gè)內(nèi)存地址告訴操作系統(tǒng),,剩下的事情就由操作系統(tǒng)接管了,。

操作系統(tǒng)中的內(nèi)存管理策略將決定映射哪塊真實(shí)的物理內(nèi)存,供應(yīng)用使用,。操作系統(tǒng)會(huì)竭盡全力滿足所有進(jìn)程合法的內(nèi)存訪問(wèn)請(qǐng)求,。

一旦發(fā)現(xiàn)應(yīng)用試圖訪問(wèn)非法內(nèi)存,它將會(huì)把進(jìn)程殺死,,防止它做“壞事”影響到系統(tǒng)或其他進(jìn)程,。

這樣做,一方面為了安全,,防止進(jìn)程操作其他進(jìn)程或者系統(tǒng)內(nèi)核的數(shù)據(jù),;另一方面為了保證系統(tǒng)可同時(shí)運(yùn)行多個(gè)進(jìn)程,且單個(gè)進(jìn)程使用的內(nèi)存空間可以超過(guò)實(shí)際的物理內(nèi)存容量,。

該做法的另一結(jié)果則是降低了每個(gè)進(jìn)程內(nèi)存管理的復(fù)雜度,,進(jìn)程只需關(guān)心如何使用自己線性排列的虛擬地址,而不需關(guān)心物理內(nèi)存的實(shí)際容量,,以及如何使用真實(shí)的物理內(nèi)存,。

1.2 虛擬空間地址排布

在 32 位系統(tǒng)下,進(jìn)程的虛擬地址空間有 4G,,其中的 1G 分配給了內(nèi)核空間,,用戶應(yīng)用可以使用剩余的 3G。

在 64 位的 Linux 系統(tǒng)上,,進(jìn)程的虛擬地址空間可以達(dá)到 256TB,,內(nèi)核和應(yīng)用分別占用 128TB,。目前看來(lái),這樣的地址空間范圍足夠用了,。

一個(gè)典型的內(nèi)存排布結(jié)構(gòu)如下圖所示:

圖#1中,,《深入程序布局內(nèi)部》中討論過(guò)的內(nèi)容,是按照 ELF 文件中的程序頭信息,,加載文件內(nèi)容所得到的,。除此之外,加載器還會(huì)為每個(gè)應(yīng)用分配棧區(qū)(Stack),、堆區(qū)(Heap)和動(dòng)態(tài)鏈接庫(kù)加載區(qū),。棧和堆分別向相對(duì)的方向增長(zhǎng),系統(tǒng)會(huì)有相應(yīng)的保護(hù)措施,,阻止越界行為發(fā)生,。

在 Linux 系統(tǒng)中,使用如下命令可查看一個(gè)運(yùn)行中的進(jìn)程的內(nèi)存排布,。

稍微修改上一篇中的示例代碼,,在 main 函數(shù)返回之前,增加一個(gè)無(wú)限循環(huán),,保持程序一直運(yùn)行,。

啟動(dòng)程序并查看該進(jìn)程的內(nèi)存布局,可以看到如下所示的信息:

從以上輸出的內(nèi)容中,,可以直觀看到進(jìn)程的段,、堆區(qū),動(dòng)態(tài)鏈接庫(kù)加載區(qū),,棧區(qū)的邏輯地址排布,,以及每塊內(nèi)存區(qū)分配到的權(quán)限等。

除此之外,,還有兩塊 vdso 和 vsyscall 內(nèi)存區(qū),。它們是一部分內(nèi)核數(shù)據(jù)在用戶空間的映射,為了提高應(yīng)用的性能而創(chuàng)建,。在《攻克 Linux 系統(tǒng)編程》中,,我們?cè)賹?zhuān)門(mén)詳細(xì)討論。

02

進(jìn)程的啟動(dòng)

從用戶角度來(lái)看,,啟動(dòng)一個(gè)進(jìn)程有許多種方式,,可以配置開(kāi)機(jī)自啟動(dòng),可以在 Shell 中手動(dòng)運(yùn)行,,也可以從腳本或其他進(jìn)程中啟動(dòng),。

而從開(kāi)發(fā)人員角度看,無(wú)非就是兩個(gè)系統(tǒng)調(diào)用,,即 fork() 和 execve(),。下面就來(lái)探究下這兩個(gè)系統(tǒng)調(diào)用的行為細(xì)節(jié)。

2.1 fork() 系統(tǒng)調(diào)用

fork() 系統(tǒng)調(diào)用將創(chuàng)建一個(gè)與父進(jìn)程幾乎一樣的新進(jìn)程,,之后繼續(xù)執(zhí)行下面的指令,。程序可以根據(jù) fork() 的返回值,確定當(dāng)前處于父進(jìn)程中,,還是子進(jìn)程中——在父進(jìn)程中,,返回值為新創(chuàng)建子進(jìn)程的進(jìn)程 ID,在子進(jìn)程中,,返回值是 0,。

一些使用多進(jìn)程模型的服務(wù)器程序(比如 sshd),就是通過(guò) fork() 系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)的,,每當(dāng)新用戶接入時(shí),,系統(tǒng)就會(huì)專(zhuān)門(mén)創(chuàng)建一個(gè)新進(jìn)程,來(lái)服務(wù)該用戶,。

fork() 系統(tǒng)調(diào)用所創(chuàng)建的新進(jìn)程,,與其父進(jìn)程的內(nèi)存布局和數(shù)據(jù)幾乎一模一樣。在內(nèi)核中,,它們的代碼段所在的只讀存儲(chǔ)區(qū)會(huì)共享相同的物理內(nèi)存頁(yè),,可讀可寫(xiě)的數(shù)據(jù)段、堆及棧等內(nèi)存,,內(nèi)核會(huì)使用寫(xiě)時(shí)拷貝技術(shù),,為每個(gè)進(jìn)程獨(dú)立創(chuàng)建一份。

在 fork() 系統(tǒng)調(diào)用剛剛執(zhí)行完的那一刻,,子進(jìn)程即可擁有一份與父進(jìn)程完全一樣的數(shù)據(jù)拷貝,。對(duì)于已打開(kāi)的文件,內(nèi)核會(huì)增加每個(gè)文件描述符的引用計(jì)數(shù),,每個(gè)進(jìn)程都可以用相同的文件句柄訪問(wèn)同一個(gè)文件,。

深入理解了這些底層行為細(xì)節(jié),就可以順理成章地理解 fork() 的一些行為表現(xiàn)和正確使用規(guī)范,,無(wú)需死記硬背,,也可獲得一些別人踩過(guò)坑后才能獲得的經(jīng)驗(yàn)。

比如,,使用多進(jìn)程模型的網(wǎng)絡(luò)服務(wù)程序中,,為什么要在子進(jìn)程中關(guān)閉監(jiān)聽(tīng)套接字,同時(shí)要在父進(jìn)程中關(guān)閉新連接的套接字呢,?

原因在于 fork() 執(zhí)行之后,,所有已經(jīng)打開(kāi)的套接字都被增加了引用計(jì)數(shù),在其中任一個(gè)進(jìn)程中都無(wú)法徹底關(guān)閉套接字,只能減少該文件的引用計(jì)數(shù),。

因此,,在 fork() 之后,每個(gè)進(jìn)程立即關(guān)閉不再需要的文件是個(gè)好的策略,,否則很容易導(dǎo)致大量沒(méi)有正確關(guān)閉的文件一直占用系統(tǒng)資源的現(xiàn)象,。

再比如,下面這段代碼是否存在問(wèn)題,?為什么在輸出文件中會(huì)出現(xiàn)兩行重復(fù)的文本,?

輸入文本:

原因是 fputs 庫(kù)函數(shù)帶有緩沖,fork() 創(chuàng)建的子進(jìn)程完全拷貝父進(jìn)程用戶空間內(nèi)存時(shí),,fputs 庫(kù)函數(shù)的緩沖區(qū)也被包含進(jìn)來(lái)了,。

所以,fork() 執(zhí)行之后,,子進(jìn)程同樣獲得了一份 fputs 緩沖區(qū)中的數(shù)據(jù),,導(dǎo)致“Message in parent”這條消息在子進(jìn)程中又被輸出了一次。要解決這個(gè)問(wèn)題,,只需在 fork() 之前,,利用 fflush 打開(kāi)文件即可,讀者可自行驗(yàn)證 ,。

另外,,希望讀者自己思考下,利用父子進(jìn)程共享相同的只讀數(shù)據(jù)段的特性,,是不是可以實(shí)現(xiàn)一套父子進(jìn)程間的通信機(jī)制呢,?

2.2 execve() 系統(tǒng)調(diào)用

execve() 系統(tǒng)調(diào)用的作用是運(yùn)行另外一個(gè)指定的程序。它會(huì)把新程序加載到當(dāng)前進(jìn)程的內(nèi)存空間內(nèi),,當(dāng)前的進(jìn)程會(huì)被丟棄,,它的堆、棧和所有的段數(shù)據(jù)都會(huì)被新進(jìn)程相應(yīng)的部分代替,,然后會(huì)從新程序的初始化代碼和 main 函數(shù)開(kāi)始運(yùn)行,。同時(shí),進(jìn)程的 ID 將保持不變,。

execve() 系統(tǒng)調(diào)用通常與 fork() 系統(tǒng)調(diào)用配合使用,。從一個(gè)進(jìn)程中啟動(dòng)另一個(gè)程序時(shí),通常是先 fork() 一個(gè)子進(jìn)程,,然后在子進(jìn)程中使用 execve() 變身為運(yùn)行指定程序的進(jìn)程,。

例如,當(dāng)用戶在 Shell 下輸入一條命令啟動(dòng)指定程序時(shí),,Shell 就是先 fork() 了自身進(jìn)程,,然后在子進(jìn)程中使用 execve() 來(lái)運(yùn)行指定的程序。

execve() 系統(tǒng)調(diào)用的函數(shù)原型為:

filename 用于指定要運(yùn)行的程序的文件名,argv 和 envp 分別指定程序的運(yùn)行參數(shù)和環(huán)境變量,。除此之外,,該系列函數(shù)還有很多變體,它們執(zhí)行大體相同的功能,,區(qū)別在于需要的參數(shù)不同,,包括 execl,、execlp,、execle、execv,、execvp,、execvpe 等。它們的參數(shù)意義和使用方法請(qǐng)讀者自行查看幫助手冊(cè),。

需要注意的是,,exec 系列函數(shù)的返回值只在遇到錯(cuò)誤的時(shí)候才有意義。如果新程序成功地被執(zhí)行,,那么當(dāng)前進(jìn)程的所有數(shù)據(jù)就都被新進(jìn)程替換掉了,,所以永遠(yuǎn)也不會(huì)有任何返回值。

對(duì)于已打開(kāi)文件的處理,,在 exec() 系列函數(shù)執(zhí)行之前,,應(yīng)該確保全部關(guān)閉。因?yàn)?exec() 調(diào)用之后,,當(dāng)前進(jìn)程就完全變身成另外一個(gè)進(jìn)程了,,老進(jìn)程的所有數(shù)據(jù)都不存在了。

如果 exec() 調(diào)用失敗,,當(dāng)前打開(kāi)的文件狀態(tài)應(yīng)該被保留下來(lái),。讓?xiě)?yīng)用層處理這種情況會(huì)非常棘手,而且有些文件可能是在某個(gè)庫(kù)函數(shù)內(nèi)部打開(kāi)的,,應(yīng)用對(duì)此并不知情,,更談不上正確地維護(hù)它們的狀態(tài)了。

所以,,對(duì)于執(zhí)行 exec() 函數(shù)的應(yīng)用,,應(yīng)該總是使用內(nèi)核為文件提供的執(zhí)行時(shí)關(guān)閉標(biāo)志(FD_CLOEXEC)。設(shè)置了該標(biāo)志之后,,如果 exec() 執(zhí)行成功,,文件就會(huì)被自動(dòng)關(guān)閉;如果 exec() 執(zhí)行失敗,,那么文件會(huì)繼續(xù)保持打開(kāi)狀態(tài),。使用系統(tǒng)調(diào)用 fcntl() 可以設(shè)置該標(biāo)志。

2.3 fexecve() 函數(shù)

glibc 從 2.3.2 版本開(kāi)始提供 fexecv() 函數(shù),它與 execve() 的區(qū)別在于,,第一個(gè)參數(shù)使用的是打開(kāi)的文件描述符,,而非文件路徑名。

增加這個(gè)函數(shù)是為了滿足這樣的應(yīng)用需求:有些應(yīng)用在執(zhí)行某個(gè)程序文件之前,,需要先打開(kāi)文件驗(yàn)證文件內(nèi)容的校驗(yàn)和,,確保文件內(nèi)容沒(méi)有被惡意修改過(guò)。

在這種情景下,,使用 fexecve 是更加安全的方案,。組合使用 open() 和 execve() 雖然可以實(shí)現(xiàn)同樣的功能,但是在打開(kāi)文件和執(zhí)行文件之間,,存在被執(zhí)行的程序文件被掉包的可能性,。

03

監(jiān)控子進(jìn)程狀態(tài)

在 Linux 應(yīng)用中,父進(jìn)程需要監(jiān)控其創(chuàng)建的所有子進(jìn)程的退出狀態(tài),,可以通過(guò)如下幾個(gè)系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn),。

  • pid_t wait(int * statua)

    一直阻塞地等待任意一個(gè)子進(jìn)程退出,返回值為退出的子進(jìn)程的 ID,,status 中包含子進(jìn)程設(shè)置的退出標(biāo)志,。

  • pid_t waitpid(pid_t pid, int * status, int options)

    可以用 pid 參數(shù)指定要等待的進(jìn)程或進(jìn)程組的 ID,options 可以控制是否阻塞,,以及是否監(jiān)控因信號(hào)而停止的子進(jìn)程等,。

  • int waittid(idtype_t idtype, id_t id, siginfo_t *infop, int options)

    提供比 waitpid 更加精細(xì)的控制選項(xiàng)來(lái)監(jiān)控指定子進(jìn)程的運(yùn)行狀態(tài)。

  • wait3() 和 wait4() 系統(tǒng)調(diào)用

    可以在子進(jìn)程退出時(shí),,獲取到子進(jìn)程的資源使用數(shù)據(jù),。

更詳細(xì)的信息請(qǐng)參考幫助手冊(cè)。

本文要重點(diǎn)討論的是:即使父進(jìn)程在業(yè)務(wù)邏輯上不關(guān)心子進(jìn)程的終止?fàn)顟B(tài),,也需要使用 wait 類(lèi)系統(tǒng)調(diào)用的底層原因,。

這其中的要點(diǎn)在于:在 Linux 的內(nèi)核實(shí)現(xiàn)中,允許父進(jìn)程在子進(jìn)程創(chuàng)建之后的任意時(shí)刻用 wait() 系列系統(tǒng)調(diào)用來(lái)確定子進(jìn)程的狀態(tài),。

也就是說(shuō),,如果子進(jìn)程在父進(jìn)程調(diào)用 wait() 之前就終止了,內(nèi)核需要保留該子進(jìn)程的終止?fàn)顟B(tài)和資源使用等數(shù)據(jù),,直到父進(jìn)程執(zhí)行 wait() 把這些數(shù)據(jù)取走,。

在子進(jìn)程終止到父進(jìn)程獲取退出狀態(tài)之間的這段時(shí)間,這個(gè)進(jìn)程會(huì)變成所謂的僵尸狀態(tài),,在該狀態(tài)下,,任何信號(hào)都無(wú)法結(jié)束它。如果系統(tǒng)中存在大量此類(lèi)僵尸進(jìn)程,,勢(shì)必會(huì)占用大量?jī)?nèi)核資源,,甚至?xí)?dǎo)致新進(jìn)程創(chuàng)建失敗,。

如果父進(jìn)程也終止,那么 init 進(jìn)程會(huì)接管這些僵尸進(jìn)程并自動(dòng)調(diào)用 wait ,,從而把它們從系統(tǒng)中移除,。但是對(duì)于長(zhǎng)期運(yùn)行的服務(wù)器程序,這一定不是開(kāi)發(fā)者希望看到的結(jié)果,。所以,,父進(jìn)程一定要仔細(xì)維護(hù)好它創(chuàng)建的所有子進(jìn)程的狀態(tài),防止僵尸進(jìn)程的產(chǎn)生,。

04 

進(jìn)程的終止

正常終止一個(gè)進(jìn)程可以用 _exit 系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn),,原型為:

其中的 status 會(huì)返回 wait() 類(lèi)的系統(tǒng)調(diào)用。進(jìn)程退出時(shí)會(huì)清理掉該進(jìn)程占用的所有系統(tǒng)資源,,包括關(guān)閉打開(kāi)的文件描述符,、釋放持有的文件鎖和內(nèi)存鎖,、取消內(nèi)存映射等,,還會(huì)給一些子進(jìn)程發(fā)送信號(hào)(后面課程再詳細(xì)展開(kāi))。該系統(tǒng)調(diào)用一定會(huì)成功,,永遠(yuǎn)不會(huì)返回,。

在退出之前,還希望做一些個(gè)性化的清理操作,,可以使用庫(kù)函數(shù) exit() ,。函數(shù)原型為:

這個(gè)庫(kù)函數(shù)先調(diào)用退出處理程序,然后再利用 status 參數(shù)調(diào)用 _exit() 系統(tǒng)調(diào)用,。這里的退出處理程序可以通過(guò) atexit() 或 on_exit() 函數(shù)注冊(cè),。

其中 atexit() 只能注冊(cè)返回值和參數(shù)都為空的回調(diào)函數(shù),而 on_exit() 可以注冊(cè)帶參數(shù)的回調(diào)函數(shù),。退出處理函數(shù)的執(zhí)行順序與注冊(cè)順序相反,。它們的函數(shù)原型如下所示:

通常情況下,個(gè)性化的退出處理函數(shù)只會(huì)在主進(jìn)程中執(zhí)行一次,,所以 exit() 函數(shù)一般在主進(jìn)程中使用,,而在子進(jìn)程中只使用 _exit() 系統(tǒng)調(diào)用結(jié)束當(dāng)前進(jìn)程。

05 

總結(jié)

本文深入探究了 Linux 進(jìn)程在用戶空間的一些內(nèi)部細(xì)節(jié),,包括邏輯內(nèi)存排布,、進(jìn)程創(chuàng)建和變身的內(nèi)部細(xì)節(jié)、進(jìn)程狀態(tài)監(jiān)控的目的和接口,,以及終止進(jìn)程的正確姿勢(shì)等,。

對(duì)這些底層實(shí)現(xiàn)細(xì)節(jié)的充分理解,能幫助讀者更好地理解各個(gè)系統(tǒng)調(diào)用的行為表現(xiàn),,并根據(jù)具體的應(yīng)用需求選擇正確,、合適的實(shí)現(xiàn)方案,。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn),。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類(lèi)似文章 更多