• 沒有找到結果。

第 5 章 ARM LINUX 驱动程序开发入门

5.3 L INUX 驱动程序开发要点

5.3.1 内存与 I/O 端口

内存与 I/O 端口是 Linux 驱动设备开发经常用到的两个概念,编写驱动程序大多数情况 下都是对内存和 I/O 端口的操作。下面将分别介绍内存和 I/O 端口在 Linux 设备驱动程序中 的使用。

5.3.1.1 内存

void *kmalloc(size_t size, int flags);

void kfree(void *obj);

这两个函数被声名在内核源码<include/linux/slab.h>文件中,设备驱动程序作为内核的一 部分,不能使用虚拟内存,必须利用内核提供的 kmalloc 与 kfree 来申请和释放内核存储空 间。Kmalloc 带两个参数,第一个参数(size)是要申请的是内存数量;第二个参数(flags)

用来控制 kmalloc 的优先权。其中 flags 参数的经常有以下几种:

ü GFP_KERNEL:它的意思是该内存分配(内部是通过调用 get_free_pages 来实现的,

所以名字中带 GFP)是由运行在内核态的进程调用的。也就是说,调用它的函数是属 于某个进程的,使用 GFP_KERNEL 优先允许 kmalloc 函数在系统空闲内存低于水平 线 min_free_pages 时延迟分配函数的返回。当空闲内存太少时,kmalloc 函数会使当 前进程进入 sleep(睡眠)*(注 2)状态,等待空闲页的出现。

ü GFP_ATOMIC:并非使用 GFP_KERNEL 优先权后一定正确,有时 kmalloc 是在进程 上下文之外调用的-一比如在中断处理,任务队列处理和内核定时器处理时发生。这

ü GFP_USER:用来分配内存给用户空间的页,当空闲内存太少时,kmalloc 函数会使 当前进程进入 sleep 状态,等待空闲页的出现。

ü GFP_HIGHUSER:类似 GFP_USER,只是从高处分配内存。

ü GFP_NOIO:类似 GFP_KERNEL,只是增加了禁止任何 I/O 初始化的限制。

ü GFP_NOFS:类似 GFP_KERNEL,不允许执行任何文件系统的调用。

*注 2:睡眠(sleep)是一个非常重要的概念在设备驱动程序开发中,当一个进程被置入 sleep 状态时,它

会被标记为一种特殊状态并从调度器的运行队列中移走,直到某些情况下修改了这个状态,进程才会在 CPU

上调度,也就是运行该进程。进入 sleep 状态的进程会被搁置在一边,等待将来的某个事件发生。

关于 kmalloc 与 kfree 的具体实现,可参考内核源程序中的<include/linux/slab.h>文件。

如果希望分配大一点的内存空间,内核会利用一个更好的面向页的机制,分配页的相关函数 有以下三个,这三个函数的定义在<mm/ page_alloc.c>文件中。

ü get_zeroed_page(unsigned int gfp_mask);

该函数的作用是申请一个新的页,初始化该页的值为零,并返回页的指针。

ü _ _get_free_page(unsigned int flags);

该函数类似于 get_zeroed_page,但是它不初始化页的值为零。

ü _ _get_free_pages(unsigned int flags, unsigned int order);

该函数类似于_ _get_free_page,但是它可以申请多个页,并且返回的是第一个页的指 针。

再介绍一个内存相关的重要概念,虚拟内存空间的分配,以上介绍的内存分配函数都是 针对实际的物理内存而言的,但在 Linux 系统中经常会使用虚拟内存的技术,虚拟内存通俗 的说就是系统在硬盘上建立的缓冲区,它并不是真正的实际内存,是计算机使用的临时存储 器,用来运行所需内存大于计算机具有的内存的程序。说到虚拟内存,首先需要说明一下 Linux 的地址类型,Linux 通常有以下几种地址类型:

ü 用户虚拟地址

这类地址是用户空间编程的常规地址,该地址通常是 32 或 64 位,它依赖于使用的硬 件体系结构,并且每一个进程有其自己的用户空间。

ü 物理地址

这类地址是用在处理器和系统内存之间的地址,该地址通常是 32 或 64 位,在有些情 况下,32 位系统可以使用更大的物理地址。

ü 总线地址

这类地址用在外围总线和内存之间,通常它们和被 CPU 使用的物理地址一样。一些 体系结构可以提供一个 I/O 内存管理单元(I/OMMU),它可以重新映射地址在总线 和主存之间。总线地址是与体系结构密不可分的。

ü 内核逻辑地址

该类地址是由普通的内核地址空间组成,这些地址映射一部分或全部主存,并且经常 被对待如同物理地址。在许多体系结构下,逻辑地址和物理地址之间只是差别一个恒 定的偏移量。逻辑地址通常储存一些变量类型,如 long, int,void*等。利用 kmalloc 可 以申请返回一个内核逻辑地址。

ü 内核虚拟地址

在从内核空间地址映射到物理地址时,内核虚拟地址与内核逻辑地址类似。内核虚拟 地址并不一定是线性的,一对一的映射到物理地址。所有的逻辑地址都是内核虚拟地 址,但是许多内核虚拟地址却不是逻辑地址。内核虚拟地址通常储存在指针变量中。

这五种地址类型经常被使用在 Linux 系统中,如图 5.2 给出了这几种地址在系统中的逻 辑关系。

图 5.2 Linux 系统中的地址类型

如果你有一个逻辑地址,通过宏__pa(x)可以得到相应的物理地址,另外,通过宏__va(x) 可以计算获得物理地址对应的逻辑地址。这两个宏被定义在<include/asm/page.h>中,具体定 义如下:

#define __pa(x) ((unsigned long) (x) - PAGE_OFFSET)

#define __va(x) ((void *)((unsigned long) (x) + PAGE_OFFSET)) 其中 PAGE_OFFSET 会根据平台的不同定义也就会不同。

现在让我们来学习虚拟内存分配相关的知识,虚拟内存分配函数通常是 vmalloc(也有 vmalloc_32 和__vmalloc),它分配虚拟地址空间的连续区域。尽管这段区域在物理上可能是 不连续的,内核却认为它们在地址上是连续的。分配的内存空间被映射进入内核数据段中,

对用户空间是不可见的,这一点上与其他分配技术不同。Vmalloc 和其相关函数的原型如下 所示,这些函数被包含在<include/linux/vmalloc.h>头文件中。

ü void* vmalloc(unsigned long size);

该函数的作用是申请 size 大小的虚拟内存空间,发生错误时返回 0,成功时返回一个 指向一个大小为 size 的线性地址空间的指针。参数 size 为申请内存的大小。

ü void vfree(void* addr);

该函数的作用是释放一个由 vmalloc(vmalloc_32 或__vmalloc)函数申请内存,释放 的内存基地址为 addr。

ü void *vmap(struct page **pages, unsigned int count, unsigned long flags, pgprot_t prot);

该函数的作用是映射一个数组(其内容为页)到连续的虚拟空间中。第一个参数 pages 为指向页数组的指针,第二个参数 count 为要映射页的个数,第三个参数 flags 为传 递 vm_area->flags 值,第四个参数 prot 为映射时页保护。

ü void vunmap(void *addr);

该函数的作用是释放由 vmap 映射的虚拟内存,释放从 addr 地址开始的连续的虚拟区 用户进程

用户虚拟地址

用户进程 用户虚拟地址

高内存 低内存

内核虚拟地址

内核逻辑地址

物理内存 地址空间

页映射

域。 分配一大块连续的用于缓冲的内存区域。注意 vmalloc 的开销要比__get_free_pages 大,因为 它处理获取内存还要建立页表。因此,不值得用 vmalloc 函数只分配一页的内存空间。

ü check_region(unsigned long s, unsigned long n);

该函数作用是察看系统的 I/O 表,看是否有别的驱动程序占用某一段 I/O 口。第一个 参数 s 是 I/O 端口的基地址;第二个参数是 I/O 端口占用的范围。返回值为 0 时表示 没有占用,非 0 时表示已经被占用。

ü request_region(start,n,name);

该函数的作用是如果这段 I/O 端口没有被占用,在我们的驱动程序中就可以使用它。 ARM,PowerPC 等)通常只实现一个物理地址空间,外设 I/O 端口成为了内存的一部分,

此时 CPU 访问 I/O 端口就像访问一个内存单元不需要单独的 I/O 指令。这两种方式在硬件

ü inb(unsigned long port);

按字节(8 位宽度)读端口。参数 port 为要读取的端口号。参数 port 在一些平台上定义 为 unsigned long,而在另一些平台上定义为 unsigned short。不同平台上 inb 返回值的

类型也不相同。

ü outb(unsigned long value, unsigned long port);

按字节(8 位宽度)写端口。第一个参数 value 是要写进端口的值,第二个参数 port 为 要写的端口号。

ü inw(unsigned long port);

按字(16 位宽度)读端口。参数 port 为要读取的端口号。

ü outw(unsigned long value, unsigned long port);

按字(16 位宽度)写端口。第一个参数 value 是要写进端口的值,第二个参数 port 为要 写的端口号。

ü inl(unsigned long port)

按双字(32 位宽度)读端口。参数 port 为要读取的端口号。

ü outb(unsigned long value, unsigned long port)

按双字(32 位宽度)写端口。第一个参数 value 是要写进端口的值,第二个参数 port 为 要写的端口号。

第二类 I/O 端口操作函数是基于内存映射方式的,常用函数如下:

ü readb(const volatile void __iomem *addr);

按字节(8 位宽度)读指定地址单元。参数 addr 为要读取的内存地址。

ü writeb(unsigned char b, volatile void __iomem *addr);

按字节(8 位宽度)写数据到指定地址单元。第一个参数 b 为要写的值,第二个参数 addr 为要写到的内存地址。

ü readw(const volatile void __iomem *addr)

与 readb 类似,只是按字(16 位宽度)读指定地址单元。参数 addr 为要读取的内存地址。

ü writew(unsigned short b, volatile void __iomem *addr);

与 writeb 类似,只是按字(16 位宽度)写到指定地址单元。第一个参数 b 为要写的值,

第二个参数 addr 为要写到的内存地址。

ü readl(const volatile void __iomem *addr);

与 readb 类似,只是按双字(32 位宽度)读指定地址单元。参数 addr 为要读取的内存地 址。

ü writel(unsigned int b, volatile void __iomem *addr);

与 writeb 类似,只是按双字(32 位宽度)写到指定地址单元。第一个参数 b 为要写的值,

第二个参数 addr 为要写到的内存地址。

总之,内存与 I/O 端口是 Linux 驱动程序开发中非常重要的两个概念,所以希望读者对 他们有较深入的理解,以便更好的学习 Linux 设备驱动程序开发。