네트워크 디바이스 드라이버 (net_device)
Linux 네트워크 디바이스 드라이버를 고처리량 데이터 경로와 운영 안정성 관점에서 심층 정리합니다. net_device/net_device_ops 초기화, NAPI 기반 RX 폴링(Polling), TX 큐 관리와 BQL, MSI-X/IRQ affinity 최적화, checksum/TSO/GRO 등 오프로드 기능, XDP/AF_XDP 연계, ethtool 통계와 링크 상태 관리, 물리 NIC와 TUN/TAP 같은 가상 netdev 공통 모델, tracepoint/perf/bpftrace를 활용한 병목(Bottleneck) 분석까지 실전 네트워크 드라이버 개발에 필요한 핵심을 다룹니다.
핵심 요약
- net_device — 인터페이스의 공통 상태와 콜백(Callback) 진입점(Entry Point)입니다.
- net_device_ops — open/stop/xmit 등 데이터 경로 계약을 정의합니다.
- NAPI — RX 인터럽트(Interrupt) 폭풍을 줄이고 폴링 기반 처리량(Throughput)을 확보합니다.
- BQL — TX 큐 지연(latency)과 버퍼블로트 리스크를 줄입니다.
- 가상 netdev — TUN/TAP처럼 하드웨어 없이도 동일한 netdev 모델을 재사용합니다.
단계별 이해
- 수명주기 설계
할당/등록/해제 순서를 먼저 확정합니다. - RX/TX 콜백 구현
ndo_start_xmit()와 NAPI poll 루프를 정확히 연결합니다. - 운영 인터페이스 연결
ethtool_ops, 통계, 링크 상태(phylink)를 연결합니다. - 가상 netdev 확장
TUN/TAP, veth, virtio-net과 공통 패턴을 통합해 이해합니다.
개념 예시가 표시된 블록은 구조와 호출 계약 이해용이며, 실습 예제가 표시된 블록은 사용자 공간(User Space)에서 실행/검증 절차를 바로 적용할 수 있도록 구성했습니다.
개요: net_device 드라이버의 역할
struct net_device 드라이버는 커널 네트워크 스택(Network Stack)과 실제/가상 링크 계층 사이의 어댑터입니다. 유저스페이스 입장에서는 eth0, ens3, tap0 모두 동일한 netdev 인터페이스처럼 보이지만, 내부 구현은 물리 NIC/가상 디바이스에 따라 크게 달라집니다.
struct net_device 핵심 필드 상세 분석
struct net_device는 리눅스 네트워크 스택에서 가장 핵심적인 구조체로, 하나의 네트워크 인터페이스가 가져야 할 모든 상태 정보를 담고 있습니다.
커널 6.x 기준 이 구조체의 크기는 약 2KB에 달하며, 수십 개의 필드가 캐시 라인 최적화(Cache Line Optimization)를 고려하여 배치되어 있습니다.
특히 핫 패스(Hot Path)에서 자주 접근하는 필드들은 ____cacheline_aligned_in_smp 어노테이션(Annotation)을 통해
동일한 캐시 라인에 모이도록 정렬됩니다. 이는 SMP 환경에서 캐시 바운싱(Cache Bouncing)을 최소화하고,
패킷 송수신 경로의 지연 시간(Latency)을 줄이기 위한 의도적 설계입니다.
net_device는 include/linux/netdevice.h에 정의되며, alloc_netdev_mqs()를 통해 할당됩니다.
핵심 필드 분류
식별 및 이름
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
name[IFNAMSIZ] |
char[] |
인터페이스 이름 (eth0, wlan0 등) |
IFNAMSIZ = 16, dev_change_net_namespace()에서 변경 가능 |
ifindex |
int |
고유 인터페이스 인덱스(Interface Index) | dev_new_index()로 할당, 네임스페이스 내 유일 |
dev_id |
u16 |
동일 MAC 주소 공유 시 구분자 | 가상 인터페이스 식별 용도 |
dev_port |
u16 |
다중 포트 디바이스에서 포트 번호 | udev 규칙에서 인터페이스 구분에 활용 |
상태 및 플래그
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
flags |
unsigned int |
IFF_UP, IFF_BROADCAST 등 인터페이스 플래그 |
ioctl/netlink로 변경 가능, dev_change_flags() |
priv_flags |
unsigned int |
드라이버/내부 전용 플래그 | 사용자 공간에서 직접 변경 불가 |
state |
unsigned long |
__LINK_STATE_* 비트맵(Bitmap) |
__LINK_STATE_START, __LINK_STATE_XOFF 등 |
operstate |
unsigned char |
RFC 2863 운영 상태(Operational State) | IF_OPER_UP, IF_OPER_DOWN 등 |
reg_state |
enum netdev_reg_state |
등록 상태 머신(Registration State Machine) | register_netdevice() / unregister_netdevice()에서 전이 |
네트워크 파라미터
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
mtu |
unsigned int |
최대 전송 단위(Maximum Transmission Unit) | 이더넷 기본 1500, dev_set_mtu()로 변경 |
min_mtu |
unsigned int |
MTU 최솟값 | 기본 ETH_MIN_MTU (68) |
max_mtu |
unsigned int |
MTU 최댓값 | 기본 ETH_MAX_MTU (65535) |
type |
unsigned short |
ARP 하드웨어 타입 | ARPHRD_ETHER, ARPHRD_LOOPBACK 등 |
hard_header_len |
unsigned short |
L2 헤더 길이(바이트) | 이더넷: ETH_HLEN (14) |
addr_len |
unsigned char |
하드웨어 주소 길이 | 이더넷: 6 (MAC 48비트) |
dev_addr |
unsigned char* |
하드웨어 주소(Hardware Address) | dev_addr_set()으로 변경, 직접 수정 금지 |
broadcast |
unsigned char* |
브로드캐스트 주소 | 이더넷: ff:ff:ff:ff:ff:ff |
콜백 테이블(Callback Table)
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
netdev_ops |
const struct net_device_ops* |
데이터/제어 경로 핵심 콜백 | ndo_open, ndo_start_xmit, ndo_stop 등 포함 |
ethtool_ops |
const struct ethtool_ops* |
ethtool 인터페이스 콜백 | 링크 속도, 통계, 드라이버 정보 조회 등 |
header_ops |
const struct header_ops* |
L2 헤더 생성/파싱 콜백 | eth_header_ops (이더넷 기본) |
xdp_metadata_ops |
const struct xdp_metadata_ops* |
XDP 메타데이터 콜백 | 커널 6.3+ XDP 힌트(Hints) 인터페이스 |
큐 및 성능
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
num_tx_queues |
unsigned int |
할당된 TX 큐 수 | alloc_netdev_mqs()에서 설정 |
real_num_tx_queues |
unsigned int |
현재 활성 TX 큐 수 | netif_set_real_num_tx_queues()로 변경 |
num_rx_queues |
unsigned int |
할당된 RX 큐 수 | alloc_netdev_mqs()에서 설정 |
real_num_rx_queues |
unsigned int |
현재 활성 RX 큐 수 | netif_set_real_num_rx_queues()로 변경 |
tx_queue_len |
unsigned long |
TX 큐 기본 길이 | 기본 1000, ip link set txqueuelen으로 변경 |
watchdog_timeo |
int |
TX 타임아웃 임계값(Threshold) | jiffies 단위, 초과 시 ndo_tx_timeout 호출 |
gso_max_size |
unsigned int |
GSO 최대 세그먼트 크기 | 기본 GSO_LEGACY_MAX_SIZE (65536) |
gso_max_segs |
u16 |
GSO 최대 세그먼트 수 | 기본 GSO_MAX_SEGS (65535) |
Feature 비트마스크
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
features |
netdev_features_t |
현재 활성화된 오프로드 기능(Feature) | 실제 적용 중인 feature 조합 |
hw_features |
netdev_features_t |
하드웨어가 지원 가능한 feature | ethtool로 on/off 토글 가능 범위 |
wanted_features |
netdev_features_t |
사용자가 요청한 feature | netdev_update_features()에서 features로 반영 |
vlan_features |
netdev_features_t |
VLAN 내부 패킷에 허용되는 feature | VLAN 디바이스가 상속받을 수 있는 오프로드 |
통계(Statistics)
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
stats |
struct net_device_stats |
기본 네트워크 통계 (레거시) | ndo_get_stats64() 또는 per-CPU 통계 권장 |
pcpu_stat_type |
enum netdev_pcpu_stat_type |
per-CPU 통계 타입 선택 | NETDEV_PCPU_STAT_NONE, TSTATS, DSTATS, LSTATS |
tstats / dstats / lstats |
union (per-CPU 포인터) | per-CPU 통계 데이터 포인터 | 코어 할당 시 netdev_alloc_pcpu_stats() 사용 |
참조 및 계층 구조
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
dev |
struct device |
리눅스 디바이스 모델 연결 | sysfs /sys/class/net/ 노드 생성 |
nd_net |
possible_net_t |
소속 네트워크 네임스페이스(Network Namespace) | dev_net() 매크로로 접근 |
ip_ptr |
void* |
IPv4 프로토콜 설정 포인터 | struct in_device로 캐스팅 |
ip6_ptr |
void* |
IPv6 프로토콜 설정 포인터 | struct inet6_dev로 캐스팅 |
캐시 라인 최적화 배치
커널은 성능 최적화를 위해 struct net_device의 필드를 캐시 라인 경계에 맞춰 배치합니다.
핫 패스에서 빈번하게 읽히는 필드가 동일한 캐시 라인에 위치하면, CPU 캐시 미스(Cache Miss)를 최소화할 수 있습니다.
다음은 커널 소스에서 실제 사용되는 캐시 라인 정렬 패턴입니다.
/* 커널 소스 분석: include/linux/netdevice.h - struct net_device 캐시 라인 배치 */
struct net_device {
/* ---- 첫 번째 캐시 라인: 이름 + 핫 패스 필드 ---- */
char name[IFNAMSIZ];
struct netdev_name_node *name_node;
struct dev_ifalias __rcu *ifalias;
unsigned long mem_end;
unsigned long mem_start;
unsigned long base_addr;
/* ---- 핫 패스 송신 경로 (____cacheline_aligned_in_smp) ---- */
unsigned long state;
struct list_head dev_list;
struct list_head napi_list;
unsigned int flags;
unsigned int priv_flags;
const struct net_device_ops *netdev_ops;
int ifindex;
unsigned short gflags;
unsigned short hard_header_len;
unsigned int mtu;
unsigned short needed_headroom;
unsigned short needed_tailroom;
netdev_features_t features; /* 현재 활성 feature */
netdev_features_t hw_features; /* HW 지원 가능 feature */
netdev_features_t wanted_features;
/* ---- 수신 경로 캐시 라인 정렬 ---- */
unsigned int num_tx_queues
____cacheline_aligned_in_smp;
unsigned int real_num_tx_queues;
struct Qdisc *qdisc;
unsigned int tx_queue_len;
unsigned int num_rx_queues
____cacheline_aligned_in_smp;
unsigned int real_num_rx_queues;
/* ---- 콜백 테이블 ---- */
const struct ethtool_ops *ethtool_ops;
const struct header_ops *header_ops;
const struct xdp_metadata_ops *xdp_metadata_ops;
unsigned char operstate;
unsigned char link_mode;
unsigned int min_mtu;
unsigned int max_mtu;
unsigned short type; /* ARPHRD_ETHER 등 */
unsigned char addr_len;
unsigned char *dev_addr;
unsigned char *broadcast;
/* ---- per-CPU 통계 ---- */
struct net_device_stats stats;
enum netdev_pcpu_stat_type pcpu_stat_type;
union {
struct pcpu_sw_netstats __percpu *tstats;
struct pcpu_dstats __percpu *dstats;
struct pcpu_lstats __percpu *lstats;
};
/* ---- 디바이스 모델 / 네임스페이스 ---- */
struct device dev;
possible_net_t nd_net;
void *ip_ptr;
void *ip6_ptr;
enum netdev_reg_state { ... } reg_state;
/* ... 그 외 다수 필드 ... */
};
num_tx_queues, real_num_tx_queues, qdisc)가 하나의 캐시 라인에 모여 있어,
dev_queue_xmit() 경로에서 단일 캐시 라인 로드로 큐 선택에 필요한 모든 정보를 얻을 수 있습니다.
RX 큐 필드도 마찬가지로 별도 캐시 라인에 정렬되어 수신 경로와 송신 경로 간 캐시 경합을 방지합니다.
flags 필드 상세 분석
flags 필드는 include/uapi/linux/if.h에 정의된 IFF_* 상수들의 비트 조합입니다.
사용자 공간에서 ioctl(SIOCSIFFLAGS) 또는 netlink를 통해 변경할 수 있으며,
커널 내부에서는 반드시 dev_change_flags()를 통해 변경해야 합니다.
| 플래그 | 값 | 의미 | 설정 시점 |
|---|---|---|---|
IFF_UP |
1<<0 |
인터페이스 활성화(Administrative Up) | ip link set dev up → dev_open() |
IFF_BROADCAST |
1<<1 |
브로드캐스트 주소 유효 | 드라이버 등록 시 ether_setup()에서 설정 |
IFF_LOOPBACK |
1<<3 |
루프백 인터페이스 | loopback_setup()에서 설정 |
IFF_POINTOPOINT |
1<<4 |
점대점(Point-to-Point) 링크 | PPP, SLIP 등 드라이버 초기화 시 |
IFF_MULTICAST |
1<<12 |
멀티캐스트 지원 | ether_setup()에서 기본 설정 |
IFF_PROMISC |
1<<8 |
무차별 모드(Promiscuous Mode) | dev_set_promiscuity(), tcpdump 등 사용 시 |
IFF_ALLMULTI |
1<<9 |
모든 멀티캐스트 수신 | dev_set_allmulti(), 멀티캐스트 라우팅 시 |
IFF_NOARP |
1<<7 |
ARP 프로토콜 사용 안 함 | 루프백, 터널 등에서 설정 |
IFF_RUNNING |
1<<6 |
드라이버 리소스 할당됨(Operational) | netif_carrier_on() / netif_carrier_off()에 연동 |
/* 커널 소스 분석: net/core/dev.c - dev_change_flags() 내부 흐름 */
int dev_change_flags(struct net_device *dev, unsigned int flags,
struct netlink_ext_ack *extack)
{
unsigned int changes;
int ret;
/* 이전 flags와 새 flags의 차이(XOR) 계산 */
changes = flags ^ dev->flags;
/* IFF_UP 변경 처리: 인터페이스 열기/닫기 */
if (changes & IFF_UP) {
if (flags & IFF_UP)
ret = __dev_open(dev, extack);
else
__dev_close(dev);
}
/* IFF_PROMISC 변경 시 하드웨어 필터 업데이트 */
if ((changes & IFF_PROMISC) && (dev->flags & IFF_UP)) {
int inc = (flags & IFF_PROMISC) ? 1 : -1;
dev_set_promiscuity(dev, inc);
}
/* IFF_ALLMULTI 변경 시 멀티캐스트 필터 업데이트 */
if ((changes & IFF_ALLMULTI) && (dev->flags & IFF_UP)) {
int inc = (flags & IFF_ALLMULTI) ? 1 : -1;
dev_set_allmulti(dev, inc);
}
/* flags 갱신 후 notifier 체인으로 변경 알림 전파 */
dev->flags = (flags & (IFF_DEBUG | IFF_NOTRAILERS | IFF_NOARP |
IFF_DYNAMIC | IFF_MULTICAST | IFF_PORTSEL |
IFF_AUTOMEDIA)) |
(dev->flags & (IFF_UP | IFF_VOLATILE | IFF_PROMISC |
IFF_ALLMULTI));
/* NETDEV_CHANGE 이벤트 발송 → rtnetlink, bonding 등 수신 */
if (changes)
call_netdevice_notifiers(NETDEV_CHANGE, dev);
return ret;
}
priv_flags 상세 분석
priv_flags는 커널 내부 또는 드라이버 전용 플래그로, 사용자 공간에서 직접 변경할 수 없습니다.
주로 가상 디바이스 유형 식별이나 내부 동작 제어에 사용됩니다.
include/linux/netdevice.h에 IFF_* 이름으로 정의되어 있으며, 사용자 공간의 IFF_* 플래그와 네임스페이스가 다르다.
| 플래그 | 설명 | 설정 대상 |
|---|---|---|
IFF_802_1Q_VLAN |
802.1Q VLAN 디바이스 | vlan_setup()에서 설정 |
IFF_EBRIDGE |
이더넷 브릿지(Ethernet Bridge) 마스터 | br_dev_setup()에서 설정 |
IFF_BONDING |
본딩(Bonding) 마스터 | bond_setup()에서 설정 |
IFF_ISATAP |
ISATAP(Intra-Site Automatic Tunnel Addressing Protocol) 인터페이스 | IPv6 SIT 터널에서 설정 |
IFF_WAN_HDLC |
WAN HDLC 디바이스 | WAN 드라이버에서 설정 |
IFF_XMIT_DST_RELEASE |
TX 시 skb_dst 조기 해제 허용 |
대부분의 이더넷 드라이버, 메모리 절약 최적화 |
IFF_DONT_BRIDGE |
이 디바이스를 브릿지 포트로 사용 금지 | 특수 가상 디바이스에서 설정 |
IFF_TEAM |
팀(Team) 디바이스 마스터 | team_setup()에서 설정 |
IFF_SUPP_NOFCS |
FCS(Frame Check Sequence) 없이 송신 지원 | 하드웨어가 FCS 생략 가능한 NIC |
IFF_LIVE_ADDR_CHANGE |
인터페이스 활성 상태에서 MAC 주소 변경 허용 | macvlan, veth 등 |
IFF_MACVLAN |
macvlan 디바이스 | macvlan_common_setup()에서 설정 |
IFF_OPENVSWITCH |
Open vSwitch 데이터패스 포트 | OVS 내부 포트 생성 시 설정 |
IFF_TX_SKB_SHARING |
TX skb를 복사 없이 공유 허용 |
veth, loopback 등 zero-copy 최적화 |
IFF_UNICAST_FLT |
하드웨어 유니캐스트 주소 필터링 지원 | NIC 드라이버에서 설정, 무차별 모드 회피 |
IFF_LIVE_RENAME_OK |
인터페이스 활성 상태에서 이름 변경 허용 | 가상 디바이스(veth, bridge 등) |
reg_state 등록 상태 전이
reg_state는 net_device의 등록 생명주기(Registration Lifecycle)를 추적하는 상태 머신입니다.
register_netdevice()에서 NETREG_REGISTERED로 전이되고,
unregister_netdevice()를 거쳐 최종적으로 free_netdev()에서 해제됩니다.
각 상태 전이는 RTNL 락(Lock) 하에서만 수행됩니다.
/* 커널 소스 분석: include/linux/netdevice.h - reg_state 열거형 */
enum {
NETREG_UNINITIALIZED = 0, /* alloc_netdev() 직후 */
NETREG_REGISTERED, /* register_netdevice() 완료 */
NETREG_UNREGISTERING, /* unregister_netdevice() 호출됨 */
NETREG_UNREGISTERED, /* netdev_run_todo() 완료 */
NETREG_RELEASED, /* netdev_release() → free 가능 */
NETREG_DUMMY, /* init_dummy_netdev()용 더미 */
};
operstate (RFC 2863) 운영 상태
operstate는 RFC 2863에서 정의한 인터페이스 운영 상태를 나타냅니다.
커널은 링크 상태, 상위/하위 레이어 상태를 종합하여 이 값을 자동으로 관리하며,
linkwatch 서브시스템이 상태 변경 이벤트를 사용자 공간으로 전달합니다.
| 상태 | 값 | 의미 | 전이 조건 |
|---|---|---|---|
IF_OPER_UNKNOWN |
0 |
상태 미확인 | 초기값, 드라이버가 상태를 보고하지 않는 경우 |
IF_OPER_NOTPRESENT |
1 |
디바이스 물리적 부재 | 핫플러그(Hotplug) 제거 시 |
IF_OPER_DOWN |
2 |
인터페이스 비활성(Down) | IFF_UP 해제 또는 netif_carrier_off() |
IF_OPER_LOWERLAYERDOWN |
3 |
하위 레이어 비활성 | VLAN/bridge의 물리 포트 다운 시 |
IF_OPER_TESTING |
4 |
테스트 모드 | 루프백 테스트, 자가 진단 수행 중 |
IF_OPER_DORMANT |
5 |
대기(Dormant) 상태 | 802.1X 인증 대기, netif_dormant_on() |
IF_OPER_UP |
6 |
완전 동작 상태 | IFF_UP 설정 + netif_carrier_on() + 인증 완료 |
/* 커널 소스 분석: net/core/link_watch.c - operstate 결정 로직 */
static void rfc2863_policy(struct net_device *dev)
{
unsigned char operstate = IF_OPER_DOWN;
if (dev->flags & IFF_UP) {
if (!netif_carrier_ok(dev))
operstate = (dev->ifindex != LOOPBACK_IFINDEX)
? IF_OPER_DOWN : IF_OPER_UNKNOWN;
else if (netif_dormant(dev))
operstate = IF_OPER_DORMANT;
else
operstate = IF_OPER_UP;
}
if (dev->operstate != operstate) {
write_lock(&dev_base_lock);
dev->operstate = operstate;
write_unlock(&dev_base_lock);
}
}
struct net_device의 필드를 직접 대입하여 변경해서는 안 됩니다.
반드시 커널이 제공하는 API 함수(dev_change_flags(), dev_set_mtu(), dev_addr_set(),
netif_set_real_num_tx_queues() 등)를 사용해야 합니다.
이 함수들은 내부적으로 RTNL 락 확인, notifier 체인 호출, 상태 머신 전이, sysfs 동기화를 수행합니다.
직접 수정하면 커널 서브시스템 간 상태 불일치가 발생하여 패킷 손실, 교착 상태(Deadlock), 또는 커널 패닉(Kernel Panic)을 유발할 수 있습니다.
드라이버 수명주기와 필수 호출 순서
가장 흔한 실수는 등록/해제 순서를 뒤섞는 것입니다. 특히 NAPI, IRQ, queue start/stop 순서는 패킷(Packet) 손실과 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 구조체는 단순히 kmalloc()으로 할당하기에는 너무 크고 복잡합니다. 커널은 정렬(Alignment), 큐 배열, 사설 데이터(Private Data) 영역까지 한 번에 관리하는 전용 할당 함수를 제공하며, 등록/해제 과정에서도 sysfs, 해시 테이블, 알림 체인(Notifier Chain), 참조 카운트(Reference Count) 등 여러 서브시스템과 연동됩니다. 이 절에서는 이들 함수의 내부 구현을 커널 소스 수준에서 분석합니다.
alloc_netdev_mqs() 내부 분석
alloc_netdev_mqs()는 모든 네트워크 디바이스 할당의 핵심 함수입니다. alloc_etherdev(), alloc_netdev() 등 편의 매크로는 궁극적으로 이 함수를 호출합니다.
/* 커널 소스 분석: net/core/dev.c - alloc_netdev_mqs() */
struct net_device *alloc_netdev_mqs(
int sizeof_priv,
const char *name,
unsigned char name_assign_type,
void (*setup)(struct net_device *),
unsigned int txqs,
unsigned int rxqs)
{
struct net_device *dev;
unsigned int alloc_size;
/* 커널 소스 분석: 전체 할당 크기 계산
* net_device + private data를 NETDEV_ALIGN(32바이트)으로 정렬
* ____cacheline_aligned_in_smp로 캐시 라인 경계 맞춤 */
alloc_size = sizeof(struct net_device);
if (sizeof_priv) {
/* net_device 뒤에 private 영역 배치, 정렬 보장 */
alloc_size = ALIGN(alloc_size, NETDEV_ALIGN);
alloc_size += sizeof_priv;
}
/* 최종 크기도 NETDEV_ALIGN 단위로 올림 */
alloc_size += NETDEV_ALIGN - 1;
/* 커널 소스 분석: kvzalloc 사용 - kmalloc 아님!
* net_device가 수 KB에 달할 수 있어 vmalloc 폴백 필요
* GFP_KERNEL | __GFP_RETRY_MAYFAIL로 할당 실패 허용 */
dev = kvzalloc(alloc_size, GFP_KERNEL | __GFP_RETRY_MAYFAIL);
if (!dev)
return NULL;
/* 커널 소스 분석: 디바이스 주소 리스트 초기화 */
dev_addr_init(dev); /* MAC 주소 리스트 */
dev_mc_init(dev); /* 멀티캐스트 주소 리스트 */
dev_uc_init(dev); /* 유니캐스트 주소 리스트 */
dev_net_set(dev, &init_net); /* 현재 네트워크 네임스페이스 설정 */
dev->gso_max_size = GSO_LEGACY_MAX_SIZE;
dev->gso_max_segs = GSO_MAX_SEGS;
dev->upper_level = 1;
dev->lower_level = 1;
INIT_LIST_HEAD(&dev->napi_list);
INIT_LIST_HEAD(&dev->adj_list.upper);
INIT_LIST_HEAD(&dev->adj_list.lower);
dev->reg_state = NETREG_UNINITIALIZED;
dev->num_tx_queues = txqs;
dev->real_num_tx_queues = txqs;
/* 커널 소스 분석: TX 큐 배열 할당 */
if (netif_alloc_netdev_queues(dev))
goto free_all;
dev->num_rx_queues = rxqs;
dev->real_num_rx_queues = rxqs;
/* 커널 소스 분석: RX 큐 배열 할당 */
if (netif_alloc_rx_queues(dev))
goto free_all;
strcpy(dev->name, name);
dev->name_assign_type = name_assign_type;
/* 커널 소스 분석: setup 콜백 호출
* 이더넷이면 ether_setup(), CAN이면 can_setup() 등
* 프로토콜별 기본값(MTU, 헤더 길이, 플래그 등) 설정 */
setup(dev);
return dev;
free_all:
free_netdev(dev);
return NULL;
}
kvzalloc()인가? struct net_device는 약 2,500바이트 이상이며 사설 데이터를 포함하면 훨씬 커질 수 있습니다. kmalloc()은 물리적으로 연속된 메모리가 필요하지만 kvzalloc()은 물리 메모리가 단편화(Fragmentation)되었을 때 vmalloc()으로 자동 폴백합니다. z는 제로 초기화를 의미합니다.
아래 다이어그램은 alloc_netdev_mqs()가 할당하는 메모리 레이아웃(Memory Layout)을 보여줍니다. netdev_priv()은 net_device 구조체 바로 뒤의 정렬된 주소를 반환합니다.
ether_setup() 내부
ether_setup()은 이더넷(Ethernet) 디바이스의 기본 속성을 설정하는 setup 콜백입니다. alloc_etherdev() 계열 매크로가 alloc_netdev_mqs()에 이 함수를 전달합니다.
/* 커널 소스 분석: net/ethernet/eth.c - ether_setup() */
void ether_setup(struct net_device *dev)
{
dev->header_ops = ð_header_ops;
dev->type = ARPHRD_ETHER; /* ARP 하드웨어 타입: 이더넷 */
dev->hard_header_len = ETH_HLEN; /* 14바이트 (DST 6 + SRC 6 + Type 2) */
dev->min_header_len = ETH_HLEN;
dev->mtu = ETH_DATA_LEN; /* 1500바이트 */
dev->min_mtu = ETH_MIN_MTU; /* 68바이트 (IPv4 최소) */
dev->max_mtu = ETH_MAX_MTU; /* 65535바이트 */
dev->addr_len = ETH_ALEN; /* 6바이트 (MAC 주소 길이) */
dev->flags = IFF_BROADCAST | IFF_MULTICAST;
dev->priv_flags |= IFF_TX_SKB_SHARING;
/* 커널 소스 분석: 브로드캐스트 주소를 FF:FF:FF:FF:FF:FF로 설정 */
eth_broadcast_addr(dev->broadcast);
/* 커널 소스 분석: eth_header_ops는 eth_header(), eth_header_parse() 등 제공
* 이더넷 헤더 생성·파싱을 표준화 */
}
setup 콜백 이후 필요한 값만 덮어쓰면 됩니다. 예를 들어 점보 프레임(Jumbo Frame)을 지원하는 NIC 드라이버는 dev->max_mtu를 9000 이상으로 재설정합니다.
편의 할당 매크로 패밀리
커널은 프로토콜별로 alloc_netdev_mqs()를 감싸는 다양한 편의 매크로(Convenience Macro)를 제공합니다.
| 매크로 | 확장 결과 | TX/RX 큐 |
|---|---|---|
alloc_etherdev(sizeof_priv) |
alloc_etherdev_mqs(sizeof_priv, 1, 1) |
1 / 1 |
alloc_etherdev_mq(sizeof_priv, count) |
alloc_etherdev_mqs(sizeof_priv, count, count) |
count / count |
alloc_etherdev_mqs(sizeof_priv, txqs, rxqs) |
alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN, ether_setup, txqs, rxqs) |
txqs / rxqs |
alloc_netdev(sizeof_priv, name, assign, setup) |
alloc_netdev_mqs(sizeof_priv, name, assign, setup, 1, 1) |
1 / 1 |
alloc_candev(sizeof_priv, echo_skb_max) |
CAN 프레임워크 전용 래퍼, can_setup 콜백 사용 |
1 / 1 |
alloc_ieee80211_hw(priv_size, ops) |
무선랜(Wi-Fi) mac80211 프레임워크 전용 래퍼 |
프레임워크 결정 |
alloc_etherdev_mqs()의 txqs/rxqs를 NIC 하드웨어 큐 수와 일치시키면 CPU별 큐 매핑이 가능해져 lock 경합을 줄일 수 있습니다.
register_netdev() / register_netdevice() 내부
register_netdev()는 RTNL 잠금(Lock)을 자동으로 관리하는 편의 함수이며, 실제 등록 로직은 register_netdevice()에 구현되어 있습니다.
/* 커널 소스 분석: net/core/dev.c - register_netdev() */
int register_netdev(struct net_device *dev)
{
int err;
/* 커널 소스 분석: RTNL(Routing Netlink) 잠금 획득
* 네트워크 디바이스 등록/해제는 반드시 RTNL 잠금 하에서 수행 */
rtnl_lock();
err = register_netdevice(dev);
rtnl_unlock();
return err;
}
/* 커널 소스 분석: net/core/dev.c - register_netdevice() 핵심 흐름 */
int register_netdevice(struct net_device *dev)
{
struct net *net = dev_net(dev);
int ret;
ASSERT_RTNL(); /* RTNL 잠금 보유 확인 */
/* 커널 소스 분석: 이름 유효성 검증
* "eth%d" 같은 패턴이면 사용 가능한 번호 자동 할당
* 이름 충돌 시 에러 반환 */
ret = dev_get_valid_name(net, dev, dev->name);
if (ret < 0)
goto out;
/* ifindex 할당 (네임스페이스 내 고유) */
dev->ifindex = dev_new_index(net);
/* 커널 소스 분석: netdev_ops 유효성 검증 */
if (dev->netdev_ops->ndo_init) {
ret = dev->netdev_ops->ndo_init(dev);
if (ret)
goto out;
}
/* 커널 소스 분석: sysfs 등록 → /sys/class/net/<name>/ 생성 */
ret = netdev_register_kobject(dev);
if (ret)
goto err_uninit;
/* 커널 소스 분석: 상태 전이 - UNINITIALIZED → REGISTERED */
dev->reg_state = NETREG_REGISTERED;
/* 커널 소스 분석: 해시 테이블 삽입
* name_hlist: 이름 기반 검색용 (dev_get_by_name)
* index_hlist: 인덱스 기반 검색용 (dev_get_by_index) */
list_netdevice(dev);
/* 커널 소스 분석: 등록 알림 발송
* 방화벽, 라우팅, netfilter 등 관심 모듈에 통지 */
call_netdevice_notifiers(NETDEV_REGISTER, dev);
return 0;
err_uninit:
if (dev->netdev_ops->ndo_uninit)
dev->netdev_ops->ndo_uninit(dev);
out:
return ret;
}
unregister_netdev() 내부와 netdev_run_todo()
네트워크 디바이스의 해제(Unregistration)는 등록보다 복잡합니다. 참조 카운트가 0이 될 때까지 실제 해제를 지연시키는 2단계 프로세스(Two-Phase Process)를 사용합니다.
/* 커널 소스 분석: net/core/dev.c - unregister_netdev() */
void unregister_netdev(struct net_device *dev)
{
rtnl_lock();
unregister_netdevice_queue(dev, NULL);
rtnl_unlock();
}
/* 커널 소스 분석: net/core/dev.c - unregister_netdevice_queue()
* RTNL 잠금 보유 상태에서 호출됨 */
void unregister_netdevice_queue(struct net_device *dev,
struct list_head *head)
{
ASSERT_RTNL();
/* 커널 소스 분석: 1단계 - 해제 마킹 */
dev->reg_state = NETREG_UNREGISTERING;
/* 인터페이스 DOWN 처리 */
if (dev->flags & IFF_UP)
dev_close(dev);
/* 해시 테이블에서 제거 */
unlist_netdevice(dev);
/* 알림 발송: 관련 모듈이 참조를 해제하도록 요청 */
call_netdevice_notifiers(NETDEV_UNREGISTER, dev);
/* 커널 소스 분석: 2단계 - todo 리스트에 추가
* RTNL 잠금 범위 내에서는 즉시 해제하지 않음
* rtnl_unlock() 시 netdev_run_todo() 자동 호출 */
if (head) {
list_add_tail(&dev->todo_list, head);
} else {
list_add_tail(&dev->todo_list, &net_todo_list);
}
}
/* 커널 소스 분석: net/core/dev.c - netdev_run_todo()
* rtnl_unlock() 내부에서 자동 호출됨 */
void netdev_run_todo(void)
{
struct net_device *dev, *tmp;
struct list_head list;
/* todo 리스트를 로컬 리스트로 이동 (RTNL 밖에서 처리) */
list_replace_init(&net_todo_list, &list);
list_for_each_entry_safe(dev, tmp, &list, todo_list) {
/* 커널 소스 분석: 참조 카운트가 0이 될 때까지 대기
* RCU grace period 경과 + 모든 dev_put() 완료 대기
* 타임아웃 시 경고 메시지 출력 */
netdev_wait_allrefs_any(dev);
/* sysfs 제거 */
netdev_unregister_kobject(dev);
/* 최종 상태 전이 */
dev->reg_state = NETREG_UNREGISTERED;
/* ndo_uninit 콜백 호출 */
if (dev->netdev_ops->ndo_uninit)
dev->netdev_ops->ndo_uninit(dev);
/* NETDEV_UNREGISTER_FINAL 알림 (완전 해제 완료) */
call_netdevice_notifiers(NETDEV_UNREGISTER_FINAL, dev);
}
}
net_device를 참조하는 코드는 반드시 dev_hold()/dev_put() 쌍을 지켜야 합니다. netdev_wait_allrefs_any()는 참조 카운트가 0이 될 때까지 최대 10초간 대기하며, 이후에도 0이 아니면 "unregister_netdevice: waiting for %s to become free" 경고를 출력하고 계속 대기합니다. RCU 읽기 측(Read-Side)에서는 dev_get_by_name_rcu()나 dev_get_by_index_rcu()를 사용하면 dev_hold() 없이 안전하게 참조할 수 있지만, RCU 읽기 구간(rcu_read_lock/rcu_read_unlock)을 벗어나면 포인터가 무효화될 수 있습니다.
free_netdev() 내부
free_netdev()는 alloc_netdev_mqs()가 할당한 모든 리소스를 해제합니다. 반드시 unregister_netdev() 이후에 호출해야 합니다.
/* 커널 소스 분석: net/core/dev.c - free_netdev() */
void free_netdev(struct net_device *dev)
{
/* 커널 소스 분석: 등록된 상태에서 free_netdev 호출 시 경고 */
if (dev->reg_state == NETREG_REGISTERED)
WARN_ON(1); /* BUG: unregister 없이 free 시도 */
/* 커널 소스 분석: TX/RX 큐 해제
* alloc_netdev_mqs()에서 별도 kmalloc_array로 할당했으므로
* net_device 본체와 별도로 해제 */
netif_free_tx_queues(dev);
netif_free_rx_queues(dev);
/* MAC 주소 리스트 해제 */
dev_addr_flush(dev);
/* XPS(Transmit Packet Steering) CPU/RX 맵 해제 */
netif_reset_xps_queues_gt(dev, 0);
/* 커널 소스 분석: 최종 상태 전이 */
dev->reg_state = NETREG_RELEASED;
/* 커널 소스 분석: net_device 메모리 해제
* kvzalloc으로 할당했으므로 kvfree 사용
* (내부적으로 vmalloc/kmalloc 여부 판단) */
kvfree(dev);
}
free_netdev()는 반드시 unregister_netdev() 완료 이후에 호출해야 합니다. 등록된 상태에서 메모리를 해제하면 해시 테이블에 댕글링 포인터(Dangling Pointer)가 남아 커널 패닉을 유발합니다. 일반적인 드라이버 제거 순서: unregister_netdev(dev) → free_netdev(dev).
dev_get_by_name() / dev_get_by_index() 내부
커널은 네트워크 디바이스를 이름(Name) 또는 인덱스(Index)로 빠르게 검색하기 위해 네임스페이스(Namespace)별 해시 테이블을 유지합니다.
/* 커널 소스 분석: net/core/dev.c - dev_get_by_name() */
struct net_device *dev_get_by_name(struct net *net, const char *name)
{
struct net_device *dev;
/* 커널 소스 분석: RCU 읽기 잠금으로 해시 테이블 검색 */
rcu_read_lock();
dev = dev_get_by_name_rcu(net, name);
/* 커널 소스 분석: 찾았으면 참조 카운트 증가
* 호출자가 dev_put()으로 해제해야 함 */
if (dev)
dev_hold(dev);
rcu_read_unlock();
return dev;
}
/* 커널 소스 분석: net/core/dev.c - dev_get_by_name_rcu()
* RCU 읽기 구간 내에서만 유효한 포인터 반환
* 참조 카운트를 증가시키지 않음 → 가볍지만 수명 제한 */
struct net_device *dev_get_by_name_rcu(struct net *net, const char *name)
{
struct netdev_name_node *node;
unsigned int hash;
/* 커널 소스 분석: 이름을 해시 키로 변환 후 해시 테이블 검색
* full_name_hash()는 djb2 기반 해시 함수 */
hash = full_name_hash(net, name, strnlen(name, IFNAMSIZ));
hlist_for_each_entry_rcu(node, &net->dev_name_head[hash & (NETDEV_HASHENTRIES - 1)],
hlist) {
if (!strcmp(node->name, name))
return node->dev;
}
return NULL;
}
/* 커널 소스 분석: net/core/dev.c - dev_get_by_index()
* 이름 기반과 동일 패턴: RCU 검색 + dev_hold() */
struct net_device *dev_get_by_index(struct net *net, int ifindex)
{
struct net_device *dev;
rcu_read_lock();
dev = dev_get_by_index_rcu(net, ifindex);
if (dev)
dev_hold(dev);
rcu_read_unlock();
return dev;
}
| 함수 | 검색 키 | 해시 테이블 | 참조 카운트 | 사용 맥락 |
|---|---|---|---|---|
dev_get_by_name() |
이름 (예: "eth0") | dev_name_head |
증가 (dev_hold) |
프로세스 컨텍스트, 사용 후 dev_put() 필수 |
dev_get_by_name_rcu() |
이름 | dev_name_head |
증가하지 않음 | RCU 읽기 구간 내 전용 |
dev_get_by_index() |
ifindex (정수) | dev_index_head |
증가 (dev_hold) |
프로세스 컨텍스트, 사용 후 dev_put() 필수 |
dev_get_by_index_rcu() |
ifindex | dev_index_head |
증가하지 않음 | RCU 읽기 구간 내 전용 |
_rcu 변형이 더 효율적입니다. dev_hold()/dev_put()은 원자적 연산(Atomic Operation)이므로 핫 경로(Hot Path)에서는 오버헤드가 됩니다.
netdev_priv() 구현
netdev_priv()은 커널에서 가장 자주 호출되는 네트워크 인라인 함수(Inline Function) 중 하나입니다. net_device 포인터로부터 드라이버 사설 데이터의 시작 주소를 산출합니다.
/* 커널 소스 분석: include/linux/netdevice.h - netdev_priv() */
static inline void *netdev_priv(const struct net_device *dev)
{
/* 커널 소스 분석: 단순 포인터 산술
* net_device 구조체 바로 뒤, NETDEV_ALIGN 경계에 위치
* 별도 메모리 할당 없이 한 블록 안에서 오프셋 계산 */
return (char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN);
}
/* 커널 소스 분석: NETDEV_ALIGN 정의 */
#define NETDEV_ALIGN 32
/* SMP 캐시 라인 크기(보통 64바이트)와 별개로,
* net_device/private 경계를 32바이트로 정렬하여
* 하드웨어 DMA 엔진의 정렬 요구사항 충족 */
드라이버에서의 전형적인 사용 패턴은 다음과 같습니다.
/* 커널 소스 분석: 드라이버에서 netdev_priv() 사용 예시 */
struct my_priv {
struct pci_dev *pdev;
void __iomem *mmio_base;
struct napi_struct napi;
spinlock_t lock;
/* ... 드라이버별 필드 */
};
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *ndev;
struct my_priv *priv;
/* sizeof(struct my_priv)만큼 private 영역 확보 */
ndev = alloc_etherdev(sizeof(struct my_priv));
if (!ndev)
return -ENOMEM;
/* 별도 할당 없이 포인터 산술로 즉시 접근 가능
* → 할당 오버헤드 제로, 캐시 지역성 우수 */
priv = netdev_priv(ndev);
priv->pdev = pdev;
spin_lock_init(&priv->lock);
/* ... 하드웨어 초기화 */
}
net_device와 같은 메모리 블록에 배치함으로써 (1) 별도 kmalloc()이 불필요하고, (2) net_device와 사설 데이터가 인접하여 CPU 캐시 지역성(Cache Locality)이 향상되며, (3) 해제 시 kvfree() 한 번으로 모두 해제됩니다. 이 패턴은 네트워크 디바이스뿐 아니라 ieee80211_hw, ib_device 등 다른 서브시스템에서도 동일하게 사용됩니다.
핵심 콜백: net_device_ops 전체 분석
net_device_ops는 커널 6.x 기준 약 60개 이상의 콜백 함수 포인터(Callback Function Pointer)를 정의하는 거대한 구조체입니다. 네트워크 드라이버의 모든 동작 — 장치 수명주기, 데이터 전송, 주소 필터링, 오프로드 설정, 가상화 지원 — 이 이 구조체를 통해 커널 네트워크 스택과 계약(Contract)을 맺습니다. 드라이버가 모든 콜백을 구현할 필요는 없지만, 구현하는 각 콜백의 호출 컨텍스트(RTNL 보호 여부, softirq/process context), 반환값 계약(소유권 이전 규칙), 동시성 보장(어떤 락이 잡혀 있는지)을 정확히 이해해야 합니다. 잘못된 컨텍스트 가정은 데드락, 메모리 누수, 데이터 손상의 직접적 원인이 됩니다.
net_device_ops 구조체는 include/linux/netdevice.h에 정의되어 있으며, 각 콜백의 호출 지점은 net/core/dev.c, net/core/dev_ioctl.c, net/core/rtnetlink.c 등에 분산되어 있습니다.
그룹별 콜백 종합 테이블
아래 테이블은 net_device_ops의 주요 콜백을 기능 그룹별로 분류하고, 각 콜백의 호출 시점, 실행 컨텍스트, 핵심 책임, 필수 여부를 정리한 것입니다.
그룹 A — 수명주기(Lifecycle)
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_init | register_netdevice() | RTNL held, process | 추가 초기화 (alloc_netdev 이후 보충) | 선택 |
ndo_uninit | unregister_netdevice() | RTNL held, process | init에서 할당한 자원 정리 | 선택 |
ndo_open | dev_open() — ip link set up | RTNL held, process | HW 활성화, IRQ 요청, NAPI 시작 | 필수 |
ndo_stop | dev_close() — ip link set down | RTNL held, process | HW 비활성화, IRQ 해제, NAPI 정지 | 필수 |
그룹 B — 데이터 경로(Data Path)
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_start_xmit | dev_hard_start_xmit() | softirq 또는 process, per-queue _xmit lock held | skb를 HW TX 링에 전달 | 필수 |
ndo_select_queue | netdev_pick_tx() | process/softirq | TX 큐 인덱스 선택 (멀티큐) | 선택 |
ndo_get_stats64 | dev_get_stats() | RTNL 또는 RCU read | per-CPU 통계를 rtnl_link_stats64에 집계 | 선택 |
그룹 C — 주소/필터 설정
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_set_rx_mode | __dev_set_rx_mode() | netif_addr_lock held (spin), BH disabled | MC/UC 필터 HW 프로그래밍 | 권장 |
ndo_set_mac_address | dev_set_mac_address() | RTNL held, process | MAC 주소 변경, HW 반영 | 선택 |
ndo_validate_addr | dev_open() 내부 | RTNL held, process | MAC 주소 유효성 검증 | 선택 |
ndo_change_mtu | dev_set_mtu() | RTNL held, process | MTU 변경 시 HW/ring 재설정 | 선택 |
그룹 D — VLAN
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_vlan_rx_add_vid | vlan_vid_add() | RTNL held, process | HW VLAN 필터에 VID 등록 | VLAN 필터 시 |
ndo_vlan_rx_kill_vid | vlan_vid_del() | RTNL held, process | HW VLAN 필터에서 VID 제거 | VLAN 필터 시 |
그룹 E — Feature/오프로드(Offload)
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_fix_features | netdev_update_features() | RTNL held, process | HW 제약에 따른 feature 조합 강제 보정 | 선택 |
ndo_set_features | netdev_update_features() | RTNL held, process | 변경된 feature를 HW 레지스터에 적용 | 선택 |
ndo_features_check | netif_skb_features() | data path (softirq/process) | per-skb feature 호환성 검사, SW fallback 결정 | 선택 |
그룹 F — TC/XDP 오프로드
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_setup_tc | TC qdisc/filter 설정 시 | RTNL held, process | TC 오프로드 구성 (mqprio, flower 등) | 선택 |
ndo_bpf | XDP 프로그램 attach/detach | RTNL held, process | XDP 프로그램 설치, HW 오프로드 | XDP 시 |
ndo_xdp_xmit | XDP_TX / XDP_REDIRECT 처리 | NAPI context (softirq) | XDP 프레임 배치 전송, doorbell 최적화 | XDP 시 |
그룹 G — 브릿지/FDB
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_fdb_add | FDB 엔트리 추가 요청 | RTNL held, process | HW FDB 테이블에 MAC 등록 | switchdev 시 |
ndo_fdb_del | FDB 엔트리 삭제 요청 | RTNL held, process | HW FDB 테이블에서 MAC 제거 | switchdev 시 |
ndo_fdb_dump | FDB 전체 덤프 요청 | RTNL held, process | HW FDB 엔트리 열거 | switchdev 시 |
ndo_bridge_setlink | 브릿지 포트 설정 변경 | RTNL held, process | 브릿지 모드, learning 등 HW 설정 | 선택 |
ndo_bridge_getlink | 브릿지 포트 설정 조회 | RTNL held, process | 현재 브릿지 설정 반환 | 선택 |
그룹 H — 에러/복구
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_tx_timeout | TX watchdog 타임아웃 | process (workqueue) | TX 경로 복구, HW 리셋 | 권장 |
ndo_change_carrier | dev_change_carrier() | RTNL held, process | carrier 상태 수동 변경 (가상 장치) | 선택 |
그룹 I — ioctl/확장
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_eth_ioctl | SIOCSHWTSTAMP 등 이더넷 ioctl | RTNL held, process | HW 타임스탬프 설정, PHY ioctl 위임 | 선택 |
ndo_siocdevprivate | 드라이버 전용 ioctl | RTNL held, process | 레거시 드라이버별 커스텀 ioctl 처리 | 선택 |
ndo_get_phys_port_id | netlink 쿼리 | RTNL held, process | 물리 포트 식별자 반환 (switchdev) | switchdev 시 |
ndo_get_phys_port_name | netlink 쿼리 | RTNL held, process | 물리 포트 이름 반환 (representor) | switchdev 시 |
ndo_get_port_parent_id | netlink 쿼리 | RTNL 또는 RCU | 부모 스위치 ID 반환 (switchdev) | switchdev 시 |
그룹 J — 가상화(SR-IOV)
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_set_vf_mac | ip link set vf N mac | RTNL held, process | VF의 MAC 주소 설정 | SR-IOV 시 |
ndo_set_vf_vlan | ip link set vf N vlan | RTNL held, process | VF의 VLAN 태그 설정 | SR-IOV 시 |
ndo_set_vf_rate | ip link set vf N rate | RTNL held, process | VF의 TX rate 제한 설정 | SR-IOV 시 |
ndo_set_vf_spoofchk | ip link set vf N spoofchk | RTNL held, process | VF의 MAC/VLAN spoof 검사 활성화 | SR-IOV 시 |
ndo_get_vf_config | VF 설정 조회 | RTNL held, process | VF의 현재 설정 반환 | SR-IOV 시 |
그룹 K — 네임스페이스/링크
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_get_iflink | netlink 쿼리, dev_get_iflink() | RCU 또는 RTNL | 실제 하위 장치 ifindex 반환 (VLAN, tunnel, macvlan 등) | 가상장치 시 |
ndo_open / ndo_stop 상세 분석
ndo_open()은 네트워크 장치가 "UP" 상태로 전환될 때 호출되며, 하드웨어를 완전한 동작 상태로 만드는 책임을 집니다. 이 콜백 내부의 초기화 순서는 엄격하게 지켜야 하며, 순서가 뒤바뀌면 인터럽트가 발생했을 때 아직 준비되지 않은 자료구조에 접근하여 커널 패닉이 발생할 수 있습니다.
/* 커널 소스 분석: ndo_open 구현의 올바른 초기화 순서 */
static int mydrv_open(struct net_device *dev)
{
struct mydrv_priv *priv = netdev_priv(dev);
int err;
/* 1단계: DMA 링 버퍼 할당
* IRQ 요청 전에 반드시 완료해야 함.
* 인터럽트 핸들러가 참조할 descriptor ring이
* 아직 없으면 NULL 역참조 발생 */
err = mydrv_alloc_rings(priv);
if (err)
return err;
/* 2단계: HW 초기화 — MAC, 필터, DMA 엔진 활성화
* 링 버퍼 주소를 HW 레지스터에 기록 */
mydrv_hw_init(priv);
/* 3단계: NAPI 활성화
* IRQ 핸들러가 napi_schedule()을 호출하기 전에
* NAPI가 활성화 상태여야 함.
* napi_enable() 없이 napi_schedule() → BUG_ON */
for (int i = 0; i < priv->num_queues; i++)
napi_enable(&priv->queues[i].napi);
/* 4단계: IRQ 요청 (인터럽트 활성화)
* NAPI 활성화 이후에 요청해야 안전.
* MSI-X: 큐별 개별 IRQ 할당 */
err = mydrv_request_irqs(priv);
if (err)
goto err_disable_napi;
/* 5단계: carrier 상태 설정
* PHY 링크가 확인되면 carrier on */
if (mydrv_link_is_up(priv))
netif_carrier_on(dev);
else
netif_carrier_off(dev);
/* 6단계: TX 큐 시작 — 반드시 마지막
* 모든 준비가 완료된 후에만 패킷 수신/송신 허용.
* 이 시점부터 ndo_start_xmit이 호출될 수 있음 */
netif_tx_start_all_queues(dev);
return 0;
err_disable_napi:
for (int i = 0; i < priv->num_queues; i++)
napi_disable(&priv->queues[i].napi);
mydrv_free_rings(priv);
return err;
}
ndo_stop()은 ndo_open()의 정확한 역순으로 자원을 해제해야 합니다. 순서를 지키지 않으면 해제 중에 인터럽트가 발생하여 이미 해제된 메모리에 접근하는 use-after-free가 발생합니다.
/* 커널 소스 분석: ndo_stop 구현의 올바른 해제 순서
* ndo_open의 정확한 역순이어야 함 */
static int mydrv_stop(struct net_device *dev)
{
struct mydrv_priv *priv = netdev_priv(dev);
/* 1단계: TX 큐 정지 — 가장 먼저
* 새로운 패킷이 ndo_start_xmit으로 들어오는 것을 차단 */
netif_tx_stop_all_queues(dev);
/* 2단계: carrier off */
netif_carrier_off(dev);
/* 3단계: IRQ 해제
* 더 이상 새로운 인터럽트가 발생하지 않도록 보장.
* free_irq()는 현재 실행 중인 핸들러 완료를 기다림 */
mydrv_free_irqs(priv);
/* 4단계: NAPI 비활성화
* napi_disable()은 진행 중인 poll 완료를 기다림.
* IRQ 해제 후 호출해야 새로운 napi_schedule 방지 */
for (int i = 0; i < priv->num_queues; i++)
napi_disable(&priv->queues[i].napi);
/* 5단계: HW 비활성화 — DMA 엔진 정지
* NAPI 비활성화 후에 해야 DMA 완료 이벤트가
* poll에 의해 정리된 후 엔진을 멈춤 */
mydrv_hw_shutdown(priv);
/* 6단계: DMA 링 해제 — 가장 마지막
* HW와 NAPI 모두 정지한 후에만 안전하게 해제 가능 */
mydrv_free_rings(priv);
return 0;
}
ndo_stop()에서 napi_disable()을 free_irq()보다 먼저 호출하면, IRQ 핸들러가 napi_schedule()을 호출한 후 실제 poll이 실행되기 전에 NAPI가 비활성화되어, 대기 중인 패킷이 영원히 처리되지 않는 문제가 발생할 수 있습니다. 반드시 IRQ를 먼저 해제하라.
ndo_start_xmit 심층 분석
ndo_start_xmit()은 네트워크 스택의 TX(송신) 경로 최종 진입점으로, 상위 계층에서 구성된 sk_buff를 하드웨어 TX 링에 전달하는 역할을 합니다. 이 콜백은 성능 최적화의 핵심이며, 호출 빈도가 매우 높아 모든 코드 경로를 최적화해야 합니다.
커널 소스 분석: ndo_start_xmit 호출 경로
상위 계층 (TCP/UDP/IP)
│
▼
dev_queue_xmit(skb) ← net/core/dev.c
│
▼
__dev_queue_xmit(skb)
│
├─ netdev_pick_tx() ← TX 큐 선택 (ndo_select_queue)
│
▼
__dev_xmit_skb(skb, q, dev, txq)
│
├─ qdisc가 있으면 → qdisc 큐에 enqueue
│ → sch_direct_xmit() 에서 dequeue 후 전송
├─ noqueue (loopback 등) → 직접 전송
│
▼
dev_hard_start_xmit(skb, dev, txq)
│
▼
netdev_start_xmit(skb, dev, txq, more)
│
▼
ops->ndo_start_xmit(skb, dev) ← 드라이버 콜백 진입
반환값 계약(Return Value Contract)은 다음과 같습니다:
/* 커널 소스 분석: ndo_start_xmit 반환값 계약 */
/* NETDEV_TX_OK (0):
* - skb 소유권이 드라이버로 완전히 이전됨
* - 드라이버가 skb를 consume하거나, DMA 완료 후 free 해야 함
* - 호출자는 skb를 더 이상 참조하지 않음
* - 실패 시에도 skb를 free하고 TX_OK를 반환하는 것이 일반적 */
#define NETDEV_TX_OK 0x00
/* NETDEV_TX_BUSY:
* - skb 소유권이 호출자에게 유지됨 (드라이버가 free하면 안 됨)
* - 큐가 다시 시도해야 함을 의미
* - 현대 드라이버에서는 사용을 강력히 비권장
* - 대신: 큐가 차면 netif_tx_stop_queue()를 호출하고
* TX_OK를 반환, TX 완료 인터럽트에서 wake */
#define NETDEV_TX_BUSY 0x10
/* 커널 소스 분석: ndo_start_xmit 구현 패턴 */
static netdev_tx_t mydrv_start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct mydrv_priv *priv = netdev_priv(dev);
struct mydrv_tx_ring *ring;
u16 qid = skb_get_queue_mapping(skb);
ring = &priv->tx_rings[qid];
/* linearization: HW가 scatter-gather를 지원하지 않거나
* fragment 수가 HW descriptor 한계를 초과할 때 필요 */
if (skb_shinfo(skb)->nr_frags > ring->max_frags) {
if (skb_linearize(skb)) {
dev_kfree_skb_any(skb);
dev->stats.tx_dropped++;
return NETDEV_TX_OK; /* skb 소비했으므로 TX_OK */
}
}
/* TX descriptor가 부족하면 큐 정지 */
if (mydrv_tx_avail(ring) < mydrv_tx_desc_needed(skb)) {
netif_tx_stop_queue(netdev_get_tx_queue(dev, qid));
/* 경쟁 조건 방지: 정지 직후 완료 인터럽트로
* 공간이 확보되었을 수 있으므로 재확인 */
smp_mb();
if (mydrv_tx_avail(ring) >= mydrv_tx_desc_needed(skb)) {
netif_tx_wake_queue(netdev_get_tx_queue(dev, qid));
} else {
return NETDEV_TX_BUSY;
}
}
/* DMA 매핑 및 descriptor 채우기 */
mydrv_tx_map(ring, skb);
/* skb->xmit_more (netdev_xmit_more()):
* true면 뒤에 더 많은 패킷이 올 예정이므로
* doorbell(HW 알림)을 지연하여 배치 처리.
* false면 즉시 doorbell을 울려 HW에 전송 시작 알림 */
if (!netdev_xmit_more())
mydrv_ring_doorbell(ring);
return NETDEV_TX_OK;
}
netdev_xmit_more()를 활용한 doorbell 배치(Batching)는 특히 고속 NIC에서 중요합니다. PCIe MMIO 쓰기는 수백 나노초가 소요되므로, 여러 패킷의 descriptor를 한 번에 쓴 뒤 마지막에만 doorbell을 울리면 처리량이 크게 향상됩니다.
ndo_set_rx_mode 상세 분석
ndo_set_rx_mode()는 네트워크 장치의 수신 필터(Receive Filter)를 업데이트할 때 호출됩니다. 멀티캐스트 그룹 가입/탈퇴, 유니캐스트 주소 추가, 무차별 모드(Promiscuous Mode) 전환 시 커널이 이 콜백을 호출하여 드라이버에 HW 필터 재프로그래밍을 요청합니다.
ndo_set_rx_mode()는 netif_addr_lock(spinlock) 하에서 BH disabled 상태로 호출됩니다. 따라서 슬립이 불가능하며, mutex 획득, 메모리 할당(GFP_KERNEL), 느린 I/O 등을 수행할 수 없습니다. HW 레지스터 접근만 가능하며, 느린 작업이 필요하면 workqueue로 지연시켜야 합니다.
/* 커널 소스 분석: ndo_set_rx_mode 구현 패턴 */
static void mydrv_set_rx_mode(struct net_device *dev)
{
struct mydrv_priv *priv = netdev_priv(dev);
u32 filter_flags = 0;
/* 1. 전역 필터 플래그 확인
* IFF_PROMISC: 모든 프레임 수신 (tcpdump 등)
* IFF_ALLMULTI: 모든 멀티캐스트 프레임 수신 */
if (dev->flags & IFF_PROMISC) {
filter_flags |= MYDRV_FILTER_PROMISC;
goto apply;
}
if (dev->flags & IFF_ALLMULTI) {
filter_flags |= MYDRV_FILTER_ALLMULTI;
} else {
/* 2. 멀티캐스트 주소 목록 순회
* dev->mc: 멀티캐스트 주소 리스트 (netdev_hw_addr_list)
* HW 필터 테이블 용량 초과 시 ALLMULTI fallback */
struct netdev_hw_addr *ha;
int mc_count = 0;
netdev_for_each_mc_addr(ha, dev) {
if (mc_count >= MYDRV_MAX_MC_ENTRIES) {
/* HW 필터 테이블 가득 참 → promiscuous fallback */
filter_flags |= MYDRV_FILTER_ALLMULTI;
break;
}
mydrv_write_mc_filter(priv, mc_count, ha->addr);
mc_count++;
}
mydrv_set_mc_count(priv, mc_count);
}
/* 3. 유니캐스트 주소 목록 순회
* dev->uc: 보조 유니캐스트 주소 리스트
* macvlan 등에서 여러 유니캐스트 주소를 등록 */
{
struct netdev_hw_addr *ha;
int uc_count = 0;
netdev_for_each_uc_addr(ha, dev) {
if (uc_count >= MYDRV_MAX_UC_ENTRIES) {
filter_flags |= MYDRV_FILTER_PROMISC;
break;
}
mydrv_write_uc_filter(priv, uc_count, ha->addr);
uc_count++;
}
mydrv_set_uc_count(priv, uc_count);
}
apply:
/* 4. 필터 플래그를 HW 레지스터에 기록 */
mydrv_write_filter_flags(priv, filter_flags);
}
ndo_change_mtu 상세 분석
ndo_change_mtu()는 사용자가 ip link set dev eth0 mtu 9000과 같이 MTU를 변경할 때 호출됩니다. 커널 코어(dev_set_mtu())가 dev->min_mtu / dev->max_mtu 범위 검증을 먼저 수행하므로, 드라이버는 HW 특화 검증과 실제 적용만 담당하면 됩니다.
/* 커널 소스 분석: dev_set_mtu() → ndo_change_mtu 호출 경로
* net/core/dev.c */
static int dev_set_mtu_ext(struct net_device *dev, int new_mtu)
{
/* 코어가 min_mtu / max_mtu 검증을 수행 */
if (new_mtu < dev->min_mtu || new_mtu > dev->max_mtu)
return -EINVAL;
if (ops->ndo_change_mtu)
err = ops->ndo_change_mtu(dev, new_mtu);
else
WRITE_ONCE(dev->mtu, new_mtu); /* 콜백 NULL → 자동 적용 */
/* ... */
}
/* 커널 소스 분석: ndo_change_mtu 구현 패턴
* running 상태에서 MTU 변경 시 stop/restart 필요 */
static int mydrv_change_mtu(struct net_device *dev, int new_mtu)
{
struct mydrv_priv *priv = netdev_priv(dev);
bool running = netif_running(dev);
/* HW 특화 검증: 예를 들어 특정 MTU는 alignment 필요 */
if (new_mtu % 4 != 0)
return -EINVAL;
/* running 상태에서는 ring 크기 변경이 필요할 수 있음.
* 안전하게 stop → 변경 → restart 수행 */
if (running)
mydrv_stop(dev);
/* RX 버퍼 크기를 새 MTU에 맞게 재계산 */
priv->rx_buf_size = SKB_DATA_ALIGN(new_mtu + ETH_HLEN +
ETH_FCS_LEN + VLAN_HLEN);
WRITE_ONCE(dev->mtu, new_mtu);
if (running)
mydrv_open(dev);
return 0;
}
alloc_etherdev()은 min_mtu = ETH_MIN_MTU (68), max_mtu = ETH_MAX_MTU (65535)로 초기화합니다. 드라이버는 dev->min_mtu와 dev->max_mtu를 HW 실제 한계에 맞게 설정해야 합니다 (예: Jumbo Frame 미지원 시 dev->max_mtu = ETH_DATA_LEN).
ndo_get_stats64 상세 분석
ndo_get_stats64()는 ip -s link show, /proc/net/dev 조회, SNMP MIB 수집 등 여러 경로에서 네트워크 통계를 요청할 때 호출됩니다. 현대 드라이버는 per-CPU 통계를 사용하여 캐시라인 경합(Cache Line Bouncing)을 방지합니다.
/* 커널 소스 분석: per-CPU 통계 구조체와 u64_stats_sync */
struct mydrv_stats {
u64 rx_packets;
u64 rx_bytes;
u64 tx_packets;
u64 tx_bytes;
struct u64_stats_sync syncp; /* 32비트 arch에서 64비트 원자적 읽기 보장 */
};
/* 드라이버 초기화에서:
* priv->pcpu_stats = netdev_alloc_pcpu_stats(struct mydrv_stats); */
/* TX 경로에서 통계 갱신 (per-CPU, 락 불필요) */
static void mydrv_tx_complete(struct mydrv_priv *priv,
unsigned int bytes)
{
struct mydrv_stats *stats = this_cpu_ptr(priv->pcpu_stats);
u64_stats_update_begin(&stats->syncp);
stats->tx_packets++;
stats->tx_bytes += bytes;
u64_stats_update_end(&stats->syncp);
}
/* 커널 소스 분석: ndo_get_stats64 구현
* 모든 CPU의 per-CPU 통계를 합산 */
static void mydrv_get_stats64(struct net_device *dev,
struct rtnl_link_stats64 *s)
{
struct mydrv_priv *priv = netdev_priv(dev);
int cpu;
for_each_possible_cpu(cpu) {
struct mydrv_stats *stats = per_cpu_ptr(priv->pcpu_stats, cpu);
u64 rx_packets, rx_bytes, tx_packets, tx_bytes;
unsigned int start;
/* u64_stats_fetch_begin: 32비트 아키텍처에서
* 64비트 값의 상위/하위 32비트가 일관되게 읽히도록 보장.
* 64비트 arch에서는 no-op으로 최적화됨 */
do {
start = u64_stats_fetch_begin(&stats->syncp);
rx_packets = stats->rx_packets;
rx_bytes = stats->rx_bytes;
tx_packets = stats->tx_packets;
tx_bytes = stats->tx_bytes;
} while (u64_stats_fetch_retry(&stats->syncp, start));
s->rx_packets += rx_packets;
s->rx_bytes += rx_bytes;
s->tx_packets += tx_packets;
s->tx_bytes += tx_bytes;
}
}
dev->pcpu_stat_type = NETDEV_PCPU_STAT_TSTATS를 설정하면, 코어가 자동으로 per-CPU 통계를 수집하여 ndo_get_stats64 구현 없이도 기본 통계(rx/tx packets/bytes)를 제공합니다. dev_get_tstats64()가 기본 핸들러로 사용됩니다.
ndo_features_check / ndo_fix_features / ndo_set_features 상호작용
네트워크 오프로드 기능(Feature)의 변경은 세 가지 콜백이 협력하여 처리됩니다. 이 세 콜백은 서로 다른 시점에 호출되며, 각각의 역할이 명확히 구분됩니다.
/* 커널 소스 분석: ndo_fix_features — HW 제약 강제 적용 */
static netdev_features_t mydrv_fix_features(struct net_device *dev,
netdev_features_t features)
{
/* HW 제약: TSO가 없으면 TX checksum offload 불가
* TSO는 CSUM을 전제로 하므로, CSUM이 꺼지면 TSO도 꺼야 함 */
if (!(features & NETIF_F_HW_CSUM))
features &= ~(NETIF_F_TSO | NETIF_F_TSO6);
/* HW 제약: scatter-gather 없으면 TSO 불가 */
if (!(features & NETIF_F_SG))
features &= ~(NETIF_F_TSO | NETIF_F_TSO6);
return features;
}
/* 커널 소스 분석: ndo_set_features — 실제 HW 레지스터 변경 */
static int mydrv_set_features(struct net_device *dev,
netdev_features_t features)
{
struct mydrv_priv *priv = netdev_priv(dev);
netdev_features_t changed = features ^ dev->features;
if (changed & NETIF_F_RXCSUM)
mydrv_set_rx_csum(priv, !!(features & NETIF_F_RXCSUM));
if (changed & (NETIF_F_TSO | NETIF_F_TSO6))
mydrv_set_tso(priv, !!(features & NETIF_F_TSO));
if (changed & NETIF_F_HW_VLAN_CTAG_RX)
mydrv_set_vlan_strip(priv, !!(features & NETIF_F_HW_VLAN_CTAG_RX));
return 0;
}
/* 커널 소스 분석: ndo_features_check — per-skb 호환성 검사
* 터널 패킷 등 HW가 처리할 수 없는 조합 감지 */
static netdev_features_t mydrv_features_check(struct sk_buff *skb,
struct net_device *dev,
netdev_features_t features)
{
/* 기본 VXLAN/GRE/Geneve 검사 적용 */
features = vlan_features_check(skb, features);
features = vxlan_features_check(skb, features);
/* HW 제한: inner header offset이 256바이트 초과 시
* TSO offload 불가 → SW로 fallback */
if (skb_is_gso(skb) &&
skb_inner_transport_offset(skb) > 256)
features &= ~(NETIF_F_TSO | NETIF_F_TSO6 | NETIF_F_GSO_GRE);
return features;
}
ndo_bpf / ndo_xdp_xmit 상세 분석
XDP(eXpress Data Path) 지원은 두 개의 콜백으로 구성됩니다. ndo_bpf()는 제어 경로(Control Path)에서 XDP 프로그램의 설치/제거를 처리하고, ndo_xdp_xmit()은 데이터 경로(Data Path)에서 XDP_TX/XDP_REDIRECT 액션의 프레임을 실제로 전송합니다.
/* 커널 소스 분석: ndo_bpf 구현 패턴
* XDP 프로그램 설치/제거 처리 */
static int mydrv_bpf(struct net_device *dev, struct netdev_bpf *bpf)
{
struct mydrv_priv *priv = netdev_priv(dev);
switch (bpf->command) {
case XDP_SETUP_PROG: {
/* 소프트웨어 XDP 프로그램 설치 (드라이버 모드)
* - RX ring에 XDP를 위한 headroom 확보 필요
* - 기존 프로그램 교체 시 rcu_assign 사용 */
struct bpf_prog *old_prog = priv->xdp_prog;
struct bpf_prog *new_prog = bpf->prog;
bool running = netif_running(dev);
/* XDP 프로그램이 처음 설치될 때:
* RX 버퍼에 XDP_PACKET_HEADROOM만큼 headroom 확보
* → ring 재할당 필요 → stop/restart */
if (running && (!!old_prog != !!new_prog)) {
mydrv_stop(dev);
priv->xdp_prog = new_prog;
if (new_prog)
priv->rx_headroom = XDP_PACKET_HEADROOM;
else
priv->rx_headroom = 0;
mydrv_open(dev);
} else {
WRITE_ONCE(priv->xdp_prog, new_prog);
}
if (old_prog)
bpf_prog_put(old_prog);
return 0;
}
case XDP_SETUP_HW_OFFLOAD:
/* SmartNIC HW 오프로드: BPF 프로그램을 NIC에 직접 로드
* 대부분의 드라이버는 미지원 → -EOPNOTSUPP 반환 */
return -EOPNOTSUPP;
default:
return -EINVAL;
}
}
/* 커널 소스 분석: ndo_xdp_xmit — XDP 프레임 배치 전송
* NAPI 컨텍스트(softirq)에서 호출됨 */
static int mydrv_xdp_xmit(struct net_device *dev, int n,
struct xdp_frame **frames,
u32 flags)
{
struct mydrv_priv *priv = netdev_priv(dev);
struct mydrv_tx_ring *ring;
int i, sent = 0;
/* XDP 전용 TX 큐 사용 (일반 TX 큐와 분리하여 락 경합 방지) */
ring = &priv->tx_rings[priv->xdp_tx_queue];
for (i = 0; i < n; i++) {
if (mydrv_tx_avail(ring) == 0)
break;
mydrv_xdp_tx_map(ring, frames[i]);
sent++;
}
/* XDP_XMIT_FLUSH: 배치 전송 완료 후 doorbell 알림
* 이 플래그가 설정되어야만 HW가 전송을 시작
* XDP_REDIRECT 배치의 마지막 호출에만 설정됨 */
if (flags & XDP_XMIT_FLUSH)
mydrv_ring_doorbell(ring);
return sent; /* 실제 전송된 프레임 수 반환 */
}
ndo_xdp_xmit()은 NAPI 컨텍스트에서 호출되므로, 일반 TX 큐를 사용하면 qdisc의 per-queue lock과 경합이 발생합니다. 최적의 설계는 XDP 전용 TX 큐를 별도로 할당하여 데이터 경로 간 독립성을 확보하는 것입니다. num_tx_queues를 XDP 큐만큼 추가로 할당하고, real_num_tx_queues는 일반 큐 수만 설정합니다.
콜백 미구현 시 기본 동작
드라이버가 특정 콜백을 NULL로 남겨두면 커널 코어가 기본 동작(Default Behavior)을 제공합니다. 아래 테이블은 주요 콜백이 NULL일 때의 동작을 정리한 것입니다.
| 콜백 | NULL 시 기본 동작 | 관련 코어 코드 |
|---|---|---|
ndo_init |
추가 초기화 없이 등록 진행 | register_netdevice() |
ndo_uninit |
추가 정리 없이 해제 진행 | unregister_netdevice() |
ndo_change_mtu |
dev->min_mtu ~ max_mtu 범위 내 자동 허용. 코어가 직접 WRITE_ONCE(dev->mtu, new_mtu) 수행 |
dev_set_mtu_ext() |
ndo_validate_addr |
항상 성공 (0 반환). 유효하지 않은 MAC으로도 UP 가능 | dev_open() |
ndo_select_queue |
netdev_pick_tx()의 기본 해시 기반 큐 선택. XPS 설정이 있으면 XPS 우선, 없으면 skb_tx_hash() 사용 |
netdev_pick_tx() |
ndo_set_rx_mode |
필터 업데이트 없음. HW 필터가 변경되지 않아 새 멀티캐스트 그룹 수신 불가 | __dev_set_rx_mode() |
ndo_set_mac_address |
-EOPNOTSUPP 반환. MAC 주소 변경 불가 |
dev_set_mac_address() |
ndo_get_stats64 |
dev->pcpu_stat_type에 따라 자동 수집 (6.1+): NETDEV_PCPU_STAT_TSTATS → dev_get_tstats64(), NETDEV_PCPU_STAT_DSTATS → dev_get_dstats64(), 미설정 → dev->stats 구조체 직접 반환 |
dev_get_stats() |
ndo_fix_features |
요청된 features 그대로 수락 (HW 제약 미적용) | netdev_fix_features() |
ndo_set_features |
feature 비트만 변경되고 HW에 반영되지 않음 | netdev_update_features() |
ndo_features_check |
passthru_features_check() — 모든 feature 허용 |
netif_skb_features() |
ndo_tx_timeout |
TX watchdog 타임아웃 시 복구 동작 없음. 인터페이스가 먹통 상태로 유지 | dev_watchdog() |
ndo_bpf |
XDP 프로그램 attach 시 -EINVAL 반환. generic XDP만 사용 가능 (성능 저하) |
dev_xdp_install() |
ndo_xdp_xmit |
XDP_TX/XDP_REDIRECT 액션 시 프레임 드롭 |
dev_xdp_enqueue() |
ndo_get_iflink |
dev->ifindex 반환 (자기 자신) |
dev_get_iflink() |
ndo_eth_ioctl |
-EOPNOTSUPP 반환. HW 타임스탬프 등 미지원 |
dev_eth_ioctl() |
ndo_tx_timeout이 NULL이면 TX 경로가 정지(stall)되었을 때 자동 복구 메커니즘이 없습니다. 실제 하드웨어 드라이버에서는 반드시 구현하여, 타임아웃 발생 시 TX 큐 리셋이나 전체 HW 리셋을 수행해야 합니다. 가상 장치(veth, bridge 등)는 HW 정지가 없으므로 생략 가능합니다.
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를 크게 좌우합니다.
RX 전체 파이프라인(Pipeline)
아래 다이어그램은 물리 와이어 수신부터 애플리케이션 소켓(Socket) 버퍼 도달까지의 전체 수신 경로를 보여줍니다. 각 단계에서 일어나는 핵심 동작에 주의하세요.
GRO (Generic Receive Offload) 동작 원리
GRO는 네트워크 스택 진입 전에 동일 flow의 연속 패킷들을 하나의 대형 sk_buff로 집계(merge)하여
프로토콜 스택 처리 횟수를 줄이는 소프트웨어 오프로드 기술입니다. 하드웨어 LRO(Large Receive Offload)와 달리
GRO는 프로토콜 레이어에서 stateless하게 동작하여 포워딩 환경에서도 안전하게 사용할 수 있습니다.
GRO 집계 과정은 다음과 같습니다:
- NAPI poll에서
napi_gro_receive()호출 시, GRO 엔진은napi→gro_hash[]테이블에서 동일 flow를 검색합니다. - 매칭되는 flow가 있으면 현재 패킷의 payload를 기존 skb의
frag_list또는frags[]에 병합합니다. - flow가 완료되거나(PSH, FIN 등) 타이머(Timer)/카운트 제한에 도달하면 집계된 대형 skb를
netif_receive_skb()로 전달합니다. - 매칭되지 않으면 새 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 재활성화 ... */
}
ethtool -K eth0 gro off를 고려하세요.
반대로, 벌크 TCP 전송(파일 서버, 스트리밍)에서는 GRO를 반드시 활성화해야 CPU 사용률이 크게 줄어듭니다.
NAPI 상태 머신
NAPI는 명확한 상태 전환 규약을 가진 상태 머신으로 동작합니다. 드라이버가 이 규약을 정확히 따르지 않으면 인터럽트 누수(IRQ 재활성화 누락)나 poll 미진입 같은 심각한 버그가 발생합니다.
| 상태 플래그 | 비트 | 의미 |
|---|---|---|
NAPI_STATE_SCHED | 0 | NAPI가 poll 목록에 스케줄됨. napi_schedule()가 설정, napi_complete_done()이 해제 |
NAPI_STATE_DISABLE | 1 | napi_disable() 진행 중. SCHED 비트 해제를 spin-wait |
NAPI_STATE_NPSVC | 2 | busy polling 서비스 중 표시. poll이 non-softirq 컨텍스트에서 실행됨을 나타냄 |
NAPI_STATE_LISTED | 3 | NAPI가 디바이스의 napi_list에 등록됨 (netif_napi_add()가 설정) |
NAPI_STATE_NO_BUSY_POLL | 4 | busy polling 비활성화 표시 (드라이버가 미지원 선언) |
NAPI_STATE_IN_BUSY_POLL | 5 | 현재 busy poll 실행 중 (re-entrant 방지) |
NAPI_STATE_PREFER_BUSY_POLL | 6 | 소켓이 busy poll 선호 표시 (SO_PREFER_BUSY_POLL) |
NAPI_STATE_THREADED | 7 | threaded NAPI 모드 활성 — 전용 커널 스레드(Kernel Thread)에서 poll 실행 |
NAPI_STATE_SCHED_THREADED | 8 | threaded 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()은 내부적으로 NAPI_STATE_SCHED 비트를 spin-wait하며, 진행 중인 poll이
napi_complete_done()을 호출해 SCHED를 해제할 때까지 대기합니다. 따라서 napi_disable() 반환 후에는
poll 콜백이 절대 실행되지 않음이 보장되며, 이후 ring 메모리를 안전하게 해제할 수 있습니다.
반드시 IRQ 비활성화 후, ring 해제 전에 호출해야 합니다.
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 스레드를 특정 코어에 바인딩하여 데이터플레인/컨트롤플레인 분리 가능 - cgroup 통합: NAPI 스레드를 cgroup에 배치하여 CPU/메모리 자원 제한 가능
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 완료 인터럽트, 디스크립터 회수까지의 전체 송신 경로를 보여줍니다.
BQL (Byte Queue Limits): 버퍼블로트 방지와 동적 큐 제한
BQL(Byte Queue Limits)은 커널 lib/dynamic_queue_limits.c에 구현된 동적 큐 깊이 제어 알고리즘입니다.
NIC TX 큐에 쌓을 수 있는 바이트 수를 실시간(Real-time)으로 조정하여, 큐가 과도하게 깊어지는 버퍼블로트(bufferbloat)를 방지하면서도
충분한 처리량을 유지합니다.
TX 경로 앞 절에서 netdev_tx_sent_queue/netdev_tx_completed_queue를 호출한 것이 바로 BQL API입니다.
이 절에서는 그 내부 알고리즘, 자료구조, sysfs 튜닝, 그리고 qdisc와의 상호작용을 깊이 있게 살펴봅니다.
버퍼블로트 문제와 BQL의 필요성
NIC의 TX ring이 크거나 드라이버가 큐 깊이를 제한하지 않으면, 상위 계층(qdisc, TCP 혼잡 제어(Congestion Control))이 내린 결정과 무관하게 수백 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을 측정하여 공정한 스케줄링이 가능합니다.
실전 디버깅(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 잠금(Lock)을 네트워크 코어가 아닌 드라이버가 직접 책임지는 오래된 모델입니다.
즉, ndo_start_xmit() 동시 호출에 대한 직렬화(Serialization)/경합(Contention) 제어를 드라이버가 스스로 보장해야 하며,
큐 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 | 드라이버/펌웨어(Firmware) 버전 |
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);
}
phylib 상태 머신 내부 분석
커널의 PHY 추상화 계층(phylib)은 drivers/net/phy/phy.c에 구현된 상태 머신(State Machine)을 중심으로 동작합니다. phy_state_machine() 함수가 delayed_work로 주기적으로 호출되며, PHY의 링크 상태를 감시하고 MAC 드라이버에 변화를 통지합니다.
PHY 상태 머신은 다음 6개 상태를 순환합니다.
| 상태 | 값 | 의미 | 진입 조건 |
|---|---|---|---|
PHY_DOWN | 0 | PHY가 초기화되지 않았거나 중지된 상태 | 드라이버 로드, phy_stop() 호출 |
PHY_READY | 1 | PHY가 초기화되었으나 아직 시작하지 않은 상태 | phy_init_hw() 완료 |
PHY_UP | 2 | PHY가 시작되었으나 링크가 아직 성립하지 않은 상태 | phy_start() 호출 |
PHY_RUNNING | 3 | 링크가 활성화되어 데이터 전송이 가능한 상태 | Auto-Negotiation 완료, 링크 업 |
PHY_NOLINK | 4 | PHY가 동작 중이나 링크가 끊어진 상태 | 케이블 분리, 원격 장애 |
PHY_HALTED | 5 | PHY가 명시적으로 정지된 상태 | phy_stop() 호출 |
커널 소스 drivers/net/phy/phy.c의 phy_state_machine() 핵심 로직을 분석하면 다음과 같습니다.
/* drivers/net/phy/phy.c - phy_state_machine() 핵심 흐름 분석 */
void phy_state_machine(struct work_struct *work)
{
struct phy_device *phydev =
container_of(to_delayed_work(work),
struct phy_device, state_queue);
bool needs_aneg = false, do_suspend = false;
enum phy_state old_state;
int err = 0;
mutex_lock(&phydev->lock);
old_state = phydev->state;
switch (phydev->state) {
case PHY_DOWN:
case PHY_READY:
break; /* 대기 상태 — 외부 이벤트 대기 */
case PHY_UP:
needs_aneg = true; /* Auto-Negotiation 시작 */
break;
case PHY_NOLINK:
case PHY_RUNNING:
err = phy_check_link_status(phydev); /* MDIO 레지스터 읽기 */
break;
case PHY_HALTED:
if (phydev->link) {
phydev->link = 0;
phy_link_down(phydev);
}
do_suspend = true;
break;
}
mutex_unlock(&phydev->lock);
if (needs_aneg)
err = phy_start_aneg(phydev);
/* 상태 변경이 있으면 콜백 호출 */
if (old_state != phydev->state) {
phydev_dbg(phydev, "PHY state change %s -> %s\n",
phy_state_to_str(old_state),
phy_state_to_str(phydev->state));
if (phydev->drv && phydev->drv->link_change_notify)
phydev->drv->link_change_notify(phydev);
}
/* 정지 상태가 아니면 작업 큐 재스케줄 */
if (!do_suspend && phy_polling_mode(phydev))
phy_queue_state_machine(phydev, PHY_STATE_TIME);
}
phy_check_link_status()는 MDIO 레지스터를 읽어 실제 링크 상태를 확인하고, phy_link_up() 또는 phy_link_down()을 호출하여 adjust_link 콜백을 트리거합니다. 상태 전이 흐름을 다이어그램으로 표현하면 다음과 같습니다.
phylink 콜백과 MAC 드라이버 계약
phylink은 phylink_mac_ops 구조체를 통해 MAC 드라이버에 링크 설정 변경을 통지합니다. drivers/net/phy/phylink.c의 phylink_resolve()가 PHY/in-band 상태를 종합하여 MAC 콜백을 호출하는 중앙 분기점입니다.
| 콜백 | 호출 시점 | 드라이버 책임 |
|---|---|---|
mac_config | 링크 파라미터 변경 시 | speed, duplex, pause 프레임 등 MAC 하드웨어 레지스터 설정 |
mac_link_up | 링크 성립 확인 후 | TX 활성화, 통계 카운터 시작, netif_carrier_on() 연계 |
mac_link_down | 링크 끊김 감지 시 | TX 비활성화, 큐 정지, DMA 드레인(Drain) |
mac_prepare | mac_config 직전 | MAC 정지, 클럭 재설정 등 사전 준비 |
mac_finish | mac_config 직후 | MAC 재시작, PCS 잠금 확인 등 사후 정리 |
mac_select_pcs | 인터페이스 모드 결정 시 | 사용할 PCS(Physical Coding Sublayer) 인스턴스 반환 |
커널 소스 phylink_resolve()의 핵심 흐름을 분석하면, PHY 상태와 in-band 상태를 종합하여 최종 링크 상태를 결정하는 과정을 확인할 수 있습니다.
/* drivers/net/phy/phylink.c - phylink_resolve() 핵심 흐름 분석 */
static void phylink_resolve(struct work_struct *w)
{
struct phylink *pl = container_of(w, struct phylink, resolve);
struct phylink_link_state link_state;
bool cur_link_is_up;
mutex_lock(&pl->state_mutex);
cur_link_is_up = pl->old_link_state;
/* 1단계: PCS/PHY 상태를 수집하여 link_state 결정 */
phylink_resolve_an_pause(&link_state);
if (pl->phydev)
phylink_get_phy_state(pl, &link_state);
if (pl->pcs)
phylink_pcs_get_state(pl, &link_state);
/* 2단계: 링크 상태가 변경된 경우에만 MAC 콜백 호출 */
if (link_state.link != cur_link_is_up) {
if (!link_state.link) {
/* 링크 다운: mac_link_down 호출 */
pl->mac_ops->mac_link_down(pl->config,
pl->cur_link_an_mode, pl->cur_interface);
} else {
/* 링크 업: mac_prepare - mac_config - mac_finish - mac_link_up */
if (pl->mac_ops->mac_prepare)
pl->mac_ops->mac_prepare(pl->config,
pl->cur_link_an_mode, pl->cur_interface);
pl->mac_ops->mac_config(pl->config,
pl->cur_link_an_mode, &link_state);
if (pl->mac_ops->mac_finish)
pl->mac_ops->mac_finish(pl->config,
pl->cur_link_an_mode, pl->cur_interface);
pl->mac_ops->mac_link_up(pl->config, pl->phydev,
pl->cur_link_an_mode, pl->cur_interface,
link_state.speed, link_state.duplex,
!!(link_state.pause & MLO_PAUSE_TX),
!!(link_state.pause & MLO_PAUSE_RX));
}
pl->old_link_state = link_state.link;
}
mutex_unlock(&pl->state_mutex);
}
실제 MAC 드라이버에서 phylink_mac_ops를 구현하는 전체 패턴은 다음과 같습니다.
/* phylink_mac_ops 전체 구현 예시 */
static void my_mac_config(struct phylink_config *config,
unsigned int mode,
const struct phylink_link_state *state)
{
struct my_priv *priv = container_of(config, struct my_priv,
phylink_config);
u32 mac_ctrl = my_read_reg(priv, MAC_CTRL);
/* speed 설정 */
mac_ctrl &= ~MAC_SPEED_MASK;
switch (state->speed) {
case SPEED_10000: mac_ctrl |= MAC_SPEED_10G; break;
case SPEED_1000: mac_ctrl |= MAC_SPEED_1G; break;
case SPEED_100: mac_ctrl |= MAC_SPEED_100; break;
}
/* duplex 설정 */
if (state->duplex == DUPLEX_FULL)
mac_ctrl |= MAC_FULL_DUPLEX;
else
mac_ctrl &= ~MAC_FULL_DUPLEX;
/* pause 프레임 설정 */
mac_ctrl &= ~(MAC_TX_PAUSE | MAC_RX_PAUSE);
if (state->pause & MLO_PAUSE_TX)
mac_ctrl |= MAC_TX_PAUSE;
if (state->pause & MLO_PAUSE_RX)
mac_ctrl |= MAC_RX_PAUSE;
my_write_reg(priv, MAC_CTRL, mac_ctrl);
}
static void my_mac_link_up(struct phylink_config *config,
struct phy_device *phy,
unsigned int mode, phy_interface_t interface,
int speed, int duplex,
bool tx_pause, bool rx_pause)
{
struct my_priv *priv = container_of(config, struct my_priv,
phylink_config);
/* MAC TX 활성화 */
my_set_bits(priv, MAC_CTRL, MAC_TX_EN | MAC_RX_EN);
/* 커널 네트워크 스택에 캐리어 상태 통지 */
netif_carrier_on(priv->ndev);
netif_tx_wake_all_queues(priv->ndev);
}
static void my_mac_link_down(struct phylink_config *config,
unsigned int mode,
phy_interface_t interface)
{
struct my_priv *priv = container_of(config, struct my_priv,
phylink_config);
/* TX 큐 정지 후 MAC 비활성화 */
netif_tx_stop_all_queues(priv->ndev);
netif_carrier_off(priv->ndev);
my_clear_bits(priv, MAC_CTRL, MAC_TX_EN);
/* DMA 진행 중인 프레임 완료 대기 */
my_drain_tx_dma(priv);
}
static const struct phylink_mac_ops my_phylink_mac_ops = {
.mac_config = my_mac_config,
.mac_link_up = my_mac_link_up,
.mac_link_down = my_mac_link_down,
};
SFP 케이지와 sfp_bus 통합
SFP(Small Form-factor Pluggable) 모듈을 지원하는 드라이버는 sfp_bus 프레임워크를 통해 모듈 삽입/제거 이벤트를 처리합니다. SFP 모듈 내부에 PHY가 포함된 경우(예: 1000BASE-T SFP) 자동으로 PHY 탐색(Discovery)과 연결이 수행됩니다.
SFP 통합의 핵심 구조는 sfp_upstream_ops 콜백입니다. 드라이버는 이 콜백을 구현하여 모듈 이벤트에 대응합니다.
/* sfp_upstream_ops를 통한 SFP 케이지 통합 */
static int my_sfp_module_insert(void *priv,
const struct sfp_eeprom_id *id)
{
struct my_priv *p = priv;
/* SFP EEPROM에서 모듈 타입/속도 확인 */
dev_info(p->dev, "SFP module inserted: %s\n",
id->base.vendor_name);
/* phylink에 SFP 모듈 정보 전달 */
return phylink_sfp_module_insert(p->phylink, id);
}
static void my_sfp_module_remove(void *priv)
{
struct my_priv *p = priv;
phylink_sfp_module_remove(p->phylink);
}
static void my_sfp_link_down(void *priv)
{
struct my_priv *p = priv;
phylink_sfp_link_down(p->phylink);
}
static void my_sfp_link_up(void *priv)
{
struct my_priv *p = priv;
phylink_sfp_link_up(p->phylink);
}
static const struct sfp_upstream_ops my_sfp_ops = {
.module_insert = my_sfp_module_insert,
.module_remove = my_sfp_module_remove,
.link_down = my_sfp_link_down,
.link_up = my_sfp_link_up,
.attach = phy_sfp_attach,
.detach = phy_sfp_detach,
.connect_phy = phy_sfp_connect_phy,
.disconnect_phy = phy_sfp_disconnect_phy,
};
/* 프로브 시점에서 SFP 버스 등록 */
static int my_probe_sfp(struct my_priv *priv)
{
struct sfp_bus *sfp_bus;
/* phylink_config에 SFP 지원 플래그 설정 */
priv->phylink_config.type = PHYLINK_NETDEV;
__set_bit(PHY_INTERFACE_MODE_SGMII,
priv->phylink_config.supported_interfaces);
__set_bit(PHY_INTERFACE_MODE_1000BASEX,
priv->phylink_config.supported_interfaces);
/* SFP 버스를 fwnode에서 검색하여 등록 */
sfp_bus = sfp_bus_find_fwnode(dev_fwnode(priv->dev));
if (sfp_bus)
sfp_bus_add_upstream(sfp_bus, priv, &my_sfp_ops);
return 0;
}
In-band Auto-Negotiation과 fixed-link
phylink은 PHY 직접 연결 외에도 두 가지 대안적 링크 모드를 지원합니다. SGMII/1000BASE-X에서 사용하는 인밴드 Auto-Negotiation(In-band AN)과, 링크 파라미터가 고정된 고정 링크(fixed-link) 모드입니다.
| 모드 | phylink_config.type | 링크 감지 방식 | 사용 시나리오 |
|---|---|---|---|
| PHY 직접 연결 | PHYLINK_NETDEV | PHY 상태 머신 (MDIO 폴링/인터럽트) | 일반적인 구리 또는 SFP PHY |
| In-band AN | PHYLINK_NETDEV | PCS 레지스터 기반 인밴드 신호 | SGMII, 1000BASE-X, USXGMII SFP |
| Fixed-link | PHYLINK_NETDEV | Device Tree/ACPI에서 고정 파라미터 | 스위치 백본, MAC-to-MAC 직결 |
| MAC 전용 | PHYLINK_DEV | MAC 자체 링크 감지 | 가상 디바이스, DSA 마스터 |
in-band AN은 SGMII에서 PHY가 MAC에게 링크 파라미터를 전달하는 방식입니다. PCS(Physical Coding Sublayer)가 SGMII 제어 워드를 파싱하여 speed/duplex를 추출합니다.
/* In-band AN 지원을 위한 PCS 구현 예시 */
static void my_pcs_get_state(struct phylink_pcs *pcs,
struct phylink_link_state *state)
{
struct my_pcs *mpcs = container_of(pcs, struct my_pcs, pcs);
u32 status = my_pcs_read(mpcs, PCS_STATUS_REG);
state->link = !!(status & PCS_LINK_UP);
state->an_complete = !!(status & PCS_AN_COMPLETE);
if (state->link) {
/* SGMII 제어 워드에서 speed/duplex 추출 */
u32 lpa = my_pcs_read(mpcs, PCS_LP_ABILITY_REG);
switch ((lpa >> 10) & 0x3) {
case 0: state->speed = SPEED_10; break;
case 1: state->speed = SPEED_100; break;
case 2: state->speed = SPEED_1000; break;
}
state->duplex = (lpa & PCS_SGMII_DUPLEX) ?
DUPLEX_FULL : DUPLEX_HALF;
}
}
static int my_pcs_config(struct phylink_pcs *pcs,
unsigned int neg_mode,
phy_interface_t interface,
const unsigned long *advertising,
bool permit_pause_to_mac)
{
struct my_pcs *mpcs = container_of(pcs, struct my_pcs, pcs);
if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) {
/* In-band AN 활성화 */
my_pcs_write(mpcs, PCS_CTRL_REG,
PCS_AN_ENABLE | PCS_AN_RESTART);
} else {
/* Forced 모드: AN 비활성화 */
my_pcs_write(mpcs, PCS_CTRL_REG, 0);
}
return 0;
}
static const struct phylink_pcs_ops my_pcs_ops = {
.pcs_get_state = my_pcs_get_state,
.pcs_config = my_pcs_config,
};
Fixed-link은 Device Tree에서 다음과 같이 설정됩니다.
/* Device Tree fixed-link 예시:
ðernet {
fixed-link {
speed = <1000>;
full-duplex;
};
};
*/
/* 드라이버에서 fixed-link 처리 -- phylink이 자동으로 감지 */
static int my_setup_phylink(struct my_priv *priv)
{
struct fwnode_handle *fwnode = dev_fwnode(priv->dev);
priv->phylink_config.type = PHYLINK_NETDEV;
priv->phylink_config.mac_capabilities =
MAC_SYM_PAUSE | MAC_10 | MAC_100 | MAC_1000FD;
/* phylink_create()는 fwnode에서 fixed-link 노드를 자동 탐색 */
priv->phylink = phylink_create(&priv->phylink_config,
fwnode, PHY_INTERFACE_MODE_SGMII,
&my_phylink_mac_ops);
/* fixed-link인 경우 phylink_of_phy_connect()는 내부적으로
* phy_connect_direct()를 건너뛰고 고정 상태를 사용합니다 */
return phylink_of_phy_connect(priv->phylink,
to_of_node(fwnode), 0);
}
PHY 인터럽트 vs 폴링 모드
PHY 상태 변화를 감지하는 방식은 크게 인터럽트 구동(Interrupt-driven)과 타이머 구동(Timer-driven, 폴링) 두 가지입니다. 인터럽트 방식이 CPU 효율이 높지만, 모든 PHY/보드 조합에서 인터럽트가 올바르게 배선되어 있지는 않습니다.
| 항목 | 인터럽트 모드 | 폴링 모드 |
|---|---|---|
| 반응 시간 | 수 마이크로초 (즉시) | 최대 PHY_STATE_TIME(1초) 지연 |
| CPU 부하 | 이벤트 발생 시만 처리 | 주기적 MDIO 읽기 (1초마다) |
| 구현 요구사항 | PHY IRQ 핀 배선, config_intr/handle_interrupt 구현 | 추가 구현 없음 (기본 동작) |
| 안정성 | IRQ 라인 노이즈에 민감할 수 있음 | 매우 안정적 |
| 적합 환경 | 고성능 서버, 빠른 failover 필요 환경 | 임베디드, IRQ 미배선 보드 |
| 커널 함수 | phy_interrupt() → phy_trigger_machine() | phy_state_machine() delayed_work |
인터럽트 모드에서 PHY 드라이버는 config_intr와 handle_interrupt 콜백을 구현해야 합니다. 커널의 phy_interrupt() 핸들러가 이 콜백들을 조율합니다.
/* drivers/net/phy/phy.c - phy_interrupt() 핵심 분석 */
static irqreturn_t phy_interrupt(int irq, void *phy_dat)
{
struct phy_device *phydev = phy_dat;
struct phy_driver *drv = phydev->drv;
irqreturn_t ret;
/* PHY 드라이버의 인터럽트 핸들러 호출 */
ret = drv->handle_interrupt(phydev);
if (ret == IRQ_HANDLED) {
/* 인터럽트가 처리되면 상태 머신을 즉시 트리거 */
phy_trigger_machine(phydev);
}
return ret;
}
/* PHY 드라이버의 인터럽트 콜백 구현 예시 (Realtek/Broadcom 패턴) */
static int my_phy_config_intr(struct phy_device *phydev)
{
u16 val;
if (phydev->interrupts == PHY_INTERRUPT_ENABLED) {
/* 링크 상태 변경 인터럽트 활성화 */
val = phy_read(phydev, MY_PHY_INTR_MASK);
val |= MY_PHY_INTR_LINK_CHANGE;
phy_write(phydev, MY_PHY_INTR_MASK, val);
} else {
/* 모든 인터럽트 비활성화 */
phy_write(phydev, MY_PHY_INTR_MASK, 0);
}
return 0;
}
static irqreturn_t my_phy_handle_interrupt(struct phy_device *phydev)
{
u16 status;
/* 인터럽트 상태 읽기 (읽으면 자동 클리어) */
status = phy_read(phydev, MY_PHY_INTR_STATUS);
if (!(status & MY_PHY_INTR_LINK_CHANGE))
return IRQ_NONE; /* 이 PHY의 인터럽트가 아님 */
/* phylib에 인터럽트 발생 알림 */
phy_trigger_machine(phydev);
return IRQ_HANDLED;
}
/* 폴링 모드 전환: IRQ를 PHY_POLL로 설정하면 자동 폴링 */
static struct phy_driver my_phy_driver[] = {{
.phy_id = MY_PHY_ID,
.phy_id_mask = 0xfffffff0,
.name = "My PHY",
.config_intr = my_phy_config_intr,
.handle_interrupt = my_phy_handle_interrupt,
/* .irq = PHY_POLL 이면 폴링 모드로 동작 */
}};
phy_polling_mode()는 PHY의 IRQ가 PHY_POLL로 설정되었는지 확인합니다. 폴링 모드에서는 phy_state_machine()이 PHY_STATE_TIME(기본 1초) 간격으로 MDIO 레지스터를 읽어 링크 상태를 확인합니다. 인터럽트 모드에서는 phy_trigger_machine()이 상태 머신을 즉시 실행하므로 링크 변화에 대한 반응이 훨씬 빠릅니다.
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;
}
}
ndo_bpf 콜백 구현 상세
XDP 프로그램을 드라이버에 부착(Attach)하려면 ndo_bpf 콜백을 구현해야 합니다. 커널의 dev_xdp_install() 함수가 이 콜백을 호출하여 XDP 프로그램의 설치/제거를 드라이버에 통지합니다.
ndo_bpf는 XDP_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 해시 등의 정보를 기록합니다.
커널 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 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에서 도입된 기능으로, 단일 페이지를 초과하는 프레임(예: 점보 프레임(Jumbo Frame))을 XDP에서 처리할 수 있게 합니다. xdp_buff에 mb(multi-buffer) 플래그가 설정되면, 추가 프래그먼트(Fragment)가 skb_shared_info 구조체를 통해 연결됩니다.
| 항목 | 단일 버퍼 XDP | 멀티버퍼 XDP |
|---|---|---|
| 최대 프레임 크기 | PAGE_SIZE - headroom - tailroom (~3500B) | 제한 없음 (드라이버 구현에 따라) |
xdp_buff.flags | 0 | XDP_FLAGS_HAS_FRAGS |
| 프래그먼트 접근 | N/A | skb_shared_info의 frags[] 배열 |
| 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/RFS | HW RSS 부족 시 보조적으로 사용 |
RSS 해시 함수와 Indirection Table 내부
RSS(Receive Side Scaling)는 수신 패킷을 여러 RX 큐에 분산하는 하드웨어 메커니즘입니다. 핵심은 토플리츠 해시(Toeplitz Hash) 알고리즘으로, 패킷의 소스/목적지 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)를 설정합니다.
/* 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_cpus | TX 완료 IRQ와 같은 CPU에서 송신하여 캐시 히트 향상 |
| RX queue map | RX 큐 → 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를 선택합니다.
| 기능 | RPS | RFS |
|---|---|---|
| 분산 기준 | 패킷 해시 (소스/목적지 IP+포트) | 소켓이 마지막으로 처리된 CPU |
| 목적 | softirq 부하 분산 | 애플리케이션-인터럽트 CPU 일치 |
| 캐시 효과 | 패킷 처리 분산 | 소켓 데이터의 캐시 지역성 극대화 |
| 설정 | /sys/class/net/dev/queues/rx-N/rps_cpus | /proc/sys/net/core/rps_sock_flow_entries |
| 커널 자료구조 | rps_dev_flow_table | rps_sock_flow_table |
| 사용 시점 | HW RSS 미지원 또는 단일 큐 NIC | RPS와 함께 사용, 서버 워크로드 |
/* 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
*/
NUMA 친화적 큐 배치 전략
NUMA(Non-Uniform Memory Access) 시스템에서 네트워크 드라이버의 성능은 큐, 인터럽트, 메모리 할당이 올바른 NUMA 노드에 배치되었는지에 크게 좌우됩니다. 원격 NUMA 노드의 메모리 접근은 로컬 접근 대비 1.5~3배의 지연이 발생합니다.
| 리소스 | 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에 매핑 | 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_REDIRECT, XDP_TX, XDP_DROP 경로별 반환 API를 혼용하면 double free/메모리 누수가 쉽게 발생합니다.
page_pool 내부 자료구조와 할당 경로
struct page_pool은 크게 세 가지 계층으로 구성됩니다. 가장 빠른 경로인 alloc 캐시(alloc cache)는 per-CPU 배열(기본 128개)로, NAPI 폴링 컨텍스트에서 락 없이 페이지를 꺼내옵니다. 캐시가 비면 ptr_ring 기반 리사이클 링으로 폴백(fallback)하며, 여기서도 부족하면 슬로 패스(slow path)로 버디 할당자(Buddy Allocator)에서 새 페이지를 할당합니다.
커널 소스(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 매핑/언매핑을 자동 관리. 재활용 시 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 등) | 메모리 효율 2~4배 향상, 단 프래그먼트 추적 오버헤드 |
PP_FLAG_SYSTEM_POOL |
시스템 전역 공유 풀 사용. 여러 netdev가 하나의 pool 공유 | 가상 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바이트)만 선형 버퍼(Linear Buffer)에 배치하고, 나머지 페이로드(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이 적절합니다.
제어 경로: 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;
}
RTNL 락 계층과 보호 영역
RTNL(Routing Netlink) 락은 리눅스 네트워크 스택에서 가장 광범위한 뮤텍스(Mutex)입니다. net_device의 등록/해제, 주소 변경, 링크 상태 변경, offload 토글 등 거의 모든 제어 경로 작업이 RTNL 보호 하에서 수행됩니다. 커널 6.13부터는 per-namespace RTNL이 도입되어 네트워크 네임스페이스(Network Namespace) 간 병렬성이 개선되었습니다.
/* net/core/rtnetlink.c — RTNL 락 구현 */
static DEFINE_MUTEX(rtnl_mutex);
void rtnl_lock(void)
{
mutex_lock(&rtnl_mutex);
}
void rtnl_unlock(void)
{
/* 언락 전에 대기 중인 netdev 해제 요청 처리 */
netdev_run_todo();
mutex_unlock(&rtnl_mutex);
}
int rtnl_trylock(void)
{
return mutex_trylock(&rtnl_mutex);
}
/* per-namespace RTNL (v6.13+) — net->rtnl_lock */
void rtnl_net_lock(struct net *net)
{
rtnl_lock(); /* 전역 RTNL 먼저 */
mutex_lock(&net->rtnl_lock); /* 이후 ns RTNL */
}
| 작업 | RTNL 필요 여부 | 비고 |
|---|---|---|
register_netdevice() | 필수 | 등록 과정 전체 RTNL 보호 |
dev_change_mtu() | 필수 | ndo_change_mtu 호출 전 RTNL 확인 |
dev_set_mac_address() | 필수 | 주소 변경 → 알림 체인 전파 |
ndo_open() / ndo_stop() | 필수 | 링크 상태 변경 보호 |
ndo_set_features() | 필수 | offload 토글 |
ndo_start_xmit() | 불필요 | 데이터 경로 — NAPI/softirq 컨텍스트 |
napi_poll() | 불필요 | 데이터 경로 — softirq 컨텍스트 |
ethtool_get_stats() | 선택적 | 일부 드라이버에서 RTNL 생략 |
devlink_health_report() | 불필요 | 자체 뮤텍스 사용 |
flush_workqueue()를 호출하면 안 됩니다. workqueue에 대기 중인 작업이 RTNL을 요청할 수 있기 때문입니다. 대신 rtnl_unlock() → flush_workqueue() → rtnl_lock() 패턴을 사용합니다.
RTNETLINK 메시지 처리 경로
RTNETLINK는 사용자 공간에서 커널 네트워크 설정을 변경하는 주요 인터페이스입니다. ip link add, ip link set 같은 명령은 모두 RTM_NEWLINK, RTM_SETLINK 등의 RTNETLINK 메시지로 변환됩니다.
/* 커스텀 netdev를 위한 rtnl_link_ops 등록 예시 */
static int my_virt_newlink(struct net *src_net,
struct net_device *dev,
struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack)
{
struct my_virt_priv *priv = netdev_priv(dev);
int err;
/* IFLA_MY_MODE 등 드라이버 전용 속성 파싱 */
if (data && data[IFLA_MY_MODE])
priv->mode = nla_get_u32(data[IFLA_MY_MODE]);
err = register_netdevice(dev);
if (err)
return err;
netif_carrier_off(dev);
return 0;
}
static void my_virt_dellink(struct net_device *dev,
struct list_head *head)
{
unregister_netdevice_queue(dev, head);
}
static const struct nla_policy my_virt_policy[IFLA_MY_MAX + 1] = {
[IFLA_MY_MODE] = { .type = NLA_U32 },
[IFLA_MY_FLAGS] = { .type = NLA_U32 },
};
static struct rtnl_link_ops my_virt_link_ops = {
.kind = "my_virt",
.priv_size = sizeof(struct my_virt_priv),
.setup = my_virt_setup,
.newlink = my_virt_newlink,
.dellink = my_virt_dellink,
.policy = my_virt_policy,
.maxtype = IFLA_MY_MAX,
};
/* 모듈 초기화 시 등록 */
static int __init my_virt_init(void)
{
return rtnl_link_register(&my_virt_link_ops);
}
RTNETLINK 메시지 처리 흐름은 다음과 같습니다. 사용자가 ip link add 명령을 실행하면, RTM_NEWLINK 메시지가 소켓을 통해 커널에 도달합니다. rtnetlink_rcv_msg()가 메시지를 디스패치하고, rtnl_newlink()가 rtnl_link_ops를 찾아 newlink 콜백을 호출합니다.
ethtool-netlink 인터페이스
커널 5.6부터 ethtool은 기존 ioctl 인터페이스를 대체하는 Netlink 기반 인터페이스를 제공합니다. ethtool-netlink(이하 ethnl)은 확장성이 뛰어나고, 변경 알림(Notification)을 지원하며, 구조화된 데이터 전달이 가능합니다.
| ethnl 커맨드 패밀리 | 대응 레거시 ioctl | 설명 |
|---|---|---|
ETHTOOL_MSG_LINKINFO_GET/SET | ETHTOOL_GSET/SSET | 링크 속도, 듀플렉스(Duplex), autoneg |
ETHTOOL_MSG_RINGS_GET/SET | ETHTOOL_GRINGPARAM | RX/TX 링 크기 |
ETHTOOL_MSG_CHANNELS_GET/SET | ETHTOOL_GCHANNELS | 큐/채널 수 |
ETHTOOL_MSG_COALESCE_GET/SET | ETHTOOL_GCOALESCE | 인터럽트 코얼레싱(Coalescing) |
ETHTOOL_MSG_STATS_GET | ETHTOOL_GSTATS | 드라이버 통계 |
ETHTOOL_MSG_PAUSE_GET/SET | ETHTOOL_GPAUSEPARAM | 일시 정지 프레임(Pause Frame) 설정 |
ETHTOOL_MSG_FEC_GET/SET | ETHTOOL_GFECPARAM | FEC(Forward Error Correction) 설정 |
/* ethtool-netlink 코얼레싱 ops 구현 예시 */
static int my_get_coalesce(struct net_device *ndev,
struct ethtool_coalesce *ec,
struct kernel_ethtool_coalesce *kec,
struct netlink_ext_ack *extack)
{
struct my_priv *priv = netdev_priv(ndev);
ec->rx_coalesce_usecs = priv->rx_usecs;
ec->rx_max_coalesced_frames = priv->rx_frames;
ec->tx_coalesce_usecs = priv->tx_usecs;
ec->tx_max_coalesced_frames = priv->tx_frames;
ec->use_adaptive_rx_coalesce = priv->adaptive_rx;
ec->use_adaptive_tx_coalesce = priv->adaptive_tx;
return 0;
}
static int my_set_coalesce(struct net_device *ndev,
struct ethtool_coalesce *ec,
struct kernel_ethtool_coalesce *kec,
struct netlink_ext_ack *extack)
{
struct my_priv *priv = netdev_priv(ndev);
if (ec->rx_coalesce_usecs > MY_MAX_COAL_USECS) {
NL_SET_ERR_MSG_MOD(extack,
"rx-usecs exceeds maximum");
return -ERANGE;
}
priv->rx_usecs = ec->rx_coalesce_usecs;
priv->rx_frames = ec->rx_max_coalesced_frames;
priv->tx_usecs = ec->tx_coalesce_usecs;
priv->tx_frames = ec->tx_max_coalesced_frames;
priv->adaptive_rx = ec->use_adaptive_rx_coalesce;
priv->adaptive_tx = ec->use_adaptive_tx_coalesce;
/* 하드웨어에 새 코얼레싱 값 적용 */
my_hw_set_coalesce(priv);
return 0;
}
static const struct ethtool_ops my_ethtool_ops = {
.supported_coalesce_params = ETHTOOL_COALESCE_USECS |
ETHTOOL_COALESCE_MAX_FRAMES |
ETHTOOL_COALESCE_USE_ADAPTIVE,
.get_coalesce = my_get_coalesce,
.set_coalesce = my_set_coalesce,
/* ... 기타 ops ... */
};
ETHTOOL_MSG_*_NTF 형태의 알림 메시지를 멀티캐스트(Multicast) 그룹(ETHNL_MCGRP_MONITOR)으로 전송합니다. ethtool --monitor 명령으로 실시간 변경 사항을 관찰할 수 있습니다.
채널/큐 동적 재구성
ethtool -L 명령으로 채널(Channel) 수를 동적으로 변경할 때, 드라이버는 stop → 재구성 → restart 패턴을 따라야 합니다. 이 과정에서 패킷 손실을 최소화하고, 경쟁 상태(Race Condition)를 방지하는 것이 핵심입니다.
/* 안전한 채널 재구성 구현 */
static int my_set_channels(struct net_device *ndev,
struct ethtool_channels *ch)
{
struct my_priv *priv = netdev_priv(ndev);
unsigned int new_rx = ch->combined_count ?: ch->rx_count;
unsigned int new_tx = ch->combined_count ?: ch->tx_count;
int err;
/* 검증: 하드웨어 최대값 초과 방지 */
if (new_rx > priv->max_rx_queues ||
new_tx > priv->max_tx_queues)
return -EINVAL;
/* XDP가 활성화된 경우 TX 큐 수 제한 확인 */
if (priv->xdp_prog && new_tx < new_rx)
return -EINVAL;
/* 인터페이스가 UP 상태인 경우만 재시작 필요 */
if (netif_running(ndev)) {
/* 1단계: 데이터 경로 중단 */
netif_tx_disable(ndev);
my_napi_disable_all(priv);
my_free_irqs(priv);
my_free_rings(priv);
/* 2단계: 새 큐 수 적용 */
priv->num_rx_queues = new_rx;
priv->num_tx_queues = new_tx;
/* 3단계: 새 링/NAPI/IRQ 할당 */
err = my_alloc_rings(priv);
if (err)
goto err_rollback;
err = my_request_irqs(priv);
if (err)
goto err_free_rings;
/* 4단계: 데이터 경로 재시작 */
my_napi_enable_all(priv);
netif_tx_start_all_queues(ndev);
/* netdev 큐 수 갱신 (XPS/RPS 재설정) */
netif_set_real_num_rx_queues(ndev, new_rx);
netif_set_real_num_tx_queues(ndev, new_tx);
} else {
priv->num_rx_queues = new_rx;
priv->num_tx_queues = new_tx;
}
return 0;
err_free_rings:
my_free_rings(priv);
err_rollback:
/* 롤백: 이전 큐 수로 복원 시도 */
priv->num_rx_queues = priv->prev_rx_queues;
priv->num_tx_queues = priv->prev_tx_queues;
my_alloc_rings(priv);
my_request_irqs(priv);
my_napi_enable_all(priv);
netif_tx_start_all_queues(ndev);
return err;
}
set_channels는 RTNL 보호 하에서 호출되므로 동시에 두 번 실행될 수 없습니다. 그러나 ndo_start_xmit()과의 경쟁은 netif_tx_disable()으로 방지해야 합니다. NAPI 폴링과의 경쟁은 napi_disable()이 보장합니다. 반드시 IRQ 해제 → NAPI disable → 큐 해제 순서를 지켜야 합니다.
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 일관성 |
| 암호화(Encryption)/터널(Tunnel) | xfrm offload, UDP tunnel offload | fallback 경로와 통계 구분 |
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)에 규칙을 삽입합니다.
/* 완전한 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_FDB | MAC 주소 → 포트 매핑 | HW FDB 테이블 업데이트 |
SWITCHDEV_OBJ_ID_PORT_MDB | 멀티캐스트(Multicast) 그룹 → 포트 매핑 | HW MDB 테이블 업데이트 |
SWITCHDEV_OBJ_ID_PORT_VLAN | VLAN ID → 포트 매핑 | HW VLAN 필터 테이블 |
SWITCHDEV_OBJ_ID_HOST_MDB | 호스트 멀티캐스트 그룹 | CPU 포트로 멀티캐스트 전달 |
SWITCHDEV_ATTR_ID_BRIDGE_VLAN_FILTERING | VLAN 필터링 활성화/비활성화 | 브리지 전체 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;
}
register_switchdev_blocking_notifier()는 블로킹 컨텍스트에서 호출되므로 직접 처리가 가능합니다.
UDP 터널 오프로드
VXLAN(Virtual Extensible LAN), Geneve 등의 UDP 터널은 NIC 하드웨어가 외부 UDP 포트를 인식해야 내부 패킷의 RSS, checksum offload 등이 올바르게 동작합니다. udp_tunnel_nic_info 구조체를 통해 드라이버는 지원하는 터널 유형과 최대 포트 수를 선언합니다.
| 터널 유형 | 기본 UDP 포트 | 대표 드라이버 지원 | 비고 |
|---|---|---|---|
| VXLAN | 4789 | mlx5, ice, bnxt, i40e | 가장 널리 사용되는 오버레이(Overlay) |
| Geneve | 6081 | mlx5, ice, bnxt | 유연한 TLV 옵션 지원 |
| VXLAN-GPE | 4790 | mlx5 | 다중 프로토콜 캡슐화 |
| GTP-U | 2152 | 일부 SmartNIC | 5G/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()를 호출하여 전체 테이블을 재동기화합니다.
리셋/장애 복구: 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과 통합
devlink health reporter 등록과 사용
devlink health 프레임워크는 NIC 드라이버의 장애 감지, 진단, 자동 복구를 체계화하는 인프라입니다. 리포터(Reporter)를 등록하면 devlink health 명령으로 운영팀이 장애 상태를 조회하고, 자동 복구를 트리거할 수 있습니다.
/* 완전한 devlink health reporter 구현 */
static int my_health_recover(struct devlink_health_reporter *reporter,
void *priv_ctx,
struct netlink_ext_ack *extack)
{
struct my_priv *priv = devlink_health_reporter_priv(reporter);
int err;
netdev_info(priv->ndev, "health recover: performing FLR\n");
rtnl_lock();
if (netif_running(priv->ndev))
my_ndo_stop(priv->ndev);
err = my_hw_function_reset(priv);
if (err) {
NL_SET_ERR_MSG_MOD(extack, "FLR failed");
rtnl_unlock();
return err;
}
err = my_ndo_open(priv->ndev);
rtnl_unlock();
return err;
}
static int my_health_dump(struct devlink_health_reporter *reporter,
struct devlink_fmsg *fmsg, void *priv_ctx,
struct netlink_ext_ack *extack)
{
struct my_priv *priv = devlink_health_reporter_priv(reporter);
struct my_err_ctx *ctx = priv_ctx;
int err;
err = devlink_fmsg_obj_nest_start(fmsg);
if (err)
return err;
if (ctx) {
devlink_fmsg_put(fmsg, "error_type", ctx->err_type);
devlink_fmsg_put(fmsg, "queue_id", ctx->queue_id);
devlink_fmsg_put(fmsg, "timestamp", ctx->timestamp);
}
devlink_fmsg_put(fmsg, "fw_state", my_hw_read_fw_state(priv));
devlink_fmsg_put(fmsg, "hw_status", my_hw_read_status(priv));
devlink_fmsg_pair_nest_start(fmsg, "queues");
devlink_fmsg_arr_pair_nest_start(fmsg, "tx_queues");
for (int i = 0; i < priv->num_tx_queues; i++) {
devlink_fmsg_obj_nest_start(fmsg);
devlink_fmsg_put(fmsg, "idx", i);
devlink_fmsg_put(fmsg, "head", priv->tx_ring[i].head);
devlink_fmsg_put(fmsg, "tail", priv->tx_ring[i].tail);
devlink_fmsg_obj_nest_end(fmsg);
}
devlink_fmsg_arr_pair_nest_end(fmsg);
devlink_fmsg_pair_nest_end(fmsg);
return devlink_fmsg_obj_nest_end(fmsg);
}
static int my_health_diagnose(struct devlink_health_reporter *reporter,
struct devlink_fmsg *fmsg,
struct netlink_ext_ack *extack)
{
struct my_priv *priv = devlink_health_reporter_priv(reporter);
devlink_fmsg_obj_nest_start(fmsg);
devlink_fmsg_put(fmsg, "link_up", netif_carrier_ok(priv->ndev));
devlink_fmsg_put(fmsg, "fw_heartbeat", my_hw_check_heartbeat(priv));
devlink_fmsg_put(fmsg, "pci_status", my_check_pci_status(priv));
return devlink_fmsg_obj_nest_end(fmsg);
}
static const struct devlink_health_reporter_ops my_health_ops = {
.name = "tx",
.recover = my_health_recover,
.dump = my_health_dump,
.diagnose = my_health_diagnose,
};
/* 프로브 시 리포터 등록 */
static int my_probe_health(struct my_priv *priv)
{
priv->health_reporter = devlink_health_reporter_create(
priv->devlink, &my_health_ops, 0, priv);
return IS_ERR(priv->health_reporter) ?
PTR_ERR(priv->health_reporter) : 0;
}
/* tx_timeout에서 health report 트리거 */
static void my_tx_timeout_health(struct net_device *ndev,
unsigned int txqueue)
{
struct my_priv *priv = netdev_priv(ndev);
struct my_err_ctx ctx = {
.err_type = "tx_timeout",
.queue_id = txqueue,
.timestamp = ktime_get_real_ns(),
};
devlink_health_report(priv->health_reporter,
"TX timeout detected", &ctx);
}
리셋 수준 세분화
NIC 리셋은 영향 범위에 따라 여러 수준으로 나뉩니다. 가능한 한 가장 좁은 범위의 리셋을 먼저 시도하고, 실패 시 더 넓은 범위로 에스컬레이션(Escalation)하는 것이 서비스 영향을 최소화하는 핵심입니다.
| 리셋 수준 | 영향 범위 | 서비스 중단 | 사용 시점 |
|---|---|---|---|
| 큐 리셋(Queue Reset) | 단일 TX/RX 큐 | 해당 큐만 일시 중단 (<1ms) | 단일 큐 TX timeout |
| FLR(Function-Level Reset) | 단일 PF/VF | 해당 함수의 모든 큐 중단 (~100ms) | 펌웨어 응답 없음 |
| PF 리셋(PF Reset) | PF + 소속 VF 전체 | 모든 VF 포함 중단 (~1s) | PF 펌웨어 hang |
| 글로벌 리셋(Global Reset) | NIC 전체 | 카드 모든 트래픽 중단 (~5s) | ASIC 오류 |
| PCIe 버스 리셋 | PCIe 슬롯 전체 | 슬롯 내 모든 디바이스 | AER fatal 오류 |
/* 다단계 리셋 구현 */
static int my_escalated_reset(struct my_priv *priv,
enum my_reset_level level)
{
int err;
switch (level) {
case MY_RESET_QUEUE:
netdev_info(priv->ndev, "attempting queue-level reset\n");
err = my_hw_queue_reset(priv, priv->err_queue);
if (!err) return 0;
/* fall through */
case MY_RESET_FLR:
my_ndo_stop(priv->ndev);
err = pcie_flr(priv->pdev);
if (!err && !my_hw_reinit(priv)) {
my_ndo_open(priv->ndev);
return 0;
}
/* fall through */
case MY_RESET_PF:
err = my_hw_pf_reset(priv);
if (!err) { my_ndo_open(priv->ndev); return 0; }
netdev_err(priv->ndev, "PF reset failed\n");
return err;
default: return -EINVAL;
}
}
/* devlink reload 지원 */
static int my_devlink_reload_down(struct devlink *devlink, bool netns_change,
enum devlink_reload_action action, enum devlink_reload_limit limit,
struct netlink_ext_ack *extack)
{
struct my_priv *priv = devlink_priv(devlink);
if (action == DEVLINK_RELOAD_ACTION_DRIVER_REINIT) {
rtnl_lock();
if (netif_running(priv->ndev)) my_ndo_stop(priv->ndev);
rtnl_unlock();
}
return 0;
}
static int my_devlink_reload_up(struct devlink *devlink,
enum devlink_reload_action action, enum devlink_reload_limit limit,
u32 *actions_performed, struct netlink_ext_ack *extack)
{
struct my_priv *priv = devlink_priv(devlink);
*actions_performed = BIT(action);
my_hw_reinit(priv);
rtnl_lock(); my_ndo_open(priv->ndev); rtnl_unlock();
return 0;
}
PCIe AER 복구 통합
PCIe AER(Advanced Error Reporting)은 하드웨어 오류를 감지하고 복구하는 PCIe 표준 메커니즘입니다. NIC 드라이버는 pci_error_handlers를 등록하여 커널의 AER 복구 흐름에 참여합니다. 복구는 error_detected → slot_reset → resume 세 단계로 진행됩니다.
/* 완전한 pci_error_handlers 구현 */
static pci_ers_result_t my_pci_error_detected(struct pci_dev *pdev,
pci_channel_state_t state)
{
struct my_priv *priv = pci_get_drvdata(pdev);
netdev_info(priv->ndev, "PCI error detected, state=%d\n", state);
if (state == pci_channel_io_perm_failure)
return PCI_ERS_RESULT_DISCONNECT;
rtnl_lock();
netif_device_detach(priv->ndev);
if (netif_running(priv->ndev)) my_ndo_stop(priv->ndev);
rtnl_unlock();
set_bit(MY_FLAG_PCI_ERR, &priv->flags);
pci_disable_device(pdev);
return PCI_ERS_RESULT_NEED_RESET;
}
static pci_ers_result_t my_pci_slot_reset(struct pci_dev *pdev)
{
struct my_priv *priv = pci_get_drvdata(pdev);
int err;
err = pci_enable_device(pdev);
if (err) return PCI_ERS_RESULT_DISCONNECT;
pci_set_master(pdev);
pci_restore_state(pdev);
pci_save_state(pdev);
err = my_hw_reinit(priv);
if (err) return PCI_ERS_RESULT_DISCONNECT;
return PCI_ERS_RESULT_RECOVERED;
}
static void my_pci_resume(struct pci_dev *pdev)
{
struct my_priv *priv = pci_get_drvdata(pdev);
clear_bit(MY_FLAG_PCI_ERR, &priv->flags);
rtnl_lock();
my_ndo_open(priv->ndev);
netif_device_attach(priv->ndev);
rtnl_unlock();
devlink_health_reporter_state_update(priv->health_reporter,
DEVLINK_HEALTH_REPORTER_STATE_HEALTHY);
}
static const struct pci_error_handlers my_pci_err_handlers = {
.error_detected = my_pci_error_detected,
.slot_reset = my_pci_slot_reset,
.resume = my_pci_resume,
};
error_detected 콜백에서는 절대로 PCIe MMIO에 접근하면 안 됩니다. 이 시점에서 PCI 버스가 이미 오류 상태이므로 레지스터 읽기/쓰기가 hang을 유발합니다. set_bit(MY_FLAG_PCI_ERR, ...)로 플래그를 설정하고, 데이터 경로의 모든 MMIO 접근에서 이 플래그를 확인해야 합니다.
펌웨어 리셋과 라이브 패치
devlink dev flash 명령은 NIC 펌웨어를 라이브 업데이트합니다. 드라이버는 devlink_flash_update_params를 통해 업데이트 요청을 받고, 펌웨어를 하드웨어에 기록한 후 활성화(Activation) 방식을 결정합니다.
/* 펌웨어 업데이트와 알림 구현 */
static int my_devlink_flash_update(struct devlink *devlink,
struct devlink_flash_update_params *params,
struct netlink_ext_ack *extack)
{
struct my_priv *priv = devlink_priv(devlink);
const struct firmware *fw = params->fw;
u32 offset = 0;
int err;
err = my_fw_validate_header(priv, fw->data, fw->size);
if (err) {
NL_SET_ERR_MSG_MOD(extack, "invalid firmware header");
return err;
}
if (!(params->overwrite_mask & DEVLINK_FLASH_OVERWRITE_SETTINGS)) {
if (my_fw_is_downgrade(priv, fw)) {
NL_SET_ERR_MSG_MOD(extack, "firmware downgrade not allowed");
return -EPERM;
}
}
devlink_flash_update_status_notify(devlink, "Preparing", NULL, 0, 0);
while (offset < fw->size) {
u32 chunk = min_t(u32, fw->size - offset, MY_FW_CHUNK_SIZE);
err = my_hw_write_fw_chunk(priv, fw->data + offset, chunk, offset);
if (err) return err;
offset += chunk;
devlink_flash_update_status_notify(devlink,
"Flashing", NULL, offset, fw->size);
}
if (priv->caps & MY_CAP_FW_LIVE_PATCH) {
err = my_hw_fw_activate(priv, MY_FW_ACTIVATE_IMMEDIATE);
devlink_flash_update_status_notify(devlink,
"Activated (live)", NULL, 0, 0);
} else {
err = my_hw_fw_activate(priv, MY_FW_ACTIVATE_PENDING);
devlink_flash_update_status_notify(devlink,
"Pending reset", NULL, 0, 0);
}
devlink_flash_update_timeout_notify(devlink,
"Flash complete", NULL, 120);
return err;
}
static const struct devlink_ops my_devlink_ops = {
.reload_actions = BIT(DEVLINK_RELOAD_ACTION_DRIVER_REINIT) |
BIT(DEVLINK_RELOAD_ACTION_FW_ACTIVATE),
.reload_down = my_devlink_reload_down,
.reload_up = my_devlink_reload_up,
.flash_update = my_devlink_flash_update,
};
- 즉시 활성화(Immediate): 서비스 중단 없이 새 펌웨어를 적용합니다. NIC가 라이브 패치(Live Patch)를 지원해야 하며, mlx5 ConnectX-6 이상에서 지원합니다.
- 지연 활성화(Pending): 새 펌웨어는 다음
devlink dev reload또는 시스템 재부팅 시 적용됩니다. 대부분의 NIC가 이 방식을 사용합니다. DEVLINK_FLASH_OVERWRITE_SETTINGS: 이 플래그가 설정되면 NIC 설정(MAC 주소, boot 옵션 등)도 함께 덮어씁니다. 다운그레이드 보호를 우회할 수 있으므로 주의가 필요합니다.
오프로드 계약: 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() 성능은 큐 선택 품질에 크게 좌우됩니다. 플로우 해시(Hash), 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와 드라이버 큐 매핑(Mapping) 일치 - RFS/RPS와 충돌 회피: RX CPU와 TX completion CPU가 지나치게 분산되지 않도록 설계
- NUMA: queue memory, IRQ, NAPI poll CPU를 같은 노드로 묶어 캐시 효율 확보
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의 핵심 속성은 다음과 같습니다.
- 단일 원자적(Atomic) MMIO write: 일반적으로 4바이트(32비트) 크기이며, CPU의 단일 스토어 명령으로 실행됩니다.
- PCIe 트랜잭션(Transaction) 비용: 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 비트맵(Bitmap) | 이벤트 비트 | 비트마스크 |
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 페이지(Page)를 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
Busy Poll 내부 동작 원리
Busy poll의 핵심은 napi_busy_loop() 함수입니다. 소켓(Socket)이 데이터를 기다릴 때 인터럽트 기반 수신 대신 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 스케줄링과 상호 배제됩니다.
드라이버 Busy Poll 지원 요구사항
현대 커널(v4.11+)에서 busy poll은 더 이상 별도의 ndo_busy_poll 콜백을 필요로 하지 않습니다. 대신 NAPI 기반 busy poll을 사용하며, 드라이버가 NAPI를 올바르게 구현하고 napi_id를 소켓에 연결하면 자동으로 지원됩니다.
드라이버의 busy poll 지원 핵심 요구사항은 다음과 같습니다.
- NAPI ID 할당:
netif_napi_add()호출 시 자동으로 고유napi_id가 할당됩니다. - 소켓-NAPI 연결: RX 경로에서
skb_mark_napi_id()를 호출하여 소켓이 어느 NAPI에서 패킷을 받는지 기록합니다. - prefer_busy_poll 플래그:
NAPI_STATE_PREFER_BUSY_POLL비트가 설정되면 일반 인터럽트 기반 NAPI 스케줄링이 양보합니다. - 예산 존중: poll 함수가 전달받은 budget 인자를 정확하게 존중해야 합니다.
/* 드라이버 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 기반 이벤트 루프에서도 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 사용률 | 적합 시나리오 |
|---|---|---|---|---|
| 인터럽트 기반 (기본) | ~20-50μs | ~100-200μs | 낮음 (유휴 가능) | 범용 서버, 배치 처리 |
| 인터럽트 + coalescing | ~50-100μs | ~150-300μs | 매우 낮음 | 고대역폭, CPU 절약 |
| Busy Poll (SO_BUSY_POLL) | ~2-10μs | ~5-15μs | 높음 (코어 점유) | 초저지연 트레이딩, HFT |
| Busy Poll + prefer | ~1-5μs | ~3-10μs | 매우 높음 (100%) | 전용 코어 할당 가능 환경 |
| XDP (커널 바이패스) | ~1-3μs | ~2-5μs | 중간 | L2/L3 패킷 처리, 방화벽 |
| DPDK (완전 사용자 공간) | <1μs | ~1-2μs | 매우 높음 | 통신사 패킷 처리 |
Busy poll이 유리한 경우:
- 고빈도 트레이딩(HFT, High-Frequency Trading) 환경에서 마이크로초 단위 지연이 수익에 직결되는 경우
- DPDK 도입이 어려운 환경에서 커널 네트워크 스택을 유지하면서 지연을 최소화해야 하는 경우
- 전용 CPU 코어를 네트워크 처리에 할당할 수 있는 경우 (
isolcpus,cpuset활용) - 패킷 도착률이 높아 인터럽트 오버헤드가 CPU 부하의 주요 원인인 경우
Busy poll이 불리한 경우:
- 멀티 테넌트(Multi-tenant) 환경에서 CPU를 공유해야 하는 경우
- 전력 소모가 제한된 임베디드(Embedded) 또는 모바일 환경
- 패킷 도착률이 낮아 대부분의 시간을 유휴 폴링에 소비하는 경우
- 다수의 소켓에서 동시에 busy poll을 사용하여 CPU 경합이 발생하는 경우
# 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
회귀 테스트: packetdrill, kselftest, fault injection
netdev 드라이버는 환경 의존성이 커서 재현 테스트가 어렵습니다. 릴리스 전에 최소 회귀 시나리오를 자동화하면 “간헐적 링크 다운” 같은 문제를 조기에 차단할 수 있습니다.
| 도구 | 테스트 대상 | 예시 |
|---|---|---|
| kselftest (net) | 기능 회귀 | MTU/VLAN/GRO/GSO 기본 동작 |
| packetdrill | 프로토콜 타이밍/에러 경로 | 재전송(Retransmission), out-of-order, checksum 에러 |
| tc + iperf3 | 성능/큐 안정성 | 장시간 부하 중 drop/timeout 감시 |
| fault injection | 복구 경로 | DMA map 실패, TX timeout, reset 반복 |
netdevsim을 활용한 드라이버 테스트
netdevsim은 실제 하드웨어 없이 네트워크 드라이버 기능을 테스트할 수 있는 가상 장치(Virtual Device)입니다. TC 오프로드, devlink health, XDP, flow offload 등 다양한 드라이버 인터페이스를 시뮬레이션하므로 CI/CD 파이프라인에 통합하기 적합합니다.
netdevsim이 지원하는 주요 테스트 영역은 다음과 같습니다.
- TC flower 오프로드: 하드웨어 TC 규칙 추가/삭제/수정 테스트
- devlink health: reporter 등록, 진단 덤프, 자동 복구 테스트
- XDP: XDP 프로그램 로드/언로드, 다양한 verdict(PASS, DROP, TX, REDIRECT) 테스트
- devlink trap: 하드웨어 drop 이벤트 시뮬레이션
- devlink params: 드라이버 매개변수 설정/조회 테스트
# netdevsim 테스트 스크립트
#!/bin/bash
set -e
# 1. netdevsim 장치 생성
modprobe netdevsim
echo "1 1" > /sys/bus/netdevsim/new_device
DEV_NAME=$(ls /sys/bus/netdevsim/devices/netdevsim1/net/)
echo "netdevsim 인터페이스: $DEV_NAME"
# 인터페이스 활성화
ip link set dev $DEV_NAME up
# 2. TC flower 오프로드 테스트
echo "--- TC flower 오프로드 테스트 ---"
tc qdisc add dev $DEV_NAME ingress
tc filter add dev $DEV_NAME ingress protocol ip \
flower src_ip 192.168.1.0/24 dst_ip 10.0.0.0/8 \
action drop skip_sw
# 오프로드된 규칙 확인
tc -s filter show dev $DEV_NAME ingress
echo "TC 오프로드 규칙 수:"
tc -s filter show dev $DEV_NAME ingress | grep -c "in_hw"
# 3. devlink health reporter 테스트
echo "--- devlink health 테스트 ---"
DEVLINK_DEV=$(devlink dev show | head -1 | awk '{print $1}')
devlink health show $DEVLINK_DEV
# 4. XDP 프로그램 로드 테스트
echo "--- XDP 로드 테스트 ---"
# 간단한 XDP_PASS 프로그램 (사전 컴파일 필요)
if [ -f /tmp/xdp_pass.o ]; then
ip link set dev $DEV_NAME xdp obj /tmp/xdp_pass.o sec xdp
ip link show dev $DEV_NAME | grep xdp
ip link set dev $DEV_NAME xdp off
fi
# 5. devlink trap 테스트
echo "--- devlink trap 테스트 ---"
devlink trap show $DEVLINK_DEV 2>/dev/null | head -10
# 6. 정리
echo "--- 정리 ---"
tc qdisc del dev $DEV_NAME ingress 2>/dev/null
echo 1 > /sys/bus/netdevsim/del_device
echo "netdevsim 테스트 완료"
kselftest 네트워크 테스트 실행
커널 소스 트리의 tools/testing/selftests/net/ 디렉터리에는 네트워크 기능을 검증하는 자동화 테스트가 포함되어 있습니다. 드라이버 개발자는 변경 사항이 기존 기능을 깨뜨리지 않는지 kselftest를 통해 검증해야 합니다.
주요 테스트 카테고리는 다음과 같습니다.
| 디렉터리/파일 | 테스트 대상 | 네트워크 구성 필요 |
|---|---|---|
net/forwarding/ | L2/L3 포워딩, VLAN, 브릿지 | veth pair + 네임스페이스 |
net/bonding/ | 본딩(Bonding) 모드, failover | 더미 인터페이스 |
net/tc-testing/ | TC qdisc, filter, action | 가상 인터페이스 |
net/mptcp/ | Multipath TCP | 네임스페이스 |
net/fib_tests.sh | 라우팅 테이블 (FIB) | 네임스페이스 |
net/gro.sh | GRO(Generic Receive Offload) | veth pair |
# kselftest 네트워크 테스트 실행 가이드
# 1. 커널 소스에서 전체 네트워크 셀프테스트 빌드 및 실행
cd /path/to/linux
make -C tools/testing/selftests TARGETS=net run_tests
# 2. 특정 테스트만 실행
# 포워딩 테스트
make -C tools/testing/selftests TARGETS=net/forwarding run_tests
# 3. 개별 테스트 스크립트 직접 실행
cd tools/testing/selftests/net
./fib_tests.sh
./gro.sh
# 4. 커스텀 kselftest 예시: 드라이버 기능 검증
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# 드라이버 MTU 변경 테스트
source lib.sh
# 네임스페이스 생성
setup_ns NS1 NS2
trap cleanup_ns EXIT
# veth 쌍 생성
ip link add veth0 netns $NS1 type veth peer name veth1 netns $NS2
ip -n $NS1 link set veth0 up
ip -n $NS2 link set veth1 up
ip -n $NS1 addr add 10.0.0.1/24 dev veth0
ip -n $NS2 addr add 10.0.0.2/24 dev veth1
# MTU 변경 테스트
for mtu in 576 1500 9000; do
ip -n $NS1 link set veth0 mtu $mtu
ip -n $NS2 link set veth1 mtu $mtu
# ping으로 연결 확인 (MTU - IP/ICMP 헤더)
payload_size=$((mtu - 28))
if ip netns exec $NS1 ping -c 3 -s $payload_size -M do 10.0.0.2 >/dev/null 2>&1; then
echo "PASS: MTU $mtu - ping 성공"
else
echo "FAIL: MTU $mtu - ping 실패"
exit 1
fi
done
echo "모든 MTU 테스트 통과"
exit 0
Fault Injection을 통한 복구 경로 검증
정상 경로(Happy Path)만 테스트해서는 드라이버의 안정성을 보장할 수 없습니다. DMA 매핑 실패, 메모리 할당 실패, 함수 에러 반환 등 오류 경로를 의도적으로 유발하여 드라이버의 복구 로직이 올바르게 동작하는지 검증해야 합니다.
커널은 다양한 오류 주입(Fault Injection) 프레임워크를 제공합니다.
- fail_function: 특정 커널 함수가 에러를 반환하도록 설정합니다. DMA 매핑 함수(
dma_map_single)를 실패시켜 드라이버의 DMA 에러 처리를 검증할 수 있습니다. - fail_page_alloc: 페이지 할당 실패를 시뮬레이션합니다. 링 버퍼 리필(Refill) 실패 시 드라이버가 graceful하게 대응하는지 확인합니다.
- fail_make_request: 블록 I/O 요청 실패를 유발합니다 (네트워크보다는 스토리지 드라이버에 유용).
- KFENCE: 메모리 접근 오류를 샘플링 기반으로 감지합니다.
# Fault Injection 테스트 스크립트
#!/bin/bash
set -e
DEV="eth0"
FAULT_DIR="/sys/kernel/debug/fail_function"
echo "=== Fault Injection 복구 경로 테스트 ==="
# 필수 커널 설정 확인
# CONFIG_FAULT_INJECTION=y
# CONFIG_FAIL_FUNCTION=y
# CONFIG_FAULT_INJECTION_DEBUG_FS=y
if [ ! -d "$FAULT_DIR" ]; then
echo "ERROR: fail_function not available"
echo "커널 CONFIG_FAIL_FUNCTION=y 필요"
exit 1
fi
# 테스트 1: DMA 매핑 실패 시 복구
echo "--- 테스트 1: dma_map_single 실패 ---"
echo dma_map_single > $FAULT_DIR/inject
# 확률 설정: 10% 확률로 실패
echo 10 > /sys/kernel/debug/fail_function/probability
echo 1 > /sys/kernel/debug/fail_function/times
echo 0 > /sys/kernel/debug/fail_function/space
echo 1 > /sys/kernel/debug/fail_function/verbose
# 부하 생성하여 오류 경로 유발
ping -c 100 -f 10.0.0.2 2>/dev/null || true
# 드라이버 상태 확인 (크래시/행 여부)
if ip link show $DEV >/dev/null 2>&1; then
echo "PASS: 인터페이스 정상 유지"
else
echo "FAIL: 인터페이스 비정상"
fi
# 오류 주입 해제
echo > $FAULT_DIR/inject
# 테스트 2: 메모리 할당 실패 (fail_page_alloc)
echo "--- 테스트 2: 페이지 할당 실패 ---"
echo 1 > /proc/sys/vm/fault_injection/fail_page_alloc/probability
echo 5 > /proc/sys/vm/fault_injection/fail_page_alloc/times
# RX 링 리필 경로 유발 (대량 패킷 수신)
ip netns exec ns_remote iperf3 -c 10.0.0.1 -t 5 -b 1G 2>/dev/null || true
# 오류 주입 해제
echo 0 > /proc/sys/vm/fault_injection/fail_page_alloc/probability
# 통계 확인
echo "--- 드라이버 에러 카운터 ---"
ethtool -S $DEV | grep -iE "alloc.*fail|dma.*err|drop"
# dmesg에서 드라이버 경고/에러 확인
echo "--- 관련 dmesg ---"
dmesg | tail -20 | grep -iE "$DEV|dma|alloc|fail"
echo "Fault injection 테스트 완료"
네트워크 네임스페이스 기반 격리 테스트
네트워크 네임스페이스(Network Namespace)를 사용하면 호스트 네트워크에 영향을 주지 않고 드라이버 기능을 안전하게 테스트할 수 있습니다. veth 쌍으로 격리된 환경을 만들고, iperf3/netperf로 부하를 생성하며, tcpdump/tshark로 패킷을 검증하는 테스트 하네스(Test Harness)를 구축합니다.
# 네임스페이스 기반 종합 테스트 하네스
#!/bin/bash
set -e
# 네임스페이스 이름
NS1="ns_sender"
NS2="ns_receiver"
RESULTS="/tmp/netns-test-$(date +%s)"
mkdir -p $RESULTS
cleanup() {
ip netns del $NS1 2>/dev/null || true
ip netns del $NS2 2>/dev/null || true
echo "정리 완료"
}
trap cleanup EXIT
# 1. 네임스페이스 및 veth 쌍 생성
echo "=== 네임스페이스 환경 구성 ==="
ip netns add $NS1
ip netns add $NS2
ip link add veth-s type veth peer name veth-r
ip link set veth-s netns $NS1
ip link set veth-r netns $NS2
# IP 설정
ip netns exec $NS1 ip addr add 10.0.0.1/24 dev veth-s
ip netns exec $NS2 ip addr add 10.0.0.2/24 dev veth-r
ip netns exec $NS1 ip link set veth-s up
ip netns exec $NS2 ip link set veth-r up
ip netns exec $NS1 ip link set lo up
ip netns exec $NS2 ip link set lo up
# 2. 기본 연결 테스트
echo "--- 기본 연결 ---"
ip netns exec $NS1 ping -c 3 10.0.0.2 | tail -1
# 3. MTU 변경 테스트
echo "--- MTU 테스트 ---"
for mtu in 68 576 1500 9000; do
ip netns exec $NS1 ip link set veth-s mtu $mtu
ip netns exec $NS2 ip link set veth-r mtu $mtu
if ip netns exec $NS1 ping -c 1 -s $((mtu - 28)) -M do 10.0.0.2 >/dev/null 2>&1; then
echo " MTU $mtu: PASS"
else
echo " MTU $mtu: FAIL"
fi
done
# MTU 복원
ip netns exec $NS1 ip link set veth-s mtu 1500
ip netns exec $NS2 ip link set veth-r mtu 1500
# 4. 처리량 테스트 (iperf3)
echo "--- 처리량 테스트 ---"
ip netns exec $NS2 iperf3 -s -D -p 5201
sleep 1
ip netns exec $NS1 iperf3 -c 10.0.0.2 -t 10 -p 5201 \
--json > $RESULTS/iperf3.json
# 결과 추출
python3 -c "
import json, sys
data = json.load(open('$RESULTS/iperf3.json'))
bps = data['end']['sum_sent']['bits_per_second']
print(f' 처리량: {bps/1e9:.2f} Gbps')
"
# 5. 패킷 캡처 검증
echo "--- 패킷 캡처 테스트 ---"
ip netns exec $NS2 tcpdump -i veth-r -c 10 -w $RESULTS/capture.pcap &
TCPDUMP_PID=$!
sleep 1
ip netns exec $NS1 ping -c 5 10.0.0.2 >/dev/null
sleep 2
kill $TCPDUMP_PID 2>/dev/null || true
echo " 캡처 파일: $RESULTS/capture.pcap"
tcpdump -r $RESULTS/capture.pcap -q 2>/dev/null | wc -l | \
xargs -I{} echo " 캡처된 패킷: {} 개"
# 6. GRO/GSO 테스트
echo "--- GRO/GSO 오프로드 테스트 ---"
for feature in gro gso tso; do
ip netns exec $NS1 ethtool -K veth-s $feature off 2>/dev/null
ip netns exec $NS1 ping -c 1 10.0.0.2 >/dev/null 2>&1 && \
echo " $feature off: PASS" || echo " $feature off: FAIL"
ip netns exec $NS1 ethtool -K veth-s $feature on 2>/dev/null
done
# iperf3 서버 정리
pkill -f "iperf3 -s" 2>/dev/null || true
echo "=== 테스트 완료. 결과: $RESULTS ==="
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 |
| PFC | priority별 pause on/off 적용 | drop 급증 또는 head-of-line blocking |
| ETS | bandwidth share를 HW arbitration에 반영 | 대역폭 분배 불일치 |
mqprio hw vs sw 모드
mqprio qdisc는 트래픽 클래스(Traffic Class, TC)별로 TX 큐를 매핑하는 멀티큐 우선순위 스케줄러입니다. 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 프레임의 동작 원리는 다음과 같습니다.
- 수신 측: 수신 버퍼가 임계값에 도달하면 해당 우선순위의 pause 프레임을 송신 측에 전송합니다.
- 송신 측: pause 프레임을 수신하면 해당 우선순위의 전송을 일시 중단합니다. 다른 우선순위의 트래픽은 영향받지 않습니다.
- 타이머: pause 프레임에는 quanta 단위의 타이머가 포함되어 있어, 타이머 만료 후 자동으로 전송이 재개됩니다.
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)가 핵심 트래픽 제어 메커니즘입니다. 리눅스 커널은 cbs와 taprio qdisc를 통해 이들을 지원하며, 하드웨어 오프로드가 가능한 NIC에서는 정밀한 타이밍 제어가 가능합니다.
- CBS (802.1Qav): 크레딧 기반 쉐이핑으로, 각 트래픽 클래스에 대역폭 한도를 설정합니다. 크레딧이 양수일 때만 전송이 허용되어 대역폭을 공정하게 분배합니다.
- TAS (802.1Qbv): 시간 기반 게이트 제어로, 사전 정의된 스케줄에 따라 특정 시점에 특정 트래픽 클래스만 전송을 허용합니다. 결정론적(Deterministic) 지연을 보장합니다.
- taprio qdisc: TAS를 리눅스에서 구현한 qdisc입니다.
flags 0x2로 하드웨어 오프로드,flags 0x1로 전체 오프로드를 설정합니다.
# 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 기능 | qdisc | igc (Intel i225) | stmmac (Intel EHL) | enetc (NXP) | am65 (TI) |
|---|---|---|---|---|---|
| CBS (802.1Qav) | cbs | HW 오프로드 | HW 오프로드 | HW 오프로드 | HW 오프로드 |
| TAS (802.1Qbv) | taprio | HW 오프로드 | HW 오프로드 | HW 오프로드 | HW 오프로드 |
| Frame Preemption (802.1Qbu) | ethtool | 지원 | 지원 | 미지원 | 미지원 |
| PTP (802.1AS) | ptp4l | HW 타임스탬프 | HW 타임스탬프 | HW 타임스탬프 | HW 타임스탬프 |
| Launch Time (ETF) | etf | 지원 | 지원 | 지원 | 미지원 |
PREEMPT_RT와 NAPI threaded 모드
실시간 커널에서는 IRQ/softirq 모델이 일반 커널과 다르게 동작합니다. 드라이버는 spinlock 길이를 줄이고, napi poll 지연 상한을 보장하도록 설계해야 합니다.
- 긴 IRQ-off 구간 제거: TX reclaim 루프를 짧게 쪼개고 예산 기반 처리 유지
- 락 경쟁 최소화: 큐별 락 분리, 필요 시 lockless ring 고려
- 우선순위 설계: IRQ thread/NAPI 실행 CPU를 RT 태스크(Task)와 분리
- 지연 측정:
cyclictest와 네트워크 부하를 동시 실행해 tail latency 확인
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 패치가 적용된 커널에서는 네트워크 스택의 동작이 크게 달라집니다. 모든 인터럽트가 스레드화(Forced Threading)되고, spinlock이 슬리핑 뮤텍스(Sleeping Mutex)로 변환되며, local_bh_disable()이 뮤텍스 기반 보호로 변경됩니다. 이러한 변화가 드라이버에 미치는 영향을 이해해야 합니다.
| 항목 | 일반 커널 | PREEMPT_RT 커널 | 드라이버 영향 |
|---|---|---|---|
| 하드 IRQ | 인터럽트 컨텍스트에서 실행 | 스레드에서 실행 (강제 스레딩) | IRQ 핸들러에서 preemption 가능 |
| softirq | ksoftirqd 또는 IRQ 반환 시 실행 | 전용 스레드 (rcuc/N 등) | NAPI poll 지연 증가 가능 |
| spinlock | busy-wait, preemption 비활성 | rt_mutex (슬리핑 가능) | 우선순위 역전(Priority Inversion) 해소 |
| local_bh_disable | softirq 비활성화 | per-CPU 뮤텍스 획득 | 경합 시 슬리핑 가능 |
| raw_spinlock | spinlock과 동일 | 진짜 spinlock (busy-wait 유지) | 극히 짧은 임계 구간에만 사용 |
| timer softirq | softirq에서 처리 | 전용 스레드 | 타이머 콜백 지연 가능 |
/* 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;
}
실시간 네트워크 지연 측정
실시간 네트워크 시스템의 성능은 평균 처리량이 아니라 최악의 경우 지연(Worst-case Latency)으로 평가합니다. cyclictest, oslat, hwlatdetect 등의 도구를 네트워크 부하와 함께 실행하여 실제 운영 환경에서의 꼬리 지연(Tail Latency)을 측정해야 합니다.
- cyclictest: 타이머 기반 지연 측정 도구로, 스레드 깨우기 지연을 마이크로초 단위로 측정합니다. 네트워크 부하를 동시에 걸어 간섭(Interference) 효과를 측정합니다.
- oslat: Red Hat이 개발한 OS 레벨 지연 측정 도구로, busy-loop 기반으로 OS가 유발하는 지터(Jitter)를 정밀하게 측정합니다.
- hwlatdetect: 하드웨어가 유발하는 지연(SMI, NMI 등)을 감지합니다. 네트워크 장비에서 SMI(System Management Interrupt)가 간헐적 패킷 손실의 원인인 경우가 있습니다.
- 네트워크 RTT:
ping -i 0.001이나 전용 PTP(Precision Time Protocol) 장비로 네트워크 왕복 지연을 측정합니다.
# 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"
Netpoll/kdump 경로 지원
패닉 상황에서 네트워크 로그 덤프(Dump)가 필요하면 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);
}
netpoll 내부 아키텍처
netpoll은 커널 패닉(Kernel Panic)이나 인터럽트가 비활성화된 극한 상황에서도 네트워크 패킷을 송신할 수 있는 최소한의 네트워크 경로입니다. struct netpoll은 일반 네트워크 스택을 우회하여 드라이버의 ndo_start_xmit을 직접 호출하는 폴링 기반(Polling-based) 전송 메커니즘을 제공합니다.
netpoll_send_skb_on_dev()의 내부 동작을 단계별로 분석하면 다음과 같습니다.
- trylock 획득: TX 큐의
_xmit_lock을__netif_tx_trylock()으로 시도합니다. 이미 잠겨 있으면(일반 경로가 사용 중) 직접 NAPI poll을 호출하여 TX completion을 처리한 뒤 재시도합니다. - 직접 전송: 락 획득에 성공하면
netpoll_start_xmit()을 통해 드라이버의ndo_start_xmit을 호출합니다. 일반 경로의 qdisc, TC, BQL 등을 모두 우회합니다. - 재시도 루프: 전송 실패 시
netpoll_poll_dev()로 NAPI poll을 호출하여 링 공간을 확보하고 최대NETPOLL_MAX_RETRIES(기본 20000)번 재시도합니다. - 인터럽트 폴링: 인터럽트가 비활성화된 상태이므로 completion을 받기 위해 드라이버의 poll 함수를 직접 호출합니다.
/* netpoll 드라이버 통합 예시
* 드라이버가 netpoll을 지원하려면 몇 가지 조건을 충족해야 합니다 */
/* 1. ndo_poll_controller 구현 (NAPI 기반) */
static void my_poll_controller(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
int i;
/* 모든 큐의 인터럽트를 비활성화하고 poll 수행 */
for (i = 0; i < priv->num_queues; i++) {
disable_irq(priv->queues[i].irq);
napi_schedule(&priv->queues[i].napi);
enable_irq(priv->queues[i].irq);
}
}
/* 2. xmit 경로에서 netpoll 안전성 확보 */
static netdev_tx_t my_start_xmit(struct sk_buff *skb,
struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
struct my_tx_ring *tx;
int qidx;
/* netpoll 경로에서는 큐 선택이 제한적 */
qidx = skb_get_queue_mapping(skb);
if (qidx >= priv->num_tx_queues)
qidx = 0;
tx = &priv->tx_ring[qidx];
/* sleep 가능한 함수 호출 금지: mutex, kmalloc(GFP_KERNEL) 등
* netpoll 경로는 인터럽트 컨텍스트에서 실행될 수 있음 */
if (unlikely(!my_tx_has_space(tx))) {
/* netpoll에서는 queue stop 대신 바로 drop */
if (unlikely(skb->dev->priv_flags & IFF_IN_NETPOLL)) {
dev_kfree_skb_any(skb);
return NETDEV_TX_OK;
}
netif_stop_subqueue(ndev, qidx);
return NETDEV_TX_BUSY;
}
my_fill_and_submit(tx, skb);
return NETDEV_TX_OK;
}
/* 3. net_device_ops에 등록 */
static const struct net_device_ops my_netdev_ops = {
.ndo_open = my_open,
.ndo_stop = my_stop,
.ndo_start_xmit = my_start_xmit,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = my_poll_controller,
#endif
};
netconsole 설정과 운영
netconsole은 netpoll 위에 구축된 커널 로그 전송 도구입니다. 시리얼 콘솔이 없는 환경에서 커널 메시지를 원격 서버로 전송할 수 있으며, 확장 netconsole(Extended Netconsole)은 사용자 정의 메타데이터까지 포함할 수 있습니다.
# netconsole 설정 스크립트
#!/bin/bash
# 기본 netconsole 설정
# 형식: netconsole=[+][src-port]@[src-ip]/[dev],[tgt-port]@/[tgt-macaddr]
# 모듈로 로드 (동적 설정 가능)
modprobe netconsole \
netconsole=@10.0.0.1/eth0,6666@10.0.0.2/aa:bb:cc:dd:ee:ff
# 또는 configfs를 통한 동적 설정 (확장 netconsole)
mkdir -p /sys/kernel/config/netconsole/target1
cd /sys/kernel/config/netconsole/target1
echo 10.0.0.1 > local_ip
echo 6665 > local_port
echo eth0 > dev_name
echo 10.0.0.2 > remote_ip
echo 6666 > remote_port
echo aa:bb:cc:dd:ee:ff > remote_mac
# 확장 netconsole: 사용자 정의 데이터 추가 (v5.18+)
echo 1 > extended
mkdir userdata/hostname
echo "$(hostname)" > userdata/hostname/value
mkdir userdata/kernel_version
echo "$(uname -r)" > userdata/kernel_version/value
# 활성화
echo 1 > enabled
# 상태 확인
echo "=== netconsole 대상 목록 ==="
ls /sys/kernel/config/netconsole/
# 수신 측 (원격 서버)에서 netconsole 메시지 수신
# 간단한 수신기: nc -u -l 6666
# 또는 syslog 데몬으로 UDP 수신 설정
# 테스트: 커널 로그 강제 출력
echo "netconsole test $(date)" > /dev/kmsg
kdump에서의 네트워크 드라이버 제약
kdump는 커널 패닉 시 캡처 커널(Capture Kernel)을 부팅하여 크래시 덤프(Crash Dump)를 저장합니다. 캡처 커널 환경은 일반 부팅과 크게 다르며, 네트워크 드라이버는 여러 제약 조건 하에서 동작해야 합니다.
- 펌웨어 상태 상속: NIC 펌웨어가 크래시 시점의 상태를 그대로 유지합니다. 펌웨어 리셋이 완전하지 않으면 드라이버 초기화가 실패할 수 있습니다.
- 제한된 메모리: 캡처 커널은
crashkernel=파라미터로 예약된 적은 메모리에서 실행됩니다. 큰 링 버퍼 할당이나 대량의 DMA 매핑이 실패할 수 있습니다. - 단일 CPU: 캡처 커널은 일반적으로 단일 CPU에서 실행됩니다. 멀티큐 드라이버는 큐 수를 최소화해야 합니다.
- IOMMU 상태: 크래시 시점의 IOMMU 매핑이 남아 있어 DMA 주소 충돌이 발생할 수 있습니다.
| 제약 사항 | 영향 | 드라이버 대응 전략 | 확인 방법 |
|---|---|---|---|
| 펌웨어 비정상 상태 | probe 실패, 행(hang) | FW 강제 리셋 (FLR/PCIe reset) | 캡처 커널에서 수동 테스트 |
| 메모리 부족 | ring alloc 실패 | 링 크기 축소 매개변수 지원 | crashkernel= 크기 조정 |
| 단일 CPU | 멀티큐 초기화 실패 | 큐 수 자동 감소 로직 | num_online_cpus() 확인 |
| MSI-X 벡터 제한 | 인터럽트 할당 실패 | INTx 폴백(Fallback) 지원 | /proc/interrupts 확인 |
| IOMMU 비활성 | DMA 주소 매핑 차이 | swiotlb 대응 | dmesg | grep SWIOTLB |
| initramfs 불일치 | 펌웨어 파일 누락 | dracut에 FW 파일 포함 | lsinitrd로 확인 |
# kdump 환경에서 네트워크 드라이버 호환성 테스트
#!/bin/bash
echo "=== kdump 네트워크 드라이버 호환성 점검 ==="
# 1. kdump 서비스 상태 확인
systemctl status kdump
# 2. crashkernel 예약 메모리 확인
echo "--- crashkernel 설정 ---"
cat /proc/cmdline | grep -o 'crashkernel=[^ ]*'
echo "예약된 메모리:"
cat /proc/iomem | grep "Crash kernel"
# 3. 캡처 커널 initramfs에 NIC 펌웨어 포함 여부 확인
NIC_DRIVER=$(ethtool -i eth0 | grep driver | awk '{print $2}')
echo "--- NIC 드라이버: $NIC_DRIVER ---"
# 펌웨어 파일 경로 확인
modinfo $NIC_DRIVER | grep firmware
echo "--- initramfs 내 펌웨어 파일 ---"
KDUMP_INITRD=$(grep -r initrd /etc/kdump.conf 2>/dev/null | awk '{print $2}')
if [ -n "$KDUMP_INITRD" ]; then
lsinitrd $KDUMP_INITRD | grep -i firmware | grep -i $NIC_DRIVER
else
echo "기본 initramfs 사용 중"
lsinitrd /boot/initramfs-$(uname -r)kdump.img | grep -i firmware 2>/dev/null
fi
# 4. 드라이버의 netpoll 지원 여부
echo "--- netpoll 지원 여부 ---"
grep -c poll_controller /sys/class/net/eth0/device/driver/module/holders/ 2>/dev/null
# 또는 커널 소스에서 ndo_poll_controller 구현 확인
# 5. NIC reset 테스트 (주의: 일시적 연결 끊김)
echo "--- FLR(Function Level Reset) 지원 ---"
PCI_ADDR=$(ethtool -i eth0 | grep bus-info | awk '{print $2}')
lspci -vvs $PCI_ADDR | grep -i "FLR"
보안 하드닝: 입력 검증과 경계 조건
네트워크 드라이버 취약점(Vulnerability)은 원격 트리거 가능성이 있습니다. 길이 검증, 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 |
RX 패킷 길이 검증 패턴
네트워크 드라이버에서 가장 흔한 취약점은 하드웨어가 보고한 패킷 길이를 무조건 신뢰하는 것입니다. NIC 펌웨어 버그, DMA 오류, 악의적인 패킷 조작 등으로 인해 실제 데이터 크기와 보고된 길이가 다를 수 있으며, 이를 검증하지 않으면 버퍼 오버리드(Buffer Overread) 또는 힙 오버플로우(Heap Overflow)가 발생합니다.
실제 CVE 사례를 통해 위험성을 확인할 수 있습니다.
- CVE-2019-14896 (Marvell WiFi): 하드웨어가 보고한 길이를 검증 없이
memcpy에 사용하여 힙 오버플로우가 발생했습니다. 원격 코드 실행(RCE)이 가능한 치명적 취약점이었습니다. - CVE-2021-45486 (IPv4): 패킷 헤더 파싱 시 길이 필드 검증 부족으로 정보 누출이 발생했습니다.
- CVE-2022-41674 (WiFi): 관리 프레임(Management Frame) 파싱 중 길이 검증 누락으로 버퍼 오버플로우가 발생했습니다.
/* 안전한 RX 패킷 길이 검증 패턴 */
/* 1단계: 하드웨어 보고 길이의 기본 범위 검증 */
static bool my_validate_rx_length(struct my_rx_desc *desc,
struct net_device *ndev)
{
u32 len = le32_to_cpu(desc->length);
/* 최소 길이: 이더넷 헤더(14) + 최소 페이로드 */
if (unlikely(len < ETH_HLEN)) {
ndev->stats.rx_length_errors++;
return false;
}
/* 최대 길이: MTU + 헤더 + VLAN 태그 + FCS */
if (unlikely(len > ndev->mtu + ETH_HLEN + VLAN_HLEN + ETH_FCS_LEN)) {
ndev->stats.rx_length_errors++;
return false;
}
/* DMA 버퍼 크기를 초과하지 않는지 확인 */
if (unlikely(len > MY_RX_BUF_SIZE)) {
netdev_warn_once(ndev,
"RX length %u exceeds buffer size %u\n",
len, MY_RX_BUF_SIZE);
ndev->stats.rx_length_errors++;
return false;
}
return true;
}
/* 2단계: 헤더 파싱 시 경계 검사 */
static int my_parse_rx_headers(struct sk_buff *skb)
{
struct ethhdr *eth;
struct iphdr *iph;
/* pskb_may_pull()로 최소 헤더 크기만큼 linear 보장 */
if (!pskb_may_pull(skb, ETH_HLEN))
return -EINVAL;
eth = eth_hdr(skb);
if (eth->h_proto == htons(ETH_P_IP)) {
/* IP 헤더 접근 전 추가 pull 필요 */
if (!pskb_may_pull(skb, ETH_HLEN + sizeof(*iph)))
return -EINVAL;
iph = ip_hdr(skb);
/* IP 헤더 길이 검증 (IHL 최소 5) */
if (iph->ihl < 5)
return -EINVAL;
/* IP 총 길이와 SKB 길이 일관성 확인 */
if (ntohs(iph->tot_len) > skb->len - ETH_HLEN)
return -EINVAL;
}
return 0;
}
/* 3단계: DMA coherent 버퍼 접근 안전 패턴 */
static void my_safe_dma_read(struct my_ring *ring, u32 idx)
{
struct my_rx_desc *desc;
/* DMA 동기화: CPU가 최신 데이터를 보도록 보장 */
dma_rmb();
desc = &ring->desc[idx];
/* volatile 읽기 또는 READ_ONCE로 컴파일러 최적화 방지 */
u32 status = READ_ONCE(desc->status);
u32 length = READ_ONCE(desc->length);
/* 동일 descriptor를 두 번 읽으면 값이 바뀔 수 있음
* (TOCTOU 방지) - 한 번 읽은 값을 로컬 변수에 저장 */
}
Ring Buffer 오버플로우 방지
링 버퍼의 프로듀서(Producer)/컨슈머(Consumer) 인덱스 관리에서 정수 오버플로우(Integer Overflow)나 래핑(Wrapping) 오류가 발생하면 임의 메모리 접근이 가능해집니다. 안전한 인덱스 관리 패턴을 일관되게 적용해야 합니다.
링 버퍼 인덱스 관리의 핵심 원칙은 다음과 같습니다.
- 마스크 기반 인덱싱: 링 크기를 2의 거듭제곱으로 설정하고 비트마스크(
size - 1)로 인덱스를 래핑합니다. 모듈로 연산(%)보다 안전하고 빠릅니다. - 소유권 검증: 디스크립터에 소유권 비트를 두어 드라이버와 하드웨어가 동시에 같은 디스크립터에 접근하지 않도록 합니다.
- 빈/가득참 구분: 프로듀서와 컨슈머 인덱스가 동일할 때 빈 상태와 가득 찬 상태를 구분하기 위해 하나의 슬롯을 비워두거나 별도 카운터를 사용합니다.
/* 안전한 Ring Buffer 인덱스 관리 패턴 */
#define MY_RING_SIZE 1024 /* 반드시 2의 거듭제곱 */
#define MY_RING_MASK (MY_RING_SIZE - 1)
struct my_ring {
struct my_desc *desc; /* DMA coherent 디스크립터 배열 */
u32 prod; /* 프로듀서 인덱스 (다음 쓸 위치) */
u32 cons; /* 컨슈머 인덱스 (다음 읽을 위치) */
u32 size; /* 링 크기 */
u32 mask; /* size - 1 */
};
/* 안전한 인덱스 래핑 - 항상 마스크 사용 */
static inline u32 ring_next(struct my_ring *ring, u32 idx)
{
return (idx + 1) & ring->mask;
}
/* 사용 가능한 슬롯 수 계산 (오버플로우 안전) */
static inline u32 ring_space(struct my_ring *ring)
{
/* u32 래핑을 활용: prod - cons는 항상 올바른 값 */
return ring->size - (ring->prod - ring->cons) - 1;
}
/* 처리 대기 중인 디스크립터 수 */
static inline u32 ring_pending(struct my_ring *ring)
{
return ring->prod - ring->cons;
}
/* TX: 안전한 디스크립터 제출 */
static netdev_tx_t my_safe_xmit(struct sk_buff *skb,
struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
struct my_ring *tx = &priv->tx_ring;
u32 idx;
/* 공간 확인 - 경쟁 조건 방지를 위해 READ_ONCE 사용 */
if (unlikely(ring_space(tx) == 0)) {
netif_stop_queue(ndev);
/* 재확인: stop과 check 사이에 completion이 올 수 있음 */
smp_mb();
if (ring_space(tx) > 0)
netif_wake_queue(ndev);
else
return NETDEV_TX_BUSY;
}
/* 마스크 기반 인덱싱으로 범위 초과 불가 */
idx = tx->prod & tx->mask;
/* WARN_ON으로 디버그 빌드에서 invariant 검증 */
WARN_ON_ONCE(idx >= tx->size);
/* 디스크립터 소유권 확인 */
if (unlikely(READ_ONCE(tx->desc[idx].flags) & DESC_HW_OWNED)) {
netdev_err(ndev, "TX desc %u still owned by HW\n", idx);
return NETDEV_TX_BUSY;
}
my_fill_tx_desc(tx, idx, skb);
tx->prod++; /* u32 자연 래핑에 의존 */
return NETDEV_TX_OK;
}
IOCTL/Netlink 입력 검증
네트워크 드라이버의 IOCTL 및 Netlink 핸들러는 사용자 공간(Userspace)에서 전달되는 데이터를 처리합니다. 모든 사용자 입력은 신뢰할 수 없으며(Untrusted), 매개변수 범위 검증, 권한 확인(Capability Check), 경계 조건 처리를 철저히 수행해야 합니다.
- CAP_NET_ADMIN 권한 확인: 장치 설정을 변경하는 모든 IOCTL은
capable(CAP_NET_ADMIN)을 먼저 확인해야 합니다. - 복사 크기 검증:
copy_from_user()에 전달하는 크기가 커널 버퍼를 초과하지 않도록 검증합니다. - 정수 오버플로우: 사용자가 전달한 값으로 산술 연산을 수행할 때 오버플로우 가능성을 확인합니다.
- 열거형 범위: 모드, 플래그 등의 값이 유효한 범위 내인지 확인합니다.
/* 안전한 ethtool IOCTL 핸들러 패턴 */
/* 링 크기 설정 - 범위 검증 필수 */
static int my_set_ringparam(struct net_device *ndev,
struct ethtool_ringparam *ring,
struct kernel_ethtool_ringparam *kernel_ring,
struct netlink_ext_ack *extack)
{
struct my_priv *priv = netdev_priv(ndev);
/* 최소/최대 범위 검증 */
if (ring->rx_pending < MY_MIN_RING_SIZE ||
ring->rx_pending > MY_MAX_RING_SIZE) {
NL_SET_ERR_MSG_MOD(extack,
"RX ring size out of range");
return -EINVAL;
}
/* 2의 거듭제곱 정렬 검증 */
if (!is_power_of_2(ring->rx_pending)) {
NL_SET_ERR_MSG_MOD(extack,
"RX ring size must be power of 2");
return -EINVAL;
}
/* TX 링 크기도 동일하게 검증 */
if (ring->tx_pending < MY_MIN_RING_SIZE ||
ring->tx_pending > MY_MAX_RING_SIZE ||
!is_power_of_2(ring->tx_pending)) {
NL_SET_ERR_MSG_MOD(extack,
"TX ring size invalid");
return -EINVAL;
}
/* 설정 적용 (락 보호 하에) */
mutex_lock(&priv->conf_lock);
priv->rx_ring_size = ring->rx_pending;
priv->tx_ring_size = ring->tx_pending;
mutex_unlock(&priv->conf_lock);
/* 런타임 적용은 인터페이스 재시작 필요 */
if (netif_running(ndev))
return my_restart_dev(priv);
return 0;
}
/* Private flags 설정 - 비트 범위 검증 */
static int my_set_priv_flags(struct net_device *ndev, u32 flags)
{
struct my_priv *priv = netdev_priv(ndev);
u32 changed;
/* 알 수 없는 플래그 비트가 설정되어 있으면 거부 */
if (flags & ~MY_KNOWN_PRIV_FLAGS) {
netdev_warn(ndev, "unknown priv flags: 0x%x\n",
flags & ~MY_KNOWN_PRIV_FLAGS);
return -EINVAL;
}
changed = priv->priv_flags ^ flags;
priv->priv_flags = flags;
/* 변경된 플래그에 따라 필요한 재구성 수행 */
if (changed & MY_PRIV_FLAG_NAPI_BUSY_POLL)
my_reconfigure_napi(priv);
return 0;
}
퍼저(Fuzzer) 기반 보안 테스트
네트워크 드라이버는 외부에서 들어오는 패킷을 처리하므로 공격 표면(Attack Surface)이 넓습니다. 퍼징(Fuzzing)은 무작위 또는 반무작위 입력을 자동 생성하여 예상치 못한 코드 경로와 크래시를 발견하는 효과적인 보안 테스트 방법입니다.
주요 퍼징 도구와 적용 방법은 다음과 같습니다.
| 도구 | 퍼징 대상 | 커버리지 유형 | 적용 난이도 |
|---|---|---|---|
| syzkaller | 시스콜(Syscall) 인터페이스, IOCTL | 커버리지 기반 (KCOV) | 중간 |
| Scapy | 프로토콜 파싱, 패킷 처리 경로 | 생성 기반 (수동 규칙) | 낮음 |
| AFL/libFuzzer | 사용자 공간 유틸리티 | 커버리지 기반 | 낮음 |
| kcov + custom | 특정 드라이버 경로 | 커버리지 가이드 | 높음 |
# syzkaller 설정 예시: 네트워크 드라이버 대상 퍼징
# 1. 커널 빌드 옵션 (필수)
# CONFIG_KCOV=y
# CONFIG_KASAN=y (메모리 오류 감지)
# CONFIG_UBSAN=y (정의되지 않은 동작 감지)
# CONFIG_LOCKDEP=y (락 오류 감지)
# CONFIG_DEBUG_KMEMLEAK=y
# 2. syzkaller 설정 파일 (syz-manager.cfg)
cat <<EOF > syz-manager.cfg
{
"target": "linux/amd64",
"http": "127.0.0.1:56741",
"workdir": "/tmp/syzkaller-workdir",
"kernel_obj": "/path/to/linux/build",
"image": "/path/to/stretch.img",
"sshkey": "/path/to/stretch.id_rsa",
"syzkaller": "/path/to/syzkaller",
"procs": 8,
"type": "qemu",
"vm": {
"count": 4,
"kernel": "/path/to/linux/build/arch/x86/boot/bzImage",
"cpu": 2,
"mem": 2048
},
"enable_syscalls": [
"setsockopt", "getsockopt",
"ioctl\$SIOCETHTOOL",
"ioctl\$SIOCDEVPRIVATE",
"sendmsg", "recvmsg",
"syz_emit_ethernet"
]
}
EOF
# 3. 실행
syz-manager -config=syz-manager.cfg
# Scapy를 사용한 패킷 퍼징 예시
python3 <<'PYEOF'
from scapy.all import *
import random
# 대상 인터페이스
iface = "eth0"
# 기본 이더넷 프레임에 무작위 페이로드
for i in range(10000):
# 무작위 EtherType
etype = random.randint(0, 0xFFFF)
# 무작위 길이 페이로드 (0 ~ 9000 바이트)
payload_len = random.randint(0, 9000)
payload = bytes(random.getrandbits(8) for _ in range(payload_len))
pkt = Ether(type=etype) / Raw(load=payload)
sendp(pkt, iface=iface, verbose=False)
# 비정상 IP 헤더 퍼징
if i % 10 == 0:
ip_pkt = Ether() / IP(
ihl=random.randint(0, 15),
tot_len=random.randint(0, 65535),
frag=random.randint(0, 8191),
proto=random.randint(0, 255)
) / Raw(load=payload[:100])
sendp(ip_pkt, iface=iface, verbose=False)
print("퍼징 완료: 10000 패킷 전송")
PYEOF
가상 netdev: TUN/TAP, veth, virtio-net
가상 인터페이스도 본질적으로는 net_device입니다. 차이는 “패킷을 어디로 내보내는가”에 있습니다. 물리 NIC는 DMA 링으로, TUN/TAP은 파일 디스크립터(File Descriptor)로, 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;
}
veth 내부 아키텍처
veth(Virtual Ethernet)는 항상 쌍(pair)으로 생성되며, 한쪽에서 전송한 패킷이 상대편의 RX 경로로 직접 전달됩니다. 컨테이너 네트워킹에서 가장 널리 사용되는 가상 인터페이스이며, XDP 지원을 통해 고성능 패킷 처리도 가능합니다.
/* drivers/net/veth.c - veth_xmit() 핵심 경로 분석 */
static netdev_tx_t veth_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
struct veth_rq *rq = NULL;
struct net_device *rcv;
int length = skb->len;
bool use_napi = false;
int rxq;
/* RCU로 peer 장치 참조 — peer가 해제 중일 수 있음 */
rcu_read_lock();
rcv = rcu_dereference(priv->peer);
if (unlikely(!rcv) || !pskb_may_pull(skb, ETH_HLEN)) {
goto drop;
}
rcv_priv = netdev_priv(rcv);
rxq = skb_get_queue_mapping(skb);
/* NAPI 모드: ptr_ring에 enqueue 후 NAPI 스케줄 */
if (rcv_priv->_xdp_prog || veth_is_napi_enabled(rcv_priv)) {
rq = &rcv_priv->rq[rxq];
use_napi = true;
/* skb를 peer의 ptr_ring에 삽입 */
if (unlikely(ptr_ring_produce(&rq->xdp_ring, skb))) {
goto drop;
}
} else {
/* 비-NAPI 모드: 직접 netif_rx()로 전달 */
skb->protocol = eth_type_trans(skb, rcv);
if (likely(netif_rx(skb) == NET_RX_SUCCESS)) {
/* 통계 업데이트 */
}
}
if (use_napi)
veth_napi_schedule(rq, skb);
rcu_read_unlock();
return NETDEV_TX_OK;
drop:
atomic64_inc(&priv->dropped);
rcu_read_unlock();
kfree_skb(skb);
return NETDEV_TX_OK;
}
veth의 XDP 지원은 peer 측에서 이루어집니다. veth_xdp_rcv()가 NAPI poll 컨텍스트에서 호출되어 ptr_ring의 패킷을 꺼내고, XDP 프로그램을 실행합니다. XDP_TX verdict는 패킷을 다시 원래 방향으로 돌려보내고, XDP_REDIRECT는 다른 인터페이스로 전달합니다.
virtio-net 드라이버 심층 분석
virtio-net은 가상 머신(Virtual Machine) 환경에서 가장 널리 사용되는 준가상화(Paravirtualization) 네트워크 드라이버입니다. virtqueue를 통해 호스트와 게스트 사이에서 제로 카피(Zero-copy)에 가까운 효율적인 패킷 전달을 구현합니다.
virtqueue의 핵심 구조는 세 부분으로 구성됩니다.
- 디스크립터 테이블(Descriptor Table): 각 디스크립터가 게스트 메모리의 버퍼 주소, 길이, 플래그를 보유합니다. 체인 연결(chaining)으로 scatter-gather를 지원합니다.
- 가용 링(Available Ring): 게스트가 호스트에 전달할 디스크립터 인덱스를 게시하는 링입니다. 게스트 → 호스트 방향의 통신 채널입니다.
- 사용 완료 링(Used Ring): 호스트가 처리를 완료한 디스크립터 인덱스를 돌려주는 링입니다. 호스트 → 게스트 방향의 통신 채널입니다.
/* drivers/net/virtio_net.c - RX 경로 (mergeable buffers 모드) */
static struct sk_buff *receive_mergeable(
struct net_device *dev,
struct virtnet_info *vi,
struct receive_queue *rq,
void *buf, unsigned int len,
void *ctx)
{
struct virtio_net_hdr_mrg_rxbuf *hdr = buf;
u16 num_buf = virtio16_to_cpu(vi->vdev, hdr->num_buffers);
struct page *page = virt_to_head_page(buf);
struct sk_buff *head_skb, *curr_skb;
unsigned int truesize;
/* 첫 번째 버퍼로 skb 헤드 구성 */
head_skb = page_to_skb(vi, rq, page, 0, len,
hdr->hdr.hdr_len, truesize);
if (unlikely(!head_skb))
goto err_skb;
curr_skb = head_skb;
/* mergeable 모드: 여러 버퍼를 하나의 skb로 병합 */
while (--num_buf) {
buf = virtqueue_get_buf_ctx(rq->vq, &len, &ctx);
if (unlikely(!buf))
goto err_buf;
page = virt_to_head_page(buf);
/* 추가 버퍼를 skb fragment로 연결 */
if (curr_skb != head_skb) {
skb_add_rx_frag(curr_skb, skb_shinfo(curr_skb)->nr_frags,
page, offset, len, truesize);
}
}
return head_skb;
}
Mergeable buffers 모드는 가변 크기 패킷을 효율적으로 처리합니다. 호스트가 패킷 크기에 따라 하나 또는 여러 개의 버퍼를 사용하고, num_buffers 필드로 게스트에게 버퍼 수를 알려줍니다. 이 모드에서는 메모리 낭비를 줄이면서도 점보 프레임(Jumbo Frame)을 지원할 수 있습니다.
Control virtqueue는 데이터 전송과 별개로 기능 협상(Feature Negotiation)에 사용됩니다. MAC 주소 변경, 멀티큐 설정, VLAN 필터링 등의 제어 명령이 이 채널을 통해 전달됩니다.
가상 netdev 성능 최적화
컨테이너 네트워킹 성능은 가상 netdev의 최적화 수준에 직접적으로 의존합니다. 최신 커널에서는 XDP 벌크 포워딩(Bulk Forwarding), Big TCP, TSO/GRO 등의 기능을 통해 가상 인터페이스의 성능을 물리 NIC에 근접하게 끌어올릴 수 있습니다.
/* XDP redirect를 통한 veth pair 간 고속 전달 예시 */
/* BPF 프로그램: veth0에서 veth1으로 XDP_REDIRECT */
SEC("xdp")
int xdp_redirect_veth(struct xdp_md *ctx)
{
/* ifindex는 peer veth의 인터페이스 인덱스 */
return bpf_redirect(PEER_IFINDEX, 0);
}
/* 커널 내부: veth XDP bulk 전달 */
/* drivers/net/veth.c - veth_xdp_xmit() */
static int veth_xdp_xmit(struct net_device *dev,
int n,
struct xdp_frame **frames,
u32 flags)
{
struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
int i, drops = 0;
rcu_read_lock();
rcv = rcu_dereference(priv->peer);
/* 벌크 전달: n개의 XDP 프레임을 한 번에 enqueue */
for (i = 0; i < n; i++) {
struct xdp_frame *frame = frames[i];
if (unlikely(ptr_ring_produce(&rq->xdp_ring, frame)))
drops++;
}
if (flags & XDP_XMIT_FLUSH)
veth_napi_schedule(rq, NULL);
rcu_read_unlock();
return n - drops;
}
| 가상 인터페이스 | 전달 방식 | XDP 지원 | 네임스페이스 | 주요 용도 | 상대 성능 |
|---|---|---|---|---|---|
| veth | peer netdev 직접 전달 | O (native) | 양쪽 독립 | 컨테이너 네트워킹 | 높음 |
| macvlan | MAC 기반 분기 | O (한정적) | 자식이 독립 | VM 브릿징, VEPA | 매우 높음 |
| ipvlan | IP 기반 분기 | X | 자식이 독립 | 다수 컨테이너 (MAC 절약) | 높음 |
| bridge | L2 FDB 기반 포워딩 | O (한정적) | 포트별 독립 | VM 호스트 브릿지 | 중간 |
| TUN/TAP | 파일 디스크립터 | X | 생성 NS | VPN, QEMU | 낮음 (컨텍스트 스위치) |
| virtio-net | virtqueue (공유 메모리) | O | 게스트 내부 | 가상 머신 | 높음 (vhost 시) |
컨테이너 네트워킹 성능 최적화 팁:
- veth + XDP:
bpf_redirect_peer()를 사용하면 peer 쪽에서 추가 netif_rx() 오버헤드 없이 직접 XDP 경로로 진입합니다. - GRO/TSO 활성화: veth에서
ethtool -K veth0 gro on tso on으로 세그멘테이션 오프로드를 활용합니다. - Big TCP: 커널 v5.19+에서
ip link set dev veth0 gso_max_size 524280으로 Big TCP를 활성화하여 GRO/TSO 효율을 극대화합니다. - NAPI 모드 활성화: veth의
peer_notif_delay설정으로 NAPI 기반 수신을 활성화하면 softirq batching의 이점을 얻습니다. - CPU affinity: 동일 NUMA 노드에 veth pair의 양쪽 트래픽을 바인딩하여 캐시 히트율을 높입니다.
네트워크 네임스페이스와 가상 netdev
네트워크 네임스페이스(Network Namespace)는 가상 인터페이스의 격리(Isolation) 단위입니다. 각 네임스페이스는 독립적인 라우팅 테이블, 방화벽 규칙, 인터페이스 목록을 보유합니다. 가상 netdev 유형에 따라 네임스페이스 경계를 넘는 방식이 다릅니다.
# 네임스페이스 간 veth pair 생성 및 설정
ip netns add container1
ip link add veth-host type veth peer name veth-ct1
ip link set veth-ct1 netns container1
# 각 네임스페이스에서 주소 할당
ip addr add 10.0.0.1/24 dev veth-host
ip link set veth-host up
ip netns exec container1 ip addr add 10.0.0.2/24 dev veth-ct1
ip netns exec container1 ip link set veth-ct1 up
# macvlan: 물리 인터페이스 위에 가상 MAC 생성
ip link add macvlan0 link eth0 type macvlan mode bridge
ip link set macvlan0 netns container1
# ipvlan: MAC 공유, IP 기반 분기 (L3 모드)
ip link add ipvlan0 link eth0 type ipvlan mode l3
ip link set ipvlan0 netns container1
| 가상 장치 | 네임스페이스 이동 | 부모 장치 필요 | 격리 수준 | 패킷 경계 넘기 방식 |
|---|---|---|---|---|
| veth | 양쪽 독립 이동 가능 | 없음 (peer pair) | 완전 격리 | peer 직접 전달 |
| macvlan | 자식만 이동 가능 | 물리/가상 인터페이스 | L2 격리 | 부모 인터페이스 경유 |
| ipvlan | 자식만 이동 가능 | 물리/가상 인터페이스 | L3 격리 (MAC 공유) | IP 기반 라우팅 |
| bridge port | 포트 장치 이동 가능 | bridge 장치 | L2 포워딩 | FDB lookup |
| TUN/TAP | 생성 NS에 고정 | 없음 | fd 기반 | 유저스페이스 read/write |
동기화, 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 단위로 세분화되었습니다. 기존에는 전체 네트워크 네임스페이스(Namespace)가 단일 글로벌 RTNL 뮤텍스(Mutex)를 공유하여, 컨테이너(Container) 수천 개를 운영하는 환경에서 제어 경로 병목이 발생했습니다. per-namespace RTNL 락을 통해 서로 다른 네임스페이스의 링크 설정 작업이 병렬로 수행되어 경합이 크게 감소합니다.
RTNL 락 획득 패턴과 데드락(Deadlock) 방지
RTNL(Route Netlink) 뮤텍스는 네트워크 서브시스템의 최상위 락으로, 장치 등록/해제, 주소 설정, 링크 상태 변경 등 모든 제어 경로를 직렬화합니다. 올바른 RTNL 사용 패턴을 이해하지 못하면 데드락이 쉽게 발생합니다.
/* net/core/rtnetlink.c - RTNL 락 API */
void rtnl_lock(void)
{
mutex_lock(&rtnl_mutex);
}
int rtnl_trylock(void)
{
return mutex_trylock(&rtnl_mutex);
}
void rtnl_unlock(void)
{
/* 지연된 netlink 알림을 unlock 시점에 배치 전송 */
netdev_run_todo();
mutex_unlock(&rtnl_mutex);
}
/* RTNL 보유 검증 매크로 — 디버깅에 필수 */
#define ASSERT_RTNL() \
WARN_ON_ONCE(!rtnl_is_locked())
ASSERT_RTNL()은 RTNL 보유를 런타임에 검증하며, netdev 코드에서 광범위하게 사용됩니다. 이 매크로가 경고를 출력하면 제어 경로에서 RTNL 없이 보호되지 않은 접근이 일어난 것이므로 반드시 수정해야 합니다.
RTNL 관련 데드락은 주로 다음 시나리오에서 발생합니다.
- RTNL + NAPI: RTNL을 보유한 상태에서
napi_disable()을 호출하면, NAPI poll 안에서 RTNL을 필요로 하는 netlink 알림이 대기할 경우 데드락이 발생합니다. - RTNL + reset work:
ndo_stop()(RTNL 보유)에서cancel_work_sync(&reset_work)를 호출하는데, reset work 내부에서도rtnl_lock()을 시도하면 데드락입니다. - RTNL + ethtool: ethtool 콜백 중 일부는 RTNL을 이미 보유한 상태에서 호출되므로, 콜백 내에서 다시
rtnl_lock()을 호출하면 안 됩니다.
/* 올바른 패턴: worker 컨텍스트에서 안전한 RTNL 획득 */
static void my_reset_work_handler(struct work_struct *work)
{
struct my_priv *priv = container_of(work, struct my_priv, reset_work);
struct net_device *ndev = priv->netdev;
/* rtnl_lock을 먼저 획득 — 락 순서: RTNL → device lock */
rtnl_lock();
/* 장치가 이미 해제 중인지 확인 */
if (!netif_running(ndev)) {
rtnl_unlock();
return;
}
/* 안전하게 장치 리셋 수행 */
my_close_locked(ndev); /* RTNL 보유 상태에서 호출 */
my_open_locked(ndev); /* RTNL 보유 상태에서 호출 */
rtnl_unlock();
}
/* 위험한 패턴 — ndo_stop에서 reset_work와 데드락 가능 */
static int my_ndo_stop_BAD(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
/* !! RTNL 이미 보유 중 (ndo_stop 호출자가 획득) */
/* !! reset_work가 rtnl_lock()을 시도하면 데드락 !! */
cancel_work_sync(&priv->reset_work); /* 위험! */
return 0;
}
/* 안전한 패턴 — 플래그 기반 취소 */
static int my_ndo_stop_SAFE(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
/* 플래그로 reset work에게 중단 신호 전달 */
set_bit(MY_STATE_CLOSING, &priv->state);
/* RTNL을 잠시 놓고 work 완료 대기 */
rtnl_unlock();
cancel_work_sync(&priv->reset_work);
rtnl_lock();
/* 재진입 후 상태 재확인 필수 */
if (!netif_running(ndev))
return 0;
my_hw_shutdown(priv);
return 0;
}
lockdep이 활성화된 커널에서 이 순서 위반을 자동으로 감지합니다.
per-CPU 통계의 u64_stats_sync 메커니즘
u64_stats_sync는 32비트 시스템에서 64비트 통계 카운터를 원자적(Atomic)으로 읽기 위한 경량 동기화 메커니즘입니다. 64비트 시스템에서는 단일 명령어로 64비트 값을 읽을 수 있지만, 32비트 시스템에서는 상위/하위 32비트를 별도로 읽어야 하므로 중간에 쓰기가 끼어들면 잘못된 값(torn read)을 읽을 수 있습니다.
/* include/linux/u64_stats_sync.h - 핵심 구조와 API */
struct u64_stats_sync {
#if BITS_PER_LONG == 32
seqcount_t seq; /* 32비트에서만 시퀀스 카운터 사용 */
#endif
};
/* 쓰기 측: 통계 업데이트 보호 */
static inline void u64_stats_update_begin(struct u64_stats_sync *syncp)
{
#if BITS_PER_LONG == 32
write_seqcount_begin(&syncp->seq);
#endif
}
static inline void u64_stats_update_end(struct u64_stats_sync *syncp)
{
#if BITS_PER_LONG == 32
write_seqcount_end(&syncp->seq);
#endif
}
/* 읽기 측: 일관된 64비트 값 읽기 */
static inline unsigned int
u64_stats_fetch_begin(const struct u64_stats_sync *syncp)
{
#if BITS_PER_LONG == 32
return read_seqcount_begin(&syncp->seq);
#else
return 0; /* 64비트에서는 no-op */
#endif
}
static inline bool
u64_stats_fetch_retry(const struct u64_stats_sync *syncp,
unsigned int start)
{
#if BITS_PER_LONG == 32
return read_seqcount_retry(&syncp->seq, start);
#else
return false; /* 64비트에서는 항상 성공 */
#endif
}
u64_stats_sync와 seqlock의 차이점을 이해하는 것이 중요합니다. seqlock은 읽기-쓰기 모두에 대한 범용 동기화인 반면, u64_stats_sync는 쓰기 측이 선점 금지(preempt_disable) 컨텍스트에서만 동작한다고 가정합니다. NAPI poll이나 softirq 컨텍스트에서 통계를 업데이트하므로 이 가정이 자연스럽게 충족됩니다.
/* 완전한 per-CPU 네트워크 통계 구현 예시 */
struct my_pcpu_stats {
struct u64_stats_sync syncp; /* 동기화 프리미티브 */
u64 rx_packets;
u64 rx_bytes;
u64 tx_packets;
u64 tx_bytes;
u64 rx_errors;
u64 tx_dropped;
};
/* 데이터 경로 (NAPI poll 컨텍스트): 통계 업데이트 */
static void my_rx_update_stats(struct my_priv *priv,
unsigned int len)
{
struct my_pcpu_stats *stats = this_cpu_ptr(priv->pcpu_stats);
u64_stats_update_begin(&stats->syncp);
stats->rx_packets++;
stats->rx_bytes += len;
u64_stats_update_end(&stats->syncp);
}
/* ndo_get_stats64: 모든 CPU의 통계를 안전하게 합산 */
static void my_get_stats64(struct net_device *ndev,
struct rtnl_link_stats64 *s)
{
struct my_priv *priv = netdev_priv(ndev);
int cpu;
for_each_possible_cpu(cpu) {
struct my_pcpu_stats *stats;
u64 rx_packets, rx_bytes, tx_packets, tx_bytes;
unsigned int start;
stats = per_cpu_ptr(priv->pcpu_stats, cpu);
do {
start = u64_stats_fetch_begin(&stats->syncp);
rx_packets = stats->rx_packets;
rx_bytes = stats->rx_bytes;
tx_packets = stats->tx_packets;
tx_bytes = stats->tx_bytes;
} while (u64_stats_fetch_retry(&stats->syncp, start));
s->rx_packets += rx_packets;
s->rx_bytes += rx_bytes;
s->tx_packets += tx_packets;
s->tx_bytes += tx_bytes;
}
}
RCU를 활용한 XDP 프로그램 교체
XDP 프로그램은 net_device에 RCU 포인터(RCU Pointer)로 저장됩니다. 이를 통해 데이터 경로에서 락 없이 XDP 프로그램을 참조하면서, 제어 경로에서는 안전하게 프로그램을 교체할 수 있습니다.
/* net/core/dev.c - dev_xdp_install() RCU 기반 XDP 프로그램 교체 */
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 bpf_prog *old_prog;
struct netdev_bpf xdp;
int err;
/* RTNL 보유 상태에서 호출됨 → 제어 경로 직렬화 보장 */
ASSERT_RTNL();
memset(&xdp, 0, sizeof(xdp));
xdp.command = XDP_SETUP_PROG;
xdp.prog = prog;
xdp.extack = extack;
xdp.flags = flags;
/* 드라이버 콜백을 통해 HW/SW XDP 프로그램 설치 */
err = bpf_op(dev, &xdp);
if (err)
return err;
/* 핵심: RCU를 통해 새 프로그램 포인터 발행 */
old_prog = rcu_replace_pointer(
dev->xdp_prog, prog,
lockdep_is_held(&rtnl_mutex));
if (old_prog) {
/* grace period 이후에 이전 프로그램 해제 */
bpf_prog_put(old_prog);
}
return 0;
}
/* 데이터 경로: RCU read-side에서 XDP 프로그램 참조 */
static u32 my_run_xdp(struct my_rx_ring *ring,
struct xdp_buff *xdp)
{
struct bpf_prog *prog;
u32 act = XDP_PASS;
/* RCU read-side: 락 없이 프로그램 포인터 읽기 */
prog = rcu_dereference(ring->netdev->xdp_prog);
if (!prog)
return act;
/* XDP 프로그램 실행 — NAPI 컨텍스트 = RCU read-side */
act = bpf_prog_run_xdp(prog, xdp);
switch (act) {
case XDP_PASS:
case XDP_TX:
case XDP_REDIRECT:
break;
default:
bpf_warn_invalid_xdp_action(ring->netdev, prog, act);
/* fallthrough */
case XDP_ABORTED:
case XDP_DROP:
act = XDP_DROP;
break;
}
return act;
}
RCU 기반 교체의 핵심은 rcu_replace_pointer()가 새 포인터를 발행(publish)한 후, 기존 데이터 경로에서 이전 프로그램을 사용 중인 모든 CPU가 RCU grace period를 통과한 뒤에야 이전 프로그램이 해제되는 점입니다. 이로써 데이터 경로는 락 없이도 항상 유효한 프로그램 포인터를 참조할 수 있습니다.
데이터 경로 동시성 모델
네트워크 드라이버의 데이터 경로는 TX(송신)와 RX(수신)에서 서로 다른 동시성 모델을 사용합니다. 이 차이를 이해하는 것이 드라이버 개발의 핵심입니다.
TX 경로: qdisc 락이 큐별로 직렬화를 제공합니다. 멀티큐 드라이버에서 각 TX 큐는 독립적인 qdisc 락을 가지므로, 서로 다른 큐에 대한 ndo_start_xmit() 호출은 병렬로 실행됩니다. 동일 큐에 대해서는 qdisc 락이 직렬화를 보장합니다.
RX 경로: NAPI poll은 동일 NAPI 인스턴스에 대해 직렬화가 보장됩니다. NAPI_STATE_SCHED 비트가 설정된 동안에는 해당 NAPI의 poll 함수가 다른 CPU에서 동시에 호출되지 않습니다. 단, 서로 다른 NAPI 인스턴스(멀티큐)는 병렬로 실행됩니다.
완료 인터럽트(Completion Interrupt)와 NAPI 컨텍스트의 상호작용: TX 완료 처리가 RX NAPI poll 안에서 이루어지는 구조(shared NAPI)에서는 TX 완료와 RX 처리가 동일 NAPI 컨텍스트에서 직렬화됩니다. 반면 별도의 TX 완료 인터럽트를 사용하는 경우, TX 완료 경로와 TX 전송 경로 사이의 동기화를 드라이버가 직접 관리해야 합니다.
| 락 유형 | 보호 대상 | 컨텍스트 | 범위 | 오버헤드 |
|---|---|---|---|---|
rtnl_mutex | 장치 설정, 등록/해제 | 프로세스 | 전역 (per-ns v6.13+) | 높음 (sleep 가능) |
netif_tx_lock | TX 큐별 전송 | BH | per-queue | 중간 |
NAPI_STATE_SCHED | NAPI poll 직렬화 | softirq/kthread | per-NAPI | 낮음 (비트 연산) |
| RCU | XDP prog, rx_handler | 모든 컨텍스트 | 읽기 경로 전역 | 매우 낮음 |
u64_stats_sync | 64비트 통계 카운터 | NAPI/BH (쓰기) | per-CPU | 0 (64비트) / 낮음 (32비트) |
dma_wmb() | DMA descriptor 순서 | 모든 컨텍스트 | CPU-장치 간 | 아키텍처 의존 |
메모리 순서와 DMA 배리어(Barrier)
네트워크 드라이버에서 DMA 배리어는 CPU가 작성한 디스크립터(Descriptor)가 장치에 올바른 순서로 보이도록 보장합니다. 배리어 유형을 잘못 선택하면 패킷 손실이나 DMA 오류가 발생합니다.
| 배리어 | 용도 | x86 | ARM64 | 사용 시점 |
|---|---|---|---|---|
dma_wmb() | DMA 쓰기 순서 보장 | 컴파일러 배리어 | dmb(oshst) | 디스크립터 필드 쓰기 후, ownership 비트 설정 전 |
dma_rmb() | DMA 읽기 순서 보장 | 컴파일러 배리어 | dmb(oshld) | ownership 비트 확인 후, 데이터 읽기 전 |
wmb() | 모든 쓰기 순서 보장 | sfence | dsb(st) | MMIO doorbell 쓰기 전 |
smp_wmb() | SMP 쓰기 순서 보장 | 컴파일러 배리어 | dmb(ishst) | CPU 간 공유 데이터 순서 보장 |
readl()/writel() | MMIO 접근 | 암묵적 순서 보장 | 암묵적 순서 보장 | 레지스터 doorbell, CSR 접근 |
/* 올바른 TX 디스크립터 쓰기 순서 예시 */
static netdev_tx_t my_start_xmit(struct sk_buff *skb,
struct net_device *ndev)
{
struct my_tx_desc *desc = my_get_next_tx_desc(ring);
/* 1단계: 디스크립터 필드 채우기 */
desc->buf_addr = dma_map_single(dev, skb->data,
skb->len, DMA_TO_DEVICE);
desc->len = cpu_to_le16(skb->len);
desc->vlan_tag = cpu_to_le16(skb_vlan_tag_get(skb));
/* 2단계: dma_wmb() — 위의 필드가 장치에 먼저 보이도록 보장 */
dma_wmb();
/* 3단계: ownership 비트 설정 — 장치가 이 디스크립터를 처리 시작 */
desc->cmd_type_len = cpu_to_le32(MY_TXD_CMD_EOP | MY_TXD_CMD_RS |
skb->len);
/* 4단계: wmb() — MMIO 쓰기 전에 모든 메모리 쓰기 완료 보장 */
wmb();
/* 5단계: doorbell — 장치에 새 디스크립터 알림 */
writel(ring->next_to_use, ring->tail_reg);
return NETDEV_TX_OK;
}
/* 올바른 RX 디스크립터 읽기 순서 예시 */
static int my_clean_rx(struct my_rx_ring *ring, int budget)
{
while (work_done < budget) {
struct my_rx_desc *desc = &ring->desc[ring->next_to_clean];
/* 1단계: ownership/status 비트 확인 */
u32 status = le32_to_cpu(desc->status);
if (!(status & MY_RXD_STAT_DD))
break; /* 장치가 아직 소유 중 */
/* 2단계: dma_rmb() — status 읽기 후, 나머지 필드 읽기 전 */
dma_rmb();
/* 3단계: 이제 길이, 체크섬 등 안전하게 읽기 가능 */
len = le16_to_cpu(desc->length);
vlan = le16_to_cpu(desc->vlan_tag);
my_process_rx_packet(ring, desc, len);
work_done++;
}
return work_done;
}
dma_wmb()와 smp_wmb()가 단순 컴파일러 배리어로 충분합니다. 반면 ARM64는 약한 메모리 순서 모델이므로 실제 하드웨어 배리어 명령어(dmb)가 필요합니다. 드라이버 코드에서는 항상 커널 배리어 API를 사용하여 아키텍처별 차이를 추상화해야 합니다.
디버깅 체크리스트와 실전 트러블슈팅
| 증상 | 의심 지점 | 확인 방법 |
|---|---|---|
| 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
TX Hang 심층 진단 플로우차트
TX 타임아웃(TX Timeout)은 네트워크 드라이버에서 가장 치명적인 장애 유형 중 하나입니다. TX 완료 인터럽트(Completion Interrupt)가 오지 않거나, 도어벨(Doorbell) 쓰기가 실패하거나, BQL(Byte Queue Limits)이 잘못 설정된 경우 등 다양한 원인이 있습니다. 체계적인 진단 플로우차트를 따라 원인을 빠르게 격리해야 합니다.
각 진단 단계에서 확인해야 할 핵심 사항은 다음과 같습니다.
- ethtool -S 카운터:
tx_timeout,tx_busy,tx_dropped값을 확인합니다. 특정 큐에만 집중되는지 전체 큐에 분산되는지가 원인 격리의 첫 단서입니다. - BQL 상태:
/sys/class/net/eth0/queues/tx-N/byte_queue_limits/아래limit,inflight값을 비교합니다.inflight가limit에 도달한 상태로 해제되지 않으면 completion 경로 문제입니다. - Completion IRQ:
/proc/interrupts에서 해당 큐의 인터럽트 카운터가 증가하는지 확인합니다. MSI-X 벡터와 큐 매핑이 일치하는지도 점검합니다. - Doorbell: MMIO 영역에 대한 쓰기가 PCIe 레벨에서 실패할 수 있습니다.
dmesg에서 AER(Advanced Error Reporting) 메시지를 확인합니다. - 펌웨어 상태:
devlink health show로 firmware reporter 상태를 확인하고, 필요 시devlink health dump을 수집합니다.
# bpftrace를 사용한 TX timeout 경로 추적
# ndo_tx_timeout 호출 추적 - 어떤 큐에서 발생하는지 확인
bpftrace -e 'kprobe:dev_watchdog {
printf("TX watchdog fired on CPU %d\n", cpu);
}'
# TX completion latency 히스토그램 (마이크로초 단위)
bpftrace -e 'kprobe:napi_complete_done {
@start[tid] = nsecs;
}
kretprobe:napi_complete_done /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# BQL inflight 모니터링
for q in /sys/class/net/eth0/queues/tx-*/byte_queue_limits; do
echo "$(basename $(dirname $q)): limit=$(cat $q/limit) inflight=$(cat $q/inflight)"
done
RX Drop 원인 분석
RX 경로에서의 패킷 손실은 여러 계층에서 발생할 수 있습니다. 하드웨어 링 버퍼(Ring Buffer) 오버플로우, NAPI 예산(Budget) 소진, softirq 처리 시간 초과, 소켓 버퍼(Socket Buffer) 가득 참 등 원인별로 관측 포인트가 다릅니다.
/proc/net/softnet_stat의 각 컬럼은 CPU별 softnet 처리 통계를 나타냅니다.
| 컬럼 | 필드명 | 의미 | 급증 시 원인 |
|---|---|---|---|
| 1번째 | processed | 처리된 총 프레임 수 | 정상 지표 (증가는 문제 아님) |
| 2번째 | dropped | netif_rx backlog 초과로 드롭 | input_pkt_queue 크기 부족, CPU 과부하 |
| 3번째 | time_squeeze | softirq 시간 제한(2ms)으로 처리 중단 | 패킷 폭주 또는 NAPI poll 비효율 |
| 9번째 | cpu_collision | TX 경로에서 CPU 경합 발생 | 여러 CPU가 동일 TX 큐에 접근 |
| 10번째 | received_rps | RPS로 전달된 프레임 | 정상 지표 (RPS 활성 시) |
| 11번째 | flow_limit_count | flow limit으로 드롭된 프레임 | 단일 플로우 과점유 |
| 12번째 | softnet_backlog_len | 현재 backlog 대기 길이 | 높으면 처리 지연 중 |
time_squeeze와 budget 소진의 차이를 이해하는 것이 중요합니다. time_squeeze는 2ms의 softirq 시간 제한에 의해 처리가 중단된 횟수이며, 이는 NAPI poll 루프에서 budget(기본 300)을 다 쓰기 전에 시간이 먼저 초과된 경우입니다. budget 소진은 300개 패킷을 모두 처리했지만 아직 더 처리할 패킷이 남은 상태를 의미합니다.
# 종합 RX drop 분석 스크립트
#!/bin/bash
DEV="eth0"
echo "=== RX Drop 분석: $DEV ==="
# 1. 하드웨어 레벨 drop 확인
echo "\n--- 하드웨어 카운터 ---"
ethtool -S $DEV | grep -iE "rx.*drop|rx.*miss|rx.*error|rx.*discard|no.buffer|no_buffer"
# 2. 커널 레벨 drop 확인
echo "\n--- 커널 네트워크 카운터 ---"
cat /proc/net/dev | grep $DEV | awk '{printf "RX packets:%s errors:%s drop:%s fifo:%s\n", $2,$4,$5,$6}'
# 3. softnet_stat 분석 (CPU별)
echo "\n--- softnet_stat (CPU별) ---"
echo "CPU processed dropped time_squeeze"
cpu=0
while IFS= read -r line; do
processed=$((16#$(echo $line | awk '{print $1}')))
dropped=$((16#$(echo $line | awk '{print $2}')))
squeeze=$((16#$(echo $line | awk '{print $3}')))
printf "CPU%-3d %10d %8d %13d\n" $cpu $processed $dropped $squeeze
cpu=$((cpu+1))
done < /proc/net/softnet_stat
# 4. 링 버퍼 사용률 확인
echo "\n--- 링 버퍼 설정 ---"
ethtool -g $DEV
# 5. SKB drop reason 추적 (perf 기반)
echo "\n--- SKB Drop Reason 추적 (5초) ---"
perf record -e skb:kfree_skb -a -- sleep 5
perf script | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
# 6. drop_monitor 사용
echo "\n--- drop_monitor (5초 샘플) ---"
dropwatch -l kas <<CMDS
start
sleep 5
stop
exit
CMDS
링크 Flap 디버깅
링크 플랩(Link Flap)은 네트워크 인터페이스가 반복적으로 up/down을 오가는 현상입니다. 물리 계층(PHY), 광모듈(SFP/QSFP), 자동 협상(Autonegotiation), 케이블 문제 등 다양한 원인이 있으며, phylink/PHY 상태 머신(State Machine)의 tracepoint를 활용하면 원인을 정밀하게 추적할 수 있습니다.
주요 원인 패턴과 진단 방법은 다음과 같습니다.
- PHY 상태 전이(State Transition) 이상: phylink은 내부적으로 상태 머신을 운영합니다.
AN_RESTART -> AN_COMPLETE -> LINK_OK순서로 진행되어야 하며,AN_RESTART가 반복되면 협상 실패입니다. - SFP 모듈 문제: 호환되지 않는 SFP 모듈, 광 출력(TX power) 저하, 수신 감도(RX sensitivity) 미달이 링크 플랩의 흔한 원인입니다.
- 신호 무결성(Signal Integrity): 케이블 길이 초과, 커넥터 접촉 불량, EMI 간섭으로 BER(Bit Error Rate)이 올라가면 PHY가 링크를 재설정합니다.
- 원격 장비 이상: 스위치 포트의 STP(Spanning Tree Protocol) 재계산, LACP 타임아웃, 원격 PHY 리셋 등이 원인일 수 있습니다.
#!/bin/bash
# 링크 플랩 모니터링 및 진단 스크립트
DEV="eth0"
LOG="/tmp/link-flap-$DEV.log"
INTERVAL=1
FLAP_COUNT=0
PREV_STATE=""
echo "=== 링크 플랩 모니터 시작: $DEV ==="
echo "로그: $LOG"
# phylink tracepoint 활성화
echo 1 > /sys/kernel/debug/tracing/events/phylink/enable 2>/dev/null
# SFP 모듈 정보 수집
echo "--- SFP 모듈 정보 ---" | tee $LOG
ethtool -m $DEV 2>/dev/null | tee -a $LOG
# 현재 PHY 상태 확인
echo "--- PHY 상태 ---" | tee -a $LOG
ethtool $DEV | grep -E "Speed|Duplex|Link|Auto" | tee -a $LOG
# 링크 상태 폴링 루프
while true; do
STATE=$(cat /sys/class/net/$DEV/operstate)
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S.%N")
if [ "$STATE" != "$PREV_STATE" ] && [ -n "$PREV_STATE" ]; then
FLAP_COUNT=$((FLAP_COUNT+1))
echo "[$TIMESTAMP] FLAP #$FLAP_COUNT: $PREV_STATE -> $STATE" | tee -a $LOG
# 플랩 시점에 추가 정보 수집
echo " ethtool:" | tee -a $LOG
ethtool $DEV 2>/dev/null | grep -E "Speed|Duplex|Link" | tee -a $LOG
# dmesg에서 관련 메시지 추출
dmesg --time-format iso | tail -5 | grep -iE "$DEV|link|phy|sfp" | tee -a $LOG
fi
PREV_STATE=$STATE
sleep $INTERVAL
done
dmesg에서 자주 보이는 링크 관련 패턴과 의미는 다음과 같습니다.
| dmesg 패턴 | 의미 | 조치 |
|---|---|---|
Link is Down / Link is Up 반복 | 물리 계층 불안정 | 케이블/SFP 교체, 속도 고정 시도 |
PHY: autonegotiation failed | 자동 협상 실패 | 양쪽 속도/듀플렉스 고정 |
sfp: module ... not supported | 미지원 SFP 모듈 | 호환 모듈로 교체 |
PCIe: AER correctable error | PCIe 링크 오류 | 슬롯 재장착, BIOS 설정 확인 |
firmware timeout | NIC 펌웨어 응답 없음 | 펌웨어 업데이트, devlink health |
성능 병목 분석 방법론
네트워크 드라이버의 성능 병목은 CPU 핫스팟(Hotspot), 락 경합(Lock Contention), 캐시 미스(Cache Miss), DMA 매핑 오버헤드 등 다양한 원인에서 발생합니다. 체계적인 분석 파이프라인을 구축하면 병목 지점을 정량적으로 식별할 수 있습니다.
- perf top: 실시간으로 CPU를 가장 많이 소비하는 함수를 확인합니다. 네트워크 부하 중
mlx5e_napi_poll,napi_gro_receive,__netif_receive_skb_core등이 상위에 나타나는 것이 정상이며, 특정 락 함수가 상위에 나타나면 경합 문제입니다. - ftrace function_graph: 특정 함수의 호출 트리와 소요 시간을 추적합니다. 패킷 처리 경로에서 예상 외로 긴 함수를 식별하는 데 유용합니다.
- bpftrace 히스토그램: 패킷당 처리 시간, NAPI poll 주기, 인터럽트 간격 등을 분포로 시각화합니다.
- 플레임 그래프(Flame Graph): 전체 콜 스택을 한눈에 파악할 수 있는 시각화 도구입니다. CPU 프로파일링 결과를 플레임 그래프로 변환하면 병목 위치가 직관적으로 드러납니다.
# 종합 네트워크 성능 분석 파이프라인
# 1. perf top으로 실시간 CPU 핫스팟 확인
perf top -g --no-children -p $(pgrep -d, ksoftirqd)
# 2. ftrace function_graph로 NAPI poll 레이턴시 추적
echo napi_poll > /sys/kernel/debug/tracing/set_graph_function
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 3
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -100
# 3. bpftrace: NAPI poll당 처리 패킷 수 히스토그램
bpftrace -e 'kretprobe:napi_poll {
@pkts = hist(retval);
}'
# 4. bpftrace: 패킷당 처리 지연 (나노초)
bpftrace -e 'kprobe:netif_receive_skb {
@start[arg0] = nsecs;
}
kprobe:__netif_receive_skb_core /@start[arg0]/ {
@latency_ns = hist(nsecs - @start[arg0]);
delete(@start[arg0]);
}'
# 5. 플레임 그래프 생성 파이프라인
# perf로 10초간 콜 스택 수집
perf record -a -g -F 99 -- sleep 10
# 플레임 그래프 변환 (Brendan Gregg의 FlameGraph 도구)
perf script | stackcollapse-perf.pl | flamegraph.pl \
--title "Network Stack CPU Flame Graph" \
--subtitle "$(hostname) - $(date)" \
--width 1200 > net-flame.svg
# 6. 락 경합 분석
perf lock record -a -- sleep 5
perf lock report --sort contended,avg_wait
운영 관측: 필수 대시보드 지표
드라이버 품질은 장애가 났을 때 “원인을 빠르게 찾을 수 있는가”로 평가됩니다. 아래 지표를 대시보드로 상시 수집하면 회귀를 조기에 감지할 수 있습니다.
| 지표 그룹 | 필수 항목 | 경보 조건 예시 |
|---|---|---|
| 링크 상태 | 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 임계 초과 |
eBPF 기반 네트워크 모니터링
eBPF(extended Berkeley Packet Filter)는 커널 수정 없이 네트워크 스택의 다양한 지점에 프로그램을 삽입하여 실시간 모니터링을 수행할 수 있는 기술입니다. XDP, TC BPF, 소켓 레벨 BPF, kprobe/tracepoint 기반 BPF 등 다양한 후크(Hook) 포인트를 활용하여 드라이버 수준의 정밀한 관측이 가능합니다.
- XDP 기반 패킷 카운팅: 드라이버에서 패킷이 커널 스택에 도달하기 전에 통계를 수집합니다. 오버헤드가 극히 낮아 10Gbps 이상에서도 모든 패킷을 관측할 수 있습니다.
- TC BPF: ingress/egress TC 후크에 BPF 프로그램을 부착하여 플로우별 통계, 지연 측정, 조건부 미러링 등을 수행합니다.
- 소켓 레벨 BPF:
SO_ATTACH_BPF로 소켓 단위 모니터링을 수행합니다. - devlink trap: 하드웨어가 드롭한 패킷의 원인을 BPF 프로그램으로 분류하고 통계를 수집합니다.
/* XDP 기반 패킷 카운팅 프로그램 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
/* per-CPU 맵: 프로토콜별 패킷/바이트 카운터 */
struct stats_key {
__u16 proto; /* EtherType */
};
struct stats_val {
__u64 packets;
__u64 bytes;
};
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 256);
__type(key, struct stats_key);
__type(value, struct stats_val);
} proto_stats SEC(".maps");
/* 드롭 원인별 카운터 */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 4);
__type(key, __u32);
__type(value, __u64);
} drop_cnt SEC(".maps");
SEC("xdp")
int xdp_monitor(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct stats_key key;
struct stats_val *val, init_val = {};
/* 경계 검사: 이더넷 헤더 접근 가능 여부 */
if (data + sizeof(*eth) > data_end)
return XDP_PASS;
key.proto = eth->h_proto;
/* 프로토콜별 통계 업데이트 */
val = bpf_map_lookup_elem(&proto_stats, &key);
if (!val) {
bpf_map_update_elem(&proto_stats, &key,
&init_val, BPF_ANY);
val = bpf_map_lookup_elem(&proto_stats, &key);
if (!val)
return XDP_PASS;
}
val->packets++;
val->bytes += (data_end - data);
return XDP_PASS; /* 패킷을 정상 처리 경로로 전달 */
}
# BPF 모니터링 프로그램 로드 및 사용
# 1. XDP 모니터 프로그램 컴파일 및 로드
clang -O2 -target bpf -c xdp_monitor.c -o xdp_monitor.o
ip link set dev eth0 xdpgeneric obj xdp_monitor.o sec xdp
# 2. 통계 조회 (bpftool)
bpftool map dump name proto_stats
# 3. TC BPF를 사용한 플로우 모니터링
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj tc_monitor.o sec classifier
# 4. bpftrace를 사용한 실시간 드라이버 관측
# NAPI poll 빈도와 처리량 관측
bpftrace -e 'tracepoint:napi:napi_poll {
@poll_work = hist(args->work);
@poll_budget = hist(args->budget);
}'
# 드라이버별 패킷 수신 추적
bpftrace -e 'kprobe:netif_receive_skb {
$skb = (struct sk_buff *)arg0;
@rx_by_dev[$skb->dev->name] = count();
}'
# 5. devlink trap 모니터링
devlink trap show pci/0000:03:00.0
devlink trap set pci/0000:03:00.0 trap source_mac_is_multicast \
action trap
# trap된 패킷 확인
devlink trap group show pci/0000:03:00.0
Prometheus/Grafana 연동 설계
네트워크 드라이버 메트릭을 Prometheus로 수집하고 Grafana 대시보드로 시각화하면 장기적인 추세 분석과 이상 탐지가 가능합니다. node_exporter의 기본 네트워크 메트릭과 ethtool 통계를 결합하여 종합적인 모니터링 체계를 구축합니다.
node_exporter는 기본적으로 /proc/net/dev, /sys/class/net/ 정보를 수집합니다. 드라이버별 ethtool 통계는 --collector.ethtool 플래그를 활성화하거나 커스텀 익스포터(Custom Exporter)를 사용합니다.
| 메트릭 | Prometheus 이름 | 임계값 예시 | 경보 심각도 |
|---|---|---|---|
| RX 에러 증가율 | node_network_receive_errs_total | rate 5m > 0 | Warning |
| TX 타임아웃 | node_ethtool_tx_timeout | increase 1h > 0 | Critical |
| RX 드롭 | node_network_receive_drop_total | rate 5m > 100 | Warning |
| 링크 상태 변경 | node_network_carrier_changes_total | increase 10m > 2 | Warning |
| 인터페이스 속도 | node_network_speed_bytes | 변경 감지 | Info |
| 큐별 패킷 수 | node_ethtool_rx_queue_N_packets | 큐간 편차 70% 초과 | Warning |
| softnet time_squeeze | node_softnet_times_squeezed_total | rate 5m > 10 | Warning |
# Prometheus 경보 규칙 예시 (alerting_rules.yml)
cat <<'EOF'
groups:
- name: network_driver
rules:
- alert: NICTxTimeout
expr: increase(node_ethtool_tx_timeout[1h]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "NIC TX timeout 발생 ({{ $labels.device }})"
description: "{{ $labels.instance }}의 {{ $labels.device }}에서
TX timeout이 감지되었습니다."
- alert: HighRxDropRate
expr: rate(node_network_receive_drop_total[5m]) > 100
for: 10m
labels:
severity: warning
annotations:
summary: "높은 RX drop rate ({{ $labels.device }})"
- alert: LinkFlapping
expr: increase(node_network_carrier_changes_total[10m]) > 4
for: 5m
labels:
severity: warning
annotations:
summary: "링크 플랩 감지 ({{ $labels.device }})"
- alert: SoftnetSqueeze
expr: rate(node_softnet_times_squeezed_total[5m]) > 10
for: 15m
labels:
severity: warning
annotations:
summary: "softnet time_squeeze 빈발 (CPU {{ $labels.cpu }})"
EOF
netlink 이벤트 모니터링
netlink 소켓(Netlink Socket)은 커널과 사용자 공간 간 네트워크 설정 변경 이벤트를 교환하는 주요 인터페이스입니다. RTNETLINK 그룹을 구독하면 링크 상태 변경, 주소 추가/삭제, 라우트 변경 등을 실시간으로 감지할 수 있습니다.
주요 RTNETLINK 멀티캐스트 그룹(Multicast Group)은 다음과 같습니다.
- RTNLGRP_LINK: 인터페이스 생성, 삭제, 상태 변경 (up/down, MTU 변경 등)
- RTNLGRP_IPV4_IFADDR / RTNLGRP_IPV6_IFADDR: IP 주소 추가/삭제
- RTNLGRP_IPV4_ROUTE / RTNLGRP_IPV6_ROUTE: 라우팅 테이블 변경
- RTNLGRP_NEIGH: ARP/NDP 이웃 항목 변경
- RTNLGRP_TC: TC qdisc, class, filter 변경
/* netlink 이벤트 모니터 - C 구현 예시 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/if.h>
#include <net/if.h>
#include <time.h>
static void handle_link_msg(struct nlmsghdr *nlh)
{
struct ifinfomsg *ifi = NLMSG_DATA(nlh);
struct rtattr *rta;
int len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*ifi));
char ifname[IF_NAMESIZE] = "unknown";
char timebuf[64];
time_t now = time(NULL);
strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
localtime(&now));
/* 인터페이스 이름 추출 */
for (rta = IFLA_RTA(ifi); RTA_OK(rta, len);
rta = RTA_NEXT(rta, len)) {
if (rta->rta_type == IFLA_IFNAME) {
strncpy(ifname, RTA_DATA(rta),
IF_NAMESIZE - 1);
break;
}
}
const char *action;
switch (nlh->nlmsg_type) {
case RTM_NEWLINK:
action = (ifi->ifi_flags & IFF_UP) ?
"UP" : "DOWN";
break;
case RTM_DELLINK:
action = "DELETED";
break;
default:
action = "UNKNOWN";
}
printf("[%s] LINK %s: %s (index=%d, flags=0x%x)\n",
timebuf, action, ifname,
ifi->ifi_index, ifi->ifi_flags);
}
int main(void)
{
int fd;
struct sockaddr_nl sa;
char buf[8192];
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0) {
perror("socket");
return 1;
}
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
/* 구독할 멀티캐스트 그룹 설정 */
sa.nl_groups = RTMGRP_LINK | /* 링크 상태 */
RTMGRP_IPV4_IFADDR | /* IPv4 주소 */
RTMGRP_IPV6_IFADDR | /* IPv6 주소 */
RTMGRP_IPV4_ROUTE | /* IPv4 라우트 */
RTMGRP_NEIGH; /* ARP/NDP 이웃 */
if (bind(fd, (struct sockaddr *)&sa,
sizeof(sa)) < 0) {
perror("bind");
close(fd);
return 1;
}
printf("netlink 이벤트 모니터 시작...\n");
while (1) {
ssize_t len = recv(fd, buf, sizeof(buf), 0);
if (len <= 0)
break;
struct nlmsghdr *nlh;
for (nlh = (struct nlmsghdr *)buf;
NLMSG_OK(nlh, len);
nlh = NLMSG_NEXT(nlh, len)) {
switch (nlh->nlmsg_type) {
case RTM_NEWLINK:
case RTM_DELLINK:
handle_link_msg(nlh);
break;
case RTM_NEWADDR:
printf("[ADDR] 새 주소 추가\n");
break;
case RTM_DELADDR:
printf("[ADDR] 주소 삭제\n");
break;
case RTM_NEWROUTE:
printf("[ROUTE] 새 라우트 추가\n");
break;
case RTM_DELROUTE:
printf("[ROUTE] 라우트 삭제\n");
break;
}
}
}
close(fd);
return 0;
}
devlink trap과 drop reason 추적
커널 v5.17부터 도입된 SKB drop reason 인프라(Infrastructure)는 패킷이 드롭되는 정확한 원인을 추적할 수 있게 합니다. 기존에는 kfree_skb tracepoint에서 호출 위치만 확인할 수 있었지만, 이제는 NOT_SPECIFIED, NO_SOCKET, TCP_OLD_DATA 등 구체적인 사유가 함께 기록됩니다.
devlink trap은 하드웨어 레벨에서 드롭된 패킷을 소프트웨어로 전달하여 분석할 수 있게 하는 메커니즘입니다. NIC이 드롭한 패킷의 원인을 파악하는 데 필수적입니다.
- devlink trap: 하드웨어 드롭 이벤트를 소프트웨어로 트랩합니다. 드라이버가
devlink_trap_report()를 호출하여 드롭된 패킷과 원인을 커널에 보고합니다. - SKB drop reason:
kfree_skb_reason()을 통해 소프트웨어 스택에서의 드롭 원인을 기록합니다.perf record -e skb:kfree_skb로 추적 가능합니다. - drop_monitor: netlink 기반 인터페이스로 드롭 이벤트를 실시간으로 수신합니다.
# SKB drop reason 및 devlink trap 종합 분석
# 1. perf를 사용한 SKB drop reason 추적
# kfree_skb tracepoint에서 reason 필드 확인
perf record -e skb:kfree_skb -a -- sleep 10
perf script --fields=comm,pid,time,event,sym,trace | \
grep -oP 'reason: \K\S+' | sort | uniq -c | sort -rn
# 2. bpftrace로 실시간 drop reason 관측
bpftrace -e 'tracepoint:skb:kfree_skb {
@drop_reason[args->reason] = count();
}
interval:s:5 {
print(@drop_reason);
clear(@drop_reason);
}'
# 3. devlink trap 관리
# 사용 가능한 trap 목록 확인
devlink trap show pci/0000:03:00.0
# 특정 trap을 활성화하여 해당 패킷을 소프트웨어로 전달
devlink trap set pci/0000:03:00.0 \
trap source_mac_is_multicast action trap
# trap 그룹별 통계
devlink trap group show pci/0000:03:00.0
# trap 통계 초기화
devlink trap group set pci/0000:03:00.0 \
group l2_drops action drop
# 4. drop_monitor netlink 인터페이스
# dropwatch 도구 사용
dropwatch -l kas
# 대화형 세션에서:
# > start
# > set alertmode packet (패킷 단위 알림)
# > set trunc 100 (패킷 첫 100바이트 캡처)
# 5. /sys/kernel/debug/tracing 기반 drop reason 추적
echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
cat /sys/kernel/debug/tracing/trace_pipe | \
grep -v "reason: NOT_SPECIFIED" | head -50
echo 0 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
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
Intel은 리눅스 네트워크 드라이버 생태계에서 가장 오랜 역사와 가장 넓은 드라이버 포트폴리오를 보유하고 있습니다. 1GbE 데스크톱용 e1000e부터 100GbE 데이터센터용 ice, 그리고 차세대 가상화(Virtualization) 인터페이스 idpf까지 세대별 설계 철학이 뚜렷하게 달라집니다. 각 드라이버의 아키텍처 차이를 이해하면 성능 튜닝과 장애 진단 전략을 드라이버 세대에 맞춰 정확히 수립할 수 있습니다.
| 드라이버 | 주요 세대/용도 | 핵심 포인트 | Max Queues | XDP 지원 | 자주 보는 이슈 |
|---|---|---|---|---|---|
e1000e | 1GbE 서버/임베디드 | 안정성 우선, 기능 단순 | 1 (싱글 큐) | 미지원 | 링크 flap, 절전 전환 후 wake 지연 |
igb | 1GbE 멀티큐 | RSS/TSO 기본, SR-IOV 일부 모델 | 8 | 지원 (5.9+) | 큐 불균형, IRQ affinity 미스매치 |
ixgbe | 10GbE(82599/X540) | Flow Director, DCB, XDP 일부 경로 | 128 | 지원 (4.17+) | tx timeout, FDIR rule 관리 복잡도 |
i40e | XL710/X710(40/10GbE) | VF 관리, DDP/firmware 의존성 | 64 (per PF) | 지원 (5.1+) | firmware 호환성, reset 후 VF 상태 불일치 |
ice | E810(100GbE) | devlink/representor, 고급 tc offload | 256 | 지원 (5.5+) | DDP 패키지 불일치, eswitch 설정 충돌 |
idpf | 신규 인프라 VF/가상화 경로 | queue model/virtchnl 중심 설계 | 가변 (CP 결정) | 계획중 | 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"
# 드라이버별 모듈 파라미터 확인
modinfo e1000e | grep parm
modinfo ice | grep parm
e1000e: 싱글 큐 레거시 아키텍처
e1000e는 Intel PRO/1000 계열 PCIe GbE 컨트롤러를 위한 드라이버로, 싱글 TX/RX 큐 모델을 채택합니다. 멀티큐를 지원하지 않기 때문에 RSS, 큐별 NAPI 분리, XDP 등 최신 데이터 경로 최적화를 적용할 수 없지만, 단순한 구조 덕분에 안정성이 매우 높아 서버 관리 인터페이스(BMC/IPMI 전용 포트)와 임베디드 환경에서 여전히 널리 사용됩니다.
e1000e가 지원하는 주요 칩 변형은 다음과 같습니다. 세대별로 전력 관리와 부가 기능에 차이가 있으며, 특히 I219는 Intel vPro 플랫폼의 AMT(Active Management Technology) 트래픽을 처리하는 ME(Management Engine) 연동 경로가 있어 드라이버 동작이 미묘하게 다릅니다.
| 칩 변형 | 대표 제품 | 최대 속도 | PCIe Gen | 전력 상태 | WoL | HW Timestamp | 비고 |
|---|---|---|---|---|---|---|---|
| 82574L | 독립형 PCIe GbE | 1Gbps | Gen1 x1 | D0/D3hot | 지원 | 미지원 | 가장 안정적인 레거시 칩 |
| I217-V/LM | Haswell LPC 통합 | 1Gbps | 내부 버스(Bus) | D0/D3hot/D3cold | 지원 | 미지원 | PHY 내장, Low Power Idle(EEE) |
| I219-V/LM | Skylake+ PCH 통합 | 1Gbps | 내부 버스 | D0/D3hot/D3cold | 지원 | 미지원 | ME 연동, vPro AMT 트래픽 분리 |
ethtool --set-eee eth0 eee off로 EEE를 비활성화하고, D3cold 진입 후 WoL 복구가 느린 경우 BIOS에서 Deep Sleep 옵션을 비활성화하세요.
D3cold 상태로 진입하여 전력 소비를 최소화할 수 있습니다. 다만 D3cold에서 복구 시 PHY 재초기화에 최대 2~3초가 소요되므로, 저지연 WoL이 필요한 환경에서는 D3hot까지만 허용하는 것이 안전합니다.
# 실습 예제: e1000e 진단 및 전력 관리
# 드라이버/칩 정보 확인
ethtool -i eth0
# 출력 예: driver: e1000e, firmware-version: 0.13-4, bus-info: 0000:00:1f.6
# EEE 상태 확인 및 비활성화 (링크 flap 방지)
ethtool --show-eee eth0
ethtool --set-eee eth0 eee off
# WoL 설정 확인 및 활성화
ethtool -s eth0 wol g
ethtool -s eth0 wol d # WoL 비활성화
# 셀프 테스트 (offline 모드: 링크 일시 단절)
ethtool -t eth0 offline
# 링크 flap 이력 확인
dmesg | grep -i "e1000e.*link\|e1000e.*changed"
ethtool -S eth0 | grep -E "rx_no_buffer|tx_timeout|rx_crc_errors"
igb: 멀티큐 RSS 모델
igb는 Intel I350, I210, I211 계열 GbE 컨트롤러를 지원하며, e1000e와 달리 멀티큐 RSS(Receive Side Scaling)를 기본 지원합니다. 서버급 환경에서 1GbE 인터페이스를 고밀도로 운영하거나, PTP(Precision Time Protocol) 하드웨어 타임스탬프가 필요한 경우 igb가 권장됩니다.
| 칩 변형 | 포트 수 | 최대 큐 | SR-IOV VF | PTP HW TS | PCIe Gen | 비고 |
|---|---|---|---|---|---|---|
| I350 | 2/4 | 8 | 최대 8 VF | 소프트웨어 TS | Gen2 x4 | 서버용 멀티포트 |
| I210 | 1 | 4 | 미지원 | 하드웨어 TS | Gen2 x1 | PTP 최적, Qbv TSN 지원 |
| I211 | 1 | 2 | 미지원 | 미지원 | Gen1 x1 | 데스크톱/NAS용 저가 |
# 실습 예제: igb RSS 및 PTP 설정
# RSS 큐 수 확인 및 변경
ethtool -l eth0
ethtool -L eth0 combined 4
# RSS indirection table 확인
ethtool -x eth0
# indirection table을 큐 0,1에만 집중
ethtool -X eth0 equal 2
# RSS 해시 키 확인
ethtool -x eth0 | head -5
# SR-IOV VF 생성 (I350 전용)
echo 4 > /sys/class/net/eth0/device/sriov_numvfs
ip link show eth0
# PTP 하드웨어 타임스탬프 설정 (I210)
ethtool -T eth0
# ptp4l로 PTP 동기화 실행
ptp4l -i eth0 -m -H
# phc2sys로 시스템 클록 동기화
phc2sys -s eth0 -c CLOCK_REALTIME -O 0 -m
# IRQ affinity 확인
grep eth0 /proc/interrupts
cat /proc/irq/*/smp_affinity_list | head -8
ixgbe: 10GbE 고성능 아키텍처
ixgbe는 Intel 82599, X540, X550 시리즈 10GbE 컨트롤러를 지원하며, Linux 10GbE 시장에서 가장 오래 검증된 드라이버입니다. 128개 TX/RX 큐, Flow Director, DCB(Data Center Bridging), SR-IOV(최대 64 VF) 등 데이터센터급 기능을 폭넓게 제공합니다.
| 기능 | 82599 (X520) | X540 | X550 |
|---|---|---|---|
| 최대 속도 | 10Gbps (SFP+) | 10Gbps (Base-T) | 10Gbps (Base-T/SFP+) |
| PCIe | Gen2 x8 | Gen3 x4 | Gen3 x4 |
| RSS 큐 | 128 | 128 | 128 |
| SR-IOV VF | 64 | 64 | 64 |
| Flow Director | ATR + Perfect (8K) | ATR + Perfect (8K) | ATR + Perfect (8K) |
| DCB (TC) | 8 TC | 8 TC | 8 TC |
| XDP | 지원 (4.17+) | 지원 (4.17+) | 지원 (4.17+) |
| VXLAN/GENEVE 오프로드 | 미지원 | 미지원 | 지원 |
| NBASE-T (2.5G/5G) | 미지원 | 미지원 | 지원 (X550-T) |
| MACsec | 미지원 | 미지원 | 일부 SKU |
tx_timeout이 간헐적으로 발생하는 경우, tx_ring->next_to_use와 next_to_clean 간 격차가 TX descriptor ring 크기에 근접했는지 확인하세요. ethtool -G eth0 tx 4096으로 ring 크기를 늘리면 완화될 수 있습니다.
# 실습 예제: ixgbe 성능 튜닝 및 Flow Director
# 큐/링 설정 확인
ethtool -l eth0 # 큐 수 확인
ethtool -g eth0 # 링 크기 확인
ethtool -G eth0 rx 4096 tx 4096 # 링 크기 증가
# Flow Director ATR 활성화 확인
ethtool -k eth0 | grep ntuple
ethtool -K eth0 ntuple on
# Perfect Filter 규칙 추가
ethtool -N eth0 flow-type tcp4 src-ip 10.0.0.1 dst-port 443 action 0
ethtool -N eth0 flow-type udp4 dst-port 4789 action 4 # VXLAN
# 활성 FDIR 규칙 및 통계
ethtool -n eth0
ethtool -S eth0 | grep fdir
# DCB/PFC 설정 (lldptool 사용)
lldptool -T -i eth0 -V PFC enableTx=yes
lldptool -T -i eth0 -V ETS-CFG willing=no \
up2tc=0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7 \
tsa=0:ets,1:ets,2:ets,3:ets,4:ets,5:ets,6:ets,7:ets \
tcbw=12,12,12,12,13,13,13,13
# XDP 프로그램 로딩
ip link set dev eth0 xdpdrv obj xdp_prog.o sec xdp
# IRQ affinity 최적화 (CPU당 큐 1:1 매핑)
# Intel 제공 set_irq_affinity 스크립트 사용
./set_irq_affinity.sh local eth0
# 성능 모니터링
ethtool -S eth0 | grep -E "rx_bytes|tx_bytes|rx_packets|tx_packets"
ethtool -S eth0 | grep -E "tx_timeout|rx_missed|fdir"
i40e: AdminQ 기반 제어 경로
i40e는 Intel XL710/X710/XXV710 시리즈(10/25/40GbE)를 지원하며, 이전 세대(ixgbe)와 근본적으로 다른 AdminQ(Admin Queue) 기반 펌웨어 제어 모델을 도입했습니다. 드라이버가 하드웨어 레지스터를 직접 조작하는 대신, AdminQ 메시지를 통해 펌웨어에 명령을 전달하는 구조입니다. 이 설계는 SR-IOV VF 관리에서 virtchnl 프로토콜로 확장되어, PF 드라이버가 VF의 리소스 요청을 중재합니다.
| AdminQ 명령 카테고리 | 대표 명령 | 설명 |
|---|---|---|
| 스위치/필터 | ADD_FILTER, REMOVE_FILTER | MAC/VLAN/클라우드 필터 추가/삭제 |
| VSI 관리 | ADD_VSI, UPDATE_VSI | 가상 스테이션 인터페이스 생성/수정 |
| 큐 설정 | CONFIG_VSI_QUEUES | TX/RX 큐 매핑 및 파라미터 설정 |
| 링크 관리 | GET_LINK_STATUS, SET_PHY_CONFIG | 링크 상태 조회, PHY 설정 변경 |
| RSS | SET_RSS_KEY, SET_RSS_LUT | RSS 해시 키 및 indirection table 설정 |
| NVM/FW | NVM_UPDATE, GET_FW_VERSION | 펌웨어 업데이트, 버전 조회 |
| VF 제어 | SET_VF_CONFIG, RESET_VF | VF 리소스 할당, VF 리셋 |
AdminQ의 내부 동작은 관리 전송 큐(ATQ, Admin Transmit Queue)와 관리 수신 큐(ARQ, Admin Receive Queue) 두 개의 링 버퍼로 구성됩니다. 드라이버가 ATQ에 명령 디스크립터를 기록하면 펌웨어가 처리 후 ARQ에 응답을 기록합니다.
| FW 버전 범위 | 지원 커널 | 주요 기능 변경 | 알려진 이슈 |
|---|---|---|---|
| 6.x | 4.x ~ 5.4 | 기본 AdminQ, VF 128개 | VF reset 후 큐 불일치 |
| 7.x | 5.4 ~ 5.15 | Cloud Filter, 향상된 VF trust | 일부 FW/드라이버 조합에서 link flap |
| 8.x ~ 9.x | 5.15+ | DDP 지원, ADQ(Application Device Queue) | ADQ + SR-IOV 동시 사용 시 성능 저하 |
| 비교 항목 | i40e (XL710) | ice (E810) |
|---|---|---|
| 최대 속도 | 40GbE | 100GbE |
| FW 제어 모델 | AdminQ | AdminQ (확장) |
| VF 프로토콜 | virtchnl v1 | virtchnl v1 + v2 (idpf) |
| DDP 필수 여부 | 선택적 | 필수 (없으면 Safe Mode) |
| eswitch 모드 | 미지원 | 지원 (switchdev) |
| devlink 통합 | 기본적 | 풍부 (health, flash, param) |
| tc offload | 제한적 | flower/u32 offload |
| PTP 정밀도 | 소프트웨어 TS | 하드웨어 TS (ns) |
| XDP | 지원 (5.1+) | 지원 (5.5+) |
# 실습 예제: i40e AdminQ 및 VF 관리
# 펌웨어 버전 확인
ethtool -i eth0
devlink dev info pci/0000:03:00.0
# AdminQ 이벤트 로그 확인
dmesg | grep -i "i40e.*adminq\|i40e.*aq\|i40e.*fw"
# VF 생성 및 관리
echo 8 > /sys/class/net/eth0/device/sriov_numvfs
ip link set eth0 vf 0 mac 00:11:22:33:44:55
ip link set eth0 vf 0 vlan 100
ip link set eth0 vf 0 trust on
ip link set eth0 vf 0 rate 1000 # TX rate 제한 (Mbps)
# ADQ (Application Device Queue) 설정
# TC 4개 생성, 각각 큐 4개 할당
tc qdisc add dev eth0 root mqprio num_tc 4 \
map 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 \
queues 4@0 4@4 4@8 4@12 hw 1 mode channel
# VF 통계 모니터링
ethtool -S eth0 | grep "vf_"
ip -s link show eth0
idpf: virtchnl2 기반 차세대 인프라 드라이버
idpf(Infrastructure Data Path Function)는 Intel의 차세대 네트워크 드라이버로, 하드웨어 추상화를 극대화한 virtchnl2 프로토콜 기반 설계입니다. 기존 iavf 드라이버가 특정 세대의 PF(i40e/ice)에 종속적이었던 것과 달리, idpf는 Control Plane(CP)과 Data Plane을 완전히 분리하여 모든 세대의 Intel NIC에 대한 통합 VF/인프라 드라이버 역할을 목표로 합니다.
iavf(Intel Adaptive Virtual Function) 드라이버를 대체할 예정입니다. iavf는 virtchnl v1에 종속되어 특정 PF 드라이버(i40e/ice)와만 호환되지만, idpf의 virtchnl2는 프로토콜 수준에서 capability 협상을 수행하므로 PF 세대에 독립적입니다. 커널 6.4부터 idpf가 메인라인에 포함되었으며, 새 배포에서는 idpf 사용을 권장합니다.
| 비교 항목 | iavf (virtchnl v1) | idpf (virtchnl v2) |
|---|---|---|
| PF 종속성 | i40e/ice PF 필수 | CP 구현 독립적 |
| 큐 모델 | Single Queue 고정 | Split/Single 선택 가능 |
| Capability 협상 | 고정된 기능 세트 | 런타임 동적 협상 |
| 큐 재설정 | 전체 리셋 필요 | 개별 큐 그룹 재설정 |
| 메인라인 커널 | 4.2+ | 6.4+ |
| 유지보수 상태 | 유지보수 모드 | 적극 개발중 |
| RDMA 지원 | 미지원 | 계획중 |
| XDP | 제한적 | 계획중 |
# 실습 예제: idpf 드라이버 확인 및 큐 모델 점검
# idpf 드라이버 로딩 확인
lsmod | grep idpf
ethtool -i eth1 # driver: idpf 확인
# virtchnl2 capability 협상 결과 확인
dmesg | grep -i "idpf.*cap\|idpf.*queue\|idpf.*split"
# 큐 설정 확인 (Split Queue 모델 여부)
ethtool -l eth1
ethtool -S eth1 | grep -E "tx_q|rx_q|comp_q|buf_q"
# 큐 수 변경
ethtool -L eth1 combined 8
# CP(Control Plane) 연결 상태 확인
dmesg | grep -i "idpf.*mailbox\|idpf.*cp\|idpf.*control"
# 기본 통계 확인
ethtool -S eth1 | head -30
ip -s link show eth1
ICE vs i40e: 아키텍처 진화
ICE(E810)는 i40e(XL710/X710)의 후속 세대로, AdminQ 기반 펌웨어 제어 모델을 계승하면서도 DDP 필수화, devlink 완전 통합, eswitch/representor 모델, 하드웨어 PTP 타임스탬프 등 근본적인 아키텍처 확장을 도입했습니다. 특히 DDP가 패킷 파서를 런타임에 재프로그래밍하는 모델은 i40e에서는 선택적이었지만, ICE에서는 정상 동작의 전제 조건이 되었습니다.
| 비교 항목 | i40e (XL710/X710) | ICE (E810) | 진화 방향 |
|---|---|---|---|
| 최대 속도 | 40GbE | 100GbE | 대역폭 2.5배 증가 |
| RSS 큐 | 최대 64 | 최대 256 | 병렬 처리 확대 |
| SR-IOV VF | 최대 128 | 최대 256 | 가상화 밀도 증가 |
| DDP | 선택적 확장 | 필수 (Safe Mode fallback) | HW 파서 유연성 극대화 |
| devlink 지원 | 기본 (info 정도) | 완전 (health, flash, param, port) | 통합 관리 인터페이스 |
| eswitch | 미지원 | switchdev 모드 | OVS offload 지원 |
| tc flower | 미지원 | HW offload 지원 | 스위칭 규칙 HW 가속 |
| PTP | SW timestamp | HW timestamp (ns급) | 정밀 시간 동기화 |
| Adaptive ITR | 지원 | 지원 (4μs 단위) | 세밀한 코얼레싱 |
| XDP | 5.1+ | 5.5+ | 성능 향상 |
| RDMA | iWARP (irdma) | iWARP + RoCEv2 (irdma) | 프로토콜 범위 확장 |
linux-firmware 패키지가 최신인지 반드시 확인하세요.
ICE eswitch: Representor 모델과 Switchdev
ICE E810은 eswitch(Embedded Switch)를 통해 NIC 내부에 가상 스위치를 구성하고, 각 VF에 대응하는 representor netdev를 호스트에 노출합니다. 이 모델은 OVS(Open vSwitch) TC offload의 핵심 인프라이며, switchdev 모드로 전환하면 호스트의 TC subsystem이 NIC 하드웨어 필터 규칙을 직접 제어할 수 있습니다.
Representor 모델은 NIC 내부의 각 VF 포트에 대응하는 호스트 측 netdev(representor)를 생성합니다. OVS나 TC는 이 representor를 일반 netdev처럼 다루면서 스위칭 규칙을 설정하고, ICE 드라이버가 해당 규칙을 NIC 하드웨어에 오프로드합니다. Slow path(매칭 실패) 패킷은 representor를 통해 호스트 OVS로 올라와 소프트웨어 처리됩니다.
switchdev 모드로 전환하면 기존 VF 설정이 초기화되며, VF를 사용하는 VM이 일시적으로 연결이 끊길 수 있습니다. 전환 전에 반드시 모든 VF 트래픽이 중단된 상태에서 수행하세요. 또한 switchdev 모드에서는 ip link set vf 명령 대신 representor netdev를 통한 TC 규칙으로 VF 트래픽 정책을 관리해야 합니다.
# 실습 예제: ICE eswitch switchdev 모드 설정
# 1. 현재 eswitch 모드 확인
devlink dev eswitch show pci/0000:af:00.0
# 출력: pci/0000:af:00.0: mode legacy
# 2. VF 생성
echo 4 > /sys/class/net/eth0/device/sriov_numvfs
# 3. switchdev 모드 전환
devlink dev eswitch set pci/0000:af:00.0 mode switchdev
# representor netdev 생성 확인
ip link show | grep "eth0_"
# 4. OVS 브리지에 representor 연결
ovs-vsctl add-br br0
ovs-vsctl add-port br0 eth0_0 # VF0 representor
ovs-vsctl add-port br0 eth0_1 # VF1 representor
ovs-vsctl add-port br0 enp1s0f0 # Uplink representor
# 5. TC flower 규칙 추가 (HW 오프로드)
# VF0 → VF1 MAC 기반 포워딩
tc filter add dev eth0_0 ingress protocol ip \
flower dst_mac aa:bb:cc:dd:ee:01 \
action mirred egress redirect dev eth0_1
# 6. HW 오프로드 상태 확인
tc -s filter show dev eth0_0 ingress
# "in_hw" 플래그가 있으면 HW 오프로드 성공
# 7. eswitch 통계 확인
ethtool -S eth0 | grep "tx_repr\|rx_repr"
devlink dev eswitch show pci/0000:af:00.0
# 8. legacy 모드로 복원
ovs-vsctl del-br br0
devlink dev eswitch set pci/0000:af:00.0 mode legacy
hw-offload 옵션을 활성화하고, tc-policy=none으로 설정하세요. 이렇게 하면 OVS가 첫 번째 패킷으로 플로우를 학습한 후, 이후 패킷은 NIC eswitch에서 하드웨어 수준으로 스위칭합니다. ovs-vsctl set Open_vSwitch . other_config:hw-offload=true other_config:tc-policy=none
| eswitch 모드 | VF 제어 방식 | OVS 연동 | tc offload | 용도 |
|---|---|---|---|---|
| legacy (기본) | ip link set vf | 미지원 | 미지원 | 일반 SR-IOV 가상화 |
| switchdev | representor + TC 규칙 | 지원 (hw-offload) | flower/u32 | SDN, 클라우드 오버레이(Overlay) |
{pf_name}_{vf_index} 패턴을 따릅니다. 예를 들어 PF가 eth0이고 VF가 4개라면 eth0_0, eth0_1, eth0_2, eth0_3이 생성됩니다. Uplink representor는 물리 포트 이름(예: enp1s0f0)을 그대로 사용합니다.
eswitch switchdev 모드에서 지원하는 주요 tc flower 매칭 필드와 액션은 다음과 같습니다.
| 카테고리 | 매칭 필드 | 지원 액션 |
|---|---|---|
| L2 | src/dst MAC, VLAN ID, VLAN priority, EtherType |
redirect (포트 간 전달)drop (패킷 드롭)vlan push/pop (VLAN 태그 조작)tunnel_key set/unset (터널 캡슐화(Encapsulation))ct (커넥션 트래킹, 제한적)
|
| L3 | src/dst IPv4, src/dst IPv6, IP proto, DSCP/ECN | |
| L4 | src/dst port (TCP/UDP), TCP flags | |
| 터널 | VXLAN VNI, GENEVE VNI, GRE key |
# 실습 예제: eswitch tc flower 고급 규칙
# VXLAN 터널 패킷을 VF0으로 스티어링
tc filter add dev enp1s0f0 ingress protocol ip \
flower enc_dst_ip 10.0.0.1 enc_key_id 100 enc_dst_port 4789 \
action tunnel_key unset \
action mirred egress redirect dev eth0_0
# VF0 → 외부: VXLAN 캡슐화
tc filter add dev eth0_0 ingress protocol ip \
flower src_mac aa:bb:cc:dd:ee:00 \
action tunnel_key set id 100 src_ip 10.0.0.1 dst_ip 10.0.0.2 dst_port 4789 \
action mirred egress redirect dev enp1s0f0
# VLAN 기반 트래픽 분리
tc filter add dev eth0_0 ingress protocol 802.1q \
flower vlan_id 100 \
action vlan pop \
action mirred egress redirect dev eth0_1
# 오프로드된 규칙 수 확인
tc -s filter show dev eth0_0 ingress | grep "in_hw\|not_in_hw"
tc -s filter show dev enp1s0f0 ingress | grep "in_hw"
# devlink 포트 정보 확인
devlink port show pci/0000:af:00.0
# eswitch 장애 진단
devlink health show pci/0000:af:00.0
dmesg | grep -i "ice.*eswitch\|ice.*repr\|ice.*switchdev"
not_in_hw로 표시되어 소프트웨어 경로로 처리됩니다. 대규모 규칙이 필요한 환경에서는 규칙의 우선순위를 세밀하게 관리하고, tc -s filter show로 HW 오프로드 상태를 주기적으로 모니터링하세요.
DDP 트러블슈팅 확장
ICE 드라이버가 DDP 패키지 로드에 실패하면 Safe Mode로 폴백하여 RSS, Flow Director, SR-IOV 고급 기능이 모두 비활성화됩니다. 주요 실패 원인과 해결 방법은 다음과 같습니다.
| 실패 원인 | 증상 (dmesg) | 해결 방법 |
|---|---|---|
| FW 버전 불일치 | ice: Failed to load DDP package, FW mismatch | FW 업데이트: devlink dev flash pci/... file ice_pkg.bin |
| 패키지 파일 손상 | ice: DDP invalid signature | linux-firmware 패키지 재설치 |
| 패키지 파일 누락 | ice: Failed to open DDP package file | /lib/firmware/intel/ice/ddp/ice.pkg 배치 확인 |
| TCAM 진입 초과 | Flow Director 규칙 추가 실패 | 규칙 수 줄이거나 SKU 업그레이드 |
E810 SKU별 TCAM 진입 한계:
E810-C(100GbE, dual-port)는 TCAM 진입을 E810-XXV(25GbE)보다 더 많이 지원합니다.
E810-C는 FD(Flow Director) 진입 최대 512개 기본 설정인 반면,
E810-XXV는 256개로 제한될 수 있습니다.
ethtool -n eth0 rx-flow-hash tcp4 및 ethtool --show-ntuple eth0으로
현재 사용 규칙 수와 최대 용량을 확인할 수 있습니다.
i40e AdminQ Timeout 복구
i40e 드라이버에서 AdminQ(Administration Queue) timeout은 PF와 펌웨어 간 명령 채널이 응답하지 않을 때 발생합니다. 이 오류는 FW hang, PCI 전송 오류, 또는 과도한 AdminQ 명령 큐잉으로 발생합니다.
AdminQ timeout 감지: i40e_aq_send_msg_to_vf가
I40E_AQ_RC_EAGAIN을 반환하거나 타임아웃 워크큐가 트리거되면
드라이버는 PF 리셋 시퀀스를 시작합니다.
PF 리셋이 완료되면 VF에게 VIRTCHNL_OP_RESET_VF 메시지를 전송하여
VF 드라이버(iavf)가 채널을 재설정하도록 지시합니다.
# DDP 로드 상태 확인
dmesg | grep -E "ice.*ddp|ice.*package|ice.*safe mode"
ls -la /lib/firmware/intel/ice/ddp/
# AdminQ 오류 확인 (i40e)
dmesg | grep -E "i40e.*AdminQ|i40e.*AQ timeout|i40e.*reset"
# i40e 드라이버 통계에서 AdminQ 오류 카운터 확인
ethtool -S eth0 | grep -E "admin_queue|aq_"
# VF 리셋 이력 확인
dmesg | grep -E "iavf.*reset|i40e.*VF.*reset"
# i40e AdminQ 링 크기 조정 (모듈 파라미터)
modprobe i40e adminq_work_limit=64
# ice DDP 패키지 수동 지정
modprobe ice fw_name=intel/ice/ddp/ice-1.3.30.0.pkg
Intel 공통 성능 튜닝 체크리스트
Intel NIC 드라이버 세대와 관계없이 공통적으로 적용할 수 있는 성능 튜닝 포인트를 정리합니다. 특히 10GbE 이상의 고대역폭 환경에서는 커널 파라미터, IRQ affinity, 링 버퍼 크기 등을 체계적으로 점검해야 합니다.
| 튜닝 항목 | 확인 명령 | 권장 설정 | 대상 드라이버 |
|---|---|---|---|
| RX/TX 링 크기 | ethtool -g eth0 | 최대값 (2048~4096) | 모든 드라이버 |
| RSS 큐 수 | ethtool -l eth0 | 물리 코어 수 이하 | igb, ixgbe, i40e, ice |
| IRQ affinity | cat /proc/interrupts | 큐당 전용 CPU 코어 | 멀티큐 드라이버 전체 |
| Adaptive Coalescing | ethtool -c eth0 | 워크로드별 조정 | 모든 드라이버 |
| GRO/TSO | ethtool -k eth0 | 활성화 유지 | 모든 드라이버 |
| XPS (TX 큐 → CPU 매핑) | cat /sys/class/net/eth0/queues/tx-*/xps_cpus | NUMA 로컬 CPU | 멀티큐 드라이버 전체 |
| Busy Polling | sysctl net.core.busy_poll | 50 (μs), 저지연 환경 | 모든 드라이버 |
| NUMA 친화 | cat /sys/class/net/eth0/device/numa_node | NIC NUMA 노드에 프로세스(Process) 고정 | 모든 드라이버 |
# Intel NIC 공통 성능 튜닝 스크립트
IFACE=eth0
# 링 버퍼 최대화
ethtool -G $IFACE rx 4096 tx 4096
# RSS 큐 수를 물리 코어 수에 맞춤
CORES=$(nproc --all)
ethtool -L $IFACE combined $CORES
# GRO, TSO, Checksum 오프로드 활성화 확인
ethtool -K $IFACE gro on tso on tx on rx on
# IRQ affinity 자동 설정 (irqbalance 대신 수동)
systemctl stop irqbalance
# Intel 제공 스크립트 또는 수동 설정
IRQS=$(grep $IFACE /proc/interrupts | awk '{print $1}' | tr -d ':')
CPU=0
for IRQ in $IRQS; do
echo $CPU > /proc/irq/$IRQ/smp_affinity_list
CPU=$((CPU + 1))
done
# XPS 설정 (TX 큐를 대응 CPU에 매핑)
for i in $(seq 0 $((CORES - 1))); do
printf "%x" $((1 << i)) > /sys/class/net/$IFACE/queues/tx-$i/xps_cpus
done
# Busy Polling (저지연 환경)
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=50
# NUMA 노드 확인 및 프로세스 고정
NUMA=$(cat /sys/class/net/$IFACE/device/numa_node)
echo "NIC NUMA node: $NUMA"
# numactl --cpunodebind=$NUMA --membind=$NUMA ./application
irqbalance 데몬의 자동 분배 대신 수동 IRQ affinity 설정을 권장합니다. irqbalance는 주기적으로 IRQ를 재배치(Relocation)하여 캐시 워밍을 깨뜨릴 수 있습니다. Intel의 set_irq_affinity.sh 스크립트를 사용하면 NUMA 토폴로지를 고려한 최적 매핑을 자동 계산합니다.
Intel NIC 드라이버 계열은 세대마다 설계 철학이 진화해 왔으며, 최신 커널에서는 ice + idpf 조합이 가장 포괄적인 기능 세트를 제공합니다. 기존 i40e/ixgbe 환경에서 업그레이드를 검토할 때는 DDP 패키지 배치, devlink 인프라 준비, SR-IOV VF 마이그레이션 계획을 사전에 수립하는 것이 중요합니다.
# Intel NIC 드라이버 세대별 빠른 점검 스크립트
# 시스템의 모든 Intel NIC 드라이버 요약
for dev in /sys/class/net/*/device/driver; do
IFACE=$(echo "$dev" | cut -d'/' -f5)
DRIVER=$(basename $(readlink "$dev"))
case "$DRIVER" in
e1000e|igb|ixgbe|i40e|ice|idpf|iavf)
FW=$(ethtool -i "$IFACE" 2>/dev/null | grep firmware | awk '{print $2}')
QUEUES=$(ethtool -l "$IFACE" 2>/dev/null | grep Combined | tail -1 | awk '{print $2}')
echo "$IFACE: driver=$DRIVER fw=$FW queues=$QUEUES"
;;
esac
done
# DDP 패키지 상태 확인 (ice 전용)
if lsmod | grep -q ice; then
echo "=== ICE DDP Status ==="
ls -la /lib/firmware/intel/ice/ddp/ 2>/dev/null
dmesg | grep -i "ice.*ddp\|ice.*package" | tail -5
fi
# devlink 지원 드라이버 일괄 점검
devlink dev info 2>/dev/null | head -20
NVIDIA/Mellanox: mlx5e / mlx4_en
NVIDIA(구 Mellanox)의 ConnectX 시리즈는 데이터센터 네트워킹의 사실상 표준으로, 하드웨어 기반 RDMA, SR-IOV, 정밀 패킷 스티어링, XDP 제로카피 등 리눅스 커널의 최신 네트워킹 기능을 가장 적극적으로 지원하는 NIC 플랫폼입니다. 커널 드라이버는 mlx5_core(통합 코어)와 mlx5e(이더넷), mlx5_ib(InfiniBand/RoCE) 모듈로 구성되며, 레거시 ConnectX-3 이하는 mlx4_core/mlx4_en으로 지원됩니다.
| 드라이버 | 지원 하드웨어 | 최대 속도 | Max Queues | XDP | RDMA | SR-IOV | 커널 모듈(Kernel Module) | 비고 |
|---|---|---|---|---|---|---|---|---|
| mlx5e | ConnectX-4 ~ ConnectX-7, BlueField-1/2/3 | 400 GbE | 256 (per port) | O (AF_XDP 포함) | RoCEv2 / IB | 1024 VFs | mlx5_core, mlx5e | 현재 권장 드라이버 |
| mlx4_en | ConnectX-2 ~ ConnectX-3 Pro | 56 Gb/s IB, 40 GbE | 64 | O (기본) | IB / RoCEv1 | 64 VFs | mlx4_core, mlx4_en | 레거시, 유지보수 모드 |
NVIDIA DOCA SDK와 커널 드라이버의 관계: DOCA(Data Center Infrastructure on Accelerated Computing) SDK는 BlueField DPU에서 사용자 공간 데이터 경로를 제공하지만, 기반 커널 드라이버는 여전히 mlx5_core입니다. upstream 커널의 mlx5 드라이버는 DOCA 없이도 독립적으로 모든 기능을 제공합니다.
mlx5 하드웨어 및 소프트웨어 아키텍처
mlx5 드라이버는 ConnectX-4 이상의 하드웨어를 지원하는 통합 코어 드라이버 mlx5_core를 기반으로 합니다. 하드웨어 내부는 패킷 수신/발신 경로를 세밀하게 제어할 수 있는 스티어링 엔진, 가상 포트(vPort), 작업 큐(WQ) 체계로 구성되며, 소프트웨어 스택은 이더넷·RDMA·vDPA 등 다양한 서브시스템을 단일 코어 위에서 분기합니다.
ConnectX 시리즈의 하드웨어 블록 구조는 다음과 같습니다. 물리 포트에서 수신된 패킷은 스티어링 엔진(Flow Steering Engine)을 거쳐 가상 포트(vPort)별로 분류되고, 각 vPort에 연결된 작업 큐(WQ)의 수신 큐(RQ) 또는 송신 큐(SQ)로 전달됩니다. 완료 이벤트는 CQ(Completion Queue)를 통해 EQ(Event Queue)로 전파되며, 최종적으로 MSI-X 인터럽트가 호스트 CPU에 전달됩니다.
소프트웨어 스택은 mlx5_core가 HCA(Host Channel Adapter) 초기화, 커맨드 인터페이스, EQ/CQ 관리 등의 공통 기반을 제공하며, 그 위에 이더넷(mlx5e), InfiniBand/RoCE(mlx5_ib), vDPA(mlx5_vdpa) 서브시스템이 독립적으로 동작합니다.
ConnectX 세대별 기능 진화는 다음 표와 같습니다. 각 세대는 대역폭뿐 아니라 하드웨어 오프로드 범위가 크게 확장되었습니다.
| 기능 | ConnectX-5 | ConnectX-6 | ConnectX-6 Dx | ConnectX-7 |
|---|---|---|---|---|
| 최대 속도 | 100 GbE (2x) | 200 GbE (HDR IB) | 100 GbE (2x) | 400 GbE (NDR IB) |
| PCIe | Gen 3.0 x16 | Gen 3.0/4.0 x16 | Gen 4.0 x16 | Gen 5.0 x16 |
| Crypto Offload | - | IPsec inline | IPsec + TLS inline | IPsec + TLS + MACsec |
| Steering 용량 | 4M rules | 16M rules | 16M rules | 64M+ rules |
| XDP | Native + AF_XDP | Native + AF_XDP | Native + AF_XDP + Multi-buf | Native + AF_XDP + Multi-buf |
| GPUDirect | RDMA | RDMA + Storage | RDMA + Storage | RDMA + Storage + Network |
| eSwitch 향상 | Switchdev 기본 | Connection Tracking HW | CT + NAT offload | CT + NAT + Sample offload |
| Scalable Functions | - | - | 지원 | 지원 (향상) |
# mlx5 디바이스 정보 확인
lspci -d 15b3: -vvv | head -30
ethtool -i enp1s0f0np0
# mlx5 드라이버 로드 상태 및 파라미터 확인
modinfo mlx5_core
cat /sys/module/mlx5_core/parameters/prof_sel
# devlink 디바이스 정보
devlink dev show pci/0000:01:00.0
devlink dev info pci/0000:01:00.0
# HCA capabilities 확인 (디버깅용)
devlink dev param show pci/0000:01:00.0
rdma dev show mlx5_0
mlx5 Flow Steering 파이프라인
mlx5의 패킷 스티어링은 하드웨어 내장 Flow Table 계층 구조를 기반으로 동작합니다. 세 가지 주요 도메인이 존재합니다: NIC Flow Table(일반 NIC RX/TX 경로), FDB(Forwarding DataBase, eSwitch 경로), RDMA Flow Table(RDMA 트래픽 경로). 각 도메인 내에서 패킷은 우선순위 기반으로 Flow Table을 순차 탐색하며, 첫 번째 매칭 규칙의 액션이 수행됩니다.
NIC RX Flow Table은 일반적으로 다음 계층으로 구성됩니다: Bypass(tc flower offload 규칙) → Kernel(ethtool ntuple 등) → Leftovers(기본 RSS/steering). FDB는 eSwitch Switchdev 모드에서 활성화되며, VF/SF 간 패킷 전달과 OVS offload의 핵심입니다.
TC flower offload는 mlx5에서 가장 강력한 스티어링 기능 중 하나다. tc flower 필터를 skip_sw 플래그와 함께 추가하면 하드웨어 Flow Table에 직접 규칙이 삽입되며, 소프트웨어 경로를 완전히 우회합니다.
# TC flower offload: 특정 IP의 트래픽을 VF representor로 리다이렉트
tc qdisc add dev enp1s0f0np0 ingress
tc filter add dev enp1s0f0np0 ingress protocol ip \
flower skip_sw \
src_ip 10.0.0.0/24 \
action mirred egress redirect dev enp1s0f0np0_0
# VXLAN encap offload
tc filter add dev enp1s0f0np0_0 egress protocol ip \
flower skip_sw \
src_ip 192.168.1.0/24 \
action tunnel_key set id 100 src_ip 10.0.0.1 dst_ip 10.0.0.2 \
action mirred egress redirect dev vxlan_sys_4789
# Connection Tracking offload (CX-6+)
tc filter add dev enp1s0f0np0 ingress protocol ip \
flower skip_sw ct_state +trk+est \
action goto chain 1
tc filter add dev enp1s0f0np0 ingress protocol ip chain 1 \
flower skip_sw \
action ct commit \
action mirred egress redirect dev enp1s0f0np0_0
# OVS HW offload 활성화 (OVS 2.8+)
ovs-vsctl set Open_vSwitch . other_config:hw-offload=true
ovs-vsctl set Open_vSwitch . other_config:tc-policy=skip_sw
systemctl restart openvswitch
# offload된 flow 통계 확인
tc -s filter show dev enp1s0f0np0 ingress
OVS offload 성능 팁: OVS HW offload를 사용할 때 tc-policy=skip_sw를 설정하면 모든 규칙이 하드웨어 우선으로 설치됩니다. Connection Tracking offload(CX-6 이상)를 함께 사용하면 stateful firewall 규칙까지 하드웨어에서 처리되어 10M+ flows/sec 수준의 성능을 달성할 수 있습니다.
mlx5 eSwitch: Legacy vs Switchdev 모드
mlx5 eSwitch는 NIC 내부의 가상 스위치로, SR-IOV VF와 PF 간의 트래픽을 관리합니다. 두 가지 동작 모드가 있습니다: Legacy 모드는 전통적인 MAC/VLAN 기반 L2 스위칭을 제공하며, Switchdev 모드는 각 VF/SF에 대한 representor netdev를 생성하여 TC/OVS 기반의 정밀한 프로그래머블 데이터 경로를 제공합니다.
eSwitch 모드 전환 시에는 기존 VF 설정이 초기화되므로, 운영 중인 VM의 네트워크 연결이 일시적으로 끊어질 수 있습니다. 사전에 VF를 unbind하고 전환 후 다시 bind하는 절차가 필요합니다.
# 현재 eSwitch 모드 확인
devlink dev eswitch show pci/0000:01:00.0
# SR-IOV VF 생성
echo 4 > /sys/class/net/enp1s0f0np0/device/sriov_numvfs
# Switchdev 모드로 전환 (VF unbind 필요)
echo 0000:01:00.2 > /sys/bus/pci/drivers/mlx5_core/unbind
echo 0000:01:00.3 > /sys/bus/pci/drivers/mlx5_core/unbind
devlink dev eswitch set pci/0000:01:00.0 mode switchdev
echo 0000:01:00.2 > /sys/bus/pci/drivers/mlx5_core/bind
echo 0000:01:00.3 > /sys/bus/pci/drivers/mlx5_core/bind
# Representor 디바이스 확인
ip link show type mlx5_rep 2>/dev/null || ip link show | grep repr
# OVS bridge에 representor 추가
ovs-vsctl add-br br-int
ovs-vsctl add-port br-int enp1s0f0np0 # uplink representor
ovs-vsctl add-port br-int enp1s0f0np0_0 # VF0 representor
ovs-vsctl add-port br-int enp1s0f0np0_1 # VF1 representor
# Scalable Functions (SF) 생성 (CX-6 Dx+)
devlink port add pci/0000:01:00.0 flavour pcisf pfnum 0 sfnum 8
devlink port function set pci/0000:01:00.0/32768 \
hw_addr 00:11:22:33:44:55 state active
# Legacy 모드로 복귀
devlink dev eswitch set pci/0000:01:00.0 mode legacy
Switchdev 모드 전환 주의사항: 모드 전환은 NIC의 모든 스티어링 규칙을 초기화합니다. 운영 환경에서는 반드시 유지보수 윈도우에서 수행해야 하며, VF가 할당된 모든 VM/컨테이너의 네트워크가 일시 중단됩니다. 또한 일부 ConnectX-5 초기 펌웨어는 Switchdev → Legacy 복귀 시 NIC 리셋이 필요할 수 있습니다.
mlx5e XDP 및 AF_XDP 제로카피
mlx5e는 MPWQE(Multi-Packet Work Queue Entry) 기반의 효율적인 XDP 경로를 제공합니다. MPWQE는 하나의 WQE에 여러 패킷을 담는 Striding RQ 방식으로, 메모리 효율과 캐시 친화성이 높습니다. XDP 프로그램은 RQ에서 패킷을 수신한 직후, SKB 할당 전에 실행되어 XDP_DROP, XDP_TX, XDP_REDIRECT, XDP_PASS 결정을 내립니다.
AF_XDP 제로카피 모드에서 mlx5e는 UMEM(User Memory)을 직접 RQ에 매핑하여 커널 버퍼 복사 없이 사용자 공간으로 패킷을 전달합니다. 아래는 세대별 AF_XDP 성능 데이터입니다.
| 시나리오 | CX-5 100G | CX-6 Dx 100G | CX-7 200G | 비고 |
|---|---|---|---|---|
| XDP_DROP (single core) | ~24 Mpps | ~28 Mpps | ~35 Mpps | 64B 패킷 |
| XDP_TX (single core) | ~18 Mpps | ~22 Mpps | ~28 Mpps | loopback 모드 |
| AF_XDP RX zero-copy | ~12 Mpps | ~16 Mpps | ~22 Mpps | 1 socket, 1 queue |
| AF_XDP TX zero-copy | ~10 Mpps | ~14 Mpps | ~19 Mpps | 1 socket, 1 queue |
| AF_XDP bidi zero-copy | ~8 Mpps | ~12 Mpps | ~16 Mpps | RX+TX 동시 |
# XDP 프로그램 로드 (native 모드)
ip link set dev enp1s0f0np0 xdp obj xdp_prog.o sec xdp
# XDP 프로그램 상태 확인
ip link show dev enp1s0f0np0
bpftool prog show
bpftool net show dev enp1s0f0np0
# AF_XDP 소켓 성능 테스트 (xdp-tools)
xdp-bench rx enp1s0f0np0 -m native
xsk_fwd -i enp1s0f0np0 -q 0 -z # zero-copy 모드
# XDP 통계 확인
ethtool -S enp1s0f0np0 | grep xdp
# rx_xdp_drop, rx_xdp_redirect, rx_xdp_tx, rx_xdp_tx_full 등
# Multi-buffer XDP (CX-6 Dx+, jumbo frame 지원)
ip link set dev enp1s0f0np0 mtu 9000
ip link set dev enp1s0f0np0 xdp obj xdp_mb_prog.o sec xdp
# XDP 프로그램 해제
ip link set dev enp1s0f0np0 xdp off
AF_XDP 성능 최적화: 최상의 성능을 위해 (1) IRQ 어피니티와 XSK 소켓을 동일 CPU에 배치하고, (2) busy_poll을 활성화하며(setsockopt SO_BUSY_POLL), (3) UMEM 프레임 크기를 2048 또는 4096으로 설정합니다. mlx5e는 XDP_USE_NEED_WAKEUP 플래그도 지원하여 불필요한 시스템콜을 줄입니다.
mlx5 RDMA: RoCEv2 / GPUDirect / ODP
mlx5_ib 모듈은 ConnectX NIC의 RDMA 기능을 IB verbs 인터페이스로 노출합니다. RoCEv2(RDMA over Converged Ethernet v2)는 UDP/IP 캡슐화를 사용하여 표준 이더넷 스위치 인프라 위에서 RDMA를 제공하며, GPUDirect RDMA는 GPU 메모리에 대한 직접 DMA를 지원하여 AI/HPC 워크로드에서 핵심 역할을 합니다.
| 기능 | 설명 | 커널 요구사항 | 최소 HW |
|---|---|---|---|
| RoCEv2 | UDP/IP 기반 RDMA, ECN/PFC QoS 연동 | mlx5_ib, rdma-core | CX-4 |
| ODP (On-Demand Paging) | MR 등록 없이 가상 메모리(Virtual Memory) 직접 사용, page fault 처리 | CONFIG_INFINIBAND_ON_DEMAND_PAGING | CX-4 |
| GPUDirect RDMA | GPU 메모리 ↔ NIC 직접 DMA (nvidia-peermem) | nvidia-peermem 모듈 | CX-5 |
| GPUDirect Storage | NVMe ↔ GPU 메모리 직접 전송 | nvidia-fs 모듈 | CX-6 |
| Adaptive Routing | 네트워크 혼잡 시 자동 경로 변경 | FW 지원 필요 | CX-7 |
| Device Memory (DM) | NIC 온보드 메모리에 QP 컨텍스트 저장 | mlx5_ib DM API | CX-5 |
| Crypto MR | 하드웨어 암호화된 RDMA 전송 | FW 지원 필요 | CX-6 Dx |
# RDMA 디바이스 및 포트 상태 확인
rdma dev show
rdma link show mlx5_0/1
ibstat mlx5_0
# RoCEv2 모드 설정 (기본값)
cma_roce_mode -d mlx5_0 -p 1 -m 2 # RoCE v2
# ECN 설정 (DCQCN)
mlnx_qos -i enp1s0f0np0 --trust dscp
mlnx_qos -i enp1s0f0np0 --pfc 0,0,0,1,0,0,0,0 # TC3에 PFC
# GPUDirect RDMA (nvidia-peermem 로드)
modprobe nvidia-peermem
cat /sys/module/nvidia_peermem/version
# RDMA 대역폭 테스트
ib_write_bw -d mlx5_0 -x 3 --report_gbits # server
ib_write_bw -d mlx5_0 -x 3 --report_gbits 10.0.0.1 # client
# ODP (On-Demand Paging) 확인
rdma dev show mlx5_0 -j | jq '.[] | .caps'
RoCEv2와 Ethernet 드라이버 상호작용
RoCEv2 트래픽은 mlx5_ib가 생성하지만,
실제 패킷 전송은 mlx5e(Ethernet 드라이버)의 SQ(Send Queue)를 공유합니다.
CQ(Completion Queue)와 EQ(Event Queue) 인프라도 Ethernet과 RDMA 경로가 공유하므로,
RDMA 워크로드가 CQ를 고갈시키면 Ethernet 트래픽에도 영향이 미칠 수 있습니다.
PFC(Priority Flow Control)와 ECN(Explicit Congestion Notification)은 RoCEv2 환경에서 필수입니다. RoCEv2는 패킷 드롭에 취약하므로(TCP의 재전송과 달리 RDMA는 재전송 메커니즘이 제한적), lossless 패브릭 구성이 요구됩니다. PFC는 특정 우선순위 큐에서 혼잡이 발생할 때 해당 우선순위 트래픽만 일시 정지시켜 패킷 드롭 없이 버퍼 압력을 완화합니다. ECN(DCQCN 알고리즘)은 네트워크 혼잡이 시작될 때 송신 속도를 선제적으로 낮춥니다.
DSCP 기반 우선순위 매핑: mlx5는 RoCEv2 패킷에 특정 DSCP 값을 부여하고, 스위치와 NIC가 해당 DSCP를 TC(Traffic Class)로 매핑하여 PFC와 ECN을 적용합니다. 일반적으로 RoCEv2는 DSCP 26(CS3) 또는 DSCP 46(EF)을 사용하며, mlnx_qos 도구로 NIC 측 매핑을 설정합니다.
# RoCEv2 설정 검증 명령어
# 1. RoCE 모드 확인 (v2 = 2)
cma_roce_mode -d mlx5_0 -p 1
# port 1 RoCE mode: RoCE v2
# 2. PFC 설정 확인 (TC3에 PFC 적용 예시)
mlnx_qos -i enp1s0f0np0 --pfc
# pfc: 0,0,0,1,0,0,0,0 (TC3만 lossless)
# 3. DSCP → 우선순위 매핑 확인
mlnx_qos -i enp1s0f0np0 --trust dscp
mlnx_qos -i enp1s0f0np0 --dscp2prio
# 4. ECN 설정 확인
cat /sys/kernel/debug/mlx5/0000:*/cc_params/cc_algo 2>/dev/null
# DCQCN이 활성화되어 있어야 함
# 5. CQ/EQ 인프라 공유 상태 확인
rdma stat show mlx5_0
ethtool -S enp1s0f0np0 | grep -E "rx_cq_|tx_cq_"
# 6. RoCE 트래픽 카운터 확인
rdma stat show -j mlx5_0 | jq '.[] | select(.type=="mr")'
perfquery -d mlx5_0 -x 3 -l # 포트 카운터 (패킷 드롭 확인)
mlx5 devlink Health Reporter
mlx5 드라이버는 devlink health 프레임워크를 통해 하드웨어 오류 감지, 진단, 자동 복구를 제공합니다. 각 reporter는 특정 하드웨어/소프트웨어 구성 요소를 모니터링하며, 오류 발생 시 진단 정보(dump)를 수집하고 자동 복구를 시도합니다.
| Reporter | 모니터링 대상 | Dump 내용 | 복구 방법 |
|---|---|---|---|
| fw | FW 비정상 syndrome | FW health buffer, syndrome code | FW 커맨드 인터페이스 리셋 |
| fw_fatal | FW 치명적 오류 (assert/crash) | CR-space 전체 덤프, FW trace | PCI reset / full HCA reset |
| tx | TX timeout, SQ 에러 | SQ 상태, WQE 내용, CQ 상태 | SQ reset + 재시작(Reboot) |
| rx | RQ 에러, RX timeout | RQ 상태, ICOSQ 상태 | RQ/channel 재시작 |
| vnic | vNIC 진단 카운터 | TX/RX queue counter, total errors | 진단 전용 (복구 없음) |
# Health reporter 목록 및 상태 확인
devlink health show pci/0000:01:00.0
# 특정 reporter 상세 정보
devlink health show pci/0000:01:00.0 reporter fw
# 진단 정보 조회
devlink health diagnose pci/0000:01:00.0 reporter fw
devlink health diagnose pci/0000:01:00.0 reporter vnic
# 덤프 수집 (FW 오류 분석용)
devlink health dump show pci/0000:01:00.0 reporter fw_fatal
# 수동 복구 트리거
devlink health recover pci/0000:01:00.0 reporter tx
# Auto-recovery 설정
devlink health set pci/0000:01:00.0 reporter fw \
auto_recover true grace_period 60000
devlink health set pci/0000:01:00.0 reporter fw_fatal \
auto_dump true
# Health 이벤트 모니터링 (실시간)
devlink health -j monitor
fw_fatal reporter와 PCI reset: fw_fatal reporter의 복구는 PCI-level reset을 수행하므로, 해당 NIC에 연결된 모든 네트워크 인터페이스와 VF가 일시적으로 중단됩니다. grace_period를 적절히 설정하여 빈번한 리셋 루프를 방지해야 합니다. 기본값은 60초입니다.
mlx5 펌웨어 관리
ConnectX NIC의 펌웨어 업데이트는 devlink 프레임워크를 통해 수행됩니다. 업데이트 프로세스는 FW 이미지 다운로드, 무결성(Integrity) 검증, 플래시 기록, 활성화의 단계로 진행되며, flash_only 옵션을 사용하면 NIC 리셋 없이 다음 부팅 시 적용할 수 있습니다.
# 현재 FW 버전 확인
devlink dev info pci/0000:01:00.0
ethtool -i enp1s0f0np0 | grep firmware
# FW 업데이트 (즉시 활성화)
devlink dev flash pci/0000:01:00.0 \
file fw-ConnectX7-rel-28_39_1002-MCX713106AS-VEA_Ax-UEFI-14.32.17-FlexBoot-3.7.300.bin
# FW 업데이트 (다음 리부팅 시 적용)
devlink dev flash pci/0000:01:00.0 \
file firmware.mfa2 component fw \
overwrite settings
# FW reset (live reset, 서비스 중단 최소화)
devlink dev reload pci/0000:01:00.0 action fw_activate
# 설정 보존 여부 확인
mlxconfig -d /dev/mst/mt4129_pciconf0 query
# NIC 설정 변경 (mlxconfig, OFED 도구)
mlxconfig -d /dev/mst/mt4129_pciconf0 set SRIOV_EN=1 NUM_OF_VFS=16
펌웨어 업데이트 시 주의: (1) 업데이트 중 전원 차단 시 NIC가 복구 불가능(brick)해질 수 있으므로 UPS 환경에서 수행합니다. (2) overwrite settings 플래그 없이 업데이트하면 기존 NIC 설정(SR-IOV, UEFI 부팅 등)이 공장 초기값으로 리셋될 수 있습니다. (3) 듀얼 포트 NIC는 한쪽 포트로만 업데이트하면 됩니다(FW는 공유).
mlx5 최대 강점: 타 드라이버 비교 심층 분석
mlx5e는 단일 드라이버 안에서 가장 넓은 범위의 커널 네트워킹 기능을 프로덕션 수준으로 지원하는 유일한 NIC 드라이버입니다. 여기서는 mlx5의 핵심 강점을 다른 드라이버와 정량적·구조적으로 비교하여, 왜 데이터센터 표준이 되었는지를 분석합니다.
강점 1: Flow Steering — 압도적 규칙 용량과 유연성
mlx5의 하드웨어 Flow Steering Engine은 64M+ 이상의 flow rules을 지원합니다(ConnectX-7 기준). 이는 경쟁 드라이버 대비 수백~수천 배의 격차입니다. 단순한 숫자 차이를 넘어, Flow Table 계층 구조의 아키텍처적 유연성이 핵심 차별점입니다.
| 드라이버 | HW Flow Rules 용량 | 매치 필드 수 | 액션 체이닝 | CT Offload | 동적 규칙 추가 | 규칙 삽입 속도 |
|---|---|---|---|---|---|---|
| mlx5e | 64M+ (EM+TCAM) | 200+ | 다단계 goto chain | HW CT+NAT | O (무중단) | ~100K rules/sec |
| ice | 16K (TCAM) + DDP 확장 | ~40 | 단일 액션 | 제한적 | O | ~10K rules/sec |
| bnxt_en | 8K EM + 2K TCAM | ~30 | TruFlow 체이닝 | HW CT | O | ~20K rules/sec |
| cxgb4 | 2K filter entries | ~15 | 단일 액션 | 미지원 | O | ~5K rules/sec |
| ena | 없음 (SW만) | - | - | - | - | - |
| r8169 | 없음 | - | - | - | - | - |
왜 mlx5가 이렇게 큰 용량을 제공하는가?
- RDMA 유산: InfiniBand에서 수백만 QP(Queue Pair)를 관리하던 하드웨어 테이블 인프라를 Ethernet steering에 재활용(Recycling). 다른 NIC 벤더는 이런 대규모 테이블 엔진을 보유하고 있지 않습니다.
- EM + TCAM 이중 구조: Exact Match 테이블(해시 기반, 대용량)과 TCAM(ternary match, 와일드카드)을 병행하여, 정확한 플로우는 EM에서 O(1)로 검색하고 서브넷/마스크 매칭은 TCAM에서 처리합니다.
- Flow Table 계층(goto chain): 여러 Flow Table을
goto chain으로 연결하여 파이프라인을 구성할 수 있습니다. 이는 OVS offload에서 복잡한 다중 매칭 파이프라인을 단일 HW 경로로 구현하는 핵심입니다.
# mlx5 Flow Steering 용량 확인
devlink resource show pci/0000:01:00.0
# 현재 HW offloaded flow 수 확인
tc -s filter show dev enp1s0f0np0 ingress | grep -c "in_hw"
# OVS HW offload 시 flow 통계
ovs-appctl dpctl/dump-flows type=offloaded | wc -l
# 비교: ice 드라이버의 제한적 steering
# ice는 tc flower 지원하지만, 16K entries 이후 SW fallback
ethtool --show-priv-flags enp2s0f0 | grep flow-director
강점 2: XDP/AF_XDP — 최고 성능과 가장 완전한 구현
mlx5e의 XDP 구현은 커널 커뮤니티에서 참조 구현(reference implementation)으로 간주됩니다. MPWQE(Multi-Packet Work Queue Entry) 기반 Striding RQ가 핵심 차별점으로, 하나의 WQE에 여러 패킷을 배치하여 descriptor 오버헤드를 최소화합니다.
| 기능 | mlx5e | ice | bnxt_en | i40e | virtio_net | r8169 |
|---|---|---|---|---|---|---|
| XDP Native | ✓ (모든 CX-4+) | ✓ | ✓ | ✓ | ✓ | ✗ |
| AF_XDP Zero-Copy | ✓ (CX-5+) | ✓ | ✗ | ✓ | ✗ | ✗ |
| XDP Multi-buffer | ✓ (CX-6 Dx+) | ✓ | ✓ | ✗ | ✓ | ✗ |
| XDP_REDIRECT page ref | ✓ (page_pool) | ✓ | ✓ | ✗ (copy) | ✗ | - |
| XDP_DROP 성능 (1코어) | ~35 Mpps | ~25 Mpps | ~20 Mpps | ~18 Mpps | ~5 Mpps | - |
| AF_XDP RX ZC (1코어) | ~22 Mpps | ~16 Mpps | - | ~12 Mpps | - | - |
mlx5 XDP 성능의 비밀: MPWQE Striding RQ
- 일반 NIC: 1 descriptor = 1 패킷. 35 Mpps에서 초당 35M 회 descriptor 갱신 → PCIe/CPU 오버헤드 병목
- mlx5 MPWQE: 1 WQE = 1 대형 페이지(64KB), 여러 패킷이 stride 방식으로 배치. descriptor 갱신 빈도가 수십 배 감소, page_pool 즉시 재활용
- Inline WQE: 소형 패킷은 WQE 안에 페이로드(Payload)를 직접 삽입하여 DMA 왕복 제거. XDP_TX에서 추가 속도 이점
# mlx5 Striding RQ 모드 확인
ethtool --show-priv-flags enp1s0f0np0 | grep striding
# rx_striding_rq: on ← MPWQE 활성화
# XDP 성능 벤치마크 (mlx5 vs ice)
ethtool -G enp1s0f0np0 rx 8192
ethtool -C enp1s0f0np0 rx-usecs 0
xdp-bench drop enp1s0f0np0 -m native # mlx5: ~35 Mpps
xdp-bench drop enp2s0f0 -m native # ice: ~25 Mpps
# AF_XDP 제로카피 비교
xsk_fwd -i enp1s0f0np0 -q 0 -z -p # mlx5: ~22 Mpps
xsk_fwd -i enp2s0f0 -q 0 -z -p # ice: ~16 Mpps
# bpftrace: XDP 경로 지연 측정
bpftrace -e 'kprobe:mlx5e_xdp_handle { @start[tid] = nsecs; }
kretprobe:mlx5e_xdp_handle /@start[tid]/ {
@ns = hist(nsecs - @start[tid]); delete(@start[tid]);
}'
강점 3: 오프로드 범위 — 유일한 Full-Stack HW Offload
mlx5e는 커널 네트워킹 스택의 거의 모든 기능을 하드웨어로 오프로드할 수 있는 유일한 드라이버입니다. TC flower + Connection Tracking + NAT + 터널(VXLAN/GRE/Geneve) + IPsec + kTLS + MACsec를 동시에 하드웨어에서 처리합니다.
| 오프로드 기능 | mlx5e | ice | bnxt_en | cxgb4 | ena |
|---|---|---|---|---|---|
| TC flower (skip_sw) | ✓ 200+ 필드 | ✓ ~40 필드 | ✓ ~30 필드 | ✓ ~15 필드 | ✗ |
| Connection Tracking | ✓ HW CT+NAT | 제한적 | ✓ HW CT | ✗ | ✗ |
| VXLAN/GRE/Geneve Encap | ✓ HW encap/decap | ✓ | ✓ | ✗ | ✗ |
| IPsec Inline | ✓ (CX-6+) | ✓ (E810) | ✗ | ✓ (T6) | ✗ |
| kTLS TX/RX Inline | ✓ (CX-6 Dx+) | ✗ | ✓ | ✓ (T6) | ✗ |
| MACsec | ✓ (CX-7) | ✗ | ✗ | ✗ | ✗ |
| OVS HW Offload | ✓ (mature) | ✓ (제한적) | ✓ | ✗ | ✗ |
| HW Timestamping (PTP) | ✓ (ns 정밀도) | ✓ (ns 정밀도) | ✓ | ✓ | ✗ |
| Header Rewrite | ✓ (MAC/IP/port) | 제한적 | ✓ | ✗ | ✗ |
| Packet Sampling (sFlow) | ✓ (CX-7) | ✗ | ✗ | ✗ | ✗ |
클라우드 네이티브 환경에서 패킷은 여러 처리 단계를 거친다: OVS/eBPF 매칭 → VXLAN 캡슐화 → Connection Tracking → NAT 변환 → IPsec 암호화. mlx5는 이 전체 파이프라인을 HW에서 처리하여 CPU를 완전히 해방합니다. 다른 드라이버는 일부 단계에서 SW fallback이 발생하여 CPU 오버헤드가 급증합니다.
강점 4: 가상화 — SR-IOV + Scalable Functions
mlx5는 전통적 SR-IOV(1024 VFs)에 더해 Scalable Functions(SF)라는 고유한 기능을 제공합니다. SF는 VF와 달리 PCIe function을 소비하지 않으므로, 하드웨어 PCIe 한계(통상 256 functions)를 초월하여 수천 개의 독립 네트워크 기능을 생성할 수 있습니다.
| 특성 | mlx5 VF | mlx5 SF | ice VF | bnxt VF | ena (PV) |
|---|---|---|---|---|---|
| 최대 수 | 1024 | 제한 없음 (수천) | 256 | 256 | N/A (PV) |
| PCIe function 소비 | 1 per VF | 0 (소프트웨어) | 1 per VF | 1 per VF | N/A |
| 독립 netdev | ✓ | ✓ | ✓ | ✓ | ✓ |
| 독립 RDMA dev | ✓ | ✓ | ✗ | ✗ | ✗ |
| eSwitch representor | ✓ (Switchdev) | ✓ (Switchdev) | ✓ | ✓ | ✗ |
| 독립 devlink 인스턴스 | ✗ | ✓ | ✗ | ✗ | ✗ |
| 동적 생성/삭제 | 재부팅 필요 | ✓ (무중단) | 재부팅 필요 | 재부팅 필요 | N/A |
# SR-IOV VF 생성
echo 16 > /sys/bus/pci/devices/0000:01:00.0/sriov_numvfs
# Scalable Function 생성 (mlx5 전용, CX-6 Dx+)
devlink port add pci/0000:01:00.0 flavour pcisf pfnum 0 sfnum 88
devlink port function set pci/0000:01:00.0/32768 \
hw_addr 00:11:22:33:44:55 state active
# SF의 독립 devlink 인스턴스 확인
devlink dev show
# pci/0000:01:00.0 ← PF
# auxiliary/mlx5_core.sf.4 ← SF 독립 인스턴스
# 비교: ice VF 생성 (최대 256, SF 미지원)
echo 256 > /sys/bus/pci/devices/0000:02:00.0/sriov_numvfs
강점 5: RDMA/GPUDirect — AI/HPC 독점적 지위
AI 훈련과 HPC 워크로드에서 mlx5는 사실상 유일한 선택지입니다. RoCEv2, InfiniBand, GPUDirect RDMA, GPUDirect Storage, On-Demand Paging(ODP)을 모두 단일 NIC에서 제공하는 드라이버는 mlx5뿐입니다.
| RDMA 기능 | mlx5 (RoCEv2/IB) | bnxt (RoCEv2) | ice/irdma | cxgb4 (iWARP) |
|---|---|---|---|---|
| 프로토콜 | RoCEv2 + InfiniBand | RoCEv2만 | iWARP + RoCEv2 | iWARP만 |
| 최대 대역폭 | 400 Gbps (NDR) | 200 Gbps | 100 Gbps | 100 Gbps |
| GPUDirect RDMA | ✓ (모든 GPU) | ✗ | ✗ | ✗ |
| GPUDirect Storage | ✓ (CX-6+) | ✗ | ✗ | ✗ |
| ODP (On-Demand Paging) | ✓ | ✗ | ✗ | ✗ |
| Adaptive Routing | ✓ (CX-7) | ✗ | ✗ | ✗ |
| NCCL SHARP 가속 | ✓ (IB 전용) | ✗ | ✗ | ✗ |
| Multi-QP 동시 사용 | 수백만 QP | 수만 QP | 수천 QP | 수천 QP |
# GPUDirect RDMA 환경 확인
nvidia-smi topo -m
cat /proc/driver/nvidia-peermem/version
# GPUDirect RDMA 대역폭 테스트
ib_write_bw -d mlx5_0 --use_cuda=0 -x 3 --report_gbits
# NCCL all-reduce 벤치마크 (multi-GPU, SHARP 가속)
nccl-tests/build/all_reduce_perf -b 1M -e 1G -g 8
강점 6: 관측성 및 관리 — 업계 최고 devlink 통합
mlx5 드라이버는 커널 devlink 프레임워크의 최초이자 가장 완전한 구현체입니다. devlink 프레임워크 자체가 mlx5 요구사항에 맞춰 설계되었으며, 이후 다른 드라이버들이 점진적으로 채택하고 있습니다.
| devlink 기능 | mlx5 | ice | bnxt_en | ena | r8169 |
|---|---|---|---|---|---|
| Health Reporters | 5개 (fw, fw_fatal, tx, rx, vnic) | 2개 | 3개 | ✗ | ✗ |
| FW Live Reset | ✓ (CX-6+) | ✗ | ✗ | ✗ | ✗ |
| Resource Monitoring | ✓ (flow tables, counters) | 제한적 | ✓ | ✗ | ✗ |
| Port Flavours (SF) | ✓ | ✗ | ✗ | ✗ | ✗ |
| Rate Limiting (per-VF/SF) | ✓ (tx_share, tx_max) | ✗ | ✗ | ✗ | ✗ |
| Trap (drop 원인 추적) | ✓ | ✗ | ✗ | ✗ | ✗ |
# 패킷 드롭 원인 추적 (mlx5 전용 trap)
devlink trap show pci/0000:01:00.0
devlink trap set pci/0000:01:00.0 trap source_mac_is_multicast action trap
# SF별 rate limiting (QoS)
devlink port function rate add pci/0000:01:00.0/sf_group \
tx_share 1gbit tx_max 10gbit
# FW live reset (재부팅 불필요, CX-6+)
devlink dev reload pci/0000:01:00.0 action fw_activate
devlink dev reload action fw_activate는
재부팅 없이 FW를 활성화하여 다운타임을 수 초로 단축합니다. 수천 대 서버 환경에서
운영 비용의 근본적 차이를 만듭니다. 현재 이 기능을 지원하는 NIC 드라이버는 mlx5뿐입니다.
종합 비교: 드라이버별 강약점 매트릭스
| 드라이버 | Flow Steering | XDP/AF_XDP | HW Offload | 가상화 | RDMA | 관측성 | 종합 |
|---|---|---|---|---|---|---|---|
| mlx5e | ★★★★★ | ★★★★★ | ★★★★★ | ★★★★★ | ★★★★★ | ★★★★★ | 30/30 |
| ice | ★★★☆☆ | ★★★★☆ | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ | 19/30 |
| bnxt_en | ★★★☆☆ | ★★★☆☆ | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ | ★★★☆☆ | 18/30 |
| cxgb4 | ★★☆☆☆ | ★☆☆☆☆ | ★★★★☆ | ★★☆☆☆ | ★★☆☆☆ | ★★☆☆☆ | 13/30 |
| ena | ★☆☆☆☆ | ★☆☆☆☆ | ★☆☆☆☆ | ★★★★☆ | ★☆☆☆☆ | ★★☆☆☆ | 10/30 |
| gve | ★☆☆☆☆ | ★☆☆☆☆ | ★☆☆☆☆ | ★★★★☆ | ★☆☆☆☆ | ★★☆☆☆ | 10/30 |
| virtio_net | ★☆☆☆☆ | ★★☆☆☆ | ★☆☆☆☆ | ★★★★★ | ★☆☆☆☆ | ★☆☆☆☆ | 10/30 |
| r8169 | ★☆☆☆☆ | ★☆☆☆☆ | ★☆☆☆☆ | ★☆☆☆☆ | ★☆☆☆☆ | ★☆☆☆☆ | 6/30 |
워크로드별 최적 드라이버 선택 가이드
| 워크로드 | 최적 드라이버 | mlx5 선택 이유 | 현실적 대안 |
|---|---|---|---|
| AI/HPC 훈련 | mlx5 (필수) | GPUDirect RDMA, SHARP, IB/RoCE | 대안 없음 |
| K8s 대규모 네트워크 정책 | mlx5 (권장) | 64M+ flow rules, OVS HW offload | bnxt (10K 미만) |
| NFV/VNF 가속 | mlx5 | SF + eSwitch + CT offload | ice (소규모), bnxt |
| TLS 프록시/LB | mlx5 / cxgb4 | kTLS inline, CPU 해방 | bnxt (kTLS 부분 지원) |
| XDP 패킷 필터링 | mlx5 | 35 Mpps, AF_XDP ZC | ice (25 Mpps) |
| AWS EC2 | ena (필수) | - | ENA Express/SRD |
| GCP | gve (필수) | - | DQO 모드 |
| 소비자/데스크탑 | r8169 | 과잉 스펙 | r8169, atlantic |
# mlx5 종합 프로파일링
devlink dev info pci/0000:01:00.0 # HW/FW 정보
ethtool -k enp1s0f0np0 | grep -E "tc-hw|ntuple|rx-gro-hw"
ethtool -S enp1s0f0np0 | head -50 # 성능 카운터
devlink health show pci/0000:01:00.0 # Health 상태
devlink resource show pci/0000:01:00.0 # 리소스 사용률
# bpftrace: mlx5e NAPI poll 효율성
bpftrace -e 'kprobe:mlx5e_napi_poll { @polls = count(); }
kretprobe:mlx5e_napi_poll /retval > 0/ { @pkts = hist(retval); }
interval:s:5 { print(@polls); print(@pkts); clear(@polls); clear(@pkts); }'
mlx4 레거시 드라이버 및 mlx5 마이그레이션
mlx4_core/mlx4_en은 ConnectX-2, ConnectX-3, ConnectX-3 Pro를 지원하는 레거시 드라이버입니다. mlx5와 비교하여 스티어링 유연성, SR-IOV 확장성, offload 기능에서 상당한 차이가 있습니다. ConnectX-3 Pro는 mlx4와 mlx5 모두에서 부분적으로 지원되지만, mlx4가 권장됩니다.
mlx4에서 mlx5로의 마이그레이션은 하드웨어 교체(ConnectX-3 → ConnectX-5/6/7)를 수반합니다. 소프트웨어 관점에서 주요 변경 사항과 마이그레이션 절차는 다음과 같습니다.
| 항목 | mlx4 | mlx5 | 마이그레이션 시 주의 |
|---|---|---|---|
| 커널 모듈 | mlx4_core, mlx4_en, mlx4_ib | mlx5_core (통합) | modprobe 설정 변경 |
| 인터페이스 명명 | eth0, eth1 (legacy) | enp1s0f0np0 (predictable) | 네트워크 설정 파일 수정 |
| SR-IOV 설정 | mlx4_core num_vfs 모듈 파라미터 | sysfs sriov_numvfs | 부팅 스크립트 변경 |
| eSwitch 모드 | Legacy만 지원 | Legacy + Switchdev | OVS 환경은 Switchdev 권장 |
| QoS 설정 | mlnx_qos (OFED) | mlnx_qos + tc + devlink | TC-based QoS로 전환 권장 |
| FW 관리 | mlxfwmanager (OFED) | devlink dev flash (in-tree) | OFED 도구 의존성 제거 가능 |
| RDMA | IB / RoCEv1 | IB / RoCEv1 / RoCEv2 | RoCEv2 전환 시 ECN/PFC 설정 |
| XDP | Native XDP (기본) | Native + AF_XDP + multi-buf | AF_XDP 앱 활용 가능 |
# mlx4 → mlx5 마이그레이션 전 현재 설정 백업
ethtool -i eth0 # 드라이버 확인 (mlx4_en)
mlxconfig -d /dev/mst/mt4099_pciconf0 query > mlx4_config_backup.txt
ip addr show eth0 > mlx4_ip_backup.txt
# 하드웨어 교체 후 mlx5 드라이버 확인
lspci -d 15b3: -k # mlx5_core가 로드되어야 함
ethtool -i enp1s0f0np0 # driver: mlx5_core
# SR-IOV 마이그레이션 (mlx4 → mlx5 방식)
# 기존 mlx4: modprobe mlx4_core num_vfs=4,0 port_type_array=2,2
# 새 mlx5:
echo 4 > /sys/class/net/enp1s0f0np0/device/sriov_numvfs
# Switchdev 모드 활용 (mlx4에서 불가했던 기능)
devlink dev eswitch set pci/0000:01:00.0 mode switchdev
# 기존 mlx4 NIC 관련 모듈 정리
echo "blacklist mlx4_core" >> /etc/modprobe.d/blacklist-mlx4.conf
echo "blacklist mlx4_en" >> /etc/modprobe.d/blacklist-mlx4.conf
mlx4 지원 상태: mlx4 드라이버는 커널에서 유지보수 모드(maintenance mode)로 관리되며, 새로운 기능은 추가되지 않습니다. 보안 패치(Patch)와 심각한 버그 수정만 적용됩니다. ConnectX-3 Pro 이하 하드웨어를 사용 중이라면, 가능한 빨리 ConnectX-5 이상으로 업그레이드하여 mlx5 드라이버의 활발한 개발 혜택을 받는 것이 권장됩니다. 특히 커널 6.x 이후로 mlx5에 추가된 기능(SF, CT offload, inline crypto 등)은 mlx4에서 사용할 수 없습니다.
ConnectX-3 CI vs CQ 모델
mlx4는 완료 처리를 위해 두 가지 모델을 제공합니다. CI(Completion Index) 모드는 드라이버가 CQ를 폴링하지 않고 완료 인덱스를 직접 쿼리하여 처리하는 방식으로, TX 경로의 저지연에 유리합니다. 반면 CQ(Completion Queue) 폴링 모드는 NAPI 루프 안에서 CQE 배치를 한꺼번에 처리하므로 높은 처리량(throughput) 환경에 적합합니다. CI 모드는 코어당 처리 패킷이 적을 때 캐시 압력을 줄이고, CQ 폴링 모드는 패킷이 몰릴 때 배치 효율로 CPU 사이클을 절약합니다.
ConnectX-3는 IB 포트와 Ethernet 포트를 동일 HCA 위에서 멀티플렉싱합니다.
mlx4_core는 부팅 시 포트 타입(IB vs Ethernet)을 port_type_array
모듈 파라미터로 설정하며, 각 포트는 독립적인 CQ/QP 자원 풀을 갖습니다.
두 포트가 공유하는 HCA 리소스(EQ, MPT 등)는 mlx4_core가 중재합니다.
QP 리소스 한계: mlx4는 포트당 최대 QP 및 CQ 수가 하드웨어에 고정되어 있으며,
SR-IOV 환경에서는 VF 수 제한이 64로 고정됩니다(SRIOV_MAX_FUNCS = 64).
이 제한은 mlx4_core 내부 리소스 파티셔닝 테이블이 64-entry로 설계된 데서 기인합니다.
ConnectX-4 이상(mlx5)에서는 이 한계가 1024 VF으로 확장되었습니다.
mlx4 운영 가이드
ConnectX-3 환경에서 중요한 모니터링 지표와 커널 호환성은 다음과 같습니다.
| 카운터/지표 | 확인 방법 | 임계값 기준 |
|---|---|---|
| TX/RX 패킷 드롭 | ethtool -S eth0 | grep drop | 0이 정상; 증가 시 링 버퍼 확대 검토 |
| PCI 오류 | dmesg | grep mlx4 | AER 오류 발생 시 슬롯 교체 고려 |
| FW Health | mlx4 장치 로그 조회 | syndrome 코드 0x0이 정상 |
| QP/CQ 고갈 | rdma stat show | free QP 0에 근접 시 VF 축소 필요 |
| 온도 / 전력 | mlxconfig -d /dev/mst/... query | 규격 최대치 90% 이하 유지 |
커널 호환성: mlx4 드라이버는 커널 4.x ~ 6.x에서 모두 컴파일되지만, 커널 5.15 이후로는 신규 기능 추가 없이 보안 패치와 버그 수정만 이루어지는 유지보수 모드(maintenance mode)다. 커널 6.8부터는 일부 레거시 compat 코드가 제거되었으므로, ConnectX-3 Pro 사용 환경에서는 커널 버전 업그레이드 전 OFED 호환성을 반드시 확인해야 합니다. 권장 마이그레이션 시점: 커널 6.x 이상 + ConnectX-3 조합은 ConnectX-5/6으로의 하드웨어 업그레이드가 강력히 권장됩니다.
BlueField DPU와 mlx5
NVIDIA BlueField DPU(Data Processing Unit)는 mlx5 NIC에 ARM 코어 클러스터를 내장하여 네트워크 처리를 호스트 CPU에서 완전히 분리(offload)하는 아키텍처입니다.
| 모델 | ARM 코어 | NIC 속도 | 주요 오프로드 | 호스트 인터페이스 |
|---|---|---|---|---|
| BF1 (BlueField-1) | Cortex-A72 × 16 | 25GbE × 2 | OVS 기본, IPsec | PCIe 3.0 × 8 |
| BF2 (BlueField-2) | Cortex-A72 × 8 | 100GbE × 2 | OVS, CT, crypto, DOCA | PCIe 4.0 × 16 |
| BF3 (BlueField-3) | Cortex-A78AE × 16 | 400GbE × 2 | OVS, AI inference, Storage | PCIe 5.0 × 16 |
호스트에서 BlueField는 두 가지 netdev로 나타납니다: 호스트 representor(호스트 측 트래픽을 DPU ARM에서 제어)와 물리 포트 representor(외부 네트워크 포트). DPU ARM 위에서 OVS, eBPF, connection tracking이 실행되며, 호스트 CPU는 패킷 처리 부담에서 해방됩니다.
ConnectX-3(mlx4) → ConnectX-4+(mlx5) 마이그레이션 시 주요 아키텍처 차이:
mlx5는 단일 mlx5_core 모듈이 Ethernet, RDMA, vDPA를 모두 담당하는 통합 구조이며,
eSwitch, Scalable Function(SF), devlink health 등이 mlx4에는 없는 핵심 기능입니다.
DPU 오프로드 기능(DOCA SDK)은 mlx5 기반 BlueField에서만 사용 가능합니다.
Broadcom: bnxt_en / bnx2x / tg3
Broadcom은 서버 및 데이터센터 시장에서 가장 광범위한 NIC 포트폴리오를 보유하고 있습니다. 최신 Thor2(BCM57608) 시리즈부터 레거시 Tigon3까지, 세 가지 주요 드라이버가 리눅스 커널에서 관리됩니다.
| 드라이버 | 칩셋 시리즈 | 최대 속도 | NAPI | XDP | SR-IOV | TruFlow | 소스 경로 |
|---|---|---|---|---|---|---|---|
bnxt_en |
BCM573xx / BCM574xx / BCM57608 (Thor2) | 400 GbE | O | O | O (256 VFs) | O | drivers/net/ethernet/broadcom/bnxt/ |
bnx2x |
BCM57710 / BCM57711 / BCM57810 | 10 GbE | O | X | O (64 VFs) | X | drivers/net/ethernet/broadcom/bnx2x/ |
tg3 |
BCM5700 / BCM5719 / BCM5720 | 1 GbE | O | X | X | X | drivers/net/ethernet/broadcom/tg3.* |
bnxt_en 아키텍처: HWRM과 링 모델
bnxt_en 드라이버의 핵심 설계 원칙은 HWRM(Hardware Resource Manager) 펌웨어 인터페이스입니다. 호스트 드라이버는 하드웨어 레지스터를 직접 조작하지 않고, HWRM 명령 채널을 통해 모든 설정과 리소스 관리를 수행합니다. 이 구조는 펌웨어 업데이트만으로 새로운 기능을 추가할 수 있는 유연성을 제공합니다.
HWRM 통신 메커니즘: 호스트는 공유 메모리 영역에 HWRM 요청 메시지를 작성하고 도어벨 레지스터를 울립니다. 펌웨어가 요청을 처리한 후 응답을 같은 영역에 기록하고 인터럽트로 완료를 알립니다. 기본 타임아웃은 500ms이며, HWRM_FUNC_QCFG 같은 복잡한 명령은 최대 5초까지 대기합니다.
링(Ring) 모델은 bnxt_en의 데이터 경로 핵심입니다. 4가지 링 타입이 서로 연동하여 패킷 송수신을 처리합니다.
| HWRM 명령 카테고리 | 주요 명령 | 용도 |
|---|---|---|
| Function | FUNC_QCFG, FUNC_CFG, FUNC_RESET |
PF/VF 설정 조회 및 변경, 펑션 리셋 |
| Ring | RING_ALLOC, RING_FREE, RING_GRP_ALLOC |
TX/RX/CP/AGG 링 할당 및 그룹화 |
| VNIC | VNIC_ALLOC, VNIC_CFG, VNIC_RSS_CFG |
가상 NIC 설정, RSS 해시 키 및 인디렉션 테이블 |
| Port | PORT_PHY_QCFG, PORT_PHY_CFG |
PHY 상태 조회, 속도/FEC/AN 설정 |
| Stat | STAT_CTX_ALLOC, STAT_CTX_QUERY |
통계 컨텍스트 할당 및 카운터 조회 |
| CFA | CFA_FLOW_ALLOC, CFA_FLOW_FREE |
TruFlow 하드웨어 오프로드 플로우 규칙 |
| Queue | QUEUE_QPORTCFG, QUEUE_CFG |
QoS 큐 설정, CoS 매핑 |
링 크기 튜닝: 고처리량 워크로드에서는 ethtool -G eth0 rx 4096 tx 4096로 링 크기를 늘립니다. Completion 링은 자동으로 rx + tx의 2배로 조정됩니다. AGG 링은 MTU가 PAGE_SIZE를 초과할 때만 활성화되며, rx_jumbo 파라미터로 크기를 별도 조절합니다.
# bnxt_en 링 및 채널 설정 확인
ethtool -g enp3s0f0
ethtool -l enp3s0f0
# 링 크기 최적화 (고처리량 서버)
ethtool -G enp3s0f0 rx 4096 tx 4096
# 결합 채널 수 조정 (CPU 수에 맞춤)
ethtool -L enp3s0f0 combined 16
# HWRM 버전 및 펌웨어 정보 확인
ethtool -i enp3s0f0
# firmware-version: 228.1.105.2 (HWRM 1.10.2.128)
# 인터럽트 코얼레싱 (적응형)
ethtool -C enp3s0f0 adaptive-rx on adaptive-tx on
# 하드웨어 GRO 활성화
ethtool -K enp3s0f0 rx-gro-hw on
/* bnxt_en HWRM 명령 전송 핵심 구조 */
struct hwrm_req {
__le16 req_type; /* 명령 타입 */
__le16 cmpl_ring; /* 완료 링 ID */
__le16 seq_id; /* 시퀀스 번호 */
__le16 target_id; /* 대상 (PF/VF) */
__le64 resp_addr; /* 응답 DMA 주소 */
};
/* 링 그룹: TX + RX + CP + AGG를 하나로 묶음 */
struct bnxt_ring_grp_info {
u16 fw_stats_ctx; /* 통계 컨텍스트 FW ID */
u16 fw_grp_id; /* FW 그룹 ID */
u16 rx_fw_ring_id;
u16 agg_fw_ring_id;
u16 cp_fw_ring_id;
};
HWRM Timeout 감지 및 복구
bnxt_hwrm_do_send_msg()는 HWRM 명령을 펌웨어로 전송하고 완료를 기다리는 핵심 함수입니다. 기본 타임아웃은 500ms이며, HWRM_FUNC_QCFG나 링 초기화처럼 복잡한 명령은 최대 10초까지 대기합니다. 타임아웃이 발생하면 드라이버는 펌웨어 사망(FW Fatal)으로 간주하고 복구 절차를 시작합니다.
HWRM Timeout과 라이브 마이그레이션: VM 라이브 마이그레이션(vMotion/live-migrate) 도중 하이퍼바이저(Hypervisor)가 vCPU를 일시 정지하면, VF의 HWRM polling 루프도 멈춥니다. 마이그레이션 완료 후 재개 시점에 이미 타임아웃이 경과하여 VF 드라이버가 FW 오류를 보고할 수 있습니다. 완화책: HWRM_FUNC_CFG의 async_event_completion_timeout을 충분히 크게 설정하거나, 하이퍼바이저 측 VFIO 드라이버가 마이그레이션 전에 VF 큐를 정지하도록 구성합니다.
PF 충돌 → VF 연쇄 영향: PF 드라이버의 HWRM 채널이 실패하면, 모든 VF는 HWRM 프록시를 잃습니다. VF는 다음 HWRM 요청에서 타임아웃을 만나고, 이를 FLR(Function Level Reset) 트리거로 처리합니다. FLR 이후 VF 드라이버는 HWRM_FUNC_VF_CFG를 다시 발행하여 채널을 재확립합니다.
PF HWRM 채널 실패
│
├─ PF: bnxt_fw_reset() 호출
│ └─ bnxt_fw_fatal_reporter 활성화
│
└─ VF들: 다음 HWRM 요청 → 타임아웃(500ms)
└─ bnxt_vf_hwrm_timeout_notify()
├─ netif_carrier_off()
├─ FLR 요청 (PCIe FLR)
└─ 채널 재확립 (HWRM_FUNC_VF_CFG)
HWRM 명령 트레이싱
HWRM 명령 흐름을 추적하는 방법은 devlink health reporter, ftrace, bpftrace 세 가지가 있습니다. devlink reporter는 FW 이상 상태를 진단하고, ftrace/bpftrace는 정상 동작 중 지연을 측정하는 데 사용됩니다.
# devlink health reporter: FW 상태 확인
devlink health show pci/0000:03:00.0
# reporter: bnxt_fw_reporter — FW 링크 이벤트 추적
# reporter: bnxt_fw_fatal_reporter — FW 크래시 덤프
devlink health diagnose pci/0000:03:00.0 reporter bnxt_fw_reporter
devlink health dump show pci/0000:03:00.0 reporter bnxt_fw_fatal_reporter
# ftrace: HWRM send/receive 함수 추적
echo 'bnxt_hwrm_do_send_msg' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 작업 수행 후
cat /sys/kernel/debug/tracing/trace | grep bnxt_hwrm | head -20
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo nop > /sys/kernel/debug/tracing/current_tracer
# bpftrace: HWRM 명령 레이턴시 히스토그램
bpftrace -e '
kprobe:bnxt_hwrm_do_send_msg { @start[tid] = nsecs; }
kretprobe:bnxt_hwrm_do_send_msg
/@start[tid]/
{
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
END { print(@latency_us); }'
# HWRM 타임아웃 카운터 확인 (ethtool 통계)
ethtool -S enp3s0f0 | grep -i hwrm
# hwrm_req_timeout: 타임아웃 발생 횟수
# hwrm_resp_err: 응답 오류 횟수
# 현재 HWRM 타임아웃 설정 확인
ethtool -i enp3s0f0
# 모듈 파라미터로 타임아웃 조정 (ms 단위)
modprobe bnxt_en hwrm_min_timeout=1000
bnxt_en TruFlow 오프로드 엔진
TruFlow는 Broadcom Thor/Thor2 NIC에 내장된 프로그래머블 패킷 처리 파이프라인입니다. TC flower 규칙을 하드웨어로 직접 오프로드하여 호스트 CPU 부하를 제거하고, 수백만 개의 플로우 규칙을 와이어 스피드로 처리합니다.
# TC flower를 이용한 TruFlow 오프로드 예시
# switchdev 모드 활성화 (TruFlow 필수 전제)
devlink dev eswitch set pci/0000:03:00.0 mode switchdev
# ingress qdisc 추가
tc qdisc add dev enp3s0f0 ingress
# VXLAN 터널 트래픽을 VF representor로 포워드
tc filter add dev enp3s0f0 protocol ip parent ffff: \
flower enc_dst_ip 10.0.0.1 enc_key_id 100 enc_dst_port 4789 \
action tunnel_key unset \
action mirred egress redirect dev enp3s0f0_0
# NAT 오프로드 (SNAT)
tc filter add dev enp3s0f0 protocol ip parent ffff: \
flower ip_proto tcp src_ip 192.168.1.0/24 \
action pedit ex munge ip src set 10.0.0.1 \
action csum ip4h tcp \
action mirred egress redirect dev enp3s0f0
# 오프로드된 플로우 통계 확인
tc -s filter show dev enp3s0f0 ingress
# TruFlow 테이블 사용량 확인
devlink resource show pci/0000:03:00.0
TruFlow 용량: Thor2 칩셋은 EM(Exact Match) 테이블에 최대 400만 엔트리, TCAM에 최대 16K 와일드카드 규칙을 지원합니다. OVS-DPDK 환경에서는 CT(Connection Tracking) 오프로드를 통해 커넥션당 양방향 플로우를 자동 설치합니다.
TruFlow TCAM 프래그먼테이션
TCAM은 와일드카드 규칙을 우선순위 순서로 저장합니다. 규칙 추가/삭제가 반복되면 우선순위 홀(priority hole)이 발생하여 실제 사용 가능한 용량이 표시 용량보다 줄어드는 프래그먼테이션 현상이 일어납니다.
프래그먼테이션 발생 메커니즘: 우선순위 1000의 규칙을 삽입하면 기존 규칙들이 물리적으로 재배치됩니다. 규칙 삭제 시 해당 슬롯은 빈 상태로 남고, 새 규칙 삽입 시 우선순위 위치가 맞지 않으면 그 슬롯을 재사용하지 못합니다. 이 과정이 반복되면 TCAM의 논리 용량이 줄어듭니다.
Defragmentation: 펌웨어는 TCAM 사용률이 임계값(기본 80%)을 초과하면 자동으로 defrag를 시작합니다. Defrag 중에는 새 규칙 설치가 최대 수십 ms 지연될 수 있으므로, 고속 플로우 설치 환경에서는 주의가 필요합니다.
# TCAM/EM 리소스 사용량 모니터링
devlink resource show pci/0000:03:00.0
# 예시 출력:
# resource TCAM: size 16384 unit entry occ 12800 -> 78%
# resource EM: size 4194304 unit entry occ 980000 -> 23%
# TC flower 현재 오프로드 규칙 수 확인
tc filter show dev enp3s0f0 ingress | grep -c 'handle'
# 오프로드 성공/실패 구분 (in_hw 플래그)
tc filter show dev enp3s0f0 ingress | grep -A3 'flower'
# in_hw: HW 오프로드 성공 / not_in_hw: 소프트웨어 폴백
TruFlow 용량 계획 가이드
EM 해시 충돌: Exact Match 테이블은 해시 기반이므로 생일 역설(birthday paradox)에 따른 충돌이 발생합니다. 테이블 크기가 N이고 k개의 엔트리가 채워졌을 때, 예상 충돌 확률은 약 k²/(2N)입니다. Thor2의 4M EM 테이블에서 100만 엔트리 사용 시 예상 충돌률은 약 12%입니다. 충돌 시 엔트리는 소프트웨어로 폴백됩니다.
TCAM vs EM 결정 기준:
- 와일드카드 규칙 (prefix match, mask 포함) → TCAM 사용
- 5-tuple 정확 매칭 (src_ip/dst_ip/src_port/dst_port/proto 고정) → EM 사용
- 혼합 시나리오 → 계층적 룩업: TCAM 우선, 미스 시 EM
CT 엔트리 추정 공식:
필요 EM 엔트리 = 동시 연결 수 × 2 (양방향) × 1.1 (10% 헤드룸)
예시: 동시 연결 50만 개
= 500,000 × 2 × 1.1 = 1,100,000 엔트리 필요
Thor2 4M EM: 충분 (사용률 26%)
Thor 1M EM: 부족 → TCAM 보조 또는 소프트웨어 폴백
| 칩셋 | SKU | EM 용량 | TCAM 용량 | CT 최대 연결 (권장) |
|---|---|---|---|---|
| Thor (BCM57508) | 100G × 2 | 1M 엔트리 | 8K 와일드카드 | ~400K 동시 연결 |
| Thor (BCM57504) | 25G × 4 | 1M 엔트리 | 8K 와일드카드 | ~400K 동시 연결 |
| Thor2 (BCM57608) | 100G × 2 | 4M 엔트리 | 16K 와일드카드 | ~1.6M 동시 연결 |
| Thor2 (BCM57604) | 25G × 4 | 4M 엔트리 | 16K 와일드카드 | ~1.6M 동시 연결 |
# 현재 EM 사용량 및 헤드룸 계산
EM_MAX=$(devlink resource show pci/0000:03:00.0 | awk '/EM/{print $4}')
EM_OCC=$(devlink resource show pci/0000:03:00.0 | awk '/EM/{print $6}')
echo "EM 사용률: $((EM_OCC * 100 / EM_MAX))%"
echo "남은 헤드룸: $((EM_MAX - EM_OCC)) 엔트리"
# CT 오프로드 통계 (conntrack + TruFlow)
conntrack -S | grep -E 'found|insert_failed|drop'
# insert_failed가 증가하면 EM 용량 부족 신호
# TruFlow 오프로드 상태 요약
devlink resource show pci/0000:03:00.0 | grep -E 'TCAM|EM'
bnxt_en SR-IOV 및 devlink 지원
bnxt_en의 SR-IOV 구현은 HWRM을 통한 완전한 VF 리소스 분할을 특징으로 합니다. PF 드라이버가 HWRM 프록시 역할을 하여 VF의 모든 하드웨어 요청을 중개합니다.
# SR-IOV VF 생성
echo 8 > /sys/class/net/enp3s0f0/device/sriov_numvfs
# VF MAC 주소 설정 (보안 강화)
ip link set enp3s0f0 vf 0 mac 00:11:22:33:44:55
ip link set enp3s0f0 vf 0 vlan 100
ip link set enp3s0f0 vf 0 spoofchk on
ip link set enp3s0f0 vf 0 trust on # 필요시 promiscuous 허용
# VF 대역폭 제한 (Mbps)
ip link set enp3s0f0 vf 0 max_tx_rate 1000
ip link set enp3s0f0 vf 0 min_tx_rate 100
# devlink 정보 확인
devlink dev info pci/0000:03:00.0
devlink port show pci/0000:03:00.0
# switchdev 모드 전환 (VF representor 생성)
devlink dev eswitch set pci/0000:03:00.0 mode switchdev
# devlink health reporter
devlink health show pci/0000:03:00.0
devlink health diagnose pci/0000:03:00.0 reporter fw
# 펌웨어 업데이트 (devlink flash)
devlink dev flash pci/0000:03:00.0 file bnxt_fw.pkg
bnx2x 레거시: 57810 NPAR 아키텍처
bnx2x는 BCM57810 시리즈의 10GbE 드라이버로, NPAR(NIC Partitioning) 기능이 핵심 차별점입니다. NPAR은 SR-IOV와 달리 하드웨어 수준에서 물리 포트를 완전히 독립된 가상 NIC으로 분할합니다.
| 비교 항목 | NPAR | SR-IOV |
|---|---|---|
| MAC 주소 | 펌웨어에서 영구 할당 (NVM) | PF 드라이버가 동적 할당 |
| OS에서 보이는 형태 | 독립 PCI Function (PF0~PF3) | VF (PF에 종속) |
| 대역폭 제어 | 하드웨어 MCP가 보장 (min/max) | 소프트웨어 기반 |
| 프로토콜 오프로드 | 파티션별 iSCSI/FCoE 전용 할당 가능 | PF만 오프로드 지원 |
| 장애 격리 | 완전 격리 (PF 리셋 독립) | PF 리셋 시 VF도 영향 |
| 구성 변경 | NVM 수정 후 재부팅 필요 | 런타임 동적 변경 가능 |
bnx2x 알려진 이슈: 커널 6.x에서 bnx2x의 MDC/MDIO 타이밍 관련 PHY 초기화 실패가 간헐적으로 발생합니다. bnx2x.debug=0x20 모듈 파라미터로 PHY 디버그 로그를 활성화할 수 있습니다. 또한 IOMMU가 활성화된 환경에서 DMA 매핑 오류 시 iommu=pt 부트 옵션이 필요할 수 있습니다.
# bnx2x NPAR 설정 확인 (펌웨어 유틸리티 사용)
bnxtnvm -dev=eth0 listcfg
# 현재 대역폭 할당 확인
ethtool -i eth0 | grep firmware
# bnx2x 디버그 레벨 설정
modprobe bnx2x debug=0x20
# 실행 중 디버그 변경
echo 0x20 > /sys/module/bnx2x/parameters/debug
# bnx2x 드라이버 통계
ethtool -S eth0 | grep -E 'rx_|tx_|brb_|pfc_'
bnx2x NPAR 운영
NPAR은 단순한 포트 분할을 넘어 세밀한 대역폭 보장과 프로토콜 오프로드 분리를 제공합니다. 실제 운영 환경에서는 파티션별 최소/최대 대역폭 설정과 SR-IOV 조합이 핵심 사용 패턴입니다.
대역폭 할당 예시 (4 파티션, 10GbE 포트):
| 파티션 | 용도 | 최소 BW | 최대 BW | 보장 대역폭 |
|---|---|---|---|---|
| NPAR 0 (eth0) | 일반 데이터 트래픽 | 25% (2.5G) | 100% (10G) | 항상 2.5G 이상 보장 |
| NPAR 1 (eth1) | iSCSI 스토리지 | 25% (2.5G) | 50% (5G) | 스토리지 트래픽 격리 |
| NPAR 2 (eth2) | FCoE | 25% (2.5G) | 50% (5G) | FC 트래픽 전용 |
| NPAR 3 (eth3) | 관리 (IPMI) | 25% (2.5G) | 10% (1G) | 관리 트래픽 분리 |
NPAR + SR-IOV 조합: NPAR 파티션 위에 SR-IOV VF를 할당할 수 있습니다. 각 파티션은 독립된 PCI Function이므로 VF는 해당 파티션의 대역폭 한도 내에서만 동작합니다. 예를 들어 NPAR 0(최대 40%) 위에 4개 VF를 생성하면, VF들은 합산 40% 한도 내에서 경쟁합니다.
MCP 펌웨어 업데이트 위험: bnx2x의 MCP(Management Controller Processor) 펌웨어 업데이트는 오류 발생 시 NIC이 완전히 응답 불능 상태(bricking)가 될 수 있습니다. 안전한 업데이트 절차는 다음과 같습니다.
- 모든 트래픽 중지:
ip link set eth0 down(모든 NPAR 파티션) - 현재 펌웨어 백업:
bnxtnvm -dev=eth0 backup - 이중화 경로 확인: 다른 NIC으로 관리 접속 유지
- 업데이트 실행:
bnxtnvm -dev=eth0 update -f new_fw.pkg - 업데이트 중 절전 모드(Suspend)/재부팅 금지 (전원 차단 = 브릭킹)
# NPAR 대역폭 설정 확인 (펌웨어 NVM에서 읽음)
bnxtnvm -dev=eth0 listcfg | grep -i bandwidth
# NPAR 파티션 별 인터페이스 목록
ip link show | grep -E 'eth[0-3]'
# 각 파티션 통계 (MCP BW 중재 결과 확인)
ethtool -S eth0 | grep -E 'npar|partition|bw'
# SR-IOV VF 생성 (NPAR 0 파티션 위에)
echo 4 > /sys/class/net/eth0/device/sriov_numvfs
# VF들은 NPAR 0의 대역폭 한도 내에서 동작
# NPAR + SR-IOV 상태 확인
ip link show eth0
# MCP 펌웨어 버전 확인
ethtool -i eth0 | grep -E 'firmware|version'
# firmware-version: BC1.5.1.0 NCSI v1.3.22.0
tg3 아키텍처: BMC 사이드밴드 통합
tg3 드라이버는 BCM5719/BCM5720 시리즈를 관리하며, 서버 관리 포트로 널리 사용됩니다. 이 NIC의 가장 큰 특징은 BMC(Baseboard Management Controller)와의 사이드밴드 채널을 통한 원격 관리 통합입니다.
BMC 공유 NIC 주의사항: BMC가 NC-SI로 NIC 포트를 공유할 때, tg3 드라이버는 APE 펌웨어와 동기화하여 MAC 필터를 관리해야 합니다. tg3_ape_lock() / tg3_ape_unlock() 함수가 이 동기를 담당합니다. BMC 트래픽은 호스트 OS의 네트워크 통계에 포함되지 않으므로, ethtool -S의 카운터만으로는 전체 포트 트래픽을 파악할 수 없습니다.
# tg3 드라이버 정보 확인
ethtool -i eth0
# driver: tg3, firmware-version: 5719-v1.46 NCSI v1.5.18.0
# APE 펌웨어 상태 (dmesg)
dmesg | grep -i "tg3.*ape"
# BMC 공유 모드 확인
ipmitool lan print 1
# tg3 WoL(Wake on LAN) 설정
ethtool -s eth0 wol g
# PHY 속도 고정 (Auto-negotiation 비활성화 시)
ethtool -s eth0 speed 1000 duplex full autoneg off
# tg3 코얼레싱 최적화 (저지연)
ethtool -C eth0 rx-usecs 10 tx-usecs 10 rx-frames 4
# 흔한 문제: EEE(Energy Efficient Ethernet) 비호환 스위치
ethtool --set-eee eth0 eee off
BCM5719 EEE 이슈: 일부 스위치와의 EEE 호환성 문제로 링크가 간헐적으로 드롭될 수 있습니다. 서버 환경에서는 ethtool --set-eee eth0 eee off로 EEE를 비활성화하는 것이 권장됩니다. 또한 tg3의 TSO 관련 체크섬(Checksum) 오류가 특정 펌웨어 버전에서 보고되므로, 문제 발생 시 ethtool -K eth0 tso off를 시도합니다.
tg3 TX/RX Descriptor 링 포맷
tg3의 TX 경로는 4개의 TX Producer 링이 단일 Consumer 모델로 수렴하는 구조입니다. 각 Producer 링은 우선순위가 다른 트래픽(일반/고우선순위/iSCSI/콘솔)에 할당되며, MAC은 Consumer 측에서 이를 스케줄링합니다.
TX Descriptor 포맷: 각 TX BD(Buffer Descriptor)는 64비트로 구성되며 start/mid/end 플래그로 멀티-BD 패킷(TSO, 점보 프레임)을 표현합니다. vlan_tag 필드는 VLAN 태그를 하드웨어가 직접 삽입하도록 지시하며, TSO 관련 mss/hdr_len도 BD에 인라인으로 포함됩니다.
/* tg3 TX Buffer Descriptor (64비트, drivers/net/ethernet/broadcom/tg3.h) */
struct tg3_tx_buffer_desc {
u32 addr_hi; /* 상위 32비트 DMA 주소 */
u32 addr_lo; /* 하위 32비트 DMA 주소 */
u32 len_flags; /* [31:16]=길이, [15:0]=플래그 */
u32 vlan_tag; /* VLAN 태그 (HW 삽입) */
};
/* len_flags 플래그 주요 비트 */
/* TXD_FLAG_START (0x0002): 패킷 첫 번째 BD */
/* TXD_FLAG_END (0x0001): 패킷 마지막 BD */
/* TXD_FLAG_TSO (0x0800): TSO 사용 (mss/hdr_len 유효) */
/* TXD_FLAG_VLAN (0x0400): VLAN 태그 삽입 지시 */
RX Descriptor 모드: tg3는 Standard RX 링(단일 SKB 버퍼, 일반 패킷)과 Jumbo RX 링(페이지 기반 분산 버퍼, 점보 프레임)을 분리 운영합니다. Standard 링의 기본 버퍼 크기는 1536바이트이며, Jumbo 링은 9KB 이상의 패킷을 여러 페이지로 분할하여 수신합니다.
APE(Application Processing Engine) FW 상호작용
BCM5719/5720의 APE는 내장 ARM 코어로, NC-SI 프로토콜 처리와 BMC 통신을 전담합니다. 호스트 드라이버와 APE는 공유 메모리 영역을 통해 통신하며, 동기화를 위해 spinlock 계열의 APE lock을 사용합니다.
/* tg3 APE 락 메커니즘 (drivers/net/ethernet/broadcom/tg3.c) */
static int tg3_ape_lock(struct tg3 *tp, int locknum)
{
int i, off;
u32 status;
off = APE_LOCK_GRANT + 4 * locknum;
/* 락 요청 */
tg3_ape_write32(tp, APE_LOCK_REQ + 4 * locknum,
APE_LOCK_REQ_DRIVER);
/* 최대 APE_LOCK_TIMEOUT 동안 대기 */
for (i = 0; i < APE_LOCK_TIMEOUT; i++) {
status = tg3_ape_read32(tp, off);
if (status == APE_LOCK_GRANT_DRIVER)
return 0;
udelay(10);
}
return -EBUSY;
}
/* NC-SI: BMC가 동일 포트를 통해 네트워크에 접근하는 프로토콜 */
/* BMC 트래픽은 APE가 처리 → 호스트 ethtool 통계에 미포함 */
BMC 공유 MAC 필터링: 호스트 OS와 BMC가 같은 MAC 주소를 사용하므로, tg3 드라이버는 APE lock을 획득한 후 MAC 필터 테이블을 업데이트합니다. BMC 전용 멀티캐스트 그룹(NC-SI management)은 APE 펌웨어가 별도 관리하며, 호스트 드라이버의 필터 변경이 BMC 통신을 끊지 않도록 APE에 사전 통보합니다.
APE 이벤트 처리: 링크 상태 변화나 시스템 종료 시 tg3 드라이버는 APE에 이벤트를 알립니다. tg3_ape_driver_state_change()가 이를 처리하며, ACPI S5(soft off) 상태에서도 BMC의 WoL(Wake-on-LAN) 기능이 유지됩니다.
tg3 트러블슈팅 가이드
EEE 링크 드롭 (BCM57766): BCM57766을 포함한 일부 BCM 기가비트 NIC은 EEE(Energy Efficient Ethernet) 협상 시 스위치 호환성 문제로 링크가 간헐적으로 드롭됩니다. 증상은 수 분~수십 분 간격으로 링크가 재협상되며, dmesg에 tg3 eth0: Link is down → Link is up이 반복됩니다.
TSO 체크섬 오류: ethtool -S의 tx_collisions 카운터는 실제 충돌이 아닌 내부 재시도를 의미하는 경우가 있습니다. TSO 관련 체크섬 오류는 tx_errors 증가로 나타나며, 특정 펌웨어 버전에서 TSO를 비활성화하면 해결됩니다.
PHY 자동협상 실패: MDI/MDIX 자동감지가 실패하는 환경(일부 구형 스위치)에서는 강제 속도 설정이 필요합니다. autoneg off 설정 시 MDI/MDIX도 수동으로 지정해야 합니다.
# EEE 링크 드롭 증상 확인
dmesg | grep -E 'tg3|eth0' | grep -E 'up|down' | tail -20
# EEE 비활성화 (BCM57766 링크 드롭 워크어라운드)
ethtool --set-eee eth0 eee off
# 영구 적용: /etc/NetworkManager/dispatcher.d/ 또는 udev rule 사용
# TSO 체크섬 오류 확인 및 비활성화
ethtool -S eth0 | grep -E 'tx_errors|tx_collisions'
ethtool -K eth0 tso off
# 체크섬 오류가 줄어드는지 확인
ethtool -S eth0 | grep tx_errors
# PHY 강제 속도 설정 (자동협상 실패 시)
ethtool -s eth0 speed 1000 duplex full autoneg off
# MDI 모드 강제 (크로스 케이블 연결 시)
ethtool -s eth0 mdix on
# APE 펌웨어 상태 및 NC-SI 버전 확인
ethtool -i eth0
# firmware-version: 5719-v1.46 NCSI v1.5.18.0 (APE FW 버전 포함)
# tg3 통계 전체 덤프 (APE/WoL 관련 포함)
ethtool -S eth0 | grep -E 'ape|wol|eee|phy'
Marvell/NXP 계열: mvneta / mvpp2 / enetc / dpaa2
Marvell과 NXP는 임베디드 및 산업용 네트워킹에서 강력한 입지를 가지고 있습니다. Marvell의 Armada 시리즈는 네트워크 어플라이언스에, NXP의 Layerscape/QorIQ 시리즈는 산업용 TSN 및 데이터 플레인 가속에 널리 사용됩니다.
| 드라이버 | SoC/칩셋 | 최대 속도 | 패킷 프로세서 | XDP | TSN | 소스 경로 |
|---|---|---|---|---|---|---|
mvneta |
Armada 370/38x/XP | 2.5 GbE | PPv1 (기본) | O | X | drivers/net/ethernet/marvell/mvneta.c |
mvpp2 |
Armada 7K/8K, CN9130 | 10 GbE | PPv2.2 | O | X | drivers/net/ethernet/marvell/mvpp2/ |
enetc |
NXP LS1028A | 2.5 GbE | ENETC SI | O | O (802.1Qbv/Qci/Qbu) | drivers/net/ethernet/freescale/enetc/ |
dpaa2-eth |
NXP LS2088A/LX2160A | 100 GbE | WRIOP | O | X | drivers/net/ethernet/freescale/dpaa2/ |
mvneta / mvpp2: Marvell 패킷 프로세서
Marvell Armada SoC의 네트워크 서브시스템은 PPv2(Packet Processor v2)라는 하드웨어 패킷 처리 엔진을 내장하고 있습니다. PPv2는 파싱, 분류, 폴리싱, RSS를 하드웨어에서 수행하여 CPU 부하를 최소화합니다.
| 비교 항목 | mvneta (PPv1) | mvpp2 (PPv2.2) |
|---|---|---|
| 지원 SoC | Armada 370, 38x, XP | Armada 7K/8K, CN9130 |
| 최대 포트 수 | 3 (SoC당) | 4 (CP당, 다중 CP 지원) |
| 최대 속도 | 2.5 GbE | 10 GbE (SFI/RXAUI) |
| 하드웨어 파서 | 기본 L2-L4 파싱 | 프로그래머블 TCAM 파서 |
| Classifier | 단순 5-tuple 해시 | C2 엔진 (TCAM + 해시) |
| Buffer Manager | 소프트웨어 관리 | 하드웨어 BM (3개 Pool) |
| XDP 지원 | O (4.19+) | O (5.4+) |
| phylink 통합 | O | O (PHY 모드 동적 전환) |
| TSO 지원 | O | O |
| per-CPU RXQ | O (소프트웨어 분배) | O (하드웨어 RSS) |
phylink 통합: mvpp2는 phylink 프레임워크를 통해 PHY 모드(SGMII, RGMII, SFI, RXAUI)를 런타임에 동적으로 전환합니다. SFP 모듈 삽입 시 자동으로 적절한 PHY 모드를 선택하며, Device Tree의 phy-mode 속성이 기본값을 결정합니다.
/* Device Tree 예시: mvpp2 포트 설정 */
&cp0_ethernet {
status = "okay";
};
&cp0_eth0 {
status = "okay";
phy-mode = "10gbase-r"; /* SFI/10G 모드 */
managed = "in-band-status"; /* phylink in-band AN */
sfp = <&sfp_cp0_eth0>; /* SFP 케이지 연결 */
};
&cp0_eth1 {
status = "okay";
phy-mode = "sgmii";
phy = <&cp0_phy0>; /* Copper PHY 연결 */
};
/* BM Pool 설정 (커널 코드) */
#define MVPP2_BM_LONG_BUF_SIZE 2048
#define MVPP2_BM_SHORT_BUF_SIZE 512
#define MVPP2_BM_POOL_SIZE_MAX 8192
# mvpp2 인터페이스 정보 확인
ethtool -i eth0
# driver: mvpp2, firmware-version: N/A
# RSS 설정 (mvpp2)
ethtool -X eth0 hfunc toeplitz
ethtool -N eth0 rx-flow-hash tcp4 sdfn
# XDP 프로그램 로드
ip link set dev eth0 xdp obj xdp_prog.o sec xdp
# mvpp2 코얼레싱
ethtool -C eth0 rx-usecs 64 rx-frames 32
# PHY/SFP 상태 확인
ethtool eth0
ethtool -m eth0 # SFP 모듈 EEPROM 정보
ENETC: IEEE TSN 지원
NXP LS1028A SoC에 내장된 ENETC(Enhanced Network Controller)는 리눅스 커널에서 가장 포괄적인 IEEE TSN(Time-Sensitive Networking) 지원을 제공합니다. 802.1Qbv(시간 인식 스케줄링), 802.1Qci(스트림 필터), 802.1Qbu(프레임 프리엠션)를 하드웨어 수준에서 구현합니다.
| TSN 기능 | IEEE 표준 | ENETC 지원 | 리눅스 인터페이스 |
|---|---|---|---|
| 시간 인식 스케줄링 | 802.1Qbv | O (최대 256 GCL 엔트리) | tc-taprio |
| 스트림 필터/게이트 | 802.1Qci (PSFP) | O (최대 2048 엔트리) | tc-flower + tc-gate |
| 프레임 프리엠션 | 802.1Qbu / 802.3br | O | ethtool --set-frame-preemption |
| Credit-Based Shaper | 802.1Qav | O | tc-cbs |
| 정밀 시간 동기화 | IEEE 1588v2 / 802.1AS | O (하드웨어 타임스탬프) | ptp4l / phc2sys |
| 경로 예약 | 802.1Qcc (SRP) | 소프트웨어 지원 | 사용자 공간 데몬 |
# 802.1Qbv: taprio qdisc 설정 (시간 인식 스케줄링)
tc qdisc replace dev eno0 parent root handle 100 taprio \
num_tc 4 \
map 0 0 1 1 2 2 3 3 0 0 0 0 0 0 0 0 \
queues 1@0 1@1 1@2 1@3 \
base-time 1000000000 \
sched-entry S 0x08 125000 \
sched-entry S 0x04 375000 \
sched-entry S 0x03 500000 \
flags 0x02 # 0x02 = 하드웨어 오프로드
# taprio 상태 확인
tc qdisc show dev eno0
# 802.1Qci: PSFP 스트림 필터 설정
tc qdisc add dev eno0 clsact
tc filter add dev eno0 ingress protocol 802.1Q \
flower vlan_prio 6 dst_mac 01:00:5e:00:01:01 \
action gate index 1 \
sched-entry open 125000 -1 \
sched-entry close 875000 -1
# 802.1Qbu: 프레임 프리엠션 활성화
ethtool --set-frame-preemption eno0 \
fp on \
preemptible-queues-mask 0x0f \
min-frag-size 60
# PTP 하드웨어 타임스탬프 설정
ptp4l -i eno0 -H -2 --step_threshold=1 &
phc2sys -s eno0 -c CLOCK_REALTIME -O 0 &
TSN 디버깅: tc -s qdisc show dev eno0로 각 TC별 전송 통계를 확인하고, ethtool --show-frame-preemption eno0로 프리엠션 상태를 점검합니다. 또한 PTP 동기화 정밀도는 pmc -u -b 0 'GET CURRENT_DATA_SET'으로 offset 값을 모니터링합니다.
dpaa2 아키텍처: WRIOP/DPIO 객체 모델
NXP의 DPAA2(Data Path Acceleration Architecture 2)는 LX2160A 등 고성능 Layerscape SoC에서 사용되는 정교한 네트워크 가속 프레임워크입니다. 기존의 고정 하드웨어 블록 대신, 소프트웨어 정의 객체 모델을 채택하여 유연한 데이터 플레인 구성을 가능하게 합니다.
DPAA2의 핵심은 MC(Management Complex) 펌웨어가 관리하는 객체(object) 시스템입니다. 각 객체는 독립된 하드웨어 리소스를 추상화하며, DPRC(Data Path Resource Container) 안에서 그룹화됩니다.
데이터 경로에서 패킷은 WRIOP(Packet I/O Engine)이 처리하고, QBMan(Queue-Based Manager) 포털을 통해 CPU와 데이터를 교환합니다.
QBMan 포털과 CDAN: 각 CPU는 전용 DPIO(QBMan 소프트웨어 포털)를 통해 프레임 큐에 접근합니다. CDAN(Channel Data Available Notification) 인터럽트가 새 패킷 도착을 알리면, NAPI 폴링이 시작됩니다. 이 구조는 CPU 간 락 경합 없이 병렬 패킷 처리를 가능하게 합니다.
# restool을 이용한 dpaa2 객체 관리
# 현재 객체 목록 조회
restool dprc show dprc.1
restool dpni info dpni.0
restool dpmac info dpmac.1
# DPNI 객체 생성 (네트워크 인터페이스)
restool dpni create \
--num-tcs=8 \
--num-queues=8 \
--options=DPNI_OPT_HAS_KEY_MASKING
# DPIO 객체 생성 (per-CPU 포털)
restool dpio create \
--channel-mode=DPIO_LOCAL_CHANNEL \
--num-priorities=8
# DPBP 객체 생성 (버퍼 풀)
restool dpbp create
# 객체를 컨테이너에 연결
restool dprc assign dprc.1 --object=dpni.0 --plugged=1
restool dprc assign dprc.1 --object=dpio.0 --plugged=1
# DPNI와 DPMAC 연결 (L2 바인딩)
restool dprc connect dprc.1 \
--endpoint1=dpni.0 \
--endpoint2=dpmac.1
# 자식 DPRC 생성 (VM 할당용)
restool dprc create dprc.1 \
--options=DPRC_CFG_OPT_ALLOC_ALLOWED
# dpaa2-eth 드라이버 운영
# 인터페이스 정보
ethtool -i eth0
# driver: dpaa2-eth
# firmware-version: 10.28.0
# 채널/큐 설정
ethtool -l eth0
ethtool -L eth0 combined 8
# RSS 설정
ethtool -X eth0 hfunc toeplitz
ethtool -N eth0 rx-flow-hash tcp4 sdfn
# XDP 프로그램 로드
ip link set eth0 xdp obj dpaa2_xdp.o sec xdp_prog
# WRIOP 통계 확인
ethtool -S eth0 | grep -E 'rx_|tx_|ch_'
# fsl-mc 버스 디바이스 확인
ls /sys/bus/fsl-mc/devices/
cat /sys/bus/fsl-mc/devices/dpni.0/driver
# DPL(Data Path Layout) 적용
# U-Boot에서: fsl_mc apply dpl 0x80100000
dpaa2 성능 최적화: LX2160A에서 최대 성능을 위해서는 DPIO 객체를 각 CPU 코어에 1:1 할당하고, DPNI의 큐 수를 CPU 수와 일치시킵니다. 또한 DPNI_OPT_HAS_KEY_MASKING 옵션을 활성화하면 RSS 해시 키 커스터마이징이 가능하여 워크로드별 최적 분배를 달성할 수 있습니다. Huge page(2MB)를 DPBP 버퍼 풀에 사용하면 TLB 미스를 줄여 추가 성능 향상을 얻습니다.
Realtek/Aquantia: r8169 / r8125 / atlantic
Realtek은 가장 널리 보급된 소비자/임베디드용 NIC 칩셋 제조사이며, Aquantia(현 Marvell 인수)는 멀티기가비트(2.5G/5G/10G) 시장을 선도합니다. 리눅스 커널에서 이 세 드라이버는 각기 다른 하드웨어 세대와 성능 계층을 담당합니다.
| 드라이버 | 주요 칩셋 | 최대 속도 | 큐 수 | NAPI | XDP | HW Offload | 커널 트리 |
|---|---|---|---|---|---|---|---|
r8169 |
RTL8111B~H, RTL8168 | 1 Gbps | 1 | O | X | VLAN, Checksum | in-tree |
r8169 (2.5G) |
RTL8125B/BG | 2.5 Gbps | 1 | O | X | VLAN, Checksum, TSO | in-tree |
r8125 (벤더) |
RTL8125B/BG | 2.5 Gbps | 최대 4 | O | X | VLAN, Checksum, TSO, RSS | out-of-tree |
atlantic |
AQC107/108/113 | 10 Gbps | 최대 8 | O | O | RSS, Checksum, TSO, LRO | in-tree |
r8169 드라이버
r8169 드라이버(drivers/net/ethernet/realtek/r8169_main.c)는
커널에서 가장 많은 하드웨어 변형(quirk)을 처리하는 NIC 드라이버 중 하나입니다.
20년 이상의 칩 세대를 단일 드라이버로 지원하며, 각 변형마다 고유한 버그 회피 코드가 존재합니다.
RTL 칩 변형별 특성 및 quirk 테이블
| 칩셋 | PCIe Gen | 최대 속도 | Jumbo (MTU) | WoL | 주요 quirk / 하드웨어 버그 |
|---|---|---|---|---|---|
| RTL8111B | 1.0 x1 | 1 Gbps | 4K | O | TX timeout 빈발, PLL 초기화 지연 필요 |
| RTL8111C | 1.0 x1 | 1 Gbps | 6K | O | ASPM L1 진입 시 링크 불안정 |
| RTL8111D | 2.0 x1 | 1 Gbps | 9K | O | RX FIFO 오버플로우 시 칩 리셋 필요 |
| RTL8111E | 2.0 x1 | 1 Gbps | 9K | O | EEE(Energy Efficient Ethernet) 호환성 문제 |
| RTL8111F | 2.1 x1 | 1 Gbps | 9K | O | dash 관리 엔진 충돌, S5 WoL 실패 보고 |
| RTL8111G | 2.1 x1 | 1 Gbps | 9K | O | 특정 BIOS에서 PCI config space 손상 |
| RTL8111H | 2.1 x1 | 1 Gbps | 9K | O | 최신 리비전, quirk 최소화 |
| RTL8168 | 1.0 x1 | 1 Gbps | 4K | O | RTL8111 모바일 변형, 동일 quirk 공유 |
| RTL8125B | 2.1 x1 | 2.5 Gbps | 9K | O | r8169 모듈에서 지원, RSS 미지원(단일 큐) |
r8169 드라이버는 이를 감지하면
자동으로 ASPM을 비활성화하지만, BIOS/ACPI가 다시 활성화하는 경우가 있습니다.
커널 부트 파라미터 pcie_aspm=off로 완전히 비활성화하거나,
pcie_aspm.policy=performance를 사용할 수 있습니다.
전원 관리(Power Management) 및 진단
# r8169 드라이버 사용 중인 장치 확인
lspci -k | grep -A3 -i realtek
# 현재 칩 리비전 확인 (dmesg에서)
dmesg | grep r8169
# 예: r8169 0000:03:00.0: RTL8111H at 0xffffa1234000, ...
# ASPM 상태 확인
lspci -vvs 03:00.0 | grep -i aspm
# WoL(Wake-on-LAN) 설정 확인 및 활성화
ethtool -s eth0 wol g
ethtool eth0 | grep Wake-on
# EEE 상태 확인 (RTL8111E 이후)
ethtool --show-eee eth0
# TX timeout 발생 시 통계 확인
ethtool -S eth0 | grep -E "tx_timeout|rx_missed"
# 인터럽트 coalescing 조정 (rx-usecs/tx-usecs)
ethtool -C eth0 rx-usecs 100 tx-usecs 200
RX 링 오버플로우 진단
r8169 드라이버의 하드웨어적 한계 중 하나는 256개 디스크립터 단일 큐 구조입니다. RTL811x ASIC는 TX/RX 각각 최대 256개 디스크립터를 지원하며, 멀티 큐 확장 없이 단일 링으로 동작합니다. 이는 높은 패킷 수신률 환경에서 링 오버플로우를 유발할 수 있습니다.
ethtool -S로 확인할 수 있는 두 카운터의 의미는 다릅니다:
rx_missed_errors: ASIC RX FIFO 오버플로우로 인해 하드웨어 수준에서 드롭된 패킷 수. RTL 레지스터MissedPacketCount(0x4C)를 직접 반영합니다.rx_fifo_errors: 소프트웨어 NAPI 링에서 기술적으로 손실된 프레임 수.rx_missed_errors값과 동일하게 매핑됩니다.
링 오버플로우를 실시간으로 진단하려면 bpftrace로
r8169_poll() 함수의 per-poll 패킷 수를 히스토그램으로 관찰할 수 있습니다:
# bpftrace: r8169_poll() 호출당 처리 패킷 수 히스토그램
bpftrace -e '
kretprobe:r8169_poll {
@hist = hist(retval);
}
interval:s:5 {
print(@hist);
clear(@hist);
}'
# rx_missed_errors 실시간 모니터링 (1초 간격)
watch -n1 "ethtool -S eth0 | grep -E 'rx_missed|rx_fifo|rx_packets'"
# 링 오버플로우 경향 확인 (누적 delta)
prev=0
while sleep 1; do
cur=$(ethtool -S eth0 | awk '/rx_missed_errors/{print $2}')
echo "delta: $((cur - prev)) total: $cur"
prev=$cur
done
# rx-usecs 증가로 NAPI 빈도 조정 (배치 크기 확대)
ethtool -C eth0 rx-usecs 200
# 링 크기 확인 (최대 256, 기본 256)
ethtool -g eth0
오버플로우 발생 시 근본 해결책은 다음 순서로 시도합니다:
(1) rx-usecs 증가로 인터럽트 코얼레싱 강화 →
(2) 트래픽 부하 자체를 줄이거나 QoS로 입력 속도 제한 →
(3) 근본적으로 멀티 큐를 지원하는 NIC(r8125 2.5GbE 또는 인텔 igc)로 교체.
r8169와 BQL(Byte Queue Limits) 상호작용
BQL(Byte Queue Limits)은 TX 링의 인플라이트 바이트 수를 동적으로 조정하여 레이턴시를 낮추는 커널 메커니즘입니다. 단일 큐 드라이버인 r8169에서는 BQL의 중요성이 멀티 큐 드라이버보다 더 높습니다. 여러 큐로 분산할 수 없기 때문에 단일 TX 링의 인플라이트 제어가 전체 레이턴시를 결정합니다.
BQL 파라미터는 sysfs를 통해 조회하고 조정할 수 있습니다:
# BQL 현재 상태 조회
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_min
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_max
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/hold_time
# BQL 인플라이트 바이트 수 확인
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/inflight
# BQL limit_max 수동 조정 (낮출수록 레이턴시 감소, 높일수록 처리량 증가)
echo 10000 > /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_max
fq_codel과의 시너지: tc qdisc로 fq_codel을 설정하면
BQL과 상호 보완적으로 동작합니다. BQL은 드라이버 TX 링 레벨에서
인플라이트 바이트를 제한하고, fq_codel은 그 위 qdisc 레이어에서
per-flow 공정성(Fairness)과 지연 기반 드롭을 수행합니다.
단일 큐 NIC에서 버퍼블로트를 완화하는 가장 효과적인 조합입니다:
# fq_codel 적용 (단일 큐 NIC 버퍼블로트 완화)
tc qdisc replace dev eth0 root fq_codel
# 상태 확인
tc qdisc show dev eth0
tc -s qdisc show dev eth0 # 통계 포함
# 파라미터 조정 (target: 목표 대기시간, interval: 측정 구간)
tc qdisc replace dev eth0 root fq_codel target 5ms interval 100ms
quirk 아키텍처 분석
r8169 드라이버의 핵심 설계 원칙은 칩별 quirk 함수 분리입니다.
각 RTL 칩 리비전마다 하드웨어 초기화 루틴이 다르기 때문에,
드라이버는 rtl_chip_infos[] 배열을 통해 칩별 특성을 등록하고
해당 rtl_hw_start_*() 함수 포인터를 호출합니다.
/* drivers/net/ethernet/realtek/r8169_main.c (발췌/단순화) */
/* 칩 정보 구조체 */
struct rtl_chip_info {
const char *name;
u8 mcfg; /* MAC 설정 레지스터 값 */
u32 RxConfigMask;
void (*hw_start)(struct rtl8169_private *tp);
};
/* 칩별 초기화 함수 테이블 (일부) */
static const struct rtl_chip_info rtl_chip_infos[] = {
[RTL_GIGA_MAC_VER_02] = {
.name = "RTL8169s",
.hw_start = rtl_hw_start_8169,
},
[RTL_GIGA_MAC_VER_07] = {
.name = "RTL8102e",
.hw_start = rtl_hw_start_8102e,
},
[RTL_GIGA_MAC_VER_46] = {
.name = "RTL8168H",
.hw_start = rtl_hw_start_8168h,
},
[RTL_GIGA_MAC_VER_63] = {
.name = "RTL8125B",
.hw_start = rtl_hw_start_8125b,
},
/* ... 50개 이상의 칩 버전 엔트리 ... */
};
/* 초기화 시 칩 버전 감지 및 함수 포인터 등록 */
static void rtl_init_one(struct rtl8169_private *tp)
{
tp->hw_start = rtl_chip_infos[tp->mac_version].hw_start;
}
ASPM L1 조건부 비활성화: 일부 칩(RTL8111B/C 등)은 ASPM L1 절전 상태에서 복귀 시 링크 불안정 현상이 보고되어 있습니다. 드라이버는 칩 버전을 참조하여 오래된 리비전에서 ASPM을 자동으로 비활성화합니다:
/* ASPM quirk 탐지 흐름 */
static void rtl_hw_aspm_clkreq_enable(struct rtl8169_private *tp, bool enable)
{
/* RTL8411B(VER_46) 이전 칩은 ASPM L1 활성화 금지 */
if (tp->mac_version < RTL_GIGA_MAC_VER_46)
return;
if (enable) {
RTL_W8(tp, Config2, RTL_R8(tp, Config2) | ClkReqEn);
RTL_W8(tp, Config5, RTL_R8(tp, Config5) | ASPM_en);
} else {
RTL_W8(tp, Config2, RTL_R8(tp, Config2) & ~ClkReqEn);
RTL_W8(tp, Config5, RTL_R8(tp, Config5) & ~ASPM_en);
}
}
/* PLL 타이밍 quirk: RTL8111B/C는 PLL off 시 복귀 지연 보정 필요 */
static void rtl8168b_hw_start(struct rtl8169_private *tp)
{
RTL_W8(tp, Config3, RTL_R8(tp, Config3) & ~Beacon_en);
/* PLL이 꺼진 상태에서의 wake-up 타이밍 보정 */
RTL_W16(tp, CPlusCmd, RTL_R16(tp, CPlusCmd) & ~R8168_CPCMD_QUIRK_MASK);
}
dmesg | grep r8169로 드라이버가 감지한 칩 버전을 확인하고,
커널 소스 drivers/net/ethernet/realtek/r8169_main.c에서
해당 RTL_GIGA_MAC_VER_* 인덱스의 hw_start 함수를
직접 추적할 수 있습니다.
커널 트레이싱 레시피
r8169 poll 경로의 성능을 분석하기 위한 ftrace/bpftrace/perf 레시피 모음입니다.
## 1. ftrace function_graph: r8169 poll 경로 전체 추적
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'r8169_poll' > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -60
## 2. bpftrace: per-poll 처리 패킷 수 히스토그램 (10초 수집)
bpftrace -e '
kretprobe:r8169_poll {
@pkt_hist = hist(retval);
}
END { print(@pkt_hist); }
' &
sleep 10 && kill %1
## 3. perf stat: softirq NET_RX 오버헤드 측정
perf stat -e irq:softirq_entry,irq:softirq_exit \
--filter 'vec == 3' \
-a sleep 5
## 4. 드라이버 함수 레이턴시 분포 (bpftrace)
bpftrace -e '
kprobe:r8169_poll { @start[tid] = nsecs; }
kretprobe:r8169_poll {
$lat = nsecs - @start[tid];
@lat_us = hist($lat / 1000);
delete(@start[tid]);
}
interval:s:5 { print(@lat_us); clear(@lat_us); }'
주요 트레이스포인트 목록:
| 트레이스포인트 / kprobe | 용도 | 비고 |
|---|---|---|
kprobe:r8169_poll |
NAPI poll 진입/복귀, 패킷 수 측정 | retval = 처리한 패킷 수 |
net:netif_receive_skb |
커널 스택 전달 시점 | tracepoint |
net:net_dev_xmit |
TX 큐잉 시점 | tracepoint |
irq:softirq_entry vec==3 |
NET_RX softirq 진입 | vec 3 = NET_RX_SOFTIRQ |
kprobe:rtl8169_tx_timeout |
TX 타임아웃 발생 추적 | 행 발생 시 경보 설정 가능 |
r8125 / RTL8125B: 2.5GbE
RTL8125B 칩셋은 2.5 Gbps 이더넷을 지원하는 소비자용 NIC입니다.
커널 5.9 이후 r8169 모듈이 기본 지원하지만,
Realtek 공식 r8125 out-of-tree 드라이버와 기능 차이가 있습니다.
in-tree(r8169) vs out-of-tree(r8125) 비교
| 항목 | r8169 (in-tree) | r8125 (Realtek 벤더) |
|---|---|---|
| 멀티큐 RSS | 미지원 (단일 큐) | 최대 4개 큐 지원 |
| TSO (TCP Segmentation Offload) | 지원 | 지원 |
| USO (UDP Segmentation Offload) | 미지원 | 지원 |
| 커널 버전 호환성 | 커널 빌드에 포함 | 별도 컴파일 필요, DKMS 지원 |
| 유지보수 | 커널 커뮤니티 관리 | Realtek 자체 업데이트 |
| 안정성 | 높음 (광범위 테스트) | 중간 (특정 커널에서 빌드 실패 보고) |
| XDP 지원 | 미지원 | 미지원 |
| Flow Control | 기본 지원 | 고급 설정 가능 |
| 성능 (iperf3 단일 스트림) | ~2.35 Gbps | ~2.35 Gbps |
| 성능 (iperf3 다중 스트림) | ~2.35 Gbps (단일 큐 한계) | ~2.40 Gbps (RSS 효과 미미) |
r8169 드라이버로 충분합니다.
2.5 Gbps 단일 스트림 성능은 두 드라이버 간 차이가 미미하며,
RSS 멀티큐가 필요한 고병렬 워크로드가 아니라면 커널 내장 드라이버를 권장합니다.
DKMS 관리 부담 없이 커널 업그레이드 시 자동으로 호환성이 보장됩니다.
# 현재 사용 중인 드라이버 확인
ethtool -i eth0
# driver: r8169 또는 driver: r8125
# 2.5G 링크 속도 확인
ethtool eth0 | grep Speed
# Speed: 2500Mb/s
# out-of-tree r8125 설치 (필요 시)
tar xf r8125-9.012.04.tar.bz2
cd r8125-9.012.04
sudo ./autorun.sh
# r8169 블랙리스트 (out-of-tree 사용 시)
echo "blacklist r8169" | sudo tee /etc/modprobe.d/r8169-blacklist.conf
sudo depmod -a
atlantic 드라이버: Aquantia AQC107/108/113
Aquantia(현 Marvell)의 AQC 시리즈는 임베디드 MIPS 프로세서를 탑재한
고성능 멀티기가비트 NIC입니다. 펌웨어가 MAC/PHY 초기화와 관리를 담당하며,
호스트 드라이버는 메일박스 인터페이스를 통해 펌웨어와 통신합니다.
atlantic 드라이버(drivers/net/ethernet/aquantia/atlantic/)는
이 독특한 아키텍처를 반영합니다.
펌웨어 버전 의존성
| 칩셋 | 최소 FW 버전 | 권장 FW 버전 | 주요 기능/수정 |
|---|---|---|---|
| AQC107 | 1.5.44 | 3.1.100+ | 10G 링크 안정성, WoL 수정, EEE 개선 |
| AQC108 | 1.5.44 | 3.1.100+ | 5G/2.5G 모드 안정성 개선 |
| AQC113 | 4.0.0 | 4.6.x+ | AQC113 전용 신규 칩, 초기 FW 버그 다수 수정 |
atlantic 드라이버의 ethtool --flash 명령이나
Marvell 공식 도구로 업데이트합니다. 펌웨어 버전이 너무 오래되면
드라이버가 경고 메시지를 출력하며 일부 기능을 비활성화합니다.
성능 튜닝
# 펌웨어 버전 확인
ethtool -i aqc0
# firmware-version: 3.1.100
# RSS 해시 키 및 인디렉션 테이블 확인
ethtool -x aqc0
# RSS 인디렉션 테이블 수정 (큐 0,1,2,3 균등 분배)
ethtool -X aqc0 equal 4
# RSS 해시 필드 설정 (TCP 4-tuple)
ethtool -N aqc0 rx-flow-hash tcp4 sdfn
# 링 버퍼 크기 증가
ethtool -G aqc0 rx 4096 tx 4096
# 인터럽트 coalescing 최적화
ethtool -C aqc0 adaptive-rx on adaptive-tx on
# IRQ affinity 설정 (CPU 코어 분산)
echo 1 > /proc/irq/50/smp_affinity # RXQ0 → CPU0
echo 2 > /proc/irq/51/smp_affinity # RXQ1 → CPU1
echo 4 > /proc/irq/52/smp_affinity # RXQ2 → CPU2
echo 8 > /proc/irq/53/smp_affinity # RXQ3 → CPU3
# Flow steering 규칙 추가 (특정 포트 → 특정 큐)
ethtool -N aqc0 flow-type tcp4 dst-port 80 action 0
ethtool -N aqc0 flow-type tcp4 dst-port 443 action 1
# 통계 확인
ethtool -S aqc0 | grep -E "Queue|rx_packets|tx_packets"
EEE(Energy Efficient Ethernet) 동작 원리
EEE(Energy Efficient Ethernet)는 IEEE 802.3az 표준으로 정의된 전력 절감 기술입니다. 트래픽이 없을 때 링크를 LPI(Low Power Idle) 상태로 전환하여 소비 전력을 줄입니다. AQC(Aquantia) MIPS 펌웨어는 링크 상태와 트래픽 활동을 감시하여 LPI 전환을 제어합니다.
IEEE 802.3az LPI 신호 교환 흐름은 다음과 같습니다: 송신측 MAC이 IDLE 상태를 감지하면 LPI Request 시그널(Signal)을 PHY에 전달하고, PHY는 링크 파트너와 협의하여 링크를 저전력 모드로 전환합니다. 패킷 전송이 필요해지면 PHY가 링크를 재활성화(Wake)하는데, 이때 PHY 재훈련(retraining) 지연이 발생합니다.
AQC MIPS FW 상태 전환: Active → LPI Request → LPI → Wake → Active
속도별 LPI 이탈 시 wake-up 지연 시간:
| 링크 속도 | Wake-up 지연 (Tw_sys_tx) | 비고 |
|---|---|---|
| 100 Mbps | 30 μs | IEEE 802.3az-2010 표준값 |
| 1 Gbps | 16.5 μs | 표준값 |
| 2.5 Gbps | 20 μs | IEEE 802.3bz |
| 5 Gbps | 25 μs | IEEE 802.3bz |
| 10 Gbps | 4.5 μs | IEEE 802.3az-2010 |
# EEE 현재 상태 확인
ethtool --show-eee aqc0
# EEE 전체 비활성화
ethtool --set-eee aqc0 eee off
# 특정 속도에서만 EEE 비활성화 (1G에서만 끄기)
ethtool --set-eee aqc0 advertise 0x0 # 모든 속도 EEE advertisement 제거
# EEE 관련 통계 (드라이버가 제공하는 경우)
ethtool -S aqc0 | grep -i eee
펌웨어 버전 매트릭스 확장
AQC NIC의 기능 지원 여부는 펌웨어 버전에 따라 달라집니다. 다음 표는 주요 FW 버전별 기능 매핑을 정리한 것입니다:
| FW 버전 범위 | 추가된 주요 기능 | 알려진 이슈 |
|---|---|---|
| 1.x | 기본 1G/2.5G/5G/10G 링크, RSS 4큐 | EEE wake-up 지연 버그 (일부 보드) |
| 2.x | 멀티캐스트 필터링 개선, WoL 강화 | 메일박스 응답 시간 간헐적 지연 |
| 3.x | XDP 지원, 16큐 RSS, FW watchdog | 체크섬 오프로드 엣지 케이스 |
| 4.x | EEE 안정화, PTP 하드웨어 타임스탬프 | - |
FW 장애 모드 및 대응:
- 메일박스 타임아웃: 드라이버가 FW에 명령을 전송했을 때
aq_hw_fw_state_check()가 응답을 기다리는 동안 타임아웃이 발생합니다. 커널 로그에FW Mailbox is full메시지가 나타납니다. - 체크섬 불일치: FW 이미지 로드 시 CRC 검증 실패.
dmesg | grep atlantic에서FW checksum mismatch확인. - 워치독 리셋: FW 내부 MIPS 코어가 응답 불가 시 하드웨어 워치독이 자동으로 FW를 재시작합니다. 링크가 짧게 끊겼다 복구됩니다.
# FW 버전 확인
ethtool -i aqc0 | grep firmware-version
# FW 관련 오류 메시지 조회
dmesg | grep -i "atlantic\|aqc\|firmware\|mailbox"
# FW 응답 상태 확인 (aq_hw_fw_state_check 관련 로그)
dmesg | grep -E "FW|fw_state|mailbox"
# NIC 재초기화로 FW 워치독 리셋 트리거 (링크 재연결)
ip link set aqc0 down && sleep 2 && ip link set aqc0 up
atlantic XDP 특성과 제약
atlantic 드라이버(drivers/net/ethernet/aquantia/atlantic/)는
XDP(eXpress Data Path)를 지원하지만 몇 가지 중요한 제약이 있습니다.
XDP 처리 경로: aq_ring_xdp_clean() 함수가
RX 링에서 패킷을 가져와 XDP 프로그램을 실행합니다.
XDP_PASS 시 일반 커널 스택으로, XDP_DROP 시 즉시 드롭,
XDP_TX 시 동일 링을 통해 재전송합니다.
XDP_TX 링 재사용 제약: XDP_TX 액션은 동일 RX 링 버퍼를 TX에 재사용합니다. 이로 인해 높은 XDP_TX 비율에서는 RX 링이 소진되어 추가 패킷 수신이 지연될 수 있습니다.
XDP 액션별 성능 비교:
| XDP 액션 | 처리 경로 | 성능 (10G 기준) | 비고 |
|---|---|---|---|
| XDP_DROP | RX 링 → XDP 프로그램 → 드롭 | ~14 Mpps | 최고 처리량, SKB 할당 없음 |
| XDP_PASS | RX 링 → XDP → 커널 스택 | ~8 Mpps | SKB 할당 오버헤드 |
| XDP_TX | RX 링 → XDP → 동일 TX | ~10 Mpps | 링 공유로 인한 제약 |
| 커널 스택 드롭 | RX → SKB 할당 → iptables DROP | ~3 Mpps | XDP_DROP 대비 ~4.7× 느림 |
Chelsio: cxgb4
Chelsio Communications는 고성능 서버/데이터센터용 NIC 전문 제조사로,
하드웨어 TCP/IP 오프로드 엔진(TOE), RDMA, 암호화 가속을 내장한
프로토콜 오프로드 어댑터의 선구자입니다.
cxgb4 드라이버(drivers/net/ethernet/chelsio/cxgb4/)는
Terminator 5/6(T5/T6) ASIC을 지원합니다.
| 드라이버 | ASIC | 최대 속도 | 큐 수 | 오프로드 엔진 | 커널 트리 |
|---|---|---|---|---|---|
cxgb4 |
T5 | 40 Gbps | 최대 128 | TOE, iSCSI, RDMA | in-tree |
cxgb4 |
T6 | 100 Gbps | 최대 256 | TOE, iSCSI, RDMA, TLS, DTLS | in-tree |
cxgb4 아키텍처
Chelsio T6 ASIC은 단순한 NIC가 아닌, 전체 네트워크 프로토콜 스택을 하드웨어로 구현한 네트워크 프로세서입니다. 주요 구성 요소는 MPS(MAC/Port Switch), TP(TCP/IP Processing Engine), ULP(Upper Layer Protocol Engine), SGE(Scatter-Gather Engine), 그리고 암호화 가속기입니다.
T5 vs T6 비교
| 항목 | T5 (Terminator 5) | T6 (Terminator 6) |
|---|---|---|
| 최대 포트 속도 | 40 Gbps (QSFP+) | 100 Gbps (QSFP28) |
| PCIe 인터페이스 | Gen3 x8 | Gen3 x16 |
| TX/RX 큐 수 | 최대 128 | 최대 256 |
| TCP 동시 연결 (TOE) | 최대 131K | 최대 262K |
| RDMA (iWARP) 연결 | 최대 131K | 최대 262K |
| 암호화 가속 | AES-128/256-CBC, SHA | AES-GCM, TLS 1.2/1.3, DTLS |
| TLS Offload | 미지원 | 지원 (인라인 TLS record) |
| TCAM 필터 수 | 496 | 2048 |
| SR-IOV VF 수 | 최대 64 | 최대 256 |
| 패킷 레이트 (64B) | ~60 Mpps | ~148 Mpps |
SGE(Scatter-Gather Engine) 모델
SGE는 호스트 CPU와 ASIC 사이의 DMA 전송을 관리하는 핵심 엔진입니다. 각 큐페어(Queue Pair)는 TX 큐, RX 큐(Ingress Queue), Free List(FL), 그리고 응답 큐(Response Queue)로 구성됩니다. 호스트 드라이버는 Doorbell 레지스터에 쓰기 연산을 수행하여 새로운 설명자를 ASIC에 통지합니다.
# cxgb4 드라이버 로드 상태 확인
lsmod | grep cxgb4
# ASIC 세대 및 포트 정보 확인
ethtool -i eth0
# driver: cxgb4
# firmware-version: 1.27.1.0
# SGE 큐 구성 확인
ethtool -l eth0
# Combined: 16 (현재) / 256 (최대)
# 큐 수 변경 (CPU 코어 수에 맞춤)
ethtool -L eth0 combined 32
# 링 버퍼 크기 조정
ethtool -G eth0 rx 8192 tx 8192
# 인터럽트 coalescing (지연 시간 vs 처리량 트레이드오프)
ethtool -C eth0 rx-usecs 10 rx-frames 64
# 드라이버 내부 통계 확인
ethtool -S eth0 | head -40
# PCIe 대역폭 확인
lspci -vvs $(ethtool -i eth0 | grep bus-info | awk '{print $2}') | grep -i width
cxgb4 오프로드 엔진
Chelsio의 핵심 차별점은 프로토콜 오프로드입니다.
TOE(TCP Offload Engine)는 TCP 상태 머신 전체를 ASIC에서 실행하여
호스트 CPU 부하를 극적으로 줄이며, RDMA(iw_cxgb4)는
제로카피 데이터 전송을 가능하게 합니다.
오프로드 성능 영향
| 모드 | CPU 사용률 (100G) | 처리량 | 지연 시간 | 동시 연결 수 | 적합 워크로드 |
|---|---|---|---|---|---|
| 일반 TCP (커널) | 60~80% | ~90 Gbps | ~15 μs | 제한 없음 | 범용 |
| TOE | 10~20% | ~95 Gbps | ~5 μs | 최대 262K | 대량 연결 서버 |
| RDMA (iWARP) | 5~10% | ~98 Gbps | ~2 μs | 최대 262K | 스토리지, HPC |
| TLS Offload (T6) | 15~25% | ~80 Gbps | ~8 μs | 최대 32K | HTTPS 서버 |
iptables/nftables 같은 커널 방화벽(Firewall) 규칙이
TOE 연결에 적용되지 않을 수 있어 보안 정책 검토가 필요합니다.
리눅스 메인라인 커널은 TOE를 공식 지원하지 않으며,
Chelsio의 out-of-tree 패치(WD-TOE)가 필요합니다.
# RDMA (iw_cxgb4) 모듈 로드 확인
lsmod | grep iw_cxgb4
# RDMA 디바이스 목록
rdma link show
# RDMA 성능 테스트 (서버 측)
ib_write_bw -d cxgb4_0 --report_gbits
# RDMA 성능 테스트 (클라이언트 측)
ib_write_bw -d cxgb4_0 192.168.1.100 --report_gbits
# TLS offload 확인 (T6 전용)
ethtool -k eth0 | grep tls
# tls-hw-tx-offload: on
# tls-hw-rx-offload: on
# TLS offload 활성화
ethtool -K eth0 tls-hw-tx-offload on tls-hw-rx-offload on
# iSCSI offload 상태 확인
iscsiadm -m iface | grep cxgb4
# 오프로드 통계 확인
ethtool -S eth0 | grep -E "toe_|rdma_|tls_"
TOE 성능 분석
TOE는 모든 워크로드에서 이득을 주는 것이 아닙니다. 연결 수와 트래픽 패턴에 따라 이득과 손해가 달라집니다.
| 워크로드 | TOE 효과 | 이유 |
|---|---|---|
| 대규모 병렬 연결 (10K+) + 대용량 전송 | CPU -60%, 지연 -3× | ASIC이 TCP 상태 머신 전담; 연결당 CPU 비용 거의 없음 |
| 고정 연결 수 + 스트리밍 (파일 서버) | 처리량 +5~10% | TCP ACK 처리 오프로드 효과 |
| 소형 패킷 + 짧은 연결 (HTTP/1.0 스타일) | 중립 또는 소폭 손해 | ASIC 연결 설정 비용이 커널보다 높음 |
| 높은 연결 생성 속도 (connection churn) | 손해 가능 | TOE 연결 설정/해제 레이턴시가 SW보다 길 수 있음 |
| 방화벽/NAT 경유 트래픽 | iptables 미적용으로 보안 위험 | TOE 연결은 커널 netfilter 우회 |
Crypto 오프로드 파이프라인
Chelsio T6 ASIC은 TLS 인라인 암호화와 IPsec 오프로드를 지원합니다. kTLS TX 경로에서 소켓 레이어가 TLS 레코드를 구성하면, T6 crypto engine이 와이어로 나가기 전 AES-GCM 암호화를 수행합니다. 이 과정에서 CPU는 TLS 레코드 메타데이터만 처리하고, 실제 암호화 연산은 ASIC이 전담합니다.
| 오프로드 유형 | 처리량 (100G) | CPU 절감 | 최소 HW |
|---|---|---|---|
| TLS 1.2 TX (AES-128-GCM) | ~80 Gbps | ~50% 감소 | T6 (cxgb4) |
| TLS 1.3 TX (AES-256-GCM) | ~75 Gbps | ~45% 감소 | T6 (cxgb4) |
| IPsec ESP (AES-128-GCM) | ~90 Gbps | ~40% 감소 | T6 (cxgb4) |
| TLS RX (복호화(Decryption)) | ~70 Gbps | ~45% 감소 | T6 (cxgb4) |
# kTLS TX 오프로드 cxgb4 활성화
ethtool -K eth0 tls-hw-tx-offload on
# kTLS 오프로드 상태 확인
ethtool -k eth0 | grep tls
# tls-hw-tx-offload: on
# tls-hw-rx-offload: on (T6 지원)
# TLS 오프로드 통계 확인
ethtool -S eth0 | grep tls_
# tls_tx_sw_fallback: 0 (SW 폴백 없으면 HW 오프로드 정상)
# tls_tx_hw_records: 1234567
# IPsec 오프로드 상태 확인
ip xfrm state list
ethtool -k eth0 | grep esp
# esp-hw-offload: on
# 오프로드 연결 수 확인
ethtool -S eth0 | grep -E "toe_conn|tls_conn"
cxgb4 패킷 분류 파이프라인
T6 ASIC은 하드웨어 기반 다단계 패킷 분류 파이프라인을 제공합니다.
MPS(MAC Port Switch)에서 L2 필터링 후, TP(TCP/IP Engine)에서
프로토콜 매칭과 TCAM/해시 기반 필터를 적용하여
최종적으로 적절한 SGE 큐로 패킷을 전달합니다.
이 파이프라인은 tc flower 오프로드를 통해 소프트웨어적으로도 제어 가능합니다.
필터 설정 및 tc offload
# TCAM 필터: 특정 목적지 IP의 패킷을 큐 5로 전달
ethtool -N eth0 flow-type tcp4 dst-ip 10.0.0.100 action 5
# 설정된 필터 규칙 확인
ethtool -n eth0
# tc flower 오프로드 (하드웨어 가속 필터)
tc qdisc add dev eth0 ingress
# 소스 IP 기반 패킷 드롭 (하드웨어 오프로드)
tc filter add dev eth0 ingress protocol ip flower \
src_ip 192.168.1.0/24 \
skip_sw \
action drop
# VLAN + 목적지 포트 기반 큐 스티어링
tc filter add dev eth0 ingress protocol 802.1Q flower \
vlan_id 100 \
dst_port 8080 \
skip_sw \
action skbedit queue_mapping 3
# 오프로드된 필터 통계 확인
tc -s filter show dev eth0 ingress
# 하드웨어 tc offload 기능 확인
ethtool -k eth0 | grep hw-tc-offload
# hw-tc-offload: on
# 하드웨어 tc offload 활성화
ethtool -K eth0 hw-tc-offload on
tc flower에서 skip_sw 플래그는 소프트웨어 경로를 건너뛰고
하드웨어만으로 필터링하며, skip_hw는 반대로 소프트웨어만 사용합니다.
/sys/kernel/debug/cxgb4/ 디버그 파일시스템(Filesystem)을 통해
내부 레지스터, 필터 테이블, 큐 상태, 펌웨어 로그 등을 상세히 확인할 수 있습니다.
cudbg(Chelsio Unified Debug) 도구를 사용하면
어댑터 전체 상태를 덤프하여 오프라인 분석이 가능합니다.
Cloud NIC: ENA / gVNIC
| 항목 | ENA (AWS) | gVNIC (GCP) |
|---|---|---|
| 모듈명 | ena | gve |
| 소스 위치 | drivers/net/ethernet/amazon/ena/ | drivers/net/ethernet/google/gve/ |
| 큐 인터페이스 | Admin Queue + I/O SQ/CQ | GQI / DQO (세대별) |
| 최대 큐 수 | 32 (인스턴스 타입별 상이) | 16 (인스턴스 타입별 상이) |
| 최대 대역폭 | 200 Gbps (p5.48xlarge) | 200 Gbps (C3D) |
| 저지연 프로토콜 | ENA Express (SRD) | 해당 없음 |
| XDP 지원 | O (v2.6.1+) | O (GQI 모드) |
| 라이선스 | GPL v2 | GPL v2 |
ENA (Elastic Network Adapter) 아키텍처
ENA는 AWS Nitro 하이퍼바이저와 통합된 네트워크 어댑터로, EC2 인스턴스에 고성능 네트워크 I/O를 제공합니다. ENA 디바이스는 Admin Queue를 통한 제어 경로와 I/O Submission/Completion Queue를 통한 데이터 경로를 분리하여 효율적인 패킷 처리를 달성합니다.
아키텍처 개요
ENA 드라이버는 struct ena_adapter를 중심으로 동작합니다.
초기화 시 Admin Queue를 통해 디바이스와 협상하여 지원 기능, 큐 수, MTU 등을 결정합니다.
데이터 전송은 I/O SQ(Submission Queue)에 디스크립터를 기록하고 MMIO doorbell을 울려 디바이스에 통보하는 방식입니다.
완료된 패킷은 CQ(Completion Queue)를 통해 NAPI 폴링으로 처리됩니다.
LLQ (Low Latency Queue) vs Regular Queue
ENA는 두 가지 큐 동작 모드를 지원합니다. Regular 모드에서는 디스크립터와 패킷 데이터가 모두 호스트 메모리에 존재하며, 디바이스가 DMA로 읽어갑니다. LLQ(Low Latency Queue) 모드에서는 디스크립터와 패킷 헤더를 디바이스 메모리(BAR 공간)에 직접 기록하여 DMA 왕복 지연을 제거합니다.
ENA Express (SRD — Scalable Reliable Datagram)
ENA Express는 AWS가 개발한 SRD(Scalable Reliable Datagram) 프로토콜을 활용하여 단일 흐름(single flow)의 대역폭을 최대 25 Gbps까지 확장하고, p99 지연 시간을 크게 줄이는 기능입니다. 전통적인 TCP/UDP는 5-tuple 해싱으로 단일 경로에 바인딩되지만, SRD는 여러 물리 경로에 패킷을 분산하고 수신 측에서 재정렬합니다.
ethtool -S에서 SRD 관련 통계를 확인할 수 있습니다.
인스턴스 타입별 ENA 기능 매트릭스
| 인스턴스 패밀리 | 최대 대역폭 | LLQ | ENA Express | 최대 큐 수 | Enhanced 모니터링 |
|---|---|---|---|---|---|
| m5 / c5 / r5 | 25 Gbps | O | O | 8 | O |
| m6i / c6i / r6i | 50 Gbps | O | O | 16 | O |
| m7g / c7g / r7g | 30 Gbps | O | O | 16 | O |
| m7i / c7i | 50 Gbps | O | O | 32 | O |
| hpc7g | 200 Gbps | O | O | 32 | O |
| p5.48xlarge | 3200 Gbps (EFA) | O | - | 32 | O |
| t3 / t3a | 5 Gbps | O | - | 2 | O |
큐 구성 및 인터럽트 조절
ENA 드라이버는 CPU 수와 디바이스가 보고하는 최대 큐 수 중 작은 값으로 큐를 생성합니다. 각 TX/RX 큐 쌍은 하나의 MSI-X 벡터에 매핑되며, NAPI 인스턴스가 할당됩니다. 적응형 인터럽트 조절(adaptive interrupt moderation)을 통해 처리량과 지연 시간 사이의 균형을 자동으로 조정합니다.
# ENA 큐 구성 확인
ethtool -l eth0
# Channel parameters for eth0:
# Pre-set maximums:
# RX: 0
# TX: 0
# Other: 0
# Combined: 8
# Current hardware settings:
# Combined: 4
# 큐 수 변경 (인스턴스 타입 최대값 이내)
ethtool -L eth0 combined 8
# 적응형 인터럽트 조절 상태 확인
ethtool -c eth0
# Adaptive RX: on TX: on
# 고정 인터럽트 조절로 전환 (지연 최소화)
ethtool -C eth0 adaptive-rx off adaptive-tx off rx-usecs 20 tx-usecs 20
# 링 버퍼 크기 확인 및 조정
ethtool -g eth0
ethtool -G eth0 rx 8192 tx 8192
ENA 메트릭 및 모니터링
ENA 드라이버는 풍부한 통계 카운터를 제공합니다.
ethtool -S로 큐별 패킷/바이트 카운터, 오류 카운터, LLQ 통계, SRD 통계를 확인할 수 있습니다.
# ENA 전체 통계 확인
ethtool -S eth0
# 주요 카운터 필터링
ethtool -S eth0 | grep -E "(tx_bytes|rx_bytes|tx_pkts|rx_pkts)"
# SRD(ENA Express) 통계 확인
ethtool -S eth0 | grep -i srd
# ena_srd_mode: 1
# ena_srd_tx_pkts: 1234567
# ena_srd_eligible_tx_pkts: 2345678
# ena_srd_rx_pkts: 1234000
# ENA 드라이버 버전 확인
ethtool -i eth0
# driver: ena
# version: 2.10.0g
# firmware-version: ...
# dmesg에서 ENA 초기화 로그 확인
dmesg | grep -i ena
# ena 0000:00:05.0: ENA device registered
# ena 0000:00:05.0: LLQ is enabled
# ena 0000:00:05.0: Creating 8 io queues
net.ipv4.tcp_max_syn_backlog),
(4) 적응형 인터럽트 조절을 워크로드에 맞게 튜닝하는 것이 좋습니다.
# ENA 기능 확인 (offload 지원)
ethtool -k eth0 | grep -E "(tx-checksumming|rx-checksumming|scatter-gather|tso|gro|lro)"
# tx-checksumming: on
# rx-checksumming: on
# scatter-gather: on
# tcp-segmentation-offload: on
# generic-receive-offload: on
# XDP 프로그램 로드
ip link set dev eth0 xdpdrv obj xdp_prog.o sec xdp
# XDP 통계 확인
ethtool -S eth0 | grep xdp
# rx_queue_0_xdp_aborted: 0
# rx_queue_0_xdp_drop: 0
# rx_queue_0_xdp_pass: 1234567
# rx_queue_0_xdp_tx: 0
# rx_queue_0_xdp_redirect: 0
ENA Express와 SRD(Scalable Reliable Datagram)
ENA Express는 AWS Nitro 인프라 수준에서 동작하는 SRD(Scalable Reliable Datagram) 프로토콜을 활용합니다. 기존 TCP/UDP는 5-tuple 해싱으로 단일 물리 경로에 고정되지만, SRD는 패킷을 여러 경로에 per-packet spraying 방식으로 분산하여 단일 흐름의 대역폭 상한을 물리 경로 수에 비례해서 높입니다.
Per-packet spraying vs ECMP per-flow hashing: 전통적인 ECMP는 flow 단위로 경로를 결정하므로 단일 TCP 흐름은 하나의 링크로만 전송됩니다. 링크 용량(예: 5 Gbps)이 단일 흐름의 상한이 됩니다. SRD는 개별 패킷을 서로 다른 경로로 분산하므로, 4개 경로가 있을 때 단일 흐름이 이론적으로 4× 대역폭을 사용할 수 있습니다. 실측에서는 5 Gbps 상한이 25 Gbps까지 확장됩니다.
수신 측 재정렬(Receiver-side reordering): 패킷을 여러 경로로 보내면 도착 순서가 뒤섞입니다. SRD는 Nitro Card 수신 측에서 투명하게 재정렬하여 TCP 스택에는 순서가 보장된 스트림이 전달됩니다. TCP 스택이 재정렬을 처리할 필요가 없어 불필요한 재전송/SACK 오버헤드가 발생하지 않습니다.
SRD 비활성화 조건: 다음 상황에서는 SRD가 자동으로 비활성화됩니다. (1) Cross-AZ 트래픽: AZ를 넘어가는 경로에서는 멀티패스 이점이 없으며 재정렬 지연이 발생합니다. (2) 미지원 인스턴스 타입: t3, t3a, c1, m1 등 구세대 인스턴스는 Nitro 기반이 아닙니다. (3) 비 TCP/UDP 트래픽: ICMP, GRE, IPsec 등 다른 프로토콜은 SRD 경로를 사용하지 않습니다. (4) 상대방 미지원: 목적지 인스턴스가 ENA Express를 지원하지 않는 경우.
성능 수치: AWS 공식 벤치마크에서 단일 TCP 흐름 기준으로 최대 대역폭이 5 Gbps → 25 Gbps로 개선되었습니다. p99 지연 시간은 기존 대비 약 50% 감소하며, 이는 경로 혼잡이 발생해도 다른 경로로 즉각 우회하기 때문입니다.
SRD 모니터링: ethtool -S의 ena_srd_eligible_tx_pkts와
ena_srd_tx_pkts 비율로 SRD 활용도를 측정합니다. 비율이 낮으면 cross-AZ 트래픽이 많거나
대상 인스턴스가 ENA Express를 지원하지 않는 것입니다.
# SRD 통계 상세 확인
ethtool -S eth0 | grep -i srd
# ena_srd_mode: 1 ← 1=활성, 0=비활성
# ena_srd_tx_pkts: 1234567 ← 실제 SRD 경로로 전송된 패킷
# ena_srd_eligible_tx_pkts: 2345678 ← SRD 적용 가능했던 총 패킷
# ena_srd_rx_pkts: 1234000 ← SRD 경로로 수신된 패킷
# ena_srd_resource_utilization: 7800 ← SRD 리소스 활용도 (0-10000)
# SRD 활용률 계산 (bash)
SRD_TX=$(ethtool -S eth0 | awk '/ena_srd_tx_pkts:/ {print $2}')
SRD_ELIG=$(ethtool -S eth0 | awk '/ena_srd_eligible_tx_pkts:/ {print $2}')
echo "SRD 활용률: $(( SRD_TX * 100 / SRD_ELIG ))%"
# ENA Express 활성화 (AWS CLI)
aws ec2 modify-network-interface-attribute \
--network-interface-id eni-xxxxxxxxxxxxxxxxx \
--ena-srd-specification \
'{"EnaSrdEnabled":true,"EnaSrdUdpSpecification":{"EnaSrdUdpEnabled":true}}'
# ENA Express 상태 확인
aws ec2 describe-network-interface-attribute \
--network-interface-id eni-xxxxxxxxxxxxxxxxx \
--attribute enaSrdSpecification
ENA 장애 감지 및 복구
ENA 드라이버는 Nitro 인프라와의 통신 장애를 자동으로 감지하고 복구합니다. 핵심은 AdminQ timeout 감지와 keep-alive watchdog의 두 가지 메커니즘입니다.
Keep-alive watchdog: ena_timer_service()가 주기적으로(기본 1초 간격) 실행되며,
디바이스로부터 keep-alive 응답이 6초(6회 타임아웃) 내에 오지 않으면 장애로 판단합니다.
watchdog은 AdminQ를 통해 keep-alive 패킷을 보내고, Nitro Card가 응답하는 방식입니다.
keep-alive 응답 실패는 Nitro Card 재시작 또는 네트워크 인프라 점검 중에 발생할 수 있습니다.
장치 리셋 흐름: 장애가 감지되면 드라이버는 ena_com_dev_reset()을 호출하여
순서대로 (1) 모든 TX/RX 큐 중단, (2) outstanding DMA 완료 대기, (3) AdminQ 리셋 명령 전송,
(4) 큐 해제, (5) 디바이스 재초기화, (6) 큐 재구성을 수행합니다.
이 과정에서 커널 네트워크 스택은 링크 다운(carrier off)을 감지하여 상위 레이어에 알립니다.
큐 불일치 복구(Queue mismatch recovery): 드라이버가 리셋 후 재초기화할 때 디바이스가 이전과 다른 수의 큐를 보고하는 경우(예: Nitro 업데이트 중)가 있습니다. 드라이버는 새로 보고된 큐 수로 재구성하고, RPS/XPS 설정도 함께 재적용합니다.
net.ipv4.tcp_retries2 감소)은 세션이 리셋 전에 끊길 수 있습니다.
완화 방법: (1) multi-path 애플리케이션의 경우 연결 재시도 로직 구현, (2) ENA Enhanced Networking 활성화로
리셋 빈도 최소화, (3) CloudWatch NetworkPacketsOut 지표에 알람 설정.
| 장애 유형 | dmesg 패턴 | 원인 | 대응 |
|---|---|---|---|
| AdminQ 타임아웃 | ena: admin_q timed out | Nitro Card 과부하, 인프라 점검 | 자동 리셋; 반복 시 인스턴스 교체 |
| Keep-alive 실패 | ena: Keep alive watchdog timeout | 호스트 인프라 재시작 | 자동 복구; 반복 시 AWS Support 연락 |
| TX 타임아웃 | ena: TX timeout on queue X | 큐 스톨, HW 버그 | drv 버전 업그레이드 확인 |
| 불량 완료 디스크립터 | ena: Invalid req_id | DMA 오염, 드라이버 버그 | 커널/드라이버 업데이트 |
| 메모리 할당 실패 | ena: Failed to alloc | 호스트 메모리 부족 | 인스턴스 타입 확장 |
# bpftrace로 AdminQ 지연 추적 (1ms 초과 이벤트 출력)
bpftrace -e '
kprobe:ena_com_wait_for_abort_completion {
@start[tid] = nsecs;
}
kretprobe:ena_com_wait_for_abort_completion {
$lat = nsecs - @start[tid];
if ($lat > 1000000) {
printf("AdminQ latency: %d us (pid %d)\n", $lat/1000, pid);
}
delete(@start[tid]);
}'
# ENA 리셋 이벤트 실시간 모니터링
dmesg -w | grep -E "(ena.*reset|ena.*timeout|ena.*watchdog)"
# ENA 드라이버 통계에서 리셋/오류 카운터 확인
ethtool -S eth0 | grep -E "(reset|watchdog|admin)"
# dev_stats_reset_fail: 0
# dev_stats_tx_timeout: 0
# ena_admin_q_pause: 0
# 최근 1시간 이내 ENA 관련 커널 메시지 확인
journalctl -k --since "1 hour ago" | grep -i "ena"
gVNIC (Google Virtual NIC) 아키텍처
gVNIC은 Google Cloud의 차세대 가상 네트워크 어댑터입니다. 기존의 VirtIO 기반 NIC를 대체하여 높은 대역폭과 낮은 지연 시간을 제공합니다. gVNIC은 두 가지 큐 인터페이스를 지원합니다: 기존의 GQI(Google Queue Interface)와 차세대 DQO(Descriptor Queue Organization)입니다.
GQI vs DQO 기능 비교
| 항목 | GQI | DQO |
|---|---|---|
| 디스크립터 형식 | 통합 디스크립터 링 | 분리된 Desc/Compl 큐 |
| 메모리 모델 | QPL (Queue Page List) 공유 메모리 | per-packet DMA 매핑 |
| 완료 처리 | 같은 링에서 완료 확인 | 별도 Completion Queue |
| 버퍼 관리 | 사전 할당된 공유 페이지 풀 | 동적 DMA 버퍼 할당 |
| XDP 지원 | O | O (5.18+) |
| Header Split | X | O |
| 큐당 최대 디스크립터 | 1024 | 4096 |
| 인터럽트 조절 | 기본 | 적응형(Dim) 지원 |
| 지원 인스턴스 | N1, N2, E2, T2D | C3, C3D, H3, A3, Z3 |
| 최대 대역폭 | 100 Gbps | 200 Gbps |
인스턴스 타입별 큐 모델 매핑
| 인스턴스 시리즈 | 큐 모델 | 최대 NIC 수 | 최대 큐 수 | 최대 대역폭 |
|---|---|---|---|---|
| N1 / N2 / N2D | GQI | 8 | 16 | 32 Gbps (100G Tier1) |
| E2 | GQI | 8 | 8 | 16 Gbps |
| T2D | GQI | 8 | 8 | 32 Gbps |
| C3 / C3D | DQO | 15 | 16 | 200 Gbps (C3D) |
| H3 | DQO | 8 | 16 | 200 Gbps |
| A3 | DQO | 15 | 16 | 200 Gbps |
| Z3 | DQO | 15 | 16 | 100 Gbps |
gVNIC 설정 및 튜닝
# gVNIC 드라이버 정보 확인
ethtool -i eth0
# driver: gve
# version: 1.4.0-K
# firmware-version: GVE-PROD-1.4.0
# 큐 모델 확인 (dmesg)
dmesg | grep gve
# gve 0000:00:04.0: GVE version: 1.4.0
# gve 0000:00:04.0: Queue format: DQO
# 큐 수 확인 및 변경
ethtool -l eth0
ethtool -L eth0 combined 16
# 링 버퍼 크기 조정
ethtool -G eth0 rx 4096 tx 4096
# offload 기능 확인
ethtool -k eth0 | grep -E "(checksum|segmentation|offload|gro)"
# 통계 확인 (큐별)
ethtool -S eth0 | head -40
# tx_queue_0_packets: 123456
# tx_queue_0_bytes: 78901234
# rx_queue_0_packets: 234567
# rx_queue_0_bytes: 89012345
# RPS 설정 (DQO 모드에서도 유용)
for i in /sys/class/net/eth0/queues/rx-*/rps_cpus; do
echo "ff" > "$i"
done
# XPS 설정
for i in $(seq 0 7); do
echo "$(( 1 << i ))" > /sys/class/net/eth0/queues/tx-$i/xps_cpus
done
gcloud compute instances create 시 --network-interface=nic-type=GVNIC을 명시해야 합니다.
DQO Header Split
DQO 모드의 핵심 최적화 중 하나는 Header Split입니다. 수신 패킷의 헤더와 페이로드를 서로 다른 메모리 풀에 배치하여 CPU 캐시 효율을 극대화합니다. 헤더는 L1/L2 캐시에 들어오는 작은 버퍼 풀에, 페이로드는 L3/LLC에 남아있는 큰 페이지 풀에 할당됩니다.
캐시 최적화 원리: 네트워크 스택은 패킷 처리 초기에 헤더(IP, TCP 등 ~80바이트)만 접근합니다. Header Split 없이 전체 패킷(헤더+페이로드)이 한 버퍼에 있으면, 페이로드 데이터가 L1/L2 캐시를 오염시켜 헤더 접근 지연이 증가합니다. 분리하면 헤더만 캐시에 올라와 처리 속도가 향상됩니다.
메모리 레이아웃: 헤더 버퍼 풀은 256B 크기의 작은 버퍼를 사전 할당하여 관리합니다.
데이터 버퍼 풀은 2K~4K 페이지 크기로 page_pool을 통해 관리됩니다. 소형 패킷
워크로드에서 memcpy 감소와 캐시 지역성 향상으로 약 15%의 처리 성능 향상이
측정됩니다.
QPL vs DQO 메모리 모델
QPL(Queue Page List)은 GQI 모드에서 사용하는 메모리 모델로, GCE 하이퍼바이저의 DMA 제약을 우회하기 위해 도입되었습니다. 게스트 VM이 특정 메모리 페이지를 하이퍼바이저와 공유 등록하여 bounce buffer로 사용합니다. 이 구조는 호스트-게스트 간 DMA 직접 매핑이 어려운 환경에서 안정적이지만 메모리 오버헤드가 큽니다.
QPL 메모리 크기 계산: QPL이 사용하는 메모리는
ring_size × max_packet_size로 계산됩니다. 예를 들어 ring_size=1024이고
max_packet_size=9000(jumbo frame)인 경우 큐당 약 9 MB, 16개 TX/RX 큐 쌍이면
총 ~288 MB가 QPL로 예약됩니다.
DQO가 QPL을 제거한 이유: DQO는 page_pool 기반의 직접 DMA 매핑을
사용합니다. 하이퍼바이저가 DQO를 지원하면 bounce buffer 없이 게스트 메모리에 직접 DMA 쓰기가
가능합니다. 이로 인해 QPL의 메모리 낭비가 사라지고, 불필요한 데이터 복사도 제거됩니다.
# gVNIC 큐 모드 확인 (DQO vs GQI)
dmesg | grep -i "queue format\|gve.*dqo\|gve.*gqi"
# gve 0000:00:04.0: Queue format: DQO ← DQO 모드
# gve 0000:00:04.0: Queue format: GQI ← GQI 모드
# Header Split 지원 여부 확인
ethtool -k eth0 | grep "header-split"
# rx-header-split: on [fixed] ← DQO 모드에서 활성
# QPL 메모리 사용량 확인 (GQI 모드)
ethtool -S eth0 | grep qpl
# tx_qpl_allocated: 16 ← TX QPL 페이지 수
# rx_qpl_allocated: 16 ← RX QPL 페이지 수
# DQO page_pool 통계 확인
ethtool -S eth0 | grep -E "(rx_buf|page_pool|alloc)"
# rx_buf_alloc_fail: 0 ← 버퍼 할당 실패 없음
# 메모리 사용 비교 (QPL vs DQO)
# QPL 예시: ring=1024, mtu=9000 → 1024*9000*2(TX+RX)=~18MB/큐
# DQO: page_pool 동적 할당, 사용 중인 패킷만 메모리 점유
클라우드 NIC 장애 복구 비교
세 가지 주요 클라우드 NIC(ENA, gVNIC, hv_netvsc)는 서로 다른 장애 감지 및 복구 메커니즘을 가집니다. 실제 운영 환경에서 장애 대응 전략을 수립할 때 각 NIC의 특성을 이해하는 것이 중요합니다.
| 항목 | ENA (AWS) | gVNIC (GCP) | hv_netvsc (Azure) |
|---|---|---|---|
| 장애 감지 방식 | AdminQ keep-alive watchdog | Admin queue + notify block polling | VMBus 채널 heartbeat |
| Keep-alive 주기 | 1초 간격, 6초 타임아웃 | 2초 간격, 10초 타임아웃 | VMBus 자체 heartbeat (~5초) |
| 복구 메커니즘 | ena_com_dev_reset() 호출 |
gve_reset() workqueue |
netvsc_channel_cb() 재연결 |
| 큐 재구성 | 전체 큐 해제 후 재생성 | 큐 tear-down + re-register | VMBus 채널 재열기 + SR-IOV VF 재연결 |
| 다운타임 (일반) | 1~3초 | 2~5초 | 1~4초 (SR-IOV 시 <1초) |
| VF failover | 해당 없음 (EFA 별도) | 해당 없음 | SR-IOV VF → netvsc synthetic 자동 failover |
| 모니터링 커맨드 | ethtool -S eth0 | grep ena_admin |
ethtool -S eth0 | grep tx_timeout |
ethtool -S eth0 | grep ring_full |
ethtool -S의 오류 카운터를 1분 주기로 수집하여 이상 증가 시 알람 설정.
(2) ENA: CloudWatch NetworkPacketsOut=0 조건 알람으로 NIC 완전 중단 감지.
(3) gVNIC: GCP 인스턴스의 compute.googleapis.com/instance/network/received_packets_count
지표 모니터링으로 재시작 패턴 식별.
(4) hv_netvsc: Azure Monitor의 Network In/Out 지표와 dmesg의
hv_netvsc 메시지를 조합하여 VF failover 횟수 추적.
가상화 NIC: virtio_net / vmxnet3 / hv_netvsc
| 항목 | virtio_net | vmxnet3 | hv_netvsc |
|---|---|---|---|
| 하이퍼바이저 | KVM / QEMU | VMware ESXi | Hyper-V / Azure |
| 모듈명 | virtio_net | vmxnet3 | hv_netvsc |
| 소스 위치 | drivers/net/virtio_net.c | drivers/net/vmxnet3/ | drivers/net/hyperv/ |
| 전송 인터페이스 | virtqueue (vring) | 공유 메모리 큐 | VMBus + VF |
| 최대 큐 수 | 호스트 설정 의존 | 32 (vHW v4+) | 64 (RSS) |
| SR-IOV VF 통합 | X (별도 구성) | UPT passthrough | O (Accelerated Networking) |
| XDP 지원 | O (5.0+) | X | O (5.6+) |
| 라이선스 | GPL v2 | GPL v2 | GPL v2 |
virtio_net
virtio_net은 OASIS virtio 표준(1.0/1.1/1.2)에 기반한 반가상화 네트워크 드라이버입니다.
게스트 커널의 virtio_net 드라이버와 호스트의 백엔드(QEMU, vhost-net, vhost-user) 사이에
virtqueue라는 공유 링 버퍼를 통해 패킷을 교환합니다.
이 구조는 불필요한 VM exit를 최소화하고, 배치 처리와 인터럽트 합산을 통해
높은 처리량을 달성합니다.
virtqueue 링 구조
각 virtqueue는 세 개의 영역으로 구성됩니다: Descriptor Table(버퍼 주소/크기/플래그), Available Ring(게스트→호스트), Used Ring(호스트→게스트). 게스트가 패킷을 전송하려면 Descriptor Table에 버퍼를 등록하고 Available Ring에 인덱스를 추가한 후 호스트에 통보(kick)합니다. 호스트가 처리를 완료하면 Used Ring에 인덱스를 기록하고 인터럽트를 발생시킵니다.
Feature Negotiation
| Feature Bit | 이름 | 설명 | 성능 영향 |
|---|---|---|---|
| 0 | VIRTIO_NET_F_CSUM | 호스트가 체크섬 오프로드 지원 | TX CPU 부하 감소 |
| 1 | VIRTIO_NET_F_GUEST_CSUM | 게스트가 체크섬 오프로드 처리 | RX CPU 부하 감소 |
| 5 | VIRTIO_NET_F_MAC | 디바이스가 MAC 주소 제공 | - |
| 11 | VIRTIO_NET_F_HOST_TSO4 | 호스트가 TCP Segmentation Offload 지원 | 대역폭 증가 |
| 15 | VIRTIO_NET_F_MRG_RXBUF | Mergeable RX Buffers | 대형 패킷 효율 향상 |
| 17 | VIRTIO_NET_F_STATUS | 링크 상태 알림 | - |
| 18 | VIRTIO_NET_F_CTRL_VQ | Control Virtqueue 지원 | 동적 설정 변경 |
| 22 | VIRTIO_NET_F_MQ | 멀티큐 지원 | 멀티코어 스케일링 |
| 25 | VIRTIO_NET_F_SPEED_DUPLEX | 속도/이중 모드 설정 | - |
| 34 | VIRTIO_F_ORDER_PLATFORM | 플랫폼 메모리 순서 사용 | 배리어 오버헤드 감소 |
| 39 | VIRTIO_F_RING_PACKED | Packed Virtqueue (1.1) | 캐시 효율 향상 |
Mergeable Buffers와 멀티큐 구성
Mergeable Buffers(VIRTIO_NET_F_MRG_RXBUF)는 수신 패킷이 단일 버퍼에
맞지 않을 때 여러 디스크립터의 버퍼를 합쳐서 하나의 패킷으로 조립하는 기능입니다.
점보 프레임이나 GRO에 의해 합쳐진 대형 패킷 처리에 필수적입니다.
mergeable이 비활성화되면 각 디스크립터가 MTU 크기 버퍼를 가져야 하므로 메모리 낭비가 발생합니다.
# virtio_net feature 확인
cat /sys/bus/virtio/devices/virtio0/features
# 또는 ethtool로 확인
ethtool -i eth0
# driver: virtio_net
# 멀티큐 설정 (QEMU 측: -device virtio-net-pci,mq=on,vectors=10)
# 게스트에서 큐 수 확인
ethtool -l eth0
# Pre-set maximums:
# Combined: 8
# Current hardware settings:
# Combined: 4
# 멀티큐 활성화
ethtool -L eth0 combined 8
# 큐별 통계 확인
ethtool -S eth0 | grep -E "^(tx|rx)_queue"
# IRQ affinity 확인
grep virtio /proc/interrupts
# mergeable buffer 동작 확인
ethtool -k eth0 | grep gro
# generic-receive-offload: on
XDP 지원
virtio_net은 Linux 5.0부터 XDP를 지원합니다.
XDP_TX, XDP_REDIRECT, XDP_DROP, XDP_PASS 모든 액션이 지원되며,
XDP_TX의 경우 전용 SQ를 할당하여 일반 TX 경로와 간섭을 방지합니다.
vhost-net 백엔드 사용 시 XDP와 함께 제로 카피 수신이 가능하여 높은 성능을 달성합니다.
# XDP 프로그램 로드 (native mode)
ip link set dev eth0 xdpdrv obj xdp_drop.o sec xdp
# XDP 프로그램 확인
ip link show eth0
# ... xdp/id:42 ...
# XDP 통계 확인
ethtool -S eth0 | grep xdp
# rx_queue_0_xdp_packets: 123456
# rx_queue_0_xdp_drops: 0
# rx_queue_0_xdp_redirects: 0
# XDP 프로그램 제거
ip link set dev eth0 xdp off
Guest-Host 최적화 팁
- vhost-net 활성화: QEMU의
-netdev tap,vhost=on으로 커널 vhost 사용 - 멀티큐: vCPU 수에 맞춰 큐 수 설정 (
mq=on,queues=N) - Mergeable buffers: 점보 프레임 사용 시 필수 활성화
- Packed Virtqueue: QEMU 6.0+ / 커널 5.0+에서
packed=on - Hugepages: 게스트 메모리에 hugepage 할당으로 TLB miss 감소
- vIOMMU 비활성화: 불필요한 IOMMU 오버헤드 제거
- CPU pinning: vCPU를 물리 CPU에 고정하여 캐시 효율 향상
Packed Virtqueue
virtio 1.1에서 도입된 Packed Virtqueue는 기존 Split Virtqueue의 메모리 레이아웃을 근본적으로 재설계했습니다.
| 항목 | Split Virtqueue | Packed Virtqueue |
|---|---|---|
| 메모리 영역 | 3개 분리 (Desc Table + Avail Ring + Used Ring) | 단일 Descriptor 배열 |
| 소유권 표시 | 별도 인덱스 카운터 | wrap counter 비트 (AVAIL/USED 플래그) |
| 캐시 라인 접근 | 여러 메모리 영역 순회 → 캐시 미스 높음 | 단일 배열 순차 접근 → 캐시 미스 ~30% 감소 |
| Feature bit | 기본 (협상 불필요) | VIRTIO_F_RING_PACKED 협상 필요 |
| 지원 커널 | 모든 virtio 커널 | Linux 5.0+, QEMU 4.2+ |
Packed Virtqueue에서 wrap counter는 드라이버와 디바이스가 링을 한 바퀴 돌 때마다
0↔1로 토글되는 1비트 카운터입니다.
디스크립터의 AVAIL/USED 플래그 조합이
현재 wrap counter 값과 일치할 때 해당 디스크립터를 처리 가능 상태로 인식합니다.
이 방식은 별도의 Available Ring과 Used Ring 없이도 소유권을 추적할 수 있어
포인터 체이싱 없는 선형 메모리 접근이 가능합니다.
# Packed Virtqueue 활성화 확인 (QEMU 6.0+ 게스트)
cat /sys/bus/virtio/devices/virtio0/features | tr ' ' '\n' | grep -c "39"
# 1이면 VIRTIO_F_RING_PACKED 협상 성공
# QEMU 명령줄에서 Packed Virtqueue 활성화
# -device virtio-net-pci,packed=on,mq=on,vectors=10
# 게스트에서 virtio 기능 비트 확인
cat /sys/bus/virtio/devices/virtio0/features
vDPA(virtio Data Path Acceleration)
vDPA는 하드웨어가 virtio 데이터 경로를 직접 구현하여
QEMU 유저스페이스 처리 오버헤드를 완전히 제거하는 아키텍처입니다.
mlx5_vdpa 드라이버는 ConnectX-6 이상에서 하드웨어 virtio 데이터 경로를 제공합니다.
| 항목 | virtio-net (QEMU) | vhost-net (커널) | vDPA (mlx5) |
|---|---|---|---|
| 데이터 경로 | QEMU 유저스페이스 | 커널 vhost 스레드 | NIC 하드웨어 직접 |
| VM exit 횟수 | 많음 | 적음 | 최소 (거의 없음) |
| 처리량 (100G 기준) | ~40 Gbps | ~70 Gbps | ~95 Gbps |
| CPU 오버헤드 | 높음 (userspace 처리) | 중간 (kernel thread) | 낮음 (HW 처리) |
| 라이브 마이그레이션 | 지원 | 지원 | 제한적 (FW 지원 필요) |
| 관리 인터페이스 | QEMU QMP | ioctl(vhost) | vDPA bus + devlink |
vDPA 아키텍처는 세 계층으로 구성됩니다: vDPA bus(커널 버스 추상화), vDPA device(mlx5_vdpa 등 하드웨어 드라이버), mediator driver(virtio-vdpa 또는 vhost-vdpa). mediator가 virtio 프로토콜 변환을 담당하므로 게스트 드라이버 수정 없이 기존 virtio_net 드라이버와 완전히 호환됩니다.
vmxnet3
vmxnet3는 VMware가 설계한 3세대 반가상화 네트워크 어댑터입니다. ESXi 하이퍼바이저의 vmkernel과 긴밀하게 통합되어, 에뮬레이션 오버헤드 없이 고성능 네트워크 I/O를 제공합니다. vmxnet3는 공유 메모리 기반의 TX/RX 큐, RSS(Receive Side Scaling), LRO(Large Receive Offload), 적응형 인터럽트 합산을 지원합니다.
vHW 버전별 기능 매트릭스
| 기능 | vHW v1 | vHW v2 | vHW v3 | vHW v4 | vHW v5 | vHW v6 | vHW v7 |
|---|---|---|---|---|---|---|---|
| ESXi 최소 버전 | 5.5 | 6.0 | 6.5 | 6.7 | 7.0 | 7.0 U2 | 8.0 |
| 최대 TX/RX 큐 | 8/8 | 8/8 | 8/8 | 32/32 | 32/32 | 32/32 | 32/32 |
| RSS | O | O | O | O | O | O | O |
| LRO | O | O | O | O | O | O | O |
| Coalescing 세분화 | 기본 | 기본 | 확장 | 확장 | 확장 | 확장 | 확장 |
| UPT (SR-IOV) | X | O | O | O | O | O | O |
| Timestamping | X | X | X | X | O | O | O |
| Uniform Passthrough v2 | X | X | X | X | X | X | O |
| Large TX desc | X | X | X | O | O | O | O |
vmxnet3 설정 및 튜닝
# vmxnet3 드라이버 정보 확인
ethtool -i eth0
# driver: vmxnet3
# version: 1.7.0.0-k
# firmware-version: ...
# 큐 설정 확인
ethtool -l eth0
# Combined: 8
# Coalescing 설정 확인 및 조정
ethtool -c eth0
# rx-usecs: 50
# rx-frames: 64
# 낮은 지연 시간 모드
ethtool -C eth0 rx-usecs 10 rx-frames 16
# 높은 처리량 모드
ethtool -C eth0 rx-usecs 100 rx-frames 128
# LRO 활성화/비활성화
ethtool -K eth0 lro on
ethtool -K eth0 lro off # 라우팅/브리징 시 비활성화 권장
# TSO 확인
ethtool -k eth0 | grep segmentation
# 링 버퍼 크기 조정
ethtool -G eth0 rx 4096 tx 4096
# RSS 해시 키 설정 확인
ethtool -x eth0
UPT(Uniform Passthrough) 아키텍처
UPT(Uniform Passthrough)는 VMware가 설계한 기술로, ESXi vmkernel을 우회하여 SR-IOV VF로부터 게스트가 패킷을 직접 수신하는 경로입니다. UPT가 활성화되면 vmkernel의 가상 스위치(vSwitch/DVS)를 거치지 않고 물리 NIC의 VF가 게스트 메모리에 직접 DMA합니다.
UPT vs SR-IOV 차이: SR-IOV는 PCIe 표준 기술이며 하이퍼바이저 중립적입니다. 반면 UPT는 VMware 전용 구현으로, ESXi vmkernel이 VF 할당, 링크 상태, 라이브 마이그레이션을 중앙에서 조율합니다. 게스트 드라이버(vmxnet3) 관점에서는 경로 전환이 투명하게 처리됩니다.
| 비교 항목 | UPT (VMware) | SR-IOV (일반) |
|---|---|---|
| 관리 주체 | ESXi vmkernel 중앙 관리 | 하이퍼바이저 독립 / PF 드라이버 |
| 라이브 마이그레이션 | 지원 (VF → emulation 폴백) | 제한적 (하드웨어 종속) |
| 성능 | vmkernel 우회, 저지연 | 최고 수준 (커널 패스 없음) |
| 호환성 | vmxnet3 드라이버 필수 | VF 드라이버 필요 (e.g., ixgbevf) |
| NIC 요구사항 | VMware 인증 물리 NIC | SR-IOV 지원 NIC 전체 |
| 투명성 | 게스트에 완전 투명 | 게스트가 VF 디바이스 직접 인식 |
hv_netvsc
hv_netvsc는 Microsoft Hyper-V 및 Azure 환경의 반가상화 네트워크 드라이버입니다. VMBus를 통한 합성(synthetic) 경로와 SR-IOV VF를 통한 가속 네트워킹(Accelerated Networking) 경로를 단일 네트워크 인터페이스 아래에서 투명하게 관리하는 것이 핵심 특징입니다. VF가 있으면 데이터 패킷은 VF를 통해 직접 전송되고, VF가 제거되면 자동으로 합성 경로로 폴백합니다.
Accelerated Networking 아키텍처: Azure의 Accelerated Networking은 Mellanox(ConnectX) 또는
MANA(Microsoft Azure Network Adapter) VF를 VM에 직접 할당합니다.
hv_netvsc는 VF 드라이버(mlx5_core 또는 mana)를 netvsc의 하위 디바이스로 등록하여,
상위 계층에서는 단일 인터페이스(eth0)만 보이도록 합니다.
이 투명한 본딩(bonding) 덕분에 라이브 마이그레이션 시에도 네트워크 연결이 유지됩니다.
VF Failover 매커니즘
hv_netvsc의 핵심 기능 중 하나는 VF failover입니다. Azure 호스트는 라이브 마이그레이션, 호스트 유지보수, 또는 기타 이유로 VF를 동적으로 제거하고 재할당할 수 있습니다. hv_netvsc는 이 과정을 상위 계층에 투명하게 처리합니다: VF가 제거되면 자동으로 합성 경로로 전환하고, VF가 다시 제공되면 가속 경로로 복귀합니다.
Accelerated Networking 상세
Azure의 Accelerated Networking은 SR-IOV를 기반으로 하드웨어 데이터 경로를 VM에 직접 노출합니다. 이를 통해 합성 경로 대비 지연 시간이 약 10배 개선되고(~100μs → ~25μs), CPU 사용률이 크게 감소합니다. D/E/F/G/M 시리즈의 대부분의 인스턴스 크기(2+ vCPU)에서 지원됩니다.
| 항목 | 합성 경로 (VMBus) | Accelerated Networking (VF) |
|---|---|---|
| 데이터 경로 | VM → VMBus → Host vSwitch → NIC | VM → VF → NIC (호스트 우회) |
| 지연 시간 (p50) | ~100 μs | ~25 μs |
| 최대 대역폭 | 인스턴스 한도 | 인스턴스 한도 |
| CPU 사용률 | 높음 (호스트 경유) | 낮음 (직접 전달) |
| 라이브 마이그레이션 | 항상 가능 | VF 폴백 후 가능 |
| VF 드라이버 | 불필요 | mlx5_core / mana |
채널/큐 관리
hv_netvsc는 VMBus 채널을 통해 멀티큐를 지원합니다. 각 VMBus 채널은 하나의 CPU에 매핑되며, send/receive 링 버퍼로 구성됩니다. VF가 활성화된 상태에서도 합성 채널은 유지되며, 제어 메시지(RNDIS 제어, 링크 상태 변경 등)에 사용됩니다.
# hv_netvsc 드라이버 정보 확인
ethtool -i eth0
# driver: hv_netvsc
# version: (kernel)
# VF 상태 확인 (Accelerated Networking)
lspci | grep -i "virtual function\|mellanox\|mana"
# 0001:00:02.0 Ethernet controller: Mellanox ConnectX-4 Lx Virtual Function
# netvsc와 VF의 관계 확인
ls -la /sys/class/net/eth0/lower_*
# lower_enP1s0f0 → VF 인터페이스
# 채널 수 확인
ethtool -l eth0
# Combined: 8
# VMBus 링 버퍼 크기 확인
cat /sys/bus/vmbus/devices/*/ring_buffer_size 2>/dev/null | head -5
# 합성 경로 통계
ethtool -S eth0 | grep -E "(vf_|tx_send_full|rx_comp_busy)"
# vf_rx_packets: 12345678
# vf_rx_bytes: 1234567890
# vf_tx_packets: 23456789
# vf_tx_bytes: 2345678901
# tx_send_full: 0
# 채널 수 변경
ethtool -L eth0 combined 16
# RSS 해시 함수 확인
ethtool -x eth0
# 인터럽트 분배 확인
grep -i hyperv /proc/interrupts
Azure 환경 고려사항
- MTU: Azure VNet 기본 MTU는 1500입니다. Jumbo Frame(MTU 9000)을 사용하려면 VM과 서브넷 수준에서 모두 설정해야 합니다.
- NSG 규칙: Accelerated Networking에서도 NSG(Network Security Group) 규칙은 Azure vSwitch의 VFP(Virtual Filtering Platform)에서 적용됩니다.
- 라이브 마이그레이션: VF 폴백 시 수 초간 합성 경로로 전환되며, TCP 연결은 유지되지만 지연 시간이 일시적으로 증가합니다.
- DPDK: Azure DPDK는 netvsc PMD를 통해 VF에 직접 접근합니다.
--vdev=net_vdev_netvsc0옵션을 사용합니다.
# Accelerated Networking 활성화 확인 (Azure CLI)
az vm show -g myRG -n myVM --query "networkProfile.networkInterfaces[].enableAcceleratedNetworking"
# VM 내부에서 VF 존재 확인
lspci | grep -i virtual
# 출력이 있으면 Accelerated Networking 활성 상태
# VF failover 이벤트 확인 (dmesg)
dmesg | grep -i "netvsc\|vf\|failover"
# hv_netvsc vmbus_0: VF registering: eth1
# hv_netvsc vmbus_0: Data path switched to VF: eth1
# 링 버퍼 크기 튜닝 (합성 경로 성능 향상)
ethtool -G eth0 rx 4096 tx 4096
# XDP 프로그램 로드 (hv_netvsc XDP 지원, 5.6+)
ip link set dev eth0 xdpdrv obj xdp_prog.o sec xdp
# MANA VF 상태 확인 (최신 Azure VM)
dmesg | grep mana
# mana 7870:00:02.0: MANA device probed successfully
dmesg에서 "Data path switched" 메시지를 확인하세요.
합성 경로의 성능이 예상보다 낮다면 VMBus 링 버퍼 크기를 늘리고(ethtool -G),
채널 수가 vCPU 수와 일치하는지 확인하세요.
Azure 직렬 콘솔(az serial-console connect)을 통해 네트워크 단절 시에도 VM에 접근할 수 있습니다.
제조사별 공통 디버깅 루틴
- 드라이버/펌웨어 버전 고정
ethtool -i결과를 티켓/배포 메타데이터에 저장 - 벤더 통계 키 추출
ethtool -S에서 reset/drop/queue 계열 카운터를 표준화 - link/queue 이벤트 타임라인화
dmesg, devlink health, orchestrator 이벤트를 같은 타임라인으로 병합 - fallback 경로 검증
오프로드 비활성화 후 재현 여부를 확인해 HW/드라이버/스택 원인을 분리 - 벤더별 재현 스크립트 유지
링크 flap, reset storm, queue resize, offload toggle 시나리오를 자동화
드라이버 간 종합 비교
리눅스 커널에는 수십 개의 네트워크 디바이스 드라이버가 존재하며, 각각 고유한 아키텍처 결정과 최적화 전략을 채택하고 있습니다. 이 섹션에서는 주요 드라이버를 기능, 아키텍처, 큐 모델, 인터럽트 처리, 메모리 관리(Memory Management) 등 다양한 축으로 비교 분석합니다. 드라이버 선택 시 워크로드 특성에 따른 최적 선택을 돕기 위한 종합 레퍼런스입니다.
전체 기능 매트릭스
아래 표는 리눅스 커널 6.x 기준 주요 네트워크 드라이버의 핵심 기능 지원 현황을 정리한 것입니다. ✓는 완전 지원, △는 부분 지원 또는 제한적 지원, ✗는 미지원을 나타냅니다.
| 드라이버 | 벤더/칩 | XDP Native | AF_XDP ZC | SR-IOV (max VFs) | tc flower offload | devlink | RDMA | PTP HW TS | page_pool | BQL |
|---|---|---|---|---|---|---|---|---|---|---|
ice |
Intel E810 | ✓ | ✓ | ✓ (256) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
mlx5e |
Mellanox/NVIDIA CX-5/6/7 | ✓ | ✓ | ✓ (1024) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
bnxt_en |
Broadcom NetXtreme-E | ✓ | ✓ | ✓ (512) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
cxgb4 |
Chelsio T4/T5/T6 | △ | ✗ | ✓ (128) | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ |
ena |
Amazon ENA | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
gve |
Google gVNIC | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
virtio_net |
QEMU/KVM virtio | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
vmxnet3 |
VMware VMXNET3 | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
hv_netvsc |
Hyper-V NetVSC | △ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
r8169 |
Realtek RTL8111/8168 | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
atlantic |
Aquantia AQC | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ |
mvpp2 |
Marvell PPv2 | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ |
dpaa2 |
NXP DPAA2 (LX2160A) | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ | ✓ | ✓ | ✓ |
mlx5e의 tc flower offload는 수백 개의 매치 필드를 지원하는 반면,
다른 드라이버는 기본적인 5-tuple 매칭만 지원할 수 있습니다. 프로덕션 배포 전 반드시 해당 드라이버의
구체적인 기능 범위를 검증해야 합니다.
각 드라이버의 고급 기능 지원 상세 비교입니다.
| 드라이버 | XDP Multi-buffer | XDP HW Offload | switchdev | TLS HW Offload | RSS Hash Configurable | Flow Director | Queue Resize (online) |
|---|---|---|---|---|---|---|---|
ice |
✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✓ |
mlx5e |
✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ |
bnxt_en |
✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ |
cxgb4 |
✗ | ✗ | ✗ | ✓ | △ | ✓ | ✗ |
ena |
✗ | ✗ | ✗ | ✗ | △ | ✗ | ✓ |
gve |
✗ | ✗ | ✗ | ✗ | ✗ | ✗ | △ |
virtio_net |
✓ | ✗ | ✗ | ✗ | △ | ✗ | ✓ |
vmxnet3 |
✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
hv_netvsc |
✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
r8169 |
✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
atlantic |
✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ |
mvpp2 |
✗ | ✗ | ✗ | ✗ | △ | ✗ | ✗ |
dpaa2 |
✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
기능 성숙도 계층
기능 지원 여부(✓/✗)만으로는 드라이버의 실제 준비 상태를 평가하기 어렵습니다. 같은 ✓라도 프로덕션 배포에서 검증된 기능과 최근 추가된 실험적 구현 사이에는 큰 차이가 있습니다.
- Production: 메인라인 포함 2년 이상, 광범위하게 배포되어 사용 중, 활발히 유지보수됨
- Stable: 메인라인 포함, 기본 테스트 완료, 일부 프로덕션 배포 사례 있음
- Experimental: 최근 추가되었거나 테스트가 제한적임, API 변경 가능성 있음
- Stub: 최소 구현만 존재, 프로덕션 미적합
| 드라이버 | XDP basic | AF_XDP ZC | tc flower | SR-IOV | devlink | page_pool | HW timestamping |
|---|---|---|---|---|---|---|---|
mlx5e |
Production (5.2) | Production (5.5) | Production (4.18) | Production (4.14) | Production (5.2) | Production (5.9) | Production (4.10) |
ice |
Production (5.5) | Production (5.10) | Production (5.7) | Production (5.5) | Production (5.5) | Production (5.14) | Production (5.5) |
bnxt_en |
Production (5.9) | Production (5.12) | Stable (5.8) | Production (4.6) | Production (5.4) | Stable (5.18) | Production (4.6) |
ena |
Stable (5.6) | Stub (—) | Stub (—) | Stub (—) | Stub (—) | Production (5.12) | Stub (—) |
gve |
Stable (5.16) | Stub (—) | Stub (—) | Stub (—) | Stub (—) | Stable (5.16) | Stub (—) |
r8169 |
Stub (—) | Stub (—) | Stub (—) | Stub (—) | Stub (—) | Production (5.10) | Stub (—) |
atlantic |
Stable (5.4) | Stub (—) | Stub (—) | Stub (—) | Stub (—) | Stable (5.11) | Stable (5.4) |
virtio_net |
Production (4.12) | Stub (—) | Stub (—) | Stub (—) | Stub (—) | Stable (5.19) | Stub (—) |
vmxnet3 |
Stable (5.5) | Stub (—) | Stub (—) | Stub (—) | Stub (—) | Stub (—) | Stub (—) |
cxgb4 |
Experimental (5.12) | Stub (—) | Stable (4.18) | Production (3.12) | Stub (—) | Stub (—) | Production (4.4) |
mlx5e는 200개 이상의 매치 필드(VLAN, MPLS, TCP 플래그, IP TOS 등)를 tc flower offload로 지원하는 반면,
ena는 tc flower offload 지원이 전혀 없습니다. 같은 "지원"이라도 실제 적용 범위가 크게 다를 수 있으므로,
프로덕션 배포 전에 반드시 대상 드라이버의 구체적인 offload 범위를 tc filter show dev <intf> ingress와 커널 소스에서 검증하십시오.
아키텍처 패턴 비교
네트워크 드라이버의 아키텍처는 크게 중앙 집중형(Centralized)과 분산형(Distributed)으로 분류할 수 있습니다. 중앙 집중형은 펌웨어 컨트롤러가 패킷 처리 파이프라인 전체를 관장하며, 드라이버는 펌웨어에 명령을 전달하는 역할에 그칩니다. 반면 분산형은 하드웨어 파이프라인이 고정된 기능 블록으로 구성되고, 드라이버가 각 블록을 직접 프로그래밍합니다.
아키텍처 선택은 드라이버의 복잡도, 디버깅 용이성, 기능 확장성에 직접적인 영향을 미칩니다.
| 특성 | 중앙 집중형 (Firmware-Centric) | 분산형 (Hardware Pipeline) |
|---|---|---|
| 드라이버 복잡도 | 낮음 — 펌웨어 API 호출 위주 | 높음 — HW 레지스터 직접 관리 |
| 디버깅 난이도 | 높음 — 펌웨어 내부 상태 불투명 | 낮음 — 레지스터 덤프로 상태 확인 가능 |
| 기능 확장 | 펌웨어 업데이트 필요 (벤더 의존) | 드라이버 코드 변경으로 가능 |
| 초기화 시간 | 길음 — 펌웨어 로딩/핸드셰이크 필요 | 짧음 — 레지스터 직접 설정 |
| 에러 복구 | 펌웨어 리셋 필요 (전체 중단 가능) | 세분화된 복구 가능 (큐 단위) |
| 대표 드라이버 | bnxt_en, ena, gve, i40e | mlx5e, ixgbe, ice (하이브리드) |
Control plane 설계 패턴도 드라이버마다 크게 다릅니다.
| 패턴 | 설명 | 적용 드라이버 |
|---|---|---|
| Mailbox Command | 공유 메모리 메일박스를 통해 펌웨어와 명령/응답 교환 | bnxt_en (HWRM), ena (Admin Queue) |
| Admin Queue | 전용 관리 큐를 통한 비동기 명령 전달 | ice (AQ), i40e (AQ), idpf |
| Command Interface | 레지스터 기반 명령 인터페이스로 HW 직접 제어 | mlx5e (cmdif), ixgbe |
| Hypervisor Channel | VMBus/virtqueue를 통한 호스트 드라이버 통신 | hv_netvsc (VMBus), virtio_net (virtqueue) |
아키텍처 설계 결정의 이유
각 드라이버가 특정 아키텍처를 선택한 데는 하드웨어 설계, 타깃 시장, 성능 목표, 소프트웨어 생태계 등 복합적인 이유가 있습니다. 단순한 "좋은 설계 vs 나쁜 설계"가 아니라, 각자의 제약 조건 안에서 최적화된 선택입니다.
mlx5e가 TX와 RX에 각각 독립된 CQ(Completion Queue)를 두는 이유는 InfiniBand/RDMA에서 비롯됩니다. RDMA 연산은 Send/Receive/RDMA Read/Write 4가지 동작이 모두 동일한 CQ에 완료 이벤트를 올려야 하기 때문에, 처음부터 "작업 큐(WQ) ↔ 완료 큐(CQ) 1:1" 구조로 설계되었습니다. 이 구조 덕분에 mlx5e는 NIC와 RDMA를 동일한 하드웨어에서 공유 CQ 폴링으로 처리할 수 있어, 인터럽트 오버헤드를 최소화하면서도 RDMA와 Ethernet 공존이 가능합니다.
/* drivers/net/ethernet/mellanox/mlx5/core/en.h */
struct mlx5e_txqsq {
struct mlx5e_cq cq; /* Tx 전용 CQ — WQ와 1:1 대응 */
struct mlx5_wq_cyc wq; /* Work Queue (송신 디스크립터 링) */
...
};
struct mlx5e_rq {
struct mlx5e_cq cq; /* Rx 전용 CQ — RDMA MR 처리와 분리 */
struct mlx5_wq_ll wq; /* Linked-list WQ (MPWRQ 지원) */
...
};
Broadcom NetXtreme-E의 HWRM(Hardware Resource Manager)은 드라이버 변경 없이 펌웨어 업데이트만으로 기능 추가가 가능하도록 설계되었습니다. HWRM은 커맨드 버전 필드를 포함하기 때문에 드라이버와 펌웨어 간 forward compatibility를 보장하며, NIC를 OEM에 공급할 때 펌웨어 커스터마이징만으로 다양한 SKU를 지원할 수 있습니다.
/* drivers/net/ethernet/broadcom/bnxt/bnxt_hwrm.h */
struct hwrm_cmd_req_hdr {
__le16 req_type; /* 커맨드 타입 */
__le16 cmpl_ring; /* 완료 링 ID */
__le16 seq_id; /* 시퀀스 번호 — 응답 매칭용 */
__le16 target_id; /* 타깃 기능 ID (VF/PF 구분) */
__le64 resp_addr; /* DMA 응답 버퍼 주소 */
};
결과적으로 bnxt 드라이버 코드는 상대적으로 작고 단순하며, 복잡한 하드웨어 로직은 펌웨어에 위임됩니다.
Intel E810의 AdminQ(Administration Queue)는 단순한 명령 채널이 아니라, Intel의 IPU(Infrastructure Processing Unit) 및 DPU 전략과 연결됩니다. AdminQ를 데이터 경로와 분리함으로써 동일한 제어 평면 코드를 NIC, IPU, DPU 세 가지 배포 모델에서 재사용할 수 있습니다. 또한 AdminQ는 비동기 이벤트(링크 상태 변화, 에러 보고 등)를 데이터 큐와 완전히 독립적으로 처리하여, 고부하 상황에서도 제어 명령이 손실되지 않도록 합니다.
/* drivers/net/ethernet/intel/ice/ice_adminq_cmd.h */
struct ice_aq_desc {
__le16 flags; /* ICE_AQ_FLAG_RD — 데이터 방향 */
__le16 opcode; /* 커맨드 opcode */
__le16 datalen; /* 데이터 길이 */
__le16 retval; /* 펌웨어 응답 코드 */
__le32 cookie_h;
__le32 cookie_l; /* 드라이버 쿠키 (비동기 완료 식별용) */
union ice_aqc_one_q params; /* 커맨드별 파라미터 */
};
Amazon ENA(Elastic Network Adapter)가 NVMe의 SQ(Submission Queue)/CQ(Completion Queue) 모델을 차용한 이유는 VM exit 오버헤드 최소화입니다. NVMe SQ/CQ 모델은 게스트 VM이 큐 tail pointer를 doorbell 레지스터에 쓰는 것만으로 작업을 제출할 수 있어, 하이퍼바이저 개입 없이 직접 하드웨어에 접근하는 것과 유사한 효율을 냅니다. 또한 AWS 커스텀 실리콘 설계 시 NVMe 검증 경험을 재활용할 수 있었습니다.
/* drivers/net/ethernet/amazon/ena/ena_com.h */
struct ena_com_io_sq {
struct ena_com_io_desc *desc_addr; /* SQ 디스크립터 링 */
u32 __iomem *db_addr; /* Doorbell 레지스터 주소 */
u16 q_depth; /* 큐 깊이 */
u16 tail; /* 제출 tail — VM exit 없이 쓰기 */
};
virtio의 split ring(레거시)은 Descriptor Table, Available Ring, Used Ring 3개 배열로 구성되어 있어, 게스트가 패킷을 제출할 때 Descriptor Table과 Available Ring을 모두 갱신해야 합니다. 이 두 자료구조가 서로 다른 캐시라인에 위치할 경우 캐시 바운싱(cache bouncing)이 발생합니다. Linux 5.0에서 도입된 packed ring은 단일 배열에 디스크립터와 완료 상태를 함께 저장하고, wrap counter를 이용해 head/tail을 추적함으로써 캐시 미스를 크게 줄였습니다.
/* include/uapi/linux/virtio_ring.h — split ring (레거시) */
struct vring {
unsigned int num;
struct vring_desc *desc; /* 디스크립터 배열 — 별도 캐시라인 */
struct vring_avail *avail; /* Available ring — 또 다른 캐시라인 */
struct vring_used *used; /* Used ring — 세 번째 캐시라인 오염 */
};
/* packed ring (5.0+) — 단일 배열, wrap counter 방식 */
struct vring_packed_desc {
__le64 addr; /* 버퍼 주소 */
__le32 len; /* 버퍼 길이 */
__le16 id; /* 디스크립터 ID */
__le16 flags; /* AVAIL/USED 비트 + wrap counter — 동일 캐시라인 */
};
커널 버전별 기능 타임라인
네트워크 드라이버의 주요 기능은 커널 버전마다 단계적으로 추가되었습니다. 아래 타임라인은 XDP, AF_XDP, page_pool, devlink 등 핵심 기능의 도입 이력을 드라이버별로 정리합니다.
| 커널 버전 | 출시 시기 | 주요 드라이버 변경사항 |
|---|---|---|
| 5.0 | 2019-03 | virtio packed ring 도입 (virtio_net); page_pool API 초기 도입; XDP multi-buffer 기반 작업 시작 |
| 5.1 | 2019-05 | mlx5e AF_XDP zero-copy 초기 지원; ice 드라이버 메인라인 합류; devlink port 기능 확장 |
| 5.2 | 2019-07 | mlx5e XDP native 안정화; bnxt_en devlink health reporter 추가; ena XDP 준비 작업 |
| 5.3 | 2019-09 | ixgbe AF_XDP zero-copy 추가; mlx5e TC flower multi-action offload; ice SR-IOV 초기 지원 |
| 5.4 | 2019-11 | bnxt_en devlink eswitch 모드; atlantic XDP native 추가; cxgb4 TC flower 확장 |
| 5.5 | 2020-01 | mlx5e AF_XDP ZC Production 안정화; ice XDP native + AF_XDP ZC; vmxnet3 XDP 지원 |
| 5.6 | 2020-03 | ena XDP native 추가; AF_XDP 소켓 통계 개선; mlx5e HW timestamps for AF_XDP |
| 5.9 | 2020-10 | mlx5e page_pool 전환 완료; bnxt_en XDP native 안정화; XDP multi-buffer 프레임워크 추가 |
| 5.10 | 2020-12 | ice AF_XDP ZC Production; r8169 page_pool 전환; mlx5e XDP multi-buffer 초기 지원 |
| 5.12 | 2021-04 | bnxt_en AF_XDP ZC 추가; ena page_pool 전환; cxgb4 XDP 실험적 지원 |
| 5.14 | 2021-08 | ice page_pool 전환; mlx5e XDP multi-buffer Production; devlink rate 기능 추가 |
| 5.16 | 2022-01 | gve XDP native + page_pool; virtio_net XDP multi-buffer 지원; bnxt_en page_pool 안정화 |
| 5.19 | 2022-07 | virtio_net page_pool 지원; ice GNSS (GPS 타임스탬프) 지원; AF_XDP 멀티큐 개선 |
| 6.0 | 2022-10 | mlx5e devlink rate 계층 구조; ice switchdev LAG; XDP hints (메타데이터 API) 도입 |
| 6.1 | 2022-12 | XDP hints Production; bnxt_en XDP hints 지원; ice DDP (Dynamic Device Personalization) 확장 |
| 6.4 | 2023-06 | mlx5e SF(Sub-Function) devlink 안정화; gve AF_XDP ZC 추가; virtio_net packed ring 성능 개선 |
| 6.6 | 2023-10 | netdev queue API (큐 단위 통계) 도입; ice IDPF 기반 가상화 지원; devlink selftests 프레임워크 |
| 6.8 | 2024-03 | mlx5e HW GRO offload; ena 버스트 큐 최적화; XDP 리디렉션 성능 개선 |
# 현재 커널에서 드라이버의 XDP 지원 확인
ethtool --show-features eth0 | grep xdp
# page_pool 통계 확인 (5.9+ 필요)
ethtool -S eth0 | grep -i page_pool
# AF_XDP ZC 지원 여부 — 실제 소켓 생성으로 확인
ip link show eth0 # XDP 플래그 확인
# 커널 소스에서 드라이버 기능 도입 버전 추적
git log --oneline drivers/net/ethernet/mellanox/mlx5/core/en_main.c | head -20
# devlink 기능 확인
devlink dev info pci/0000:XX:00.0
커널 버전이 기능 도입 버전보다 낮으면 해당 기능을 사용할 수 없습니다.
배포 환경의 커널 버전(uname -r)을 반드시 확인하십시오.
큐 모델 분류
네트워크 드라이버의 큐 모델은 성능 확장성과 CPU 활용 효율에 결정적 영향을 미칩니다. 단일 큐에서 시작하여 대칭 멀티큐, 분리 큐, 완료 큐 독립 모델로 진화해 왔습니다.
링 구조와 디스크립터 포맷도 드라이버마다 상이합니다.
| 드라이버 | Tx 디스크립터 크기 | Rx 디스크립터 크기 | CQ 존재 | 기본 링 크기 | 최대 링 크기 | 디스크립터 타입 |
|---|---|---|---|---|---|---|
ice |
16B | 32B | ✗ (인라인) | 1024 | 8192 | Flex descriptor |
mlx5e |
64B (WQE) | 변동 (MPWQE) | ✓ (64B CQE) | 1024 | 8192 | WQE/CQE |
bnxt_en |
16B | 16B | ✓ (32B CQE) | 512 | 32768 | BD (Buffer Descriptor) |
ixgbe |
16B | 16/32B | ✗ | 512 | 4096 | Advanced descriptor |
ena |
16B | 16B | ✓ | 1024 | 16384 | SQ/CQ descriptor |
virtio_net |
가변 (SG) | 가변 (SG) | ✓ (Used Ring) | 256 | 32768 | Virtqueue descriptor |
r8169 |
16B | 16B | ✗ | 256 | 1024 | Legacy descriptor |
인터럽트 처리 비교
MSI-X 벡터 할당 전략은 드라이버 성능에 큰 영향을 미칩니다. 관리용 벡터와 데이터 큐 벡터의 분리 방식, 공유 여부가 드라이버마다 다릅니다.
인터럽트 코얼레싱(coalescing) 파라미터도 드라이버 간 차이가 큽니다.
| 드라이버 | Rx usecs 기본값 | Tx usecs 기본값 | Adaptive 지원 | Adaptive 알고리즘 | CQ 기반 모더레이션 |
|---|---|---|---|---|---|
ice |
50 µs | 50 µs | ✓ | DIM (Dynamically-tuned Interrupt Moderation) | ✗ |
mlx5e |
8 µs | 16 µs | ✓ | DIM + HW 자체 모더레이션 | ✓ |
bnxt_en |
16 µs | 16 µs | ✓ | DIM | ✓ |
ixgbe |
20 µs | 72 µs | ✓ | 커널 DIM 프레임워크 | ✗ |
ena |
20 µs | 64 µs | ✓ | 자체 적응형 (ENA DIM) | ✗ |
virtio_net |
N/A | N/A | ✗ | — | ✗ |
r8169 |
고정 | 고정 | ✗ | — | ✗ |
net/core/dim/에 통합된 DIM(Dynamically-tuned Interrupt Moderation) 프레임워크는
패킷 수와 바이트 수를 기반으로 인터럽트 간격을 자동 조정합니다. ice, mlx5e, bnxt_en 등 최신 드라이버는
모두 이 공통 프레임워크를 활용하며, ethtool -C <dev> adaptive-rx on으로 활성화합니다.
메모리 모델 비교
DMA 매핑 수명주기와 버퍼 할당 전략은 드라이버 성능의 핵심 결정 요소입니다. 패킷당 매핑/해제 방식에서 사전 매핑 풀 방식으로 진화하면서 CPU 오버헤드가 크게 감소했습니다.
page_pool 도입 현황과 버퍼 할당 전략을 비교합니다.
| 드라이버 | page_pool 사용 | 할당 단위 | 버퍼 전략 | DMA 방향 | 재활용 방식 |
|---|---|---|---|---|---|
mlx5e |
✓ | PAGE (4KB/64KB) | MPWQE (Multi-Packet WQE) | DMA_FROM_DEVICE | page_pool + frag 재활용 |
ice |
✓ | PAGE (4KB) | 1 page per descriptor | DMA_FROM_DEVICE | page_pool |
bnxt_en |
✓ | PAGE frag | page frag + aggregation | DMA_FROM_DEVICE | page_pool |
ixgbe |
△ (전환 중) | half-page | 페이지 분할 (2KB×2) | DMA_FROM_DEVICE | 자체 page flip |
ena |
✓ | PAGE | Large buffer mode | DMA_FROM_DEVICE | page_pool |
virtio_net |
✓ | PAGE frag | mergeable buffer | DMA_FROM_DEVICE | page_pool |
r8169 |
✓ | PAGE frag | Single buffer | DMA_FROM_DEVICE | page_pool |
vmxnet3 |
✗ | SKB data | alloc_skb per packet | DMA_FROM_DEVICE | 없음 (매번 free) |
hv_netvsc |
✗ | VMBus buffer | 공유 메모리 링 | N/A (VMBus) | VMBus 재활용 |
mlx5e의 고유 최적화로, 하나의 WQE(Work Queue Element)가
여러 패킷을 수용할 수 있는 큰 버퍼(예: 64KB)를 가리킵니다. 하드웨어가 패킷을 연속으로 채우고 CQE에서 stride 정보를 제공하여
디스크립터 소비를 극적으로 줄입니다. 특히 소형 패킷이 대량으로 도착하는 워크로드에서 효과적입니다.
펌웨어 의존성 레벨 비교
네트워크 드라이버의 펌웨어 의존도는 기능성, 디버깅 용이성, 장애 복구 시간에 직접적 영향을 미칩니다. 다음 표는 주요 드라이버를 펌웨어 의존도별로 분류합니다.
| 레벨 | 드라이버 | 펌웨어 역할 | 디버깅 난이도 | 복구 시간 | 기능 업데이트 방법 |
|---|---|---|---|---|---|
| None | r8169, tg3 |
없음 (레지스터 직접 접근) | 낮음 | <1초 | 드라이버 코드 수정 |
| Light | igb, ixgbe |
PHY 초기화, NVM 관리 | 낮음 | 1~2초 | 드라이버 + 선택적 FW 업데이트 |
| Medium | ice, i40e |
Admin Queue 명령 처리, 일부 오프로드 | 중간 | 3~5초 | DDP 프로필 + FW 업데이트 |
| Heavy | bnxt_en, mlx5e |
패킷 분류, VXLAN/GRE 오프로드, eSwitch | 높음 | 5~15초 | 펌웨어 필수 업데이트 |
| Critical | ena, gve |
데이터 경로 전체 (하이퍼바이저 펌웨어) | 매우 높음 | N/A (클라우드 관리) | 클라우드 프로바이더 의존 |
bnxt_en의 경우 devlink health를 통해
펌웨어 상태를 모니터링하고 자동 복구를 시도하지만, ena/gve는 인스턴스 재시작이
유일한 복구 수단일 수 있습니다.
# 펌웨어 버전 및 상태 확인
ethtool -i eth0 | grep firmware
# devlink 기반 펌웨어 상태 모니터링 (bnxt, mlx5, ice)
devlink health show pci/0000:03:00.0
# 펌웨어 리포터 진단 정보 확인
devlink health diagnose pci/0000:03:00.0 reporter fw
# 펌웨어 업데이트 (devlink 지원 드라이버)
devlink dev flash pci/0000:03:00.0 file firmware.bin
리셋 전략 비교
NIC 장애 복구에서 리셋 세분화 수준은 서비스 가용성에 직접적 영향을 미칩니다. 최신 드라이버일수록 전체 NIC 리셋 없이 개별 큐나 VF 단위의 세밀한 복구를 지원합니다.
| 드라이버 | Global Reset | PF Reset | VF Reset | Queue Reset | Tx Timeout 처리 | Watchdog 자동 복구 |
|---|---|---|---|---|---|---|
ice |
✓ | ✓ | ✓ | ✓ (Gen3) | PF 리셋 | ✓ |
mlx5e |
✓ | ✓ | ✓ | ✓ | 큐 리셋 시도 → PF 리셋 | ✓ |
bnxt_en |
✓ | ✓ | ✓ | ✗ | PF 리셋 | ✓ (devlink health) |
i40e |
✓ | ✓ | △ | ✗ | PF 리셋 | ✓ |
ena |
✓ | N/A | N/A | ✗ | 디바이스 리셋 | ✓ (keep-alive) |
virtio_net |
✓ | N/A | N/A | ✗ | virtqueue 리셋 | ✗ |
r8169 |
✓ | N/A | N/A | ✗ | 전체 리셋 | ✓ (링크 watchdog) |
# 리셋 통계 확인 (ethtool)
ethtool -S eth0 | grep -i reset
# devlink health 리셋 이력 (ice, mlx5, bnxt)
devlink health show pci/0000:03:00.0
# 수동 리셋 트리거 (디버깅용)
devlink dev reload pci/0000:03:00.0
# VF 리셋 (SR-IOV 환경)
echo 1 > /sys/bus/pci/devices/0000:03:00.2/reset
성능 심층 분석
네트워크 드라이버의 실제 성능은 코얼레싱 설정, 링 버퍼 크기, NUMA 배치, XDP 지원 수준 등 다양한 요소의 조합에 의해 결정됩니다. 이 섹션에서는 각 요소를 심층 분석하고 드라이버 간 성능 차이의 근본 원인을 파악합니다.
벤치마크 방법론
드라이버 성능 측정은 재현 가능한 환경과 올바른 도구 선택이 핵심입니다. 잘못된 측정 방법은 최대 수십 배의 오차를 초래하며, 최적화 방향을 완전히 잘못 이끌 수 있습니다.
테스트 환경 명세 템플릿
성능 측정 결과를 공유하거나 재현하려면 아래 항목을 반드시 명시해야 합니다. 특히 BIOS 설정은 결과에 수십 % 영향을 미치므로 절대 생략할 수 없습니다.
| 분류 | 필수 항목 | 확인 명령 / 경로 | 성능 영향 |
|---|---|---|---|
| CPU | 모델, 코어 수, 클럭, NUMA 토폴로지 | lscpu, numactl --hardware |
매우 높음 |
| 커널 | 버전, CONFIG_HZ, CONFIG_PREEMPT, RPS/RFS 여부 | uname -r, zcat /proc/config.gz |
높음 |
| NIC 펌웨어 | 드라이버 버전, FW 버전, NVM 버전 | ethtool -i eth0 |
중간 |
| BIOS: C-states | C1E/C3/C6 비활성화 여부 | cpupower idle-info, /sys/devices/system/cpu/cpu*/cpuidle/ |
매우 높음 (수십%) |
| BIOS: SMT/HT | Hyper-Threading 활성화 여부 | lscpu | grep Thread |
높음 |
| BIOS: IOMMU | IOMMU 활성화 여부 (DMAR on/off) | dmesg | grep IOMMU |
중간~높음 |
| 메모리 | 용량, 채널 수, 속도, NUMA 배치 | dmidecode -t memory |
중간 |
| PCIe | Gen/레인 수, ACS 설정 | lspci -vvv | grep -i width |
중간 |
| OS | 배포판, glibc 버전, IRQ 밸런싱 데몬 | cat /etc/os-release |
낮음~중간 |
# 전체 환경 스냅샷 원라이너 (벤치마크 기록용)
echo "=== CPU ===" && lscpu | grep -E 'Model|CPU\(s\)|Socket|NUMA|MHz'
echo "=== Kernel ===" && uname -a
echo "=== NIC ===" && ethtool -i eth0
echo "=== C-states ===" && cpupower idle-info 2>/dev/null
echo "=== IOMMU ===" && dmesg | grep -i iommu | head -5
echo "=== IRQ balance ===" && systemctl is-active irqbalance
perf/ftrace/bpftrace 방법론
올바른 계측 도구를 선택하는 것은 측정 오버헤드와 가시성 간 균형을 결정합니다.
perf stat는 전체 통계를, ftrace는 경로 추적을, bpftrace는 동적 히스토그램 수집을 제공합니다.
## perf stat: NIC 드라이버 프로파일링 레시피
# 코어 고정 후 인터럽트 통계 수집 (5초)
taskset -c 2 perf stat -e \
net:netif_receive_skb,net:net_dev_xmit,\
irq:irq_handler_entry,irq:softirq_raise,\
cache-misses,cache-references,cycles,instructions \
-I 1000 -C 2 sleep 5
# 드라이버 함수 핫스팟 (perf record + report)
perf record -C 2 -g -e cycles:u -- sleep 5
perf report --stdio --no-children | head -40
# NAPI poll 지연 분포 측정
perf record -e 'net:napi_poll' -C 2 sleep 5
perf script | awk '{print $NF}' | sort -n | uniq -c
## ftrace: NAPI → GRO 경로 추적 (function_graph)
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'napi_poll' > /sys/kernel/debug/tracing/set_graph_function
echo 'napi_gro_receive' >> /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -80
# mlx5e 전용: poll 함수 그래프
echo 'mlx5e_napi_poll' > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 0.1
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
## bpftrace: 드라이버별 NAPI poll 지연 히스토그램
bpftrace -e '
kprobe:napi_poll {
@start[arg0] = nsecs;
}
kretprobe:napi_poll /@start[arg0]/ {
@latency_us = hist((nsecs - @start[arg0]) / 1000);
delete(@start[arg0]);
}
interval:s:5 { print(@latency_us); clear(@latency_us); exit(); }
'
## p50/p95/p99 신뢰도: 30초 이상 수집, warm-up 10초 제외
bpftrace -e '
kprobe:napi_poll { @s[arg0] = nsecs; }
kretprobe:napi_poll /@s[arg0]/ {
$lat = (nsecs - @s[arg0]) / 1000;
@dist = lhist($lat, 0, 1000, 10);
delete(@s[arg0]);
}
interval:s:30 { print(@dist); exit(); }'
- 웜업: 측정 전 최소 10초 트래픽으로 TLB/캐시 워밍
- 반복 횟수: 최소 3회 측정, 최고·최저 제외 후 중간값 사용
- 이상치 제거: p99.9 이상은 OS 스케줄러(Scheduler)/SMI 인터럽트 영향으로 분리 분석
- 동시 모니터링 금지:
top,htop,sar등 계측 도구 자체가 캐시 오염 유발
패킷 생성기 선택 가이드
워크로드와 측정 목적에 맞는 패킷 생성기 선택이 중요합니다. 잘못된 생성기 선택은 DUT(Device Under Test)가 아닌 생성기 자체가 병목이 되는 결과를 초래합니다.
| 생성기 | 최대 속도 | 유연성 | 설치 난이도 | single-flow | multi-flow | stateful 지원 | 권장 용도 |
|---|---|---|---|---|---|---|---|
| pktgen (커널) | ~10~40Mpps | 낮음 | 매우 쉬움 | ✓ | △ (제한적) | ✗ | 단순 드라이버 처리량 테스트, 빠른 기준선 |
| TRex | ~200Gbps+ | 높음 | 중간 | ✓ | ✓ | ✓ | 실제 트래픽 시뮬레이션, NFV 검증 |
| DPDK-pktgen | ~100~200Gbps | 중간 | 높음 | ✓ | ✓ | △ | DPDK 기반 드라이버 성능 측정 |
| MoonGen | ~100Gbps | 매우 높음 | 높음 | ✓ | ✓ | ✓ | 정밀 타임스탬프 지연 측정, 학술 연구 |
## pktgen (커널 내장): 가장 빠른 기준선 측정
modprobe pktgen
echo "add_device eth0@0" > /proc/net/pktgen/kpktgend_0
pgset() { echo "$1" > /proc/net/pktgen/eth0@0; }
pgset "count 10000000"
pgset "pkt_size 64"
pgset "dst_mac 00:11:22:33:44:55"
pgset "dst 192.168.1.1"
echo "start" > /proc/net/pktgen/pgctrl
cat /proc/net/pktgen/eth0@0 | grep -E 'pps|bps|errors'
드라이버별 코얼레싱 전략
인터럽트 코얼레싱은 지연 시간(latency)과 처리량(throughput) 사이의 근본적 트레이드오프입니다. 코얼레싱 간격이 길수록 인터럽트 오버헤드가 줄어 throughput이 향상되지만, 패킷 처리 지연이 증가합니다.
주요 드라이버의 코얼레싱 파라미터를 비교합니다.
| 드라이버 | Rx usecs | Rx frames | Tx usecs | Tx frames | Adaptive 모드 | 추천 워크로드 |
|---|---|---|---|---|---|---|
mlx5e |
8 | 128 | 16 | 128 | DIM + CQ 모더레이션 | 저지연/HPC |
bnxt_en |
16 | 64 | 16 | 64 | DIM | 범용 |
ixgbe |
20 | 8 | 72 | 32 | DIM | 범용/서버 |
ice |
50 | 128 | 50 | 128 | DIM | 고처리량/NFV |
i40e |
50 | — | 50 | — | 커널 DIM | 고처리량 |
ena |
20 | — | 64 | — | ENA 자체 적응형 | 클라우드 범용 |
cxgb4 |
10 | 8 | 1 | 8 | 자체 타이머 | RDMA/iSCSI |
# 현재 코얼레싱 설정 확인
ethtool -c eth0
# 저지연 튜닝 (mlx5e 예시: 코얼레싱 최소화)
ethtool -C eth0 rx-usecs 0 tx-usecs 0 adaptive-rx off adaptive-tx off
# 고처리량 튜닝 (ice 예시: 코얼레싱 증가)
ethtool -C eth0 rx-usecs 100 tx-usecs 100 rx-frames 256 tx-frames 256
# Adaptive 코얼레싱 활성화 (대부분의 워크로드에 추천)
ethtool -C eth0 adaptive-rx on adaptive-tx on
- 금융/HFT (초저지연):
rx-usecs 0,adaptive off, busy-poll 병행 - 웹 서버 (범용):
adaptive-rx on, 기본값 사용 - 스트리밍/CDN (고처리량):
rx-usecs 100~250,rx-frames 256+ - NFV/패킷 처리:
rx-usecs 0, XDP + busy-poll 조합
코얼레싱과 XDP 상호작용
XDP를 사용할 때 코얼레싱 설정은 일반 스택 경로와 다른 영향을 미칩니다. 드라이버마다 HW 코얼레싱 지원 수준이 다르며, XDP 동작 모드에 따라 최적 설정이 달라집니다.
| 드라이버 | 코얼레싱 방식 | rx-usecs 범위 | rx-frames 지원 | adaptive RX | XDP와 상호작용 |
|---|---|---|---|---|---|
mlx5e |
HW 타이머 + DIM | 0~8192 | ✓ | ✓ (DIM) | XDP_DROP 시 rx-usecs=0 최적, XDP_REDIRECT는 8~16 권장 |
ice |
HW 타이머 + DIM | 0~8160 | ✓ | ✓ (DIM) | DIM이 XDP 배치 크기 자동 조정, 수동 설정 가능 |
r8169 |
고정 타이머만 | 고정값 | ✗ | ✗ | 코얼레싱 조정 불가, XDP 성능 제한 요인 |
atlantic |
adaptive (자체) | 0~500 | ✓ | ✓ (자체) | XDP 로드 시 adaptive 동작 유지, 개별 조정 지원 |
gve |
DQO adaptive | 호스트 관리 | △ | ✓ (DQO) | GCP 호스트가 코얼레싱 조정, 직접 제어 제한적 |
virtio_net |
없음 (HW 없음) | N/A | ✗ | ✗ | HW 코얼레싱 없음; vhost 배치 크기로 간접 조정 |
hv_netvsc |
호스트 관리 | 호스트 결정 | ✗ | △ (호스트) | Hyper-V 호스트가 코얼레싱 결정, 게스트 조정 불가 |
- XDP_DROP:
rx-usecs=0+adaptive-rx off— 각 패킷을 즉시 처리하여 최소 지연 - XDP_REDIRECT (devmap bulk):
rx-usecs=8~16— 배치 flush 크기와 정렬하여 처리량 최대화 - XDP_PASS (스택 전달): adaptive 코얼레싱 유지 — 일반 스택 경로와 동일한 최적화
- XDP_TX (동일 NIC 반환): RX/TX 코얼레싱 동시 조정 필요, 동일 값 설정 권장
## bpftrace: 코얼레싱 설정과 XDP 처리 지연 상관관계 추적
bpftrace -e '
kprobe:xdp_do_redirect {
@xdp_start[tid] = nsecs;
}
kretprobe:xdp_do_redirect /@xdp_start[tid]/ {
@xdp_lat_us = hist((nsecs - @xdp_start[tid]) / 1000);
delete(@xdp_start[tid]);
}
kprobe:napi_poll {
@napi_start[arg0] = nsecs;
}
kretprobe:napi_poll /@napi_start[arg0]/ {
@napi_lat_us = hist((nsecs - @napi_start[arg0]) / 1000);
delete(@napi_start[arg0]);
}
interval:s:10 {
print("XDP redirect 지연:");
print(@xdp_lat_us);
print("NAPI poll 지연:");
print(@napi_lat_us);
exit();
}'
링 버퍼 크기 영향 분석
링 버퍼 크기는 버스트 트래픽 흡수 능력과 메모리 사용량, 그리고 캐시 효율성 사이의 균형을 결정합니다. 버퍼가 작으면 트래픽 버스트 시 패킷 손실이 발생하고, 너무 크면 bufferbloat로 인한 지연 증가와 메모리 낭비가 발생합니다.
| 드라이버 | Rx 기본 | Rx 최소 | Rx 최대 | Tx 기본 | Tx 최소 | Tx 최대 | 2의 거듭제곱 필수 |
|---|---|---|---|---|---|---|---|
ice |
1024 | 64 | 8192 | 1024 | 64 | 8192 | ✓ |
mlx5e |
1024 | 64 | 8192 | 1024 | 64 | 8192 | ✓ |
bnxt_en |
512 | 32 | 32768 | 512 | 32 | 32768 | ✗ |
ixgbe |
512 | 64 | 4096 | 512 | 64 | 4096 | ✓ (8의 배수) |
ena |
1024 | 256 | 16384 | 1024 | 256 | 16384 | ✗ |
virtio_net |
256 | 32 | 32768 | 256 | 32 | 32768 | ✓ |
r8169 |
256 | 256 | 1024 | 256 | 256 | 1024 | ✓ |
| 링 크기 | 장점 | 단점 | 추천 워크로드 |
|---|---|---|---|
| 64~256 | 최소 메모리, 캐시 적중률 최대 | 버스트 시 패킷 손실 위험 | 저지연/HFT, busy-poll 사용 시 |
| 512~1024 | 범용 균형, 적당한 버스트 흡수 | — | 일반 서버, 웹/앱 서비스 |
| 2048~4096 | 높은 버스트 흡수력 | 메모리 증가, 캐시 미스 증가 | 고속(25G+), 트래픽 변동 큰 환경 |
| 8192+ | 극한 버스트 대응 | bufferbloat 위험, 메모리 다량 소비 | 패킷 캡처, 특수 목적 |
# 현재 링 버퍼 크기 확인
ethtool -g eth0
# 링 버퍼 크기 조정 (온라인, 대부분 트래픽 순간 중단)
ethtool -G eth0 rx 2048 tx 2048
# 메모리 사용량 추정 (ring_size × descriptor_size × num_queues)
# 예: 2048 × 16B × 32큐 = 1MB (디스크립터만)
# 실제 버퍼 메모리: 2048 × 4KB × 32큐 = 256MB (page_pool 기준)
# 패킷 드롭 모니터링 (링 오버플로우 감지)
ethtool -S eth0 | grep -E 'rx_dropped|rx_no_buffer|rx_missed'
버퍼블로트 진단 워크플로우
버퍼블로트(bufferbloat)는 과도한 링 버퍼 또는 qdisc 큐로 인해 RTT가 폭발적으로 증가하는 현상입니다. 증상만 보면 단순 지연 증가처럼 보이지만, 원인은 링 레벨 또는 qdisc 레벨 중 어느 한 곳에 있습니다.
| 버퍼블로트 유형 | 원인 | 진단 도구 | 지표 |
|---|---|---|---|
| 링 레벨 | TX ring 가득 참 + TX 소비 느림 | ethtool -S, bpftrace |
tx_queue_stopped, sojourn time >1ms |
| qdisc 레벨 | pfifo/bfifo 큐 과다 적재 | tc -s qdisc |
dropped 증가, backlog 상시 nonzero |
| BQL 미적용 | Byte Queue Limits 비활성화 | /sys/class/net/eth0/queues/tx-N/byte_queue_limits/ |
limit 값 과다 또는 0 |
| 애플리케이션 | send() 버스트 + SO_SNDBUF 과대 | ss -ti |
RTT 폭증, snd_wnd 대비 snd_cwnd 불균형 |
아래는 단계별 버퍼블로트 진단 워크플로우입니다. 위에서 아래로 순서대로 점검합니다.
## Step 1: 링 충전율 확인
# TX ring이 가득 찬 횟수 (링 레벨 bufferbloat 지표)
ethtool -S eth0 | grep -E 'tx_queue_stopped|tx_busy|tx_restart'
## Step 2: BQL 상태 확인
# limit: 현재 허용 바이트, limit_max: 최대 허용
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_max
# BQL 수동 제한 (bufferbloat 완화)
echo 10240 > /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_max
## Step 3: qdisc 큐 상태 확인
tc -s qdisc show dev eth0
# 출력에서 backlog nonzero 지속 + dropped 증가 = qdisc bufferbloat
# 해결: fq_codel 또는 cake qdisc로 교체
tc qdisc replace dev eth0 root fq_codel
## Step 4: 애플리케이션 RTT 분석
# ss -ti: srtt (smoothed RTT), rttvar (RTT variance)
ss -ti dst 192.168.1.1 | grep -E 'rtt|cwnd|wscale'
## bpftrace: TX ring sojourn time (패킷이 TX ring에서 보내는 시간)
# 높은 sojourn time = TX ring에서 드라이버가 패킷을 늦게 소비
bpftrace -e '
kprobe:dev_queue_xmit {
@enqueue[skbuff->hash] = nsecs;
}
kprobe:dev_hard_start_xmit /@enqueue[skbuff->hash]/ {
$sojourn = nsecs - @enqueue[skbuff->hash];
@sojourn_us = hist($sojourn / 1000);
delete(@enqueue[skbuff->hash]);
}
interval:s:10 {
print("TX ring sojourn time (us):");
print(@sojourn_us);
exit();
}'
| 드라이버 | 권장 RX 링 크기 | 권장 TX 링 크기 | 워크로드 | 이유 |
|---|---|---|---|---|
mlx5e |
1024~2048 | 1024 | 고처리량 서버 | HW rate limiting 지원으로 큰 링도 bufferbloat 없음 |
mlx5e |
256~512 | 512 | HFT/저지연 | 작은 링으로 캐시 효율 극대화, busy-poll 병행 |
ice |
1024 | 1024 | 범용/NFV | DIM adaptive가 동적 조정, 기본값 유지 권장 |
r8169 |
256 | 256 | 데스크톱/소규모 | 백프레셔 없음, 큰 링은 bufferbloat 직결 |
bnxt_en |
1024~2048 | 512 | 고처리량 | HW CQ 분리 구조로 큰 링 허용, TX는 작게 유지 |
virtio_net |
512 | 256 | VM 범용 | vhost 배치와 정렬, 큰 링은 VM 간 지연 증가 |
ena |
1024 | 1024 | 클라우드 서버 | AWS ENA 드라이버 자체 adaptive, 기본값 최적화됨 |
NUMA 친화성
멀티소켓 시스템에서 NIC의 NUMA 노드와 CPU/메모리 배치가 일치하지 않으면 크로스-노드 메모리 접근으로 인한 심각한 성능 저하가 발생합니다. 올바른 NUMA 배치는 10~30%의 성능 차이를 만들어냅니다.
# NIC의 NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node
# CPU별 NUMA 노드 확인
lscpu | grep -i numa
numactl --hardware
# IRQ affinity를 NIC의 NUMA 노드 CPU에 고정
# 예: NIC가 NUMA 0 (CPU 0-15)에 있을 때
for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
echo 0000ffff > /proc/irq/$irq/smp_affinity
done
# irqbalance에서 NUMA 인식 활성화
IRQBALANCE_ARGS="--hintpolicy=exact"
# 애플리케이션의 NUMA 바인딩
numactl --cpunodebind=0 --membind=0 ./my_server
# NUMA 메모리 접근 통계 확인
numastat -p $(pgrep my_server)
| 구성 | 상대 성능 (PPS) | 평균 지연 | 비고 |
|---|---|---|---|
| 동일 NUMA (최적) | 100% | 기준값 | NIC/CPU/메모리 동일 노드 |
| CPU만 원격 NUMA | 70~85% | +15~30% | DMA는 로컬, 처리는 원격 |
| 메모리만 원격 NUMA | 75~90% | +10~20% | page_pool 할당이 원격 |
| 전체 크로스-NUMA | 60~75% | +30~50% | 모든 접근이 QPI/UPI 경유 |
XDP 성능 비교
XDP(eXpress Data Path)는 드라이버 레벨에서 패킷을 처리하여 커널 네트워크 스택 오버헤드를 완전히 제거합니다. 드라이버의 XDP 구현 품질과 하드웨어 능력에 따라 성능 차이가 현저합니다.
| 드라이버 | XDP Native | XDP Redirect | XDP Multi-buf | XDP HW Offload | XDP 연결 방식 | XDP 메타데이터 |
|---|---|---|---|---|---|---|
mlx5e |
✓ | ✓ | ✓ | ✗ | bpf_prog_attach |
✓ (rx_hash, mark) |
ice |
✓ | ✓ | ✓ | ✗ | ndo_bpf |
✓ (rx_hash) |
bnxt_en |
✓ | ✓ | ✓ | ✗ | ndo_bpf |
✓ |
i40e |
✓ | ✓ | ✗ | ✗ | ndo_bpf |
△ |
ixgbe |
✓ | ✓ | ✗ | ✗ | ndo_bpf |
✗ |
ena |
✓ | ✓ | ✗ | ✗ | ndo_bpf |
✗ |
virtio_net |
✓ | ✓ | ✓ | ✗ | ndo_bpf |
✗ |
atlantic |
✓ | ✓ | ✗ | ✗ | ndo_bpf |
✗ |
mvpp2 |
✓ | ✓ | ✗ | ✗ | ndo_bpf |
✗ |
# XDP 프로그램 로드 (Native 모드)
ip link set dev eth0 xdp obj xdp_drop.o sec xdp
# XDP 프로그램 상태 확인
ip link show dev eth0 | grep xdp
bpftool net show
# XDP 성능 벤치마크 (xdp-tools 활용)
xdp-bench drop eth0
# XDP redirect 벤치마크 (인터페이스 간 포워딩)
xdp-bench redirect eth0 eth1
# XDP 통계 확인
bpftool prog show
ethtool -S eth0 | grep xdp
# Generic XDP fallback (Native 미지원 드라이버)
ip link set dev eth0 xdpgeneric obj xdp_prog.o sec xdp
netif_receive_skb() 이후
SKB가 이미 생성된 상태에서 실행되므로 Native XDP 대비 5~10배 느립니다. 성능이 중요한 환경에서는
반드시 Native XDP를 지원하는 드라이버를 선택해야 합니다.
XDP REDIRECT: copy vs reference 경로
XDP_REDIRECT 성능은 드라이버가 page_pool shared reference를 사용하는지(zero-copy), 아니면 패킷을 메모리 복사하는지(copy)에 따라 크게 달라집니다. 참조 방식은 복사 오버헤드가 없지만 page lifetime 관리가 복잡하고, 복사 방식은 단순하지만 64B 패킷 기준 20~40ns의 추가 지연이 발생합니다.
| 드라이버 | zero-copy XDP_TX | page_pool 사용 | REDIRECT 모드 | memory overhead | 비고 |
|---|---|---|---|---|---|
mlx5e |
✓ | ✓ | reference | 낮음 (ref count만) | page_pool ptr ring, XDP_TX 동일 큐 최적화 |
ice |
✓ | ✓ | reference | 낮음 | devmap bulk flush 지원 |
bnxt_en |
✓ | ✓ | reference | 낮음 | page_pool recycle 경로 최적화 |
i40e |
✓ | △ | copy | 중간 | page_pool 부분 지원, REDIRECT는 복사 경로 |
ixgbe |
△ | ✗ | copy | 높음 | 레거시 alloc 방식, 모든 XDP action에 복사 |
virtio_net |
✗ | ✗ | copy | 높음 | vhost 구조상 zero-copy 불가, 항상 복사 |
ena |
✗ | △ | copy | 중간~높음 | AWS ENA v2+ 부분 page_pool 지원 |
- same-queue TX:
mlx5e/ice는 RX 큐와 동일한 TX 큐에 XDP_TX를 처리하여 큐 락 오버헤드 없음 - cross-queue TX: RX와 TX 큐가 다를 경우 스핀락(Spinlock) 경합 발생 — IRQ affinity 설정 시 같은 큐 쌍 보장 필요
- devmap bulk flush: XDP_REDIRECT는
bpf_redirect_map()+ devmap을 사용할 때 배치 크기(32~64)로 flush하여 NIC 제출 횟수 감소
XDP 멀티버퍼 프로그래밍
점보 프레임(Jumbo frame) 또는 TSO/GRO 세그먼트를 XDP로 처리하려면 XDP multi-buffer 지원이 필요합니다.
단일 xdp_buff가 여러 page fragment를 가지며, BPF 프로그램에서 명시적으로 순회해야 합니다.
/* xdp_buff vs xdp_frame 구조 핵심 필드 */
struct xdp_buff {
void *data; /* 현재 패킷 데이터 포인터 */
void *data_end; /* 데이터 끝 포인터 */
void *data_meta; /* 메타데이터 영역 */
void *data_hard_start;
struct xdp_rxq_info *rxq;
struct xdp_txq_info *txq;
u32 flags; /* XDP_FLAGS_HAS_FRAGS 등 */
};
/* 멀티버퍼 여부 확인 */
if (xdp_buff_has_frags(xdp)) {
struct skb_shared_info *sinfo = xdp_get_shared_info_from_buff(xdp);
u32 total_len = xdp_get_buff_len(xdp); /* 전체 길이 */
/* fragment 순회 */
for (int i = 0; i < sinfo->nr_frags; i++) {
skb_frag_t *frag = &sinfo->frags[i];
void *frag_data = skb_frag_address(frag);
u32 frag_size = skb_frag_size(frag);
/* frag_data[0..frag_size-1] 처리 */
}
}
/* BPF 프로그램에서 XDP multi-buffer fragment 순회 예시 */
SEC("xdp")
int xdp_multibuf_example(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* 헤더 파싱 (첫 번째 버퍼) */
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_DROP;
/* 멀티버퍼: bpf_xdp_get_buff_len()으로 전체 길이 확인 */
__u32 total = bpf_xdp_get_buff_len(ctx);
if (total > 65535)
return XDP_DROP; /* 비정상 크기 드롭 */
/* fragment 접근: bpf_xdp_load_bytes() 사용 (오프셋 기반) */
__u8 buf[16];
if (bpf_xdp_load_bytes(ctx, sizeof(*eth) + 20, buf, sizeof(buf)) < 0)
return XDP_PASS;
return XDP_PASS;
}
- fragment 순회 비용:
nr_frags가 클수록 루프 반복 증가 — 일반적으로 nr_frags ≤ 5 권장 bpf_xdp_load_bytes()는 경계 검사를 포함하므로 단순 포인터 접근보다 느림- 멀티버퍼 XDP_REDIRECT는 각 fragment의 page_pool ref를 개별 처리 — redirect 비용이 단일 버퍼 대비 nr_frags배
- 드라이버 지원 여부 확인:
ethtool -k eth0 | grep scatter
드라이버별 커널 트레이싱 레시피 통합
아래 표는 주요 드라이버별로 즉시 사용 가능한 ftrace, bpftrace, perf 원라이너를 정리한 것입니다. 각 원라이너는 해당 드라이버의 핵심 성능 함수를 대상으로 하며, 커널 버전 6.x 기준입니다.
| 드라이버 | ftrace 원라이너 | bpftrace 원라이너 | perf 원라이너 |
|---|---|---|---|
mlx5e |
echo mlx5e_napi_poll > set_graph_function |
bpftrace -e 'kprobe:mlx5e_napi_poll{@[tid]=nsecs} kretprobe:mlx5e_napi_poll/@[tid]/{@us=hist((nsecs-@[tid])/1000);delete(@[tid])} i:s:5{print(@us);exit()}' |
perf record -e mlx5e:* -C 2 sleep 5; perf report |
ice |
echo ice_napi_poll > set_graph_function |
bpftrace -e 'kprobe:ice_napi_poll{@s[arg0]=nsecs} kretprobe:ice_napi_poll/@s[arg0]/{@=hist((nsecs-@s[arg0])/1000);delete(@s[arg0])} i:s:5{print(@);exit()}' |
perf stat -e 'ice:*' -C 2 sleep 5 |
bnxt_en |
echo bnxt_poll > set_graph_function |
bpftrace -e 'kprobe:bnxt_poll{@s[arg0]=nsecs} kretprobe:bnxt_poll/@s[arg0]/{@=hist((nsecs-@s[arg0])/1000);delete(@s[arg0])} i:s:5{print(@);exit()}' |
perf record -g -e cycles:u -C 2 -- sleep 5; perf report -F sym | grep bnxt |
r8169 |
echo rtl8169_poll > set_graph_function |
bpftrace -e 'kprobe:rtl8169_poll{@s[arg0]=nsecs} kretprobe:rtl8169_poll/@s[arg0]/{@=hist((nsecs-@s[arg0])/1000);delete(@s[arg0])} i:s:5{print(@);exit()}' |
perf stat -e cache-misses,cycles -C 2 sleep 5 |
atlantic |
echo aq_napi_poll > set_graph_function |
bpftrace -e 'kprobe:aq_napi_poll{@s[arg0]=nsecs} kretprobe:aq_napi_poll/@s[arg0]/{@=hist((nsecs-@s[arg0])/1000);delete(@s[arg0])} i:s:5{print(@);exit()}' |
perf record -e cycles:u -g -C 2 sleep 5; perf report | grep aq_ |
ena |
echo ena_io_poll > set_graph_function |
bpftrace -e 'kprobe:ena_io_poll{@s[arg0]=nsecs} kretprobe:ena_io_poll/@s[arg0]/{@=hist((nsecs-@s[arg0])/1000);delete(@s[arg0])} i:s:5{print(@);exit()}' |
perf stat -e net:napi_poll -C 2 sleep 5 |
gve |
echo gve_rx_poll > set_graph_function |
bpftrace -e 'kprobe:gve_rx_poll{@s[arg0]=nsecs} kretprobe:gve_rx_poll/@s[arg0]/{@=hist((nsecs-@s[arg0])/1000);delete(@s[arg0])} i:s:5{print(@);exit()}' |
perf record -e cycles -g -C 2 sleep 5; perf report | grep gve |
virtio_net |
echo virtnet_poll > set_graph_function |
bpftrace -e 'kprobe:virtnet_poll{@s[arg0]=nsecs} kretprobe:virtnet_poll/@s[arg0]/{@=hist((nsecs-@s[arg0])/1000);delete(@s[arg0])} i:s:5{print(@);exit()}' |
perf stat -e 'virtio_net:*' -C 2 sleep 5 2>/dev/null || perf stat -e net:napi_poll -C 2 sleep 5 |
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo function_graph > current_tracer
echo '드라이버_함수명' > set_graph_function # 위 표의 ftrace 원라이너 적용
echo 1 > tracing_on
sleep 1
echo 0 > tracing_on
cat trace | head -100
AF_XDP Zero-Copy 지원 매트릭스
AF_XDP Zero-Copy는 커널 버퍼 복사 없이 사용자 공간에서 직접 패킷을 송수신하는 최고 성능 경로입니다. 모든 드라이버가 Zero-Copy를 지원하는 것은 아니며, 지원하더라도 구현 성숙도에 차이가 있습니다.
| 드라이버 | AF_XDP ZC | ZC RX | ZC TX | Multi-queue ZC | ZC 성능 (RX, 64B) | Copy 모드 대비 향상 |
|---|---|---|---|---|---|---|
ice |
✓ | ✓ | ✓ | ✓ | ~20 Mpps | 2~3x |
mlx5e |
✓ | ✓ | ✓ | ✓ | ~22 Mpps | 2~3x |
bnxt_en |
✓ | ✓ | ✓ | ✓ | ~15 Mpps | 2x |
i40e |
✓ | ✓ | ✓ | ✓ | ~12 Mpps | 2x |
ixgbe |
✓ | ✓ | ✓ | △ | ~8 Mpps | 1.5~2x |
igc |
✓ | ✓ | ✓ | ✓ | ~1.4 Mpps | 1.5x |
stmmac |
✓ | ✓ | ✓ | ✓ | 하드웨어 의존 | 1.5~2x |
ena |
✗ | — | — | — | Copy만 가능 | — |
virtio_net |
✗ | — | — | — | Copy만 가능 | — |
gve |
✗ | — | — | — | Copy만 가능 | — |
# AF_XDP Zero-Copy 소켓 생성 확인
# xdpsock 도구로 ZC 모드 테스트
xdpsock -i eth0 -r -z # -z: zero-copy 모드
# ZC 지원 여부 확인 (에러 없이 바인드되면 지원)
xdpsock -i eth0 -r -z -q 0
# Copy 모드 (fallback, 모든 드라이버 지원)
xdpsock -i eth0 -r -c # -c: copy 모드
# AF_XDP 큐 할당 확인
ethtool -N eth0 rx-flow-hash udp4 sdfn
ethtool -X eth0 equal 4 # RSS 큐 수 조정
mlx5e와 ice는 가장 성숙한 ZC 구현을 제공합니다.
패킷 처리 한계
네트워크 드라이버의 이론적 최대 PPS(Packets Per Second)는 링크 속도와 패킷 크기에 의해 결정되지만, 실제 달성 가능한 PPS는 드라이버 효율성, CPU 클럭, 메모리 대역폭에 의해 제한됩니다.
병목 지점을 식별하는 방법론입니다.
| 병목 유형 | 증상 | 진단 방법 | 해결 전략 |
|---|---|---|---|
| CPU 병목 | softirq CPU 100%, PPS 정체 | mpstat -P ALL 1, perf top |
XDP, 큐 수 증가, busy-poll |
| 메모리 대역폭 | NUMA 미스, LLC 미스 증가 | perf stat -e LLC-load-misses |
NUMA 배치 최적화, page_pool |
| PCIe 대역폭 | 양방향 100G+ 시 포화 | lspci -vvv (LinkSta) |
PCIe Gen4 x16 이상 슬롯 사용 |
| 링 버퍼 부족 | rx_no_buffer_count 증가 |
ethtool -S 드롭 카운터 |
링 크기 증가, NAPI budget 조정 |
| 인터럽트 폭풍 | IRQ 처리 CPU 과점유 | /proc/interrupts |
코얼레싱 증가, adaptive 활성화 |
| 드라이버 락 경합 | lock_stat에서 spinlock 경합 | perf lock record/report |
per-CPU 큐 분리, lockless 디자인 |
# 종합 성능 진단 스크립트
# 1. CPU softirq 부하 확인
mpstat -P ALL 1 5 | grep -E 'CPU|all|Average'
# 2. 인터럽트 분포 확인
watch -n 1 'grep eth0 /proc/interrupts'
# 3. 드라이버 통계 (드롭/에러 카운터)
ethtool -S eth0 | grep -E 'drop|error|miss|no_buffer|overflow'
# 4. PCIe 대역폭 상태
lspci -s $(ethtool -i eth0 | grep bus-info | awk '{print $2}') -vvv | grep -E 'LnkSta:|Width|Speed'
# 5. perf로 핫스팟 분석
perf top -g --no-children -e cycles:ppp -p $(pgrep -d, ksoftirqd)
CPU 활용 효율 (cycles/packet)
패킷당 CPU 사이클 소비량은 드라이버 효율성의 핵심 지표입니다. 동일한 CPU에서 더 적은 사이클로 패킷을 처리할수록 더 높은 PPS를 달성할 수 있습니다. 이 값은 드라이버의 데이터 경로 최적화 수준, DMA 매핑 전략, 캐시 친화성에 의해 결정됩니다.
| 드라이버 | 일반 RX 경로 (cycles/pkt) | XDP_DROP (cycles/pkt) | XDP_TX (cycles/pkt) | 주요 최적화 |
|---|---|---|---|---|
mlx5e |
~800 | ~100 | ~180 | MPWQE, 인라인 WQE, CQE 압축 |
ice |
~850 | ~110 | ~200 | Flex descriptor, page_pool, DIM |
bnxt_en |
~900 | ~130 | ~220 | TPA (GRO HW), page_pool |
ixgbe |
~1000 | ~150 | ~250 | 페이지 분할, bulk alloc |
i40e |
~950 | ~140 | ~230 | DIM, 동적 ITR |
ena |
~1200 | ~400 | ~500 | LLQ (Low Latency Queue) |
virtio_net |
~1500 | ~500 | ~700 | mergeable buffer, page_pool |
vmxnet3 |
~1800 | ~600 | N/A | 제한적 (가상화 오버헤드) |
r8169 |
~2000 | N/A | N/A | 기본적 (1GbE 전용) |
perf stat -e cycles,instructions -C <cpu> -- sleep 1로 해당 CPU의 사이클 수를 측정하고,
동시에 ethtool -S에서 패킷 카운터 변화를 관찰하여 산출합니다.
정확한 측정을 위해 단일 큐/단일 CPU로 고정하고, 다른 워크로드를 제거해야 합니다.
CPU 효율성에 영향을 미치는 핵심 요소와 드라이버별 최적화 전략입니다.
| 최적화 기법 | 사이클 절감 효과 | 적용 드라이버 | 원리 |
|---|---|---|---|
| page_pool 재활용 | ~150 cycles | mlx5e, ice, bnxt 등 | DMA map/unmap 제거, 페이지 할당 회피 |
| NAPI bulk alloc | ~80 cycles | 대부분 최신 드라이버 | 배치 할당으로 per-packet 오버헤드 분산 |
| XDP 바이패스 | ~600 cycles | XDP 지원 드라이버 | SKB 생성 및 커널 스택 완전 우회 |
| CQE 압축 | ~50 cycles | mlx5e | 여러 CQE를 하나의 압축 CQE로 처리 |
| Inline WQE | ~30 cycles (TX) | mlx5e | 소형 패킷 헤더를 WQE에 인라인 삽입 |
| DIM 적응형 코얼레싱 | 가변 | ice, mlx5e, bnxt | 워크로드에 따라 인터럽트 빈도 자동 조정 |
| GRO HW (TPA) | ~200 cycles | bnxt_en, mlx5e | 하드웨어에서 TCP 세그먼트 병합 |
| Prefetch 힌트 | ~30 cycles | ixgbe, i40e | 다음 디스크립터/데이터를 미리 캐시 로드 |
# CPU 사이클 효율 측정 (단일 코어 고정)
# 1. IRQ를 특정 CPU에 고정
echo 1 > /proc/irq/48/smp_affinity_list
# 2. 해당 CPU의 사이클 측정
perf stat -e cycles,instructions,cache-misses,LLC-load-misses -C 1 -- sleep 10
# 3. 패킷 카운터 확인 (10초 전후 차이 계산)
ethtool -S eth0 | grep rx_packets
# 4. cycles/packet 계산
# cycles_per_pkt = total_cycles / (rx_packets_after - rx_packets_before)
# 5. 함수별 사이클 분포 (드라이버 핫스팟 분석)
perf record -g -C 1 -- sleep 10
perf report --no-children --sort=dso,symbol
- 베어메탈 고성능 NIC (mlx5e, ice): XDP → page_pool → NUMA 배치 → 코얼레싱 순으로 최적화
- 클라우드 가상 NIC (ena, gve): 큐 수 최적화 → 링 버퍼 크기 → 코얼레싱 순으로 최적화 (드라이버 내부 변경 불가)
- 가상화 환경 (virtio_net): vhost-net/vDPA 활성화 → 멀티큐 → mergeable buffer → busy-poll 순으로 최적화
- 레거시/데스크톱 (r8169): 기본 설정으로 충분, 특별한 튜닝 불필요 (1GbE 대역폭이 병목)
DPDK PMD 관점의 NIC 드라이버 분석
지금까지 살펴본 커널 네트워크 드라이버(net_device)는 리눅스 커널 네트워크 스택 내에서 동작합니다. 그러나 통신사 코어, NFV, 고빈도 거래(HFT) 등 극한의 패킷 처리 성능이 요구되는 환경에서는 커널 스택을 완전히 우회하는 DPDK(Data Plane Development Kit)가 사실상 표준입니다. DPDK의 핵심은 PMD(Poll Mode Driver) — 커널 드라이버 대신 유저스페이스에서 NIC 하드웨어를 직접 제어하는 드라이버다. 이 섹션에서는 각 NIC 드라이버의 DPDK PMD 관점 차이, 아키텍처, 성능 비교를 분석합니다.
커널 드라이버 vs DPDK PMD 아키텍처
커널 네트워크 스택에서 패킷이 애플리케이션에 도달하려면 socket → TCP/IP → netfilter → net_device → NIC 경로를 거치며, 매 패킷마다 컨텍스트 스위치, 인터럽트 처리, sk_buff 할당/해제 등 상당한 오버헤드가 발생합니다. DPDK PMD는 이 경로를 전부 제거하고 App → rte_eth_rx/tx_burst() → PMD → NIC로 직접 접근합니다.
UIO vs VFIO-pci 바인딩 메커니즘
DPDK PMD가 NIC 하드웨어에 직접 접근하려면 기존 커널 드라이버에서 디바이스를 분리(unbind)하고 DPDK 호환 드라이버에 바인딩해야 합니다. 두 가지 주요 메커니즘이 있습니다.
| 항목 | igb_uio (레거시) | vfio-pci (권장) |
|---|---|---|
| IOMMU 보호 | 없음 — DMA가 전체 물리 메모리(Physical Memory) 접근 가능 | 있음 — IOMMU가 DMA 범위 제한 |
| 보안 수준 | 낮음 — 악성 PMD가 커널 메모리 손상 가능 | 높음 — HW 수준 격리 |
| 권한 요구 | root 필수 | non-root 가능 (vfio 그룹 권한) |
| 멀티 디바이스 | IOMMU 그룹 무시 — 격리 불가 | IOMMU 그룹 단위 관리 |
| 인터럽트 지원 | 기본 INTX/MSI | MSI-X, eventfd 기반 |
| 상태 | DPDK에서 deprecated, 제거 예정 | 기본 권장, 프로덕션 표준 |
디바이스 바인딩 워크플로우 — dpdk-devbind.py를 사용한 전형적인 바인딩 절차:
# 1. 현재 NIC 상태 확인
dpdk-devbind.py --status
# 출력 예시:
# 0000:03:00.0 'Ethernet Controller E810-C' if=ens3f0 drv=ice unused=vfio-pci
# 0000:03:00.1 'Ethernet Controller E810-C' if=ens3f1 drv=ice unused=vfio-pci
# 2. vfio-pci 모듈 로드
modprobe vfio-pci
# 3. 커널 드라이버에서 분리 후 vfio-pci에 바인딩
dpdk-devbind.py --bind=vfio-pci 0000:03:00.0
# 4. 바인딩 확인
dpdk-devbind.py --status
# 0000:03:00.0 'Ethernet Controller E810-C' drv=vfio-pci unused=ice
# 5. 원복 (운영 중 필요 시)
dpdk-devbind.py --bind=ice 0000:03:00.0
Hugepage 설정
DPDK는 hugepage를 통해 TLB 미스를 최소화하고 대용량 연속 메모리를 확보합니다. NUMA 토폴로지를 반드시 고려해야 합니다.
# 1G hugepage (권장 — TLB 엔트리 절약, 고성능)
# 커널 부팅 파라미터에 추가:
# default_hugepagesz=1G hugepagesz=1G hugepages=8
# 2M hugepage (기본, 런타임 할당 가능)
echo 2048 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# NUMA 노드별 할당 (NIC과 같은 NUMA 노드에 배치)
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 1024 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
# hugetlbfs 마운트
mkdir -p /dev/hugepages
mount -t hugetlbfs nodev /dev/hugepages
# NIC의 NUMA 노드 확인
cat /sys/bus/pci/devices/0000:03:00.0/numa_node
igb_uio는 IOMMU 보호가 없어 PMD 코드의 버그나 공격으로 인해 DMA가 커널 메모리를 직접 읽고 쓸 수 있습니다. 프로덕션 환경에서는 반드시 vfio-pci를 사용하고, BIOS에서 VT-d(Intel) 또는 AMD-Vi를 활성화해야 합니다. 커널 부팅 파라미터에 intel_iommu=on iommu=pt를 추가하면 IOMMU passthrough 모드로 DPDK 최적 성능을 얻을 수 있습니다.
Bifurcated 드라이버 모델
전통적인 DPDK 사용 방식은 NIC을 커널 드라이버에서 분리하여 DPDK에 전용으로 할당하는 것입니다. 그러나 이 방식은 ethtool, ip link, devlink 등 커널 관리 도구를 사용할 수 없고, 디바이스가 커널에서 완전히 사라진다는 운영상의 큰 단점이 있습니다.
Mellanox/NVIDIA mlx5는 이 문제를 근본적으로 해결하는 bifurcated 드라이버 모델을 제공합니다. 커널 mlx5_core 드라이버가 NIC에 바인딩된 상태를 유지하면서, DPDK net_mlx5 PMD가 동시에 같은 NIC의 데이터 경로에 접근합니다.
| 항목 | 전통적 바인딩 (vfio-pci) | Bifurcated 모델 (mlx5) |
|---|---|---|
| 디바이스 바인딩 | 커널 드라이버 unbind → vfio-pci bind 필수 | mlx5_core 유지, 추가 바인딩 불필요 |
| 커널 관리 도구 | 사용 불가 (ethtool, ip, devlink) | 정상 사용 가능 |
| RDMA 공존 | 불가 — 커널 드라이버 언바인드됨 | 가능 — mlx5_ib 동시 동작 |
| SR-IOV 관리 | VF 생성 전 PF unbind 필요 (복잡) | PF/VF 모두 bifurcated 동작 |
| Live Migration | 제한적 (vfio 기반 migration) | mlx5_vdpa 경유 live migration 지원 |
| FW 업데이트 | DPDK 앱 중단 → 커널 드라이버 복원 필요 | devlink fw 명령으로 즉시 가능 |
| 모니터링 | DPDK telemetry API만 가능 | ethtool -S + DPDK telemetry 모두 가능 |
| 운영 복잡도 | 높음 — 바인딩 스크립트, 복구 절차 필요 | 낮음 — 표준 커널 운영 워크플로우 유지 |
ethtool -S), FW 관리(devlink), 장애 복구, live migration이 동시에 필요합니다. Bifurcated 모델은 이 모든 요구를 충족하면서 DPDK 데이터 경로 성능은 동일하게 유지합니다. 이것이 mlx5 NIC이 DPDK 환경에서 사실상 1순위로 선택되는 핵심 이유입니다.
mlx5 testpmd 실행 — 디바이스 언바인드 없이 바로 DPDK 앱을 실행할 수 있습니다:
# mlx5 bifurcated: 커널 드라이버 유지한 채 바로 testpmd 실행
# (dpdk-devbind.py --bind 불필요!)
dpdk-testpmd -l 0-3 -n 4 \
-a 0000:03:00.0 \
-- -i --nb-cores=2 --rxq=2 --txq=2
# 동시에 다른 터미널에서 커널 관리 도구 사용 가능
ethtool -S ens3f0 | grep rx_packets
devlink dev info pci/0000:03:00.0
ip link show ens3f0
# ice (전통적 바인딩)와 비교 — 반드시 unbind 필요
dpdk-devbind.py --bind=vfio-pci 0000:04:00.0
dpdk-testpmd -l 4-7 -n 4 \
-a 0000:04:00.0 \
-- -i --nb-cores=2 --rxq=2 --txq=2
# ethtool ens4f0 → 실패 (커널에서 디바이스 사라짐)
드라이버별 DPDK PMD 종합 비교
리눅스 커널 NIC 드라이버 각각에 대응하는 DPDK PMD의 특성을 종합 비교합니다. 바인딩 모델, rte_flow 하드웨어 오프로드 지원, 성숙도 등이 드라이버 선택의 핵심 기준입니다.
| 커널 드라이버 | DPDK PMD | 바인딩 모델 | rte_flow | 최대 큐 | RSS | FDIR/필터 | 성숙도 | 주요 장점 | 주요 단점 |
|---|---|---|---|---|---|---|---|---|---|
| mlx5_core | net_mlx5 | Bifurcated | Full (최대) | 1024+ | 토플리츠/XOR | 64M+ rules | ★★★★★ | bifurcated, RDMA 공존, rte_flow 완전 지원, ConnectX-7 400G | 드라이버 복잡, NVIDIA FW 종속 |
| ice | net_ice | VFIO | 확장 (DDP) | 2048 | DDP 기반 | 16K TCAM + DDP | ★★★★☆ | DDP 프로파일로 유연한 파싱, AF_XDP PMD 겸용 | DDP 프로파일 필수, bifurcated 미지원 |
| i40e | net_i40e | VFIO | 기본 지원 | 1536 | 토플리츠 | FDIR 8K | ★★★★★ | 가장 오래 검증된 PMD, 매우 안정적, 레퍼런스 구현 | 기능 동결 (ice로 이전), 100G 미지원 |
| bnxt_en | net_bnxt | VFIO | TruFlow | 512 | 토플리츠 | EM+TCAM | ★★★☆☆ | TruFlow 오프로드, OVS-DPDK 지원 | FW 종속성 높음, 커뮤니티 문서 부족 |
| ixgbe | net_ixgbe | VFIO | 기본 지원 | 128 | 토플리츠/XOR | FDIR 32K | ★★★★★ | 10G 레퍼런스, 최고 안정성, 문서 풍부 | 10G 속도 한계, 기능 동결 |
| igb | net_e1000_igb | VFIO | 미지원 | 8 | 제한적 | 기본 필터 | ★★★★☆ | 1G 레거시 환경, 테스트/개발용 | 기능 매우 제한, 성능 한계 |
| virtio_net | net_virtio | virtio-user | 미지원 | 256 | SW 기반 | 없음 | ★★★★☆ | vhost-user 연동, 가상화 표준, virtio-user PMD | HW 오프로드 없음, 호스트 의존 |
| vmxnet3 | net_vmxnet3 | VFIO | 미지원 | 32 | 기본 | 없음 | ★★★☆☆ | VMware 전용 최적화 | rte_flow 미지원, VMware 종속 |
| ena | net_ena | VFIO | 미지원 | 32 | 토플리츠 | 없음 | ★★★☆☆ | AWS 전용, LLQ 모드 저지연 | rte_flow 미지원, AWS 환경 한정 |
| cxgb4 | net_cxgbe | VFIO | 제한적 | 128 | 기본 | 2K 필터 | ★★☆☆☆ | T5/T6 TOE/TLS 오프로드 | DPDK 유지보수 소극적, 커뮤니티 지원 약함 |
| hv_netvsc | net_netvsc | failsafe PMD | 미지원 | 가변 | Azure 관리 | 없음 | ★★★☆☆ | Azure failsafe — VF 핫플러그(Hotplug) 자동 처리 | 간접 접근(failsafe), 성능 제한 |
rte_flow 오프로드 능력 상세 비교
rte_flow는 DPDK의 하드웨어 흐름 분류/오프로드 API로, 드라이버마다 지원 범위가 크게 다르다. 이 차이가 OVS-DPDK, 방화벽, 로드밸런서 등의 성능에 직결됩니다.
| 기능 | mlx5 | ice | i40e | bnxt | ixgbe |
|---|---|---|---|---|---|
| Match: L2 (MAC/VLAN) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Match: L3 (IPv4/IPv6) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Match: L4 (TCP/UDP) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Match: 터널 (VXLAN/GRE/Geneve) | ✓ | ✓ (DDP) | 제한적 | ✓ | ✗ |
| Match: 내부 헤더 (inner L3/L4) | ✓ | ✓ (DDP) | ✗ | ✓ | ✗ |
| Action: Queue/RSS 분배 | ✓ | ✓ | ✓ | ✓ | ✓ |
| Action: Drop | ✓ | ✓ | ✓ | ✓ | ✓ |
| Action: Mark/Flag | ✓ | ✓ | ✓ | ✓ | 제한적 |
| Action: Modify (NAT) | ✓ | ✗ | ✗ | ✓ (TruFlow) | ✗ |
| Action: Encap/Decap | ✓ | ✗ | ✗ | ✓ (TruFlow) | ✗ |
| Action: Count | ✓ | ✓ | ✓ | ✓ | ✗ |
| Action: Meter (QoS) | ✓ | ✗ | ✗ | ✗ | ✗ |
| Action: CT (Connection Tracking) | ✓ | ✗ | ✗ | ✗ | ✗ |
| 최대 룰 수 | 수백만 | 16K | 8K | ~10K | 32K |
| 동적 룰 추가/삭제 | μs 단위 | ms 단위 | ms 단위 | ms 단위 | ms 단위 |
rte_flow 기반 하드웨어 오프로드를 활용하려면 mlx5 또는 bnxt가 사실상 필수입니다. 특히 CT(Connection Tracking) 오프로드, NAT 오프로드, 터널 Encap/Decap까지 완전히 HW로 처리하는 것은 현재 mlx5만 가능합니다. ice/i40e는 기본적인 flow 분류만 가능하고, ena/gve/vmxnet3는 rte_flow 자체를 지원하지 않아 OVS-DPDK 오프로드가 불가능합니다.
DPDK vs 커널 성능 비교 및 선택 가이드
동일 NIC에서 커널 드라이버와 DPDK PMD의 패킷 처리 성능 차이를 비교합니다. 64바이트 소형 패킷, 단일 코어 기준으로 DPDK는 커널 대비 2.5~4배의 처리율을 달성합니다.
testpmd 벤치마크 성능 수치
| 드라이버 | NIC 모델 | 속도 | 커널 (Mpps) | DPDK (Mpps) | 향상 배율 | 비고 |
|---|---|---|---|---|---|---|
| mlx5 | ConnectX-6 Dx | 100G | ~15 | ~40+ | ~2.7× | bifurcated, 멀티코어 시 100+ Mpps |
| ice | E810-C | 100G | ~12 | ~35 | ~2.9× | DDP 최적화 시 추가 향상 |
| i40e | X710-DA4 | 40G | ~10 | ~30 | ~3.0× | 가장 안정적인 DPDK PMD |
| bnxt | BCM57508 | 100G | ~10 | ~28 | ~2.8× | TruFlow 오프로드 시 추가 향상 |
| ixgbe | X520-DA2 | 10G | ~8 | ~14.88 | ~1.86× | 10G 물리 한계(line rate) 달성 |
| virtio | vhost-user | 가변 | ~2 | ~8 | ~4.0× | vhost-user 백엔드 기준, 최대 향상률 |
testpmd 기본 벤치마크 명령어:
# testpmd 기본 벤치마크 (64B 패킷, io 포워딩, 2코어)
dpdk-testpmd -l 0-3 -n 4 -a 0000:03:00.0 -- \
--forward-mode=io \
--nb-cores=2 \
--rxq=2 --txq=2 \
--rxd=2048 --txd=2048 \
--burst=64
# testpmd 내부 명령으로 통계 확인
testpmd> start
testpmd> show port stats all
# macswap 모드 (실제 트래픽 처리 시뮬레이션)
dpdk-testpmd -l 0-3 -n 4 -a 0000:03:00.0 -a 0000:03:00.1 -- \
--forward-mode=macswap \
--nb-cores=2 \
--rxq=2 --txq=2
# DPDK telemetry로 실시간 모니터링
dpdk-telemetry.py
DPDK vs AF_XDP vs 커널 XDP 선택 가이드
DPDK만이 유일한 고성능 솔루션은 아니다. AF_XDP와 커널 XDP도 상황에 따라 적합한 대안이 될 수 있습니다. 워크로드 특성에 따른 선택 기준을 정리합니다.
| 기준 | DPDK PMD | AF_XDP (유저스페이스 XDP) | 커널 XDP |
|---|---|---|---|
| 데이터 경로 | 유저스페이스, 커널 완전 우회 | 유저스페이스, 커널 XDP 경유 | 커널 내부 (드라이버 레벨) |
| 성능 (64B) | ★★★★★ (40+ Mpps) | ★★★★☆ (20~25 Mpps) | ★★★★☆ (25~35 Mpps DROP) |
| 커널 기능 유지 | ✗ (별도 바인딩 필요, bifurcated 제외) | ✓ (커널 드라이버 유지) | ✓ (커널 드라이버 유지) |
| 프로그래밍 모델 | rte_* API (독자 생태계) | AF_XDP 소켓 + libxdp | BPF/XDP 프로그램 (C + clang) |
| NIC 드라이버 지원 | PMD 별도 구현 필요 | 모든 XDP 지원 드라이버 | 모든 XDP 지원 드라이버 |
| zero-copy | 기본 (mempool) | 드라이버 의존 (mlx5, ice, i40e) | 해당 없음 (커널 내부) |
| 운영 복잡도 | 높음 (hugepage, 바인딩, NUMA) | 중간 (소켓 설정) | 낮음 (BPF 프로그램 로드) |
| 적합 용도 | NFV, HFT, 통신사 코어, OVS-DPDK | 커널 통합 유지 + 고성능 필요 | DDoS 필터링, 방화벽, 라우팅(Routing) |
- 극저지연 + 최대 PPS (HFT, 통신사 5G UPF): → DPDK PMD (mlx5 bifurcated 최적)
- 커널 기능 유지 + 고성능 패킷 처리: → AF_XDP (커널 모니터링/관리 가능)
- L3/L4 필터링, DDoS 차단: → 커널 XDP (BPF 프로그램으로 유연한 로직)
- 범용 서버, 관리 편의 우선: → 커널 NAPI (표준 TCP/IP 스택 사용)
- OVS 기반 가상 네트워크: → OVS-DPDK (mlx5/bnxt의 rte_flow 오프로드 활용)
- 가상머신 간 통신: → vhost-user + virtio PMD (zero-copy 가능)
OVS-DPDK vs OVS-kernel 성능 비교
Open vSwitch(OVS)는 가상 네트워크의 표준 스위치이며, 커널 datapath와 DPDK datapath 두 가지 모드로 동작합니다.
| 항목 | OVS-kernel | OVS-DPDK | OVS-DPDK + HW offload |
|---|---|---|---|
| 데이터 경로 | 커널 모듈 (openvswitch.ko) | DPDK PMD (유저스페이스) | NIC 하드웨어 (rte_flow) |
| 64B 처리량 (single core) | ~2~3 Mpps | ~10~15 Mpps | ~40+ Mpps (NIC 처리) |
| 지연 시간 | ~50~100 μs | ~10~20 μs | <5 μs |
| CPU 사용 | 인터럽트 기반, 유휴 시 0% | PMD 폴링, 항상 100% (코어 전용) | 오프로드된 flow는 CPU 0% |
| 드라이버 요구 | 커널 드라이버 | DPDK PMD (vfio-pci/bifurcated) | mlx5/bnxt (rte_flow full) |
| HW offload | tc-flower (제한적) | rte_flow 기반 | rte_flow + CT offload |
구현 가이드: 최소 골격부터 확장까지
- 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과 공통 코어 재사용 전략 수립
상태 관리: carrier, operstate, link 매크로
네트워크 디바이스의 운영 상태(Operational State)는 단일 변수가 아니라 여러 비트 플래그와 참조 카운트의 조합으로 결정됩니다. 드라이버는 dev->state 비트맵, dev->operstate, TX 큐 상태, promiscuous/allmulti 참조 카운트를 정확히 조작해야 합니다.
carrier 상태 관리
netif_carrier_on()/netif_carrier_off()는 물리 링크의 연결/단절을 커널에 알리는 핵심 인터페이스입니다. PHY 드라이버가 링크 상태 변화를 감지하면 phylink_mac_link_up()/phylink_mac_link_down()을 통해 이 함수를 호출합니다.
/* 커널 소스 분석: netif_carrier_on/off 내부 구현 (include/linux/netdevice.h, net/sched/sch_generic.c) */
static inline void netif_carrier_on(struct net_device *dev)
{
/* __LINK_STATE_NOCARRIER 비트를 클리어 → carrier 존재 */
if (test_and_clear_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
if (dev->reg_state == NETREG_REGISTERED)
linkwatch_fire_event(dev); /* 비동기 operstate 업데이트 트리거 */
}
}
static inline void netif_carrier_off(struct net_device *dev)
{
/* __LINK_STATE_NOCARRIER 비트를 설정 → carrier 없음 */
if (!test_and_set_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
if (dev->reg_state == NETREG_REGISTERED)
linkwatch_fire_event(dev);
}
}
/* 커널 소스 분석: linkwatch_fire_event → 워크큐에 operstate 갱신 예약 */
void linkwatch_fire_event(struct net_device *dev)
{
bool urgent = netif_running(dev) && netif_carrier_ok(dev);
if (test_and_set_bit(__LINK_STATE_LINKWATCH_PENDING, &dev->state))
return;
dev_hold(dev);
if (urgent)
mod_delayed_work(system_wq, &linkwatch_work, 0);
else
schedule_delayed_work(&linkwatch_work, linkwatch_nextevent - jiffies);
}
netif_carrier_on/off는 프로세스 컨텍스트(Process Context)뿐 아니라 인터럽트 컨텍스트에서도 안전하게 호출 가능합니다. linkwatch_fire_event()가 실제 상태 전파를 워크큐(Workqueue)로 연기하기 때문입니다.
link state 비트맵 (__LINK_STATE_*)
dev->state 필드는 unsigned long 비트맵으로, 디바이스의 다양한 링크 상태를 원자적(Atomic)으로 관리합니다.
| 비트 | 의미 | 조회 매크로 |
|---|---|---|
__LINK_STATE_START | 디바이스가 ndo_open()을 통해 활성화됨 | netif_running() |
__LINK_STATE_PRESENT | 디바이스가 물리적으로 존재함 (hot-unplug 감지) | netif_device_present() |
__LINK_STATE_NOCARRIER | 물리 링크 없음 (케이블 미연결 등) | netif_carrier_ok() (반전) |
__LINK_STATE_LINKWATCH_PENDING | linkwatch 이벤트가 워크큐에 대기 중 | 직접 조회 불필요 |
__LINK_STATE_DORMANT | 802.1X 인증 대기, supplicant 미완료 | netif_dormant() |
__LINK_STATE_TESTING | 자가 진단(Self-test) 실행 중 | netif_testing() |
netif_* 상태 조회 매크로
| 매크로 | 검사 대상 | 반환값 | 호출 컨텍스트 | 용도 |
|---|---|---|---|---|
netif_running(dev) | test_bit(__LINK_STATE_START, &dev->state) | bool | 모든 컨텍스트 | ndo_open() 이후 true, 데이터 경로 진입 가드 |
netif_carrier_ok(dev) | !test_bit(__LINK_STATE_NOCARRIER, &dev->state) | bool | 모든 컨텍스트 | PHY 링크 연결 상태 확인 |
netif_oper_up(dev) | dev->operstate == IF_OPER_UP | bool | RTNL 또는 RCU | 라우팅 판단 시 실제 운영 상태 확인 |
netif_device_present(dev) | test_bit(__LINK_STATE_PRESENT, &dev->state) | bool | 모든 컨텍스트 | hot-unplug/AER 복구 시 접근 가능 여부 |
netif_dormant(dev) | test_bit(__LINK_STATE_DORMANT, &dev->state) | bool | 모든 컨텍스트 | 802.1X 인증 완료 대기 중 여부 |
netif_testing(dev) | test_bit(__LINK_STATE_TESTING, &dev->state) | bool | 모든 컨텍스트 | 자가 진단 실행 중 여부 |
netif_device_detach() / netif_device_attach()
PCIe AER 복구(Advanced Error Recovery)나 suspend/resume 시나리오에서 디바이스를 일시적으로 분리/재연결하는 함수입니다.
/* 커널 소스 분석: netif_device_detach/attach 내부 (net/core/dev.c) */
void netif_device_detach(struct net_device *dev)
{
/* PRESENT 비트 클리어 → netif_device_present() == false */
if (test_and_clear_bit(__LINK_STATE_PRESENT, &dev->state) &&
netif_running(dev)) {
netif_tx_stop_all_queues(dev); /* 모든 TX 큐 정지 */
}
}
void netif_device_attach(struct net_device *dev)
{
/* PRESENT 비트 설정 → 디바이스 복귀 */
if (!test_and_set_bit(__LINK_STATE_PRESENT, &dev->state) &&
netif_running(dev)) {
netif_tx_wake_all_queues(dev); /* 모든 TX 큐 재개 */
__netdev_watchdog_up(dev);
}
}
/* 개념 예시: PCIe AER 복구에서의 사용 패턴 */
static pci_ers_result_t my_io_error_detected(struct pci_dev *pdev,
pci_channel_state_t state)
{
struct net_device *ndev = pci_get_drvdata(pdev);
netif_device_detach(ndev); /* 패킷 경로 차단 */
if (state == pci_channel_io_perm_failure)
return PCI_ERS_RESULT_DISCONNECT;
my_disable_hw(ndev);
return PCI_ERS_RESULT_NEED_RESET;
}
static void my_io_resume(struct pci_dev *pdev)
{
struct net_device *ndev = pci_get_drvdata(pdev);
my_reinit_hw(ndev);
netif_device_attach(ndev); /* 패킷 경로 복원 */
}
TX 큐 제어 매크로 상세 분석
TX 큐 제어는 dev->_tx[i].state 비트맵의 __QUEUE_STATE_DRV_XOFF 비트를 조작합니다. BQL(Byte Queue Limits)과 연동하여 큐를 정지/재개하면 버퍼블로트(Bufferbloat)를 효과적으로 방지할 수 있습니다.
/* 커널 소스 분석: TX 큐 상태 비트 조작 (include/linux/netdevice.h) */
/* __QUEUE_STATE_DRV_XOFF: 드라이버가 큐를 정지시킨 상태 */
static inline void netif_tx_stop_queue(struct netdev_queue *dev_queue)
{
set_bit(__QUEUE_STATE_DRV_XOFF, &dev_queue->state);
}
static inline void netif_tx_wake_queue(struct netdev_queue *dev_queue)
{
if (test_and_clear_bit(__QUEUE_STATE_DRV_XOFF, &dev_queue->state))
__netif_schedule(dev_queue->qdisc); /* qdisc에 재스케줄 요청 */
}
static inline bool netif_tx_queue_stopped(const struct netdev_queue *dev_queue)
{
return test_bit(__QUEUE_STATE_DRV_XOFF, &dev_queue->state);
}
/* netif_tx_start_all_queues: 모든 큐의 XOFF 비트 클리어 */
static inline void netif_tx_start_all_queues(struct net_device *dev)
{
unsigned int i;
for (i = 0; i < dev->num_tx_queues; i++) {
struct netdev_queue *txq = netdev_get_tx_queue(dev, i);
netif_tx_start_queue(txq);
}
}
/* netif_tx_disable: 모든 큐 정지 + BH 완료 대기 (안전한 종료) */
void netif_tx_disable(struct net_device *dev)
{
unsigned int i;
local_bh_disable();
for (i = 0; i < dev->num_tx_queues; i++) {
struct netdev_queue *txq = netdev_get_tx_queue(dev, i);
__netif_tx_lock(txq, smp_processor_id());
netif_tx_stop_queue(txq);
__netif_tx_unlock(txq);
}
local_bh_enable();
}
ndo_start_xmit()에서 TX 링이 가득 찼을 때 netif_tx_stop_queue()를 호출하고, TX completion 인터럽트에서 공간이 확보되면 netif_tx_wake_queue()를 호출하는 패턴이 표준입니다. 이 stop/wake 짝이 맞지 않으면 큐가 영구 정지되는 TX hang이 발생합니다.
promiscuous/allmulti 모드 관리
dev_set_promiscuity()와 dev_set_allmulti()는 참조 카운트(Reference Count) 기반으로 동작합니다. 여러 소비자(tcpdump, 브릿지, VLAN 등)가 독립적으로 모드를 요청하고 해제할 수 있습니다.
/* 커널 소스 분석: dev_set_promiscuity 내부 (net/core/dev.c) */
static int __dev_set_promiscuity(struct net_device *dev, int inc, bool notify)
{
unsigned int old_flags = dev->flags;
int old_pcount = dev->promiscuity;
dev->promiscuity += inc;
if (dev->promiscuity == 0) {
/* 참조 카운트가 0 → promisc 모드 해제 */
dev->flags &= ~IFF_PROMISC;
} else if (dev->promiscuity > 0) {
dev->flags |= IFF_PROMISC;
}
if (dev->flags != old_flags) {
/* 플래그 변경 시 하드웨어 RX 필터 갱신 */
__dev_set_rx_mode(dev); /* → ndo_set_rx_mode 콜백 호출 */
if (notify)
__dev_notify_flags(dev, old_flags, 0);
}
return 0;
}
/* dev_set_allmulti: 동일한 참조 카운트 패턴 */
int dev_set_allmulti(struct net_device *dev, int inc)
{
unsigned int old_flags = dev->flags;
dev->allmulti += inc;
if (dev->allmulti == 0)
dev->flags &= ~IFF_ALLMULTI;
else if (dev->allmulti > 0)
dev->flags |= IFF_ALLMULTI;
if (dev->flags ^ old_flags)
__dev_set_rx_mode(dev);
return 0;
}
dev_set_promiscuity(dev, 1)을 호출했다면 반드시 dev_set_promiscuity(dev, -1)로 해제해야 합니다. 모듈 언로드나 에러 경로에서 해제를 빠뜨리면 디바이스가 영구적으로 promiscuous 모드에 머무릅니다.
netdev feature 시스템
netdev_features_t 비트맵은 하드웨어 오프로드(Offload) 기능, 프로토콜 기능, 소프트웨어 기능을 단일 64-bit 비트맵으로 관리합니다. ethtool -k로 확인하는 모든 feature가 이 비트맵에 매핑됩니다.
netdev_features_t 비트맵 개요
/* 커널 소스 분석: netdev_features_t 정의 (include/linux/netdev_features.h) */
typedef u64 netdev_features_t;
/* 각 NETIF_F_* 플래그는 단일 비트 위치에 대응 */
#define NETIF_F_SG __NETIF_F(SG)
#define NETIF_F_IP_CSUM __NETIF_F(IP_CSUM)
#define NETIF_F_TSO __NETIF_F(TSO)
#define NETIF_F_GRO __NETIF_F(GRO)
/* ... 60+ 비트 정의 */
주요 NETIF_F_* 플래그 분류
체크섬(Checksum) 오프로드
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_IP_CSUM | IPv4 TCP/UDP 하드웨어 체크섬 계산 | Checksum |
NETIF_F_IPV6_CSUM | IPv6 TCP/UDP 하드웨어 체크섬 계산 | Checksum |
NETIF_F_HW_CSUM | 프로토콜 무관 완전 하드웨어 체크섬 (L3/L4 구분 없이) | Checksum |
NETIF_F_RXCSUM | 수신 체크섬 오프로드 (HW가 검증 완료 표시) | Checksum |
세그멘테이션(Segmentation) 오프로드
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_TSO | TCP Segmentation Offload (IPv4) | Segmentation |
NETIF_F_TSO6 | TSO IPv6 | Segmentation |
NETIF_F_TSO_ECN | ECN 지원 TSO | Segmentation |
NETIF_F_GSO | Generic Segmentation Offload (소프트웨어 fallback 포함) | Segmentation |
NETIF_F_GSO_GRE | GRE 터널 GSO | Segmentation |
NETIF_F_GSO_UDP_TUNNEL | UDP 터널(VXLAN, Geneve) GSO | Segmentation |
Scatter/Gather 및 DMA
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_SG | Scatter/Gather I/O (비연속 메모리 전송) | SG/DMA |
NETIF_F_FRAGLIST | Fragment list 지원 | SG/DMA |
NETIF_F_HIGHDMA | High memory 영역 DMA 가능 | SG/DMA |
수신(Receive) 오프로드
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_GRO | Generic Receive Offload (소프트웨어 패킷 집계) | Receive |
NETIF_F_GRO_HW | 하드웨어 GRO (NIC이 직접 coalescing) | Receive |
NETIF_F_LRO | Large Receive Offload (라우팅 환경 비권장) | Receive |
VLAN 오프로드
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_HW_VLAN_CTAG_TX | VLAN C-Tag 하드웨어 삽입 | VLAN |
NETIF_F_HW_VLAN_CTAG_RX | VLAN C-Tag 하드웨어 추출 | VLAN |
NETIF_F_HW_VLAN_CTAG_FILTER | VLAN 하드웨어 필터링 | VLAN |
기타 기능
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_LLTX | Lockless TX (드라이버 자체 동기화 보장) | TX |
NETIF_F_LOOPBACK | 루프백(Loopback) feature | 기타 |
NETIF_F_NTUPLE | N-tuple 필터 (Flow Director) | 필터 |
NETIF_F_RXHASH | RX 해시 제공 (RSS 연동) | 수신 |
NETIF_F_HW_TC | TC 오프로드 지원 | QoS |
feature 필드 간 관계
net_device에는 feature와 관련된 5개의 필드가 있으며, 각 필드의 역할과 상호 관계를 정확히 이해해야 합니다.
| 필드 | 의미 | 설정 주체 |
|---|---|---|
dev->hw_features | 하드웨어가 지원하는 전체 feature set | 드라이버 probe 시 설정 |
dev->features | 현재 활성 feature (hw_features의 부분집합) | 커널 코어가 계산 |
dev->wanted_features | 사용자가 ethtool -K로 요청한 feature | ethtool 명령으로 설정 |
dev->vlan_features | VLAN 하위 디바이스에 전파할 feature | 드라이버 probe 시 설정 |
dev->hw_enc_features | 터널(Tunnel) encapsulation에 사용할 feature | 드라이버 probe 시 설정 |
netdev_update_features() 내부 흐름
ndo_fix_features 패턴
ndo_fix_features()는 하드웨어 제약에 따라 feature 조합을 보정하는 콜백입니다. TSO는 체크섬 오프로드를 필요로 하는 등 feature 간 의존성(Dependency)을 여기서 강제합니다.
/* 개념 예시: ndo_fix_features에서 feature 의존성 강제 */
static netdev_features_t my_fix_features(struct net_device *ndev,
netdev_features_t features)
{
/* TSO는 체크섬 오프로드가 반드시 필요 */
if (!(features & NETIF_F_HW_CSUM) &&
!(features & (NETIF_F_IP_CSUM | NETIF_F_IPV6_CSUM))) {
features &= ~(NETIF_F_TSO | NETIF_F_TSO6 | NETIF_F_TSO_ECN);
netdev_warn(ndev, "TSO disabled: requires CSUM offload\n");
}
/* SG 없이 TSO 불가 */
if (!(features & NETIF_F_SG))
features &= ~(NETIF_F_TSO | NETIF_F_TSO6);
/* 특정 HW 리비전은 GRO_HW 미지원 */
struct my_priv *priv = netdev_priv(ndev);
if (priv->hw_rev < 3)
features &= ~NETIF_F_GRO_HW;
/* LRO와 포워딩(forwarding)은 공존 불가 */
if (features & NETIF_F_LRO) {
struct net *net = dev_net(ndev);
if (net->ipv4.sysctl_ip_forward)
features &= ~NETIF_F_LRO;
}
return features;
}
ethtool -k eth0에서 [fixed]로 표시되는 항목은 hw_features에 포함되지 않은 feature입니다. [requested on]은 wanted_features에 설정되었으나 ndo_fix_features에 의해 비활성화된 상태입니다. [not requested]은 wanted_features에 설정되지 않은 feature입니다.
netdev notifier chain
netdev 알림 체인(Notifier Chain)은 네트워크 디바이스의 상태 변경 이벤트를 관심 있는 서브시스템에 전달하는 옵저버 패턴(Observer Pattern)입니다. IPv4/IPv6 스택, 라우팅, 방화벽(Firewall), 브릿지(Bridge), bonding 등이 이 체인에 등록하여 디바이스 이벤트에 반응합니다.
주요 NETDEV_* 이벤트
| 이벤트 | 발생 시점 | 주요 수신자 |
|---|---|---|
NETDEV_UP | ndo_open() 성공 후 | IP 스택 (주소 활성화), 라우팅 (경로 추가) |
NETDEV_DOWN | ndo_stop() 호출 전 | IP 스택 (주소 비활성화), 라우팅 (경로 제거) |
NETDEV_REGISTER | register_netdevice() 완료 | sysfs, procfs (디바이스 노드 생성) |
NETDEV_UNREGISTER | unregister_netdevice() 시작 | 모든 참조자 (참조 정리 시작) |
NETDEV_CHANGENAME | 인터페이스 이름 변경 | udev, sysfs (심볼릭 링크 갱신) |
NETDEV_CHANGEADDR | MAC 주소 변경 | ARP, NDP (이웃 캐시 갱신) |
NETDEV_CHANGEMTU | MTU 변경 | IPv6 (MTU 업데이트), PMTUD |
NETDEV_CHANGE | carrier/operstate 변경 | 라우팅 (경로 가중치 업데이트) |
NETDEV_FEAT_CHANGE | feature 변경 | VLAN 하위 디바이스 (feature 재전파) |
NETDEV_PRE_UP | ndo_open() 직전 | 보안 모듈, 정책 검사 (open 거부 가능) |
NETDEV_GOING_DOWN | ndo_stop() 직전 | 연결 종료 시작 신호 |
NETDEV_CHANGE_TX_QUEUE_LEN | TX 큐 길이 변경 | qdisc (큐 재구성) |
NETDEV_BONDING_FAILOVER | bonding failover 발생 | ARP (GARP 전송), 라우팅 |
NETDEV_JOIN | bonding/team 합류 | bonding 매니저 |
NETDEV_RESEND_IGMP | IGMP 재전송 요청 | 멀티캐스트(Multicast) 스택 |
register_netdevice_notifier() 내부
등록 시 기존의 모든 netdev에 대해 NETDEV_REGISTER + NETDEV_UP 이벤트를 리플레이(Replay)합니다. 이를 통해 늦게 등록한 서브시스템도 현재 존재하는 모든 디바이스를 인지할 수 있습니다.
/* 커널 소스 분석: register_netdevice_notifier 내부 (net/core/dev.c) */
int register_netdevice_notifier(struct notifier_block *nb)
{
struct net_device *dev;
struct net *net;
int err;
rtnl_lock();
err = raw_notifier_chain_register(&netdev_chain, nb);
if (err)
goto unlock;
/* 기존 모든 네임스페이스의 모든 netdev에 대해 이벤트 리플레이 */
for_each_net(net) {
for_each_netdev(net, dev) {
err = call_netdevice_register_notifiers(nb, dev);
if (err)
goto rollback;
}
}
unlock:
rtnl_unlock();
return err;
rollback:
/* 실패 시 이미 리플레이한 디바이스에 UNREGISTER 역전 */
raw_notifier_chain_unregister(&netdev_chain, nb);
rtnl_unlock();
return err;
}
/* 개념 예시: notifier 콜백 등록과 핸들러 구현 */
static int my_netdev_event(struct notifier_block *nb,
unsigned long event, void *ptr)
{
struct net_device *dev = netdev_notifier_info_to_dev(ptr);
switch (event) {
case NETDEV_UP:
pr_info("device %s came up\n", dev->name);
my_add_device(dev);
break;
case NETDEV_DOWN:
pr_info("device %s going down\n", dev->name);
my_remove_device(dev);
break;
case NETDEV_CHANGEMTU:
my_update_mtu(dev, dev->mtu);
break;
}
return NOTIFY_DONE;
}
static struct notifier_block my_netdev_notifier = {
.notifier_call = my_netdev_event,
};
/* 모듈 초기화에서 등록 */
register_netdevice_notifier(&my_netdev_notifier);
call_netdevice_notifiers() 내부
이벤트 발생 시 등록된 모든 콜백을 순차 호출합니다. 콜백의 반환값에 따라 체인 실행이 조기 종료될 수 있습니다.
/* 커널 소스 분석: call_netdevice_notifiers (net/core/dev.c) */
int call_netdevice_notifiers(unsigned long val, struct net_device *dev)
{
struct netdev_notifier_info info = {
.dev = dev,
};
return raw_notifier_call_chain(&netdev_chain, val, &info);
/*
* 반환값:
* NOTIFY_DONE (0x0000) — 관심 없음, 다음 콜백 계속
* NOTIFY_OK (0x0001) — 처리 완료, 다음 콜백 계속
* NOTIFY_STOP_MASK (0x8000) — 체인 실행 중단
* NOTIFY_BAD (0x8002) — 오류, 체인 실행 중단
*/
}
/* 개념 예시: notifier 콜백에서 이벤트 거부 (PRE_UP 시) */
static int my_security_check(struct notifier_block *nb,
unsigned long event, void *ptr)
{
struct net_device *dev = netdev_notifier_info_to_dev(ptr);
if (event == NETDEV_PRE_UP) {
if (!my_policy_allows_device(dev)) {
pr_warn("policy denied: %s\n", dev->name);
return NOTIFY_BAD; /* ndo_open() 실패로 전파 */
}
}
return NOTIFY_DONE;
}
per-namespace notifier (6.x+)
커널 6.x부터 register_netdevice_notifier_net()을 사용하면 특정 네트워크 네임스페이스(Network Namespace)의 이벤트만 수신할 수 있습니다. 컨테이너(Container) 환경에서 불필요한 이벤트 수신을 줄여 성능을 개선합니다.
/* 커널 소스 분석: per-namespace notifier 등록 */
int register_netdevice_notifier_net(struct net *net,
struct notifier_block *nb)
{
int err;
rtnl_lock();
err = raw_notifier_chain_register(&net->netdev_chain, nb);
if (!err) {
struct net_device *dev;
/* 해당 네임스페이스의 디바이스만 리플레이 */
for_each_netdev(net, dev)
call_netdevice_register_notifiers(nb, dev);
}
rtnl_unlock();
return err;
}
netdev 유틸리티 함수/매크로
네트워크 디바이스 관리에 자주 사용되는 헬퍼 함수(Helper Function)와 매크로를 정리합니다. 이 함수들은 드라이버 코드 전반에서 반복적으로 등장하므로 내부 구현을 이해하면 코드 리딩(Code Reading) 속도가 크게 향상됩니다.
netdev_priv() 내부 구현
수명주기 섹션에서 다룬 netdev_priv()는 net_device 구조체 바로 뒤에 정렬된 드라이버 전용 영역의 포인터를 반환합니다.
/* 커널 소스 분석: netdev_priv 내부 (include/linux/netdevice.h) */
static inline void *netdev_priv(const struct net_device *dev)
{
return (void *)((char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN));
}
SET_NETDEV_DEV / SET_NETDEV_DEVTYPE
SET_NETDEV_DEV()는 ndev->dev.parent를 설정하여 sysfs 계층, 전원 관리(Power Management), DMA 디바이스를 연결합니다. 반드시 register_netdev() 전에 호출해야 합니다.
/* 커널 소스 분석: SET_NETDEV_DEV 매크로 (include/linux/netdevice.h) */
#define SET_NETDEV_DEV(net, pdev) ((net)->dev.parent = (pdev))
#define SET_NETDEV_DEVTYPE(net, devtype) ((net)->dev.type = (devtype))
/* 개념 예시: PCI 디바이스와 netdev 연결 */
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *ndev;
ndev = alloc_etherdev(sizeof(struct my_priv));
if (!ndev)
return -ENOMEM;
/* sysfs: /sys/class/net/eth0/device → PCI 디바이스 링크
* DMA: dev_is_dma_coherent(&ndev->dev) 올바르게 동작
* PM: netdev가 PCI 전원 관리 계층에 포함 */
SET_NETDEV_DEV(ndev, &pdev->dev);
pci_set_drvdata(pdev, ndev);
return register_netdev(ndev); /* SET_NETDEV_DEV 이후에 호출 */
}
for_each_netdev 매크로 패밀리
/* 커널 소스 분석: for_each_netdev 매크로들 (include/linux/netdevice.h) */
/* RTNL lock 보유 상태에서 순회 */
#define for_each_netdev(net, d) \
list_for_each_entry(d, &(net)->dev_base_head, dev_list)
/* RCU read lock 하에서 순회 (데이터 경로 안전) */
#define for_each_netdev_rcu(net, d) \
list_for_each_entry_rcu(d, &(net)->dev_base_head, dev_list)
/* 순회 중 디바이스 삭제 가능 (safe 변형) */
#define for_each_netdev_safe(net, d, n) \
list_for_each_entry_safe(d, n, &(net)->dev_base_head, dev_list)
/* 특정 지점부터 계속 순회 */
#define for_each_netdev_continue(net, d) \
list_for_each_entry_continue(d, &(net)->dev_base_head, dev_list)
/* 개념 예시: 네임스페이스 내 모든 디바이스 MTU 출력 */
void dump_all_mtus(struct net *net)
{
struct net_device *dev;
rcu_read_lock();
for_each_netdev_rcu(net, dev) {
pr_info("%s: mtu=%d\n", dev->name, dev->mtu);
}
rcu_read_unlock();
}
dev_alloc_name() / dev_get_valid_name()
printf 스타일 패턴("eth%d")으로 인터페이스 이름을 자동 할당합니다. IDA(ID Allocator) 기반으로 번호를 할당하며, IFNAMSIZ (16바이트) 길이 제한이 적용됩니다.
/* 커널 소스 분석: dev_alloc_name 패턴 (net/core/dev.c) */
int dev_alloc_name(struct net_device *dev, const char *name)
{
/* name = "eth%d" → "eth0", "eth1", ... 자동 할당
* 내부적으로 __dev_alloc_name 호출 → IDA 기반 번호 할당
* IFNAMSIZ(16) 초과 시 -EINVAL */
return dev_get_valid_name(dev_net(dev), dev, name);
}
netdev_info/warn/err/dbg 로깅 매크로
디바이스 이름을 자동으로 포함하는 로깅 매크로 패밀리입니다. netdev_dbg는 CONFIG_DYNAMIC_DEBUG 활성 시 런타임에 on/off 제어가 가능합니다.
/* 커널 소스 분석: netdev 로깅 매크로 (include/linux/netdevice.h) */
#define netdev_info(dev, fmt, ...) \
dev_info(&(dev)->dev, fmt, ##__VA_ARGS__)
/* 출력: "my_driver 0000:03:00.0 eth0: link up at 10Gbps" */
/* 레벨별 매크로: emerg > alert > crit > err > warn > notice > info > dbg */
#define netdev_dbg(dev, fmt, ...) /* CONFIG_DYNAMIC_DEBUG 시 런타임 제어 */
#define netdev_info(dev, fmt, ...)
#define netdev_notice(dev, fmt, ...)
#define netdev_warn(dev, fmt, ...)
#define netdev_err(dev, fmt, ...)
#define netdev_crit(dev, fmt, ...)
#define netdev_alert(dev, fmt, ...)
#define netdev_emerg(dev, fmt, ...)
/* 개념 예시: 드라이버 링크 상태 로깅 */
static void my_link_up(struct net_device *ndev, int speed)
{
netdev_info(ndev, "link up at %dMbps\n", speed);
netif_carrier_on(ndev);
}
static void my_link_down(struct net_device *ndev)
{
netdev_warn(ndev, "link down detected\n");
netif_carrier_off(ndev);
}
/* Dynamic debug 런타임 제어:
* echo 'module my_driver +p' > /sys/kernel/debug/dynamic_debug/control */
static void my_rx_debug(struct net_device *ndev, struct sk_buff *skb)
{
netdev_dbg(ndev, "rx: len=%u protocol=0x%04x\n",
skb->len, ntohs(skb->protocol));
}
dev_hold() / dev_put() 참조 카운트 관리
커널 6.0 이후 refcnt_tracker 기반 참조 카운팅(Reference Counting)이 도입되어 참조 누수를 추적할 수 있습니다.
/* 커널 소스 분석: dev_hold/dev_put 내부 (include/linux/netdevice.h) */
static inline void dev_hold(struct net_device *dev)
{
netdev_hold(dev, NULL, GFP_ATOMIC);
}
static inline void dev_put(struct net_device *dev)
{
netdev_put(dev, NULL);
}
/* 커널 소스 분석: 내부 구현 — refcount + tracker */
static inline void netdev_hold(struct net_device *dev,
struct netdev_tracker *tracker,
gfp_t gfp)
{
if (dev) {
refcount_inc(&dev->dev_refcnt);
netdev_tracker_alloc(dev, tracker, gfp);
}
}
static inline void netdev_put(struct net_device *dev,
struct netdev_tracker *tracker)
{
if (dev) {
netdev_tracker_free(dev, tracker);
if (refcount_dec_and_test(&dev->dev_refcnt))
netdev_free(dev); /* 참조 0 도달 → 해제 트리거 */
}
}
/* 개념 예시: 추적 가능한 참조 카운트 사용 (6.0+) */
struct my_context {
struct net_device *dev;
struct netdev_tracker dev_tracker;
};
void my_grab_dev(struct my_context *ctx, struct net_device *dev)
{
netdev_hold(dev, &ctx->dev_tracker, GFP_KERNEL);
ctx->dev = dev;
}
void my_release_dev(struct my_context *ctx)
{
netdev_put(ctx->dev, &ctx->dev_tracker);
ctx->dev = NULL;
}
eth_type_trans() 내부
eth_type_trans()는 수신 패킷의 이더넷(Ethernet) 헤더를 분석하여 skb의 프로토콜, 패킷 유형, 디바이스를 설정하는 핵심 함수입니다.
/* 커널 소스 분석: eth_type_trans 내부 (net/ethernet/eth.c) */
__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)
{
unsigned short _service_access_point;
const struct ethhdr *eth;
skb->dev = dev;
skb_reset_mac_header(skb);
eth = (const struct ethhdr *)skb_mac_header(skb);
skb_pull_inline(skb, ETH_HLEN); /* 14바이트 이더넷 헤더 소비 */
/* pkt_type 결정: 목적지 MAC 주소 기반 */
if (unlikely(!ether_addr_equal_64bits(eth->h_dest, dev->dev_addr))) {
if (is_multicast_ether_addr_64bits(eth->h_dest)) {
if (ether_addr_equal_64bits(eth->h_dest, dev->broadcast))
skb->pkt_type = PACKET_BROADCAST;
else
skb->pkt_type = PACKET_MULTICAST;
} else {
skb->pkt_type = PACKET_OTHERHOST;
}
}
/* 기본값은 PACKET_HOST (우리 MAC 주소와 일치) */
/* protocol 필드 결정 */
if (likely(eth_proto_is_802_3(eth->h_proto)))
return eth->h_proto; /* ETH_P_IP, ETH_P_IPV6, ETH_P_ARP 등 */
/* 802.2 LLC 프레임 처리 (레거시) */
return htons(ETH_P_802_2);
}
dev_set_mtu() / dev_validate_mtu()
dev_set_mtu()는 RTNL lock 하에서 min_mtu/max_mtu 범위를 먼저 검증한 뒤, 드라이버의 ndo_change_mtu 콜백을 호출합니다. 코어 검증과 드라이버 검증이 분리되어 있습니다.
/* 커널 소스 분석: dev_set_mtu 내부 (net/core/dev.c) */
int dev_set_mtu(struct net_device *dev, int new_mtu)
{
int err, old_mtu;
ASSERT_RTNL();
/* 코어 범위 검증 (드라이버가 probe 시 설정한 min/max) */
err = dev_validate_mtu(dev, NULL, new_mtu, NULL);
if (err)
return err;
if (!netif_device_present(dev))
return -ENODEV;
old_mtu = dev->mtu;
err = __dev_set_mtu(dev, new_mtu);
if (err)
return err;
if (dev->mtu != old_mtu)
call_netdevice_notifiers(NETDEV_CHANGEMTU, dev);
return 0;
}
/* 코어 MTU 범위 검증 */
int dev_validate_mtu(struct net_device *dev, struct nlattr *tb[],
int new_mtu, struct netlink_ext_ack *extack)
{
int min_mtu = dev->min_mtu ? dev->min_mtu : ETH_MIN_MTU;
int max_mtu = dev->max_mtu ? dev->max_mtu : INT_MAX;
if (new_mtu < min_mtu || new_mtu > max_mtu)
return -EINVAL;
return 0;
}
dev_change_flags() 내부
dev_change_flags()는 IFF_UP, IFF_PROMISC, IFF_ALLMULTI 등 디바이스 플래그 변경을 처리하는 중앙 함수입니다.
/* 커널 소스 분석: dev_change_flags 핵심 경로 (net/core/dev.c) */
int dev_change_flags(struct net_device *dev, unsigned int flags,
struct netlink_ext_ack *extack)
{
unsigned int old_flags = dev->flags;
int ret;
ret = __dev_change_flags(dev, flags, extack);
if (ret < 0)
return ret;
/* 변경 통지 */
__dev_notify_flags(dev, old_flags, 0);
return ret;
}
int __dev_change_flags(struct net_device *dev, unsigned int flags,
struct netlink_ext_ack *extack)
{
unsigned int changes = flags ^ dev->flags;
/* IFF_UP 토글 처리 */
if (changes & IFF_UP) {
if (flags & IFF_UP)
return __dev_open(dev, extack); /* ndo_open 호출 */
else
__dev_close_many(&single); /* ndo_stop 호출 */
}
/* IFF_PROMISC 변경: 참조 카운트 조정 */
if (changes & IFF_PROMISC) {
int inc = (flags & IFF_PROMISC) ? 1 : -1;
dev_set_promiscuity(dev, inc);
}
/* IFF_ALLMULTI 변경: 참조 카운트 조정 */
if (changes & IFF_ALLMULTI) {
int inc = (flags & IFF_ALLMULTI) ? 1 : -1;
dev_set_allmulti(dev, inc);
}
return 0;
}
/* 변경 완료 후 통지 */
void __dev_notify_flags(struct net_device *dev, unsigned int old_flags,
unsigned int gchanges)
{
unsigned int changes = dev->flags ^ old_flags;
if (changes)
rtmsg_ifinfo(RTM_NEWLINK, dev, changes, GFP_KERNEL);
if (changes & IFF_UP)
call_netdevice_notifiers(
(dev->flags & IFF_UP) ? NETDEV_UP : NETDEV_DOWN, dev);
if (changes & ~(IFF_UP | IFF_PROMISC | IFF_ALLMULTI | IFF_VOLATILE))
call_netdevice_notifiers(NETDEV_CHANGE, dev);
}
netif_receive_skb() / __netif_receive_skb_core() 내부 처리 순서
수신된 sk_buff가 프로토콜 핸들러에 전달되기까지의 전체 처리 파이프라인입니다. 각 단계에서 패킷이 가로채지거나 소비될 수 있습니다.
/* 커널 소스 분석: __netif_receive_skb_core 핵심 경로 (net/core/dev.c) */
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
struct packet_type **ppt_prev)
{
struct sk_buff *skb = *pskb;
struct net_device *orig_dev = skb->dev;
/* 1단계: ptype_all — 패킷 탭(tcpdump, AF_PACKET) */
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
/* 2단계: ingress qdisc / tc BPF */
skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev,
&another);
if (!skb)
goto out; /* TC_ACT_STOLEN: 패킷 소비됨 */
/* 3단계: rx_handler (bridge, OVS, macvlan, bonding) */
rx_handler = rcu_dereference(skb->dev->rx_handler);
if (rx_handler) {
switch (rx_handler(&skb)) {
case RX_HANDLER_CONSUMED:
goto out;
case RX_HANDLER_ANOTHER:
goto another;
case RX_HANDLER_EXACT:
deliver_exact = 1;
break;
case RX_HANDLER_PASS:
break;
}
}
/* 4단계: VLAN 처리 */
if (skb_vlan_tag_present(skb)) {
if (vlan_do_receive(&skb))
goto another; /* VLAN 하위 디바이스로 재진입 */
}
/* 5단계: ptype_base — 프로토콜 핸들러 매칭 */
type = skb->protocol;
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&ptype_base[ntohs(type) & PTYPE_HASH_MASK]);
/* 6단계: deliver_skb → ip_rcv / ipv6_rcv / arp_rcv */
if (pt_prev)
*ppt_prev = pt_prev;
return ret;
}
dev_change_net_namespace() 내부
네트워크 디바이스를 다른 네트워크 네임스페이스로 이동하는 함수입니다. 컨테이너에 인터페이스를 할당할 때 사용됩니다.
/* 커널 소스 분석: dev_change_net_namespace 핵심 경로 (net/core/dev.c) */
int dev_change_net_namespace(struct net_device *dev,
struct net *net,
const char *pat)
{
struct net *net_old = dev_net(dev);
int err;
char buf[IFNAMSIZ];
ASSERT_RTNL();
/* 열린 소켓이 있으면 이동 불가 */
if (dev->flags & IFF_UP)
return -EBUSY;
/* 현재 네임스페이스에서 해제 통지 */
call_netdevice_notifiers(NETDEV_UNREGISTER, dev);
/* 새 네임스페이스에서 이름 충돌 확인 */
if (__dev_get_by_name(net, dev->name)) {
/* 충돌 시 pat 패턴으로 자동 이름 변경 */
if (pat) {
err = dev_get_valid_name(net, dev, pat);
if (err < 0)
return err;
} else {
snprintf(buf, IFNAMSIZ, "dev%%d");
err = dev_get_valid_name(net, dev, buf);
if (err < 0)
return err;
}
}
/* 실제 네임스페이스 전환 */
dev_net_set(dev, net);
/* sysfs, procfs 등 리소스 재생성 */
err = device_rename(&dev->dev, dev->name);
/* 새 네임스페이스에서 등록 통지 */
call_netdevice_notifiers(NETDEV_REGISTER, dev);
return 0;
}
/* 개념 예시: ip netns exec를 통한 네임스페이스 이동 */
/*
* $ ip link set eth1 netns my_container
* → dev_change_net_namespace(eth1, my_container_ns, NULL)
* → NETDEV_UNREGISTER (기존 ns) → 이동 → NETDEV_REGISTER (새 ns)
*/
dev_change_net_namespace()는 디바이스가 IFF_UP 상태이면 -EBUSY를 반환합니다. 따라서 ip link set dev eth1 down으로 먼저 인터페이스를 내린 뒤 네임스페이스를 이동해야 합니다. veth 쌍의 한쪽을 컨테이너에 할당하는 것이 일반적인 패턴입니다.
관련 문서
- 디바이스 드라이버 — 전체 드라이버 프레임워크 문맥
- 네트워크 스택 — 커널 패킷 경로와 NAPI 배경
- TUN/TAP — 가상 인터페이스 구현 상세
- BPF/XDP — 드라이버 레벨 초고속 패킷 처리
- AF_XDP — 유저스페이스 zero-copy 경로