一般我们说内存,即指“物理内存”。那为什么本文要强调“物理内存”的概念呢?这是为了和“虚拟内存”的概念区分。常见的操作系统,例如Linux和windows,都是建立在存储映射机制的基础上。不同计算平台的“物理内存”大小不尽相同,其在操作系统初始化时,根据硬件情况确定,而“虚拟内存”的大小是由操作系统本身决定,例如32位系统的每一个用户态进程拥有的“虚拟内存”大小就是固定4G。
简单地说,同一个操作系统不管运行在什么机器上,每个进程拥有的虚拟内存空间都是固定的;而同一台机器不管在上面运行什么操作系统,其物理内存大小也是固定的。
用户应用程序始终使用虚拟地址,无需关心物理地址。虚拟地址到物理地址的映射则由操作系统来管理。操作系统正是借助这样的机制,隐藏了不同机器的内存差异,使得同一个应用程序可以运行在各种不同大小的物理内存的机器上。
Linux采用页式存储管理,顾名思义就是将物理内存分成一页一页的,内存分配和回收的最小单位即为一页。实际上Linux系统每一页的大小为4K,例如一台插有1G内存条的机器,则将“物理内存”分为256k个物理页面。
操作系统启动前,运行BIOS程序,这段程序初始化硬件,操作系统正是通过BIOS程序获取了物理内存大小。系统加载后,初始化过程创建了一个全局数据数组mem_map,每个数据元素为struct page。每一个struct page表示一个物理页面,page结构大小32 bytes,mem_map的数组大小即为物理页面的个数。
struct page虽然是按mem_map数组的顺序来存储,但是分配和回收物理页面显然不会采用数组顺序遍历的方式,因为数组的组织方式将所有页面都混合在一起不易管理,并且查找效率比较低。实际上在Linux系统中,mem_map中的每个page又被按所属管理区(DMA区域、普通区、高区)、是否被空闲、最近使用频率等因素分别重新组织到不同链表中。
那么Linux管理一个物理页面都需要哪些信息呢?一般容易想到,物理页面的大小,物理页面的物理地址范围。实际上述信息都不需要,原因是物理页面的大小固定4K bytes,并且每个页面的物理地址范围也可以根据页面大小和mem_map数组下标推算。Linux的page结构如下,可以看出,page结构本身并不复杂,其中主要包含三类字段。一类是链表指针,例如list、next_hash、pprev_hash、lru等;还有一类是物理页面本身的信息,例如index、count、flags、age、zone、virtual;最后一类是为物理页面映射到磁盘设置,例如mapping、buffer等。
struct page {
struct list_head list; // page链表
struct address_space *mapping; // 所属文件映射
unsigned long index; // mem_map数组下标
struct page *next_hash; // page组织成hash表
acomic_t count; // 页面使用计数
unsigned long flags; // 页是否脏、是否锁定等标志
struct list_head lru; // 最近最久未使用链表
unsigned long age;
wait_queue_head_t wait; // 等待队列链表
struct page **pprev_hash;
struct buffer_head * buffers; // 物理内存到块设备偏移
void *virtual; // 页的虚拟地址
struct zone_struct *zone; // 所属管理区
}
Linux系统将物理内存分成了几个管理区(DMA区域、NORMAL区、HIGHMEM区)。不同管理区的物理内存用途各不相同。
DMA区域就是给DMA控制器使用,不参与分配和回收。
NORMAL用作操作系统内核和用户进程正常映射的区域。这个区域线性映射到内核的虚拟地址空间,可以被内核直接访问。
需要注意的是,32位Linux系统当内存大小大于1G时,还可能存在HIGHMEM区内存。HIGHMEM区的存在是因为32位系统的内核虚拟地址空间只有1G,如果不设计这个区域,那么内核将无法访问高于1G的物理内存,这决定了HIGHMEM区不能永久映射到内核空间。另外,64位系统由于内核地址空间很多,就不存在HIGHMEM区。
Linux将内存的管理区用zone_struct表示,如下所示,可以将zone_struct理解为物理内存管理的顶层数据结构。由于系统中只存在2或3个管理区,因此系统全局只有2到3个zone_struct结构。
struct free_area {
struct list_head free_list; // 空闲区域链表
unsigned int *map; //
};
struct zone {
spinlock_t lock;
unsigned long free_pages;
unsigned long inactive_clean_pages;
unsigned long inactive_dirty_pages;
...
free_area_t free_area[MAX_ORDER]; // 空闲物理页面,分别表示大小为1,2,4,...,2MAX_ORDER个物理页面的连续内存
...
};
由于Linux物理内存分配的最小单位是页,这其中必然涉及到怎么分配物理内存,才能使得物理内存的利用率更高,内存碎片越少的问题。
伙伴系统
从struct zone中空闲内存结构free_area也可以看出,Linux将空闲内存按大小分别组织链接成不同链表。
物理内存分配
假如分配3个页面大小的物理内存,则优先选择从大小为1的空闲区域分别选择3块。如果大小为1的空闲区域没有3个,则从大小为2选择。总之就是优先选择小内存的空闲区域拼凑。当然前提是申请的内存没有连续的要求,如果要求连续,则先从大小为3的链表选取,如果没有再考虑将大内存拆到小内存。
物理内存回收
物理内存回收时,会查找回收区域上部和下部是否空间,如果空闲则将小内存与连续的空闲区域合并成大块空闲区域。