Network Device 드라이버 (net_device)

Linux 네트워크 디바이스 드라이버를 고처리량 데이터 경로와 운영 안정성 관점에서 심층 정리합니다. net_device/net_device_ops 초기화, NAPI 기반 RX 폴링, TX 큐 관리와 BQL, MSI-X/IRQ affinity 최적화, checksum/TSO/GRO 등 오프로드 기능, XDP/AF_XDP 연계, ethtool 통계와 링크 상태 관리, 물리 NIC와 TUN/TAP 같은 가상 netdev 공통 모델, tracepoint/perf/bpftrace를 활용한 병목 분석까지 실전 네트워크 드라이버 개발에 필요한 핵심을 다룹니다.

전제 조건: 디바이스 드라이버Workqueue 문서를 먼저 읽으세요. 입출력 인터페이스 드라이버는 데이터 경로와 제어 경로를 동시에 다루므로 큐/버퍼/비동기 처리 경계를 먼저 구분해야 합니다.
일상 비유: 이 주제는 콜센터 접수와 처리 라인 분리와 비슷합니다. 요청 접수와 실제 처리를 분리해 병목을 줄이듯이, 드라이버도 IRQ·큐·작업 스레드를 역할별로 나눠야 안정적입니다.

핵심 요약

  • net_device — 인터페이스의 공통 상태와 콜백 진입점입니다.
  • net_device_ops — open/stop/xmit 등 데이터 경로 계약을 정의합니다.
  • NAPI — RX 인터럽트 폭풍을 줄이고 폴링 기반 처리량을 확보합니다.
  • BQL — TX 큐 지연(latency)과 버퍼블로트 리스크를 줄입니다.
  • 가상 netdev — TUN/TAP처럼 하드웨어 없이도 동일한 netdev 모델을 재사용합니다.

단계별 이해

  1. 수명주기 설계
    할당/등록/해제 순서를 먼저 확정합니다.
  2. RX/TX 콜백 구현
    ndo_start_xmit()와 NAPI poll 루프를 정확히 연결합니다.
  3. 운영 인터페이스 연결
    ethtool_ops, 통계, 링크 상태(phylink)를 연결합니다.
  4. 가상 netdev 확장
    TUN/TAP, veth, virtio-net과 공통 패턴을 통합해 이해합니다.
예제 읽기 가이드: 이 문서는 개념 중심 설명을 기본으로 하되, 운영 환경에서 바로 점검할 수 있는 실습 예제를 함께 제공합니다. 코드 주석에 개념 예시가 표시된 블록은 구조와 호출 계약 이해용이며, 실습 예제가 표시된 블록은 사용자 공간에서 실행/검증 절차를 바로 적용할 수 있도록 구성했습니다.

개요: net_device 드라이버의 역할

struct net_device 드라이버는 커널 네트워크 스택과 실제/가상 링크 계층 사이의 어댑터입니다. 유저스페이스 입장에서는 eth0, ens3, tap0 모두 동일한 netdev 인터페이스처럼 보이지만, 내부 구현은 물리 NIC/가상 디바이스에 따라 크게 달라집니다.

User Space socket / ip / tc Network Stack sk_buff / routing net_device Driver net_device_ops + napi_struct + ethtool_ops 물리 NIC + 가상 netdev 공통 모델 PCIe NIC DMA/Ring Virtual netdev TUN/TAP, veth
net_device 모델은 물리 NIC와 가상 인터페이스를 동일한 커널 데이터 경로에 연결합니다.

드라이버 수명주기와 필수 호출 순서

가장 흔한 실수는 등록/해제 순서를 뒤섞는 것입니다. 특히 NAPI, IRQ, queue start/stop 순서는 패킷 손실과 use-after-free를 바로 유발합니다.

/* 개념 예시: net_device 수명주기와 등록 순서 */
#include <linux/netdevice.h>
#include <linux/etherdevice.h>

struct my_priv {
    struct net_device *ndev;
    struct napi_struct napi;
    spinlock_t tx_lock;
    void __iomem *bar0;
    int irq;
};

static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *ndev;
    struct my_priv *priv;
    int ret;

    ndev = alloc_etherdev_mqs(sizeof(*priv), 8, 8);
    if (!ndev)
        return -ENOMEM;

    priv = netdev_priv(ndev);
    priv->ndev = ndev;
    spin_lock_init(&priv->tx_lock);

    netif_napi_add(ndev, &priv->napi, my_napi_poll);
    ndev->netdev_ops = &my_netdev_ops;
    ndev->ethtool_ops = &my_ethtool_ops;

    ret = register_netdev(ndev);
    if (ret) {
        netif_napi_del(&priv->napi);
        free_netdev(ndev);
        return ret;
    }

    return 0;
}

static void my_remove(struct pci_dev *pdev)
{
    struct net_device *ndev = pci_get_drvdata(pdev);
    struct my_priv *priv = netdev_priv(ndev);

    unregister_netdev(ndev);
    netif_napi_del(&priv->napi);
    free_netdev(ndev);
}
실수 방지: free_netdev()는 반드시 unregister_netdev() 이후에 호출하세요. 등록된 netdev를 먼저 해제하면 notifier/RCU 경로에서 즉시 use-after-free가 발생할 수 있습니다.

핵심 콜백: net_device_ops 계약

net_device_ops는 드라이버와 코어 네트워크 스택 간의 ABI 역할을 합니다. 모든 콜백을 구현할 필요는 없지만, open/stop/xmit/statistics의 책임 분리는 명확해야 합니다.

콜백호출 시점핵심 책임
ndo_openip link set upIRQ/NAPI 활성화, RX/TX queue 시작
ndo_stopip link set downqueue 정지, IRQ 비활성화, NAPI 비활성화
ndo_start_xmit송신 경로skb를 TX ring에 게시하고 doorbell 트리거
ndo_get_stats64통계 조회race-safe한 64-bit 통계 제공
ndo_set_featuresoffload 변경TSO/GRO checksum offload 토글 처리
/* 개념 예시: open/stop에서 NAPI-IRQ 순서 보장 */
static int my_ndo_open(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    my_hw_rx_ring_init(priv);
    my_hw_tx_ring_init(priv);

    napi_enable(&priv->napi);
    my_enable_irq(priv);
    netif_tx_start_all_queues(ndev);
    return 0;
}

static int my_ndo_stop(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    netif_tx_disable(ndev);
    my_disable_irq(priv);
    napi_disable(&priv->napi);
    my_hw_rx_ring_cleanup(priv);
    my_hw_tx_ring_cleanup(priv);
    return 0;
}

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를 크게 좌우합니다.

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);
}

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

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

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

NIC의 TX ring이 크거나 드라이버가 큐 깊이를 제한하지 않으면, 상위 계층(qdisc, TCP 혼잡 제어)이 내린 결정과 무관하게 수백 ms에서 수 초 분량의 패킷이 하드웨어 큐에 쌓일 수 있습니다. 이 "버퍼블로트"는 지연(latency)을 극적으로 증가시키면서 처리량(throughput)은 거의 높이지 않습니다. 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을 측정하여 공정한 스케줄링이 가능합니다.

실전 디버깅과 모니터링

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() 동시 호출에 대한 직렬화/경합 제어를 드라이버가 스스로 보장해야 하며, 큐 stop/wake, timeout 복구, completion 경로까지 하나의 동시성 계약으로 맞춰야 합니다.

핵심 경고: LLTX는 신규 드라이버의 기본 선택지가 아닙니다. 멀티큐 + queue별 락/BQL 모델이 이미 충분히 고성능이며, LLTX는 lock inversion, queue state 경합, watchdog 오탐(false timeout) 같은 장애 확률을 높입니다.
항목일반 TX 경로LLTX 경로
직렬화 주체코어/큐 락 + 드라이버 보조 락드라이버가 전적으로 책임
병목 위치락 경합은 비교적 예측 가능드라이버 구현 품질에 따라 편차 큼
디버깅 난이도표준 패턴과 도구가 많음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 계측을 적용한 뒤 성능/지연을 재측정하는 접근이 안전합니다.

통계와 ethtool 연동

운영 환경에서는 “성능이 안 나온다”보다 “왜 안 나오는가”를 보여주는 통계가 더 중요합니다. ethtool -S로 확인 가능한 드라이버 통계를 설계하면 장애 분석 시간이 크게 줄어듭니다.

/* 개념 예시: ethtool 통계 구조와 per-CPU 집계 */
struct my_pcpu_stats {
    u64 rx_packets;
    u64 rx_bytes;
    u64 tx_packets;
    u64 tx_bytes;
    struct u64_stats_sync syncp;
};

static void my_ndo_get_stats64(struct net_device *ndev,
                               struct rtnl_link_stats64 *stats)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        struct my_pcpu_stats *pcpu = per_cpu_ptr(my_stats, cpu);
        u64 rx_pkts, rx_bytes, tx_pkts, tx_bytes;
        unsigned int start;

        do {
            start = u64_stats_fetch_begin(&pcpu->syncp);
            rx_pkts = pcpu->rx_packets;
            rx_bytes = pcpu->rx_bytes;
            tx_pkts = pcpu->tx_packets;
            tx_bytes = pcpu->tx_bytes;
        } while (u64_stats_fetch_retry(&pcpu->syncp, start));

        stats->rx_packets += rx_pkts;
        stats->rx_bytes += rx_bytes;
        stats->tx_packets += tx_pkts;
        stats->tx_bytes += tx_bytes;
    }
}
진단 명령확인 포인트
ethtool -i eth0드라이버/펌웨어 버전
ethtool -k eth0TSO/GRO/checksum offload 상태
ethtool -S eth0링 드롭, 에러, 큐별 카운터
ethtool -l eth0채널(RX/TX queue) 구성
ip -s link show dev eth0커널 링크 통계의 상위 뷰

현대 NIC/MAC 드라이버는 PHY 연결을 phylink로 통합하는 추세입니다. SFP, fixed-link, in-band status를 동시에 다뤄야 하는 경우 phylink가 사실상 표준입니다.

/* 개념 예시: phylink 초기화와 플랫폼별 연결 분기 */
static const struct phylink_mac_ops my_phylink_ops = {
    .mac_config = my_mac_config,
    .mac_link_up = my_mac_link_up,
    .mac_link_down = my_mac_link_down,
};

static int my_phylink_init(struct my_priv *priv)
{
    struct phylink_config *cfg = &priv->phylink_config;
    struct fwnode_handle *fwnode = dev_fwnode(priv->dev);
    phy_interface_t iface = priv->phy_mode; /* DT/ACPI 설정에서 파생 */

    priv->phylink = phylink_create(cfg, fwnode, iface,
                                 &my_phylink_ops);
    if (IS_ERR(priv->phylink))
        return PTR_ERR(priv->phylink);

    /* 펌웨어 타입(OF/fwnode)에 맞는 connect 경로를 선택 */
    if (is_of_node(fwnode))
        return phylink_of_phy_connect(priv->phylink, to_of_node(fwnode), 0);
    return phylink_fwnode_phy_connect(priv->phylink, fwnode, 0);
}

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 문서에서 이어서 확인하세요.

멀티큐, RSS, IRQ affinity 설계

10/25/100GbE 구간에서는 단일 큐 모델이 거의 항상 병목입니다. 드라이버는 RX/TX 큐, MSI-X vector, NAPI 인스턴스를 1:1 또는 N:1로 설계하고, NUMA/CPU 토폴로지에 맞춰 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 부족 시 보조적으로 사용

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/메모리 누수가 쉽게 발생합니다.

제어 경로: RTNL, RTNETLINK, feature 토글

데이터 경로가 빠르더라도 제어 경로가 불안정하면 운영 장애가 반복됩니다. MTU 변경, queue 개수 변경, 링크 down/up, offload 토글은 모두 RTNL 보호 하에서 일관되게 처리해야 합니다.

/* 개념 예시: MTU 변경 시 stop/open 오류 경로 보강 */
static int my_ndo_change_mtu(struct net_device *ndev, int new_mtu)
{
    int ret;
    int old_mtu = ndev->mtu;

    if (new_mtu < 68 || new_mtu > 9700)
        return -EINVAL;

    if (netif_running(ndev)) {
        ret = my_ndo_stop(ndev);
        if (ret)
            return ret;

        ndev->mtu = new_mtu;
        ret = my_ndo_open(ndev);
        if (ret) {
            ndev->mtu = old_mtu;
            return ret;
        }
        return 0;
    }

    ndev->mtu = new_mtu;
    return 0;
}

static int my_ndo_set_features(struct net_device *ndev, netdev_features_t features)
{
    netdev_features_t changed = ndev->features ^ features;

    if (changed & NETIF_F_TSO)
        my_hw_toggle_tso(ndev, !!(features & NETIF_F_TSO));
    if (changed & NETIF_F_GRO)
        my_hw_toggle_gro(ndev, !!(features & NETIF_F_GRO));

    return 0;
}

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 일관성
암호화/터널xfrm offload, UDP tunnel offloadfallback 경로와 통계 구분

리셋/장애 복구: devlink health와 watchdog

실서비스에서는 드라이버의 평균 성능보다 복구 전략이 더 중요합니다. TX timeout, 펌웨어 hang, PCI AER 오류에서 자동 복구가 되지 않으면 장기 장애로 이어집니다.

/* 개념 예시: tx_timeout 복구를 workqueue로 분리 */
static void my_tx_timeout(struct net_device *ndev, unsigned int txqueue)
{
    struct my_priv *priv = netdev_priv(ndev);

    netdev_warn(ndev, "tx timeout on queue %u\\n", txqueue);
    schedule_work(&priv->reset_work);
}

static void my_reset_work(struct work_struct *work)
{
    struct my_priv *priv = container_of(work, struct my_priv, reset_work);

    rtnl_lock();
    my_ndo_stop(priv->ndev);
    my_hw_function_reset(priv);
    my_ndo_open(priv->ndev);
    rtnl_unlock();
}

오프로드 계약: 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와 통계 동기화

PTP 하드웨어 타임스탬프와 시간 동기화

금융/통신/분산 DB 워크로드에서는 네트워크 성능만큼 시간 정확도가 중요합니다. PTP 지원 NIC 드라이버는 SIOCSHWTSTAMP 설정, TX timestamp completion, PHC 노출을 안정적으로 제공해야 합니다.

/* 개념 예시: HW timestamp 사용자 요청 검증 */
static int my_hwtstamp_set(struct net_device *ndev, struct ifreq *ifr)
{
    struct hwtstamp_config cfg;

    if (copy_from_user(&cfg, ifr->ifr_data, sizeof(cfg)))
        return -EFAULT;

    if (cfg.tx_type != HWTSTAMP_TX_OFF && cfg.tx_type != HWTSTAMP_TX_ON)
        return -ERANGE;

    my_hw_config_timestamp(ndev, &cfg);

    if (copy_to_user(ifr->ifr_data, &cfg, sizeof(cfg)))
        return -EFAULT;

    return 0;
}

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 간 상태 동기화가 어긋나면 패킷 블랙홀이나 정책 누락이 발생합니다.

검증 매트릭스: 릴리스 전 체크 항목

영역필수 검증합격 기준 예시
기능up/down, MTU, VLAN, bridge, bond10k회 반복 시 누수/lockup 없음
성능단일/다중 스트림, 작은 패킷/점보 프레임목표 PPS/Gbps 달성, drop rate 임계 이내
안정성link flap, reset storm, hotplug, suspend/resume자동 복구 성공, 수동 재로드 불필요
가시성ethtool/stat/devlink health dump장애 원인 식별 가능한 텔레메트리 제공
보안XDP/tc rule 경계값, malformed packetcrash 없이 drop/에러 처리

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 메모리 배리어

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

Doorbell이란?

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

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 비트맵이벤트 비트비트마스크
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를 분리하는 편이 안전합니다.

회귀 테스트: packetdrill, kselftest, fault injection

netdev 드라이버는 환경 의존성이 커서 재현 테스트가 어렵습니다. 릴리스 전에 최소 회귀 시나리오를 자동화하면 “간헐적 링크 다운” 같은 문제를 조기에 차단할 수 있습니다.

도구테스트 대상예시
kselftest (net)기능 회귀MTU/VLAN/GRO/GSO 기본 동작
packetdrill프로토콜 타이밍/에러 경로재전송, out-of-order, checksum 에러
tc + iperf3성능/큐 안정성장시간 부하 중 drop/timeout 감시
fault injection복구 경로DMA map 실패, TX timeout, reset 반복

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

데이터센터 환경에서는 대역폭 분배와 무손실 트래픽 제어가 중요합니다. 드라이버가 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에 반영대역폭 분배 불일치

PREEMPT_RT와 NAPI threaded 모드

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

Netpoll/kdump 경로 지원

패닉 상황에서 네트워크 로그 덤프가 필요하면 netpoll/netconsole 경로가 사용됩니다. 일반 데이터 경로와 독립된 최소 송신 경로를 유지해야 crash dump 신뢰성이 올라갑니다.

/* 개념 예시: netpoll 경로의 재진입/잠금 제약 */
static void my_netpoll_send_skb(struct net_device *ndev, struct sk_buff *skb)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* 최소 TX 경로: sleep 금지, 동적 메모리 할당 최소화 */
    if (!my_tx_ring_has_space(priv)) {
        dev_kfree_skb_any(skb);
        return;
    }
    my_map_skb_to_tx_desc(priv, skb);
    my_ring_doorbell(priv);
}
운영 주의: kdump 캡처 커널에서 동일 NIC 드라이버가 정상 동작하는지 별도 검증이 필요합니다. 본 커널에서 정상이어도 캡처 커널 initramfs/펌웨어 누락으로 전송 실패가 발생할 수 있습니다.

보안 하드닝: 입력 검증과 경계 조건

네트워크 드라이버 취약점은 원격 트리거 가능성이 있습니다. 길이 검증, ring index 범위 확인, DMA 주소 검증은 성능 최적화보다 우선되어야 합니다.

취약 패턴점검 포인트방어 전략
RX length 신뢰HW가 넘긴 length를 그대로 사용최소/최대 길이, headroom 검증
ring index overflowproducer/consumer wrap 처리 누락mask 기반 인덱싱 + assert
UAF on resetreset 중 skb/page 소유권 경합state machine + refcount 엄격화
ioctl/netlink 입력 검증 부족사용자 파라미터 경계값 누락range check, capability check

가상 netdev: TUN/TAP, veth, virtio-net

가상 인터페이스도 본질적으로는 net_device입니다. 차이는 “패킷을 어디로 내보내는가”에 있습니다. 물리 NIC는 DMA 링으로, TUN/TAP은 파일 디스크립터로, veth는 peer netdev로 전달합니다.

Kernel Network Stack ip_rcv / dev_queue_xmit 공통 sk_buff 경로 Physical NIC Driver RX/TX Ring + DMA e1000e/ixgbe/mlx5 TUN/TAP Driver /dev/net/tun read/write 유저스페이스 VPN/QEMU veth / virtio-net peer queue / virtqueue 패킷 종착지 하드웨어 링크 유저스페이스 프로세스 네임스페이스 peer 가상머신 virtio backend 모두 net_device 추상화 재사용
물리/가상 인터페이스는 전송 매체만 다를 뿐 동일한 netdev 운영 모델을 공유합니다.
/* 개념 예시: drivers/net/tun.c 핵심 경로 요약 */
static int tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);

    if (!ptr_ring_produce(&tun->tx_ring, skb))
        return NETDEV_TX_OK;

    /* 유저스페이스가 fd read()로 수신 */
    dev_kfree_skb_any(skb);
    return NETDEV_TX_OK;
}

동기화, RTNL, 메모리 모델

netdev 코드의 동기화는 단일 락으로 끝나지 않습니다. 설정 경로는 RTNL, 데이터 경로는 per-queue spinlock/NAPI, 통계 경로는 u64_stats_sync를 조합합니다.

RTNL per-namespace 락 (v6.13+): 커널 6.13에서 rtnl_lockper-network namespace 단위로 세분화되었습니다. 기존에는 전체 네트워크 네임스페이스가 단일 글로벌 RTNL 뮤텍스를 공유하여, 컨테이너 수천 개를 운영하는 환경에서 제어 경로 병목이 발생했습니다. per-namespace RTNL 락을 통해 서로 다른 네임스페이스의 링크 설정 작업이 병렬로 수행되어 경합이 크게 감소합니다.

디버깅 체크리스트와 실전 트러블슈팅

증상의심 지점확인 방법
TX 멈춤queue wake 누락, completion path 손상ethtool -S, netif_tx_queue_stopped() 추적
RX drop 급증NAPI budget 과소, ring refill 지연/proc/net/softnet_stat, RX no-buffer 카운터
링크 flapPHY state machine/interrupt stormdmesg, phylink tracepoint
고부하에서 패킷 손실IRQ affinity/NUMA 불일치/proc/interrupts, ethtool -x/-X
XDP 적용 후 비정상page_pool recycle/XDP verdict 처리 오류bpftool prog, drop reason trace
# 실습 예제: 큐/오프로드/통계 빠른 점검
# 큐/오프로드/통계 빠른 점검
ethtool -i eth0
ethtool -k eth0
ethtool -l eth0
ethtool -S eth0 | grep -E "drop|error|timeout|busy"

# 소프트넷 병목 확인
cat /proc/net/softnet_stat

# netdev 관련 tracepoint 예시
trace-cmd record -e net -e napi -e skb

운영 관측: 필수 대시보드 지표

드라이버 품질은 장애가 났을 때 “원인을 빠르게 찾을 수 있는가”로 평가됩니다. 아래 지표를 대시보드로 상시 수집하면 회귀를 조기에 감지할 수 있습니다.

지표 그룹필수 항목경보 조건 예시
링크 상태link up/down flap 횟수, 속도/duplex 변경10분 내 flap 3회 이상
RX/TX 에러crc/frame/rx_missed/tx_timeout분당 에러 증가율 급등
큐 불균형queue별 packet/byte 편차, backlog상위 큐 편중 70% 초과
복구 이벤트reset 횟수, devlink health recover 횟수하루 1회 이상 자동 리셋
지연 품질p99/p999 RTT, drop reason 통계tail latency 임계 초과

Bring-up 30분 체크리스트

  1. 장치 인식 확인
    lspci -nn, dmesg | grep -i <driver>로 probe 성공 여부 확인
  2. 링크 기본 동작
    ip link set dev eth0 up, ethtool eth0로 speed/duplex/link 검증
  3. 기본 송수신
    ping, 단일 iperf3로 RX/TX 모두 정상 동작 확인
  4. 오프로드/큐 설정
    ethtool -k/-l/-x 확인 후 장비 정책에 맞게 조정
  5. 에러 카운터 스냅샷
    ethtool -S eth0 초기값 저장, 10분 부하 후 증분 비교
  6. 리셋 복구
    의도적 link flap 또는 함수 리셋 후 자동 복구 성공 여부 확인
  7. 관측/알람 연계
    링크/에러/reset 지표가 모니터링 시스템에 수집되는지 검증

코드 리뷰 체크리스트 (net_device 전용)

제조사별 NIC 드라이버 상세 매트릭스

같은 net_device 모델이라도 벤더별로 펌웨어 의존성, 오프로드 범위, reset 전략이 다릅니다. 운영 환경에서는 “벤더별 특성”을 분리해서 튜닝/장애 대응해야 재현성이 올라갑니다.

Intel: e1000e / igb / ixgbe / i40e / ice / idpf

드라이버주요 세대/용도핵심 포인트자주 보는 이슈
e1000e1GbE 서버/임베디드안정성 우선, 기능 단순링크 flap, 절전 전환 후 wake 지연
igb1GbE 멀티큐RSS/TSO 기본, SR-IOV 일부 모델큐 불균형, IRQ affinity 미스매치
ixgbe10GbE(82599/X540)Flow Director, DCB, XDP 일부 경로tx timeout, FDIR rule 관리 복잡도
i40eXL710/X710(40/10GbE)VF 관리, DDP/firmware 의존성firmware 호환성, reset 후 VF 상태 불일치
iceE810(100GbE)devlink/representor, 고급 tc offloadDDP 패키지 불일치, eswitch 설정 충돌
idpf신규 인프라 VF/가상화 경로queue model/virtchnl 중심 설계PF-VF 제어채널 상태 불일치
# 실습 예제: Intel 계열 NIC 운영 점검 루틴
# Intel 계열 공통 점검
ethtool -i eth0
ethtool -S eth0 | grep -Ei "fdir|tx_timeout|rx_missed|reset"
dmesg | grep -Ei "ixgbe|i40e|ice|idpf|firmware|ddp"

ICE DDP (Dynamic Device Personalization)

Intel E810(ice 드라이버)은 DDP(Dynamic Device Personalization)를 통해 패킷 파서 파이프라인을 펌웨어 수준에서 재구성합니다. DDP 패키지가 없으면 드라이버는 Safe Mode로 진입하여 고급 기능이 모두 비활성화됩니다.

항목설명
기본 패키지 경로/lib/firmware/intel/ice/ddp/ice.pkg
디바이스별 패키지ice-<device-id>.pkg — 특수 프로토콜(GTP, PPPoE 등) 파싱용
검색 순서① 디바이스별 → ② ice.pkg (기본) → ③ Safe Mode 진입
Safe Mode 진입 조건DDP 파일 부재, DDP 버전 불일치, DDP 로딩 실패
PF probe 시작 request_firmware() DDP 패키지 요청 로딩 성공? 정상 모드 모든 기능 활성 Yes Safe Mode 기본 TX/RX만 가능 No
ICE DDP 로딩 흐름: PF probe → firmware 요청 → DDP 검색 → 정상/Safe Mode 분기
# DDP 패키지 확인
ls -la /lib/firmware/intel/ice/ddp/
dmesg | grep -i "ice.*ddp\|ice.*package\|ice.*safe"

# DDP 버전 확인 (devlink 사용)
devlink dev info pci/0000:af:00.0 | grep -i "fw\|ddp"

# DDP 패키지 수동 배치 (배포판에서 누락 시)
cp ice-1.3.36.0.pkg /lib/firmware/intel/ice/ddp/ice.pkg
# 드라이버 리로드로 DDP 적용
modprobe -r ice && modprobe ice

ICE Adaptive ITR (Interrupt Throttle Rate)

ICE는 하드웨어 타이머 단위가 4μs이며, rx-usecs/tx-usecs 값은 내부적으로 4μs 단위로 반올림됩니다. Adaptive ITR은 트래픽 패턴에 따라 인터럽트 간격을 자동 조절합니다.

파라미터범위설명
rx-usecs0–236 (4μs 단위)RX 인터럽트 코얼레싱 타이머
tx-usecs0–236 (4μs 단위)TX 인터럽트 코얼레싱 타이머
rx-usecs-high0–236Adaptive 모드 상한 바운딩
adaptive-rxon/offAdaptive ITR 활성화 (기본: on)
# Adaptive ITR 상태 확인
ethtool -c eth0

# per-queue 코얼레싱 설정 (큐 0)
ethtool --per-queue eth0 queue_mask 0x1 --coalesce rx-usecs 32 tx-usecs 32

# Adaptive 상한 바운딩 (최대 100μs로 제한)
ethtool -C eth0 adaptive-rx on rx-usecs-high 100

# 워크로드별 프로파일 예시
# ① 벌크 처리량: 높은 코얼레싱
ethtool -C eth0 adaptive-rx off rx-usecs 128
# ② 저지연: 낮은 코얼레싱
ethtool -C eth0 adaptive-rx off rx-usecs 8
# ③ 혼합 트래픽: Adaptive + 바운딩
ethtool -C eth0 adaptive-rx on rx-usecs-high 64

ICE Flow Director 상세

ICE Flow Director는 ntuple 필터를 통해 특정 플로우를 지정 RX 큐로 스티어링합니다. ICE 고유 기능으로 Flex Byte 필터(user-def 필드)를 지원하여 임의 패킷 오프셋의 바이트 매칭이 가능합니다.

지원 flow type필드Flex Byte
tcp4 / tcp6src/dst IP, src/dst port지원
udp4 / udp6src/dst IP, src/dst port지원
sctp4 / sctp6src/dst IP, verification tag미지원
ip4 / ip6src/dst IP, L4 proto미지원
# ntuple 필터링 활성화
ethtool -K eth0 ntuple on

# 특정 TCP 플로우를 큐 4로 스티어링
ethtool -N eth0 flow-type tcp4 src-ip 10.0.0.1 dst-port 80 action 4

# Flex Byte 필터: user-def로 임의 바이트 매칭
# user-def 상위 32비트=오프셋, 하위 32비트=매칭값
ethtool -N eth0 flow-type tcp4 src-ip 10.0.0.1 \
  user-def 0x003C00000000ABCD action 7

# 활성 필터 확인
ethtool -n eth0

# Flow Director 통계
ethtool -S eth0 | grep fdir
Input set 변경 제약: ICE에서 ethtool -N eth0 rx-flow-hash로 해시 input set을 변경하면 기존 Flow Director 규칙과 충돌할 수 있습니다. input set 변경 전에 반드시 기존 FDIR 규칙을 삭제하세요.

ICE SR-IOV VF 운영

ICE는 PF당 최대 256개 VF를 지원하며, VF 신뢰 모드, MDD(Malicious Driver Detection), Anti-Spoofing 등 세밀한 VF 관리 기능을 제공합니다.

기능설명설정 방법
VF Trust 모드 신뢰된 VF에 promiscuous 모드 허용 ip link set eth0 vf 0 trust on
True Promisc Trust VF의 실제 무차별 수신 활성화 모듈 파라미터: vf-true-promisc-support
MDD 악의적 VF 드라이버 탐지 및 자동 리셋 devlink dev param set ... name mdd-auto-reset-vf value true cmode runtime
Anti-Spoofing VF MAC/VLAN 스푸핑 방지 (기본 활성) ip link set eth0 vf 0 spoofchk on
MAC 할당 PF에서 VF MAC 강제 지정 ip link set eth0 vf 0 mac aa:bb:cc:dd:ee:ff
# VF 생성 (최대 256개)
echo 8 > /sys/class/net/eth0/device/sriov_numvfs

# VF 신뢰 모드 + promiscuous 허용
ip link set eth0 vf 0 trust on
# True Promisc 모듈 파라미터 (재로딩 필요)
modprobe ice vf-true-promisc-support=1

# VF에 VLAN 할당 (QoS 포함)
ip link set eth0 vf 0 vlan 100 qos 3

# MDD 자동 리셋 설정
devlink dev param set pci/0000:af:00.0 \
  name mdd-auto-reset-vf value true cmode runtime

# VF 상태 확인
ip link show eth0 | grep "vf "
Link Aggregation 제약: ICE에서 SR-IOV와 Link Aggregation(bonding active-backup 제외)은 상호 배타적입니다. SR-IOV VF가 활성화된 상태에서 LACP bonding을 구성하면 예기치 않은 동작이 발생할 수 있습니다.

ICE Safe Mode 및 장애 복구

Safe Mode는 DDP 로딩 실패 시 진입하는 제한 동작 모드입니다. 기본 송수신만 가능하며, 대부분의 고급 기능이 비활성화됩니다.

Safe Mode 시 비활성화 기능Safe Mode 시 동작 가능 기능
  • Flow Director / ntuple 필터
  • RSS 해시 커스터마이징
  • 프로토콜별 오프로드 (VXLAN, GENEVE 등)
  • DCB / QoS / PFC
  • XDP
  • 기본 L2 송수신
  • 링크 업/다운
  • 기본 통계 수집
  • SR-IOV (제한적)
# Safe Mode 진입 여부 확인
dmesg | grep -i "ice.*safe mode"

# devlink health로 장애 진단
devlink health show pci/0000:af:00.0
devlink health diagnose pci/0000:af:00.0 reporter fw

# 복구 절차
# 1. DDP 패키지 설치 확인
ls /lib/firmware/intel/ice/ddp/
# 2. 펌웨어 업데이트 (필요시)
devlink dev flash pci/0000:af:00.0 file ice_fw.bin
# 3. 드라이버 리로드
modprobe -r ice && modprobe ice

NVIDIA/Mellanox: mlx5e / mlx4_en

드라이버강점운영 포인트장애 패턴
mlx5e고성능 tc/XDP/RDMA/representor 통합devlink health, eswitch 모드 관리 필수rule scale 초과, FW 이벤트 후 recover 반복
mlx4_en구세대 ConnectX 지원기능 범위 제한, 최신 offload 일부 미지원혼합 환경에서 기능 차이로 운영 혼선

Broadcom: bnxt_en / bnx2x / tg3

드라이버주요 환경핵심 포인트자주 보는 이슈
bnxt_enNetXtreme-E(25/50/100GbE)HWRM 펌웨어 인터페이스 의존도 높음FW 이벤트 후 queue reset/복구 지연
bnx2x구세대 10GbE안정 운영 가능하나 최신 기능 제한장시간 부하 후 tx timeout
tg31GbE 온보드기능 단순, 링크 안정성 위주ASPM/절전과 연계된 링크 변동

Marvell/NXP 계열: mvneta / mvpp2 / enetc / dpaa2

드라이버플랫폼핵심 포인트주의사항
mvneta, mvpp2ARM SoC 내장 MACDevice Tree, phylink 설정 정확도 중요MTU/queue 설정과 DT 불일치 시 성능 저하
enetcNXP LS 계열TSN/시간 동기화 활용 환경 다수ptp 설정 누락 시 시간 오차 급증
dpaa2-ethNXP 가속 프레임워크하드웨어 객체 기반 큐 모델control-plane 설정 복잡도 높음

Realtek/Aquantia: r8169 / r8125 / atlantic

드라이버용도핵심 포인트자주 보는 이슈
r8169데스크톱/워크스테이션범용성 높음, 커널 기본 탑재특정 보드에서 절전 연계 링크 불안정
r81252.5GbE(환경별 out-of-tree 혼재)커널/벤더 드라이버 차이 관리 필요업데이트 시 성능/안정성 회귀
atlanticAquantia 5/10GbE멀티큐/오프로드 기본 제공firmware 조합별 성능 편차

Chelsio: cxgb4

강점운영 포인트리스크
TOE/RDMA/iSCSI 오프로드 통합오프로드 경로와 커널 순수 경로를 분리 모니터링기능이 많은 만큼 설정 조합 복잡도 증가

Cloud NIC: ena / gve

드라이버클라우드핵심 포인트장애 패턴
enaAWS ENA큐 스케일/interrupt moderation 튜닝 중요burst 트래픽에서 drop spike
gveGoogle GVNICvirtqueue/doorbell 모델 이해 필요queue config mismatch 시 throughput 급락

가상화 NIC: virtio_net / vmxnet3 / hv_netvsc

드라이버하이퍼바이저핵심 포인트주의사항
virtio_netKVM/QEMUvhost/mergeable buffer/GSO 조합 최적화호스트-게스트 설정 불일치 시 지연 증가
vmxnet3VMware멀티큐와 coalescing 설정이 성능 좌우드라이버/툴 버전 불일치
hv_netvscHyper-V/Azuresynthetic + VF path 전환 관리VF failover 시 순간 패킷 손실

제조사별 공통 디버깅 루틴

  1. 드라이버/펌웨어 버전 고정
    ethtool -i 결과를 티켓/배포 메타데이터에 저장
  2. 벤더 통계 키 추출
    ethtool -S에서 reset/drop/queue 계열 카운터를 표준화
  3. link/queue 이벤트 타임라인화
    dmesg, devlink health, orchestrator 이벤트를 같은 타임라인으로 병합
  4. fallback 경로 검증
    오프로드 비활성화 후 재현 여부를 확인해 HW/드라이버/스택 원인을 분리
  5. 벤더별 재현 스크립트 유지
    링크 flap, reset storm, queue resize, offload toggle 시나리오를 자동화
권장 운영 전략: “공통 net_device 체크리스트 + 벤더별 확장 체크리스트” 2단 구조로 운영하세요. 공통 지표만 보면 벤더 특이 장애를 놓치기 쉽고, 벤더 전용 지표만 보면 스택 공통 회귀를 놓치기 쉽습니다.

구현 가이드: 최소 골격부터 확장까지

  1. 1단계: 최소 송수신 경로ndo_open/stop/start_xmit, 단일 NAPI queue, 기본 IRQ 동작
  2. 2단계: 안정성 확보 — 에러 경로 정리, queue stop/wake 일관성, teardown 순서 검증
  3. 3단계: 운영성 확보ethtool_ops, 통계, self-test, 링 파라미터 조정
  4. 4단계: 성능 확장 — 멀티큐 RSS, XDP/AF_XDP, page_pool, BQL, NUMA affinity
  5. 5단계: 가상 netdev 통합 — TUN/TAP, veth, virtio-net과 공통 코어 재사용 전략 수립
권장 학습 순서: 네트워크 스택Network Device 드라이버 (net_device)TUN/TAPBPF/XDP 순서로 보면 드라이버-스택-가속 경로가 자연스럽게 연결됩니다.