커널 네트워크 스택 #0 초기화
커널 네트워크 스택 요약
NIC 에서 패킷을 수신하면 DMA를 통해 커널의 메모리 영역에 존재하는 rx ring 버퍼 에 수신한 정보를 복사하고 이 후 CPU 에 인터럽트를 걸어 request를 보내면 CPU는 커널 인터럽트 함수를 수행한다.
irq 핸들러는 인터럽트 번호를 보고 드라이버 인터럽트 핸들러를 호출한다.
드라이버 인터럽트 핸들러 함수는 (napi_schedule()) 소프트웨어 인터럽트(softirq) 를 요청하는 일을 수행하는데 softirq 핸들러 함수가 net_rx_action()이다.
대략적으로 다음과 같다.
- 드라이버가 로드되고 초기화
- 패킷이 네트워크에서 NIC에 도착
- 패킷은 (DMA를 통해) 커널 메모리의 링 버퍼에 복사
- 패킷이 메모리에 있음을 시스템에 알리기 위해 하드웨어 인터럽트가 생성
- 드라이버는 폴 루프가 아직 실행 중이 아니면 폴 루프를 시작하기 위해 NAPI를 호출
- ksoftirqd 에 의해 패킷 처리
- 커널 소프트웨어 인터럽트를 처리하는 프로세스
- ksoftirqd 프로세스는 시스템의 각 CPU에서 실행된다. 부팅 시에 등록된다.
- ksoftirqd 프로세스는 장치 드라이버가 초기화 중에 등록한 NAPI 폴 함수를 호출하여 링 버퍼에서 패킷을 끌어온다.
- 네트워크 데이터가 쓰여진 링 버퍼의 메모리 영역은 매핑 해제
- 패킷 데이터가 커널 메모리에서 처리되면, DMA에 의해 매핑된 메모리 영역을 해제(unmap)하여 다시 사용 가능한 상태로 만든다.
- 메모리로 DMA된 데이터는 추가 처리를 위해 'skb'로 네트워킹 계층으로 전달
- 패킷 데이터는 skb(socket buffer)라는 커널 데이터 구조로 변환된다.
- 패킷 스티어링은 패킷 처리 부하를 여러 CPU(여러 수신 대기열이 있는 NIC 대신)로 분산
- 패킷 스티어링: CPU 간의 네트워크 처리 부하를 분산시키는 기술
- RPS(Receive Packet Steering): 소프트웨어 기반으로 CPU 부하를 분산
- RSS(Receive Side Scaling): NIC 하드웨어를 이용해 패킷을 여러 큐로 분산
- 패킷은 큐에서 프로토콜 계층으로 전달
- 프로토콜 계층은 소켓에 연결된 수신 버퍼에 이를 추가
먼저 초기화 과정부터 살펴보자.
네트워크 디바이스 드라이버 초기화
네트워크 장치가 사용 가능하려면 커널에 의해 인식되고 적절한 드라이버와 연결되어야한다.
드라이버는 module_init 매크로를 사용하여 초기화 함수를 등록하고 이 함수는 드라이버가 로드될 때 커널에 의해 호출된다.
static int __init i40e_init_module(void)
{
...
return pci_register_driver(&i40e_driver);
}
드라이버가 커널에 로드되면, 커널은 XX_init_module에서 pci_register_driver를 호출한다. 디바이스를 초기화 하는 대부분의 작업은 pci_register_driver 호출을 통해 이루어진다.
이를 통해 리눅스 커널은 XX_driver_name, XX_probe와 같은 드라이버의 세부 정보를 알게된다.
static struct pci_driver i40e_driver = {
.name = i40e_driver_name,
.id_table = i40e_pci_tbl,
.probe = i40e_probe,
/* more stuff */
}
PCI 초기화
Intel i350 의 NIC는 PCI Express Device이다.
PCI 디바이스는 PCI 구성 공간에 있는 일련의 레지스터를 통해 자신을 식별한다.
디바이스 드라이버가 컴파일될 때 MODULE_DEVICE_TABLE 매크로(include/module.h) 가 사용되어 디바이스 드라이버가 제어할 수 있는 PCI 디바이스 ID table을 내보낸다. 이 테이블은 구조체의 일부로도 사용된다.
커널은 이 테이블을 사용하여 어떤 디바이스 드라이버를 로드해 해당 디바이스를 제어해야하는지 결정한다.
이를 통해 os는 시스템에 연결된 디바이스와 해당 디바이스와 통신하기 위해 사용해야할 드라이버를 파악할 수 있다.
- PCI 장치 ID 테이블 정의
- 이 목록은 드라이버 소스 코드에 정의되며, PCI ID를 기반으로 드라이버가 해당 장치를 제어할 수 있음을 명시한다.
- PCI ID는 벤더 ID와 디바이스 ID로 구성되며, 이를 통해 특정 하드웨어 장치를 고유하게 식별할 수 있다.
- MODULE_DEVICE_TABLE 매크로는 특정 장치 드라이버가 지원할 수 있는 PCI 장치 ID 목록을 커널에 내보낸다.
/* i40e_pci_tbl - PCI Device ID Table
*
* Last entry must be all 0s
*
* { Vendor ID, Device ID, SubVendor ID, SubDevice ID,
* Class, Class Mask, private data (not used) }
*/
static const struct pci_device_id i40e_pci_tbl[] = {
{PCI_VDEVICE(INTEL, I40E_DEV_ID_SFP_XL710), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_QEMU), 0},
...
}
MODULE_DEVICE_TABLE(pci, i40e_pci_tbl);
driver load/pci 요약
- 드라이버가 로드되면, 보통 초기화 함수에서 pci_register_driver가 호출된다.
- pci_register_driver -> driver_register -> bus_add_driver 순서로 함수가 호출된다.
- 대부분의 작업은 bus_add_driver에 위임하고 pci_register_driver, driver_register 함수는 드라이버를 PCI 서브시스템에 등록하는 작업을한다.
- bus_add_driver 에서 실제로 버스 레벨에서 드라이버를 등록하는 작업을 수행한다.
- pci_register_driver 함수는 struct pci_driver 구조체를 사용해 PCI 장치 드라이버의 정보를 커널에 전달한다.
- 커널은 드라이버가 등록한 함수를 사용하여 PCI 디바이스를 활성화한다.
- 커널은 시스템의 모든 PCI 장치를 스캔하고, 각 장치의 PCI ID를 확인한다.
- 드라이버가 내보낸 PCI ID 테이블과 대조하여 해당 장치를 제어할 수 있는 드라이버를 결정한다.
- 일치하는 드라이버가 발견되면, 해당 드라이버를 로드한다.
이를 통해 OS는 시스템에 연결된 디바이스와 해당 디바이스와 통신하기 위해 사용해야할 드라이버를 파악할 수 있다.
PCI probe
PCI ID를 통해 디바이스가 식별되면, 커널은 해당 디바이스를 제어할 적절한 드라이버를 선택할 수 있다. 각 PCI 드라이버는 커널의 PCI 시스템에 probe 함수를 등록한다. 커널은 아직 특정 드라이버가 claim 하지 않은 디바이스에 대해 이 함수를 호출한다.
한번 디바이스가 소유되면 다른 드라이버들은 해당 디바이스에 대해 호출하지 않는다.
- probe 함수
- probe 함수는 드라이버가 PCI 서브시스템에 등록될 때 장치가 감지되었을 때 호출되는 콜백 함수
- 하드웨어 초기화를 수행하고, 장치가 정상적으로 동작할 수 있도록 준비
probe 함수의 동작 흐름은 다음과 같은 순서로 동작 한다.
- PCI 장치 감지 및 리소스 매핑
- PCI 메모리 공간 매핑.
- 필요한 DMA 버퍼 또는 I/O 공간 설정.
- 장치 설정
- MAC 주소 읽기 및 설정.
- 네트워크 인터페이스 초기화(net_device).
- 핵심 기능 등록
- NAPI 폴링 함수, ethtool 함수, 인터럽트 핸들러 등록.
- 운영체제와 네트워크 계층 통합
- NIC를 커널 네트워크 스택에 연결.
- 장치 활성화
- 하드웨어 동작 시작.
- 네트워크 인터페이스를 UP 상태로 설정.
대부분의 드라이버는 디바이스를 사용할 준비를 하기 위해 많은 코드를 실행한다.
실행되는 작업은 드라이버마다 다르지만 일반적으로 수행되는 작업은 다음과 같다.
- 주요 동작
- 장치의 리소스(예: 메모리, 인터럽트)를 요청하고 설정
- 메모리 범위 및 IO 포트 요청
- 인터럽트 요청 및 등록
- DMA 마스크 설정
- 장치 드라이버와 하드웨어 간의 인터페이스를 설정
- PCI 디바이스 활성화
- ethtool과 같은 유틸리티를 통해 제공되는 하드웨어 제어 함수 등록
- 하드웨어 특성(quirks)에 따른 워크어라운드 처리
- 네트워크 인터페이스와 같은 상위 수준의 데이터 구조(예: struct net_device) 생성 및 등록
- struct net_device_ops 구조체 생성, 초기화 및 등록.
- 이 구조체는 디바이스를 열거나, 네트워크로 데이터를 전송하거나, MAC 주소를 설정하는 데 필요한 다양한 함수 포인터를 포함
- 필요 시 워치독(watchdog) 작업 등록(예: e1000e는 하드웨어가 멈췄는지 확인하기 위해 워치독 작업을 수행).
- 장치의 리소스(예: 메모리, 인터럽트)를 요청하고 설정
일반적으로 네트워크 드라이버의 probe 함수에서 수행되는 초기화 작업은 다음과 같다.
- 먼저 pci_enable_device_mem을 사용해 디바이스를 초기화
- 이 함수는 디바이스가 절전 모드(suspended) 상태라면 이를 깨우고, 메모리 리소스를 활성화하는 등의 작업을 수행
- 다음으로 DMA 마스크를 설정한다.
- 만약 디바이스가 64비트 메모리 주소에 읽기 및 쓰기가 가능하면, dma_set_mask_and_coherent 함수가 DMA_BIT_MASK(64)와 함께 호출됨
- 메모리 영역은 pci_request_selected_regions 호출을 통해 예약된다.
- PCI Express 고급 오류 보고(AER)가 PCI AER 드라이버가 로드된 경우 활성화된다. 이를 위해 pci_enable_pcie_error_reporting이 호출된다.
- DMA가 pci_set_master 호출을 통해 활성화된다.
- 마지막으로, PCI 구성 공간은 pci_save_state를 통해 저장된다.
Network Device 초기화
이 후 네트워크 드라이버 초기화가 이루어진다. 절차는 다음과 같다.
- ethtool 드라이버가 지원하는 함수 등록
- ethtool은 네트워크 인터페이스 카드(NIC)의 속성(예: 링크 속도, 듀플렉스 모드)을 제어하거나 상태를 확인하기 위한 유틸리티
- 드라이버는 ethtool_ops 구조체를 설정하여 NIC와 관련된 정보를 제공하거나 제어할 수 있는 함수를 등록
- 들어오는 패킷을 수집하기 위한 NAPI 폴링 함수 등록
- NAPI (New API)는 네트워크 데이터 패킷을 수신하는 효율적인 방법을 제공
- 드라이버는 NAPI와 연계하여 수신된 패킷을 처리하는 폴링 함수를 등록
- 이를 통해 높은 네트워크 부하에서도 성능을 유지할 수 있음
- NIC의 MAC 주소 설정
- 드라이버는 장치의 MAC 주소를 읽어와 네트워크 계층에 등록
- MAC 주소는 NIC의 고유 식별자로, 네트워크 통신에 필수적
- net_device 구조체 초기화
- net_device는 리눅스 네트워크 계층에서 장치를 표현하는 구조체
- 드라이버는 이 구조체를 초기화하고, NIC와 연계하여 동작할 수 있도록 설정
- struct net_device_ops가 등록됨
- 인터럽트가 활성화될 때 장치에서 사용되는 하드웨어 IRQ 번호 설정
- 장치가 인터럽트를 사용할 경우, IRQ 번호를 요청하고 설정
- 이 단계에서 장치는 인터럽트를 발생시킬 준비를 한다.
- 감시(Watchdog) 태스크 설정
- 일부 드라이버는 Watchdog 태스크를 설정하여 장치가 정상적으로 작동하는지 확인
- 해결 방법이나 특이한 점 처리 또는 이와 유사한 것과 같은 기타 장치별 사항
- 하드웨어의 결함(quirks)이나 특정 조건에서 필요한 워크어라운드를 적용
- 예: 특정 NIC에서의 데이터 손실 방지, 버그 회피 코드 등
struct net_device_ops
struct net_device_ops는 네트워크 서브시스템이 디바이스를 제어하기 위해 필요한 많은 중요한 작업에 대한 함수 포인터를 포함한다.
static const struct net_device_ops i40e_netdev_ops = {
.ndo_open = i40e_open,
.ndo_stop = i40e_close,
.ndo_start_xmit = i40e_lan_xmit_frame,
.ndo_get_stats64 = i40e_get_netdev_stats_struct,
/* ... */
};
ethtool 등록
ethtool은 다양한 드라이버 및 하드웨어 옵션을 조회하고 설정할 수 있는 cli 프로그램이다.
ethtool의 일반적인 사용사례는 네트워크 장치에서 상세한 통계를 수집하는 것이다. ethtool 은 ioctl 시스템 콜을 사용하여 디바이스 드라이버와 통신한다. 디바이스 드라이버는 ethtool 작업에 해당하는 일련의 함수를 등록하며 커널은 이를 연결한다.
ethtool 에서 ioctl 호출이 발생하면 커널은 적절한 드라이버에 의해 등록된 ethtool 구조체를 찾아 등록된 함수를 실행한다.
드라이버의 ethtool 함수 구현은 단순히 드라이버 내 소프트웨어 플래그를 변경하는 것부터 NIC 하드웨어의 동작을 조정하는 작업(예: 디바이스 레지스터 값을 변경)까지 다양한 작업을 수행할 수 있다.
static const struct ethtool_ops i40e_ethtool_ops = {
.get_settings = i40e_get_settings,
.set_settings = i40e_set_settings,
.get_drvinfo = i40e_get_drvinfo,
.get_regs_len = i40e_get_regs_len,
.get_regs = i40e_get_regs,
/* ... */
};
각 드라이버는 어떤 ethtool 함수가 적합한지 결정하고 이를 구현해야한다. 모든 드라이버가 모든 ethtool을 구현하는 것은 아니므로 일부 기능은 지원되지 않을 수 있다.
IRQ (interrupt request)
data frame이 DMA를 통해 RAM에 기록될 때 NIC는 나머지 시스템에 데이터가 처리될 준비가 되었다는 것을 어떻게 알릴까
전통적으로 NIC는 데이터가 도착했음을 나타내는 인터럽트 요청(IRQ) 을 생성한다. IRQ 에는 다음과 같이 세가지 일반적인 유형이 있다.
- MSI-X
- MSI
- Legacy IRQ
데이터가 DMA를 통해 RAM에 기록될 때 NIC가 IRQ를 생성하는 것은 간단하지만 많은 데이터 프레임이 도착하면 많은 IRQ가 생성될 수 있다. 생성되는 IRQ가 많을 수록 사용자 프로세스와 같은 더 높은 수준의 작업을 처리할 수 있는 CPU 시간이 줄어든다.
New API(NAPI) 는 패킷 도착 시 네트워크 장치에서 생성되는 IRQ 수를 줄이기 위한 메커니즘으로 만들어졌다. NAPI는 IRQ 수를 줄이는 데 도움이 되지만 IRQ를 완전히 없앨 수는 없다.
NAPI(New API)
NAPI는 데이터 수집 방식에서 기존 레거시 방식과 여러 중요한 점에서 차이가 있다. NAPI는 디바이스 드라이버가 데이터를 수집하기 위해 호출되는 폴링(poll) 함수를 등록할 수 있도록 한다. 이 함수는 NAPI 서브시스템에 의해 호출된다.
네트워크 디바이스 드라이버에서 NAPI를 사용하는 의도된 흐름은 다음과 같다.
- NAPI 활성화 및 초기 상태:
드라이버에 의해 NAPI가 활성화되지만, 초기 상태는 꺼져(off) 있다. - 패킷 수신 및 메모리에 저장:
NIC가 패킷을 수신하고 이를 메모리에 DMA 방식으로 저장한다. - IRQ 생성:
NIC가 IRQ를 생성하여 드라이버의 IRQ 핸들러를 트리거한다. - NAPI 서브시스템 깨움:
드라이버가 소프트IRQ(softirq)를 사용하여 NAPI 서브시스템을 깨운다. NAPI는 별도의 실행 스레드에서 드라이버에 등록된 폴 함수(poll function)를 호출하여 패킷을 수집하기 시작한다. - NIC의 추가 IRQ 비활성화:
디바이스에서 더 이상 IRQ가 발생하지 않도록 드라이버가 이를 비활성화한다. 이는 NAPI 서브시스템이 장치의 방해 없이 패킷을 처리할 수 있도록 하기 위함 - 작업 완료 및 NAPI 비활성화:
처리할 작업이 더 이상 없으면 NAPI 서브시스템이 비활성화되고, 장치의 IRQ가 다시 활성화된다. - 다시 시작:
프로세스는 2단계로 돌아가서 반복된다.
하드웨어 인터럽트
NIC 드라이버가 어떻게 작성되고 어떻게 상위 네트워킹 계층과 연동되는지 확인해보자
모든 인터럽트는 인터럽트 핸들러라는 함수를 작동시킨다. 이 함수는 장치별로 다르며 따라서 디바이스 드라이버에 의해 설치된다.
전형적으로 디바이스 드라이버가 NIC를 등록할 경우 드라이버는 IRQ를 요청하고 할당받는다. 그런 다음 해당 IRQ에 대한 핸들러를 등록하거나 해제하는데 이 때 아키텍처 의존적인 함수 2개를 사용한다.
- request_irq
- setup_irq를 둘러싸는 래퍼 함수
- free_irq
커널이 인터럽트 알림을 받으면 IRQ 번호를 사용해 드라이버 핸들러를 찾고 수행시킨다.
핸들러를 찾기 위해 커널은 IRQ 번호와 핸들러 함수를 전역 테이블에 저장한다.
장치 처리 계층 초기화
subsys_initcall 매크로는 리눅스 커널 초기화 과정에서 네트워크 서브시스템의 초기화를 보장한다.
net_dev_init은 커널의 네트워크 서브시스템을 초기화하는 핵심 함수로, NIC 드라이버가 등록되기 전에 필요한 네트워크 인프라를 설정한다.
트래픽 컨트롤과 각 CPU 별 수신 큐 등을 포함한 네트워킹 코드 초기화의 중요한 부분은 net_dev_init 에서 부팅 시에 이뤄진다.
nic 디바이스 드라이버가 자신을 등록하기 전에 subsys_initcall 매크로가 net_dev_init 을 먼저 수행하는 방법과 이것이 중요한 이유
- net_dev_init
softirq
Linux 커널의 시스템 softirq은 커널이 장치 드라이버 IRQ 컨텍스트 외부에서 작업을 처리하는 데 사용하는 시스템이다.
네트워크 장치의 경우, softirq시스템은 들어오는 패킷을 처리하는 역할을 하고 아래와 같이 net_dev_init에서 open_softirq를 호출하여 네트워크 관련 인터럽트에 net_tx_action, net_rx_action 핸들러를 등록한다.
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
- net_rx_action
- 들어오는 프레임을 처리하는 데 사용하는 후반부 함수
- 프레임은 net_rx_action에서 처리되기 위해 다음 두군데에서 대기할 수 있다.
- 공용의 cpu별 큐
- 장치 메모리
시스템 softirq은 커널의 부팅 프로세스 중에 일찍 초기화되는데 초기화 과정은 다음과 같다.
- SoftIRQ 커널 쓰레드는 kernel/softirq.c에 있는 spawn_ksoftirqd 함수에서 생성되며, 이 함수는 kernel/smpboot.c의 smpboot_register_percpu_thread를 호출하여 CPU당 하나씩 생성된다.
- run_ksoftirqd 함수가 thread_fn으로 등록되어 있으며, 이 함수는 루프에서 실행된다.
- ksoftirqd 쓰레드는 run_ksoftirqd 함수 내에서 실행 루프를 시작한다.
- softnet_data 구조체가 생성되는데, 이는 CPU당 하나씩 만들어진다.
- 이 구조체는 네트워크 데이터를 처리하기 위한 중요한 데이터 구조에 대한 참조를 포함한다.
- poll_list는 디바이스 드라이버의 napi_schedule 또는 다른 NAPI API 호출에 의해 NAPI 폴 워커(NAPI poll worker) 구조체가 추가되는곳
- net_dev_init**은 open_softirq 호출을 통해 NET_RX_SOFTIRQ 소프트IRQ를 소프트IRQ 시스템에 등록한다.
- net_rx_action: SoftIRQ 커널 쓰레드가 패킷을 처리하기 위해 실행하는 함수
net_device
int (*init)(struct net_device *dev);
int (*open)(struct net_device *dev);
int (*stop)(struct net_device *dev);
int (*hard_start_xmit)(struct sk_buff *skb, struct net_device *dev);
int (*poll)(struct net_device *dev, int *quota);
int (*hard_header)(struct sk_buff *skb, struct net_device *dev,
unsigned short type, void *daddr, void *saddr,
unsigned len);
References
- https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=07580f3dbc687e3927f9e41a2fcf074f033f416b
- https://ggaaooppeenngg.github.io/zh-CN/2016/11/21/kernel-%E5%8D%8F%E8%AE%AE%E6%A0%88%E9%93%BE%E8%B7%AF%E5%B1%82%E5%88%86%E6%9E%90/
- https://maxnilz.com/docs/004-network/005-linux-rx/
- https://blog.packagecloud.io/illustrated-guide-monitoring-tuning-linux-networking-stack-receiving-data/
- https://blog.packagecloud.io/monitoring-tuning-linux-networking-stack-receiving-data/
- ㅇㄹㅇㄴㄹ
- ㅇㄹㅇㄹ