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를 활용한 병목 분석까지 실전 네트워크 드라이버 개발에 필요한 핵심을 다룹니다.
핵심 요약
- net_device — 인터페이스의 공통 상태와 콜백 진입점입니다.
- net_device_ops — open/stop/xmit 등 데이터 경로 계약을 정의합니다.
- NAPI — RX 인터럽트 폭풍을 줄이고 폴링 기반 처리량을 확보합니다.
- BQL — TX 큐 지연(latency)과 버퍼블로트 리스크를 줄입니다.
- 가상 netdev — TUN/TAP처럼 하드웨어 없이도 동일한 netdev 모델을 재사용합니다.
단계별 이해
- 수명주기 설계
할당/등록/해제 순서를 먼저 확정합니다. - RX/TX 콜백 구현
ndo_start_xmit()와 NAPI poll 루프를 정확히 연결합니다. - 운영 인터페이스 연결
ethtool_ops, 통계, 링크 상태(phylink)를 연결합니다. - 가상 netdev 확장
TUN/TAP, veth, virtio-net과 공통 패턴을 통합해 이해합니다.
개념 예시가 표시된 블록은 구조와 호출 계약 이해용이며, 실습 예제가 표시된 블록은 사용자 공간에서 실행/검증 절차를 바로 적용할 수 있도록 구성했습니다.
개요: net_device 드라이버의 역할
struct net_device 드라이버는 커널 네트워크 스택과 실제/가상 링크 계층 사이의 어댑터입니다. 유저스페이스 입장에서는 eth0, ens3, tap0 모두 동일한 netdev 인터페이스처럼 보이지만, 내부 구현은 물리 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_open | ip link set up | IRQ/NAPI 활성화, RX/TX queue 시작 |
ndo_stop | ip link set down | queue 정지, IRQ 비활성화, NAPI 비활성화 |
ndo_start_xmit | 송신 경로 | skb를 TX ring에 게시하고 doorbell 트리거 |
ndo_get_stats64 | 통계 조회 | race-safe한 64-bit 통계 제공 |
ndo_set_features | offload 변경 | 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;
}
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의 핵심은 lib/dynamic_queue_limits.c에 구현된 DQL(Dynamic Queue Limits) 알고리즘입니다.
드라이버가 패킷을 큐에 넣을 때(dql_queued)와 완료될 때(dql_completed)를 추적하여,
현재 inflight(미완료) 바이트가 LIMIT을 초과하면 큐를 멈추고,
완료 시 실제 사용 패턴에 따라 LIMIT을 올리거나 내립니다.
- 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)을 반영하여 불필요한 여유를 제거합니다.
/* 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초) */
};
limit과 num_completed는 TX 완료 경로(보통 softirq)에서 빈번히 갱신되므로
____cacheline_aligned_in_smp로 분리하여 num_queued/adj_limit(송신 경로)와의 false sharing을 방지합니다.
드라이버 API 통합
드라이버가 BQL을 사용하려면 TX 경로의 세 지점에서 API를 호출합니다. 모든 API는 바이트 단위로 동작하며, 패킷 수가 아닌 누적 바이트를 전달해야 합니다.
/* 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 파일 | 읽기/쓰기 | 설명 |
|---|---|---|
limit | R | 현재 동적 LIMIT (바이트). 알고리즘이 자동 조정 |
limit_max | R/W | LIMIT 상한. 낮추면 최대 큐 깊이를 제한 |
limit_min | R/W | LIMIT 하한. 높이면 최소 처리량 보장 |
hold_time | R/W | slack 관찰 윈도우 (ms). 기본 1000 |
inflight | R | 현재 미완료 바이트 (num_queued − num_completed) |
BQL과 qdisc/TC의 상호작용
BQL은 qdisc 아래, NIC ring 위에 위치합니다. 패킷 흐름에서 BQL의 정확한 위치를 이해하면
fq_codel 같은 AQM(Active Queue Management)과의 시너지를 극대화할 수 있습니다.
- 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 경로까지 하나의 동시성 계약으로 맞춰야 합니다.
| 항목 | 일반 TX 경로 | LLTX 경로 |
|---|---|---|
| 직렬화 주체 | 코어/큐 락 + 드라이버 보조 락 | 드라이버가 전적으로 책임 |
| 병목 위치 | 락 경합은 비교적 예측 가능 | 드라이버 구현 품질에 따라 편차 큼 |
| 디버깅 난이도 | 표준 패턴과 도구가 많음 | race 재현/분석 난이도 높음 |
| 권장도 | 신규 구현 권장 | 기존 드라이버 유지보수 목적 외 비권장 |
LLTX를 유지해야 하는 코드베이스라면 아래 4가지를 반드시 고정 규칙으로 문서화해야 합니다.
- xmit 직렬화 규칙
ndo_start_xmit()의 re-entry 허용 범위(전역/큐별)를 명시하고 락 순서를 고정 - queue 상태 전이 규칙
netif_tx_stop_queue()/netif_tx_wake_queue()호출 조건을 단일 함수로 중앙화 - completion 메모리 순서
descriptor reclaim 이후 wake 판단 전까지의 barrier 규칙을 아키텍처별로 검증 - 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;
}
통계와 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 eth0 | TSO/GRO/checksum offload 상태 |
ethtool -S eth0 | 링 드롭, 에러, 큐별 카운터 |
ethtool -l eth0 | 채널(RX/TX queue) 구성 |
ip -s link show dev eth0 | 커널 링크 통계의 상위 뷰 |
링크 계층: PHY, phylib, phylink
현대 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;
}
}
멀티큐, 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/RFS | HW 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_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 | 규칙 우선순위/충돌 처리 |
| eSwitch | switchdev, representor netdev | VF/representor 일관성 |
| 암호화/터널 | xfrm offload, UDP tunnel offload | fallback 경로와 통계 구분 |
리셋/장애 복구: 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();
}
- watchdog:
ndo_tx_timeout()에서 즉시 heavy reset을 수행하지 말고 workqueue로 이관 - devlink health: reporter dump/recover 콜백으로 운영팀의 장애 자동화와 연동
- AER 연계: PCIe fatal/non-fatal 이벤트를 reset state machine과 통합
오프로드 계약: 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 offload | NETIF_F_HW_CSUM, skb->ip_summed | partial checksum descriptor 구성 |
| TSO/GSO | NETIF_F_TSO*, gso_size | 세그먼트 제한, header split 처리 |
| GRO/LRO | napi_gro_receive() | 재조립 후 메타데이터 일관성 |
| VLAN offload | NETIF_F_HW_VLAN_CTAG_TX/RX | tag 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;
}
- PHC:
/dev/ptpN제공과ptp4l/phc2sys연동 검증 - TX timestamp: skb 소유권과 timestamp completion 경합 방지
- RX filter: 지원 가능한 timestamp filter를 정확히 반환
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;
}
검증 매트릭스: 릴리스 전 체크 항목
| 영역 | 필수 검증 | 합격 기준 예시 |
|---|---|---|
| 기능 | up/down, MTU, VLAN, bridge, bond | 10k회 반복 시 누수/lockup 없음 |
| 성능 | 단일/다중 스트림, 작은 패킷/점보 프레임 | 목표 PPS/Gbps 달성, drop rate 임계 이내 |
| 안정성 | link flap, reset storm, hotplug, suspend/resume | 자동 복구 성공, 수동 재로드 불필요 |
| 가시성 | ethtool/stat/devlink health dump | 장애 원인 식별 가능한 텔레메트리 제공 |
| 보안 | XDP/tc rule 경계값, malformed packet | crash 없이 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;
}
- XPS:
/sys/class/net/<dev>/queues/tx-*/xps_cpus와 드라이버 큐 매핑 일치 - RFS/RPS와 충돌 회피: RX CPU와 TX completion CPU가 지나치게 분산되지 않도록 설계
- NUMA: queue memory, IRQ, NAPI poll CPU를 같은 노드로 묶어 캐시 효율 확보
Doorbell 메커니즘과 DMA 메모리 배리어
약한 메모리 모델 CPU(ARM64 등)에서 descriptor write와 doorbell MMIO write의 순서가 보장되지 않으면 간헐적 TX hang이 생깁니다. 게시 경로에서 barrier 사용 규칙을 문서화해야 합니다.
Doorbell이란?
Doorbell은 PCIe BAR 공간의 장치 레지스터에 MMIO write를 수행하여 NIC에 새 작업(디스크립터)이 준비되었음을 알리는 메커니즘입니다. Doorbell이 없으면 NIC이 링 버퍼를 지속적으로 폴링해야 하며, 이는 PCIe 대역폭 낭비와 전력 소모 증가를 초래합니다.
Doorbell의 핵심 속성은 다음과 같습니다.
- 단일 원자적 MMIO write: 일반적으로 4바이트(32비트) 크기이며, CPU의 단일 스토어 명령으로 실행됩니다.
- PCIe 트랜잭션 비용: doorbell 1회당 수백 나노초(200~500 ns)가 소요되며, PCIe TLP(Transaction Layer Packet) 오버헤드를 포함합니다.
- 메모리 배리어 선행 필수: doorbell 이전에 반드시 적절한 메모리 배리어(
dma_wmb())가 선행되어야 디바이스가 완전한 디스크립터를 읽을 수 있습니다.
서브시스템별 Doorbell 비교
Doorbell 메커니즘은 NIC뿐 아니라 다양한 PCIe 디바이스 서브시스템에서 사용됩니다. 아래 표는 주요 서브시스템별 doorbell 특성을 비교합니다.
| 서브시스템 | Doorbell 대상 | 기록 값 | 최적화 기법 |
|---|---|---|---|
| NIC TX | Tail Pointer 레지스터 | 마지막 디스크립터 인덱스 | 배치 게시 |
| NVMe | SQ Tail Doorbell | 큐 tail 포인터 | Shadow Doorbell Buffer |
| xHCI (USB 3.x) | Doorbell Array | EP 인덱스 | 스트림 기반 배치 |
| NTB (PCI) | Doorbell 비트맵 | 이벤트 비트 | 비트마스크 |
Doorbell 최적화 기법
Doorbell은 PCIe MMIO 트랜잭션이므로 호출 빈도를 줄이는 것이 성능 최적화의 핵심입니다. 주요 기법은 다음과 같습니다.
- 배치 게시(Batch Posting): 여러 디스크립터를 작성한 후 doorbell을 1회만 수행합니다. NVMe의
commit_rqs콜백이 대표적인 패턴으로, 다수의 SQ entry를 기록한 뒤 마지막에 한 번만 tail doorbell을 갱신합니다. NIC 드라이버에서도xmit_more플래그를 확인하여 배치 doorbell을 구현할 수 있습니다. - Shadow Doorbell: MMIO 대신 호스트 메모리 영역에 tail 값을 기록하고, 디바이스가 해당 메모리를 폴링하는 방식입니다. NVMe 1.3+ 스펙의 Shadow Doorbell Buffer가 대표적이며, PCIe MMIO 왕복 비용을 제거하여 수십 퍼센트의 IOPS 향상을 달성할 수 있습니다.
- Write Combining: doorbell 레지스터가 위치한 PCIe BAR 페이지를 WC(Write Combining) 매핑하여, 여러 doorbell write가 단일 PCIe 트랜잭션으로 합쳐지도록 합니다. 이를 통해 PCIe TLP 오버헤드를 줄이고 대역폭 효율을 향상시킵니다.
/* 개념 예시: 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 doorbell | dma_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
회귀 테스트: 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 |
| PFC | priority별 pause on/off 적용 | drop 급증 또는 head-of-line blocking |
| ETS | bandwidth share를 HW arbitration에 반영 | 대역폭 분배 불일치 |
PREEMPT_RT와 NAPI threaded 모드
실시간 커널에서는 IRQ/softirq 모델이 일반 커널과 다르게 동작합니다. 드라이버는 spinlock 길이를 줄이고, napi poll 지연 상한을 보장하도록 설계해야 합니다.
- 긴 IRQ-off 구간 제거: TX reclaim 루프를 짧게 쪼개고 예산 기반 처리 유지
- 락 경쟁 최소화: 큐별 락 분리, 필요 시 lockless ring 고려
- 우선순위 설계: IRQ thread/NAPI 실행 CPU를 RT 태스크와 분리
- 지연 측정:
cyclictest와 네트워크 부하를 동시 실행해 tail latency 확인
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);
}
보안 하드닝: 입력 검증과 경계 조건
네트워크 드라이버 취약점은 원격 트리거 가능성이 있습니다. 길이 검증, ring index 범위 확인, DMA 주소 검증은 성능 최적화보다 우선되어야 합니다.
| 취약 패턴 | 점검 포인트 | 방어 전략 |
|---|---|---|
| RX length 신뢰 | HW가 넘긴 length를 그대로 사용 | 최소/최대 길이, headroom 검증 |
| ring index overflow | producer/consumer wrap 처리 누락 | mask 기반 인덱싱 + assert |
| UAF on reset | reset 중 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로 전달합니다.
/* 개념 예시: 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 보호 영역: 장치 등록/이름 변경/링크 설정 같은 제어 경로
- NAPI 컨텍스트: RX poll은 softirq 문맥에서 동작
- TX 락: 멀티큐에서 queue별 락 또는 lockless ring 설계
- RCU: XDP program pointer, filter table 조회 경로
- 메모리 배리어: descriptor 게시 전 doorbell write ordering 보장
rtnl_lock이 per-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 카운터 |
| 링크 flap | PHY state machine/interrupt storm | dmesg, 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분 체크리스트
- 장치 인식 확인
lspci -nn,dmesg | grep -i <driver>로 probe 성공 여부 확인 - 링크 기본 동작
ip link set dev eth0 up,ethtool eth0로 speed/duplex/link 검증 - 기본 송수신
ping, 단일iperf3로 RX/TX 모두 정상 동작 확인 - 오프로드/큐 설정
ethtool -k/-l/-x확인 후 장비 정책에 맞게 조정 - 에러 카운터 스냅샷
ethtool -S eth0초기값 저장, 10분 부하 후 증분 비교 - 리셋 복구
의도적 link flap 또는 함수 리셋 후 자동 복구 성공 여부 확인 - 관측/알람 연계
링크/에러/reset 지표가 모니터링 시스템에 수집되는지 검증
코드 리뷰 체크리스트 (net_device 전용)
- 수명주기:
register_netdev()이후/이전 해제 순서가 정확한가 - NAPI: poll budget 준수, complete/unmask 순서가 안전한가
- TX 계약: queue stop/wake,
NETDEV_TX_BUSY반환 조건이 일관적인가 - 동기화: RTNL, spinlock, RCU 경계가 명확하고 lock inversion 위험이 없는가
- 오프로드: advertise feature와 실제 HW 동작이 일치하는가
- 에러 경로: DMA map 실패, IRQ 요청 실패, reset 실패 시 자원 정리가 누락되지 않는가
- 텔레메트리: 장애 분석에 필요한 통계/로그/devlink dump가 충분한가
제조사별 NIC 드라이버 상세 매트릭스
같은 net_device 모델이라도 벤더별로 펌웨어 의존성, 오프로드 범위, reset 전략이 다릅니다. 운영 환경에서는 “벤더별 특성”을 분리해서 튜닝/장애 대응해야 재현성이 올라갑니다.
Intel: e1000e / igb / ixgbe / i40e / ice / idpf
| 드라이버 | 주요 세대/용도 | 핵심 포인트 | 자주 보는 이슈 |
|---|---|---|---|
e1000e | 1GbE 서버/임베디드 | 안정성 우선, 기능 단순 | 링크 flap, 절전 전환 후 wake 지연 |
igb | 1GbE 멀티큐 | RSS/TSO 기본, SR-IOV 일부 모델 | 큐 불균형, IRQ affinity 미스매치 |
ixgbe | 10GbE(82599/X540) | Flow Director, DCB, XDP 일부 경로 | tx timeout, FDIR rule 관리 복잡도 |
i40e | XL710/X710(40/10GbE) | VF 관리, DDP/firmware 의존성 | firmware 호환성, reset 후 VF 상태 불일치 |
ice | E810(100GbE) | devlink/representor, 고급 tc offload | DDP 패키지 불일치, 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 로딩 실패 |
# 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-usecs | 0–236 (4μs 단위) | RX 인터럽트 코얼레싱 타이머 |
tx-usecs | 0–236 (4μs 단위) | TX 인터럽트 코얼레싱 타이머 |
rx-usecs-high | 0–236 | Adaptive 모드 상한 바운딩 |
adaptive-rx | on/off | Adaptive 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 / tcp6 | src/dst IP, src/dst port | 지원 |
udp4 / udp6 | src/dst IP, src/dst port | 지원 |
sctp4 / sctp6 | src/dst IP, verification tag | 미지원 |
ip4 / ip6 | src/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
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 "
ICE Safe Mode 및 장애 복구
Safe Mode는 DDP 로딩 실패 시 진입하는 제한 동작 모드입니다. 기본 송수신만 가능하며, 대부분의 고급 기능이 비활성화됩니다.
| Safe Mode 시 비활성화 기능 | Safe Mode 시 동작 가능 기능 |
|---|---|
|
|
# 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 일부 미지원 | 혼합 환경에서 기능 차이로 운영 혼선 |
- 핵심: tc flower offload와 representor 경로를 함께 운영하는 경우 rule life-cycle 관리가 가장 중요
- 권장: devlink health dump를 수집해 장애 당시 FW/queue 상태를 보존
Broadcom: bnxt_en / bnx2x / tg3
| 드라이버 | 주요 환경 | 핵심 포인트 | 자주 보는 이슈 |
|---|---|---|---|
bnxt_en | NetXtreme-E(25/50/100GbE) | HWRM 펌웨어 인터페이스 의존도 높음 | FW 이벤트 후 queue reset/복구 지연 |
bnx2x | 구세대 10GbE | 안정 운영 가능하나 최신 기능 제한 | 장시간 부하 후 tx timeout |
tg3 | 1GbE 온보드 | 기능 단순, 링크 안정성 위주 | ASPM/절전과 연계된 링크 변동 |
Marvell/NXP 계열: mvneta / mvpp2 / enetc / dpaa2
| 드라이버 | 플랫폼 | 핵심 포인트 | 주의사항 |
|---|---|---|---|
mvneta, mvpp2 | ARM SoC 내장 MAC | Device Tree, phylink 설정 정확도 중요 | MTU/queue 설정과 DT 불일치 시 성능 저하 |
enetc | NXP LS 계열 | TSN/시간 동기화 활용 환경 다수 | ptp 설정 누락 시 시간 오차 급증 |
dpaa2-eth | NXP 가속 프레임워크 | 하드웨어 객체 기반 큐 모델 | control-plane 설정 복잡도 높음 |
Realtek/Aquantia: r8169 / r8125 / atlantic
| 드라이버 | 용도 | 핵심 포인트 | 자주 보는 이슈 |
|---|---|---|---|
r8169 | 데스크톱/워크스테이션 | 범용성 높음, 커널 기본 탑재 | 특정 보드에서 절전 연계 링크 불안정 |
r8125 | 2.5GbE(환경별 out-of-tree 혼재) | 커널/벤더 드라이버 차이 관리 필요 | 업데이트 시 성능/안정성 회귀 |
atlantic | Aquantia 5/10GbE | 멀티큐/오프로드 기본 제공 | firmware 조합별 성능 편차 |
Chelsio: cxgb4
| 강점 | 운영 포인트 | 리스크 |
|---|---|---|
| TOE/RDMA/iSCSI 오프로드 통합 | 오프로드 경로와 커널 순수 경로를 분리 모니터링 | 기능이 많은 만큼 설정 조합 복잡도 증가 |
Cloud NIC: ena / gve
| 드라이버 | 클라우드 | 핵심 포인트 | 장애 패턴 |
|---|---|---|---|
ena | AWS ENA | 큐 스케일/interrupt moderation 튜닝 중요 | burst 트래픽에서 drop spike |
gve | Google GVNIC | virtqueue/doorbell 모델 이해 필요 | queue config mismatch 시 throughput 급락 |
가상화 NIC: virtio_net / vmxnet3 / hv_netvsc
| 드라이버 | 하이퍼바이저 | 핵심 포인트 | 주의사항 |
|---|---|---|---|
virtio_net | KVM/QEMU | vhost/mergeable buffer/GSO 조합 최적화 | 호스트-게스트 설정 불일치 시 지연 증가 |
vmxnet3 | VMware | 멀티큐와 coalescing 설정이 성능 좌우 | 드라이버/툴 버전 불일치 |
hv_netvsc | Hyper-V/Azure | synthetic + VF path 전환 관리 | VF failover 시 순간 패킷 손실 |
제조사별 공통 디버깅 루틴
- 드라이버/펌웨어 버전 고정
ethtool -i결과를 티켓/배포 메타데이터에 저장 - 벤더 통계 키 추출
ethtool -S에서 reset/drop/queue 계열 카운터를 표준화 - link/queue 이벤트 타임라인화
dmesg, devlink health, orchestrator 이벤트를 같은 타임라인으로 병합 - fallback 경로 검증
오프로드 비활성화 후 재현 여부를 확인해 HW/드라이버/스택 원인을 분리 - 벤더별 재현 스크립트 유지
링크 flap, reset storm, queue resize, offload toggle 시나리오를 자동화
구현 가이드: 최소 골격부터 확장까지
- 1단계: 최소 송수신 경로 —
ndo_open/stop/start_xmit, 단일 NAPI queue, 기본 IRQ 동작 - 2단계: 안정성 확보 — 에러 경로 정리, queue stop/wake 일관성, teardown 순서 검증
- 3단계: 운영성 확보 —
ethtool_ops, 통계, self-test, 링 파라미터 조정 - 4단계: 성능 확장 — 멀티큐 RSS, XDP/AF_XDP, page_pool, BQL, NUMA affinity
- 5단계: 가상 netdev 통합 — TUN/TAP, veth, virtio-net과 공통 코어 재사용 전략 수립
관련 문서
- 디바이스 드라이버 — 전체 드라이버 프레임워크 문맥
- 네트워크 스택 — 커널 패킷 경로와 NAPI 배경
- TUN/TAP — 가상 인터페이스 구현 상세
- BPF/XDP — 드라이버 레벨 초고속 패킷 처리
- AF_XDP — 유저스페이스 zero-copy 경로