Linux内核-内存-分区页框分配器

简介

分区页框分配器处理对连续页框组的内存分配请求,主要组成如下图所示:

Linux内核-内存-分区页框分配器_第1张图片

管理区分配器接受动态内存分配与释放的请求,它首先从每CPU页框高速缓存中请求页框,若无法满足才从伙伴系统中请求分配。


源码分析(Linux2.6/mm/page_alloc.c)

  1. buffered_rmqueue()

    此函数从指定内存管理区中分配页框,对应上图中的分配机制。

     static struct page *buffered_rmqueue(struct zone *zone, int order, int gfp_flags)
        {
            unsigned long flags;
            struct page *page = NULL;
            int cold = !(gfp_flags & __GFP_COLD);
    
            /**
             * 如果order!=0,则每CPU页框高速缓存就不能被使用,因为每CPU页框高速缓存是对单个页框的管理
             */
            if (order == 0) {
                struct per_cpu_pages *pcp;
    
                /**
                 * 检查由__GFP_COLD标志所标识的内存管理区本地CPU高速缓存是否需要被补充。
                 * 其count字段小于或者等于low
                 */
                pcp = &zone->pageset[get_cpu()].pcp[cold];
                local_irq_save(flags);
                /**
                 * 当前缓存中的页框数低于low,需要从伙伴系统中补充页框。
                 * 调用rmqueue_bulk函数从伙伴系统中分配batch个单一页框
                 * rmqueue_bulk反复调用__rmqueue,直到缓存的页框达到low。
                 */
                if (pcp->count <= pcp->low)
                    pcp->count += rmqueue_bulk(zone, 0,
                                pcp->batch, &pcp->list);
                /**
                 * 如果count为正,函数从高速缓存链表中获得一个页框。
                 * count减1
                 */
                if (pcp->count) {
                    page = list_entry(pcp->list.next, struct page, lru);
                    list_del(&page->lru);
                    pcp->count--;
                }
                local_irq_restore(flags);
                /**
                 * 没有和get_cpu配对使用呢?
                 * 这就是内核,外层一定调用了get_cpu。这种代码看起来头疼。
                 */
                put_cpu();
            }
    
            /**
             * 内存请求没有得到满足,或者是因为请求跨越了几个连续页框,或者是因为被选中的页框高速缓存为空。
             * 调用__rmqueue函数(因为已经保护了,直接调用__rmqueue即可)从伙伴系统中分配所请求的页框(见伙伴系统一文)
             */
            if (page == NULL) {
                spin_lock_irqsave(&zone->lock, flags);
                page = __rmqueue(zone, order);
                spin_unlock_irqrestore(&zone->lock, flags);
            }
    
            /**
             * 如果内存请求得到满足,函数就初始化(第一个)页框的页描述符
             */
            if (page != NULL) {
                BUG_ON(bad_range(zone, page));
                /**
                 * 将第一个页清除一些标志,将private字段置0,并将页框引用计数器置1。
                 */
                mod_page_state_zone(zone, pgalloc, 1 << order);
                prep_new_page(page, order);
    
                /**
                 * 如果__GFP_ZERO标志被置位,则将被分配的区域填充0。
                 */
                if (gfp_flags & __GFP_ZERO)
                    prep_zero_page(page, order, gfp_flags);
    
                if (order && (gfp_flags & __GFP_COMP))
                    prep_compound_page(page, order);
            }
            return page;
        }
  2. __alloc_pages()

    __alloc_pages()函数是管理区分配器的核心,请求页框的函数,比如alloc_page()是对__alloc_pages()的封装,该函数比较长,但是不算难理解,耐心点看完吧。该函数接收3个参数:

    • gfp_mask:在内存分配请求中指定的标志
    • order: 连续分配的页框数量的对数(实际分配的是2^order个连续的页框)
    • zonelist: zonelist数据结构的指针。该结构按优先次序描述了适于内存分配的内存管理区。它实际上是指向zone数组,存放本节点和其他节点的同类型管理区

    代码如下:

    struct page * fastcall
        __alloc_pages(unsigned int gfp_mask, unsigned int order,
            struct zonelist *zonelist)
        {
            const int wait = gfp_mask & __GFP_WAIT;
            struct zone **zones, *z;
            struct page *page;
            struct reclaim_state reclaim_state;
            struct task_struct *p = current;
            int i;
            int classzone_idx;
            int do_retry;
            int can_try_harder;
            int did_some_progress;
    
            might_sleep_if(wait);
    
            /**
            * if语句在汇编中的实现:如果条件满足就执行下一条指令,如果不满足则跳转,unlikely是为了使程序顺序执行而避免跳转
            * rt_task判断是否为实时进程,如果是实时进程且不可中断或者不可等待
            * 那么can_try_harder被置位,阈值将会再次减少(见下面zone_watermark_ok()函数)
            */
    
            can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait;
    
            zones = zonelist->zones;  
    
            if (unlikely(zones[0] == NULL)) {
                return NULL;
            }
    
            classzone_idx = zone_idx(zones[0]);
    
         restart:
    
            /**
             * 扫描包含在zonelist数据结构中的每个内存管理区
             */
            for (i = 0; (z = zones[i]) != NULL; i++) {
                /**
                 *对于每个内存管理区,该函数将空闲页框的个数与一个阀值进行比较
                 *该值取决于内存分配标志、当前进程的类型及管理区被函数检查的次数。
                 *实际上,如果空闲内存不足,那么每个内存管理区一般会被检查几次。
                 *每一次在所请求的空闲内存最低量的基础上使用更低的值进行扫描。
                 * 因此,这段循环代码会被复制几次,而变化很小。
                 */
    
                /**
                 *zone_watermark_ok辅助函数接收几个参数,它们决定内存管理区中空闲页框个数的阀值min。
                 *这是对内存管理区的第一次扫描,在第一次扫描中,阀值设置为z->pages_low
                 */
                if (!zone_watermark_ok(z, order, z->pages_low,
                               classzone_idx, 0, 0))
                    continue;
    
                page = buffered_rmqueue(z, order, gfp_mask);
                if (page)
                    goto got_pg;
            }
    
            /**
             * 一般来说,应当在上一次扫描时得到内存。
             * 运行到此,表示内存已经紧张了(没有连续的页框可供分配了)
             * 就唤醒kswapd内核线程来异步的开始回收页框。
             */
            for (i = 0; (z = zones[i]) != NULL; i++)
                wakeup_kswapd(z, order);
    
            /**
             * 执行对内存管理区的第二次扫描,将值z->pages_min作为阀值传入。这个值已经在上一步的基础上降低了。
             * 当然,实际的min值还是要由can_try_harder和gfp_high确定。z->pages_min仅仅是一个参考值而已。
             */
            for (i = 0; (z = zones[i]) != NULL; i++) {
                if (!zone_watermark_ok(z, order, z->pages_min,
                               classzone_idx, can_try_harder,
                               gfp_mask & __GFP_HIGH))
                    continue;
    
                page = buffered_rmqueue(z, order, gfp_mask);
                if (page)
                    goto got_pg;
            }
    
            /**
             * 上一步都还没有获得内存,系统内存肯定是不足了。
             */
    
            /**
             * 如果产生内存分配的内核控制路径不是一个中断处理程序或者可延迟函数,
             * 并且它试图回收页框(PF_MEMALLOC,TIF_MEMDIE标志被置位),那么才对内存管理区进行第三次扫描。
             */
            if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE))) && !in_interrupt()) {
    
                for (i = 0; (z = zones[i]) != NULL; i++) {
                    /**
                    *本次扫描就不调用zone_watermark_ok,它忽略阀值,这样才能从预留的页中分配页。
                    *允许这样做,因为是这个进程想要归还页框,那就暂借一点给它吧(舍不得孩子套不到狼)。
                    */
                    page = buffered_rmqueue(z, order, gfp_mask);
                    if (page)
                        goto got_pg;
                }
    
                /**
                 * 运行到这里来,实在是没有内存了。
                 *不论是高端内存区还是普通内存区、还是DMA内存区,甚至这些管理区中保留的内存都没有了。
                 * 意味着我们的家底都完了。
                 */
                goto nopage;
            }
    
            /**
             * 如果gfp_mask的__GFP_WAIT标志没有被置位,函数就返回NULL。
             */
            if (!wait)
                goto nopage;
    
        rebalance:
            /**
             * 如果当前进程能够被阻塞,调用cond_resched检查是否有其他进程需要CPU
             */
            cond_resched();
    
            /*
            * 设置PF_MEMALLOC标志来表示进程已经准备好执行内存回收。
            */
            p->flags |= PF_MEMALLOC;
            reclaim_state.reclaimed_slab = 0;
            /**
             * 将reclaim_state数据结构指针存入reclaim_state。这个结构只包含一个字段reclaimed_slab,初始值为0
             */
            p->reclaim_state = &reclaim_state;
    
            /**
             * 调用try_to_free_pages寻找一些页框来回收。
             * 这个函数可能会阻塞当前进程。一旦返回,就重设PF_MEMALLOC,并再次调用cond_resched
             */
            did_some_progress = try_to_free_pages(zones, gfp_mask, order);
    
            p->reclaim_state = NULL;
            p->flags &= ~PF_MEMALLOC;
    
            cond_resched();
    
            /**
             * 如果已经回收了一些页框,那么执行第二遍扫描类似的操作。
             */
            if (likely(did_some_progress)) {
                for (i = 0; (z = zones[i]) != NULL; i++) {
                    if (!zone_watermark_ok(z, order, z->pages_min,
                                   classzone_idx, can_try_harder,
                                   gfp_mask & __GFP_HIGH))
                        continue;
    
                    page = buffered_rmqueue(z, order, gfp_mask);
                    if (page)
                        goto got_pg;
                }
            } else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {
                /**
                 *没有释放任何页框,说明内核遇到很大麻烦了。因为内存少又不能释放页框。
                 *如果允许杀死进程:__GFP_FS被置位并且__GFP_NORETRY标志为0。
                 * 那就开始准备杀死进程吧。
                 */
    
                /**
                 * 再扫描一次内存管理区。
                 *这样做有点莫名其妙,既然申请少一点的内存都不行,为什么还要传入z->pages_high??它看起来更不会成功。
                 *其实这样做还是有道理的:实际上,只有另一个内核控制路径已经杀死一个进程来回收它的内存后,这步才会成功。
                 * 因此,这步避免了两个(而不是一个)无辜的进程被杀死。
                 */
                for (i = 0; (z = zones[i]) != NULL; i++) {
                    if (!zone_watermark_ok(z, order, z->pages_high,
                                   classzone_idx, 0, 0))
                        continue;
    
                    page = buffered_rmqueue(z, order, gfp_mask);
                    if (page)
                        goto got_pg;
                }
    
                /**
                 * 还是不行,就杀死一些进程再试吧。
                 */
                out_of_memory(gfp_mask);
                goto restart;
            }
    
            /**
             * 如果内存分配请求不能被满足,那么函数决定是否应当继续扫描内存管理区。
             * 如果__GFP_NORETRY被清除,并且内存分配请求跨越了多达8个页框或者__GFP_REPEAT被置位,或者__GFP_NOFAIL被置位。
             */
            do_retry = 0;
            if (!(gfp_mask & __GFP_NORETRY)) {
                if ((order <= 3) || (gfp_mask & __GFP_REPEAT))
                    do_retry = 1;
                if (gfp_mask & __GFP_NOFAIL)
                    do_retry = 1;
            }
            /**
             * 要重试,就调用blk_congestion_wait使进程休眠一会。再跳到rebalance重试。
             */
            if (do_retry) {
                blk_congestion_wait(WRITE, HZ/50);
                goto rebalance;
            }
            /**
             * 既然不用重试,那就执行到nopage返回NULL了。
             */
        nopage:
            if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {
                printk(KERN_WARNING "%s: page allocation failure."
                    " order:%d, mode:0x%x\n",
                    p->comm, order, gfp_mask);
                dump_stack();
            }
            return NULL;
        got_pg:
            zone_statistics(zonelist, z);
            return page;
        }
    

    zone_watermark_ok()函数如下:

    int zone_watermark_ok(struct zone *z, int order, unsigned long mark,int classzone_idx, int can_try_harder, int gfp_high)
        {
            long min = mark, free_pages = z->free_pages - (1 << order) + 1;
            int o;
    
            /**
             * 如果gfp_high标志被置位。则base除2。
             * 注意这里不是:min /= 2(min为奇偶数的差别)
             * 一般来说,如果gfp_mask的__GFP_WAIT标志被置位,那么这个标志就会为1
             * 换句话说,就是指从高端内存中分配。
             */
            if (gfp_high)
                min -= min / 2;
            /**
             * 如果作为参数传递的can_try_harder标志被置位,这个值再减少1/4
             * can_try_harder=1一般是当:gfp_mask中的__GFP_WAIT标志被置位,或者当前进程是一个实时进程并且在进程上下文中已经完成了内存分配。
             */
            if (can_try_harder)
                min -= min / 4;
    
            if (free_pages <= min + z->lowmem_reserve[classzone_idx])
                return 0;
            /*
            * for函数作用:除了被分配的页框外,在order至少为k的块中起码还有min/2^k个空闲页框
            */
            for (o = 0; o < order; o++) {
    
                free_pages -= z->free_area[o].nr_free << o;
    
                /* Require fewer higher order pages to be free */
                min >>= 1;
    
                if (free_pages <= min)
                    return 0;
            }
            return 1;
        }
    

你可能感兴趣的:(Linux,kernel,内存,页框分配)