이 코드는 어떤 문제를 푸나요?
NIC가 메모리에 받은 패킷은 드라이버와 NAPI를 거쳐 네트워크 계층으로 올라옵니다. 이때 공통 수신 처리는 VLAN·브리지·프로토콜 분류 등으로 패킷과 수신 장치를 바꿀 수도 있습니다. 아래 __netif_receive_skb_one_core 전체는 그 공통 처리를 거친 뒤 마지막 프로토콜 수신 함수에 넘기는 경계를 보여 줍니다. NAPI와 GRO의 전체 구현을 이 짧은 함수 하나로 대신하지는 않습니다.
읽을 범위: v6.18.37 · net/core/dev.c · __netif_receive_skb_one_core 6110–6121행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.
먼저 알아둘 개념
sk_buff
패킷 데이터와 메타데이터를 관리하는 객체입니다. skb 포인터와 실제 데이터 시작 주소는 같은 개념이 아닙니다.
NAPI와 GRO
NAPI는 패킷 처리를 일정 작업량 단위로 폴링하고 GRO는 합칠 수 있는 흐름의 패킷을 묶어 후속 비용을 줄입니다. 모든 패킷이 항상 합쳐지는 것은 아닙니다.
packet_type
프로토콜과 연결된 수신 처리 정보를 나타냅니다. 여기서 pt_prev는 공통 수신 단계가 마지막으로 남긴 처리 대상입니다.
원래 장치와 현재 장치
수신 처리 도중 논리 장치를 거칠 수 있으므로 처음 들어온 장치와 현재 skb->dev가 다를 수 있습니다. 두 값을 모두 전달하는 데 이유가 있습니다.
처음 읽을 때
수신 화살표를 NIC에서 사용자 프로그램으로 한 번에 이어 그리지 마십시오. 드라이버, 공통 네트워크 처리, IP 계층, 전송 계층, 소켓 큐를 거쳐야 하며 이 함수는 그중 공통 처리와 IP 등의 프로토콜 경계에 있습니다.
더 깊이 살펴볼 때
__netif_receive_skb_core가 skb의 주소를 받으므로 포인터 자체를 바꾸거나 패킷을 소비할 수 있다는 점을 보십시오. pt_prev가 남았는지 확인한 뒤에만 최종 프로토콜 콜백을 실행하며, pfmemalloc은 메모리 회수용 비상 자원으로 받은 패킷의 취급을 구분합니다.
그림으로 보는 변화

1. 수신 패킷을 넘깁니다
NAPI 등의 앞 단계가 준비한 skb가 이 공통 수신 경계에 도착합니다.
화살표는 패킷 처리 책임이 다음 계층으로 넘어가는 방향입니다.
2. 공통 처리를 실행합니다
원래 장치를 보관한 뒤 패킷 분류와 장치별 처리를 수행합니다.
화살표는 공통 처리의 실행 순서이며 skb 데이터 전체를 복사한다는 뜻은 아닙니다.
3. 프로토콜 함수에 전달합니다
마지막 수신 대상이 남아 있으면 IPv4·IPv6 등의 콜백을 실행합니다.
분기 화살표는 선택된 하나의 프로토콜 처리이며 모든 프로토콜을 동시에 호출하지 않습니다.
__netif_receive_skb_one_core를 한 줄씩 읽기
줄 번호는 v6.18.37 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.
static int __netif_receive_skb_one_core(struct sk_buff *skb, bool pfmemalloc)
{
struct net_device *orig_dev = skb->dev;
struct packet_type *pt_prev = NULL;
int ret;
ret = __netif_receive_skb_core(&skb, pfmemalloc, &pt_prev);
if (pt_prev)
ret = INDIRECT_CALL_INET(pt_prev->func, ipv6_rcv, ip_rcv, skb,
skb->dev, pt_prev, orig_dev);
return ret;
}static int __netif_receive_skb_one_core(struct sk_buff *skb, bool pfmemalloc)패킷 하나와 비상 메모리 할당 여부를 받아 공통 수신 처리와 마지막 프로토콜 호출을 연결합니다.
struct net_device *orig_dev = skb->dev;수신 처리 중 skb->dev가 바뀌어도 원래 입력 장치를 기억하도록 별도로 보관합니다.
struct packet_type *pt_prev = NULL;아직 최종 프로토콜 처리 대상을 정하지 않았다는 뜻으로 NULL을 둡니다.
int ret;공통 처리 또는 프로토콜 함수의 반환 상태를 담습니다.
ret = __netif_receive_skb_core(&skb, pfmemalloc, &pt_prev);skb 포인터의 주소를 전달해 공통 처리에서 포인터를 갱신할 수 있게 합니다. pfmemalloc 조건과 최종 처리 대상 pt_prev도 함께 전달합니다.
if (pt_prev)공통 처리 뒤 실제로 호출할 프로토콜 수신 함수가 남아 있는지 확인합니다.
ret = INDIRECT_CALL_INET(pt_prev->func, ipv6_rcv, ip_rcv, skb,IPv6·IPv4의 흔한 수신 함수에는 간접 호출 최적화를 적용하고 그 밖에는 등록된 함수를 호출합니다. 셋을 모두 실행하는 매크로가 아닙니다.
skb->dev, pt_prev, orig_dev);현재 장치, 선택한 packet_type과 처음 입력 장치를 최종 수신 함수에 전달합니다.
return ret;패킷 처리 상태를 상위 수신 경로에 반환합니다. 사용자에게 파일 데이터를 반환하는 위치는 아닙니다.
함께 생각해 볼 질문
pt_prev가 NULL이면 ip_rcv를 무조건 불러야 하나요?
아닙니다. 공통 처리에서 패킷을 소비하거나 다른 처리를 끝냈을 수 있으므로 이 코드는 최종 콜백을 생략합니다.
skb->dev만 넘기면 왜 부족할 수 있나요?
논리 장치 처리를 거치며 현재 장치가 달라질 수 있어 처음 들어온 장치 정보도 필요합니다.
여기서 반환되면 사용자 read가 완료됐나요?
아닙니다. 프로토콜 처리, 소켓 큐 전달과 사용자 읽기는 이후의 별도 단계입니다.
출처와 읽은 범위
Linux stable v6.18.37 · net/core/dev.c
해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고
