Zoned Allocator -5- (Per-CPU Page Frame Cache)

<kernel v5.0>

Per-CPU Page Frame Cache

  • 커널에서 메모리 할당은 주로 큰 페이지보다 single 페이지(0-order page) 프레임을 요청하는 경우가 대부분이다.
  • single 페이지 요청인 경우에만 할당 처리 성능을 높이기 위해 각각의 zone에 per-cpu page frame cache를 준비하고 미리 여러 개의 페이지를 준비한 후 요청한 single 페이지에 대해 buddy를 사용하지 않고 곧바로 캐시된 페이지를 요청자에게 전달한다.
    • 버디를 사용할 경우에는 존에 대한 락이 필요한데, per-cpu를 사용하여 lock-less로 구현하여 성능을 올렸다.
  • 기존 커널에서 각 zone에는 hot 캐시와 cold 캐시를  사용해왔었는데 그 용도가 하나로 통합되었고 hot 요청인 경우 준비된 캐시 페이지 중 앞쪽을 사용하게 하고 cold 요청인 경우 뒷 쪽 페이지를 사용하게 한다.
  • 커널 2.6.25-rc1 이후 부터 각 zone 마다 3개의 migratetype 수 만큼 캐시 배열로 관리한다.
  • 커널 모니터가 캐시 페이지가 low 워터마크 수 이하로 떨어지는 것을 커널 모니터가 detect하면 미리 batch 수 만큼 페이지를 캐시에 할당해 놓는다.
  • single 페이지 요청 시 캐시된 페이지가 없는 경우에는 batch 수 만큼 페이지를 캐시에 할당 받은 후 그 중 한 페이지를 요청자에게 전달한다.
  • single 페이지 해제 시 캐시된 페이지가 high 이상인 경우 batch 수 만큼 버디 시스템에 되돌린다.

 


pcp에서 order 0 페이지 할당/해제

order 0 페이지 할당

rmqueue_pcplist()

mm/page_alloc.c

/* Lock and remove page from the per-cpu list */
static struct page *rmqueue_pcplist(struct zone *preferred_zone,
                        struct zone *zone, unsigned int order,
                        gfp_t gfp_flags, int migratetype,
                        unsigned int alloc_flags)
{
        struct per_cpu_pages *pcp;
        struct list_head *list;
        struct page *page;
        unsigned long flags;

        local_irq_save(flags);
        pcp = &this_cpu_ptr(zone->pageset)->pcp;
        list = &pcp->lists[migratetype];
        page = __rmqueue_pcplist(zone,  migratetype, alloc_flags, pcp, list);
        if (page) {
                __count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
                zone_statistics(preferred_zone, zone);
        }
        local_irq_restore(flags);
        return page;
}

@migratetype의 order 0 페이지를 pcp에서 할당하고 페이지 디스크립터를 반환한다.

  • 코드 라인 12~15에서 per-cpu로 구현된 버디 시스템 캐시인 pcp는 cost가 많이 소모되는 lock을 사용하지 않고 로컬 인터럽트만 disable한 상태로 @migratetype의 order 0 페이지를 pcp에서 할당한다.
  • 코드 라인 16~19에서 PGALLOC 카운터를 페이지 수 만큼 증가시킨다.
  • 코드 라인 21에서 할당한 페이지를 반환한다.

 

__rmqueue_pcplist()

mm/page_alloc.c

/* Remove page from the per-cpu list, caller must protect the list */
static struct page *__rmqueue_pcplist(struct zone *zone, int migratetype,
                        unsigned int alloc_flags,
                        struct per_cpu_pages *pcp,
                        struct list_head *list)
{
        struct page *page;

        do {
                if (list_empty(list)) {
                        pcp->count += rmqueue_bulk(zone, 0,
                                        pcp->batch, list,
                                        migratetype, alloc_flags);
                        if (unlikely(list_empty(list)))
                                return NULL;
                }

                page = list_first_entry(list, struct page, lru);
                list_del(&page->lru);
                pcp->count--;
        } while (check_new_pcp(page));

        return page;
}

@migratetype의 order 0 페이지를 pcp로부터 할당하고 페이지 디스크립터를 반환한다.

  • 코드 라인 9~16에서 pcp의 @list가 비어있는 경우 버디 시스템에서 pcp->batch 수 만큼 이주시킨다.
  • 코드 라인 18~20에서 pcp의 @list에서 첫 엔트리를 가져온다.
  • 코드 라인 21에서 할당할 엔트리에 문제가 없는지 체크한다.
  • 코드 라인 23에서 할당할 order 0 페이지를 반환한다.

 

order 0 페이지 회수

free_unref_page()

mm/page_alloc.c

/*
 * Free a 0-order page
 */
void free_unref_page(struct page *page)
{
        unsigned long flags;
        unsigned long pfn = page_to_pfn(page);

        if (!free_unref_page_prepare(page, pfn))
                return;

        local_irq_save(flags);
        free_unref_page_commit(page, pfn);
        local_irq_restore(flags);
}

order 0 페이지를 pcp에 회수한다.

  • 코드 라인 6~7에서 free할 페이지를 준비한다. 만일 페이지 상태가 bad 판정된 경우 함수를 빠져나간다.
  • 코드 라인 9~11에서 로컬 irq를 disable한 상태로 order 0 페이지를 pcp에 회수한다.

 

free_unref_page_prepare()

mm/page_alloc.c

static bool free_unref_page_prepare(struct page *page, unsigned long pfn)
{
        int migratetype;

        if (!free_pcp_prepare(page))
                return false;

        migratetype = get_pfnblock_migratetype(page, pfn);
        set_pcppage_migratetype(page, migratetype);
        return true;
}

free할 페이지를 준비한다. (정상=true, bad=false)

  • 코드 라인 5~6에서 free할 페이지의 상태를 체크하여 bad 판정된 경우 false 결과를 반환한다.
  • 코드 라인 8~10에서 페이지가 속한 페이지 블럭의 migrate 타입을 페이지에 저장하고 true를 반환한다.

 

free_unref_page_commit()

mm/page_alloc.c

static void free_unref_page_commit(struct page *page, unsigned long pfn)
{
        struct zone *zone = page_zone(page);
        struct per_cpu_pages *pcp;
        int migratetype;

        migratetype = get_pcppage_migratetype(page);
        __count_vm_event(PGFREE);

        /*
         * We only track unmovable, reclaimable and movable on pcp lists.
         * Free ISOLATE pages back to the allocator because they are being
         * offlined but treat HIGHATOMIC as movable pages so we can get those
         * areas back if necessary. Otherwise, we may have to free
         * excessively into the page allocator
         */
        if (migratetype >= MIGRATE_PCPTYPES) {
                if (unlikely(is_migrate_isolate(migratetype))) {
                        free_one_page(zone, page, pfn, 0, migratetype);
                        return;
                }
                migratetype = MIGRATE_MOVABLE;
        }

        pcp = &this_cpu_ptr(zone->pageset)->pcp;
        list_add(&page->lru, &pcp->lists[migratetype]);
        pcp->count++;
        if (pcp->count >= pcp->high) {
                unsigned long batch = READ_ONCE(pcp->batch);
                free_pcppages_bulk(zone, batch, pcp);
        }
}

free할 0-order 페이지를 pcp로 회수한다.

  • 코드 라인 8에서 PGFREE 카운터를 증가시킨다.
  • 코드 라인 17~23에서 isolate 타입은 버디 시스템에 회수시키고, pcp에서 취급하지 않는 나머지 cma와 highatomic 타입은 movable 타입으로 변경한다.
  • 코드 라인 25~27에서 migrate 타입의 pcp에 추가한다.
  • 코드 라인 28~31에서 pcp 리스트의 엔트리 수가 pcp->high 이상인 경우이다. pcp에 일정 분량만을 관리하기 위해 pcp->batch 수 만큼 버디 시스템으로 이동시킨다.

 

get_pcppage_migratetype()

include/linux/mm.h

/*
 * A cached value of the page's pageblock's migratetype, used when the page is
 * put on a pcplist. Used to avoid the pageblock migratetype lookup when
 * freeing from pcplists in most cases, at the cost of possibly becoming stale.
 * Also the migratetype set in the page does not necessarily match the pcplist
 * index, e.g. page might have MIGRATE_CMA set but be on a pcplist with any
 * other index - this ensures that it will be put on the correct CMA freelist.
 */
static inline int get_pcppage_migratetype(struct page *page)
{
        return page->index;
}

page->index에 저장된 migratetype을 알아온다.

 


pcp <-> 버디시스템 벌크 할당/회수

pcp <- 버디시스템 벌크 할당

rmqueue_bulk()

mm/page_alloc.c

/*
 * Obtain a specified number of elements from the buddy allocator, all under
 * a single hold of the lock, for efficiency.  Add them to the supplied list.
 * Returns the number of new pages which were placed at *list.
 */
static int rmqueue_bulk(struct zone *zone, unsigned int order,
                        unsigned long count, struct list_head *list,
                        int migratetype, unsigned int alloc_flags)
{
        int i, alloced = 0;

        spin_lock(&zone->lock);
        for (i = 0; i < count; ++i) {
                struct page *page = __rmqueue(zone, order, migratetype,
                                                                alloc_flags);
                if (unlikely(page == NULL))
                        break;

                if (unlikely(check_pcp_refill(page)))
                        continue;

                /*
                 * Split buddy pages returned by expand() are received here in
                 * physical page order. The page is added to the tail of
                 * caller's list. From the callers perspective, the linked list
                 * is ordered by page number under some conditions. This is
                 * useful for IO devices that can forward direction from the
                 * head, thus also in the physical page order. This is useful
                 * for IO devices that can merge IO requests if the physical
                 * pages are ordered properly.
                 */
                list_add_tail(&page->lru, list);
                alloced++;
                if (is_migrate_cma(get_pcppage_migratetype(page)))
                        __mod_zone_page_state(zone, NR_FREE_CMA_PAGES,
                                              -(1 << order));
        }

        /*
         * i pages were removed from the buddy list even if some leak due
         * to check_pcp_refill failing so adjust NR_FREE_PAGES based
         * on i. Do not confuse with 'alloced' which is the number of
         * pages added to the pcp list.
         */
        __mod_zone_page_state(zone, NR_FREE_PAGES, -(i << order));
        spin_unlock(&zone->lock);
        return alloced;
}

버디 시스템의 @order slot에서 @count 만큼 free 페이지를 가져와서 @list에 이동시킨다. 그런 후 실제 이동시킨 수를 반환한다.

  • 코드 라인 8~15에서 count 수 만큼 루프를 돌며 버디 시스템으로 부터 @order 페이지를 가져온다.
  • 코드 라인 27~28에서 가져온 페이지를 @list에 추가한다.
  • 코드 라인 29~31에서 cma 페이지인 경우 NR_FREE_CMA_PAGES 카운터를 페이지 수 만큼 감소시킨다.
  • 코드 라인 40에서 NR_FREE_PAGES를 루프를 돌며 이동시킨 페이지 수 만큼 감소시킨다.
  • 코드 라인 42에서 이동시킨 수를 반환한다.

 

다음 그림은 버디시스템에 있는 free 페이지들이 batch 수 만큼 pcp로 벌크 이동하는 모습을 보여준다.

 

pcp -> 버디시스템 벌크 회수

free_pcppages_bulk()

/*
 * Frees a number of pages from the PCP lists
 * Assumes all pages on list are in same zone, and of same order.
 * count is the number of pages to free.
 *
 * If the zone was previously in an "all pages pinned" state then look to
 * see if this freeing clears that state.
 *
 * And clear the zone's pages_scanned counter, to hold off the "all pages are
 * pinned" detection logic.
 */
static void free_pcppages_bulk(struct zone *zone, int count,
                                        struct per_cpu_pages *pcp)
{
        int migratetype = 0;
        int batch_free = 0;
        int prefetch_nr = 0;
        bool isolated_pageblocks;
        struct page *page, *tmp;
        LIST_HEAD(head);

        while (count) {
                struct list_head *list;

                /*
                 * Remove pages from lists in a round-robin fashion. A
                 * batch_free count is maintained that is incremented when an
                 * empty list is encountered.  This is so more pages are freed
                 * off fuller lists instead of spinning excessively around empty
                 * lists
                 */
                do {
                        batch_free++;
                        if (++migratetype == MIGRATE_PCPTYPES)
                                migratetype = 0;
                        list = &pcp->lists[migratetype];
                } while (list_empty(list));

                /* This is the only non-empty list. Free them all. */
                if (batch_free == MIGRATE_PCPTYPES)
                        batch_free = count;

                do {
                        page = list_last_entry(list, struct page, lru);
                        /* must delete to avoid corrupting pcp list */
                        list_del(&page->lru);
                        pcp->count--;

                        if (bulkfree_pcp_prepare(page))
                                continue;

                        list_add_tail(&page->lru, &head);

                        /*
                         * We are going to put the page back to the global
                         * pool, prefetch its buddy to speed up later access
                         * under zone->lock. It is believed the overhead of
                         * an additional test and calculating buddy_pfn here
                         * can be offset by reduced memory latency later. To
                         * avoid excessive prefetching due to large count, only
                         * prefetch buddy for the first pcp->batch nr of pages.
                         */
                        if (prefetch_nr++ < pcp->batch)
                                prefetch_buddy(page);
                } while (--count && --batch_free && !list_empty(list));
        }

        spin_lock(&zone->lock);
        isolated_pageblocks = has_isolate_pageblock(zone);

        /*
         * Use safe version since after __free_one_page(),
         * page->lru.next will not point to original list.
         */
        list_for_each_entry_safe(page, tmp, &head, lru) {
                int mt = get_pcppage_migratetype(page);
                /* MIGRATE_ISOLATE page should not go to pcplists */
                VM_BUG_ON_PAGE(is_migrate_isolate(mt), page);
                /* Pageblock could have been isolated meanwhile */
                if (unlikely(isolated_pageblocks))
                        mt = get_pageblock_migratetype(page);

                __free_one_page(page, page_to_pfn(page), zone, 0, mt);
                trace_mm_page_pcpu_drain(page, 0, mt);
        }
        spin_unlock(&zone->lock);
}

요청 zone의 pcp를 @count 만큼 버디시스템으로 회수한다.

  • 코드 라인 11에서 @count 수 만큼 순회한다.
  • 코드 라인 21~30에서 3가지 migrate 타입의 pcp 리스트를 순회하도록 migratetype을 정한다. 단 빈 pcp 리스트는 skip 한다.
    • 처음 시작 시 movable(1), reclaimable(2), unmovable(0) migrate 타입으로 진행한다.
    • batch_free 수 만큼씩 로드밸런싱하는데, 리스트가 비게 되면 너무 spin 되는 것을 억제하게 하기 위해 batch_free를 추가 증가시킨다.
      • empty된 리스트 없이, 세 리스트에서 작업 시 1개씩 돌아가며 처리한다.
      • 한 리스트가 empty 되고, 남은 두 리스트에서 작업 시 2개씩 처리한다.
      • 두 리스트가 empty 되고, 마지막 리스트만 남게되면 한꺼번에 처리하기 위해 @count를 대입한다.
  • 코드 라인 32~41에서 지정된 migratetype의 pcp 리스트에서 tail 방향 엔트리를 가져와서 임시 리스트의 head 방향에 추가한다.
  • 코드 라인 52~53에서 pcp->batch 까지는 페이지에 대한 buddy 페이지를 prefetch 한다.이렇게 하면 버디 시스템에서 조금 더 빠른 성능으로 처리하기 위함이다.
  • 코드 라인 54에서 한 개의 pcp 리스트에서 batch_free 수 만큼만 반복처리한다. 단 empty 되거나, @count가 0이되어 모두 처리한 경우 완료된다.
  • 코드 라인 64~74에서 임시 리스트를 순회하며 해당 페이지가 속한 migrate 타입을 사용하여 버디 시스템의 해당 migrate 타입을 사용한 리스트로 회수시킨다. 단 회수 시킬 때 존에 isolate 타입 페이지가 존재하는 경우 페이지가 속한 페이지블럭의 migrate 타입을 사용한다.

 

아래 그림은 pcp가 overflow되어 batch 수 만큼 buddy로 이주하는 과정과 순서를 보여준다.

  • free_list[0] 슬롯으로 페이지가 이주될 때 free_list[0]에 buddy 페이지가 존재하는 경우 buddy 페이지를 제거하고 다음 order인 free_list[1]으로 합쳐서 추가한다. 동일하게 free_list[1]에서도 buddy 페이지가 발견되면 다음 order로 통합하면서 buddy 페이지가 발견되지 않을 때까지 통합한다.

 

다음 그림은 pcp에서 버디로 옮겨지는 페이지의 순서를 보여준다.

 

다음과 같이 zone별 pagesets에 대한 카운터 정보를 확인할 수 있다.

pi@pi /proc $ cat zoneinfo
Node 0, zone   Normal
  pages free     190861
        min      2048
        low      2560
        high     3072
        scanned  0
        spanned  241664
        present  241664
        managed  233403
    nr_free_pages 190861
(...생략...)
    nr_free_cma  935
        protection: (0, 0)
  pagesets
    cpu: 0
              count: 50
              high:  186
              batch: 31
  vm stats threshold: 24
    cpu: 1
              count: 106
              high:  186
              batch: 31
  vm stats threshold: 24
    cpu: 2
              count: 153
              high:  186
              batch: 31
  vm stats threshold: 24
    cpu: 3
              count: 156
              high:  186
              batch: 31
  vm stats threshold: 24
  all_unreclaimable: 0
  start_pfn:         0
  inactive_ratio:    1

 


PCP(Per-Cpu Page frame cache) Drain

drain_all_pages()

/*
 * Spill all the per-cpu pages from all CPUs back into the buddy allocator.
 *
 * When zone parameter is non-NULL, spill just the single zone's pages.
 *
 * Note that this can be extremely slow as the draining happens in a workqueue.
 */
void drain_all_pages(struct zone *zone)
{
        int cpu;

        /*
         * Allocate in the BSS so we wont require allocation in
         * direct reclaim path for CONFIG_CPUMASK_OFFSTACK=y
         */
        static cpumask_t cpus_with_pcps;

        /*
         * Make sure nobody triggers this path before mm_percpu_wq is fully
         * initialized.
         */
        if (WARN_ON_ONCE(!mm_percpu_wq))
                return;

        /*
         * Do not drain if one is already in progress unless it's specific to
         * a zone. Such callers are primarily CMA and memory hotplug and need
         * the drain to be complete when the call returns.
         */
        if (unlikely(!mutex_trylock(&pcpu_drain_mutex))) {
                if (!zone)
                        return;
                mutex_lock(&pcpu_drain_mutex);
        }

        /*
         * We don't care about racing with CPU hotplug event
         * as offline notification will cause the notified
         * cpu to drain that CPU pcps and on_each_cpu_mask
         * disables preemption as part of its processing
         */
        for_each_online_cpu(cpu) {
                struct per_cpu_pageset *pcp;
                struct zone *z;
                bool has_pcps = false;

                if (zone) {
                        pcp = per_cpu_ptr(zone->pageset, cpu);
                        if (pcp->pcp.count)
                                has_pcps = true;
                } else {
                        for_each_populated_zone(z) {
                                pcp = per_cpu_ptr(z->pageset, cpu);
                                if (pcp->pcp.count) {
                                        has_pcps = true;
                                        break;
                                }
                        }
                }

                if (has_pcps)
                        cpumask_set_cpu(cpu, &cpus_with_pcps);
                else
                        cpumask_clear_cpu(cpu, &cpus_with_pcps);
        }

        for_each_cpu(cpu, &cpus_with_pcps) {
                struct pcpu_drain *drain = per_cpu_ptr(&pcpu_drain, cpu);

                drain->zone = zone;
                INIT_WORK(&drain->work, drain_local_pages_wq);
                queue_work_on(cpu, mm_percpu_wq, &drain->work);
        }
        for_each_cpu(cpu, &cpus_with_pcps)
                flush_work(&per_cpu_ptr(&pcpu_drain, cpu)->work);

        mutex_unlock(&pcpu_drain_mutex);
}

지정된 zone의 모든 online cpu에 있는 Per-CPU Page Frame Cache를 버디 메모리 할당자로 옮긴다. zone을 지정하지 않은 경우는 모든 populated zone에 대해 수행한다.

 

drain_local_pages()

mm/page_alloc.c

/*
 * Spill all of this CPU's per-cpu pages back into the buddy allocator.
 *
 * The CPU has to be pinned. When zone parameter is non-NULL, spill just
 * the single zone's pages.
 */
void drain_local_pages(struct zone *zone)
{
        int cpu = smp_processor_id();

        if (zone)
                drain_pages_zone(cpu, zone);
        else
                drain_pages(cpu);
}

 

drain_pages()

mm/page_alloc.c

/*
 * Drain pcplists of all zones on the indicated processor.
 *
 * The processor must either be the current processor and the
 * thread pinned to the current processor or a processor that
 * is not online.
 */
static void drain_pages(unsigned int cpu)
{
        struct zone *zone;

        for_each_populated_zone(zone) {
                drain_pages_zone(cpu, zone);
        }
}

활성화된 zone 모두에 대해 Per-Cpu Page Fram Cache를 비운다.

 

drain_pages_zone()

mm/page_alloc.c

/*
 * Drain pcplists of the indicated processor and zone.
 *
 * The processor must either be the current processor and the
 * thread pinned to the current processor or a processor that
 * is not online.
 */
static void drain_pages_zone(unsigned int cpu, struct zone *zone)
{
        unsigned long flags;
        struct per_cpu_pageset *pset;
        struct per_cpu_pages *pcp;

        local_irq_save(flags);
        pset = per_cpu_ptr(zone->pageset, cpu);

        pcp = &pset->pcp;
        if (pcp->count)
                free_pcppages_bulk(zone, pcp->count, pcp);
        local_irq_restore(flags);
}

요청 zone에 대한 Per-Cpu Page Fram Cache에 등록된 페이지들 모두 buddy 시스템으로 이주시킨다.

 

참고

 

 

Control Group for Memory

 

mem_cgroup_page_lruvec()

mm/memcontrol.c

/**
 * mem_cgroup_page_lruvec - return lruvec for isolating/putting an LRU page
 * @page: the page
 * @zone: zone of the page
 *                                                  
 * This function is only safe when following the LRU page isolation
 * and putback protocol: the LRU lock must be held, and the page must
 * either be PageLRU() or the caller must have isolated/allocated it.
 */
struct lruvec *mem_cgroup_page_lruvec(struct page *page, struct zone *zone)
{                                                   
        struct mem_cgroup_per_zone *mz;
        struct mem_cgroup *memcg;
        struct lruvec *lruvec; 

        if (mem_cgroup_disabled()) {
                lruvec = &zone->lruvec;
                goto out;
        }

        memcg = page->mem_cgroup;
        /*
         * Swapcache readahead pages are added to the LRU - and
         * possibly migrated - before they are charged.
         */
        if (!memcg)
                memcg = root_mem_cgroup;

        mz = mem_cgroup_page_zoneinfo(memcg, page);
        lruvec = &mz->lruvec;
out:
        /*
         * Since a node can be onlined after the mem_cgroup was created,
         * we have to be prepared to initialize lruvec->zone here;
         * and if offlined then reonlined, we need to reinitialize it.
         */
        if (unlikely(lruvec->zone != zone))
                lruvec->zone = zone;
        return lruvec;             
}

메모리 cgroup이 disable되어 있는 경우 zone이 가리키는 lruvec 정보를 가져오고 enable 되어 있는 경우 메모리 cgroup에 연결되어 있는 mem_cgroup_per_zone에 존재하는 lruvec 정보를 리턴한다. 리턴되는 lruvec 내의 zone 정보는 인수로 지정한 zone으로 갱신된다.

  • if (mem_cgroup_disabled()) { lruvec = &zone->lruvec; goto out; }
    • memory control group 서브 시스템이 disable되어 있는 경우 zone->lruvec을 기억하고 out:으로 이동한다.
  • if (!memcg) memcg = root_mem_cgroup;
    • 페이지에 지정된 mem_cgroup이 없는 경우 root_mem_cgroup을 대입한다.
  • mz = mem_cgroup_page_zoneinfo(memcg, page);
    • 페이지 정보로 mem_cgroup_per_zone 구조체 정보를 알아온다.
  • lruvec = &mz->lruvec;
    • 찾은 mem_cgroup_per_zone 구조체의 멤버 lruvec
  • if (unlikely(lruvec->zone != zone)) lruvec->zone = zone;
    • lruvec->zone을 갱신한다.

 

mem_cgroup_disabled()

include/linux/memcontrol.h

static inline bool mem_cgroup_disabled(void) 
{
        if (memory_cgrp_subsys.disabled)
                return true;
        return false;
}

메모리 Control Group 서브시스템의 disable 여부를 리턴한다.

 

mem_cgroup_page_zoneinfo()

mm/memcontrol.c

static struct mem_cgroup_per_zone *
mem_cgroup_page_zoneinfo(struct mem_cgroup *memcg, struct page *page)
{
        int nid = page_to_nid(page);
        int zid = page_zonenum(page);

        return &memcg->nodeinfo[nid]->zoneinfo[zid];
}

페이지 정보로 mem_cgroup_per_zone 정보를 알아온다.

  •  int nid = page_to_nid(page);
    • Sparse 메모리 시스템이 아닌 경우 page->flags 정보에 있는 노드 id
    • Sparse 메모리 시스템인 경우 페이지에 해당하는 섹션에 담긴 노드 id
  • int zid = page_zonenum(page);
    • 페이지에 담긴 zone 타입
  • return &memcg->nodeinfo[nid]->zoneinfo[zid];
    • memcg 뒤에 따라 붙은 주소에서 지정된 노드와 zone의 mem_cgroup_per_zone 구조체 주소를 알아온다.

아래 그림은 2 개의 노드와 3개의 zone으로 구성된 memory control group 서브 시스템을 보여준다.

mem_cgroup_page_zoneinfo-1

 

구조체

mem_cgroup 구조체

mm/memcontrol.c

/*
 * The memory controller data structure. The memory controller controls both
 * page cache and RSS per cgroup. We would eventually like to provide
 * statistics based on the statistics developed by Rik Van Riel for clock-pro,
 * to help the administrator determine what knobs to tune.
 *
 * TODO: Add a water mark for the memory controller. Reclaim will begin when
 * we hit the water mark. May be even add a low water mark, such that
 * no reclaim occurs from a cgroup at it's low water mark, this is
 * a feature that will be implemented much later in the future.
 */
struct mem_cgroup {
        struct cgroup_subsys_state css;

        /* Accounted resources */
        struct page_counter memory;
        struct page_counter memsw;
        struct page_counter kmem;

        /* Normal memory consumption range */
        unsigned long low;
        unsigned long high;

        unsigned long soft_limit;

        /* vmpressure notifications */
        struct vmpressure vmpressure;

        /* css_online() has been completed */
        int initialized;

        /*
         * Should the accounting and control be hierarchical, per subtree?
         */
        bool use_hierarchy;

        bool            oom_lock;
        atomic_t        under_oom;
        atomic_t        oom_wakeups;

        int     swappiness;
        /* OOM-Killer disable */
        int             oom_kill_disable;

        /* protect arrays of thresholds */
        struct mutex thresholds_lock;

        /* thresholds for memory usage. RCU-protected */
        struct mem_cgroup_thresholds thresholds;

        /* thresholds for mem+swap usage. RCU-protected */
        struct mem_cgroup_thresholds memsw_thresholds;

        /* For oom notifier event fd */
        struct list_head oom_notify;

 

        /*
         * Should we move charges of a task when a task is moved into this
         * mem_cgroup ? And what type of charges should we move ?
         */
        unsigned long move_charge_at_immigrate;
        /*
         * set > 0 if pages under this cgroup are moving to other cgroup.
         */
        atomic_t                moving_account;
        /* taken only while moving_account > 0 */
        spinlock_t              move_lock;
        struct task_struct      *move_lock_task;
        unsigned long           move_lock_flags;
        /*
         * percpu counter.
         */
        struct mem_cgroup_stat_cpu __percpu *stat;
        /*
         * used when a cpu is offlined or other synchronizations
         * See mem_cgroup_read_stat().
         */
        struct mem_cgroup_stat_cpu nocpu_base;
        spinlock_t pcp_counter_lock;

#if defined(CONFIG_MEMCG_KMEM) && defined(CONFIG_INET)
        struct cg_proto tcp_mem;
#endif
#if defined(CONFIG_MEMCG_KMEM)
        /* Index in the kmem_cache->memcg_params.memcg_caches array */
        int kmemcg_id;
        bool kmem_acct_activated;
        bool kmem_acct_active;
#endif

        int last_scanned_node;
#if MAX_NUMNODES > 1
        nodemask_t      scan_nodes;
        atomic_t        numainfo_events;
        atomic_t        numainfo_updating;
#endif

        /* List of events which userspace want to receive */
        struct list_head event_list;
        spinlock_t event_list_lock;

        struct mem_cgroup_per_node *nodeinfo[0];
        /* WARNING: nodeinfo must be the last member here */
};

 

mem_cgroup_per_zone 구조체

mm/memcontrol.c

/*
 * per-zone information in memory controller.
 */
struct mem_cgroup_per_zone {
        struct lruvec           lruvec;
        unsigned long           lru_size[NR_LRU_LISTS];

        struct reclaim_iter     iter[DEF_PRIORITY + 1];

        struct rb_node          tree_node;      /* RB tree node */
        unsigned long           usage_in_excess;/* Set to the value by which */
                                                /* the soft limit is exceeded*/
        bool                    on_tree;
        struct mem_cgroup       *memcg;         /* Back pointer, we cannot */
                                                /* use container_of        */
};

 

/*
 * The "priority" of VM scanning is how much of the queues we will scan in one
 * go. A value of 12 for DEF_PRIORITY implies that we will scan 1/4096th of the
 * queues ("queue_length >> 12") during an aging round.
 */
#define DEF_PRIORITY 12

 

reclaim_iter 구조체

mm/memcontrol.c

struct reclaim_iter {
        struct mem_cgroup *position;
        /* scan generation, increased every round-trip */
        unsigned int generation;
};

 

 

커널 코드 영역 확인(func_ptr_is_kernel_text())

 

 

func_ptr_is_kernel_text()

kernel/extable.c

/*
 * On some architectures (PPC64, IA64) function pointers
 * are actually only tokens to some data that then holds the
 * real function address. As a result, to find if a function
 * pointer is part of the kernel text, we need to do some
 * special dereferencing first.
 */
int func_ptr_is_kernel_text(void *ptr)
{
        unsigned long addr;
        addr = (unsigned long) dereference_function_descriptor(ptr);
        if (core_kernel_text(addr))
                return 1;
        return is_module_text_address(addr);
}

ptr 주소가 커널 코드 또는 모듈 코드 영역인지 여부를 알아온다.

 

__kernel_text_address()

kernel/extable.c

int __kernel_text_address(unsigned long addr)
{
        if (core_kernel_text(addr))
                return 1;
        if (is_module_text_address(addr))
                return 1;
        if (is_ftrace_trampoline(addr))
                return 1;
        /*
         * There might be init symbols in saved stacktraces.
         * Give those symbols a chance to be printed in
         * backtraces (such as lockdep traces).
         *
         * Since we are after the module-symbols check, there's
         * no danger of address overlap:
         */
        if (init_kernel_text(addr))
                return 1;
        return 0;
}

addr 주소가 core 커널 코드, 모듈 코드, ftrace 코드, init 커널 코드 영역인 경우 1을 리턴하고 그렇지 않으면 0을 리턴한다.

 

core_kernel_text()

kernel/extable.c

int core_kernel_text(unsigned long addr)
{
        if (addr >= (unsigned long)_stext &&
            addr < (unsigned long)_etext)
                return 1;

        if (system_state == SYSTEM_BOOTING &&
            init_kernel_text(addr))
                return 1;
        return 0;
}

addr 주소가 _stext ~ _etext 범위에 있거나 부팅 중 init kernel 코드 영역인 경우 1을 리턴하고 그렇지 않으면 0을 리턴한다.

 

core_kernel_data()

kernel/extable.c

/**
 * core_kernel_data - tell if addr points to kernel data
 * @addr: address to test
 *
 * Returns true if @addr passed in is from the core kernel data
 * section.
 *
 * Note: On some archs it may return true for core RODATA, and false
 *  for others. But will always be true for core RW data.
 */
int core_kernel_data(unsigned long addr)
{
        if (addr >= (unsigned long)_sdata &&
            addr < (unsigned long)_edata)
                return 1;
        return 0;
}

addr 주소가 _sdata ~ _edata 영역에 포함되어 있는지 여부를 리턴한다.

 

init_kernel_text()

kernel/extable.c

static inline int init_kernel_text(unsigned long addr)
{
        if (addr >= (unsigned long)_sinittext &&
            addr < (unsigned long)_einittext)
                return 1;
        return 0;
}

addr 주소가 _sinittext ~ _einittext 영역에 포함되어 있는지 여부를 리턴한다.

 

is_module_text_address()

kernel/module.c

/*
 * is_module_text_address - is this address inside module code?
 * @addr: the address to check.
 *
 * See is_module_address() if you simply want to see if the address is
 * anywhere in a module.  See kernel_text_address() for testing if an
 * address corresponds to kernel or module code.
 */
bool is_module_text_address(unsigned long addr)
{
        bool ret;
                                      
        preempt_disable();
        ret = __module_text_address(addr) != NULL;
        preempt_enable();

        return ret;
}

addr 주소가 모듈 코드 영역에 포함되어 있는지 여부를 알아온다.

 

__module_text_address()

kernel/module.c

/*
 * __module_text_address - get the module whose code contains an address.
 * @addr: the address.
 *
 * Must be called with preempt disabled or module mutex held so that
 * module doesn't get freed during this.
 */
struct module *__module_text_address(unsigned long addr)
{
        struct module *mod = __module_address(addr);
        if (mod) {
                /* Make sure it's within the text section. */
                if (!within(addr, mod->module_init, mod->init_text_size)
                    && !within(addr, mod->module_core, mod->core_text_size))
                        mod = NULL;
        }
        return mod;
}
EXPORT_SYMBOL_GPL(__module_text_address);

addr 주소가 모듈 초기화 코드 및 모듈 코어 코드에 포함되어 있는지 여부를 알아온다.

 

__module_address()

kernel/module.c

/*
 * __module_address - get the module which contains an address.
 * @addr: the address.
 *
 * Must be called with preempt disabled or module mutex held so that
 * module doesn't get freed during this.
 */
struct module *__module_address(unsigned long addr)
{
        struct module *mod;

        if (addr < module_addr_min || addr > module_addr_max)
                return NULL;

        list_for_each_entry_rcu(mod, &modules, list) {
                if (mod->state == MODULE_STATE_UNFORMED)
                        continue;
                if (within_module(addr, mod))
                        return mod;
        }
        return NULL;
}
EXPORT_SYMBOL_GPL(__module_address);

addr 주소가 module_addr_min ~ module_addr_max에 포함되어 있고 로드된 모듈의 코드영역에 있는 경우 module 포인터를 리턴하고 그렇지 않으면 null을 리턴한다.

 

within_module()

include/linux/module.h

static inline bool within_module(unsigned long addr, const struct module *mod)
{
        return within_module_init(addr, mod) || within_module_core(addr, mod);
}

addr 주소가 지정된 모듈의 초기화 코드 또는 코어 코드에 포함되어 있는지 여부를 리턴한다.

 

within_module_init()

include/linux/module.h

static inline bool within_module_init(unsigned long addr,
                                      const struct module *mod)
{                       
        return (unsigned long)mod->module_init <= addr &&
               addr < (unsigned long)mod->module_init + mod->init_size;
}

addr 주소가 지정된 모듈의 초기화 코드 영역에 포함되어 있는지 여부를 리턴한다.

 

is_ftrace_trampoline()

kernel/trace/ftrace.c

/*
 * This is used by __kernel_text_address() to return true if the
 * address is on a dynamically allocated trampoline that would
 * not return true for either core_kernel_text() or
 * is_module_text_address().
 */
bool is_ftrace_trampoline(unsigned long addr)
{
        struct ftrace_ops *op;
        bool ret = false;

        /*
         * Some of the ops may be dynamically allocated,
         * they are freed after a synchronize_sched().
         */
        preempt_disable_notrace();

        do_for_each_ftrace_op(op, ftrace_ops_list) {
                /*
                 * This is to check for dynamically allocated trampolines.
                 * Trampolines that are in kernel text will have
                 * core_kernel_text() return true.
                 */
                if (op->trampoline && op->trampoline_size)
                        if (addr >= op->trampoline &&
                            addr < op->trampoline + op->trampoline_size) {
                                ret = true;
                                goto out; 
                        }       
        } while_for_each_ftrace_op(op);
        
 out:   
        preempt_enable_notrace(); 

        return ret;
}

addr 주소가 각 ftrace_ops_list를 뒤져 각 op 구조체의 영역에 포함되어 있는지 여부를 알아내어 리턴한다.

 

do_for_each_ftrace_op()

kernel/trace/ftrace.c

/*
 * Traverse the ftrace_global_list, invoking all entries.  The reason that we
 * can use rcu_dereference_raw_notrace() is that elements removed from this list
 * are simply leaked, so there is no need to interact with a grace-period
 * mechanism.  The rcu_dereference_raw_notrace() calls are needed to handle
 * concurrent insertions into the ftrace_global_list.
 *
 * Silly Alpha and silly pointer-speculation compiler optimizations!
 */
#define do_for_each_ftrace_op(op, list)                 \
        op = rcu_dereference_raw_notrace(list);                 \
        do

 

while_for_each_ftrace_op()

kernel/trace/ftrace.c

/*
 * Optimized for just a single item in the list (as that is the normal case).
 */
#define while_for_each_ftrace_op(op)                            \
        while (likely(op = rcu_dereference_raw_notrace((op)->next)) &&  \
               unlikely((op) != &ftrace_list_end))

 

hotcpu_notifier()

 

hotcpu_notifier()를 사용하여 등록하는 callback 함수들

  • page_alloc_cpu_notify,
  • percpu_counter_hotcpu_callback
  • radix_tree_callback
  • blk_mq_queue_reinit_notify
  • pfault_cpu_notify
  • smp_cpu_notify
  • vgetcpu_cpu_notifier
  • apbt_cpuhp_notify
  • hpet_cpuhp_notify
  • uv_scir_cpu_notify
  • vfp_hotplug
  • loongson3_cpu_callback
  • octeon_cpu_callback
  • buffer_cpu_notify
  • cpu_callback
  • memcg_cpu_hotplug_callback
  • cpuset_cpu_inactive
  • cpuset_cpu_active
  • sched_domains_numa_masks_update
  • hotplug_hrtick
  • workqueue_cpu_down_callback
  • console_cpu_notify
  • profile_cpu_callback
  • topology_cpu_callback
  • cacheinfo_cpu_callback
  • dev_cpu_callback

 

hotcpu notifier 등록

hotcpu_notifier()

include/linux/cpu.h

#define hotcpu_notifier(fn, pri)        cpu_notifier(fn, pri)

 

cpu_notifier()

include/linux/cpu.h

/* Need to know about CPUs going up/down? */
#if defined(CONFIG_HOTPLUG_CPU) || !defined(MODULE)
#define cpu_notifier(fn, pri) {                                 \
        static struct notifier_block fn##_nb =                  \
                { .notifier_call = fn, .priority = pri };       \
        register_cpu_notifier(&fn##_nb);                        \
}
#else /* #if defined(CONFIG_HOTPLUG_CPU) || !defined(MODULE) */
#define cpu_notifier(fn, pri)   do { (void)(fn); } while (0)
#endif /* #else #if defined(CONFIG_HOTPLUG_CPU) || !defined(MODULE) */

신규 notifier_block 구조체 객체를 만들고 만들어진 객체를 cpu_chain에 추가하되 priority가 가장 높은 값이 선두에 위치한다.

  • 예) hotcpu_notifer(page_alloc_cpu_notify, 0)
    • static struct notifier_block page_alloc_cpu_notify_nb = { .notifier_call = page_alloc_cpu_notify, .priority = 0 };
    • register_cpu_notifier(page_alloc_cpu_notify_nb);

 

register_cpu_notifer()

kernel/cpu.c

/* Need to know about CPUs going up/down? */
int __ref register_cpu_notifier(struct notifier_block *nb)
{
        int ret;
        cpu_maps_update_begin();
        ret = raw_notifier_chain_register(&cpu_chain, nb);
        cpu_maps_update_done();
        return ret;
}

mutex lock으로 보호한 후 cpu chain에 신규 nb를 추가한다.

 

raw_notifier_chain_register()

kernel/notifier.c

/*
 *      Raw notifier chain routines.  There is no protection;
 *      the caller must provide it.  Use at your own risk!
 */

/**
 *      raw_notifier_chain_register - Add notifier to a raw notifier chain
 *      @nh: Pointer to head of the raw notifier chain
 *      @n: New entry in notifier chain
 *
 *      Adds a notifier to a raw notifier chain.
 *      All locking must be provided by the caller.
 *              
 *      Currently always returns zero.
 */             
int raw_notifier_chain_register(struct raw_notifier_head *nh,
                struct notifier_block *n)
{
        return notifier_chain_register(&nh->head, n);
}
EXPORT_SYMBOL_GPL(raw_notifier_chain_register);

아래 그림과 같이 raw_notifier_head nh에 신규 notifier_block n을 추가하되 priority가 가장 높은 값이 선두에 위치한다. 동일 priority의 경우 나중에 추가한 블럭은 뒤로 추가된다.

raw_notifier_chain_register-1

 

notifier_chain_register()

kernel/notifier.c

/*
 *      Notifier chain core routines.  The exported routines below
 *      are layered on top of these, with appropriate locking added.
 */

static int notifier_chain_register(struct notifier_block **nl,
                struct notifier_block *n)
{
        while ((*nl) != NULL) {
                if (n->priority > (*nl)->priority)
                        break;
                nl = &((*nl)->next);
        }
        n->next = *nl;
        rcu_assign_pointer(*nl, n);
        return 0;
}

아래 그림과 같이 신규 n의 우선순위가 비교 블럭 nl의 우선순위보다 높은 경우 신규 n을 비교 블럭 nl 앞에 끼워넣는다.

notifier_chain_register-1

호출(Notify)

 

호출 action

#define CPU_ONLINE              0x0002 /* CPU (unsigned)v is up */
#define CPU_UP_PREPARE          0x0003 /* CPU (unsigned)v coming up */
#define CPU_UP_CANCELED         0x0004 /* CPU (unsigned)v NOT coming up */
#define CPU_DOWN_PREPARE        0x0005 /* CPU (unsigned)v going down */
#define CPU_DOWN_FAILED         0x0006 /* CPU (unsigned)v NOT going down */
#define CPU_DEAD                0x0007 /* CPU (unsigned)v dead */
#define CPU_DYING               0x0008 /* CPU (unsigned)v not running any task,
                                        * not handling interrupts, soon dead.
                                        * Called on the dying cpu, interrupts
                                        * are already disabled. Must not
                                        * sleep, must not fail */
#define CPU_POST_DEAD           0x0009 /* CPU (unsigned)v dead, cpu_hotplug
                                        * lock is dropped */
#define CPU_STARTING            0x000A /* CPU (unsigned)v soon running.
                                        * Called on the new cpu, just before
                                        * enabling interrupts. Must not sleep,
                                        * must not fail */

 

cpu_notify()

kernel/cpu.c

static int cpu_notify(unsigned long val, void *v)
{
        return __cpu_notify(val, v, -1, NULL);
}
  • cpu action val과 data v로 cpu_chin에 등록되어 있는 모든 콜백함수를 호출한다.

 

__cpu_notify()

kernel/cpu.c

static int __cpu_notify(unsigned long val, void *v, int nr_to_call,
                        int *nr_calls)
{
        int ret;

        ret = __raw_notifier_call_chain(&cpu_chain, val, v, nr_to_call,
                                        nr_calls);

        return notifier_to_errno(ret);
}

cpu_chain에 등록된 콜백함수를 nr_to_call 수만큼 순서대로 호출하되 인수로 cpu action val과 데이터 v를 사용한다. 출력 인수 nr_calls에 호출된 수를 저장하고 에러 여부를 리턴한다.

 

__raw_notifier_call_chain()

kernel/notifier.c

/**
 *      __raw_notifier_call_chain - Call functions in a raw notifier chain
 *      @nh: Pointer to head of the raw notifier chain
 *      @val: Value passed unmodified to notifier function
 *      @v: Pointer passed unmodified to notifier function
 *      @nr_to_call: See comment for notifier_call_chain.
 *      @nr_calls: See comment for notifier_call_chain
 *              
 *      Calls each function in a notifier chain in turn.  The functions
 *      run in an undefined context.
 *      All locking must be provided by the caller.
 *
 *      If the return value of the notifier can be and'ed
 *      with %NOTIFY_STOP_MASK then raw_notifier_call_chain()
 *      will return immediately, with the return value of
 *      the notifier function which halted execution.
 *      Otherwise the return value is the return value
 *      of the last notifier function called.
 */
int __raw_notifier_call_chain(struct raw_notifier_head *nh,
                              unsigned long val, void *v,
                              int nr_to_call, int *nr_calls)
{               
        return notifier_call_chain(&nh->head, val, v, nr_to_call, nr_calls);
}
EXPORT_SYMBOL_GPL(__raw_notifier_call_chain);

nh가 가리키는 첫번째 콜백함수 부터 nr_to_call 수만큼 순서대로 호출하되 인수로 cpu action val과 데이터 v를 사용한다. 출력 인수 nr_calls에 호출된 수를 저장하고 에러 여부를 리턴한다.

 

notifier_call_chain()

kernel/notifier.c

/**
 * notifier_call_chain - Informs the registered notifiers about an event.
 *      @nl:            Pointer to head of the blocking notifier chain
 *      @val:           Value passed unmodified to notifier function
 *      @v:             Pointer passed unmodified to notifier function
 *      @nr_to_call:    Number of notifier functions to be called. Don't care
 *                      value of this parameter is -1.
 *      @nr_calls:      Records the number of notifications sent. Don't care
 *                      value of this field is NULL.
 *      @returns:       notifier_call_chain returns the value returned by the
 *                      last notifier function called.
 */
static int notifier_call_chain(struct notifier_block **nl,
                               unsigned long val, void *v,
                               int nr_to_call, int *nr_calls)
{
        int ret = NOTIFY_DONE;
        struct notifier_block *nb, *next_nb;

        nb = rcu_dereference_raw(*nl);

        while (nb && nr_to_call) {
                next_nb = rcu_dereference_raw(nb->next);

#ifdef CONFIG_DEBUG_NOTIFIERS
                if (unlikely(!func_ptr_is_kernel_text(nb->notifier_call))) {
                        WARN(1, "Invalid notifier called!");
                        nb = next_nb;
                        continue;
                }
#endif
                ret = nb->notifier_call(nb, val, v);

                if (nr_calls)
                        (*nr_calls)++;

                if ((ret & NOTIFY_STOP_MASK) == NOTIFY_STOP_MASK)
                        break;
                nb = next_nb;
                nr_to_call--;
        }
        return ret;
}
NOKPROBE_SYMBOL(notifier_call_chain);

첫번째 콜백함수 nl 부터 nr_to_call 수만큼 순서대로 호출하되 인수로 cpu action val과 데이터 v를 사용한다. 출력 인수 nr_calls에 호출된 수를 저장하고 에러 발생 시 NOTIFY_STOP_MASK 비트를 포함하는 경우 루프를 탈출하고 에러 값을 리턴한다.

 

구조체

include/linux/notifier.h

/*      
 * Notifier chains are of four types:
 *
 *      Atomic notifier chains: Chain callbacks run in interrupt/atomic
 *              context. Callouts are not allowed to block.
 *      Blocking notifier chains: Chain callbacks run in process context.
 *              Callouts are allowed to block.
 *      Raw notifier chains: There are no restrictions on callbacks,
 *              registration, or unregistration.  All locking and protection
 *              must be provided by the caller.
 *      SRCU notifier chains: A variant of blocking notifier chains, with
 *              the same restrictions.
 *      
 * atomic_notifier_chain_register() may be called from an atomic context,
 * but blocking_notifier_chain_register() and srcu_notifier_chain_register()
 * must be called from a process context.  Ditto for the corresponding
 * _unregister() routines.
 *
 * atomic_notifier_chain_unregister(), blocking_notifier_chain_unregister(),
 * and srcu_notifier_chain_unregister() _must not_ be called from within
 * the call chain.
 *
 * SRCU notifier chains are an alternative form of blocking notifier chains.
 * They use SRCU (Sleepable Read-Copy Update) instead of rw-semaphores for
 * protection of the chain links.  This means there is _very_ low overhead
 * in srcu_notifier_call_chain(): no cache bounces and no memory barriers.
 * As compensation, srcu_notifier_chain_unregister() is rather expensive.
 * SRCU notifier chains should be used when the chain will be called very
 * often but notifier_blocks will seldom be removed.  Also, SRCU notifier
 * chains are slightly more difficult to use because they require special
 * runtime initialization.
 */

 

notifier_fn_t  타입

typedef int (*notifier_fn_t)(struct notifier_block *nb,
                        unsigned long action, void *data);

 

notifier_block 구조체

struct notifier_block {
        notifier_fn_t notifier_call;
        struct notifier_block __rcu *next;
        int priority;
};

 

atomic_notifier_head 구조체

struct atomic_notifier_head {
        spinlock_t lock;
        struct notifier_block __rcu *head;
};

 

blocking_notifier_head 구조체

struct blocking_notifier_head {
        struct rw_semaphore rwsem;
        struct notifier_block __rcu *head;
};

 

raw_notifier_head 구조체

struct raw_notifier_head {
        struct notifier_block __rcu *head;
};

 

srcu_notifier_head 구조체

struct srcu_notifier_head {
        struct mutex mutex;
        struct srcu_struct srcu;
        struct notifier_block __rcu *head;
};

 

전역 cpu_chain 구조체

static RAW_NOTIFIER_HEAD(cpu_chain);
  • static raw_notifier_head cpu_chain = { .head = null }

 

/* srcu_notifier_heads cannot be initialized statically */

#define ATOMIC_NOTIFIER_HEAD(name)                              \
        struct atomic_notifier_head name =                      \
                ATOMIC_NOTIFIER_INIT(name)
#define BLOCKING_NOTIFIER_HEAD(name)                            \
        struct blocking_notifier_head name =                    \
                BLOCKING_NOTIFIER_INIT(name)
#define RAW_NOTIFIER_HEAD(name)                                 \
        struct raw_notifier_head name =                         \
                RAW_NOTIFIER_INIT(name)

/* srcu_notifier_heads must be initialized and cleaned up dynamically */
extern void srcu_init_notifier_head(struct srcu_notifier_head *nh);
#define srcu_cleanup_notifier_head(name)        \
                cleanup_srcu_struct(&(name)->srcu);

#define ATOMIC_NOTIFIER_INIT(name) {                            \
                .lock = __SPIN_LOCK_UNLOCKED(name.lock),        \
                .head = NULL }
#define BLOCKING_NOTIFIER_INIT(name) {                          \
                .rwsem = __RWSEM_INITIALIZER((name).rwsem),     \
                .head = NULL }
#define RAW_NOTIFIER_INIT(name) {                               \
                .head = NULL }

 

build_all_zonelists()

<kernel v5.0>

이 함수는 부팅 시 호출이 되는 경우 전체 노드에 대해 zonelist를 구성하고 현재 cpu에 대해 모든 노드의 메모리를 access 할 수 있도록 설정한다. 또한 운영 중에 핸들러에 의해 호출(hotplug memory)되는 경우 전체 cpu를 멈추고 zone에 대한 boot pageset 테이블 구성 및 전체 노드에 대해 zonelist를 다시 구성한다. 구성 후 free 페이지가 적은 경우 전역 변수 page_group_by_mobility_disabled를 1로 하여 mobility 기능을 잠시 disable하고 나중에 enable한다.

 

zone 구성

  • zone은 하나 이상 구성할 수 있다. 32bit 시스템에서는 최대 4개까지 조합이 가능하다.
    • ZONE_DMA
    • ZONE_DMA32
    • ZONE_NORMAL
    • ZONE_HIGHMEM
    • ZONE_MOVABLE
    • ZONE_DEVICE

 

zonelists

  • fallback
    • 각 노드에 존들이 구성되어 있고 특정 노드에서 메모리를 할당해야 할 때 할당할 영역(노드 + 존)을 선택하는데, 그 영역에서 메모리 부족(out of memory)으로 인해 할당이 실패하는 경우 대체(fallback) 영역에서 할당을 재시도해야 한다. 그때마다 영역을 찾지 않고 일정한 규칙에 의해 할당 우선순위 리스트를 만들어두어 사용하는데, 이것을 zonelist라고 한다.
  • zonelist order
    • 우선순위를 두어 단계적인 fallback용 zonelist를 구성한다. 시스템 설계에 따라 노드 우선(node order) 또는 존 우선(zone order)을 결정하여 순서를 정할 수 있다. 디폴트로 32비트 시스템은 존 우선으로 되어 있고, 64비트 시스템은 노드 우선으로 구성한다. 단 커널 버전 4.14-rc1부터 존 우선(zone order)으로 zonelist를 생성하는 루틴을 삭제하고 노드 우선(node order)만사용한다.
    • 리눅스 시스템은 zonelist order와 관련하여 다음과 같이 두 가지 중 하나를 선택하여 동작한다.
      • 노드 우선(node order)
        • 베스트 노드부터 역순으로 정렬한 존 타입으로 zonelist를 구성한다. 노드 우선으로 동작하더라도 ZONE_DMA 또는 ZONE_DMA32에 대해서는 아키텍처에 따라 할당을 제한하기도 한다.
      • 존 우선(zone order)
        • 역순으로 정렬한 존 타입부터 베스트 노드 순서대로 zonelist를 구성한다. ZONE_NORMAL 영역에 대해 충분히 free 영역을 확보할 필요가 있기 때문에, 보통 메모리가 적은 32비트 시스템에서는 존 우선 방식을 디폴트로 사용하여 ZONE_HIGHMEM(ZONE_MOVABLE)부터 우선 사용할 수 있게 한다. 32비트 시스템에서는 보통 메모리가 충분하지 않으므로 ZONE_DMA 및 ZONE_NORMAL 메모리를 보호하기 위해 존 우선을 사용한다
  • node와 zone의 검색 방향
    • 노드(순방향)
      • 노드 간 접근 속도가 다르므로 현재 노드부터 가장 빠른 노드(best)를 우선순위로 둔다.
    • 존(역방향)
      •  존에 대해 높은 번호부터 아래 번호 순서로 할당 시도를 한다. DMA 영역은 가장 낮은 순위로 메모리 할당을 하여 보호받는다.
        • ZONE_DEVICE(최상위 우선 순위)
        • ZONE_MOVABLE
        • ZONE_HIGHMEM(64비트 시스템에서는 사용되지 않는다.)
        • ZONE_NORMAL
        • ZONE_DMA32
        • ZONE_DMA
  • NUMA 시스템에서는 2개의 zonelists를 사용한다.
    • zonelists[0]은 전체 노드를 대상으로 만들어진다.
    • zonelists[1]은 현재 노드만을 대상으로 만들어진다(NUMA only). _ _GFP_THISNODE 사용 시 활용한다.

 

MOVABLE 존

이 영역은 특별히 버디 시스템으로 구현된 페이지 할당자가 메모리 단편화를 막기 위해 전용으로 사용하는 영역이며, 동시에 메모리 핫플러그를 지원하기 위해 구성돼야 하는 영역이다. 버디 시스템에서 메모리 단편화를 막기 위해 사용하는 방법으로 가능하면 마이그레이션 타입별로 관리를 하는데, 좀 더 확실한 영역을 지정하여 더 효율적으로 사용할 수 있다.

두 가지 구성 방법을 알아보자.

  • 크기로 설정
    • 커널 파라미터로 ‘kernelcore=’ 또는 ‘movablecore’를 사용해 크기를 지정하여 사용한다. kernelcore=를 사용하는 경우 전체 메모리에서 지정된 크기를 제외한 나머지 크기가 ZONE_MOVABLE로 설정된다. movablecore=를 사용하여 지정된 크기가 ZONE_MOVABLE로 설정된다.
  • 특정 노드 메모리를 지정
    • CONFIG_MOVABLE_NODE 커널 옵션을 사용하면서 커널 파라미터로 movable_node를 사용하고 memory memblock regions의 flags에 MEMBLOCK
      _HOTPLUG 플래그 비트를 설정한 메모리 블록을 통째로 ZONE_MOVABLE로 지정할 수 있다. 메모리 핫플러그가 가능한 메모리 영역을 지정하여 사용한다.

리눅스에서 메모리 핫플러그 구현은 완료되었고, 이 기능을 응용하여 유연하게 메모리의 동적 구성이 가능하다. 실제 하드웨어로 메모리 핫플러그가 시험되었는지 여부는 미지수다. 구현 당시에는 하드웨어가 준비되지 않은 상태였다.

movable(_ _GFP_MOVABLE) 속성을 가진 페이지들만 ZONE_MOVABLE에 들어갈 수 있다. 버디 시스템으로 구현된 페이지 할당자에서 연속된 페이지 요청을 수행하기 힘든 경우 메모리 컴팩션이 일어나는데, 이때 movable 속성을 가진 페이지를 다른 주소로 마이그레이션(migration) 시킨다.

메모리 컴팩션은 버디 시스템에서 연속된 free 메모리가 부족하여 요청한 오더 페이지에 대한 할당이 불가능할 때 해당 오더의 앞쪽에 있는 사용된 페이지를 뒤쪽으로 옮겨서 앞부분에 필요한 공간을 만드는 메커니즘이라고 할 수 있다.

ZONE_MOVABLE은 highest 존의 메모리를 분할하여 사용한다. 또한 아키텍처와 메모리 크기에 따라 해당 시스템의 highest 존이 다르다.

  • ARM32 예) CONFIG_HIGHMEM을 선택하고, 만일 없으면 CONFIG_NORMAL을 선택함
  • ARM64 예) CONFIG_NORMAL을 선택하고, 만일 없으면 ZONE_DMA32를 선택함
  • x86_32 예) ZONE_HIGHMEM을 선택하고, 만일 없으면 ZONE_NORMAL을 선택함
  • x86_64 예) ZONE_NORMAL을 선택하고, 만일 없으면 ZONE_DMA32를 선택함

 


zonelist 초기화

이 함수는 부팅 시에 호출되는 경우 전체 노드에 대해 zonelist를 구성하고 현재 cpu에 대해 모든 노드의 메모리를 액세스할 수 있도록 설정한다. 또한 운영 중에 핸들러에 의해 호출(핫플러그 메모리)되는 경우 전체 cpu를 멈추고 존에 대한 boot pageset 테이블 구성 및 전체 노드에 대해 zonelist를 다시 구성한다. 구성 후 free 페이지가 적은 경우 전역 변수 page_group_by_mobility_disabled를 1로 하여 mobility 기능을 잠시 disable하고 나중에 enable한다.

 

다음 그림은 zonelist를 구성할 때의 함수 호출 관계이다.

 

build_all_zonelists()

mm/page_alloc.c

/*
 * unless system_state == SYSTEM_BOOTING.
 *
 * __ref due to call of __init annotated helper build_all_zonelists_init
 * [protected by SYSTEM_BOOTING].
 */
void __ref build_all_zonelists(pg_data_t *pgdat)
{
        if (system_state == SYSTEM_BOOTING) {
                build_all_zonelists_init();
        } else {
                __build_all_zonelists(pgdat);
                /* cpuset refresh routine should be here */
        }
        vm_total_pages = nr_free_pagecache_pages();
        /*
         * Disable grouping by mobility if the number of pages in the
         * system is too low to allow the mechanism to work. It would be
         * more accurate, but expensive to check per-zone. This check is
         * made on memory-hotadd so a system can start with mobility
         * disabled and enable it later
         */
        if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
                page_group_by_mobility_disabled = 1;
        else
                page_group_by_mobility_disabled = 0;

        pr_info("Built %i zonelists, mobility grouping %s.  Total pages: %ld\n",
                nr_online_nodes,
                page_group_by_mobility_disabled ? "off" : "on",
                vm_total_pages);
#ifdef CONFIG_NUMA
        pr_info("Policy zone: %s\n", zone_names[policy_zone]);
#endif
}

각 노드의 활성화된 존에 fallback list인 zonelist를 생성한다. 첫 번째 인자로 주어지는 pgdat의 타입 pg_data_t는 노드 정보를 담고 있는 pglist_data 구조체를 타입 정의(define)하여 사용했다.

  • 코드 라인 3~8에서 최초 부팅 시에 호출된 경우 곧바로 최초 zonelist를 구성한다. 최초 부팅 시가 아니면 zonelist를 재구성한다. 만일 핫플러그 메모리가 지원되는 경우 존에 대한 per-cpu 페이지 세트 프레임 캐시를 할당받고 구성한다.
  • 코드 라인 9에서 zonelist에서 high 워터마크까지의 페이지 개수를 제외한 free 페이지를 재계산한다.
  • 코드 라인 17~20에서 vm_total_pages가 migrate 타입 수(최대 6개) * 페이지 블록 크기보다 작으면 free 메모리가 매우 부족한 상태이므로 전역 page_group_by_mobility_disabled 변수를 true로 설정하여 페이지 블록에 migratetype 저장 시 항상 unmovable 타입을 사용하여 mobility 기능을 제한한다.
  • 코드 라인 22~25에서 zonelist 순서(order) 정보와 모빌리티 그루핑 동작 여부와 전체 페이지 수 정보등을 출력한다.
    • 예) “Built 1 zonelists in Zone order, mobility grouping on. Total pages: 999432”
  • 코드 라인 27에서 NUMA 시스템인 경우 policy 존 정보를 출력한다.
    • 예) “Policy zone: Normal”

 

policy_zone

  • vma_migrate() 함수를 통해 사용되는 movable을 제외한 최상위 존을 담은 변수로 policy 존 미만을 사용하는 존으로 migrate를 제한할 목적으로 사용된다.
  • zonelist를 만드는 과정에서 build_zonerefs_node() -> check_highest_zone() 함수를 통해 policy_zone이 설정된다.

 

system_states

include/linux/kernel.h

/*
 * Values used for system_state. Ordering of the states must not be changed
 * as code checks for <, <=, >, >= STATE.
 */
extern enum system_states {
        SYSTEM_BOOTING,
        SYSTEM_SCHEDULING,
        SYSTEM_RUNNING,
        SYSTEM_HALT,
        SYSTEM_POWER_OFF,
        SYSTEM_RESTART,
        SYSTEM_SUSPEND,
} system_state;

 

build_all_zonelists_init()

mm/page_alloc.c

static noinline void __init
build_all_zonelists_init(void)
{
        int cpu;

        __build_all_zonelists(NULL);

        /*
         * Initialize the boot_pagesets that are going to be used
         * for bootstrapping processors. The real pagesets for
         * each zone will be allocated later when the per cpu
         * allocator is available.
         *
         * boot_pagesets are used also for bootstrapping offline
         * cpus if the system is already booted because the pagesets
         * are needed to initialize allocators on a specific cpu too.
         * F.e. the percpu allocator needs the page allocator which
         * needs the percpu allocator in order to allocate its pagesets
         * (a chicken-egg dilemma).
         */
        for_each_possible_cpu(cpu)
                setup_pageset(&per_cpu(boot_pageset, cpu), 0);

        mminit_verify_zonelist();
        cpuset_init_current_mems_allowed();
}

노드별로 zonelist를 생성하고 현재 태스크의 mems_allowed 노드마스크를 모두 설정하여 전체 메모리 노드에서 할당받을 수 있음을 나타낸다.

 

__build_all_zonelists()

mm/page_alloc.c

static void __build_all_zonelists(void *data)
{
        int nid;
        int __maybe_unused cpu;
        pg_data_t *self = data;
        static DEFINE_SPINLOCK(lock);

        spin_lock(&lock);

#ifdef CONFIG_NUMA
        memset(node_load, 0, sizeof(node_load));
#endif

        /*
         * This node is hotadded and no memory is yet present.   So just
         * building zonelists is fine - no need to touch other nodes.
         */
        if (self && !node_online(self->node_id)) {
                build_zonelists(self);
        } else {
                for_each_online_node(nid) {
                        pg_data_t *pgdat = NODE_DATA(nid);

                        build_zonelists(pgdat);
                }

#ifdef CONFIG_HAVE_MEMORYLESS_NODES
                /*
                 * We now know the "local memory node" for each node--
                 * i.e., the node of the first zone in the generic zonelist.
                 * Set up numa_mem percpu variable for on-line cpus.  During
                 * boot, only the boot cpu should be on-line;  we'll init the
                 * secondary cpus' numa_mem as they come on-line.  During
                 * node/memory hotplug, we'll fixup all on-line cpus.
                 */
                for_each_online_cpu(cpu)
                        set_cpu_numa_mem(cpu, local_memory_node(cpu_to_node(cpu)));
#endif
        }

        spin_unlock(&lock);
}

인자를 통해 지정한 노드 또는 모든 노드에 대해 zonelist와 per-cpu 페이지 프레임 캐시를 구성한다.

  • 코드 라인 5에서 최초 build_zonelists_all() 함수 호출 시 NULL이 data 인자로 주어졌다.
  • 코드 라인 18~19에서 인자로 요청한 노드가 온라인 노드가 아니면 zonelists를 구성한다.
  • 코드 라인 21~25에서 모든 온라인 노드를 순회하며 각 노드의 zonelists를 구성한다.
  • 코드 라인 36~37에서 커널이 메모리리스 노드를 지원하는 경우 온라인 cpu별로 메모리가 있는 인접 노드를 지정한다.

 

build_zonelists()

mm/page_alloc.c

/*
 * Build zonelists ordered by zone and nodes within zones.
 * This results in conserving DMA zone[s] until all Normal memory is
 * exhausted, but results in overflowing to remote node while memory
 * may still exist in local DMA zone.
 */
static void build_zonelists(pg_data_t *pgdat)
{
        static int node_order[MAX_NUMNODES];
        int node, load, nr_nodes = 0;
        nodemask_t used_mask;
        int local_node, prev_node;

        /* NUMA-aware ordering of nodes */
        local_node = pgdat->node_id;
        load = nr_online_nodes;
        prev_node = local_node;
        nodes_clear(used_mask);

        memset(node_order, 0, sizeof(node_order));
        while ((node = find_next_best_node(local_node, &used_mask)) >= 0) {
                /*
                 * We don't want to pressure a particular node.
                 * So adding penalty to the first node in same
                 * distance group to make it round-robin.
                 */
                if (node_distance(local_node, node) !=
                    node_distance(local_node, prev_node))
                        node_load[node] = load;

                node_order[nr_nodes++] = node;
                prev_node = node;
                load--;
        }

        build_zonelists_in_node_order(pgdat, node_order, nr_nodes);
        build_thisnode_zonelists(pgdat);
}

유저 설정 또는 디폴트 설정에 따라 노드 우선 또는 존 우선을 선택하여 zonelist를 만들어낸다.

  • 코드 라인 15~코드 used_mask 비트맵에 포함되지 않은 노드 중 가장 인접한 노드 id를 구한다.
  • 코드 라인 21~23에서 현재 노드와 알아온 인접 노드의 거리가 현재 노드와 기존 노드의 거리와 다르다면, 즉 이전 노드와 거리가 다르다면 node_load[node]에 load 값을 설정한다.
  • 코드 라인 26~27에서 prev_node에 방금 처리한 현재 노드 id를 설정하고 load 값을 감소시킨다.
  • 코드 라인 30에서 노드 오더용 zonelist를 구성하여 zonelists[0]에 추가한다.
  • 코드 라인 31에서 현재 노드에 대해서만 활성화된 존을 zonelists[1]에 추가한다.
    • GFP_THISNODE 플래그 옵션이 주어지는 경우 노드 fallback 기능을 사용하지 않도록 이 zonelists[1]을 사용한다.

 

“numa_zonelist_order” 커널 파라메터 & “/proc/sys/vm/numa_zonelist_order”

setup_numa_zonelist_order()

mm/page_alloc.c

static __init int setup_numa_zonelist_order(char *s)
{
        if (!s)
                return 0;

        return __parse_numa_zonelist_order(s);
}
early_param("numa_zonelist_order", setup_numa_zonelist_order);

누마 시스템에서 zonelist order를 다음 중 하나로 선택하였었으나 커널 v4.14-rc1 이후로는 이러한 커널 파라미터를 사용하는 경우 경고 메시지를 출력한다.

  • “numa_zonelist_order=d”
    • automatic configuration
  • “numa_zonelist_order=n”
    • node order (노드 거리에 따른 가장 가까운 best 노드 순)
  • “numa_zonelist_order=z”
    • zone order (zone type 역순)

 

__parse_numa_zonelist_order()

mm/page_alloc.c

static int __parse_numa_zonelist_order(char *s)
{
        /*
         * We used to support different zonlists modes but they turned
         * out to be just not useful. Let's keep the warning in place
         * if somebody still use the cmd line parameter so that we do
         * not fail it silently
         */
        if (!(*s == 'd' || *s == 'D' || *s == 'n' || *s == 'N')) {
                pr_warn("Ignoring unsupported numa_zonelist_order value:  %s\n", s);
                return -EINVAL;
        }
        return 0;
}

문자열이 Default 및 Node 약자인 d, D, n, N 문자를 사용하면 경고 메시지를 출력한다.

  • 그 외의 문자는 skip 한다. (“Z”, “z” 등)

 

node_load[] 및 node_order[]

다음 그림은 NUMA 시스템에서 node_load[] 및 node_order[]를 알아본 사례이다.

  • node_order[]
    • 현재 부팅한 cpu가 있는 로컬 노드를 대상으로 가장 빠른(가까운) 순서의 노드 번호를 저장한다.
  • node_load[]
    • 다음 best 노드를 찾을 때 사용할 노드별 가중치 값

build_zonelists-1c

 

build_zonelists_in_node_order()

mm/page_alloc.c

/*
 * Build zonelists ordered by node and zones within node.
 * This results in maximum locality--normal zone overflows into local
 * DMA zone, if any--but risks exhausting DMA zone.
 */
static void build_zonelists_in_node_order(pg_data_t *pgdat, int *node_order,
                unsigned nr_nodes)
{
        struct zoneref *zonerefs;
        int i;

        zonerefs = pgdat->node_zonelists[ZONELIST_FALLBACK]._zonerefs;

        for (i = 0; i < nr_nodes; i++) {
                int nr_zones;

                pg_data_t *node = NODE_DATA(node_order[i]);

                nr_zones = build_zonerefs_node(node, zonerefs);
                zonerefs += nr_zones;
        }
        zonerefs->zone = NULL;
        zonerefs->zone_idx = 0;
}

zonelist를 노드 우선으로 만든다.

 

아래 그림은 4개 노드에 대해 node order로 구축한 zonelists를 보여준다.

  • 경우에 따라서는 DMA(DMA32)만 특별히 가장 밑으로 옮긴 경우도 있다.

build_zonelists_in_node_order-1b

 

아래 그림은 4개 노드에 대해 zone order로 구축한 zonelists를 보여준다. (커널 4.14-rc1 부터 사용하지 않음)

build_zonelists_in_zone_order-1b

 

build_zonerefs_node()

mm/page_alloc.c

/*
 * Builds allocation fallback zone lists.
 *
 * Add all populated zones of a node to the zonelist.
 */
static int build_zonerefs_node(pg_data_t *pgdat, struct zoneref *zonerefs)
{
        struct zone *zone;
        enum zone_type zone_type = MAX_NR_ZONES;
        int nr_zones = 0;

        do {
                zone_type--;
                zone = pgdat->node_zones + zone_type;
                if (managed_zone(zone)) {
                        zoneref_set_zone(zone, &zonerefs[nr_zones++]);
                        check_highest_zone(zone_type);
                }
        } while (zone_type);

        return nr_zones;
}

노드에 관련된 zone 정보를 구성하고, 구성한 존 수를 반환한다.

  • 코드 라인 4~9에서 MAX_NR_ZONES(3)가 가리키는 최상위 존부터 ~ 최하위 0번을 제외한 1번 존까지 순회한다.
  • 코드 라인 10~13에서 실제 사용할 페이지가 있는 zone인 경우 출력 인자 @zonerefs 배열에 zone 정보를 추가하고 최상위 존인 경우 policy zone을 갱신한다.

 

build_thisnode_zonelists()

mm/page_alloc.c

/*
 * Build gfp_thisnode zonelists
 */
static void build_thisnode_zonelists(pg_data_t *pgdat)
{
        struct zoneref *zonerefs;
        int nr_zones;

        zonerefs = pgdat->node_zonelists[ZONELIST_NOFALLBACK]._zonerefs;
        nr_zones = build_zonerefs_node(pgdat, zonerefs);
        zonerefs += nr_zones;
        zonerefs->zone = NULL;
        zonerefs->zone_idx = 0;
}

요청한 단일 노드용 zonelist를 구성한다.

 

Zonelist 캐시

zonelist 캐시는 커널 v4.4-rc1에서 제거되었다.

 

아래 그림은 zonelist_cache를 구성한 모습이다. fullzones 비트맵은 0으로 clear한다.

build_zonelist_cache-1b

 


mminit_verify_zonelist()

mm/mm_init.c

/* The zonelists are simply reported, validation is manual. */
void __init mminit_verify_zonelist(void)
{
        int nid;

        if (mminit_loglevel < MMINIT_VERIFY)
                return;

        for_each_online_node(nid) {
                pg_data_t *pgdat = NODE_DATA(nid);
                struct zone *zone;
                struct zoneref *z;
                struct zonelist *zonelist;
                int i, listid, zoneid;

                BUG_ON(MAX_ZONELISTS > 2);
                for (i = 0; i < MAX_ZONELISTS * MAX_NR_ZONES; i++) {
 
                        /* Identify the zone and nodelist */
                        zoneid = i % MAX_NR_ZONES;
                        listid = i / MAX_NR_ZONES;
                        zonelist = &pgdat->node_zonelists[listid];
                        zone = &pgdat->node_zones[zoneid];
                        if (!populated_zone(zone))
                                continue;

                        /* Print information about the zonelist */
                        printk(KERN_DEBUG "mminit::zonelist %s %d:%s = ",
                                listid > 0 ? "thisnode" : "general", nid,
                                zone->name);

                        /* Iterate the zonelist */
                        for_each_zone_zonelist(zone, z, zonelist, zoneid)
                                pr_cont("%d:%s ", zone_to_nid(zone), zone->name);
                        printk("\n");
                }               
        }
}

모든 노드의 존에 대해서 zonelists를 출력한다.

 

cpuset_init_current_mems_allowed()

kernel/cpuset.c

void __init cpuset_init_current_mems_allowed(void)
{
        nodes_setall(current->mems_allowed);
}

현재 태스크가 모든 노드의 메모리를 사용할 수 있도록 설정한다.

  • 현재 태스크의 mems_allowed 노드마스크 비트맵에 대해 모든 비트를 1로 설정한다.

 

nodes_setall()

include/linux/nodemask.h

#define nodes_setall(dst) __nodes_setall(&(dst), MAX_NUMNODES)
static inline void __nodes_setall(nodemask_t *dstp, unsigned int nbits)
{
        bitmap_fill(dstp->bits, nbits);
}

지정된 dst 노드 비트맵 마스크에 대해 모든 노드의 메모리를 사용할 수 있도록 설정한다.

  • dstp 노드마스크 비트맵에 대해 MAX_NUMNODES 수 만큼의 비트를 1로 설정한다

 

managed_zone()

include/linux/mmzone.h

/*
 * Returns true if a zone has pages managed by the buddy allocator.
 * All the reclaim decisions have to use this function rather than
 * populated_zone(). If the whole zone is reserved then we can easily
 * end up with populated_zone() && !managed_zone().
 */
static inline bool managed_zone(struct zone *zone)
{
        return zone_managed_pages(zone);
}

해당 존이 버디 할당자가 관리하는 페이지인지 여부를 반환한다.

zone_managed_pages()

include/linux/mmzone.h

static inline unsigned long zone_managed_pages(struct zone *zone)
{
        return (unsigned long)atomic_long_read(&zone->managed_pages);
}

해당 존에서 버디 할당자가 관리하는 페이지 수를 반환한다.

zoneref_set_zone()

mm/page_alloc.c

static void zoneref_set_zone(struct zone *zone, struct zoneref *zoneref)
{
        zoneref->zone = zone;
        zoneref->zone_idx = zone_idx(zone);
}

zoneref 구조체에 zone 정보를 기록한다.

 

check_highest_zone()

include/linux/mempolicy.h

static inline void check_highest_zone(enum zone_type k)
{
        if (k > policy_zone && k != ZONE_MOVABLE)
                policy_zone = k;
}

인자로 요청한 존이 최상인 경우 policy 존으로 갱신한다. movable 존을 제외하고, device 존 -> highmem 존 -> normal 존 순서이다.

 


빈 페이지 캐시 페이지 산출

nr_free_pagecache_pages()

mm/page_alloc.c

/**
 * nr_free_pagecache_pages - count number of pages beyond high watermark
 *
 * nr_free_pagecache_pages() counts the number of pages which are beyond the
 * high watermark within all zones.
 */
unsigned long nr_free_pagecache_pages(void)
{
        return nr_free_zone_pages(gfp_zone(GFP_HIGHUSER_MOVABLE));
}

유저 페이지 캐시용(movable 가능한 페이지)으로 사용할 수 있는 빈 페이지 수를 반환한다. 단 high 워터마크 미만 페이지들은 제외한다.

  • highmem 존 또는 movable 존 이하 대상

 

nr_free_zone_pages()

mm/page_alloc.c

/**
 * nr_free_zone_pages - count number of pages beyond high watermark
 * @offset: The zone index of the highest zone
 *
 * nr_free_zone_pages() counts the number of counts pages which are beyond the
 * high watermark within all zones at or below a given zone index.  For each
 * zone, the number of pages is calculated as:
 *
 *     nr_free_zone_pages = managed_pages - high_pages
 */
static unsigned long nr_free_zone_pages(int offset)
{
        struct zoneref *z;
        struct zone *zone;

        /* Just pick one node, since fallback list is circular */
        unsigned long sum = 0;

        struct zonelist *zonelist = node_zonelist(numa_node_id(), GFP_KERNEL);

        for_each_zone_zonelist(zone, z, zonelist, offset) {
                unsigned long size = zone_managed_pages(zone);
                unsigned long high = high_wmark_pages(zone);
                if (size > high)
                        sum += size - high;
        }

        return sum;
}

zonelist의 존들 중 offset 존 이하의 zone에 대해 free 페이지 수를 알아오는데 high 워터마크 페이지 수를 제외한다.

  • 코드 라인 9에서 현재 cpu의 노드용 zonelist이다.
  • 코드 라인 11~16에서 zonelist에 포함된 모든 zone들 중 offset 이하의 존을 순회하며 버디 시스템이 관리하는 free 페이지인 managed 페이지를 누적하되 high 워터마크 이하의 페이지는 제외시킨다.

 

numa_node_id()

include/linux/topology.h

#ifndef numa_node_id
/* Returns the number of the current Node. */
static inline int numa_node_id(void)
{
        return raw_cpu_read(numa_node);
}
#endif

per-cpu 데이터인 numa_node 값을 알아온다. 즉 해당 cpu에 대한 numa_node id 값을 리턴한다.

  • 이 값은 set_numa_node() 또는 set_cpu_numa_node() 함수에 의해 설정된다.

 

node_zonelist()

include/linux/gfp.h

/*
 * We get the zone list from the current node and the gfp_mask.
 * This zone list contains a maximum of MAXNODES*MAX_NR_ZONES zones.
 * There are two zonelists per node, one for all zones with memory and
 * one containing just zones from the node the zonelist belongs to.
 *
 * For the normal case of non-DISCONTIGMEM systems the NODE_DATA() gets
 * optimized to &contig_page_data at compile-time.
 */
static inline struct zonelist *node_zonelist(int nid, gfp_t flags)
{
        return NODE_DATA(nid)->node_zonelists + gfp_zonelist(flags);
}

노드에 대응하는 2 개의 zonelists 중 하나를 다음 조건으로 리턴한다.

  • NUMA 시스템에서 __GFP_THISNODE 플래그 비트가 있는 경우 오직 자신의 노드에 대한 zone 만 포함된 zonelists[1]
  • otherwise, 모든 노드에 대한 zone이 포함된 zonelists[0]

 

gfp_zonelist()

include/linux/gfp.h

/*
 * There is only one page-allocator function, and two main namespaces to
 * it. The alloc_page*() variants return 'struct page *' and as such
 * can allocate highmem pages, the *get*page*() variants return
 * virtual kernel addresses to the allocated page(s).
 */
static inline int gfp_zonelist(gfp_t flags)
{
#ifdef CONFIG_NUMA
        if (unlikely(flags & __GFP_THISNODE))
                return ZONELIST_NOFALLBACK;
#endif
        return ZONELIST_FALLBACK;
}

 

요청 플래그에 따른 zonelist 인덱스를 반환한다. ZONELIST_FALLBACK(0) 또는 ZONELIST_NOFALLBACK(1)

  • __GFP_THISNODE 플래그가 유무에 따라
    • 플래그가 있으면 zonelist의 fallback을 허용하지 않고 현재 노드에서만 할당하도록 1번 인덱스를 반환한다.
    • 플래그가 없으면 fallback 가능한 여러 노드에서 할당할 수 있게 0번 인덱스를 반환한다.

 

구조체

zonelist 구조체

include/linux/mmzone.h

/*
 * One allocation request operates on a zonelist. A zonelist
 * is a list of zones, the first one is the 'goal' of the
 * allocation, the other zones are fallback zones, in decreasing
 * priority.
 *
 * To speed the reading of the zonelist, the zonerefs contain the zone index
 * of the entry being read. Helper functions to access information given
 * a struct zoneref are
 *
 * zonelist_zone()      - Return the struct zone * for an entry in _zonerefs
 * zonelist_zone_idx()  - Return the index of the zone for an entry
 * zonelist_node_idx()  - Return the index of the node for an entry
 */
struct zonelist {
        struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
  • _zonerefs[]
    • 메모리 할당을 위해 fallback을 위한 zone들로 구성된다.

 

zoneref 구조체

include/linux/mmzone.h

/*
 * This struct contains information about a zone in a zonelist. It is stored
 * here to avoid dereferences into large structures and lookups of tables
 */
struct zoneref {
        struct zone *zone;      /* Pointer to actual zone */
        int zone_idx;           /* zone_idx(zoneref->zone) */
};
  • *zone
    • 존 포인터
  • zone_idx
    • 존에 대한 인덱스(0~3)

 

참고