uzguns 2024. 11. 18. 13:49

 

대부분의 device(nic..) 들은 두가지 방법으로 커널과 통신한다.

[출처] https://maxnilz.com/docs/004-network/005-linux-rx/

  • polling
    • 커널 측에서 수행
    • 커널이 장치의 레지스터를 읽어 주기적으로 패킷의 수신 여부를 확인
    • 장치가 새로운 패킷을 가지고 있을 경우 이를 처리
    • CPU 리소스 절약이 가능하지만, 불필요한 리소스 소모 가능
  • interrupt
    • device 측에서 수행
    • 장치가 새로운 패킷을 받을 경우 하드웨어 IRQ를 사용하여 커널에게 알려 주는 방식
    • 커널이 IRQ를 받게 되면, 해당 IRQ를 처리 하기 위해서 미리 등록된 디바이스 드라이버의 인터럽트의 수신 패킷 처리 루틴(Interrupt Handler)을 호출
    • 인터럽트 핸들러(top half handler)는 수신한 패킷을 복사하고 큐에 넣은 후
    • softirq를 사용하여 커널이 해당 패킷을 처리 할 수 있도록 한다.
    • 커널의 softirq 핸들러(bottom half handler)는 해당 패킷을 네트워크 프로토콜 해당 장비의 특성(브릿지 또는 라우터 , 스위치 여부 )과 네트워크 레이어에 따라 에 맞게 처리
    • 낮은 트래픽 부하에서는 최적의 방식이지만, 높은 부하에서는 패킷 마다 인터럽트를 발생 시키기 때문에 receive live lock 위험이 존재.

NAPI 는 인터럽트 기반과 폴링 기반 방식의 장점을 결합한 혼합된 방식이다.

현재 디바이스 드라이버(커널 측)가 바쁜 상황에서는 인터럽트를 사용하지 않고, 디바이스 드라이버에서 Polling 방식을 사용하도록한다.

 

이 방식에서 디바이스 드라이버는 "첫 번째" 패킷이 수신될 때 인터럽트를 생성한다. 이후 인터럽트 핸들러는 폴링 목록에 해당 장치를 추가한 뒤, 남은 프레임 처리를 위해 커널에 알린다. 이후 추가적인 인터럽트는 비활성화된다.

패킷 처리시 인터럽트를 막고, 패킷 처리가 끝 난 후 폴링 방식으로 다른 패킷이 도착 했는지 검사 하는 방식이다.

 

net_rx_action 함수는 napi_poll() 함수를 호출하여 network device driver 의 콜백함수를 호출하는 구조를 가졌다.

static int __napi_poll(struct napi_struct *n, bool *repoll)
{
  int work, weight;

  weight = n->weight; // NAPI에서 처리할 최대 작업량 (Budget).

  /*
   * NAPI_STATE_SCHED 확인: NAPI가 스케줄링된 상태인지 확인.
   * 스케줄링 상태일 때만 드라이버의 poll() 호출.
   */
  work = 0;
  if (test_bit(NAPI_STATE_SCHED, &n->state)) {
    work = n->poll(n, weight); // 드라이버의 poll() 함수 호출.
    trace_napi_poll(n, work, weight); // 작업량 추적.
  }

  /*
   * 작업량이 weight 초과 시 경고 출력.
   * 이는 드라이버의 잘못된 구현을 나타냄.
   */
  if (unlikely(work > weight))
    pr_err_once("NAPI poll function %pS returned %d, exceeding its budget of %d.\n",
        n->poll, work, weight);

  /*
   * 작업량이 weight 미만일 경우: 작업 완료. 더 이상 스케줄링 필요 없음.
   */
  if (likely(work < weight))
    return work;

  /*
   * NAPI가 비활성화 상태로 전환 예정일 경우, NAPI를 완료하고 반환.
   */
  if (unlikely(napi_disable_pending(n))) {
    napi_complete(n); // NAPI 상태를 완료로 설정.
    return work;
  }

  /*
   * Generic Receive Offload(GRO) 처리:
   * 오래된 패킷을 플러시하여 패킷 처리를 최적화.
   */
  if (n->gro_list) {
    napi_gro_flush(n, HZ >= 1000); // HZ 값이 낮으면 모든 패킷 플러시.
  }

  /*
   * poll_list에 NAPI가 재스케줄링된 경우 경고 출력.
   */
  if (unlikely(!list_empty(&n->poll_list))) {
    pr_warn_once("%s: Budget exhausted after napi rescheduled\n",
        n->dev ? n->dev->name : "backlog");
    return work;
  }

  /*
   * 더 처리할 패킷이 예상되는 경우, repoll 플래그 설정.
   * 이후 NAPI가 다시 스케줄링될 수 있도록 요청.
   */
  *repoll = true;

  return work; // 작업량 반환.
}

 

network device driver 의 n->poll()과 연결된 콜백함수를 호출하여 poll()에 연결된 driver 의 특정함수에서 polling 방식으로 network 기기의 버퍼를 주기적으로 확인하여 packet을 처리하게 된다. 

 

그 후 buffer의 모든 패킷들이 network device driver 에서 처리되었다면 driver 의 폴링 함수를 빠져나와 다시 interrupt 모드를 타게 된다. 

 


NAPI 준수 네트워크 드라이버를 생성하기 위해 필요한 작업들을 알아보자

 

1. struct napi_struct의 생성 및 초기화

각 인터럽트 벡터에 대해 드라이버는 struct napi_struct의 인스턴스를 할당해야한다. 

이 작업은 특별한 함수 호출 없이 수행되며, 보통 드라이버의 프라이빗 구조체에 포함된다.

각 napi_struct는 반드시 네트워크 장치 자체보다 먼저 초기화 및 등록되어야 하며, 이는 netif_napi_add()를 통해 수행된다.

반대로 네트워크 장치가 해제된 후에는 netif_napi_del()로 등록을 해제해야한다.

 

2. 인터럽트 핸들러 수정

인터럽트 핸들러를 일부 수정해야한다. 

만약 새로운 패킷이 수신되어 인터럽트가 발생한 경우, 그 패킷을 즉시 처리해서는 안된다.

대신, 드라이버는 추가적인 "패킷 수신" 인터럽트를 비활성화하고 네트워킹 서브시스템에 드라이버를 곧 폴링(polling)하여 모든 수신된 패킷을 가져가도록 요청해야한다.

이때, 인터럽트를 비활성화하는 작업은 하드웨어와 드라이버 간의 특정 작업이므로 하드웨어에 따라 다르다.

폴링을 요청하는 작업은 다음 함수를 호출하여 수행된다.

void napi_schedule(struct napi_struct *napi);

 

일부 드라이버에서는 아래와 같은 대체 형식을 사용할 수도 있다.

if (napi_schedule_prep(napi))
    __napi_schedule(napi);

결과는 동일하다. (napi_schedule_prep()이 0을 반환하면 이미 폴링이 예약되어 있으므로 추가적인 인터럽트를 수신하지 않아야한다.)

 

3. poll() 메서드 작성

드라이버의 poll() 메서드를 작성해야한다. 이 메서드는 네트워크 인터페이스에서 패킷을 가져와 커널로 전달하는 역할을 한다. poll() 함수의 프로토타입은 다음과 같다.

int (*poll)(struct napi_struct *napi, int budget);

poll() 함수는 기존에 인터럽트 핸들러에서 수행했던 것처럼 모든 수신된 패킷을 처리해야 한다. 하지만 몇 가지 예외가 있다.

  • netif_rx() 대신 netif_receive_skb() 사용
    • 패킷은 netif_rx()가 아니라 아래 함수로 전달해야한다.
int netif_receive_skb(struct sk_buff *skb);
  • 작업 예산 제한
    • budget 매개변수는 드라이버가 수행할 수 있는 작업의 양을 제한한다.
    • 수신된 각 패킷은 작업 1 단위로 계산된다.
    • poll() 함수가 TX 완료(TX completion)를 처리할 경우, TX 링 전체를 처리했다면 그 작업도 나머지 예산으로 간주해야한다.
    • 그렇지 않은 경우, TX 완료 작업은 예산에 포함되지 않는다.
  • 반환값
    • poll() 함수는 수행한 작업량을 반환해야한다.
  • 인터럽트 재활성화 조건
    • 반환값이 budget보다 작은 경우에만 드라이버는 인터럽트를 다시 활성화하고 폴링을 중지해야한다.
    • 폴링은 아래 함수를 사용하여 중지한다.
void napi_complete(struct napi_struct *napi);

네트워킹 서브시스템은 동일한 napi_struct에 대해 여러 프로세서에서 동시에 poll()을 호출하지 않을 것을 보장한다.

 

4. poll() 메서드 등록

마지막 단계는 poll() 메서드를 네트워킹 서브시스템에 알리는 것이다. 이는 napi_struct를 등록할 때 수행된다.

netif_napi_add(dev, &napi, my_poll, 16);

여기서 마지막 매개변수인 weight는 이 인터페이스의 중요도를 나타내며, poll()의 budget 인수에 동일한 값으로 전달된다. 기가비트 또는 더 빠른 어댑터 드라이버는 일반적으로 weight 값을 64로 설정된다. 느린 매체(slower media)의 경우 더 작은 값을 사용할 수 있다.

 

NAPI(Network API)를 사용하려면 다음과 같은 기능이 반드시 하드웨어에서 제공되어야한다.

 

필수 하드웨어 요구사항

  • DMA 링(DMA ring) 또는 소프트웨어 디바이스에서 패킷을 저장할 충분한 RAM
  • 인터럽트를 비활성화할 수 있는 기능 또는 스택으로 패킷을 전송하는 이벤트를 중단할 수 있는 기능.

NAPI에서의 poll() 처리

NAPI는 패킷 이벤트를 napi->poll() 메서드에서 처리한다. 

일반적으로 수신 패킷 이벤트만 napi->poll()에서 처리된다. 

나머지 이벤트는 일반 인터럽트 핸들러에서 처리하여 처리 지연(latency)을 줄이는 것이 좋다. 

이는 수신 이벤트 외의 다른 이벤트가 많지 않다는 점에서도 정당화된다.

단, NAPI는 napi->poll()이 반드시 수신 이벤트만 처리해야 한다고 강제하지 않는다.

 

 

References