zoukankan      html  css  js  c++  java
  • I/O(一):基础知识

    本文假设你已经具备一些计算机的基本知识,包括但不限于:
    • Linux系统运行基础知识,如用户态、内核态。
    • Linux内存管理相关知识,如虚拟地址、物理地址、页表。
    • 汇编语言。
    • C语言。
    参考书籍和博客列表如下:
     

    一、I/O体系结构


      下图是计算机中I/O体系的分层结构图,其中操作系统又分为了文件系统、通用块层、设备驱动程序三个层次,分层设计的目的是将具体实现对用户进行屏蔽,方便上层用户使用以及下层扩展新类型的硬件I/O设备。

      

    二、I/O设备与总线


      本节我们学习I/O分层中的最底层,即与I/O设备相关的硬件知识。

    2.1.标准I/O设备    

      标准I/O设备,指的是I/O设备的模型规范。一般来说,任何一个真实的I/O设备(例如HDD,即磁盘驱动器)都是基于此标准I/O设备模型进行设计的:
      一个标准的I/O设备分为两部分,分别是硬件接口和内部结构:
    (1)硬件接口:硬件接口本质就是I/O设备提供的各式寄存器,系统软件通过与这些寄存器进行交互,达到控制I/O设备的目的。
    (2)内部结构:实现硬件接口提供的功能,不同的I/O设备具有不同功能,因此它们的内部实现和包含的元器件也不尽相同。
     

    2.2.计算机总线

      总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,传送的信息包括了数据、数据地址和控制信号。下面的图片引用自《深入理解计算机原理》,它形象的描述了CPU、内存、I/O设备之间如何通过总线相连:
      可以看到,不同的I/O设备(如键盘、鼠标、磁盘等)需要通过相应的接口电路与总线相连接,这些接口电路由“控制器”或“适配器”提供(后面统称为“设备控制器”)。不同的设备控制器能够支持不同的接口协议,下图引用自《操作系统导论》,它描述了几种常见的接口协议的I/O设备能够接入I/O总线这个事实:
      值得一提的是,根据接口协议的性能区别,现代计算机对I/O总线进行了分层。在上图中,图像或者其他高性能的I/O设备通过常规的I/O总线连接到系统,在许多现代系统中会是PCI或它的衍生形式。而一些相对较慢的I/O设备则通过外围总线(peripheral bus)连接到系统,比如使用SCSI、SATA或者USB等协议的I/O设备。
     

    三、与I/O设备交互


      本节我们站在I/O分层中软件与硬件的边界,去学习现代计算机如何与I/O设备交互。

    3.1.访问I/O设备

      主机对I/O设备进行访问的目标是I/O设备的寄存器或者内存。常见的I/O设备都只提供寄存器供主机访问,对于低速外设这样的模式是足够的,但是对于需要大量、高速数据交互的外设(如显卡、网卡),就需要主机能够直接访问外设的内存了。
      现代计算机提供了两种方式来访问I/O设备,它们分别是PMIO和MMIO:
    • PMIO:端口映射I/O(Port-mapped I/O)。将I/O设备独立看待,并使用CPU提供的专用I/O指令(如X86架构的in和out)访问。
    • MMIO:内存映射I/O(Memory-mapped I/O)。将I/O设备看作内存的一部分,不使用单独的I/O指令,而是使用内存读写指令访问。

    3.1.1.PMIO

      端口映射I/O,又叫做被隔离的I/O(isolated I/O),它提供了一个专门用于I/O设备“注册”的地址空间,该地址空间被称为I/O地址空间,最大寻址范围为64K,如下图所示:
     
      为了使I/O地址空间与内存地址空间隔离,要么在CPU物理接口上增加一个I/O引脚,要么增加一条专用的I/O总线。因此,并不是所有的平台都支持PMIO,常见的ARM平台就不支持PMIO。支持PMIO的CPU通常具有专门执行I/O操作的指令,例如在Intel-X86架构的CPU中,I/O指令是in和out,这两个指令可以读/写1、2、4个字节(outb, outw, outl)从内存到I/O接口上。
      由于I/O地址空间比较小,因此I/O设备一般只在其中“注册”自己的寄存器,之后系统可以通过PMIO对它们进行访问。

    3.1.2.MMIO

      在MMIO中,物理内存和I/O设备共享内存地址空间(注意,这里的内存地址空间实际指的是内存的物理地址空间),如下图所示:

      当CPU访问某个虚拟内存地址时,该虚拟地址首先转换为一个物理地址,对该物理地址的访问,会通过南北桥(现在被合并为I/O桥)的路由机制被定向到物理内存或者I/O设备上。因此,用于访问内存的CPU指令也可用于访问I/O设备,并且在内存(的物理)地址空间上,需要给I/O设备预留一个地址区域,该地址区域不能给物理内存使用。

      MMIO是应用得最为广泛的一种I/O方式,由于内存地址空间远大于I/O地址空间,I/O设备可以在内存地址空间上暴露自己的内存或者寄存器,以供主机进行访问。

    3.1.3.PCI设备

      PCI及其衍生的接口(如PCIE)主要服务于高速I/O设备(如显卡或网卡),使用PCI接口的设备又被称为PCI设备。与慢速I/O设备不同,计算机既需要访问它们的寄存器,也需要访问它们的内存。
      每个PCI设备都有一个配置空间(实际就是设备上一组连续的寄存器),大小为256byte。配置空间中包含了6个BAR(Base Address Registers,基址寄存器),BAR中记录了设备所需要的地址空间类型、基址以及其他属性,格式如下:
      可以看到,PCI设备能够申请两类地址空间,即内存地址空间和I/O地址空间,它们用BAR的最后一位区别开来。因此,PCI设备可以通过PMIO和MMIO将自己的I/O存储器(Registers/RAM/ROM)暴露给CPU(通常寄存器使用PMIO,而内存使用MMIO的方式暴露)。
      配置空间中的每个BAR可以映射一个地址空间,因此每个PCI设备最多能映射6段地址空间,但实际上很多设备用不了这么多。PCI配置空间的初始值是由厂商预设在设备中的,也就是说,设备需要哪些地址空间都是其自己定的,这可能会造成不同的PCI设备所映射的地址空间冲突,因此在PCI设备枚举(也叫总线枚举,由BIOS或者OS在启动时完成)的过程中,会重新为其分配地址空间,然后写入PCI配置空间中。
      在PCI总线之前的ISA总线是使用跳线帽来分配外设的物理地址,每插入一个新设备都要改变跳线帽以分配物理地址,这是十分麻烦且易错的,但这样的方式似乎我们更容易理解。能够分配自己总线上挂载设备的物理地址这也是PCI总线相较于I2C、SPI等低速总线一个最大的特色。
     

    3.2.数据交互流程

      使用I/O设备的目的是为了交互数据,不管是网卡、磁盘,亦或是键盘,总归要将数据进行输入输出。本小节以循序渐进的方式讲解主机与I/O设备的交互流程(或者称为“协议”),在其中我们可以看到PMIO的实际使用以及理解PIO、DMA的概念。

    3.2.1.标准交互流程

      一般来说,主机与I/O设备要进行数据交互,会经过这样一个过程:
    (1)CPU通过I/O设备的硬件接口(以下简称I/O接口)获取设备状态(即状态寄存器的值),只有“就绪”状态的设备才能进行数据传输。
    (2)CPU通过I/O接口下达交互指令:如果是读数据,则向I/O接口的命令寄存器输入要获取的数据在I/O设备的内部位置以及读设备指令;如果是写数据,则向I/O接口的命令寄存器输入要存放的数据在I/O设备的内部位置、写设备指令,以及向数据寄存器写入数据。
    (3)I/O设备内部根据I/O接口中寄存器的值,开始执行数据传输工作。
    (4)CPU在I/O设备完成工作后,执行其他操作,完成数据传送。
      标准交互流程实现起来比较简单,但是难免会有一些低效和不方便。第一个问题就是轮询过程比较低效,在等待设备是否满足某种状态时浪费大量CPU时间(下图描述的就是磁盘在执行数据传输过程中,CPU不能执行其他任务,只能等待传输完成),如果此时操作系统可以切换执行下一个就绪进程,就可以大大提高CPU的利用率。

    3.2.2.引入中断

      为了解决标准交互流程中CPU轮询低效的问题,我们需要引入中断来实现计算与I/O重叠。有了中断机制,CPU向设备发出I/O请求后,就可以让对应进程进入睡眠等待,从而切换执行其他进程。当设备完成I/O请求后,它会抛出一个硬件中断,引发CPU跳转执行操作系统预先定义好的中断处理程序,中断处理程序会挂起正在执行的进程,同时唤醒等待I/O的进程并继续执行。如下图所示,在磁盘执行进程1的I/O过程中,CPU同时执行进程2,并且在I/O请求执行完毕后,回过头来再次执行进程1:

      为了深入理解,我们引入一段《操作系统导论》中的代码:

     1 /**
     2  * 等待设备就绪
     3  */
     4 static int ide_wait_ready() {
     5     while (((int r = inb(0x1f7)) & IDE_BSY) || !(r & IDE_DRDY)))
     6         ; //轮询直到设备状态不为busy
     7 }
     8 
     9 /**
    10  * 开始执行IO请求
    11  */
    12 static void ide_start_request(struct buf *b) {
    13     ide_wait_ready();
    14     outb(0x3f6, 0); //向IDE磁盘控制寄存器写入0,即开启中断
    15     outb(0x1f2, 1); //向IDE磁盘命令寄存器的0x1f2地址写入扇区数
    16     outb(0x1f3, b->sector & 0xff); //向IDE磁盘命令寄存器的0x1f3地址写入对应逻辑块地址的低字节
    17     outb(0x1f4, (b->sector >> 8) & 0xff); //向IDE磁盘命令寄存器的0x1f3地址写入对应逻辑块地址的中字节
    18     outb(0x1f5, (b->sector >> 16) & 0xff); //向IDE磁盘命令寄存器的0x1f3地址写入对应逻辑块地址的高字节
    19     outb(0x1f6, 0xe0 | ((b->dev&1) << 4) | ((b->sector >> 24) & 0x0f)); //向IDE磁盘命令寄存器的0x1f6地址写入驱动编号
    20     if (b->flags & B_DIRTY) {
    21         outb(0x1f7, IDE_CMD_WRITE); //如果是写操作,向IDE磁盘命令寄存器的0x1f7地址写入写操作命令
    22         outsl(0x1f0, b->data, 512/4); //向IDE磁盘命令寄存器的0x1f0地址写入数据
    23     } else {
    24         outb(0x1f7, IDE_CMD_READ); //如果是读操作,向IDE磁盘命令寄存器的0x1f7地址写入读操作命令
    25     }
    26 }
    27 
    28 /**
    29  * IDE磁盘读写
    30  */
    31 void ide_rw(struct buf *b) {
    32     acquire(&ide_lock);
    33     for (struct buf **pp = &ide_queue; *pp; pp = &(*pp)->qnext)
    34         ; //遍历链式队列,获取队尾元素
    35     *pp = b; //将请求入队
    36     if (ide_queue == b) 
    37         ide_start_request(b); //如果队列为空,直接执行请求
    38     while ((b->flags & (B_VALID | B_DIRTY)) != B_VALID)
    39         sleep(b, &ide_lock); //进程睡眠等待IO设备执行完请求,会释放锁ide_lock
    40     release(&ide_lock);
    41 }
    42 
    43 /**
    44  * 中断响应程序
    45  */
    46 void ide_intr() {
    47     struct buf *b;
    48     acquire(&ide_lock);
    49     if (!(b->flags & B_DIRTY) && ide_wait_ready() >= 0)
    50         insl(0x1f0, b->data, 512/4); //如果是读请求,获取数据到内存
    51     b->flags != B_VALID;
    52     b->flags &= ~B_DIRTY;
    53     wakeup(b); //唤醒等待的主线程
    54     if ((ide_queue = b->qnext) != 0) 
    55         ide_start_request(ide_queue); //如果队列还有其他请求,则开始新的请求
    56     release(&ide_lock);
    57 }

      这段代码描述了操作系统通过中断的方式向IDE磁盘发送I/O请求,通过3个主要函数来实现:

    (1)第一个函数是ide_rw():它会将一个请求加入队列(如果前面还有请求未处理完成),或者直接将请求发送到磁盘(如果队列为空,直接调用ide_start_request()函数),但不论哪种情况,调用进程进入睡眠状态,等待请求处理完成。
    (2)第二个函数是ide_start_request():它使用outb等函数(这些函数封装了PMIO的out指令),向I/O接口的命令寄存器写入指令(见代码注释),如果是写请求,还会向数据寄存器写入数据。在发起请求之前,ide_start_request()会调用ide_wait_ready(),来确保驱动处于就绪状态。
    (3)第三个函数是ide_intr():它是一个中断响应处理程序,当IDE磁盘执行完I/O操作,会发出一个硬件中断,ide_intr()会被调用。如果是写操作,表示写操作已经执行完毕;如果是读操作,表示磁盘已经将内部数据送至I/O接口的数据寄存器,可以进行使用(即insl(0x1f0, b->data, 512/4)这行代码,操作系统使用in命令读取到内存去)。之后唤醒等待的进程,如果此时在队列中还有别的未处理的请求,则调用ide_start_request()接着处理下一个I/O请求。

    3.2.3.引入DMA

      在标准交互流程和引入中断流程中,数据在硬件中的移动都是通过CPU完成的,比如CPU从内存读取数据到CPU寄存器,然后将CPU寄存器的数据写入I/O设备寄存器。但是对CPU来说,它的主要功能是使用内部的算数/逻辑单元(ALU)执行计算,而不是做一个数据搬运工,如果CPU参与大量数据的移动,就白白浪费了宝贵的时间和算力。为了让CPU从数据移动的工作中解放出来,我们需要引入DMA机制。
      DMA,全称为direct memory access,直接内存访问。它是I/O设备与主存之间由硬件组成的直接数据通路,用于高速I/O设备与主存之间的成组数据(即数据块)传送。实现DMA机制的硬件叫做DMA控制器,一个典型的DMA控制器组成如下:
      DMA控制器包含了多个设备寄存器(如ADR、DBR),以及中断控制逻辑、DMA控制逻辑、DMA接口连接线,这些构件的具体功能,有兴趣的读者可以阅读《计算机组成与结构(清华大学出版社)》一书的“DMA输入输出方式”章节,此处不属于本文讨论的范畴,略过不表。
      引入了DMA机制之后,与I/O设备的数据交互流程变为下图所示:
     
    (1)DMA预处理:在进行DMA数据传送之前要用程序做一些必要的准备工作。先由CPU执行几条IN/OUT指令,测试设备状态,向DMA控制器的设备地址寄存器中送入I/O设备地址并启动I/O设备,向主存地址寄存器中送入交换数据的主存起始地址,在数据字数寄存器中送入交换的数据个数。这些工作完成之后,CPU继续执行原来的程序。
    (2)DMA控制I/O设备与主存之间的数据交换,并且在数据交换完毕或者出错时,向CPU发出结束中断请求或出错中断请求。
    (3)CPU中断程序进行后处理,若需继续交换数据,则要对DMA控制器进行初始化;若不需要交换数据,则停止外设;若为出错,则转错误诊断及处理程序。
      下图仍然是与磁盘交互时各硬件执行进程任务的时间轴,可以看到,CPU将原本用于移动进程1的I/O数据的时间用于执行进程2,相应的,DMA代替了数据移动的工作:
     

    3.2.4.总结与补充

      如果根据CPU是否参与数据移动来划分I/O类型,可以将I/O分为以下2种:
    • PIO:即编程的I/O(programmed I/O),CPU参与数据移动,数据流向为"device <-> CPU register <-> memory"。
    • DMA:CPU不参与数据移动,它只要启动I/O设备并向DMA控制器发送数据传输相关信息,就可以去执行其他任务,数据流向为"device <-> DMA <-> memory"。
      最后,纵观上文,我们只使用PMIO来访问I/O设备,以磁盘访问的C代码为例,如何使用MMIO的方式向设备写入控制指令呢?Linux为我们封装了一切,只需要使I/O设备通过MMIO来访问,然后使用Linux提供的MMIO函数即可,我们将在下面小节详细讨论Linux是如何支持PMIO与MMIO的。
     

    四、Linux的具体实现


      不同架构的CPU访问I/O设备的方式不尽相同,对Linux来说,它需要兼容多种访问方式,并尽可能提供统一的抽象。

    4.1.共享内存地址空间

      PMIO的I/O地址空间独立于内存地址空间,管理起来比较简单,而MMIO需要I/O设备和物理内存共享内存(的物理)地址空间,Linux必须精心管理内存以实现共享。我并不打算从头开始讲解Linux如何管理内存,而是在分页和分段内存管理的基础上进一步深入讨论。

    4.1.1.划分物理地址空间

      以X86架构为例,32位CPU最大支持4G物理地址空间,该空间被划分为若干段:
    • ZONE_DMA:范围是0~16M,该段的内存页专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。其中640K~1M这段地址空间被BIOS和VGA适配器所占据。
    • ZONE_NORMAL:范围是16M~896M,该区域的物理页是内核能够直接使用的。
    • ZONE_HIGHMEM:范围是896M~结束,该区域即为高端内存,内核不能直接使用。

      可以看到,ZONE_DMA中640K~1M的区域以及ZONE_HIGHMEM中用于MMIO的区域,其被I/O设备等占用。当CPU访问这两个区域的物理地址时,北桥会自动将物理地址路由到相应的I/O设备上,不会发送给物理内存,因此在此处的物理内存无法被访问,从而形成RAM空洞

    4.1.2.内核虚拟地址空间

      虚拟地址空间中内核使用的部分与物理地址空间存在映射关系:

      Linux使用分页机制管理内存,内核想要访问物理地址空间的话,必须先建立映射关系,然后通过虚拟地址来访问。为了能够访问所有的物理地址空间,就要将全部物理地址空间映射到1G的虚拟地址空间中,这显然不可能,于是内核采用了分类的思想来解决这个问题:

    (1)内核将0~896M的物理地址空间一对一映射到自己的虚拟地址空间中,这样它便可以随时访问ZONE_DMA和ZONE_NORMAL里的物理页面,所以内核会将频繁使用的数据,如kernel代码、GDT、IDT、PGD、mem_map数组等放在ZONE_NORMAL里。

    (2)此时内核剩下的128M虚拟地址空间不足以完全映射所有ZONE_HIGHMEM,Linux采取了动态映射的方法,即按需的将ZONE_HIGHMEM里的物理页面映射到kernel space的最后128M虚拟地址空间里,使用完之后释放映射关系,以供其它物理页面映射,虽然这样存在效率的问题,但是内核毕竟可以正常的访问所有的物理地址空间了。128M虚拟地址空间主要由3部分组成,分别为vmalloc area、持久化内核映射区、临时内核映射区,类似用户数据、页表(PT)等不常用数据放在ZONE_HIGHMEM里,只在要访问这些数据时才建立映射关系。

     

    4.1.3.用户虚拟地址空间

      虚拟地址空间中用户使用的部分与物理地址空间存在映射关系,下图是实际情况中的一种映射关系:
     
      可以看到,用户虚拟地址空间是无法访问内核直接映射的0~896M这一块物理地址,这与用户态无法访问内核使用的内存保持了一致。用户虚拟地址空间的详细布局如下,每个分段的功能不属于本文所讲内容范围:     
      总的来说,内核态可以访问所有的物理地址空间,而用户态只能访问ZONE_HIGHMEM区域中的物理地址空间,并且其中被内核动态映射的部分无法访问,而且不能超过其虚拟地址空间中用户区域大小。
      

    4.2.抽象:I/O资源

      为了统一管理PMIO和MMIO这两种访问方式的I/O设备,Linux提供了一个统一的抽象,叫做“I/O资源”,它是一个树状结构,每个结点记录已分配地址的设备信息,包括设备名称、地址范围、状态/权限标识、父节点/兄弟节点/孩子节点指针,并且PMIO和MMIO有各自独立的I/O资源。I/O资源的结构体定义代码如下:
    1 struct resource {
    2     resource_size_t start; //资源范围的开始
    3     resource_size_t end; //资源范围的结束
    4     const char *name; //资源拥有者的名字
    5     unsigned long flags; //各种标志
    6     struct resource *parent, *sibling, *child; //指向资源树中父亲,兄弟和孩子的指针
    7 };

      此外,Linux为PMIO和MMIO提供了2个独立的函数用于申请I/O资源,它们分别是request_region()、request_mem_region(),在使用I/O设备前,必须先通过它们申请I/O资源。我们可以简单看一下这两个函数的部分代码和注释:

     1 //可以看到,这两个函数本质上都是宏定义,真正调用的是函数__request_region,但是传入的第一个参数不同
     2 #define request_region(start, n, name) __request_region(&ioport_resource, (start), (n), (name))
     3 #define request_mem_region(start, n, name) __request_region(&iomem_resource, (start), (n), (name))
     4 
     5 //ioport_resouce,是I/O资源resource结构体的一个变量
     6 struct resource ioport_resource = {
     7     .name = "PCI IO",
     8      .start = 0x0000,
     9      .end = IO_SPACE_LIMIT,
    10      .flags = IORESOURCE_IO,
    11 };
    12 
    13 //iomem_resource,也是I/O资源resource结构体的一个变量
    14 struct resource iomem_resource = {
    15     .name = "PCI mem",
    16      .start = 0UL,
    17      .end = ~0UL,
    18      .flags = IORESOURCE_MEM,
    19 };
    20 
    21 //__request_region方法,代码略。该函数没有做实际性的映射工作,只是告诉内核要使用一块内存地址,
    22 //并声明占有,内核会为其找到符合条件的一块内存地址
    23 struct resource * __request_region(struct resource *parent, unsigned long start, unsigned long n, const char *name) {
    24     //......
    25 }

      Linux在使用I/O设备之前必须先申请I/O资源的做法,目的是告诉内核某个I/O设备的驱动程序将使用此范围的I/O地址,这将防止其他驱动程序对同一地址区域重复申请使用,该函数不进行任何类型的映射,它只是一种纯保留机制。

     

    4.3.访问MMIO设备

      为了使用MMIO寻址方式来访问I/O设备,第一步先调用request_mem_region()申请I/O资源,此时只是完成了对该I/O设备使用的声明,在物理地址空间上完成了占用。接着调用ioremap()将I/O设备的物理地址映射到操作系统内核虚拟地址空间,之后就可以通过Linux提供的函数访问这些I/O设备接口了。访问完成后,释放申请的地址映射以及I/O资源。

      

    4.4.访问PMIO设备

      Linux实现了2种方式来访问PMIO的I/O设备。
      第一种方式比较好理解,就是直接使用IN/OUT指令,Linux为in、out、ins和outs汇编指令包装了一系列辅助函数:
    函数 说明
    inb()、inw()、inl() 分别从I/O接口读取1、2或4个连续字节。后缀“b”、“w”、“l”分别代表一个字节(8位)、一个字(16位)以及一个长整型(32位)
    inb_p()、inw_p()、inl_p() 分别从I/O接口读取1、2或4个连续字节,然后执行一条“哑元(dummy,即空指令)”指令使CPU暂停
    outb()、outw()、outl() 分别向一个I/O接口写入1、2或4个连续字节
    outb_p()、outw_p()、outl_p() 分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“哑元”指令使CPU暂停
    insb()、insw()、insl() 分别从I/O端口读入以1、2或4个字节为一组的连续字节序列,字节序列的长度由该函数的参数给出
    outsb()、outsw()、outsl() 分别向I/O端口写入以1、2或4个字节为一组的连续字节序列
      使用这些辅助函数,I/O设备访问流程如下:
     
      第二种方式在第一种方式上增加了一层映射,目的是使用与MMIO相同的辅助函数来访问PMIO下的I/O设备,流程如下:
     
      可以看到,第二种方式的整体流程与MMIO非常相似,都是先调用request_region()申请I/O资源,然后调用ioport_map()这个函数,将其分配的地址映射到一个新的“内存地址”,接着可以使用Linux提供的包装辅助函数来访问I/O设备,访问完毕后,释放映射与I/O资源。值得重点关注的是,ioport_map()函数到底做了什么“内存映射”,是否同MMIO的ioremap()一样?下面是ioremap的源码:
    1 void __iomem *ioport_map(unsigned long port, unsigned int nr) {
    2     if (port > PIO_MASK)
    3         return NULL;
    4     return (void __iomem *) (unsigned long) (port + PIO_OFFSET);
    5 }

       ioport_map仅仅是将I/O设备接口的物理地址简单加上PIO_OFFSET(64k),这样PMIO的64k地址空间就被映射到64k~128k之间,而ioremap()返回的虚拟地址则肯定在3G之上。ioport_map所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,它并没有实际映射到内核虚拟地址,仅仅是为了让用户可以使用统一的辅助函数来访问I/O接口,这些辅助函数如下:

    函数 说明
    unsigned int ioread8(void *addr) 在I/O设备的端口地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是还是建议使用Linux内核的提供的函数来访问I/O映射内存。此函数用于读取指定I/O映射内存地址的连续8位
    unsigned int ioread16(void *addr) 使用方式同ioread8,功能为读取指定I/O映射内存地址的连续16位
    unsigned int ioread32(void *addr) 使用方式同ioread8,功能为读取指定I/O映射内存地址的连续32位
    void iowrite8(u8 value, void *addr) 使用方式同ioread8,功能为向指定I/O映射内存地址写入8位数据
    void iowrite16(u16 value, void *addr) 使用方式同ioread8,功能为向指定I/O映射内存地址写入16位数据
    void iowrite32(u32 value, void *addr) 使用方式同ioread8,功能为向指定I/O映射内存地址写入32位数据
      最后来看一下ioread8的源码,它内部对虚拟地址进行了判断,以区分PMIO映射地址和MMIO映射地址,然后分别使用inb/outb和readb/writeb来读写,readb/writeb是普通的内存访问函数:
     1 //ioread8源码,调用一个宏命令
     2 unsigned int fastcall ioread8(void __iomem *addr) {
     3     IO_COND(addr, return inb(port), return readb(addr));
     4 }
     5 
     6 //宏命令IO_COND
     7 #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)
     8 #define IO_COND(addr, is_pio, is_mmio) do { 
     9     unsigned long port = (unsigned long __force)addr;
    10         if (port < PIO_RESERVED) {
    11             VERIFY_PIO(port);
    12             port &= PIO_MASK;
    13             is_pio; 
    14         } else {
    15             is_mmio;
    16         }
    17 } while (0)
    18 
    19 //宏展开后的ioread8源码
    20 unsigned int fastcall ioread8(void __iomem *addr)
    21 {
    22     unsigned long port = (unsigned long __force)addr;
    23     if( port < 0x40000UL ) {
    24         BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );
    25         port &= PIO_MASK;
    26         return inb(port);
    27     }else{
    28         return readb(addr);
    29     }
    30 }
     
  • 相关阅读:
    CentOS部署ElasticSearch7.6.1集群
    Linux安装Elasticsearch7.x
    ElasticSearch安装为Windows服务
    SolrNet Group分组 实现
    ubuntu 下安装sublime
    LeetCode 3: Longest Substring Without Repeating Characters
    LeetCode 179: Largest Number
    LeetCode 1: Two Sum
    LeetCode 190: Reverse Bits
    LeetCode 7: Reverse Integer
  • 原文地址:https://www.cnblogs.com/manayi/p/15332978.html
Copyright © 2011-2022 走看看