標籤:des io os 使用 ar for 資料 sp art
- 簡介
- 許多類型的驅動程式編程都需要瞭解一些虛擬記憶體子系統如何工作的知識
- 當遇到更為複雜、效能要求更為苛刻的子系統時,本章所討論的內容遲早都要用到
- 本章的內容分成三個部分
- 講述mmap系統調用的實現過程
- 講述如何跨越邊界直接存取使用者空間的記憶體頁
- 講述了直接記憶體存取(DMA)I/O操作,它使得外設具有直接存取系統記憶體的能力
- Linux的記憶體管理
- 地址類型
- Linux是一個虛擬記憶體系統,這意味著使用者程式所使用的地址與硬體使用的物理地址是不等同的
- 有了虛擬記憶體,在系統中啟動並執行程式可以分配比實體記憶體更多的記憶體,甚至一個單獨的進程都能擁有比系統實體記憶體更多的虛擬位址空間
- 下面是一個Linux使用的地址類型列表
- 使用者虛擬位址
- 物理地址
- 匯流排地址
- 該地址在外圍匯流排和記憶體之間使用,通常它們與處理器使用的物理地址相同
- 核心邏輯地址
- 核心邏輯地址組成了核心的常規地址空間
- 在大多數體系架構中,邏輯地址和與其相關聯的物理地址不同,僅僅在它們之間存在一個固定的位移量
- kmalloc返回的記憶體就是核心邏輯地址
- 核心虛擬位址
- 和核心邏輯地址的相同之處在於,它們都將核心空間的地址映射到物理地址上
- 核心虛擬位址與物理地址的映射不必是線性一對一的
- 所有的邏輯地址都是核心虛擬位址,但是很多核心虛擬位址不是邏輯地址
- vmalloc分配的記憶體具有一個虛擬位址
- <asm/page.h>
- __pa()
- __va()
- 將物理地址逆向映射到邏輯地址,但這隻對低端記憶體頁有效
- 物理地址和頁
- 物理地址被分成離散的單元,稱之為頁
- <asm/page.h>
- 目前大多數系統都使用每頁4096個位元組
- 高端與低端記憶體
- 使用32位系統只能在4GB的記憶體中定址
- 核心將4GB的虛擬位址空間分割為使用者空間和核心空間,一個典型的分割是將3GB分配給使用者空間,1GB分配給核心空間
- 低端記憶體
- 高端記憶體
- 記憶體映射和頁結構
- <linux/mm.h>
- struct page
- atomic_t count;
- 對該頁的訪問計數。當計數值為0時,該頁將返回給空閑鏈表
- void *virtual;
- 如果頁面被映射,則指向頁的核心虛擬位址;如果未被映射則為NULL
- unsigned long flags;
- 描述頁狀態的一系列標誌
- PG_locked表示記憶體中的頁已經被鎖住
- PG_reserved表示禁止記憶體管理系統訪問該頁
- struct page *virt_to_page(void *kaddr);
- struct page *pfn_to_page(int pfn);
- void *page_address(struct page *page);
- <linux/highmem.h>
- <asm/kmap_types.h>
- void *kmap(struct page *page);
- 對於低端記憶體頁來說,返回頁的邏輯地址
- 對於高端記憶體,在專用的核心地址空間建立特殊的映射
- void kunmap(struct page *page);
- void *kmap_atomic(struct page *page, enum km_type type);
- void kunmap_atomic(void *addr, enum km_type type);
- 頁表
- 處理器必須使用某種機制同,將虛擬位址轉換為相應的物理地址,這種機制被稱為頁表
- 它基本上是一個多層樹形結構,結構化的資料中包含了虛擬位址到物理地址的映射和相關的標誌位
- 虛擬記憶體區
- 虛擬記憶體區(VMA)用於管理進程地址空間中不同地區的核心資料結構
- 進程的記憶體映射包含下面這些地區
- 程式的可執行代碼地區
- 多個資料區,其中包含初始化資料、非初始化資料以及程式堆棧
- 與每個活動的記憶體映射對應的地區
- /proc/<pid>/maps
- start-end perm offset major:minor inode image
- vm_area_struct結構
- <linux/mm.h>
- struct vm_area_struct
- unsigned long vm_start;
- unsigned long vm_end;
- struct file *vm_file;
- unsigned long vm_pgoff;
- unsigned long vm_flags;
- struct vm_operations_struct *vm_ops;
- void *vm_private_data;
- struct vm_operations_struct
- void (*open) (struct vm_area_struct *vma);
- void (*close) (struct vm_area_struct *vma);
- struct page *(*nopage) (struct vm_area_struct *vma, unsigned long address, int *type);
- int (*populate) (struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
- 記憶體映射處理
- <linux/sched.h>
- current->mm
- mmap裝置操作
- 記憶體映射可以提供給使用者程式直接存取裝置記憶體的能力
- 映射一個裝置意味著將使用者空間的一段記憶體與裝置記憶體關聯起來
- 像串口和其他面向流的裝置就不能進行mmap抽象
- 必須以PAGE_SIZE為單位進行映射
- mmap方法是file_operations結構的一部分
- mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
- int (*mmap) (struct file *filp, struct vm_area_struct *vma);
- 有兩種建立頁表的方法
- 使用remap_pfn_range函數一次全部建立
- 通過nopage VMA方法每次建立一個頁表
- 使用remap_pfn_range
- int rempa_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);
- int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long phys_addr, unsigned long size, pgprot_t prot);
- vma
- virt_addr
- pfn
- 與實體記憶體對應的頁幀號,虛擬記憶體將要被映射到該實體記憶體
- 頁幀號只是將物理地址右移PAGE_SHIFT位
- size
- prot
- 新VMA要求的“保護(protection)”屬性
- 一個簡單的實現
- drivers/char/mem.c
- remap_pfn_range(vma, vma->vm_start, vm_.vm_pgoff, vma->vm_end – vma->vm_start, vma->vm_page_prot)
- 為VMA添加操作
- struct vm_operations_struct simple_remap_vm_ops = {.open = simple_vma_open, .close = simple_vma_close,}
- 使用nopage映射記憶體
- 如果要支援mremap系統調用,就必須實現nopage函數
- struct page *(*nopage) (struct vm_area_struct *vma, unsigned long address, int *type);
- get_page(struct page *pageptr);
- static int simple_nopage_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))
- vm->vm_flags |= VM_RESERVED;
- vm->vm_ops = &simple_nopage_vm_ops;
- simple_vma_open(vma);
- return 0;
- }
- struct page *simple_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))
- pageptr = pfn_to_page(pageframe);
- get_page(pageptr);
- if (type)
- return pageptr;
- }
- 重新對應RAM
- 對remap_pfn_range函數的一個限制是:它只能訪問保留頁和超出實體記憶體的物理地址
- remap_pfn_range不允許重新對應常規地址
- 使用nopage方法重新對應RAM
- 使用vm_ops->nopage一次處理一個頁錯誤
- 重新對應核心虛擬位址
- page = vmalloc_to_page(pageptr);
- get_page(page);
- 執行直接I/O訪問
- 如果需要傳輸的資料量非常大,直接進行資料轉送,而不需要額外地從核心空間拷貝資料操作的參與,這將會大大提高速度
- 設定直接I/O的開銷非常巨大
- 使用直接I/O需要write系統調用同步執行
- 在每個寫操作完成之前不能停止應用程式
- <linux/mm.h>
- int get_user_pages(struct task_struct *tsk, struct mm_struct *mm, unsigned long start, int len, int write, int force, struct page **pages, struct vm-area_struct **vmas);
- tsk
- 指向執行I/O的任務指標,該參數幾乎是current
- mm
- 指向描述被映射地址空間的記憶體管理結構的指標
- 對驅動程式來說,該參數總是current->mm
- force
- 如果write非零,對映射的頁有寫入權限
- 驅動程式對該參數總是設定為0
- pages
- 如果調用成功,pages中包含了一個描述使用者空間緩衝區page結構的指標列表
- vmas
- 使用直接I/O的裝置通常使用DMA操作
- 一旦直接I/O操作完成,就必須釋放使用者記憶體頁
- <linux/page-flags.h>
- void SetPageDirty(struct page *page);
- void page_cache_release(struct page *page);
- 非同步I/O
- <linux/aio.h>
- ssize_t (*aio_read) (struct kiocb *iocb, char *buffer, size_t count, loff_t offset);
- ssize_t (*aio_write) (struct kiocb *iocb, const char *buffer, size_t count, loff_t offset);
- int (*aio_fsync) (struct kiocb *iocb, int datasync);
- int is_sync_kiocb(struct kiocb *iocb);
- int aio_complete(struct kiocb *iocb, long res, long res2);
- 直接記憶體存取
- DMA是一種硬體機制同,它允許外圍裝置和主記憶體之間直接傳輸它們的I/O資料,而不需要系統處理器的參與
- 使用這種機制可以大大提高與裝置通訊的輸送量
- DMA資料轉送概覽
- 有兩種方式引發資料轉送
- 軟體對資料的請求
- 當進程調用read,驅動程式函數分配一個DMA緩衝區,並讓硬體將資料轉送到這個緩衝區中,進程處於睡眠狀態
- 硬體將資料寫入到DMA緩衝區中,當寫入完畢,產生一個中斷
- 中斷處理常式獲得輸入的資料,應答中斷,並且喚醒進程,該進程現在即可讀取資料
- 硬體非同步地將資料傳遞給系統
- 硬體產生中斷,宣告新資料的到來
- 中斷處理常式分配一個緩衝區,並且告訴硬體向哪裡傳輸資料
- 外圍裝置將資料寫入緩衝區,完成後產生另外一個中斷
- 處理常式分發新資料,喚醒任何相關進程,然後執行清理工作
- 分配DMA緩衝區
- 使用DMA緩衝區的主要問題是:當大於一頁時,它們必須佔據串連的物理頁,這是因為裝置使用ISA或者PCI系統匯流排傳輸資料,而這兩種方式使用的都是物理地址
- DIY分配
- get_free_pages函數可以分配多達幾M位元組的記憶體,但是對較大數量的請求,甚至是遠少於128KB的請求也通常會失敗,這是因為此時系統記憶體中充滿了記憶體片段
- 當核心不能返回請求數量的記憶體或需要超過128KB記憶體時,除了返回-ENOMEM,另外一個方法是在引導時分配記憶體或是為緩衝區保留頂部物理RAM
- 還有一個方法是使用GFP_NOFAIL分配標誌來為緩衝區分配記憶體
- 匯流排地址
- 使用DMA的裝置驅動程式將與串連到匯流排介面上的硬體通訊,硬體使用的是物理地址,而程式碼使用的是虛擬位址
- <asm/io.h>
- unsigned long virt_to_bus(volatile void *address);
- void *bus_to_virt(unsigned long address);
- 通用DMA層
- 核心提供了一個與匯流排體系架構無關的DMA層
- <linux/dma-mapping.h>
- 處理複雜的硬體
- int dma_set_mask(struct device *dev, u64 mask);
- 該掩碼顯示與裝置能定址能力對應的位
- 如果dma_set_mask返回0,則對該裝置不能使用DMA
- DMA映射
- 一個DMA映射是要分配的DMA緩衝區與為該緩衝區產生的、裝置可訪問地址的組合
- DMA映射建立了一個新的結構類型——dma_addr_t來表示匯流排地址
- 根據DMA緩衝區期望保留的時間長短,PCI代碼區分兩種類型的DMA映射
- 一致性DMA映射
- 這種類型的映射存在於驅動程式生命週期中
- 一致性映射的緩衝區必須可同時被CPU和外圍裝置訪問
- 建立和使用一致性映射的開銷是很大的
- 流式DMA映射
- 通常為單獨的操作建立流式映射
- 核心開發人員建議盡量使用流式映射,然後再考慮一致性映射
- 在支援映射寄存器的系統中,每個DMA映射使用匯流排上的一個或者多個映射寄存器
- 在一些硬體中,流式映射可以被最佳化,但最佳化的方法對一致性映射無效
- 建立一致性DMA映射
- void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
- 返回值是緩衝區的核心虛擬位址
- 與其相關的匯流排地址,儲存在dma_handle中
- void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);
- DMA池
- DMA池是一個產生小型、一致性DMA映射的機制
- <linux/dmapool.h>
- struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);
- allocation不為零,表示記憶體邊界不能超越allocation
- void dma_pool_destroy(struct dma_pool *pool);
- void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);
- void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
- 建立流式DMA映射
- 當建立流式映射時,必須告訴核心資料流動的方向
- enum dma_data_direction
- DMA_TO_DEVICE
- DMA_FROM_DEVICE
- DMA_BIDIRECTIONAL
- DMA_NONE
- dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
- void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
- 有幾條非常重要的原則用於流式DMA映射
- 緩衝區只能用於這樣的傳送,即其傳送方向匹配於映射時給定的方向wfhg
- 一旦緩衝區被映射,它將屬於裝置,而不是處理器
- 在DMA處於活動期間內,不能撤銷對緩衝區映射,否則會嚴重破壞系統的穩定性
- void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
- void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
- 單頁流式映射
- dma_addr_t dma_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction direction);
- void dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction);
- 分散/聚焦映射
- 這是一種特殊的流式DMA映射
- 假設有幾個緩衝區,它們需要與裝置雙向傳輸資料
- 有幾種方式能產生這種情形
- 從raedv或者writev系統調用產生
- 從叢集的磁碟I/O請求產生
- 從映射的核心I/O緩衝區中的頁面鏈表產生
- 許多裝置都能接受一個指標數組的分散表,以及它的長度,然後在一次DMA操作中把它們全部傳輸走
- 映射分散表的第一步是建立並填充一個描述被傳送緩衝區的scatterlist結構的數組
- <linux/scatterlist.h>
- struct scatterlist
- struct page *page;
- unsigned int length;
- unsigned int offset;
- int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
- nents是傳入的分散表入口的數量
- 返回值是要傳送的DMA緩衝區數
- 驅動程式應該傳輸由dma_map_sg函數返回的每個緩衝區
- dma_addr_t sg_dma_address(struct scatterlist *sg);
- unsinged int sg_dma_len(struct scatterlist *sg);
- void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);
- nents一定是先前傳遞給dma_map_sg函數的入口項的數量
- void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
- void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
- PCI雙重地址周期映射
- 通常DMA支援層使用32位匯流排地址,其為裝置的DMA掩碼所約束
- PCI匯流排還支援64位地址模式,既雙重地址周期(DAC)
- 如果裝置需要使用放在高端記憶體的大塊緩衝區,可以考慮實現DAC支援
- <linux/pci.h>
- int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);
- dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);
- direction
- PCI_DMA_TODEVICE
- PCI_DMA_FROMDEVICE
- PCI_DMA_BIDIRECTIONAL
- void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);
- void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);
- ISA裝置的DMA
- ISA匯流排允許兩種DMA傳輸:本地(native)DMA和ISA匯流排控制(bus-master)DMA
- 本地DMA使用主板上的標準DMA控制器電路來驅動ISA匯流排上的訊號線
- ISA匯流排控制DMA完全由外圍裝置控制
- 有三種實現涉及到ISA匯流排上的DMA資料轉送
- 8237 DMA控制器(DMAC)
- 外圍裝置
- 裝置驅動程式
- 需要驅動程式完成的工作很少,它只是負責提供DMA控制器的方向、匯流排地址、傳輸量的大小等等
- 註冊DMA
- <asm/dma.h>
- int request_dma(unsigned int channel, const char *name);
- void free_dma(unsigned int channel);
- 與DMA控制器通訊
- unsigned long claim_dma_lock();
- 必須被裝入控制器的資訊包含三個部分:RAM的地址、必須被傳輸的原子項個數以及傳輸的方向
- void set_dma_mode(unsigned int channel, char mode);
- mode
- DMA_MODE_READ
- DMA_MODE_WRITE
- DMA_MODE_CASCADE
- void set_dma_addr(unsigned int channel, unsigned int addr);
- void set_dma_count(unsigned int channel, unsigned int count);
- void disable_dma(unsigned int channel);
- void enable_dma(unsigned int channel);
- int get_dma_residue(unsigned int channel);
- void clear_dma_ff(unsigned int channel);
《Linux Device Drivers》第十五章 記憶體映射和DMA——note