개요

리눅스의 대부분 Network 기기는 NAPI 모드(대부분의 자료에서 NAPI를 NAPI 모드라고 불르진 않지만,, 편의상 본페이지에서는 NAPI 모드라 부른다..ㅎ)를 지원한다.

NAPI(New API)란 Interrupt 방식과 Polling 방식이 결합된 구조로 동작하는 모드이다.

 

Network 기기에서 NAPI 방식을 채택하는 이유는 간단하다.

 

만약, 특정 기기의 GPIO가 Falling이나 Rising Edge를 통해 Interrupt를 발생시킨다면, CPU는 해당 Interrupt를 ISR(Interrupt Sevice Routine)로 처리한 뒤 Interrupt를 초기화시킬 것이다.

 

하지만, 매우 잦고 빠른 속도로 Packet의 Receive(하위 Rx로 표기)가 일어나는 Network 기기와 같은 곳에서는 Interrupt 방식만을 사용하는 것은 매우 비효율적인 동작이다. 그 이유는 모든 Rx Packet마다 ISR을 수행하게 된다면 다른 HW 기기들의 Interrupt가 올바른 시간에 처리되지 못하기 때문이다.

 

그렇다고 해서 Polling 방식만으로 Rx Packet들을 처리한다면, Network가 활발하지 않은 시간대에는 Polling에 불필요한 자원을 낭비하게 될 것이다.

 

그래서 Interrupt와 Polling 방식이 결합된 NAPI 모드가 등장하게 된 것이다.

NAPI 모드는 Network 기기가 한가할 땐 Interrupt로 동작하며 바쁠때는 Polling 방식으로 동작하는 모드이다.

 

NAPI 모드를 조금 더 자세히 설명하자면, Network 기기에 Packet들이 들어오게 되면, 가장 첫 Packet을 통해 Interrupt가 발생하게 된다. 그 후 Packet이 지속적으로 들어오는지 Polling 방식으로 Packet을 담는 Buffer를 확인한다. 시간이 지난 뒤 다시 Packet이 들어오지 않은 상태가 되면 Network 기기는 Interrupt 모드로 동작하게 된다.

 

이와 같은 방식으로 Network 기기는 NAPI 모드를 통해 자원의 낭비를 줄이게 된다.

 

net_rx_action() 함수

리눅스 기반의 Network 기기가 NAPI를 지원한다면, Packet이 Rx될 때 net_rx_action() 함수가 호출된다. 

 

그 후 Network 기기를 지원하는 Network Device Driver의 콜백 함수를 호출하게 되며, Driver가 Polling으로 Buffer에 Packet이 있는지 주기적으로 조회하게 된다. (Driver 내에서 Upper Layer로 Packet을 전달한다.)

 

아래는 2023년 07월 03일 기준 가장 최신 리눅스 커널(v6.4.1) 소스로 살펴본 내용이다.

 

/**
 * Path : net/core/dev.c 
 */

static __latent_entropy void net_rx_action(struct softirq_action *h)
{
  struct softnet_data *sd = this_cpu_ptr(&softnet_data);
  unsigned long time_limit = jiffies + usecs_to_jiffies(READ_ONCE(netdev_budget_usecs));
  int budget = READ_ONCE(netdev_budget);
  LIST_HEAD(list);
  LIST_HEAD(repoll);

start:
  sd->in_net_rx_action = true;
  local_irq_disable();
  list_splice_init(&sd->poll_list, &list);
  local_irq_enable();
  
  for (;;) {
    struct napi_struct *n;
    
    skb_defer_free_flush(sd);
    
    if (list_empty(&list)) {
      if (list_empty(&repoll)) {
        sd->in_net_rx_action = false;
        barrier();
        /* We need to check if ____napi_schedule()
         * had refilled poll_list while
         * sd->in_net_rx_action was true.
         */
        if (!list_empty(&sd->poll_list))
          goto start;
        if (!sd_has_rps_ipi_waiting(sd))
          goto end;
        }
        break;
      }
      
      n = list_first_entry(&list, struct napi_struct, poll_list);
      budget -= napi_poll(n, &repoll);
      
      /* If softirq window is exhausted then punt.
       * Allow this to run for 2 jiffies since which will allow
       * an average latency of 1.5/HZ.
       */
      if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit))) {
        sd->time_squeeze++;
        break;
      }
  }
  
  local_irq_disable();
  
  list_splice_tail_init(&sd->poll_list, &list);
  list_splice_tail(&repoll, &list);
  list_splice(&list, &sd->poll_list);
  if (!list_empty(&sd->poll_list))
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
  else
    sd->in_net_rx_action = false;
  
  net_rps_action_and_irq_enable(sd);
end:;
}

현재 기준 가장 최신 리눅스 커널의 Network Core 쪽 소스를 살펴보았을 때, net_rx_action() 함수는 napi_poll()을 호출하여 그 속에서 최종적으로 Network Device 드라이버의 콜백 함수를 호출하는 구조를 가졌다.

 

/**
 * Path : net/core/dev.c 
 */

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;
}

napi_poll() 함수 내부에서는 __napi_poll()을 또다시 호출하게 되며, 찐으로(ㅋㅋ..) 특정 Network Device Driver의 콜백 함수를 호출하게 된다.

 

/**
 * Path : net/core/dev.c 
 */

static int __napi_poll(struct napi_struct *n, bool *repoll)
{
  int work, weight;
  
  weight = n->weight;
  
  /* This NAPI_STATE_SCHED test is for avoiding a race
   * with netpoll's poll_napi().  Only the entity which
   * obtains the lock and sees NAPI_STATE_SCHED set will
   * actually make the ->poll() call.  Therefore we avoid
   * accidentally calling ->poll() when NAPI is not scheduled.
   */
   work = 0;
   if (test_bit(NAPI_STATE_SCHED, &n->state)) {
     work = n->poll(n, weight);
     trace_napi_poll(n, work, weight);
   }
   
   if (unlikely(work > weight))
     netdev_err_once(n->dev, "NAPI poll function %pS returned %d, exceeding its budget of %d.\n", n->poll, work, weight);
     
   if (likely(work < weight))
     return work;
   
   /* Drivers must not modify the NAPI state if they
    * consume the entire weight.  In such cases this code
    * still "owns" the NAPI instance and therefore can
    * move the instance around on the list at-will.
    */
   if (unlikely(napi_disable_pending(n))) {
     napi_complete(n);
     return work;
   }
   
   /* The NAPI context has more processing work, but busy-polling
    * is preferred. Exit early.
    */
   if (napi_prefer_busy_poll(n)) {
     if (napi_complete_done(n, work)) {
       /* If timeout is not set, we need to make sure
        * that the NAPI is re-scheduled.
        */
        napi_schedule(n);
      }
      return work;
    }
    
    if (n->gro_bitmask) {
      /* flush too old packets
       * If HZ < 1000, flush all packets.
       */
      napi_gro_flush(n, HZ >= 1000);
    }
    
    gro_normal_list(n);
    
    /* Some drivers may have called napi_schedule
     * prior to exhausting their budget.
     */
    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 = true;
    
    return work;
}

다음과 같이 최종적으로 특정 Network Device Driver의 n->poll()과 연결된 콜백함수를 호출하게 되며, poll()에 연결된 Driver의 특정 함수에서 polling 방식으로 Network 기기의 Buffer를 주기적으로 확인하여 Packet들을 처리하게 된다.

 

그 후 Buffer의 모든 Packet들이 Network Device Driver에서 처리되었다면, Driver의 Polling 함수를 빠져나와 다시 Interrupt 모드를 타게된다.

 

NAPI 구조체를 제대로 확인하지 않아서 어떤식으로 NAPI를 커널에 등록하는진 모르겠으나.. 내가 현재 다루고 있는 Network 기기의 Network Device Driver에서는 위와 같은 NAPI 방식으로 Packet을 처리하는 것을 커널 로그를 통해 확인할 수 있었다.

 

Refereces

https://nitw.tistory.com/148

https://seongsee.tistory.com/9

https://en.wikipedia.org/wiki/New_API

https://elixir.bootlin.com/linux/v6.4.1/source

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기