访问与总线无关的设备 【ChatGPT】

发布时间 2023-12-10 20:23:57作者: 摩斯电码

Bus-Independent Device Accesses

作者 Matthew Wilcox
作者 Alan Cox

介绍

Linux提供了一个API,它抽象了在所有总线和设备上执行IO的过程,允许独立于总线类型编写设备驱动程序。

内存映射IO

获取设备访问权限

最广泛支持的IO形式是内存映射IO。也就是说,CPU的一部分地址空间被解释为对设备的访问,而不是对内存的访问。一些体系结构将设备定义为固定地址,但大多数体系结构都有一些发现设备的方法。PCI总线遍历就是这样一种方案的很好的例子。本文档不涵盖如何接收此类地址的内容,而是假设您已经具备一个地址。物理地址的类型为unsigned long。

不应直接使用此地址。相反,为了获得适合传递给下面描述的访问器函数的地址,您应该调用ioremap()。将返回适合访问设备的地址。

在使用设备完成后(例如,在模块的退出例程中),调用iounmap()以将地址空间返回给内核。大多数体系结构在每次调用ioremap()时都会分配新的地址空间,如果不调用iounmap(),它们可能会用完。

访问设备

驱动程序中最常用的接口部分是对设备上的内存映射寄存器进行读写。Linux提供了读写8位、16位、32位和64位数量的接口。由于历史原因,它们被命名为字节、字、长和四字节访问。目前不支持预取支持。

这些函数的名称分别为readb()、readw()、readl()、readq()、readb_relaxed()、readw_relaxed()、readl_relaxed()、readq_relaxed()、writeb()、writew()、writel()和writeq()。

某些设备(例如帧缓冲区)希望一次使用比8字节更大的传输。对于这些设备,提供了memcpy_toio()、memcpy_fromio()和memset_io()函数。不要对IO地址使用memset或memcpy,因为不能保证按顺序复制数据。

读取和写入函数被定义为有序的。也就是说,编译器不允许重新排序I/O序列。当可以对排序进行编译器优化时,可以使用__readb()等函数来指示放松排序。请谨慎使用此功能。

虽然基本函数在彼此之间被定义为同步的,并且在彼此之间有序,但是设备所在的总线本身可能具有异步性。特别是许多作者都被PCI总线写入是异步的事实所困扰。驱动程序作者必须发出对同一设备的读取,以确保写入在作者关心的特定情况下已经发生。这种属性无法在API中对驱动程序编写者隐藏。在某些情况下,用于刷新设备的读取可能预期失败(例如,如果卡正在重置)。在这种情况下,应该从配置空间进行读取,如果卡没有响应,配置空间将保证软故障。

以下是在驱动程序希望确保写入效果在继续执行之前可见时刷新写入设备的示例:

static inline void
qla1280_disable_intrs(struct scsi_qla_host *ha)
{
    struct device_reg *reg;

    reg = ha->iobase;
    /* disable risc and host interrupts */
    WRT_REG_WORD(&reg->ictrl, 0);
    /*
     * The following read will ensure that the above write
     * has been received by the device before we return from this
     * function.
     */
    RD_REG_WORD(&reg->ictrl);
    ha->flags.ints_enabled = 0;
}

PCI订购规则还保证了PIO读取响应在总线上任何未完成的DMA写入之后到达,因为对于某些设备,readb()调用的结果可能会向驱动程序发出DMA事务已完成的信号。然而,在许多情况下,驱动程序可能希望指示下一个readb()调用与设备执行的任何先前DMA写入无关。驱动程序可以在这些情况下使用readb_relaxed(),尽管只有一些平台会支持放松的语义。在支持的平台上使用放松的读取函数将提供显著的性能优势。qla2xxx驱动程序提供了如何使用readX_relaxed()的示例。在许多情况下,驱动程序的大多数readX()调用可以安全地转换为readX_relaxed()调用,因为只有少数会指示或依赖于DMA完成。

端口空间访问

端口空间解释

另一种常见支持的IO形式是端口空间。这是一个与正常内存地址空间分开的地址范围。对这些地址的访问通常不像对内存映射地址的访问那样快速,并且它的地址空间可能更小。

与内存映射IO不同,访问端口空间不需要准备工作。

访问端口空间

对这个空间的访问是通过一组函数提供的,这些函数允许8位、16位和32位的访问;也称为字节、字和长。这些函数是inb()、inw()、inl()、outb()、outw()和outl()。

这些函数提供了一些变体。一些设备要求对它们的端口访问进行减速。这个功能是通过在函数的末尾添加_p来提供的。还有相当于memcpy的函数。ins()和outs()函数将字节、字或长复制到给定的端口。

__iomem指针标记

MMIO地址的数据类型是一个带有__iomem修饰的指针,比如void __iomem *reg。在大多数体系结构上,它是一个指向虚拟内存地址的常规指针,可以进行偏移或解引用,但在可移植的代码中,它只能从函数传递到显式操作__iomem标记的函数,并且从这些函数返回。特别是ioremap()和readl()/writel()函数。'稀疏'语义代码检查器可用于验证是否正确执行了这些操作。

虽然在大多数体系结构上,ioremap()为指向物理MMIO地址的未缓存虚拟地址创建了一个页表条目,但是一些体系结构需要特殊的指令来进行MMIO,而__iomem指针只是编码了物理地址或一个由readl()/writel()解释的可偏移的cookie。

I/O访问函数之间的差异

  • readq()、readl()、readw()、readb()、writeq()、writel()、writew()、writeb()
    这些是最通用的访问器,提供了与其他MMIO访问和DMA访问的串行化,以及访问小端PCI设备和芯片外设的固定字节顺序。可移植的设备驱动程序通常应该对任何对__iomem指针的访问使用这些函数。

    请注意,已发布的写入与自旋锁不是严格有序的,请参阅对内存映射地址的I/O写入排序。

  • readq_relaxed()、readl_relaxed()、readw_relaxed()、readb_relaxed()、writeq_relaxed()、writel_relaxed()、writew_relaxed()、writeb_relaxed()

    在需要昂贵的屏障以进行与DMA串行化的体系结构上,这些MMIO访问器的“放松”版本只与彼此串行化,但包含一个更便宜的屏障操作。设备驱动程序可能会在特别性能敏感的快速路径中使用这些函数,同时注释中解释了为什么在特定位置使用这些函数是安全的,而不需要额外的屏障。

    有关非放松和放松版本的精确排序保证的更详细讨论,请参阅memory-barriers.txt。

  • ioread64()、ioread32()、ioread16()、ioread8()、iowrite64()、iowrite32()、iowrite16()、iowrite8()

    这些是与normal readl()/writel()函数几乎相同行为的替代函数,但它们还可以操作由pci_iomap()或ioport_map()返回的__iomem标记映射的PCI I/O空间。在需要特殊指令进行I/O端口访问的体系结构上,这会增加在lib/iomap.c中实现的间接函数调用的小开销,而在其他体系结构上,这些函数只是别名。

  • ioread64be()、ioread32be()、ioread16be()、iowrite64be()、iowrite32be()、iowrite16be()

    这些函数的行为方式与ioread32()/iowrite32()系列相同,但是对于访问具有大端MMIO寄存器的设备,它们的字节顺序是相反的。可以操作大端或小端寄存器的设备驱动程序可能需要实现一个自定义包装函数,根据找到的设备选择其中一个。

    注意:在某些体系结构上,normal readl()/writel()函数传统上假定设备与CPU具有相同的字节顺序,而在运行大端内核时,在PCI总线上使用硬件字节反转。以这种方式使用readl()/writel()的驱动程序通常不具有可移植性,但往往限于特定的SoC。

  • hi_lo_readq()、lo_hi_readq()、hi_lo_readq_relaxed()、lo_hi_readq_relaxed()、ioread64_lo_hi()、ioread64_hi_lo()、ioread64be_lo_hi()、ioread64be_hi_lo()、hi_lo_writeq()、lo_hi_writeq()、hi_lo_writeq_relaxed()、lo_hi_writeq_relaxed()、iowrite64_lo_hi()、iowrite64_hi_lo()、iowrite64be_lo_hi()、iowrite64be_hi_lo()

    一些设备驱动程序具有64位寄存器,在32位体系结构上无法以原子方式访问,但允许两个连续的32位访问。由于取决于特定设备,必须首先访问这两个半部分中的哪一个,因此为每个64位访问器与低/高或高/低字顺序组合提供了一个辅助函数。设备驱动程序必须包含<linux/io-64-nonatomic-lo-hi.h>或<linux/io-64-nonatomic-hi-lo.h>,以获取函数定义以及在不提供64位访问的体系结构上将正常readq()/writeq()重定向到它们的辅助函数。

  • __raw_readq()、__raw_readl()、__raw_readw()、__raw_readb()、__raw_writeq()、__raw_writel()、__raw_writew()、__raw_writeb()

    这些是没有屏障或字节顺序更改以及特定体系结构行为的低级MMIO访问器。访问通常是原子的,即四字节的__raw_readl()不会被拆分为单独的字节加载,但多个连续访问可以在总线上合并。在可移植的代码中,只能安全地使用这些访问器来访问设备总线后面的内存,而不是MMIO寄存器,因为它们与其他MMIO访问或甚至自旋锁之间没有排序保证。字节顺序通常与正常内存相同,因此与其他函数不同,这些函数可以用于在内核内存和设备内存之间复制数据。

  • inl()、inw()、inb()、outl()、outw()、outb()

    PCI I/O端口资源传统上需要单独的帮助程序,因为它们是在x86体系结构上使用特殊指令实现的。在大多数其他体系结构上,这些在内部被映射到readl()/writel()风格的访问器,通常指向虚拟内存中的固定区域。地址不是一个__iomem指针,而是一个32位整数标记,用于标识端口号。PCI要求I/O端口访问是非发布的,这意味着outb()必须在以下代码执行之前完成,而正常的writeb()可能仍在进行中。在正确实现这一点的体系结构上,I/O端口访问因此与自旋锁有序。然而,许多非x86 PCI主机桥实现和CPU体系结构未能在PCI上实现非发布的I/O空间,因此它们最终可能会在此类硬件上发布。

    在一些体系结构中,I/O端口号空间与__iomem指针具有1:1的映射,但这不被推荐,设备驱动程序不应该依赖于它来实现可移植性。类似地,内核提供的PCI基址寄存器中描述的I/O端口号可能不对应于设备驱动程序看到的端口号。可移植的驱动程序需要读取内核提供的资源的端口号。

    没有直接的64位I/O端口访问器,但可以使用pci_iomap()结合ioread64/iowrite64来代替。

  • inl_p()、inw_p()、inb_p()、outl_p()、outw_p()、outb_p()

    在需要特定时间的ISA设备上,I/O访问器的_p版本会添加一个小的延迟。在没有ISA总线的体系结构上,这些是inb/outb帮助程序的别名。

  • readsq、readsl、readsw、readsb、writesq、writesl、writesw、writesb、ioread64_rep、ioread32_rep、ioread16_rep、ioread8_rep、iowrite64_rep、iowrite32_rep、iowrite16_rep、iowrite8_rep、insl、insw、insb、outsl、outsw、outsb

    这些是访问相同地址多次的帮助程序,通常用于在内核内存字节流和FIFO缓冲区之间复制数据。与正常的MMIO访问器不同,这些在大端内核上不执行字节交换,因此FIFO寄存器中的第一个字节对应于内存缓冲区中的第一个字节,而不考虑体系结构。

设备内存映射模式

某些架构支持多种设备内存映射模式。ioremap_*()变体提供了一个通用的抽象,围绕这些特定于架构的模式,具有共享的语义。

ioremap()是最常见的映射类型,适用于典型的设备内存(例如I/O寄存器)。如果架构支持,其他模式可以提供更弱或更强的保证。从最常见到最不常见,它们如下所示:

ioremap()

默认模式,适用于大多数内存映射设备,例如控制寄存器。使用ioremap()进行内存映射具有以下特点:

  • 无缓存(Uncached) - 绕过CPU端的缓存,所有读写操作直接由设备处理

  • 无推测操作(No speculative operations) - CPU在未达到已提交程序流的指令之前,不会对此内存进行读取或写入。

  • 无重排序(No reordering) - CPU不会对此内存映射的访问进行重排序。在某些架构上,这依赖于readl_relaxed()/writel_relaxed()中的屏障。

  • 无重复(No repetition) - CPU不会为单个程序指令发出多次读取或写入。

  • 无写组合(No write-combining) - 每个I/O操作导致向设备发出一个离散的读取或写入,并且多个写入不会合并为更大的写入。在使用__raw I/O访问器或指针解引用时,可能会或可能不会执行此操作。

  • 不可执行(Non-executable) - CPU不允许从此内存推测指令执行(可能不言而喻,但也不允许跳转到设备内存)。

在许多平台和总线(例如PCI)上,通过ioremap()映射发出的写入是已发布的,这意味着CPU在写入指令退休之前不会等待写入实际到达目标设备。

在许多平台上,I/O访问必须与访问大小对齐;不这样做将导致异常或不可预测的结果。

ioremap_wc()

将I/O内存映射为具有写组合的普通内存。与ioremap()不同的是,

  • CPU可以推测性地从程序实际未执行的设备中读取,并且可以选择基本上读取任何它想要的内容。

  • CPU可以重新排序操作,只要结果从程序的角度来看是一致的。

  • CPU可以多次写入同一位置,即使程序只发出了一次写入。

  • CPU可以将多个写入组合为单个更大的写入。

此模式通常用于视频帧缓冲区,可以提高写入性能。它也可以用于设备中的其他内存块(例如缓冲区或共享内存),但是必须小心,因为访问不保证与没有显式屏障的正常ioremap() MMIO寄存器访问有序。

在PCI总线上,通常可以在标记为IORESOURCE_PREFETCH的MMIO区域上使用ioremap_wc(),但不能在没有该标志的区域上使用。对于片上设备,没有相应的标志,但驱动程序可以在已知安全的设备上使用ioremap_wc()。

ioremap_wt()

将I/O内存映射为具有写透传缓存的普通内存。与ioremap_wc()类似,但还有以下特点:

  • CPU可以将写入设备的写入和从设备读取的读取缓存,并从该缓存中提供读取。

这种模式有时用于视频帧缓冲区,其中驱动程序仍然希望写入及时到达设备(而不会停留在CPU缓存中),但为了提高效率,读取可以从缓存中提供。然而,这在现今很少有用,因为帧缓冲区驱动程序通常只执行写入操作,对于这种情况,ioremap_wc()更有效(因为它不会不必要地破坏缓存)。大多数驱动程序不应使用此模式。

ioremap_np()

类似于ioremap(),但明确请求非发布写入语义。在某些架构和总线上,ioremap()映射具有发布写入语义,这意味着从CPU的角度来看,写入数据实际到达目标设备之前,写入指令可能会“完成”。写入仍然与同一设备的其他写入和读取有序,但由于发布写入语义,与其他设备不是这种情况。ioremap_np()明确请求非发布语义,这意味着写入指令在设备接收到(并在某种特定于平台的程度上确认)写入数据之前不会“完成”。

此映射模式主要用于适应需要此特定映射模式才能正常工作的总线结构的平台。这些平台为需要ioremap_np()语义的资源设置了IORESOURCE_MEM_NONPOSTED标志,便携式驱动程序应使用在适当情况下自动选择它的抽象(请参阅下面的更高级别的ioremap抽象部分)。

裸的ioremap_np()仅在某些架构上可用;在其他架构上,它始终返回NULL。除非它们是特定于平台的或从支持的情况中获益的驱动程序,否则驱动程序通常不应使用它,并且可以在其他情况下回退到ioremap()。确保发布写入完成的正常方法是在写入后执行一个虚拟读取,如“访问设备”中所述,这在所有平台上都适用于ioremap()。

对于PCI驱动程序,永远不应使用ioremap_np()。即使在其他实现了ioremap_np()的架构上,PCI内存空间写入也始终是发布的。使用ioremap_np()用于PCI BAR最多会导致发布写入语义,最坏的情况下会导致完全破坏。

请注意,非发布写入语义与CPU端的排序保证是正交的。CPU仍然可以选择在非发布写入指令退休之前发出其他读取或写入。有关CPU方面的详细信息,请参阅前面关于MMIO访问函数的部分。

ioremap_uc()

ioremap_uc()的行为类似于ioremap(),只是在x86架构上(没有'PAT'模式)时,即使MTRR将其指定为可缓存,它也将内存标记为无缓存,请参阅PAT(页面属性表)。

便携式驱动程序应避免使用ioremap_uc()。

ioremap_cache()

ioremap_cache()将I/O内存有效地映射为普通RAM。可以使用CPU写回缓存,并且CPU可以自由地将设备视为块RAM。这不应用于具有任何副作用的设备内存,也不应用于在读取时不返回先前写入数据的实际RAM。

对于实际的RAM,也不应使用它,因为返回的指针是一个__iomem标记。可以使用memremap()将位于线性内核内存区域之外的普通RAM映射到常规指针。

便携式驱动程序应避免使用ioremap_cache()。

架构示例

以下是上述模式在ARM64架构上的内存属性设置映射:

image

高级 ioremap 抽象

与使用上述原始 ioremap() 模式不同,驱动程序被鼓励使用更高级的 API。这些 API 可能实现特定平台的逻辑,以自动选择适当的 ioremap 模式在任何给定的总线上,从而使得一个与平台无关的驱动程序可以在这些平台上工作而无需任何特殊情况。在撰写本文时,以下 ioremap() 包装器具有这样的逻辑:

  • devm_ioremap_resource()
    如果在结构资源上设置了 IORESOURCE_MEM_NONPOSTED 标志,则可以根据平台要求自动选择 ioremap_np() 而不是 ioremap()。在驱动程序的 probe() 函数失败或设备从其驱动程序中解绑时,使用 devres 自动取消映射资源。

    在 Devres - 管理设备资源 中有文档记录。

  • of_address_to_resource()
    对于需要对某些总线进行非发布写操作的平台(参见非发布-mmio 和发布-mmio 设备树属性),自动设置 IORESOURCE_MEM_NONPOSTED 标志。

  • of_iomap()
    映射设备树中 reg 属性描述的资源,并执行所有必需的转换。根据平台要求自动选择 ioremap_np(),如上所述。

  • pci_ioremap_bar(), pci_ioremap_wc_bar()
    映射描述在 PCI 基地址中的资源,无需首先提取物理地址。

  • pci_iomap(), pci_iomap_wc()
    类似于 pci_ioremap_bar()/pci_ioremap_bar(),但与 ioread32()/iowrite32() 和类似的访问器一起在 I/O 空间上工作。

  • pcim_iomap()
    类似于 pci_iomap(),但使用 devres 在驱动程序的 probe() 函数失败或设备从其驱动程序中解绑时自动取消映射资源。

    在 Devres - 管理设备资源 中有文档记录。

不使用这些包装器可能使驱动程序在具有更严格的 I/O 内存映射规则的某些平台上无法使用。

对系统和 I/O 内存的通用访问

在访问内存区域时,根据其位置,用户可能需要使用 I/O 操作或内存加载/存储操作。例如,将数据复制到系统内存可以使用 memcpy(),将数据复制到 I/O 内存可以使用 memcpy_toio()。

void *vaddr = ...; // 指向系统内存的指针
memcpy(vaddr, src, len);

void *vaddr_iomem = ...; // 指向 I/O 内存的指针
memcpy_toio(vaddr_iomem, src, len);

这样的指针的使用者可能不了解该区域的映射,或者可能希望有一个单一的代码路径来处理该缓冲区上的操作,无论它是位于系统内存还是 I/O 内存。类型 struct iosys_map 及其辅助函数对此进行了抽象,因此可以将缓冲区传递给其他驱动程序,或者在同一驱动程序内具有分开的分配、读取和写入操作。

直接访问 struct iosys_map 的字段被认为是不良的编码风格。而不是直接访问其字段,应该使用提供的辅助函数之一,或者实现自己的辅助函数。例如,可以使用 IOSYS_MAP_INIT_VADDR() 静态初始化 struct iosys_map 的实例,或者使用 iosys_map_set_vaddr() 在运行时初始化。这些辅助函数将设置系统内存中的地址。

struct iosys_map map = IOSYS_MAP_INIT_VADDR(0xdeadbeaf);

iosys_map_set_vaddr(&map, 0xdeadbeaf);

要设置 I/O 内存中的地址,使用 IOSYS_MAP_INIT_VADDR_IOMEM() 或 iosys_map_set_vaddr_iomem()。

struct iosys_map map = IOSYS_MAP_INIT_VADDR_IOMEM(0xdeadbeaf);

iosys_map_set_vaddr_iomem(&map, 0xdeadbeaf);

struct iosys_map 的实例不必清理,但可以使用 iosys_map_clear() 将其清除为 NULL。清除的映射始终指向系统内存。

iosys_map_clear(&map);

使用 iosys_map_is_set() 或 iosys_map_is_null() 测试映射是否有效。

if (iosys_map_is_set(&map) != iosys_map_is_null(&map))
        // 总是为真

struct iosys_map 的实例可以使用 iosys_map_is_equal() 进行相等性比较。指向不同内存空间(系统或 I/O)的映射永远不相等。即使这两个空间位于相同的地址空间,两个映射包含相同的地址值,或者两个映射都指向 NULL,这也是正确的。

struct iosys_map sys_map; // 指向系统内存
struct iosys_map io_map; // 指向 I/O 内存

if (iosys_map_is_equal(&sys_map, &io_map))
        // 总是为假

设置好的 struct iosys_map 的实例可以用于访问或操作缓冲区内存。根据内存的位置,提供的辅助函数将选择正确的操作。数据可以使用 iosys_map_memcpy_to() 复制到内存中。地址可以使用 iosys_map_incr() 进行操作。

const void *src = ...; // 源缓冲区
size_t len = ...; // src 的长度

iosys_map_memcpy_to(&map, src, len);
iosys_map_incr(&map, len); // 转到 memcpy 后的第一个字节

更多参考 https://www.kernel.org/doc/html/v6.6/driver-api/device-io.html#c.iosys_map

提供的公共函数

https://www.kernel.org/doc/html/v6.6/driver-api/device-io.html#public-functions-provided