使用内存映射IO时调用ioread函数有什么好处

What is the benefit of calling ioread functions when using memory mapped IO

要使用内存映射I/O,我们需要先调用request_mem_region。

struct resource *request_mem_region(
                unsigned long start,
                unsigned long len,
                char *name);

然后,由于内核是运行在虚拟地址space,我们需要通过运行ioremap函数将物理地址映射到虚拟地址space。

void *ioremap(unsigned long phys_addr, unsigned long size);

那为什么我们不能直接访问return值。

来自 Linux 设备驱动程序手册

Once equipped with ioremap (and iounmap), a device driver can access any I/O memory address, whether or not it is directly mapped to virtual address space. Remember, though, that the addresses returned from ioremap should not be dereferenced directly; instead, accessor functions provided by the kernel should be used.

任何人都可以解释这背后的原因或访问函数的优势,例如 ioread32iowrite8()

您需要 ioread8 / iowrite8 或任何 至少 强制转换为 volatile* 以确保优化仍然导致恰好 1 次访问(不为 0 或大于 1)。事实上,他们做的不止于此,处理字节序(他们还处理字节序,访问设备内存作为小端。或者 ioread32be 用于大端)和一些编译时重新排序内存屏障语义 Linux 选择包含在这些函数中。由于 DMA,读取后甚至会出现运行时障碍。使用 _rep 版本仅通过一个屏障从设备内存中复制一个块。


在 C 中,数据竞争是 UB(未定义行为)。这意味着允许编译器假设通过非 volatile 指针访问的内存在访问之间不会改变。并且 if (x) y = *ptr; 可以转换为 tmp = *ptr; if (x) y = tmp; 即编译时推测加载,如果已知 *ptr 没有错误。 (相关:Who's afraid of a big bad optimizing compiler? 回复:为什么 Linux 内核需要 volatile 来滚动它自己的原子。)

MMIO 寄存器即使对于读取也可能有副作用,因此您必须 阻止编译器执行不在源代码中的加载,并且必须强制它执行所有加载 在源代码中正好出现一次。

实体店优惠相同。 (编译器甚至不允许发明对非易失性对象的写入,但它们可以删除死存储。例如 *ioreg = 1; *ioreg = 2; 通常会编译为 *ioreg = 2; 第一个存储被删除为“死”,因为它被认为没有明显的副作用。

C volatile 语义是 MMIO 的理想选择,但是 Linux 除了 volatile 之外,还包含更多内容。


通过谷歌搜索 ioread8 并在 https://elixir.bootlin.com/linux/latest/source/lib/iomap.c#L11 中四处寻找,我们发现 Linux I/O 地址可以编码 IO 地址 space(端口I/O,又名 PIO;in / out x86 上的指令)与内存地址 space(普通 load/store 到特殊地址)。 ioread* 函数实际上会检查并相应地进行调度。

    /*
     * Read/write from/to an (offsettable) iomem cookie. It might be a PIO
     * access or a MMIO access, these functions don't care. The info is
     * encoded in the hardware mapping set up by the mapping functions
     * (or the cookie itself, depending on implementation and hw).
     *
     * The generic routines don't assume any hardware mappings, and just
     * encode the PIO/MMIO as part of the cookie. They coldly assume that
     * the MMIO IO mappings are not in the low address range.
     *
     * Architectures for which this is not true can't use this generic
     * implementation and should do their own copy.
     */

例如实现,这里是ioread16。 (IO_COND 是一个根据预定义常量检查地址的宏:低地址是 PIO 地址)。

    unsigned int ioread16(void __iomem *addr)
    {
      IO_COND(addr, return inw(port), return readw(addr));
      return 0xffff;
    }

如果您将 ioremap 结果转换为 volatile uint32_t* 会发生什么问题?

例如如果您使用 READ_ONCE / WRITE_ONCE 只是转换为 volatile unsigned char* 或其他,并且用于对共享变量的原子访问。 (在 Linux 的手卷 volatile + 原子的内联 asm 实现中,它使用它而不是 C11 _Atomic)。

如果编译时重新排序不是问题,那实际上可能适用于 x86 等小端 ISA,但其他 ISA 需要更多障碍。如果您查看 definition of readlioread32 用于 MMIO,而不是 inl 用于 PIO),它使用围绕 volatile 指针的取消引用的障碍。

(这个和它使用的宏在与这个相同的 io.h 中定义,或者您可以使用 LXR 链接进行导航:每个标识符都是一个超链接。)

static inline u32 readl(const volatile void __iomem *addr) {
    u32 val;
    __io_br();
    val = __le32_to_cpu(__raw_readl(addr));
    __io_ar(val);
    return val;
}

泛型 __raw_readl 只是 volatile 解引用;一些 ISA 可能会提供他们自己的。

__io_ar() 使用 rmb()barrier() 阅读后。 /* prevent prefetching of coherent DMA data ahead of a dma-complete */。 Before Read 障碍只是 barrier() - 在没有 asm 指令的情况下阻止编译时重新排序。



错误问题的旧答案:下面的文字回答了为什么你需要调用ioremap


因为它是一个物理地址并且内核内存不是标识映射 (virt = phys) 到物理地址。

并且返回虚拟地址不是一个选项:并非所有系统都有足够的虚拟地址 space 甚至可以将所有物理地址 space 直接映射为连续的虚拟地址范围。 (但是当 space 足够时,Linux 会这样做,例如 x86-64 Linux 的虚拟地址-space 布局记录在 x86_64/mm.txt

特别是 RAM 大于 1 或 2GB 的系统上的 32 位 x86 内核(取决于内核的配置方式:2:2 或 1:3 kernel:user 虚拟地址分割space)。使用用于 36 位物理地址的 PAE space,32 位 x86 内核可以使用 更多 物理内存,而不是一次映射。 (这太可怕了,让内核的日子不好过:一些随机的博客转载了 Linus Torvald 关于如何 PAE really really sucks 的评论。)


其他 ISA 可能也有这个,并且我知道 Alpha 在需要字节访问时对 IO 内存做了什么;也许将字 loads/stores 映射到字节 loads/stores 的物理地址 space 区域较早处理,因此您请求正确的物理地址。 (http://www.tldp.org/HOWTO/Alpha-HOWTO-8.html)

但 32 位 x86 PAE 显然是 Linux 非常关心的 ISA,甚至在 Linux 历史的早期。