📄 ch15s04.html
字号:
<html xmlns:cf="http://docbook.sourceforge.net/xmlns/chunkfast/1.0"><head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>15.4. 直接内存存取-Linux设备驱动第三版(中文版)</title><meta name="description" content="驱动开发" /><meta name="keywords" content="Linux设备驱动,中文版,第三版,ldd,linux device driver,驱动开发,电子版,程序设计,软件开发,开发频道" /><meta name="verify-v1" content="5asbXwkS/Vv5OdJbK3Ix0X8osxBUX9hutPyUxoubhes=" /><link rel="stylesheet" href="docbook.css" type="text/css"><meta name="generator" content="DocBook XSL Stylesheets V1.69.0"><link rel="start" href="index.html" title="Linux 设备驱动 Edition 3"><link rel="up" href="ch15.html" title="第 15 章 内存映射和 DMA "><link rel="prev" href="ch15s03.html" title="15.3. 进行直接 I/O"><link rel="next" href="ch15s05.html" title="15.5. 快速参考"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">15.4. 直接内存存取</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch15s03.html">上一页</a> </td><th width="60%" align="center">第 15 章 内存映射和 DMA </th><td width="20%" align="right"> <a accesskey="n" href="ch15s05.html">下一页</a></td></tr></table><hr></div><div class="sect1" lang="zh-cn"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="DirectMemoryAccess.sect1"></a>15.4. 直接内存存取</h2></div></div></div><p>直接内存存取, 或者 DMA, 是结束我们的内存问题概览的高级主题. DMA 是硬件机制允许外设组件来直接传输它们的 I/O 数据到和从主内存, 而不需要包含系统处理器. 这种机制的使用能够很大提高吞吐量到和从一个设备, 因为大量的计算开销被削减了.</p><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="OverviewofDMADataTransfer.sect2"></a>15.4.1. 一个 DMA 数据传输的概况</h3></div></div></div><p>在介绍程序细节之前, 让我们回顾一个 DMA 传输如何发生的, 只考虑输入传输来简化讨论.</p><p>数据传输可由 2 种方法触发:或者软件请求数据(通过一个函数例如 read)或者硬件异步推数据到系统.</p><p>在第一种情况, 包含的步骤总结如下:</p><div class="itemizedlist"><ul type="disc"><li><p>1. 当一个进程调用 read, 驱动方法分配一个 DMA 缓冲并引导硬件来传输它的数据到那个缓冲. 这个进程被置为睡眠.</p></li><li><p>2. 硬件写数据到这个 DMA 缓冲并且在它完成时引发一个中断.</p></li><li><p>3. 中断处理获得输入数据, 确认中断, 并且唤醒进程, 它现在可以读数据了.</p></li></ul></div><p>第 2 种情况到来是当 DMA 被异步使用. 例如, 这发生在数据获取设备, 它在没有人读它们的时候也持续推入数据. 在这个情况下, 驱动应当维护一个缓冲以至于后续的读调用能返回所有的累积的数据给用户空间. 这类传输包含的步骤有点不同:</p><div class="itemizedlist"><ul type="disc"><li><p>1. 硬件引发一个中断来宣告新数据已经到达.</p></li><li><p>2. 中断处理分配一个缓冲并且告知硬件在哪里传输数据.</p></li><li><p>3. 外设写数据到缓冲并且引发另一个中断当完成时.</p></li><li><p>处理者分派新数据, 唤醒任何相关的进程, 并且负责杂务.</p></li></ul></div><p>异步方法的变体常常在网卡中见到. 这些卡常常期望见到一个在内存中和处理器共享的环形缓冲(常常被称为一个 DMA 的缓冲); 每个到来的报文被放置在环中下一个可用的缓冲, 并且发出一个中断. 驱动接着传递网络本文到内核其他部分并且在环中放置一个新 DMA 缓冲.</p><p>在所有这些情况中的处理的步骤都强调, 有效的 DMA 处理依赖中断报告. 虽然可能实现 DMA 使用一个轮询驱动, 它不可能有意义, 因为一个轮询驱动可能浪费 DMA 提供的性能益处超过更容易的处理器驱动的I/O.<sup>[<a name="id498425" href="#ftn.id498425">49</a>]</sup></p><p>在这里介绍的另一个相关项是 DMA 缓冲. DMA 要求设备驱动来分配一个或多个特殊的适合 DMA 的缓冲. 注意许多驱动分配它们的缓冲在初始化时并且使用它们直到关闭 -- 在之前列表中的分配一词, 意思是"获得一个之前分配的缓冲".</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="AllocationgtheDMABuffer.sect2"></a>15.4.2. 分配 DMA 缓冲</h3></div></div></div><p>本节涵盖 DMA 缓冲在底层的分配; 我们稍后介绍一个高级接口, 但是来理解这里展示的内容仍是一个好主意.</p><p>随 DMA 缓冲带来的主要问题是, 当它们大于一页, 它们必须占据物理内存的连续页因为设备使用 ISA 或者 PCI 系统总线传输数据, 它们都使用物理地址. 注意有趣的是这个限制不适用 SBus ( 见 12 章的"SBus"一节 ), 它在外设总线上使用虚拟地址. 一些体系结构还可以在 PCI 总线上使用虚拟地址, 但是一个可移植的驱动不能依赖这个功能.</p><p>尽管 DMA 缓冲可被分配或者在系统启动时或者在运行时, 模块只可在运行时分配它们的缓冲. (第 8 章介绍这些技术; "获取大缓冲"一节涵盖在系统启动时分配, 而"kmalloc 的真实"和"get_free_page 和其友"描述在运行时分配). 驱动编写者必须关心分配正确的内存,当它被用做 DMA 操作时; 不是所有内存区是合适的. 特别的, 在一些系统中的一些设备上高端内存可能不为 DMA 工作 - 外设完全无法使用高端地址.</p><p>在现代总线上的大部分设备可以处理 32-位 地址, 意思是正常的内存分配对它们是刚刚好的. 一些 PCI 设备, 但是, 不能实现完整的 PCI 标准并且不能使用 32-位 地址. 并且 ISA 设备, 当然, 限制只在 24-位 地址.</p><p>对于有这种限制的设备, 内存应当从 DMA 区进行分配, 通过添加 GFP_DMA 标志到 kmalloc 或者 get_free_pages 调用. 当这个标志存在, 只有可用 24-位 寻址的内存被分配. 另一种选择, 你可以使用通用的 DMA 层( 我们马上讨论这个 )来分配缓冲以解决你的设备的限制.</p><div class="sect3" lang="zh-cn"><div class="titlepage"><div><div><h4 class="title"><a name="Doityourselfallocation.sect3"></a>15.4.2.1. 自己做分配</h4></div></div></div><p>我们已见到 get_free_pages 如何分配直到几个 MByte (由于 order 可以直到 MAX_ORDER, 当前是 11), 但是高级数的请求容易失败当请求的缓冲远远小于 128 KB, 因为系统内存时间长了变得碎裂.<sup>[<a name="id498543" href="#ftn.id498543">50</a>]</sup></p><p>当内核无法返回请求数量的内存或者当你需要多于 128 KB(例如, 一个通常的 PCI 帧抓取的请求), 一个替代返回 -ENOMEM 的做法是在启动时分配内存或者保留物理 RAM 的顶部给你的缓冲. 我们在第 8 章的 "获得大量缓冲" 一节描述在启动时间分配, 但是它对模块是不可用的. 保留 RAM 的顶部是通过在启动时传递一个 mem= 参数给内核实现的. 例如, 如果你有 256 MB, 参数 mem=255M 使内核不使用顶部的 MByte. 你的模块可能后来使用下列代码来获得对这个内存的存取:</p><pre class="programlisting">dmabuf = ioremap (0xFF00000 /* 255M */, 0x100000 /* 1M */); </pre><p>分配器, 配合本书的例子代码的一部分, 提供了一个简单的 API 来探测和管理这样的保留 RAM 并且已在几个体系上被成功使用. 但是, 这个技巧当你有一个高内存系统时无效(即, 一个有比适合 CPU 地址空间更多的物理内存的系统 ).</p><p>当然, 另一个选项, 是使用 GFP_NOFAIL 来分配你的缓冲. 这个方法, 但是, 确实严重地对内存管理子系统有压力, 并且它冒锁住系统的风险; 最好是避免除非确实没有其他方法.</p><p>如果你分配一个大 DMA 缓冲到这样的长度, 但是, 值得想一下替代的方法. 如果你的设备可以做发散/汇聚 I/O, 你可以分配你的缓冲以更小的片段并且让设备做其他的. 发散/汇聚 I/O 也可以用当进行直接 I/O 到用户空间时, 它可能是最好地解决方法当需要一个真正大缓冲时.</p></div></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="BusAddresses.sect2"></a>15.4.3. 总线地址</h3></div></div></div><p>一个使用 DMA 的设备驱动必须和连接到接口总线的硬件通讯, 总线使用物理地址, 而程序代码使用虚拟地址.</p><p>事实上, 情况比这个稍微有些复杂. 基于DMA 的硬件使用总线地址, 而不是物理地址. 尽管 ISA 和 PCI 总线地址在 PC 上完全是物理地址, 这对每个平台却不总是真的. 有时接口总线被通过桥接电路连接, 它映射 I/O 地址到不同的物理地址. 一些系统甚至有一个页映射机制, 使任意的页连续出现在外设总线.</p><p>在最低级别(再次, 我们将马上查看一个高级解决方法), Linux 内核提供一个可移植的方法, 通过输出下列函数, 在 <asm/io.h> 定义. 这些函数的使用不被推荐, 因为它们只在有非常简单的 I/O 体系的系统上正常工作; 但是, 你可能遇到它们当使用内核代码时.</p><pre class="programlisting">unsigned long virt_to_bus(volatile void *address);void *bus_to_virt(unsigned long address);</pre><p>这些函数进行一个简单的转换在内核逻辑地址和总线地址之间. 它们在许多情况下不工作, 一个 I/O 内存管理单元必须被编程的地方或者必须使用反弹缓冲的地方. 做这个转换的正确方法是使用通用的 DMA 层, 因此我们现在转移到这个主题.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="TheGenericDMALayer.sect2"></a>15.4.4. 通用 DMA 层</h3></div></div></div><p>DMA 操作, 最后, 下到分配一个缓冲并且传递总线地址到你的设备. 但是, 编写在所有体系上安全并正确进行 DMA 的可移植启动的任务比想象的要难. 不同的系统有不同的概念, 关于缓存一致性应当如何工作的概念; 如果你不正确处理这个问题, 你的驱动可能破坏内存. 一些系统有复杂的总线硬件, 它使 DMA 任务更容易 - 或者更难. 并且不是所有的系统可以在内存所有部分进行 DMA. 幸运的是, 内核提供了一个总线和体系独立的 DMA 层来对驱动作者隐藏大部分这些问题. 我们非常鼓励你来使用这个层来 DMA 操作, 在任何你编写的驱动中.</p><p>下面的许多函数需要一个指向 struct device 的指针. 这个结构是 Linux 设备模型中设备的低级表示. 它不是驱动常常必须直接使用的东西, 但是你确实需要它当使用通用 DMA 层时. 常常地, 你可发现这个结构, 深埋在描述你的设备的总线. 例如, 它可在 struct pci_device 或者 struct usb_device 中发现它作为 dev 成员. 设备结构在 14 章中详细描述.</p><p>使用下面函数的驱动应当包含 <linux/dma-mapping.h>.</p><div class="sect3" lang="zh-cn"><div class="titlepage"><div><div><h4 class="title"><a name="Dealingwithdifficulthardware.sect3"></a>15.4.4.1. 处理困难硬件</h4></div></div></div><p>在尝试 DMA 之前必须回答的第一个问题是给定设备是否能够在当前主机上做这样的操作. 许多设备受限于它们能够寻址的内存范围, 因为许多理由. 缺省地, 内核假定你的设备能够对任何 32-位 地址进行 DMA. 如果不是这样, 你应当通知内核这个事实, 使用一个调用:</p><pre class="programlisting"> int dma_set_mask(struct device *dev, u64 mask); </pre><p>mask 应当显示你的设备能够寻址的位; 如果它被限制到 24 位, 例如, 你要传递 mask 作为 0x0FFFFFF. 返回值是非零如果使用给定的 mask 可以 DMA; 如果 dma_set_mask 返回 0, 你不能对这个设备使用 DMA 操作. 因此, 设备的驱动中的初始化代码限制到 24-位 DMA 操作可能看来如:</p><pre class="programlisting">if (dma_set_mask (dev, 0xffffff)) card->use_dma = 1;else{ card->use_dma = 0; /* We'll have to live without DMA */ printk (KERN_WARN, "mydev: DMA not supported\n");}</pre><p>再次, 如果你的设备支持正常的, 32-位 DMA 操作, 没有必要调用 dma_set_mask.</p></div><div class="sect3" lang="zh-cn"><div class="titlepage"><div><div><h4 class="title"><a name="DMAmappings.sect3"></a>15.4.4.2. DMA 映射</h4></div></div></div><p>一个 DMA 映射是分配一个 DMA 缓冲和产生一个设备可以存取的地址的结合. 它试图使用一个简单的对 virt_to_bus 的调用来获得这个地址, 但是有充分的理由来避免那个方法. 它们中的第一个是合理的硬件带有一个 IOMMU 来为总线提供一套映射寄存器. IOMMU 可为任何物理内存安排来出现在设备可存取的地址范围内, 并且它可使物理上散布的缓冲对设备看来是连续的. 使用 IOMMU 需要使用通用的 DMA 层; virt_to_bus 不负责这个任务.</p><p>注意不是所有的体系都有一个 IOMMU; 特别的, 流行的 x86 平台没有 IOMMU 支持. 一个正确编写的驱动不需要知道它在之上运行的 I/O 支持硬件, 但是.</p><p>为设备设置一个有用的地址可能也, 在某些情况下, 要求一个反弹缓冲的建立. 反弹缓冲是当一个驱动试图在一个外设不能达到的地址上进行 DMA 时创建的, 比如一个高内存地址. 数据接着根据需要被拷贝到和从反弹缓冲. 无需说, 反弹缓冲的使用能拖慢事情, 但是有时没有其他选择.</p><p>DMA 映射也必须解决缓存一致性问题. 记住现代处理器保持最近存取的内存区的拷贝在一个快速的本地缓冲中; 如果没有这个缓存, 合理的性能是不可能的. 如果你的设备改变主存一个区, 会强制使任何包含那个区的处理器缓存被失效; 负责处理器可能使用不正确的主存映象, 并且导致数据破坏. 类似地, 当你的设备使用 DMA 来从主存中读取数据, 任何对那个驻留在处理器缓存的内存的改变必须首先被刷新. 这些缓存一致性问题可以产生无头的模糊和难寻的错误, 如果编程者不小心. 一个体系在硬件中管理缓存一致性, 但是其他的要求软件支持. 通用的 DMA 层深入很多来保证在所有体系上事情都正确工作, 但是, 如同我们将见到的, 正确的行为要求符合一些规则.</p><p>DMA 映射设置一个新类型, dma_addr_t, 来代表总线地址. 类型 dma_addr_t 的变量应当被驱动当作不透明的; 唯一可允许的操作是传递它们到 DMA 支持过程和设备自身. 作为一个总线地址, dma_addr_t 可导致不期望的问题如果被 CPU 直接使用.</p><p>PCI 代码在 2 类 DMA 映射中明显不同, 依赖 DMA 缓冲被期望停留多长时间:</p>Coherent DMA mappings <p>连贯的 DMA 映射. 这些映射常常在驱动的生命期内存在. 一个连贯的缓冲必须是同时对 CPU 和外设可用(其他的映射类型, 如同我们之后将看到的, 在任何给定时间只对一个或另一个可用). 结果, 一致的映射必须在缓冲一致的内存. 一致的映射建立和使用可能是昂贵的.</p>Streaming DMA mappings <p>流 DMA 映射. 流映射常常为一个单个操作建立. 一些体系当使用流映射时允许大的优化, 如我们所见, 但是这些映射也服从一个更严格的关于如何存取它们的规则. 内核开发者建议使用一致映射而不是流映射在任何可能的时候. 这个建议有 2 个原因. 第一个, 在支持映射寄存器的系统上, 每个 DMA 映射在总线上使用它们一个或多个. 一致映射, 有长的生命周期, 可以长时间独占这些寄存器, 甚至当它们不在使用时. 另外一个原因是, 在某些硬件上, 流映射可以用无法在一致映射中使用的方法来优化.</p><p>这 2 种映射类型必须以不同的方式操作; 是时候看看细节了.</p></div>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -