Linux 0.11原始碼閱讀筆記-記憶體管理
記憶體管理
Linux核心使用段頁式記憶體管理方式。
- 記憶體池
物理頁:物理空閒記憶體被劃分為固定大小(4k)的頁
記憶體池:所有空閒物理頁組成記憶體池,以頁為單位進行分配回收。並通過點陣圖記錄了每個物理頁是否空閒,點陣圖下標對應物理頁號。
- 分頁記憶體管理
虛擬頁:程序虛地址空間被劃分為固定大小(4k)的頁
分頁記憶體管理:通過頁目錄和頁表維護程序虛擬頁號到物理頁號的對映。設定好頁目錄、頁表之後,虛擬地址到實體地址之間的轉換通過記憶體管理單元(MMU)自動完成轉換。若訪問的虛擬頁沒有實際分配物理頁,則放生缺頁中斷,核心會為其分配物理頁。
- 分段記憶體管理
分段:程序虛地址空間被劃分為多個邏輯段,程式碼段、資料段、棧段等,每個段有一個段號。程序程式碼不直接使用虛擬地址,而是段號+段內偏移的二維邏輯地址。
分段記憶體管理:通過段表維護每個段的資訊,段表項包括段基址和段限長。設定好段表之後,段號+段內偏移二維邏輯地址到虛擬線性地址的轉換由MMU單元自動完成。
- 相關程式碼檔案
page.s:僅包含記憶體缺頁中斷處理程式
memory.c:記憶體管理的核心檔案,用於記憶體池的初始化操作、頁目錄和頁表的管理和核心其他部分對記憶體的申請處理過程。
實體記憶體管理
除去以被核心佔用的記憶體外,剩餘為佔用記憶體會使用記憶體池進行管理,用於動態的分配和回收。
記憶體池初始化
mem_init初始化空閒記憶體。將空閒記憶體劃分為4k大小頁,並在點陣圖mem_map中標記為空閒。點陣圖中還包含物理頁的引用計數,支援記憶體共享機制。
void mem_init(long start_mem, long end_mem) { int i; HIGH_MEMORY = end_mem; # 在點陣圖中,設定所有頁面為佔用狀態 for (i=0 ; i<PAGING_PAGES ; i++) mem_map[i] = USED; # 在點陣圖中,將核心未使用的空閒頁面設定為空閒狀態,start_mem為空閒記憶體起始地址 i = MAP_NR(start_mem); // 主記憶體區起始位置處頁面號 end_mem -= start_mem; end_mem >>= 12; // 主記憶體區中的總頁面數 while (end_mem-->0) mem_map[i++]=0; // 主記憶體區頁面對應位元組值清零 }
記憶體分配回收
核心程式碼通過get_free_page和free_page函式分配和回收物理記憶體頁。
- 分配
get_free_page函式用於分配物理頁。在點陣圖中查詢空閒物理頁,並標記為佔用,然後返回一個空閒的頁實體地址。
// 不要陷入程式碼細節
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t" // 置方向位,al(0)與對應每個頁面的(di)內容比較
"jne 1f\n\t" // 如果沒有等於0的位元組,則跳轉結束(返回0).
"movb $1,1(%%edi)\n\t" // 1 => [1+edi],將對應頁面記憶體映像bit位置1.
"sall $12,%%ecx\n\t" // 頁面數*4k = 相對頁面其實地址
"addl %2,%%ecx\n\t" // 再加上低端記憶體地址,得頁面實際物理起始地址
"movl %%ecx,%%edx\n\t" // 將頁面實際其實地址->edx暫存器。
"movl $1024,%%ecx\n\t" // 暫存器ecx置計數值1024
"leal 4092(%%edx),%%edi\n\t" // 將4092+edx的位置->dei(該頁面的末端地址)
"rep ; stosl\n\t" // 將edi所指記憶體清零(反方向,即將該頁面清零)
"movl %%edx,%%eax\n" // 將頁面起始地址->eax(返回值)
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
);
return __res; // 返回空閒物理頁面地址(若無空閒頁面則返回0).
}
- 回收
free_page函式用於釋放物理頁。釋放實體地址addr處的物理頁,並在點陣圖中標記為未佔用狀態。
void free_page(unsigned long addr)
{
// 判斷地址是否在合法範圍內
if (addr < LOW_MEM) return;
if (addr >= HIGH_MEMORY)
panic("trying to free nonexistent page");
addr -= LOW_MEM;
addr >>= 12;
if (mem_map[addr]--) return;
mem_map[addr]=0;
panic("trying to free free page");
}
分頁記憶體管理
- 多級頁表
多級頁表用於實現虛擬頁到物理頁的對映,程序基於多級頁表管理其佔用的實體記憶體頁。
使用單級頁表實現虛擬頁到物理頁的對映會浪費較多的記憶體空間,將單級頁表劃分為固定的大小(4k)的頁表,並使用頁目錄登記頁表,從而實現兩級頁表,進一步可實現多級頁表。使用多級頁表的好處在於節省空閒頁表佔用的記憶體空間,當4k大小頁表沒有頁項使用時,可以不為其申請記憶體空間。
- 線性虛擬地址翻譯
線性地址可以劃分為頁目錄項、頁表項、頁內偏移。
頁目錄項:作為下標訪問頁目錄表項,表項記錄頁表資訊
頁表項:作為下標訪問頁表項,也表項記錄物理頁資訊
頁內偏移:作為物理頁內偏移訪問具體的實體地址單元
- 複製頁表
copy_page_tables函式用於複製當前程序的頁目錄和頁表。首先會申請記憶體作為頁目錄和也表的儲存空間,然後進行復制,複製後的兩個程序的目標共享實際實體記憶體。fork新程序程時,會呼叫該函式為新程序從原程序複製頁表。
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;
}
- 分配物理頁
put_page函式為指定虛擬頁分配物理頁,並在頁表中登記對映關係。
//為程序虛頁分配分配物理頁,主要過程
//1. 呼叫get_free_page分配一個物理頁
//2. 呼叫put_page在頁表中修改頁項,建立虛頁到物理頁的對映
void get_empty_page(unsigned long address)
{
unsigned long tmp;
// 如果不能取得有一空閒頁面,或者不能將所取頁面放置到指定地址處,則顯示記憶體不夠資訊。
if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
free_page(tmp); /* 0 is ok - ignored */
oom();
}
}
//將物理頁對映到地址address中
unsigned long put_page(unsigned long page,unsigned long address)
{
unsigned long tmp, *page_table;
/* NOTE !!! This uses the fact that _pg_dir=0 */
if (page < LOW_MEM || page >= HIGH_MEMORY)
printk("Trying to put page %p at %p\n",page,address);
if (mem_map[(page-LOW_MEM)>>12] != 1)
printk("mem_map disagrees with %p at %p\n",page,address);
page_table = (unsigned long *) ((address>>20) & 0xffc);
if ((*page_table)&1)
page_table = (unsigned long *) (0xfffff000 & *page_table);
else {
if (!(tmp=get_free_page()))
return 0;
*page_table = tmp|7;
page_table = (unsigned long *) tmp;
}
page_table[(address>>12) & 0x3ff] = page | 7; //登記頁表項
/* no need for invalidate */
return page;
}
- 釋放物理頁
free_page_tables函式釋放連續一到多個虛擬頁,並修改頁表。
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
if (from & 0x3fffff)
panic("free_page_tables called with wrong alignment");
if (!from)
panic("Trying to free up swapper memory space");
size = (size + 0x3fffff) >> 22;
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
for ( ; size-->0 ; dir++) {
if (!(1 & *dir))
continue;
pg_table = (unsigned long *) (0xfffff000 & *dir); // 取頁表地址
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table) // 若該項有效,則釋放對應頁。
free_page(0xfffff000 & *pg_table);
*pg_table = 0; // 該頁表項內容清零。
pg_table++; // 指向頁表中下一項。
}
free_page(0xfffff000 & *dir); // 釋放該頁表所佔記憶體頁面。
*dir = 0; // 對應頁表的目錄項清零
}
invalidate(); // 重新整理頁變換高速緩衝。
return 0;
}
分段記憶體管理
虛擬記憶體被劃分為多個邏輯段,程式碼段、只讀資料段等,不同資料段的屬性不同,方便管理和保護安全。
全域性描述符表(GDT)和區域性描述符表(LDT)用於記錄段資訊,包含段基址和段限長等。GDT用於記錄核心使用的各種資料段,僅有一個;LDT用於記錄程序使用的各種資料段,一個程序對應一個。
暫存器GDTR和LDTR分別用於儲存GDT首地址和當前執行程序的LDT首地址。運行於使用者態時,地址翻譯使用LDTR暫存器指向的程序段表;運行於核心態時,地址翻譯使用LDTR暫存器指向的核心段表。
段頁式記憶體管理
前面分別介紹了分頁記憶體管理和分段記憶體管理,及兩者各自地址翻譯過程,此處總結linux段頁式記憶體翻譯的整個流程,並介紹一些相關的暫存器和TLB快表。
地址翻譯過程主要分為兩個部分:段+偏移二維邏輯地址轉化為虛擬線性地址;虛擬線性地址轉化為實體地址。第一部分翻譯過程依賴資料結構GDT或LDT,其中記錄了段資訊;第二部分翻譯過程依賴頁表資料結構,記錄了虛擬頁到物理頁的對映關係,CR3暫存器儲存當前程序頁目錄地址。
-
MMU:設定好暫存器GDTR、LDTR、CR3暫存器後,MMU記憶體管理單元只懂執行地址翻譯過程。
-
TLB:多級頁表導致地址翻譯過程較慢,使用TLB快表可快取頁表項,加快地址翻譯過程。
頁面出錯異常
缺頁或者寫時拷貝會都會引起頁面出錯異常(page_fault int14),但錯處碼不同。page_fault中斷處理函式根據出錯碼呼叫do_no_page處理缺頁中斷,或者呼叫do_wp_page處理寫時拷貝。
缺頁處理
程序訪問虛地址記憶體時,若未分配實體記憶體,將導致頁面出錯異常(page_fault int14),並呼叫異常處理函式do_no_page()
do_no_page將為虛擬頁分配物理頁,並從磁碟調入相應資料(若該虛頁對應磁碟資料)。
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);
//檔案末尾資料可能不足一個記憶體塊,剩下的記憶體空間清0
i = tmp + 4096 - current->end_data;
tmp = page + 4096;
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
// 最後把引起缺頁異常的一頁物理頁面對映到指定線性地址address處。若操作成功
// 就返回。否則就釋放記憶體頁,顯示記憶體不夠。
if (put_page(page,address))
return;
free_page(page);
oom();
}
寫時拷貝
fork新程序時,父子程序共享相同的實體記憶體頁,並設定共享記憶體頁只讀。當父子程序中的一個寫共享記憶體時,將導致頁面出錯異常(page_fault int14),並呼叫異常處理函式do_wp_page()處理。
do_wp_page會對共享記憶體頁取消共享,並複製出一個新的記憶體頁,使用父子程序各擁有一份自己的物理頁面,可正常讀寫。
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
if (CODE_SPACE(address))
do_exit(SIGSEGV);
#endif
// 呼叫上面函式un_wp_page()來處理取消頁面保護。
un_wp_page((unsigned long *)
(((address>>10) & 0xffc) + (0xfffff000 &
*((unsigned long *) ((address>>20) &0xffc)))));
}
// 取消保護頁函式
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;
old_page = 0xfffff000 & *table_entry;
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2;
invalidate();
return;
}
if (!(new_page=get_free_page())) //分配新頁
oom();
if (old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--;
*table_entry = new_page | 7;
invalidate();
copy_page(old_page,new_page); //複製物理頁
}