程式
什麼是程式?程式是一個執行中的程式實體,擁有獨立的堆疊、記憶體空間和邏輯控制流。
這是標準的程式概念。讓我們通過作業系統的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;
}
- 兩個引數a和b儲存在程式的堆疊中。
- 指標
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程式的獨立程式,所以:
- B程式的資料空間中的資料和A程式的資料空間的資料一致,但是,兩個程式的資料空間卻是不同的記憶體空間。
- B程式表中,指向LDT的選擇子和A程式表中的LDT選擇子不同。
- B程式表中的程式ID和A程式表中的程式ID不同。
堆疊
猜猜看,子程式的堆疊是在程式表中還是在資料空間中?
回答是:在程式的資料空間中。
在前面,我們雖然把堆疊和資料空間分開說,那是為了強調兩個要素在儲存資料時的差異。堆疊中的資料隨時變化,例如,程式中的一個函式執行結束,堆疊中的資料就會發生變化。
程式的資料空間呢?我以為,當程式結束執行的時候,程式的資料空間中的資料才會消失。這是我的猜測,暫時不知道怎麼去驗證。
認為堆疊儲存在資料空間中的依據是什麼?因為暫存器ss
中的選擇子指向的描述符描述的那段記憶體空間就是資料空間。
程式的ds、es、ss
的選擇子相同,指向相同的資料空間。
LDT、GDT和LDT選擇子
每個程式都有一個LDT。LDT儲存在程式的程式表中。
在程式的程式表中,有一個LDT選擇子。根據LDT選擇子,能從GDT中找到指向LDT的描述符。
有點繞。連起來再說一次:通過程式表中的LDT選擇子,從GDT中找到指向LDT的描述符,根據描述符找到LDT,LDT也在程式表中。
我的收穫
- 程式的堆疊儲存在程式的資料空間中。
- 堆疊是動態變化的,例如程式中的一個函式執行結束。堆疊中的資料容易消失,所以不能函式的返回值不能是指向堆疊的記憶體地址。
- 在函式中建立字串變數、結構體變數,資料儲存在程式的資料空間中,儲存在堆疊中的只是資料的記憶體地址。
- 每個程式的堆疊棧頂可以是相同的。我的作業系統在初始化程式時,之所以使用不同的堆疊棧頂,是因為我的作業系統沒有開啟虛擬記憶體地址,使用的是相同的記憶體空間。如果使用相同的堆疊棧頂,不同程式的堆疊會相互覆蓋。
- fork的實現:
- 子程式複製父程式的程式表,但是要使用不同的LDT選擇子。
- 子程式要複製父程式的資料空間,同時要修改子程式的LDT。