들어가며
우리가 매일 사용하는 서버 애플리케이션은 수많은 네트워크 요청을 처리합니다. 그렇다면 이 요청들, 즉 네트워크 패킷은 서버의 네트워크 카드(NIC)에 도착해서부터 우리가 작성한 애플리케이션 코드까지 어떤 경로를 거쳐 전달될까요?

위 그림은 패킷이 NIC에서 애플리케이션까지 도달하는 전체 여정을 추상화한 것입니다. 이번 글에서는 첫 번째 관문, NIC에서 커널 메모리의 링 버퍼(Ring Buffer)까지 데이터가 이동하는 과정에 대해 자세히 살펴보겠습니다.

환경
- NIC : Intel X710 시리즈
- 커널 : Linux 5.15
☕️ 핵심 구성 요소: NIC, DMA, 그리고 링 버퍼
데이터 수신 과정을 이해하기 위해 먼저 세 가지 핵심 구성 요소를 알아야 합니다.
- NIC (Network Interface Card)
네트워크 인터페이스 카드는 우리 컴퓨터가 외부 네트워크와 통신할 수 있게 해주는 하드웨어 장치입니다. 물리적인 네트워크 선을 통해 들어온 전기 신호(L1)를 디지털 데이터(0과 1)로 변환하고, 이더넷 프레임(L2)을 검증하여 메모리로 전달할 준비를 합니다. - DMA (Direct Memory Access)
DMA는 NIC가 CPU의 도움 없이 직접 시스템 메모리(RAM)에 데이터를 쓰고 읽을 수 있게 하는 기술입니다. 만약 NIC가 수신한 모든 데이터를 CPU가 일일이 옮겨야 한다면 CPU는 다른 중요한 작업을 처리하지 못하고 병목 현상이 발생할 것입니다. DMA 덕분에 NIC는 수신한 이더넷 프레임을 CPU를 거치지 않고 바로 지정된 메모리 영역(링 버퍼)으로 효율적으로 복사할 수 있습니다. - 링 버퍼 (Ring Buffer)
링 버퍼는 NIC와 커널(정확히는 디바이스 드라이버) 사이의 핵심적인 데이터 교환 메커니즘입니다. 이름에서 알 수 있듯이, 고정된 크기의 메모리 공간을 원형 큐(Circular Queue)처럼 사용하는 자료구조입니다.
인텔 e710 NIC 공식문서 8.3.3 Lan Receive Queue(Ring) 을 확인해보면
Received packets are posted to host memory through a set of queues. Each queue is a cyclic ring made of a sequence of receive descriptors in contiguous memory. These queues are also called 'descriptor rings'.
- 디스크립터(Descriptor) 배열: 링 버퍼는 여러 개의 '디스크립터'라는 작은 데이터 구조로 이루어진 배열입니다. 각 디스크립터는 수신된 이더넷 프레임이 저장될 메모리 버퍼의 주소와 상태 정보 등을 담고 있습니다.
- DMA의 목적지: NIC는 DMA를 사용해 수신한 프레임을 이 디스크립터가 가리키는 메모리 버퍼로 직접 전송합니다.
- 생산자-소비자 패턴: NIC는 프레임을 링 버퍼에 쓰는 '생산자' 역할을 하고, 커널(드라이버)은 링 버퍼에서 프레임을 읽어 처리하는 '소비자' 역할을 합니다. 링 버퍼는 이 둘 사이의 속도 차이를 완충하고, 각자의 작업에 집중할 수 있도록 분리해줍니다.

링 버퍼는 물리적으로는 선형적인 배열이지만, Head와 Tail이라는 두 개의 포인터(또는 인덱스)를 사용하여 원형 큐처럼 동작합니다.위 그림을 통해 링 버퍼의 동작 방식을 좀 더 자세히 살펴보겠습니다. (수신 링 버퍼 기준)
- 디스크립터 (Descriptor): 그림의 각 네모칸 하나하나가 디스크립터입니다. 각 디스크립터는 실제 이더넷 프레임 데이터가 저장될 메모리 공간(버퍼)을 가리키는 포인터와 메타데이터(길이, 상태 등)를 포함합니다.
- Base & Length: 링 버퍼의 시작 주소와 전체 크기(디스크립터의 개수)를 나타냅니다.
- Head : NIC가 다음에 사용할 디스크립터 인덱스 (즉, 여기에 데이터를 쓸 예정).
- Tail : 드라이버가 NIC에게 "여기까지 디스크립터는 사용해도 좋다"고 알려준 마지막 인덱스.
🎲 Linux 커널의 링 버퍼 구현 (i40e 드라이버)
이제 실제 리눅스 커널 코드에서는 이 링 버퍼가 어떻게 표현되는지 살펴보겠습니다. Intel e710 NIC는 i40e 드라이버에 의해 관리됩니다. 관련 코드는 다음 경로에서 찾아볼 수 있습니다.
https://github.com/torvalds/linux/blob/v5.15/drivers/net/ethernet/intel/i40e/i40e_txrx.h#L320
/* Transmit and Receive queues */
struct i40e_ring {
void *desc; /* Descriptor ring memory */
dma_addr_t dma; /* DMA address of the ring */
u16 next_to_use; // Next descriptor to USE by SW (HW's Tail)
u16 next_to_clean; // Next descriptor to CLEAN by SW (HW's Head)
u16 reg_idx; /* HW register index of the queue */
u16 queue_index; /* tracks VSI queue_id */
u8 dcb_tc; /* DCB traffic class */
u16 count; /* Number of descriptors */
...
}
주요 멤버 변수들을 살펴보겠습니다 (수신 링 Rx 기준):
void *desc: 내부 디스크립터의 첫번째 주소를 가리키는 포인터 입니다.struct device *dev: NIC 를 가리키는 포인터 이다.u16 next_to_use (NTU): 드라이버가 다음에 사용할 디스크립터의 인덱스입니다.(그림의 Tail과 유사한 역할)u16 next_to_clean (NTC): 드라이버가 다음에 정리할 디스크립터의 인덱스입니다. (그림의 Head와 유사한 역할)
마무리
지금까지 네트워크 프레임이 NIC에 도착하여 커널의 링 버퍼에 안착하는 과정을 살펴보았습니다.
다음 글에서는 이 링 버퍼에 도착한 데이터가 어떻게 커널의 네트워크 스택(NAPI, SKB 생성 등)을 거쳐 애플리케이션까지 전달되는지 그 여정을 계속 따라가 보겠습니다.
Reference
https://www.intel.com/content/www/us/en/content-details/332464/intel-ethernet-controller-x710-xxv710-xl710-datasheet.html
https://github.com/torvalds/linux/blob/v5.15/drivers/net/ethernet/intel/i40e/i40e_txrx.h
'CS > Linux' 카테고리의 다른 글
| [Linux] recv()는 어떻게 잠들고 깨어날까? (0) | 2025.05.23 |
|---|---|
| [Linux] 네트워크 커널 스택: 리눅스 커널 네트워크 패킷 수신 여정 #3 (0) | 2025.05.22 |
| [Linux] Ring Buffer 에서 NAPI: 리눅스 커널 네트워크 패킷 수신 여정 #2 (0) | 2025.05.21 |
| [Linux] 페이지 테이블 구조 (0) | 2025.05.15 |
| 시스템 콜과 표준 라이브러리 관계 파헤치기: glibc는 어떻게 시스템 콜을 호출할까? (0) | 2025.05.09 |