네트워크 디바이스 드라이버 (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는 리눅스 네트워크 스택에서 가장 핵심적인 구조체(Struct)로, 하나의 네트워크 인터페이스가 가져야 할 모든 상태 정보를 담고 있습니다. 최근 6.x 계열에서 이 구조체의 크기는 대략 2KB 안팎이며, 수십 개의 필드가 캐시 라인(Cache Line) 최적화(Cache Line Optimization)를 고려하여 배치되어 있습니다. 특히 핫 패스(Hot Path)에서 자주 접근하는 필드들은 ____cacheline_aligned_in_smp 어노테이션(Annotation)을 통해 동일한 캐시 라인에 모이도록 정렬됩니다. 이는 SMP 환경에서 캐시(Cache) 바운싱(Cache Bouncing)을 최소화하고, 패킷(Packet) 송수신 경로의 지연 시간(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()로 할당, 네임스페이스(Namespace) 내 유일
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() 매크로(Macro)로 접근
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 큐 필드도 마찬가지로 별도 캐시 라인에 정렬되어 수신 경로와 송신 경로 간 캐시 경합(Contention)을 방지합니다.

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 브로드캐스트 주소 유효 드라이버 등록(Driver Registration) 시 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(), 멀티캐스트 라우팅(Routing) 시
IFF_NOARP 1<<7 ARP 프로토콜 사용 안 함 루프백, 터널(Tunnel) 등에서 설정
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 순서는 패킷 손실과 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, 해시 테이블(Hash Table), 알림 체인(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()은 물리 메모리(Physical Memory)가 단편화(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()를 감싸는 다양한 편의 매크로를 제공합니다.

매크로확장 결과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별 큐 매핑(Mapping)이 가능해져 lock 경합을 줄일 수 있습니다.

register_netdev() / register_netdevice() 내부

register_netdev()는 RTNL 잠금을 자동으로 관리하는 편의 함수이며, 실제 등록 로직은 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)을 벗어나면 포인터가 무효화(Invalidation)될 수 있습니다.

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) 연산(Atomic Operation)이므로 핫 경로(Hot Path)에서는 오버헤드(Overhead)가 됩니다.

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 계열에서 수십 개의 콜백 함수 포인터(Callback Function Pointer)를 정의하는 거대한 구조체입니다. 네트워크 드라이버의 모든 동작 — 장치 수명주기, 데이터 전송, 주소 필터링, 오프로드 설정, 가상화(Virtualization) 지원 — 이 이 구조체를 통해 커널 네트워크 스택과 계약(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 레지스터(Register)에 적용선택
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 전체 덤프(Dump) 요청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 핸들러(Handler)가 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 상태로 호출됩니다. 따라서 슬립(Sleep)이 불가능하며, 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()의 기본 해시(Hash) 기반 큐 선택. 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 정지가 없으므로 생략 가능합니다.

상태 관리: 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 비트맵으로, 디바이스의 다양한 링크 상태를 원자적으로 관리합니다.

비트의미조회 매크로
__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()참조 카운트 기반으로 동작합니다. 여러 소비자(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터널 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 알림 체인은 네트워크 디바이스의 상태 변경 이벤트를 관심 있는 서브시스템에 전달하는 옵저버 패턴(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 재전송(Retransmission) 요청멀티캐스트(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 이벤트가 발생하면 등록된 모든 서브시스템 콜(System Call)백이 순차적으로 호출됩니다.

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가 프로토콜 핸들러에 전달되기까지의 전체 처리 파이프라인(Pipeline)입니다. 각 단계에서 패킷이 가로채지거나 소비될 수 있습니다.

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 쌍의 한쪽을 컨테이너에 할당하는 것이 일반적인 패턴입니다.

net_device 최신 변화 (v6.8~v6.15)

net_device 인프라는 v6.8 이후 RTNL 잠금 세분화, netdev-genl 내성 확장, Byte Queue Limits(BQL) 저변 확대, PHY link topology 도입, Threaded NAPI 기본화 방향으로 진화하고 있습니다.

per-netns RTNL (v6.13, 실험적)

v6.13에서 RTNL 잠금의 범위를 per-netns로 나누는 대규모 리팩터링이 시작되었습니다. 컨테이너 밀집 환경에서 rtnl_lock 경합이 주요 병목이었기에, 이 작업은 컨테이너 orchestrator의 네임스페이스 생성/삭제 지연을 크게 줄일 잠재력이 있습니다.

주의: 프로덕션에서는 기본값(전역 RTNL) 유지 권장. 실험적 플래그로 테스트 후 v6.16+ 정식 기본화 시점을 주시하세요.

netdev-genl의 큐/NAPI/Page Pool 내성 (v6.8~v6.9)

v6.8에서 netdev-genl에 큐(queue)와 NAPI 정보가 추가되었고, v6.9에서 per-queue 통계가 추가되었습니다.

# ynl-py로 NAPI, 큐, page_pool 속성 조회
ynl --spec netdev.yaml --do napi-get --json '{}'
ynl --spec netdev.yaml --do queue-get --json '{"ifindex": 2}'
ynl --spec netdev.yaml --do page-pool-get --json '{}'

PHY link topology 추적 (v6.12)

v6.12에서 PHY link topology 인프라가 추가되어 하나의 net_device에 연결된 여러 PHY(예: QSFP 멀티-포트, switchdev PCS, PSE 등)의 위상을 커널이 공식적으로 추적합니다. ETHTOOL_MSG_PHY_GET을 통해 phyindex가 유저스페이스로 노출됩니다.

NAPI suspension + threaded 기본화 (v6.13)

v6.13의 NAPI suspension은 net_device_ops가 아닌 NAPI 인스턴스 레벨에서 busy poll/interrupt 전환을 자동화합니다. 초기 지원 드라이버는 mlx4, mlx5, bnxt, ice이며, 드라이버는 새 NAPI config API를 채택하는 것이 권장됩니다.

Device Memory TCP 전송/수신 (v6.12~v6.16)

기타 주요 변화

net_device 최신 변화 (v6.16~v6.19)

v6.16부터 net_device 드라이버 생태계는 RTNL 잠금 해제 완성, devmem TCP TX 도입, OpenVPN DCO 커널 내장, DCCP 제거, 그리고 대규모 전송 성능 개선이 이루어졌습니다.

Device Memory TCP TX 본격 도입 (v6.16)

v6.16에서 devmem TCP TX 경로가 병합되어 GPU vRAM 등의 디바이스 메모리에서 직접 TCP 데이터를 전송하는 제로-카피(zero-copy) 경로가 완성되었습니다. v6.12에서 RX가 먼저 도입된 데 이어, TX까지 통합되어 AI/HPC 워크로드에서 큰 의미를 가집니다.

커널 6.16부터: devmem TCP TX — GPU vRAM 등 디바이스 메모리의 데이터를 커널 메모리를 거치지 않고 네트워크로 직접 전송합니다. NIC의 헤더/데이터 분리(header/data split)와 DMA-BUF 지원이 필요합니다.

RTNL per-netns 잠금 해제 진전 (v6.16)

v6.13부터 시작된 RTNL per-netns 리팩터링이 v6.16에서 큰 진전을 이루었습니다. IPv6 라우팅 테이블(Routing Table) 관리가 RTNL 범위 밖으로 이전되어 원격 제어 경로(remote control path) 속도가 약 3배 향상되었습니다. 컨테이너 환경에서 네트워크 네임스페이스 생성/삭제 지연이 크게 줄어듭니다.

커널 6.16부터: IPv6 라우팅 테이블이 RTNL 범위 밖에서 관리되어 멀티-네임스페이스 환경에서의 경합이 대폭 감소합니다.

OpenVPN DCO 드라이버 내장 (v6.16)

v6.16에서 OpenVPN 데이터 채널 오프로드(DCO, Data Channel Offload) 드라이버(ovpn)가 커널에 병합되었습니다. 기존 TUN 기반 사용자 공간 처리보다 훨씬 빠른 VPN 데이터 처리가 커널 내부에서 이루어집니다.

DCCP 프로토콜 제거 (v6.16)

Deprecated (v6.16+): DCCP(Datagram Congestion Control Protocol) 구현이 v6.16에서 커널 트리에서 제거되었습니다. DCCP 기반 net_device 드라이버나 관련 의존성이 있는 코드는 SCTP 또는 QUIC(UDP GSO 기반)으로 이전이 필요합니다.

UDP DDoS 방어 성능 향상 (v6.18)

v6.18에서 UDP 수신 경로가 DDoS 상황에서의 확장성을 위해 광범위하게 재작성되었습니다. 데이터 구조 레이아웃 최적화와 NUMA 인지(NUMA-aware) 잠금이 도입되어, 6-NUMA 노드 Intel Xeon 플랫폼에서 UDP 수신 처리량이 47% 향상되고 초당 1,420만 패킷 추가 처리가 측정되었습니다.

커널 6.18부터: UDP RX 경로의 잠금 경합(Lock Contention) 감소 및 NUMA 최적화로 DDoS 상황에서도 처리량이 크게 향상됩니다. 개선 폭은 위 6-NUMA 노드 Intel Xeon 측정 결과(47%)를 참고하되, 하드웨어·워크로드에 따라 달라질 수 있습니다. v6.18은 LTS(Long-Term Support) 커널 후보입니다.

Google PSP 기반 TCP 암호화 (v6.18)

v6.18에서 Google이 개발한 PSP(Protocol Security Protocol) 기반 TCP 연결 암호화가 커널에 병합되었습니다. kTLS와는 달리 NIC 오프로드를 통해 패킷별 고유 키를 사용하는 호스트-내부 암호화 모델로, 대규모 데이터센터 환경에서의 네트워크 암호화를 위한 새로운 방향을 제시합니다.

전송 큐잉 락-프리 최적화 (v6.19)

v6.19에서 전송 큐잉(queueing discipline) 계층의 핵심 잠금이 락-프리 리스트로 교체되어 대용량 전송 워크로드에서 약 4배의 처리량 향상이 측정되었습니다. net_device의 ndo_start_xmit() 호출 전 큐잉 단계 경합이 사라져 멀티코어 환경에서 확장성이 크게 개선됩니다.

기타 주요 변화 (v6.16~v6.19)

권장 학습 순서: 네트워크 스택Network Device 드라이버 (net_device)TUN/TAPBPF/XDP 순서로 보면 드라이버-스택-가속 경로가 자연스럽게 연결됩니다.