티스토리 뷰

  • first chunk 의 맵 엔트리 관리

map 으로 관리하는데, 코드를 봐야 명확한 개념을 알 수 있을 것 같다.

struct pcpu_chunk {
  int* map
}

dchunk->map[0] = 1;
dchunk->map[1] = pcpu_reserved_chunk_limit;
dchunk->map[2] = (pcpu_reserved_chunk_limit + dchunk->free_size) | 1;
per-cpu chunk 할당 방식
  1. embed 방식
    large page 를 사용하여 tlb 캐시 효율을 올리는 방식. NUMA 는 각 노드 메모리 별로 할당
    lowmem 영역을 사용 (ZONE_NORMAL)

  2. page 방식
    vmalloc 영역에 최소 페이지 단위로 페이지 할당하여 매핑. chunk 를 노드별로 나누지 않음

per-cpu 정적 선언 및 저장 섹션

커널 이미지에 .data..percpu 에 들어간다

디멘드 페이징이란?

first chunk 는 커널이 초기화 될 때, 메모리를 할당받아 구성하고 매핑된다.
추가되는 chunk 들은 슬럽 메모리 할당자가 동작한 후 디맨드 페이징을 이용한다.

pcpu_chunk 구조체에, chunk 영역만 지정하고, 실제 물리 메모리에서 페이지를 할당받지 않는다.
-> 실제로 필요할 때 할당?

embed & paging 방식에 관계 없이 추가되는 chunk 는 vmalloc 영역에 설정된다.

  • embed 방식은 vmalloc 영역의 위에서 아래 방향으로 (first chunk 는 lowmem 영역)
  • paging 방식은 vmalloc 영역의 아래에서 위 방향으로 (first chunk 는 vmalloc 의 아래)

또한 chunk 는 모든 possible cpu 에 대해 하나의 chunk 로 구성한다.

per-cpu 의 NUMA 지원

static const int *pcpu_unit_map __read_mostly;        /* cpu -> unit */

근데 unit 이 뭐지?

4.9.2 per-cpu 초기화

.data..percpu 를 first chunk 로 복사하자.

4-162 mm/percpu.c setup_per_cpu_areas

first chunk 구성하기

프로세서 구조 : UP(Uniprocessor) / SMP(Symmetric Multiprocessing) 에 따라 구현이 달라짐. (아예 별도도 구성 가능)

UP 버전

  • 시스템의 per-cpu. 1개의 그룹과 1개의 유닛만 구성
pcpu_alloc_alloc_info(int nr_groups, int nr_units) -> 1, 1 로 호출

struct pcpu_alloc_info {
    size_t            static_size;
    size_t            reserved_size;
    size_t            dyn_size;
    size_t            unit_size;
    size_t            atom_size;
    size_t            alloc_size;
    size_t            __ai_size;    /* internal, don't use */
    int            nr_groups;    /* 0 if grouping unnecessary */
    struct pcpu_group_info    groups[];
};

// 초기화시 unit 을 NR_CPUS 로 지정
for (unit = 0; unit < nr_units; unit++)
  ai->groups[0].cpu_map[unit] = NR_CPUS;

// fc = first chunk
fc = memblock_virt_alloc_from_nopanic(unit_size,
  PAGE_SIZE,
  __pa(MAX_DMA_ADDRESS));

// size, align, limit 으로 unit_size 를 align 한 메모리 공간 할당 받기
if (limit && bdata->node_min_pfn >= PFN_DOWN(limit))
    break;

pcpu_setup_first_chunk(ai, fc) // 코드 4-170

SMP 버전

PERCPU_MODULE_RESERVE : DEFINE_PER_CPU 를 커버할 수 있는 크기.
PERCPU_DYNAMIC_RESERVE : first chunk 에 dynamic per-cpu 할당 할 수 있는 크기.

unit 은 chunk 하나에 cpu 별로 나눠지는 영역을 뜻하는 듯 하다.

pcpu_embed_first_chunk 에서 first chunk 를 구성 // 코드 4-164

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];

4-164 mm/percpu.c pcpu_embed_first_chunk

embed 방식으로 large 페이지 구성

int __init pcpu_embed_first_chunk(
  size_t reserved_size, // 모듈에서 사용하는 reserved 영역의 크기
  size_t dyn_size, // dynamic 영역의 크기. ARM64 -> 28K
  size_t atom_size, // 할당 최소 크기
  pcpu_fc_cpu_distance_fn_t cpu_distance_fn, // cpu 간 distance 를 구하는 함수 포인터. NUMA 의 노드 그룹에 따라 사용. ARM64 -> null
  pcpu_fc_alloc_fn_t alloc_fn, // per-cpu 페이지 할당 함수
  pcpu_fc_free_fn_t free_fn // per-cpu 페이지 free 함수
)

pcpu_build_alloc_info // pcpu 할당 정보 구성. 코드 4-166

size_sum = ai->static_size + ai->reserved_size + ai->dyn_size; // chunk 의 사이즈
areas_size = PFN_ALIGN(ai->nr_groups * sizeof(void *));           // node 그룹별로 pcpu 의 주소를 저장하기 위함. areas[] 의 사이즈

for group {
  // group 당 한번 할당
  ptr = alloc_fn(cpu, gi->nr_units * ai->unit_size, atom_size);
  areas[group] = ptr;

  base = min(ptr, base); // -> 그룹중 가장 낮은 곳
}

// alloc_fn 기본값
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));
}

for group ; {
  안쓰는 cpu 영역 해제
  쓰는 cpu 라면 unit_size 에서 안쓰는 부분만큼 해제
}

for group ; {
  max distance = group 별 base
}

max distance += ai -> unit_size(first chunk 를 위함) // 75% 넘는 경우 경고. 옵션에 따라 사용 못함 -> rc 가 null 로 리턴되면 page 방식 사용
*여기가 unit_size 만큼 더하는게 아니라 unit 을 곱했어야함. 리눅스 6에선 패치되어있음을 확인*

embed 방식이 위에서 아래로 할당하는 이유도, 전에 할당 취소를 많이 했었기 때문에 빈공간 검색이 용이

이후 free 에서 임시로 사용했던 구조체들을 free 시킨다

4-166 mm/percpu.c pcpu_build_alloc_info

  • pcpu chunk 할당 정보 구성
  • upa : unit per allocation
    • 4K page 로는 하나의 유닛을 만들 수 없음. 보통 수십 KB가 필요
    • 2M huge page 등을 처리할 때 효과적인 유닛의 수를 정함
atom_size: 각 CPU에 할당될 최소 단위의 메모리 크기
size_sum : static + reserve + dyn
min unit = min(size_sum, PCPU_MIN_UNIT_SIZE);

alloc_size = min unit 사이즈를 atom_size 배수로 올림 처리
upa = alloc_size / min_unit_size

atom_size 가 4K 라면 upa = 1, max_upa =1 이 되겠지만
예를들어 2M, 16M 이 된다면, upa 가 커짐.

offset_in_page(alloc_size / upa) 유닛 하나가 페이지 단위로 떨어지는 경우
max_upa = upa 를 하나씩 줄여가며, 유닛크기가 4K 페이지의 2 의 n 승이 되도록 조절

4-167

cpu_distance_fn
group_map[tcpu] == group

if (group_map[tcpu] == group && cpu_distance_fn &&  // target cpu 가 마지막으로 검사한 그룹과 같으면 체크 
  (cpu_distance_fn(cpu, tcpu) > LOCAL_DISTANCE || // cpu_distance_fn 없으면 cpu 갯수가 group 이 됨
  cpu_distance_fn(tcpu, cpu) > LOCAL_DISTANCE)) {
    group++; // -> 다음 그룹인 애는 같은 group 이 없으니 여기서 다음에 새로 등록됨

groupmap 에는 group 이, group_cnt 에는 그룹의 카운트를 등록

// best upa 를 max upa 에서 1씩 깍으며 찾음
1. max_upa 찾듯이, page offset 체크
2. 위에서 설정한 group_cnt 를 upa 로 나눈 뒤 round up 하면 할당 요청한 페이지 수 (this_alloc)
3. 이 페이지에서 사용하지 않는 유닛은 wasted 에 더한뒤 너무 많으면 스킵한다

nr_cpu = 8 일때, wasted 의 합이 2를 넘어가면 안됨
alloc_size = 2M

upa group_cnt this_allocs wasted
32 (4,4) (1,1) (28,28) 56 > 2(x)
16 (4,4) (1,1) (12,12) 24 >2 (x)
8 (4,4) (1,1) (4,4) 8 >2 (x)
4 (4,4) (1,1) (0,0) 0 =< 2 (o)
2 (4,4) (2,2) x 여기서 루프 종료. 4가 best (0,0)

4-167

struct pcpu_group_info {
    int            nr_units;    /* aligned # of units */
    unsigned long        base_offset;    /* base address offset */
    unsigned int        *cpu_map;    /* unit->cpu map, empty
                         * entries contain NR_CPUS */
};

struct pcpu_alloc_info {
    size_t            static_size;
    size_t            reserved_size;
    size_t            dyn_size;
    size_t            unit_size;
    size_t            atom_size;
    size_t            alloc_size;
    size_t            __ai_size;    /* internal, don't use */
    int            nr_groups;    /* 0 if grouping unnecessary */
    struct pcpu_group_info    groups[];
};

ai 구조

|------필드들------|-group[0]-|-group[1]-|-..group[nr_groups -1]-|-cpu_map[0]-|-cpu_map[1]-|-...cpu_map[nr_unit - 1]-|
cpu_map[0] 이 group[0] 의 cpu_map 포인터

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

그래서 결국 cpu_map 에 각 그룹별로 점프뛰면서 unit -> cpu 를 가리키게 됐음

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

    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;
}

이 정보를 가지고 가서, 결국 노드 그룹별로 unit 묶음인 chunk 들을 만들테고, 이중 사용하지 않는건 코드 4-165 의 pcpu_embed_first_chunk 에서 free 하나보다..! 노드별로 locality 를 위해 큰 덩어리로 할당하는듯. 그래서 gi->nr_units 를 지정하고 그만큼씩 할당한다.

4.9.3 first chunk 구성

  • reserved 영역이 지정되지 않는 경우 schunk 에 static+dynamic 만 구성
  • reserved 영역이 지정되는 경우 schunk 는 static + dynamic 으로, dchunk 는 dynamic 으로 구성

4-170 4-171 mm/percpu.c pcpu_setup_first_chunk

  • smap, dmap 을 PERCPU_DYNAMIC_EARLY_SLOT 만큼 만들었는데, 이 인덱스가 뭔지는 아직 모름
group_offsets[group] = gi->base_offset
group_sizes[group] = gi->nr_unit * ai->unit_size

for (group 의 nr_unit) {
  cpu = gi->cpu_map[i];

  unit_map[cpu] = unit + i; // cpu 에 unit 을 매핑
  unit_off[cpu] = gi->base_offset + i * ai->unit_size; // cpu 가 사용중인 unit 의 offset 지정

  /* determine low/high unit_cpu */
  // set min, max offset 
  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; // upa 에 따라 cpu 갯수와 다를 수 있음

// 각종 전역 변수들 설정
pcpu_chunk_struct_size = sizeof(struct pcpu_chunk) +
  BITS_TO_LONGS(pcpu_unit_pages) * sizeof(unsigned long); // populated 배열 까지 합쳐 배열 합산

struct pcpu_chunk {
  ..
  unsigned long        populated[];    /* populated bitmap */
};

// 

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]);
// 이게 무슨뜻이지? chunk 리스트를 관리하는 최대 슬롯수 + 2
pcpu_nr_slots = __pcpu_size_to_slot(pcpu_unit_size) + 2; 

// ai->unit_size = alloc_size / upa; -> 유닛 하나당 사이즈
pcpu_unit_pages = ai->unit_size >> PAGE_SHIFT; // 유닛 하나에 필요한 page
pcpu_unit_size = pcpu_unit_pages << PAGE_SHIFT; // 유닛 하나에 필요한 메모리양

__pcpu_size_to_slot = chunk 리스트를 관리하는 최대 슬롯수 ?

static int __pcpu_size_to_slot(int size)
{
    int highbit = fls(size);    /* size is in bytes */
    return max(highbit - PCPU_SLOT_BASE_SHIFT + 2, 1); // +2 를 하는이유는 좀더 넉넉한 슬롯을 위해서
}

--> 다음주에 다시보자 이뇨속!
댓글