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