1. 程式人生 > >Linux記憶體管理:HighMemory

Linux記憶體管理:HighMemory

HighMemory介紹

Linux一般把整個4GB可以map的記憶體中的1GB用於低端記憶體。從0xC0000000開始的話(CONFIG_PAGE_OFFSET配置),低端記憶體的地址範圍就是0xC0000000到high_memory地址。
high_memory = __va(arm_lowmem_limit - 1) + 1,arm_lowmem_limit也是0xff00000減去vmalloc大小什麼的算出來的,和vmalloc_min一樣。所以可以直接map的lowmemory小於1GB。如果vmalloc區域等於340MB的話,大小一般也就600多MB。

high_memory = __va(arm_lowmem_limit - 1
) + 1 static void * __initdata vmalloc_min = (void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET); #define VMALLOC_OFFSET (8*1024*1024) #define VMALLOC_START (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1)) #define VMALLOC_END 0xff000000UL

high_memory儲存了高階記憶體的開始地址。
這裡寫圖片描述

那如果用大於1GB記憶體怎麼辦呢?這時候如果不想把大於1GB的記憶體浪費掉,就需要定義CONFIG_HIGHMEM。
但Highmemory區域是不會在核心初始化的時候,直接map到記憶體可以訪問的。
核心採用三種不同的機制將高階記憶體的頁框對映過來。分別叫做臨時核心對映,永久核心對映以及非連續記憶體分配(vmalloc)。

永久核心對映

http://blog.csdn.net/xiaojsj111/article/details/11817587
kmap()函式建立永久核心對映。
這種對映的建立,可能會發生阻塞,不適用於中斷上下文(中斷處理程式及可延遲函式)
函式kmap()被用來建立永久核心對映,如下所示,該程式碼的核心在函式kunmap_high()中,所用到的資料結構有

  • 雜湊陣列page_address_htable,陣列每個元素都是一個連結串列,連結串列的每個節點中都存放了一個頁描述符指標及相應的虛擬地址
  • pte_t * pkmap_page_table,主核心頁表中專門用於永久核心對映的頁表項陣列,其大小由LAST_PKMAP指出,如果不使用擴充套件實體記憶體,其值為1024,正好是佔一頁頁表。
  • int pkmap_count[LAST_PKMAP],與每個用於永久核心對映的頁表項是一對一的關係
    • 0,對應頁表項沒有對映任何高階記憶體,並且是可用的
    • 1,對應頁表項沒有對映任何高階記憶體,但不可用,因最後一次被使用後,相應的TLB表還沒有被重新整理
    • 大於1,n-1個核心成分在使用對應的頁表項
  • PKMAP_BASE,陣列pkmap_page_table中第一個頁表項所對應的虛擬地址

函式kunmap_high()呼叫了函式page_address(),對該頁描述符所代表的高階記憶體,在雜湊陣列page_address_htable中,查詢其相應的虛擬地址,找不到,則返回NULL,然後呼叫函式map_new_virtual()。

函式map_new_virtual()全部程式碼如下,其使用陣列pkmap_count查詢空閒可用的頁表項,找到後,設定該頁表項指向函式void *kmap(struct page *page)的實參所引用的頁框,同時,在雜湊陣列page_address_htable的某個元素連結串列上新增節點,以記錄該頁描述符所對應的虛擬地址。另外,要注意的是,找不到空閒可用的頁表項時,會睡眠等待。

這裡寫圖片描述

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

PKMAP_BASE,PKMAP_SIZE這兩個定義了PKMAP區域的開始地址和大小

在Highmemory區域裡分配的page,需要map一下核心才能訪問。以下看一下幾個相關的介面函式。

//這個函式檢查當前page是否是lowmemory,如果是的話就呼叫page_address()把當前page的地址轉成虛擬地址返回。但如果是高階記憶體的page,則需要用到PKMAP區域重新map一下,才能讓核心進行訪問。這樣map的page釋放的時候必須使用kumap來釋放。由於這種map用到的PKMAP區域大小有限,建議不要佔據這種記憶體太長時間(ldd3書裡邊寫)?
void *kmap(struct page *page)
{
    might_sleep();
    //當前記憶體可能會進入睡眠,如果在atomic上下文呼叫kmap函式,
    //migh_sleep函式會列印stack trace
    //這個函式需要使能CONFIG_DEBUG_ATOMIC_SLEEP

    if (!PageHighMem(page))
        return page_address(page);
    return kmap_high(page);
}

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;
}

//如果是highmemory的page,則會呼叫如下函式
static inline unsigned long map_new_virtual(struct page *page)
{
    unsigned long vaddr;
    int count;

start:
    count = LAST_PKMAP;
    /* Find an empty entry */
    for (;;) {
        //last_pkmap_nr儲存上一次map過的值,從這個值開始尋找。如果二級頁表是512的話,最大隻能到512
        //所以與了一個LAST_PKMAP_MASK。
        //如果二級頁表是512的話,就可以猜到pkmap區域的大小應該至少是2MB,
        //http://blog.csdn.net/hongzg1982/article/details/47341881 

        //pkmap_count[]標誌總共512個裡邊,哪個是可以map的。 
        last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
        if (!last_pkmap_nr) {
            flush_all_zero_pkmaps();
            count = LAST_PKMAP;
        }
        if (!pkmap_count[last_pkmap_nr]) 
            break;  /* Found a usable entry */
        if (--count)
            continue;

        /*
         * Sleep for somebody else to unmap their entries
         */
        { //如果找不到剩餘的,則先進入睡眠,等有kumap的時候再醒來繼續尋找。
          //這裡用的waitqueue的方式,也可以看一下 
            DECLARE_WAITQUEUE(wait, current);

            __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);
    //highmemory的虛擬地址是PKMAP_BASE +last_pkmap_nr<<PAGE_SHIFT,所以很容從虛擬地址看出來當前頁是否是highmemory。因為最多就可以只map 512個page,也就是2MB大小的highmemory,所以最好不要使用太長時間,導致highmemory分配失敗!!這個也是上面說過的

    //下面的函式就簡單配置一下頁表就可以了
    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;
}

kmap()可以看到,在分配不到的時候,可能會進入睡眠。
那還有一種kmap_atomic()介面是比kmap更為高效且不進入睡眠的,可以在atomic上下文進行呼叫的。
下面看一下其實現:

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

    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 = type + KM_TYPE_NR * smp_processor_id();
    vaddr = __fix_to_virt(FIX_KMAP_BEGIN + 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_top_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_top_pte(vaddr, mk_pte(page, kmap_prot));

    return (void *)vaddr;
}

臨時核心對映

臨時核心對映比永久核心對映的實現要簡單;此外它可以在中斷處理程式和可延遲函式的內部,因為這個臨時核心對映函式從來不阻塞當前程序。為了建立臨時核心對映,核心呼叫kmap_atomic()函式。
核心在 FIXADDR_START 到 FIXADDR_TOP 之間保留了一些線性空間用於特殊需求。這個空間稱為“固定對映空間”
在這個空間中,有一部分用於高階記憶體的臨時對映。

分配page或者free page的時候會判斷是不是highmemory。

highmemory的大小:
memblock或者meminfo()裡邊,highmemory的最大地址減去arm_lowmem_limit的地址之後算出來的大小就是highmemory的大小。比如像下面log這樣,最後一段count = 3的區域為highmemory區域。其size為768MB,但highmemory開始地址小於arm_lowmem_limit,所以減去arm_lowmem_limit - reg->base的虛擬地址(18MB),highmemory的大小就是750MB。這個大小與/proc/meminfo裡邊讀出來的HighTotal的大小是一致的。

<5>[0.000000]  [0:swapper:0] arm_lowmem_limit = 0xf1200000 
<6>[0.000000]  [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x5500000
<6>[0.000000]  [0:swapper:0] count = 2 , reg->base =0x8cb00000 , reg->size =0x23200000
<6>[0.000000]  [0:swapper:0] count = 3 , reg->base =0xb0000000 , reg->size =0x30000000

然後再arm_bootmem_free裡邊

#ifdef CONFIG_HIGHMEM
    zone_size[ZONE_HIGHMEM] = max_high - max_low;//可以在find_limits函式裡邊找到max_high和max_low的定義
#endif
//max_high就是high memory最大地址對應的pfn,max_low是lowmemory的最大地址對應的pfn

使用者空間對映:
雖然核心地址空間有限,但是每個程序的使用者地址空間都可以達到3G,Highmem的頁框可以不受限制的對映到使用者線性地址空間。當訪問使用者地址空間地址發生缺頁異常時,核心的page allocator會優先從highmem zone分配頁面,只有當highmem zone沒有足夠的空閒頁面時,才會選擇Normal或者DMA zone進行分配。

因此highmem記憶體的主要使用者是應用程序的頁面對映,核心kernel通過pkmap fixmap方式,同時使用的Highmem記憶體,理論上最多2MB/4MB + 3.xMB;由於Highmem的存在,使得應用地址空間缺頁異常處理,檔案對映,堆分配等操作優先使用highmem zone的記憶體,減輕了Normal zone的分配壓力,某種程度上避免了Normal區的碎片化。我們甚至可以禁止使用者空間地址的HIGH_MEM分配使用Normal zone和 DMA zone,使得Normal DMA只用於核心地址空間記憶體的分配,儘量減少碎片化,避免記憶體分配失敗。我想這就是HighMem存在的意義吧。

非連續記憶體區管理 vmalloc

如果一段記憶體不是很頻繁訪問,那麼通過連續的線性地址來訪問非連續的頁框這樣一種分配模式就會有很大的意義。這種模式的優點是避免了外碎片(??),而缺點是必須重新建立頁表。
顯然通過vmalloc分配的記憶體大小必須是4096位元組,也就是page大小的倍數!!!

如下圖可以看到vmalloc開始和結束分別與high_memory與PKMAP_BASE大小相關,但一般都會插入一個8MB的區域,目的是為了”捕獲”對記憶體的越界訪問。
出於同樣理由,vmalloc區域之間也會留下4kB的安全區來隔離非連續的記憶體區
這裡寫圖片描述

這種方式很簡單,因為通過 vmalloc() ,在”核心動態對映空間”(上圖的VMALLOC_START到VMALLOC_END)申請記憶體的時候,就可能從高階記憶體獲得頁面(參看 vmalloc 的實現),因此說高階記憶體有可能對映到”核心動態對映空間” 中。
vmalloc區域的起始線性地址是VMALLOC_START,結束線性地址是VMALLOC_END。每個vmalloc記憶體區都對應著一個vm_struct資料結構。

struct vm_struct {
    struct vm_struct    *next;
    void            *addr;
    unsigned long       size;
    unsigned long       flags;
    struct page     **pages;
    unsigned int        nr_pages;
    phys_addr_t     phys_addr;
    const void      *caller;
};

 - addr,第一個記憶體單元的線性地址
 - size,記憶體區的大小加4096的安全區
 - pages,這個陣列中存放的是被對映的物理頁的頁框描述符
 - nr_pages,陣列pages的維數
 - phy_addr,除非記憶體被用來對映一個硬體裝置的IO共享記憶體,否則為0
 - next,所有的非連續記憶體區的vm_struct描述符都通過該欄位連結在全域性變數vmlist中
 - flags,
     - VM_ALLOC,使用vmalloc()分配的非連續記憶體區
     - VM_MAP,使用vmap()對映已經分配好的頁框
     - VM_IOREMAP,使用ioremap()對映的硬體裝置上的記憶體

使用vmalloc(size)分配非連續的記憶體區,該函式的核心是__vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL),下面介紹該函式

1) 使用kmalloc(sizeof(vm_struct), GFP_KERNEL),根據vm_struct資料結構的大小,從幾何分佈的
slab中為vm_struct分配記憶體,並在線性地址從VMALLOC_START到VMALLOC_END之間尋找一塊空閒區域,大小至
少為size+4096,找到後,用該段線性地址的起始值初始化vm_struct->addr,並初始化vm_struct->flags,且將vm_struct->size記為size+4096

2)  初始化vm_struct->nr_pages為(size >> PAGE_SHIFT),然後,為頁描述符指標陣列
vm_struct->pages分配空間,需要分配的大小是
(vm_struct->nr_pages*sizeof(struct page *)),若陣列佔用記憶體大於一頁,則使用__vmalloc(array_size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL)分配,否則,使用
kmalloc(array_size, (GFP_KERNEL | __GFP_HIGHMEM& ~__GFP_HIGHMEM))分配。這
裡需注意的是,所謂一頁的安全區只是指線性地址需要,實際的頁框中並沒有

3) 使用函式alloc_page(GFP_KERNEL | __GFP_HIGHMEM)(單個頁框分配函式)分配
vm_struct->nr_pages個頁框,將頁描述符記錄在陣列vm_struct->pages中

4)接下來,就是要修改核心頁表,以表明非連續記憶體區的每個頁框都對應著一個線性地址,使用的
函式是map_vm_area(area, PAGE_KERNEL,&vm_struct->pages),該函式先是通過
pgd_offset(&init_mm, vm_struct->addr)得到主核心頁全域性目錄pgd,然後,為每段4KB大小的線性地址(除卻最後4KB用作安全區的的線性地址),
分配所需的各級頁中間目錄項,並最終建立頁表項,並將每個頁描述符對應的頁的實體地址,
連同PAGE_KERNEL標誌設定到相應的頁表項中。注意,PAGE_KERNEL等同於(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_NX)

需要注意的是,vmalloc(size)並未觸及當前程序的頁表,因此,核心態的程序訪問非連續記憶體區時,由於在程序頁表中找不到對應的表項,所以,缺頁異常發生,然後,缺頁處理程式發現這個缺頁線性地址在主核心頁表中,所以,就把主核心頁表中相應的值拷貝到程序的頁表中,最後恢復程序的執行,詳見 缺頁異常??????
使用vfree(void *addr)來釋放非連續的記憶體區,該函式

- 呼叫remove_vm_area(void *addr),根據線性地址addr在vmlist中找到vm_struct,並清除該非連續記憶體區中的線性地址對應的所有的核心的頁表項,注意,這裡只清楚了頁表項,不清楚各級頁中間目錄項,因為核心永遠也不會回收紮根於主核心頁全域性目錄中的頁上級目錄、頁中間目錄和頁表。
- 呼叫函式__free_page(),將vm_struct->pages陣列中的每個頁歸還到頁框分配器,然後,呼叫vfree(area->pages)(該陣列佔用空間大於4096KB時)或kfree(vm_struct->pages)釋放這個陣列本身
- 呼叫kfree(vm_struct),釋放資料結構vm_struct