首頁 > 軟體

Linux裝置驅動之記憶體管理

2020-06-16 17:26:29

對於包含 MMU 的處理器而言, Linux 系統提供了複雜的儲存管理系統,使得進程所能存取的記憶體達到 4GB。進程的 4GB 記憶體空間被分為兩個部分—使用者空間與核心空間。使用者空間地址一般分布為 0~3GB(即 PAGE_OFFSET),這樣,剩下的 3~4GB 為核心空間。
核心空間申請記憶體涉及的函數主要包括 kmalloc()、__get_free_pages()和 vmalloc()等。
通過記憶體對映,使用者進程可以在使用者空間直接存取裝置。


核心地址空間

每個進程的使用者空間都是完全獨立、互不相干的,使用者進程各自有不同的頁表。而核心空間是由核心負責對映,它並不會跟著進程改變,是固定的。核心空間地址有自己對應的頁表,核心的虛擬空間獨立於其他程式。使用者進程只有通過系統呼叫(代表使用者進程在核心態執行)等方式才可以存取到核心空間。

Linux 中 1GB 的核心地址空間又被劃分為實體記憶體對映區、虛擬記憶體分配區、高階頁面對映區、專用頁面對映區和系統保留對映區這幾個區域,如圖所示。

  • 保留區

    Linux 保留核心空間最頂部 FIXADDR_TOP~4GB 的區域作為保留區。
  • 專用頁面對映區

    緊接著最頂端的保留區以下的一段區域為專用頁面對映區(FIXADDR_START~FIXADDR_TOP),它的總尺寸和每一頁的用途由 fixed_address 列舉結構在編譯時預定義,用__fix_to_virt(index)可獲取專用區內預定義頁面的邏輯地址。
  • 高階記憶體對映區

    當系統實體記憶體大於 896MB 時,超過實體記憶體對映區的那部分記憶體稱為高階記憶體(而未超過實體記憶體對映區的記憶體通常被稱為常規記憶體),核心在存取高階記憶體時必須將它們對映到高階頁面對映區。
  • 虛存記憶體分配區

    用於 vmalloc()函數,它的前部與實體記憶體對映區有一個隔離帶,後部與高階對映區也有一個隔離帶。
  • 實體記憶體對映區

    一般情況下,實體記憶體對映區最大長度為 896MB,系統的實體記憶體被順序對映在核心空間的這個區域中。

虛擬地址與實體地址關係

對於核心實體記憶體對映區的虛擬記憶體,使用 virt_to_phys()可以實現核心虛擬地址轉化為實體地址, virt_to_phys()的實現是體系結構相關的,對於 ARM 而言, virt_to_phys()的定義如程式碼:

    static inline unsigned long virt_to_phys(void *x)
    {
        return __virt_to_phys((unsigned long)(x));
    }

    /* PAGE_OFFSET 通常為 3GB,而 PHYS_OFFSET 則定於為系統 DRAM 記憶體的基地址 */
    #define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)

記憶體分配

在 Linux 核心空間申請記憶體涉及的函數主要包括 kmalloc()、__get_free_pages()和 vmalloc()等。kmalloc()和__get_free_pages()( 及其類似函數) 申請的記憶體位於實體記憶體對映區域,而且在物理上也是連續的,它們與真實的實體地址只有一個固定的偏移,因此存在較簡單的轉換關係。而vmalloc()在虛擬記憶體空間給出一塊連續的記憶體區,實質上,這片連續的虛擬記憶體在實體記憶體中並不一定連續,而 vmalloc()申請的虛擬記憶體和實體記憶體之間也沒有簡單的換算關係。

kmalloc()

    void *kmalloc(size_t size, int flags);

給 kmalloc()的第一個引數是要分配的塊的大小,第二個引數為分配標誌,用於控制 kmalloc()的行為。

flags

  • 最常用的分配標誌是 GFP_KERNEL,其含義是在核心空間的進程中申請記憶體。 kmalloc()的底層依賴__get_free_pages()實現,分配標誌的字首 GFP 正好是這個底層函數的縮寫。使用 GFP_KERNEL 標誌申請記憶體時,若暫時不能滿足,則進程會睡眠等待頁,即會引起阻塞,因此不能在中斷上下文或持有自旋鎖的時候使用 GFP_KERNEL 申請記憶體。
  • 在中斷處理常式、 tasklet 和核心定時器等非進程上下文中不能阻塞,此時驅動應當使用GFP_ATOMIC 標誌來申請記憶體。當使用 GFP_ATOMIC 標誌申請記憶體時,若不存在空閒頁,則不等待,直接返回。
  • 其他的相對不常用的申請標誌還包括 GFP_USER(用來為使用者空間頁分配記憶體,可能阻塞)、GFP_HIGHUSER(類似 GFP_USER,但是從高階記憶體分配)、 GFP_NOIO(不允許任何 I/O 初始化)、 GFP_NOFS(不允許進行任何檔案系統呼叫)、 __GFP_DMA(要求分配在能夠 DMA 的記憶體區)、 __GFP_HIGHMEM(指示分配的記憶體可以位於高階記憶體)、 __GFP_COLD(請求一個較長時間不存取的頁)、 __GFP_NOWARN(當一個分配無法滿足時,阻止核心發出警告)、 __GFP_HIGH(高優先順序請求,允許獲得被核心保留給緊急狀況使用的最後的記憶體頁)、 __GFP_REPEAT(分配失敗則盡力重複嘗試)、 __GFP_NOFAIL(標誌只許申請成功,不推薦)和__GFP_NORETRY(若申請不到,則立即放棄)。

使用 kmalloc()申請的記憶體應使用 kfree()釋放,這個函數的用法和使用者空間的 free()類似。

__get_free_pages ()

__get_free_pages()系列函數/宏是 Linux 核心本質上最底層的用於獲取空閒記憶體的方法,因為底層的夥伴演算法以 page 的 2 的 n 次冪為單位管理空閒記憶體,所以最底層的記憶體申請總是以頁為單位的。
__get_free_pages()系列函數/宏包括 get_zeroed_page()、 __get_free_page()和__get_free_pages()。

    /* 該函數返回一個指向新頁的指標並且將該頁清零 */
    get_zeroed_page(unsigned int flags);
    /* 該宏返回一個指向新頁的指標但是該頁不清零 */
    __get_free_page(unsigned int flags);
    /* 該函數可分配多個頁並返回分配記憶體的首地址,分配的頁數為 2^order,分配的頁也不清零 */
    __get_free_pages(unsigned int flags, unsigned int order);

    /* 釋放 */
    void free_page(unsigned long addr);
    void free_pages(unsigned long addr, unsigned long order);

__get_free_pages 等函數在使用時,其申請標誌的值與 kmalloc()完全一樣,各標誌的含義也與kmalloc()完全一致,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。

vmalloc()

vmalloc()一般用在為只存在於軟體中(沒有對應的硬體意義)的較大的順序緩衝區分配記憶體,vmalloc()遠大於__get_free_pages()的開銷,為了完成 vmalloc(),新的頁表需要被建立。因此,只是呼叫 vmalloc()來分配少量的記憶體(如 1 頁)是不妥的。
vmalloc()申請的記憶體應使用 vfree()釋放, vmalloc()和 vfree()的函數原型如下:

    void *vmalloc(unsigned long size);
    void vfree(void * addr);

vmalloc()不能用在原子上下文中,因為它的內部實現使用了標誌為 GFP_KERNEL 的 kmalloc()。

slab

一方面,完全使用頁為單元申請和釋放記憶體容易導致浪費(如果要申請少量位元組也需要 1 頁);另一方面,在作業系統的運作過程中,經常會涉及大量物件的重複生成、使用和釋放記憶體問題。在Linux 系統中所用到的物件,比較典型的例子是 inode、 task_struct 等。如果我們能夠用合適的方法使得在物件前後兩次被使用時分配在同一塊記憶體或同一類記憶體空間且保留了基本的資料結構,就可以大大提高效率。 核心的確實現了這種型別的記憶體池,通常稱為後備快取記憶體(lookaside cache)。核心對快取記憶體的管理稱為slab分配器。實際上 kmalloc()即是使用 slab 機制實現的。
注意, slab 不是要代替__get_free_pages(),其在最底層仍然依賴於__get_free_pages(), slab在底層每次申請 1 頁或多頁,之後再分隔這些頁為更小的單元進行管理,從而節省了記憶體,也提高了 slab 緩衝物件的存取效率。

    #include <linux/slab.h>

    /* 建立一個新的快取記憶體物件,其中可容納任意數目大小相同的記憶體區域 */
    struct kmem_cache *kmem_cache_create(const char *name, /* 一般為將要快取記憶體的結構型別的名字 */
            size_t size, /* 每個記憶體區域的大小 */
            size_t offset, /* 第一個物件的偏移量,一般為0 */
            unsigned long flags, /* 一個位掩碼:
                                    SLAB_NO_REAP 即使記憶體緊縮也不自動收縮這塊快取,不建議使用 
                                    SLAB_HWCACHE_ALIGN 每個資料物件被對齊到一個快取行
                                    SLAB_CACHE_DMA 要求資料物件在DMA記憶體區分配
                                  */

            /* 可選引數,用於初始化新分配的物件,多用於一組物件的記憶體分配時使用 */
            void (*constructor)(void*, struct kmem_cache *, unsigned long), 
            void (*destructor)(void*, struct kmem_cache *, unsigned long)
            );

    /* 在 kmem_cache_create()建立的 slab 後備緩衝中分配一塊並返回首地址指標 */
    void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

    /* 釋放 slab 快取 */
    void kmem_cache_free(struct kmem_cache *cachep, void *objp);

    /* 收回 slab 快取,如果失敗則說明記憶體漏失 */
    int kmem_cache_destroy(struct kmem_cache *cachep);

Tip: 快取記憶體的使用統計情況可以從/proc/slabinfo獲得。

記憶體池(mempool)

核心中有些地方的記憶體分配是不允許失敗的,核心開發者建立了一種稱為記憶體池的抽象。記憶體池其實就是某種形式的高速後備快取,它試圖始終保持空閒的記憶體以便在緊急狀態下使用。mempool很容易浪費大量記憶體,應儘量避免使用。

    #include <linux/mempool.h>

    /* 建立 */
    mempool_t *mempool_create(int min_nr, /* 需要預分配物件的數目 */
            mempool_alloc_t *alloc_fn, /* 分配函數,一般直接使用核心提供的mempool_alloc_slab */
            mempool_free_t *free_fn, /* 釋放函數,一般直接使用核心提供的mempool_free_slab */
            void *pool_data); /* 傳給alloc_fn/free_fn的引數,一般為kmem_cache_create建立的cache */

    /* 分配釋放 */
    void *mempool_alloc(mempool_t *pool, int gfp_mask);
    void mempool_free(void *element, mempool_t *pool);

    /* 回收 */
    void mempool_destroy(mempool_t *pool);

記憶體對映

一般情況下,使用者空間是不可能也不應該直接存取裝置的,但是,裝置驅動程式中可實現mmap()函數,這個函數可使得使用者空間直能接存取裝置的實體地址。
這種能力對於顯示介面卡一類的裝置非常有意義,如果使用者空間可直接通過記憶體對映存取視訊記憶體的話,螢幕幀的各點的畫素將不再需要一個從使用者空間到核心空間的複製的過程。
從 file_operations 檔案操作結構體可以看出,驅動中 mmap()函數的原型如下:

    int(*mmap)(struct file *, struct vm_area_struct*);

驅動程式中 mmap()的實現機制是建立頁表,並填充 VMA 結構體中 vm_operations_struct 指標。VMA 即 vm_area_struct,用於描述一個虛擬記憶體區域:

    struct vm_area_struct {
        unsigned long vm_start; /* 開始虛擬地址 */
        unsigned long vm_end; /* 結束虛擬地址 */

        unsigned long vm_flags; /* VM_IO 設定一個記憶體對映I/O區域;
                                   VM_RESERVED 告訴記憶體管理系統不要將VMA交換出去 */

        struct vm_operations_struct *vm_ops; /* 操作 VMA 的函數集指標 */

        unsigned long vm_pgoff; /* 偏移(頁幀號) */

        void *vm_private_data;
        ...
    }

    struct vm_operations_struct {
        void(*open)(struct vm_area_struct *area); /*開啟 VMA 的函數*/
        void(*close)(struct vm_area_struct *area); /*關閉 VMA 的函數*/
        struct page *(*nopage)(struct vm_area_struct *area, unsigned long address, int *type); /*存取的頁不在記憶體時呼叫*/

        /* 當使用者存取頁前,該函數允許核心將這些頁預先裝入記憶體。驅動程式一般不必實現 */
        int(*populate)(struct vm_area_struct *area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
        ...

建立頁表的方法有兩種:使用remap_pfn_range函數一次全部建立或者通過nopage VMA方法每次建立一個頁表。

  • remap_pfn_range
    remap_pfn_range負責為一段實體地址建立新的頁表,原型如下:

    int remap_pfn_range(struct vm_area_struct *vma, /* 虛擬記憶體區域,一定範圍的頁將被對映到該區域 */
            unsigned long addr, /* 重新對映時的起始使用者虛擬地址。該函數為處於addr和addr+size之間的虛擬地址建立頁表 */
            unsigned long pfn, /* 與實體記憶體對應的頁幀號,實際上就是實體地址右移 PAGE_SHIFT 位 */
            unsigned long size, /* 被重新對映的區域大小,以位元組為單位 */
            pgprot_t prot); /* 新頁所要求的保護屬性 */

    demo:

       static int xxx_mmap(struct file *filp, struct vm_area_struct *vma)
       {
        if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot)) /* 建立頁表 */
            return - EAGAIN;
        vma->vm_ops = &xxx_remap_vm_ops; 
        xxx_vma_open(vma);
        return 0;
       }
    
    /* VMA 開啟函數 */
    void xxx_vma_open(struct vm_area_struct *vma) 
    {
        ...
        printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %lxn", vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);
    }
    /* VMA 關閉函數 */
    void xxx_vma_close(struct vm_area_struct *vma)
    {
        ...
        printk(KERN_NOTICE "xxx VMA close.n");
    }
    
    static struct vm_operations_struct xxx_remap_vm_ops = { /* VMA 操作結構體 */
        .open = xxx_vma_open,
        .close = xxx_vma_close,
        ...
    };
  • nopage
    除了 remap_pfn_range()以外,在驅動程式中實現 VMA 的 nopage()函數通常可以為裝置提供更加靈活的記憶體對映途徑。當存取的頁不在記憶體,即發生缺頁異常時, nopage()會被核心自動呼叫。

    static int xxx_mmap(struct file *filp, struct vm_area_struct *vma)
    {
        unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
        if (offset >= _ _pa(high_memory) || (filp->f_flags &O_SYNC))
            vma->vm_flags |= VM_IO;
        vma->vm_flags |= VM_RESERVED; /* 預留 */
        vma->vm_ops = &xxx_nopage_vm_ops;
        xxx_vma_open(vma);
        return 0;
    }
    
    struct page *xxx_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)
    {
        struct page *pageptr;
        unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
        unsigned long physaddr = address - vma->vm_start + offset; /* 實體地址 */
        unsigned long pageframe = physaddr >> PAGE_SHIFT; /* 頁幀號 */
        if (!pfn_valid(pageframe)) /* 頁幀號有效? */
            return NOPAGE_SIGBUS;
        pageptr = pfn_to_page(pageframe); /* 頁幀號->頁描述符 */
        get_page(pageptr); /* 獲得頁,增加頁的使用計數 */
        if (type)
            *type = VM_FAULT_MINOR;
        return pageptr; /*返回頁描述符 */
    }

    上述函數對常規記憶體進行對映, 返回一個頁描述符,可用於擴大或縮小對映的記憶體區域。

由此可見, nopage()與 remap_pfn_range()的一個較大區別在於 remap_pfn_range()一般用於裝置記憶體對映,而 nopage()還可用於 RAM 對映,其呼叫發生在缺頁異常時。

本文永久更新連結地址http://www.linuxidc.com/Linux/2016-12/138132.htm


IT145.com E-mail:sddin#qq.com