首頁>技術>

在 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 程序的 頁表項 複製到當前程序的 頁表項 中,這樣就可以實現所有程序的核心記憶體地址空間同步。

9
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 「前端進階系列」如何實現一個深淺複製?