Linux页框管理

在前面的博文里,我们讲解了基于80x86体系的Linux内核分段和分页机制,并详细地讨论了Linux的内存布局。有了这些基本概念以后,我们就来详细讨论内核如何动态地管理那些可用的内存空间。

 

对于80386这种32位的处理器结构,Linux采用4KB页框大小作为标准的内存分配单元。内核必须记录每个页框的当前状态,例如,区分哪些页框包含的是属于进程的页,而哪些页框包含的是内核代码或内核数据。内核还必须能够确定动态内存中的页框是否空闲,如果动态内存中的页框不包含有用的数据,那么这个页框就是空闲的。在以下情况下页框是不空闲的:包含用户态进程的数据、某个软件高速缓存的数据、动态分配的内核数据结构、设备驱动程序缓冲的数据、内核模块的代码等等。

 

内核用数据结构page描述一个页框的状态信息,所有的页描述符存放在全局mem_map数组中,其数组的下标为页框号(pfn)。因为每个描述符长度为32字节,所以mem_map所需要的空间略小于整个RAM的1%。

 

那么一个页描述符怎样与一个占据4k的页框相联系(映射)呢?有了mem_map数组,这个问题就很简单了。因为如果知道了page数据的地址pd,用pd去减去mem_map就得到了pd的页框号pfn。那么这个物理页的物理地址是physAddr = pfn << PAGE_SHIFT  。


在得知该物理页的物理地址是physAddr后,就可以视physAddr的大小得到它的虚拟地址:
1.physAddr < 896M  对应虚拟地址是 physAddr + PAGE_OFFSET   (PAGE_OFFSET=3G)
2.physAddr >= 896M 对应虚拟地址不是静态映射的,通过内核的高端虚拟地址映射得到一个虚拟地址。

 

在得到该页的虚拟地址之后,内核就可以正常访问这个物理页了。

 

内核提供一个virt_to_page(addr)宏来产生线性地址addr对应的页描述符地址。pfn_to_page(pfn)宏产生与页框号pfn对应的页描述符地址。相反,也提供page_to_pfn(pg)宏来产生页描述符对应的页的页框号pfn。注意,针对80x86结构,上述宏并不是直接通过men_map数组来确定页框号的,而是通过内存管理区的zone_mem_map来确定的,不过原理是一样的:

#define page_to_pfn(pg)       /
({         /
 struct page *__page = pg;     /
 struct zone *__zone = page_zone(__page);   /
 (unsigned long)(__page - __zone->zone_mem_map)   /
  + __zone->zone_start_pfn;    /
})

 

这里千万要注意!不要混淆一个概念。这里的physAddr虽然表示物理地址,但是并不能说明该地址的数据就一定存在于物理内存中。那么如何判断这个页到底在不在内存中呢?你看,前面的知识就用到了——分页机制。也就是说,如果这个页因为各种各样五花八门的原因被交换出去了,那么它对应的页的Present标志就为0。这里就牵涉到缺页异常了,要深入了解,请关注笔者后面的博文。

 

在这里我们只需要对数据结构page详细讨论以下两个字段:
1、_count:页的引用计数器。如果该字段为-1,则相应页框空闲,并可被分配给任一进程或内核本身;如果该字段的值大于或等于0,则说明页框被分配给一个或多个进程,或用于存放一些内核数据结构。page_count()函数返回_count加1后的值,也就是该页的使用者的数目。
2、flags:包含多达32个用来描述页框状态的标志。对于每个PG_xyz标志,内核都定义了操纵其值的一些宏。通常,PageXyz宏返回标志的值,而SetPageXyz和ClearPageXyz宏分别设置和清除相应的位。

 

标志名

含义

PG_locked

页被锁定,例如,在磁盘I/O操作中涉及的页。

PG_error

在传输页时发生错误

PG_referenced

刚刚访问过的页

PG_uptodate

在完成读操作后置位,除非发生磁盘I/O 错误

PG_dirty

页已经被修改

PG_lru

页在活动或非活动页链表中

PG_active

页在活动页链表中

PG_slab

包含在slab 中的页框

PG_highmem

页框属于ZONE_HIGHMEM 管理区

PG_checked

由一些文件系统(如Ext2 Ext3)使用的标志

PG_arch_1

80x86 体系结构上没有使用

PG_reserved

页框留给内核代码或没有使用

PG_private

页描述符的private字段存放了有意义的数据

PG_writeback

正在使用writepage方法将页写到磁盘上

PG_nosave

系统挂起 / 唤醒时使用

PG_compound

通过扩展分页机制处理页框

PG_swapcache

页属于对换高速缓存

PG_mappedtodisk

页框中的所有数据对应于磁盘上分配的块

PG_reclaim

为回收内存对页已经做了写入磁盘的标记

PG_nosave_free

系统挂起 / 恢复时使用

 

系统是怎么为进程或内核分配一个内存空间,或者说怎么给他们分配一个线性页描述符所对应线性地址的页面呢?这个需要借助内核的分区页框分配器和伙伴系统算法。在讨论这些细节之前,先介绍一些必要的概念。

 

1 非统一内存访问(NUMA)架构

 

Linux2.6支持非统一内存访问(NUMA)模型,在这种模型中,给定CPU对不同内存单元的访问时间可能不一样。系统的物理内存被划分为几个节点(node)。在一个单独的节点内,任一给定CPU访问页面所需要的时间都是相同的,而对于不同的CPU,这个时间就不同。对每个CPU而言,内核都试图把耗时节点的访问次数减到最少,这就必须要将那些CPU最常引用的内核数据结构的存放位置选好。

 

每个节点由一个类型为pg_data_t的描述符表示,所有节点的描述符存放在一个单向链表中,它的第一个元素由内核全局变量pgdat_list指向。在x86体系中,即使是多核,内存访问时间也是相同的,所以不需要NUMA,但是内核还是使用节点,不过,这只是一个单独的节点,它包含了系统中所有的物理内存。因此,pgdat_list变量指向一个链表,此链表只有一个元素组成的,这个元素就是节点0描述符,它被存放在contig_page_data变量中。

 

pg_data_t描述符中要注意到的三个字段分别是node_zones、node_zonelists、node_mem_map,分别是zone_t[]、zonelist_t[]和page类型。前两个是用来描述内存管理区的,下面马上要谈到;node_mem_map是本节点所有页的页描述符数组。内核将这三个字段放在里边,就是为内存区、页框建立一些列的联系。

 

2 内存管理区

 

由于Linux内核必须处理80x86体系结构中的两种硬件约束:
(1)ISA总线的直接内存存取(DMA)处理器有一个严格的限制:他们只能对RAM的前16MB寻址。
(2)在具有较大容量RAM的现代32位计算机中,CPU不能直接访问所有的物理内存,因为线性地址空间太小。

 

为了应对这两种限制,Linux2.6把每个内存节点的物理内存划分成3个管理区(zone)。在80x86的UMA体系结构中的管理区分为:
ZONE_DMA:包含低于16MB的内存页框
ZONE_NORMAL:包含高于16MB而低于896MB的内存页框
ZONE_HIGHMEM:包含从896MB开始高于896MB的内存页框

 

ZONE_DMA和ZONE_NORMAL区包含内存“常规”页框,通过把他们线性地址映射到线性地址空间的第4个GB,内核就可以直接进行访问。ZONE_HIGHMEM区包含的内存页不能由内核直接访问,尽管它们也可以通过高端内存内核映射,线性映射到线性地址空间的第4个GB。

 

每个内存管理区都有自己的描述符zone_t,其字段中很多用于回收页框时使用。其实每个页描述符page都有到内存节点和到内存节点管理区的链接。那我们为啥看不到呢,原因是为了节省空间,这些链接的存放方式和典型的指针不同,是被编码成索引存放在flags字段的高位。

 

 zone_t字段如下:

类型

名称

说明

unsigned long

free_pages

管理区中空闲页的数量

unsigned long

pages_min

管理区中保留页的数目

unsigned long

pages_low

回收页框使用的下界;同时也被管理区分配器作为阈值使用

unsigned long

pages_high

回收页框使用的上界;同时也被管理区分配器作为阈值使用

unsigned long []

lowmem_reserve

指明在处理内存不足的临界情况下每个管理区必须保留的页框数目

struct per_cpu_pageset[]

pageset

数据结构用于实现单一页框的特殊高速缓存

spinlock_t

lock

保护该描述符的自旋锁

struct free_area []

free_area

标识出管理区中的空闲页框块

spinlock_t

lru_lock

活动以及非活动链表使用的自旋锁

struct list head

active_list

管理区中的活动页链表

struct list head

inactive_list

管理区中的非活动页链表

unsigned long

nr_scan_active

回收内存时需要扫描的活动页数目

unsigned long

nr_scan_inactive

回收内存时需要扫描的非活动页数目

unsigned long

nr_active

管理区的活动链表上的页数目

unsigned long

nr_inactive

管理区的非活动链表上的页数目

unsigned long

pages_scanned

管理区内回收页框时使用的计数器

int

all_unreclaimable

在管理区中填满不可回收页时此标志被置位

int

temp_priority

临时管理区的优先级(回收页框时使用)

int

prev_priority

管理区优先级,范围在12 0 之间(由回收页框算法使用)

wait_queue_head_t *

wait_table

进程等待队列的散列表,这些进程正在等待管理区中的某页

unsigned long

wait_table_size

等待队列散列表的大小

unsigned long

wait_table_bits

等待队列散列表数组大小,值为2order

struct pglist_data *

zone_pgdat

内存节点

struct page *

zone_mem_map

指向管理区的第一个页描述符的指针

unsigned long

zone_start_pfn

管理区第一个页框的下标

unsigned long

spanned_pages

以页为单位的管理区的总大小,包括洞

unsigned long

present_pages

以页为单位的管理区的总大小,不包括洞

char *

name

指针指向管理区的传统名称:“DMA”,“NORMAL”或“HighMem

 

实际上,刻画页框的标志的数目是有限的,因此保留flags字段的最高位来编码内存节点和管理区是绰绰有余的。Linux提供page_zone()函数用来接收一个页描述符的地址作为它的参数;它读取该描述符中的flags字段的最高位,然后通过查看zone_table数组来确定相应管理区描述符的地址。顺便提一下,在系统启动时用,内核将所有内存节点的所有管理区描述符的地址放到这个zone_table数组里边。

 

当内核调用一个内存分配函数时,必须指明请求页框所在的管理区。内核通常指明它愿意使用哪个管理区。为了在内存分配请求中指定首选管理区,内核使用zonelist数据结构,这就是管理区描述符指针数组,在80x86中只有三个zone,所以zonelist数据结构中指向这三个zone的指针按照一定规则排列。如图,则zonelist数组就是这三个zone的排列组合。

例如,要分配一个用来做DMA的页框,则在指定zonelist数组中的某个zonelist元素中获得首选的zone,应该是ZONE_DMA,如果该区空间已使用完,就选ZONE_NORMA区,随后再是ZONE_HIGHMEM。

你可能感兴趣的:(疯狂内核之内存管理)