1 非連續內存區的線性地址
2 非連續內存區的描述符
3 申請非連續物理內存區
4 釋放非連續內存區
5 vmalloc和kmalloc
我們已經知道,最好將虛擬地址映射到連續頁幀,從而更好地利用緩存并實現更低的平均內存訪問時間。然而,如果對內存區域的請求并不頻繁,那么考慮基于通過連續線性地址訪問非連續頁幀的分配方案是有意義的。該模式的主要優點是避免了外部碎片,而缺點是需要修改內核頁表。顯然,非連續內存區域的大小必須是4096的倍數。Linux使用非連續物理內存區的場景有幾種:(1)為swap區分配數據結構;(2)為模塊分配空間(參見附錄B);(3)或者為一些I/O驅動程序分配緩沖區。此外,非連續物理內存區還提供了另一種利用高端內存的方法。
1 非連續內存區的線性地址
要查找線性地址的空閑范圍,我們可以從PAGE_OFFSET開始的區域(通常是0xc0000000,3G→4G)。下圖展示了這1G的線性地址的使用方式:
這1G大小的線性地址的第一部分是映射前896M物理內存的線性地址;與直接映射的物理內存的結尾對應的線性地址存儲在high_memory變量中。
這1G大小的線性地址的最后部分是固定映射的線性地址。
從PKMAP_BASE線性地址開始,是高端內存頁幀的永久內核映射使用的線性地址空間。
余下的線性地址空間用作非連續內存區域的映射。在前896M的線性地址之后與第一個非連續內存空間插入一個安全樁(大小為8M的地址間隔,使用宏VMALLOC_OFFSET獲取該值),以便捕獲越界內存訪問。基于這個目的,后面每個非連續內存區域之間都插入一個4K大小的地址間隔。
圖8-8 內核地址空間的布局
VMALLOC_START宏定義了為非連續內存區保留的線性空間的起始地址,而VMALLOC_END定義了它的結束地址。
2 非連續內存區的描述符
每個非連續內存區都有一個類型為vm_struct的描述符進行表達,各個成員如下所示:
表8-13 vm_struct各個成員的描述
類型 | 名稱 | 描述 |
---|---|---|
void * | addr | 該區域的第一個存儲單元的線性地址 |
unsigned long | size | 該區域的大小+4096(內存區域間的安全間隔) |
unsigned long | flags | 映射的內存類型 |
struct page ** | pages | 指向頁描述符的nr_pages指針數組 |
unsigned int | nr_pages | 該區域填充的頁數 |
unsigned long | phys_addr | 設置為0,除非是創建的內存區用來映射硬件設備的I/O共享內存 |
struct vm_struct* | next | 指向下一個vm_struct結構 |
這些描述符通過“next”字段插入到一個簡單的列表中;列表中第一個元素的地址存儲在vmlist變量中。通過“vmlist_lock”讀/寫自旋鎖保護對該列表的訪問。flags字段標識該內存區映射的內存類型:VM_ALLOC用于通過vmalloc()獲得的頁面,VM_MAP用于通過vmap()映射的已經分配的頁面(參見下一節),VM_IOREMAP用于通過ioremap()映射的硬件設備的板載內存(參見第13章)。
get_vm_area()負責在VMALLOC_START和VMALLOC_END之間找一段空閑的連續線性地址。這個函數作用于兩個參數:size,要創建的內存區的字節數;flag,指定要創建的內存類型。執行以下步驟:
structvm_struct*__get_vm_area(unsignedlongsize,unsignedlongflags, unsignedlongstart,unsignedlongend) { structvm_struct**p,*tmp,*area; unsignedlongalign=1; unsignedlongaddr; //...省略 addr=ALIGN(start,align); /*1.創建一個內核slab通用對象, *保存vm_struct(一段虛擬內存的描述符)的內容 */ area=kmalloc(sizeof(*area),GFP_KERNEL); if(unlikely(!area)) returnNULL; /* *2.創建一個保護頁(4K大小的間隔) */ size+=PAGE_SIZE; if(unlikely(!size)){ kfree(area); returnNULL; } /*3.獲取用于寫入的vmlist_lock鎖, *并掃描類型為vm_struct的描述符列表(也就是vmlist), *查找至少包含size+4096的線性地址空間 *(4k是內存區域之間的安全間隔的大小)。 */ write_lock(&vmlist_lock); for(p=&vmlist;(tmp=*p)!=NULL;p=&tmp->next){ if((unsignedlong)tmp->addraddr+tmp->size>=addr) addr=ALIGN(tmp->size+ (unsignedlong)tmp->addr,align); continue; } if((size+addr)addr) gotofound; addr=ALIGN(tmp->size+(unsignedlong)tmp->addr,align); if(addr>end-size) gotoout; } found: /*4.如果找到合適的一段線性地址空間, *則初始化申請的描述符并釋放鎖, *然后返回描述符的地址。 */ area->next=*p; *p=area; area->flags=flags; area->addr=(void*)addr; area->size=size; area->pages=NULL; area->nr_pages=0; area->phys_addr=0; write_unlock(&vmlist_lock); returnarea; out: /*5.釋放獲得的描述符,并釋放鎖,并返回NULL*/ write_unlock(&vmlist_lock); kfree(area); returnNULL; }
3 申請非連續物理內存區
vmalloc()函數為內核分配了一個非連續物理內存。參數size表示請求內存的大小。如果函數能夠滿足請求,則返回新區域的初始線性地址;否則,它返回一個NULL指針:
void*vmalloc(unsignedlongsize) { structvm_struct*area; structpage**pages; unsignedintarray_size,i; //將size按照4k對齊 size=(size+PAGE_SIZE-1)&PAGE_MASK; //創建新的頁描述符并返回對應的線性地址 //標志是VM_ALLOC,表示非連續物理頁幀將被映射到一段線性地址空間 area=get_vm_area(size,VM_ALLOC); if(!area) returnNULL; area->nr_pages=size>>PAGE_SHIFT; array_size=(area->nr_pages*sizeof(structpage*)); //申請一個數組的物理內存對象,保存頁描述符指針數組 area->pages=pages=kmalloc(array_size,GFP_KERNEL); if(!area_pages){ remove_vm_area(area->addr); kfree(area); returnNULL; } //將指針數組的元素清零。 memset(area->pages,0,array_size); /*根據需要的內存頁數,重復調用alloc_page函數 *給每一個頁分配一個頁幀,并將相應的頁描述符存入數組中。 *注意,這兒使用數據保存頁描述符是非常有必要的, *因為這些頁幀屬于高端內存,它們不用映射為線性地址。 */ for(i=0;inr_pages;i++){ area->pages[i]=alloc_page(GFP_KERNEL|__GFP_HIGHMEM); if(!area->pages[i]){ area->nr_pages=i; fail:vfree(area->addr); returnNULL; } } /*到這兒,我們已經獲得了一段線性地址空間; *也獲得了一組非連續的物理頁幀。那么關鍵的一步就是, *修改頁表項,將每個分配的頁幀與一個線性地址建立映射關系 *實現的函數就是map_vm_area */ if(map_vm_area(area,__pgprot(0x63),&pages)) gotofail; returnarea->addr; }
將線性地址與非連續物理頁幀建立映射關系由map_vm_area()函數實現,使用3個參數:
area:指向該內存區域的vm_struct描述符的指針。
prot:已分配頁幀的保護位。總是設為0x63,對應于Present、Accessed、Read/Write和Dirty
pages: 指向頁描述符指針數組的變量的地址(因此,struct page ***用作數據類型!)。
具體代碼如下所示:
intmap_vm_area(structvm_struct*area,pgprot_tprot,structpage***pages) { /*1.獲取線性地址的起始位置、結束位置*/ unsignedlongaddress=(unsignedlong)area->addr; unsignedlongend=address+(area->size-PAGE_SIZE); unsignedlongnext; pgd_t*pgd; interr=0; inti; /*2.使用pgd_offset_k宏在主內核PGD頁全局目錄中 *導出與該區域的初始線性地址相關的表項 */ pgd=pgd_offset_k(address); /*3.申請內核頁表自旋鎖*/ spin_lock(&init_mm.page_table_lock); for(i=pgd_index(address);i<=?pgd_index(end-1);?i++)?{ ????????/*?4.?為新內存分配PUD頁表中間目錄, ?????????*????并將其正確的物理地址寫入PGD目錄中 ?????????*/ ????????pud_t?*pud?=?pud_alloc(&init_mm,?pgd,?address); ????????if?(!pud)?{ ????????????err?=?-ENOMEM; ????????????break; ????????} ????????/*?5.?分配與新PUD目錄關聯的所有頁表。 ?????????*????map_area_pud將單個PUD所跨越的線性地址范圍的大小 ?????????*????(如果啟用了PAE,則為常數2^30,否則為2^22)加到當前的 ?????????*????address值上,并增加指向PGD的指針pgd。 ?????????*????重復這個循環,直到所有指向非連續內存區的頁表項都設置好。 ?????????*/ ????????next?=?(address?+?PGDIR_SIZE)?&?PGDIR_MASK; ????????if?(next?end) next=end; if(map_area_pud(pud,address,next,prot,pages)){ err=-ENOMEM; break; } address=next; pgd++; } spin_unlock(&init_mm.page_table_lock); flush_cache_vmap((unsignedlong)area->addr,end); returnerr; }
map_area_pud()對PUD指向的所有頁表也執行相似的循環:
do{ pmd_t*pmd=pmd_alloc(&init_mm,pud,address); if(!pmd) return-ENOMEM; if(map_area_pmd(pmd,address,end-address,prot,pages)) return-ENOMEM; address=(address+PUD_SIZE)&PUD_MASK; pud++; }while(address
map_area_pmd()對PMD指向的所有頁表也執行相似的循環:
do{ pte_t*pte=pte_alloc_kernel(&init_mm,pmd,address); if(!pte) return-ENOMEM; if(map_area_pte(pte,address,end-address,prot,pages)) return-ENOMEM; address=(address+PMD_SIZE)&PMD_MASK; pmd++; }while(address
pte_alloc_kernel()函數分配一個新頁表,并更新PMD頁中間目錄的對應表項。接下來,調用map_area_pte()為新頁表中的每一項分配物理頁幀。變量address的值增加2^22(正好是一個PMD頁表中一項跨越的線性地址范圍。
map_area_pte()主要工作是:
do{ structpage*page=**pages; set_pte(pte,mk_pte(page,prot)); address+=PAGE_SIZE; pte++; (*pages)++; }while(address
要映射的頁幀的頁描述符地址page從地址pages的變量所指向的數組項中讀取。新頁幀的物理地址通過set_pte和mk_pte宏寫入頁表。在為地址添加常數4096(頁幀的長度)后,重復這個循環。
注意,map_vm_area()還沒有修改當前進程的頁表。因此,當內核態的進程訪問非連續內存區域時,就會發生Page Fault,因為進程頁表中沒有該區域的映射關系。然而,Page Fault處理程序根據主內核頁表(即init_mm.pgdPGD頁全局目錄及其子頁表)檢查錯誤的線性地址;一旦處理器發現一個主內核頁表包含一個非空的地址項,它就把它的值復制到相應進程的頁表項中,然后恢復進程的正常執行。該機制在第9章的“頁面錯誤異常處理程序”一節中進行了描述。
除了vmalloc()之外,非連續內存區的分配還可以由vmalloc_32()完成。它與vmalloc()類似,但是只分配ZONE_NORMAL和ZONE_DMA內存區。
Linux v2.6還有一個vmap()函數,它映射已經在非連續內存區中分配的頁幀:本質上,這個函數接收一個指向頁描述符的指針數組作為它的參數,調用get_vm_area()來獲得一個新的vm_struct描述符,然后調用map_vm_area()來映射頁幀。因此,該函數類似于vmalloc(),但它不分配頁幀。
所以說,vmalloc和vmap的操作,大部分的邏輯是一樣的,比如從VMALLOC_START ~ VMALLOC_END非連續物理內存映射區之間查找并分配vmap_area。不同之處,在于vmap建立映射時,page是函數傳入進來的,而vmalloc是通過調用alloc_page接口向Buddy系統申請分配的。
4 釋放非連續內存區
vfree()函數釋放由vmalloc()或vmalloc_32()創建的非連續內存區域,而vunmap()函數釋放由vmap()創建的內存區域。兩個函數都有一個參數-待釋放區域的初始線性地址的地址;它們都依賴于__vunmap()函數來完成實際的工作。
__vunmap()函數接收兩個參數:要釋放的區域的初始線性地址的地址addr和標志deallocate_pages,如果在該區域中映射的頁幀應該被釋放到ZONE頁幀分配器(vfree()調用),則設置該標志,否則將被清除(vunmap()調用)。該函數的主要功能如下:
void__vunmap(void*addr,intdeallocate_pages) { //...省略 /*1.獲取vm_struct描述符的地址; *解除非連續物理內存與線性地址在頁表中的映射關系。 */ area=remove_vm_area(addr); if(unlikely(!area)){ //...省略 return; } if(deallocate_pages){ inti; /* *2.掃描頁描述符指針數據;對數組每個元素調用__free_page(), *將頁幀釋放回`ZONE`頁幀分配器中。 */ for(i=0;inr_pages;i++){ //...省略 __free_page(area->pages[i]); } if(area->nr_pages>PAGE_SIZE/sizeof(structpage*)) vfree(area->pages); else /*釋放指針數組,因為它是從連續物理內存中申請的, *所以調用kfree */ kfree(area->pages); } /*3.釋放vm_struct描述符*/ kfree(area); return; }
remove_vm_area()執行下面的循環:
structvm_struct*remove_vm_area(void*addr) { structvm_struct**p,*tmp; //申請鎖 write_lock(&vmlist_lock); /*搜索從addr開始的內核虛擬內存區域, *找到要釋放的線性地址區域area */ for(p=&vmlist;(tmp=*p)!=NULL;p=&tmp->next){ if(tmp->addr==addr) gotofound; } write_unlock(&vmlist_lock); returnNULL; found: /*釋放area*/ unmap_vm_area(tmp); *p=tmp->next; write_unlock(&vmlist_lock); returntmp; }write_lock(&vmlist_lock); for(p=&vmlist;(tmp=*p);p=&tmp->next){ if(tmp->addr==addr){ unmap_vm_area(tmp); *p=tmp->next; break; } } write_unlock(&vmlist_lock); returntmp;
map_vm_area()函數的內容如下所示,執行與map_vm_area()函數相逆的過程:
address=area->addr; end=address+area->size; pgd=pgd_offset_k(address); for(i=pgd_index(address);i<=?pgd_index(end-1);?i++)?{ ????next?=?(address?+?PGDIR_SIZE)?&?PGDIR_MASK; ????if?(next?<=?address?||?next?>end) next=end; unmap_area_pud(pgd,address,next-address); address=next; pgd++; }
繼而,unmap_area_pud()執行與map_area_pud()相逆的過程:
do{ unmap_area_pmd(pud,address,end-address); address=(address+PUD_SIZE)&PUD_MASK; pud++; }while(address&&(address
unmap_area_pmd()執行與map_area_pmd()相逆的過程:
do{ unmap_area_pte(pmd,address,end-address); address=(address+PMD_SIZE)&PMD_MASK; pmd++; }while(address
最后,unmap_area_pte()執行與map_area_pte()相逆的過程:
do{ pte_tpage=ptep_get_and_clear(pte); address+=PAGE_SIZE; pte++; if(!pte_none(page)&&!pte_present(page)) printk("Whee...Swappedoutpageinkernelpagetable "); }while(address
在循環的每次迭代中,pte指向的頁表項被ptep_get_and_clear宏設置為0。
至于vmalloc(),內核修改主內核頁全局目錄及其子頁表的項(參見第2章的“內核頁表”一節),但它保持進程頁表映射第4G的項不變。這很好,因為內核永遠不會回收基于主內核頁全局目錄的頁上目錄(PUD)、頁中間目錄(PMD)和頁表。
例如,假設內核態進程訪問了一個非連續內存區域,該內存區域隨后被釋放。進程的PGD項等于主內核PGD的相應項,這要歸功于第9章“Page Fault異常處理程序”一節中解釋的機制;它們指向相同的頁上目錄、頁中間目錄和頁表。unmap_area_pte()函數只清除頁表的項(不回收頁表本身)。由于頁表項為空,進程對釋放的非連續內存區域的進一步訪問將觸發Page fault。然而,處理程序會認為這樣的訪問是一個錯誤,因為主內核頁表不包括有效的項。
5 vmalloc和kmalloc
到現在,我們應該能清楚vmalloc和kmalloc的差異了吧,kmalloc會根據申請的大小來選擇基于slab分配器或者基于buddy系統來申請連續的物理內存。而vmalloc則是通過alloc_page申請order = 0的頁面,再映射到連續的虛擬空間中,物理地址不連續。此外vmalloc可以休眠,不應在中斷處理程序中使用。與vmalloc相比,kmalloc使用ZONE_DMA和ZONE_NORMAL空間,性能更快,缺點是連續物理內存空間的分配容易帶來碎片問題,讓碎片的管理變得困難。
審核編輯:湯梓紅
-
內核
+關注
關注
3文章
1372瀏覽量
40277 -
Linux
+關注
關注
87文章
11292瀏覽量
209328 -
內存管理
+關注
關注
0文章
168瀏覽量
14134
原文標題:Linux內核8.8-內存管理之內核非連續物理內存分配
文章出處:【微信號:嵌入式ARM和Linux,微信公眾號:嵌入式ARM和Linux】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論