BLOG

Record, summarize, and improve.

MMIO/PMIO

MMIO & PMIO

Memory-mapped I/O 内存映射I/O(MMIO)和 port-mapped I/O 端口映射I/O(PMIO)是在计算机中的中央处理器(CPU)和外围设备之间执行输入/输出(I/O)的两种互补方法(通常通过芯片组进行中介访问)。另一种方法是使用专用I/O处理器,通常称为大型计算机上的通道,它们执行自己的指令。

内存映射 I/O 使用相同的地址空间来寻址主内存和 I/O 设备。I/O 设备的内存和寄存器被映射到(与)地址值相关联,因此内存地址既可以指物理 RAM 的一部分,也可以指 I/O 设备的内存和寄存器。因此,CPU 用于访问内存的指令(如 MOV ...)也可用于访问设备。每个 I/O 设备都会监控 CPU 的地址总线,并响应 CPU 对分配给该设备的地址的任何访问,将系统总线连接到所需设备的硬件寄存器,或者使用专用的地址总线。

为了容纳 I/O 设备,CPU 使用的地址总线的某些区域必须为 I/O 预留,而不能用于正常的物理内存;用于 I/O 设备的地址范围由硬件决定。预留可以是永久性的,也可以是临时性的(通过库切换实现)。后者的一个例子是 Commodore 64,它使用一种内存映射形式,使 RAM 或 I/O 硬件出现在 0xD000-0xDFFF 范围内。

端口映射 I/O 通常使用一类专门为执行 I/O 而设计的特殊 CPU 指令,例如基于 x86 架构的微处理器上的 in 和 out 指令。这两条指令的不同形式可以在 CPU 的 EAX 寄存器或该寄存器的一个子寄存器与指定给 I/O 设备的 I/O 端口地址之间复制一个、两个或四个字节(分别为 outb、outw 和 outl)。I/O 设备拥有独立于普通内存的地址空间,这可以通过 CPU 物理接口上额外的 "I/O "引脚或专门用于 I/O 的整条总线来实现。由于 I/O 的地址空间与主内存地址空间隔离,因此有时也被称为隔离 I/O。在 x86 架构上,端口映射 I/O 通常使用索引/数据对。

概述

不同的CPU到设备通信方法(如内存映射)不会影响设备的直接内存访问(DMA),因为根据定义,DMA是绕过CPU的内存到设备通信方法。

硬件中断是CPU和外围设备之间的另一种通信方法,然而,由于许多原因,中断总是被分开处理。中断是由设备发起的,而不是上面提到的由CPU发起的方法。它也是单向的,因为信息只从设备流向CPU。最后,每条中断线仅携带一位具有固定含义的信息,即“在该中断线上的设备中发生了需要注意的事件”。

如果共享地址和数据总线,I/O 操作会减慢内存访问速度。这是因为外围设备通常比主内存慢得多。在某些架构中,端口映射 I/O 通过专用 I/O 总线运行,从而缓解了这一问题。

内存映射 I/O 的一个优点是,由于摒弃了端口 I/O 带来的额外复杂性,CPU 所需的内部逻辑更少,因此成本更低、速度更快、更易于构建、功耗更低、物理尺寸更小;这符合精简指令集计算的基本原则,在嵌入式系统中也很有利。另一个优点是,由于使用常规内存指令对设备寻址,CPU 的所有寻址模式都可用于 I/O 和内存,而且直接对内存操作数执行 ALU 运算(从内存位置加载操作数、将结果存储到内存位置或两者兼而有之)的指令也可用于 I/O 设备寄存器。相比之下,端口映射 I/O 指令通常非常有限,通常只能在 CPU 寄存器和 I/O 端口之间进行简单的加载和存储操作,例如,要在端口映射设备寄存器中添加一个常量,就需要三条指令:读取端口到 CPU 寄存器,将常量添加到 CPU 寄存器,并将结果写回端口。

由于 16 位处理器已经过时,取而代之的是普遍使用的 32 位和 64 位处理器,为 I/O 预留内存地址空间范围的问题已经不大,因为处理器的内存地址空间通常比系统中所有内存和 I/O 设备所需的空间大得多。因此,利用内存映射 I/O 的优势变得更加频繁和实用。不过,即使地址空间不再是一个主要问题,两种 I/O 映射方法也并非都优于另一种,在某些情况下,使用端口映射 I/O 仍然更可取。

X86

在基于 x86 的体系结构中,内存映射 I/O 是首选,因为执行基于端口 I/O 的指令仅限于一个寄存器: EAX、AX 和 AL 是唯一可以将数据移入或移出的寄存器,指令中的字节大小立即值或寄存器 DX 中的值决定哪个端口是传输的源端口或目的端口。由于任何通用寄存器都可以向内存和内存映射 I/O 设备发送或接收数据,因此内存映射 I/O 比端口 I/O 使用的指令更少,运行速度更快。AMD 在定义 x86-64 架构时没有扩展端口 I/O 指令以支持 64 位端口,因此无法使用端口 I/O 执行 64 位传输。

在从 2008 5 系列开始的较新英特尔平台上,芯片组上的 I/O 设备通过专用的直接媒体接口Direct Media Interface  (DMI) 总线直接通信。

Memory barriers

由于高速缓存是内存地址访问的中介,因此写入不同地址的数据可能会不按程序顺序到达外设的内存或寄存器,也就是说,如果软件先向一个地址写入数据,然后再向另一个地址写入数据,高速缓存写入缓冲区并不能保证数据会按顺序到达外设。如果高速缓存系统优化了写入顺序,那么任何在序列中每次写入后不包含高速缓存刷新指令的程序都可能会产生意想不到的 IO 效应。对内存的写入通常可以重新排序,以减少冗余或更好地利用内存访问周期,而不会改变存储内容的最终状态;但同样的优化可能会完全改变对内存映射 I/O 区域写入的意义和效果。

由于对内存映射 I/O 区域的选择缺乏前瞻性,导致老一代计算机在 RAM 容量方面存在许多障碍。设计人员很少期望计算机的发展能充分利用架构的理论 RAM 容量,因此经常将地址空间的一些高阶位用作内存映射 I/O 功能的选择器。例如,IBM PC 及其衍生产品中的 640 KB 障碍就是由于将 640 KB 和 1024 KB 之间的区域(64K 第 10 段至第 16 段)保留给了上层内存区。这一选择最初影响不大,但最终限制了 20 位可用地址空间内的可用 RAM 总量。3 GB 障碍和 PCI 漏洞是 32 位地址空间的类似表现,x86 启动过程和 MMU 设计的细节加剧了这种情况。64 位架构在技术上通常也存在类似问题,但这些问题很少会产生实际后果。

Examples

Address range (hexadecimal)地址范围(十六进制) Size Device
0000–7FFF 32 KiB RAM
8000–80FF 256 bytes General-purpose I/O 通用I/O
9000–90FF 256 bytes Sound controller 声音控制器
A000–A7FF 2 KiB Video controller/text-mapped display RAM视频控制器/文本映射显示RAM
C000–FFFF 16 KiB ROM

一个围绕 8 位微处理器构建的简单系统可能提供 16 位地址线,允许它寻址多达 64 千字节(KiB)的内存。在这样的系统中,前 32 KiB 的地址空间可分配给随机存取存储器 (RAM),另外 16 KiB 分配给只读存储器 (ROM),其余的分配给各种其他设备,如定时器、计数器、视频显示芯片、发声设备等。

系统硬件的安排是,地址总线上的设备只对特定的地址做出响应,而其他地址则被忽略。这是地址解码电路的工作,也是系统内存映射的建立。因此,系统的内存映射可能如右表所示。该内存映射包含间隙,这在实际系统架构中也很常见。

假设视频控制器的第四寄存器设置了屏幕的背景颜色,那么 CPU 就可以通过使用标准内存写入指令将数值写入内存位置 A003 来设置这种颜色。使用同样的方法,将字符值写入视频控制器内 RAM 的特殊区域,就能在屏幕上显示图形。在廉价 RAM 实现位映射显示之前,这种字符单元方法是计算机视频显示的常用技术(参见文本用户界面)。

Basic types of address decoding

地址解码类型(其中设备可以完全或不完全解码地址)包括以下类型:

Complete (exhaustive) decoding 完全(穷举)解码

唯一地址到一个硬件寄存器(物理内存位置)的1:1映射。包括检查地址总线的每一行。

Incomplete (partial) decoding 不完全(部分)解码

n:1 映射 n 个唯一地址到一个硬件寄存器。部分解码允许一个内存位置拥有多个地址,从而允许程序员使用 n 个不同的地址来引用一个内存位置。当不需要 CPU 的全部地址空间时,也可以使用更简单、更便宜的逻辑,只检查部分地址线,从而简化解码硬件。通常情况下,解码本身是可编程的,因此系统可以根据需要重新配置自己的内存映射,但这是一种较新的发展,通常与降低成本的初衷相冲突。

同义词:折返、多重映射、部分映射、地址别名。

Linear decoding 线性解码

地址线可直接使用,无需任何解码逻辑。这种方式适用于 RAM 和 ROM 等具有地址输入序列的设备,以及具有类似输入序列的外设芯片,用于寻址寄存器组。线性寻址很少单独使用(只在总线上设备较少时才使用,因为对一个以上设备使用纯线性寻址通常会浪费大量地址空间),而是与其他方法之一结合使用,以选择一个或一组设备,线性寻址在其中选择单个寄存器或内存位置。

通过设备驱动程序的端口I/O

在基于 Windows 的计算机中,也可以通过特定的驱动程序访问内存,如 DOLLx8KD,它可以在从 Windows 95 到 Windows 7 的大多数 Windows 平台上提供 8 位、16 位和 32 位 I/O 访问。安装 I/O 端口驱动程序可通过简单的 DLL 调用激活驱动程序,允许端口 I/O 访问内存。

Linux 提供了 pcimem 工具,允许读取和写入 MMIO 地址。Linux 内核还允许使用内核的 mmiotrace 调试工具跟踪内核模块(驱动程序)对 MMIO 的访问。mmiotrace 用于调试闭源设备驱动程序。

CPU 和 DMA 地址

内核通常使用虚拟地址。 kmalloc() 、 vmalloc() 和类似接口返回的任何地址都是虚拟地址,可以存储在 void * 中。

虚拟内存系统(TLB、页表等)将虚拟地址转换为CPU物理地址,存储为“phys_addr_t”或“resource_size_t”。内核将设备资源(如寄存器)作为物理地址进行管理。这些是 /proc/iomem 中的地址。物理地址对于驱动程序来说并不直接有用;它必须使用 ioremap() 来映射空间并生成虚拟地址。

I/O 设备使用第三种地址:“总线地址”。如果设备在 MMIO 地址处有寄存器,或者执行 DMA 来读取或写入系统内存,则设备使用的地址是总线地址。在某些系统中,总线地址与 CPU 物理地址相同,但通常情况不同。 IOMMU 和主桥可以在物理地址和总线地址之间生成任意映射。

从设备的角度来看,DMA 使用总线地址空间,但它可能仅限于该空间的子集。例如,即使系统支持主内存和 PCI BAR 的 64 位地址,它也可能使用 IOMMU,因此设备只需要使用 32 位 DMA 地址。

这是一张图片和一些示例:

             CPU                  CPU                  Bus
           Virtual              Physical             Address
           Address              Address               Space
            Space                Space

          +-------+             +------+             +------+
          |       |             |MMIO  |   Offset    |      |
          |       |  Virtual    |Space |   applied   |      |
        C +-------+ --------> B +------+ ----------> +------+ A
          |       |  mapping    |      |   by host   |      |
+-----+   |       |             |      |   bridge    |      |   +--------+
|     |   |       |             +------+             |      |   |        |
| CPU |   |       |             | RAM  |             |      |   | Device |
|     |   |       |             |      |             |      |   |        |
+-----+   +-------+             +------+             +------+   +--------+
          |       |  Virtual    |Buffer|   Mapping   |      |
        X +-------+ --------> Y +------+ <---------- +------+ Z
          |       |  mapping    | RAM  |   by IOMMU
          |       |             |      |
          |       |             |      |
          +-------+             +------+

在枚举过程中,内核了解 I/O 设备及其 MMIO 空间以及将它们连接到系统的主机桥。例如,如果 PCI 设备有 BAR,则内核从 BAR 中读取总线地址(A)并将其转换为 CPU 物理地址(B)。地址 B 存储在结构资源中,通常通过 /proc/iomem 公开。当驱动程序声明一个设备时,它通常使用 ioremap() 将物理地址 B 映射到虚拟地址 (C)。然后,它可以使用 ioread32(C) 等来访问总线地址 A 处的设备寄存器。

如果设备支持 DMA,则驱动程序会使用 kmalloc() 或类似接口设置缓冲区,该接口返回虚拟地址 (X)。虚拟内存系统将 X 映射到系统 RAM 中的物理地址 (Y)。驱动程序可以使用虚拟地址 X 来访问缓冲区,但设备本身不能,因为 DMA 不经过 CPU 虚拟内存系统。

在一些简单的系统中,设备可以直接对物理地址 Y 进行 DMA。但在许多其他系统中,有 IOMMU 硬件将 DMA 地址转换为物理地址,例如,它将 Z 转换为 Y。这是 DMA 的部分原因API:驱动程序可以向 dma_map_single() 等接口提供虚拟地址 X,该接口设置任何所需的 IOMMU 映射并返回 DMA 地址 Z。然后驱动程序告诉设备对 Z 执行 DMA,IOMMU 将其映射到系统 RAM 中地址 Y 处的缓冲区。

为了使 Linux 能够使用动态 DMA 映射,它需要驱动程序的一些帮助,即它必须考虑到 DMA 地址应该仅在实际使用时进行映射,并在 DMA 传输后取消映射。

当然,即使在不存在此类硬件的平台上,以下 API 也可以工作。

请注意,DMA API 可与独立于底层微处理器架构的任何总线配合使用。您应该使用 DMA API 而不是特定于总线的 DMA API,即使用 dma_map_() 接口而不是 pci_map_() 接口。

首先,您应该确保:

#include <linux/dma-mapping.h>

在您的驱动程序中,它提供了 dma_addr_t 的定义。此类型可以保存平台的任何有效 DMA 地址,并且应该在保存从 DMA 映射函数返回的 DMA 地址的任何地方使用。

哪些内存支持 DMA

必须了解的第一条信息是 DMA 映射工具可以使用哪些内核内存。

如果你通过页分配器(即使用__get_free_page*())或通用内存分配器(即使用kmalloc()kmem_cache_alloc())获得了内存,那么你可以使用这些例程返回的地址进行DMA(直接内存访问)。

这具体意味着您可能_不_使用从 vmalloc() 返回的内存/地址进行 DMA。可以对映射到 vmalloc() 区域的_underlying_内存进行 DMA,但这需要遍历页表来获取物理地址,然后使用类似 __va( )。

此规则还意味着您既不能使用内核映像地址(数据/文本/bss 段中的项目),也不能使用模块映像地址,也不能使用 DMA 的堆栈地址。这些都可以映射到与物理内存的其余部分完全不同的地方。即使这些类别的内存在物理上可以与 DMA 配合使用,您也需要确保 I/O 缓冲区是缓存行对齐的。否则,您会在具有 DMA 不相干缓存的 CPU 上看到缓存行共享问题(数据损坏)。 (CPU 可以写入一个字,DMA 可以写入同一缓存行中的另一个字,并且其中一个字可以被覆盖。)

此外,这还意味着不能使用 kmap() 调用的返回值,也不能将 DMA 从 kmap() 调用返回到 kmap()。这与 vmalloc() 类似。

块 I/O 和网络缓冲区怎么样?块 I/O 和网络子系统确保它们使用的缓冲区对于您的 DMA 传输有效。

DMA 寻址功能

默认情况下,内核假定您的设备可以寻址 32 位 DMA 寻址。对于支持 64 位的设备,需要增加该值,对于有限制的设备,需要减少该值。

关于 PCI 的特别说明:PCI-X 规范要求 PCI-X 设备支持所有事务的 64 位寻址 (DAC)。并且至少有一个平台(SGI SN2)需要 64 位一致分配才能在 IO 总线处于 PCI-X 模式时正确运行。

为了正确操作,您必须设置 DMA 掩码以告知内核您的设备 DMA 寻址功能。

这是通过调用 dma_set_mask_and_coherent() 执行的:

int dma_set_mask_and_coherent(struct device *dev, u64 mask);

这将为流式 API 和一致性 API 一起设置掩码。如果您有一些特殊要求,则可以使用以下两个单独的调用来代替:

流映射的设置是通过调用 dma_set_mask() 执行的:

int dma_set_mask(struct device *dev, u64 mask);

一致分配的设置是通过调用 dma_set_coherent_mask() 执行的:

int dma_set_coherent_mask(struct device *dev, u64 mask);

这里,dev 是指向设备的设备结构的指针,mask 是描述设备支持的地址的哪些位的位掩码。通常,设备的设备结构嵌入在设备的总线特定设备结构中。例如,&pdev->dev 是指向 PCI 设备的设备结构的指针(pdev 是指向您设备的 PCI 设备结构的指针)。

这些调用通常返回零,表示您的设备可以在给定您提供的地址掩码的机器上正确执行 DMA,但如果掩码太小而无法在给定系统上支持,则它们可能会返回错误。如果它返回非零,则您的设备无法在此平台上正确执行 DMA,并且尝试这样做将导致未定义的行为。除非 dma_set_mask 函数系列返回成功,否则不得在此设备上使用 DMA。

这意味着在失败的情况下,您有两种选择:

  1. 如果可能,请使用某种非 DMA 模式进行数据传输。
  2. 忽略该设备并且不对其进行初始化。

DMA 映射的类型

DMA 映射有两种类型:

  • 一致的 DMA 映射通常在驱动程序初始化时映射,在结束时取消映射,为此硬件应保证设备和 CPU 可以并行访问数据,并且会看到彼此进行的更新,而无需任何显式软件刷新。

    将“一致”视为“同步”或“一致”。

    当前默认是在 DMA 空间的低 32 位中返回一致的内存。但是,为了将来的兼容性,即使此默认值适合您的驱动程序,您也应该设置一致的掩码。

    使用一致映射的好例子是:

    • 网卡 DMA 环描述符。
    • SCSI 适配器邮箱命令数据结构。
    • 在主存储器外执行的设备固件微代码。

    这些示例都要求不变的是,任何 CPU 存储到内存的操作都对设备立即可见,反之亦然。一致的映射保证了这一点。

    重要:

    一致的 DMA 内存并不排除使用适当的内存屏障。 CPU 可以将存储重新排序到一致内存,就像它可以正常内存一样。示例:如果设备在第二个字之前看到描述符的第一个字更新很重要,则必须执行以下操作:

    desc->word0 = address;
    wmb();
    desc->word1 = DESC_VALID;

    以确保在所有平台上获得正确的行为。

    此外,在某些平台上,您的驱动程序可能需要以与刷新 PCI 桥中的写入缓冲区大致相同的方式刷新 CPU 写入缓冲区(例如,在写入寄存器的值后读取该值)。

  • 流 DMA 映射,通常针对一次 DMA 传输进行映射,在其之后立即取消映射(除非您使用下面的 dma_sync_*),并且硬件可以针对顺序访问进行优化。

    将“流”视为“异步”或“在一致性域之外”。

    使用流映射的好例子是:

    • 设备发送/接收的网络缓冲区。
    • 文件系统缓冲区由 SCSI 设备写入/读取。

    使用此类映射的接口的设计方式使得实现可以进行硬件允许的任何性能优化。为此,在使用此类映射时,您必须明确您想要发生的情况。

两种类型的 DMA 映射都没有来自底层总线的对齐限制,尽管某些设备可能有此类限制。此外,当底层缓冲区不与其他数据共享缓存行时,具有非 DMA 一致性缓存的系统将工作得更好。

使用一致性 DMA 映射

要分配和映射大型(PAGE_SIZE 左右)一致的 DMA 区域,您应该执行以下操作:

dma_addr_t dma_handle;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

其中设备是 struct device * 。这可以在中断上下文中使用 GFP_ATOMIC 标志来调用。

Size 是要分配的区域的长度(以字节为单位)。

该例程将为该区域分配 RAM,因此它的作用与 __get_free_pages() 类似(但采用大小而不是页面顺序)。如果您的驱动程序需要大小小于页面的区域,您可能更喜欢使用 dma_pool 接口,如下所述。

一致的 DMA 映射接口默认返回 32 位可寻址的 DMA 地址。即使设备指示(通过 DMA 掩码)它可以寻址高 32 位,如果已通过 dma_set_coherent_mask() 显式更改了一致的 DMA 掩码,一致分配也只会返回 > 32 位的 DMA 地址。 dma_pool 接口也是如此。

dma_alloc_coherent() 返回两个值:可用于从 CPU 访问它的虚拟地址和传递给卡的 dma_handle。

CPU 虚拟地址和 DMA 地址都保证与大于或等于请求大小的最小 PAGE_SIZE 顺序对齐。这个不变量的存在(例如)是为了保证如果您分配小于或等于 64 KB 的块,则您接收的缓冲区范围不会跨越 64K 边界。

要取消映射并释放此类 DMA 区域,您可以调用:

dma_free_coherent(dev, size, cpu_addr, dma_handle);

其中 dev、size 与上面的调用相同,cpu_addr 和 dma_handle 是 dma_alloc_coherent() 返回给您的值。该函数不能在中断上下文中调用。

如果您的驱动程序需要大量较小的内存区域,您可以编写自定义代码来细分 dma_alloc_coherent() 返回的页面,或者您可以使用 dma_pool API 来执行此操作。 dma_pool 类似于 kmem_cache,但它使用 dma_alloc_coherent(),而不是 __get_free_pages()。此外,它还了解对齐的常见硬件约束,例如队列头需要在 N 字节边界上对齐。

struct dma_pool *pool;

pool = dma_pool_create(name, dev, size, align, boundary);

“名称”用于诊断(如 kmem_cache 名称); dev 和 size 如上。设备对此类数据的硬件对齐要求是“align”(以字节表示,并且必须是2的幂)。如果您的设备没有边界穿越限制,则为边界传递 0;传递 4096 表示从该池分配的内存不得跨越 4KByte 边界(但此时直接使用 dma_alloc_coherent() 可能更好)。

从 DMA 池中分配内存,如下所示:

cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);

如果允许阻塞(不允许 in_interrupt 或持有 SMP 锁),则标志为 GFP_KERNEL,否则为 GFP_ATOMIC。与 dma_alloc_coherent() 类似,它返回两个值:cpu_addr 和 dma_handle。

从 dma_pool 分配的空闲内存如下:

dma_pool_free(pool, cpu_addr, dma_handle);

其中 pool 是您传递给 dma_pool_alloc() 的内容,cpu_addr 和 dma_handle 是返回的值 dma_pool_alloc() 。该函数可以在中断上下文中调用。

通过调用以下命令销毁 dma_pool:

dma_pool_destroy(pool);

确保在销毁池之前已调用 dma_pool_free() 来获取从池中分配的所有内存。该函数不能在中断上下文中调用。

DMA 方向

本文档后续部分中描述的接口采用 DMA 方向参数,该参数是一个整数并采用以下值之一:

DMA_BIDIRECTIONAL
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_NONE

如果您知道的话,您应该提供准确的 DMA 方向。

从主存到设备” DMA_FROM_DEVICE 表示“从设备到主存” 是 DMA 传输过程中数据移动的方向。

我们_强烈_鼓励您尽可能准确地指定这一点。

如果您绝对无法知道 DMA 传输的方向,请指定 DMA_BIDIRECTIONAL。这意味着 DMA 可以朝任一方向进行。该平台保证您可以合法地指定这一点,并且它会起作用,但这可能会以性能为代价。

值 DMA_NONE 用于调试。在您知道精确方向之前,可以将其保存在数据结构中,这将有助于捕获方向跟踪逻辑未能正确设置的情况。

精确指定该值的另一个优点(除了潜在的特定于平台的优化之外)是为了调试。有些平台实际上有一个写权限布尔值,可以用它来标记 DMA 映射,就像用户程序地址空间中的页面保护一样。当 DMA 控制器硬件检测到违反权限设置时,此类平台可以并且确实会在内核日志中报告错误。

仅流映射指定方向,一致映射隐式具有 DMA_BIDIRECTIONAL 方向属性设置。

SCSI 子系统告诉您驱动程序正在处理的 SCSI 命令的“sc_data_direction”成员中使用的方向。

对于网络驱动程序来说,这是一件相当简单的事情。对于传输数据包,使用 DMA_TO_DEVICE 方向说明符映射/取消映射它们。对于接收数据包,恰恰相反,使用 DMA_FROM_DEVICE 方向说明符映射/取消映射它们。

使用流 DMA 映射

流 DMA 映射例程可以从中断上下文中调用。每个映射/取消映射都有两个版本,一种将映射/取消映射单个内存区域,另一种将映射/取消映射分散列表。

要绘制单个区域的地图,您需要:

struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
void *addr = buffer->ptr;
size_t size = buffer->len;

dma_handle = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
        /*
         * reduce current DMA mapping usage,
         * delay and try again later or
         * reset driver.
         */
        goto map_error_handling;
}

并取消映射:

dma_unmap_single(dev, dma_handle, size, direction);

您应该调用 dma_mapping_error(),因为 dma_map_single() 可能会失败并返回错误。这样做将确保映射代码在所有 DMA 实现上正确工作,而不依赖于底层实现的细节。在不检查错误的情况下使用返回的地址可能会导致各种失败,从恐慌到静默数据损坏。这同样适用于 dma_map_page()。

当 DMA 活动完成时,您应该调用 dma_unmap_single(),例如,从通知您 DMA 传输已完成的中断中调用 dma_unmap_single()。

像这样对单个映射使用 CPU 指针有一个缺点:您无法以这种方式引用 HIGHMEM 内存。因此,存在类似于 dma_{map,unmap}_single() 的映射/取消映射接口对。这些接口处理页/偏移量对而不是 CPU 指针。具体来说:

struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
struct page *page = buffer->page;
unsigned long offset = buffer->offset;
size_t size = buffer->len;

dma_handle = dma_map_page(dev, page, offset, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
        /*
         * reduce current DMA mapping usage,
         * delay and try again later or
         * reset driver.
         */
        goto map_error_handling;
}

...

dma_unmap_page(dev, dma_handle, size, direction);

这里,“偏移量”是指给定页面内的字节偏移量。

您应该调用 dma_mapping_error(),因为 dma_map_page() 可能会失败并返回错误,如 dma_map_single() 讨论中所述。

当 DMA 活动完成时,您应该调用 dma_unmap_page(),例如,从通知您 DMA 传输已完成的中断中调用 dma_unmap_page()。

使用分散列表,您可以通过以下方式映射从多个区域收集的区域:

int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist *sg;

for_each_sg(sglist, sg, count, i) {
        hw_address[i] = sg_dma_address(sg);
        hw_len[i] = sg_dma_len(sg);
}

其中 nents 是 sglist 中的条目数。

该实现可以自由地将多个连续的 sglist 条目合并为一个(例如,如果 DMA 映射以 PAGE_SIZE 粒度完成,则任何连续的 sglist 条目都可以合并为一个,前提是第一个在页面边界上结束,第二个在页面边界上开始 - 事实上,这是对于不能进行分散收集或分散收集条目数量非常有限的卡来说,这是一个巨大的优势)并返回其映射到的 sg 条目的实际数量。失败时返回 0。

然后,您应该循环 count 次(注意:这可能小于 nents 次),并在您之前访问 sg->address 和 sg->length 的位置使用 sg_dma_address() 和 sg_dma_len() 宏,如上所示。

要取消映射分散列表,只需调用:

dma_unmap_sg(dev, sglist, nents, direction);

再次确保 DMA 活动已经完成。

Note

dma_unmap_sg 调用的“nents”参数必须与您传递到 dma_map_sg 调用的参数相同,它不应该是从 dma_map_sg 调用返回的“count”值。

每个 dma_map_{single,sg}() 调用都应该有其对应的 dma_unmap_{single,sg}() 调用,因为 DMA 地址空间是共享资源,您可能会通过消耗所有 DMA 地址而导致机器不可用。

如果您需要多次使用相同的流 DMA 区域并在 DMA 传输之间接触数据,则需要正确同步缓冲区,以便 CPU 和设备能够看到最新且正确的数据副本。 DMA 缓冲区。

因此,首先,只需使用 dma_map_{single,sg}() 进行映射,然后在每次 DMA 传输调用之后:

dma_sync_single_for_cpu(dev, dma_handle, size, direction);

or:

dma_sync_sg_for_cpu(dev, sglist, nents, direction);

作为适当的。

然后,如果您希望让设备再次访问 DMA 区域,请完成 CPU 对数据的访问,然后在实际将缓冲区提供给硬件调用之前:

dma_sync_single_for_device(dev, dma_handle, size, direction);

or:

dma_sync_sg_for_device(dev, sglist, nents, direction);

作为适当的。

Note

dma_sync_sg_for_cpu() 和 dma_sync_sg_for_device() 的“nents”参数必须与传递给 dma_map_sg() 的参数相同。它不是 dma_map_sg() 返回的计数。

最后一次 DMA 传输后,调用 DMA 取消映射例程之一 dma_unmap_{single,sg}()。如果您从第一个 dma_map_*() 调用到 dma_unmap_*() 都没有接触过数据,那么您根本不必调用 dma_sync_*() 例程。

下面的伪代码显示了您需要使用 dma_sync_*() 接口的情况:

`my_card_setup_receive_buffer(struct my_card *cp, char *buffer, int len)
{
        dma_addr_t mapping;

        mapping = dma_map_single(cp->dev, buffer, len, DMA_FROM_DEVICE);
        if (dma_mapping_error(cp->dev, mapping)) {
                /*
                 * reduce current DMA mapping usage,
                 * delay and try again later or
                 * reset driver.
                 */
                goto map_error_handling;
        }

        cp->rx_buf = buffer;
        cp->rx_len = len;
        cp->rx_dma = mapping;

        give_rx_buf_to_card(cp);
}

...

my_card_interrupt_handler(int irq, void *devid, struct pt_regs *regs)
{
        struct my_card *cp = devid;

        ...
        if (read_card_status(cp) == RX_BUF_TRANSFERRED) {
                struct my_card_header *hp;

                /* Examine the header to see if we wish
                 * to accept the data.  But synchronize
                 * the DMA transfer with the CPU first
                 * so that we see updated contents.
                 */
                dma_sync_single_for_cpu(&cp->dev, cp->rx_dma,
                                        cp->rx_len,
                                        DMA_FROM_DEVICE);

                /* Now it is safe to examine the buffer. */
                hp = (struct my_card_header *) cp->rx_buf;
                if (header_is_ok(hp)) {
                        dma_unmap_single(&cp->dev, cp->rx_dma, cp->rx_len,
                                         DMA_FROM_DEVICE);
                        pass_to_upper_layers(cp->rx_buf);
                        make_and_setup_new_rx_buf(cp);
                } else {
                        /* CPU should not write to
                         * DMA_FROM_DEVICE-mapped area,
                         * so dma_sync_single_for_device() is
                         * not needed here. It would be required
                         * for DMA_BIDIRECTIONAL mapping if
                         * the memory was modified.
                         */
                        give_rx_buf_to_card(cp);
                }
        }
}`

处理错误

DMA 地址空间在某些架构上受到限制,分配失败可以通过以下方式确定:

  • 检查 dma_alloc_coherent() 是否返回 NULL 或 dma_map_sg 返回 0
  • 使用 dma_mapping_error() 检查从 dma_map_single() 和 dma_map_page() 返回的 dma_addr_t:
    dma_addr_t dma_handle;
    
    dma_handle = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_handle)) {
            /*
             * reduce current DMA mapping usage,
             * delay and try again later or
             * reset driver.
             */
            goto map_error_handling;
    }
  • 当多页映射尝试过程中发生映射错误时,取消映射已映射的页。这些示例也适用于 dma_map_page()。

示例1:

dma_addr_t dma_handle1;
dma_addr_t dma_handle2;

dma_handle1 = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle1)) {
        /*
         * reduce current DMA mapping usage,
         * delay and try again later or
         * reset driver.
         */
        goto map_error_handling1;
}
dma_handle2 = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle2)) {
        /*
         * reduce current DMA mapping usage,
         * delay and try again later or
         * reset driver.
         */
        goto map_error_handling2;
}

...

map_error_handling2:
        dma_unmap_single(dma_handle1);
map_error_handling1:

Example 2: 示例2:

/*
 * if buffers are allocated in a loop, unmap all mapped buffers when
 * mapping error is detected in the middle
 */

dma_addr_t dma_addr;
dma_addr_t array[DMA_BUFFERS];
int save_index = 0;

for (i = 0; i < DMA_BUFFERS; i++) {

        ...

        dma_addr = dma_map_single(dev, addr, size, direction);
        if (dma_mapping_error(dev, dma_addr)) {
                /*
                 * reduce current DMA mapping usage,
                 * delay and try again later or
                 * reset driver.
                 */
                goto map_error_handling;
        }
        array[i].dma_addr = dma_addr;
        save_index++;
}

...

map_error_handling:

for (i = 0; i < save_index; i++) {

        ...

        dma_unmap_single(array[i].dma_addr);
}

如果传输挂钩 (ndo_start_xmit) 上的 DMA 映射失败,网络驱动程序必须调用 dev_kfree_skb() 来释放套接字缓冲区并返回 NETDEV_TX_OK。这意味着在失败情况下套接字缓冲区将被丢弃。

如果队列命令挂钩中的 DMA 映射失败,SCSI 驱动程序必须返回 SCSI_MLQUEUE_HOST_BUSY。这意味着 SCSI 子系统稍后会再次将命令传递给驱动程序。

优化取消映射状态空间消耗

在许多平台上,dma_unmap_{single,page}() 只是一个 nop。因此,跟踪映射地址和长度是浪费空间。不是用 ifdef 等填充驱动程序来“解决”这个问题(这会破坏可移植 API 的全部目的),而是提供以下功能。

实际上,我们不会一一描述宏,而是转换一些示例代码。

  1. 在状态保存结构中使用 DEFINE_DMA_UNMAP_{ADDR,LEN}。之前的例子:
    struct ring_state {
            struct sk_buff *skb;
            dma_addr_t mapping;
            __u32 len;
    };

    after: 后:

    struct ring_state {
            struct sk_buff *skb;
            DEFINE_DMA_UNMAP_ADDR(mapping);
            DEFINE_DMA_UNMAP_LEN(len);
    };
  2. Use dma_unmap_{addr,len}_set() to set these values. Example, before:使用 dma_unmap_{addr,len}_set() 设置这些值。之前的例子:
    ringp->mapping = FOO;
    ringp->len = BAR;

    after:

    dma_unmap_addr_set(ringp, mapping, FOO);
    dma_unmap_len_set(ringp, len, BAR);
  3. 使用 dma_unmap_{addr,len}() 访问这些值。之前的例子:
    dma_unmap_single(dev, ringp->mapping, ringp->len,
                     DMA_FROM_DEVICE);

    after:

    dma_unmap_single(dev,
                     dma_unmap_addr(ringp, mapping),
                     dma_unmap_len(ringp, len),
                     DMA_FROM_DEVICE);

这确实应该是不言自明的。我们单独对待 ADDR 和 LEN,因为实现可能只需要地址即可执行取消映射操作。

平台问题

如果您只是为 Linux 编写驱动程序并且不维护内核的体系结构端口,则可以安全地跳到“关闭”。

  1. Struct scatterlist requirements.结构分散列表要求。

    如果架构支持 IOMMU(包括软件 IOMMU),则需要启用 CONFIG_NEED_SG_DMA_LENGTH。

  2. ARCH_DMA_MINALIGN

    架构必须确保 kmalloc 的缓冲区是 DMA 安全的。驱动程序和子系统依赖于它。如果架构不完全 DMA 一致(即硬件不能确保 CPU 缓存中的数据与主内存中的数据相同),则必须设置 ARCH_DMA_MINALIGN,以便内存分配器确保 kmalloc'ed 缓冲区不会' t 与其他人共享缓存行。请参阅 arch/arm/include/asm/cache.h 作为示例。

    请注意,ARCH_DMA_MINALIGN 与 DMA 内存对齐约束有关。您无需担心架构数据对齐约束(例如关于 64 位对象的对齐约束)。

mmap

在计算机中, mmap(2) 是一个符合POSIX的Unix系统调用,它将文件或设备映射到内存中。它是一种内存映射文件I/O方法。它实现了按需分页,因为文件内容不是立即从磁盘读取的,并且最初根本不使用物理RAM。从磁盘的实际读取是在访问特定位置之后以惰性方式执行的。当映射不再需要时,指针必须使用 munmap(2) 取消映射。可以使用 mprotect(2) 管理保护信息(例如,将映射区域标记为可执行),并可以使用 madvise(2) 强制执行特殊处理。

在 Linux、macOS 和 BSD 中,mmap 可以创建多种类型的映射。其他操作系统可能只支持其中的一部分;例如,在没有全局 VFS 或 I/O 缓存的操作系统中,共享映射可能并不实用。

文件备份和匿名

文件支持映射将进程的虚拟内存区域映射到文件;也就是说,读取这些内存区域会导致文件被读取。这是默认的映射类型。

匿名映射映射进程虚拟内存中没有任何文件支持的区域。其内容初始化为零。在这方面,匿名映射类似于 malloc,在某些 malloc 实现中用于某些分配,尤其是大的分配。匿名映射不是 POSIX 标准的一部分,但几乎所有操作系统都使用 MAP_ANONYMOUSMAP_ANON 标志来实现匿名映射。

内存可见性

如果映射是共享的(设置了 MAP_SHARED 标志),那么当进程fork(使用 fork(2) 系统调用)时,它会被保留。因此,对一个进程中映射区域的写入在所有相关(父、子或同级)进程中立即可见。如果映射是由一个文件(不是 MAP_ANONYMOUS )共享和支持的,则底层文件介质只能在传递给 msync(2) 系统调用后才能被写入。相反,如果映射是私有的(设置了 MAP_PRIVATE 标志),则更改既不会被其他进程看到,也不会写入文件。

从底层文件中阅读或向其写入的进程不会总是看到与映射该文件的不同进程相同的数据,因为文件的段被复制到RAM中,并且仅定期刷新到磁盘。可以通过调用 msync(2) 强制同步。

在文件上使用mmap可以显著减少应用程序访问同一文件的内存开销;它们可以共享文件包含的内存区域,而不是为每个想要访问它的应用程序加载文件。这意味着mmap(2)有时用于进程间通信(IPC)。在现代操作系统中,mmap(2)通常比System V IPC共享内存工具更受欢迎。

System V共享内存(shutdown)和内存映射I/O(mmap)之间的主要区别是System V共享内存是持久的:除非被进程显式删除,否则它将保留在内存中并保持可用,直到系统关闭。mmap的内存在应用程序执行之间不是持久的(除非它由文件支持)。

在数据库实现中的使用

mmap系统调用已经在各种数据库实现中被用作实现缓冲池的替代方案,尽管这会产生一组不同的问题,实际上只能使用缓冲池来解决。

Memory-mapped file

内存映射文件是虚拟内存  的一段,它被分配了与文件或类似文件的资源的某个部分的直接字节对字节相关性。此资源通常是物理上存在于磁盘上的文件,但也可以是设备、共享内存对象或操作系统可以通过文件描述符引用的其他资源。一旦存在,文件和内存空间之间的这种相关性允许应用程序将映射部分视为主内存。

优点

内存映射文件的好处是提高I/O性能,特别是在大文件上使用时。对于小文件,内存映射文件可能会导致空闲空间  的浪费,因为内存映射总是与页面大小对齐,页面大小通常为4 KiB。因此,5 KiB文件将分配8 KiB,因此浪费了3 KiB。由于两个原因,内存映射文件比使用直接读写操作更快。首先,系统调用比简单地改变程序的本地内存要慢几个数量级。其次,在大多数操作系统中,内存区域映射实际上是内核的页面缓存(文件缓存),这意味着不需要在用户空间中创建副本。

某些应用程序级内存映射文件操作的性能也优于其物理文件操作。应用程序可以直接就地访问和更新文件中的数据,而不是从文件的开头查找或将整个编辑内容重写到临时位置。由于内存映射文件是在页面中内部处理的,因此线性文件访问(例如,在平面文件数据存储或配置文件中)仅在跨越新页面边界时才需要磁盘访问,并且可以在单个操作中将文件的较大部分写入磁盘。

内存映射文件的一个可能的好处是“延迟加载”,因此即使对于非常大的文件也只使用少量的RAM。尝试加载明显大于可用内存量的文件的全部内容可能会导致严重的颠簸,因为操作系统从磁盘读取到内存,并同时将页面从内存写回磁盘。内存映射不仅可以完全绕过页面文件,而且还允许在编辑数据时加载较小的页面大小的部分,类似于用于程序的请求分页。

内存映射过程由虚拟内存管理器处理,它是负责处理页面文件的同一个子系统。内存映射文件每次加载一整页到内存中。页面大小由操作系统选择以获得最佳性能。由于页面文件管理是虚拟存储器系统的最关键的元素之一,因此将文件的页面大小的部分加载到物理存储器中通常是非常高度优化的系统功能。

类型

有两种类型的内存映射文件:

Persisted 持续

持久化文件与磁盘上的源文件相关联。最后一个过程完成后,数据将保存到磁盘上的源文件中。这些内存映射文件适合处理非常大的源文件。

Non-persisted 非持续性

非持久化文件与磁盘上的文件不关联。当最后一个进程完成对文件的处理时,数据将丢失。这些文件适用于为进程间通信(IPC)创建共享内存。

缺点

选择内存映射文件I/O的主要原因是性能。然而,可以有权衡。由于系统调用开销和内存复制,标准I/O方法的成本很高。内存映射方法的代价是出现较小的页面错误--当一个数据块加载到页面缓存中,但尚未映射到进程的虚拟内存空间时。在某些情况下,内存映射文件I/O可能比标准文件I/O慢得多。

内存映射文件的另一个缺点与给定架构的地址空间有关:大于可寻址空间的文件一次只能映射部分,从而使阅读复杂化。例如,32位架构(如Intel的IA-32)只能直接寻址4 GiB或更小的文件部分。单个程序可用的可寻址空间量甚至更小-通常在2到3 GiB的范围内,具体取决于操作系统内核。然而,这个缺点在现代64位架构上几乎被消除了。

mmap的可伸缩性也往往低于标准的文件I/O方式,因为许多操作系统(包括Linux)对处理页面错误的内核数量有上限。极快的设备,如现代NVM Express SSD,能够使开销成为真实的问题。

底层文件上的I/O错误(例如,其可移动驱动器被拔出或光学介质被弹出,写入时磁盘已满等)在POSIX上,当访问其映射的内存时,会向应用程序报告SIGSEGV/SIGBUS信号,而在Windows上则报告EXECUTE_IN_PAGE_ERROR结构化异常。所有访问映射内存的代码都必须准备好处理这些错误,这些错误通常不会在访问内存时发生。

只有具有MMU的硬件体系结构才能支持内存映射文件。在没有MMU的架构上,操作系统可以在请求映射文件时将整个文件复制到内存中,但如果只有一小部分文件将被访问,这是非常浪费和缓慢的,并且只能对适合可用内存的文件起作用。

常见用途

也许内存映射文件最常见的用途是在大多数现代操作系统(包括Microsoft Windows和类Unix系统)中的进程加载器。当一个进程启动时,操作系统使用内存映射文件将可执行文件沿着任何可执行模块放入内存中执行。大多数内存映射系统使用一种称为请求分页的技术,其中文件以子集(每个页面一个)的形式加载到物理内存中,并且只有当该页面实际被引用时才加载。  在可执行文件的特定情况下,这允许OS选择性地仅加载实际需要执行的进程映像的那些部分。

内存映射文件的另一个常见用途是在多个进程之间共享内存。在现代保护模式操作系统中,进程通常不被允许访问被分配用于由另一进程使用的存储器空间。(程序试图这样做会导致无效的页面错误或分段冲突。)有许多技术可以安全地共享内存,内存映射文件I/O是最流行的技术之一。两个或多个应用程序可以同时将单个物理文件映射到内存中并访问该内存。例如,Microsoft Windows操作系统为应用程序提供了一种机制,用于对系统页面文件本身的共享段进行内存映射,并通过此段共享数据。