Linux從程序到進(jìn)程
作者:Vamei 出處:http://www.rzrgm.cn/vamei 歡迎轉(zhuǎn)載,也請(qǐng)保留這段聲明。謝謝!
計(jì)算機(jī)如何執(zhí)行進(jìn)程呢?這是計(jì)算機(jī)運(yùn)行的核心問題。即使已經(jīng)編寫好程序,但程序是死的。只有活的進(jìn)程才能產(chǎn)出。我們已經(jīng)從Linux進(jìn)程基礎(chǔ)中了解了進(jìn)程。現(xiàn)在我們看一下從程序到進(jìn)程的漫漫征程。
一段程序
下面是一個(gè)簡(jiǎn)單的C程序,假設(shè)該程序已經(jīng)編譯好,生成可執(zhí)行文件vamei.exe。
#include <stdio.h>
int glob=0; /*global variable*/
void main(void) {
int main1=5; /*local variable of main()*/
int main2; /*local variable of main()*/
main2 = inner(main1); /* call inner() function */
printf("From Main: glob: %d \n", glob);
printf("From Main: main2: %d \n", main2);
}
int inner(int inner1) { /*inner1 is an argument, also local to inner()*/
int inner2=10; /*local variable of inner()*/
printf("From inner: glob: %d \n", glob);
return(inner1+inner2);
}
(選取哪一個(gè)語言或者具體的語法并不是關(guān)鍵,大部分語言都可以寫出類似上面的程序。在看Python教程的讀者也可以利用Python的函數(shù)結(jié)構(gòu)和print寫一個(gè)類似的python程序。當(dāng)然,還可以是C++,Java,Objective-C等等。選用C語言的原因是:它是為UNIX而生的語言。)
main()函數(shù)中調(diào)用了inner()函數(shù)。inner()中調(diào)用一次printf()以輸出。最后,在main()中進(jìn)行了兩次printf()。
注意變量的作用范圍。簡(jiǎn)單地說,變量可以分為全局變量和局部變量。在所有函數(shù)之外聲明的變量為全局變量,比如glob,在任何時(shí)候都可以使用。在函數(shù)內(nèi)定義的變量為局部變量,只能在該函數(shù)的作用域(range)內(nèi)使用,比如說我們?cè)趇nner()工作的時(shí)候不能使用main()函數(shù)中聲明的main1變量,而在main()中我們無法使用inner()函數(shù)中聲明的inner2變量。
不用太過在意這個(gè)程序的具體功能。要點(diǎn)是這個(gè)程序的運(yùn)行過程。下圖為該程序的運(yùn)行過程,以及各個(gè)變量的作用范圍:

運(yùn)行流程
進(jìn)程空間
為了進(jìn)一步了解上面程序的運(yùn)行,我們還需要知道,進(jìn)程如何使用內(nèi)存。當(dāng)程序文件運(yùn)行為進(jìn)程時(shí),進(jìn)程在內(nèi)存中獲得空間。這個(gè)空間是進(jìn)程自己的小屋子。
每個(gè)進(jìn)程空間按照如下方式分為不同區(qū)域:

內(nèi)存空間
Text區(qū)域用來儲(chǔ)存指令(instruction),說明每一步的操作。Global Data用于存放全局變量,棧(Stack)用于存放局部變量,堆(heap)用于存放動(dòng)態(tài)變量 (dynamic variable. 程序利用malloc系統(tǒng)調(diào)用,直接從內(nèi)存中為dynamic variable開辟空間)。Text和Global data在進(jìn)程一開始的時(shí)候就確定了,并在整個(gè)進(jìn)程中保持固定大小。
棧(Stack)以幀(stack frame)為單位。當(dāng)程序調(diào)用函數(shù)的時(shí)候,比如main()函數(shù)中調(diào)用inner()函數(shù),stack會(huì)向下增長(zhǎng)一幀。幀中存儲(chǔ)該函數(shù)的參數(shù)和局部變量,以及該函數(shù)的返回地址(return address)。此時(shí),計(jì)算機(jī)將控制權(quán)從main()轉(zhuǎn)移到inner(),inner()函數(shù)處于激活(active)狀態(tài)。位于棧最下方的幀,和全局變量一起,構(gòu)成了當(dāng)前的環(huán)境(context)。激活函數(shù)可以從環(huán)境中調(diào)用需要的變量。典型的編程語言都只允許你使用位于stack最下方的幀 ,而不允許你調(diào)用其它的幀 (這也符合stack結(jié)構(gòu)“先進(jìn)后出”的特征。但也有一些語言允許你調(diào)用棧的其它部分,相當(dāng)于允許你在運(yùn)行inner()函數(shù)的時(shí)候調(diào)用main()中聲明的局部變量,比如Pascal)。當(dāng)函數(shù)又進(jìn)一步調(diào)用另一個(gè)函數(shù)的時(shí)候,一個(gè)新的幀會(huì)繼續(xù)增加到棧的下方,控制權(quán)轉(zhuǎn)移到新的函數(shù)中。當(dāng)激活函數(shù)返回的時(shí)候,會(huì)從棧中彈出(pop,讀取并從棧中刪除)該幀,并根據(jù)幀中記錄的返回地址,將控制權(quán)交給返回地址所指向的指令(比如從inner()函數(shù)中返回,繼續(xù)執(zhí)行main()中賦值給main2的操作)。
下圖是棧在運(yùn)行過程中的變化。箭頭表示棧的增長(zhǎng)方向。每個(gè)方塊代表一幀。開始的時(shí)候我們有一個(gè)為main()服務(wù)的幀,隨著調(diào)用inner(),我們?yōu)閕nner()增加一個(gè)幀。在inner()返回時(shí),我們?cè)俅沃挥衜ain()的幀,直到最后main()返回,其返回地址為空,所以進(jìn)程結(jié)束。

stack變化
在進(jìn)程運(yùn)行的過程中,通過調(diào)用和返回函數(shù),控制權(quán)不斷在函數(shù)間轉(zhuǎn)移。進(jìn)程可以在調(diào)用函數(shù)的時(shí)候,原函數(shù)的幀中保存有在我們離開時(shí)的狀態(tài),并為新的函數(shù)開辟所需的幀空間。在調(diào)用函數(shù)返回時(shí),該函數(shù)的幀所占據(jù)的空間隨著幀的彈出而清空。進(jìn)程再次回到原函數(shù)的幀中保存的狀態(tài),并根據(jù)返回地址所指向的指令繼續(xù)執(zhí)行。上面過程不斷繼續(xù),棧不斷增長(zhǎng)或減小,直到main()返回的時(shí)候,棧完全清空,進(jìn)程結(jié)束。
當(dāng)程序中使用malloc的時(shí)候,堆(heap)會(huì)向上增長(zhǎng),其增長(zhǎng)的部分就成為malloc從內(nèi)存中分配的空間。malloc開辟的空間會(huì)一直存在,直到我們用free系統(tǒng)調(diào)用來釋放,或者進(jìn)程結(jié)束。一個(gè)經(jīng)典的錯(cuò)誤是內(nèi)存泄漏(memory leakage), 就是指我們沒有釋放不再使用的堆空間,導(dǎo)致堆不斷增長(zhǎng),而內(nèi)存可用空間不斷減少。
棧和堆的大小則會(huì)隨著進(jìn)程的運(yùn)行增大或者變小。當(dāng)棧和堆增長(zhǎng)到兩者相遇時(shí)候,也就是內(nèi)存空間圖中的藍(lán)色區(qū)域(unused area)完全消失的時(shí)候,再無可用內(nèi)存。進(jìn)程會(huì)出現(xiàn)棧溢出(stack overflow)的錯(cuò)誤,導(dǎo)致進(jìn)程終止。在現(xiàn)代計(jì)算機(jī)中,內(nèi)核一般會(huì)為進(jìn)程分配足夠多的藍(lán)色區(qū)域,如果清理及時(shí),棧溢出很容易避免。即便如此,內(nèi)存負(fù)荷過大,依然可能出現(xiàn)棧溢出的情況。我們就需要增加物理內(nèi)存了。
Stack overflow可以說是最出名的計(jì)算機(jī)錯(cuò)誤了,所以才有IT網(wǎng)站(stackoverflow.com)以此為名。

在高級(jí)語言中,這些內(nèi)存管理的細(xì)節(jié)對(duì)于用戶來說不透明。在編程的時(shí)候,我們只需要記住上一節(jié)中的變量作用域就可以了。但在想要寫出復(fù)雜的程序或者debug的時(shí)候,我們就需要相關(guān)的知識(shí)了。
進(jìn)程附加信息
除了上面的信息之外,每個(gè)進(jìn)程還要包括一些進(jìn)程附加信息,包括PID,PPID,PGID(參考Linux進(jìn)程基礎(chǔ)以及Linux進(jìn)程關(guān)系)等,用來說明進(jìn)程的身份、進(jìn)程關(guān)系以及其它統(tǒng)計(jì)信息。這些信息并不保存在進(jìn)程的內(nèi)存空間中。內(nèi)核會(huì)為每個(gè)進(jìn)程在內(nèi)核自己的空間中分配一個(gè)變量(task_struct結(jié)構(gòu)體)以保存上述信息。內(nèi)核可以通過查看自己空間中的各個(gè)進(jìn)程的附加信息就能知道進(jìn)程的概況,而不用進(jìn)入到進(jìn)程自身的空間 (就好像我們可以通過門牌就可以知道房間的主人是誰一樣,而不用打開房門)。每個(gè)進(jìn)程的附加信息中有位置專門用于保存接收到的信號(hào)(正如我們?cè)?a href="http://www.rzrgm.cn/vamei/archive/2012/10/07/2713023.html">Linux信號(hào)基礎(chǔ)中所說的“信箱”)。
fork & exec
現(xiàn)在,我們可以更加深入地了解fork和exec(參考Linux進(jìn)程基礎(chǔ))的機(jī)制了。當(dāng)一個(gè)程序調(diào)用fork的時(shí)候,實(shí)際上就是將上面的內(nèi)存空間,包括text, global data, heap和stack,又復(fù)制出來一個(gè),構(gòu)成一個(gè)新的進(jìn)程,并在內(nèi)核中為改進(jìn)程創(chuàng)建新的附加信息 (比如新的PID,而PPID為原進(jìn)程的PID)。此后,兩個(gè)進(jìn)程分別地繼續(xù)運(yùn)行下去。新的進(jìn)程和原有進(jìn)程有相同的運(yùn)行狀態(tài)(相同的變量值,相同的instructions...)。我們只能通過進(jìn)程的附加信息來區(qū)分兩者。
程序調(diào)用exec的時(shí)候,進(jìn)程清空自身內(nèi)存空間的text, global data, heap和stack,并根據(jù)新的程序文件重建text, global data, heap和stack (此時(shí)heap和stack大小都為0),并開始運(yùn)行。
(現(xiàn)代操作系統(tǒng)為了更有效率,改進(jìn)了管理fork和exec的具體機(jī)制,但從邏輯上來說并沒有差別。具體機(jī)制請(qǐng)參看Linux內(nèi)核相關(guān)書籍)
這一篇寫了整合了許多東西,所以有些長(zhǎng)。這篇文章主要是概念性的,許多細(xì)節(jié)會(huì)根據(jù)語言和平臺(tái)乃至于編譯器的不同而有所變化,但大體上,以上的概念適用于所有的計(jì)算機(jī)進(jìn)程(無論是Windows還是UNIX)。更加深入的內(nèi)容,包括線程(thread)、進(jìn)程間通信(IPC)等,都依賴于這里介紹的內(nèi)容。
總結(jié)
函數(shù),變量的作用范圍,global/local/dynamic variables
global data, text,
stack, stack frame, return address, stack overflow
heap, malloc, free, memory leakage
進(jìn)程附加信息, task_struct
fork & exec
歡迎閱讀“騎著企鵝采樹莓”系列文章
如果你喜歡這篇文章,歡迎推薦。

浙公網(wǎng)安備 33010602011771號(hào)