1. 程式人生 > >作業系統(10) -- 段頁結合的實際記憶體管理模型

作業系統(10) -- 段頁結合的實際記憶體管理模型

前面說過使用者程式喜歡分段來管理記憶體,但是實際的實體記憶體更加傾向於分頁管理,因為這樣可以使記憶體的利用率最大化。作為作業系統,既要向上負責,又要向下負責。這一篇部落格主要談談使用者程式需要的段和實體記憶體需要的頁是如何結合到一起的。

虛擬記憶體

虛擬記憶體的引入

首先兩個條件,第一:實體記憶體必須得是分頁管理的;第二:對使用者來說是分段的。但是使用者程式最終又得在記憶體上面跑,因此肯定需要某種機制或者轉化使得以使用者程式的視角看起來記憶體是分段的,以實體記憶體的視角看起來又是分頁的,這種機制就是虛擬記憶體

什麼是虛擬記憶體

虛擬記憶體是一種和實體記憶體差不多的東西,每一個位元組都有對應的地址。但是有一點與實體記憶體不同,其實從它的名字就能看出來,“虛擬”:即實際上並不存在,它只是一種機制,純粹是用程式表示的,沒有這種硬體。它的作用就是讓上層程式看起來是記憶體是分段的,而實際上是分頁的。那它是怎麼實現的呢?

基本思想:使用者程式使用了一段記憶體,那麼首先會在虛擬記憶體上面找到一段空的記憶體,然後將使用者程式使用的記憶體對映到這段記憶體上,然後將這段記憶體分頁,再對映到實體記憶體上面。

從這裡能看出,使用者程式使用的邏輯記憶體經過了兩次對映才達到實體記憶體,第一次對映是段的對映,需要段表;第二次是頁的對映,需要頁表。那麼邏輯地址究竟是如何變成實體地址的呢?邏輯地址是段號+偏移(CS:IP)組成的,首先根據段號在段表中找到虛擬記憶體的段基址,然後加上偏移得到虛擬地址(即在虛擬記憶體上面的地址),格式是:頁號+偏移。然後根據頁號在頁表中找到對應的頁框號,再加上偏移得到最後的實體地址。實現了邏輯地址與實體地址的對應。也就是重定位操作。

一個實際的段、頁式記憶體管理

記憶體管理的核心就是記憶體分配,所以從程式放入記憶體、使用記憶體開始。其實只要程式可以使用記憶體,就說明記憶體使用起來了;管理就是如何更好的使用;這點可以類似CPU的管理,首先使用起來,然後如何讓它更高效的使用。

程式放入記憶體首先就是要在虛擬記憶體中給它分配段、建立段表,然後是分配頁、建立頁表;注意先後關係。第一步:在虛擬記憶體上分配段;如何分配呢?首先肯定是得找到空閒的段,如何找,可以使用前面談到過的記憶體分割槽方法。這個見前文《記憶體的分段與分頁》。然後將使用者程式對映到虛擬記憶體,建立段表,然後分配頁,建立頁表。這裡的分配頁並不是在記憶體裡面查詢空閒頁的,而是另外一種方式。

分配記憶體、建立段表

從前面的系統呼叫可以知道fork()呼叫首先是sys_fork->copy_process。

在Linux/kernel/fork.c
int copy_process(int nr, long ebp...)
{
	………………
	copy_mem(nr,p);
	………………
}

int copy_mem(int nr, task_struct *p)
{
	unsigned long new_data_base;
	new_data_base = nr*0x4000000;		// nr * 64M
	set_base(p_>ldt[1], new_data_base);		// 程式碼段
	set_base(p->ldt[2], new_data_base);		// 資料段
	………………
}

上面是fork()建立一個程序執行的程式碼。首先經過系統呼叫到copy_process中, 然後在copy_process中呼叫copy_men();這個函式就是給該程序在虛擬記憶體上分配記憶體空間的,形參nr和p分別表示:第nr個程序和該程序的pcb。

new_data_base = nr*0x4000000;		// nr * 64M

首先給該程序在虛擬記憶體上分配一塊64M的記憶體塊。可以看到第0個程序記憶體區域就是064M,第一個程序64128M,依次類推,互不重疊。然後將p的ldt[1]和ldt[2]都指向這塊記憶體。如下圖 在這裡插入圖片描述 到這裡為止,在虛擬記憶體上分配記憶體、建立段表就弄好了。

分配記憶體、建立頁表

接下來就是分配記憶體、建立頁表。還是上面那個copy_mem()函式

int copy_mem(int nr, task_struct *p)
{
	unsigned long old_data_base;
	old_data_base = get_base(currnet->ldt[2]);
	copy_page_tables(old_data_base, new_data_base, data_limit);
	………………
}

int copy_page_tables(unsigned long from, unsigned long to , long size)
{
	from_dir = (unsigned long *) ((from>>20) & 0xffc);
	to_dir = (unsigned long * )((to>>20) & 0xffc);
	size = (unsigned long)(size + 0x3fffff) >> 22;

	for (; size-->0; from_dir++, to_dir++)
	{
		from_page_table=(0xfffff000 & *from_dir);
		to_page_table = get_free_page();
		*to_dir = ((unsigned long) to_page_table) | 7;
	}
}

首先看copy_mem函式

old_data_base = get_base(currnet->ldt[2]);

這條語句的含義就是得到當前程序的虛擬記憶體地址賦給old_data_base;然後呼叫copy_page_tables()函式,首先from和to是什麼?從形參以及copy_mem裡面的呼叫可以看出,這兩個都是32為虛擬記憶體地址。from_dir指向一個父程序的頁目錄項(章),to_dir指向一個子程序的頁目錄項(章)。前面說過了32位虛擬記憶體地址的構成如下圖。 在這裡插入圖片描述

from_dir = (unsigned long *) ((from>>20) & 0xffc);

這句話是什麼意思?from右移22位得到的是頁目錄號,但是

(from>>20) & 0xffc

是什麼意思。回想一下多級頁表的工作原理,from>>22得到的是目錄項編號,每一項都是4位元組,即from>>22之後乘以4就得到該項的相對於頁目錄指標(CR3)的偏移了,也就是可以找到具體的頁目錄號。(from>>20) & 0xffc這裡看視訊的時候沒怎麼聽懂,上面這個解釋是我自己的理解。而from>>22乘以4不正好是(from>>20) & 0xffc嗎。

size就是頁目錄項數(章數)。

for (; size-->0; from_dir++, to_dir++)
{
	from_page_table=(0xfffff000 & *from_dir);
	to_page_table = get_free_page();
	*to_dir = ((unsigned long) to_page_table) | 7;
}

前面說過from_dir指向一個父程序的頁目錄項(章),那麼*from_dir就是from_dir對應的那個頁目錄表(節),也就是from_page_table的含義。get_free_page()新建一個子程序的頁目錄表(節);然後將這個頁目錄表賦給to_dir,但是to_dir指向的這個表裡面的內容還是空的,接下來就是要將這個表填上

for (; nr-->0; from_page_table++, to_page_table++)
{
	this_page = *from_page_table;
	this_page &= ~2;		// 設定為只讀
	*to_page_table = this_page;
	*from_page_table = this_page;
	this_page -= LOW_MEN;
	this->page >>= 12;
	mem_map[this_page]++;
}

主要就是看這一段

this_page = *from_page_table;
this_page &= ~2;		// 設定為只讀
*to_page_table = this_page;

這三句的含義就是將父程序的from_page_table賦值給子程序的to_page_table,並且將對應的頁設定為只讀。這也是前面說的為什麼不用為子程序找空閒頁,因為子程序用的就是父程序的記憶體。為什麼要設定為只讀屬性?兩個程序共享同一塊記憶體,如果都是讀,沒有任何問題,但是如何要寫呢?那麼就出問題了;因此要設定為只讀。到這裡分配物理頁、建立頁表就說完了。

MMU地址處理

到目前為止,分配記憶體、建立段表,分配記憶體、建立頁表都講完了。程式就可以正確的儲存到實體記憶體了。真不容易啊-。接下來程式執行的時候只需要根據這兩張表找到對應的記憶體就好了;當然如果查這兩張表的操作全部由軟體來實現的話就要浪費很多時間了,因此計算機將查表的操作交給硬體來完成,只要從使用者程式那裡得到CS:ip,硬體會自動得到該邏輯地址對應的實體地址的,這個硬體就是MMU。

參考資料

哈工大李志軍作業系統