Embedded : : Linux/: : Device Tree

[Device Tree] 14. struct device_node / struct property 내부 구조

Jay.P Morgan 2026. 3. 4. 17:03

 

 

 

  14.  struct device_node / struct property 내부 구조

 

디바이스 트리가 바이너리(DTB) 상태를 벗어나 커널 메모리에 올라오면, 각 노드와 속성들은 struct device_node struct property라는 C 구조체 객체로 변환됩니다. 이들은 서로 포인터로 연결되어 거대한 양방향 연결 리스트(Linked List) 형태의 트리 구조를 형성합니다.

 

  14.1  struct device_node (노드 객체)

 

이 구조체는 디바이스 트리의 각 "노드"를 나타냅니다. 단순히 이름만 가진 것이 아니라, 트리 내에서의 위치 정보와 속성 리스트를 모두 포함합니다.

/* include/linux/of.h (주요 필드 요약) */
struct device_node {
    const char *name;              /* 노드의 이름 (예: "serial") */
    const char *full_name;         /* 전체 경로 (예: "/soc/serial@1c28000") */
    struct property *properties;   /* 이 노드가 가진 속성들의 리스트 (헤드) */
    struct device_node *parent;    /* 부모 노드 포인터 */
    struct device_node *child;     /* 첫 번째 자식 노드 포인터 */
    struct device_node *sibling;   /* 형제 노드 포인터 */
    phandle phandle;               /* 노드 간 참조를 위한 고유 ID */
    unsigned long _flags;          /* 노드 상태 (활성, 해제 등) */
    void *data;                    /* 드라이버가 임시로 쓸 수 있는 데이터 포인터 */
};

 

핵심 관계망 (Tree Navigation)

  • parent, child, sibling: 이 포인터들 덕분에 커널은 트리 전체를 자유롭게 탐색할 수 있습니다.
  • properties: 해당 노드에 정의된 모든 reg, compatible 등의 속성 데이터가 담긴 struct property 리스트의 시작점을 가리킵니다.

 

 

 

  14.2  struct property (속성 객체)

 

노드 내부에 정의된 개별 속성(Key-Value 쌍)을 나타냅니다.

/* include/linux/of.h */
struct property {
    char *name;                /* 속성 이름 (예: "compatible") */
    int length;                /* 데이터의 길이 (bytes) */
    void *value;               /* 실제 데이터 저장 공간 */
    struct property *next;     /* 다음 속성을 가리키는 포인터 (단방향 리스트) */
    unsigned long _flags;      /* 속성 상태 플래그 */
};

 

데이터 해석 방식

  • value: 이 포인터는 타입이 void *입니다. DTB에서 넘어온 바이너리 데이터를 그대로 들고 있기 때문에, 드라이버가 문자열로 읽을지(of_property_read_string), 숫자 배열로 읽을지(of_property_read_u32_array)에 따라 다르게 해석됩니다.

 

 

  14.3  구조체 간 연결 방식 (In-Memory Layout)

 

커널 메모리에서 이 둘은 다음과 같이 얽혀 있습니다.

  1. 루트 노드(of_root)에서 시작하여 child 포인터를 타고 내려가면 하위 노드들을 만납니다.
  2. 각 노드 내부의 properties 포인터는 첫 번째 속성 객체를 가리킵니다.
  3. 첫 번째 속성의 next 포인터는 두 번째 속성을 가리키는 식으로 단방향 연결 리스트를 구성합니다.

 

  14.4  구조화의 장점

 

 빠른 탐색: 바이너리 형태(FDT)는 특정 노드를 찾으려면 처음부터 끝까지 파싱해야 하지만, 구조체로 펼쳐놓으면(Unflatten) 포인터만 따라가면 되므로 매우 빠릅니다.

  1. 동적 수정 (Overlays): 시스템 실행 중에 새로운 하드웨어가 추가되면, 새로운 device_node 구조체를 만들어 기존 트리의 sibling이나 child 포인터에 연결만 해주면 됩니다.
  2. 추상화: 드라이버 개발자는 복잡한 바이너리 오프셋을 계산할 필요 없이, node->properties를 순회하며 필요한 정보를 편하게 꺼내 쓸 수 있습니다.

 

 

💡 실무 디버깅 팁

커널 소스에서 특정 노드의 속성을 수동으로 순회하고 싶을 때 다음과 같은 매크로를 자주 사용합니다.

struct property *pp;
// 노드(np)의 모든 속성을 하나씩 순회하며 이름을 출력
for_each_property_of_node(np, pp) {
    printk("Property name: %s\n", pp->name);
}

 

 

 

  14.5  DTB를 메모리상에서 어떻게 객체화 하는지: 데이터의 구조

 

unflatten_device_tree() 완료 후 커널 메모리에 존재하는 자료구조입니다. 모든 of_* API는 이 구조체를 통해 DT 정보에 접근합니다.

 

 
  /* include/linux/of.h */
 
  struct device_node {
         const char *name;                          /* 노드 이름 (@ 앞 부분) */
         phandle phandle;                            /* 고유 식별자 (phandle 프로퍼티 값) */
         const char *full_name;                   /* 전체 경로명 또는 name[@unit-address] */
         struct fwnode_handle fwnode;     /* 펌웨어 노드 추상화 (DT/ACPI 통합) */
 
         struct property *properties;          /* 프로퍼티 연결 리스트 헤드 */
         struct property *deadprops;         /* 제거된 프로퍼티 (overlay undo용) */
 
         /* 트리 탐색 포인터 */
         struct device_node *parent;         /* 부모 노드 */
         struct device_node *child;            /* 첫 번째 자식 */
         struct device_node *sibling;         /* 다음 형제 */
 
  #if defined(CONFIG_OF_KOBJ)
         struct kobject kobj;                        /* sysfs 표현 (/sys/firmware/devicetree/) */
  #endif
         unsigned long _flags;                    /* OF_POPULATED, OF_DETACHED 등 */
         void *data;                                      /* 드라이버 private 데이터 */
  };
 
  /* 플래그 상수 */
  #define OF_DYNAMIC                 /* overlay로 동적 생성된 노드 */
  #define OF_DETACHED             2  /* 트리에서 분리된 노드 */
  #define OF_POPULATED            /* platform_device가 이미 생성됨 */
  #define OF_POPULATED_BUS  /* 자식 디바이스들도 생성됨 */
 
  struct property {
         char *name;                                     /* 프로퍼티 이름 ("compatible", "reg" 등) */
         int length;                                         /* 값의 바이트 길이 */
         void *value;                                      /* 프로퍼티 값 (raw 바이트) */
         struct property *next;                     /* 같은 노드의 다음 프로퍼티 */
  #if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
         unsigned long _flags;
  #endif
  #if defined(CONFIG_OF_KOBJ)
         struct bin_attribute attr;                 /* sysfs 바이너리 속성 */
  #endif
  };
 
  /* ===== device_node 트리 순회 매크로 ===== */
 
  /* 모든 자식 노드 순회 */
  for_each_child_of_node(parent, child) { ... }
 
  /* available(status != "disabled") 자식만 순회 */
  for_each_available_child_of_node(parent, child) { ... }
 
  /* 특정 compatible을 가진 노드만 순회 */
  for_each_compatible_node(dn, type, compatible) { ... }
 
  /* 특정 프로퍼티를 가진 노드 순회 */
  for_each_node_with_property(dn, prop_name) { ... }
 
  /* of_node 참조 카운팅 */
  struct device_node *np = of_node_get(node);   /* refcount++ */
  of_node_put(np);                                                    /* refcount-- */
  /* for_each_* 매크로는 루프 내에서 자동으로 get/put 처리
   * 주의: break로 루프를 탈출하면 of_node_put()을 수동 호출해야 함! */
 
  /* ===== 노드 → platform_device 변환 흐름 =====
   *
   * 1. unflatten_device_tree() → device_node 트리 구축
   * 2. of_platform_default_populate()
   *     → 루트의 direct children 중 compatible 있는 노드를 platform_device로 생성
   *     → "simple-bus", "simple-mfd", "isa", "arm,amba-bus" compatible의 노드는
   *          재귀적으로 자식도 platform_device로 생성
   * 3. 각 platform_device의 compatible과 등록된 platform_driver의 of_match_table 비교
   * 4. 매칭 성공 → driver->probe() 호출
   * 5. probe 시 의존성(clk, regulator 등) 미충족이면 -EPROBE_DEFER 반환
   *     → 나중에 재시도 (deferred probe)
   */
 

 

위 코드는 리눅스 커널이 디바이스 트리(DT) 바이너리를 메모리 상에서 어떻게 객체화하고 관리하는지를 보여주는 가장 핵심적인 데이터 구조입니다.

이 구조체들이 메모리에서 어떻게 얽혀 있고, 실제 드라이버까지 어떻게 이어지는지 구조적으로 설명해 드릴게요.

 

 

 

1. struct device_node: 트리의 중심점

device_node는 트리의 '마디(Node)'입니다. 이 구조체의 가장 큰 특징은 가족 관계 포인터를 통해 거대한 계층 구조를 형성한다는 점입니다.

  • 가족 관계 (Tree Navigation): * parent, child, sibling 포인터를 사용하여 상/하/좌/우로 트리를 탐색합니다.
    • child는 첫 번째 자식만 가리키며, 나머지 형제들은 sibling을 통해 단방향 리스트로 연결됩니다.
  • fwnode (펌웨어 추상화): 최근 커널은 DT뿐만 아니라 ACPI 등 다른 펌웨어 인터페이스도 지원합니다. fwnode_handle은 이들을 통합하여 드라이버가 펌웨어 종류에 상관없이 정보를 읽을 수 있게 돕는 인터페이스입니다.
  • data (드라이버의 사물함): 드라이버가 이 노드에 특정 데이터를 임시로 저장해두고 싶을 때 사용하는 자유 포인터입니다.

 

 

2. struct property: 노드의 세부 정보

property는 노드 안에 들어있는 '속성(Key-Value)'입니다. device_node 하나에는 여러 개의 property가 붙을 수 있습니다.

  • 연결 리스트 구조: device_node의 properties 필드가 첫 번째 속성을 가리키고, 각 속성은 next 포인터를 통해 줄줄이 비엔나 소시지처럼 연결됩니다.
  • value (Raw Data): DTB 바이너리에서 추출된 원시 데이터가 저장됩니다.
    • status = "okay"라면 문자열 데이터가,
    • reg = <0x100 0x10>이라면 바이너리 정수 데이터가 들어있습니다.
  • deadprops: 런타임에 디바이스 트리를 수정하는 Overlay 기능 때문에 존재합니다. 속성을 삭제해도 즉시 메모리에서 날리지 않고 여기에 보관했다가, Overlay를 취소(Revert)할 때 다시 복구합니다.

 

 

3. 노드 → 디바이스 변환 (Populate) 메커니즘

코드 하단에 설명된 "Populate" 과정은 DT를 실제 리눅스 장치 모델(struct device)로 승격시키는 매우 중요한 단계입니다.

 

왜 모든 노드가 장치가 되지 않나요?

디바이스 트리에는 하드웨어뿐만 아니라 단순한 설정 정보(예: opp-table, chosen)도 포함되어 있습니다. 커널은 다음과 같은 규칙으로 장치를 생성합니다.

  1. Compatible 검사: compatible 속성이 있는 노드 위주로 platform_device를 생성합니다.
  2. Bus 재귀 탐색: 루트 노드의 직계 자식들은 장치로 만들어집니다. 하지만 자식의 자식(손자) 노드들은 기본적으로 생성되지 않습니다.
    • 단, 부모 노드가 simple-bus 같은 "버스 성격"의 compatible을 가지고 있다면 커널은 "아, 이 아래에는 내가 관리할 장치들이 더 있구나!"라고 판단하여 재귀적으로 자식 노드들도 장치로 등록합니다.

 

 

4. 실무적인 주의사항: 참조 카운팅 (Reference Counting)

코드 예시의 of_node_get / of_node_put은 메모리 관리를 위해 필수적입니다.

  • 왜 필요한가? 어떤 드라이버가 특정 노드를 참조하고 있는데, 갑자기 Overlay가 해제되어 노드가 사라지면 커널 패닉이 발생합니다.
  • 매크로의 편리함: for_each_child_of_node 같은 매크로는 다음 노드로 넘어갈 때 이전 노드의 카운트를 자동으로 깎아줍니다.
  • 🚨 치명적인 실수: 루프 중간에 break나 return으로 빠져나올 경우, 매크로가 마지막 노드의 put을 수행하지 못합니다. 이때는 반드시 수동으로 of_node_put()을 해줘야 메모리 누수를 막을 수 있습니다.

 

 

💡정리하자면

  • device_node는 트리의 구조(뼈대)를 담당하고,
  • property는 노드의 데이터(살점)를 담당하며,
  • platform_device는 이들을 바탕으로 커널이 실제 제어할 수 있는 객체가 된 상태입니다.

 

 

 

참고: https://www.minzkn.com/linuxkernel/pages/device-tree.html#dt-compile