原文地址:Linux內存空間訪問札記
引:本來打算將這部分內容併入到《The Linux Kernel Module Programming Guide筆記》中去,但是想下內存空間管理訪問相當基礎重要而且內容量較大,所以就單獨記錄。
注:在x86架構上,會分爲內存空間和I/O空間,但是在大多數嵌入式控制器如ARM、PowerPC並不提供I/O空間。我面向的主要是嵌入式方面的驅動開發,所以這裏並不討論I/O空間的內容。本文的內容大多數是摘錄《Linux設備驅動開發詳解》的第十一章,並結合我在開發過程的一些心得體會。
一、內存管理單元MMU
MMU輔助操作系統進行內存管理、提供虛擬地址和物理地址的映射、內存訪問權限保護和Cache緩存控制等硬件支持,可見,這將使得Linux操作系統能單獨爲系統的每個用戶分配獨立的內存空間並保證用戶空間不能訪問內核空間的地址,爲操作系統的虛擬內存管理模塊提供了硬件基礎。
在s3c2410的vivi這個bootloader中,建立了一個4GB物理地址與虛擬地址一一映射的一級頁表,我們可以通過函數mem_mapping_linear()來探尋一下其創建過程
static inline void mem_mapping_linear(void) {
unsigned long pageoffset, sectionNumber;
/*4GB虛擬地址映射到相應的物理地址上,均不能緩存*/
for (sectionNumber = 0; sectionNumber < 4096; sectionNumber++)
{
pageoffset = (sectionNumber << 20);
*(mmu_tlb_base + (pageoffset >> 20)) = pageoffset | MMU_SECDESC;
//mmu_tlb_base爲存放頁表的首地址,tlb是轉換旁路緩存,是轉換表的Cache
}
/*使能DRAM的區域可緩存*/
/*SDRAM物理地址0x30000000-0x33ffffff,DRAM_BASE=0x30000000,DRAM_SIZE=64M*/
for (pageoffset = DRAM_BASE; pageoffset < (DRAM_BASE + DRAM_SIZE); pageoffset += SZ_1M)
{
*(mmu_tlb_base + (pageoffset >> 20)) = pageoffset | MMU_SECDESC | MMU_CACHEEABLE;
} }
|
這裏使用了ARM920T內存映射的Section模式(實際等同於頁大小爲1MB的情況),將4GB的虛擬內存空間分爲4096個段,因此我們用4096個描述符來對這組段進行描述。這4096個描述符構成的表格就是轉換表,保存在MMU的TLB中。
二、內核空間內存動態申請
在Linux內核空間申請內存涉及的函數主要包括kmalloc()、__get_free_pages()和vmalloc()。kmalloc()、__get_free_pages()申請的內存位於物理內存映射區域,而且物理上也是連續的,它們與真實的物理地址只有一個固定的偏移,而vmalloc()在虛擬內存空間給出一塊連續的內存區,實際上,這片連續的虛擬內存在物理內存中並不一定連續。vmalloc()一般用在爲較大的順序緩衝區分配內存,vmalloc()的開銷遠大於__get_free_pages(),爲了完成vmalloc(),需要建立新的頁表。
另外還有slab和內存池,這裏不進行詳述,可參考相關資料。
對於內核內存空間映射區的虛擬內存(如kmalloc分配的內存),使用virt_to_phys()可以實現內核虛擬地址轉化爲物理地址,與之對應的函數爲phys_to_virt(),它將物理地址轉化爲內核虛擬地址。
三、將設備地址映射到用戶空間
一般情況下,用戶空間不會也不應該直接訪問設備的,但是,設備驅動程序中可實現mmap()函數,這個函數可使得用戶空間能直接訪問設備的物理地址。實際上,mmap實現了一個映射過程:將用戶空間的一段內存與設備內存空間相關聯,當用戶訪問用戶空間的這段地址範圍時,事實上轉化成對設備的訪問。
這個特性對顯示設備非常有意義,如果用戶空間可直接用過內存映射訪問顯存的話,屏幕幀的各點像素將不再需要從用戶空間複製到內核空間。
我們看看mmap的系統調用原型:
caddr_t mmap(caddr_t addr, size_t len, int prot, int flags, int fd, off_t
offset);
/*
**參數fd爲文件描述符,
**len是映射到用戶空間的字節數,它從被映射文件開頭offset開始算起
**prot指定訪問權限,PROT_READ(可讀)、PROT_WRITE(可寫)、PROT_EXEC(可執行)、PROT_NONE(不可訪問)
**參數addr指定文件應被映射到用戶空間的起始地址,一般爲NULL,這樣起始地址的任務將由內核完成,而函數返回值就是映射到用戶空間的地址
*/
|
當用戶調用mmap()時,內核會進行如下處理:
1、在進程的虛擬空間查找一塊VMA
2、將這塊VMA進行映射到設備地址空間,如果file_operations定義了mmap()操作,則調用它
3、將這個VMA插入到進程的VMA鏈表中
|
vm_operations_struct操作範例,取自fbmem.c
static int
fb_mmap(struct file *file, struct vm_area_struct * vma) {
int fbidx = iminor(file->f_path.dentry->d_inode);
struct fb_info *info = registered_fb[fbidx];
struct fb_ops *fb = info->fbops;
unsigned long off;
unsigned long start;
u32 len;
if (vma->vm_pgoff > (~0UL >> PAGE_SHIFT))
return -EINVAL;
off = vma->vm_pgoff << PAGE_SHIFT;
if (!fb)
return -ENODEV;
if (fb->fb_mmap) {
int res;
lock_kernel();
res = fb->fb_mmap(info, vma);
unlock_kernel();
return res;
}
lock_kernel();
/* frame buffer memory */
start = info->fix.smem_start;
len = PAGE_ALIGN((start & ~PAGE_MASK) + info->fix.smem_len);
if (off >= len) {
/* memory mapped io */
off -= len;
if (info->var.accel_flags) {
unlock_kernel();
return -EINVAL;
}
start = info->fix.mmio_start;
len = PAGE_ALIGN((start & ~PAGE_MASK) + info->fix.mmio_len);
}
unlock_kernel();
start &= PAGE_MASK;
if ((vma->vm_end - vma->vm_start + off) > len)
return -EINVAL;
off += start;
vma->vm_pgoff = off >> PAGE_SHIFT;
/* This is an IO map - tell maydump to skip this VMA */
vma->vm_flags |= VM_IO | VM_RESERVED;
fb_pgprotect(file, vma, off);
if (io_remap_pfn_range(vma, vma->vm_start, off >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot))
return -EAGAIN;
return 0; }
/*
**這段代碼的核心是io_remap_pfn_range(vma, vma->vm_start, off >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot)) **其中vma->vm_start就是用戶內存映射開始處的虛擬地址 **vma->vm_end - vma->vm_start是映射的虛擬地址範圍 **而off >> PAGE_SHIFT是虛擬地址應該映射到的物理地址off的頁幀號,實際上就是物理地址off右移了PAGE_SHIFT位:
off = vma->vm_pgoff << PAGE_SHIFT;
start = info->fix.smem_start;//smem_start是顯存的起始物理地址
start &= PAGE_MASK;
off += start; **從上述過程可以看出,將顯存的物理地址的頁幀號映射到用戶空間的虛擬地址上
**mmap必須以PAGE_SIZE爲單位進行映射,實際上內存只能以頁爲單位進行映射,如果非PAGE_SIZE整數倍的地址範圍,要先進行頁對齊,強行以PAGE_SIZE的倍數大小進行映射
*/
|
在驅動程序中,我們能使用remap_pfn_range()映射內存中的保留頁和設備IO內存,另外kmalloc申請的內存若要被映射到用戶空間可以通過mem_map_reserve()設置爲保留後進行。這個特性可用於用戶程序要頻繁將數據寫到設備中的buffer時,這樣可以減少系統read、write等調用開銷。
【Note】:mem_map_reserve是2.4版本內核的函數,2.6內核用SetPageReserved取代之
映射kmalloc申請的內存到用戶空間範例
/*內核模塊加載函數*/ int __init kmalloc_map_init(void) {
//申請設備號,添加cdev結構體
buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
for (page = virt_to_page(buffer); page < virt_to_page(buffer + BUFFER_SIZE); page++)
{
mem_map_reserve(page);//置頁爲保留,virt_to_page()將內核虛擬地址轉化爲頁
} } /*mmap()函數*/ static int kmalloc_map_mmap(struct file *filp, struct vm_area_struct *vma) {
unsigned long page, pos;
unsigned long start = (unsigned long)vma->vm_start;
unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);
if (size > BUFFER_SIZE)
{
return - EINVAL;
}
pos = (unsigned long)buffer;
/*映射buffer中的所有頁*/
while (size > 0)
{
page = virt_to_phys((void *)pos);
if (remap_page_range(start, page, PAGE_SIZE, PAGE_SHARED))
return - EAGAIN;
start += PAGE_SIZE;
pos += PAGE_SIZE;
size -= PAGE_SIZE;
}
/*
**可否用io_remap_pfn_range(vma, vma->vm_start, virt_to_phys((void *)buffer) >>
PAGE_SHIFT, vma->vm_end - vma->vm_start, PAGE_SHARED)來替代remap_page_range?
**在Linux kernel 2.6.27中,已經找不到remap_page_range的實現,見Linux
kernel change log: Changes remap_page_range to remap_pfn_range for 2.6.10 and above kernels
*/
return 0; }
|
說得極其簡煉易懂,我實在非常佩服。
1. 內核初始化:
* 內核建立好內核頁目錄頁表數據庫,假設物理內存大小爲len,則建立了[3G--3G+len]::[0--len]這樣的虛地址vaddr和物理地址paddr的線性對應關係;
* 內核建立一個page數組,page數組和物理頁面系列完全是線性對應,page用來管理該物理頁面狀態,每個物理頁面的虛地址保存在page->virtual中;
* 內核建立好一個free_list,將沒有使用的物理頁面對應的page放入其中,已經使用的就不用放入了;
2. 內核模塊申請內存vaddr = get_free_pages(mask,order):
* 內存管理模塊從free_list找到一個page,將page->virtual作爲返回值,該返回值就是對應物理頁面的虛地址;
* 將page從free_list中脫離;
* 模塊使用該虛擬地址操作對應的物理內存;
3. 內核模塊使用vaddr,例如執行指令mov(eax, vaddr):
* CPU獲得vaddr這個虛地址,利用建立好的頁目錄頁表數據庫,找到其對應的物理內存地址;
* 將eax的內容寫入vaddr對應的物理內存地址內;
4. 內核模塊釋放內存free_pages(vaddr,order):
* 依據vaddr找到對應的page;
* 將該page加入到free_list中;
5. 用戶進程申請內存vaddr = malloc(size):
* 內存管理模塊從用戶進程內存空間(0--3G)中找到一塊還沒使用的空間vm_area_struct(start--end);
* 隨後將其插入到task->mm->mmap鏈表中;
6. 用戶進程寫入vaddr(0-3G),例如執行指令mov(eax, vaddr):
* CPU獲得vaddr這個虛地址,該虛地址應該已經由glibc庫設置好了,一定在3G一下的某個區域,根據CR3寄存器指向的current->pgd查當前進程的頁目錄頁表數據庫,發現該vaddr對應的頁目錄表項爲0,故產生異常;
* 在異常處理中,發現該vaddr對應的vm_area_struct已經存在,爲vaddr對應的頁目錄表項分配一個頁表;
* 隨後從free_list找到一個page,將該page對應的物理頁面物理首地址賦給vaddr對應的頁表表項,很明顯,此時的vaddr和paddr不是線性對應關係了;
* 將page從free_list中脫離;
* 異常處理返回;
* CPU重新執行剛剛發生異常的指令mov(eax, vaddr);
* CPU獲得vaddr這個虛地址,根據CR3寄存器指向的current->pgd,利用建立好的頁目錄頁表數據庫,找到其對應的物理內存地址;
* 將eax的內容寫入vaddr對應的物理內存地址內;
7. 用戶進程釋放內存vaddr,free(vaddr):
* 找到該vaddr所在的vm_area_struct;
* 找到vm_area_struct:start--end對應的所有頁目錄頁表項,清空對應的所有頁表項;
* 釋放這些頁表項指向物理頁面所對應的page,並將這些page加入到free_list隊列中;
* 有必要還會清空一些頁目錄表項,並釋放這些頁目錄表項指向的頁表;
* 從task->mm->mmap鏈中刪除該vm_area_struct並釋放掉;
綜合說明:
* 可用物理內存就是free_list中各page對應的物理內存;
* 頁目錄頁表數據庫的主要目的是爲CPU訪問物理內存時轉換vaddr-->paddr使用,分配以及釋放內存時不會用到,但是需要內核內存管理系統在合適時機爲CPU建立好該庫;
* 對於用戶進程在6中獲得的物理頁面,有兩個頁表項對應,一個就是內核頁目錄頁表數據庫的某個pte[i ],一個就是當前進程內核頁目錄頁表數據庫的某個 pte[j],但是隻有一個page和其對應。如果此時調度到其他進程,其他進程申請並訪問某個內存,則不會涉及到該物理頁面,因爲其分配時首先要從 free_list中找一個page,而該物理頁面對應的page已經從free_list中脫離出來了,因此不存在該物理頁面被其他進程改寫操作的情況。內核中通過get_free_pages等方式獲取內存時,也不會涉及到該物理頁面,原理同前所述。