linux内核中内存管理数据结构关系及机制(原题:linux虚拟内存组织结构浅析)

来源:互联网 发布:ubuntu opera flash 编辑:程序博客网 时间:2024/05/05 07:07

http://blog.csdn.net/gxfan/article/details/2954928

众所周知,linux内核支持绝大多数体系结构,因此linux内核必须采取一种与具体体系结构无关的方法来描述物理内存的组织结构,这个问题就是本系列文章要讨论的话题。

 

要理解linux虚拟内存在逻辑上的组织结构,我们首先要明白两个概念:UMA(Uniform Memory Access)、NUMA(Non Uniform Memory Access)。UMA指一致性内存访问,这是单CPU机器常用的体系结构,在这种结构下,CPU访问系统内存的任何存储位置的代价都是一样的;而NUMA是指非一致性内存访问,常用于多CPU机器,在这种体系结构中,不同内存相对于不同的CPU而言所处的位置不一样,最典型的就是每个CPU都有自己的本地内存(Local Memory),不同CPU之间通过总线连接起来,如图1所示。在这种结构中,CPU访问本地内存的代价比访问远端的内存代价要小。

 

 

为了支持NUMA,Linux将物理内存划分成不同的节点(node),节点用结构体pg_data_t表示,以上图为例,图中每个CPU的本地物理内存都称为一个节点;即使在UMA结构中也有节点的概念,此时系统中就只有一个节点;系统中的多个节点被连接起来保存在一个称为pgdat_list的链表上。每个节点又被划分成不同的区(zone),节点是通过其结构体内的数组node_zones来跟踪节点内的区的。区是指一个节点内一段连续的物理内存范围。Linux中主要有3个区:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM,它们的划分如下:ZONE_DMA位于物理内存开始的一段区域内,主要用来供一些ISA设备使用;ZONE_NORMAL位于ZONE_DMA后面,这个区域被内核直接映射到线性地址的高端部分;ZONE_HIGHMEM指系统中剩下的物理内存,这个区域不能直接被内核映射。为了清晰起见,我们可以看看在x86平台上的区是如何划分的:

                     ZONE_HIGHMEM —— 物理内存起始16M

                     ZONE_NORMAL —— 16M—896M

                     ZONE_HIGHMEM —— 896M—物理内存结束

 

  最后,在每个区中都有一个指向mem_map数组中某个单元的指针zone_mem_map,这是干什么的呢?通过查看源代码我们可以知道,mem_map是类型为struct page的数组,而Linux内核正是利用struct page结构体来描述每个物理内存页的,在系统启动时,内核就会为整个系统的内存建立好一个全局的页描述数组mem_map,在以后的运行过程中,Linux内核最终就是根据这个全局内存描述数组来控制对物理内存的分配、回收等操作的。明白了mem_map的作用,我们再来看看每个区中的zone_mem_map指针,这个指针指向的是mem_map中的某一个单元,而这个单元的内容恰恰描述了这个区内的第一页物理内存,这样Linux就把节点、区、page结构联系起来了,它们共同组织起来完成了对系统所有物理内存的描述。因此,我们现在就可以清晰地得出Linux在逻辑上是如何描述所有的物理内存了,其图如图2所示:


Linux虚拟内存组织结构浅析(二)

在前一篇文章中我们介绍了Linux虚拟内存在逻辑上的组织结构,现在就让我们从源代码入手,从程序级仔细看看各个数据结构体的内部组成如何,源代码来自于最新的kernel2.6.26.5,分析过程中主要参考了《Understanding the linux virtual memory》这本书,有兴趣的朋友可以去阅读一下。

 

一、节点的数据表示

在内核中,节点由结构体pg_data_t来表示,它是由结构体pglist_data通过typedef来定义的,位于文件<linux/mmzone.h>,其内容如下:

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

   struct page *node_mem_map;

#endif

   struct bootmem_data *bdata;

#ifdef CONFIG_MEMORY_HOTPLUG

   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:包含本节点内区的描述结构体,一般而言MAX_NR_ZONES为3。

node_zonelists:这个数组指明了在分配内存时区的选择顺序。

nr_zones:指明本节点内区的个数,这一般都是1到3中的一个数字。并不是所有的节点都有3个区,比如有些节点就没有ZONE_DMA区。

node_mem_map:指向全局内存描述数组中的某一项,该元素描述了本节点的第一页物理内存。

bdata:指向bootmem_data结构体,该结构体用于系统启动时的内存分配器分配、管理该节点的内存。该内存分配器在系统启动完毕后不再使用。

node_size_lock:该自旋锁用于保护node_start_pfn、node_present_pages以及node_spanned_pages,当你要取得这些变量的值时,必须首先获取这个自旋锁。当你调用pfn_valid()前也应当获得这个自旋锁。并且注意必须在zone->lock和zone->size_seqlock前获取这个自旋锁。

node_start_pfn:这个是该节点内第一页物理内存在全局数组mem_map中的序号。

node_present_pages:当前该节点内可获得的物理页总数。

node_spanned_pages:节点内可访问的所有物理页数,包含可能存在的空洞页。

node_id:当前节点的编号,从0开始。

kswapd_wait:kswapd线程的等待队列。kswapd是一个内核线程,当系统中内存页较少时它负责回收物理内存页,这个线程常常处于睡眠状态。

kswapd:指向kswapd线程的进程描述符。

kswapd_max_order:kswapd线程要释放内存块大小取对数后的值。

 

二、区的数据表示

每个区由结构体struct zone来描述,其中包含了页使用统计、空闲页数、琐等信息,这个结构体在<linux/mmzone.h>中定义:

struct zone {

   unsigned long       pages_min, pages_low, pages_high;

   unsigned long       lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA

   int node;

   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

   spinlock_t      lock;

#ifdef CONFIG_MEMORY_HOTPLUG

   seqlock_t       span_seqlock;

#endif

   struct free_area    free_area[MAX_ORDER];

 

#ifndef CONFIG_SPARSEMEM

   unsigned long       *pageblock_flags;

#endif /* CONFIG_SPARSEMEM */

 

   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       pages_scanned;     /* since last reclaim */

   unsigned long       flags;

   atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];

   int prev_priority;

   wait_queue_head_t   * wait_table;

   unsigned long       wait_table_hash_nr_entries;

   unsigned long       wait_table_bits;

   struct pglist_data  *zone_pgdat;

   unsigned long       zone_start_pfn;

   unsigned long       spanned_pages; 

   unsigned long       present_pages; 

   const char      *name;

} ____cacheline_internodealigned_in_smp;

 

各字段含义如下:

pages_min:管理区中保留的页数,当区中空闲的页框数达到这个值时kswapd线程会被唤醒并以同步的方式回收区中的页框。

pages_low:回收页框的下限,当区中空闲的页框数达到这个值时,伙伴分配器会唤起kswapd线程开始回收页框。

pages_high:回收页框的上限,当kswapd线程被唤醒回收页框时,它会一直回收页框直到区中空闲的页框数达到这个值为止。

lowmem_reserve:指明在靠近内存低端的区内需要保留的页框数,这个主要是为了防止这样一种情况:当内存低端的页框全部被分配后,如果这时候有新的页分配请求就可能会产生OOM,即使此刻在高端内存我们还有大量空闲的页框。

node:指明这个区所属的节点编号。

pageset:用于实现每cpu高速缓存的数组,每个cpu在该数组内占有一项。由于内核经常请求和释放单个页框,因此每cpu高速缓存包含一些预先分配的页框,它们用于满足本地cpu发出的单一内存请求。

lock:保护该描述符的自旋锁。

span_seqlock:保护zone_start_pfn、spanned_pages和present_pages的顺序锁,之所以使用顺序锁来保护这3个变量是因为我们经常需要在没有获得zone->lock的情况下读取它们的值,同时我们很少更改这些值。

free_area:标识出区中空闲页框快,这个结构是伙伴分配器的主要数据结构,伙伴分配器就是根据该结构体来对区中内存进行分配、回收等操作的。

pageblock_flags:一个内存块的标志,该内存块大小为pageblock_nr_pages。

lru_lock:活动及非活动页链表使用的自旋锁,主要在页框回收时使用。

active_list:区的活动页链表。

inactive_list:区的非活动页链表。

nr_scan_active:回收页框要扫描的活动页数目。

nr_scan_inactive:回收页框要扫描的非活动页数目。

pages_scanned:页框回收时使用的计数器。

flags:区的标志,目前有三个标志可以设置:

ZONE_ALL_UNRECLAIMABLE——区中所有页框被锁定不能回收

ZONE_RECLAIM_LOCKED——不能并发回收区中页框

ZONE_OOM_LOCKED——区处于OOM的区链表中

vm_stat:包含区的统计信息。

prev_priority:区扫描优先级,主要用于页框回收过程中。

wait_table:进程等待队列的hash表,表中的进程在等待区中的某一页内存。

wait_table_hash_nr_entries:等待队列hash表数组的大小。

wait_table_bits:等待队列hash表大小,1<<wait_table_bits。

zone_pgdat:指向区所属节点的指针。

zone_start_pfn:区中第一个页框在mem_map数组中的编号。

spanned_pages:区中的页框数,包含空洞。

present_pages:区中包含的页框数,不包含空洞。

name:指向区的名称,这个域很少使用。

 

三、页的数据表示

这个结构体是描述物理内存的最小单位,它描述了每个物理页框的属性,定义于文件<linux/mm_types.h>:

struct page {

   unsigned long flags;       

   atomic_t _count;   

   union {

       atomic_t _mapcount;

       struct {       

           u16 inuse;

           u16 objects;

       };

   };

   union {

       struct {

       unsigned long private;     

       struct address_space *mapping; 

       };

#if NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS

       spinlock_t ptl;

#endif

       struct kmem_cache *slab;

       struct page *first_page;

   };

   union {

       pgoff_t index;      /* Our offset within mapping. */

       void *freelist;     /* SLUB: freelist req. slab lock */

   };

   struct list_head lru;      

 

#if defined(WANT_PAGE_VIRTUAL)

   void *virtual;         

#endif

#ifdef CONFIG_CGROUP_MEM_RES_CTLR

   unsigned long page_cgroup;

#endif

};

各字段含义如下:

flags:一组原子型标志,有些标志可以异步更新,这个域功能繁多,有兴趣的可以进一步看看源代码。

_count:页框的引用计数器。如果该字段为-1,则相应页框空闲,并可被分配给任一进程或内核本身;如果该字段大于或等于0,则说明该页框被分配给了一个或多个进程,或用于存放一些内核数据结构。

_mapcount:本页框对应的页表项数目,本页框若未被映射则为-1。

inuse、objects:这2个标志在SLUB分配器中使用。

private:可用于正在使用该页的内核成分使用,如果页是空闲的则该字段由伙伴系统使用。

mapping:当页被插入页高速缓存中时使用。

slab:指向slab的指针。

first_page:

index、freelist:作为不同的含义被几种内核成分使用。

lru:将页链接到最近最少使用双向链表中。

virtual:指向本页框的内核虚拟地址。

page_cgroup:这个域只有在内存资源控制器起作用的情况下使用,用于统计处于内存资源控制器下的页框。