1 / 63

Chapter 13

Chapter 13. MMAP 與 DMA. 13.1 Linux 的記憶體管理. 主要是描述用於控管記憶體的各種資料結構 , 相當冗長 . 有了必要的基礎知識後 , 我們就可以開始使用這些結構. 13.1.1 位址的分類 (1/4). 作業系統的分類上 ,Linux 是一種虛擬記憶系統 . 虛擬記憶體系統將邏輯世界 ( 軟體 ) 與現實世界 ( 硬體 ) 分隔開來 , 最大的好處是軟體可配置的空間超過 RAM 的實際容量 . 另一項優點是核心可在執行期改變行程的部分記憶空間 .

wade-garza
Download Presentation

Chapter 13

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Chapter 13 MMAP與DMA

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

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

  4. 13.1.1 位址的分類(2/4)

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

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

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

  8. 13.1.3 記憶體對應表與structpage(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.

  9. 13.1.3 記憶體對應表與structpage(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()有可能會休眠.

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

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

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

  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的實際型別,隨底層硬體與核心組態而定.

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

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

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

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

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

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

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

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

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

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

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

  25. 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; • }

  26. 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; • }

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

  28. 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; • }

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

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

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

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

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

  34. 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; • }

  35. 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; • }

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

  37. 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; • }

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

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

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

  41. 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陣列.

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

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

  44. 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()函式只處理事前準備與事後收尾的工作,真正的傳輸工作是交給另一個函式來執行.

  45. 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; • }

  46. 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; • }

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

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

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

More Related