1. 程式人生 > >高階記憶體對映之kmap持久核心對映--Linux記憶體管理(二十)

高階記憶體對映之kmap持久核心對映--Linux記憶體管理(二十)

1 高階記憶體與核心對映

儘管vmalloc函式族可用於從高階記憶體域向核心對映頁幀(這些在核心空間中通常是無法直接看到的), 但這並不是這些函式的實際用途.

重要的是強調以下事實 : 核心提供了其他函式用於將ZONE_HIGHMEM頁幀顯式對映到核心空間, 這些函式與vmalloc機制無關. 因此, 這就造成了混亂.

而在高階記憶體的頁不能永久地對映到核心地址空間. 因此, 通過alloc_pages()函式以__GFP_HIGHMEM標誌獲得的記憶體頁就不可能有邏輯地址.

在x86_32體系結構總, 高於896MB的所有實體記憶體的範圍大都是高階記憶體, 它並不會永久地或自動對映到核心地址空間, 儘管X86處理器能夠定址物理RAM的範圍達到4GB(啟用PAE可以定址64GB), 一旦這些頁被分配, 就必須對映到核心的邏輯地址空間上. 在x86_32上, 高階地址的頁被對映到核心地址空間(即虛擬地址空間的3GB~4GB)

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

該部分有3個用途。

  1. 虛擬記憶體中連續、但實體記憶體中不連續的記憶體區,可以在vmalloc區域分配. 該機制通常用於使用者過程, 核心自身會試圖盡力避免非連續的實體地址。核心通常會成功,因為大部分大的記憶體塊都在啟動時分配給核心,那時記憶體的碎片尚不嚴重。但在已經運行了很長時間的系統上, 在核心需要實體記憶體時, 就可能出現可用空間不連續的情況. 此類情況, 主要出現在動態載入模組時.

  2. 持久對映用於將高階記憶體域中的非持久頁對映到核心中

  3. 固定對映是與實體地址空間中的固定頁關聯的虛擬地址空間項,但具體關聯的頁幀可以自由選擇. 它與通過固定公式與實體記憶體關聯的直接對映頁相反,虛擬固定對映地址與實體記憶體位置之間的關聯可以自行定義,關聯建立後核心總是會注意到的.

在這裡有兩個前處理器符號很重要 __VMALLOC_RESERVE設定了vmalloc區域的長度, 而MAXMEM則表示核心可以直接定址的實體記憶體的最大可能數量.

核心中, 將記憶體劃分為各個區域是通過圖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也無需做什麼實際的事情. 下一個原子對映將自動覆蓋前一個對映.