전체 흐름
net_rx_action()의 softirq 경로는 napi_poll()을 호출한다. __napi_poll()은 스케줄된 NAPI의 n->poll callback을 조건부 호출하며, 드라이버 NAPI라면 실제 장치 ring 처리는 그 callback의 책임이다. 코어 backlog NAPI와 threaded NAPI·busy-poll 경로도 있으므로 그림을 모든 RX의 필수 호출열로 읽지 않는다.
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을 구분해 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.
GitHub 원본:
대표 코드
__netif_receive_skb_core
__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
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 형식을 사용한다.
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를 찾는다.
napi->gro_hash · napi->rx_list
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 경로에서 달라진다.
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 스케줄 정책은 driver별이다. core napi_poll()에는 NIC ring 직접 청소 코드가 없다. core backlog NAPI도 존재한다.
NORMAL은 normal list, HELD/MERGED는 지연·병합이다. MERGED_FREE는 병합된 입력 skb를 해제하며 별도 packet loss를 뜻하지 않는다. 이 수만으로 drop을 계산할 수 없다.
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 분기였는가?