《从实践中学嵌入式 LINUX 操作系统》》
作者:华清远见
第 4 章 存储管理
本章简介
存储管理是操作系统的重要组成部分。Linux 操作系统采用了请求式分页虚拟
存储管理
方法
快递客服问题件处理详细方法山木方法pdf计算方法pdf华与华方法下载八字理论方法下载
。系统为每个进程提供了 4GB的虚拟内存空间。各个进程的虚拟内存
彼此独立。从计算机发展早期开始,就存在对大于系统中实际物理内存容量进行访
问的需要。为了克服这种限制,系统开发者
设计
领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计
了许多策略,其中最成功的就是虚
拟内存技术。本章主要讲解 Linux系统的存储管理方式。
专业始于专注 卓识源于远见
- 2 -
进程虚存空间的管理
Linux 运行在 X86 架构时,进程的虚拟内存为 4GB。进程虚存空间的划分在系统初始化时由 GDT 确
定,它定义在/arch/i386/kernel/head.S 文件中,代码如下:
.quad 0x0000000000000000 /* NULL 描述符 */
.quad 0x0000000000000000 /* 未使用*/
.quad 0xc0c39a000000ffff /* 内核代码段 1GB在 0xc0000000 */
.quad 0xc0c392000000ffff /* 内核数据段 1GB在 0xc0000000 */
.quad 0x00cbfa000000ffff /* 用户代码段 3GB在 0x00000000 */
.quad 0x00cbf2000000ffff /* 用户数据段 3GB在 0x00000000 */
.quad 0x0000000000000000 /* 未使用 */
.quad 0x0000000000000000 /* 未使用 */
.fill 2*NR_TASKS,8,0 /* 各个进程 LDT描述符和 TSS描述符的空间 */
Linux 的存储管理主要是管理进程虚拟内存的用户区。进程虚拟内存的用户区分为代码段、数据段、
堆栈,以及进程运行的环境变量、参数传递区域等。每个进程都用一个 mm_struct 结构体来定义它的虚存
用户区。mm_struct 结构体首地址在任务结构体 task_struct 成员项 mm 中。
4.1.1 进程的虚存区域
虚存区域是虚存空间中一个连续的区域,在这个区域中的信息具有相同的操作和访问特性。每个虚拟
区域用一个 vm_area_struct 结构体进行描述,它定义在/include/linux/mm_types.h 中,代码如下:
/*
* This struct defines a memory VMM memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
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. */
/* The first byte after our end address within vm_mm. */
unsigned long vm_end;
/* 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
专业始于专注 卓识源于远见
- 3 -
* 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_node; /* Serialized by anon_vma->lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
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
atomic_t vm_usage; /* refcount (VMAs shared if !MMU) */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};
其中的各个域说明如下。
vm_mm 指针指向进程的 mm_struct 结构体。
vm_start 和 vm_end 虚拟区域的开始和终止地址。
vm_flags 指出了虚存区域的操作特性。
VM_READ:虚存区域允许读取。
VM_WRITE:虚存区域允许写入。
VM_EXEC:虚存区域允许执行。
VM_SHARED :虚存区域允许多个进程共享。
VM_GROWSDOWN:虚存区域可以向下延伸。
VM_GROWSUP:虚存区域可以向上延伸。
VM_SHM:虚存区域是共享存储器的一部分。
VM_LOCKED:虚存区域可以加锁。
VM_STACK_FLAGS:虚存区域作为堆栈使用。
vm_page_prot 虚存区域的页面的保护特性。
若虚存区域映射的是磁盘文件或设备文件的内容,则 vm_inode 指向这个文件的 inode 结构体,否
则 vm_inode 为 NULL。
vm_offset 是该区域的内容相对于文件起始位置的偏移量,或相对于共享内存首址的偏移量。
所有 vm_area_struct 结构体链接成一个单向链
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
,vm_next 指向下一个 vm_area_struct 结构体。链
表的首地址由 mm_struct 中成员项 mmap 指出。
vm_ops 是指向 vm_operations_struct 结构体的指针。该结构体中包含指向各种操作的函数的指针。
所有 vm_area_struct 结构体组成一个 AVL 树(AVL 树是一种具有平衡结构的二叉树)。
vm_avl_left:左指针指向相邻的低地址虚存区域。
vm_avl_right:右指针指向相邻的高地址虚存区域。
mmap_avl:表示进程 AVL 树的根。
vm_avl_hight:表示 AVL 树的高度。
vm_next_share 和 vm_prev_share,把有关的 vm_area_struct 结合成一个共享内存时使用的双向链
表。
4.1.2 虚存空间的映射和虚存区域的建立
专业始于专注 卓识源于远见
- 4 -
虚拟内存(虚存)使计算机可以操纵更大的地址空间,还可以使系统中的每一个进程都有自己的虚拟
地址空间。这些虚拟的地址空间是相互完全分离的,所以,运行一个应用程序的进程不会影响另外的进程。
另外,硬件的虚拟内存机制允许对内存区写保护。这可以防止代码和数据被错误的程序覆盖。内存映射可
以将 CPU 的虚拟地址空间映射到系统的物理内存。
核心的共享虚拟内存机制,虽然允许进程拥有分离(虚拟)的地址空间,但有时也需要进程之间共享
内存。例如,系统中可能有多个进程运行命令解释程序 bash 。尽管可以在每一个进程的虚拟地址空间都
拥有一份 bash 的备份文件,但更好的方法是在物理内存中只拥有一份备份文件,所有运行 bash 的进程
共享代码。动态链接库是多个进程共享执行代码的另一个常见例子。另外,共享内存也经常用于进程间通
信机制,两个或多个进程可以通过共同拥有的内存交换信息。Linux 系统支持系统 V 的共享内存 IPC 机制。
Linux 虚存采用动态地址映射方式,即进程的地址空间和存储空间的对应关系是在程序的执行过程中
实现的。进程使用的是虚拟地址,因此,它对每个地址的访问都需通过 MMU 把虚拟地址转化为内存的物
理地址。
动态地址映射使 Linux 可以实现进程在主存中的动态重定位、虚存段的动态扩展和移动,也为虚存的
实现提供了基础。当 Linux 中的进程映像执行时,需要调入可执行映像的内容。但并不用把这些数据直接
调入物理内存,而只需要把这些数据放入该进程的虚拟内存区。只有当执行需要这些数据时才真正调入内
存。这种进程的映像和虚拟进程空间的连接称为内存映像。当需要将进程映像调入进程的虚拟内存空间时,
需要申请一段合适的虚拟内存空间,这时需要用到 mmap 系统调用来获得所需的内存空间。
Linux 使用 do_mmap()函数完成可执行映像向虚存区域的映射,由它建立有关的虚存区域 do_mmap()
函数定义在 include\linux\mm.h 文件中:
static inline unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long offset)
{
unsigned long ret = -EINVAL;
if ((offset + PAGE_ALIGN(len)) < offset)
goto out;
if (!(offset & ~PAGE_MASK))
ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
return ret;
}
addr 是虚存区域在虚拟内存空间的起始地址。
len 是这个虚存区域的长度。
file 是指向该文件结构体的指针,若 file 为 NULL,则称为匿名映射(Anonymous Mapping)。
offset 是相对于文件起始位置的偏移量。
prot 指定了虚存区域的访问特性。
PROT_READ 0x1:对虚存区域允许读取。
PROT_WEITE 0x2:对虚存区域允许写入。
PROT_EXEC 0x4:虚存区域(代码)允许执行。
PROT_NONE 0x0:不允许访问该虚存区域。
flag 指定了虚存区域的属性。
MAP_FIXED:指定虚存区域固定在 addr 的位置上。
MAP_SHARED:指定对虚存区域的操作是作用在共享页面上。
MAP_PRIVATE 指定了对虚存区域的写入操作将引起页面复制。
内存空间/地址类型
专业始于专注 卓识源于远见
- 5 -
CPU 地址空间(Address Space)是一段表示内存位置的地址范围。在 X86 架构下,地址空间有 3 种:
物理地址空间、线性地址空间和逻辑地址空间(虚拟地址空间)。而在 ARM 体系结构下只有物理地址空间
和逻辑地址空间(虚拟地址空间)。
物理地址是一个系统中可用的真实的硬件地址。例如,一个系统有 128MB 内存,它的合法地址范围
是 0~0x8000000(以十六进制表示)。每个地址都对应于所安装的 SIMMs 中的一组晶体管,而且对应于处
理器地址总线上的一组特定信号。
逻辑地址则是 CPU 所能处理的地址空间的总和,对于 32 位 CPU 而言,它的逻辑地址空间是 4G.B。
采用逻辑地址空间的好处是每个用户进程都有自己独立的运行空间,而不用管自己在物理内存的实际位
置。在 Linux 系统中,4GB 的地址空间由 Linux 内核与 Linux 应用程序共同分享,如图 4.1 所示。
图 4.1 内核空间与用户空间
用户空间使用 0x00000000~0xBFFFFFFF 共 3GB 的地址空间,用户态进程可以直接访问此空间。内核
空间则使用 0xC0000000~0xFFFFFFFF 剩下的 1GB 地址空间,存放内核访问的代码和数据,用户态进程不
能直接访问。用户进程只有通过中断或系统调用进入核心态时才有权利访问。
在逻辑地址和物理地址之间相互转换的工作是 CPU 的内存管理单元(MMU)完成的。Linux 内核负
责告诉 MMU 如何把逻辑页面映射到物理页面。通常,内核需要维护每个进程的逻辑地址和物理地址对照
表,在切换进程时,更新 MMU 的对照表信息。而 MMU 在进程提出内存请求时会自动完成实际的地址转
换工作。
在 X86 体系结构上,把线性地址映射到物理地址分为两个步骤,整个过程如图 4.2 所示。提供给进程
的线性地址被分为 3 个部分:一个页目录索引、一个页表索引和一个偏移量。页目录(Page Directory)是
一个指向页表的指针数组,页表(Page Table)是一个指向页面的指针数组,因此,地址映射就是一个跟踪
指针链的过程。一个页目录能够确定一个页表,继而得到一个页面,然后页面中的偏移量(Offset)能够
指出该页面中的一个地址。
为了进行更详细而准确的描述,给定页目录索引中的页目录项保存着存储在物理内存中的一个页表地
址,给定页表索引中的页表项保存着物理内存上相应物理页面的基地址,然后线性地址的偏移量加到这个
物理地址上形成最终物理页面内的目的地址。MMU 对线性地址的转换如图 4.2 所示。
目录 偏移量页
目录项 页表项
页表基址 页基址 偏移量
线性地址
物理地址
图 4.2 MMU线性地址到物理地址转换图
分页机制与MMU
Linux 的内存管理采用页式管理,使用多级页表,动态地址转换机构与主存、辅存共同实现虚拟内存。
每个用户进程拥有 4GB 的虚拟地址空间,进程在运行过程中可以动态浮动和扩展,为用户提供了透明的、
专业始于专注 卓识源于远见
- 6 -
灵活有效的内存使用方式。这正是进程被分配一个逻辑地址空间的原因之一。即使每个进程有相同的逻辑
地址空间,通过分页机制,相应进程的物理地址也都是不同的,因此,它们在物理上不会彼此重叠。
从内核的角度来看,逻辑和物理地址都被划分成固定大小的页面。每个合法的逻辑页面恰好处于一个
物理页面中,方便 MMU 的地址转换。当地址转换无法完成时(例如,由于给定的逻辑地址不合法或由于
逻辑页面没有对应的物理页面),MMU 将产生中断,向核心发出信号。Linux 核心可以处理这种页面错误
(Page Fault)问
题
快递公司问题件快递公司问题件货款处理关于圆的周长面积重点题型关于解方程组的题及答案关于南海问题
。
MMU 也负责增强内存保护,当一个应用程序试图在它的内存中对一个已标明是只读的页面进行写操
作时,MMU 也会产生中断错误,通知内核。在没有 MMU 的情况下,内核不能防止一个进程非法存取其
他进程的内存空间。
每个进程都有一套自己的页目录与页表,其中页目录的基地址是关键,通过它才能查到逻辑所对应的
物理地址。页目录的基地址是每个进程的私有资源,保存在该进程的 task_struct 对象的 mm_struct 结构变
量 mm 中。
mm_struct 结构在 include\linux\mm_types.h 中定义,task_struct 结构定义将在第 5 章进行介绍。
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 */
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);
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;
mm_counter_t _file_rss;
mm_counter_t _anon_rss;
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 */
cpumask_t cpu_vm_mask;
/* Architecture-specific MM context */
mm_context_t context;
unsigned int faultstamp;
unsigned int token_priority;
unsigned int last_interval;
unsigned long flags; /* Must use atomic bitops to access the bits */
专业始于专注 卓识源于远见
- 7 -
struct core_state *core_state; /* coredumping support */
/* aio bits */
rwlock_t ioctx_list_lock; /* aio lock */
struct kioctx *ioctx_list;
#ifdef CONFIG_MM_OWNER
struct task_struct *owner;
#endif
#ifdef CONFIG_PROC_FS
/* store ref to file /proc/
/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
};
在进程切换时,CPU 会把新进程的页目录基地址填入 CPU 的页目录寄存器,供 MMU 使用。当新进
程有地址访问时,MMU 会根据被访问地址的最高 10 位从页目录中找到对应的页表基地址,然后根据次高
10 位再从页表中找到对应的物理地址的页首,最后根据剩下的 12 位偏移量与页首地址找到逻辑地址对应
的真正物理地址。在 Linux 内核中,有关页目录与页表的操作已被定义成一套宏,使用起来非常方便。相
关定义都在文件中。
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
#define __pgd_offset(addr) pgd_index(addr)
提取被访问地址中用于在页目录中索引的部分。
#define pgd_offset(mm, addr) ((mm)->pgd+pgd_index(addr))
根据逻辑地址及页目录基地址找到对应的页目录项。
#define pte_none(pte) (!pte_val(pte))
判断页表项是否为空。
#define pte_clear(ptep) set_pte((ptep), __pte(0))
将对应页表项清为空。
#define pte_page(x) (virt_to_page(__va(pte_val((x)))))
找出页表项对应的页首地址。
#define mk_pte(page,pgprot)
({
pte_t __pte;
pte_val(__pte) = __pa(page_address(page)) + pgprot_val(pgprot); \
__pte;
})
根据页首地址和访问属性合成为一个合法的页表项。
static inline pte_t pte_modify(pte_t pte, pgprot_t newprot)
{
pte_val(pte) = (pte_val(pte) & _PAGE_CHG_MASK) | pgprot_val(newprot);
return pte;
}
用新的属性替代页表项中旧的属性。
高速缓存
Linux 使用了许多与高速缓存相关的内存管理策略。
专业始于专注 卓识源于远见
- 8 -
1.缓冲区高速缓存
缓冲区高速缓存中包含被块设备驱动使用的数据缓冲,这些缓冲单元的大小一般都固定(如 512 B),
包含从块设备读出或写入的信息块。块设备是仅能够以固定大小块进行读/写操作的设备,所有的硬盘都是
块设备。利用设备标识符和所需块号作索引可以在缓冲区高速缓存中迅速找到数据块。块设备只能够通过
缓冲区高速缓存来存取。如果数据在缓冲区缓存中可以找到,则无须从物理块设备(如硬盘)中读取,这
样可以加速设备的访问速度。
2.页高速缓存
页高速缓存用来加速块设备上可执行映像文件与数据文件的存取。它每次缓冲一个页面的文件内容。
页面从块设备上读入内存后放入页高速缓存中。
3.交换高速缓存
只有修改过的页面才存储在交换文件中。如果这些页面在写入到交换文件后没有被修改,则下次被交
换出内存时就不必再进行更新写操作。这些页面都可以丢弃在交换频繁发生的系统中。交换高速缓存可以
减少很多不必要且耗时的块设备操作。
4.硬件高速缓存
常见的硬件高速缓存是处理器中的指令和数据 Cache,它缓存 CPU 最近访问过的指令和数据,使 CPU
不需要到内存中获取数据。Cache 是 CPU 与内存之间的桥梁。目前,常见的 CPU 中 Cache 的实现按读/写
方式分类,不外乎如下几类。
1)贯穿读出式(Look Through)
该方式将 Cache 隔在 CPU 与主存之间,CPU 将主存的所有数据请求都首先送到 Cache,由 Cache 自行
在自身查找。如果命中,则切断 CPU 对主存的请求,并将数据送出;如果没有命中,则将数据请求传给
主存。该方法的优点是降低了 CPU 对主存的请求次数,缺点是延迟了 CPU 对主存的访问时间。
2)旁路读出式(Look Aside)
在这种方式中,CPU 发出数据请求时,并不是单通道地穿过 Cache,而是向 Cache 和主存同时发出请
求。由于 Cache 速度更快,如果命中,则 Cache 在将数据回送给 CPU 的同时,还来得及中断 CPU 对主存
的请求;如果没有命中,则 Cache 不做任何动作,由 CPU 直接访问主存。
它的优点是没有时间延迟,缺点是每次 CPU 对主存的访问都存在,这样,就占用了一部分总线时间。
3)写穿式(Write Through)
任一从 CPU 发出的写信号送到 Cache 的同时,也写入主存,以保证主存的数据能同步更新。它的优
点是操作简单,但由于主存的速度慢,从而降低了系统的写速度并占用了总线的时间。
4)回写式(Copy Back)
为了克服贯穿读出式中每次数据写入时都要访问主存,从而导致系统写速度降低并占用总线时间的弊
病,尽量减少对主存的访问次数,又有了回写式。它的工作原理为:数据一般只写到 Cache,这样有可能
出现 Cache 中的数据得到更新而主存中的数据不变(数据陈旧)的情况。但此时可在 Cache 中设定一个标
识地址及数据陈旧的信息,只有当 Cache 中的数据被再次更改时,才将原更新的数据写入主存相应的单元
中,然后再接受再次更新的数据。这样保证了 Cache 和主存中的数据不产生冲突。
内存区域 Zone
Linux 通过伙伴算法管理和分配页,但由于硬件的原因,内存中的不同区域会有不同的特性。主要有
以下两个问题:
专业始于专注 卓识源于远见
- 9 -
一些硬件只能用某些特定的内存地址来执行 DMA。
一些体系结构中有一些内存不能永久映射到内核空间上。
因此某些内存必须从特定区域上分配,不能由单一的伙伴系统管理。为了区分这些内存区域,Linux
使用了 3 个 Zone,每个 Zone 由一个自己的伙伴系统来管理,如下:
ZONE_DMA 包含可以用来执行 DMA 操作的内存。
ZONE_NORMAL 包含可以正常映射到虚拟地址的内存区域。
ZONE_HIGHMEM 包含不能永久映射到内核地址空间的内存区域。
这些区域的划分和具体的体系结构有关,例如,某些体系结构中 ZONE_NORMAL 覆盖了所有的内存
区域,而另外两个区均为空。在 X86 系统中,ZONE_DMA 为 0~16MB 的内存范围,ZONE_HIGHMEM 包
含了所有高于 896MB 的物理内存,ZONE_NORMAL 覆盖其余部分。这个规定可在中找
到,源代码如下:
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
/*
* x86_64 needs two ZONE_DMAs because it supports devices that are
* only able to do DMA to the lower 16M but also 32 bit devices that
* can only do DMA areas below 4G.
*/
ZONE_DMA32,
#endif
/*
* Normal addressable memory is in ZONE_NORMAL. DMA operations can be
* performed on pages in ZONE_NORMAL if the DMA devices support
* transfers to all addressable memory.
*/
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
__MAX_NR_ZONES
};
这些内存区域被分别管理,而在分配时,不同的任务会从不同的区域分配,例如,DMA 任务只能从
ZONE_DMA 中分配内存,而普通的内存请求则会依次尝试从 ZONE_NORMAL、 ZONE_HIGHMEM 和
ZONE_DMA 中分配。在分配器看来,这 3 个区只是 3 个不同的内存池对象,用相同的方法即可进行处理。
zone 结构表示区在文件中定义,它包含了该伙伴系统的所有信息。zone 结构如下:
struct zone {
/* Fields commonly accessed by the page allocator */
unsigned long pages_min, pages_low, pages_high;
unsigned long lowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
int node;
/*
* zone reclaim becomes active if more unmapped pages exist.
*/
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
struct per_cpu_pageset *pageset[NR_CPUS];
#else
struct per_cpu_pageset pageset[NR_CPUS];
#endif
/*
* free areas of different sizes
*/
spinlock_t lock;
专业始于专注 卓识源于远见
- 10 -
#ifdef CONFIG_MEMORY_HOTPLUG
/* see spanned/present_pages for more description */
seqlock_t span_seqlock;
#endif
struct free_area free_area[MAX_ORDER];
#ifndef CONFIG_SPARSEMEM
/*
* Flags for a pageblock_nr_pages block. See pageblock-flags.h.
* In SPARSEMEM, this map is stored in struct mem_section
*/
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
ZONE_PADDING(_pad1_)
/* Fields commonly accessed by the page reclaim scanner */
spinlock_t lru_lock;
struct {
struct list_head list;
unsigned long nr_scan;
} lru[NR_LRU_LISTS];
unsigned long recent_rotated[2];
unsigned long recent_scanned[2];
unsigned long pages_scanned; /* since last reclaim */
unsigned long flags; /* zone flags, see below */
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
int prev_priority;
/*
* The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on
* this zone's LRU. Maintained by the pageout code.
*/
unsigned int inactive_ratio;
…
ZONE_PADDING(_pad2_)
/* Rarely used or read-mostly fields */
wait_queue_head_t * wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
/*
* Discontig memory support fields.
*/
struct pglist_data *zone_pgdat;
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;
…
unsigned long spanned_pages; /* total size, including holes */
unsigned long present_pages; /* amount of memory (excluding holes) */
/*
* rarely used fields:
*/
const char *name;
} --cacheline_internodealigned_in_smp;
其中,zone_mem_map 成员用来指向所有这个 zone 管理到 page 数组集合。
专业始于专注 卓识源于远见
- 11 -
获得内存页面
系统中对于物理页有大量的需求,当程序映像加载到内存中时,操作系统需要分配页;当程序结束执
行并卸载时,操作系统需要释放这些页。另外,为了存放相关的数据结构(如页表自身),也需要物理页。
这种用于分配和回收页的机制和数据结构对于维护虚拟内存子系统的效率是非常重要的。
系统中的所有物理页都使用 page 数据结构来描述。每一个物理页对应一个 page 变量。一个 zone 的所
有 page 变量集合形成数组,由 zone 的 zone_mem_map 成员指针指向数组的起始地址,页数组的初始化在
系统启动时完成。
页分配器的算法是在伙伴系统之上的,伙伴系统将内存区域组织为以页为单位的块,n 称为该块的“级
别”,具有相同级别的块用链表接在一起。每次分配必须指定一个级别,并以该级别块的大小为单位。
在分配时,依次从级别 n 到最大级别开始搜索,直到找到一个非空的块为止。如果这个非空块级别不
是 n,则将其拆成两份,一份放到其对应的级别的空闲块中,另一份如果还不是级别 n 就继续拆分,直到
最后返回那个级别为 n的块。
在回收时,首先计算出被回收的块的伙伴,然后查看这个伙伴是否在级别为 n 的空闲链中。如果找到
了,则将这个块与这个伙伴合并(伙伴从空闲链删除,并修整“当前块”的位置即可),然后 n := n+1,继
续这个合并过程。当伙伴不在该空闲链中时,合并过程结束。free_area 所管理的内存如图 4.3 所示。
2 0
2 1
2 2
2 3
2 4
2 5
2 6
2 7
2 8
2 9
zone_mem_map[]
free_area[] bitmaps
图 4.3 free_area管理内存结构图
上面的算法可以非常有效地分配和回收内存,但同时也限制了分配的灵活性(页面数必须是 2 的指数)。
zone 中与这个算法相关的 field 主要是:
struct free_area free_area[MAX_ORDER];
这里就是各个级别的空闲链的入口。free_area 结构是一个非常简单的结构,其中包括一个链表项和该
级别的链表元素数目:
struct free_area {
struct list_head free_list;
unsigned long nr_free;
};
而这个 free_list 所指向的对象则是 struct page 中的一个链表成员 lru,通过它将 page 串在一起组成该级
别的空闲链。free_list->next 和 free_list->prev 分别指向空闲链的头和尾。当级别 n > 0 时,这个块含有一个
以上的 page,而这时只使用块中的首个 page 来进行链接,其他(连续相邻的)page 则隐含地存在于这个
块中,数据结构如图 4.4 所示(图中的链接实际上都是双链,一组小括号表示一个 page 结构)。
专业始于专注 卓识源于远见
- 12 -
图 4.4 数据结构
伙伴系统的数据结构由 mm/page_alloc.c 中的各个函数来建立和维护。主要包括下面几个函数:
struct page *__alloc_pages_internal(gfp_t gfp_mask, unsigned int order,
struct zonelist *zonelist, nodemask_t *nodemask)
{
const gfp_t wait = gfp_mask & __GFP_WAIT;
enum zone_type high_zoneidx = gfp_zone(gfp_mask);
struct zoneref *z;
struct zone *zone;
struct page *page;
struct reclaim_state reclaim_state;
struct task_struct *p = current;
int do_retry;
int alloc_flags;
unsigned long did_some_progress;
unsigned long pages_reclaimed = 0;
might_sleep_if(wait);
if (should_fail_alloc_page(gfp_mask, order))
return NULL;
restart:
z = zonelist->_zonerefs; /* the list of zones suitable for gfp_mask */
if (unlikely(!z->zone)) {
/*
* Happens if we have an empty zonelist as a result of
* GFP_THISNODE being used on a memoryless node
*/
return NULL;
}
page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
zonelist, high_zoneidx, ALLOC_WMARK_LOW|ALLOC_CPUSET);
if (page)
goto got_pg;
if (NUMA_BUILD && (gfp_mask & GFP_THISNODE) == GFP_THISNODE)
goto nopage;
for_each_zone_zonelist(zone, z, zonelist, high_zoneidx)
wakeup_kswapd(zone, order);
alloc_flags = ALLOC_WMARK_MIN;
if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait)
alloc_flags |= ALLOC_HARDER;
if (gfp_mask & __GFP_HIGH)
alloc_flags |= ALLOC_HIGH;
if (wait)
alloc_flags |= ALLOC_CPUSET;
page = get_page_from_freelist(gfp_mask, nodemask, order, zoneli