이 페이지는 위 원문 경로의 전문 번역입니다. 식별자, 함수명, sysfs·sysctl 이름, netlink attribute와 코드 블록은 원문 표기를 유지했습니다.
개요
napi.rst:9-23NAPI는 Linux 네트워크 스택이 사용하는 이벤트 처리 기구다. 현재 NAPI라는 이름은 특별한 어떤 문구의 약자를 뜻하지 않는다.
기본 동작에서 장치는 새 이벤트가 생겼음을 interrupt로 host에 알린다. 그러면 host는 해당 이벤트를 처리할 NAPI instance를 schedule한다. 먼저 interrupt를 받지 않고 NAPI를 통해 장치의 이벤트를 poll할 수도 있는데, 이를 busy polling이라 한다.
NAPI 처리는 보통 software interrupt context에서 실행된다. 다만 NAPI 처리를 별도 kernel thread에서 수행하도록 선택할 수도 있다.
결국 NAPI는 packet Rx와 Tx 이벤트를 어떤 context와 설정으로 처리할지를 driver에서 분리해 추상화한다.
Driver API
napi.rst:25-32
NAPI에서 가장 중요한 두 요소는 struct napi_struct와 그에 연결된
poll method다. struct napi_struct는 NAPI instance의
상태를 보관하고, poll method는 driver에 종속된 event handler다.
일반적으로 이 method는 전송이 끝난 Tx packet을 해제하고 새로 수신한 packet을 처리한다.
Control API
napi.rst:36-52
netif_napi_add()와 netif_napi_del()은 NAPI instance를
system에 추가하거나 제거한다. instance는 인수로 전달된 netdevice에
연결되며, netdevice가 unregister될 때 자동으로 삭제된다. 새 instance는
disabled 상태로 추가된다.
napi_enable()과 napi_disable()은 disabled 상태를
관리한다. disabled NAPI는 schedule될 수 없고, 그 poll method가
호출되지 않음이 보장된다. napi_disable()은 NAPI instance의 ownership이
해제될 때까지 기다린다.
control API는 idempotent하지 않다. control API 호출과 datapath API의 동시 실행은
안전하지만, control API 자체의 호출 순서가 잘못되면 crash, deadlock 또는 race
condition이 생길 수 있다. 예를 들어 napi_disable()을 연달아 여러 번
호출하면 deadlock이 발생한다.
Datapath API
napi.rst:54-98
napi_schedule()은 NAPI poll을 schedule하는 기본 함수다. driver는
interrupt handler에서 이 함수를 호출해야 한다. napi_schedule() 호출에
성공하면 해당 호출이 NAPI instance의 ownership을 획득한다.
NAPI가 schedule된 뒤에는 event와 packet을 처리하기 위해 driver의
poll method가 호출된다. 이 method는 budget 인수를 받는다.
driver는 완료된 Tx packet을 개수 제한 없이 처리할 수 있지만, Rx packet은
budget 개까지만 처리해야 한다. 일반적으로 Rx 처리가 훨씬 비싸기 때문이다.
다시 말해 Rx 경로에서 budget은 한 번의 poll로 driver가 처리할 수 있는
packet 수를 제한한다. budget이 0이면 page pool이나 XDP 같은 Rx 전용
API를 전혀 사용할 수 없다. skb Tx 처리는 budget 값과 무관하게 수행해야
하지만, 값이 0일 때 driver는 XDP API나 page pool API를 호출할 수 없다.
budget이 0일 수 있다.
poll method는 실제로 처리한 work의 양을 반환한다. 아직 처리할 일이
남아 있다면, 예를 들어 budget을 모두 소진했다면 정확히
budget을 반환해야 한다. 이 경우 NAPI instance는 별도의 schedule 없이
다시 service되고 poll된다.
모든 outstanding packet을 처리해 event 처리가 끝났다면, poll method는
반환하기 전에 napi_complete_done()을 호출해야 한다.
napi_complete_done()은 instance의 ownership을 해제한다.
budget만큼 일한 경우는 조심해서
다뤄야 한다. 이 드문 조건을 stack에 직접 보고할 방법이 없으므로, driver는
napi_complete_done()을 호출하지 않고 다음 호출을 기다리거나
budget - 1을 반환해야 한다. budget이 0이면
napi_complete_done()을 절대로 호출하면 안 된다.
호출 순서
napi.rst:100-113
driver는 정확한 호출 순서를 가정해서는 안 된다. instance가 disabled 상태가 아니라면,
driver가 직접 instance를 schedule하지 않았어도 poll method가 호출될 수
있다. 반대로 napi_schedule()이 성공했어도, 예를 들어 곧바로 instance가
disable되면 poll method가 실제로 호출된다는 보장은 없다.
앞의 Control API 절에서 설명했듯이 napi_disable()과 이후의
poll method 호출은 poll method 자체가 끝날 때가 아니라
instance의 ownership이 해제될 때까지만 기다린다. 따라서 driver는
napi_complete_done() 호출 뒤에 어떤 data structure에도 접근하지
않도록 해야 한다.
스케줄링과 IRQ masking
napi.rst:117-148driver는 NAPI instance를 schedule한 뒤 NAPI polling이 끝날 때까지 interrupt를 masked 상태로 유지해야 한다. 그동안 추가 interrupt는 필요하지 않다.
장치가 IRQ를 자동으로 mask하지 않아 driver가 명시적으로 interrupt를 mask해야 한다면,
napi_schedule_prep()과 __napi_schedule()을 사용해야 한다.
if (napi_schedule_prep(&v->napi)) {
mydrv_mask_rxtx_irq(v->idx);
/* race를 피하려고 mask한 다음 schedule한다 */
__napi_schedule(&v->napi);
}
IRQ는 napi_complete_done() 호출이 성공한 뒤에만 unmask해야 한다.
if (budget && napi_complete_done(&v->napi, work_done)) {
mydrv_unmask_rxtx_irq(v->idx);
return min(work_done, budget - 1);
}
napi_schedule_irqoff()은 IRQ context에서 호출된다는 보장, 즉
interrupt를 따로 mask할 필요가 없다는 점을 이용하는 napi_schedule()
variant다. PREEMPT_RT가 활성화된 경우처럼 IRQ가 threaded 방식이면
napi_schedule_irqoff()은 napi_schedule()로 fallback한다.
인스턴스와 큐 매핑
napi.rst:150-172
최신 장치는 interface 하나에 여러 NAPI instance, 즉 여러
struct napi_struct를 둔다. instance를 queue와 interrupt에 매핑하는
강제 규칙은 없다. NAPI는 사용자에게 노출되는 특정 의미를 정의하기보다 polling과
processing을 추상화한다. 그럼에도 실제 network device는 대체로 비슷한 구성을 사용한다.
가장 흔한 구성은 NAPI instance, interrupt, queue pair가 1:1:1로 대응하는 것이다. 여기서 queue pair는 Rx queue 하나와 Tx queue 하나의 묶음이다.
덜 흔한 구성에서는 NAPI instance 하나가 여러 queue를 처리하거나, 한 core에서 Rx queue와 Tx queue를 서로 다른 NAPI instance가 처리할 수 있다. queue 배치가 어떻든 NAPI instance와 interrupt 사이에는 보통 1:1 대응이 유지된다.
ethtool API는 channel이라는 용어를 쓰며 각 channel은
rx, tx, combined 중 하나일 수 있다.
channel의 의미가 엄밀히 정해진 것은 아니지만, 권장 해석은 특정 종류의 queue를
service하는 IRQ/NAPI로 이해하는 것이다. 예를 들어 rx 1개,
tx 1개, combined 1개 구성은 interrupt 3개, Rx queue
2개, Tx queue 2개를 사용하는 것으로 예상한다.
영속 NAPI 설정
napi.rst:174-187
driver는 NAPI instance를 동적으로 할당하고 해제하는 경우가 많다. 그러면 instance가
다시 할당될 때마다 NAPI 관련 사용자 설정이 사라진다.
netif_napi_add_config() API는 queue number 같은 driver 정의 index를
기준으로 각 NAPI instance를 영속 NAPI 설정과 연결해 이 손실을 막는다.
이 API를 사용하면 NAPI ID를 비롯한 여러 설정을 영속적으로 유지할 수 있다.
SO_INCOMING_NAPI_ID를 사용하는 userspace program에 특히 유용하다.
가능한 driver는 netif_napi_add_config()를 사용해야 한다.
User API
napi.rst:189-210
사용자가 NAPI와 상호작용할 때에는 NAPI instance ID를 사용한다. 이 ID는
SO_INCOMING_NAPI_ID socket option을 통해서만 사용자에게 보인다.
사용자는 netlink로 장치 또는 장치 queue의 NAPI ID를 조회할 수 있다. userspace
application에서 직접 구현하거나 kernel source tree의
tools/net/ynl/pyynl/cli.py script를 사용할 수 있다.
다음은 장치의 모든 queue를 dump해 각 queue의 NAPI ID를 확인하는 예다.
$ kernel-source/tools/net/ynl/pyynl/cli.py \
--spec Documentation/netlink/specs/netdev.yaml \
--dump queue-get \
--json='{"ifindex": 2}'
사용할 수 있는 operation과 attribute의 자세한 내용은
Documentation/netlink/specs/netdev.yaml을 참고한다.
Software IRQ coalescing
napi.rst:212-251NAPI는 기본적으로 명시적인 event coalescing을 수행하지 않는다. 대부분의 경우 batching은 장치의 IRQ coalescing 때문에 자연스럽게 생긴다. 하지만 software coalescing이 도움이 되는 경우도 있다.
모든 packet을 처리한 즉시 hardware interrupt를 unmask하는 대신, NAPI가 repoll
timer를 arm하도록 설정할 수 있다. netdevice의 gro_flush_timeout
sysfs 설정을 timer 지연 시간으로 재사용하고, napi_defer_hard_irqs는
NAPI가 포기하고 hardware IRQ 사용으로 돌아가기 전까지 연속으로 허용할 empty poll
횟수를 정한다.
이 값은 netdev-genl netlink를 사용해 NAPI별로 설정할 수도 있다. NAPI별 netlink
설정에서는 underscore 대신 hyphen을 쓴 gro-flush-timeout과
napi-defer-hard-irqs라는 이름을 사용한다.
NAPI별 설정은 userspace application에서 직접 수행하거나 kernel source tree의
tools/net/ynl/pyynl/cli.py를 사용할 수 있다.
$ kernel-source/tools/net/ynl/pyynl/cli.py \
--spec Documentation/netlink/specs/netdev.yaml \
--do napi-set \
--json='{"id": 345,
"defer-hard-irqs": 111,
"gro-flush-timeout": 11111}'
마찬가지로 irq-suspend-timeout은 netdev-genl netlink로 설정한다.
이 값에는 system 전체에 적용하는 sysfs parameter가 없다.
irq-suspend-timeout은 application이 IRQ를 완전히 suspend할 수 있는
시간을 정한다. epoll context별로 EPIOCSPARAMS ioctl을 사용해 설정하는
SO_PREFER_BUSY_POLL과 함께 사용한다.
Busy polling
napi.rst:255-266busy polling을 사용하면 user process가 device interrupt가 발생하기 전에 들어온 packet을 확인할 수 있다. 다른 busy polling과 마찬가지로 CPU cycle을 더 사용하는 대신 latency를 줄인다. NAPI busy polling이 production에서 얼마나 사용되는지는 잘 알려져 있지 않다.
선택한 socket에 SO_BUSY_POLL을 설정하거나 system 전체 sysctl인
net.core.busy_poll과 net.core.busy_read를 사용해
busy polling을 활성화한다. NAPI busy polling을 위한 io_uring API도 있다.
epoll 기반 busy polling
napi.rst:268-306
epoll_wait 호출에서 packet 처리를 직접 시작할 수 있다. 이 기능을
사용하려면 application이 하나의 epoll context에 추가한 모든 file descriptor가
같은 NAPI ID를 갖도록 해야 한다.
전용 acceptor thread를 사용하는 application은 SO_INCOMING_NAPI_ID로
들어온 connection의 NAPI ID를 얻은 뒤, 해당 file descriptor를 worker thread에
분배할 수 있다. worker는 받은 descriptor를 자신의 epoll context에 추가한다.
이 방식이면 각 worker thread의 epoll context에 같은 NAPI ID를 가진 FD만 들어간다.
다른 방법으로 SO_REUSEPORT를 사용한다면 BPF 또는 eBPF program을
삽입해, 각 thread가 동일한 NAPI ID의 incoming connection만 받도록 분배할 수 있다.
system에 NIC가 여러 개일 수 있는 경우를 주의해서 처리해야 한다.
busy polling을 활성화하는 방법은 두 가지다.
-
/proc/sys/net/core/busy_poll에 event를 기다리며 busy loop할 시간을 microsecond 단위로 설정한다. system 전체 설정이므로 모든 epoll 기반 application이epoll_wait을 호출할 때 busy poll한다. busy polling이 필요 없는 application까지 영향을 받으므로 바람직하지 않을 수 있다. -
최신 kernel에서는 epoll context file descriptor에 ioctl을 실행해
struct epoll_params를 설정(EPIOCSPARAMS)하거나 읽을(EPIOCGPARAMS) 수 있다. user program은 구조체를 다음과 같이 정의할 수 있다.
struct epoll_params {
uint32_t busy_poll_usecs;
uint16_t busy_poll_budget;
uint8_t prefer_busy_poll;
/* 구조체 크기를 64bit 배수로 맞춘다 */
uint8_t __pad;
};
IRQ 완화
napi.rst:308-346busy polling은 low-latency application을 위한 기능이지만, 비슷한 기구를 IRQ 완화에도 사용할 수 있다. 초당 요청 수가 매우 많은 application, 특히 routing/forwarding application과 AF_XDP socket을 사용하는 application은 요청이나 packet batch 처리를 끝낼 때까지 interrupt를 받고 싶지 않을 수 있다.
이런 application은 주기적으로 busy polling을 수행하겠다고 kernel에 약속할 수 있고,
driver는 device IRQ를 계속 masked 상태로 유지해야 한다. 이 mode는
SO_PREFER_BUSY_POLL socket option으로 활성화한다. system 오동작을
막기 위해, busy poll 호출 없이 gro_flush_timeout이 지나면 이 약속은
취소된다.
epoll 기반 application에서는 struct epoll_params의
prefer_busy_poll을 1로 설정하고 EPIOCSPARAMS ioctl을
실행해 이 mode를 활성화한다.
일반 busy polling은 낮은 latency를 목표로 하므로 NAPI budget이 기본값보다 작다.
IRQ 완화에는 이 제한이 적용되지 않으며, SO_BUSY_POLL_BUDGET socket
option으로 budget을 조절할 수 있다. epoll 기반 application은
struct epoll_params의 busy_poll_budget을 원하는 값으로
바꾼 뒤, EPIOCSPARAMS ioctl로 특정 epoll context에 설정한다.
gro_flush_timeout을 크게 잡으면 IRQ를 미뤄 batching 효율을 높일 수
있지만, system 부하가 낮을 때 latency가 증가한다. 너무 작게 잡으면 busy polling을
시도하는 application을 device IRQ와 softirq 처리가 방해할 수 있다. 이 trade-off를
고려해 값을 신중히 선택해야 한다. epoll 기반 application은 적절한
maxevents 값을 선택해 user processing의 양을 완화할 수 있다.
이 trade-off를 다루는 다른 방법으로 IRQ suspension도 고려할 수 있다.
IRQ suspension
napi.rst:348-427
IRQ suspension은 epoll이 NAPI packet 처리를 시작하는 동안 device IRQ를 mask하는
기구다. application의 epoll_wait 호출이 event를 성공적으로 가져오는
동안 kernel은 IRQ suspension timer를 뒤로 미룬다. busy polling 중 event가 없으면,
예를 들어 network traffic이 줄면 IRQ suspension을 해제하고 앞에서 설명한 IRQ
완화 방식을 사용한다. 이로써 CPU 사용량과 network 처리 효율 사이의 균형을 맞출 수 있다.
이 기구를 사용하려면 다음과 같이 설정한다.
-
NAPI별
irq-suspend-timeout을 application이 IRQ를 suspend할 수 있는 최대 시간으로, nanosecond 단위로 설정한다. 앞에서 설명한 netlink를 사용한다. application이 멈춘 경우 driver의 interrupt 처리를 다시 시작하기 위한 안전장치다.epoll_wait에서 받은 data를 application이 처리하는 시간을 덮을 수 있게 잡아야 한다. application은epoll_wait의max_events로 가져오는 data 양을 조절할 수 있다는 점도 고려한다. -
sysfs 또는 NAPI별 설정인
gro_flush_timeout과napi_defer_hard_irqs를 작은 값으로 설정할 수 있다. busy poll에서 data를 찾지 못한 뒤 IRQ를 미루는 데 이 값들이 사용된다. -
prefer_busy_pollflag를 true로 설정해야 한다. 앞에서 설명한EPIOCSPARAMSioctl을 사용한다. - application은 앞에서 설명한 epoll 방식으로 NAPI packet 처리를 시작한다.
이후의 epoll_wait 호출이 계속 userspace에 event를 반환하는 동안
irq-suspend-timeout은 계속 뒤로 밀리고 IRQ는 disabled 상태를 유지한다.
application은 interrupt의 간섭 없이 data를 처리할 수 있다.
epoll_wait에서 event를 찾지 못하면 IRQ suspension은 자동으로
해제되고 gro_flush_timeout과 napi_defer_hard_irqs
완화 기구가 이어받는다. irq-suspend-timeout은 한 번의 userspace 처리
주기 동안 IRQ를 suspend해야 하므로 gro_flush_timeout보다 훨씬 큰 값으로
설정할 것으로 예상한다.
IRQ suspension에 napi_defer_hard_irqs와
gro_flush_timeout이 반드시 필요한 것은 아니지만, 함께 사용할 것을
강하게 권장한다.
IRQ suspension을 사용하면 system은 polling mode와 IRQ 기반 packet delivery 사이를
오간다. 부하가 높은 동안에는 irq-suspend-timeout이
gro_flush_timeout보다 우선해 busy polling을 유지한다. epoll에서 event를
찾지 못하면 gro_flush_timeout과 napi_defer_hard_irqs
설정이 다음 동작을 결정한다.
network 처리와 packet delivery에는 본질적으로 다음 세 loop가 있다.
hardirq -> softirq -> napi poll: 기본 interrupt deliverytimer -> softirq -> napi poll: 지연된 IRQ 처리epoll -> busy-poll -> napi poll: busy loop
gro_flush_timeout과 napi_defer_hard_irqs를 설정하면
loop 2가 loop 1의 제어권을 가져갈 수 있다. 같은 값을 설정했을 때 loop 2와 loop 3은
서로 제어권을 차지하려 한다. 부하가 높은 동안에는
irq-suspend-timeout이 loop 2의 timer로 사용되어 packet 처리를 loop 3
쪽으로 기울인다.
gro_flush_timeout과 napi_defer_hard_irqs를 설정하지 않으면
loop 3은 loop 1의 제어권을 가져갈 수 없다. 따라서 두 값을 설정하는 방식이 권장된다.
그렇지 않으면 irq-suspend-timeout을 설정해도 눈에 띄는 효과가 없을 수 있다.
Threaded NAPI
napi.rst:431-453
Threaded NAPI는 NAPI 처리를 software IRQ context가 아니라 전용 kernel thread에서
수행하는 mode다. threaded NAPI instance마다
napi/${ifc-name}-${napi-id}라는 별도 thread가 하나씩 생성된다.
각 kernel thread는 interrupt를 처리하는 CPU와 같은 단일 CPU에 pin할 것을 권장한다. IRQ와 NAPI instance의 매핑은 단순하지 않을 수 있으며 driver에 따라 달라진다. NAPI instance ID는 kernel thread의 process ID와 반대 순서로 배정된다.
netdev의 sysfs directory에 있는 threaded 파일에 0 또는 1을 써서
threaded NAPI를 제어한다. netlink interface로 특정 NAPI에만 활성화할 수도 있다.
$ ynl --family netdev --do napi-set --json='{"id": 66, "threaded": 1}'
각주: NAPI는 Linux 2.4 시기에 처음에는 New API라고 불렸다.