fork、父程式和子程式

東垂小夫發表於2021-07-23

程式

什麼是程式?程式是一個執行中的程式實體,擁有獨立的堆疊、記憶體空間和邏輯控制流。

這是標準的程式概念。讓我們通過作業系統的fork函式看看這個抽象的概念是怎麼在程式的實現中體現出來的。

構成要素

建立一個程式,需要程式體、程式表和資料空間。

程式體在C程式碼中對應一個函式,編譯成二進位制程式碼後就是一組指令。

程式表用來記錄程式的程式ID、程式名稱、暫存器快照空間。簡單說,當中斷髮生時,會儲存此刻CPU的狀態,然後記錄到程式表中。

程式表的作用就是用來儲存程式快照。

程式堆疊的作用是什麼?儲存程式中函式的引數,儲存程式執行過程中的區域性資料。

資料空間呢?先看一段簡單的程式碼。

char *f(int a, int b);

int main(int argc, char **argv)
{
  	f(5, 6);
  
  	return 0;
}

char *f(int a, int b)
{
  	int c = a + b;
  	char *str = "Hello, World!";
  	return str;
}
  1. 兩個引數a和b儲存在程式的堆疊中。
  2. 指標char *str指向的記憶體中的資料STR儲存在程式的資料空間中。

為什麼STR不是儲存在程式的堆疊中呢?

函式f的返回值是STR的記憶體地址。執行這段程式碼,我們會發現:呼叫函式f能正確獲得STR。

試想一下,假如STR儲存在程式的堆疊中,當f執行結束後,堆疊中的資料會被清空,我們呼叫函式f是不能正確獲得STR的。

STR儲存在程式的資料空間中,儲存在程式堆疊中的只是儲存STR的記憶體空間的記憶體地址。

fork

程式A呼叫fork新建程式B,A是B的父程式,B是A的子程式。

fork執行結束後,如果能成功建立B程式,B程式的資料空間、堆疊和程式表和A程式的這些要素完全相同。

差異

B程式畢竟是不同於A程式的獨立程式,所以:

  1. B程式的資料空間中的資料和A程式的資料空間的資料一致,但是,兩個程式的資料空間卻是不同的記憶體空間。
  2. B程式表中,指向LDT的選擇子和A程式表中的LDT選擇子不同。
  3. B程式表中的程式ID和A程式表中的程式ID不同。

堆疊

猜猜看,子程式的堆疊是在程式表中還是在資料空間中?

回答是:在程式的資料空間中。

在前面,我們雖然把堆疊和資料空間分開說,那是為了強調兩個要素在儲存資料時的差異。堆疊中的資料隨時變化,例如,程式中的一個函式執行結束,堆疊中的資料就會發生變化。

程式的資料空間呢?我以為,當程式結束執行的時候,程式的資料空間中的資料才會消失。這是我的猜測,暫時不知道怎麼去驗證。

認為堆疊儲存在資料空間中的依據是什麼?因為暫存器ss中的選擇子指向的描述符描述的那段記憶體空間就是資料空間。

程式的ds、es、ss的選擇子相同,指向相同的資料空間。

LDT、GDT和LDT選擇子

每個程式都有一個LDT。LDT儲存在程式的程式表中。

在程式的程式表中,有一個LDT選擇子。根據LDT選擇子,能從GDT中找到指向LDT的描述符。

有點繞。連起來再說一次:通過程式表中的LDT選擇子,從GDT中找到指向LDT的描述符,根據描述符找到LDT,LDT也在程式表中。

我的收穫

  1. 程式的堆疊儲存在程式的資料空間中。
  2. 堆疊是動態變化的,例如程式中的一個函式執行結束。堆疊中的資料容易消失,所以不能函式的返回值不能是指向堆疊的記憶體地址。
  3. 在函式中建立字串變數、結構體變數,資料儲存在程式的資料空間中,儲存在堆疊中的只是資料的記憶體地址。
  4. 每個程式的堆疊棧頂可以是相同的。我的作業系統在初始化程式時,之所以使用不同的堆疊棧頂,是因為我的作業系統沒有開啟虛擬記憶體地址,使用的是相同的記憶體空間。如果使用相同的堆疊棧頂,不同程式的堆疊會相互覆蓋。
  5. fork的實現:
    1. 子程式複製父程式的程式表,但是要使用不同的LDT選擇子。
    2. 子程式要複製父程式的資料空間,同時要修改子程式的LDT。

相關文章