네트워크 디바이스 드라이버 (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) 분석까지 실전 네트워크 드라이버 개발에 필요한 핵심을 다룹니다.

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

핵심 요약

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

단계별 이해

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

개요: net_device 드라이버의 역할

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

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

struct net_device 핵심 필드 상세 분석

struct net_device는 리눅스 네트워크 스택에서 가장 핵심적인 구조체로, 하나의 네트워크 인터페이스가 가져야 할 모든 상태 정보를 담고 있습니다. 커널 6.x 기준 이 구조체의 크기는 약 2KB에 달하며, 수십 개의 필드가 캐시 라인 최적화(Cache Line Optimization)를 고려하여 배치되어 있습니다. 특히 핫 패스(Hot Path)에서 자주 접근하는 필드들은 ____cacheline_aligned_in_smp 어노테이션(Annotation)을 통해 동일한 캐시 라인에 모이도록 정렬됩니다. 이는 SMP 환경에서 캐시 바운싱(Cache Bouncing)을 최소화하고, 패킷 송수신 경로의 지연 시간(Latency)을 줄이기 위한 의도적 설계입니다. net_deviceinclude/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;
    /* ... 그 외 다수 필드 ... */
};
캐시 라인 정렬의 효과: TX 큐 관련 필드(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 updev_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.hIFF_* 이름으로 정의되어 있으며, 사용자 공간의 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_statenet_device의 등록 생명주기(Registration Lifecycle)를 추적하는 상태 머신입니다. register_netdevice()에서 NETREG_REGISTERED로 전이되고, unregister_netdevice()를 거쳐 최종적으로 free_netdev()에서 해제됩니다. 각 상태 전이는 RTNL 락(Lock) 하에서만 수행됩니다.

NETREG_ UNINITIALIZED NETREG_ REGISTERED NETREG_ UNREGISTERING NETREG_ UNREGISTERED NETREG_ RELEASED register_ netdevice() unregister_ netdevice() netdev_run_ todo() netdev_ release() alloc_netdev() 직후 초기 상태 sysfs 노출, 패킷 송수신 가능 해제 진행 중, 참조 카운트 대기 sysfs 제거됨, notifier 완료 free_netdev() 호출 가능
/* 커널 소스 분석: 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);
    }
}
net_device 필드 직접 수정 금지: 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 구조체 바로 뒤의 정렬된 주소를 반환합니다.

struct net_device ~2500+ bytes PAD NETDEV _ALIGN private data sizeof_priv bytes TX queues txqs × queue RX queues rxqs × queue kvzalloc() 할당 영역 netdev_priv(dev) = (char *)dev + ALIGN(sizeof(net_device), 32) dev ← NETDEV_ALIGN = 32 bytes 경계 → alloc_netdev_mqs() 초기화 순서 kvzalloc() dev_addr/mc/uc_init alloc TX/RX queues setup() 콜백 dev_net_set() TX/RX 큐는 별도 kmalloc_array()로 할당 → net_device 본체와 독립 메모리 setup() 콜백이 프로토콜별 기본값 설정 (이더넷: ether_setup, CAN: can_setup 등)

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      = &eth_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() 등 제공
     * 이더넷 헤더 생성·파싱을 표준화 */
}
ether_setup()의 역할: 이 함수가 설정하는 값들은 대부분 이더넷 표준(IEEE 802.3)에서 정의한 것입니다. 드라이버는 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 프레임워크 전용 래퍼 프레임워크 결정
멀티큐(Multi-Queue) 선택 기준: 최신 고성능 NIC는 RSS(Receive Side Scaling)와 다중 TX 큐를 지원합니다. 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;
}
register_netdevice() 내부 호출 체인 ASSERT_RTNL() 잠금 보유 확인 dev_get_valid_name() 이름 검증 / %d 번호 부여 dev_new_index() ifindex 할당 ndo_init() [선택적] 드라이버 초기화 콜백 netdev_register_kobject() /sys/class/net/<name> 생성 reg_state = NETREG_REGISTERED 상태 전이 list_netdevice() 해시 테이블 삽입 call_netdevice_notifiers(NETDEV_REGISTER) 방화벽·라우팅·netfilter 알림 name_hlist 이름 → net_device 매핑 index_hlist ifindex → net_device 매핑 reg_state 상태 전이 UNINITIALIZED register REGISTERED unregister UNREGISTERING todo UNREGISTERED

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 변형 선택 기준: 패킷 수신 경로(softirq)처럼 이미 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);

    /* ... 하드웨어 초기화 */
}
netdev_priv()의 설계 이점: 사설 데이터를 net_device와 같은 메모리 블록에 배치함으로써 (1) 별도 kmalloc()이 불필요하고, (2) net_device와 사설 데이터가 인접하여 CPU 캐시 지역성(Cache Locality)이 향상되며, (3) 해제 시 kvfree() 한 번으로 모두 해제됩니다. 이 패턴은 네트워크 디바이스뿐 아니라 ieee80211_hw, ib_device 등 다른 서브시스템에서도 동일하게 사용됩니다.
Here is the expanded HTML content: ```html

핵심 콜백: 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_initregister_netdevice()RTNL held, process추가 초기화 (alloc_netdev 이후 보충)선택
ndo_uninitunregister_netdevice()RTNL held, processinit에서 할당한 자원 정리선택
ndo_opendev_open()ip link set upRTNL held, processHW 활성화, IRQ 요청, NAPI 시작필수
ndo_stopdev_close()ip link set downRTNL held, processHW 비활성화, IRQ 해제, NAPI 정지필수

그룹 B — 데이터 경로(Data Path)

콜백호출 시점컨텍스트핵심 책임필수
ndo_start_xmitdev_hard_start_xmit()softirq 또는 process, per-queue _xmit lock heldskb를 HW TX 링에 전달필수
ndo_select_queuenetdev_pick_tx()process/softirqTX 큐 인덱스 선택 (멀티큐)선택
ndo_get_stats64dev_get_stats()RTNL 또는 RCU readper-CPU 통계를 rtnl_link_stats64에 집계선택

그룹 C — 주소/필터 설정

콜백호출 시점컨텍스트핵심 책임필수
ndo_set_rx_mode__dev_set_rx_mode()netif_addr_lock held (spin), BH disabledMC/UC 필터 HW 프로그래밍권장
ndo_set_mac_addressdev_set_mac_address()RTNL held, processMAC 주소 변경, HW 반영선택
ndo_validate_addrdev_open() 내부RTNL held, processMAC 주소 유효성 검증선택
ndo_change_mtudev_set_mtu()RTNL held, processMTU 변경 시 HW/ring 재설정선택

그룹 D — VLAN

콜백호출 시점컨텍스트핵심 책임필수
ndo_vlan_rx_add_vidvlan_vid_add()RTNL held, processHW VLAN 필터에 VID 등록VLAN 필터 시
ndo_vlan_rx_kill_vidvlan_vid_del()RTNL held, processHW VLAN 필터에서 VID 제거VLAN 필터 시

그룹 E — Feature/오프로드(Offload)

콜백호출 시점컨텍스트핵심 책임필수
ndo_fix_featuresnetdev_update_features()RTNL held, processHW 제약에 따른 feature 조합 강제 보정선택
ndo_set_featuresnetdev_update_features()RTNL held, process변경된 feature를 HW 레지스터에 적용선택
ndo_features_checknetif_skb_features()data path (softirq/process)per-skb feature 호환성 검사, SW fallback 결정선택

그룹 F — TC/XDP 오프로드

콜백호출 시점컨텍스트핵심 책임필수
ndo_setup_tcTC qdisc/filter 설정 시RTNL held, processTC 오프로드 구성 (mqprio, flower 등)선택
ndo_bpfXDP 프로그램 attach/detachRTNL held, processXDP 프로그램 설치, HW 오프로드XDP 시
ndo_xdp_xmitXDP_TX / XDP_REDIRECT 처리NAPI context (softirq)XDP 프레임 배치 전송, doorbell 최적화XDP 시

그룹 G — 브릿지/FDB

콜백호출 시점컨텍스트핵심 책임필수
ndo_fdb_addFDB 엔트리 추가 요청RTNL held, processHW FDB 테이블에 MAC 등록switchdev 시
ndo_fdb_delFDB 엔트리 삭제 요청RTNL held, processHW FDB 테이블에서 MAC 제거switchdev 시
ndo_fdb_dumpFDB 전체 덤프 요청RTNL held, processHW FDB 엔트리 열거switchdev 시
ndo_bridge_setlink브릿지 포트 설정 변경RTNL held, process브릿지 모드, learning 등 HW 설정선택
ndo_bridge_getlink브릿지 포트 설정 조회RTNL held, process현재 브릿지 설정 반환선택

그룹 H — 에러/복구

콜백호출 시점컨텍스트핵심 책임필수
ndo_tx_timeoutTX watchdog 타임아웃process (workqueue)TX 경로 복구, HW 리셋권장
ndo_change_carrierdev_change_carrier()RTNL held, processcarrier 상태 수동 변경 (가상 장치)선택

그룹 I — ioctl/확장

콜백호출 시점컨텍스트핵심 책임필수
ndo_eth_ioctlSIOCSHWTSTAMP 등 이더넷 ioctlRTNL held, processHW 타임스탬프 설정, PHY ioctl 위임선택
ndo_siocdevprivate드라이버 전용 ioctlRTNL held, process레거시 드라이버별 커스텀 ioctl 처리선택
ndo_get_phys_port_idnetlink 쿼리RTNL held, process물리 포트 식별자 반환 (switchdev)switchdev 시
ndo_get_phys_port_namenetlink 쿼리RTNL held, process물리 포트 이름 반환 (representor)switchdev 시
ndo_get_port_parent_idnetlink 쿼리RTNL 또는 RCU부모 스위치 ID 반환 (switchdev)switchdev 시

그룹 J — 가상화(SR-IOV)

콜백호출 시점컨텍스트핵심 책임필수
ndo_set_vf_macip link set vf N macRTNL held, processVF의 MAC 주소 설정SR-IOV 시
ndo_set_vf_vlanip link set vf N vlanRTNL held, processVF의 VLAN 태그 설정SR-IOV 시
ndo_set_vf_rateip link set vf N rateRTNL held, processVF의 TX rate 제한 설정SR-IOV 시
ndo_set_vf_spoofchkip link set vf N spoofchkRTNL held, processVF의 MAC/VLAN spoof 검사 활성화SR-IOV 시
ndo_get_vf_configVF 설정 조회RTNL held, processVF의 현재 설정 반환SR-IOV 시

그룹 K — 네임스페이스/링크

콜백호출 시점컨텍스트핵심 책임필수
ndo_get_iflinknetlink 쿼리, 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_mtudev->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;
    }
}
커널 6.1+ 자동 수집: 드라이버가 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)의 변경은 세 가지 콜백이 협력하여 처리됩니다. 이 세 콜백은 서로 다른 시점에 호출되며, 각각의 역할이 명확히 구분됩니다.

ethtool -K eth0 tso on netdev_update_features() net/core/dev.c ndo_fix_features() HW 제약 강제 적용 예: TSO 없이 CSUM 불가 → CSUM 제거 features 변경됨? old vs new 비교 Yes ndo_set_features() HW 레지스터 반영 netdev_features_change() No → 변경 없음 데이터 경로 (per-skb) netif_skb_features() ndo_features_check() 이 skb에 대해 feature 사용 가능? 불가능한 feature 비트 제거 → SW fallback HW 오프로드 feature 유지 SW fallback 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; /* 실제 전송된 프레임 수 반환 */
}
XDP 큐 분리: 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_TSTATSdev_get_tstats64(), NETDEV_PCPU_STAT_DSTATSdev_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_timeoutNULL이면 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;
}
성능 포인트: GRO를 쓰는 드라이버는 napi_gro_receive() 경로와 page recycling 전략을 함께 설계해야 합니다. 고속 NIC에서는 RX ring refill 정책이 drop/jitter를 크게 좌우합니다.

RX 전체 파이프라인(Pipeline)

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

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

GRO (Generic Receive Offload) 동작 원리

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

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

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

    skb_gro_reset_offset(skb);

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

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

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

NAPI 상태 머신

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

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

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

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

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

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

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

    return 0;
}

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

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

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

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

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

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

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

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

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

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

    spin_lock_irqsave(&priv->tx_lock, flags);

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

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

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

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

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

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

TX 전체 파이프라인

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

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

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

BQL(Byte Queue Limits)은 커널 lib/dynamic_queue_limits.c에 구현된 동적 큐 깊이 제어 알고리즘입니다. NIC TX 큐에 쌓을 수 있는 바이트 수를 실시간(Real-time)으로 조정하여, 큐가 과도하게 깊어지는 버퍼블로트(bufferbloat)를 방지하면서도 충분한 처리량을 유지합니다. 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 없음) 큐 깊이 (BQL 적용) 지연 (BQL 적용) BQL LIMIT (동적) 버퍼블로트 영역 — 실선: BQL 적용 --- 점선: BQL 미적용 BQL은 처리량 유지 + 지연 억제
BQL이 없으면 TX 큐 깊이가 무한히 증가하여 지연이 급등합니다. BQL은 동적 LIMIT으로 큐 깊이를 최소한으로 유지합니다.

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

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

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

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

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

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

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

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

핵심 자료구조: struct dql

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

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

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

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

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

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

드라이버 API 통합

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

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

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

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

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

sysfs 인터페이스와 튜닝

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

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

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

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

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

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

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

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

BQL과 qdisc/TC의 상호작용

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

통계와 ethtool 연동

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

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

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

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

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

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

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

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

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

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

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

phylib 상태 머신 내부 분석

커널의 PHY 추상화 계층(phylib)은 drivers/net/phy/phy.c에 구현된 상태 머신(State Machine)을 중심으로 동작합니다. phy_state_machine() 함수가 delayed_work로 주기적으로 호출되며, PHY의 링크 상태를 감시하고 MAC 드라이버에 변화를 통지합니다.

PHY 상태 머신은 다음 6개 상태를 순환합니다.

상태의미진입 조건
PHY_DOWN0PHY가 초기화되지 않았거나 중지된 상태드라이버 로드, phy_stop() 호출
PHY_READY1PHY가 초기화되었으나 아직 시작하지 않은 상태phy_init_hw() 완료
PHY_UP2PHY가 시작되었으나 링크가 아직 성립하지 않은 상태phy_start() 호출
PHY_RUNNING3링크가 활성화되어 데이터 전송이 가능한 상태Auto-Negotiation 완료, 링크 업
PHY_NOLINK4PHY가 동작 중이나 링크가 끊어진 상태케이블 분리, 원격 장애
PHY_HALTED5PHY가 명시적으로 정지된 상태phy_stop() 호출

커널 소스 drivers/net/phy/phy.cphy_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 콜백을 트리거합니다. 상태 전이 흐름을 다이어그램으로 표현하면 다음과 같습니다.

PHY_DOWN PHY_READY PHY_UP PHY_RUNNING PHY_NOLINK PHY_HALTED phy_init_hw() phy_start() AN 완료, link up AN 완료, no link link down link up phy_stop() phy_stop() phy_start() 재시작 상태 전이 트리거 요약 • phy_init_hw(): PHY_DOWN → PHY_READY (H/W 초기화 완료) • phy_start(): PHY_READY/HALTED → PHY_UP (Auto-Negotiation 시작) • phy_check_link_status(): PHY_UP/NOLINK → PHY_RUNNING, 또는 RUNNING → NOLINK • phy_stop(): 모든 활성 상태 → PHY_HALTED (명시적 정지) • 상태 머신 주기: PHY_STATE_TIME (기본 1초) 간격으로 delayed_work 실행

phylinkphylink_mac_ops 구조체를 통해 MAC 드라이버에 링크 설정 변경을 통지합니다. drivers/net/phy/phylink.cphylink_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_preparemac_config 직전MAC 정지, 클럭 재설정 등 사전 준비
mac_finishmac_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;
}

phylink은 PHY 직접 연결 외에도 두 가지 대안적 링크 모드를 지원합니다. SGMII/1000BASE-X에서 사용하는 인밴드 Auto-Negotiation(In-band AN)과, 링크 파라미터가 고정된 고정 링크(fixed-link) 모드입니다.

모드phylink_config.type링크 감지 방식사용 시나리오
PHY 직접 연결PHYLINK_NETDEVPHY 상태 머신 (MDIO 폴링/인터럽트)일반적인 구리 또는 SFP PHY
In-band ANPHYLINK_NETDEVPCS 레지스터 기반 인밴드 신호SGMII, 1000BASE-X, USXGMII SFP
Fixed-linkPHYLINK_NETDEVDevice Tree/ACPI에서 고정 파라미터스위치 백본, MAC-to-MAC 직결
MAC 전용PHYLINK_DEVMAC 자체 링크 감지가상 디바이스, 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 예시:
   &ethernet {
       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_intrhandle_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;
    }
}
연계 문서: XDP 프로그램 작성과 AF_XDP 유저스페이스 큐 모델은 BPF/XDP, AF_XDP 문서에서 이어서 확인하세요.

ndo_bpf 콜백 구현 상세

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

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

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

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

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

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

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

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

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

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

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

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

    return 0;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

AF_XDP Zero-Copy 드라이버 통합

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

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

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

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

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

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

    return rx_done;
}

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

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

        if (!desc)
            break;

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

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

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

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

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

    return cleaned;
}

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

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

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

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

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

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

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

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

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

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

멀티큐, RSS, IRQ affinity 설계

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

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

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

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

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

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

RSS 해시 함수와 Indirection Table 내부

RSS(Receive Side Scaling)는 수신 패킷을 여러 RX 큐에 분산하는 하드웨어 메커니즘입니다. 핵심은 토플리츠 해시(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)를 설정합니다.

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

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

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

    if (ret < 0)
        return ret;

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

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

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

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

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

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

    return ret;

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

XPS (Transmit Packet Steering) 내부 동작

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

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

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

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

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

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

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

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

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

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

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

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

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

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

done:
    return cpu;
}

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

NUMA 친화적 큐 배치 전략

NUMA(Non-Uniform Memory Access) 시스템에서 네트워크 드라이버의 성능은 큐, 인터럽트, 메모리 할당이 올바른 NUMA 노드에 배치되었는지에 크게 좌우됩니다. 원격 NUMA 노드의 메모리 접근은 로컬 접근 대비 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 + page_pool 조합에서는 frame ownership 규칙을 엄격히 지켜야 합니다. 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)에서 새 페이지를 할당합니다.

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

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

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

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

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

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

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

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

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

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

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

PP_FLAG 옵션 상세

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

플래그설명일반적 사용처성능 영향
PP_FLAG_DMA_MAP page_pool이 DMA 매핑/언매핑을 자동 관리. 재활용 시 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, 다수 인터페이스 환경 메모리 절약, 캐시 히트율 하락 가능
권장 조합: 대부분의 물리 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()불필요자체 뮤텍스 사용
데드락 주의: RTNL을 잡은 상태에서 flush_workqueue()를 호출하면 안 됩니다. workqueue에 대기 중인 작업이 RTNL을 요청할 수 있기 때문입니다. 대신 rtnl_unlock()flush_workqueue()rtnl_lock() 패턴을 사용합니다.

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 콜백을 호출합니다.

커널 5.6부터 ethtool은 기존 ioctl 인터페이스를 대체하는 Netlink 기반 인터페이스를 제공합니다. ethtool-netlink(이하 ethnl)은 확장성이 뛰어나고, 변경 알림(Notification)을 지원하며, 구조화된 데이터 전달이 가능합니다.

ethnl 커맨드 패밀리대응 레거시 ioctl설명
ETHTOOL_MSG_LINKINFO_GET/SETETHTOOL_GSET/SSET링크 속도, 듀플렉스(Duplex), autoneg
ETHTOOL_MSG_RINGS_GET/SETETHTOOL_GRINGPARAMRX/TX 링 크기
ETHTOOL_MSG_CHANNELS_GET/SETETHTOOL_GCHANNELS큐/채널 수
ETHTOOL_MSG_COALESCE_GET/SETETHTOOL_GCOALESCE인터럽트 코얼레싱(Coalescing)
ETHTOOL_MSG_STATS_GETETHTOOL_GSTATS드라이버 통계
ETHTOOL_MSG_PAUSE_GET/SETETHTOOL_GPAUSEPARAM일시 정지 프레임(Pause Frame) 설정
ETHTOOL_MSG_FEC_GET/SETETHTOOL_GFECPARAMFEC(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-netlink은 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규칙 우선순위/충돌 처리
eSwitchswitchdev, representor netdevVF/representor 일관성
암호화(Encryption)/터널(Tunnel)xfrm offload, UDP tunnel offloadfallback 경로와 통계 구분

tc flower 오프로드 내부 흐름

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

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

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

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

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

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

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

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

    entry->cookie = f->cookie;

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

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

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

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

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

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

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

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

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

    if (type != TC_SETUP_CLSFLOWER)
        return -EOPNOTSUPP;

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

하드웨어 Flow Table 관리

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

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

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

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

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

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

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

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

switchdev 통합과 FDB 오프로드

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

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

    if (!my_is_our_port(dev))
        return NOTIFY_DONE;

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

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

    return NOTIFY_DONE;
}

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

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

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

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

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

UDP 터널 오프로드

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

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

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

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

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

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

    return 0;
}

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

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

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

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

리셋/장애 복구: 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();
}

devlink health 프레임워크는 NIC 드라이버의 장애 감지, 진단, 자동 복구를 체계화하는 인프라입니다. 리포터(Reporter)를 등록하면 devlink health 명령으로 운영팀이 장애 상태를 조회하고, 자동 복구를 트리거할 수 있습니다.

devlink health 복구 흐름 오류 감지 TX timeout / FW hang devlink_health_report() 리포터에 오류 보고 자동 복구 판단 grace period 확인 recover() 콜백 HW 리셋 수행 dump() 콜백 FW 상태/레지스터 덤프 diagnose() 콜백 현재 상태 진단 자동 Health 상태 전이 HEALTHY ERROR RECOVERING 오류 발생 복구 시작 복구 성공 복구 실패
/* 완전한 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_detectedslot_resetresume 세 단계로 진행됩니다.

/* 완전한 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 offloadNETIF_F_HW_CSUM, skb->ip_summedpartial checksum descriptor 구성
TSO/GSONETIF_F_TSO*, gso_size세그먼트 제한, header split 처리
GRO/LROnapi_gro_receive()재조립 후 메타데이터 일관성
VLAN offloadNETIF_F_HW_VLAN_CTAG_TX/RXtag insert/strip와 통계 동기화

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

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

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

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

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

    my_hw_config_timestamp(ndev, &cfg);

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

    return 0;
}

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

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

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

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

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

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

TX 큐 선택: ndo_select_queue, XPS, CPU locality

멀티큐 NIC에서 ndo_start_xmit() 성능은 큐 선택 품질에 크게 좌우됩니다. 플로우 해시(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;
}

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

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

Doorbell이란?

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

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

서브시스템별 Doorbell 비교

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

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

Doorbell 최적화 기법

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

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

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

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

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

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

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

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

Busy Poll 내부 동작 원리

Busy poll의 핵심은 napi_busy_loop() 함수입니다. 소켓(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 스케줄링과 상호 배제됩니다.

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

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

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

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

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

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

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

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

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

    return work_done;
}

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

소켓 레벨 Busy Poll 설정

소켓 레벨에서 busy poll을 활성화하는 방법은 두 가지입니다. 시스템 전역 sysctl 설정과 소켓별 옵션 설정이며, 소켓별 설정이 우선합니다. epoll 기반 이벤트 루프에서도 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이 유리한 경우:

Busy poll이 불리한 경우:

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

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

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

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

회귀 테스트: 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이 지원하는 주요 테스트 영역은 다음과 같습니다.

# 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.shGRO(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) 프레임워크를 제공합니다.

# 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
PFCpriority별 pause on/off 적용drop 급증 또는 head-of-line blocking
ETSbandwidth 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 프레임의 동작 원리는 다음과 같습니다.

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

# PFC 설정 및 모니터링

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

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

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

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

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

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

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

# TSN 설정 예시

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

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

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

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

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

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

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

PREEMPT_RT와 NAPI threaded 모드

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

NAPI threaded 모드 내부 구현

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    raw_spin_lock(&priv->irq_lock);

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

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

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

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

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

    return 0;
}

실시간 네트워크 지연 측정

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

# 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);
}
운영 주의: kdump 캡처 커널에서 동일 NIC 드라이버가 정상 동작하는지 별도 검증이 필요합니다. 본 커널에서 정상이어도 캡처 커널 initramfs/펌웨어 누락으로 전송 실패가 발생할 수 있습니다.

netpoll 내부 아키텍처

netpoll은 커널 패닉(Kernel Panic)이나 인터럽트가 비활성화된 극한 상황에서도 네트워크 패킷을 송신할 수 있는 최소한의 네트워크 경로입니다. struct netpoll은 일반 네트워크 스택을 우회하여 드라이버의 ndo_start_xmit을 직접 호출하는 폴링 기반(Polling-based) 전송 메커니즘을 제공합니다.

netpoll 전송 아키텍처 커널 패닉 / oops netconsole / netdump netpoll_send_udp() netpoll_send_skb() trylock(txq->_xmit_lock) 실패 시 poll + 재시도 ndo_start_xmit() 직접 호출 (일반 스택 우회) HW TX ring → doorbell 네트워크로 패킷 전송 인터럽트 컨텍스트 제약 sleep 금지 / GFP_ATOMIC만 허용 / trylock만 사용 napi_poll() 직접 호출로 TX completion 처리

netpoll_send_skb_on_dev()의 내부 동작을 단계별로 분석하면 다음과 같습니다.

/* 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)를 저장합니다. 캡처 커널 환경은 일반 부팅과 크게 다르며, 네트워크 드라이버는 여러 제약 조건 하에서 동작해야 합니다.

제약 사항영향드라이버 대응 전략확인 방법
펌웨어 비정상 상태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 overflowproducer/consumer wrap 처리 누락mask 기반 인덱싱 + assert
UAF on resetreset 중 skb/page 소유권 경합state machine + refcount 엄격화
ioctl/netlink 입력 검증 부족사용자 파라미터 경계값 누락range check, capability check

RX 패킷 길이 검증 패턴

네트워크 드라이버에서 가장 흔한 취약점은 하드웨어가 보고한 패킷 길이를 무조건 신뢰하는 것입니다. NIC 펌웨어 버그, DMA 오류, 악의적인 패킷 조작 등으로 인해 실제 데이터 크기와 보고된 길이가 다를 수 있으며, 이를 검증하지 않으면 버퍼 오버리드(Buffer Overread) 또는 힙 오버플로우(Heap Overflow)가 발생합니다.

실제 CVE 사례를 통해 위험성을 확인할 수 있습니다.

/* 안전한 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) 오류가 발생하면 임의 메모리 접근이 가능해집니다. 안전한 인덱스 관리 패턴을 일관되게 적용해야 합니다.

링 버퍼 인덱스 관리의 핵심 원칙은 다음과 같습니다.

/* 안전한 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 핸들러는 사용자 공간(Userspace)에서 전달되는 데이터를 처리합니다. 모든 사용자 입력은 신뢰할 수 없으며(Untrusted), 매개변수 범위 검증, 권한 확인(Capability Check), 경계 조건 처리를 철저히 수행해야 합니다.

/* 안전한 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로 전달합니다.

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

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

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

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는 다른 인터페이스로 전달합니다.

veth pair 네임스페이스 간 데이터 흐름 Network Namespace 1 (호스트) 애플리케이션 TCP/IP Stack veth0 veth_xmit() XDP prog (선택) veth_xdp_rcv() ptr_ring Network Namespace 2 (컨테이너) 애플리케이션 TCP/IP Stack veth1 (peer) NAPI poll XDP prog (선택) veth_xdp_rcv() ptr_ring peer 전달 (동일 커널 내)
veth pair는 동일 커널 내에서 ptr_ring을 통해 패킷을 교환합니다. 각 방향에 독립적인 XDP 프로그램을 연결할 수 있습니다.

virtio-net 드라이버 심층 분석

virtio-net은 가상 머신(Virtual Machine) 환경에서 가장 널리 사용되는 준가상화(Paravirtualization) 네트워크 드라이버입니다. virtqueue를 통해 호스트와 게스트 사이에서 제로 카피(Zero-copy)에 가까운 효율적인 패킷 전달을 구현합니다.

virtqueue의 핵심 구조는 세 부분으로 구성됩니다.

/* 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 지원네임스페이스주요 용도상대 성능
vethpeer netdev 직접 전달O (native)양쪽 독립컨테이너 네트워킹높음
macvlanMAC 기반 분기O (한정적)자식이 독립VM 브릿징, VEPA매우 높음
ipvlanIP 기반 분기X자식이 독립다수 컨테이너 (MAC 절약)높음
bridgeL2 FDB 기반 포워딩O (한정적)포트별 독립VM 호스트 브릿지중간
TUN/TAP파일 디스크립터X생성 NSVPN, QEMU낮음 (컨텍스트 스위치)
virtio-netvirtqueue (공유 메모리)O게스트 내부가상 머신높음 (vhost 시)

컨테이너 네트워킹 성능 최적화 팁:

네트워크 네임스페이스와 가상 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 per-namespace 락 (v6.13+): 커널 6.13에서 rtnl_lockper-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 관련 데드락은 주로 다음 시나리오에서 발생합니다.

/* 올바른 패턴: 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;
}
락 순서 규칙: 네트워크 서브시스템의 락 획득 순서는 반드시 RTNL → 장치 spinlock → NAPI 순서를 따라야 합니다. 역순 획득은 데드락을 유발합니다. 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_syncseqlock의 차이점을 이해하는 것이 중요합니다. 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;
    }
}
u64_stats_sync: torn read 방지 메커니즘 (32비트 시스템) Writer (NAPI poll) seq=0 B seq=1 (홀수) 상위 32비트 쓰기 하위 32비트 쓰기 E seq=2 (짝수) Reader 1 (torn read 감지) B start=0 상위 읽기 하위 읽기 R seq=2 != start=0 → 재시도! Reader 2 (정상 읽기) B start=2 상위 읽기 하위 읽기 R seq=2 == start=2 → 성공 B = fetch_begin / update_begin E = update_end R = fetch_retry (seq 비교) 64비트 시스템에서는 모든 동기화가 no-op으로 컴파일됩니다 (단일 명령어 원자적 읽기)
u64_stats_sync는 시퀀스 카운터를 사용하여 32비트 시스템에서 64비트 값의 torn read를 감지하고 재시도합니다.

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 전송 경로 사이의 동기화를 드라이버가 직접 관리해야 합니다.

net_device 락 계층 구조와 적용 범위 RTNL Mutex (제어 경로 전체) register/unregister, open/close, 주소 변경, MTU 변경, ethtool 설정 TX qdisc Lock (큐별) ndo_start_xmit 직렬화, netif_tx_lock NAPI State Bit (인스턴스별) NAPI_STATE_SCHED로 poll 직렬화 TX Queue 0 qdisc_lock(q) TX Queue 1 qdisc_lock(q) NAPI 0 (RX Q0) napi_poll() NAPI 1 (RX Q1) napi_poll() 병렬 실행 가능 병렬 실행 가능 RCU Read-Side (락 없는 읽기) XDP prog 참조, rx_handler, filter table, neigh entry, route lookup u64_stats_sync (통계 경로) per-CPU 카운터 읽기/쓰기 — 32비트 시스템에서만 실제 동기화 DMA/Memory Barriers: dma_wmb(), smp_wmb() — descriptor ring 순서 보장
네트워크 드라이버는 경로별로 다른 동기화 메커니즘을 조합합니다. RTNL이 최상위, RCU/u64_stats가 최하위 오버헤드입니다.
락 유형보호 대상컨텍스트범위오버헤드
rtnl_mutex장치 설정, 등록/해제프로세스전역 (per-ns v6.13+)높음 (sleep 가능)
netif_tx_lockTX 큐별 전송BHper-queue중간
NAPI_STATE_SCHEDNAPI poll 직렬화softirq/kthreadper-NAPI낮음 (비트 연산)
RCUXDP prog, rx_handler모든 컨텍스트읽기 경로 전역매우 낮음
u64_stats_sync64비트 통계 카운터NAPI/BH (쓰기)per-CPU0 (64비트) / 낮음 (32비트)
dma_wmb()DMA descriptor 순서모든 컨텍스트CPU-장치 간아키텍처 의존

메모리 순서와 DMA 배리어(Barrier)

네트워크 드라이버에서 DMA 배리어는 CPU가 작성한 디스크립터(Descriptor)가 장치에 올바른 순서로 보이도록 보장합니다. 배리어 유형을 잘못 선택하면 패킷 손실이나 DMA 오류가 발생합니다.

배리어용도x86ARM64사용 시점
dma_wmb()DMA 쓰기 순서 보장컴파일러 배리어dmb(oshst)디스크립터 필드 쓰기 후, ownership 비트 설정 전
dma_rmb()DMA 읽기 순서 보장컴파일러 배리어dmb(oshld)ownership 비트 확인 후, 데이터 읽기 전
wmb()모든 쓰기 순서 보장sfencedsb(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;
}
x86 vs ARM64 배리어 차이: x86은 강한 메모리 순서(TSO, Total Store Order) 모델이므로 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 카운터
링크 flapPHY state machine/interrupt stormdmesg, phylink tracepoint
고부하에서 패킷 손실IRQ affinity/NUMA 불일치/proc/interrupts, ethtool -x/-X
XDP 적용 후 비정상page_pool recycle/XDP verdict 처리 오류bpftool prog, drop reason trace
# 실습 예제: 큐/오프로드/통계 빠른 점검
# 큐/오프로드/통계 빠른 점검
ethtool -i eth0
ethtool -k eth0
ethtool -l eth0
ethtool -S eth0 | grep -E "drop|error|timeout|busy"

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

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

TX Hang 심층 진단 플로우차트

TX 타임아웃(TX Timeout)은 네트워크 드라이버에서 가장 치명적인 장애 유형 중 하나입니다. TX 완료 인터럽트(Completion Interrupt)가 오지 않거나, 도어벨(Doorbell) 쓰기가 실패하거나, BQL(Byte Queue Limits)이 잘못 설정된 경우 등 다양한 원인이 있습니다. 체계적인 진단 플로우차트를 따라 원인을 빠르게 격리해야 합니다.

TX Timeout 발생 ethtool -S: tx_timeout 카운터 확인 TX ring에 미완료 descriptor 존재? No BQL 설정 확인 Yes Completion IRQ 수신 중? Yes Queue wake 누락 No Doorbell write 정상? No MMIO 접근 실패 Yes FW 응답 정상 (health reporter)? No 펌웨어 행/크래시 Yes DMA mapping / IOMMU 확인 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번째droppednetif_rx backlog 초과로 드롭input_pkt_queue 크기 부족, CPU 과부하
3번째time_squeezesoftirq 시간 제한(2ms)으로 처리 중단패킷 폭주 또는 NAPI poll 비효율
9번째cpu_collisionTX 경로에서 CPU 경합 발생여러 CPU가 동일 TX 큐에 접근
10번째received_rpsRPS로 전달된 프레임정상 지표 (RPS 활성 시)
11번째flow_limit_countflow limit으로 드롭된 프레임단일 플로우 과점유
12번째softnet_backlog_len현재 backlog 대기 길이높으면 처리 지연 중

time_squeezebudget 소진의 차이를 이해하는 것이 중요합니다. 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를 활용하면 원인을 정밀하게 추적할 수 있습니다.

주요 원인 패턴과 진단 방법은 다음과 같습니다.

#!/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 errorPCIe 링크 오류슬롯 재장착, BIOS 설정 확인
firmware timeoutNIC 펌웨어 응답 없음펌웨어 업데이트, devlink health

성능 병목 분석 방법론

네트워크 드라이버의 성능 병목은 CPU 핫스팟(Hotspot), 락 경합(Lock Contention), 캐시 미스(Cache Miss), DMA 매핑 오버헤드 등 다양한 원인에서 발생합니다. 체계적인 분석 파이프라인을 구축하면 병목 지점을 정량적으로 식별할 수 있습니다.

# 종합 네트워크 성능 분석 파이프라인

# 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 기반 패킷 카운팅 프로그램 */

#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_totalrate 5m > 0Warning
TX 타임아웃node_ethtool_tx_timeoutincrease 1h > 0Critical
RX 드롭node_network_receive_drop_totalrate 5m > 100Warning
링크 상태 변경node_network_carrier_changes_totalincrease 10m > 2Warning
인터페이스 속도node_network_speed_bytes변경 감지Info
큐별 패킷 수node_ethtool_rx_queue_N_packets큐간 편차 70% 초과Warning
softnet time_squeezenode_softnet_times_squeezed_totalrate 5m > 10Warning
# 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 Socket)은 커널과 사용자 공간 간 네트워크 설정 변경 이벤트를 교환하는 주요 인터페이스입니다. RTNETLINK 그룹을 구독하면 링크 상태 변경, 주소 추가/삭제, 라우트 변경 등을 실시간으로 감지할 수 있습니다.

주요 RTNETLINK 멀티캐스트 그룹(Multicast Group)은 다음과 같습니다.

/* 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;
}

커널 v5.17부터 도입된 SKB drop reason 인프라(Infrastructure)는 패킷이 드롭되는 정확한 원인을 추적할 수 있게 합니다. 기존에는 kfree_skb tracepoint에서 호출 위치만 확인할 수 있었지만, 이제는 NOT_SPECIFIED, NO_SOCKET, TCP_OLD_DATA 등 구체적인 사유가 함께 기록됩니다.

devlink trap은 하드웨어 레벨에서 드롭된 패킷을 소프트웨어로 전달하여 분석할 수 있게 하는 메커니즘입니다. NIC이 드롭한 패킷의 원인을 파악하는 데 필수적입니다.

# 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분 체크리스트

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

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

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

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

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

Intel은 리눅스 네트워크 드라이버 생태계에서 가장 오랜 역사와 가장 넓은 드라이버 포트폴리오를 보유하고 있습니다. 1GbE 데스크톱용 e1000e부터 100GbE 데이터센터용 ice, 그리고 차세대 가상화(Virtualization) 인터페이스 idpf까지 세대별 설계 철학이 뚜렷하게 달라집니다. 각 드라이버의 아키텍처 차이를 이해하면 성능 튜닝과 장애 진단 전략을 드라이버 세대에 맞춰 정확히 수립할 수 있습니다.

드라이버주요 세대/용도핵심 포인트Max QueuesXDP 지원자주 보는 이슈
e1000e1GbE 서버/임베디드안정성 우선, 기능 단순1 (싱글 큐)미지원링크 flap, 절전 전환 후 wake 지연
igb1GbE 멀티큐RSS/TSO 기본, SR-IOV 일부 모델8지원 (5.9+)큐 불균형, IRQ affinity 미스매치
ixgbe10GbE(82599/X540)Flow Director, DCB, XDP 일부 경로128지원 (4.17+)tx timeout, FDIR rule 관리 복잡도
i40eXL710/X710(40/10GbE)VF 관리, DDP/firmware 의존성64 (per PF)지원 (5.1+)firmware 호환성, reset 후 VF 상태 불일치
iceE810(100GbE)devlink/representor, 고급 tc offload256지원 (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 싱글 큐 데이터 경로 ── TX 경로 ── Application Socket TCP/IP Stack Single TX Queue (ndo_start_xmit) DMA TX NIC PHY → Wire ── RX 경로 ── NIC PHY Wire → DMA RX Single RX Queue (NAPI poll) TCP/IP Stack Socket Application 단일 IRQ → 단일 NAPI 인스턴스 CPU 하나에 모든 처리 집중 특징: • 싱글 큐 → 멀티코어 CPU에서 병렬 처리 불가 • MSI 인터럽트 1개 → IRQ affinity 분산 불가 • XDP/AF_XDP 미지원 • 최대 1Gbps, PCIe 1x 슬롯에도 대역폭 충분
e1000e 싱글 큐 데이터 경로: TX/RX 모두 단일 큐와 단일 NAPI 인스턴스로 처리

e1000e가 지원하는 주요 칩 변형은 다음과 같습니다. 세대별로 전력 관리와 부가 기능에 차이가 있으며, 특히 I219는 Intel vPro 플랫폼의 AMT(Active Management Technology) 트래픽을 처리하는 ME(Management Engine) 연동 경로가 있어 드라이버 동작이 미묘하게 다릅니다.

칩 변형대표 제품최대 속도PCIe Gen전력 상태WoLHW Timestamp비고
82574L독립형 PCIe GbE1GbpsGen1 x1D0/D3hot지원미지원가장 안정적인 레거시 칩
I217-V/LMHaswell LPC 통합1Gbps내부 버스(Bus)D0/D3hot/D3cold지원미지원PHY 내장, Low Power Idle(EEE)
I219-V/LMSkylake+ PCH 통합1Gbps내부 버스D0/D3hot/D3cold지원미지원ME 연동, vPro AMT 트래픽 분리
알려진 링크 flap 이슈: I217/I219 칩셋에서 EEE(Energy Efficient Ethernet)가 활성화된 상태로 특정 스위치와 연결하면 주기적인 링크 다운/업 반복(link flap)이 발생할 수 있습니다. 운영 환경에서는 ethtool --set-eee eth0 eee off로 EEE를 비활성화하고, D3cold 진입 후 WoL 복구가 느린 경우 BIOS에서 Deep Sleep 옵션을 비활성화하세요.
전력 관리 최적화: e1000e는 S0ix(Modern Standby) 환경에서 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가 권장됩니다.

igb RSS 멀티큐 RX 흐름 Incoming Packet L3/L4 Parser IP/TCP 헤더 추출 Toeplitz Hash RSS Key (40B) → Indirection Table RX Queue 0 RX Queue 1 RX Queue 2 RX Queue 3 NAPI poll[0] NAPI poll[1] NAPI poll[2] NAPI poll[3] Network Stack (GRO) MSI-X 인터럽트 큐당 독립 IRQ벡터 CPU별 affinity 설정 igb RSS 핵심: • Toeplitz 해시로 동일 플로우를 항상 같은 큐에 배치 (flow affinity 보장) • I350: 최대 8큐, I210: 최대 4큐, I211: 최대 2큐 • ethtool -X로 indirection table 커스터마이징 가능 • XDP 지원 (커널 5.9+), AF_XDP ZC 지원 (커널 5.14+)
igb RSS 멀티큐 흐름: 패킷 → L3/L4 파싱 → Toeplitz 해시 → 큐 분배 → per-queue NAPI → Network Stack
칩 변형포트 수최대 큐SR-IOV VFPTP HW TSPCIe Gen비고
I3502/48최대 8 VF소프트웨어 TSGen2 x4서버용 멀티포트
I21014미지원하드웨어 TSGen2 x1PTP 최적, Qbv TSN 지원
I21112미지원미지원Gen1 x1데스크톱/NAS용 저가
PTP 하드웨어 타임스탬프: I210은 IEEE 1588 PTP v2 하드웨어 타임스탬프를 직접 지원하는 유일한 igb 칩입니다. 나노초 수준의 시간 동기화가 필요한 금융 거래 시스템이나 산업용 TSN(Time-Sensitive Networking) 환경에서는 I210을 선택하세요. I350은 소프트웨어 타임스탬프만 지원하므로 마이크로초 수준 정밀도에 한계가 있습니다.
# 실습 예제: 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) 등 데이터센터급 기능을 폭넓게 제공합니다.

ixgbe 내부 아키텍처 (82599/X540/X550) PCIe Interface Gen2 x8 (82599) Gen3 x4 (X540) Gen3 x4 (X550) DMA Engine TX DMA: 128 desc rings RX DMA: 128 desc rings Head/Tail 레지스터 TX Queues 최대 128개 (TC별 분배) TSO/Checksum Offload RX Queues 최대 128개 (RSS/FDIR) LRO/RSC 지원 MAC 10GbE/1GbE 선택 Flow Control/PFC PHY/SFP+ SFP+ / Base-T DAC 케이블 Wire Flow Director ATR (자동) / Perfect Filter 8K 필터 엔트리 DCB / PFC 8 Traffic Class ETS 대역폭 배분 SR-IOV 최대 64 VF VF MAC/VLAN 관리 세대별 차이: • 82599: PCIe Gen2 x8, SFP+ 전용, Flow Director ATR/Perfect • X540: PCIe Gen3 x4, Base-T(RJ45) 통합, MACsec 미지원 • X550: PCIe Gen3 x4, Base-T + SFP+, VXLAN/GENEVE 오프로드 • 공통: XDP 지원 (4.17+), RSS 128큐, SR-IOV 64 VF • X550-T2: 10GBASE-T 듀얼포트, NBASE-T(2.5G/5G) 네고시에이션 지원
ixgbe 내부 아키텍처: PCIe → DMA → TX/RX 큐(128) → MAC → PHY, Flow Director와 DCB는 큐 선택 및 스케줄링에 관여
ixgbe Flow Director: ATR vs Perfect Filter RX Packet Wire에서 수신 FDIR 매치? No Match ATR (자동 학습) TX 시 5-tuple 해시 기록 → RX 시 같은 CPU 큐 배치 Match Perfect Filter ethtool -N 규칙 매칭 → 지정 큐로 직접 스티어링 Target RX Queue NAPI poll → Stack CPU locality 보장 FDIR 비활성 RSS Hash 분배 기본 큐 선택 방식 ATR vs Perfect Filter 비교: • ATR: 자동 학습, 설정 불필요, TX→RX CPU locality 최적화, 대부분의 워크로드에 적합 • Perfect: 수동 규칙(ethtool -N), 정확한 큐 지정, 지연시간 민감 트래픽에 사용, 최대 8K 엔트리 • ATR과 Perfect Filter는 동시 사용 가능 — Perfect 규칙이 ATR보다 우선순위가 높음
ixgbe Flow Director: ATR(자동 학습)과 Perfect Filter(수동 규칙)의 패킷 스티어링 경로 비교
기능82599 (X520)X540X550
최대 속도10Gbps (SFP+)10Gbps (Base-T)10Gbps (Base-T/SFP+)
PCIeGen2 x8Gen3 x4Gen3 x4
RSS 큐128128128
SR-IOV VF646464
Flow DirectorATR + Perfect (8K)ATR + Perfect (8K)ATR + Perfect (8K)
DCB (TC)8 TC8 TC8 TC
XDP지원 (4.17+)지원 (4.17+)지원 (4.17+)
VXLAN/GENEVE 오프로드미지원미지원지원
NBASE-T (2.5G/5G)미지원미지원지원 (X550-T)
MACsec미지원미지원일부 SKU
DCB/PFC 구성 팁: ixgbe에서 DCB를 활성화하면 RSS 큐 수가 Traffic Class 수에 의해 제한됩니다. 예를 들어 8 TC를 활성화하면 큐는 TC당 최대 16개(128/8)로 분배됩니다. DCB와 SR-IOV를 동시에 사용하는 경우 VF당 할당 가능한 TC는 PF 설정에 종속됩니다.
tx_timeout 패턴: 82599에서 높은 PPS 환경에서 tx_timeout이 간헐적으로 발생하는 경우, tx_ring->next_to_usenext_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의 리소스 요청을 중재합니다.

i40e PF-VF 아키텍처: AdminQ + virtchnl PF 드라이버 (i40e) 데이터 경로 관리 VF 리소스 중재 ethtool/devlink 인터페이스 AdminQ 비동기 명령 큐 ATQ(요청) + ARQ(응답) FW 이벤트 알림 Firmware (FW) 스위치 로직 / 필터 관리 DDP 파서 파이프라인 PHY/링크 제어 VF0 (iavf) Guest VM #1 VF1 (iavf) Guest VM #2 VFn (iavf) Guest VM #n virtchnl (VF ↔ PF 메시지 프로토콜) 공유 리소스 • VSI(Virtual Station Interface) 풀 • 큐 페어 (최대 1536 per device) • 필터 엔트리, 인터럽트 벡터 핵심 구조: • PF가 AdminQ를 통해 FW와 통신, VF는 virtchnl을 통해 PF에 리소스 요청 • VF 드라이버(iavf)는 하드웨어를 직접 접근하지 않고, PF를 통해 간접 제어 • 최대 128 VF, 1536 큐 페어 풀
i40e PF-VF 아키텍처: PF는 AdminQ로 펌웨어와 통신하고, VF는 virtchnl로 PF에 리소스를 요청
AdminQ 명령 카테고리대표 명령설명
스위치/필터ADD_FILTER, REMOVE_FILTERMAC/VLAN/클라우드 필터 추가/삭제
VSI 관리ADD_VSI, UPDATE_VSI가상 스테이션 인터페이스 생성/수정
큐 설정CONFIG_VSI_QUEUESTX/RX 큐 매핑 및 파라미터 설정
링크 관리GET_LINK_STATUS, SET_PHY_CONFIG링크 상태 조회, PHY 설정 변경
RSSSET_RSS_KEY, SET_RSS_LUTRSS 해시 키 및 indirection table 설정
NVM/FWNVM_UPDATE, GET_FW_VERSION펌웨어 업데이트, 버전 조회
VF 제어SET_VF_CONFIG, RESET_VFVF 리소스 할당, VF 리셋

AdminQ의 내부 동작은 관리 전송 큐(ATQ, Admin Transmit Queue)와 관리 수신 큐(ARQ, Admin Receive Queue) 두 개의 링 버퍼로 구성됩니다. 드라이버가 ATQ에 명령 디스크립터를 기록하면 펌웨어가 처리 후 ARQ에 응답을 기록합니다.

i40e AdminQ 메시지 흐름 (ATQ/ARQ 링 구조) i40e 드라이버 (PF) i40e_aq_send_command() 명령 디스크립터 작성 → opcode, datalen, cookie, flags, params ATQ (Admin Transmit Queue) 링 버퍼 (기본 32 엔트리) Tail Doorbell → FW 통보 ARQ (Admin Receive Queue) 링 버퍼 (기본 32 엔트리) FW 이벤트 + 응답 수신 펌웨어 (Firmware) ATQ에서 명령 fetch 처리 후 retval 설정 ARQ에 응답/이벤트 기록 인터럽트 발생 AdminQ 디스크립터 구조 (struct i40e_aq_desc, 32바이트) flags (2B) opcode (2B) datalen (2B) retval (2B) cookie (8B) params/addr (16B) ← 총 32바이트
i40e AdminQ 메시지 흐름: ① 드라이버가 ATQ에 디스크립터 기록 → ② Tail doorbell로 FW에 통보 → ③ FW가 처리 후 ARQ에 응답 → ④ 인터럽트로 드라이버에 완료 통지
DDP(Dynamic Device Personalization): i40e도 DDP를 지원하지만 ice와 달리 DDP 없이도 대부분의 기능이 정상 동작합니다. i40e DDP는 GTP, PPPoE 등 비표준 프로토콜 파싱을 위한 선택적 확장 기능으로, 텔레코어/5G 환경에서 주로 사용됩니다.
FW 버전 범위지원 커널주요 기능 변경알려진 이슈
6.x4.x ~ 5.4기본 AdminQ, VF 128개VF reset 후 큐 불일치
7.x5.4 ~ 5.15Cloud Filter, 향상된 VF trust일부 FW/드라이버 조합에서 link flap
8.x ~ 9.x5.15+DDP 지원, ADQ(Application Device Queue)ADQ + SR-IOV 동시 사용 시 성능 저하
비교 항목i40e (XL710)ice (E810)
최대 속도40GbE100GbE
FW 제어 모델AdminQAdminQ (확장)
VF 프로토콜virtchnl v1virtchnl 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/인프라 드라이버 역할을 목표로 합니다.

idpf virtchnl2 제어 경로 idpf 드라이버 • 하드웨어 비종속적 설계 • Split/Single Queue 선택 • netdev 등록/해제 • 런타임 큐 재설정 virtchnl2 Mailbox Queue • 제어 메시지 송수신 • Capability 협상 • 큐 그룹 설정 • 이벤트 알림 Control Plane (CP) • 리소스 할당/해제 • 큐 모델 결정 (Split/Single) • 필터/RSS 규칙 관리 • MAC/VLAN 정책 적용 HW/FW/SW CP 구현 가능 (FPGA, IMC FW, 소프트웨어 등) Data Path (TX/RX) DMA 직접 접근 (성능 경로) HW Queue Pairs TX/RX Descriptor Rings DMA (데이터 경로 — CP 개입 없음) 핵심 설계 원칙: • 제어 경로(virtchnl2 + Mailbox)와 데이터 경로(DMA)가 완전히 분리 • CP는 HW FW, FPGA, 또는 소프트웨어로 구현 가능 → 드라이버 코드 변경 없이 다양한 플랫폼 지원
idpf virtchnl2 제어 경로: 드라이버 → Mailbox Queue → Control Plane, 데이터 경로는 DMA 직접 접근
idpf 큐 모델: Split Queue vs Single Queue Split Queue 모델 (기본) TX 측 TX Queue (전송 요청) TX Completion Queue RX 측 RX Buffer Queue RX Completion Queue Split Queue 장점 • 버퍼 보충과 완료 처리가 독립적으로 수행 • Completion Queue를 여러 Buffer Queue가 공유 가능 • HW가 out-of-order 완료 처리 최적화 가능 • 캐시 효율성 향상 (Hot/Cold 데이터 분리) • in-order 보장 불필요 → 파이프라인 병렬화 • 대규모 VF 환경에서 큐 리소스 효율적 사용 Single Queue 모델 (호환) TX 측 TX Queue (전송 + 완료 통합) RX 측 RX Queue (수신 + 버퍼 보충 통합) Single Queue 용도 • 기존 iavf/i40e/ice와 호환 필요 시 • 구조가 단순하여 디버깅 용이 • 소규모 환경에서 오버헤드 최소화 • 레거시 게스트 OS 지원 • CP 협상 시 Split 미지원 fallback
idpf 큐 모델 비교: Split Queue(기본)는 전송/완료를 분리하여 파이프라인 병렬화, Single Queue(호환)는 기존 모델과 동일
iavf 대체 전략: idpf는 장기적으로 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 → ICE 아키텍처 진화 i40e (XL710/X710) PF 드라이버 (i40e) AdminQ → FW RSS (64큐) virtchnl v1 VF DDP (선택적) 기본 ethtool 통계 미지원: eswitch, representor, devlink health 미지원: tc flower offload, HW PTP timestamp 진화 ICE (E810) PF 드라이버 (ice) AdminQ 확장 + devlink RSS (256큐) virtchnl v1/v2 + idpf DDP (필수) devlink health/flash 추가: eswitch (switchdev), representor netdev 추가: tc flower offload, HW PTP (ns), ADQ, LAG 100GbE (E810-CQDA2), 25GbE (E810-XXVDA2/4) 256 VF, GNSS 동기화, MACsec 일부 모델
i40e → ICE 아키텍처 진화: DDP 필수화, devlink/eswitch 통합, HW PTP, tc flower offload 추가
비교 항목i40e (XL710/X710)ICE (E810)진화 방향
최대 속도40GbE100GbE대역폭 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 가속
PTPSW timestampHW timestamp (ns급)정밀 시간 동기화
Adaptive ITR지원지원 (4μs 단위)세밀한 코얼레싱
XDP5.1+5.5+성능 향상
RDMAiWARP (irdma)iWARP + RoCEv2 (irdma)프로토콜 범위 확장
마이그레이션 권장사항: i40e에서 ICE로 마이그레이션할 때 가장 주의해야 할 점은 DDP 패키지 배치입니다. ICE는 DDP 없이 Safe Mode로 동작하여 기존 i40e에서 당연히 사용하던 RSS, Flow Director, SR-IOV 고급 기능 등이 비활성화됩니다. 배포판의 linux-firmware 패키지가 최신인지 반드시 확인하세요.

ICE eswitch: Representor 모델과 Switchdev

ICE E810은 eswitch(Embedded Switch)를 통해 NIC 내부에 가상 스위치를 구성하고, 각 VF에 대응하는 representor netdev를 호스트에 노출합니다. 이 모델은 OVS(Open vSwitch) TC offload의 핵심 인프라이며, switchdev 모드로 전환하면 호스트의 TC subsystem이 NIC 하드웨어 필터 규칙을 직접 제어할 수 있습니다.

ICE eswitch Representor 흐름 Host Kernel PF netdev eth0 (ice) VF0 repr eth0_0 VF1 repr eth0_1 VFn repr eth0_n OVS Bridge / TC flower (sw + hw offload) Uplink repr enp1s0f0 NIC Hardware (E810) Embedded Switch (eswitch) MAC 학습 / VLAN 필터 / tc flower 규칙 HW 오프로드 VF0 port VF1 port VFn port PHY (Wire) tc flower 규칙 HW 오프로드
ICE eswitch representor 흐름: 호스트의 representor netdev → OVS/TC → NIC 내장 eswitch → VF/PHY 포트

Representor 모델은 NIC 내부의 각 VF 포트에 대응하는 호스트 측 netdev(representor)를 생성합니다. OVS나 TC는 이 representor를 일반 netdev처럼 다루면서 스위칭 규칙을 설정하고, ICE 드라이버가 해당 규칙을 NIC 하드웨어에 오프로드합니다. Slow path(매칭 실패) 패킷은 representor를 통해 호스트 OVS로 올라와 소프트웨어 처리됩니다.

eswitch 전환 시 주의: 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
OVS TC offload 최적화: ICE eswitch + OVS 환경에서 최대 성능을 얻으려면 OVS의 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 가상화
switchdevrepresentor + TC 규칙지원 (hw-offload)flower/u32SDN, 클라우드 오버레이(Overlay)
Representor netdev 이름 규칙: ICE eswitch에서 생성되는 representor netdev 이름은 PF netdev 이름을 기반으로 {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"
tc flower 규칙 수 제한: ICE E810의 eswitch에서 하드웨어 오프로드 가능한 tc flower 규칙 수는 NIC 내부 TCAM 크기에 의해 제한됩니다. 규칙 수가 한계를 초과하면 새 규칙은 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 mismatchFW 업데이트: devlink dev flash pci/... file ice_pkg.bin
패키지 파일 손상ice: DDP invalid signaturelinux-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 tcp4ethtool --show-ntuple eth0으로 현재 사용 규칙 수와 최대 용량을 확인할 수 있습니다.

i40e AdminQ Timeout 복구

i40e 드라이버에서 AdminQ(Administration Queue) timeout은 PF와 펌웨어 간 명령 채널이 응답하지 않을 때 발생합니다. 이 오류는 FW hang, PCI 전송 오류, 또는 과도한 AdminQ 명령 큐잉으로 발생합니다.

AdminQ timeout 감지: i40e_aq_send_msg_to_vfI40E_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 affinitycat /proc/interrupts큐당 전용 CPU 코어멀티큐 드라이버 전체
Adaptive Coalescingethtool -c eth0워크로드별 조정모든 드라이버
GRO/TSOethtool -k eth0활성화 유지모든 드라이버
XPS (TX 큐 → CPU 매핑)cat /sys/class/net/eth0/queues/tx-*/xps_cpusNUMA 로컬 CPU멀티큐 드라이버 전체
Busy Pollingsysctl net.core.busy_poll50 (μs), 저지연 환경모든 드라이버
NUMA 친화cat /sys/class/net/eth0/device/numa_nodeNIC 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 vs 수동 affinity: 고성능 네트워킹 환경에서는 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 하드웨어 아키텍처 블록 다이어그램 Physical Port 0 25/50/100/200/400G Physical Port 1 Dual-Port NIC Steering Engine Flow Table Hierarchy Match + Action NIC / FDB / RDMA Header Rewrite, Encap vPort 0 (PF) Uplink vPort 1 (VF) SR-IOV VF vPort N (SF) Scalable Func SQ / RQ Work Queue WQE stride MPWQE Multi-Packet WQE Striding RQ CQ Completion Queue EQ Event Queue MSI-X CPU IRQ PCIe Gen4/5 x16 — DMA Engine — UAR (User Access Region) — HCA BAR Host Memory Ring Buffers (SQ/RQ) CQ/EQ Entries SKB / xdp_frame Page Pool (frag alloc) UMEM (AF_XDP) Firmware (FW) Command Interface HCA Capabilities Flow Steering Tables Health Monitoring Crypto / TLS offload Management FW devlink params FW Update (MCC) Health Reporters Port Config eSwitch Mgmt BlueField DPU Arm SoC (embedded) SmartNIC mode Separated Host OVS HW offload IPsec / TLS inline
그림 ndd-19. mlx5 하드웨어 아키텍처 블록 다이어그램 — 물리 포트에서 MSI-X 인터럽트까지의 데이터 흐름과 호스트 메모리, 펌웨어, 관리 평면의 상호작용

소프트웨어 스택은 mlx5_core가 HCA(Host Channel Adapter) 초기화, 커맨드 인터페이스, EQ/CQ 관리 등의 공통 기반을 제공하며, 그 위에 이더넷(mlx5e), InfiniBand/RoCE(mlx5_ib), vDPA(mlx5_vdpa) 서브시스템이 독립적으로 동작합니다.

mlx5 소프트웨어 스택 계층 구조 User Space iproute2 / tc rdma-core / libibverbs DPDK mlx5 PMD libxdp / AF_XDP netdev subsystem net_device / ethtool / tc RDMA subsystem ib_core / ib_uverbs vDPA subsystem vhost / virtio devlink subsystem params / health / port mlx5e Ethernet / TC offload mlx5_ib RoCEv2 / IB verbs mlx5_vdpa vDPA net device devlink ops Health / FW mgmt mlx5_core Command Interface | EQ/CQ Mgmt | Page Alloc | HCA Init | eSwitch | FW Cmd Mailbox ConnectX HCA Firmware — PCIe BAR — DMA — IOMMU
그림 ndd-20. mlx5 소프트웨어 스택 — mlx5_core 통합 코어 위에 이더넷(mlx5e), RDMA(mlx5_ib), vDPA(mlx5_vdpa), devlink 서브시스템이 독립 동작

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의 핵심입니다.

mlx5 Flow Steering 파이프라인 Ingress Packet In NIC RX Flow Tables Bypass tc flower rules Kernel ethtool ntuple Leftovers RSS / Default VLAN 802.1Q filter TTC (Traffic Type Classifier) L3/L4 proto → RQ group FDB (eSwitch) Flow Tables Pre-ACL ingress filter TC offload match + action FDB Main L2 forwarding CT Table conntrack offload Post-ACL / Miss → SW Slow Path unmatched → representor netdev RX Queue (RQ) → NAPI poll Drop HW counter++ vPort (VF/SF) FDB forward Encap / Decap VXLAN / GRE HW Flow Counters packets / bytes per rule tc -s filter show dev ... ethtool -S (steering stats)
그림 ndd-21. mlx5 Flow Steering 파이프라인 — NIC RX와 FDB(eSwitch) 도메인의 Flow Table 계층 구조와 패킷 처리 경로

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 기반의 정밀한 프로그래머블 데이터 경로를 제공합니다.

mlx5 eSwitch Legacy vs Switchdev 모드 비교 Legacy Mode PF netdev enp1s0f0np0 VF0 VM/Container VF1 VM/Container VF2 VM/Container eSwitch (Legacy) MAC/VLAN 기반 L2 스위칭 제한된 필터링 (ip link set vf) NIC HW (Port) Wire TC offload 불가, OVS HW offload 불가, 제한적 QoS Switchdev Mode Uplink Rep enp1s0f0np0 (PF) VF0 Rep eth0 (repr) VF1 Rep eth1 (repr) SF Rep en3f0pf0sf8 OVS Bridge / TC flower (slow path) 소프트웨어 판단 → 규칙 하드웨어 설치 FDB (HW fast path) TC offload / CT offload / Encap-Decap Match+Action → vPort 직접 전달 NIC HW (Port) Wire TC/OVS offload, CT offload, 프로그래머블 FDB, SF 지원
그림 ndd-22. mlx5 eSwitch Legacy vs Switchdev 모드 비교 — Switchdev 모드에서 representor netdev와 FDB 기반 하드웨어 fast path 제공

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 결정을 내립니다.

mlx5e XDP 데이터 경로 NIC HW DMA to Host Mem RSS → RQ MPWQE RQ Striding RQ page_pool frag xdp_buff 생성 CQE → NAPI XDP Program bpf_prog_run_xdp() xdp_buff → verdict multi-buffer 지원 XDP_DROP page recycle XDP_TX same port SQ XDP_REDIRECT other dev / CPU XDP_PASS → SKB alloc AF_XDP Zero-Copy Path UMEM (user mem) XSK RQ (dedicated) XSK CQ → user space DMA 직접 → UMEM 프레임 → XDP_REDIRECT → xsk_sendmsg() (TX) UMEM bound XDP TX SQ 전용 SQ (per channel) doorbell batching inline WQE (small pkt)
그림 ndd-23. mlx5e XDP 데이터 경로 — MPWQE Striding RQ에서 XDP 프로그램 실행까지의 흐름과 AF_XDP 제로카피 경로

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    # 포트 카운터 (패킷 드롭 확인)
RoCEv2 lossless 패브릭 요구사항: PFC와 ECN 없이 RoCEv2를 운영하면 네트워크 혼잡 시 RDMA 연결이 끊기고 복구 불가능한 상태가 될 수 있습니다. 스위치와 NIC 양쪽에서 PFC가 일치하지 않으면 pause storm이 발생할 수 있으므로, 엔드투엔드 구성 검증이 필수입니다.

mlx5 드라이버는 devlink health 프레임워크를 통해 하드웨어 오류 감지, 진단, 자동 복구를 제공합니다. 각 reporter는 특정 하드웨어/소프트웨어 구성 요소를 모니터링하며, 오류 발생 시 진단 정보(dump)를 수집하고 자동 복구를 시도합니다.

mlx5 devlink health 복구 흐름 HW Event FW syndrome CQ/EQ error Reporter fw / fw_fatal tx / rx vnic Diagnose 상태 조회 카운터 수집 FW health buf Dump CR-space dump FW trace log SQ/RQ state Recover SQ/RQ reset FW reset Full HCA reset Verify health OK? link up? 복구 실패 시 재시도 (grace period 후) auto_recover: true | grace_period: 60s | auto_dump: true | devlink health 관리
그림 ndd-24. mlx5 devlink health 복구 흐름 — 하드웨어 이벤트 감지에서 자동 복구까지의 파이프라인
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 리셋 없이 다음 부팅 시 적용할 수 있습니다.

mlx5 펌웨어 업데이트 흐름 1. Download .mfa2 이미지 devlink flash MCC 커맨드 2. Verify SHA-256 검증 디지털 서명 HW 호환성 3. Flash SPI Flash 기록 진행률 표시 ~2-5분 소요 4. Activate FW reset cmd 또는 flash_only (다음 부팅 적용) 5. Reset PCI reset 또는 시스템 리부팅 검증 실패 → 중단 Live Patch (CX-6+, FW 지원 시) devlink dev flash ... overwrite settings 리셋 없이 FW 핫 업그레이드 (제한적 지원)
그림 ndd-25. mlx5 펌웨어 업데이트 흐름 — 이미지 다운로드에서 활성화까지의 단계별 과정
# 현재 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의 핵심 강점을 다른 드라이버와 정량적·구조적으로 비교하여, 왜 데이터센터 표준이 되었는지를 분석합니다.

mlx5e 핵심 강점 — 타 드라이버 비교 1. Flow Steering 용량 mlx5e: 64M+ rules (CX-7) ice: 16K TCAM + DDP profiles bnxt: 8K EM + 2K TCAM ena/gve: HW steering 없음 cxgb4: 2K filter entries mlx5 대비 400~4000배 격차 2. XDP / AF_XDP 성능 mlx5e: 35 Mpps DROP (CX-7) ice: ~25 Mpps (E810 100G) bnxt: ~20 Mpps (BCM57508) i40e: ~18 Mpps (X710) r8169: XDP 미지원 AF_XDP ZC: mlx5/ice/i40e만 지원 3. 오프로드 범위 mlx5e: TC+CT+NAT+VXLAN+IPsec+TLS bnxt: TC+CT+VXLAN (TruFlow) ice: TC+VXLAN+IPsec (제한적 CT) cxgb4: TOE+TLS+IPsec (다른 모델) ena/gve: 오프로드 미지원 유일한 full-stack HW offload 4. 가상화 확장성 mlx5: 1024 VFs + SF (무제한) ice: 256 VFs, SF 없음 bnxt: 256 VFs, SF 없음 mlx4: 64 VFs, SF 없음 ena/gve: SR-IOV 미지원 (PV 모델) SF: mlx5 독점 기능 (CX-6 Dx+) 5. RDMA / GPUDirect mlx5: RoCEv2+IB+GPUDirect+ODP bnxt: RoCEv2 (제한적) cxgb4: iWARP (RoCE 미지원) ice: 별도 irdma (iWARP+RoCE) 기타: RDMA 미지원 AI/HPC 워크로드: mlx5 사실상 독점 6. 관측성 / 관리 mlx5: devlink 5개 reporter bnxt: devlink 3개 reporter ice: devlink 2개 reporter ena: CloudWatch 메트릭 의존 r8169: ethtool -S 기본만 FW live update: mlx5만 지원 결론: mlx5e는 6개 핵심 영역 모두에서 최고 수준 — 유일한 "전 영역 리더" NIC 드라이버 범례: mlx5e (NVIDIA/Mellanox) ice (Intel E810) bnxt_en (Broadcom) cxgb4 (Chelsio) 클라우드/소비자 NIC ※ 성능 수치는 단일 코어, 64B 패킷 기준. 실제 성능은 CPU, PCIe 세대, FW 버전, 커널 버전에 따라 변동
그림 ndd-71. mlx5e 핵심 강점 비교 — 6개 카테고리에서 타 드라이버 대비 우위 분석
강점 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가 이렇게 큰 용량을 제공하는가?

# 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
실전 영향: Kubernetes 환경에서 수천 개의 Pod에 대한 네트워크 정책을 HW offload할 때, mlx5는 64M+ rules로 충분한 여유가 있지만, ice(16K)나 bnxt(10K)는 빠르게 한계에 도달하여 SW fallback이 발생합니다. 10K+ Pod 클러스터에서는 사실상 mlx5만 완전한 HW offload가 가능합니다.
강점 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

# 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 오버헤드가 급증합니다.

kTLS offload 성능: mlx5e의 kTLS inline 암호화는 AES-GCM-256을 line rate로 처리합니다. 100 GbE에서 SW kTLS는 CPU 8~12코어를 소모하지만, mlx5 HW offload 시 CPU 사용량이 거의 0에 수렴합니다. 이는 HTTPS가 대부분인 현대 웹서비스에서 막대한 TCO 절감을 의미합니다.
강점 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
SF vs VF — 언제 SF를 사용하는가? SF는 컨테이너 네트워킹(각 Pod에 전용 NIC 리소스), microservice isolation, DPU(BlueField)에서의 서비스 체이닝에 이상적입니다. VF와 달리 재부팅 없이 동적 생성/삭제가 가능하고, 각 SF가 독립 devlink/RDMA 인스턴스를 갖기 때문에 세밀한 관리와 모니터링이 가능합니다.
강점 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 vs 전통적 전송 경로 전통적 경로 (3단계 복사) GPU Memory VRAM (HBM) copy① CPU Memory DRAM bounce copy② NIC TX DMA ring Network CPU 오버헤드: 높음 대역폭: PCIe 병목 GPUDirect RDMA (mlx5 전용) GPU Memory VRAM (HBM) P2P DMA (nvidia-peermem) CPU 바이패스 mlx5 NIC ConnectX HCA Network CPU 오버헤드: 0 대역폭: line rate 성능 비교 (ConnectX-7, A100 GPU, PCIe Gen5) 전통적: ~50 GB/s CPU 2코어 점유 GPUDirect: ~400 Gbps wire CPU 0코어 점유 SHARP (IB): all-reduce 2x 스위치 내 in-network 연산
그림 ndd-72. GPUDirect RDMA vs 전통적 전송 경로 — CPU 바이패스로 GPU↔NIC 직접 DMA, mlx5 ConnectX만 지원
# 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
AI 훈련 네트워크에서 mlx5가 필수인 이유: NVIDIA NCCL은 mlx5 InfiniBand에서 SHARP(Scalable Hierarchical Aggregation and Reduction Protocol)를 통해 스위치 내에서 all-reduce를 HW 가속합니다. GPU 수가 많을수록 통신 오버헤드가 기하급수적으로 줄어드는 효과를 제공하며, 다른 NIC에서는 원천적으로 불가능한 기능입니다.
강점 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
FW Live Reset의 운영 가치: mlx5의 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
평가 기준 참고: ★★★★★ = 업계 최고/참조 구현, ★★★★☆ = 프로덕션 완성도, ★★★☆☆ = 기능 존재하나 범위 제한, ★★☆☆☆ = 기본/레거시 수준, ★☆☆☆☆ = 미지원 또는 최소 구현. 각 드라이버는 설계 목적이 다르므로(ena/gve는 클라우드 최적화, r8169는 소비자용), 낮은 점수가 반드시 품질 문제를 의미하지 않습니다. 목적 적합성(fit-for-purpose)을 함께 고려해야 합니다.
워크로드별 최적 드라이버 선택 가이드
워크로드 최적 드라이버 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); }'
mlx5의 약점도 인식해야 합니다: (1) 가격 — ConnectX-7 400G NIC는 $1,000+ 수준으로, r8169($5~15) 대비 수십~수백 배. (2) 드라이버 복잡도 — mlx5_core 소스 10만 줄+, 디버깅/커스터마이징 어려움. (3) OFED 의존성 — SHARP, GPUDirect Storage 등 일부 고급 기능은 NVIDIA OFED/DOCA 필요. (4) 전력 소비 — ConnectX-7 TDP 30W+, 저전력 환경 부적합. 워크로드 요구사항과 TCO를 종합적으로 판단해야 합니다.

mlx4 레거시 드라이버 및 mlx5 마이그레이션

mlx4_core/mlx4_en은 ConnectX-2, ConnectX-3, ConnectX-3 Pro를 지원하는 레거시 드라이버입니다. mlx5와 비교하여 스티어링 유연성, SR-IOV 확장성, offload 기능에서 상당한 차이가 있습니다. ConnectX-3 Pro는 mlx4와 mlx5 모두에서 부분적으로 지원되지만, mlx4가 권장됩니다.

mlx4 vs mlx5 아키텍처 비교 mlx4 (Legacy) ConnectX-2 / 3 / 3 Pro mlx4_en Ethernet driver mlx4_ib IB / RoCEv1 mlx4_core HCA 초기화, 포트 관리 제한사항 - 최대 64 VFs (vs mlx5: 1024) - Switchdev 모드 미지원 - TC flower offload 미지원 - AF_XDP zero-copy 미지원 - RoCEv2 미지원 (CX-2/3, v1만) - devlink health 미지원 - TLS/IPsec offload 미지원 - Connection Tracking 미지원 mlx5 (Current) ConnectX-4 ~ 7 / BlueField 1~3 mlx5e Ethernet mlx5_ib RDMA mlx5_vdpa vDPA mlx5_core 통합 코어 + eSwitch + devlink 핵심 기능 - 최대 1024 VFs + Scalable Functions - Switchdev + Representors + FDB - TC flower / OVS HW offload - AF_XDP zero-copy + multi-buf XDP - RoCEv2 + GPUDirect RDMA - devlink health / params / flash - TLS / IPsec / MACsec inline - HW CT + NAT + Sample offload
그림 ndd-26. mlx4 vs mlx5 아키텍처 비교 — 레거시 mlx4의 제한사항과 mlx5의 확장된 기능 세트

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 drop0이 정상; 증가 시 링 버퍼 확대 검토
PCI 오류dmesg | grep mlx4AER 오류 발생 시 슬롯 교체 고려
FW Healthmlx4 장치 로그 조회syndrome 코드 0x0이 정상
QP/CQ 고갈rdma stat showfree 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 × 1625GbE × 2OVS 기본, IPsecPCIe 3.0 × 8
BF2 (BlueField-2)Cortex-A72 × 8100GbE × 2OVS, CT, crypto, DOCAPCIe 4.0 × 16
BF3 (BlueField-3)Cortex-A78AE × 16400GbE × 2OVS, AI inference, StoragePCIe 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에서만 사용 가능합니다.

mlx4 지원 상태: mlx4 드라이버는 커널에서 유지보수 모드(maintenance mode)로 관리되며, 새로운 기능은 추가되지 않습니다. 보안 패치와 심각한 버그 수정만 적용됩니다. ConnectX-3 Pro 이하 하드웨어를 사용 중이라면, 가능한 빨리 ConnectX-5 이상으로 업그레이드하여 mlx5 드라이버의 활발한 개발 혜택을 받는 것이 권장됩니다. 특히 커널 6.x 이후로 mlx5에 추가된 기능(SF, CT offload, inline crypto 등)은 mlx4에서 사용할 수 없습니다.

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초까지 대기합니다.

Host Driver (bnxt_en) HWRM Command Channel TX Rings RX Rings CP Rings AGG Rings Doorbell Registers (BAR) NAPI Poll XDP Prog ethtool / devlink Interface HWRM Firmware Command Processor Ring Mgr Port Mgr VF Mgr Stat Mgr FW Health Monitor Thor2 ASIC Packet Engine (L2/L3/L4) TruFlow RSS Engine DMA Engine (Host ↔ NIC) MAC 0 MAC 1 SerDes (PAM4 / NRZ) Integrated PHY 100G/200G/400G HWRM DMA (Ring Data) Doorbell
그림 ndd-27. bnxt_en 전체 아키텍처 — Host Driver가 HWRM을 통해 Thor2 ASIC의 모든 리소스를 관리

링(Ring) 모델은 bnxt_en의 데이터 경로 핵심입니다. 4가지 링 타입이 서로 연동하여 패킷 송수신을 처리합니다.

TX Ring tx_buf_ring[] (SW) tx_desc_ring[] (HW BD) Producer: 드라이버 Consumer: HW BD 타입: SHORT/LONG/INLINE 기본: 512 엔트리 TSO: 다중 BD 체인 Doorbell로 HW 알림 STOP 임계: 엔트리 < MAX_SKB_FRAGS RX Ring rx_buf_ring[] (SW) rx_desc_ring[] (HW BD) Producer: 드라이버 Consumer: HW 버퍼 크기: PAGE_SIZE (헤더+데이터) 기본: 512 엔트리 점보: AGG 링 연계 HW GRO 지원 XDP: redirect/pass/drop Completion Ring cp_desc_ring[] (HW CMP) Producer: HW Consumer: NAPI CMP 타입: TX_L2 (TX 완료) RX_L2 (RX 완료) 기본: 1024 엔트리 Toggle bit으로 유효성 MSI-X 벡터 1:1 매핑 HWRM Async Event도 이 링으로 전달 AGG Ring rx_agg_ring[] (SW) rx_agg_desc_ring[] (BD) Producer: 드라이버 Consumer: HW 점보 프레임 전용: RX BD → AGG BD 체인 기본: 1024 엔트리 PAGE_SIZE 단위 버퍼 Scatter-Gather 수신 MTU > PAGE_SIZE 시 활성
그림 ndd-28. bnxt_en 4종 링 구조 — TX/RX/Completion/AGG 링의 역할과 Producer/Consumer 관계
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_CFGasync_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 부하를 제거하고, 수백만 개의 플로우 규칙을 와이어 스피드로 처리합니다.

Ingress Packet Parser L2/L3/L4 헤더 터널 디캡슐 Key 추출 TCAM 와일드카드 매칭 우선순위 기반 Exact Match 해시 기반 룩업 수백만 엔트리 CFA (Context Filter Accelerator) Action Forward Drop Modify (NAT) Count Forward → VF/Port Drop Modify NAT/VLAN/TTL Miss → Host Flow Stats pkt/byte counters
그림 ndd-29. TruFlow 오프로드 파이프라인 — Parser → TCAM/EM 룩업 → Action 단계를 거쳐 패킷 처리
# 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 결정 기준:

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'
TruFlow TCAM/EM 용량 계획 결정 트리 새 플로우 규칙 추가 와일드카드/마스크 포함? Yes TCAM 와일드카드 룩업 TCAM 여유 있음? Yes TCAM 설치 우선순위 배치 No SW 폴백 CPU 처리 (성능↓) → TCAM 증설 검토 No (정확 매칭) Exact Match (EM) 해시 룩업 해시 충돌 발생? No EM 설치 성공 와이어스피드 처리 Yes TCAM 계층 룩업 충돌 엔트리 TCAM 이동 → TCAM 용량 소비 혼합 시나리오 TCAM → EM 2단계 계층 룩업
그림 ndd-70. TruFlow TCAM/EM 용량 계획 결정 트리 — 규칙 특성에 따라 TCAM(와일드카드), EM(정확 매칭), 또는 계층 룩업 경로 선택

bnxt_en의 SR-IOV 구현은 HWRM을 통한 완전한 VF 리소스 분할을 특징으로 합니다. PF 드라이버가 HWRM 프록시 역할을 하여 VF의 모든 하드웨어 요청을 중개합니다.

PF Driver HWRM Proxy (VF 대행) devlink eswitch VF 최대 256개 VF 0 Ring 0-3 VNIC 0 Stat Ctx 0 VF 1 Ring 4-7 VNIC 1 Stat Ctx 1 ... VF n Ring n*4.. VNIC n Stat Ctx n HWRM FW Resource Manager Ring Pool VNIC Pool Stat Pool MSI-X Pool 접근 제어 (VF 격리) NIC HW TX/RX Queues RSS/TruFlow MAC/VLAN Filter SerDes/PHY HWRM Cmd VF→PF Proxy VF Representors (switchdev 모드) enp3s0f0_0, enp3s0f0_1, ... enp3s0f0_n → OVS/TC 연동
그림 ndd-30. bnxt_en PF-VF HWRM 모델 — PF가 VF의 HWRM 요청을 프록시하여 리소스 격리 보장
# 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으로 분할합니다.

Physical Port 10 GbE MCP Management Controller BW Arbiter NPAR 분배 관리 NPAR 0 MAC: 00:11:22:33:44:00 Queue 0-3 CP 0-3 BW: min 25% / max 40% iSCSI Offload NPAR 1 MAC: 00:11:22:33:44:01 Queue 4-7 CP 4-7 BW: min 25% / max 50% FCoE Offload NPAR 2 MAC: 00:11:22:33:44:02 Queue 8-11 CP 8-11 BW: min 25% / max 30% 네트워크 전용 NPAR 3 MAC: 00:11:22:33:44:03 Queue 12-15 CP 12-15 BW: min 25% / max 30% 관리용(IPMI) Host OS 1 eth0 (NPAR 0) Host OS 2 eth0 (NPAR 1) VMware ESXi vmnic2 (NPAR 2)
그림 ndd-31. bnx2x NPAR 구조 — 물리 포트를 4개의 독립 가상 NIC으로 분할, 각각 고유 MAC/큐/대역폭 보유
비교 항목 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)가 될 수 있습니다. 안전한 업데이트 절차는 다음과 같습니다.

  1. 모든 트래픽 중지: ip link set eth0 down (모든 NPAR 파티션)
  2. 현재 펌웨어 백업: bnxtnvm -dev=eth0 backup
  3. 이중화 경로 확인: 다른 NIC으로 관리 접속 유지
  4. 업데이트 실행: bnxtnvm -dev=eth0 update -f new_fw.pkg
  5. 업데이트 중 절전 모드(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)와의 사이드밴드 채널을 통한 원격 관리 통합입니다.

Host CPU / OS tg3 드라이버 eth0 (data) eth1 (mgmt) PCIe Bus (Gen2 x1) APE FW 인터페이스 BCM5719/5720 NIC APE (ARM Processor Engine) NCSI / ASF / IPMI 처리 TX DMA RX DMA MAC (L2 Filter / VLAN) Copper PHY SerDes NC-SI Sideband Channel BMC IPMI Stack SOL KVM/vMedia Redfish / Web UI SMBus / I2C 연결 네트워크 (RJ45) PCIe NC-SI Sideband ASF 2.0 경로 Heartbeat (ARP/UDP) Alert (PET Trap → SNMP)
그림 ndd-32. tg3 + BMC 사이드밴드 통합 — APE 프로세서가 NC-SI를 통해 BMC와 NIC 포트를 공유

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) 협상 시 스위치 호환성 문제로 링크가 간헐적으로 드롭됩니다. 증상은 수 분~수십 분 간격으로 링크가 재협상되며, dmesgtg3 eth0: Link is downLink is up이 반복됩니다.

TSO 체크섬 오류: ethtool -Stx_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 부하를 최소화합니다.

Port GMAC/ XLMAC Parser L2 헤더 파싱 L3/L4 식별 VLAN 처리 PPPoE/DSA 커스텀 프로토콜 (TCAM 기반) Classifier C2 엔진 (TCAM 룩업) Flow ID 생성 QoS 마킹 DSCP→TC 매핑 Policer Token Bucket CIR/CBS 설정 초과 시 Drop RSS 해시 계산 5-tuple 기반 Indirection Table RXQ 0 RXQ 1 RXQ 2..N DMA BD Ring Scatter Gather CPU NAPI Poll BM Pool (Buffer Manager) Long Pool (2K) Short Pool (512) Jumbo Pool (10K) 하드웨어 버퍼 할당/해제 — CPU 개입 최소화 버퍼 공급
그림 ndd-33. mvpp2 하드웨어 파이프라인 — Parser → Classifier → Policer → RSS → RXQ → DMA → CPU, BM Pool이 버퍼 관리
비교 항목 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(프레임 프리엠션)를 하드웨어 수준에서 구현합니다.

IEEE 1588 PTP 시간 기준 (gPTP Grandmaster 동기화) 나노초 정밀도 — 모든 TSN 기능의 시간 참조 Traffic Classes TC 7 (최고 우선순위) 제어 트래픽 TC 6 (Scheduled) 실시간 데이터 TC 5 (Reserved) AV 스트림 TC 4..1 TC 0 (Best Effort) 일반 트래픽 802.1Q 우선순위 매핑 Gate Control List (802.1Qbv Time-Aware Shaper) Slot 0: 0~125us Gate: TC7=O TC6=O 나머지=C Slot 1: 125~500us Gate: TC6=O TC5=O 나머지=C Slot 2: 500~875us Gate: TC5=O TC4..0=O TC7,6=C Slot 3: 875~1000us Gate: 전체 Open (BE) 주기: 1ms (1000us) O = Open, C = Closed 802.1Qci Stream Filter Stream ID 매칭 Gate Check Rate Policer 802.1Qbu Frame Preemption Express: TC7,6 Preemptable: TC0-5 TX MAC eMAC (express) pMAC (preempt) Wire 2.5 GbE
그림 ndd-34. ENETC TSN 스케줄링 — 802.1Qbv Gate Control List가 시간 슬롯별로 TC 게이트를 제어, Qbu 프리엠션 연동
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) 안에서 그룹화됩니다.

MC (Management Complex) Firmware 객체 생성 / 연결 / 수명 관리 — restool 또는 DPL(Data Path Layout)로 설정 DPRC (Root Container) — dprc.1 리눅스 커널 fsl-mc 버스 드라이버가 관리 DPRC (Child) — dprc.2 VM/Container 할당 가능 DPNI.0 Network Interface → eth0 DPNI.1 Network Interface → eth1 DPIO.0 QBMan Portal CPU 0 DPIO.1 QBMan Portal CPU 1 DPBP.0 Buffer Pool 2048 buffers DPMAC.1 MAC/PHY 25 GbE DPMAC.2 MAC/PHY 10 GbE DPCON.0 Concentrator (CH) DPSW.0 L2 Switch (선택 사항) DPNI.2 VM eth0 DPIO.2 vCPU Portal DPBP.1 VM 버퍼풀
그림 ndd-35. dpaa2 객체 모델 — MC 펌웨어가 DPRC 컨테이너 안에서 DPNI/DPIO/DPBP/DPMAC 객체를 관리

데이터 경로에서 패킷은 WRIOP(Packet I/O Engine)이 처리하고, QBMan(Queue-Based Manager) 포털을 통해 CPU와 데이터를 교환합니다.

Network Wire DPMAC SerDes Lane phylink L1 처리 WRIOP (Packet I/O Engine) Parser (L2-L4) Distribution (RSS) Policer/Shaper Order Restoration DPNI Flow Table TX/RX Queues QoS Table QBMan Queue Manager Frame Queue Buffer Pool Channel (CDAN) Enqueue/Dequeue Lock-free 디자인 DPIO QBMan Portal MMIO Region per-CPU 바인딩 CPU dpaa2-eth 드라이버 NAPI + XDP RX FQ MMIO CDAN IRQ TX Enqueue DPBP Buffer Pool 객체
그림 ndd-36. dpaa2 데이터 경로 — DPMAC → WRIOP → DPNI → QBMan → DPIO Portal → 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년 이상의 칩 세대를 단일 드라이버로 지원하며, 각 변형마다 고유한 버그 회피 코드가 존재합니다.

r8169 TX/RX 데이터 경로 TX 경로 Application Socket qdisc (single TXQ) TX DMA Ring 256 desc PCI DMA PCIe Bus RTL Chip PHY RX 경로 PHY RTL Chip RX DMA Ring 256 desc NAPI Poll budget=64 GRO 병합 napi_gro_receive netif_receive_skb Socket IRQ Coalescing IntrMask: RxOK|TxOK 첫 IRQ→NAPI 스케줄
그림 37. r8169 TX/RX 데이터 경로 — 단일 큐 구조에서 NAPI 폴링 기반 수신 처리
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 미지원(단일 큐)
ASPM 상호작용 문제: RTL8111C/D 칩셋에서 ASPM(Active State Power Management) L1 상태 진입 시 링크가 불안정해지는 현상이 보고됩니다. 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개 디스크립터를 지원하며, 멀티 큐 확장 없이 단일 링으로 동작합니다. 이는 높은 패킷 수신률 환경에서 링 오버플로우를 유발할 수 있습니다.

RTL ASIC 백프레셔 부재: RTL811x/RTL8125 ASIC는 RX FIFO가 가득 찰 때 PHY 레벨 흐름제어(pause frame)를 자동으로 발생시키지 않습니다. 결과적으로 FIFO 오버플로우가 발생하면 패킷이 조용히 드롭됩니다.

ethtool -S로 확인할 수 있는 두 카운터의 의미는 다릅니다:

링 오버플로우를 실시간으로 진단하려면 bpftracer8169_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)로 교체.

링크 드롭/지연 의심 ethtool -S eth0 rx_missed_errors 증가? YES HW FIFO 오버플로우 확인됨 NO 다른 원인 조사 (케이블, 드라이버 버그 등) rx_fifo_errors도 증가? YES 링 오버플로우 확진 rx-usecs 증가 (코얼레싱 강화) QoS로 입력 속도 제한 (tc qdisc) 멀티 큐 NIC 교체 (igc, r8125)
r8169 RX Ring Overflow 진단 플로우차트 — rx_missed_errors → rx_fifo_errors 순서로 확인 후 해결책 선택
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);
}
quirk 수동 확인 방법: 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 효과 미미)
어떤 드라이버를 사용할 것인가: 대부분의 사용 환경에서 in-tree 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/)는 이 독특한 아키텍처를 반영합니다.

AQC 펌웨어 아키텍처 호스트 (x86/ARM) atlantic 드라이버 aq_nic.c aq_ring.c FW Mailbox Interface DMA Rings (TX/RX) PCIe Gen3 x4 AQC ASIC MIPS FW Core 펌웨어 엔진 Mailbox Regs Offload Engine Checksum/LSO RSS Engine Toeplitz Hash MAC (10G) PHY (10GBASE-T) MDI (RJ45) Flow Steering / EtherType Filter / VLAN Filter
그림 38. AQC 펌웨어 아키텍처 — MIPS 코어 기반 펌웨어와 호스트 드라이버 메일박스 통신 구조
AQC RSS 멀티큐 수신 흐름 Ingress Packet L2/L3/L4 Parser src/dst IP+Port 추출 RSS Hash (Toeplitz) 40-byte 비밀 키 → 32bit hash Queue Selector hash % N큐 인디렉션 테이블 RXQ 0 RXQ 1 RXQ 2 RXQ 3 RXQ N (최대 8큐) NAPI 0 NAPI 1 NAPI 2 NAPI 3 NAPI N
그림 39. AQC RSS 멀티큐 수신 흐름 — Toeplitz 해시 기반 큐 분배와 per-queue NAPI 인스턴스
펌웨어 버전 의존성
칩셋 최소 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 버그 다수 수정
펌웨어 업데이트: AQC 시리즈의 펌웨어는 NIC 플래시 ROM에 저장되며, 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로 인한 링크 드롭 증상: EEE wake-up 지연 중 들어오는 패킷이 유실될 수 있습니다. 특히 레이턴시 민감한 실시간 애플리케이션이나 짧은 버스트 트래픽 환경에서 EEE가 간헐적 링크 드롭이나 TCP 재전송을 유발할 수 있습니다.
# 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 버전 확인
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 시 동일 링을 통해 재전송합니다.

AF_XDP zero-copy 미지원 이유: AQC NIC의 DMA 버퍼 할당은 MIPS FW가 제어합니다. FW가 DMA 버퍼 레이아웃을 관리하기 때문에 커널이 해당 메모리 페이지를 사용자공간과 직접 공유(zero-copy)하는 구조를 구현할 수 없습니다. AF_XDP zero-copy는 드라이버가 DMA 버퍼를 완전히 제어할 때만 가능합니다.

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× 느림
Active 정상 링크 동작 LPI Request IDLE 감지, 신호 전송 LPI 저전력 유지 중 Wake PHY 재훈련 중 IDLE 감지 (트래픽 없음) 파트너 동의 TX 필요 재훈련 완료 취소 Wake-up 지연 (Tw_sys_tx) 100M: 30 μs 1G: 16.5 μs 2.5G: 20 μs 5G: 25 μs 10G: 4.5 μs PHY 재훈련 지연 링크 파트너와 협상 재시작 패킷 손실 가능 구간
EEE LPI 상태 전환 다이어그램 — Active↔LPI Request→LPI↔Wake 전환 및 속도별 wake-up 지연 시간

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), 그리고 암호화 가속기입니다.

Chelsio T6 ASIC 엔진 구조 100G Port ×2 MPS MAC Lookup VLAN Filter Port Switch Multicast Replication 512 MAC 엔트리 TP (TCP/IP Engine) Protocol Parsing TCP State Machine Checksum Verify RSS Hash Calc Flow Lookup TCAM Filter ───────── Retransmission Congestion Ctrl Segmentation Reassembly ULP Engine TOE iSCSI Offload RDMA (iWARP) NVMe-oF FCoE TLS Record Crypto Engine AES-GCM/CBC SHA-256/512 DTLS/TLS 1.3 SGE Scatter-Gather Engine TX/RX Queues Free List Mgmt DMA Coalescing Doorbell 최대 256 큐페어 PCIe IF Gen3 x16 128 Gbps SR-IOV → Host CPU 프로토콜 처리 오프로드 엔진 암호화 가속 DMA 엔진
그림 40. Chelsio T6 ASIC 엔진 구조 — MPS, TP, ULP, SGE, Crypto 엔진의 파이프라인 구성
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)는 제로카피 데이터 전송을 가능하게 합니다.

커널 TCP 스택 vs TOE 오프로드 경로 비교 일반 커널 TCP 경로 Application (send/recv) Socket Layer TCP/IP Stack (커널) qdisc / TC Layer NIC Driver (cxgb4) T6 ASIC (SGE only) CPU 부하 높음 TOE 오프로드 경로 Application (send/recv) Socket Layer TOE Module (toecore) TCP/IP 바이패스 (커널 스택 우회) cxgb4 + tom (TOE driver) T6 ASIC (TP+ULP+SGE) HW TCP State Machine CPU 부하 낮음 모든 패킷 CPU 처리 데이터만 CPU 전달
그림 41. 커널 TCP 스택 vs TOE 오프로드 경로 — TOE는 TCP/IP 처리를 ASIC으로 완전히 오프로드
오프로드 성능 영향
모드 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 서버
TOE 사용 시 주의사항: TOE는 TCP 연결 상태를 ASIC 메모리에 유지하므로, 동시 연결 수에 하드웨어 제한이 있습니다. 또한 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 오프로드를 통해 소프트웨어적으로도 제어 가능합니다.

cxgb4 패킷 분류 파이프라인 Ingress Packet Wire/PHY MPS MAC Lookup (DMAC 매칭) VLAN Filter Promisc Mode Multicast Hash 512 MAC entries TP Protocol Parse L3/L4 Decode TCP/UDP 구분 Checksum 검증 RSS Hash 계산 Toeplitz/XOR Filter TCAM 매칭 (정확/와일드카드) Hash Filter Action 결정 Drop/Pass/Steer 2048 TCAM rules Queue Steering RSS 인디렉션 Filter 결과 큐 선택 결정 Priority 반영 → SGE Queues SGE Q0 SGE Q1 SGE Q... SGE QN DROP 분류 순서: 1. L2 (MPS) 2. L3/L4 (TP) 3. 필터 (TCAM) 4. 큐 배정
그림 42. cxgb4 패킷 분류 파이프라인 — MPS→TP→Filter→Queue Steering→SGE 다단계 하드웨어 분류
필터 설정 및 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
TCAM vs Hash 필터 선택: TCAM 필터는 와일드카드 매칭을 지원하지만 엔트리 수가 제한적(T6에서 2048개)입니다. 대량의 정확한 매칭 규칙이 필요한 경우 해시 기반 필터를 사용하면 수만 개의 규칙을 하드웨어에 오프로드할 수 있습니다. tc flower에서 skip_sw 플래그는 소프트웨어 경로를 건너뛰고 하드웨어만으로 필터링하며, skip_hw는 반대로 소프트웨어만 사용합니다.
cxgb4 디버깅: Chelsio 어댑터는 /sys/kernel/debug/cxgb4/ 디버그 파일시스템(Filesystem)을 통해 내부 레지스터, 필터 테이블, 큐 상태, 펌웨어 로그 등을 상세히 확인할 수 있습니다. cudbg(Chelsio Unified Debug) 도구를 사용하면 어댑터 전체 상태를 덤프하여 오프라인 분석이 가능합니다.

Cloud NIC: ENA / gVNIC

Cloud NIC는 퍼블릭 클라우드 환경(AWS, GCP)에서 가상 머신에 노출되는 전용 네트워크 어댑터입니다. 기존 에뮬레이션 기반 NIC(e1000, rtl8139)와 달리, 호스트 하이퍼바이저의 네트워크 스택과 긴밀하게 통합되어 베어메탈에 가까운 성능을 제공합니다. AWS의 ENA(Elastic Network Adapter)와 Google Cloud의 gVNIC(Google Virtual NIC)이 대표적입니다.
Cloud NIC 드라이버 비교
항목ENA (AWS)gVNIC (GCP)
모듈명enagve
소스 위치drivers/net/ethernet/amazon/ena/drivers/net/ethernet/google/gve/
큐 인터페이스Admin Queue + I/O SQ/CQGQI / DQO (세대별)
최대 큐 수32 (인스턴스 타입별 상이)16 (인스턴스 타입별 상이)
최대 대역폭200 Gbps (p5.48xlarge)200 Gbps (C3D)
저지연 프로토콜ENA Express (SRD)해당 없음
XDP 지원O (v2.6.1+)O (GQI 모드)
라이선스GPL v2GPL v2

ENA (Elastic Network Adapter) 아키텍처

ENA는 AWS Nitro 하이퍼바이저와 통합된 네트워크 어댑터로, EC2 인스턴스에 고성능 네트워크 I/O를 제공합니다. ENA 디바이스는 Admin Queue를 통한 제어 경로와 I/O Submission/Completion Queue를 통한 데이터 경로를 분리하여 효율적인 패킷 처리를 달성합니다.

핵심 개념: ENA는 NVMe와 유사한 큐 기반 아키텍처를 채택합니다. Admin Queue는 NVMe의 Admin Queue에 해당하고, I/O SQ/CQ는 NVMe의 I/O Queue에 대응합니다. 이 설계 덕분에 멀티코어 환경에서 CPU별 독립 큐를 할당하여 lock 경합 없이 패킷을 처리할 수 있습니다.
아키텍처 개요

ENA 드라이버는 struct ena_adapter를 중심으로 동작합니다. 초기화 시 Admin Queue를 통해 디바이스와 협상하여 지원 기능, 큐 수, MTU 등을 결정합니다. 데이터 전송은 I/O SQ(Submission Queue)에 디스크립터를 기록하고 MMIO doorbell을 울려 디바이스에 통보하는 방식입니다. 완료된 패킷은 CQ(Completion Queue)를 통해 NAPI 폴링으로 처리됩니다.

EC2 Instance Application (socket API) 커널 네트워크 스택 ENA Driver (ena.ko) Admin Queue I/O SQ/CQ pairs MMIO Doorbells ENA Device (Nitro Card) Admin Engine I/O Processing Engine DMA Engine Network Port Network Fabric 제어 경로 데이터 경로 doorbell
그림 43. ENA 아키텍처 — Admin Queue 제어 경로와 I/O SQ/CQ 데이터 경로 분리
LLQ (Low Latency Queue) vs Regular Queue

ENA는 두 가지 큐 동작 모드를 지원합니다. Regular 모드에서는 디스크립터와 패킷 데이터가 모두 호스트 메모리에 존재하며, 디바이스가 DMA로 읽어갑니다. LLQ(Low Latency Queue) 모드에서는 디스크립터와 패킷 헤더를 디바이스 메모리(BAR 공간)에 직접 기록하여 DMA 왕복 지연을 제거합니다.

LLQ의 이점: 패킷 헤더(최대 224바이트)를 디바이스 메모리에 push 방식으로 기록하면, 디바이스가 호스트 메모리로 DMA 읽기를 수행하는 단계가 생략됩니다. 이로 인해 소형 패킷의 전송 지연이 수 마이크로초 단축됩니다. 대부분의 Nitro 기반 인스턴스에서 LLQ가 기본 활성화됩니다.
LLQ vs Regular Queue 비교 Regular Mode Host Memory Descriptor Pkt Data ENA Device (DMA 읽기) DMA ⏱ 높은 지연: doorbell → DMA desc → DMA data → 전송 1. Doorbell 2. DMA Desc 3. DMA Data 4. 전송 LLQ Mode (Low Latency Queue) Host Memory Pkt Body Device Memory Descriptor Pkt Header MMIO Push DMA Body ⏱ 낮은 지연: push desc+hdr → DMA body → 전송 1. Push Hdr 2. DMA Body 3. 전송
그림 44. LLQ vs Regular Queue — LLQ는 디스크립터+헤더를 디바이스 메모리에 직접 push하여 DMA 왕복 제거
ENA Express (SRD — Scalable Reliable Datagram)

ENA Express는 AWS가 개발한 SRD(Scalable Reliable Datagram) 프로토콜을 활용하여 단일 흐름(single flow)의 대역폭을 최대 25 Gbps까지 확장하고, p99 지연 시간을 크게 줄이는 기능입니다. 전통적인 TCP/UDP는 5-tuple 해싱으로 단일 경로에 바인딩되지만, SRD는 여러 물리 경로에 패킷을 분산하고 수신 측에서 재정렬합니다.

주의: ENA Express는 드라이버 수준이 아닌 Nitro 네트워크 인프라에서 동작합니다. 드라이버 코드에 SRD 로직이 포함되지 않으며, EC2 콘솔 또는 AWS CLI에서 인스턴스/ENI 단위로 활성화합니다. ENA Express가 활성화되면 ethtool -S에서 SRD 관련 통계를 확인할 수 있습니다.
인스턴스 타입별 ENA 기능 매트릭스
EC2 인스턴스 패밀리별 ENA 기능 지원
인스턴스 패밀리최대 대역폭LLQENA Express최대 큐 수Enhanced 모니터링
m5 / c5 / r525 GbpsOO8O
m6i / c6i / r6i50 GbpsOO16O
m7g / c7g / r7g30 GbpsOO16O
m7i / c7i50 GbpsOO32O
hpc7g200 GbpsOO32O
p5.48xlarge3200 Gbps (EFA)O-32O
t3 / t3a5 GbpsO-2O
큐 구성 및 인터럽트 조절

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
성능 튜닝 팁: ENA의 최적 성능을 위해서는 (1) RPS/XPS를 적절히 설정하여 CPU 분산을 최적화하고, (2) 점보 프레임(MTU 9001)을 활성화하며, (3) TCP 세그먼트 수를 늘리고(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(Scalable Reliable Datagram) 멀티패스 아키텍처 Per-packet spraying → Nitro Card 수신 측 재정렬 → TCP 투명 전달 Instance A ENA (Nitro Card) SRD per-packet spraying Nitro Card A (sender) Pkt sprayer Seq numbering Path selector 경로 1 (Pkt 1,5…) 경로 2 (Pkt 2,6…) 경로 3 (Pkt 3,7…) 경로 4 (Pkt 4,8…) Path 1 Path 2 Path 3 Path 4 Nitro Card B (receiver) Reorder buffer Seq reordering TCP-transparent out Instance B TCP stack sees ordered stream only 기존 ECMP: ~5 Gbps/flow SRD 멀티패스: ~25 Gbps/flow p99 지연 ~50% 감소 (경로 혼잡 우회)
그림 68. SRD 멀티패스 아키텍처 — 패킷을 4개 경로에 분산하고 수신 측 Nitro Card에서 재정렬하여 단일 흐름 25 Gbps 달성

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 -Sena_srd_eligible_tx_pktsena_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 설정도 함께 재적용합니다.

프로덕션 트래픽 중 ENA 리셋 영향: ENA 리셋은 일반적으로 1~3초의 네트워크 중단을 유발합니다. TCP 세션은 재전송 타이머(RTO)에 의해 자동 복구되지만, 짧은 RTO 설정 (net.ipv4.tcp_retries2 감소)은 세션이 리셋 전에 끊길 수 있습니다. 완화 방법: (1) multi-path 애플리케이션의 경우 연결 재시도 로직 구현, (2) ENA Enhanced Networking 활성화로 리셋 빈도 최소화, (3) CloudWatch NetworkPacketsOut 지표에 알람 설정.
ENA 주요 장애 패턴 및 dmesg 메시지
장애 유형dmesg 패턴원인대응
AdminQ 타임아웃ena: admin_q timed outNitro Card 과부하, 인프라 점검자동 리셋; 반복 시 인스턴스 교체
Keep-alive 실패ena: Keep alive watchdog timeout호스트 인프라 재시작자동 복구; 반복 시 AWS Support 연락
TX 타임아웃ena: TX timeout on queue X큐 스톨, HW 버그drv 버전 업그레이드 확인
불량 완료 디스크립터ena: Invalid req_idDMA 오염, 드라이버 버그커널/드라이버 업데이트
메모리 할당 실패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는 1세대 인터페이스로 공유 메모리 기반 디스크립터 링을 사용합니다. DQO는 2세대 인터페이스로 TX/RX 각각에 디스크립터 큐와 별도의 Completion 큐를 두어, NVMe와 유사한 방식으로 더 효율적인 완료 처리를 지원합니다. C3/C3D/H3 등 최신 인스턴스에서는 DQO가 기본으로 사용됩니다.
GQI (Google Queue Interface) 모델 Guest VM gve Driver (GQI mode) TX Desc Ring (QPL buffer) RX Desc Ring (QPL buffer) Shared Memory (Queue Page List) Guest ↔ Host 공유 페이지 영역 Notification Blocks (doorbell) Host (gVNIC Backend) gVNIC Backend Engine Shared Memory Access QPL 페이지를 직접 참조 (복사 불필요) Andromeda vSwitch Physical NIC QPL 공유 notify Network
그림 45. GQI 모델 — QPL(Queue Page List) 공유 메모리 기반 디스크립터 링
DQO (Descriptor Queue Organization) 모델 Guest VM gve Driver (DQO mode) TX Desc Queue TX Compl Queue RX Desc Queue RX Compl Queue DMA-mapped buffers (per-packet) Host (gVNIC Backend) DMA Engine Packet Processing Pipeline Completion Generation Andromeda → Network DMA fetch completion
그림 46. DQO 모델 — 디스크립터 큐와 Completion 큐를 분리하여 NVMe 스타일의 비동기 완료 처리
GQI vs DQO 성능 특성 비교 상대 성능 (%) 0 25 50 75 100 처리량 (단일 흐름) 처리량 (다중 흐름) 지연 시간 (p50) 지연 시간 (p99) CPU 효율 GQI DQO 75% 100% 90% 97% 75% 92% 62% 87% 70% 90%
그림 47. GQI vs DQO 성능 비교 — DQO가 모든 지표에서 GQI 대비 우수 (높을수록 좋음)
GQI vs DQO 기능 비교
GQI vs DQO 상세 비교
항목GQIDQO
디스크립터 형식통합 디스크립터 링분리된 Desc/Compl 큐
메모리 모델QPL (Queue Page List) 공유 메모리per-packet DMA 매핑
완료 처리같은 링에서 완료 확인별도 Completion Queue
버퍼 관리사전 할당된 공유 페이지 풀동적 DMA 버퍼 할당
XDP 지원OO (5.18+)
Header SplitXO
큐당 최대 디스크립터10244096
인터럽트 조절기본적응형(Dim) 지원
지원 인스턴스N1, N2, E2, T2DC3, C3D, H3, A3, Z3
최대 대역폭100 Gbps200 Gbps
인스턴스 타입별 큐 모델 매핑
GCP 인스턴스와 gVNIC 큐 모델
인스턴스 시리즈큐 모델최대 NIC 수최대 큐 수최대 대역폭
N1 / N2 / N2DGQI81632 Gbps (100G Tier1)
E2GQI8816 Gbps
T2DGQI8832 Gbps
C3 / C3DDQO1516200 Gbps (C3D)
H3DQO816200 Gbps
A3DQO1516200 Gbps
Z3DQO1516100 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
GCP 네트워크 성능 최적화: (1) Tier_1 네트워킹을 활성화하면 N2 인스턴스에서도 최대 100 Gbps 대역폭을 사용할 수 있습니다. (2) gVNIC은 Jumbo Frame(MTU 8896)을 지원하며, VPC 내부 통신에서 대역폭을 크게 향상시킵니다. (3) 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의 메모리 낭비가 사라지고, 불필요한 데이터 복사도 제거됩니다.

DQO Header Split 버퍼 풀 구조 gVNIC (DQO mode) RX Desc Q RX Compl Q DMA Engine Header + Data Header Pool (256B 고정 크기) hdr[0] hdr[1] L1/L2 캐시 적합 Data Pool (2K~4K 페이지) page[0] page[1] page_pool 관리 DMA hdr DMA data GRO merge skb coalesce frag list merge Network Stack TCP/IP processing hdr: L1/L2 cache hit data: zero-copy possible ~15% 성능 향상 ← 헤더: L1/L2 적합 (소형) | 데이터: L3/LLC 수용 (대형) →
그림 69. DQO Header Split — 헤더(256B)와 페이로드(4K page)를 별도 풀에 분리하여 캐시 효율 최적화
# 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의 특성을 이해하는 것이 중요합니다.

클라우드 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
클라우드 NIC 헬스 모니터링 권장 사항: (1) 공통: 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 지표와 dmesghv_netvsc 메시지를 조합하여 VF failover 횟수 추적.

가상화 NIC: virtio_net / vmxnet3 / hv_netvsc

가상화 NIC는 하이퍼바이저가 게스트 가상 머신에 제공하는 반가상화(paravirtualized) 네트워크 어댑터입니다. 하드웨어를 완전 에뮬레이션하는 방식(e1000, rtl8139)과 달리, 게스트와 호스트가 협력하여 최적화된 데이터 경로를 구성합니다. virtio_net(KVM/QEMU), vmxnet3(VMware), hv_netvsc(Hyper-V/Azure)가 각 하이퍼바이저 생태계의 표준 반가상화 NIC입니다.
가상화 NIC 드라이버 비교
항목virtio_netvmxnet3hv_netvsc
하이퍼바이저KVM / QEMUVMware ESXiHyper-V / Azure
모듈명virtio_netvmxnet3hv_netvsc
소스 위치drivers/net/virtio_net.cdrivers/net/vmxnet3/drivers/net/hyperv/
전송 인터페이스virtqueue (vring)공유 메모리 큐VMBus + VF
최대 큐 수호스트 설정 의존32 (vHW v4+)64 (RSS)
SR-IOV VF 통합X (별도 구성)UPT passthroughO (Accelerated Networking)
XDP 지원O (5.0+)XO (5.6+)
라이선스GPL v2GPL v2GPL 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에 인덱스를 기록하고 인터럽트를 발생시킵니다.

Packed Virtqueue (virtio 1.1): 기존 Split Virtqueue에서는 Descriptor Table, Available Ring, Used Ring이 별도 메모리 영역에 존재하여 캐시 효율이 낮았습니다. virtio 1.1에서 도입된 Packed Virtqueue는 세 영역을 하나의 링으로 통합하여 캐시 라인(Cache Line) 활용도를 높이고, wrap counter 기반으로 소유권을 관리합니다.
Guest VM virtio_net Driver TX Virtqueue desc + avail + used RX Virtqueue desc + avail + used vring (Shared Memory) Guest-Host 공유 메모리 영역 NAPI poll Mergeable Bufs Control Virtqueue (CVQ) kick (notify) interrupt Host vhost-net (kernel module) QEMU (userspace 폴백) vhost-user (DPDK 등) tap device Linux Bridge OVS Physical NIC Network vring 접근
그림 48. virtio_net 아키텍처 — Guest의 virtqueue에서 Host의 vhost-net/QEMU를 거쳐 물리 NIC까지의 전체 데이터 경로
Virtqueue 디스크립터 체인과 링 동작 Descriptor Table [0] addr=0x1000 len=14 F=NEXT [1] addr=0x2000 len=1500 F=0 [2] addr=0x3000 len=4096 F=WR [3] addr=0x4000 len=4096 F=WR [4] (free) ... next Available Ring (Guest → Host) idx=0 idx=2 ... flags | idx | ring[] | used_event Used Ring (Host → Guest) id=0,len=14 ... flags | idx | ring[]{id,len} Host Backend 1. avail ring 확인 2. desc chain 처리 3. used ring 기록 4. interrupt 발생 kick 완료
그림 49. Virtqueue 디스크립터 체인 — Descriptor Table에서 체인을 구성하고 Available/Used Ring으로 소유권 이전
vhost-net (커널) vs vhost-user (유저스페이스) 비교 vhost-net (커널 모드) Guest VM virtio_net driver vring (공유 메모리) QEMU (제어만) vhost-net 커널 모듈 커널 스레드가 vring 직접 접근 (제로 카피) tap device Physical NIC 직접 접근 장점 VM exit 최소, 제로 카피 vhost-user (유저스페이스 모드) Guest VM virtio_net driver vring (공유 메모리) QEMU (제어만) vhost-user Backend (DPDK/SPDK) 유저 프로세스가 hugepage 통해 vring 접근 mmap 접근 Physical NIC (PMD 직접) 장점 커널 우회, 폴링 모드, 극저지연 Unix Socket 제어 (fd 전달, 메모리 맵)
그림 50. vhost-net vs vhost-user — vhost-net은 커널에서 vring 직접 처리, vhost-user는 DPDK 등 유저스페이스에서 hugepage 기반 접근
Feature Negotiation
virtio_net 주요 Feature Bit
Feature Bit이름설명성능 영향
0VIRTIO_NET_F_CSUM호스트가 체크섬 오프로드 지원TX CPU 부하 감소
1VIRTIO_NET_F_GUEST_CSUM게스트가 체크섬 오프로드 처리RX CPU 부하 감소
5VIRTIO_NET_F_MAC디바이스가 MAC 주소 제공-
11VIRTIO_NET_F_HOST_TSO4호스트가 TCP Segmentation Offload 지원대역폭 증가
15VIRTIO_NET_F_MRG_RXBUFMergeable RX Buffers대형 패킷 효율 향상
17VIRTIO_NET_F_STATUS링크 상태 알림-
18VIRTIO_NET_F_CTRL_VQControl Virtqueue 지원동적 설정 변경
22VIRTIO_NET_F_MQ멀티큐 지원멀티코어 스케일링
25VIRTIO_NET_F_SPEED_DUPLEX속도/이중 모드 설정-
34VIRTIO_F_ORDER_PLATFORM플랫폼 메모리 순서 사용배리어 오버헤드 감소
39VIRTIO_F_RING_PACKEDPacked 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 최적화 팁
virtio_net 성능 최적화 체크리스트:
  • 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 VirtqueuePacked 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 QMPioctl(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), 적응형 인터럽트 합산을 지원합니다.

UPT (Uniform Passthrough) 모드: vmxnet3는 SR-IOV VF를 투명하게 통합하는 UPT 모드를 지원합니다. UPT 활성 시 패킷이 vmkernel을 우회하여 VF에서 게스트로 직접 전달되며, VF 하드웨어가 없거나 기능 제한이 필요한 경우 자동으로 에뮬레이션 모드로 폴백합니다. 게스트 드라이버 관점에서 경로 전환은 투명합니다.
Guest VM vmxnet3 Driver TX Queue desc + data ring RX Queue ring1 + ring2 Completion Rings (TX/RX) Shared Memory Region driver ↔ vmkernel 공유 RSS (Toeplitz) Interrupt Coalescing ESXi vmkernel vmxnet3 Backend vSwitch (Standard/DVS) Uplink vmnic 에뮬레이션 Physical NIC UPT Bypass SR-IOV VF Direct I/O Physical NIC UPT (vmkernel 우회)
그림 51. vmxnet3 아키텍처 — 에뮬레이션 경로(vmkernel 경유)와 UPT bypass 경로(SR-IOV VF 직접)
vHW 버전별 기능 매트릭스
vmxnet3 Virtual Hardware 버전별 지원 기능
기능vHW v1vHW v2vHW v3vHW v4vHW v5vHW v6vHW v7
ESXi 최소 버전5.56.06.56.77.07.0 U28.0
최대 TX/RX 큐8/88/88/832/3232/3232/3232/32
RSSOOOOOOO
LROOOOOOOO
Coalescing 세분화기본기본확장확장확장확장확장
UPT (SR-IOV)XOOOOOO
TimestampingXXXXOOO
Uniform Passthrough v2XXXXXXO
Large TX descXXXOOOO
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
vmxnet3 LRO 주의사항: vmxnet3의 LRO는 하드웨어 수준에서 패킷을 합치므로, VM이 라우터나 브리지(Bridge)로 동작하는 경우 LRO를 비활성화해야 합니다. 합쳐진 패킷을 다른 인터페이스로 전달하면 수신 측에서 TSO 정보가 없어 재분할이 불가능한 대형 패킷이 전송될 수 있습니다.
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 인증 물리 NICSR-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) 덕분에 라이브 마이그레이션 시에도 네트워크 연결이 유지됩니다.

Azure VM Application 커널 네트워크 스택 hv_netvsc (eth0) 패킷 경로 결정 및 VF 관리 Synthetic Path VMBus 채널 VF Path SR-IOV VF 직접 VMBus Ring Buffer (send/recv rings) VF Driver (mlx5_core / mana) RNDIS/NVSP 프로토콜 VF Hardware (PCIe BAR) Hyper-V Host Azure Virtual Switch SDN, ACL, VFP (Virtual Filtering Platform) NetVSP (합성 백엔드) SR-IOV PF (VF 관리) Physical NIC (Mellanox ConnectX / MANA) Azure Network VMBus PCIe passthrough
그림 52. hv_netvsc 이중 경로 — VMBus 합성 경로(폴백)와 SR-IOV VF 가속 경로(데이터 평면)를 투명하게 관리
VF Failover 매커니즘

hv_netvsc의 핵심 기능 중 하나는 VF failover입니다. Azure 호스트는 라이브 마이그레이션, 호스트 유지보수, 또는 기타 이유로 VF를 동적으로 제거하고 재할당할 수 있습니다. hv_netvsc는 이 과정을 상위 계층에 투명하게 처리합니다: VF가 제거되면 자동으로 합성 경로로 전환하고, VF가 다시 제공되면 가속 경로로 복귀합니다.

VF Failover 시퀀스 — 라이브 마이그레이션 시나리오 시간 정상 동작 VF 활성 VF: 데이터 전송 Synthetic: 대기 40 Gbps (가속) t0 VF 제거 마이그레이션 시작 VF: revoke 이벤트 Synthetic: 활성화 10 Gbps (합성) t1 합성 전용 마이그레이션 진행중 VF: 없음 Synthetic: 전송중 10 Gbps t2 VF 재할당 마이그레이션 완료 VF: offer 이벤트 Synthetic: 대기 전환 전환중 (VF probe) t3 복구 40G t4
그림 53. VF Failover 시퀀스 — VF 제거 → 합성 경로 폴백 → VF 재할당 → 가속 복귀 (애플리케이션 투명)
Accelerated Networking 상세

Azure의 Accelerated Networking은 SR-IOV를 기반으로 하드웨어 데이터 경로를 VM에 직접 노출합니다. 이를 통해 합성 경로 대비 지연 시간이 약 10배 개선되고(~100μs → ~25μs), CPU 사용률이 크게 감소합니다. D/E/F/G/M 시리즈의 대부분의 인스턴스 크기(2+ vCPU)에서 지원됩니다.

합성 경로 vs Accelerated Networking 비교
항목합성 경로 (VMBus)Accelerated Networking (VF)
데이터 경로VM → VMBus → Host vSwitch → NICVM → 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 환경 고려사항
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
hv_netvsc 디버깅 팁: VF failover 문제 진단 시 dmesg에서 "Data path switched" 메시지를 확인하세요. 합성 경로의 성능이 예상보다 낮다면 VMBus 링 버퍼 크기를 늘리고(ethtool -G), 채널 수가 vCPU 수와 일치하는지 확인하세요. Azure 직렬 콘솔(az serial-console connect)을 통해 네트워크 단절 시에도 VM에 접근할 수 있습니다.

제조사별 공통 디버깅 루틴

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

드라이버 간 종합 비교

리눅스 커널에는 수십 개의 네트워크 디바이스 드라이버가 존재하며, 각각 고유한 아키텍처 결정과 최적화 전략을 채택하고 있습니다. 이 섹션에서는 주요 드라이버를 기능, 아키텍처, 큐 모델, 인터럽트 처리, 메모리 관리(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)
기능 지원 vs 성숙도: 표에서 ✓로 표시된 기능이라도 드라이버마다 구현 성숙도가 다릅니다. 예를 들어 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)
범위 면책 조항 — tc flower offload: mlx5e는 200개 이상의 매치 필드(VLAN, MPLS, TCP 플래그, IP TOS 등)를 tc flower offload로 지원하는 반면, ena는 tc flower offload 지원이 전혀 없습니다. 같은 "지원"이라도 실제 적용 범위가 크게 다를 수 있으므로, 프로덕션 배포 전에 반드시 대상 드라이버의 구체적인 offload 범위를 tc filter show dev <intf> ingress와 커널 소스에서 검증하십시오.

아키텍처 패턴 비교

네트워크 드라이버의 아키텍처는 크게 중앙 집중형(Centralized)분산형(Distributed)으로 분류할 수 있습니다. 중앙 집중형은 펌웨어 컨트롤러가 패킷 처리 파이프라인 전체를 관장하며, 드라이버는 펌웨어에 명령을 전달하는 역할에 그칩니다. 반면 분산형은 하드웨어 파이프라인이 고정된 기능 블록으로 구성되고, 드라이버가 각 블록을 직접 프로그래밍합니다.

중앙 집중형 vs 분산형 아키텍처 패턴 중앙 집중형 (Firmware-Centric) bnxt_en, i40e, ena, gve 드라이버 (Thin Shim Layer) HWRM 명령 펌웨어 컨트롤러 패킷 분류 엔진 RSS/Flow Director Offload 처리 큐 관리자 DMA 엔진 / 스케줄러 NIC 하드웨어 (MAC/PHY) 분산형 (Hardware Pipeline) mlx5e, ixgbe, ice 드라이버 (Rich Control Plane) Flow Steering TC Offload eSwitch 레지스터 직접 접근 Flow Table HW Match/Action Steering Unit 최소 펌웨어 (초기화/에러 처리만) TxQ DMA RxQ DMA CQ DMA MAC/PHY ■ 펌웨어 ■ 하드웨어 블록 ■ 드라이버 소프트웨어
그림 ndd-54. 중앙 집중형(펌웨어 중심) vs 분산형(하드웨어 파이프라인) 아키텍처 비교

아키텍처 선택은 드라이버의 복잡도, 디버깅 용이성, 기능 확장성에 직접적인 영향을 미칩니다.

특성 중앙 집중형 (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 나쁜 설계"가 아니라, 각자의 제약 조건 안에서 최적화된 선택입니다.

mlx5 통합 CQ 모델 — RDMA heritage
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 지원) */
    ...
};
bnxt HWRM 선택 이유 — 프로그래머블 펌웨어
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 드라이버 코드는 상대적으로 작고 단순하며, 복잡한 하드웨어 로직은 펌웨어에 위임됩니다.
ice AdminQ 분리 이유 — IPU/DPU 수렴 전략
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; /* 커맨드별 파라미터 */
};
ENA NVMe-like 큐 모델 이유 — 가상화 최적화
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 vs packed ring — 캐시라인 최적화의 역사
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 활용 효율에 결정적 영향을 미칩니다. 단일 큐에서 시작하여 대칭 멀티큐, 분리 큐, 완료 큐 독립 모델로 진화해 왔습니다.

네트워크 드라이버 큐 모델 분류 체계 큐 모델 (Queue Model) 단일 큐 1 TxQ + 1 RxQ e1000e, r8169 대칭 멀티큐 N×(TxQ + RxQ) 대칭 igb, ixgbe, i40e 분리 큐 TxQ ≠ RxQ 비대칭 idpf, ice (Gen3) CQ 분리형 독립 Completion Queue bnxt, mlx5e 특징 • 단일 NAPI 인스턴스 • 글로벌 Tx 락 필요 • CPU 확장 불가 • 1Gbps 이하 전용 특징 • CPU당 TxQ+RxQ 쌍 • RSS로 RxQ 분산 • NAPI per queue-pair • 10~40Gbps 적합 특징 • Tx/Rx 큐 수 독립 조절 • Tx Completion 큐 분리 • Flow-based 스케줄링 • 100Gbps+ 최적화 특징 • CQ가 Tx/Rx 완료 통합 • CQ → EQ 이벤트 전달 • 유연한 인터럽트 매핑 • RDMA 통합 최적 성능 확장성 증가 방향 → 단순 · 저속 복잡 · 고속
그림 ndd-55. 네트워크 드라이버 큐 모델 분류 체계 — 단일 큐에서 CQ 분리형까지의 진화

링 구조와 디스크립터 포맷도 드라이버마다 상이합니다.

드라이버 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 벡터 할당 전략은 드라이버 성능에 큰 영향을 미칩니다. 관리용 벡터와 데이터 큐 벡터의 분리 방식, 공유 여부가 드라이버마다 다릅니다.

MSI-X 벡터 할당 전략 비교 Per-Queue 독립 벡터 ixgbe, igb, ice MSI-X Vec 0 ← Admin/Misc MSI-X Vec 1 → TxQ0+RxQ0 MSI-X Vec 2 → TxQ1+RxQ1 MSI-X Vec 3 → TxQ2+RxQ2 ... MSI-X Vec N → TxQN+RxQN ✓ 간단한 IRQ affinity ✓ 큐 쌍 단위 NAPI ✗ 벡터 수 = 큐 수 + 1 ✗ 벡터 많이 소비 공유 벡터 모델 cxgb4, vmxnet3 MSI-X Vec 0 ← Non-data MSI-X Vec 1 RxQ0 RxQ1 MSI-X Vec 2 RxQ2 RxQ3 MSI-X Vec 3 TxQ 전체 공유 ✓ 적은 벡터로 운용 ✓ VM 환경 벡터 제한 대응 ✗ 큐 간 인터럽트 간섭 ✗ CPU affinity 세밀 조정 불가 EQ/CQ 기반 분리 mlx5e, bnxt_en MSI-X Vec 0 ← Async Events MSI-X Vec 1 ← Command CQ EQ 0 → CQ0(Tx) + CQ1(Rx) EQ 1 → CQ2(Tx) + CQ3(Rx) EQ N → CQ_N(Tx+Rx) ✓ 유연한 CQ→EQ 매핑 ✓ RDMA/Storage CQ 공유 가능 ✓ 관리/데이터 완전 분리 ✗ 초기 설정 복잡도 높음
그림 ndd-56. MSI-X 벡터 할당 전략 비교 — Per-Queue 독립, 공유 벡터, EQ/CQ 기반 분리

인터럽트 코얼레싱(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 고정 고정
DIM 프레임워크: 커널 5.x 이후 net/core/dim/에 통합된 DIM(Dynamically-tuned Interrupt Moderation) 프레임워크는 패킷 수와 바이트 수를 기반으로 인터럽트 간격을 자동 조정합니다. ice, mlx5e, bnxt_en 등 최신 드라이버는 모두 이 공통 프레임워크를 활용하며, ethtool -C <dev> adaptive-rx on으로 활성화합니다.

메모리 모델 비교

DMA 매핑 수명주기와 버퍼 할당 전략은 드라이버 성능의 핵심 결정 요소입니다. 패킷당 매핑/해제 방식에서 사전 매핑 풀 방식으로 진화하면서 CPU 오버헤드가 크게 감소했습니다.

DMA 매핑 수명주기 비교 alloc dma_map RX 사용 dma_unmap free/recycle 패킷당 (r8169) alloc map HW unmap free 매 패킷 사전매핑 (ixgbe) init 시 alloc + map (1회) HW sync 재사용 (unmap 없이) init 1회 page_pool (mlx5e 등) pool alloc+map 풀에서 가져옴 HW skb build page_pool 자동 재활용 (DMA 매핑 유지) CPU 비용 높은 작업 경량/캐시 친화적 작업 → 재활용 경로 (CPU 비용 최소)
그림 ndd-57. DMA 매핑 수명주기 비교 — 패킷당 map/unmap, 사전 매핑, page_pool 재활용

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 재활용
MPWQE (Multi-Packet WQE): 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 (클라우드 관리) 클라우드 프로바이더 의존
펌웨어 장애 시 영향: Heavy/Critical 레벨 드라이버에서 펌웨어 크래시가 발생하면 전체 NIC가 비응답 상태가 될 수 있습니다. 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 전체 NIC 리셋 모든 PF/VF 중단 복구 시간: 5~30초 r8169, tg3, vmxnet3 PF Reset Physical Function 리셋 해당 PF의 VF만 영향 복구 시간: 2~5초 ice, i40e, bnxt_en VF Reset Virtual Function 리셋 단일 VF만 영향 복구 시간: <1초 mlx5e, ice, bnxt_en Queue Reset 개별 큐 리셋 트래픽 중단 최소 복구 시간: <100ms mlx5e, ice (Gen3) 세분화 수준 증가 → 영향 범위 감소 → 복구 시간 단축 드라이버별 지원 범위 mlx5e Global → PF → VF → Queue (전체 지원) ice Global → PF → VF → Queue (Gen3 이후) bnxt_en Global → PF → VF (큐 단위 미지원) i40e Global → PF (VF 리셋 제한적) r8169 Global만 지원 ena Device Reset (하이퍼바이저 제어)
그림 ndd-58. 리셋 세분화 수준 및 드라이버별 지원 범위
드라이버 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'
벤치마크 파이프라인 구성 Traffic Generator (TRex / pktgen / MoonGen) CPU pinning: isolcpus=4-7 IRQ affinity: CPU 4 NIC: 100GbE port 0 TX rate 측정점 패킷 전송 latency probe DUT (Device Under Test) 커널 + NIC 드라이버 perf/bpftrace 계측 IRQ affinity: CPU 0-3 RX rate 측정점 반환/포워딩 Traffic Sink (또는 루프백) RX 확인 / 드롭 카운트 latency timestamp 비교 수신 확인점 측정 포인트 latency probe 패킷 흐름 * CPU 고정(isolcpus)과 IRQ affinity 설정은 재현성 확보의 핵심
그림 ndd-65. 벤치마크 파이프라인: Traffic Generator → DUT → Traffic Sink 구성 및 측정 포인트

드라이버별 코얼레싱 전략

인터럽트 코얼레싱은 지연 시간(latency)과 처리량(throughput) 사이의 근본적 트레이드오프입니다. 코얼레싱 간격이 길수록 인터럽트 오버헤드가 줄어 throughput이 향상되지만, 패킷 처리 지연이 증가합니다.

코얼레싱 — Latency vs Throughput 트레이드오프 코얼레싱 간격 (µs) → 성능 지표 0 8 20 50 100 250 500 Throughput Latency IRQ/s mlx5e (8µs) bnxt (16µs) ixgbe (20µs) ice (50µs) 범용 최적 구간 저지연 워크로드 고처리량 워크로드 ● Throughput (높을수록 좋음) ● Latency (낮을수록 좋음) - - IRQ/s (CPU 부하 지표)
그림 ndd-59. 코얼레싱 간격에 따른 Latency/Throughput/IRQ 트레이드오프 및 주요 드라이버 기본 설정 위치

주요 드라이버의 코얼레싱 파라미터를 비교합니다.

드라이버 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 동작 모드별 최적 코얼레싱 전략:
  • 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, 기본값 최적화됨
버퍼블로트 진단 결정 트리 높은 RTT / 지연 급증 감지 ethtool -S: tx_queue_stopped > 0? Yes 링 레벨 bufferbloat TX 링 축소 또는 BQL limit_max 감소 No tc -s qdisc: backlog 지속 증가? Yes qdisc 레벨 bufferbloat fq_codel / cake qdisc로 교체 No BQL limit 값이 과다하게 큰가? Yes BQL 미적용 문제 limit_max 감소 (10240~65536 bytes) No 애플리케이션 수준 ss -ti로 RTT/cwnd 분석 SO_SNDBUF 축소 검토 진단 도구 요약 링 레벨: ethtool -S | grep tx_queue qdisc 레벨: tc -s qdisc show dev eth0 BQL: /sys/.../byte_queue_limits/ 애플리케이션: ss -ti dst <peer> 자동 완화: tc qdisc add fq_codel 또는 cake (CAKE AQM) 추적: bpftrace TX sojourn time ndd-66 레시피 참조
그림 ndd-66. 버퍼블로트 진단 결정 트리: 링 레벨 → BQL → qdisc → 애플리케이션 순서로 점검

NUMA 친화성

멀티소켓 시스템에서 NIC의 NUMA 노드와 CPU/메모리 배치가 일치하지 않으면 크로스-노드 메모리 접근으로 인한 심각한 성능 저하가 발생합니다. 올바른 NUMA 배치는 10~30%의 성능 차이를 만들어냅니다.

2-소켓 시스템 NUMA 큐-CPU-메모리 최적 배치 NUMA Node 0 (Socket 0) CPU 코어 C0 C1 C2 C3 ... C15 로컬 메모리 (DDR4) L3 캐시 (공유) NIC (PCIe) NUMA Node 0 소속 IRQ→큐 매핑 (최적): IRQ0→Q0→C0 | IRQ1→Q1→C1 | IRQ2→Q2→C2 | IRQ3→Q3→C3 ... 로컬 DMA (최적) ✓ 최적: 동일 NUMA 노드 배치 NUMA Node 1 (Socket 1) CPU 코어 C16 C17 C18 C19 ... C31 로컬 메모리 (DDR4) L3 캐시 (공유) 크로스-NUMA DMA (비최적, +40~80ns) 잘못된 IRQ→큐 매핑: IRQ0→Q0→C16 | IRQ1→Q1→C17 (원격 노드 CPU!) ✗ 비최적: 크로스-NUMA 배치 QPI/UPI
그림 ndd-60. 2-소켓 NUMA 시스템에서의 큐-CPU-메모리 배치 최적화와 크로스-NUMA 영향
# 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_DROP 성능 비교 (단일 코어, 64B 패킷) 100GbE NIC 기준, 상대적 비교 (Mpps) Mpps (백만 패킷/초) → 0 5 10 15 20 25 mlx5e ~24 ice ~22 bnxt_en ~18 i40e ~14 ixgbe ~12 ena ~5 virtio_net ~3 atlantic ~8 mvpp2 ~4 generic XDP ~2 ※ 실측값은 CPU 클럭, 메모리 대역폭, 펌웨어 버전에 따라 변동됩니다
그림 ndd-61. 드라이버별 XDP_DROP PPS 비교 (단일 코어, 64B 패킷, 100GbE 기준 상대 비교)
드라이버 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
Generic vs Native XDP 성능 차이: Generic 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 지원
XDP_TX 경로: same-queue vs cross-queue 차이:
  • 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;
}
XDP 멀티버퍼 성능 주의사항:
  • fragment 순회 비용: nr_frags가 클수록 루프 반복 증가 — 일반적으로 nr_frags ≤ 5 권장
  • bpf_xdp_load_bytes()는 경계 검사를 포함하므로 단순 포인터 접근보다 느림
  • 멀티버퍼 XDP_REDIRECT는 각 fragment의 page_pool ref를 개별 처리 — redirect 비용이 단일 버퍼 대비 nr_frags배
  • 드라이버 지원 여부 확인: ethtool -k eth0 | grep scatter
XDP_REDIRECT: copy 경로 vs zero-copy reference 경로 Copy 경로 (ixgbe, virtio_net, i40e) Zero-copy Reference 경로 (mlx5e, ice, bnxt) Driver kmalloc 패킷 수신 버퍼 할당 XDP BPF 실행 XDP_REDIRECT 결정 memcpy() 새 버퍼로 복사 (+20~40ns) 목적지 NIC 전송 복사된 버퍼 사용 총 오버헤드: kmalloc + memcpy + kfree = ~50~80ns/pkt page_pool 할당 pre-allocated page 재사용 XDP BPF 실행 XDP_REDIRECT 결정 page ref++ (no copy) 포인터만 전달 (~2ns) 목적지 NIC 전송 동일 page DMA 총 오버헤드: ref_count 조작 + DMA 재설정 = ~5~10ns/pkt devmap Bulk Flush 최적화 (양쪽 경로 공통) XDP_REDIRECT + BPF_MAP_TYPE_DEVMAP: 32~64개 패킷을 배치로 묶어 단일 ndo_xdp_xmit() 호출 → NIC 제출 시스템콜 오버헤드 대폭 감소 | mlx5e: bulk=32, ice: bulk=64 copy 경로 zero-copy reference 경로
그림 ndd-67. XDP_REDIRECT copy 경로 vs zero-copy reference 경로 비교 및 devmap bulk flush 최적화

드라이버별 커널 트레이싱 레시피 통합

아래 표는 주요 드라이버별로 즉시 사용 가능한 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
ftrace 원라이너 공통 설정 절차:
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 큐 수 조정
AF_XDP Zero-Copy 요구사항: Zero-Copy를 사용하려면 (1) 드라이버 ZC 지원, (2) XDP 프로그램이 해당 인터페이스에 로드되어 있어야 하며, (3) UMEM 영역이 hugepage로 할당되면 성능이 추가 향상됩니다. mlx5eice는 가장 성숙한 ZC 구현을 제공합니다.

패킷 처리 한계

네트워크 드라이버의 이론적 최대 PPS(Packets Per Second)는 링크 속도와 패킷 크기에 의해 결정되지만, 실제 달성 가능한 PPS는 드라이버 효율성, CPU 클럭, 메모리 대역폭에 의해 제한됩니다.

패킷 크기별 이론적 최대 PPS vs 실측 PPS 링크 속도별 비교 (Ethernet 프레이밍 포함) 패킷 크기 10 GbE 25 GbE 40 GbE 100 GbE 400 GbE 64B (최소 프레임) 14.88 Mpps 실측: ~12 37.20 Mpps 실측: ~25 59.52 Mpps 실측: ~35 148.81 Mpps 실측: ~80 595.24 Mpps 실측: ~150 512B 2.35 Mpps 5.87 Mpps 9.39 Mpps 23.49 Mpps 93.98 Mpps 1518B (MTU 1500) 0.81 Mpps 실측: ~0.8 2.03 Mpps 실측: ~2.0 3.25 Mpps 실측: ~3.2 8.12 Mpps 실측: ~8.0 32.47 Mpps 실측: ~30 핵심 병목 분석 64B: CPU cycles/packet이 병목 (작은 패킷 → 높은 인터럽트 빈도, 디스크립터 처리 오버헤드) 1518B: 링크 대역폭이 병목 (큰 패킷 → 이론치에 근접, CPU 오버헤드 상대적으로 미미) 이론치 대비 실측 달성률 64B (100G) ~54% (CPU 병목) 512B (100G) ~80% 1518B (100G) ~98% (링크 병목)
그림 ndd-62. 패킷 크기별 이론적 최대 PPS vs 실측 달성률 (속도별 비교)

병목 지점을 식별하는 방법론입니다.

병목 유형 증상 진단 방법 해결 전략
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 전용)
cycles/packet 측정 방법: 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로 직접 접근합니다.

커널 네트워크 스택 vs DPDK PMD 데이터 경로 커널 네트워크 스택 경로 Application (socket) System Call (syscall) TCP/IP Stack netfilter / tc net_device (NAPI) 커널 드라이버 (IRQ) NIC Hardware 7단계, 인터럽트+컨텍스트 스위치 vs DPDK PMD 경로 Application (rte_mbuf) rte_eth_rx/tx_burst() PMD (polling, no IRQ) NIC Hardware 3단계, 폴링 전용, zero-copy 커널 우회! No IRQ!

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/MSIMSI-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
VFIO vs UIO 보안 차이: 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의 데이터 경로에 접근합니다.

전통적 바인딩 vs Bifurcated 드라이버 모델 전통적 바인딩 (ice, ixgbe 등) DPDK App ethtool ✗ ip ✗ PMD (net_ice) vfio-pci 커널 드라이버 언바인드됨 1 커널 드라이버 unbind 2 vfio-pci bind 3 DPDK 앱 실행 NIC Hardware Bifurcated 모델 (mlx5) DPDK App ethtool ✓ ip ✓ PMD (net_mlx5) mlx5_core (커널) 디바이스 언바인드 불필요 커널 관리 도구 계속 사용 가능 RDMA + DPDK 동시 사용 SR-IOV VF도 bifurcated 지원 NIC Hardware (동시 접근)
항목전통적 바인딩 (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 모두 가능
운영 복잡도높음 — 바인딩 스크립트, 복구 절차 필요낮음 — 표준 커널 운영 워크플로우 유지
Bifurcated 모델이 프로덕션에서 선호되는 이유: 통신사/클라우드 운영 환경에서는 DPDK 성능뿐 아니라 모니터링(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, 방화벽, 로드밸런서 등의 성능에 직결됩니다.

기능mlx5icei40ebnxtixgbe
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)
최대 룰 수수백만16K8K~10K32K
동적 룰 추가/삭제μs 단위ms 단위ms 단위ms 단위ms 단위
OVS-DPDK 환경 드라이버 호환성: OVS-DPDK에서 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배의 처리율을 달성합니다.

DPDK vs 커널 드라이버 패킷 처리 성능 (64B, single core, Mpps) 45 35 25 15 5 Mpps (백만 패킷/초) 15 40+ mlx5 ×2.7 12 35 ice ×2.9 10 30 i40e ×3.0 10 28 bnxt ×2.8 8 14.88 ixgbe line rate! 2 8 virtio ×4.0 커널 드라이버 DPDK PMD Line Rate

testpmd 벤치마크 성능 수치

드라이버NIC 모델속도커널 (Mpps)DPDK (Mpps)향상 배율비고
mlx5ConnectX-6 Dx100G~15~40+~2.7×bifurcated, 멀티코어 시 100+ Mpps
iceE810-C100G~12~35~2.9×DDP 최적화 시 추가 향상
i40eX710-DA440G~10~30~3.0×가장 안정적인 DPDK PMD
bnxtBCM57508100G~10~28~2.8×TruFlow 오프로드 시 추가 향상
ixgbeX520-DA210G~8~14.88~1.86×10G 물리 한계(line rate) 달성
virtiovhost-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 PMDAF_XDP (유저스페이스 XDP)커널 XDP
데이터 경로유저스페이스, 커널 완전 우회유저스페이스, 커널 XDP 경유커널 내부 (드라이버 레벨)
성능 (64B)★★★★★ (40+ Mpps)★★★★☆ (20~25 Mpps)★★★★☆ (25~35 Mpps DROP)
커널 기능 유지✗ (별도 바인딩 필요, bifurcated 제외)✓ (커널 드라이버 유지)✓ (커널 드라이버 유지)
프로그래밍 모델rte_* API (독자 생태계)AF_XDP 소켓 + libxdpBPF/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-kernelOVS-DPDKOVS-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 offloadtc-flower (제한적)rte_flow 기반rte_flow + CT offload

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

  1. 1단계: 최소 송수신 경로ndo_open/stop/start_xmit, 단일 NAPI queue, 기본 IRQ 동작
  2. 2단계: 안정성 확보 — 에러 경로 정리, queue stop/wake 일관성, teardown 순서 검증
  3. 3단계: 운영성 확보ethtool_ops, 통계, self-test, 링 파라미터 조정
  4. 4단계: 성능 확장 — 멀티큐 RSS, XDP/AF_XDP, page_pool, BQL, NUMA affinity
  5. 5단계: 가상 netdev 통합 — TUN/TAP, veth, virtio-net과 공통 코어 재사용 전략 수립
Now I have a good understanding of the style. Let me produce the four sections.

상태 관리: 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)로 연기하기 때문입니다.

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_PENDINGlinkwatch 이벤트가 워크큐에 대기 중직접 조회 불필요
__LINK_STATE_DORMANT802.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_UPboolRTNL 또는 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();
}
BQL 연동: 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 모드에 머무릅니다.
UNREGISTERED alloc_etherdev 직후 register_netdev REGISTERED PRESENT=1, START=0 ndo_open RUNNING (UP) START=1, NOCARRIER=1 carrier_on RUNNING + CARRIER START=1, NOCARRIER=0 carrier_off NO CARRIER START=1, NOCARRIER=1 ndo_stop STOPPED START=0 DORMANT 802.1X 인증 대기 netif_dormant_on DETACHED PRESENT=0 (AER/suspend) device_detach
net_device 상태 전이: register → open → carrier on/off → stop → detach/dormant 경로를 포함합니다.

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_CSUMIPv4 TCP/UDP 하드웨어 체크섬 계산Checksum
NETIF_F_IPV6_CSUMIPv6 TCP/UDP 하드웨어 체크섬 계산Checksum
NETIF_F_HW_CSUM프로토콜 무관 완전 하드웨어 체크섬 (L3/L4 구분 없이)Checksum
NETIF_F_RXCSUM수신 체크섬 오프로드 (HW가 검증 완료 표시)Checksum

세그멘테이션(Segmentation) 오프로드

플래그설명카테고리
NETIF_F_TSOTCP Segmentation Offload (IPv4)Segmentation
NETIF_F_TSO6TSO IPv6Segmentation
NETIF_F_TSO_ECNECN 지원 TSOSegmentation
NETIF_F_GSOGeneric Segmentation Offload (소프트웨어 fallback 포함)Segmentation
NETIF_F_GSO_GREGRE 터널 GSOSegmentation
NETIF_F_GSO_UDP_TUNNELUDP 터널(VXLAN, Geneve) GSOSegmentation

Scatter/Gather 및 DMA

플래그설명카테고리
NETIF_F_SGScatter/Gather I/O (비연속 메모리 전송)SG/DMA
NETIF_F_FRAGLISTFragment list 지원SG/DMA
NETIF_F_HIGHDMAHigh memory 영역 DMA 가능SG/DMA

수신(Receive) 오프로드

플래그설명카테고리
NETIF_F_GROGeneric Receive Offload (소프트웨어 패킷 집계)Receive
NETIF_F_GRO_HW하드웨어 GRO (NIC이 직접 coalescing)Receive
NETIF_F_LROLarge Receive Offload (라우팅 환경 비권장)Receive

VLAN 오프로드

플래그설명카테고리
NETIF_F_HW_VLAN_CTAG_TXVLAN C-Tag 하드웨어 삽입VLAN
NETIF_F_HW_VLAN_CTAG_RXVLAN C-Tag 하드웨어 추출VLAN
NETIF_F_HW_VLAN_CTAG_FILTERVLAN 하드웨어 필터링VLAN

기타 기능

플래그설명카테고리
NETIF_F_LLTXLockless TX (드라이버 자체 동기화 보장)TX
NETIF_F_LOOPBACK루프백(Loopback) feature기타
NETIF_F_NTUPLEN-tuple 필터 (Flow Director)필터
NETIF_F_RXHASHRX 해시 제공 (RSS 연동)수신
NETIF_F_HW_TCTC 오프로드 지원QoS

feature 필드 간 관계

net_device에는 feature와 관련된 5개의 필드가 있으며, 각 필드의 역할과 상호 관계를 정확히 이해해야 합니다.

필드의미설정 주체
dev->hw_features하드웨어가 지원하는 전체 feature set드라이버 probe 시 설정
dev->features현재 활성 feature (hw_features의 부분집합)커널 코어가 계산
dev->wanted_features사용자가 ethtool -K로 요청한 featureethtool 명령으로 설정
dev->vlan_featuresVLAN 하위 디바이스에 전파할 feature드라이버 probe 시 설정
dev->hw_enc_features터널(Tunnel) encapsulation에 사용할 feature드라이버 probe 시 설정

netdev_update_features() 내부 흐름

ethtool -K wanted_features 갱신 netdev_update_features() features = hw_features & wanted_features ndo_fix_features() HW 제약 반영·의존성 강제 변경 있는가? features != dev->features Yes ndo_set_features() HW 레지스터 반영 netdev_features_change() NETDEV_FEAT_CHANGE notifier VLAN 하위 디바이스 feature 전파 vlan_features 기반 재계산 No → 변경 없음, 종료
ethtool -K 명령부터 ndo_set_features → NETDEV_FEAT_CHANGE notifier까지의 feature 업데이트 흐름입니다.

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 출력과 필드 매핑: 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_UPndo_open() 성공 후IP 스택 (주소 활성화), 라우팅 (경로 추가)
NETDEV_DOWNndo_stop() 호출 전IP 스택 (주소 비활성화), 라우팅 (경로 제거)
NETDEV_REGISTERregister_netdevice() 완료sysfs, procfs (디바이스 노드 생성)
NETDEV_UNREGISTERunregister_netdevice() 시작모든 참조자 (참조 정리 시작)
NETDEV_CHANGENAME인터페이스 이름 변경udev, sysfs (심볼릭 링크 갱신)
NETDEV_CHANGEADDRMAC 주소 변경ARP, NDP (이웃 캐시 갱신)
NETDEV_CHANGEMTUMTU 변경IPv6 (MTU 업데이트), PMTUD
NETDEV_CHANGEcarrier/operstate 변경라우팅 (경로 가중치 업데이트)
NETDEV_FEAT_CHANGEfeature 변경VLAN 하위 디바이스 (feature 재전파)
NETDEV_PRE_UPndo_open() 직전보안 모듈, 정책 검사 (open 거부 가능)
NETDEV_GOING_DOWNndo_stop() 직전연결 종료 시작 신호
NETDEV_CHANGE_TX_QUEUE_LENTX 큐 길이 변경qdisc (큐 재구성)
NETDEV_BONDING_FAILOVERbonding failover 발생ARP (GARP 전송), 라우팅
NETDEV_JOINbonding/team 합류bonding 매니저
NETDEV_RESEND_IGMPIGMP 재전송 요청멀티캐스트(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;
}
주의: notifier 콜백은 blocking notifier이므로 sleep이 가능하지만, 작업 시간을 최소화해야 합니다. 콜백이 오래 걸리면 같은 체인의 다른 수신자가 지연되고, RTNL lock 보유 시간이 늘어나 네트워크 설정 변경 전체가 지연됩니다. 무거운 작업은 워크큐로 연기하세요.
ip link set up dev_open() call_netdevice_notifiers (NETDEV_UP, dev) raw_notifier_call_chain IPv4/IPv6 스택 주소 활성화, RA 시작 라우팅 서브시스템 경로 추가/갱신 브릿지/bonding 포트 상태 갱신 netfilter/nftables 규칙 재평가 → NOTIFY_DONE → NOTIFY_OK → NOTIFY_DONE → NOTIFY_DONE
NETDEV_UP 이벤트가 발생하면 등록된 모든 서브시스템 콜백이 순차적으로 호출됩니다.

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_dbgCONFIG_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(skb) 1. ptype_all (packet taps) tcpdump, raw socket, AF_PACKET 2. ingress qdisc / tc BPF tc filter, cls_bpf, redirect 3. rx_handler bridge, OVS, macvlan, bonding 4. vlan_do_receive() VLAN tag 처리, VLAN 하위 디바이스 전달 5. ptype_base (프로토콜 핸들러) ip_rcv, ipv6_rcv, arp_rcv 6. deliver_skb() 최종 프로토콜 핸들러에 전달 사본 전달 (원본은 계속 진행) TC_ACT_STOLEN 시 여기서 소비 RX_HANDLER_CONSUMED 시 종료 VLAN dev 있으면 dev 교체 protocol 기반 해시 매칭
__netif_receive_skb_core() 내부 처리 단계: packet taps → ingress tc → rx_handler → VLAN → 프로토콜 핸들러 순서로 진행됩니다.
/* 커널 소스 분석: __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 쌍의 한쪽을 컨테이너에 할당하는 것이 일반적인 패턴입니다.
권장 학습 순서: 네트워크 스택Network Device 드라이버 (net_device)TUN/TAPBPF/XDP 순서로 보면 드라이버-스택-가속 경로가 자연스럽게 연결됩니다.