네트워크 보안/네트워크

커널 네트워크 스택 #1 NIC 드라이버에서 패킷 수신

uzguns 2024. 11. 18. 09:21

패킷 수신

  1. NIC는 네트워크로부터 데이터를 수신
  2. NIC는 DMA를 사용하여 네트워크 데이터를 RAM에 씀
    • NIC는 CPU 개입 없이 DMA를 사용해 패킷 데이터를 RAM의 링 버퍼로 전송
    • 링 버퍼는 네트워크 스택에서 패킷 처리를 위해 사용되는 메모리 영역
  3. NIC가 IRQ를 발생
    • probe 함수에서 request_irq 핸들러 등록됨
  4. NIC 드라이버의 등록된 IRQ 핸들러가 실행
  5. IRQ는 NIC에서 지워져서 새로운 패킷이 도착할 때 IRQ를 생성할 수 있음
  6. 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에서 검색되는 과정은 다음과 같다.

  1. net_rx_action루프는 NAPI 구조에 대한 NAPI 폴 목록을 확인하는 것으로 시작한다.
  2. SoftIRQ가 CPU 시간을 독점하지 않도록 **작업량(budget)**과 **경과 시간(elapsed time)**을 확인한다.
  3. 등록된 poll 함수가 호출된다.
  4. 드라이버의 poll 함수는 RAM의 링 버퍼에서 패킷을 수집 한다.
  5. 수집된 패킷은 napi_gro_receive로 전달되며, 여기에서 **Generic Receive Offloading(GRO)**이 처리될 수 있다 .
  6. 패킷은 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가 비활성화된 경우

  1. netif_receive_skb가 데이터를 __netif_receive_core로 전달
  2. __netif_receive_core는 데이터를 Tap(예: PCAP)으로 전달
  3. 이후, __netif_receive_core는 데이터를 등록된 프로토콜 계층 핸들러로 전달
    • 대부분의 경우, IPv4 프로토콜 스택에 등록된 ip_rcv 함수로 전달

RPS가 활성화된 경우

  1. netif_receive_skb가 데이터를 enqueue_to_backlog로 전달
  2. 패킷은 각 CPU별 입력 큐에 저장됨
  3. 원격 CPU의 NAPI 구조체가 해당 CPU의 poll_list에 추가되고, IPI(프로세서 간 인터럽트)가 큐에 추가됨
    • 이를 통해 원격 CPU의 ksoftirqd 커널 스레드가 실행 중이 아니면 이를 깨움
  4. 원격 CPU의 ksoftirqd 커널 스레드가 실행되면, 이전 섹션에서 설명된 패턴을 따름
    • 단, 이 경우 등록된 poll 함수는 process_backlog이며, 이 함수는 현재 CPU의 입력 큐에서 패킷을 수집함
  5. 패킷은 __netif_receive_skb_core로 전달
  6. __netif_receive_core는 데이터를 Tap(예: PCAP)으로 전달
  7. 이후, __netif_receive_core는 데이터를 등록된 프로토콜 계층 핸들러로 전달
    • 대부분의 경우, IPv4 프로토콜 스택에 등록된 ip_rcv 함수로 전달

 

 

References