歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> Linux設備驅動之內存管理

Linux設備驅動之內存管理

日期:2017/2/28 13:46:11   编辑:Linux教程

對於包含 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 %lx\n", 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 映射,其調用發生在缺頁異常時。

Copyright © Linux教程網 All Rights Reserved