程序
什麼是程序?程序是一個執行中的程式實體,擁有獨立的堆疊、記憶體空間和邏輯控制流。
這是標準的程序概念。讓我們通過作業系統的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。