很多情况下设备驱动程序和设备之间的数据交换是通过内存 进行的。有两种可能的变体:
(a) 内存位于设备卡上
(b) 内存为计算机的主内存
情况(a)中,驱动程序可能需要在卡上的内存与主存之间来回
拷贝数据。为了将卡上的内存映射到内核的虚地址空间,卡上内存的
物理地址和长度必须被定义为SYS_RES_MEMORY资源。
然后资源就可以被分配并激活,它的虚地址通过使用 rman_get_virtual()
获取。较老的驱动程序 将函数pmap_mapdev()
用于此目的,现在
不应当再直接使用此函数。它已成为资源激活的一个内部步骤。
大多数ISA卡的内存配置为物理地位于640KB-1MB范围之间的 某个位置。某些ISA卡需要更大的内存范围,位于16M以下的某个 位置(由于ISA总线上24位地址限制)。这种情况下,如果机器有 比设备内存的起始地址更多的内存(换句话说,它们重叠),则 必须在被设备使用的内存起始地址处配置一个内存空洞。许多 BIOS允许在起始于14MB或15MB处配置1M的内存空洞。如果BIOS 正确地报告内存空洞,FreeBSD就能够正确处理它们(此特性 在老BIOS上可能会出问题)。
情况(b)中,只是数据的地址被发送到设备,设备使用DMA实际 访问主存中的数据。存在两个限制:首先,ISA卡只能访问16MB以下 的内存。其次,虚地址空间中连续的页面在物理地址空间中可能不 连续,设备可能不得不进行分散/收集操作。总线子系统为这些问题 提供现成现成的解决办法,剩下的必须由驱动程序自己完成。
DMA内存分配使用了两个结构, bus_dma_tag_t
和
bus_dmamap_t
。
标签(tag)描述了DMA内存要求的特性。映射(map)表示按照这些
特性分配的内存块。多个映射可以与同一标签关联。
标签按照对特性的继承而被组织成树型层次结构。子标签继承父 标签的所有要求,可以令其更严格,但不允许放宽要求。
一般地,每个设备单元创建一个顶层标签(没有父标签)。如果 每个设备需要不同要求的内存区,则为每个内存区都会创建一个标签,这些 标签作为父标签的孩子。
使用标签创建映射的方法有两种。
其一,分配一大块符合标签要求的连续内存(以后可以被释放)。 这一般用于分配为了与设备通信而存在相对较长时间的那些内存区。 将这样的内存加载到映射中非常容易:它总是被看作位于适当物理 内存范围的一整块。
其二,将虚拟内存中的任意区域加载到映射中。这片内存的 每一页都被检查,看是否符合映射的要求。如何符合则留在原始位置。 如果不符合则分配一个新的符合要求的 “反弹页面(bounce page)”,用作中间存储。 当从不符合的原始页面写入数据时,数据首先被拷贝到反弹页面, 然后从反弹页面传递到设备。当读取时,数据将会从设备到反弹页面, 然后被拷贝到它们不符合的原始页面。原始和反弹页面之间的拷贝 处理被称作同步。这一般用于单次传输的基础之上:每次传输时 加载缓冲区,完成传输,卸载缓冲区。
工作在DMA内存上的函数有:
int bus_dma_tag_create(bus_dma_tag_t parent, bus_size_t
alignment, bus_size_t boundary, bus_addr_t lowaddr, bus_addr_t highaddr, bus_dma_filter_t
*filter, void *filterarg, bus_size_t maxsize, int nsegments, bus_size_t maxsegsz, int
flags, bus_dma_tag_t *dmat)
创建新标签。成功则返回0,否则返回错误码。
parent - 父标签或者NULL, NULL用于创建顶层标签。
alignment -
对将要分配给标签的内存区的对齐要求。“no specific alignment”时值为1。仅应用于以后的
bus_dmamem_alloc()
而不是 bus_dmamap_create()
调用。
boundary - 物理地址边界,
分配内存时不能穿过。对于“no boundary” 使用0。仅应用于以后的 bus_dmamem_alloc()
而不是 bus_dmamap_create()
调用。
必须为2的乘方。如果计划以非层叠DMA方式使用内存(也就是说, DMA地址由ISA
DMA控制器提供而不是设备自身),则由于DMA硬件 限制,边界必须不能大于64KB (64*1024)。
lowaddr, highaddr - 名字稍微 有些误导。这些值用于限制可用于内存分配的物理地址的允许 范围。其确切含义根据以后不同的使用而有所不同。
对于bus_dmamem_alloc()
,
从0到lowaddr-1的所有地址被视为允许,更高的地址不允许 使用。
对于bus_dmamap_create()
, 闭区间[lowaddr;
highaddr]之外的所有地址被视为可访问。
范围之内的地址页面被传递给过滤函数,由它决定是否可访问。
如果没有提供过滤函数,则整个区间被视为不可访问。
对于ISA设备,正常值(没有过滤函数)为:
lowaddr = BUS_SPACE_MAXADDR_24BIT
highaddr = BUS_SPACE_MAXADDR
filter, filterarg - 过滤函数及其
参数。如果filter为NULL,则当调用 bus_dmamap_create()
时,整个区间 [lowaddr,
highaddr]被视为不可访问。 否则,区间[lowaddr; highaddr]内的每个被试图访问的页面的
物理地址被传递给过滤函数,由它决定是否可访问。过滤函数的 原型为:int filterfunc(void *arg, bus_addr_t
paddr)
。当页面可以被访问时它必须 返回0,否则返回非零值。
maxsize - 通过此标签可以分配的 最大内存值(以字节计)。有时这个值很难估算,或者可以任意大, 这种情况下,对于ISA设备这个值可以设为 BUS_SPACE_MAXSIZE_24BIT。
nsegments - 设备支持的分散/收集段 的最大数目。如果不加限制,则使用应当使用值 BUS_SPACE_UNRESTRICTED。 建议对父标签使用这个值,而为子孙标签指定实际限制。 nsegments值等于 BUS_SPACE_UNRESTRICTED 的标签不能用于实际加载映射,仅可以将它们作为父标签。 nsetments 的实际限制大约为250-300,再高的值将导致内核堆栈溢出(硬件 无法正常支持那么多的分散/收集缓冲区)。
maxsegsz - 设备支持的分散/收集段 的最大尺寸。对于ISA设备的最大值为 BUS_SPACE_MAXSIZE_24BIT。
flags - 旗标的位图。感兴趣的旗标 只有:
BUS_DMA_ALLOCNOW - 创建标签时 请求分配所有可能用到的反射页面。
BUS_DMA_ISA - 比较神秘的一个标志, 仅用于Alpha机器。i386机器没有定义它。Alpha机器的所有ISA设备 都应当使用这个标志,但似乎还没有这样的驱动程序。
dmat - 指向返回的新标签的存储的 指针。
int bus_dma_tag_destroy(bus_dma_tag_t dmat)
销毁标签。成功则返回0,否则返回错误码。
dmat - 被销毁的标签。
int bus_dmamem_alloc(bus_dma_tag_t dmat, void** vaddr, int
flags, bus_dmamap_t *mapp)
分配标签所描述的一块连续内存区。被分配的内存的大小为标签的
maxsize。成功则返回0,否则返回错误码。调用结果被用于获取内存的
物理地址,但在此之前必须用bus_dmamap_load()
将其加载。
dmat - 标签
vaddr - 指向存储的指针,该存储空间 用于返回的分配区域的内核虚地址。
flags - 旗标的位图。唯一感兴趣的旗标为:
BUS_DMA_NOWAIT - 如果内存不能 立即可用则返回错误。如果此标志没有设置,则允许例程 睡眠,直到内存可用为止。
mapp - 指向返回的新映射的存储的指针。
void bus_dmamem_free(bus_dma_tag_t dmat, void *vaddr,
bus_dmamap_t map)
释放由bus_dmamem_alloc()
分配的内存。
目前,对分配的带有ISA限制的内存的释放没有实现。因此,建议的
使用模型为尽可能长时间地保持和重用分配的区域。不要轻易地
释放某些区域,然后再短时间地分配它。这并不意味着不应当使用 bus_dmamem_free()
:希望很快它就会被 完整地实现。
dmat - 标签
vaddr - 内存的内核虚地址
map - 内存的映射(跟 bus_dmamem_alloc()
返回的一样)
int bus_dmamap_create(bus_dma_tag_t dmat, int flags,
bus_dmamap_t *mapp)
为标签创建映射,以后用于 bus_dmamap_load()
。成功则返回0,否则 返回错误码。
dmat - 标签
flags - 理论上是旗标的位图。但还 从未定义过任何旗标,因此目前总是0。
mapp - 指向返回的新映射的存储的指针。
int bus_dmamap_destroy(bus_dma_tag_t dmat, bus_dmamap_t
map)
销毁映射。成功则返回0,否则返回错误码。
dmat - 与映射关联的标签
map - 将要被销毁的映射
int bus_dmamap_load(bus_dma_tag_t dmat, bus_dmamap_t map,
void *buf, bus_size_t buflen, bus_dmamap_callback_t *callback, void *callback_arg, int
flags)
加载缓冲区到映射中(映射必须事先由 bus_dmamap_create()
或者 bus_dmamem_alloc()
)创建。缓冲区的所有
页面都会被检查,看是否符合标签的要求,并为那些不符合的分配
反弹页面。会创建物理段描述符的数组,并将其传递给回调函数。
回调函数以某种方式处理这个数组。系统中的反弹缓冲区是受限的,
因此如果需要的反弹缓冲区不能立即获得,则将请求入队,当反弹
缓冲区可用时再调用回调函数。如果回调函数立即执行则返回0,
如果请求被排队,等待将来执行,则返回 “EINPROGRESS”。后一种情况下,
与排队的回调函数之间的同步由驱动程序负责。
dmat - 标签
map - 映射
buf - 缓冲区的内核虚地址
buflen - 缓冲区的长度
callback,
callback_arg
- 回调函数及其参数
回调函数的原型为:
void callback(void *arg, bus_dma_segment_t *seg, int
nseg, int error)
arg - 与传递给 bus_dmamap_load()
的callback_arg 相同。
seg - 段描述符的数组
nseg - 数组中的描述符个数
error - 表示段数目溢出:如被设为 “EFBIG”, 则标签允许的最大数目的段无法容纳缓冲区。 这种情况下数组中的描述符的数目只有标签许可的那么多。 对这种情况的处理由驱动程序决定:根据希望的语义,驱动 程序可以视其为错误,或将缓冲区分为两个并单独处理第二个。
段数组中的每一项包含如下字段:
ds_addr - 段物理地址
ds_len - 段长度
void bus_dmamap_unload(bus_dma_tag_t dmat, bus_dmamap_t
map)
unload the map.
dmat - 标签
map - 已加载的映射
void bus_dmamap_sync (bus_dma_tag_t dmat, bus_dmamap_t
map, bus_dmasync_op_t op)
与设备进行物理传输前后,将加载的缓冲区与其反弹页面进行同步。 此函数完成原始缓冲区与其映射版本之间所有必需的数据拷贝工作。 进行传输之前和之后必须对缓冲区进行同步。
dmat - 标签
map - 已加载的映射
op - 要执行的同步操作的类型:
BUS_DMASYNC_PREREAD
- 从设备到
缓冲区的读操作之前
BUS_DMASYNC_POSTREAD
- 从设备到
缓冲区的读操作之后
BUS_DMASYNC_PREWRITE
- 从缓冲区到
设备的写操作之前
BUS_DMASYNC_POSTWRITE
- 从缓冲区到
设备的写操作之后
当前PREREAD和POSTWRITE为空操作,但将来可能会改变,因此驱动程序
中不能忽略它们。由bus_dmamem_alloc()
获得的内存不需要同步。
从bus_dmamap_load()
中调用回调函数之前,
段数组是存储在栈中的。并且是按标签允许的最大数目的段预先分配
好的。这样由于i386体系结构上对段数目的实际限制约为250-300
(内核栈为4KB减去用户结构的大小,段数组条目的大小为8字节,和
其它必须留出来的空间)。由于数组基于最大数目而分配,因此这个值
必须不能设置成超出实际需要。幸运的是,对于大多数硬件而言,
所支持的段的最大数目低很多。但如果驱动程序想处理具有非常多
分散/收集段的缓冲区,则应当一部分一部分地处理:加载缓冲区的
一部分,传输到设备,然后加载缓冲区的下一部分,如此反复。
另一个实践结论是段数目可能限制缓冲区的大小。如果缓冲区中的 所有页面碰巧物理上不连续,则分片情况下支持的最大缓冲区尺寸 为(nsegments * page_size)。例如,如果支持的段的最大数目为10, 则在i386上可以确保支持的最大缓冲区大小为40K。如果希望更大的 则需要在驱动程序中使用一些特殊技巧。
如果硬件根本不支持分散/收集,或者驱动程序希望即使在严重分片的 情况下仍然支持某种缓冲区大小,则解决办法是:如果无法容纳下原始 缓冲区,就在驱动程序中分配一个连续的缓冲区作为中间存储。
下面是当使用映射时的典型调用顺序,根据对映射的具体使用而不同。 字符->用于显示时间流。
对于从连接到分离设备,这期间位置一直不变的缓冲区:
bus_dmamem_alloc -> bus_dmamap_load -> ...use buffer... -> -> bus_dmamap_unload -> bus_dmamem_free
对于从驱动程序外部传递进去,并且经常变化的缓冲区:
bus_dmamap_create -> -> bus_dmamap_load -> bus_dmamap_sync(PRE...) -> do transfer -> -> bus_dmamap_sync(POST...) -> bus_dmamap_unload -> ... -> bus_dmamap_load -> bus_dmamap_sync(PRE...) -> do transfer -> -> bus_dmamap_sync(POST...) -> bus_dmamap_unload -> -> bus_dmamap_destroy
当加载由bus_dmamem_alloc()
创建的映射时,传递
进去的缓冲区的地址和大小必须和 bus_dmamem_alloc()
中使用的一样。这种情况下就
可以保证整个缓冲区被作为一个段而映射(因而回调可以基于此假设),
并且请求被立即执行(永远不会返回EINPROGRESS)。这种情况下回调函数
需要作的只是保存物理地址。
典型示例如下:
static void alloc_callback(void *arg, bus_dma_segment_t *seg, int nseg, int error) { *(bus_addr_t *)arg = seg[0].ds_addr; } ... int error; struct somedata { .... }; struct somedata *vsomedata; /* 虚地址 */ bus_addr_t psomedata; /* 物理总线相关的地址 */ bus_dma_tag_t tag_somedata; bus_dmamap_t map_somedata; ... error=bus_dma_tag_create(parent_tag, alignment, boundary, lowaddr, highaddr, /*filter*/ NULL, /*filterarg*/ NULL, /*maxsize*/ sizeof(struct somedata), /*nsegments*/ 1, /*maxsegsz*/ sizeof(struct somedata), /*flags*/ 0, &tag_somedata); if(error) return error; error = bus_dmamem_alloc(tag_somedata, &vsomedata, /* flags*/ 0, &map_somedata); if(error) return error; bus_dmamap_load(tag_somedata, map_somedata, (void *)vsomedata, sizeof (struct somedata), alloc_callback, (void *) &psomedata, /*flags*/0);
代码看起来有点长,也比较复杂,但那是正确的使用方法。实际结果是: 如果分配多个内存区域,则总将它们组合成一个结构并作为整体分配 (如果对齐和边界限制允许的话)是一个很好的主意。
当加载任意缓冲区到由bus_dmamap_create()
创建的映射时,由于回调可能被延迟,因此必须采取特殊措施与回调
函数进行同步。代码看起来像下面的样子:
{ int s; int error; s = splsoftvm(); error = bus_dmamap_load( dmat, dmamap, buffer_ptr, buffer_len, callback, /*callback_arg*/ buffer_descriptor, /*flags*/0); if (error == EINPROGRESS) { /* * 执行必要的操作以确保与回调的同步。 * 回调被确保直到我们执行了splx()或tsleep()才会被调用。 */ } splx(s); }
处理请求的两种方法分别是:
1. 如果通过显式地标记请求已经结束来完成请求(例如CAM请求),则 将所有进一步的处理放入回调驱动程序中会比较简单,回调结束后会 标记请求。之后不需要太多额外的同步。由于流控制的原因,冻结请求 队列直到请求完成才释放可能是个好主意。
2. 如果请求是在函数返回时完成(例如字符设备上传统的读写请求),
则需要在缓冲区描述符上设置同步标志,并调用 tsleep()
。后面当回调函数被调用时,它将
执行处理并检查同步标志。如果设置了同步标志,它应该发出一个
唤醒操作。在这种方法中,回调函数或者进行所由必需的处理(就像
前面的情况),或者简单在缓冲区描述符中存储段数组。回调完成
后,回调函数就能使用这个存储的段数组并进行所有的处理。
本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.
如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系<questions@FreeBSD.org>.
关于本文档的问题请发信联系 <doc@FreeBSD.org>.