高端內存映射之kmap持久內核映射--Linux內存管理(二十)

1 高端內存與內核映射

儘管vmalloc函數族可用於從高端內存域向內核映射頁幀(這些在內核空間中一般是沒法直接看到的), 但這並非這些函數的實際用途.前端

重要的是強調如下事實 : 內核提供了其餘函數用於將ZONE_HIGHMEM頁幀顯式映射到內核空間, 這些函數與vmalloc機制無關. 所以, 這就形成了混亂.linux

而在高端內存的頁不能永久地映射到內核地址空間. 所以, 經過alloc_pages()函數以__GFP_HIGHMEM標誌得到的內存頁就不可能有邏輯地址.c#

在x86_32體系結構總, 高於896MB的全部物理內存的範圍大都是高端內存, 它並不會永久地或自動映射到內核地址空間, 儘管X86處理器可以尋址物理RAM的範圍達到4GB(啓用PAE能夠尋址64GB), 一旦這些頁被分配, 就必須映射到內核的邏輯地址空間上. 在x86_32上, 高端地址的頁被映射到內核地址空間(即虛擬地址空間的3GB~4GB)數組

內核地址空間的最後128 MiB用於何種用途呢?緩存

該部分有3個用途。數據結構

  1. 虛擬內存中連續、但物理內存中不連續的內存區,能夠在vmalloc區域分配. 該機制一般用於用戶過程, 內核自身會試圖盡力避免非連續的物理地址。內核一般會成功,由於大部分大的內存塊都在啓動時分配給內核,那時內存的碎片尚不嚴重。但在已經運行了很長時間的系統上, 在內核須要物理內存時, 就可能出現可用空間不連續的狀況. 此類狀況, 主要出如今動態加載模塊時.併發

  2. 持久映射用於將高端內存域中的非持久頁映射到內核中app

  3. 固定映射是與物理地址空間中的固定頁關聯的虛擬地址空間項,但具體關聯的頁幀能夠自由選擇. 它與經過固定公式與物理內存關聯的直接映射頁相反,虛擬固定映射地址與物理內存位置之間的關聯能夠自行定義,關聯創建後內核老是會注意到的.electron

在這裏有兩個預處理器符號很重要 __VMALLOC_RESERVE設置了vmalloc區域的長度, 而MAXMEM則表示內核能夠直接尋址的物理內存的最大可能數量.ide

內核中, 將內存劃分爲各個區域是經過圖3-15所示的各個常數控制的。根據內核和系統配置, 這些常數可能有不一樣的值。直接映射的邊界由high_memory指定。

  1. 直接映射區

線性空間中從3G開始最大896M的區間, 爲直接內存映射區,該區域的線性地址和物理地址存在線性轉換關係:線性地址=3G+物理地址。

  1. 動態內存映射區

該區域由內核函數vmalloc來分配, 特色是 : 線性空間連續, 可是對應的物理空間不必定連續. vmalloc分配的線性地址所對應的物理頁可能處於低端內存, 也可能處於高端內存.

  1. 永久內存映射區

該區域可訪問高端內存. 訪問方法是使用alloc_page(_GFP_HIGHMEM)分配高端內存頁或者使用kmap函數將分配到的高端內存映射到該區域.

  1. 固定映射區

該區域和4G的頂端只有4k的隔離帶,其每一個地址項都服務於特定的用途,如ACPI_BASE等。

說明

注意用戶空間固然可使用高端內存,並且是正常的使用,內核在分配那些不常用的內存時,都用高端內存空間(若是有),所謂不常用是相對來講的,好比內核的一些數據結構就屬於常用的,而用戶的一些數據就屬於不常用的。用戶在啓動一個應用程序時,是須要內存的,而每一個應用程序都有3G的線性地址,給這些地址映射頁表時就能夠直接使用高端內存。

並且還要糾正一點的是:那128M線性地址不只僅是用在這些地方的,若是你要加載一個設備,而這個設備須要映射其內存到內核中,它也須要使用這段線性地址空間來完成,不然內核就不能訪問設備上的內存空間了.

總之,內核的高端線性地址是爲了訪問內核固定映射之外的內存資源。進程在使用內存時,觸發缺頁異常,具體將哪些物理頁映射給用戶進程是內核考慮的事情. 在用戶空間中沒有高端內存這個概念.

即內核對於低端內存, 不須要特殊的映射機制, 使用直接映射便可以訪問普通內存區域, 而對於高端內存區域, 內核能夠採用三種不一樣的機制將頁框映射到高端內存 : 分別叫作永久內核映射臨時內核映射以及非連續內存分配

2 持久內核映射

若是須要將高端頁幀長期映射(做爲持久映射)到內核地址空間中, 必須使用kmap函數. 須要映射的頁用指向page的指針指定,做爲該函數的參數。該函數在有必要時建立一個映射(即,若是該頁確實是高端頁), 並返回數據的地址.

若是沒有啓用高端支持, 該函數的任務就比較簡單. 在這種狀況下, 全部頁均可以直接訪問, 所以只須要返回頁的地址, 無需顯式建立一個映射.

若是確實存在高端頁, 狀況會比較複雜. 相似於vmalloc, 內核首先必須創建高端頁和所映射到的地址之間的關聯. 還必須在虛擬地址空間中分配一個區域以映射頁幀, 最後, 內核必須記錄該虛擬區域的哪些部分在使用中, 哪些仍然是空閒的.

2.1 數據結構

內核在IA-32平臺上在vmalloc區域以後分配了一個區域, 從PKMAP_BASEFIXADDR_START. 該區域用於持久映射. 不一樣體系結構使用的方案是相似的.

永久內核映射容許內核創建高端頁框到內核地址空間的長期映射。 他們使用着內核頁表中一個專門的頁表, 其地址存放在變量pkmap_page_table中, 頁表中的表項數由LAST_PKMAP宏產生. 所以,內核一次最多訪問2MB或4MB的高端內存.

#define PKMAP_BASE              (PAGE_OFFSET - PMD_SIZE)

頁表映射的線性地址從PKMAP_BASE開始. pkmap_count數組包含LAST_PKMAP個計數器,pkmap_page_table頁表中的每一項都有一個。

//  http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126
static int pkmap_count[LAST_PKMAP];
static  __cacheline_aligned_in_smp DEFINE_SPINLOCK(kmap_lock);

pte_t * pkmap_page_table;

高端映射區邏輯頁面的分配結構用分配表(pkmap_count)來描述,它有1024項,對應於映射區內不一樣的邏輯頁面。當分配項的值等於0時爲自由項,等於1時爲緩衝項,大於1時爲映射項。映射頁面的分配基於分配表的掃描,當全部的自由項都用完時,系統將清除全部的緩衝項,若是連緩衝項都用完時,系統將進入等待狀態。

// http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126
/* 
高端映射區邏輯頁面的分配結構用分配表(pkmap_count)來描述,它有1024項, 
對應於映射區內不一樣的邏輯頁面。當分配項的值等於零時爲自由項,等於1時爲 
緩衝項,大於1時爲映射項。映射頁面的分配基於分配表的掃描,當全部的自由 
項都用完時,系統將清除全部的緩衝項,若是連緩衝項都用完時,系 
統將進入等待狀態。 
*/  
static int pkmap_count[LAST_PKMAP];

pkmap_count(在mm/highmem.c?v=4.7, line 126定義)是一容量爲LAST_PKMAP的整數數組, 其中每一個元素都對應於一個持久映射頁。它其實是被映射頁的一個使用計數器,語義不太常見.

內核能夠經過get_next_pkmap_nr獲取到pkmap_count數組中元素的個數, 該函數定義在mm/highmem.c?v=4.7, line 66

/*
 * Get next index for mapping inside PKMAP region for page with given color.
 */
static inline unsigned int get_next_pkmap_nr(unsigned int color)
{
    static unsigned int last_pkmap_nr;

    last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
    return last_pkmap_nr;
}

爲了記錄高端內存頁框與永久內核映射包含的線性地址之間的聯繫,內核使用了page_address_htable散列表.

該表包含一個page_address_map數據結構,用於爲高端內存中的每個頁框進行當前映射。而該數據結構還包含一個指向頁描述符的指針和分配給該頁框的線性地址。

/*
 * Describes one page->virtual association
 */
struct page_address_map
{
    struct page *page;
    void *virtual;
    struct list_head list;
};

該結構用於創建page-->virtual的映射(該結構由此得名).

字段 描述
page 是一個指向全局mem_map數組中的page實例的指針
virtual 指定了該頁在內核虛擬地址空間中分配的位置

爲便於組織, 映射保存在散列表中, 結構中的鏈表元素用於創建溢出鏈表,以處理散列碰撞. 該散列表經過page_address_htable數組實現, 定義在mm/highmem.c?v=4.7, line 392

static struct page_address_slot *page_slot(const struct page *page)
{
    return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}

2.2 page_address函數

page_address是一個前端函數, 使用上述數據結構肯定給定page實例的線性地址, 該函數定義在mm/highmem.c?v=4.7, line 408)

/**
 * page_address - get the mapped virtual address of a page
 * @page: &struct page to get the virtual address of
 *
 * Returns the page's virtual address.
 */
void *page_address(const struct page *page)
{
    unsigned long flags;
    void *ret;
    struct page_address_slot *pas;
    /*若是頁框不在高端內存中*/  
    if (!PageHighMem(page))
         /*線性地址老是存在,經過計算頁框下標 
            而後將其轉換成物理地址,最後根據相應的 
            /物理地址獲得線性地址*/
        return lowmem_page_address(page);
    /*從page_address_htable散列表中獲得pas*/  
    pas = page_slot(page);
    ret = NULL;
    spin_lock_irqsave(&pas->lock, flags);
    if (!list_empty(&pas->lh)) {{/*若是對應的鏈表不空, 
    該鏈表中存放的是page_address_map結構*/  
        struct page_address_map *pam;
        /*對每一個鏈表中的元素*/
        list_for_each_entry(pam, &pas->lh, list) {
            if (pam->page == page) {
                /*返回線性地址*/ 
                ret = pam->virtual;
                goto done;
            }
        }
    }
done:
    spin_unlock_irqrestore(&pas->lock, flags);
    return ret;
}

EXPORT_SYMBOL(page_address);

page_address首先檢查傳遞進來的page實例在普通內存仍是在高端內存.

  • 若是是前者(普通內存區域), 頁地址能夠根據page在mem_map數組中的位置計算. 這個工做能夠經過lowmem_page_address調用page_to_virt(page)來完成
  • 對於後者, 可經過上述散列表查找虛擬地址.

2.3 kmap建立映射

2.3.1 kmap函數

爲經過page指針創建映射, 必須使用kmap函數.

不一樣體系結構的定義可能不一樣, 可是大多數體系結構的定義都以下所示, 好比arm上該函數定義在arch/arm/mm/highmem.c?v=4.7, line 37, 以下所示

/*高端內存映射,運用數組進行操做分配狀況 
分配好後須要加入哈希表中;*/  
void *kmap(struct page *page)
{
    might_sleep();
    if (!PageHighMem(page)) /*若是頁框不屬於高端內存*/  
        return page_address(page);
    return kmap_high(page); /*頁框確實屬於高端內存*/  
}
EXPORT_SYMBOL(kmap);

kmap函數只是一個page_address的前端,用於確認指定的頁是否確實在高端內存域中. 不然, 結果返回page_address獲得的地址. 若是確實在高端內存中, 則內核將工做委託給kmap_high

kmap_high的實如今函數mm/highmem.c?v=4.7, line 275中, 定義以下

2.3.2 kmap_high函數

/**
 * kmap_high - map a highmem page into memory
 * @page: &struct page to map
 *
 * Returns the page's virtual memory address.
 *
 * We cannot call this from interrupts, as it may block.
 */
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) )/*  若是沒有被映射  */    
        /*把頁框的物理地址插入到pkmap_page_table的 
        一個項中並在page_address_htable散列表中加入一個 
        元素*/  
        vaddr = map_new_virtual(page);
    /*分配計數加一,此時流程都正確應該是2了*/  
    pkmap_count[PKMAP_NR(vaddr)]++;
    BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
    unlock_kmap();
    return (void*) vaddr;   ;/*返回地址*/ 
}

EXPORT_SYMBOL(kmap_high);

2.3.3 map_new_virtual函數

上文討論的page_address函數首先檢查該頁是否已經映射. 若是它不對應到有效地址, 則必須使用map_new_virtual映射該頁.

該函數定義在mm/highmem.c?v=4.7, line 213, 將執行下列主要的步驟.

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);   /*加1,防止越界*/  
        /* 接下來判斷何時last_pkmap_nr等於0,等於0就表示1023(LAST_PKMAP(1024)-1)個頁表項已經被分配了 
        ,這時候就須要調用flush_all_zero_pkmaps()函數,把全部pkmap_count[] 計數爲1的頁表項在TLB裏面的entry給flush掉 
        ,並重置爲0,這就表示該頁表項又能夠用了,可能會有疑惑爲何不在把pkmap_count置爲1的時候也 
        就是解除映射的同時把TLB也flush呢? 
        我的感受有多是爲了效率的問題吧,畢竟等到不夠的時候再刷新,效率要好點吧。*/  
        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.*/  
    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,1不是表示不可用嗎, 
    既然映射已經創建好了,應該賦值爲2呀,其實這個操做 
    是在他的上層函數kmap_high裏面完成的(pkmap_count[PKMAP_NR(vaddr)]++).*/  
    pkmap_count[last_pkmap_nr] = 1;
    /*到此爲止,整個映射就完成了,再把page和對應的線性地址 
    加入到page_address_htable哈希鏈表裏面就能夠了*/  
    set_page_address(page, (void *)vaddr);

    return vaddr;
}
  1. 從最後使用的位置(保存在全局變量last_pkmap_nr中)開始,反向掃描pkmap_count數組, 直至找到一個空閒位置. 若是沒有空閒位置,該函數進入睡眠狀態,直至內核的另外一部分執行解除映射操做騰出空位. 在到達pkmap_count的最大索引值時, 搜索從位置0開始. 在這種狀況下, 還調用 flush_all_zero_pkmaps函數刷出CPU高速緩存(讀者稍後會看到這一點)。

  2. 修改內核的頁表,將該頁映射在指定位置。但還沒有更新TLB.

  3. 新位置的使用計數器設置爲1。如上所述,這意味着該頁已分配但沒法使用,由於TLB項未更新.

  4. set_page_address將該頁添加到持久內核映射的數據結構。 該函數返回新映射頁的虛擬地址. 在不須要高端內存頁的體系結構上(或沒有設置CONFIG_HIGHMEM),則使用通用版本的kmap返回頁的地址,且不修改虛擬內存

2.4 kunmap解除映射

用kmap映射的頁, 若是再也不須要, 必須用kunmap解除映射. 照例, 該函數首先檢查相關的頁(由page實例標識)是否確實在高端內存中. 假若如此, 則實際工做委託給mm/highmem.c中的kunmap_high, 該函數的主要任務是將pkmap_count數組中對應位置在計數器減1

該機制永遠不能將計數器值下降到小於1. 這意味着相關的頁沒有釋放。由於對使用計數器進行了額外的加1操做, 正如前文的討論, 這是爲確保CPU高速緩存的正確處理.

也在上文提到的flush_all_zero_pkmaps是最終釋放映射的關鍵. 在map_new_virtual從頭開始搜索空閒位置時, 老是調用該函數.

它負責如下3個操做。

  1. flush_cache_kmaps在內核映射上執行刷出(在須要顯式刷出的大多數體系結構上,將使用flush_cache_all刷出CPU的所有的高速緩存), 由於內核的全局頁表已經修改.

  2. 掃描整個pkmap_count數組. 計數器值爲1的項設置爲0,從頁表刪除相關的項, 最後刪除該映射。
  3. 最後, 使用flush_tlb_kernel_range函數刷出全部與PKMAP區域相關的TLB項.

2.4.1 kunmap函數

同kmap相似, 每一個體繫結構都應該實現本身的kmap函數, 大多數體系結構的定義都以下所示, 參見arch/arm/mm/highmem.c?v=4.7, line 46

void kunmap(struct page *page)
{
    BUG_ON(in_interrupt());
    if (!PageHighMem(page))
        return;
    kunmap_high(page);
}
EXPORT_SYMBOL(kunmap);

內核首先檢查待釋放內存區域是否是在高端內存區域

  • 若是內存區域在普通內存區, 則內核並無經過kmap_high對其創建持久的內核映射, 固然也無需用kunmap_high釋放
  • 若是內存區域在高端內存區, 則內核經過kunmap_high釋放該內存空間

2.4.2 kunmap_high函數

kunmap_high函數定義在mm/highmem.c?v=4.7, line 328

#ifdef CONFIG_HIGHMEM
/**
 * kunmap_high - unmap a highmem page into memory
 * @page: &struct page to unmap
 *
 * If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called
 * only from user context.
 */
void kunmap_high(struct page *page)
{
    unsigned long vaddr;
    unsigned long nr;
    unsigned long flags;
    int need_wakeup;
    unsigned int color = get_pkmap_color(page);
    wait_queue_head_t *pkmap_map_wait;

    lock_kmap_any(flags);
    vaddr = (unsigned long)page_address(page);
    BUG_ON(!vaddr);
    nr = PKMAP_NR(vaddr);   /*永久內存區域開始的第幾個頁面*/  

    /*
     * A count must never go down to zero
     * without a TLB flush!
     */
    need_wakeup = 0;
    switch (--pkmap_count[nr]) {    /*減少這個值,由於在映射的時候對其進行了加2*/  
    case 0:
        BUG();
    case 1:
        /*
         * Avoid an unnecessary wake_up() function call.
         * The common case is pkmap_count[] == 1, but
         * no waiters.
         * The tasks queued in the wait-queue are guarded
         * by both the lock in the wait-queue-head and by
         * the kmap_lock.  As the kmap_lock is held here,
         * no need for the wait-queue-head's lock.  Simply
         * test if the queue is empty.
         */
        pkmap_map_wait = get_pkmap_wait_queue_head(color);
        need_wakeup = waitqueue_active(pkmap_map_wait);
    }
    unlock_kmap_any(flags);

    /* do wake-up, if needed, race-free outside of the spin lock */
    if (need_wakeup)
        wake_up(pkmap_map_wait);
}

EXPORT_SYMBOL(kunmap_high);
#endif

3 臨時內核映射

剛纔描述的kmap函數不能用於中斷處理程序, 由於它可能進入睡眠狀態. 若是pkmap數組中沒有空閒位置, 該函數會進入睡眠狀態, 直至情形有所改善.

void *kmap_atomic(struct page *page)
{
    unsigned int idx;
    unsigned long vaddr;
    void *kmap;
    int type;

    preempt_disable();
    pagefault_disable();
    if (!PageHighMem(page))
        return page_address(page);

#ifdef CONFIG_DEBUG_HIGHMEM
    /*
     * There is no cache coherency issue when non VIVT, so force the
     * dedicated kmap usage for better debugging purposes in that case.
     */
    if (!cache_is_vivt())
        kmap = NULL;
    else
#endif
        kmap = kmap_high_get(page);
    if (kmap)
        return kmap;

    type = kmap_atomic_idx_push();

    idx = FIX_KMAP_BEGIN + type + KM_TYPE_NR * smp_processor_id();
    vaddr = __fix_to_virt(idx);
#ifdef CONFIG_DEBUG_HIGHMEM
    /*
     * With debugging enabled, kunmap_atomic forces that entry to 0.
     * Make sure it was indeed properly unmapped.
     */
    BUG_ON(!pte_none(get_fixmap_pte(vaddr)));
#endif
    /*
     * When debugging is off, kunmap_atomic leaves the previous mapping
     * in place, so the contained TLB flush ensures the TLB is updated
     * with the new mapping.
     */
    set_fixmap_pte(idx, mk_pte(page, kmap_prot));

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

這個函數不會被阻塞, 所以能夠用在中斷上下文和起亞不能從新調度的地方. 它也禁止內核搶佔, 這是有必要的, 所以映射對每一個處理器都是惟一的(調度可能對哪一個處理器執行哪一個進程作變更).

3.2 kunmap_atomic函數

能夠經過函數kunmap_atomic取消映射

/*
 * Prevent people trying to call kunmap_atomic() as if it were kunmap()
 * kunmap_atomic() should get the return value of kmap_atomic, not the page.
 */
#define kunmap_atomic(addr)                     \
do {                                \
    BUILD_BUG_ON(__same_type((addr), struct page *));       \
    __kunmap_atomic(addr);                  \
} while (0)

這個函數也不會阻塞. 在不少體系結構中, 除非激活了內核搶佔, 不然kunmap_atomic根本無事可作, 由於只有在下一個臨時映射到來前上一個臨時映射纔有效. 所以, 內核徹底能夠」忘掉」kmap_atomic映射, kunmap_atomic也無需作什麼實際的事情. 下一個原子映射將自動覆蓋前一個映射.

相關文章
相關標籤/搜索