LDD3读书笔记----内存映射

1.Linux 中的内存管理

Linux 是, 当然, 一个虚拟内存系统, 意味着用户程序见到的地址不直接对应于硬件使用的物理地址. 虚拟内存引入了一个间接层, 它允许了许多好事情. 有了虚拟内存, 系统重运行的程序可以分配远多于物理上可用的内存; 确实, 即便一个单个进程可拥有一个虚拟地址空间大于系统的物理内存. 虚拟内存也允许程序对进程的地址空间运用多种技巧, 包括映射成员的内存到设备内存.

User virtual addresses

这是被用户程序见到的常规地址. 用户地址在长度上是 32 位或者 64 位, 依赖底层的硬件结构, 并且每个进程有它自己的虚拟地址空间.

Physical addresses

在处理器和系统内存之间使用的地址. 物理地址是 32- 或者 64-位的量; 甚至 32-位系统在某些情况下可使用更大的物理地址.

Bus addresses

在外设和内存之间使用的地址. 经常, 它们和被处理器使用的物理地址相同, 但是这不是必要的情况. 一些体系可提供一个 I/O 内存管理单元(IOMMU), 它在总线和主内存之间重映射地址. 一个 IOMMU 可用多种方法使事情简单(例如, 使散布在内存中的缓冲对设备看来是连续的, 例如), 但是当设定 DMA 操作时对 IOMMU 编程是一个必须做的额外的步骤. 总线地址是高度特性依赖的, 当然.

Kernel logical addresses

这些组成了正常的内核地址空间. 这些地址映射了部分(也许全部)主存并且常常被当作它们是物理内存来对待. 在大部分的体系上, 逻辑地址和它们的相关物理地址只差一个常量偏移. 逻辑地址使用硬件的本地指针大小并且, 因此, 可能不能在重装备的 32-位系统上寻址所有的物理内存. 逻辑地址常常存储于 unsigned long 或者 void * 类型的变量中. 从 kmalloc 返回的内存有内核逻辑地址.

Kernel virtual addresses

内核虚拟地址类似于逻辑地址, 它们都是从内核空间地址到物理地址的映射. 内核虚拟地址不必有逻辑地址空间具备的线性的, 一对一到物理地址的映射, 但是. 所有的逻辑地址是内核虚拟地址, 但是许多内核虚拟地址不是逻辑地址. 例如, vmalloc 分配的内存有虚拟地址(但没有直接物理映射). kmap 函数(本章稍后描述)也返回虚拟地址. 虚拟地址常常存储于指针变量.

内存映射和 struct page

系统中每一个物理页有一个 struct page. 这个结构的一些成员包括下列:

atomic_t count;

这个页的引用数. 当这个 count 掉到 0, 这页被返回给空闲列表.

void *virtual;

这页的内核虚拟地址, 如果它被映射; 否则, NULL. 低内存页一直被映射; 高内存页常常不是. 这个成员不是在所有体系上出现; 它通常只在页的内核虚拟地址无法轻易计算时被编译. 如果你想查看这个成员, 正确的方法是使用 page_address 宏, 下面描述.

unsigned long flags;

一套描述页状态的一套位标志. 这些包括 PG_locked, 它指示该页在内存中已被加锁, 以及 PG_reserved, 它防止内存管理系统使用该页.

有很多的信息在 struct page 中, 但是它是内存管理的更深的黑魔法的一部分并且和驱动编写者无关.

内核维护一个或多个 struct page 项的数组来跟踪系统中所有物理内存.

有些函数和宏被定义来在 struct page 指针和虚拟地址之间转换:

struct page *virt_to_page(void *kaddr);

这个宏, 定义在 <asm/page.h>, 采用一个内核逻辑地址并返回它的被关联的 struct page 指针. 因为它需要一个逻辑地址, 它不使用来自 vmalloc 的内存或者高内存.

struct page *pfn_to_page(int pfn);

为给定的页帧号返回 struct page 指针. 如果需要, 它在传递给 pfn_to_page 之前使用 pfn_valid 来检查一个页帧号的有效性.

void *page_address(struct page *page);

返回这个页的内核虚拟地址, 如果这样一个地址存在. 对于高内存, 那个地址仅当这个页已被映射才存在. 这个函数在 <linux/mm.h> 中定义. 大部分情况下, 你想使用 kmap 的一个版本而不是 page_address.

#include <linux/highmem.h>
void *kmap(struct page *page);
void kunmap(struct page *page);

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

#include <linux/highmem.h>
#include <asm/kmap_types.h>
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 插口的竞争看来不是个问题.

虚拟内存区

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

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

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

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

一个进程的内存区可看到通过 /proc/<pid/maps>(这里 pid, 当然, 用一个进程的 ID 来替换). /proc/self 是一个 /proc/id 的特殊情况, 因为它常常指当前进程.

每行的字段是:

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

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

vm_area_struct 结构

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 定义于 <linux/mm.h>; 它包括下面列出的操作. 这些操作是唯一需要来处理进程的内存需要的, 它们以被声明的顺序列出. 本章后面, 一些这些函数被实现.

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

这个方法允许内核"预错"页到内存, 在它们被用户空间存取之前. 对于驱动通常没有必要来实现这个填充方法.

mmap 设备操作

映射一个设备意味着关联一些用户空间地址到设备内存. 无论何时程序在给定范围内读或写, 它实际上是在存取设备.

系统调用如下一样被声明

mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset) 

另一方面, 文件操作声明如下:

int (*mmap) (struct file *filp, struct vm_area_struct *vma);

方法中的 filp 参数象在第 3 章介绍的那样, 而 vma 包含关于用来存取设备的虚拟地址范围的信息. 因此, 大量工作被内核完成; 为实现 mmap, 驱动只要建立合适的页表给这个地址范围, 并且, 如果需要, 用新的操作集合替换 vma->vm_ops.

有 2 个建立页表的方法:调用 remap_pfn_range 一次完成全部, 或者一次一页通过 nopage VMA 方法. 每个方法有它的优点和限制. 我们从"一次全部"方法开始, 它更简单. 从这里, 我们增加一个真实世界中的实现需要的复杂性.

 使用 remap_pfn_range

建立新页来映射物理地址的工作由 remap_pfn_range 和 io_remap_page_range 来处理, 它们有下面的原型:

int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot); 
int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long phys_addr, unsigned long size, pgprot_t prot); 

由这个函数返回的值常常是 0 或者一个负的错误值. 让我们看看这些函数参数的确切含义:

vma

页范围被映射到的虚拟内存区

virt_addr

重新映射应当开始的用户虚拟地址. 这个函数建立页表为这个虚拟地址范围从 virt_addr 到 virt_addr_size.

pfn

页帧号, 对应虚拟地址应当被映射的物理地址. 这个页帧号简单地是物理地址右移 PAGE_SHIFT 位. 对大部分使用, VMA 结构的 vm_paoff 成员正好包含你需要的值. 这个函数影响物理地址从 (pfn<<PAGE_SHIFT) 到 (pfn<<PAGE_SHIFT)+size.

size

正在被重新映射的区的大小, 以字节.

prot

给新 VMA 要求的"protection". 驱动可(并且应当)使用在 vma->vm_page_prot 中找到的值.

给 remap_fpn_range 的参数是相当直接的, 并且它们大部分是已经在 VMA 中提供给你, 当你的 mmap 方法被调用时. 你可能好奇为什么有 2 个函数, 但是. 第一个 (remap_pfn_range)意图用在 pfn 指向实际的系统 RAM 的情况下, 而 io_remap_page_range 应当用在 phys_addr 指向 I/O 内存时. 实际上, 这 2 个函数在每个体系上是一致的, 除了 SPARC, 并且你在大部分情况下被使用看到 remap_pfn_range . 为编写可移植的驱动, 但是, 你应当使用 remap_pfn_range 的适合你的特殊情况的变体.

另一种复杂性不得不处理缓存: 常常地, 引用设备内存不应当被处理器缓存. 常常系统 BIOS 做了正确设置, 但是它也可能通过保护字段关闭特定 VMA 的缓存. 不幸的是, 在这个级别上关闭缓存是高度处理器依赖的. 好奇的读者想看看来自 drivers/char/mem.c 的 pgprot_noncached 函数来找到包含什么. 我们这里不进一步讨论这个主题.

添加 VMA 的操作

如我们所见, vm_area_struct 结构包含一套操作可以用到 VMA.

使用 nopage 映射内存

尽管 remap_pfn_range 对许多人工作得不错, 如果不是大部分人, 驱动 mmap 的实现有时有点更大的灵活性是必要的. 在这样的情况下, 一个使用 nopage VMA 方法的实现可被调用.

nopage 方法, 记住, 有下列原型:

struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);

当一个用户进程试图存取在一个不在内存中的 VMA 中的一个页, 相关的 nopage 函数被调用. 地址参数包含导致出错的虚拟地址, 向下圆整到页的开始. nopage 函数必须定位并返回用户需要的页的 struct page 指针. 这个函数必须也负责递增它通过调用 get_page 宏返回的页的使用计数.

 get_page(struct page *pageptr); 

这一步是需要的来保持在被映射页的引用计数正确. 内核为每个页维护这个计数; 当计数到 0, 内核知道这个页可被放置在空闲列表了. 当一个 VMA 被去映射, 内核递减使用计数给区中每个页. 如果你的驱动在添加一个页到区时不递增计数, 使用计数过早地成为 0, 系统的整体性被破坏了.

nopage 方法也应当存储错误类型在由 type 参数指向的位置 -- 但是只当那个参数不为 NULL. 在设备驱动中, 类型的正确值将总是 VM_FAULT_MINOR.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值