Top Banner
Chapter 13 MMAP 與 DMA
63

Chapter 13

Mar 15, 2016

Download

Documents

wade-garza

Chapter 13. MMAP 與 DMA. 13.1 Linux 的記憶體管理. 主要是描述用於控管記憶體的各種資料結構 , 相當冗長 . 有了必要的基礎知識後 , 我們就可以開始使用這些結構. 13.1.1 位址的分類 (1/4). 作業系統的分類上 ,Linux 是一種虛擬記憶系統 . 虛擬記憶體系統將邏輯世界 ( 軟體 ) 與現實世界 ( 硬體 ) 分隔開來 , 最大的好處是軟體可配置的空間超過 RAM 的實際容量 . 另一項優點是核心可在執行期改變行程的部分記憶空間 . - PowerPoint PPT Presentation
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Chapter 13

Chapter 13

MMAP 與 DMA

Page 2: Chapter 13

13.1 Linux 的記憶體管理 主要是描述用於控管記憶體的各種資料結構 ,相當冗長 .有了必要的基礎知識後 ,我們就可以開始使用這些結構 .

Page 3: Chapter 13

13.1.1 位址的分類 (1/4)

作業系統的分類上 ,Linux 是一種虛擬記憶系統 . 虛擬記憶體系統將邏輯世界 (軟體 )與現實世界 (硬體 )分隔開來 ,最大的好處是軟體可配置的空間超過 RAM 的實際容量 . 另一項優點是核心可在執行期改變行程的部分記憶空間 . Linux 系統上不只有兩種位址 ,而且每種位址都有其特殊用途 . 但核心原始程式裡沒有明確定義何種位只適用何種情況 ,所以必須相當謹慎小心 .

Page 4: Chapter 13

13.1.1 位址的分類 (2/4)

Page 5: Chapter 13

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()

Page 6: Chapter 13

13.1.1 位址的分類 (4/4)

<asm/page.h> 定義了兩個可換算位址的巨集 . 如果你有一個邏輯位址 ,__pa() 巨集可換算出其對應的實體位址 . __va() 可將實體位址換算回邏輯位址 ,但僅限於低畫分區的實體位址才有效 ,因為高畫分區沒有邏輯位址 . 不同的核心函式 ,需要不同類型的位址 .如果各種位址都有不同的 C型別 ,程式師就可明確知道何種情況該用何種位址 .然而 ,我們並沒有如此幸運 ,所以認命吧 .

Page 7: Chapter 13

13.1.2 高低劃分區 核心邏輯位址與核心虛擬位址之間的差異 ,再配備超大量記憶體的 32-bits 系統上才凸顯出來 . 低畫分區 (Low memory) 在 kernel-space 裡可用邏輯位址來定位的記憶體 高畫分區 (High memory) 沒有邏輯位址的記憶體 ,因為安裝超過定址範圍的實體記憶體 . 高低區之間的分界線 , 是核心在開機期間依據 BIOS 提供的資訊來決定的 .在 i386系統 ,分界通常位於 1GB 以下 .這是核心自己設下的限制 , 因為核心必須將 32-bit 位址空間劃分成 kernel-space 與 user-space 兩大部份 .

Page 8: Chapter 13

13.1.3 記憶體對應表與 struct page(1/2) 由於高畫分區沒有邏輯位址 ,處理記憶體的核心函式 ,紛紛改用 struct page 來代替邏輯位址 . page 結構紀錄了關於實體記憶頁的一切資訊 . 系統上的每一頁記憶體 ,都有一個專屬的 struct page, 幾個重要欄位如下 . atomic_t count; 此記憶頁的用量計次 . 當降為 0 時 ,會被釋放回自由串列 . wait_queue_head_t wait; 正在等待此記憶頁的所有行程 . void *virtual; 本記憶頁對應的核心虛擬位址 ; 若無 ( 高劃分 )則指向 NULL. unsigned long flags; 一組描述記憶頁狀態的位元旗標 .如 PG_locked、 PG_reserved.

Page 9: Chapter 13

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() 有可能會休眠 .

Page 10: Chapter 13

13.1.4 分頁表 (1/7)

每當程式用到一個虛擬位址 ,CPU 必須先將它轉換成實體位址 ,然後才能存取實體記憶體 . 轉換過程中 ,虛擬位址被拆成幾個位元欄 , 每個位元欄分別被當成不同陣列的索引 , 這些陣列就稱為分頁表 . 不管在何種平台上 ,Linux 統一使用三層分頁表 , 是為了讓位址範圍能被稀疏分布 ,即使硬體只支援兩層 , 或是另有特殊的虛擬 -實體位址對應法 . 一致的三層式架構 ,使得 Linux 核心成是不必寫一大堆 #ifdef 敘述 ,就可以同時支援兩層與三層式處理器 . 在只提供兩層分頁表的硬體上 ,多出來的中間層會被編譯器予以“最佳化” , 所以不會造成額外負擔 .

Page 11: Chapter 13
Page 12: Chapter 13

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 的直就是資料頁的實體位址 .

Page 13: Chapter 13

13.1.4 分頁表 (4/7)

對於各種硬體平台在記憶體管理機制上的差異 ,Linux 以巧妙的安排來解決這個問題 :將整個記憶體管理系統分為兩個部份 , 低階部份負責設定硬體的分頁機制 , 高階部分以一致的三層是分頁表來管理位址空間 . 硬體上的差異 ,全部都隱藏在低階部份 , 這部份的程式必須按照平台的特性來寫 , 所以各種系統都不太一樣 , 但它們都呈現一致的三層式分頁表存在 ,而不必理會硬體上的差異 . Linux 以軟體手法模擬出來的三層式分頁表 , 可用 <asm/page.h>和 <asm/pgtable.h> 所定義的一組符號來存取 :

Page 14: Chapter 13

13.1.4 分頁表 (5/7)

PTRS_PER_PGD PTRS_PER_PMD PTRS_PER_PTE 各層分頁表的大小 .在只有兩層分頁表的系統上 ,PTRS_PER_PMD 式設定為 1,藉此避免處理中間層的負擔 . 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 的實際型別 ,隨底層硬體與核心組態而定 .

Page 15: Chapter 13

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 之上 .

Page 16: Chapter 13

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/目錄之下 .

Page 17: Chapter 13

13.1.5 虛擬記憶區 (Virtual Memory Areas)(1/6) 核心需要一個較高層級的機制 , 才能處理行程所見到的記憶體佈局 . 在 Linux, 這機制稱為虛擬記憶區 (virtual memory areas), 通常簡稱為區域或 VMA. 行程的記憶對應表 ,由下列區域構成 : 一個存放程式碼 (executable binary) 的區域 . 通常稱為text. 一個存放資料的區域 .包括有初值資料 ,沒初值資料以及堆疊 . 每一個有效的對應關係 (memory mapping), 各有一個區域 .

Page 18: Chapter 13

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/maps08048000-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 # test40015000-40016000 rw-p 00014000 03:02 1149683 /lib/ld-2.3.2.so # data40016000-40017000 rw-p 00000000 00:00 0 # ld.so 的 bss42000000-4212e000 r-xp 00000000 03:02 809632 /lib/tls/libc-2.3.2.so # text4212e000-42131000 rw-p 0012e000 03:02 809632 /lib/tls/libc-2.3.2.so # data42131000-42133000 rw-p 00000000 00:00 0 # libc.si 的 bssbfffe000-c0000000 rwxp fffff000 00:00 0 # 堆疊區 (映射到 page 0)

Page 19: Chapter 13

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 結構 ,放在要求映射裝置的行程的位址空間裡 .

Page 20: Chapter 13

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 結構 .

Page 21: Chapter 13

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; 供驅動程式用於儲存私有資訊的欄位 .

Page 22: Chapter 13

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 意味著可容許共享 .

Page 23: Chapter 13

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_SVGA08327000-08369000 rw-p 002de000 08:01 55505 /usr/X11R6/bin/XF86_SVGA 40015000-40019000 rw-s fe2fc000 08:01 10778 /dev/mem40131000-40141000 rw-s 000a0000 08:01 10778 /dev/mem40141000-40941000 rw-s f4000000 08:01 10778 /dev/mem

Page 24: Chapter 13

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被存取時 ,才一次處理一頁 .

Page 25: Chapter 13

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找到的值 .

Page 26: Chapter 13

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; }

Page 27: Chapter 13

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; }

Page 28: Chapter 13

13.2.4 使用 nopage映射記憶體 (1/3) 雖然 remap_page_range()已經夠用了 ,但偶爾會需要多一點彈性 .對於這類情況 ,VMA 的 nopage 作業方法或許是比較理想的選擇 . 適合使用 nopage 作業方法來映射位址空間的情況之一 ,是應用程式可能發出 mremap() 系統呼叫的時候 . 此系統呼叫的作用是改變映射區的束縛位址 . 如果映射區範圍縮減 ,驅動程式的 unmap 作業方法確實會收到通知 ,但如果是範圍擴張 ,則不會發生任何 callback動作 . 之所以不讓驅動程式收到映射區擴張通知 ,是因為記憶體被實際應用之前 , 沒有處理的必要 ,而當真的有必要時 , 核心可觸發 nopage 來處理 .

Page 29: Chapter 13

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; }

Page 30: Chapter 13

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().

Page 31: Chapter 13

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);

Page 32: Chapter 13

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

Page 33: Chapter 13

13.2.6 重新映射 RAM 如果要適度容許映射擴張 ,比較完善的做法 , 是檢查引發分頁失誤的位址 ,是否在有效的實體範圍內 , 如果是 ,才容許映射 . remap_page_range() 有一樣值得玩味的限制 :只有保留頁 ,以及在實體記憶體 (RAM)頂端之上的與實體位址 ,它才有作用 .保留頁被鎖在記憶體裡 (不會被換出到磁碟上 ),所以可以安全地映射到 user-space;這項限制式系統穩定度的基本要求 . 由於 remap_page_range() 沒有處理 RAM 的能力 , 這表示類似 scullp那樣的裝置將難以作出自己的 mmap, 因為其裝置記憶體是一般的 RAM 而非 I/O memory. 幸好 ,可以使用 nopage 作業方法 .

Page 34: Chapter 13

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, 而且還必須存取記憶對應表 ,調整記憶頁的用量計次 .

Page 35: Chapter 13

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; }

Page 36: Chapter 13

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) { ScullP_Dev *dev = scullp_vma_to_dev(vma); dev->vmas--; MOD_DEC_USE_COUNT; }

Page 37: Chapter 13

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信號 */

Page 38: Chapter 13

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; }

Page 39: Chapter 13

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 140mapped "/dev/scullp" from 0 to 140total 232crw------- 1 root root 10, 10 Jan 30 2003 adbmousecrw-r--r-- 1 root root 10, 175 Jan 30 2003 agpgart[root@sip scullp]# ../misc-progs/mapper /dev/scullp 8192 200mapped "/dev/scullp" from 8192 to 8392h1494brw-rw---- 1 root floppy 2, 92 Jan 30 2003 fd0h1660brw-rw---- 1 root floppy 2, 20 Jan 30 2003 fd0h360brw-rw---- 1 root floppy 2, 12 Jan 30 2003 fd0H360

Page 40: Chapter 13

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;

Page 41: Chapter 13

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.

Page 42: Chapter 13

13.3 kiobuf (kernel I/O buffer)介面 此介面的主要用意 ,讓驅動程式以及系統上其它需要執行I/O的部份看不到虛擬記憶系統的複雜性 . 但是這些功能主要 2.4 核心用於將 user-space buffer映射到 kernel-space. 必須引入 <linux/iobuf.h>, 此檔案定義了 kiobuf 介面的心臟— struct kiobuf, 此結構描述構成一次 I/O作業所涉及的一個 page陣列 .

Page 43: Chapter 13

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主要是應用在驅動程式 .

Page 44: Chapter 13

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

Page 45: Chapter 13

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) { Sbull_Dev *dev = sbull_devices + MINOR(filp->f_dentry->d_inode->i_rdev); return sbullr_transfer(dev, (char *) buf, size, off, WRITE); }

sbullr_transfer() 函式只處理事前準備與事後收尾的工作 ,真正的傳輸工作是交給另一個函式來執行 .

Page 46: Chapter 13

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) return -EINVAL; /* 配置一個 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; }

Page 47: Chapter 13

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; }

Page 48: Chapter 13

13.3.2 User-Space緩衝區的映射與 Raw I/O (5/5) 分別在 sbullr 與 sbull 作了一些簡單的資料傳輸測試 ,結果發現同樣的資料量下 ,sbullr 所耗掉的系統時間大約只有 sbull 的三分之二 . 節省下來的時間 ,是因為 sbullr 的資料不必另外抄寫到緩衝快取區 .但反覆多次讀取相同資料 ,就沒有節省的效果了 . 提供修補程式 ,使我們可以輕易地使用一個 kiobuf將核心虛擬記憶映射到行程的位址空間 ,所以先前的 nopage也就不必要了 .

Page 49: Chapter 13

13.4 直接記憶體存取與匯流排主控 DMA 是一種硬體機制 ,讓週邊元件可以直接與主記憶體交換 I/O資料 ,而不必經過系統處理器 . 不幸地 ,由於 DMA 是“硬體”機制 , 其設定程序完全隨系統架構而定 . 主要重點放在 PCI匯流排 ,因為它是目前最熱門、最普遍的週邊匯流排 ,而且其概念有廣泛的通適性 .

Page 50: Chapter 13

13.4.1 DMA 資料傳輸的流程 有兩種方式可觸發資料傳輸 :軟體主動要求 , 或週邊硬體主動將資料推入 ( 簡化討論 , 只考慮輸入方向 ). 第一種情況的步驟 : 1. 當行程發出一次 read(),驅動程式的 read 作業方法就配置一塊 DMA緩衝區 ,並指示週邊硬體開始傳輸資料 .行程會進入休眠狀態 . 2. 週邊硬體將資料寫到 DMA緩衝區 ,在完成傳輸之後 ,對CPU發出一次中斷訊號 . 3.驅動程式的 interrupt handler收下輸入資料、回應中斷、然後喚醒行程 ,讓行程讀走資料 .

Page 51: Chapter 13

13.4.1 DMA 資料傳輸的流程 第二種情況的步驟 : 1. 週邊硬體觸發一次中斷 ,讓系統處理器知道新資料已經到達 . 2.驅動程式的 interrupt handler 配置一個緩衝區 ,並將該緩衝區的位置告訴週邊硬體 ,使其知道資料應該傳送到何處 . 3. 週邊硬體將資料寫入指定的緩衝區 ,在完成傳輸之後 ,觸發另一次中斷 . 4.Interrupt handler 分配新資料 ,喚醒任何相關行程 ,並處理一些例行工作 .

Page 52: Chapter 13

13.4.1 DMA 資料傳輸的流程 網路卡與 CPU 之間通常是透過主記憶體上的一塊環型緩衝區 (稱為 DMA ring buffer)互相交換資料 . 每當網路卡從外界收到一個封包 , 就將它放入環型緩衝區裡的下一個空位 ,然後發出中斷通知 . 驅動程式將網路封包傳給核心裡的其它部門 , 並將一個新的 DMA 空位放回環型緩衝區 . 大多數驅動程式在初始期就預先配置好所需的緩衝區 ,並全程使用同一塊緩衝區 ,直到關閉時才予以釋放 .

Page 53: Chapter 13

13.4.2 配置 DMA緩衝區 並非所有記憶體都可以用來當成 DMA緩衝區 ,因此 ,要配置一塊適合 DMA 的緩衝區 ,不是隨意配置一塊普通記憶體就了事 . 對於有這種限制的裝置 ,應該使用來自 DMA專區的記憶體 .也就是說 , 在呼叫 kmalloc() 或 get_free_pages()時 , 要加上 __GFP_DMA旗標 . 自助配置法 : 需靠核心的開機期參數配合 , 如原有 32M, 當 mem=31, 之後可用dmabuf=ioremap(0x1f00000,0x100000) 來存取保留的 1M記憶體 . 積極配置法 : 呼叫 kmalloc(GFP_ATOMIC)多次 , 當它失敗時 , 就等待核心釋出一些記憶體 ,然後再重新配置一次所有東西 .

Page 54: Chapter 13

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);

Page 55: Chapter 13

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>.

Page 56: Chapter 13

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().

Page 57: Chapter 13

13.4.4.1 DMA 對應 在 PCI匯流排上的 DMA 對應備分成兩種類型 ,主要差別在於 DMA緩衝區的存活時間長短 . 這兩種對映模式如下 : 常態性 DMA 對應 (consistent DMA mapping) 若 DMA緩衝區的生命期與驅動程式一樣長 ,就稱為之 .DMA緩衝區必須能夠同時被 CPU 與週邊使用 ,也應該被排除在快取機制之外 . 臨時性 DMA 對應 (streaming DMA mapping) 為了單次操作而臨時設置的 DMA 對應 .基於兩項原因 ,核心團隊建議盡可能使用臨時性的對應模式 .首先 , 只有在每次 DMA 對應時 , 才會使用 bus 上的一或多個對應暫存器 ,另一項是某些週邊硬體特地針對臨時性對應作了最佳化 ,而這些最佳化措施不能運用在常態性對應 .

Page 58: Chapter 13

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);

Page 59: Chapter 13

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);

Page 60: Chapter 13

13.4.4.3 設定臨時性 DMA 對應 (2/2) 臨時性對應必須遵守三點重要法則 : 緩衝區的使用 ,必須符合映射時所設定的傳輸方向 . 在緩衝區映射到匯流排位址之後 ,就屬於裝置 ,而非處理器 .這意味著你必須先將要寫入裝置的資料放在緩衝區 ,然後才能映射它 . 在 DMA動作期間 ,不能解除對映 ,否則保證系統一定會嚴重不穩定 . 為何驅動程式不能接觸已被對應的緩衝區 ?有兩項原因 .第一 ,核心必須確保要放在 DMA緩衝區的資料 ,已經確實全數寫入記憶體 ; 第二 , 如果被映射的緩衝區位於週邊裝置無法存取的區域 ,某些平台會直接讓 DMA 作業失敗 , 而其它平台則可能會建立一個轉進緩衝區 .

Page 61: Chapter 13

轉進緩衝區只是另一塊裝置可以存取的記憶區 . 偶爾 ,驅動程式需要在解除對映前 ,先存取臨時 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緩衝區之後 .

Page 62: Chapter 13

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; }

Page 63: Chapter 13

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