아래의 순서도는 Generic-SMP 기반의 setup_per_cpu_areas()를 사용하는 것을 기준으로 만들어졌다.
- ARM은 UP-Generic 또는 SMP-Generic 코드를 사용한다.
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() – 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
- PERCPU_DYNAMIC_RESERVE는 아키텍처마다 다르다.
- PCPU_MIN_UNIT_SIZE(32K)와 PERCPU_DYNAMIC_RESERVE와 비교하여 가장 큰 크기로 유닛 크기를 결정한다.
- ai = pcpu_alloc_alloc_info(1, 1);
- 인수로 그룹(노드) 수와 유닛 수를 대입하는데 UP 시스템이므로 두 인수가 모두 1로 호출하여 pcpu_alloc_info 구조체 등을 할당 받아 온다.
- if (pcpu_setup_first_chunk(ai, fc) < 0)
- first chunk를 구성한다.
- unit_size:
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[]도 구성되어 있다.
- 주어진 인수 값을 가지고 ai(pcpu_alloc_info 구조체) 값을 구성하고 만들어 온다.
- 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 수행에 의해 추가되었다.
- size_sum은 unit_size와 다를 수 있다.
- pcpu_build_alloc_info()
- 그룹(노드)별로 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 에 할당한다.
- areas_size = PFN_ALIGN(ai->nr_groups * sizeof(void *));
- 그룹 수만큼 루프를 돌며 각 그룹의 유닛 수(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이므로 처음에 무조건 갱신된다.
- 처음에 만드는 per-cpu 영역은 버디 시스템등 정식 메모리 관리자가 동작하기 전에는 memblock을 통해 즉 각 그룹(노드) 메모리에 할당을 하지 않고 lowmem 영역에 만든다.
/* * 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 영역에서 할당 해제한다.
- void *ptr = areas[group];
/* 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_free_alloc_info()
- per-cpu data를 만들때 사용한 관리정보를 모두 삭제한다.
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
/* * 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 보다 크므로)
- PCPU_MIN_UNIT_SIZE
- 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).
- 계산해 내는 단계는 3단계로 구성한다.
- 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이다)
- ARM: alloc_size=0xb000, max_upa=1, nr_cpu=4인 경우 계산되어지는 값들
- 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가 배치된 경우이다.
- 아래 그림은 첫 번째 그룹(노드)에 4개, 두 번째 그룹(노드)에 2개의 cpu가 배치된 경우이다.
/* * 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
- wastage가 1/3을 초과하면 낭비가 많아서 포기.
- if (allocs > last_allocs)
- allocs=2, last_allocs=1, best_upa=4
- for (upa = max_upa; upa; upa–) {
- 예) 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
- wastage가 1/3을 초과하면 낭비가 많아서 포기.
- if (allocs > last_allocs)
- allocs=2, last_allocs=1, best_upa=8
- for (upa = max_upa; upa; upa–) {
- 아래 표는 min_unit_size=44K 를 다양한 조건에서 best_upa값을 추적하였다.
- X축: alloc_size
- Y축: group별 cpu 수_nr_cpu, nr_group
- 아래 표 역시 min_unit_size=44K 를 다양한 조건에서 allocs, wasted 값들을 추적하였다.
- X축: upa
- Y축: group 루프
- 그룹: cpu, group
- 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가 나온 사례이다.
- allocs=2, best_upa=4
- 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는 절반씩 줄여 나간다.
- allocs=3, best_upa=4
- 비대칭 NUMA 노드가 unit->cpu 매핑이 과정도 유의깊게 확인해야 한다.
- 1) group-0에는 4개의 cpu, group-1에는 2개의 cpu, 그리고 max_upa=32일 때
/* 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)와 다름을 알 수 있다.
- ai = pcpu_alloc_alloc_info(nr_groups, nr_units);
- pcpu_alloc_info 구조체를 memblock 영역에 만들고 리턴한다.
- pcpu_alloc_info 구조체 안에는 pcpu_group_info[] 구조체 배열과 그 안에 cpu_map[] 배열도 구성한다.
- pcpu_alloc_info 구조체를 memblock 영역에 만들고 리턴한다.
- 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 매핑 배열
- ai(pcpu_alloc_info 구조체) 내부의 멤버 변수에 사이즈 등을 지정한다.
- 그룹 수 만큼 루프를 돌며 아래 값들을 설정한다.
- gi->base_offset:
- 그룹별 base 주소
- gi->cpu_map[]:
- unit -> cpu 매핑 값
- 매핑 되지 않은 값은 NR_CPUS 값이 들어간다.
- gi->nr_units:
- 그룹별 유닛 수 (비 대칭 NUMA 시스템에서 생길 수 있는 매핑이 되지 않는 빈 유닛도 포함된다)
- gi->base_offset:
- 아래 그림은 NUMA 시스템이면서 큰 페이지 메모리를 지원하는 경우 per-cpu 데이터 할당을 각각의 노드 메모리에 나누어서 한 경우를 표현하였다.
- 아래 표는 2개의 노드를 가진 대칭형 NUMA 시스템에서 cpu_map[]을 배치한 모습이다.
- 예) NR_CPUS=8, cpus={ 4, 4 }, max_upa=32
- allocs(할당 수)=2, nr_units=8, best_upa=4
- 예) NR_CPUS=8, cpus={ 4, 4 }, max_upa=32
- 아래 표와 그림은 2개의 노드를 가진 비대칭형 NUMA 시스템에서 cpu_map[]을 배치한 모습이다.
- 예) NR_CPUS=9, cpus = { 8, 1 }, max_upa=32, best_upa=4
- allocs(할당 수)=3, nr_units=12, best_upa=4
- 예) NR_CPUS=9, cpus = { 8, 1 }, max_upa=32, best_upa=4
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))
- #define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (typeof(x))(a) – 1)
- x값을 a값 단위로 align한다.
- 예) ALIGN(7, 4) = 8
- #define ALIGN(x, a) __ALIGN_KERNEL((x), (a))
- __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에는 할당 공간 사이즈를 집어넣는다.
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가 된다.
- reserved(module) 영역이 지정되지 않는 경우 하나의 매핑을 준비한다.
/** * 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_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 주소를 저장한다.
- group_offsets[group] = gi->base_offset;
/* 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 타입 사이즈
- pcpu_nr_units:
- 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_size_to_slot()
- pcpu_slot[]
- pcpu_nr_slots 수 만큼 list_head 구조체 크기를 memblock에 할당한다.
- INIT_LIST_HEAD(&pcpu_slot[i]);
- pcpu_nr_slots 수 만큼 pcpu_slot[] list 구조를 초기화한다.
- pcpu_nr_slots = __pcpu_size_to_slot(pcpu_unit_size) + 2;
/* * 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 할당의 매핑 관리가 이루어진다.
- rpi2: dyn_size=0x5140, static_size=0x3ec0
/* 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 상태라고 매핑되었다.
- rpi2: dyn_size=0x5140, static_size=0x3ec0, reserved=0x2000
- first chunk에 dynamic 영역에 대한 매핑 관리를 위해 dchunk가 선택되었고, reserved chunk는 schunk를 대입하여 관리한다.
- schunk->free_size는 reserved_size 와 같다.
- dchunk->free_size는 dyn_size와 같다.
/* 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_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 값을 알아본 사례이다.
- 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이 리턴되는 사례를 보여준다.
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이다.
- chunk내의 사이즈에 해당하는 nslot(계산된 슬롯 번호)를 알아온다.
- 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] 리스트에 처음 추가된다.
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() 함수를 사용한다.
- ai->__ai_size:
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)을 사용한다.
- 빈 chunk 회수(reclaim)
- 스케쥴러를 사용하여 pcpu_balance_workfn() 호출하여 free chunk 및 페이지 할당(populate) 수를 관리한다.
기타
- laze allocation:
- NUMA 아키텍처 중 NR_CPUS가 수 천 개등 매우 큰 시스템에서는 모든 possible cpu에 실제 할당을 하기가 힘들기 떄문에 lazy allocation을 사용한다.
참고
- Per-cpu | 문c