Linux 设备驱动 (第三版)第 15 章 内存映射和 DMA15.1. Linux 中的内存管理

15.1. Linux 中的内存管理

kmap 为系统中的任何页返回一个内核虚拟地址. 对于低内存页, 它只返回页的逻辑地址; 对于高内存, kmap 在内核地址空间的一个专用部分中创建一个特殊的映射. 使用 kmap 创建的映射应当一直使用 kunmap 来释放;一个有限数目的这样的映射可用, 因此最好不要在它们上停留太长时间. kmap 调用维护一个计数器, 因此如果 2 个或 多个函数都在同一个页上调用 kmap, 正确的事情发生了. 还要注意 kmap 可能睡眠当没有映射可用时.

include #include void kmap_atomic(struct page page, enum km_type type);void kunmap_atomic(void *addr, enum km_type type);

kmap_atomic 是 kmap 的一种高性能形式. 每个体系都给原子的 kmaps 维护一小列插口( 专用的页表项); 一个 kmap_atomic 的调用者必须在 type 参数中告知系统使用这些插口中的哪个. 对驱动有意义的唯一插口是 KM_USER0 和 KM_USER1 (对于直接从来自用户空间的调用运行的代码), 以及 KM_IRQ0 和 KM_IRQ1(对于中断处理). 注意原子的 kmaps 必须被原子地处理; 你的代码不能在持有一个时睡眠. 还要注意内核中没有什么可以阻止 2 个函数试图使用同一个插口并且相互干扰( 尽管每个 CPU 有独特的一套插口). 实际上, 对原子的 kmap 插口的竞争看来不是个问题.

在本章后面和后续章节中当我们进入例子代码时, 我们看到这些函数的一些使用,

15.1.5. 页表

在任何现代系统上, 处理器必须有一个机制来转换虚拟地址到它的对应物理地址. 这个机制被称为一个页表; 它本质上是一个多级树型结构数组, 包含了虚拟-到-物理的映射和几个关联的标志. Linux 内核维护一套页表即便在没有直接使用这样页表的体系上.

设备驱动通常可以做的许多操作能涉及操作页表. 幸运的是对于驱动作者, 2.6 内核已经去掉了任何直接使用页表的需要. 结果是, 我们不描述它们的任何细节; 好奇的读者可能想读一下 Understanding The Linux Kernel 来了解完整的内容, 作者是 Daniel P. Bovet 和 Marco Cesati (O' Reilly).

15.1.6. 虚拟内存区

虚拟内存区( VMA )用来管理一个进程的地址空间的独特区域的内核数据结构. 一个 VMA 代表一个进程的虚拟内存的一个同质区域: 一个有相同许可标志和被相同对象(如, 一个文件或者交换空间)支持的连续虚拟地址范围. 它松散地对应于一个"段"的概念, 尽管可以更好地描述为"一个有它自己特性的内存对象". 一个进程的内存映射有下列区组成:

  • 给程序的可执行代码(常常称为 text)的一个区.

  • 给数据的多个区, 包括初始化的数据(它有一个明确的被分配的值, 在执行开始), 未初始化数据(BBS), [48]以及程序堆栈.

  • 给每个激活的内存映射的一个区域.

一个进程的内存区可看到通过 /proc/(这里 pid, 当然, 用一个进程的 ID 来替换). /proc/self 是一个 /proc/id 的特殊情况, 因为它常常指当前进程. 作为一个例子, 这里是几个内存映射(我们添加了简短注释)


# cat /proc/1/maps look at init
08048000-0804e000 r-xp 00000000 03:01 64652
0804e000-0804f000 rw-p 00006000 03:01 64652
0804f000-08053000 rwxp 00000000 00:00 0
40000000-40015000 r-xp 00000000 03:01 96278
40015000-40016000 rw-p 00014000 03:01 96278
40016000-40017000 rw-p 00000000 00:00 0
42000000-4212e000 r-xp 00000000 03:01 80290
4212e000-42131000 rw-p 0012e000 03:01 80290
42131000-42133000 rw-p 00000000 00:00 0
bffff000-c0000000 rwxp 00000000 00:00 0
ffffe000-fffff000 ---p 00000000 00:00 0
/sbin/init text /sbin/init data zero-mapped BSS /lib/ld-2.3.2.so text /lib/ld-2.3.2.so data BSS for ld.so /lib/tls/libc-2.3.2.so text /lib/tls/libc-2.3.2.so data BSS for libc Stack segment vsyscall page
# rsh wolf cat /proc/self/maps #### x86-64 (trimmed)
00400000-00405000 r-xp 00000000 03:01 1596291 /bin/cat text
00504000-00505000 rw-p 00004000 03:01 1596291 /bin/cat data
00505000-00526000 rwxp 00505000 00:00 0 bss
3252200000-3252214000 r-xp 00000000 03:01 1237890 /lib64/ld-2.3.3.so
3252300000-3252301000 r--p 00100000 03:01 1237890 /lib64/ld-2.3.3.so
3252301000-3252302000 rw-p 00101000 03:01 1237890 /lib64/ld-2.3.3.so
7fbfffe000-7fc0000000 rw-p 7fbfffe000 00:00 0 stack
ffffffffff600000-ffffffffffe00000 ---p 00000000 00:00 0 vsyscall

每行的字段是:


start-end perm offset major:minor inode image

每个在 /proc/*/maps (出来映象的名子) 对应 struct vm_area_struct 中的一个成员:

start end
这个内存区的开始和结束虚拟地址.

perm
带有内存区的读,写和执行许可的位掩码. 这个成员描述进程可以对属于这个区的页做什么. 成员的最后一个字符要么是给"私有"的 p 要么是给"共享"的 s.

offset
内存区在它被映射到的文件中的起始位置. 0 偏移意味着内存区开始对应文件的开始.

major minor
持有已被映射文件的设备的主次编号. 易混淆地, 对于设备映射, 主次编号指的是持有被用户打开的设备特殊文件的磁盘分区, 不是设备自身.

inode
被映射文件的 inode 号.

image
已被映射的文件名((常常在一个可执行映象中).

15.1.6.1. vm_area_struct 结构

当一个用户空间进程调用 mmap 来映射设备内存到它的地址空间, 系统通过一个新 VMA 代表那个映射来响应. 一个支持 mmap 的驱动(并且, 因此, 实现 mmap 方法)需要来帮助那个进程来完成那个 VMA 的初始化. 驱动编写者应当, 因此, 为支持 mmap 应至少有对 VMA 的最少的理解.

让我们看再 struct vm_area_struct 中最重要的成员( 在 中定义). 这些成员应当被设备驱动在它们的 mmap 实现中使用. 注意内核维护 VMA 的链表和树来优化区查找, 并且 vm_area_struct 的几个成员被用来维护这个组织. 因此, VMA 不是有一个驱动任意创建的, 否则这个结构破坏了. VMA 的主要成员是下面(注意在这些成员和我们刚看到的 /proc 输出之间的相似)

unsigned long vm_start;unsigned long vm_end;
被这个 VMA 覆盖的虚拟地址范围. 这些成员是在 /proc/*/maps中出现的头 2 个字段.

struct file *vm_file;
一个指向和这个区(如果有一个)关联的 struct file 结构的指针.

unsigned long vm_pgoff;
文件中区的偏移, 以页计. 当一个文件和设备被映射, 这是映射在这个区的第一页的文件位置.

unsigned long vm_flags;
描述这个区的一套标志. 对设备驱动编写者最感兴趣的标志是 VM_IO 和 VM_RESERVUED. VM_IO 标志一个 VMA 作为内存映射的 I/O 区. 在其他方面, VM_IO 标志阻止这个区被包含在进程核转储中. VM_RESERVED 告知内存管理系统不要试图交换出这个 VMA; 它应当在大部分设备映射中设置.

struct vm_operations_struct *vm_ops;
一套函数, 内核可能会调用来在这个内存区上操作. 它的存在指示内存区是一个内核"对象", 象我们已经在全书中使用的 struct file.

void *vm_private_data;
驱动可以用来存储它的自身信息的成员.

象 struct vm_area_struct, vm_operations_struct 定义于 ; 它包括下面列出的操作. 这些操作是唯一需要来处理进程的内存需要的, 它们以被声明的顺序列出. 本章后面, 一些这些函数被实现.

void (open)(struct vm_area_struct vma);
open 方法被内核调用来允许实现 VMA 的子系统来初始化这个区. 这个方法被调用在任何时候有一个新的引用这个 VMA( 当生成一个新进程, 例如). 一个例外是当这个 VMA 第一次被 mmap 创建时; 在这个情况下, 驱动的 mmap 方法被调用来替代.

void (close)(struct vm_area_struct vma);
当一个区被销毁, 内核调用它的关闭操作. 注意没有使用计数关联到 VMA; 这个区只被使用它的每个进程打开和关闭一次.

struct page (nopage)(struct vm_area_struct vma, unsigned long address, int type);
当一个进程试图存取使用一个有效 VMA 的页, 但是它当前不在内存中, nopage 方法被调用(如果它被定义)给相关的区. 这个方法返回 struct page 指针给物理页, 也许在从第 2 级存储中读取它之后. 如果 nopage 方法没有为这个区定义, 一个空页由内核分配.

int (populate)(struct vm_area_struct vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
这个方法允许内核"预错"页到内存, 在它们被用户空间存取之前. 对于驱动通常没有必要来实现这个填充方法.

15.1.7. 进程内存映射

内存管理难题的最后部分是进程内存映射结构, 它保持所有其他数据结构在一起. 每个系统中的进程(除了几个内核空间帮助线程)有一个 struct mm_struct ( 定义在 ), 它含有进程的虚拟内存区列表, 页表, 和各种其他的内存管理管理信息, 包括一个旗标( mmap_sem )和一个自旋锁( page_table_lock ). 这个结构的指针在任务结构中; 在很少的驱动需要存取它的情况下, 通常的方法是使用 current->mm. 注意内存关联结构可在进程之间共享; Linux 线程的实现以这种方式工作, 例如.

这总结了我们对 Linux 内存管理数据结构的总体. 有了这些, 我们现在可以继续 mmap 系统调用的实现.

[47] 许多非-x86体系可以有效工作在没有这里描述的内核/用户空间的划分, 因此它们可以在 32-位系统使用直到 4-GB 内核地址空间. 但是, 本节描述的限制仍然适用这样的系统当安装有多于 4GB 内存时.

[48] BSS 的名子是来自一个老的汇编操作符的历史遗物, 意思是"由符号开始的块". 可执行文件的 BSS 段不存储在磁盘上, 并且内核映射零页到 BSS 地址范围.