蒙恩
一个睡不醒的老男孩儿

导航

 

基于2.6.32内核源码分析

首选内存区和gfp描述符关系运算

64位系统默认没有开启CONFIG_HIGHMEM选项,因此只有4个内存区DMA(0),DMA32(1),NORMAL(2),MOVABLE(3),因此在gfp标志中关于内存区选择的标志有如下4个,并通过宏GFP_ZONEMASK从gfp标志中提取出这些个标志。 然后通过一个人工定义的映射表将上面的四个gfp标志映射为具体的内存区,进而确定分配内存时首选内存区。

1 #define __GFP_DMA    ((__force gfp_t)0x01u)
2 #define __GFP_HIGHMEM    ((__force gfp_t)0x02u)
3 #define __GFP_DMA32    ((__force gfp_t)0x04u)
4 #define __GFP_MOVABLE    ((__force gfp_t)0x08u)  /* Page is movable */
5 #define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
View Code

 这里特意提到:在64位系统下一共有四个内存区,通过gfp标志位选定首选内存区,需要4个标志,上面代码确实有4个标志位,但是对比内存区类型可以看出,标志位多了一个__GFP_HIGHMEM,却少了一个__GFP_NORMAL(内核代码中也确实没有定义这个标志)。为什么会多了__GFP_HIGHMEM下文在详细说,接下来先解释下为什么会少了__GFP_NORMAL,内核使用了一个隐含规则:如果gfp标志低4位全位0(即__GFP_DMA,__GFP_HIGHMEM,__GFP_DMA32,__GFP_MOVABLE都没有被值位时),那么就默认将NORMAL区作为gfp指定的首选内存区。

 1 #define GFP_ZONE_TABLE ( \
 2     (ZONE_NORMAL << 0 * ZONES_SHIFT)                \    //0
 3     | (OPT_ZONE_DMA << __GFP_DMA * ZONES_SHIFT)             \ //2
 4     | (OPT_ZONE_HIGHMEM << __GFP_HIGHMEM * ZONES_SHIFT)        \ //4
 5     | (OPT_ZONE_DMA32 << __GFP_DMA32 * ZONES_SHIFT)            \ //8
 6     | (ZONE_NORMAL << __GFP_MOVABLE * ZONES_SHIFT)            \ //16
 7     | (OPT_ZONE_DMA << (__GFP_MOVABLE | __GFP_DMA) * ZONES_SHIFT)    \  //18
 8     | (ZONE_MOVABLE << (__GFP_MOVABLE | __GFP_HIGHMEM) * ZONES_SHIFT)\ //20
 9     | (OPT_ZONE_DMA32 << (__GFP_MOVABLE | __GFP_DMA32) * ZONES_SHIFT)\ //24
10 )
View Code

2.如果默认没有设定gfp标志,也就是说GFP_ZONEMASK&gfp为0的,选择NORMAL

3.如果只设置了__GFP_DMA,首选DMA内存区

4.如果只设置了__GFP_HIGHMEM,首选NORMAL(由于64位架构下没有HIGHTMEM区,所以这里即使传入了__GFP_HIGHMEM也是首选NORMAL区)

5.如果只设置了__GFP_DMA32,首选DMA32内存区

6.如果只设置了__GFP_MOVABLE,仍然首选NORMAL区

7.如果同时设置了__GFP_MOVABLE和__GFP_DMA,首选DMA区

8.如果同时设置了__GFP_MOVABLE和__GFP_HIGHMEM,首选MOVABLE区(这个MOVABLE区是个什么东西,内存都来至于哪里?)

9.如果同时设置了__GFP_MOVABLE和__GFP_DMA32 首选DMA32区

上面这个映射表其实给出了gfp中关于首选内存区选择的几个标志的所有的合法组合,在内存分配过程中这种从GFP标志到首选内存区的映射是由gfp_zone函数完成。

__cpuset_node_allowed_softwall 和 __cpuset_node_allowed_hardwall

用来根据GFP标志和当前上下文判断某个内存节点是否满足内存分配的要求

softwall限制如下:

1.如果在中断上下文(包括硬中断上下文,软中断上下文,以及不可屏蔽中断上下文,总是允许在被检查node上分配)

2.如果gfp表中中被打上了__GFP_THISNODE标志,总是允许在被检查node上分配

3.如果1,2条件不满足,但是被检查node在当前进程的current->mems_allowed中允许在被检查node上分配

4.如果当前进程正在因OOM被杀掉时,允许在被检查node上分配。

5.如果设置了gfp __GFP_HARDWALL标志 又不满足1,2,3,4则不允许在被检查node上分配

6.如果1,2,3,4,5都不满足,当前进程正在退出(exiting)则允许在被检查node上分配

7.如果1,2,3,4,5,6都不满足,沿着当前进程所在的cpuset往父亲方向遍历找到第一个设置了mem_exclusive或者mem_hardwall的cpuset,如果被检查内存在这个cpuset允许的node列表中,则允许在当前node分配。

8.如果1,2,3,4,5,6,7都不满足这不允许在被检查node上分配。

hardwall限制如下:

1.如果在中断上下文(包括硬中断上下文,软中断上下文,以及不可屏蔽中断上下文,总是允许在被检查node上分配)

2.如果gfp表中中被打上了__GFP_THISNODE标志,总是允许在被检查node上分配

3.如果1,2条件不满足,但是被检查node在当前进程的current->mems_allowed中允许在被检查node上分配

4.如果当前进程正在因OOM被杀掉时,允许在被检查node上分配。

5.如果1,2,3,4都不满足则不允许在当前node上分配

总结:

1.如果gfp标志设置了__GFP_HARDWALL的话,那么softwall和hardwall功能相同

2.如果gfp没有设置__GFP_HARDWALL hardwall要比softwall检查严格,放宽了两种情况下的内存分配。1进程处于exiting状态;2.如果第一个mem_exclusive或者mem_hardwall祖先cpuset中的node列表中有被检查node节点的话,是允许在被检查节点上分配的。

3.如果一个cpuset设置了mem_exclusive或者mem_hardwall的话,无论如何处于以当前cpuset为根的树下的进程上下文是分不到该cpuset之外内存节点上的内存的。

4.中断上下文(包括软中断,硬中断,不可屏蔽中断)中分配内存时是不受cpuset的限制的。

 1 static unsigned interleave_nodes(struct mempolicy *policy)
 2 {
 3     unsigned nid, next;
 4     struct task_struct *me = current;
 5 
 6     nid = me->il_next;
 7     next = next_node(nid, policy->v.nodes);
 8     if (next >= MAX_NUMNODES)
 9         next = first_node(policy->v.nodes);
10     if (next < MAX_NUMNODES)
11         me->il_next = next;
12     return nid;
13 }
View Code

 

 1 /* Convert GFP flags to their corresponding migrate type */
 2 static inline int allocflags_to_migratetype(gfp_t gfp_flags)
 3 {
 4     WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);
 5 
 6     if (unlikely(page_group_by_mobility_disabled))
 7         return MIGRATE_UNMOVABLE;
 8 
 9     /* Group based on mobility */
10     return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) |
11         ((gfp_flags & __GFP_RECLAIMABLE) != 0);
12 }
View Code

 

 

 

内存水线检查函数:zone_watermark_ok

 1 //如果zone检查通过返回1,否者返回0
 2 int zone_watermark_ok(struct zone *z, int order, unsigned long mark,
 3               int classzone_idx, int alloc_flags)
 4 {
 5     /* free_pages my go negative - that's OK */
 6     long min = mark;
 7     long free_pages = zone_page_state(z, NR_FREE_PAGES) - (1 << order) + 1;
 8     int o;
 9 
10     if (alloc_flags & ALLOC_HIGH)
11         min -= min / 2;
12     if (alloc_flags & ALLOC_HARDER)
13         min -= min / 4;
14 
15     if (free_pages <= min + z->lowmem_reserve[classzone_idx])
16         return 0;
17     for (o = 0; o < order; o++) {
18         /* At the next order, this order's pages become unavailable */
19         free_pages -= z->free_area[o].nr_free << o;
20 
21         /* Require fewer higher order pages to be free */
22         min >>= 1;
23 
24         if (free_pages <= min)
25             return 0;
26     }
27     return 1;
28 }
View Code

1.首先获取当前zone中的空闲页框个数,这里提到一点,这个空闲页框个数是一个估计值且是一个瞬时值(没有加锁)并且减去了本次要申请的2^order次方的页框后加1(这里为什么加1?)

2.根据alloc_flags中是否包含了ALLOC_HIGH或者ALLOC_HARDER对实际水线做调整,得到一个结论,如果打了ALLOC_HIGH或ALLOC_HARDER时将水线调低,等于是放宽了检查标准(俗称:击穿水线)

3.运用lowmem_reserve,先做了一下判断,classzone_idx是首选内存区的下标,这个逻辑的原因举个例子来说明:假设系统中有两个node,分贝是node0,node1,总量分别是5G和90G,这样一来node0会有DMA(16M),DMA32(4G),NOMAL(1G),而node2所有的内存都属于NORMAL(96G)。假设分配流程选到了node0来分配内存,并且首选内存区是NORMAL,而node0上的NORMAL只有1G,假设已经被完全分配出去。根据zone的fallback规则,会依次选择node0(DMA32)->node0(DMA)->node(NORMAL)来分配内存。这样以来这次分配就会优先落到node0的DMA32区和DMA区,但是DMA32和DMA区内存一般相对较少的,如果这种分配过多(实际系统中很可能会出现这种情况)那么node0的DMA和DMA32就会被挤占完。当真正需要分配DMA和DMA32的请求到来时就没有办法满足要求,这会导致不必要的回收,甚至会引起一些驱动不能正常工作。因此针对每一个内存区引入了lowmem_reserve数组。假设node0的DMA32区和DMA区的lowmem_resver数组分别时[0,0,1000],[0,100,500],这样一来即使出现了上述情况,也能为DMA32保留1000个页框,为DMA区保留500个页框。这样至少一定程度上减轻了上面的情况造成的影响。lowmem_reserve参数是可以动态配置的(通过proc文件系统)。

4.接下来的for循环做了更进一步检查,检查的标准是:针对每一个小于order的阶N,大于N阶中,内存的总空闲页框数必须大于(调整后的水线)/2^N次方。这段逻辑的原因没有查到具体的内核文档,作者个人的理解:这么写是提早发现,避免内存的碎片化,因为这段逻辑看,高阶内存过少时,即使总体的空闲页框数满足内存水线,这里内存水线检查函数还是有可能通不过的,这么一来就有两个结果:其一,选择其他的内存区分配内存,其二,提早触发zone回收流程。不管怎样都是为了避免过分分配页框导致当前zone内存碎片化。

 page_to_pfn,pfn_to_page 页描述符和页框号的相互转化

pfn_to_page完全由宏定义完成,代码如下:

1.第3行将pfn_to_page 定义为__pfn_to_page

2.第4行开始到第46行定义了具体的__pfn_to_page,__pfn_to_page的具体定义,受内核编译选项控制。具体由如下四个编译选项:CONFIG_FLATMEM,CONFIG_DISCONTIGMEM,CONFIG_SPARSEMEM_VMEMMAP,CONFIG_SPARSEMEM,注意编译控制是if,elif结构控制的,也就是说只能由一种选择,并且如果同时使能了上面几个编译选项(可以同时使能几个编译选项吗?),后面的编译选项会被前面的编译选项覆盖。作者自己的使用的内核没有定义CONFIG_FLATMEM和CONFIG_DISCONTIGMEM,但是同时定义了CONFIG_SPARSEMEM_VMEMMAP和CONFIG_SPARSEMEM。因此这里下面的宏定义中起作用的就是第24到29行。

结合第27行,第2行,第1行可以得到如下结论:

a:linux将所由的页描述符放到了一个大数组里面,数组的名字是vmemmap,数组的其实地址是0xffffea0000000000(一个核心态虚拟地址)

b:从pfn(页框号)到page(页描述符)的转换就是以pfn作为下标取vmemmap数组中的一个元素

c:CONFIG_SPARSEMEM_VMEMMAP的意思就是用一段虚拟地址连续的内核态地址段存放页描述符数组(这段内存的物理地址是不是连续的呢?)

结合第28行,第2行,第1行代码可以得到如下结论

a:从page到pfn的转换就是去了page再vmemmap数组中的下标

[注]:redhat6和redhat7系列的这两个宏定义就是这个逻辑,可以通过crash工具,利用“kmem -p"命令查看具体的vmemmap数组的基地址以及数组中页描述符

TODO:解释下其他的几个配置下,叶匡号和页描述符之间的转换逻辑

 1 #define VMEMMAP_START     _AC(0xffffea0000000000, UL)
 2 #define vmemmap ((struct page *)VMEMMAP_START)
 3 #define pfn_to_page __pfn_to_page
 4 #if defined(CONFIG_FLATMEM)
 5 
 6 #define __pfn_to_page(pfn)    (mem_map + ((pfn) - ARCH_PFN_OFFSET))
 7 #define __page_to_pfn(page)    ((unsigned long)((page) - mem_map) + \
 8                  ARCH_PFN_OFFSET)
 9 #elif defined(CONFIG_DISCONTIGMEM)
10 
11 #define __pfn_to_page(pfn)            \
12 ({    unsigned long __pfn = (pfn);        \
13     unsigned long __nid = arch_pfn_to_nid(__pfn);  \
14     NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
15 })
16 
17 #define __page_to_pfn(pg)                        \
18 ({    struct page *__pg = (pg);                    \
19     struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg));    \
20     (unsigned long)(__pg - __pgdat->node_mem_map) +            \
21      __pgdat->node_start_pfn;                    \
22 })
23 
24 #elif defined(CONFIG_SPARSEMEM_VMEMMAP)
25 
26 /* memmap is virtually contigious.  */
27 #define __pfn_to_page(pfn)    (vmemmap + (pfn))
28 #define __page_to_pfn(page)    (unsigned long)((page) - vmemmap)
29 
30 #elif defined(CONFIG_SPARSEMEM)
31 /*
32  * Note: section's mem_map is encorded to reflect its start_pfn.
33  * section[i].section_mem_map == mem_map's address - start_pfn;
34  */
35 #define __page_to_pfn(pg)                    \
36 ({    struct page *__pg = (pg);                \
37     int __sec = page_to_section(__pg);            \
38     (unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));    \
39 })
40 
41 #define __pfn_to_page(pfn)                \
42 ({    unsigned long __pfn = (pfn);            \
43     struct mem_section *__sec = __pfn_to_section(__pfn);    \
44     __section_mem_map_addr(__sec) + __pfn;        \
45 })
46 #endif
View Code

move_freepages和move_freepages_block:再不同的类型的buddy链表中移动页框

move_freepages

理解这两个函数读者要先了解两个概念:buddy系统,内存迁移类型,(TODO:解释buddy系统和内存类型的概念,以及作用)。在2.6.32源码中move_freepages只被move_freepages_block调用。内核中没有其他的逻辑在直接调用move_freepages. move_freepages代码如下:

函数作用:将buddy系统中一个链表中的伙伴页移动到两外一个链表中,由意义的代码是第20行到第40行的for循环,其他的代码是冗余代码没有任何实际意义。在没有定义CONFIG_HOLE_IN_ZONE(实际redhat6和redhat7发行版本里没有定义这个编译选项 TODO:什么意义?)的时,第24行逻辑始终未假,因此if分支不会走到。要理解这个函数要将剩下的第29行到39行作为一个整体看,这段代码是基于如下几个前提:

1.buddy系统中只有伙伴页的首页被链接到buddy系统的相应链表

2.buddy系统中只有伙伴页的首页被打上pg_buddy标志

3.buddy系统中只有伙伴页的首页其page->private才被设置成该伙伴页的阶

第29行if分支存在的愿意:start_page可能不是伙伴页的首页,因此接下来挨着start_page的若干页也可能不是伙伴系统的首页,用这个分支跳过这些页,已经在链表中移动是针对伙伴页的首页的。

经过第29行的检查通过的页一定是伙伴页的首页,因此才有34行从page->private中取到order,接着将首页从buddy链表中摘下来移动到对应order的目标迁移类型所在的链表上。因为一次移动虽然只移动了一个首页,根据伙伴系统的原理其实以这个首页未首的2的order次方个页都移动到了目标迁移类型上,所以才由pages_moved+=1<<order的逻辑。page_moved记录了移动的总页数。

结论:

1.该函数的作用是将start_page和end_page中间的页,以伙伴页未单位移动到目标迁移类型所在的链表上。并返回移动的总页数

 1 static int move_freepages(struct zone *zone,
 2               struct page *start_page, struct page *end_page,
 3               int migratetype)
 4 {
 5     struct page *page;
 6     unsigned long order;
 7     int pages_moved = 0;
 8 
 9 #ifndef CONFIG_HOLES_IN_ZONE
10     /*
11      * page_zone is not safe to call in this context when
12      * CONFIG_HOLES_IN_ZONE is set. This bug check is probably redundant
13      * anyway as we check zone boundaries in move_freepages_block().
14      * Remove at a later date when no bug reports exist related to
15      * grouping pages by mobility
16      */
17     BUG_ON(page_zone(start_page) != page_zone(end_page));
18 #endif
19 
20     for (page = start_page; page <= end_page;) {
21         /* Make sure we are not inadvertently changing nodes */
22         VM_BUG_ON(page_to_nid(page) != zone_to_nid(zone));
23 
24         if (!pfn_valid_within(page_to_pfn(page))) {
25             page++;
26             continue;
27         }
28 
29         if (!PageBuddy(page)) {
30             page++;
31             continue;
32         }
33 
34         order = page_order(page);
35         list_del(&page->lru);
36         list_add(&page->lru,
37             &zone->free_area[order].free_list[migratetype]);
38         page += 1 << order;
39         pages_moved += 1 << order;
40     }
41 
42     return pages_moved;
43 }
View Code

move_freepages_block

linux内核将物理页划分成了页块,页块是独立于页的阶的概念,具体的页块划分方法见下面代码第1行到第16行,第16行说明页块的大小是pageblock_order次方,在redhat6和redhat7发行版本中定义了CONFIG_HUGETLB_PAGE(内核支持透明巨页 TODO:解释透明巨页的概念),但是没有定义CONFIG_HUGETLB_PAGE_SIZE_VARIABLE(透明巨页的大小是可变的)。因此pageblock_order等于HUGETLB_PAGE_ORDER(透明巨页的阶:为什么要等于透明巨页的阶)。结合第1,2,3行代码看到透明巨页的页是9(PAGE_SHIFT是12)因此在上面的内核编译选项配置下得到结论:

1.透明巨页的大小是固定的,即2的9次方个页框,也就是2MB

2.buddy系统划分的页块大小等于透明巨页的大小也是2MB

【注】buddy系统最大的连续页框的阶是10,即最大连续物理地址大小是4MB,大于页块的大小,也大于透明巨页的大小。

因此可以得到move_freepages_block函数的作用如下:

1.将page所在的页块,整块一定到迁移类型migratetype所在的链表中,并处理如下连个特殊情况

a:如果页块的首页和page不在一个zone中,则从zone中的第一个页移动,知道页块结束

b:如果页块所在的尾页和page不在一个zone中,则不移动页块(为什么和a的处理逻辑不一样呢?)

解释:a的情况是存在的,因为页块划分时页块大小时固定的,而zone的大小也是固定的,所以有可能一个页块跨越了两个zone

【注1】由于伙伴系统做大连续物理页框的阶时10,而页块的连续物理内存的阶是9,因此一个页块中有可能包含若干个buddy系统中的伙伴页块,一个伙伴页块中也可能包含一个(当伙伴系统中的连续物理页的阶是9时)或两个页块(当伙伴系统中的连续物理页的阶时10时)

【注2】在移动的过程中保证伙伴系统中连续物理页的阶不变

(一个页块中的物理页有没有可能存在于不同的迁移类型链表中?答:有 ,举例说明?)

 1 #define PMD_SHIFT    21
 2 #define HPAGE_SHIFT        PMD_SHIFT
 3 #define HUGETLB_PAGE_ORDER    (HPAGE_SHIFT - PAGE_SHIFT)
 4 #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE
 5 /* Huge page sizes are variable */
 6 extern int pageblock_order;
 7 #else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */
 8 /* Huge pages are a constant size */
 9 #define pageblock_order        HUGETLB_PAGE_ORDER
10 #endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */
11 #else /* CONFIG_HUGETLB_PAGE */
12 /* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
13 #define pageblock_order        (MAX_ORDER-1)
14 #endif /* CONFIG_HUGETLB_PAGE */
15 
16 #define pageblock_nr_pages    (1UL << pageblock_order)
17 static int move_freepages_block(struct zone *zone, struct page *page,
18                 int migratetype)
19 {
20     unsigned long start_pfn, end_pfn;
21     struct page *start_page, *end_page;
22 
23     start_pfn = page_to_pfn(page);
24     start_pfn = start_pfn & ~(pageblock_nr_pages-1);
25     start_page = pfn_to_page(start_pfn);
26     end_page = start_page + pageblock_nr_pages - 1;
27     end_pfn = start_pfn + pageblock_nr_pages - 1;
28 
29     /* Do not cross zone boundaries */
30     if (start_pfn < zone->zone_start_pfn)
31         start_page = page;
32     if (end_pfn >= zone->zone_start_pfn + zone->spanned_pages)
33         return 0;
34 
35     return move_freepages(zone, start_page, end_page, migratetype);
36 }
View Code

__rmqueue_smallest

 函数作用:根据入参zone,order以及migratetype 从buddy系统伙伴页链表中找到2的order次方个连续的物理页框返回,如果找到返回首页的页描述符,如果找不到返回null

代码第10行到第22行从入参order向高阶内存迭代:

a:第12行逻辑:如果current_order阶migratetype所在的伙伴页链表为空则迭代下一个较高阶的伙伴页链表

b:第15行到19行:如果current_order阶migratetype所在的伙伴页链表不为空,则取下链表中的一个伙伴页组,将首页的页描述符的order信息抹掉(page->private=0),接着递减current_order阶伙伴页组的个数

c:第20行:存在的原因是:有可能current_order大于order,如果大于,几个例子:当current_order=4,但是order等于2时,就将2的4次方个页(16个页)中的最后8个放到3阶内存migratetype对应的伙伴页链表上,第4到第7个页放到2阶内存migratetype对应的伙伴页链表上,这时page刚好是第0个页到第3个页的首页,因为第17行已经将page从阶为4的内存migratetype链表上摘下来了,page为首的四个页不在buddy系统,作为结果返回。

d:如果从order一直迭代到MAX_ORDER,migratetype对应的伙伴页链表都是空的,(即:当前zone中migratetype对应的所有伙伴页链表都时空的)则返回NULL。

【注】这个函数不牵涉到不同migratetype链表之间伙伴页组的移动,分配到的页组,以及分配过程中伙伴页组的移动都是在同一个zone同一个migratetype所在的链表之间移动(expand

 1 static inline
 2 struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
 3                         int migratetype)
 4 {
 5     unsigned int current_order;
 6     struct free_area * area;
 7     struct page *page;
 8 
 9     /* Find a page of the appropriate size in the preferred list */
10     for (current_order = order; current_order < MAX_ORDER; ++current_order) {
11         area = &(zone->free_area[current_order]);
12         if (list_empty(&area->free_list[migratetype]))
13             continue;
14 
15         page = list_entry(area->free_list[migratetype].next,
16                             struct page, lru);
17         list_del(&page->lru);
18         rmv_page_order(page);
19         area->nr_free--;
20         expand(zone, page, order, current_order, area, migratetype);
21         return page;
22     }
23 
24     return NULL;
25 }
View Code

pfn_to_section_nr,__nr_to_section,__pfn_to_section

 __pfn_to_section:由page页的物理页框号导出页所在的物理内存section,linux会将内存划分成更小的连续物理内存段,不同的划分规则,对应不同的管理目的,由此引出了下面几个即相互关联又相互独立的概念:node(内存节点),zone(内存去),section(为了更好的支持稀疏内存:为啥在稀疏内存的时候会划分section?),order(著名的buddy系统11阶划分法),page block(页块,为了支持内存迁移类型的概念,内存迁移类型是以page block为单位的)。__pfn_to_section就是由page页的叶匡号导出描述该页框所在的section的描述符。

pfn_to_section_nr:每一个section有一个编号,姑且称为section num,该函数的作用就是将页框号转化成页所在的section的section num

代码第30行到第47行给出了pfn_to_section_nr的实现,同时也给出了划分section的方法,在划分section时满足如下约束条件:

1.系统中所有的section中物理页框个数彼此相同并且对其到2的order次方(代码第42,43行)。

2.每一个section中所有的物理页框在物理上连续(物理叶匡号是连续的)。

在linux具体实现中,section的长度是128MB(第37行),因此将pfn向左移动15位就是对应的section num(假设页框大小是4KB,pfn等于对应的页框物理首地址向右移动12位,因此section num等于pfn对应页框的物理首地址向左移动27位)

__nr_to_section:该函数通过secton num得到section描述符,同时也反应系统中所有section描述符的组织形式,代码第1行到24行给出了该函数的实现。系统中所有的section描述符的组织形式如下:

1.所有的section描述符都在一个二维数组里(代码13到17行,redhat6和redhat7发行版本中默认开启了:CONFIG_SPARSEMEM_EXTREME),数组的第二维长度是4KB(第7到11行)。系统中最大的section个数是2的19次方个(代码1,3,37,39行)。 

【注1】:__pfn_to_section其实就是先通过pfn_to_section_nr将页框号导出页所在section的section num,再由section num导出section描述符

【注2】:section描述的的是对物理地址的划分,划分标准:等大,section中的物理页相邻.

【注3】:每一个section又一个编号section num,从0开始到最大值(见上文),每个section又一个section描述符,所有的section描述符放到一个全局二维数组里面,数组第二位长度大小是4KB

TODO:解释为啥要将内存划分成section

 1 #define SECTIONS_SHIFT        (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)
 2 
 3 #define NR_MEM_SECTIONS        (1UL << SECTIONS_SHIFT)
 4 
 5 #define NR_SECTION_ROOTS    (NR_MEM_SECTIONS / SECTIONS_PER_ROOT)
 6 
 7 #ifdef CONFIG_SPARSEMEM_EXTREME
 8 #define SECTIONS_PER_ROOT       (PAGE_SIZE / sizeof (struct mem_section))
 9 #else
10 #define SECTIONS_PER_ROOT    1
11 #endif
12 
13 #ifdef CONFIG_SPARSEMEM_EXTREME
14 extern struct mem_section *mem_section[NR_SECTION_ROOTS];
15 #else
16 extern struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT];
17 #endif
18 #define SECTION_NR_TO_ROOT(sec)    ((sec) / SECTIONS_PER_ROOT)
19 static inline struct mem_section *__nr_to_section(unsigned long nr)
20 {
21     if (!mem_section[SECTION_NR_TO_ROOT(nr)])
22         return NULL;
23     return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
24 }
25 
26 #ifdef CONFIG_X86_32
27 # ifdef CONFIG_X86_PAE
28 #  define SECTION_SIZE_BITS    29
29 #  define MAX_PHYSADDR_BITS    36
30 #  define MAX_PHYSMEM_BITS    36
31 # else
32 #  define SECTION_SIZE_BITS    26
33 #  define MAX_PHYSADDR_BITS    32
34 #  define MAX_PHYSMEM_BITS    32
35 # endif
36 #else /* CONFIG_X86_32 */
37 # define SECTION_SIZE_BITS    27 /* matt - 128 is convenient right now */
38 # define MAX_PHYSADDR_BITS    44
39 # define MAX_PHYSMEM_BITS    46
40 #endif
41 
42 #define PFN_SECTION_SHIFT    (SECTION_SIZE_BITS - PAGE_SHIFT)
43 #define pfn_to_section_nr(pfn) ((pfn) >> PFN_SECTION_SHIFT)
44 
45 static inline struct mem_section *__pfn_to_section(unsigned long pfn)
46 {
47     return __nr_to_section(pfn_to_section_nr(pfn));
48 }
View Code

set_pageblock_flags_group,set_pageblock_migratetype

 每一个page block有一组标志,标志该page block的属性,该函数就是设置page所在的page block对应的标志位。由于在内核源码中该函数只被set_pageblock_migratetype调用,因此将这两个函数放到一起来说,redhat6,7模式具体是现实如下:

1.每一个page block有且仅有3个标志位,分别用来标志page block的4个迁移属性(MIGRATE_UNMOVABLE,MIGRATE_RECLAIMABLE,MIGRATE_MOVABLE,MIGRATE_ISOLATE),解释下:3个标志位全位0时表示该page block的迁移属性时MIGRATE_UNMOVABLE,所以可以用3个标志位表示四个迁移属性。

3.标志位存放在section描述符中,由于一个section的长度时128MB(见上文),而每个page block的长度时2MB(见上文),因此每个section可以包含64个page block,一个page block需要3bit的flag,所以在对应的section描述符中就存放了3*64/8bit长度的字节数组描述该section中所有page block的标志位

4.针对每一个page block标志位如下:000:MIGRATE_UNMOVABLE,001:MIGRATE_RECLAIMABLE,010:MIGRATE_MOVABLE,100:MIGRATE_ISOLATE(是否可以同时设置多个标志位,比如:111,110,011,101这种组合?

 1 #define MIGRATE_UNMOVABLE 0
 2 #define MIGRATE_RECLAIMABLE 1
 3 #define MIGRATE_MOVABLE 2
 4 #define MIGRATE_PCPTYPES 3 /* the number of types on the pcp lists */
 5 #define MIGRATE_RESERVE 3
 6 #define MIGRATE_ISOLATE 4 /* can't allocate from here */
 7 #define MIGRATE_TYPES 5
 8 /**
 9  * set_pageblock_flags_group - Set the requested group of flags for a pageblock_nr_pages block of pages
10  * @page: The page within the block of interest
11  * @start_bitidx: The first bit of interest
12  * @end_bitidx: The last bit of interest
13  * @flags: The flags to set
14  */
15 void set_pageblock_flags_group(struct page *page, unsigned long flags,
16                     int start_bitidx, int end_bitidx)
17 {
18     struct zone *zone;
19     unsigned long *bitmap;
20     unsigned long pfn, bitidx;
21     unsigned long value = 1;
22 
23     zone = page_zone(page);
24     pfn = page_to_pfn(page);
25     bitmap = get_pageblock_bitmap(zone, pfn);
26     bitidx = pfn_to_bitidx(zone, pfn);
27     VM_BUG_ON(pfn < zone->zone_start_pfn);
28     VM_BUG_ON(pfn >= zone->zone_start_pfn + zone->spanned_pages);
29 
30     for (; start_bitidx <= end_bitidx; start_bitidx++, value <<= 1)
31         if (flags & value)
32             __set_bit(bitidx + start_bitidx, bitmap);
33         else
34             __clear_bit(bitidx + start_bitidx, bitmap);
35 }
36 
37 static void set_pageblock_migratetype(struct page *page, int migratetype)
38 {
39 
40     if (unlikely(page_group_by_mobility_disabled))
41         migratetype = MIGRATE_UNMOVABLE;
42 
43     set_pageblock_flags_group(page, (unsigned long)migratetype,
44                     PB_migrate, PB_migrate_end);
45 }
View Code

change_pageblock_range  

 start_order是buddy系统的阶,pageblock_page是一个阶为start_order的内存块的首页描述符。函数功能:将以pageblock_page为首页的阶为start_order的内存段中包含的所有页块儿的迁移类型设置为migratetype.

第4行:求start_order阶内存段中包含了多少个页块(pageblock_order见上文)

第6-9行:开始迭代,将所有的页块的迁移类型设置为migratetype

【注】内核所有的控制路径(其实就一处调用该函数),在调用该函数前,都检查了start_order是否大于pageblock_order,只有在大于的情况下才调用该函数,因此第4行是安全的

 1 static void change_pageblock_range(struct page *pageblock_page,
 2                     int start_order, int migratetype)
 3 {
 4     int nr_pageblocks = 1 << (start_order - pageblock_order);
 5 
 6     while (nr_pageblocks--) {
 7         set_pageblock_migratetype(pageblock_page, migratetype);
 8         pageblock_page += pageblock_nr_pages;
 9     }
10 }
View Code

__rmqueue_fallback

当前zone在start_migratetype表示的迁移类型链表中分配order阶内存失败(参考:__rmqueue_smalles函数)后,会调用到该函数,从而,从当前zone中后备迁移类型链表中分配内存,后备迁移类型链表是通过静态定义一个规则确定的。

代码第5-10行:各个类型的迁移链表的后备迁移类型链表,其中等号左边时start_migratetype的可能类型(MIGRATE_RESERVE除外),等号的右边是等号左边满足不了order阶内存申请时,依次fallback到的迁移类型链表

代码第21-76行:按内存阶从大到小迭代,开始时current_order=MAX_ORDER-1(10),结束时current_order大于等于order(入参),这里是外层迭代

代码第23-75行:在current_order下根据start_migratetype和fallback规则,迭代规则中定义的所有fallback迁移类型链表,当前迁移类型是migratetype

代码第27-28行:保证该函数不处理迁移类型:MIGRATE_RESERVE(为啥?)

代码第30-36行:判断current_order对应的area中迁移类型位migratetype的链表是否未空:如果为空,则迭代下一个迁移类型;如果不为空,则取到链表中第一个current_order阶连续物理页框的第一个页的页描述符地址

代码第44-58行:如果current_order大于等于pageblock_order/2,或者start_migratetype为MIGRATE_RECLAIMABLE,或者 page_group_by_mobility_disabled = 1时,执行如下操作:

1.将当前page所在的page block中的所有空闲页都移动到start_migratetype所在的链表中(参考上文:move_freepages_block)

2.如果1中移动的空闲页个数大于等于2^(pageblock_order-1),则将当前页所在的page block的类型设置成start_migratetype(参考上文:set_pageblock_migratetype)

3.函数执行到44行if分支,可以确定整个函数,将在本次内迭代中结束,又因为在本分支中将从page开始的current_order阶个连续页框已经移动到start_migratetype对应的链表上,为了第65行到74行处理的统一性(即在进入44行分支和不进入44行分支两种情况下,65行和74行的逻辑使用相同的代码)将migratetype改成了start_migratetype.

代码第61-62行:将current_order阶个连续物理页框从其所在的链表上摘下来(这里有可能经过44行的分支迁移过,也可能没有经过第44行的分支迁移过),将page->private清零

代码第65-67行:如果current_order大于pageblock_order(这种情况是存在的,具体见上文关于move_freepages_block的解释),就将当前这个current_order大小的连续物理页框中的所有page block的迁移类型都设置成start_migratetype(注意:这一步和44-58行中的第2步的功能并不完全等价,即在current_order>=pageblock_order+2时必须用到该逻辑(实际上这种情况在redhat6,7的实现中是不存在的,之所以这么写可能作者考虑到max page order和page block order是可配置的,这么些逻辑上比较完备))

代码第69行:将以page 为首的current_order阶连续物理内存分裂出order大小,作为返回值返回。将分裂出的剩余内存放回到对应的buddy系统链表。

代码第78行:如果两层迭代完后仍然找不到非空的链表,则返回NULL,表示本轮fallback分配失败

 1 /*
 2  * This array describes the order lists are fallen back to when
 3  * the free lists for the desirable migrate type are depleted
 4  */
 5 static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
 6     [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_RESERVE },
 7     [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_RESERVE },
 8     [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
 9     [MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,   MIGRATE_RESERVE }, /* Never used */
10 };
11 /* Remove an element from the buddy allocator from the fallback list */
12 static inline struct page *
13 __rmqueue_fallback(struct zone *zone, int order, int start_migratetype)
14 {
15     struct free_area * area;
16     int current_order;
17     struct page *page;
18     int migratetype, i;
19 
20     /* Find the largest possible block of pages in the other list */
21     for (current_order = MAX_ORDER-1; current_order >= order;
22                         --current_order) {
23         for (i = 0; i < MIGRATE_TYPES - 1; i++) {
24             migratetype = fallbacks[start_migratetype][i];
25 
26             /* MIGRATE_RESERVE handled later if necessary */
27             if (migratetype == MIGRATE_RESERVE)
28                 continue;
29 
30             area = &(zone->free_area[current_order]);
31             if (list_empty(&area->free_list[migratetype]))
32                 continue;
33 
34             page = list_entry(area->free_list[migratetype].next,
35                     struct page, lru);
36             area->nr_free--;
37 
38             /*
39              * If breaking a large block of pages, move all free
40              * pages to the preferred allocation list. If falling
41              * back for a reclaimable kernel allocation, be more
42              * agressive about taking ownership of free pages
43              */
44             if (unlikely(current_order >= (pageblock_order >> 1)) ||
45                     start_migratetype == MIGRATE_RECLAIMABLE ||
46                     page_group_by_mobility_disabled) {
47                 unsigned long pages;
48                 pages = move_freepages_block(zone, page,
49                                 start_migratetype);
50 
51                 /* Claim the whole block if over half of it is free */
52                 if (pages >= (1 << (pageblock_order-1)) ||
53                         page_group_by_mobility_disabled)
54                     set_pageblock_migratetype(page,
55                                 start_migratetype);
56 
57                 migratetype = start_migratetype;
58             }
59 
60             /* Remove the page from the freelists */
61             list_del(&page->lru);
62             rmv_page_order(page);
63 
64             /* Take ownership for orders >= pageblock_order */
65             if (current_order >= pageblock_order)
66                 change_pageblock_range(page, current_order,
67                             start_migratetype);
68 
69             expand(zone, page, order, current_order, area, migratetype);
70 
71             trace_mm_page_alloc_extfrag(page, order, current_order,
72                 start_migratetype, migratetype);
73 
74             return page;
75         }
76     }
77 
78     return NULL;
79 }
View Code

 __rmqueue

函数作用:在zone中分配order阶连续的物理内存,在分配的时候首先在在migratetype类型的内存中分配,如果分配不到,就fallback到其他迁移类型的内存中分配内存。

函数第13行的if分支最多进入依次,也就是说第23行的goto语句最多被执行一次:if分支被执行一次的条件:__rmqueue_smallest函数返回NULL且migratetype不为MIGRATE_RESERVE,goto被执行一次的条件是:__rmqueue_smallest返回NULL且migratetype不为MIGRATE_RESERVE且__rmqueue_fallback也返回了NULL,

goto语句意义:如果migratetype不为MIGRATE_RESERVE,如果__rmqueue_smallest函数返回NULL,并且__rmqueue_fallback函数也返回NULL,则将migratetype设置成MIGRATE_RESERVE,从新调用__rmqueue_smallest函数。关于__rmqueue_smallest函数以及__rm_queue_fallback函数的说明参考上文。

【注】:_rmqueue_fallback函数是不会从MIGRATE_RESERVE类型的内存中分配内存的,只有__rmqueue_smallest函数会从MIGRATE_RESERVE类型的内存中分配内存。也就是说:该函数实现的核心功能:如果migratetype不等于MIGRATETYPE_RESERVE,那么先从migratetype的链表中分配内存(第11行),如果分配失败,根据fallback规则依次从migratetype的fallback链表中分配内存(第14行,注意这里是不处理MIGRATETYPE_RESERVE迁移类型链表的),如果仍然分配失败,则从MIGRATE_RESERVE链表中分配内存。

 1 /*
 2  * Do the hard work of removing an element from the buddy allocator.
 3  * Call me with the zone->lock already held.
 4  */
 5 static struct page *__rmqueue(struct zone *zone, unsigned int order,
 6                         int migratetype)
 7 {
 8     struct page *page;
 9 
10 retry_reserve:
11     page = __rmqueue_smallest(zone, order, migratetype);
12 
13     if (unlikely(!page) && migratetype != MIGRATE_RESERVE) {
14         page = __rmqueue_fallback(zone, order, migratetype);
15 
16         /*
17          * Use MIGRATE_RESERVE rather than fail an allocation. goto
18          * is used because __rmqueue_smallest is an inline function
19          * and we want just one call site
20          */
21         if (!page) {
22             migratetype = MIGRATE_RESERVE;
23             goto retry_reserve;
24         }
25     }
26 
27     trace_mm_page_alloc_zone_locked(page, order, migratetype);
28     return page;
29 }
View Code

 rmqueue_bulk

 从zone中最多申请count个order阶连续内存段,并且将这些连续内存段的首页描述符的private字段初始化为migrate类型,然后将这些描述符挂接到list链表上,返回实际申请到的连续order阶物理连续内存段的实际个数

代码第12行:获取自旋锁(避免多cpu上执行路径产生竞争),因为在调用该函数之间已经关闭了中断(避免了进程上下文和中断上下文产生的竞争:参考下文:buffered_rmqueue),关了抢占(避免当前cpu上由于任务切换导致不同进程上下文的竞争:参考下文:buffered_rmqueue),因此这三个条件只能保证了只有在当前cpu上的当前上下文中才能修改zone相关,以及zone中page对应的pageblock相关,以及当前cpu中的percpu_pageset中相关的全局数据(接下来的__rmqueue函数会修改zone相关,以及zone中page对应的pageblock相关的数据--参考上文:__rmqueue函数。本函数剩下逻辑会修改percpu_pageset以及zone相关的数据)

代码第13行:开始count次迭代,每次迭代从zone中以migratetype的为首选迁移类型试图分配一段order阶连续的物理内存(注意:这里分配到的内存的迁移类型不一定就是migratetype,因为__rmqueue会根据迁移类型的fallback规则分配其他迁移类型下的页,具体参考函数:__rmqueue

代码第15-16行:如果分配失败,证明当前zone中迁移类型为MIGRATE_UNMOVABLE,MIGRATE_RELAIMABLE,MIGRATE_MOVABLE,MIGRATE_RESERVE这几个迁移类型对应的链表中没有大于等于order阶的连续物理内存了,接着尝试分配意义不大,因此这里break跳出循环

代码第27-30行:如果是cold为真,则将page挂到list链表的结尾,如果cold为假,就将page挂到list的开头(为什么要这么做?)

代码第31行:将page的private字段设置为migratetype(这里有个疑问:其实这个内存段所在的迁移类型链表,以及对应的pageblock的迁移类型都有可能不为migratetype,这里将page的private字段设置为migratetype是否有意义呢?)

代码第34行:退出for迭代后,i等于实际分配到的连续物理内存段的个数,这里将zone中的NR_FREE_PAGE(zone中空闲页的总数)剪掉i*2^order个

代码第36行:释放自旋锁,和代码第12行对应

【注】代码第18行到25行的注释,值得把玩下,结合page cache的分配流程,以及第32,27-32行的逻辑就能体会到作者的用意。

 1 /* 
 2  * Obtain a specified number of elements from the buddy allocator, all under
 3  * a single hold of the lock, for efficiency.  Add them to the supplied list.
 4  * Returns the number of new pages which were placed at *list.
 5  */
 6 static int rmqueue_bulk(struct zone *zone, unsigned int order, 
 7             unsigned long count, struct list_head *list,
 8             int migratetype, int cold)
 9 {
10     int i;
11     
12     spin_lock(&zone->lock);
13     for (i = 0; i < count; ++i) {
14         struct page *page = __rmqueue(zone, order, migratetype);
15         if (unlikely(page == NULL))
16             break;
17 
18         /*
19          * Split buddy pages returned by expand() are received here
20          * in physical page order. The page is added to the callers and
21          * list and the list head then moves forward. From the callers
22          * perspective, the linked list is ordered by page number in
23          * some conditions. This is useful for IO devices that can
24          * merge IO requests if the physical pages are ordered
25          * properly.
26          */
27         if (likely(cold == 0))
28             list_add(&page->lru, list);
29         else
30             list_add_tail(&page->lru, list);
31         set_page_private(page, migratetype);
32         list = &page->lru;
33     }
34     __mod_zone_page_state(zone, NR_FREE_PAGES, -(i << order));
35     spin_unlock(&zone->lock);
36     return i;
37 }
View Code

 __mod_zone_page_state,__count_zone_vm_events,zone_statistics

这几个函数用于更新zone相关的统计信息,首先看__mod_zone_page_state,item是需要修改的具体统计项,代码第1到38行给出了zone相关的所有统计项,linux用一个数组记录这些统计数据,数组的长度是统计项的个数(NR_Vm_ZONE_STAT_ITEMS),在系统中存在三种这样的数组,可以从下面的代码看出都有那三种。代码第45行去了第一种数组:zone中有一个percpu变量:per_cpu_pageset。该变量中有一个统计数组,也就是说这种统计数组是“每cpu,每zone的”

代码第46-49行:获取了item对应的统计项的当前值(46行),并将本次的差分累加到统计项上(49行)

代码第51行:如果累加后的统计项大于pcp->stat_threadhold或小于负的pcp->stat_threadhold 就调用zone_page_stat_add函数,将该项的“每cpu统计值”一次累加到zone的整体统计项里,同时,将该项的“每cpu统计值”一次累加到系统全局的统计项里,并将percpu中该项对应的统计项清0.

代码第57-62行:每个zone有一个统计数组,记录该zone的整体统计信息(代码第60行),系统中有一个全局的统计数组,记录了系统中所有内存的统计信息(第61行)该函数的作用就是

【注】TODO:解释:1.为啥需要针对zone做每cpu统计数组?2.zone整体统计信息有没有可能出现负值?3.系统全局的统计项信息有没有可能出现负值?4.percpu的统计项信息有没有可能出现负值?

 1 enum zone_stat_item {
 2     /* First 128 byte cacheline (assuming 64 bit words) */
 3     NR_FREE_PAGES,
 4     NR_LRU_BASE,
 5     NR_INACTIVE_ANON = NR_LRU_BASE, /* must match order of LRU_[IN]ACTIVE */
 6     NR_ACTIVE_ANON,        /*  "     "     "   "       "         */
 7     NR_INACTIVE_FILE,    /*  "     "     "   "       "         */
 8     NR_ACTIVE_FILE,        /*  "     "     "   "       "         */
 9     NR_UNEVICTABLE,        /*  "     "     "   "       "         */
10     NR_MLOCK,        /* mlock()ed pages found and moved off LRU */
11     NR_ANON_PAGES,    /* Mapped anonymous pages */
12     NR_FILE_MAPPED,    /* pagecache pages mapped into pagetables.
13                only modified from process context */
14     NR_FILE_PAGES,
15     NR_FILE_DIRTY,
16     NR_WRITEBACK,
17     NR_SLAB_RECLAIMABLE,
18     NR_SLAB_UNRECLAIMABLE,
19     NR_PAGETABLE,        /* used for pagetables */
20     NR_KERNEL_STACK,
21     /* Second 128 byte cacheline */
22     NR_UNSTABLE_NFS,    /* NFS unstable pages */
23     NR_BOUNCE,
24     NR_VMSCAN_WRITE,
25     NR_WRITEBACK_TEMP,    /* Writeback using temporary buffers */
26     NR_ISOLATED_ANON,    /* Temporary isolated pages from anon lru */
27     NR_ISOLATED_FILE,    /* Temporary isolated pages from file lru */
28     NR_SHMEM,        /* shmem pages (included tmpfs/GEM pages) */
29 #ifdef CONFIG_NUMA
30     NUMA_HIT,        /* allocated in intended node */
31     NUMA_MISS,        /* allocated in non intended node */
32     NUMA_FOREIGN,        /* was intended here, hit elsewhere */
33     NUMA_INTERLEAVE_HIT,    /* interleaver preferred this zone */
34     NUMA_LOCAL,        /* allocation from local node */
35     NUMA_OTHER,        /* allocation from other node */
36 #endif
37     NR_VM_ZONE_STAT_ITEMS 
38 };
39 /*
40  * For use when we know that interrupts are disabled.
41  */
42 void __mod_zone_page_state(struct zone *zone, enum zone_stat_item item,
43                 int delta)
44 {
45     struct per_cpu_pageset *pcp = zone_pcp(zone, smp_processor_id());
46     s8 *p = pcp->vm_stat_diff + item;
47     long x;
48 
49     x = delta + *p;
50 
51     if (unlikely(x > pcp->stat_threshold || x < -pcp->stat_threshold)) {
52         zone_page_state_add(x, zone, item);
53         x = 0;
54     }
55     *p = x;
56 }
57 static inline void zone_page_state_add(long x, struct zone *zone,
58                  enum zone_stat_item item)
59 {
60     atomic_long_add(x, &zone->vm_stat[item]);
61     atomic_long_add(x, &vm_stat[item]);
62 }
View Code

 __count_zone_vm_events

代码第1-28行:定义了需要统计的事件类型

代码第29-32行:获取每cpu变量vm_event_states,将delta变量累加到对应的时间类型

代码第33-35行:将delta累加到zone对应的item统计事件类型上,对第34行的理解可以结合代码第1行的定义。

 1 #define FOR_ALL_ZONES(xx) DMA_ZONE(xx) DMA32_ZONE(xx) xx##_NORMAL HIGHMEM_ZONE(xx) , xx##_MOVABLE
 2 
 3 enum vm_event_item { PGPGIN, PGPGOUT, PSWPIN, PSWPOUT,
 4         FOR_ALL_ZONES(PGALLOC),
 5         PGFREE, PGACTIVATE, PGDEACTIVATE,
 6         PGFAULT, PGMAJFAULT,
 7         FOR_ALL_ZONES(PGREFILL),
 8         FOR_ALL_ZONES(PGSTEAL),
 9         FOR_ALL_ZONES(PGSCAN_KSWAPD),
10         FOR_ALL_ZONES(PGSCAN_DIRECT),
11 #ifdef CONFIG_NUMA
12         PGSCAN_ZONE_RECLAIM_FAILED,
13 #endif
14         PGINODESTEAL, SLABS_SCANNED, KSWAPD_STEAL, KSWAPD_INODESTEAL,
15         PAGEOUTRUN, ALLOCSTALL, PGROTATED,
16 #ifdef CONFIG_HUGETLB_PAGE
17         HTLB_BUDDY_PGALLOC, HTLB_BUDDY_PGALLOC_FAIL,
18 #endif
19         UNEVICTABLE_PGCULLED,    /* culled to noreclaim list */
20         UNEVICTABLE_PGSCANNED,    /* scanned for reclaimability */
21         UNEVICTABLE_PGRESCUED,    /* rescued from noreclaim list */
22         UNEVICTABLE_PGMLOCKED,
23         UNEVICTABLE_PGMUNLOCKED,
24         UNEVICTABLE_PGCLEARED,    /* on COW, page truncate */
25         UNEVICTABLE_PGSTRANDED,    /* unable to isolate on unlock */
26         UNEVICTABLE_MLOCKFREED,
27         NR_VM_EVENT_ITEMS
28 };
29 static inline void __count_vm_events(enum vm_event_item item, long delta)
30 {
31     __get_cpu_var(vm_event_states).event[item] += delta;
32 }
33 #define __count_zone_vm_events(item, zone, delta) \
34         __count_vm_events(item##_NORMAL - ZONE_NORMAL + \
35         zone_idx(zone), delta)
View Code

 zone_statistics

该函数主要做NUMA相关的统计包括:NUMA_HIT,NUMA_MISS,NUMA_LOCAL,NUMA_OTHER. numa hit:故名思意,就是如果页分配流程分配到的页所在的内存区和首选内存区来至同一个numa节点时,则认为分配命中了numa节点;相反如果页分配流程分配到的页所在的内存区和首选内存区不属于同一个内存节点,则认为本次分配MISS了。如果页分配流程分配到的页所在的内存区属于当前cpu的本地numa节点,则累加NUMA_LOCAL技术,想法则认为本次内存分配分配到了非本地numa节点的内存,则递增NUMA_OTHER计数。该函数主要为了跟踪系统在多zone,多numa节点的内存分配情况。

代码9-10行:如果z所在的内存区和perferred_zone来至同一个内存节点就增加z和全局统计的NUMA_HIT计数

代码11-13行:否则就增加z和全局的NUMA_MISS计数,并增加perferred_zone和全局的NUMA_FOREIGN计数

代码15-17行:如果z所在的内存节点是前cpu的本地numa节点,则增加z和全局NUMA_LOCAL计数

代码17-18行:否则增加z和全局的NUMA_OTHER计数

代码第21-56行:给出了zone_statistics调用的子函数:__inc_zone_state的逻辑,该函数实际逻辑和上文提到的__mod_zone_page_state函数相同,同时又一下区别:

第一个不同:__mod_zone_page_state每次更改zone相关的统计信息时,一次可以改变绝对值大于等于1的值,为什么时觉得值呢?因为__mod_zone_page_state可以将值改大或者改小;但是__inc_zone_state只能改大相关统计项的值,并且一次只能增加1。既然__mod_zone_page_state实现了__inc_zone_state的功能,为啥还要定义这么个“多余”的函数呢?原因:递增zone统计值的操作在内核热点路径上,这么以来该函数的执行效率就变得非常非常重要,对比__mod_zone_page_state的代码,代码第43行的++操作会被编译器优化成inc指令,而__mod_zone_page_state就只能用add指令,inc指令比add指令执行的更快;再者:对比55行的逻辑,正式因为__inc_zone_state函数只增不减,相比较__mod_zone_page_state少了一个判断分支,这也可以提高执行效率。因此在设计内核代码时,一定先评估所写的代码执行热度,并且尝试优化任何可优化的内容(甚至不惜嵌入汇编指令)

第二个不同:代码第50-54行:__mod_zone_page_state这里判断per cpu统计量大于stat_threadhold后,就直接将per cpu统计量分别累加到zone中的统计量和全局统计量,但是__inc_zone_state却只累加了一般(为什么?)

 1 /*
 2  * zonelist = the list of zones passed to the allocator
 3  * z         = the zone from which the allocation occurred.
 4  *
 5  * Must be called with interrupts disabled.
 6  */
 7 void zone_statistics(struct zone *preferred_zone, struct zone *z)
 8 {
 9     if (z->zone_pgdat == preferred_zone->zone_pgdat) {
10         __inc_zone_state(z, NUMA_HIT);
11     } else {
12         __inc_zone_state(z, NUMA_MISS);
13         __inc_zone_state(preferred_zone, NUMA_FOREIGN);
14     }
15     if (z->node == numa_node_id())
16         __inc_zone_state(z, NUMA_LOCAL);
17     else
18         __inc_zone_state(z, NUMA_OTHER);
19 }
20 /*
21  * Optimized increment and decrement functions.
22  *
23  * These are only for a single page and therefore can take a struct page *
24  * argument instead of struct zone *. This allows the inclusion of the code
25  * generated for page_zone(page) into the optimized functions.
26  *
27  * No overflow check is necessary and therefore the differential can be
28  * incremented or decremented in place which may allow the compilers to
29  * generate better code.
30  * The increment or decrement is known and therefore one boundary check can
31  * be omitted.
32  *
33  * NOTE: These functions are very performance sensitive. Change only
34  * with care.
35  *
36  * Some processors have inc/dec instructions that are atomic vs an interrupt.
37  * However, the code must first determine the differential location in a zone
38  * based on the processor number and then inc/dec the counter. There is no
39  * guarantee without disabling preemption that the processor will not change
40  * in between and therefore the atomicity vs. interrupt cannot be exploited
41  * in a useful way here.
42  */
43 void __inc_zone_state(struct zone *zone, enum zone_stat_item item)
44 {
45     struct per_cpu_pageset *pcp = zone_pcp(zone, smp_processor_id());
46     s8 *p = pcp->vm_stat_diff + item;
47 
48     (*p)++;
49 
50     if (unlikely(*p > pcp->stat_threshold)) {
51         int overstep = pcp->stat_threshold / 2;
52 
53         zone_page_state_add(*p + overstep, zone, item);
54         *p = -overstep;
55     }
56 }
View Code

 buffered_rmqueue

 从zone中分配一个order阶的物理连续内存段,首选迁移类型为:migratetype,首选内存去是perferred_zone:

 1 /*
 2  * Really, prep_compound_page() should be called from __rmqueue_bulk().  But
 3  * we cheat by calling it from here, in the order > 0 path.  Saves a branch
 4  * or two.
 5  */
 6 static inline
 7 struct page *buffered_rmqueue(struct zone *preferred_zone,
 8             struct zone *zone, int order, gfp_t gfp_flags,
 9             int migratetype)
10 {
11     unsigned long flags;
12     struct page *page;
13     int cold = !!(gfp_flags & __GFP_COLD);
14     int cpu;
15 
16 again:
17     cpu  = get_cpu();
18     if (likely(order == 0)) {
19         struct per_cpu_pages *pcp;
20         struct list_head *list;
21 
22         pcp = &zone_pcp(zone, cpu)->pcp;
23         list = &pcp->lists[migratetype];
24         local_irq_save(flags);
25         if (list_empty(list)) {
26             pcp->count += rmqueue_bulk(zone, 0,
27                     pcp->batch, list,
28                     migratetype, cold);
29             if (unlikely(list_empty(list)))
30                 goto failed;
31         }
32 
33         if (cold)
34             page = list_entry(list->prev, struct page, lru);
35         else
36             page = list_entry(list->next, struct page, lru);
37 
38         list_del(&page->lru);
39         pcp->count--;
40     } else {
41         if (unlikely(gfp_flags & __GFP_NOFAIL)) {
42             /*
43              * __GFP_NOFAIL is not to be used in new code.
44              *
45              * All __GFP_NOFAIL callers should be fixed so that they
46              * properly detect and handle allocation failures.
47              *
48              * We most definitely don't want callers attempting to
49              * allocate greater than order-1 page units with
50              * __GFP_NOFAIL.
51              */
52             WARN_ON_ONCE(order > 1);
53         }
54         spin_lock_irqsave(&zone->lock, flags);
55         page = __rmqueue(zone, order, migratetype);
56         __mod_zone_page_state(zone, NR_FREE_PAGES, -(1 << order));
57         spin_unlock(&zone->lock);
58         if (!page)
59             goto failed;
60     }
61 
62     __count_zone_vm_events(PGALLOC, zone, 1 << order);
63     zone_statistics(preferred_zone, zone);
64     local_irq_restore(flags);
65     put_cpu();
66 
67     VM_BUG_ON(bad_range(zone, page));
68     if (prep_new_page(page, order, gfp_flags))
69         goto again;
70     return page;
71 
72 failed:
73     local_irq_restore(flags);
74     put_cpu();
75     return NULL;
76 }
View Code

 

 

changelog----------------------------------------------------------------------------------------------------------------------------------------------
20190109:添加了page_to_pfn,pfn_to_page,move_freepages,move_freepages_block说明
20190110:添加了__rmqueue_smallest说明
20190111:添加了pfn_to_section_nr,__nr_to_section,__pfn_to_section,set_pageblock_migratetype->set_pageblock_flags_group的说明
20190113:修改了代码框,原有的代码框在google浏览器上展开后,看不到折叠按钮,从新修订了代码框的样式,现在能在google,火狐上能展开和折叠,但是ie展开和折叠代码仍然有问题,暂时就先这样。
20190114:添加了对__rmqueue_fallback函数的部分说明,没有能添加完,需要进一步完善
20190115:完善了__rmqueue_fallback函数的说明,添加了rmqueue_bulk函数的说明
20190116:添加了__mod_zone_page_state相关说明
20190118:添加了 zone_statistics, __count_zone_vm_events相关说明
TODO--------------------------------------------------------------------------------------------------------------------------------------------------
长期目标:本文要详细描述内核buddy子系统的相关函数,对外提供的内存分配和回收接口的流程,不拘泥于形式,力求深入测底。
计划分三步完成:
第一步:阅读代码,零碎整理各个函数的作用。
第二步:梳理流程,描述buddy系统对外提供的接口,每个接口的整体流程,使用的先行条件:例如,是否可以睡眠,是否需要持有锁,是否需要关中断,以及能够使用的上下文。
第三步:总结buddy系统代码实现的关键点,涉及的所有概念,以及概念之间的关系。
现阶段目标:get_page_from_freelist->buffered_rmqueue,总结page统计相关几个全局数据结构,说明page prepare相关逻辑

  

posted on 2018-12-19 22:20  DoOrDie  阅读(1113)  评论(0编辑  收藏  举报