国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

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

你寫了一個多進(jìn)程模型的服務(wù)器,但總感覺新進(jìn)程啟動地不干凈,有時會有些父進(jìn)程的東西摻和到子進(jìn)程里來。

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

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

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

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

  • 進(jìn)程的啟動

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

  • 進(jìn)程的終止

01

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

1.1 虛擬空間及其功能

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

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

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

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

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

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

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

該做法的另一結(jié)果則是降低了每個進(jìn)程內(nèi)存管理的復(fù)雜度,進(jìn)程只需關(guān)心如何使用自己線性排列的虛擬地址,而不需關(guān)心物理內(nèi)存的實際容量,以及如何使用真實的物理內(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。目前看來,這樣的地址空間范圍足夠用了。

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

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

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

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

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

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

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

02

進(jìn)程的啟動

從用戶角度來看,啟動一個進(jìn)程有許多種方式,可以配置開機自啟動,可以在 Shell 中手動運行,也可以從腳本或其他進(jìn)程中啟動。

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

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

fork() 系統(tǒng)調(diào)用將創(chuàng)建一個與父進(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),就是通過 fork() 系統(tǒng)調(diào)用來實現(xiàn)的,每當(dāng)新用戶接入時,系統(tǒng)就會專門創(chuàng)建一個新進(jìn)程,來服務(wù)該用戶。

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

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

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

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

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

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

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

輸入文本:

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

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

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

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

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

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

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

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

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

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

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

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

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

2.3 fexecve() 函數(shù)

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

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

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

03

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

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

  • pid_t wait(int * statua)

    一直阻塞地等待任意一個子進(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)控因信號而停止的子進(jìn)程等。

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

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

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

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

更詳細(xì)的信息請參考幫助手冊。

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

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

也就是說,如果子進(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)之間的這段時間,這個進(jìn)程會變成所謂的僵尸狀態(tài),在該狀態(tài)下,任何信號都無法結(jié)束它。如果系統(tǒng)中存在大量此類僵尸進(jìn)程,勢必會占用大量內(nèi)核資源,甚至?xí)?dǎo)致新進(jìn)程創(chuàng)建失敗。

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

04 

進(jìn)程的終止

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

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

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

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

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

通常情況下,個性化的退出處理函數(shù)只會在主進(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)程的正確姿勢等。

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

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
超硬核,進(jìn)程在內(nèi)存中的樣子!以及進(jìn)程的一生
【Linux操作系統(tǒng)分析】進(jìn)程的創(chuàng)建與可執(zhí)行程序的加載
execve()函數(shù)的研究
淺析Linux計算機進(jìn)程地址空間與內(nèi)核裝載ELF
一文帶你了解操作系統(tǒng)核心概念
fork()創(chuàng)建新進(jìn)程
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服