第十五章 内存映射和DMA
1、 Linux的内存管理
(1)地址类型
用户虚拟地址
这是在用户空间程序所能看到的常规地址。用户地址或者是 32 位的,或者是 64 位的,这取决于硬件的体系架构。每个进程都有自己的虚拟地址空间。
物理地址
该地址在处理器和系统内存之间使用。物理地址也是 32 位或者 64 位长的,在某些情况下甚至 32 位系统也能使用 64 位的物理内存。
总线地址
该地址在外围总线和内存之间使用。通常它们与处理器使用的物理地址相同,但这么做并不是必需的。一些计算机体系架构提供了 I/O 内存管理单元(IOMMU),它实现总线和主内存之间的重新映射。IOMMU 可以用很多种方式让事情变得简单(比如使内存中的分散缓冲区对设备来说是连续的),但是当设置 DMA 操作时,编写 IOMMU 相关的代码是一个必需的额外步骤。当然总线地址是与体系架构密切相关的。
内核逻辑地址
内核逻辑地址组成了内核的常规地址空间。该地址映射了部分(或者全部)内存,并经常被视为物理地址。在大多数体系架构中,逻辑地址和与其相关联的物理地址的不同,仅仅是在它们之间存在一个固定的偏多量。逻辑地址使用硬件内建的指针大小,因此在安装了大量内存的 32 位系统中,它无法寻址全部的物理地址。逻辑地址通常保存在 unsigned long 或者 void * 这样类型的变量中。kmalloc 返回的内存就是内核逻辑地址。
内核虚拟地址
内核虚拟地址和逻辑地址的相同之处在于,它们都将内核空间的地址映射到物理地址上。内核虚拟地址与物理地址的映射不必是线性的和一对一的,而这是逻辑地址空间的特点。所有的逻辑地址都是内核虚拟地址,但是许多内核虚拟地址不是逻辑地址。举个例子,vmalloc 分配的内存具有一个虚拟地址(但并不存在直接的物理映射)。kmap 函数(在本章后面论述)也返回一个虚拟地址。虚拟地址通常保存在指针变量中。
如果有一个逻辑地址,宏 __pa()(在 中定义)返回其对应的物理地址;使用宏 __va() 也能将物理地址逆向映射到逻辑地址,但这只对低端内存页有效。
不同的内核函数需要不同类型的地址。如果在 C 中已经定义好了不同的类型,那么需要的地址类型将很明确,但现实不是这样的。在本章中,将明确表述在何处使用何种类型的地址。
(2)物理地址和页
物理地址被分成离散的单元,称之为页。系统内部许多对内存的操作都是基于单个页的。每个页的大小随体系架构的不同而不同,但是目前大多数系统都使用每页 4096 个字节。常量 PAGE_SIZE(在 中定义)给出了在任何指定体系架构下的页大小。
仔细观察内存地址,无论是虚拟的还是物理的,它们都被分为页号和一个页内的偏移量。举个例子,如果使用每页 4096 个字节,那么最后的 12 位是偏移量,而剩余的高位则指定了页号。如果忽略了地址偏移量,并将除去偏移量的剩余位移到右端,称该结果为页帧数。移动位以在页帧数和地址间进行转换是一个常用操作;宏 PAGE_SHIFT 将告诉程序员,必须移动多少位才能完成这个转换。
(3)高端与低端内存
在装有大量内存的 32 位系统中,逻辑和内核虚拟地址的不同将非常突出。使用 32 位只能在 4GB 的内存中寻址。由于这种建立虚拟地址空间的问题,直到最近,32 位系统的 Linux 仍被限制使用少于 4GB 的内存。
内核(在 x86 体系架构中,这是默认的设置)将 4GB 的虚拟地址空间分割为用户空间和内核空间;在二者的上下文中使用同样的映射。一个典型的分割是将 3GB 分配给用户空间,1GB 分配给内核空间(注 1)。内核代码和数据结构必须与这样的空间相匹配,但是占用内核地址空间最大的部分是物理内存的虚拟映射。内核无法直接操作没有映射到内核地址空间的内存。换句话说,内核对任何内存的访问,都需要使用自己的虚拟地址。因此许多年来,由内核所能处理的最大物理内存数量,就是将映射至虚拟地址空间内核部分的大小,再减去内核代码自身所占用的空间。因此,基于 x86 的 Linux 系统所能使用的最大物理内存,会比 1GB 小一点。
注 1: 许多非x86 的体系架构不需需要这里描述的内核 / 用户空间分割即可有效工作,因此,这些体系架构在 32 位系统上就能获得 4GB 的内核地址空间。但是,这一小节描述的限制对安装有多于 4GB 内存的系统仍然适用。
为了应对商业压力,在不破坏 32 位应用程序和系统兼容性的情况下,为了能使用更多的内存,处理器制造厂家为他们的产品增添了 “地址扩展” 特性。其结果是在许多情况下,即使 32 位的处理器都可以在大于 4GB 的物理地址空间寻址。然而有多少内存可以直接映射到逻辑地址的限制依然存在。只有内存的低端部分(依赖与硬件和内核的设置,一般为 1 到 2GB)拥有逻辑地址(注 2);剩余的部分(高端内存)是没有的。在访问特定的高端内存页前,内核必须建立明确的虚拟映射,使该页可在内核地址空间中被访问。因此,许多内核数据结构必须被放置在低端内存中;而高端内存更趋向于为用户空间进程页所保留。
注 2: 2.6 内核通过一个补丁可在 x86 硬件上持 “4G/4G” 模式,它可以让内核和用户虚拟地址空间变大,但会引入微小的性能代价。
术语 “高端内存” 可能对一些人来说理解起来比较困难、特别是在 PC 世界中,它还有着其他的含义。因此为了弄清这个问题,这里先对它进行定义:
低端内存
存在于内核空间上的逻辑地址内存。几乎所有现在读者遇到的系统,它全部的内存都是低端内存。
高端内存
是指那些不存在逻辑地址的内存,这是因为它们处于内核虚拟地址之上。
在 i386 系统中,虽然在内核配置的时候能够改变低端内存和高端内存的界限,但是通常将该界限设置为小于 1GB。这个界限与早期 PC 中的 640K 限制没有任何关系,并且它的设置也与硬件无关。相反它是由内核设置的,把 32 位地址空间分割成内核空间与用户空间。
在后面的部分中,将指出使用高端内存的限制。
(4)内存映射和页结构
由于历史的关系,内核使用逻辑地址来引用物理内存中的页。然而由于支持了高端内存,就暴露出一个明显的问题 —— 在高端内存中将无法使用逻辑地址。因此内核中处理内存的函数趋向使用指向 page 结构的指针(在 中定义)。该数据结构用来保存内核需要知道的所有物理内存信息;对系统中每个物理页,都有一个 page 结构相对应。下面介绍该结构中包含的几个成员:
// include/linux/mm_types.h struct page { unsigned long flags; /* Atomic flags, some possibly * updated asynchronously */ atomic_t _count; /* Usage count, see below. */ union { atomic_t _mapcount; /* Count of ptes mapped in mms, * to show when page is mapped * & limit reverse map searches. */ struct { /* SLUB */ u16 inuse; u16 objects; }; }; union { struct { unsigned long private; /* Mapping-private opaque data: * usually used for buffer_heads * if PagePrivate set; used for * swp_entry_t if PageSwapCache; * indicates order in the buddy * system if PG_buddy is set. */ struct address_space *mapping; /* If low bit clear, points to * inode address_space, or NULL. * If page mapped as anonymous * memory, low bit is set, and * it points to anon_vma object: * see PAGE_MAPPING_ANON below. */ }; #if USE_SPLIT_PTLOCKS spinlock_t ptl; #endif struct kmem_cache *slab; /* SLUB: Pointer to slab */ struct page *first_page; /* Compound tail pages */ }; union { pgoff_t index; /* Our offset within mapping. */ void *freelist; /* SLUB: freelist req. slab lock */ }; struct list_head lru; /* Pageout list, eg. active_list * protected by zone->lru_lock ! */ /* * On machines where all RAM is mapped into kernel address space, * we can simply calculate the virtual address. On machines with * highmem some memory is mapped into kernel virtual memory * dynamically, so we need a place to store that address. * Note that this field could be 16 bits on x86 ... ;) * * Architectures with slow multiplication can define * WANT_PAGE_VIRTUAL in asm/page.h */ #if defined(WANT_PAGE_VIRTUAL) void *virtual; /* Kernel virtual address (NULL if not kmapped, ie. highmem) */ #endif /* WANT_PAGE_VIRTUAL */ #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS unsigned long debug_flags; /* Use atomic bitops on this */ #endif #ifdef CONFIG_KMEMCHECK /* * kmemcheck wants to track the status of each byte in a page; this * is a pointer to such a status block. NULL if not tracked. */ void *shadow; #endif };
atomic_t count;
对该页的访问计数。当计数值为 0 时,该页将返回给空闲链表。
void *virtual;
如果页面被映射,则指向页的内核虚拟地址;如果未被映射则为 NULL。低端内存页总是被映射;而高端内存页通常不被映射。并不是在所有体系架构中都有该成员;只有在页的内核虚拟地址不容易被计算时,它才被编译。如果要访问该成员,正确的方法是使用下面讲述的 page_address 宏。
unsigned long flags;
描述页状态的一系列标志。其中,PG_locked 表示内存中的页已经被锁住,而 PG_reserved 表示禁止内存管理系统访问该页。
在 page 结构中还包含了许多信息,但这是深层次内存管理所关心的问题,而驱动程序作者不必要了解。
内核维护了一个或者多个 page 结构数组,用来跟踪系统中的物理内存。在一些系统中,有一个单独的数组称之为 mem_map。在另外一些系统中,情况将会复杂很多。非一致性内存访问(Nonuniform Memory Access,NUMA)系统和有大量不连续物理内存的系统会有多个内存映射数组,因此从可移植性考虑,代码不要直接访问那些数组。幸运的是,通常只需要使用 page 结构的指针,而不需要了解它们是怎么来的。
有一些函数和宏用来在 page 结构指针与虚拟地址之间进行转换:
// arch/x86/include/asm/page.h struct page *virt_to_page(void *kaddr); #define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT) #define __pa(x) __phys_addr((unsigned long)(x)) #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) // arch/x86/include/asm/page_types.h #define PAGE_SHIFT 12
该宏在 中定义,负责将内核逻辑地址转换为相应的 page 结构指针。由于它需要一个逻辑地址,因此它不能操作 vmalloc 生成的地址以及高端内存。
// include/asm-generic/memory_model.h struct page *pfn_to_page(int pfn); #define __pfn_to_page(pfn) (vmemmap + (pfn)) #define __page_to_pfn(page) (unsigned long)((page) - vmemmap) // arch/x86/include/asm/page_64_types.h #define vmemmap ((struct page *)VMEMMAP_START) // arch/x86/include/asm/pgtable_64_types.h #define VMEMMAP_START _AC(0xffffea0000000000, UL) // include/linux/const.h #define __AC(X,Y) (X##Y) #define _AC(X,Y) __AC(X,Y)
针对给定的页帧号,返回 page 结构指针。如果需要的话,在将页帧号传递给 pfn_to_page 前,使用 pfn_valid 检查页帧号的合法性。
void *page_address(struct page *page); // include/linux/mm.h #define page_address(page) lowmem_page_address(page) static __always_inline void *lowmem_page_address(struct page *page) { return __va(page_to_pfn(page) << PAGE_SHIFT); }
如果地址存在的话,则返回页的内核虚拟地址。对于高端内存来说,只有当内存页被映射后该地址才存在。该函数定义在 中。在大多数情况下,要使用 kmap 而不是 page_address。
#include <linux/highmem.h> void *kmap(struct page *page); void kunmap(struct page *page);
kmap 为系统中的页返回内核虚拟地址。对于低端内存页来说,它只返回页的逻辑地址;对于高端内存,kmap 在专用的内核地址空间创建特殊的映射。由 kmap 创建的映射需要用 kunmap 释放;对该种映射的数量是有限的,因此不要持有映射过长的时间。kmap 调用维护了一个计数器,因此如果两个或是多个函数对同一页调用 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 的高性能版本。每个体系架构都为原子的 kmap 维护着一个槽(专用页表入口)的列表;kmap_atomic 的调用者必须告诉系统,type 参数使用的是哪个槽。对驱动程序有意义的槽只有 KM_USER0 和 KM_USER1(针对在用户空间中直接运行的代码),KM_IRQ0 和 KM_IRQ1(针对中断处理程序)。要注意的是原子的 kmap 必须原子地处理,也就是说,在拥有它的时候,代码不能进入睡眠状态。还要注意的是在内核中,没有任何机制能防止这两个函数使用相同的槽,以及防止它们之间的相互干涉(虽然对每个 CPU 都有一套特定的槽)。在实际情况中,对原子的 kmap 槽的争夺并不会引起什么问题。
在研究例子代码时,以及在本章及后面的章节中,读者会看到如何使用这些面数。
(5)页表
在任何现代的系统中,处理器必须使用某种机制、将虚拟地址转换为相应的物理地址。这种机制被称为页表;它基本上是一个多层树形结构,结构化的数组中包含了虚拟地址到物理地址的映射和相关的标志位。即使在不直接使用这种页表的体系架构中,Linux 内核也维护了一系列的页表。
设备驱动程序执行了大量操作,用来处理页表。幸运的是,对驱动程序作者来说,在 2.6 版内核中删除了对页表直接操作的需求。因此这里不对它们做详细讲解;富有好奇心的读者可以阅读 Daniel P. Bovet 和 Marco Cesati 编写的《Understanding The Linux Kernel》(O’Reilly)一书了解详细情况。
(6)虚拟内存区
虚拟内存区(VMA)用于管理进程地址空间中不同区域的内核数据结构。一个 VMA 表示在进程的虚拟内存中的一个同类区域: 拥有同样权限标志位和被同样对象(一个文件或者交换空间)备份的一个连续的虚拟内存地址范围。它符合更宽泛的 “段” 的概念,但是把其描述成 “拥有自身属性的内存对象” 更为贴切。进程的内存映射(至少)包含下面这些区域:
程序的可执行代码(通常称为 text)区域。
多个数据区,其中包含初始化数据(在开始执行的时候就拥有明确的值)、非初始化数据(BSS,注 3)以及程序堆栈。
与每个活动的内存映射对应的区域。
注 3: BSS 这一名称是一个历史遗物,来自一条老的汇编操作称为 “block started by symbol(符号定义的块)”。可执行文件的 BSS 段并不会存储在磁盘上,而是由内核将零页映射到 BSS 地址范围。
查看 /proc/(其中的 pid 要替换为具体的进程 ID)文件就能了解进程的内存区域。/proc/self 是一个特殊的文件,因为它始终指向当前进程。下面是多个内存映射的例子(注释以斜体的方式给出):
每行都是用下面的形式表示的:
start-end perm offset major:minor inode image
在 /proc/*/maps 中的每个成员(除映像名外)都与 vm_area_struct 结构中的一个成员相对应:
start
end
该内存区域的起始处和结束处的虚拟地址。
perm
内存区域的读、写和执行权限的位掩码。该成员描述了允许什么样的进程能访问属于该区域的页。该成员的最后一个字母或者是 p 表示私有,或者是 s 表示共享。
offset
表示内存区域在映射文件中的起始位置。偏移量为 0 表示内存区域的起始位置映射到文件的开始位置。
major
minor
拥有映射文件的设备的主设备号和次设备号。对于设备映射来说,主设备号和次设备号指的是包含设备特殊文件的磁盘分区,该文件由用户而非设备自身打开。
inode
被映射的文件的索引节点号。
image
被映射文件(通常是一个可执行映像)的名称。
(a)vm_area_struct 结构
当用户空间进程调用 mmap、将设备内存映射到它的地址空间时,系统通过创建一个表示该映射的新 VMA 作为响应。支持 mmap 的驱动程序(当然要实现 mmap 方法)需要帮助进程完成 VMA 的初始化。因此驱动程序作者为了能支持 mmap,需要对 VMA 有所了解。
现在来学习 vm_area_struct 结构(在 中定义)中最重要的成员。在设备驱动程序对 mmap 的实现中会使用到这些成员。请注意,为优化查找方法,内核维护了 VMA 的链表和树型结构,而 vm_area_struct 中的许多成员都是用来维护这个结构的。因此驱动程序不能任意创建 VMA,或者打破这种组织结构。VMA 的主要成员如下所示(请注意这些成员和刚才看到的 /proc 文件输出之间的区别):
// include/linux/mm_types.h struct vm_area_struct { struct mm_struct * vm_mm; /* The address space we belong to. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next; pgprot_t vm_page_prot; /* Access permissions of this VMA. */ unsigned long vm_flags; /* Flags, see mm.h. */ struct rb_node vm_rb; /* * For areas with an address space and backing store, * linkage into the address_space->i_mmap prio tree, or * linkage to the list of like vmas hanging off its node, or * linkage of vma in the address_space->i_mmap_nonlinear list. */ union { struct { struct list_head list; void *parent; /* aligns with prio_tree_node parent */ struct vm_area_struct *head; } vm_set; struct raw_prio_tree_node prio_tree_node; } shared; /* * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma * list, after a COW of one of the file pages. A MAP_SHARED vma * can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack * or brk vma (with NULL file) can only be in an anon_vma list. */ struct list_head anon_vma_chain; /* Serialized by mmap_sem & * page_table_lock */ struct anon_vma *anon_vma; /* Serialized by page_table_lock */ /* Function pointers to deal with this struct. */ const struct vm_operations_struct *vm_ops; /* Information about our backing store: */ unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */ struct file * vm_file; /* File we map to (can be NULL). */ void * vm_private_data; /* was vm_pte (shared mem) */ unsigned long vm_truncate_count;/* truncate_count or restart_addr */ #ifndef CONFIG_MMU struct vm_region *vm_region; /* NOMMU mapping region */ #endif #ifdef CONFIG_NUMA struct mempolicy *vm_policy; /* NUMA policy for the VMA */ #endif };
unsigned long vm_start;
unsigned long vm_end;
该 VMA 所覆盖的虚拟地址范围。这是 /proc/*/maps 中最前面的两个成员。
struct file *vm_file;
指向与该区域(如果存在的话)相关联的 file 结构指针。
unsigned long vm_pgoff;
以页为单位,文件中该区域的偏移量。当映射一个文件或者设备时,它是该区域中被映射的第一页在文件中的位置。
unsigned long vm_flags;
描述该区域的一套标志。驱动程序最感兴趣的标志是 VM_IO 和 VM_RESERVED。 VM_IO 将 VMA 设置成一个内存映射 I/O 区域。VM_IO 会阻止系统将该区域包含在进程的核心转储中。VM_RESERVED 告诉内存管理系统不要将该 VMA 交换出去;大多数设备映射中都设置该标志。
struct vm_operations_struct *vm_ops;
内核能调用的一套函数,用来对该内存区进行操作。它的存在表示内存区域是一个内核 “对象” ,这点和在本书中使用的 file 结构很相似。
void *vm_private_data;
驱动程序用来保存自身信息的成员。
与 vm_area_struct 结构类似,vm_operations_struct 结构也定义在 中,其中包含了下面列出的函数。这些操作只是用来处理进程的内存需求,并按照声明的顺序将它们列了出来。在本章后面的部分,将介绍如何实现其中的几个函数。
// include/linux/mm.h struct vm_operations_struct { void (*open)(struct vm_area_struct * area); void (*close)(struct vm_area_struct * area); int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); /* notification that a previously read-only page is about to become * writable, if an error is returned it will cause a SIGBUS */ int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf); /* called by access_process_vm when get_user_pages() fails, typically * for use by special VMAs that can switch between memory and hardware */ int (*access)(struct vm_area_struct *vma, unsigned long addr, void *buf, int len, int write); #ifdef CONFIG_NUMA /* * set_policy() op must add a reference to any non-NULL @new mempolicy * to hold the policy upon return. Caller should pass NULL @new to * remove a policy and fall back to surrounding context--i.e. do not * install a MPOL_DEFAULT policy, nor the task or system default * mempolicy. */ int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new); /* * get_policy() op must add reference [mpol_get()] to any policy at * (vma,addr) marked as MPOL_SHARED. The shared policy infrastructure * in mm/mempolicy.c will do this automatically. * get_policy() must NOT add a ref if the policy at (vma,addr) is not * marked as MPOL_SHARED. vma policies are protected by the mmap_sem. * If no [shared/vma] mempolicy exists at the addr, get_policy() op * must return NULL--i.e., do not "fallback" to task or system default * policy. */ struct mempolicy *(*get_policy)(struct vm_area_struct *vma, unsigned long addr); int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from, const nodemask_t *to, unsigned long flags); #endif };
void (*open)(struct vm_area_struct *vma);
内核调用 open 函数,以允许实现 VMA 的子系统初始化该区域。当对 VMA 产生一个新的引用时(比如 fork 进程时),则调用这个函数。唯一的例外发生在 mmap 第一次创建 VMA 时;在这种情况下,需要调用驱动程序的 mmap 方法。
void (*close)(struct vm_area_struct *vma);
当销毁一个区域时,内核将调用 close 操作。请注意由于 VMA 没有使用相应的计数,所以每个使用区域的进程都只能打开和关闭它一次。
struct page *(*nopage) (struct vm_area_struct *vma, unsigned long address, int *type);
当一个进程要访问属于合法 VMA 的页,但该页又不在内存中时,则为相关区域调用 nopage 函数(如果定义了的话)。在将物理页从辅助存储器中读入后,该函数返回指向物理页的 page 结构指针。如果在该区域没有定义 nopage 函数,则内核将为其分配一个空页。
int (*populate)(struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
在用户空间访问页前,该函数允许内核将这些页预先装入内存。一般来说,驱动程序不必实现 populate 方法。
(7)内存映射处理
最后一个内存管理难题是处理内存映射结构,它负责整合所有其他的数据结构。在系统中的每个进程(除了内核空间的一些辅助线程外)都拥有一个 struct mm_struct 结构(在 中定义),其中包含了虚拟内存区域链表、页表以及其他大量内存管理信息,还包含一个信号灯(mmap_sea)和一个自旋锁(page_table_lock)。在 task 结构中能找到该结构的指针;在少数情况下当驱动程序需要访问它时,常用的办法是使用 current->mm。请注意,多个进程可以共享内存管理结构,Linux 就是用这种方法实现线程的。
为了能对 Linux 内存管理数据结构有一个通盘的了解,现在首先看看 mmap 系统调用是如何实现的。
// include/linux/mm_types.h struct mm_struct { struct vm_area_struct * mmap; /* list of VMAs */ struct rb_root mm_rb; struct vm_area_struct * mmap_cache; /* last find_vma result */ #ifdef CONFIG_MMU unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags); void (*unmap_area) (struct mm_struct *mm, unsigned long addr); #endif unsigned long mmap_base; /* base of mmap area */ unsigned long task_size; /* size of task vm space */ unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */ unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */ pgd_t * pgd; atomic_t mm_users; /* How many users with user space? */ atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ int map_count; /* number of VMAs */ struct rw_semaphore mmap_sem; spinlock_t page_table_lock; /* Protects page tables and some counters */ struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung * together off init_mm.mmlist, and are protected * by mmlist_lock */ unsigned long hiwater_rss; /* High-watermark of RSS usage */ unsigned long hiwater_vm; /* High-water virtual memory usage */ unsigned long total_vm, locked_vm, shared_vm, exec_vm; unsigned long stack_vm, reserved_vm, def_flags, nr_ptes; unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */ /* * Special counters, in some configurations protected by the * page_table_lock, in other configurations by being atomic. */ struct mm_rss_stat rss_stat; struct linux_binfmt *binfmt; cpumask_t cpu_vm_mask; /* Architecture-specific MM context */ mm_context_t context; /* Swap token stuff */ /* * Last value of global fault stamp as seen by this process. * In other words, this value gives an indication of how long * it has been since this task got the token. * Look at mm/thrash.c */ unsigned int faultstamp; unsigned int token_priority; unsigned int last_interval; unsigned long flags; /* Must use atomic bitops to access the bits */ struct core_state *core_state; /* coredumping support */ #ifdef CONFIG_AIO spinlock_t ioctx_lock; struct hlist_head ioctx_list; #endif #ifdef CONFIG_MM_OWNER /* * "owner" points to a task that is regarded as the canonical * user/owner of this mm. All of the following must be true in * order for it to be changed: * * current == mm->owner * current->mm != mm * new_owner->mm == mm * new_owner->alloc_lock is held */ struct task_struct *owner; #endif #ifdef CONFIG_PROC_FS /* store ref to file /proc/<pid>/exe symlink points to */ struct file *exe_file; unsigned long num_exe_file_vmas; #endif #ifdef CONFIG_MMU_NOTIFIER struct mmu_notifier_mm *mmu_notifier_mm; #endif };
2、mmap 设备操作
在现代 Unix 系统中,内存映射是最吸引人的特征。对于驱动程序来说,内存映射可以提供给用户程序直接访问设备内存的能力。
使用 mmap 的一个例子是看一下 X Window 系统服务器的部分虚拟内存区域:
X 服务器完整的 VMA 的清单很长,但这里对其中的大部分内容都不感兴趣。可以看到,有四个独立的 /dev/mem 的映射,它为我们揭示了 X 服务器如何使用显示卡工作的内幕。第一个映射开始位置是 a0000,这是在 640KB ISA 结构中显示 RAM 的标准位置。往下可以看到更大的一块映射区域 e8000000,其地址位于系统最大 RAM 地址之上。这是对显示适配器中显存的直接映射。
在 /proc/iomem 中也可以看到:
映射一个设备意味着将用户空间的一段内存与设备内存关联起来。无论何时当程序在分配的地址范围内读写时,实际上访问的就是设备。在 X 服务器例子中,使用 mmap 就能迅速而便捷地访问显卡内存。对于那些与此类似、性能要求苛刻的应用程序,直接访问能显著提高性能。
正如读者怀疑的那样,不是所有的设备都能进行 mmap 抽象的。比如像串口和其他面向流的设备就不能做这样的抽象。对 mmap 的另外一个限制是:必须以 PAGE_SIZE 为单位进行映射。内核只能在页表一级上对虚拟地址进行管理,因此那些被映射的区域必须是 PAGE_SIZE 的整数倍,并且在物理内存中的起始地址也要求是 PAGE_SIZE 的整数倍。如果区域的大小不是页的整数倍,则内核强制指定比区域稍大一点的尺寸作为映射的粒度。
对于驱动程序来说这些限制并不是什么大问题,因为访问设备的程序都是与设备相关的。由于程序必须知道设备的工作过程,因此程序员不会被诸如页边界之类的需求所困扰。在某些非 x86 平台上工作的 ISA 设备面临更大的制约,因为它们的 ISA 硬件视图是不连续的。比如一些 Alpha 计算机视 ISA 内存为不可直接映射的 8 位、16 位或者 32 位的离散项的集合。在这种情况下,根本无法使用 mmap。无法将 ISA 地址直接映射到 Alpha 地址,是由于这两种系统间,存在着不兼容的数据传输规则。虽然早期的 Alpha 处理器只能解决 32 位和 64 位内存访问问题,但是对于 ISA 来说只能进行 8 位和 16 位的传输,没有办法透明地将一个协议映射到另外一个协议上。
当灵活使用 mmap 时,它具有很大的优势。比如在 X 服务器例子中,它负责和显存间读写大量数据;与使用 lseek/write 相比,将图形显示映射到用户空间极大地提高了吞吐量。另外一个典型例子是控制 PCI 设备的程序。大多数 PCI 外围设备将它们的控制寄存器映射到内存地址中、高性能的应用程序更愿意直接访问寄存器,而不是不停的调用 ioctl 去获得需要的信息。
mmap 方法是 file_operations 结构的一部分,并且执行 mmap 系统调用时将调用该方法。使用 mmap,内核在调用实际函数之前,就能完成大量的工作,因此该方法的原型与系统调用有着很大的不同。它也与诸如 ioctl 和 poll 不同,内核在调用那些函数前不用做什么工作。
系统调用有着以下声明(在 mmap(2) 手册页中描述):
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 参数与第三章中介绍的一样,vma 包含了用于访问设备的虚拟地址的信息。因此大量的工作由内核完成;为了执行 mmap,驱动程序只需要为该地址范围建立合适的页表,并将 vma->vm_ops 替换为一系列的新操作就可以了。
有两种建立页表的方法:使用 remap_pfn_range 函数一次全部建立,或者通过 nopage VMA 方法每次建立一个页表。这两种方法有它各自的优势和局限性。这里首先介绍一次全部建立的方法,因为它最简单。从这开始,将会为实际的实现方法逐渐增加其复杂性:
(1)使用 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_pgoff 成员包含了用户需要的值。该函数对处于(pfn<
size
以字节为单位,被重新映射的区域大小。
prot
新 VMA 要求的 “保护(protection)” 属性。驱动程序能够(也应该)使用 vma->vm_page_prot 中的值。
remap_pfn_range 函数的参数非常简单,当调用 mmap 函数的时候,它们中大部分的值在 VMA 中提供。也许读者会奇怪,为什么会有两个函数呢? 第一个函数(remap_pfn_range)是在 pfn 指向实际系统 RAM 的时候使用,而 io_remap_page_range 是在 phys_addr 指向 I/O 内存的时候使用。在实际应用中,除了 SPARC 外,对每个体系架构这两个函数是等价的,而在大多数情况下会使用 remap_pfn_range 函数。对于有可移植性要求的驱动程序,要使用与特定情形相符的 remap_pfn_range 变种。
另外复杂性也表现在缓存上: 对设备内存的引用通常不能被处理器所缓存。系统的 BIOS 会正确设置缓存,但是也可以通过 protection 成员禁止缓存特定的 VMA。不幸的是,在这个层面上的禁止缓存是与处理器高度相关的。好奇的读者可以参考 drivers/char/mem.c 中的 pgprot_noncached 函数以了解其中细节。本书不对这个主题进行讨论。
(2)一个简单的实现
如果驱动程序要将设备内存线性地映射到用户地址空间中,程序员基本上就只需要调用 remap_pfn_range 函数。下面的代码来自 drivers/char/mem.c,并且揭示了在一个被称为 simple(Simple Implementation Mapping Pages with Little Enthusiasm) 的典型模块中,该任务是如何被完成的:
static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma) { if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot)) return -EAGAIN; vma->vm_ops = &simple_remap_vm_ops; simple_vma_open(vma); return 0; }
可见,重新映射内存就是调用 remap_pfn_range 函数创建所需的页表。
(3)为 VMA 添加操作
如上所述,vm_area_struct 结构包含了一系列针对 VMA 的操作。现在来看看如何简单实现这些函数。本节提供了针对 VMA 的 open 和 close 操作。当进程打开或者关闭 VMA 时,会调用这些操作;特别是当 fork 进程或者创建一个新的对 VMA 引用时,随时都会调用 open 函数。对 VMA 函数 open 和 close 的调用由内核处理,因此它们没有必要重复内核中的工作。它们存在的意义在于为驱动程序处理其他所需要的事情。
除此之外,一个诸如 simple 这样的简单驱动程序不需再做什么特别的事情了。这里创建了open 和 close 函数,它们负责向系统日志中输入信息,告诉系统它们被调用了。此外没有其他特殊用途了,不过它能告诉读者: 如何提供这些函数以及何时调用它们。
因此,代码中用调用 printk 的新操作,覆盖了默认的 vma->vm_ops:
void simple_vma_open(struct vm_area_struct *vma) { printk(KERN_NOTICE "Simple VMA open, virt %1x, phys %1x\n", vma->vm_start, vma->vm_pgoff << PAGE_SHIFT); } void simple_vma_close(struct vm_area_struct *vma) { printk(KERN_NOTICE "Simple VMA close.\n"); } static struct vm_operations_struct simple_remap_vm_ops = .open = simple_vma_open, .close = simple_vma_close, };
为了使这些操作对特定的映射有效,需要在相关 VMA 的 vm_ops 成员中保存指向 simple_remap_vm_ops 的指针。这通常在 mmap 方法中完成。如果回过头去看 simple_remap_mmap 示例,能看到如下代码:
vma->vm_ops = &simple_remap_vm_ops; simple_vma_open(vma);
请注意对 simple_vma_open 函数的显式调用。由于在原来的 mmap 中没有调用 open 函数,因此必须显式调用它才能使其正常运行。
Linux 设备驱动程序(三)(中):https://developer.aliyun.com/article/1597488