Archive for the ‘内存管理’ category

物理内存管理:Page的逆向映射

May 18th, 2015

kernel中包含着庞大的元数据struct page,一个struct page管理一个4k大小的物理内存(默认),之前我们进行的是从task_struct向vm_area_struct一直到最后的struct page的寻找过程。

但是从RAS角度,如果一个page发生问题,那么如何从struct page向上找到对应的page table呢(找到page table也就找到了pid、task_struct….)?这时候就要使用逆向映射

为了尽可能减小strut page的大小,Andrea复用了page的struct address_space *mapping数据结构来表示不同类型的页,主要分为三个类型swap、page cache、anonymous page。这里我们只说明page cache 与 anonymous page的逆向映射。

0.什么是匿名页?

想说明匿名页,必须首先说明什么是映射页。在用户态下打开一个文件,kernel会使用map() 映射文件的某个部分,这个部分在用户态地址空间存在地址,这种页要被回收的时候,会检查是否是dirty,如果为dirty,则需要写回相应的磁盘文件。

而匿名页没有对应了打开的磁盘文件,比如进程的用户态堆和stack可以称为匿名页,当匿名页过长时间驻留内存时,kernel可以要把它保存到一个特定的磁盘分区,这就是swap分区!

首先我们看一下struct page的结构:

struct page {
          /* First double word block */
          unsigned long flags;            /* Atomic flags, some possibly
                                           * updated asynchronously */
          union {
                  struct address_space *mapping;  /* If low bit clear, points to
                                                   * inode address_space, or NULL.
                                                   * If page mapped as anonymous
                                                   * memory, low bit is set, and
                                                   * it points to anon_vma object:
                                                   * see PAGE_MAPPING_ANON below.
                                                   */
                  void *s_mem;                    /* slab first object */
          };

          /* Second double word */
          struct {
                  union {
                          pgoff_t index;          /* Our offset within mapping. */
 ....

struct address_space *mapping是确定页是映射还是匿名

  • mapping为空表示该页属于交换高速缓存swap;
  • mapping非空,且最低位是1,表示该页为匿名页,同时mapping字段中存放的是指向anon_vma描述符的指针;
  • mapping非空,且最低位是0,表示该页为映射页;同时mapping字段指向对应文件的address_space对象。

而第二部分的pgoff_t index是用来指明偏移量的,在映射页中是以页为单位偏移。

1.当我们判断这个页是映射页时,每个page的mapping都指向一个对应的address_space,这个结构是page cache的核心,可以通过这个找到具体的inode!

2.当我们判断这个页是匿名页时,那么这个page指向struct anon_vma

但是这里kernel在2010年提交过一个补丁,为了解决大量fork出来的子进程占用大量anon_vma结构,他们的结构完全相同,而且这种结构导致匿名映射寻找也是O(N)的复杂度。所以引入了struct anon_vma_chain

假设系统只有一个task_struct时,组织结构如下:

anon_map

struct anon_vma_chain {
         struct vm_area_struct *vma;
         struct anon_vma *anon_vma;
         struct list_head same_vma;   /* locked by mmap_sem & page_table_lock */
         struct rb_node rb;                      /* locked by anon_vma->rwsem */
         unsigned long rb_subtree_last;
 #ifdef CONFIG_DEBUG_VM_RB
         unsigned long cached_vma_start, cached_vma_last;
 #endif
};

这样子就形成了anon_vma与vm_area_struct N对N的组织形式,当父进程fork出一个子进程,那么这个子进程拥有一个vm_area_struct,当然也就拥有一个anon_vma,除非发生COW,否则父子进程共享vm_area_struct。

this patch changes the way anon_vmas and VMAs are linked, which allows us to associate multiple anon_vmas with a VMA. At fork time, each child process gets its own anon_vmas, in which its COWed pages will be instantiated. The parents’ anon_vma is also linked to the VMA, because non-COWed pages could be present in any of the children.

3.刚才说的是正向的一个匿名页查找,现在我们反过来,一直一个struct page,找到这个匿名页的page table。结合正向的结构图与相关struct成员,我们可以画出相应的结构图,这里page指向anon_vma就是通过mapping做到的:

 

anonvma2

 

而anon_vma (AV),anon_vma_chain entry (AVC),vm_area_struct(VMA)三者的关系如下

avchain1

当father process fork()出来子进程后,会产生新的struct anon_vma ,这个结构就会分裂,创建VMA->创建AVC->创建AV。

avchain4

 

 

 

参考:

http://lwn.net/Articles/335768/

http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=5beb49305251e5669852ed541e8e2f2f7696c53e

Linux内存管理:SLAB分配器

May 15th, 2015

之前学习过linux物理内存管理中的伙伴系统,他的分配单位是以page为单位分配。对于远远小于page的内存分配请求,比如几个字节~几百个字节,如果分配一个页框,那就是极大地浪费,会产生内部碎片,这时SLAB分配器就派上用场了。

与slab有相同地位的有slob(simple linked list of block)与slub两种备用类型的内存分配器,slob主要是使用内存块链表展开,使用最先适配算法,主要是使用在嵌入式系统,对于内存分配紧张的的系统。对于拥有大量物理内存的并行系统,slab会占用非常大的内存来存储元数据,而slub会将page打包成组,所以这里主要使用slub作为大型系统的内存分配方案。

slab、slob、slub 三者都拥有相同的内核分配函数接口。三者的选择主要在编译时就要选择确定下来。我们以slab作为下面说明的,slab会将某种特定类型的数据集中分配在一起,并且将这些对象放到高速缓存中,方便存取。

举例:进程描述符,当新进程创建时,内核直接从slab中获取一个初始化好的对象,当释放后,这个page不会放到伙伴系统,而是放入slab中。

slab分配器在最上层拥有节点是struct kmem_cache,他是slab分配器的核心结构体

struct kmem_cache {
    struct array_cache __percpu *cpu_cache;
/* 1) Cache tunables. Protected by slab_mutex */
    unsigned int batchcount;
    unsigned int limit;
    unsigned int shared;
    unsigned int size;
    struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */
    unsigned int flags;     /* constant flags */
    unsigned int num;       /* # of objs per slab */
/* 3) cache_grow/shrink */       

    /* order of pgs per slab (2^n) */
    unsigned int gfporder;
    /* force GFP flags, e.g. GFP_DMA */  

    gfp_t allocflags;
    size_t colour;          /* cache colouring range */
    unsigned int colour_off;    /* colour offset */
    struct kmem_cache *freelist_cache;
    unsigned int freelist_size;                                                                   

    /* constructor func */
    void (*ctor)(void *obj);
/* 4) cache creation/removal */
    const char *name;
    struct list_head list;
    int refcount;
    int object_size;
    int align;

/* 5) statistics */
#ifdef CONFIG_DEBUG_SLAB
.....
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_MEMCG_KMEM
    struct memcg_cache_params *memcg_params;
#endif

    struct kmem_cache_node *node[MAX_NUMNODES];
};

这里至于管理slab的节点的数据结构就是struct kmem_cache_node,每个kmem_cache结构中并不包含对具体slab的描述,而是通过kmem_cache_node结构组织各个slab。该结构的定义如下:

struct kmem_cache_node {
    spinlock_t list_lock;

#ifdef CONFIG_SLAB
    struct list_head slabs_partial; /* partial list first, better asm code */
    struct list_head slabs_full;
    struct list_head slabs_free;
    unsigned long free_objects;
    unsigned int free_limit;
    unsigned int colour_next;   /* Per-node cache coloring */
    struct array_cache *shared; /* shared per node */
    struct alien_cache **alien; /* on other nodes */
    unsigned long next_reap;    /* updated without locking */
    int free_touched;       /* updated without locking */
#endif
...
};

可以看到,该结构将当前缓存中的所有slab分为三个部分:空闲对象的slab链表slabs_free,非空闲对象的slab链表slabs_full以及部分空闲对象的slab链表slabs_partial。至于在链上的结构体,在kernel 3.11前后发生了重大变化,在3.11前的版本。使用struct slab来管理slab资源。

struct slab {
    union {
        struct {
            struct list_head list;
            unsigned long colouroff;
            void *s_mem;        /* including colour offset */
            unsigned int inuse; /* num of objs active in slab */
            kmem_bufctl_t free;
            unsigned short nodeid;
        };
        struct slab_rcu __slab_cover_slab_rcu;
    };
}

在3.11之后Joonsoo Kim 提出方案认为大量的slab对象严重占用内存,所以之后struct slab融合进struct page结构体。显著降低了元数据的内存使用量,具体查看 。他并对修改后的struct page 进行了介绍 。

也就是slabs_partial 链接的是一个个struct page结构体。根据这几个结构体的关系,我们可以总结出slab的结构

slab

在kernel 3.11 以前,结构图是:

1339687849_2988

最后还要说明struct array_cache 也是一个重要的结构体,cpu首先分配一项专有object是通过struct array_cache来进行分配,这个结构体是每个cpu都会存在一个。

struct array_cache {
	unsigned int avail;/*本地高速缓存中可用的空闲对象数*/
	unsigned int limit;/*空闲对象的上限*/
	unsigned int batchcount;/*一次转入和转出的对象数量*/
	unsigned int touched;   /*标识本地CPU最近是否被使用*/
	spinlock_t lock;
	void *entry[];	/*这是一个伪数组,便于对后面用于跟踪空闲对象的指针数组的访问
			 * Must have this definition in here for the proper
			 * alignment of array_cache. Also simplifies accessing
			 * the entries.
			 */
};

在每个array_cache的末端都用一个指针数组记录了slab中的空闲对象,分配对象时,采用LIFO方式,也就是将该数组中的最后一个索引对应的对象分配出去,以保证该对象还驻留在高速缓存中的可能性。实际上,每次分配内存都是直接与本地CPU高速缓存进行交互,只有当其空闲内存不足时,才会从kmem_list中的slab中引入一部分对象到本地高速缓存中,而kmem_list中的空闲对象也不足了,那么就要从伙伴系统中引入新的页来建立新的slab了,这一点也和伙伴系统的每CPU页框高速缓存很类似。

slab高速缓存分为两类,普通高速缓存和专用高速缓存。

  • 普通高速缓存并不针对内核中特定的对象,它首先会为kmem_cache结构本身提供高速缓存,这类缓存保存在cache_cache变量中,该变量即代表的是cache_chain链表中的第一个元素;
  • 专用高速缓存为内核提供了一种通用高速缓存。专用高速缓存是根据内核所需,通过指定具体的对象而创建。

最后使用这种slab分配非常简单

1.创建

struct kmem_cache *cachep = NULL;
cachep = kmem_cache_create("cache_name", sizeof(struct yourstruct), 0, SLAB_HWCACHE_ALIGN, NULL, NULL);

2.分配一个struct yourstruct的结构体空间时

调用kmem_cache_alloc函数,就可以获得一个足够使用的空间的指针(SLAB_HWCACHE_ALIGN,这个标志会让分配的空间对于硬件来说是对齐的,而不一定恰好等于sizeof(struct yourstruct)的结果)。范例代码如下:

struct yourstruct *bodyp = NULL;
bodyp = (struct yourstruct *) kmem_cache_alloc(cachep, GFP_ATOMIC & ~__GFP_DMA);

3.销毁

kmem_cache_free(cachep, bodyp);

 

参考:

http://lwn.net/Articles/570504/
https://lwn.net/Articles/565097/
https://lwn.net/Articles/335768/
http://blog.csdn.net/vanbreaker/article/details/7664296

物理内存管理:伙伴系统释放页框

April 24th, 2015

相对与页框慢速分配快速分配而言。页框释放函数非常简单,主要的函数就是__free_pages()和__free_page()。很明显,类似于之前page分配的上层函数,也是层层wrapper。这里__free_page()是__free_pages()的包装。

#define __free_page(page) __free_pages((page), 0)

void __free_pages(struct page *page, unsigned int order)
{
        if (put_page_testzero(page)) {
                if (order == 0)
                        free_hot_cold_page(page, 0);
                else
                        __free_pages_ok(page, order);
        }
}

通过代码我们发现当要释放的页order==0,直接使用per-cpu缓存释放,大于0使用__free_pages_ok(page, order)释放。

这里kernel使用free_hot_cold_page() 方式释放order为0的页,这个函数只将不可移动页,可回收页和可移动页放入每个CPU页框高速缓存中,如果迁移类型不再这个范围,那么这个页要回收到伙伴系统中!这里的策略是:热页加入表头,冷页加入表尾。这个部分不在本文中叙述,查看代码便知。

来看__free_pages_ok()函数,这个函数也是一个wrapper函数,主要用来检查参数是否正确,做一些释放page前的准备工作。当一切就绪后就会调用free_one_page()函数。大致调用关系是:

static void __free_pages_ok(struct page *page, unsigned int order)
{
....
    free_one_page(page_zone(page), page, pfn, order, migratetype);
....
}
static void free_one_page(struct zone *zone,
                struct page *page, unsigned long pfn,
                unsigned int order,
                int migratetype)
{
    unsigned long nr_scanned;
    spin_lock(&zone->lock);
...
    __free_one_page(page, pfn, zone, order, migratetype);
    spin_unlock(&zone->lock);
}

在调用__free_one_page()之前,kernel换回更新当前zone下面page状态:更新当前内存管理区的空闲页面数,也就是更新zone下面的vm_stat数组,这个数组用于统计当前内存信息。

在核心函数__free_one_page()中,这个函数主要完成page的回收,完成页的合并,首先要获取要释放page的page index

static inline void __free_one_page(struct page *page,
        unsigned long pfn,
        struct zone *zone, unsigned int order,
        int migratetype)
{
    unsigned long page_idx;
    unsigned long combined_idx;
    unsigned long uninitialized_var(buddy_idx);
    struct page *buddy;
    int max_order = MAX_ORDER;

...
    page_idx = pfn & ((1 << max_order) - 1);
...
    while (order < max_order - 1) {
        buddy_idx = __find_buddy_index(page_idx, order);
        buddy = page + (buddy_idx - page_idx);
        if (!page_is_buddy(page, buddy, order))
            break;
        if (page_is_guard(buddy)) {
            clear_page_guard_flag(buddy);
            set_page_private(buddy, 0);
            if (!is_migrate_isolate(migratetype)) {
                __mod_zone_freepage_state(zone, 1 << order,
                              migratetype);
            }
        } else {
            list_del(&buddy->lru);
            zone->free_area[order].nr_free--;
            rmv_page_order(buddy);
        }
        combined_idx = buddy_idx & page_idx;
        page = page + (combined_idx - page_idx);
        page_idx = combined_idx;
        order++;
    }
    set_page_order(page, order);

    if ((order < MAX_ORDER-2) && pfn_valid_within(page_to_pfn(buddy))) {
        struct page *higher_page, *higher_buddy;
        combined_idx = buddy_idx &amp; page_idx;
        higher_page = page + (combined_idx - page_idx);
        buddy_idx = __find_buddy_index(combined_idx, order + 1);
        higher_buddy = higher_page + (buddy_idx - combined_idx);
        if (page_is_buddy(higher_page, higher_buddy, order + 1)) {
            list_add_tail(&page->lru,
                &zone->free_area[order].free_list[migratetype]);
            goto out;
        }
    list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);
out:
    zone->free_area[order].nr_free++;
}

然遍历当前order到max-order所有阶,使用__page_find_buddy()找到当前page_idx的伙伴buddy_idx,然后根据idx偏移量,找到目标buddy。后将这个buddy从list删除,更新nr_free数量。删除buddy标记。最后使用 buddy_idx & page_idx 获取合并后的combined_idx。由于page永远都指向要释放页框块的首页框描述符,所以讲这个combined_idx赋值给page_idx。最后将order+1,然后通过set_page_order()设置这个page的各种信息。

在最新的源码里面,还会判断是否这个page是否是最大的页,如果找到则继续合并,并将合并后page放到list tail中,最后将当前order下的nr_order++。

最后我们来看__find_buddy_index()函数

static inline unsigned long
__find_buddy_index(unsigned long page_idx, unsigned int order)
{
      return page_idx ^ (1 << order);
}

这个函数用来查找buddy的index,这个函数非常简单。

综上所述,所以整个页框分配的核心就是那个while循环,我们可以把页框释放当成页框分裂的逆过程,也就是回收order大于0的页:
举例:

page_idx = 10 -> buddy_idx = 11 -> combined_idx = 10
page_idx = 10 -> buddy_idx = 8 -> combined_idx = 8
page_idx = 8 -> buddy_idx = 12 -> combined_idx = 8
page_idx = 8 -> buddy_idx = 0 -> combined_idx = 0
->无法继续进行->结束

这就是那些位操作的实际含义,page_idx以二倍速率寻找buddy page然后合并,至此伙伴系统回收页框完毕。

 

参考:

http://blog.csdn.net/vanbreaker/article/details/7624628

物理内存管理:伙伴系统page分裂函数分析

April 14th, 2015

快速分配函数中,大部分代码都是一些具体策略的实现,包括watermark,NUMA,公平分配ALLOC_FAIR等。真正与buddy system有关的就是buffered_rmqueue()函数。

我们看到当进入到这个函数,会先对order进行一个判断,如果order==0,则kernel不会从伙伴系统分配,而是从per-cpu缓存加速请求的处理。如果缓存为空就要调用rmqueue_bulk()函数填充缓存,道理还是从伙伴系统中移出一页,添加到缓存。

当传入的order>0时,则调用_rmqueue()从伙伴系统中选择适合的内存块,有可能会将大的内存块分裂成为小的内存块,用来满足分配请求。

static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            gfp_t gfp_flags, int migratetype)
{
    unsigned long flags;
    struct page *page;
    bool cold = ((gfp_flags & __GFP_COLD) != 0);

again:
    if (likely(order == 0)) {
...
    } else {
        if (unlikely(gfp_flags & __GFP_NOFAIL)) {
            WARN_ON_ONCE(order > 1);
        }
        spin_lock_irqsave(&zone->lock, flags);
        page = __rmqueue(zone, order, migratetype);
        spin_unlock(&zone->lock);
        if (!page)
            goto failed;
        __mod_zone_freepage_state(zone, -(1 << order),
                      get_freepage_migratetype(page));
    }
...
}
...
static struct page *__rmqueue(struct zone *zone, unsigned int order,
                        int migratetype)
{
    struct page *page;

retry_reserve:
    page = __rmqueue_smallest(zone, order, migratetype);

    if (unlikely(!page) && migratetype != MIGRATE_RESERVE) {
        page = __rmqueue_fallback(zone, order, migratetype);

        if (!page) {
            migratetype = MIGRATE_RESERVE;
            goto retry_reserve;
        }
    }

    trace_mm_page_alloc_zone_locked(page, order, migratetype);
    return page;
}

上面说的buffered_rmqueue()是进入伙伴系统的前置函数,而__rmqueue是进入伙伴系统的最后一个包装函数,通过这个函数,__rmqueue_smallest()会扫描当前zone下面的空闲区域。

如果不能通过这种方式分配出空闲页,那么系统会调用__rmqueue_fallback()来遍历不同的迁移类型,试图找出不同的迁移类型中空闲的page。这里我们不会仔细分析,但是我们看一下__rmqueue_fallback()的遍历头就能发现端倪。kernel会从start_migratetype迁移类型考虑备用列表的不同迁移类型,具体可以查看http://lxr.free-electrons.com/source/mm/page_alloc.c#L1036

这里找到一篇叙述迁移页的博客《通过迁移类型分组来实现反碎片》,讲的很清楚!

static inline struct page *
__rmqueue_fallback(struct zone *zone, unsigned int order, int start_migratetype)
{
    struct free_area *area;
    unsigned int current_order;
    struct page *page;
    int migratetype, new_type, i;

    /* Find the largest possible block of pages in the other list */
    for (current_order = MAX_ORDER-1;
                current_order >= order && current_order <= MAX_ORDER-1;
                --current_order) {
        for (i = 0;; i++) {
            migratetype = fallbacks[start_migratetype][i];

...

__rmqueue_smallest()函数传入的参数有order,kernel会遍历当前zone下面从指定order到MAX_ORDER的伙伴系统,这个时候迁移类型是指定的,如图:
buddy

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                        int migratetype)
{
    unsigned int current_order;
    struct free_area *area;
    struct page *page;

    /* Find a page of the appropriate size in the preferred list */
    for (current_order = order; current_order < MAX_ORDER; ++current_order) {
        area = &(zone->free_area[current_order]);
        if (list_empty(&area->free_list[migratetype]))
            continue;

        page = list_entry(area->free_list[migratetype].next,
                            struct page, lru);
        list_del(&page->lru);
        rmv_page_order(page);
        area->nr_free--;
        expand(zone, page, order, current_order, area, migratetype);
        set_freepage_migratetype(page, migratetype);
        return page;
    }

    return NULL;
}

我们看到kernel会提取出当前zone下面的struct free_area,我们知道struct free_area是上图整体的一个结构,然后我们按照order可以找到指定的free_list结构,这个结构链接了所有系统中空闲page。

找到这个page,则将这个page从list上面删除,然后rmv_page_order()可也将page的标志位PG_buddy位删除,表示这个页不包含于buddy system。

将这个page所在的nr_free自减,之后调用expand()函数,这个函数存在的意义就是在高阶order分配了一个较小的page,但同时低阶order又没有合适的页分配。

static inline void expand(struct zone *zone, struct page *page,
    int low, int high, struct free_area *area,
    int migratetype)
{
    unsigned long size = 1 << high;

    while (high > low) {
        area--;
        high--;
        size >>= 1;
.....
        list_add(&page[size].lru, &area->free_list[migratetype]);
        area->nr_free++;
        set_page_order(&page[size], high);
    }
}

进入到expand()函数,这个函数是从current_order到请求order倒序遍历,函数的核心是通过current_order折半分裂,分裂完之后,将空闲的page挂到相同的order阶上面的free_area,然后nr_free++,set_page_order()作用是对于回收到伙伴系统的的内存的一个struct page实例中的private设置分配阶,并且设置PG_buddy。

 

完成伙伴系统的高阶page分裂。

 

PLKA: Page190

物理内存管理:伙伴系统数据结构分析

物理内存管理:请求PFN快速分配实现(3)

April 13th, 2015

前面主体函数慢速分配函数最后都会调用到get_page_from_freelist()函数,区别在于主体函数是直接调用,而慢速分配是通过回收page,或者交换到swap分区的方式从已经满了的zone里面回收空闲的page。

这个函数可以看做buddy system 的前置函数,通过传入order与flag判断当前的内存是否可以分配一块内核,如果可以转入buffered_rmqueue()进行,如果实在没有的话,就只能返回NULL了。

static struct page *
get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order,
                 struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
                 struct zone *preferred_zone, int classzone_idx, int migratetype)
{
         struct zoneref *z;
         struct page *page = NULL;
         struct zone *zone;
         nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
         int zlc_active = 0;             /* set if using zonelist_cache */
         int did_zlc_setup = 0;          /* just call zlc_setup() one time */
         bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) &&
                                 (gfp_mask & __GFP_WRITE);
         int nr_fair_skipped = 0;
         bool zonelist_rescan;

上面这个声明头,安装了zlc,也就是zonelist_cache还有判断zone_dirty的位,包括是否进行fairness分配等。

zonelist_scan:
        zonelist_rescan = false;
        for_each_zone_zonelist_nodemask(zone, z, zonelist,
                                                high_zoneidx, nodemask) {
                 unsigned long mark;

                 if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
                         !zlc_zone_worth_trying(zonelist, z, allowednodes))
                                 continue;
                 if (cpusets_enabled() &&
                         (alloc_flags & ALLOC_CPUSET) &&
                         !cpuset_zone_allowed(zone, gfp_mask))
                                 continue;
                 if (alloc_flags & ALLOC_FAIR) {
                        if (!zone_local(preferred_zone, zone))
                                 break;
                        if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
                                 nr_fair_skipped++;
                                 continue;
                         }
                 }

                 if (consider_zone_dirty && !zone_dirty_ok(zone))
                         continue;

上面的这个段有一个zonelist_scan段,因为扫描空闲的zonelist可能会出现找不到page,所以kernel会使用goto重新扫描遍历zonelist寻找合适page。

for_each_zone_zonelist_nodemask()是一个宏函数,用来进行zonelist的遍历,我们之前结合内存结构分析过NUMA架构中存在多个pg_list,而high_zoneidx则是遍历的边界,也就是说如果high_zoneidx为ZONE_NORMAL,那么遍历的zone区域就是ZONE_DMA、ZONE_NORMAL。而zone的区域都是通过enmu来声明的。

                 mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
                 if (!zone_watermark_ok(zone, order, mark,
                                        classzone_idx, alloc_flags)) {
                         int ret;

                         BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
                         if (alloc_flags & ALLOC_NO_WATERMARKS)
                                 goto try_this_zone;

                         if (IS_ENABLED(CONFIG_NUMA) &&
                                         !did_zlc_setup && nr_online_nodes > 1) {

                                 allowednodes = zlc_setup(zonelist, alloc_flags);
                                 zlc_active = 1;
                                 did_zlc_setup = 1;
                         }

                         if (zone_reclaim_mode == 0 ||
                             !zone_allows_reclaim(preferred_zone, zone))
                                 goto this_zone_full;

                         if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
                                 !zlc_zone_worth_trying(zonelist, z, allowednodes))
                                 continue;

上面这些代码都是存在于遍历zonelist的循环中,如果当前的watermark使能,而且zone_watermark_ok()判断在当前的watermark下是否可以分配内存。如果可以分配内存则跳入最后的try_this_zone段。

在下面会提到。如果这里分配page失败,也就意味内存不足,需要进行page回收:zone_reclaim(),上面这段代码还包括一些其他判断,如果在nodemask中指定了在当前节点分配page,而同时当前node又不满足分配的需求,这里只能返回this_zone_full段。

 ret = zone_reclaim(zone, gfp_mask, order);
 switch (ret) {
      case ZONE_RECLAIM_NOSCAN:
              continue;
      case ZONE_RECLAIM_FULL:
              continue;
      default:
              if (zone_watermark_ok(zone, order, mark,
                      classzone_idx, alloc_flags))
                   goto try_this_zone;

              if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) ||
                   ret == ZONE_RECLAIM_SOME)
                   goto this_zone_full;

              continue;
           }
     }

如果运行到这里,说明此刻空闲内存不足,需要使用zone_reclaim()进行内存回收,回收的情况通过ret来确定。如果结果是ZONE_RECLAIM_NOSCAN,说明并没有进行回收,那么直接尝试下一个zone,如果结果是ZONE_RECLAIM_FULL,说明虽然进行了回收但是并没有回收到,默认的情况则是没有回收到足够多的内存。后两种情况均跳入default处。

这里需要再次判断是否分配的内存在watermark以下,如果可以的话,跳入try_this_zone。第二个if是为了判断分配的mask是否是最小值,这里涉及到ZONE_RECLAIM_SOME,不太清楚,需要继续看。

这里需要声明的是ALLOC_WMARK_MIN指的是当前的内存空闲量已经很低,我们可以将内存看成一桶水,用掉内存的相当于用掉的水,所以越用越少。我们查看代码,发现使用watermark的话,会主要的存在以下几个标志位:

#define ALLOC_WMARK_MIN         WMARK_MIN
#define ALLOC_WMARK_LOW         WMARK_LOW
#define ALLOC_WMARK_HIGH        WMARK_HIGH
#define ALLOC_NO_WATERMARKS     0x04 /* don't check watermarks at all */

大致分为MIN、LOW、HIGH三个watermark标志。当系统当前node节点内存非常少的时候,就会启用ALLOC_NO_WATERMARKS标志。

try_this_zone:
                 page = buffered_rmqueue(preferred_zone, zone, order,
                                                 gfp_mask, migratetype);
                 if (page)
                         break;

上面这个段就是如果系统找到空闲的page,会跳到这里,这个函数就是buddy system的前置函数。

 this_zone_full:
                 if (IS_ENABLED(CONFIG_NUMA) && zlc_active)
                         zlc_mark_zone_full(zonelist, z);
         }

上面这个段说明当前zone的空闲内存不足,那么标记它。这样下次分配时可以直接将其忽略。

         if (page) {
                page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);
                return page;
         }

上面这个已经跳出了遍历zonelist的范围,如果分配到了page,走到这里,系统已经分配了page,设置pfmemalloc 也就意味着目前系统在内存方面有压力,应该保持这个page不被swap出内存,并使得系统换出其他的页。

         if (alloc_flags & ALLOC_FAIR) {
                 alloc_flags &= ~ALLOC_FAIR;
                 if (nr_fair_skipped) {
                         zonelist_rescan = true;
                         reset_alloc_batches(preferred_zone);
                 }
                 if (nr_online_nodes > 1)
                         zonelist_rescan = true;
         }
         if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) {
                 /* Disable zlc cache for second zonelist scan */
                 zlc_active = 0;
                 zonelist_rescan = true;
         }

         if (zonelist_rescan)
                goto zonelist_scan;

         return NULL;

走到这里,一般意味着page为NULL,这里我们要考虑到一种情况本地的node,内存分配已经达到饱和,而远端的node还没有被考虑,所以这个时候系统放弃fairness,将远端的node节点纳入到考虑范围。

当然这是在进入慢速分配和唤醒kswapd的前提下,总的来说系统page分配的mechanism顺序是:local node -> remote node -> slowpath -> kswapd ->NULL

这里设置了zonelist_rescan 位,如果这一位为真,则重新扫描所有的内存node节点,包括远端node!

 

[1] http://lxr.free-electrons.com/source/mm/page_alloc.c#L2036