RX/TX 심화 & 오프로드 (net_device)

net_device 데이터 경로의 고성능 구현과 오프로드(Offload) 기능을 심층 분석합니다. NAPI 기반 RX/TX 경로, BQL/LLTX 전송 제어, XDP/AF_XDP, 멀티큐·RSS, page_pool, TC 오프로드, busy poll, RT NAPI까지 다룹니다.

전제 조건: 네트워크 디바이스 드라이버 (net_device) 문서를 먼저 읽으세요. RX/TX 경로는 NAPI·큐·잠금(Lock) 모델을 전제로 하므로, netdev 수명주기와 콜백(Callback) 계약을 먼저 이해해야 합니다.
일상 비유: 이 주제는 공항 수하물 처리 라인과 비슷합니다. 도착(수신 RX)과 출발(송신 TX)을 분리하고, 게이트마다 대기열(Queue)을 두며, 폭주 시 속도 제한(BQL)과 우회로(오프로드)를 운영합니다.

핵심 요약

  • NAPI — 인터럽트(Interrupt) 폭풍을 막는 폴링(Polling) 기반 RX 처리 모델입니다.
  • TX 큐/BQL — 버퍼(Buffer)블로트(Bufferbloat)를 막고 지연(latency)을 낮춥니다.
  • XDP/AF_XDP — 드라이버 단에서 고속 패킷(Packet) 처리를 수행합니다.
  • page_pool — RX 페이지 재활용으로 캐시(Cache) 효율을 높입니다.
  • 오프로드 — checksum/GRO/TC 등을 하드웨어로 위임합니다.

단계별 이해

  1. RX 경로 이해
    IRQ → NAPI poll → skb 전달 흐름을 고정합니다.
  2. TX 경로 이해
    전송 요청부터 완료 인터럽트까지 추적합니다.
  3. 큐 제어 적용
    BQL/LLTX로 큐 정지·재개 조건을 설계합니다.
  4. 오프로드/고속 경로 확장
    XDP, page_pool, TC 오프로드를 순차 적용합니다.
대상 독자: 데이터 경로 성능 최적화와 오프로드 기능 구현이 필요한 드라이버 개발자를 대상으로 합니다. 기본 구조는 네트워크 디바이스 드라이버 (net_device)를 먼저 읽을 것을 권장합니다.

RX 경로: IRQ, NAPI, budget 처리

수신 경로는 인터럽트 기반 진입 후 NAPI poll로 전환하는 모델이 표준입니다. 드라이버는 budget를 존중하며 완료 시 napi_complete_done()를 호출해야 합니다.

/* 개념 예시: IRQ top-half와 NAPI poll 연계 */
static irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_priv *priv = data;

    my_mask_rx_irq(priv);
    napi_schedule_irqoff(&priv->napi);
    return IRQ_HANDLED;
}

static int my_napi_poll(struct napi_struct *napi, int budget)
{
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    int work_done = 0;

    while (work_done < budget) {
        struct sk_buff *skb = my_rx_one_skb(priv);

        if (!skb)
            break;

        skb->protocol = eth_type_trans(skb, priv->ndev);
        napi_gro_receive(napi, skb);
        work_done++;
    }

    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        my_unmask_rx_irq(priv);
    }

    return work_done;
}
성능 포인트: GRO를 쓰는 드라이버는 napi_gro_receive() 경로와 page recycling 전략을 함께 설계해야 합니다. 고속 NIC에서는 RX ring refill 정책이 drop/jitter를 크게 좌우합니다.

RX 전체 파이프라인(Pipeline)

아래 다이어그램은 물리 와이어 수신부터 애플리케이션 소켓(Socket) 버퍼 도달까지의 전체 수신 경로를 보여줍니다. 각 단계에서 일어나는 핵심 동작에 주의하세요.

Wire 물리 매체 NIC RX Ring DMA → 호스트 메모리 RX Completion desc 상태 확인 IRQ Top-Half mask + napi_schedule NAPI Poll Loop softirq 컨텍스트 실행 eth_type_trans 프로토콜 식별 + skb 설정 GRO 집계 napi_gro_receive netif_receive_skb 프로토콜 핸들러 전달 Protocol Stack IP → TCP/UDP Socket Buffer sk→sk_receive_queue Application recv() / read() RX 경로 핵심 포인트 ① IRQ top-half에서는 인터럽트 마스크 + napi_schedule만 수행 (최소 지연) ② NAPI poll에서 budget 미소진 시 napi_complete_done → IRQ 재활성화 ③ GRO는 동일 flow의 연속 패킷을 하나의 대형 skb로 집계하여 프로토콜 스택 부하 감소 ④ page pool / page recycling으로 RX ring refill 시 할당 오버헤드 최소화
RX 파이프라인 전체 흐름: 와이어 수신부터 애플리케이션 recv()까지의 전체 수신 경로입니다.

GRO (Generic Receive Offload) 동작 원리

GRO는 네트워크 스택(Network Stack) 진입 전에 동일 flow의 연속 패킷들을 하나의 대형 sk_buff로 집계(merge)하여 프로토콜 스택 처리 횟수를 줄이는 소프트웨어 오프로드 기술입니다. 하드웨어 LRO(Large Receive Offload)와 달리 GRO는 프로토콜 레이어에서 stateless하게 동작하여 포워딩 환경에서도 안전하게 사용할 수 있습니다.

GRO 집계 과정은 다음과 같습니다:

  1. NAPI poll에서 napi_gro_receive() 호출 시, GRO 엔진은 napi→gro_hash[] 테이블에서 동일 flow를 검색합니다.
  2. 매칭되는 flow가 있으면 현재 패킷의 payload를 기존 skb의 frag_list 또는 frags[]에 병합합니다.
  3. flow가 완료되거나(PSH, FIN 등) 타이머(Timer)/카운트 제한에 도달하면 집계된 대형 skb를 netif_receive_skb()로 전달합니다.
  4. 매칭되지 않으면 새 flow 엔트리를 생성하거나 즉시 전달합니다.
비교 항목LRO (Large Receive Offload)GRO (Generic Receive Offload)
구현 위치NIC 하드웨어 또는 드라이버커널 네트워크 스택 (dev_gro_receive)
상태 관리Stateful — TCP 헤더를 재작성Stateless — 원본 헤더 보존
포워딩 호환불가 — 재작성된 헤더로 인해 checksum/시퀀스 불일치가능 — GSO로 재분할 시 원본 복원
프로토콜 지원TCP만 (일반적)TCP, UDP, GRE, VXLAN 등 확장 가능
GSO 연계없음GRO → GSO 대칭 구조로 설계됨
커널 권장비권장 (ethtool -K eth0 lro off)기본 활성 (ethtool -K eth0 gro on)
/* GRO 수신 경로 핵심 흐름 (개념 예시) */
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
    gro_result_t ret;

    skb_gro_reset_offset(skb);

    ret = dev_gro_receive(napi, skb);    /* flow 매칭 + 병합 시도 */

    switch (ret) {
    case GRO_MERGED:
    case GRO_MERGED_FREE:
        break;                              /* 병합 성공, skb 보관 */
    case GRO_HELD:
        break;                              /* 새 flow로 보관 */
    case GRO_NORMAL:
        gro_normal_one(napi, skb, 1);    /* GRO 불가, 즉시 전달 */
        break;
    case GRO_CONSUMED:
        break;
    }
    return ret;
}

/* NAPI poll 종료 시 GRO flush — 보관 중인 flow를 모두 전달 */
void napi_complete_done(struct napi_struct *napi, int work_done)
{
    gro_normal_list(napi);           /* 버퍼링된 GRO 패킷 일괄 전달 */
    /* ... napi state 전환, IRQ 재활성화 ... */
}
GRO가 성능을 저하시키는 경우: 소형 패킷 워크로드(DNS, VoIP, 게임 서버 등)에서는 GRO 집계 대기 시간(Latency)이 오히려 latency를 증가시킬 수 있습니다. 또한 flow 수가 극도로 많아 GRO hash 충돌이 빈번한 환경에서는 CPU 오버헤드(Overhead)만 추가됩니다. 이런 경우 ethtool -K eth0 gro off를 고려하세요. 반대로, 벌크 TCP 전송(파일 서버, 스트리밍)에서는 GRO를 반드시 활성화해야 CPU 사용률이 크게 줄어듭니다.

NAPI 상태 머신

NAPI는 명확한 상태 전환 규약을 가진 상태 머신으로 동작합니다. 드라이버가 이 규약을 정확히 따르지 않으면 인터럽트 누수(IRQ 재활성화 누락)나 poll 미진입 같은 심각한 버그가 발생합니다.

IDLE SCHED softirq 대기 중 POLL softirq 컨텍스트 실행 DISABLED 드라이버 정지 napi_schedule() softirq 진입 napi_complete_done() budget 미소진 budget 소진 재스케줄 napi_disable() napi_disable() napi_enable()
NAPI 상태 머신: IDLE → SCHED → POLL → IDLE 순환과 DISABLED 전환 경로입니다.
상태 플래그비트의미
NAPI_STATE_SCHED0NAPI가 poll 목록에 스케줄됨. napi_schedule()가 설정, napi_complete_done()이 해제
NAPI_STATE_DISABLE1napi_disable() 진행 중. SCHED 비트 해제를 spin-wait
NAPI_STATE_NPSVC2busy polling 서비스 중 표시. poll이 non-softirq 컨텍스트에서 실행됨을 나타냄
NAPI_STATE_LISTED3NAPI가 디바이스의 napi_list에 등록됨 (netif_napi_add()가 설정)
NAPI_STATE_NO_BUSY_POLL4busy polling 비활성화 표시 (드라이버가 미지원 선언)
NAPI_STATE_IN_BUSY_POLL5현재 busy poll 실행 중 (re-entrant 방지)
NAPI_STATE_PREFER_BUSY_POLL6소켓이 busy poll 선호 표시 (SO_PREFER_BUSY_POLL)
NAPI_STATE_THREADED7threaded NAPI 모드 활성 — 전용 커널 스레드(Kernel Thread)에서 poll 실행
NAPI_STATE_SCHED_THREADED8threaded NAPI에서 스케줄됨 표시
/* NAPI enable/disable 올바른 순서 (개념 예시) */

/* === ndo_open: 활성화 순서 === */
static int my_open(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* 1. 하드웨어 초기화 (ring 할당, DMA 설정) */
    my_hw_init(priv);

    /* 2. NAPI 활성화 — 이 시점부터 napi_schedule 가능 */
    napi_enable(&priv->napi);

    /* 3. IRQ 등록 — NAPI enable 후에 해야 schedule이 동작 */
    request_irq(priv->irq, my_irq_handler, 0, "mynic", priv);

    /* 4. TX 큐 시작 */
    netif_tx_start_all_queues(ndev);

    return 0;
}

/* === ndo_stop: 비활성화 순서 === */
static int my_stop(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* 1. TX 큐 정지 — 새 xmit 진입 차단 */
    netif_tx_disable(ndev);

    /* 2. IRQ 비활성화 — 새 napi_schedule 차단 */
    disable_irq(priv->irq);

    /* 3. napi_disable — 진행 중인 poll 완료를 대기 (barrier 역할) */
    napi_disable(&priv->napi);
    /* 이 시점 이후 poll 콜백이 절대 실행되지 않음을 보장 */

    /* 4. IRQ 해제 */
    free_irq(priv->irq, priv);

    /* 5. 하드웨어 정리 (ring 해제, DMA 해제) */
    my_hw_cleanup(priv);

    return 0;
}
napi_disable()의 barrier 의미: napi_disable()은 내부적으로 NAPI_STATE_SCHED 비트를 spin-wait하며, 진행 중인 poll이 napi_complete_done()을 호출해 SCHED를 해제할 때까지 대기합니다. 따라서 napi_disable() 반환 후에는 poll 콜백이 절대 실행되지 않음이 보장되며, 이후 ring 메모리를 안전하게 해제할 수 있습니다. 반드시 IRQ 비활성화 후, ring 해제 전에 호출해야 합니다.
NAPI threaded 모드 (커널 5.12+): echo 1 > /sys/class/net/eth0/threaded 또는 드라이버에서 dev_set_threaded(ndev, true)를 호출하면 NAPI poll이 softirq 대신 전용 커널 스레드(napi/eth0-N)에서 실행됩니다. 이 모드의 주요 장점:
  • RT 커널 호환: softirq는 PREEMPT_RT에서 스레드화되지만 우선순위(Priority) 제어가 어렵습니다. threaded NAPI는 chrt로 직접 스케줄링 정책 설정 가능
  • CPU 격리(Isolation): taskset으로 NAPI 스레드(Thread)를 특정 코어에 바인딩하여 데이터플레인/컨트롤플레인 분리 가능
  • cgroup 통합: NAPI 스레드를 cgroup에 배치하여 CPU/메모리 자원 제한 가능
단, softirq 대비 컨텍스트 스위치 오버헤드가 추가되므로 초고속 NIC에서는 throughput이 약간 감소할 수 있습니다.

TX 경로: ndo_start_xmit, 큐 정지/재개, BQL

송신 경로의 핵심은 링 용량 관리입니다. TX ring이 포화됐을 때는 NETDEV_TX_BUSY를 남발하지 말고 queue stop/wake 모델을 일관되게 유지해야 합니다.

/* 개념 예시: 멀티큐 TX stop/wake + BQL 경로 */
static netdev_tx_t my_ndo_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    struct netdev_queue *txq = netdev_get_tx_queue(ndev, skb_get_queue_mapping(skb));
    unsigned long flags;

    spin_lock_irqsave(&priv->tx_lock, flags);

    if (my_tx_ring_avail(priv) < MAX_SKB_FRAGS + 2) {
        netif_tx_stop_queue(txq);
        spin_unlock_irqrestore(&priv->tx_lock, flags);
        return NETDEV_TX_BUSY;
    }

    my_map_skb_to_tx_desc(priv, skb);
    netdev_tx_sent_queue(txq, skb->len);
    my_ring_doorbell(priv);

    spin_unlock_irqrestore(&priv->tx_lock, flags);
    return NETDEV_TX_OK;
}

static void my_tx_complete(struct my_priv *priv, u16 qid)
{
    struct netdev_queue *txq = netdev_get_tx_queue(priv->ndev, qid);
    u32 bytes = 0, pkts = 0;

    my_reclaim_tx_desc(priv, &bytes, &pkts);
    netdev_tx_completed_queue(txq, pkts, bytes);

    if (netif_tx_queue_stopped(txq) && my_tx_ring_avail(priv) > 64)
        netif_tx_wake_queue(txq);
}

TX 전체 파이프라인

아래 다이어그램은 유저스페이스 send() 호출부터 물리 와이어 송출, TX 완료 인터럽트, 디스크립터 회수까지의 전체 송신 경로를 보여줍니다.

send() User Space sk_buff 생성 sock_alloc_send_skb Qdisc enqueue + dequeue dev_queue_xmit __dev_xmit_skb ndo_start_xmit 드라이버 진입점 TX Desc Ring desc 기록 + skb 연결 DMA Mapping dma_map_single/sg Doorbell Write MMIO → NIC에 알림 NIC Wire 송출 물리 매체 전송 TX Completion IRQ NIC → CPU 인터럽트 Desc Reclaim dma_unmap + kfree_skb BQL Feedback netdev_tx_completed_queue Queue Wake netif_tx_wake_queue 핵심 주의사항 ① DMA map 실패 시 skb를 free하고 ndev→stats.tx_dropped++ 처리 필수 ② queue stop과 doorbell 사이 race: stop → desc 확인 → 필요시 wake 패턴 권장 ③ BQL netdev_tx_sent_queue()는 doorbell 전에 호출해야 정확한 in-flight 바이트 추적 가능
TX 파이프라인 전체 흐름: 유저스페이스 send()부터 NIC 송출, 완료 인터럽트, 디스크립터 회수까지의 12단계 경로입니다.

BQL (Byte Queue Limits): 버퍼블로트 방지와 동적 큐 제한

BQL(Byte Queue Limits)은 커널 lib/dynamic_queue_limits.c에 구현된 동적 큐 깊이 제어 알고리즘입니다. NIC TX 큐에 쌓을 수 있는 바이트 수를 실시간(Real-time)으로 조정하여, 큐가 과도하게 깊어지는 버퍼블로트(bufferbloat)를 방지하면서도 충분한 처리량(Throughput)을 유지합니다. TX 경로 앞 절에서 netdev_tx_sent_queue/netdev_tx_completed_queue를 호출한 것이 바로 BQL API입니다. 이 절에서는 그 내부 알고리즘, 자료구조, sysfs 튜닝, 그리고 qdisc와의 상호작용을 깊이 있게 살펴봅니다.

버퍼블로트 문제와 BQL의 필요성

NIC의 TX ring이 크거나 드라이버가 큐 깊이를 제한하지 않으면, 상위 계층(qdisc, TCP 혼잡 제어(Congestion Control))이 내린 결정과 무관하게 수백 ms에서 수 초 분량의 패킷이 하드웨어 큐에 쌓일 수 있습니다. 이 "버퍼블로트"는 지연을 극적으로 증가시키면서 처리량은 거의 높이지 않습니다. BQL은 "지금 하드웨어에 내려보낸 바이트"와 "아직 완료되지 않은 바이트"를 추적하여 큐 깊이를 필요 최소한으로 동적 조절합니다.

큐 깊이 / 지연 시간 → 큐 깊이 (BQL 없음) 지연 (BQL 없음) 큐 깊이 (BQL 적용) 지연 (BQL 적용) BQL LIMIT (동적) 버퍼블로트 영역 — 실선: BQL 적용 --- 점선: BQL 미적용 BQL은 처리량 유지 + 지연 억제
BQL이 없으면 TX 큐 깊이가 무한히 증가하여 지연이 급등합니다. BQL은 동적 LIMIT으로 큐 깊이를 최소한으로 유지합니다.

BQL 동작 원리: 동적 한계 조정 알고리즘

BQL의 핵심은 lib/dynamic_queue_limits.c에 구현된 DQL(Dynamic Queue Limits) 알고리즘입니다. 드라이버가 패킷을 큐에 넣을 때(dql_queued)와 완료될 때(dql_completed)를 추적하여, 현재 inflight(미완료) 바이트LIMIT을 초과하면 큐를 멈추고, 완료 시 실제 사용 패턴에 따라 LIMIT을 올리거나 내립니다.

DQL 핵심 규칙:
  • LIMIT 감소 (오버슈트 교정): 완료 시점에 inflight가 LIMIT보다 큰 적이 없었으면(BELOW), LIMIT = inflight × (LIMIT / (LIMIT − ovlimit + slack)). 즉 과잉 분을 잘라냅니다.
  • LIMIT 증가 (여유 확보): 완료 시점에 inflight가 LIMIT 이상이었으면(ABOVE), 다음 주기에서 LIMIT이 부족한 것으로 보고 LIMIT += (completed − LIMIT) / 16 형태로 서서히 올립니다.
  • Slack: slack_hold_time(기본 HZ) 동안 관찰된 최소 여유분(slack)을 반영하여 불필요한 여유를 제거합니다.
BELOW inflight < LIMIT 유지 큐에 여유 있음 completed 시: LIMIT ↓ (과잉 제거) ABOVE inflight ≥ LIMIT 도달 큐 정지됨 completed 시: LIMIT ↑ (여유 확보) queued → inflight ≥ LIMIT completed → inflight < LIMIT LIMIT 조정 로직 (dql_completed 시) BELOW 경로: new_limit = LIMIT − (ovlimit − slack) → 축소 ABOVE 경로: new_limit = LIMIT + (completed − LIMIT)/16 → 확장 min_limit ≤ new_limit ≤ max_limit 범위 강제 (sysfs로 조정 가능)
DQL 알고리즘은 BELOW/ABOVE 두 상태 사이를 전이하며 LIMIT을 동적 조정합니다. 수렴 후에는 최소한의 큐 깊이만 유지합니다.
/* DQL 알고리즘 의사코드 (lib/dynamic_queue_limits.c 기반) */

/* ① 드라이버가 패킷을 큐에 넣을 때 */
void dql_queued(struct dql *dql, u32 count)
{
    dql->last_obj_cnt  = count;
    dql->num_queued   += count;

    /* inflight = num_queued - num_completed */
    if (inflight >= dql->adj_limit)
        netif_tx_stop_queue();   /* 큐 정지 — LIMIT 도달 */
}

/* ② TX 완료 인터럽트에서 */
void dql_completed(struct dql *dql, u32 count)
{
    dql->num_completed += count;
    ovlimit = dql->num_queued - dql->num_completed - dql->limit;

    if (ovlimit <= 0) {
        /* BELOW: inflight가 LIMIT 아래 — 과잉 제거 */
        dql->slack = min(dql->slack, ovlimit + dql->slack_start);
        if (slack_expired)
            new_limit = dql->limit - (ovlimit + dql->slack);
    } else {
        /* ABOVE: inflight가 LIMIT 이상 — LIMIT 확장 */
        new_limit = dql->limit + count / 16;  /* 완료분의 1/16 증가 */
    }
    dql->limit = clamp(new_limit, dql->min_limit, dql->max_limit);

    if (inflight < dql->adj_limit)
        netif_tx_wake_queue();   /* 큐 재개 */
}

핵심 자료구조: struct dql

BQL의 상태는 struct dql(include/linux/dynamic_queue_limits.h)에 저장됩니다. 각 TX 큐(struct netdev_queue)마다 하나의 dql 인스턴스가 내장되어 있습니다.

/* include/linux/dynamic_queue_limits.h */
struct dql {
    unsigned int  num_queued;       /* 큐에 넣은 누적 바이트 (단조 증가) */
    unsigned int  adj_limit;        /* 현재 유효 한계 (limit - num_completed) */
    unsigned int  last_obj_cnt;     /* 마지막 dql_queued 호출의 count */

    unsigned int  limit       ____cacheline_aligned_in_smp;
                                     /* 동적 LIMIT (바이트 단위) */
    unsigned int  num_completed;    /* 완료된 누적 바이트 (단조 증가) */

    unsigned int  prev_ovlimit;     /* 이전 주기의 오버리밋 값 */
    unsigned int  prev_num_queued;  /* 이전 주기의 num_queued */
    unsigned int  prev_last_obj_cnt;/* 이전 주기의 last_obj_cnt */

    unsigned int  lowest_slack;     /* 관찰된 최소 여유분 */
    unsigned long slack_start_time; /* slack 관찰 시작 시각 */

    unsigned int  max_limit;        /* sysfs 설정: LIMIT 상한 (기본 DQL_MAX_LIMIT) */
    unsigned int  min_limit;        /* sysfs 설정: LIMIT 하한 (기본 0) */
    unsigned int  slack_hold_time;  /* slack 관찰 윈도우 (기본 HZ=1초) */
};
캐시라인 분리: limitnum_completed는 TX 완료 경로(보통 softirq)에서 빈번히 갱신되므로 ____cacheline_aligned_in_smp로 분리하여 num_queued/adj_limit(송신 경로)와의 false sharing을 방지합니다.

드라이버 API 통합

드라이버가 BQL을 사용하려면 TX 경로의 세 지점에서 API를 호출합니다. 모든 API는 바이트 단위로 동작하며, 패킷 수가 아닌 누적 바이트를 전달해야 합니다.

① ndo_start_xmit skb를 TX ring에 매핑 DMA 디스크립터 작성 ② netdev_tx_sent_queue BQL에 전송 바이트 기록 → dql_queued(txq→dql, bytes) ③ doorbell kick NIC에 새 디스크립터 알림 wmb() + MMIO write ④ TX 완료 IRQ/NAPI 디스크립터 회수, bytes/pkts 집계 ⑤ netdev_tx_completed_queue BQL에 완료 바이트 기록 → dql_completed(txq→dql, bytes) → LIMIT 조정 netdev_tx_reset_queue 링크 다운/리셋 시 호출 BQL 카운터 초기화 NIC 처리 LIMIT 피드백 → 큐 wake/stop sent_queue()가 LIMIT 초과하면 큐 정지 → completed_queue()가 LIMIT 갱신 후 큐 재개
BQL API는 TX 송신(②)과 완료(⑤) 두 지점에서 호출되며, LIMIT 피드백으로 큐 stop/wake를 자동 제어합니다.
/* BQL 드라이버 API 3종 — 호출 시점과 인자 */

/* ① ndo_start_xmit() 내부, skb를 ring에 넣은 직후 */
netdev_tx_sent_queue(txq, skb->len);
/*  txq  : netdev_get_tx_queue(ndev, queue_index)
 *  bytes: 전송한 바이트 수 (skb->len)
 *  내부: dql_queued(&txq->dql, bytes)
 *  inflight ≥ limit이면 __netif_tx_stop_queue() 호출 */

/* ② TX 완료 인터럽트/NAPI에서, 디스크립터 회수 후 */
netdev_tx_completed_queue(txq, pkts, bytes);
/*  pkts : 완료된 패킷 수 (BQL 자체는 bytes만 사용)
 *  bytes: 완료된 바이트 수
 *  내부: dql_completed(&txq->dql, bytes)
 *  LIMIT 재조정 + 큐 wake 판단 */

/* ③ ndo_stop() 또는 링크 다운/리셋 시 */
netdev_tx_reset_queue(txq);
/*  모든 BQL 카운터 초기화 (num_queued, num_completed 등)
 *  인터페이스 down → up 사이클에서 반드시 호출
 *  빠뜨리면 stale 카운터로 큐가 영구 정지될 수 있음 */
흔한 실수: ndo_stop()에서 netdev_tx_reset_queue()를 빠뜨리면, 다음 ndo_open() 후 stale 카운터 때문에 BQL이 즉시 큐를 멈추고 트래픽이 흐르지 않습니다. 멀티큐 드라이버는 모든 TX 큐에 대해 개별 reset을 호출해야 합니다.

sysfs 인터페이스와 튜닝

각 TX 큐의 BQL 파라미터는 /sys/class/net/<dev>/queues/tx-<N>/byte_queue_limits/ 경로에 노출됩니다. 운영 환경에서 BQL 동작을 관찰하고 미세 조정할 수 있는 핵심 인터페이스입니다.

# BQL sysfs 파일 확인 (예: eth0의 tx-0 큐)
ls /sys/class/net/eth0/queues/tx-0/byte_queue_limits/
# 출력: hold_time  inflight  limit  limit_max  limit_min

# 현재 동적 LIMIT 확인 (알고리즘이 결정한 값)
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit

# inflight 바이트 확인 (현재 NIC에서 처리 중인 양)
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/inflight

# LIMIT 상한 조정 (기본값: DQL_MAX_LIMIT = 매우 큰 값)
# 지연에 민감한 워크로드에서 상한을 낮추면 지연이 더 줄어들 수 있음
echo 30000 > /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_max

# LIMIT 하한 조정 (기본값: 0)
# 너무 낮은 LIMIT으로 인한 성능 저하 방지
echo 1500 > /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_min

# slack 관찰 윈도우 조정 (기본값: 1000 = HZ, 즉 1초)
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/hold_time

# 모든 큐의 BQL limit 한 번에 확인
for q in /sys/class/net/eth0/queues/tx-*/byte_queue_limits/limit; do
    echo "$(dirname $(dirname $q)): $(cat $q)"
done
sysfs 파일읽기/쓰기설명
limitR현재 동적 LIMIT (바이트). 알고리즘이 자동 조정
limit_maxR/WLIMIT 상한. 낮추면 최대 큐 깊이를 제한
limit_minR/WLIMIT 하한. 높이면 최소 처리량 보장
hold_timeR/Wslack 관찰 윈도우 (ms). 기본 1000
inflightR현재 미완료 바이트 (num_queued − num_completed)

BQL과 qdisc/TC의 상호작용

BQL은 qdisc 아래, NIC ring 위에 위치합니다. 패킷 흐름에서 BQL의 정확한 위치를 이해하면 fq_codel 같은 AQM(Active Queue Management)과의 시너지를 극대화할 수 있습니다.

Application (send/sendmsg) TCP/UDP → sk_buff 생성 qdisc (fq_codel, htb, pfifo_fast ...) 스케줄링 + AQM (ECN marking, drop) BQL (Byte Queue Limits) 동적 LIMIT으로 큐 stop/wake 제어 NIC TX Ring (DMA descriptors) Wire (물리 매체) 정책 결정 깊이 제어 하드웨어 fq_codel + BQL 시너지 BQL: 하드웨어 큐 제한 fq_codel: 소프트웨어 AQM → 전 구간 지연 최소화 → 공정한 대역폭 분배
BQL은 qdisc(소프트웨어 스케줄링)와 NIC ring(하드웨어 큐) 사이에서 동작합니다. fq_codel과 결합하면 소프트웨어+하드웨어 전 구간의 지연을 제어할 수 있습니다.
fq_codel + BQL 조합이 강력한 이유:
  • fq_codel은 qdisc 레벨에서 소프트웨어 큐의 지연을 제어합니다 (sojourn time 기반 drop/ECN).
  • BQL은 드라이버 레벨에서 하드웨어 큐에 과도한 바이트가 쌓이는 것을 방지합니다.
  • BQL 없이 fq_codel만 사용하면 NIC ring에 수백 패킷이 쌓여 fq_codel의 AQM 효과가 무력화됩니다.
  • BQL이 하드웨어 큐 깊이를 최소화하면, fq_codel이 더 정확한 sojourn time을 측정하여 공정한 스케줄링이 가능합니다.

실전 디버깅(Debugging)과 모니터링

BQL이 올바르게 동작하는지 확인하고, 문제 발생 시 원인을 추적하는 방법입니다.

# ── BQL 상태 종합 확인 ──
# 모든 TX 큐의 limit과 inflight를 한 번에 출력
for q in /sys/class/net/eth0/queues/tx-*/byte_queue_limits; do
    echo "=== $(basename $(dirname $q)) ==="
    echo "  limit:    $(cat $q/limit)"
    echo "  inflight: $(cat $q/inflight)"
    echo "  max:      $(cat $q/limit_max)"
    echo "  min:      $(cat $q/limit_min)"
done

# ── tc 통계와 BQL 연계 확인 ──
# qdisc의 backlog과 BQL의 inflight를 비교하여 병목 위치 판단
tc -s qdisc show dev eth0

# ── bpftrace로 BQL limit 변화 실시간 추적 ──
# dql_completed 호출 시 limit 값 변화를 추적
bpftrace -e 'kprobe:dql_completed {
    $dql = (struct dql *)arg0;
    printf("cpu=%d limit=%u completed=%u\n",
           cpu, $dql->limit, arg1);
}'

# ── perf로 BQL 관련 함수 호출 빈도 확인 ──
perf stat -e 'probe:dql_queued,probe:dql_completed' -a sleep 5

# ── 문제 진단 체크리스트 ──
# 1. limit이 0이면? → netdev_tx_reset_queue() 누락 가능
# 2. inflight가 limit과 같고 큐 정지? → 정상 (완료 대기 중)
# 3. limit이 limit_max에 고정? → 트래픽이 항상 LIMIT 소진 → limit_max 낮출 것
# 4. limit이 매우 작고 throughput 저하? → limit_min을 MTU 이상으로 설정
빠른 진단 공식: inflight ≈ limit이 지속되면 BQL이 적극적으로 큐를 제한하고 있는 뜻입니다. 이때 throughput이 충분하면 정상이고, 부족하면 limit_min을 높이거나 NIC의 TX 완료 인터럽트 코얼레싱을 줄여 완료 통지를 빠르게 받아 LIMIT을 더 빨리 해제하세요.

LLTX (NETIF_F_LLTX): lockless TX 계약과 실무 주의점

NETIF_F_LLTX는 TX 잠금을 네트워크 코어가 아닌 드라이버가 직접 책임지는 오래된 모델입니다. 즉, ndo_start_xmit() 동시 호출에 대한 직렬화(Serialization)/경합(Contention) 제어를 드라이버가 스스로 보장해야 하며, 큐 stop/wake, timeout 복구, completion 경로까지 하나의 동시성 계약으로 맞춰야 합니다.

핵심 경고: LLTX는 신규 드라이버의 기본 선택지가 아닙니다. 멀티큐 + queue별 잠금/BQL 모델이 이미 충분히 고성능이며, LLTX는 lock inversion, queue state 경합, watchdog 오탐(false timeout) 같은 장애 확률을 높입니다.
항목일반 TX 경로LLTX 경로
직렬화 주체코어/큐 잠금 + 드라이버 보조 잠금드라이버가 전적으로 책임
병목(Bottleneck) 위치잠금 경합(Lock Contention)은 비교적 예측 가능드라이버 구현 품질에 따라 편차 큼
디버깅 난이도표준 패턴과 도구가 많음race 재현/분석 난이도 높음
권장도신규 구현 권장기존 드라이버 유지보수 목적 외 비권장

LLTX를 유지해야 하는 코드베이스라면 아래 4가지를 반드시 고정 규칙으로 문서화해야 합니다.

  1. xmit 직렬화 규칙
    ndo_start_xmit()의 re-entry 허용 범위(전역/큐별)를 명시하고 잠금 순서를 고정
  2. queue 상태 전이 규칙
    netif_tx_stop_queue()/netif_tx_wake_queue() 호출 조건을 단일 함수로 중앙화
  3. completion 메모리 순서
    descriptor reclaim 이후 wake 판단 전까지의 barrier 규칙을 아키텍처별로 검증
  4. timeout 복구 규칙
    ndo_tx_timeout()에서 즉시 리셋하지 말고 workqueue로 이관해 중복 reset 방지
/* LLTX 유지보수 시 권장되는 최소 패턴 (개념 예시) */
static netdev_tx_t my_lltx_xmit(struct sk_buff *skb, struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    unsigned long flags;

    /* LLTX에서는 드라이버가 자체 직렬화를 반드시 보장 */
    spin_lock_irqsave(&priv->tx_lock, flags);

    if (!my_has_room(priv)) {
        my_stop_txq_if_needed(priv);
        spin_unlock_irqrestore(&priv->tx_lock, flags);
        return NETDEV_TX_BUSY;
    }

    my_post_desc(priv, skb);
    my_kick_doorbell(priv);
    spin_unlock_irqrestore(&priv->tx_lock, flags);
    return NETDEV_TX_OK;
}
마이그레이션 전략: LLTX 기반 구형 드라이버를 개선할 때는 한 번에 lockless를 유지하려 하지 말고, 먼저 queue별 잠금 + 명확한 stop/wake + BQL 계측을 적용한 뒤 성능/지연을 재측정하는 접근이 안전합니다.

XDP, AF_XDP, 드라이버 오프로드

XDP 지원 드라이버는 RX hot path 초기에 프로그램을 실행해 drop/redirect를 빠르게 처리합니다. ndo_bpf, zero-copy AF_XDP, page_pool의 조합이 고성능 경로의 핵심입니다.

/* 개념 예시: XDP action 분기와 프레임 반환 계약 */
static int my_xdp_run(struct my_priv *priv, struct xdp_buff *xdp)
{
    u32 act;

    act = bpf_prog_run_xdp(rcu_dereference(priv->xdp_prog), xdp);

    switch (act) {
    case XDP_PASS:
        return XDP_PASS;
    case XDP_DROP:
        xdp_return_frame_rx_napi(xdp);
        return XDP_DROP;
    case XDP_TX:
        my_xdp_xmit(priv, xdp);
        return XDP_TX;
    default:
        xdp_return_frame_rx_napi(xdp);
        return XDP_ABORTED;
    }
}
연계 문서: XDP 프로그램 작성과 AF_XDP 유저스페이스 큐 모델은 BPF/XDP, AF_XDP 문서에서 이어서 확인하세요.

ndo_bpf 콜백 구현 상세

XDP 프로그램을 드라이버에 부착(Attach)하려면 ndo_bpf 콜백을 구현해야 합니다. 커널의 dev_xdp_install() 함수가 이 콜백을 호출하여 XDP 프로그램의 설치/제거를 드라이버에 통지합니다.

ndo_bpfXDP_SETUP_PROG 명령을 통해 프로그램을 설치합니다. 드라이버는 이 시점에 RX 링 크기 재설정, 헤더룸(Headroom) 확보 등을 수행해야 합니다.

/* net/core/dev.c - dev_xdp_install() 핵심 분석 */
static int dev_xdp_install(struct net_device *dev,
                           bpf_op_t bpf_op,
                           struct netlink_ext_ack *extack,
                           u32 flags,
                           struct bpf_prog *prog)
{
    struct netdev_bpf xdp;

    memset(&xdp, 0, sizeof(xdp));
    xdp.command = XDP_SETUP_PROG;
    xdp.extack = extack;
    xdp.flags = flags;
    xdp.prog = prog;

    /* 드라이버의 ndo_bpf 콜백 호출 */
    return bpf_op(dev, &xdp);
}

/* 드라이버 측 ndo_bpf 구현 예시 */
static int my_ndo_bpf(struct net_device *dev,
                      struct netdev_bpf *bpf)
{
    struct my_priv *priv = netdev_priv(dev);

    switch (bpf->command) {
    case XDP_SETUP_PROG:
        return my_xdp_setup_prog(priv, bpf->prog, bpf->extack);
    case XDP_SETUP_XSK_POOL:
        return my_xsk_pool_setup(priv, bpf->xsk.pool,
                                 bpf->xsk.queue_id);
    default:
        return -EINVAL;
    }
}

static int my_xdp_setup_prog(struct my_priv *priv,
                              struct bpf_prog *prog,
                              struct netlink_ext_ack *extack)
{
    struct bpf_prog *old_prog;
    bool need_reconfig;

    /* MTU 제한 검증: XDP는 단일 페이지에 맞아야 함 */
    if (prog && priv->ndev->mtu > MY_XDP_MAX_MTU) {
        NL_SET_ERR_MSG_MOD(extack,
            "MTU too large for XDP");
        return -EINVAL;
    }

    /* XDP 프로그램 유무에 따라 링 재설정 필요 여부 결정 */
    old_prog = rcu_dereference_protected(priv->xdp_prog,
        lockdep_is_held(&priv->lock));
    need_reconfig = (!!prog != !!old_prog);

    if (need_reconfig && netif_running(priv->ndev)) {
        /* RX 링 재설정: XDP headroom 확보 */
        my_stop_rx_rings(priv);
        my_reconfigure_rx_headroom(priv,
            prog ? XDP_PACKET_HEADROOM : 0);
        my_start_rx_rings(priv);
    }

    /* RCU로 프로그램 교체 */
    rcu_assign_pointer(priv->xdp_prog, prog);
    if (old_prog)
        bpf_prog_put(old_prog);

    return 0;
}

XDP 메타데이터와 힌트(Hints) 시스템

XDP 프로그램은 xdp_buff의 메타데이터(Metadata) 영역을 통해 드라이버로부터 하드웨어 힌트(Hints)를 수신할 수 있습니다. 메타데이터 영역은 data_meta에서 data 사이에 위치하며, 드라이버가 RX 타임스탬프(Timestamp), RSS 해시(Hash) 등의 정보를 기록합니다.

커널 6.3 이후 도입된 kfunc 기반 메타데이터 접근 방식은 XDP 힌트(XDP Hints)라고 불리며, 구조화된 인터페이스를 제공합니다.

/* XDP 메타데이터 영역 구조:
 *
 * +------------------+------------------+------------------+
 * |   data_hard_start|     data_meta    |      data        |
 * +------------------+------------------+------------------+
 *                    |<-- metadata -->|<-- packet ------>|
 *                    |  (rx_timestamp)  |  (Ethernet frame) |
 *                    |  (rx_hash)       |                    |
 */

/* 드라이버 RX 경로에서 메타데이터 기록 */
static void my_rx_populate_metadata(struct xdp_buff *xdp,
                                    struct my_rx_desc *desc)
{
    struct xdp_hints_common *hints;

    /* 메타데이터 영역 확보 */
    hints = xdp->data - sizeof(*hints);
    if ((void *)hints < xdp->data_hard_start)
        return; /* 공간 부족 */

    /* RX 타임스탬프 기록 */
    if (desc->flags & RX_DESC_TS_VALID) {
        hints->rx_timestamp = my_hw_ts_to_ns(desc->timestamp);
        hints->flags |= XDP_HINTS_RX_TIMESTAMP;
    }

    /* RSS 해시 기록 */
    if (desc->flags & RX_DESC_HASH_VALID) {
        hints->rx_hash = desc->rss_hash;
        hints->rx_hash_type = my_hash_type_to_xdp(desc->hash_type);
        hints->flags |= XDP_HINTS_RX_HASH;
    }

    xdp->data_meta = (void *)hints;
}

/* XDP kfunc 기반 힌트 접근 (커널 6.3+) */
/* XDP 프로그램에서 bpf_xdp_metadata_rx_timestamp()으로 접근 가능 */
static int my_xdp_rx_timestamp(const struct xdp_md *ctx,
                               u64 *timestamp)
{
    struct xdp_buff *xdp = (struct xdp_buff *)ctx;
    struct my_rx_ring *ring = my_get_rx_ring(xdp);
    struct my_rx_desc *desc = my_get_current_desc(ring);

    if (!(desc->flags & RX_DESC_TS_VALID))
        return -ENODATA;

    *timestamp = my_hw_ts_to_ns(desc->timestamp);
    return 0;
}

static const struct xdp_metadata_ops my_xdp_metadata_ops = {
    .xmo_rx_timestamp = my_xdp_rx_timestamp,
    .xmo_rx_hash      = my_xdp_rx_hash,
};

AF_XDP Zero-Copy 드라이버 통합

AF_XDP(Address Family XDP)의 제로 카피(Zero-Copy) 모드는 커널 메모리 복사 없이 유저스페이스와 NIC 간 직접 DMA 전송을 수행합니다. 드라이버는 xsk_pool을 등록하고, UMEM(User Memory) 영역의 버퍼를 직접 DMA 맵핑하여 사용합니다.

유저스페이스 (AF_XDP 소켓) FILL Ring COMPLETION Ring RX Ring TX Ring UMEM (공유 메모리 영역) Frame 0 Frame 1 Frame 2 Frame 3 ... Frame N 커널 (xsk_pool + NAPI) xsk_buff_alloc() xsk_buff_free() xsk_tx_peek_desc() wakeup NIC 하드웨어 (DMA) RX DMA → UMEM Frame TX DMA ← UMEM Frame FILL zero-copy RX zero-copy TX 완료
/* AF_XDP zero-copy를 지원하는 NAPI poll 구현 */
static int my_xsk_napi_poll(struct napi_struct *napi, int budget)
{
    struct my_qvec *qv = container_of(napi, struct my_qvec, napi);
    struct my_priv *priv = qv->priv;
    struct xsk_buff_pool *pool = qv->xsk_pool;
    int rx_done = 0, tx_done = 0;

    if (pool) {
        /* Zero-copy RX 처리 */
        rx_done = my_xsk_rx_clean(qv, budget);

        /* Zero-copy TX 처리 */
        tx_done = my_xsk_tx_clean(qv);

        /* XSK wakeup 처리 */
        if (xsk_uses_need_wakeup(pool))
            xsk_set_tx_need_wakeup(pool);
    } else {
        /* 일반 경로 */
        rx_done = my_rx_clean(qv, budget);
        tx_done = my_tx_clean(qv);
    }

    if (rx_done < budget) {
        napi_complete_done(napi, rx_done);
        my_enable_irq(qv);
    }

    return rx_done;
}

/* XSK RX 경로: UMEM 버퍼를 직접 DMA로 수신 */
static int my_xsk_rx_clean(struct my_qvec *qv, int budget)
{
    struct xsk_buff_pool *pool = qv->xsk_pool;
    int cleaned = 0;

    while (cleaned < budget) {
        struct xdp_buff *xdp;
        struct my_rx_desc *desc = my_get_next_rx_desc(qv);

        if (!desc)
            break;

        /* FILL ring에서 UMEM 프레임 가져오기 */
        xdp = xsk_buff_alloc(pool);
        if (!xdp)
            break;

        /* 디스크립터에서 길이 가져와서 xdp_buff에 설정 */
        xsk_buff_set_size(xdp, desc->length);
        xsk_buff_dma_sync_for_cpu(xdp, pool);

        /* XDP 프로그램 실행 */
        u32 act = bpf_prog_run_xdp(
            rcu_dereference(qv->priv->xdp_prog), xdp);

        switch (act) {
        case XDP_PASS:
            /* SKB로 변환하여 스택으로 전달 */
            my_xsk_to_skb(qv, xdp);
            break;
        case XDP_REDIRECT:
            xdp_do_redirect(qv->priv->ndev, xdp,
                rcu_dereference(qv->priv->xdp_prog));
            break;
        default:
            xsk_buff_free(xdp);
            break;
        }
        cleaned++;
    }

    /* FILL ring 리필 */
    my_xsk_refill_rx(qv, pool);

    return cleaned;
}

XDP 멀티버퍼(Multi-Buffer) 지원

XDP 멀티버퍼(Multi-Buffer)는 커널 6.0에서 도입된 기능으로, 단일 페이지(Page)를 초과하는 프레임(예: 점보 프레임(Jumbo Frame))을 XDP에서 처리할 수 있게 합니다. xdp_buffmb(multi-buffer) 플래그가 설정되면, 추가 프래그먼트(Fragment)가 skb_shared_info 구조체(Struct)를 통해 연결됩니다.

항목단일 버퍼 XDP멀티버퍼 XDP
최대 프레임 크기PAGE_SIZE - headroom - tailroom (~3500B)제한 없음 (드라이버 구현에 따라)
xdp_buff.flags0XDP_FLAGS_HAS_FRAGS
프래그먼트 접근N/Askb_shared_infofrags[] 배열
XDP 프로그램 요구사항기본BPF_F_XDP_HAS_FRAGS 플래그로 로드
성능 영향최소 오버헤드프래그먼트 순회 비용 추가
드라이버 지원 (주요)거의 모든 XDP 드라이버mlx5, i40e, ice, veth, virtio_net
/* 드라이버에서 XDP 멀티버퍼 프레임 구성 */
static void my_rx_build_xdp_mb(struct my_qvec *qv,
                               struct xdp_buff *xdp,
                               struct my_rx_desc *first_desc)
{
    struct skb_shared_info *sinfo;
    struct my_rx_desc *desc = first_desc;
    int nr_frags = 0;

    /* 첫 번째 디스크립터: 선형 데이터 */
    xdp_init_buff(xdp, MY_RX_BUF_SIZE, &qv->xdp_rxq);
    xdp_prepare_buff(xdp, page_address(desc->page),
                     XDP_PACKET_HEADROOM, desc->length, true);

    /* multi-buffer 플래그 설정 */
    xdp->flags |= XDP_FLAGS_HAS_FRAGS;

    /* tailroom에 skb_shared_info 배치 */
    sinfo = xdp_get_shared_info_from_buff(xdp);
    sinfo->nr_frags = 0;

    /* 후속 디스크립터들을 프래그먼트로 추가 */
    while (!(desc->flags & RX_DESC_EOP)) {
        desc = my_get_next_rx_desc(qv);
        if (!desc || nr_frags >= MAX_SKB_FRAGS)
            break;

        skb_frag_fill_page_desc(&sinfo->frags[nr_frags],
                                desc->page, 0, desc->length);
        sinfo->nr_frags = ++nr_frags;
        sinfo->xdp_frags_size += desc->length;
    }
}

/* ndo_bpf에서 멀티버퍼 능력 광고 */
static int my_xdp_setup_prog(struct my_priv *priv,
                              struct bpf_prog *prog,
                              struct netlink_ext_ack *extack)
{
    /* 멀티버퍼 미지원 프로그램이 점보 프레임 환경에서 동작하면 거부 */
    if (prog && !prog->aux->xdp_has_frags &&
        priv->ndev->mtu > MY_XDP_MAX_MTU) {
        NL_SET_ERR_MSG_MOD(extack,
            "Non-multi-buffer XDP prog with jumbo MTU");
        return -EINVAL;
    }

    /* ... 이하 프로그램 설치 로직 ... */
    return 0;
}

멀티큐, RSS, IRQ affinity 설계

10/25/100GbE 구간에서는 단일 큐 모델이 거의 항상 병목입니다. 드라이버는 RX/TX 큐, MSI-X vector, NAPI 인스턴스를 1:1 또는 N:1로 설계하고, NUMA/CPU 토폴로지(Topology)에 맞춰 IRQ affinity를 배치해야 합니다.

/* 개념 예시: 멀티큐 qvec와 MSI-X 벡터 매핑 */
struct my_qvec {
    struct napi_struct napi;
    int qid;
    int irq;
};

static int my_alloc_qvecs(struct my_priv *priv, int num_q)
{
    int i;

    for (i = 0; i < num_q; i++) {
        struct my_qvec *qv = &priv->qvec[i];

        qv->qid = i;
        netif_napi_add(priv->ndev, &qv->napi, my_qvec_poll);
        my_request_msix_vector(priv, i, &qv->irq, my_msix_irq_handler);
    }

    return 0;
}
설정 항목실무 기준
큐 개수활성 CPU 수와 동일 또는 NUMA 노드 단위
RSS indirection핫플로우가 특정 큐에 치우치지 않게 분산
IRQ affinity해당 큐를 소비하는 CPU에 고정
RPS/RFSHW RSS 부족 시 보조적으로 사용

RSS 해시 함수와 Indirection Table 내부

RSS(Receive Side Scaling)는 수신 패킷을 여러 RX 큐에 분산하는 하드웨어 메커니즘입니다. 핵심은 토플리츠 해시 알고리즘으로, 패킷의 소스/목적지 IP와 포트를 입력으로 받아 해시 값을 계산하고, 간접 테이블(Indirection Table)을 통해 최종 큐 번호를 결정합니다.

토플리츠 해시는 비밀 키(Secret Key)와 입력 데이터의 비트별 XOR 연산으로 구성됩니다. 커널은 netdev_rss_key_fill()로 부팅 시 랜덤 키를 생성하여 모든 디바이스에 공유합니다.

/* net/core/ethtool.c - RSS 키와 indirection table 관리 */

/* netdev_rss_key_fill(): 전역 RSS 키를 랜덤 생성 */
void netdev_rss_key_fill(void *buffer, size_t len)
{
    static u8 netdev_rss_key[NETDEV_RSS_KEY_LEN] __read_mostly;
    static bool rss_key_initialized = false;

    if (!rss_key_initialized) {
        get_random_bytes(netdev_rss_key, sizeof(netdev_rss_key));
        rss_key_initialized = true;
    }

    memcpy(buffer, netdev_rss_key, len);
}

/* 드라이버에서 RSS indirection table 설정 */
static int my_set_rxfh(struct net_device *dev,
                       struct ethtool_rxfh_param *rxfh,
                       struct netlink_ext_ack *extack)
{
    struct my_priv *priv = netdev_priv(dev);
    int i;

    /* 해시 함수 변경 */
    if (rxfh->hfunc != ETH_RSS_HASH_NO_CHANGE) {
        if (rxfh->hfunc != ETH_RSS_HASH_TOP)
            return -EOPNOTSUPP; /* Toeplitz만 지원 */
        priv->rss_hfunc = rxfh->hfunc;
    }

    /* RSS 키 갱신 */
    if (rxfh->key)
        memcpy(priv->rss_key, rxfh->key, MY_RSS_KEY_SIZE);

    /* Indirection table 갱신 */
    if (rxfh->indir) {
        for (i = 0; i < MY_RSS_INDIR_SIZE; i++) {
            if (rxfh->indir[i] >= priv->num_rx_queues)
                return -EINVAL;
            priv->rss_indir[i] = rxfh->indir[i];
        }
    }

    /* 하드웨어에 새 RSS 설정 적용 */
    return my_hw_write_rss_config(priv);
}

/* 대칭 RSS (Symmetric RSS): 양방향 플로우를 같은 큐로 */
/* 대칭 Toeplitz는 src/dst를 XOR하여 순서 무관하게 동일 해시 생성 */
static u32 my_symmetric_toeplitz_hash(const u8 *key,
                                       __be32 saddr, __be32 daddr,
                                       __be16 sport, __be16 dport)
{
    /* src XOR dst를 입력으로 사용하면 A->B와 B->A가 동일 해시 */
    __be32 addr_xor = saddr ^ daddr;
    __be16 port_xor = sport ^ dport;

    return toeplitz_hash(key, addr_xor, port_xor);
}

MSI-X 벡터 할당과 IRQ affinity

고성능 NIC 드라이버는 MSI-X(Message Signaled Interrupts - Extended) 인터럽트를 사용하여 각 RX/TX 큐에 독립적인 인터럽트 벡터를 할당합니다. pci_alloc_irq_vectors()가 벡터를 할당하고, irq_set_affinity_hint()가 CPU 친화도(Affinity)를 설정합니다.

NIC MSI-X 벡터 Vec 0 (RX/TX Q0) Vec 1 (RX/TX Q1) Vec 2 (RX/TX Q2) Vec 3 (RX/TX Q3) ... Vec N (관리/이벤트) NAPI 인스턴스 napi[0] poll napi[1] poll napi[2] poll napi[3] poll CPU 코어 (NUMA 노드) NUMA 0 CPU 0 ← Vec 0 CPU 1 ← Vec 1 CPU 2, CPU 3 NUMA 1 CPU 4 ← Vec 2 CPU 5 ← Vec 3 CPU 6, CPU 7 IRQ Affinity 전략 • managed: 커널이 NUMA 기반 자동 배치 • 로컬 NUMA 코어 우선 할당 • Vec:NAPI:Queue = 1:1:1 매핑 권장 • 관리 벡터는 별도 (이벤트/mailbox) sysfs 확인 /proc/interrupts /proc/irq/N/smp_affinity_list /sys/class/net/eth0/queues/
/* MSI-X 벡터 할당과 managed affinity 설정 */
static int my_alloc_msix(struct my_priv *priv)
{
    struct irq_affinity affd = {
        .pre_vectors  = 1,  /* 관리/이벤트 벡터 1개 예약 */
        .post_vectors = 0,
    };
    int num_vecs, ret;

    /* 원하는 큐 수 + 관리 벡터 */
    num_vecs = priv->num_queues + affd.pre_vectors;

    /* managed affinity로 벡터 할당 — 커널이 NUMA 최적 배치 */
    ret = pci_alloc_irq_vectors_affinity(priv->pdev,
        affd.pre_vectors + 1,  /* 최소: 관리 + 큐 1개 */
        num_vecs,               /* 최대 */
        PCI_IRQ_MSIX | PCI_IRQ_AFFINITY,
        &affd);

    if (ret < 0)
        return ret;

    priv->num_msix_vecs = ret;
    priv->num_queues = ret - affd.pre_vectors;

    /* 각 큐 벡터에 대해 IRQ 핸들러 등록 */
    for (int i = 0; i < priv->num_queues; i++) {
        int vec = i + affd.pre_vectors;
        struct my_qvec *qv = &priv->qvec[i];

        qv->irq = pci_irq_vector(priv->pdev, vec);

        ret = request_irq(qv->irq, my_msix_handler,
                          0, qv->name, qv);
        if (ret)
            goto err_free;

        /* managed affinity 사용 시 hint는 불필요
         * 비-managed인 경우에만 수동 설정 */
        if (!(priv->pdev->msix_enabled))
            irq_set_affinity_hint(qv->irq,
                get_cpu_mask(cpumask_local_spread(i,
                    dev_to_node(&priv->pdev->dev))));
    }

    /* 관리 벡터 등록 (벡터 0) */
    priv->mgmt_irq = pci_irq_vector(priv->pdev, 0);
    ret = request_irq(priv->mgmt_irq, my_mgmt_handler,
                      0, "my-mgmt", priv);

    return ret;

err_free:
    while (--i >= 0)
        free_irq(priv->qvec[i].irq, &priv->qvec[i]);
    pci_free_irq_vectors(priv->pdev);
    return ret;
}

XPS (Transmit Packet Steering) 내부 동작

XPS(Transmit Packet Steering)는 송신 패킷을 특정 TX 큐로 유도하여 캐시 효율을 높이는 메커니즘입니다. CPU 맵(CPU map)과 RX 큐 맵(RX queue map) 두 가지 모드가 있습니다.

XPS 모드맵 기준설정 경로효과
CPU map송신 CPU → TX 큐/sys/class/net/dev/queues/tx-N/xps_cpusTX 완료 IRQ와 같은 CPU에서 송신하여 캐시 히트 향상
RX queue mapRX 큐 → TX 큐/sys/class/net/dev/queues/tx-N/xps_rxqs수신-응답 경로가 동일 큐 쌍을 사용하여 지역성(Locality) 향상
/* net/core/dev.c - __netif_set_xps_queue() 핵심 분석 */
/* XPS는 dev->xps_maps에 CPU/RX큐 -> TX큐 매핑을 저장 */

/* XPS와 ndo_select_queue의 상호작용 */
static u16 my_select_queue(struct net_device *dev,
                           struct sk_buff *skb,
                           struct net_device *sb_dev)
{
    /* 커널 기본 경로:
     * 1. skb->queue_mapping이 설정되어 있으면 그대로 사용
     * 2. XPS 맵에서 현재 CPU에 해당하는 TX 큐 선택
     * 3. XPS가 없으면 해시 기반 분산 (skb_tx_hash)
     */

    /* 드라이버가 특별한 큐 선택 로직이 필요한 경우에만 구현 */
    /* 예: TC(Traffic Class) 기반 큐 선택 */
    if (skb->priority >= MY_PRIO_THRESHOLD)
        return priv->high_prio_queue;

    /* 기본 XPS/해시 선택에 위임 */
    return netdev_pick_tx(dev, skb, sb_dev);
}

/* sysfs를 통한 XPS CPU map 설정 예시 */
/*
 * # TX 큐 0은 CPU 0,1에서 사용
 * echo 3 > /sys/class/net/eth0/queues/tx-0/xps_cpus
 *
 * # TX 큐 1은 CPU 2,3에서 사용
 * echo c > /sys/class/net/eth0/queues/tx-1/xps_cpus
 *
 * # RX 큐 맵: TX 큐 0은 RX 큐 0의 응답 경로
 * echo 1 > /sys/class/net/eth0/queues/tx-0/xps_rxqs
 */

RPS/RFS 소프트웨어 스티어링

RPS(Receive Packet Steering)와 RFS(Receive Flow Steering)는 하드웨어 RSS를 지원하지 않는 디바이스에서 소프트웨어적으로 패킷을 여러 CPU에 분산하는 메커니즘입니다. RPS는 패킷 해시 기반으로, RFS는 애플리케이션의 소켓 위치 기반으로 CPU를 선택합니다.

기능RPSRFS
분산 기준패킷 해시 (소스/목적지 IP+포트)소켓이 마지막으로 처리된 CPU
목적softirq 부하 분산(Load Balancing)애플리케이션-인터럽트 CPU 일치
캐시 효과패킷 처리 분산소켓 데이터의 캐시 지역성 극대화
설정/sys/class/net/dev/queues/rx-N/rps_cpus/proc/sys/net/core/rps_sock_flow_entries
커널 자료구조rps_dev_flow_tablerps_sock_flow_table
사용 시점HW RSS 미지원 또는 단일 큐 NICRPS와 함께 사용, 서버 워크로드
/* net/core/dev.c - get_rps_cpu() 핵심 분석 */
static int get_rps_cpu(struct net_device *dev,
                       struct sk_buff *skb,
                       struct rps_dev_flow **rflowp)
{
    const struct rps_sock_flow_table *sock_flow_table;
    struct netdev_rx_queue *rxqueue = dev->_rx;
    struct rps_dev_flow_table *flow_table;
    struct rps_map *map;
    u32 hash, next_cpu, ident;
    int cpu = -1;

    /* 1단계: 패킷 해시 계산 (또는 HW 해시 재사용) */
    hash = skb_get_hash(skb);
    if (!hash)
        goto done;

    /* 2단계: RPS 맵에서 해시 기반 CPU 선택 */
    map = rcu_dereference(rxqueue->rps_map);
    if (map) {
        cpu = map->cpus[hash & (map->len - 1)]; /* reciprocal_scale 대신 간소화 */
    }

    /* 3단계: RFS 활성화 시 소켓 CPU와 비교 */
    sock_flow_table = rcu_dereference(rps_sock_flow_table);
    if (sock_flow_table) {
        ident = sock_flow_table->ents[hash & sock_flow_table->mask];
        next_cpu = ident & rps_cpu_mask;

        /* 소켓이 처리 중인 CPU가 RPS 맵에 있으면 그 CPU 선택 */
        if (cpu_online(next_cpu))
            cpu = next_cpu;
    }

    /* 4단계: 디바이스 플로우 테이블 갱신 */
    flow_table = rcu_dereference(rxqueue->rps_flow_table);
    if (flow_table) {
        struct rps_dev_flow *rflow =
            &flow_table->flows[hash & flow_table->mask];
        rflow->cpu = cpu;
        *rflowp = rflow;
    }

done:
    return cpu;
}

/* RPS/RFS 활성화 예시 (sysfs) */
/*
 * # 모든 CPU에서 RPS 활성화 (8 CPU 시스템)
 * echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
 *
 * # RFS 플로우 테이블 크기 설정 (보통 32768)
 * echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
 *
 * # 디바이스별 플로우 테이블 크기
 * echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
 */
실무 지침: HW RSS를 지원하는 NIC에서는 RPS/RFS를 함께 사용할 필요가 거의 없습니다. 단, RSS 큐 수가 CPU 수보다 적거나, 특정 플로우가 단일 큐에 집중되는 경우에는 RPS/RFS를 보조적으로 활성화하면 도움이 됩니다.

NUMA 친화적 큐 배치 전략

NUMA(Non-Uniform Memory Access) 시스템에서 네트워크 드라이버의 성능은 큐, 인터럽트, 메모리 할당이 올바른 NUMA 노드에 배치되었는지에 크게 좌우됩니다. 원격 NUMA 노드의 메모리 접근은 로컬 접근 대비 상당히 높은 지연이 발생합니다.

리소스NUMA 최적화 방법커널 API
DMA 링 메모리NIC가 연결된 NUMA 노드에 할당dev_to_node(), dma_alloc_coherent()
NAPI 구조체처리 CPU와 같은 노드에 할당kzalloc_node()
page_pool 페이지page_pool_params.nid 설정page_pool_create()
IRQ affinity로컬 NUMA 코어에 고정cpumask_local_spread()
XPS 맵TX 큐를 로컬 CPU에 매핑(Mapping)netif_set_xps_queue()
/* NUMA 친화적 큐 초기화 패턴 (ixgbe/mlx5 참고) */
static int my_alloc_queue_resources(struct my_priv *priv, int qid)
{
    struct device *dev = &priv->pdev->dev;
    int numa_node = dev_to_node(dev);
    struct my_ring *rx_ring, *tx_ring;

    /* 1. 링 구조체를 NIC의 NUMA 노드에 할당 */
    rx_ring = kzalloc_node(sizeof(*rx_ring), GFP_KERNEL, numa_node);
    tx_ring = kzalloc_node(sizeof(*tx_ring), GFP_KERNEL, numa_node);
    if (!rx_ring || !tx_ring)
        return -ENOMEM;

    /* 2. DMA 일관성 메모리도 같은 노드에서 할당 */
    rx_ring->desc = dma_alloc_coherent(dev,
        rx_ring->size * sizeof(struct my_rx_desc),
        &rx_ring->dma_addr, GFP_KERNEL);

    /* 3. page_pool을 같은 NUMA 노드로 설정 */
    struct page_pool_params pp = {
        .nid    = numa_node,
        .dev    = dev,
        .flags  = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
        .dma_dir = DMA_FROM_DEVICE,
    };
    rx_ring->pp = page_pool_create(&pp);

    /* 4. IRQ affinity: cpumask_local_spread()로 로컬 CPU 우선 */
    int target_cpu = cpumask_local_spread(qid, numa_node);
    irq_set_affinity_hint(priv->qvec[qid].irq,
                          get_cpu_mask(target_cpu));

    /* 5. XPS 설정: TX 큐를 로컬 CPU에 매핑 */
    netif_set_xps_queue(priv->ndev,
                        get_cpu_mask(target_cpu), qid);

    priv->rx_ring[qid] = rx_ring;
    priv->tx_ring[qid] = tx_ring;

    return 0;
}

/* cpumask_local_spread() 동작 원리:
 *   - idx=0이면 numa_node의 첫 번째 온라인 CPU 반환
 *   - idx가 로컬 CPU 수를 초과하면 원격 NUMA 노드 CPU 반환
 *   - 이렇게 하면 큐 0~N은 로컬 CPU에 우선 배치되고,
 *     남는 큐는 원격 CPU에 할당됩니다
 */

/* 실무: ethtool로 NUMA 배치 확인 */
/*
 * # NIC의 NUMA 노드 확인
 * cat /sys/class/net/eth0/device/numa_node
 *
 * # 각 큐의 IRQ affinity 확인
 * for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
 *     echo "IRQ $irq: $(cat /proc/irq/$irq/smp_affinity_list)"
 * done
 *
 * # NUMA 노드별 메모리 사용 확인
 * numastat -p $(pidof ksoftirqd/0)
 */
주의: dev_to_node()NUMA_NO_NODE(-1)을 반환하는 경우가 있습니다(예: 가상 머신, ACPI SRAT 테이블 누락). 이 경우 first_online_node로 폴백(Fallback)해야 합니다. 또한 kzalloc_node()에 잘못된 노드를 전달하면 로컬 노드로 자동 폴백되므로, 할당 자체는 실패하지 않지만 성능이 저하될 수 있습니다.

RX 메모리 경로: page_pool과 DMA recycling

고속 수신 경로에서 alloc_pages()/dma_map를 패킷마다 반복하면 CPU 비용이 폭증합니다. page_pool 기반 재사용은 대부분의 고성능 NIC 드라이버에서 사실상 표준 패턴입니다.

/* 개념 예시: page_pool 기반 RX 메모리 재사용 */
static int my_rx_pool_init(struct my_priv *priv)
{
    struct page_pool_params pp = {
        .flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
        .order = 0,
        .pool_size = 4096,
        .nid = dev_to_node(priv->dev),
        .dev = priv->dev,
        .dma_dir = DMA_FROM_DEVICE,
    };

    priv->rx_pp = page_pool_create(&pp);
    if (IS_ERR(priv->rx_pp))
        return PTR_ERR(priv->rx_pp);

    return 0;
}

static void my_rx_recycle_page(struct my_priv *priv, struct page *page)
{
    page_pool_recycle_direct(priv->rx_pp, page);
}
주의: XDP + page_pool 조합에서는 frame ownership 규칙을 엄격히 지켜야 합니다. XDP_REDIRECT, XDP_TX, XDP_DROP 경로별 반환 API를 혼용하면 double free/메모리 누수가 쉽게 발생합니다.

page_pool 내부 자료구조와 할당 경로

struct page_pool은 크게 세 가지 계층으로 구성됩니다. 가장 빠른 경로인 alloc 캐시는 per-CPU 배열(기본 128개)로, NAPI 폴링 컨텍스트에서 잠금 없이 페이지를 꺼내옵니다. 캐시가 비면 ptr_ring 기반 리사이클 링으로 폴백(fallback)하며, 여기서도 부족하면 슬로 패스(slow path)로 버디 할당자(Buddy Allocator)에서 새 페이지를 할당합니다.

page_pool 할당/재활용 생명주기 NAPI RX Poll Alloc Cache (Fast) per-CPU 배열, 128개 ptr_ring (Recycle) lock-free MPSC 링 miss Buddy Allocator (Slow) alloc_pages() + DMA map miss SKB / XDP Frame 네트워크 스택 전달 page page_pool_put_page skb_mark_for_recycle direct ring DMA 매핑 생명주기 dma_map_page() DMA_SYNC_DEV HW 사용 (RX/TX) 재활용 (unmap 생략) PP_FLAG_DMA_MAP 설정 시 page_pool이 DMA 매핑을 자동 관리 → 재활용 시 unmap/remap 비용 제거 PP_FLAG_DMA_SYNC_DEV 설정 시 할당 반환마다 dma_sync_single_range_for_device() 자동 호출

커널 소스(net/core/page_pool.c)에서 page_pool_alloc_pages()의 핵심 흐름을 분석하면 다음과 같습니다.

/* net/core/page_pool.c — 할당 경로 핵심 분석 */
struct page *page_pool_alloc_pages(struct page_pool *pool,
                                    gfp_t gfp)
{
    struct page *page;

    /* 1단계: alloc 캐시에서 꺼냄 (가장 빠름, 락 없음) */
    if (likely(pool->alloc.count)) {
        page = pool->alloc.cache[--pool->alloc.count];
        return page;
    }

    /* 2단계: ptr_ring에서 벌크로 캐시 채움 */
    page = __page_pool_get_cached(pool);
    if (page)
        return page;

    /* 3단계: 슬로 패스 — 버디 할당자에서 새 페이지 할당 + DMA 매핑 */
    page = __page_pool_alloc_pages_slow(pool, gfp);
    return page;
}

/* 슬로 패스: DMA 매핑 포함 */
static struct page *__page_pool_alloc_pages_slow(
    struct page_pool *pool, gfp_t gfp)
{
    struct page *page;

    page = alloc_pages_node(pool->p.nid, gfp, pool->p.order);
    if (unlikely(!page))
        return NULL;

    /* PP_FLAG_DMA_MAP이 설정된 경우 자동 DMA 매핑 */
    if (pool->p.flags & PP_FLAG_DMA_MAP) {
        if (__page_pool_dma_map(pool, page)) {
            put_page(page);
            return NULL;
        }
    }

    page_pool_set_pp_info(pool, page);
    pool->pages_state_hold_cnt++;
    return page;
}

핵심 포인트는 alloc 캐시가 비었을 때 ptr_ring에서 최대 PP_ALLOC_CACHE_REFILL(기본 64)개를 한 번에 가져와 캐시를 채우는 벌크 리필(bulk refill) 전략입니다. 이 방식으로 ptr_ring 잠금 경합을 최소화합니다.

PP_FLAG 옵션 상세

page_pool_params에 설정하는 플래그(Flag)는 page_pool의 동작을 근본적으로 변경합니다. 각 플래그의 의미와 성능 영향을 정확히 이해해야 합니다.

플래그설명일반적 사용처성능 영향
PP_FLAG_DMA_MAP page_pool이 DMA 매핑/언매핑(Unmapping)을 자동 관리. 재활용(Recycling) 시 remap 생략 모든 NIC 드라이버 (기본 권장) 재활용 경로에서 dma_map/unmap 제거 → 10~30% RX 성능 향상
PP_FLAG_DMA_SYNC_DEV 페이지 반환 시 dma_sync_single_range_for_device() 자동 호출 non-coherent DMA 아키텍처 (ARM 등) 캐시 일관성(Cache Coherency) 보장, 약간의 오버헤드 추가
PP_FLAG_PAGE_FRAG 하나의 페이지를 여러 프래그먼트(Fragment)로 분할 할당 소형 패킷 위주 트래픽 (DNS, VoIP 등) 메모리 효율 크게 향상, 단 프래그먼트 추적 오버헤드
PP_FLAG_SYSTEM_POOL 시스템 전역 공유 풀 사용. 여러 netdev가 하나의 pool 공유 가상 NIC, 다수 인터페이스 환경 메모리 절약, 캐시 히트율 하락 가능
권장 조합: 대부분의 물리 NIC 드라이버에서는 PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV 조합이 표준입니다. ARM64 서버에서는 PP_FLAG_DMA_SYNC_DEV가 필수이고, x86에서도 안전을 위해 포함하는 것이 좋습니다.

page_pool 통계와 모니터링

page_pool은 커널 6.2부터 ethtool 통계 인터페이스를 통해 재활용 효율(Recycle Efficiency)을 실시간으로 모니터링할 수 있습니다. 재활용 히트율(Hit Ratio)이 90% 이하로 떨어지면 패킷 처리 성능이 급격히 저하됩니다.

/* ethtool을 통한 page_pool 통계 조회 */
/* $ ethtool -S eth0 | grep page_pool */
/*   page_pool_alloc_fast: 1234567   ← alloc 캐시 히트 */
/*   page_pool_alloc_slow: 456       ← 버디 할당자 폴백 */
/*   page_pool_alloc_refill: 12345   ← ptr_ring 리필 */
/*   page_pool_recycle_cached: 1234000 ← 직접 캐시 반환 */
/*   page_pool_recycle_ring: 567      ← ptr_ring 반환 */
/*   page_pool_recycle_released: 89   ← 풀로 못 돌려 해제 */

/* 드라이버에서 page_pool 통계를 ethtool에 노출하는 방법 */
static void my_get_ethtool_stats(struct net_device *ndev,
                                  struct ethtool_stats *stats,
                                  u64 *data)
{
    struct my_priv *priv = netdev_priv(ndev);
    struct page_pool_stats pp_stats = {};
    int i = 0;

    /* per-queue 통계 수집 */
    for (int q = 0; q < priv->num_rx_queues; q++) {
        if (!priv->rx_ring[q].page_pool)
            continue;
        page_pool_get_stats(priv->rx_ring[q].page_pool,
                            &pp_stats);
    }

    data[i++] = pp_stats.alloc_stats.fast;
    data[i++] = pp_stats.alloc_stats.slow;
    data[i++] = pp_stats.alloc_stats.slow_high_order;
    data[i++] = pp_stats.alloc_stats.empty;
    data[i++] = pp_stats.alloc_stats.refill;
    data[i++] = pp_stats.alloc_stats.waive;
    data[i++] = pp_stats.recycle_stats.cached;
    data[i++] = pp_stats.recycle_stats.cache_full;
    data[i++] = pp_stats.recycle_stats.ring;
    data[i++] = pp_stats.recycle_stats.ring_full;
    data[i++] = pp_stats.recycle_stats.released_refcnt;
}

bpftrace를 사용하면 런타임에 page_pool 슬로 패스 진입 빈도를 추적할 수 있습니다.

/* bpftrace: page_pool 슬로 패스 추적 */
/* $ sudo bpftrace -e '
   kprobe:__page_pool_alloc_pages_slow {
       @slow_alloc[comm] = count();
   }
   kprobe:page_pool_alloc_pages {
       @total_alloc[comm] = count();
   }
   interval:s:5 {
       print(@slow_alloc);
       print(@total_alloc);
       clear(@slow_alloc);
       clear(@total_alloc);
   }
' */
재활용 히트율 계산: hit_ratio = (alloc_fast + alloc_refill) / (alloc_fast + alloc_refill + alloc_slow). 이 비율이 95% 이상이어야 최적 성능입니다. 90% 이하이면 alloc.count 튜닝이나 NAPI budget 조정을 검토해야 합니다.

프래그먼트 모드와 Header/Data Split

PP_FLAG_PAGE_FRAG를 설정하면 page_pool은 하나의 페이지를 여러 개의 작은 프래그먼트(Fragment)로 나누어 할당합니다. 예를 들어 4KB 페이지에서 256바이트짜리 DNS 패킷용 버퍼 15개를 할당할 수 있어 메모리 효율이 크게 향상됩니다.

헤더/데이터 분리(Header/Data Split) 패턴에서는 이더넷(Ethernet) 프레임의 헤더 부분(일반적으로 128~256바이트)만 선형 버퍼에 배치하고, 나머지 페이로드(Payload)는 page_pool 프래그먼트로 구성합니다. 이 방식은 캐시 효율과 제로 카피(Zero-copy) 전달을 동시에 만족시킵니다.

/* 프래그먼트 기반 RX 경로 구현 예시 */
static struct sk_buff *my_rx_build_skb_frag(
    struct my_rx_ring *ring,
    struct my_rx_desc *desc)
{
    struct page *page = ring->rx_buf[desc->idx].page;
    unsigned int offset = ring->rx_buf[desc->idx].offset;
    unsigned int len = desc->length;
    unsigned int hdr_len = min_t(unsigned int, len,
                                  ring->rx_hdr_size);
    unsigned int data_len = len - hdr_len;
    struct sk_buff *skb;

    /* 헤더 부분으로 SKB 생성 */
    skb = napi_alloc_skb(&ring->napi, hdr_len);
    if (unlikely(!skb))
        return NULL;

    /* 헤더 복사 (캐시 효율적, 작은 크기) */
    memcpy(skb_put(skb, hdr_len),
           page_address(page) + offset, hdr_len);

    /* 데이터 부분은 프래그먼트로 추가 (제로 카피) */
    if (data_len) {
        skb_add_rx_frag(skb, 0, page,
                        offset + hdr_len, data_len,
                        ring->rx_buf_size);
        /* page_pool 재활용을 위해 마킹 */
        skb_mark_for_recycle(skb);
    } else {
        /* 헤더만으로 충분한 소형 패킷: 페이지 즉시 반환 */
        page_pool_put_full_page(ring->page_pool,
                                page, false);
    }

    return skb;
}

/* page_pool 프래그먼트 모드 초기화 */
static int my_setup_page_pool_frag(struct my_priv *priv)
{
    struct page_pool_params pp = {
        .order       = 0,
        .pool_size   = priv->rx_ring_size * 2,
        .nid         = dev_to_node(&priv->pdev->dev),
        .dev         = &priv->pdev->dev,
        .dma_dir     = DMA_FROM_DEVICE,
        .flags       = PP_FLAG_DMA_MAP |
                       PP_FLAG_DMA_SYNC_DEV |
                       PP_FLAG_PAGE_FRAG,
        .max_len     = PAGE_SIZE,
    };

    priv->rx_pp = page_pool_create(&pp);
    return IS_ERR(priv->rx_pp) ?
           PTR_ERR(priv->rx_pp) : 0;
}
성능 팁: 프래그먼트 모드에서는 max_len 파라미터를 실제 최대 패킷 크기에 맞추어야 합니다. 기본값 PAGE_SIZE보다 작게 설정하면 하나의 페이지에서 더 많은 프래그먼트를 할당할 수 있습니다. MTU 1500 환경에서는 max_len = 2048이 적절합니다.

TC/NFT 오프로드와 switchdev 연계

데이터센터 NIC는 tc flower 규칙을 하드웨어 테이블로 오프로드해 CPU 부하를 낮춥니다. 이때 드라이버는 수용 가능한 매치/액션 집합을 명확히 제한하고, 부분 실패 시 fallback 정책을 분명히 해야 합니다.

/* 개념 예시: TC setup type별 오프로드 분기 */
static int my_ndo_setup_tc(struct net_device *ndev, enum tc_setup_type type, void *type_data)
{
    switch (type) {
    case TC_SETUP_BLOCK:
        return my_tc_block_cb_setup(ndev, type_data);
    case TC_SETUP_QDISC_MQPRIO:
        return my_mqprio_setup(ndev, type_data);
    default:
        return -EOPNOTSUPP;
    }
}
오프로드 대상대표 인터페이스주의점
분류/필터tc flower + ndo_setup_tc규칙 우선순위/충돌 처리
eSwitchswitchdev, representor netdevVF/representor 일관성
암호화(Encryption)/터널(Tunnel)xfrm offload, UDP tunnel offloadfallback 경로와 통계 구분

tc flower 오프로드 내부 흐름

tc flower 오프로드는 FLOW_CLS_REPLACE, FLOW_CLS_DESTROY, FLOW_CLS_STATS 세 가지 명령으로 구성됩니다. 사용자가 tc filter add를 실행하면 커널은 fl_hw_replace_filter()를 통해 드라이버의 ndo_setup_tc를 호출하고, 드라이버는 하드웨어 플로 테이블(Flow Table)에 규칙을 삽입합니다.

tc flower 오프로드 흐름: 사용자 공간 → 하드웨어 tc filter add (사용자 공간) RTM_NEWTFILTER Netlink 메시지 cls_flower fl_change() → 파싱 fl_hw_replace_filter HW 오프로드 요청 flow_block_cb TC_SETUP_BLOCK 등록 flow_cls_offload FLOW_CLS_REPLACE 드라이버 콜백 매치/액션 변환 HW Flow Table TCAM/eSwitch 명령별 흐름 FLOW_CLS_REPLACE → HW 규칙 삽입 FLOW_CLS_DESTROY → HW 규칙 삭제 + 쿠키 해제 FLOW_CLS_STATS → HW 카운터 → tc 통계 flow_cls_offload 주요 필드 • command: REPLACE / DESTROY / STATS • cookie: 규칙 식별자 (tc가 할당) • rule → match.key/mask + action 지원 매치 키/액션 (대표) • Key: eth_type, ip_proto, src/dst IP, L4 port, VLAN • Action: drop, redirect, mirred, pedit, vlan push/pop • 부분 오프로드 시 FLOW_ACT_NO_APPEND 반환
/* 완전한 flower 오프로드 콜백 구현 */
static int my_flower_replace(struct my_priv *priv,
                             struct flow_cls_offload *f)
{
    struct flow_rule *rule = flow_cls_offload_flow_rule(f);
    struct flow_match_eth_addrs match_eth;
    struct flow_match_ipv4_addrs match_ip;
    struct flow_match_ports match_ports;
    struct my_flow_entry *entry;
    int err;

    /* 매치 키 파싱 */
    if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_ETH_ADDRS))
        flow_rule_match_eth_addrs(rule, &match_eth);

    if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_IPV4_ADDRS))
        flow_rule_match_ipv4_addrs(rule, &match_ip);

    if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_PORTS))
        flow_rule_match_ports(rule, &match_ports);

    /* 지원하지 않는 매치 키 검사 */
    if (flow_rule_match_key(rule, FLOW_DISSECTOR_KEY_ENC_KEYID)) {
        NL_SET_ERR_MSG_MOD(f->common.extack,
            "tunnel key match not supported");
        return -EOPNOTSUPP;
    }

    /* 액션 파싱 */
    if (!flow_action_has_entries(&rule->action))
        return -EINVAL;

    entry = kzalloc(sizeof(*entry), GFP_KERNEL);
    if (!entry)
        return -ENOMEM;

    entry->cookie = f->cookie;

    /* HW 플로 테이블에 규칙 삽입 */
    err = my_hw_add_flow(priv, entry, rule);
    if (err) {
        kfree(entry);
        return err;
    }

    /* 쿠키 기반 해시 테이블에 저장 (나중에 삭제/통계 조회용) */
    hash_add(priv->flow_table, &entry->node,
             entry->cookie);
    return 0;
}

static int my_flower_destroy(struct my_priv *priv,
                             struct flow_cls_offload *f)
{
    struct my_flow_entry *entry;

    entry = my_flow_lookup(priv, f->cookie);
    if (!entry)
        return -ENOENT;

    my_hw_del_flow(priv, entry);
    hash_del(&entry->node);
    kfree(entry);
    return 0;
}

static int my_flower_stats(struct my_priv *priv,
                            struct flow_cls_offload *f)
{
    struct my_flow_entry *entry;
    u64 packets, bytes, lastused;

    entry = my_flow_lookup(priv, f->cookie);
    if (!entry)
        return -ENOENT;

    my_hw_read_flow_stats(priv, entry,
                          &packets, &bytes, &lastused);
    flow_stats_update(&f->stats, bytes, packets,
                      0, lastused,
                      FLOW_ACTION_HW_STATS_DELAYED);
    return 0;
}

/* block 콜백 진입점 */
static int my_tc_block_cb(enum tc_setup_type type,
                          void *type_data, void *cb_priv)
{
    struct my_priv *priv = cb_priv;
    struct flow_cls_offload *f = type_data;

    if (type != TC_SETUP_CLSFLOWER)
        return -EOPNOTSUPP;

    switch (f->command) {
    case FLOW_CLS_REPLACE:
        return my_flower_replace(priv, f);
    case FLOW_CLS_DESTROY:
        return my_flower_destroy(priv, f);
    case FLOW_CLS_STATS:
        return my_flower_stats(priv, f);
    default:
        return -EOPNOTSUPP;
    }
}

하드웨어 Flow Table 관리

하드웨어 플로 테이블(Hardware Flow Table)은 TCAM(Ternary Content-Addressable Memory) 또는 eSwitch의 플로 테이블로 구현됩니다. 테이블 항목의 생명주기를 올바르게 관리하지 않으면 규칙 누수(Rule Leak)나 스톨(Stall) 오프로드(Stale Offload)가 발생합니다.

각 플로 항목은 tc가 할당한 쿠키(Cookie)로 식별됩니다. 쿠키는 규칙의 전체 생명주기 동안 유일하며, FLOW_CLS_DESTROY 시 드라이버는 이 쿠키로 대응하는 하드웨어 항목을 찾아 삭제합니다.

/* 부분 오프로드 처리 패턴 */
static int my_flower_replace_partial(struct my_priv *priv,
                                     struct flow_cls_offload *f)
{
    struct flow_rule *rule = flow_cls_offload_flow_rule(f);
    const struct flow_action_entry *act;
    int i;

    /* 모든 액션 순회하며 HW 지원 여부 확인 */
    flow_action_for_each(i, act, &rule->action) {
        switch (act->id) {
        case FLOW_ACTION_DROP:
        case FLOW_ACTION_REDIRECT:
        case FLOW_ACTION_MIRRED:
            break;  /* 지원 */

        case FLOW_ACTION_MANGLE:
            /* 헤더 수정: L3/L4만 지원, L2 수정은 SW fallback */
            if (act->mangle.htype == FLOW_ACT_MANGLE_HDR_TYPE_ETH) {
                NL_SET_ERR_MSG_MOD(f->common.extack,
                    "L2 header modification not supported in HW");
                return -EOPNOTSUPP;
            }
            break;

        default:
            NL_SET_ERR_MSG_MOD(f->common.extack,
                "unsupported action for HW offload");
            return -EOPNOTSUPP;
        }
    }

    /* HW 테이블 용량 확인 */
    if (atomic_read(&priv->flow_count) >= priv->max_flows) {
        NL_SET_ERR_MSG_MOD(f->common.extack,
            "HW flow table full");
        return -ENOSPC;
    }

    return my_hw_insert_flow(priv, f);
}
에러 보고: 오프로드 실패 시 NL_SET_ERR_MSG_MOD()를 사용해 사용자에게 구체적인 실패 이유를 전달해야 합니다. 단순히 -EOPNOTSUPP만 반환하면 운영자가 원인을 파악하기 어렵습니다. tc -s filter show로 오프로드 상태(in_hw / not_in_hw)를 확인할 수 있습니다.

switchdev 통합과 FDB 오프로드

switchdev 모델은 하드웨어 스위치 ASIC를 리눅스 브리지(Linux Bridge)와 통합하는 프레임워크입니다. 드라이버는 SWITCHDEV_OBJ_ID_PORT_FDB 등의 알림(Notification)을 처리하여 FDB(Forwarding Database) 항목을 하드웨어에 오프로드합니다.

switchdev 오브젝트설명대응 동작
SWITCHDEV_OBJ_ID_PORT_FDBMAC 주소 → 포트 매핑HW FDB 테이블 업데이트
SWITCHDEV_OBJ_ID_PORT_MDB멀티캐스트(Multicast) 그룹 → 포트 매핑HW MDB 테이블 업데이트
SWITCHDEV_OBJ_ID_PORT_VLANVLAN ID → 포트 매핑HW VLAN 필터 테이블
SWITCHDEV_OBJ_ID_HOST_MDB호스트 멀티캐스트 그룹CPU 포트로 멀티캐스트 전달
SWITCHDEV_ATTR_ID_BRIDGE_VLAN_FILTERINGVLAN 필터링 활성화/비활성화브리지 전체 VLAN 모드 설정
/* switchdev 알림 등록과 FDB 오프로드 */
static int my_switchdev_event(struct notifier_block *nb,
                              unsigned long event,
                              void *ptr)
{
    struct net_device *dev = switchdev_notifier_info_to_dev(ptr);
    struct switchdev_notifier_fdb_info *fdb_info;

    if (!my_is_our_port(dev))
        return NOTIFY_DONE;

    switch (event) {
    case SWITCHDEV_FDB_ADD_TO_DEVICE:
        fdb_info = ptr;
        /* 비동기 처리를 위해 workqueue에 이관 */
        my_schedule_fdb_work(dev, fdb_info, true);
        break;

    case SWITCHDEV_FDB_DEL_TO_DEVICE:
        fdb_info = ptr;
        my_schedule_fdb_work(dev, fdb_info, false);
        break;
    }

    return NOTIFY_DONE;
}

static struct notifier_block my_switchdev_nb = {
    .notifier_call = my_switchdev_event,
};

/* 모듈 초기화 시 등록 */
static int __init my_sw_init(void)
{
    int err;

    err = register_switchdev_notifier(&my_switchdev_nb);
    if (err)
        return err;

    err = register_switchdev_blocking_notifier(
              &my_switchdev_blocking_nb);
    if (err) {
        unregister_switchdev_notifier(&my_switchdev_nb);
        return err;
    }

    return 0;
}
주의: switchdev 알림은 atomic 컨텍스트에서 호출될 수 있으므로, FDB 알림 핸들러(Handler)에서 직접 하드웨어를 프로그래밍하면 안 됩니다. 반드시 workqueue로 이관하여 프로세스(Process) 컨텍스트에서 처리해야 합니다. register_switchdev_blocking_notifier()는 블로킹 컨텍스트에서 호출되므로 직접 처리가 가능합니다.

UDP 터널 오프로드

VXLAN(Virtual Extensible LAN), Geneve 등의 UDP 터널은 NIC 하드웨어가 외부 UDP 포트를 인식해야 내부 패킷의 RSS, checksum offload 등이 올바르게 동작합니다. udp_tunnel_nic_info 구조체를 통해 드라이버는 지원하는 터널 유형과 최대 포트 수를 선언합니다.

터널 유형기본 UDP 포트대표 드라이버 지원비고
VXLAN4789mlx5, ice, bnxt, i40e가장 널리 사용되는 오버레이(Overlay)
Geneve6081mlx5, ice, bnxt유연한 TLV 옵션 지원
VXLAN-GPE4790mlx5다중 프로토콜 캡슐화(Encapsulation)
GTP-U2152일부 SmartNIC5G/LTE 백홀(Backhaul)
/* UDP 터널 오프로드 구성 */
static const struct udp_tunnel_nic_info my_tunnel_info = {
    .set_port   = my_udp_tunnel_set_port,
    .unset_port = my_udp_tunnel_unset_port,
    .sync_table = my_udp_tunnel_sync,
    .flags      = UDP_TUNNEL_NIC_INFO_MAY_SLEEP |
                  UDP_TUNNEL_NIC_INFO_OPEN_ONLY,
    .tables     = {
        {
            .n_entries  = 2,  /* 최대 2개 VXLAN 포트 */
            .tunnel_types = UDP_TUNNEL_TYPE_VXLAN,
        },
        {
            .n_entries  = 2,  /* 최대 2개 Geneve 포트 */
            .tunnel_types = UDP_TUNNEL_TYPE_GENEVE,
        },
    },
};

/* set_port 콜백: HW에 UDP 포트 등록 */
static int my_udp_tunnel_set_port(
    struct net_device *ndev,
    unsigned int table, unsigned int entry,
    struct udp_tunnel_info *ti)
{
    struct my_priv *priv = netdev_priv(ndev);
    u16 port = ntohs(ti->port);

    netdev_info(ndev,
        "adding UDP tunnel port %u type %d\n",
        port, ti->type);

    /* 하드웨어 레지스터에 터널 포트 설정 */
    my_hw_write_tunnel_port(priv, table, entry, port,
                            ti->type);

    /* RSS 해시 정책을 터널 내부 헤더 기반으로 변경 */
    my_hw_set_inner_rss(priv, true);

    return 0;
}

/* ndo_open 시 터널 포트 동기화 요청 */
static int my_ndo_open_with_tunnel(struct net_device *ndev)
{
    int err;

    err = my_hw_init(ndev);
    if (err)
        return err;

    /* 커널에 등록된 터널 포트를 HW로 푸시 */
    udp_tunnel_nic_reset_ntf(ndev);

    netif_tx_start_all_queues(ndev);
    return 0;
}
포트 동기화: UDP_TUNNEL_NIC_INFO_OPEN_ONLY 플래그를 설정하면 인터페이스가 UP 상태일 때만 포트를 동기화합니다. 인터페이스가 DOWN 상태에서 터널이 생성/삭제되면, 다음 ndo_open()udp_tunnel_nic_reset_ntf()를 호출하여 전체 테이블을 재동기화합니다.

오프로드 계약: checksum/GSO/GRO/VLAN

오프로드 기능은 “켜고 끄는 옵션”이 아니라 드라이버와 스택 사이의 계약입니다. advertise한 기능을 데이터 경로에서 일관되게 지키지 않으면 패킷 손실, checksum 오류, MTU 이상 동작이 발생합니다.

/* 개념 예시: feature dependency를 fix_features에서 강제 */
static netdev_features_t my_ndo_fix_features(struct net_device *ndev,
                                       netdev_features_t features)
{
    /* HW가 IPv6 TSO를 지원하지 않으면 강제 비활성화 */
    if (!(features & NETIF_F_IP_CSUM))
        features &= ~NETIF_F_TSO;

    if (!my_hw_supports_tso6(ndev))
        features &= ~NETIF_F_TSO6;

    return features;
}

static netdev_features_t my_ndo_features_check(struct sk_buff *skb,
                                            struct net_device *ndev,
                                            netdev_features_t features)
{
    /* 헤더 길이/세그먼트 조건 미충족 시 SW fallback */
    if (skb_is_gso(skb) && skb_shinfo(skb)->gso_segs > 512)
        features &= ~(NETIF_F_GSO_MASK);

    return features;
}
기능군관련 플래그/API드라이버 확인 포인트
Checksum offloadNETIF_F_HW_CSUM, skb->ip_summedpartial checksum descriptor 구성
TSO/GSONETIF_F_TSO*, gso_size세그먼트 제한, header split 처리
GRO/LROnapi_gro_receive()재조립 후 메타데이터 일관성
VLAN offloadNETIF_F_HW_VLAN_CTAG_TX/RXtag insert/strip와 통계 동기화

SR-IOV, representor, 스위치 모드 전환

클라우드 환경에서는 PF/VF 분리와 representor netdev 운영이 기본입니다. 드라이버는 legacy 모드와 switchdev 모드 전환 시 control-plane 일관성을 보장해야 합니다.

/* 개념 예시: eswitch 모드 전환과 대표자 netdev 동기화 */
static int my_eswitch_mode_set(struct my_priv *priv, u16 mode)
{
    if (mode == DEVLINK_ESWITCH_MODE_SWITCHDEV)
        return my_enable_representors(priv);
    if (mode == DEVLINK_ESWITCH_MODE_LEGACY)
        return my_disable_representors(priv);

    return -EOPNOTSUPP;
}
운영 포인트: VF reset, 링크 이벤트, tc offload rule 삭제 시 PF/VF/representor 간 상태 동기화가 어긋나면 패킷 블랙홀이나 정책 누락이 발생합니다.

TX 큐 선택: ndo_select_queue, XPS, CPU locality

멀티큐 NIC에서 ndo_start_xmit() 성능은 큐 선택 품질에 크게 좌우됩니다. 플로우 해시, CPU affinity, XPS 정책이 맞지 않으면 lock 경합과 cache miss가 급증합니다.

/* 개념 예시: qdisc/XPS 힌트를 반영한 TX queue 선택 */
static u16 my_ndo_select_queue(struct net_device *dev, struct sk_buff *skb,
                               struct net_device *sb_dev)
{
    u32 hash = skb_get_hash(skb);
    u16 q = reciprocal_scale(hash, dev->real_num_tx_queues);

    /* 로컬 CPU 우선 정책이 있으면 q를 재매핑 */
    q = my_xps_remap(dev, q, raw_smp_processor_id());
    return q;
}

Doorbell 메커니즘과 DMA 메모리 배리어(Memory Barrier)

약한 메모리 모델 CPU(ARM64 등)에서 descriptor write와 doorbell MMIO write의 순서가 보장되지 않으면 간헐적 TX hang이 생깁니다. 게시 경로에서 barrier 사용 규칙을 문서화해야 합니다.

Doorbell이란?

Doorbell은 PCIe BAR 공간의 장치 레지스터(Register)에 MMIO write를 수행하여 NIC에 새 작업(디스크립터)이 준비되었음을 알리는 메커니즘입니다. Doorbell이 없으면 NIC이 링 버퍼(Ring Buffer)를 지속적으로 폴링해야 하며, 이는 PCIe 대역폭(Bandwidth) 낭비와 전력 소모 증가를 초래합니다.

Doorbell의 핵심 속성은 다음과 같습니다.

서브시스템별 Doorbell 비교

Doorbell 메커니즘은 NIC뿐 아니라 다양한 PCIe 디바이스 서브시스템에서 사용됩니다. 아래 표는 주요 서브시스템별 doorbell 특성을 비교합니다.

서브시스템Doorbell 대상기록 값최적화 기법
NIC TXTail Pointer 레지스터마지막 디스크립터 인덱스배치 게시
NVMeSQ Tail Doorbell큐 tail 포인터Shadow Doorbell Buffer
xHCI (USB 3.x)Doorbell ArrayEP 인덱스스트림 기반 배치
NTB (PCI)Doorbell 비트맵(Bitmap)이벤트 비트비트마스크
CPU: desc 작성 TX Ring (DMA 메모리) dma_wmb() writel() Doorbell (MMIO write) NIC HW (DMA fetch) dma_wmb()는 descriptor 쓰기가 doorbell MMIO 쓰기보다 먼저 디바이스에 보이도록 보장합니다.

Doorbell 최적화 기법

Doorbell은 PCIe MMIO 트랜잭션이므로 호출 빈도를 줄이는 것이 성능 최적화의 핵심입니다. 주요 기법은 다음과 같습니다.

/* 개념 예시: doorbell 전 메모리 배리어 보장 */
static void my_post_tx_desc(struct my_priv *priv, struct my_desc *d)
{
    priv->tx_ring[d->idx] = *d;

    /* descriptor 메모리 write 완료 보장 */
    dma_wmb();

    /* 이후 doorbell write */
    writel(d->idx, priv->tx_doorbell);
}
상황권장 배리어목적
descriptor → MMIO doorbelldma_wmb()디바이스가 완전한 descriptor만 보도록 보장
MMIO status read 후 메모리 참조dma_rmb()완료 상태와 data buffer ordering 보장
일반 CPU 공유 데이터smp_wmb/rmb소프트웨어 스레드 간 ordering 보장

Busy Poll/NAPI 조합과 지연 최적화

초저지연 워크로드에서는 interrupt moderation보다 busy-poll이 유리할 수 있습니다. 드라이버는 NAPI 상태 전이를 안정적으로 유지해 busy-poll 사용자와 일반 트래픽이 충돌하지 않게 해야 합니다.

# 실습 예제: busy poll 파라미터 조정 및 즉시 확인
# 소켓 단위 busy poll (마이크로초)
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=50

# NIC interrupt moderation과 함께 튜닝
ethtool -C eth0 rx-usecs 0
ethtool -C eth0 tx-usecs 0
주의: busy-poll은 tail latency를 낮출 수 있지만 CPU 사용률을 크게 증가시킵니다. 배치 처리 워크로드와 혼재 시에는 cpuset/isolcpus로 busy-poll 전용 CPU를 분리하는 편이 안전합니다.

Busy Poll 내부 동작 원리

Busy poll의 핵심은 napi_busy_loop() 함수입니다. 소켓이 데이터를 기다릴 때 인터럽트 기반 수신 대신 NAPI poll 함수를 직접 호출하여 패킷을 가져옵니다. 이 과정에서 커널은 sk_can_busy_loop()으로 소켓이 busy poll 가능 상태인지 먼저 확인합니다.

/* net/core/dev.c - napi_busy_loop() 핵심 경로 분석 */
void napi_busy_loop(unsigned int napi_id,
                    bool (*loop_end)(void *, unsigned long),
                    void *loop_end_arg, bool prefer_busy_poll,
                    u16 budget)
{
    unsigned long start_time = loop_end ? busy_loop_current_time() : 0;
    int (*napi_poll)(struct napi_struct *napi, int budget);
    struct napi_struct *napi;

    restart:
    napi_poll = NULL;

    rcu_read_lock();
    napi = napi_by_id(napi_id);
    if (!napi)
        goto out;

    /* NAPI가 스케줄 가능 상태인지 확인 */
    if (!test_bit(NAPI_STATE_SCHED, &napi->state))
        goto out;

    /* prefer_busy_poll 플래그로 NAPI 독점 모드 요청 */
    if (prefer_busy_poll)
        set_bit(NAPI_STATE_PREFER_BUSY_POLL, &napi->state);

    for (;;) {
        /* napi_poll 함수 포인터를 통해 드라이버의 poll 직접 호출 */
        work = napi_poll(napi, budget);

        if (loop_end && loop_end(loop_end_arg, start_time))
            break;

        /* 타임아웃 또는 시그널 확인 */
        if (need_resched())
            break;

        cpu_relax();  /* 전력 소모를 약간 줄이는 힌트 */
    }
out:
    rcu_read_unlock();
}

sk_can_busy_loop()은 소켓의 busy poll 적격 여부를 판단합니다. NAPI ID가 할당되어 있고, 소켓에 SO_BUSY_POLL 옵션이 설정되어 있어야 합니다.

/* include/net/busy_poll.h - busy poll 적격 판단 */
static inline bool sk_can_busy_loop(const struct sock *sk)
{
    /* 1) NAPI ID가 유효한지 (드라이버가 할당했는지) */
    /* 2) 소켓에 busy_poll 타임아웃이 설정되어 있는지 */
    /* 3) 소켓이 커널에 의해 잠기지 않았는지 */
    return sk->sk_napi_id &&
           READ_ONCE(sk->sk_ll_usec) &&
           !skb_queue_empty_lockless(&sk->sk_receive_queue);
}

Poll 예산(Budget)은 busy-poll 모드에서 일반 NAPI poll과 다르게 동작합니다. 기본 NAPI poll의 예산이 64인 반면, busy-poll에서는 소켓별로 설정된 예산을 사용하며, 이는 처리량과 독점 방지 사이의 균형을 조절합니다. NAPI_STATE_SCHED 비트가 설정되어 있어야 busy-poll이 NAPI를 접근할 수 있으며, 일반 인터럽트 기반 NAPI 스케줄링과 상호 배제(Mutual Exclusion)됩니다.

인터럽트 기반 수신 vs Busy Poll 수신 타임라인 인터럽트 기반 수신 시간 → 패킷 도착 HW IRQ softirq 대기 NAPI poll 소켓 전달 앱 수신 총 지연: ~20-100μs Busy Poll 수신 시간 → napi_busy_loop() 폴링 패킷 도착 직접 poll 앱 수신 총 지연: ~2-10μs CPU 사용 패턴 비교 인터럽트 기반 poll poll poll 낮은 CPU 사용 (유휴 구간 존재) Busy Poll napi_busy_loop() 연속 실행 100% CPU 점유 (연속 폴링)
인터럽트 기반 수신은 IRQ → softirq → NAPI 단계를 거치지만, busy poll은 애플리케이션이 직접 NAPI poll을 호출하여 지연을 최소화합니다.

드라이버 Busy Poll 지원 요구사항

현대 커널(v4.11+)에서 busy poll은 더 이상 별도의 ndo_busy_poll 콜백을 필요로 하지 않습니다. 대신 NAPI 기반 busy poll을 사용하며, 드라이버가 NAPI를 올바르게 구현하고 napi_id를 소켓에 연결하면 자동으로 지원됩니다.

드라이버의 busy poll 지원 핵심 요구사항은 다음과 같습니다.

/* 드라이버 busy poll 지원 확인 패턴 */
static int my_driver_rx_poll(struct napi_struct *napi, int budget)
{
    struct my_rx_ring *ring = container_of(napi, struct my_rx_ring, napi);
    int work_done = 0;

    while (work_done < budget) {
        struct sk_buff *skb = my_fetch_rx_packet(ring);
        if (!skb)
            break;

        /* 핵심: NAPI ID를 skb에 기록 → 소켓과 NAPI 연결 */
        skb_mark_napi_id(skb, napi);

        napi_gro_receive(napi, skb);
        work_done++;
    }

    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        /* 인터럽트 재활성화 */
        my_enable_rx_irq(ring);
    }

    return work_done;
}

/* 드라이버 초기화에서 NAPI 등록 */
static void my_driver_init_napi(struct my_priv *priv)
{
    /* netif_napi_add()가 napi_id를 자동 할당 */
    netif_napi_add(priv->netdev, &priv->rx_ring.napi,
                   my_driver_rx_poll);
    napi_enable(&priv->rx_ring.napi);
}

소켓 레벨 Busy Poll 설정

소켓 레벨에서 busy poll을 활성화하는 방법은 두 가지입니다. 시스템 전역 sysctl 설정과 소켓별 옵션 설정이며, 소켓별 설정이 우선합니다. epoll 기반 이벤트 루프(Event Loop)에서도 busy poll을 활용할 수 있습니다.

/* 애플리케이션 레벨 busy poll 설정 예시 */
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>

int setup_busy_poll_socket(int sockfd)
{
    int busy_poll_usec = 50;       /* 50μs busy poll 시간 */
    int prefer_busy_poll = 1;      /* busy poll 우선 모드 */
    int busy_budget = 8;           /* poll당 최대 처리 패킷 수 */

    /* SO_BUSY_POLL: 소켓별 busy poll 타임아웃 (μs) */
    setsockopt(sockfd, SOL_SOCKET, SO_BUSY_POLL,
               &busy_poll_usec, sizeof(busy_poll_usec));

    /* SO_PREFER_BUSY_POLL: 인터럽트 대신 busy poll 우선 */
    setsockopt(sockfd, SOL_SOCKET, SO_PREFER_BUSY_POLL,
               &prefer_busy_poll, sizeof(prefer_busy_poll));

    /* SO_BUSY_POLL_BUDGET: poll 1회당 처리 예산 */
    setsockopt(sockfd, SOL_SOCKET, SO_BUSY_POLL_BUDGET,
               &busy_budget, sizeof(busy_budget));

    return 0;
}

/* epoll 기반 busy poll 활용 */
int busy_poll_event_loop(int epfd, int sockfd)
{
    struct epoll_event events[64];

    setup_busy_poll_socket(sockfd);

    for (;;) {
        /* epoll_wait timeout=0 → busy poll이 데이터 폴링 */
        int n = epoll_wait(epfd, events, 64, 0);

        for (int i = 0; i < n; i++) {
            if (events[i].events & EPOLLIN) {
                /* 데이터 즉시 수신 가능 — 지연 최소화 */
                process_packet(events[i].data.fd);
            }
        }
    }
}
설정 방법범위파라미터설명
net.core.busy_poll시스템 전역μs 단위 타임아웃poll()/select() 시 기본 busy poll 시간
net.core.busy_read시스템 전역μs 단위 타임아웃read()/recv() 시 기본 busy poll 시간
SO_BUSY_POLL소켓별μs 단위 타임아웃소켓별 busy poll 타임아웃 (전역 설정 오버라이드)
SO_PREFER_BUSY_POLL소켓별0 또는 1인터럽트 대신 busy poll 우선 사용
SO_BUSY_POLL_BUDGET소켓별패킷 수busy poll 1회당 최대 처리 패킷

Busy Poll 성능 분석과 트레이드오프

Busy poll은 지연 시간을 극적으로 줄이지만 CPU 사용률이라는 대가를 치릅니다. 워크로드 특성에 따라 이 트레이드오프가 유리할 수도, 불리할 수도 있습니다.

수신 방식평균 지연P99 지연CPU 사용률적합 시나리오
인터럽트 기반 (기본)높음높음낮음 (유휴 가능)범용 서버, 배치 처리
인터럽트 + coalescing높음매우 높음매우 낮음고대역폭, CPU 절약
Busy Poll (SO_BUSY_POLL)낮음낮음높음 (코어 점유)초저지연 트레이딩, HFT
Busy Poll + prefer매우 낮음매우 낮음매우 높음 (100%)전용 코어 할당 가능 환경
XDP (커널 바이패스)매우 낮음매우 낮음중간L2/L3 패킷 처리, 방화벽(Firewall)
DPDK (완전 사용자 공간(User Space))매우 낮음매우 낮음매우 높음통신사 패킷 처리

Busy poll이 유리한 경우:

Busy poll이 불리한 경우:

# bpftrace로 busy poll 효과 측정
# napi_busy_loop 진입/종료 시간 측정
bpftrace -e '
kprobe:napi_busy_loop {
    @start[tid] = nsecs;
}
kretprobe:napi_busy_loop /@start[tid]/ {
    @busy_loop_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
'

# busy poll에서 실제 패킷을 처리한 비율 확인
bpftrace -e '
tracepoint:napi:napi_poll {
    @total++;
    if (args->work > 0) { @useful++; }
}
interval:s:5 {
    printf("유효 poll 비율: %d/%d (%d%%)\n",
           @useful, @total,
           @total ? @useful * 100 / @total : 0);
    clear(@total); clear(@useful);
}
'

# 지연 시간 비교: busy poll ON vs OFF
# 1) busy poll 비활성화 상태 측정
sysctl -w net.core.busy_poll=0
sockperf under-load -i 10.0.0.1 -p 12345 --mps=10000 -t 30

# 2) busy poll 활성화 상태 측정
sysctl -w net.core.busy_poll=50
sockperf under-load -i 10.0.0.1 -p 12345 --mps=10000 -t 30

QoS/DCB: mqprio, ETS, PFC 운영 포인트

데이터센터 환경에서는 대역폭 분배와 무손실 트래픽 제어(Traffic Control)가 중요합니다. 드라이버가 mqprio, DCB, PFC를 부분 지원하는 경우 지원 범위를 명확히 노출해야 운영 오해를 줄일 수 있습니다.

# 실습 예제: mqprio/ethtool로 큐 정책 검증
# mqprio qdisc 예시 (TC별 큐 매핑)
tc qdisc replace dev eth0 root mqprio num_tc 4 \
  map 0 1 2 3 3 3 3 3 \
  queues 1@0 1@1 2@2 4@4 hw 1

# DCB/PFC 상태 확인 예시 (환경별 도구 상이)
dcbtool gc eth0 dcb
ethtool --show-priv-flags eth0
항목드라이버 책임실패 시 증상
TC→queue 매핑qdisc 설정과 HW scheduler 동기화특정 클래스 starvation
PFCpriority별 pause on/off 적용drop 급증 또는 head-of-line blocking
ETSbandwidth share를 HW arbitration에 반영대역폭 분배 불일치

mqprio hw vs sw 모드

mqprio qdisc는 트래픽 클래스(Traffic Class, TC)별로 TX 큐를 매핑하는 멀티큐 우선순위 스케줄러(Scheduler)입니다. hw 0(소프트웨어 모드)과 hw 1(하드웨어 오프로드 모드)의 차이를 정확히 이해해야 올바른 QoS 정책을 구현할 수 있습니다.

hw 1 모드에서는 TC_MQPRIO_HW_OFFLOAD_TCS 플래그가 드라이버에 전달되며, 드라이버는 하드웨어 스케줄러에 TC-to-queue 매핑을 프로그래밍해야 합니다. 소프트웨어 모드에서는 커널이 큐 선택만 수행하고 실제 우선순위 처리는 하지 않습니다.

속성hw 0 (소프트웨어)hw 1 (하드웨어 오프로드)
큐 매핑커널이 TC→큐 매핑 수행드라이버가 HW scheduler에 프로그래밍
우선순위 보장없음 (단순 큐 분리)하드웨어 레벨 strict/WRR 지원
대역폭 제어불가ETS/rate limit 가능
드라이버 콜백불필요ndo_setup_tc 필수
성능 영향최소하드웨어 의존적
# mqprio 설정 및 검증 예시

# 1. 소프트웨어 모드: TC별 큐 분리만 수행
tc qdisc replace dev eth0 root mqprio \
    num_tc 4 \
    map 0 1 2 3 3 3 3 3 0 1 2 3 3 3 3 3 \
    queues 2@0 2@2 2@4 2@6 \
    hw 0

# 2. 하드웨어 오프로드 모드: NIC scheduler에 TC 매핑
tc qdisc replace dev eth0 root mqprio \
    num_tc 4 \
    map 0 1 2 3 3 3 3 3 0 1 2 3 3 3 3 3 \
    queues 2@0 2@2 2@4 2@6 \
    hw 1 \
    mode dcb

# 3. 채널(Channel) 모드: TC별 독립 qdisc 연결 가능
tc qdisc replace dev eth0 root mqprio \
    num_tc 3 \
    map 0 1 2 2 2 2 2 2 \
    queues 4@0 4@4 4@8 \
    hw 1 \
    mode channel \
    shaper bw_rlimit \
    min_rate 1Gbit 2Gbit 0 \
    max_rate 5Gbit 8Gbit 10Gbit

# 검증: TC 매핑 확인
tc qdisc show dev eth0
tc class show dev eth0

# 큐별 패킷 통계로 분배 확인
ethtool -S eth0 | grep -E "tx_queue_[0-9]+_packets"

PFC 데드락 방지와 워치독

우선순위 기반 흐름 제어(Priority Flow Control, PFC)는 IEEE 802.1Qbb 표준으로, 특정 우선순위의 트래픽에 대해서만 pause 프레임(Pause Frame)을 보내 무손실(Lossless) 전송을 보장합니다. 그러나 PFC 스톰(PFC Storm)이 발생하면 해당 우선순위의 트래픽이 완전히 차단되는 데드락(Deadlock) 상태에 빠질 수 있습니다.

PFC pause 프레임의 동작 원리는 다음과 같습니다.

PFC 스톰은 수신 측이 지속적으로 pause 프레임을 보내는 상태로, 네트워크 전체로 전파(Head-of-Line Blocking)될 수 있습니다. 이를 감지하고 자동 복구하는 워치독(Watchdog) 메커니즘이 필요합니다.

# PFC 설정 및 모니터링

# 1. PFC 활성화 (priority 3, 4에 대해)
mlnx_qos -i eth0 --pfc 0,0,0,1,1,0,0,0

# lldptool을 사용한 PFC 설정 (lldpad 사용 시)
lldptool -T -i eth0 -V PFC enabled=0,0,0,1,1,0,0,0

# 2. PFC 카운터 모니터링
ethtool -S eth0 | grep -iE "pfc|pause"
# 주요 카운터:
# rx_pfc_pri_N_pause   - 수신한 PFC pause 프레임 수
# tx_pfc_pri_N_pause   - 송신한 PFC pause 프레임 수
# rx_pfc_pri_N_duration - pause 지속 시간 (quanta)

# 3. PFC 워치독 상태 확인 (드라이버 지원 시)
devlink health show pci/0000:03:00.0 reporter tx

# 4. PFC 스톰 감지 스크립트
PREV_PAUSE=0
while true; do
    CURR_PAUSE=$(ethtool -S eth0 | grep rx_pfc_pri_3_pause | awk '{print $2}')
    RATE=$((CURR_PAUSE - PREV_PAUSE))
    if [ $RATE -gt 1000 ]; then
        echo "[경고] PFC 스톰 의심: priority 3, rate=$RATE/sec"
        # 자동 대응: PFC 비활성화 또는 관리자 알림
    fi
    PREV_PAUSE=$CURR_PAUSE
    sleep 1
done

CBS/TAS IEEE 802.1Qav/Qbv 오프로드

시간 민감 네트워킹(Time-Sensitive Networking, TSN)에서는 CBS(Credit Based Shaper, IEEE 802.1Qav)와 TAS(Time-Aware Shaper, IEEE 802.1Qbv)가 핵심 트래픽 제어 메커니즘입니다. 리눅스 커널은 cbstaprio qdisc를 통해 이들을 지원하며, 하드웨어 오프로드가 가능한 NIC에서는 정밀한 타이밍 제어가 가능합니다.

# TSN 설정 예시

# 1. CBS qdisc 설정 (802.1Qav)
# TC 0에 대해 idleSlope=100Mbit, sendSlope=-900Mbit (1Gbit 링크 기준)
tc qdisc replace dev eth0 parent root handle 100 mqprio \
    num_tc 3 map 2 2 1 0 2 2 2 2 \
    queues 1@0 1@1 2@2 hw 0

tc qdisc replace dev eth0 parent 100:1 cbs \
    idleslope 100000 sendslope -900000 \
    hicredit 12 locredit -88 offload 1

# 2. taprio qdisc 설정 (802.1Qbv)
# 1ms 주기: TC0 200us, TC1 300us, TC2 500us
tc qdisc replace dev eth0 parent root taprio \
    num_tc 3 \
    map 2 2 1 0 2 2 2 2 2 2 2 2 2 2 2 2 \
    queues 1@0 1@1 2@2 \
    base-time 1000000000 \
    sched-entry S 01 200000 \
    sched-entry S 02 300000 \
    sched-entry S 04 500000 \
    flags 0x2 \
    clockid CLOCK_TAI

# 검증: taprio 스케줄 확인
tc qdisc show dev eth0 root

# PTP 클럭 동기화 확인 (TSN 필수 조건)
ptp4l -i eth0 -m &
phc2sys -s eth0 -c CLOCK_REALTIME -w -m &

TSN 기능별 드라이버 지원 현황은 다음과 같습니다.

TSN 기능qdiscigc (Intel i225)stmmac (Intel EHL)enetc (NXP)am65 (TI)
CBS (802.1Qav)cbsHW 오프로드HW 오프로드HW 오프로드HW 오프로드
TAS (802.1Qbv)taprioHW 오프로드HW 오프로드HW 오프로드HW 오프로드
Frame Preemption (802.1Qbu)ethtool지원지원미지원미지원
PTP (802.1AS)ptp4lHW 타임스탬프HW 타임스탬프HW 타임스탬프HW 타임스탬프
Launch Time (ETF)etf지원지원지원미지원

PREEMPT_RT와 NAPI threaded 모드

실시간 커널에서는 IRQ/softirq 모델이 일반 커널과 다르게 동작합니다. 드라이버는 spinlock 길이를 줄이고, napi poll 지연 상한을 보장하도록 설계해야 합니다.

NAPI threaded 모드 내부 구현

커널 5.12부터 도입된 NAPI 스레드 모드(Threaded Mode)는 softirq 컨텍스트 대신 전용 커널 스레드에서 NAPI poll을 실행합니다. 이를 통해 cgroup 기반 CPU 제어, 우선순위 설정, CPU 친화성(Affinity) 지정이 가능해지며, PREEMPT_RT 환경에서 특히 유용합니다.

dev_set_threaded() API를 호출하면 해당 net_device에 등록된 모든 NAPI 인스턴스에 대해 전용 스레드가 생성됩니다. 내부적으로 NAPI_STATE_THREADED 플래그가 설정되며, 이후 napi_schedule()은 softirq 대신 해당 스레드를 깨웁니다.

/* NAPI threaded 모드 활성화 - 드라이버 코드 예시 */

/* probe 함수에서 threaded 모드 활성화 */
static int my_probe(struct pci_dev *pdev,
                    const struct pci_device_id *id)
{
    struct net_device *ndev;
    struct my_priv *priv;
    int err;

    /* ... 기본 초기화 ... */

    /* NAPI 등록 */
    for (int i = 0; i < priv->num_queues; i++) {
        netif_napi_add(ndev, &priv->queues[i].napi,
                       my_poll);
    }

    /* threaded NAPI 활성화
     * 커널이 napi-N 형태의 kthread를 자동 생성합니다
     * 예: napi/eth0-0, napi/eth0-1, ... */
    err = dev_set_threaded(ndev, true);
    if (err)
        netdev_warn(ndev,
            "threaded NAPI 활성화 실패: %d\n", err);

    /* ... 나머지 초기화 ... */
    return 0;
}

/* napi_threaded_poll() 커널 소스 분석 (net/core/dev.c)
 *
 * static int napi_threaded_poll(void *data)
 * {
 *     struct napi_struct *napi = data;
 *
 *     while (!kthread_should_stop()) {
 *         // NAPI_STATE_SCHED 플래그 대기
 *         set_current_state(TASK_INTERRUPTIBLE);
 *         while (!test_bit(NAPI_STATE_SCHED, &napi->state))
 *             schedule();
 *         set_current_state(TASK_RUNNING);
 *
 *         // poll 실행 (budget = netdev_budget)
 *         napi_threaded_poll_loop(napi);
 *     }
 * }
 */
# sysfs를 통한 NAPI threaded 모드 제어

# 현재 상태 확인
cat /sys/class/net/eth0/threaded
# 1 = threaded 모드 활성, 0 = softirq 모드

# 활성화
echo 1 > /sys/class/net/eth0/threaded

# NAPI 스레드 확인
ps -eo pid,cls,pri,ni,comm | grep napi

# NAPI 스레드 CPU 친화성 설정
# 예: 큐 0은 CPU 2에, 큐 1은 CPU 3에 고정
for pid in $(pgrep -f "napi/eth0"); do
    echo "PID $pid: $(cat /proc/$pid/comm)"
    taskset -pc $pid
done

taskset -pc 2 $(pgrep -f "napi/eth0-0")
taskset -pc 3 $(pgrep -f "napi/eth0-1")

# RT 우선순위 설정 (SCHED_FIFO)
chrt -f -p 50 $(pgrep -f "napi/eth0-0")

PREEMPT_RT에서의 네트워크 스택 동작

PREEMPT_RT 패치(Patch)가 적용된 커널에서는 네트워크 스택의 동작이 크게 달라집니다. 모든 인터럽트가 스레드화(Forced Threading)되고, spinlock이 슬리핑 뮤텍스(Mutex)로 변환되며, local_bh_disable()이 뮤텍스 기반 보호로 변경됩니다. 이러한 변화가 드라이버에 미치는 영향을 이해해야 합니다.

항목일반 커널PREEMPT_RT 커널드라이버 영향
하드 IRQ인터럽트 컨텍스트에서 실행스레드에서 실행 (강제 스레딩)IRQ 핸들러에서 preemption 가능
softirqksoftirqd 또는 IRQ 반환 시 실행전용 스레드 (rcuc/N 등)NAPI poll 지연 증가 가능
spinlockbusy-wait, preemption 비활성rt_mutex (슬리핑 가능)우선순위 역전(Priority Inversion) 해소
local_bh_disablesoftirq 비활성화per-CPU 뮤텍스 획득경합 시 슬리핑 가능
raw_spinlockspinlock과 동일진짜 spinlock (busy-wait 유지)극히 짧은 임계 구간에만 사용
timer softirqsoftirq에서 처리전용 스레드타이머 콜백 지연 가능
/* PREEMPT_RT 안전한 드라이버 코드 패턴 */

/* 1. raw_spinlock: 진짜 인터럽트 비활성화가 필요한 최소 구간 */
struct my_priv {
    raw_spinlock_t  irq_lock;     /* HW 레지스터 접근 보호 */
    spinlock_t      config_lock;  /* 설정 변경 보호 (RT에서 mutex) */
};

/* IRQ 핸들러: raw_spinlock 사용 (최소한의 작업만) */
static irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_priv *priv = data;
    u32 status;

    raw_spin_lock(&priv->irq_lock);

    status = my_read_isr(priv);
    if (!status) {
        raw_spin_unlock(&priv->irq_lock);
        return IRQ_NONE;
    }

    /* 인터럽트 비활성화하고 NAPI 스케줄 */
    my_disable_irq(priv);
    raw_spin_unlock(&priv->irq_lock);

    napi_schedule_irqoff(&priv->napi);
    return IRQ_HANDLED;
}

/* 2. 설정 경로: 일반 spinlock (RT에서 mutex로 변환) */
static int my_set_features(struct net_device *ndev,
                           netdev_features_t features)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* RT 커널에서 이 lock은 sleeping mutex로 동작
     * → 우선순위 상속(Priority Inheritance) 지원 */
    spin_lock(&priv->config_lock);
    my_apply_features(priv, features);
    spin_unlock(&priv->config_lock);

    return 0;
}

실시간 네트워크 지연 측정

실시간 네트워크 시스템의 성능은 평균 처리량이 아니라 최악의 경우 지연으로 평가합니다. cyclictest, oslat, hwlatdetect 등의 도구를 네트워크 부하와 함께 실행하여 실제 운영 환경에서의 꼬리 지연을 측정해야 합니다.

# RT 네트워크 지연 종합 테스트 스크립트
#!/bin/bash

DURATION=300    # 5분 테스트
RT_PRIO=80
ISOL_CPUS="2,3"  # 격리된 CPU
NET_IF="eth0"
RESULTS_DIR="/tmp/rt-net-test-$(date +%Y%m%d_%H%M%S)"
mkdir -p $RESULTS_DIR

echo "=== RT 네트워크 지연 테스트 시작 ==="
echo "기간: ${DURATION}초, RT 우선순위: $RT_PRIO, 격리 CPU: $ISOL_CPUS"

# 1. 하드웨어 지연 감지 (SMI 등)
echo "--- hwlatdetect (60초) ---"
hwlatdetect --duration=60 --threshold=10 | tee $RESULTS_DIR/hwlat.log

# 2. 네트워크 부하 생성 (백그라운드)
echo "--- 네트워크 부하 생성 ---"
iperf3 -c 10.0.0.2 -t $DURATION -P 4 --bind-dev $NET_IF &
IPERF_PID=$!

# 3. cyclictest 실행 (격리 CPU에서)
echo "--- cyclictest 실행 ---"
cyclictest \
    --mlockall \
    --smp \
    --priority=$RT_PRIO \
    --interval=1000 \
    --distance=0 \
    --duration=$DURATION \
    --affinity=$ISOL_CPUS \
    --histofall=1000 \
    --histfile=$RESULTS_DIR/cyclictest-hist.txt \
    | tee $RESULTS_DIR/cyclictest.log &
CYCLIC_PID=$!

# 4. oslat 실행 (별도 CPU)
echo "--- oslat 실행 ---"
oslat \
    --duration $DURATION \
    --rtprio $RT_PRIO \
    --cpu-list $ISOL_CPUS \
    | tee $RESULTS_DIR/oslat.log &
OSLAT_PID=$!

# 5. 네트워크 RTT 측정
echo "--- ping RTT 측정 ---"
ping -i 0.01 -c $((DURATION * 100)) -D 10.0.0.2 \
    | tee $RESULTS_DIR/ping.log &
PING_PID=$!

# 대기 및 결과 수집
wait $CYCLIC_PID $OSLAT_PID $PING_PID
kill $IPERF_PID 2>/dev/null

# 6. 결과 요약
echo "\n=== 결과 요약 ==="
echo "--- cyclictest 최대 지연 ---"
grep -E "Max|Avg" $RESULTS_DIR/cyclictest.log

echo "--- ping 통계 ---"
tail -3 $RESULTS_DIR/ping.log

echo "결과 디렉터리: $RESULTS_DIR"