1. 程式人生 > 其它 >Linux 0.11原始碼閱讀筆記-記憶體管理

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);		//複製物理頁
}