Linux Kernel #24 percpu

mythos·2021년 6월 22일
0

Linux Tutorial

목록 보기
25/25
post-thumbnail

이번 장에서는 percpu 에 대해 알아볼 것이다. percpu 는 리눅스 커널 2.6 버전부터 추가된 기능으로 각 CPU 코어 별로 독립적인 메모리 공간을 제공한다.
싱글 코어 CPU 에서 멀티코어 CPU 로 발전하면서, SMP(Symmetric Multi-Processing) 환경에서 효율적으로 동작하기 위한 CPU 별 전용 데이터가 필요로 했다. percpu 는 이러한 요구 조건을 만족하기 위해 도입된 새로운 커널의 기능이다.

1. ELF Sections

운영체제의 각 프로세스는 고유의 메모리를 가진다. 이는 독립적인 메모리 공간을 가진다는 것을 의미한다. 그렇기에 각 프로세스가 서로의 데이터를 침범하지 않을 수 있는 것이다. 마찬가지로 각 CPU 가 고유의 데이터를 가지기 위해서는 고유의 메모리 공간을 할당받아야 한다.

vmlinux 파일 분석

운영체제도 뭔가 대단해 보이지만 실상은 하나의 프로그램이다. 이 프로그램이 자원을 영속적으로, 병렬적으로, 추상적으로 처리하는 것 뿐이다. 우리가 만드는 프로그램도 힙 영역과 스택 영역으로 나뉘는 것처럼 운영체제 역시 다양한 영역으로 데이터가 나뉘어 저장되어 진다. 이를 확인하기 위해 리눅스의 vmlinux 라는 이름의 ELF 을 분석해보겠다.
여기에서 ELFExecutable 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 데이터 역시 위에서 나온 주소에 적절하게 배치되어질 것이다.

2. 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_SECTIONinclude/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 에서 검색하는 것도 좋다.

3. percpu 관련 API

DEFINE_PER_CPU(long, s64_counter) 와 같이 변수를 정의하면 __attribute__((noderef, address_space()) __attribute((section(".data..percpu")) long s64_counter 로 치환되어 진다. 복잡해보여도 그냥 long 자료형의 변수이니까 일반적인 C 언어의 연산자를 사용하면 될 것 같지만 그러면 안된다. 실제 percpu 자료형의 주소는 데이터의 주소를 직접 가르키는 것이 아니기 때문에 반드시 적절한 API 함수를 사용해야 한다.

1. 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 에 대해 찾아보길 바란다.

2. 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 로 확장되어진다. 앞서 설명했던 내용이므로 더 이상의 자세한 내용을 줄이겠다.

4. 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
[책] 리눅스 커널 소스 해설: 기초 입문 (정재준 저)

profile
2000.11.30, 부끄럽게 살지 말자.

0개의 댓글