記憶體管理
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); //復制物理頁
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/455362.html
標籤:Linux
