首页 > 解决方案 > 使用内存映射IO时调用ioread函数有什么好处

问题描述

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

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

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

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

那为什么我们不能直接访问返回值呢。

来自 Linux 设备驱动程序书

一旦配备了 ioremap(和 iounmap),设备驱动程序就可以访问任何 I/O 内存地址,无论它是否直接映射到虚拟地址空间。但请记住,从 ioremap 返回的地址不应直接取消引用;相反,应该使用内核提供的访问器函数。

任何人都可以解释这背后的原因或访问器功能(如ioread32or )的优势iowrite8()吗?

标签: linuxiolinux-kernelx86linux-device-driver

解决方案


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


在 C 中,数据竞争是 UB (Undefined Behaviour)。这意味着允许编译器假设通过非volatile指针访问的内存在访问之间不会改变。并且这种转变if (x) y = *ptr;可以转化为tmp = *ptr; if (x) y = tmp;即编译时的推测性负载。

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

商店也一样。(编译器甚至不允许对非易失性对象进行写操作,但它们可以删除死存储。例如*ioreg = 1; *ioreg = 2;,通常编译与*ioreg = 2; 第一个存储被删除为“死”相同,因为它不被认为具有可见的副作用。

Cvolatile语义对于 MMIO 来说是理想的,但 Linux 围绕它们包装了更多的东西,而不仅仅是 volatile。


ioread8通过谷歌搜索并在https://elixir.bootlin.com/linux/latest/source/lib/iomap.c#L11中快速浏览后,我们看到 Linux I/O 地址可以编码 IO 地址空间(端口 I/ O,又名 PIO;in/ outx86 上的指令)与内存地址空间(正常加载/存储到特殊地址)。并且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 + inline asm 原子实现中,它使用而不是 C11 _Atomic)。

如果编译时重新排序不是问题,这实际上可能适用于 x86 等一些小端 ISA,但其他人需要更多障碍。如果您查看(用于 MMIO,而不是用于 PIO)的定义readl,它会在指针的取消引用周围使用障碍。ioread32inlvolatile

( this 和 this 使用的宏定义与io.hthis 相同,或者您可以使用 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只是可变的取消引用;一些 ISA 可能会提供自己的。

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




对错误问题的旧答案:下面的文字回答了您需要致电的原因ioremap

因为它是一个物理地址,并且内核内存没有身份映射(virt = phys)到物理地址。

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

尤其是 32 位 x86 内核在 RAM 超过 1 或 2GB 的系统上(取决于内核的配置方式:2:2 或 1:3 内核:虚拟地址空间的用户分割)。使用 36 位物理地址空间的 PAE,32 位 x86 内核可以使用比一次映射更多的物理内存。(这太可怕了,让内核很难过:一些随机博客转发了 Linus Torvald 关于PAE 真的很糟糕的评论。)


其他 ISA 也可能有这个,并且 IDK 在需要字节访问时 Alpha 对 IO 内存做了什么;也许将字加载/存储映射到字节加载/存储的物理地址空间区域已提前处理,因此您请求正确的物理地址。( http://www.tldp.org/HOWTO/Alpha-HOWTO-8.html )

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


推荐阅读