630 likes | 709 Views
Chapter 13. MMAP 與 DMA. 13.1 Linux 的記憶體管理. 主要是描述用於控管記憶體的各種資料結構 , 相當冗長 . 有了必要的基礎知識後 , 我們就可以開始使用這些結構. 13.1.1 位址的分類 (1/4). 作業系統的分類上 ,Linux 是一種虛擬記憶系統 . 虛擬記憶體系統將邏輯世界 ( 軟體 ) 與現實世界 ( 硬體 ) 分隔開來 , 最大的好處是軟體可配置的空間超過 RAM 的實際容量 . 另一項優點是核心可在執行期改變行程的部分記憶空間 .
E N D
Chapter 13 MMAP與DMA
13.1 Linux的記憶體管理 • 主要是描述用於控管記憶體的各種資料結構,相當冗長.有了必要的基礎知識後,我們就可以開始使用這些結構.
13.1.1 位址的分類(1/4) • 作業系統的分類上,Linux是一種虛擬記憶系統. • 虛擬記憶體系統將邏輯世界(軟體)與現實世界(硬體)分隔開來,最大的好處是軟體可配置的空間超過RAM的實際容量. • 另一項優點是核心可在執行期改變行程的部分記憶空間. • Linux系統上不只有兩種位址,而且每種位址都有其特殊用途. 但核心原始程式裡沒有明確定義何種位只適用何種情況,所以必須相當謹慎小心.
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>定義了兩個可換算位址的巨集. • 如果你有一個邏輯位址,__pa()巨集可換算出其對應的實體位址. • __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 記憶體對應表與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.
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()有可能會休眠.
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 • 各層分頁表的大小.在只有兩層分頁表的系統上,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的實際型別,隨底層硬體與核心組態而定.
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) • { • ScullP_Dev *dev = scullp_vma_to_dev(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)介面 • 此介面的主要用意,讓驅動程式以及系統上其它需要執行I/O的部份看不到虛擬記憶系統的複雜性. • 但是這些功能主要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) • { • Sbull_Dev *dev = sbull_devices + MINOR(filp->f_dentry->d_inode->i_rdev); • return sbullr_transfer(dev, (char *) buf, size, off, WRITE); • } • sbullr_transfer()函式只處理事前準備與事後收尾的工作,真正的傳輸工作是交給另一個函式來執行.
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; • }
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資料,而不必經過系統處理器. • 不幸地,由於DMA是“硬體”機制,其設定程序完全隨系統架構而定. • 主要重點放在PCI匯流排,因為它是目前最熱門、最普遍的週邊匯流排,而且其概念有廣泛的通適性.
13.4.1 DMA資料傳輸的流程 • 有兩種方式可觸發資料傳輸:軟體主動要求,或週邊硬體主動將資料推入(簡化討論,只考慮輸入方向). • 第一種情況的步驟: 1.當行程發出一次read(),驅動程式的read作業方法就配置一塊DMA緩衝區,並指示週邊硬體開始傳輸資料.行程會進入休眠狀態. 2.週邊硬體將資料寫到DMA緩衝區,在完成傳輸之後,對CPU發出一次中斷訊號. 3.驅動程式的interrupt handler收下輸入資料、回應中斷、然後喚醒行程,讓行程讀走資料.