linux驅(qū)動(dòng)之內(nèi)存使用

一、前言

Linux設(shè)備驅(qū)動(dòng) 中,內(nèi)存使用 是一個(gè)逃不掉的話題。Linux內(nèi)核 的內(nèi)存管理龐大且復(fù)雜,要想理解透徹需要花費(fèi)不少的心思和時(shí)間,本文將簡單的對 Linux設(shè)備驅(qū)動(dòng) 中涉及到的部分 內(nèi)存原理及使用 做一個(gè)簡單的探討。

二、正文

2.1 mm_struct

Linux內(nèi)核 使用 內(nèi)存描述符mm_struct 來描述進(jìn)程的 用戶虛擬地址空間,其主要成員如下:

struct mm_struct {
    struct vm_area_struct *mmap;        /* list of VMAs */
    int map_count;              /* number of VMAs */
    struct rb_root mm_rb;
    unsigned long mmap_base;        /* base of mmap area */
    unsigned long task_size;
    pgd_t * pgd;
    atomic_t mm_users;
    atomic_t mm_count;
    unsigned long start_code, end_code;
    unsigned long start_data, end_data;
    unsigned long start_brk, brk, 
    unsigned long start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;
    mm_context_t context;
    ......
} 
成員 說明
mmap 虛擬內(nèi)存區(qū)域(VMA)鏈表
mm_rb 虛擬內(nèi)存區(qū)域(VMA)紅黑樹
map_count 虛擬內(nèi)存區(qū)域(VMA)數(shù)量
task_size 用戶虛擬地址空間長度
pgd 指向 頁全局目錄(第一級頁表)
mm_users 共享該 用戶虛擬地址空間進(jìn)程數(shù)量(即 線程組 包含的進(jìn)程數(shù)量)
mm_count 內(nèi)存描述符引用計(jì)數(shù)
mmap_base 內(nèi)存映射區(qū)域起始地址
start_code | end_code 代碼段起始地址 | 結(jié)束地址
start_data | end_data 數(shù)據(jù)段起始地址 | 結(jié)束地址
start_brk | brk 起始地址 | 結(jié)束地址
start_stack 起始地址
start_brk | brk 起始地址 | 結(jié)束地址
arg_start | arg_end 參數(shù)字符串起始地址 | 結(jié)束地址
env_start | env_end 環(huán)境變量起始地址 | 結(jié)束地址
context 內(nèi)存管理上下文(與處理器架構(gòu)相關(guān))

進(jìn)程描述符 中通常有 內(nèi)存描述符(struct mm_struct) 成員來描述 進(jìn)程內(nèi)存

struct task_struct {
    ......
    struct mm_struct        *mm;
    struct mm_struct        *active_mm;
    ......
};
成員 說明
mm 指向 用戶空間進(jìn)程的內(nèi)存描述符,內(nèi)核線程 沒有 用戶虛擬地址空間,其 mm
active_mm 一般情況下,active_mmmm指向同一個(gè) 內(nèi)存描述符 內(nèi)核線程active_mm 成員在沒有運(yùn)行時(shí)為 ,運(yùn)行時(shí)時(shí)指向 借用進(jìn)程的內(nèi)存描述符

網(wǎng)絡(luò)上有一張圖片可以形象的描述 mm_struct ,如下:(出處來自文末的參考鏈接)

mm_struct()

  • 進(jìn)程描述符mmactive_mm 的區(qū)別:
    如果當(dāng) 用戶進(jìn)程 在運(yùn)行時(shí) 發(fā)生 系統(tǒng)調(diào)用,程序會(huì)從 用戶態(tài) 進(jìn)入 內(nèi)核態(tài)。內(nèi)核態(tài)中會(huì)執(zhí)行 內(nèi)核線程。上面說到 內(nèi)核線程mm_struct,即 沒有虛擬內(nèi)存地址空間內(nèi)核線程 不需要訪問 用戶進(jìn)程地址空間,但是需要 頁表 等數(shù)據(jù)信息來訪問 內(nèi)核空間。由于 所有用戶進(jìn)程的內(nèi)核頁表都是一樣的 ,所以 內(nèi)核線程用戶進(jìn)程 那里 借來 一個(gè) mm_strcut,以讓 內(nèi)核線程 能夠訪問 內(nèi)核地址空間。 內(nèi)核線程借來的mm_struct 即存放在 active_mm 中。而如果是 不要外部事件就自行運(yùn)轉(zhuǎn)的內(nèi)核線程,其 active_mm 指向 上一個(gè)用戶進(jìn)程的mm_struct

  • 內(nèi)存描述符mm_usersmm_count 的區(qū)別:

    1. mm_users 表示 正在引用地址空間用戶進(jìn)程數(shù)目,比如 父進(jìn)程 克隆出 子進(jìn)程時(shí),如果共享一個(gè) mm_struct,此時(shí) mm_users 即會(huì) +1。
    2. mm_count 表示 正在引用地址空間內(nèi)核線程數(shù)目,比如 用戶進(jìn)程 進(jìn)入 內(nèi)核態(tài) 后有可能會(huì)執(zhí)行 內(nèi)核線程,此時(shí) 內(nèi)核線程 會(huì) 借用 用戶線程mm_strcut,此時(shí) mm_count 即會(huì) +1。

2.2 內(nèi)存映射

2.2.1 內(nèi)核物理地址空間映射

CPU 通過 外圍設(shè)備控制寄存器 來訪問外設(shè),寄存器 一般分為 控制寄存器、狀態(tài)寄存器數(shù)據(jù)寄存器。一般情況下的 寄存器 都是連續(xù)編址的。

由于 驅(qū)動(dòng)程序 是通過 虛擬地址 來訪問外設(shè)寄存器,所以需要通過內(nèi)核接口實(shí)現(xiàn) 寄存器物理地址到虛擬地址 的映射,以讓驅(qū)動(dòng)程序訪問 外設(shè)控制器

2.2.2 用戶空間內(nèi)存映射

內(nèi)存映射 可以根據(jù) 數(shù)據(jù)源 為以下 2類

  • 文件映射:把 文件的某個(gè)區(qū)間 映射到進(jìn)程的 虛擬地址空間,數(shù)據(jù)源為 文件。該情況下 物理頁 稱為 文件頁
  • 匿名映射:把 物理內(nèi)存 映射到進(jìn)程的 虛擬地址空間,無數(shù)據(jù)源。該情況下 物理頁 稱為 匿名頁

如果根據(jù) 是否對其他進(jìn)程可見是否傳遞到底層文件 可以分為以下 2類

  • 共享映射:修改數(shù)據(jù)時(shí),映射相同區(qū)域 的其他進(jìn)程可以看見。如果是 文件映射,修改會(huì)同步到 文件
  • 私有映射:第一次修改數(shù)據(jù)時(shí)會(huì)從 數(shù)據(jù)源 上復(fù)制副本,然后修改副本。其他進(jìn)程不可見,修改不會(huì)同步到 文件

在進(jìn)程的 虛擬地址空間 中的 代碼段數(shù)據(jù)段私有的文件映射,按照筆者的理解就是 數(shù)據(jù)來源 為程序,但是修改不會(huì)同步到 程序。未初始化的數(shù)據(jù)段、堆和棧私有的匿名映射,按照筆者的理解就是 沒有數(shù)據(jù)來源 (因?yàn)椴恍枰獜某绦蛏献x取任何東西,符合這些 segment 的特性),且修改不會(huì)同步到 程序。

2.2.2 內(nèi)存映射基本原理

PS:本節(jié)僅介紹基本原理,不對實(shí)現(xiàn)細(xì)節(jié)進(jìn)行講解,有興趣的讀者請自行閱讀源碼。

內(nèi)存映射 一般分為 3個(gè)階段

  1. 進(jìn)程啟動(dòng)映射,在 虛擬地址空間中為創(chuàng)建 虛擬映射區(qū)域(VMA)
    1.1. 用戶進(jìn)程 調(diào)用 mmap庫函數(shù)。
    1.2. 在進(jìn)程的 虛擬地址空間 中,尋找一段 滿足要求的、空閑的、連續(xù)的虛擬地址。
    1.3. 為該段 虛擬地址 分配一個(gè) VMA結(jié)構(gòu) 并進(jìn)行 初始化。
    1.4. 將 VMA結(jié)構(gòu) 插入進(jìn)程的 mm_struct的鏈表或樹中
  2. 內(nèi)核空間執(zhí)行 文件操作mmap,創(chuàng)建 物理地址用戶虛擬地址 的映射關(guān)系。
    2.1. 找到文件的 文件結(jié)構(gòu)體(struct file)。
    2.2. 找到文件的 文件操作集file_operations,并執(zhí)行其中的 mmap函數(shù)。
    2.3. mmap函數(shù) 通過 inode結(jié)構(gòu)體 定位到文件在 磁盤 上的 物理地址。
    2.4. 通過 remap_pfn_range函數(shù) 建立 頁表,即建立了 文件地址虛擬映射區(qū)域(VMA) 的映射關(guān)系。注意,此時(shí) VMA 并沒有分配到實(shí)際的 物理內(nèi)存 。
  3. 用戶進(jìn)程 訪問 映射空間 并引發(fā) 缺頁異常,實(shí)現(xiàn) 文件內(nèi)容物理內(nèi)存 的拷貝
    3.1. CPU 通過 MMUtranslation table walking 機(jī)制引發(fā) 缺頁異常
    3.2. 內(nèi)核 發(fā)起 請求調(diào)頁過程
    3.3. 調(diào)頁過程 先在 交換緩存空間(swap cache) 中尋找 需要訪問的內(nèi)存頁,如果 沒有 則調(diào)用nopage函數(shù) 分配 物理頁 并把內(nèi)容讀取到 物理頁 中。
    3.4. 用戶進(jìn)程映射內(nèi)存 進(jìn)行 讀寫操作。如果 寫操作 修改了內(nèi)容,則一定時(shí)間后系統(tǒng)會(huì)自動(dòng)回寫 內(nèi)存中的數(shù)據(jù) 到對應(yīng) 磁盤地址。該過程有一定的 時(shí)間延遲,可以調(diào)用 msync強(qiáng)制同步。

2.2.2 內(nèi)存映射接口

1、用戶接口

內(nèi)存映射 有以下常用的 系統(tǒng)調(diào)用

  1. mmap:用于 創(chuàng)建 內(nèi)存映射,接口為 void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset)

1.1. 作用:

  • 創(chuàng)建 匿名映射,把 物理內(nèi)存 映射到進(jìn)程的 虛擬地址空間
  • 創(chuàng)建 文件映射,直接通過 訪問內(nèi)存 來訪問文件。從而避免系統(tǒng)調(diào)用的切換,提高讀寫效率
  • 創(chuàng)建 兩個(gè)進(jìn)程 對同一個(gè)文件的 共享內(nèi)存映射

1.2. 重要參數(shù):

  • prot:即 保護(hù)位,如下:
含義
PROT_EXEC 可執(zhí)行
PROT_READ 可讀
PROT_WRITE 可寫
PROT_NONE 不可訪問
  • flags:映射標(biāo)志,如下:
含義
MAP_SHARED 共享映射
MAP_PRIVATE 私有映射
MAP_ANONYMOUS 匿名映射
MAP_FIXED 固有映射,指定映射地址 必須 為參數(shù) addr
MAP_HUGETLB 使用 巨型頁
MAP_LOCKED 把頁 在內(nèi)存中
MAP_NORESERVE 不預(yù)留 物理內(nèi)存
MAP_POPULATE 分配并填充 頁表,文件映射 使用該標(biāo)志會(huì)導(dǎo)致 執(zhí)行預(yù)讀
MAP_NONBLOCK 不阻塞,不執(zhí)行 預(yù)讀,只為已存在于內(nèi)存中的頁面建立頁表入口

PS:書中說到MAP_NONBLOCK需要和MAP_POPULATE一起使用才有意義,但這2者是矛盾的。目前筆者還不甚了解,日后再補(bǔ)上

  1. mremap:用于 擴(kuò)大或縮小 已經(jīng)存在的內(nèi)存映射
  2. mumap:用于 刪除 內(nèi)存映射
  3. mprotect:用于設(shè)置 虛擬內(nèi)存區(qū)域訪問權(quán)限

PS:2 - 4接口的參數(shù)與 mmap 相似,這里不在贅述

2、內(nèi)核接口

  • ioremap:把 寄存器物理地址 映射到 內(nèi)核虛擬地址空間
  • iounmap:取消 地址映射
  • remap_pfn_range:把 物理內(nèi)存 映射到進(jìn)程的 虛擬地址空間,即創(chuàng)建 頁表項(xiàng)。實(shí)現(xiàn) 進(jìn)程內(nèi)核 共享內(nèi)存。接口如下:
int remap_pfn_range(struct vm_area_struct *, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t)
  • io_remap_pfn_range:把 寄存器物理地址 映射到 用戶虛擬地址空間,一般情況下與 remap_pfn_range。都是將 內(nèi)核 可以訪問的 地址 映射到 用戶空間。接口如下:
static inline int io_remap_pfn_range(struct vm_area_struct *vma, 
                                     unsigned long vaddr, 
                                     unsigned long pfn, 
                                     unsigned long size, 
                                     pgprot_t prot)

驅(qū)動(dòng)實(shí)現(xiàn) mmap函數(shù) 時(shí),一般需要調(diào)用 remap_pfn_rangeio_remap_pfn_range,這 2 個(gè)函數(shù)都有 VMA結(jié)構(gòu)體。
內(nèi)核的mmap操作函數(shù) 接口如下:

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

可以看到也帶有 VMA結(jié)構(gòu)體,這是操作系統(tǒng)內(nèi)部通過查找獲取到的有效 VMA區(qū)域,那么 內(nèi)核映射接口 通過對該 VMA區(qū)域 進(jìn)行映射即可。

VMA結(jié)構(gòu)體 的部分代碼如下:

struct vm_area_struct {
    unsigned long vm_start;//虛擬地址區(qū)域起始地址
    unsigned long vm_end;//虛擬地址區(qū)域結(jié)束地址,一般區(qū)級為[start, end)
    struct vm_area_struct *vm_next, *vm_prev;//VMA結(jié)構(gòu)體鏈表,按起始地址排序。用于組織一個(gè)進(jìn)程的VMA結(jié)構(gòu)體
    struct rb_node vm_rb;//VMA結(jié)構(gòu)體紅黑樹。用于組織一個(gè)進(jìn)程的VMA結(jié)構(gòu)體
    struct mm_struct *vm_mm;//指向內(nèi)存描述符
    pgprot_t vm_page_prot;//保護(hù)位
    struct list_head anon_vma_chain; //匿名VMA鏈表,用于組合本進(jìn)程和父進(jìn)程的所有匿名VMA
    struct anon_vma *anon_vma;  //該結(jié)構(gòu)體用于組織匿名頁被映射到的所有虛擬地址空間
    const struct vm_operations_struct *vm_ops;//VMA操作集
    unsigned long vm_pgoff;//文件偏移,在文件映射時(shí)有效
    struct file * vm_file;//指向文件,在文件映射時(shí)有效
    void * vm_private_data;
} __randomize_layout;

內(nèi)核驅(qū)動(dòng)在實(shí)現(xiàn) mmap操作函數(shù) 時(shí),需要對 VMA結(jié)構(gòu)體 進(jìn)行映射,該結(jié)構(gòu)體中有一個(gè) vm_ops成員,該成員是一些對 虛擬內(nèi)存區(qū)域 操作的函數(shù),其中一些重要的回調(diào)函數(shù)含義如下:

  • open:在 創(chuàng)建虛擬內(nèi)存區(qū)域 時(shí)調(diào)用該方法,通常不用,可設(shè)置為空
  • close:在 刪除虛擬內(nèi)存區(qū)域 時(shí)調(diào)用該方法,通常不用,可設(shè)置為空
  • mremap:在應(yīng)用層調(diào)用 mremap系統(tǒng)調(diào)用 時(shí)會(huì)執(zhí)行該回調(diào)
  • fault:當(dāng)執(zhí)行 缺頁異常中斷 時(shí)會(huì)執(zhí)行該回調(diào)。由于有可能訪問的虛擬地址還沒映射到物理頁,所以會(huì)產(chǎn)生 缺頁異常。
  • huge_fault:與 fault方法 類似,在該函數(shù)的使用對象是 透明巨型頁的文件映射

PS:在文末附有筆者的代碼例程和注釋

2.3 物理內(nèi)存組織形式

在以前的 單CPU 架構(gòu)中,對于內(nèi)存的訪問形式是比較直接的,直接通過地址跟數(shù)據(jù)總線即可訪問內(nèi)存,如下圖所示:

單CPU內(nèi)存架構(gòu)

但在 多處理器 架構(gòu)種,對于內(nèi)存的訪問則復(fù)雜得多,其架構(gòu)也有所不同。目前 多處理器內(nèi)存訪問架構(gòu) 有以下兩種:

  • 非一致內(nèi)存訪問:即 Non-Uniform Memory Access(NUMA)NUMA 將內(nèi)存劃分為多個(gè) 內(nèi)存節(jié)點(diǎn),每個(gè) 處理器 都有一個(gè) 直連內(nèi)存節(jié)點(diǎn),也稱為 本地內(nèi)存節(jié)點(diǎn),其余節(jié)點(diǎn)稱為 遠(yuǎn)程內(nèi)存節(jié)點(diǎn)。內(nèi)存訪問時(shí)間 取決于 處理器內(nèi)存節(jié)點(diǎn) 的距離。一般訪問 本地內(nèi)存節(jié)點(diǎn)遠(yuǎn)程內(nèi)存節(jié)點(diǎn) 要快。
    一般 NUMA架構(gòu) 用于 中高端服務(wù)器,其架構(gòu)圖如下所示:

    NUMA架構(gòu)

  • 對稱對處理器:即 Symmetric Multi-Processor(SMP)SMP所有 處理器 共享內(nèi)存子系統(tǒng)以及總線。每個(gè) 處理器 訪問內(nèi)存花費(fèi)的時(shí)間是相同的,訪問內(nèi)存 時(shí)每個(gè) 處理器 地位都 平等。這種架構(gòu)使得 每個(gè)處理器 能夠共享 內(nèi)存和其他資源,即意味著 處理器內(nèi)存 中的每個(gè)數(shù)據(jù)都只能 保持或共享 唯一的一個(gè)數(shù)值,即具有 一致性。所以 SMP 一般也稱為 一致性訪問,即 Uniform Memory Access(UMA)。SMP 的缺點(diǎn)就是當(dāng) 處理器 足夠多的時(shí)候,總線速度 會(huì)達(dá)到 飽和,此后就無法通過 增加處理器提高性能。
    SMP 架構(gòu)如如下所示:

    SMP架構(gòu)

NUMA架構(gòu) 下,內(nèi)存管理子系統(tǒng) 在軟件使用三級結(jié)構(gòu)來描述 NUMA,即:

  • 節(jié)點(diǎn)(node):需要分 2 種情況來看
    1. NUMA架構(gòu)內(nèi)存節(jié)點(diǎn) 根據(jù) 處理器內(nèi)存距離 來劃分
    2. 不具備連續(xù)內(nèi)存UMA架構(gòu) 中,根據(jù)物理地址是否連續(xù)來劃分,每塊 連續(xù)的物理地址內(nèi)存 是一個(gè) 內(nèi)存節(jié)點(diǎn)
  • 區(qū)域(zone):即常見的 內(nèi)存區(qū)域劃分,一般分為:
    1. DMA區(qū)域
    2. 線性映射區(qū)域
    3. 高端內(nèi)存區(qū)域
    4. 可移動(dòng)區(qū)域:一般用于 反碎片技術(shù)
  • 頁(page):即 物理內(nèi)存頁

2.4 頁分配器

Linux內(nèi)核 使用了一種算法來實(shí)現(xiàn) 物理頁分配器,用于負(fù)責(zé)管理 物理內(nèi)存的分配和釋放。其算法稱為 伙伴算法,特點(diǎn)是 簡單高效。所以也被稱為 伙伴分配器。

2.4.1 頁分配器算法原理

連續(xù)的物理頁 稱為 頁塊(page block)2^n 個(gè) 連續(xù)頁 稱為 n階頁塊,階數(shù)order。滿足以下條件的 2個(gè)n階頁塊 稱為 伙伴(buddy)

  • 兩個(gè)頁塊是 相鄰 的,即物理地址是 連續(xù)
  • 頁塊的 第一頁的物理頁號(hào) 必須是 2^n整數(shù)倍
  • 如果合并成 (n+1)階頁塊,那么 第一頁的物理頁號(hào) 必須是 2^{n+1} 的整數(shù)倍

即內(nèi)核將 物理內(nèi)存 分為 來管理,每個(gè) 物理頁 都有 頁號(hào)。以 單頁 來舉例,0號(hào)頁1號(hào)頁 是伙伴,但 1號(hào)頁2號(hào)頁 不是伙伴,因?yàn)?1號(hào)頁2號(hào)頁 合并后 第一頁的物理頁號(hào)(即1) 不是 2的整數(shù)倍

伙伴分配器 分配和釋放物理頁 的數(shù)量單位是 ,其過程如下:

  1. 查看是否有 空閑的n階頁塊。如果有則直接分配,如果沒有繼續(xù)執(zhí)行
  2. 查看是否存在 空閑的(n+1)階頁塊。如果有,把 (n+1)階頁塊 分裂為兩個(gè) n階頁塊,一個(gè)插入空閑 n階頁塊鏈表,另一個(gè)分配出去。如果沒有則繼續(xù)執(zhí)行。
  3. 查看是否存在 空閑的(n+2)階頁塊。如果有,把 (n+2)階頁塊 分裂為兩個(gè) n+1階頁塊 ,一個(gè)插入 空閑(n+1)階頁塊鏈表,另一個(gè)分裂為2個(gè) n階頁塊,一個(gè)插入空閑 n階頁塊鏈表,另一個(gè)分配出去。如果沒有,則繼續(xù)查看是否存在 更高階的空白內(nèi)存塊 繼續(xù)執(zhí)行。

釋放 n階頁塊 時(shí),需要查看它的 伙伴 是否 空閑,如果伙伴 不空閑,那么把 n階頁塊 插入 空閑的n階頁塊鏈表。如果 伙伴空閑 的,那么合并為 (n+1)階頁塊。

2.4.2 分配頁

2.4.2.1 分配頁接口

  • alloc_pages(gfp_mask, order):用于請求分配一個(gè) 階數(shù)為order 的頁,返回一個(gè) page實(shí)例
  • alloc_page(gfp_mask):用于請求分配一個(gè) 階數(shù)為0 的頁(即只分配 一頁),返回一個(gè) page實(shí)例
  • __get_free_pages(gfp_mask, order):對函數(shù) alloc_pages 進(jìn)行封裝,只從 低端內(nèi)存區(qū)域(線性映射區(qū)) 分配頁,并且返回 虛擬地址。
  • __get_free_page(gfp_mask):是函數(shù) __get_free_pages階數(shù)為0 情況下的簡化形式,只分配 一頁。
  • __get_zeroed_page(gfp_mask):是函數(shù) __get_free_pagegfp_mask 設(shè)置為 __GFP_ZERO階數(shù)為0 的簡化形式,只分配 一頁初始化為0。

2.4.2.2 分配標(biāo)志位

分配頁塊時(shí)都帶有 分配標(biāo)志位,該標(biāo)志位類型眾多繁雜。雖然有些標(biāo)志位并不常用,但為了能夠更加全面的認(rèn)識(shí),筆者還是全部羅列出來??梢詫⑵浞譃橐韵?5類

  1. 區(qū)域修飾符:指定分配頁的 區(qū)域 類型
    • __GFP_DMA:從 DMA區(qū)域 分配頁
    • __GFP_HIGHMEM:從 高端內(nèi)存區(qū)域 分配頁
    • __GFP_DMA32:從 DMA32區(qū)域 分配頁
    • __GFP_MOVABLE:從 可移動(dòng)區(qū)域 分配頁
  2. 頁移動(dòng)性和位置提示:指定頁的 遷移類型內(nèi)存節(jié)點(diǎn)
    • __GFP_MOVABLE:申請 可移動(dòng)頁
    • __GFP_RECLAIMABLE:申請 可回收頁
    • __GFP_WRITE:指明調(diào)用者的 寫物理頁 意圖。盡量把該類型的頁分布到本地節(jié)點(diǎn)的 所有區(qū)域,避免所有 臟頁 在一個(gè) 內(nèi)存區(qū)域
    • __GFP_HARDWALL:實(shí)施 cpuset內(nèi)存分配策略(有興趣的讀者自行了解)
    • __GFP_ACCOUNT:把分配的也記錄在 內(nèi)核內(nèi)存控制組(有興趣的讀者自行了解)
    • __GFP_THISNODE:強(qiáng)制從 本地節(jié)點(diǎn) 分配頁
  3. 水線修飾符
    • __GFP_HIGH:指明調(diào)用者為 高優(yōu)先級,系統(tǒng)必須通過請求
    • __GFP_ATOMIC:指明調(diào)用者為 高優(yōu)先級,不能回收頁或者進(jìn)入睡眠
    • __GFP_MEMALLOC:允許訪問所有內(nèi)存
    • __GFP_NOMEMALLOC:進(jìn)制訪問 緊急保留內(nèi)存
  4. 回收修飾符
    • __GFP_IO:允許 讀寫存儲(chǔ)設(shè)備
    • __GFP_FS:允許向下調(diào)用到 底層文件系統(tǒng)。當(dāng) 文件系統(tǒng) 申請頁時(shí),如果內(nèi)存 嚴(yán)重不足,直接回收頁,把 臟頁 會(huì)寫到 存儲(chǔ)設(shè)備。為了避免調(diào)用 文件系統(tǒng)函數(shù) 可能會(huì)導(dǎo)致的 死鎖,文件系統(tǒng)申請頁的時(shí)候應(yīng)該 清除 該標(biāo)志位。
    • __GFP_DIRECT_RECLAIM:調(diào)用者可以 直接回收頁
    • __GFP_KSWAPD_RECLAIM:當(dāng) 空閑頁數(shù) 達(dá)到 低水線 時(shí),調(diào)用者想要喚醒 頁回收線程kswapd(即 異步回收頁)
    • __GFP_RECLAIM:允許 直接回收頁異步回收頁
    • __GFP_RETRY_MAYFAI允許重試,直到多次以后放棄,分配可能 失敗
    • __GFP_NOFAIL必須無限次重試,因?yàn)檎{(diào)用者 不能處理分配失敗
    • __GFP_NORETRY不要重試,當(dāng) 直接回收頁內(nèi)存碎片整理 不能使得分配成功的時(shí)候,應(yīng)該放棄。
  5. 行動(dòng)修飾符
    • __GFP_COLD:調(diào)用不期望分配的頁很快被使用,盡可能分配 緩存冷頁(即 數(shù)據(jù)在不處理器緩存中)
    • __GFP_NOWARN:如果分配失敗,不要打印告警信息
    • __GFP_COMP:把分配的頁組成 復(fù)合頁(compound page)
    • __GFP_ZERO:把 使用 0 進(jìn)行初始化

2.4.2.3 分配標(biāo)志位組合

以上是比較繁瑣復(fù)雜的 分配標(biāo)志,但往往 分配標(biāo)志位 都是組合使用。常用的組合也已經(jīng)在 內(nèi)核 中定義好可以直接使用,如下所示:
PS:有關(guān)于宏的組合請有興趣的讀者到內(nèi)核中去查看即可

  • GFP_ATOMIC原子分配 內(nèi)核使用的頁,不能睡眠。調(diào)用者為 高優(yōu)先級,允許 異步回收頁。
  • GFP_KERNEL:分配 內(nèi)核 使用的頁,可能睡眠。從 低端內(nèi)存區(qū)域 分配,允許 直接回收頁異步回收頁,允許 讀寫存儲(chǔ)設(shè)備,允許 調(diào)用到底層文件系統(tǒng)。
  • GFP_NOWAIT:分配 內(nèi)核 使用的頁,不用等待。允許 異步回收頁,不允許 直接回收頁,不允許 讀寫存儲(chǔ)設(shè)備,不允許 調(diào)用到底層文件系統(tǒng)
  • GFP_NOIO:不允許 讀寫存儲(chǔ)設(shè)備,允許 直接回收頁異步回收頁
  • GFP_NOFS:不允許 調(diào)用到底層文件系統(tǒng),允許 讀寫存儲(chǔ)設(shè)備,允許 直接回收頁異步回收頁。
  • GFP_USER:分配 用戶空間 使用的頁,內(nèi)核或硬件 也可以直接訪問,從 普通區(qū)域(線性映射區(qū)) 分配分配,允許 調(diào)用到底層文件系統(tǒng),允許 讀寫存儲(chǔ)設(shè)備,允許 直接回收頁異步回收頁,允許 實(shí)施cpuset內(nèi)存分配策略
  • GFP_HIGHUSER:分配 用戶空間 使用的頁,內(nèi)核或硬件 不直接訪問,從 高端區(qū)域 分配分配,物理頁使用過程中 不可以移動(dòng)。
  • GFP_HIGHUSER_MOVABLE:分配 用戶空間 使用的頁,內(nèi)核或硬件 不直接訪問,從 高端區(qū)域 分配分配,物理頁可以通過 頁回收頁遷移技術(shù)移動(dòng)。
  • GFP_TRANSHUGE_LIGHT:分配 用戶空間 使用的 巨型頁,把分配的頁塊組成 復(fù)合頁,禁止使用 緊急保留內(nèi)存,禁止 打印告警信息,不允許 直接回收頁異步回收頁。
  • GFP_TRANSHUGE:和 GFP_TRANSHUGE_LIGHT 類似,不同之處在于允許 直接回收頁

2.4.3 釋放頁

  • __free_pages(page, order):參數(shù) page頁實(shí)例,參數(shù) order階數(shù)
  • free_pages(addr, order):參數(shù) addr內(nèi)核起始虛擬地址,參數(shù) order階數(shù)

一般釋放時(shí)是把 頁的計(jì)數(shù)1,直到為 0 時(shí)才釋放頁。如果頁的階數(shù)為 0,則將其作為 緩存熱頁 存儲(chǔ)到某個(gè)區(qū)域中,不返回給 伙伴分配器,如果階數(shù) 大于0 則釋放

2.5 塊分配器

頁分配器 只能提高最小 4Kb 的內(nèi)存頁,但實(shí)際場景中往往會(huì)使用到 小于4Kb 的內(nèi)存,這樣會(huì)遭殃嚴(yán)重的內(nèi)存浪費(fèi)。為了解決 小塊內(nèi)存 的分配問題,Linux內(nèi)核 提供 塊分配器塊分配器 使用 頁分配器 申請出 物理頁,然后對 物理頁 進(jìn)行切分,再將切分后的內(nèi)存分配出去,減少內(nèi)存浪費(fèi)。

塊分配器 為每種 對象類型 創(chuàng)建一個(gè) 內(nèi)存緩存,每個(gè) 內(nèi)存緩存 又分為多個(gè) 內(nèi)存塊(slab),一個(gè) 大塊一個(gè)或多個(gè)連續(xù)的物理頁 組成。而每個(gè)大塊有被切分為多個(gè) 對象。可以看出 塊分配器 使用的是 面向?qū)ο?/strong> 的思想,基于 對象類型 管理內(nèi)存。其結(jié)構(gòu)如下圖所示:

塊分配器結(jié)構(gòu)

塊分配器 有幾種實(shí)現(xiàn)方式,分別為:

  • SLAB:適用于 大量物理內(nèi)存 的機(jī)器
  • SLUB:與 SLAB 相同,在 SLAB 的基礎(chǔ)上進(jìn)行優(yōu)化,其內(nèi)存開銷比較小
  • SLOB:適用于 小內(nèi)存 的嵌入式機(jī)器

2.5.1 塊分配器接口

雖然 塊分配器 具有多種實(shí)現(xiàn),但對外提供了統(tǒng)一接口,只是底層實(shí)現(xiàn)不同。

2.5.1.1 通用內(nèi)存緩存接口

  • kmalloc:用于 分配內(nèi)存。塊分配器找到一個(gè)合適的 通用內(nèi)存緩存(即對象長度剛好大于或等于請求的內(nèi)存長度),從中 分配對象 并返回 對象地址
  • krealloc:用于 重新 分配內(nèi)存,根據(jù)新的長度為對象分配新的內(nèi)存。
  • kfree:釋放內(nèi)存

通用內(nèi)存緩存接口 需要找到一個(gè) 對象長度剛好大于或等于請求的內(nèi)存長度通用緩存。如果請求的內(nèi)存長度和申請到的內(nèi)存緩存對象的長度相差太遠(yuǎn)會(huì)浪費(fèi)較大的內(nèi)存

2.5.1.2 專用內(nèi)存緩存接口

為了解決 通用內(nèi)存緩存的問題,可以使用 專用內(nèi)存緩存。專用內(nèi)存緩存 可以指定 對象長度,有效解決內(nèi)存浪費(fèi)的問題。其接口如下:

  • keme_cache_create:創(chuàng)建內(nèi)存緩存
  • kmem_cache_destroy:銷毀內(nèi)存緩存
  • kmem_cache_alloc:從內(nèi)存緩存中分配對象
  • kmem_cache_free:釋放對象,將其放回內(nèi)存緩存中

2.5.2 SLAB分配器

本小節(jié)將簡單地介紹 SLAB分配器 的原理,有助理解內(nèi)核的內(nèi)存管理。
SLAB 的數(shù)據(jù)結(jié)構(gòu)如圖所示:

SLAB

圖中需要關(guān)注的數(shù)據(jù)結(jié)構(gòu)有 3個(gè)

  • struct kmem_cache:即 內(nèi)存緩存,所有操作都在該結(jié)構(gòu)體上
  • struct kmem_cache_node:即 內(nèi)存緩存節(jié)點(diǎn),每一個(gè) 內(nèi)存節(jié)點(diǎn) 都對應(yīng)一個(gè)該結(jié)構(gòu)體
  • struct page:即 page示例,一個(gè) page實(shí)例 對應(yīng)一個(gè) 物理頁

2.5.2.1 struct kmem_cache

其結(jié)構(gòu)體代碼如下:

struct array_cache {
    /* 數(shù)組entry存放的可用對象數(shù)量 */
    unsigned int avail;
    /* 數(shù)組entry的大小 */
    unsigned int limit;
    /* 對象地址數(shù)組,用于存放釋放對象的指針 */
    void *entry[];
};

struct kmem_cache {
    /* 
    指向本地CPU高速緩存的指針數(shù)組。每個(gè)CPU在其中都有對應(yīng)的結(jié)構(gòu)體。
    當(dāng)有對象釋放時(shí),優(yōu)先放入本地CPU高速緩存中 
    */
    struct array_cache __percpu *cpu_cache;
    /* 每個(gè)SLAB擁有的對象數(shù)量 */
    unsigned int num;
    /* slab的階數(shù),代表slab擁有即2^gfporder個(gè)物理頁 */
    unsigned int gfporder;
    /* 對象的原始長度 */
    int object_size;
    /* 
    每個(gè)對象都帶有填充信息,以讓塊分配器可以進(jìn)行某些操作 
    size = 填充信息長度 + object_size
    */
    int size;
    /* slab節(jié)點(diǎn)鏈表,每個(gè)內(nèi)存節(jié)點(diǎn)都對應(yīng)一個(gè)鏈表節(jié)點(diǎn) */
    struct kmem_cache_node *node[MAX_NUMNODES];
};
  1. 剛釋放的對象 存放在 cpu_cache 可以有效的 減少鏈表操作和鎖操作,并提高分配速度。
  2. sizeobject_size 的關(guān)系如下圖所示:
    size關(guān)系圖
    • 修改幻數(shù)1:長度 8字節(jié),如果該值被修改說明對象被改寫。該字段打開調(diào)試宏時(shí)才有
    • 修改幻數(shù)2:與 修改幻數(shù)1 相同。該字段打開調(diào)試宏時(shí)才有
    • 最后調(diào)用者地址:長度 8字節(jié),用于確定對象被誰改寫。該字段打開調(diào)試宏時(shí)才有
      PS:關(guān)于這3個(gè)調(diào)試字段,有興趣的讀者自行查閱資料

2.5.2.2 struct kmem_cache_node

重要成員代碼如下:

/* kmem_cache_node鏈表的節(jié)點(diǎn),本質(zhì)上就是一個(gè)page實(shí)例,即物理頁  */
struct page {
    /* 本質(zhì)是一個(gè)數(shù)組,該指針指向第一個(gè)對象的地址 */
    void *s_mem;
    /* 本質(zhì)是一個(gè)數(shù)組,其元素存放的是空閑對象在s_mem數(shù)組中的下標(biāo) */
    void *freelist; 
    /* 
    active具有2層含義:
    1. 記錄該slab分配出去的對象數(shù)量
    2. 是一個(gè)數(shù)組下標(biāo),指向freelist數(shù)組中的第一個(gè)空閑對象下標(biāo)。該下標(biāo)所在的元素是s_mem中對象的下表你
     */
    unsigned int active;
    /* 鏈表節(jié)點(diǎn),用于串起多個(gè)slab */
    struct list_head lru;
    /* 指向所屬的內(nèi)存緩存 */
    struct kmem_cache *slab_cache;
};
struct kmem_cache_node {
    /* 只分配了部分對象的slab(page)鏈表 */
    struct list_head slabs_partial;
    /* 所有對象都已經(jīng)分配出去的slab(page)鏈表 */
    struct list_head slabs_full;
    /* 所有對象都空閑的slab(page)鏈表 */
    struct list_head slabs_free;
};

這里可能讀者對于 freelists_mem 的關(guān)系有些許疑惑,筆者簡單地做以下說明:

  1. 空閑階段:此時(shí)沒有分配任何對象,所以可以看到 s_mem 中的所有對象為 空閑, active 也為 0,表示沒有對象分配出去。且 active 指向 freelist 的第一個(gè)元素,而 freelistactive 所指向的元素的值為 0,表明此時(shí) s_mem 中下標(biāo)為 freelistactive 的對象是 空閑

    空閑階段

  2. 分配階段:此時(shí)分配出 1個(gè)對象,可以看到 s_mem 中的 對象0 已經(jīng)被分配出去了。且 active 的值為 1,指向了 freelist 的元素也改變了

    分配階段

  3. 連續(xù)分配:經(jīng)過多次使用,s_mem 已經(jīng)分配出 3 個(gè)對象了,其中 active 的值為 3。指向了 freelist 的元素值為 3s_mem[freelist[active]] 的對象是空閑的。以此類推

    連續(xù)分配

  4. 釋放階段:使用完了以后釋放 對象0,可以看到 freelist[active] 的值為 0,那么 s_mem[freelist[active]] 的對象已經(jīng)被釋放且狀態(tài)為 空閑

    釋放階段

從代碼中可以看出 freelist 僅僅只是一個(gè) 指針,那么就需要為這個(gè)指針 開辟內(nèi)存空間。而關(guān)于存放該內(nèi)存空間的地方也有一定的講究,這里筆者簡單地說明一下,請有興趣的讀者自行查閱資料。

  1. s_mem 中分配出一個(gè) 對象 存放 freelist數(shù)組,即使用內(nèi)部空間
  2. 重新在 s_mem 之外的地方開辟一個(gè) 數(shù)組 來存放 freelist數(shù)組,即使用外部空間

SLAB分配 會(huì)定時(shí) 回收對象和空閑slab,其實(shí)現(xiàn)方法是在 每個(gè)處理器 上向 全局工作隊(duì)列 添加 延遲工作項(xiàng),其處理函數(shù)為 cache_reap

一般來講,SLAB 釋放對象后,對象優(yōu)先放在 CPU_cache。當(dāng) CPU_cache 滿了以后再將 空閑對象 批量釋放到 空閑SLAB。而 空閑SLAB 達(dá)到一定數(shù)量后再將 物理頁 釋放到 頁分配器

2.5.3 SLUB分配器

前面提到 SLAB分配器 釋放 對象 時(shí)會(huì)優(yōu)先回到 CPU_cache,這將造成一個(gè)問題。設(shè)想一下,如果 SLAB 迎來了一個(gè) 分配高峰期,則會(huì)從 頁分配器 申請?jiān)S多 SLAB(物理頁)。之后這些 物理內(nèi)存 將優(yōu)先放回 CPU_cache三個(gè)SLAB鏈表 中,而不是回到 頁分配器,從而造成內(nèi)存浪費(fèi)。

為了解決該問題,內(nèi)核提供了 SLUB分配器,其數(shù)據(jù)結(jié)構(gòu)圖下圖所示:

SLUB

相比于 SLAB分配器,SLUB分配器 有以下改進(jìn):

  • SLUB分配器 的數(shù)據(jù)結(jié)構(gòu)開銷小
  • SLUB分配器 僅保留了 部分空閑鏈表(partial_slab)
  • SLUB分配器 不進(jìn)行 著色(用于加快內(nèi)存訪問,有興趣讀者請自行查閱資料)

SLAB分配器 一樣,重要的數(shù)據(jù)結(jié)構(gòu)還是:

  • kmem_cache
  • kmem_cache_node
  • page

2.5.3.1 kmem_cache

SLUB分配器 部分代碼如下

struct kmem_cache_cpu {
    /* 指向當(dāng)前使用slab的空閑對線鏈表 */
    void **freelist;    
    /* 指向當(dāng)前使用的slab(page實(shí)例) */
    struct page *page;  
    /* 指向當(dāng)前每處理器部分空閑slab鏈表 */
    struct page *partial;   
};
struct kmem_cache {
    /* 處理器緩存 */
    struct kmem_cache_cpu __percpu *cpu_slab;
    /* 對象長度,帶填充信息 */
    int size;   
    /* 對象原始長度 */
    int object_size;    
    /* 
    低16位為最優(yōu)對象數(shù)量
    高16位為最優(yōu)階數(shù)
     */
    struct kmem_cache_order_objects oo;
    /* 
    低16位為最小對象數(shù)量
    高16位為最小階數(shù)
    當(dāng)設(shè)備長時(shí)間運(yùn)行后,內(nèi)存碎片化,分配連續(xù)的物理頁很難成功。如果分配最優(yōu)階數(shù)的slab失敗,則分配最小階數(shù)slab
     */
    struct kmem_cache_order_objects min;
    /* 內(nèi)存節(jié)點(diǎn) */
    struct kmem_cache_node *node[MAX_NUMNODES];
};

SLAB分配器 同理,對象所具備的信息不只是對象本身,還附帶有其他填充信息。SLUB分配器 的一種(SLUB分配器有多種對象布局,本文按照下面這種來講述)填充信息如下圖所示:

SLUB對象

除了對象之外的其余 填充信息 均要打開 調(diào)試宏 才具備,其功能與 SLAB分配器 類似,需要注意的是 SLUB對象最后調(diào)用者地址 修改為 分配者地址釋放者地址。

SLAB分配器CPU_cache(每處理器緩存) 是以 對象 為單位,而 SLUB分配器CPU_slab(每處理器緩存) 則以 SLAB 為單位

2.5.3.2 kmem_cache_node

SLUB分配器kmem_cache_node 結(jié)構(gòu)復(fù)用了 SLAB分配器, 其部分重要成員如下:

struct kmem_cache_node {
    /* 空閑slab的數(shù)量 */
    unsigned long nr_partial;
    /* 空閑slab鏈表,鏈表節(jié)點(diǎn)為slab,即page實(shí)例 */
    struct list_head partial;
};

2.5.3.3 page

SLUB分配器 也復(fù)用了 struct page 來作為 鏈表節(jié)點(diǎn),其部分成員如下:

struct page {
    /* 一般設(shè)置為 PG_SLAB << 1 來表示該頁屬于 SLUB分配器 */
    unsigned long flags;
    /* 空閑對象鏈表 */
    void *freelist;
    struct {
        /* 已分配對象的數(shù)量 */
        unsigned inuse:16;
        /* 對象數(shù)量 */
        unsigned objects:15;
        /* 表示當(dāng)前page實(shí)例是否被凍結(jié)在cpu_slab中 */
        unsigned frozen:1;
    };   
    /* 鏈表節(jié)點(diǎn) */
    struct list_head lru;
    /* 指向所屬的kmem_cache */
    struct kmem_cache *slab_cache;
}

SLAB分配器空閑鏈表是一個(gè)數(shù)組,而 SLUB分配器空閑鏈表 則是一個(gè)貨真價(jià)實(shí)的鏈表。對象 中的 空閑指針 指向了 下一個(gè)對象的地址,從而將所有 對象 串起來。空閑鏈表 的幾種狀態(tài)如下:

  • 空閑狀態(tài)
    空閑freelist
  • 分配狀態(tài):修改 freelist指針的指向,如下圖
    分配對象

釋放時(shí)將對象繼續(xù)插入回 freelist鏈表

2.5.3.4 CPU_slab

SLUB分配器CPU_slab 結(jié)構(gòu)圖如下所示:

CPU_slab

struct kmem_cache_cpu {
    /* 指向當(dāng)前使用slab的空閑對線鏈表 */
    void **freelist;    
    /* 指向當(dāng)前使用的slab(page實(shí)例) */
    struct page *page;  
    /* 指向當(dāng)前每處理器部分空閑slab鏈表 */
    struct page *partial;   
};
struct kmem_cache {
    struct kmem_cache_cpu  *cpu_slab;
    /* 處理器緩存 */
    int CPU_partial
};

一些重要數(shù)據(jù)結(jié)構(gòu)的說明如下:

  • CPU_slab 中的 page成員 指向了當(dāng)前正在使用的 slab。而 partial 指向了 等待使用的部分空閑slab,并且串成鏈表。
  • partial鏈表第一個(gè)slab 中的 pages 表明了該鏈表中 slab的數(shù)量,pobjects 表明鏈表中 空閑對象的數(shù)量。(請注意鏈表后面的slab并沒有使用這2個(gè)成員)
  • 內(nèi)存緩存kmem_cache 中的 cpu_partial 決定了 partial鏈表空閑對象的數(shù)量

上面簡單講了數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系,下面我們看看 分配對象釋放對象 時(shí)如何操作 cpu_slab。

  • 分配對象

    1. 當(dāng)前處理器cpu_slab 分配,如果 當(dāng)前使用的slab空閑對象,那么分配一個(gè)對象。
    2. 如果 當(dāng)前處理器的cpu_slab鏈表 沒有 空閑對象,那么取 partial鏈表 中的 第一個(gè)slab 作為當(dāng)前使用的slab,并繼續(xù)分配對象
  • 釋放對象

    1. 如果 對象 所屬的 slab 在此之前沒有 空閑對象,并且 沒有凍結(jié)cpu_slab 中,那么把該 slab 存放到 partial鏈表
    2. 如果發(fā)現(xiàn) partial鏈表 中的 空閑對象總數(shù) 超過了 cpu_partial,那么把 partial鏈表 中的 所有slab 歸還到 內(nèi)存節(jié)點(diǎn)部分空閑slab鏈表 中。

可以發(fā)現(xiàn),釋放對象 時(shí)會(huì)檢查 slab空閑對象數(shù)量,并據(jù)此進(jìn)行操作。這樣做的好處是:優(yōu)先使用空閑對象少的slab,并且從中分配對象,減少內(nèi)存浪費(fèi)

2.6 不連續(xù)內(nèi)存分配

當(dāng)設(shè)備長時(shí)間裕興后,內(nèi)存會(huì)碎片化,很難找到 連續(xù)的物理也。如果在這種情況下仍需要分配出 長度超過一頁的內(nèi)存塊,可以使用 不連續(xù)分配器。使用 不連續(xù)分配器 分配 內(nèi)存頁虛擬地址 上是 連續(xù)的,而在 物理地址不連續(xù)。
而且 不連續(xù)分配器 是優(yōu)先從 高端區(qū)域 分配虛擬地址,減少了 常規(guī)線性區(qū)域 的使用。
其實(shí)現(xiàn)原理是使用 MMU 建立 *物理地址虛擬地址 的映射,并使用方法確保 不連續(xù)的物理地址 按照固定的映射關(guān)系映射到 連續(xù)的虛擬地址

PS:不連續(xù)內(nèi)存分配器的映射單位為物理頁,小于物理頁尺寸的內(nèi)存無法映射到連續(xù)的虛擬地址上,只能將多個(gè)不連續(xù)的物理頁映射到連續(xù)的虛擬地址上

其接口如下:

  • vmalloc:分配 不連續(xù)的物理頁 并將 物理頁地址 映射到 連續(xù)的虛擬地址

  • vfree:使用內(nèi)存

  • vmap(pages, count, flags, prot):把已經(jīng)分配的 不連續(xù)頁 映射到 連續(xù)的虛地址,參數(shù)如下

    1. pagespage實(shí)例 的指針數(shù)組
    2. count指針數(shù)組 的大小
    3. flags:標(biāo)志位
    4. prot:保護(hù)位
  • vunmap:釋放映射的 虛擬地址

  • kvmalloc:優(yōu)先使用 kmalloc 分配內(nèi)存,如果失敗再使用 vmalloc

  • kvfree:釋放 kvmalloc 分配的內(nèi)存

那么以上就是本文的所有內(nèi)容,只是簡單地對內(nèi)核的內(nèi)存管理進(jìn)行整理,梳理出框架脈絡(luò)。本文內(nèi)容僅占Linux內(nèi)存管理的冰山一角,有興趣的讀者請自行查閱其他資料補(bǔ)齊知識(shí)框架

三、附錄

3.1 例程代碼

/* 內(nèi)核驅(qū)動(dòng) */
#include <linux/module.h>   
#include <linux/init.h>  
#include <linux/fs.h>   
#include <linux/device.h>   
#include <linux/slab.h>  
#include <linux/cdev.h>  
#include <linux/err.h>  
#include <linux/mm_types.h>  
#include <asm/uaccess.h>  
#include <linux/io.h>  
#include <linux/platform_device.h>
#include <linux/kern_levels.h>
#include <linux/ioport.h>      
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <linux/uaccess.h>


#define DEVICE_MM_SIZE 4096

struct test_device
{
    dev_t n_dev;            
    struct cdev cdev;     
    struct class *dev_class;
    struct device *dev;  
    u8*  share_buffer;
    u32  buffer_size;  
};

static int open(struct inode* inode, struct file* filp)
{   
    printk("device open, filp = %p, f_owner.pid = %p\n", filp, filp->f_owner.pid);
    filp->private_data = (void*)container_of(inode->i_cdev, struct test_device, cdev);
    return 0;
}

static ssize_t read(struct file* filp, char __user * buf, size_t size, loff_t* ppos)
{
    int ret = 0;
    struct test_device* test_device = (struct test_device*)filp->private_data;
    if(size > test_device->buffer_size)
    {
        printk("size(%d) is out of range(%d)\n", size, test_device->buffer_size);
        return -1;
    }

    ret = copy_to_user(buf, test_device->share_buffer, size);
    if(-1 == ret)
    {
        printk("read failed\n");
        return -1;        
    }

    return size;
}

static ssize_t write(struct file* filp, const char __user * buf, size_t size, loff_t* ppos)
{
    int ret = 0;
    struct test_device* test_device = (struct test_device*)filp->private_data;

    if(size > test_device->buffer_size)
    {
        printk("size(%d) is out of range(%d)\n", size, test_device->buffer_size);
        return -1;
    }

    ret = copy_from_user(test_device->share_buffer, buf, size);    
    if(-1 == ret)
    {
        printk("write failed\n");
        return -1;        
    }

    return size;
}

static int mmap(struct file* filp, struct vm_area_struct* vma)
{
    struct test_device* test_device = (struct test_device*)filp->private_data;
    vma->vm_flags |= VM_IO;
    if(remap_pfn_range(vma, 
                       vma->vm_start, 
                       virt_to_phys(test_device->share_buffer) >> PAGE_SHIFT, 
                       vma->vm_end - vma->vm_start, 
                       vma->vm_page_prot))
    {
        return -EAGAIN;
    }
    return 0;
}

static struct file_operations test_fops = 
{
    .owner = THIS_MODULE, 
    .open = open,
    .read = read,
    .write = write,
    .mmap = mmap,
};

static int probe(struct platform_device *pdev)
{
    int ret = 0;
    struct test_device* test_device = NULL;

    /* 1. 為設(shè)備數(shù)據(jù)分配空間 */
    test_device = devm_kzalloc(&pdev->dev, sizeof(struct test_device), GFP_KERNEL);
    if(!test_device)
    {
        printk("can't create test_device\n");
        goto ALLOC_FAIL;
    }
    platform_set_drvdata(pdev, test_device);

    /* 2.1 分配設(shè)備號(hào) */
    int base_minor = 0;
    int n_num = 1;
    ret = alloc_chrdev_region(&test_device->n_dev, base_minor, n_num, "test_device");
    if(0 != ret)
    {
        printk(KERN_INFO"can't alloc a chrdev number\n");
        goto REGION_FAIL;
    }
    printk("major = %d, minor = %d\n", MAJOR(test_device->n_dev), MINOR(test_device->n_dev));

    /* 2.2 初始化字符設(shè)備 */
    int dev_count = 1;
    cdev_init(&test_device->cdev, &test_fops);
    test_device->cdev.owner = THIS_MODULE;
    ret = cdev_add(&test_device->cdev, test_device->n_dev, dev_count);
    if(0 != ret)
    {
        printk(KERN_INFO"add a cdev error\n");
        goto CDEV_FAILE;
    }  

    /* 2.3 創(chuàng)建設(shè)備,發(fā)出uevent事件 ,在/sys/class/目錄下創(chuàng)建設(shè)備類別目錄dev_class */
    test_device->dev_class = class_create(THIS_MODULE, "dev_class");
    if(IS_ERR(test_device->dev_class)) 
    {
        printk(KERN_INFO"create a class error\n");
        goto CDEV_FAILE;
    }       

    /* 2.4 在/dev/目錄和/sys/class/gpio_class目錄下分別創(chuàng)建設(shè)備文件gpio_dev */
    test_device->dev = device_create(test_device->dev_class, NULL, test_device->n_dev, NULL, "test_dev");
    if(IS_ERR(test_device->dev)) 
    {
        printk(KERN_INFO"create a device error\n");
        goto CLASS_FAILE;
    }

    test_device->buffer_size = DEVICE_MM_SIZE;
#if 1    
    /*
        請注意,要進(jìn)行mmap映射的內(nèi)存地址必須是4k對齊,不然應(yīng)用層使用mmap映射后的地址可能是不對的。
        筆者理解是:因?yàn)閞emap_pfn_range調(diào)用會(huì)對地址進(jìn)行檢查,將地址映射到4k對齊的虛擬地址。
        舉個(gè)例子,假如該內(nèi)存地址是不對齊的,比如為0x0000fb10,那么有可能會(huì)導(dǎo)致地址映射到0x0000fb00,這樣導(dǎo)致應(yīng)用層映射到的地址是從0x0000fb00開始
    */
    test_device->share_buffer = kmalloc(test_device->buffer_size, GFP_KERNEL);
#else
    /*
        使用devm_kzalloc會(huì)導(dǎo)致出來的地址并不是4k對齊,從而導(dǎo)致應(yīng)用層會(huì)發(fā)生映射錯(cuò)誤
        筆者一開始使用的是devm_kzalloc分配,分配出來的地址為0xc3b96010,從而導(dǎo)致應(yīng)用層讀取錯(cuò)誤,需要加上16的便宜才能訪問到正確的地址
        修改為kmalloc,分配出來的地址是0xb6f9a000,該地址是4k對齊,所以應(yīng)用層映射后的地址是正確的。
    */
    test_device->share_buffer = devm_kzalloc(&pdev->dev, test_device->buffer_size, GFP_KERNEL);
#endif
    if(!test_device->share_buffer)
    {
        printk("can't create share buffer\n");
        goto ALLOC_BUFFER_FAIL;
    }

    return 0;
ALLOC_BUFFER_FAIL:
    device_destroy(test_device->dev_class, test_device->n_dev);
CLASS_FAILE:
    class_destroy(test_device->dev_class);
CDEV_FAILE:    
    unregister_chrdev_region(test_device->n_dev, n_num); 
REGION_FAIL:    
    devm_kfree(&pdev->dev, test_device); 
ALLOC_FAIL:
    return -1;
}  
static int remove(struct platform_device *pdev)
{    
    struct test_device* test_device = (struct test_device*)platform_get_drvdata(pdev);
    int n_num = 1;
#if 1  
    kfree(test_device->share_buffer);
#else
    devm_kfree(&pdev->dev, test_device->share_buffer);
#endif    
    device_destroy(test_device->dev_class, test_device->n_dev);
    class_destroy(test_device->dev_class);
    cdev_del(&test_device->cdev);
    unregister_chrdev_region(test_device->n_dev, n_num); 
    devm_kfree(&pdev->dev, test_device);
    return 0;
}  

static const struct of_device_id of_dev_match[] = {
    { .compatible = "device_node", .data = NULL},
    {},
};
static struct platform_driver dev_driver = {
    .probe  = probe,
    .remove = remove,
    .driver = {
        .name   = "dev_driver",
        .of_match_table = of_dev_match,
    },
};

module_platform_driver(dev_driver);
MODULE_LICENSE("GPL");  

/* 應(yīng)用代碼 */
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <strings.h>

#define MM_SIZE 4096

int main(int argc, char* argv[])
{
    int dev_fd = 0;
    char* buf_addr = NULL;
    char buffer[MM_SIZE] = {0};

    dev_fd = open("/dev/test_dev", O_RDWR);//必須設(shè)置為O_RDWR,不然無法使用MAP_SHARED映射標(biāo)志
    if(-1 == dev_fd)
    {
        printf("open device error\n");
        return -1;
    }

    buf_addr = mmap(NULL, MM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, 0) + 16;
    
    strcpy(buf_addr, "read_device_mmap");
    bzero(buffer, MM_SIZE);
    int ret = read(dev_fd, buffer, MM_SIZE);
    printf("read buffer: %s\n", buffer);

    
    bzero(buffer, MM_SIZE);
    write(dev_fd, buffer, MM_SIZE);
    write(dev_fd, "write_device_mmap", strlen("write_device_mmap"));
    printf("write buffer: %s\n", buf_addr);

    munmap(buf_addr, MM_SIZE);
}

3.2 參考鏈接

mm_struct
認(rèn)真分析mmap:是什么 為什么 怎么用
linux內(nèi)核線程的問題
CPU與內(nèi)存互聯(lián)的架構(gòu)演變
Linux系統(tǒng)調(diào)用--mmap/munmap函數(shù)詳解
linux用戶態(tài)和kernel之間共享內(nèi)存
io_remap_pfn_range
linux內(nèi)存管理 之 內(nèi)存節(jié)點(diǎn)和內(nèi)存分區(qū)
linux內(nèi)存源碼分析 - SLAB分配器概述
Linux內(nèi)存管理 (5)slab分配器
Linux slab 分配器剖析
slab為什么要進(jìn)行著色處理
slab著色區(qū)的作用
SLUB和SLAB的區(qū)別
Linux驅(qū)動(dòng)mmap內(nèi)存映射

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容