JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

行程的內部:位址空間與 PCB

把一個執行中的行程剖開來看看。我們走訪它私有記憶體的四個街區——程式碼、資料、堆積與堆疊——再認識 PCB:核心的那份病歷,讓行程能被暫停又恢復,自己卻渾然不覺。

我們在哪裡:從「活著」到「它由什麼構成」

在上一篇裡,我們在程式與行程之間畫下了那條大界線:程式是書架上的食譜,行程則是真正在被烹調的那道菜。我們說過行程帶著「執行所需的進行中狀態」,但把那團狀態擱成了一個含糊的包裹。這篇就要把蓋子撬開。讀完之後,你能指出某個變數住在哪裡、為什麼一個巨大的陣列卻能產生一個極小的檔案,以及核心如何把一個行程在半步之間凍結、稍後再解凍,彷彿什麼都沒發生過。

要理解一個行程,其實有兩半。一半是行程自己看得到的:它私有的記憶體,稱為位址空間,劃分成幾個一眼能認出的街區。另一半是只有核心看得到的:一筆關於該行程的隱藏紀錄,也就是行程控制區塊,被妥善地放在行程碰不到的地方。我們先走訪看得見的那一半,再看隱藏的那一半,最後看著兩者如何協同運作。

位址空間的四個街區

位址空間想成一間私有公寓,從位址 0 的大門到最高位址那面最裡的牆,一間一間編號。行程在裡面隨自己喜好擺家具,而且窺探不到別人的公寓。這間公寓裡住著四個眾所周知的區域,一旦你叫得出它們的名字,多數記憶體之謎便迎刃而解。由低位址往上讀:文字區段,接著是資料與 BSS,再來是堆積,最頂端則是堆疊。

文字區段存放程式的機器指令——也就是 CPU 所執行的編譯後程式碼。它就是印出來的食譜步驟:你讀它們,但絕不在上面塗改,所以核心把這些頁標記為唯讀且可執行。由於指令對每個實例都相同、又永不改變,核心可以只保留一份實體副本,讓所有執行同一程式的行程共用。緊鄰其上的是資料區段,存放「帶著寫進程式裡的非零起始值」的全域與靜態變數(例如一個設為 1 的計數器),以及存放「從零開始」之全域與靜態變數的 BSS。巧妙之處在於:從零開始的變數不需要儲存任何值,只需保留一個大小,這正是為什麼一個宣告了百萬位元組零填充陣列的程式,編譯出來仍是個極小的可執行檔。

最上面的兩個街區是活潑的那兩個。堆積是一間公用儲物室:當程式在執行時要求記憶體(C 裡的 malloc、高階語言裡的一個新物件),堆積管理器交回一塊區塊,程式用完後再歸還。堆積隨著要求增多而往上長。堆疊則是一疊自助餐托盤:每一次函式呼叫推進一個框,存放該次呼叫的區域變數、參數與返回位址;每一次返回則彈出它的框。堆疊位於高端並往下長,於是堆積(往上長)與堆疊(往下長)穿越中間那塊大空隙朝彼此推進,各自為對方留下擴張的餘地。

一個微妙的事實:那些位址是虛擬的

讓「私有公寓」這個比喻站得住腳的安靜把戲在此。行程使用的那些號碼是邏輯(虛擬)位址,並非 RAM 裡真正的位置。同一個位址號碼在兩個不同的行程裡指向兩個不同的實體格子,因為硬體會即時地、分別地轉譯每個行程的位址。於是兩個行程可以都安心地使用「位址 4096」而毫無衝突。這正是公寓得以保持私密、每個行程得以假裝自己擁有一整塊從零開始之乾淨記憶體的原因。

一個直接的後果絆倒了許多初學者:位址空間的大小不等於正在使用的 RAM 量。一個 32 位元行程能指名從 0 到 2^32 - 1 的每一個位址,範圍達 4 GB,即使機器只有 1 GB 的 RAM 也一樣。那個範圍幾乎全是空的。只有行程實際碰到的頁——一些程式碼、一些資料、它觸及過的堆積與堆疊頁——才有真正的記憶體支撐。位址空間是一張「可能位址」的地圖,而不是已耗用記憶體的度量。我們爬到記憶體管理那幾級時,會見到負責這項轉譯的機制(記憶體管理單元與分頁表);現在只要記住:行程所見的,是一個私有的、虛擬的視野。

PCB:核心為單一行程準備的病歷

上面所說的一切,都是行程看得到的。現在來認識只有核心看得到的。想像醫院為每位病人保留一份病歷:姓名與編號、目前狀況、用藥、待做的檢查。護理師可以先把一位病人擱下、稍後再接手,因為恢復照護所需的一切都在病歷上。行程控制區塊(PCB)正是某個行程的那份病歷——核心私有的紀錄,存放著它為了正確地暫停與恢復這個行程所需的一切。

病歷上記些什麼?一個好用的檢驗能回答它:核心在該行程「不執行的期間」必須記住的任何東西,都得住在 PCB 裡,因為一旦另一個行程開始執行,CPU 的暫存器就被覆寫了。所以一個典型的 PCB 存放著行程識別碼(PID);當前狀態(執行中、就緒、等待);保存下來的 CPU 暫存器,其中至關重要的是指出下一條指令的程式計數器;說明此行程位址空間位於何處的記憶體管理資訊;開啟檔案的清單;像優先權這類排程資訊;以及像已使用 CPU 時間這類記帳資料。從很真實的意義上說,PCB 就是「被濃縮成單一紀錄、核心能握在手中」的那個行程。

把它們串起來:暫停與恢復一個行程

位址空間與 PCB 並非兩件各自獨立的事實;它們是同一個機制的兩半。當眾多行程共用一顆 CPU 時,核心必須不斷地停下一個、啟動另一個。PCB 正是「讓這件事得以發生、而執行中的程式卻從未察覺到中斷」的東西。這就是它的核心——那組「保存與還原」,下一篇將以「環境切換」之名完整研究它。

  1. 一個切換的理由到來:執行中行程輪到的時間結束、一個中斷觸發,或它要求了某個尚未備妥的東西而必須阻塞。
  2. 核心把執行中行程的 CPU 暫存器與程式計數器從 CPU 複製進該行程的 PCB,將它在半步之間凍結。
  3. 核心選出下一個要執行的行程,並更新記憶體管理狀態(指向「哪個位址空間正在生效」的那個指標),使硬體接下來會去轉譯新行程的虛擬位址。
  4. 核心把選中行程被保存的暫存器與程式計數器從它的 PCB 複製回 CPU,執行便從它先前抵達的那一條指令處精確地恢復——彷彿它從未停下。

現在你能看出為什麼兩半要合在一起才有意義。切換位址空間指標,正是讓「位址 4096」指向新行程的記憶體、而非舊行程之記憶體的那個動作。從 PCB 還原暫存器,則是讓程式計數器精準落回「下一條指令」的那個動作。核心不會把這些任何一項存進行程自己的記憶體——那塊記憶體屬於行程,甚至可能被換出到磁碟。它把這份「可恢復的快照」存在 PCB 裡,並把它所知的每個行程的 PCB 統統放進一張總清單——行程表——好讓它能藉走訪這張表來尋找、排程並清理任何一個行程。