네트워크 디바이스 드라이버 (net_device)
Linux 네트워크 디바이스 드라이버를 고처리량 데이터 경로와 운영 안정성 관점에서 심층 정리합니다. net_device/net_device_ops 초기화, NAPI 기반 RX 폴링(Polling), TX 큐 관리와 BQL, MSI-X/IRQ affinity 최적화, checksum/TSO/GRO 등 오프로드 기능, XDP/AF_XDP 연계, ethtool 통계와 링크 상태 관리, 물리 NIC와 TUN/TAP 같은 가상 netdev 공통 모델, tracepoint/perf/bpftrace를 활용한 병목(Bottleneck) 분석까지 실전 네트워크 드라이버 개발에 필요한 핵심을 다룹니다.
핵심 요약
- net_device — 인터페이스의 공통 상태와 콜백(Callback) 진입점(Entry Point)입니다.
- net_device_ops — open/stop/xmit 등 데이터 경로 계약을 정의합니다.
- NAPI — RX 인터럽트(Interrupt) 폭풍을 줄이고 폴링 기반 처리량(Throughput)을 확보합니다.
- BQL — TX 큐 지연(latency)과 버퍼블로트 리스크를 줄입니다.
- 가상 netdev — TUN/TAP처럼 하드웨어 없이도 동일한 netdev 모델을 재사용합니다.
단계별 이해
- 수명주기 설계
할당/등록/해제 순서를 먼저 확정합니다. - RX/TX 콜백 구현
ndo_start_xmit()와 NAPI poll 루프를 정확히 연결합니다. - 운영 인터페이스 연결
ethtool_ops, 통계, 링크 상태(phylink)를 연결합니다. - 가상 netdev 확장
TUN/TAP, veth, virtio-net과 공통 패턴을 통합해 이해합니다.
개념 예시가 표시된 블록은 구조와 호출 계약 이해용이며, 실습 예제가 표시된 블록은 사용자 공간(User Space)에서 실행/검증 절차를 바로 적용할 수 있도록 구성했습니다.
개요: net_device 드라이버의 역할
struct net_device 드라이버는 커널 네트워크 스택(Network Stack)과 실제/가상 링크 계층 사이의 어댑터입니다. 유저스페이스 입장에서는 eth0, ens3, tap0 모두 동일한 netdev 인터페이스처럼 보이지만, 내부 구현은 물리 NIC/가상 디바이스에 따라 크게 달라집니다.
struct net_device 핵심 필드 상세 분석
struct net_device는 리눅스 네트워크 스택에서 가장 핵심적인 구조체(Struct)로, 하나의 네트워크 인터페이스가 가져야 할 모든 상태 정보를 담고 있습니다.
최근 6.x 계열에서 이 구조체의 크기는 대략 2KB 안팎이며, 수십 개의 필드가 캐시 라인(Cache Line) 최적화(Cache Line Optimization)를 고려하여 배치되어 있습니다.
특히 핫 패스(Hot Path)에서 자주 접근하는 필드들은 ____cacheline_aligned_in_smp 어노테이션(Annotation)을 통해
동일한 캐시 라인에 모이도록 정렬됩니다. 이는 SMP 환경에서 캐시(Cache) 바운싱(Cache Bouncing)을 최소화하고,
패킷(Packet) 송수신 경로의 지연 시간(Latency)을 줄이기 위한 의도적 설계입니다.
net_device는 include/linux/netdevice.h에 정의되며, alloc_netdev_mqs()를 통해 할당됩니다.
핵심 필드 분류
식별 및 이름
| 필드 | 타입 | 설명 | 비고 |
|---|---|---|---|
name[IFNAMSIZ] |
char[] |
인터페이스 이름 (eth0, wlan0 등) |
IFNAMSIZ = 16, dev_change_net_namespace()에서 변경 가능 |
ifindex |
int |
고유 인터페이스 인덱스(Interface Index) | dev_new_index()로 할당, 네임스페이스(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;
/* ... 그 외 다수 필드 ... */
};
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 up → dev_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.h에 IFF_* 이름으로 정의되어 있으며, 사용자 공간의 IFF_* 플래그와 네임스페이스가 다르다.
| 플래그 | 설명 | 설정 대상 |
|---|---|---|
IFF_802_1Q_VLAN |
802.1Q VLAN 디바이스 | vlan_setup()에서 설정 |
IFF_EBRIDGE |
이더넷 브릿지(Ethernet Bridge) 마스터 | br_dev_setup()에서 설정 |
IFF_BONDING |
본딩(Bonding) 마스터 | bond_setup()에서 설정 |
IFF_ISATAP |
ISATAP(Intra-Site Automatic Tunnel Addressing Protocol) 인터페이스 | IPv6 SIT 터널에서 설정 |
IFF_WAN_HDLC |
WAN HDLC 디바이스 | WAN 드라이버에서 설정 |
IFF_XMIT_DST_RELEASE |
TX 시 skb_dst 조기 해제 허용 |
대부분의 이더넷 드라이버, 메모리 절약 최적화 |
IFF_DONT_BRIDGE |
이 디바이스를 브릿지 포트로 사용 금지 | 특수 가상 디바이스에서 설정 |
IFF_TEAM |
팀(Team) 디바이스 마스터 | team_setup()에서 설정 |
IFF_SUPP_NOFCS |
FCS(Frame Check Sequence) 없이 송신 지원 | 하드웨어가 FCS 생략 가능한 NIC |
IFF_LIVE_ADDR_CHANGE |
인터페이스 활성 상태에서 MAC 주소 변경 허용 | macvlan, veth 등 |
IFF_MACVLAN |
macvlan 디바이스 | macvlan_common_setup()에서 설정 |
IFF_OPENVSWITCH |
Open vSwitch 데이터패스 포트 | OVS 내부 포트 생성 시 설정 |
IFF_TX_SKB_SHARING |
TX skb를 복사 없이 공유 허용 |
veth, loopback 등 zero-copy 최적화 |
IFF_UNICAST_FLT |
하드웨어 유니캐스트 주소 필터링 지원 | NIC 드라이버에서 설정, 무차별 모드 회피 |
IFF_LIVE_RENAME_OK |
인터페이스 활성 상태에서 이름 변경 허용 | 가상 디바이스(veth, bridge 등) |
reg_state 등록 상태 전이
reg_state는 net_device의 등록 생명주기(Registration Lifecycle)를 추적하는 상태 머신입니다.
register_netdevice()에서 NETREG_REGISTERED로 전이되고,
unregister_netdevice()를 거쳐 최종적으로 free_netdev()에서 해제됩니다.
각 상태 전이는 RTNL 잠금(Lock) 하에서만 수행됩니다.
/* 커널 소스 분석: include/linux/netdevice.h - reg_state 열거형 */
enum {
NETREG_UNINITIALIZED = 0, /* alloc_netdev() 직후 */
NETREG_REGISTERED, /* register_netdevice() 완료 */
NETREG_UNREGISTERING, /* unregister_netdevice() 호출됨 */
NETREG_UNREGISTERED, /* netdev_run_todo() 완료 */
NETREG_RELEASED, /* netdev_release() → free 가능 */
NETREG_DUMMY, /* init_dummy_netdev()용 더미 */
};
operstate (RFC 2863) 운영 상태
operstate는 RFC 2863에서 정의한 인터페이스 운영 상태를 나타냅니다.
커널은 링크 상태, 상위/하위 레이어 상태를 종합하여 이 값을 자동으로 관리하며,
linkwatch 서브시스템이 상태 변경 이벤트를 사용자 공간으로 전달합니다.
| 상태 | 값 | 의미 | 전이 조건 |
|---|---|---|---|
IF_OPER_UNKNOWN |
0 |
상태 미확인 | 초기값, 드라이버가 상태를 보고하지 않는 경우 |
IF_OPER_NOTPRESENT |
1 |
디바이스 물리적 부재 | 핫플러그(Hotplug) 제거 시 |
IF_OPER_DOWN |
2 |
인터페이스 비활성(Down) | IFF_UP 해제 또는 netif_carrier_off() |
IF_OPER_LOWERLAYERDOWN |
3 |
하위 레이어 비활성 | VLAN/bridge의 물리 포트 다운 시 |
IF_OPER_TESTING |
4 |
테스트 모드 | 루프백 테스트, 자가 진단 수행 중 |
IF_OPER_DORMANT |
5 |
대기(Dormant) 상태 | 802.1X 인증 대기, netif_dormant_on() |
IF_OPER_UP |
6 |
완전 동작 상태 | IFF_UP 설정 + netif_carrier_on() + 인증 완료 |
/* 커널 소스 분석: net/core/link_watch.c - operstate 결정 로직 */
static void rfc2863_policy(struct net_device *dev)
{
unsigned char operstate = IF_OPER_DOWN;
if (dev->flags & IFF_UP) {
if (!netif_carrier_ok(dev))
operstate = (dev->ifindex != LOOPBACK_IFINDEX)
? IF_OPER_DOWN : IF_OPER_UNKNOWN;
else if (netif_dormant(dev))
operstate = IF_OPER_DORMANT;
else
operstate = IF_OPER_UP;
}
if (dev->operstate != operstate) {
write_lock(&dev_base_lock);
dev->operstate = operstate;
write_unlock(&dev_base_lock);
}
}
struct net_device의 필드를 직접 대입하여 변경해서는 안 됩니다.
반드시 커널이 제공하는 API 함수(dev_change_flags(), dev_set_mtu(), dev_addr_set(),
netif_set_real_num_tx_queues() 등)를 사용해야 합니다.
이 함수들은 내부적으로 RTNL 잠금 확인, notifier 체인 호출, 상태 머신 전이, sysfs 동기화를 수행합니다.
직접 수정하면 커널 서브시스템 간 상태 불일치가 발생하여 패킷 손실, 교착 상태(Deadlock), 또는 커널 패닉(Kernel Panic)을 유발할 수 있습니다.
드라이버 수명주기와 필수 호출 순서
가장 흔한 실수는 등록/해제 순서를 뒤섞는 것입니다. 특히 NAPI, IRQ, queue start/stop 순서는 패킷 손실과 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 구조체 바로 뒤의 정렬된 주소를 반환합니다.
ether_setup() 내부
ether_setup()은 이더넷(Ethernet) 디바이스의 기본 속성을 설정하는 setup 콜백입니다. alloc_etherdev() 계열 매크로가 alloc_netdev_mqs()에 이 함수를 전달합니다.
/* 커널 소스 분석: net/ethernet/eth.c - ether_setup() */
void ether_setup(struct net_device *dev)
{
dev->header_ops = ð_header_ops;
dev->type = ARPHRD_ETHER; /* ARP 하드웨어 타입: 이더넷 */
dev->hard_header_len = ETH_HLEN; /* 14바이트 (DST 6 + SRC 6 + Type 2) */
dev->min_header_len = ETH_HLEN;
dev->mtu = ETH_DATA_LEN; /* 1500바이트 */
dev->min_mtu = ETH_MIN_MTU; /* 68바이트 (IPv4 최소) */
dev->max_mtu = ETH_MAX_MTU; /* 65535바이트 */
dev->addr_len = ETH_ALEN; /* 6바이트 (MAC 주소 길이) */
dev->flags = IFF_BROADCAST | IFF_MULTICAST;
dev->priv_flags |= IFF_TX_SKB_SHARING;
/* 커널 소스 분석: 브로드캐스트 주소를 FF:FF:FF:FF:FF:FF로 설정 */
eth_broadcast_addr(dev->broadcast);
/* 커널 소스 분석: eth_header_ops는 eth_header(), eth_header_parse() 등 제공
* 이더넷 헤더 생성·파싱을 표준화 */
}
setup 콜백 이후 필요한 값만 덮어쓰면 됩니다. 예를 들어 점보 프레임(Jumbo Frame)을 지원하는 NIC 드라이버는 dev->max_mtu를 9000 이상으로 재설정합니다.
편의 할당 매크로 패밀리
커널은 프로토콜별로 alloc_netdev_mqs()를 감싸는 다양한 편의 매크로를 제공합니다.
| 매크로 | 확장 결과 | TX/RX 큐 |
|---|---|---|
alloc_etherdev(sizeof_priv) |
alloc_etherdev_mqs(sizeof_priv, 1, 1) |
1 / 1 |
alloc_etherdev_mq(sizeof_priv, count) |
alloc_etherdev_mqs(sizeof_priv, count, count) |
count / count |
alloc_etherdev_mqs(sizeof_priv, txqs, rxqs) |
alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN, ether_setup, txqs, rxqs) |
txqs / rxqs |
alloc_netdev(sizeof_priv, name, assign, setup) |
alloc_netdev_mqs(sizeof_priv, name, assign, setup, 1, 1) |
1 / 1 |
alloc_candev(sizeof_priv, echo_skb_max) |
CAN 프레임워크 전용 래퍼, can_setup 콜백 사용 |
1 / 1 |
alloc_ieee80211_hw(priv_size, ops) |
무선랜(Wi-Fi) mac80211 프레임워크 전용 래퍼 |
프레임워크 결정 |
alloc_etherdev_mqs()의 txqs/rxqs를 NIC 하드웨어 큐 수와 일치시키면 CPU별 큐 매핑(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;
}
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 변형이 더 효율적입니다. 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);
/* ... 하드웨어 초기화 */
}
net_device와 같은 메모리 블록에 배치함으로써 (1) 별도 kmalloc()이 불필요하고, (2) net_device와 사설 데이터가 인접하여 CPU 캐시 지역성(Cache Locality)이 향상되며, (3) 해제 시 kvfree() 한 번으로 모두 해제됩니다. 이 패턴은 네트워크 디바이스뿐 아니라 ieee80211_hw, ib_device 등 다른 서브시스템에서도 동일하게 사용됩니다.
핵심 콜백: 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_init | register_netdevice() | RTNL held, process | 추가 초기화 (alloc_netdev 이후 보충) | 선택 |
ndo_uninit | unregister_netdevice() | RTNL held, process | init에서 할당한 자원 정리 | 선택 |
ndo_open | dev_open() — ip link set up | RTNL held, process | HW 활성화, IRQ 요청, NAPI 시작 | 필수 |
ndo_stop | dev_close() — ip link set down | RTNL held, process | HW 비활성화, IRQ 해제, NAPI 정지 | 필수 |
그룹 B — 데이터 경로(Data Path)
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_start_xmit | dev_hard_start_xmit() | softirq 또는 process, per-queue _xmit lock held | skb를 HW TX 링에 전달 | 필수 |
ndo_select_queue | netdev_pick_tx() | process/softirq | TX 큐 인덱스 선택 (멀티큐) | 선택 |
ndo_get_stats64 | dev_get_stats() | RTNL 또는 RCU read | per-CPU 통계를 rtnl_link_stats64에 집계 | 선택 |
그룹 C — 주소/필터 설정
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_set_rx_mode | __dev_set_rx_mode() | netif_addr_lock held (spin), BH disabled | MC/UC 필터 HW 프로그래밍 | 권장 |
ndo_set_mac_address | dev_set_mac_address() | RTNL held, process | MAC 주소 변경, HW 반영 | 선택 |
ndo_validate_addr | dev_open() 내부 | RTNL held, process | MAC 주소 유효성 검증 | 선택 |
ndo_change_mtu | dev_set_mtu() | RTNL held, process | MTU 변경 시 HW/ring 재설정 | 선택 |
그룹 D — VLAN
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_vlan_rx_add_vid | vlan_vid_add() | RTNL held, process | HW VLAN 필터에 VID 등록 | VLAN 필터 시 |
ndo_vlan_rx_kill_vid | vlan_vid_del() | RTNL held, process | HW VLAN 필터에서 VID 제거 | VLAN 필터 시 |
그룹 E — Feature/오프로드(Offload)
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_fix_features | netdev_update_features() | RTNL held, process | HW 제약에 따른 feature 조합 강제 보정 | 선택 |
ndo_set_features | netdev_update_features() | RTNL held, process | 변경된 feature를 HW 레지스터(Register)에 적용 | 선택 |
ndo_features_check | netif_skb_features() | data path (softirq/process) | per-skb feature 호환성 검사, SW fallback 결정 | 선택 |
그룹 F — TC/XDP 오프로드
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_setup_tc | TC qdisc/filter 설정 시 | RTNL held, process | TC 오프로드 구성 (mqprio, flower 등) | 선택 |
ndo_bpf | XDP 프로그램 attach/detach | RTNL held, process | XDP 프로그램 설치, HW 오프로드 | XDP 시 |
ndo_xdp_xmit | XDP_TX / XDP_REDIRECT 처리 | NAPI context (softirq) | XDP 프레임 배치 전송, doorbell 최적화 | XDP 시 |
그룹 G — 브릿지/FDB
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_fdb_add | FDB 엔트리 추가 요청 | RTNL held, process | HW FDB 테이블에 MAC 등록 | switchdev 시 |
ndo_fdb_del | FDB 엔트리 삭제 요청 | RTNL held, process | HW FDB 테이블에서 MAC 제거 | switchdev 시 |
ndo_fdb_dump | FDB 전체 덤프(Dump) 요청 | RTNL held, process | HW FDB 엔트리 열거 | switchdev 시 |
ndo_bridge_setlink | 브릿지 포트 설정 변경 | RTNL held, process | 브릿지 모드, learning 등 HW 설정 | 선택 |
ndo_bridge_getlink | 브릿지 포트 설정 조회 | RTNL held, process | 현재 브릿지 설정 반환 | 선택 |
그룹 H — 에러/복구
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_tx_timeout | TX watchdog 타임아웃 | process (workqueue) | TX 경로 복구, HW 리셋 | 권장 |
ndo_change_carrier | dev_change_carrier() | RTNL held, process | carrier 상태 수동 변경 (가상 장치) | 선택 |
그룹 I — ioctl/확장
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_eth_ioctl | SIOCSHWTSTAMP 등 이더넷 ioctl | RTNL held, process | HW 타임스탬프 설정, PHY ioctl 위임 | 선택 |
ndo_siocdevprivate | 드라이버 전용 ioctl | RTNL held, process | 레거시 드라이버별 커스텀 ioctl 처리 | 선택 |
ndo_get_phys_port_id | netlink 쿼리 | RTNL held, process | 물리 포트 식별자 반환 (switchdev) | switchdev 시 |
ndo_get_phys_port_name | netlink 쿼리 | RTNL held, process | 물리 포트 이름 반환 (representor) | switchdev 시 |
ndo_get_port_parent_id | netlink 쿼리 | RTNL 또는 RCU | 부모 스위치 ID 반환 (switchdev) | switchdev 시 |
그룹 J — 가상화(SR-IOV)
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_set_vf_mac | ip link set vf N mac | RTNL held, process | VF의 MAC 주소 설정 | SR-IOV 시 |
ndo_set_vf_vlan | ip link set vf N vlan | RTNL held, process | VF의 VLAN 태그 설정 | SR-IOV 시 |
ndo_set_vf_rate | ip link set vf N rate | RTNL held, process | VF의 TX rate 제한 설정 | SR-IOV 시 |
ndo_set_vf_spoofchk | ip link set vf N spoofchk | RTNL held, process | VF의 MAC/VLAN spoof 검사 활성화 | SR-IOV 시 |
ndo_get_vf_config | VF 설정 조회 | RTNL held, process | VF의 현재 설정 반환 | SR-IOV 시 |
그룹 K — 네임스페이스/링크
| 콜백 | 호출 시점 | 컨텍스트 | 핵심 책임 | 필수 |
|---|---|---|---|---|
ndo_get_iflink | netlink 쿼리, dev_get_iflink() | RCU 또는 RTNL | 실제 하위 장치 ifindex 반환 (VLAN, tunnel, macvlan 등) | 가상장치 시 |
ndo_open / ndo_stop 상세 분석
ndo_open()은 네트워크 장치가 "UP" 상태로 전환될 때 호출되며, 하드웨어를 완전한 동작 상태로 만드는 책임을 집니다. 이 콜백 내부의 초기화 순서는 엄격하게 지켜야 하며, 순서가 뒤바뀌면 인터럽트가 발생했을 때 아직 준비되지 않은 자료구조에 접근하여 커널 패닉이 발생할 수 있습니다.
/* 커널 소스 분석: ndo_open 구현의 올바른 초기화 순서 */
static int mydrv_open(struct net_device *dev)
{
struct mydrv_priv *priv = netdev_priv(dev);
int err;
/* 1단계: DMA 링 버퍼 할당
* IRQ 요청 전에 반드시 완료해야 함.
* 인터럽트 핸들러가 참조할 descriptor ring이
* 아직 없으면 NULL 역참조 발생 */
err = mydrv_alloc_rings(priv);
if (err)
return err;
/* 2단계: HW 초기화 — MAC, 필터, DMA 엔진 활성화
* 링 버퍼 주소를 HW 레지스터에 기록 */
mydrv_hw_init(priv);
/* 3단계: NAPI 활성화
* IRQ 핸들러가 napi_schedule()을 호출하기 전에
* NAPI가 활성화 상태여야 함.
* napi_enable() 없이 napi_schedule() → BUG_ON */
for (int i = 0; i < priv->num_queues; i++)
napi_enable(&priv->queues[i].napi);
/* 4단계: IRQ 요청 (인터럽트 활성화)
* NAPI 활성화 이후에 요청해야 안전.
* MSI-X: 큐별 개별 IRQ 할당 */
err = mydrv_request_irqs(priv);
if (err)
goto err_disable_napi;
/* 5단계: carrier 상태 설정
* PHY 링크가 확인되면 carrier on */
if (mydrv_link_is_up(priv))
netif_carrier_on(dev);
else
netif_carrier_off(dev);
/* 6단계: TX 큐 시작 — 반드시 마지막
* 모든 준비가 완료된 후에만 패킷 수신/송신 허용.
* 이 시점부터 ndo_start_xmit이 호출될 수 있음 */
netif_tx_start_all_queues(dev);
return 0;
err_disable_napi:
for (int i = 0; i < priv->num_queues; i++)
napi_disable(&priv->queues[i].napi);
mydrv_free_rings(priv);
return err;
}
ndo_stop()은 ndo_open()의 정확한 역순으로 자원을 해제해야 합니다. 순서를 지키지 않으면 해제 중에 인터럽트가 발생하여 이미 해제된 메모리에 접근하는 use-after-free가 발생합니다.
/* 커널 소스 분석: ndo_stop 구현의 올바른 해제 순서
* ndo_open의 정확한 역순이어야 함 */
static int mydrv_stop(struct net_device *dev)
{
struct mydrv_priv *priv = netdev_priv(dev);
/* 1단계: TX 큐 정지 — 가장 먼저
* 새로운 패킷이 ndo_start_xmit으로 들어오는 것을 차단 */
netif_tx_stop_all_queues(dev);
/* 2단계: carrier off */
netif_carrier_off(dev);
/* 3단계: IRQ 해제
* 더 이상 새로운 인터럽트가 발생하지 않도록 보장.
* free_irq()는 현재 실행 중인 핸들러 완료를 기다림 */
mydrv_free_irqs(priv);
/* 4단계: NAPI 비활성화
* napi_disable()은 진행 중인 poll 완료를 기다림.
* IRQ 해제 후 호출해야 새로운 napi_schedule 방지 */
for (int i = 0; i < priv->num_queues; i++)
napi_disable(&priv->queues[i].napi);
/* 5단계: HW 비활성화 — DMA 엔진 정지
* NAPI 비활성화 후에 해야 DMA 완료 이벤트가
* poll에 의해 정리된 후 엔진을 멈춤 */
mydrv_hw_shutdown(priv);
/* 6단계: DMA 링 해제 — 가장 마지막
* HW와 NAPI 모두 정지한 후에만 안전하게 해제 가능 */
mydrv_free_rings(priv);
return 0;
}
ndo_stop()에서 napi_disable()을 free_irq()보다 먼저 호출하면, IRQ 핸들러(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_mtu와 dev->max_mtu를 HW 실제 한계에 맞게 설정해야 합니다 (예: Jumbo Frame 미지원 시 dev->max_mtu = ETH_DATA_LEN).
ndo_get_stats64 상세 분석
ndo_get_stats64()는 ip -s link show, /proc/net/dev 조회, SNMP MIB 수집 등 여러 경로에서 네트워크 통계를 요청할 때 호출됩니다. 현대 드라이버는 per-CPU 통계를 사용하여 캐시라인 경합(Cache Line Bouncing)을 방지합니다.
/* 커널 소스 분석: per-CPU 통계 구조체와 u64_stats_sync */
struct mydrv_stats {
u64 rx_packets;
u64 rx_bytes;
u64 tx_packets;
u64 tx_bytes;
struct u64_stats_sync syncp; /* 32비트 arch에서 64비트 원자적 읽기 보장 */
};
/* 드라이버 초기화에서:
* priv->pcpu_stats = netdev_alloc_pcpu_stats(struct mydrv_stats); */
/* TX 경로에서 통계 갱신 (per-CPU, 락 불필요) */
static void mydrv_tx_complete(struct mydrv_priv *priv,
unsigned int bytes)
{
struct mydrv_stats *stats = this_cpu_ptr(priv->pcpu_stats);
u64_stats_update_begin(&stats->syncp);
stats->tx_packets++;
stats->tx_bytes += bytes;
u64_stats_update_end(&stats->syncp);
}
/* 커널 소스 분석: ndo_get_stats64 구현
* 모든 CPU의 per-CPU 통계를 합산 */
static void mydrv_get_stats64(struct net_device *dev,
struct rtnl_link_stats64 *s)
{
struct mydrv_priv *priv = netdev_priv(dev);
int cpu;
for_each_possible_cpu(cpu) {
struct mydrv_stats *stats = per_cpu_ptr(priv->pcpu_stats, cpu);
u64 rx_packets, rx_bytes, tx_packets, tx_bytes;
unsigned int start;
/* u64_stats_fetch_begin: 32비트 아키텍처에서
* 64비트 값의 상위/하위 32비트가 일관되게 읽히도록 보장.
* 64비트 arch에서는 no-op으로 최적화됨 */
do {
start = u64_stats_fetch_begin(&stats->syncp);
rx_packets = stats->rx_packets;
rx_bytes = stats->rx_bytes;
tx_packets = stats->tx_packets;
tx_bytes = stats->tx_bytes;
} while (u64_stats_fetch_retry(&stats->syncp, start));
s->rx_packets += rx_packets;
s->rx_bytes += rx_bytes;
s->tx_packets += tx_packets;
s->tx_bytes += tx_bytes;
}
}
dev->pcpu_stat_type = NETDEV_PCPU_STAT_TSTATS를 설정하면, 코어가 자동으로 per-CPU 통계를 수집하여 ndo_get_stats64 구현 없이도 기본 통계(rx/tx packets/bytes)를 제공합니다. dev_get_tstats64()가 기본 핸들러로 사용됩니다.
ndo_features_check / ndo_fix_features / ndo_set_features 상호작용
네트워크 오프로드 기능(Feature)의 변경은 세 가지 콜백이 협력하여 처리됩니다. 이 세 콜백은 서로 다른 시점에 호출되며, 각각의 역할이 명확히 구분됩니다.
/* 커널 소스 분석: ndo_fix_features — HW 제약 강제 적용 */
static netdev_features_t mydrv_fix_features(struct net_device *dev,
netdev_features_t features)
{
/* HW 제약: TSO가 없으면 TX checksum offload 불가
* TSO는 CSUM을 전제로 하므로, CSUM이 꺼지면 TSO도 꺼야 함 */
if (!(features & NETIF_F_HW_CSUM))
features &= ~(NETIF_F_TSO | NETIF_F_TSO6);
/* HW 제약: scatter-gather 없으면 TSO 불가 */
if (!(features & NETIF_F_SG))
features &= ~(NETIF_F_TSO | NETIF_F_TSO6);
return features;
}
/* 커널 소스 분석: ndo_set_features — 실제 HW 레지스터 변경 */
static int mydrv_set_features(struct net_device *dev,
netdev_features_t features)
{
struct mydrv_priv *priv = netdev_priv(dev);
netdev_features_t changed = features ^ dev->features;
if (changed & NETIF_F_RXCSUM)
mydrv_set_rx_csum(priv, !!(features & NETIF_F_RXCSUM));
if (changed & (NETIF_F_TSO | NETIF_F_TSO6))
mydrv_set_tso(priv, !!(features & NETIF_F_TSO));
if (changed & NETIF_F_HW_VLAN_CTAG_RX)
mydrv_set_vlan_strip(priv, !!(features & NETIF_F_HW_VLAN_CTAG_RX));
return 0;
}
/* 커널 소스 분석: ndo_features_check — per-skb 호환성 검사
* 터널 패킷 등 HW가 처리할 수 없는 조합 감지 */
static netdev_features_t mydrv_features_check(struct sk_buff *skb,
struct net_device *dev,
netdev_features_t features)
{
/* 기본 VXLAN/GRE/Geneve 검사 적용 */
features = vlan_features_check(skb, features);
features = vxlan_features_check(skb, features);
/* HW 제한: inner header offset이 256바이트 초과 시
* TSO offload 불가 → SW로 fallback */
if (skb_is_gso(skb) &&
skb_inner_transport_offset(skb) > 256)
features &= ~(NETIF_F_TSO | NETIF_F_TSO6 | NETIF_F_GSO_GRE);
return features;
}
ndo_bpf / ndo_xdp_xmit 상세 분석
XDP(eXpress Data Path) 지원은 두 개의 콜백으로 구성됩니다. ndo_bpf()는 제어 경로(Control Path)에서 XDP 프로그램의 설치/제거를 처리하고, ndo_xdp_xmit()은 데이터 경로(Data Path)에서 XDP_TX/XDP_REDIRECT 액션의 프레임을 실제로 전송합니다.
/* 커널 소스 분석: ndo_bpf 구현 패턴
* XDP 프로그램 설치/제거 처리 */
static int mydrv_bpf(struct net_device *dev, struct netdev_bpf *bpf)
{
struct mydrv_priv *priv = netdev_priv(dev);
switch (bpf->command) {
case XDP_SETUP_PROG: {
/* 소프트웨어 XDP 프로그램 설치 (드라이버 모드)
* - RX ring에 XDP를 위한 headroom 확보 필요
* - 기존 프로그램 교체 시 rcu_assign 사용 */
struct bpf_prog *old_prog = priv->xdp_prog;
struct bpf_prog *new_prog = bpf->prog;
bool running = netif_running(dev);
/* XDP 프로그램이 처음 설치될 때:
* RX 버퍼에 XDP_PACKET_HEADROOM만큼 headroom 확보
* → ring 재할당 필요 → stop/restart */
if (running && (!!old_prog != !!new_prog)) {
mydrv_stop(dev);
priv->xdp_prog = new_prog;
if (new_prog)
priv->rx_headroom = XDP_PACKET_HEADROOM;
else
priv->rx_headroom = 0;
mydrv_open(dev);
} else {
WRITE_ONCE(priv->xdp_prog, new_prog);
}
if (old_prog)
bpf_prog_put(old_prog);
return 0;
}
case XDP_SETUP_HW_OFFLOAD:
/* SmartNIC HW 오프로드: BPF 프로그램을 NIC에 직접 로드
* 대부분의 드라이버는 미지원 → -EOPNOTSUPP 반환 */
return -EOPNOTSUPP;
default:
return -EINVAL;
}
}
/* 커널 소스 분석: ndo_xdp_xmit — XDP 프레임 배치 전송
* NAPI 컨텍스트(softirq)에서 호출됨 */
static int mydrv_xdp_xmit(struct net_device *dev, int n,
struct xdp_frame **frames,
u32 flags)
{
struct mydrv_priv *priv = netdev_priv(dev);
struct mydrv_tx_ring *ring;
int i, sent = 0;
/* XDP 전용 TX 큐 사용 (일반 TX 큐와 분리하여 락 경합 방지) */
ring = &priv->tx_rings[priv->xdp_tx_queue];
for (i = 0; i < n; i++) {
if (mydrv_tx_avail(ring) == 0)
break;
mydrv_xdp_tx_map(ring, frames[i]);
sent++;
}
/* XDP_XMIT_FLUSH: 배치 전송 완료 후 doorbell 알림
* 이 플래그가 설정되어야만 HW가 전송을 시작
* XDP_REDIRECT 배치의 마지막 호출에만 설정됨 */
if (flags & XDP_XMIT_FLUSH)
mydrv_ring_doorbell(ring);
return sent; /* 실제 전송된 프레임 수 반환 */
}
ndo_xdp_xmit()은 NAPI 컨텍스트에서 호출되므로, 일반 TX 큐를 사용하면 qdisc의 per-queue lock과 경합이 발생합니다. 최적의 설계는 XDP 전용 TX 큐를 별도로 할당하여 데이터 경로 간 독립성을 확보하는 것입니다. num_tx_queues를 XDP 큐만큼 추가로 할당하고, real_num_tx_queues는 일반 큐 수만 설정합니다.
콜백 미구현 시 기본 동작
드라이버가 특정 콜백을 NULL로 남겨두면 커널 코어가 기본 동작(Default Behavior)을 제공합니다. 아래 테이블은 주요 콜백이 NULL일 때의 동작을 정리한 것입니다.
| 콜백 | NULL 시 기본 동작 | 관련 코어 코드 |
|---|---|---|
ndo_init |
추가 초기화 없이 등록 진행 | register_netdevice() |
ndo_uninit |
추가 정리 없이 해제 진행 | unregister_netdevice() |
ndo_change_mtu |
dev->min_mtu ~ max_mtu 범위 내 자동 허용. 코어가 직접 WRITE_ONCE(dev->mtu, new_mtu) 수행 |
dev_set_mtu_ext() |
ndo_validate_addr |
항상 성공 (0 반환). 유효하지 않은 MAC으로도 UP 가능 | dev_open() |
ndo_select_queue |
netdev_pick_tx()의 기본 해시(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_TSTATS → dev_get_tstats64(), NETDEV_PCPU_STAT_DSTATS → dev_get_dstats64(), 미설정 → dev->stats 구조체 직접 반환 |
dev_get_stats() |
ndo_fix_features |
요청된 features 그대로 수락 (HW 제약 미적용) | netdev_fix_features() |
ndo_set_features |
feature 비트만 변경되고 HW에 반영되지 않음 | netdev_update_features() |
ndo_features_check |
passthru_features_check() — 모든 feature 허용 |
netif_skb_features() |
ndo_tx_timeout |
TX watchdog 타임아웃 시 복구 동작 없음. 인터페이스가 먹통 상태로 유지 | dev_watchdog() |
ndo_bpf |
XDP 프로그램 attach 시 -EINVAL 반환. generic XDP만 사용 가능 (성능 저하) |
dev_xdp_install() |
ndo_xdp_xmit |
XDP_TX/XDP_REDIRECT 액션 시 프레임 드롭 |
dev_xdp_enqueue() |
ndo_get_iflink |
dev->ifindex 반환 (자기 자신) |
dev_get_iflink() |
ndo_eth_ioctl |
-EOPNOTSUPP 반환. HW 타임스탬프 등 미지원 |
dev_eth_ioctl() |
ndo_tx_timeout이 NULL이면 TX 경로가 정지(stall)되었을 때 자동 복구 메커니즘이 없습니다. 실제 하드웨어 드라이버에서는 반드시 구현하여, 타임아웃 발생 시 TX 큐 리셋이나 전체 HW 리셋을 수행해야 합니다. 가상 장치(veth, bridge 등)는 HW 정지가 없으므로 생략 가능합니다.
상태 관리: carrier, operstate, link 매크로
네트워크 디바이스의 운영 상태(Operational State)는 단일 변수가 아니라 여러 비트 플래그와 참조 카운트의 조합으로 결정됩니다. 드라이버는 dev->state 비트맵, dev->operstate, TX 큐 상태, promiscuous/allmulti 참조 카운트를 정확히 조작해야 합니다.
carrier 상태 관리
netif_carrier_on()/netif_carrier_off()는 물리 링크의 연결/단절을 커널에 알리는 핵심 인터페이스입니다. PHY 드라이버가 링크 상태 변화를 감지하면 phylink_mac_link_up()/phylink_mac_link_down()을 통해 이 함수를 호출합니다.
/* 커널 소스 분석: netif_carrier_on/off 내부 구현 (include/linux/netdevice.h, net/sched/sch_generic.c) */
static inline void netif_carrier_on(struct net_device *dev)
{
/* __LINK_STATE_NOCARRIER 비트를 클리어 → carrier 존재 */
if (test_and_clear_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
if (dev->reg_state == NETREG_REGISTERED)
linkwatch_fire_event(dev); /* 비동기 operstate 업데이트 트리거 */
}
}
static inline void netif_carrier_off(struct net_device *dev)
{
/* __LINK_STATE_NOCARRIER 비트를 설정 → carrier 없음 */
if (!test_and_set_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
if (dev->reg_state == NETREG_REGISTERED)
linkwatch_fire_event(dev);
}
}
/* 커널 소스 분석: linkwatch_fire_event → 워크큐에 operstate 갱신 예약 */
void linkwatch_fire_event(struct net_device *dev)
{
bool urgent = netif_running(dev) && netif_carrier_ok(dev);
if (test_and_set_bit(__LINK_STATE_LINKWATCH_PENDING, &dev->state))
return;
dev_hold(dev);
if (urgent)
mod_delayed_work(system_wq, &linkwatch_work, 0);
else
schedule_delayed_work(&linkwatch_work, linkwatch_nextevent - jiffies);
}
netif_carrier_on/off는 프로세스 컨텍스트(Process Context)뿐 아니라 인터럽트 컨텍스트에서도 안전하게 호출 가능합니다. linkwatch_fire_event()가 실제 상태 전파를 워크큐(Workqueue)로 연기하기 때문입니다.
link state 비트맵 (__LINK_STATE_*)
dev->state 필드는 unsigned long 비트맵으로, 디바이스의 다양한 링크 상태를 원자적으로 관리합니다.
| 비트 | 의미 | 조회 매크로 |
|---|---|---|
__LINK_STATE_START | 디바이스가 ndo_open()을 통해 활성화됨 | netif_running() |
__LINK_STATE_PRESENT | 디바이스가 물리적으로 존재함 (hot-unplug 감지) | netif_device_present() |
__LINK_STATE_NOCARRIER | 물리 링크 없음 (케이블 미연결 등) | netif_carrier_ok() (반전) |
__LINK_STATE_LINKWATCH_PENDING | linkwatch 이벤트가 워크큐에 대기 중 | 직접 조회 불필요 |
__LINK_STATE_DORMANT | 802.1X 인증 대기, supplicant 미완료 | netif_dormant() |
__LINK_STATE_TESTING | 자가 진단(Self-test) 실행 중 | netif_testing() |
netif_* 상태 조회 매크로
| 매크로 | 검사 대상 | 반환값 | 호출 컨텍스트 | 용도 |
|---|---|---|---|---|
netif_running(dev) | test_bit(__LINK_STATE_START, &dev->state) | bool | 모든 컨텍스트 | ndo_open() 이후 true, 데이터 경로 진입 가드 |
netif_carrier_ok(dev) | !test_bit(__LINK_STATE_NOCARRIER, &dev->state) | bool | 모든 컨텍스트 | PHY 링크 연결 상태 확인 |
netif_oper_up(dev) | dev->operstate == IF_OPER_UP | bool | RTNL 또는 RCU | 라우팅 판단 시 실제 운영 상태 확인 |
netif_device_present(dev) | test_bit(__LINK_STATE_PRESENT, &dev->state) | bool | 모든 컨텍스트 | hot-unplug/AER 복구 시 접근 가능 여부 |
netif_dormant(dev) | test_bit(__LINK_STATE_DORMANT, &dev->state) | bool | 모든 컨텍스트 | 802.1X 인증 완료 대기 중 여부 |
netif_testing(dev) | test_bit(__LINK_STATE_TESTING, &dev->state) | bool | 모든 컨텍스트 | 자가 진단 실행 중 여부 |
netif_device_detach() / netif_device_attach()
PCIe AER 복구(Advanced Error Recovery)나 suspend/resume 시나리오에서 디바이스를 일시적으로 분리/재연결하는 함수입니다.
/* 커널 소스 분석: netif_device_detach/attach 내부 (net/core/dev.c) */
void netif_device_detach(struct net_device *dev)
{
/* PRESENT 비트 클리어 → netif_device_present() == false */
if (test_and_clear_bit(__LINK_STATE_PRESENT, &dev->state) &&
netif_running(dev)) {
netif_tx_stop_all_queues(dev); /* 모든 TX 큐 정지 */
}
}
void netif_device_attach(struct net_device *dev)
{
/* PRESENT 비트 설정 → 디바이스 복귀 */
if (!test_and_set_bit(__LINK_STATE_PRESENT, &dev->state) &&
netif_running(dev)) {
netif_tx_wake_all_queues(dev); /* 모든 TX 큐 재개 */
__netdev_watchdog_up(dev);
}
}
/* 개념 예시: PCIe AER 복구에서의 사용 패턴 */
static pci_ers_result_t my_io_error_detected(struct pci_dev *pdev,
pci_channel_state_t state)
{
struct net_device *ndev = pci_get_drvdata(pdev);
netif_device_detach(ndev); /* 패킷 경로 차단 */
if (state == pci_channel_io_perm_failure)
return PCI_ERS_RESULT_DISCONNECT;
my_disable_hw(ndev);
return PCI_ERS_RESULT_NEED_RESET;
}
static void my_io_resume(struct pci_dev *pdev)
{
struct net_device *ndev = pci_get_drvdata(pdev);
my_reinit_hw(ndev);
netif_device_attach(ndev); /* 패킷 경로 복원 */
}
TX 큐 제어 매크로 상세 분석
TX 큐 제어는 dev->_tx[i].state 비트맵의 __QUEUE_STATE_DRV_XOFF 비트를 조작합니다. BQL(Byte Queue Limits)과 연동하여 큐를 정지/재개하면 버퍼블로트(Bufferbloat)를 효과적으로 방지할 수 있습니다.
/* 커널 소스 분석: TX 큐 상태 비트 조작 (include/linux/netdevice.h) */
/* __QUEUE_STATE_DRV_XOFF: 드라이버가 큐를 정지시킨 상태 */
static inline void netif_tx_stop_queue(struct netdev_queue *dev_queue)
{
set_bit(__QUEUE_STATE_DRV_XOFF, &dev_queue->state);
}
static inline void netif_tx_wake_queue(struct netdev_queue *dev_queue)
{
if (test_and_clear_bit(__QUEUE_STATE_DRV_XOFF, &dev_queue->state))
__netif_schedule(dev_queue->qdisc); /* qdisc에 재스케줄 요청 */
}
static inline bool netif_tx_queue_stopped(const struct netdev_queue *dev_queue)
{
return test_bit(__QUEUE_STATE_DRV_XOFF, &dev_queue->state);
}
/* netif_tx_start_all_queues: 모든 큐의 XOFF 비트 클리어 */
static inline void netif_tx_start_all_queues(struct net_device *dev)
{
unsigned int i;
for (i = 0; i < dev->num_tx_queues; i++) {
struct netdev_queue *txq = netdev_get_tx_queue(dev, i);
netif_tx_start_queue(txq);
}
}
/* netif_tx_disable: 모든 큐 정지 + BH 완료 대기 (안전한 종료) */
void netif_tx_disable(struct net_device *dev)
{
unsigned int i;
local_bh_disable();
for (i = 0; i < dev->num_tx_queues; i++) {
struct netdev_queue *txq = netdev_get_tx_queue(dev, i);
__netif_tx_lock(txq, smp_processor_id());
netif_tx_stop_queue(txq);
__netif_tx_unlock(txq);
}
local_bh_enable();
}
ndo_start_xmit()에서 TX 링이 가득 찼을 때 netif_tx_stop_queue()를 호출하고, TX completion 인터럽트에서 공간이 확보되면 netif_tx_wake_queue()를 호출하는 패턴이 표준입니다. 이 stop/wake 짝이 맞지 않으면 큐가 영구 정지되는 TX hang이 발생합니다.
promiscuous/allmulti 모드 관리
dev_set_promiscuity()와 dev_set_allmulti()는 참조 카운트 기반으로 동작합니다. 여러 소비자(tcpdump, 브릿지, VLAN 등)가 독립적으로 모드를 요청하고 해제할 수 있습니다.
/* 커널 소스 분석: dev_set_promiscuity 내부 (net/core/dev.c) */
static int __dev_set_promiscuity(struct net_device *dev, int inc, bool notify)
{
unsigned int old_flags = dev->flags;
int old_pcount = dev->promiscuity;
dev->promiscuity += inc;
if (dev->promiscuity == 0) {
/* 참조 카운트가 0 → promisc 모드 해제 */
dev->flags &= ~IFF_PROMISC;
} else if (dev->promiscuity > 0) {
dev->flags |= IFF_PROMISC;
}
if (dev->flags != old_flags) {
/* 플래그 변경 시 하드웨어 RX 필터 갱신 */
__dev_set_rx_mode(dev); /* → ndo_set_rx_mode 콜백 호출 */
if (notify)
__dev_notify_flags(dev, old_flags, 0);
}
return 0;
}
/* dev_set_allmulti: 동일한 참조 카운트 패턴 */
int dev_set_allmulti(struct net_device *dev, int inc)
{
unsigned int old_flags = dev->flags;
dev->allmulti += inc;
if (dev->allmulti == 0)
dev->flags &= ~IFF_ALLMULTI;
else if (dev->allmulti > 0)
dev->flags |= IFF_ALLMULTI;
if (dev->flags ^ old_flags)
__dev_set_rx_mode(dev);
return 0;
}
dev_set_promiscuity(dev, 1)을 호출했다면 반드시 dev_set_promiscuity(dev, -1)로 해제해야 합니다. 모듈 언로드나 에러 경로에서 해제를 빠뜨리면 디바이스가 영구적으로 promiscuous 모드에 머무릅니다.
netdev feature 시스템
netdev_features_t 비트맵은 하드웨어 오프로드(Offload) 기능, 프로토콜 기능, 소프트웨어 기능을 단일 64-bit 비트맵으로 관리합니다. ethtool -k로 확인하는 모든 feature가 이 비트맵에 매핑됩니다.
netdev_features_t 비트맵 개요
/* 커널 소스 분석: netdev_features_t 정의 (include/linux/netdev_features.h) */
typedef u64 netdev_features_t;
/* 각 NETIF_F_* 플래그는 단일 비트 위치에 대응 */
#define NETIF_F_SG __NETIF_F(SG)
#define NETIF_F_IP_CSUM __NETIF_F(IP_CSUM)
#define NETIF_F_TSO __NETIF_F(TSO)
#define NETIF_F_GRO __NETIF_F(GRO)
/* ... 60+ 비트 정의 */
주요 NETIF_F_* 플래그 분류
체크섬(Checksum) 오프로드
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_IP_CSUM | IPv4 TCP/UDP 하드웨어 체크섬 계산 | Checksum |
NETIF_F_IPV6_CSUM | IPv6 TCP/UDP 하드웨어 체크섬 계산 | Checksum |
NETIF_F_HW_CSUM | 프로토콜 무관 완전 하드웨어 체크섬 (L3/L4 구분 없이) | Checksum |
NETIF_F_RXCSUM | 수신 체크섬 오프로드 (HW가 검증 완료 표시) | Checksum |
세그멘테이션(Segmentation) 오프로드
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_TSO | TCP Segmentation Offload (IPv4) | Segmentation |
NETIF_F_TSO6 | TSO IPv6 | Segmentation |
NETIF_F_TSO_ECN | ECN 지원 TSO | Segmentation |
NETIF_F_GSO | Generic Segmentation Offload (소프트웨어 fallback 포함) | Segmentation |
NETIF_F_GSO_GRE | GRE 터널 GSO | Segmentation |
NETIF_F_GSO_UDP_TUNNEL | UDP 터널(VXLAN, Geneve) GSO | Segmentation |
Scatter/Gather 및 DMA
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_SG | Scatter/Gather I/O (비연속 메모리 전송) | SG/DMA |
NETIF_F_FRAGLIST | Fragment list 지원 | SG/DMA |
NETIF_F_HIGHDMA | High memory 영역 DMA 가능 | SG/DMA |
수신(Receive) 오프로드
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_GRO | Generic Receive Offload (소프트웨어 패킷 집계) | Receive |
NETIF_F_GRO_HW | 하드웨어 GRO (NIC이 직접 coalescing) | Receive |
NETIF_F_LRO | Large Receive Offload (라우팅 환경 비권장) | Receive |
VLAN 오프로드
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_HW_VLAN_CTAG_TX | VLAN C-Tag 하드웨어 삽입 | VLAN |
NETIF_F_HW_VLAN_CTAG_RX | VLAN C-Tag 하드웨어 추출 | VLAN |
NETIF_F_HW_VLAN_CTAG_FILTER | VLAN 하드웨어 필터링 | VLAN |
기타 기능
| 플래그 | 설명 | 카테고리 |
|---|---|---|
NETIF_F_LLTX | Lockless TX (드라이버 자체 동기화 보장) | TX |
NETIF_F_LOOPBACK | 루프백(Loopback) feature | 기타 |
NETIF_F_NTUPLE | N-tuple 필터 (Flow Director) | 필터 |
NETIF_F_RXHASH | RX 해시 제공 (RSS 연동) | 수신 |
NETIF_F_HW_TC | TC 오프로드 지원 | QoS |
feature 필드 간 관계
net_device에는 feature와 관련된 5개의 필드가 있으며, 각 필드의 역할과 상호 관계를 정확히 이해해야 합니다.
| 필드 | 의미 | 설정 주체 |
|---|---|---|
dev->hw_features | 하드웨어가 지원하는 전체 feature set | 드라이버 probe 시 설정 |
dev->features | 현재 활성 feature (hw_features의 부분집합) | 커널 코어가 계산 |
dev->wanted_features | 사용자가 ethtool -K로 요청한 feature | ethtool 명령으로 설정 |
dev->vlan_features | VLAN 하위 디바이스에 전파할 feature | 드라이버 probe 시 설정 |
dev->hw_enc_features | 터널 encapsulation에 사용할 feature | 드라이버 probe 시 설정 |
netdev_update_features() 내부 흐름
ndo_fix_features 패턴
ndo_fix_features()는 하드웨어 제약에 따라 feature 조합을 보정하는 콜백입니다. TSO는 체크섬 오프로드를 필요로 하는 등 feature 간 의존성(Dependency)을 여기서 강제합니다.
/* 개념 예시: ndo_fix_features에서 feature 의존성 강제 */
static netdev_features_t my_fix_features(struct net_device *ndev,
netdev_features_t features)
{
/* TSO는 체크섬 오프로드가 반드시 필요 */
if (!(features & NETIF_F_HW_CSUM) &&
!(features & (NETIF_F_IP_CSUM | NETIF_F_IPV6_CSUM))) {
features &= ~(NETIF_F_TSO | NETIF_F_TSO6 | NETIF_F_TSO_ECN);
netdev_warn(ndev, "TSO disabled: requires CSUM offload\n");
}
/* SG 없이 TSO 불가 */
if (!(features & NETIF_F_SG))
features &= ~(NETIF_F_TSO | NETIF_F_TSO6);
/* 특정 HW 리비전은 GRO_HW 미지원 */
struct my_priv *priv = netdev_priv(ndev);
if (priv->hw_rev < 3)
features &= ~NETIF_F_GRO_HW;
/* LRO와 포워딩(forwarding)은 공존 불가 */
if (features & NETIF_F_LRO) {
struct net *net = dev_net(ndev);
if (net->ipv4.sysctl_ip_forward)
features &= ~NETIF_F_LRO;
}
return features;
}
ethtool -k eth0에서 [fixed]로 표시되는 항목은 hw_features에 포함되지 않은 feature입니다. [requested on]은 wanted_features에 설정되었으나 ndo_fix_features에 의해 비활성화된 상태입니다. [not requested]은 wanted_features에 설정되지 않은 feature입니다.
netdev notifier chain
netdev 알림 체인은 네트워크 디바이스의 상태 변경 이벤트를 관심 있는 서브시스템에 전달하는 옵저버 패턴(Observer Pattern)입니다. IPv4/IPv6 스택, 라우팅, 방화벽(Firewall), 브릿지(Bridge), bonding 등이 이 체인에 등록하여 디바이스 이벤트에 반응합니다.
주요 NETDEV_* 이벤트
| 이벤트 | 발생 시점 | 주요 수신자 |
|---|---|---|
NETDEV_UP | ndo_open() 성공 후 | IP 스택 (주소 활성화), 라우팅 (경로 추가) |
NETDEV_DOWN | ndo_stop() 호출 전 | IP 스택 (주소 비활성화), 라우팅 (경로 제거) |
NETDEV_REGISTER | register_netdevice() 완료 | sysfs, procfs (디바이스 노드 생성) |
NETDEV_UNREGISTER | unregister_netdevice() 시작 | 모든 참조자 (참조 정리 시작) |
NETDEV_CHANGENAME | 인터페이스 이름 변경 | udev, sysfs (심볼릭 링크 갱신) |
NETDEV_CHANGEADDR | MAC 주소 변경 | ARP, NDP (이웃 캐시 갱신) |
NETDEV_CHANGEMTU | MTU 변경 | IPv6 (MTU 업데이트), PMTUD |
NETDEV_CHANGE | carrier/operstate 변경 | 라우팅 (경로 가중치 업데이트) |
NETDEV_FEAT_CHANGE | feature 변경 | VLAN 하위 디바이스 (feature 재전파) |
NETDEV_PRE_UP | ndo_open() 직전 | 보안 모듈, 정책 검사 (open 거부 가능) |
NETDEV_GOING_DOWN | ndo_stop() 직전 | 연결 종료 시작 신호 |
NETDEV_CHANGE_TX_QUEUE_LEN | TX 큐 길이 변경 | qdisc (큐 재구성) |
NETDEV_BONDING_FAILOVER | bonding failover 발생 | ARP (GARP 전송), 라우팅 |
NETDEV_JOIN | bonding/team 합류 | bonding 매니저 |
NETDEV_RESEND_IGMP | IGMP 재전송(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;
}
netdev 유틸리티 함수/매크로
네트워크 디바이스 관리에 자주 사용되는 헬퍼 함수(Helper Function)와 매크로를 정리합니다. 이 함수들은 드라이버 코드 전반에서 반복적으로 등장하므로 내부 구현을 이해하면 코드 리딩(Code Reading) 속도가 크게 향상됩니다.
netdev_priv() 내부 구현
수명주기 섹션에서 다룬 netdev_priv()는 net_device 구조체 바로 뒤에 정렬된 드라이버 전용 영역의 포인터를 반환합니다.
/* 커널 소스 분석: netdev_priv 내부 (include/linux/netdevice.h) */
static inline void *netdev_priv(const struct net_device *dev)
{
return (void *)((char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN));
}
SET_NETDEV_DEV / SET_NETDEV_DEVTYPE
SET_NETDEV_DEV()는 ndev->dev.parent를 설정하여 sysfs 계층, 전원 관리(Power Management), DMA 디바이스를 연결합니다. 반드시 register_netdev() 전에 호출해야 합니다.
/* 커널 소스 분석: SET_NETDEV_DEV 매크로 (include/linux/netdevice.h) */
#define SET_NETDEV_DEV(net, pdev) ((net)->dev.parent = (pdev))
#define SET_NETDEV_DEVTYPE(net, devtype) ((net)->dev.type = (devtype))
/* 개념 예시: PCI 디바이스와 netdev 연결 */
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *ndev;
ndev = alloc_etherdev(sizeof(struct my_priv));
if (!ndev)
return -ENOMEM;
/* sysfs: /sys/class/net/eth0/device → PCI 디바이스 링크
* DMA: dev_is_dma_coherent(&ndev->dev) 올바르게 동작
* PM: netdev가 PCI 전원 관리 계층에 포함 */
SET_NETDEV_DEV(ndev, &pdev->dev);
pci_set_drvdata(pdev, ndev);
return register_netdev(ndev); /* SET_NETDEV_DEV 이후에 호출 */
}
for_each_netdev 매크로 패밀리
/* 커널 소스 분석: for_each_netdev 매크로들 (include/linux/netdevice.h) */
/* RTNL lock 보유 상태에서 순회 */
#define for_each_netdev(net, d) \
list_for_each_entry(d, &(net)->dev_base_head, dev_list)
/* RCU read lock 하에서 순회 (데이터 경로 안전) */
#define for_each_netdev_rcu(net, d) \
list_for_each_entry_rcu(d, &(net)->dev_base_head, dev_list)
/* 순회 중 디바이스 삭제 가능 (safe 변형) */
#define for_each_netdev_safe(net, d, n) \
list_for_each_entry_safe(d, n, &(net)->dev_base_head, dev_list)
/* 특정 지점부터 계속 순회 */
#define for_each_netdev_continue(net, d) \
list_for_each_entry_continue(d, &(net)->dev_base_head, dev_list)
/* 개념 예시: 네임스페이스 내 모든 디바이스 MTU 출력 */
void dump_all_mtus(struct net *net)
{
struct net_device *dev;
rcu_read_lock();
for_each_netdev_rcu(net, dev) {
pr_info("%s: mtu=%d\n", dev->name, dev->mtu);
}
rcu_read_unlock();
}
dev_alloc_name() / dev_get_valid_name()
printf 스타일 패턴("eth%d")으로 인터페이스 이름을 자동 할당합니다. IDA(ID Allocator) 기반으로 번호를 할당하며, IFNAMSIZ (16바이트) 길이 제한이 적용됩니다.
/* 커널 소스 분석: dev_alloc_name 패턴 (net/core/dev.c) */
int dev_alloc_name(struct net_device *dev, const char *name)
{
/* name = "eth%d" → "eth0", "eth1", ... 자동 할당
* 내부적으로 __dev_alloc_name 호출 → IDA 기반 번호 할당
* IFNAMSIZ(16) 초과 시 -EINVAL */
return dev_get_valid_name(dev_net(dev), dev, name);
}
netdev_info/warn/err/dbg 로깅 매크로
디바이스 이름을 자동으로 포함하는 로깅 매크로 패밀리입니다. netdev_dbg는 CONFIG_DYNAMIC_DEBUG 활성 시 런타임에 on/off 제어가 가능합니다.
/* 커널 소스 분석: netdev 로깅 매크로 (include/linux/netdevice.h) */
#define netdev_info(dev, fmt, ...) \
dev_info(&(dev)->dev, fmt, ##__VA_ARGS__)
/* 출력: "my_driver 0000:03:00.0 eth0: link up at 10Gbps" */
/* 레벨별 매크로: emerg > alert > crit > err > warn > notice > info > dbg */
#define netdev_dbg(dev, fmt, ...) /* CONFIG_DYNAMIC_DEBUG 시 런타임 제어 */
#define netdev_info(dev, fmt, ...)
#define netdev_notice(dev, fmt, ...)
#define netdev_warn(dev, fmt, ...)
#define netdev_err(dev, fmt, ...)
#define netdev_crit(dev, fmt, ...)
#define netdev_alert(dev, fmt, ...)
#define netdev_emerg(dev, fmt, ...)
/* 개념 예시: 드라이버 링크 상태 로깅 */
static void my_link_up(struct net_device *ndev, int speed)
{
netdev_info(ndev, "link up at %dMbps\n", speed);
netif_carrier_on(ndev);
}
static void my_link_down(struct net_device *ndev)
{
netdev_warn(ndev, "link down detected\n");
netif_carrier_off(ndev);
}
/* Dynamic debug 런타임 제어:
* echo 'module my_driver +p' > /sys/kernel/debug/dynamic_debug/control */
static void my_rx_debug(struct net_device *ndev, struct sk_buff *skb)
{
netdev_dbg(ndev, "rx: len=%u protocol=0x%04x\n",
skb->len, ntohs(skb->protocol));
}
dev_hold() / dev_put() 참조 카운트 관리
커널 6.0 이후 refcnt_tracker 기반 참조 카운팅(Reference Counting)이 도입되어 참조 누수를 추적할 수 있습니다.
/* 커널 소스 분석: dev_hold/dev_put 내부 (include/linux/netdevice.h) */
static inline void dev_hold(struct net_device *dev)
{
netdev_hold(dev, NULL, GFP_ATOMIC);
}
static inline void dev_put(struct net_device *dev)
{
netdev_put(dev, NULL);
}
/* 커널 소스 분석: 내부 구현 — refcount + tracker */
static inline void netdev_hold(struct net_device *dev,
struct netdev_tracker *tracker,
gfp_t gfp)
{
if (dev) {
refcount_inc(&dev->dev_refcnt);
netdev_tracker_alloc(dev, tracker, gfp);
}
}
static inline void netdev_put(struct net_device *dev,
struct netdev_tracker *tracker)
{
if (dev) {
netdev_tracker_free(dev, tracker);
if (refcount_dec_and_test(&dev->dev_refcnt))
netdev_free(dev); /* 참조 0 도달 → 해제 트리거 */
}
}
/* 개념 예시: 추적 가능한 참조 카운트 사용 (6.0+) */
struct my_context {
struct net_device *dev;
struct netdev_tracker dev_tracker;
};
void my_grab_dev(struct my_context *ctx, struct net_device *dev)
{
netdev_hold(dev, &ctx->dev_tracker, GFP_KERNEL);
ctx->dev = dev;
}
void my_release_dev(struct my_context *ctx)
{
netdev_put(ctx->dev, &ctx->dev_tracker);
ctx->dev = NULL;
}
eth_type_trans() 내부
eth_type_trans()는 수신 패킷의 이더넷(Ethernet) 헤더를 분석하여 skb의 프로토콜, 패킷 유형, 디바이스를 설정하는 핵심 함수입니다.
/* 커널 소스 분석: eth_type_trans 내부 (net/ethernet/eth.c) */
__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)
{
unsigned short _service_access_point;
const struct ethhdr *eth;
skb->dev = dev;
skb_reset_mac_header(skb);
eth = (const struct ethhdr *)skb_mac_header(skb);
skb_pull_inline(skb, ETH_HLEN); /* 14바이트 이더넷 헤더 소비 */
/* pkt_type 결정: 목적지 MAC 주소 기반 */
if (unlikely(!ether_addr_equal_64bits(eth->h_dest, dev->dev_addr))) {
if (is_multicast_ether_addr_64bits(eth->h_dest)) {
if (ether_addr_equal_64bits(eth->h_dest, dev->broadcast))
skb->pkt_type = PACKET_BROADCAST;
else
skb->pkt_type = PACKET_MULTICAST;
} else {
skb->pkt_type = PACKET_OTHERHOST;
}
}
/* 기본값은 PACKET_HOST (우리 MAC 주소와 일치) */
/* protocol 필드 결정 */
if (likely(eth_proto_is_802_3(eth->h_proto)))
return eth->h_proto; /* ETH_P_IP, ETH_P_IPV6, ETH_P_ARP 등 */
/* 802.2 LLC 프레임 처리 (레거시) */
return htons(ETH_P_802_2);
}
dev_set_mtu() / dev_validate_mtu()
dev_set_mtu()는 RTNL lock 하에서 min_mtu/max_mtu 범위를 먼저 검증한 뒤, 드라이버의 ndo_change_mtu 콜백을 호출합니다. 코어 검증과 드라이버 검증이 분리되어 있습니다.
/* 커널 소스 분석: dev_set_mtu 내부 (net/core/dev.c) */
int dev_set_mtu(struct net_device *dev, int new_mtu)
{
int err, old_mtu;
ASSERT_RTNL();
/* 코어 범위 검증 (드라이버가 probe 시 설정한 min/max) */
err = dev_validate_mtu(dev, NULL, new_mtu, NULL);
if (err)
return err;
if (!netif_device_present(dev))
return -ENODEV;
old_mtu = dev->mtu;
err = __dev_set_mtu(dev, new_mtu);
if (err)
return err;
if (dev->mtu != old_mtu)
call_netdevice_notifiers(NETDEV_CHANGEMTU, dev);
return 0;
}
/* 코어 MTU 범위 검증 */
int dev_validate_mtu(struct net_device *dev, struct nlattr *tb[],
int new_mtu, struct netlink_ext_ack *extack)
{
int min_mtu = dev->min_mtu ? dev->min_mtu : ETH_MIN_MTU;
int max_mtu = dev->max_mtu ? dev->max_mtu : INT_MAX;
if (new_mtu < min_mtu || new_mtu > max_mtu)
return -EINVAL;
return 0;
}
dev_change_flags() 내부
dev_change_flags()는 IFF_UP, IFF_PROMISC, IFF_ALLMULTI 등 디바이스 플래그 변경을 처리하는 중앙 함수입니다.
/* 커널 소스 분석: dev_change_flags 핵심 경로 (net/core/dev.c) */
int dev_change_flags(struct net_device *dev, unsigned int flags,
struct netlink_ext_ack *extack)
{
unsigned int old_flags = dev->flags;
int ret;
ret = __dev_change_flags(dev, flags, extack);
if (ret < 0)
return ret;
/* 변경 통지 */
__dev_notify_flags(dev, old_flags, 0);
return ret;
}
int __dev_change_flags(struct net_device *dev, unsigned int flags,
struct netlink_ext_ack *extack)
{
unsigned int changes = flags ^ dev->flags;
/* IFF_UP 토글 처리 */
if (changes & IFF_UP) {
if (flags & IFF_UP)
return __dev_open(dev, extack); /* ndo_open 호출 */
else
__dev_close_many(&single); /* ndo_stop 호출 */
}
/* IFF_PROMISC 변경: 참조 카운트 조정 */
if (changes & IFF_PROMISC) {
int inc = (flags & IFF_PROMISC) ? 1 : -1;
dev_set_promiscuity(dev, inc);
}
/* IFF_ALLMULTI 변경: 참조 카운트 조정 */
if (changes & IFF_ALLMULTI) {
int inc = (flags & IFF_ALLMULTI) ? 1 : -1;
dev_set_allmulti(dev, inc);
}
return 0;
}
/* 변경 완료 후 통지 */
void __dev_notify_flags(struct net_device *dev, unsigned int old_flags,
unsigned int gchanges)
{
unsigned int changes = dev->flags ^ old_flags;
if (changes)
rtmsg_ifinfo(RTM_NEWLINK, dev, changes, GFP_KERNEL);
if (changes & IFF_UP)
call_netdevice_notifiers(
(dev->flags & IFF_UP) ? NETDEV_UP : NETDEV_DOWN, dev);
if (changes & ~(IFF_UP | IFF_PROMISC | IFF_ALLMULTI | IFF_VOLATILE))
call_netdevice_notifiers(NETDEV_CHANGE, dev);
}
netif_receive_skb() / __netif_receive_skb_core() 내부 처리 순서
수신된 sk_buff가 프로토콜 핸들러에 전달되기까지의 전체 처리 파이프라인(Pipeline)입니다. 각 단계에서 패킷이 가로채지거나 소비될 수 있습니다.
/* 커널 소스 분석: __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의 네임스페이스 생성/삭제 지연을 크게 줄일 잠재력이 있습니다.
CONFIG_DEBUG_NET_SMALL_RTNL플래그로 보호되며 기본 비활성- RCU화된 FIB 제어 경로, 네임스페이스화된 IPv4 주소 해시 테이블,
rtnl_{new,del,set}link()에서 검증 로직을 잠금 밖으로 분리 - phonet 핸들러의 RCU 전환, IPv4 주소 조작의 per-netns RTNL 전환
- v6.14에서도 계속 확장 — Paolo Abeni가 "이번 사이클 가장 흥미로운 작업"으로 소개
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 '{}'
- 기존
/proc/net/softnet_stat,/sys/class/net/*/queues/*를 대체/보완 - threaded NAPI일 때 kthread PID, IRQ 번호, 큐 매핑 노출
PHY link topology 추적 (v6.12)
v6.12에서 PHY link topology 인프라가 추가되어 하나의 net_device에 연결된 여러 PHY(예: QSFP 멀티-포트, switchdev PCS, PSE 등)의 위상을 커널이 공식적으로 추적합니다. ETHTOOL_MSG_PHY_GET을 통해 phyindex가 유저스페이스로 노출됩니다.
- MAC ↔ PHY ↔ SFP 모듈을 트리 구조로 표현
ethtool --show-pse,--set-pse로 PoE 제어 가능 (v6.10+)- MAC Merge(802.3br) 레이어 통계도 link topology 상에서 aggregated/express/preemptable로 분리 조회 가능
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)
- v6.12: devmem TCP RX 본격 병합 — NIC 헤더 분리 필요, 페이로드(Payload)는 GPU/가속기 DMABUF로 직접 전달
- v6.14: Broadcom BNXT devmem TCP 드라이버 지원
- v6.15: io_uring zerocopy receive 병합 (호스트 메모리 우선)
- devmem TCP TX는 v6.16 사이클 예정
기타 주요 변화
- v6.10: vduse 네트워크 디바이스 지원 — 유저스페이스에서 vhost-user 스타일로 NIC 에뮬레이션
- v6.10: TC classifier offload 경로에서 HW-only 모드 시 SW 처리 완전 생략으로 지연 감소
- v6.11: VirtIO-net AF_XDP Rx zero-copy + Byte Queue Limits 지원
- v6.12:
fib_rules에 DSCP selector 추가, bonding의 xfrm state offload - v6.13: TX H/W shaping을 유연하게 설정하는 netlink generic 인터페이스
- v6.14: Intel IGB AF_XDP zero-copy, XDP socket 코드 리팩터링
- v6.15: Intel ICE E830, MLX5 200Gbps per-lane 링크 모드 지원
참고 링크
- netdev 연간 리뷰 2024: netdev in 2024
- PHY link topology 문서: kernel.org — phy-link-topology
- Phoronix Linux 6.13 네트워킹: Linux 6.13 Networking
- Phoronix Linux 6.15 네트워킹: Linux 6.15 Networking
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 지원이 필요합니다.
- TX 경로: 소켓(Socket) 버퍼가 아닌 DMA-BUF 메모리를 직접 참조해
skb복사 없이 전송 - io_uring zerocopy receive(v6.15)와 함께 완전한 제로-카피 왕복 경로 구성
- v6.16 이후 DMA-BUF 기반 io_uring zero-copy RX도 확장 예정
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 데이터 처리가 커널 내부에서 이루어집니다.
- 모듈명:
ovpn—CONFIG_OVPN으로 빌드 - 암호화(Encryption)/복호화(Decryption)를 커널에서 직접 처리해 컨텍스트 스위칭(Context Switching) 오버헤드(Overhead) 제거
- 기존
tun기반 OpenVPN과 호환되며 점진적 전환 가능
DCCP 프로토콜 제거 (v6.16)
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) 커널 후보입니다.
- 소켓 수신 잠금의 NUMA 지역성 개선으로 크로스-NUMA 접근 감소
- UDP 소켓 연결 테이블의 캐시 라인 정렬 최적화
- Intel
iceE830, MLX5 200Gbps per-lane 지원 등 신규 하드웨어
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)
- v6.16: TCP 수신 버퍼 자동 조정(auto-tuning) 개선 — 고대역폭(Bandwidth) 단일 플로우에서 처리량 크게 향상
- v6.16: Realtek RTL8127A 10GbE 컨트롤러 드라이버 추가
- v6.17: tun/virtio-net UDP 터널 GSO 지원 — 가상화 환경 오프로드 효율 향상
- v6.17: per-NAPI 스레드 설정 교착 상태 방지 패치(Patch) 반영
- v6.18: Accurate ECN(Explicit Congestion Notification) TCP 지원 — 혼잡 제어(Congestion Control) 정밀도 향상
- v6.19: MPTCP 수신 백로그 처리 최적화 — 단일 서브플로우 모드에서 수신 성능 개선
- v6.19: Airoha RISC-V NPU 패킷 처리 유닛 지원
참고 링크 (v6.16~v6.19)
- Phoronix Linux 6.16 네트워킹: Linux 6.16 Networking
- Phoronix Linux 6.18 네트워킹: Linux 6.18 Networking
- Phoronix Linux 6.19 네트워킹: Linux 6.19 Networking (4x 향상)
- io_uring Zero Copy Rx 문서: kernel.org — iou-zcrx
- Linux 6.16 커널 뉴비스: kernelnewbies.org — Linux_6.16
관련 문서
- 디바이스 드라이버 — 전체 드라이버 프레임워크 문맥
- 네트워크 스택 — 커널 패킷 경로와 NAPI 배경
- TUN/TAP — 가상 인터페이스 구현 상세
- BPF/XDP — 드라이버 레벨 초고속 패킷 처리
- AF_XDP — 유저스페이스 zero-copy 경로
- 네트워크 드라이버 구현 가이드 — net_device_ops 기반 구현 실습 절차
- RX/TX 심화 & 오프로드 — NAPI 데이터 경로, BQL/LLTX, XDP, TC 오프로드
- 네트워크 드라이버 디버깅(Debugging) & 체크리스트 — devlink health, 회귀 테스트, 트러블슈팅
- NIC 드라이버 벤더 매트릭스 — 제조사별 드라이버 구조·성능 비교