Linux 0.11源碼閱讀筆記-記憶體管理

来源:https://www.cnblogs.com/cwcheng/archive/2022/03/29/16073554.html
-Advertisement-
Play Games

記憶體管理 Linux內核使用段頁式記憶體管理方式。 記憶體池 物理頁:物理空閑記憶體被劃分為固定大小(4k)的頁 記憶體池:所有空閑物理頁組成記憶體池,以頁為單位進行分配回收。並通過點陣圖記錄了每個物理頁是否空閑,點陣圖下標對應物理頁號。 分頁記憶體管理 虛擬頁:進程虛地址空間被劃分為固定大小(4k)的頁 分頁記憶體 ...


記憶體管理

Linux內核使用段頁式記憶體管理方式。

  • 記憶體池

物理頁:物理空閑記憶體被劃分為固定大小(4k)的頁

記憶體池:所有空閑物理頁組成記憶體池,以頁為單位進行分配回收。並通過點陣圖記錄了每個物理頁是否空閑,點陣圖下標對應物理頁號。

  • 分頁記憶體管理

虛擬頁:進程虛地址空間被劃分為固定大小(4k)的頁

分頁記憶體管理:通過頁目錄和頁表維護進程虛擬頁號到物理頁號的映射。設置好頁目錄、頁表之後,虛擬地址到物理地址之間的轉換通過記憶體管理單元(MMU)自動完成轉換。若訪問的虛擬頁沒有實際分配物理頁,則放生缺頁中斷,內核會為其分配物理頁。

  • 分段記憶體管理

分段:進程虛地址空間被劃分為多個邏輯段,代碼段、數據段、棧段等,每個段有一個段號。進程代碼不直接使用虛擬地址,而是段號+段內偏移的二維邏輯地址。

分段記憶體管理:通過段表維護每個段的信息,段表項包括段基址和段限長。設置好段表之後,段號+段內偏移二維邏輯地址到虛擬線性地址的轉換由MMU單元自動完成。

  • 相關代碼文件

page.s:僅包含記憶體缺頁中斷處理程式

memory.c:記憶體管理的核心文件,用於記憶體池的初始化操作、頁目錄和頁表的管理和內核其他部分對記憶體的申請處理過程。

物理記憶體管理

除去以被內核占用的記憶體外,剩餘為占用記憶體會使用記憶體池進行管理,用於動態的分配和回收。

image

記憶體池初始化

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大小頁表沒有頁項使用時,可以不為其申請記憶體空間。

image

  • 線性虛擬地址翻譯

線性地址可以劃分為頁目錄項、頁表項、頁內偏移。

頁目錄項:作為下標訪問頁目錄表項,表項記錄頁表信息

頁表項:作為下標訪問頁表項,也表項記錄物理頁信息

頁內偏移:作為物理頁內偏移訪問具體的物理地址單元

image

  • 複製頁表

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寄存器指向的內核段表。

image

段頁式記憶體管理

前面分別介紹了分頁記憶體管理和分段記憶體管理,及兩者各自地址翻譯過程,此處總結linux段頁式記憶體翻譯的整個流程,並介紹一些相關的寄存器和TLB快表。

地址翻譯過程主要分為兩個部分:段+偏移二維邏輯地址轉化為虛擬線性地址;虛擬線性地址轉化為物理地址。第一部分翻譯過程依賴數據結構GDT或LDT,其中記錄了段信息;第二部分翻譯過程依賴頁表數據結構,記錄了虛擬頁到物理頁的映射關係,CR3寄存器存儲當前進程頁目錄地址。

  • MMU:設置好寄存器GDTR、LDTR、CR3寄存器後,MMU記憶體管理單元只懂執行地址翻譯過程。

  • TLB:多級頁表導致地址翻譯過程較慢,使用TLB快表可緩存頁表項,加快地址翻譯過程。

image

頁面出錯異常

缺頁或者寫時拷貝會都會引起頁面出錯異常(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);		//複製物理頁
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • OAuth2客戶端按照它們與授權伺服器進行安全認證的能力可以分為機密類型(Confidential)和公共類型(Public)。 機密類型的自身會有個密碼憑據,比如Web伺服器後端程式;而公共類型則沒有密碼憑據,純瀏覽器前端應用或者移動客戶端應用大都屬於這一種類型。不管是哪一種,它們都有客戶端ID( ...
  • 一、LinkedList集合特有的方法 方法名說明 public void addFirst(E e) 在該鏈表的頭部插入指定的元素 public void addLast(E e) 在該鏈表的尾部追加指定的元素 public E getLast() 返回此鏈表的最後一個元素 public E ge ...
  • 對於spring-security來說,當你訪問一個受保護資源時,需要檢查你的token,當沒有傳遞,或者傳遞的token有錯誤時,將出現401unauthorized異常;當你傳遞的token是有效的,但解析後並沒有訪問這個資源的許可權時,將返回403forbidden的異常,而你通過攔截器@Res ...
  • 最終版:07_中證網(Plus -Pro).py # coding=utf-8 import requests from bs4 import BeautifulSoup import io import sys import os sys.stdout = io.TextIOWrapper(sys ...
  • 上文,我們通過剖析一個最簡單的 Blazor WASM 項目,講明白了 Razor 文件是什麼,以及它被轉譯成 C#後長什麼樣子。也介紹了 Razor 中最簡單的一個語法:Razor Expression,也就是 Razor 表達式 本文將介紹兩個內容: 首先我們將書接上文,再介紹一丁點 Razor ...
  • 最近在做這個登錄功能介面,記錄一下 1、小程式端調用wx.login方法獲取code,後端使用WeChatAuth方法請求auth.code2Session介面使用appid、secret、js_code、grant_type:預設authorization_code獲取session_key、op ...
  • 對於語音識別,一般有實時語音識別和語音文件的識別處理等方式,如在會議、培訓等場景中,可以對錄製的文件進行文字的轉錄,對於轉錄文字的成功率來說,如果能夠轉換90%以上的正確語音內容,肯定能減輕很多相關語音文本編輯的繁瑣工作,而目前大多數語音轉錄的介面基本都能夠保證在這個成功率上,有些甚至超過98%以上... ...
  • Linux 0.11源碼閱讀筆記-文件管理 文件系統 生磁碟 未安裝文件系統的磁碟稱之為生磁碟,生磁碟也可以作為文件讀寫,linux中一切皆文件。 磁碟分區 生磁碟可以被分區,分區中可以安裝文件系統,常見的文件系統有fat32、ext2、ext4等。分區後的磁碟結構佈局如下圖,其中主引導扇區記錄了分 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...