Chapter 15 MMAP與DMA 601430026 許名宏
15.1 Linux的記憶體管理 主要是描述用於控管記憶體的各種資料結構,相 當冗長。有了必要的基礎知識後,我們就可以開 始使用這些結構。
15.1.1 位址的分類(1/4) 作業系統的分類上,Linux是一種虛擬記憶系統。 虛擬記憶系統將邏輯世界(軟體)與現實世界(硬體)分隔開來, 最大的好處是軟體可配置的空間超過RAM的實際容量。 另一項優點是核心可在執行期間改變行程的部分記憶空間。 Linux系統上不只有兩種位址(虛擬、實體),而且每種位址 都有其特殊用途。但核心原始程式裡沒有明確定義何種位 址適用何種情況,所以必須相當謹慎小心。
15.1.1 位址的分類(2/4)
15.1.1 位址的分類(3/4) 使用者虛擬位址(User Virtual Address) 簡稱為虛擬位址,也就是 user-process 所見到的一般位址。虛擬位址寬度隨CPU架構而定。 實體位址(Physical Address) 用於CPU與系統記憶體之間的位址。寬度依CPU而定,但不一定與CPU暫存器的寬度相符。 匯流排位址(Bus Address) 用於周邊匯流排與記憶體的位址,具有高度的平台相依性。 核心邏輯位址(Kernel Logical Address) 這類位址構成核心的正常位址空間,他們對應到所有主記憶體,而且通常被當作實體位 址來使用。邏輯位址與實體位址只差距一段固定偏移量,通常存放在unsigned long或void *型別變數上。kmalloc()所傳回的記憶體,就是以邏輯位址來定位。 核心虛擬位址(Kernel Virtual Address) 核心虛擬位址跟邏輯位址不同之處,在於核心虛擬位址與實體位址不一定有直接對應關 係,虛擬位址通常存放在指標變數中。vmalloc()配置而來得記憶體位址是以虛擬位址來表 示。
15.1.1 位址的分類(4/4) <asm/page.h>定義了兩個可換算位址的巨集。 _ _va()可將實體位址換算回邏輯位址,但僅限於低記憶體 的實體位址才有效,因為高記憶體沒有邏輯位址。
15.1.2 高低記憶體 核心邏輯位址與核心虛擬位址之間的差異,在配備超大量 記憶體的32-bits系統上才凸顯出來。 低記憶體(Low memory) 存在於kernel-space裡,具有邏輯位址的記憶體。 高記憶體(High memory) 沒有邏輯位址的記憶體,因為超過了核心的虛擬位址空間。 高低記憶體之間的分界線 核心在開機期間依據BIOS提供的資訊來決定的。在i386系 統,分界通常位於1GB的位置。這是核心自己設下的限制, 因為核心必須將32-bit位址空間劃分成kernel-space與user- space兩大部份。
15.1.3 記憶體對應表與struct page(1/2) page結構含有關於實體記憶體的一切資訊。系統上的每一個實 體記憶頁,都有一個專屬的struct page,以下是page結構裡幾個 比較重要的欄位。 atomic_t count; 此記憶頁的用量計次。當count值降為0時,記憶頁會被釋放回自 由串列。 void *virtual; 本記憶頁對應的核心虛擬位址;若無對應的虛擬位址則指向 NULL 。 unsigned long flags; 一組描述記憶頁狀態的位元旗標。如PG_locked(代表記憶頁是否 已被鎖定)、PG_reserved(是否受記憶體管理系統的管轄) 。 /usr/src/kernels/linux-3.0.8/include/linux/mm.h
15.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) ; kmap()可傳回系統上任何記憶頁的核心虛擬位址。 Page在低記憶體→則傳回該記憶頁的邏輯位址。 Page在高記憶體→kmap()主動將它映射到特殊的虛擬空間。 如果分頁表剛好沒有空位,kmap()有可能會休眠。 void kunmap(struct page *page) ; 將kmap()所建立的特殊對應解除。
15.1.4 虛擬記憶體分區(1/6) 核心需要一個較高層級的機制,才能處理行程所見到 的記憶體佈局。在Linux,這機制稱為虛擬記憶體分區 (virtual memory areas),通常簡稱為分區或VMA。 用來管理使用者行程的虛擬位址空間的各個區域。 一個行程的虛擬位址空間,至少含有下列幾個VMA: 一個存放程式碼(executable binary)的區域,通 常稱為text。 多個資料分區,包括有初值的資料、沒初值的資料, 以及程式堆疊。 每一個有效的記憶體對映(memory mapping),各有 一個分區。 http://blog.chinaunix.net/uid-28263175-id-3541869.html
15.1.4 虛擬記憶體分區(2/6) 例:以下是init行程VMA的分布情形。cat /proc/1/maps 08048000-0804e000 r-xp 00000000 03:01 64652 /sbin/init text 0804e000-0804f000 rw-p 00006000 03:01 64652 /sbin/init data 0804f000-08053000 rwxp 00000000 00:00 0 zero-mapped BSS 40000000-40015000 r-xp 00000000 03:01 96278 /lib/ld-2.3.2.so text 40015000-40016000 rw-p 00014000 03:01 96278 /lib/ld-2.3.2.so data 40016000-40017000 rw-p 00000000 00:00 0 BSS for ld.so 42000000-4212e000 r-xp 00000000 03:01 80290 /lib/tls/libc-2.3.2.so text 4212e000-42131000 rw-p 0012e000 03:01 80290 /lib/tls/libc-2.3.2.so data 42131000-42133000 rw-p 00000000 00:00 0 BSS for libc bffff000-c0000000 rwxp 00000000 00:00 0 Stack segment ffffe000-fffff000 ---p 00000000 00:00 0 vsyscall page 各欄位的格式如下: start-end|perm|offset|major:minor|inode|imagename
15.1.4 虛擬記憶體分區(3/6) 上面每一欄除了imagename之外,都分別對應到struct vm_area_struct裡的欄位,這些欄位意義如下: start、end VMA前後邊界的虛擬位址 perm VMA的存取位元遮罩(r、w、x、p/s) offset VMA所映射的檔案內容之起點 major:minor 持有映射檔的裝置的主、次編號 inode 被映射檔案的inode編號 imagename 被映射檔案(通常是可執行檔)的名稱 mmap() 是Unix 系統的一個重要的系統呼叫,其作用是將 裝置記憶體映射到 user-space 行程的虛擬記憶空間。
15.1.4 虛擬記憶體分區(4/6) Linux核心是以 struct vm_area_struct 來表示VMA。 驅動程式不能任意建立新的VMA,否則會破壞整個組織, 在核心內部,VMA是以串列與樹狀結構組織在一起(為了 提升查詢VMA的效率) 。 vm_area_struct的主要欄位: unsigned long vm_start; unsigned long vm_end; VMA所涵蓋的虛擬位址範圍。 struct file *vm_file; 如果VMA的映射對象是檔案,則vm_file指向該檔案的 struct file結構。 定義在 /usr/src/kernels/linux-3.0.8/include/linux/mm.h struct file *vm_file: 指向與該區域相關聯的file結構指針
15.1.4 虛擬記憶體分區(5/6) unsigned long vm_pgoff; 此區域在檔案裡的相對位置(以page為單位)。當一個檔案或裝置 被映射到記憶體, vm_pgoff就是映射到此區域的第一頁的檔案 位置。 unsigned long vm_flags; 描述VMA性質的一組旗標。對裝置驅動程式而言,最可能用到 的旗標是VM_IO 與VM_RESERVED。其中VM_IO表示該VMA 是映射到硬體裝置上的I/O 位址區;VM_RESERVED要求記憶體 管理系統不要將該VMA置換到磁碟上。 struct vm_operations_struct *vm_ops; 一組可供核心用來操作此VMA的函式。這函式指標的存在,表 示核心將VMA當成一種物件來看待。 void *vm_private_data; 供驅動程式用於儲存私有資訊的欄位。 VM_IO的作用之一,是避免行程的 core dump含有該分區(因為 I/O區不可隨意讀寫)。
15.1.4 虛擬記憶體分區(6/6) void (*open)(struct vm_area_struct *vma); 核心會呼叫open作業方法,讓實作VMA的子系統有機會初 始VMA 、調整用量計次...等等。 void (*close)(struct vm_area_struct *vma); 當VMA被摧毀,核心會呼叫它的close作業方法。 struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int type); 行程試圖讀取某個VMA記憶頁,但該記憶頁目前不在主記 憶體裡,則會執行VMA的nopage作業方法。nopage作業方 法通常會從磁碟上的交換區讀回記憶頁的內容,然後傳回 一個指向實體記憶頁的struct page的指標。若果VMA沒定義 它自己的nopage作業方法,則核心會配置一個空的記憶頁。
remap_pfn_range() 與 nopage 作法圖示 ︰
15.2 mmap作業方法(1/2) 就驅動程式的觀點而言,記憶體映射可用來提供直接存取裝置記憶體 的能力給user-space應用程式。 mmap()最經典的應用,就是 X Window System server 利用它來存取顯 示卡的視訊記憶體。以下是 X server 行程的記憶體對應表: cat /proc/731/maps 000a0000-000c0000 rwxs 000a0000 03:01 282652 /dev/mem 000f0000-00100000 r-xs 000f0000 03:01 282652 /dev/mem 00400000-005c0000 r-xp 00000000 03:01 1366927 /usr/X11R6/bin/Xorg 006bf000-006f7000 rw-p 001bf000 03:01 1366927 /usr/X11R6/bin/Xorg 2a95828000-2a958a8000 rw-s fcc00000 03:01 282652 /dev/mem 2a958a8000-2a9d8a8000 rw-s e8000000 03:01 282652 /dev/mem a0000:VGA卡的視訊記憶體的標準位置 e8000000:位於系統記憶體的頂端,直接對應到顯卡上的視訊記憶體 cat /proc/iomem
15.2 mmap作業方法(2/2) 由於X server時常需要傳輸大量資料到視訊記憶體,如果使用傳統的 lseek()、write(),勢必引發相當頻繁的context switch,而傳輸效率當然 就很差勁;然而如果將視訊記憶體直接映射到user-space,則應用程式 可以直接填寫視訊記憶體,所以傳輸效率得以大幅提升。 mmap作業方法屬於file_operations結構的一部份,由mmap()系統呼叫 觸發。 mmap (caddr_t addr, size_t length, int prot, int flags, int fd, off_t offset); /此為mmap()系統呼叫的宣告形式/ int (*mmap)(struct file *filp, struct vm_area_struct *vma); /mmap作業方法宣告方式/ 有兩種方法可以製作分頁表:全部交給remap_pfn_range()函式一次搞定。 或者透過VMA的nopage作業方法,在VMA被存取時,才一次處理一頁。 file 代表結構,而 filp 是指向 struct file 的指標
15.2.1 使用remap_pfn_range() 要將一段虛擬位址映射到一段實體位址,必須另外產生新 的分頁表,這個任務就交給它來完成。 int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot); 映射成功,則傳回0,否則傳回一個負值錯誤碼。 vma 記憶頁所要映射到的VMA。 virt_addr 實體位址所要映射到的虛擬位址範圍之起點。 pfn 虛擬位址所要映射到的實體位址的頁框編號。 size 映射區的規模(以byte為計算單位) 。 prot 新VMA的保護方式。驅動程式能使用 vma->vm_page_port所提供的值。 virt_addr +size 之間的虛擬位址建立頁表
15.2.2 mmap實例 簡單、線性的映射作法,讓應用程式可透過user-space的某段虛擬位 址來存取裝置記憶體 static int simple_remap_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 = &simple_remap_vm_ops; simple_vma_open(vma); return 0; } http://blog.chinaunix.net/uid-28263175-id-3541869.html /建立頁表/ 當一個檔案或裝置被映射到記憶體, vm_pgoff就是映射到此區域的第一頁的檔案位置。(以頁為單位,文件中該區域的偏移量)。 vm_page_prot (訪問權限:可讀、可寫、可執行、不可訪問等等)。 vma->vm_ops = &simple_remap_vm_ops (核心能調用的一套函數,用來對該記憶體區進行操作)。
15.2.3 增添新的VMA作業方法 void simple_vma_open(struct vm_area_struct *vma) { printk(KERN_NOTICE "Simple VMA open, virt %lx, phys %lx\n", vma->vm_start, vma->vm_pgoff << PAGE_SHIFT); } void simple_vma_close(struct vm_area_struct *vma) printk(KERN_NOTICE "Simple VMA close.\n"); static struct vm_operations_struct simple_remap_vm_ops = { .open = simple_vma_open, .close = simple_vma_close, }; vm_operations_struct 結構中有 open和 close欄位 . 是告訴它我這兩個欄位要填入什麼東西
15.2.4 使用nopage映射記憶體(1/3) 雖然remap_page_range()已足以應付許多驅動程式的mmap, 但偶爾會需要多一點彈性。對於這類情況,VMA的nopage 作業方法或許是可以考慮的選擇。 適合使用nopage作業方法來映射位址空間的情況,是當驅 動程式只需在VMA發生變化才會有處理動作時。應用程式 可透過mremap()系統呼叫來改變VMA的邊界或大小。 發生mremap()系統呼叫時,核心不一定會通知驅動程式; 假如改變的結果是VMA範圍縮減,核心可以默默清理掉多 出來的記憶頁,而不必通知驅動程式;但如果是範圍擴張, 核心才會呼叫該VMA的nopage作業方法來配置新的記憶頁。 之所以不讓驅動程式收到映射區擴張通知,是因為記憶體 被實際應用之前,沒有處理的必要,而當真的有必要時, 核心可觸發nopage來處理。
15.2.4 使用nopage映射記憶體(2/3) 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)) return NOPAGE_SIGBUS; pageptr = pfn_to_page(pageframe); get_page(pageptr); //遞增用量計次 if (type) *type = VM_FAULT_MINOR; return pageptr; } Nopage 必須負責遞增它所傳回的 page 之用量計次,get_page(struct page *pageptr) ,這是保持映射記憶頁用量計次正確的必要步驟。
15.2.4 使用nopage映射記憶體(3/3) 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)) vma->vm_flags |= VM_IO; vma->vm_flags |= VM_RESERVED; vma->vm_ops = &simple_nopage_vm_ops; simple_vma_open(vma); return 0; }
15.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_pfn_range(vma, vma->vm_start, physical, vsize, vma->vm_page_prot);
15.2.5 重新映射特定I/O區( 2/2) 要避免映射範圍擴張,最簡單的辦法是實作一個 簡單的nopage作業方法,讓它回覆一個SIGBUS信 號給發生失誤的行程。例如: struct page *simple_nopage(struct vm_area_struct *vma, unsigned long address, int *type) { return NOPAGE_SIGBUS; /* 傳回一個 SIGBUS */ }
15.2.6 重新映射 RAM 如果要適度容許映射擴張,比較完善的做法,是檢查引發 分頁失誤的位址,是否在有效的實體範圍內,如果是,才 容許映射。 remap_page_range() :只有保留頁,以及在實體記憶體(RAM) 頂端之上的實體位址,它才有作用。保留頁被鎖在記憶體 裡(不會被換出到磁碟上) ,所以可以安全地映射到user- space;這項限制式系統穩定度的基本要求。 由於remap_page_range()沒有處理RAM的能力,這表示類 似scullp那樣的裝置將難以作出自己的mmap ,因為其裝置 記憶體是一般的RAM而非I/O memory。幸好,還是可以使 用nopage作業方法。
15.2.6.1 使用nopage重新映射RAM(1/3) 先看看有哪些設計抉擇會影響scullp的mmap: 在裝置被映射之後,scullp就不釋放其裝置記憶體,而且不能 像scull或類似裝置那樣,在被開啟成write模式時,裝置長度 就被截為0;要避免釋放已映射的裝置,驅動程式必須自己計 算有效的映射次數,scullp_device結構中的vmas欄位,可當此 用途來使用。 只有在scullp的order參數值為0 ,才容許映射記憶體。因為 get_free_pages()和free_pages()只修改串列中第一個空頁計次值。 要遵循上述規則來映射RAM的程式,需要實作出open、close和 nopage,而且還必須存取記憶對應表,調整記憶頁的用量計次。
15.2.6.1 使用nopage重新映射RAM(2/3) int scullp_mmap(struct file *filp, struct vm_area_struct *vma) { struct inode *inode = filp ->f_dentry->d_inode; /* 如果order不等於0,則拒絕映射 */ if (scullp_devices[iminor(inode)].order) return -ENODEV; /* 這裡不作任何事。交給“nopage”搞定 */ vma->vm_ops = &scullp_vm_ops; vma->vm_flags |= VM_RESERVED; vma->vm_private_data = filp->private_data; scullp_vma_open(vma); return 0; }
15.2.6.1 使用nopage重新映射RAM(3/3) void scullp_vma_open(struct vm_area_struct *vma) { struct scullp_dev *dev = vma->vm_private_data; dev->vmas++; } void scullp_vma_close(struct vm_area_struct *vma) dev->vmas--;
15.3 直接I/O 大部份的I/O作業,透過kernel-space的緩衝區做緩 衝,某種程度上隔離了user-space與實際裝置。好 處:讓程式比較好寫,提升效能。 但在傳輸大量資料時,讓user-space直接與裝置I/O 做溝通反而會比較好。
15.3.1直接I/O的關鍵函式 在2.6版核心實作直接I/O的關鍵函式是 get_user_pages() 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); struct task_struct *tsk :此指標指向要執行I/O的task_struct。 struct mm_struct *mm:此指標所指的mm_struct結構,是行程的虛擬位址空間的所有VMA。 unsigned long start:user-space緩衝區的起始位址。 int len:該緩衝區的長度,以頁為計算單位。 int write:若不是零,則要映射的記憶頁是供write存取之用。 int force:要求 get_user_pages() 不理會指定記憶頁的保護旗標,對驅動程式應設為零。 struct page **pages:page應該指向一個描述user-space緩衝區的struct page串列。 struct vm_area_struct **vmas :vmas應含有相關的VMA的指標。 get_user_pages() 的回傳值:是實際映射的記憶頁數量,此數量有可能少於要求的數量,但 至少會大於零。
15.3.2釋放記憶頁之前要做的事情 完成直接I/O作業之後,必須釋放user-space記憶頁。 1.檢查記憶頁是否為記憶體對應表中的保留部份,因為保 留頁絕對不會被置換出去,可使用PageReserved()來檢查特 定記憶頁。 非絕對必要:user-space記憶體都不是保留頁。 2.若改變了記憶頁內容,則使用SetPageDirty(struct page *page);將記憶頁做標記,否則核心會直接釋放該記憶頁, 而不寫回儲存裝置。 3.不管是否有改變記憶頁內容,都應將page從page cache釋 放,這使用void page_cache_release(struct page *page);
15.3.3非同步I/O 非同步 I/O 讓 user-space 可發動 I/O 作業要求,而不必 等待 I/O 作業完成,也就是說在 I/O 進行過程中,應 用程式仍可以做其它事。對於複雜的高效率應用程式, 非同步 I/O 可讓它們同時進行多項工作。 核心沒強制要求驅動程式必須提供非同步 I/O 的能力, 也只有非常少數的驅動程式設計者需要考慮這項能力, 因為這通常無助於裝置的傳輸效率。
15.4 直接記憶體存取(DMA) DMA是一種硬體機制,讓周邊元件可以直接與主 記憶體交換I/O資料,而不必經過系統處理器。 DMA機制可大幅提昇周邊裝置的資料吞吐量,同 時減輕CPU的運算負擔。
15.4.1 DMA資料傳輸的流程(1/3) 有兩種機會可觸發DMA資料傳輸:軟體主動要求, 或周邊硬體主動將資料推入系統。 第一種情況(軟體觸發)所涉及的步驟: 1.當行程發出一次read(),驅動程式的read作業方 法就配置一塊DMA緩衝區,並指示周邊硬體開始 傳輸資料。行程會進入休眠狀態。 2.周邊硬體將資料寫到DMA緩衝區,在完成傳輸之 後,對CPU發出一次中斷訊號。 3.ISR(中斷服務程式)收下輸入資料、回應中斷、 然後喚醒行程,讓行程讀走資料。
15.4.1 DMA資料傳輸的流程(2/3) 第二種情況,發生在DMA被當成非同步傳輸機制使用 時的步驟: 1.周邊硬體觸發一次中斷,讓系統處理器知道新資料 已經到達。 2.ISR配置一個緩衝區,並將該緩衝區的位置告訴周邊 硬體,使其知道資料應該傳送到何處。 3.周邊硬體將資料寫入指定的緩衝區,在完成傳輸之 後,觸發另一次中斷。 4.ISR將新資料存放在適當位置,喚醒任何相關行程, 並處理一些例行工作。
15.4.1 DMA資料傳輸的流程(3/3) 網路卡與CPU之間通常是透過主記憶體上的一塊環 型緩衝區(稱為DMA ring buffer)互相交換資料。 當網路卡從外界收到一個封包,就將它放入環型 緩衝區裡的下一個空位,然後發出中斷通知。 驅動程式將網路封包傳給核心裡的其它部門,並 將一個新的DMA空位放回環型緩衝區。 大多數驅動程式在初始期就預先配置好所需的緩 衝區,並全程使用同一塊緩衝區,直到關閉時才 予以釋放。
15.4.2 配置DMA緩衝區 並非所有記憶體都可以用來當成DMA緩衝區,因此要配置一塊 適合DMA的緩衝區,不是隨意配置一塊普通記憶體就了事了。 DMA緩衝區必須是實體記憶體上的連續頁,因為周邊裝置使用 ISA或PCI匯流排來傳輸資料,而兩種匯流排都使用實體位址。 自助配置法: 需靠核心的mem=開機期參數配合,如原有256M的記憶體,而你 需要1MB記憶體來當DMA緩衝區使用,則可使用mem=255M參 數要求核心將最高的1MB保留給你。保留記憶體的模組程式為: dmabuf=ioremap(0x1f00000 /* 255M*/ ,0x100000 /* 1M */)。 積極配置法: 使用 GFP_NOFAIL 積極配置足夠的DMA緩衝空間;不過這應該 被視為最後手段,除非其它所有辦法都無效,因為積極配置會導 致沉重的系統負載,甚至可能鎖死系統。
15.4.3 匯流排位址 具有DMA能力的周邊硬體,其實是使用匯流排位址,而非實體 位址。在x86 PC上,ISA與PCI匯流排的位址確實等於x86 CPU 的實體位址,但並非所有平台都這樣,有些平台的介面匯流排 是透過橋接電路連接在一起,它們的I/O位址被映射到不同的實 體位址。 在最底層,Linux核心提供一套通用的解決方案,以下兩個函式: unsigned long virt_to_bus(volatile void * address); void *bus_to_virt(unsigned long address); 這兩個函式只是在虛擬位址與匯流排位址之間做轉換。通常不 應該使用它們,因為它們僅適用於 I/O 架構非常簡單的平台, 若是遇到有 I/O MMU 的平台就無能為力了。
15.4.4 DMA抽象層 從驅動程式的觀點來看,DMA終究不過是配置一個緩 衝區,並傳遞匯流排位置給裝置。 對於快取而言,如何保持快取的一致性,各家系統各 有自己獨到的邏輯,若你的驅動程式沒能正確處理這 方面的問題,可能會造成系統記憶錯亂。 並非所有系統的每一塊記憶體都可以配合DMA作業。 例如: x86平台的高記憶體就不能當成DMA緩衝區。 核心提供了一個DMA抽象層,可跨越各種不同平台與 匯流排之間的差異。
15.4.4.1 排除不支援的硬體 嘗試進行DMA作業之前,第一個要回答的問題是當時的平台是 否支援目標裝置所需的DMA能力。 原則上核心假設裝置可在任何32-bit位址執行DMA作業,如果裝 置不合這項假設,可以下列方式通知核心: int dma_set_mask (struct device *dev, u64 mask); 其中mask代表目標裝置的定址空間的位元數。 假如驅動的是只有24-bit定址能力的ISA裝置: if (dma_set_mask (dev, 0xffffff)) card->use_dma=1; else { card->use_dma=0; printk (KERN_WARN, “mydev: DMA not supported\n”); }
15.4.4.2 DMA 對應 配置一個DMA緩衝區,並為該緩衝區產生一個可供裝置存取的位址,這 兩個動作的組合就稱為 DMA 對應。 在PCI匯流排上的DMA對應被分成兩種類型,主要差別在於 DMA緩衝區的存活時間長短。這兩種對應模式如下: 常態性DMA對應(coherent DMA mapping) 若DMA緩衝區的生命期與驅動程式一樣長,就稱為常態性DMA對應。 DMA緩衝區必須能夠同時被CPU與周邊使用,甚至應該被排除在快取 機制之外,以免一方看不見另一方的更新。 臨時性DMA對應(streaming DMA mapping) 為了單次DMA作業而臨時設置的DMA對應;這比較有彈性但必須遵 守一些限制;建議盡可能使用臨時性對應基於兩項理由。首先,在配 置對應暫存器的系統上,每組DMA對應都需要用掉一或多個暫存器, 常態性對應會長期佔用這些暫存器;其次,有不少硬體平台特地針對 臨時性對應做了最佳化,這些最佳化措施不能運行在常態性對應。
15.4.4.3 設定常態性DMA對應 驅動程式可呼叫dma_alloc_coherent() 來設定一組常態性的DMA對應,此 函式包辦了緩衝區的配置與對應工作。 void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag); dma_alloc_coherent() 內部使用 get_free_pages() 來配置指定的記憶空間, 以便得到可以配置 DMA 作業的緩衝區;flag通常是設定為 GFP_KERNEL (可能會休眠)或 GFP_ATOMIC (絕不會休眠)。 當不再需要緩衝區時(通常是在卸載模組時,就應該盡快使用 dma_free_coherent() 將緩衝區還給系統,此函式需同時提供CPU位址與匯 流排位址。 void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);
15.4.4.4 設定臨時性DMA對應(1/3) 臨時性 DMA 對應的軟體介面稍微複雜些,因為驅動程式自己得事先配置好緩衝區,並處 理它們所沒選擇的位址。在某些平台上,臨時性對應甚至接受多個不連續的記憶頁。 設定臨性對應時,必須讓核心知道資料的移動方向,方向是以 enum dma_data_direction 型別的符號來描述,如下: DMA_TO_DEVICE 如果是要將資料傳送到裝置上(為了回應 write() 系統呼叫)。 DMA_FROM_DEVICE 如果是要將資料傳送到裝置上(為了回應 read() 系統呼叫)。 DMA_BIDIRECTIONAL 表示如果容許資料雙向移動。 DMA_NONE 供除錯用途。 當你只有一個緩衝區要傳輸,可使用 dma_map_single() 來將該緩衝區映射到裝置定址空 間。 dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction); 傳回值是可以傳給目標裝置的匯流排位址。如果映射失敗,則會傳回 NULL。 完成傳輸之後,應該立刻使用 pci_unmap_single() 來解除對應。 void pci_unmap_single(struct pci_dev *pdev, dma_addr_t bus_addr, size_t size, int direction); 這裡的 size 和 direction 引數,必須符合當初映射緩衝區時所用的引數值。
15.4.4.4 設定臨時性DMA對應(2/3) 臨時性 DMA 對應必須遵守三點重要法則: 緩衝區的使用,必須符合映射時所設定的傳輸方向。 在緩衝區映射到匯流排位址之後,就屬於裝置,而非處理器。 在解除對應之前,驅動程式不能以任何方式接觸緩衝區。只 有在呼叫 dma_unmap_single() 之後,驅動程式才能安全存取 緩衝區的內容。這意味著你必須先將要寫入裝置的資料放在 緩衝區,然後才能映射它。 在DMA動作期間,不能解除對應,否則保證系統一定會嚴重 錯亂。 為何驅動程式不能接觸已被對應的緩衝區?有兩項原因,第一, 如果要輸出資料到裝置上,核心必須確保要放在DMA緩衝區的 資料,已經確實全數寫入記憶體;第二,如果被映射的緩衝區 位於周邊裝置無法存取的區域時,某些平台會直接讓DMA作業 失敗,而其它平台則可能會建立一個轉進緩衝區(bounce buffer), 轉進緩衝區只是另一塊裝置可以存取的記憶區。
15.4.4.4 設定臨時性DMA對應(3/3) 偶爾,驅動程式需要在解除對應之前,先存取臨時 DMA 緩衝區 的內容,可用以下函式: void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction); 此函式的呼叫時機,必須在處理器存取 DMA_FROM_DEVICE 緩衝區之前。一旦此函式 返回,CPU就全權擁有 DMA 緩衝區了。 在裝置存取 DMA 緩衝區之前,必須將所有權轉移回去: void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction); 呼叫了 void dma_sync_single_for_device() 函式之後,CPU就不 應該碰觸 DMA 緩衝區了。
15.4.4.5 臨時性的單頁對應 偶爾,可能會想要將 DMA 緩衝區映射到一個已有其 struct page 指標的 記憶頁;比如當想要讓裝置直接將資料傳輸到一個用 get_user_page () 所取得的 user-space 暫存區時,就會有這樣的需要。 要讓 DMA 緩衝區暫時映射到 struct page 指標所指的記憶頁,可用下列 函式: 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); offset 與 size 引數可用來縮限映射範圍,不過應該盡量避免不滿一頁的 映射。當映射範圍不滿一個記憶頁時,而配置範圍只包含部份的快取 線,就有可能導致快取失調的問題,進而導致記憶錯亂。
Demo(1/8)
Demo(2/8)
Demo(3/8)
Demo(4/8)
Demo(5/8)
Demo(6/8)
Demo(7/8)
Demo(8/8)