본문 바로가기
임베디드 개발/임베디드 리눅스

Linux Kernel ] Container_of (+ C언어의 상속)

by eteo 2025. 2. 6.
반응형

 

 

Container_of

Container_of는 리눅스 커널에서 매우 유용하게 자주 쓰이는 매크로로 구조체 멤버의 포인터로부터 해당 멤버가 속한 구조체의 시작 주소를 얻는데 사용된다. 

 

Container_of 매크로의 정의는 다음과 같으며, 인자로 '구조체 멤버의 포인터', '전체 구조체의 타입', '구조체 멤버의 이름'을 받고 '전체 구조체의 시작 주소'를 반환한다.

 

#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 구현

 

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - (size_t)&(((type *)0)->member)))

 

동작 원리는다음과 같다.

  1. (type *)0 : 구조체의 시작 주소를 0으로 가정한다.
  2. (size_t)&(((type *)0)->member) : 멤버의 주소를 정수형으로 반환한다. 가상 주소 0에서 시작하는 구조체의 멤버의 위치는 구조체 시작 주소와 멤버 주소 사이의 바이트 오프셋과 동일하다. 이 과정은 offsetof() 매크로와 동일한 동작을 수행한다.
  3. ((char *)(ptr) - (size_t)&(((type *)0)->member)) : 인수로 받은 멤버 주소에서 바이트 오프셋을 빼서 전체 구조체의 시작 주소를 얻는다.
  4. ((type *)((char *)(ptr) - (size_t)&(((type *)0)->member))) : 마지막으로 type *로 캐스팅하고 반환한다.

 

 

사용 예시.

#include <stdio.h>

struct student {
    short id;          
    const char *name;
    int age;          
};

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - (size_t)&(((type *)0)->member)))

void foo(const char **name_ptr) {
    struct student *ps = container_of(name_ptr, struct student, name);

    printf("Using container_of:\n");
    printf("ID: %d, Name: %s, Age: %d\n", ps->id, ps->name, ps->age);
}

int main() {
    struct student s = {1, "Alice", 20};
    
    foo(&s.name);

    return 0;
}

 

 

foo 함수에서는 구조체의 멤버의 주소를 인수로 받아서 이를통해 전체 구조체에 접근하는 예시이다.

 

 

 

아래와 같이 구조체를 생성하고 직접 오프셋을 계산하는 방법도 있다. 다만 이런 방식은 매크로 함수와 달리 구조체 이름과 멤버 이름을 매개변수로 받을 수 없으므로 함수화는 어렵다.

 

struct student s;
size_t offset = (char *)&s.name - (char *)&s;
struct student *ps = (struct student *)((char *)name_ptr - offset);
printf("Using container_of:\n");
printf("ID: %d, Name: %s, Age: %d\n", ps->id, ps->name, ps->age);

 

 

 

 

언제 쓰이나?

리눅스 커널 소스는 C 언어 기반이기 때문에 C++처럼 상속을 직접 지원하지 않는다. 대신, 부모 구조체를 자식 구조체의 멤버로 포함시키는 방식으로 상속과 유사한 구조를 구현한다.

 

예를 들어, 리눅스 장치 모델에서 struct pci_dev나 struct usb_device는 내부에 struct device dev; 멤버를 부모 개념으로 가지고 있다. 이를 통해 장치의 전원 관리, 참조 계수, kobject 기반 계층 구조와 같은 공통 기능을 상속받아 공유한다.

또한 드라이버를 작성할 때도, 보통 장치를 제어하는 데 필요한 모든 걸 모아둔 사용자 정의 구조체 안에 장치 구조체를 부모 개념으로 멤버로 포함시킨다.

 

다음과 같은 구조체가 있고,

 

struct my_device {
    struct device dev;   // 부모
    int custom_data;     // 자식 고유 멤버
};

 

위 구조에서 어떤 콜백 함수가 struct device *만 인자로 받는다고 가정해보자.
하지만 실제로 우리가 다루고 싶은 것은 struct my_device일 수 있다.

이때 container_of를 사용하면 부모 멤버(dev)의 주소를 기반으로, 그것을 포함하고 있는 전체 구조체(struct my_device)의 시작 주소를 얻을 수 있다.

 

static void my_callback(struct device *dev)
{
    struct my_device *mydev;
    mydev = container_of(dev, struct my_device, dev);

    mydev->custom_data = 0;
}

 

즉, 부모 타입의 주소만 알고 있고 좀 더 구체적 타입에 접근해야하는 상황에서 container_of 매크로가 사용된다.

반응형