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