Linux 设备驱动程序(一)((下)

简介: Linux 设备驱动程序(一)

Linux 设备驱动程序(一)(中):https://developer.aliyun.com/article/1597396

八、分配内存

1、 kmalloc函数的内幕

可参考 ==> 4、kmalloc 函数

(1)flags 参数

#include <linux/slab.h>
void *kmalloc(size_t size, int flags)

 kmalloc 的第一个参数是要分配的块的大小,第二个参数是分配标志(flags),更有意思的是、它能够以多种方式控制 kmalloc 的行为。


 最常用的标志是 GFP_KERNEL,它表示内存分配(最终总是调用 get_free_pages 来实现实际的分配,这就是 GFP_ 前级的由来)是代表运行在内核空间的进程执行的。换句话说,这意味着调用它的函数正代表某个进程执行系统调用。使用 GFP_KERNEL 允许 kmalloc 在空闲内存较少时把当前进程转入休眠以等待一个页面。因此,使用 GFP_KERNEL 分配内存的函数必须是可重入的。在当前进程休眠时,内核会采取适当的行动,或者是把缓冲区的内容刷写到硬盘上,或者是从一个用户进程换出内存,以获取一个内存页面。


 GFP_KERNEL 分配标志并不是始终适用,有时 kmalloc 是在进程上下文之外被调用的,例如在中断处理例程、tasklet 以及内核定时器中调用。这种情况下 current 进程就不应该休眠,驱动程序则应该换用 GFP_ATOMIC 标志。内核通常会为原子性的分配预留一些空闲页面。使用 GFP_ATOMIC 标志时,kmalloc 甚至可以用掉最后一个空闲页面。不过如果连最后一页都没有了,分配就返回失败。


 除了GFP_KERNEL 和 GFP_ATOMIC 外,还有一些其他的标志可用于替换或补充这两个标志,不过这两个标志已经可以满足大多数驱动程序的需要了。所有的标志都定义在 <linux/gfp.h> 中,有个别的标志使用两个下划线作为前级,比如 __GFP_DMA。另外,还有一些符号表示这些标志的常用组合,它们没有这种前级,并且有时称为 “分配优先级”。

2、 后备高速缓存

 设备驱动程序常常会反复地分配很多同一大小的内存块。既然内核已经维护了一组拥有同一大小内存块的内存池,那么为什么不为这些反复使用的块增加某些特殊的内存池呢? 实际上,内核的确实现了这种形式的内存池,通常称为后备高速缓存(lookaside cache)。设备驱动程序通常不会涉及这种使用后备高速缓存的内存行为,但也有例外, Linux 2.6 中的 USB 和 SCSI 驱动程序就使用了这种高速缓存。


 Linux 内核的高速缓存管理有时称为 “slab 分配器” 。因此,相关函数和类型在 <linux/slab.h> 中声明。slab 分配器实现的高速缓存具有 kmem_cache_t 类型,可通过调用

kmem_cache_create 创建:

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 与这个高速缓存相关联,其功能是保管一些信息以便追踪问题,它通常被设置为将要高速缓存的结构类型的名字。高速缓存保留指向该名称的指针,而不是复制其内容,因此,驱动程序应该将指向静态存储(通常可取直接字符串)的指针传递给这个函数。名称中不能包含空白。


 offset 参数是页面中第一个对象的偏移量,它可以用来确保对已分配的对象进行某种特殊的对齐,但是最常用的就是 0,表示使用默认值。flags 控制如何完成分配,是一个位掩码,可取的值如下:


SLAB_NO_REAP

设置这个标志可以保护高速缓存在系统寻找内存的时候不会被减少。设置该标志通常不是好主意,因为我们不应该对内存分配器的自由做一些人为的、不必要的限制。

SLAB_HWCACHE_ALIGN

这个标志要求所有数据对象跟高速缓存行(cache line)对齐;实际的操作则依赖于主机平台的硬件高速缓存布局。如果在 SMP 机器上,高速缓存中包含有频繁访问的数据项的话,则该选项将是非常好的选择。但是,为了满足高速缓存行的对齐需求,必要的填白可能浪费大量内存。

SLAB_CACHE_DMA

这个标志要求每个数据对象都从可用于 DMA 的内存区段中分配。

 还有一些标志可用于高速缓存分配的调试,详情请见 mm/slab.c 文件。但通常这些标志只在开发系统中通过内核配置选项而全局地设置。


 constructor 和 destructor 参数是可选的函数(但是不能只有 destructor 而没有 constructor);前者用于初始化新分配的对象,而后者用于 “清除” 对象 —— 在内存空间被整个释放给系统之前。


 constructor 和 destructor 很有用,不过使用时有一些限制。constructor 函数是在分配用于一组对象的内存时调用的。因为这些内存中可能会包含好几个对象,所以 constructor 函数可能会被多次调用。我们不能认为分配一个对象后随之就会调用一次 constructor。类似地,destructor 函数也有可能不是在一个对象释放后就立即被调用,而是在将来的某个未知的时间才被调用。constructor 和 destructor 可能允许也可能不允许休眠,这要看是否向它们传递了 SLAB_CTOR_ATOMIC 标志(CTOR 是 constructor 的简写)。


 为了简便起见,程序员可以使用同一个函数同时作为 constructor 和 destructor 使用;当调用的是一个 constructor 函数的时候,slab 分配器总是传递 SLAB_CTOR_CONSTRUCTOR 标志。


 一旦某个对象的高速缓存被创建,就可以调用 kmem_cache_alloc 从中分配内存对象:

void *kmem_cache_alloc(kmem_cache_t *cache, int flags);

 这里、参数 cache 是先前创建的高速缓存;参数 flags 和传递给 kmalloc 的相同,并且当需要分配更多内存来满足 kmem_cache_alloc 时,高速缓存还会利用这个参数。

  释放一个内存对象时使用 kmem_cache_free

void kmem_cache_free(kmem_cache_t *cache, const void *obj);

  如果驱动程序代码中和高速缓存有关的部分已经处理完了(一个典型情况是模块被卸载的时候),这时驱动程序应该释放它的高速缓存,如下所示:

int kmem_cache_destroy(kmem_cache_t *cache);

  这个释放操作只有在已将从缓存中分配的所有对象都归还后才能成功。所以,模块应该检查 kmem_cache_destroy 的返回状态;如果失败、则表明模块中发生了内存泄漏(因为有一些对象被漏掉了)。

  使用后备式缓存带来的另一个好处是内核可以统计高速缓存的使用情况。高速缓存的使用统计情况可以从 /proc/slabinfo 获得。

(1)基于 slab 高速缓存的 scull:scullc

现在该举个例子了。scullc 是 scull 模块的一个缩减版本,只实现了裸设备 —— 即持久的内存区。与 scull 使用 kmalloc 不同的是,scullc 使用内存高速缓存。数据对象的大小可以在编译或加载时修改,但不能在运行时修改 —— 那样需要创建一个新的内存高速缓存,而这里不必处理那些不需要的细节问题。


 scullc 是一个完整的例子,可以用于测试 slab 分配器。它和 scull 只有几行代码的不同。首先,我们必须声明自己的 slab 高速缓存:

/* 声明一个高速缓存指针,它将用于所有设备 */
kmem_cache_t *scullc_cache;

slab 高速缓存的创建代码如下所示(在模块装载阶段):

/* scullc_init: 为我们的量子创建一个高速缓存 */
scullc_cache = kmem_cache_create("scullc", scullc_quantum, 0, SLAB_HWCACHE_ALIGN, 
                NULL, NULL); /* 没有 ctor/dtor */
if (!scullc_cache) {
  scullc_cleanup();
  return -ENOMEM;
} 

下面是分配内存量子的代码:

/* 使用内存高速缓存来分配一个量子 */
if (!dptr->data[s_pos]) {
  dptr->data[s_pos] = kmem_cache_alloc(scullc_cache, GFP_KERNEL);
  if (!dptr->data[s_pos])
    goto nomem:
  memset(dptr->data[s_pos], 0, scullc_quantum);
} 

下面的代码将释放内存:

for (i = 0; i < qset; i++)
  if (dptr->data[i])
    kmem_cache_free(scullc_cache, dptr->data[i]);

最后,在模块卸载期间,我们必须将高速缓存返回给系统:

/* scullc_cleanup: 释放量子使用的高速缓存 */
if (scullc_cache)
  kmem_cache_destroy(scullc_cache);

  和 scull 相比,scullc 的最主要差别是运行速度略有提高,并且对内存的利用率更佳。由于数据对象是从内存池中分配的,而内存池中的所有内存块都具有同样大小,所以这些数据对象在内存中的位置排列达到了最大程度的密集,相反的,scull 的数据对象则会引入不可预测的内存碎片。

(2)内存池

  内核中有些地方的内存分配是不允许失败的。为了确保这种情况下的成功分配,内核开发者建立了一种称为内存池(或者 “mempool” )的抽象。内存池其实就是某种形式的后备高速缓存,它试图始终保存空闲的内存,以便在紧急状态下使用。

  内存池对象的类型为 mempool_t(在 <linux/mempool.h> 中定义),可使用 mempool_create 来建立内存池对象:

mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn,
              void *pool_data);

  min_nr 参数表示的是内存池应始终保持的已分配对象的最少数目。对象的实际分配和释放由 alloc_fnfree_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。


 如果读者计划在自己的驱动程序中使用 mempool,则应记住下面这点:mempool 会分配一些内存块,空闲且不会真正得到使用。因此,使用 mempool 很容易浪费大量内存。几乎在所有情况下,最好不使用 mempool 而是处理可能的分配失败。如果驱动程序存在某种方式可以响应分配的失败,而不会导致对系统一致性的破坏,则应该使用这种方式,也就是说,应尽量避免在驱动程序代码中使用 mempool。

3、get_free_page 和相关函数

可参考 ==> 3、页操作

  如果模块需要分配大块的内存,使用面向页的分配技术会更好些。整页的分配还有其他

优点,以后会在第十五章介绍。

分配页面可使用下面的函数:

get_zeroed_page(unsigned int flags);
// 返回指向新页面的指针并将页面清零。
_get_free_page(unsigned int flags);
// 类似于 get_zeroed_page,但不清零页面。
_get_free_pages(unsigned int flags, unsigned int order);
// 分配若干(物理连续的)页面,并返回指向该内存区域第一个字节的指针,但不清零页面。

 参数 flags 的作用和 kmalloc 中的一样;通常使用 GFP_KERNEL 或 GFP_ATOMIC,也许还会加上 __GFP_DMA 标志(申请可用于 ISA 直接内存访问操作的内存)或者 __GFP_HIGHMEM 标志(使用高端内存)。参数 order 是要申请或释放的页面数

的以 2 为底的对数(即 log2N)。例如,order(阶数)为 0 表示一个页面,order 为 3 表示 8 个页面。如果 order 太大, 而又没有那么大的连续区域可以分配,就会返回失败。get_order 函数使用一个整数参数,可根据宿主平台上的大小(必须是 2 的幕)返回 order 值。可允许的最大 order 值是 10 或者 11(对应于 1024 或 2048 个页),这依赖于体系结构。但是,相比具有大量内存的刚刚启动的系统而言,以阶数值为 10 进行分配而成功的机会很小。


 如果读者对此好奇,/proc/buddyinfo 可告诉你系统中每个内存区段上每个阶数下可获得的数据块数目。


 当程序不再需要使用页面时,它可以使用下列函数之一来释放它们。第一个函数是一个宏、展开后就是对第二个函数的调用:

void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);

 如果试图释放和先前分配数目不等的页面,内存映射关系就会被破坏,随后系统就会出错。


 值得强调的是,只要符合和和 kmalloc 同样的规则,get_free_pages 和其他函数可以在任何时间调用。某些情况下函数分配内存时会失败,特别是在使用了 GFP_ATOMIC 的时候。因此,调用了这些函数的程序在分配出错时都应提供相应的处理。


 尽管 kmalloc (GFP_KERNEL) 在没有空闲内存时有时会失败,但内核总会尽可能满足这个内存分配请求。因此,如果分配太多内存,系统的响应性能就很容易降下来。例如,如果往 scull 设备写入大量数据,计算机可能就会死掉;当系统为满足 kmalloc 分配请求而试图换出尽可能多的内存页时,就会变得很慢。所有资源都被贪婪的设备所吞噬,计算机很快就变的无法使用了;此时甚至已经无法为解决这个问题而生成新的进程。我们没有在 scull 模块中提到这个问题,因为它只是个例子,并不会真正在多用户系统中使用。但作为一个编程者必须小心,因为模块是特权代码,会带来新的系统安全漏洞,例如很可能会造成 Dos (denail-of-service,拒绝服务攻击)安全漏洞。

(1)alloc_pages 接口

 为完整起见,本节将介绍内存分配的另一个接口,但在第十五章才会使用这个接口。现在,我们只要知道 struct page 是内核用来描述单个内存页的数据结构就足够了。我们将看到,内核中有许多地方需要使用 page 结构,尤其在需要使用高端内存(高端内存在内核空间没有对应不变的地址)的地方。

  Linux 页分配器的核心代码是称为 alloc_pages_node 的函数:

struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);

 这个函数具有两个变种(它们只是简单的宏),大多数情况下我们使用这两个宏:

struct page *alloc_pages(unsigned int flags, unsigned int order);
struct page *alloc_page(unsigned int flags);

 核心函数 alloc_pages_node 要求传入三个参数。nid 是 NUMA 节点的 ID 号(注 3),表示要在其中分配内存,flags 是通常的 GFP_ 分配标志,而 order 是要分配的内存大小。该函数的返回值是指向第一个 page 结构(可能返回多个页)的指针,它描述了已分配的内存;或者在失败时返回NULL。


 alloc_pages 通过在当前的 NUMA 节点上分配内存而简化了 alloc_pages_node 函数,它将 numa_node_id 的返回值作为 nid 参数而调用了 alloc_pages_node 函数。另外, alloc_page 函数显然忽略了 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 和内核通信。这个信息可帮助内存分配器优化内存的使用。

4、 vmalloc及其辅助函数

 下面要介绍的内存分配函数是 vmalloc,它分配虚拟地址空间的连续区域。尽管这段区域在物理上可能是不连续的(要访问其中的每个页面都必须独立地调用函数 alloc_page),内核却认为它们在地址上是连续的。vmalloc 在发生错误时返回 0(NULL 地址),成功时返回一个指针,该指针指向一个线性的、大小最少为 size 的线性内存区域。


 我们在这里描述 vmalloc 的原因是,它是 Linux 内存分配机制的基础。但是,我们要注意在大多数情况下不鼓励使用 vmalloc。通过 vmalloc 获得的内存使用起来效率不高,而且在某些体系架构上,用于 vmalloc 的地址空间总量相对较小。如果希望将使用 vmalloc 的代码提交给内核主线代码,则可能会受到冷遇。如果可能,应该直接和单个的页面打交道,而不是使用 vmalloc。


 虽然这么说 ,但我们还是要看看如何使用 vmalloc 。该函数的原型及其相关函数(ioremap、并不是严格的分配函数,将在本节后面讨论)如下所示:

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

 要强调的是,由 kmalloc 和 __get_free_pages 返回的内存地址也是虚拟地址,其实际值仍然要由 MMU(内存管理单元,通常是 CPU 的组成部分)处理才能转为物理内存地址(注 4)。vmalloc 在如何使用硬件上没有区别,区别在于内核如何执行分配任务上。


 注 4: 实际上,其些体系架构定义了保留的 “虚拟” 地址范围,用于寻址物理内存。遇到这种情况时,Linux 内核会利用这种特性,内核和 __get_free_pages 地址均位于这种内存范围。其中的区别对设备驱动程序是透明的,对不直接涉及内存管理子系统的其他内核代码来说也是透明的。


  kmalloc 和 __get_free_pages 使用的(虚拟)地址范围与物理内存是一一对应的,可能会有基于常量 PAGE_OFFSET 的一个偏移。这两个函数不需要为该地址段修改页表。但另一方面,vmalloc 和 ioremap 使用的地址范围完全是虚拟的,每次分配都要通过对页表的适当设置来建立(虚拟)内存区域。


 可以通过比较内存分配函数返回的指针来发现这种差别。在某些平台上(如 x86),vmalloc 返回的地址仅仅比 kmalloc 返回的地址高一些;而在其他平台上(如 MIPS 和 IA-64),它们就完全属于不同的地址范围了。vmalloc 可以获得的地址在 VMALLOC_START 到 VMALLOC_END 的范围中。这两个符号都在 <asm/pgtable.h> 中定义。


 用 vmalloc 分配得到的地址是不能在微处理器之外使用的,因为它们只在处理器的内存管理单元上才有意义。当驱动程序需要真正的物理地址时(像外设用以驱动系统总线的 DMA 地址),就不能使用 vmalloc 了。使用 vmalloc 函数的正确场合是在分配一大块连续的、只在软件中存在的、用于缓冲的内存区域的时候。注意 vmalloc 的开销要比 __get_free_pages 大,因为它不但获取内存,还要建立页表。因此,用 vmalloc 函数分配仅仅一页的内存空间是不值得的。


 使用 vmalloc 函数的一个例子函数是 create_module 系统调用,它利用 vmalloc 函数来获取装模块所需的内存空间。在调用 insmod 来重定位模块代码后,接着会调用 copy_from_user 函数把模块代码和数据复制到分配而得的空间内。这样,模块看来像是在连续的内存空间内。但通过检查 /proc/ksyms 文件就能发现模块导出的内核符号和内核本身导出的符号分布在不同的内存范围上。


 用 vmalloc 分配得到的内存空间要用 vfree 函数来释放,这就像要用 kfree 函数来释放 kmalloc 函数分配得到的内存空间一样。


 和 vmalloc 一样,ioremap 也建立新的页表,但和 vmalloc 不同的是,ioremap 并不实际分配内存。ioremap 的返回值是一个特殊的虚拟地址,可以用来访问指定的物理内存区域、这个虚拟地址最后要调用 iounmap 来释放掉。


  ioremap 更多用于映射(物理的)PCI 缓冲区地址到(虚拟的)内核空间。例如,可以用来访问 PCI 视频设备的帧缓冲区;该缓冲区通常被映射到高物理地址,超出了系统初始化时建立的页表地址范围。PCI 的详细内容将在第十二章中讨论。


 要注意,为了保持可移植性,不应把 ioremap 返回的地址当作指向内存的指针而直接访问。相反,应该使用 readb 或其他 I/O 函数(在第九章 “使用 I/O 内存” 一节中介绍)。这是因为,在如 Alpha 的一些平台上,由于 PCI 规范和 Alpha 处理器在数据传输方式上的差异,不能直接把 PCI 内存区映射到处理器的地址空间。


  ioremap 和 vmalloc 函数都是面向页的(它们都会修改页表),因此重新定位或分配的内存空间实际上都会上调到最近的一个页边界。ioremap 通过把重新映射的地址向下下调到页边界,并返回在第一个重新映射页面中的偏移量的方法模拟了不对齐的映射。


  vmalloc 函数的一个小缺点是它不能在原子上下文中使用,因为它的内部实现调用了 kmalloc(GFP_KERNEL) 来获取页表的存储空间,因而可能休眠。但这不是什么问题 —— 如果 __get_free_page 函数都还不能满足中断处理例程的需求的话,那应该修改软件的设计了。

5、 per-CPU变量

 per-CPU(每 CPU)变量是 2.6 内核的一个有趣特性。当建立一个 per-CPU 变量时,系统中的每个处理器都会拥有该变量的特有副本。这看起来有些奇怪,但它有其优点。对 per-CPU 变量的访问(几乎)不需要锁定,因为每个处理器在其自己的副本上工作。per-CPU 变量还可以保存在对应处理器的高速缓存中,这样,就可以在频繁更新时获得更好的性能。


 关于 per-CPU 变量使用的例子可见于网络子系统中。内核维护着大量计数器,这些计数器跟踪已接收到的各类数据包数量,而这些计数器每秒可能被更新上千次。网络子系统的开发者将这些统计用的计数器放在了 per-CPU 变量中,这样,他们就不需要处理缓存和锁定问题,而更新可在不用锁的情况下快速完成。在用户空间偶尔请求这些计数器的值时,只需将每个处理器的版本相加并返回合计值即可。


 用于 per-CPU 变量的声明可见于 <linux/percpu.h> 中。要在编译期间创建一个 per-CPU 变量,可使用下面的宏:

DEFINE_PER_CPU(type, name);

  如果该变量(称为 name)是一个数组,需在 type 中包含数组的维数。这样,具有三个整数的 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 的调用将返回当前处理器变量版本的 Ivalue 值,并禁止抢占。因为返回的是 Ivalue,因此可直接赋值或者操作。例如,网络代码对一个计数器的递增使用了下面的两条语句:

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

 在大多数情况下可使用 alloc_percpu 完成分配工作;但如果需要特定的对齐,则应该调用 __alloc_percpu 函数。不管使用哪个函数,可使用 free_percpu 将 per-CPU 变量返回给系统。对动态分配的 per-CPU 变量的访问通过 per_cpu_ptr 完成:


per_cpu_ptr(void *per_cpu_var, int cpu_id);

 这个宏返回指向对应于给定 cpu_id 的 per_cpu_var 版本的指针。如果打算读取该变量的其他 CPU 版本,则可以引用该指针并进行相关操作。但是,如果正在操作当前处理器的版本,则应该首先确保自己不会被切换到其他处理器上运行。如果对 per-CPU 变量的整个访问发生在拥有某个自旋锁的情况下,则不会出现任何问题。但是,在使用该变量的时候通常需要使用 get_cpu 来阻塞抢占。这样,使用动态 per-CPU 变量的代码类似下面所示:

int cpu;
cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var, cpu);
/* 使用 ptr */
put_cpu();

  如果使用编译期间的 per-CPU 变量,则 get_cpu_varput_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);

 使用 DECLARE_PER_CPU(而不是 DEFINE_PER_CPU),将告诉编译器要使用一个外部引用。


 如果读者打算使用 per-CPU 变量来建立简单的整数计数器,可参考 <linux/percpu_counter.h> 中已封装好的实现。最后要注意,在某些体系架构上,per-CPU 变量可使用的地址空间是受限制的。因此,如果要创建 per-CPU 变量,则应该保持这些变量较小。

6、获取大的缓冲区

 我们在前面的小节中提到,大的、连续内存缓冲区的分配易流于失败。系统内存会随着时间的流逝而碎片化,这导致无法获得真正的大内存区域。因为,不需要大的缓冲区也可以有其他途径来完成自己的工作,因此内核开发者并没有将大缓冲区分配工作作为高优先级的任务来计划。在试图获得大内存区之前,我们应该仔细考虑其他的实现途径。


 到目前为止,执行大的 I/O 操作的最好方式是通过离散 / 聚集操作,我们将在第十章的 “离散 / 聚集映射” 中讨论这种操作。

(1)在引导时获得专用缓冲区

 如果的确需要连续的大块内存用作缓冲区,就最好在系统引导期间通过请求内存来分配。在引导时就进行分配是获得大量连续内存页面的唯一方法,它绕过了 __get_free_pages 函数在缓冲区大小上的最大尺寸和固定粒度的双重限制。在引导时分配缓冲区有点 “脏”、因为它通过保留私有内存池而跳过了内核的内存管理策略。这种技术比较粗暴也很不灵活,但也是最不容易失败的。显然,模块不能在引导时分配内存,而只有直接链接到内核的设备驱动程序才能在引导时分配内存。


 还有一个值得注意的问题是,对于普通用户来说引导时的分配不是一个切实可用的选项,因为这种机制只对链接到内核映像中的代码可用。要安装或替换使用了这种分配技术的驱动程序,就只能重新编译内核并重启计算机。


 内核被引导时,它可以访问系统所有的物理内存,然后调用各个子系统的初始化函数进行初始化,它允许初始化代码分配私有的缓冲区,同时减少了留给常规系统操作的 RAM 数量。


 通过调用下列函数之一则可完成引导时的内存分配:

#include <linux/bootmem.h>
void *alloc_bootmem(unsigned long size);
void *alloc_bootmem_low(unsigned long size);
void *alloc_bootmem_pages(unsigned long size);
void *alloc_bootmem_low_pages(unsigned long size);

 这些函数要么分配整个页(若以 pages 结尾)、要么分配不在页面边界上对齐的内存区。除非使用具有 _low 后级的版本,否则分配的内存可能会是高端内存。如果我们正在为设备驱动程序分配缓冲区,则可能希望将其用于 DMA 操作,而高端内存并不总是支持 DMA 操作;这样,我们可能需要使用上述函数的一个 _low 变种。

  很少会释放引导时分配的内存,而且也没有任何办法可将这些内存再次拿回。但是,内核也提供了一种释放这种内存的接口:

void free_bootmem(unsigned long addr, unsigned long size);

 注意,通过上述函数释放的部分页面不会返回给系统 —— 但是 ,如果我们使用这种技术,则其实已经分配得到了一些完整的页面。


 如果必须使用引导时的分配,则应该将驱动程序直接链接到内核。关于直接链接到内核的实现细节,可参阅内核源代码中 Documentation/kbuild 目录下的文件。

7、快速参考

  与内存分配有关的函数和符号如下:

#include <linux/slab.h>
void *kmalloc(size_t size, int flags);
void kfree(void *obj);
// 最常用的内存分配接口。
#include <linux/mm.h>

GFP_USER

GFP_KERNEL

GFP_NOFS

GFP_NOIO

GFP_ATOMIC

 用来控制内存分配执行方式的标志,其排列从最少限制到最多限制,GFP_USER 和

 GFP_KERNEL 优先级允许当前进程休眠以满足分配请求。GFP_NOFS 和 GFP_NOIO 分

 别禁用文件系统操作和所有的 I/O 操作,而 GFP_ATOMIC 根本不允许休眠。

 

__GFP_DMA

__GFP_HIGHMEM

__GFP_COLD

__GFP_NOWARN

__GFP_HIGH

__GFP_REPEAT

__GFP_NOFAIL

__GFP_NORETRY

 上述标志在分配内存时修改内核的行为。

#include <linux/malloc.h>
kmem_cache_t *kmem_cache_create(char *name, size_t size, size_t offset,
                unsigned long flags, constructor(), destructor());
int kmem_cache_destroy(kmem_cache_t *cache);
// 创建和销毁一个包含固定大小内存块的 slab 高速缓存,我们可以从这个高速缓存中
// 分配具有固定大小的对象。

SLAB_NO_REAP

SLAB_HWCACHE_ALIGN

SLAB_CACHE_DMA

 在创建高速缓存时指定的标志。

 

SLAB_CTOR_ATOMIC

SLAB_CTOR_CONSTRUCTOR

 可由分配器传递给 constructor 和 destructor 函数的标志。

void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
void kmem_cache_free(kmem_cache_t *cache, const void *obj);
// 从高速缓存中分配和释放一个对象。

/proc/slabinfo

  包含有 slab 高速缓存使用统计信息的虚拟文件。

#include <linux/mempool.h>
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,
              mempool_free_t *free_fn, void *data);
void mempool_destroy(mempool_t *pool);
// 用于创建内存池的函数。内存池通过保留预分配项的"急用链表"来避免内存分配的失败。

void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
// 从内存池分配或者释放对象的函数。

unsigned long get_zeroed_page(int flags);
unsigned long __get_free_page(int flags);
unsigned long __get_free_pages(int flags, unsigned long order);
// 面向页的分配函数。get_zeroed_page 返回单个已清零的页面,而其他所有调用不
// 进行页面的初始化。

int get_order(unsigned long size);
// 根据 PAGE_SIZE 返回当前平台上和 size 关联的分配阶数。该函数的参数必须是 2
// 的幕,而返回值至少为 0 。

void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
// 这些函数释放面向页分配的内存。

struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);
struct page *alloc_pages(unsigned int flags, unsigned int order);
struct page *alloc_page(unsigned int flags);
// Linux 内核中最底层页分配器的所有变种。

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);
// 用于释放由 alloc_page 的某种形式分配的页的不同函数。
#include <linux/vmalloc.h>
void * vmalloc(unsigned long size);
void vfree(void * addr);

#include <asm/io.h>
void * ioremap(unsigned long offset, unsigned long size);
void iounmap(void *addr);
// 这些面数分配或释放连续的虚拟地址空间。ioremap 通过虚拟地址访问物理内存,
// 而 vmalloc 分配空闲页面。使用 ioremap 映射的区域用 iounmap 释放,而从 vmalloc
// 获得的页面用 vfree 释放。
#include <linux/percpu.h>
DEFINE_PER_CPU(type, name);
DECLARE_PER_CPU(type, name);
// 定义和声明 per-CPU 变量的宏。

per_cpu(variable, int cpu_id)
get_cpu_var(variable)
put_cpu_var(variable)
// 用于访问静态声明的 per-CPU 变量的宏。

void *alloc_percpu(type);
void *__alloc_percpu(size_t size, siz_t align);
void free_percpu(void *variable);
// 执行 per-CPU 变量的运行时分配和释放的函数。

int get_cpu();
void put_cpu();
per_cpu_ptr(void *variable, int cpu_id)
// get_cpu 获得对当前处理器的引用(因此避免抢占以及切换到其他处理器)并返回
// 处理器的 ID 号;而 put_cpu 返回该引用。为了访问动态分配的 per-CPU 变量,应
// 使用 per_cpu_ptr,并传递要访问的变量版本的 CPU ID 号。对某个动态的 per-CPU
// 变量的当前 CPU 版本的操作,应该包含在对 get_cpu 和 put_cpu 的调用中间。
#include <linux/bootmem.h>
void *alloc_bootmem(unsigned long size);
void *alloc_bootmem_low(unsigned long size);
void *alloc_bootmem_pages(unsigned long size);
void *alloc_bootmem_low_pages(unsigned long size);
void free_bootmem(unsigned long addr, unsigned long size);
// 在系统引导期间执行内核分配和释放的函数。这些函数只能在直接链接到内核的驱动程序中使用。

九、与硬件通信

1、 I/O 端口和 I/O 内存

可参考 ==> (3)优化和内存屏障

字符驱动之八IO访问(硬件操作)

Android深度探索:HAL与驱动开发学习笔记–内存管理

驱动中动态内存申请(request_mem_region)

关于CPU地址空间

#include <linux/kernel.h>
void barrier(void)
// 这个函数通知编译器插入一个内存屏障,但对硬件没有影响。编译后的代码会把当
// 前 CPU 寄存器中的所有修改过的数值保存到内存中,需要这些数据的时候再重新
// 读出来。对 barrier的调用可避免在屏障前后的编译器优化,但硬件能完成自己的
// 重新排序。
#include <asm/system.h>
void rmb(void);
void read_barrier_depends (void);
void wmb(void);
void mb(void);
// 这些函数在已编译的指令流中插入硬件内存屏障;具体的实现方法是平台相关的。
// rmb (读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。
// wmb 保证写操作不会乱序, mb 指令保证了两者都不会。这些函数都是 barrier 的超集
// read_barrier_depends是一种特殊的、弱一些的读屏障形式。我们知道,rmb 避免
// 屏障前后的所有读取指令被重新排序,而 read_barrier_depends 仅仅阻止某些读取
// 操作的重新排序,这些读取依赖于其他读取操作返回的数据。它和 rmb 的区别很微
// 妙,而且并不是所有的架构上都存在这个函数。除非读者能够正确理解它们之间的
// 差别,并且有理由相信完整的读取屏障会导致额外的性能消耗,否则就应该始终使
// 用 rmb。
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
// 上述屏障宏版本也插入硬件屏障,但仅仅在内核针对 SMP 系统编译时有效;在单
// 处理器系统上,它们均会被扩展为上面那些简单的屏障调用。

2、 使用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值。如果 request_region 返回 NULL,那么我们就不能使用这些期望的端口。


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


 如果不再使用某组 I/O 端口(可能在卸载模块时),则应该使用下面的函数将这些端口返回给系统:

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

下面的函数允许驱动程序检查给定的 I/O 端口集是否可用:

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

 这里,如果给定的端口不可用,则返回值是负的错误代码。我们不赞成使用这个函数,因为它的返回值并不能确保分配是否能够成功,这是因为,检查和其后的分配并不是原子的操作。我们在这里列出这个函数,是因为仍有一些驱动程序在使用它,但是我们应该始终使用 request_region,因为这个函数执行了必要的锁定,以确保分配过程以安全、原子的方式完成。

(2)操作 I/O 端口

  当驱动程序请求了需要使用的 I/O 端口范围后,必须读取和/或写入这些端口。为此,大多数硬件都会把 8 位、16 位和 32 位的端口区分开来。它们不能像访问系统内存那样混淆使用(注 2)。

 注 2: 有时 I/O 端口和内存一样,例如,可以将两个 8 位的操作合并成一个 16 位的操作。如 PC 的显卡就可以,但一般来说不能认为一定具有这种功能。


 因此,C 语言程序必须调用不同的函数来访问大小不同的端口。如前一节所述,那些只支持内存映射的 I/O 寄存器的计算机体系架构通过把 I/O 端口地址重新映射到内存地址来伪装端口 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 一样,“长字” I/O 在 S390 平台上也不可使用。


 注意: 从现在开始,如果我们使用 unsigned 而不进一步指定类型信息的话,那么就是在谈论一个与体系架构相关的定义,此时不必关心它的准确特性。这些函数基本是可移植的,因为编译器在赋值时会自动进行强制类型转换(cast) 强制转换成 unsigned 类型可防止编译时出现的警告信息。只要程序员赋值时注意避免溢出,这种强制类型转换就不会丢失信息。在本章剩余部分将会一直保持这种 “不完整的类型定义” 的方式。


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

(3)在用户空间访问 I/O 端口

 上面这些函数主要是提供给设备驱动程序使用的,但它们也可以在用户空间使用,至少在 PC 类计算机上可以使用。GNU 的 C 库在 <sys/io.h> 中定义了这些函数。如果要在用户空间代码中使用 inb 及其相关函数,则必须满足下面这些条件:


编译该程序时必须带 ~O 选项来强制内联通数的展开。

必须用 ioperm 或 iopl 系统调用来获取对端口进行 I/O 操作的权限。ioperm 用来获取对单个端口的操作权限,而 iopl 用来获取对整个 I/O 空间的操作权限。这两个函数都是 x86 平台特有的。

必须以 root 身份运行该程序才能调用 ioperm 或 iopl(注 3)。或者,进程的祖先进程之一已经以 root 身份获取对端口的访问。

 注 3: 从技术上说,必须有 CAP_SYS_RAWIO 的权能,但这与在当前系统上以 root 身份运行是一样的。


 如果宿主平台没有 ioperm 和 iopl 系统调用,则用户空间程序仍然可以用 /dev/port 设备文件访问 I/O 端口。不过要注意,该设备文件的含义与平台密切相关,并且除 PC 平台以外,它几乎没有什么用处。

(4)串操作

 以上的 I/O 操作都是一次传输一个数据,作为补充,有些处理器上实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字。这些指令称为串操作指令,它们执行这些任务时比一个 C 语言编写的循环语句快得多。下面列出的宏实现了串 I/O,它们或者使用一条机器指令实现,或者在没有串 I/O 指令的平台上使用紧凑循环实现。S390 平台上没有定义这些宏。这不会影响可移植性,因为该平台的外设总线不同,通常不会和其他平台使用同样的设备驱动程序。


串 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);
// Read or write 32-bit values to a single 32-bit port.
// 对一个 32 位端口读 / 写 32 位数据。

  在使用串 I/O 操作函数时,需要铭记的是: 它们直接将字节流从端口中读取或写入。因此,当端口和主机系统具有不同的字节序时,将导致不可预期的结果。使用 inw 读取端口将在必要时交换字节,以便确保读入的值匹配于主机的字节序。然而,串函数不会完成这种交换。

3、 使用I/O内存

(1)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 得到。

  不再使用已分配的内存区域时,使用下面的接口释放:

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

下面是用来检查给定的 I/O 内存区域是否可用的老函数:

int check_mem_region(unsigned long start, unsigned long len);

 但是,和 check_region 一样,这个函数不安全,应避免使用。


 分配 I/O 内存并不是访问这些内存之前需要完成的唯一步骤,我们还必须确保该 I/O 内存对内核而言是可访问的。获取 I/O 内存并不仅仅意味着可引用对应的指针;在许多系统上,I/O 内存根本不能通过这种方式直接访问。因此,我们必须首先建立映射。映射的建立由 ioremap 函数完成,我们在第八章的 “vmalloc 及其辅助函数” 一节中介绍过这个函数。该函数专用于为 I/O 内存区域分配虚拟地址。


 一旦调用 ioremap(以及 iounmap)之后,设备驱动程序即可访问任意的 I/O 内存地址了,而无论 I/O 内存地址是否直接映射到虚拟地址空间。但要记住,由 ioremap 返回的地址不应直接引用,而应该使用内核提供的 accessor 函数。在我们介绍这些函数之前,首先复习一下 ioremap 的原型并介绍一些先前章节中跳过的细节内容。


 我们根据以下的定义来调用 ioremap 函数

#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void * addr);

 首先读者会注意到新的函数:ioremap_nocache。我们并没有在第八章介绍这个函数,因为该函数的功能和硬件相关。内核头文件中有如下一段解释: “如果某些控制寄存器也在此类区域,而不希望出现写人组合或者读取高速缓存的话,则可使用该函数。” 实际上,在大多数计算机平台上,该函数的实现和 ioremap 相同: 当所有 I/O 内存已属于非缓存地址时,就没有必要实现 ioremap 的独立的、非缓存版本。

(2)访问 I/O 内存

  在某些平台上,我们可以将 ioremap 的返回值直接当作指针使用。但是,这种使用不具有可移植性,而内核开发者正在致力于减少这类使用。访问 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(ul6 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, ursigned long count);
void ioread32_rep(void *addr, void *buf, ursigned 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);

  上述函数从给定的 buf 向给定的 addr 读取或写入 count 个值。注意,count 以被写入的数据大小为单位表示,比如,ioread32_repaddr 中读取 count32 位的值到 buf 中。

  上面给出的通数均在给定的 addr 处执行所有的 I/O 操作。如果我们要在一块 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);

  上述函数和 C 函数库的对应函数功能一致。

  如果读者阅读内核源代码,可能会遇到一组老的 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 字(quad-word)的命名是个历史遗留问题,那时候所有的处理器都只有16 位的字。实际上,现在把 32 位的数值叫做 L(长字)已经是不正确的了,不过如果对一切都重新命名、只会把事情搞得更复杂。

(3)像 I/O 内存一样使用端口

 某些硬件具有一种有趣的特性: 某些版本使用 I/O 端口,而其他版本使用 I/O 内存。导出给处理器的寄存器在两种情况下都是一样的,但访问方法却不同。为了让处理这类硬件的驱动程序更加易于编写,也为了最小化 I/O 端口和内存访问之间的表面区别,2.6 内核引入了 ioport_map 函数:

void *ioport_map(unsigned long port, unsigned int count);

  该函数重新映射 countI/O 端口,使其看起来像 I/O 内存。此后,驱动程序可在该函数返回的地址上使用 ioread8 及其同类函数,这样就不必理会 I/O 端口和 I/O 内存之间的区别了。

  当不再需要这种映射时,需要调用下面的函数来撤消:

void ioport_unmap(void *addr);

  这些函数使得 I/O 端口看起来像内存。但需要注意的是、在重新映射之前、我们必须通过 request_region 来分配这些 I/O 端口。

4、 快速参考

  本章引入下列与硬件管理有关的符号:

#include <linux/kernel.h>
void barrier(void)
// 这个 "软件" 内存屏障要求编译器考虑执行到该指令时所有的内存易变性。
#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
// 硬件内存屏障。要求 CPU (和编译器)执行该指令时检查所有必需的内存读、写
// (或二者兼有)已经执行完毕。
#include <asm/io.h>
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
unsigned inl(unsigned port);
void outl(unsigned doubleword, unsigned port);
// 这些函数用于读和写 I/O端口。如果用户空间的程序有访问端口的权限,则也可以
// 调用这些函数。

unsigned inb_p(unsigned port);
// 如果 I/O 操作之后需要一小段延时,可以用上面介绍的函数的 6 个暂停式的变体。
// 这些暂停式的函数都以p结尾。

void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
void insl(unsigned port, void *addr, unsigned long count);
void outsl (unsigned port, void *addr, unsigned long count);
// 这些 "串操作函数" 为输入端口与内存区之间的数据传输做了优化。这类传输是通
// 过对同一端口连续读 / 写 count 次实现的。

#include <linux/ioport.h>
struct resource *request_region(unsigned long start, unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);
int check_region(unsigned long start, unsigned long len);
// 为 I/O 端口分配资源的函数。check_ 函数在成功时返回 0,出错时返回负值,但我
// 们不建议使用该函数。

struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
void release_mem_region(unsigned long start, unsigned long len);
int check_mem_region(unsigned long start, unsigned long len);
// 这些函数处理对内存区域的资源分配。

#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void *virt_addr);
// ioremap 把一个物理地址范围重新映射到处理器的虚拟地址空间,以供内核使用。
// iounmap 用来解除这个映射。

#include <asm/io.h>
unsigned int ioread8(void *addr);
unsigned int ioreadi6(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);
// 用来访问 I/O 内存的函数。

void ioread8_rep(void *addr, void *buf, unsigned 1ong 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, unsiged long count);
// I/O 内存访问原语的 "重复" 版本。

unsigned readb(address);
unsigned readw(address);
unsigned readl (address);
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigmed value, address);
memset_io(address, value, count);
memcpy_fromio(dest, source, nbytes);
memcpy_toio(dest, source, nbytes);
// 也是用来访问 I/O 内存的函数,但老一些且不安全。

void *ioport_map(unsigned long port, unsigned int count);
void ioport_unmap(void *addr);
// 如果驱动程序作者希望将I/O 端口作为 I/O 内存一样进行操作,则可将这些端口传
// 递给 ioport_map函数。不再使用这种映射时,应该使用 ioport_unmap 函数解除映射。

目录
相关文章
|
12天前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
18天前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
22 6
|
18天前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
23 5
|
18天前
|
存储 缓存 Unix
Linux 设备驱动程序(三)(上)
Linux 设备驱动程序(三)
18 3
|
18天前
|
安全 数据管理 Linux
Linux 设备驱动程序(一)(中)
Linux 设备驱动程序(一)
17 2
|
18天前
|
Linux
Linux 设备驱动程序(四)
Linux 设备驱动程序(四)
10 1
|
18天前
|
存储 数据采集 缓存
Linux 设备驱动程序(三)(中)
Linux 设备驱动程序(三)
13 1
|
18天前
|
存储 前端开发 大数据
Linux 设备驱动程序(二)(中)
Linux 设备驱动程序(二)
15 1
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(二)(上)
Linux 设备驱动程序(二)
16 1
|
18天前
|
存储 缓存 安全
Linux 设备驱动程序(三)(下)
Linux 设备驱动程序(三)
11 0