10.6 总线内存映射

  很多情况下设备驱动程序和设备之间的数据交换是通过内存 进行的。有两种可能的变体:

  (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_tbus_dmamap_t。 标签(tag)描述了DMA内存要求的特性。映射(map)表示按照这些 特性分配的内存块。多个映射可以与同一标签关联。

  标签按照对特性的继承而被组织成树型层次结构。子标签继承父 标签的所有要求,可以令其更严格,但不允许放宽要求。

  一般地,每个设备单元创建一个顶层标签(没有父标签)。如果 每个设备需要不同要求的内存区,则为每个内存区都会创建一个标签,这些 标签作为父标签的孩子。

  使用标签创建映射的方法有两种。

  其一,分配一大块符合标签要求的连续内存(以后可以被释放)。 这一般用于分配为了与设备通信而存在相对较长时间的那些内存区。 将这样的内存加载到映射中非常容易:它总是被看作位于适当物理 内存范围的一整块。

  其二,将虚拟内存中的任意区域加载到映射中。这片内存的 每一页都被检查,看是否符合映射的要求。如何符合则留在原始位置。 如果不符合则分配一个新的符合要求的 “反弹页面(bounce page)”,用作中间存储。 当从不符合的原始页面写入数据时,数据首先被拷贝到反弹页面, 然后从反弹页面传递到设备。当读取时,数据将会从设备到反弹页面, 然后被拷贝到它们不符合的原始页面。原始和反弹页面之间的拷贝 处理被称作同步。这一般用于单次传输的基础之上:每次传输时 加载缓冲区,完成传输,卸载缓冲区。

  工作在DMA内存上的函数有:

   当前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>.