我是如何學習寫一個操做系統(八):內存管理和段頁機制

前言

多進程和內存管理是緊密相連的兩個模塊,由於運行進程也就是從內存中取指執行,建立進程首先要將程序和數據裝入內存。將用戶原程序變成可在內存中執行的程序,而這就涉及到了內存管理。html

內存的裝入

  • 絕對裝入。

    在編譯時,若是知道程序將駐留在內存的某個位置,編譯程序將產生絕對地址的目標代碼。絕對裝入程序按照裝入模塊的地址,將程序和數據裝入內存。裝入模塊被裝入內存後,因爲程序中的邏輯地址與實際地址徹底相同,故不需對程序和數據的地址進行修改。git

  • 可重定位裝入。

    在多道程序環境下,多個目標模塊的起始地址一般都是從0開始,程序中的其餘地址都是相對於起始地址的,此時應採用可重定位裝入方式。根據內存的當前狀況,將裝入模塊裝入到內存的適當位置。裝入時對目標程序中指令和數據的修改過程稱爲重定位,地址變換一般是裝入時一次完成,因此成爲靜態重定位。 其特色是在一個做業裝入內存時,必須分配器要求的所有內存空間,若是沒有足夠的內存,就不能裝入,此外一旦做業進入內存後,在整個運行期間,不能在內存中移動,也不能再申請內存空間。程序員

  • 動態運行時裝入

    也成爲動態重定位,程序在內存中若是發生移動,就須要採用動態的裝入方式。算法

    動態運行時的裝入程序在把裝入模塊裝入內存後,並不當即把裝入模塊中的相對地址轉換爲絕對地址,而是把這種地址轉換推遲到程序真正要執行時才進行。所以,裝入內存後的全部地址均爲相對地址,這種方式須要一個重定位寄存器的支持。 其特色是能夠將程序分配到不連續的存儲區中;在程序運行以前能夠只裝入它的部分代碼便可運行,而後在程序運行期間,根據須要動態申請分配內存;便於程序段的共享,能夠向用戶提供一個比存儲空間大得多的地址空間。緩存

參考連接函數

因此裝入內存的最好方法應該就是動態運行時的裝入,可是這種方法須要一個方法來進行重定位。這個重定位的信息就保存在每一個進程的PCB中,也就是保存這塊內存的基地址,因此最後在運行時的地址就是邏輯地址 + 基地址,而硬件也提供了相應計算的支持,也就是MMU性能

分段機制

可是在程序員眼裏:程序由若干部分(段)組成,每一個段有各自的特色、用途:代碼段只讀,代碼/數據段不會動態增加...。這樣就引出了對內存進行分段測試

分段

假如用戶進程由主程序、兩個字程序、棧和一段數據組成,因而能夠把這個用戶進程劃分爲5個段,每段從0開始編址,並採用一段連續的地址空間(段內要求連續,段間不要求連續),其邏輯地址由兩部分組成:段號與段內偏移量,分別記爲S、W。this

段號爲16位,段內偏移量爲16位,則一個做業最多可有2的16次方16=65536個段,最大段長64KB。spa

sdasd.png

GDT和LDT

每一個進程都有一張邏輯空間與主存空間映射的段表,其中每一段表項對應進程的一個段,段表項紀錄路該段在內存中的起始地址和段的長度。在配置了段表後,執行中的進程可經過查找段表,找到每一個段所對應的內存區。段表用於實現從邏輯端段到物理內存區的映射。

而這個段表就是以前在保護模式提到的GDT和LDT

一個處理器只有一個GDT。LDT(局部描述表),一個進程一個LDT,其實是GTD的一個「子表」。

LDT和GDT從本質上說是相同的,只是LDT嵌套在GDT之中。

有一個專門的寄存器LDTR來記錄局部描述符表的起始位置,記錄的是在GDT中的一個段選擇子。因此本質上LDT是一個段描述符,這個描述符就存儲在GDT中,對應這個表述符也會有一個選擇子。

內存分區和分頁

在用了分段機制後,那麼就須要對內存進行分區,讓各個段載入到相應的內存分區中

內存分配算法

在進程裝入或換入主存時。若是內存中有多個足夠大的空閒塊,操做系統必須肯定分配那個內存塊給進程使用,一般有這幾種算法

  • 首次適應算法:空閒分區以地址遞增的次序連接。分配內存時順序查找,找到大小能知足要求的第一個空閒分區。

  • 最佳適應算法:空閒分區按容量遞增造成分區鏈,找到第一個能知足要求的空閒分區。

  • 最壞適應算法:有稱最大適應算法,空閒分區以容量遞減次序連接。找到第一個能知足要求的空閒分區,也就是挑選最大的分區。

  • 臨近適應算法:又稱循環首次適應算法,由首次適應算法演變而成。不一樣之處是分配內存時今後查找結束的位置開始繼續查找。

內存分頁

引入內存分頁就是爲了解決在進行內存分區時致使的內存效率問題

分頁就是把真正的內存空間劃分爲大小相等且固定的塊,塊相對較小,做爲內存的基本單位。每一個進程也以塊爲單位進行劃分,進程在執行時,以塊爲單位逐個申請主存中的塊空間。因此這時候對真正的物理內存地址的映射就不能再用分段機制的那套了

就引入了頁表概念:系統爲每一個進程創建一張頁表,記錄頁面在內存中對應的物理塊號,因此對地址的轉換變成了對頁表的轉換

在系統中一般設置一個頁表寄存器PTR,存放頁表在內存的初值和頁表長度。

  • 地址分爲頁號和頁內偏移量兩部分,再用頁號去檢索頁表。。

  • 將頁表始址與頁號和頁表項長度的乘積相加,便獲得該表項在頁表中的位置,因而可從中獲得該頁的物理塊號。

可是頁表仍是有兩個問題:

  1. 頁表佔用的內存大
  2. 頁表須要頻繁的進行地址訪問,因此訪存速度必須很是快

多級頁表

爲了解決頁表佔用的內存太大,就引入了多級頁表

頁目錄有2的十次方個4字節的表項,這些表項指向對應的二級表,線性地址的最高10位做爲頁目錄用來尋找二級表的索引

二級頁表裏的表項含有相關頁面的20位物理基地址,二級頁表使用線性地址中間10位來做爲尋找表項的索引

  • 進程訪問某個邏輯地址
  • 由線性地址中的頁號,以及外層頁表寄存器(CR3)中的外層頁表始址,找到二級頁表的始址
  • 由二級頁表的始址,加上線性地址中的外層頁內地址,找到對應的二級頁表中的頁表項
  • 由頁表項中的物理塊號,加上線性地址中的頁內地址,找到對物理地址

快表

爲了解決訪存速度,就有了快表

在系統中一般設置一個頁表寄存器PTR,存放頁表在內存的初值和頁表長度。

  • CPU給出有效地址後,由硬件進行地址轉換,並將頁號送入高速緩存寄存器,並將此頁號與快表中的全部頁號同時進行比較。

  • 若是有找到匹配的頁號,說明索要訪問的頁表項在快表中,則能夠直接從中讀出該頁對應的頁框號。

  • 若是沒有找到,則須要訪問主存中的頁表,在讀出頁表項後,應同時將其存入快表中,以供後面可能的再次訪問。可是若是快表已滿,就必須按照必定的算法對其中舊的頁表項進行替換。

段頁結合(虛擬內存)

既然有了段和頁,程序員但願能用段,計算機設計但願用頁,那麼就須要將兩者結合

因此邏輯地址和物理地址的轉換:

  • 首先將給定一個邏輯地址,

  • 利用其段式內存管理單元,也就是GDT中的斷描述符,先將爲個邏輯地址轉換成一個線性地址,

  • 再利用頁式內存管理單元查表,轉換爲物理地址。

虛擬內存的管理

在實際的操做上,頗有可能當前可用的物理內存遠小於分配的虛擬內存(4G),這時候就須要請求掉頁功能和頁面置換功能,也就是在進行地址轉換的時候找不到對應頁,就啓動頁錯誤處理程序來完成調頁

這樣在頁表項中增長了四個段:

  • 狀態位P:用於指示該頁是否已調入內存,共程序訪問時參考。

  • 訪問字段A:用於記錄本頁在一段時間內被訪問的次數,或記錄本頁最近已有多長時間未被訪問,供置換算法換出頁面時參考。

  • 修改位M:表示該頁在調入內存後是否被修改過。

  • 外存地址:用於指出該也在外存上的地址,一般是物理塊號,供調入該頁時參考。

因此如今在查找物理地址的時候就變成了:

  • 先檢索快表

  • 若找到要訪問的頁,邊修改頁表中的訪問位,而後利用頁表項中給出的物理塊號和頁內地址造成物理地址。

  • 若爲找到該頁的頁表項,應到內存中去查找頁表,在對比頁表項中的狀態位P,看該頁是否已調入內存,未調入則產生缺頁中斷,請求從外存把該頁調入內存。

頁面置換算法

既然有頁面的換入換出,那天然就會有相應的不一樣的算法

最佳置換算法所選擇的被淘汰頁面將是之後永不使用的,或者是在最長時間內再也不被訪問的頁面,這樣能夠保證得到最低的缺頁率。但因爲人們目前沒法預知進程在內存下的若干頁面中那個是將來最長時間內再也不被訪問的,可是這種算法沒法實現。

LRU算法

選擇最近最長時間未訪問過的頁面予以淘汰,他認爲過去一段時間內未訪問過的頁面,在最近的未來可能也不會被訪問。該算法爲每一個頁面設置一個訪問字段,來記錄頁面自上次被訪問以來所經歷的時間,淘汰頁面時選擇現有頁面中值最大的予以淘汰。

LRU算法通常有兩種實現:

  • 時間戳

每次地址訪問都修改時間戳,淘汰頁的時候只須要選擇次數最少的便可

可是需維護一個全局時鐘,需找到最小值,實現代價較大

  • 頁碼棧

在每次地址訪問的時候都修改棧,這樣在淘汰的時候,只須要將棧底換出

fx.png

可是和上面用時間戳的方法同樣,實現的代價都很是大

CLOCK算法

給每一個頁幀關聯一個使用位。當該頁第一次裝入內存或者被從新訪問到時,將使用位置爲1。每次須要替換時,查找使用位被置爲0的第一個幀進行替換。在掃描過程當中,若是碰到使用位爲1的幀,將使用位置爲0,在繼續掃描。若是所謂幀的使用位都爲0,則替換第一個幀

CLOCK算法的性能比較接近LRU,而經過增長使用的位數目,但是使得CLOCK算法更加高效。在使用位的基礎上再增長一個修改位,則獲得改進型的CLOCK置換算法。這樣,每一幀都出於如下四種狀況之一。

  1. 最近未被訪問,也未被修改(u=0,m=0)。
  2. 最近被訪問,但未被修改(u=1,m=0)。
  3. 最近未被訪問,但被修改(u=0,m=1)。
  4. 最近被訪問,被修改(u=1,m=1)。

算法執行以下操做步驟:

  • 從指針的當前位置開始,掃描幀緩衝區。在此次掃描過程當中,對使用位不做任何修改,選擇遇到的第一個幀(u=0,m=0)用於替換。
  • 若是第1步失敗,則從新掃描,查找(u=0,m=1)的幀。選額遇到的第一個這樣的幀用於替換。在這個掃面過程當中,對每一個跳過的幀,把它的使用位設置成0.
  • 若是第2步失敗,指針將回到它的最初位置,而且集合中全部幀的使用位均爲0.重複第一步,而且若是有必要重複第2步。這樣將能夠找到供替換的幀。

改進型的CLOCK算法優於簡單的CLOCK算法之處在於替換時首選沒有變化的頁。因爲修改過的頁在被替換以前必須寫回,於是這樣作會節省時間。

Linux 0.11的故事

全部有關管理內存都是爲了服務進程而誕生的,因此先來看一下Linux 0.11裏從建立進程開始的故事

fork

  • 首先經過系統調用的中斷來建立進程,fork()->sys_fork->copy_process
  • copy_process的主要做用就是爲子進程建立TSS描述符、分配內存和文件設定等等
  • copy_mem這裏僅爲新進程設置本身的頁目錄表項和頁表項,而沒有實際爲新進程分配物理內存頁面。此時新進程與其父進程共享全部內存頁面。
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, long ebx,long ecx,long edx, long fs,long es,long ds, long eip,long cs,long eflags,long esp,long ss) {
	struct task_struct *p;
	int i;
	struct file *f;

	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)
		if ((f=p->filp[i]))
			f->f_count++;
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case */
	return last_pid;
}

int copy_mem(int nr,struct task_struct * p) {
	unsigned long old_data_base,new_data_base,data_limit;
	unsigned long old_code_base,new_code_base,code_limit;

	code_limit=get_limit(0x0f);
	data_limit=get_limit(0x17);
	old_code_base = get_base(current->ldt[1]);
	old_data_base = get_base(current->ldt[2]);
	if (old_data_base != old_code_base)
		panic("We don't support separate I&D");
	if (data_limit < code_limit)
		panic("Bad data_limit");
	new_data_base = new_code_base = nr * 0x4000000;
	p->start_code = new_code_base;
	set_base(p->ldt[1],new_code_base);
	set_base(p->ldt[2],new_data_base);
	if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
		printk("free_page_tables: from copy_mem\n");
		free_page_tables(new_data_base,data_limit);
		return -ENOMEM;
	}
	return 0;
}
複製代碼

page

  • copy_page_tables就是複製頁目錄表項和頁表項,從而被複制的頁目錄和頁表對應的原物理內存頁面區被兩套頁表映射而共享使用。複製時,需申請新頁面來存放新頁表,原物理內存區將被共享。此後兩個進程(父進程和其子進程)將共享內存區,直到有一個進程執行操做時,內核纔會爲寫操做進程分配新的內存頁(寫時複製機制)。
int copy_page_tables(unsigned long from,unsigned long to,long size) {
	unsigned long * from_page_table;
	unsigned long * to_page_table;
	unsigned long this_page;
	unsigned long * from_dir, * to_dir;
	unsigned long nr;

	if ((from&0x3fffff) || (to&0x3fffff))
		panic("copy_page_tables called with wrong alignment");
	from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
	to_dir = (unsigned long *) ((to>>20) & 0xffc);
	size = ((unsigned) (size+0x3fffff)) >> 22;
	for( ; size-->0 ; from_dir++,to_dir++) {
		if (1 & *to_dir)
			panic("copy_page_tables: already exist");
		if (!(1 & *from_dir))
			continue;
		from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
		if (!(to_page_table = (unsigned long *) get_free_page()))
			return -1;	/* Out of memory, see freeing */
		*to_dir = ((unsigned long) to_page_table) | 7;
		nr = (from==0)?0xA0:1024;
		for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
			this_page = *from_page_table;
			if (!(1 & this_page))
				continue;
			this_page &= ~2;
			*to_page_table = this_page;
			if (this_page > LOW_MEM) {
				*from_page_table = this_page;
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;
			}
		}
	}
	invalidate();
	return 0;
}
複製代碼

no_page

若是找不到相應的頁,也就是要執行換入和換出了,在此以前CPU會先觸發缺頁異常

  • 缺頁異常中斷的處理,會調用do_no_page
page_fault:
	xchgl %eax,(%esp)       # 取出錯碼到eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%edx         # 置內核數據段選擇符
	mov %dx,%ds
	mov %dx,%es
	mov %dx,%fs
	movl %cr2,%edx          # 取引發頁面異常的線性地址
	pushl %edx              # 將該線性地址和出錯碼壓入棧中,做爲將調用函數的參數
	pushl %eax
	testl $1,%eax           # 測試頁存在標誌P(爲0),若是不是缺頁引發的異常則跳轉
	jne 1f
	call do_no_page         # 調用缺頁處理函數
	jmp 2f
1:	call do_wp_page         # 調用寫保護處理函數
2:	addl $8,%esp            # 丟棄壓入棧的兩個參數,彈出棧中寄存器並退出中斷。
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret
複製代碼
  • 該函數首先嚐試與已加載的相同文件進行頁面共享,或者只是因爲進程動態申請內存頁面而只需映射一頁物理內存便可。若共享操做不成功,那麼只能從相應文件中讀入所缺的數據頁面到指定線性地址處。
void do_no_page(unsigned long error_code,unsigned long address) {
	int nr[4];
	unsigned long tmp;
	unsigned long page;
	int block,i;

	address &= 0xfffff000;
	tmp = address - current->start_code;
	if (!current->executable || tmp >= current->end_data) {
		get_empty_page(address);
		return;
	}
	if (share_page(tmp))
		return;
	if (!(page = get_free_page()))
		oom();
/* remember that 1 block is used for header */
	block = 1 + tmp/BLOCK_SIZE;
	for (i=0 ; i<4 ; block++,i++)
		nr[i] = bmap(current->executable,block);
	bread_page(page,current->executable->i_dev,nr);
	i = tmp + 4096 - current->end_data;
	tmp = page + 4096;
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;
	}
	if (put_page(page,address))
		return;
	free_page(page);
	oom();
}
複製代碼

小結

這一篇的篇幅很長,由於把有關內存管理的東西都寫在一塊兒了,主要有三個關鍵點:

  • 分段

    對內存的分段引伸的GDT和IDT來進行物理地址的尋址

  • 分頁

    再因爲分段引出的內存分區,爲了提升效率而引入的分頁機制,重點就是用頁式內存管理單元查表

  • 段頁結合 因此爲了將段和頁結合就須要一個機制來轉化邏輯地址和物理地址,也就分爲兩步走

    1. 利用其段式內存管理單元,也就是GDT中的斷描述符,先將爲個邏輯地址轉換成一個線性地址,

    2. 再利用頁式內存管理單元查表,轉換爲物理地址。

相關文章
相關標籤/搜索