linux kernel 学习手记3 I/O

 9  与硬件通讯

在硬件级别上, 内存区和 I/O 区域没有概念上的区别: 它们都是通过在地址总线和控制总线上发出电信号来存取(, 读写信号)[32]并且读自或者写到数据总线. 也有例外,有些cpu在芯片上一个单个地址空间, 如:x86有人开的读和写电线给I/O端口和特殊的cpu指令存取端口,也有的在芯片组或cpu中附加额外的电路,后一种方法在很多嵌入式cpu中出现较多。

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

对编译器优化和硬件重编排的解决方法是安放一个内存屏障。

 

#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);

void smp_rmb(void);

void smp_read_barrier_depends(void);

void smp_wmb(void);

void smp_mb(void);

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

I/O 端口分配

#include <linux/ioport.h>

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

这个函数告诉内核, 你要使用 n 个端口, first 开始. name 参数应当是你的设备的名子. 如果分配成功返回值是非 NULL. 如果你从 request_region 得到 NULL, 你将无法使用需要的端口.

所有的的端口分配显示在 /proc/ioports . 如果你不能分配一个需要的端口组, 这是地方来看看谁先到那里了.

当你用完一组 I/O 端口(在模块卸载时, 也许), 应当返回它们给系统, 使用:

void release_region(unsigned long start, unsigned long n); 

还有一个函数以允许你的驱动来检查是否一个给定的 I/O 端口组可用:

int check_region(unsigned long first, unsigned long n); 

但是一般不去使用这个函数,因为无法保证能获取端口,这是由于非原子性造成。

I/O 端口操作

Linux 内核头文件(特别地, 体系依赖的头文件 <asm/io.h>) 定义了下列内联函数来存取 I/O 端口:

unsigned inb(unsigned port);

void outb(unsigned char byte, unsigned port);

读或写字节端口( 8 位宽 ). port 参数定义为 unsigned long 在某些平台以及 unsigned short 在其他的上. inb 的返回类型也是跨体系而不同的.

unsigned inw(unsigned port);

void outw(unsigned short word, unsigned port);

这些函数存取 16-位 端口( 一个字宽 ); 在为 S390 平台编译时它们不可用, 它只支持字节 I/O.

unsigned inl(unsigned port);

void outl(unsigned longword, unsigned port);

这些函数存取 32-位 端口. longword 声明为或者 unsigned long 或者 unsigned int, 根据平台. 如同字 I/O, "Long" I/O S390 上不可用.

从用户空间访问I/O端口

下列条件应当应用来对于 inb 及其友在用户空间代码中使用:

·         程序必须使用 -O 选项编译来强制扩展内联函数.

·         ioperm iopl 系统调用必须用来获得权限来进行对端口的 I/O 操作. ioperm 为单独端口获取许可, iopl 为整个 I/O 空间获取许可. 2 个函数都是 x86 特有的.

·         程序必须作为 root 来调用 ioperm 或者 iopl.[34] 可选地, 一个它的祖先必须已赢得作为 root 运行的端口权限.

如果没有 ioperm iopl系统调用,则用户空间可以通过/dev/port设备文件访问I/O端口

可以尝试在程序中调用setuid root这样就不用显式获取特权了。

技术上来说,必须有CAP_SYS_RAWIO权限

字符串操作

直接的串操作比c下的循环效率要高很多,

字串函数的原型是:

void insb(unsigned port, void *addr, unsigned long count);

void outsb(unsigned port, void *addr, unsigned long count);

读或写从内存地址 addr 开始的 count 字节. 数据读自或者写入单个 port 端口.

void insw(unsigned port, void *addr, unsigned long count);

void outsw(unsigned port, void *addr, unsigned long count);

读或写 16-位 值到一个单个 16-位 端口.

void insl(unsigned port, void *addr, unsigned long count);

void outsl(unsigned port, void *addr, unsigned long count);

读或写 32-位 值到一个单个 32-位 端口.

有件事要记住, 当使用字串函数时: 它们移动一个整齐的字节流到或自端口. 当端口和主系统有不同的字节对齐规则, 结果可能是令人惊讶的. 使用 inw 读取一个端口交换这些字节, 如果需要, 来使读取的值匹配主机字节序. 字串函数, 相反, 不进行这个交换.

 

暂停式I/O

一些平台 - 最有名的 i386 - 可能有问题当处理器试图太快传送数据到或自总线. 当处理器对于外设总线被过度锁定时可能引起问题( 想一下 ISA )并且可能当设备单板太慢时表现出来. 解决方法是插入一个小的延时在每个 I/O 指令后面, 如果跟随着另一个指令. x86 , 这个暂停是通过进行一个 outb 指令到端口 0x80 ( 正常地不是常常用到 )实现的, 或者通过忙等待. 细节见你的平台的 asm 子目录的 io.h 文件.

 

平台依赖性

IA-32 (x86)

x86_64

这个体系支持所有的本章描述的函数. 端口号是 unsigned short 类型.

IA-64 (Itanium)

支持所有函数; 端口是 unsigned long(以及内存映射的)). 字串函数用 C 实现.

Alpha

支持所有函数, 并且端口是内存映射的. 端口 I/O 的实现在不同 Alpha 平台上是不同的, 根据它们使用的芯片组. 字串函数用 C 实现并且定义在 arch/alpha/lib/io.c 中定义. 端口是 unsigned long.

ARM

端口是内存映射的, 并且支持所有函数; 字串函数用 C 实现. 端口是 unsigned int 类型.

Cris

这个体系不支持 I/O 端口抽象, 甚至在一个模拟模式; 各种端口操作定义成什么不做.

M68k

M68k

端口是内存映射的. 支持字串函数, 并且端口类型是 unsigned char.

MIPS

MIPS64

MIPS 端口支持所有的函数. 字串操作使用紧凑汇编循环来实现, 因为处理器缺乏机器级别的字串 I/O. 端口是内存映射的; 它们是 unsigned long.

PA

支持所有函数; 端口是 int 在基于 PCI 的系统上以及 unsigned short EISA 系统, 除了字串操作, 它们使用 unsigned long 端口号.

PowerPC

PowerPC64

支持所有函数; 端口有 unsigned char * 类型在 32-位 系统上并且 unsigned long 64-位 系统上.

S390

类似于 M68k, 这个平台的头文件只支持字节宽的端口 I/O, 而没有字串操作. 端口是 char 指针并且是内存映射的.

Super

端口是 unsigned int ( 内存映射的 ), 并且支持所有函数.

SPARC SPARC64

再一次, I/O 空间是内存映射的. 端口函数的版本定义来使用 unsigned long 端口.

好奇的读者能够从 io.h 文件中获得更多信息, 这个文件有时定义几个结构特定的函数, 加上我们在本章中描述的那些. 但是, 警告有些这些文件是相当难读的.

使用I/O内存

I/O 端口在 x86 世界中流行, 用来和设备通讯的主要机制是通过内存映射的寄存器和设备内存. 2 者都称为 I/O 内存, 因为寄存器和内存之间的区别对软件是透明的.

I/O内存访问的方法和计算机体系架构有关,不过原理相同,I/O 内存可以或者不可以通过页表来存取. 当通过页表存取, 内核必须首先安排从你的驱动可见的物理地址, 并且这常常意味着你必须调用 ioremap 在做任何 I/O 之前. 如果不需要页表, I/O 内存位置看来很象 I/O 端口, 并且你只可以使用正确的包装函数读和写它们.不管如何都不鼓励使用I/O内存指针。

I/O内存的分配和映射

I/O 内存区必须在使用前分配. 分配内存区的接口是( <linux/ioport.h> 定义):

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

这个函数分配一个 len 字节的内存区, start 开始. 如果一切顺利, 一个非NULL 指针返回; 否则返回值是 NULL. 所有的 I/O 内存分配来 /proc/iomem 中列出.

内存区在不再需要时应当释放:

void release_mem_region(unsigned long start, unsigned long len); 

还有一个旧的检查 I/O 内存区可用性的函数:

int check_mem_region(unsigned long start, unsigned long len); //尽量少用,如果需要分配用第一个较好

 

I/O 内存读, 使用下列之一:

unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);

这里, addr 应当是从 ioremap 获得的地址(也许与一个整型偏移); 返回值是从给定 I/O 内存读取的.

有类似的一系列函数来写 I/O 内存:

void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);

如果你必须读和写一系列值到一个给定的 I/O 内存地址, 你可以使用这些函数的重复版本:

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 开始.

上面描述的函数进行所有的 I/O 到给定的 addr. 如果, 相反, 你需要操作一块 I/O 地址, 你可使用下列之一:

void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);

如果你通览内核源码, 你可看到许多调用旧的一套函数, 当使用 I/O 内存时. 这些函数仍然可以工作, 但是它们在新代码中的使用不鼓励. 除了别的外, 它们较少安全因为它们不进行同样的类型检查. 但是, 我们在这里描述它们:

unsigned readb(address);
unsigned readw(address);
unsigned readl(address); 

这些宏定义用来从 I/O 内存获取 8-, 16-, 32-位数据值.

void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address); 

如同前面的函数, 这些函数()用来写 8-, 16-, 32-位数据项.

一些 64-位平台也提供 readq writeq, PCI 总线上的 4-(8-字节)内存操作. 这个 4-字 的命名是一个从所有的真实处理器有 16-位 字的时候的历史遗留. 实际上, 用作 32-位 值的 L 命名也已变得不正确, 但是命名任何东西可能使事情更混淆.

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值