在 Linux 系統中的每個程序都有獨立 4GB 記憶體空間,而 Linux 把這 4GB 記憶體空間劃分為使用者記憶體空間(0 ~ 3GB)和核心記憶體空間(3GB ~ 4GB),而核心記憶體空間由劃分為直接記憶體對映區和動態記憶體對映區(vmalloc區)。
直接記憶體對映區從 3GB 開始到 3GB+896MB 處結束,直接記憶體對映區的特點就是物理地址與虛擬地址的關係為:虛擬地址 = 物理地址 + 3GB。而動態記憶體對映區不能透過這種簡單的關係關聯,而是需要訪問動態記憶體對映區時,由核心動態申請物理記憶體並且對映到動態記憶體對映區中。下圖是動態記憶體對映區在記憶體空間的位置:
為什麼需要vmalloc區由於直接記憶體對映區(3GB ~ 3GB+896MB)是直接對映到物理地址(0 ~ 896MB)的,所以核心不能透過直接記憶體對映區使用到超過 896MB 之外的物理記憶體。這時候就需要提供一個機制能夠讓核心使用 896MB 之外的物理記憶體,所以 Linux 就實現了一個 vmalloc 機制。vmalloc 機制的目的是在核心記憶體空間提供一個記憶體區,能夠讓這個記憶體區對映到 896MB 之外的物理記憶體。如下圖:
那麼什麼時候使用 vmalloc 呢?一般來說,如果要申請大塊的記憶體就可以用vmalloc。
vmalloc實現可以透過 vmalloc() 函式向核心申請一塊記憶體,其原型如下:
void * vmalloc(unsigned long size);
引數 size 表示要申請的記憶體塊大小。
我們看看看 vmalloc() 函式的實現,程式碼如下:
static inline void * vmalloc(unsigned long size){ return __vmalloc(size, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL);}
從上面程式碼可以看出,vmalloc() 函式直接呼叫了 __vmalloc() 函式,而 __vmalloc() 函式的實現如下:
void * __vmalloc(unsigned long size, int gfp_mask, pgprot_t prot){ void * addr; struct vm_struct *area; size = PAGE_ALIGN(size); // 記憶體對齊 if (!size || (size >> PAGE_SHIFT) > num_physpages) { BUG(); return NULL; } area = get_vm_area(size, VM_ALLOC); // 申請一個合法的虛擬地址 if (!area) return NULL; addr = area->addr; // 對映物理記憶體地址 if (vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask, prot)) { vfree(addr); return NULL; } return addr;}
__vmalloc() 函式主要工作有兩點:
呼叫 get_vm_area() 函式申請一個合法的虛擬記憶體地址。呼叫 vmalloc_area_pages() 函式把虛擬記憶體地址對映到物理記憶體地址。【文章福利】需要C/C++ Linux伺服器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)
接下來,我們看看 get_vm_area() 函式的實現,程式碼如下:
struct vm_struct * get_vm_area(unsigned long size, unsigned long flags){ unsigned long addr; struct vm_struct **p, *tmp, *area; area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL); if (!area) return NULL; size += PAGE_SIZE; addr = VMALLOC_START; write_lock(&vmlist_lock); for (p = &vmlist; (tmp = *p) ; p = &tmp->next) { if ((size + addr) < addr) goto out; if (size + addr <= (unsigned long) tmp->addr) break; addr = tmp->size + (unsigned long) tmp->addr; if (addr > VMALLOC_END-size) goto out; } area->flags = flags; area->addr = (void *)addr; area->size = size; area->next = *p; *p = area; write_unlock(&vmlist_lock); return area;out: write_unlock(&vmlist_lock); kfree(area); return NULL;}
get_vm_area() 函式比較簡單,首先申請一個型別為 vm_struct 的結構 area 用於儲存申請到的虛擬記憶體地址。然後查詢可用的虛擬記憶體地址,如果找到,就把虛擬記憶體到虛擬記憶體地址儲存到 area 變數中。最後把 area 連線到 vmalloc 虛擬記憶體地址管理連結串列 vmlist 中。vmlist 連結串列最終結果如下圖:
申請到虛擬記憶體地址後,__vmalloc() 函式會呼叫 vmalloc_area_pages() 函式來對虛擬記憶體地址與物理記憶體地址進行對映。
我們知道,對映過程就是對程序的 頁表 進行對映。但每個程序都有一個獨立 頁表(核心執行緒除外),並且我們知道核心空間是所有程序共享的,那麼就有個問題:如果只對映當前程序 頁表 的核心空間,那麼怎麼同步到其他程序的核心空間呢?
為了解決核心空間同步問題,Linux 並不是直接對當前程序的核心空間對映的,而是對 init 程序的核心空間(init_mm)進行對映,我們來看看 vmalloc_area_pages() 函式的實現:
inline int vmalloc_area_pages (unsigned long address, unsigned long size, int gfp_mask, pgprot_t prot){ pgd_t * dir; unsigned long end = address + size; int ret; dir = pgd_offset_k(address); // 獲取 address 地址在 init 程序對應的頁目錄項 spin_lock(&init_mm.page_table_lock); // 對 init_mm 上鎖 do { pmd_t *pmd; pmd = pmd_alloc(&init_mm, dir, address); ret = -ENOMEM; if (!pmd) break; ret = -ENOMEM; if (alloc_area_pmd(pmd, address, end - address, gfp_mask, prot)) // 對頁目錄項進行對映 break; address = (address + PGDIR_SIZE) & PGDIR_MASK; dir++; ret = 0; } while (address && (address < end)); spin_unlock(&init_mm.page_table_lock); return ret;}
從上面程式碼可以看出,vmalloc_area_pages() 函式對映的主體是 init 程序的記憶體空間。因為對映的 init 程序的記憶體空間,所以當前程序訪問 vmalloc() 函式申請的記憶體時,由於沒有對虛擬記憶體進行對映,所以會發生 缺頁異常 而觸發核心呼叫 do_page_fault() 函式來修復。我們看看 do_page_fault() 函式對 vmalloc() 申請的記憶體異常處理:
void do_page_fault(struct pt_regs *regs, unsigned long error_code){ ... __asm__("movl %%cr2,%0":"=r" (address)); // 獲取出錯的虛擬地址 ... if (address >= TASK_SIZE && !(error_code & 5)) goto vmalloc_fault; ...vmalloc_fault: { int offset = __pgd_offset(address); pgd_t *pgd, *pgd_k; pmd_t *pmd, *pmd_k; pte_t *pte_k; asm("movl %%cr3,%0":"=r" (pgd)); pgd = offset + (pgd_t *)__va(pgd); pgd_k = init_mm.pgd + offset; if (!pgd_present(*pgd_k)) goto no_context; set_pgd(pgd, *pgd_k); pmd = pmd_offset(pgd, address); pmd_k = pmd_offset(pgd_k, address); if (!pmd_present(*pmd_k)) goto no_context; set_pmd(pmd, *pmd_k); pte_k = pte_offset(pmd_k, address); if (!pte_present(*pte_k)) goto no_context; return; }}
上面的程式碼就是當程序訪問 vmalloc() 函式申請到的記憶體時,發生 缺頁異常 而進行的異常修復,主要的修復過程就是把 init 程序的 頁表項 複製到當前程序的 頁表項 中,這樣就可以實現所有程序的核心記憶體地址空間同步。