setup_per_cpu_areas()

아래의 순서도는 Generic-SMP 기반의 setup_per_cpu_areas()를 사용하는 것을 기준으로 만들어졌다.

  • ARM은 UP-Generic 또는 SMP-Generic 코드를 사용한다.

setup_per_cpu_areas-1

setup_per_cpu_areas()의 3가지 구현 방법

setup_per_cpu_areas() 함수의 구현은 3가지로 구분된다.

  • UP Generic:
    • UP 시스템에서 동작하는 공통 setup_per_cpu_areas() 구현
  • SMP Generic:
    • SMP 시스템에서 동작하는 공통 setup_per_cpu_areas() 구현
    • rpi2가 사용하는 구현이다.
  • 아키텍처 고유 구현:
    • CONFIG_HAVE_SETUP_PER_CPU_AREA 커널 옵션을 사용하며 각 arch 디렉토리에 별도의 setup_per_cpu_areas() 함수를 제공한다.
    • ia64, sparc64, powerpc64, x86 아키텍처에서는 고유 구현을 사용한다.
    • 이렇게 별도의 고유 구현을 사용하는 이유는 아키텍처가 수천개의 cpu도 구성할  수 있고 , NUMA 설계 및 sparse 메모리도 사용하는 시스템이 있어 노드 구성에 따라 per-cpu 데이터를 할당하는 메모리 위치가 다르기 때문에 이를 지원하기 위함이다.

setup_per_cpu_areas-24

setup_per_cpu_areas() – UP Generic

mm/percpu.c

/*
 * UP percpu area setup.
 *
 * UP always uses km-based percpu allocator with identity mapping.
 * Static percpu variables are indistinguishable from the usual static
 * variables and don't require any special preparation.
 */
void __init setup_per_cpu_areas(void)
{
        const size_t unit_size =
                roundup_pow_of_two(max_t(size_t, PCPU_MIN_UNIT_SIZE,
                                         PERCPU_DYNAMIC_RESERVE));
        struct pcpu_alloc_info *ai; 
        void *fc; 

        ai = pcpu_alloc_alloc_info(1, 1);
        fc = memblock_virt_alloc_from_nopanic(unit_size,
                                              PAGE_SIZE,
                                              __pa(MAX_DMA_ADDRESS));
        if (!ai || !fc)
                panic("Failed to allocate memory for percpu areas.");
        /* kmemleak tracks the percpu allocations separately */
        kmemleak_free(fc);

        ai->dyn_size = unit_size;
        ai->unit_size = unit_size;
        ai->atom_size = unit_size;
        ai->alloc_size = unit_size;
        ai->groups[0].nr_units = 1;
        ai->groups[0].cpu_map[0] = 0;

        if (pcpu_setup_first_chunk(ai, fc) < 0)
                panic("Failed to initialize percpu areas.");
}
  • UP 시스템에서 per-cpu 데이터형은 큰 의미가 없지만 1개 그룹(노드) 및 1개 유닛으로 구성된 SMP generic 코드를 그대로 활용한다.
    • unit_size:
      • PCPU_MIN_UNIT_SIZE(32K)와 PERCPU_DYNAMIC_RESERVE와 비교하여 가장 큰 크기로 유닛 크기를 결정한다.
        • PERCPU_DYNAMIC_RESERVE는 아키텍처마다 다르다.
          • ARM 32bit=20K, ARM 64bit=28K
    • ai = pcpu_alloc_alloc_info(1, 1);
      • 인수로 그룹(노드) 수와 유닛 수를  대입하는데 UP 시스템이므로 두 인수가 모두 1로 호출하여 pcpu_alloc_info 구조체 등을 할당 받아 온다.
    • if (pcpu_setup_first_chunk(ai, fc) < 0)
      • first chunk를 구성한다.

 

setup_per_cpu_areas() – SMP Generic

다음은 SMP Generic 구현 기반의 setup_per_cpu_areas() 함수이다.

mm/percpu.c

/*
 * Generic SMP percpu area setup.
 *
 * The embedding helper is used because its behavior closely resembles
 * the original non-dynamic generic percpu area setup.  This is
 * important because many archs have addressing restrictions and might
 * fail if the percpu area is located far away from the previous
 * location.  As an added bonus, in non-NUMA cases, embedding is
 * generally a good idea TLB-wise because percpu area can piggy back
 * on the physical linear memory mapping which uses large page
 * mappings on applicable archs.
 */
void __init setup_per_cpu_areas(void)
{
        unsigned long delta;
        unsigned int cpu; 
        int rc;

        /*
         * Always reserve area for module percpu variables.  That's
         * what the legacy allocator did.
         */
        rc = pcpu_embed_first_chunk(PERCPU_MODULE_RESERVE,
                                    PERCPU_DYNAMIC_RESERVE, PAGE_SIZE, NULL,
                                    pcpu_dfl_fc_alloc, pcpu_dfl_fc_free);
        if (rc < 0) 
                panic("Failed to initialize percpu areas.");

        delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start;
        for_each_possible_cpu(cpu)
                __per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu];
}
  • 이 함수는 per-cpu 데이터를 사용할 수 있도록 준비한다.
  •  pcpu_embed_first_chunk()
    • per-cpu 데이터 사용을 위해 first chunk를 구성한다.
    • ARM 아키텍처에서는 항상 이 함수를 호출하여 first chunk를 만든다.
    • 다른 몇 개 아키텍처에서는 옵션에 따라 pcpu_page_first_chunk()를 호출하여 사용할 수도 있다.
    •  PERCPU_MODULE_RESERVE
      • 모듈에서 사용하는 모든 DEFINE_PER_CPU를 커버할 수 있는 크기이다.
      • CONFIG_MODULES 옵션이 설정되어 있는 경우 8K이며 설정되어 있지 않으면 0이다.
    • PERCPU_DYNAMIC_RESERVE
      • first chunk에 dynamic per-cpu 할당을 할 수 있는 크기를 나타낸다.
      • 값을 설정할 때 1-2 페이지 정도의 여유가 남아 있을 수 있도록 설정한다.
      • default 값은 예전 보다 용량이 조금 더 증가되었고 다음과 같다.
        • 32bit 시스템의 경우 20K
        • 64bit 시스템의 경우 28K
  • delta 보정
    • pcpu_base_addr와 __per_cpu_start가 다를 수 있으며 이 때 delta가 발생한다.
    • __per_cpu_offset[] 값에 delta를 모두 적용한다.

 

pcpu_embed_first_chunk()

이 함수에서는first chunk를 생성 시 large 페이지를 사용할 수 있는 embed 방식으로 구성한다. 이 때 구성된 정보를 가지고 추가 chunk를 만들 때 사용한다.

 

  • 초기에 설계될 때에는 bootmem에 만들었었는데 지금은 memblock 이라는 early memory allocator에 할당을 한다.
  • @reserved_size:
    • 모듈 용도의 static per-cpu로 할당할 reserved area 크기를 지정한다.
    • ARM에서는 CONFIG_MODULE이 설정되어 있으면 PERCPU_MODULE_REWSERVE(8K)의 크기로 만들고, 그렇지 않으면 0이 지정된다.
  • @dyn_size:
    • cpu-per 데이터를 다이나믹하게 할당할 수 있도록 dynamic area 크기를 지정한다.
    • ARM에서는 PERCPU_DYNAMIC_RESERVER(32bit=20K, 64bit=28K)만큼 만든다.
  • @atom_size:
    • 할당 최소 사이즈로 주어진 atom_size에 대해 2의 n차수로 할당 영역이 만들어진다.
    • ARM에서는  PAGE_SIZE(4K)를 사용한다.
  • @cpu_distance_fn:
    • CPU간 distance를 알아오는 함수 포인터로 NUMA 아키텍처를 지원해야하는 경우 사용된다.
    • NUMA 아키텍처에서는 노드 서로간에 메모리 접근 시간이 다르다.
    • 구현된 NUMA 시스템마다 노드와 CPU간의 구성이 복잡하므로 NUMA 시스템을 제공하는 업체에서 이 함수를 제공한다.
    • NUMA 아키텍처 구현에 따라 CPU간에 LOCAL_DISTANCE(10), REMOTE_DISTANCE(20)등 distance 값을 알아오게 된다.
    • 이 함수에서는 이렇게 알아오는 distance 값으로 그룹(노드)을 구분하는데 사용한다.
    • ARM에서는 null 이다.
  • @alloc_fn:
    • per-cpu 페이지를 할당할 함수를 지정한다.
    • null을 사용하면 vmalloc 영역을 사용한다.
  • @free_fn:
    • per-cpu 페이지를 회수(free)할 함수를 지정한다.
    • null을 함수하면 vmalloc 영역에서 회수(free)한다.

mm/percpu.c

#if defined(BUILD_EMBED_FIRST_CHUNK)
/**
 * pcpu_embed_first_chunk - embed the first percpu chunk into bootmem
 * @reserved_size: the size of reserved percpu area in bytes
 * @dyn_size: minimum free size for dynamic allocation in bytes
 * @atom_size: allocation atom size
 * @cpu_distance_fn: callback to determine distance between cpus, optional
 * @alloc_fn: function to allocate percpu page
 * @free_fn: function to free percpu page
 *
 * This is a helper to ease setting up embedded first percpu chunk and
 * can be called where pcpu_setup_first_chunk() is expected.
 *
 * If this function is used to setup the first chunk, it is allocated
 * by calling @alloc_fn and used as-is without being mapped into
 * vmalloc area.  Allocations are always whole multiples of @atom_size
 * aligned to @atom_size.
 *
 * This enables the first chunk to piggy back on the linear physical
 * mapping which often uses larger page size.  Please note that this
 * can result in very sparse cpu->unit mapping on NUMA machines thus
 * requiring large vmalloc address space.  Don't use this allocator if
 * vmalloc space is not orders of magnitude larger than distances
 * between node memory addresses (ie. 32bit NUMA machines).
 *
 * @dyn_size specifies the minimum dynamic area size.
 *
 * If the needed size is smaller than the minimum or specified unit
 * size, the leftover is returned using @free_fn.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
int __init pcpu_embed_first_chunk(size_t reserved_size, size_t dyn_size,
                                  size_t atom_size,
                                  pcpu_fc_cpu_distance_fn_t cpu_distance_fn,
                                  pcpu_fc_alloc_fn_t alloc_fn,
                                  pcpu_fc_free_fn_t free_fn)
{
        void *base = (void *)ULONG_MAX;
        void **areas = NULL;
        struct pcpu_alloc_info *ai; 
        size_t size_sum, areas_size, max_distance;
        int group, i, rc;

        ai = pcpu_build_alloc_info(reserved_size, dyn_size, atom_size,
                                   cpu_distance_fn);
        if (IS_ERR(ai))
                return PTR_ERR(ai);

        size_sum = ai->static_size + ai->reserved_size + ai->dyn_size;
        areas_size = PFN_ALIGN(ai->nr_groups * sizeof(void *)); 

        areas = memblock_virt_alloc_nopanic(areas_size, 0);
        if (!areas) {
                rc = -ENOMEM;
                goto out_free;
        }

        /* allocate, copy and determine base address */
        for (group = 0; group < ai->nr_groups; group++) {
                struct pcpu_group_info *gi = &ai->groups[group];
                unsigned int cpu = NR_CPUS;
                void *ptr;

                for (i = 0; i < gi->nr_units && cpu == NR_CPUS; i++)
                        cpu = gi->cpu_map[i];
                BUG_ON(cpu == NR_CPUS);

                /* allocate space for the whole group */
                ptr = alloc_fn(cpu, gi->nr_units * ai->unit_size, atom_size);
                if (!ptr) {
                        rc = -ENOMEM;
                        goto out_free_areas;
                }
                /* kmemleak tracks the percpu allocations separately */
                kmemleak_free(ptr);
                areas[group] = ptr;

                base = min(ptr, base);
        }
  • per-cpu 데이터 영역 할당에 필요한 구성정보를 준비한다.
    •  pcpu_build_alloc_info()
      • 주어진 인수 값을 가지고 ai(pcpu_alloc_info 구조체) 값을 구성하고 만들어 온다.
        • pcpu_alloc_info 구조체 멤버 전체가 설정되어 리턴된다.
        • pcpu_alloc_info 구조체 안에 있는 pcpu_group_info 구조체 정보도 모두 설정되며 cpu_map[]도 구성되어 있다.
    • size_sum = ai->static_size + ai->reserved_size + ai->dyn_size;
      • size_sum은 unit_size와 다를 수 있다.
        • ARM 등에서 atom_size가 4K page를 사용할 때에는 unit_size와 size_sum이 동일 하다.
        • 다른 아키텍처에서 atom_size가 1M, 2M, 16M 등을 사용하는 경우 unit_size는 보통 size_sum 보다 크다.
      • rpi2: size_sum = 0xb000 = 0x3ec0 + 0x2000 + 0x5140
        • dyn_size의 0x5140에서 0x140 은 size_sum의 page align 수행에 의해 추가되었다.
  • 그룹(노드)별로 per-cpu 데이터가 위치할 영역의 주소는 areas[] 라는 배열에서 관리되는데 이 배열을 memblock에 할당한다.
    • areas_size = PFN_ALIGN(ai->nr_groups * sizeof(void *));
      • 그룹(노드) 수 만큼의 주소(void *) 크기만큼 필요한 크기이며 PFN_ALIGN한 사이즈이다.
    • areas = memblock_virt_alloc_nopanic(areas_size, 0);
      • 위의 areas_size만큼 memblock 에 할당한다.
  • 그룹 수만큼 루프를 돌며 각 그룹의 유닛 수(ai->nr_units) x 유닛 사이즈(unit_size) 만큼 per-cpu 데이터 영역을 memblock에 할당한다.
    • 처음에 만드는 per-cpu 영역은 버디 시스템등 정식 메모리 관리자가 동작하기 전에는 memblock을 통해 즉 각 그룹(노드) 메모리에 할당을 하지 않고 lowmem 영역에 만든다.
      • NUMA 시스템에서는 각 노드의 lowmem에 할당한다.
      • 32bit NUMA 시스템의 경우 lowmem에 할당이 불가능한 경우 first chunk를 embed 방식을 사용하지 않고 paged 방식으로 생성하는 방법을 사용한다.
    • for (i = 0; i < gi->nr_units && cpu == NR_CPUS; i++)
      • 각 그룹의 유닛 수(gi->nr_units)만큼 루프를 도는데 매핑되어 있지 않은 경우(cpu =NR_CPUS) 루프 조건에 포함된다.
    • cpu = gi->cpu_map[i];
      • 이 cpu 번호가 그룹에서 unit->cpu 매핑으로 처음 접한 cpu 번호이다.
    • ptr = alloc_fn(cpu, gi->nr_units * ai->unit_size, atom_size);
      • 할당 사이즈(그룹에 포함된 유닛수 x 유닛 사이즈) 만큼 pcpu_dfl_fc_alloc() 함수를 통해 atom_size로 align하여 memblock에 할당한다.
    • kmemleak_free(ptr);
      • CONFIG_DEBUG_KMEMLEAK 커널 옵션이 설정된 경우 메모리 leak를 분석하기 위해 호출한다.
    • areas[group] = ptr;
      • areas[] 배열 영역에 해당 그룹(노드)의 per-cpu 데이터 영역으로 할당받은 base 주소를 기록한다.
    • base = min(ptr, base);
      • 각 그룹이 할당 받은 주소 중 가장 작은 주소를 기억한다.
      • base의 초기 값은 ULONG_MAX이므로 처음에 무조건 갱신된다.
        /*
         * Copy data and free unused parts.  This should happen after all
         * allocations are complete; otherwise, we may end up with
         * overlapping groups.
         */
        for (group = 0; group < ai->nr_groups; group++) {
                struct pcpu_group_info *gi = &ai->groups[group];
                void *ptr = areas[group];

                for (i = 0; i < gi->nr_units; i++, ptr += ai->unit_size) {
                        if (gi->cpu_map[i] == NR_CPUS) {
                                /* unused unit, free whole */
                                free_fn(ptr, ai->unit_size);
                                continue;
                        }
                        /* copy and return the unused part */
                        memcpy(ptr, __per_cpu_load, ai->static_size);
                        free_fn(ptr + size_sum, ai->unit_size - size_sum);
                }
        }
  • 그룹 수만큼 루프를 돌며 unit->cpu 매핑이 되지 않은 빈 공간을 memblock 할당에서 제거하고 Static area를 각 유닛 영역에 복사하며 유닛 내의 사용하지 않는 공간도 memblock 할당에서 제거한다. 단 small page를 사용하는 경우 불필요한 공간이 없으므로 제거할 영역은 없다.
    • void *ptr = areas[group];
      • 각 그룹의 영역 할당 주소를 ptr에 담는다.
    • if (gi->cpu_map[i] == NR_CPUS) {
      • unit->cpu 매핑이 되지 않은 유닛에 대한 공간을 제거하기 위한 조건이다.
    • free_fn(ptr, ai->unit_size);
      • 유닛에 해당하는 memblock 영역에서 유닛 사이즈만큼 할당 해제한다.
    •  memcpy(ptr, __per_cpu_load, ai->static_size);
      • __per_cpu_load에 위치한 per-cpu static 데이터를 해당 유닛에 대응하는 주소에 copy한다.
    • free_fn(ptr + size_sum, ai->unit_size – size_sum);
      • 해당 유닛내에서 복사한 사이즈를 제외한 공간 만큼의 사이즈를 memblock 영역에서 할당 해제한다.

setup_per_cpu_areas-10

        /* base address is now known, determine group base offsets */
        max_distance = 0;
        for (group = 0; group < ai->nr_groups; group++) {
                ai->groups[group].base_offset = areas[group] - base;
                max_distance = max_t(size_t, max_distance,
                                     ai->groups[group].base_offset);
        }
        max_distance += ai->unit_size;

        /* warn if maximum distance is further than 75% of vmalloc space */
        if (max_distance > VMALLOC_TOTAL * 3 / 4) {
                pr_warning("PERCPU: max_distance=0x%zx too large for vmalloc "
                           "space 0x%lx\n", max_distance,
                           VMALLOC_TOTAL);
#ifdef CONFIG_NEED_PER_CPU_PAGE_FIRST_CHUNK
                /* and fail if we have fallback */
                rc = -EINVAL;
                goto out_free;
#endif
        }

        pr_info("PERCPU: Embedded %zu pages/cpu @%p s%zu r%zu d%zu u%zu\n",
                PFN_DOWN(size_sum), base, ai->static_size, ai->reserved_size,
                ai->dyn_size, ai->unit_size);

        rc = pcpu_setup_first_chunk(ai, base);
        goto out_free;

out_free_areas:
        for (group = 0; group < ai->nr_groups; group++)
                if (areas[group])
                        free_fn(areas[group],
                                ai->groups[group].nr_units * ai->unit_size);
out_free:
        pcpu_free_alloc_info(ai);
        if (areas)
                memblock_free_early(__pa(areas), areas_size);
        return rc;
}
#endif /* BUILD_EMBED_FIRST_CHUNK */
  • max_distance를 계산하여 VMALLOC_TOTAL의 75%를 초과하는지 검사하고 초과하는 경우 경고를 출력한다.
    • CONFIG_NEED_PER_CPU_PAGE_FIRST_CHUNK 옵션도 설정되어 있는 다른 아키텍처에서는 이 함수가 실패하여 빠져나가게 되면 page 방식으로 다시 한 번 준비하게 된다.
    • max_distance는 모든 그룹의 base_offset 중 가장 큰 주소를 담고 여기에 유닛 사이즈 만큼 더한다.
    • 추가되는 chunk 들은 first chunk를 만들때의 그룹(노드) offset 만큼 간격을 유지하여 vmalloc 공간에 메모리를 할당하고 매핑을 해야하기 때문에 제약조건이 생기므로 vmalloc 공간의 최대 75%를 초과하는 경우 first chunk를 만들 때 embed 방식을 사용하지 못하게 한다.
    • embed 방식을 사용하는 경우 각 그룹 간의 간격을 유지하게 하기 위해 vmalloc 공간의 할당을 반대편에서 즉 위에서 아래로 빈 공간을 검색하여 사용하게 하여 추가되는 chunk 할당에 대해 실패 확률을 줄여주게 설계되었다.
  • rc = pcpu_setup_first_chunk(ai, base);
    • 이 함수에서 first chunk에 대한 관리 정보를 각종 전역변수에 설정하고 out_free: 레이블로 이동하여 임시로 만들었던 정보(ai)들을 해제(free)한다.
  • out_free_areas:
    • 그룹 수만큼 areas[]를 통해 per-cpu 데이터 영역을 할당한 곳의 주소를 알아오고 해당 영역을 memblock에서 할당 해제한다.
  • out_free:
    • per-cpu data를 만들때 사용한 관리정보를 모두 삭제한다.
      • pcpu_free_alloc_info()
        • pcpu_alloc_info와 관련된 관리 정보를 모두 memblock 영역에서 해제한다.
      • memblock_free_early(__pa(areas), areas_size);
        • areas 정보를 memblock 영역에서 할당 해제한다.

 

pcpu_build_alloc_info()

/* pcpu_build_alloc_info() is used by both embed and page first chunk */
#if defined(BUILD_EMBED_FIRST_CHUNK) || defined(BUILD_PAGE_FIRST_CHUNK)
/**
 * pcpu_build_alloc_info - build alloc_info considering distances between CPUs
 * @reserved_size: the size of reserved percpu area in bytes
 * @dyn_size: minimum free size for dynamic allocation in bytes
 * @atom_size: allocation atom size
 * @cpu_distance_fn: callback to determine distance between cpus, optional
 *
 * This function determines grouping of units, their mappings to cpus
 * and other parameters considering needed percpu size, allocation
 * atom size and distances between CPUs.
 *
 * Groups are always mutliples of atom size and CPUs which are of
 * LOCAL_DISTANCE both ways are grouped together and share space for
 * units in the same group.  The returned configuration is guaranteed
 * to have CPUs on different nodes on different groups and >=75% usage
 * of allocated virtual address space.
 *
 * RETURNS:
 * On success, pointer to the new allocation_info is returned.  On
 * failure, ERR_PTR value is returned.
 */
static struct pcpu_alloc_info * __init pcpu_build_alloc_info(
                                size_t reserved_size, size_t dyn_size,
                                size_t atom_size,
                                pcpu_fc_cpu_distance_fn_t cpu_distance_fn)
{
        static int group_map[NR_CPUS] __initdata;
        static int group_cnt[NR_CPUS] __initdata;
        const size_t static_size = __per_cpu_end - __per_cpu_start;
        int nr_groups = 1, nr_units = 0;
        size_t size_sum, min_unit_size, alloc_size;
        int upa, max_upa, uninitialized_var(best_upa);  /* units_per_alloc */
        int last_allocs, group, unit;
        unsigned int cpu, tcpu;
        struct pcpu_alloc_info *ai;
        unsigned int *cpu_map;

        /* this function may be called multiple times */
        memset(group_map, 0, sizeof(group_map));
        memset(group_cnt, 0, sizeof(group_cnt));

        /* calculate size_sum and ensure dyn_size is enough for early alloc */
        size_sum = PFN_ALIGN(static_size + reserved_size +
                            max_t(size_t, dyn_size, PERCPU_DYNAMIC_EARLY_SIZE));
        dyn_size = size_sum - static_size - reserved_size;
  • 처음 ARM 32비트 아키텍처 first chunk를 만들기 위해 호출된 경우 지정된 인수는 다음과 같다.
    • reserved_size=8K, dyn_size=20K, atom_size=4K
    • atom_size는 대부분 4K 페이지를 사용하여 할당 영역을 align되게 유도하는데 큰 페이지를 사용하는 몇 개의 아키텍처에서는 1M, 2M, 16M 등을 지원하기도 한다. (ARM은 4K)
  • const size_t static_size = __per_cpu_end – __per_cpu_start;
    • Per-cpu 데이터의 Static 선언자로 만들어지면 위의 영역에 들어가게 된다.
    • static_size: 해당 static 영역의 사이즈
      • rpi2(설정마다 약간씩 다름): static_size = 0x3ec0 bytes
  • size_sum:
    • static 영역 + reserved 영역 + dynamic 영역을 모두 더한 사이즈를 페이지 align한 사이즈
    • dyn_size는 최소 PERCPU_DYNAMIC_EARLY_SIZE(12K)이다.
    • 이 함수가 ARM에서 first chunk를 만들 때 호출되었으면 dynamic 사이즈는 32비트 시스템에서 20K이고, 64비트 시스템에서는 28K이다.
    • rpi2: size_sum = 0xb000 = PFN_ALIGN(0xaec0)
  • dyn_size = size_sum – static_size – reserved_size;
    • 처음 요청 받은 20K 사이즈 + static 영역을 align 하며 남은 자투리를 합한 값과 같다.
    • rpi2: dyn_size = 0x5140
  • uninitialized_var(best_upa);
    • 초기화 되지 않은 로컬 변수를 사용 시 compile warning을 없애기 위한 매크로이다.
    • #define uninitialized_var(x) x = x

setup_per_cpu_areas-3a

        /*
         * Determine min_unit_size, alloc_size and max_upa such that
         * alloc_size is multiple of atom_size and is the smallest
         * which can accommodate 4k aligned segments which are equal to
         * or larger than min_unit_size.
         */
        min_unit_size = max_t(size_t, size_sum, PCPU_MIN_UNIT_SIZE);

        alloc_size = roundup(min_unit_size, atom_size);
        upa = alloc_size / min_unit_size;
        while (alloc_size % upa || ((alloc_size / upa) & ~PAGE_MASK))
                upa--;
        max_upa = upa;
  • min_unit_size = max_t(size_t, size_sum, PCPU_MIN_UNIT_SIZE);
    • PCPU_MIN_UNIT_SIZE
      • 최소 유닛 사이즈로 32K이다.
    • rpi2: 예) min_unit_size = 0xb000 (size_sum이 32K 보다 크므로)
  • alloc_size = roundup(min_unit_size, atom_size);
    • 최소 유닛 사이즈를  atom_size(ARM: 4K) 단위로 roundup 한다.
    • roundup()에서 두 번째 인수가 scalar type이어야 한다.
    • rpi2: alloc_size = 0xb000
    • 다른 몇 개의 아키텍처에서 atom_size 1M, 2M, 16M를 지원하는 경우 alloc_size는 atom_size 단위로 만들어진다.
  • 할당 사이즈(alloc_size)가 계산되었으면 이 할당에 배치될 유닛 수를 계산해야 한다.
    • 계산해 내는 단계는 3단계로 구성한다.
      • 1단계) upa = 할당 사이즈 / 최소 할당할 유닛 사이즈(min_unit_size)
      • 2단계) max_upa = 나누어진 유닛 사이즈가 항상 4K 페이지에 대해 2의 n차수가 되게 조정
      • 3단계) best_upa = 그룹(노드)과 cpu의 갯 수에 따라 max_upa를 조정
        • 예) max_upa가 8인, 즉 한 할당에 8개의 유닛을 배치할 수 있는 상태이지만 group(노드) 수 가 4이고, 각 group의 cpu가 2개인 경우 각 그룹마다 할당을 각각해야 하므로 4개의 그룹마다 할당을 하나씩 만들고 그 할당 하나당 2개의 유닛을 배치하여 사용한다. (best_upa=2).
  • upa = alloc_size / min_unit_size;
    • 먼저 하나의 할당에 들어갈 수 있는 유닛 수를 계산한다.
    • upa(Unit Per Allocation) = 할당 사이즈를 / 최소 유닛 사이즈로 나눈다.
    • ARM과 같이 atom_size가 작은 4K 인경우 alloc_size가 항상 4K 보다 같거나 큰 경우가 되므로 upa 값은 항상 1이다.
    • 다른 특별한 아키텍처에서는 atom_size를 1M, 2M, 및 16M 등으로 지정할 수도 있고 그런 경우 할당 하나에 여려 개의 유닛이 들어간다.
    • 예) atom_size가 2M인 아키텍처: upa=46
      • upa(46)=alloc_size(2M) / min_unit_size(44K)
    • 예) 아래 그림과  같이 하나의 할당을 여러 개의 페이지에 해야 하는 경우 upa=1 이다.
      • ARM: alloc_size=0xb000, max_upa=1, nr_cpu=4인 경우 계산되어지는 값들
        • upa=1 (upa가 1이면 max_upa, best_upa도 1이다)

setup_per_cpu_areas-20a

  • while (alloc_size % upa || ((alloc_size / upa) & ~PAGE_MASK))
    • max_upa를 계산한다. (upa보다 같거나 작아진다)
    •  ~PAGE_MASK
      • ARM 4K의 경우 0x0000_0FFF.
    • alloc_size가 4K에 대해 2의 n차수에서만 루프를 탈출할 기회가 있다.
  • max_upa:
    • 4K 페이지에 대해 2의 n차수로 시작하는 하나의 할당 크기에 최대 할당 가능한 유닛 수가 담긴다.
    • 즉 max_upa는 2^N 값이다.
      • 2^N, …, 2^6(64), 2^5(32), 2^4(16), 8(2^4), 4(2^2), 2(2^1), 1(2^0)
      • 예) alloc_size=2M, min_unit_size=0xb000 인경우 처음 upa=46
      • 최종 max_upa=32 (46, 44, 43, … 32)
        /* group cpus according to their proximity */
        for_each_possible_cpu(cpu) {
                group = 0;
        next_group:
                for_each_possible_cpu(tcpu) {
                        if (cpu == tcpu)
                                break;
                        if (group_map[tcpu] == group && cpu_distance_fn &&
                            (cpu_distance_fn(cpu, tcpu) > LOCAL_DISTANCE ||
                             cpu_distance_fn(tcpu, cpu) > LOCAL_DISTANCE)) {
                                group++;
                                nr_groups = max(nr_groups, group + 1);
                                goto next_group;
                        }
                }
                group_map[cpu] = group;
                group_cnt[group]++;
        }
  • group_map[]
    • ARM 같이 UMA 시스템에서는 group(노드)이 하나이므로 모두 0이다.
    • NUMA 시스템에서는 아래 그림과 같이 group(노드)끼리 묶어진다.
      • CPU간 distance 값이 LOCAL_DISTANCE(10)보다 큰 경우는 서로 다른 group(노드)이라 인식한다.
  • group_cnt[]
    • ARM 같이 UMA 시스템에서는 group(노드)이 하나이므로 group_cnt[0]에만 cpu 수가 들어간다.
    • NUMA 시스템에서는 아래 그림과 같이 각 group(노드)이 소유한 cpu 수가 들어간다. 아래 그림과 같이 두 개의 그룹에 각각 4개씩의 cpu 수가 입력되었다.
  • nr_groups
    • 그룹(노드) 수
  • 아래 그림은 각각의 그룹(노드)에 4개씩 cpu가 배치된 경우이다.

setup_per_cpu_areas-2a

  • 아래 그림은 첫 번째 그룹(노드)에 4개,  두 번째 그룹(노드)에 2개의 cpu가 배치된 경우이다.

setup_per_cpu_areas-18

        /*
         * Expand unit size until address space usage goes over 75%
         * and then as much as possible without using more address
         * space.
         */
        last_allocs = INT_MAX;
        for (upa = max_upa; upa; upa--) {
                int allocs = 0, wasted = 0;

                if (alloc_size % upa || ((alloc_size / upa) & ~PAGE_MASK))
                        continue;

                for (group = 0; group < nr_groups; group++) {
                        int this_allocs = DIV_ROUND_UP(group_cnt[group], upa);
                        allocs += this_allocs;
                        wasted += this_allocs * upa - group_cnt[group];
                }

                /*
                 * Don't accept if wastage is over 1/3.  The
                 * greater-than comparison ensures upa==1 always
                 * passes the following check.
                 */
                if (wasted > num_possible_cpus() / 3)
                        continue;

                /* and then don't consume more memory */
                if (allocs > last_allocs)
                        break;
                last_allocs = allocs;
                best_upa = upa;
        }
        upa = best_upa;
  • best_upa:
    • ARM과 4K 페이지를 사용하는 경우 하나의 할당에 1개의 유닛만 사용하게 되는데 이 경우에는 best_upa도 항상 1을 나타낸다.
    • NUMA 시스템에서는 여러개의 group(노드)으로 나뉘는데 이 때 메모리 할당이 group(노드) 수 만큼 나뉘어질 수도 있다. 이러한 경우 atom_size가 큰 페이지라 하더라도 최소한 group(노드) 수 만큼 할당을 해야 한다. 따라서 max_upa 값을 그대로 사용하지 않고 하나의 할당에 사용할 배치할 적절한 유닛 수를 계산해낸다.
      • 가능한 그룹(node)에 연결된 cpu 수 만큼 배치하려한다.
    • 예) alloc_size=2M, min_unit_size=0xb000, max_upa=32, nr_cpu=8, nr_group=2 인경우 best_upa=4 이다.
      •  for (upa = max_upa; upa; upa–) {
        • upa= 32~1까지 루프
      • if (alloc_size % upa || ((alloc_size / upa) & ~PAGE_MASK))
        • 페이지에 대해 2의 n차수가 아니면 continue
        • upa=32, 16, 8, 4, 2, 1이 아니면 continue
      • for (group = 0; group < nr_groups; group++) {
        • 2개 그룹만큼 루프(group=0, 1)
      • int this_allocs = DIV_ROUND_UP(group_cnt[group], upa);
        • this_allocs=(1, 1), (1, 1), (1, 1), (1, 1), (2, 2), (4, 4)
        • 괄호 () 안은 group 루프
      • allocs += this_allocs;
        • allocs= (1, 2), (1, 2), (1, 2), (1, 2), (2, 4), (4, 8)
        • 괄호 () 안은 group 루프
      • wasted += this_allocs * upa – group_cnt[group];
        • wasted = (28, 56), (12, 24), (4, 8), (0, 0), (0, 0), (0, 0)
      • if (wasted > num_possible_cpus() / 3)
        • wastage가 1/3을 초과하면 낭비가 많아서 포기.
          • wasted가 2(8/3의 몫)를 초과하면 continue
      • if (allocs > last_allocs)
        • allocs=2, last_allocs=1, best_upa=4

setup_per_cpu_areas-19

  • 예) alloc_size=2M, min_unit_size=0xb000, max_upa=32, nr_cpu=8, nr_group=1 인경우 best_upa=8 이다.
    • for (upa = max_upa; upa; upa–) {
      • upa= 32~1까지 루프
    • if (alloc_size % upa || ((alloc_size / upa) & ~PAGE_MASK))
      • 4K 페이지에 대해 2의 n차수가 아니면 continue
      • upa=32, 16, 8, 4, 2, 1이 아니면 continue
    • for (group = 0; group < nr_groups; group++) {
      • 1개 그룹만큼 루프(group=0)
    • int this_allocs = DIV_ROUND_UP(group_cnt[group], upa);
      • this_allocs=1, 1, 1, 2, 4, 8
    • allocs += this_allocs;
      • allocs= 1, 1, 1, 2, 4, 8
    • wasted += this_allocs * upa – group_cnt[group];
      • wasted = 24, 8, 0, 0, 0, 0
    • if (wasted > num_possible_cpus() / 3)
      • wastage가 1/3을 초과하면 낭비가 많아서 포기.
        • wasted가 2(8/3의 몫)를 초과하면 continue
    • if (allocs > last_allocs)
      • allocs=2, last_allocs=1, best_upa=8
  • 아래 표는 min_unit_size=44K 를 다양한 조건에서 best_upa값을 추적하였다.
    • X축: alloc_size
    • Y축: group별 cpu 수_nr_cpu, nr_group

setup_per_cpu_areas-4b

  • 아래 표 역시 min_unit_size=44K 를 다양한 조건에서 allocs, wasted 값들을 추적하였다.
    • X축: upa
    • Y축: group 루프
    • 그룹: cpu, group

setup_per_cpu_areas-5c

  • 1아래 표와 같이 NUMA 시스템에서 비대칭으로 동작하는 상황을 2 가지 사례로 체크해 보았다.
    • 1) group-0에는 4개의 cpu, group-1에는 2개의 cpu, 그리고 max_upa=32일 때
      • allocs=2, best_upa=4
        • 할당은 2개, 하나의 할당 공간에 유닛을 4개.
      • 그룹당 최대 cpu 수만큼 4개의 best_upa가 나온 사례이다.
    • 2) group-0에는 8개의 cpu, group-1에는 1개의 cpu, 그리고 max_upa=32일 때
      • allocs=3, best_upa=4
        • 할당은 3개, 하나에 할당 공간에 유닛을 4개.
      • 보통은 그룹당 최대 cpu 수만큼 8개의 best_upa가 나오는데 이런 경우 1개의 cpu가 배정받는 할당 영역이 너무 많이 남는 공간이 생기므로 낭비가 생기지 않을 때 까지 할당 수를 더 늘리고 best_upa는 절반씩 줄여 나간다.
    • 비대칭 NUMA 노드가 unit->cpu 매핑이 과정도 유의깊게 확인해야 한다.

setup_per_cpu_areas-23

        /* allocate and fill alloc_info */
        for (group = 0; group < nr_groups; group++)
                nr_units += roundup(group_cnt[group], upa);

        ai = pcpu_alloc_alloc_info(nr_groups, nr_units);
        if (!ai)
                return ERR_PTR(-ENOMEM);
        cpu_map = ai->groups[0].cpu_map;

        for (group = 0; group < nr_groups; group++) {
                ai->groups[group].cpu_map = cpu_map;
                cpu_map += roundup(group_cnt[group], upa);
        }

        ai->static_size = static_size;
        ai->reserved_size = reserved_size;
        ai->dyn_size = dyn_size;
        ai->unit_size = alloc_size / upa;
        ai->atom_size = atom_size;
        ai->alloc_size = alloc_size;

        for (group = 0, unit = 0; group_cnt[group]; group++) {
                struct pcpu_group_info *gi = &ai->groups[group];

                /*
                 * Initialize base_offset as if all groups are located
                 * back-to-back.  The caller should update this to
                 * reflect actual allocation.
                 */
                gi->base_offset = unit * ai->unit_size;

                for_each_possible_cpu(cpu)
                        if (group_map[cpu] == group)
                                gi->cpu_map[gi->nr_units++] = cpu;
                gi->nr_units = roundup(gi->nr_units, upa);
                unit += gi->nr_units;
        }
        BUG_ON(unit != nr_units);

        return ai;
}
  • nr_units
    • 만들게 될 총 유닛 수가 담긴다.
    • 그룹 수 만큼 루프를 돌며 그룹당 cpu 수를 upa 크기 단위로 자리 올림한 후 더한다.
    • 만일 NUMA 노드가 2개가 있고 비 대칭형태로 디자인되어 한 쪽 group(노드)은 8개의 cpu를 사용하였고, 다른 한 쪽 group(노드)이 1개의 cpu를 사용한 경우 upa(best_upa) 값이 4라고 가정할 때 총 nr_units 수는 24가 되어 nr_cpu(9)와 다름을 알 수 있다.

setup_per_cpu_areas-22

  • ai = pcpu_alloc_alloc_info(nr_groups, nr_units);
    • pcpu_alloc_info 구조체를 memblock 영역에 만들고 리턴한다.
      • pcpu_alloc_info 구조체 안에는 pcpu_group_info[] 구조체 배열과 그 안에 cpu_map[] 배열도 구성한다.
  • ai->groups[group].cpu_map:
    • 그룹별 cpu_map[] 배열에 각 그룹에 속한 cpu_map 영역을 지정한다.
    • cpu_map += roundup(group_cnt[group], upa);
      • 그룹에 속한 cpu  수를 upa 단위로 올림(roundup) 처리한다.
    • 예) nr_groups=4, NR_CPUS=16, nr_units=4일 경우다음과 같이 배당된다. 매핑 정보에는 NR_CPUS 값으로 초기화 되어 있는 상태이다.
      •  ai->groups[0].cpu_map[0~3] -> 0번 group의 unit->cpu 매핑 배열
      • ai->groups[1].cpu_map[0~3] -> 1번 group의 unit->cpu 매핑 배열
      • ai->groups[2].cpu_map[0~3] -> 2번 group의 unit->cpu  매핑 배열
      • ai->groups[3].cpu_map[0~3] -> 3번 group의 unit->cpu  매핑 배열

setup_per_cpu_areas-7a

  • ai(pcpu_alloc_info 구조체) 내부의 멤버 변수에 사이즈 등을 지정한다.
  • 그룹 수 만큼 루프를 돌며 아래 값들을 설정한다.
    • gi->base_offset:
      • 그룹별 base 주소
    • gi->cpu_map[]:
      • unit -> cpu 매핑 값
      • 매핑 되지 않은 값은 NR_CPUS 값이 들어간다.
    • gi->nr_units:
      • 그룹별 유닛 수 (비 대칭 NUMA 시스템에서 생길 수 있는 매핑이 되지 않는 빈 유닛도 포함된다)
  • 아래 그림은 NUMA 시스템이면서 큰 페이지 메모리를 지원하는 경우 per-cpu 데이터 할당을 각각의 노드 메모리에 나누어서 한 경우를 표현하였다.

setup_per_cpu_areas-8b

 

  • 아래 표는 2개의 노드를 가진 대칭형 NUMA 시스템에서 cpu_map[]을 배치한 모습이다.
    • 예) NR_CPUS=8, cpus={ 4, 4 }, max_upa=32
      • allocs(할당 수)=2, nr_units=8, best_upa=4

setup_per_cpu_areas-9

  • 아래 표와 그림은 2개의 노드를 가진 비대칭형 NUMA 시스템에서 cpu_map[]을 배치한 모습이다.
    • 예) NR_CPUS=9, cpus = { 8, 1 }, max_upa=32, best_upa=4
      • allocs(할당 수)=3, nr_units=12, best_upa=4

setup_per_cpu_areas-22a

pcpu_alloc_alloc_info()

/**
 * pcpu_alloc_alloc_info - allocate percpu allocation info
 * @nr_groups: the number of groups
 * @nr_units: the number of units
 *
 * Allocate ai which is large enough for @nr_groups groups containing
 * @nr_units units.  The returned ai's groups[0].cpu_map points to the
 * cpu_map array which is long enough for @nr_units and filled with
 * NR_CPUS.  It's the caller's responsibility to initialize cpu_map
 * pointer of other groups.
 *
 * RETURNS:
 * Pointer to the allocated pcpu_alloc_info on success, NULL on
 * failure.
 */
struct pcpu_alloc_info * __init pcpu_alloc_alloc_info(int nr_groups,
                                                      int nr_units)
{
        struct pcpu_alloc_info *ai;
        size_t base_size, ai_size;
        void *ptr;
        int unit;

        base_size = ALIGN(sizeof(*ai) + nr_groups * sizeof(ai->groups[0]),
                          __alignof__(ai->groups[0].cpu_map[0]));
        ai_size = base_size + nr_units * sizeof(ai->groups[0].cpu_map[0]);

        ptr = memblock_virt_alloc_nopanic(PFN_ALIGN(ai_size), 0);
        if (!ptr)
                return NULL;
        ai = ptr;
        ptr += base_size;

        ai->groups[0].cpu_map = ptr;

        for (unit = 0; unit < nr_units; unit++)
                ai->groups[0].cpu_map[unit] = NR_CPUS;

        ai->nr_groups = nr_groups;
        ai->__ai_size = PFN_ALIGN(ai_size);

        return ai;
}
  • ALIGN()
    • #define ALIGN(x, a)             __ALIGN_KERNEL((x), (a))
      • #define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (typeof(x))(a) – 1)
        • #define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask))
    • x값을 a값 단위로 align한다.
      • 예) ALIGN(7, 4) = 8
  •  __alignof__
    • 인수에 타입을 지정하면 align에 필요한 bytes 수를 리턴한다.
    • 만일 lvalue를 지정한 경우에는 sizeof()와 동일하다.
    • __alignof__(ai->groups[0].cpu_map[0])
      • sizeof(unsigned int)와 같다.
  •  base_size:
    • pcpu_alloc_info 구조체 하나의 사이즈 + nr_groups 만큼의 pcpu_group_info 구조체 사이즈를  ai->groups[0].cpu_map[0] 의 타입인 unsigned int의 크기로 align한다.
  • ai_size:
    • base_size에 cpu_map[nr_units] 사이즈를 더한다.
  • ptr = memblock_virt_alloc_nopanic(PFN_ALIGN(ai_size), 0);
    • ai_size를 페이지 크기로 align하여 memblock에 할당 한 주소를 ptr에 대입한다.
  • ai->groups[0].cpu_map = ptr;
    • 첫 번째 그룹에만 cpu_map[] 공간을 연결한다.
  • nr_units 수만큼 ai->groups[0].cpu_map[] 각 항목에 NR_CPUS 값으로 초기화한다.
  • ai->nr_groups에는 인수 지정한 nr_groups 값을 집어 넣는다.
  • ai->__ai_size에는 할당 공간 사이즈를 집어넣는다.

setup_per_cpu_areas-6

 

pcpu_dfl_fc_alloc()

static void * __init pcpu_dfl_fc_alloc(unsigned int cpu, size_t size,
                                       size_t align)
{
        return  memblock_virt_alloc_from_nopanic(
                        size, align, __pa(MAX_DMA_ADDRESS));
}

 

pcpu_dfl_fc_free()

static void __init pcpu_dfl_fc_free(void *ptr, size_t size)
{
        memblock_free_early(__pa(ptr), size);
}

 

pcpu_setup_first_chunk()

per-cpu 데이터의 first chunk를 초기화한다.

  • @ai: 이 곳에 초기화는데 사용할 모든 정보가 담겨있다.
  • @base_addr: 매핑할 주소
  • 이 함수를 진입하기 전에 이미 per-cpu 영역의 메모리는 할당되어 있고 이 함수에서는 매핑을 관리할 구조체가 준비된다.
  • 조건에 따라 한 개 또는 두 개의 매핑 매커니즘이 구성된다.
    • reserved(module) 영역이 지정되지 않는 경우 하나의 매핑을 준비한다.
      • schunk: static + dynamic 영역을 구성하고 first chunk가 된다.
      • dchunk: 구성하지 않는다.
    • reserved(module) 영역이 지정된 경우는 두 개의 매핑을 준비한다.
      • schunk: static + reserved 영역을 구성하여 reserved chunk가 된다.
      • dchunk:  dynamic 영역을 구성하여 first chunk가 된다.

setup_per_cpu_areas-11a

/**
 * pcpu_setup_first_chunk - initialize the first percpu chunk
 * @ai: pcpu_alloc_info describing how to percpu area is shaped
 * @base_addr: mapped address
 *
 * Initialize the first percpu chunk which contains the kernel static
 * perpcu area.  This function is to be called from arch percpu area
 * setup path.
 *
 * @ai contains all information necessary to initialize the first
 * chunk and prime the dynamic percpu allocator.
 *
 * @ai->static_size is the size of static percpu area.
 *
 * @ai->reserved_size, if non-zero, specifies the amount of bytes to
 * reserve after the static area in the first chunk.  This reserves
 * the first chunk such that it's available only through reserved
 * percpu allocation.  This is primarily used to serve module percpu
 * static areas on architectures where the addressing model has
 * limited offset range for symbol relocations to guarantee module
 * percpu symbols fall inside the relocatable range.
 *
 * @ai->dyn_size determines the number of bytes available for dynamic
 * allocation in the first chunk.  The area between @ai->static_size +
 * @ai->reserved_size + @ai->dyn_size and @ai->unit_size is unused.
 *
 * @ai->unit_size specifies unit size and must be aligned to PAGE_SIZE
 * and equal to or larger than @ai->static_size + @ai->reserved_size +
 * @ai->dyn_size.
 *
 * @ai->atom_size is the allocation atom size and used as alignment
 * for vm areas.
 *
 * @ai->alloc_size is the allocation size and always multiple of
 * @ai->atom_size.  This is larger than @ai->atom_size if
 * @ai->unit_size is larger than @ai->atom_size.
 *
 * @ai->nr_groups and @ai->groups describe virtual memory layout of
 * percpu areas.  Units which should be colocated are put into the
 * same group.  Dynamic VM areas will be allocated according to these
 * groupings.  If @ai->nr_groups is zero, a single group containing
 * all units is assumed.
 *
 * The caller should have mapped the first chunk at @base_addr and
 * copied static data to each unit.
 *
 * If the first chunk ends up with both reserved and dynamic areas, it
 * is served by two chunks - one to serve the core static and reserved
 * areas and the other for the dynamic area.  They share the same vm
 * and page map but uses different area allocation map to stay away
 * from each other.  The latter chunk is circulated in the chunk slots
 * and available for dynamic allocation like any other chunks.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
int __init pcpu_setup_first_chunk(const struct pcpu_alloc_info *ai,
                                  void *base_addr)
{
        static int smap[PERCPU_DYNAMIC_EARLY_SLOTS] __initdata;
        static int dmap[PERCPU_DYNAMIC_EARLY_SLOTS] __initdata;
        size_t dyn_size = ai->dyn_size;
        size_t size_sum = ai->static_size + ai->reserved_size + dyn_size;
        struct pcpu_chunk *schunk, *dchunk = NULL;
        unsigned long *group_offsets;
        size_t *group_sizes;
        unsigned long *unit_off;
        unsigned int cpu;
        int *unit_map;
        int group, unit, i;

#define PCPU_SETUP_BUG_ON(cond) do {                                    \
        if (unlikely(cond)) {                                           \
                pr_emerg("PERCPU: failed to initialize, %s", #cond);    \
                pr_emerg("PERCPU: cpu_possible_mask=%*pb\n",            \
                         cpumask_pr_args(cpu_possible_mask));           \
                pcpu_dump_alloc_info(KERN_EMERG, ai);                   \
                BUG();                                                  \
        }                                                               \
} while (0)
  • PERCPU_DYNAMIC_EARLY_SLOTS:
    • 초기에 만들어지는 map의 수는 128개이고 pcpu_need_to_extend() 함수를 사용하여 증가시킬 필요가 있을 때 pcpu_extend_area_map() 함수를 호출하여 map 관리 수를 늘릴 수 있다.
    • 빈 공간의 margin 이 부족해질 때 맵을 증가시킨다.
        /* sanity checks */
        PCPU_SETUP_BUG_ON(ai->nr_groups <= 0);
#ifdef CONFIG_SMP
        PCPU_SETUP_BUG_ON(!ai->static_size);
        PCPU_SETUP_BUG_ON((unsigned long)__per_cpu_start & ~PAGE_MASK);
#endif
        PCPU_SETUP_BUG_ON(!base_addr);
        PCPU_SETUP_BUG_ON((unsigned long)base_addr & ~PAGE_MASK);
        PCPU_SETUP_BUG_ON(ai->unit_size < size_sum);
        PCPU_SETUP_BUG_ON(ai->unit_size & ~PAGE_MASK);
        PCPU_SETUP_BUG_ON(ai->unit_size < PCPU_MIN_UNIT_SIZE);
        PCPU_SETUP_BUG_ON(ai->dyn_size < PERCPU_DYNAMIC_EARLY_SIZE);
        PCPU_SETUP_BUG_ON(pcpu_verify_alloc_info(ai) < 0);

 

        /* process group information and build config tables accordingly */
        group_offsets = memblock_virt_alloc(ai->nr_groups *
                                             sizeof(group_offsets[0]), 0);
        group_sizes = memblock_virt_alloc(ai->nr_groups *
                                           sizeof(group_sizes[0]), 0);
        unit_map = memblock_virt_alloc(nr_cpu_ids * sizeof(unit_map[0]), 0);
        unit_off = memblock_virt_alloc(nr_cpu_ids * sizeof(unit_off[0]), 0);

        for (cpu = 0; cpu < nr_cpu_ids; cpu++)
                unit_map[cpu] = UINT_MAX;

        pcpu_low_unit_cpu = NR_CPUS;
        pcpu_high_unit_cpu = NR_CPUS;

        for (group = 0, unit = 0; group < ai->nr_groups; group++, unit += i) {
                const struct pcpu_group_info *gi = &ai->groups[group];

                group_offsets[group] = gi->base_offset;
                group_sizes[group] = gi->nr_units * ai->unit_size;

                for (i = 0; i < gi->nr_units; i++) {
                        cpu = gi->cpu_map[i];
                        if (cpu == NR_CPUS)
                                continue;

                        PCPU_SETUP_BUG_ON(cpu >= nr_cpu_ids);
                        PCPU_SETUP_BUG_ON(!cpu_possible(cpu));
                        PCPU_SETUP_BUG_ON(unit_map[cpu] != UINT_MAX);

                        unit_map[cpu] = unit + i;
                        unit_off[cpu] = gi->base_offset + i * ai->unit_size;

                        /* determine low/high unit_cpu */
                        if (pcpu_low_unit_cpu == NR_CPUS ||
                            unit_off[cpu] < unit_off[pcpu_low_unit_cpu])
                                pcpu_low_unit_cpu = cpu;
                        if (pcpu_high_unit_cpu == NR_CPUS ||
                            unit_off[cpu] > unit_off[pcpu_high_unit_cpu])
                                pcpu_high_unit_cpu = cpu;
                }
        }
        pcpu_nr_units = unit;

        for_each_possible_cpu(cpu)
                PCPU_SETUP_BUG_ON(unit_map[cpu] == UINT_MAX);
  • 매핑에 필요한 관리 데이터를 memblock에 할당하고 초기화한다.
    •  group_offsets[]:
      • 그룹 수 만큼 group_offsets의 배열을 memblock에 할당하여 offset 데이터를 관리한다.
    • group_sizes[]:
      • 그룹 수 만큼 group_sizes의 배열을 memblock에 할당하여 offset 데이터를 관리한다.
    • unit_map[]:
      • nr_cpu_ids: possible cpu 즉 커널에 설정한 최대 지원 cpu 수
      • nr_cpu_ids 만큼의 unit_map 배열을 memblock에 할당하여 cpu->unit 매핑 데이터를 관리한다. (각 그룹의 cpu_map과 반대 방향)
    • unit_off[]:
      • nr_cpu_ids 만큼의 unit_off 배열을 memblock에 할당하여 각 unit에 대한 offset 데이터를 관리한다
  • 매핑 구성 정보를 설정한다.
    • group_offsets[group] = gi->base_offset;
      • 기존 그룹 정보에 있는 base_offset 값을 group_offsets[] 배열에 저장한다.
    • group_sizes[group] = gi->nr_units * ai->unit_size;
      • 기존 그룹 정보에 있는 nr_units 수 만큼 유닛 사이즈를 곱한 값을 group_sizes[] 배열에 저장한다.
    • cpu = gi->cpu_map[i];
      • 그룹의 nr_units 수 만큼 unit ->cpu 매핑을 해서 cpu 번호를 알아온다.
      • if (cpu == NR_CPUS)
        • 사용하지 않는 매핑은 생략하고 다음 cpu 루프를 진행한다.
      • unit_map[cpu] = unit + i;
        • cpu -> unit 매핑한다.
      • unit_off[cpu] = gi->base_offset + i * ai->unit_size;
        • cpu 별 접근 시 필요한 offset 값을 설정한다.
        • 그룹의 base_offset + 해당 유닛 사이즈만큼 더한 위치
      • pcpu_low_unit_cpu:
        • 모든 cpu 중에서 가장 낮은 base 주소를 저장한다.
      • pcpu_high_unit_cpu:
        • 모든 cpu 중에서 가장 높은 base 주소를 저장한다.
        /* we're done parsing the input, undefine BUG macro and dump config */
#undef PCPU_SETUP_BUG_ON
        pcpu_dump_alloc_info(KERN_DEBUG, ai);

        pcpu_nr_groups = ai->nr_groups;
        pcpu_group_offsets = group_offsets;
        pcpu_group_sizes = group_sizes;
        pcpu_unit_map = unit_map;
        pcpu_unit_offsets = unit_off;

        /* determine basic parameters */
        pcpu_unit_pages = ai->unit_size >> PAGE_SHIFT;
        pcpu_unit_size = pcpu_unit_pages << PAGE_SHIFT;
        pcpu_atom_size = ai->atom_size;
        pcpu_chunk_struct_size = sizeof(struct pcpu_chunk) +
                BITS_TO_LONGS(pcpu_unit_pages) * sizeof(unsigned long);

        /*
         * Allocate chunk slots.  The additional last slot is for
         * empty chunks.
         */
        pcpu_nr_slots = __pcpu_size_to_slot(pcpu_unit_size) + 2;
        pcpu_slot = memblock_virt_alloc(
                        pcpu_nr_slots * sizeof(pcpu_slot[0]), 0);
        for (i = 0; i < pcpu_nr_slots; i++)
                INIT_LIST_HEAD(&pcpu_slot[i]);
  • pcpu_로 시작하는 전역 변수들 설정
    •  pcpu_nr_units:
      • 각 그룹의 유닛 수를 더한 수
      • 비 대칭 NUMA 시스템의 경우 매핑되지 않은 유닛도 포함된다.
        • 예) cpus = { 8, 1 }의 경우 best_upa=4일 때 각 그룹의 nr_units 수는 8과 4이다.
    • PCPU_SETUP_BUG_ON(unit_map[cpu] == UINT_MAX);
      • 모든 cpu에 대해 매핑이 안되어 있으면 에러 출력
    • pcpu_dump_alloc_info()
      • 할당에 관련된 디버깅용 정보를 콘솔에 출력한다.
    • pcpu_nr_groups:
      • 전체 그룹 수
    • pcpu_group_offsets[]:
      • 전체 그룹만큼 offset이 담겨 있는 배열
    • pcpu_group_sizes[]:
      • 전체 그룹만큼 각 그룹에서 사용하는 유닛 수 x 유닛 사이즈가 담겨있다.
    • pcpu_unit_map[]:
      • cpu->unit에 대한 매핑이 있는 배열
    • pcpu_unit_offsets[]:
      • 각 cpu에 대한(배열 인덱스가 unit이 아니고 cpu) offset이 있는 배열
    • pcpu_chunk_struct_size:
      • pcpu_chunk 구조체 사이즈 + pcpu_unit_pages 의 비트맵 관리를 위한 수 x long 타입 사이즈
  • chunk 리스트를 관리하는 슬롯을 memblock 영역에 할당한다.
    • pcpu_nr_slots = __pcpu_size_to_slot(pcpu_unit_size) + 2;
      • __pcpu_size_to_slot()
        • fls() 참고: Bit operations | 문c
        • PCPU_SLOT_BASE_SHIFT=5
        • 예) size에 따른 slot 값
          • 32K  -> 13번 slot
          • 64K -> 12번 slot
          • 1M -> 8번 slot
          • 2M -> 7번 slot
      • 유닛 사이즈로 slot을 알아온다음 2를 더한다.
      • 마지막 슬롯은 빈 chunk를 위한 슬롯이다.
    • pcpu_slot[]
      • pcpu_nr_slots 수 만큼 list_head 구조체 크기를 memblock에 할당한다.
    • INIT_LIST_HEAD(&pcpu_slot[i]);
      • pcpu_nr_slots 수 만큼 pcpu_slot[] list 구조를 초기화한다.
        /*
         * Initialize static chunk.  If reserved_size is zero, the
         * static chunk covers static area + dynamic allocation area
         * in the first chunk.  If reserved_size is not zero, it
         * covers static area + reserved area (mostly used for module
         * static percpu allocation).
         */
        schunk = memblock_virt_alloc(pcpu_chunk_struct_size, 0);
        INIT_LIST_HEAD(&schunk->list);
        INIT_WORK(&schunk->map_extend_work, pcpu_map_extend_workfn);
        schunk->base_addr = base_addr;
        schunk->map = smap;
        schunk->map_alloc = ARRAY_SIZE(smap);
        schunk->immutable = true;
        bitmap_fill(schunk->populated, pcpu_unit_pages);
        schunk->nr_populated = pcpu_unit_pages;

        if (ai->reserved_size) {
                schunk->free_size = ai->reserved_size;
                pcpu_reserved_chunk = schunk;
                pcpu_reserved_chunk_limit = ai->static_size + ai->reserved_size;
        } else {
                schunk->free_size = dyn_size;
                dyn_size = 0;                   /* dynamic area covered */
        }
        schunk->contig_hint = schunk->free_size;

        schunk->map[0] = 1;
        schunk->map[1] = ai->static_size;
        schunk->map_used = 1;
        if (schunk->free_size)
                schunk->map[++schunk->map_used] = 1 | (ai->static_size + schunk->free_size);
        else
                schunk->map[1] |= 1;
  • 아래의 그림은 reserved(module) 영역을 요청하지 않은 경우에 할당된 map[]의 예이다.
    • rpi2: dyn_size=0x5140, static_size=0x3ec0
      • map 값의 lsb 1비트는 in-use(1=사용중)과 0비트는 free(0=사용 가능)의 표시이다.
      • 아래와 같이 0x0~0x3ec0 전 까지 in-use이며, 0x3ec0~0x9000까지 free 상태라고 매핑되었다.
    • first chunk에 schunk가 선택되었고, reserved chunk는 영역이 없어 null이 대입된다.
    • schunk->free_size는 dyn_size 와 같다.
    • first chunk를 사용하여 dynamic 할당의 매핑 관리가 이루어진다.

setup_per_cpu_areas-12

        /* init dynamic chunk if necessary */
        if (dyn_size) {
                dchunk = memblock_virt_alloc(pcpu_chunk_struct_size, 0);
                INIT_LIST_HEAD(&dchunk->list);
                INIT_WORK(&dchunk->map_extend_work, pcpu_map_extend_workfn);
                dchunk->base_addr = base_addr;
                dchunk->map = dmap;
                dchunk->map_alloc = ARRAY_SIZE(dmap);
                dchunk->immutable = true;
                bitmap_fill(dchunk->populated, pcpu_unit_pages);
                dchunk->nr_populated = pcpu_unit_pages;

                dchunk->contig_hint = dchunk->free_size = dyn_size;
                dchunk->map[0] = 1;
                dchunk->map[1] = pcpu_reserved_chunk_limit;
                dchunk->map[2] = (pcpu_reserved_chunk_limit + dchunk->free_size) | 1;
                dchunk->map_used = 2;
        }
  • 아래의 그림은 reserved(module) 영역을 요청한 경우 schunk와 dchunk의 두 개로 나누어 관리를 하며 그 에 따른 할당 map[]의 예이다.
    • rpi2: dyn_size=0x5140, static_size=0x3ec0, reserved=0x2000
      • schunk에서의 매핑은 0x0~0x3ec0 전까지 in-use이며, 0x3ec0~0x5ec0까지 free 상태라고 매핑되었다.
      • dchunk에서의 매핑은 0x0~0x5ec0 전까지 in-use이며, 0x5ec0~0xb000까지 free 상태라고 매핑되었다.
  • first chunk에 dynamic 영역에 대한 매핑 관리를 위해 dchunk가 선택되었고, reserved chunk는 schunk를 대입하여 관리한다.
    • schunk->free_size는 reserved_size 와 같다.
    • dchunk->free_size는 dyn_size와 같다.

setup_per_cpu_areas-13a

        /* link the first chunk in */
        pcpu_first_chunk = dchunk ?: schunk;
        pcpu_nr_empty_pop_pages +=
                pcpu_count_occupied_pages(pcpu_first_chunk, 1);
        pcpu_chunk_relocate(pcpu_first_chunk, -1);

        /* we're done */
        pcpu_base_addr = base_addr;
        return 0;
}
  • pcpu_first_chunk = dchunk ?: schunk;
    • pcpu_first_chink에는 dchunk를 대입하되 null인 경우 schunk를 대입한다.
  • pcpu_nr_empty_pop_pages:
    • 빈 populate 페이지의 수가 담기고 reserved chunk는 관여하지 않는다.
    • pcpu_count_occupied_pages()
      • first chunk에서 1번 인덱스 맵으로 사용중(점유된)인 페이지 수를 알아온다.
  • pcpu_chunk_relocate(pcpu_first_chunk, -1);
    • first chunk의 free_size로 슬롯 번호를 알아와서 해당 슬롯 리스트에 first chunk를 추가한다.

 

다음 그림은 per-cpu chunk들을 슬롯에서 관리하는 모습을 보여준다.

pcpu_setup_first_chunk-1a

 

pcpu_count_occupied_pages()

/**
 * pcpu_count_occupied_pages - count the number of pages an area occupies
 * @chunk: chunk of interest
 * @i: index of the area in question
 *
 * Count the number of pages chunk's @i'th area occupies.  When the area's
 * start and/or end address isn't aligned to page boundary, the straddled
 * page is included in the count iff the rest of the page is free.
 */
static int pcpu_count_occupied_pages(struct pcpu_chunk *chunk, int i)
{
        int off = chunk->map[i] & ~1;
        int end = chunk->map[i + 1] & ~1;

        if (!PAGE_ALIGNED(off) && i > 0) {
                int prev = chunk->map[i - 1];

                if (!(prev & 1) && prev <= round_down(off, PAGE_SIZE))
                        off = round_down(off, PAGE_SIZE);
        }

        if (!PAGE_ALIGNED(end) && i + 1 < chunk->map_used) {
                int next = chunk->map[i + 1];
                int nend = chunk->map[i + 2] & ~1;

                if (!(next & 1) && nend >= round_up(end, PAGE_SIZE))
                        end = round_up(end, PAGE_SIZE);
        }

        return max_t(int, PFN_DOWN(end) - PFN_UP(off), 0);
}
  • off:
    • 지정된 인덱스의 값으로 시작 offset
  • end:
    • 지정된 인덱스+1의 값으로 끝 offset
  • 아래 그림은 index를 1로 주어 off와 end 값을 알아본 사례이다.

setup_per_cpu_areas-14

  • if (!PAGE_ALIGNED(off) && i > 0) {
    • 인덱스 i가 0보다 크면서 off 값이 PAGE align이 안되어 있는 경우
  • if (!(prev & 1) && prev <= round_down(off, PAGE_SIZE))
    • 바로 아래 index 영역이 in-use(1=사용중) 상태가 아니면서 off가 그 전 영역의 offset 보다 크거나 같으면 off를 페이지 사이즈로 round up 한다.
  • if (!(next & 1) && nend >= round_up(end, PAGE_SIZE))
    • 위 index 영역이 in-use(1=사용중) 상태가 아니면서 end가 그 다음 영역의 끝보다 작거나 같으면 end를 페이지 사이즈로 round up한다.
  • return max_t(int, PFN_DOWN(end) – PFN_UP(off), 0);
    • off부터 end까지의 페이지 수를 리턴한다.
    • 영역이 페이지에 겹치는 경우는 겹친 이웃 영역이 사용 가능한 상태에서는 포함되고 그렇지 않으면 겹친 영역은 카운트에서 배제한다.
    • rpi2: schunk→map[] = { 1, 0x3ec0, 0x9001 } 사례에서 index=1로 주면
      • return되는 값은 중복된 off 영역을 제외하고 5이다.
  • 아래의 또하나의 그림은 인덱스로 지정한 영역이 페이지에 겹친 경우 주위 이웃 영역 상태에 대응하여 1또는 3이 리턴되는 사례를 보여준다.

setup_per_cpu_areas-15

 

pcpu_chunk_relocate()

/**
 * pcpu_chunk_relocate - put chunk in the appropriate chunk slot
 * @chunk: chunk of interest
 * @oslot: the previous slot it was on
 *
 * This function is called after an allocation or free changed @chunk.
 * New slot according to the changed state is determined and @chunk is
 * moved to the slot.  Note that the reserved chunk is never put on
 * chunk slots.
 *
 * CONTEXT:
 * pcpu_lock.
 */
static void pcpu_chunk_relocate(struct pcpu_chunk *chunk, int oslot)
{
        int nslot = pcpu_chunk_slot(chunk);

        if (chunk != pcpu_reserved_chunk && oslot != nslot) {
                if (oslot < nslot)
                        list_move(&chunk->list, &pcpu_slot[nslot]);
                else
                        list_move_tail(&chunk->list, &pcpu_slot[nslot]);
        }
}
  • int nslot = pcpu_chunk_slot(chunk);
    • chunk내의 사이즈에 해당하는 nslot(계산된 슬롯 번호)를 알아온다.
      • chunk내에 sizeof(int)만큼의 사이즈도 할당할 공간이 없으면 nslot은 0이 된다.
      • rpi2: chunk->free_size=0x5140이라 할 경우 nslot=12이다.
    • pcpu_size_to_slot()의 결과는 항상 1이상이다.
      • 인수로 0~0xF까지 slot 번호는 1이다.
  • if (chunk != pcpu_reserved_chunk && oslot != nslot) {
    • 요청 chunk가 reserved chunk도 아니면서 인수로 요청한 oslot이 계산된 nslot과 다른 경우
    • first chunk를 만들면서 이 함수에 진입한 경우 chunk가 first chunk이고, oslot=-1이므로 항상 조건 성립한다.
  • list_move(&chunk->list, &pcpu_slot[nslot]);
    • 현재 chunk를 기존 pcpu_slot 리스트에서 제거하고 새로운 의 pcpu_slot 리스트의 앞에 추가한다.
  • list_move_tail(&chunk->list, &pcpu_slot[nslot]);
    • 현재 chunk를 기존 pcpu_slot 리스트에서 제거하고 새로운 의 pcpu_slot 리스트의 뒤에 추가한다.
  • 아래 그림은 chunk의 사이즈별 pcpu_slot 리스트에서 관리를 하는 위치를 나타낸다.
    • pcpu_slot 배열은 pcpu_unit_size보다 2가 크게 만들어져 있다.
    • pcpu_chunk_relocate() 함수를 사용해서 chunk를 relocation 시킬 수 있다.
    • rpi2: first chunk의 free_size가 0x5140일 때 nslot=12이므로 first chunk는 pcpu_slot[12] 리스트에 처음 추가된다.

setup_per_cpu_areas-16a

 

pcpu_free_alloc_info()

/**
 * pcpu_free_alloc_info - free percpu allocation info
 * @ai: pcpu_alloc_info to free
 *
 * Free @ai which was allocated by pcpu_alloc_alloc_info().
 */
void __init pcpu_free_alloc_info(struct pcpu_alloc_info *ai)
{
        memblock_free_early(__pa(ai), ai->__ai_size);
}
  • memblock_free_early() 함수를 호출하여 memblock에서 pcpu_alloc_info 구조체를 전부 지운다.
    • ai->__ai_size:
      • pcpu_alloc_info 하위에 존재하는 그룹 정보 및 매핑 정보를 모두 포함한 사이즈
    • 함수 내부에서는 memblock_remove_range() 함수를 사용한다.

 

pcpu_schedule_balance_work()

/*
 * Balance work is used to populate or destroy chunks asynchronously.  We
 * try to keep the number of populated free pages between
 * PCPU_EMPTY_POP_PAGES_LOW and HIGH for atomic allocations and at most one
 * empty chunk.
 */
static void pcpu_balance_workfn(struct work_struct *work);
static DECLARE_WORK(pcpu_balance_work, pcpu_balance_workfn);
static bool pcpu_async_enabled __read_mostly;
static bool pcpu_atomic_alloc_failed;

static void pcpu_schedule_balance_work(void)
{
        if (pcpu_async_enabled)
                schedule_work(&pcpu_balance_work);
}
  •  pcpu_async_enable 플래그가 설정되어 있으면 schedule_work()를 수행한다.
    • 스케쥴러를 사용하여 pcpu_balance_workfn() 호출하여 free chunk 및 페이지 할당(populate) 수를 관리한다.
      • 빈 chunk 회수(reclaim)
        • 하나만 빼고 완전히 빈 chunk 모두를 이 루틴에서 회수한다.
      • 몇 개의 free populated page 유지
        • atomic allocation을 위해 항상 약간의 free populated page를 유지하려 한다.
        • 페이지 수: PCPU_EMPTY_POP_PAGES_LOW(2) ~ PCPU_EMPTY_POP_PAGES_HIGH(4) 범위
        • 이전에 atomic allocation이 실패한 경우에는 최대 값(PCPU_EMPTY_POP_PAGES_HIGH)을 사용한다.

 

기타

  • laze allocation:
    • NUMA 아키텍처 중 NR_CPUS가 수 천 개등 매우 큰 시스템에서는 모든 possible cpu에 실제 할당을 하기가 힘들기 떄문에 lazy allocation을 사용한다.

 

참고

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.