概述
作業系統中使用段頁式記憶體管理政策的目的是為了更好地通路記憶體。那段頁式記憶體管理政策究竟是什麼樣呢?先看一張有關這個政策的圖檔:
這張圖檔從左向右看,最開始的cs:ip(也被稱為邏輯位址或者偏移位址),與中間的虛拟位址(也被稱為線性位址)建立關聯,然後将虛拟位址與右邊的記憶體實體位址建立關聯。
問題:我們使用記憶體的目的其實是把程式放入記憶體中,然後在必要的時候通路記憶體,讀寫記憶體。為什麼把程式段放入記憶體頁的過程中插進來一個虛拟位址的映射呢?
解釋:老實說作業系統引導程式就是直接把程式段放入指定的實體記憶體位址中的,中間沒有進行虛拟位址的映射,之後在作業系統上建立起的程式在通路記憶體的時候都會添加一步虛拟位址映射過程。之是以這樣是因為作業系統上的程式可以有多個,編寫時并不能确定實際的位址在哪裡,需要在運作時确定。引入虛拟記憶體這個概念也是為了屏蔽實體層記憶體位址的限制,使得程式編寫時更加友善。
記憶體管理之段表
程式段與虛拟位址建立的關聯可以記錄在一個表中,這個表就被稱為段表:
其中段号就是程式段的編号,基址就是關聯的虛拟位址,長度就是該段号下程式段的長度,保護就是該程式段的通路屬性。
記憶體管理之頁表
虛拟位址與實體記憶體頁建立的關聯可以記錄在一個表中,這個表就稱為頁表:
這裡的頁号是一種邏輯頁号,是由虛拟位址提供的,頁框号是實體記憶體的頁号,保護是該記憶體頁的可讀可寫屬性,有效是表示該記憶體頁是否有資料。無論是邏輯頁還是記憶體頁,它們的每頁的大小都是4k。
建立段頁式記憶體管理
從上面講述可知建立段頁式記憶體管理可分為4大步:配置設定段,建段表,配置設定頁,建頁表。
1、程式在編譯後,編譯器已經為該程式分了文本段、資料段等程式段。
2、那如何建段表呢?可以結合代碼分析:
// linux0.11/kernel/fork.c
int copy_process(int nr,long ebp...)
{...
if (copy_mem(nr,p)) { // 拷貝記憶體
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}...
}
int copy_mem(int nr,struct task_struct * p)
{...
new_data_base = new_code_base = nr * 0x4000000; // 配置設定段表中的基位址
set_base(p->ldt[1],new_code_base); // 建立代碼段的基位址,也就是段表
set_base(p->ldt[2],new_data_base); // 建立資料段的基位址,也就是段表
...
}
從上面代碼可以看出在拷貝程序時會進入拷貝記憶體接口,此時上面的幾句代碼就建立了段表,建立段表的核心是建立基位址,這個基位址就是虛拟位址。
3、配置設定頁也就是從實體記憶體配置設定一頁空閑記憶體出來,不過在linux0.11系統中記憶體管理采用的是寫時複制,是以程序拷貝時,連同父程序的實體記憶體頁也拷貝了進來,是以此時不用配置設定頁,當子程序對記憶體進行寫操作時系統才會向記憶體申請一塊新的空閑記憶體區,完成配置設定頁。
4、建立頁表就是拷貝父程序的頁表資訊:
// linux-0.11/kernel/fork.c
int copy_mem(int nr,struct task_struct * p)
{...
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;
}
// linux-0.11/mm/memroy.c
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) (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) { // 該頁如果大于低端記憶體頁的話,将該頁的映射數量加1,防止被過度釋放
*from_page_table = this_page; // 此時父程序也沒有寫權限
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}
從上述代碼注釋可以看出子程序的頁表是現在記憶體中擷取一頁空閑頁來存放頁表,然後将這個頁表對象關聯到子程序目錄項的指針上,接着拷貝父程序下目錄項中的頁表到子程序目錄項的頁表中。是以父的頁表與實體頁表框的映射資訊也被複制到子的中,此時共享讀這頁記憶體,雙方都沒有寫權限.
總結
記憶體管理代碼相當複雜,下回着重分析怎樣邏輯位址與虛拟位址,虛拟位址與實體位址之間如何映射的。