네트워크 보안/네트워크
커널 네트워크 스택 #1 NIC 드라이버에서 패킷 수신
uzguns
2024. 11. 18. 09:21
패킷 수신
- NIC는 네트워크로부터 데이터를 수신
- NIC는 DMA를 사용하여 네트워크 데이터를 RAM에 씀
- NIC는 CPU 개입 없이 DMA를 사용해 패킷 데이터를 RAM의 링 버퍼로 전송
- 링 버퍼는 네트워크 스택에서 패킷 처리를 위해 사용되는 메모리 영역
- NIC가 IRQ를 발생
- probe 함수에서 request_irq 핸들러 등록됨
- NIC 드라이버의 등록된 IRQ 핸들러가 실행
- IRQ는 NIC에서 지워져서 새로운 패킷이 도착할 때 IRQ를 생성할 수 있음
- NAPI(NIC Polling) SoftIRQ 폴 루프가 napi_schedule 호출로 시작 됨
패킷 처리
net_rx_action 함수(ksoftirqd 커널 스레드에서 호출됨)는 현재 CPU의 poll_list에 추가된 NAPI poll 구조체를 처리하기 시작한다.
poll_list에 poll 구조체가 추가되는 경우는 두 가지 일반적인 상황이 있다.
- 디바이스 드라이버에서 napi_schedule을 호출한 경우
- 프로세서 간 인터럽트(Inter-processor Interrupt, IPI)를 통해, Receive Packet Steering(RPS)에서 패킷 처리를 위해 IPI를 사용하는 경우
드라이버의 NAPI 구조체가 poll_list에서 검색되는 과정은 다음과 같다.
- net_rx_action루프는 NAPI 구조에 대한 NAPI 폴 목록을 확인하는 것으로 시작한다.
- SoftIRQ가 CPU 시간을 독점하지 않도록 **작업량(budget)**과 **경과 시간(elapsed time)**을 확인한다.
- 등록된 poll 함수가 호출된다.
- 드라이버의 poll 함수는 RAM의 링 버퍼에서 패킷을 수집 한다.
- 수집된 패킷은 napi_gro_receive로 전달되며, 여기에서 **Generic Receive Offloading(GRO)**이 처리될 수 있다 .
- 패킷은 GRO를 위해 보류되거나, 체인이 종료되며, 또는 netif_receive_skb로 전달되어 프로토콜 스택(IP, TCP/UDP 등)으로 올라 간다.
네트워크 데이터 처리는 netif_receive_skb( __netif_receive_skb_core )에서 계속되지만, 데이터 처리 경로는 Receive Packet Steering (RPS)이 활성화되어 있는지 여부에 따라 달라진다.
기본적으로 Linux 커널에서는 RPS가 기본적으로 활성화되어 있지 않으며, 사용하려면 명시적으로 활성화하고 구성해야한다.
get_rps_cpu
- RPS 맵 또는 플로우 테이블을 참조하여 패킷 처리를 담당할 CPU를 결정
- 소켓 플로우 테이블이나 RX 큐 플로우 테이블에서 이전 패킷 처리가 이루어진 CPU를 기반으로 CPU를 선택
- CPU가 오프라인 상태이거나 유효하지 않으면 다른 CPU를 선택
입력
- dev: 패킷을 수신한 네트워크 디바이스
- skb: 수신된 패킷
- rflowp: 선택된 플로우에 대한 정보를 반환
출력
- 선택된 CPU의 ID를 반환. 실패 시 -1
RX 큐의 인덱스 확인
if (skb_rx_queue_recorded(skb)) {
u16 index = skb_get_rx_queue(skb);
if (unlikely(index >= dev->real_num_rx_queues)) {
WARN_ONCE(dev->real_num_rx_queues > 1,
"%s received packet on queue %u, but number "
"of RX queues is %u\n",
dev->name, index, dev->real_num_rx_queues);
goto done;
}
rxqueue += index;
}
- 패킷이 RX 큐에서 처리되었는지 확인
- skb_get_rx_queue로 RX 큐 인덱스를 가져옴
- RX 큐 인덱스가 네트워크 디바이스의 RX 큐 수(real_num_rx_queues)를 초과하면 경고를 출력하고 종료
RPS 맵 및 플로우 테이블 확인
flow_table = rcu_dereference(rxqueue->rps_flow_table);
map = rcu_dereference(rxqueue->rps_map);
if (!flow_table && !map)
goto done;
- RX 큐에 RPS 맵(rps_map) 또는 플로우 테이블(rps_flow_table)이 활성화되었는지 확인
- 둘 다 활성화되지 않은 경우, 처리할 CPU가 없으므로 종료
패킷 해시 계산
skb_reset_network_header(skb);
hash = skb_get_hash(skb);
if (!hash)
goto done;
- 네트워크 헤더를 리셋하고, 패킷의 해시 값을 계산
- 해시 값은 RPS 맵 또는 플로우 테이블에서 CPU를 선택하는 데 사용
- 해시 값이 없으면 종료
소켓 플로우 테이블에서 CPU 검색
sock_flow_table = rcu_dereference(rps_sock_flow_table);
if (flow_table && sock_flow_table) {
ident = sock_flow_table->ents[hash & sock_flow_table->mask];
if ((ident ^ hash) & ~rps_cpu_mask)
goto try_rps;
next_cpu = ident & rps_cpu_mask;
rflow = &flow_table->flows[hash & flow_table->mask];
tcpu = rflow->cpu;
if (unlikely(tcpu != next_cpu) &&
(tcpu >= nr_cpu_ids || !cpu_online(tcpu) ||
((int)(per_cpu(softnet_data, tcpu).input_queue_head -
rflow->last_qtail)) >= 0)) {
tcpu = next_cpu;
rflow = set_rps_cpu(dev, skb, rflow, next_cpu);
}
if (tcpu < nr_cpu_ids && cpu_online(tcpu)) {
*rflowp = rflow;
cpu = tcpu;
goto done;
}
}
- 소켓 플로우 테이블(rps_sock_flow_table)에서 해시 값을 기반으로 CPU를 선택
- 현재 CPU(tcpu)와 다음 CPU(next_cpu)가 다르거나, 현재 CPU가 유효하지 않은 경우 다음 CPU로 이동
- 조건
- 현재 CPU가 유효하지 않은 경우(tcpu >= nr_cpu_ids).
- 현재 CPU가 오프라인 상태인 경우(!cpu_online(tcpu))
- 이전 패킷의 큐 상태(last_qtail)가 만료된 경우
- 유효한 CPU가 선택되면 종료
RPS 맵에서 CPU 검색
try_rps:
if (map) {
tcpu = rps_table_lookup(map, hash);
if (cpu_online(tcpu)) {
cpu = tcpu;
goto done;
}
}
- 소켓 플로우 테이블에서 유효한 CPU를 찾지 못한 경우, RPS 맵에서 CPU를 검색
- RPS 맵의 해시 기반 조회 함수(rps_table_lookup)를 사용하여 CPU를 선택
- 선택된 CPU가 온라인 상태인지 확인 후 종료
CPU 반환
done:
return cpu;
- 최종적으로 선택된 CPU를 반환. 유효한 CPU가 없으면 -1 반환
RPS가 비활성화된 경우
- netif_receive_skb가 데이터를 __netif_receive_core로 전달
- __netif_receive_core는 데이터를 Tap(예: PCAP)으로 전달
- 이후, __netif_receive_core는 데이터를 등록된 프로토콜 계층 핸들러로 전달
- 대부분의 경우, IPv4 프로토콜 스택에 등록된 ip_rcv 함수로 전달
RPS가 활성화된 경우
- netif_receive_skb가 데이터를 enqueue_to_backlog로 전달
- 패킷은 각 CPU별 입력 큐에 저장됨
- 원격 CPU의 NAPI 구조체가 해당 CPU의 poll_list에 추가되고, IPI(프로세서 간 인터럽트)가 큐에 추가됨
- 이를 통해 원격 CPU의 ksoftirqd 커널 스레드가 실행 중이 아니면 이를 깨움
- 원격 CPU의 ksoftirqd 커널 스레드가 실행되면, 이전 섹션에서 설명된 패턴을 따름
- 단, 이 경우 등록된 poll 함수는 process_backlog이며, 이 함수는 현재 CPU의 입력 큐에서 패킷을 수집함
- 패킷은 __netif_receive_skb_core로 전달
- __netif_receive_core는 데이터를 Tap(예: PCAP)으로 전달
- 이후, __netif_receive_core는 데이터를 등록된 프로토콜 계층 핸들러로 전달
- 대부분의 경우, IPv4 프로토콜 스택에 등록된 ip_rcv 함수로 전달
References
- https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=07580f3dbc687e3927f9e41a2fcf074f033f416b
- https://ggaaooppeenngg.github.io/zh-CN/2016/11/21/kernel-%E5%8D%8F%E8%AE%AE%E6%A0%88%E9%93%BE%E8%B7%AF%E5%B1%82%E5%88%86%E6%9E%90/
- https://maxnilz.com/docs/004-network/005-linux-rx/
- https://seongsee.tistory.com/9
- https://nitw.tistory.com/148
- ㄴㅇㄴㅇ
- ㅇㄹㄴㅇㄹ