이번 장에서는 percpu
에 대해 알아볼 것이다. percpu
는 리눅스 커널 2.6 버전부터 추가된 기능으로 각 CPU
코어 별로 독립적인 메모리 공간을 제공한다.
싱글 코어 CPU
에서 멀티코어 CPU
로 발전하면서, SMP(Symmetric Multi-Processing)
환경에서 효율적으로 동작하기 위한 CPU
별 전용 데이터가 필요로 했다. percpu
는 이러한 요구 조건을 만족하기 위해 도입된 새로운 커널의 기능이다.
ELF Sections
운영체제의 각 프로세스
는 고유의 메모리를 가진다. 이는 독립적인 메모리 공간을 가진다는 것을 의미한다. 그렇기에 각 프로세스
가 서로의 데이터를 침범하지 않을 수 있는 것이다. 마찬가지로 각 CPU
가 고유의 데이터를 가지기 위해서는 고유의 메모리 공간을 할당받아야 한다.
vmlinux
파일 분석 운영체제도 뭔가 대단해 보이지만 실상은 하나의 프로그램이다. 이 프로그램이 자원을 영속적으로, 병렬적으로, 추상적으로 처리하는 것 뿐이다. 우리가 만드는 프로그램도 힙 영역과 스택 영역으로 나뉘는 것처럼 운영체제 역시 다양한 영역으로 데이터가 나뉘어 저장되어 진다. 이를 확인하기 위해 리눅스의 vmlinux
라는 이름의 ELF
을 분석해보겠다.
여기에서 ELF
란 Executable and Linkable Format
의 약자로 UNIX
시스템의 실행파일 포맷으로 보면 된다. Windows
운영체제에는 .exe
로 끝나는 확장자의 PE(Portable Executable)
포맷을 사용한다.
아무튼 빌드된 운영체제 파일인 vmlinux
파일은 리눅스 커널 빌드 경로에 최상단에 저장되어 있다.
vmlinux
파일을 file
명령어로 찍어보면 다음과 같은 결과를 확인할 수 있다.
뭔가 복잡하게 이래저래 나오지만 아무튼 x86-64
환경에서 동작하는 ELF 64-bit
포맷의 실행파일임을 알 수 있다. 이 파일은 readelf
프로그램을 통해 분석이 가능하다.
readelf -Sl vmlinux
를 입력하여 그 결과를 확인해보았다.
일반적인 실행파일과 달리 수 많은 섹션들이 나타난다.
아래로 내려서 35
번째 섹션인 .data..percpu
섹션이 존재하는 것을 확인할 수 있다. 크기는 0000 0000 0002 d000
이며, 주소는 0000 0000 0000 0000
번지이며, 오프셋은 0260 0000
임을 확인할 수 있다.WA
플래그는 write
그리고 alloc
의 약자이다.
이러한 섹션 정보들은 컴파일러의 한 부분인 링커 프로그램이 활용하여, 데이터를 각 섹션의 적절한 위치에 배치하게 된다. 앞서 소개한 percpu
데이터 역시 위에서 나온 주소에 적절하게 배치되어질 것이다.
percpu
정의 하기percpu
는 아래와 같은 매크로를 사용하여 선언 및 정의한다.
// extern 으로 percpu 선언
DECLARE_PER_CPU(unsigned long, u64_conuter);
// percpu 정의
DEFINE_PER_CPU(long, s64_counter);
위 코드는 include/linux/percpu-defs.h
경로에서 확인할 수 있으며 아래와 가이 정의되어 있다:
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES
#define DECLARE_PER_CPU_SECTION(type, name, sec) \
extern __PCPU_ATTRS(sec) __typeof__(type) name
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) __typeof__(type) name
#endif
#define DECLARE_PER_CPU(type, name) \
DECLARE_PER_CPU_SECTION(type, name, "")
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
아래의 DECLARE/DEFINE_PPER_CPU
정의는 최종적으로 __PCPU_ATTRS
매크로로 확장되어진다. 또한 __PCPU_ATTRS
매크로에 등장하는 section(PER_CPU_BASE_SECTION)
의 PER_CPU_BASE_SECTION
은 include/asm-generic/percpu.h
헤더에서 그 값을 확인할 수 있다.
#ifndef PER_CPU_BASE_SECTION
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
#endif
".data..percpu"
어디서 많이 보지 않았나? 이는 1절(ELF Sections
) 에서 보았던 그 섹션명이다.
위에서 설명한 DECLARE_PER_CPU
매크로를 순차적으로 치환하면 다음의 코드를 얻을 수 있다:
// 매크로 치환 전
DECLARE_PER_CPU(unsigned long, u64_counter);
// 1 단계
DECLARE_PER_CPU_SECTION(unsigned long, u64_counter, "");
// 2 단계
extern __PCPU_ATTRS("") __typeof__(unsigned long) u64_counter;
// 3 단계
extern __percpu __attribute__((section(".data..percpu" "")) \
PER_CPU_ATTRIBUTES __typeof__(unsigned long) u64_counter;
// 최종
extern __attribute__((noderef, address_space())) \
__attribute((section(".data..percpu")) \
unsigned long u64_counter;
뭔가 엄청 복잡해 보이지만 실상은 그냥 extern unsigned long u64_counter;
변수를 선언하고 이를 ".data..percpu"
섹션에 배치하기 위해 여러 컴파일러 확장 기능을 사용한 것이다.
매크로 정보는 include/linux/compiler_types.h
에서 확인 가능하다. elixir.bootlin.com
에서 검색하는 것도 좋다.
percpu
관련 API DEFINE_PER_CPU(long, s64_counter)
와 같이 변수를 정의하면 __attribute__((noderef, address_space()) __attribute((section(".data..percpu")) long s64_counter
로 치환되어 진다. 복잡해보여도 그냥 long
자료형의 변수이니까 일반적인 C
언어의 연산자를 사용하면 될 것 같지만 그러면 안된다. 실제 percpu
자료형의 주소는 데이터의 주소를 직접 가르키는 것이 아니기 때문에 반드시 적절한 API 함수를 사용해야 한다.
percpu
값 읽기percpu
로 선언된 변수의 값을 읽기 위해서 아래의 매크로를 사용할 수 있다:
__this_cpu_read(s64_counter);
위 매크로는 include/linux/percpu-defs.h
에 정의되어 있으며 아래와 같이 확장된다:
#define __this_cpu_read(pcp) \
({ \
__this_cpu_preempt_check("read"); \
raw_cpu_read(pcp); \
})
가장 처음 나온 __this_cpu_preempt_check()
매크로는 CPU
의 선점 여부를 확인하는 매크로이다. 위에서 나온 연산은 선점(preemption
) 과 인터럽트(intterupt
, IRQ
) 등으로부터 안전해야 하므로 그걸 확인하는 매크로이다.
raw_cpu_read()
매크로가 실제로 값을 읽어 들이는 매크로인데 이는 다시 아래와 같이 확장된다:
#define __pcpu_size_call_return(stem, variable) \
({ \
typeof(variable) pscr_ret__; \
__verify_pcpu_ptr(&(variable)); \
switch(sizeof(variable)) { \
case 1: pscr_ret__ = stem##1(variable); break; \
case 2: pscr_ret__ = stem##2(variable); break; \
case 4: pscr_ret__ = stem##4(variable); break; \
case 8: pscr_ret__ = stem##8(variable); break; \
default: \
__bad_size_call_parameter(); break; \
} \
pscr_ret__; \
})
#define raw_cpu_read(pcp) __pcpu_size_call_return(raw_cpu_read_, pcp)
각 변수의 크기에 따라 switch - case
로 그 내용이 갈리는데 결국 핵심은 stem_1
, stem_2
, stem_4
, stem_8
매크로 중 하나가 호출된다는 뜻이다. 여기에서 stem
은 __pcpu_size_call_return
의 인자로 전달된 raw_cpu_read
이므로 결국 raw_cpu_read_4
와 같은 이름의 매크로로 확장된다. 이들은 arch/x86/include/asm/percpu.h
에서 다시 확인이 가능하다.
#define percpu_from_op(size, qual, op, _var) \
({ \
__pcpu_type_##size pfo_val__; \
asm qual (__pcpu_op2_##size(op, __percpu_arg([var]), "%[val]") \
: [val] __pcpu_reg_##size("=", pfo_val__) \
: [var] "m" (_var)); \
(typeof(_var))(unsigned long) pfo_val__; \
})
#define this_cpu_read_1(pcp) percpu_from_op(1, volatile, "mov", pcp)
#define this_cpu_read_2(pcp) percpu_from_op(2, volatile, "mov", pcp)
#define this_cpu_read_4(pcp) percpu_from_op(4, volatile, "mov", pcp)
#define this_cpu_read_8(pcp) percpu_from_op(8, volatile, "mov", pcp)
보는 것과 같이 inline assembly
로 확장되어진다.
#define __pcpu_type_1 u8
#define __pcpu_type_2 u16
#define __pcpu_type_4 u32
#define __pcpu_type_8 u64
#define __pcpu_cast_1(val) ((u8)(((unsigned long) val) & 0xff))
#define __pcpu_cast_2(val) ((u16)(((unsigned long) val) & 0xffff))
#define __pcpu_cast_4(val) ((u32)(((unsigned long) val) & 0xffffffff))
#define __pcpu_cast_8(val) ((u64)(val))
#define __pcpu_op1_1(op, dst) op "b " dst
#define __pcpu_op1_2(op, dst) op "w " dst
#define __pcpu_op1_4(op, dst) op "l " dst
#define __pcpu_op1_8(op, dst) op "q " dst
#define __pcpu_op2_1(op, src, dst) op "b " src ", " dst
#define __pcpu_op2_2(op, src, dst) op "w " src ", " dst
#define __pcpu_op2_4(op, src, dst) op "l " src ", " dst
#define __pcpu_op2_8(op, src, dst) op "q " src ", " dst
#define __pcpu_reg_1(mod, x) mod "q" (x)
#define __pcpu_reg_2(mod, x) mod "r" (x)
#define __pcpu_reg_4(mod, x) mod "r" (x)
#define __pcpu_reg_8(mod, x) mod "r" (x)
#define __pcpu_reg_imm_1(x) "qi" (x)
#define __pcpu_reg_imm_2(x) "ri" (x)
#define __pcpu_reg_imm_4(x) "ri" (x)
#define __pcpu_reg_imm_8(x) "re" (x)
#define __percpu_arg(x) __percpu_prefix "%" #x
간단하게 __this_cpu_read_4(val)
가정하면 이는 아래와 같이 확장된다:
// 시작
#define __this_cpu_read(val) \
({ \
__this_cpu_preempt_check("read"); \
raw_cpu_read(val); \
})
// raw_cpu_read(val) => __pcpu_size_call_return(raw_cpu_read_, val)
#define raw_cpu_read(val) __pcpu_size_call_return(raw_cpu_read_, val)
// __pcpu_size_call_return(raw_cpu_read_, val)
#define __pcpu_size_call_return(raw_cpu_read_, val) \
({ \
typeof(val) pscr_ret__; \
__verify_pcpu_ptr(&(val)); \
switch(sizeof(val)) { \
case 1: pscr_ret__ = raw_cpu_read_1(val); break; \
case 2: pscr_ret__ = raw_cpu_read_2(val); break; \
case 4: pscr_ret__ = raw_cpu_read_4(val); break; \
case 8: pscr_ret__ = raw_cpu_read_8(val); break; \
default: \
__bad_size_call_parameter(); break; \
} \
pscr_ret__; \
})
// raw_cpu_read_4(val) => percpu_from_op(4, /* NONE */, "mov", val)
#define raw_cpu_read_4(pcp) percpu_from_op(4, , "mov", pcp)
#define percpu_from_op(4, , "mov", val) \
({ \
u32 pfo_val__; \
asm (__pcpu_op2_4("mov", __percpu_arg([val]), "%[val]") \
: [val] __pcpu_reg_4("=", pfo_val__) \
: [val] "m" (val)); \
(typeof(val))(unsigned long) pfo_val__; \
})
#define __pcpu_op2_4(op, src, dst) op "l " src ", " dst
#define percpu_from_op(4, , "mov", val) \
({ \
u32 pfo_val__; \
asm ("movl %%gs":" val, %[val]" \
: [val] "=r" (pfo_ret__) \
: [val] "m" (val)); \
(typeof(_var))(unsigned long) pfo_val__; \
})
인라인 어셈블리는 그 내용이 많고 복잡하며 아키텍쳐에 종속적인 내용이므로 따로 설명하진 않겠다. 자세한 내용은 GNU GCC inline assembly
에 대해 찾아보길 바란다.
percpu
값 쓰기 (연산 수행)percpu
의 값을 쓰는 것 역시 적절한 API 를 사용해야 하고 이는 __this_cpu_write(pcp, val)
을 통해서 가능하다.
#define __this_cpu_write(pcp, val) \
({ \
__this_cpu_preempt_check("write"); \
raw_cpu_write(pcp, val); \
})
매크로는 위와 같이 정의되어 있으며 __this_cpu_read()
매크로와 마찬가지로 최종적으로 inline assembly
로 확장되어진다. 앞서 설명했던 내용이므로 더 이상의 자세한 내용을 줄이겠다.
percpu
구조 percpu
내부 코어 소스는 모두 mm/percpu.c
에서 확인할 수 있다:
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[];
};
struct pcpu_chunk {
#ifdef CONFIG_PERCPU_STATS
int nr_alloc; /* # of allocations */
size_t max_alloc_size; /* largest allocation size */
#endif
struct list_head list; /* linked to pcpu_slot lists */
int free_bytes; /* free bytes in the chunk */
struct pcpu_block_md chunk_md;
void *base_addr; /* base address of this chunk */
unsigned long *alloc_map; /* allocation map */
unsigned long *bound_map; /* boundary map */
struct pcpu_block_md *md_blocks; /* metadata blocks */
void *data; /* chunk data */
bool immutable; /* no [de]population allowed */
int start_offset; /* the overlap with the previous
region to have a page aligned
base_addr */
int end_offset; /* additional area required to
have the region end page
aligned */
#ifdef CONFIG_MEMCG_KMEM
struct obj_cgroup **obj_cgroups; /* vector of object cgroups */
#endif
int nr_pages; /* # of pages served by this chunk */
int nr_populated; /* # of populated pages */
int nr_empty_pop_pages; /* # of empty populated pages */
unsigned long populated[]; /* populated bitmap */
};
struct pcpu_chunk
의 구조체는 mm/percpu-internal.h
헤더파일에 담겨있다.
[사이트] http://jake.dothome.co.kr/per-cpu/
[사이트] https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[사이트] https://en.wikipedia.org/wiki/Portable_Executable
[사이트] https://gcc.gnu.org/onlinedocs/gcc/
[사이트] https://elixir.bootlin.com
[사이트] https://0xax.gitbooks.io/linux-insides/content/Theory/linux-theory-3.html
[사이트] https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#index-asm-assembler-template
[책] 리눅스 커널 소스 해설: 기초 입문 (정재준 저)