Linux Device Drivers, Third Edition [LWN.net]
第十五章 Memory Mapping and DMA
Address Types
Linux是一个虚拟内存系统,用户程序与硬件使用的物理地址不直接对应。虚拟内存引入了一层间接寻址,从而支持很多高级特性。通过虚拟内存,系统上可以给运行程序分配大于实际物理内存的内存空间;甚至,单个进程也可以拥有比物理内存大的虚拟地址地址空间。虚拟内存允许程序使用进程地址空间做一些高级操作,比如将程序内存映射到设备内存。
Linux系统有好几种类型的地址,每个地址都有自己的语义。但是,有时候内核不知道哪种情况下使用哪种类型的地址,因此程序员必须小心。
下面是Linux中使用的地址类型列表,以及于物理内存的关系。
User virtual addresses 用户虚拟地址
用户空间程序看到的常规地址(regular addresses)。用户地址为32位或64位长度,具体取决于底层硬件架构,每个进程都有自己的虚拟地址空间。Physical addresses 物理地址
处理器和系统内存交互使用的地址。物理的地址是32位或64位的数量;某些情况下,32位系统也可以使用更大的物理地址。Bus addresses 总线地址
总线和内存交互使用的地址。通常,与处理器使用的物理地址相同,但也不一定。某些架构可以提供I/O内存管理单元(IOMMU),在总线和主存之间重新映射地址。IOMMU可以通过多种方式使得更简单,但必须在设置DMA时对IOMMU进行编程。总线地址,高度依赖架构。Kernel logical addresses 内核逻辑地址
这些组成了内核地址空间的normal部分。这些地址映射主内存的一部分(也许是全部),通常被视为物理地址。在大多数架构上,逻辑地址和它们关联的物理地址仅差一个固定的偏移量。逻辑地址使用硬件的原生指针大小,因此在32位系统上可能无法给所有的物理内存映射地址。逻辑地址通常存储在unsigned long
或void*
类型的变量中。kmalloc返回的内存有一个内核逻辑地址。Kernel virtual addresses 内核虚拟地址
内核虚拟地址类似与逻辑地址,都是从内核空间地址到物理地址的映射。内核虚拟地址
不一定是线性的、一对一的映射,这些是逻辑地址的特征。逻辑地址一定是内核地址虚拟地址,但内核虚拟地址不一定是逻辑地址。
例如,vmalloc分配的内存有一个虚拟地址(不是直接到物理内存的映射)。kmap也返回虚拟地址。虚拟地址通常存储在指针变量中。
Physical Addresses and Pages
物理内存被分成很多离散单元,称为页(page)。系统对内存的大部分内部处理基于页。页大小因架构而异,大多是4096-byte。常量PAGE_SIZE(定义在<asm/PAGE.h>
)可以在任何架构上定义页大小。
如果查看地址(虚拟or物理),它被划分为一个页码(page number)和一个偏移量(offset)。如果使用4k页面,则低12位为偏移量,其余高位表示页码。
如果放弃该偏移并将其余向右移动,结果称为PFN(page frame number)。在PFN和地址之间转换是常见操作;宏PAGE_SHIFT告诉此转换须偏移多少位。
物理页通过Page Frame Number (PFN)识别。PFN可以从物理地址轻易计算出来,除以page size 或 去掉PAGE_SHIFT bits。
PFN计算:
PFN = (physical address
/ page frame size
) 取整。余数是page frame offset。
通常,page frames大小等于virtual pages。
High and Low Memory
在32位系统上配备很多内存,逻辑地址和内核虚拟地址之间就差异明显了。使用32位,只可以寻址4 GB内存。
32位Linux系统因为其虚拟地址空间的设置方式,直到现在还被限制在比这少的内存上。
x86架构默认配置下,内核划分4GB虚拟地址空间给用户空间和内核空间;它们的contexts中使用相同的映射集。一个典型的划分方式:3 GB用于用户空间,1 GB用于内核空间。
内核的代码和data structures必须加载到该空间,但是使用内核地址空间最多的是到物理内存的虚拟映射。内核不能直接操作未映射到内核地址空间的内存。换句话说,内核需要它自己的虚拟地址来存放它必须直接接触的内存。
因此,多年来,内核可以处理的最大物理内存量是:可以映射到内核虚拟地址空间部分的内存量,减去内核代码本身所需的空间。所i有,x86 Linux 最多只可以使用略低于1GB的物理内存。
不破坏32位程序和系统兼容性,还能支持更多内存呢,就成了商业压力。为此,处理器制造商在产品中提供了地址扩展(address extension)功能。这样,32位处理器也可以寻址超过4GB的物理内存。
然而,可以被直接映射到逻辑地址的物理内存限制仍然存在。只有内存的最低位部分(最多1或2 GB,取决于硬件和内核配置)具有逻辑地址;其余的(高位内存)没有。在访问特定的高位内存页之前,内核必须设置显式虚拟映射,以使该页在内核地址空间中可用。
因此,许多内核data structures必须放在低位内存中;高位内存倾向于为用户空间进程页保留。
high memory一词可能会让一些人感到困惑,特别是因为它在PC世界中有其他含义。因此,我们在这里作术语定义:
Low memory
在内核空间中存在逻辑地址的内存。几乎每个系统上,所有内存都是low memory。High memory
不存在逻辑地址的内存,因为它超出了为内核虚拟地址预留的地址范围。
在i386系统上,低位内存和高位内存之间的边界通常设置为略低于1GB,但是可以在内核配置更改。此边界仅于内核设置有关,与硬件无关。
Page Tables
现代系统,处理器都必须具备相应的机制将虚拟地址转换为物理地址制。该机制称为页表 (page table);本质上,它是一个多级树状数组,包含虚拟-物理的映射和一些相关的标志(flags)。Linux内核甚至在不直接使用页表的架构上,也维护着一组页表。
通常,设备驱动程序执行的许多操作都可能涉及到操作页表。幸运的是,2.6内核已经不再直接使用页表。
推荐阅读《Understanding The Linux Kernel》(O’Reilly),作者 Daniel P. Bovet and Marco Cesati 。
Virtual Memory Areas
VMA - 虚拟内存区域,是用于管理进程地址空间的不同区域的内核数据结构(kernel data structure)。
VMA维护着进程虚拟内存中的一个由相同类型组成的区域:具有相同权限标志的,后端是同一对象(例如文件或交换空间)的,连续虚拟地址范围。
它大致上对应于segment(段)的概念,不过最好将其描述为“具有自身属性的内存对象”。进程的内存映射由(至少)以下区域组成:
程序可执行代码的区域(通常称为文本 - text)
多个数据区域,包括初始化数据(在执行开始时具有显式赋值的数据)、未初始化数据(BSS),和程序堆栈
每个活动的内存映射一个区域
通过查看/proc/<pid/maps>
可以看到进程的内存区域。/proc/self
是/proc/pid
的特例,它对应当前进程。范例:
# cat /proc/1/maps
look at init
08048000-0804e000 r-xp 00000000 03:01 64652 /sbin/init #text
0804e000-0804f000 rw-p 00006000 03:01 64652 /sbin/init #data
0804f000-08053000 rwxp 00000000 00:00 0 #zero-mapped BSS
40000000-40015000 r-xp 00000000 03:01 96278 /lib/ld-2.3.2.so #text
40015000-40016000 rw-p 00014000 03:01 96278 /lib/ld-2.3.2.so #data
40016000-40017000 rw-p 00000000 00:00 0 #BSS for ld.so
42000000-4212e000 r-xp 00000000 03:01 80290 /lib/tls/libc-2.3.2.so #text
4212e000-42131000 rw-p 0012e000 03:01 80290 /lib/tls/libc-2.3.2.so #data
42131000-42133000 rw-p 00000000 00:00 0 #BSS for libc
bffff000-c0000000 rwxp 00000000 00:00 0 #Stack segment
ffffe000-fffff000 ---p 00000000 00:00 0 #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