Chapter 13 MMAP與DMA.

Slides:



Advertisements
Similar presentations
第一單元 建立java 程式.
Advertisements

LinkIt ONE開發板的簡介.
Chapter 15 MMAP與DMA 許名宏.
校園網路管理實電務 電子計算機中心 謝進利.
Linux File System Li-Shien Chen.
陳維魁 博士 儒林圖書公司 第九章 資料抽象化 陳維魁 博士 儒林圖書公司.
Project 2 JMVC code tracing
主題五 CPU Learning Lab.
題目:十六對一多工器 姓名:李國豪 學號:B
Chapter 5 迴圈.
程式設計概論 1.1 程式設計概論 程式語言的演進 物件導向程式 程式開發流程 1.2 C++開發工具
TCP協定 (傳輸層).
Q101 在701 SDX Linux上的標準安裝與使用程序v2
第一篇 Unix/Linux 操作介面 第 1 章 Unix/Linux 系統概論 第 2 章 開始使用 Unix/Linux
JDK 安裝教學 (for Win7) Soochow University
第1章 認識Arduino.
Chapter 15 MMAP與DMA.
101北一女中 資訊選手培訓營 妳不可不了解的指標 Nan.
第二章 LINUX存储管理 LINUX的分页管理机制.
類別(class) 類別class與物件object.
SQL Stored Procedure SQL 預存程序.
CH.8 硬體管理.
R教學 安裝RStudio 羅琪老師.
ASP.NET基本設計與操作 建國科技大學 資管系 饒瑞佶 2007年.
TCP/IP介紹 講師:陳育良 2018/12/28.
2017 Operating Systems 作業系統實習 助教:陳主恩、林欣穎 實驗室:720A.
雲端運算的基石(2) 虛擬化技術實作(XP篇─上)
檔案與磁碟的基本介紹.
Java 程式設計 講師:FrankLin.
Ch9 Communicating with Hardware
Chap3 Linked List 鏈結串列.
網路安全技術 OSI七層 學生:A 郭瀝婷 指導教授:梁明章.
Chapter 13 MMAP與DMA.
Topic Introduction—RMI
第一單元 建立java 程式.
Ch20. 計算器 (Mac 版本).
網路安全管理報告 緩衝區溢位攻擊 學生:吳忠祐 指導教授:梁明章.
Chapter 7 掌控記憶體.
緩衝區溢位攻擊 學生:A 羅以豪 教授:梁明章
SOCKET( ).
C qsort.
Chapter 7 掌控記憶體.
DRC with Calibre 課程名稱:VLSI 報告人:黃家洋 日期: 改版(蔡秉均) 1.
流程控制:Switch-Case 94學年度第一學期‧資訊教育 東海大學物理系.
MiRanda Java Interface v1.0的使用方法
陣列與結構.
基本指令.
Chapter 15 檔案存取 LabVIEW中的檔案存取函數也可將程式中的資料儲存成Excel或Word檔。只要將欲存取的檔案路徑位址透過LabVIEW中的路徑元件告訴檔案存取函數後,LabVIEW便可將資料存成Excel或Word檔;當然也可以將Excel或Word檔的資料讀入LabVIEW的程式中。
北一女中 資訊選手培訓營 妳不可不了解的指標 Nan.
Cloud Operating System - Unit 03: 雲端平台建構實驗
2018 Operating Systems 作業系統實習 助教:林欣穎 實驗室:720A.
1757: Secret Chamber at Mount Rushmore
資料表示方法 資料儲存單位.
資料結構與C++程式設計進階 期末考 講師:林業峻 CSIE, NTU 7/ 15, 2010.
第四章 陣列、指標與參考 4-1 物件陣列 4-2 使用物件指標 4-3 this指標 4-4 new 與 delete
作業系統實習課(二) -Scheduler-Related System Calls-
中国科学技术大学计算机系 陈香兰(0551- ) Spring 2009
獨孤派作業系統 main memory 中正大學 作業系統實驗室 指導教授:羅習五.
ABAP Basic Concept (2) 運算子 控制式與迴圈 Subroutines Event Block
String類別 在C語言中提供兩種支援字串的方式 可以使用傳統以null結尾的字元陣列 使用string類別
NFC (近場通訊, Near Field Communication) 靜宜大學資管系 楊子青
SQLite資料庫 靜宜大學資管系 楊子青.
Chapter 4 Multi-Threads (多執行緒).
快取映射 之直接對映 計算整理.
Unix指令4-文字編輯與程式撰寫.
Develop and Build Drives by Visual C++ IDE
Lab#9 Serial Port 實驗.
Department of Computer Information Science, NCTU
ABAP Basic Concept (2) 運算子 控制式與迴圈 Subroutines Event Block
Presentation transcript:

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…等等*/ }