NAPI (New API) — 네트워크 패킷(Packet) 처리

NAPI는 리눅스 커널의 핵심 수신 경로 최적화 메커니즘입니다. 인터럽트(Interrupt) 폭풍을 방지하고 고속 네트워크 환경에서 최대 처리량(Throughput)을 달성하기 위해 폴링(Polling) 기반 배치 처리를 활용합니다. napi_struct의 내부 구조부터 드라이버 구현 패턴, 멀티큐 스케일링, 스레드(Thread) NAPI, NAPI 메모리 관리(napi_alloc_skb, page_pool), 해시 테이블(Hash Table) 기반 소켓(Socket) 바인딩, IRQ 일시 중단(Suspension), 버지 폴링, XDP 연동까지 전 영역을 상세히 다룹니다.

핵심 요약

  • 인터럽트 → 폴링 전환 — NAPI는 고속 네트워크에서 인터럽트 폭풍(Interrupt Storm)을 방지하기 위해, 패킷 수신 시 인터럽트를 끄고 폴링(Polling) 모드로 전환하여 배치 처리합니다.
  • 버짓(Budget) 기반 스케줄링 — 각 NAPI 인스턴스는 한 번의 폴링에서 처리할 최대 패킷 수(기본 64)를 할당받아 CPU 독점을 방지합니다.
  • GRO 통합 — Generic Receive Offload가 NAPI 폴링 루프 내에서 동작하여 작은 패킷들을 큰 패킷으로 병합, 프로토콜 스택 오버헤드를 줄입니다.
  • 멀티큐·스레드 NAPI — NIC 큐마다 독립 napi_struct를 두어 코어별 병렬 처리하며, 스레드 NAPI(5.10+)로 PREEMPT_RT 환경에서도 안정적으로 동작합니다.
  • XDP 연동 — NAPI 폴링 최상단에서 XDP 프로그램이 실행되어 skb 할당 없이 라인 레이트에 근접한 패킷 처리가 가능합니다.

단계별 이해

  1. napi_struct 구조 파악
    napi_struct의 핵심 필드(state, weight, poll 함수 포인터)와 생명주기(init → enable → schedule → poll → complete)를 이해합니다.
  2. 폴링 루프 추적
    하드웨어 인터럽트 → napi_schedule() → softirq의 net_rx_action() → 드라이버 poll()napi_complete_done() 순서를 따라갑니다.
  3. GRO·XDP 경로 이해
    napi_gro_receive()의 패킷 병합 로직과 bpf_prog_run_xdp()의 early-path 처리 메커니즘을 학습합니다.
  4. 성능 튜닝 실습
    버짓 조정, IRQ affinity 설정, Busy Polling, 스레드 NAPI 활성화 등 환경별 최적화 파라미터를 실습합니다.

NAPI 개요와 탄생 배경

인터럽트 기반 수신의 한계

초기 리눅스 네트워크 드라이버는 패킷이 도착할 때마다 하드웨어 인터럽트(IRQ)를 발생시키고, 각 인터럽트 핸들러(Handler)에서 패킷을 직접 처리했습니다. 100 Mbps 시대에는 이 방식으로 충분했으나, 1 Gbps 이상의 고속 네트워크에서는 치명적인 문제가 드러났습니다.

링크 속도별 초당 최대 패킷 수

링크 속도최소 프레임(64B) 기준 PPS인터럽트/초 (순수 인터럽트 방식)실용 가능 여부
100 Mbps 약 148,800 pkt/s 약 148,800 IRQ/s 가능 (초기 리눅스 커널 기준)
1 Gbps 약 1,488,000 pkt/s 약 1,488,000 IRQ/s CPU 포화 위험
10 Gbps 약 14,880,000 pkt/s 약 14,880,000 IRQ/s 불가 (인터럽트 오버헤드(Overhead)만으로 CPU 전부 소모)
25 Gbps 약 37,200,000 pkt/s 약 37,200,000 IRQ/s NAPI 필수
100 Gbps 약 148,800,000 pkt/s 약 148,800,000 IRQ/s NAPI + 멀티큐 + XDP 필수
인터럽트 폭풍(Interrupt Storm) 문제: 1 Gbps 링크에서 최소 크기(64 바이트) 패킷이 초당 약 148만 개 유입되면, 인터럽트 처리만으로 CPU가 포화 상태가 됩니다. 실제 패킷 처리 로직은 커녕 인터럽트 컨텍스트 전환 오버헤드만으로 전체 CPU 사이클을 소진합니다.

pre-NAPI vs NAPI 코드 경로 비교

항목pre-NAPI (인터럽트 기반)NAPI (하이브리드 폴링)
패킷 처리 트리거 매 패킷마다 하드 IRQ 발생 첫 패킷만 하드 IRQ, 이후 softIRQ 폴링
패킷당 인터럽트 수 1 IRQ / 패킷 1 IRQ / 수십~수백 패킷 배치
컨텍스트 전환 매우 빈번 (하드 IRQ 컨텍스트) 최소화 (softIRQ 컨텍스트 배치 처리)
캐시(Cache) 지역성 불량 (임의 타이밍 인터럽트) 양호 (배치 처리로 캐시 핫)
GRO/배치 최적화 불가 가능 (gro_hash 버킷 활용)
라이브록 위험 높음 낮음 (버짓/시간 제한)
구현 복잡도 단순 중간 (드라이버 NAPI API 사용)
대표 커널 코드 netif_rx() 직접 호출 napi_schedule()net_rx_action()

인터럽트 완화 전후 CPU 사용률 비교

0% 20% 40% 60% 80% 100% IRQ 오버헤드 70% 실처리 10% pre-NAPI 1Gbps, 64B pkt IRQ 5% 실처리 60% NAPI 1Gbps, 64B pkt 100% 포화 pre-NAPI 10Gbps, 64B pkt IRQ 30% 실처리 50% NAPI+멀티큐 10Gbps, 64B pkt IRQ 오버헤드 실제 처리 유휴 CPU 사용률 분포: pre-NAPI vs NAPI 비교 (개념적 수치)

라이브록(Livelock) 발생 시나리오

라이브록은 시스템이 실제로 유용한 작업을 처리하지 못하고 인터럽트 처리에만 매달리는 상태입니다. pre-NAPI 환경에서 다음과 같이 발생합니다:

  1. 패킷 A 도착 → IRQ 핸들러 진입, 패킷 처리 시작
  2. 패킷 처리 도중 패킷 B, C, D가 연속 도착 → 새 IRQ 발생
  3. 현재 IRQ 핸들러 종료 즉시 다음 IRQ 처리 시작
  4. IRQ 처리가 끊이지 않아 사용자 공간(User Space) 프로세스(Process), 타이머(Timer), TCP 재전송(Retransmission) 등이 실행 불가
  5. 결과: 패킷은 도착하지만 소켓 버퍼(Buffer)에 전달되지 않아 TCP 타임아웃 발생

NAPI는 첫 번째 IRQ에서 이후 IRQ를 비활성화하고 softIRQ 컨텍스트에서 배치 처리함으로써 이 라이브록을 원천 차단합니다. netdev_budgetnetdev_budget_usecs 제한으로 softIRQ도 CPU를 독점하지 못하도록 합니다.

softnet_data 구조체(Struct)와 NAPI의 관계

softnet_data는 per-CPU 자료구조로, NAPI 폴링의 실제 큐 역할을 합니다. 커널 소스 include/linux/netdevice.h에 정의되어 있습니다.

/* include/linux/netdevice.h (Linux 6.x, 주요 필드만 발췌) */
struct softnet_data {
    /* poll_list: NAPI 인스턴스들이 등록되는 링크드 리스트.
       net_rx_action()이 이 리스트를 순회하며 각 napi_struct를 폴링함 */
    struct list_head    poll_list;

    /* output_queue: TX 완료 처리 대기 큐 */
    struct Qdisc       *output_queue;
    struct Qdisc      **output_queue_tailp;

    /* completion_queue: 해제 대기 중인 sk_buff 체인.
       softIRQ에서 일괄 해제하여 IRQ 컨텍스트에서의 해제 비용 감소 */
    struct sk_buff     *completion_queue;

    /* input_pkt_queue: RPS가 다른 CPU로 패킷을 전달할 때 사용하는 큐 */
    struct sk_buff_head input_pkt_queue;

    /* backlog: 단일 큐 NIC 또는 loopback에서 사용하는 기본 NAPI 인스턴스 */
    struct napi_struct  backlog;

    /* time_squeeze: 버짓/시간 초과로 인해 소프트IRQ가 조기 종료된 횟수.
       /proc/net/softnet_stat의 3번째 열 */
    unsigned int        time_squeeze;

    /* received_rps: RPS를 통해 이 CPU로 전달된 패킷 수 */
    unsigned int        received_rps;

    /* dropped: backlog 큐 초과로 드롭된 패킷 수.
       /proc/net/softnet_stat의 2번째 열 */
    unsigned int        dropped;

#ifdef CONFIG_RPS
    /* rps_ipi_list: RPS IPI 대기 리스트 */
    struct softnet_data *rps_ipi_list;
#endif
};

NAPI 커널 버전별 주요 개선 이력

커널 버전주요 개선 사항관련 개발자
2.4.20 (2001) NAPI 최초 도입. 인터럽트 완화 기본 메커니즘 Alexey Kuznetsov, Jamal Hadi Salim
2.6.x (2003~) NAPI 표준화, netif_napi_add() API 확립, softnet_data 통합 Jeff Garzik, David S. Miller
3.x (2011~) GRO(Generic Receive Offload) 통합, napi_gro_receive() 추가, gro_list 구조 Herbert Xu
3.11 (2013) 버지 폴링(Busy Polling) 추가, SO_BUSY_POLL, napi_busy_loop() Eliezer Tamir
4.5 (2016) XDP(eXpress Data Path) native NAPI 연동, bpf_prog_run_xdp() Tom Herbert, Jesper Dangaard Brouer
5.3 (2019) napi_defer_hard_irqs, gro_flush_timeout 도입으로 IRQ 지연(Latency) 제어 강화 Paolo Abeni
5.10 (2020) 스레드 NAPI(Threaded NAPI) 추가, napi_set_threaded(), dev_set_threaded() Wei Wang
5.11 (2021) SO_PREFER_BUSY_POLL, SO_BUSY_POLL_BUDGET 추가 Björn Töpel
6.x (2022~) netif_napi_add_config(), page_pool NAPI 통합 강화, gro_hash 버킷 확장 Jakub Kicinski, Yunsheng Lin

NAPI의 주요 설계 원칙

원칙구현 방법효과
인터럽트 완화 첫 패킷만 인터럽트, 나머지는 폴링 인터럽트 오버헤드 최소화
공정 배치 처리 버짓(기본 300) 내 다중 패킷 처리 처리량 극대화, 지연 조절 가능
다중 NIC 공정성(Fairness) poll_list 라운드-로빈 순회 하나의 NIC가 독점 방지
백프레셔(backpressure) 버짓 소진 시 재스케줄, 처리 유예 시스템 과부하 방지

NAPI 처리 흐름

하드웨어 인터럽트(IRQ) IRQ 핸들러 napi_schedule() IRQ 비활성화 NET_RX_SOFTIRQ net_rx_action() 버짓: 300 드라이버 poll() 패킷 배치 수신 napi_gro_receive() 패킷 소진 napi_complete_done() 버짓 소진 재스케줄(MISSED) IRQ 재활성화 후 대기 NAPI 하이브리드 인터럽트-폴링 흐름

핵심 자료구조: napi_struct

napi_struct는 NAPI의 핵심 자료구조로, 각 수신 큐(RX queue)마다 하나씩 존재합니다. 커널 소스의 include/linux/netdevice.h에 정의되어 있습니다.

/* include/linux/netdevice.h (Linux 6.x 기준, 일부 생략) */
struct napi_struct {
    /* poll_list: softirq의 NET_RX_SOFTIRQ가 순회하는 링크드 리스트 */
    struct list_head    poll_list;

    /* state: 원자적으로 조작되는 상태 비트맵 (NAPI_STATE_*) */
    unsigned long       state;

    /* weight: 한 번의 poll() 호출에서 처리할 최대 패킷 수 (버짓) */
    int                 weight;

    /* defer_hard_irqs_count: 지연된 하드 IRQ 재활성화 카운터 */
    int                 defer_hard_irqs_count;

    /* gro_bitmask: GRO 활성 버킷 비트마스크 (gro_hash 중 유효 버킷 표시) */
    unsigned long       gro_bitmask;

    /* poll: 드라이버가 등록하는 폴링 함수 포인터 */
    int                 (*poll)(struct napi_struct *, int);

#ifdef CONFIG_NETPOLL
    struct netpoll_info __rcu *napi_id_list;
#endif

    /* dev: 이 NAPI가 속한 net_device */
    struct net_device   *dev;

    /* gro_hash: GRO 병합 대기 중인 skb 체인 (GRO_HASH_BUCKETS=8 버킷) */
    struct gro_list     gro_hash[GRO_HASH_BUCKETS];

    /* skb: 현재 처리 중인 skb (GRO 경로에서 사용) */
    struct sk_buff      *skb;

    /* rx_list: 처리 완료된 skb들의 임시 큐 */
    struct list_head    rx_list;
    int                 rx_count;

    /* napi_id: NAPI 인스턴스 고유 ID (busy polling 식별용, MIN_NAPI_ID 이상) */
    unsigned int        napi_id;

    /* threaded: 스레드 NAPI 사용 여부 */
    u8                  threaded;

    /* thread: 스레드 NAPI 전용 커널 스레드 포인터 */
    struct task_struct  *thread;

    /* dev_list: net_device의 napi_list에 연결 */
    struct list_head    dev_list;

    /* poll_owner: 현재 poll()을 실행 중인 CPU (-1이면 유휴) */
    int                 poll_owner;
};
필드 상세 설명
  • poll_list 소프트IRQ의 net_rx_action()이 순회하는 링크드 리스트 노드. napi_schedule() 호출 시 per-CPU softnet_data.poll_list에 추가됩니다.
  • state 비트마스크 상태 필드. NAPI_STATE_SCHED, NAPI_STATE_DISABLE, NAPI_STATE_NPSVC, NAPI_STATE_MISSED 등의 플래그를 원자적(Atomic)으로 관리합니다.
  • weight 한 번의 poll() 호출에서 처리 가능한 최대 패킷 수. 일반 NIC는 기본값 NAPI_POLL_WEIGHT(64)이나, netif_napi_add()로 재설정 가능. 소프트웨어 장치(loopback 등)는 더 높은 값 사용.
  • gro_hash GRO 병합 대기 중인 skb들을 버킷별로 관리하는 해시 테이블. flush 전까지 동일 플로우 패킷들이 여기서 병합됩니다.
  • napi_id 커널이 할당하는 고유 식별자. SO_BUSY_POLL 소켓 옵션에서 이 ID로 특정 NAPI를 지목하여 직접 폴링합니다.
  • poll_owner 현재 poll()을 실행 중인 CPU 번호. -1이면 유휴 상태(Idle State). SMP 환경에서 동시 실행 방지에 사용.

gro_hash 버킷 구조 상세

GRO는 napi_struct 내의 gro_hash 배열에 패킷을 버킷 단위로 보관합니다. 버킷 수는 GRO_HASH_BUCKETS(8)이며, 해시(Hash) 키로 플로우를 분산시킵니다.

/* net/core/dev.c (Linux 6.x) */
#define GRO_HASH_BUCKETS  8

struct gro_list {
    /* list: 동일 해시 버킷의 GRO 대기 skb 체인 */
    struct list_head  list;
    /* count: 버킷 내 skb 개수 (MAX_GRO_SKBS=8 초과 시 flush) */
    int               count;
};

/* NAPI_GRO_CB: skb->cb 영역에 GRO 전용 메타데이터 저장 */
struct napi_gro_cb {
    /* data_offset: skb->data에서 헤더 시작까지의 오프셋 */
    unsigned int   data_offset;

    /* flush: 즉시 flush 필요 여부 (순서 역전, 헤더 불일치 등) */
    u8             flush;

    /* flush_id: 병합 후 flush할 패킷 식별 ID */
    u16            flush_id;

    /* count: 이 GRO skb에 병합된 패킷 수 */
    u16            count;

    /* same_flow: 동일 플로우로 판별된 경우 true */
    u8             same_flow;

    /* ip_fixedid: IP ID가 고정(incrementing)인지 여부 */
    u8             ip_fixedid;

    /* encap_mark: 터널 헤더 처리 중임을 표시 */
    u8             encap_mark;

    /* csum_valid: 체크섬 이미 검증됨 */
    u8             csum_valid;

    /* is_atomic: 원자적 GRO (단편화 없음) */
    u8             is_atomic;

    /* tot_len: 병합된 전체 페이로드 길이 */
    unsigned int   tot_len;
};

#define NAPI_GRO_CB(skb) ((struct napi_gro_cb *)(skb)->cb)

napi_struct와 net_device의 연결 관계

/* include/linux/netdevice.h, net/core/dev.c — 간략화 */
/* net_device는 등록된 모든 NAPI 인스턴스를 napi_list로 추적 */
struct net_device {
    /* ... */
    /* napi_list: 이 디바이스의 모든 napi_struct 링크드 리스트 */
    struct list_head    napi_list;
    /* ... */
};

/* netif_napi_add() 내부에서 dev->napi_list에 추가 */
void netif_napi_add(struct net_device *dev, struct napi_struct *napi, ...)
{
    /* napi_id 할당: napi_gen_id()로 전역 카운터에서 증가 */
    napi->napi_id = napi_gen_id();   /* >= MIN_NAPI_ID (0x10000) */

    /* napi_hash에 등록 (busy polling 조회용 해시 테이블) */
    napi_hash_add(napi);

    /* dev->napi_list에 연결 */
    list_add_rcu(&napi->dev_list, &dev->napi_list);
    set_bit(NAPI_STATE_LISTED, &napi->state);
}

/* 디바이스의 모든 NAPI 인스턴스 순회 예 */
static void mynic_enable_all_napi(struct net_device *dev)
{
    struct napi_struct *napi;
    list_for_each_entry(napi, &dev->napi_list, dev_list)
        napi_enable(napi);
}

napi_id 할당 메커니즘

napi_id는 버지 폴링에서 특정 NAPI 인스턴스를 찾기 위한 키입니다. 전역 해시 테이블 napi_hash에 등록되어 napi_by_id()로 조회됩니다.

/* net/core/dev.c — 간략화 */
/* MIN_NAPI_ID: 소켓 식별자(sk_napi_id)와 구분을 위한 최솟값 */
#define MIN_NAPI_ID     ((unsigned int)(NR_CPUS + 1))

/* 전역 NAPI ID 카운터 (원자적 증가) */
static atomic_t napi_gen_id_counter = ATOMIC_INIT(MIN_NAPI_ID);

static unsigned int napi_gen_id(void)
{
    unsigned int id;
    do {
        id = atomic_inc_return(&napi_gen_id_counter);
        if (id < MIN_NAPI_ID)
            id = atomic_inc_return(&napi_gen_id_counter);
    } while (napi_by_id(id));  /* 충돌 시 재시도 */
    return id;
}

NAPI_STATE_* 플래그 테이블

플래그비트의미조작 함수
NAPI_STATE_SCHED 0 poll_list에 등록됨 (스케줄됨). 중복 스케줄 방지용 원자 세팅 napi_schedule_prep(), napi_complete_done()
NAPI_STATE_MISSED 1 버짓 소진 후 새 패킷 도착. 폴링 재개 필요 표시 napi_schedule()(폴링 중 호출 시), napi_complete_done()
NAPI_STATE_DISABLE 2 napi_disable() 호출 상태. poll_list 추가 차단 napi_disable(), napi_enable()
NAPI_STATE_NPSVC 3 NetPoll 서비스 중 (네트워크 콘솔용) 내부 NetPoll 코드
NAPI_STATE_LISTED 4 dev->napi_list에 연결됨 netif_napi_add(), netif_napi_del()
NAPI_STATE_NO_BUSY_POLL 5 버지 폴링 비활성화 netif_napi_add() 내 조건부 설정
NAPI_STATE_IN_BUSY_POLL 6 현재 버지 폴링 중 (소프트IRQ와 동시 실행 방지) napi_busy_loop()
NAPI_STATE_THREADED 7 스레드 NAPI 모드 활성화 napi_set_threaded()
NAPI_STATE_SCHED_THREADED 8 스레드 NAPI 스레드가 깨워진 상태 __napi_schedule_threaded()

NAPI API와 동작 원리

초기화/해제 API

/* include/linux/netdevice.h, net/core/dev.c — 간략화 */
/* NIC의 RX 큐에 NAPI 인스턴스 등록 */
void netif_napi_add(struct net_device *dev,
                    struct napi_struct *napi,
                    int (*poll)(struct napi_struct *, int),
                    int weight);

/* 멀티큐 드라이버: 큐 인덱스와 함께 등록 (Linux 6.1+) */
void netif_napi_add_config(struct net_device *dev,
                           struct napi_struct *napi,
                           int (*poll)(struct napi_struct *, int),
                           int weight, int napi_id);

/* tx 경로 전용 NAPI (weight = NAPI_POLL_WEIGHT) */
void netif_napi_add_tx(struct net_device *dev,
                       struct napi_struct *napi,
                       int (*poll)(struct napi_struct *, int));

/* NAPI 인스턴스 해제 (napi_list에서 제거, napi_hash에서 제거) */
void netif_napi_del(struct napi_struct *napi);

/* NAPI 활성화: NAPI_STATE_DISABLE 클리어 → 스케줄 허용 */
void napi_enable(struct napi_struct *napi);

/* NAPI 비활성화: 진행 중인 poll() 완료 대기 후 DISABLE 비트 설정 */
void napi_disable(struct napi_struct *napi);

/* NAPI poll이 완전히 중단될 때까지 대기 (RCU 동기화 포함) */
void napi_synchronize(const struct napi_struct *napi);

napi_disable() 내부 구현

/* net/core/dev.c */
void napi_disable(struct napi_struct *napi)
{
    unsigned long val, new;

    might_sleep();
    set_bit(NAPI_STATE_DISABLE, &napi->state);

    /* SCHED 또는 SCHED_THREADED 비트가 클리어될 때까지 대기
       (진행 중인 poll()이 완료될 때까지 폴링 대기) */
    do {
        val = READ_ONCE(napi->state);
        if (!(val & (NAPIF_STATE_SCHED | NAPIF_STATE_SCHED_THREADED)))
            break;
        /* 짧게 슬립하여 CPU 낭비 방지 (usleep_range: 200~500μs) */
        usleep_range(200, 500);
    } while (1);

    /* IN_BUSY_POLL도 해소될 때까지 대기 */
    do {
        val = READ_ONCE(napi->state);
        if (!(val & NAPIF_STATE_IN_BUSY_POLL))
            break;
        usleep_range(200, 500);
    } while (1);

    clear_bit(NAPI_STATE_DISABLE, &napi->state);
}

napi_synchronize() vs napi_disable() 차이점

함수동작사용 시점이후 상태
napi_disable() poll() 완료 대기 + NAPI 영구 비활성화 (새 스케줄 불가) 드라이버 stop(), 디바이스 제거 시 NAPI 완전 중단, IRQ 비활성화 전에 호출 권장
napi_synchronize() 현재 진행 중인 poll() 완료만 대기, 비활성화 안 함 NAPI를 중단하지 않고 완료 시점 동기화 필요 시 NAPI 계속 동작 가능, 설정 변경 후 동기화에 활용

스케줄링 API와 __napi_schedule() 내부 구현

/* include/linux/netdevice.h, net/core/dev.c — 간략화 */
/* IRQ 핸들러에서 NAPI 스케줄 (인터럽트 컨텍스트에서 안전) */
void napi_schedule(struct napi_struct *napi);

/* IRQ 비활성화된 상태에서 스케줄 (local_irq_save 생략으로 더 빠름) */
void napi_schedule_irqoff(struct napi_struct *napi);

/* napi_schedule의 실제 구현 */
static inline void napi_schedule(struct napi_struct *napi)
{
    /* NAPI_STATE_SCHED 비트를 원자적으로 세팅 → 이미 스케줄된 경우 무시 */
    if (napi_schedule_prep(napi))
        __napi_schedule(napi);
}

/* __napi_schedule: 실제로 per-CPU poll_list에 추가 */
void __napi_schedule(struct napi_struct *n)
{
    unsigned long flags;
    struct softnet_data *sd;

    local_irq_save(flags);
    sd = this_cpu_ptr(&softnet_data);

    /* 스레드 NAPI 활성화 시 커널 스레드 깨우기 */
    if (test_bit(NAPI_STATE_THREADED, &n->state)) {
        __napi_schedule_threaded(n);
    } else {
        /* per-CPU poll_list에 추가 (tail) */
        list_add_tail(&n->poll_list, &sd->poll_list);
        /* NET_RX_SOFTIRQ 트리거 */
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);
    }
    local_irq_restore(flags);
}

완료 API와 napi_complete_done() 내부 로직

/* net/core/dev.c — 간략화 */
/* poll() 내에서 패킷 소진 시 호출 */
bool napi_complete_done(struct napi_struct *napi, int work_done)
{
    unsigned long flags, val, new_val;

    /* 1. GRO 버퍼 flush: 남은 병합 패킷을 상위 스택으로 전달 */
    if (napi->gro_bitmask)
        napi_gro_flush(napi, false);

    /* 2. gro_flush_timeout이 설정된 경우 타이머 재설정 */
    if (work_done) {
        if (READ_ONCE(napi->dev->gro_flush_timeout))
            hrtimer_start(&napi->timer,
                           READ_ONCE(napi->dev->gro_flush_timeout),
                           HRTIMER_MODE_REL_PINNED);
    }

    /* 3. NAPI_STATE_MISSED 확인: 폴링 중 새 IRQ가 왔는가? */
    local_irq_save(flags);
    val = READ_ONCE(napi->state);

    if (unlikely(val & NAPIF_STATE_MISSED)) {
        /* MISSED: 즉시 재스케줄 (GRO는 이미 flush됨) */
        __napi_schedule(napi);
        local_irq_restore(flags);
        return false;  /* IRQ 재활성화 금지 */
    }

    /* 4. NAPI_STATE_SCHED 클리어 → NAPI 완전 종료 */
    new_val = val & ~(NAPIF_STATE_MISSED | NAPIF_STATE_SCHED |
                      NAPIF_STATE_SCHED_THREADED | NAPIF_STATE_PREFER_BUSY_POLL);
    WRITE_ONCE(napi->state, new_val);
    local_irq_restore(flags);

    return true;  /* 드라이버는 HW IRQ 재활성화 필요 */
}

/* 단순화 버전 (work_done = 0으로 호출, GRO flush만 수행) */
static inline bool napi_complete(struct napi_struct *napi)
{
    return napi_complete_done(napi, 0);
}

API 호출 순서 다이어그램

단계드라이버 함수커널 NAPI API설명
1. probe mynic_probe() netif_napi_add() NAPI 등록, napi_id 할당, napi_hash 등록
2. open mynic_open() napi_enable() DISABLE 비트 클리어, 스케줄 허용
3. IRQ mynic_irq_handler() napi_schedule_irqoff() HW IRQ 마스크 후 softIRQ 큐 등록
4. softIRQ (커널 내부) net_rx_action() 버짓 내 NAPI 인스턴스 순차 폴링
5. poll mynic_poll() napi_gro_receive() 패킷 배치 수신, GRO 병합
6. complete mynic_poll() napi_complete_done() GRO flush, SCHED 비트 클리어, IRQ 재활성화
7. stop mynic_stop() napi_disable() poll() 완료 대기, DISABLE 비트 설정
8. remove mynic_remove() netif_napi_del() napi_list/napi_hash에서 제거

전형적인 IRQ 핸들러 패턴

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
static irqreturn_t mynic_irq_handler(int irq, void *data)
{
    struct mynic_rx_ring *ring = data;
    struct mynic_hw      *hw   = ring->hw;
    u32 status;

    status = mynic_read_reg(hw, MYNIC_IRQ_STATUS);
    if (!(status & MYNIC_RX_INT))
        return IRQ_NONE;

    /* 하드웨어 인터럽트 마스크 (이후 NAPI poll에서 처리) */
    mynic_disable_rx_irq(hw, ring->queue_idx);

    /* NAPI 스케줄: NAPI_STATE_SCHED가 이미 세팅된 경우 무시됨
       _irqoff: IRQ 이미 비활성화 상태이므로 local_irq_save 생략 가능 */
    napi_schedule_irqoff(&ring->napi);

    return IRQ_HANDLED;
}

softnet_data 구조체 상세 분석

softnet_data는 per-CPU 네트워크 처리 상태(Per-CPU Network Processing State)를 관리하는 핵심 구조체입니다. 각 CPU마다 하나씩 존재하며, NAPI poll_list, 패킷 입력 큐, 백로그(Backlog) NAPI 인스턴스, 다양한 통계 카운터를 포함합니다. net/core/dev.c에서 DEFINE_PER_CPU_ALIGNED로 정의되어 캐시 라인(Cache Line) 경계에 정렬됩니다.

/* net/core/dev.c (Linux 6.x) — softnet_data 정의 */
DEFINE_PER_CPU_ALIGNED(struct softnet_data, softnet_data);
EXPORT_PER_CPU_SYMBOL(softnet_data);

/* include/linux/netdevice.h — 구조체 전체 필드 (주요 필드 발췌) */
struct softnet_data {
    /* poll_list: 스케줄된 NAPI 인스턴스들의 연결 리스트.
       net_rx_action()이 이 리스트를 순회하며 각 napi_struct->poll()을 호출.
       local_irq_save() 보호 하에 추가/삭제 */
    struct list_head    poll_list;

    /* input_pkt_queue: RPS(Receive Packet Steering) 또는 netif_rx()로
       인입된 패킷을 임시 저장하는 큐. 다른 CPU에서 IPI를 통해 전달됨.
       backlog NAPI가 이 큐에서 패킷을 꺼내 처리 */
    struct sk_buff_head input_pkt_queue;

    /* process_queue: backlog NAPI의 poll 루프에서 처리 중인 패킷 큐.
       input_pkt_queue에서 splice로 옮겨와 lock 경합을 최소화 */
    struct sk_buff_head process_queue;

    /* backlog: per-CPU 디폴트 NAPI 인스턴스.
       poll 콜백은 process_backlog(). 단일 큐 NIC, loopback,
       netif_rx() 경로에서 사용 */
    struct napi_struct  backlog;

    /* output_queue / output_queue_tailp: TX completion 대기 큐.
       qdisc_run()에서 처리하지 못한 TX 패킷들의 Qdisc를 연결 */
    struct Qdisc       *output_queue;
    struct Qdisc      **output_queue_tailp;

    /* completion_queue: 해제 대기 중인 sk_buff 체인.
       IRQ 컨텍스트에서 직접 해제하지 않고 softIRQ에서 일괄 해제 */
    struct sk_buff     *completion_queue;

    /* === 통계 카운터 (per-CPU, 락 불필요) === */

    /* dropped: netif_rx() 경로에서 backlog 큐(input_pkt_queue) 초과 드롭.
       /proc/net/softnet_stat 1번째 열 */
    unsigned int        dropped;

    /* time_squeeze: net_rx_action()에서 버짓 또는 시간 초과로
       poll_list에 아직 작업이 남았는데 조기 종료된 횟수.
       /proc/net/softnet_stat 2번째 열 */
    unsigned int        time_squeeze;

    /* cpu_collision: TX에서 다른 CPU가 이미 Qdisc lock을 잡고 있어
       경합이 발생한 횟수. /proc/net/softnet_stat 3번째 열 */
    unsigned int        cpu_collision;

    /* received_rps: RPS IPI를 통해 이 CPU로 전달된 패킷 수.
       /proc/net/softnet_stat 4번째 열 */
    unsigned int        received_rps;

    /* flow_limit_count: flow limit 히트 횟수.
       특정 플로우가 CPU 시간을 독점하지 못하도록 제한.
       /proc/net/softnet_stat 5번째 열 */
    unsigned int        flow_limit_count;

#ifdef CONFIG_RPS
    /* rps_ipi_list: RPS IPI 전송 대기 중인 remote CPU 리스트.
       NET_RX_SOFTIRQ 처리 시작 시 IPI를 일괄 전송 */
    struct softnet_data *rps_ipi_list;

    /* input_queue_head/tail: flow limit 판단용 큐 위치 추적 */
    unsigned int        input_queue_head;
    unsigned int        input_queue_tail;
#endif

    /* xmit.recursion: TX 재진입 방지 카운터.
       dev_queue_xmit() 내부에서 nesting depth 추적 (최대 XMIT_RECURSION_LIMIT) */
    unsigned int        xmit_recursion;
};

per-CPU softnet_data와 NAPI poll_list 관계

CPU 0 — softnet_data poll_list (head) input_pkt_queue backlog (napi_struct) napi_struct (eth0 RXQ0) poll = e1000_poll napi_struct (eth0 RXQ1) poll = e1000_poll CPU 1 — softnet_data poll_list (head) input_pkt_queue backlog (napi_struct) napi_struct (eth1 RXQ0) poll = ixgbe_poll net_rx_action() 동작 1. this_cpu_ptr(&softnet_data) 획득 2. poll_list 순회하며 napi->poll() 호출 3. budget 소진 또는 2ms 경과 시 종료 backlog NAPI 인스턴스 poll = process_backlog() input_pkt_queue → process_queue splice 후 처리 per-CPU softnet_data는 각각 독립된 poll_list를 유지하여 CPU 간 lock 경합 없이 NAPI 폴링 수행
/proc/net/softnet_stat 열 매핑: 이 파일은 CPU당 한 줄씩, 16진수 값을 공백으로 구분하여 출력합니다.
열 번호softnet_data 필드의미높을 때 조치
1번째processed (총 처리 패킷)이 CPU가 처리한 총 네트워크 프레임 수
2번째droppedbacklog 큐 초과로 드롭된 패킷 수netdev_max_backlog 증가
3번째time_squeeze버짓/시간 초과로 조기 종료된 횟수netdev_budget / netdev_budget_usecs 증가
4번째~7번째(미사용)레거시 필드, 항상 0
8번째cpu_collisionTX Qdisc lock 경합 횟수TX 큐 수 증가, XPS 설정
9번째received_rpsRPS IPI로 전달된 패킷 수RPS 설정 확인, IRQ affinity 조정
10번째flow_limit_countflow limit 히트 횟수flow limit 임계값 조정
11번째softnet_backlog_len현재 backlog 큐 길이 (Linux 5.18+)

NAPI 핵심 함수 내부 구현

netif_napi_add() 내부 구현

netif_napi_add()는 드라이버가 NAPI 인스턴스를 등록할 때 호출하는 진입점(Entry Point)입니다. 내부에서 napi_struct의 모든 필드를 초기화하고, GRO 해시 버킷 생성, 해시 테이블 등록, 디바이스 연결까지 일괄 수행합니다.

/* net/core/dev.c (Linux 6.x) — netif_napi_add() 내부 구현 */
void netif_napi_add_weight(struct net_device *dev,
                           struct napi_struct *napi,
                           int (*poll)(struct napi_struct *, int),
                           int weight)
{
    /* 커널 소스 분석: poll_list 초기화 — 빈 리스트로 설정 */
    INIT_LIST_HEAD(&napi->poll_list);

    /* 커널 소스 분석: GRO 해시 버킷 초기화 (8개 버킷) */
    for (int i = 0; i < GRO_HASH_BUCKETS; i++) {
        INIT_LIST_HEAD(&napi->gro_hash[i].list);
        napi->gro_hash[i].count = 0;
    }
    napi->gro_bitmask = 0;

    /* 커널 소스 분석: hrtimer 초기화 — GRO flush 타이머 */
    hrtimer_init(&napi->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED);
    napi->timer.function = napi_watchdog;

    /* 커널 소스 분석: 핵심 필드 설정 */
    napi->poll = poll;              /* 드라이버 폴링 콜백 */
    napi->weight = weight;          /* 기본값 NAPI_POLL_WEIGHT = 64 */
    napi->dev = dev;                /* 소속 net_device */
    napi->poll_owner = -1;          /* 현재 poll 실행 중인 CPU (-1 = 없음) */
    napi->list_owner = -1;

    /* 커널 소스 분석: 상태 비트 초기화 — SCHED 비트 설정 (비활성 상태)
       napi_enable()이 호출될 때까지 스케줄 불가 */
    set_bit(NAPI_STATE_SCHED, &napi->state);
    set_bit(NAPI_STATE_NPSVC, &napi->state);

    /* 커널 소스 분석: napi_hash_add() — 전역 해시 테이블에 등록 */
    napi_hash_add(napi);

    /* 커널 소스 분석: dev->napi_list에 RCU 보호 하에 추가 */
    list_add_rcu(&napi->dev_list, &dev->napi_list);

    /* 커널 소스 분석: 디바이스가 threaded NAPI를 사용하면 상속 */
    napi_set_threaded(napi, dev->threaded);
}

napi_hash_add() 내부 동작

napi_hash_add()는 NAPI 인스턴스에 전역 고유 ID를 할당하고, 해시 테이블(Hash Table)에 삽입합니다. 이 ID는 버지 폴링(Busy Polling)에서 napi_by_id()를 통해 소켓과 NAPI 인스턴스를 연결하는 데 사용됩니다.

/* net/core/dev.c — napi_hash_add() 내부 */
static void napi_hash_add(struct napi_struct *napi)
{
    /* 커널 소스 분석: IDA(ID Allocator) 기반 ID 할당
       MIN_NAPI_ID(NR_CPUS+1)부터 시작하여 소켓 ID와 충돌 방지 */
    if (test_bit(NAPI_STATE_NO_BUSY_POLL, &napi->state))
        return;

    spin_lock(&napi_hash_lock);

    /* 전역 카운터에서 원자적 ID 할당 */
    napi->napi_id = napi_gen_id();

    /* 커널 소스 분석: RCU 보호 해시 테이블에 삽입
       napi_hash[]는 NAPI_HASH_SIZE 크기의 hlist 배열 */
    hlist_add_head_rcu(&napi->napi_hash_node,
                       &napi_hash[napi->napi_id % NAPI_HASH_SIZE]);

    spin_unlock(&napi_hash_lock);
}

/* busy polling에서 napi_id로 인스턴스 조회 */
struct napi_struct *napi_by_id(unsigned int napi_id)
{
    struct napi_struct *napi;

    hlist_for_each_entry_rcu(napi,
        &napi_hash[napi_id % NAPI_HASH_SIZE], napi_hash_node)
        if (napi->napi_id == napi_id)
            return napi;
    return NULL;
}

napi_enable() 내부 구현

napi_enable()은 NAPI 인스턴스를 활성 상태로 전환합니다. netif_napi_add()에서 설정한 NAPI_STATE_SCHED 비트를 클리어하여 napi_schedule() 호출이 가능하도록 합니다. 원자적 비교-교환(Compare-and-Exchange, cmpxchg)으로 레이스 컨디션(Race Condition)을 방지합니다.

/* net/core/dev.c (Linux 6.x) — napi_enable() 내부 구현 */
void napi_enable(struct napi_struct *napi)
{
    unsigned long new, val = READ_ONCE(napi->state);

    do {
        /* 커널 소스 분석: SCHED 비트가 반드시 설정되어 있어야 함.
           netif_napi_add()가 SCHED을 설정하므로 정상이면 통과.
           이미 enable된 상태에서 재호출하면 BUG_ON 트리거 */
        BUG_ON(!test_bit(NAPI_STATE_SCHED, &val));

        /* 커널 소스 분석: SCHED와 NPSVC(No Poll Service) 비트를 클리어.
           SCHED 클리어 → napi_schedule_prep()이 성공 가능
           NPSVC 클리어 → poll service 가능 상태 */
        new = val & ~(NAPIF_STATE_SCHED | NAPIF_STATE_NPSVC);

        /* 커널 소스 분석: 스레드 NAPI인 경우 THREADED 비트 설정.
           napi->thread가 존재하면 poll이 커널 스레드에서 실행됨 */
        new |= NAPIF_STATE_THREADED * !!napi->thread;

    } while (!try_cmpxchg(&napi->state, &val, new));

    /* 커널 소스 분석: 스레드 NAPI 활성화 시 커널 스레드를 깨움.
       napi_threaded_poll() 루프가 시작됨 */
    if (napi->thread)
        wake_up_process(napi->thread);
}
napi_enable()/napi_disable() 호출 위치 규칙: napi_enable()은 반드시 ndo_open()(인터페이스 활성화) 내에서 호출해야 하며, napi_disable()ndo_stop()(인터페이스 비활성화) 내에서 호출해야 합니다. 이 순서를 위반하면 패킷 손실 또는 BUG_ON 크래시가 발생할 수 있습니다. 특히 napi_enable()을 두 번 연속 호출하면 두 번째 호출에서 BUG_ON이 트리거됩니다.

napi_schedule_prep() 내부 구현

napi_schedule_prep()은 NAPI 인스턴스가 스케줄 가능한 상태인지 확인하고, 가능하다면 NAPI_STATE_SCHED 비트를 원자적으로 설정합니다. 이 함수가 true를 반환해야만 __napi_schedule()을 호출할 수 있습니다.

/* include/linux/netdevice.h (Linux 6.x) — napi_schedule_prep() */
static inline bool napi_schedule_prep(struct napi_struct *n)
{
    unsigned long new, val = READ_ONCE(n->state);

    do {
        /* 커널 소스 분석: DISABLE 상태면 스케줄 불가.
           napi_disable() 진행 중이면 즉시 실패 반환 */
        if (unlikely(val & NAPIF_STATE_DISABLE))
            return false;

        /* 커널 소스 분석: 이미 SCHED 비트가 설정되어 있으면 중복 스케줄 방지.
           IRQ 핸들러가 동시에 호출되어도 하나만 성공.
           단, MISSED 비트를 설정하여 napi_complete_done()에 알림 */
        if (unlikely(val & NAPIF_STATE_SCHED))
            return false;

        /* 커널 소스 분석: SCHED 비트 원자적 설정 (cmpxchg 루프) */
        new = val | NAPIF_STATE_SCHED;
    } while (!try_cmpxchg(&n->state, &val, new));

    return true;
}

주목할 점은 이미 스케줄된 상태에서 새 패킷이 도착하면 napi_schedule_prep()false를 반환하는 것입니다. 이때 최신 커널(6.x)에서는 드라이버가 직접 NAPI_STATE_MISSED 비트를 설정하여, napi_complete_done()이 이를 감지하고 즉시 재스케줄하도록 합니다.

__napi_schedule() 내부 구현

__napi_schedule()은 NAPI 인스턴스를 실제로 per-CPU poll_list에 추가하고 NET_RX_SOFTIRQ를 발생시키는 함수입니다. 스레드 NAPI(Threaded NAPI)인 경우에는 softIRQ 대신 전용 커널 스레드를 깨웁니다.

/* net/core/dev.c (Linux 6.x) — __napi_schedule() */
void __napi_schedule(struct napi_struct *n)
{
    unsigned long flags;

    /* 커널 소스 분석: local_irq_save — 중첩 인터럽트 방지.
       per-CPU poll_list 접근 중 다른 IRQ가 동일 리스트를 수정하는 것을 방지 */
    local_irq_save(flags);
    ____napi_schedule(this_cpu_ptr(&softnet_data), n);
    local_irq_restore(flags);
}

/* 실제 스케줄링 수행 — 이중 언더스코어 버전 */
static inline void ____napi_schedule(
    struct softnet_data *sd,
    struct napi_struct *napi)
{
    struct task_struct *thread;

    /* 커널 소스 분석: 스레드 NAPI 경로 확인.
       NAPI_STATE_THREADED 비트가 설정되어 있으면 커널 스레드에서 poll 실행 */
    if (test_bit(NAPI_STATE_THREADED, &napi->state)) {
        thread = READ_ONCE(napi->thread);
        if (thread) {
            /* 커널 소스 분석: 스레드가 이미 종료되었으면 경고 */
            if (WARN_ON(thread->state == TASK_DEAD))
                return;
            /* SCHED_THREADED 비트 설정: 스레드가 poll을 실행할 것임을 표시 */
            set_bit(NAPI_STATE_SCHED_THREADED, &napi->state);
            /* 커널 스레드 깨우기: napi_threaded_poll() 루프 재개 */
            wake_up_process(thread);
            return;
        }
    }

    /* 커널 소스 분석: 일반 NAPI 경로 — per-CPU poll_list 추가.
       리스트 tail에 추가하여 FIFO 순서 유지 (공정 스케줄링) */
    list_add_tail(&napi->poll_list, &sd->poll_list);

    /* 커널 소스 분석: NET_RX_SOFTIRQ 발생.
       이미 IRQ 비활성화 상태이므로 _irqoff 변형 사용.
       softIRQ는 do_softirq() → net_rx_action()으로 실행됨 */
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
스레드 NAPI vs 일반 NAPI 스케줄링 경로: 일반 NAPI는 poll_list에 추가 후 NET_RX_SOFTIRQ를 발생시켜 softIRQ 컨텍스트(ksoftirqd 또는 인터럽트 반환 시점)에서 net_rx_action()이 poll을 실행합니다. 스레드 NAPI는 poll_list를 사용하지 않고 전용 커널 스레드 (napi/%s-%d)를 깨워 napi_threaded_poll()에서 직접 poll을 실행합니다. 스레드 NAPI의 장점은 PREEMPT_RT 커널에서 우선순위 기반 스케줄링이 가능하고, cgroup으로 CPU 시간을 제어할 수 있는 것입니다.

napi_complete_done() 내부 구현

napi_complete_done()은 NAPI 폴링 사이클의 완료(Completion)를 처리하는 가장 중요한 함수입니다. GRO 플러시(Flush), IRQ 일시 중단(Suspension), MISSED 비트 감지, 상태 비트 클리어를 모두 하나의 원자적 경로에서 수행합니다.

/* net/core/dev.c (Linux 6.x) — napi_complete_done() 전체 구현 */
bool napi_complete_done(struct napi_struct *n, int work_done)
{
    unsigned long flags, val, new, timeout = 0;
    bool ret = true;

    /* ① GRO flush 타이머 설정 또는 즉시 flush.
       work_done > 0이면 gro_flush_timeout 값을 확인.
       napi->gro_bitmask가 설정되어 있으면 GRO 리스트에 미완성 패킷 존재 */
    if (work_done)
        timeout = READ_ONCE(n->dev->gro_flush_timeout);

    if (n->gro_bitmask) {
        /* 커널 소스 분석: timeout 설정 시 hrtimer로 지연 flush.
           타이머 만료 시 napi_watchdog()이 GRO flush + 재스케줄 */
        if (timeout)
            hrtimer_start(&n->timer, ns_to_ktime(timeout),
                         HRTIMER_MODE_REL_PINNED);
        else
            /* timeout 미설정 시 즉시 GRO flush → 프로토콜 스택 전달 */
            napi_gro_flush(n, false);
    }

    /* ② IRQ suspension (defer_hard_irqs) 처리.
       napi_defer_hard_irqs > 0으로 설정된 경우:
       IRQ unmask를 지연하고 타이머 기반으로 폴링을 반복.
       초저지연(Ultra-Low Latency) 환경에서 IRQ 오버헤드를 더욱 줄임 */
    if (READ_ONCE(n->defer_hard_irqs_count) && work_done) {
        n->defer_hard_irqs_count--;
        timeout = READ_ONCE(n->dev->gro_flush_timeout);
        if (timeout) {
            hrtimer_start(&n->timer, ns_to_ktime(timeout),
                         HRTIMER_MODE_REL_PINNED);
        }
        /* 커널 소스 분석: SCHED 비트를 유지한 채 false 반환.
           → 드라이버가 IRQ를 재활성화하지 않음.
           → 타이머 만료 시 napi_watchdog()이 재스케줄 */
        return false;
    }

    /* ③ SCHED 비트 클리어 (원자적 cmpxchg 루프) */
    val = READ_ONCE(n->state);
    do {
        /* 커널 소스 분석: SCHED와 PREFER_BUSY_POLL 비트 클리어 */
        new = val & ~(NAPIF_STATE_SCHED | NAPIF_STATE_PREFER_BUSY_POLL);

        if (unlikely(val & NAPIF_STATE_MISSED)) {
            /* 커널 소스 분석: MISSED 비트 감지!
               poll() 실행 중에 새 패킷이 도착하여 napi_schedule_prep()이
               실패했지만, 드라이버가 MISSED 비트를 설정한 경우.
               → MISSED 클리어 + SCHED 유지 → 즉시 재스케줄
               → 패킷 손실(Packet Loss) 방지의 핵심 메커니즘 */
            new |= NAPIF_STATE_SCHED;
            new &= ~NAPIF_STATE_MISSED;
            ret = false;  /* 호출자에게 "IRQ 재활성화 불필요" 알림 */
        }
    } while (!try_cmpxchg(&n->state, &val, new));

    /* ④ 반환값 계약(Return Value Contract):
       ret == true  → 정상 완료. 드라이버가 HW IRQ를 재활성화해야 함
       ret == false → MISSED 감지 또는 defer_hard_irqs.
                      IRQ 재활성화 불필요 (자동 재스케줄됨) */

    return ret;
}

MISSED 비트의 핵심 역할

NAPI_STATE_MISSED 비트는 NAPI의 패킷 손실 방지(Packet Loss Prevention) 메커니즘의 핵심입니다. 다음과 같은 레이스 컨디션을 해결합니다:

  1. NAPI poll() 실행 중 (SCHED 비트 설정 상태)
  2. 새로운 패킷이 NIC에 도착하여 하드웨어 IRQ 발생
  3. IRQ 핸들러에서 napi_schedule_prep() 호출 → SCHED 이미 설정되어 false 반환
  4. IRQ 핸들러가 NAPI_STATE_MISSED 비트를 설정
  5. 현재 poll()이 완료되고 napi_complete_done() 호출
  6. napi_complete_done()MISSED 비트를 감지하여 SCHED 유지
  7. net_rx_action()이 즉시 다음 poll 사이클 실행 → 새 패킷 처리

이 메커니즘이 없으면, poll 중 도착한 패킷의 IRQ가 사라지고 다음 패킷이 올 때까지 해당 큐의 패킷이 영구적으로 처리되지 않는 문제가 발생합니다.

napi_complete_done() 결정 흐름

napi_complete_done(n, work_done) gro_bitmask? Yes timeout? Yes hrtimer No napi_gro_flush() No defer_hard_irqs && work_done? Yes SCHED 유지, return false IRQ unmask 불필요 (타이머 재스케줄) No cmpxchg: state 비트 갱신 MISSED 비트? 설정? Yes SCHED 유지 + MISSED 클리어 return false (즉시 재폴링) No SCHED 클리어 + PREFER_BUSY_POLL 클리어 return true 드라이버가 HW IRQ unmask 수행 napi_complete_done() 반환값에 따른 드라이버 동작 결정 흐름
드라이버 poll() 함수에서의 올바른 사용 패턴: napi_complete_done()의 반환값을 반드시 확인하고, true일 때만 하드웨어 인터럽트를 재활성화해야 합니다. 이를 무시하면 불필요한 IRQ unmask로 인한 중복 인터럽트 또는 MISSED 비트 경로에서의 이중 스케줄링이 발생할 수 있습니다.
/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* 드라이버 poll() 함수 — 올바른 napi_complete_done() 사용 예 */
static int mynic_poll(struct napi_struct *napi, int budget)
{
    struct mynic_priv *priv = container_of(napi, struct mynic_priv, napi);
    int work_done = 0;

    work_done = mynic_clean_rx(priv, budget);

    if (work_done < budget) {
        /* 커널 소스 분석: budget 미소진 → 모든 패킷 처리 완료 */
        if (napi_complete_done(napi, work_done))
            mynic_enable_irq(priv);  /* true일 때만 IRQ unmask! */
    }

    return work_done;
}

napi_schedule_irqoff() 내부

napi_schedule_irqoff()napi_schedule()의 최적화 변형(Optimized Variant)입니다. 이미 인터럽트가 비활성화된 컨텍스트(하드웨어 IRQ 핸들러 내부)에서 호출되며, local_irq_save()/local_irq_restore() 쌍을 생략합니다.

/* include/linux/netdevice.h (Linux 6.x) — napi_schedule_irqoff() */
static inline void napi_schedule_irqoff(struct napi_struct *n)
{
    /* 커널 소스 분석: napi_schedule_prep()로 SCHED 비트 원자적 설정 시도 */
    if (napi_schedule_prep(n))
        /* local_irq_save/restore 없이 직접 ____napi_schedule 호출.
           호출자가 이미 IRQ를 비활성화한 상태를 전제 (하드 IRQ 핸들러 등) */
        ____napi_schedule(this_cpu_ptr(&softnet_data), n);
}

/* 비교: napi_schedule()은 local_irq_save/restore를 포함 */
static inline void napi_schedule(struct napi_struct *n)
{
    if (napi_schedule_prep(n))
        __napi_schedule(n);  /* 내부에서 local_irq_save() 호출 */
}
napi_schedule_irqoff()의 성능 이점: local_irq_save()는 x86에서 pushf/cli 명령어, local_irq_restore()popf 명령어를 실행합니다. 하드웨어 IRQ 핸들러는 이미 인터럽트가 비활성화된 상태이므로 이 명령어 쌍이 불필요합니다. 고속 NIC에서 초당 수백만 번 호출되는 IRQ 핸들러에서는 이 수 나노초의 절약이 유의미한 성능 차이를 만듭니다. 주의: softIRQ 컨텍스트나 프로세스 컨텍스트에서 napi_schedule_irqoff()를 호출하면 인터럽트 보호가 없어 데이터 구조 손상이 발생할 수 있습니다. 반드시 IRQ가 이미 비활성화된 컨텍스트에서만 사용해야 합니다.

폴링 메커니즘과 버짓 관리

net_rx_action() 내부 동작

소프트IRQ NET_RX_SOFTIRQ가 트리거되면 net_rx_action()이 실행됩니다. 이 함수가 NAPI 폴링의 핵심 루프를 담당합니다.

/* net/core/dev.c */
static void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    unsigned long        time_limit = jiffies() + usecs_to_jiffies(netdev_budget_usecs);
    int                  budget     = netdev_budget;  /* 기본값 300 */
    struct list_head     list;
    struct list_head     repoll;

    local_irq_disable();
    list_splice_init(&sd->poll_list, &list);
    local_irq_enable();

    INIT_LIST_HEAD(&repoll);

    for (;;) {
        struct napi_struct *n;

        if (list_empty(&list)) {
            if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
                return;
            break;
        }

        n = list_first_entry(&list, struct napi_struct, poll_list);
        list_del_init(&n->poll_list);

        /* napi_poll: 실제 드라이버 poll() 호출, work_done 반환 */
        budget -= napi_poll(n, &repoll);

        /* 전체 버짓 소진 또는 시간 초과 시 조기 종료 */
        if (unlikely(budget <= 0 ||
                    time_after_eq(jiffies(), time_limit))) {
            sd->time_squeeze++;  /* /proc/net/softnet_stat 열 3 증가 */
            break;
        }
    }

    /* 처리 못한 NAPI들을 다시 poll_list에 연결 후 softIRQ 재트리거 */
    local_irq_disable();
    list_splice_tail_init(&sd->poll_list, &list);
    list_splice_tail(&repoll, &list);
    list_splice(&list, &sd->poll_list);

    if (!list_empty(&sd->poll_list))
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);  /* ksoftirqd 깨우기 */

    net_rps_action_and_irq_enable(sd);
}

napi_poll() 내부 함수 구현

/* net/core/dev.c: 실제 드라이버 poll()을 호출하는 내부 래퍼 */
static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
    int  work, weight;

    /* 현재 CPU를 poll_owner로 기록 (동시 실행 방지) */
    WRITE_ONCE(n->poll_owner, smp_processor_id());

    weight = n->weight;

    /* 드라이버 poll() 호출: 처리한 패킷 수 반환 */
    work = n->poll(n, weight);

    if (unlikely(work > weight))
        pr_err_once("NAPI poll function %pS returned %d, exceeding weight %d\n",
                    n->poll, work, weight);

    if (work < weight) {
        /* 버짓 미달: 큐 소진 → poll_list에서 제거됨
           (드라이버가 napi_complete_done() 호출하여 SCHED 비트 클리어) */
        if (unlikely(napi_is_scheduled(n))) {
            /* MISSED 비트 설정됨: 재스케줄이 필요 */
            list_add_tail(&n->poll_list, repoll);
        }
    } else {
        /* 버짓 전부 사용: 더 처리할 패킷 있을 가능성 → repoll 큐에 추가 */
        if (unlikely(test_and_clear_bit(NAPI_STATE_MISSED, &n->state))) {
            /* 폴링 중 IRQ 발생: 즉시 재스케줄 */
            napi_schedule(n);
        } else {
            list_add_tail(&n->poll_list, repoll);
        }
    }

    WRITE_ONCE(n->poll_owner, -1);
    return work;
}

버짓 분배 시나리오

3개의 NIC가 각각 weight=64로 등록된 환경에서 netdev_budget=300일 때의 동작:

순서NAPI 인스턴스처리 패킷남은 버짓결과
1회 eth0 (weight=64) 64 (버짓 전부 소진) 300 - 64 = 236 repoll 큐에 재등록
2회 eth1 (weight=64) 30 (큐 소진) 236 - 30 = 206 napi_complete_done() 호출, IRQ 재활성화
3회 eth2 (weight=64) 64 (버짓 전부 소진) 206 - 64 = 142 repoll 큐에 재등록
4회 eth0 (repoll) 64 (버짓 전부 소진) 142 - 64 = 78 repoll 큐에 재등록
5회 eth2 (repoll) 64 (버짓 전부 소진) 78 - 64 = 14 repoll 큐에 재등록
6회 eth0 (repoll) 14 (버짓 소진 → 조기 종료) 0 이하 → 종료 time_squeeze++, softIRQ 재트리거

time_squeeze 발생 시나리오

time_squeeze는 두 가지 상황에서 증가합니다:

time_squeeze가 지속적으로 증가한다면 다음을 검토해야 합니다:

NAPI 공정성(Fairness) 메커니즘

net_rx_action()은 poll_list를 head에서부터 순차 처리합니다. 버짓을 모두 소진한 NAPI는 repoll 큐의 tail에 추가되고, 다음 라운드에서 다시 head부터 처리됩니다. 이 라운드로빈 방식이 공정성을 보장합니다.

버짓: 300 netdev_budget 8ms 시간 제한 eth0 NAPI weight=64 64패킷 소진 → repoll eth1 NAPI weight=64 30패킷 → 완료(IRQ 재활성화) eth2 NAPI weight=64 64패킷 소진 → repoll 300 - 64 = 236 남음 (eth0 소진 후) 236 - 30 = 206 남음 (eth1 완료 후) 206 - 64 = 142 남음 (eth2 소진 후) repoll 큐 eth0 (64 더 있을 가능성) eth2 (64 더 있을 가능성) → 다음 라운드에서 처리 또는 버짓 소진 시 재트리거 NAPI 버짓 분배: 3개 인스턴스가 300 버짓을 라운드로빈으로 소비

버짓(Budget) 관리 핵심 파라미터

파라미터경로기본값의미
netdev_budget /proc/sys/net/core/netdev_budget 300 softIRQ 1회 실행에서 처리 가능한 총 패킷 수 (전체 NAPI 합산)
netdev_budget_usecs /proc/sys/net/core/netdev_budget_usecs 8000 μs softIRQ 1회 실행 최대 시간 제한 (시간 초과 시 재스케줄)
napi weight 드라이버 netif_napi_add() 64 NAPI 인스턴스 1회 poll()에서 처리할 최대 패킷 수
gro_flush_timeout /proc/sys/net/core/gro_flush_timeout 0 (비활성) GRO 버퍼를 강제 flush하는 타임아웃 (나노초)
napi_defer_hard_irqs /proc/sys/net/core/napi_defer_hard_irqs 0 하드 IRQ 재활성화를 지연하는 NAPI 폴링 주기 수
드라이버 poll() 반환 규칙:
  • 처리한 패킷 수(work_done)가 budget보다 작으면: 큐가 비었음 → napi_complete_done() 호출 필수
  • 처리한 패킷 수가 budget같으면: 큐에 더 있을 가능성 → napi_complete_done() 호출 금지, budget 반환

GRO(Generic Receive Offload) 통합

GRO의 역할과 성능 효과

GRO는 NAPI poll 경로에서 동일 TCP/IP 플로우에 속하는 여러 패킷을 하나의 큰 패킷으로 병합하는 최적화입니다. 상위 스택(TCP 등)이 처리해야 하는 패킷 수를 줄여 CPU 사용률과 처리량을 개선합니다.

10 Gbps 링크에서 1500B 패킷의 경우 GRO가 8개를 병합하면 상위 스택이 처리하는 패킷 수가 약 8배 감소합니다. 실제 환경에서 GRO를 활성화했을 때:

GRO 내부 콜체인

/* net/core/gro.c — 간략화 */
/* NAPI poll() → napi_gro_receive() → dev_gro_receive() 경로 */

/* 1단계: napi_gro_receive() - 진입점 */
gro_result_t napi_gro_receive(struct napi_struct *napi,
                               struct sk_buff *skb)
{
    gro_result_t ret;

    skb_mark_napi_id(skb, napi);  /* skb에 napi_id 설정 */
    trace_napi_gro_receive_entry(skb);

    skb_gro_reset_offset(skb, 0);

    ret = dev_gro_receive(napi, skb);
    trace_napi_gro_receive_exit(ret);

    return napi_skb_finish(napi, skb, ret);
}

/* 2단계: dev_gro_receive() - 프로토콜별 GRO 핸들러 호출 */
static gro_result_t dev_gro_receive(struct napi_struct *napi,
                                     struct sk_buff *skb)
{
    u32               hash;
    struct gro_list  *gro_list;
    struct sk_buff   *pp = NULL;
    struct sk_buff   *p;
    const struct packet_offload *ptype;
    gro_result_t       ret = GRO_NORMAL;

    /* VLAN 태그, 터널 헤더 처리 */
    if (skb->protocol == htons(ETH_P_8021Q) || ...)
        goto normal;

    /* 프로토콜별 gro_receive 핸들러 검색
       ETH_P_IP  → inet_gro_receive()
       ETH_P_IPV6 → ipv6_gro_receive() */
    ptype = gro_find_receive_by_type(skb->protocol);
    if (!ptype)
        goto normal;

    /* gro_hash 버킷 선택: 4-tuple 해시 기반 */
    hash = skb_get_hash_raw(skb) & (GRO_HASH_BUCKETS - 1);
    gro_list = &napi->gro_hash[hash];

    /* 동일 플로우 검색 후 병합 시도 */
    pp = ptype->callbacks.gro_receive(&gro_list->list, skb);
    ...
}

/* 3단계: inet_gro_receive() → tcp4_gro_receive() 콜체인 */
struct sk_buff *inet_gro_receive(struct list_head *head,
                                  struct sk_buff *skb)
{
    const struct iphdr *iph = skb_gro_network_header(skb);
    const struct net_offload *ops;

    /* IP 헤더 검증: TTL, ToS, checksum 비교 */
    skb_gro_pull(skb, sizeof(*iph));

    /* 전송 계층 GRO로 위임:
       IPPROTO_TCP → tcp4_gro_receive()
       IPPROTO_UDP → udp4_gro_receive() */
    ops = rcu_dereference(inet_offloads[iph->protocol]);
    if (!ops || !ops->callbacks.gro_receive)
        goto out;
    skb = ops->callbacks.gro_receive(head, skb);
    ...
}

GRO 병합 알고리즘 단계별 설명

tcp4_gro_receive()에서 실제 TCP 세그먼트 병합이 이루어집니다. 아래는 병합 알고리즘의 핵심 단계입니다:

/* net/ipv4/tcp_offload.c — 간략화 */
/* 의사코드: GRO 병합 알고리즘 핵심 로직 */
gro_result_t tcp4_gro_receive(struct list_head *head,
                                struct sk_buff *skb)
{
    struct tcphdr *th = tcp_gro_pull_header(skb);
    struct sk_buff *p;

    list_for_each_entry(p, head, list) {
        struct tcphdr *th2 = tcp_hdr(p);

        /* 단계 1: 동일 플로우 검사 (src/dst port 동일 여부) */
        if (th->source != th2->source || th->dest != th2->dest) {
            NAPI_GRO_CB(p)->same_flow = 0;
            continue;
        }

        /* 단계 2: TCP 플래그 검사 (SYN, FIN, RST, URG 등 거부) */
        if (th->fin || th->syn || th->rst || th->urg) {
            NAPI_GRO_CB(p)->flush = 1;
            continue;
        }

        /* 단계 3: 시퀀스 번호 연속성 검사 */
        if (!tcp_gro_seq_check(p, skb, th)) {
            /* 순서 역전: flush 표시 */
            NAPI_GRO_CB(p)->flush = 1;
            continue;
        }

        /* 단계 4: 크기 검사 (병합 결과가 GRO_MAX_HEAD 이하인지) */
        if (skb_gro_len(skb) > GRO_MAX_HEAD) {
            NAPI_GRO_CB(p)->flush = 1;
            break;
        }

        /* 단계 5: 실제 병합: skb 데이터를 p의 frag_list에 추가 */
        NAPI_GRO_CB(p)->count++;
        NAPI_GRO_CB(p)->tot_len += skb_gro_len(skb);
        tcp_gro_merge(p, skb, th);  /* skb → p의 frag_list 연결 */
        return GRO_MERGED_FREE;
    }

    /* 병합 실패: 새 GRO 헤드로 등록 */
    NAPI_GRO_CB(skb)->count = 1;
    NAPI_GRO_CB(skb)->tot_len = skb_gro_len(skb);
    list_add(&skb->list, head);
    return GRO_HELD;
}

터널(Tunnel) GRO: VXLAN/GRE 중첩 처리

VXLAN이나 GRE 터널 패킷은 중첩 헤더 구조를 가집니다. GRO는 외부 헤더와 내부 헤더를 모두 검사하여 병합 가능 여부를 판단합니다.

/* drivers/net/vxlan/vxlan_core.c — 간략화 */
/* VXLAN GRO: 외부 UDP/IP + 내부 Ethernet/IP/TCP 모두 동일해야 병합 */
/* 내부 패킷의 gro_receive 핸들러도 재귀적으로 호출됨 */
struct sk_buff *vxlan_gro_receive(struct list_head *head,
                                   struct sk_buff *skb,
                                   struct udphdr *uh)
{
    /* 외부 VXLAN VNI 동일 여부 검사 */
    struct vxlanhdr *vh = skb_gro_header_fast(skb, off);
    if (vh->vx_vni != NAPI_GRO_CB(p_skb)->vx_vni)
        continue;

    /* 내부 헤더(Ethernet → IP → TCP)에 대해 재귀적 GRO 처리 */
    skb_gro_pull(skb, sizeof(*vh));
    NAPI_GRO_CB(skb)->encap_mark = 1;
    pp = eth_gro_receive(&gro_head, skb);
    ...
}

GRO 병합 다이어그램

패킷 #1 seq=1000, 1460B 패킷 #2 seq=2460, 1460B 패킷 #3 seq=3920, 1460B 패킷 #4 seq=5380, 1460B GRO 처리 gro_hash 버킷 검색 플로우 동일성 확인 시퀀스 연속성 확인 frag_list 병합 NAPI_GRO_CB.count=4 병합 SKB 5840B 페이로드 (4 x 1460B) 헤더 1개만 처리 frag_list: pkt2,3,4 TCP 스택 1회 처리로 완료 (vs 4회 없이) GRO: 4개 TCP 세그먼트 → 1개 대형 SKB 병합 (상위 스택 호출 4배 감소)

GRO 관련 주요 API

/* net/core/gro.c, include/linux/netdevice.h — 간략화 */
/* GRO 수신 함수: napi poll()에서 직접 호출 */
gro_result_t napi_gro_receive(struct napi_struct *napi,
                               struct sk_buff *skb);

/* Frags(page 기반) GRO 수신: page_pool과 함께 사용 */
gro_result_t napi_gro_frags(struct napi_struct *napi);

/* GRO 결과 코드 */
enum gro_result {
    GRO_MERGED,         /* 기존 GRO 패킷에 병합 완료 */
    GRO_MERGED_FREE,    /* 병합 완료, 원본 skb는 해제 */
    GRO_HELD,           /* GRO 버퍼에 보관 중 (flush 대기) */
    GRO_NORMAL,         /* GRO 미적용, 일반 처리 경로 */
    GRO_CONSUMED,       /* 패킷 소비됨 (드롭 아님) */
};

/* napi poll() 내 GRO 사용 예 */
static int mynic_poll(struct napi_struct *napi, int budget)
{
    struct mynic_rx_ring *ring = container_of(napi, struct mynic_rx_ring, napi);
    int work_done = 0;

    while (work_done < budget) {
        struct sk_buff *skb = mynic_get_next_skb(ring);
        if (!skb)
            break;
        skb->ip_summed = CHECKSUM_UNNECESSARY;
        napi_gro_receive(napi, skb);
        work_done++;
    }

    if (work_done < budget) {
        if (napi_complete_done(napi, work_done))
            mynic_enable_rx_irq(ring);
    }
    return work_done;
}

GRO flush 타이밍과 gro_flush_timeout

GRO 버퍼는 다음 상황에서 즉시 flush됩니다:

gro_flush_timeout 설정 가이드:
  • 0 (기본값): 타이머 비활성. NAPI 완료 시점에만 flush. 고처리량에 유리
  • 100000 (100μs): 주기적 강제 flush로 GRO 지연 제한. 균형 잡힌 설정
  • 1000000 (1ms): 배치 크기 극대화, 레이턴시 허용 범위가 넓은 경우

napi_gro_frags() 상세 — 프래그먼트 기반 GRO 경로

napi_gro_frags()는 드라이버가 헤더와 페이로드(Payload)를 별도 프래그먼트로 분리하여 전달할 때 사용합니다. napi_gro_receive()와 달리 SKB의 linear 영역에는 L2 헤더만 존재하고, 나머지 데이터는 skb_shinfo(skb)->frags[]에 page 프래그먼트로 저장됩니다.

/* net/core/gro.c: napi_gro_frags() 콜체인 */

/* 1단계: napi->skb 캐시에서 SKB 획득 */
struct sk_buff *napi_get_frags(struct napi_struct *napi)
{
    struct sk_buff *skb = napi->skb;
    if (!skb) {
        skb = napi_alloc_skb(napi, GRO_MAX_HEAD);
        napi->skb = skb;
    }
    return skb;
}

/* 2단계: 프래그먼트 GRO 수신 */
gro_result_t napi_gro_frags(struct napi_struct *napi)
{
    struct sk_buff *skb = napi->skb;
    /* skb->data에는 이더넷 헤더만 존재
       skb_shinfo(skb)->frags[]에 IP+TCP+페이로드 page 매핑 */

    gro_normal_one(napi, skb, 1);
    /* → dev_gro_receive() → 프로토콜별 GRO 콜백 체인 */
}

/* napi_gro_receive()와 napi_gro_frags() 선택 기준:
   - napi_gro_receive(): 드라이버가 완전한 skb를 생성한 경우
   - napi_gro_frags():   헤더만 linear, 페이로드는 frag로 전달하는 경우
                         (e1000e, ixgbe, ice 등 대부분의 고성능 드라이버) */

skb_gro_receive() — GRO 패킷 병합 핵심

skb_gro_receive()는 GRO 엔진이 동일 플로우의 패킷을 실제로 하나로 합치는 핵심 함수입니다. 병합 방식은 두 가지가 있으며, 커널이 상황에 따라 자동으로 선택합니다.

/* net/core/skbuff.c: skb_gro_receive() 내부 병합 전략 */
int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
{
    unsigned int headlen = skb_headlen(skb);

    /* 전략 1: frag_list 병합
       p->frag_list에 skb를 연결 리스트로 추가.
       단순하지만 이후 TCP coalescing에서 비효율적.
       주로 비표준 프레임이나 frag 공간 부족 시 사용 */
    if (skb_shinfo(p)->frag_list)
        NAPI_GRO_CB(p)->last->next = skb;
    else
        skb_shinfo(p)->frag_list = skb;

    /* 전략 2: frags[] 병합 (선호)
       skb의 데이터를 p->frags[]에 page 프래그먼트로 추가.
       단일 SKB에 모든 데이터가 포함되어 효율적.
       MAX_SKB_FRAGS(17) 제한 내에서만 가능 */
    if (skb_shinfo(p)->nr_frags + delta <= MAX_SKB_FRAGS) {
        skb_frag_list_init(skb);
        /* page 참조를 p->frags[]로 이동 */
    }

    p->len      += skb->len;
    p->data_len += skb->len;
    p->truesize += skb->truesize;
    NAPI_GRO_CB(p)->count++;

    /* MAX_GRO_SKBS(8) 초과 시 flush 트리거 */
    return 0;
}

napi_gro_list_prepare() — 해시 기반 플로우 매칭

GRO 엔진은 수신 패킷을 기존 GRO 리스트와 비교하여 동일 플로우를 찾습니다. napi_gro_list_prepare()는 해시 버킷 내의 모든 대기 중인 SKB를 순회하며 same_flowflush 플래그를 설정합니다.

/* net/core/gro.c: GRO 플로우 매칭 */
static void napi_gro_list_prepare(
    const struct napi_struct *napi,
    const struct sk_buff *skb)
{
    struct sk_buff *p;
    unsigned long diffs;

    /* napi->gro_hash[bucket] 리스트를 순회 */
    list_for_each_entry(p, head, list) {
        diffs = (unsigned long)p->dev ^ (unsigned long)skb->dev;
        diffs |= skb_vlan_tag_present(p) ^
                 skb_vlan_tag_present(skb);

        /* MAC 헤더 비교 (EtherType, VLAN 등) */
        diffs |= compare_ether_header(
            skb_mac_header(p), skb_mac_header(skb));

        NAPI_GRO_CB(p)->same_flow = !diffs;
        NAPI_GRO_CB(p)->flush     = 0;
        /* 이후 프로토콜 콜백에서 flush 여부를 정밀 판단 */
    }
}

프로토콜별 GRO 콜백(Callback) 테이블

GRO는 계층별 콜백 함수를 체인으로 호출하여 프로토콜 헤더를 검증하고 병합 가능 여부를 판단합니다. 각 프로토콜은 struct net_offload 또는 struct packet_offloadgro_receive/gro_complete 콜백을 등록합니다.

계층콜백 함수등록 구조체핵심 동작
L2 (Ethernet) eth_gro_receive() packet_offload EtherType으로 상위 프로토콜 결정
L3 (IPv4) inet_gro_receive() net_offload IP 헤더 검증, ID 연속성, TTL/TOS 일치
L3 (IPv6) ipv6_gro_receive() net_offload Flow Label, Hop Limit 일치
L4 (TCP) tcp4_gro_receive() net_offload SEQ 연속성, 윈도우, 타임스탬프, PSH 플래그
L4 (UDP) udp4_gro_receive() net_offload GRO-UDP (Linux 6.0+), 같은 포트/길이
GRO 내부 콜체인 상세 드라이버에서 napi_gro_receive를 호출한 뒤 프로토콜별 콜백을 거쳐 skb_gro_receive에 도달하는 전체 흐름 GRO 내부 콜체인 (napi_gro_receive 경로) Driver poll() napi_gro_receive() dev_gro_receive() napi_gro_list_prepare() eth_gro_receive() inet_gro_receive() tcp4_gro_receive() skb_gro_receive() frags[] 또는 frag_list 병합 GRO_NORMAL GRO_MERGED 콜백 체인: L2 → L3 → L4 → 병합 same_flow=1 & flush=0 → skb_gro_receive()로 병합
GRO의 병합 알고리즘, 프로토콜별 콜백 체인(offload_callbacks), GSO와의 대칭 관계, sk_buff 메모리 레이아웃 등 GRO 자체의 내용은 GSO/GRO 문서를 참고하세요.

NAPI 메모리 및 버퍼 관리

NAPI 메모리 할당 개요

NAPI 폴링 컨텍스트는 softIRQ나 스레드 NAPI 내에서 실행되며, 일반 메모리 할당과 다른 전용 캐시 메커니즘을 사용합니다. 커널은 NAPI 전용 할당 API를 통해 per-CPU 캐시(napi_alloc_cache)를 활용하여 할당 오버헤드를 최소화합니다.

할당 계층메커니즘대표 API사용 시나리오
slab (일반) kmem_cache (SLUB) __alloc_skb() 프로세스 컨텍스트, TX 경로
NAPI 캐시 per-CPU napi_alloc_cache napi_alloc_skb() NAPI poll 내부 RX 경로
page_pool per-NAPI 페이지(Page) 재활용(Recycling) page_pool_alloc_pages() 고성능 드라이버 RX (제로카피)
page frag per-CPU 페이지 프래그먼트 napi_alloc_frag() 소형 패킷 RX, 버퍼 슬라이싱

napi_alloc_skb() / __napi_alloc_skb()

NAPI 폴링 컨텍스트 전용 SKB 할당 함수입니다. per-CPU napi_alloc_cache를 통해 slab 할당자의 lock contention을 회피하고, GFP_ATOMIC 없이도 빠르게 할당합니다.

/* include/linux/skbuff.h */
struct sk_buff *napi_alloc_skb(
    struct napi_struct *napi,
    unsigned int length);  /* 헤더 영역 크기 */

struct sk_buff *__napi_alloc_skb(
    struct napi_struct *napi,
    unsigned int length,
    gfp_t gfp_mask);

/* 내부 구현 핵심 (net/core/skbuff.c):
   1. per-CPU napi_alloc_cache에서 skb 구조체 획득 (slab bypass)
   2. 페이지 프래그먼트에서 데이터 영역 할당
   3. skb->head, skb->data, skb->tail 초기화
   4. napi->skb_cache_lock 없이 lockless 동작 */

/* 사용 예 (드라이버 poll 함수 내부) */
struct sk_buff *skb = napi_alloc_skb(napi, 256);
if (!skb)
    return -ENOMEM;
/* skb->data에 256바이트 linear 영역 확보
   나머지 페이로드는 frags[]로 매핑 가능 */

napi_build_skb() / __napi_build_skb()

napi_build_skb()이미 할당된 버퍼(페이지)를 기반으로 SKB를 생성합니다. 데이터 복사 없이 SKB 메타데이터만 초기화하므로 제로카피 수신 경로의 핵심입니다. napi_alloc_skb()와 달리 데이터 영역을 별도로 할당하지 않습니다.

/* include/linux/skbuff.h */
struct sk_buff *napi_build_skb(
    void *data,              /* 이미 할당된 버퍼 포인터 */
    unsigned int frag_size); /* 버퍼 전체 크기 */

/* napi_alloc_skb() vs napi_build_skb() 비교:
 *
 * napi_alloc_skb(napi, 256):
 *   - SKB 구조체 할당 + 256바이트 데이터 영역 할당
 *   - DMA 버퍼 → memcpy → SKB 데이터 영역
 *   - 소형 패킷이나 레거시 드라이버에 적합
 *
 * napi_build_skb(page_addr, PAGE_SIZE):
 *   - SKB 구조체만 할당, data는 이미 존재하는 page를 가리킴
 *   - DMA 버퍼 = SKB 데이터 영역 (제로카피)
 *   - page_pool 기반 고성능 드라이버에 적합
 */

/* page_pool + napi_build_skb 패턴 */
struct page *page = page_pool_dev_alloc_pages(ring->page_pool);
void *va = page_address(page) + offset;

/* DMA에서 직접 이 페이지에 수신 데이터를 기록 */
dma_sync_single_for_cpu(dev, dma_addr, len, DMA_FROM_DEVICE);

struct sk_buff *skb = napi_build_skb(va - headroom, frag_size);
skb_reserve(skb, headroom);
skb_put(skb, len);
skb_mark_for_recycle(skb); /* page_pool 재활용 마킹 */

napi_alloc_frag() / napi_alloc_frag_align()

페이지 프래그먼트(page fragment)는 하나의 물리 페이지를 여러 소형 버퍼로 분할 사용하는 기법입니다. napi_alloc_frag()는 per-CPU napi_alloc_cache.page에서 요청 크기만큼의 프래그먼트를 슬라이싱하여 반환합니다.

/* include/linux/skbuff.h */
void *napi_alloc_frag(unsigned int fragsz);

void *napi_alloc_frag_align(
    unsigned int fragsz,
    unsigned int align);  /* 정렬 요구사항 (예: L1_CACHE_BYTES) */

/* 내부 동작:
   1. per-CPU napi_alloc_cache.page에서 남은 공간 확인
   2. fragsz만큼 슬라이싱 (offset 증가)
   3. 페이지 소진 시 새 compound page 할당
   4. refcount로 프래그먼트 수명 관리 */

/* 사용 예: 헤더 영역만 별도 할당 */
void *header = napi_alloc_frag_align(256, SMP_CACHE_BYTES);
if (!header)
    return -ENOMEM;
/* 이 영역에 패킷 헤더를 복사, 페이로드는 page_pool page를 frags[]로 연결 */

napi_get_frags() / napi_reuse_skb()

napi_get_frags()는 GRO 프래그먼트 경로에서 사용하는 per-NAPI SKB 캐시입니다. 각 napi_structnapi->skb 필드에 하나의 재사용 가능 SKB를 보관합니다.

/* net/core/gro.c */
struct sk_buff *napi_get_frags(struct napi_struct *napi)
{
    struct sk_buff *skb = napi->skb;
    if (!skb) {
        skb = napi_alloc_skb(napi, GRO_MAX_HEAD);
        if (skb)
            napi->skb = skb;
    }
    return skb;
}

/* GRO 병합 성공 후 SKB 재활용 */
static void napi_reuse_skb(struct napi_struct *napi,
                            struct sk_buff *skb)
{
    if (unlikely(skb->pfmemalloc)) {
        consume_skb(skb);
        return;
    }
    __skb_pull(skb, skb_headlen(skb));
    skb_reserve(skb, NET_IP_ALIGN - skb_headroom(skb));
    __vlan_hwaccel_clear_tag(skb);
    skb->dev = napi->dev;
    napi->skb = skb;  /* 다음 napi_get_frags()에서 재사용 */
}

/* GRO 결과에 따른 경로:
   GRO_MERGED      → napi_reuse_skb(): SKB 재활용
   GRO_MERGED_FREE → napi_skb_free(): SKB 해제 + frag 해제
   GRO_NORMAL      → napi_skb_finish(): netif_receive_skb()로 전달
   GRO_HELD        → GRO 리스트에 보관 (flush 대기) */

napi_consume_skb() — budget 인식 SKB 해제

napi_consume_skb()는 NAPI 컨텍스트에서 SKB를 해제하는 최적화된 함수입니다. TX 완료 경로에서 주로 사용되며, budget 인자를 통해 NAPI poll과 non-NAPI 컨텍스트를 자동으로 구분합니다.

/* net/core/skbuff.c */
void napi_consume_skb(struct sk_buff *skb, int budget)
{
    if (unlikely(!skb))
        return;

    /* budget > 0: NAPI poll 컨텍스트
       → per-CPU napi_alloc_cache로 SKB 반환 (bulk free)
       budget == 0: 비-NAPI 컨텍스트 (예: 타이머, netpoll)
       → 일반 kfree_skb_reason() 경로 */
    if (budget) {
        napi_skb_cache_put(skb); /* lockless 캐시 반환 */
    } else {
        kfree_skb_reason(skb, SKB_DROP_REASON_NOT_SPECIFIED);
    }
}

/* TX 완료 처리에서의 사용 패턴 */
static bool mynic_clean_tx(struct mynic_tx_ring *ring, int budget)
{
    while (tx_cleaned < budget) {
        struct sk_buff *skb = ring->tx_buf[idx].skb;
        dma_unmap_single(dev, dma, len, DMA_TO_DEVICE);
        napi_consume_skb(skb, budget); /* budget 전달! */
        ring->tx_buf[idx].skb = NULL;
        tx_cleaned++;
    }
}

page_pool 통합

page_pool은 NAPI 전용 고성능 페이지 할당/재활용 프레임워크입니다. DMA 매핑(Mapping)을 캐싱하고, 페이지를 재활용하여 메모리 할당 오버헤드와 IOMMU/SWIOTLB 비용을 획기적으로 줄입니다.

/* include/net/page_pool/types.h */
struct page_pool_params {
    int           order;      /* 페이지 order (0=4K, 1=8K) */
    unsigned int  flags;      /* PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV */
    int           pool_size;  /* 초기 풀 크기 (디스크립터 수 권장) */
    int           nid;        /* NUMA 노드 */
    struct device *dev;       /* DMA 매핑용 디바이스 */
    struct napi_struct *napi; /* 연결된 NAPI 인스턴스 */
    enum dma_data_direction dma_dir;
    unsigned int  offset;     /* NET_SKB_PAD + NET_IP_ALIGN */
    unsigned int  max_len;    /* 최대 데이터 길이 */
};

/* page_pool 핵심 API 체인 */
struct page_pool *page_pool_create(
    const struct page_pool_params *params);

/* 할당: 캐시 → 링 → buddy allocator 순서 */
struct page *page_pool_dev_alloc_pages(
    struct page_pool *pool);

/* DMA 주소 획득 (이미 매핑됨, IOMMU 비용 제로) */
dma_addr_t page_pool_get_dma_addr(struct page *page);

/* 직접 재활용: NAPI poll 내에서 즉시 풀로 반환 */
void page_pool_recycle_direct(
    struct page_pool *pool, struct page *page);

/* SKB에 page_pool 재활용 마킹 (네트워크 스택 통과 후 자동 재활용) */
void skb_mark_for_recycle(struct sk_buff *skb);

/* 풀 해제 */
void page_pool_destroy(struct page_pool *pool);
NAPI 메모리 할당 경로 비교 napi_alloc_skb, napi_build_skb, napi_alloc_frag, page_pool 네 가지 메모리 할당 경로를 병렬 비교하는 다이어그램 NAPI 메모리 할당 경로 비교 Driver poll() napi_alloc_skb() napi_alloc_cache page frag 할당 SKB + 데이터 복사 napi_build_skb() 기존 page 사용 SKB 메타만 할당 제로카피 SKB napi_alloc_frag() per-CPU page 슬라이스 offset 증가 raw 버퍼 포인터 page_pool_alloc() 캐시 → 링 → buddy DMA 매핑 캐싱 DMA-ready page 데이터 복사 발생 소형 패킷 최적 레거시 드라이버 제로카피 page_pool 연동 고성능 드라이버 권장 페이지 분할 사용 헤더 버퍼용 가장 작은 할당 단위 DMA 재매핑 제거 자동 재활용 최신 드라이버 표준 page_pool 생명주기 page_pool에서 페이지가 할당되고 DMA 매핑을 거쳐 사용된 후 재활용되는 전체 순환 흐름 page_pool 페이지 생명주기 page_pool 캐시 alloc_cache[] / ring buffer DMA 매핑 dma_map_page() NIC DMA 수신 HW가 페이지에 기록 네트워크 스택 처리 napi_build_skb → GRO → TCP 재활용 (recycle) DMA unmap 불필요! 직접 반환 (fast) page_pool_recycle_direct() 할당 poll() skb_mark_for_recycle 캐시 적재

NAPI 메모리 API 종합 비교표

함수 컨텍스트 메모리 소스 제로카피 DMA 캐시 주요 용도
napi_alloc_skb() NAPI poll napi_alloc_cache + page frag 아니오 별도 매핑 per-CPU SKB 캐시 범용 RX SKB 할당
napi_build_skb() NAPI poll 외부 제공 버퍼 외부 관리 per-CPU SKB 캐시 page_pool 기반 RX
napi_alloc_frag() NAPI poll per-CPU page frag 해당없음 별도 매핑 per-CPU page 소형 헤더 버퍼
napi_get_frags() NAPI poll napi->skb 캐시 해당없음 해당없음 per-NAPI SKB GRO frag 경로
page_pool_alloc() NAPI poll 캐시 → 링 → buddy 자동 매핑/캐싱 per-NAPI pool 고성능 DMA 버퍼
napi_consume_skb() NAPI poll / 기타 해당없음 (해제) 해당없음 해당없음 budget 인식 해제 TX 완료 SKB 해제

NAPI 메모리 함수 내부 구현 상세

NAPI 컨텍스트에서 사용되는 메모리 할당/해제 함수들은 일반 커널 할당자와 달리 per-CPU 캐시(per-CPU Cache)를 적극 활용하여 락 경합(Lock Contention)을 최소화합니다. 이 절에서는 각 함수의 내부 구현을 커널 소스 수준에서 분석합니다.

napi_alloc_skb() / __napi_alloc_skb() 내부

napi_alloc_skb()는 NAPI poll 컨텍스트 전용 skb 할당 함수로, per-CPU napi_alloc_cache 구조체를 통해 skb 헤더를 사전 할당(Pre-allocation)하여 재사용합니다. 내부적으로 __napi_alloc_skb()를 호출합니다.

/* 커널 소스 분석: net/core/skbuff.c — per-CPU NAPI 할당 캐시 */

#define NAPI_SKB_CACHE_SIZE    64
#define NAPI_SKB_CACHE_BULK    16

struct napi_alloc_cache {
    struct page_frag_cache page;        /* 페이지 프래그먼트 캐시 */
    unsigned int           skb_count;    /* 캐시된 skb 헤더 개수 */
    void                   *skb_cache[NAPI_SKB_CACHE_SIZE]; /* skb 헤더 풀 */
};

static DEFINE_PER_CPU(struct napi_alloc_cache, napi_alloc_cache);

/* 커널 소스 분석: __napi_alloc_skb() 핵심 구현 흐름 */
struct sk_buff *__napi_alloc_skb(struct napi_struct *napi,
                                unsigned int len, gfp_t gfp_mask)
{
    struct napi_alloc_cache *nc;
    struct sk_buff *skb;
    void *data;
    bool pfmemalloc;

    /* 1단계: 데이터 길이 정렬 (SMP_CACHE_BYTES 단위) */
    len += NET_SKB_PAD + NET_IP_ALIGN;
    len = SKB_DATA_ALIGN(len);
    len += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));

    nc = this_cpu_ptr(&napi_alloc_cache);

    /* 2단계: per-CPU 페이지 프래그먼트에서 데이터 영역 할당
     *        slab 할당자를 거치지 않고 페이지 조각을 직접 잘라 사용 */
    data = page_frag_alloc_align(&nc->page, len, gfp_mask,
                                  SMP_CACHE_BYTES);
    if (unlikely(!data))
        return NULL;

    pfmemalloc = page_frag_cache_is_pfmemalloc(&nc->page);

    /* 3단계: napi_alloc_cache에서 skb 헤더 가져오기
     *        캐시가 비었으면 kmem_cache_alloc_bulk()로 일괄 보충 */
    if (unlikely(!nc->skb_count)) {
        nc->skb_count = kmem_cache_alloc_bulk(
            skbuff_cache, gfp_mask,
            NAPI_SKB_CACHE_BULK,
            nc->skb_cache);
        if (unlikely(!nc->skb_count))
            return NULL;
    }
    skb = nc->skb_cache[--nc->skb_count];

    /* 4단계: skb 헤더와 데이터 프래그먼트 연결 */
    __build_skb_around(skb, data, len);
    skb->pfmemalloc = pfmemalloc;

    /* 5단계: 네트워크 정렬을 위한 headroom 예약
     *        NET_SKB_PAD: DMA 정렬, NET_IP_ALIGN: IP 헤더 정렬 */
    skb_reserve(skb, NET_SKB_PAD + NET_IP_ALIGN);
    skb->dev = napi->dev;

    return skb;
}
성능 핵심: __napi_alloc_skb()의 핵심 최적화는 두 가지입니다. 첫째, kmem_cache_alloc_bulk()로 16개 skb 헤더를 한 번에 할당하여 slab 락 진입 횟수를 1/16로 줄입니다. 둘째, per-CPU 페이지 프래그먼트 캐시로 데이터 영역을 slab 없이 할당합니다.

skb 할당 함수 비교

함수사용 컨텍스트캐시 방식락 경합주요 용도
napi_alloc_skb()NAPI poll 전용per-CPU napi_alloc_cache + 페이지 프래그먼트최소 (벌크 할당)RX 경로 (poll 내부)
netdev_alloc_skb()제한 없음per-CPU 페이지 프래그먼트낮음RX 경로 (일반)
__alloc_skb()제한 없음slab 할당자 직접 사용발생 가능TX 경로, 범용

napi_build_skb() 내부

napi_build_skb()는 드라이버가 이미 할당한 데이터 버퍼(page_pool 또는 드라이버 링 버퍼)에 skb 헤더만 감싸는 함수입니다. napi_alloc_skb()와 달리 데이터 영역을 새로 할당하지 않으므로, DMA로 직접 채워진 페이지를 즉시 skb로 변환할 수 있습니다.

/* 커널 소스 분석: net/core/skbuff.c — napi_build_skb() 구현 */

struct sk_buff *napi_build_skb(void *data, unsigned int frag_size)
{
    struct napi_alloc_cache *nc;
    struct sk_buff *skb;

    nc = this_cpu_ptr(&napi_alloc_cache);

    /* 1단계: napi_alloc_cache에서 skb 헤더만 가져오기
     *        데이터 버퍼는 이미 존재하므로 헤더만 필요 */
    if (unlikely(!nc->skb_count)) {
        nc->skb_count = kmem_cache_alloc_bulk(
            skbuff_cache, GFP_ATOMIC,
            NAPI_SKB_CACHE_BULK,
            nc->skb_cache);
        if (unlikely(!nc->skb_count))
            return NULL;
    }
    skb = nc->skb_cache[--nc->skb_count];

    /* 2단계: skb 초기화 후 기존 데이터 버퍼에 연결
     *        head, data, tail, end 포인터를 data 기반으로 설정 */
    memset(skb, 0, offsetof(struct sk_buff, tail));

    /* __build_skb_around: skb->head = data
     *                      skb->data = data
     *                      skb->tail = 0
     *                      skb->end  = frag_size - skb_shared_info 크기 */
    __build_skb_around(skb, data, frag_size);

    return skb;
}

/* 커널 소스 분석: __build_skb_around() — skb를 기존 데이터에 연결 */
static void __build_skb_around(struct sk_buff *skb,
                               void *data, unsigned int frag_size)
{
    struct skb_shared_info *shinfo;
    unsigned int size = frag_size;

    size -= SKB_DATA_ALIGN(sizeof(struct skb_shared_info));

    /* skb 포인터 설정: head/data → 버퍼 시작, end → 유효 영역 끝 */
    skb->head     = data;
    skb->data     = data;
    skb_reset_tail_pointer(skb);
    skb->end      = size;
    skb->mac_header = (__u16)~0U;
    skb->transport_header = (__u16)~0U;

    /* skb_shared_info 초기화 (데이터 끝 부분에 위치) */
    shinfo = skb_shinfo(skb);
    memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
    atomic_set(&shinfo->dataref, 1);

    skb->head_frag = 1;
    skb->truesize = SKB_TRUESIZE(size);
    refcount_set(&skb->users, 1);
}
현대적 고성능 RX 경로 패턴: 최신 NIC 드라이버는 page_pool에서 페이지를 할당하고, DMA로 패킷을 직접 기록한 뒤, napi_build_skb()로 skb 헤더만 감쌉니다. 데이터 복사가 전혀 없는 제로카피(Zero-copy) 수신 경로를 구현하는 핵심 패턴입니다.

napi_consume_skb() 내부

napi_consume_skb()는 NAPI 컨텍스트에서 skb를 해제하는 최적화된 함수입니다. 즉시 해제하지 않고 skb 헤더를 napi_alloc_cache에 반환하여 재사용합니다.

/* 커널 소스 분석: net/core/skbuff.c — napi_consume_skb() 구현 */

void napi_consume_skb(struct sk_buff *skb, int budget)
{
    /* NULL skb 안전 처리 */
    if (unlikely(!skb))
        return;

    /* 1단계: budget == 0이면 poll 외부 → 즉시 해제
     *        napi_complete_done() 또는 NAPI 외부에서 호출된 경우 */
    if (unlikely(!budget)) {
        dev_kfree_skb_any(skb);
        return;
    }

    /* 2단계: 참조 카운트 감소 — 다른 곳에서 참조 중이면 반환 */
    if (likely(refcount_read(&skb->users) == 1))
        smp_rmb();
    else if (likely(!refcount_dec_and_test(&skb->users)))
        return;

    /* 3단계: skb 데이터 영역 해제 (frag, frag_list 등) */
    skb_release_data(skb, SKB_CONSUMED);

    /* 4단계: skb 헤더를 napi_alloc_cache에 반환하여 재사용
     *        캐시가 가득 차면 kmem_cache_free_bulk()로 일괄 해제 */
    struct napi_alloc_cache *nc = this_cpu_ptr(&napi_alloc_cache);

    if (nc->skb_count == NAPI_SKB_CACHE_SIZE) {
        /* 캐시 만료: 절반을 slab으로 일괄 반환 */
        kmem_cache_free_bulk(skbuff_cache,
                            NAPI_SKB_CACHE_BULK,
                            nc->skb_cache);
        nc->skb_count -= NAPI_SKB_CACHE_BULK;
    }
    nc->skb_cache[nc->skb_count++] = skb;
}
budget 파라미터의 의미: napi_consume_skb(skb, budget)에서 budget > 0이면 NAPI poll 내부에서 호출된 것이므로 per-CPU 캐시에 안전하게 반환할 수 있습니다. budget == 0이면 poll 외부(예: napi_complete_done() 이후)에서 호출된 것이므로 즉시 dev_kfree_skb_any()로 해제합니다. TX completion 경로에서 잘못된 budget 값을 전달하면 per-CPU 캐시 오염이 발생할 수 있습니다.

napi_alloc_frag() 내부

napi_alloc_frag()는 per-CPU 페이지 프래그먼트 할당자(Page Fragment Allocator)를 사용하여 하나의 페이지를 여러 개의 작은 조각으로 나누어 할당합니다. 작은 패킷에 특히 효율적입니다.

/* 커널 소스 분석: net/core/skbuff.c — napi_alloc_frag() 구현 */

void *napi_alloc_frag(unsigned int fragsz)
{
    return napi_alloc_frag_align(fragsz, ~0u);
}

void *napi_alloc_frag_align(unsigned int fragsz, unsigned int align)
{
    struct napi_alloc_cache *nc = this_cpu_ptr(&napi_alloc_cache);

    /* page_frag_alloc_align: per-CPU 페이지 프래그먼트 캐시에서 할당
     *
     * 내부 동작:
     *   1. nc->page.va (현재 페이지)에 남은 공간이 있는지 확인
     *   2. 충분하면: offset 계산 후 va + offset 반환, offset 갱신
     *   3. 부족하면: __page_frag_cache_refill()로 새 페이지 할당
     *      - order-3 (32KB) 페이지 시도 → 실패 시 order-0 (4KB) 폴백
     *      - 새 페이지의 refcount를 높게 설정 (조각 수만큼)
     */
    return page_frag_alloc_align(&nc->page, fragsz,
                                  GFP_ATOMIC, align);
}

/* 커널 소스 분석: mm/page_frag_cache.c — 서브페이지 할당 핵심 */
void *page_frag_alloc_align(struct page_frag_cache *nc,
                           unsigned int fragsz,
                           gfp_t gfp_mask,
                           unsigned int align_mask)
{
    unsigned int size, offset;
    struct page *page;

    if (unlikely(!nc->va)) {
refill:
        /* 새 페이지 할당 (order-3 시도 → order-0 폴백) */
        page = __page_frag_cache_refill(nc, gfp_mask);
        if (!page)
            return NULL;
    }

    size = nc->size;
    offset = nc->offset;

    /* fragsz만큼 잘라서 반환 — 페이지 끝에서 시작으로 할당 */
    if (unlikely(offset < fragsz)) {
        /* 남은 공간 부족 → 새 페이지 할당 */
        page_frag_cache_drain(nc);
        goto refill;
    }

    offset = (offset - fragsz) & align_mask;
    nc->offset = offset;

    return nc->va + offset;
}

napi_get_frags() 내부

napi_get_frags()는 GRO 프래그먼트 경로에서 사용하는 캐시된 skb를 반환합니다. napi->skb에 부분적으로 구성된 skb를 유지하면서, 드라이버가 페이지 프래그먼트로 전달하는 RX 데이터를 누적합니다.

/* 커널 소스 분석: net/core/gro.c — napi_get_frags() 구현 */

struct sk_buff *napi_get_frags(struct napi_struct *napi)
{
    struct sk_buff *skb = napi->skb;

    /* 캐시된 skb가 있으면 그대로 반환 */
    if (!skb) {
        /* 없으면 새로 할당 — GRO_MAX_HEAD 크기의 선형 헤더 공간 확보
         * GRO_MAX_HEAD = MAX_HEADER + 128 (L2/L3/L4 헤더용) */
        skb = napi_alloc_skb(napi, GRO_MAX_HEAD);
        if (skb)
            napi->skb = skb;
    }
    return skb;
}

/* 드라이버 사용 패턴 예시 (frag 기반 RX) */
static void driver_rx_frag_path(struct napi_struct *napi,
                                 struct page *page,
                                 unsigned int offset,
                                 unsigned int len)
{
    struct sk_buff *skb = napi_get_frags(napi);

    if (unlikely(!skb))
        return;

    /* 헤더 데이터를 skb 선형 영역에 복사 */
    skb_fill_page_desc(skb, skb_shinfo(skb)->nr_frags,
                       page, offset, len);
    skb->len += len;
    skb->data_len += len;
    skb->truesize += len;

    /* GRO frag 경로로 전달 — napi->skb 캐시 자동 소비 */
    napi_gro_frags(napi);
}
캐시 라이프사이클: napi->skbnapi_get_frags()로 생성되어 napi_gro_frags()에서 소비됩니다. GRO 처리 후 napi->skb는 NULL로 리셋되어 다음 프래그먼트 수신 시 새로 할당됩니다. 하나의 NAPI 인스턴스에서 동시에 하나의 캐시 skb만 유지됩니다.

GRO 핵심 함수 내부 구현

GRO(Generic Receive Offload)는 수신된 작은 패킷들을 상위 스택 전달 전에 하나의 큰 패킷으로 병합하여 프로토콜 처리 오버헤드를 줄이는 메커니즘입니다. 이 절에서는 GRO의 핵심 함수들을 커널 소스 수준에서 분석합니다.

napi_gro_receive() 내부

napi_gro_receive()는 수신 패킷을 GRO 엔진에 전달하는 주요 진입점입니다. 내부적으로 dev_gro_receive()를 호출하여 패킷 병합을 시도합니다.

/* 커널 소스 분석: net/core/gro.c — napi_gro_receive() 전체 호출 체인 */

gro_result_t napi_gro_receive(struct napi_struct *napi,
                              struct sk_buff *skb)
{
    /* skb 타임스탬프 기록 (GRO 병합 판정에 사용) */
    skb_gro_reset_offset(skb, 0);

    return napi_skb_finish(napi, skb,
                           dev_gro_receive(napi, skb));
}

/* dev_gro_receive: GRO 엔진 핵심 */
static enum gro_result dev_gro_receive(struct napi_struct *napi,
                                        struct sk_buff *skb)
{
    struct list_head *head = &offload_base;
    struct packet_offload *ptype;
    struct sk_buff *pp = NULL;
    enum gro_result ret;
    __be16 type = skb->protocol;

    /* ── 1단계: gro_list_prepare() ──
     * gro_hash 버킷에서 same_flow 후보 탐색
     * 5-tuple {src_ip, dst_ip, src_port, dst_port, protocol} 비교 */
    gro_list_prepare(&napi->gro_hash[gro_hash_bucket(skb)], skb);

    /* rcu 보호 하에 등록된 프로토콜 오프로드 콜백 탐색 */
    rcu_read_lock();
    list_for_each_entry_rcu(ptype, head, list) {
        if (ptype->type != type || !ptype->callbacks.gro_receive)
            continue;

        /* ── 2단계: 프로토콜 오프로드 콜백 호출 ──
         * L3: inet_gro_receive() → L4: tcp4_gro_receive()
         * 콜백은 병합 가능한 기존 skb 포인터(pp)를 반환하거나
         * NULL을 반환 (병합 불가 또는 새 항목 추가) */
        skb->dev = napi->dev;
        skb_set_network_header(skb, skb_gro_offset(skb));
        pp = call_gro_receive(ptype->callbacks.gro_receive,
                              &napi->gro_hash[gro_hash_bucket(skb)],
                              skb);
        break;
    }
    rcu_read_unlock();

    /* ── 3단계: 결과 처리 ── */
    if (&ptype->list == head)
        goto normal;   /* 해당 프로토콜 오프로드 없음 */

    if (PTR_ERR(pp) == -EINPROGRESS) {
        ret = GRO_CONSUMED;
        goto ok;
    }

    if (same_flow)
        goto ok;       /* GRO_MERGED 또는 GRO_MERGED_FREE */

    if (NAPI_GRO_CB(skb)->flush)
        goto normal;

    /* 새 GRO 항목으로 gro_hash 버킷에 추가 */
    gro_list_add(&napi->gro_hash[gro_hash_bucket(skb)], skb);
    ret = GRO_HELD;
    goto ok;

normal:
    ret = GRO_NORMAL;
ok:
    return ret;
}

/* napi_skb_finish: GRO 결과에 따른 후처리 */
static gro_result_t napi_skb_finish(struct napi_struct *napi,
                                     struct sk_buff *skb,
                                     enum gro_result ret)
{
    switch (ret) {
    case GRO_NORMAL:
        /* GRO 불가 → 즉시 네트워크 스택으로 전달 */
        gro_normal_one(napi, skb, 1);
        break;

    case GRO_MERGED_FREE:
        /* 병합 완료 + 원본 skb 해제 */
        napi_consume_skb(skb, 1);
        break;

    case GRO_HELD:
    case GRO_MERGED:
    case GRO_CONSUMED:
        /* 병합됨/보류/소비됨 → 추가 작업 불필요 */
        break;
    }

    return ret;
}

gro_list_prepare() 내부

gro_list_prepare()는 수신 패킷의 5-tuple(소스 IP, 목적지 IP, 소스 포트, 목적지 포트, 프로토콜)을 해시하여 gro_hash 버킷을 선택하고, 기존 GRO 항목과 same_flow 비교를 수행합니다.

/* 커널 소스 분석: net/core/gro.c — gro_list_prepare() */

static void gro_list_prepare(struct gro_list *gro_list,
                             const struct sk_buff *skb)
{
    struct sk_buff *p;
    unsigned int maclen = skb->dev->hard_header_len;
    u32 hash = skb_get_hash_raw(skb);

    list_for_each_entry(p, &gro_list->list, list) {
        unsigned long diffs;

        NAPI_GRO_CB(p)->flush = 0;
        NAPI_GRO_CB(p)->same_flow = 0;

        if (unlikely(p->dev != skb->dev))
            continue;

        /* 1차 필터: 해시값 비교 (빠른 불일치 검출) */
        if (skb_get_hash_raw(p) != hash)
            continue;

        /* 2차 필터: MAC 헤더 바이트 단위 비교 */
        diffs = (unsigned long)p->dev ^ (unsigned long)skb->dev;
        diffs |= skb_vlan_tag_present(p) ^ skb_vlan_tag_present(skb);
        diffs |= compare_ether_header(
                    skb_mac_header(p),
                    skb_mac_header(skb));

        /* same_flow 판정: 모든 차이가 0이면 동일 플로우 */
        NAPI_GRO_CB(p)->same_flow = !diffs;
    }
}

GRO 병합 조건

조건설명검증 위치
same_flow == 15-tuple이 동일한 플로우gro_list_prepare()
프로토콜 핸들러 호환L3/L4 프로토콜이 GRO 콜백을 등록dev_gro_receive()
skb->len + new < 65535GRO 병합 최대 크기 미초과tcp4_gro_receive()
TCP 타임스탬프 일관성TCP timestamp 옵션이 단조 증가tcp4_gro_receive()
IP 옵션 없음IP options가 있으면 GRO 불가inet_gro_receive()
ECN 호환ECN 플래그가 호환 가능한 조합inet_gro_receive()
시퀀스 연속TCP 시퀀스 번호가 연속적tcp4_gro_receive()
napi_gro_receive() 내부 결정 트리 napi_gro_receive(napi, skb) gro_list_prepare(): 해시 버킷 탐색 same_flow? YES gro_receive() 콜백 호출 병합 가능? YES GRO_MERGED 기존 skb에 병합 NO GRO_NORMAL 즉시 스택 전달 NO flush? NO GRO_HELD gro_hash에 추가 YES GRO_NORMAL MERGED_FREE 병합 + skb 해제 결과: GRO_HELD (보류) GRO_MERGED / MERGED_FREE (병합) GRO_NORMAL (즉시 전달) 결정 분기

napi_gro_flush() 내부

napi_gro_flush()gro_hash 버킷에 보류 중인 모든 GRO 패킷을 네트워크 스택으로 전달(flush)합니다. 주로 napi_complete_done()에서 poll 종료 시, 또는 gro_flush_timeout 타이머 만료 시 호출됩니다.

/* 커널 소스 분석: net/core/gro.c — napi_gro_flush() 구현 */

void napi_gro_flush(struct napi_struct *napi, bool flush_old)
{
    struct sk_buff *skb, *p;
    unsigned long bitmask = napi->gro_bitmask;
    unsigned int i, base = ~0U;

    /* 각 gro_hash 버킷을 순회 */
    while ((i = ffs(bitmask)) != 0) {
        bitmask >>= i;
        base += i;

        list_for_each_entry_safe(skb, p,
                &napi->gro_hash[base].list, list) {

            /* flush_old == true: 오래된 패킷만 flush
             * NAPI_GRO_CB(skb)->age가 jiffies보다
             * gro_flush_timeout 이상 오래되었는지 확인 */
            if (flush_old &&
                NAPI_GRO_CB(skb)->age == jiffies)
                continue;

            /* GRO complete 콜백 호출:
             * L4 gro_complete → L3 gro_complete
             * TCP 체크섬 최종화, 헤더 정리 등 */
            skb_list_del_init(skb);
            napi_gro_complete(napi, skb);
            napi->gro_hash[base].count--;
        }

        /* 버킷이 비었으면 bitmask에서 해당 비트 해제 */
        if (list_empty(&napi->gro_hash[base].list))
            napi->gro_bitmask &= ~(1 << base);
    }
}

/* napi_gro_complete: 개별 GRO 패킷 완성 처리 */
static void napi_gro_complete(struct napi_struct *napi,
                              struct sk_buff *skb)
{
    struct packet_offload *ptype;
    __be16 type = skb->protocol;

    rcu_read_lock();
    list_for_each_entry_rcu(ptype, &offload_base, list) {
        if (ptype->type != type || !ptype->callbacks.gro_complete)
            continue;

        /* 프로토콜별 gro_complete 콜백:
         * inet_gro_complete → tcp4_gro_complete
         * TCP pseudo-header 체크섬 갱신, skb 메타데이터 정리 */
        ptype->callbacks.gro_complete(skb, 0);
        break;
    }
    rcu_read_unlock();

    /* 완성된 GRO 패킷을 gro_normal_list에 추가
     * 배치 전달: gro_normal_batch 개수 도달 시
     * netif_receive_skb_list()로 일괄 전달 */
    gro_normal_one(napi, skb, NAPI_GRO_CB(skb)->count);
}

GRO flush 타이밍 전략

상황flush 시점설정
poll 종료 (work < budget)napi_complete_done()에서 즉시 flush기본 동작 (설정 불필요)
타이머 기반gro_flush_timeout 나노초 후/sys/class/net/<dev>/gro_flush_timeout
카운트 기반gro_hash 버킷 항목 초과 시napi->gro_max_size (기본 65536)
강제 flushnapi_disable() 또는 디바이스 downnapi_gro_flush(napi, false)
지연 vs 처리량 트레이드오프: gro_flush_timeout을 길게 설정하면 더 많은 패킷이 병합되어 처리량이 향상되지만, 개별 패킷의 지연 시간이 증가합니다. 지연에 민감한 워크로드에서는 gro_flush_timeout = 0 (타이머 비활성)과 작은 napi_defer_hard_irqs 값을 사용합니다.

napi_gro_frags() 내부

napi_gro_frags()napi_get_frags()로 가져온 캐시 skb에 대해 GRO를 수행합니다. napi_gro_receive()와 유사하지만, 입력이 프래그먼트 리스트(Fragment List) 기반 skb라는 점이 다릅니다.

/* 커널 소스 분석: net/core/gro.c — napi_gro_frags() 구현 */

gro_result_t napi_gro_frags(struct napi_struct *napi)
{
    struct sk_buff *skb;
    gro_result_t ret;

    /* 1단계: napi->skb에서 캐시된 frag skb 가져오기 */
    skb = napi_frags_skb(napi);
    if (!skb)
        return GRO_DROP;

    /* 2단계: dev_gro_receive()로 GRO 엔진에 전달
     *        napi_gro_receive()와 동일한 병합 로직 수행 */
    ret = napi_frags_finish(napi, skb,
                            dev_gro_receive(napi, skb));

    return ret;
}

/* napi_frags_skb: frag skb를 GRO 처리용으로 준비 */
static struct sk_buff *napi_frags_skb(struct napi_struct *napi)
{
    struct sk_buff *skb = napi->skb;

    /* napi->skb 캐시 소비 (NULL로 리셋) */
    napi->skb = NULL;

    /* L2 헤더를 frag에서 선형 영역으로 pull
     * 최소 ETH_HLEN (14바이트)만큼 필요 */
    if (unlikely(!pskb_may_pull(skb, ETH_HLEN))) {
        napi_reuse_skb(napi, skb);
        return NULL;
    }

    /* eth_type_trans: MAC 헤더 파싱, skb->protocol 설정
     * skb->data를 L3 헤더 시작으로 이동 */
    skb->protocol = eth_type_trans(skb, skb->dev);

    /* GRO 오프셋 초기화 */
    skb_gro_reset_offset(skb, skb_headlen(skb));

    return skb;
}
napi_gro_receive() vs napi_gro_frags(): 두 함수 모두 동일한 dev_gro_receive() 엔진을 사용합니다. 차이점은 입력 형태입니다. napi_gro_receive()는 완전히 구성된 선형 skb를 받고, napi_gro_frags()napi->skb에 캐시된 프래그먼트 기반 skb를 사용합니다. 페이지 기반 RX를 사용하는 드라이버(예: ixgbe, mlx5)는 napi_gro_frags()를 선호합니다.

GRO 프로토콜별 오프로드 콜백

GRO 엔진은 packet_offload 구조체를 통해 등록된 프로토콜별 콜백을 호출합니다. 각 프로토콜은 gro_receive(병합 시도)와 gro_complete(완성 처리) 두 개의 콜백을 제공합니다.

/* 커널 소스 분석: include/linux/netdevice.h — packet_offload 구조체 */

struct packet_offload {
    __be16            type;   /* ETH_P_IP, ETH_P_IPV6 등 */
    u16               priority;
    struct offload_callbacks callbacks;
    struct list_head  list;
};

struct offload_callbacks {
    /* 병합 시도: 병합 가능한 기존 skb 포인터 반환 또는 NULL */
    struct sk_buff *(*gro_receive)(
        struct list_head *head,
        struct sk_buff *skb);

    /* 완성 처리: flush 시 호출, 체크섬 최종화 등 */
    int (*gro_complete)(
        struct sk_buff *skb,
        int nhoff);
};

/* 커널 소스 분석: net/ipv4/af_inet.c — IPv4 오프로드 등록 */
static struct packet_offload ip_packet_offload __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .callbacks = {
        .gro_receive = inet_gro_receive,
        .gro_complete = inet_gro_complete,
    },
};

/* 커널 소스 분석: net/ipv4/af_inet.c — inet_gro_receive() 핵심 흐름 */
struct sk_buff *inet_gro_receive(struct list_head *head,
                                 struct sk_buff *skb)
{
    const struct iphdr *iph;
    struct sk_buff *pp = NULL;
    const struct net_offload *ops;
    int proto;

    iph = skb_gro_header(skb, sizeof(*iph), 0);

    /* IP 헤더 검증: 버전, IHL, 프래그먼트 여부 */
    if (unlikely(iph->version != 4))
        goto out;
    if (unlikely(iph->ihl < 5))
        goto out;
    if (unlikely(iph->frag_off & htons(IP_MF | IP_OFFSET)))
        goto out;

    proto = iph->protocol;

    /* L4 오프로드 콜백 조회: TCP, UDP 등 */
    ops = rcu_dereference(inet_offloads[proto]);
    if (!ops || !ops->callbacks.gro_receive)
        goto out;

    /* same_flow 세부 검증: src/dst IP 비교 */
    list_for_each_entry(p, head, list) {
        const struct iphdr *iph2;
        if (!NAPI_GRO_CB(p)->same_flow)
            continue;
        iph2 = (struct iphdr *)(p->data + 0);
        if (iph->saddr != iph2->saddr ||
            iph->daddr != iph2->daddr) {
            NAPI_GRO_CB(p)->same_flow = 0;
            continue;
        }
    }

    /* L4 콜백 호출: 예) tcp4_gro_receive() */
    pp = call_gro_receive(ops->callbacks.gro_receive, head, skb);

out:
    return pp;
}

GRO 오프로드 콜백 등록 테이블

프로토콜gro_receivegro_complete등록 위치
IPv4inet_gro_receive()inet_gro_complete()net/ipv4/af_inet.c
IPv6ipv6_gro_receive()ipv6_gro_complete()net/ipv6/ip6_offload.c
TCP (v4)tcp4_gro_receive()tcp4_gro_complete()net/ipv4/tcp_offload.c
TCP (v6)tcp6_gro_receive()tcp6_gro_complete()net/ipv6/tcpv6_offload.c
UDP (v4)udp4_gro_receive()udp4_gro_complete()net/ipv4/udp_offload.c
UDP (v6)udp6_gro_receive()udp6_gro_complete()net/ipv6/udp_offload.c
VXLANvxlan_gro_receive()vxlan_gro_complete()drivers/net/vxlan/vxlan_core.c
GENEVEgeneve_gro_receive()geneve_gro_complete()drivers/net/geneve.c
GREgre_gro_receive()gre_gro_complete()net/ipv4/gre_offload.c
계층적 GRO 콜백 체인: GRO 콜백은 계층적으로 동작합니다. dev_gro_receive()가 L3 콜백(inet_gro_receive)을 호출하면, L3 콜백 내부에서 다시 L4 콜백(tcp4_gro_receive)을 호출합니다. 터널 프로토콜(VXLAN, GRE)의 경우 외부 L3/L4 → 터널 → 내부 L3/L4 순으로 재귀적으로 GRO가 수행됩니다.

멀티큐 NAPI와 스케일링

멀티큐 아키텍처

현대 NIC는 수십~수백 개의 하드웨어 RX 큐를 갖춥니다. 각 큐는 독립적인 napi_struct와 IRQ를 할당받아 서로 다른 CPU에서 병렬 처리됩니다. 이 구조가 RSS(Receive Side Scaling)의 기반입니다.

NIC (4큐 RSS) 10 Gbps RX Queue 0 RX Queue 1 RX Queue 2 RX Queue 3 napi_struct[0] IRQ → CPU 0 napi_struct[1] IRQ → CPU 1 napi_struct[2] IRQ → CPU 2 napi_struct[3] IRQ → CPU 3 softnet_data CPU 0 poll_list softnet_data CPU 1 poll_list softnet_data CPU 2 poll_list softnet_data CPU 3 poll_list TCP/IP 스택 GRO 완료 패킷 netif_receive_skb() → ip_rcv() → tcp_v4_rcv() 멀티큐 NAPI: RSS 기반 per-CPU 병렬 폴링 구조

RSS 해시 알고리즘: Toeplitz 해시

RSS는 Toeplitz 해시 함수를 사용하여 패킷을 큐에 분산합니다. 해시 입력은 IP/TCP 4-tuple이며, 하드웨어가 직접 계산합니다.

/* net/core/flow_dissector.c, include/net/flow_keys.h — 간략화 */
/* RSS Toeplitz 해시: 4-tuple (src_ip, dst_ip, src_port, dst_port) 기반 */
/* 128비트 무작위 해시 키(ethtool -x 출력)를 사용하여 큐 번호 결정 */

/* RSS 해시 조회 (소프트웨어 계산 시) */
u32 rss_toeplitz_hash(const u8 *key, u32 keylen,
                         const u8 *data, u32 datalen)
{
    u32 result = 0;
    u32 i, b;
    u32 key_data = 0;

    for (i = 0; i < keylen; i++)
        key_data = (key_data << 8) | key[i];

    for (b = 0; b < datalen * 8; b++) {
        if (data[b / 8] & (0x80 >> (b % 8)))
            result ^= key_data;
        key_data = (key_data << 1) |
                   ((key[(keylen - 1 - b / 8)] >> (b % 8)) & 1);
    }
    return result;
}

/* 큐 번호 결정: 해시값 → indirection table(RETA) 조회 */
/* RETA(Redirection Table): 128~512 엔트리, 각 엔트리가 큐 번호 */
u16 queue = reta[hash & (reta_size - 1)];

XPS: eXpress Path Send (송신 큐 CPU 어피니티)

RSS가 수신 큐를 CPU에 매핑하는 것처럼, XPS는 송신 큐도 CPU에 매핑합니다. 동일 CPU에서 RX/TX를 처리하여 캐시 지역성을 극대화합니다.

# XPS 설정: TX 큐 0을 CPU 0에 할당
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus

# XPS RXQS 모드: RX 큐와 동일한 CPU로 TX 큐 매핑 (RSS/XPS 통합)
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_rxqs

# 4큐 NIC에서 CPU-큐 1:1 대응 설정 스크립트
for i in 0 1 2 3; do
    echo $((1 << i)) > /sys/class/net/eth0/queues/tx-$i/xps_cpus
done

NUMA 토폴로지(Topology)와 NIC 큐 배치

PCIe 슬롯의 NUMA 노드와 NIC 큐를 처리하는 CPU의 NUMA 노드가 다르면 메모리 접근 레이턴시가 증가합니다. NUMA 노드를 확인하고 큐-CPU를 같은 노드로 배치해야 합니다.

# NIC의 NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node

# PCIe 슬롯의 NUMA 노드 확인 (PCI 주소 먼저 확인)
ethtool -i eth0 | grep bus-info
cat /sys/bus/pci/devices/0000:81:00.0/numa_node

# NUMA 노드 0의 CPU 목록 확인
numactl --hardware | grep "node 0 cpus"

# NUMA 노드 0에 속한 CPU에만 IRQ 어피니티 설정 (예: CPU 0-7이 NUMA 0)
for irq in $(cat /proc/interrupts | grep eth0 | awk '{print $1}' | tr -d ':'); do
    echo 00ff > /proc/irq/$irq/smp_affinity  # CPU 0-7 = 0x00ff
done

# 프로세스도 동일 NUMA 노드에 바인딩
numactl --cpunodebind=0 --membind=0 ./myapp

aRFS: accelerated Receive Flow Steering

aRFS는 HW flow director(Intel Ethernet 등)를 활용하여 특정 플로우를 특정 큐로 자동 라우팅(Routing)합니다. RFS가 소프트웨어로 CPU를 선택한다면, aRFS는 하드웨어가 직접 큐를 선택합니다.

/* aRFS: 커널이 ndo_rx_flow_steer()로 드라이버에 플로우→큐 매핑 설정 */
struct net_device_ops {
    /* ... */
    int  (*ndo_rx_flow_steer)(struct net_device *dev,
                              const struct sk_buff *skb,
                              u16 rxq_index,
                              u32 flow_id);
};
# aRFS 활성화 (ntuple 필터 지원 NIC 필요)
ethtool -K eth0 ntuple on

# RFS 전역 플로우 테이블 크기 설정 (aRFS도 이 테이블 활용)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# 큐별 플로우 수 설정
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

멀티큐 NAPI 드라이버 초기화 패턴

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
struct mynic_adapter {
    struct net_device   *netdev;
    int                  num_queues;
    struct mynic_rx_ring rx_rings[MYNIC_MAX_QUEUES];
};

static int mynic_open(struct net_device *netdev)
{
    struct mynic_adapter *adapter = netdev_priv(netdev);
    int i, err;

    for (i = 0; i < adapter->num_queues; i++) {
        struct mynic_rx_ring *ring = &adapter->rx_rings[i];

        netif_napi_add(netdev, &ring->napi,
                       mynic_poll, NAPI_POLL_WEIGHT);
        napi_enable(&ring->napi);

        err = request_irq(adapter->msix_entries[i].vector,
                           mynic_irq_handler, 0,
                           adapter->irq_names[i], ring);
        if (err)
            goto err_irq;

        irq_set_affinity_hint(adapter->msix_entries[i].vector,
                              cpumask_of(i % num_online_cpus()));
    }
    return 0;

err_irq:
    while (--i >= 0) {
        free_irq(adapter->msix_entries[i].vector, &adapter->rx_rings[i]);
        napi_disable(&adapter->rx_rings[i].napi);
        netif_napi_del(&adapter->rx_rings[i].napi);
    }
    return err;
}

실전 스크립트: 큐 수, IRQ 어피니티, XPS 일괄 설정

#!/bin/bash
# multiqueue_setup.sh: 멀티큐 NAPI 최적화 일괄 설정

NIC=eth0
NUM_QUEUES=8

# 1. 큐 수 설정 (NIC 지원 최대값 확인 후)
ethtool -L $NIC combined $NUM_QUEUES

# 2. irqbalance 중지 (수동 어피니티 설정 시 필수)
systemctl stop irqbalance

# 3. IRQ 어피니티: 각 큐 IRQ를 해당 CPU에 고정
i=0
for irq in $(grep "${NIC}-rx" /proc/interrupts | awk -F: '{print $1}'); do
    echo $((1 << i)) > /proc/irq/$irq/smp_affinity
    echo "IRQ $irq → CPU $i"
    i=$((i + 1))
    [ $i -ge $NUM_QUEUES ] && break
done

# 4. XPS 설정: TX 큐도 동일 CPU에 바인딩
for i in $(seq 0 $((NUM_QUEUES - 1))); do
    echo $((1 << i)) > /sys/class/net/$NIC/queues/tx-$i/xps_cpus
done

# 5. RPS/RFS 설정 (단일 큐 NIC 폴백 또는 추가 분산)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for i in $(seq 0 $((NUM_QUEUES - 1))); do
    echo $((0xFF << (i * 0))) > /sys/class/net/$NIC/queues/rx-$i/rps_cpus
    echo 2048 > /sys/class/net/$NIC/queues/rx-$i/rps_flow_cnt
done

# 6. 링 버퍼 크기 최대화
MAX_RX=$(ethtool -g $NIC | grep "RX:" | head -1 | awk '{print $2}')
ethtool -G $NIC rx $MAX_RX

echo "멀티큐 NAPI 설정 완료: $NIC, $NUM_QUEUES 큐"

RSS와 인터럽트 어피니티

# RX 큐별 IRQ 확인
cat /proc/interrupts | grep eth0

# IRQ 126을 CPU 3에 고정
echo 8 > /proc/irq/126/smp_affinity  # CPU 3 = 비트 3 = 0x8

# ethtool로 RSS 큐 수 확인/변경
ethtool -l eth0
ethtool -L eth0 combined 8

# RSS 해시 키 및 필드 설정
ethtool -x eth0
ethtool -X eth0 hkey <key>

# irqbalance 중지 후 수동 어피니티 설정 권장
systemctl stop irqbalance

스레드 NAPI (Threaded NAPI)

스레드 NAPI의 배경

기존 NAPI는 소프트IRQ 컨텍스트에서 실행되므로 실시간(RT) 커널과 충돌이 발생합니다. 소프트IRQ는 실시간 태스크(Task)보다 낮은 우선순위(Priority)를 가지지만, 선점(Preemption) 불가 구간에서 실행되므로 레이턴시 스파이크를 유발합니다. 스레드 NAPI(Threaded NAPI)는 poll()을 커널 스레드(Kernel Thread)로 옮겨 이 문제를 해결합니다.

스레드 NAPI 활성화 API

/* 스레드 NAPI 활성화 (드라이버 probe에서 또는 런타임에) */
int napi_set_threaded(struct napi_struct *napi, bool threaded);

/* 활성화 시 커널이 자동으로 스레드 생성:
   스레드 이름: "napi/<netdev_name>-<queue_idx>"
   예: "napi/eth0-0", "napi/eth0-1" */

/* 전체 디바이스에 대해 스레드 NAPI 활성화 */
void dev_set_threaded(struct net_device *dev, bool threaded);

napi_threaded_poll() 커널 스레드 함수 구현

/* net/core/dev.c: 스레드 NAPI의 커널 스레드 메인 함수 */
static int napi_threaded_poll(void *data)
{
    struct napi_struct *napi = data;
    struct net_device  *dev  = napi->dev;
    void               *have;

    while (!kthread_should_stop()) {
        /* 1. 처리할 패킷이 있을 때까지 대기 */
        do {
            set_current_state(TASK_INTERRUPTIBLE);
            if (kthread_should_stop())
                break;
            if (napi_schedule_prep(napi)) {
                __set_current_state(TASK_RUNNING);
                break;
            }
            schedule();  /* CPU 반납, wake_up_process()로 깨어남 */
        } while (1);

        if (kthread_should_stop())
            break;

        /* 2. local_bh_disable: softIRQ와의 동시 실행 방지 */
        local_bh_disable();
        have = netpoll_poll_lock(napi);

        /* 3. NAPI poll 실행: 드라이버 poll() 직접 호출 */
        if (test_bit(NAPI_STATE_SCHED_THREADED, &napi->state)) {
            napi_poll(napi, NULL);
        }

        netpoll_poll_unlock(have);
        local_bh_enable();
    }

    __set_current_state(TASK_RUNNING);
    return 0;
}

런타임 sysfs 제어

# 특정 NIC의 스레드 NAPI 활성화
echo 1 > /sys/class/net/eth0/threaded

# 스레드 NAPI 스레드 확인
ps aux | grep napi/eth0

# 스레드 우선순위 조정 (SCHED_FIFO RT 스케줄러 사용)
chrt -f -p 50 $(pgrep "napi/eth0-0")

# 스레드를 특정 CPU에 고정 (CPU 격리과 함께 사용)
taskset -p 0x10 $(pgrep "napi/eth0-0")  # CPU 4에 고정

# cgroup cpuset으로 스레드 격리
echo $(pgrep "napi/eth0-0") > /sys/fs/cgroup/cpuset/realtime/tasks

PREEMPT_RT와 소프트IRQ 스레드화

CONFIG_PREEMPT_RT가 활성화된 실시간 커널에서는 소프트IRQ가 자동으로 스레드화됩니다. 이 경우 ksoftirqd가 각 CPU에서 실시간 스케줄러(Scheduler)로 동작합니다.

환경NAPI 실행 컨텍스트선점 가능RT 태스크 우선순위 제어
일반 커널 + 기본 NAPI softIRQ (ksoftirqd) 불가 (선점 불가 구간) 불가
일반 커널 + 스레드 NAPI 커널 스레드 napi/<if>-N 가능 가능 (chrt, nice)
PREEMPT_RT + 기본 NAPI ksoftirqd/N (스레드화) 가능 (RT 스레드로 동작) 가능 (자동 스레드화)
PREEMPT_RT + 스레드 NAPI 커널 스레드 napi/<if>-N 가능 가능 (명시적 우선순위 설정)

스레드 NAPI 우선순위 정책 권장 표

정책설정 명령적용 시나리오특징
SCHED_OTHER (기본) chrt -o -p 0 <PID> 일반 서버, 배치 처리 nice 값 조절 가능, 우선순위 낮음
SCHED_FIFO + RT 우선순위 chrt -f -p 50 <PID> 실시간 처리, HFT, 저지연 응용 선점형 RT, 동일 우선순위 내 FIFO 순서
SCHED_RR + RT 우선순위 chrt -r -p 50 <PID> 여러 NIC 큐가 동일 우선순위 필요 시 동일 우선순위 라운드로빈, 공정성 보장
SCHED_DEADLINE chrt -d --sched-runtime 2ms --sched-deadline 10ms -p 0 <PID> 엄격한 데드라인 보장 필요 시 최악 지연 보장, 고급 설정 필요

스레드 NAPI + CPU 격리(Isolation) 조합

# GRUB 설정: CPU 8-15를 일반 스케줄러에서 격리
# /etc/default/grub: GRUB_CMDLINE_LINUX="isolcpus=8-15 nohz_full=8-15 rcu_nocbs=8-15"
update-grub && reboot

# 격리 후 스레드 NAPI를 격리 CPU에 배치
echo 1 > /sys/class/net/eth0/threaded
for i in $(seq 0 7); do
    pid=$(pgrep "napi/eth0-$i")
    taskset -p $((1 << (i + 8))) $pid  # CPU 8+i에 배치
    chrt -f -p 60 $pid                   # RT 우선순위 60
done

# cgroup cpuset으로 네트워크 전용 CPU 격리
mkdir -p /sys/fs/cgroup/cpuset/netpoll
echo 8-15 > /sys/fs/cgroup/cpuset/netpoll/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/netpoll/cpuset.mems
for pid in $(pgrep "napi/eth0"); do
    echo $pid > /sys/fs/cgroup/cpuset/netpoll/tasks
done

결정론적 지연(Deterministic Latency) 측정 방법

# cyclictest로 인터럽트 레이턴시 측정 (스레드 NAPI 효과 확인)
cyclictest -m -sp99 -d0 -i200 -l10000 --cpu=8

# hping3으로 왕복 레이턴시 측정 (마이크로초 단위)
hping3 -S --fast -p 80 --icmp target_ip 2>&1 | awk '/rtt/{print $NF}'

# perf latency 추적: NAPI poll 시작부터 소켓 수신까지
perf trace -e 'napi:napi_poll,sock:inet_sock_set_state' -a sleep 5

# bpftrace로 IRQ → NAPI poll 레이턴시 측정
bpftrace -e '
kprobe:__napi_schedule { @t[arg0] = nsecs; }
kprobe:napi_poll / @t[arg0] / {
    $lat = (nsecs - @t[arg0]) / 1000;
    @sched_to_poll_us = hist($lat);
    delete(@t[arg0]);
}
interval:s:5 { print(@sched_to_poll_us); }'

Linux 6.6부터 ethtool Netlink 인터페이스를 통해 개별 NAPI 인스턴스에 대한 세밀한 제어가 가능해졌습니다. ETHTOOL_MSG_NAPI_SET 명령으로 per-NAPI IRQ suspend timeout과 버지 폴링 파라미터를 설정할 수 있습니다.

/* net/ethtool/napi.c — 간략화 */
/* ethtool Netlink: per-NAPI 설정 (Linux 6.6+) */

/* NAPI ID 조회 */
/* ethtool --json -S eth0 로 napi_id 확인 가능 */

/* Netlink 명령 구조:
   ETHTOOL_MSG_NAPI_GET    → NAPI 인스턴스 목록/상태 조회
   ETHTOOL_MSG_NAPI_SET    → per-NAPI 파라미터 설정

   설정 가능 속성:
   ETHTOOL_A_NAPI_IRQ_SUSPEND_TIMEOUT  → IRQ 유예 타임아웃 (ns)
   ETHTOOL_A_NAPI_DEFER_HARD_IRQS      → 하드 IRQ 연기 횟수 */

/* 사용자 공간에서 per-NAPI 설정 예 (libnl 기반) */
struct nlattr *nla;
nla_put_u32(msg, ETHTOOL_A_NAPI_ID, napi_id);
nla_put_u64_64bit(msg, ETHTOOL_A_NAPI_IRQ_SUSPEND_TIMEOUT,
                  100000, /* 100μs */
                  ETHTOOL_A_NAPI_PAD);

IRQ/스레드 마이그레이션 전략

스레드 NAPI에서는 IRQ 어피니티와 NAPI 스레드 어피니티를 동기화하는 것이 중요합니다. 불일치 시 IRQ가 CPU A에서 발생하지만 poll()은 CPU B에서 실행되어 캐시 바운싱과 불필요한 IPI(Inter-Processor Interrupt)가 발생합니다.

전략IRQ 어피니티스레드 어피니티장점단점
동일 CPU 고정 CPU N CPU N 캐시 친화적, 최소 레이턴시 CPU 하나에 부하 집중
NUMA 노드 로컬 NUMA 0 CPU들 NUMA 0 CPU들 NUMA 교차 트래픽 회피 노드 내 부하 분산(Load Balancing) 필요
IRQ/스레드 분리 CPU N CPU M (격리) RT 환경에서 간섭 최소화 캐시 미스 증가
irqbalance 자동 동적 동적 관리 용이 마이그레이션 오버헤드
# IRQ와 NAPI 스레드를 동일 CPU에 고정하는 스크립트
# 1. IRQ 번호와 NAPI 스레드 PID 매핑
for q in $(seq 0 7); do
    irq=$(grep "eth0-TxRx-$q" /proc/interrupts | awk '{print $1}' | tr -d ':')
    pid=$(pgrep -f "napi/eth0-$q")
    cpu=$q

    # IRQ 어피니티 설정
    echo $((1 << cpu)) > /proc/irq/$irq/smp_affinity

    # NAPI 스레드도 동일 CPU에 고정
    taskset -p $((1 << cpu)) $pid
done

napi_thread_fn() 상태 전이 상세

스레드 NAPI의 커널 스레드는 NAPI_STATE_SCHED_THREADED 비트를 통해 softIRQ 경로와 구분됩니다. IRQ 핸들러에서 napi_schedule() 호출 시 이 비트의 존재 여부에 따라 softIRQ 또는 스레드 wake-up 경로가 선택됩니다.

/* net/core/dev.c — 간략화 */
/* napi_schedule() → 스레드 NAPI 경로 분기 */
void ____napi_schedule(struct softnet_data *sd,
                        struct napi_struct *napi)
{
    if (test_bit(NAPI_STATE_THREADED, &napi->state)) {
        /* 스레드 NAPI: kthread를 wake_up */
        if (!__napi_schedule_irqoff(napi))
            wake_up_process(napi->thread);
        return;
    }
    /* 일반 NAPI: softIRQ poll_list에 추가 */
    list_add_tail(&napi->poll_list, &sd->poll_list);
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

/* 상태 전이:
   [IRQ 발생]
     → napi_schedule_prep(): NAPI_STATE_SCHED 비트 설정
     → NAPI_STATE_THREADED 확인
       ├─ YES → wake_up_process(napi->thread)
       │        → napi_threaded_poll() 실행
       │        → napi_complete_done() → NAPI_STATE_SCHED 해제
       │        → schedule() (다음 IRQ 대기)
       └─ NO  → poll_list에 추가
                → NET_RX_SOFTIRQ 발생
                → net_rx_action() → poll()
                → napi_complete_done() → NAPI_STATE_SCHED 해제 */
소프트IRQ NAPI vs 스레드 NAPI 처리량 차이: 스레드 NAPI는 컨텍스트 전환 오버헤드로 인해 최대 처리량이 5~10% 낮을 수 있습니다. 그러나 PREEMPT_RT 환경이나 레이턴시 지터(jitter)가 중요한 환경에서는 스레드 NAPI가 훨씬 우수한 결정론적 레이턴시를 제공합니다. 고처리량 서버에서는 기본 NAPI, 레이턴시 민감 응용에서는 스레드 NAPI를 권장합니다.

NAPI 일시 중단 (IRQ Suspension)

배경과 필요성

Linux 6.3에서 도입된 NAPI IRQ Suspension은 유휴 상태의 NAPI 인스턴스에서 불필요한 인터럽트를 억제하여 전력 소비와 CPU 오버헤드를 줄이는 기능입니다. 멀티큐 NIC에서 일부 큐만 활성화되고 나머지는 유휴 상태인 경우가 흔한데, 기존에는 유휴 큐도 인터럽트를 주기적으로 받아 CPU를 깨웠습니다.

IRQ Suspension은 일정 기간 패킷이 도착하지 않은 NAPI 인스턴스의 인터럽트를 일시 중단하고, 패킷이 다시 도착하면 자동으로 재개합니다. 이는 특히 다음 환경에서 효과적입니다:

napi_suspend_irqs() / napi_resume_irqs() API

/* include/linux/netdevice.h */

/* IRQ 일시 중단: poll() 완료 시 유휴 판단 후 호출 */
bool napi_suspend_irqs(struct napi_struct *napi);

/* IRQ 재개: 패킷 도착 또는 타임아웃 시 호출 */
void napi_resume_irqs(struct napi_struct *napi);

/* napi_suspend_irqs() 내부 구현:
   1. NAPI_STATE_SCHED 비트 유지 (다른 스케줄링 차단)
   2. NIC의 해당 큐 인터럽트 마스킹
   3. gro_flush_timeout을 suspend timeout으로 활용
   4. 타이머 등록: timeout 만료 시 napi_resume_irqs() 호출

   반환값:
   true  → 성공적으로 중단됨
   false → 이미 스케줄됨 또는 중단 불가 */

/* napi_resume_irqs() 내부 구현:
   1. NAPI_STATE_SCHED 비트 해제
   2. NIC의 해당 큐 인터럽트 언마스킹
   3. 대기 중인 패킷이 있으면 즉시 napi_schedule() */

유휴 기간 최적화 전략

IRQ Suspension의 효과를 극대화하려면 Adaptive ITR과 결합하여 트래픽 패턴에 따라 suspension timeout을 동적으로 조절해야 합니다.

트래픽 패턴suspend timeout기대 효과설정 방법
고부하 지속 비활성 (0) IRQ가 항상 필요, 중단 불필요 napi_suspend_irqs() 호출 안 함
간헐적 버스(Bus)트 100~500μs 버스트 간 유휴 구간에서 IRQ 절약 gro_flush_timeout 활용
대부분 유휴 1~10ms CPU C-state 진입 빈도 증가, 전력 절감 Netlink per-NAPI 설정
완전 유휴 무한 (IRQ 완전 중단) 최대 전력 절감, 재개 시 레이턴시 증가 드라이버 유휴 감지 로직

드라이버 구현 예제

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* poll 함수에서 IRQ Suspension 통합 패턴 */
static int mynic_poll(struct napi_struct *napi, int budget)
{
    struct mynic_ring *ring = container_of(napi, struct mynic_ring, napi);
    int work_done = mynic_clean_rx(ring, budget);

    if (work_done < budget) {
        if (napi_complete_done(napi, work_done)) {
            /* 유휴 판단: 연속 N회 빈 poll이면 suspend */
            if (work_done == 0 && ++ring->idle_count > 3) {
                if (napi_suspend_irqs(napi)) {
                    ring->suspended = true;
                    return work_done;
                }
            }
            mynic_enable_rx_irq(ring);
        }
    }

    if (work_done > 0)
        ring->idle_count = 0;

    return work_done;
}

/* IRQ 핸들러에서 resume */
static irqreturn_t mynic_irq_handler(int irq, void *data)
{
    struct mynic_ring *ring = data;

    if (ring->suspended) {
        napi_resume_irqs(&ring->napi);
        ring->suspended = false;
        ring->idle_count = 0;
    }

    napi_schedule_irqoff(&ring->napi);
    return IRQ_HANDLED;
}
NAPI 전체 상태 머신 (Suspension 포함) INIT에서 ENABLED, SCHED, POLLING, COMPLETE, IDLE, SUSPENDED를 거치는 NAPI 상태 머신 전체 흐름 NAPI 전체 상태 머신 (IRQ Suspension 포함) INIT ENABLED SCHED POLLING COMPLETE IDLE SUSPENDED IRQ 마스킹됨 DISABLED DEL napi_enable IRQ 발생 poll 시작 budget 미소진 budget 소진 IRQ 활성화 새 패킷 napi_suspend_irqs napi_resume napi_disable __netif_napi_del 일반 전이: INIT → ENABLED → SCHED → POLLING → COMPLETE → IDLE (반복) Suspension 전이: IDLE → SUSPENDED → (패킷 도착) → SCHED (Linux 6.3+)
IRQ Suspension vs Coalescing: IRQ Suspension은 유휴 큐의 인터럽트를 완전히 중단하는 반면, Interrupt Coalescing은 활성 큐의 인터럽트 빈도를 조절합니다. 두 기법은 상호 보완적이며, 함께 사용하면 전력 효율과 레이턴시를 동시에 최적화할 수 있습니다.

전력 최적화: C-state와 NAPI Suspension

NAPI Suspension은 CPU C-state 진입과 밀접하게 관련됩니다. IRQ가 완전히 중단된 상태에서 CPU는 더 깊은 C-state로 진입할 수 있어 전력 소비를 크게 줄일 수 있습니다.

C-state진입 조건깨어남 레이턴시NAPI 영향
C0 (Active) 항상 0ns busy polling에 이상적
C1 (Halt) 짧은 유휴 ~1μs IRQ coalescing에 적합
C3 (Sleep) 중간 유휴 ~30-80μs IRQ suspension 필요, 재활성화 지연 발생
C6 (Deep Sleep) 장시간 유휴 ~100-200μs IRQ suspension 최적, 레이턴시 민감 환경 주의
# 레이턴시 민감 환경: 깊은 C-state 비활성화
# (busy polling 또는 낮은 gro_flush_timeout 사용 시)
cpupower idle-set -D 1  # C1까지만 허용

# 전력 효율 환경: IRQ suspension + 깊은 C-state
echo 10 > /sys/class/net/eth0/napi_defer_hard_irqs
echo 200000 > /sys/class/net/eth0/gro_flush_timeout  # 200μs

# 전력 소비 모니터링
turbostat --interval 5 --show PkgWatt,CorWatt,IRQ

버지 폴링 (Busy Polling)

버지 폴링의 개념과 원리

버지 폴링(Busy Polling)은 소켓 수신 대기 중에 커널이 NAPI poll()을 반복 호출하여 패킷이 도착하면 인터럽트나 소프트IRQ를 거치지 않고 즉시 처리하는 기법입니다. 레이턴시를 수십 마이크로초에서 수 마이크로초로 줄일 수 있지만, CPU를 100% 점유하는 트레이드오프가 있습니다.

일반 수신 경로는 패킷 → NIC DMA → HW IRQ → softIRQ → 소켓 버퍼 → epoll/recv 순서이지만, 버지 폴링은 recv/recvmsg() 호출 시 소켓이 속한 NAPI를 직접 폴링하여 HW IRQ/softIRQ 경로 자체를 우회합니다.

sk_napi_id 할당 경로

/* include/linux/netdevice.h, net/core/sock.c — 간략화 */
/* 패킷 수신 시 skb → sock → sk_napi_id 설정 경로 */

/* 1단계: napi_gro_receive()에서 skb에 napi_id 기록 */
static inline void skb_mark_napi_id(struct sk_buff *skb,
                                    struct napi_struct *napi)
{
    skb->napi_id = napi->napi_id;
}

/* 2단계: tcp_v4_rcv() → sk_mark_napi_id() → 소켓에 napi_id 전파 */
static inline void sk_mark_napi_id(struct sock *sk,
                                    const struct sk_buff *skb)
{
    if (READ_ONCE(sk->sk_napi_id) != skb->napi_id)
        WRITE_ONCE(sk->sk_napi_id, skb->napi_id);
}

/* 3단계: recvmsg() 진입 시 sk_napi_id로 버지 폴링 NAPI 결정 */
static inline int sock_recvmsg(struct socket *sock, struct msghdr *msg,
                                int flags)
{
    /* SO_BUSY_POLL 또는 SO_PREFER_BUSY_POLL 설정 시 버지 폴링 먼저 시도 */
    if (sk_can_busy_loop(sock->sk) &&
        skb_queue_empty_lockless(&sock->sk->sk_receive_queue))
        sk_busy_loop(sock->sk, flags & MSG_DONTWAIT);
    ...
}

소켓 수준 버지 폴링 설정

/* SO_BUSY_POLL: 폴링 대기 시간 설정 (마이크로초) */
int busy_poll_us = 50;  /* 50μs 동안 버지 폴링 */
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL,
           &busy_poll_us, sizeof(busy_poll_us));

/* SO_PREFER_BUSY_POLL: 항상 버지 폴링 선호 (Linux 5.11+) */
int val = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL,
           &val, sizeof(val));

/* SO_BUSY_POLL_BUDGET: NAPI poll()당 처리할 최대 패킷 수 (Linux 5.11+) */
int budget = 8;
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL_BUDGET,
           &budget, sizeof(budget));

epoll + 버지 폴링 연동

/* fs/eventpoll.c — 간략화 */
/* epoll_wait() 내부에서 BUSY_POLL 처리 흐름 */
/* ep_poll() → ep_busy_loop() → sk_busy_loop() 경로 */

static int ep_busy_loop(struct eventpoll *ep, int nonblock)
{
    unsigned int napi_id = ep_get_busy_poll_napi_id(ep);

    if (!napi_id)
        return false;

    return napi_busy_loop(napi_id,
                          nonblock ? NULL : ep_busy_loop_end,
                          ep,
                          prefer_busy_poll(ep),
                          ep->busy_poll_budget);
}

/* 실제 사용: epoll + SO_BUSY_POLL 조합 */
int setup_epoll_busy_poll(int epfd, int sockfd)
{
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = sockfd };

    /* 소켓에 버지 폴링 활성화 */
    int bp = 50;
    setsockopt(sockfd, SOL_SOCKET, SO_BUSY_POLL, &bp, sizeof(bp));
    int prefer = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_PREFER_BUSY_POLL, &prefer, sizeof(prefer));

    return epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
}

/* epoll_wait 호출 시 내부적으로 버지 폴링 먼저 시도 후 블록 */
int ready = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms);

io_uring + 버지 폴링

/* io_uring: IORING_FEAT_FAST_POLL을 통한 버지 폴링 통합 */
struct io_uring_params params = {};
int ring_fd = io_uring_setup(256, &params);

/* IORING_FEAT_FAST_POLL 지원 여부 확인 */
if (params.features & IORING_FEAT_FAST_POLL) {
    /* io_uring이 소켓의 버지 폴링을 자동으로 활용
       IORING_OP_RECV, IORING_OP_RECVMSG 등에서 적용됨 */
}

/* io_uring SQE 제출: 버지 폴링 활성화 플래그 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, sockfd, buf, sizeof(buf), 0);
/* io_uring은 SO_BUSY_POLL 설정된 소켓에 대해 자동으로 fast_poll 경로 사용 */
io_uring_submit(&ring);

버지 폴링 수신 경로 비교

일반 NAPI 수신 경로 NIC → HW IRQ softIRQ NET_RX napi_poll() 소켓 버퍼 추가 epoll/recv 깨우기 애플리케이션 수신 레이턴시: ~50~200μs 버지 폴링 수신 경로 NIC DMA 완료 recv() 진입 (소켓 버퍼 비어있음) sk_busy_loop() 직접 napi_poll() 패킷 도착 감지 (CPU 스핀) 즉시 recv() 반환 레이턴시: ~1~10μs CPU 100% 점유 일반 수신(좌) vs 버지 폴링(우): 레이턴시 vs CPU 사용 트레이드오프

내부 구현: napi_busy_loop()

/* net/socket.c의 recvmsg() → sock_recvmsg() 경로에서 호출 */
bool sk_busy_loop(struct sock *sk, int nonblock)
{
    unsigned int napi_id = READ_ONCE(sk->sk_napi_id);
    struct napi_struct *napi;

    if (napi_id < MIN_NAPI_ID)
        return false;

    napi = napi_by_id(napi_id);
    if (!napi)
        return false;

    return napi_busy_loop(napi_id, nonblock ? NULL : sk_busy_loop_end, sk,
                          prefer_busy_poll(sk), READ_ONCE(sk->sk_ll_usec));
}

/* napi_busy_loop: 지정된 NAPI를 직접 반복 폴링 */
bool napi_busy_loop(unsigned int napi_id,
                    bool (*loop_end)(void *, unsigned long),
                    void *loop_end_arg,
                    bool prefer_busy_poll,
                    u16 budget)
{
    struct napi_struct *napi;
    unsigned long       start_time = local_clock();

    do {
        rcu_read_lock();
        napi = napi_by_id(napi_id);
        if (napi) {
            /* NAPI_STATE_IN_BUSY_POLL 세팅으로 softIRQ와 동시 실행 방지 */
            if (!napi_try_get(napi))
                goto busy_loop_end;
            napi_poll(napi, NULL);  /* 직접 폴링 */
            napi_put(napi);
        }
        rcu_read_unlock();

        if (loop_end && loop_end(loop_end_arg, start_time))
            return true;
        cpu_relax();  /* PAUSE 명령으로 CPU 전력 절감 + 하이퍼스레딩 힌트 */
    } while (!need_resched());

busy_loop_end:
    rcu_read_unlock();
    return false;
}

레이턴시 비교 표

수신 방식p50 레이턴시p99 레이턴시CPU 사용적용 시나리오
인터럽트 기반 (pre-NAPI) 100~500μs 1~5ms 낮음 (IRQ 시) 저속 NIC, 단순 환경
NAPI (기본) 50~200μs 500μs~2ms 중간 범용 서버, 고처리량
버지 폴링 2~10μs 10~50μs 매우 높음 (100%) HFT, 실시간 게임, 금융 거래
XDP (native) 1~5μs 5~20μs 높음 (드라이버 종류 의존) 고성능 패킷 처리, DDoS 방어
AF_XDP (zero-copy) 1~3μs 3~15μs 높음 (전용 코어) 사용자 공간 패킷 처리, DPDK 대안

시스템 전역 설정

# 전역 기본 버지 폴링 시간 (μs, 0이면 비활성)
echo 50 > /proc/sys/net/core/busy_poll

# 전역 기본 버지 읽기 시간
echo 50 > /proc/sys/net/core/busy_read

sk_busy_loop() / sk_can_busy_loop() 내부

sk_busy_loop()는 소켓의 recvmsg()/epoll_wait() 경로에서 호출되어 지정된 시간 동안 NAPI를 직접 폴링합니다. 이 함수가 버지 폴링의 실체입니다.

/* net/core/dev.c: sk_busy_loop() 핵심 로직 */
void sk_busy_loop(struct sock *sk, int nonblock)
{
    unsigned int napi_id = READ_ONCE(sk->sk_napi_id);
    unsigned long end_time = busy_loop_end_time(sk, nonblock);
    int (*busy_poll)(
        struct napi_struct *napi, int budget);

    /* NAPI ID → napi_struct 조회 (해시 테이블) */
    struct napi_struct *napi = napi_by_id(napi_id);
    if (!napi)
        return;

    /* 반복 폴링 루프 */
    do {
        /* NAPI poll 직접 호출 (budget = SO_BUSY_POLL_BUDGET) */
        napi_busy_loop(napi, busy_poll,
                       sk_busy_loop_end, sk);
    } while (!sk_busy_loop_end(sk, end_time));
}

/* sk_can_busy_loop(): 버지 폴링 가능 여부 판단 */
static inline bool sk_can_busy_loop(struct sock *sk)
{
    return READ_ONCE(sk->sk_napi_id) &&
           !signal_pending(current) &&
           !need_resched();
    /* 조건: NAPI ID 바인딩 + 시그널 없음 + 재스케줄 불필요 */
}

/* 종료 조건 (sk_busy_loop_end):
   1. 소켓에 데이터 도착 (sk_rcvlowat 충족)
   2. timeout 만료 (busy_poll/busy_read 설정값)
   3. need_resched() — 다른 태스크가 CPU 요청
   4. signal_pending() — 시그널 수신 */

SO_INCOMING_NAPI_ID 소켓 옵션 활용

SO_INCOMING_NAPI_ID는 소켓에 마지막으로 패킷을 전달한 NAPI 인스턴스의 ID를 사용자 공간에서 조회할 수 있게 합니다. 이 정보를 활용하면 특정 NAPI(=특정 CPU)에 소켓을 어피니티 바인딩하여 캐시 효율을 극대화할 수 있습니다.

/* 사용자 공간: SO_INCOMING_NAPI_ID 조회 */
unsigned int napi_id;
socklen_t len = sizeof(napi_id);

getsockopt(fd, SOL_SOCKET, SO_INCOMING_NAPI_ID, &napi_id, &len);

/* napi_id를 활용한 CPU 어피니티 최적화:
   1. napi_id → IRQ 번호 → CPU 매핑 조회
   2. 워커 스레드를 해당 CPU에 고정
   3. epoll 그룹별 NAPI 어피니티 분리 */

/* ethtool Netlink로 NAPI 정보 조회 (Linux 6.6+) */
/* ETHTOOL_MSG_NAPI_GET:
   응답에 NAPI ID, IRQ 번호, 큐 인덱스,
   per-NAPI 통계 포함 */

/* 커널 내부: 소켓에 NAPI ID가 기록되는 시점 */
/* TCP RX: tcp_v4_rcv() → sk_mark_napi_id()
   UDP RX: udp_queue_rcv_skb() → sk_mark_napi_id()
   → sk->sk_napi_id = skb->napi_id; */

SO_PREFER_BUSY_POLL / SO_BUSY_POLL_BUDGET

Linux 5.11에서 추가된 이 소켓 옵션들은 per-소켓 버지 폴링을 더 세밀하게 제어합니다.

/* per-소켓 버지 폴링 설정 */
int prefer = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL,
           &prefer, sizeof(prefer));
/* 효과: 이 소켓의 NAPI에 NAPI_STATE_PREFER_BUSY_POLL 설정
   → napi_complete_done()에서 IRQ 재활성화를 지연
   → 버지 폴링 소켓이 독점적으로 NAPI 사용 가능 */

int budget = 32;
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL_BUDGET,
           &budget, sizeof(budget));
/* 효과: 버지 폴링 시 per-call budget 조절
   기본값 8 → 32로 증가 시 한 번의 폴링에서 더 많은 패킷 처리
   트레이드오프: 높은 budget = 높은 처리량, 긴 폴링 시간 */

/* NAPI_STATE_PREFER_BUSY_POLL 상호작용:
   napi_complete_done() 내부:
   if (test_bit(NAPI_STATE_PREFER_BUSY_POLL, &napi->state)) {
       // IRQ 재활성화를 gro_flush_timeout 후로 연기
       // → 버지 폴링 소켓이 다시 폴링할 기회 제공
       napi_schedule_irqoff(napi);  // 바로 재스케줄
       return false;
   } */
버지 폴링이 유리한 환경:
  • 초저지연 요구: HFT(고빈도 거래), 실시간 게임 서버, 금융 거래 시스템
  • CPU 여유가 있는 환경 (전용 코어 할당 가능)
  • 패킷 도착 간격이 수십 마이크로초 미만인 고속 스트리밍
버지 폴링이 불리한 환경:
  • CPU 집약적인 멀티태스킹 서버 (CPU 낭비)
  • 패킷 도착이 간헐적인 경우 (슬립(Sleep)이 더 효율적)
  • 배터리 기반 장치 (전력 소비 급증)

NAPI 해시 테이블과 소켓 바인딩

NAPI 해시 테이블 개요

커널은 모든 활성 NAPI 인스턴스를 전역 해시 테이블 napi_hash[]에 등록합니다. 이 테이블의 주요 목적은 NAPI ID로 napi_struct를 빠르게 조회하는 것이며, 버지 폴링과 Netlink 인터페이스에서 핵심적으로 사용됩니다.

/* net/core/dev.c */
#define NAPI_HASH_SIZE  256  /* 해시 버킷 수 (2^8) */

/* 전역 해시 테이블: hlist_head 배열 */
static struct hlist_head napi_hash[NAPI_HASH_SIZE];

/* 해시 함수: NAPI ID → 버킷 인덱스 */
static inline struct hlist_head *
napi_hash_bucket(unsigned int napi_id)
{
    return &napi_hash[napi_id % NAPI_HASH_SIZE];
}

/* NAPI ID는 per-net_device 순차 할당:
   netif_napi_add() → napi->napi_id = ++napi_gen_id;
   (전역 atomic counter) */

napi_hash_add() / napi_hash_del()

NAPI 인스턴스가 생성/삭제될 때 해시 테이블에 자동으로 추가/제거됩니다. RCU(Read-Copy-Update)로 보호되어 조회 측은 lock 없이 안전하게 접근할 수 있습니다.

/* net/core/dev.c */
static void napi_hash_add(struct napi_struct *napi)
{
    /* netif_napi_add()에서 자동 호출 */
    if (test_bit(NAPI_STATE_NO_BUSY_POLL, &napi->state))
        return;  /* 버지 폴링 비활성 NAPI는 해시 등록 생략 */

    spin_lock(&napi_hash_lock);
    hlist_add_head_rcu(&napi->napi_hash_node,
                       napi_hash_bucket(napi->napi_id));
    spin_unlock(&napi_hash_lock);
}

static void napi_hash_del(struct napi_struct *napi)
{
    spin_lock(&napi_hash_lock);
    hlist_del_init_rcu(&napi->napi_hash_node);
    spin_unlock(&napi_hash_lock);

    /* RCU grace period 대기:
       이미 napi_by_id()로 조회 중인 reader가
       안전하게 완료할 때까지 실제 해제 지연 */
    synchronize_rcu();
}

napi_by_id() — NAPI 조회

napi_by_id()는 NAPI ID를 키로 해시 테이블에서 napi_struct를 조회합니다. RCU read-side critical section 내에서 호출되며, 버지 폴링과 Netlink 인터페이스의 핵심입니다.

/* net/core/dev.c */
struct napi_struct *napi_by_id(unsigned int napi_id)
{
    struct napi_struct *napi;
    struct hlist_head *head =
        napi_hash_bucket(napi_id);

    /* RCU 보호 하에 해시 체인 순회 */
    hlist_for_each_entry_rcu(napi, head, napi_hash_node) {
        if (napi->napi_id == napi_id)
            return napi;
    }
    return NULL;
}

/* 사용처:
   1. sk_busy_loop() → napi_by_id(sk->sk_napi_id)
      → 소켓의 NAPI 인스턴스 직접 폴링
   2. ethtool Netlink ETHTOOL_MSG_NAPI_GET
      → NAPI 인스턴스 정보 조회
   3. SO_INCOMING_NAPI_ID getsockopt
      → 소켓에 바인딩된 NAPI ID 반환 */

sk_mark_napi_id() / sk_mark_napi_id_once()

소켓-NAPI 바인딩은 패킷 수신 경로에서 자동으로 이루어집니다. sk_mark_napi_id()는 수신된 SKB의 napi_id를 소켓에 기록하여 이후 버지 폴링 시 올바른 NAPI 인스턴스를 찾을 수 있게 합니다.

/* include/net/busy_poll.h */
static inline void sk_mark_napi_id(
    struct sock *sk,
    const struct sk_buff *skb)
{
    /* 매 패킷 수신 시 갱신 (마이그레이션 추적) */
    WRITE_ONCE(sk->sk_napi_id, skb->napi_id);
}

static inline void sk_mark_napi_id_once(
    struct sock *sk,
    const struct sk_buff *skb)
{
    /* 최초 한 번만 기록 (TCP 연결 수립 시) */
    if (!READ_ONCE(sk->sk_napi_id))
        WRITE_ONCE(sk->sk_napi_id, skb->napi_id);
}

/* 호출 경로:
   TCP: tcp_v4_rcv() → tcp_v4_do_rcv()
        → sk_mark_napi_id(sk, skb)
   UDP: udp_queue_rcv_skb()
        → sk_mark_napi_id(sk, skb)
   TCP Listener: tcp_v4_conn_request()
        → sk_mark_napi_id_once(sk, skb)

   skb->napi_id는 NAPI poll() 시작 시 설정:
   napi_gro_receive() → skb->napi_id = napi->napi_id; */

SO_INCOMING_NAPI_ID 상세

사용자 공간 애플리케이션은 SO_INCOMING_NAPI_ID로 소켓에 바인딩된 NAPI ID를 조회하고, 이를 기반으로 CPU 어피니티를 최적화할 수 있습니다.

/* 실전 패턴: NAPI 어피니티 기반 워커 배치 */
int optimize_worker_affinity(int sockfd)
{
    unsigned int napi_id;
    socklen_t len = sizeof(napi_id);

    /* 1. 소켓의 NAPI ID 조회 */
    getsockopt(sockfd, SOL_SOCKET,
               SO_INCOMING_NAPI_ID, &napi_id, &len);

    /* 2. /sys/class/net/eth0/napi_defer_hard_irqs 등으로
          NAPI ID → CPU 매핑 확인 */

    /* 3. 워커 스레드를 해당 CPU에 고정 */
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(target_cpu, &cpuset);
    sched_setaffinity(0, sizeof(cpuset), &cpuset);

    return 0;
}

/* Netlink 기반 NAPI 정보 조회 (Linux 6.6+):
   ETHTOOL_MSG_NAPI_GET → 응답:
   ETHTOOL_A_NAPI_ID           → NAPI 인스턴스 ID
   ETHTOOL_A_NAPI_IFINDEX      → 네트워크 인터페이스 인덱스
   ETHTOOL_A_NAPI_IRQ          → 연결된 IRQ 번호
   ETHTOOL_A_NAPI_PID          → 스레드 NAPI PID (있는 경우) */
NAPI 해시 테이블 구조 NAPI 해시 테이블의 버킷, hlist 체인, napi_by_id 조회 경로를 보여주는 구조도 NAPI 해시 테이블 (napi_hash[256]) napi_hash[] [0] napi #256 (eth0-0) napi #512 (eth1-0) NULL [1] napi #257 (eth0-1) NULL [2] napi #258 (eth0-2) NULL [255] NULL napi_by_id(257) 조회 1. 해시: 257 % 256 = 1 2. bucket[1] → hlist 순회 3. napi->napi_id == 257? 4. 일치! → napi_struct 반환 사용처: • sk_busy_loop() — 버지 폴링 • ETHTOOL_MSG_NAPI_GET 소켓 ↔ NAPI 바인딩 흐름 패킷 수신: skb->napi_id = napi->napi_id (poll 시작 시) 소켓 전달: sk_mark_napi_id(sk, skb) → sk->sk_napi_id = skb->napi_id napi_schedule() 내부 흐름 상세 napi_schedule 호출부터 스레드 NAPI와 softIRQ 분기까지의 내부 흐름 napi_schedule() 내부 흐름 IRQ 핸들러 napi_schedule_prep(napi) test_and_set_bit(NAPI_STATE_SCHED) 이미 스케줄됨 (무시) ____napi_schedule(sd, napi) NAPI_STATE_THREADED 확인 THREADED=1 THREADED=0 wake_up_process(napi->thread) 커널 스레드 깨우기 napi_threaded_poll() → poll() list_add_tail(poll_list) __raise_softirq(NET_RX_SOFTIRQ) net_rx_action() → poll()
NAPI 해시 테이블 크기와 충돌: NAPI_HASH_SIZE(256)는 대부분의 환경에서 충분합니다. 100G NIC의 64큐 × 4포트 = 256개 NAPI라도 해시 충돌은 제한적이며, RCU 기반 조회이므로 체인 길이가 짧다면 성능 영향은 무시할 수 있습니다.

XDP와 NAPI 연동

XDP의 실행 위치

XDP(eXpress Data Path)는 NAPI poll() 내부에서 패킷이 sk_buff로 변환되기 전에 실행됩니다. NIC 드라이버가 DMA에서 직접 페이지를 받아 XDP 프로그램에 전달하므로 커널 네트워크 스택(Network Stack) 오버헤드를 완전히 우회할 수 있습니다.

XDP 모드 비교

모드실행 위치드라이버 요구사항성능제약사항
Native XDP NAPI poll() 내부, sk_buff 생성 전 드라이버에 ndo_bpf 구현 필요 최고 (수백만 pps) 드라이버별 구현 필요, 멀티버퍼 제한 있음
Generic XDP netif_receive_skb() 이후 (sk_buff 생성됨) 드라이버 수정 불필요 (모든 NIC 지원) 중간 (sk_buff 오버헤드 있음) zero-copy 불가, 일부 XDP 기능 제한
Offloaded XDP NIC 하드웨어 내부 XDP offload 지원 NIC 필요 (Netronome 등) 최고 (호스트 CPU 사용 없음) BPF 명령어 제한, 지원 NIC 매우 적음

xdp_rxq_info와 napi_struct 연결

/* include/net/xdp.h — 간략화 */
/* XDP RX 큐 정보 구조체: NAPI와 XDP 프로그램을 연결 */
struct xdp_rxq_info {
    struct net_device  *dev;
    u32                 queue_index;
    u32                 reg_state;
    struct xdp_mem_info mem;
    unsigned int        napi_id;   /* NAPI 인스턴스 ID */
    u32                 frag_size;
} __rcu;

/* 드라이버 probe/open에서 xdp_rxq_info 등록 */
static int mynic_setup_xdp_rxq(struct mynic_rx_ring *ring)
{
    int err;

    /* XDP RX 큐 정보 등록 */
    err = xdp_rxq_info_reg(&ring->xdp_rxq,
                           ring->hw->netdev,
                           ring->queue_idx,
                           ring->napi.napi_id);  /* NAPI ID 연결 */
    if (err)
        return err;

    /* 메모리 모델 등록: page_pool 사용 시 */
    err = xdp_rxq_info_reg_mem_model(&ring->xdp_rxq,
                                       MEM_TYPE_PAGE_POOL,
                                       ring->page_pool);
    return err;
}

XDP metadata: xdp_buff.data_meta 활용

/* include/net/xdp.h — 간략화 */
/* XDP metadata 영역: data_meta ~ data 사이에 드라이버/BPF 메타데이터 저장 */
struct xdp_buff {
    void               *data;        /* 패킷 데이터 시작 */
    void               *data_end;    /* 패킷 데이터 끝 */
    void               *data_meta;   /* 메타데이터 시작 (data 이전) */
    void               *data_hard_start; /* 페이지 헤드룸 시작 */
    struct xdp_rxq_info *rxq;
    struct xdp_txq_info *txq;
    u32                  frame_sz;
    u32                  flags;
};

/* BPF 프로그램에서 메타데이터 조작 */
/* bpf_xdp_adjust_meta(ctx, delta): data_meta 포인터를 delta만큼 이동
   양수 delta: 메타데이터 영역 축소, 음수: 확장 */

/* 예: 드라이버가 타임스탬프를 메타데이터에 기록 */
struct meta {
    u64 rx_timestamp;
} *meta;

/* BPF 코드: */
int bpf_prog(struct xdp_md *ctx) {
    if (bpf_xdp_adjust_meta(ctx, -(int)sizeof(*meta)))
        return XDP_ABORTED;
    meta = (void *)(long)ctx->data_meta;
    meta->rx_timestamp = bpf_ktime_get_ns();
    return XDP_PASS;
}

AF_XDP zero-copy 경로 상세

/* include/net/xdp_sock_drv.h, net/xdp/xsk.c — 간략화 */
/* AF_XDP 소켓 구조: 사용자 공간 ↔ 커널 간 zero-copy 패킷 교환 */

/* 4개의 링 구조:
   1. UMEM fill ring: 사용자 → 커널 (빈 버퍼 공급)
   2. UMEM completion ring: 커널 → 사용자 (TX 완료 버퍼 반환)
   3. RX ring: 커널 → 사용자 (수신 패킷 전달)
   4. TX ring: 사용자 → 커널 (송신 패킷 전달) */

/* 드라이버 측 XDP_REDIRECT → xsk_map 경로 */
int xsk_rcv(struct xdp_sock *xs, struct xdp_buff *xdp)
{
    u64 addr;
    int err;

    /* 패킷 데이터를 UMEM RX 버퍼에 직접 기록 (zero-copy) */
    addr = xp_get_handle(xs->pool, xdp->data);
    err  = xskq_prod_reserve_desc(xs->rx, addr, xdp->data_end - xdp->data);
    if (err)
        return err;

    xsk_set_rx_need_wakeup(xs->pool);
    return 0;
}
# XDP 프로그램 로드 (native mode)
ip link set eth0 xdp obj myxdp.o

# offloaded mode: NIC 하드웨어에서 실행
ip link set eth0 xdpoffload obj myxdp.o

# generic mode: 스택 최상단 (드라이버 지원 불필요)
ip link set eth0 xdpgeneric obj myxdp.o

# AF_XDP 버지 폴링과 SO_PREFER_BUSY_POLL 결합
# → NAPI가 XDP verdict 처리 후 사용자 공간까지 레이턴시 최소화

XDP 처리 경로 다이어그램

NIC DMA RX 링 버퍼 XDP 프로그램 bpf_prog_run_xdp() xdp_buff 접근 XDP_PASS → sk_buff 생성 XDP_DROP → 즉시 드롭 XDP_TX → 동일 NIC 재전송 XDP_REDIRECT → 타 NIC/AF_XDP XDP_ABORTED → 오류 드롭+추적 TCP/IP 스택 napi_gro_receive() AF_XDP UMEM zero-copy RX ring Native XDP: NAPI poll() 내 sk_buff 생성 전 5가지 verdict 분기

XDP_REDIRECT와 xdp_do_flush()

중요: XDP_REDIRECT 후에는 반드시 xdp_do_flush()를 호출해야 합니다. 이 함수가 리다이렉트 큐를 플러시(Flush)하지 않으면 패킷이 목적지에 전달되지 않습니다. 일반적으로 napi_complete_done() 전에 호출합니다.
/* poll() 끝에서 XDP 리다이렉트 플러시 */
if (xdp_redirect_used) {
    xdp_do_flush();  /* 리다이렉트 큐 → 목적지로 일괄 전송 */
}
if (work_done < budget && napi_complete_done(napi, work_done))
    mynic_enable_rx_irq(ring);

NAPI poll() 내 XDP 처리 전체 흐름

드라이버 poll() 함수 내에서 XDP 프로그램이 실행되는 정확한 위치와 각 verdict별 처리 로직입니다. 이 코드는 실제 드라이버(ixgbe, ice, mlx5 등)의 공통 패턴을 정리한 것입니다.

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* 드라이버 poll() 내부의 XDP 처리 전체 흐름 (공통 패턴) */
static int mynic_poll_xdp(struct napi_struct *napi, int budget)
{
    struct mynic_rx_ring *ring = container_of(napi, ...);
    struct bpf_prog *xdp_prog;
    int work_done = 0;
    bool xdp_xmit = false;
    u32 act;

    /* RCU 보호 하에 XDP 프로그램 참조 (hot path 최적화) */
    xdp_prog = READ_ONCE(ring->xdp_prog);

    while (likely(work_done < budget)) {
        struct mynic_rx_desc *desc = mynic_get_rx_desc(ring);
        struct xdp_buff xdp;
        struct sk_buff *skb;

        if (!desc)
            break;

        /* DMA 동기화: 디바이스 → CPU */
        dma_sync_single_range_for_cpu(ring->dev,
            page_pool_get_dma_addr(desc->page),
            desc->offset, desc->len, DMA_FROM_DEVICE);

        if (xdp_prog) {
            /* ① xdp_buff 초기화: 페이지 → xdp_buff 변환 */
            xdp_init_buff(&xdp, PAGE_SIZE, &ring->xdp_rxq);
            xdp_prepare_buff(&xdp,
                page_address(desc->page) + desc->offset,
                ring->rx_headroom,  /* XDP headroom: 보통 256B */
                desc->len, true);

            /* ② XDP 프로그램 실행 (BPF JIT 코드 호출) */
            act = bpf_prog_run_xdp(xdp_prog, &xdp);

            switch (act) {
            case XDP_PASS:
                /* ③ 정상 경로: sk_buff 생성 → GRO/스택 전달 */
                break;  /* 아래 skb 생성으로 진행 */

            case XDP_TX:
                /* ④ 동일 NIC으로 재전송 */
                if (mynic_xmit_xdp_ring(ring, &xdp))
                    goto consumed;
                trace_xdp_exception(ring->netdev, xdp_prog, act);
                goto xdp_drop;

            case XDP_REDIRECT:
                /* ⑤ 다른 NIC/AF_XDP/cpumap으로 리다이렉트 */
                if (!xdp_do_redirect(ring->netdev, &xdp, xdp_prog)) {
                    xdp_xmit = true;
                    goto consumed;
                }
                goto xdp_drop;

            case XDP_ABORTED:
                /* ⑥ BPF 오류: 드롭 + xdp:xdp_exception 트레이스 */
                trace_xdp_exception(ring->netdev, xdp_prog, act);
                /* fallthrough */
            default: /* XDP_DROP */
xdp_drop:
                /* ⑦ 페이지를 page_pool로 즉시 반환 (sk_buff 미생성) */
                page_pool_recycle_direct(ring->page_pool, desc->page);
                goto consumed;
            }
        }

        /* XDP_PASS 또는 XDP 미설정: sk_buff 생성 */
        skb = napi_build_skb(page_address(desc->page) + desc->offset,
                              ring->rx_buf_len);
        if (unlikely(!skb))
            goto xdp_drop;

        /* XDP metadata → skb metadata 전파 */
        if (xdp_prog && xdp.data_meta < xdp.data)
            skb_metadata_set(skb, xdp.data - xdp.data_meta);

        skb_mark_for_recycle(skb);  /* page_pool 반환 표시 */
        napi_gro_receive(napi, skb);
consumed:
        work_done++;
    }

    /* poll() 종료 전: XDP_REDIRECT 버퍼 일괄 플러시 */
    if (xdp_xmit)
        xdp_do_flush();

    if (work_done < budget && napi_complete_done(napi, work_done))
        mynic_enable_rx_irq(ring);

    return work_done;
}

XDP 멀티버퍼 (Multi-buffer XDP)

Linux 6.0부터 XDP는 단일 페이지를 초과하는 프레임(점보 프레임, TSO 세그먼트 등)을 xdp_buffskb_shared_info frags 영역으로 처리할 수 있습니다. 이전에는 MTU > PAGE_SIZE인 패킷에 XDP를 적용할 수 없었습니다.

/* include/net/xdp.h — 간략화 */
/* XDP 멀티버퍼 구조: xdp_buff의 끝에 skb_shared_info가 위치 */
struct xdp_buff {
    void *data;           /* 선형 데이터 시작 */
    void *data_end;       /* 선형 데이터 끝 */
    void *data_meta;
    void *data_hard_start;
    struct xdp_rxq_info *rxq;
    struct xdp_txq_info *txq;
    u32 frame_sz;
    u32 flags;            /* XDP_FLAGS_HAS_FRAGS: 멀티버퍼 표시 */
};

/* 멀티버퍼 확인 */
static inline bool xdp_buff_has_frags(struct xdp_buff *xdp)
{
    return !!(xdp->flags & XDP_FLAGS_HAS_FRAGS);
}

/* 드라이버: 멀티버퍼 xdp_buff 구성 */
if (desc->len > ring->rx_buf_len) {
    /* 첫 페이지: 선형 데이터 */
    xdp_prepare_buff(&xdp, data, headroom, ring->rx_buf_len, true);

    /* 나머지 페이지들: frags로 추가 */
    struct skb_shared_info *sinfo = xdp_get_shared_info_from_buff(&xdp);
    sinfo->nr_frags = 0;

    for (frag_idx = 1; remaining > 0; frag_idx++) {
        skb_frag_fill_page_desc(&sinfo->frags[sinfo->nr_frags++],
                                next_page, 0, min(remaining, PAGE_SIZE));
        remaining -= PAGE_SIZE;
    }
    xdp.flags |= XDP_FLAGS_HAS_FRAGS;
}

/* BPF 헬퍼: 멀티버퍼 데이터 접근 */
/* bpf_xdp_load_bytes(ctx, offset, buf, len) - 프래그먼트 경계 투명 읽기
   bpf_xdp_store_bytes(ctx, offset, buf, len) - 프래그먼트 경계 투명 쓰기
   bpf_xdp_adjust_tail(ctx, delta)            - 프레임 크기 조정 */

XDP Hints / kfuncs (Linux 6.3+)

XDP hints는 NIC 하드웨어가 제공하는 메타데이터(RX 타임스탬프, 체크섬(Checksum), 해시 등)를 XDP 프로그램에서 직접 읽을 수 있게 하는 kfunc 기반 인터페이스입니다. 기존 data_meta 방식보다 타입 안전하고 표준화되어 있습니다.

/* include/net/xdp.h — 간략화 */
/* XDP hints kfunc 정의 (드라이버 측) */
struct xdp_metadata_ops {
    /* HW RX 타임스탬프 조회 */
    int (*xmo_rx_timestamp)(const struct xdp_md *ctx, u64 *timestamp);
    /* HW 해시값 조회 */
    int (*xmo_rx_hash)(const struct xdp_md *ctx, u32 *hash,
                        enum xdp_rss_hash_type *rss_type);
    /* VLAN 태그 조회 */
    int (*xmo_rx_vlan_tag)(const struct xdp_md *ctx,
                           __be16 *vlan_proto, u16 *vlan_tci);
};

/* BPF 프로그램에서 사용 (kfunc 호출) */
SEC("xdp")
int xdp_hints_prog(struct xdp_md *ctx)
{
    u64 timestamp;
    u32 hash;
    enum xdp_rss_hash_type hash_type;

    /* kfunc: 하드웨어 타임스탬프 읽기 (NIC가 지원하는 경우) */
    if (!bpf_xdp_metadata_rx_timestamp(ctx, &timestamp))
        bpf_printk("HW timestamp: %llu", timestamp);

    /* kfunc: RSS 해시 읽기 */
    if (!bpf_xdp_metadata_rx_hash(ctx, &hash, &hash_type))
        bpf_printk("RSS hash: 0x%x type: %d", hash, hash_type);

    return XDP_PASS;
}
XDP kfunc하드웨어 메타데이터지원 드라이버 (Linux 6.6+)
bpf_xdp_metadata_rx_timestamp() NIC RX 하드웨어 타임스탬프 (ns) mlx5, bnxt, ice, stmmac
bpf_xdp_metadata_rx_hash() RSS 해시값 + 해시 타입(L3/L4) mlx5, bnxt, ice, veth
bpf_xdp_metadata_rx_vlan_tag() VLAN proto + TCI mlx5, bnxt

cpumap: XDP를 통한 CPU 간 패킷 분산

BPF_MAP_TYPE_CPUMAP은 XDP에서 패킷을 다른 CPU로 리다이렉트하는 메커니즘으로, 소프트웨어 RPS의 XDP 버전입니다. NAPI poll()에서 XDP_REDIRECT를 통해 특정 CPU의 전용 큐로 패킷을 전송하며, 수신 CPU에서 sk_buff 생성과 스택 처리가 이루어집니다.

/* cpumap BPF 프로그램 */
struct {
    __uint(type, BPF_MAP_TYPE_CPUMAP);
    __uint(key_size, sizeof(u32));    /* CPU ID */
    __uint(value_size, sizeof(u32)); /* 큐 크기 */
    __uint(max_entries, 256);        /* 최대 CPU 수 */
} cpu_map SEC(".maps");

/* XDP에서 플로우 해시 기반 CPU 분산 */
SEC("xdp")
int xdp_cpumap_redirect(struct xdp_md *ctx)
{
    u32 cpu = (get_flow_hash(ctx)) % num_cpus;
    return bpf_redirect_map(&cpu_map, cpu, 0);
}

/* cpumap 내부 처리:
   1. 원래 CPU (NAPI poll 실행 CPU):
      - XDP_REDIRECT → cpumap 큐에 xdp_frame 인큐
      - ptr_ring 기반 lockless SPSC 큐
   2. 대상 CPU:
      - kthread가 ptr_ring에서 디큐
      - xdp_frame → sk_buff 변환
      - netif_receive_skb() 호출 → 정상 스택 경로
      - cpumap에서 2차 XDP 프로그램 실행 가능 (6.0+) */

devmap: XDP를 통한 NIC 간 포워딩

BPF_MAP_TYPE_DEVMAP/DEVMAP_HASH는 XDP에서 패킷을 다른 네트워크 인터페이스로 리다이렉트합니다. L2 스위칭, 로드 밸런싱, NAT 등에 사용됩니다.

/* devmap: 인터페이스 인덱스 → ifindex 매핑 */
struct {
    __uint(type, BPF_MAP_TYPE_DEVMAP_HASH);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(struct bpf_devmap_val));
    __uint(max_entries, 64);
} tx_port SEC(".maps");

/* bpf_devmap_val: devmap 엔트리에 2차 XDP 프로그램 부착 가능 */
struct bpf_devmap_val {
    __u32 ifindex;    /* 대상 인터페이스 */
    __u32 bpf_prog_fd; /* 선택적: TX 시 실행할 XDP 프로그램 */
};

/* L2 포워딩 예: MAC 주소 기반 출력 포트 결정 */
SEC("xdp")
int xdp_l2_forward(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data     = (void *)(long)ctx->data;
    struct ethhdr *eth = data;

    if (data + sizeof(*eth) > data_end)
        return XDP_DROP;

    /* FDB(MAC 테이블) 조회 → 출력 포트 결정 */
    u32 *port = bpf_map_lookup_elem(&fdb_map, eth->h_dest);
    if (!port)
        return XDP_PASS;  /* 알 수 없는 MAC → 커널 스택으로 */

    return bpf_redirect_map(&tx_port, *port, 0);
}

XDP 성능 비교

처리 경로64B pps (단일 코어)레이턴시 (p50)CPU 비용사용 사례
Native XDP_DROP ~24Mpps <1μs 최소 (sk_buff 미생성) DDoS 방어, ACL 필터
Native XDP_TX ~14Mpps ~2μs 낮음 (DMA 재매핑) 리플렉터, 헤어핀
XDP_REDIRECT (devmap) ~12Mpps ~3μs 낮음 (bulk flush) L2 스위칭, 로드밸런서
XDP_REDIRECT (AF_XDP) ~10Mpps ~4μs 중간 (UMEM 관리) 유저스페이스 패킷처리
XDP_PASS → GRO → TCP ~3Mpps ~10μs 높음 (전체 스택) 일반 네트워크 처리
Generic XDP ~2Mpps ~15μs 높음 (sk_buff 이미 생성) 디버깅(Debugging), 범용 필터링
iptables (Netfilter) ~1Mpps ~20μs 매우 높음 전통적 방화벽(Firewall)
XDP 성능 최적화 핵심: XDP가 NAPI poll() 내에서 sk_buff를 생성하기 전에 실행되므로, DROP/TX/REDIRECT 판정이 빠를수록 alloc_skb(), skb_put(), netif_receive_skb() 등의 비용을 완전히 회피합니다. 이것이 iptables 대비 10배 이상의 성능 차이를 만드는 핵심 요인입니다.

NAPI poll() 내 XDP 실행 타이밍

NAPI poll() 내 패킷 처리 단계별 비용 시간→ DMA 동기화 ~50ns XDP BPF 실행 ~30-100ns DROP/TX/REDIRECT 여기서 종료 (~100-200ns) sk_buff 생성 ~200ns GRO 병합 ~50-150ns netif_receive_skb() ~300-500ns (TC, Netfilter...) 누적 비용 비교: XDP DROP: ~80-150ns/pkt XDP PASS+GRO: ~400-600ns/pkt 전체 스택: ~800-1500ns/pkt XDP는 초기 단계에서 verdict를 내려 후속 스택 비용을 완전히 회피할 수 있음

XDP와 page_pool 연동

XDP는 page_pool과 밀접하게 연동됩니다. XDP_DROP 시 페이지를 page_pool로 즉시 재활용하고, XDP_TX/XDP_REDIRECT에서는 페이지 소유권이 전달 대상으로 이동한 뒤 TX 완료 시점에 page_pool로 반환됩니다.

XDP verdict페이지 소유권page_pool 반환 시점DMA 해제
XDP_DROP 드라이버 유지 즉시 (page_pool_recycle_direct()) 불필요 (DMA 매핑 유지)
XDP_PASS sk_buff로 이전 skb 해제 시 (skb_mark_for_recycle()) skb 해제 시 자동
XDP_TX TX 링으로 이전 TX 완료 인터럽트 시 TX 완료 시 (DMA 방향 전환)
XDP_REDIRECT 대상 디바이스/소켓 대상에서 소비 후 대상 디바이스의 DMA 매핑으로 교체
XDP_ABORTED 드라이버 유지 즉시 (DROP과 동일) 불필요

드라이버 구현 패턴

완전한 NAPI 드라이버 예제

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/pci.h>
#include <net/page_pool/api.h>

#define MYNIC_RX_DESC_NUM   256
#define MYNIC_TX_DESC_NUM   256
#define MYNIC_NAPI_WEIGHT  64

struct mynic_rx_ring {
    struct napi_struct   napi;
    struct mynic_hw     *hw;
    struct pci_dev      *pdev;
    struct page_pool    *page_pool;  /* page_pool 통합 */
    struct xdp_rxq_info  xdp_rxq;    /* XDP 큐 정보 */
    void                *desc_base;
    u16                  next_to_clean;
    u16                  next_to_alloc;
    u32                  queue_idx;
};

page_pool 통합

page_pool은 NAPI 전용 고성능 페이지 할당자(Page Allocator)입니다. DMA 재매핑 없이 페이지를 재활용하여 수신 경로의 메모리 할당 오버헤드를 대폭 줄입니다.

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* page_pool 생성 및 NAPI 연결 */
static int mynic_setup_page_pool(struct mynic_rx_ring *ring)
{
    struct page_pool_params pp_params = {
        .order     = 0,         /* 4K 페이지 */
        .flags     = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
        .pool_size = MYNIC_RX_DESC_NUM,
        .nid       = dev_to_node(&ring->pdev->dev),
        .dev       = &ring->pdev->dev,
        .napi      = &ring->napi,    /* NAPI와 page_pool 연결 */
        .dma_dir   = DMA_FROM_DEVICE,
        .offset    = NET_SKB_PAD,
        .max_len   = PAGE_SIZE - NET_SKB_PAD,
    };

    ring->page_pool = page_pool_create(&pp_params);
    return PTR_ERR_OR_ZERO(ring->page_pool);
}

/* RX 링 버퍼 할당: page_pool에서 페이지 가져오기 */
static int mynic_alloc_rx_buf(struct mynic_rx_ring *ring)
{
    struct page *page;
    dma_addr_t   dma;

    /* page_pool에서 DMA 매핑된 페이지 할당 (캐시에서 재활용) */
    page = page_pool_alloc_pages(ring->page_pool, GFP_ATOMIC | __GFP_NOWARN);
    if (unlikely(!page))
        return -ENOMEM;

    dma = page_pool_get_dma_addr(page);

    /* 디스크립터에 DMA 주소 등록 */
    mynic_set_rx_dma(ring, ring->next_to_alloc, dma);
    ring->pages[ring->next_to_alloc] = page;
    ring->next_to_alloc = (ring->next_to_alloc + 1) % MYNIC_RX_DESC_NUM;
    return 0;
}

/* SKB 생성 후 page_pool 재활용 마킹 */
static inline void mynic_build_skb(struct mynic_rx_ring *ring,
                                    struct page *page, u16 len)
{
    struct sk_buff *skb;

    skb = build_skb(page_address(page) + NET_SKB_PAD, PAGE_SIZE);
    if (unlikely(!skb)) {
        page_pool_recycle_direct(ring->page_pool, page);
        return;
    }

    skb_put(skb, len);
    /* skb_mark_for_recycle: sk_buff 해제 시 자동으로 page_pool에 반환 */
    skb_mark_for_recycle(skb);

    napi_gro_receive(&ring->napi, skb);
}

TX 완료 처리를 같은 poll()에서 처리하는 패턴

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* TX 완료와 RX 수신을 동일 poll()에서 처리 (인터럽트 절약) */
static int mynic_poll(struct napi_struct *napi, int budget)
{
    struct mynic_rx_ring *rx_ring =
        container_of(napi, struct mynic_rx_ring, napi);
    struct mynic_tx_ring *tx_ring = rx_ring->hw->tx_rings[rx_ring->queue_idx];
    int work_done = 0;
    bool tx_cleaned;

    /* 1. TX 완료 처리 먼저 (TX ring 공간 확보) */
    tx_cleaned = mynic_clean_tx_ring(tx_ring);

    /* 2. RX 패킷 처리 */
    while (work_done < budget) {
        struct page *page;
        u16 len;

        if (!mynic_get_rx_page(rx_ring, &page, &len))
            break;

        mynic_build_skb(rx_ring, page, len);
        work_done++;
    }

    /* 3. TX 완료 후 netdev_tx_completed_queue() 호출 */
    if (tx_cleaned)
        netif_tx_wake_all_queues(rx_ring->hw->netdev);

    if (work_done < budget && napi_complete_done(napi, work_done))
        mynic_enable_irq(rx_ring);

    return work_done;
}

에러 처리와 카운터 관리

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* 드라이버 통계 구조체 */
struct mynic_stats {
    u64 rx_packets;
    u64 rx_bytes;
    u64 rx_dropped;        /* 소프트웨어 드롭 (예: skb 할당 실패) */
    u64 rx_csum_errors;    /* 체크섬 오류 패킷 수 */
    u64 rx_missed;         /* HW 링 버퍼 오버플로우 (NIC 통계) */
    u64 rx_gro_packets;    /* GRO로 병합된 패킷 수 */
};

/* poll() 내 에러 처리 패턴 */
static void mynic_process_rx_desc(struct mynic_rx_ring *ring,
                                   struct mynic_rx_desc *desc)
{
    struct mynic_stats *stats = ring->stats;

    /* 체크섬 에러 감지 */
    if (unlikely(mynic_has_csum_error(desc))) {
        stats->rx_csum_errors++;
        /* CHECKSUM_NONE: 스택이 직접 체크섬 검증 수행 */
        ring->current_skb->ip_summed = CHECKSUM_NONE;
    }

    /* 드롭 처리: skb 할당 실패 */
    if (unlikely(!ring->current_skb)) {
        stats->rx_dropped++;
        return;
    }

    /* HW missed 카운터 주기적 폴링 (ethtool -S 출력용) */
    stats->rx_missed += mynic_read_rx_missed(ring->hw);
}

netdev_alloc_skb_ip_align() vs build_skb() vs napi_alloc_skb() 비교

함수메모리 출처DMA 매핑적용 시나리오특징
netdev_alloc_skb_ip_align() slab 할당자 별도 수행 필요 단순 드라이버, 소규모 패킷 IP 헤더 정렬(+2) 자동 처리
napi_alloc_skb() per-NAPI frag_list 캐시 별도 수행 필요 NAPI poll() 내부 빈번한 할당 NAPI 컨텍스트 최적화 할당, 캐시 재활용
build_skb() 기존 페이지(DMA 버퍼) 페이지 재사용 (zero-copy) page_pool, DMA 버퍼 직접 사용 복사 없음, 고성능 드라이버 표준
napi_build_skb() 기존 페이지 (page_pool) page_pool DMA 재활용 최신 드라이버 (6.x+) build_skb + page_pool 통합, 최적화

net_device_ops에서 napi_enable/disable

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
static int mynic_open(struct net_device *netdev)
{
    struct mynic_hw *hw = netdev_priv(netdev);
    int i;

    for (i = 0; i < hw->num_queues; i++) {
        napi_enable(&hw->rx_rings[i].napi);
        request_irq(..., mynic_msix_rx, 0, ..., &hw->rx_rings[i]);
    }
    netif_carrier_on(netdev);
    return 0;
}

static int mynic_stop(struct net_device *netdev)
{
    struct mynic_hw *hw = netdev_priv(netdev);
    int i;

    netif_carrier_off(netdev);
    for (i = 0; i < hw->num_queues; i++) {
        free_irq(hw->msix_entries[i].vector, &hw->rx_rings[i]);
        napi_disable(&hw->rx_rings[i].napi);
    }
    return 0;
}

실전 드라이버 참조 — ICE NAPI 패턴

Intel E810(ice) 드라이버는 큐 벡터-NAPI 1:1 매핑 구조를 사용하며, 각 큐 벡터(ice_q_vector)가 하나의 NAPI 인스턴스를 소유합니다. 이 패턴은 앞서 설명한 mynic 예제의 실전 적용 사례입니다.

mynic 예제ICE 드라이버 대응설명
struct mynic_rx_ringstruct ice_rx_ring디스크립터 링, page_pool 포함
mynic_poll()ice_napi_poll()RX clean + TX clean 통합 poll
mynic_rx_clean()ice_clean_rx_irq()디스크립터 → skb 변환, GRO 전달
mynic_tx_clean()ice_clean_tx_irq()완료된 TX 디스크립터 해제
Adaptive coalescingice_update_itr()poll 완료 시 Adaptive ITR 갱신
MYNIC_NAPI_WEIGHTNAPI_POLL_WEIGHT (64)budget 기본값 동일
/* drivers/net/ethernet/intel/ice/ice_txrx.c — 간략화 */
/* ICE NAPI poll 구조 (단순화) */
int ice_napi_poll(struct napi_struct *napi, int budget)
{
    struct ice_q_vector *q_vector =
        container_of(napi, struct ice_q_vector, napi);
    bool clean_complete = true;
    int budget_per_ring;

    /* TX 완료 처리 (budget 무관) */
    ice_for_each_tx_ring(tx_ring, q_vector->tx) {
        if (!ice_clean_tx_irq(tx_ring, budget))
            clean_complete = false;
    }

    /* RX 처리 (budget 분배) */
    budget_per_ring = max(budget / q_vector->num_ring_rx, 1);
    ice_for_each_rx_ring(rx_ring, q_vector->rx) {
        int cleaned = ice_clean_rx_irq(rx_ring, budget_per_ring);
        if (cleaned >= budget_per_ring)
            clean_complete = false;
    }

    /* 완료 시: NAPI complete + Adaptive ITR 갱신 + IRQ 재활성화 */
    if (clean_complete && napi_complete_done(napi, budget)) {
        ice_update_itr(q_vector);        /* ← Adaptive ITR 핵심 */
        ice_enable_interrupt(q_vector);
    }
    return min(budget, work_done);
}
ICE Adaptive ITR 갱신 포인트: ice_update_itr()napi_complete_done() 직후에 호출됩니다. 이 시점에서 최근 poll 사이클의 바이트/패킷 통계를 기반으로 다음 인터럽트 간격을 결정합니다. HW 타이머 해상도가 4μs이므로 설정값은 항상 4의 배수로 반올림됩니다.

TX 전용 NAPI 패턴 (netif_napi_add_tx())

netif_napi_add_tx()는 TX 완료 처리 전용 NAPI를 등록합니다. RX+TX 결합 NAPI에서 TX 완료가 RX budget을 소비하지 않도록 분리할 때 사용합니다. weight가 고정(NAPI_POLL_WEIGHT)이며, TX 전용이므로 NAPI_STATE_NO_BUSY_POLL이 자동 설정됩니다.

/* include/linux/netdevice.h */
static inline void
netif_napi_add_tx(struct net_device *dev,
                   struct napi_struct *napi,
                   int (*poll)(struct napi_struct *, int))
{
    netif_napi_add_weight(dev, napi, poll, NAPI_POLL_WEIGHT);
    set_bit(NAPI_STATE_NO_BUSY_POLL, &napi->state);
}

/* RX+TX 결합 vs RX/TX 분리 NAPI 비교:
 *
 * 결합 패턴 (ice, ixgbe 등):
 *   poll() {
 *     ice_clean_tx_irq();  // TX 완료 (budget 무관)
 *     ice_clean_rx_irq();  // RX 처리 (budget 사용)
 *   }
 *   장점: IRQ 1개, 컨텍스트 전환 최소
 *   단점: TX 지연이 RX에 영향
 *
 * 분리 패턴 (mlx5 등):
 *   rx_poll() { mlx5_rx_clean(); }
 *   tx_poll() { mlx5_tx_clean(); }  // netif_napi_add_tx()
 *   장점: RX/TX 독립 budget, 세밀한 제어
 *   단점: IRQ 2개 필요, 약간의 오버헤드 */

/* TX 전용 poll 구현 예 */
static int mynic_tx_poll(struct napi_struct *napi, int budget)
{
    struct mynic_tx_ring *ring =
        container_of(napi, struct mynic_tx_ring, napi);
    int cleaned = mynic_clean_tx(ring, budget);

    if (cleaned < budget && napi_complete_done(napi, cleaned))
        mynic_enable_tx_irq(ring);

    /* 정지된 TX 큐 재개 */
    if (netif_tx_queue_stopped(ring->txq) &&
        mynic_tx_avail(ring) > MYNIC_TX_WAKE_THRESH)
        netif_tx_wake_queue(ring->txq);

    return cleaned;
}

napi_consume_skb() TX 정리 패턴

TX 완료 경로에서 전송 완료된 SKB를 해제할 때는 반드시 napi_consume_skb()를 사용해야 합니다. budget 인자를 전달하면 per-CPU 캐시를 활용한 bulk free가 가능해져 dev_kfree_skb_any() 대비 최대 30% 성능 향상을 얻을 수 있습니다.

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* TX 완료 정리 — 올바른 패턴 */
static bool mynic_clean_tx(struct mynic_tx_ring *ring, int budget)
{
    unsigned int total_bytes = 0, total_pkts = 0;
    u16 ntc = ring->next_to_clean;

    while (total_pkts < (unsigned int)budget) {
        struct mynic_tx_buf *buf = &ring->tx_buf[ntc];

        /* HW 소유 디스크립터는 건너뜀 */
        if (!mynic_tx_desc_done(ring, ntc))
            break;

        /* DMA 언매핑 */
        dma_unmap_single(&ring->pdev->dev,
                        buf->dma, buf->len, DMA_TO_DEVICE);

        total_bytes += buf->skb->len;
        total_pkts++;

        /* ★ napi_consume_skb: budget 전달 필수! */
        napi_consume_skb(buf->skb, budget);
        buf->skb = NULL;

        ntc = (ntc + 1) % ring->count;
    }

    ring->next_to_clean = ntc;

    /* 통계 갱신 (struct u64_stats_sync 보호) */
    u64_stats_update_begin(&ring->syncp);
    ring->stats.bytes += total_bytes;
    ring->stats.packets += total_pkts;
    u64_stats_update_end(&ring->syncp);

    return total_pkts < budget;
}
TX 완료 경로 TX DMA 완료 인터럽트부터 NAPI poll, 링 스캔, consume_skb, 큐 재개까지의 TX 완료 처리 흐름 TX 완료 경로 (NAPI poll 내부) TX DMA 완료 HW IRQ 발생 NAPI poll() clean_tx 호출 TX 링 스캔 next_to_clean → HW 포인터 DMA 언매핑 dma_unmap_single() napi_consume_skb(skb, budget) per-CPU 캐시 반환 (bulk free) 통계 갱신 (bytes/pkts) netif_tx_wake_queue() 정지된 TX 큐 재개 (공간 확보) napi_consume_skb budget 동작 budget > 0 → napi_skb_cache_put (fast) budget = 0 → kfree_skb_reason (slow)

드라이버 메모리 전략 선택 가이드

기준napi_alloc_skb + memcpynapi_build_skb + page_poolheader split + frags
구현 난이도 낮음 (가장 단순) 중간 높음
패킷 크기 최적 소형 (64~256B) 대형 (1500B+) 대형 + 헤더 분리
CPU 사용률 높음 (memcpy) 낮음 (제로카피) 가장 낮음
DMA 관리 드라이버 직접 page_pool 자동 page_pool + frag
메모리 효율 중간 높음 (재활용) 최고 (재활용 + 슬라이싱)
대표 드라이버 e100, 8139too ice, ixgbe, mlx5 bnxt, gve
권장 시나리오 레거시/저속 NIC 범용 고성능 NIC 100G+ 초고성능

napi_consume_skb() vs dev_kfree_skb_*() 비교표

함수컨텍스트캐시 활용성능사용 시점
napi_consume_skb(skb, budget) NAPI poll (budget > 0) per-CPU napi_skb_cache 최고 (bulk free) TX 완료 정리 (poll 내부)
dev_kfree_skb_any(skb) IRQ 또는 프로세스 없음 중간 컨텍스트 불확실할 때
dev_kfree_skb_irq(skb) IRQ 컨텍스트 전용 없음 중간 IRQ 핸들러 내 해제
consume_skb(skb) 프로세스 컨텍스트 없음 낮음 일반 SKB 해제
kfree_skb(skb) 어디서든 없음 낮음 (drop 추적) 에러/드롭 경로

성능 튜닝과 파라미터

핵심 커널 파라미터

파라미터경로기본값고처리량 권장값저지연 권장값
netdev_budget /proc/sys/net/core/netdev_budget 300 600~1200 100~200
netdev_budget_usecs /proc/sys/net/core/netdev_budget_usecs 8000 16000 2000
netdev_max_backlog /proc/sys/net/core/netdev_max_backlog 1000 10000 1000
gro_flush_timeout /proc/sys/net/core/gro_flush_timeout 0 100000 ns 0 (비활성)
napi_defer_hard_irqs /proc/sys/net/core/napi_defer_hard_irqs 0 64 0
busy_poll /proc/sys/net/core/busy_poll 0 0 50
busy_read /proc/sys/net/core/busy_read 0 0 50

인터럽트 코얼레싱(Interrupt Coalescing) 개요

인터럽트 코얼레싱은 NIC가 패킷 하나마다 즉시 IRQ를 발생시키지 않고, 일정 패킷 수(rx-frames) 또는 일정 시간(rx-usecs)이 경과한 후에 IRQ를 발생시키는 기법입니다. 하드웨어 타이머 동작 원리, Adaptive ITR 알고리즘, 드라이버별 구현 비교 등 내용은 아래 전용 섹션에서 다룹니다.

ITR(Interrupt Throttle Rate) 하드웨어 섹션 참조

ethtool 설정

# 인터럽트 코얼레싱 설정
ethtool -C eth0 rx-usecs 50 tx-usecs 50 rx-frames 16

# adaptive 코얼레싱 활성화
ethtool -C eth0 adaptive-rx on adaptive-tx on

# 현재 코얼레싱 설정 확인
ethtool -c eth0

# 링 버퍼 크기 조정
ethtool -G eth0 rx 4096 tx 4096

# RSS 큐 수 설정
ethtool -L eth0 combined 16

# GRO/LRO 활성화 확인
ethtool -k eth0 | grep -E "generic-receive-offload|large-receive-offload"

# GRO 비활성화 (디버깅 목적)
ethtool -K eth0 gro off

C-state 영향과 설정

CPU C-state가 깊을수록 절전 효과는 크지만, C-state 탈출(wakeup) 레이턴시가 증가합니다. 이는 첫 번째 인터럽트 처리 레이턴시에 직접 영향을 줍니다.

C-state전력 절감복귀 레이턴시NAPI 영향
C0 (활성) 없음 0μs 영향 없음
C1 (HALT) 낮음 ~1μs 거의 없음
C3 (Sleep) 중간 ~30~100μs 첫 IRQ 레이턴시 증가
C6 (Deep Sleep) 높음 ~100~300μs 심각한 레이턴시 스파이크 가능
# C-state 제한: C1 이하만 허용 (저지연 필요 시)
echo 1 > /sys/devices/system/cpu/cpu0/cpuidle/state2/disable  # C3 비활성화
echo 1 > /sys/devices/system/cpu/cpu0/cpuidle/state3/disable  # C6 비활성화

# 전체 CPU에 적용 (bash 루프)
for cpu in /sys/devices/system/cpu/cpu*/cpuidle/state[2-9]; do
    echo 1 > $cpu/disable
done

# GRUB 설정으로 영속화: intel_idle.max_cstate=1 또는 processor.max_cstate=1

# tuned-adm으로 레이턴시 프로파일 적용
tuned-adm profile latency-performance

NUMA 최적화

# NIC의 NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node
# 결과 예: 1  → NUMA 노드 1에 연결된 NIC

# NUMA 노드 1의 CPU 목록 확인
numactl --hardware | grep "node 1 cpus"
# 결과 예: node 1 cpus: 8 9 10 11 12 13 14 15

# NUMA 노드 1의 CPU에만 IRQ 어피니티 설정
for irq in $(grep eth0 /proc/interrupts | cut -d: -f1); do
    echo ff00 > /proc/irq/$irq/smp_affinity  # CPU 8-15 = 0xff00
done

# 애플리케이션을 동일 NUMA 노드에서 실행
numactl --cpunodebind=1 --membind=1 ./server_app

시나리오별 튜닝 가이드

시나리오 A: 최대 처리량 (데이터센터, CDN)
  • RSS 큐를 CPU 코어 수만큼 설정, IRQ 어피니티 1:1 매핑
  • netdev_budget=1200, netdev_budget_usecs=16000
  • GRO 활성화, gro_flush_timeout=200000
  • Adaptive 코얼레싱 활성화 (adaptive-rx on)
sysctl -w net.core.netdev_budget=1200
sysctl -w net.core.netdev_budget_usecs=16000
sysctl -w net.core.gro_flush_timeout=200000
sysctl -w net.core.napi_defer_hard_irqs=128
ethtool -C eth0 rx-usecs 100 adaptive-rx on
ethtool -G eth0 rx 8192
시나리오 B: 초저지연 (HFT, 실시간 거래)
  • 버지 폴링 활성화 (SO_BUSY_POLL)
  • GRO 비활성화, C-state 제한
  • 코얼레싱 최소화 (rx-usecs=0, rx-frames=1)
  • CPU 격리 + 스레드 NAPI
sysctl -w net.core.netdev_budget=64
sysctl -w net.core.netdev_budget_usecs=1000
sysctl -w net.core.gro_flush_timeout=0
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=50
ethtool -C eth0 rx-usecs 0 rx-frames 1
ethtool -K eth0 gro off
시나리오 C: 멀티큐 + 코어 격리 (커널 스택 최적화)
  • isolcpus로 전용 코어 격리
  • 스레드 NAPI + RT 우선순위
  • NUMA 최적화 (NIC-CPU 동일 노드)
# GRUB: isolcpus=8-15 nohz_full=8-15 rcu_nocbs=8-15
for i in $(seq 0 7); do
  irq=$(grep eth0-rx-$i /proc/interrupts | awk '{print $1}' | tr -d ':')
  echo $((1 << (i+8))) > /proc/irq/$irq/smp_affinity
done
echo 1 > /sys/class/net/eth0/threaded

sysctl 영속화 설정 파일

# /etc/sysctl.d/99-napi-tuning.conf (고처리량 서버용)
cat > /etc/sysctl.d/99-napi-tuning.conf <<'EOF'
# NAPI 버짓 및 처리량 튜닝
net.core.netdev_budget = 1200
net.core.netdev_budget_usecs = 16000
net.core.netdev_max_backlog = 10000

# GRO 타임아웃 (100μs)
net.core.gro_flush_timeout = 100000

# IRQ 지연 (폴링 모드 유지)
net.core.napi_defer_hard_irqs = 64

# 소켓 버퍼 크기 확장
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728

# TCP 오프로드 최적화
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_sack = 1
EOF

# 즉시 적용
sysctl -p /etc/sysctl.d/99-napi-tuning.conf

napi_defer_hard_irqs와 gro_flush_timeout 최적화

이 두 파라미터를 함께 사용하면 IRQ를 더 오래 비활성화된 상태로 유지하여 패킷 배치 크기를 키울 수 있습니다.

NAPI 튜닝 파라미터 상호작용 매트릭스

NAPI 관련 파라미터들은 서로 밀접하게 연관되어 있습니다. 하나를 변경하면 다른 파라미터의 효과가 달라지므로 전체적으로 이해해야 합니다.

파라미터netdev_budgetrx-usecs (ITR)defer_hard_irqsgro_flush_timeoutbusy_poll
netdev_budget ITR↑ → 배치↑ → budget 소진 빠름 defer↑ → 연속 poll → budget 빨리 소진 간접 (GRO 배치 크기 영향) 독립 (busy poll은 별도 경로)
rx-usecs (ITR) budget↑ 필요할 수 있음 defer가 ITR보다 우선 (SW 레벨) ITR > gro_flush이면 의미 감소 busy_poll 시 ITR 무관
defer_hard_irqs budget↑ 권장 defer > 0이면 ITR 효과 감소 반드시 함께 설정 (타이머 역할) 독립
gro_flush_timeout 간접 영향 gro_flush < ITR이면 GRO 효과 감소 defer와 함께 "폴링 주기" 결정 독립
busy_poll 독립 busy poll 중 ITR 무관 독립 독립

워크로드별 추천 설정

워크로드목표핵심 설정
웹 서버 (많은 짧은 연결) 처리량 최대화 netdev_budget=600, rx-usecs=50, RSS 다중 큐
HFT/저지연 트레이딩 레이턴시 최소화 busy_poll=50, busy_read=50, C-state=C1, CPU 격리
스트리밍/대용량 전송 Gbps 최대화 GRO on, gro_flush_timeout=20000, 큰 SO_RCVBUF
DDoS 방어 pps 최대 드롭 XDP DROP, defer_hard_irqs=64, gro_flush_timeout=100000
가상화(Virtualization) 호스트 VM 간 공정성 기본 설정, 스레드 NAPI, cgroup 제한
실시간 제어 (PREEMPT_RT) 결정론적 레이턴시 스레드 NAPI, CPU 격리, RT 우선순위
절전 (IoT/엣지) 전력 최소화 defer_hard_irqs=10, gro_flush_timeout=200000, 깊은 C-state

ITR(Interrupt Throttle Rate) 하드웨어

ITR 하드웨어 타이머 동작 원리

ITR(Interrupt Throttle Rate)은 NIC 인터럽트 컨트롤러(Interrupt Controller) 내부의 하드웨어 타이머로, 이벤트(패킷 수신/송신 완료) 발생 후 인터럽트 어서션(assertion)을 일정 시간 지연시키는 메커니즘입니다. ethtool의 rx-usecs / tx-usecs 파라미터가 이 HW 레지스터(Register)에 직접 매핑됩니다.

타이머 생명주기:

  1. 패킷 도착 → NIC가 DMA로 링 버퍼(Ring Buffer)에 디스크립터 기록
  2. DMA 완료 → ITR 타이머 카운트다운 시작
  3. 타이머 만료 → MSI-X 인터럽트 어서션 → CPU에 IRQ 전달

타이머 재시작(Reboot) 정책:

정책동작특성
절대 타이머 (Absolute) 첫 이벤트 시점부터 고정 카운트다운, 추가 이벤트가 타이머를 재시작하지 않음 최대 지연 시간이 보장됨 (예측 가능)
상대 타이머 (Relative) 매 이벤트 도착 시 타이머 재시작 버스트 트래픽에서 코얼레싱 효과가 크지만, 연속 트래픽 시 지연 무한 증가 가능

프레임 카운트 임계값: rx-frames 값이 설정된 경우, 누적 패킷 수가 임계값에 도달하면 타이머 만료 이전이라도 즉시 인터럽트를 발생시킵니다. 이는 버스트 트래픽에서 지연 시간의 상한을 보장하는 안전장치 역할을 합니다.

ITR 하드웨어 타이밍 시나리오 시나리오 A — 타이머 만료 (rx-usecs 도달) 시간 Pkt 1 Pkt 2 Pkt 3 rx-usecs 타이머 윈도우 IRQ! → 3패킷을 1회 IRQ로 처리 시나리오 B — 프레임 카운트 (rx-frames 도달) 시간 Pkt 1~8 (버스트) rx-frames=8 타이머 진행 중… 즉시 IRQ! (타이머 잔여 시간 취소) 코얼레싱 트리거 규칙 rx-usecs와 rx-frames 중 먼저 도달한 조건이 IRQ를 트리거합니다

ITR과 NAPI 상태 전이 연동

ITR은 하드웨어 계층에서 인터럽트 빈도를 제한하고, NAPI는 소프트웨어 계층에서 인터럽트를 폴링으로 전환합니다. 두 메커니즘은 상호 보완적으로 동작합니다.

측면ITR (하드웨어)NAPI (소프트웨어)
위치 NIC 인터럽트 컨트롤러 커널 net/core/dev.c
제어 ethtool -C → HW 레지스터 napi_schedule / napi_complete_done
목적 첫 인터럽트 발생 빈도 제한 인터럽트 후 폴링 전환
트레이드오프 ITR 높음 = 지연↑, CPU↓ budget 높음 = 배치↑

연동 흐름:

  1. ITR 타이머 만료 → MSI-X 인터럽트 어서션
  2. IRQ 핸들러napi_schedule() 호출, IRQ 비활성화
  3. NAPI poll 루프 → 링 버퍼에서 패킷 배치 처리
  4. napi_complete_done() → poll 완료, ice_update_itr()로 다음 ITR 값 계산
  5. IRQ 재활성화 → 다음 ITR 타이머 사이클 시작

Adaptive ITR 알고리즘 상세

Adaptive ITR은 드라이버가 NAPI poll 완료 시점에서 수집한 통계(처리된 바이트 수, 패킷 수)를 기반으로 다음 인터럽트 간격을 동적으로 결정하는 알고리즘입니다. ICE 드라이버의 ice_update_itr() 함수를 예로 살펴봅니다.

3가지 동작 모드:

모드조건 (avg_pkt_size)ITR 값대상 워크로드
Low Latency < 128 바이트 ≈ 20μs (ITR_20K) 소형 패킷 (DNS, ARP, 제어 메시지)
Balanced 128 ~ 1200 바이트 ≈ 80μs (ITR_12K) 혼합 트래픽 (웹, 일반 통신)
Bulk > 1200 바이트 ≈ 196μs (ITR_5K) 대용량 전송 (파일 복사, 스트리밍)

rx-usecs-high 파라미터가 설정된 경우, Adaptive 알고리즘이 산출한 값이 이 상한을 초과하지 않도록 바운딩됩니다. HW 타이머 해상도가 4μs이므로 실제 적용값은 항상 4의 배수로 반올림됩니다.

Adaptive ITR 결정 흐름도 (ICE 드라이버) napi_complete_done() 호출 bytes / pkts 통계 수집 avg_pkt_size = bytes ÷ pkts avg_pkt_size 범위 판별 < 128B Low Latency ≈ 20μs 128~1200B Balanced ≈ 80μs > 1200B Bulk ≈ 196μs 4μs 단위 반올림 → HW 레지스터 기록

드라이버별 ITR 구현 비교

각 NIC 드라이버는 서로 다른 Adaptive ITR 알고리즘을 구현합니다. 아래 표는 주요 드라이버의 구현 특성을 비교합니다.

드라이버 Adaptive HW 해상도 알고리즘 핵심 함수
ice O 4μs bytes/pkts 3-모드 ice_update_itr()
ixgbe O 2μs 이동 평균 ixgbe_update_itr()
i40e O 2μs ice 유사 i40e_update_itr()
mlx5 O (DIM) 1μs net_dim 프레임워크 mlx5e_rx_dim_work()
net_dim (Dynamic Interrupt Moderation): Linux 커널 4.17부터 도입된 공통 프레임워크로, 드라이버가 개별적으로 구현하던 Adaptive 코얼레싱 로직을 include/linux/dim.h에서 통합 제공합니다. mlx5, bnxt, ena 등 최신 드라이버가 이 프레임워크를 활용하며, 이벤트 수·바이트 수를 기반으로 통계적으로 최적의 코얼레싱 프로파일(Low/Default/Aggressive)을 선택합니다. 드라이버 자체 알고리즘보다 유지보수가 용이하고 일관된 동작을 보장합니다.
# ICE per-queue Adaptive ITR 설정 예제
# 큐 0~3에 Adaptive 활성화 + 상한 100μs
ethtool --per-queue eth0 queue_mask 0xf --coalesce adaptive-rx on rx-usecs-high 100

# 특정 큐만 고정 코얼레싱 (저지연 전용 큐)
ethtool --per-queue eth0 queue_mask 0x10 --coalesce adaptive-rx off rx-usecs 8

디버깅과 모니터링

/proc/net/softnet_stat 해석

# CPU별 소프트넷 통계 확인
cat /proc/net/softnet_stat

출력 예시 (각 행이 하나의 CPU):

00094e79 00000000 00000004 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00036f22 00000000 00000001 ...
의미높은 값의 시사점
열 1 (total) 처리한 총 프레임 수 정상 트래픽 (높을수록 좋음)
열 2 (dropped) backlog 초과로 드롭된 패킷 netdev_max_backlog 증가 필요
열 3 (time_squeeze) 버짓/시간 소진으로 재스케줄 netdev_budget 증가 또는 큐 수 늘리기
열 10 (received_rps) RPS로 리다이렉트된 패킷 수 RPS 부하 분산 현황 파악
열 11 (flow_limit_count) flow limit으로 드롭된 수 단일 플로우 독점 발생

/proc/net/softnet_stat 파싱 스크립트

#!/bin/bash
# softnet_stat_parse.sh: /proc/net/softnet_stat 열 이름 매핑하여 출력

COLS=(total dropped time_squeeze throttled
      irq_poll cpu_collision received_rps
      flow_limit_count backlog_drops filter_drops
      unknown1 unknown2 unknown3)

echo "=== /proc/net/softnet_stat 분석 ==="
cpu=0
while IFS= read -r line; do
    echo -n "CPU $cpu: "
    i=0
    for val in $line; do
        dec=$((16#$val))
        if [ $dec -gt 0 ] && [ $i -lt ${#COLS[@]} ]; then
            echo -n "${COLS[$i]}=$dec "
        fi
        i=$((i+1))
    done
    echo
    cpu=$((cpu+1))
done < /proc/net/softnet_stat

# 경고 감지
echo
echo "=== 경고 감지 ==="
cpu=0
while IFS= read -r line; do
    vals=($line)
    dropped=$((16#${vals[1]}))
    squeeze=$((16#${vals[2]}))
    [ $dropped -gt 0 ] && echo "  CPU $cpu: 드롭 $dropped개 (netdev_max_backlog 증가 권장)"
    [ $squeeze -gt 1000 ] && echo "  CPU $cpu: time_squeeze $squeeze회 (netdev_budget 증가 권장)"
    cpu=$((cpu+1))
done < /proc/net/softnet_stat

커널 트레이스포인트 목록

트레이스포인트위치인수용도
napi:napi_poll napi_poll() 시작/종료 napi, work, budget NAPI poll 레이턴시, work_done 분포 측정
net:napi_gro_receive_entry napi_gro_receive() 진입 skb GRO 입력 패킷 추적
net:napi_gro_receive_exit napi_gro_receive() 종료 ret(gro_result) GRO 병합 결과 통계
net:net_dev_queue netdev TX 큐 진입 skbaddr, len, name TX 큐 지연 측정
net:netif_receive_skb netif_receive_skb() 진입 skbaddr, len, name RX 처리 완료 패킷 추적
skb:kfree_skb kfree_skb() 호출 시 skbaddr, location, reason 패킷 드롭 원인 추적
irq:softirq_entry softIRQ 핸들러 진입 vec(softirq 종류) NET_RX_SOFTIRQ 실행 빈도 측정

perf stat으로 소프트IRQ 관련 PMU 이벤트 수집

# softIRQ 전용 CPU 사이클 측정
perf stat -e cycles:k,instructions:k,cache-misses \
          -a --per-cpu sleep 5 2>&1 | grep CPU

# net_rx_action() 함수 프로파일링
perf record -g -F 999 -e cycles:k -a -- sleep 10
perf report --stdio --dsos vmlinux | grep -A 20 net_rx_action

# softIRQ 처리 시간 측정 (irq 이벤트 활용)
perf stat -e softirqs/NET_RX/ -a sleep 5

# NAPI poll CPU 점유율 확인 (함수별 분류)
perf top -e cycles:k --stdio -d 5 | grep -E "napi|gro|net_rx"

bpftrace 레시피

# 레시피 1: NAPI poll 레이턴시 히스토그램 (스케줄 → poll 시작까지)
bpftrace -e '
kprobe:__napi_schedule {
    @start[arg0] = nsecs;
}
kprobe:napi_poll / @start[arg0] / {
    $lat_us = (nsecs - @start[arg0]) / 1000;
    @sched_to_poll_us = hist($lat_us);
    delete(@start[arg0]);
}
interval:s:10 {
    printf("=== NAPI 스케줄 → poll 레이턴시 (μs) ===\n");
    print(@sched_to_poll_us);
    clear(@sched_to_poll_us);
}'

# 레시피 2: GRO 병합률 측정 (초당 GRO_MERGED vs GRO_NORMAL)
bpftrace -e '
tracepoint:net:napi_gro_receive_exit {
    if (args->ret == 0) @gro_merged = count();       // GRO_MERGED
    else if (args->ret == 3) @gro_normal = count();  // GRO_NORMAL
}
interval:s:1 {
    $total = @gro_merged + @gro_normal;
    if ($total > 0) {
        printf("GRO 병합률: %d/%d (%d%%)\n",
               @gro_merged, $total, @gro_merged * 100 / $total);
    }
    clear(@gro_merged); clear(@gro_normal);
}'

# 레시피 3: 패킷 드롭 위치 추적 (이유별 분류)
bpftrace -e '
tracepoint:skb:kfree_skb {
    @drop_reason[args->reason] = count();
}
interval:s:5 {
    printf("=== 패킷 드롭 이유별 통계 ===\n");
    print(@drop_reason);
    clear(@drop_reason);
}'

ethtool 통계

# NIC별 상세 통계 (NAPI 관련 포함)
ethtool -S eth0 | grep -E "rx_missed|rx_dropped|rx_csum|gro"

# 주요 카운터:
#   rx_missed_errors: NIC 버퍼 오버플로우 (ring 크기 늘리기)
#   rx_dropped:       소프트웨어 드롭
#   rx_gro_packets:   GRO로 병합된 패킷 수
#   rx_gro_chunks:    GRO 병합 결과 청크 수

perf와 ftrace 활용

# NAPI poll CPU 사용 추적
perf record -g -e cycles:k -- sleep 5
perf report --stdio | grep -A5 net_rx_action

# ftrace로 NAPI 이벤트 추적
cd /sys/kernel/debug/tracing
echo napi:napi_poll > set_event
cat trace

# 함수 그래프 추적 (poll() 내부 시간 측정)
echo function_graph > current_tracer
echo mynic_poll > set_graph_function
cat trace_pipe

NAPI 상태 진단 플로우

DISABLED napi_disable() IDLE IRQ 대기 중 SCHED poll_list 등록 POLLING poll() 실행 중 MISSED 버짓 소진 중 IRQ COMPLETE IRQ 재활성화 napi_enable() napi_schedule() napi_poll() 패킷 소진 버짓 소진 + 새 IRQ 재스케줄 IRQ 재활성화 → IDLE 대기 NAPI 상태 머신

일반적인 NAPI 버그 패턴

버그 패턴증상원인 및 해결
napi_complete() 누락 패킷이 처리되지 않음, NAPI가 영구 스케줄 상태 work_done < budget 분기에서 반드시 호출. 잊으면 NAPI_STATE_SCHED 영구 세팅
이중 스케줄(double schedule) 커널 경고 "NAPI already scheduled" napi_schedule()NAPI_STATE_SCHED로 중복 방지하지만, 초기화 전 스케줄 시 발생 가능
IRQ 재활성화 누락 패킷이 처음 한 번만 처리되고 이후 수신 없음 napi_complete_done()true 반환 시 반드시 HW IRQ 재활성화 필요
napi_disable() 전 free_irq() race condition, use-after-free free_irq()napi_disable() 순서 반드시 유지 (반대 순서 금지)
GRO flush 없이 netif_rx() 패킷 순서 오류, TCP 성능 저하 GRO 사용 시 napi_gro_receive() 대신 netif_rx() 직접 호출 금지
poll_owner 경합(Contention) 버지 폴링과 softIRQ 동시 poll 시도 내부적으로 NAPI_STATE_IN_BUSY_POLL로 방지됨. 직접 napi_poll() 호출 금지

lockdep으로 NAPI 관련 락 순서 검증

# lockdep 활성화 커널 빌드 옵션
# CONFIG_LOCK_STAT=y
# CONFIG_DEBUG_LOCK_ALLOC=y
# CONFIG_PROVE_LOCKING=y

# NAPI 락 순서 위반 감지 로그 확인
dmesg | grep -E "lockdep|WARNING.*napi|possible circular"

# NAPI 관련 락 통계 확인
cat /proc/lock_stat | grep -E "napi|softirq|bh"
/* drivers/net/ethernet/ 일반 예제 — 간략화 */
/* NAPI 락 사용 패턴: bh 컨텍스트에서만 접근해야 하는 자료구조 */

/* 올바른 패턴: softIRQ(BH) 컨텍스트에서 spin_lock_bh() 불필요 */
static int mynic_poll(struct napi_struct *napi, int budget)
{
    /* softIRQ 내부: local_bh_disable 상태이므로 spin_lock 충분 */
    spin_lock(&ring->lock);
    /* ... */
    spin_unlock(&ring->lock);
}

/* 잘못된 패턴: 프로세스 컨텍스트에서 BH를 비활성화하지 않고 접근 */
static void mynic_bad_access(void)
{
    /* 위험: softIRQ(NAPI poll)와 경합 가능 */
    spin_lock(&ring->lock);    /* spin_lock_bh() 사용해야 함 */
    /* ... */
    spin_unlock(&ring->lock);
}

일반적인 문제 진단

증상진단 명령원인 및 해결
패킷 드롭 증가 cat /proc/net/softnet_stat 열 2 확인 netdev_max_backlog 증가, RPS 활성화
time_squeeze 증가 cat /proc/net/softnet_stat 열 3 확인 netdev_budget 증가, 큐 수 늘리기
RX 버퍼 오버플로(Buffer Overflow)우 ethtool -S eth0 | grep missed ethtool -G eth0 rx 4096으로 링 확장
CPU 불균형 mpstat -P ALL 1, sar -n DEV IRQ 어피니티 재설정, RPS/RFS 활성화
레이턴시 스파이크 bpftrace NAPI poll 레이턴시 추적 스레드 NAPI, 버지 폴링, IRQ 어피니티 격리
GRO 오작동 ethtool -S eth0 | grep gro ethtool -K eth0 gro off으로 비활성화 테스트
NAPI poll이 실행 안 됨 ftrace: napi:napi_poll 이벤트 없음 napi_enable() 누락, IRQ 마스크 해제 안 됨

RPS/RFS와 NAPI 연동

단일 큐 NIC에서도 소프트웨어 멀티큐를 구현하는 RPS(Receive Packet Steering)와 RFS(Receive Flow Steering)는 NAPI poll 후 패킷을 다른 CPU로 재분산합니다.

# RPS 활성화: eth0의 첫 번째 큐에서 모든 CPU로 분산
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# RFS 활성화: 소켓이 실행 중인 CPU로 패킷 유도
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
다음 학습:

네트워크 성능 모니터링 스크립트

#!/bin/bash
# napi_monitor.sh: NAPI 관련 핵심 지표 실시간 모니터링

NIC=${1:-eth0}
INTERVAL=${2:-1}

prev_total=0; prev_dropped=0; prev_squeeze=0

while true; do
    clear
    echo "=== NAPI 성능 모니터: $NIC ($(date)) ==="
    echo

    # softnet_stat 합산
    total=0; dropped=0; squeeze=0
    while IFS= read -r line; do
        vals=($line)
        total=$((total + 16#${vals[0]}))
        dropped=$((dropped + 16#${vals[1]}))
        squeeze=$((squeeze + 16#${vals[2]}))
    done < /proc/net/softnet_stat

    echo "패킷 처리량:  $((total - prev_total)) pkt/s"
    echo "드롭:         $((dropped - prev_dropped)) pkt/s"
    echo "time_squeeze: $((squeeze - prev_squeeze)) /s"
    prev_total=$total; prev_dropped=$dropped; prev_squeeze=$squeeze

    echo
    echo "=== NIC 통계 ($NIC) ==="
    ethtool -S $NIC 2>/dev/null | \
        grep -E "rx_packets|rx_bytes|rx_missed|rx_dropped|rx_csum|rx_gro" | head -10

    echo
    echo "=== IRQ 분포 ==="
    grep $NIC /proc/interrupts | \
        awk '{printf "IRQ %s: total=%s\n", $1, $NF}' | head -8

    echo
    echo "=== CPU별 NET_RX softIRQ ==="
    cat /proc/softirqs | grep NET_RX

    sleep $INTERVAL
done

NAPI와 SR-IOV/컨테이너 환경

SR-IOV(Single Root I/O Virtualization) VF 드라이버도 PF와 동일한 NAPI 패턴을 따릅니다. 컨테이너 환경에서 NAPI 관련 설정은 호스트 커널이 관리합니다.

설정컨테이너 내부 변경 가능비고
netdev_budget 불가 (privileged 컨테이너 예외) 호스트 sysctl로 설정
SO_BUSY_POLL 가능 (setsockopt) 호스트의 busy_poll sysctl도 확인 필요
IRQ 어피니티 불가 호스트에서만 설정 가능
RSS 큐 수 불가 (veth는 공유) SR-IOV VF 사용 시 일부 독립 가능
스레드 NAPI 불가 호스트 /sys/class/net/ 에서 설정

NAPI 관련 커널 설정 옵션

커널 설정의미기본값
CONFIG_NET_RX_BUSY_POLL 버지 폴링 기능 활성화 y
CONFIG_PAGE_POOL page_pool 고성능 할당자 y (modern 커널)
CONFIG_PAGE_POOL_STATS page_pool 통계 수집 y (디버그 빌드)
CONFIG_RPS Receive Packet Steering y (SMP 빌드)
CONFIG_RFS_ACCEL aRFS(accelerated RFS) 지원 드라이버 지원 시 활성화
CONFIG_XDP_SOCKETS AF_XDP 소켓 지원 y (modern 커널)
CONFIG_PREEMPT_RT 완전 선점형 RT 커널 (softIRQ 스레드화) 별도 패치(Patch)셋 필요
CONFIG_PROVE_LOCKING lockdep 활성화 (NAPI 락 순서 검증) 디버그 빌드에서 활성화

NAPI 성능 벤치마크

# pktgen: 커널 내장 패킷 생성기로 NIC 처리량 측정
modprobe pktgen

echo "add_device eth0" > /proc/net/pktgen/kpktgend_0
echo "clone_skb 1000" > /proc/net/pktgen/eth0
echo "pkt_size 64" > /proc/net/pktgen/eth0
echo "count 10000000" > /proc/net/pktgen/eth0
echo "start" > /proc/net/pktgen/pgctrl

# iperf3: TCP 처리량 측정 (GRO 효과 측정)
iperf3 -s -p 5201  # 서버
iperf3 -c server_ip -p 5201 -P 8 -t 10  # 클라이언트 (8스트림)

# sockperf: 초저지연 왕복 측정 (버지 폴링 효과)
sockperf server -i server_ip -p 11111  # 서버
sockperf ping-pong -i server_ip -p 11111 --time 10  # 클라이언트

# netperf: TCP RR 레이턴시 측정
netperf -H server_ip -t TCP_RR -l 30 -- -r 1,1
NAPI 성능 최적화 체크리스트:
  1. RSS 큐 수를 CPU 코어 수에 맞게 설정 (ethtool -L eth0 combined N)
  2. IRQ 어피니티 1:1 매핑 (큐 N → CPU N)
  3. irqbalance 중지 (수동 어피니티 시)
  4. GRO 활성화 확인 (ethtool -k eth0 | grep gro)
  5. NUMA 노드 정렬 확인 (NIC PCIe ↔ CPU 동일 노드)
  6. C-state 제한 (저지연 요구 시)
  7. netdev_budgetnetdev_budget_usecs 조정
  8. 링 버퍼 크기 최대화 (ethtool -G eth0 rx 4096)
  9. Adaptive 코얼레싱 활성화 (고처리량) 또는 rx-usecs=0 (저지연)
  10. 버지 폴링 활성화 (저지연 응용, 전용 CPU 보유 시)

NAPI 트러블슈팅 플로우차트

NAPI 관련 문제가 발생했을 때 순차적으로 확인해야 할 진단 절차입니다.

단계확인 항목명령어정상 기준
1 드롭 여부 확인 cat /proc/net/softnet_stat 열 2(dropped) = 0
2 time_squeeze 확인 cat /proc/net/softnet_stat 열 3(time_squeeze) 증가율 낮음
3 NIC 링 오버플로우 ethtool -S eth0 | grep missed rx_missed = 0
4 CPU 불균형 cat /proc/interrupts | grep eth0 IRQ 카운트 균등 분포
5 GRO 병합률 ethtool -S eth0 | grep gro gro_packets / gro_chunks 비율 > 4
6 NAPI poll 실행 여부 echo napi:napi_poll > /sys/kernel/debug/tracing/set_event 이벤트 정상 출력
7 스택 레이턴시 bpftrace -e 'kprobe:napi_poll { @t=nsecs; } kretprobe:napi_poll { @hist=hist((nsecs-@t)/1000); }' p99 < 1ms (일반 환경)

자주 묻는 질문 (FAQ)

Q: NAPI weight를 높이면 무조건 좋은가?

아닙니다. weight를 높이면 한 NAPI 인스턴스가 더 많은 패킷을 처리할 수 있지만, 동시에 다른 NAPI 인스턴스(다른 NIC 또는 같은 NIC의 다른 큐)의 처리 기회가 줄어듭니다. 멀티큐 환경에서 특정 큐에 weight를 너무 높게 설정하면 다른 큐의 레이턴시가 증가합니다. 일반적으로 기본값 64가 균형 잡힌 설정이며, netdev_budget을 조정하는 것이 더 안전합니다.

Q: GRO와 LRO의 차이점은?

LRO(Large Receive Offload)는 NIC 하드웨어에서 패킷을 병합하는 방식이고, GRO(Generic Receive Offload)는 소프트웨어(NAPI poll 내)에서 병합합니다. LRO는 IP 헤더를 수정하는 경우가 있어 라우터/브리지(Bridge) 환경에서 문제가 발생할 수 있습니다. GRO는 원본 헤더를 보존하면서 병합하므로 더 안전합니다. 현대 리눅스에서는 LRO 대신 GRO 사용을 권장합니다.

Q: napi_disable()은 언제 free_irq() 전에 호출해야 하나?

반드시 free_irq()를 먼저 호출하여 새 IRQ가 발생하지 않도록 한 후, napi_disable()로 진행 중인 poll()이 완료될 때까지 기다려야 합니다. 만약 순서가 반대라면 (napi_disable()free_irq()): IRQ 핸들러에서 napi_schedule()을 호출할 수 있지만 DISABLE 상태라 무시됩니다. 이는 올바른 순서입니다. 단, IRQ 핸들러가 NAPI 이외의 작업도 수행한다면 free_irq() 먼저 호출 후 napi_disable()을 권장합니다.

Q: 버지 폴링 사용 시 softIRQ와 충돌하지 않는가?

NAPI_STATE_IN_BUSY_POLL 비트로 충돌을 방지합니다. 버지 폴링이 NAPI를 점유하면 softIRQ의 net_rx_action()은 해당 NAPI를 건너뜁니다. 반대로 softIRQ가 NAPI를 폴링 중이면 버지 폴링은 napi_try_get() 실패 시 스킵하고 소켓 큐를 직접 확인합니다.

NAPI Backlog과 process_backlog()

softnet_data.backlog의 역할

모든 CPU는 softnet_data 내에 기본(default) NAPI 인스턴스인 backlog을 갖고 있습니다. 이 backlog NAPI는 다음 상황에서 사용됩니다:

/* net/core/dev.c - per-CPU backlog NAPI 초기화 */
static int __init net_dev_init(void)
{
    int i;
    for_each_possible_cpu(i) {
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        INIT_LIST_HEAD(&sd->poll_list);
        skb_queue_head_init(&sd->input_pkt_queue);

        /* backlog NAPI 등록:
           poll 함수 = process_backlog
           weight = NAPI_POLL_WEIGHT * 2 = 128
           (일반 드라이버 NAPI보다 2배 높은 버짓) */
        init_gro_hash(&sd->backlog);
        sd->backlog.poll = process_backlog;
        sd->backlog.weight = weight_p;  /* 기본 64 */

        /* backlog은 napi_hash에 등록되지 않음 (busy poll 불가) */
    }
    /* NET_RX_SOFTIRQ 핸들러 등록 */
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
    return 0;
}

process_backlog() 내부 구현

process_backlog()은 backlog NAPI의 poll 함수로, input_pkt_queue에서 sk_buff를 꺼내 __netif_receive_skb()로 전달합니다.

/* net/core/dev.c */
static int process_backlog(struct napi_struct *napi, int quota)
{
    struct softnet_data *sd = container_of(napi, struct softnet_data, backlog);
    int work = 0;

    /* ① input_pkt_queue → process_queue로 일괄 이동
       (입력 큐는 다른 CPU에서도 접근하므로 락 보호 필요) */
    while (work < quota) {
        struct sk_buff *skb;
        unsigned int qlen;

        local_irq_disable();
        rps_lock(sd);
        /* input_pkt_queue의 내용을 process_queue로 스플라이스 */
        qlen = skb_queue_len(&sd->input_pkt_queue);
        if (qlen) {
            skb_queue_splice_tail_init(&sd->input_pkt_queue,
                                      &sd->process_queue);
        }
        rps_unlock(sd);
        local_irq_enable();

        /* ② process_queue에서 하나씩 꺼내 처리 */
        while ((skb = __skb_dequeue(&sd->process_queue))) {
            __netif_receive_skb(skb);
            work++;
            if (work >= quota)
                return work;
        }

        /* ③ 큐가 비었으면 NAPI 완료 */
        if (!qlen) {
            napi_complete_done(napi, work);
            return work;
        }
    }
    return work;
}

netif_rx()의 내부 흐름

NAPI를 사용하지 않는 레거시 드라이버나 특수한 경로(loopback, tun/tap)에서 netif_rx()가 호출됩니다. 이 함수는 backlog 큐에 skb를 인큐(Enqueue)하고 backlog NAPI를 스케줄합니다.

/* net/core/dev.c */
int netif_rx(struct sk_buff *skb)
{
    struct softnet_data *sd;
    int ret;

    sd = this_cpu_ptr(&softnet_data);

    rps_lock(sd);
    /* input_pkt_queue 길이 확인 (netdev_max_backlog 초과 시 드롭) */
    if (skb_queue_len(&sd->input_pkt_queue) >= netdev_max_backlog) {
        sd->dropped++;  /* /proc/net/softnet_stat 열 2 */
        rps_unlock(sd);
        kfree_skb(skb);
        return NET_RX_DROP;
    }

    /* 큐에 인큐 */
    __skb_queue_tail(&sd->input_pkt_queue, skb);

    /* backlog NAPI 스케줄 */
    if (napi_schedule_prep(&sd->backlog))
        ____napi_schedule(sd, &sd->backlog);
    rps_unlock(sd);

    return NET_RX_SUCCESS;
}

backlog 관련 sysctl 파라미터

파라미터기본값설명조절 가이드
net.core.netdev_max_backlog 1000 per-CPU backlog 큐 최대 길이. 초과 시 패킷 드롭 10G+ 환경: 10000~50000, RPS 사용 시 더 높게
net.core.netdev_budget 300 net_rx_action() 한 번에 처리할 총 패킷 수 트래픽 많을 때 600~1200, time_squeeze 증가 시 올림
net.core.netdev_budget_usecs 2000 (2ms) net_rx_action() 최대 실행 시간 (μs) 레이턴시 민감: 1000, 처리량 우선: 8000
net.core.dev_weight 64 backlog NAPI의 weight (process_backlog 버짓) RPS 환경에서 128~256으로 증가 가능
net.core.dev_weight_rx_bias 1 RX 처리에 대한 가중치 편향 (weight에 곱해짐) RX 집중 워크로드: 2~4
net.core.dev_weight_tx_bias 1 TX 처리에 대한 가중치 편향 TX 집중 워크로드: 2~4
NAPI Backlog 처리 흐름 CPU A (수신 CPU) NIC IRQ → NAPI poll() get_rps_cpu() → CPU B 선택 enqueue_to_backlog(skb, CPU B) IPI CPU B (대상 CPU) sd->input_pkt_queue에 skb 인큐 napi_schedule(&sd->backlog) NET_RX_SOFTIRQ → net_rx_action() process_backlog() __netif_receive_skb() → TCP/UDP 스택 레거시: netif_rx() 로컬 CPU backlog에 직접 인큐 (loopback, tun, veth 등) 큐 오버플로우 input_pkt_queue > netdev_max_backlog → 패킷 드롭 sd->dropped++ backlog NAPI는 RPS 분산과 레거시 드라이버의 수신 경로를 통합 처리 확인: /proc/net/softnet_stat — 열 1=processed, 열 2=dropped, 열 3=time_squeeze
/proc/net/softnet_stat 열 2(dropped) 증가 시: netdev_max_backlog 값을 올리세요. 기본값 1000은 10G+ 환경에서 부족합니다. sysctl -w net.core.netdev_max_backlog=10000으로 시작하고, 모니터링하면서 조정합니다. 근본 원인은 CPU가 패킷을 처리하는 속도보다 인입이 빠른 것이므로 RPS/RSS 분산 또는 XDP 조기 드롭도 함께 고려해야 합니다.

RPS/RFS와 NAPI 통합

RPS(Receive Packet Steering) 개요

RPS는 소프트웨어 기반 RSS(Receive Side Scaling)입니다. 하드웨어 RSS를 지원하지 않는 NIC이나 단일 큐 NIC에서 패킷 처리를 여러 CPU로 분산합니다. NAPI poll()에서 수신한 패킷의 플로우 해시를 계산하고 rps_cpu_mask에 따라 대상 CPU를 결정합니다.

/* net/core/dev.c - RPS CPU 선택 */
static int get_rps_cpu(struct net_device *dev,
                      struct sk_buff *skb,
                      struct rps_dev_flow **rflowp)
{
    struct netdev_rx_queue *rxqueue;
    struct rps_map *map;
    struct rps_sock_flow_table *sock_flow_table;
    u32 hash, next_cpu, ident;
    int cpu = -1;

    /* 1. 플로우 해시 계산 (skb->hash 또는 Toeplitz 해시) */
    hash = skb_get_hash(skb);
    if (!hash)
        return -1;

    /* 2. RPS 맵에서 후보 CPU 선택 */
    rxqueue = &dev->_rx[skb_get_rx_queue(skb)];
    map = rcu_dereference(rxqueue->rps_map);
    if (map) {
        next_cpu = map->cpus[reciprocal_scale(hash, map->len)];
    }

    /* 3. RFS 활성 시: 소켓이 마지막으로 실행된 CPU 우선 */
    sock_flow_table = rcu_dereference(rps_sock_flow_table);
    if (sock_flow_table) {
        ident = sock_flow_table->ents[hash & sock_flow_table->mask];
        /* 소켓의 CPU가 RPS 맵에 포함되면 그 CPU 사용 */
        if (cpu_online(ident & rps_cpu_mask))
            cpu = ident & rps_cpu_mask;
    }

    return cpu;
}

/* RPS가 선택한 CPU의 backlog에 패킷 전달 */
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
                              unsigned int *qtail)
{
    struct softnet_data *sd = &per_cpu(softnet_data, cpu);

    rps_lock(sd);
    if (skb_queue_len(&sd->input_pkt_queue) <= netdev_max_backlog) {
        __skb_queue_tail(&sd->input_pkt_queue, skb);
        /* 대상 CPU에 IPI(Inter-Processor Interrupt) 전송 */
        napi_schedule_rps(sd);  /* → __napi_schedule(&sd->backlog) */
        rps_unlock(sd);
        return NET_RX_SUCCESS;
    }
    sd->dropped++;
    rps_unlock(sd);
    kfree_skb(skb);
    return NET_RX_DROP;
}

RFS(Receive Flow Steering)

RFS는 RPS를 확장하여 패킷을 해당 소켓을 처리하는 CPU로 전달합니다. 이를 통해 L1/L2 캐시 히트율을 극대화합니다.

/* RFS 소켓 플로우 테이블 */
struct rps_sock_flow_table {
    u32 mask;    /* entries - 1 (2의 거듭제곱 - 1) */
    u32 ents[];  /* 해시 → CPU 매핑 (동적 갱신) */
};

/* 소켓이 recvmsg() 호출 시 자동 갱신:
   inet_recvmsg() → sock_rps_record_flow()
   → rps_sock_flow_table[hash] = 현재 CPU */

/* aRFS(Accelerated RFS): 하드웨어 지원 RFS
   NIC가 ntuple 필터로 플로우를 특정 큐로 직접 스티어링
   드라이버 콜백: ndo_rx_flow_steer() */

RPS/RFS 설정

# RPS: 패킷 처리 CPU 지정 (비트마스크)
# eth0의 큐 0에서 CPU 0-7로 분산
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# RPS 플로우 해시 테이블 크기 (전역)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# per-큐 RFS 플로우 테이블 크기
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

# 확인: RPS가 활성화되면 softnet_stat의 received_rps 열 증가
watch -n1 cat /proc/net/softnet_stat

# aRFS 활성화 (드라이버 지원 필요)
ethtool -K eth0 ntuple on
echo 1 > /proc/sys/net/core/rps_sock_flow_entries

RPS vs RSS vs XDP cpumap 비교

방식처리 위치해시CPU 선택오버헤드장점단점
RSS (HW) NIC 하드웨어 Toeplitz (HW) 인다이렉션 테이블 제로 CPU 오버헤드 없음 HW 지원 필요, 큐 수 제한
RPS (SW) NAPI poll 후 Toeplitz (SW) rps_cpus 마스크 IPI + backlog 인큐 모든 NIC 지원 IPI 비용, 캐시 미스
RFS NAPI poll 후 플로우 해시 소켓 CPU 추적 IPI + 테이블 조회 캐시 지역성 최적 소켓 마이그레이션 지연
aRFS (HW) NIC 하드웨어 플로우 해시 ntuple 필터 제로 HW 수준 최적화 지원 NIC 제한, 필터 수 제한
XDP cpumap NAPI poll() 내 BPF 프로그램 BPF 맵 ptr_ring + kthread 완전한 제어, 필터링 가능 BPF 프로그래밍 필요
RPS/RFS 패킷 분산 경로 NAPI poll() CPU 0 (IRQ CPU) netif_receive_skb() get_rps_cpu() 호출 RPS 비활성: 로컬 처리 RPS 활성: CPU N 선택 RFS: 소켓 CPU로 재지정 대상 CPU backlog 큐 CPU 1 process_backlog() CPU 2 process_backlog() CPU 3 process_backlog() CPU N process_backlog() ... __netif_receive_skb() → 프로토콜 핸들러 → 소켓 각 CPU에서 독립적으로 TCP/UDP 스택 처리 → 소켓 수신 큐에 전달 RFS: 소켓의 recvmsg()를 호출하는 CPU와 동일 CPU에서 처리 → L1/L2 캐시 히트 최대화
RPS vs RSS 선택 기준: RSS(하드웨어)를 사용할 수 있다면 항상 RSS를 우선합니다. RPS는 IPI(Inter-Processor Interrupt) 비용이 발생하여 1~3μs의 추가 레이턴시가 있습니다. 단일 큐 NIC, 가상 NIC(veth, virtio-net의 일부 모드), 또는 RSS 큐 수보다 CPU 수가 훨씬 많은 경우에만 RPS를 사용합니다.

네트워크 스택 전달 경로 (NAPI → 소켓)

NAPI에서 소켓까지의 전체 패킷 경로

NAPI poll()에서 수신된 패킷이 최종적으로 애플리케이션의 소켓 버퍼에 도달하기까지의 전체 경로입니다. 각 단계에서 어떤 처리가 이루어지는지 이해하면 성능 병목(Bottleneck)을 정확히 진단할 수 있습니다.

패킷 수신 전체 경로: NIC → 애플리케이션 ① NIC DMA → RX 링 버퍼 ② NAPI poll(): XDP → sk_buff → GRO napi_gro_receive() / napi_gro_flush() ③ netif_receive_skb_list() / __netif_receive_skb() RPS 판단, Generic XDP, TC ingress, ptype 디스패치 ④ ip_rcv() → Netfilter PREROUTING IP 헤더 검증, conntrack, DNAT, 라우팅 결정 ⑤ ip_local_deliver() → Netfilter INPUT 로컬 전달: 프로토콜 핸들러 디스패치 ⑥ tcp_v4_rcv() TCP 상태 머신, ACK 처리 sk_mark_napi_id() ⑥ udp_rcv() UDP 체크섬 검증 sk_mark_napi_id() ⑦ sock_queue_rcv_skb() → sk->sk_receive_queue 소켓 버퍼 크기 확인 (SO_RCVBUF), 메모리 계정 ⑧ recvmsg() / read() / epoll_wait() 사용자 공간: copy_to_user() 또는 zero-copy (MSG_ZEROCOPY) 하드웨어 softIRQ softIRQ softIRQ 프로세스 ②~⑦ 전체가 softIRQ 컨텍스트(또는 스레드 NAPI) 내에서 실행됨

단계별 상세 설명

단계함수주요 처리성능 영향관련 sysctl/설정
① NIC DMA (하드웨어) 패킷을 링 버퍼에 DMA 전송 PCIe 대역폭(Bandwidth), IOMMU ethtool -G (링 크기)
② NAPI poll napi_gro_receive() XDP, sk_buff 생성, GRO 병합 배치 처리 효율, page_pool napi_defer_hard_irqs, gro_flush_timeout
③ 수신 디스패치(Dispatch) __netif_receive_skb() RPS, TC ingress, ptype 분류 TC 규칙 수, RPS IPI rps_cpus, TC BPF
④ IP 수신 ip_rcv() IP 검증, conntrack, 라우팅 conntrack 테이블 크기 nf_conntrack_max
⑤ 로컬 전달 ip_local_deliver() Netfilter INPUT 체인 iptables 규칙 수 nftables/iptables
⑥ L4 처리 tcp_v4_rcv() 소켓 조회, TCP 상태 머신 소켓 해시 테이블 크기 tcp_max_syn_backlog
⑦ 소켓 큐 sock_queue_rcv_skb() 수신 버퍼 관리, 웨이크업 SO_RCVBUF 크기 net.core.rmem_max
⑧ 사용자 읽기 tcp_recvmsg() copy_to_user, 버퍼 해제 컨텍스트 스위치 epoll, io_uring

GRO flush에서 프로토콜 핸들러까지

/* net/core/gro.c, net/core/dev.c — 간략화 */
/* GRO flush 후 패킷 전달 경로 */

/* 1. napi_gro_flush() → GRO 병합된 skb를 상위로 전달 */
void napi_gro_flush(struct napi_struct *napi, bool flush_old)
{
    /* gro_hash 버킷 순회: 병합된 skb들을 napi->rx_list에 추가 */
    for (i = 0; i < GRO_HASH_BUCKETS; i++) {
        list_for_each_entry_safe(skb, ...) {
            __napi_gro_flush_chain(napi, i, flush_old);
        }
    }
}

/* 2. gro_normal_list() → 일괄 netif_receive_skb() */
static void gro_normal_list(struct napi_struct *napi)
{
    /* rx_list의 skb들을 배치로 전달 (GRO_NORMAL_BATCH=8) */
    netif_receive_skb_list_internal(&napi->rx_list);
    napi->rx_count = 0;
}

/* 3. __netif_receive_skb() 내부 */
static int __netif_receive_skb_core(struct sk_buff **pskb, ...)
{
    /* a) Generic XDP (드라이버 미지원 NIC용) */
    if (static_branch_unlikely(&generic_xdp_needed_key))
        do_xdp_generic(skb);

    /* b) TC ingress (tc filter, cls_bpf) */
    skb = sch_handle_ingress(skb, ...);

    /* c) ptype_all 핸들러 (tcpdump, AF_PACKET 등) */
    list_for_each_entry_rcu(ptype, &ptype_all, list)
        deliver_skb(skb, ptype, orig_dev);

    /* d) ptype 프로토콜 핸들러 디스패치 (L3) */
    /*    ETH_P_IP → ip_rcv()
         ETH_P_IPV6 → ipv6_rcv()
         ETH_P_ARP → arp_rcv() */
    deliver_ptype_list_skb(skb, ...);
}

TCP Fast Path와 NAPI 최적화

TCP의 fast path는 GRO로 병합된 대용량 세그먼트를 효율적으로 처리합니다. NAPI/GRO와 TCP fast path의 시너지가 현대 리눅스의 높은 TCP 처리량의 핵심입니다.

최적화NAPI/GRO 기여TCP 효과
GRO → TCP coalescing 64KB까지 세그먼트 병합 tcp_rcv_established() 호출 횟수 감소 (수십 배)
배치 ACK 여러 세그먼트를 한 poll()에서 처리 ACK 발생 감소, 지연 ACK 최적화
sk_mark_napi_id 소켓-NAPI 바인딩 busy polling 시 정확한 NAPI 타겟
per-NAPI GRO hash 플로우별 병합 관리 멀티 플로우 환경에서도 효과적 병합
early demux GRO 단계에서 소켓 조회 힌트 ip_rcv()에서 라우팅 캐시 히트

실제 드라이버 사례 연구

ixgbe (Intel 10GbE) — 멀티큐 NAPI의 교과서

ixgbe는 Intel 82599/X540 기반 10G NIC 드라이버로, 리눅스 NAPI 멀티큐 구현의 참조 모델입니다. 최대 64개의 RX/TX 큐를 지원하며, RSS + Adaptive ITR + Flow Director를 결합합니다.

/* drivers/net/ethernet/intel/ixgbe/ixgbe_main.c */

/* ixgbe의 NAPI 구조: q_vector가 NAPI + IRQ를 묶는 단위 */
struct ixgbe_q_vector {
    struct ixgbe_adapter *adapter;
    int                  v_idx;        /* 벡터 인덱스 */
    int                  cpu;          /* 어피니티 CPU */
    struct napi_struct   napi;         /* NAPI 인스턴스 */
    struct ixgbe_ring   *rx_ring;     /* 연결된 RX 링 */
    struct ixgbe_ring   *tx_ring;     /* 연결된 TX 링 */
    struct ixgbe_ring_container rx, tx;
    /* Adaptive ITR 상태 */
    u16                  itr;
    char                 name[IFNAMSIZ + 9]; /* "eth0-TxRx-0" */
};

/* ixgbe poll: RX + TX 동시 처리 */
static int ixgbe_poll(struct napi_struct *napi, int budget)
{
    struct ixgbe_q_vector *q_vector =
        container_of(napi, struct ixgbe_q_vector, napi);
    int per_ring_budget, work_done = 0;
    bool clean_complete = true;

    /* ① TX 링 정리 (TX 완료 인터럽트 처리) */
    ixgbe_for_each_ring(ring, q_vector->tx)
        if (!ixgbe_clean_tx_irq(q_vector, ring, budget))
            clean_complete = false;

    /* ② RX 버짓 분배: 여러 RX 링에 균등 분배 */
    if (q_vector->rx.count > 1)
        per_ring_budget = max(budget / q_vector->rx.count, 1);
    else
        per_ring_budget = budget;

    /* ③ 각 RX 링 폴링 */
    ixgbe_for_each_ring(ring, q_vector->rx) {
        int cleaned = ixgbe_clean_rx_irq(q_vector, ring, per_ring_budget);
        work_done += cleaned;
        if (cleaned >= per_ring_budget)
            clean_complete = false;
    }

    /* ④ NAPI 완료 + Adaptive ITR 갱신 */
    if (!clean_complete)
        return budget;  /* 버짓 소진: 재스케줄 */

    if (likely(napi_complete_done(napi, work_done)))
        ixgbe_irq_enable_queues(adapter, BIT(q_vector->v_idx));

    return min(work_done, budget - 1);
}

mlx5 (Mellanox/NVIDIA ConnectX) — 고성능 NAPI

mlx5는 ConnectX-4/5/6/7 시리즈 NIC 드라이버로, 100G+ 환경에서 NAPI + page_pool + XDP를 가장 적극적으로 활용합니다.

/* drivers/net/ethernet/mellanox/mlx5/core/en_rx.c */

/* mlx5의 핵심 특징:
   1. Completion Queue (CQ)를 NAPI와 1:1 매핑
   2. Striding RQ: 큰 페이지를 여러 패킷이 공유 (page_pool 효율 극대화)
   3. SHAMPO (Split Header And Mark Payload Offload): 헤더/페이로드 분리
   4. XDP_REDIRECT가 zero-copy AF_XDP과 통합 */

/* mlx5 poll: CQ 기반 배치 처리 */
static int mlx5e_napi_poll(struct napi_struct *napi, int budget)
{
    struct mlx5e_channel *c = container_of(napi, ...);
    int work_done = 0;
    bool busy = false;

    /* TX CQ 처리 (TX 완료) */
    for (i = 0; i < c->num_tc; i++)
        busy |= mlx5e_poll_tx_cq(&c->sq[i].cq, budget);

    /* XDP TX CQ 처리 */
    busy |= mlx5e_poll_xdpsq_cq(&c->xdpsq.cq);

    /* RX CQ 처리: 패킷 수신 + XDP 실행 */
    work_done = mlx5e_poll_rx_cq(&c->rq.cq, budget);
    busy |= (work_done == budget);

    /* AF_XDP: 별도 RX CQ 처리 */
    if (c->xskrq_active)
        busy |= mlx5e_poll_xsk_rx_cq(&c->xskrq.cq, budget);

    if (busy) {
        mlx5e_cq_arm(&c->rq.cq);  /* CQ doorbell */
        return budget;
    }

    if (napi_complete_done(napi, work_done))
        mlx5e_cq_arm(&c->rq.cq);

    return work_done;
}

virtio-net — 가상화 환경 NAPI

virtio-net은 KVM/QEMU 가상머신의 네트워크 드라이버로, 하이퍼바이저(Hypervisor)와 게스트 간 virtqueue를 통해 패킷을 교환합니다. NAPI + 멀티큐 + XDP를 지원합니다.

/* drivers/net/virtio_net.c */

/* virtio-net의 NAPI 특수성:
   1. virtqueue는 vring(공유 메모리 링)으로 구현
   2. 인터럽트 = virtqueue callback (호스트 → 게스트 통지)
   3. 배치 버퍼 할당으로 vmexit 횟수 최소화
   4. mergeable rx buffers: 가변 크기 패킷 효율적 처리 */

static int virtnet_poll(struct napi_struct *napi, int budget)
{
    struct receive_queue *rq =
        container_of(napi, struct receive_queue, napi);
    unsigned int received;

    /* XDP 활성 시: mergeable 대신 1:1 페이지 모드 */
    received = virtnet_receive(rq, budget, ...);

    /* 빈 버퍼 보충 (vring에 새 버퍼 추가) */
    if (rq->vq->num_free >= virtqueue_get_vring_size(rq->vq) / 2)
        try_fill_recv(rq->vi, rq, GFP_ATOMIC);

    if (received < budget) {
        napi_complete_done(napi, received);
        /* virtqueue 인터럽트 재활성화 */
        if (unlikely(!virtqueue_enable_cb_delayed(rq->vq)))
            virtqueue_napi_schedule(napi, rq->vq);
    }

    return received;
}

/* virtio-net 특유의 인터럽트 재활성화 패턴:
   virtqueue_enable_cb_delayed()는 "좀 더 기다렸다가 인터럽트"
   → 배치 처리 효율 향상 (HW ITR과 유사한 효과) */

드라이버별 NAPI 구현 비교

드라이버NIC최대 큐NAPI 단위XDPpage_pool특이사항
ixgbe Intel 82599/X540 64 q_vector (RX+TX 결합) Native 미사용 (자체 캐시) Adaptive ITR, Flow Director
ice Intel E810 256 q_vector (RX+TX 결합) Native + AF_XDP 사용 RDMA 통합, ADQ
mlx5 ConnectX-4/5/6/7 256 channel (CQ 단위) Native + AF_XDP 사용 (Striding RQ) SHAMPO, HW TLS
bnxt Broadcom BCM57xxx 128 bnxt_napi (RX+TX+CQ) Native + AF_XDP 사용 TPA(LRO), header split
virtio-net 가상 NIC 호스트 설정 receive_queue Native 미사용 mergeable bufs, vhost-net
gve Google Virtio Ethernet 16 gve_notify_block 제한적 사용 GCE 전용, DQO 모드
ena Amazon ENA 32 ena_napi Native 사용 AWS EC2 전용, 적응형 코알레싱

드라이버 NAPI 초기화 패턴 비교

/* drivers/net/ethernet/intel/ixgbe/ixgbe_main.c — 간략화 */
/* 패턴 1: ixgbe — q_vector 기반 (RX+TX 결합 인터럽트) */
for (v_idx = 0; v_idx < adapter->num_q_vectors; v_idx++) {
    q_vector = adapter->q_vector[v_idx];
    netif_napi_add(adapter->netdev, &q_vector->napi,
                   ixgbe_poll, 64);
    /* MSI-X 벡터: 하나의 IRQ가 RX+TX 모두 처리 */
    request_irq(entry->vector, ixgbe_msix_clean_rings,
                0, q_vector->name, q_vector);
}

/* 패턴 2: mlx5 — 채널 기반 (분리된 CQ) */
for (i = 0; i < priv->channels.num; i++) {
    struct mlx5e_channel *c = priv->channels.c[i];
    netif_napi_add(priv->netdev, &c->napi,
                   mlx5e_napi_poll, 64);
    /* 각 채널에 독립 EQ(Event Queue) → CQ 연결 */
}

/* 패턴 3: virtio-net — virtqueue 콜백 */
for (i = 0; i < vi->curr_queue_pairs; i++) {
    netif_napi_add(vi->dev, &vi->rq[i].napi,
                   virtnet_poll, napi_weight);
    /* virtqueue callback → NAPI schedule */
    virtio_device_ready(vi->vdev);
}

컨테이너/네임스페이스(Namespace) 환경의 NAPI

veth 쌍과 NAPI

컨테이너 네트워킹에서 가장 흔한 가상 인터페이스인 veth(virtual Ethernet pair)는 NAPI를 사용하여 패킷을 수신합니다. veth의 한쪽에서 전송된 패킷은 상대편의 NAPI를 통해 수신됩니다.

/* drivers/net/veth.c */

/* veth의 NAPI 흐름:
   1. 컨테이너 A에서 패킷 전송 (vethA의 xmit)
   2. 상대편 vethB의 수신 큐에 인큐
   3. vethB의 NAPI 스케줄
   4. vethB의 네임스페이스에서 네트워크 스택 처리 */

/* veth xmit → 상대편 NAPI */
static netdev_tx_t veth_xmit(struct sk_buff *skb,
                              struct net_device *dev)
{
    struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
    struct net_device *rcv;

    /* 피어 디바이스 참조 */
    rcv = rcu_dereference(priv->peer);
    rcv_priv = netdev_priv(rcv);

    /* XDP 활성 시: NAPI 기반 처리 */
    if (rcv_priv->_xdp_prog) {
        /* ptr_ring에 인큐 → NAPI 스케줄 */
        veth_xdp_rcv(rq, budget, ...);
    } else {
        /* XDP 없으면 직접 netif_rx() */
        netif_rx(skb);
    }
    return NETDEV_TX_OK;
}

/* veth NAPI poll */
static int veth_poll(struct napi_struct *napi, int budget)
{
    struct veth_rq *rq =
        container_of(napi, struct veth_rq, xdp_napi);
    int done;

    /* XDP 프로그램 실행 + 패킷 수신 */
    done = veth_xdp_rcv(rq, budget, ...);

    if (done < budget && napi_complete_done(napi, done)) {
        /* veth는 IRQ가 없으므로 재활성화 불필요 */
        if (unlikely(!__ptr_ring_empty(&rq->xdp_ring)))
            napi_schedule(napi);
    }
    return done;
}

네임스페이스 격리와 NAPI 동작

항목호스트 네임스페이스컨테이너 네임스페이스
물리 NIC NAPI 호스트에서 실행 (softIRQ) 직접 접근 불가 (SR-IOV VF 제외)
veth NAPI 호스트 측 veth는 별도 NAPI 없음 컨테이너 측 veth에서 NAPI 실행
NAPI ID 전역 할당 (모든 NS 공유) 컨테이너 내 NIC도 전역 ID 사용
busy polling 물리 NIC에 직접 가능 veth NAPI에 가능 (효과 제한적)
softnet_stat 물리 NIC 처리 통계 컨테이너 내 통계 별도 (init_net 기준)
XDP 물리 NIC에 native XDP veth에 XDP 부착 가능
CPU 어피니티 IRQ affinity로 제어 cgroup cpuset으로 제한 가능

컨테이너 네트워크 성능 최적화

# 1. veth에 XDP 부착 (컨테이너 → 호스트 방향 가속)
ip link set veth_host xdp obj veth_xdp.o

# 2. TC BPF로 veth 패킷 처리 가속
tc qdisc add dev veth_host clsact
tc filter add dev veth_host ingress bpf obj tc_fwd.o

# 3. macvlan/ipvlan: veth 대신 직접 연결 (NAPI 경유 감소)
ip link add mvlan0 link eth0 type macvlan mode bridge
ip link set mvlan0 netns container_ns

# 4. SR-IOV VF passthrough: 물리 NIC NAPI 직접 사용
#    → 컨테이너에서 물리 NIC 성능에 가장 가까움
echo 4 > /sys/class/net/eth0/device/sriov_numvfs
ip link set eth0 vf 0 mac 00:11:22:33:44:55
ip link set enp1s0f0v0 netns container_ns

# 5. 컨테이너 내 busy polling 활성화
# (veth NAPI에 대한 busy poll — 효과는 물리 NIC보다 작음)
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=50
컨테이너 환경의 NAPI 토폴로지 호스트 네임스페이스 eth0 (물리 NIC) NAPI poll() — HW IRQ br0 (브리지) veth_host_A veth_host_B SR-IOV VF passthrough 물리 NAPI 직접 사용 컨테이너 A (netns) eth0 (veth 피어) veth NAPI poll() 네트워크 스택 (격리) → 컨테이너 내 소켓 컨테이너 B (netns) eth0 (veth 피어) veth NAPI poll() 컨테이너 C (SR-IOV) enp1s0f0v0 (VF) HW NAPI poll() — 최고 성능 veth: 소프트웨어 NAPI (IPI 기반) | SR-IOV VF: 하드웨어 NAPI (MSI-X 인터럽트)

NAPI 성능 벤치마킹 방법론

벤치마크 도구 비교

도구용도측정 지표장점제약
pktgen 커널 내 패킷 생성 pps, 드롭률 최대 TX 속도 측정, 스택 우회 TX 전용, RX 측정 불가
iperf3 TCP/UDP 처리량 Gbps, 재전송, 지연 실제 스택 성능 측정 애플리케이션 계층 포함
netperf 네트워크 레이턴시/처리량 RR 레이턴시, 스트림 처리량 다양한 테스트 유형 유지보수 중단
xdp-bench XDP 성능 측정 Mpps (XDP verdict별) NAPI/XDP 특화 XDP 경로만 측정
sockperf 소켓 레이턴시 μs 단위 p50/p99/p999 busy polling 레이턴시 UDP 주로 사용
neper 멀티 커넥션 처리량 연결 수, 총 처리량 다중 소켓 스케일링 Google 내부 도구 기반
bpftrace NAPI 내부 프로파일링(Profiling) poll 지속 시간, 패킷/poll 커널 내부 계측 오버헤드로 수치 변동

pktgen으로 NAPI 처리량 측정

# 커널 pktgen 모듈 로드
modprobe pktgen

# CPU 0에서 eth0으로 64B 패킷 전송
# /proc/net/pktgen/ 인터페이스 사용
cat > /proc/net/pktgen/kpktgend_0 <<'EOF'
rem_device_all
add_device eth0@0
EOF

cat > /proc/net/pktgen/eth0@0 <<'EOF'
count 10000000
min_pkt_size 64
max_pkt_size 64
dst 192.168.1.2
dst_mac aa:bb:cc:dd:ee:ff
delay 0
clone_skb 100000
EOF

# 테스트 시작
echo start > /proc/net/pktgen/pgctrl

# 결과 확인
cat /proc/net/pktgen/eth0@0
# Result: OK: ... (pps)
# 수신 측에서 /proc/net/softnet_stat 모니터링

NAPI 성능 기준선 (참고 수치)

환경패킷 크기단일 큐 pps단일 큐 Gbps다중 큐 (8코어)
1G NIC (e1000e) 64B ~1.2Mpps ~0.6Gbps N/A (단일 큐)
10G NIC (ixgbe) 64B ~3Mpps ~1.5Gbps ~14Mpps
25G NIC (mlx5) 64B ~5Mpps ~2.5Gbps ~25Mpps
100G NIC (mlx5) 64B ~6Mpps ~3Gbps ~40Mpps
100G + XDP DROP 64B ~24Mpps ~12Gbps ~100Mpps
virtio-net (KVM) 64B ~0.5Mpps ~0.25Gbps ~2Mpps
veth (컨테이너) 64B ~1Mpps ~0.5Gbps ~4Mpps (RPS)
벤치마크 주의사항: 위 수치는 참고용 개념적 기준선입니다. 실제 성능은 CPU 모델, BIOS 설정(C-states, Turbo Boost), NUMA 토폴로지, PCIe 세대/레인 수, 커널 버전, 드라이버 버전, sysctl 튜닝에 따라 크게 달라집니다. 항상 자신의 환경에서 직접 측정하세요.

bpftrace를 이용한 NAPI 내부 프로파일링

# 1. poll() 당 처리 패킷 수 분포
bpftrace -e '
tracepoint:napi:napi_poll {
    @pkts_per_poll = hist(args->work);
    @by_dev[str(args->dev_name)] = count();
}'

# 2. poll() 지속 시간 측정 (μs)
bpftrace -e '
kprobe:napi_poll { @start[tid] = nsecs; }
kretprobe:napi_poll /@start[tid]/ {
    @poll_duration_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# 3. GRO 병합률 측정
bpftrace -e '
kprobe:napi_gro_receive { @gro_in = count(); }
kprobe:napi_gro_flush { @gro_flush = count(); }
interval:s:5 {
    printf("GRO ratio: %d pkts → %d flushes\n",
           @gro_in, @gro_flush);
    clear(@gro_in); clear(@gro_flush);
}'

# 4. NAPI 스케줄 → poll 실행 지연 (softIRQ 스케줄링 레이턴시)
bpftrace -e '
kprobe:__napi_schedule { @sched[tid] = nsecs; }
kprobe:napi_poll /@sched[tid]/ {
    @sched_to_poll_us = hist((nsecs - @sched[tid]) / 1000);
    delete(@sched[tid]);
}'

# 5. time_squeeze 발생 빈도
bpftrace -e '
tracepoint:net:net_rx_action_time_squeeze {
    @squeeze = count();
    @squeeze_cpu = lhist(cpu, 0, 64, 1);
}'

벤치마크 체크리스트

항목확인 내용명령어
CPU governor performance 모드 (절전 비활성화) cpupower frequency-set -g performance
C-state 깊은 C-state 비활성화 (레이턴시) cpupower idle-set -D 1
IRQ 어피니티 NIC IRQ가 올바른 CPU에 고정 cat /proc/interrupts | grep eth0
NUMA 로컬리티 NIC과 CPU가 같은 NUMA 노드 cat /sys/class/net/eth0/device/numa_node
링 크기 드롭 방지를 위한 링 크기 확인 ethtool -g eth0
오프로드 GRO, TSO, RSS 등 활성 확인 ethtool -k eth0
conntrack 벤치마크 시 비활성화 고려 sysctl net.netfilter.nf_conntrack_max
배경 트래픽 다른 네트워크 트래픽 최소화 iftop / nload

커널 설정 옵션 레퍼런스

옵션기본값설명영향
CONFIG_NET_RX_BUSY_POLL y 버지 폴링 지원 활성화 SO_BUSY_POLL, napi_busy_loop() 사용 가능
CONFIG_RPS y Receive Packet Steering 활성화 소프트웨어 RSS, rps_cpus 설정 가능
CONFIG_RFS_ACCEL y (RPS 의존) Accelerated RFS (하드웨어 지원) ndo_rx_flow_steer 콜백 활성화
CONFIG_XPS y Transmit Packet Steering TX 큐 → CPU 매핑 최적화
CONFIG_XDP_SOCKETS y (선택) AF_XDP 소켓 지원 UMEM, zero-copy XDP 패킷 수신
CONFIG_PAGE_POOL y page_pool 할당자 NAPI 기반 페이지 재활용
CONFIG_PAGE_POOL_STATS y (선택) page_pool 통계 수집 ethtool로 page_pool 사용량 조회
CONFIG_NET_FLOW_LIMIT y 플로우 단위 수신 제한 특정 플로우의 backlog 독점 방지
CONFIG_BPF_SYSCALL y BPF 시스콜 (XDP 필수) XDP 프로그램 로드/실행
CONFIG_PREEMPT_RT n (선택) 실시간 프리엠션 스레드 NAPI 자동 활성화 고려
CONFIG_NET_EGRESS y TC egress 훅 XDP_TX 경로에서 TC 처리 가능

런타임 sysfs/procfs 인터페이스 종합

경로설명예시
/proc/net/softnet_stat per-CPU softnet 통계 (hex) 열: processed, dropped, time_squeeze, ...
/sys/class/net/<dev>/queues/rx-N/rps_cpus RPS CPU 비트마스크 echo ff > rps_cpus
/sys/class/net/<dev>/queues/rx-N/rps_flow_cnt per-큐 RFS 플로우 수 echo 4096 > rps_flow_cnt
/sys/class/net/<dev>/gro_flush_timeout GRO 플러시 타이머 (ns) echo 20000 > gro_flush_timeout
/sys/class/net/<dev>/napi_defer_hard_irqs IRQ 재활성화 지연 횟수 echo 10 > napi_defer_hard_irqs
/proc/sys/net/core/netdev_budget net_rx_action 총 버짓 sysctl -w net.core.netdev_budget=600
/proc/sys/net/core/netdev_budget_usecs net_rx_action 시간 제한 sysctl -w net.core.netdev_budget_usecs=4000
/proc/sys/net/core/busy_poll poll()/select() 버지 폴링 μs sysctl -w net.core.busy_poll=50
/proc/sys/net/core/busy_read read() 버지 폴링 μs sysctl -w net.core.busy_read=50
/proc/sys/net/core/netdev_max_backlog backlog 큐 최대 길이 sysctl -w net.core.netdev_max_backlog=10000
/proc/sys/net/core/rps_sock_flow_entries 전역 RFS 플로우 테이블 크기 sysctl -w net.core.rps_sock_flow_entries=32768
/proc/sys/net/core/flow_limit_cpu_bitmap 플로우 제한 활성 CPU echo ff > flow_limit_cpu_bitmap

NAPI 상태 머신 완전 레퍼런스

전체 상태 전이 다이어그램

NAPI의 모든 상태 비트와 전이 경로를 종합한 완전한 상태 머신입니다. 각 전이는 특정 함수 호출에 의해 트리거됩니다.

NAPI 상태 머신 (State Machine) IDLE state = 0 (LISTED만 설정) SCHEDULED NAPI_STATE_SCHED 설정 napi_schedule() POLLING poll() 실행 중 net_rx_action() napi_complete_done() [패킷 소진, MISSED 없음] 버짓 소진 (재스케줄) MISSED SCHED + MISSED 설정 poll 중 새 IRQ 즉시 재스케줄 SCHED_THREADED 커널 스레드에서 poll() THREADED=1 complete DISABLED NAPI_STATE_DISABLE 설정 napi_disable() napi_enable() IN_BUSY_POLL 사용자 공간 직접 폴링 sk_busy_loop() busy_loop 종료 전이 트리거: napi_schedule() — IRQ 핸들러 net_rx_action() — softIRQ napi_complete_done() — poll() 내 napi_disable()/enable() — 드라이버

상태 비트 동시성과 경쟁 조건(Race Condition)

경쟁 조건관련 비트해결 메커니즘코드 경로
IRQ + softIRQ 동시 스케줄 SCHED test_and_set_bit() 원자 연산 napi_schedule_prep()
poll() 중 새 IRQ 도착 SCHED + MISSED MISSED 비트 설정 → complete에서 재스케줄 napi_complete_done()
busy_poll + softIRQ 동시 IN_BUSY_POLL IN_BUSY_POLL 설정 시 softIRQ 스킵 napi_poll(), sk_busy_loop()
disable + 진행중 poll() DISABLE + SCHED usleep_range()으로 SCHED 클리어 대기 napi_disable()
threaded + softIRQ 경쟁 SCHED_THREADED THREADED 비트 확인 후 분기 __napi_schedule()
enable 직후 즉시 schedule DISABLE → SCHED enable이 DISABLE 클리어 후 즉시 스케줄 가능 napi_enable()napi_schedule()

NAPI_STATE_MISSED의 중요성

NAPI_STATE_MISSED는 NAPI의 가장 정교한 동기화 메커니즘 중 하나입니다. 이 비트 없이는 다음과 같은 패킷 손실 창(window)이 발생할 수 있습니다:

MISSED 비트 없는 경우의 레이스 (문제 상황):

CPU 0 (softIRQ poll)           CPU 1 (IRQ 핸들러)
─────────────────────          ─────────────────────
poll() 실행 중...
패킷 모두 처리 완료
                               새 패킷 도착!
                               napi_schedule_prep()
                               → SCHED 이미 설정됨 → 실패!
napi_complete_done()
SCHED 비트 클리어
IRQ 재활성화
→ 새 패킷이 IRQ에 의해
  재발견될 때까지 지연!

해결: MISSED 비트 적용 후:

CPU 0 (softIRQ poll)           CPU 1 (IRQ 핸들러)
─────────────────────          ─────────────────────
poll() 실행 중...
패킷 모두 처리 완료
                               새 패킷 도착!
                               napi_schedule_prep()
                               → SCHED 이미 설정됨
                               → MISSED 비트 설정!
napi_complete_done()
MISSED 확인 → 즉시 재스케줄!
→ 패킷 손실 없음
상태 비트 디버깅: NAPI 상태 비트는 /sys/kernel/debug/napi_threadedbpftrace로 실시간 관찰할 수 있습니다: bpftrace -e 'kprobe:napi_complete_done { printf("state=0x%lx\n", ((struct napi_struct *)arg0)->state); }'

DIM (Dynamic Interrupt Moderation)과 NAPI

DIM 개요

동적 인터럽트 조절(Dynamic Interrupt Moderation, DIM)은 네트워크 트래픽 패턴에 따라 인터럽트 coalescing 파라미터(rx-usecs, rx-frames 등)를 자동으로 조정하는 커널 라이브러리입니다. 커널 소스 트리의 lib/dim/ 디렉토리에 위치하며, 네트워크 드라이버뿐 아니라 스토리지 드라이버에서도 활용할 수 있는 범용 프레임워크로 설계되었습니다.

전통적으로 드라이버는 인터럽트 조절 레지스터(ITR) 값을 하드코딩하거나, 관리자가 ethtool -C로 수동 설정해야 했습니다. 이 방식은 트래픽 패턴이 변화하는 실제 환경에서 최적 성능을 유지하기 어렵습니다. DIM 알고리즘은 언덕 오르기(Hill-climbing) 방식을 사용하여 처리량(throughput)과 지연(latency) 간 최적 균형점을 동적으로 탐색합니다.

DIM의 동작 원리는 다음과 같습니다:

  1. NAPI poll() 완료 시점에서 드라이버가 DIM에 패킷/바이트/이벤트 통계를 전달합니다.
  2. DIM 라이브러리가 이전 측정 구간과 비교하여 성능 변화를 감지합니다.
  3. 성능 변화에 따라 coalescing 프로파일 인덱스를 조정합니다.
  4. 워크큐(workqueue)를 통해 새 프로파일을 하드웨어에 적용합니다.

DIM 자료구조

DIM의 핵심 자료구조는 세 가지입니다. struct dim은 DIM 인스턴스(보통 큐당 하나), struct dim_sample은 매 poll에서 수집하는 통계, struct dim_cq_moder는 실제 하드웨어에 적용할 coalescing 파라미터를 나타냅니다.

/* 커널 소스 분석: include/linux/dim.h */

/* DIM 인스턴스 — per-queue로 할당 */
struct dim {
    u8                     mode;         /* CQ period 모드 */
    u8                     tune_state;   /* GOING_RIGHT / GOING_LEFT / PARKING */
    u8                     profile_ix;   /* 현재 프로파일 인덱스 (0~4) */
    u8                     steps_right;  /* 오른쪽 탐색 남은 횟수 */
    u8                     steps_left;   /* 왼쪽 탐색 남은 횟수 */
    u8                     tired;        /* 방향 전환 없이 연속 주차 횟수 */
    u8                     state;        /* DIM_START_MEASURE / DIM_MEASURE_IN_PROGRESS */
    struct dim_stats       prev_stats;   /* 이전 측정 구간 통계 */
    struct dim_sample     measuring_sample; /* 현재 측정 시작 시점 샘플 */
    struct work_struct    work;         /* 프로파일 적용 workqueue */
    unsigned long          jiffies_last; /* 마지막 업데이트 시각 */
    u64                    start_sample; /* 측정 시작 누적 이벤트 수 */
};

/* 매 poll에서 수집하는 누적 통계 */
struct dim_sample {
    u32     pkt_ctr;    /* 처리한 패킷 수 (누적) */
    u32     byte_ctr;   /* 처리한 바이트 수 (누적) */
    u16     event_ctr;  /* 인터럽트/poll 실행 횟수 (누적) */
    u32     comp_ctr;   /* completion 이벤트 수 */
};

/* HW coalescing 레지스터에 적용할 파라미터 */
struct dim_cq_moder {
    u16     usec;           /* 인터럽트 지연 (마이크로초) */
    u16     pkts;           /* 인터럽트당 패킷 수 임계값 */
    u16     cq_period_mode; /* EQE 기반 vs CQE 기반 */
};

/* DIM 모드 상수 */
#define DIM_CQ_PERIOD_MODE_START_FROM_EQE  0  /* 이벤트 기반 */
#define DIM_CQ_PERIOD_MODE_START_FROM_CQE  1  /* completion 기반 */

/* 튜닝 상태 상수 */
#define DIM_PARKING      0  /* 최적점 도달, 대기 */
#define DIM_GOING_RIGHT  1  /* aggressive 방향 탐색 */
#define DIM_GOING_LEFT   2  /* conservative 방향 탐색 */

DIM 알고리즘 내부

DIM은 언덕 오르기(Hill-climbing) 알고리즘을 사용합니다. 핵심 아이디어는 현재 프로파일에서 성능 지표(패킷 처리율, 바이트 처리율)를 측정하고, 프로파일 인덱스를 한 단계씩 이동하면서 성능이 개선되는 방향을 찾는 것입니다.

5단계 프로파일은 net_dim_rx_mode_profiles 배열에 정의되며, 인덱스가 높을수록 더 공격적인 coalescing(높은 처리량, 높은 지연)을 의미합니다:

프로파일usecpkts특성
028최저 지연 (Ultra-low latency)
1816저지연 (Low latency)
23232균형 (Moderate)
36464고처리량 (High throughput)
4128128최대 처리량 (Ultra-high throughput)

알고리즘의 상태 전이 로직은 다음과 같습니다:

  1. GOING_RIGHT: 프로파일 인덱스를 증가시키며 성능을 측정합니다. 처리량이 개선되면 계속 같은 방향으로 이동하고, 악화되면 GOING_LEFT로 전환합니다.
  2. GOING_LEFT: 프로파일 인덱스를 감소시키며 성능을 측정합니다. 개선되면 계속 왼쪽으로, 악화되면 PARKING 상태로 전환합니다.
  3. PARKING: 최적점에 도달했다고 판단하고 대기합니다. 통계에 유의미한 변화가 감지되면 다시 탐색을 시작합니다.
/* 커널 소스 분석: lib/dim/net_dim.c — net_dim_decision() 핵심 로직 */

static int net_dim_step(struct dim *dim)
{
    /* 현재 상태에 따라 프로파일 인덱스 조정 */
    if (dim->tune_state == DIM_GOING_RIGHT) {
        if (dim->profile_ix == (NET_DIM_PARAMS_NUM_PROFILES - 1))
            return DIM_ON_EDGE;  /* 이미 최대 프로파일 */
        dim->profile_ix++;
        dim->steps_right++;
    }
    if (dim->tune_state == DIM_GOING_LEFT) {
        if (dim->profile_ix == 0)
            return DIM_ON_EDGE;  /* 이미 최소 프로파일 */
        dim->profile_ix--;
        dim->steps_left++;
    }
    return DIM_STEPPED;
}

static void net_dim_decision(struct dim_stats *curr_stats,
                              struct dim *dim)
{
    int prev_ix = dim->profile_ix;
    int stats_res;  /* WORSE / SAME / BETTER */
    int step_res;

    /* 이전 구간 대비 성능 변화 판정 */
    stats_res = net_dim_stats_compare(curr_stats, &dim->prev_stats);

    switch (dim->tune_state) {
    case DIM_PARKING:
        /* 통계 변화가 충분하면 탐색 재개 */
        if (stats_res != DIM_STATS_SAME)
            net_dim_exit_parking(dim);
        break;

    case DIM_GOING_RIGHT:
    case DIM_GOING_LEFT:
        step_res = net_dim_step(dim);
        if (step_res == DIM_ON_EDGE)
            net_dim_turn(dim);  /* 경계 도달: 방향 전환 */

        if (stats_res == DIM_STATS_WORSE)
            net_dim_turn(dim);  /* 성능 악화: 방향 전환 */

        /* 너무 많이 왔다갔습니다 하면 PARKING */
        if (dim->steps_right > DIM_THRESH ||
            dim->steps_left > DIM_THRESH)
            dim->tune_state = DIM_PARKING;
        break;
    }

    /* 프로파일이 변경되었으면 workqueue 예약 */
    if (prev_ix != dim->profile_ix)
        schedule_work(&dim->work);

    dim->prev_stats = *curr_stats;
}
GOING_RIGHT profile_ix 증가 방향 GOING_LEFT profile_ix 감소 방향 PARKING 최적점 유지, 대기 성능 개선? YES: 계속 NO 성능 개선? YES NO 통계 변화 감지 → 탐색 재개

드라이버의 DIM 통합 패턴

드라이버에서 DIM을 통합하는 전형적인 패턴은 세 단계로 구성됩니다: 초기화, poll 통계 전달, 워크큐 핸들러에서 하드웨어 설정 적용.

/* 커널 소스 분석: 드라이버 DIM 통합 패턴 */

/* === 1단계: probe() 시 DIM 초기화 === */
static int my_probe(struct pci_dev *pdev,
                     const struct pci_device_id *id)
{
    struct my_priv *priv;
    int i;

    /* ... 장치 초기화 생략 ... */

    /* 각 RX 큐마다 DIM 인스턴스 초기화 */
    for (i = 0; i < priv->num_rx_queues; i++) {
        INIT_WORK(&priv->rx_ring[i].dim.work, my_dim_work);
        priv->rx_ring[i].dim.mode =
            DIM_CQ_PERIOD_MODE_START_FROM_EQE;
    }
    return 0;
}

/* === 2단계: poll() 완료 시 DIM에 통계 전달 === */
static int my_poll(struct napi_struct *napi, int budget)
{
    struct my_rx_ring *ring =
        container_of(napi, struct my_rx_ring, napi);
    struct dim_sample dim_sample = {};
    int work_done;

    work_done = my_clean_rx(ring, budget);

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

    /* DIM에 누적 통계 전달 */
    dim_update_sample(ring->total_packets,
                      ring->total_bytes,
                      &dim_sample);
    net_dim(&ring->dim, dim_sample);

    return work_done;
}

/* === 3단계: DIM workqueue 핸들러 — 새 프로파일 적용 === */
static void my_dim_work(struct work_struct *work)
{
    struct dim *dim =
        container_of(work, struct dim, work);
    struct my_rx_ring *ring =
        container_of(dim, struct my_rx_ring, dim);

    /* DIM이 결정한 새 프로파일 가져오기 */
    struct dim_cq_moder moder =
        net_dim_get_rx_moderation(dim->mode, dim->profile_ix);

    /* HW 인터럽트 coalescing 레지스터에 새 값 적용 */
    my_set_coalesce(ring, moder.usec, moder.pkts);

    /* 측정 상태 리셋: 다음 측정 구간 시작 */
    dim->state = DIM_START_MEASURE;
}

ethtool과의 연동도 중요합니다. 관리자가 ethtool -C eth0 adaptive-rx on 명령을 실행하면 드라이버의 set_coalesce 콜백에서 DIM을 활성화합니다. 반대로 adaptive-rx off는 DIM을 비활성화하고 수동 coalescing 값을 적용합니다.

/* 커널 소스 분석: ethtool set_coalesce에서 DIM 토글 */

static int my_set_coalesce(struct net_device *dev,
                            struct ethtool_coalesce *ec,
                            struct kernel_ethtool_coalesce *kec,
                            struct netlink_ext_ack *extack)
{
    struct my_priv *priv = netdev_priv(dev);

    if (ec->use_adaptive_rx_coalesce) {
        /* DIM 활성화: 초기 프로파일 설정 후 자동 조정 시작 */
        priv->adaptive_rx = 1;
        /* DIM은 다음 poll() 호출부터 동작 시작 */
    } else {
        /* DIM 비활성화: 수동 값 적용 */
        priv->adaptive_rx = 0;
        cancel_work_sync(&priv->rx_ring[0].dim.work);
        my_write_coalesce_hw(priv,
                              ec->rx_coalesce_usecs,
                              ec->rx_max_coalesced_frames);
    }
    return 0;
}
DIM 워크큐 설계 의도: DIM은 워크큐(workqueue)에서 HW coalescing 레지스터를 변경합니다. NAPI poll 경로에서는 dim_update_sample()net_dim() 호출만 수행하며, 이 두 함수는 경량 연산(통계 비교, 상태 전이)만 포함합니다. 실제 하드웨어 레지스터 쓰기는 워크큐 컨텍스트에서 별도로 수행되므로, poll 경로의 지연에 영향을 주지 않습니다.

napi_busy_loop() 내부 구현

소켓 레벨 busy polling 메커니즘

바쁜 폴링(Busy polling)은 애플리케이션 스레드가 소프트인터럽트(softirq) 경로를 거치지 않고, 소켓의 recvmsg() 또는 epoll_wait() 호출 내에서 직접 NAPI poll을 실행하는 기법입니다. 이를 통해 인터럽트 → softirq → 웨이크업의 오버헤드를 제거하고 수 마이크로초 단위의 지연 단축을 달성할 수 있습니다.

전체 호출 체인은 다음과 같습니다:

/* 호출 체인 개요 */
recvmsg() / epoll_wait()
  → sk_busy_loop(sk, nonblock)
    → napi_busy_loop(napi_id, loop_end, loop_end_arg,
                      prefer_busy_poll, budget)
      → napi_poll_owner(napi, budget)
        → napi->poll(napi, budget)  /* 드라이버 poll 직접 호출 */

napi_busy_loop()의 핵심 구현을 분석합니다:

/* 커널 소스 분석: net/core/dev.c — napi_busy_loop() */

void napi_busy_loop(unsigned int napi_id,
                    bool (*loop_end)(void *, unsigned long),
                    void *loop_end_arg,
                    bool prefer_busy_poll, u16 budget)
{
    unsigned long start_time =
        loop_end ? busy_loop_current_time() : 0;
    struct napi_struct *napi;

restart:
    /* napi_id로 해시 테이블에서 NAPI 인스턴스 조회 (O(1)) */
    napi = napi_by_id(napi_id);
    if (!napi)
        return;

    preempt_disable();
    for (;;) {
        int work = 0;

        local_bh_disable();
        /* softirq가 이미 이 NAPI를 처리 중인지 확인 */
        if (!napi_poll_lock(napi)) {
            /* 잠금 실패: softirq가 처리 중
             * prefer_busy_poll이 설정되면 힌트 비트를 남겨
             * softirq가 빨리 양보하도록 유도 */
            if (prefer_busy_poll)
                set_bit(NAPI_STATE_PREFER_BUSY_POLL,
                        &napi->state);
            local_bh_enable();
            preempt_enable();

            /* 종료 조건 확인 후 재시도 또는 종료 */
            if (loop_end &&
                loop_end(loop_end_arg, start_time))
                return;
            cpu_relax();
            goto restart;
        }

        /* IN_BUSY_POLL 비트 설정: 독점 폴링 모드 진입 */
        work = napi_poll_owner(napi, budget);

        napi_poll_unlock(napi);
        local_bh_enable();

        /* 패킷을 처리했으면 타임아웃 갱신 */
        if (work > 0)
            busy_loop_reset_time(&start_time);

        /* 종료 조건: 타임아웃 초과 또는 데이터 수신 완료 */
        if (loop_end &&
            loop_end(loop_end_arg, start_time))
            break;

        /* 스케줄링이 필요하면 양보 후 재개 */
        if (unlikely(need_resched())) {
            preempt_enable();
            cond_resched();
            preempt_disable();
        }
    }
    preempt_enable();
}

napi_by_id()는 전역 해시 테이블 napi_hash[]에서 napi_id를 키로 O(1) 조회합니다. 소켓이 특정 NAPI 인스턴스에 바인딩되면 sk->sk_napi_id에 해당 ID가 저장되어, 이후 busy poll 시 빠르게 올바른 NAPI를 찾을 수 있습니다.

소켓 옵션과 busy loop의 관계는 다음과 같습니다:

소켓 옵션설명기본값
SO_BUSY_POLLbusy poll 타임아웃 (μs). 이 시간 동안 poll 반복0 (비활성)
SO_BUSY_POLL_BUDGETbusy poll 한 번에 처리할 최대 패킷 수NAPI_POLL_WEIGHT (64)
SO_PREFER_BUSY_POLLsoftirq보다 busy poll 우선. softirq에게 양보 힌트 전달false
SO_INCOMING_NAPI_ID소켓에 바인딩된 NAPI ID 조회 (읽기 전용)N/A

시스템 전역 sysctl 설정도 busy poll 동작에 영향을 줍니다:

sysctl설명기본값
net.core.busy_poll전역 busy poll 타임아웃 (μs). SO_BUSY_POLL 미설정 시 적용0
net.core.busy_readrecvmsg()에서의 busy read 타임아웃 (μs)0
Busy Poll 경로 vs 일반 인터럽트 경로 Busy Poll 경로 (저지연) recvmsg() sk_busy_loop() napi_busy_loop() napi_poll_owner() 드라이버 poll() 반환 애플리케이션 스레드가 직접 poll() 호출 — softirq/컨텍스트 스위치 없음 일반 인터럽트 경로 HW IRQ napi_schedule() NET_RX softirq net_rx_action() 드라이버 poll() (ksoftirqd 컨텍스트) 소켓 버퍼 전달 wake_up(sk) 컨텍스트 스위치 → 앱 스레드 깨움 recvmsg() 반환 Busy poll: ~2-5μs 직접 poll → 즉시 반환 (단일 컨텍스트) 인터럽트 경로: ~20-50μs IRQ → softirq → wake → 컨텍스트 스위치 포함 ※ 지연 값은 일반적인 10GbE NIC 환경 기준 근사치
PREFER_BUSY_POLL의 역할: SO_PREFER_BUSY_POLL이 설정되면, busy loop 진입 시 NAPI_STATE_PREFER_BUSY_POLL 비트가 설정됩니다. softirq 측의 net_rx_action()은 이 비트를 확인하고, 해당 NAPI에 대한 처리를 건너뛰어 busy poll 스레드에게 우선권을 양보합니다. 이는 softirq와 busy poll 간의 경합을 줄이는 협력적 메커니즘입니다.

NAPI weight 전략과 설계 가이드

weight(버짓)의 정확한 의미

NAPI의 weight는 한 번의 poll() 호출에서 드라이버가 처리할 수 있는 최대 패킷 수를 의미합니다. 이 값은 netif_napi_add() 호출 시 설정되며, NAPI 인스턴스의 고유 속성으로 런타임에 변경되지 않습니다.

net_rx_action()은 전체 netdev_budget(기본 300)을 각 NAPI 인스턴스에 weight 단위로 분배합니다. poll 함수의 반환값은 실제 처리한 패킷 수이며, 이 값이 NAPI의 상태 전이를 결정하는 핵심 신호입니다:

/* 커널 소스 분석: net/core/dev.c — napi_poll() 내부 로직 */

static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
    int work, weight;

    list_del_init(&n->poll_list);

    weight = n->weight;

    /* budget = min(weight, 남은 netdev_budget) */
    work = n->poll(n, weight);

    /* 드라이버가 weight 초과 반환 시 경고 */
    if (unlikely(work > weight))
        pr_err_once(
            "NAPI poll function %pS returned %d, "
            "exceeding its budget of %d.\n",
            n->poll, work, weight);

    /* weight만큼 처리 = 더 있을 수 있음 → repoll */
    if (likely(work < weight))
        goto out;

    /* NAPI_STATE_DISABLE이면 완료 처리 */
    if (unlikely(napi_disable_pending(n))) {
        napi_complete(n);
        goto out;
    }

    /* repoll 리스트에 추가: 다음 루프에서 다시 poll */
    if (n->gro_bitmask) {
        /* GRO flush 수행 */
        napi_gro_flush(n, HZ >= 1000);
    }
    list_add_tail(&n->poll_list, repoll);

out:
    return work;
}

weight 값별 특성

커널에서 사용하는 대표적인 weight 값과 각각의 설계 의도는 다음과 같습니다:

Weight용도장점단점
64 (NAPI_POLL_WEIGHT)물리 NIC 표준여러 NAPI 간 공정한 시분할 보장. net_rx_action 루프에서 균등하게 처리 기회 분배고부하에서 빈번한 poll 호출 전환으로 인한 오버헤드
256per-CPU backlog NAPI소프트웨어 경로이므로 HW 제약 없음. 높은 처리량 확보다른 물리 NIC NAPI와 처리 시간 불균형 가능
64 (netif_napi_add_tx)TX completion 처리RX와 동일한 공정성 기준 적용TX-heavy 워크로드에서 completion 지연 발생 가능
weight 제한 규칙: 물리 NIC 드라이버는 반드시 NAPI_POLL_WEIGHT (64) 이하의 weight를 사용해야 합니다. netif_napi_add()에서 weight가 NAPI_POLL_WEIGHT를 초과하면 커널이 경고를 출력하고 강제로 NAPI_POLL_WEIGHT로 클램핑합니다. 소프트웨어 NAPI(backlog)만이 예외적으로 높은 값을 사용할 수 있으며, 이는 netif_napi_add_weight()를 통해 설정합니다.
/* 커널 소스 분석: net/core/dev.c — weight 검증 */

void netif_napi_add_weight(struct net_device *dev,
                            struct napi_struct *napi,
                            int (*poll)(struct napi_struct *, int),
                            int weight)
{
    /* 내부적으로 NAPI 초기화 */
    INIT_LIST_HEAD(&napi->poll_list);
    napi->poll = poll;
    napi->weight = weight;

    /* weight 유효성 검증: 드라이버 버그 감지 */
    if (WARN_ON(weight > NAPI_POLL_WEIGHT &&
               !test_bit(NAPI_STATE_NO_BUSY_POLL, &napi->state)))
        napi->weight = NAPI_POLL_WEIGHT;

    /* ... 해시 테이블 등록, NAPI ID 할당 ... */
}

/* 편의 매크로: 물리 NIC용 (weight=64 고정) */
static inline void
netif_napi_add(struct net_device *dev,
               struct napi_struct *napi,
               int (*poll)(struct napi_struct *, int))
{
    netif_napi_add_weight(dev, napi, poll, NAPI_POLL_WEIGHT);
}

/* TX completion 전용 (weight=64 고정) */
static inline void
netif_napi_add_tx(struct net_device *dev,
                   struct napi_struct *napi,
                   int (*poll)(struct napi_struct *, int))
{
    netif_napi_add_weight(dev, napi, poll, NAPI_POLL_WEIGHT);
}

budget vs weight 차이

budgetweight는 자주 혼동되지만 의미가 다릅니다. weight는 NAPI 인스턴스의 고유 속성이고, budget은 실제 poll 호출 시 전달되는 처리 한도입니다:

용어의미범위제어 주체
weightNAPI 인스턴스 고유 속성. poll당 최대 처리량netif_napi_add()에서 설정드라이버
budgetnet_rx_action()poll()에 전달하는 실제 처리 한도min(weight, 남은 netdev_budget)커널 코어
netdev_budgetnet_rx_action()의 전체 처리 한도 (1회 softirq)/proc/sys/net/core/netdev_budget시스템 관리자 (기본 300)
netdev_budget_usecsnet_rx_action()의 시간 제한/proc/sys/net/core/netdev_budget_usecs시스템 관리자 (기본 2000μs)

budgetweight보다 작아지는 상황을 이해하는 것이 중요합니다. net_rx_action()은 poll_list의 NAPI를 순회하면서 전체 netdev_budget에서 각 poll의 처리량을 차감합니다. 예를 들어 netdev_budget=300이고 weight=64인 NAPI 5개가 있다면:

/* 커널 소스 분석: net/core/dev.c — net_rx_action budget 분배 */

static __latent_entropy void net_rx_action(void)
{
    struct list_head *list = &sd->poll_list;
    int budget = READ_ONCE(net_hotdata.netdev_budget);
    unsigned long time_limit =
        jiffies + usecs_to_jiffies(
            READ_ONCE(net_hotdata.netdev_budget_usecs));

    /*
     * NAPI 순회 예시 (netdev_budget=300, 각 weight=64):
     *
     * NAPI-1: budget=64, 처리=64 → 남은 전체 budget=236
     * NAPI-2: budget=64, 처리=64 → 남은 전체 budget=172
     * NAPI-3: budget=64, 처리=64 → 남은 전체 budget=108
     * NAPI-4: budget=64, 처리=64 → 남은 전체 budget=44
     * NAPI-5: budget=44 (weight 64보다 작음!), 처리=44
     *                              → 남은 전체 budget=0
     *
     * NAPI-5는 weight(64)보다 작은 budget(44)을 받음
     * → poll()이 44를 반환하면 budget == work_done이므로
     *   repoll 대상이 됨 (큐가 비었어도!)
     */

    for (;;) {
        struct napi_struct *n;

        skb_defer_free_flush(sd);

        if (list_empty(list)) {
            /* poll_list가 비었으면
             * repoll 리스트를 다시 poll_list로 이동 */
            if (!sd_has_rps_ipi_waiting(sd) &&
                list_empty(&repoll))
                return;
            break;
        }

        n = list_first_entry(list, struct napi_struct,
                              poll_list);
        budget -= napi_poll(n, &repoll);

        /* 전체 budget 소진 또는 시간 초과 시 종료 */
        if (unlikely(budget <= 0 ||
                     time_after_eq(jiffies, time_limit))) {
            sd->time_squeeze++;
            break;
        }
    }
}
netdev_budget 튜닝 가이드: netdev_budgetnetdev_budget_usecs는 softirq의 실행 시간을 제어합니다. 값을 높이면 패킷 처리량이 증가하지만, softirq가 CPU를 더 오래 점유하여 다른 태스크의 지연이 늘어난다. /proc/net/softnet_stat의 두 번째 열(time_squeeze)이 지속적으로 증가하면 budget이 부족한 것이므로 값을 높이는 것을 고려합니다. 반대로 대화형 워크로드에서 반응성이 중요하면 기본값 유지 또는 감소를 고려합니다.

GRO flush와 napi_watchdog() 메커니즘

일반 수신 경로(Generic Receive Offload, GRO)는 NAPI 폴링 루프 내에서 작은 패킷들을 하나의 큰 패킷으로 병합하여 상위 프로토콜 스택의 처리 횟수를 줄입니다. 그러나 병합된 패킷(held skb)을 언제 상위 스택으로 올려보낼지 결정하는 flush 타이밍이 처리량(Throughput)과 지연(Latency) 사이의 핵심 트레이드오프(Trade-off)를 결정합니다.

GRO flush 타이밍 3가지

커널은 다음 세 가지 조건에서 GRO 버퍼를 flush합니다.

  1. poll 완료 시 즉시 flushnapi_complete_done()이 호출되는 시점에 napi_gro_flush(napi, false)로 모든 held skb를 즉시 상위 스택으로 전달합니다. 이것이 가장 일반적인 flush 경로입니다.
  2. gro_flush_timeout 타이머 — NIC 드라이버가 napi->gro_flush_timeout을 0이 아닌 값(나노초 단위)으로 설정하면, napi_watchdog() hrtimer가 해당 시간이 경과한 후 자동으로 NAPI를 재스케줄하여 flush를 유발합니다. 패킷이 드문드문 도착하는 상황에서 GRO 버퍼가 오래 머무르지 않도록 합니다.
  3. MAX_GRO_SKBS 초과 — GRO 해시 버킷에 누적된 held skb 수가 MAX_GRO_SKBS(기본값 8)를 초과하면 가장 오래된 항목을 즉시 flush합니다. 메모리 고갈을 방지하는 안전장치입니다.

napi_gro_flush() 내부 구현

/* net/core/gro.c */
void napi_gro_flush(struct napi_struct *napi, bool flush_old)
{
    struct gro_node *gro = &napi->gro;
    u32 i;

    for (i = 0; i < GRO_HASH_BUCKETS; i++) {
        struct list_head *head = &gro->hash[i].list;
        struct sk_buff *skb, *p;

        if (list_empty(head))
            continue;

        if (flush_old) {
            /* flush_old=true: 오래된(age가 현재와 다른) skb만 flush
             * napi_watchdog 경로에서 사용 */
            list_for_each_entry_safe(skb, p, head, list) {
                if (NAPI_GRO_CB(skb)->age == jiffies)
                    continue;
                list_del(&skb->list);
                skb_list_del_init(skb);
                napi_gro_complete(gro, skb);
                gro->hash[i].count--;
            }
        } else {
            /* flush_old=false: 버킷 내 모든 held skb를 즉시 상위 스택으로 전달 */
            list_for_each_entry_safe(skb, p, head, list) {
                skb_list_del_init(skb);
                napi_gro_complete(gro, skb);
            }
            gro->hash[i].count = 0;
        }
    }
    gro->bitmask = 0;
}
flush_old 파라미터의 의미:
  • napi_gro_flush(napi, false) — poll 완료 시 호출. 모든 held skb를 즉시 flush합니다. 버킷이 완전히 비워집니다.
  • napi_gro_flush(napi, true)napi_watchdog()에서 호출. 현재 jiffies와 age가 다른(= 이전 poll 사이클에서 누적된) skb만 선택적으로 flush합니다. 같은 jiffies 내에 병합 중인 skb는 보존합니다.

napi_watchdog() hrtimer 콜백

/* net/core/dev.c */
static enum hrtimer_restart napi_watchdog(struct hrtimer *timer)
{
    struct napi_struct *napi;

    napi = container_of(timer, struct napi_struct, timer);

    /* GRO 버퍼에 오래된 skb가 있으면 NAPI를 재스케줄하여
     * napi_gro_flush(napi, true)가 실행되도록 유도합니다 */
    if (!napi_disable_pending(napi) &&
        !test_bit(NAPI_STATE_SCHED, &napi->state))
        napi_schedule(napi);

    return HRTIMER_NORESTART;
}

드라이버에서 napi->gro_flush_timeout을 설정하면 napi_complete_done()hrtimer_start()로 타이머를 재무장합니다. 타이머 만료 시 napi_watchdog()가 NAPI를 재스케줄하고, 다음 폴링에서 napi_gro_flush(napi, true)로 오래된 GRO 버퍼를 처리합니다.

napi_skb_finish()와 GRO 결과 처리

/* net/core/gro.c — GRO 병합 결과에 따른 분기 처리 */
static void napi_skb_finish(struct napi_struct *napi,
                             struct sk_buff *skb,
                             gro_result_t ret)
{
    switch (ret) {
    case GRO_NORMAL:
        /* 병합 불가 — 단일 skb로 바로 프로토콜 스택에 전달 */
        gro_normal_one(napi, skb, 1);
        break;

    case GRO_MERGED_FREE:
        /* 기존 held skb에 병합됨, 새 skb는 해제 */
        napi_reuse_skb(napi, skb);
        break;

    case GRO_HELD:
        /* GRO 버퍼에 보관 중 — 추후 flush 시 상위 전달 */
        break;

    case GRO_MERGED:
        break;

    case GRO_CONSUMED:
        break;
    }
}

GRO와 TSO의 대칭 관계

GRO(Generic Receive Offload)와 TSO(TCP Segmentation Offload)는 서로 대칭적인 관계입니다. TSO는 송신 경로에서 커널이 하나의 큰 세그먼트(Segment)를 NIC에 전달하면 NIC가 MTU(Maximum Transmission Unit) 단위로 분할하여 전송합니다. GRO는 수신 경로에서 NIC가 수신한 여러 개의 작은 패킷을 커널이 하나의 큰 패킷으로 재병합하여 프로토콜 스택에 전달합니다. 두 기술 모두 프로토콜 스택이 처리해야 하는 패킷 수를 줄여 CPU 오버헤드를 감소시킵니다.

GRO flush 타이밍 시퀀스 다이어그램

시간 ① poll 완료 즉시 flush 패킷 수신 GRO 병합 napi_complete_done() napi_gro_flush(napi, false) → 전체 flush ② watchdog 타이머 flush 패킷 드문드문 GRO held timeout 경과 napi_watchdog() napi_gro_flush(napi, true) → 오래된 것만 ③ MAX_GRO _SKBS 초과 새 패킷 도착 버킷 count > MAX_GRO_SKBS 가장 오래된 항목 즉시 flush napi_gro_complete() → netif_receive_skb() GRO flush 타이밍 3가지 경로 ① poll 완료 즉시(flush_old=false) ② gro_flush_timeout 타이머(flush_old=true) ③ MAX_GRO_SKBS 초과 패킷 수신 GRO 병합/보관 flush 트리거 flush 실행 상위 스택 전달

실용적 튜닝

GRO 설정과 gro_flush_timeout 튜닝:
# GRO 활성화/비활성화
ethtool -K eth0 gro on
ethtool -K eth0 gro off

# gro_flush_timeout 조회 (나노초 단위, 0=비활성화)
ip link show eth0

# gro_flush_timeout 설정 (예: 100μs = 100000ns)
ip link set eth0 gro_flush_timeout 100000

# napi_defer_hard_irqs와 함께 사용하는 경우
# (IRQ 지연으로 GRO 병합 효율 증가, 지연도 증가)
echo 50 > /sys/class/net/eth0/napi_defer_hard_irqs
ip link set eth0 gro_flush_timeout 500000
  • gro_flush_timeout=0 (기본값): 타이머 비활성화. poll 완료 시 즉시 flush. 낮은 지연에 적합합니다.
  • gro_flush_timeout > 0: 타이머 활성화. GRO 버퍼가 더 오래 누적되어 병합 효율 향상. 처리량 중심 워크로드에 적합합니다.

napi_consume_skb()와 SKB 배치 해제

패킷 처리가 완료된 후 소켓 버퍼(Socket Buffer, SKB)를 메모리 할당자에 반환하는 방식은 성능에 큰 영향을 미칩니다. NAPI 컨텍스트에서는 일반적인 개별 해제 대신 배치 해제(Bulk Free)를 활용하여 캐시 라인(Cache Line) 경합과 할당자 잠금(Lock) 오버헤드를 최소화합니다.

SKB 해제 API 비교

함수 사용 컨텍스트 배치 해제 트레이스포인트 용도
dev_kfree_skb_any() 인터럽트/프로세스 모두 없음 kfree_skb 드롭(오류) 경로
dev_consume_skb_any() 인터럽트/프로세스 모두 없음 consume_skb 정상 전달 완료 경로
napi_consume_skb() NAPI poll 컨텍스트 전용 있음 (per-CPU 캐시) consume_skb TX 완료 처리 (NAPI)

dev_kfree_skb_any()dev_consume_skb_any()의 차이는 트레이스포인트(Tracepoint)에 있습니다. kfree_skb는 패킷이 드롭되었음을 나타내고, consume_skb는 정상적으로 소비되었음을 나타냅니다. 이를 통해 dropwatch 같은 도구가 패킷 드롭을 감지합니다.

napi_consume_skb() 내부 구현

/* net/core/skbuff.c */
void napi_consume_skb(struct sk_buff *skb, int budget)
{
    /* budget == 0은 IRQ 컨텍스트에서의 호출을 의미합니다.
     * 이 경우 배치 해제를 사용하지 않고 일반 경로로 폴백(Fallback)합니다. */
    if (unlikely(!budget)) {
        dev_consume_skb_irq(skb);
        return;
    }

    lockdep_assert_in_softirq();

    if (!skb_unref(skb))
        return;

    /* skb가 확장 헤더나 공유 데이터를 가지면 즉시 해제 */
    if (likely(skb_is_recycleable(skb))) {
        struct napi_alloc_cache *nc;

        nc = this_cpu_ptr(&napi_alloc_cache);
        if (nc->skb_count < NAPI_SKB_CACHE_SIZE) {
            /* per-CPU SKB 캐시에 보관 (kmem_cache_free 호출 지연) */
            nc->skb_cache[nc->skb_count++] = skb;
            return;
        }
        /* 캐시 가득 참 → 절반을 bulk free로 한꺼번에 반환 */
        kmem_cache_free_bulk(skbuff_cache,
                             NAPI_SKB_CACHE_BULK,
                             nc->skb_cache);
        nc->skb_count -= NAPI_SKB_CACHE_BULK;
        memmove(nc->skb_cache,
                nc->skb_cache + NAPI_SKB_CACHE_BULK,
                nc->skb_count * sizeof(void *));
        nc->skb_cache[nc->skb_count++] = skb;
        return;
    }

    __kfree_skb(skb);
}
budget 파라미터의 의미: napi_consume_skb(skb, budget)에서 budget은 현재 NAPI poll의 버짓 값입니다. budget == 0이면 IRQ 컨텍스트(예: TX 완료 인터럽트 핸들러에서의 직접 호출)를 의미하며, 이 경우 per-CPU 캐시를 사용하지 않습니다. budget > 0이면 softirq 컨텍스트임이 보장되므로 per-CPU 접근이 안전하고 배치 해제를 활용합니다.

per-CPU SKB 캐시 구조와 bulk 할당/해제

/* include/net/sock.h — per-CPU SKB 캐시 구조 */
#define NAPI_SKB_CACHE_SIZE  64
#define NAPI_SKB_CACHE_BULK  16
#define NAPI_SKB_CACHE_HALF  (NAPI_SKB_CACHE_SIZE / 2)

struct napi_alloc_cache {
    struct page_frag_cache page;   /* 프래그먼트(Fragment) 페이지 캐시 */
    struct page_frag_cache page_small; /* 소형 패킷 전용 캐시 */
    unsigned int           skb_count; /* 현재 캐시에 보관된 skb 수 */
    void                  *skb_cache[NAPI_SKB_CACHE_SIZE]; /* skb 포인터 배열 */
};

/* bulk 할당 예: napi_alloc_skb() 내부 */
static inline struct sk_buff *napi_alloc_skb(struct napi_struct *napi,
                                               unsigned int length)
{
    unsigned int len = SKB_DATA_ALIGN(length + NET_SKB_PAD) +
                       SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
    return __napi_alloc_skb(napi, len, GFP_ATOMIC | __GFP_NOWARN);
}

SKB 할당/해제 경로 비교 다이어그램

일반 해제 경로 dev_kfree_skb_any() kfree_skb() / consume_skb() __kfree_skb() kmem_cache_free() skb 1개씩 반환 (슬랩 잠금) 슬랩 할당자 NAPI 배치 해제 경로 napi_consume_skb(skb, budget) napi_consume_skb() budget > 0 (softirq 컨텍스트) per-CPU napi_alloc_cache skb_cache[] 배열에 보관 캐시 가득 참? (count == NAPI_SKB_CACHE_SIZE) 아니오 캐시에 저장 (해제 지연) 예 (bulk) kmem_cache_free_bulk() NAPI_SKB_CACHE_BULK개 일괄 반환 슬랩 할당자 배치 해제: 슬랩 잠금 획득 횟수를 NAPI_SKB_CACHE_BULK분의 1로 감소

__kfree_skb_defer()와 softnet_data.completion_queue

/* net/core/skbuff.c — defer 경로: softirq 종료 시 일괄 처리 */
void __kfree_skb_defer(struct sk_buff *skb)
{
    skb_release_all(skb, SKB_DROP_REASON_NOT_SPECIFIED, 0);
    __skb_queue_tail(&__get_cpu_var(softnet_data).completion_queue, skb);
}

/* net_rx_action() 종료 직전에 completion_queue를 비운다 */
static void skb_defer_free_flush(struct softnet_data *sd)
{
    struct sk_buff *skb, *next;

    if (skb_queue_empty(&sd->completion_queue))
        return;

    spin_lock(&sd->completion_queue.lock);
    skb = __skb_dequeue_all(&sd->completion_queue);
    spin_unlock(&sd->completion_queue.lock);

    while (skb) {
        next = skb->next;
        prefetch(next);
        napi_skb_cache_put(skb);
        skb = next;
    }
}

TX 경로에서의 NAPI SKB 해제 패턴

TX 완료(Transmit Completion) 처리는 NAPI 폴링 루프 내에서 이루어질 수 있습니다. 드라이버가 TX 완료 큐를 함께 처리하는 경우 napi_consume_skb()를 사용하여 배치 해제의 이점을 활용합니다.

/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* TX 완료 처리 — NAPI poll 함수 내 예시 */
static int driver_poll(struct napi_struct *napi, int budget)
{
    int tx_done = 0, rx_done = 0;

    /* TX 완료 처리: budget 전달로 배치 해제 활성화 */
    while (tx_pending(&priv->tx_ring)) {
        struct sk_buff *skb = tx_ring_get_skb(&priv->tx_ring);
        napi_consume_skb(skb, budget);   /* budget > 0 → per-CPU 배치 해제 */
        tx_done++;
    }

    /* RX 처리: 최대 budget개까지 */
    rx_done = driver_rx_poll(napi, budget);

    if (rx_done < budget && !tx_pending(&priv->tx_ring))
        napi_complete_done(napi, rx_done);

    return rx_done;
}

page_pool recycle과 napi_consume_skb의 상호작용

page_pool을 사용하는 드라이버에서는 SKB 데이터 영역이 page_pool로 관리되는 페이지를 참조합니다. 이 경우 napi_consume_skb()가 skb 헤더 구조체를 per-CPU 캐시에 보관하는 동시에, skb 데이터 영역의 페이지는 page_pool_put_page()를 통해 별도의 per-NAPI 풀(Pool)로 재활용됩니다. 두 경로가 모두 NAPI 컨텍스트에서 실행되므로 잠금(Lock) 경합 없이 효율적인 재활용이 가능합니다.

NAPI와 TC(Traffic Control) ingress 연동

리눅스 트래픽 컨트롤(Traffic Control, TC)은 네트워크 패킷의 분류(Classification), 필터링(Filtering), 큐잉(Queuing)을 담당하는 커널 서브시스템입니다. NAPI 폴링 루프에서 수신된 패킷은 프로토콜 스택에 도달하기 전에 TC ingress 훅(Hook)을 통과하며, 이 지점에서 eBPF 프로그램, flower 필터, TC redirect 등 다양한 처리가 가능합니다.

TC ingress 훅 위치

TC ingress 훅은 __netif_receive_skb_core() 내부의 sch_handle_ingress() 호출로 구현됩니다. NAPI 폴링에서 패킷을 수신한 후 netif_receive_skb()__netif_receive_skb()__netif_receive_skb_core()sch_handle_ingress() 순서로 TC ingress 훅이 실행됩니다.

sch_handle_ingress() 내부와 tcf_classify() 호출

/* net/core/dev.c — TC ingress 훅 처리 핵심 경로 */
static inline struct sk_buff *
sch_handle_ingress(struct sk_buff *skb,
                   struct packet_type **pt_prev,
                   int *ret,
                   struct net_device *orig_dev,
                   bool *another)
{
    struct mini_Qdisc *miniq = rcu_dereference_bh(skb->dev->miniq_ingress);
    struct tcf_result cl_res;

    if (!miniq)
        return skb;  /* ingress qdisc 미설정 시 즉시 반환 */

    qdisc_skb_cb(skb)->pkt_len = skb->len;
    tc_skb_cb(skb)->mru = 0;
    tc_skb_cb(skb)->post_ct = false;

    switch (tcf_classify(skb, miniq->block, miniq->filter_list,
                         &cl_res, false)) {
    case TC_ACT_OK:
    case TC_ACT_RECLASSIFY:
        skb->tc_index = TC_H_MIN(cl_res.classid);
        break;
    case TC_ACT_SHOT:
        /* 패킷 드롭 */
        kfree_skb_reason(skb, SKB_DROP_REASON_TC_INGRESS);
        *ret = NET_RX_DROP;
        return NULL;
    case TC_ACT_STOLEN:
    case TC_ACT_QUEUED:
    case TC_ACT_TRAP:
        /* TC가 skb 소유권을 가져감 (redirect, mirror 등) */
        *ret = NET_RX_SUCCESS;
        return NULL;
    case TC_ACT_REDIRECT:
        /* 다른 디바이스로 리다이렉트(Redirect) */
        skb_do_redirect(skb);
        *ret = NET_RX_SUCCESS;
        return NULL;
    }

    return skb;  /* TC_ACT_OK: 정상 경로로 계속 진행 */
}

NAPI 수신 경로에서 TC ingress 훅 위치 다이어그램

NIC 하드웨어 수신 NAPI poll() — softirq 컨텍스트 napi_gro_receive() / XDP 실행 netif_receive_skb() __netif_receive_skb_core() TC ingress 훅 sch_handle_ingress() → tcf_classify() TC_ACT_SHOT 패킷 드롭 TC_ACT_REDIRECT 다른 디바이스로 전달 TC_ACT_OK cls_act 필터 체인 cls_bpf / cls_flower / cls_u32 프로토콜 스택 IP → TCP/UDP → 소켓 수신 버퍼 XDP vs TC ingress XDP: skb 없음, 드라이버 레벨 TC: skb 존재, 더 많은 기능 XDP > TC (성능), TC > XDP (기능) clsact로 ingress+egress 동시 처리 NAPI 수신 경로의 TC ingress 훅 위치 TC ingress는 netif_receive_skb() 내부에서 실행 — XDP보다 늦은 단계이지만 skb 기반으로 더 풍부한 기능 제공

cls_bpf와 XDP의 차이

구분 XDP TC cls_bpf (BPF_PROG_TYPE_SCHED_CLS)
실행 시점 NAPI poll 최상단 (드라이버 내부) __netif_receive_skb_core() 내부
skb 존재 여부 없음 (raw 패킷 포인터) 있음 (완전한 sk_buff)
헤더 조작 제한적 (포인터 산술) 풍부 (skb helper 함수 다수)
CT/NAT 접근 불가 가능 (bpf_skb_ct_lookup())
페이로드 수정 가능 (제한적) 가능 (bpf_skb_store_bytes())
성능 특성 최고 (skb 할당 없음) XDP보다 느리나 iptables보다 빠름
리다이렉트 bpf_redirect() bpf_redirect(), TC_ACT_REDIRECT

tc-flower 하드웨어 오프로드와 NAPI의 관계

tc-flower 필터는 NIC이 하드웨어 오프로드를 지원하는 경우 필터 규칙이 NIC 펌웨어(Firmware)로 프로그래밍됩니다. 이 경우 패킷은 NAPI poll 단계에서 이미 분류 결과 메타데이터가 skb->tc_index에 설정된 채로 전달되며, 소프트웨어 분류 단계를 건너뜁니다. 하드웨어 오프로드 규칙과 일치하지 않는 패킷만 소프트웨어 TC 경로를 거칩니다.

/* drivers/net/ethernet/xxx/xxx_main.c — 하드웨어 오프로드 TC 콜백 등록 예 */
static int driver_setup_tc(struct net_device *dev, enum tc_setup_type type,
                            void *type_data)
{
    switch (type) {
    case TC_SETUP_CLSFLOWER:
        return driver_flower_offload(dev, type_data);
    case TC_SETUP_BLOCK:
        return driver_setup_tc_block(dev, type_data);
    default:
        return -EOPNOTSUPP;
    }
}

/* net_device_ops에 ndo_setup_tc 콜백 등록 */
static const struct net_device_ops driver_netdev_ops = {
    .ndo_start_xmit  = driver_start_xmit,
    .ndo_setup_tc    = driver_setup_tc,
    /* ... */
};

TC redirect와 NAPI 컨텍스트의 관계

TC redirect(TC_ACT_REDIRECT)는 패킷을 다른 네트워크 디바이스로 전달합니다. 중요한 점은 이 리다이렉트가 NAPI softirq 컨텍스트에서 실행되므로, 목적지 디바이스로의 전송도 같은 CPU에서 즉시 처리됩니다. 이를 통해 컨텍스트 스위칭(Context Switching) 없이 패킷 포워딩(Forwarding)을 구현할 수 있습니다. skb_do_redirect()dev_queue_xmit()를 통해 목적지 디바이스의 TX 큐에 패킷을 직접 삽입합니다.

실용적 TC ingress 예제

# clsact qdisc 추가 (ingress + egress 동시 처리 지원)
tc qdisc add dev eth0 clsact

# eBPF 프로그램을 TC ingress에 연결
tc filter add dev eth0 ingress bpf da obj my_filter.o sec tc_ingress

# flower 필터: 특정 IP에서 오는 패킷을 eth1로 리다이렉트
tc filter add dev eth0 ingress \
    protocol ip flower \
    src_ip 10.0.0.1/32 \
    action mirred egress redirect dev eth1

# flower 필터 하드웨어 오프로드 활성화 여부 확인
tc filter show dev eth0 ingress

# cls_bpf: BPF 프로그램을 ingress에 직접 연결
tc filter add dev eth0 ingress \
    bpf direct-action \
    obj my_bpf.o \
    sec classifier

# 통계 확인 (TC 필터 히트 카운트)
tc -s filter show dev eth0 ingress

clsact qdisc의 장점

clsact vs 전통적 ingress qdisc:
  • clsact: ingress와 egress를 모두 단일 qdisc에서 처리합니다. tc qdisc add dev eth0 clsact 한 번으로 양방향 필터 설정이 가능합니다.
  • ingress qdisc (구버전): ingress만 처리합니다. egress TC 훅을 사용하려면 별도의 qdisk가 필요합니다.
  • BPF direct-action: bpf da 옵션으로 BPF 프로그램의 반환값이 직접 TC 액션으로 사용됩니다. 별도의 cls_act 액션 없이 BPF 내에서 TC_ACT_OK, TC_ACT_SHOT, TC_ACT_REDIRECT를 반환하여 패킷 처리 흐름을 제어합니다.
  • NAPI 컨텍스트 보장: TC ingress 처리는 항상 NAPI softirq 컨텍스트에서 실행되므로 per-CPU 자료구조 접근이 안전하며 선점 불가능합니다.

패킷 수신 전체 경로(End-to-End) 추적

패킷이 NIC(Network Interface Card)에 도착하여 사용자 공간의 read()/recv()까지 전달되는 전 과정을 단계별로 추적합니다. 각 단계에서 실행되는 커널 함수와 소스 파일 위치를 함께 확인하면 성능 병목 지점을 정확히 파악할 수 있습니다.

DMA 링 버퍼(Ring Buffer) 구조

현대 NIC는 호스트 메모리에 미리 할당된 DMA 링 버퍼를 통해 패킷을 수신합니다. 드라이버가 디스크립터(Descriptor) 배열을 물리 주소로 NIC에 등록해 두면, NIC는 패킷 도착 시 CPU 개입 없이 직접 해당 버퍼에 데이터를 기록합니다.

필드역할관리 주체
next_to_use (head) 드라이버가 다음으로 채울 디스크립터 위치 — 새 버퍼를 NIC에 제공할 때 사용합니다 드라이버 (RX 리필 시 갱신)
next_to_clean (tail) 드라이버가 다음으로 회수할 디스크립터 위치 — NIC가 완료 표시한 항목부터 처리합니다 드라이버 poll() 내부에서 갱신
DD(Descriptor Done) 비트 NIC가 1로 설정 시 해당 버퍼에 패킷 수신이 완료된 상태입니다 NIC 하드웨어가 DMA 완료 후 자동 설정
버퍼 주소 (DMA addr) 호스트 물리 주소 — NIC가 패킷을 직접 기록하는 위치입니다 드라이버가 dma_map_page()로 매핑 후 NIC에 등록
링 크기 일반적으로 256~4096개 디스크립터 (전원 2승)입니다 ethtool -G로 런타임 조정 가능
/* drivers/net/ethernet/ 기반 예제 — 간략화 */
/* 전형적인 RX 디스크립터 구조 (드라이버마다 다름) */
struct rx_desc {
    __le64  buffer_addr;   /* DMA 물리 주소 — NIC가 패킷 기록 위치 */
    __le16  length;        /* 수신된 패킷 바이트 수 (NIC가 채움) */
    __le16  vlan_tag;      /* VLAN 태그 (VLAN offload 시) */
    __u8    status;        /* bit0=DD(Done), bit1=EOP(End of Packet) */
    __u8    errors;        /* 수신 오류 비트 */
    __le16  reserved;
};

/* 드라이버 RX 링 관리 구조 */
struct rx_ring {
    struct rx_desc    *desc;          /* 디스크립터 배열 (DMA 할당) */
    struct sk_buff   **skb_buf;       /* 디스크립터 대응 skb 포인터 배열 */
    dma_addr_t         dma;           /* 디스크립터 배열의 DMA 주소 */
    u16                count;         /* 링 크기 (예: 256) */
    u16                next_to_clean; /* 다음 회수 위치 (tail) */
    u16                next_to_use;   /* 다음 리필 위치 (head) */
    struct napi_struct napi;          /* 이 큐의 NAPI 인스턴스 */
};

전체 수신 경로 단계별 설명

#단계커널 함수소스 파일컨텍스트
NIC DMA → 링 버퍼 기록 (하드웨어) drivers/net/ethernet/*/ 하드웨어
하드웨어 인터럽트 발생 드라이버 ISR (예: igb_msix_ring()) drivers/net/ethernet/intel/igb/igb_main.c 하드 IRQ
NAPI 스케줄 napi_schedule()__napi_schedule() net/core/dev.c 하드 IRQ
softirq 실행 net_rx_action() net/core/dev.c softirq
드라이버 poll() 호출 igb_poll() 등 드라이버별 구현 드라이버별 *_main.c softirq
XDP 경로 (설정 시) bpf_prog_run_xdp() include/linux/filter.h softirq
GRO 병합 napi_gro_receive()dev_gro_receive() net/core/gro.c softirq
GRO flush → 스택 전달 napi_gro_flush()napi_skb_finish() net/core/gro.c softirq
netif_receive_skb 진입 netif_receive_skb_list()__netif_receive_skb_core() net/core/dev.c softirq
ptype_all 전달 (tcpdump) deliver_skb(), ptype_all 리스트 순회 net/core/dev.c softirq
L3 처리 (IP) ip_rcv()ip_rcv_core() net/ipv4/ip_input.c softirq
L4 처리 (TCP) tcp_v4_rcv()tcp_rcv_established() net/ipv4/tcp_ipv4.c, net/ipv4/tcp_input.c softirq
소켓 수신 큐 삽입 __sk_receive_skb()sk_data_ready() net/core/sock.c softirq
사용자 공간 수신 tcp_recvmsg()skb_copy_datagram_iter() net/ipv4/tcp.c 프로세스 컨텍스트

전체 경로 SVG 다이어그램

하드웨어 레인 하드 IRQ 컨텍스트 softirq 컨텍스트 (NET_RX_SOFTIRQ) 프로세스 컨텍스트 ① NIC → DMA 링 버퍼 디스크립터 기록 ② 하드웨어 IRQ MSI-X 인터럽트 발생 ③ napi_schedule() IRQ 비활성화 + poll_list 등록 ④ net_rx_action() NET_RX_SOFTIRQ, 버짓=300 ⑤ 드라이버 poll() igb_poll() — 링 버퍼 순회 ⑥ XDP (선택) bpf_prog_run_xdp() DROP/TX/REDIRECT → 종료 XDP_PASS ↓ ⑦ napi_gro_receive() → dev_gro_receive() net/core/gro.c — gro_hash 버킷 병합 ⑧ napi_gro_flush() → napi_skb_finish() 병합 완료 skb를 스택으로 전달 ⑨ netif_receive_skb_list() → __netif_receive_skb_core() net/core/dev.c — RPS 분산, Generic XDP, TC ingress, ptype 디스패치 ⑩ ptype_all deliver_skb() — tcpdump ⑪ ip_rcv() → ip_rcv_core() net/ipv4/ip_input.c — Netfilter PREROUTING ⑫ ip_local_deliver() Netfilter INPUT → 프로토콜 핸들러 디스패치 ⑬ tcp_v4_rcv() net/ipv4/tcp_ipv4.c ⑬ udp_rcv() net/ipv4/udp.c ⑭ 소켓 수신 큐 (sk_receive_queue) ⑮ 사용자 공간 read() / recv() tcp_recvmsg() → skb_copy_datagram_iter() 하드웨어 하드 IRQ softirq 프로세스 XDP (선택) 패킷 수신 전체 경로: NIC DMA → 사용자 공간

콜체인(Call Chain) 의사코드 — NIC에서 소켓까지

/* ═══════════════════════════════════════════════════════════════════
 * 단계 1: NIC 하드웨어 인터럽트 → NAPI 스케줄
 * 파일: drivers/net/ethernet/intel/igb/igb_main.c
 * ═══════════════════════════════════════════════════════════════════ */
static irqreturn_t igb_msix_ring(int irq, void *data)
{
    struct igb_q_vector *q_vector = data;

    /* 인터럽트 마스킹: 동일 큐의 중복 IRQ 방지 */
    igb_write_itr(q_vector);

    /* NAPI 인스턴스를 softirq 큐에 등록 */
    napi_schedule(&q_vector->napi);
    return IRQ_HANDLED;
}

/* napi_schedule() 내부: include/linux/netdevice.h */
static inline void napi_schedule(struct napi_struct *n)
{
    if (napi_schedule_prep(n))   /* NAPI_STATE_SCHED 비트 설정 */
        __napi_schedule(n);      /* per-CPU poll_list에 추가 */
}

/* ═══════════════════════════════════════════════════════════════════
 * 단계 2: softirq → net_rx_action() → 드라이버 poll()
 * 파일: net/core/dev.c
 * ═══════════════════════════════════════════════════════════════════ */
static void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    unsigned long time_limit = jiffies + usecs_to_jiffies(netdev_budget_usecs);
    int budget = netdev_budget;  /* 기본 300 */

    for (;;) {
        struct napi_struct *n;
        if (list_empty(&sd->poll_list))
            break;
        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
        budget -= napi_poll(n, &repoll);  /* 드라이버 poll() 호출 */
        if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit))) {
            sd->time_squeeze++;
            break;
        }
    }
}

/* ═══════════════════════════════════════════════════════════════════
 * 단계 3: 드라이버 poll() → XDP → GRO → napi_gro_receive()
 * 파일: drivers/net/ethernet/intel/igb/igb_main.c
 * ═══════════════════════════════════════════════════════════════════ */
static void igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget)
{
    while (work_done < budget) {
        rx_desc = IGB_RX_DESC(rx_ring, rx_ring->next_to_clean);

        /* DD 비트 미설정: NIC가 아직 수신을 완료하지 않음 */
        if (!igb_test_staterr(rx_desc, E1000_RXD_STAT_DD))
            break;

        /* XDP 처리: skb 할당 이전에 실행 (zero-copy 경로) */
        if (ring->xdp_prog) {
            struct xdp_buff xdp;
            xdp_prepare_buff(&xdp, ...);
            u32 act = bpf_prog_run_xdp(ring->xdp_prog, &xdp);
            switch (act) {
            case XDP_PASS:   break;           /* 상위 스택으로 계속 */
            case XDP_DROP:   goto next_desc;  /* 패킷 폐기 */
            case XDP_TX:     ...               /* 동일 NIC로 재전송 */
            case XDP_REDIRECT: ...             /* 다른 인터페이스로 전달 */
            }
        }

        /* skb 구성 후 GRO로 전달 */
        skb = igb_construct_skb(rx_ring, rx_desc, &xdp, ...);
        napi_gro_receive(&q_vector->napi, skb);  /* net/core/gro.c */

        rx_ring->next_to_clean = (cleaned_count + 1) % rx_ring->count;
        work_done++;
    }
}

/* ═══════════════════════════════════════════════════════════════════
 * 단계 4: netif_receive_skb → __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 packet_type *ptype, *pt_prev = NULL;

    /* 1. ptype_all 리스트 순회: tcpdump/AF_PACKET 프로미스큐어스 수신 */
    list_for_each_entry_rcu(ptype, &ptype_all, list) {
        if (!ptype->dev || ptype->dev == skb->dev)
            deliver_skb(skb, ptype, skb->dev);
    }

    /* 2. TC ingress: eBPF 클래스파이어, 네트워크 가속 */
    if (static_branch_unlikely(&ingress_needed_key))
        skb = sch_handle_ingress(skb, &pt_prev, &ret, ...);

    /* 3. 프로토콜 타입별 핸들러 조회 (ETH_P_IP → ip_rcv) */
    list_for_each_entry_rcu(ptype,
            &ptype_base[ntohs(skb->protocol) & PTYPE_HASH_MASK], list) {
        if (ptype->type == skb->protocol)
            pt_prev = ptype;
    }

    if (pt_prev)
        deliver_skb(skb, pt_prev, orig_dev);  /* ip_rcv() 호출 */

    return ret;
}

/* ═══════════════════════════════════════════════════════════════════
 * 단계 5: tcp_v4_rcv → 소켓 큐 삽입
 * 파일: net/ipv4/tcp_ipv4.c
 * ═══════════════════════════════════════════════════════════════════ */
int tcp_v4_rcv(struct sk_buff *skb)
{
    struct sock *sk;

    /* NAPI ID를 소켓에 연결 (busy polling 경로 식별용) */
    sk_mark_napi_id(sk, skb);

    if (sk->sk_state == TCP_ESTABLISHED)
        tcp_rcv_established(sk, skb);    /* 빠른 경로 */
    else
        tcp_rcv_state_process(sk, skb);  /* 느린 경로 (상태 전이) */
}

/* 소켓 수신 큐에 skb 추가 후 대기 중인 프로세스 깨우기 */
static inline void sock_def_readable(struct sock *sk)
{
    wake_up_interruptible_all(&sk->sk_wq->wait);
    /* → 사용자 공간의 read()/recv()/epoll_wait() 반환 */
}

netif_receive_skb_list()와 배치 처리 최적화

리눅스 5.x 이후 GRO flush 경로는 단일 skb 전달 대신 netif_receive_skb_list()를 통해 skb 리스트를 배치로 상위 스택에 전달합니다. 이를 통해 __netif_receive_skb_core() 진입 횟수를 줄이고 캐시 지역성(Cache Locality)을 향상시킵니다.

/* net/core/dev.c — 배치 전달 내부 구현 */
static void __netif_receive_skb_list_core(struct list_head *head, bool pfmemalloc)
{
    struct sk_buff *skb, *next;

    list_for_each_entry_safe(skb, next, head, list) {
        struct packet_type *pt_prev = NULL;
        list_del_init(&skb->list);

        __netif_receive_skb_core(&skb, pfmemalloc, &pt_prev);
        if (pt_prev) {
            if (pt_prev->list_func != NULL)
                /* ip_list_rcv() 등 배치 핸들러 사용
                   → 동일 프로토콜 패킷을 리스트로 묶어 1회 호출 */
                INDIRECT_CALL_INET(pt_prev->list_func,
                                   ipv6_list_rcv, ip_list_rcv,
                                   head, pt_prev, orig_dev);
            else
                deliver_skb(skb, pt_prev, orig_dev);
        }
    }
}
배치 처리 최적화 효과 상세
  • ip_list_rcv(): IP 패킷을 리스트 단위로 처리하여 헤더 파싱과 라우팅 조회 결과를 연속 패킷에 재활용합니다 (net/ipv4/ip_input.c).
  • 소켓 락 최소화: 동일 소켓으로 향하는 연속 패킷을 소켓 락(Socket Lock) 한 번으로 처리합니다.
  • 캐시 히트율 향상: 동일 skb/소켓 구조체가 L1/L2 캐시에 남아 있는 상태에서 연속 처리하므로 캐시 미스가 감소합니다.
  • ptype 탐색 최소화: 개별 skb마다 ptype_base를 탐색하지 않고 프로토콜별로 그룹화하여 일괄 처리합니다.

NAPI와 ksoftirqd/softirq 실행 정책

NAPI의 폴링 경로는 소프트IRQ(softirq) 컨텍스트에서 실행됩니다. 소프트IRQ가 언제, 어떤 컨텍스트에서 실행되는지 이해하는 것은 NAPI 레이턴시 분석과 튜닝에 필수적입니다.

softirq 실행의 3가지 경로

경로트리거 지점함수실행 조건
① irq_exit() 직후 하드 IRQ 핸들러 반환 직후 irq_exit_rcu()invoke_softirq()__do_softirq() pending 비트가 설정된 softirq가 존재할 때 즉시 실행됩니다
② local_bh_enable() 시점 BH(Bottom Half) 재활성화 시 local_bh_enable()do_softirq() BH 비활성 구간에서 pending된 softirq를 재활성화 시점에 처리합니다
③ ksoftirqd 커널 스레드 __do_softirq() 제한 초과 또는 명시적 깨우기 wakeup_softirqd()ksoftirqd/N MAX_SOFTIRQ_RESTART(10) 초과 또는 2ms 시간 제한 초과 시 위임합니다

__do_softirq() 내부: MAX_SOFTIRQ_RESTART(10) 제한과 ksoftirqd 전환

/* kernel/softirq.c — __do_softirq() 핵심 루프 (Linux 6.x) */
#define MAX_SOFTIRQ_TIME    msecs_to_jiffies(2)  /* 2ms 시간 제한 */
#define MAX_SOFTIRQ_RESTART 10                    /* 최대 재시작 횟수 */

void __do_softirq(void)
{
    unsigned long  end        = jiffies + MAX_SOFTIRQ_TIME;
    int            max_restart = MAX_SOFTIRQ_RESTART;
    __u32          pending;
    int            softirq_bit;

    /* 현재 pending된 softirq 비트맵 스냅샷 */
    pending = local_softirq_pending();
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);

restart:
    /* pending 비트 클리어 후 로컬 변수에 보관
       실행 중 새로 발생한 softirq는 다음 반복에서 처리됩니다 */
    set_softirq_pending(0);
    local_irq_enable();  /* softirq 실행 중에는 IRQ를 허용합니다 */

    h = softirq_vec;

    while ((softirq_bit = ffs(pending))) {
        h += softirq_bit - 1;
        trace_softirq_entry(vec_nr);
        h->action(h);   /* NET_RX_SOFTIRQ → net_rx_action() */
        trace_softirq_exit(vec_nr);
        h++;
        pending >>= softirq_bit;
    }

    local_irq_disable();

    /* 실행 중 새로 발생한 pending softirq를 확인합니다 */
    pending = local_softirq_pending();
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;  /* 최대 10회까지 재시작합니다 */

        /* 조건 불충족 시 ksoftirqd에게 처리를 위임합니다:
           1) max_restart 소진 (10회 반복)
           2) 2ms 시간 제한(MAX_SOFTIRQ_TIME) 초과
           3) need_resched() — 더 높은 우선순위 태스크 존재 */
        wakeup_softirqd();
    }

    __local_bh_enable(SOFTIRQ_OFFSET, preempt_count);
}

ksoftirqd가 깨어나는 조건과 NAPI 레이턴시 영향

ksoftirqd__do_softirq()가 제한에 걸렸을 때 깨어나 남은 softirq 처리를 이어받습니다. NAPI 관점에서 이것은 폴링이 인터럽트 컨텍스트 직후가 아닌 스케줄러(Scheduler)의 선택에 따라 지연될 수 있음을 의미합니다.

상황NAPI 레이턴시해결 방법
irq_exit() 직후 즉시 실행 최소 (약 10~50 µs) 기본 경로, 별도 튜닝 불필요합니다
ksoftirqd에 위임 (부하 낮음) 스케줄러 지연 추가 (약 100~500 µs) ksoftirqd nice 값 낮추기 또는 스레드 NAPI를 사용합니다
ksoftirqd에 위임 (CPU 포화) ms 단위 지연 가능 스레드 NAPI + RT 우선순위 또는 busy polling을 사용합니다
PREEMPT_RT 커널 ksoftirqd가 RT 스레드로 동작, 예측 가능 RT 우선순위 설정으로 최악 지연 보장이 가능합니다

softirq 실행 결정 흐름도

하드 IRQ 핸들러 반환 (irq_exit) pending softirq? No 종료 Yes __do_softirq() NET_RX_SOFTIRQ → net_rx_action() 실행 새 pending && restart < 10 && <2ms && !need_resched() Yes (goto restart) No wakeup_softirqd() ksoftirqd/N 커널 스레드 깨우기 ksoftirqd/N 스케줄링 run_ksoftirqd() → __do_softirq() 반복 NAPI poll() 실행: net_rx_action() → 드라이버 poll() softirq 또는 ksoftirqd 어느 쪽에서든 동일하게 실행됩니다 local_bh_enable() BH 재활성화 경로 → do_softirq() softirq 실행 결정 흐름: irq_exit → __do_softirq → ksoftirqd

PREEMPT_RT에서의 softirq 처리 변화

CONFIG_PREEMPT_RT가 활성화된 실시간 커널에서는 모든 softirq가 자동으로 커널 스레드로 전환됩니다. 이로 인해 NAPI 동작 방식이 근본적으로 달라집니다.

항목일반 커널PREEMPT_RT 커널
softirq 실행 방식 irq_exit() 직후 인라인 실행 또는 ksoftirqd 항상 ksoftirqd 스레드에서 실행됩니다
선점 가능 여부 softirq 컨텍스트는 선점 불가 스레드화되어 선점 가능합니다
우선순위 제어 불가 (softirq 고정 우선순위) chrt로 ksoftirqd RT 우선순위 설정 가능합니다
NAPI 스레드 NAPI 필요성 선택 사항 ksoftirqd가 이미 스레드화되므로 스레드 NAPI와 유사하게 동작합니다
레이턴시 예측 가능성 낮음 (인라인/ksoftirqd 혼합) 높음 (스레드 우선순위로 제어 가능합니다)

실용적 튜닝: /proc/softirqs 모니터링

# softirq 실행 횟수 확인 (CPU별, 타입별)
cat /proc/softirqs
# 출력 예:
#                 CPU0       CPU1       CPU2       CPU3
#       HI:          0          0          0          0
#    TIMER:   1234567    987654   1123456    876543
#   NET_TX:     12345      9876     11234      8765
#   NET_RX:   5678901   4321098   5123456   4098765  ← NAPI 경로
#    BLOCK:    234567    198765    212345    176543
#    TASKLET:   34567     27654     31234     25432

# /proc/net/softnet_stat으로 NAPI 통계 확인
# 컬럼: total processed, dropped, time_squeeze, 0, 0, 0, 0, 0, 0, cpu_collision, received_rps, flow_limit_count
cat /proc/net/softnet_stat

# time_squeeze 증가율 모니터링 (값이 지속 증가하면 budget 부족)
watch -n 1 'awk "{print \$3}" /proc/net/softnet_stat | paste -sd+ | bc'

# ksoftirqd CPU 사용률 확인
pidstat -p $(pgrep -d, ksoftirqd) 1

# perf로 softirq 실행 시간 측정
perf stat -e softirq:softirq_entry,softirq:softirq_exit -a sleep 5

# ksoftirqd 우선순위 조정 (NAPI 레이턴시 개선)
for pid in $(pgrep ksoftirqd); do
    chrt -f -p 20 $pid
done
softirq 지연 진단 순서: (1) /proc/net/softnet_stattime_squeeze 증가 여부 확인 → (2) perf top으로 net_rx_action CPU 점유율 확인 → (3) ksoftirqd CPU 사용률 확인 → (4) 스레드 NAPI 활성화 또는 netdev_budget 증가 검토 순으로 접근합니다.

napi_threaded_poll() 내부 구현 상세

스레드 NAPI는 NAPI 폴링을 softirq 컨텍스트가 아닌 전용 커널 스레드(Kernel Thread)에서 실행하는 방식입니다. Linux 5.10에서 도입되었으며, 특히 PREEMPT_RT 환경과 실시간 네트워킹이 필요한 환경에서 softirq의 비결정적 실행 타이밍 문제를 해결합니다.

스레드 생성: napi_kthread_create() 내부

스레드 NAPI가 활성화되면 커널은 각 napi_struct마다 전용 커널 스레드를 생성합니다. 스레드 이름 규칙은 "napi/<장치명>-<큐번호>"이며, 예를 들어 eth0의 첫 번째 큐는 "napi/eth0-0"으로 생성됩니다.

/* net/core/dev.c — napi_kthread_create() */
static int napi_kthread_create(struct napi_struct *n)
{
    int err = 0;

    /* 스레드 이름: "napi/%s-%d" (장치명-큐인덱스)
       예: "napi/eth0-0", "napi/eth0-1", "napi/ens3f0-2" */
    struct task_struct *t = kthread_create(napi_threaded_poll,
                                            n,            /* 스레드 인자 */
                                            "napi/%s-%d",
                                            n->dev->name,
                                            n->dev->dev_id);

    if (IS_ERR(t)) {
        err = PTR_ERR(t);
        pr_err("kthread_create failed for napi %s: err=%d\n",
               n->dev->name, err);
        return err;
    }

    /* 초기 스케줄링 정책: SCHED_OTHER (nice=0)
       사용자가 chrt/nice로 사후 변경 가능합니다 */
    set_user_nice(t, 0);

    /* napi_struct에 스레드 포인터 저장 */
    n->thread = t;

    /* 스레드를 준비 상태로 전환 (아직 깨우지 않음) */
    get_task_struct(t);
    wake_up_process(t);

    return 0;
}

/* napi_set_threaded(): 개별 NAPI 인스턴스에 스레드 NAPI 활성화 */
int napi_set_threaded(struct napi_struct *napi, bool threaded)
{
    if (threaded) {
        /* 스레드가 없는 경우에만 생성합니다 */
        if (!napi->thread)
            return napi_kthread_create(napi);

        set_bit(NAPI_STATE_THREADED, &napi->state);
    } else {
        clear_bit(NAPI_STATE_THREADED, &napi->state);
    }
    return 0;
}

/* dev_set_threaded(): 전체 net_device의 모든 NAPI에 일괄 활성화 */
void dev_set_threaded(struct net_device *dev, bool threaded)
{
    struct napi_struct *napi;

    if (dev->threaded == threaded)
        return;

    dev->threaded = threaded;

    /* dev->napi_list를 순회하며 각 NAPI에 적용합니다 */
    list_for_each_entry(napi, &dev->napi_list, dev_list)
        napi_set_threaded(napi, threaded);
}

napi_threaded_poll() 전체 루프 구현 상세

/* net/core/dev.c — napi_threaded_poll() 전체 구현 (Linux 6.x) */
static int napi_threaded_poll(void *data)
{
    struct napi_struct *napi = data;
    struct net_device  *dev  = napi->dev;
    void               *have;

    /* 메인 루프: kthread_stop()이 호출될 때까지 반복합니다 */
    while (!kthread_should_stop()) {

        /* ── 대기 루프: NAPI_STATE_SCHED_THREADED 비트가 설정될 때까지 ── */
        do {
            /* TASK_INTERRUPTIBLE 상태로 진입: CPU를 반납합니다 */
            set_current_state(TASK_INTERRUPTIBLE);

            if (kthread_should_stop())
                break;

            /* napi_schedule_prep(): NAPI_STATE_SCHED_THREADED 비트 확인
               __napi_schedule_irqoff()가 이 비트를 설정합니다 */
            if (napi_schedule_prep(napi)) {
                __set_current_state(TASK_RUNNING);
                break;
            }

            /* 조건 미충족: CPU를 반납하고 wake_up_process()를 기다립니다 */
            schedule();
        } while (1);

        if (kthread_should_stop())
            break;

        /* ── 폴링 실행 구간 ── */

        /* local_bh_disable(): softirq와의 동시 실행을 방지합니다
           BH를 비활성화하더라도 NET_RX_SOFTIRQ는 이 스레드가 실행하므로
           실제로는 softirq 핸들러 중복 진입 방지 목적입니다 */
        local_bh_disable();
        have = netpoll_poll_lock(napi);

        /* NAPI_STATE_SCHED_THREADED 비트가 설정된 경우에만 poll 실행 */
        if (test_bit(NAPI_STATE_SCHED_THREADED, &napi->state))
            napi_poll(napi, NULL);
        /* napi_poll() 내부에서 드라이버 poll() 함수를 직접 호출합니다
           softirq 경로의 net_rx_action()과 동일한 napi_poll()을 사용합니다 */

        netpoll_poll_unlock(have);
        local_bh_enable();

        /* 루프 재시작: 다시 대기 상태로 돌아갑니다 */
    }

    __set_current_state(TASK_RUNNING);
    return 0;
}

/* NAPI_STATE_SCHED_THREADED 비트 설명:
   - napi_schedule()이 스레드 NAPI 모드에서 설정합니다
   - napi_threaded_poll()의 대기 루프 탈출 조건입니다
   - napi_complete()에서 클리어됩니다 */

dev_set_threaded() vs napi_set_threaded() 차이

함수적용 범위사용 시점내부 동작
dev_set_threaded(dev, true) net_device 전체의 모든 NAPI 인스턴스 디바이스 레벨 일괄 설정 (sysfs /sys/class/net/eth0/threaded) dev->napi_list를 순회하며 각 napi에 napi_set_threaded() 호출
napi_set_threaded(napi, true) 개별 napi_struct 하나 드라이버 probe() 또는 큐별 세밀한 제어 스레드 없으면 napi_kthread_create(), 있으면 NAPI_STATE_THREADED 비트 설정

softirq NAPI vs 스레드 NAPI 성능 비교

항목softirq NAPI (기본)스레드 NAPI
처리량(Throughput) 높음 — softirq 인라인 실행으로 컨텍스트 전환 없음 약간 낮음 — 스레드 깨우기/잠들기 오버헤드 존재
레이턴시(Latency) 낮지만 비결정적 (ksoftirqd 위임 시 지연) RT 우선순위 설정 시 더 예측 가능한 레이턴시 달성 가능
CPU 격리 제어 IRQ affinity + RPS 조합 필요 taskset/cpuset으로 스레드를 특정 CPU에 직접 고정 가능
cgroup CPU 제어 불가 (softirq는 cgroup 영향 받지 않음) 가능 — 스레드가 cgroup에 속하므로 CPU 할당 제어 가능
PREEMPT_RT 호환성 레이턴시 스파이크 발생 가능 RT 환경에서 권장 — 선점 가능하고 우선순위 제어 가능
디버깅 용이성 낮음 (softirq 컨텍스트는 추적 어려움) 높음 (ps로 스레드 확인, perf로 프로파일링 용이)
사용 권장 환경 일반 서버, 고처리량 우선 환경 실시간 응용, 저지연 우선, PREEMPT_RT 환경

스레드 NAPI 상태 전이 SVG 다이어그램

대기 상태 TASK_INTERRUPTIBLE schedule() — CPU 반납 스케줄 준비 확인 napi_schedule_prep() NAPI_STATE_SCHED_THREADED? 폴링 실행 TASK_RUNNING local_bh_disable() napi_poll() → 드라이버 poll() NAPI 완료 napi_complete_done() SCHED_THREADED 비트 클리어 스레드 종료 kthread_should_stop() → 반환 wake_up_process() IRQ → napi_schedule() SCHED_THREADED 비트 설정 Yes TASK_RUNNING No → schedule() 패킷 소진 대기 재진입 kthread_stop() NAPI 상태 비트 (관련) NAPI_STATE_SCHED_THREADED 스레드 NAPI 스케줄 대기 표시 비트 NAPI_STATE_THREADED 스레드 NAPI 모드 활성화 표시 비트 스레드 NAPI 상태 전이: napi_threaded_poll() 내부 흐름

cgroup CPU 제어와 스레드 NAPI 결합

스레드 NAPI의 가장 강력한 장점은 일반 커널 스레드이므로 cgroup(Control Group)의 CPU 할당 제어를 그대로 받을 수 있는 점입니다. softirq는 cgroup의 영향을 받지 않으므로 기존 NAPI에서는 불가능한 기능입니다.

# 스레드 NAPI 활성화
echo 1 > /sys/class/net/eth0/threaded

# 스레드 이름 확인 (napi/eth0-N 형식)
ps -eo pid,comm | grep "napi/eth0"
# 출력 예:
# 1234 napi/eth0-0
# 1235 napi/eth0-1
# 1236 napi/eth0-2
# 1237 napi/eth0-3

# 스레드 NAPI를 RT 우선순위로 실행 (저지연 환경)
for pid in $(pgrep "napi/eth0"); do
    chrt -f -p 50 $pid
done

# cgroup cpuset으로 스레드 NAPI를 전용 CPU에 격리
mkdir -p /sys/fs/cgroup/cpuset/napi-isolated
echo "8-11" > /sys/fs/cgroup/cpuset/napi-isolated/cpuset.cpus
echo "0" >    /sys/fs/cgroup/cpuset/napi-isolated/cpuset.mems
for pid in $(pgrep "napi/eth0"); do
    echo $pid > /sys/fs/cgroup/cpuset/napi-isolated/tasks
done

# CPU 할당량 제한 (다른 태스크와 CPU 경쟁 방지)
mkdir -p /sys/fs/cgroup/cpu/napi-limited
echo 200000 > /sys/fs/cgroup/cpu/napi-limited/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/napi-limited/cpu.cfs_period_us
for pid in $(pgrep "napi/eth0"); do
    echo $pid > /sys/fs/cgroup/cpu/napi-limited/tasks
done

# taskset으로 개별 큐 스레드를 특정 CPU에 고정
taskset -p 0x100 $(pgrep "napi/eth0-0")  # CPU 8
taskset -p 0x200 $(pgrep "napi/eth0-1")  # CPU 9

PREEMPT_RT 환경에서 스레드 NAPI의 필수성

CONFIG_PREEMPT_RT 커널에서는 softirq가 이미 스레드화되어 있지만, 명시적 스레드 NAPI를 사용하면 우선순위와 CPU 친화성(Affinity)을 더 세밀하게 제어할 수 있어 실시간 응용에서 선호됩니다.

환경NAPI 실행 위치우선순위 제어선점 가능권장 설정
일반 커널, 기본 NAPI softirq (irq_exit 또는 ksoftirqd) 불가 불가 (softirq 구간) 일반 서버, 고처리량 우선
일반 커널, 스레드 NAPI napi/<if>-N 커널 스레드 가능 (chrt, nice) 가능 저지연 서버, NIC별 CPU 고정
PREEMPT_RT, 기본 NAPI ksoftirqd/N (RT 스레드화) 가능 (chrt) 가능 RT 커널 기본 구성
PREEMPT_RT, 스레드 NAPI napi/<if>-N 커널 스레드 가능 (세밀한 제어) 가능 RT 커널 + 고정밀 저지연 필수
스레드 NAPI 도입 체크리스트:
  • echo 1 > /sys/class/net/eth0/threaded으로 활성화 후 ps aux | grep napi/eth0으로 스레드 생성을 확인합니다.
  • perf stat -e context-switches -p $(pgrep napi/eth0-0) sleep 5로 컨텍스트 전환 횟수를 측정하여 오버헤드를 평가합니다.
  • 처리량보다 레이턴시가 중요한 경우 chrt -f -p 50으로 RT 우선순위를 설정합니다.
  • cgroup으로 CPU를 제한할 때는 스레드 NAPI가 패킷을 처리하기에 충분한 CPU 할당량을 확보합니다.

AF_XDP zero-copy와 NAPI 통합

AF_XDP(Address Family XDP)는 커널 네트워크 스택을 우회하여 패킷을 유저스페이스(Userspace)에서 직접 처리하는 소켓(Socket) 인터페이스입니다. NAPI poll 루프와 긴밀하게 통합되어 XDP 프로그램이 패킷을 XDP_REDIRECT로 AF_XDP 소켓에 전달할 수 있습니다. zero-copy 모드에서는 DMA 영역을 유저스페이스와 직접 공유하여 메모리 복사 없이 패킷을 처리하므로 DPDK(Data Plane Development Kit)에 준하는 성능을 커널 내 XDP 인프라와 함께 활용할 수 있습니다.

UMEM과 링(Ring) 구조

AF_XDP의 핵심 자료구조는 UMEM(Userspace Memory)입니다. UMEM은 유저스페이스가 mmap()으로 커널에 등록한 메모리 영역으로, 고정 크기의 청크(chunk)로 분할됩니다. NAPI poll과의 연동은 4개의 링으로 이루어집니다.

링 이름방향생산자소비자역할
Fill Ring유저→커널유저스페이스커널(드라이버)수신 버퍼로 사용할 UMEM 청크 주소를 커널에 공급
Completion Ring커널→유저커널(드라이버)유저스페이스송신 완료된 UMEM 청크 주소를 유저스페이스에 반환
RX Ring커널→유저커널(드라이버)유저스페이스수신된 패킷의 UMEM 청크 주소와 길이 전달
TX Ring유저→커널유저스페이스커널(드라이버)송신할 패킷의 UMEM 청크 주소와 길이 전달
AF_XDP UMEM Ring과 NAPI poll 연동 구조 유저스페이스 (Userspace) 커널 (NAPI poll 경로) UMEM (DMA-mapped 메모리 청크) 고정 크기 청크 배열 — mmap()으로 공유 Fill Ring 빈 청크 → 커널 공급 Completion Ring TX 완료 청크 반환 RX Ring 수신 패킷 청크 수신 TX Ring 송신할 청크 전달 xdpsock 애플리케이션 poll() → rx_ring 소비 → tx_ring 생산 NIC 하드웨어 DMA Fill Ring에서 받은 청크 주소로 직접 DMA NAPI poll() — XDP 경로 xsk_buff_alloc() → bpf_prog_run_xdp() XDP_REDIRECT → xsk_rcv() xsk_buff_pool UMEM 청크 관리 xsk_socket napi_id 바인딩 XDP 프로그램 (BPF) bpf_redirect_map(xsks_map, …) 빈 청크 공급 xsk_rcv() → rx_ring

NAPI poll()에서 AF_XDP zero-copy 경로

zero-copy 모드의 핵심은 xsk_buff_alloc()이 Fill Ring에서 유저스페이스가 제공한 UMEM 청크 주소를 직접 DMA 버퍼로 사용하는 점입니다. 패킷이 도착하면 NIC가 해당 주소에 직접 DMA 전송을 수행하고, NAPI poll이 xsk_rcv()를 통해 RX Ring에 완료 정보를 기록합니다.

/* 커널 소스 분석: net/xdp/xsk.c — xsk_rcv() 내부 동작 */

/* zero-copy 경로: XDP_REDIRECT → xsk_rcv_zc() */
static int xsk_rcv_zc(struct xdp_sock *xs,
                     struct xdp_buff *xdp,
                     u32 len)
{
    struct xdp_desc desc;
    dma_addr_t dma;

    /* UMEM 청크에서 DMA 주소를 오프셋으로 변환 */
    dma = xsk_buff_xdp_get_dma(xdp);
    desc.addr = xsk_umem_get_rx_desc_addr(xs->umem, dma);
    desc.len  = len;
    desc.options = 0;

    /* RX Ring에 기록: 유저스페이스가 소비 */
    if (xskq_prod_reserve_desc(xs->rx, &desc)) {
        xs->rx_dropped++;
        return -ENOBUFS;
    }

    xskq_prod_submit(xs->rx);
    return 0;
}

/* copy 경로: 기존 skb를 UMEM에 복사 */
static int xsk_rcv_skb(struct xdp_sock *xs,
                      struct sk_buff *skb)
{
    u32 len = skb_headlen(skb);
    void *buffer;
    dma_addr_t dma;
    struct xdp_desc desc;

    /* Fill Ring에서 빈 UMEM 청크 가져오기 */
    if (xskq_cons_peek_addr_unchecked(xs->umem->fq,
                                       &desc.addr) == 0)
        return -ENOBUFS;

    buffer = xdp_umem_get_data(xs->umem, desc.addr);

    /* copy 모드: skb 데이터를 UMEM 청크에 복사 */
    memcpy(buffer, skb->data, len);
    desc.len = len;

    xskq_cons_release(xs->umem->fq);
    xskq_prod_reserve_desc(xs->rx, &desc);
    xskq_prod_submit(xs->rx);
    return 0;
}

xsk_buff_pool과 page_pool 통합

커널 5.13 이후 xsk_buff_poolpage_pool과 통합되어 메모리 재활용 경로를 공유합니다. zero-copy 모드에서 xsk_buff_pool은 NIC 드라이버의 DMA 매핑을 직접 관리하며, page_pool처럼 per-NAPI 인스턴스로 설정하여 NUMA(Non-Uniform Memory Access) 로컬 접근을 보장합니다.

/* 커널 소스 분석: net/xdp/xsk_buff_pool.c — 풀 생성 */

struct xsk_buff_pool *
xp_create_and_assign_umem(struct xdp_sock *xs,
                           struct xdp_umem *umem)
{
    struct xsk_buff_pool *pool;
    u32 chunks;

    chunks = div_u64(umem->size, umem->chunk_size);

    pool = kvzalloc(struct_size(pool, heads, chunks),
                    GFP_KERNEL);

    pool->umem   = umem;
    pool->chunks = chunks;
    pool->napi   = NULL; /* napi_id는 소켓 바인딩 시 설정 */

    /* DMA 매핑: NIC ↔ UMEM 청크 주소 변환 테이블 */
    xp_init_dma_info(pool, ...);

    return pool;
}

/* xsk_socket과 NAPI 인스턴스 바인딩 */
int xp_assign_dev(struct xsk_buff_pool *pool,
                   struct net_device *netdev,
                   u16 queue_id, u16 flags)
{
    pool->queue_id = queue_id;
    pool->netdev   = netdev;

    if (flags & XDP_ZEROCOPY) {
        /* 드라이버의 ndo_bpf() 콜백으로 zero-copy 활성화 */
        return xp_assign_dev_shared(pool, netdev,
                                       queue_id);
    }
    return 0;
}

xsk_socket과 napi_id 바인딩

SO_INCOMING_NAPI_ID 소켓 옵션은 마지막으로 수신한 패킷을 처리한 NAPI 인스턴스의 ID를 반환합니다. 이를 활용하면 애플리케이션이 패킷을 처리한 NAPI와 동일한 CPU에서 busy polling을 수행할 수 있습니다. XDP_USE_NEED_WAKEUP 플래그를 사용하면 AF_XDP 소켓이 NAPI에게 wake-up 신호를 명시적으로 요청하여 불필요한 busy loop를 줄입니다.

/* 유저스페이스: SO_INCOMING_NAPI_ID로 napi_id 획득 */

int napi_id;
socklen_t optlen = sizeof(napi_id);

/* 패킷 수신 후 처리한 NAPI ID 조회 */
getsockopt(xsk_fd, SOL_SOCKET,
           SO_INCOMING_NAPI_ID,
           &napi_id, &optlen);

/* XDP_USE_NEED_WAKEUP: 드라이버가 필요할 때만 wake-up 발생 */
__u32 flags = XDP_USE_NEED_WAKEUP;
setsockopt(xsk_fd, SOL_XDP,
           XDP_UMEM_REG,
           &umem_reg, sizeof(umem_reg));

/* poll()로 NAPI 트리거 여부 확인:
 * POLLIN  → 수신 데이터 있음
 * POLLOUT → TX 버퍼 여유 있음
 * XDP_USE_NEED_WAKEUP → 필요 시 sendto()로 kick */
struct pollfd fds = {
    .fd     = xsk_fd,
    .events = POLLIN,
};
poll(&fds, 1, 0);

/* TX kick: NEED_WAKEUP 비트가 설정된 경우에만 호출 */
if (xsk_ring_prod__needs_wakeup(&tx_ring))
    sendto(xsk_fd, NULL, 0, MSG_DONTWAIT,
           NULL, 0);

zero-copy vs copy 모드 비교

항목zero-copy 모드copy 모드
메모리 복사없음 — DMA 주소를 직접 공유패킷당 1회 memcpy() 필요
성능 (10GbE)약 20~30 Mpps (드라이버·CPU 의존)약 5~10 Mpps
드라이버 요구사항XDP_SETUP_XSK_POOL 지원 필수기본 XDP 지원만으로 충분
지원 드라이버ice, mlx5, i40e, ixgbe, bnxt_en 등XDP 지원 모든 드라이버
UMEM 청크 크기NIC MTU 이상, 2의 거듭제곱 권장동일
DMA 매핑드라이버가 UMEM 직접 DMA 매핑커널 내부 버퍼에서 UMEM으로 복사
설정 복잡도높음 (drv 지원, queue 독점)낮음 (기존 스택과 공존 가능)

드라이버별 AF_XDP 지원 현황

드라이버NIC 제조사zero-copy멀티큐 XDP비고
iceIntel E800 시리즈지원지원커널 5.5+, 가장 완성도 높음
mlx5Mellanox ConnectX-4/5/6지원지원커널 5.3+, RDMA와 공존 가능
i40eIntel X710/XL710지원지원커널 5.2+
ixgbeIntel X540/X550지원지원커널 4.20+, 비교적 초기 구현
bnxt_enBroadcom NetXtreme지원지원커널 5.10+
veth가상 이더넷 쌍미지원지원copy 모드만 가능

AF_XDP + busy polling 초저지연 패턴 실용 예제

아래는 xdpsock 스타일로 AF_XDP 소켓을 설정하고 NAPI busy poll과 결합하여 초저지연 패킷 처리를 구현하는 패턴입니다.

/* xdpsock 스타일: AF_XDP + NAPI busy poll 설정 예제 */
#include <linux/if_xdp.h>
#include <bpf/xsk.h>

struct xsk_config {
    struct xsk_ring_prod fill_ring;
    struct xsk_ring_cons comp_ring;
    struct xsk_ring_cons rx_ring;
    struct xsk_ring_prod tx_ring;
    struct xsk_socket   *xsk;
    struct xsk_umem    *umem;
    void               *bufs;
};

int setup_af_xdp_socket(struct xsk_config *cfg,
                        const char *iface,
                        int queue_id)
{
    struct xsk_umem_config umem_cfg = {
        .fill_size      = XSK_RING_PROD__DEFAULT_NUM_DESCS,
        .comp_size      = XSK_RING_CONS__DEFAULT_NUM_DESCS,
        .frame_size     = XSK_UMEM__DEFAULT_FRAME_SIZE,
        .frame_headroom = 0,
        .flags          = 0,
    };
    struct xsk_socket_config xsk_cfg = {
        .rx_size        = XSK_RING_CONS__DEFAULT_NUM_DESCS,
        .tx_size        = XSK_RING_PROD__DEFAULT_NUM_DESCS,
        /* zero-copy 요청: 드라이버 미지원 시 copy로 폴백 */
        .libbpf_flags   = XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD,
        .xdp_flags      = XDP_FLAGS_DRV_MODE,
        .bind_flags     = XDP_ZEROCOPY | XDP_USE_NEED_WAKEUP,
    };
    int ret;

    /* UMEM 할당 및 등록 */
    posix_memalign(&cfg->bufs, getpagesize(),
                   NUM_FRAMES * XSK_UMEM__DEFAULT_FRAME_SIZE);
    xsk_umem__create(&cfg->umem, cfg->bufs,
                     NUM_FRAMES * XSK_UMEM__DEFAULT_FRAME_SIZE,
                     &cfg->fill_ring, &cfg->comp_ring,
                     &umem_cfg);

    /* AF_XDP 소켓 생성 및 큐 바인딩 */
    ret = xsk_socket__create(&cfg->xsk, iface,
                              queue_id, cfg->umem,
                              &cfg->rx_ring,
                              &cfg->tx_ring,
                              &xsk_cfg);

    /* busy polling 활성화: SO_BUSY_POLL + SO_PREFER_BUSY_POLL */
    int busy_timeout = 20;   /* 20μs busy polling */
    int xsk_fd = xsk_socket__fd(cfg->xsk);
    setsockopt(xsk_fd, SOL_SOCKET, SO_BUSY_POLL,
               &busy_timeout, sizeof(busy_timeout));
    int prefer = 1;
    setsockopt(xsk_fd, SOL_SOCKET, SO_PREFER_BUSY_POLL,
               &prefer, sizeof(prefer));

    return ret;
}

/* RX 처리 루프: zero-copy → 직접 접근 */
void rx_loop(struct xsk_config *cfg)
{
    __u32 idx_rx = 0, idx_fq = 0;
    unsigned int rcvd;

    for (;;) {
        /* busy poll: poll() 호출이 NAPI를 직접 트리거 */
        struct pollfd fds = {
            .fd     = xsk_socket__fd(cfg->xsk),
            .events = POLLIN,
        };
        poll(&fds, 1, 0); /* timeout=0: 즉시 반환 */

        rcvd = xsk_ring_cons__peek(&cfg->rx_ring,
                                    BATCH_SIZE, &idx_rx);
        if (!rcvd)
            continue;

        for (unsigned int i = 0; i < rcvd; i++) {
            const struct xdp_desc *desc =
                xsk_ring_cons__rx_desc(&cfg->rx_ring,
                                         idx_rx++);
            /* zero-copy: UMEM 청크를 직접 포인터로 접근 */
            void *pkt = xsk_umem__get_data(
                cfg->bufs, desc->addr);
            process_packet(pkt, desc->len);
        }
        xsk_ring_cons__release(&cfg->rx_ring, rcvd);

        /* Fill Ring 보충: 소비한 청크만큼 반환 */
        xsk_ring_prod__reserve(&cfg->fill_ring,
                               rcvd, &idx_fq);
        for (unsigned int i = 0; i < rcvd; i++) {
            *xsk_ring_prod__fill_addr(
                &cfg->fill_ring, idx_fq++) =
                    i * XSK_UMEM__DEFAULT_FRAME_SIZE;
        }
        xsk_ring_prod__submit(&cfg->fill_ring, rcvd);
    }
}
AF_XDP 성능 최적화 포인트:
  • XDP_USE_NEED_WAKEUP를 반드시 활성화하여 불필요한 sendto() kick을 방지합니다.
  • Fill Ring은 항상 절반 이상을 채워두어 DMA 버퍼 고갈로 인한 패킷 드롭을 방지합니다.
  • zero-copy 모드에서는 AF_XDP 소켓이 해당 큐를 독점합니다. 다른 소켓과 동일 큐 공유가 불가능합니다.
  • SO_PREFER_BUSY_POLL과 함께 사용하면 softirq가 해당 NAPI를 건너뛰어 busy poll 스레드에 우선권을 양보합니다.

NAPI NUMA 토폴로지와 CPU affinity

현대 서버 시스템은 대부분 NUMA(Non-Uniform Memory Access) 아키텍처를 채택하고 있습니다. NIC가 연결된 PCIe 슬롯은 특정 NUMA 노드와 물리적으로 가깝고, 해당 노드의 CPU와 메모리 접근 레이턴시가 훨씬 낮습니다. NAPI의 IRQ affinity 설정이 잘못되면 패킷 처리 시 크로스 NUMA 접근이 발생하여 처리량이 크게 감소합니다.

IRQ affinity와 NAPI poll CPU의 관계

NAPI poll은 하드웨어 인터럽트를 수신한 CPU에서 softirq로 실행됩니다. 따라서 /proc/irq/N/smp_affinity로 설정하는 IRQ affinity가 곧 NAPI poll이 실행될 CPU를 결정합니다. NIC IRQ를 NIC와 같은 NUMA 노드의 CPU에 고정하는 것이 성능의 핵심입니다.

NUMA 노드별 NIC-IRQ-CPU-메모리 매핑 관계도 NUMA Node 0 (로컬) NIC 0 (eth0) PCIe Bus — Node 0 CPU 0~3 (NUMA Node 0) CPU 0 CPU 1 CPU 2 CPU 3 DRAM — Node 0 page_pool: dev_to_node() → 로컬 페이지 할당 로컬 접근 레이턴시: ~70ns (DDR4 기준) NUMA Node 1 (크로스 접근 — 성능 저하) NIC 0 IRQ → Node 1 CPU? 잘못된 affinity 설정 예시 CPU 4~7 (NUMA Node 1) CPU 4 CPU 5 CPU 6 CPU 7 DRAM — Node 1 원격 노드 접근 시 QPI/UPI 인터커넥트 경유 원격 접근 레이턴시: ~130~180ns (+85~150%) IRQ→CPU 잘못된 affinity QPI/UPI

NUMA-aware page_pool 할당

page_pool은 생성 시 dev_to_node()를 통해 NIC 디바이스가 속한 NUMA 노드를 확인하고, 해당 노드의 로컬 메모리에서 페이지를 할당합니다. 이를 통해 DMA 전송과 CPU 접근 모두 동일 NUMA 노드 메모리를 사용합니다.

/* 커널 소스 분석: net/core/page_pool.c — NUMA 인식 할당 */

struct page_pool *
page_pool_create(const struct page_pool_params *params)
{
    struct page_pool *pool;

    pool = kzalloc_node(sizeof(*pool), GFP_KERNEL,
                        params->nid); /* NUMA 노드 지정 */

    pool->p = *params;

    /* nid 미설정 시 NIC 디바이스 NUMA 노드로 자동 설정 */
    if (pool->p.nid == NUMA_NO_NODE)
        pool->p.nid = dev_to_node(pool->p.dev);

    return pool;
}

/* 드라이버에서 page_pool 설정 시 NUMA 최적화 */
struct page_pool_params pp_params = {
    .order   = 0,
    .flags   = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
    .pool_size = 512,
    .nid     = dev_to_node(dev->dev.parent),
    .dev     = dev->dev.parent,
    .dma_dir = DMA_FROM_DEVICE,
};
rx_ring->page_pool = page_pool_create(&pp_params);

irqbalance와 NAPI 성능 영향

irqbalance 데몬은 시스템 부하를 분산하기 위해 IRQ affinity를 동적으로 조정합니다. 일반 환경에서는 유용하지만, 고성능 네트워킹 환경에서는 의도치 않은 IRQ 이동이 NAPI poll CPU를 바꾸어 캐시 미스와 NUMA 크로스 접근을 유발합니다. 25 Gbps 이상의 고성능 환경에서는 irqbalance를 비활성화하고 수동으로 IRQ affinity를 고정하는 것을 권장합니다.

# irqbalance 비활성화 (고성능 서버 환경)
systemctl disable --now irqbalance

# NIC IRQ 목록 조회 (eth0 예시)
grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'

# IRQ N을 CPU 0에 고정: smp_affinity는 CPU 비트마스크
echo 1 > /proc/irq/<N>/smp_affinity      # CPU 0만 (비트 0)
echo 3 > /proc/irq/<N>/smp_affinity      # CPU 0+1 (비트 0,1)

# smp_affinity_list로 CPU 번호 직접 지정 (가독성 향상)
echo 0-3 > /proc/irq/<N>/smp_affinity_list   # CPU 0~3
echo 0   > /proc/irq/<N>/smp_affinity_list   # CPU 0만

RSS 큐와 IRQ affinity 자동 설정 스크립트

RSS(Receive Side Scaling)는 수신 패킷을 여러 큐로 분산하는 기능입니다. 각 큐에 대응하는 IRQ를 적절한 CPU에 고정하고, 동일 NUMA 노드 내에서 분산하면 최적 성능을 달성할 수 있습니다.

#!/bin/bash
# set_irq_affinity.sh: NIC IRQ를 NUMA 로컬 CPU에 자동 고정
# 사용법: ./set_irq_affinity.sh eth0

IFACE=$1
# NIC의 NUMA 노드 확인
NIC_NODE=$(cat /sys/class/net/${IFACE}/device/numa_node)
if [ "$NIC_NODE" -lt 0 ]; then
    NIC_NODE=0  # NUMA 미지원 시스템: 노드 0 사용
fi

# 해당 NUMA 노드의 CPU 목록 획득
CPUS=$(cat /sys/devices/system/node/node${NIC_NODE}/cpulist)
echo "NIC ${IFACE} is on NUMA node ${NIC_NODE}, CPUs: ${CPUS}"

# NIC IRQ 목록 수집
IRQ_LIST=$(grep "${IFACE}" /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')

# CPU 배열 생성 (NUMA 로컬 CPU만)
CPU_ARRAY=($(echo $CPUS | tr ',' ' ' | tr '-' ' ' | xargs seq 2>/dev/null || echo $CPUS | tr ',' '\n'))

IDX=0
for IRQ in $IRQ_LIST; do
    CPU=${CPU_ARRAY[$((IDX % ${#CPU_ARRAY[@]}))]}
    echo "IRQ ${IRQ} → CPU ${CPU}"
    echo ${CPU} > /proc/irq/${IRQ}/smp_affinity_list
    IDX=$((IDX + 1))
done

# XPS(Transmit Packet Steering)도 동일 CPU에 설정
QUEUE_IDX=0
for XPS_FILE in /sys/class/net/${IFACE}/queues/tx-*/xps_cpus; do
    CPU=${CPU_ARRAY[$((QUEUE_IDX % ${#CPU_ARRAY[@]}))]}
    CPU_MASK=$((1 << CPU))
    printf '%x' $CPU_MASK > ${XPS_FILE}
    QUEUE_IDX=$((QUEUE_IDX + 1))
done

echo "IRQ affinity 설정 완료"

ethtool로 NAPI 큐 수와 RSS 해시 테이블 조정

ethtool -L로 NIC 큐 수를 조정하고, ethtool -X로 RSS 인디렉션 테이블을 설정하면 트래픽 분산 방식을 세밀하게 제어할 수 있습니다. 큐 수는 NUMA 노드당 CPU 수와 일치시키는 것이 일반적으로 최적입니다.

# 현재 큐 설정 조회
ethtool -l eth0
# Combined (RX+TX 겸용) 큐 수를 4개로 설정
ethtool -L eth0 combined 4

# RSS 인디렉션 테이블 조회
ethtool -x eth0
# RSS 인디렉션 테이블을 기본값으로 초기화 (균등 분산)
ethtool -X eth0 default

# RSS 인디렉션 테이블 수동 설정 (큐 0~3을 균등 분산)
ethtool -X eth0 equal 4

# RSS 해시 키 조회 및 변경 (Toeplitz 키)
ethtool -x eth0
ethtool -X eth0 hkey <32바이트 16진수 키>

# NAPI 스레드 모드 설정 (threaded NAPI, 커널 5.10+)
echo 1 > /sys/class/net/eth0/threaded

# NAPI 스레드를 특정 CPU에 고정 (set_cpus_allowed_ptr 효과)
# 스레드 이름: napi/eth0-0, napi/eth0-1, ...
for pid in $(pgrep -f "napi/eth0"); do
    taskset -cp 0-3 $pid
done

크로스 NUMA 접근의 성능 패널티

접근 유형레이턴시 (DDR4 2-소켓 기준)대역폭 영향발생 조건
로컬 NUMA 메모리 접근~70ns최대 (100%)IRQ affinity가 NIC와 같은 노드
원격 NUMA 메모리 접근~130~180ns약 60~70%IRQ affinity가 반대 노드로 설정됨
잘못된 page_pool NUMA 설정~130~180ns약 60~70%nid 미지정 또는 반대 노드 지정
irqbalance가 IRQ를 이동불규칙 (70~180ns)불규칙 (60~100%)irqbalance 활성 + 고부하 상황
고성능 환경 NUMA 최적화 체크리스트:
  1. cat /sys/class/net/eth0/device/numa_node로 NIC NUMA 노드 확인
  2. irqbalance 비활성화: systemctl disable --now irqbalance
  3. NIC IRQ를 로컬 NUMA CPU에 고정: /proc/irq/N/smp_affinity_list
  4. page_pool nid를 dev_to_node()로 설정 (드라이버 코드 확인)
  5. 스레드 NAPI 사용 시 taskset으로 로컬 CPU에 고정
  6. numactl --hardware로 NUMA 토폴로지 사전 확인

인터럽트 코얼레싱(Interrupt Coalescing) 전략

인터럽트 코얼레싱(Interrupt Coalescing)은 NIC 하드웨어가 패킷이 도착할 때마다 즉시 인터럽트를 발생시키는 대신, 일정 시간 또는 일정 패킷 수가 쌓일 때까지 기다렸다가 한 번에 인터럽트를 발생시키는 기술입니다. 이를 통해 인터럽트 처리 오버헤드를 줄이고 배치 처리 효율을 높이지만, 패킷당 처리 지연(레이턴시)이 증가하는 트레이드오프가 있습니다. 소프트웨어 레벨에서는 커널의 defer_hard_irqsgro_flush_timeout이 유사한 역할을 합니다.

하드웨어 코얼레싱 파라미터

파라미터설명단위기본값 (드라이버별 상이)
rx-usecs수신 인터럽트 최대 지연 시간마이크로초(μs)50~200μs
rx-frames이 수만큼 수신 패킷이 쌓이면 인터럽트 발생패킷 수1 (즉시)
tx-usecs송신 완료 인터럽트 최대 지연 시간마이크로초(μs)50~200μs
tx-frames이 수만큼 송신 완료 시 인터럽트 발생패킷 수1 (즉시)
rx-usecs-irq인터럽트 핸들러 내 추가 코얼레싱 (일부 드라이버)마이크로초0
adaptive-rx트래픽 패턴에 따라 rx-usecs 자동 조정on/off드라이버 의존
adaptive-tx트래픽 패턴에 따라 tx-usecs 자동 조정on/off드라이버 의존
# 현재 코얼레싱 설정 조회
ethtool -c eth0

# 코얼레싱 설정 변경 예시
# 저지연 우선: 코얼레싱 최소화
ethtool -C eth0 rx-usecs 10 rx-frames 1 tx-usecs 10 tx-frames 1

# 고처리량 우선: 코얼레싱 최대화
ethtool -C eth0 rx-usecs 200 rx-frames 64 tx-usecs 200 tx-frames 64

# Adaptive Interrupt Moderation 활성화 (DIM 등)
ethtool -C eth0 adaptive-rx on adaptive-tx on

# 소프트웨어 코얼레싱: defer_hard_irqs와 gro_flush_timeout
# NAPI poll이 끝난 후 이 횟수만큼은 하드 IRQ 재활성화 지연
echo 3 > /proc/sys/net/core/napi_defer_hard_irqs
# GRO 버퍼를 최대 이 시간(나노초) 동안 유지 후 flush
echo 50000 > /proc/sys/net/core/gro_flush_timeout

인터럽트 코얼레싱 vs defer_hard_irqs 비교 타임라인

인터럽트 코얼레싱 vs defer_hard_irqs 타임라인 시간 → 패킷 도착 pkt1 pkt2 pkt3 pkt4 pkt5 pkt6 pkt7 pkt8 코얼레싱 없음 (rx-usecs=0) IRQ IRQ IRQ IRQ IRQ IRQ IRQ IRQ ← 8 IRQ/배치 (오버헤드 높음) HW 코얼레싱 (rx-usecs=50) 50μs 코얼레싱 대기 IRQ 50μs 코얼레싱 대기 IRQ ← 2 IRQ NAPI poll×4 SW: defer_hard_irqs (defer=3 rounds) IRQ1 poll×2 defer poll defer poll IRQ↑ IRQ 재활성화 코얼레싱 없음 레이턴시: 최소 (즉시 처리) CPU 오버헤드: 최대 적합: 초저지연 금융 거래 throughput: 낮음 (인터럽트 포화) HW 코얼레싱 (rx-usecs=50~200) 레이턴시: 50~200μs 추가 CPU 오버헤드: 대폭 감소 적합: 고처리량 서버 (CDN, 스트리밍) throughput: 높음 SW defer_hard_irqs 레이턴시: 가변 (poll 주기 의존) CPU 오버헤드: 중간 적합: HW 코얼레싱 미지원 NIC throughput: 중상

Adaptive Interrupt Moderation (AIM/DIM)

Adaptive Interrupt Moderation은 드라이버가 현재 트래픽 패턴(초당 패킷 수, 패킷 크기)을 모니터링하여 rx-usecs를 동적으로 조정하는 기능입니다. 트래픽이 많을 때는 코얼레싱을 늘려 CPU 오버헤드를 줄이고, 트래픽이 적을 때는 코얼레싱을 줄여 레이턴시를 낮춥니다. 커널 5.x에서는 DIM(Dynamic Interrupt Moderation) API로 표준화되었습니다. DIM의 상세 동작은 DIM 알고리즘 섹션을 참고하십시오.

커널 소스 분석: ethtool 코얼레싱 설정 경로

ethtool -C 명령어는 ethtool_set_coalesce()를 통해 드라이버의 set_coalesce 콜백을 호출합니다.

/* 커널 소스 분석: net/core/ethtool.c — ethtool_set_coalesce() */

static int
ethtool_set_coalesce(struct net_device *dev,
                     void __user *useraddr)
{
    struct ethtool_coalesce coalesce;
    struct kernel_ethtool_coalesce kernel_coalesce = {};
    int ret;

    if (!dev->ethtool_ops->set_coalesce ||
        !dev->ethtool_ops->supported_coalesce_params)
        return -EOPNOTSUPP;

    if (copy_from_user(&coalesce, useraddr, sizeof(coalesce)))
        return -EFAULT;

    /* 드라이버 지원 파라미터 검증 */
    ret = ethtool_check_ops(dev->ethtool_ops,
                            &coalesce);
    if (ret)
        return ret;

    /* 드라이버 콜백 호출: ice_set_coalesce(), mlx5e_set_coalesce() 등 */
    ret = dev->ethtool_ops->set_coalesce(dev, &coalesce,
                                           &kernel_coalesce,
                                           NULL);
    return ret;
}

/* ice 드라이버 예시: drivers/net/ethernet/intel/ice/ice_ethtool.c */
static int
ice_set_coalesce(struct net_device *netdev,
                 struct ethtool_coalesce *ec,
                 struct kernel_ethtool_coalesce *kernel_coal,
                 struct netlink_ext_ack *extack)
{
    struct ice_netdev_priv *np = netdev_priv(netdev);
    struct ice_vsi *vsi = np->vsi;
    int ret = 0;

    for (u16 q = 0; q < vsi->num_rxq; q++) {
        struct ice_ring_container *rc =
            &vsi->rx_rings[q]->q_vector->rx;

        /* ITR (Interrupt Throttling Rate) 레지스터 업데이트 */
        rc->itr_setting = ec->rx_coalesce_usecs;

        /* DIM 비활성화 (수동 설정) */
        if (!ec->use_adaptive_rx_coalesce)
            rc->dim_prog_allowed = false;

        ice_write_itr(rc, rc->itr_setting);
    }

    return ret;
}

/* defer_hard_irqs: 소프트웨어 레벨 코얼레싱
 * net/core/dev.c — napi_complete_done() 내부 */
bool napi_complete_done(struct napi_struct *n, int work_done)
{
    unsigned long flags;
    bool ret = true;

    /* gro_flush_timeout: GRO 버퍼 flush 타임아웃 체크 */
    if (READ_ONCE(n->gro_flush_timeout) &&
        napi_defer_gro(n))
        return false; /* GRO flush 타임아웃 미경과: 계속 폴링 */

    /* defer_hard_irqs: 하드 IRQ 재활성화를 N라운드 지연 */
    if (READ_ONCE(n->defer_hard_irqs_count) > 0) {
        n->defer_hard_irqs_count--;
        /* IRQ 비활성 유지: repoll 요청 */
        __napi_schedule(n);
        return false;
    }

    /* 정상 완료: NAPI 비활성화 + 인터럽트 재활성화 */
    list_del_init(&n->poll_list);
    __clear_bit(NAPI_STATE_SCHED, &n->state);

    return ret;
}

워크로드별 최적 코얼레싱 설정 가이드

워크로드rx-usecsrx-framesadaptivedefer_hard_irqsgro_flush_timeout이유
금융 거래 시스템
(초저지연)
0~10 1 off 0 0 레이턴시 우선 — 패킷당 즉시 인터럽트 + busy poll 조합
웹 서버 / CDN
(고처리량)
100~200 32~64 on 0~3 50000~200000 처리량 우선 — 배치 처리, CPU 효율 극대화
데이터베이스 서버
(혼합)
50~100 16 on 0~1 0~50000 쿼리 레이턴시와 처리량 균형
스트리밍 / 미디어
(일정 처리량)
100~150 32 on 1~3 100000 지터(jitter) 최소화 + CPU 효율
VoIP / 실시간
(저지연+저대역)
10~20 1~4 off 0 0 소규모 패킷의 일관된 저지연 필요

금융 거래 시스템 vs CDN 서버 설정 예제

#!/bin/bash
# 금융 거래 시스템: 초저지연 설정

IFACE="eth0"

# 하드웨어 코얼레싱 최소화
ethtool -C $IFACE rx-usecs 0 rx-frames 1 \
                  tx-usecs 0 tx-frames 1 \
                  adaptive-rx off adaptive-tx off

# 소프트웨어 코얼레싱 비활성화
echo 0 > /proc/sys/net/core/napi_defer_hard_irqs
echo 0 > /proc/sys/net/core/gro_flush_timeout

# busy poll 설정 (소켓 레벨은 애플리케이션에서 설정)
echo 50 > /proc/sys/net/core/busy_poll    # 50μs
echo 50 > /proc/sys/net/core/busy_read

# GRO 비활성화 (레이턴시 우선)
ethtool -K $IFACE gro off

# CPU 전원 관리: C-state 제한으로 레이턴시 스파이크 방지
for cpu_dir in /sys/devices/system/cpu/cpu*/power; do
    echo "performance" > ${cpu_dir}/../cpufreq/scaling_governor \
        2>/dev/null || true
done

# 결과 확인
echo "=== 금융 거래 설정 완료 ==="
ethtool -c $IFACE | grep -E "rx-usecs|tx-usecs|adaptive"
#!/bin/bash
# CDN 서버: 고처리량 설정

IFACE="eth0"

# 하드웨어 코얼레싱 최대화
ethtool -C $IFACE rx-usecs 200 rx-frames 64 \
                  tx-usecs 200 tx-frames 64 \
                  adaptive-rx on adaptive-tx on

# 소프트웨어 코얼레싱 활성화
echo 3 > /proc/sys/net/core/napi_defer_hard_irqs
echo 200000 > /proc/sys/net/core/gro_flush_timeout  # 200μs

# GRO, GSO, TSO 활성화
ethtool -K $IFACE gro on gso on tso on

# netdev_budget 증가: softirq 1회에 더 많은 패킷 처리
echo 600 > /proc/sys/net/core/netdev_budget
echo 4000 > /proc/sys/net/core/netdev_budget_usecs  # 4ms

# RSS 큐 수를 코어 수에 맞게 설정
NCPUS=$(nproc)
ethtool -L $IFACE combined $NCPUS 2>/dev/null || true

# 결과 확인
echo "=== CDN 고처리량 설정 완료 ==="
ethtool -c $IFACE | grep -E "rx-usecs|tx-usecs|adaptive"
cat /proc/sys/net/core/napi_defer_hard_irqs
cat /proc/sys/net/core/gro_flush_timeout
코얼레싱 튜닝 검증 방법:
  • /proc/net/softnet_stat의 세 번째 열(throttled)이 증가하면 인터럽트 처리 과부하 징조입니다. rx-usecs를 늘리거나 defer_hard_irqs를 활성화하십시오.
  • perf top으로 __do_softirq, net_rx_action의 CPU 점유율을 모니터링합니다.
  • ethtool -S eth0 | grep -i drop으로 RX 드롭 카운터를 주기적으로 확인합니다.
  • 레이턴시 측정에는 hping3 --fast 또는 sockperf ping-pong을 활용합니다.

실전 트러블슈팅(Troubleshooting) 시나리오

NAPI 관련 성능 문제는 증상이 다양하고 원인이 복합적입니다. 패킷 드롭(Drop), 레이턴시(Latency) 스파이크(Spike), CPU 불균형, GRO 오동작 등 대표적인 6가지 시나리오별로 진단 명령어와 해결 방법을 체계적으로 설명합니다.

NAPI 성능 이상 감지 /proc/net/softnet_stat 확인 패킷 드롭 발생 dropped 열 증가 레이턴시 스파이크 p99 지연 급증 time_squeeze 높음 2번째 열 지속 증가 CPU 불균형 특정 코어만 과부하 진단: ethtool -S, dmesg rx_missed_errors 확인 NIC 큐 깊이 부족 가능성 진단: perf top, bpftrace softirq 핫스팟 확인 NAPI poll 실행 시간 측정 해결: budget 증가 netdev_budget 조정 IRQ affinity 재배치 해결: RSS 큐 재배치 /proc/interrupts 분석 ethtool -X 큐 매핑 해결: ethtool -G rx/tx NIC 큐 링 버퍼 크기 증가 해결: GRO/IRQ coalescing ethtool -C 인터럽트 병합 해결: sysctl 조정 netdev_budget_usecs 증가 해결: RPS/XPS 설정 소프트웨어 큐 분산 패킷 드롭 레이턴시 time_squeeze CPU 불균형

시나리오 1: 패킷 드롭 진단

수신 패킷 드롭은 NIC 링 버퍼(Ring Buffer) 오버플로우(Overflow)나 소프트웨어 큐 포화로 발생합니다. 먼저 /proc/net/softnet_stat의 세 번째 열(dropped)을 확인하고, NIC 드라이버 통계에서 하드웨어 레벨 드롭을 구분합니다.

# /proc/net/softnet_stat 분석
# 형식: total processed | time_squeeze | dropped | ... (CPU당 1행)
cat /proc/net/softnet_stat

# 컬럼 해석 스크립트
awk 'NR==1{print "CPU total time_squeeze dropped throttled"}
{printf "CPU%d %s %s %s %s\n", NR-1, $1, $2, $3, $9}' /proc/net/softnet_stat

# NIC 드라이버 수준 통계 (rx_missed_errors: 하드웨어 큐 오버플로우)
ethtool -S eth0 | grep -E 'rx_missed|rx_drop|rx_no_buffer|rx_fifo'

# dmesg에서 NAPI/NIC 관련 경고 확인
dmesg | grep -E 'NAPI|napi|rx_ring|ring buffer|dropped'

# NIC 링 버퍼 현재 설정 및 최댓값 확인
ethtool -g eth0

# 링 버퍼 크기 증가 (RX/TX 모두)
ethtool -G eth0 rx 4096 tx 4096
rx_missed_errors vs softnet_stat dropped: ethtool -Srx_missed_errors는 NIC 하드웨어 링 버퍼가 가득 차서 발생한 드롭입니다. 반면 /proc/net/softnet_stat의 dropped 열은 소프트웨어 netdev backlog 큐가 포화된 경우입니다. 두 값을 함께 확인하여 하드웨어 vs 소프트웨어 계층 중 어디서 드롭이 발생하는지 구분해야 합니다.

시나리오 2: 레이턴시 스파이크 진단

p99 이상의 레이턴시 스파이크는 softIRQ가 특정 CPU에서 과도하게 실행되거나, NAPI poll 함수 내부에서 처리 지연이 발생할 때 나타납니다. perf top으로 핫스팟을 파악하고, bpftrace로 poll 실행 시간 분포를 측정합니다.

# perf top으로 softirq 핫스팟 확인 (net_rx_action, 드라이버 poll 함수)
perf top -e cpu-clock --sort comm,dso,symbol | grep -A5 'net_rx_action\|napi_poll'

# CPU별 softirq 시간 비율 확인
mpstat -I SCPU 1 5

# /proc/softirqs 변화 추적 (NET_RX 항목)
watch -n 1 'cat /proc/softirqs | grep -E "NET_RX|NET_TX"'
# bpftrace: NAPI poll 실행 시간 히스토그램 (마이크로초 단위)
bpftrace -e '
kprobe:napi_poll {
    @start[tid] = nsecs;
}
kretprobe:napi_poll /@start[tid]/ {
    @poll_latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
END {
    print(@poll_latency_us);
}'

# 특정 드라이버 poll 함수 추적 (igb_poll 예시)
bpftrace -e '
kprobe:igb_poll {
    @start[tid] = nsecs;
}
kretprobe:igb_poll /@start[tid]/ {
    @igb_poll_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
END { print(@igb_poll_us); }'
# ftrace: NAPI 이벤트 트레이싱 설정
cd /sys/kernel/debug/tracing

# NAPI 관련 트레이스포인트 목록 확인
grep napi available_events

# napi_poll_entry/exit 트레이스포인트 활성화
echo napi:napi_poll > set_event
echo 1 > tracing_on

# 잠시 후 트레이스 결과 확인
sleep 3
echo 0 > tracing_on
cat trace | head -50

# function_graph tracer로 net_rx_action 내부 추적
echo function_graph > current_tracer
echo net_rx_action > set_graph_function
echo 1 > tracing_on
sleep 1
echo 0 > tracing_on
cat trace | head -100

시나리오 3: time_squeeze 높음

/proc/net/softnet_stat의 두 번째 열 time_squeeze가 지속적으로 증가하면 net_rx_action()이 budget 또는 시간 제한으로 중단되고 있음을 의미합니다. netdev_budgetnetdev_budget_usecs를 조정하여 해결합니다.

# time_squeeze 현황 확인 (2번째 열)
awk '{print "CPU" NR-1 ": time_squeeze=" $2}' /proc/net/softnet_stat

# 현재 budget 설정 확인
sysctl net.core.netdev_budget
sysctl net.core.netdev_budget_usecs

# budget 증가 (기본값 300 → 고트래픽 환경에서 600~1000)
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_budget_usecs=8000

# IRQ affinity 확인: 모든 인터럽트가 CPU0에 집중되는지 확인
for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d :); do
    echo -n "IRQ $irq affinity: "
    cat /proc/irq/$irq/smp_affinity_list
done

# IRQ affinity 재배치: 큐별로 코어 분산
# eth0 큐 0 → CPU0, 큐 1 → CPU1, ...
for i in $(seq 0 3); do
    irq=$(grep "eth0-$i" /proc/interrupts | awk '{print $1}' | tr -d :)
    echo $i > /proc/irq/$irq/smp_affinity_list
done

시나리오 4: CPU 불균형 진단

특정 CPU만 과부하 상태이고 나머지 CPU는 유휴 상태라면, RSS(Receive Side Scaling) 설정이 올바르지 않거나 IRQ affinity가 단일 코어에 집중된 것입니다.

# CPU별 인터럽트 분포 확인
cat /proc/interrupts | grep eth0

# CPU별 softirq 처리량 확인
cat /proc/softirqs | grep NET_RX

# NIC 큐 수 및 RSS 설정 확인
ethtool -l eth0

# RSS 큐 수 조정 (Combined = RX+TX 묶음)
ethtool -L eth0 combined 8

# RSS 해시 키 및 인다이렉션 테이블 확인
ethtool -x eth0

# 인다이렉션 테이블 재설정 (균등 분배)
ethtool -X eth0 equal 8

# XPS(Transmit Packet Steering) 설정
# TX 큐 i를 CPU i에 바인딩
for i in $(seq 0 7); do
    echo $((1 << i)) > /sys/class/net/eth0/queues/tx-$i/xps_cpus
done

시나리오 5: GRO 관련 이상

GRO(Generic Receive Offload)가 패킷을 과도하게 병합하면 레이턴시가 증가하고, 비활성화되어 있으면 처리량이 감소합니다. gro_flush_timeout이 너무 크면 병합 대기 시간이 길어져 레이턴시 스파이크가 발생합니다.

# GRO 및 오프로드 기능 상태 확인
ethtool -k eth0 | grep -E 'generic-receive-offload|gro|large-receive'

# GRO 활성화/비활성화
ethtool -K eth0 gro on
ethtool -K eth0 gro off

# gro_flush_timeout 확인 및 조정 (나노초 단위)
# 기본값 0 = 즉시 플러시, 양수 = 대기 시간 (레이턴시 vs 처리량 트레이드오프)
cat /sys/class/net/eth0/gro_flush_timeout
echo 0 > /sys/class/net/eth0/gro_flush_timeout    # 저레이턴시 모드
echo 100000 > /sys/class/net/eth0/gro_flush_timeout # 100μs 대기 (처리량 우선)

# napi_defer_hard_irqs 확인 (GRO 배치 효과 극대화)
cat /sys/class/net/eth0/napi_defer_hard_irqs
echo 2 > /sys/class/net/eth0/napi_defer_hard_irqs  # 2회 지연 후 폴링 종료

# GRO 병합 통계 확인
ethtool -S eth0 | grep -E 'gro|lro|aggregat'

시나리오 6: 스레드 NAPI 전환 후 성능 저하

스레드 NAPI(Threaded NAPI)로 전환한 후 처리량이 감소하는 경우, 커널 스레드의 스케줄링 우선순위나 CPU 친화성(Affinity) 설정이 올바르지 않을 수 있습니다.

# 스레드 NAPI 활성화 상태 확인
cat /sys/class/net/eth0/threaded

# NAPI 커널 스레드 확인 (napi/$ifname 형식)
ps aux | grep 'napi/'

# NAPI 스레드 PID 및 스케줄링 정책 확인
for pid in $(pgrep -f 'napi/eth0'); do
    echo -n "PID $pid: "
    chrt -p $pid
    taskset -cp $pid
done

# NAPI 스레드를 FIFO 실시간 정책으로 변경 (우선순위 50)
for pid in $(pgrep -f 'napi/eth0'); do
    chrt -f -p 50 $pid
done

# NAPI 스레드를 특정 CPU 코어에 고정 (CPU 4~7)
for pid in $(pgrep -f 'napi/eth0'); do
    taskset -cp 4-7 $pid
done

# 스레드 NAPI vs softirq NAPI 처리량 비교
# softirq 방식으로 되돌리기
echo 0 > /sys/class/net/eth0/threaded
스레드 NAPI 주의 사항: 스레드 NAPI는 PREEMPT_RT 패치 환경이나 실시간 워크로드에서 유용하지만, 일반 서버 환경에서는 softIRQ 기반 NAPI보다 스케줄링 오버헤드가 더 클 수 있습니다. 전환 전후 iperf3netperf로 반드시 성능을 측정하여 비교하시기 바랍니다.

netpoll과 NAPI 디버그 경로

netpoll은 커널 패닉(Panic)이나 디버거 브레이크포인트(Breakpoint) 상황처럼 정상적인 네트워크 스택을 사용할 수 없는 환경에서도 네트워크 통신을 가능하게 하는 메커니즘입니다. 대표적인 사용 사례는 네트워크 콘솔(netconsole)과 이더넷(Ethernet) 기반 KGDB입니다.

netpoll의 목적과 동작 원리

netpoll은 세 가지 전제 조건에서 동작하도록 설계되었습니다. 첫째, 인터럽트가 비활성화된 상태에서도 동작해야 합니다. 둘째, 스케줄러(Scheduler)가 동작하지 않더라도 패킷을 주고받을 수 있어야 합니다. 셋째, 메모리 할당 실패 시에도 안전하게 동작해야 합니다. 이를 위해 netpoll은 NAPI poll을 직접 호출하는 "강제 폴링(Forced Polling)" 경로를 사용합니다.

# netconsole 모듈 설정 예시
# 형식: src_port@src_ip/dev,dst_port@dst_ip/dst_mac
modprobe netconsole netconsole=6666@192.168.1.10/eth0,514@192.168.1.1/aa:bb:cc:dd:ee:ff

# 런타임 설정 (configfs 인터페이스)
mkdir /sys/kernel/config/netconsole/target1
echo 6666 > /sys/kernel/config/netconsole/target1/local_port
echo 514  > /sys/kernel/config/netconsole/target1/remote_port
echo 192.168.1.10 > /sys/kernel/config/netconsole/target1/local_ip
echo 192.168.1.1  > /sys/kernel/config/netconsole/target1/remote_ip
echo eth0 > /sys/kernel/config/netconsole/target1/dev_name
echo 1 > /sys/kernel/config/netconsole/target1/enabled

# kgdb over ethernet 설정
echo ttyS0,115200 > /sys/module/kgdboc/parameters/kgdboc
# 또는 네트워크 기반:
echo g > /proc/sysrq-trigger  # 강제 kgdb 진입

NAPI_STATE_NPSVC 플래그와 netpoll 상호작용

netpoll이 NAPI poll을 강제 호출할 때, 일반 softIRQ 경로와 충돌을 방지하기 위해 NAPI_STATE_NPSVC(NetPoll Service) 비트를 사용합니다. 이 비트가 설정된 NAPI 인스턴스는 netpoll이 현재 서비스 중임을 나타내며, softIRQ의 net_rx_action()은 해당 NAPI를 건너뜁니다.

/* include/linux/netdevice.h — NAPI 상태 비트 정의 */

enum {
    NAPI_STATE_SCHED,        /* poll_list에 등록됨 */
    NAPI_STATE_MISSED,       /* poll 중 새 패킷 도착 */
    NAPI_STATE_DISABLE,      /* napi_disable() 호출됨 */
    NAPI_STATE_NPSVC,        /* netpoll 서비스 진행 중 — 핵심 */
    NAPI_STATE_LISTED,       /* napi_hash에 등록됨 */
    NAPI_STATE_NO_BUSY_POLL, /* busy poll 비활성화 */
    NAPI_STATE_IN_BUSY_POLL, /* busy poll 실행 중 */
    NAPI_STATE_PREFER_BUSY_POLL,
    NAPI_STATE_THREADED,     /* 스레드 NAPI 활성화 */
    NAPI_STATE_SCHED_THREADED,
};

/* net/core/netpoll.c — netpoll_poll_dev() 핵심 구현 */

static void netpoll_poll_dev(struct net_device *dev)
{
    const struct net_device_ops *ops = dev->netdev_ops;
    struct napi_struct *napi;

    /* 디바이스가 폴링 가능한지 확인 */
    if (!netif_running(dev) || !ops->ndo_poll_controller)
        return;

    /* 모든 NAPI 인스턴스에 대해 강제 폴링 수행 */
    list_for_each_entry(napi, &dev->napi_list, dev_list) {
        if (napi_schedule_prep(napi)) {
            /* NAPI_STATE_NPSVC 설정: softIRQ와 충돌 방지 */
            set_bit(NAPI_STATE_NPSVC, &napi->state);
            /* 버짓 1: 가능한 적게 처리하여 오버헤드 최소화 */
            napi->poll(napi, 1);
            clear_bit(NAPI_STATE_NPSVC, &napi->state);
        }
    }

    /* poll_controller: 하드웨어에서 패킷을 강제로 가져오도록 NIC에 신호 */
    if (ops->ndo_poll_controller)
        ops->ndo_poll_controller(dev);

    zap_completion_queue();
}

/* netpoll_rx(): 수신 패킷이 netpoll 소비자용인지 확인 */

bool netpoll_rx(struct sk_buff *skb)
{
    struct net_device *dev = skb->dev;
    struct netpoll_info *npinfo;
    bool ret = false;

    rcu_read_lock();
    npinfo = rcu_dereference(dev->npinfo);

    if (!npinfo) {
        goto out;  /* netpoll 미등록 디바이스 */
    }

    if (skb->protocol == htons(ETH_P_ARP)) {
        netpoll_neigh_reply(skb, npinfo);
        ret = true;
    } else if (skb->protocol == htons(ETH_P_IP)) {
        /* UDP 패킷만 처리: netconsole/kgdb 수신 경로 */
        ret = netpoll_rx_on(skb);
    }

out:
    rcu_read_unlock();
    return ret;
}

커널 패닉 시 netconsole 동작 원리

커널 패닉 시에도 netconsole이 로그를 전송할 수 있는 이유는 panic_notifier_list에 등록된 콜백이 netpoll_poll_dev()를 직접 호출하기 때문입니다. 패닉 상황에서는 스케줄러, softIRQ, 인터럽트가 모두 비활성화되므로, netpoll은 드라이버의 ndo_poll_controller()를 통해 하드웨어를 직접 폴링합니다.

/* net/core/netpoll.c — 패닉 핸들러 등록 */

static int netpoll_netdev_event(struct notifier_block *nb,
                                  unsigned long event, void *p)
{
    /* NETDEV_DOWN 이벤트 처리: netpoll 정리 */
    ...
}

/* drivers/net/netconsole.c — 패닉 시 강제 TX 경로 */
static void write_ext_msg(struct console *con, const char *msg,
                           unsigned int len)
{
    struct netconsole_target *nt;

    rcu_read_lock();
    list_for_each_entry_rcu(nt, &target_list, list) {
        if (nt->enabled && netif_running(nt->np.dev)) {
            /* netpoll_send_udp(): 인터럽트 없이 직접 TX */
            netpoll_send_udp(&nt->np, msg, len);
        }
    }
    rcu_read_unlock();
}

/* netpoll_send_udp() → netpoll_send_skb() → netpoll_send_skb_on_dev()
 * → dev_queue_xmit() 우회 → ops->ndo_start_xmit() 직접 호출
 * 이 경로에서는 qdisc, tc, netfilter 모두 우회됩니다 */

netpoll과 스레드 NAPI 호환성 이슈

스레드 NAPI가 활성화된 경우, napi_poll()이 커널 스레드 컨텍스트에서 실행되므로 netpoll이 직접 NAPI poll을 호출하면 경쟁 조건(Race Condition)이 발생할 수 있습니다. 커널은 NAPI_STATE_THREADED 비트를 확인하여 스레드 NAPI 인스턴스에 대한 netpoll 강제 폴링을 제한합니다. 이로 인해 스레드 NAPI와 netconsole을 동시에 사용할 경우 로그 전송이 불완전할 수 있으므로 주의가 필요합니다.

/* net/core/dev.c — 스레드 NAPI에서 netpoll 처리 */

static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
    void *have;
    int work = 0, weight;

    list_del_init(&n->poll_list);

    have = netpoll_poll_lock(n);  /* netpoll 락 획득 시도 */

    weight = n->weight;

    /* NAPI_STATE_NPSVC: netpoll이 이미 서비스 중이면 스킵 */
    if (test_bit(NAPI_STATE_NPSVC, &n->state)) {
        netpoll_poll_unlock(have);
        return work;
    }

    work = n->poll(n, weight);

    netpoll_poll_unlock(have);
    return work;
}

e1000e/igb 드라이버 NAPI 상세 분석

인텔(Intel)의 e1000e와 igb 드라이버는 단일 큐 NAPI와 멀티큐 NAPI의 교과서적인 구현체입니다. 두 드라이버를 비교 분석하면 NAPI API의 진화 방향과 멀티큐 스케일링의 설계 원칙을 이해할 수 있습니다.

e1000e: 단일 큐 NAPI의 대표 구현

e1000e는 Intel PRO/1000 계열 1GbE NIC 드라이버입니다. 단일 Tx/Rx 큐 쌍을 하나의 napi_struct로 관리하며, 인터럽트 핸들러에서 napi_schedule_irqoff()를 호출하는 가장 기본적인 패턴을 보여줍니다.

/* drivers/net/ethernet/intel/e1000e/netdev.c */

/* MSI 인터럽트 핸들러: 모든 인터럽트가 단일 핸들러로 집중 */
static irqreturn_t e1000_intr_msi(int irq, void *data)
{
    struct net_device *netdev = data;
    struct e1000_adapter *adapter = netdev_priv(netdev);
    struct e1000_hw *hw = &adapter->hw;
    u32 icr = er32(ICR);  /* 인터럽트 원인 레지스터 읽기 (자동 클리어) */

    /* 링크 상태 변경 처리 */
    if (icr & E1000_ICR_LSC) {
        hw->mac.get_link_status = true;
        /* 워크큐(Workqueue)에서 링크 처리 (sleepable context) */
        schedule_work(&adapter->watchdog_task);
    }

    if (napi_schedule_prep(&adapter->napi)) {
        adapter->total_tx_bytes = 0;
        adapter->total_tx_packets = 0;
        adapter->total_rx_bytes = 0;
        adapter->total_rx_packets = 0;
        /* 인터럽트 비활성화 + poll_list 등록 (irq-off 최적화 버전) */
        __napi_schedule_irqoff(&adapter->napi);
    }

    return IRQ_HANDLED;
}

/* e1000_clean(): 단일 poll 함수에서 RX + TX 동시 처리
 * adapter->napi에 등록된 poll 콜백 */
static int e1000_clean(struct napi_struct *napi, int budget)
{
    struct e1000_adapter *adapter =
        container_of(napi, struct e1000_adapter, napi);
    int tx_clean_complete = 1, work_done = 0;

    /* TX completion 처리: budget 소비 없음 (TX는 work_done에 미포함) */
    e1000_clean_tx_irq(adapter->tx_ring);

    /* RX 처리: budget 한도 내에서 최대한 처리 */
    adapter->clean_rx(adapter, adapter->rx_ring, &work_done, budget);

    /* budget 다 소진: 큐에 패킷이 더 있을 가능성 → repoll */
    if (work_done < budget) {
        /* 큐가 비었음: 인터럽트 재활성화 + NAPI 해제 */
        if (adapter->itr_setting & 3)
            e1000_set_itr(adapter); /* 어댑티브 ITR 조정 */
        napi_complete_done(napi, work_done);
        if (!test_bit(__E1000_DOWN, &adapter->flags))
            ew32(IMS, IMS_ENABLE_MASK); /* 인터럽트 마스크 복원 */
    }

    return work_done;
}

/* 어댑티브 인터럽트 모더레이션(ITR): itr_setting 값에 따라 동적 조정
 *   itr_setting == 0: 인터럽트 모더레이션 없음 (최저 레이턴시)
 *   itr_setting == 1: 동적 모드 (패킷 크기/속도에 따라 자동 조정)
 *   itr_setting == 2: 고정 벌크 모드 (고처리량)
 *   itr_setting == 3: 고정 레이턴시 모드 (저레이턴시)
 */
static void e1000_set_itr(struct e1000_adapter *adapter)
{
    u16 current_itr;
    u32 new_itr = adapter->itr;

    if (!adapter->itr_setting)
        return;

    /* 패킷 크기에 따라 최적 ITR 계산
     * 소형 패킷(< 512B): 저레이턴시 ITR (높은 인터럽트 빈도)
     * 대형 패킷(>= 512B): 고처리량 ITR (낮은 인터럽트 빈도) */
    if (adapter->rx_ring->total_packets <= 10) {
        current_itr = MINIMUM_LATENCY_ITR;
    } else if (adapter->rx_ring->total_bytes /
               adapter->rx_ring->total_packets < 512) {
        current_itr = LOWEST_LATENCY_ITR;
    } else {
        current_itr = BULK_LATENCY_ITR;
    }

    if (current_itr != adapter->itr) {
        adapter->itr = current_itr;
        ew32(ITR, 1000000000 / (current_itr * 256));
    }
}
e1000e itr_setting 튜닝: ethtool -C eth0 rx-usecs <값>으로 인터럽트 병합(Coalescing) 시간을 직접 설정할 수 있습니다. 값이 0이면 모더레이션 없음(최저 레이턴시), 100~200μs면 고처리량 모드입니다. 어댑티브 모드(adaptive-rx on)는 트래픽 패턴에 따라 자동으로 전환됩니다.

igb: 멀티큐 NAPI의 교과서적 구현

igb는 Intel I350/I210 계열 1GbE NIC 드라이버입니다. 큐(Queue)별로 독립적인 napi_struct를 보유하며, MSI-X(Message Signaled Interrupts eXtended) 벡터를 각 큐에 1:1로 매핑합니다. 이를 통해 큐마다 서로 다른 CPU에서 독립적으로 NAPI poll이 실행되어 멀티코어 시스템에서 선형 처리량 확장이 가능합니다.

/* drivers/net/ethernet/intel/igb/igb_main.c */

/* igb_ring 구조체: 큐당 하나씩 생성, napi_struct 내장 */
struct igb_ring {
    struct napi_struct q_vector_napi;  /* 내장 NAPI (q_vector 통해 접근) */
    struct igb_q_vector *q_vector;    /* 소속 큐 벡터 포인터 */
    u8 __iomem *tail;                  /* HW 큐 테일 레지스터 */
    struct igb_tx_buffer *tx_buffer_info;
    struct igb_rx_buffer *rx_buffer_info;
    unsigned long state;
    u16 count;           /* 디스크립터 링 크기 */
    u16 next_to_use;     /* 생산자 인덱스 */
    u16 next_to_clean;   /* 소비자 인덱스 */
    u16 next_to_alloc;   /* 다음 버퍼 할당 위치 */
};

/* igb_q_vector: MSI-X 벡터 하나에 대응하는 큐 벡터
 * TX 링 1개 + RX 링 1개를 묶어서 단일 napi_struct로 관리 */
struct igb_q_vector {
    struct igb_adapter *adapter;
    int cpu;             /* 이 벡터가 처리되는 CPU (-1=무관) */
    u32 eims_value;      /* MSI-X 벡터 마스크 비트 */
    struct napi_struct napi;  /* 이 벡터의 NAPI 인스턴스 */
    struct igb_ring_container rx, tx;
    u16 itr_val;         /* 현재 ITR 레지스터 값 */
    u8 itr_update;
    struct rcu_head rcu;
    char name[25];       /* "igb-ethX-TxRx-N" 형식 */
    /* NUMA 정렬을 위한 캐시 라인 패딩 */
    cpumask_t affinity_mask;
};

/* MSI-X 큐별 인터럽트 핸들러: q_vector마다 고유 핸들러 등록 */
static irqreturn_t igb_msix_ring(int irq, void *data)
{
    struct igb_q_vector *q_vector = data;

    /* 이 벡터의 인터럽트 원인 클리어 (EICR 레지스터) */
    igb_write_itr(q_vector);

    /* 해당 q_vector의 NAPI를 poll_list에 등록
     * napi_schedule_irqoff: IRQ-off 상태에서 호출되는 최적화 버전 */
    napi_schedule_irqoff(&q_vector->napi);

    return IRQ_HANDLED;
}

/* igb_poll(): 큐 벡터별 poll 함수 — TX 완료 후 RX 처리 */
static int igb_poll(struct napi_struct *napi, int budget)
{
    struct igb_q_vector *q_vector =
        container_of(napi, struct igb_q_vector, napi);
    bool clean_complete = true;
    int work_done = 0;

    if (IS_ENABLED(CONFIG_IGB_DCA))
        igb_update_dca(q_vector);  /* DCA: 데이터를 CPU 캐시로 프리페치 */

    /* 1단계: TX 완료 처리 (budget 소비 없음)
     * clean_complete: TX 큐가 완전히 비었으면 true */
    if (q_vector->tx.ring)
        clean_complete = igb_clean_tx_irq(q_vector, budget);

    /* 2단계: RX 처리 (budget 내에서 최대한) */
    if (q_vector->rx.ring) {
        int cleaned = igb_clean_rx_irq(q_vector, budget);

        work_done += cleaned;
        if (cleaned >= budget)
            clean_complete = false;
    }

    /* 3단계: 완료 여부 확인 및 인터럽트 재활성화 */
    if (budget && clean_complete) {
        if (q_vector->rx.ring && q_vector->rx.ring->xsk_pool) {
            /* AF_XDP 소켓 사용 중: 추가 처리 필요 여부 확인 */
            if (igb_xsk_any_tx_pending(q_vector))
                goto &q_vector->napi;  /* repoll */
        }

        /* NAPI 완료: IRQ 재활성화 + poll_list에서 제거 */
        if (napi_complete_done(napi, work_done)) {
            u32 eims_value = q_vector->eims_value;

            if (!(q_vector->adapter->flags & IGB_FLAG_NEED_CTX_IDX) &&
                test_bit(IGB_RING_FLAG_RX_ALLOC_FAILED,
                          &q_vector->rx.ring->flags))
                eims_value &= ~q_vector->eims_value;

            /* MSI-X 벡터 인터럽트 재활성화 */
            igb_wr32(E1000_EIMS, eims_value);
        }
    }

    return work_done;
}

/* igb_open(): NAPI 초기화 — 큐 벡터별로 napi_struct 등록 */
static int igb_open(struct net_device *netdev)
{
    struct igb_adapter *adapter = netdev_priv(netdev);
    int i;

    /* 큐 벡터 수만큼 napi_struct 등록 */
    igb_for_each_q_vector(adapter, i) {
        struct igb_q_vector *qv = adapter->q_vector[i];
        netif_napi_add(netdev, &qv->napi, igb_poll);
    }

    /* IRQ 요청, 큐 할당, NAPI 활성화 */
    igb_setup_all_tx_resources(adapter);
    igb_setup_all_rx_resources(adapter);
    igb_configure(adapter);
    igb_request_irq(adapter);

    igb_for_each_q_vector(adapter, i)
        napi_enable(&adapter->q_vector[i]->napi);

    netif_tx_start_all_queues(netdev);
    return 0;
}

e1000e vs igb vs ixgbe vs ice NAPI 구현 진화 비교

항목 e1000e (1GbE) igb (1GbE 멀티큐) ixgbe (10GbE) ice (25/100GbE)
대상 NIC Intel PRO/1000 Intel I350/I210 Intel X520/X540 Intel E810/E830
큐 수 1 (Tx/Rx 공유) 최대 8큐 최대 64큐 최대 256큐
napi_struct 위치 e1000_adapter.napi (단일) igb_q_vector.napi (큐별) ixgbe_q_vector.napi (큐별) ice_q_vector.napi (큐별)
인터럽트 핸들러 e1000_intr_msi() (단일) igb_msix_ring() (큐별) ixgbe_msix_clean_rings() ice_msix_clean_rings()
poll 함수 e1000_clean() igb_poll() ixgbe_poll() ice_napi_poll()
TX/RX 처리 순서 TX → RX (함께) TX → RX (함께) TX → RX (함께) TX → RX (분리 가능)
인터럽트 모더레이션 어댑티브 ITR (itr_setting) 어댑티브 ITR (q_vector.itr) 동적 ITR (eitr) 동적 ITR + 인터럽트 병합
DCA 지원 미지원 지원 (CONFIG_IGB_DCA) 지원 (CONFIG_IXGBE_DCA) 미지원 (ADQ 사용)
XDP 지원 미지원 부분 지원 (AF_XDP) XDP_TX, XDP_REDIRECT 완전 지원 (ADQ + XDP)
page_pool 사용 미사용 미사용 미사용 (skb_alloc) 사용 (ice_rx_ring.page_pool)
스레드 NAPI 커널 공통 지원 커널 공통 지원 커널 공통 지원 커널 공통 지원
napi_complete_done 호출 위치 work_done < budget 시 clean_complete == true 시 clean_complete == true 시 clean_complete == true 시
e1000e — 단일 큐 NAPI Intel PRO/1000 NIC 단일 Tx/Rx 링 버퍼 MSI 단일 인터럽트 e1000_intr_msi() napi_struct (1개) adapter->napi poll: e1000_clean() CPU 0 (단일) softIRQ 처리 e1000_clean() TX 완료 + RX 수신 순차 처리 (단일 루프) igb — 멀티큐 NAPI Intel I350/I210 NIC 큐 0~N 독립 Tx/Rx 링 버퍼 MSI-X 큐 0 igb_msix_ring() MSI-X 큐 1 igb_msix_ring() napi_struct #0 q_vector[0].napi poll: igb_poll() napi_struct #1 q_vector[1].napi poll: igb_poll() CPU 0 softIRQ 큐 0 CPU 1 softIRQ 큐 1 igb_poll() #0 TX 완료 → RX 독립 실행 igb_poll() #1 TX 완료 → RX 독립 실행 병목: 단일 CPU 바인딩 모든 패킷 → CPU 0 처리 장점: 큐별 CPU 병렬 처리 RSS로 큐별 CPU 고정 가능
igb와 e1000e 드라이버 소스 위치:
  • drivers/net/ethernet/intel/e1000e/netdev.c — e1000e 메인 네트워크 드라이버
  • drivers/net/ethernet/intel/igb/igb_main.c — igb 메인 드라이버, igb_poll() 구현
  • drivers/net/ethernet/intel/ixgbe/ixgbe_main.c — ixgbe 드라이버 (10GbE)
  • drivers/net/ethernet/intel/ice/ice_main.c — ice 드라이버 (25/100GbE)
make M=drivers/net/ethernet/intel/igb로 igb 모듈만 빌드할 수 있습니다.
필수 관련 문서:
  • DPDK — DPDK (Data Plane Development Kit) — EAL, PMD, rte_
  • AF_XDP (XDP Sockets) — Linux 커널 AF_XDP 소켓 — xsk_socket, UMEM, XDP_SHARED_
참고 문서: