深入理解Linux内存管理 - 物理内存

Linux存在于各种体系结构上,所以描述内存需要架构独立的方法。本章会描述用来管理memory bank,页框的数据结构以及那些影响VM行为的flags

VM第一个重要的流行概念是Non Uniform Memory Access(NUMA)。对于大型机来说,不同banks内的内存由于和处理器的距离不同,访问代价也不同。比如,一个内存bank可以指定给每一个CPU,或者一个非常适合DMA操作的内存bank可以指定给它附近的设备或者卡。

我们把bank称为一个node,在linux系统中不管是Non Unifomr Memory Access(NUMA)还是Uniform Memory Access(UMA),都使用struct pglist_data (typedef pg_data_t)来表示一个节点。系统中的每一个节点都存放在链表pgdat_list中,node通过pg_data_t->node_next链接到下一个node。对于PC desktops这样的UMA结构,仅仅有一个称为contig_page_data的静态pg_data_t结构。我们将在2.1节继续讨论node

每个node又被划分为多个内存区,这些内存区称之为zone。struct zone_struct用来描述zone,zone有如下类型:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。不同类型的zone用做不同的场合。ZONE_DMA zone包含低端物理内存适合于那些无法访问超过16MB物理内存的设备。ZONE_NORMAL则被直接映射到kernel线性地址空间的低地址区。我们将在4.1节进一步讨论。ZONE_HIGHMEM则是那些无法直接映射到kernel空间的剩余内存。

对于X86机器,内存zones如下:

ZONE_DMA            First 16MiB of memory

ZONE_NORMAL    16MiB - 896MiB

ZONE_HIGHMEM   896MiB - End



内核大部分操作都是使用ZONE_NORMAL,所以ZONE_NORMAL是性能最关键的zone,在2.2节我们将进一步讨论这些zones。 系统内存是由固定尺寸的page frames组成,物理page frame由一个struct page表示,这些page structs都保存在全局mem_map数组中,mem_map通常存储在ZONE_NORMAL的起始位置或者内核镜像保留区域的后面。

我们会在2.4节详细讨论struct page,在3.7节详细讨论全局mem_map数组。图2.1演示了这些数据结构之间的关系。

因为可以被内核直接访问的内存数目(ZONE_NORMAL区大小)是有限的,Linux通过high memory支持更多的物理内存,high memory我们将在2.7 节讨论。在介绍high memory管理之前,先讨论nodes,zones和pages是如何表示的。

2.1 Nodes

之前已经提到过,每一个内存node由pg_data_t描述,也就是类型strcut pglist_data。当分配一个page时,Linux使用本地节点分配策略从距离当前CPU最近的node分配内存,因为进程很有可能也在这个CPU上运行。这个数据结构在<linux/mmzone.h>中定义

struct bootmem_data;
typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];
    struct zonelist node_zonelists[MAX_ZONELISTS];
    int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
    struct page *node_mem_map;
#ifdef CONFIG_CGROUP_MEM_RES_CTLR
    struct page_cgroup *node_page_cgroup;
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
    struct bootmem_data *bdata;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
    /*
     * Must be held any time you expect node_start_pfn, node_present_pages
     * or node_spanned_pages stay constant.  Holding this will also
     * guarantee that any pfn_valid() stays that way.
     *
     * Nests above zone->lock and zone->size_seqlock.
     */
    spinlock_t node_size_lock;
#endif
    unsigned long node_start_pfn;
    unsigned long node_present_pages; /* total number of physical pages */
    unsigned long node_spanned_pages; /* total size of physical page
                         range, including holes */
    int node_id;
    wait_queue_head_t kswapd_wait;
    struct task_struct *kswapd;
    int kswapd_max_order;
} pg_data_t;

数据结构成员简介

node_zones: 是这个节点内的zones,类型包括ZONE_HIGHMEM, ZONE_NORMAL, ZONE_DMA

node_zonelists: 这是node的分配顺序,在mm/page_alloc.c中的build_zonelists()函数创建这个node_zonelists。在ZONE_HIGHMEM分配失败,会尝试从ZONE_NORMAL分配,如果ZONE_NORMAL失败则尝试从ZONE_DMA分配。

nr_zones: 这个node包含的zone数目,通常是1 ~ 3。并不是所有的node都有三个zones,比如一个bank可能没有ZONE_DMA

node_mem_map:node由多个physical frame组成,每一个物理页框都有一个page结构表示,node_mem_map是第一个物理页框的page结构,这个page结构在全局mem_map数组的某个位置。

bdata:boot memory allocator使用这个成员,我们将在第五章讨论boot memory allocator

node_start_pfn:node起始物理页框号

node_present_pages:node包含的物理页面总数

node_spanned_pages:node物理页框的总尺寸,包括holes

系统中所有的nodes都通过list pgdat_list维护。在初始化函数init_bootmem_core()中创建这个list,我们将在5.3节讨论pgdat_list的初始化。在kernel 2.4.18之前,遍历pgdat_list链表的代码如下:

pg_data_t *pgdat;
pgdat = pgdat_list;
do {
    /* do something with pgdata_t */

} while ((pgdat = pgdat->node_next));

在最近的kernel版本,宏for_each_pgdat提供了遍历pgdat_list的功能。

2.2 Zones

每个zone通过struct zone_struct描述,struct zone_struct保存着页面使用的统计信息,空闲信息和locks,这个结构在<linux/mmzone.h>中声明:

typedef struct zone_struct {
    spinlock_t       lock;
    unsigned long    free_pages;
    unsigned long    pages_min, pages_low, pages_high;
    int              need_balance;

    free_area_t      free_area[MAX_ORDER];
    wait_queue_head_t *wait_table;
    unsigned long    wait_table_size;
    unsigned long    wait_table_shift;

    struct pglist_data *zone_pgdat;
    struct page      *zone_mem_map;
    unsigned long    zone_start_paddr;
    unsigned long    zone_start_mapnr;

    char             *name;
    unsigned long    size;
} zone_t;

数据结构成员简介:

lock:一个spinlock,保护对zone的并发访问

free_pages:zone内空闲pages的总数

pages_min, page_low, pages_high:这个zone的watermark,2.3节详细描述这几个watermark

need_balance:这个flag用来通知kswapd balance这个zone。当一个zone的可用页面数目达到zone watermark时,标记need_balance标志。

free_area:zone 空闲区位图,标识每一个page的使用情况,buddy分配器使用这个位图来进行分配和释放。

wait_table:page页面上进程等待队列的hash table,wait_on_page()和unlock_page函数将使用这个hsah table。我们将在2.2.3节讨论wait_table

wait_table_size:这个hash table的等待队列数目,是2的幂次方

wait_table_shift:

zone_pgdat:指向zone所在node 对应的结构pg_data_t

zone_mem_map:这个zone中第一个物理页框对应page

zone_start_paddr:这个zone的起始物理地址

zone_start_mapnr:第一个物理页框对应的page,在全局mem_map中的偏移

name:描述这个zone的字符串:“DMA”, “Normal”和“HighMem”

size:zone所包含的页框数目

2.2.1 Zone Watermarks

当系统的可用内存变低时,页面pageout守护进程kswapd被唤醒,开始释放空闲pages。在压力较高的情况下,守护进程立刻同步的释放内存,我们称之为direct-reclaim。这些控制pageout行为的参数在FreeBSD和Solaris都有类似实现。

....................................................................

系统根据zone内page使用情况,在处于不同的watermark时,会采取不同的动作

pages_low: 当空闲页面数目低于pages_low时,kswapd被buddy allocator唤醒释放pages。值缺省是pages_min的两倍。

pages_min: 当空闲页面数目低于pages_min时,allocator将代替kswapd同步释放pages,也就是直接回收

pages_high: 当kswapd被唤醒来释放pages时,回收的pages已经达到pages_high标记的页面数,kswapd将停止回收,进入休眠。缺省值一般为pages_min的三倍。

不管这些参数在类似系统中叫什么名字,作用都是类似的,辅助pageout守护进程或者线程进行页面回收。

2.2.2 Calculating the Size of Zones

zone的尺寸是在函数setup_memory中计算,如图2.3

PFN物理页框号,是物理页框在内存中的page 偏移。系统内的第一个PFN,保存在min_low_pfn,是loaded kernel镜像后面第一个page。这个值保存在mm/bootmem.c中

最后一个物理页框max_pfn是怎么计算的呢? 不同的系统计算方法不同

max_low_pfn是指低端最大物理页框号,用来标记ZONE_NORMAL的结尾。这是内核可以直接访问的最大物理内存,这个地址值和kernel/userspace地址空间划分相关。这个值保存在mm/bootmem.c中,对于低内存系统,max_pfn是等于max_low_pfn的。

使用min_low_pfn, max_low_pfn和max_pfn,可以很容易的计算出high memory的起始和结束地址 highstart_pfn, highend_pfn。

2.2.3 Zone Wait Queue Table

2.3 Zone Initialization

2.4 Initializing mem_map

mem_map 在系统启动时创建,在UMA系统中free_area_init()使用contig_page_data作为node,global mem_map作为这个ode的本地mem_map。

free_area_init_core()为当前节点分配一个本地mem_map。mem_map数组是通过boot memory 分配器alloc_bootmem_node来分配的。

2.5 Pages

系统内的每一个物理page frame都有一个相应的page来跟踪这个page的状态,在2.2kernel,这个结构和System V中的类似,但是对于其他的UNIX变种,这个结构做了写改变。struct page在<linux/mm.h>中定义:

typedef struct page {
    struct list_head list;
    struct address_space *mapping;
    unsigned long index;
    struct page *next_hash;
    atomic_t count;
    unsigned long flags;
    struct list_head lru;
    struct page **prev_hash;
    struct buffer_head *buffers;

#if defined(CONFIG_HIGHMEM) || defined(WAIT_PAGE_VIRTUAL)
    void *virtual;
#endif
} mem_map_t;

下面是page结构中成员变量的简介:

list:pages可以属于多种链表,这个list成员就是用来挂接到这些链表的。例如,一个映射的pages可以属于address_space中三个循环链表中的一个。他们是clean_pages, dirty_pages和locked_pages。在slab分配器中,当一个page已经被slab分配器分配,它被用来存储page所管理的slab和cache结构指针。它也可以用来把page内空闲block连接到一起

mapping:当文件和设备做内存mapped时,他们的inode有一个address_space。page的这个成员就指向这个address space如果pages属于这个文件。如果page 是匿名映射,那么address_space指向swapper_space。

index:这个成员有两个用途,具体是哪一个取决于page的状态。如果page是文件映射的一部分,那么index是文件内的页偏移。如果page是swap cache的一部分,那么index则是交换地址空间swapper_space内的偏移量。如果pages内的块正在被特定进程释放中,正在被释放block的order存放在index中,这个值被函数_free_pages_ok()设置。

next_hash:如果Pages作为文件映射的一部分,那么pages使用inode和offset做hash。这个next_hash把具有相同hash的pages链接到一起。

count:page的索引计数。如果变为0,那么这个page可以被释放。如果大于0,那么说明他被一个或者多个进程使用,或者被kernel使用中

flags:这个flags用来描述page的状态。这些状态标记在<linux/mm.h>中定义,表2.1中也列除了这些状态标记。

lru:对于page替换策略,active_list或者inactive_list链表中的pages可能会被交换出。lru是这些这些可交换pages的Least Recently Used 链表的链表头,我们将在chapter 10讨论这两个链表。

pprev_hash:和next_hash互为补充,形成一个双向链表。

buffers:如果一个page和一个块设备的buffer相关联,那么这个成员用来记录buffer_head。如果一个匿名page被交换文件后备时,那么这个page也有相应的buffer_head。因为这个page不得不同步数据到后备storage的block-size 块中。

virtual:正常情况下,仅仅ZONE_NORMAL的pages被kernel直接映射。对于ZONE_HIGHMEM的pages,kernel使用kmap来映射page,我们将在第九章进一步讨论。仅仅固定数目的pages可以被映射。当一个page被映射后,这个成员记录了page的虚拟地址

2.6 Mapping Pages to Zones

最近的kernel版本2.4.18,struct page增加了成员指针zone,指向page所在的zone。后来这个成员被认为没什么作用,因为即便是一个指针,对于大量的struct page,仍然消耗许多的内存。在最近的kernel,这个zone成员被去掉了。取而代之的是通过page->flags中的最高ZONE_SHIFT位来决定page属于哪个zone。这些位记录了page对应的zone在zone_table中的索引。

在系统启动时会建立zones的zone_table,zone_table在mm/page_alloc.c中声明如下:

zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES];
EXPORT_SYMBOL(zone_table);

MAX_NR_ZONES是一个node中zones的最大数目,比如3。MAX_NR_NODES是系统nodes的最大数目。函数EXPORT_SYMBOL使得zone_table可以被loadable modules访问。这个表就像一个多维数组。在free_area_init_core时,node内的所有pages都被初始化。首先把这个zone记录到zone_table中

zone_table[nid * MAX_NR_ZONES + j] = zone

nid是node的ID,j是zone在这个node中的索引,zone是zone_t结构,对于每一个page,都会执行函数set_page_zone

set_page_zone(page, nid * MAX_NR_ZONES + j)

参数@page的zone index被设置为nid * MAX_NR_ZONES+j

2.7 High Memory

因为内核可用的地址空间(ZONE_NORMAL)大小是有限的,所以kernel通过high  memory支持更多的物理内存。在32-bit x86系统上有两个阀值,一个是4GiB,一个是64GiB。

4GiB限制是32-bit物理地址最大可寻址空间,为了访问1GiB到4GiB范围的内存(1GiB这个下限并不是确定的,不仅和预留的vmalloc空间有关,而且还和kernel/user space的地址空间划分有关),kernel需要使用kmap临时映射pages到ZONE_NORMAL。我们将在第九章进一步讨论。

第二个限制64GiB是和PAE相关的,Intel的发明允许在32-bit系统上访问更多的RAM。它使用额外的位进行内存寻址,寻址范围可以达到2^36(64GiB)

虽然理论上PAE允许寻址范围是64GiB的,但是实际中linux进程无法访问那么多的RAM,因为虚拟地址空间仍然是4GiB。如果用户想在他们的进程内使用malloc分配所有的物理内存,那么是不可能的。

此外PAE也不允许kernel本身有那么多的RAM,结构page被用来描述每一个页框,结构page的大小为44字节,存放在ZONE_NORMAL这个虚拟地址空间中。这意味着1GiB的物理内存将占用11MiB的内存,而对于16GiB则是176MiB,这就导致ZONE_NORMAL变得更小。虽然目前看起来还不算太坏,但是我们再考虑其他小结构体,比如Page Table Entry(PTEs),那么在最坏的情况下需要16MiB(16GiB内存需要4M个PTEs)空间。这使得16MiB成为x86 linux可用物理内存的实际限制。如果需要访问更多的物理内存,给个简单明了的建议,使用64-bit系统。

详细页面:http://www.verydemo.com/demo_c167_i80129.html

猜你喜欢

转载自xieshuai19887893.iteye.com/blog/2075425