1、3项技术: 1,mmap系统调用可以实现将设备内存映射到用户进程的地址空间。 2,使用get_user_pages,可以把用户空间内存映射到内核中。 3,DMA的I/O操作,使得外设具有直接访问系统内存的能力。 ------------- 内存管理 内核用来管理内存的数据结构 --------- 地址内型 Linux是一个虚拟内存系统,即用户程序使用的地址与硬件使用的物理地址是不等同的。 虚拟内存引入了一个间接层,使得许多操作成为可能: *有了虚拟内存,系统中运行的程序可以分配比物理内存更多的内存。 *虚拟地址还能让程序在进程的地址空间内使用更多的技巧,包括将程序的内存
2、映射到设备内存上。 地址内型列表 *用户虚拟地址 每个进程都有自己的虚拟地址空间。 *物理地址 处理器访问系统内存时使用的地址。 *总线地址 在外围总线和内存之间使用。MMU可以实现总线和主内存之间的重新映射。 当设置DMA操作时,编写MMU相关的代码是一个必需的步骤。 *内核逻辑地址 内核逻辑地址组成了内核的常规地址空间,该地址映射了部分(或全部)内存, 并经常被视为物理地址。在大多数体系架构中,逻辑地址与其相关联的物理地址 的不同,仅仅在于它们之间存在一个固定的偏移量。kmalloc返
3、回的内存就是
内核逻辑地址。
*内核虚拟地址
内核虚拟地址与逻辑地址相同之处在于,都将内核空间的地址映射到物理地址上。
不同之处在于,内核虚拟地址与物理地址的映射不是线性的和一对一的。
vmalloc返回一个虚拟地址,kmap函数也返回一个虚拟地址。
------------------
物理地址和页
物理地址被分为离散的单元,称之为页。
系统内部许多对内存的操作都是基于单个页的。
大多数系统都使用每页4096个字节,PAGE_SIZE
4、 观察内存地址,无论是虚拟的还是物理的,它们都被分为页号和一个页内的偏移量。 如果每页4096个字节,那么最后的12位就是偏移量,剩余的高位则指定页号。 页帧数:将除去偏移量的剩余位移到右端,称该结果为页帧数。 ------------------- 高端与低端内存 内核(在x86架构中)将4GB的虚拟地址空间分割为用户空间和内核空间。 一个典型的分割是将3GB分配给用户空间,1GB分配给内核空间。 占用内核地址空间最大的部分是物理内存的虚拟映射, 内核无法直接操作没有映射到内核地址空间的内存。 低端内存: 只有内存的低端部分拥有逻辑地址。内核的数据结
5、构必须放置在低端内存中。
高端内存:
除去低端内存的剩余部分没有逻辑地址。它们处于内核虚拟地址之上。
--------------------
内存映射和页结构
内核使用逻辑地址来引用物理内存中的页。
为解决在高端内存中无法使用逻辑地址的问题,内核中处理内存的函数趋向于使用
指向page结构的指针
6、页面被映射,则指向页的内核虚拟地址; 如果未被映射,则为NULL。 低端内存页总是被映射,而高端内存页通常不被映射。 unsigned long flags; 描述页状态的一系列标志。 PG_locked表示内存中的页已经被锁住, 而PG_reserved表示禁止内存管理系统访问该页。 ----- 内核维护了一个或者多个page结构的数组,用来跟踪系统中的物理内存。 ----- 有一些函数和宏用来在page结构指针与虚拟地址之
7、间进行转换:
struct page *virt_to_page(void *kaddr);
8、的逻辑地址;
对于高端内存页,kmap在专用的内核地址空间创建特殊的映射。
void kunmap(struct page *page);
释放由kmap创建的映射。
void *kmap_atomic(struct page *page,enum km_type type);
void kunmap_atomic(void *addr,enum km_type type);
9、 这种机制成为页表。 ------- 虚拟内存区 虚拟内存区(VMA)用于管理进程地址空间中不同区域的内核数据结构。 可以将其描述为“拥有自身属性的内存对象”。 进程的内存映射包含下面这些区域: *可执行代码区域 *多个数据区:初始化数据,非初始化数据(BSS),程序堆栈。 *与每个活动的内存映射对应的区域 #cat /proc/1/maps 可以了解进程的内存区域。 /proc/self始终指向当前进程。 每行都是用下面的形式表示的: start-end perm offset major:minor inode image 在/proc/*/maps中的每个成员
10、除映像名外)都与vm_area_struct结构中的一个成员对应: start end 该内存区域的起始处和结束处的虚拟地址。 perm 读、写和执行权限,最后一位若是p表示私有,s表示共享。 offset 内存区域在映射文件中的起始位置。 major minor 拥有映射文件的设备的主设备号和次设备号。 inode 被映射的文件的索引节点号。 image 被映射文件的名称。 ------------- vm_area_struct结构 当用户空间进程调用mmap,将设备内存映射到它的地址空间时, 系统通过创建一个表示该映射的新VMA作为响应。 支持mm
11、ap的驱动程序需要帮助进城完成VMA的初始化。
vm_area_struct结构是在
12、s; 内核能调用的一套函数,用来对该内存区进行操作。 它的存在表示内存区域是一个内核“对象”。 void *vm_private_data; 驱动程序用来保存自身信息的成员。 vm_operations_struct结构的几个成员: void (*open)(struct vm_area_struct *vma); void (*close)(struct vm_area_struct *vma); struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address,in
13、t *type);
int (*populate)(struct vm_area_struct *vm,
unsigned long address,unsigned long len,
pgprot_t prot,unsigned long pgoff,int nonblock);
--------------------------
内存映射处理
在系统中的每个进程都拥有一个struct mm_struct结构
14、个信号灯(mmap_sem)和一个自旋锁(page_table_lock)。 ----- mmap设备操作 对于驱动程序来说,内存映射可以提供给用户程序直接访问设备内存的能力。 映射一个设备意味着将用户空间的一段内存与设备内存关联起来。 无论何时当程序在分配的地址范围内读写时,实际上访问的就是设备。 要注意: 像串口和其它面向流的设备不能进行mmap抽象。 必须以PAGE_SIZE为单位进行映射。 --------- rmpap_pfn_range和io_remap_page_range为一段物理地址建立新的页表。 int remap_pfn_range(struct v
15、m_area_struct *vm, unsigned long virt_addr,unsigned long pfn, unsigned long size,pgprot_t prot); int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr,unsigned long phys_addr, unsigned
16、long size,pgprot_t prot); vma:虚拟内存区域,在一定范围内的页将被映射到该区域内。 virt_addr:重新映射时的起始用户虚拟地址。该函数为处于virt_addr和virt_addr+size 之间的虚拟地址建立页表。 pfn:与物理内存对应的页帧号,虚拟内存将要被映射到该物理内存上。 size:以字节为单位,被重新映射的区域大小。 prot:新VMA要求的"保护"属性。 ------------ 一个简单的实现 static int simple_remap_mmap(struct file *filp,struct vm_
17、area_struct *vma) { if(remap_pfn_range(vma,vma->vm_start,vma->vm_pgoff, vma->vm_end-vma->vm->vm_start,vma->vm_page_prot)) return -EAGAIN; vma->vm_ops = &simple_remap_vm_ops; simple_vma_open(vma); return 0; } 可见,重新映射内存就是调用remap_pfn_range函数创建所需的页表。 -------------
18、 为VMA添加操作 vm_area_struct结构包含了一系列针对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 "Simpl
19、e VMA close.\n"); } static struct vm_operations_struct simple_remap_vm_ops = { .open = simple_vma_open, .close = simple_vma_close, }; ---------------- 使用nopage映射内存 有时驱动程序对mmap的实现必须具有更好的灵活性,在这种情况下, 提倡使用VMA的nopage方法实现内存映射。 如果要支持mremap系统调用,就必须实现nopage函数。 struct page *(*nopage)(struct vm_
20、area_struct *vma,
unsigned long address,int *type);
-----------------
重映射特定的I/O区域
一个典型的驱动程序只映射与其外围设备相关的一小段地址,而不是映射全部地址。
-----------------
重新映射RAM
--
使用nopage方法重映射RAM
--
重新映射内核虚拟地址
-------------------
执行直接I/O访问
实现直接I/O的关键是get_user_pages()函数:
21、er_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);
----------
异步I/O
22、 直接内存访问 DMA DMA是一种硬件机制,它允许外围设备和主内存之间直接传输它们的I/O数据, 而不需要系统处理器的参与。 -------------------------- DMA数据传输概览 有两种方式引发数据传输: 1,软件对数据的请求,比如通过read函数。 2,硬件异步地将数据传递给系统。 第一种情况的步骤如下: 1)当进程调用read,驱动程序分配一个DMA缓冲区, 并让硬件将数据传输到这个缓冲区中。进程处于睡眠状态。 2)硬
23、件将数据写入到DMA缓冲区中,当写入完毕,产生一个中断。 3)中断处理程序获得输入的数据,应答中断,并且唤醒进程, 该进程即可读取数据。 第二种情况发生在异步使用DMA时。 比如对于一个数据采集设备,即使没有进程读取数据,它也不断地写入数据。 此时,驱动程序应该维护一个缓冲区,其后的read调用将返回所有积累的数据给用户空间。 这种传输方式的步骤如下: 1)硬件产生中断,宣告新数据的到来。 2)中断处理程序分配一个缓冲区,并且告诉硬件向哪里传输数据。 3)外围设备将数据写入缓冲区,完成后产生另外一个中断。 4)处理程序分发新数据,唤醒任何相关进程,然后执行清理工作。 高效
24、的DMA处理依赖于中断报告!!! ------------------ 分配DMA缓冲区 使用DMA缓冲区的主要问题是:当大于一页时,它们必须占据连续的物理页, 这是因为使用ISA或者PCI系统总线传输数据,而这两种方式使用的都是物理地址。 驱动程序作者必须谨慎地为DMA操作分配正确的内存类型,因为 并不是所有内存区间都适合DMA操作。 在实际操作中,一些设备和一些系统中的高端内存不能用于DMA, 这是因为外围设备不能使用高端内存的地址。 对于有限制的设备,应使用GFP_DMA标志调用kmalloc或者get_free_pages从 DMA区间分配内存。另外,还可以通过
25、使用通用DMA层来分配缓冲区。
-------------------
总线地址
使用DMA的设备驱动程序将与连接到总线接口上的硬件通信,
硬件使用的是物理地址,而程序代码使用的是虚拟地址。
实际上,基于DMA的硬件使用总线地址,而非物理地址。
26、
通用DMA层
DMA操作最终会分配缓冲区,并将总线地址传递给设备。
内核提供了一个与总线---体系架构无关的DMA层,它会隐藏大多数问题。
在编写驱动程序时,为DMA操作使用该层。
device结构
该结构是在Linux设备模型中用来表示设备底层的,驱动程序通常不直接使用该结构,
但是,在使用通用DMA层时,需要使用它。
该结构内部隐藏了描述设备的总线细节。
27、用dma_set_mask()函数解决。 -------------------------- DMA映射 一个DMA映射是要分配的DMA缓冲区与为该缓冲区生成的、设备可访问地址的组合。 DMA映射建立了一个新的结构类型---dma_addr_t来表示总线地址。 dma_addr_t类型的变量对驱动程序是不透明的, 唯一允许的操作是将它们传递给DMA支持例程以及设备本身。 根据DMA缓冲区期望保留的时间长短,PCI代码有两种DMA映射: 1)一致性映射 2)流式DMA映射(推荐) ---------------------------- 建立一致性DMA映射 void
28、dma_alloc_coherent(struct device *dev,size_t size, dma_addr_t *dma_handle,int flag); 该函数处理了缓冲区的分配和映射。 前两个参数是device结构和所需缓冲区的大小。 函数在两处返回结果: 1)函数的返回值时缓冲区的内核虚拟地址,可以被驱动程序使用。 2)相关的总线地址则保存在dma_handle中。 向系统返回缓冲区 void dma_free_coherent(struct device *dev,size_t size,
29、 void *vaddr,dma_addr_t dma_handle);
---------
DMA池
DMA池是一个生成小型、一致性DMA映射的机制。
调用dma_alloc_coherent函数获得的映射,可能其最小大小为单个页。
如果设备需要的DMA区域比这还小,就要用DMA池了。
30、e,size_t align, size_t allocation); name是DMA池的名字,dev是device结构,size是从该池中分配的缓冲区大小, align是该池分配操作所必须遵守的硬件对齐原则。 销毁DMA池 void dma_pool_destroy(struct dma_pool *pool); DMA池分配内存 void *dma_pool_alloc(struct dma_pool *pool,int mem_flags, dma_addr_t
31、handle); 释放内存 void dma_pool_free(struct dma_pool *pool,void *vaddr,dma_addr_t addr); --------------------------- 建立流式DMA映射 流式映射具有比一致性映射更为复杂的接口。 这些映射希望能与已经由驱动程序分配的缓冲区协同工作, 因而不得不处理那些不是它们选择的地址。 当建立流式映射时,必须告诉内核数据流动的方向。 枚举类型dma_data_direction: DMA_TO_DEVICE 数据发送到设备(如write系统调用) DMA_FROM_DEVICE
32、 数据被发送到CPU 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); 返回值是总线地址,可以把它传递给设备。 当传输完毕后,使用dma_unmap_single函数删除映射: void dma_unma
33、p_single(struct device *dev,dma_addr_t dma_addr,size_t size, enum dma_data_direction direction); 流式DMA映射的几条原则: *缓冲区只能用于这样的传送,即其传送方向匹配于映射时给定的方向。 *一旦缓冲区被映射,它将属于设备,而不是处理器。 直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。 *在DMA处于活动期间内,不能撤销对缓冲区映射,否则会严重破坏系统的稳定性。 驱动程序需要不经过撤销映射就访问流式DMA缓冲区的内容,有如下
34、调用: void dma_sync_single_for_cpu(struct device *dev,dma_handle_t bus_addr, size_t size,enum dma_data_direction direction); 将缓冲区所有权交还给设备: void dma_sync_single_for_device(struct device *dev,dma_handle_t bus_addr, size_t size,enum dma_da
35、ta_direction direction); -------------------------- 单页流式映射 有时候,要为page结构指针指向的缓冲区建立映射,比如 为get_user_pages获得的用户空间缓冲区。 dma_addr_t dma_map_page(struct device *dev,struct page *page, unsigned long offset,size_t size, enum dma_data_direction direction);
36、
void dma_unmap_page(struct device *dev,dma_addr_t dma_address,
size_t size,enum dma_data_direction direction);
---------------------------
分散/聚集映射
有几个缓冲区,它们需要与设备双向传输数据。
可以简单地依次映射每一个缓冲区并且执行请求的操作,
但是一次映射整个缓冲区表还是很有利的。
映射分散表的第一步是建立并填充一个描述被传输缓冲区的
scatterlist结构的数组。
37、atterlist.h>
scatterlist结构的成员:
struct page *page;
unsigned int length;
unsigned int offset;
映射
int dma_map_sg(struct device *dev,struct scatterlist *sg,int nents,
enum dma_data_direction direction);
解除
void dma_unmap_sg(struct device *dev,struct scatterlsit *list,
38、 int nents,enum dma_data_direction direction);
--------------------------
PCI双重地址周期映射
通用DMA支持层使用32位总线地址,然而PCI总线还支持64位地址模式,即
双重地址周期(DAC)。 39、dma(struct pci_dev *pdev,struct page *page,
unsigned long offset,int direction);
-----------------------------
一个简单的PCI DMA例子
这里提供了一个PCI设备的DMA例子dad(DMA Acquisition Device)的一部分,说明如何使用DMA映射:
int dad_transfer(struct dad_dev *dev,int write,void *buffer,size_t count 40、)
{
dma_addr_t bus_addr;
dev->dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
dev->dma_size = count;
/*映射DMA需要的缓冲区*/
bus_addr = dma_map_single(&dev->pci_dev->dev,buffer,count,dev->dma_dir);
writeb(dev->mand,DAD_CMD_DISABLEDMA);
writeb(dev->mand,write ? DAD_CMD_WR : DAD_CMD_RD);
writeb( 41、dev->registers.addr,cpu_to_le32(bus_addr)); /*设置*/
writeb(dev->registers.len,cpu_to_le32(count));
/*开始操作*/
writeb(dev->mand,DAD_CMD_ENABLEDMA);
return 0;
}
该函数映射了准备进行传输的缓冲区并且启动设备操作。
另一半工作必须在中断服务例程中完成,如:
void dad_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
struct dad_dev *dev = ( 42、struct dad_dev *)dev_id;
/* 确定中断是由对应的设备发来的*/
dma_unmap_single(dev->pci_dev->dev,dev->dma_addr,
dev->dma_size,dev->dma_dir);
/* 释放之后,才能访问缓冲区,把它拷贝给用户 */
...
}
-------------------------------------------------------------------
ISA设备的DMA
ISA总线允许两种DMA传输:本地DMA和ISA总线控制DMA 43、
只讨论本地(native)DMA。***********************非常重要!!!!!
本地DMA使用主板上的标准DMA控制器电路来驱动ISA总线上的信号线。
本地DMA,要关注三种实体:
*8237 DMA控制器(DMAC)
控制器保存了有关DMA传输的信息,如方向、内存地址、传输数据量大小等。
还包含了一个跟踪传送状态的计数器。
当控制器接收到一个DMA请求信号时,它将获得总线控制权并驱动信号线,
这样设备就能读写数据了。
*外围设备
当设备准备传送数据时,必须激活DMA请求信号。
44、 DMAC负责管理实际的传输工作;当控制器选通设备后,
硬件设备就可以顺序地读/写总线上的数据。
当传输结束时,设备通常会产生一个中断。
*设备驱动程序
设备驱动程序完成的工作很少,
它只是负责提供DMA控制器的方向、总线地址、传输量的大小等。
它还与外围设备通信,做好传输数据的准备,当DMA传输完毕后,响应中断。
在PC中使用的早期DMA控制器能够管理四个“通道”,
每个通道都与一套DMA寄存器相关联。
DMA控制器是系统资源,因此,内核协助处理这一资源。
内核使用DMA注册表为DMA通道提供了请求/释放机制,
45、
并且提供了一组函数在DMA控制器中配置通道信息。
------------------------
注册DMA
46、ile *filp)
{
sturct dad_device *my_device;
...
if((error = request_irq(my_device.irq,dad_interrupt,
SA_INTERRUPT,"dad",NULL)))
return error;
if((error = request_dma(my_device.dma,"dad")))
{
free_irq(my_device.irq,NULL);
return error;
}
...
return 47、 0;
}
close
-------
void dad_close(struct inode *inode,struct file *filp)
{
struct dad_device *my_device;
...
free_dma(my_device.dma);
free_irq(my_device.irq,NULL);
...
}
-------------------------
与DMA控制器通信
注册之后,驱动程序的主要任务包括为适当的操作配置DMA控制器。
幸运的是,内核导出了驱动程序所需要的所有函数。
当read或者write函数被调用时,或者 48、准备异步传输时,
驱动程序都要对DMA控制器进行配置。
unsigned long claim_dma_lock(); 获得DMA自旋锁
void release_dma_lock(unsigned long flags); 返回DMA自旋锁
49、r);
为DMA缓冲区分配地址。该函数将addr的最低24位存储到控制器中。
addr参数必须是总线地址。
void set_dma_count(unsigned int channel,unsigned int count);
为传输的字节量赋值。
void disable_dma(unsigned int channel);
控制器内的DMA通道可以被禁用。
void enable_dma(unsigned int channel);
该函数告诉控制器,DMA通道包含了合法的数据。
int get_dma_residue(unsigned int channel);
驱动程序有时需要知道DMA传输是否已经结束。
该函数返回还未传输的字节数。
void clear_dma_ff(unsigned int channel);
该函数清除了DMA的触发器。
使用这些函数,驱动程序可以实现如下函数,为DMA传输做准备:
----------------------------
int dad_dma_prepare(int channel,int mode,unsigned int buf,
unsigned int count)
{






