理解進程的新建和執行過程
- 2020 年 3 月 12 日
- 筆記
本文以linux0.11版本為基礎,分析進程的記憶體布局,現代版本已經發生比較大的變化,都是很多原理都是類似的。 系統維護了一個全局的數據結構叫GDT( Global Descriptor Table),他保存了所有進程的程式碼段數據段的一些資訊。系統有專門的暫存器保存了GDT的地址,叫GDTR。GTDR的格式如下。

有專門的指令把這個地址載入到GDTR中。叫LGDT。每個進程可以定義一個LDT,用於存儲程式碼段和數據段資訊。GDT布局如下。

GDT每個項(GDT描述符)對應的結構體是

我們回顧task_struct結構,看到有兩個屬性desc_struct,和tss_struct。desc_struct是保存進程程式碼段和數據段資訊的,tss_struct是保存進程執行上下文的。這兩個結構體的定義如下。
struct desc_struct { unsigned long a,b; } struct tss_struct { long back_link; /* 16 high bits zero */ long esp0; long ss0; /* 16 high bits zero */ long esp1; long ss1; /* 16 high bits zero */ long esp2; long ss2; /* 16 high bits zero */ long cr3; long eip; long eflags; long eax,ecx,edx,ebx; long esp; long ebp; long esi; long edi; long es; /* 16 high bits zero */ long cs; /* 16 high bits zero */ long ss; /* 16 high bits zero */ long ds; /* 16 high bits zero */ long fs; /* 16 high bits zero */ long gs; /* 16 high bits zero */ long ldt; /* 16 high bits zero */ long trace_bitmap; /* bits: trace 0, bitmap 16-31 */ struct i387_struct i387; };
我們從fork函數開始,看看這些數據結構的設置和關係。我們先來看一下一些宏定義。他是根據進程號(進程id)計算出在GDT中的索引。可參考上圖。
/* * Entry into gdt where to find first TSS. 0-nul, 1-cs, 2-ds, 3-syscall * 4-TSS0, 5-LDT0, 6-TSS1 etc ... */ #define FIRST_TSS_ENTRY 4 #define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1) // 第一個tss選擇子的偏移是4<<3,4乘以8,等於32,即從GDT的偏移為32開始算,第一個進程的n是0,tss是32 #define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3)) // 第一個ldt選擇子的偏移是5<<3,5乘以8,等於40,即從GDT的偏移為40開始算,第一個進程的n是0,ldt是40 #define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
下面程式碼來自fork。
// nr是進程id,計算進程的ldt結構在gdt中的索引,執行該進程的時候,從GDT的第tss->ldt項中取得進程的資訊。p即task_struct p->tss.ldt = _LDT(nr); // 設置進程的線性地址的首地址,每個進程佔64MB,0.11的進程線性地址是每個進程64M,不是4GB new_data_base = new_code_base = nr * 0x4000000; p->start_code = new_code_base; // 設置線性地址到ldt的描述符中 set_base(p->ldt[1],new_code_base); set_base(p->ldt[2],new_data_base); // 把父進程的頁目錄項和頁表複製到子進程,old_data_base,new_data_base是線性地址,父子進程共享物理頁面,即copy on write copy_page_tables(old_data_base,new_data_base,data_limit); /* 掛載tss和ldt地址到gdt,nr << 1即乘以2,這裡算出的是第nr個進程距離第一個tss描述符地址的偏移, 單位是8個位元組,即選擇描述符大小,_LDT是偏移的大小,單位是1,這裡是8 */ set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));

fork執行完之後,新新建的相關數據結構已經建立好了,並且也和系統的管理數據產生了關聯。有自己獨立的頁表,和父進程共享物理地址。那麼當這個進程被調度的時候,他會發生什麼。 執行進程的時候,根據進程號,算出tss在gdt的索引,然後把索引里指向的tss里的上下文也載入到對應的暫存器,tss資訊中的ldt索引首先從gdt找到進程ldt結構體數據的首地址,即desc_struct結構體數組,然後根據當前段的屬性,比如程式碼段,則從cs中取得選擇子,系統從ldt表中取得進程線性空間的首地址、限長、許可權等資訊。用線性地址的首地址加上ip中的偏移,得到線性地址,然後再通過頁目錄和頁表得到物理地址,物理地址還沒有分配則進行缺頁異常等處理。