【问题标题】:What is the benefit of calling ioread functions when using memory mapped IO使用内存映射IO时调用ioread函数有什么好处
【发布时间】:2019-11-30 06:35:52
【问题描述】:

要使用内存映射 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 返回的地址不应直接取消引用; 应该使用内核提供的访问器函数。

谁能解释这背后的原因或使用ioread32iowrite8()等访问函数的优势?

【问题讨论】:

    标签: linux io linux-kernel x86 linux-device-driver


    【解决方案1】:

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


    在 C 中,数据竞争是 UB(未定义行为)。这意味着允许编译器假设通过非volatile 指针访问的内存在访问之间不会改变。而转向 if (x) y = *ptr; 可以转换为 tmp = *ptr; if (x) y = tmp; 即编译时推测加载。

    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 地址空间(端口 I/O,又名 PIO;in/out 说明x86)与内存地址空间(正常加载/存储到特殊地址)。而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 等一些 little-endian 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 只是可变的解引用;一些 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

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


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

    但 32 位 x86 PAE 显然是 Linux 非常关心的一个 ISA,即使在 Linux 历史的早期也是如此。

    【讨论】:

    • ioremap 正确返回虚拟地址,我的问题是为什么我们不能直接使用地址并取消引用它
    • @md.jamal:哦,我看错了问题,抱歉。我的回答是解释为什么你需要ioremap >.READ_ONCE 或 WRITE_ONCE 强制转换为 volatile* 以确保优化仍然会导致恰好 1 次访问(不是 0 次或超过 1 次)。
    • 超过1是什么意思
    • @md.jamal:更新了我的答案。如果多次使用tmp 并且编译器决定只从*ioreg 重新加载,而不是将tmp 保留在寄存器或堆栈空间中,您可能会从tmp = *ioreg; 中获得多个asm 加载。但如果访问权限是volatile,则不会。
    • @md.jamal:我想我被标题弄糊涂了。您是否编造了“ioread 函数”的名称?我想我误认为ioremap 在您的问题中显示为唯一类似名称的函数。你的意思是 READ_ONCE 和 WRITE_ONCE,对吧?
    猜你喜欢
    • 2011-04-22
    • 2011-04-20
    • 2011-04-29
    • 2016-01-04
    • 1970-01-01
    • 2020-05-20
    • 2019-10-12
    • 2012-05-22
    • 2010-10-14
    相关资源
    最近更新 更多