Chapter 13 MMAP與DMA
13.1 Linux的記憶體管理 主要是描述用於控管記憶體的各種資料結構,相當冗長.有了必要的基礎知識後,我們就可以開始使用這些結構.
13.1.1 位址的分類(1/4) 作業系統的分類上,Linux是一種虛擬記憶系統. 虛擬記憶體系統將邏輯世界(軟體)與現實世界(硬體)分隔開來,最大的好處是軟體可配置的空間超過RAM的實際容量. 另一項優點是核心可在執行期改變行程的部分記憶空間. Linux系統上不只有兩種位址,而且每種位址都有其特殊用途. 但核心原始程式裡沒有明確定義何種位只適用何種情況,所以必須相當謹慎小心.
13.1.1 位址的分類(2/4)
13.1.1 位址的分類(3/4) 使用者虛擬位址(User Virtual Address) 簡稱為虛擬位址,位址寬度隨CPU架構而定 實體位址(Physical Address) 位址匯流排上的位址,寬度依CPU而定,但不一定與暫存器相符 匯流排位址(Bus Address) 用於週邊匯流排與記憶體的位址,具有高度的平台依存性 核心邏輯位址(Kernel Logical Address) 與實體位址只差距幾段固定偏移量,通常存放在unsigned long或void *型別變數上. kmalloc() 核心虛擬位址(Kernel Virtual Address) 與實體位址不一定有直接對應關係,通常存放在指標變數中. vmalloc()
13.1.1 位址的分類(4/4) <asm/page.h>定義了兩個可換算位址的巨集. __va()可將實體位址換算回邏輯位址,但僅限於低畫分區的實體位址才有效,因為高畫分區沒有邏輯位址. 不同的核心函式,需要不同類型的位址.如果各種位址都有不同的C型別,程式師就可明確知道何種情況該用何種位址.然而,我們並沒有如此幸運,所以認命吧.
13.1.2 高低劃分區 核心邏輯位址與核心虛擬位址之間的差異,再配備超大量記憶體的32-bits系統上才凸顯出來. 低畫分區(Low memory) 在kernel-space裡可用邏輯位址來定位的記憶體 高畫分區(High memory) 沒有邏輯位址的記憶體,因為安裝超過定址範圍的實體記憶體. 高低區之間的分界線,是核心在開機期間依據BIOS提供的資訊來決定的.在i386系統,分界通常位於1GB以下.這是核心自己設下的限制,因為核心必須將32-bit位址空間劃分成kernel-space與user-space兩大部份.
13.1.3 記憶體對應表與struct page(1/2) page結構紀錄了關於實體記憶頁的一切資訊.系統上的每一頁記憶體,都有一個專屬的struct page,幾個重要欄位如下. atomic_t count; 此記憶頁的用量計次.當降為0時,會被釋放回自由串列. wait_queue_head_t wait; 正在等待此記憶頁的所有行程. void *virtual; 本記憶頁對應的核心虛擬位址;若無(高劃分)則指向NULL. unsigned long flags; 一組描述記憶頁狀態的位元旗標.如PG_locked、PG_reserved.
13.1.3 記憶體對應表與struct page(2/2) 為了方便在struct page指標與虛擬位址之間轉換,Linux定義了一組方便的函式與巨集: struct page *virt_to_page(void *kaddr); 將核心邏輯位址轉換成對應的struct page指標. void *page_address(struct page *page); 傳回指定的page的核心虛擬位址.高劃分記憶頁除非已事先映射到虛擬位址空間,否則沒有虛擬位址. #include <linux/highmem.h> void *kmap(struct page *page); void kunmap(struct page *page); kmap()可傳回系統上任何記憶頁的核心虛擬位址. 如果分頁表剛好沒有空位,kmap()有可能會休眠.
13.1.4 分頁表(1/7) 每當程式用到一個虛擬位址,CPU必須先將它轉換成實體位址,然後才能存取實體記憶體. 轉換過程中,虛擬位址被拆成幾個位元欄,每個位元欄分別被當成不同陣列的索引,這些陣列就稱為分頁表. 不管在何種平台上,Linux統一使用三層分頁表,是為了讓位址範圍能被稀疏分布,即使硬體只支援兩層,或是另有特殊的虛擬-實體位址對應法. 一致的三層式架構,使得Linux核心成是不必寫一大堆#ifdef敘述,就可以同時支援兩層與三層式處理器. 在只提供兩層分頁表的硬體上,多出來的中間層會被編譯器予以“最佳化”,所以不會造成額外負擔.
13.1.4 分頁表(3/7) 頂層頁目錄(Page Directory, PGD) 第一層的分頁表.PGD是一個由pgd_t構成的陣列,每一個pgd_t各自指向一個第二層的分頁表. 中層頁目錄(Page Mid-level Directory, PMD) 第二層的分頁表.PMD是一個由pmd_t構成的陣列,每個pmd_t都是指向第三層分頁表的指標.在只有兩層分頁表的處理器上,由於缺乏實體上的PMD,所以其PMD被宣告成只有一個pmd_t的陣列,而這唯一的pmd_t指標是指向PMD自己. 分頁表(Page Table) 第三層的分頁表.為一個由分頁表項目(Page Table Entry, PTE)所構成的陣列,核心使用pte_t型別來表示分頁表項目,pte_t的直就是資料頁的實體位址.
13.1.4 分頁表(4/7) 對於各種硬體平台在記憶體管理機制上的差異,Linux以巧妙的安排來解決這個問題:將整個記憶體管理系統分為兩個部份,低階部份負責設定硬體的分頁機制,高階部分以一致的三層是分頁表來管理位址空間. 硬體上的差異,全部都隱藏在低階部份,這部份的程式必須按照平台的特性來寫,所以各種系統都不太一樣,但它們都呈現一致的三層式分頁表存在,而不必理會硬體上的差異. Linux以軟體手法模擬出來的三層式分頁表,可用<asm/page.h>和<asm/pgtable.h>所定義的一組符號來存取:
13.1.4 分頁表(5/7) PTRS_PER_PGD PTRS_PER_PMD PTRS_PER_PTE unsigned pdg_val(pgd_t pgd); unsigned pmg_val(pmd_t pmd); unsigned pte_val(pte_t pte); 這些巨集用於取得特定型別項目的unsigned值.pgd_t、pmd_t、pte_t的實際型別,隨底層硬體與核心組態而定.
13.1.4 分頁表(6/7) pgd_t *pgd_offset(struct mm_struct *mm, unsigned long address); pmd_t *pmd_offset(pgd_t *dir, unsigned long address); pte_t *pte_offset(pmd_t *dir, unsigned long address); 這些內插函式用於取得address所關聯的pgd、pmd和pte項目. 對於user-space的目前行程,此指標關聯的記憶對應表(memory map)是current->mm;在kernel-space則是以&init_mm來描述此指標. 在只有兩層分頁表的系統,pmd_offset(dir,add)被定義成(pmd_t *)dir,也就是將pmd“翻蓋”在pgd之上.
13.1.4 分頁表(7/7) struct page *pte_page(pte_t pte) 找出pte所代表的struct page,並傳回該結構的指標.處理分頁表的程式通常使用pte_page(),而非pte_val(),因為pte_page()能處理分頁表項目在處理器上的實際格式,並傳回我們通常想要的struct page指標. pte_present(pte_t pte) 此巨集傳回一個邏輯值,表示pte所指的記憶頁目前是否在主記憶體上.但分頁表本身必定留在主記憶體裡,如此可以簡化核心程式的寫作. 身為驅動程式設計者的你,大略知道如何管理記憶頁就夠了,因為需要自己處理分頁表的機會並不多.詳情請見include/asm/和mm/目錄之下.
13.1.5 虛擬記憶區(Virtual Memory Areas)(1/6) 核心需要一個較高層級的機制,才能處理行程所見到的記憶體佈局.在Linux,這機制稱為虛擬記憶區(virtual memory areas),通常簡稱為區域或VMA. 行程的記憶對應表,由下列區域構成: 一個存放程式碼(executable binary)的區域.通常稱為text. 一個存放資料的區域.包括有初值資料,沒初值資料以及堆疊. 每一個有效的對應關係(memory mapping),各有一個區域.
13.1.5 虛擬記憶區(Virtual Memory Areas)(2/6) 特定行程的各個VMA,可從/proc/pid/maps看到. 各欄位的格式如下: start-end perm offset major:minor inode imagename [root@sip root]# cat /proc/1/maps 08048000-0804e000 r-xp 00000000 03:02 405289 /sbin/init # 程式區(text) 0804e000-0804f000 rw-p 00006000 03:02 405289 /sbin/init # 資料區(data) 0804f000-08052000 rwxp 00000000 00:00 0 # bss(映射到page0) 40000000-40015000 r-xp 00000000 03:02 1149683 /lib/ld-2.3.2.so # test 40015000-40016000 rw-p 00014000 03:02 1149683 /lib/ld-2.3.2.so # data 40016000-40017000 rw-p 00000000 00:00 0 # ld.so 的 bss 42000000-4212e000 r-xp 00000000 03:02 809632 /lib/tls/libc-2.3.2.so # text 4212e000-42131000 rw-p 0012e000 03:02 809632 /lib/tls/libc-2.3.2.so # data 42131000-42133000 rw-p 00000000 00:00 0 # libc.si的bss bfffe000-c0000000 rwxp fffff000 00:00 0 # 堆疊區(映射到page 0)
13.1.5 虛擬記憶區(Virtual Memory Areas)(3/6) 上面每一欄除了imagename之外,都分別對應到struct vm_area_struct裡的欄位,這些欄位意義如下: start-end VMA前後邊界的虛擬位址 perm VMA的存取位元遮罩 offset 檔案從何處開始映射到此VMA的起點 major:minor 映射檔案所在裝置(磁碟,分割)的主次編號 inode 被映射檔案的inode編號 imagename 被映射檔案(通常是可執行檔)的名稱 要實作mmap作業方法的驅動程式,必須填寫一個VMA結構,放在要求映射裝置的行程的位址空間裡.
13.1.5 虛擬記憶區(Virtual Memory Areas)(4/6) 我們看看struct vm_area_struct(定義在<linux/mm.h>)裡幾個最重要的欄位(很相似/proc/*/maps),因為驅動程式的mmap作業方法可能會需要用到這些欄位. 驅動程式不能任意建立新的VMA,否則會破壞整個組織(串列與樹狀). unsigned long vm_start; unsigned long vm_end; 此VMA涵蓋的虛擬位址範圍. struct file *vm_file; 如果有檔案關聯到此區域,則vm_file指向該檔案的struct file結構.
13.1.5 虛擬記憶區(Virtual Memory Areas)(5/6) unsigned long vm_pgoff; 此區域在檔案的相對位置(以page為單位). unsigned long vm_flags; 一組描述VMA屬性的旗標.VM_IO表示此VMA映射到I/O region,以及避免VMA被包含在行程的code dump裡.VM_RESERVED要求記憶體管理系統不要將此VMA交換到磁碟上. struct vm_operations_struct *vm_ops; 一組可供核心用來操作VMA的函式,當成一種物件來看待. void *vm_private_data; 供驅動程式用於儲存私有資訊的欄位.
13.1.5 虛擬記憶區(Virtual Memory Areas)(6/6) vm_operations_struct它紀錄了處理行程記憶體所需的三項作業方法:open、close與nopage如下所述. void (*open)(struct vm_area_struct *area); 初始VMA 調整用量計次...等. void (*close)(struct vm_area_struct *area); 當VMA被摧毀,核心會呼叫它的close作業方法. struct page *(*nopage)(struct vm_area_struct *vma,insigned long address,int write_access); 行程試圖讀取某個有效的VMA記憶頁,但不在主記憶體裡,通常會從磁碟上的交換區讀回記憶頁內容,然後傳回一個指向實體記憶頁的struct page指標.若沒定義方法,則核心會配置一個空的記憶頁.write_access:非零值代表該記憶頁只能由目前行程擁有,而0意味著可容許共享.
13.2 mmap作業方法(1/2) 就驅動程式的觀點而言,記憶映射可用來提供直接存取裝置記憶體的能力給user-space應用程式. 觀察X Window System server的VMA如何映射到/dev/mem,有助於理解mmap()系統呼叫的典型用法. 第一組VMA映射到fe2fc000,此段範圍事實上是PCI顯示卡上的一段I/O memory,用於控制該介面卡. 第二組VMA映射到000a0000,也就是視訊記憶體在640Kb ISA hole的標準位址. 最後一組VMA映射到f4000000,此對為視訊記憶體(8MB)本身. cat /proc/731/maps 08048000-08327000 r-xp 00000000 08:01 55505 /usr/X11R6/bin/XF86_SVGA 08327000-08369000 rw-p 002de000 08:01 55505 /usr/X11R6/bin/XF86_SVGA 40015000-40019000 rw-s fe2fc000 08:01 10778 /dev/mem 40131000-40141000 rw-s 000a0000 08:01 10778 /dev/mem 40141000-40941000 rw-s f4000000 08:01 10778 /dev/mem
13.2 mmap作業方法(2/2) 由於X server時常需要傳輸大量資料到視訊記憶體,如果使用傳統的lseek()、write()勢必引發相當頻繁的環境切換,傳輸效率當然就很差勁;如果將視訊記憶體直接映射到user-space,則應用程式可以直接填寫視訊記憶體,所以傳輸效率得以大幅提升. mmap作業方法屬於file_operations結構的一部分,由mmap()系統呼叫觸發. void *mmap(void *start,size_t length,int port,int flags,int fd,off_t offset); int (*mmap)(struct filp *filp,struct vm_area_struct *vma); 有兩中方法可以製作分頁表:全部交給remap_page_ranfe()函式一次搞定.或者透過VMA的nopage作業方法,在VMA被存取時,才一次處理一頁.
13.2.1 使用remap_page_range() 要將某段虛擬位址映射到某段實體位址,必須另外產生新的分頁表,這個任務就交給它來完成. int remap_page_range(unsigned long virt_add,unsigned long phys_add,unsigned long size,pgprot_t port); 映射成功傳回0,失敗傳回錯誤碼 virt_add 要被重新映射的虛擬位址 ~ virt_add+size phys_add 所要對應的實體位址 ~ phys_add+size size 映射區規模(byte為單位) prot VMA的保護方式.驅程能使用vma->vm_page_port找到的值.
13.2.2 簡單的mmap實作 #include <linux/mm.h> int simple_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; if (remap_page_range(vma->vm_start, offset, vma->vm_end-vma->vm_start, vma->vm_page_prot)) return -EAGAIN; return 0; }
13.2.3 增添新的VMA作業方法 void simple_vma_open(struct vm_area_struct *vma) { MOD_INC_USE_COUNT; } void simple_vma_close(struct vm_area_struct *vma) { MOD_DEC_USE_COUNT; } static struct vm_operations_struct simple_remap_vm_ops = { open: simple_vma_open, close: simple_vma_close, }; int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma) { unsigned long offset = VMA_OFFSET(vma); //版本差異 byte page if (offset >= _pa(high_memory) || (filp->f_flags & O_SYNC)) vma->vm_flags |= VM_IO; vma->vm_flags |= VM_RESERVED; if (remap_page_range(vma->vm_start, offset, vma->vm_end-vma->vm_start, vma->vm_page_prot)) return -EAGAIN; vma->vm_ops = &simple_remap_vm_ops; simple_vma_open(vma); return 0; }
13.2.4 使用nopage映射記憶體(1/3) 雖然remap_page_range()已經夠用了,但偶爾會需要多一點彈性.對於這類情況,VMA的nopage作業方法或許是比較理想的選擇. 適合使用nopage作業方法來映射位址空間的情況之一,是應用程式可能發出mremap()系統呼叫的時候.此系統呼叫的作用是改變映射區的束縛位址. 如果映射區範圍縮減,驅動程式的unmap作業方法確實會收到通知,但如果是範圍擴張,則不會發生任何callback動作. 之所以不讓驅動程式收到映射區擴張通知,是因為記憶體被實際應用之前,沒有處理的必要,而當真的有必要時,核心可觸發nopage來處理.
13.2.4 使用nopage映射記憶體(2/3) struct page *simple_vma_nopage(struct vm_area_struct *vma, unsigned long address, int write_access) { struct page *pageptr; unsigned long physaddr = address - vma->vm_start + VMA_OFFSET(vma); pageptr = virt_to_page(_va(physaddr)); get_page(pageptr); //遞增用量計次 return pageptr; } int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma) { unsigned long offset = VMA_OFFSET(vma); if (offset >= __pa(high_memory) || (filp->f_flags & O_SYNC)) vma->vm_flags |= VM_IO; vma->vm_flags |= VM_RESERVED; vma->vm_ops = &simple_nopage_vm_ops; //不同處 simple_vma_open(vma); return 0;
13.2.4 使用nopage映射記憶體(3/3) 如果不實作nopage作業方法(讓simple_nopage_vm_ops的nopage欄位等於NULL),核心裡負責處理分頁失誤的程式,會將第零頁映射到造成失誤的虛擬位址. 若行程發出mremap()來擴張一個映射區,而沒提供作業方法,結果會映射到第零頁,而不會造成segmentation fault. nopage作業方法通常會傳回一個struct page的指標.如果有任何原因無法達成要求(要求位址超過裝置記憶區),則應傳回NOPAGE_SIGBUS來表示發生錯誤,或者傳回NOPAGE_OOM來表示資源限制而發生的錯誤. 使用nopage的mmap可以用來映射ISA記憶體,但對PCI匯流排則無效.對於PCI裝置上的記憶體,你應該使用remap_page_range().
13.2.5 重新映射特定I/O區(1/2) 如果只想將整段位址中的一小段映射到user-space,驅動程式必須自己處理偏移位置(offset). 例如,若要將實體位置simple_region_start開始的simple_region_size個位元組映射到user-space: unsigned long off = vma->vm_pgoff << PAGE_SHIFT; unsigned long physical = simple_region_start + off; unsigned long vsize = vma->vm_end - vma->vm_start; unsigned long psize = simple_region_size - off; if (vsize > psize) return -EINVAL; //跨越範圍太大 remap_page_range(vma_>vm_start, physical, vsize, vma->vm_page_prot);
13.2.5 重新映射特定I/O區(2/2) 要避免映射範圍擴張,最簡單的辦法是時作一個簡單的nopage作業方法,讓它回覆一個SIGBUS信號給發生失誤的行程.例如: struct page *simple_nopage(struct vm_area_struct *vma, unsigned long address, int write_access) { return NOPAGE_SIGBUS; /* send a SIGBUS */}
13.2.6 重新映射RAM 如果要適度容許映射擴張,比較完善的做法,是檢查引發分頁失誤的位址,是否在有效的實體範圍內,如果是,才容許映射. remap_page_range()有一樣值得玩味的限制:只有保留頁,以及在實體記憶體(RAM)頂端之上的與實體位址,它才有作用.保留頁被鎖在記憶體裡(不會被換出到磁碟上),所以可以安全地映射到user-space;這項限制式系統穩定度的基本要求. 由於remap_page_range()沒有處理RAM的能力,這表示類似scullp那樣的裝置將難以作出自己的mmap,因為其裝置記憶體是一般的RAM而非I/O memory.幸好,可以使用nopage作業方法.
13.2.6.1 使用nopage重新映射RAM(1/6) 先看看有哪些設計抉擇會影響scullp的mmap: 在裝置被映射之後,scullp就不釋放其裝置記體,而且不能像scull或類似裝置那樣,在被開啟成write模式時,裝置長度就被截為0.要避免釋放已映射的裝置,驅程必須自己計算有效的映射次數,scullp_device結構中的vmas欄位,可當此用途來用. 只有在scullp的order參數值為0,才容許映射記憶體.因為get_free_pages()和free_pages()只修改串列中第一個空頁計次值. 要遵循上述規則來映射RAM的程式,需要實作出open、close和nopage,而且還必須存取記憶對應表,調整記憶頁的用量計次.
13.2.6.1 使用nopage重新映射RAM(2/6) int scullp_mmap(struct file *filp, struct vm_area_struct *vma) { struct inode *inode = INODE_FROM_F(filp); /* 如果order不等於0,則拒絕映射 */ if (scullp_devices[MINOR(inode->i_rdev)].order) return -ENODEV; /* 這裡不作任何事.交給“nopage”搞定 */ vma->vm_ops = &scullp_vm_ops; vma->vm_flags |= VM_RESERVED; vma->vm_private_data = scullp_devices + MINOR(inode->i_rdev); scullp_vma_open(vma); return 0; }
13.2.6.1 使用nopage重新映射RAM(3/6) void scullp_vma_open(struct vm_area_struct *vma) { ScullP_Dev *dev = scullp_vma_to_dev(vma); dev->vmas++; MOD_INC_USE_COUNT; } void scullp_vma_close(struct vm_area_struct *vma) dev->vmas--; MOD_DEC_USE_COUNT;
13.2.6.1 使用nopage重新映射RAM(4/6) struct page *scullp_vma_nopage(struct vm_area_struct *vma, unsigned long address, int write) { unsigned long offset; ScullP_Dev *ptr, *dev = scullp_vma_to_dev(vma); struct page *page = NOPAGE_SIGBUS; void *pageptr = NULL; /* 預設為從缺 */ down(&dev->sem); offset = (address - vma->vm_start) + VMA_OFFSET(vma); if (offset >= dev->size) goto out; /* 超出範圍 */ /* 從串列裡取出scullp裝置,然後是記憶頁. 如果裝置有空洞,當process在存取空洞時,會收到一個SIGBUS信號 */
13.2.6.1 使用nopage重新映射RAM(5/6) offset >>= PAGE_SHIFT; /* offset 是頁數 */ for (ptr = dev; ptr && offset >= dev->qset;) { ptr = ptr->next; offset -= dev->qset; } if (ptr && ptr->data) pageptr = ptr->data[offset]; if (!pageptr) goto out; /* 空洞或檔尾 */ page = virt_to_page(pageptr); /* 找到了,可以遞增計次值 */ get_page(page); out: up(&dev->sem); return page;
13.2.6.1 使用nopage重新映射RAM(6/6) [root@sip scullp]# ls -l /dev > /dev/scullp [root@sip scullp]# ../misc-progs/mapper /dev/scullp 0 140 mapped "/dev/scullp" from 0 to 140 total 232 crw------- 1 root root 10, 10 Jan 30 2003 adbmouse crw-r--r-- 1 root root 10, 175 Jan 30 2003 agpgart [root@sip scullp]# ../misc-progs/mapper /dev/scullp 8192 200 mapped "/dev/scullp" from 8192 to 8392 h1494 brw-rw---- 1 root floppy 2, 92 Jan 30 2003 fd0h1660 brw-rw---- 1 root floppy 2, 20 Jan 30 2003 fd0h360 brw-rw---- 1 root floppy 2, 12 Jan 30 2003 fd0H360
13.2.7 重新映射虛擬位址(1/2) 記住,只有vmalloc()或kmap()函式傳回的位址,才是真正的虛擬位址,也就是說虛擬位址是透過核心分頁表映射而來的. pgd_t *pgd; pmd_t *pmd; pte_t *pte; unsigned long lpage; /* 經過scullv查表後,page現在是目前行程所需的記憶頁的位址,由於page是vmalloc()傳回的位址,所以要先從分頁表取得要被查詢的unsigned long值*/ lpage = VMALLOC_VMADDR(pageptr); spin_lock(&init_mm.page_table_lock); pgd = pgd_offset(&init_mm, lpage); pmd = pmd_offset(pgd, lpage); pte = pte_offset(pmd, lpage); page = pte_page(*pte); spin_unlock(&init_mm.page_table_lock); //到手,可以遞增計次值 get_page(page); out: up(&dev->sem); return page;
13.2.7 重新映射虛擬位址(2/2) 被查詢的記憶對應表,是存放在kernel-space的一個記憶結構:init_mm.注意到scullv必須先取得page_table_lock,然後才能開始查閱分頁表. VMALLOC_VMADDR(pageptr)巨集可從一個vmalloc()傳回的位址,傳回一個可用於查詢分頁表的unsigned long值. 因此,可能會想要將ioremap傳回的位址映射到user-space.你可直接使用remap_page_range()來達成,而不必另外物VMA實作nopage作業方法. 所以,remap_page_range()已經有能力產生新分頁表來將I/O memory映射到user-space.
13.3 kiobuf (kernel I/O buffer)介面 但是這些功能主要2.4核心用於將user-space buffer映射到kernel-space. 必須引入<linux/iobuf.h>,此檔案定義了kiobuf介面的心臟—struct kiobuf,此結構描述構成一次I/O作業所涉及的一個page陣列.
13.3.1 kiobuf結構 int nr_pages; //記憶頁數量 int length; //緩衝區的資料量 int offset; //緩衝區第一個有效位元組的相對位置 struct page **maplist; //每一頁都有此結構陣列.主要介面的關鍵. void kiobuf_init(struct kiobuf *iobuf); //使用前必須初始 int alloc_kiovec(int nr,struct kiobuf **iovec); 通常它是整組配置的.傳回0為成功 void free_kiovec(int nr,struct kiobuf **);//還回系統 int lock_kiovec(int nr,struct kiobuf *iovec[],int wait); int unlock_kiovec(int nr,struct kiobuf *iovec[]); 鎖定及解開kiovec被映射的記憶頁. 用此函式鎖定kiovec是不必要的,因為kiobuf主要是應用在驅動程式.
13.3.2 User-Space緩衝區的映射與Raw I/O (1/5) 傳統Unix系統提供一個raw(原始)介面給某些裝置-特別是區塊裝置-使其能夠透過一個user-space buffer來直接進行I/O,而不必透過核心來傳輸資料. Raw I/O帶來的效能提升幅度,不見得能滿足每一個人的預期,所以驅動程式設計者不應該只是為了能夠raw I/O而強加它進入.一次的raw I/O的事前準備工作相當繁重,而且損失緩衝資料留在核心快取的優點. 區塊裝置的raw I/O,必須對齊磁區(sector)來進行,所以每次的傳輸資料量必須剛好是磁區大小的整數倍. # define SBULLR_SECTOR 512 /* 堅持此長度 */ # define SBULLR_SECTOR_MASK (SBULLR_SECTOR - 1) # define SBULLR_SECTOR_SHIFT 9
13.3.2 User-Space緩衝區的映射與Raw I/O (2/5) ssize_t sbullr_read(struct file *filp, char *buf, size_t size, loff_t *off) { Sbull_Dev *dev = sbull_devices + MINOR(filp->f_dentry->d_inode->i_rdev); return sbullr_transfer(dev, buf, size, off, READ); } ssize_t sbullr_write(struct file *filp, const char *buf, size_t size, loff_t *off) return sbullr_transfer(dev, (char *) buf, size, off, WRITE); sbullr_transfer()函式只處理事前準備與事後收尾的工作,真正的傳輸工作是交給另一個函式來執行.
static int sbullr_transfer (Sbull_Dev. dev, char static int sbullr_transfer (Sbull_Dev *dev, char *buf, size_t count, loff_t *offset, int rw) { struct kiobuf *iobuf; int result; /* 只容許對齊磁區,容量符合規定的區塊 */ if ((*offset & SBULLR_SECTOR_MASK) || (count & SBULLR_SECTOR_MASK)) return -EINVAL; if ((unsigned long) buf & SBULLR_SECTOR_MASK) /* 配置一個 I/O 向量 */ result = alloc_kiovec(1, &iobuf); if (result) return result; /* 映射 user I/O buffer 然後執行 I/O. */ result = map_user_kiobuf(rw,iobuf,(unsigned long)buf,count);//睡 if (result) { free_kiovec(1, &iobuf); return result; } spin_lock(&dev->lock); result = sbullr_rw_iovec(dev, iobuf, rw, *offset >> SBULLR_SECTOR_SHIFT, count >> SBULLR_SECTOR_SHIFT); spin_unlock(&dev->lock); /* 清除 然後返回 */ unmap_kiobuf(iobuf); free_kiovec(1, &iobuf); if (result > 0) *offset += result << SBULLR_SECTOR_SHIFT; return result << SBULLR_SECTOR_SHIFT; }
static int sbullr_rw_iovec(Sbull_Dev. dev, struct kiobuf static int sbullr_rw_iovec(Sbull_Dev *dev, struct kiobuf *iobuf, int rw, int sector, int nsectors) { struct request fakereq; struct page *page; int offset = iobuf->offset, ndone = 0, pageno, result; /* 以sector為傳輸單位 */ fakereq.sector = sector; fakereq.current_nr_sectors = 1; fakereq.cmd = rw; for (pageno = 0; pageno < iobuf->nr_pages; pageno++) { page = iobuf->maplist[pageno]; while (ndone < nsectors) { /* 虛構一個request結構操作*/ fakereq.buffer = (void *) (kmap(page) + offset); result = sbull_transfer(dev, &fakereq); kunmap(page); if (result == 0) return ndone; /* 下一個 */ ndone++; fakereq.sector++; offset += SBULLR_SECTOR; if (offset >= PAGE_SIZE) { offset = 0; break; } } return ndone;
13.3.2 User-Space緩衝區的映射與Raw I/O (5/5) 分別在sbullr與sbull作了一些簡單的資料傳輸測試,結果發現同樣的資料量下,sbullr所耗掉的系統時間大約只有sbull的三分之二. 節省下來的時間,是因為sbullr的資料不必另外抄寫到緩衝快取區.但反覆多次讀取相同資料,就沒有節省的效果了. 提供修補程式,使我們可以輕易地使用一個kiobuf將核心虛擬記憶映射到行程的位址空間,所以先前的nopage也就不必要了.
13.4 直接記憶體存取與匯流排主控 DMA是一種硬體機制,讓週邊元件可以直接與主記憶體交換I/O資料,而不必經過系統處理器. 主要重點放在PCI匯流排,因為它是目前最熱門、最普遍的週邊匯流排,而且其概念有廣泛的通適性.
13.4.1 DMA資料傳輸的流程 有兩種方式可觸發資料傳輸:軟體主動要求,或週邊硬體主動將資料推入(簡化討論,只考慮輸入方向). 第一種情況的步驟: 1.當行程發出一次read(),驅動程式的read作業方法就配置一塊DMA緩衝區,並指示週邊硬體開始傳輸資料.行程會進入休眠狀態. 2.週邊硬體將資料寫到DMA緩衝區,在完成傳輸之後,對CPU發出一次中斷訊號. 3.驅動程式的interrupt handler收下輸入資料、回應中斷、然後喚醒行程,讓行程讀走資料.
13.4.1 DMA資料傳輸的流程 第二種情況的步驟: 1.週邊硬體觸發一次中斷,讓系統處理器知道新資料已經到達. 2.驅動程式的interrupt handler配置一個緩衝區,並將該緩衝區的位置告訴週邊硬體,使其知道資料應該傳送到何處. 3.週邊硬體將資料寫入指定的緩衝區,在完成傳輸之後,觸發另一次中斷. 4.Interrupt handler分配新資料,喚醒任何相關行程,並處理一些例行工作.
13.4.1 DMA資料傳輸的流程 網路卡與CPU之間通常是透過主記憶體上的一塊環型緩衝區(稱為DMA ring buffer)互相交換資料. 每當網路卡從外界收到一個封包,就將它放入環型緩衝區裡的下一個空位,然後發出中斷通知. 驅動程式將網路封包傳給核心裡的其它部門,並將一個新的DMA空位放回環型緩衝區. 大多數驅動程式在初始期就預先配置好所需的緩衝區,並全程使用同一塊緩衝區,直到關閉時才予以釋放.
13.4.2 配置DMA緩衝區 並非所有記憶體都可以用來當成DMA緩衝區,因此,要配置一塊適合DMA的緩衝區,不是隨意配置一塊普通記憶體就了事. 對於有這種限制的裝置,應該使用來自DMA專區的記憶體.也就是說,在呼叫kmalloc()或get_free_pages()時,要加上__GFP_DMA旗標. 自助配置法: 需靠核心的開機期參數配合,如原有32M,當mem=31,之後可用dmabuf=ioremap(0x1f00000,0x100000)來存取保留的1M記憶體. 積極配置法: 呼叫kmalloc(GFP_ATOMIC)多次,當它失敗時,就等待核心釋出一些記憶體,然後再重新配置一次所有東西.
13.4.3 匯流排位址 具有DMA能力的週邊硬體,其實是使用匯流排位址,而非實體位址.在x86 PC上匯流排位址是等於實體位址,但有些平台的介面匯流排是透過橋接電路連接在一起,它們的I/O位址被映射到不同的實體位址. 在最底層,Linux核心提供一套通用的解決方案,也就是<asm/io.h>所定義的兩個函式: unsigned long virt_to_bus(volatile void * address); void *bus_to_virt(unsigned long address);
13.4.4 PCI匯流排上的DMA(1/2) 2.4版核心包含了一組有彈性的機制來支援PCI DMA–也稱為匯流排主控.此機制處理緩衝區配置的細節,如果bus支援多頁傳輸,它也可以幫你設定定bus硬體.在某些平台上,若緩衝區不位於有DMA能力的記憶區,此機制也會想辦法移位. 本節的函式需要一個代表目標裝置的struct pci_dev結構,關於PCI裝置的設定細節,請見第十五章. 要注意的是,這些函式其實也可以用在ISA裝置上,在這種情況下,只要將struct pci_dev指標引數設定為NULL即可. 使用下列函式的驅動程式都必須引入<linux/pci.h>.
13.4.4 PCI匯流排上的DMA(2/2) 有許多PCI裝置並沒有完整的32-bit匯流排位址空間,因為它們只是舊式ISA硬體的修改版本.Linux核心會嘗試使用這類裝置,但不保證一定可以. 如果你要的驅動裝置,恰好沒有完整的定只能力,則必須呼叫pci_dma_supported(): int pci_dma_supported(struct pci_dev *pdev,dma_addr_t mask); 若傳回非零值,表示目標裝置可在目前平台上執行DMA作業,之後需將pci_dev結構裡的dma_mask欄位設定成mask值. 2.4.3版核心還提供另一個新函式 – pci_set_dma_mask(),原型如下: int pci_set_dma_mask(struct pci_dev *pdev, dma_addr_t mask); 若給定的mask可以支援DMA,此函式會傳回零,並幫你設定好dma_mask欄位,否則傳回-EIO. 對於支援32-bit位址的裝置,就沒必要呼叫pci_dma_supportted().
13.4.4.1 DMA 對應 在PCI匯流排上的DMA對應備分成兩種類型,主要差別在於DMA緩衝區的存活時間長短.這兩種對映模式如下: 常態性DMA對應(consistent DMA mapping) 若DMA緩衝區的生命期與驅動程式一樣長,就稱為之.DMA緩衝區必須能夠同時被CPU與週邊使用,也應該被排除在快取機制之外. 臨時性DMA對應(streaming DMA mapping) 為了單次操作而臨時設置的DMA對應.基於兩項原因,核心團隊建議盡可能使用臨時性的對應模式.首先,只有在每次DMA對應時,才會使用bus上的一或多個對應暫存器,另一項是某些週邊硬體特地針對臨時性對應作了最佳化,而這些最佳化措施不能運用在常態性對應.
13.4.4.2 設定常態性DMA對應 驅動程式可呼叫pci_alloc_consistent()來設定一次常態性的DMA對應,此函式包辦了緩衝區的配置與對映工作. void *pci_alloc_consistent(struct pic_dev *pdev,size_t size,dma_addr_t *bus_addr); 在支援PCI的大部份平台,是以GFP_ATOMIC優先度來配置DMA緩衝區,所以此函式不會休眠. 當不再需要緩衝區時(卸載模組),就應該盡快使用pci_free_consistent()將緩衝區還給系統,此函式需同時提供CPU位址與匯流排位址. void pci_free_consistent(struct pci_dev *pdev,size_t size,void *cpu_addr,dma_addr_t bus_addr);
13.4.4.3 設定臨時性DMA對應(1/2) 設定臨時對應時,必須讓核心知道資料的移動方向. PCI_DMA_TODEVICE write() PCI_DMA_FROMDEVICE read() PCI_DMA_BIDIRECTIONAL both PCI_DMA_NONE debug 當你只有一個緩衝區要傳輸,可使用pci_map_single()來將該緩衝區映射到裝置位址空間. dma_addr_t pci_map_single(struct pci_dev *pdev,void *buffer,size_t size,int direction); 完成傳輸之後,應該立刻使用pci_unmap_single()來解除對映. void pci_unmap_single(struct pci_dev *pdev,dma_addr_t bus_addr,size_t size,int direction);
13.4.4.3 設定臨時性DMA對應(2/2) 臨時性對應必須遵守三點重要法則: 緩衝區的使用,必須符合映射時所設定的傳輸方向. 在緩衝區映射到匯流排位址之後,就屬於裝置,而非處理器.這意味著你必須先將要寫入裝置的資料放在緩衝區,然後才能映射它. 在DMA動作期間,不能解除對映,否則保證系統一定會嚴重不穩定. 為何驅動程式不能接觸已被對應的緩衝區?有兩項原因.第一,核心必須確保要放在DMA緩衝區的資料,已經確實全數寫入記憶體;第二,如果被映射的緩衝區位於週邊裝置無法存取的區域,某些平台會直接讓DMA作業失敗,而其它平台則可能會建立一個轉進緩衝區.
轉進緩衝區只是另一塊裝置可以存取的記憶區. 偶爾,驅動程式需要在解除對映前,先存取臨時DMA緩衝區的內容,可用以下函式. void pci_sync_single(struct pci_dev *pdev,dma_handle_t bus_addr,size_t size,int direction); 此函式呼叫時機,必須在處理器存取PCI_DMA_FROMDEVICE緩衝區之前,或是在存取了PCI_DMA_TODEVICE緩衝區之後.
13.4.4.4 一個簡單的PCI DMA範例 (1/2) 不同類型的目標裝置,可能有著天差地遠的操作程序. int dad_transfer(struct dad_dev *dev,int write,void *buffer, size_t count) { dma_addr_t bus_addr; unsigned long flags; /* 映射 DMA 緩衝區 */ dev->dma_dir=(write?PCI_DMA_TODEVICE:PCI_DMA_FROMDEVICE); dev->dma_size=count; bus_addr=pci_map_single(dev->pci_dev,buffer,count,dev->dma_dir); dev->dma_addr = bus_addr; /* 裝置設定 */ writeb(dev->registers.command, DAD_CMD_DISABLEDMA); writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD); writel(dev->registers.addr, cpu_to_le32(bus_addr)); writel(dev->registers.len, cpu_to_le32(count)); /* 開始傳輸 */ writeb(dev->registers.command, DAD_CMD_ENABLEDMA); return 0; }
13.4.4.4 一個簡單的PCI DMA範例 (2/2) 上頁函式先將要被傳輸的緩衝區映射到PCI匯流排,然後啟動目標裝置的DMA傳輸功能.另一半的工作是由中斷服務程序(ISR)負責的,類似下列函式: void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs) { struct dad_dev *dev = (struct dad_dev *) dev_id; /* 確定中斷真的是來自我們的目標裝置 */ /* 解除DMA緩衝區的對應 */ pci_unmap_single(dev->pci_dev, dev->dma_addr, dev->dma_size, dev->dma_dir); /*只有現在可安全存取緩衝區,將資料操寫到user-space…等等*/ }