Linux设备驱动程序学习(八)——内存与I/O访问

内存空间和I/O空间

  在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的,它通过特定的指令in、out来访问。端口号标识了外设的寄存器地址。Intel语法中的in、out指令格式如下:

IN 累加器, {端口号│DX}
OUT {端口号│DX},累加器

  目前,大多数嵌入式微控制器(如ARM、PowerPC等)中并不提供I/O空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序及在程序运行中使用的变量和其他数据都存在于内存空间中。内存地址可以直接由C语言指针操作。
  即便是在X86处理器中,虽然提供了I/O空间,如果由我们自己设计电路板,外设仍然可以只挂接在
内存空间中。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的I/O指令。
  因此,内存空间是必需的,而I/O空间是可选的,如图:
在这里插入图片描述

内存分配

  在前面我们就接触到了内存的分配,使用过kmalloc和kfree来分配和释放内存,但Linux内核实际上提供了更加丰富的内存分配原语集。

kmalloc函数介绍

  kmalloc内存分配引擎是一个功能强大的工具,除非被阻塞,否则这个函数可以运行的很快,而且不对所获取的内存空间清零。
  kmalloc函数:

#include<linux/slab.h>
void *kmalloc(size_t size,int flags);
/*
size:要分配块大小
flags:分配标志,可以有多种方式控制kmalloc行为*/
Flags(分配标志):
GFP_KERNEL :内核内存通常的分配方法,可能引起休眠。
GFP_ATOMIC :用于在中断处理例程或其它运行于进程上下文之外的代码中分配内存,不会休眠。
GFP_USER:用于为用户空间分配内存,可能会引起休眠。
GFP_HIGHUSER:类似于GFP_USER,不过如果有高端内存的话就从那里分配。。
GFP_NOIO:在GFP_KERNEL的基础上,禁止任何I/O的初始化。
GFG_NOFS:在GFP_KERNEL的基础上,不允许执行任何文件系统的调用。
*/

另外有一些分配标志与上述“或”起来使用:

__GFP_DMA: 这个标志要求分配在能够 DMA 的内存区. 
__GFP_HIGHMEM:这个标志指示分配的内存可以位于高端内存. 
__GFP_COLD:   对于DMA读取的页面分配
__GFP_NOWARN:这个标志很少使用,可以避免内核无法满足内存分配时产生警告
__GFP_HIGH:   这个标志标识了一个高优先级请求, 它被允许来消耗甚至被内核保留给紧急状况的最后的内存页. 
__GFP_REPEAT: 重新尝试分配,仍有可能失败
__GFP_NOFAIL: 始终不返回失败,努力满足分配需求,不推荐使用
__GFP_NORETRY:如果请求的内存不可获得就立即返回
/*这三个标志告诉分配器在满足分配需求遇到困难时该采取何种行为*/

  Linux内核把内存分为三个区段:可用于DMA的内存,常规内存以及高端内存

  • 通常内存分配都是发生在常规内存区,但是通过设置也能获得其它两类的内存分配。
  • 可用于DMA的内存指的是存在于特别地址范围内的内存,外设可以使用这些内存执行DMA访问。
  • 高端内存是32位平台为了访问大量的内存而存在的一种机制,如果不首先完成一些特殊的映射,我们就无法从内核中直接访问这些内存。

  Size参数
  linux处理内存分配:创建一系列的内存对象池,每个池中的内存块大小是固定一致的。处理分配请求时,就直接在包含有足够大的内存块的池中传递一个整块给请求者。
  内核只能分配一些预定义的,固定大小的字节数组。若申请任意数量的内存,则得到的可能会多一些,最多可以得到申请数量的两倍。
  kmalloc 能够处理的最小内存块是 32 或者 64 字节, 具体是哪个取决于当前体系结构使用的页面大小。
  注意:对于kmalloc能够分配的内存块大小存在一个上限,这个取决于当前体系结构的不同而不同。对于kmalloc不应该分配大于128kb的内存,对于更大的内存获取,应采取其他方法。

后备高速缓存

  驱动程序常常需要反复分配许多相同大小内存块的情况,增加了一些特殊的内存池,称为后备高速缓存(lookaside cache)。
  Linux 内核的高速缓存管理器有时称为“slab 分配器”,相关函数和类型在 <linux/slab.h> 中声明。slab 分配器实现的高速缓存具有 kmem_cache_t 类型。
  具体实现过程:

  1. 创建高速缓存:
kmem_cache_t *kmem_cache_create(const char *name, size_t size,size_t offset,
           unsigned long flags,
              void (*constructor)(void *, kmem_cache_t *,unsigned long flags),
              void (*destructor)(void *, kmem_cache_t *, unsigned long flags));
/*创建一个可以容纳任意数目内存区域的、大小都相同的高速缓存对象,这些区域的大小都相同,由size参数决定。*/
参数*name: 一个指向 name 的指针,name和这个后备高速缓存相关联,功能是管理信息以便追踪问题;通常设置为被缓存的结构类型的名字,不能包含空格。
参数size:每个内存区域的大小。
参数offset:页内第一个对象的偏移量;用来确保被分配对象的特殊对齐,0 表示缺省值。
参数flags:控制分配方式的位掩码:
SLAB_NO_REAP        保护缓存在系统查找内存时不被削减,不推荐。
SLAB_HWCACHE_ALIGN  所有数据对象跟高速缓存行对齐,平台依赖,可能浪费内存。
SLAB_CACHE_DMA      每个数据对象在 DMA 内存区段分配。
  1. 一旦某个对象的高速缓存被创建,就可以调用kmem_cache_alloc从中分配内存对象:
    viod *kmem_cache_alloc(kmem_cache_t *cache,int flags);
    cache是之前创建的高速缓存,flags和传递给kmalloc的相同,并且当需要分配更多的内存来满足kmem_cache_alloc时,高速缓存还会利用这个参数

  2. 释放一个内存对象,使用kmem_cache_free:
    void kmem_cache_free(kmem_cache_t *cache,const void *obj);

  3. 如果驱动程序代码中和高速缓存有关的部分已经处理完了(典型情况:模块被卸载的时候),这时驱动程序应该释放它的高速缓存:
    int kmem_cache_destroy(kmem_cache_t *cache); /*只在从这个缓存中分配的所有的对象都已返时才成功。因此,应检查 kmem_cache_destroy 的返回值:失败指示模块存在内存泄漏*/

内存池

  为了确保在内存分配不允许失败情况下成功分配内存,内核提供了称为内存池( “mempool” )的抽象,它其实是某种形式的后备高速缓存。它为了紧急情况下的使用,尽力一直保持空闲内存。所以使用时必须注意: mempool 会分配一些内存块,使其空闲而不真正使用,所以容易消耗大量内存 。因此不要使用 mempool 处理可能失败的分配。应避免在驱动代码中使用 mempool。
  内存池的类型为 mempool_t ,在 <linux/mempool.h> ,使用方法如下:
  创建mempool

mempool_t *mempool_create(int min_nr,
                         mempool_alloc_t *alloc_fn,
                         mempool_free_t *free_fn,
                         void *pool_data);
/*min_nr 参数是内存池应当一直保留的最小数量的分配对象*/
/*实际的分配和释放对象由 alloc_fn 和 free_fn 处理,原型:*/
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
/* mempool_create 最后的参数 *pool_data 被传递给 alloc_fn 和 free_fn */

  可以编写特殊用途的函数来处理 mempool 的内存分配,但通常只需使用 slab 分配器来处理这个任务。mempool_alloc_slab 和 mempool_free_slab的原型和上述内存池分配原型匹配,并使用kmem_cache_alloc 和 kmem_cache_free 处理内存的分配和释放。
  典型的设置内存池的代码如下:

cache = kmem_cache_create(. . .); 
pool = mempool_create(MY_POOL_MINIMUM,mempool_alloc_slab, mempool_free_slab, cache);

  创建内存池后,分配和释放对象:

void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);

  在创建mempool时,分配函数将被调用多次来创建预先分配的对象。因此,对 mempool_alloc 的调用是试图用分配函数请求额外的对象,如果失败,则返回预先分配的对象(如果存在)。用 mempool_free 释放对象时,若预分配的对象数目小于最小量,就将它保留在池中,否则将它返回给系统。
  可用一下函数重定义mempool预分配对象的数量:

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);
/*若成功,内存池至少有 new_min_nr 个对象*/
若不再需要内存池,则返回给系统:
void mempool_destroy(mempool_t *pool); 
/*在销毁 mempool 之前,必须返回所有分配的对象,否则会产生 oops*/

get_free_page和相关函数

  如果模块需要分配大块的内存的话,使用面向页的分配技术会更好,分配页面可以使用以下函数:

__get_free_page(unsigned int flags); 
/*返回一个指向新页的指针, 未清零该页*/
get_zeroed_page(unsigned int flags); 
/*类似于__get_free_page,但用零填充该页*/
__get_free_pages(unsigned int flags, unsigned int order); 
/*分配是若干(物理连续的)页面并返回指向该内存区域的第一个字节的指针,该内存区域未清零*/
/*参数flags 与 kmalloc 的用法相同;
  参数order 是请求或释放的页数以 2 为底的对数。就是order为0时时一个页面,为1时2个页面,为2时4个页面,若其值过大(没有这么大的连续区可用), 则分配失败,可允许的最大order值一般时10或11*/

  get_order 函数可以用来从一个整数参数 size(必须是 2 的幂) 中提取 order。

  当程序不需要页面时,它可用下列函数之一来释放它们:

void free_page(unsigned long addr); //这是一个宏,展开后就是对于下面的函数的调用。
void free_pages(unsigned long addr, unsigned long order);

  若试图释放和你分配的数目不等的页面,会破坏内存映射关系,系统会出错。

struct page接口

  struct page 是一个描述一个内存页的内部内核结构,定义在<linux/Mm_types.h>
  Linux 页分配器的核心是称为 alloc_pages_node 的函数:

struct page *alloc_pages_node(int nid, unsigned int flags,
 unsigned int order);
/*以下是这个函数的 2 个变体(是简单的宏):*/
struct page *alloc_pages(unsigned int flags, unsigned int order);
struct page *alloc_page(unsigned int flags);
  • 参数nid 是要分配内存的 NUMA 节点 ID,
  • 参数flags 是 GFP_ 分配标志,
  • 参数order 是分配内存的大小.
  • 返回值是一个指向第一个(可能返回多个页)page结构的指针, 失败时返回NULL。

  alloc_pages 通过在当前 NUMA 节点分配内存( 它使用 numa_node_id 的返回值作为 nid 参数调用 alloc_pages_node)简化了alloc_pages_node调用。alloc_pages 省略了 order 参数而只分配单个页面。
  释放分配的页:

void __free_page(struct page *page);
void __free_pages(struct page *page, unsigned int order);
void free_hot_page(struct page *page);
void free_cold_page(struct page *page);
/*若知道某个页中的内容是否驻留在处理器高速缓存中,可以使用 free_hot_page (对于驻留在缓存中的页) 或 free_cold_page(对于没有驻留在缓存中的页) 通知内核,帮助分配器优化内存使用*/

vmalloc及其辅助函数

  vmalloc 是一个基本的 Linux 内存分配机制,它在虚拟内存空间分配一块连续的内存区,尽管这些页在物理内存中不连续 (使用一个单独的 alloc_page 调用来获得每个页),但内核认为它们地址是连续的。
  注意:vmalloc 在大部分情况下不推荐使用。因为在某些体系上留给 vmalloc 的地址空间相对小,且效率不高。函数原型如下:

#include <linux/vmalloc.h> 
void *vmalloc(unsigned long size);
void vfree(void * addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void * addr);

  kmalloc 和 _get_free_pages 返回的内存地址也是虚拟地址,其实际值仍需 MMU 处理才能转为物理地址。vmalloc和它们在使用硬件上没有不同,不同是在内核如何执行分配任务上:kmalloc 和 __get_free_pages 使用的(虚拟)地址范围和物理内存是一对一映射的, 可能会偏移一个常量 PAGE_OFFSET 值,无需修改页表。
  vmalloc 和 ioremap 使用的地址范围完全是虚拟的,且每次分配都要通过适当地设置页表来建立(虚拟)内存区域。 vmalloc 可获得的地址在从 VMALLOC_START 到 VAMLLOC_END 的范围中,定义在 <asm/patable.h> 中。vmalloc 分配的地址只在处理器的 MMU 之上才有意义。当驱动需要真正的物理地址时,就不能使用 vmalloc。 调用 vmalloc 的正确场合是分配一个大的、只存在于软件中的、用于缓存的内存区域时。
  注意:vamlloc 比 __get_free_pages 要更多开销,因为它必须即获取内存又建立页表。因此, 调用 vmalloc 来分配仅仅一页是不值得的。vmalloc 的一个小的缺点在于它无法在原子上下文中使用。因为它内部使用 kmalloc(GFP_KERNEL) 来获取页表的存储空间,因此可能休眠。
  ioremap 也要建立新页表,但它实际上不分配任何内存,其返回值是一个特殊的虚拟地址可用来访问特定的物理地址区域。
  为了保持可移植性,不应当像访问内存指针一样直接访问由 ioremap 返回的地址,而应当始终使用 readb 和 其他 I/O 函数。

  • ioremap 和 vmalloc 是面向页的(它们会修改页表),重定位的或分配的空间都会被上调到最近的页边界。
  • ioremap 通过将重映射的地址下调到页边界,并返回第一个重映射页内的偏移量来模拟一个非对齐的映射。

per_CPU变量

  per-CPU 变量是一个有趣的 2.6 内核特性,定义在 <linux/percpu.h> 中。当创建一个per-CPU变量,系统中每个处理器都会获得该变量的副本。其优点是对per-CPU变量的访问(几乎)不需要加锁,因为每个处理器都使用自己的副本。per-CPU 变量也可存在于它们各自的处理器缓存中,这就在频繁更新时带来了更好性能。
  在编译时间创建一个per-CPU变量使用如下宏定义:

DEFINE_PER_CPU(type, name);
/*若变量( name)是一个数组,则必须包含类型的维数信息,例如一个有 3 个整数的per-CPU 数组创建如下: */
DEFINE_PER_CPU(int[3], my_percpu_array);

  虽然操作per-CPU变量几乎不必使用锁定机制。 但是必须记住 2.6 内核是可抢占的,所以在修改一个per-CPU变量的临界区中间时可能被抢占。并且还要避免进程在对一个per-CPU变量访问时被移动到另一个处理器上运行。所以必须显式使用 get_cpu_var 宏来访问当前处理器的变量副本, 并在结束后调用 put_cpu_var。   对 get_cpu_var 的调用返回一个当前处理器变量版本的 lvalue ,并且禁止抢占。又因为返回的是lvalue,所以可被直接赋值或操作。例如:

get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);

  当要访问另一个处理器的变量副本时, 使用:
  per_cpu(variable, int cpu_id);

  当代码涉及到多处理器的per-CPU变量,就必须实现一个加锁机制来保证访问安全。
  动态分配per-CPU变量方法如下:

void *alloc_percpu(type);
void *__alloc_percpu(size_t size, size_t align);/*需要一个特定对齐的情况下调用*/
void free_percpu(void *per_cpu_var);      /* 将per-CPU 变量返回给系统*/
/*访问动态分配的per-CPU变量通过 per_cpu_ptr 来完成,这个宏返回一个指向给定 cpu_id 版本的per_cpu_var变量的指针。若操作当前处理器版本的per-CPU变量,必须保证不能被切换出那个处理器:*/
per_cpu_ptr(void *per_cpu_var, int cpu_id);

  通常使用 get_cpu 来阻止在使用per-CPU变量时被抢占,典型代码如下:

int cpu; 
cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var, cpu);
put_cpu();
/*当使用编译时的per-CPU 变量, get_cpu_var 和 put_cpu_var 宏将处理这些细节。动态per-CPU变量需要更明确的保护*/
per-CPU变量可以导出给模块, 但必须使用一个特殊的宏版本:
EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);
/*要在模块中访问这样一个变量,声明如下:*/
DECLARE_PER_CPU(type, name);

  注意:在某些体系架构上,per-CPU变量的使用是受地址空间有限的。若在代码中创建per-CPU变量, 应当尽量保持变量较小。

获得大的缓冲区

  大量连续内存缓冲的分配是容易失败的。到目前止执行大 I/O 操作的最好方法是通过离散/聚集操作 。

在引导时获得专用缓冲区

  若真的需要大块连续的内存作缓冲区,最好的方法是在引导时来请求内存来分配。在引导时分配是获得大量连续内存页(避开 __get_free_pages 对缓冲大小和固定颗粒双重限制)的唯一方法。一个模块无法在引导时分配内存,只有直接连接到内核的驱动才可以。 而且这对普通用户不是一个灵活的选择,因为这个机制只对连接到内核映象中的代码才可用。要安装或替换使用这种分配方法的设备驱动,只能通过重新编译内核并且重启计算机。

  当内核被引导, 它可以访问系统种所有可用物理内存,接着通过调用子系统的初始化函数, 允许初始化代码通过减少留给常规系统操作使用的 RAM 数量来分配私有内存缓冲给自己。
  在引导时获得专用缓冲区要通过调用下面函数进行:

#include <linux/bootmem.h>
/*分配不在页面边界上对齐的内存区*/
void *alloc_bootmem(unsigned long size); 
void *alloc_bootmem_low(unsigned long size); /*分配非高端内存。希望分配到用于DMA操作的内存可能需要,因为高端内存不总是支持DMA*/
/*分配整个页*/
void *alloc_bootmem_pages(unsigned long size); 
void *alloc_bootmem_low_pages(unsigned long size);/*分配非高端内存*/
/*很少在启动时释放分配的内存,但肯定不能在之后取回它。注意:以这个方式释放的部分页不返回给系统*/
void free_bootmem(unsigned long addr, unsigned long size);

I/O端口和I/O内存

  设备驱动程序是软件概念和硬件电路之间的一个抽象层,因此,两方面都要理解,前面介绍的主要是软件方面的内容,后面的部分的主要是讲解驱动程序在Linux平台下如何保持可移植性的前提下访问I/O内存和I/O端口。

I/O端口与I/O内存

  每种外设都是通过读写寄存器来进行控制,大部分外设都有几个寄存器,不管是在内存地址空间还是I/O地址空间,这些寄存器的访问地址都是连续的。
  在硬件层,内存区域和I/O区域没有概念上的区别:他们都通过像地址总线和控制总线发送电平信号(比如读和写信号)进行访问,在通过数据总线读写数据。

  • I/O端口:当一个寄存器或内存位于I/O空间时,称其为I/O端口。
  • I/O内存:当一个寄存器或内存位于内存空间时,称其为I/O内存。
I/O寄存器和常规内存

   I/O寄存器和内存非常相似,但是还是有一些区别的:

  • I/O寄存器与RAM的最主要区别就是I/O操作具有边际效应:读取某个地址时可能导致这个地址的内容发生变化,比如很多中断寄存器的值一经读取,便自动清零。
  • 编译器可以将数据缓存在CPU寄存器而不用写入内存,即使存储数据,读写操作也都能在高速缓存中进行而不用访问物理内存。
  • 常规内存可以优化,但是对寄存器进行优化可能导致致命错误。驱动程序一定要保证不适用那个高速缓存,并且,在访问寄存器时不发生读或写指令的重新排序。
  • 由硬件自身缓存引起的问题很好解决:只要在把底层硬件配置成(自动配置或是linux初始化代码完成)在访问I/O区域(不管是内存还是端口)时禁止硬件缓存即可。
    +由编译器优化和硬件重新排序引起的问题的解决办法:对硬件或其他处理器必须以特定顺序执行的操作之间设置内存屏障。linux有四个宏解决所有可能的排序问题:
#include<linux/kernel.h>
void barrier(void);
/*这个函数通知编译器插入一个内存屏障,对硬件无影响,对barrier的调用可防止在屏障前后的编译器优化,但硬件能完成自己的重新排序。*/
#include<asm/system.h>
void rmb(void);      /*保证任何出现于屏障前的读在执行任何后续的读之前完成*/
void wmb(void);     /*保证任何出现于屏障前的写在执行任何后续的写之前完成*/
void mb(void);      /*保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成*/
void read_barrier_depends(void);     /*一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小, 且不在所有体系中存在。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/
实例:
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);

  注意:内存屏障影响性能,所以应当只在确实需要它们的地方使用。不同的类型对性能的影响也不同,因此要尽可能地使用需要的特定类型。值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t,也可作为内存屏障使用。

I/O端口的使用

内核空间定义I/O端口

  I/O端口是驱动程序于许多设备之间的通信方式,因此定义一个I/O端口的步骤主要为:

  1. 端口分配
    在尚未取得对一个I/O端口的独占访问时,我们不能对这些端口操作。因此内核给我们提供了一个注册用的接口,核心函数是request_region:
#include<linux/ioport.h> 
struct resource *request_region(unsigned long first,unsigned long n,const char *name);
/*该函数通知内核我们要使用起始于first的n个端口。
name应该是设备的名称。若申请成功,返回非NULL,失败,返回NULL*/

  注意:所有的端口分配可从/proc/ioports中得到,如果我们无法分配到需要的端口集合,则可以从这个文件中得知哪个驱动程序已经分配了这个端口。

  1. 端口操作:
    当驱动程序请求到了需要使用的I/O端口范围时,必须读取或写入这些端口,因此,大多数硬件都会把8位、16位、32位的端口区分开来,它们不能像访问系统内存那样混淆使用。
    在内核头文件<asm/io.h>中,定义了这样一些内联函数:
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位(字宽度)端口*/
unsigned inl(unsigned port); 
void outl(unsigned longword, unsigned port); 
/*访问 32位 端口。 longword 声明有的平台为 unsigned long ,有的为 unsigned int。*/

  注意:没有定义64位的I/O操作,即使在64位的体系架构上,端口地址空间也只是用最大32位的数据通路。

  1. 释放端口:
void release_region(unsigned long start,unsigned long n);
/*当不再使用I/O端口,或者卸载模块时应使用该函数将这些端口返回给系统。*/
int check_region(unsigned long first,unsigned long n);
/*该函数用来检测给定的I/O端口是否可用。(不建议使用,因为它的返回值不能确保分配是否能够成功)*/
用户空间访问I/O端口

  如果要在用户空间中使用inb等相关函数,也是可行的,因为在GNU的C库的<syslio.h>中定义了这些函数,不过必须满足以下条件:

  • 程序必须使用 -O 选项编译来强制扩展内联函数
  • 必须用ioperm 和 iopl 系统调用(#include <sys/perm.h>) 来获得对端口 I/O 操作的权限。ioperm 为获取单独端口操作权限,而 iopl 为整个 I/O 空间的操作权限。 (这两个函数都是x86 特有的)
  • 程序以 root 来调用 ioperm 和 iopl,或是其父进程必须以 root 获得端口操作权限
    注意:若平台没有 ioperm 和 iopl 系统调用,用户空间可以仍然通过使用 /dev/prot 设备文件访问 I/O 端口。但是这个文件的定义是体系相关的,并且I/O 端口必须先被注册。
串操作

  除了一次传递一个数据的I/O操作,linux还提供了一次传递一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作指令。它们完成任务比一个 C 语言循环更快。下列宏定义实现了串I/O,它们有的通过单个机器指令实现;但如果目标处理器没有进行串 I/O 的指令,则通过执行一个紧凑的循环实现。 有的体系的原型如下:

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位数据,即使在64位的体系结构上,端口地址也只使用最低32位的数据通路*/
暂停式I/O

  为了匹配低速外设的速度,有时若 I/O 指令后面还紧跟着另一个类似的I/O指令,就必须在 I/O 指令后面插入一个小延时。在这种情况下,可以使用暂停式的I/O函数代替通常的I/O函数,它们的名字以 _p 结尾,如 inb_p、outb_p等等。
  这些函数定义被大部分体系支持,尽管它们常常被扩展为与非暂停式I/O 同样的代码。因为如果体系使用一个合理的现代外设总线,就没有必要额外暂停。细节可参考平台的 asm 子目录的 io.h 文件。由于ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。

平台相关性

  由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。

I/O内存的使用

  除了 x86上普遍使用的I/O 端口外,和设备通讯另一种主要机制是通过使用映射到内存的寄存器或设备内存,统称为 I/O 内存。因为寄存器和内存之间的区别对软件来说透明的。
  I/O 内存仅仅是类似 RAM 的一个区域,处理器通过总线访问这个区域,以实现设备的访问。
  根据平台和总线的不同,I/O 内存可以就是否通过页表访问分类。若通过页表访问,内核必须首先安排物理地址使其对设备驱动程序可见,在进行任何 I/O 之前必须调用 ioremap。若不通过页表,I/O 内存区域就类似I/O 端口,可以使用适当形式的函数访问它们。因为“side effect”的影响,
  不管是否需要 ioremap ,都不鼓励直接使用 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);
/* 从 start 开始,分配一个 len 字节的内存区域。成功返回一个非NULL指针,否则返回NULL。所有的 I/O 内存分配情况都 /proc/iomem 中列出。*/
/*I/O内存区域在不再需要时应当释放*/
void release_mem_region(unsigned long start, unsigned long len); 

/*一个旧的检查 I/O 内存区可用性的函数,和check_region一样不推荐使用*/
int check_mem_region(unsigned long start, unsigned long len);

  分配I/O内存后并不意味着可以引用对应的指针,还需确保该I/O内存对于内核而言是可以访问的,因此不许先建立映射,它调用ioremap函数:

#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);  /*如果控制寄存器也在该区域,应使用的非缓存版本,以实现side effect。*/
void iounmap(void * addr);
访问I/O 内存

  访问I/O 内存的正确方式是通过一系列专用于此目的的函数(在 <asm/io.h> 中定义的):

/*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 内存地址,从给定的 buf 读或写 count 个值到给定的 addr */
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);

/*需要操作一块 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);

/*旧函数接口,仍可工作, 但不推荐。*/
unsigned readb(address);
unsigned readw(address);
unsigned readl(address); 
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
像 I/O 内存一样使用端口

  一些硬件有一个有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 内存。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:

void *ioport_map(unsigned long port, unsigned int count);      
/*重映射 count 个I/O 端口,使其看起来像 I/O 内存。
此后,驱动程序可以在返回的地址上使用 ioread8 和同类函数。其在编程时消除了I/O 端口和I/O 内存的区别。*/

/*这个映射应当在它不再被使用时撤销:*/
void ioport_unmap(void *addr); 

注意:I/O 端口仍然必须在重映射前使用 request_region 分配I/O 端口。ARM9不支持这两个函数!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值