Linux設備驅動之記憶體管理

来源:http://www.cnblogs.com/archiexie/archive/2016/12/09/6132440.html
-Advertisement-
Play Games

對於包含 MMU 的處理器而言, Linux 系統提供了複雜的存儲管理系統,使得進程所能訪問的記憶體達到 4GB。進程的 4GB 記憶體空間被分為兩個部分—用戶空間與內核空間。用戶空間地址一般分佈為 0~3GB(即 PAGE_OFFSET),這樣,剩下的 3~4GB 為內核空間。內核空間申請記憶體涉及的函... ...


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


References

1. Linux Device Drivers
2. Linux設備驅動開發詳解(宋寶華第二版)


Copyright (C) 2016 archiexie@cnblogs. All Rights Reserved.



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 實驗內容: 實驗分析: 在本次資料庫實驗中,我完成了實驗要求。本次實驗內容是關於sql語言進行用戶許可權的授予和回收,實體完整性,參照完整性及用戶定義的完整性的定義。在課堂上,老師講授了oracle的安全性和完整性控制相關知識,我也用筆練習寫了sql語句,但是感覺印象還不是很深刻,有些不太理解。在實驗 ...
  • 實驗分析: 在本次資料庫實驗中,我完成了實驗要求。本次實驗內容是關於嵌套查詢,在課堂上,老師講授了嵌套查詢相關知識,我也用筆練習寫了sql語句,但是感覺印象還不是很深刻,有些不太理解。在實驗課中我練習了sql語句,對課堂上所學的知識有了更深的理解,收穫很多。實驗中,我遇到了一些問題,通過查詢資料和老 ...
  • 一.環境說明 虛擬機:vmware 11 操作系統:Ubuntu 16.04 Hadoop版本:2.7.2 Zookeeper版本:3.4.9 二.節點部署說明 三.Hosts增加配置 sudo gedit /etc/hosts wxzz-pc、wxzz-pc0、wxzz-pc1、wxzz-pc2均 ...
  • mybatis-generator-gui是什麼 介紹mybatis-generator-gui之前,有必要介紹一下什麼是mybatis generator(熟悉的同學可以跳過這一節).我們都知道,通常編寫Mybatis應用程式,需要寫sqlmap、實體類、Dao介面和Dao實現類,需要對於一個成百 ...
  • 問題描述: 附加數據時,提示無法打開物理文件,操作系統錯誤5。如下圖: 問題原因:可能是文件訪問許可權方面的問題。 解決方案:找到資料庫的mdf和ldf文件,賦予許可權即可。如下圖: 找到mdf和ldf文件,本演示以ldf為例。 1.點擊文件右鍵屬性-->安全-->編輯 2.編輯-->添加 3.添加-- ...
  • 1、MDK、Keil C51 編譯後數據 Code:程式大小 Flash RO-data:常量 Flash RW-data:(已初始化的)可讀可寫變數 Flash RAM ZI-data:未初始化的變數 RAM ...
  • 1、上電,短接ERASE,>10秒後,拔USB。 2、短接TST,上電,>10秒後,拔USB。 3、安裝驅動。(看別人教程,下載到INF文件,WIN7不能右擊安裝,好,換虛擬機XP) 4、成功識別,但ISP居然不能下載,write Flash為灰色。 卡了N久 百度,別人write Flash 灰色 ...
  • 基於STM32F1開發平臺,從頭開始完成一個基於時間片輪詢和優先順序搶占的實時任務調度內核 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...