Linux內核之高端內存

物理地址空間佈局

CPU所訪問的都是虛擬內存地址。通常32位Linux內核地址空間劃分0~3G爲用戶空間,3~4G爲內核空間。

這裏寫圖片描述

Linux系統在初始化時,會根據實際的物理內存的大小,爲每個物理頁面創建一個page對象,所有的page對象構成一個mem_map數組。
進一步,針對不同的用途,Linux內核將所有的物理頁面劃分到3類內存管理區中,如圖,分別爲ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。

ZONE_DMA的範圍是0~16M,該區域的物理頁面專門供I/O設備的DMA使用。之所以需要單獨管理DMA的物理頁面,是因爲DMA使用物理地址訪問內存,不經過MMU,並且需要連續的緩衝區,所以爲了能夠提供物理上連續的緩衝區,必須從物理地址空間專門劃分一段區域用於DMA。

ZONE_NORMAL的範圍是16M~896M,該區域的物理頁面是內核能夠直接使用的。

ZONE_HIGHMEM的範圍是896M~結束,該區域即爲高端內存,內核不能直接使用。

爲什麼有高端內存的概念?

當內核模塊代碼或線程訪問內存時,代碼中的內存地址都爲邏輯地址,而對應到真正的物理內存地址,需要地址一對一的映射,假設簡單的地址映射關係,那麼內核邏輯地址空間訪問爲0xc0000000 ~ 0xffffffff,那麼只能訪問1G物理內存。

若機器中安裝8G物理內存,那麼內核就只能訪問前1G物理內存,後面7G物理內存將會無法訪問。顯然不能將內核地址空間0xc0000000 ~ 0xfffffff全部用來簡單的地址映射。因此32bit OS將內核地址空間劃分三部分:ZONE_DMA、ZONE_NORMAL和 ZONE_HIGHMEM。ZONE_HIGHMEM即爲高端內存.

只有1G的線性地址空間,如何使用大於1G的物理內存?這就是高端內存的由來。

內核不能把1G的線性地址全部用來直接內存地址映射,而是需要保留128M的地址空間用來映射896M以上的內存地址。

舉個簡單的例子:
從公司下班回家需要乘坐328公交,公交車有30個固定的位置以及可以容納20人站立的過道,公交途徑很多站,假設公交一開始三十個位置已經固定有人,且中途不下車,在各個站臺,過道的乘客上上下下。
328公交只能載客50人嗎?顯然不是,到達終點站時,售票員發現售出了150張票。
如何將50人載客量的公交實際載客150人?那就是通過在不同的站臺上上下下,但任一時刻,公交最多容納50人。
與之類比,內核空間中896M對應公交的30個固定位置,用作直接映射。高端內存的128M對應讓乘客站着的空間,用作臨時映射,這樣可以通過128M的線性地址空間,訪問超過1G的物理內存。

linux 高端內存的劃分

內核將高端內存劃分爲3部分:
VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。

這裏寫圖片描述

對 於高端內存,可以通過 alloc_page() 或者其它函數獲得對應的 page,但是要想訪問實際物理內存,還得把 page 轉爲線性地址才行(爲什麼?想想 MMU 是如何訪問物理內存的),也就是說,我們需要爲高端內存對應的 page 找一個線性空間,這個過程稱爲高端內存映射。

對應高端內存的3部分,高端內存映射有三種方式:
映射到”內核動態映射空間”(noncontiguous memory allocation)
這種方式很簡單,因爲通過 vmalloc() ,在”內核動態映射空間”申請內存的時候,就可能從高端內存獲得頁面(參看 vmalloc 的實現),因此說高端內存有可能映射到”內核動態映射空間”中。

持久內核映射(permanent kernel mapping)
如果是通過 alloc_page() 獲得了高端內存對應的 page,如何給它找個線性空間?
內核專門爲此留出一塊線性空間,從 PKMAP_BASE 到 FIXADDR_START ,用於映射高端內存。在 2.6內核上,這個地址範圍是 4G-8M 到 4G-4M 之間。這個空間起叫”內核永久映射空間”或者”永久內核映射空間”。這個空間和其它空間使用同樣的頁目錄表,對於內核來說,就是 swapper_pg_dir,對普通進程來說,通過 CR3 寄存器指向。通常情況下,這個空間是 4M 大小,因此僅僅需要一個頁表即可,內核通過來 pkmap_page_table 尋找這個頁表。通過 kmap(),可以把一個 page 映射到這個空間來。由於這個空間是 4M 大小,最多能同時映射 1024 個 page。因此,對於不使用的的 page,及應該時從這個空間釋放掉(也就是解除映射關係),通過 kunmap() ,可以把一個 page 對應的線性地址從這個空間釋放出來。

臨時映射(temporary kernel mapping)
內核在 FIXADDR_START 到 FIXADDR_TOP 之間保留了一些線性空間用於特殊需求。這個空間稱爲”固定映射空間”在這個空間中,有一部分用於高端內存的臨時映射。

非連續內存的管理

非連續內存的線性地址空間是從VMALLOC_START~VMALLOC_END,共128MB大小。當內核需要用vmalloc類的函數進行非連續內存分配時,就會申請一個vm_struct結構來描述對應的vmalloc區,若分配多個vmalloc的內存區,那麼相鄰兩個vmalloc區之間的間隔大小至少爲4KB,即至少是一個頁框大小。

vmalloc是內核中使用到的內存分配函數,一般用來分配大塊內存,
這個函數得到的是連續的虛擬地址,物理上地址不連續。

連續物理內存的分配並不是必要的。對於大部分DMA操作,我們的確需要連續的物理內存;但是對於某些分配內存情況:比如,模塊加載,設備和聲音驅動程序中,可以在內核源碼中關鍵字vmalloc查找,對vmalloc的使用有個感性認識。

內核中描述非連續區的數據結構是struct vm_struct:

struct vm_struct { 
            struct vm_struct *next;   //指向下一個vm_struct區,所有非連續區組成一個單鏈表 
            void *addr;      //代表每個內存區的起始地址,即指向申請的內存區的第一個內存單元(線性地址)
            unsigned long size;       //當前所申請的內存區大小加4KB(安全區) 
            unsigned long flags;       //標識內存區類型 
            struct page **pages;       //指向nr_pages頁描述符指針數組的指針 
            unsigned int nr_pages;     //所申請的內存區大小對應的頁框數
            phys_addr_t phys_addr;     //該字段一般爲0,除非內存已經被申請用作映射一個硬件設備的I/O共享內存
            const void *caller;        //當前調用vmalloc類的函數的返回地址
 };

1分配非連續的內存區
分配函數主要是vmalloc(),vmap(),vmalloc()會去調用__vmalloc_node_range()函數:

void *__vmalloc_node_range(unsigned long size, unsigned long align,
            unsigned long start, unsigned long end, gfp_t gfp_mask,
            pgprot_t prot, unsigned long vm_flags, int node,
            const void *caller)
{
    struct vm_struct *area;
    void *addr;
    unsigned long real_size = size;

    //size要對其爲4K的整數倍,因爲非連續內存區域是將各個物理頁進行映射
    size = PAGE_ALIGN(size);
    if (!size || (size >> PAGE_SHIFT) > totalram_pages)
        goto fail;

    //找到一塊空閒的線性地址區域,用來映射該非連續內存
    area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
                vm_flags, start, end, node, gfp_mask, caller);
    if (!area)
        goto fail;

    addr = __vmalloc_area_node(area, gfp_mask, prot, node);
    if (!addr)
        return NULL;

    /*
     * In this function, newly allocated vm_struct has VM_UNINITIALIZED
     * flag. It means that vm_struct is not fully initialized.
     * Now, it is fully initialized, so remove this flag here.
     */
    clear_vm_uninitialized_flag(area);

    /*
     * A ref_count = 2 is needed because vm_struct allocated in
     * __get_vm_area_node() contains a reference to the virtual address of
     * the vmalloc'ed block.
    /
    /*一個內存塊分配的通知*/
    kmemleak_alloc(addr, real_size, 2, gfp_mask);

    return addr;

fail:
    warn_alloc(gfp_mask,
              "vmalloc: allocation failure: %lu bytes", real_size);
    return NULL;
}

分配物理頁面 並映射
__vmalloc_area_node:

static 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;
    const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
    const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;

    // 計算想要分配的物理頁數
    nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
    array_size = (nr_pages * sizeof(struct page *));

    area->nr_pages = nr_pages;
    /* Please note that the recursion is strictly bounded. */
    //array_size大於PAGE_SIZE的話,就需要遞歸調用__vmalloc_node申請內存,  
    //否則直接調用kmalloc_node申請內存  
    if (array_size > PAGE_SIZE) {
        pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                PAGE_KERNEL, node, area->caller);
    } else {
        pages = kmalloc_node(array_size, nested_gfp, node);
    }
    area->pages = pages;
    if (!area->pages) {
        remove_vm_area(area->addr);
        kfree(area);
        return NULL;
    }

    //爲非連續內存進行頁面的分配,每次分配一個頁面,將其頁框指針記錄在pages數組中,可以看到物理內存地址不是連續的
    for (i = 0; i < area->nr_pages; i++) {
        struct page *page;

        if (node == NUMA_NO_NODE)
            page = alloc_page(alloc_mask);
        else
            page = alloc_pages_node(node, alloc_mask, 0);

        if (unlikely(!page)) {
            /* Successfully allocated i pages, free them in __vunmap() */
            area->nr_pages = i;
            goto fail;
        }
        area->pages[i] = page;
        if (gfpflags_allow_blocking(gfp_mask))
            cond_resched();
    }

    //將虛擬地址跟每個物理頁面建立映射  
    if (map_vm_area(area, prot, pages))
        goto fail;
    return area->addr;

fail:
    warn_alloc(gfp_mask,
              "vmalloc: allocation failure, allocated %ld of %ld bytes",
              (area->nr_pages*PAGE_SIZE), area->size);
    vfree(area->addr);
    return NULL;
}

持久內核映射

從 PKMAP_BASE 到 FIXADDR_START,爲內核的持久映射空間。
通過kmap函數實現,將高端內存長期映射到內核地址空間中。
kmap的內核實現:

void *kmap(struct page *page)
{
    BUG_ON(in_interrupt());
    if (!PageHighMem(page))
        return page_address(page);

    return kmap_high(page);
}

如果給定的page不是高端物理頁面,直接通過page_address返回該頁面的虛擬地址
否則調用kmap_high建立高端映射.

kmap_high:

void *kmap_high(struct page *page)
{
    unsigned long vaddr;

    /*
     * For highmem pages, we can't trust "virtual" until
     * after we have the lock.
     */
    lock_kmap();
    vaddr = (unsigned long)page_address(page);
    if (!vaddr)
        vaddr = map_new_virtual(page);
    pkmap_count[PKMAP_NR(vaddr)]++;
    BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
    unlock_kmap();
    return (void*) vaddr;
}

在kmap_high函數中先調用page_address檢查該page是否已經被映射。如果沒有映射調用map_new_virtual(page)處理。

pkmap_count是一個計數數組,標示該頁被引用的次數。 每一個持久地址映射項都對應一個計數.
對於這個計數,我們主要區分三種情況:
1. count= 0, 相應的頁表項還沒有映射到高端物理內存頁框,可以使用它
2. count = 1,相應的頁表項沒有映射到高端物理內存頁框,但是現在不能使用它,因爲自從上次使用過後,相應的TLB項還沒有刷新。
3. count > 1,相應的頁表項已經映射到高端物理內存頁框,使用者的數目是n - 1

步驟:通過kmap將傳入的page*映射到虛擬內存時分以下幾步:1.在內核地址空間(持久映射區)分配一個頁;2.建立傳入的物理內存頁和虛擬地址之間的映射;3.統計內核地址空間中哪些頁被引用。

map_new_virtual:

static inline unsigned long map_new_virtual(struct page *page)
{
    unsigned long vaddr;
    int count;
    unsigned int last_pkmap_nr;
    unsigned int color = get_pkmap_color(page);

start:
    count = get_pkmap_entries_count(color);
    /* Find an empty entry */
    for (;;) {
        last_pkmap_nr = get_next_pkmap_nr(color);
        if (no_more_pkmaps(last_pkmap_nr, color)) {
            flush_all_zero_pkmaps();
            count = get_pkmap_entries_count(color);
        }
        if (!pkmap_count[last_pkmap_nr])
            break;  /* Found a usable entry */
        if (--count)
            continue;

        /*
         * Sleep for somebody else to unmap their entries
         */
        {
            DECLARE_WAITQUEUE(wait, current);
            wait_queue_head_t *pkmap_map_wait =
                get_pkmap_wait_queue_head(color);

            __set_current_state(TASK_UNINTERRUPTIBLE);
            add_wait_queue(pkmap_map_wait, &wait);
            unlock_kmap();
            schedule();
            remove_wait_queue(pkmap_map_wait, &wait);
            lock_kmap();

            /* Somebody else might have mapped it while we slept */
            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));

    pkmap_count[last_pkmap_nr] = 1;
    set_page_address(page, (void *)vaddr);

    return vaddr;
}

1.從上一次使用的位置last_pkmap_nr開始反向掃描pkmap_count數組,直到找到一個空閒位置,如果沒有空閒位置,該函數進入睡眠狀態,直到內核的另一部分執行解除映射操作。
2.調用set_pte_at()修改內核頁表,將該頁映射到指定位置,但尚未更新TLB。
3.pkmap_count[last_pkmap_nr] = 1;新位置的使用計數器設置爲1,表示該分頁已分配,但是無法使用。因爲TLB項未更新。
4.調用set_page_address()將該頁添加到內核映射的數據結構。

set_page_address:

/**
 * set_page_address - set a page's virtual address
 * @page: &struct page to set
 * @virtual: virtual address to use
 */
void set_page_address(struct page *page, void *virtual)
{
    unsigned long flags;
    struct page_address_slot *pas;
    struct page_address_map *pam;

    BUG_ON(!PageHighMem(page));

    pas = page_slot(page);
    if (virtual) {      /* Add */
        pam = &page_address_maps[PKMAP_NR((unsigned long)virtual)];
        pam->page = page;
        pam->virtual = virtual;

        spin_lock_irqsave(&pas->lock, flags);
        list_add_tail(&pam->list, &pas->lh);
        spin_unlock_irqrestore(&pas->lock, flags);
    } else {        /* Remove */
        spin_lock_irqsave(&pas->lock, flags);
        list_for_each_entry(pam, &pas->lh, list) {
            if (pam->page == page) {
                list_del(&pam->list);
                spin_unlock_irqrestore(&pas->lock, flags);
                goto done;
            }
        }
        spin_unlock_irqrestore(&pas->lock, flags);
    }
done:
    return;
}

該函數爲page指針和虛擬地址建立一個page_address_map結構來映射,並將結構添加到page_address_maps全局hash鏈表中。

總結步驟:
1.當調用kmap函數,傳入一個物理頁幀page的指針時,會返回該page關聯的虛擬地址。kmap函數中先判斷該page是低端內存還是高端內存,如果是低端內存,則直接調用page_address函數,通過直接映射關係,計算出該page關聯的虛擬地址;如果傳入kmap函數的page對應的是高端內存的物理頁幀,則調用kmap_high函數處理。

2.進入在kmap_high函數中,說明入參page對應高端內存的物理頁幀,仍然要先調用page_address判斷該物理頁是否已經關聯了虛擬地址。如果已經關聯則將對應的虛擬地址在pkmap_count中的項+1,返回該page對應的虛擬地址。如果沒有關聯,則調用map_new_virtual函數,給該page建立關聯的物理地址。

3.在map_new_virtual函數中,先通過掃描pkmap_count中的項爲該page找到未用的虛擬地址,然後修改頁表,便於通過虛擬地址找到物理地址。然後調用set_page_address函數爲page和虛擬地址建立映射結構page_address_map,並添加到上圖所示的結構中。

臨時映射

臨時內核映射實現簡單,可以用在中斷處理程序和可延遲函數的內部(這些函數不能被阻塞),因爲臨時內核映射從來不阻塞當前進程,因爲它被設計成是原子的。對比永久內核映射,發現如果頁框暫時沒有空閒的虛擬地址可以映射,那麼永久內核映射將要被阻塞。

建立臨時內核映射禁用內核搶佔,這是必須的,因爲映射對於每個處理器都是獨特的,如果沒有禁用搶佔,那麼哪個任務在哪個CPU上運行是不確定的。

臨時映射通過kmap_atomic函數實現:

void *kmap_atomic(struct page *page)
{
    int idx, cpu_idx;
    unsigned long vaddr;

    /**
     *原子映射是基於每個cpu的,因此在當前cpu上應用搶佔,直到unmap的時候才 
     *開啓,這樣不會導致原子映射的重入了, 
     */  
    preempt_disable();
    pagefault_disable();
    if (!PageHighMem(page))
        return page_address(page);

    //遞增一個每cpu變量,返回遞增後的結果
    cpu_idx = kmap_atomic_idx_push();

    /* 
     *kernel可以在多個cpu上同時運行不同的task,他們共同使用一個內存地址空間
     *也就是說,內存空間對於多個cpu看到的是同一個,該函數使用的是地址空間中頂部的 
     *一小段地址空間,也就是臨時映射區,內核邏輯將這一小段地址空間分成若干各節 
     *每一節的大小是一個頁面的大小,可以映射一個頁面,根據公用地址空間的原理 
     *所有的cpu共同使用這些節,因此如何能保證N個cpu調用此函數不會將page映射一個地址呢,
     *這就是這個數學公式所起到的作用 
     */  
    idx = cpu_idx + KM_TYPE_NR * smp_processor_id();
    //固定映射的線性地址轉化成虛擬地址  
    vaddr = FIXMAP_ADDR(idx);

    //設置頁表
    set_pte_at(&init_mm, vaddr, fixmap_page_table + idx,
           mk_pte(page, kmap_prot));

    return (void *)vaddr;
}
EXPORT_SYMBOL(kmap_atomic);

調用kmap_atomic的時候,有一個參數是的枚舉類型km_type,不同的cpu得到的vaddr不一樣,保證N個cpu調用此函數不會將page映射一個地址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章