Linux內(nèi)存管理--高端內(nèi)存映射與非連續(xù)內(nèi)存分配
對于32位的機器來說,高于896的物理內(nèi)存在內(nèi)核中屬于高端內(nèi)存,并沒有對內(nèi)存做一一的映射,系統(tǒng)保留了128M的線性地址空間來臨時映射這些高于896M的高端物理內(nèi)存,該線性地址為3G+768m~4G。返回頁框線性地址的頁分配函數(shù)對于高端內(nèi)存是無效的,因為高端內(nèi)存不會自動的映射到某個線性地址。例如__get_free_pages(GFP_HIGH_MEM,0)函數(shù)分配高端內(nèi)存頁框時,返回的是NULL;內(nèi)核可以采用三種方式來使用高端物理內(nèi)存:***內(nèi)核映射,臨時內(nèi)核映射和非連續(xù)內(nèi)存分配。建立***內(nèi)核映射可能會阻塞當前進程的執(zhí)行,這發(fā)生在沒有高端內(nèi)存沒有空閑的頁表項來做映射的情況下,因此在中斷等不能阻塞的代碼中不要使用***內(nèi)核映射。臨時內(nèi)核映射不會發(fā)生阻塞的情況,但必須保證沒有其他的內(nèi)核路徑在使用同樣的臨時內(nèi)核映射。
一、***內(nèi)存映射
***內(nèi)核映射使用的是內(nèi)核主頁表中的一個專門的頁表,其地址存放在pkmap_page_table中,頁表的頁表項由宏LAST_PKMAP產(chǎn)生,頁表中包含512或者1024項。
該頁表映射的線性地址從PKMAP_BASE開始,pkmap_count數(shù)組包含了LAST_PKMAP個計數(shù)器,pkmap_page_table頁表中的每項都有對應(yīng)一個計數(shù)值:
計數(shù)器為0:對應(yīng)的頁表項是空閑可用的。
計數(shù)器為1:對應(yīng)的頁表項沒有映射任何高端內(nèi)存,但是它不能夠使用,因為自從***一次使用以來,其相應(yīng)的TLB尚未被刷新。
計數(shù)器為n:有多個內(nèi)核成分使用該頁表項所對應(yīng)的頁框。
源碼分析:
- void fastcall *kmap_high(struct page *page)
- {
- unsigned long vaddr;
- spin_lock(&kmap_lock);
- //page->virtual記錄了頁框?qū)?yīng)的線性地址
- vaddr = (unsigned long)page_address(page);
- //若頁框未被映射過,分配新的空閑頁表項
- if (!vaddr)
- vaddr = map_new_virtual(page);
- //若是剛分配到了空閑頁表項的話,在map_new_virtual()中其count
- //值被設(shè)置為了1,在這里再次++
- pkmap_count[PKMAP_NR(vaddr)]++;
- BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
- spin_unlock(&kmap_lock);
- return (void*) vaddr;
- }
- static inline unsigned long map_new_virtual(struct page *page)
- {
- unsigned long vaddr;
- int count;
- start:
- count = LAST_PKMAP;
- //尋找一個空的頁表項
- for (;;) {
- //從上一次找到的空閑頁表項的位置開始尋找
- last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
- if (!last_pkmap_nr) {
- flush_all_zero_pkmaps();
- count = LAST_PKMAP;
- }
- //找到一個未用的空閑頁表項
- if (!pkmap_count[last_pkmap_nr])
- break; /* Found a usable entry */
- //count變?yōu)?的話,意味著當前沒有空閑的頁表項
- if (--count)
- continue;
- //沒有找到空閑的頁表項,將當前進程加入到等待隊列,進行調(diào)度,直到
- //有空閑的頁表項或者該頁面被別人映射
- {
- DECLARE_WAITQUEUE(wait, current);
- __set_current_state(TASK_UNINTERRUPTIBLE);
- add_wait_queue(&pkmap_map_wait, &wait);
- spin_unlock(&kmap_lock);
- schedule();
- remove_wait_queue(&pkmap_map_wait, &wait);
- spin_lock(&kmap_lock);
- //有可能在該進程睡眠期間,有其它進程對該頁面做了內(nèi)存映射
- if (page_address(page))
- return (unsigned long)page_address(page);
- /* Re-start */
- goto start;
- }
- }
- //得到對應(yīng)頁表項對應(yīng)的線性地址
- vaddr = PKMAP_ADDR(last_pkmap_nr);
- //設(shè)置對應(yīng)的頁表項
- set_pte_at(&init_mm, vaddr,
- &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
- //設(shè)置***內(nèi)存映射數(shù)組的值
- pkmap_count[last_pkmap_nr] = 1;
- //將page->virtual的值設(shè)為vaddr,ok
- set_page_address(page, (void *)vaddr);
- return vaddr;
- }
二、臨時內(nèi)核映射
臨時內(nèi)核映射比較簡單,在內(nèi)核中,為每個cpu都保存了一組頁表項,每個頁表項由一個特定的內(nèi)核成分使用,需要注意的是,不同的內(nèi)核控制路徑不應(yīng)該同時使用一個頁表項,這樣的話,會使后一個內(nèi)核控制路徑將前一個內(nèi)核控制路徑設(shè)置頁表項給沖掉。
建立臨時內(nèi)核映射使用kmap_atomic()函數(shù)。
- void *__kmap_atomic(struct page *page, enum km_type type)
- {
- enum fixed_addresses idx;
- unsigned long vaddr;
- //禁止內(nèi)核搶占,以預(yù)防不同內(nèi)核控制路徑使用同一頁表項
- inc_preempt_count();
- //非高端內(nèi)存,不用進行高端內(nèi)存映射
- if (!PageHighMem(page))
- return page_address(page);
- //得到使用的頁表項的下表索引
- idx = type + KM_TYPE_NR*smp_processor_id();
- //得到相關(guān)頁表項的線性地址
- vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
- //設(shè)置對應(yīng)的頁表項
- set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));
- local_flush_tlb_one((unsigned long)vaddr);
- return (void*) vaddr;
- }
三、非連續(xù)內(nèi)存分配
下圖顯示了如何使用高于0xc0000000線性地址的線性地址空間:
- 內(nèi)存區(qū)的開始部分包含的是對前896MB的RAM進行映射的線性地址,直接映射的物理內(nèi)存的末尾的線性地址保存在high_memory變量中。
- 內(nèi)存區(qū)的結(jié)尾位置包含的是固定映射的線性地址。
- 從PKMAP_BASE開始,是用于高端內(nèi)存***映射的線性地址。
- 其余的線性地址用于非連續(xù)內(nèi)存區(qū),在物理內(nèi)存映射和***個內(nèi)存區(qū)間有一個8M的安全區(qū),用于捕捉對內(nèi)存的越界訪問,同樣道理,插入其它4KB大小的內(nèi)存區(qū)來隔離非連續(xù)內(nèi)存區(qū)。
非連續(xù)內(nèi)存區(qū)描述符數(shù)據(jù)結(jié)構(gòu):
- struct vm_struct {
- void *addr;//內(nèi)存區(qū)***個內(nèi)存單元的線性地址
- unsigned long size;//內(nèi)存區(qū)的大小加上4K,4K是用來檢查越界的內(nèi)存
- unsigned long flags;//非連續(xù)內(nèi)存的類型,VM_ALLOC表示使用vmalloc分配的內(nèi)存,VM_MAP表示使用vmap分配的內(nèi)存,
- //VM_IOREMAP表示用ioremap()分配的內(nèi)存
- struct page **pages;//非連續(xù)內(nèi)存的的物理頁數(shù)組
- unsigned int nr_pages;//非連續(xù)內(nèi)存的物理頁的個數(shù)
- unsigned long phys_addr;
- struct vm_struct *next;//用來將各個非連續(xù)內(nèi)存描述符串聯(lián)起來
- };
1、分配非連續(xù)的內(nèi)存區(qū)
分配函數(shù)主要是vmalloc(),vmap(),vmalloc()會去調(diào)用__vmalloc_node()函數(shù):
- void *__vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot,
- int node)
- {
- struct vm_struct *area;
- //size要對其為4K的整數(shù)倍,因為非連續(xù)內(nèi)存區(qū)域是將各個物理頁進行映射
- size = PAGE_ALIGN(size);
- if (!size || (size >> PAGE_SHIFT) > num_physpages)
- return NULL;
- //找到一塊空閑的線性地址區(qū)域,用來映射該非連續(xù)內(nèi)存
- area = get_vm_area_node(size, VM_ALLOC, node);
- if (!area)
- return NULL;
- return __vmalloc_area_node(area, gfp_mask, prot, node);
- }
- void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
- pgprot_t prot, int node)
- {
- struct page **pages;
- unsigned int nr_pages, array_size, i;
- //計算要映射的物理頁數(shù)
- nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT;
- //計算vm_struct中pages數(shù)組的數(shù)組元素個數(shù)
- array_size = (nr_pages * sizeof(struct page *));
- //記錄下物理頁面的數(shù)目
- area->nr_pages = nr_pages;
- //為vm_struct中的pages數(shù)組分配內(nèi)存
- if (array_size > PAGE_SIZE) {
- pages = __vmalloc_node(array_size, gfp_mask, PAGE_KERNEL, node);
- area->flags |= VM_VPAGES;
- } else
- pages = kmalloc_node(array_size, (gfp_mask & ~__GFP_HIGHMEM), node);
- area->pages = pages;
- if (!area->pages) {
- remove_vm_area(area->addr);
- kfree(area);
- return NULL;
- }
- memset(area->pages, 0, array_size);
- //為非連續(xù)內(nèi)存進行頁面的分配,每次分配一個頁面,將其頁框指針記錄在pages數(shù)組中
- for (i = 0; i < area->nr_pages; i++) {
- if (node < 0)
- area->pages[i] = alloc_page(gfp_mask);
- else
- area->pages[i] = alloc_pages_node(node, gfp_mask, 0);
- if (unlikely(!area->pages[i])) {
- /* Successfully allocated i pages, free them in __vunmap() */
- area->nr_pages = i;
- goto fail;
- }
- }
- //將各個物理頁框映射到分配好的空閑線性區(qū)里面去
- if (map_vm_area(area, prot, &pages))
- goto fail;
- return area->addr;
- fail:
- vfree(area->addr);
- return NULL;
- }
__vmalloc_node()并不觸及當前進程的頁表,因此當內(nèi)核態(tài)進程訪問非連續(xù)內(nèi)存區(qū)時,會發(fā)生缺頁異常,因為對應(yīng)的進程的相應(yīng)地址對應(yīng)的頁表項為空。當缺頁異常發(fā)生時,異常處理程序會到內(nèi)核主頁表(init_mm.pgd頁全局目錄)中去查看是否有對應(yīng)的頁表項,有的話,就會修改當前進程的頁表項,并繼續(xù)進程的執(zhí)行。
2、釋放非連續(xù)的內(nèi)存區(qū)
- void vfree(void *addr)
- {
- BUG_ON(in_interrupt());
- __vunmap(addr, 1);
- }
- void __vunmap(void *addr, int deallocate_pages)
- {
- struct vm_struct *area;
- if (!addr)
- return;
- //釋放的地址應(yīng)該是4k的整數(shù)倍
- if ((PAGE_SIZE-1) & (unsigned long)addr) {
- printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
- WARN_ON(1);
- return;
- }
- //移除對應(yīng)的vm_area數(shù)據(jù)描述符,解除對各個物理頁面的頁面映射項
- area = remove_vm_area(addr);
- if (unlikely(!area)) {
- printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
- addr);
- WARN_ON(1);
- return;
- }
- debug_check_no_locks_freed(addr, area->size);
- //需要向伙伴系統(tǒng)歸還非連續(xù)的物理頁
- if (deallocate_pages) {
- int i;
- //將各個物理頁面歸還給伙伴系統(tǒng)
- for (i = 0; i < area->nr_pages; i++) {
- BUG_ON(!area->pages[i]);
- __free_page(area->pages[i]);
- }
- if (area->flags & VM_VPAGES)
- vfree(area->pages);
- else
- kfree(area->pages);
- }
- kfree(area);
- return;
- }
與vmalloc()一樣,該函數(shù)修改的是主內(nèi)核頁全局目錄和它的頁表表項,內(nèi)核永遠不會回收頁全局,頁上級,頁中間目錄,也不會回收頁表,而進程的頁表會指向這些表項。這樣的話,假設(shè)一個內(nèi)核進程訪問已經(jīng)釋放的非連續(xù)內(nèi)存,最終就會訪問到已經(jīng)被清空的頁表表項,從而引發(fā)缺頁異常,這就是一個錯誤。