CPU要想控制所链接的设备,不可避免需要通过IO(input/output)与外设打交道,CPU通过IO操纵设备上的寄存器等来实现对 设备的控制。一般厂商按照IO空间性质将IO划分为IO 端口和IO内存。

IO 端口 VS IO内存

两者差别如下:

两者划分按照空间是否与CPU空间独立划分:

  • IO内存:IO内存又称为Memory-Mapped I/O(MMIO),该IO空间处在CPU空间范围内,IO内存和普通的内存没什么区别,两者都是通过CPU的地址总线和控制总线发送电平信号进行访问,再通过数据总线读写数据。要想操纵该IO就得首先将该IO映射到CPU的地址中,然后就可以访问该IO,如同访问内存。大多数嵌入式设备都属于此。
  • IO端口:又称为Port(PIO),该IO的空间与CPU空间相互独立,两者互相独立,相互不干扰,这种类型IO在X86中比较常见,该IO端口有独立的空间,所以CPU要想访问该端口就得通过一些专有函数或者指令。

IO端口

IO端口拥有独立空间,与CPU 处在不同得空间,CPU如果要想访问该端口就需要使用专有的指令集,比如X86提供IN 和OUT指令集。

linux内核也为访问IO端口提供了一系列函数和方法:

在内核中如果要对某些端口进行操作,就要首先获取到访问该IO权限,以防止其他程序同时操作该端口。要获取端口端口权限可以使用request_region函数,该函数定义include/linux/ioport.h文件中:

struct resource *request_region(unsigned long first, unsigned long n, const  char *name)

  • first:要获取的起始端口。如果要同时获取多个连续端口,则该参数为起始端口
  • n: 要获取端口数量
  • name:设备名字

当获取到端口之后,可以在/proc/ioports文件中查看当前系统所有已经被分配的端口。

当端口使用完毕或者驱动模块卸载时,需要将占用的端口给释放掉,以供其他程序使用,释放端口函数为release_region()函数:

void release_region(unsigned long start, unsigned long n)

从内核源码ioport.h文件中可以看到,其实上述两个函数为宏定义:

...
#define request_region(start,n,name)        __request_region(&ioport_resource, (start), (n), (name), 0)...
#define release_region(start,n) __release_region(&ioport_resource, (start), (n))

真正起作用的为__request_region和    __release_region函数,其实所有以及被分配的IO端口在ioport_resource中进行管理,查看__request_region代码:


/*** __request_region - create a new busy resource region* @parent: parent resource descriptor* @start: resource start address* @n: resource region size* @name: reserving caller's ID string* @flags: IO resource flags*/
struct resource * __request_region(struct resource *parent,resource_size_t start, resource_size_t n,const char *name, int flags)
{DECLARE_WAITQUEUE(wait, current);struct resource *res = alloc_resource(GFP_KERNEL);struct resource *orig_parent = parent;if (!res)return NULL;res->name = name;res->start = start;res->end = start + n - 1;write_lock(&resource_lock);for (;;) {struct resource *conflict;res->flags = resource_type(parent) | resource_ext_type(parent);res->flags |= IORESOURCE_BUSY | flags;res->desc = parent->desc;conflict = __request_resource(parent, res);if (!conflict)break;/** mm/hmm.c reserves physical addresses which then* become unavailable to other users.  Conflicts are* not expected.  Warn to aid debugging if encountered.*/if (conflict->desc == IORES_DESC_DEVICE_PRIVATE_MEMORY) {pr_warn("Unaddressable device %s %pR conflicts with %pR",conflict->name, conflict, res);}if (conflict != parent) {if (!(conflict->flags & IORESOURCE_BUSY)) {parent = conflict;continue;}}if (conflict->flags & flags & IORESOURCE_MUXED) {add_wait_queue(&muxed_resource_wait, &wait);write_unlock(&resource_lock);set_current_state(TASK_UNINTERRUPTIBLE);schedule();remove_wait_queue(&muxed_resource_wait, &wait);write_lock(&resource_lock);continue;}/* Uhhuh, that didn't work out.. */free_resource(res);res = NULL;break;}write_unlock(&resource_lock);if (res && orig_parent == &iomem_resource)revoke_devmem(res);return res;
}

从上述代码中可以看到,将IO端口抽象成resource结构资源,里面存放着每次申请的起始端口和端口数量,如果本次申请的IO资源以及在ioport_resource存在,则说明存在资源冲突,造成申请失败。如果不存在冲突 ,则申请IO端口资源成功并将其加入到ioport_resource中。

当IO端口占用成功之后,就可以对IO端口进行读写操作,需要用到专有的读写操作函数in和out系列函数:

unsigned in[bwl](unsigned long port)
void out[bwl](value, unsigned long port)
  • b:bytes操作IO端口数据为一个字节
  • w:word 操作IO端口数据为两个字节
  • l:long操作IO端口数据为四个字节

另外还提供了参数为字符变量接口,比在外面使用C循环更加高效:

void ins[bwl](unsigned port, void *addr, unsigned long count)
void outs[bwl](unsigned port, void *addr, unsigned long count)

例如 :从端口中读取一个8 bits:

oldlcr = inb(baseio + UART_LCR)

往一个端口中写入一个8 bit数据:

outb(MOXA_MUST_ENTER_ENCHANCE, baseio + UART_LCR)

上述为内核对IO端口操作的一个基本流程。

IO内存

IO内存又称为MMIO,该IO空间就是处于CPU的空间,原因就是占用了CPU的总线地址空间,其性质和普通的内存一样,由于访问时该IO时和访问内存一样都是物理地址,而在linux中并不会直接对物理地址进行操作,需要将其映射到虚拟地址中。由于linux属于宏内核,驱动位于内核中,一个驱动程序要想访问IO内存就必须将其映射到内核的虚拟地址空间中(linux在空间划分时会专门留出一段空间预留给IO使用),然后才能对IO进行读写操作。

早期对IO内存操作过程和IO端口类似,首先需要调用request_mem_region函数将该IO内存资源占用,防止其他程序占用该IO:

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

  • start: 该IO的地址相当与物理地址,一般该地址是由芯片分配好,可以从使用的CPU datasheet中查到。
  • len: 申请IO数量,必须是start起始地址连续 len个字节
  • nane: 设备名称或者IO端口名称,用以标记该端口被谁使用

占用完毕之后,可以使用/proc/iomem文件中查看当前系统IO内存使用情况。

当使用完毕之后,可以使用release_mem_region()函数,将占用的IO释放掉。

上述函数仅仅只是完成了对IO占用,并没有将其映射到内核的空间中,内核提供了ioremap函数,将其IO内存映射到内核的虚拟空间中:

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

ioremap返回值为映射到内核的虚拟地址,之后对该IO操作就使用映射之后的该虚拟地址进行操作。

当使用完毕之后,可以使用iounmap将映射释放掉:

void iounmap(void * addr)

为了操作一个IO内存,首先需要取得所有权然后进行映射到虚拟地址空,操作起来其实不是太方便,上述方法已经在内核中废弃掉了,只是由于历史原因还有些旧的驱动在使用,后面驱动开发人员不要再进行使用。

新的IO内存操作接口为devm_ioremap():

void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,    resource_size_t size)

该接口集成了获取IO所有权和映射功能,所以使用IO内存时只需要使用这一个接口即可

释放端口可以使用:

void devm_iounmap(struct device *dev, void __iomem *addr)

虽然IO内存和普通内存地址一样,但是不建议直接使用指针对IO内存地址进行访问,使用专有的函数进行访问。

针对不相同架构上可能访问方法不一样,比如针对PCI中的端口,都为小字节序,所以可以使用下面函数进行操作:

unsigned read[bwl](void *addr);
void write[bwl](unsigned val, void *addr);

如果时一个raw,并且不需要字节转换可以使用下面接口进行操作:

unsigned __raw_read[bwl](void *addr);
void __raw_write[bwl](unsigned val, void *addr);

现今大部分的IO都为小字节序,因为这样可以避免再次的字节序转换,从而提高效率。

比如在:drivers/tty/serial/uartlite.c文件中对一个IO内存写入32bit数据:

writel(c & 0xff, port->membase + 4);

read[bwl]和write[bwl]历史缺陷

read[bwl]和write[bwl]系列函数实现位于各个芯片架构io.h文件中,不同的架构实现稍有差别,主要差异点在与其指令集的实现,include/asm-generic/io.h文件中。

以writeb为例子,其源代码为:

static inline void writeb(u8 value, volatile void __iomem *addr)
{__io_bw();__raw_writeb(value, addr);__io_aw();
}

可以看到最后本质上还是调用的__raw_writeb()函数:

static inline void __raw_writeb(u8 value, volatile void __iomem *addr)
{*(volatile u8 __force *)addr = value;
}

LDD3指出read[bwl]和write[bwl]系列函数存在一些列的不安全问题:

Other drivers, knowing that I/O memory addresses are not real pointers, store them in integer variables; that works until they encounter a system with a physical address space which doesn't fit into 32 bits. And, in any case, readb() and friends perform no type checking, and thus fail to catch errors which could be found at compile time.

linus在邮件列表中指出,由于历史原因,在调用read/write时 很多驱动开发人员传入不是一个地址而是使用一个integer整型,那么在64位芯片时由于地址时8个字节,而不是整型4个字节,此时会出现问题

mostly just because of historical reasons, and as a result some drivers didn't use a pointer at all, but some kind of integer.
Sometimes even one that couldn't _fit_ a MMIO address in it on a 64-bit machine.

在2.6.9 kernel版本中对IO 内存提供了一系列新的API对IO进行操作

unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);//“string”字符串版本
void ioread8_rep(void __iomem *port, void *buf, unsigned long count)

再次特别指出笔者在最新的5.11.5内核版本中发现read和write函数最后的调用实际上和ioread和iowrite一样,笔者猜测可能时后面防止出现问题,在后面的版本中也将read和write函数缺陷给修改掉了,但是并没有在邮件列表中找到。

为了论证笔者的猜测,笔者翻到了当初2.6.9版本中的解决方案说明:

The 2.6.9 kernel will contain a series of changes designed to improve how the kernel works with I/O memory. The first of these is a new __iomem annotation used to mark pointers to I/O memory. These annotations work much like the __user markers, except that they reference a different address space. As with __user, the __iomem marker serves a documentation role in the kernel code; it is ignored by the compiler. When checking the code with sparse, however, developers will see a whole new set of warnings caused by code which mixes normal pointers with __iomem pointers, or which dereferences those pointers.

在2.6.9的解决方案中,通过增加对IO memory 指针增加了一个 __iomem修饰,在编译时对地址进行检查。  __iomem 是一个cookie,当编译内核是使用C=1,编译器可以通过sparse来检查使用该修饰符标记的地址是否合法,该字符定义为:

# define __iomem        __attribute__((noderef, address_space(2)))

目的就是在访问IO memory时需要进行严格检查,避免程序出错,如果在使用IO 内存地址时不加该参数,在编译解决会直接告警。

其中address_space指明该地址位于那个地址空间,共划分为四个地址空间:

v: 0 内核空间
        v: 1 用户空间
        v: 2 io存储空间
        v: 3 cpu空间

在5.11.5版本中发现read[bwl]和write[bwl] 函数也对地址进行了__iomem修饰,该系列函数存在的类型安全问题应该已经不存在。LLD3中代码版本较老,很长时间都没有进行更新,在学习时还是需要做下代码对比。

针对__iomem解决的整个思路,linus在https://lwn.net/Articles/102240/邮件中说的非常明白,有兴趣的同学可以了解一下,

Big-endian大字节序接口

针对有些端口为大字节序的问题,linux还提供了一些列的专有函数:

unsigned int ioread16be(const void __iomem *)
unsigned int ioread32be(const void __iomem *)
u64 ioread64be(const void __iomem *)
void iowrite16be(u16, void __iomem *)
void iowrite32be(u32, void __iomem *)
void iowrite64be(u64, void __iomem *)

rep接口

如果要想读或者写一系列的值到一个给定的IO memory内存地址,可以使用一下接口重复版本,相对与在使用循环调用效率要高:

void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr, const void *buf, unsigned long count);

这些函数读或写 count 值从给定的 buf 到 给定的 addr. 注意 count 表达为在被写入的数据大小; ioread32_rep 读取 count 32-位值从 buf 开始。

MMIO side effects

在操作MMIO时,需要特别注意IO的side effects(很多地方都译为边际效应,其实字面意思不是很容易理解,其实质为副作用)。MMIO边际效率,其来源就是因为CPU缓存原因。现在CPU架构或者说是计算机的架构都是采用哈弗结构形式,即计算和存储进行分离,CPU只参与数据的计算而无法对数据或者指令进行保存,要想取得数据或者指令必须从存储中读取,而由于半导体摩尔定律的发展,CPU的运算速度每间隔2年都会有很大提升,但是对存储或者说内存的访问速率并没有太大提升,这就明显限制了整个CPU性能。为了解决IO和CPU性能之前巨大的性能差异,CPU厂商一般都会在内部集成缓存以临时存储数据,减少对外部内存的读写操作。在对普通的内存操作中,很多其实并不是从内存中读写,而是从缓存中读取(现代CPU一般都会设计多级缓存以增加性能)。而在操作IO时,如果操作IO的数据也被存储到缓存中,那么如果一旦硬件数据发生变化,此时存储在缓存中的IO数据并没有感知,进而造成程序中正在读取的IO寄存器数据和实际硬件中的IO寄存器的值并不相同,造成IO 寄存器的边际效应。

下面可以使用一个简单的代码来说明该问题

if (x) y = *ptr

有一个指针为ptr,当X不为零时,会将ptr地址的值复制给y。在编译器编译之后,和下面代码是相同的代码:

tmp = *ptr; if (x) y = tmp

首先将ptr地址的值赋值给tmp,然后当x不等于0时,将tmp值赋值为y。

上述两段代码针对普通的内存操作是没有问题的,而当ptr指向IO mem时将会出现很大问题,因为此时io端口寄存器的值被存储到tmp中,而tmp值为一个缓存变量,如果在之后io端口寄存器的值发生变化,tmp并无法感知,此时就会造成y的值最终和该IO寄存器的值不一样。

为了解决该问题,一般在定义寄存器地址时,一般使用volatile关键字修饰指向IO地址的指针,就是告诉编译器每次都从源中存取,不从缓存中存取,防止编译器进行优化从缓存中存取。

除了上述问题之外,编译器在对代码编译时会出现过度优化,通过解析前后代码依赖顺序,将串行代码编译乱序执行,以提高效率。CPU的乱序执行也对IO mem造成很大副作用,在一个串行执行的代码中本来设计的时后读IO寄存器的值,有可能被优化成先执行读取IO寄存器的值,这样就有机会造成IO寄存器的值和实际读取出来存到变量中的值不一样,为了解决上述问题目前主要有两者手段:

  • 对硬件IO操作关闭缓存
  • 强制在代码中加入同步机制,防止乱序执行造成问题。

linux 中提供了4个宏来来解决排序问题:

#include <linux/kernel.h>
void barrier(void)
这个函数告知编译器插入一个内存屏障但是对硬件没有影响. 编译的代码将所有的
当前改变的并且驻留在 CPU 寄存器的值存储到内存, 并且后来重新读取它们当需
要时. 对屏障的调用阻止编译器跨越屏障的优化, 而留给硬件自由做它的重编排.
#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
这些函数插入硬件内存屏障在编译的指令流中; 它们的实际实例是平台相关的. 一
个 rmb ( read memory barrier) 保证任何出现于屏障前的读在执行任何后续读之
前完成. wmb 保证写操作中的顺序, 并且 mb 指令都保证. 每个这些指令是一个屏
障的超集

现在会过头看下前面的 writeb函数,里面其实也是插入了硬件同步__io_bw和__io_aw:

static inline void writeb(u8 value, volatile void __iomem *addr)
{__io_bw();__raw_writeb(value, addr);__io_aw();
}

这也就是为什么必须使用专有函数访问IO mem的最大原因。

参考资料

https://stackoverflow.com/questions/19100536/what-is-the-use-of-iomem-in-linux-while-writing-device-drivers

https://stackoverflow.com/questions/59113831/what-is-the-benefit-of-calling-ioread-functions-when-using-memory-mapped-io

https://lwn.net/Articles/102232/

https://lwn.net/Articles/102240/

LDD3

IO 端口和IO 内存(原理篇)相关推荐

  1. io端口与ion内存

    木子你妹 博客园 首页 新随笔 联系 管理 订阅 随笔- 109  文章- 12  评论- 8  <摘录>io端口和io内存 linux中的 IO端口映射和IO内存映射 (一)地址的概念 ...

  2. 【linux开发】IO端口和IO内存的区别及分别使用的函数接口

    IO端口和IO内存的区别及分别使用的函数接口 每个外设都是通过读写其寄存器来控制的.外设寄存器也称为I/O端口,通常包括:控制寄存器.状态寄存器和数据寄存器三大类.根据访问外设寄存器的不同方式,可以把 ...

  3. Linux系统IO端口,Linux系统对IO端口和IO内存的管理

    五.Linux下访问IO端口 对于某一既定的系统,它要么是独立编址.要么是统一编址,具体采用哪一种则取决于CPU的体系结构. 如,PowerPC.m68k等采用统一编址,而X86等则采用独立编址,存在 ...

  4. IO端口、IO内存、IO空间、内存空间的含义和联系

    1,IO空间:X86一个特有的空间,与内存空间独立的空间,同样利用IO空间可以操作数据,只不过是利用对应的IO端口操作函数,例如inb(), inbw(), inl(); outb(), outw() ...

  5. IO端口和IO接口的区别

    IO接口:主机和外设之间的交接界面,通过接口可以实现主机和外设之间的信息交换. IO端口:接口电路中可被CPU直接访问的寄存器. 扩展知识: IO端口分为:数据端口,状态端口,控制端口 若干个端口加上 ...

  6. 【操作系统之哲学导论】内存原理篇

    基本内存管理 内存管理的环境 程序要运行,必须先加载到内存,内存管理负责对内存架构进行管理,使用户无需担心自己的程序是在缓存,主存,还是磁盘上,实现的手段就是靠虚拟内存,虚拟内存就是一个幻像,给用户提 ...

  7. IO的端口映射和内存映射 (Port mapped I/O 和 Memory mapped I/O说明)

    IO端口和IO内存的区别及分别使用的函数接口  每个外设都是通过读写其寄存器来控制的.外设寄存器也称为I/O端口,通常包括:控制寄存器.状态寄存器和数据寄存器三大类.根据访问外设寄存器的不同方式,可以 ...

  8. linux系统下:IO端口,内存,PCI总线 的 读写(I/O)操作

    [GitHub开源项目]https://github.com/sig-ishihara/linux_pysical_address_rw_cmd Table of Contents IO端口的in/o ...

  9. Windows内核原理-同步IO与异步IO

    目录 Windows内核原理-同步IO与异步IO 背景 目的 I/O 同步I/O 异步I/O I/O完成通知 总结 参考文档 Windows内核原理-同步IO与异步IO 背景 在前段时间检查异常连接导 ...

最新文章

  1. 被七牛云OSS对象存储测试域名回收后正确数据迁移姿势!
  2. handler消息机制入门
  3. C#设计模式(1)——单例模式
  4. http协议的缺点和确保web安全的https协议
  5. Thread class vs Runnnable interface(转)
  6. SecureCRT报错ImportError: No module named itertools(解决方案无法复现)
  7. 作用域js和java区别_js作用域理解
  8. ubuntu11.10安装arm-linux-gcc详解
  9. MFC基于对话框的商场交易软件实现
  10. python os.system
  11. HTML 制作简历表单
  12. HTMl5 的新特性
  13. HDU 2148 Score
  14. 程序之间耦合以及解耦问题探究
  15. SqlServer 2017 下载地址及密钥下载地址
  16. 打卡1 谭浩强c语言程序设计第三章
  17. ctbs mysql_C/C++/Java
  18. Pyspark 案例实践 假新闻分类
  19. 论文总结——SIPaKMeD宫颈细胞Pap涂片数据集
  20. Python压缩、减压7z文件

热门文章

  1. 构建ASP.NET MVC5+EF6+EasyUI 1.4.3+Unity4.x注入的后台管理系统(52)-美化EasyUI皮肤和图标
  2. 微信网站-微信应用-微信二次开发-演示方案
  3. 十二. python面向对象主动调用其他类
  4. Red 编程语言 2019 开发计划:全速前进!
  5. 华为VLAN间互访配置
  6. Hadoop中Partition解析
  7. java.lang.ClassNotFoundException: org.springframework.web.util.IntrospectorCleanupListener错误解决方案...
  8. 选择适合 Rails 开发的操作系统
  9. Mysql总结_02_mysql数据库忘记密码时如何修改
  10. android 上线流程