대부분의 device(nic..) 들은 두가지 방법으로 커널과 통신한다.
- 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
'네트워크 보안 > 네트워크' 카테고리의 다른 글
커널 네트워크 스택 #2 데이터 링크 계층 (0) | 2024.11.18 |
---|---|
네트워크 디바이스 드라이버별 벤더 NIC (0) | 2024.11.18 |
커널 네트워크 스택 #1 NIC 드라이버에서 패킷 수신 (0) | 2024.11.18 |
커널 네트워크 스택 #0 초기화 (0) | 2024.11.17 |
strongswan #5 IKE SA INIT, IKE_AUTH, CHILD_SA 설정 (0) | 2024.11.10 |