Network · Linux v6.6 · source + diagram note

RX path

NAPI callback, GRO 분기와 조건부 protocol handler 전달.

전체 흐름

net_rx_action()의 softirq 경로는 napi_poll()을 호출한다. __napi_poll()은 스케줄된 NAPI의 n->poll callback을 조건부 호출하며, 드라이버 NAPI라면 실제 장치 ring 처리는 그 callback의 책임이다. 코어 backlog NAPI와 threaded NAPI·busy-poll 경로도 있으므로 그림을 모든 RX의 필수 호출열로 읽지 않는다.

그림 1. 코어 NAPI dispatcher와 poll callback의 경계
NAPI가 스케줄된 경우의 코어 경로
드라이버 IRQ 등 스케줄 조건은 드라이버별 구현
→
net_rx_action() softirq가 poll_list와 budget 처리
→
napi_poll() __napi_poll() 호출 · repoll 목록 관리
→
n->poll(n, weight) driver NAPI면 ring 작업; backlog NAPI 예외
napi_poll() 자체가 NIC ring에서 packet을 수거하는 함수는 아니다.
skb를 상위로 driver가 GRO 또는 직접 수신 API 선택
다른 결과 driver XDP·drop·redirect 등은 skb 전달 생략 가능

skb가 만들어진 뒤의 경로는 드라이버 선택과 packet 결과에 따라 달라진다.

층 고정 원문 책임
softirq dispatcher net_rx_action → napi_poll poll budget·시간 제한 관리
NAPI wrapper napi_poll → __napi_poll 목록·netpoll 잠금·repoll
NAPI callback n->poll(n, weight) 드라이버 NAPI의 ring 청소·skb 생성은 callback 구현에 따라; backlog NAPI는 별도
상위 수신 netif_receive_skb_internal / list_internal 직접 skb 또는 GRO normal batch가 진입

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: net/core/dev.c
Linux v6.6: net/core/gro.c
Linux v6.6: net/ipv4/ip_input.c

설명: 수신 dispatcher, GRO 처리, packet-type handler와 실제 드라이버 poll callback을 구분해 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.

대표 코드

__netif_receive_skb_core

원본 코드: net/core/dev.c:5340-5542 · v6.6 · 로컬 원문
__netif_receive_skb_core 함수 전체 (203줄)
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
				    struct packet_type **ppt_prev)
{
	struct packet_type *ptype, *pt_prev;
	rx_handler_func_t *rx_handler;
	struct sk_buff *skb = *pskb;
	struct net_device *orig_dev;
	bool deliver_exact = false;
	int ret = NET_RX_DROP;
	__be16 type;

	net_timestamp_check(!READ_ONCE(netdev_tstamp_prequeue), skb);

	trace_netif_receive_skb(skb);

	orig_dev = skb->dev;

	skb_reset_network_header(skb);
	if (!skb_transport_header_was_set(skb))
		skb_reset_transport_header(skb);
	skb_reset_mac_len(skb);

	pt_prev = NULL;

another_round:
	skb->skb_iif = skb->dev->ifindex;

	__this_cpu_inc(softnet_data.processed);

	if (static_branch_unlikely(&generic_xdp_needed_key)) {
		int ret2;

		migrate_disable();
		ret2 = do_xdp_generic(rcu_dereference(skb->dev->xdp_prog), skb);
		migrate_enable();

		if (ret2 != XDP_PASS) {
			ret = NET_RX_DROP;
			goto out;
		}
	}

	if (eth_type_vlan(skb->protocol)) {
		skb = skb_vlan_untag(skb);
		if (unlikely(!skb))
			goto out;
	}

	if (skb_skip_tc_classify(skb))
		goto skip_classify;

	if (pfmemalloc)
		goto skip_taps;

	list_for_each_entry_rcu(ptype, &ptype_all, list) {
		if (pt_prev)
			ret = deliver_skb(skb, pt_prev, orig_dev);
		pt_prev = ptype;
	}

	list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
		if (pt_prev)
			ret = deliver_skb(skb, pt_prev, orig_dev);
		pt_prev = ptype;
	}

skip_taps:
#ifdef CONFIG_NET_INGRESS
	if (static_branch_unlikely(&ingress_needed_key)) {
		bool another = false;

		nf_skip_egress(skb, true);
		skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev,
					 &another);
		if (another)
			goto another_round;
		if (!skb)
			goto out;

		nf_skip_egress(skb, false);
		if (nf_ingress(skb, &pt_prev, &ret, orig_dev) < 0)
			goto out;
	}
#endif
	skb_reset_redirect(skb);
skip_classify:
	if (pfmemalloc && !skb_pfmemalloc_protocol(skb))
		goto drop;

	if (skb_vlan_tag_present(skb)) {
		if (pt_prev) {
			ret = deliver_skb(skb, pt_prev, orig_dev);
			pt_prev = NULL;
		}
		if (vlan_do_receive(&skb))
			goto another_round;
		else if (unlikely(!skb))
			goto out;
	}

	rx_handler = rcu_dereference(skb->dev->rx_handler);
	if (rx_handler) {
		if (pt_prev) {
			ret = deliver_skb(skb, pt_prev, orig_dev);
			pt_prev = NULL;
		}
		switch (rx_handler(&skb)) {
		case RX_HANDLER_CONSUMED:
			ret = NET_RX_SUCCESS;
			goto out;
		case RX_HANDLER_ANOTHER:
			goto another_round;
		case RX_HANDLER_EXACT:
			deliver_exact = true;
			break;
		case RX_HANDLER_PASS:
			break;
		default:
			BUG();
		}
	}

	if (unlikely(skb_vlan_tag_present(skb)) && !netdev_uses_dsa(skb->dev)) {
check_vlan_id:
		if (skb_vlan_tag_get_id(skb)) {
			/* Vlan id is non 0 and vlan_do_receive() above couldn't
			 * find vlan device.
			 */
			skb->pkt_type = PACKET_OTHERHOST;
		} else if (eth_type_vlan(skb->protocol)) {
			/* Outer header is 802.1P with vlan 0, inner header is
			 * 802.1Q or 802.1AD and vlan_do_receive() above could
			 * not find vlan dev for vlan id 0.
			 */
			__vlan_hwaccel_clear_tag(skb);
			skb = skb_vlan_untag(skb);
			if (unlikely(!skb))
				goto out;
			if (vlan_do_receive(&skb))
				/* After stripping off 802.1P header with vlan 0
				 * vlan dev is found for inner header.
				 */
				goto another_round;
			else if (unlikely(!skb))
				goto out;
			else
				/* We have stripped outer 802.1P vlan 0 header.
				 * But could not find vlan dev.
				 * check again for vlan id to set OTHERHOST.
				 */
				goto check_vlan_id;
		}
		/* Note: we might in the future use prio bits
		 * and set skb->priority like in vlan_do_receive()
		 * For the time being, just ignore Priority Code Point
		 */
		__vlan_hwaccel_clear_tag(skb);
	}

	type = skb->protocol;

	/* deliver only exact match when indicated */
	if (likely(!deliver_exact)) {
		deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
				       &ptype_base[ntohs(type) &
						   PTYPE_HASH_MASK]);
	}

	deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
			       &orig_dev->ptype_specific);

	if (unlikely(skb->dev != orig_dev)) {
		deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
				       &skb->dev->ptype_specific);
	}

	if (pt_prev) {
		if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
			goto drop;
		*ppt_prev = pt_prev;
	} else {
drop:
		if (!deliver_exact)
			dev_core_stats_rx_dropped_inc(skb->dev);
		else
			dev_core_stats_rx_nohandler_inc(skb->dev);
		kfree_skb_reason(skb, SKB_DROP_REASON_UNHANDLED_PROTO);
		/* Jamal, now you will not able to escape explaining
		 * me how you were going to use this. :-)
		 */
		ret = NET_RX_DROP;
	}

out:
	/* The invariant here is that if *ppt_prev is not NULL
	 * then skb should also be non-NULL.
	 *
	 * Apparently *ppt_prev assignment above holds this invariant due to
	 * skb dereferencing near it.
	 */
	*pskb = skb;
	return ret;
}

수신 패킷을 여러 hook과 protocol handler로 전달하는 핵심 경로다. skb가 교체되거나 다른 경로로 넘어갈 수 있으므로 pskb와 pt_prev의 소유권을 함께 읽는다. 함수명만으로 XDP, VLAN, bridge, rx_handler 등의 모든 packet이 동일한 분기를 거친다고 가정하지 않는다.

napi_poll

원본 코드: net/core/dev.c:6613-6631 · v6.6 · 로컬 원문
static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
	bool do_repoll = false;
	void *have;
	int work;

	list_del_init(&n->poll_list);

	have = netpoll_poll_lock(n);

	work = __napi_poll(n, &do_repoll);

	if (do_repoll)
		list_add_tail(&n->poll_list, repoll);

	netpoll_poll_unlock(have);

	return work;
}

확인 사항: 이 wrapper는 poll_list에서 NAPI를 꺼내 __napi_poll()에 넘기고, do_repoll이 참일 때 repoll 목록에 다시 넣는다. NIC ring 접근은 드라이버 NAPI일 때 n->poll callback 구현에서 확인한다.

함수별 분석

대표 발췌 순서는 실제 호출 순서의 반대다. __netif_receive_skb_core()는 IRQ 완화나 NAPI schedule을 시작하지 않는다. __napi_poll()이 등록된 poll callback을 부르고, 드라이버가 상위 수신 API를 사용한 경우에 receive core에 도달한다. 코어 backlog callback도 같은 n->poll 형식을 사용한다.

그림 2. GRO 선택과 normal/held/merged 분기
드라이버가 GRO API를 택한 경우
n->poll callback driver가 skb를 마련
→
napi_gro_receive() 프로토콜 GRO handler가 합침 가능성 판단
→
결과 분기 NORMAL / HELD / MERGED / MERGED_FREE / CONSUMED
GRO_NORMAL normal RX list에 적재; batch/flush 때 상위 전달
HELD · MERGED · MERGED_FREE · CONSUMED 즉시 protocol 전달 아님; MERGED_FREE는 병합 뒤 입력 skb 해제
normal batch 또는 보류된 GRO skb가 flush되면 list 수신으로 넘어갈 수 있다.
GRO를 거치지 않는 대안
driver가 직접 수신 API 호출 netif_receive_skb() 등
→
RPS 선택 가능 다른 CPU backlog로 enqueue할 수 있음
→
receive core 단일 skb 또는 list 경로

GRO 결과 수가 줄어든 것은 곧바로 drop 수가 늘어난 뜻이 아니다. skb count·segment count·drop reason을 구분한다.

단계 코드 조건 수신 경로에서의 의미
poll callback test_bit(NAPI_STATE_SCHED, &n->state) → n->poll 코어 wrapper와 callback 작업 분리
GRO 진입 napi_gro_receive 구현 · 결과 switch GRO_NORMAL은 normal list, HELD/MERGED는 즉시 전달 아님; MERGED_FREE는 병합된 입력 skb 해제
normal batch gro_normal_one/list batch threshold 또는 flush에서 list_internal 호출
budget 소진 napi_gro_flush() + gro_normal_list(n) flush와 repoll은 조건부
GRO 없이 직접 수신 netif_receive_skb_internal RPS가 선택되면 backlog 경유 가능

구조체와 필드

sk_buff → napi_struct → gro_list → ptype tables는 소유권의 직렬 이동이 아니다. NAPI는 callback과 GRO 보류 상태를 가지고, skb는 별도 수신 API에서 packet-type handler를 찾는다.

그림 3. NAPI·GRO·skb·packet_type의 관계
struct napi_struct의 poll callback과 GRO 상태
napi->gro_hash · napi->rx_list
n->poll / n->weight 스케줄된 NAPI의 callback·budget; driver나 core backlog
GRO 상태 napi->gro_hash · napi->rx_list; 보류/normal skb가 별도로 남을 수 있음
struct sk_buff skb->dev, skb->protocol, skb_iif가 전달·재순회에 관여
packet_type / pt_prev tap·프로토콜 handler 후보를 순회하며 마지막 handler를 보류
rx_handler 등록된 경우 CONSUMED/ANOTHER/EXACT/PASS로 갈림
orig_dev 수신을 시작한 장치; skb->dev가 VLAN/handler로 바뀔 수 있음

GRO 정상 전달·보류·병합과 receive core의 handler 선택은 서로 다른 단계다.

객체·필드 원문 근거 읽는 법
n->poll / weight 스케줄 검사·callback driver NAPI라면 ring 처리 코드는 드라이버 측
GRO normal list napi->gro_hash · napi->rx_list 일정량/flush에서 receive list로
*pskb / skb->dev receive core와 another_round VLAN·rx_handler가 skb/장치를 바꿀 수 있음
pt_prev 최종 packet_type 보류 최종 대상을 남기면 callback 후보; 앞 단계 소비와 drop을 구분
tap 범위 &ptype_all 전역 ptype_all + 장치별 ptype_all

실행 단계와 상태 변화

단일 skb 수신은 RPS에 따라 backlog를 경유할 수 있고, list 수신은 GRO normal batch를 처리한다. 양쪽 모두 __netif_receive_skb_core에서 조건부 hook과 packet-type 목록을 살핀다. 최종 pt_prev callback 여부는 단일 경로와 list 경로에서 달라진다.

그림 4. receive core의 protocol dispatch와 중단 분기
단일 skb 또는 GRO normal list가 receive core로
netif_receive_skb(_list) timestamp/RPS 분기 가능
→
__netif_receive_skb_core generic XDP·VLAN·tc/nf·rx_handler 조건부
→
ptype 선택 protocol + device matching
VLAN/ingress/rx_handler는 우회·재순회·소비할 수 있어 모든 skb가 IP handler로 가지 않는다.
pt_prev 있음 단일 func 또는 list_func에 전달; ip_rcv/ipv6_rcv는 등록된 경우
없음 / 앞 단계 소비 최종 대상 부재의 drop 또는 앞 단계 소비·종료

protocol handler 전달은 L2 수신 뒤의 조건부 결과다. IP callback 호출도 socket queue나 userspace wakeup까지 보장하지 않는다.

사건 분기 근거
RPS 조건이 맞으면 다른 CPU backlog에 enqueue single / list
tap·ingress·VLAN pfmemalloc/설정 및 hook 결과에 따라 skip, drop, another_round tap / ingress·VLAN
rx_handler CONSUMED / ANOTHER / EXACT / PASS switch
protocol handler 공통 코어가 pt_prev를 남기면 func/list_func; NULL이면 이전 소비·drop 결과 유지 ptype 선택·drop / list dispatch

불변 조건과 실패 사례

v6.6에서는 최종 pt_prev가 있어도 skb_orphan_frags_rx() 실패 시 drop으로 갈 수 있다. 코어가 호출자에게 pt_prev를 넘긴 뒤의 최종 콜백과, 내부 후보가 존재하는 상태를 구분한다.

IRQ와 poll 분리

IRQ 스케줄 정책은 driver별이다. core napi_poll()에는 NIC ring 직접 청소 코드가 없다. core backlog NAPI도 존재한다.

GRO는 조건부

NORMAL은 normal list, HELD/MERGED는 지연·병합이다. MERGED_FREE는 병합된 입력 skb를 해제하며 별도 packet loss를 뜻하지 않는다. 이 수만으로 drop을 계산할 수 없다.

budget 소진

work가 weight에 닿으면 disable/busy-poll/repoll 등 조건을 확인한다. 소진만으로 driver 결함을 단정하지 않는다.

프로토콜 도달

generic XDP, ingress, VLAN, rx_handler가 skb를 소비·변경할 수 있다. 최종 drop 레이블의 집계는 deliver_exact가 거짓이면 rx_dropped, 참이면 rx_nohandler다. 최종 콜백이 없다는 사실만으로 drop이나 특정 카운터 증가를 단정하지 않는다.

계측과 검증

공유 tracefs 설정을 바꾸는 대신, 먼저 읽기 전용 counter와 고정 소스의 분기 조건을 대조한다.

관찰점 질문 한계
ip -s link show dev <ifname> driver와 상위 RX/drop counter의 변화가 다른가? 드라이버별 집계 기준이 다름
ethtool -S <ifname> ring·XDP·GRO 관련 카운터가 제공되는가? 이름·의미는 드라이버별
cat /proc/net/softnet_stat softnet backlog·time squeeze 징후가 있는가? 필드 해석은 커널 버전 코드와 대조
GRO/NAPI tracepoint가 이미 수집된 경우 poll work·budget과 GRO 결과가 다른가? tracepoint는 실행한 곳만 보여 주며 drop 원인 전체는 아님

추가 확인 사항

  • 이 드라이버의 IRQ는 언제 NAPI를 스케줄하고, 등록된 poll callback은 어떤 RX ring을 읽는가?
  • GRO_NORMAL, HELD, MERGED 중 어느 결과가 나왔으며 normal list는 언제 flush됐는가?
  • RPS, VLAN, ingress, rx_handler를 거친 뒤 최종 pt_prev handler가 있었는가? 없었다면 어느 drop/consume 분기였는가?