plist (Priority-Sorted List)

리눅스 커널의 우선순위 정렬 이중 연결 리스트(Priority-Sorted Linked List)인 plist를 심층 분석합니다. include/linux/plist.hlib/plist.c에 정의된 이 자료구조는 일반 list_head와 달리 두 개의 리스트(prio_list와 node_list)를 결합하여 우선순위 기반 삽입을 O(K) (K = 고유 우선순위 수)에 수행합니다. RT-Mutex(Real-Time Mutex) 대기자 큐, PM QoS(Power Management Quality of Service) 레이턴시(Latency) 제약, Futex PI(Priority Inheritance) 등 커널의 핵심 실시간 서브시스템에서 활용됩니다.

전제 조건: Linked List, 동기화 기법 문서를 먼저 읽으세요. plist는 struct list_head 이중 연결 리스트 위에 구축되므로, list_add(), list_del(), container_of() 매크로의 기본 동작을 먼저 이해해야 합니다.
일상 비유: plist는 병원 응급실 대기열과 같습니다. 환자가 도착하면 증상의 심각도(우선순위)에 따라 대기열의 올바른 위치에 삽입됩니다. 같은 심각도의 환자끼리는 도착 순서(FIFO)를 유지합니다. 간호사는 항상 가장 심각한 환자(리스트의 첫 번째 노드)를 바로 호출할 수 있습니다. prio_list는 각 심각도 등급의 대표 환자만 연결한 "빠른 색인"이고, node_list는 모든 환자가 순서대로 나열된 "전체 목록"입니다.

핵심 요약

  • 이중 리스트 구조 — 각 노드는 prio_list(고유 우선순위 대표 노드 연결)와 node_list(전체 노드 정렬 연결) 두 개의 list_head를 보유합니다.
  • O(K) 삽입 — 삽입 시 prio_list만 순회하므로, 전체 노드 수 N이 아닌 고유 우선순위 수 K에 비례하는 시간이 소요됩니다 (K ≤ N).
  • O(1) 최솟값 접근plist_first()로 가장 높은 우선순위(가장 작은 prio 값) 노드에 상수 시간 접근이 가능합니다.
  • FIFO 동일 우선순위 — 같은 우선순위의 노드는 삽입 순서가 유지되어 공정성(Fairness)을 보장합니다.
  • 핵심 활용처 — RT-Mutex 대기자 큐(rt_mutex_waiter), PM QoS 제약(pm_qos_constraints), Futex PI 상태 관리에 사용됩니다.

단계별 이해

  1. list_head 복습
    커널의 기본 이중 연결 리스트 struct list_headcontainer_of() 매크로를 복습합니다.
  2. plist의 필요성 파악
    일반 list_head에 우선순위 기반 정렬 삽입을 추가할 때 O(N) 문제가 발생하는 이유를 이해합니다.
  3. 이중 리스트 메커니즘 학습
    prio_list와 node_list의 분리 설계가 어떻게 O(K) 삽입을 달성하는지 파악합니다.
  4. API와 삽입 알고리즘 추적
    plist_add()의 내부 로직을 소스 코드 수준에서 따라갑니다.
  5. 실전 활용 사례 분석
    RT-Mutex, PM QoS, Futex에서 plist가 어떻게 사용되는지 확인합니다.
소스 위치: include/linux/plist.h (헤더, 인라인 함수), lib/plist.c (plist_add, plist_del, plist_requeue 구현). plist는 2006년 Inaky Perez-Gonzalez가 PI futex 지원을 위해 도입했습니다. 종합 목록은 참고자료 섹션을 참고하세요.

개요: list_head와의 차이

커널의 표준 struct list_head는 삽입/삭제가 O(1)이지만, 정렬 상태를 유지하지 않습니다. 우선순위 기반 대기열이 필요한 서브시스템에서 list_head를 사용하면, 삽입 시마다 올바른 위치를 찾기 위해 전체 리스트를 순회해야 합니다. 이는 O(N) 복잡도를 가집니다.

plist는 이 문제를 두 개의 리스트를 결합하는 방식으로 해결합니다. 핵심 아이디어는 다음과 같습니다.

비교 항목list_head (정렬 삽입)plist
삽입 복잡도O(N) — 전체 노드 순회O(K) — 고유 우선순위만 순회
삭제 복잡도O(1)O(1)
최솟값 접근O(1) — head->nextO(1) — plist_first()
메모리 오버헤드2개 포인터4개 포인터 + int prio
동일 우선순위 순서FIFO (삽입 구현에 따라)FIFO 보장

실제 커널 사용 패턴에서 대기자의 우선순위 종류 K는 전체 노드 수 N보다 훨씬 적습니다. 예를 들어, RT-Mutex 대기열에서 100개 태스크(Task)가 대기 중이더라도 고유 우선순위 수는 보통 10~20개 이내입니다. 이 경우 plist는 list_head 대비 5~10배 빠른 삽입을 제공합니다.

list_head 정렬 삽입 vs plist 삽입 비교 list_head 정렬 삽입: O(N) prio=5 prio=5 prio=10 prio=20 삽입 위치를 찾으려면 모든 노드를 순회 N=100 이면 최악 100회 비교 plist 삽입: O(K) prio=5 prio=10 prio=20 prio_list (고유 우선순위만) prio_list만 순회하여 삽입 위치 결정 N=100, K=3 이면 최대 3회 비교 복잡도 비교 (N=전체 노드, K=고유 우선순위) list_head 정렬 삽입: O(N) | plist 삽입: O(K) | K ≤ N RT-Mutex 대기열: N=100 태스크, K=10~20 우선순위 → 5~10배 빠른 삽입 삭제: 두 방식 모두 O(1) | 최솟값 접근: 두 방식 모두 O(1)

plist 구조체

plist의 핵심은 두 개의 구조체입니다: struct plist_head(리스트 헤드)와 struct plist_node(개별 노드). 이 구조체들은 include/linux/plist.h에 정의되어 있습니다.

struct plist_head

/* include/linux/plist.h */
struct plist_head {
    struct list_head node_list;
};
코드 설명

struct plist_head는 놀라울 정도로 단순합니다. 단일 list_head 필드 하나만 보유합니다.

  • node_list: 모든 노드가 우선순위 순서로 연결되는 마스터 리스트의 앵커(Anchor)입니다. plist_first()는 이 리스트의 첫 번째 엔트리를 반환하여 O(1) 최솟값 접근을 제공합니다.

초기 구현에서는 prio_list 앵커도 포함했지만, 커널 6.x에서는 prio_list가 각 노드의 plist_node.prio_list끼리 직접 연결되는 구조로 단순화되었습니다. 헤드의 node_list만으로 전체 리스트를 관리할 수 있습니다.

struct plist_node

/* include/linux/plist.h */
struct plist_node {
    int                 prio;
    struct list_head    prio_list;
    struct list_head    node_list;
};
코드 설명

struct plist_node는 세 개의 필드를 갖습니다.

  • prio: 노드의 우선순위 값입니다. 값이 작을수록 높은 우선순위를 나타냅니다. RT-Mutex에서는 태스크의 prio 필드 값이 그대로 사용됩니다.
  • prio_list: 같은 우선순위의 첫 번째(대표) 노드만 이 리스트에 연결됩니다. "빠른 색인" 역할을 하며, 삽입 시 이 리스트를 순회하여 삽입 위치를 O(K)에 결정합니다.
  • node_list: 모든 노드가 우선순위 오름차순으로 연결됩니다. 같은 우선순위 내에서는 FIFO(First-In-First-Out) 순서를 유지합니다. plist_for_each_entry() 등의 순회 매크로는 이 리스트를 사용합니다.

각 노드는 20바이트(32비트) 또는 36바이트(64비트)의 메모리를 사용합니다. list_head(8/16바이트) 대비 2.5~4.5배이지만, 이 오버헤드는 빠른 삽입 성능으로 보상됩니다.

struct plist_node 메모리 레이아웃 (64비트) struct plist_node (36 bytes on 64-bit) prio int (4 bytes) pad (4) prio_list (struct list_head) *prev (8 bytes) *next (8 bytes) node_list (struct list_head) *prev (8 bytes) *next (8 bytes) 우선순위 값 작을수록 높은 우선순위 고유 우선순위 대표 노드만 연결 "빠른 색인" — 삽입 시 O(K) 탐색 모든 노드가 우선순위순 연결 "전체 목록" — 순회/삭제에 사용 메모리 오버헤드 비교 list_head: 16 bytes (64-bit) | plist_node: 36 bytes (64-bit) | 2.25배 증가, O(N)→O(K) 삽입으로 보상

초기화 매크로

/* plist_head 초기화 */
#define PLIST_HEAD_INIT(head) { .node_list = LIST_HEAD_INIT((head).node_list) }
#define PLIST_HEAD(head) struct plist_head head = PLIST_HEAD_INIT(head)

static inline void plist_head_init(struct plist_head *head)
{
    INIT_LIST_HEAD(&head->node_list);
}

/* plist_node 초기화 */
#define PLIST_NODE_INIT(node, __prio) {             \
    .prio  = (__prio),                              \
    .prio_list = LIST_HEAD_INIT((node).prio_list),   \
    .node_list = LIST_HEAD_INIT((node).node_list),   \
}

static inline void plist_node_init(struct plist_node *node, int prio)
{
    node->prio = prio;
    INIT_LIST_HEAD(&node->prio_list);
    INIT_LIST_HEAD(&node->node_list);
}
코드 설명

초기화 시 prio_listnode_list 모두 자기 자신을 가리키도록 설정됩니다. 이는 커널 list_head의 표준 빈 리스트 표현입니다.

  • PLIST_HEAD_INIT / plist_head_init: 헤드의 node_list를 빈 리스트로 초기화합니다.
  • PLIST_NODE_INIT / plist_node_init: 노드의 prio 값을 설정하고, 두 리스트를 빈 상태로 초기화합니다. prio_list가 자기 자신을 가리키면 "이 노드는 어떤 plist에도 속하지 않음"을 의미합니다.

이중 리스트 메커니즘

plist의 핵심 설계는 prio_listnode_list라는 두 개의 독립적인 연결 리스트를 하나의 노드에 결합한 것입니다. 이 설계가 어떻게 O(K) 삽입을 달성하는지 상세히 분석합니다.

node_list: 전체 노드 정렬 리스트

node_list는 plist에 속한 모든 노드가 우선순위 오름차순으로 연결됩니다. 같은 우선순위의 노드는 삽입 순서(FIFO)를 유지합니다. 이 리스트는 일반 list_head의 이중 연결 리스트와 동일하게 동작하며, 헤드의 node_list가 앵커(Anchor) 역할을 합니다.

prio_list: 고유 우선순위 색인 리스트

prio_list는 각 고유 우선순위 그룹의 첫 번째(대표) 노드만 연결합니다. 같은 우선순위의 두 번째 이후 노드는 prio_list에 연결되지 않고, prio_list가 자기 자신을 가리키는 상태를 유지합니다.

삽입 알고리즘은 prio_list를 순회하여 새 노드의 우선순위보다 높은(값이 큰) 첫 번째 대표 노드를 찾습니다. 이 순회에서 방문하는 노드 수는 고유 우선순위 수 K에 비례하므로, 전체 노드 수 N과 무관하게 O(K) 삽입이 가능합니다.

plist 이중 리스트 구조 (prio=5, 5, 10, 10, 10, 20) plist_head node_list prio_list 연결 node_list 연결 prio_list 대표 노드 노드 A prio = 5 prio_list 대표 node_list ↓ 노드 B prio = 5 prio_list: self 노드 C prio = 10 prio_list 대표 node_list ↓ 노드 D prio = 10 prio_list: self 노드 E prio = 10 prio_list: self 노드 F prio = 20 prio_list 대표 node_list 순서: A(5) → B(5) → C(10) → D(10) → E(10) → F(20) prio_list 순서: A(5) ─── C(10) ─── F(20) (대표 노드만) prio_list 순회 = 3회 (K=3) vs node_list 순회 = 6회 (N=6)

대표 노드 판별

plist 코드에서 노드가 자신의 우선순위 그룹에서 "대표"인지 판별하는 방법은 prio_list가 비어있지 않은지(자기 자신을 가리키지 않는지) 확인하는 것입니다.

/* 대표 노드 여부 확인: prio_list가 연결되어 있으면 대표 */
static inline int plist_node_on_prio_list(struct plist_node *node)
{
    return !list_empty(&node->prio_list);
}

/* 노드가 plist에 속해있는지 확인 */
static inline int plist_node_empty(const struct plist_node *node)
{
    return list_empty(&node->node_list);
}
코드 설명
  • prio_list가 비어있지 않음 = 이 노드는 prio_list에 연결된 대표 노드입니다. 삭제 시 후속 노드에게 대표 역할을 승계해야 합니다.
  • prio_list가 비어있음(자기 자신 가리킴) = 이 노드는 같은 우선순위 그룹의 비대표 노드입니다. 삭제 시 단순히 node_list에서 제거하면 됩니다.
  • node_list가 비어있음 = 이 노드는 어떤 plist에도 속하지 않습니다.

핵심 API

plist는 include/linux/plist.h에 인라인(Inline) 함수와 매크로로 정의된 API와, lib/plist.c에 구현된 핵심 함수로 구성됩니다.

API 전체 목록

함수/매크로복잡도설명
plist_head_init(head)O(1)plist_head를 빈 리스트로 초기화합니다.
plist_node_init(node, prio)O(1)plist_node를 주어진 우선순위로 초기화합니다.
plist_add(node, head)O(K)우선순위 순서를 유지하며 노드를 삽입합니다.
plist_del(node, head)O(1)노드를 리스트에서 제거합니다.
plist_requeue(node, head)O(K)노드의 우선순위가 변경된 후 올바른 위치로 재배치합니다.
plist_head_empty(head)O(1)리스트가 비어있는지 확인합니다.
plist_node_empty(node)O(1)노드가 어떤 리스트에도 속하지 않는지 확인합니다.
plist_first(head)O(1)가장 높은 우선순위(최소 prio) 노드를 반환합니다.
plist_last(head)O(1)가장 낮은 우선순위(최대 prio) 노드를 반환합니다.
plist_first_entry(head, type, member)O(1)첫 번째 노드를 포함하는 구조체 포인터를 반환합니다.
plist_last_entry(head, type, member)O(1)마지막 노드를 포함하는 구조체 포인터를 반환합니다.
plist_for_each(pos, head)O(N)모든 노드를 우선순위 순서대로 순회합니다.
plist_for_each_safe(pos, n, head)O(N)안전한 순회 (순회 중 삭제 가능).
plist_for_each_entry(pos, head, mem)O(N)container_of 버전의 순회입니다.
plist_for_each_entry_safe(pos, n, head, m)O(N)안전한 container_of 순회입니다.

plist_first / plist_last

/* include/linux/plist.h */
static inline struct plist_node *plist_first(const struct plist_head *head)
{
    return list_entry(head->node_list.next,
                       struct plist_node, node_list);
}

static inline struct plist_node *plist_last(const struct plist_head *head)
{
    return list_entry(head->node_list.prev,
                       struct plist_node, node_list);
}

#define plist_first_entry(head, type, member) \
    container_of(plist_first(head), type, member)

#define plist_last_entry(head, type, member) \
    container_of(plist_last(head), type, member)
코드 설명
  • plist_first: head->node_list.next는 가장 작은 prio 값을 가진 노드의 node_list를 가리킵니다. list_entry()(= container_of())로 plist_node 포인터를 역산합니다.
  • plist_last: head->node_list.prev는 가장 큰 prio 값을 가진 노드를 가리킵니다.
  • plist_first_entry / plist_last_entry: plist_node를 내장한 외부 구조체의 포인터를 직접 얻는 편의 매크로입니다.

순회 매크로

/* 모든 노드를 우선순위 순서대로 순회 */
#define plist_for_each(pos, head) \
    list_for_each_entry(pos, &(head)->node_list, node_list)

/* 안전한 순회 (순회 중 삭제 가능) */
#define plist_for_each_safe(pos, n, head) \
    list_for_each_entry_safe(pos, n, &(head)->node_list, node_list)

/* container_of 순회 */
#define plist_for_each_entry(pos, head, mem) \
    list_for_each_entry(pos, &(head)->node_list, mem.node_list)

/* 안전한 container_of 순회 */
#define plist_for_each_entry_safe(pos, n, head, m) \
    list_for_each_entry_safe(pos, n, &(head)->node_list, m.node_list)
코드 설명

순회 매크로는 모두 node_list를 기준으로 동작합니다. prio_list는 삽입 최적화용이므로 순회에는 사용되지 않습니다.

  • plist_for_each: list_for_each_entrynode_list에 적용합니다. posstruct plist_node *입니다.
  • plist_for_each_entry: plist_node를 내장한 외부 구조체를 직접 순회합니다. mem은 외부 구조체 내 plist_node 필드명입니다.
  • _safe 변형: 다음 노드를 미리 저장하므로, 순회 중 현재 노드를 plist_del()로 삭제해도 안전합니다.

삽입 알고리즘

plist_add()는 plist의 가장 핵심적인 함수입니다. O(K) 복잡도로 우선순위 정렬을 유지하면서 노드를 삽입합니다. lib/plist.c에 구현되어 있습니다.

plist_add() 소스 코드

/* lib/plist.c */
void plist_add(struct plist_node *node, struct plist_head *head)
{
    struct plist_node *first, *iter, *prev = NULL;
    struct list_head *node_next = &head->node_list;

    plist_check_head(head);
    WARN_ON(!plist_node_empty(node));
    WARN_ON(!list_empty(&node->prio_list));

    /* 빈 리스트이면 바로 삽입 */
    if (plist_head_empty(head))
        goto ins_node;

    first = plist_first(head);

    /* prio_list를 순회하여 삽입 위치 결정 (O(K)) */
    do {
        /* 현재 대표 노드보다 우선순위가 높으면(prio가 작으면) */
        if (node->prio < first->prio) {
            node_next = &first->node_list;
            break;
        }

        prev = first;
        /* prio_list의 다음 대표 노드로 이동 */
        iter = list_entry(first->prio_list.next,
                          struct plist_node, prio_list);

        /* prio_list가 한 바퀴 돌아서 첫 노드로 되돌아왔으면 */
        if (&iter->prio_list == &first->prio_list)
            break;

        first = iter;
    } while (1);

ins_node:
    /* node_list에 삽입 */
    list_add_tail(&node->node_list, node_next);

    /* prio_list 연결 결정 */
    if (!prev || prev->prio != node->prio) {
        /* 새로운 고유 우선순위: prio_list에 추가 */
        list_add(&node->prio_list, &iter->prio_list);
    }

    plist_check_head(head);
}
코드 설명

plist_add의 핵심 로직을 단계별로 분석합니다.

  • 1단계 - 빈 리스트 검사: 리스트가 비어있으면 ins_node로 바로 점프하여 첫 번째 노드로 삽입합니다.
  • 2단계 - prio_list 순회: first를 prio_list의 첫 대표 노드로 설정한 후, do-while 루프에서 prio_list를 따라 이동합니다. 새 노드의 prio보다 큰 대표 노드를 찾으면 해당 위치 앞에 삽입합니다.
  • 3단계 - node_list 삽입: list_add_tail()node_next 앞에 삽입합니다. 이는 같은 우선순위 그룹의 마지막 위치에 삽입하여 FIFO 순서를 보장합니다.
  • 4단계 - prio_list 연결: 이전 대표 노드(prev)가 없거나, prev의 우선순위가 새 노드와 다르면 새로운 고유 우선순위이므로 prio_list에 추가합니다. 같은 우선순위가 이미 존재하면 prio_list는 변경하지 않습니다.
plist_add(node prio=10) 삽입 과정 초기 상태: [5] → [5] → [20] A: prio=5 대표 B: prio=5 C: prio=20 대표 NEW: prio=10 삽입할 노드 1단계: prio_list 순회 (O(K)) A: prio=5 C: prio=20 10 < 20 → 여기 앞에 삽입! prio_list에서 A(5) 확인 → 10 ≥ 5 통과 → C(20) 확인 → 10 < 20 발견! 2단계: node_list에 list_add_tail() A: 5 B: 5 NEW: 10 C: 20 C의 node_list 앞에 삽입 (list_add_tail) 3단계: prio_list에 새 대표 노드 등록 A: 5 NEW: 10 C: 20 prev(A)의 prio=5 ≠ 10 → 새로운 고유 우선순위 → prio_list에 추가 최종 상태 node_list: A(5) → B(5) → NEW(10) → C(20) prio_list: A(5) ── NEW(10) ── C(20) prio_list 순회 2회만으로 삽입 위치 결정 완료 (K=2 → K=3)

같은 우선순위 노드 삽입

이미 존재하는 우선순위(예: prio=5가 이미 있는데 prio=5 노드를 추가)의 경우, plist_add()의 동작이 달라집니다.

/* plist_add에서 같은 우선순위 처리 부분 */
if (!prev || prev->prio != node->prio) {
    /* 새로운 고유 우선순위: prio_list에 추가 */
    list_add(&node->prio_list, &iter->prio_list);
}
/* else: 같은 우선순위가 이미 존재 → prio_list 변경 없음
 * node의 prio_list는 초기화 상태(자기 자신 가리킴) 유지
 * node_list에만 삽입되어 FIFO 순서로 배치됨 */
코드 설명
  • prev->prio == node->prio: 같은 우선순위의 대표 노드가 이미 존재합니다. 새 노드는 prio_list에 연결되지 않고, node_list에서만 해당 우선순위 그룹의 끝에 위치합니다.
  • FIFO 보장: list_add_tail()로 삽입하므로, 같은 우선순위의 기존 노드들 뒤에 배치됩니다. 이는 동일 우선순위 태스크 간의 공정한 순서를 보장합니다.

삭제 알고리즘

plist_del()은 노드를 리스트에서 제거합니다. 핵심은 삭제되는 노드가 prio_list의 대표 노드인 경우, 같은 우선순위의 다음 노드에게 대표 역할을 승계해야 하는 것입니다.

/* lib/plist.c */
void plist_del(struct plist_node *node, struct plist_head *head)
{
    plist_check_head(head);

    if (!list_empty(&node->prio_list)) {
        /* 대표 노드 삭제: 후속 노드에 대표 역할 승계 */
        if (node->node_list.next != &head->node_list) {
            struct plist_node *next;

            next = list_entry(node->node_list.next,
                              struct plist_node, node_list);

            /* 다음 노드가 같은 우선순위이면 대표 승계 */
            if (next->prio == node->prio)
                list_add(&next->prio_list, &node->prio_list);
        }
        /* 기존 대표의 prio_list 연결 해제 */
        list_del_init(&node->prio_list);
    }

    /* node_list에서 제거 */
    list_del_init(&node->node_list);

    plist_check_head(head);
}
코드 설명

plist_del의 핵심 로직은 다음과 같습니다.

  • 대표 노드 확인: !list_empty(&node->prio_list)가 참이면, 이 노드는 prio_list에 연결된 대표 노드입니다.
  • 후속 노드 확인: 다음 노드(node_list.next)가 헤드가 아니고, 같은 우선순위를 가지면 대표 역할을 승계합니다. list_add(&next->prio_list, &node->prio_list)로 다음 노드의 prio_list를 현재 노드의 위치에 삽입합니다.
  • 같은 우선순위 노드가 없으면: 다음 노드가 다른 우선순위이거나 리스트 끝이면, prio_list에서 단순 제거하여 해당 우선순위 그룹이 사라집니다.
  • list_del_init: 포인터를 자기 자신으로 재설정하여 plist_node_empty()가 올바르게 동작하도록 합니다.
plist_del: 대표 노드 A(prio=5) 삭제 삭제 전 A: prio=5 대표 (삭제 대상) B: prio=5 비대표 C: prio=10 대표 next(B)의 prio == A의 prio(5) → B에게 대표 역할 승계 삭제 후 A: 제거됨 prio/node_list: self B: prio=5 새 대표! C: prio=10 대표 비교: 유일한 대표 노드 삭제 (다음 노드가 다른 우선순위) C: prio=10 삭제 대상 (유일) next 노드의 prio ≠ 10 → 승계 없음, prio=10 그룹 자체가 prio_list에서 제거 prio_list: A(5) ── F(20) (K: 3 → 2)

plist_requeue: 우선순위 변경 후 재배치

plist_requeue()는 노드의 prio 값이 변경된 후, 리스트에서의 위치를 올바르게 재조정합니다. 단순히 plist_del()plist_add()를 호출하는 것과 의미적으로 동일하지만, 내부적으로는 약간 더 최적화되어 있습니다.

/* lib/plist.c */
void plist_requeue(struct plist_node *node, struct plist_head *head)
{
    struct plist_node *iter;
    struct list_head *node_next = &head->node_list;

    plist_check_head(head);
    BUG_ON(plist_head_empty(head));
    BUG_ON(plist_node_empty(node));

    if (node == plist_last(head))
        return;

    iter = plist_next(node);

    if (node->prio != iter->prio)
        return;

    plist_del(node, head);

    plist_for_each_continue(iter, head) {
        if (node->prio != iter->prio) {
            node_next = &iter->node_list;
            break;
        }
    }
    list_add_tail(&node->node_list, node_next);

    plist_check_head(head);
}
코드 설명

plist_requeue는 RT-Mutex에서 같은 우선순위 대기자의 FIFO 순서를 재조정할 때 사용됩니다.

  • 빠른 탈출 조건: 노드가 리스트의 마지막이거나, 다음 노드와 우선순위가 다르면 (이미 자기 그룹의 끝에 있으면) 아무것도 하지 않고 반환합니다.
  • 재배치 로직: 노드를 삭제한 후, 같은 우선순위 그룹의 끝을 찾아 list_add_tail()로 다시 삽입합니다. 이를 통해 해당 노드가 같은 우선순위 그룹 내에서 FIFO 순서의 맨 뒤로 이동합니다.

RT-Mutex 활용

plist의 가장 중요한 활용처는 RT-Mutex(Real-Time Mutex)의 대기자 큐입니다. RT-Mutex는 우선순위 상속(Priority Inheritance) 프로토콜을 구현하며, 가장 높은 우선순위의 대기자를 O(1)에 조회해야 합니다.

struct rt_mutex_waiter

/* kernel/locking/rtmutex_common.h */
struct rt_mutex_waiter {
    struct rb_node        tree_entry;    /* RB-tree 엔트리 (최근 커널) */
    struct rb_node        pi_tree_entry; /* PI chain RB-tree */
    struct task_struct    *task;         /* 대기 중인 태스크 */
    struct rt_mutex       *lock;         /* 대기 중인 mutex */
    int                   prio;          /* 대기자의 우선순위 */
    u64                   deadline;      /* SCHED_DEADLINE 데드라인 */
};

/* 초기 구현에서는 plist를 사용했습니다:
 * struct plist_node  list_entry;   -- mutex의 대기자 리스트
 * struct plist_node  pi_list_entry; -- 소유자의 PI 리스트
 *
 * 커널 3.x에서 plist → rbtree로 전환되었으나,
 * 개념적으로 동일한 우선순위 정렬 메커니즘입니다.
 */
코드 설명

RT-Mutex 대기자 관리의 역사적 변천입니다.

  • 초기 구현 (2.6.18~3.x): struct plist_node를 직접 사용하여 대기자를 우선순위 순서로 관리했습니다. plist_first()로 가장 높은 우선순위 대기자를 O(1)에 조회하고, 이 우선순위를 mutex 소유자에게 상속했습니다.
  • 현재 구현 (3.x~): SCHED_DEADLINE 지원을 위해 rbtree로 전환되었습니다. plist는 deadline 값에 따른 복합 정렬을 지원하지 않기 때문입니다.
  • PI Chain: 대기자의 pi_list_entry(또는 pi_tree_entry)는 mutex 소유자의 PI 리스트에 연결되어, 소유자가 보유한 모든 mutex에서 가장 높은 대기 우선순위를 추적합니다.

우선순위 상속 체인에서의 plist

RT-Mutex 우선순위 상속 체인 (plist 기반 초기 구현) Task A (prio=120) rt_mutex 소유자 PI boost: prio 120 → 50 pi_waiters (plist): rt_mutex M owner = Task A wait_list (plist): owns Task B (prio=50) waiter.prio = 50 Task C (prio=80) waiter.prio = 80 Task D (prio=80) waiter.prio = 80 rt_mutex M->wait_list (plist) B: prio=50 prio_list 대표 C: prio=80 prio_list 대표 D: prio=80 비대표 우선순위 상속 메커니즘 plist_first(M->wait_list) = Task B (prio=50) Task A의 실행 우선순위를 50으로 부스트(Boost) → 우선순위 역전(Priority Inversion) 방지 Task A가 unlock하면 plist_first()로 Task B를 깨우고, Task A의 우선순위를 원래 120으로 복원

RT-Mutex에서 plist(또는 이를 대체한 rbtree)의 핵심 역할은 다음과 같습니다.

PM QoS 활용

PM QoS(Power Management Quality of Service) 서브시스템은 plist를 사용하여 시스템의 전력 관리 제약 조건을 관리합니다. 여러 드라이버와 사용자 공간 프로그램이 CPU 레이턴시, 네트워크 대기 시간 등의 제약을 등록하면, PM QoS는 가장 엄격한(가장 작은) 제약 값을 빠르게 조회합니다.

struct pm_qos_constraints

/* include/linux/pm_qos.h */
struct pm_qos_constraints {
    struct plist_head list;          /* 요청 리스트 (plist) */
    s32              target_value;   /* 현재 활성 제약 값 */
    s32              default_value;  /* 기본값 */
    s32              no_constraint_value; /* 제약 없음 값 */
    enum pm_qos_type type;          /* MIN/MAX/SUM */
    struct blocking_notifier_head *notifiers;
};

struct pm_qos_request {
    struct plist_node node;           /* plist 노드 */
    s32              priority;        /* = node.prio */
};

/* PM QoS 타입별 집계 방식 */
enum pm_qos_type {
    PM_QOS_UNITIALIZED,
    PM_QOS_MAX,   /* plist_last() 사용: 최대값 반환 */
    PM_QOS_MIN,   /* plist_first() 사용: 최소값 반환 */
    PM_QOS_SUM,   /* 전체 합계 계산 */
};
코드 설명
  • PM_QOS_MIN: CPU 레이턴시 제약에 사용됩니다. 예를 들어, 드라이버 A가 "100us 이하", 드라이버 B가 "50us 이하"를 요청하면, plist_first()가 50us(가장 엄격한 제약)를 O(1)에 반환합니다.
  • PM_QOS_MAX: plist_last()로 최대값을 반환합니다.
  • PM_QOS_SUM: 네트워크 대역폭 요청 등에 사용되며, 전체 리스트를 순회하여 합계를 계산합니다.
  • pm_qos_request.node: plist_node를 내장하여 plist_add() / plist_del()로 제약을 등록/해제합니다.

PM QoS 요청 등록 흐름

/* kernel/power/qos.c (단순화) */
void pm_qos_update_target(struct pm_qos_constraints *c,
                          struct plist_node *node,
                          enum pm_qos_req_action action,
                          s32 value)
{
    s32 prev_value, curr_value;

    prev_value = pm_qos_get_value(c);

    switch (action) {
    case PM_QOS_ADD_REQ:
        plist_node_init(node, value);
        plist_add(node, &c->list);     /* O(K) 삽입 */
        break;
    case PM_QOS_REMOVE_REQ:
        plist_del(node, &c->list);     /* O(1) 삭제 */
        break;
    case PM_QOS_UPDATE_REQ:
        plist_del(node, &c->list);
        plist_node_init(node, value);
        plist_add(node, &c->list);
        break;
    }

    curr_value = pm_qos_get_value(c);   /* O(1): plist_first/last */

    if (prev_value != curr_value)
        blocking_notifier_call_chain(c->notifiers,
                                     0, &curr_value);
}

static s32 pm_qos_get_value(struct pm_qos_constraints *c)
{
    if (plist_head_empty(&c->list))
        return c->no_constraint_value;

    switch (c->type) {
    case PM_QOS_MIN:
        return plist_first(&c->list)->prio;  /* O(1) */
    case PM_QOS_MAX:
        return plist_last(&c->list)->prio;   /* O(1) */
    /* ... */
    }
}
코드 설명

PM QoS에서 plist를 활용하는 전체 흐름입니다.

  • 요청 등록(ADD): 드라이버가 pm_qos_add_request()를 호출하면, 내부적으로 plist_add()가 수행됩니다. prio 값으로 제약 값(예: 레이턴시 us)이 설정됩니다.
  • 요청 업데이트(UPDATE): del + add로 위치를 재조정합니다.
  • 현재 값 조회: PM_QOS_MIN 타입에서는 plist_first()->prio가 현재 가장 엄격한 제약 값을 O(1)에 반환합니다.
  • 변경 알림: 활성 제약 값이 변경되면 notifier를 통해 CPU idle governor 등에 알립니다.

Futex 활용

Futex(Fast Userspace Mutex)의 PI(Priority Inheritance) 변형에서도 plist가 사용됩니다. FUTEX_LOCK_PI 시스템 호출은 커널 내부에서 RT-Mutex 메커니즘을 활용하며, 이 과정에서 plist(또는 rbtree)로 대기자를 관리합니다.

futex_pi_state

/* kernel/futex/futex.h */
struct futex_pi_state {
    struct list_head list;          /* 글로벌 해시 체인 */
    struct rt_mutex_base pi_mutex;  /* RT-Mutex (내부적으로 plist/rbtree 사용) */
    struct task_struct *owner;      /* futex 소유자 */
    refcount_t refcount;            /* 참조 카운트 */
    union futex_key key;            /* futex 식별 키 */
};
코드 설명
  • pi_mutex: rt_mutex_base를 내장하여 대기자를 우선순위 순서로 관리합니다. FUTEX_LOCK_PI로 futex를 잠그려는 태스크가 대기할 때, 이 mutex의 대기 리스트(plist 또는 rbtree)에 추가됩니다.
  • 우선순위 상속: futex 소유자의 우선순위가 가장 높은 대기자의 우선순위로 부스트됩니다. 이는 RT-Mutex의 표준 PI 메커니즘과 동일합니다.
  • 유저 공간 연동: futex 값(유저 공간 32비트 워드)과 커널의 PI 상태 관리를 연결하여, 유저 공간 mutex에서도 우선순위 상속이 가능하도록 합니다.
Futex PI: 유저 공간 ↔ 커널 공간 연동 유저 공간 futex 워드 (32-bit) = owner TID | WAITERS pthread_mutex_lock() FUTEX_LOCK_PI syscall 커널 공간 futex_pi_state pi_mutex (rt_mutex) owner, refcount rt_mutex waiters plist / rbtree 우선순위 정렬 대기열 Task B (prio=50) waiter 엔트리 Task C (prio=80) waiter 엔트리 plist_first() = Task B(50) → futex 소유자(Task A)의 우선순위를 50으로 부스트

성능 분석

plist의 성능을 다른 자료구조와 정량적으로 비교합니다. 핵심 메트릭은 삽입, 삭제, 최솟값 접근의 복잡도와 메모리 오버헤드입니다.

복잡도 비교

연산plistlist_head (정렬)rbtreemin-heap
삽입O(K)O(N)O(log N)O(log N)
삭제O(1)O(1)O(log N)O(log N)
최솟값 접근O(1)O(1)O(1)*O(1)
임의 검색O(N)O(N)O(log N)O(N)
순회O(N)O(N)O(N)O(N log N)
노드 오버헤드(64비트)36 bytes16 bytes24 bytes8 bytes

* rbtree의 최솟값 접근은 rb_root_cached를 사용할 때 O(1)입니다.

plist가 유리한 조건

plist가 다른 자료구조보다 유리한 조건은 다음과 같습니다.

plist가 불리한 조건

삽입 성능 비교: N=100, K=10 기준 비교 횟수 (worst case) 자료구조 0 25 50 75 100 100 list_head O(N) 10 plist O(K) 7 rbtree O(log N) 7 min-heap O(log N)

디버깅

CONFIG_DEBUG_PLIST 커널 설정을 활성화하면, plist의 무결성(Integrity)을 삽입/삭제 시마다 검증합니다.

CONFIG_DEBUG_PLIST

/* lib/plist.c - CONFIG_DEBUG_PLIST 활성화 시 */
static void plist_check_prev_next(struct list_head *t,
                                  struct list_head *p,
                                  struct list_head *n)
{
    WARN(n->prev != p || p->next != n,
         "top: %p, move prev: %p, prev->next: %p, "
         "move next: %p, next->prev: %p\n",
         t, p, p->next, n, n->prev);
}

static void plist_check_list(struct list_head *top)
{
    struct list_head *prev = top, *next = top->next;

    while (next != top) {
        plist_check_prev_next(top, prev, next);
        WARN_ON(next == next->next);
        prev = next;
        next = prev->next;
    }
    plist_check_prev_next(top, prev, next);
}

static void plist_check_head(struct plist_head *head)
{
    if (!plist_head_empty(head))
        plist_check_list(&head->node_list);

    /* prio_list도 검증 (생략) */
}
코드 설명

CONFIG_DEBUG_PLIST가 활성화되면 다음을 검증합니다.

  • 이중 연결 무결성: p->next == n이면 n->prev == p인지 확인합니다. 리스트 포인터가 손상되면 즉시 WARN을 출력합니다.
  • 무한 루프 감지: next == next->next 조건으로 자기 참조 루프를 감지합니다.
  • 호출 위치: plist_add()plist_del()의 시작과 끝에서 plist_check_head()가 호출됩니다.

프로덕션(Production) 커널에서는 CONFIG_DEBUG_PLIST를 비활성화하여 오버헤드를 제거합니다. plist_check_head는 빈 함수로 컴파일됩니다.

디버깅 기법

# CONFIG_DEBUG_PLIST 활성화
CONFIG_DEBUG_PLIST=y

# plist 관련 WARN 메시지 확인
dmesg | grep -i plist

# ftrace로 plist_add/plist_del 추적
echo plist_add > /sys/kernel/debug/tracing/set_ftrace_filter
echo plist_del >> /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace

# KGDB로 plist 내용 출력 (GDB 명령)
(gdb) p *(struct plist_head *)&pm_qos->constraints.list
(gdb) set $node = pm_qos->constraints.list.node_list.next
(gdb) p ((struct plist_node *)((char *)$node - 16))->prio

소스 코드 워크스루

plist의 핵심 소스 파일을 상세히 추적합니다. 전체 구현은 놀라울 정도로 작습니다: lib/plist.c는 약 130줄에 불과합니다.

plist_add 내부 로직 추적

6개의 노드가 있는 plist에 새 노드(prio=15)를 삽입하는 과정을 소스 코드 수준에서 추적합니다.

/* 초기 상태:
 * node_list: [5]->[5]->[10]->[10]->[20]->[20]->HEAD
 * prio_list: [5]---->[10]---->[20]---->[5] (순환)
 * 삽입할 노드: new_node, prio=15
 */

void plist_add(struct plist_node *node, struct plist_head *head)
{
    /* node = new_node (prio=15) */
    struct plist_node *first, *iter, *prev = NULL;
    struct list_head *node_next = &head->node_list;

    /* 디버그 검증 (CONFIG_DEBUG_PLIST) */
    plist_check_head(head);                 /* 무결성 확인 */
    WARN_ON(!plist_node_empty(node));       /* 이미 다른 리스트에 있으면 경고 */
    WARN_ON(!list_empty(&node->prio_list)); /* prio_list가 비어있어야 함 */

    /* 리스트가 비어있지 않으므로 skip */
    first = plist_first(head);  /* first = [5]의 첫 노드 */

    /* === prio_list 순회 루프 시작 === */

    /* 반복 1: first=[5], 15 >= 5 → 통과 */
    prev = first;               /* prev = [5] */
    iter = prio_list의 다음;    /* iter = [10] */

    /* 반복 2: first=[10], 15 >= 10 → 통과 */
    prev = first;               /* prev = [10] */
    iter = prio_list의 다음;    /* iter = [20] */

    /* 반복 3: first=[20], 15 < 20 → 여기 앞에 삽입! */
    node_next = &first->node_list;  /* [20]의 node_list 앞 */
    break;

    /* === 순회 종료: 3회 비교 (K=3 고유 우선순위) === */

ins_node:
    /* [10]->[10] 뒤, [20] 앞에 삽입 */
    list_add_tail(&node->node_list, node_next);
    /* node_list: [5]->[5]->[10]->[10]->[15]->[20]->[20]->HEAD */

    /* prev=[10], prev->prio(10) != node->prio(15) */
    /* → 새로운 고유 우선순위! prio_list에 추가 */
    list_add(&node->prio_list, &iter->prio_list);
    /* prio_list: [5]---->[10]---->[15]---->[20]---->[5] */

    plist_check_head(head);     /* 최종 무결성 확인 */
}

/* 최종 상태:
 * node_list: [5]->[5]->[10]->[10]->[15]->[20]->[20]->HEAD
 * prio_list: [5]---->[10]---->[15]---->[20]---->[5]
 * K: 3→4, N: 6→7
 */

plist_del 대표 노드 삭제 추적

/* 초기 상태:
 * node_list: [5.A]->[5.B]->[10.C]->HEAD
 * prio_list: [5.A]---->[10.C]---->[5.A]
 * 삭제할 노드: [5.A] (prio=5의 대표 노드)
 */

void plist_del(struct plist_node *node, struct plist_head *head)
{
    /* node = [5.A] */
    plist_check_head(head);

    /* [5.A]의 prio_list가 비어있지 않음 → 대표 노드 */
    if (!list_empty(&node->prio_list)) {  /* true */

        /* [5.A]의 다음 노드가 HEAD가 아님 */
        if (node->node_list.next != &head->node_list) {  /* true: next = [5.B] */
            struct plist_node *next;
            next = list_entry(node->node_list.next,
                              struct plist_node, node_list);
            /* next = [5.B] */

            /* [5.B]의 prio(5) == [5.A]의 prio(5) → 대표 승계! */
            if (next->prio == node->prio)  /* true */
                list_add(&next->prio_list, &node->prio_list);
                /* [5.B]가 prio_list에 연결됨 */
        }
        /* [5.A]의 prio_list 연결 해제 */
        list_del_init(&node->prio_list);
    }

    /* [5.A]의 node_list 연결 해제 */
    list_del_init(&node->node_list);

    plist_check_head(head);
}

/* 최종 상태:
 * node_list: [5.B]->[10.C]->HEAD
 * prio_list: [5.B]---->[10.C]---->[5.B]
 * [5.A]: node_list=self, prio_list=self (격리됨)
 */

관련 자료구조 비교

커널에서 우선순위 기반 데이터를 관리하는 다양한 자료구조를 비교합니다. 각 자료구조는 특정 사용 패턴에 최적화되어 있습니다.

종합 비교표

특성plistlist_headrbtreemin-heap
정렬 유지항상 정렬비정렬 (수동)항상 정렬부분 정렬
삽입O(K)O(1)/O(N)O(log N)O(log N)
삭제O(1)O(1)O(log N)O(log N)
최솟값O(1)O(N)/O(1)O(1)*O(1)
FIFO 동일 키보장삽입 방식에 따라미보장미보장
임의 접근O(N)O(N)O(log N)O(N)
구현 복잡도~130줄~50줄~500줄~200줄
커널 활용PM QoS대부분CFS, VMAperf, hrtimer

선택 가이드

우선순위 정렬이 필요한가?
├── 아니오 → list_head (가장 단순, O(1) 삽입/삭제)
└── 예
    ├── 복합 정렬 키 (priority + deadline)?
    │   └── 예 → rbtree (유연한 비교 함수)
    ├── K « N (고유 우선순위 ≪ 전체 노드)?
    │   └── 예 → plist (O(K) 삽입, O(1) 삭제)
    ├── 고정 크기 배열 가능?
    │   └── 예 → min-heap (캐시 친화적, 낮은 메모리)
    └── 그 외 → rbtree (범용 O(log N))
우선순위 자료구조 선택 가이드 우선순위 정렬이 필요한가? 아니오 list_head O(1) 삽입/삭제, 가장 단순 복합 정렬 키가 필요한가? rbtree O(log N), 유연한 비교 함수 아니오 K « N (고유 우선순위 « 전체)? plist O(K) 삽입, O(1) 삭제, FIFO 보장 아니오 고정 크기 배열 가능? min-heap O(log N), 캐시 친화적, 낮은 메모리 아니오 rbtree 범용 O(log N)

실전 사용 패턴

plist를 커널 모듈이나 서브시스템에서 사용하는 실전 패턴을 소개합니다.

기본 사용 패턴

#include <linux/plist.h>

struct my_task {
    struct plist_node pnode;
    char              name[32];
    int               data;
};

static PLIST_HEAD(task_queue);        /* 정적 선언 */
static DEFINE_SPINLOCK(queue_lock);  /* plist 보호용 락 */

/* 태스크 추가: 우선순위 순서 유지 */
void enqueue_task(struct my_task *task, int prio)
{
    plist_node_init(&task->pnode, prio);

    spin_lock(&queue_lock);
    plist_add(&task->pnode, &task_queue);
    spin_unlock(&queue_lock);
}

/* 가장 높은 우선순위 태스크 꺼내기 */
struct my_task *dequeue_highest(void)
{
    struct my_task *task = NULL;

    spin_lock(&queue_lock);
    if (!plist_head_empty(&task_queue)) {
        task = plist_first_entry(&task_queue,
                                  struct my_task, pnode);
        plist_del(&task->pnode, &task_queue);
    }
    spin_unlock(&queue_lock);

    return task;
}

/* 모든 태스크 순회 */
void print_all_tasks(void)
{
    struct my_task *task;

    spin_lock(&queue_lock);
    plist_for_each_entry(task, &task_queue, pnode) {
        pr_info("task: %s, prio: %d\n",
                task->name, task->pnode.prio);
    }
    spin_unlock(&queue_lock);
}
코드 설명
  • PLIST_HEAD(task_queue): 정적 plist_head를 선언하고 초기화합니다.
  • spinlock 보호: plist 자체는 동시성(Concurrency) 보호를 제공하지 않으므로, 반드시 외부 락으로 보호해야 합니다.
  • enqueue_task: plist_node_init()으로 우선순위를 설정한 후 plist_add()로 삽입합니다.
  • dequeue_highest: plist_first_entry()로 최고 우선순위 태스크를 O(1)에 가져오고, plist_del()로 제거합니다.
  • print_all_tasks: plist_for_each_entry()로 모든 태스크를 우선순위 순서대로 순회합니다.

동적 우선순위 변경

/* 태스크의 우선순위를 동적으로 변경 */
void update_task_priority(struct my_task *task, int new_prio)
{
    spin_lock(&queue_lock);

    if (!plist_node_empty(&task->pnode)) {
        /* 리스트에서 제거 후 새 우선순위로 재삽입 */
        plist_del(&task->pnode, &task_queue);
        plist_node_init(&task->pnode, new_prio);
        plist_add(&task->pnode, &task_queue);
    } else {
        /* 리스트에 없으면 prio만 업데이트 */
        task->pnode.prio = new_prio;
    }

    spin_unlock(&queue_lock);
}

/* 최고/최저 우선순위 값 조회 */
int get_highest_priority(void)
{
    int prio = INT_MAX;

    spin_lock(&queue_lock);
    if (!plist_head_empty(&task_queue))
        prio = plist_first(&task_queue)->prio;  /* O(1) */
    spin_unlock(&queue_lock);

    return prio;
}

int get_lowest_priority(void)
{
    int prio = INT_MIN;

    spin_lock(&queue_lock);
    if (!plist_head_empty(&task_queue))
        prio = plist_last(&task_queue)->prio;   /* O(1) */
    spin_unlock(&queue_lock);

    return prio;
}

안전한 순회와 조건부 삭제

/* 특정 우선순위 이하의 모든 태스크 제거 */
void purge_low_priority(int threshold)
{
    struct my_task *task, *tmp;

    spin_lock(&queue_lock);
    plist_for_each_entry_safe(task, tmp, &task_queue, pnode) {
        /* prio가 threshold보다 크면(낮은 우선순위) 제거 */
        if (task->pnode.prio > threshold) {
            plist_del(&task->pnode, &task_queue);
            kfree(task);
        }
    }
    spin_unlock(&queue_lock);
}
코드 설명
  • plist_for_each_entry_safe: tmp에 다음 노드를 미리 저장하므로, 순회 중 plist_del()로 현재 노드를 삭제해도 안전합니다.
  • threshold 비교: prio 값이 클수록 낮은 우선순위이므로, task->pnode.prio > threshold로 저우선순위 태스크를 필터링합니다.

커널 소스 분석: 전체 plist.c

lib/plist.c의 전체 구현은 약 130줄로, 커널 자료구조 중 가장 간결한 구현 중 하나입니다. 핵심 함수 3개(plist_add, plist_del, plist_requeue)와 디버그 코드로 구성됩니다.

파일 구조

include/linux/plist.h     ~180줄  구조체, 매크로, 인라인 함수
lib/plist.c               ~130줄  plist_add, plist_del, plist_requeue
lib/Kconfig.debug         CONFIG_DEBUG_PLIST 정의

주요 사용처:
kernel/power/qos.c        PM QoS 제약 관리
kernel/locking/rtmutex.c  RT-Mutex (역사적 사용)
kernel/futex/             Futex PI (간접 사용)

plist.h 헤더 분석

include/linux/plist.h의 주요 구성 요소를 분석합니다.

/* include/linux/plist.h 핵심 구조 */

/* 1. 헤더 가드 및 의존성 */
#ifndef _LINUX_PLIST_H_
#define _LINUX_PLIST_H_

#include <linux/kernel.h>
#include <linux/list.h>

/* 2. 구조체 정의 (앞서 분석) */

/* 3. 유틸리티 인라인 함수 */
static inline int plist_head_empty(const struct plist_head *head)
{
    return list_empty(&head->node_list);
}

static inline int plist_node_empty(const struct plist_node *node)
{
    return list_empty(&node->node_list);
}

/* 4. 외부 함수 선언 */
extern void plist_add(struct plist_node *node, struct plist_head *head);
extern void plist_del(struct plist_node *node, struct plist_head *head);
extern void plist_requeue(struct plist_node *node, struct plist_head *head);

/* 5. plist_next/plist_prev 인라인 */
static inline struct plist_node *plist_next(struct plist_node *pos)
{
    return list_next_entry(pos, node_list);
}

static inline struct plist_node *plist_prev(struct plist_node *pos)
{
    return list_prev_entry(pos, node_list);
}

#endif /* _LINUX_PLIST_H_ */

내부 불변식(Invariant)

plist가 올바르게 동작하기 위해서는 다음 불변식이 항상 유지되어야 합니다. CONFIG_DEBUG_PLIST는 이 불변식을 런타임에 검증합니다.

6가지 핵심 불변식

#불변식위반 시 증상
I1node_list는 prio 값의 비감소 순서(non-decreasing order)로 정렬됩니다.plist_first()가 최고 우선순위를 반환하지 않음
I2같은 prio 값의 노드는 삽입 순서(FIFO)를 유지합니다.동일 우선순위 태스크 간 불공정
I3각 고유 prio 값에 대해 정확히 하나의 대표 노드만 prio_list에 연결됩니다.중복 대표 → O(K) 보장 깨짐
I4대표 노드는 해당 prio 그룹의 첫 번째 노드(node_list 기준)입니다.대표 삭제 후 승계 실패
I5비대표 노드의 prio_list는 자기 자신을 가리킵니다 (list_empty 상태).prio_list 순회에서 잘못된 노드 방문
I6prio_list는 node_list와 동일한 prio 순서로 정렬됩니다.삽입 위치 결정 오류
plist 불변식 시각화 (6개 노드, 3개 고유 우선순위) I1: node_list 비감소 정렬 | I2: 같은 prio FIFO A: 5 B: 5 C: 10 D: 10 E: 10 F: 20 5 ≤ 5 ≤ 10 ≤ 10 ≤ 10 ≤ 20 A before B (FIFO) I3: 고유 prio당 정확히 1개 대표 | I4: 대표 = 그룹 첫 번째 A: 5 REP C: 10 REP F: 20 REP K=3 대표 노드 각 그룹의 첫 번째만 I5: 비대표 prio_list = self | I6: prio_list 순서 = node_list 순서 B: 5 D: 10 E: 10 self self self B, D, E: prio_list가 자기 자신을 가리킴 불변식 요약 plist_add/plist_del은 6개 불변식을 항상 유지하도록 설계되었습니다. CONFIG_DEBUG_PLIST는 I1~I6을 매 연산마다 검증합니다 (프로덕션에서는 비활성화).

엣지 케이스 분석

plist 구현이 올바르게 처리해야 하는 엣지 케이스(Edge Case)를 분석합니다. 이 케이스들은 plist 단위 테스트(lib/plist.cplist_test_requeue 등)에서 검증됩니다.

빈 리스트에 첫 노드 삽입

/* 엣지 케이스 1: 빈 리스트에 삽입 */
struct plist_head head;
struct plist_node n1;

plist_head_init(&head);
plist_node_init(&n1, 10);

plist_add(&n1, &head);
/* 결과:
 * - plist_head_empty(&head) == false
 * - plist_first(&head) == &n1
 * - plist_last(&head) == &n1
 * - n1이 prio_list 대표 (유일한 노드)
 * - node_list: head <-> n1 <-> head (순환)
 */

/* 유일한 노드 삭제 → 빈 리스트 복원 */
plist_del(&n1, &head);
/* 결과:
 * - plist_head_empty(&head) == true
 * - plist_node_empty(&n1) == true
 */

모든 노드가 같은 우선순위

/* 엣지 케이스 2: 모든 노드가 prio=5 */
struct plist_node n1, n2, n3;

plist_node_init(&n1, 5);
plist_node_init(&n2, 5);
plist_node_init(&n3, 5);

plist_add(&n1, &head);  /* K=1, n1은 대표 */
plist_add(&n2, &head);  /* K=1, n2는 비대표 */
plist_add(&n3, &head);  /* K=1, n3는 비대표 */

/* node_list: n1 -> n2 -> n3 (FIFO)
 * prio_list: n1만 연결 (K=1)
 *
 * 삽입마다 prio_list를 순회하지만 K=1이므로
 * 실질적으로 O(1) 삽입!
 *
 * 대표 n1 삭제 시:
 * - n2가 새 대표로 승계
 * - node_list: n2 -> n3
 * - prio_list: n2만 연결
 */

모든 노드가 고유 우선순위

엣지 케이스 3: 모든 노드가 다른 prio (최악 케이스) prio: 1, 2, 3, 4, 5 -- 5개 노드, 5개 고유 우선순위

K = N = 5 → plist_add는 O(N)으로 퇴화 이 경우 plist의 이점이 없으며, rbtree의 O(log N)이 더 효율적입니다.

그러나 모든 노드가 prio_list 대표이므로:

실전에서 이 패턴은 드물지만, SCHED_DEADLINE 태스크는 각각 고유한 deadline을 가지므로 K ≈ N이 될 수 있습니다. 이것이 RT-Mutex가 plist에서 rbtree로 전환된 핵심 이유입니다.

plist_requeue 엣지 케이스

/* 엣지 케이스 4: requeue - 이미 그룹 끝에 있는 경우 */
/* node_list: n1(5) -> n2(5) -> n3(10) */

plist_requeue(&n2, &head);
/* n2는 이미 prio=5 그룹의 마지막 (다음이 n3, prio=10)
 * → node->prio(5) != iter->prio(10)
 * → 즉시 반환 (no-op)
 */

plist_requeue(&n1, &head);
/* n1은 prio=5 그룹의 첫 번째, 다음이 n2(prio=5)
 * → node->prio(5) == iter->prio(5)
 * → n1을 삭제 후, prio=5 그룹 끝(n3 앞)에 재삽입
 * 결과: n2(5) -> n1(5) -> n3(10)
 * n2가 새 대표
 */

역사적 변천

plist는 리눅스 커널에서 10년 이상 사용되어 온 안정적인 자료구조입니다. 도입 배경과 이후 변화를 정리합니다.

주요 이력

시기이벤트의미
2006Inaky Perez-Gonzalez가 PI futex 지원을 위해 plist 도입RT-Mutex 대기자 관리의 기초 자료구조로 채택
2006~2012RT-Mutex, PM QoS, Futex PI에서 활발히 사용커널의 핵심 실시간 인프라를 지탱
2013fb00aca47440: RT-Mutex가 plist에서 rbtree로 전환SCHED_DEADLINE 복합 키 지원 필요. plist는 단순 정수 키만 지원
2013~현재PM QoS에서 계속 활발히 사용PM QoS의 MIN/MAX 집계에 plist의 O(1) first/last가 최적
201677ba89c5cf2e: plist_head 구조 단순화plist_head에서 별도의 prio_list 앵커 제거
2020~현재안정 유지, 큰 변화 없음성숙한 자료구조로 API 변경 최소화

RT-Mutex의 plist → rbtree 전환 분석

RT-Mutex가 plist에서 rbtree로 전환된 핵심 이유를 분석합니다.

/* SCHED_DEADLINE 이전: plist로 충분 */
struct rt_mutex_waiter {
    struct plist_node list_entry;    /* prio 기반 정렬 */
    struct plist_node pi_list_entry; /* PI chain 정렬 */
};

/* SCHED_DEADLINE 도입 후: 복합 키 필요 */
/* waiter 비교: (prio, deadline) 순서
 * plist의 prio는 단일 int → deadline 비교 불가
 * rbtree의 비교 함수로 (prio, deadline) 복합 정렬 가능
 *
 * 예: Task A (prio=49, deadline=100ms)
 *     Task B (prio=49, deadline=50ms)
 * plist: 같은 prio → FIFO (A 먼저) ← 부적절!
 * rbtree: deadline 비교 → B 먼저 (올바른 EDF 순서)
 */

/* 전환 후 구조 */
struct rt_mutex_waiter {
    struct rb_node tree_entry;      /* rbtree 엔트리 */
    struct rb_node pi_tree_entry;   /* PI chain rbtree */
    struct task_struct *task;
    struct rt_mutex *lock;
    int prio;
    u64 deadline;                   /* SCHED_DEADLINE 지원 */
};

/* 비교 함수 */
static inline bool rt_mutex_waiter_less(
    struct rt_mutex_waiter *left,
    struct rt_mutex_waiter *right)
{
    if (left->prio < right->prio)
        return 1;
    if (left->prio > right->prio)
        return 0;
    /* 같은 prio: deadline이 작은 것이 우선 */
    if (dl_prio(left->prio))
        return dl_time_before(left->deadline, right->deadline);
    return 0;
}
코드 설명

RT-Mutex 전환의 핵심 포인트입니다.

  • plist 한계: plist의 정렬 키는 단일 int prio입니다. SCHED_DEADLINE 태스크는 동일 prio에서 deadline에 따라 추가 정렬이 필요하지만, plist는 이를 지원하지 않습니다.
  • rbtree 장점: 커스텀 비교 함수(rt_mutex_waiter_less)로 (prio, deadline) 복합 키를 유연하게 처리합니다.
  • 성능 트레이드오프: rbtree 삽입은 O(log N)이지만, RT-Mutex 대기자 수는 보통 적어서 (수~수십 개) 실측 성능 차이는 미미합니다.
  • PM QoS는 유지: PM QoS는 단일 정수 키(레이턴시 us 값)만 사용하므로, plist가 여전히 최적입니다.

커널 모듈 테스트 예제

plist의 동작을 검증하는 간단한 커널 모듈 예제입니다. dmesg로 출력을 확인할 수 있습니다.

/* plist_test.c - plist 기능 검증 커널 모듈 */
#include <linux/module.h>
#include <linux/plist.h>
#include <linux/slab.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("plist test module");

struct test_item {
    struct plist_node node;
    int               id;
};

static void dump_plist(struct plist_head *head, const char *label)
{
    struct test_item *item;

    pr_info("[plist_test] %s:\n", label);
    if (plist_head_empty(head)) {
        pr_info("  (empty)\n");
        return;
    }
    plist_for_each_entry(item, head, node) {
        pr_info("  id=%d prio=%d rep=%s\n",
                item->id, item->node.prio,
                !list_empty(&item->node.prio_list) ? "YES" : "no");
    }
    pr_info("  first: id=%d prio=%d\n",
            plist_first_entry(head, struct test_item, node)->id,
            plist_first(head)->prio);
    pr_info("  last:  id=%d prio=%d\n",
            plist_last_entry(head, struct test_item, node)->id,
            plist_last(head)->prio);
}

static int __init plist_test_init(void)
{
    PLIST_HEAD(head);
    struct test_item items[6];
    int prios[] = { 10, 5, 20, 5, 10, 15 };
    int i;

    pr_info("[plist_test] === plist test start ===\n");

    /* 1. 순차 삽입 */
    for (i = 0; i < 6; i++) {
        items[i].id = i;
        plist_node_init(&items[i].node, prios[i]);
        plist_add(&items[i].node, &head);
    }
    dump_plist(&head, "after 6 insertions");
    /* 기대 순서: [5,1] [5,3] [10,0] [10,4] [15,5] [20,2] */

    /* 2. 대표 노드 삭제 */
    plist_del(&items[1].node, &head);  /* id=1, prio=5 (대표) 삭제 */
    dump_plist(&head, "after del id=1(prio=5 rep)");
    /* id=3이 prio=5의 새 대표 */

    /* 3. 비대표 노드 삭제 */
    plist_del(&items[4].node, &head);  /* id=4, prio=10 (비대표) 삭제 */
    dump_plist(&head, "after del id=4(prio=10 non-rep)");

    /* 4. first/last 확인 */
    pr_info("[plist_test] first prio=%d, last prio=%d\n",
            plist_first(&head)->prio,
            plist_last(&head)->prio);

    /* 5. 전체 삭제 */
    plist_del(&items[3].node, &head);
    plist_del(&items[0].node, &head);
    plist_del(&items[5].node, &head);
    plist_del(&items[2].node, &head);
    dump_plist(&head, "after all deletions");

    pr_info("[plist_test] === plist test done ===\n");
    return 0;
}

static void __exit plist_test_exit(void)
{
    pr_info("[plist_test] module unloaded\n");
}

module_init(plist_test_init);
module_exit(plist_test_exit);
코드 설명

이 커널 모듈은 plist의 핵심 동작을 검증합니다.

  • dump_plist: 리스트의 모든 노드를 출력하며, 각 노드의 대표 여부(prio_list 연결 상태)를 표시합니다.
  • 순차 삽입: 다양한 우선순위(10, 5, 20, 5, 10, 15)를 비순서대로 삽입하여 정렬이 올바른지 확인합니다.
  • 대표 노드 삭제: prio=5의 대표인 id=1을 삭제하면, id=3이 새 대표로 승계되는지 확인합니다.
  • 비대표 삭제: prio=10의 비대표인 id=4를 삭제하여 prio_list에 영향이 없는지 확인합니다.
  • 전체 삭제: 모든 노드를 삭제한 후 빈 리스트가 되는지 확인합니다.

빌드 Makefile

obj-m += plist_test.o

KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

예상 출력

# 모듈 로드 후 dmesg 확인
$ sudo insmod plist_test.ko
$ dmesg | tail -30

[plist_test] === plist test start ===
[plist_test] after 6 insertions:
  id=1 prio=5 rep=YES
  id=3 prio=5 rep=no
  id=0 prio=10 rep=YES
  id=4 prio=10 rep=no
  id=5 prio=15 rep=YES
  id=2 prio=20 rep=YES
  first: id=1 prio=5
  last:  id=2 prio=20
[plist_test] after del id=1(prio=5 rep):
  id=3 prio=5 rep=YES           ← id=3이 새 대표!
  id=0 prio=10 rep=YES
  id=4 prio=10 rep=no
  id=5 prio=15 rep=YES
  id=2 prio=20 rep=YES
  first: id=3 prio=5
  last:  id=2 prio=20
[plist_test] after del id=4(prio=10 non-rep):
  id=3 prio=5 rep=YES
  id=0 prio=10 rep=YES          ← id=0은 여전히 대표
  id=5 prio=15 rep=YES
  id=2 prio=20 rep=YES
  first: id=3 prio=5
  last:  id=2 prio=20
[plist_test] first prio=5, last prio=20
[plist_test] after all deletions:
  (empty)
[plist_test] === plist test done ===

PM QoS 심층 분석

PM QoS에서 plist가 어떻게 활용되는지 더 깊이 분석합니다. CPU idle governor가 PM QoS 제약을 기반으로 idle 상태를 선택하는 전체 경로를 추적합니다.

CPU Latency QoS

/* 드라이버에서 CPU 레이턴시 제약 등록 */
struct pm_qos_request my_req;

/* 네트워크 드라이버: 패킷 처리 시 50us 이하 깨어남 보장 요청 */
cpu_latency_qos_add_request(&my_req, 50);
/* 내부: plist_node_init(&my_req.node, 50);
 *       plist_add(&my_req.node, &cpu_latency_constraints.list);
 * plist에 prio=50 노드 삽입 */

/* USB 드라이버: 100us 이하 레이턴시 요청 */
struct pm_qos_request usb_req;
cpu_latency_qos_add_request(&usb_req, 100);
/* plist에 prio=100 노드 삽입 */

/* 오디오 드라이버: 20us 이하 레이턴시 요청 */
struct pm_qos_request audio_req;
cpu_latency_qos_add_request(&audio_req, 20);
/* plist에 prio=20 노드 삽입 */

/* 현재 활성 제약 = plist_first()->prio = 20us
 * CPU idle governor는 탈출 레이턴시가 20us 이하인 idle 상태만 선택
 */

/* 오디오 재생 종료 → 제약 해제 */
cpu_latency_qos_remove_request(&audio_req);
/* plist_del → 새 first = 50us (네트워크 제약)
 * CPU idle governor가 더 깊은 idle 상태를 선택 가능 → 전력 절감
 */
코드 설명

PM QoS CPU 레이턴시 제약의 전체 흐름입니다.

  • 여러 드라이버의 제약 공존: 네트워크(50us), USB(100us), 오디오(20us) 드라이버가 각각 제약을 등록합니다. plist는 이 값들을 정렬하여 관리합니다.
  • 가장 엄격한 제약: plist_first()->prio가 20(오디오)을 O(1)에 반환합니다. CPU idle governor는 이 값보다 긴 탈출 레이턴시를 가진 깊은 idle 상태를 사용하지 않습니다.
  • 동적 갱신: 제약이 추가/제거될 때마다 가장 엄격한 값이 자동으로 갱신됩니다. notifier를 통해 idle governor에 즉시 알립니다.
PM QoS CPU Latency: plist → idle 상태 선택 드라이버 요청 오디오: 20us 네트워크: 50us USB: 100us plist_add cpu_latency_constraints.list 20 first 50 100 target_value = 20us = plist_first()->prio (O(1)) notifier CPU Idle Governor (menu/teo) target_latency = 20us C1 (exit: 10us) ≤ 20us → 허용 C2 (exit: 50us) > 20us → 금지 C3 (exit: 200us) > 20us → 금지 선택: C1 오디오 재생 종료 후 (20us 제약 해제) plist (업데이트됨) 50 new first 100 Governor (업데이트됨) target_latency = 50us C1 (10us) ≤ 50us → 허용 C2 (50us) ≤ 50us → 허용! 선택: C2 → 더 깊은 절전! 오디오 제약(20us) 해제 → 새 최솟값 50us → C2 idle 허용 → 전력 절감 plist의 O(1) plist_first()가 PM QoS의 핵심 성능을 보장합니다. 제약 등록/해제: O(K) 삽입 + O(1) 삭제 | 집계 조회: O(1) plist_first()

Device PM QoS와 per-device plist

/* 디바이스별 PM QoS 제약도 plist를 사용합니다 */
struct dev_pm_qos {
    struct pm_qos_constraints resume_latency;  /* plist 사용 */
    struct pm_qos_constraints latency_tolerance;
    struct pm_qos_flags flags;
};

/* 예: eMMC 컨트롤러 resume latency 제약 */
dev_pm_qos_add_request(mmc_dev, &mmc_qos_req,
                       DEV_PM_QOS_RESUME_LATENCY, 100);
/* mmc_dev의 resume_latency plist에 prio=100 삽입
 * 이 디바이스가 100us 이내에 resume 가능한 전력 상태만 사용 */

/* 사용자 공간에서도 제약 가능 */
/* /sys/devices/.../power/pm_qos_resume_latency_us */
/* echo 50 > pm_qos_resume_latency_us */

메모리 오버헤드 상세 분석

plist의 메모리 오버헤드를 아키텍처별로 정량 분석합니다.

노드당 오버헤드

아키텍처list_headplist_noderb_nodemin_heap 엔트리
32비트8 bytes20 bytes12 bytes4~8 bytes
64비트16 bytes36 bytes (패딩 포함)24 bytes8 bytes
/* 64비트 plist_node 메모리 레이아웃 상세 */
struct plist_node {
    int  prio;                /* 오프셋 0, 4 bytes */
    /* 4 bytes 패딩 (8바이트 정렬) */
    struct list_head prio_list; /* 오프셋 8, 16 bytes (prev+next) */
    struct list_head node_list; /* 오프셋 24, 16 bytes (prev+next) */
};                              /* 총: 4 + 4(pad) + 16 + 16 = 40 bytes */

/* 실제 크기 확인 */
pr_info("sizeof(plist_node) = %zu\n", sizeof(struct plist_node));
/* x86_64: 40 bytes (컴파일러 패딩 포함)
 * arm64:  40 bytes
 * 32-bit: 20 bytes
 */

/* 실전 오버헤드 계산 예시:
 * PM QoS에서 동시 활성 요청 20개 가정
 * 추가 메모리 = 20 * (40 - 16) = 480 bytes (list_head 대비)
 * → 무시할 수 있는 수준
 *
 * RT-Mutex 대기열에서 100개 태스크 가정
 * 추가 메모리 = 100 * (40 - 16) = 2,400 bytes
 * → 여전히 무시할 수 있는 수준
 */

캐시 동작 특성

plist의 캐시 동작 분석

캐시 라인 크기: 일반적으로 64 bytes

plist_node (40 bytes) → 1개 캐시 라인에 1개 노드 plist_add의 prio_list 순회:

list_head (16 bytes) → 1개 캐시 라인에 4개 노드

min-heap (연속 배열) → 가장 캐시 친화적

rbtree (노드 포인터) → 트리 구조로 캐시 미스 빈번

결론: 캐시 효율은 min-heap > list_head > plist ≈ rbtree 그러나 plist의 순회 거리 K가 작으므로 실측 영향은 미미합니다

흔한 실수와 주의사항

동시성 보호 누락

/* ✗ 잘못된 사용: 락 없이 plist 조작 */
plist_add(&node, &head);  /* 레이스 조건 발생 가능! */

/* ✓ 올바른 사용: 반드시 외부 락으로 보호 */
spin_lock(&lock);
plist_add(&node, &head);
spin_unlock(&lock);

이중 삽입

/* ✗ 잘못된 사용: 이미 삽입된 노드를 다시 삽입 */
plist_add(&node, &head);
plist_add(&node, &head);  /* WARN_ON 발생! 리스트 손상! */

/* ✓ 올바른 사용: 삽입 전 empty 확인 */
if (plist_node_empty(&node))
    plist_add(&node, &head);

삽입 후 prio 직접 변경

/* ✗ 잘못된 사용: prio를 직접 수정 → 정렬 깨짐 */
plist_add(&node, &head);
node.prio = 5;  /* 불변식 I1 위반! 정렬 순서 깨짐! */

/* ✓ 올바른 사용: del → init → add 또는 requeue */
plist_del(&node, &head);
plist_node_init(&node, 5);
plist_add(&node, &head);

안전하지 않은 순회 중 삭제

/* ✗ 잘못된 사용: 일반 순회 중 삭제 */
plist_for_each_entry(item, &head, node) {
    if (should_remove(item))
        plist_del(&item->node, &head);  /* 순회 깨짐! */
}

/* ✓ 올바른 사용: _safe 변형 사용 */
plist_for_each_entry_safe(item, tmp, &head, node) {
    if (should_remove(item))
        plist_del(&item->node, &head);  /* 안전! */
}

API 빠른 참조

plist API의 사용 패턴을 한눈에 파악할 수 있는 빠른 참조표입니다.

/* ============================================================
 *  plist API Quick Reference
 * ============================================================ */

/* --- 선언 및 초기화 --- */
PLIST_HEAD(name);                              /* 정적 선언 + 초기화 */
plist_head_init(&head);                        /* 동적 초기화 */
PLIST_NODE_INIT(node, prio);                   /* 노드 정적 초기화 */
plist_node_init(&node, prio);                  /* 노드 동적 초기화 */

/* --- 삽입/삭제/재배치 --- */
plist_add(&node, &head);                       /* O(K) 정렬 삽입 */
plist_del(&node, &head);                       /* O(1) 삭제 */
plist_requeue(&node, &head);                   /* O(K) FIFO 재배치 */

/* --- 상태 확인 --- */
plist_head_empty(&head);                       /* 리스트 비어있는지 */
plist_node_empty(&node);                       /* 노드가 미연결인지 */

/* --- 최솟값/최댓값 접근 --- */
plist_first(&head);                             /* O(1) 최솟값 plist_node* */
plist_last(&head);                              /* O(1) 최댓값 plist_node* */
plist_first_entry(&head, type, member);        /* O(1) 외부 구조체* */
plist_last_entry(&head, type, member);         /* O(1) 외부 구조체* */

/* --- 순회 --- */
plist_for_each(pos, &head);                    /* plist_node 순회 */
plist_for_each_safe(pos, n, &head);            /* 안전한 순회 */
plist_for_each_entry(pos, &head, mem);         /* 외부 구조체 순회 */
plist_for_each_entry_safe(pos, n, &head, m);   /* 안전한 외부 순회 */

/* --- 이동 --- */
plist_next(&node);                              /* 다음 노드 */
plist_prev(&node);                              /* 이전 노드 */

plist_add 시각화

plist_add()는 삽입 위치에 따라 세 가지 대표적인 케이스로 분류됩니다. 각 케이스에서 prio_list와 node_list가 어떻게 변화하는지 Before/After 상태를 시각적으로 비교합니다.

케이스별 상세 분석
  • 케이스 1 — 새로운 고유 우선순위 삽입: 기존에 같은 prio가 없으므로 prio_list에도 새 엔트리가 추가됩니다. 새 노드가 해당 우선순위 그룹의 대표 노드가 됩니다.
  • 케이스 2 — 기존 우선순위에 합류: 이미 같은 prio가 존재하므로 prio_list는 변경 없이, node_list에만 FIFO 순서로 뒤에 추가됩니다. 기존 대표 노드가 유지됩니다.
  • 케이스 3 — 빈 리스트에 첫 삽입: head의 prio_list와 node_list 모두 비어 있는 상태에서 첫 노드가 삽입됩니다. 삽입된 노드가 유일한 대표 노드이자 plist_first 결과가 됩니다.
plist_add 세 가지 케이스 케이스 1: 새로운 고유 우선순위 (prio=10 삽입) Before prio_list: A: 5 C: 20 node_list: A(5) → B(5) → C(20) After prio_list: A: 5 NEW: 10 C: 20 node_list: A(5) → B(5) → NEW(10) → C(20) 케이스 2: 기존 우선순위 합류 (prio=5 삽입) Before prio_list: A: 5 C: 20 node_list: A(5) → B(5) → C(20) After prio_list: A: 5 C: 20 (변경 없음) node_list: A(5) → B(5) → NEW(5) → C(20) 케이스 3: 빈 리스트에 첫 삽입 (prio=7 삽입) Before prio_list: (비어 있음) node_list: (비어 있음) After prio_list: NEW: 7 (유일한 대표) node_list: NEW(7) 삽입 결정 로직 요약 ① prio_list를 순회하며 node→prio보다 큰 첫 번째 대표 노드를 찾습니다 (node_next) ② node_list에서 node_next 바로 앞에 list_add_tail()로 삽입합니다 → FIFO 보장 ③ 직전 노드(prev)의 prio가 다르면 → 새 고유 우선순위 → prio_list에도 추가합니다 ④ 직전 노드(prev)의 prio가 같으면 → 기존 그룹에 합류 → prio_list는 변경하지 않습니다 케이스별 비용 케이스 1 (새 고유 prio) prio_list 순회 O(K) + prio_list 삽입 O(1) + node_list 삽입 O(1) 케이스 2 (기존 prio 합류) prio_list 순회 O(K) + node_list 삽입 O(1) (prio_list 변경 없음) 케이스 3 (빈 리스트) 순회 없음 O(1) + prio_list 삽입 O(1) + node_list 삽입 O(1) K = 고유 우선순위 수, 최악: K = N (모두 다른 prio), 최선: K = 1 (모두 같은 prio)
/* 케이스 1: 새로운 고유 우선순위 삽입 — plist_add() 내부 핵심 경로 */
/* lib/plist.c — plist_add 핵심 루프 */
void plist_add(struct plist_node *node, struct plist_head *head)
{
    struct plist_node *first, *iter, *prev = NULL;
    struct list_head *node_next = &head->node_list;

    /* 1단계: prio_list 순회 — O(K) */
    if (!plist_head_empty(head)) {
        first = iter = plist_first(head);
        do {
            if (node->prio < iter->prio) {
                node_next = &iter->node_list;
                break;     /* 삽입 위치 발견 */
            }
            prev = iter;
            iter = list_entry(iter->prio_list.next,
                              struct plist_node, prio_list);
        } while (iter != first);
    }

    /* 2단계: node_list 삽입 (FIFO) */
    list_add_tail(&node->node_list, node_next);

    /* 3단계: 새 고유 우선순위이면 prio_list에도 추가 */
    if (!prev || prev->prio != node->prio)
        list_add_tail(&node->prio_list, &iter->prio_list);
    /* 기존 prio와 같으면 prio_list는 변경 없음 (INIT_LIST_HEAD 상태 유지) */
}
/* 케이스별 삽입 동작 검증 코드 */
PLIST_HEAD(head);
struct plist_node a, b, c, new_node;

/* 초기 상태 구축: A(5), B(5), C(20) */
plist_node_init(&a, 5);
plist_node_init(&b, 5);
plist_node_init(&c, 20);
plist_add(&a, &head);
plist_add(&b, &head);
plist_add(&c, &head);
/* 상태: node_list = [A(5), B(5), C(20)]
 *        prio_list = [A(5), C(20)]           */

/* 케이스 1: 새 고유 우선순위 10 삽입 */
plist_node_init(&new_node, 10);
plist_add(&new_node, &head);
/* 결과: node_list = [A(5), B(5), NEW(10), C(20)]
 *        prio_list = [A(5), NEW(10), C(20)]
 *        NEW가 prio=10의 대표 노드                */

/* 케이스 2: 기존 우선순위 5에 합류 */
struct plist_node d;
plist_node_init(&d, 5);
plist_add(&d, &head);
/* 결과: node_list = [A(5), B(5), D(5), NEW(10), C(20)]
 *        prio_list = [A(5), NEW(10), C(20)]
 *        A가 여전히 prio=5의 대표 노드 (D는 비대표)   */

plist_del 시각화

plist_del()의 핵심 복잡성은 대표 노드 삭제 시 승격(Promotion)에 있습니다. 삭제할 노드가 해당 우선순위 그룹의 대표 노드(prio_list에 연결된 노드)라면, 같은 우선순위의 다음 노드가 새 대표로 승격되어야 합니다.

plist_del: 대표 노드 삭제 → 승격 Before: A(5)를 삭제 — A는 prio=5의 대표 노드 prio_list A: prio=5 삭제 대상 D: prio=10 E: prio=20 node_list A(5) B(5) C(5) D(10) E(20) ✕ A 제거 B가 prio=5의 새 대표로 승격 After: B가 prio=5 대표로 승격 prio_list B: prio=5 승격됨 D: prio=10 E: prio=20 node_list B(5) C(5) D(10) E(20) plist_del 승격 결정 로직 ① node의 prio_list가 비어 있으면(list_empty) → 비대표 노드 → node_list에서만 제거합니다 ② node의 prio_list가 연결되어 있으면 → 대표 노드 → 승격 필요 여부를 확인합니다 ③ node_list의 다음 노드(next)가 같은 prio이면 → next를 prio_list에 연결 (대표 승격) ④ 다음 노드가 다른 prio이면 → 해당 그룹의 마지막 노드 → prio_list에서도 제거합니다 ⑤ 마지막으로 node_list에서 node를 제거하고, plist_node를 재초기화합니다
/* lib/plist.c — plist_del 핵심 로직: 대표 노드 승격 */
void plist_del(struct plist_node *node, struct plist_head *head)
{
    if (!list_empty(&node->prio_list)) {
        /* 대표 노드 삭제 — 승격 필요 여부 확인 */
        struct plist_node *next;

        next = list_entry(node->node_list.next,
                          struct plist_node, node_list);

        /* 다음 노드가 같은 prio이면 대표 승격 */
        if (node->node_list.next != &head->node_list &&
            next->prio == node->prio) {
            /* next의 prio_list를 node의 prio_list 위치에 교체 */
            list_add(&next->prio_list, &node->prio_list);
        }
        /* 다른 prio이면 해당 그룹의 마지막 노드 → prio_list 엔트리 자체 제거 */
        list_del_init(&node->prio_list);
    }

    /* node_list에서 제거 */
    list_del_init(&node->node_list);
}
/* 대표 노드 삭제 → 승격 검증 */
PLIST_HEAD(head);
struct plist_node a, b, c, d, e;

plist_node_init(&a, 5);   /* prio=5 대표 */
plist_node_init(&b, 5);   /* prio=5 비대표 */
plist_node_init(&c, 5);   /* prio=5 비대표 */
plist_node_init(&d, 10);  /* prio=10 대표 */
plist_node_init(&e, 20);  /* prio=20 대표 */
plist_add(&a, &head);
plist_add(&b, &head);
plist_add(&c, &head);
plist_add(&d, &head);
plist_add(&e, &head);

/* A 삭제 — 대표 노드, B가 승격 */
plist_del(&a, &head);
WARN_ON(list_empty(&b.prio_list));  /* B가 prio_list에 연결됨 확인 */
WARN_ON(plist_first(&head)->prio != 5);  /* 첫 노드는 여전히 prio=5 */

/* B도 삭제 — 대표 노드, C가 승격 */
plist_del(&b, &head);
WARN_ON(list_empty(&c.prio_list));  /* C가 새 대표 */

/* C 삭제 — 그룹의 마지막 노드, prio_list에서도 제거 */
plist_del(&c, &head);
WARN_ON(plist_first(&head)->prio != 10);  /* 이제 D(10)가 첫 노드 */

RT-Mutex PI 체인

우선순위 역전(Priority Inversion)은 높은 우선순위 태스크가 낮은 우선순위 태스크가 보유한 잠금을 기다릴 때 발생합니다. RT-Mutex는 우선순위 상속(Priority Inheritance, PI) 프로토콜로 이를 해결하며, plist는 이 메커니즘의 핵심 자료구조로 pi_waiters 관리에 사용됩니다.

PI 체인 동작 상세
  • pi_waiters: 각 태스크가 보유한 RT-Mutex를 통해 자신을 기다리는 최고 우선순위 대기자들의 plist입니다. 태스크의 유효 우선순위(Effective Priority)는 자신의 기본 우선순위와 pi_waiters의 plist_first 중 더 높은 값으로 결정됩니다.
  • 체인 전파: 태스크 A가 뮤텍스 M1을 기다리고, M1의 소유자 B가 뮤텍스 M2를 기다리면, 우선순위 상속이 체인을 따라 전파됩니다. 각 단계에서 plist가 재정렬됩니다.
  • plist에서 rbtree로: 커널 3.x 이후 RT-Mutex 대기자 큐가 rbtree로 전환되었지만, pi_waiters의 개념과 PI 체인 로직은 동일합니다. plist는 PM QoS와 일부 내부 구조에서 계속 사용됩니다.
우선순위 역전 → PI 프로토콜 해결 PI 없는 경우: 우선순위 역전 발생 시간 → H (prio=1) 실행 블록됨 (L이 Lock 보유 + M이 선점) M (prio=50) M이 L을 선점 → H가 무기한 대기 L (prio=99) Lock 보유 나중에 재개 H가 L의 Lock을 기다리는 동안, 관계없는 M이 L을 선점 → H의 응답 시간 무한 증가 PI 적용: 우선순위 상속으로 해결 시간 → H (prio=1) 실행 블록 (짧게) Lock 획득 → 실행 계속 M (prio=50) H 완료 후 실행 L (prio=99) Lock 보유 prio→1 부스트 PI 상속 Lock 해제 L의 pi_waiters plist: H (prio=1) → plist_first = H → L의 유효 prio = min(99, 1) = 1 pi_waiters에서 plist_first()로 O(1)에 최고 우선순위 대기자를 가져옵니다
PI 체인 전파: 중첩 Lock 시나리오 H(prio=1) → Lock M1 대기 → 소유자 B(prio=50) → Lock M2 대기 → 소유자 C(prio=99) 1단계: H가 M1 요청 H: prio=1 M1 대기 waiter M1 (owner: B) PI 전파 B: 50→1 부스트 B의 pi_waiters: [H(1)] → 유효 prio=1 2단계: B도 M2를 대기 중 → 체인 전파 B: prio=1(부스트) M2 대기 waiter M2 (owner: C) PI 전파 C: 99→1 부스트 C의 pi_waiters: [B(1)] → 유효 prio=1 PI 체인에서 plist의 역할 각 태스크의 pi_waiters plist에서 plist_first()로 최고 우선순위 대기자를 O(1)에 조회합니다 PI 전파 시 plist_del() + plist_add() (또는 plist_requeue)로 대기자 우선순위를 재배치합니다 유효 우선순위 = min(기본 prio, pi_waiters의 plist_first→prio) — plist_head_empty() 체크 필수 체인 깊이 제한: 커널은 무한 체인 방지를 위해 deadlock 감지 로직을 포함합니다
/* PI 체인에서 plist를 사용하는 유효 우선순위 계산 (개념적 코드) */
static int effective_prio(struct task_struct *task)
{
    int prio = task->normal_prio;  /* 기본 우선순위 */

    /* pi_waiters가 비어 있지 않으면 최고 대기자 확인 */
    if (!plist_head_empty(&task->pi_waiters)) {
        int pi_prio = plist_first(&task->pi_waiters)->prio;
        if (pi_prio < prio)   /* 낮은 값 = 높은 우선순위 */
            prio = pi_prio;   /* 우선순위 부스트 */
    }
    return prio;
}
/* PI 전파 시 plist 재정렬 — rt_mutex_adjust_prio_chain 핵심 로직 */
static int rt_mutex_adjust_prio_chain(struct task_struct *task, ...)
{
    /* 체인을 따라가며 각 단계에서: */
    while (task) {
        struct rt_mutex *lock = task->pi_blocked_on;
        struct task_struct *owner;

        if (!lock)
            break;  /* 체인 종료 */

        owner = rt_mutex_owner(lock);

        /* 대기자의 우선순위가 변경되었으므로 plist 재정렬 */
        plist_del(&waiter->list_entry, &lock->wait_list);
        waiter->list_entry.prio = task->prio;
        plist_add(&waiter->list_entry, &lock->wait_list);

        /* owner의 pi_waiters도 재정렬 */
        plist_del(&waiter->pi_list_entry, &owner->pi_waiters);
        waiter->pi_list_entry.prio = task->prio;
        plist_add(&waiter->pi_list_entry, &owner->pi_waiters);

        /* owner의 유효 우선순위 업데이트 */
        __rt_mutex_adjust_prio(owner);

        /* 체인의 다음 단계로 */
        task = owner;
    }
    return 0;
}
/* PI 관련 plist 검증 모듈 */
static void test_pi_plist(void)
{
    PLIST_HEAD(pi_waiters);
    struct plist_node w1, w2, w3;

    /* 대기자 3개: prio 50, 10, 30 */
    plist_node_init(&w1, 50);
    plist_node_init(&w2, 10);
    plist_node_init(&w3, 30);
    plist_add(&w1, &pi_waiters);
    plist_add(&w2, &pi_waiters);
    plist_add(&w3, &pi_waiters);

    /* plist_first = w2(10) → 유효 prio = min(기본, 10) */
    pr_info("highest waiter prio: %d\n",
            plist_first(&pi_waiters)->prio);  /* 10 */

    /* w2의 prio 변경: 10 → 60 (우선순위 하락) */
    plist_del(&w2, &pi_waiters);
    plist_node_init(&w2, 60);
    plist_add(&w2, &pi_waiters);

    /* 새로운 plist_first = w3(30) */
    pr_info("after reprio: %d\n",
            plist_first(&pi_waiters)->prio);  /* 30 */
}

PM QoS 상세

PM QoS(Power Management Quality of Service)는 장치 드라이버와 사용자 공간이 전력 관리에 대한 레이턴시 제약(Latency Constraint)을 등록하는 프레임워크입니다. cpu_dma_latency가 대표적인 PM QoS 매개변수이며, 등록된 모든 제약 중 최소값(가장 엄격한 제약)을 plist_first()로 O(1)에 조회합니다.

PM QoS 동작 메커니즘
  • cpu_dma_latency: CPU가 저전력 상태(C-state)에서 깨어나는 최대 허용 시간을 마이크로초 단위로 지정합니다. 예: 오디오 드라이버가 100μs를 등록하면, CPU는 깨어나는 데 100μs 이상 걸리는 깊은 C-state에 진입하지 않습니다.
  • plist 사용 이유: 여러 드라이버가 각기 다른 제약을 등록하며, 항상 가장 작은 값(가장 엄격한 제약)이 적용되어야 합니다. plist는 plist_first()로 O(1) 최솟값 접근을 제공하므로 이 용도에 최적입니다.
  • 갱신 경로: pm_qos_update_target()에서 기존 request를 plist_del()로 제거하고, 새 값으로 plist_add()를 수행합니다. 이 과정에서 plist_first()의 값이 변경되면 노티파이어(Notifier) 체인을 통해 cpuidle 거버너에 알립니다.
PM QoS cpu_dma_latency: plist 기반 제약 관리 등록된 QoS 요청 (plist로 관리) plist_head (cpu_dma_latency constraints) 오디오: 100μs plist_first() USB: 500μs 대표 노드 네트워크: 1000μs 대표 노드 GPU: 2000μs → 적용값: 100μs 제약 갱신 흐름 드라이버 pm_qos_add_request() pm_qos_update_target() plist_add/del plist_first() 최소 레이턴시 조회 cpuidle 거버너 C-state 선택 C-state 선택 예시 (제약: 100μs) C1: 10μs ✓ C2: 80μs ✓ C3: 500μs ✕ C4: 2000μs ✕ → C2까지만 허용 오디오 드라이버가 해제(pm_qos_remove_request)하면 → plist_del → 새 plist_first = USB(500μs) → C3(500μs)도 허용되어 전력 절감이 증가합니다. notifier 체인으로 cpuidle에 즉시 통보됩니다. plist의 O(1) 최솟값 접근이 PM QoS의 성능 핵심입니다
/* kernel/power/qos.c — PM QoS plist 핵심 구조 */
struct pm_qos_constraints {
    struct plist_head list;       /* 모든 request의 plist */
    s32              target_value; /* 현재 적용 중인 값 */
    s32              default_value;
    s32              no_constraint_value;
    enum pm_qos_type type;        /* MIN, MAX, SUM */
    struct blocking_notifier_head *notifiers;
};

/* pm_qos_update_target — plist 갱신 후 notifier 호출 */
static int pm_qos_update_target(
    struct pm_qos_constraints *c,
    struct plist_node *node,
    enum pm_qos_req_action action,
    s32 value)
{
    s32 prev_value, curr_value;

    prev_value = pm_qos_get_value(c);  /* 갱신 전 값 */

    switch (action) {
    case PM_QOS_ADD_REQ:
        plist_add(node, &c->list);
        break;
    case PM_QOS_UPDATE_REQ:
        plist_del(node, &c->list);
        node->prio = value;
        plist_add(node, &c->list);
        break;
    case PM_QOS_REMOVE_REQ:
        plist_del(node, &c->list);
        break;
    }

    curr_value = pm_qos_get_value(c);  /* 갱신 후 값 */

    /* 값이 변경되면 notifier 체인 호출 → cpuidle 거버너 알림 */
    if (prev_value != curr_value) {
        c->target_value = curr_value;
        blocking_notifier_call_chain(c->notifiers,
            (unsigned long)curr_value, NULL);
    }
    return prev_value != curr_value;
}

/* pm_qos_get_value — type에 따라 plist_first/last 사용 */
static s32 pm_qos_get_value(struct pm_qos_constraints *c)
{
    if (plist_head_empty(&c->list))
        return c->no_constraint_value;

    switch (c->type) {
    case PM_QOS_MIN:
        return plist_first(&c->list)->prio;  /* O(1) 최솟값 */
    case PM_QOS_MAX:
        return plist_last(&c->list)->prio;   /* O(1) 최댓값 */
    default:
        return c->no_constraint_value;
    }
}
/* 사용자 공간에서 PM QoS 제약 등록 예시 (/dev/cpu_dma_latency) */
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>

int main(void)
{
    int fd;
    int32_t latency_us = 100;  /* 100μs 제약 */

    /* fd를 열고 write하면 pm_qos_add_request() 호출
     * → plist에 prio=100인 노드 삽입
     * → plist_first()가 100이면 cpuidle 거버너에 통보 */
    fd = open("/dev/cpu_dma_latency", O_WRONLY);
    write(fd, &latency_us, sizeof(latency_us));

    /* fd가 열려 있는 동안 제약 유지 */
    sleep(60);

    /* close() → pm_qos_remove_request() → plist_del()
     * → 제약 해제 → 깊은 C-state 허용 */
    close(fd);
    return 0;
}

메모리 레이아웃

plist 자료구조의 실제 메모리 배치와 크기를 분석합니다. struct plist_node는 두 개의 list_head와 하나의 정수 필드로 구성되며, 아키텍처에 따라 패딩(Padding)이 발생할 수 있습니다.

plist 메모리 레이아웃 (64-bit, LP64) struct plist_node (40 bytes on x86-64) 오프셋 필드 크기 설명 +0 int prio 4B 우선순위 값 (낮을수록 높은 우선순위) +4 (padding) 4B 정렬 패딩 (list_head는 8B 정렬 필요) +8 prio_list.next 8B prio_list 순방향 포인터 +16 prio_list.prev 8B prio_list 역방향 포인터 +24 node_list.next 8B node_list 순방향 포인터 +32 node_list.prev 8B node_list 역방향 포인터 합계: 40 bytes (4 + 4패딩 + 16 + 16) struct plist_head (16 bytes on x86-64, 커널 6.x) +0 node_list (list_head) 16B 전체 노드 연결 리스트 헤드 (prio_list 앵커는 커널 6.x에서 제거됨) 합계: 16 bytes (list_head 1개) 자료구조 크기 비교 (64-bit) list_head: 16B (노드) plist_node: 40B (+150%) rb_node: 24B (참고) plist_node는 list_head 대비 2.5배 크지만, 노드 수가 적은(수십 개) RT/PM QoS 용도에서는 무시 가능합니다 32-bit 시스템: plist_node = 20B (prio 4B + prio_list 8B + node_list 8B, 패딩 없음)
/* plist_node 크기 분석 — pahole 출력 재현 */
struct plist_node {
    int                prio;         /*     0     4 */

    /* XXX 4 bytes hole, try to pack */

    struct list_head   prio_list;    /*     8    16 */
    struct list_head   node_list;    /*    24    16 */

    /* size: 40, cachelines: 1, members: 3 */
    /* sum members: 36, holes: 1, sum holes: 4 */
    /* last cacheline: 40 bytes */
};

struct plist_head {
    struct list_head   node_list;    /*     0    16 */

    /* size: 16, cachelines: 1, members: 1 */
    /* last cacheline: 16 bytes */
};

/* 캐시라인 분석: 64B 캐시라인 기준
 * plist_node 1개 = 40B → 1 캐시라인에 들어갑니다
 * plist_head + 첫 노드 = 16 + 40 = 56B → 1 캐시라인
 * → plist_first() 접근 시 head와 첫 노드가 같은 캐시라인에
 *   들어갈 수 있어 캐시 효율이 좋습니다 */

모듈 예제

plist의 동작을 /proc 파일 시스템을 통해 확인할 수 있는 커널 모듈 예제입니다. 모듈 로드 시 plist를 구성하고, /proc/plist_lab에서 현재 상태를 읽을 수 있습니다.

/* plist_lab.c — /proc 인터페이스 plist 실습 모듈 */
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/plist.h>
#include <linux/slab.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("plist /proc lab module");

struct task_entry {
    struct plist_node node;
    char              name[16];
    int               id;
};

static PLIST_HEAD(task_plist);
static struct task_entry entries[8];
static int nr_entries;

static const struct {
    const char *name;
    int         prio;
} init_data[] = {
    { "audio-rt",     1  },
    { "network-irq",  10 },
    { "usb-poll",     10 },
    { "display-vsync", 5 },
    { "gpu-render",   20 },
    { "disk-flush",   15 },
    { "timer-tick",   1  },
    { "idle-bg",      99 },
};

/* /proc/plist_lab 출력 */
static int plist_lab_show(struct seq_file *m, void *v)
{
    struct task_entry *entry;
    int idx = 0;

    seq_puts(m, "=== plist 상태 ===\n");
    seq_printf(m, "비어 있음: %s\n",
               plist_head_empty(&task_plist) ? "예" : "아니오");

    if (!plist_head_empty(&task_plist)) {
        struct task_entry *first, *last;
        first = plist_first_entry(&task_plist, struct task_entry, node);
        last  = plist_last_entry(&task_plist, struct task_entry, node);
        seq_printf(m, "최고 우선순위: %s (prio=%d)\n",
                   first->name, first->node.prio);
        seq_printf(m, "최저 우선순위: %s (prio=%d)\n",
                   last->name, last->node.prio);
    }

    seq_puts(m, "\n순서  이름             prio  대표\n");
    seq_puts(m, "----  ---------------  ----  ----\n");
    plist_for_each_entry(entry, &task_plist, node) {
        seq_printf(m, "%4d  %-15s  %4d  %s\n",
                   idx++, entry->name, entry->node.prio,
                   !list_empty(&entry->node.prio_list) ?
                       "YES" : "no");
    }

    seq_printf(m, "\n총 노드 수: %d\n", idx);
    seq_printf(m, "plist_node 크기: %zu bytes\n",
               sizeof(struct plist_node));
    seq_printf(m, "plist_head 크기: %zu bytes\n",
               sizeof(struct plist_head));
    seq_printf(m, "task_entry 크기: %zu bytes\n",
               sizeof(struct task_entry));
    return 0;
}

static int plist_lab_open(struct inode *inode, struct file *file)
{
    return single_open(file, plist_lab_show, NULL);
}

static const struct proc_ops plist_lab_ops = {
    .proc_open    = plist_lab_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

static int __init plist_lab_init(void)
{
    int i;

    for (i = 0; i < ARRAY_SIZE(init_data); i++) {
        plist_node_init(&entries[i].node, init_data[i].prio);
        strscpy(entries[i].name, init_data[i].name,
                sizeof(entries[i].name));
        entries[i].id = i;
        plist_add(&entries[i].node, &task_plist);
    }
    nr_entries = ARRAY_SIZE(init_data);

    proc_create("plist_lab", 0444, NULL, &plist_lab_ops);
    pr_info("plist_lab: loaded with %d entries\n", nr_entries);
    return 0;
}

static void __exit plist_lab_exit(void)
{
    int i;

    remove_proc_entry("plist_lab", NULL);
    for (i = 0; i < nr_entries; i++)
        plist_del(&entries[i].node, &task_plist);
    pr_info("plist_lab: unloaded\n");
}

module_init(plist_lab_init);
module_exit(plist_lab_exit);
# 모듈 빌드 및 테스트
# Makefile:
# obj-m += plist_lab.o
# make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

$ sudo insmod plist_lab.ko
$ cat /proc/plist_lab
=== plist 상태 ===
비어 있음: 아니오
최고 우선순위: audio-rt (prio=1)
최저 우선순위: idle-bg (prio=99)

순서  이름             prio  대표
----  ---------------  ----  ----
   0  audio-rt            1  YES
   1  timer-tick           1  no
   2  display-vsync        5  YES
   3  network-irq         10  YES
   4  usb-poll            10  no
   5  disk-flush          15  YES
   6  gpu-render          20  YES
   7  idle-bg             99  YES

총 노드 수: 8
plist_node 크기: 40 bytes
plist_head 크기: 16 bytes
task_entry 크기: 64 bytes

$ sudo rmmod plist_lab

커널 소스 심층 분석

plist의 핵심 구현을 소스 코드 수준에서 더 깊이 분석합니다. plist_add()의 이중 리스트 조작, plist_del()의 대표 노드 승계 메커니즘, 그리고 plist_first() / plist_last()의 O(1) 접근 원리를 추적합니다.

plist_add(): prio_list + node_list 이중 구조 유지

plist_add()의 가장 정교한 부분은 두 리스트를 동시에 올바르게 조작하는 것입니다. 삽입 시 prio_list를 순회하여 위치를 결정하고, node_list에는 FIFO 순서를 유지하며, 새로운 고유 우선순위인 경우에만 prio_list에 노드를 추가합니다.

/* lib/plist.c - plist_add 핵심 로직 상세 분석 */
void plist_add(struct plist_node *node, struct plist_head *head)
{
    struct plist_node *first, *iter, *prev = NULL;
    struct list_head *node_next = &head->node_list;

    /* [Phase 0] 디버그 검증
     * CONFIG_DEBUG_PLIST 활성화 시 6개 불변식 검증.
     * 프로덕션에서는 빈 인라인 함수로 컴파일됩니다. */
    plist_check_head(head);
    WARN_ON(!plist_node_empty(node));
    WARN_ON(!list_empty(&node->prio_list));

    /* [Phase 1] 빈 리스트 처리
     * head->node_list가 자기 자신을 가리키면 빈 리스트.
     * 첫 노드는 node_list와 prio_list 모두의 유일한 엔트리가 됩니다. */
    if (plist_head_empty(head))
        goto ins_node;

    /* [Phase 2] prio_list 순회 - O(K) 핵심 루프
     * prio_list는 각 고유 우선순위의 대표 노드만 연결합니다.
     * 이 루프에서 방문하는 노드 수 = K (고유 우선순위 수).
     *
     * 불변식: prio_list는 prio 오름차순으로 정렬되어 있으므로,
     * node->prio보다 큰 첫 대표 노드를 찾으면 그 앞이 삽입 위치입니다. */
    first = plist_first(head);

    do {
        /* 현재 대표 노드의 prio보다 새 노드의 prio가 작으면
         * (= 더 높은 우선순위이면) 이 대표 노드 앞에 삽입합니다. */
        if (node->prio < first->prio) {
            node_next = &first->node_list;
            break;
        }

        /* prev를 갱신: 나중에 같은 prio인지 판별하는 데 사용됩니다.
         * prev->prio == node->prio이면 기존 그룹에 합류 (prio_list 변경 없음).
         * prev->prio != node->prio이면 새 고유 prio (prio_list에 추가). */
        prev = first;

        /* prio_list의 다음 대표 노드로 이동.
         * prio_list.next가 자기 자신(first)의 prio_list를 가리키면
         * 순환(circular) 리스트를 한 바퀴 돈 것이므로 루프를 종료합니다. */
        iter = list_entry(first->prio_list.next,
                          struct plist_node, prio_list);

        if (&iter->prio_list == &first->prio_list)
            break;  /* 순환 완료: 리스트 끝에 삽입 */

        first = iter;
    } while (1);

ins_node:
    /* [Phase 3] node_list 삽입
     * list_add_tail()은 node_next 바로 앞에 삽입합니다.
     *
     * 같은 prio 그룹이 이미 있을 때:
     *   node_next = 다음 prio 그룹의 첫 노드의 node_list
     *   → 현재 prio 그룹의 마지막 뒤, 다음 그룹 앞에 삽입 = FIFO
     *
     * 새 prio일 때:
     *   node_next = first(다음으로 큰 prio)의 node_list
     *   → 정확히 올바른 정렬 위치에 삽입 */
    list_add_tail(&node->node_list, node_next);

    /* [Phase 4] prio_list 연결 결정
     * prev == NULL: 빈 리스트에 삽입하거나, 모든 기존 노드보다
     *               높은 우선순위 → 새로운 고유 prio → prio_list에 추가
     * prev->prio != node->prio: 새로운 고유 prio → prio_list에 추가
     * prev->prio == node->prio: 기존 그룹 합류 → prio_list 변경 없음
     *                           node의 prio_list는 self-pointing 유지 */
    if (!prev || prev->prio != node->prio) {
        list_add(&node->prio_list, &iter->prio_list);
    }

    plist_check_head(head);
}
코드 설명

plist_add()는 4단계(Phase 0~4)로 동작합니다.

  • Phase 0 (검증): 노드가 이미 다른 리스트에 속해 있지 않은지, prio_list가 초기 상태(self-pointing)인지 확인합니다. 위반 시 WARN_ON으로 경고합니다.
  • Phase 1 (빈 리스트): 빈 리스트이면 Phase 2를 건너뛰고 바로 삽입합니다. 이때 node_next&head->node_list이므로 head 바로 앞(= 유일한 엔트리)에 삽입됩니다.
  • Phase 2 (prio_list 순회): do-while 루프에서 K개의 대표 노드만 방문합니다. node->prio < first->prio 조건으로 삽입 위치를 결정합니다. 순환 리스트 탐지(&iter->prio_list == &first->prio_list)로 리스트 끝을 감지합니다.
  • Phase 3 (node_list 삽입): list_add_tail()로 FIFO 순서를 보장합니다. list_add()(앞에 삽입)가 아닌 list_add_tail()(뒤에 삽입)을 사용하여, 같은 우선순위 그룹 내에서 기존 노드들 뒤에 배치됩니다.
  • Phase 4 (prio_list 연결): 새로운 고유 우선순위인 경우에만 prio_list에 추가합니다. 기존 그룹에 합류하는 경우 prio_list는 건드리지 않아 대표 노드가 변경되지 않습니다.

plist_del(): 대표 노드 승계의 정확한 메커니즘

plist_del()에서 가장 미묘한 부분은 대표 노드가 삭제될 때 후속 노드에게 prio_list 연결을 정확히 이관하는 과정입니다. 이 과정에서 list_add()를 사용하여 원자적으로(Atomically) 승계합니다.

/* lib/plist.c - plist_del 대표 승계 메커니즘 상세 분석 */
void plist_del(struct plist_node *node, struct plist_head *head)
{
    plist_check_head(head);

    /* [Step 1] 대표 노드 여부 판별
     * list_empty()는 prio_list가 자기 자신을 가리키는지 확인합니다.
     * - 대표 노드: prio_list가 이전/다음 대표 노드를 가리킴 → !empty
     * - 비대표 노드: prio_list가 자기 자신을 가리킴 → empty */
    if (!list_empty(&node->prio_list)) {
        /* 대표 노드 삭제 처리 */

        /* [Step 2] 후속 노드 존재 여부 확인
         * node_list.next가 head의 node_list이면 이 노드가 리스트의
         * 마지막 노드입니다. 이 경우 승계할 후속 노드가 없습니다. */
        if (node->node_list.next != &head->node_list) {
            struct plist_node *next;

            /* node_list의 다음 노드를 plist_node로 변환 */
            next = list_entry(node->node_list.next,
                              struct plist_node, node_list);

            /* [Step 3] 같은 우선순위 승계 판별
             * 다음 노드의 prio가 현재 노드와 같으면:
             *   → 해당 그룹이 여전히 존재하므로 다음 노드가 새 대표
             *   → list_add()로 next의 prio_list를 node의 prio_list 위치에 삽입
             *
             * 다음 노드의 prio가 다르면:
             *   → 이 prio 그룹의 유일한 노드였으므로 그룹 자체가 소멸
             *   → prio_list에서 단순 제거 */
            if (next->prio == node->prio)
                list_add(&next->prio_list, &node->prio_list);
                /* list_add의 효과:
                 * next->prio_list.prev = node->prio_list.prev (이전 대표)
                 * next->prio_list.next = node->prio_list.next (다음 대표)
                 * → next가 prio_list 체인에 node의 자리를 그대로 이어받음 */
        }
        /* [Step 4] 기존 대표의 prio_list 연결 해제
         * list_del_init()은 node의 prio_list를 체인에서 제거하고,
         * self-pointing 상태로 복원합니다.
         * Step 3에서 승계가 이루어졌다면 next가 이미 체인에 들어갔으므로
         * node를 제거해도 체인이 끊어지지 않습니다. */
        list_del_init(&node->prio_list);
    }

    /* [Step 5] node_list에서 제거
     * 대표/비대표 모두 node_list에서는 단순 제거합니다.
     * list_del_init()으로 self-pointing 상태를 복원하여
     * plist_node_empty()가 올바르게 true를 반환하도록 합니다. */
    list_del_init(&node->node_list);

    plist_check_head(head);
}
코드 설명

plist_del()의 승계 메커니즘에서 핵심은 list_add()의 동작입니다.

  • list_add(&next->prio_list, &node->prio_list): 이 호출은 node->prio_list 바로 뒤에 next->prio_list를 삽입합니다. 그 후 list_del_init(&node->prio_list)으로 node를 제거하면, 결과적으로 next가 node의 prio_list 체인 위치를 정확히 이어받습니다.
  • 순서가 중요합니다: 반드시 list_add()를 먼저 수행한 후 list_del_init()을 수행해야 합니다. 순서가 바뀌면 node가 체인에서 제거된 후 next를 삽입할 참조 지점이 사라집니다.
  • 비대표 노드 삭제: prio_list가 self-pointing이면 아무런 prio_list 조작 없이 node_list에서만 제거합니다. 이 경우 O(1) 상수 시간에 완료됩니다.

plist_first() / plist_last(): O(1) 접근의 원리

plist_first()plist_last()가 O(1)에 동작하는 이유는 node_list가 항상 정렬 상태를 유지하기 때문입니다. head의 node_list.next는 최소 prio, node_list.prev는 최대 prio를 가진 노드를 가리킵니다.

/* include/linux/plist.h - O(1) 접근 함수 */

/* plist_first: 가장 높은 우선순위(최소 prio) 노드
 *
 * head->node_list 는 이중 연결 순환 리스트의 앵커입니다.
 * node_list.next는 항상 가장 작은 prio를 가진 노드의 node_list를 가리킵니다.
 *
 * 이 불변식은 plist_add()가 올바른 위치에 삽입하고,
 * plist_del()이 정렬을 깨뜨리지 않으므로 항상 유지됩니다.
 *
 * 메모리 접근: 1회 포인터 추적 + container_of 오프셋 계산
 * = 순수 O(1), 분기(branch) 없음 */
static inline struct plist_node *plist_first(
    const struct plist_head *head)
{
    return list_entry(head->node_list.next,
                       struct plist_node, node_list);
}

/* plist_last: 가장 낮은 우선순위(최대 prio) 노드
 *
 * node_list.prev는 항상 가장 큰 prio를 가진 노드를 가리킵니다.
 * PM QoS의 PM_QOS_MAX 타입에서 사용됩니다. */
static inline struct plist_node *plist_last(
    const struct plist_head *head)
{
    return list_entry(head->node_list.prev,
                       struct plist_node, node_list);
}

/* 활용 예: RT-Mutex에서 최고 우선순위 대기자 조회
 *
 * struct plist_node *top = plist_first(&mutex->wait_list);
 * int top_prio = top->prio;
 * // 소유자의 실행 우선순위를 top_prio로 부스트
 *
 * 이 조회가 O(1)이므로 우선순위 상속 프로토콜의
 * 실시간 보장(Real-Time Guarantee)을 충족합니다. */
코드 설명
  • list_entry: container_of()의 별칭으로, node_list 포인터에서 plist_node 구조체의 시작 주소를 역산합니다. 이 연산은 컴파일 타임 상수 오프셋 뺄셈이므로 런타임 비용이 제로입니다.
  • 분기 없는 구현: plist_first()plist_last()는 조건문이 없습니다. 호출자가 plist_head_empty()로 빈 리스트를 먼저 확인해야 합니다. 빈 리스트에서 호출하면 head 자체를 plist_node로 잘못 해석하여 정의되지 않은 동작이 발생합니다.
  • 실시간 보장: RT 시스템에서 최고 우선순위 대기자 조회는 WCET(Worst-Case Execution Time)가 보장되어야 합니다. plist_first()의 O(1) 상수 시간은 이 요구사항을 완벽히 충족합니다.

PI(Priority Inheritance) Mutex에서의 plist 활용

RT-Mutex의 초기 구현에서 plist가 우선순위 상속 체인을 관리하는 방식을 소스 코드 수준에서 분석합니다. 현재 커널은 rbtree를 사용하지만, 개념적 메커니즘은 동일합니다.

/* kernel/locking/rtmutex.c (2.6.x~3.x 시절 plist 기반 구현) */

/* 태스크가 RT-Mutex를 기다릴 때:
 * 1. waiter 구조체의 plist_node를 태스크 prio로 초기화
 * 2. mutex의 wait_list(plist)에 삽입
 * 3. 소유자의 pi_waiters(plist)에도 삽입
 * 4. pi_waiters의 first가 변경되면 소유자 prio 부스트 */

static int rt_mutex_adjust_prio_chain(
    struct task_struct *task,
    int deadlock_detect,
    struct rt_mutex *orig_lock,
    struct rt_mutex_waiter *orig_waiter,
    struct task_struct *top_task)
{
    struct rt_mutex_waiter *waiter, *top_waiter;
    struct rt_mutex *lock;

    /* PI 체인을 따라 올라가며 우선순위를 전파합니다.
     * 각 단계에서 plist_first()로 최고 대기 우선순위를 O(1)에 조회합니다. */

    while (1) {
        /* task가 대기 중인 mutex의 소유자를 찾습니다 */
        lock = task->pi_blocked_on->lock;

        /* 소유자의 pi_waiters plist에서 최고 우선순위 조회 */
        if (!plist_head_empty(&task->pi_waiters)) {
            top_waiter = plist_first_entry(
                &task->pi_waiters,
                struct rt_mutex_waiter, pi_list_entry);
            /* O(1)에 최고 대기 우선순위 획득 */

            /* 소유자의 실행 우선순위를 부스트 */
            if (top_waiter->pi_list_entry.prio < task->prio)
                rt_mutex_setprio(task, top_waiter->pi_list_entry.prio);
        }

        /* 체인의 다음 단계로 이동 (소유자가 대기 중인 다른 mutex) */
        if (!task->pi_blocked_on)
            break;
        task = lock->owner;
    }
    return 0;
}
코드 설명

PI 체인 전파에서 plist의 역할을 분석합니다.

  • pi_waiters plist: 각 태스크가 보유한 모든 RT-Mutex에서 대기 중인 태스크들의 우선순위를 관리합니다. plist_first()로 가장 높은 대기 우선순위를 O(1)에 조회합니다.
  • 체인 전파: Task A가 Mutex M1을 보유하고, Task B(prio=50)가 M1을 대기합니다. Task A의 pi_waiters plist에서 first가 B(50)이므로 A의 우선순위를 50으로 부스트합니다. A가 다시 M2를 대기 중이면 M2의 소유자에게도 전파합니다.
  • O(1) 조회의 중요성: PI 체인 전파는 실시간 태스크의 스케줄링 레이턴시에 직접 영향을 줍니다. 각 체인 단계에서 O(1)에 최고 우선순위를 조회할 수 있어야 전파 시간이 예측 가능합니다.

plain list_sort 접근 방식과의 비교

plist 대신 일반 list_headlist_sort()를 사용하는 접근 방식과 비교합니다.

/* 대안 1: list_head + list_sort() (비추천) */
struct waiter_simple {
    struct list_head list;
    int              prio;
};

/* 삽입 후 매번 전체 리스트를 재정렬
 * list_sort(): 머지 소트 O(N log N)
 * → 삽입마다 O(N log N) 비용! */
list_add_tail(&waiter->list, &head);
list_sort(NULL, &head, compare_prio);

/* 대안 2: list_head + 정렬 삽입 O(N) */
struct waiter_simple *pos;
list_for_each_entry(pos, &head, list) {
    if (waiter->prio < pos->prio) {
        list_add_tail(&waiter->list, &pos->list);
        goto done;
    }
}
list_add_tail(&waiter->list, &head);
done:

/* 비교:
 * 방식         삽입      삭제   최솟값  메모리(노드)
 * list_sort   O(NlogN)  O(1)   O(1)    16 bytes
 * list 정렬삽입 O(N)     O(1)   O(1)    16 bytes
 * plist       O(K)      O(1)   O(1)    40 bytes
 *
 * N=100, K=10일 때:
 * list_sort: ~660회 비교 (삽입마다)
 * list 정렬삽입: 최대 100회 비교
 * plist: 최대 10회 비교
 *
 * plist는 메모리 24 bytes 추가로 삽입 성능 10배 향상을 달성합니다.
 * 대기열 크기가 작고 고유 우선순위가 제한적인 RT 환경에서 최적입니다. */
plist 이중 리스트 내부 포인터 상세 구조 prio_list: 고유 우선순위 대표만 연결 | node_list: 모든 노드 정렬 연결 prio_list 연결 node_list 연결 plist_head node_list (앵커) 노드 A prio = 5 prio_list: 연결됨 (대표) node_list: prev=HEAD node_list: next=B 노드 B prio = 5 prio_list: self (비대표) node_list: prev=A, next=C 노드 C prio = 10 prio_list: 연결됨 (대표) node_list: prev=B node_list: next=D 노드 D prio = 10 prio_list: self (비대표) node_list: prev=C, next=E 노드 E prio = 20 prio_list: 연결됨 (대표) node_list: prev=D node_list: next=HEAD self self prio_list 순회 경로 (삽입 시) A(5) ---- C(10) ---- E(20) 3개 노드만 방문 (K=3), B/D는 건너뜀 node_list 순회 경로 (순회 시) A(5) → B(5) → C(10) → D(10) → E(20) 5개 노드 모두 방문 (N=5) 핵심: 이중 리스트의 역할 분리 prio_list = 삽입 최적화 (O(K) 탐색) | node_list = 순회/삭제/first/last (O(1)/O(N))

plist_add 삽입 위치 결정 다이어그램

plist_add가 새 노드의 삽입 위치를 결정하는 과정을 단계별로 시각화합니다. prio_list를 따라 이동하며 node->prio < first->prio 조건을 검사하는 핵심 루프의 동작을 보여줍니다.

plist_add(prio=12) 삽입 위치 결정: prio_list 순회 초기 상태: node_list = [3] → [5] → [5] → [10] → [10] → [20] → HEAD prio_list 체인 (대표 노드만): prio=3 대표 prio=5 대표 prio=10 대표 prio=20 대표 NEW: prio=12 삽입할 노드 루프 반복 1: first = prio=3 12 < 3? 아니오 (12 ≥ 3) → prev = prio=3, first를 다음 대표(prio=5)로 이동 prio_list.next를 따라 prio=5 대표 노드로 이동 루프 반복 2: first = prio=5 12 < 5? 아니오 (12 ≥ 5) → prev = prio=5, first를 다음 대표(prio=10)로 이동 prio_list.next를 따라 prio=10 대표 노드로 이동 루프 반복 3: first = prio=10 12 < 10? 아니오 (12 ≥ 10) → prev = prio=10, first를 다음 대표(prio=20)로 이동 prio_list.next를 따라 prio=20 대표 노드로 이동 루프 반복 4: first = prio=20 12 < 20? 예! (12 < 20) → node_next = &prio=20의 node_list, break! 삽입 위치 결정: prio=20 노드의 node_list 앞 (= prio=10 그룹 뒤) 삽입 결과 node_list: [3] → [5] → [5] → [10] → [10] → [12] → [20] → HEAD prio_list: [3] ---- [5] ---- [10] ---- [12] ---- [20] (K: 4 → 5) prev->prio(10) ≠ node->prio(12) → 새로운 고유 우선순위 → prio_list에 추가 prio_list 순회 4회 (K=4) vs node_list 전체 순회 6회 (N=6) → 33% 절감

RT-Mutex PI 체인과 plist 상세 다이어그램

RT-Mutex의 우선순위 상속(Priority Inheritance) 체인에서 plist가 어떻게 사용되는지 상세하게 시각화합니다. 복수의 mutex와 태스크가 연쇄적으로 연결된 PI 체인에서 plist의 O(1) 최고 우선순위 조회가 실시간 성능을 보장하는 원리를 보여줍니다.

RT-Mutex PI 체인: 연쇄 우선순위 상속과 plist Task C(prio=30)가 Mutex M2를 소유하고, Task B(prio=60)가 M2를 대기. Task B는 Mutex M1을 소유하고, Task A(prio=90)가 M1을 대기. Task A (원래 prio=90) Mutex M1 대기 중 pi_waiters: (비어있음) pi_blocked_on: M1 Mutex M1 owner = Task B wait_list (plist): [ A(90) ] Task B (원래 prio=60) M1 소유, M2 대기 중 pi_waiters: [ A(90) ] pi_blocked_on: M2 Mutex M2 owner = Task C wait_list (plist): [ B(60) ] waits owns waits Task D (prio=30)가 Mutex M1을 요청하여 대기열에 합류 plist_add(&D_waiter, &M1->wait_list) → M1의 wait_list: [ D(30), A(90) ] PI 체인 전파 결과 Task D (prio=30) M1 대기 중 pi_blocked_on: M1 Task A (prio=90) M1 대기 중 M1->wait_list (plist) D: 30 first A: 90 plist_first() = D(30) O(1)에 최고 우선순위 조회 Task B (원래 60) boosted prio = 30 pi_waiters plist: [ D(30), A(90) ] first=D(30) → B의 prio를 30으로 owns Task C (원래 30) boosted prio = 30 M2 소유 중 pi_waiters: [ B(30) ] first=B(30) → C prio 유지 owns PI 체인: D(30) → M1 → B → M2 → C plist의 핵심 역할: 체인 각 단계에서 O(1) 최고 우선순위 조회 M1->wait_list: plist_first() = D(30) → B를 30으로 부스트 B->pi_waiters: plist_first() = D(30) → M2 소유자(C)에게 전파 체인 길이가 L이면 총 L회의 plist_first() 호출 = O(L) 전파 시간

구현 예제: RT-Mutex 대기자 큐

RT-Mutex의 대기자 큐를 plist로 구현하는 패턴을 소스 코드 수준에서 재현합니다. 이 예제는 커널 2.6.x~3.x 시절의 실제 RT-Mutex 구현을 단순화한 것입니다.

#include <linux/plist.h>
#include <linux/spinlock.h>
#include <linux/sched.h>

/* RT-Mutex 대기자 구조체 (plist 기반) */
struct rt_waiter {
    struct plist_node    list_entry;    /* mutex의 wait_list에 연결 */
    struct plist_node    pi_list_entry; /* 소유자의 pi_waiters에 연결 */
    struct task_struct   *task;         /* 대기 중인 태스크 */
};

/* RT-Mutex 구조체 */
struct my_rt_mutex {
    struct plist_head    wait_list;     /* 대기자 plist (prio 정렬) */
    struct task_struct   *owner;        /* 현재 소유자 */
    raw_spinlock_t       wait_lock;     /* wait_list 보호용 락 */
};

/* mutex 초기화 */
static inline void my_rt_mutex_init(struct my_rt_mutex *lock)
{
    plist_head_init(&lock->wait_list);
    lock->owner = NULL;
    raw_spin_lock_init(&lock->wait_lock);
}

/* 대기자 등록: 태스크를 mutex의 대기열에 추가 */
static void rt_mutex_enqueue_waiter(
    struct my_rt_mutex *lock,
    struct rt_waiter *waiter)
{
    /* waiter의 plist_node를 태스크의 현재 우선순위로 초기화 */
    plist_node_init(&waiter->list_entry,
                    waiter->task->prio);

    /* mutex의 wait_list에 우선순위 순서로 삽입 (O(K)) */
    plist_add(&waiter->list_entry, &lock->wait_list);

    /* 소유자의 pi_waiters에도 등록하여 PI 추적 */
    if (lock->owner) {
        plist_node_init(&waiter->pi_list_entry,
                        waiter->task->prio);
        /* 소유자의 pi_waiters plist에 삽입
         * (실제 코드에서는 task_struct 내부의 pi_waiters 사용) */
    }
}

/* 우선순위 상속: 소유자의 우선순위를 부스트 */
static void rt_mutex_adjust_prio(
    struct my_rt_mutex *lock)
{
    struct task_struct *owner = lock->owner;
    int top_prio;

    if (!owner || plist_head_empty(&lock->wait_list))
        return;

    /* O(1)에 가장 높은 대기 우선순위 조회 */
    top_prio = plist_first(&lock->wait_list)->prio;

    /* 대기자의 우선순위가 소유자보다 높으면 부스트 */
    if (top_prio < owner->prio) {
        /* 소유자의 실행 우선순위를 대기자 수준으로 상승 */
        pr_info("PI boost: task %s prio %d -> %d\n",
                owner->comm, owner->prio, top_prio);
        /* rt_mutex_setprio(owner, top_prio); */
    }
}

/* 대기자 제거: mutex 해제 또는 대기 취소 시 */
static void rt_mutex_dequeue_waiter(
    struct my_rt_mutex *lock,
    struct rt_waiter *waiter)
{
    /* wait_list에서 O(1) 삭제 */
    plist_del(&waiter->list_entry, &lock->wait_list);

    /* PI 우선순위 재조정 필요 여부 확인 */
    rt_mutex_adjust_prio(lock);
}

/* mutex unlock: 최고 우선순위 대기자를 깨움 */
static struct rt_waiter *rt_mutex_top_waiter(
    struct my_rt_mutex *lock)
{
    if (plist_head_empty(&lock->wait_list))
        return NULL;

    /* O(1)에 최고 우선순위 대기자 반환 */
    return plist_first_entry(&lock->wait_list,
                             struct rt_waiter, list_entry);
}
코드 설명

이 예제는 RT-Mutex의 plist 기반 대기자 관리 패턴을 재현합니다.

  • 이중 plist_node: 각 대기자는 두 개의 plist_node를 보유합니다. list_entry는 mutex의 wait_list에, pi_list_entry는 소유자의 pi_waiters에 연결됩니다.
  • rt_mutex_enqueue_waiter: 태스크의 현재 prio 값으로 plist_node를 초기화한 후 plist_add()로 삽입합니다. O(K) 시간에 올바른 위치에 삽입됩니다.
  • rt_mutex_adjust_prio: plist_first()로 O(1)에 최고 대기 우선순위를 조회하고, 소유자의 우선순위보다 높으면 부스트합니다. 이 함수는 대기자 추가/제거 시마다 호출됩니다.
  • rt_mutex_top_waiter: mutex unlock 시 깨울 대기자를 O(1)에 결정합니다. 항상 가장 높은 우선순위의 태스크가 먼저 깨어납니다.

구현 예제: PM QoS 제약 관리

PM QoS(Power Management Quality of Service)에서 plist를 활용하여 다수의 드라이버가 등록한 전력 관리 제약 조건을 효율적으로 집계하는 패턴을 구현합니다.

#include <linux/plist.h>
#include <linux/spinlock.h>
#include <linux/notifier.h>

/* PM QoS 제약 집계 타입 */
enum qos_type {
    QOS_MIN,  /* 최솟값: 가장 엄격한 제약 (CPU latency) */
    QOS_MAX,  /* 최댓값: 가장 관대한 제약 */
};

/* 제약 관리 구조체 */
struct qos_constraints {
    struct plist_head    list;             /* 요청 plist */
    s32                  target_value;     /* 현재 활성 제약 */
    s32                  default_value;    /* 제약 없을 때 기본값 */
    enum qos_type       type;             /* 집계 방식 */
    spinlock_t           lock;
};

/* 개별 요청 구조체 */
struct qos_request {
    struct plist_node    node;             /* plist 엔트리 */
    const char           *name;            /* 요청자 이름 (디버그용) */
};

/* 제약 초기화 */
static void qos_constraints_init(
    struct qos_constraints *c,
    s32 default_val, enum qos_type type)
{
    plist_head_init(&c->list);
    c->target_value = default_val;
    c->default_value = default_val;
    c->type = type;
    spin_lock_init(&c->lock);
}

/* 현재 활성 제약 값 조회 (O(1)) */
static s32 qos_get_value(struct qos_constraints *c)
{
    if (plist_head_empty(&c->list))
        return c->default_value;

    switch (c->type) {
    case QOS_MIN:
        /* 가장 작은 값 = plist_first()->prio
         * PM QoS에서는 prio 필드에 제약 값(예: us)을 저장합니다.
         * plist가 오름차순 정렬이므로 first가 최솟값입니다. */
        return plist_first(&c->list)->prio;
    case QOS_MAX:
        /* 가장 큰 값 = plist_last()->prio */
        return plist_last(&c->list)->prio;
    }
    return c->default_value;
}

/* 제약 요청 추가 */
static void qos_add_request(
    struct qos_constraints *c,
    struct qos_request *req,
    s32 value)
{
    s32 prev_value, new_value;

    plist_node_init(&req->node, value);

    spin_lock(&c->lock);
    prev_value = qos_get_value(c);

    /* O(K) 삽입: prio=value로 정렬 위치에 삽입 */
    plist_add(&req->node, &c->list);

    new_value = qos_get_value(c);
    c->target_value = new_value;
    spin_unlock(&c->lock);

    if (prev_value != new_value) {
        pr_info("[PM QoS] %s: constraint changed %d -> %d\n",
                req->name, prev_value, new_value);
        /* 실제 커널에서는 notifier_call_chain()으로
         * idle governor 등에 변경을 알립니다. */
    }
}

/* 제약 요청 제거 */
static void qos_remove_request(
    struct qos_constraints *c,
    struct qos_request *req)
{
    s32 prev_value, new_value;

    spin_lock(&c->lock);
    prev_value = qos_get_value(c);

    /* O(1) 삭제 */
    plist_del(&req->node, &c->list);

    new_value = qos_get_value(c);
    c->target_value = new_value;
    spin_unlock(&c->lock);

    if (prev_value != new_value)
        pr_info("[PM QoS] %s removed: constraint %d -> %d\n",
                req->name, prev_value, new_value);
}

/* 제약 값 업데이트 */
static void qos_update_request(
    struct qos_constraints *c,
    struct qos_request *req,
    s32 new_value)
{
    spin_lock(&c->lock);
    if (!plist_node_empty(&req->node)) {
        /* del + reinit + add 패턴으로 위치 재조정 */
        plist_del(&req->node, &c->list);
    }
    plist_node_init(&req->node, new_value);
    plist_add(&req->node, &c->list);

    c->target_value = qos_get_value(c);
    spin_unlock(&c->lock);
}

/* 사용 예시: CPU Latency QoS */
static struct qos_constraints cpu_lat_qos;
static struct qos_request audio_req, net_req, usb_req;

static void pm_qos_example(void)
{
    /* QOS_MIN: 가장 엄격한(가장 작은) 레이턴시 제약을 활성화 */
    qos_constraints_init(&cpu_lat_qos, S32_MAX, QOS_MIN);

    /* 세 드라이버가 각각 제약 등록 */
    audio_req.name = "audio";
    qos_add_request(&cpu_lat_qos, &audio_req, 20);
    /* plist: [20(audio)], target=20 */

    net_req.name = "network";
    qos_add_request(&cpu_lat_qos, &net_req, 50);
    /* plist: [20(audio)] → [50(network)], target=20 */

    usb_req.name = "usb";
    qos_add_request(&cpu_lat_qos, &usb_req, 100);
    /* plist: [20(audio)] → [50(network)] → [100(usb)], target=20 */

    pr_info("current CPU latency target: %d us\n",
            cpu_lat_qos.target_value);
    /* 출력: 20 (가장 엄격한 오디오 제약) */

    /* 오디오 재생 종료 → 제약 해제 */
    qos_remove_request(&cpu_lat_qos, &audio_req);
    /* plist: [50(network)] → [100(usb)], target=50
     * → idle governor가 더 깊은 절전 상태 허용 */

    pr_info("after audio stop: target = %d us\n",
            cpu_lat_qos.target_value);
    /* 출력: 50 (네트워크 제약이 새 활성 제약) */

    /* 네트워크 드라이버가 제약 완화 */
    qos_update_request(&cpu_lat_qos, &net_req, 200);
    /* plist: [100(usb)] → [200(network)], target=100 */
}
코드 설명

PM QoS 제약 관리에서 plist의 활용 패턴을 분석합니다.

  • prio 필드의 의미 변환: RT-Mutex에서는 prio가 태스크 우선순위(작을수록 높음)를 의미합니다. PM QoS에서는 제약 값(예: 레이턴시 us)을 prio에 저장합니다. 두 경우 모두 "작은 값이 먼저"라는 plist의 정렬 규칙을 활용합니다.
  • QOS_MIN 집계: plist_first()->prio가 최솟값을 O(1)에 반환합니다. 여러 드라이버의 제약 중 가장 엄격한 값이 자동으로 활성화됩니다.
  • 동적 제약 갱신: 드라이버가 제약을 추가/제거/업데이트할 때마다 plist가 정렬을 유지하므로, 항상 O(1)에 현재 활성 제약을 조회할 수 있습니다.
  • 실전 영향: CPU idle governor는 target_value보다 긴 탈출 레이턴시를 가진 깊은 idle 상태를 사용하지 않습니다. 제약이 완화되면 더 깊은 절전이 허용되어 전력을 절감합니다.

참고 자료

plist와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.