I/O 寄存器和常规内存

Posted fanweisheng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I/O 寄存器和常规内存相关的知识,希望对你有一定的参考价值。

不管硬件寄存器和内存之间的强相似性, 存取 I/O 寄存器的程序员必须小心避免被 CPU(或者编译器)优化所戏弄, 它可能修改希望的 I/O 行为.

 

I/O 寄存器和 RAM 的主要不同是 I/O 操作有边际效果, 而内存操作没有: 一个内存写的 唯一效果是存储一个值到一个位置, 并且一个内存读返回最近写到那里的值. 因为内存存 取速度对 CPU 性能是至关重要的, 这种无边际效果的情况已被多种方式优化: 值被缓存, 并且 读/写指令被重编排.

 

编译器能够缓存数据值到 CPU 寄存器而不写到内存, 并且即便它存储它们, 读和写操作 都能够在缓冲内存中进行而不接触物理 RAM. 重编排也可能在编译器级别和在硬件级别都 发生: 常常一个指令序列能够执行得更快, 如果它以不同于在程序文本中出现的顺序来执 行, 例如, 为避免在 RISC 流水线中的互锁. 在 CISC 处理器, 要花费相当数量时间的操 作能够和其他的并发执行, 更快的.

 

当应用于传统内存时(至少在单处理器系统)这些优化是透明和有益的, 但是它们可能对正 确的 I/O 操作是致命的, 因为它们干扰了那些"边际效果", 这是主要的原因为什么一个 驱动存取 I/O 寄存器. 处理器无法预见这种情形, 一些其他的操作(在一个独立处理器上 运行, 或者发生在一个 I/O 控制器的事情)依赖内存存取的顺序. 编译器或者 CPU 可能 只尽力胜过你并且重编排你请求的操作; 结果可能是奇怪的错误而非常难于调试. 因此, 一个驱动必须确保没有进行缓冲并且在存取寄存器时没有发生读或写的重编排.

 

硬件缓冲的问题是最易面对的:底层的硬件已经配置(或者自动地或者通过 Linux 初始化 代码)成禁止任何硬件缓冲, 当存取 I/O 区时(不管它们是内存还是端口区域).

 

对编译器优化和硬件重编排的解决方法是安放一个内存屏障在必须以一个特殊顺序对硬件 (或者另一个处理器)可见的操作之间. 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 指令都保证. 每个这些指令是一个屏 障的超集.

 

read_barrier_depends 是读屏障的一个特殊的, 弱些的形式. 而 rmb 阻止所有跨 越屏障的读的重编排, read_barrier_depends 只阻止依赖来自其他读的数据的读 的重编排. 区别是微小的, 并且它不在所有体系中存在. 除非你确切地理解做什么, 并且你有理由相信, 一个完整的读屏障确实是一个过度地性能开销, 你可能应当坚 持使用 rmb.

 

void smp_rmb(void);

void smp_read_barrier_depends(void); void smp_wmb(void);

void smp_mb(void);

 

屏障的这些版本仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为 一个简单的屏障调用.

 

在一个设备驱动中一个典型的内存屏障的用法可能有这样的形式:

 

writel(dev->registers.addr, io_destination_address); writel(dev->registers.size, io_size);

writel(dev->registers.operation, DEV_READ); wmb();

writel(dev->registers.control, DEV_GO);

 

在这种情况, 是重要的, 确保所有的控制一个特殊操作的设备寄存器在告诉它开始前已被 正确设置. 内存屏障强制写以需要的顺序完成.

 

因为内存屏障影响性能, 它们应当只用在确实需要它们的地方. 屏障的不同类型也有不同 的性能特性, 因此值得使用最特定的可能类型. 例如, 在 x86 体系上, wmb() 目前什么 都不做, 因为写到处理器外不被重编排. 但是, 读被重编排, 因此 mb() 被 wmb() 慢.

 

值得注意大部分的其他的处理同步的内核原语, 例如自旋锁和原子的 _t 操作, 如同内存 屏障一样是函数. 还值得注意的是一些外设总线(例如 PCI 总线)有它们自己的缓冲问题; 我们在以后章节遇到时讨论它们.

 

一些体系允许一个赋值和一个内存屏障的有效组合. 内核提供了几个宏来完成这个组合; 在缺省情况下, 它们如下定义:

 

#define set_mb(var, value) do var = value; mb();  while 0

#define set_wmb(var, value) do var = value; wmb(); while 0

#define set_rmb(var, value) do var = value; rmb(); while 0

 

在合适的地方, <asm/system.h> 定义这些宏来使用体系特定的指令来很快完成任务. 注 意 set_rmb 只在少量体系上定义. (一个 do...while 结构的使用是一个标准 C 用语, 来使被扩展的宏作为一个正常的 C 语句可在所有上下文中工作).

以上是关于I/O 寄存器和常规内存的主要内容,如果未能解决你的问题,请参考以下文章

I/O端口和IO内存

I/O 端口和 I/O 内存

linux中readl()和writel()函数---用于读写寄存器

Linux 设备驱动IO操作

汇编小知识

linux 系统对IO端口和IO内存的管理