fork、父進程和子進程

  • 2021 年 7 月 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。