Chapter 13 MMAP與DMA.

Slides:



Advertisements
Similar presentations
高等医药院校药学类第三轮规划教材——大学计算机基础
Advertisements

第十章 UNIX系统内核结构 10.1 UNIX系统概述 10.2 进程的描述和控制 10.3 进程的同步与通信 10.4 存储器管理
Chap4 電腦硬體基本單元 高中資訊科技概論 松崗圖書公司.
第一章 C语言概述 计算机公共教学部.
嵌入式图形显示.
中央广播电视大学开放教育试点课程 计算机操作系统.
Chapter 13 MMAP與DMA.
Chapter 15 MMAP與DMA 許名宏.
最新計算機概論 第3章 計算機組織.
基于ARM和linux的开发 华中科技大学 武汉创维特 2017/3/20.
Chapter 6 時序.
Chapter 13 輸入/輸出系統 (I/O Systems)
Linux Further.
© 2000 Wind River Systems, Inc.
主讲教师:唐大仕 第5讲 计算机硬件 主讲教师:唐大仕
嵌入式操作系统 陈香兰 Fall 2009.
作業系統 第十三章 檔案系統實例.
Operating System Concepts 作業系統原理 Chapter 3 行程觀念 (Process Concept)
Chapter 5 Tree & Binary Tree
第 13 章 DNS 著作權所有 © 旗標出版股份有限公司.
C H A P T E R 11 体系结构对操作系统的支持.
中国科学技术大学计算机系 陈香兰(0512- ) spring 2011
计算概论 第二十一讲 文件操作 北京大学信息学院.
中国科学技术大学计算机系 陈香兰(0512- ) Spring 2011
Chapter 15 MMAP與DMA.
补充内容 结构体 概述 定义结构体类型和定义结构体变量 结构体变量的引用 结构体变量的初始化 指针与结构体 用typedef定义类型的别名.
第七章. 文件系统 (lab5).
第7章 Linux环境编程.
第八章 输入输出程序设计 总线 CPU MEM I/O接口 I/O设备.
第二章 LINUX存储管理 LINUX的分页管理机制.
第三章 系统的启动和初始化 (include lab1)
Chapter 3 行程觀念 (Process Concept)
C 程式設計— 檔案處理 台大資訊工程學系 資訊系統訓練班.
CH.8 硬體管理.
在專用的嵌入式板子運行 GNU/Linux 系統已經變得越來越流行。一個嵌入式 Linux 系統從軟體的角度看通常可以分為四個層次:
中国科学技术大学计算机系 陈香兰(0512- ) Autumn 2009
中国科学技术大学计算机系 陈香兰(0512- ) Spring 2011
KeyStone I DSP[C665x 与 C6678] 视频教程
操作系统实验 lab1-系统引导
Linux操作系统分析 中国科学技术大学计算机系 陈香兰(0512- )
作業系統實習課(四) -檔案管理- 實驗室:720A 助教:鄧執中.
第3章 微型计算机输入输出接口 3.1 输入/输出接口 3.2 输入输出数据传输的控制方式 3.3 开关量输入输出接口 欢迎辞.
Ch9 Communicating with Hardware
如何生成设备节点 广州创龙电子科技有限公司
Linux 文件操作——系统调用和标准 IO 库
第3章 認識處理元.
第五章 中断与异常 中断的基本知识 中断描述符表的初始化 中断处理 中断的下半部处理机制 中断的应用-时钟中断.
组员:吴迪&王柳杨&金虎&陈武荣&谭金柏
7.1.1 设备管理的功能(P95) 分配设备:按设备的不同类型和操作系统选用的算法分配。包括分配相应的通道、设备控制器以及对未分配到的任务或怍业进行排队等; 控制和实现真正的输入输出操作。包括通道程序控制、启动设备、及时响应及处理中断讯号等; 对输入输出缓冲区进行管理。例如逻辑名的管理,多个缓冲区的分时以及串并行操作,同类多个外部设备的均衡工作,避免“忙的忙”和“闲的闲”;
第1章 概述 本章要点: C语言程序结构和特点 C语言程序的基本符号与关键字 C语言程序的编辑及运行 学习方法建议:
操作系统原理与设计 Operating Systems: Design and Implementation
Holtek C Compiler V3--advanced
(第2版).
第9章 虛擬記憶體 (virtual memory)
作業系統 Operating System 第四單元 檔案系統
Chapter 7 掌控記憶體.
<编程达人入门课程> 本节内容 为什么要使用变量? 视频提供:昆山爱达人信息技术有限公司 官网地址: 联系QQ:
Chapter 7 掌控記憶體.
第六章 記憶體.
实验2 中断模拟实验.
第二章 类型、对象、运算符和表达式.
1.3操作系统提供的服务和用户接口 操作系统提供的用户接口 程序接口与系统调用 操作接口与系统程序
C 程式設計— 檔案處理 台大資訊工程學系 資訊系統訓練班.
实验二:添加Linux系统调用及熟悉常见系统调用
中国科学技术大学计算机系 陈香兰(0551- ) Spring 2009
獨孤派作業系統 main memory 中正大學 作業系統實驗室 指導教授:羅習五.
第11章 儲存裝置 與其管理.
《操作系统设计与实现》 第5章 文件系统.
《操作系统设计与实现》 Linux系统编程.
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而定,但不一定與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) 沒有邏輯位址的記憶體,因為系統上安裝超過32-bits定址範圍的實體記憶體. 高低區之間的分界線,是核心在開機期間依據BIOS提供的資訊來決定的.在i386系統,分界通常位於1GB以下.這是核心自己設下的限制,因為核心必須將32-bit位址空間劃分成kernel-space與user-space兩大部份.

13.1.3 記憶體對應表與struct page(1/2) page結構紀錄了關於實體記憶頁的一切資訊.系統上的每一頁記憶體,都有一個專屬的struct page,幾個重要欄位如下. atomic_t count; 此記憶頁的用量計次.當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); 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.

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); 核心會呼叫open作業方法,讓實作VMA的子系統有機會初始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記憶頁,但記憶頁目前不在主記憶體裡,則VMA的nopage作業方法會被呼叫。nopage通常會從磁碟上的交換區讀回記憶頁內容,然後傳回一個指向實體記憶頁的struct page指標.若VMA沒定義自己的nopage方法,則核心會配置一個空的記憶頁.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 要被重新映射的虛擬位址 phys_add 所要對應的實體位址 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; /* 傳回一個 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) { pgd_t *pgd; pmd_t *pmd; pte_t *pte; 只有vmalloc()或kmap()函式傳回的位址,才是真正的虛擬位址,也就是說虛擬位址是透過核心分頁表映射而來的. struct page *scullv_vma_nopage(struct vm_area_struct *vma, unsigned long address,int write) { 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) VMALLOC_VMADDR(pageptr)巨集可從一個vmalloc()傳回的位址,傳回一個可用於查詢分頁表的unsigned long值. 被查詢的記憶對應表,是存放在kernel-space的一個記憶結構:init_mm. scullv必須先取得page_table_lock,然後才能開始查閱分頁表. 若要將ioremap傳回的位址映射到user-space.你可直接使用remap_page_range()來達成,而不必另外物VMA實作nopage作業方法. remap_page_range()已經有能力產生新分頁表來將I/O memory映射到user-space.

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

13.3.1 kiobuf結構 int nr_pages; //這個kiobuf所包含的記憶頁數量 int length; //緩衝區的資料量 int offset; //緩衝區第一個有效位元組的相對位置 struct page **maplist; //在kiobuf裡每一頁都有此結構陣列. Kiobuf介面ㄉ關鍵在於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.4 直接記憶體存取與匯流排主控 DMA是一種硬體機制,讓週邊元件可以直接與主記憶體交換I/O資料,而不必經過系統處理器. 本節主要重點放在PCI匯流排,因為它是目前最熱門、最普遍的週邊匯流排,而且其概念有廣泛的通適性.

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

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

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

13.4.2 配置DMA緩衝區 並非所有記憶體都可以用來當成DMA緩衝區,因此,要配置一塊適合DMA的緩衝區,不是隨意配置一塊普通記憶體就了事. 自助配置法: 需靠核心的mem=開機期參數配合,如原有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緩衝區之後.