文章目錄
- 系統內存布局
- 內核地址的低端和高端內存概念
- 低端內存
- 高端內存
- 地址轉換和MMU
- Linux中的四級分頁模型
- 虛擬地址字段
- 頁表處理
- 將虛擬地址轉換物理地址
Linux系統中的每個內存地址都是虛擬的,它們不直接指向任何物理內存地址。每當訪問內存位置時,可以執行轉換機制以匹配相應的物理內存,所以我們在程序中必須用虛擬地址來訪問數據。
注:下面所說的內核空間和用戶空間這樣的術語指的都是虛擬地址空間
系統內存布局
在Linux系統中,每個進程都有自己獨立的虛擬地址空間。它是一種內存沙箱,存在于進程的生命周期中。在32位系統上,該地址空間大小是4GB。針對每一個進程,4GB的地址空間被分割成兩個部分:
- 內核空間虛擬地址
- 用戶空間虛擬地址
分割方式依賴于特殊的內核配置選項CONFIG_PAGE_OFFSET,這個選項定義內核虛擬地址部分在進程地址空間的起始位置。典型的進程虛擬地址空間布局如下圖:
內核空間和用戶空間所使用的地址都是虛擬地址,不同的是,訪問內核空間地址需要特權模式,當CPU運行用戶空間代碼時,活動進程被認為運行在用戶模式下,當CPU運行內核空間代碼時,活動進程被認為運行在內核模式下。
內核與每個進程共享其地址空間,原因如下:因為每個進程在給定的時刻都使用系統調用,這將涉及內核。將內核的虛擬內存地址映射到每個進程的虛擬地址空間能夠避免每次進入(或者退出)內核時內存地址切換產生的開銷。這就是內核地址空間被永久映射到每個進程頂部的原因—加快系統調用對內核的訪問。
每個進程地址空間頂部都是內核的虛擬地址空間,這一部分每個進程都是相同的。
內存管理單元把內存組織為大小固定的單元—頁面,內存頁(虛擬頁)指的是連續虛擬內存塊,內核數據結構也使用相同的名稱頁面來表示內存頁。幀(頁面幀)指一段固定長度的連續物理內存塊,操作系統在其上映射內存頁。每個頁面幀都有一個號碼,叫做頁面幀號(PFN)。
內核地址的低端和高端內存概念
Linux內核具有自己的虛擬地址空間。比如32位的x86,內核的虛擬地址空間是1GB大小,分成兩個部分:
- 低端內存或LOWMEM:第一個896MB
- 高端內存或HIGHMEM:頂部的128MB
低端內存
內核地址空間的第一個896MB空間構成低端內存區域。在啟動早期,內核永久映射這896MB的空間。該映射產生的地址為邏輯地址,這些都是虛擬地址,但是減去固定的偏移量后就可以將其轉換為物理地址。因為映射是永久的,并且事先知道。大多數內核內存函數返回低端內存。事實上,為了滿足不同的用途,內核內存被劃分為區域,LOWMEM的第一個16MB內存保留為DMA使用。內核空間可以確定3種不同的內存區域:
- ZONE_DMA:包含的內存頁面幀在0~16MB,用于直接內存訪問(DMA)
- ZONE_NORMAL:包含的內存頁面幀為16MB~896MB,常規使用
- ZONE_HIGHMEM:包含的內存頁面幀位于896MB及其以上
這就是說,512MB的系統上,不存在以上的劃分。
邏輯地址的另一個定義:線性映射到物理地址上的內核空間中的地址,可以用偏移量或者應用位掩碼將其轉為物理地址,使用__pa(地址)宏可以將邏輯地址(內核中的虛擬地址)轉換為物理地址,使用__va(地址)可以做相反的操作。
高端內存
內核地址空間頂部頂部128MB稱為高端內存區域,內核用它臨時映射1GB以上的物理內存,當需要訪問896MB以上的物理內存時,內核會使用這128MB創建到其虛擬地址空間的臨時映射,也就是將需要訪問數據的物理頁映射到這128MB內核虛擬地址空間來,從而實現訪問所有物理頁面的目標。可以把高端內存定義為邏輯地址存在的內存,但不會將其永久映射到內核地址空間。896MB以上的物理內存按需映射到HIGHMEM區域的128MB。
訪問高端內存的映射由內核動態創建,訪問后銷毀,這使高內存訪問速度變慢,64位系統上不存在高端內存這一概念。
地址轉換和MMU
每次訪問內存位置時,由CPU完成從虛擬地址到物理地址的轉換。該機制稱為地址轉換,這由CPU中的內存管理單元(MMU)來執行。MMU轉換的都是虛擬地址,所以訪問數據,必須是虛擬地址,不能是物理地址,否則訪問不了數據。
對于虛擬內存,內存組織為固定大小的頁,而物理內存則按幀組織,頁面表(PTE)概念的引入是為了管理頁面和幀之間的映射。頁面分部在表間,因此每個PTE的表項對于一個頁面和幀之間的映射,然后給每個進程一組頁面表來描述其整個內存空間。
Linux中的四級分頁模型
Linux采用了一種同時適用于32位和64位系統的普通分頁模型。從2.6.11版本開始,采用了四級分頁模型:
上圖展示的4種頁表分別被稱為:
- 頁全局目錄(Page Global Directory,PGD)
- 頁上級目錄(Page Upper Directory,PUD)
- 頁中間目錄(Page Middle Directory,PMD)
- 頁表(Page Table,PTE)
虛擬地址被分為5個部分,每個頁表項指向一個頁框,每一部分的大小與具體的計算機體系結構有關。
MMU如何知道進程頁面表?很簡單,MMU不存儲任何地址。但CPU有一個特殊的寄存器,稱為頁面表基址寄存器(PTBR)或轉換基址寄存器0(TTBR0),它指向進程1級頁面表(PGD)的基址。這正是struct mm_struct
的字段pgd指向的地址:current->mm.pgd == TTBR0.
上面圖中的cr3保存的就是該值
虛擬地址字段
下面宏簡化了頁表處理:
- PAGE_SHIFT:指定Offset字段的位數,這個宏由PAGE_SIZE使用返回頁的大小。最后,PAGE_MASK宏用以屏蔽Offset字段的所有位。
- PMD_SHIFT:指定虛擬地址的offset字段和table字段的總位數,PMD_MASK宏用于屏蔽Offset字段與Table字段的所有位
- PUD_SHIFT:確定頁上級目錄項能映射的區域大小的對數,PUD_MASK宏用于屏蔽Offset字段,Table字段、Middle Air字段和Upper Air字段的所有位
- PGDIR_SHIFT:確定頁全局目錄項能映射的區域大小的對數。PGDIR_MASK宏用于屏蔽Offset、Table、Middle Air及Upper Air字段的所有位
- PTRS_PER_PTE,PTRS_PER_PMD,PTRS_PER_PUS以及PTRS_PER_PGD:用于計算頁表、頁中間目錄、頁上級目錄和頁全局目錄表中表現的個數。
頁表處理
pte_t、pmd_t、pud_t和pgd_t分別描述頁表項、頁中間目錄項、頁上級目錄項和頁全局目錄項。
五個類型轉換宏(__pte,__pmd,__pgd和__pgprot)把一個無符號整數轉換成所需的類型。另外的五個類型轉換宏(pte_val,pmd_val,pud_val,pgd_val和pgport_val)執行相反的轉換。
如果相應的表項值位0,那么,宏pte_none、pmd_none、pud_none、和pgd_none產生的值為1,否則產生的值為0
將虛擬地址轉換物理地址
進程訪問的都是用戶空間內虛擬地址,內核訪問的都是內核虛擬地址,進程用不了內核虛擬地址,同樣內核用不了進程內虛擬地址。如果我們將用戶空間地址傳給內核,內核必須先將找到該用戶空間虛擬地址對應的物理地址,再將物理地址轉換成內核虛擬地址,內核才能訪問數據。
代碼地址:http://edsionte.com/techblog/archives/1966
static void get_pgtable_macro(void)
{printk("PAGE_OFFSET = 0x%lx\n", PAGE_OFFSET);printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);printk("PUD_SHIFT = %d\n", PUD_SHIFT);printk("PMD_SHIFT = %d\n", PMD_SHIFT);printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}
static unsigned long vaddr2paddr(unsigned long vaddr)
{pgd_t *pgd;pud_t *pud;pmd_t *pmd;pte_t *pte;unsigned long paddr = 0;unsigned long page_addr = 0;unsigned long page_offset = 0;pgd = pgd_offset(current->mm, vaddr);printk("pgd_val = 0x%lx\n", pgd_val(*pgd));printk("pgd_index = %lu\n", pgd_index(vaddr));if (pgd_none(*pgd)) {printk("not mapped in pgd\n");return -1;}pud = pud_offset(pgd, vaddr);printk("pud_val = 0x%lx\n", pud_val(*pud));if (pud_none(*pud)) {printk("not mapped in pud\n");return -1;}pmd = pmd_offset(pud, vaddr);printk("pmd_val = 0x%lx\n", pmd_val(*pmd));printk("pmd_index = %lu\n", pmd_index(vaddr));if (pmd_none(*pmd)) {printk("not mapped in pmd\n");return -1;}pte = pte_offset_kernel(pmd, vaddr);printk("pte_val = 0x%lx\n", pte_val(*pte));printk("pte_index = %lu\n", pte_index(vaddr));if (pte_none(*pte)) {printk("not mapped in pte\n");return -1;}//頁框物理地址機制 | 偏移量page_addr = pte_val(*pte) & PAGE_MASK;page_offset = vaddr & ~PAGE_MASK;paddr = page_addr | page_offset;printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);return paddr;
}
static int __init v2p_init(void)
{unsigned long vaddr = 0;printk("vaddr to paddr module is running..\n");get_pgtable_macro();printk("\n");vaddr = (unsigned long)vmalloc(1000 * sizeof(char));if (vaddr == 0) {printk("vmalloc failed..\n");return 0;}printk("vmalloc_vaddr=0x%lx\n", vaddr);vaddr2paddr(vaddr);printk("\n\n");vaddr = __get_free_page(GFP_KERNEL);if (vaddr == 0) {printk("__get_free_page failed..\n");return 0;}printk("get_page_vaddr=0x%lx\n", vaddr);vaddr2paddr(vaddr);return 0;
}
static void __exit v2p_exit(void)
{printk("vaddr to paddr module is leaving..\n");vfree((void *)vaddr);free_page(vaddr);
}
整個程序的結構如下:
-
get_pgtable_macro()打印當前系統分頁機制中的一些宏。
-
通過vmalloc()在內核空間中分配內存,調用vaddr2paddr()將虛擬地址轉化成物理地址。
-
通過__get_free_pages()在內核空間中分配頁框,調用vaddr2paddr()將虛擬地址轉化成物理地址。
-
分別通過vfree()和free_page()釋放申請的內存空間。
vaddr2paddr()的執行過程如下:
-
通過pgd_offset計算頁全局目錄項的線性地址pgd,傳入的參數為內存描述符mm和線性地址vaddr。接著打印pgd所指的頁全局目錄項。
-
通過pud_offset計算頁上級目錄項的線性地址pud,傳入的參數為頁全局目錄項的線性地址pgd和線性地址vaddr。接著打印pud所指的頁上級目錄項。
-
通過pmd_offset計算頁中間目錄項的線性地址pmd,傳入的參數為頁上級目錄項的線性地址pud和線性地址vaddr。接著打印pmd所指的頁中間目錄項。
-
通過pte_offset_kernel計算頁表項的線性地址pte,傳入的參數為頁中間目錄項的線性地址pmd和線性地址vaddr。接著打印pte所指的頁表項。
-
pte_val(*pte)先取出頁表項,與PAGE_MASK相與的結果是得到要訪問頁的物理地址;vaddr&~PAGE_MASK用來得到線性地址offset字段;兩者或運算得到最終的物理地址。
-
打印物理地址。
我們可以獲得物理地址了,就可以使用__pa(地址)宏可以將物理地址轉換為邏輯地址(內核中的虛擬地址),使用__va(地址)可以做相反的操作