小公司研发总监,既当司令也当兵!
分类: linux
2015-09-29 15:32:13
原文地址:linux内核dma机制 作者:piaoyizu
目录 |
dma允许外围设备和主内存之间直接传输 i/o 数据, dma 依赖于系统。每一种体系结构dma传输不同,编程接口也不同。
数据传输可以以两种方式触发:一种软件请求数据,另一种由硬件异步传输。
在第一种情况下,调用的步骤可以概括如下(以read为例):
(1)在进程调用 read 时,驱动程序的方法分配一个 dma 缓冲区,随后指示硬件传送它的数据。进程进入睡眠。
(2)硬件将数据写入 dma 缓冲区并在完成时产生一个中断。
(3)中断处理程序获得输入数据,应答中断,最后唤醒进程,该进程现在可以读取数据了。
第二种情形是在 dma 被异步使用时发生的。以数据采集设备为例:
(1)硬件发出中断来通知新的数据已经到达。
(2)中断处理程序分配一个dma缓冲区。
(3)外围设备将数据写入缓冲区,然后在完成时发出另一个中断。
(4)处理程序利用dma分发新的数据,唤醒任何相关进程。
网卡传输也是如此,网卡有一个循环缓冲区(通常叫做 dma 环形缓冲区)建立在与处理器共享的内存中。每一个输入数据包被放置在环形缓冲区中下一个可用缓冲区,并且发出中断。然后驱动程序将网络数据包传给内核的其它部分处理,并在环形缓冲区中放置一个新的 dma 缓冲区。
驱动程序在初始化时分配dma缓冲区,并使用它们直到停止运行。
dma控制器依赖于平台硬件,这里只对i386的8237 dma控制器做简单的说明,它有两个控制器,8个通道,具体说明如下:
控制器1: 通道0-3,字节操作, 端口为 00-1f
控制器2: 通道 4-7, 字操作, 端口咪 c0-df
- 所有寄存器是8 bit,与传输大小无关。
- 通道 4 被用来将控制器1与控制器2级联起来。
- 通道 0-3 是字节操作,地址/计数都是字节的。
- 通道 5-7 是字操作,地址/计数都是以字为单位的。
- 传输器对于(0-3通道)必须不超过64k的物理边界,对于5-7必须不超过128k边界。
- 对于5-7通道page registers 不用数据 bit 0, 代表128k页
- 对于0-3通道page registers 使用 bit 0, 表示 64k页
dma 传输器限制在低于16m物理内存里。装入寄存器的地址必须是物理地址,而不是逻辑地址。
对于0-3通道来说地址对寄存器的映射如下:a23 ... a16 a15 ... a8 a7 ... a0 (物理地址) | ... | | ... | | ... | | ... | | ... | | ... | | ... | | ... | | ... | p7 ... p0 a7 ... a0 a7 ... a0 | page | addr msb | addr lsb | (dma 地址寄存器)
a23 ... a17 a16 a15 ... a9 a8 a7 ... a1 a0 (物理地址)| ... | \ \ ... \ \ \ ... \ \ | ... | \ \ ... \ \ \ ... \ (没用) | ... | \ \ ... \ \ \ ... \ p7 ... p1 (0) a7 a6 ... a0 a7 a6 ... a0| page | addr msb | addr lsb | (dma 地址寄存器)
在include/asm-i386/dma.h中有i386平台的8237 dma控制器的各处寄存器的地址及寄存器的定义,这里只对控制寄存器加以说明:
dma channel control/status register (dcsrx)
第31位 表明是否开始
第30位 选定descriptor和non-descriptor模式
第29位 判断有无中断
第8位 请求处理 (request pending)
第3位 channel是否运行
第2位 当前数据交换是否完成
第1位 是否由descriptor产生中断
第0位 是否由总线错误引起中断
struct dma_chan { int lock; const char *device_id; }; static struct dma_chan dma_chan_busy[max_dma_channels] = { [4] = { 1, "cascade" }, };
由于dma需要连续的内存,因而在引导时分配内存或者为缓冲区保留物理 ram 的顶部。在引导时给内核传递一个"mem="参数可以保留 ram 的顶部。例如,如果系统有 32mb 内存,参数"mem=31m"阻止内核使用最顶部的一兆字节。稍后,模块可以使用下面的代码来访问这些保留的内存:
dmabuf = ioremap( 0x1f00000 /* 31m */, 0x100000 /* 1m */);
分配 dma 空间的方法,代码调用 kmalloc(gfp_atomic) 直到失败为止,然后它等待内核释放若干页面,接下来再一次进行分配。最终会发现由连续页面组成的dma 缓冲区的出现。
一个使用 dma 的设备驱动程序通常会与连接到接口总线上的硬件通讯,这些硬件使用物理地址,而程序代码使用虚拟地址。基于 dma 的硬件使用总线地址而不是物理地址,有时,接口总线是通过将 i/o 地址映射到不同物理地址的桥接电路连接的。甚至某些系统有一个页面映射方案,能够使任意页面在外围总线上表现为连续的。
当驱动程序需要向一个 i/o 设备(例如扩展板或者dma控制器)发送地址信息时,必须使用 virt_to_bus 转换,在接受到来自连接到总线上硬件的地址信息时,必须使用 bus_to_virt 了。
因为 dma 控制器是一个系统级的资源,所以内核协助处理这一资源。内核使用 dma 注册表为 dma 通道提供了请求/释放机制,并且提供了一组函数在 dma 控制器中配置通道信息。
dma 控制器使用函数request_dma和free_dma来获取和释放 dma 通道的所有权,请求 dma 通道应在请求了中断线之后,并且在释放中断线之前释放它。每一个使用 dma 的设备也必须使用中断信号线,否则就无法发出数据传输完成的通知。这两个函数的声明列出如下(在kernel/dma.c中):int request_dma(unsigned int channel, const char *name); void free_dma(unsigned int channel);
unsigned long claim_dma_lock(); 获取 dma 自旋锁,该函数会阻塞本地处理器上的中断,因此,其返回值是"标志"值,在重新打开中断时必须使用该值。
void release_dma_lock(unsigned long flags); 释放 dma 自旋锁,并且恢复以前的中断状态。
dma 控制器的控制设置信息由ram 地址、传输的数据(以字节或字为单位),以及传输的方向三部分组成。下面是i386平台的8237 dma控制器的操作函数说明(在include/asm-i386/dma.h中),使用这些函数设置dma控制器时,应该持有自旋锁。但在驱动程序做i/o 操作时,不能持有自旋锁。
void set_dma_mode(unsigned int channel, char mode); 该函数指出通道从设备读(dma_mode_write)或写(dma_mode_read)数据方式,当mode设置为 dma_mode_cascade时,表示释放对总线的控制。
void set_dma_addr(unsigned int channel, unsigned int addr); 函数给 dma 缓冲区的地址赋值。该函数将 addr 的最低 24 位存储到控制器中。参数 addr 是总线地址。
void set_dma_count(unsigned int channel, unsigned int count);该函数对传输的字节数赋值。参数 count 也代表 16 位通道的字节数,在此情况下,这个数字必须是偶数。
除了这些操作函数外,还有些对dma状态进行控制的工具函数:
void disable_dma(unsigned int channel); 该函数设置禁止使用dma 通道。这应该在配置 dma 控制器之前设置。
void enable_dma(unsigned int channel); 在dma 通道中包含了合法的数据时,该函数激活dma 控制器。
int get_dma_residue(unsigned int channel); 该函数查询一个 dma 传输还有多少字节还没传输完。函数返回没传完的字节数。当传输成功时,函数返回值是0。
void clear_dma_ff(unsigned int channel) 该函数清除 dma 触发器(flip-flop),该触发器用来控制对 16 位寄存器的访问。可以通过两个连续的 8 位操作来访问这些寄存器,触发器被清除时用来选择低字节,触发器被置位时用来选择高字节。在传输 8 位后,触发器会自动反转;在访问 dma 寄存器之前,程序员必须清除触发器(将它设置为某个已知状态)。
一个dma映射就是分配一个 dma 缓冲区并为该缓冲区生成一个能够被设备访问的地址的组合操作。一般情况下,简单地调用函数virt_to_bus 就设备总线上的地址,但有些硬件映射寄存器也被设置在总线硬件中。映射寄存器(mapping register)是一个类似于外围设备的虚拟内存等价物。在使用这些寄存器的系统上,外围设备有一个相对较小的、专用的地址区段,可以在此区段执行 dma。通过映射寄存器,这些地址被重映射到系统 ram。映射寄存器具有一些好的特性,包括使分散的页面在设备地址空间看起来是连续的。但不是所有的体系结构都有映射寄存器,特别地,pc 平台没有映射寄存器。
在某些情况下,为设备设置有用的地址也意味着需要构造一个反弹(bounce)缓冲区。例如,当驱动程序试图在一个不能被外围设备访问的地址(一个高端内存地址)上执行 dma 时,反弹缓冲区被创建。然后,按照需要,数据被复制到反弹缓冲区,或者从反弹缓冲区复制。
根据 dma 缓冲区期望保留的时间长短,pci 代码区分两种类型的 dma 映射:
static inline void *pci_alloc_consistent(struct pci_dev *hwdev, size_t size, dma_addr_t *dma_handle) { return dma_alloc_coherent(hwdev == null ? null : &hwdev->dev, size, dma_handle, gfp_atomic); }
struct dma_coherent_mem {void *virt_base; u32 device_base; int size; int flags; unsigned long *bitmap;
};
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int gfp) { void *ret; //若是设备,得到设备的dma内存区域,即mem= dev->dma_mem struct dma_coherent_mem *mem = dev ? dev->dma_mem : null; int order = get_order(size);//将size转换成order,即 //忽略特定的区域,因而忽略这两个标识 gfp &= ~(__gfp_dma | __gfp_highmem); if (mem) {//设备的dma映射,mem= dev->dma_mem //找到mem对应的页 int page = bitmap_find_free_region(mem->bitmap, mem->size, order); if (page >= 0) { *dma_handle = mem->device_base (page << page_shift); ret = mem->virt_base (page << page_shift); memset(ret, 0, size); return ret; } if (mem->flags & dma_memory_exclusive) return null; } //不是设备的dma映射 if (dev == null || (dev->coherent_dma_mask < 0xffffffff)) gfp |= gfp_dma; //分配空闲页 ret = (void *)__get_free_pages(gfp, order); if (ret != null) { memset(ret, 0, size);//清0 *dma_handle = virt_to_phys(ret);//得到物理地址 } return ret; }
在流式 dma 映射的操作中,缓冲区传送方向应匹配于映射时给定的方向值。缓冲区被映射后,它就属于设备而不再属于处理器了。在缓冲区调用函数pci_unmap_single撤销映射之前,驱动程序不应该触及其内容。
在缓冲区为 dma 映射时,内核必须确保缓冲区中所有的数据已经被实际写到内存。可能有些数据还会保留在处理器的高速缓冲存储器中,因此必须显式刷新。在刷新之后,由处理器写入缓冲区的数据对设备来说也许是不可见的。
如果欲映射的缓冲区位于设备不能访问的内存区段时,某些体系结构仅仅会操作失败,而其它的体系结构会创建一个反弹缓冲区。反弹缓冲区是被设备访问的独立内存区域,反弹缓冲区复制原始缓冲区的内容。
函数pci_map_single映射单个用于传送的缓冲区,返回值是可以传递给设备的总线地址,如果出错的话就为 null。一旦传送完成,应该使用函数pci_unmap_single 删除映射。其中,参数direction为传输的方向,取值如下:
pci_dma_todevice 数据被发送到设备。
pci_dma_fromdevice如果数据将发送到 cpu。
pci_dma_bidirectional数据进行两个方向的移动。
pci_dma_none 这个符号只是为帮助调试而提供。
函数pci_map_single分析如下(在arch/i386/kernel/pci-dma.c中):static inline dma_addr_t pci_map_single(struct pci_dev *hwdev, void *ptr, size_t size, int direction) { return dma_map_single(hwdev == null ? null : &hwdev->dev, ptr, size, (enum ma_data_direction)direction); }
static inline dma_addr_t dma_map_single(struct device *dev, void *ptr,size_t size, enum dma_data_direction direction){ bug_on(direction == dma_none); //可能有些数据还会保留在处理器的高速缓冲存储器中,因此必须显式刷新 flush_write_buffers(); return virt_to_phys(ptr); //虚拟地址转化为物理地址
}
分散/集中映射是流式 dma 映射的一个特例。它将几个缓冲区集中到一起进行一次映射,并在一个 dma 操作中传送所有数据。这些分散的缓冲区由分散表结构scatterlist来描述,多个分散的缓冲区的分散表结构组成缓冲区的struct scatterlist数组。
分散表结构列出如下(在include/asm-i386/scatterlist.h):struct scatterlist { struct page *page; unsigned int offset; dma_addr_t dma_address; //用在分散/集中操作中的缓冲区地址 unsigned int length;//该缓冲区的长度 };
//从该分散表项中返回总线地址#define sg_dma_address(sg) �sg)->dma_address) //返回该缓冲区的长度
#define sg_dma_len(sg) �sg)->length)
static inline int pci_map_sg(struct pci_dev *hwdev, struct scatterlist *sg, int nents, int direction) { return dma_map_sg(hwdev == null ? null : &hwdev->dev, sg, nents, (enum dma_data_direction)direction); } include/asm-i386/dma-mapping.h static inline int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction) { int i; bug_on(direction == dma_none); for (i = 0; i < nents; i ) { bug_on(!sg[i].page); //将页及页偏移地址转化为物理地址 sg[i].dma_address = page_to_phys(sg[i].page) sg[i].offset; } //可能有些数据还会保留在处理器的高速缓冲存储器中,因此必须显式刷新 flush_write_buffers(); return nents; }
许多驱动程序需要又多又小的一致映射内存区域给dma描述子或i/o缓存buffer,这使用dma池比用dma_alloc_coherent分配的一页或多页内存区域好,dma池用函数dma_pool_create创建,用函数dma_pool_alloc从dma池中分配一块一致内存,用函数dmp_pool_free放内存回到dma池中,使用函数dma_pool_destory释放dma池的资源。
结构dma_pool是dma池描述结构,列出如下:struct dma_pool { /* the pool */ struct list_head page_list;//页链表 spinlock_t lock; size_t blocks_per_page; //每页的块数 size_t size; //dma池里的一致内存块的大小 struct device *dev; //将做dma的设备 size_t allocation; //分配的没有跨越边界的块数,是size的整数倍 char name [32]; //池的名字 wait_queue_head_t waitq; //等待队列 struct list_head pools; };
函数dma_pool_create返回创建的带有要求字符串的dma池,若创建失败返回null。对被给的dma池,函数dma_pool_alloc被用来分配内存,这些内存都是一致dma映射,可被设备访问,且没有使用缓存刷新机制,因为对齐原因,分配的块的实际尺寸比请求的大。如果分配非0的内存,从函数dma_pool_alloc返回的对象将不跨越size边界(如不跨越4k字节边界)。这对在个体的dma传输上有地址限制的设备来说是有利的。
函数dma_pool_create分析如下(在drivers/base/dmapool.c中):struct dma_pool *dma_pool_create (const char *name, struct device *dev, size_t size, size_t align, size_t allocation) { struct dma_pool *retval; if (align == 0) align = 1; if (size == 0) return null; else if (size < align) size = align; else if ((size % align) != 0) {//对齐处理 size = align 1; size &= ~(align - 1); } //如果一致内存块比页大,是分配为一致内存块大小,否则,分配为页大小 if (allocation == 0) { if (page_size < size)//页比一致内存块小 allocation = size; else allocation = page_size;//页大小 // fixme: round up for less fragmentation } else if (allocation < size) return null; //分配dma_pool结构对象空间 if (!(retval = kmalloc (sizeof *retval, slab_kernel))) return retval; strlcpy (retval->name, name, sizeof retval->name); retval->dev = dev; //初始化dma_pool结构对象retval init_list_head (&retval->page_list);//初始化页链表 spin_lock_init (&retval->lock); retval->size = size; retval->allocation = allocation; retval->blocks_per_page = allocation / size; init_waitqueue_head (&retval->waitq);//初始化等待队列 if (dev) {//设备存在时 down (&pools_lock); if (list_empty (&dev->dma_pools)) //给设备创建sysfs文件系统属性文件 device_create_file (dev, &dev_attr_pools); /* note: not currently insisting "name" be unique */ list_add (&retval->pools, &dev->dma_pools); //将dma池加到dev中 up (&pools_lock); } else init_list_head (&retval->pools); return retval; }
函数dma_pool_alloc包裹了dma_alloc_coherent页分配器,这样小块更容易被总线的主控制器使用。这可能共享slab分配器的内容。
函数dma_pool_alloc分析如下(在drivers/base/dmapool.c中):void *dma_pool_alloc (struct dma_pool *pool, int mem_flags, dma_addr_t *handle) { unsigned long flags; struct dma_page *page; int map, block; size_t offset; void *retval; restart: spin_lock_irqsave (&pool->lock, flags); list_for_each_entry(page, &pool->page_list, page_list) { int i; /* only cachable accesses here ... */ //遍历一页的每块,而每块又以32字节递增 for (map = 0, i = 0; i < pool->blocks_per_page; //每页的块数 i = bits_per_long, map) { // bits_per_long定义为32 if (page->bitmap [map] == 0) continue; block = ffz (~ page->bitmap [map]);//找出第一个0 if ((i block) < pool->blocks_per_page) { clear_bit (block, &page->bitmap [map]); //得到相对于页边界的偏移 offset = (bits_per_long * map) block; offset *= pool->size; goto ready; } } } //给dma池分配dma_page结构空间,加入到pool->page_list链表, //并作dma一致映射,它包括分配给dma池一页。 // slab_atomic表示调用 kmalloc(gfp_atomic) 直到失败为止, //然后它等待内核释放若干页面,接下来再一次进行分配。 if (!(page = pool_alloc_page (pool, slab_atomic))) { if (mem_flags & __gfp_wait) { declare_waitqueue (wait, current); current->state = task_interruptible; add_wait_queue (&pool->waitq, &wait); spin_unlock_irqrestore (&pool->lock, flags); schedule_timeout (pool_timeout_jiffies); remove_wait_queue (&pool->waitq, &wait); goto restart; } retval = null; goto done; } clear_bit (0, &page->bitmap [0]); offset = 0; ready: page->in_use ; retval = offset page->vaddr; //返回虚拟地址 *handle = offset page->dma; //相对dma地址 #ifdef config_debug_slab memset (retval, pool_poison_allocated, pool->size); #endif done: spin_unlock_irqrestore (&pool->lock, flags); return retval; }
示例:下面是一个简单的使用dma进行传输的驱动程序,它是一个假想的设备,只列出dma相关的部分来说明驱动程序中如何使用dma的。
函数dad_transfer是设置dma对内存buffer的传输操作函数,它使用流式映射将buffer的虚拟地址转换到物理地址,设置好dma控制器,然后开始传输数据。int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count) { dma_addr_t bus_addr; unsigned long flags; /* map the buffer for dma */ dev->dma_dir = (write ? pci_dma_todevice : pci_dma_fromdevice); dev->dma_size = count; //流式映射,将buffer的虚拟地址转化成物理地址 bus_addr = pci_map_single(dev->pci_dev, buffer, count, dev->dma_dir); dev->dma_addr = bus_addr; //dma传送的buffer物理地址 //将操作控制写入到dma控制器寄存器,从而建立起设备 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));//buffer物理地址 writel(dev->registers.len, cpu_to_le32(count)); //传输的字节数 //开始激活dma进行数据传输操作 writeb(dev->registers.command, dad_cmd_enabledma); return 0; }
void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs){
struct dad_dev *dev = (struct dad_dev *) dev_id;
/* make sure it's really our device interrupting */
/* unmap the dma buffer */ pci_unmap_single(dev->pci_dev, dev->dma_addr, dev->dma_size, dev->dma_dir);
/* only now is it safe to access the buffer, copy to user, etc. */ ...
}
int dad_open (struct inode *inode, struct file *filp){
struct dad_device *my_device;
// sa_interrupt表示快速中断处理且不支持共享 irq 信号线 if ( (error = request_irq(my_device.irq, dad_interrupt, sa_interrupt, "dad", null)) ) return error; /* or implement blocking open */
if ( (error = request_dma(my_device.dma, "dad")) ) { free_irq(my_device.irq, null); return error; /* or implement blocking open */ }
return 0;}
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); ……}
int dad_dma_prepare(int channel, int mode, unsigned int buf,unsigned int count){
unsigned long flags;
flags = claim_dma_lock(); disable_dma(channel); clear_dma_ff(channel); set_dma_mode(channel, mode); set_dma_addr(channel, virt_to_bus(buf)); set_dma_count(channel, count); enable_dma(channel); release_dma_lock(flags);
return 0;}
int dad_dma_isdone(int channel) { int residue; unsigned long flags = claim_dma_lock (); residue = get_dma_residue(channel); release_dma_lock(flags); return (residue == 0); }