Kernel那些事儿之内存管理(1)

有人的地方就有江湖。要介绍内存管理这个江湖,首先还得从这里面的主要人物讲起。

在NUMA结构中,物理内存首先被分成若干nodes。每一个node进一步被分成若干zones。每一个zone又关联了一个描述page frames的数组,该数组包含了属于该zone的所有page frame的描述符。

不难看出,在这个江湖里主要有三位重要人物:nodes, zones 和 page frames。这三者的关系和地位大体可以用下图来描述(该图取自“Professional Linux Kernel Architecture”):

下面我们对这三位人物逐一介绍。请注意这里主要描述了我们关心的内容。

1. Node

在Kernel中,node是由结构体 pg_data_t 来描述的。

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
   
    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: 每个node被分为若干zones,这些zones的信息就保存在这个数组中。
  • node_zonelists: 当我们要在某一个指定的zone中分配内存,而该zone中又没有足够多的空闲内存时,怎么办?正所谓狡兔三窟,我们得给自己留点后路,node_zonelists正是这种情况下的后路。它指定了所有的备选的zones。当然这些备选zones也是有优先顺序的,毕竟只有小三也不能满足需求时,才会再去找小四。
  • nr_zones: 顾名思义,就是该node中zone的个数。
  • node_mem_map: 属于该node的所有page frame的数组。page frame就是最基层的士兵,无论你是一个团(zone),还是一个师(node),说到底都是有一个个士兵组成的。
  • node_start_pfn: 注意所有node中的page frame是统一顺序编号的,并不是说这个师里有个士兵9527,那个师里还有个士兵9527。9527在整个部队中只有一个。而node_start_pfn就是该node中第一个page frame的编号。
  • node_present_pages: 该node中page frames的个数。
  • node_spanned_pages:这个也是该node中page frames的个数,不过把memory holes也包含进来了,所以node_spanned_pages一般要比node_present_pages大。所谓的memory hole,就是内存空间中有些区域可能会被预留用来做I/O映射,或是被BIOS预留,这样地址空间就有了一个个窟窿。啊,忽然想起了一个朋友的真实故事。。。当年他加入某NB团队做当时无比高大的分布式内存管理,地址空间address space中间有memory hole。于是他们team的一位大美女便给链表命名为as_hole。当时没人有异议,只是每次code review,大家看到这个变量的时候都会呐呐地绕开,直到后来。。。(我这么直接引用别人的故事,还是真实的故事,是不是侵权了啊?请版权所有者联系我,我把这段给删掉。)
  • node_id: 该node的编号,0 1 2 ...
  • kswapd_wait, kswapd, kswapd_max_order: 这三个成员变量用于swapping 机制,现在先略去不讲。

2. Memory Zones

这个人物比较复杂,在这里我们先有个第一印象,混个脸熟。以后会经常打交道,再慢慢了解。

在Kernel中,memory zone是由结构体 struct 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];

    struct per_cpu_pageset  pageset[NR_CPUS];

    /*
     * free areas of different sizes
     */
    spinlock_t      lock;
    struct free_area    free_area[MAX_ORDER];

    unsigned long       *pageblock_flags;

    ZONE_PADDING(_pad1_)

    /* Fields commonly accessed by the page reclaim scanner */
    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;         /* zone flags, see below */

    /* Zone statistics */
    atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];

    int prev_priority;

    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;
    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_PADDING,把该结构体分成了三部分。

为什么会有ZONE_PADDING这个东东在这里呢?事情是这样的。在SMP系统中,多个CPU经常会同时访问同一个zone结构体。这样lock就必不可少了。该结构体主要有两个lock,zone->lock和zone->lru_lock。为了能让这两个lock在不同的CPU cache line中,只好忍忍心在他俩之间隔上一条银河了。

第一部分的内容主要由page allocator使用,用来申请分配内存页。

  • pages_min, pages_low, pages_high: 这哥仨被称为"watermarks",在申请物理内存以及内存回收过程中都会起到作用。

    • 如果空闲内存页数大于pages_high,那么这个zone就被认为是很空闲的。
    • 如果空闲内存页数小于pages_low, 需要内存回收了。
    • 如果空闲内存页数小于pages_min, 内存回收亚历山大,危险危险。
  • lowmem_reserve:指定了每一个zone必须预留多少内存页。这些预留的内存页主要用来处理low-on-memory的紧急情况。
  • pageset: 这是一个per-CPU的内存页缓存。内核先提前申请好一些内存页,放在这个缓存中。当需要申请单页内存时,就可以直接在这个缓存中拿了。这是Kernel的一个惯用伎俩,以后还会见到更多类似的例子。
  • free_area: 这个就是大名鼎鼎的buddy system所在的地方了。后面会有专门的博文来讲buddy system。在这里只要知道,这个成员变量用来存放该zone中的空闲内存页。
  • pageblock_flags: 这个也是用于buddy system,用在防止碎片的机制中(anti-fragmentation)。

  • ZONE_PADDING(_pad1_)    华丽的分割线


第二部分的内容主要由内存回收机制使用。所有的内存页按其活跃情况被分类:active OR inactive。不同类型的内存页会被放在不同的LRU链表上。当要进行内存回收时,这个分类就很重要了。那些inactive的内存页会优先被回收掉。正应了鲁迅的那句话:不在沉默中爆发,就在沉默中灭亡。

  • active_list, inactive_list: 活跃/不活跃的内存页的链表。
  • nr_scan_active, nr_scan_inactive: 当进行内存回收时,需要扫描多少活跃/不活跃的内存页。
  • flags:描述了该zone的状态。
typedef enum {
    ZONE_ALL_UNRECLAIMABLE,     /* all pages pinned */
    ZONE_RECLAIM_LOCKED,        /* prevents concurrent reclaim */
    ZONE_OOM_LOCKED,            /* zone is in OOM killer zonelist */
} zone_flags_t;
  • vm_stat: 保存了关于该zone的各种统计信息,例如NR_FREE_PAGES, NR_FILE_PAGES等。这些信息会随时随地更新。
  • prev_priority:内存回收时用来保存上次扫描该zone时的权值。(是不是还是搞不清楚这是干啥的?没关系,当我们讲到内存回收的细节时,还会再回头看这些变量的。)

  • ZONE_PADDING(_pad2_)    华丽的分割线


第三部分主要是很少用到的,或是主要用来读的内容。

  • wait_table, wait_table_hash_nr_entries, wait_table_bits: 这三位师兄弟实现了一个等待队列。当某一个内存页暂时不可用时,想要使用该内存页的进程便会等待在该队列中。
  • zone_pgdat: 该zone所属的node。这个是我用来寻找组织的,很重要的。
  • zone_start_pfn:该zone中第一个内存页的编号。
  • spanned_pages:该zone中内存页的个数,包含memory holes。  《==== 是不是似曾相识啊
  • present_pages: 该zone中真正能用的内存页的个数。
  • name: 出来混江湖,总得有个名号啊。

(TO BE CONTINUED...)

时间: 2025-01-19 21:57:13

Kernel那些事儿之内存管理(1)的相关文章

Kernel那些事儿之内存管理(3) --- 久别重逢

上次我们讲到page frame是物理内存的基本组成单位.那Kernel就必须要有一套机制来管理空闲的page frames.这一点不难理解.每个县长必须要把本县可用的劳动力登记在册,这样哪天皇帝要征兵了,你才不至于手忙脚乱. 这个问题看似简单,实则不然.因为这里面有一个外碎片的问题. 在物理内存中,连续的物理内存页有时是很重要的.例如在DMA操作中,由于大部分DMA处理器都没有分页机制,它们会直接访问物理内存地址,因此DMA 所用的缓冲区在物理地址空间必须连续:再例如,使用连续的物理内存页,可

Kernel那些事儿之内存管理(8) --- Slab(中)

上篇讲了Slab中的数据结构,这篇该讲Slab中的操作了. 既然是内存管理,那操作无非就两点:allocate 和 free. 1. 申请一个object 在Slab中,申请一个object是通过函数 kmem_cache_alloc() 来完成的. 3618 void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 3619 { 3620     return __cache_alloc(cachep, flags, __bu

Kernel那些事儿之内存管理(7) --- Slab(上)

前面讲的buddy system算法,分配内存的最小单位是一个页面(例如 4K).这对于大的内存申请比较适用.可是实际生活中,Kernel经常需要分配小的内存空间,比如几十个字节,这个时候怎么办呢? 不同的人可能会想到不同的解决办法. 对于财大气粗的富人一族,办法很简单:申请一个页面,只用其中的几十字节,剩下的直接丢掉. 对于锱铢必较的穷人一族,就不敢这么挥霍了:申请一个页面,然后记录好页面内哪些内存区用了,哪些还没用,没用的部分可以用来满足其他的内存分配请求.总之务必做到物尽其用. 很不幸,K

Kernel那些事儿之内存管理(2) --- 百闻不如一见

上次介绍了物理内存管理中三位主要人物中的node 和zone.这两位是当官的,一个是县长,一个是里长,不敢不先介绍啊.接下来出场的就是我们的老百姓了 --- page frame. Page frame是物理内存的基本组成单位,在Kernel中由结构体 struct page 来描述. struct page {     unsigned long flags;          atomic_t _count;          union {         atomic_t _mapcou

Kernel那些事儿之内存管理(13) --- 内核映射(下)

前面讲过,针对于内核地址空间中后面的128MB空间,Kernel提供了三种机制来映射物理内存.之前讲过了两种,即持久内核映射和临时内核映射.这两种机制的目的都是一样的:使Kernel能够访问到高端内存. 今天讲一下第三种机制:非连续内存分配,也就是vmalloc.这个机制同样可以使Kernel能够访问到高端内存,不过这不是该机制的主要目的.该机制的主要目的是:把物理上不连续的页面映射到连续的内核线性地址空间中. 非连续内存区域管理 既然是映射,肯定会涉及到三个元素:集合L,集合P,映射M. 集合

Kernel那些事儿之内存管理(11) --- 内核映射(上)

前面简单地介绍了三种不同的地址空间,接下来重点讲述线性地址空间到物理地址空间的映射. 我们先从32位系统开始. 在32位系统中,线性地址空间的大小为 2^32,即4GB.Kernel一般会按照 3:1 的比例,把线性地址空间分为两部分: 0~3GB 用户地址空间 3GB~4GB 内核地址空间. 用户地址空间的管理和映射是个大的topic.我们后面再详细讲述. 内核地址空间只有1GB大小,最多只能映射1GB的物理内存.那问题来了:1GB之外物理内存怎么办? Kernel 给出的解决办法是,把1GB

Kernel那些事儿之内存管理(4) --- 未雨绸缪

上次讲的buddy system算法虽然效率很高,但是要从buddy system中分配出一个内存页块来,还是要做不少工作的,有时想想都会觉得很累. 在系统运行过程中,Kernel经常会有单个页面的申请和释放操作.为了进一步提高性能,也为了让生活变得轻松一点,Kernel采用了这样一种cache机制: Memory zone为每个CPU定义了page frame cache.Kernel会在适当的时机提前从buddy system中分配好若干单页,放在这些cache中.以后Kernel若要申请单

Kernel那些事儿之内存管理(6) --- 衣带渐宽终不悔(下)

接着上篇写,继续介绍zone allocator.上一篇介绍了周边,现在来看看它的全貌 --- 函数__alloc_pages(). Kernel源代码里是这样注释函数__alloc_pages()的.其重要地位可见一斑. 1451 /* 1452  * This is the 'heart' of the zoned buddy allocator. 1453  */ __alloc_pages()的工作模式很清晰:利用函数get_page_from_freelist()多次遍历zonelis

Kernel那些事儿之内存管理(5) --- 衣带渐宽终不悔(上)

Kernel中负责分配一个连续内存页块的子系统一般被称为zoned page frame allocator.前面讲了函数 buffered_rmqueue() 是如何从指定zone的buddy system中分配一个连续内存页块的.这个函数貌似完成了内存页块分配相关的所有工作,然而实际上,这个函数只是zone allocator的冰山一角. 记得我刚上大学那会,拥有一个MP3还是一件能够令男生羡慕.令女生着迷的事情.于是我咬咬牙,花了近一个月的生活费买了一个漂亮的MP3,从此过上了非凡的生活.