container_of 의 정의와 용법

markdown 원문 보기
ckernellinux
☰Series:linux-kernel/data-structure#1

리눅스 커널 소스코드를 분석하다 보면, 디렉토리를 구분하지 않고 container_of 매크로를 많이 사용하는 것을 볼 수 있다. 그중에서도 커널 자료구조, RCU, 동기화, 커널 구조체를 조작하는 형태의 코드에서 주로 보인다.

container_of의 정의와 offsetof

container_of는 include/linux/container_of.h에 정의되어 있다.

include/linux/container_of.h
/* SPDX-License-Identifier: GPL-2.0 */#ifndef _LINUX_CONTAINER_OF_H#define _LINUX_CONTAINER_OF_H#include <linux/build_bug.h>#include <linux/stddef.h>#define typeof_member(T, m)	typeof(((T*)0)->m)/** * container_of - cast a member of a structure out to the containing structure * @ptr:	the pointer to the member. * @type:	the type of the container struct this is embedded in. * @member:	the name of the member within the struct. * * WARNING: any const qualifier of @ptr is lost. */#define container_of(ptr, type, member) ({				\	void *__mptr = (void *)(ptr);					\	static_assert(__same_type(*(ptr), ((type *)0)->member) ||	\		      __same_type(*(ptr), void),			\		      "pointer type mismatch in container_of()");	\	((type *)(__mptr - offsetof(type, member))); })/** * container_of_const - cast a member of a structure out to the containing *			structure and preserve the const-ness of the pointer * @ptr:		the pointer to the member * @type:		the type of the container struct this is embedded in. * @member:		the name of the member within the struct. */#define container_of_const(ptr, type, member)				\	_Generic(ptr,							\		const typeof(*(ptr)) *: ((const type *)container_of(ptr, type, member)),\		default: ((type *)container_of(ptr, type, member))	\	)#endif	/* _LINUX_CONTAINER_OF_H */
인용 1

파일 전체의 소스 내용은 그렇게 많지 않다. 모든 비필수적인 기능들을 제거한 최소한의 코드만 놓고 보면 다음과 같다.

#define container_of(ptr, type, member) ({				\	(type *)((void *)(ptr) - offsetof(type, member)); })
예제 1

offsetof의 정의는 다음과 같다.

include/linux/stddef.h
#define offsetof(TYPE, MEMBER)	__builtin_offsetof(TYPE, MEMBER)
인용 2

결국 container_of는 offsetof를 사용한 단순한 오프셋 계산식이다. 원래 offsetof는 C 언어 표준이 <stddef.h>에 정의한 기능이다. 다만, 리눅스 커널은 -nostdinc로 빌드되어 표준 헤더를 쓰지 않으므로, offsetof처럼 C 언어 표준과 동일한 기능들을 자체적으로 정의한다. offsetof는 이와 같이 GCC/Clang이 지원하는 __builtin_offsetof를 이용한다.

다음 예제는 __builtin_offsetof로 각 멤버의 오프셋을 구하고, 실제 멤버 주소와 함께 출력한다.

#include <stdio.h>struct example {    char    a;    int     b;    double  c;    short   d;};int main(void){    struct example e;    printf("sizeof(struct example) = %zu\n", sizeof(struct example));    printf("offsetof(a) = %zu\n", __builtin_offsetof(struct example, a));    printf("offsetof(b) = %zu\n", __builtin_offsetof(struct example, b));    printf("offsetof(c) = %zu\n", __builtin_offsetof(struct example, c));    printf("offsetof(d) = %zu\n", __builtin_offsetof(struct example, d));    printf("\n");    printf("&e      = %p\n", (void *)&e);    printf("&e.a    = %p\n", (void *)&e.a);    printf("&e.b    = %p\n", (void *)&e.b);    printf("&e.c    = %p\n", (void *)&e.c);    printf("&e.d    = %p\n", (void *)&e.d);    return 0;}
예제 2

실행 결과는 다음과 같다.

sizeof(struct example) = 24offsetof(a) = 0offsetof(b) = 4offsetof(c) = 8offsetof(d) = 16&e      = 0x7ffd329cce10&e.a    = 0x7ffd329cce10&e.b    = 0x7ffd329cce14&e.c    = 0x7ffd329cce18&e.d    = 0x7ffd329cce20

__builtin_offsetof는 표준 offsetof와 인자 순서도 결과도 같다. 둘 다 첫 번째 인자로 구조체 타입을, 두 번째 인자로 멤버 이름을 받는다. 그리고 구조체 시작에서 그 멤버까지의 거리를 size_t 타입의 바이트 수로 돌려준다. 실제로 GCC와 Clang의 <stddef.h>는 offsetof를 __builtin_offsetof로 정의한다. 이 거리에는 멤버 앞에 들어간 패딩도 포함된다. 위 결과에서 b의 오프셋이 1이 아니라 4인 것은 char a 뒤에 3바이트 패딩이 들어갔기 때문이다. 반면 마지막 멤버 d(오프셋 16, 2바이트) 뒤의 6바이트 패딩은 어느 멤버의 오프셋에도 들어가지 않고 sizeof에만 드러난다. 그래서 구조체 크기는 18이 아니라 24이다.

container_of에 담긴 커널과 C의 설계 철학

container_of를 통해 커널의 설계 철학, 더 나아가서는 C 언어의 설계 철학을 알 수 있다.

[LEMMA]
타입이 다르면 같은 시작 주소에서도 연산의 대상이 되는 메모리 영역의 범위가 달라진다(예: int, char, void * 타입 등).
[THEOREM]
container_of 매크로 자체는 아는 정보가 하나도 없다. container_of를 활용하는 외부 자료구조에 데이터(구조체)의 구조 및 사용 방법의 책임을 넘긴다.
[COROLLARY]
그래서 container_of를 다양한 커널 자료구조에서 사용할 수 있다.

이는 여타 객체지향 언어에서 자주 쓰이는 iterable하고 상속 가능한 것을, C 언어가 허용하는 범위 안에서 최대한 strict하고 type-safe하게 구현하고자 했던 각종 노력의 결과물이다. 그래서 후술하겠지만, static_assert 같은 기능을 이용하여 타입 안전성을 검사한다.

container_of의 용법: 리스트 순회 예제

// SPDX-License-Identifier: 0BSD#include <linux/list.h>#include <linux/module.h>struct user_info {	char username[256];	struct list_head list;	u8 age;};static LIST_HEAD(user_info_list);static struct user_info users[] = {	{ .username = "alice", .age = 20 },	{ .username = "bob", .age = 30 },	{ .username = "carol", .age = 40 },};static int __init user_info_init(void){	struct list_head *pos;	struct user_info *user;	size_t i;	pr_info("user_info: init\n");	for (i = 0; i < ARRAY_SIZE(users); ++i) {		user = &users[i];		list_add_tail(&user->list, &user_info_list);	}	list_for_each(pos, &user_info_list) {		user = container_of(pos, struct user_info, list);		pr_info("username=%s age=%u\n", user->username, user->age);	}	return 0;}static void __exit user_info_exit(void){	pr_info("user_info: exit\n");}module_init(user_info_init);module_exit(user_info_exit);MODULE_LICENSE("Dual BSD/GPL");MODULE_DESCRIPTION("container_of over a list_head list");
예제 3

위 코드는 container_of를 활용하는 예제 코드이다. 이와 같이 container_of는 그 자체로서는 별 기능이 없다. 그리고 container_of에 넘기는 타입과 멤버 이름은 ptr이 실제로 속한 상위 구조체의 타입, 멤버 이름과 일치해야 올바르게 작동한다.

high low users[1] 0xffffffffc0203138 0xffffffffc0203131 0xffffffffc0203130 0xffffffffc0203128 ptr: pos 0xffffffffc0203120 user == &users[0] 0xffffffffc0203020 ... (users[1], bob) 00 00 00 00 00 00 00 (padding, +0x111) 0x14 (age, +0x110) 0xffffffffc0203370 (list.prev, +0x108) 0xffffffffc0203238 (list.next, +0x100) "alice" (username, +0x000) type struct user_info (users[0]) member list (+0x100 .. +0x10f) offsetof (type, member) = 0x100 user = container_of(pos, struct user_info, list); (struct user_info *)(0xffffffffc0203120 - 0x100) = 0xffffffffc0203020 user == &users[0]
다이어그램 1

다이어그램 1과 같이 struct user_info는 총 280바이트이고, 맨 뒤에 7바이트 패딩이 붙는다. user = container_of(pos, struct user_info, list);는 pos에서 바깥 구조체의 주소를 역산한다. pos는 user_info 안에 든 list 멤버의 주소이고, list의 타입은 struct list_head이다. 이 주소에서 list의 오프셋인 offsetof(struct user_info, list), 즉 0x100을 빼면 list를 품은 user_info의 시작 주소가 나온다.

list_head를 쓰는 구조체는 user_info만이 아니다. struct task_struct는 tasks 멤버로 모든 프로세스를, struct module은 list 멤버로 적재된 모듈을, struct net_device는 dev_list 멤버로 같은 네트워크 네임스페이스의 장치를 잇는다. 세 멤버 tasks, list, dev_list의 타입은 모두 list_head이다. 리스트를 잇고, 끊고, 순회하는 연산(list_add_tail, list_del, list_for_each)은 list_head만 다룬다. 원소의 데이터가 필요할 때만 container_of로 바깥 구조체를 구한다. 그래서 구조체마다 next() 같은 순회 함수나 getter 같은 접근 함수를 따로 만들 필요가 없다. 어떤 구조체든 list_head를 멤버로 품기만 하면 같은 인터페이스를 그대로 쓴다. 따라서 구조체는 자기 정보를 온전하게 담는 데에만 집중하고, 탐색과 값 추출은 이미 있는 인터페이스에 맡기면 된다.

다이어그램 2는 예제 모듈의 리스트를 이 관점에서 그린 것이다. list_for_each는 next를 따라 list_head만 옮겨 다니고, 각 원소의 바깥 구조체는 container_of가 offsetof만큼 거슬러 올라가 구한다.

users[0] username "alice" next prev age 20 padding offsetof = 0x100 users[1] username "bob" next prev age 30 padding next users[2] username "carol" next prev age 40 padding next next prev user_info_list (head) next next list_for_each(pos, &user_info_list) user = container_of(pos, struct user_info, list);
다이어그램 2