内核代码阅读(7) - 物理页面的分配alloc_pages

物理页面的分配

内核代码中申请物理页面的函数是 alloc_pages

页面分配需要解决的问题

NUMA
buddy和位图
内存不够时如何处理

NUMA结构的处理逻辑

NUMA的处理是功过宏 CONFIG_NUMA走不同的分支
struct page * alloc_pages(int gfp_mask, unsigned long order)
{
    struct page *ret = 0;
    pg_data_t *start, *temp;
#ifndef CONFIG_NUMA
    unsigned long flags;
    static pg_data_t *next = 0;
#endif
    if (order >= MAX_ORDER)
    return NULL;
#ifdef CONFIG_NUMA
    temp = NODE_DATA(numa_node_id());
#else
    spin_lock_irqsave(&node_lock, flags);
    if (!next) next = pgdat_list;
    temp = next;
    next = next->node_next;
    spin_unlock_irqrestore(&node_lock, flags);
#endif
    start = temp;
    while (temp) {
    if ((ret = alloc_pages_pgdat(temp, gfp_mask, order)))
        return(ret);
    temp = temp->node_next;
    }
    temp = pgdat_list;
    while (temp != start) {
    if ((ret = alloc_pages_pgdat(temp, gfp_mask, order)))
        return(ret);
    temp = temp->node_next;
    }
    return(0);
}

0) 参数: gfp_mask 是一个整数代表使用哪中分配策略;order是分配物理页的大小。

1) 若果定义了CONFIG_NUMA 就用 NODE_DATA(numa_node_id()) 获取当前node对应的pg_data_t, 否则就用全局的pgdata_list。

2) pgdata_list是node的一个数组。

3) 然后,循环遍历所有的pg_data,也就是循环所有的分配策略,从每一个分配策略中调用 alloc_pages_pgdat分配。


alloc_pages_pgdat

static struct page * alloc_pages_pgdat(pg_data_t *pgdat, int gfp_mask,
                       unsigned long order)
{
return __alloc_pages(pgdat->node_zonelists + gfp_mask, order);
}
1) gfp_mask就是在一个node中的分配策略数组的下标 pgdat->node_zonelists + gfp_mask

开始分配内存__alloc_pages

struct page * __alloc_pages(zonelist_t *zonelist, unsigned long order)
{
    zone_t **zone;
    int direct_reclaim = 0;
    unsigned int gfp_mask = zonelist->gfp_mask;
    struct page * page;
    memory_pressure++;
    if (order == 0 && (gfp_mask & __GFP_WAIT) &&
    !(current->flags & PF_MEMALLOC))
    direct_reclaim = 1;
    if (inactive_shortage() > inactive_target / 2 && free_shortage())
    wakeup_kswapd(0);
    else if (free_shortage() && nr_inactive_dirty_pages > free_shortage()
         && nr_inactive_dirty_pages >= freepages.high)
    wakeup_bdflush(0);

1) memory_pressure 统计VM子系统的压力

2) gfp_mask 这个 gfp_mask不同于上面那个,代表一个具体的分配策略的控制位

#define __GFP_WAIT    0x01
#define __GFP_HIGH    0x02
#define __GFP_IO    0x04
#define __GFP_DMA    0x08
#ifdef CONFIG_HIGHMEM
#define __GFP_HIGHMEM    0x10
#else
#define __GFP_HIGHMEM    0x0 /* noop */
#endif
_GFP_WAIT 表示要等待分配完成。
order==0 表示分配的是单个页面
!(current->falgs & PF_MEMALLOC) 表示当前进程不是‘内存分配的管理进程’
满足上3个条件就允许从相应管理区“不活跃干净页面”的缓冲队列中回收, 设置标记direct_reclaim=1。

3) 当内核缺少页面,并且 inactive_shortage 不活跃的页面的缺口也不多时候,启动 wakeup_kswapd,帮助交换一些页面出去。

4) 当内核缺少页面,并且 nr_inactive_dirty_pages 脏页很多时候,启动 wakeup_bdflush, 帮助刷页面。


从free pages较多的zone中分配

zone = zonelist->zones;
for (;;) {
    zone_t *z = *(zone++);
    if (!z)
    break;
    if (!z->size)
    BUG();
    if (z->free_pages >= z->pages_low) {
    page = rmqueue(z, order);
    if (page)
        return page;
    } else if (z->free_pages < z->pages_min &&
           waitqueue_active(&kreclaimd_wait)) {
    wake_up_interruptible(&kreclaimd_wait);
    }
}

1) 遍历当前分配测率中的zone。

2) 如果zone的 free_pages 高于pages_low水位线就从这个zone中分配。

但是不一定保证分配成功,因为申请的是连续的2^order物理页。 free_pages高于low水位线不代表就一定有这么多的连续的物理页。

3) 如果 free_pages 低于pages_min水位线,而且有进程在 kreclaimd_wait队列上等待回收页面,就唤醒这个进程,让它开始干活。

从一个zone中分配连续的物理页面rmqueue

static struct page * rmqueue(zone_t *zone, unsigned long order)
{
    free_area_t * area = zone->free_area + order;
    unsigned long curr_order = order;
    struct list_head *head, *curr;
    unsigned long flags;
    struct page *page;
    spin_lock_irqsave(&zone->lock, flags);
    do {
    head = &area->free_list;
    curr = memlist_next(head);
    if (curr != head) {
        unsigned int index;
        page = memlist_entry(curr, struct page, list);
        if (BAD_RANGE(zone,page))
        BUG();
        memlist_del(curr);
        index = (page - mem_map) - zone->offset;
        MARK_USED(index, curr_order, area);
        zone->free_pages -= 1 << order;
        page = expand(zone, page, index, order, curr_order, area);
        spin_unlock_irqrestore(&zone->lock, flags);
        set_page_count(page, 1);
        if (BAD_RANGE(zone,page))
        BUG();
        DEBUG_ADD_PAGE
        return page;    
    }
    curr_order++;
    area++;
    } while (curr_order < MAX_ORDER);
    spin_unlock_irqrestore(&zone->lock, flags);
    return NULL;
}

1) 这个函数很重要,是buddy算法和位图的实现。

2) zone->free_area + order是从order的链表开始寻找空闲页面。

3) 进入循环之前对zone->lock上锁,因为别的进程有可能也正在这个zone上分配内存。

4) head = &area->free_list;

free_list 是一个双向队列,把2^order大小的页面串了起来,现在从头部取出一个。

5) page = memlist_entry(curr, struct page, list);

memlist_entry 是在从cur指针,page结构体,以及list在page结构体中的偏移,找到 *page

6) index = (page - mem_map) - zone->offset;

计算出这个空闲的page在mem_map中的偏移量。设置pgd,pte使用的是一个页面在mem_map的偏移量。

mem_map是指向全局的page大数组的起始地址,而从free_list双向链表通过memlist_entry获取的是指向一个page的指针。这个指针的值就是在mem_map数组中的。

为什么要减去zone->offset? zone->offset 是zone所管理的内存区域在mem_map起始偏移量,所以,这个index是在zone的偏移量。

MARK_USED更新位图,位图是为了在整理buddy时方便把连续的内存结合从更大order时用。参考ULK。

7) zone->free_pages -= 1 << order;

更新当前zone下的空闲内存。

8) expand 整理高order内存。比如,申请4个页面,而只有16个页面的话,就这个16页面拆开。

free_pages不够

page = __alloc_pages_limit(zonelist, order, PAGES_HIGH, direct_reclaim);
page = __alloc_pages_limit(zonelist, order, PAGES_LOW, direct_reclaim);
    
wakeup_kswapd(0);
if (gfp_mask & __GFP_WAIT) {
    __set_current_state(TASK_RUNNING);
    current->policy |= SCHED_YIELD;
    schedule();
}
page = __alloc_pages_limit(zonelist, order, PAGES_MIN, direct_reclaim);

1) 若果free_pages不够多,则把当前zone里面的inactive_clean_page也算上。

2) 尝试3询,每次都到一个zone里,看看这个zone的free_pages+inactive_clean_pages是否高于3个级别的水位线。

3) 第3次查询,说明内存已经很吃紧,唤醒kswapd。把当前进程放弃执行权,让给kswapd。好处有4个。

主动冲洗脏页

if (!(current->flags & PF_MEMALLOC)) {
    if (order > 0 && (gfp_mask & __GFP_WAIT)) {
    zone = zonelist->zones;
    current->flags |= PF_MEMALLOC;
    page_launder(gfp_mask, 1);
    current->flags &= ~PF_MEMALLOC;
    for (;;) {
        zone_t *z = *(zone++);
        if (!z)
        break;
        if (!z->size)
        continue;
        while (z->inactive_clean_pages) {
        struct page * page;
        page = reclaim_page(z);
        if (!page)
            break;
        __free_page(page);
        page = rmqueue(z, order);
        if (page)
            return page;
        }
    }
    }
    if ((gfp_mask & (__GFP_WAIT|__GFP_IO)) == (__GFP_WAIT|__GFP_IO)) {
    wakeup_kswapd(1);
    memory_pressure++;
    if (!order)
        goto try_again;
    } else if (gfp_mask & __GFP_WAIT) {
    try_to_free_pages(gfp_mask);
    memory_pressure++;
    if (!order)
        goto try_again;
    }
}

0) 首先要看当前进程是不是一个‘内存的管理者。防止进入死递归。

1) 不是,如果仍然找不到,并且是大页面(order>0),而且设置了__GFP_WAIT,就开始调用page_launder,刷洗脏页面,从inactive_clean_list回收

2) 是,不能再次调用kswapd,因为当前这个进程是内存的管理者,就是因为某个时机内存不够用被唤醒了。说明这个进程在管理内存(比如kswapd,flushd)时候出现了内存吃紧。不能再调用kswapd。

解决方法是:直接调用kswapd对应的逻辑函数。

最后一招放宽水位线

zone = zonelist->zones;
for (;;) {
    zone_t *z = *(zone++);
    struct page * page = NULL;
    if (!z)
    break;
    if (!z->size)
    BUG();
    if (direct_reclaim) {
    page = reclaim_page(z);
    if (page)
        return page;
    }
    if (z->free_pages < z->pages_min / 4 &&
    !(current->flags & PF_MEMALLOC))
    continue;
    page = rmqueue(z, order);
    if (page)
    return page;
}
printk(KERN_ERR "__alloc_pages: %lu-order allocation failed.\n", order);
return NULL;

0) 如果 direct_reclaim

1) if (z->free_pages < z->pages_min / 4 && !(current->flags & PF_MEMALLOC)) continue;

如果进程不是 PF_MEMALLOC进程,直接continue。

只有管理进程才会使用最后一点‘血本’。

上一篇:软件研发项目关键影响因素


下一篇:winform 读取TXT文件 放在Label中