이더넷 (Ethernet)
1973년 Xerox PARC에서 탄생한 이더넷은 반세기가 넘는 진화를 거쳐 10 Mbps 동축 케이블 버스(Bus)에서 800 Gbps 광 링크까지 확장되었습니다. 이 문서는 이더넷의 역사, 물리 계층(PHY), 프레임 구조, 리눅스 커널 구현, 오프로드 기술, 에너지 효율, 산업용 이더넷까지 다양한 시각에서 종합적으로 다룹니다.
핵심 요약
- 프레임 (Frame) — 이더넷의 전송 단위. 목적지/출발지 MAC, EtherType, 페이로드, FCS로 구성되며 64~1518바이트 크기
- MAC 주소 — 48비트(6바이트) 하드웨어 주소로, NIC마다 고유하게 할당되어 L2에서 장치를 식별
- PHY / MAC — PHY는 전기/광 신호 변환을 담당하는 물리 계층 칩, MAC은 프레임 조립/해석을 담당하는 계층. RGMII 등의 인터페이스로 연결
- ethtool — 리눅스에서 NIC 속도, 오프로드, 링 버퍼(Ring Buffer), EEE 등을 조회하고 설정하는 핵심 유틸리티
- 오프로드 (Offload) — TSO, GRO, RSS 등 CPU 부담을 NIC 하드웨어로 넘기는 기술. 고성능 네트워크의 핵심
단계별 이해
- 프레임 구조부터 파악하기
이더넷의 기본은 프레임입니다. Preamble → MAC 주소 → EtherType → Payload → FCS 순서를 먼저 이해하세요.EtherType ≥ 0x0600이면 Ethernet II, 그보다 작으면 802.3 프레임입니다. - PHY-MAC 연결 이해하기
NIC 내부에서 MAC 계층이 프레임을 처리하고, PHY 칩이 실제 전기/광 신호로 변환합니다.ethtool eth0으로 현재 링크 속도와 PHY 상태를 확인할 수 있습니다. - 커널 수신 경로 따라가기
케이블 → NIC DMA → IRQ →napi_schedule()→ NAPI poll →eth_type_trans()→netif_receive_skb()순서로 프레임이 커널에 전달됩니다. 이 흐름이 이더넷 드라이버의 핵심입니다. - 오프로드와 성능 튜닝 적용하기
ethtool -k eth0로 현재 오프로드 상태를 확인하고,ethtool -G eth0 rx 4096으로 링 버퍼를 조정하며,ethtool -C eth0 adaptive-rx on으로 인터럽트(Interrupt) 병합을 최적화합니다.
개요
이더넷(Ethernet)은 IEEE 802.3 표준으로 규정된 유선 LAN 기술로, 전 세계 네트워크 인프라의 핵심입니다. 원래 공유 매체(Shared Medium) 기반 CSMA/CD 프로토콜이었지만, 스위칭 기술과 전이중(Full-Duplex) 통신의 도입으로 현대 이더넷은 충돌 없는 점대점(Point-to-Point) 링크가 되었습니다.
- 프레임 기반 — 가변 길이(64~1518바이트, Jumbo Frame 시 ~9000바이트) 프레임 전송
- MAC 주소 — 48비트 EUI-48 하드웨어 주소로 장치 식별
- 속도 진화 — 10M → 100M → 1G → 10G → 25G → 40G → 100G → 200G → 400G → 800G
- 물리 계층 독립 — 동축, UTP, 광섬유, 백플레인 등 다양한 매체 지원
- 리눅스 커널 —
struct ethhdr,eth_type_trans(),ethtool_ops, phylink 프레임워크로 구현
역사와 진화
이더넷의 발전 과정은 컴퓨터 네트워킹 역사 그 자체입니다.
| 연도 | 표준/기술 | 속도 | 매체 | 의의 |
|---|---|---|---|---|
| 1973 | Xerox Ethernet (실험) | 2.94 Mbps | 동축 케이블 | Robert Metcalfe, Xerox PARC에서 최초 구현 |
| 1980 | DIX Ethernet (v1.0) | 10 Mbps | 동축 (Thick) | DEC-Intel-Xerox 공동 표준, EtherType 필드 도입 |
| 1983 | IEEE 802.3 (10BASE5) | 10 Mbps | Thick 동축 | 첫 IEEE 표준화, CSMA/CD 공식 규정 |
| 1985 | 10BASE2 (Thin Ethernet) | 10 Mbps | Thin 동축 (RG-58) | 저비용 동축, 185m 세그먼트 |
| 1990 | 10BASE-T | 10 Mbps | UTP Cat3 | 트위스트 페어 혁명, 스타 토폴로지(Topology) 전환 |
| 1995 | 802.3u (Fast Ethernet) | 100 Mbps | UTP Cat5 / 광 | 100BASE-TX, Auto-Negotiation 도입 |
| 1998 | 802.3z (Gigabit Ethernet) | 1 Gbps | 광섬유 | 1000BASE-SX/LX, 8B/10B 부호화 |
| 1999 | 802.3ab | 1 Gbps | UTP Cat5e | 1000BASE-T, PAM-5 4쌍 전송 |
| 2002 | 802.3ae (10GbE) | 10 Gbps | 광섬유 | CSMA/CD 사실상 폐기, 전이중 전용 |
| 2006 | 802.3an (10GBASE-T) | 10 Gbps | UTP Cat6a | 구리선 10G, LDPC 부호화 |
| 2010 | 802.3ba | 40/100 Gbps | 광섬유 | 다중 레인(4×10G, 10×10G, 4×25G) |
| 2016 | 802.3by (25GbE) | 25 Gbps | SFP28 | 서버 연결 최적 대역폭(Bandwidth) |
| 2017 | 802.3bs | 200/400 Gbps | 광섬유 | PAM4 변조, 데이터센터 스파인 |
| 2024 | 802.3df | 800 Gbps/1.6 Tbps | 광섬유 | AI 클러스터, 100G 레인 기술 |
CSMA/CD와 충돌 도메인
초기 이더넷의 핵심 프로토콜인 CSMA/CD(Carrier Sense Multiple Access / Collision Detection)는 공유 매체에서의 다중 접근 문제를 해결합니다.
알고리즘 단계
- Carrier Sense — 전송 전 매체가 유휴 상태(Idle State)인지 확인
- Transmit — 유휴 시 프레임 전송 시작
- Collision Detection — 전송 중 충돌 감지 시 jam 신호(32비트) 발송
- Backoff — Binary Exponential Backoff 알고리즘으로 재전송(Retransmission) 지연(Latency)
- n번째 충돌 시: 0 ~ 2min(n,10)-1 슬롯 중 랜덤 선택
- 슬롯 시간: 10 Mbps에서 51.2 μs (512 bit times)
- 16번 충돌 시 전송 포기
random(0, 2^min(n,10) - 1) x 슬롯시간입니다. 예를 들어 3번째 충돌이면 0~7 슬롯(0~358.4 us @10Mbps) 중 랜덤 선택합니다. 지수를 min(n,10)으로 제한하는 이유는 2^16 = 65,536 슬롯(약 3.35초)이면 대기 시간이 과도하기 때문입니다. n=10에서 최대 1,023 슬롯(약 52.4ms)으로 캡됩니다.
ethtool에서도 duplex 설정을 확인할 수 있습니다.
충돌 도메인 vs 브로드캐스트 도메인
| 구분 | 충돌 도메인 | 브로드캐스트 도메인 |
|---|---|---|
| 정의 | 동시 전송 시 충돌이 발생하는 범위 | 브로드캐스트 프레임이 도달하는 범위 |
| 분리 장비 | 스위치, 브릿지 (포트별 분리) | 라우터, VLAN |
| 허브 | 모든 포트가 하나의 충돌 도메인 | 모든 포트가 하나의 브로드캐스트 도메인 |
| 스위치 | 포트별 독립 충돌 도메인 | 전체가 하나의 브로드캐스트 도메인 |
프레임 구조
이더넷 프레임은 여러 변형이 존재합니다. 가장 널리 사용되는 Ethernet II(DIX)와 IEEE 802.3 프레임을 비교합니다.
주요 EtherType 값
| EtherType | 프로토콜 | 커널 상수 |
|---|---|---|
| 0x0800 | IPv4 | ETH_P_IP |
| 0x0806 | ARP | ETH_P_ARP |
| 0x8100 | 802.1Q VLAN | ETH_P_8021Q |
| 0x86DD | IPv6 | ETH_P_IPV6 |
| 0x8847 | MPLS Unicast | ETH_P_MPLS_UC |
| 0x88A8 | 802.1ad (QinQ) | ETH_P_8021AD |
| 0x88CC | LLDP | ETH_P_LLDP |
| 0x88F7 | PTP (IEEE 1588) | ETH_P_1588 |
| 0x8906 | FCoE | ETH_P_FCOE |
Jumbo Frames
표준 이더넷 MTU(1500바이트)를 넘는 Jumbo Frame(최대 9000바이트)은 데이터센터 환경에서 처리량(Throughput)을 높이고 CPU 오버헤드(Overhead)를 줄입니다.
# MTU 변경
ip link set eth0 mtu 9000
# 확인
ip -d link show eth0 | grep mtu
# 경로 MTU 확인
tracepath -n 192.168.1.1
이더넷 인코딩과 변조 기법
이더넷 물리 계층은 속도가 올라갈수록 더 정교한 라인 코딩(Line Coding)과 변조(Modulation) 기법을 사용합니다. 초기 10 Mbps의 단순한 맨체스터 인코딩(Manchester Encoding)에서 400G 이상의 PAM4(Pulse Amplitude Modulation 4-level)까지, 각 세대는 대역폭 효율과 클럭 복원(Clock Recovery) 사이의 절충을 다르게 풀었습니다.
인코딩 방식 비교
| 인코딩 | 적용 표준 | 속도 | 대역폭 효율 | 클럭 복원 | 핵심 특징 |
|---|---|---|---|---|---|
| 맨체스터(Manchester) | 10BASE-T | 10 Mbps | 50% (20 MBaud → 10 Mbps) | 매 비트 천이 보장 | 비트 중앙에서 천이, 자기 동기화(Self-clocking) |
| 4B/5B + MLT-3 | 100BASE-TX | 100 Mbps | 80% (125 MBaud → 100 Mbps) | 연속 0 제한 (최대 3개) | 4비트를 5비트로 매핑, MLT-3 3-레벨 신호 |
| 8B/10B | 1000BASE-X, FC | 1 Gbps | 80% (1.25 GBaud → 1 Gbps) | RD(Running Disparity) 유지 | DC 밸런스 보장, K-코드로 제어 심볼(Control Symbol) 전송 |
| PAM-5 | 1000BASE-T | 1 Gbps | 4쌍 × 250 Mbps | Viterbi 디코더 | 5-레벨 신호, 4쌍 동시 양방향(Full-Duplex) 전송 |
| 64B/66B | 10GBASE-R, 25G, 40G, 100G | 10~100 Gbps | 97% (10.3125 GBaud → 10 Gbps) | Sync 헤더(Header) 2비트 | 8B/10B 대비 오버헤드(Overhead) 대폭 감소 |
| PAM4 | 50GBASE-R, 100G, 200G, 400G, 800G | 50~800 Gbps | 비트/심볼 = 2 | FEC 필수 | 심볼당 2비트 전송, SNR 마진 감소로 FEC 필수 |
| 128B/132B | PCIe Gen3+ (참고) | — | 97% | 블록 동기화 | 64B/66B의 확장형, 이더넷에서는 미사용 |
맨체스터 인코딩 (Manchester Encoding)
10BASE-T에서 사용하는 맨체스터 인코딩은 각 비트 구간(Bit Period)의 중앙에서 반드시 전압 천이(Transition)가 발생합니다. 논리 1은 Low→High, 논리 0은 High→Low 천이로 표현됩니다 (IEEE 802.3 규약). 이 방식은 클럭 정보를 데이터와 함께 전송하므로 별도의 클럭 라인이 필요 없지만, 10 Mbps 데이터를 전송하는 데 20 MHz 대역폭이 필요하여 효율이 50%에 불과합니다.
NRZ vs PAM4 신호 비교
NRZ(Non-Return-to-Zero)는 2개의 전압 레벨로 심볼(Symbol)당 1비트를 전송합니다. PAM4는 4개의 전압 레벨(0, 1, 2, 3)로 심볼당 2비트를 전송하여 같은 보드 레이트(Baud Rate)에서 두 배의 데이터 속도를 달성합니다. 그러나 레벨 간격이 1/3로 줄어들어 SNR(Signal-to-Noise Ratio) 마진이 약 9.5 dB 감소합니다.
64B/66B 인코딩
8B/10B의 20% 오버헤드를 3%로 줄이기 위해 10GBASE-R에서 도입된 64B/66B 인코딩은 64비트 페이로드 앞에 2비트 동기화 헤더(Sync Header)를 붙입니다. 헤더 값 01은 데이터 블록, 10은 제어 블록을 나타냅니다. 스크램블러(Scrambler)가 DC 밸런스와 천이 밀도를 유지하며, 수신기는 헤더의 01/10 패턴으로 블록 경계를 찾습니다.
PAM-5와 1000BASE-T
기가비트 이더넷의 구리선 표준(1000BASE-T)은 4쌍의 UTP 케이블을 모두 사용하며, 각 쌍에서 PAM-5(5-레벨 펄스 진폭 변조)로 125 MBaud를 전송합니다. 5개 레벨(-2, -1, 0, +1, +2)은 Trellis Coded Modulation(TCM)과 결합되어, 4쌍이 동시에 양방향(전이중)으로 데이터를 전송하면서 에코 캔슬레이션(Echo Cancellation)으로 자기 신호와 상대 신호를 분리합니다. 이 구조를 통해 CAT5e 케이블로 1 Gbps를 달성합니다.
/* include/uapi/linux/ethtool.h — 속도/인코딩 관련 상수 */
#define SPEED_10 10
#define SPEED_100 100
#define SPEED_1000 1000
#define SPEED_2500 2500
#define SPEED_5000 5000
#define SPEED_10000 10000
#define SPEED_25000 25000
#define SPEED_40000 40000
#define SPEED_50000 50000
#define SPEED_100000 100000
#define SPEED_200000 200000
#define SPEED_400000 400000
#define SPEED_800000 800000
#define SPEED_UNKNOWN -1
/* include/linux/phy.h — PHY 인터페이스 모드 (인코딩과 관련) */
typedef enum {
PHY_INTERFACE_MODE_MII, /* 10/100 Mbps */
PHY_INTERFACE_MODE_GMII, /* 1 Gbps (PAM-5 on copper) */
PHY_INTERFACE_MODE_SGMII, /* 1 Gbps SerDes (8B/10B) */
PHY_INTERFACE_MODE_RGMII, /* 1 Gbps reduced pin (DDR) */
PHY_INTERFACE_MODE_XGMII, /* 10 Gbps (64B/66B) */
PHY_INTERFACE_MODE_10GBASER, /* 10GBASE-R PCS (64B/66B) */
PHY_INTERFACE_MODE_25GBASER, /* 25GBASE-R (64B/66B, NRZ) */
PHY_INTERFACE_MODE_USXGMII, /* 멀티레이트 USXGMII */
PHY_INTERFACE_MODE_10GKR, /* 10GBASE-KR 백플레인 */
...
} phy_interface_t;
PAM4 SNR 페널티 계산
PAM4에서 레벨 간격은 NRZ 대비 1/3로 줄어들며, 이에 따른 SNR 페널티는 다음과 같이 계산됩니다.
PAM4 SNR 페널티 계산: NRZ는 2개 레벨로 레벨 간격이 V_swing(전체 진폭)이며, PAM4는 4개 레벨(0, 1, 2, 3)로 레벨 간격이 V_swing / 3입니다.
SNR_penalty = 20 × log10(3) ≈ 9.54 dB
이는 같은 BER(Bit Error Rate)을 유지하기 위해 PAM4가 NRZ보다 약 9.5 dB 더 높은 SNR이 필요함을 의미합니다. 실제 PAM4 링크에서의 주요 수치는 다음과 같습니다.
- pre-FEC BER 목표: ~2.4e-4 (RS-FEC KP4 기준)
- post-FEC BER 목표: ~1e-15 (사실상 에러 프리)
- RS(544,514) FEC가 ~6.2 dB 코딩 이득을 제공합니다.
400GBASE-DR4 예시:
- 레인 속도: 106.25 Gbps (53.125 GBaud PAM4)
- 레인 수: 4 (QSFP-DD 또는 OSFP)
- FEC: RS(544,514,10) — KP4 FEC
- 유효 데이터 속도: 4 × 106.25 × (514/544) ≈ 401.5 Gbps
ethtool을 통한 인코딩 정보 확인
# FEC 모드 확인 (PAM4 링크에서는 RS-FEC 활성 확인 필수)
ethtool --show-fec eth0
# 출력 예시:
# FEC parameters for eth0:
# Configured FEC encodings: RS
# Active FEC encoding: RS
# 링크 모드에서 인코딩 관련 정보 확인
ethtool eth0 | grep -E "Speed|Duplex|Link detected|Supported link modes"
# Speed: 100000Mb/s → 100GBASE (PAM4 또는 NRZ 4×25G)
# 100GBASE-SR4: NRZ 4×25G
# 100GBASE-DR: PAM4 1×100G
# PHY 통계에서 FEC 카운터 확인 (PAM4 링크 품질 지표)
ethtool -S eth0 | grep -i fec
# fec_corrected_blocks: 12345 ← RS-FEC가 수정한 블록 수
# fec_uncorrectable_blocks: 0 ← 수정 불가 블록 (0이어야 정상)
물리 계층 (PHY)
이더넷 PHY는 MAC이 만든 디지털 데이터를 실제 전기·광 신호로 변환하고, 링크 속도·듀플렉스·pause를 협상하며, 선로 상태를 감시하는 계층입니다. 이 문서에서는 PHY를 요약만 다루고, MDIO/Clause 22·45, phylib/phylink, Device Tree, 보드 지연 보상, 실전 장애 대응은 이더넷 PHY (Physical Layer) 전용 문서로 분리했습니다.
phy-mode 설계, phylib/phylink 수명주기, 링크 디버깅(Debugging)을 깊게 다룹니다.
| 구성 | 요약 역할 | 대표 이슈 |
|---|---|---|
| MAC | 프레임, DMA, 큐, 통계 처리 | TX timeout, queue hang |
| PHY | 부호화, 직렬화(Serialization), 링크 협상, 신호 송수신 | 링크 불안정, CRC 증가 |
| MDIO | PHY 레지스터 관리 버스 | PHY ID 불일치, 설정 읽기 실패 |
| 매체 | 케이블, 모듈, magnetics, 상대편 포트 | 거리 민감, 속도 강등 |
rgmii-id 설정과 보드 지연을 동시에 쓰면 CRC 오류와 간헐적 링크 다운이 흔하게 발생합니다.
SFP/QSFP 트랜시버 상세
광 또는 구리 이더넷 연결에서 트랜시버(Transceiver) 모듈은 전기 신호와 광/전기 매체를 변환하는 핵심 부품입니다. SFP(Small Form-factor Pluggable) 계열의 모듈은 핫플러그가 가능하며, 리눅스 커널의 sfp 서브시스템이 모듈 감지, EEPROM 읽기, DDM(Digital Diagnostic Monitoring)을 처리합니다.
폼팩터(Form Factor) 비교
| 폼팩터 | 최대 속도 | 레인 수 | 전력(Typical) | 최대 도달 거리(SMF) | 규격 |
|---|---|---|---|---|---|
| SFP | 1 Gbps | 1 | ~1 W | 10 km (1000BASE-LX) | SFF-8472, INF-8074 |
| SFP+ | 10 Gbps | 1 | ~1 W | 10 km (10GBASE-LR) | SFF-8472, SFF-8431 |
| SFP28 | 25 Gbps | 1 | ~1 W | 10 km (25GBASE-LR) | SFF-8472 |
| QSFP+ | 40 Gbps | 4×10G | ~1.5 W | 10 km (40GBASE-LR4) | SFF-8636 |
| QSFP28 | 100 Gbps | 4×25G | ~3.5 W | 10 km (100GBASE-LR4) | SFF-8636, CMIS |
| QSFP-DD | 400 Gbps | 8×50G | ~12 W | 10 km (400GBASE-DR4+) | CMIS 4.0+ |
| OSFP | 800 Gbps | 8×100G | ~20 W | 10 km (800GBASE-DR8) | OSFP MSA |
SFP 모듈 내부 구조
SFP 모듈은 물리적으로 작지만 내부에 복잡한 광전자(Optoelectronic) 시스템을 포함합니다. 송신부(TOSA, Transmitter Optical Sub-Assembly), 수신부(ROSA, Receiver Optical Sub-Assembly), 마이크로컨트롤러(MCU), EEPROM, DDM 센서가 하나의 패키지에 통합되어 있습니다.
DDM (Digital Diagnostic Monitoring)
SFF-8472 규격에 따라 SFP/SFP+ 모듈은 실시간 진단 정보를 I2C 주소 0x51(A2h 페이지)을 통해 제공합니다. 리눅스에서는 ethtool -m 명령으로 이 정보를 읽을 수 있습니다.
| 파라미터 | A2h 오프셋 | 크기 | 단위 | 일반적 임계값 (SFP+ 10G-LR) |
|---|---|---|---|---|
| 온도(Temperature) | 96~97 | 2바이트 (signed) | °C (1/256 °C) | 경고: 70°C, 알람: 78°C |
| 공급 전압(Vcc) | 98~99 | 2바이트 (unsigned) | 0.1 mV | 경고: 3.0~3.6V, 알람: 2.9~3.7V |
| TX Bias 전류 | 100~101 | 2바이트 (unsigned) | 2 μA | 경고: 70 mA, 알람: 84 mA |
| TX 출력 파워 | 102~103 | 2바이트 (unsigned) | 0.1 μW | 경고: -1~3 dBm, 알람: -2~4 dBm |
| RX 입력 파워 | 104~105 | 2바이트 (unsigned) | 0.1 μW | 경고: -15~-1 dBm, 알람: -17~0 dBm |
# SFP 모듈 정보 읽기 (ethtool -m)
ethtool -m eth0
# 출력 예시:
# Identifier : 0x03 (SFP)
# Extended identifier : 0x04 (GBIC/SFP defined)
# Connector : 0x07 (LC)
# Transceiver type : 10G Ethernet: 10G Base-LR
# Encoding : 0x06 (64B/66B)
# BR, Nominal : 10300MBd
# Length (SMF) : 10km
# Vendor name : FINISAR CORP.
# Vendor PN : FTLX1471D3BCV
# Vendor SN : ABC1234
# Laser wavelength : 1310nm
# Module temperature : 35.25 degrees C
# Module voltage : 3.2980 V
# Laser bias current : 25.346 mA
# Laser output power : 0.5623 mW / -2.50 dBm
# Receiver signal power : 0.1023 mW / -9.90 dBm
# DDM 임계값 포함 상세 출력
ethtool -m eth0 --json
# JSON 형식으로 모든 DDM 값과 알람/경고 임계값 출력
# raw hex 덤프 (SFF-8472 A0h 페이지)
ethtool -m eth0 hex on offset 0 length 128
# QSFP28 모듈의 경우 (SFF-8636 / CMIS)
ethtool -m eth0
# 4개 레인별 TX/RX 파워, 온도 등이 개별 표시됨
SFP EEPROM 구조 (SFF-8472)
SFP 모듈은 두 개의 I2C 주소를 사용합니다. 0x50(A0h)은 모듈 식별 정보를, 0x51(A2h)은 DDM 데이터를 저장합니다. QSFP 모듈(SFF-8636)은 단일 I2C 주소 0x50에 페이지 기반으로 모든 정보를 저장합니다.
커널 SFP 서브시스템
리눅스 커널의 SFP 드라이버(drivers/net/phy/sfp.c)는 SFP 모듈의 감지, EEPROM 읽기, 링크 상태 관리를 담당합니다. phylink 프레임워크와 통합되어 모듈 핫플러그 시 자동으로 링크 모드를 재협상합니다.
/* drivers/net/phy/sfp.c — SFP 버스 구조체 */
struct sfp_bus {
struct kref kref;
struct list_head node;
const struct fwnode_handle *fwnode;
struct sfp *sfp; /* SFP 소켓 드라이버 */
const struct sfp_socket_ops *socket_ops;
struct net_device *netdev; /* 연결된 네트워크 장치 */
const struct sfp_upstream_ops *upstream_ops;
void *upstream; /* phylink 또는 MAC 드라이버 */
bool registered;
bool started;
};
/* SFP upstream 콜백 — MAC/phylink이 구현 */
struct sfp_upstream_ops {
void (*attach)(void *priv, struct sfp_bus *bus);
void (*detach)(void *priv, struct sfp_bus *bus);
int (*module_insert)(void *priv, const struct sfp_eeprom_id *id);
void (*module_remove)(void *priv);
void (*link_down)(void *priv);
void (*link_up)(void *priv);
int (*connect_phy)(void *priv, struct phy_device *phydev);
void (*disconnect_phy)(void *priv);
};
/* SFP EEPROM 식별 구조체 */
struct sfp_eeprom_id {
struct sfp_eeprom_base base; /* A0h 0x00~0x3F: 기본 ID */
struct sfp_eeprom_ext ext; /* A0h 0x40~0x5F: 확장 ID */
};
SFP 호환성 이슈와 Quirks
실무에서 SFP 모듈은 벤더 간 호환성 문제가 빈번합니다. 일부 스위치/NIC는 자사 모듈만 허용하는 벤더 잠금(Vendor Lock-in)을 적용하며, 커널 SFP 드라이버에는 특정 모듈의 비표준 동작을 처리하는 quirks 테이블이 있습니다.
/* drivers/net/phy/sfp.c — SFP quirks 테이블 (일부) */
static const struct sfp_quirk sfp_quirks[] = {
/* RollBall/Hilink copper SFP — 10/100/1000BASE-T PHY 내장 */
SFP_QUIRK_F("OEM", "SFP-10G-T", sfp_quirk_oem_10g_t),
/* 일부 모듈은 soft rate select가 아닌 하드웨어 핀 기반 */
SFP_QUIRK("ALCATELLUCENT", "3FE46541AA",
sfp_quirk_alcatel_lucent),
/* Huawei MA5671A GPON SFP — 비표준 초기화 필요 */
SFP_QUIRK("HUAWEI", "MA5671A",
sfp_quirk_huawei),
};
/*
* SFP 모듈 감지 흐름:
* 1. MOD_DEF0 핀(Module Present) 감지 → sfp_sm_mod_probe()
* 2. I2C로 A0h EEPROM 읽기 → sfp_eeprom_id 파싱
* 3. quirks 테이블에서 벤더/PN 매칭
* 4. sfp_upstream_ops->module_insert() 호출
* 5. phylink이 링크 모드 결정 후 PCS/MAC 설정
* 6. TX_FAULT/LOS 핀 모니터링 시작
*/
allow_unsupported_sfp 모듈 파라미터로 이를 우회할 수 있습니다: modprobe i40e allow_unsupported_sfp=1. 다만 이 경우 호환성 문제로 인한 링크 불안정은 사용자 책임입니다.
drivers/net/phy/sfp.c가 CMIS 모듈도 부분적으로 지원합니다.
Auto-Negotiation
Auto-Negotiation은 양쪽 PHY가 지원 속도, 듀플렉스, pause 같은 능력을 교환해 최종 링크 모드를 결정하는 절차입니다. 구리 PHY는 주로 Clause 28 FLP, SerDes 기반 연결은 Clause 37/73 in-band status를 사용합니다.
- 링크 업과 링크 품질은 별개입니다. 링크가 올라와도 pause나 duplex가 잘못 합의되면 처리량과 오류율이 나빠질 수 있습니다.
- 한쪽만
autoneg off로 강제하면 상대편이 평행 감지로 링크를 올리면서 duplex를 다르게 해석할 수 있습니다. - 멀티기가비트 포트가 1G로만 붙으면 케이블 품질과 advertisement mask를 먼저 확인하세요.
# 현재 링크 협상 결과
ethtool eth0
# 자동 협상 끄고 강제 설정
ethtool -s eth0 speed 1000 duplex full autoneg off
# 상세 PHY 문서 참고: Clause 28/37/73, advertisement, EEE, 트러블슈팅
phylink 프레임워크
phylink는 MAC, PHY, PCS, SFP 모듈, fixed-link를 하나의 정책 계층으로 묶어 주는 프레임워크입니다. 외부 PHY 하나만 단순 연결하는 경우보다, SFP 핫플러그(Hotplug), SerDes 모드 전환, in-band status가 필요한 현대 NIC/SoC에서 특히 중요합니다.
- phylib는 PHY 장치를 공통 인터페이스로 다루는 계층입니다.
- phylink는 그 위에서 MAC 콜백(Callback)과 링크 정책을 통합합니다.
- 상세 API와 상태 머신은 이더넷 PHY (Physical Layer)와 Network Device 드라이버 문서를 함께 보면 흐름이 연결됩니다.
/* include/linux/phylink.h */
struct phylink_mac_ops {
void (*mac_config)(...);
void (*mac_link_down)(...);
void (*mac_link_up)(...);
};
커널 자료구조
리눅스 커널에서 이더넷 프레임을 처리하는 핵심 자료구조입니다.
struct ethhdr
/* include/uapi/linux/if_ether.h */
struct ethhdr {
unsigned char h_dest[ETH_ALEN]; /* 목적지 MAC (6 bytes) */
unsigned char h_source[ETH_ALEN]; /* 출발지 MAC (6 bytes) */
__be16 h_proto; /* EtherType 또는 Length */
} __attribute__((packed));
/* 주요 상수 */
#define ETH_ALEN 6 /* MAC 주소 길이 */
#define ETH_HLEN 14 /* 이더넷 헤더 길이 */
#define ETH_ZLEN 60 /* 최소 프레임 (FCS 제외) */
#define ETH_DATA_LEN 1500 /* 최대 페이로드 */
#define ETH_FRAME_LEN 1514 /* 최대 프레임 (FCS 제외) */
#define ETH_FCS_LEN 4 /* FCS 길이 */
eth_type_trans() 프로토콜 판별
/* net/ethernet/eth.c */
__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)
{
struct ethhdr *eth;
skb->dev = dev;
skb_reset_mac_header(skb); /* MAC 헤더 위치 기록 */
eth = (struct ethhdr *)skb->data;
skb_pull_inline(skb, ETH_HLEN); /* data 포인터를 페이로드로 이동 */
/* 수신 패킷 유형 분류 */
if (unlikely(is_multicast_ether_addr(eth->h_dest))) {
if (ether_addr_equal_64bits(eth->h_dest, dev->broadcast))
skb->pkt_type = PACKET_BROADCAST;
else
skb->pkt_type = PACKET_MULTICAST;
} else if (unlikely(!ether_addr_equal_64bits(eth->h_dest, dev->dev_addr)))
skb->pkt_type = PACKET_OTHERHOST;
/* EtherType vs Length 판별 */
if (likely(ntohs(eth->h_proto) >= ETH_P_802_3_MIN))
return eth->h_proto; /* Ethernet II: EtherType 그대로 */
/* 802.3 프레임: LLC/SNAP 확인 후 프로토콜 결정 */
...
}
ethtool_ops 구조체(Struct)
NIC 드라이버가 구현하는 ethtool_ops는 ethtool 유틸리티의 백엔드입니다.
/* include/linux/ethtool.h (주요 콜백) */
struct ethtool_ops {
void (*get_drvinfo)(...); /* 드라이버 정보 */
int (*get_link_ksettings)(...); /* 링크 속도/듀플렉스 조회 */
int (*set_link_ksettings)(...); /* 링크 설정 변경 */
void (*get_ethtool_stats)(...); /* NIC 통계 */
void (*get_strings)(...); /* 통계 이름 문자열 */
int (*get_sset_count)(...); /* 통계 세트 크기 */
u32 (*get_link)(...); /* 링크 상태 */
int (*get_coalesce)(...); /* 인터럽트 병합 설정 */
int (*set_coalesce)(...); /* 인터럽트 병합 변경 */
void (*get_ringparam)(...); /* 링 버퍼 크기 */
int (*set_ringparam)(...); /* 링 버퍼 변경 */
int (*get_rxnfc)(...); /* RSS/ntuple 규칙 */
int (*get_ts_info)(...); /* 타임스탬프 능력 */
int (*get_eee)(...); /* EEE 설정 조회 */
int (*set_eee)(...); /* EEE 설정 변경 */
};
net_device 구조체 상세 분석
struct net_device는 리눅스 네트워크 스택의 핵심 자료구조로, 모든 네트워크 인터페이스를 추상화합니다. 이더넷, Wi-Fi, 루프백(Loopback), VLAN, 브릿지(Bridge) 등 모든 네트워크 장치가 이 구조체의 인스턴스(Instance)입니다. 커널 소스 include/linux/netdevice.h에서 정의되며 수백 개의 필드를 포함합니다.
핵심 필드 분석
/* include/linux/netdevice.h (주요 필드 발췌) */
struct net_device {
/* ===== 식별 및 이름 ===== */
char name[IFNAMSIZ]; /* 인터페이스 이름 "eth0" */
int ifindex; /* 전역 고유 인덱스 */
unsigned int flags; /* IFF_UP, IFF_PROMISC 등 */
unsigned int priv_flags; /* IFF_802_1Q_VLAN 등 */
unsigned short type; /* ARPHRD_ETHER (1) */
/* ===== 주소 ===== */
unsigned char dev_addr[MAX_ADDR_LEN]; /* 하드웨어 MAC 주소 */
unsigned char broadcast[MAX_ADDR_LEN]; /* 브로드캐스트 주소 */
unsigned char addr_len; /* MAC 주소 길이 (6) */
/* ===== MTU 및 헤더 ===== */
unsigned int mtu; /* 현재 MTU */
unsigned int min_mtu; /* 최소 MTU (68) */
unsigned int max_mtu; /* 최대 MTU (드라이버 설정) */
unsigned short hard_header_len; /* ETH_HLEN (14) */
unsigned short needed_headroom; /* 추가 헤더 공간 */
unsigned short needed_tailroom; /* 추가 트레일러 공간 */
/* ===== 오퍼레이션 콜백 ===== */
const struct net_device_ops *netdev_ops; /* 핵심 콜백 테이블 */
const struct ethtool_ops *ethtool_ops; /* ethtool 콜백 */
const struct header_ops *header_ops; /* L2 헤더 조작 */
/* ===== 큐 관리 ===== */
unsigned int num_tx_queues; /* TX 큐 개수 */
unsigned int real_num_tx_queues; /* 실제 활성 TX 큐 */
unsigned int num_rx_queues; /* RX 큐 개수 */
unsigned int real_num_rx_queues; /* 실제 활성 RX 큐 */
struct netdev_queue *_tx; /* TX 큐 배열 */
/* ===== NAPI 및 오프로드 ===== */
struct list_head napi_list; /* NAPI 인스턴스 목록 */
netdev_features_t features; /* 활성 오프로드 기능 */
netdev_features_t hw_features; /* HW가 지원하는 기능 */
netdev_features_t wanted_features; /* 사용자가 요청한 기능 */
netdev_features_t vlan_features; /* VLAN 장치에 상속할 기능 */
/* ===== 통계 ===== */
struct net_device_stats stats; /* 기본 패킷 통계 */
atomic_long_t rx_dropped; /* RX 드롭 카운터 */
atomic_long_t tx_dropped; /* TX 드롭 카운터 */
/* ===== 드라이버 Private 데이터 (구조체 끝에 배치) ===== */
/* alloc_etherdev_mqs()가 net_device + priv를 연속 할당 */
};
alloc_etherdev_mqs() 내부 동작
alloc_etherdev_mqs()는 이더넷 net_device를 할당하는 표준 함수입니다. 내부적으로 alloc_netdev_mqs()를 호출하며, ether_setup()을 설정 콜백으로 전달합니다. 드라이버의 Private 데이터는 net_device 바로 뒤에 정렬(Align)되어 연속 할당됩니다.
/* net/ethernet/eth.c */
struct net_device *alloc_etherdev_mqs(int sizeof_priv,
unsigned int txqs,
unsigned int rxqs)
{
return alloc_netdev_mqs(sizeof_priv, "eth%d",
NET_NAME_UNKNOWN,
ether_setup, /* 이더넷 기본 설정 콜백 */
txqs, rxqs);
}
EXPORT_SYMBOL(alloc_etherdev_mqs);
/* net/core/dev.c — alloc_netdev_mqs 핵심 로직 */
struct net_device *alloc_netdev_mqs(int sizeof_priv,
const char *name, unsigned char name_assign_type,
void (*setup)(struct net_device *), unsigned int txqs, unsigned int rxqs)
{
struct net_device *dev;
unsigned int alloc_size;
/* net_device + 정렬 패딩 + private 데이터 크기 계산 */
alloc_size = sizeof(struct net_device);
alloc_size = ALIGN(alloc_size, NETDEV_ALIGN); /* 32바이트 정렬 */
alloc_size += sizeof_priv;
alloc_size += NETDEV_ALIGN - 1;
dev = kvzalloc(alloc_size, GFP_KERNEL_ACCOUNT);
if (!dev)
return NULL;
/* TX/RX 큐 배열 할당 */
dev->num_tx_queues = txqs;
dev->num_rx_queues = rxqs;
...
setup(dev); /* → ether_setup() 호출 */
...
return dev;
}
ether_setup() 기본 초기화
/* net/ethernet/eth.c — 이더넷 장치 기본값 설정 */
void ether_setup(struct net_device *dev)
{
dev->header_ops = ð_header_ops; /* L2 헤더 생성/파싱 */
dev->type = ARPHRD_ETHER; /* 이더넷 하드웨어 유형 */
dev->hard_header_len = ETH_HLEN; /* 14바이트 */
dev->min_header_len = ETH_HLEN;
dev->mtu = ETH_DATA_LEN; /* 1500 */
dev->min_mtu = ETH_MIN_MTU; /* 68 */
dev->max_mtu = ETH_DATA_LEN; /* 기본 1500, 드라이버가 재설정 */
dev->addr_len = ETH_ALEN; /* 6바이트 */
dev->flags = IFF_BROADCAST | IFF_MULTICAST;
dev->priv_flags |= IFF_TX_SKB_SHARING;
eth_broadcast_addr(dev->broadcast); /* ff:ff:ff:ff:ff:ff */
}
net_device_ops 주요 콜백 카탈로그
struct net_device_ops는 네트워크 장치의 모든 동작을 정의하는 함수 포인터 테이블입니다. 이더넷 드라이버가 구현해야 하는 핵심 콜백을 분류하면 다음과 같습니다.
| 카테고리 | 콜백 | 호출 시점 | 필수 여부 |
|---|---|---|---|
| 장치 제어 | ndo_open | ip link set up (IFF_UP) | 필수 |
ndo_stop | ip link set down | 필수 | |
| 송신 | ndo_start_xmit | 패킷 송신 요청 | 필수 |
| TX 큐 | ndo_select_queue | 멀티큐 TX 큐 선택 | 선택 |
| 링크 설정 | ndo_set_mac_address | MAC 주소 변경 | 권장 |
ndo_change_mtu | MTU 변경 요청 | 권장 | |
| 수신 필터 | ndo_set_rx_mode | 멀티캐스트/프로미스큐어스(Promiscuous) 변경 | 권장 |
ndo_set_features | 오프로드 기능 변경 | 선택 | |
| 통계 | ndo_get_stats64 | 통계 조회 (ip -s link) | 권장 |
| VLAN | ndo_vlan_rx_add_vid | VLAN ID 등록 | 선택 |
| IOCTL | ndo_eth_ioctl | PHY IOCTL 중계 | 선택 |
| TC/XDP | ndo_bpf | XDP 프로그램 로드/언로드 | 선택 |
| 타임스탬프 | ndo_hwtstamp_set | 하드웨어 타임스탬프 설정 | 선택 |
net_device 수명주기(Lifecycle)
이더넷 net_device는 할당부터 해제까지 명확한 상태 전이를 거칩니다. 드라이버 개발자는 이 수명주기를 이해해야 리소스 누수(Leak)와 use-after-free를 방지할 수 있습니다.
/* include/linux/netdevice.h — netdev_priv() */
static inline void *netdev_priv(const struct net_device *dev)
{
return (char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN);
}
/* 드라이버에서의 사용 예시 (e1000e) */
struct e1000_adapter *adapter = netdev_priv(netdev);
/* alloc_etherdev 호출 시 sizeof_priv에 드라이버 구조체 크기 전달 */
netdev = alloc_etherdev_mqs(sizeof(struct e1000_adapter), tx_queues, rx_queues);
이더넷 주소 필터링과 프로미스큐어스 모드
이더넷 NIC은 수신한 프레임 중 자신에게 해당하는 것만 호스트로 전달하기 위해 MAC 주소 필터링을 수행합니다. 필터링 방식은 NIC 하드웨어 능력에 따라 다르며, 리눅스 커널은 ndo_set_rx_mode 콜백을 통해 드라이버에 필터 설정을 요청합니다.
필터링 유형
| 유형 | 대상 | 하드웨어 필터 | 커널 플래그/함수 |
|---|---|---|---|
| 유니캐스트(Unicast) | 자신의 MAC 주소와 일치하는 프레임 | Perfect Match (기본) | dev->dev_addr |
| 브로드캐스트(Broadcast) | ff:ff:ff:ff:ff:ff 목적지 | 항상 수신 | IFF_BROADCAST |
| 멀티캐스트(Multicast) | 그룹 MAC 주소 (bit 0 of octet 0 = 1) | Perfect/Hash 필터 | IFF_MULTICAST |
| All-Multicast | 모든 멀티캐스트 프레임 | 멀티캐스트 필터 비활성화 | IFF_ALLMULTI |
| 프로미스큐어스 | 모든 프레임 (목적지 무관) | 모든 필터 비활성화 | IFF_PROMISC |
Perfect Match vs Hash 기반 필터링
Perfect Match 필터는 NIC 내부의 CAM(Content-Addressable Memory) 테이블에 MAC 주소를 정확히 저장합니다. 일치하는 프레임만 수신하므로 정확하지만 테이블 크기에 제한이 있습니다. Hash 기반 필터는 MAC 주소의 해시 값으로 비트맵(Bitmap)을 인덱싱하여 필터링합니다. 더 많은 주소를 처리할 수 있지만 해시 충돌로 인한 오탐(False Positive)이 발생할 수 있습니다.
프로미스큐어스 모드와 ndo_set_rx_mode
프로미스큐어스(Promiscuous) 모드는 NIC이 모든 프레임을 필터링 없이 호스트로 전달하는 모드입니다. tcpdump, 브릿지, 소프트웨어 스위치 등에서 필요합니다. 커널은 dev_set_rx_mode()를 통해 현재 플래그와 주소 목록을 드라이버에 전달합니다.
/* net/core/dev.c — RX 모드 설정 */
void dev_set_rx_mode(struct net_device *dev)
{
/* rtnl_lock 또는 netdev instance lock 보유 상태에서 호출 */
if (!(dev->flags & IFF_UP))
return;
if (!netif_device_present(dev))
return;
/* 드라이버의 ndo_set_rx_mode 콜백 호출 */
if (dev->netdev_ops->ndo_set_rx_mode)
dev->netdev_ops->ndo_set_rx_mode(dev);
}
/* __dev_set_rx_mode() — 플래그 결정 로직 */
void __dev_set_rx_mode(struct net_device *dev)
{
/* UC 주소가 하드웨어 필터 한계를 초과하면 프로미스큐어스로 전환 */
if (netdev_uc_count(dev) > dev->uc_promisc_limit)
dev->flags |= IFF_PROMISC;
/* MC 주소가 한계를 초과하면 All-Multicast로 전환 */
if (netdev_mc_count(dev) > dev->mc_limit)
dev->flags |= IFF_ALLMULTI;
dev_set_rx_mode(dev); /* 드라이버에 최종 설정 전달 */
}
드라이버 구현 예시 (Intel ice)
/* drivers/net/ethernet/intel/ice/ice_main.c — RX 모드 설정 */
static void ice_set_rx_mode(struct net_device *netdev)
{
struct ice_netdev_priv *np = netdev_priv(netdev);
struct ice_vsi *vsi = np->vsi;
if (!vsi || ice_is_switchdev_running(vsi->back))
return;
/* 프로미스큐어스/All-Multicast 플래그 확인 및 설정 */
if (netdev->flags & IFF_PROMISC) {
ice_fltr_set_vsi_promisc(
&vsi->back->hw, vsi->idx,
ICE_PROMISC_UCAST_RX | ICE_PROMISC_MCAST_RX);
} else {
ice_fltr_clear_vsi_promisc(...);
/* 유니캐스트 필터 동기화 */
__dev_uc_sync(netdev, ice_fltr_add_mac, ice_fltr_remove_mac);
if (netdev->flags & IFF_ALLMULTI) {
ice_fltr_set_vsi_promisc(
&vsi->back->hw, vsi->idx,
ICE_PROMISC_MCAST_RX);
} else {
/* 멀티캐스트 필터 동기화 */
__dev_mc_sync(netdev, ice_fltr_add_mac, ice_fltr_remove_mac);
}
}
}
# 프로미스큐어스 모드 활성화/비활성화
ip link set eth0 promisc on # IFF_PROMISC 설정
ip link set eth0 promisc off # IFF_PROMISC 해제
# All-Multicast 모드
ip link set eth0 allmulticast on
# 현재 RX 필터 상태 확인
ip -d link show eth0 | grep -E "PROMISC|ALLMULTI|MULTICAST"
# 유니캐스트/멀티캐스트 주소 목록 확인
ip maddr show dev eth0
# HW 필터 통계 (드라이버별 ethtool -S 출력)
ethtool -S eth0 | grep -i "filter\|promisc\|rx_mode"
# rx_unicast_filtered: 0
# rx_multicast_filtered: 0
# promisc_mode_changes: 3
brctl addif br0 eth0으로 인터페이스를 브릿지에 추가하면 자동으로 프로미스큐어스 모드가 활성화됩니다. 이는 브릿지가 모든 프레임을 받아서 소프트웨어적으로 포워딩(Forwarding) 결정을 해야 하기 때문입니다. 커널 3.15부터는 bridge vlan 필터링과 결합하여 필요한 VLAN의 프레임만 수신하도록 최적화할 수 있습니다.
패킷 송수신 경로
이더넷 프레임이 커널에서 처리되는 핵심 경로입니다.
NAPI 상세 동작과 Budget 관리
NAPI(New API)는 리눅스 커널의 네트워크 수신 처리 프레임워크로, 인터럽트(Interrupt)와 폴링(Polling)을 혼합하여 고속 네트워크에서 IRQ 오버헤드를 극적으로 줄입니다. 이 섹션에서는 NAPI의 내부 상태 머신, 버짓(Budget) 관리, 바쁜 폴링(Busy Polling), 그리고 GRO(Generic Receive Offload) 병합 로직을 커널 소스 수준에서 분석합니다.
Budget과 Weight 메커니즘
NAPI 폴링은 budget과 weight 두 가지 매개변수로 제어됩니다. net_rx_action()은 전체 budget(기본 300)을 각 NAPI 인스턴스에 배분하고, 각 드라이버는 자신의 weight(기본 64) 이하로 패킷을 처리합니다.
/* net/core/dev.c - net_rx_action() 핵심 루프 분석 */
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)); /* 기본 2ms */
int budget = READ_ONCE(netdev_budget); /* 기본 300 */
LIST_HEAD(list);
LIST_HEAD(repoll);
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 (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
return;
break;
}
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll); /* 각 NAPI에 weight만큼 할당 */
if (unlikely(budget <= 0 ||
time_after_eq(jiffies, time_limit))) {
sd->time_squeeze++; /* /proc/net/softnet_stat 에 반영 */
break;
}
}
/* ... repoll 리스트에 남은 NAPI를 다시 poll_list에 추가 ... */
}
/proc/net/softnet_stat의 세 번째 열이 time_squeeze입니다. 이 값이 지속적으로 증가하면 softirq budget이 부족하여 패킷 처리가 지연되고 있는 의미입니다. sysctl net.core.netdev_budget=600이나 net.core.netdev_budget_usecs=4000으로 늘려볼 수 있습니다.
/* net/core/dev.c - __napi_poll() 단일 NAPI 인스턴스 폴링 */
static int __napi_poll(struct napi_struct *n, bool *repoll)
{
int work, weight;
weight = n->weight; /* 드라이버가 netif_napi_add()에서 설정, 기본 NAPI_POLL_WEIGHT=64 */
work = n->poll(n, weight); /* 드라이버의 poll 콜백 호출 */
if (unlikely(work > weight))
netdev_err_once(n->dev,
"NAPI poll function %pS returned %d, exceeding budget of %d.\n",
n->poll, work, weight);
if (likely(work < weight))
goto out;
/* work == weight: budget 소진, 재스케줄 필요 */
if (napi_disable_pending(n)) {
*repoll = false;
} else {
*repoll = true; /* repoll 리스트에 추가하여 다음 턴에 계속 */
}
out:
return work;
}
napi_complete_done()과 IRQ 재활성화
드라이버의 poll 함수가 weight보다 적은 패킷을 처리했을 때 napi_complete_done()을 호출하여 NAPI 폴링을 종료하고 인터럽트를 재활성화합니다.
/* 드라이버 poll 함수의 전형적 패턴 (e1000e 예시) */
static int e1000_clean(struct napi_struct *napi, int budget)
{
int work_done = 0;
/* TX 완료 처리 */
e1000_clean_tx_irq(adapter);
/* RX 패킷 수신: 최대 budget개까지 처리 */
work_done = e1000_clean_rx_irq(adapter, budget);
if (work_done < budget) {
/* budget 미소진: 폴링 종료, IRQ 재활성화 */
napi_complete_done(napi, work_done);
e1000_irq_enable(adapter); /* NIC 인터럽트 마스크 해제 */
}
/* work_done == budget이면 그냥 반환 → 재스케줄됨 */
return work_done;
}
Busy Polling: 초저지연 패킷 수신
Busy Polling은 유저스페이스 스레드가 softirq를 거치지 않고 직접 드라이버의 NAPI poll 함수를 호출하여 패킷을 수신하는 기법입니다. 금융 거래(HFT), 게임 서버 등 마이크로초 단위 지연이 중요한 환경에서 사용합니다.
/* Busy polling 활성화 */
# 소켓별 설정 (마이크로초 단위)
int busy_poll_usec = 50; /* 50μs 동안 busy-poll */
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &busy_poll_usec, sizeof(busy_poll_usec));
# 전역 설정 (sysctl)
# net.core.busy_poll = 50 전역 busy-poll 타임아웃 (μs)
# net.core.busy_read = 50 recv()에서의 busy-poll 타임아웃 (μs)
# net.core.prefer_busy_poll = 1 softirq보다 busy-poll 우선
/* net/core/dev.c - napi_busy_loop() 핵심 로직 */
void napi_busy_loop(unsigned int napi_id,
bool (*loop_end)(void *, unsigned long),
void *loop_end_arg, bool prefer_busy_poll,
u16 budget)
{
unsigned long start_time = loop_end ? busy_loop_current_time() : 0;
struct napi_struct *napi;
napi = napi_by_id(napi_id);
if (!napi)
return;
for (;;) {
if (napi_poll) { /* NAPI가 스케줄 상태이면 */
napi->poll(napi, budget); /* 프로세스 컨텍스트에서 직접 poll */
}
if (loop_end && loop_end(loop_end_arg, start_time))
break; /* 타임아웃 또는 데이터 수신 완료 */
if (unlikely(need_resched())) {
if (loop_end && !loop_end(loop_end_arg, start_time))
cond_resched();
else
break;
}
cpu_relax(); /* spin hint (pause on x86) */
}
}
GRO (Generic Receive Offload) 내부 동작
GRO는 동일한 플로우에 속하는 여러 소형 패킷을 하나의 대형 skb로 병합하여 상위 스택 처리 횟수를 줄입니다. LRO(Large Receive Offload)의 소프트웨어 대체이며, 헤더를 정확히 보존하므로 포워딩 경로에서도 안전합니다.
/* net/core/gro.c - napi_gro_receive() 핵심 흐름 */
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
gro_result_t ret;
skb_gro_reset_offset(skb);
ret = dev_gro_receive(napi, skb); /* 프로토콜별 GRO 콜백 체인 */
if (ret == GRO_NORMAL)
gro_normal_one(napi, skb, 1); /* 병합 불가: 개별 전달 */
return ret;
}
/* dev_gro_receive()에서 수행되는 병합 판단:
* 1. gro_list를 순회하며 동일 플로우(flow) 검색
* 2. 프로토콜 gro_receive 콜백 호출 (inet_gro_receive → tcp_gro_receive)
* 3. tcp_gro_receive: TCP 시퀀스 번호 연속성 확인
* 4. 병합 성공 시 skb_gro_receive()로 frag 추가
* 5. NAPI_GRO_CB(skb)->count가 증가
* 6. gro_flush_oldest()로 오래된 항목 강제 플러시
*/
/* GRO 플러시 조건 */
#define MAX_GRO_SKBS 8 /* gro_list 최대 항목 수 */
/* gro_list에 MAX_GRO_SKBS개 이상 쌓이면 가장 오래된 것부터 플러시 */
/* 추가 플러시 트리거:
* - napi_complete_done() 호출 시 잔여 gro_list 전체 플러시
* - gro_normal_list가 tp->gro_max_size 초과 시
* - 타이머 기반: gro_flush_timeout (sysctl)
*/
GRO 관련 sysctl 튜닝
# GRO 플러시 타임아웃 (나노초) — 0이면 즉시 플러시
sysctl net.core.gro_flush_timeout=20000
# GRO normal list 크기 제한 — 이 수만큼 모아서 한 번에 스택에 전달
sysctl net.core.gro_normal_batch=16
# per-device GRO 활성화/비활성화
ethtool -K eth0 gro on
ethtool -K eth0 gro off
# GRO 효과 확인: 병합된 패킷 크기 분포 모니터링
ethtool -S eth0 | grep gro
성능 비교: IRQ-only vs NAPI vs Busy-Poll
| 항목 | IRQ-only (구형) | NAPI (기본) | Busy-Poll |
|---|---|---|---|
| 패킷당 인터럽트 | 1:1 (매 패킷마다 IRQ) | 1:N (한 번 IRQ → 여러 패킷 poll) | 0 (인터럽트 없이 직접 poll) |
| 지연 시간 | 20~50μs | 5~15μs | 1~3μs |
| CPU 사용률 (idle) | 낮음 | 낮음 | 높음 (spin-wait) |
| CPU 사용률 (패킷 폭주) | 매우 높음 (IRQ storm) | 중간 (폴링 전환) | 높음 (지속 poll) |
| 처리량 (10G) | ~2 Mpps (IRQ bottleneck) | ~8-12 Mpps | ~12-14 Mpps |
| 적합한 워크로드 | 저속/간헐적 | 범용 | HFT, 실시간 |
| 커널 버전 | 2.4 이전 | 2.6+ | 3.11+ (SO_BUSY_POLL) |
isolcpus로 격리하고 해당 코어에 NIC IRQ와 애플리케이션 스레드를 고정(pinning)하는 것이 좋습니다. net.core.prefer_busy_poll=1을 설정하면 softirq보다 busy-poll 경로를 우선합니다.
멀티큐와 RSS 아키텍처 상세
현대 NIC은 수십 개의 하드웨어 큐(Queue)를 갖추고 있으며, 각 큐를 독립된 CPU 코어에 매핑하여 병렬 패킷 처리를 수행합니다. 이를 가능하게 하는 핵심 기술이 RSS(Receive Side Scaling)와 관련 소프트웨어 스티어링(Steering) 메커니즘입니다.
RSS 설정과 모니터링
# RSS 해시 키 및 indirection table 조회
ethtool -x eth0
# 출력 예시:
# RX flow hash indirection table for eth0 with 8 RX ring(s):
# 0: 0 1 2 3 4 5 6 7
# 8: 0 1 2 3 4 5 6 7
# ...
# RSS hash key:
# 6d:5a:56:da:25:5b:0e:c2:41:67:25:3d:43:a3:8f:b0:d0:...
# indirection table 변경 (큐 0-3만 사용)
ethtool -X eth0 equal 4
# 특정 가중치로 indirection table 설정 (큐 0에 2배 가중치)
ethtool -X eth0 weight 2 1 1 1
# RSS 해시 키 직접 설정 (대칭 키 예시)
ethtool -X eth0 hkey 6d:5a:56:da:25:5b:0e:c2:41:67:25:3d:43:a3:8f:b0:d0:ca:2b:cb:ae:7b:30:b4:77:cb:2d:a3:80:30:f2:0c:6a:42:b7:3b:be:ac:01:fa
# 해시 필드 설정: TCP 4-tuple 사용
ethtool -N eth0 rx-flow-hash tcp4 sdfn
# s=src IP, d=dst IP, f=src port, n=dst port
# 해시 필드 확인
ethtool -n eth0 rx-flow-hash tcp4
RPS와 RFS: 소프트웨어 패킷 스티어링
RPS(Receive Packet Steering)는 RSS의 소프트웨어 구현으로, 하드웨어 멀티큐를 지원하지 않는 NIC에서도 패킷을 여러 CPU로 분산합니다. RFS(Receive Flow Steering)는 RPS를 확장하여 패킷을 해당 플로우를 처리하는 애플리케이션이 실행 중인 CPU로 보내 캐시 적중률을 높입니다.
/* net/core/dev.c - get_rps_cpu() 핵심 로직 (RPS) */
static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
struct rps_dev_flow **rflowp)
{
const struct rps_sock_flow_table *sock_flow_table;
struct netdev_rx_queue *rxqueue = dev->_rx;
struct rps_map *map;
struct rps_dev_flow_table *flow_table;
u32 hash, next_cpu, ident;
int cpu = -1;
/* 1단계: skb 해시 계산 (skb_get_hash) */
hash = skb_get_hash(skb);
if (!hash)
goto done;
/* 2단계: RFS 플로우 테이블 확인 (설정된 경우) */
sock_flow_table = rcu_dereference(rps_sock_flow_table);
flow_table = rcu_dereference(rxqueue->rps_flow_table);
if (flow_table && sock_flow_table) {
ident = sock_flow_table->ents[hash & sock_flow_table->mask];
/* RFS: 마지막으로 이 플로우를 처리한 앱의 CPU를 선택 */
next_cpu = ident & rps_cpu_mask;
if (cpu_online(next_cpu)) {
cpu = next_cpu;
goto done;
}
}
/* 3단계: RPS 맵에서 해시 기반 CPU 선택 */
map = rcu_dereference(rxqueue->rps_map);
if (map) {
cpu = map->cpus[((u64)hash * map->len) >> 32];
}
done:
return cpu;
}
RPS/RFS/XPS 설정
# RPS: CPU 비트마스크 설정 (큐 0에 CPU 0-3 매핑)
echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus
# RPS 플로우 테이블 크기 (큐당)
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
# RFS: 전역 플로우 테이블 크기 (활성 연결 수에 맞춤)
sysctl net.core.rps_sock_flow_entries=32768
# XPS: TX 큐 0을 CPU 0에 매핑
echo "1" > /sys/class/net/eth0/queues/tx-0/xps_cpus
echo "2" > /sys/class/net/eth0/queues/tx-1/xps_cpus
echo "4" > /sys/class/net/eth0/queues/tx-2/xps_cpus
echo "8" > /sys/class/net/eth0/queues/tx-3/xps_cpus
# aRFS 활성화 (드라이버 지원 필요)
ethtool -K eth0 ntuple on
# ntuple 필터로 특정 플로우를 특정 큐로 스티어링
ethtool -N eth0 flow-type tcp4 src-ip 192.168.1.10 dst-port 80 action 2
# 현재 ntuple 규칙 확인
ethtool -n eth0
# 큐별 패킷 통계 확인
ethtool -S eth0 | grep rx_queue
스티어링 메커니즘 비교
| 메커니즘 | 방향 | 계층 | 해시 위치 | 플로우 친화성 | 설정 |
|---|---|---|---|---|---|
| RSS | RX | 하드웨어 | NIC (Toeplitz) | 없음 (해시 기반) | ethtool -X |
| RPS | RX | 소프트웨어 | 커널 (jhash) | 없음 (해시 기반) | rps_cpus |
| RFS | RX | 소프트웨어 | 커널 + 소켓 | 있음 (앱 CPU 추적) | rps_sock_flow_entries |
| aRFS | RX | 하드웨어 | NIC (Flow Director) | 있음 (HW 프로그래밍) | ethtool -K ntuple on |
| XPS | TX | 소프트웨어 | CPU 매핑 | TX lock 최적화 | xps_cpus |
| ntuple | RX | 하드웨어 | 정확 매칭 | 규칙 기반 | ethtool -N |
하드웨어 오프로드
현대 NIC은 CPU 부담을 줄이기 위해 다양한 하드웨어 오프로드를 지원합니다.
오프로드 유형
| 오프로드 | 방향 | 기능 | ethtool 기능명 |
|---|---|---|---|
| TX Checksum | 송신 | IP/TCP/UDP 체크섬(Checksum)을 NIC에서 계산 | tx-checksum-ipv4 |
| RX Checksum | 수신 | 수신 패킷 체크섬 HW 검증 | rx-checksum |
| TSO (TCP Segmentation Offload) | 송신 | 대형 TCP 세그먼트를 NIC에서 분할 | tx-tcp-segmentation |
| GSO (Generic Segmentation Offload) | 송신 | SW 폴백 세그멘테이션 | tx-generic-segmentation |
| GRO (Generic Receive Offload) | 수신 | 소형 패킷을 대형으로 병합 | rx-gro |
| LRO (Large Receive Offload) | 수신 | HW에서 대형 세그먼트 조합 (deprecated) | rx-lro |
| RSS (Receive Side Scaling) | 수신 | 해시(Hash) 기반 멀티큐 분산 | rx-hashing |
| Scatter-Gather | 송신 | 비연속 메모리 DMA 전송 | tx-scatter-gather |
오프로드 확인/설정
# 현재 오프로드 상태 확인
ethtool -k eth0
# TSO 비활성화 (디버깅용)
ethtool -K eth0 tso off
# GRO 비활성화
ethtool -K eth0 gro off
# RSS 해시 키/indirection table 확인
ethtool -x eth0
# RSS 해시 필드 설정 (TCP 4-tuple)
ethtool -N eth0 rx-flow-hash tcp4 sdfn
# 링 버퍼 크기 조정
ethtool -G eth0 rx 4096 tx 4096
# 인터럽트 병합 (Coalescing) 설정
ethtool -C eth0 rx-usecs 50 rx-frames 64
Scatter-Gather DMA와 체크섬 오프로드 구현
고성능 이더넷 드라이버는 비연속(Non-contiguous) 메모리 영역에 분산된 패킷 데이터를 복사 없이 DMA로 전송하는 Scatter-Gather I/O를 사용합니다. 이 메커니즘은 skb_shared_info의 frags[] 배열과 DMA 매핑 API를 통해 구현됩니다.
/* 드라이버에서의 Scatter-Gather TX 처리 (ice 드라이버 기반) */
static netdev_tx_t ice_xmit_frame(struct sk_buff *skb,
struct net_device *dev)
{
struct ice_tx_ring *tx_ring;
struct ice_tx_desc *tx_desc;
dma_addr_t dma;
unsigned int i, nr_frags;
/* 1. 선형 데이터(헤더) DMA 매핑 */
dma = dma_map_single(tx_ring->dev,
skb->data,
skb_headlen(skb), /* head 영역 크기 */
DMA_TO_DEVICE);
if (dma_mapping_error(tx_ring->dev, dma))
goto dma_error;
/* 첫 번째 TX descriptor: 선형 데이터 */
tx_desc = ICE_TX_DESC(tx_ring, i);
tx_desc->buf_addr = cpu_to_le64(dma);
tx_desc->cmd_type_offset_bsz = ice_build_ctob(
ICE_TX_DESC_CMD_IIPT_IPV4, /* IP 유형 */
skb_headlen(skb));
/* 2. 페이지 프래그먼트들 DMA 매핑 */
nr_frags = skb_shinfo(skb)->nr_frags;
for (i = 0; i < nr_frags; i++) {
skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
dma = skb_frag_dma_map(tx_ring->dev, /* dma_map_page 래퍼 */
frag, 0,
skb_frag_size(frag),
DMA_TO_DEVICE);
tx_desc = ICE_TX_DESC(tx_ring, i);
tx_desc->buf_addr = cpu_to_le64(dma);
/* 마지막 fragment에 EOP(End-of-Packet) 플래그 설정 */
if (i == nr_frags - 1)
tx_desc->cmd_type_offset_bsz |= ICE_TX_DESC_CMD_EOP;
}
/* 3. Doorbell: NIC에 새 descriptor 알림 */
writel(i, tx_ring->tail);
return NETDEV_TX_OK;
}
TX 체크섬 오프로드 내부
TX 체크섬 오프로드는 CHECKSUM_PARTIAL 모드를 사용합니다. 커널은 의사 헤더(Pseudo Header) 체크섬만 계산하고, NIC이 패킷 전송 시 나머지 체크섬을 완성합니다.
/* net/core/dev.c - validate_xmit_skb() 체크섬 처리 */
static struct sk_buff *validate_xmit_skb(struct sk_buff *skb,
struct net_device *dev,
bool *again)
{
/* ... */
if (skb->ip_summed == CHECKSUM_PARTIAL) {
if (skb_needs_check(skb, true) &&
!skb_is_gso(skb)) {
/* NIC이 이 프로토콜의 체크섬 오프로드를 지원하는지 확인 */
if (!(features & NETIF_F_CSUM_MASK)) {
/* 미지원: 소프트웨어로 체크섬 계산 */
if (skb_checksum_help(skb))
goto out_kfree_skb;
}
}
}
/* ... */
}
/* include/linux/skbuff.h - 체크섬 오프로드 힌트 */
struct sk_buff {
/* ... */
__u8 ip_summed:2; /* CHECKSUM_NONE/PARTIAL/UNNECESSARY/COMPLETE */
__u16 csum_start; /* 체크섬 계산 시작점 (skb->head 기준 오프셋) */
__u16 csum_offset; /* csum_start부터 결과 저장 위치까지의 오프셋 */
/* ... */
};
/* TCP의 경우:
* csum_start = IP 헤더 이후 TCP 헤더 시작점
* csum_offset = 16 (TCP 체크섬 필드의 TCP 헤더 내 오프셋)
* NIC은 csum_start부터 패킷 끝까지 1의 보수 합을 계산하여
* csum_start + csum_offset 위치에 기록합니다.
*/
/* net/core/skbuff.c - skb_checksum_help() SW 폴백 */
int skb_checksum_help(struct sk_buff *skb)
{
__wsum csum;
int ret = 0, offset;
if (skb->ip_summed == CHECKSUM_COMPLETE)
goto out_set_summed;
/* 비선형 영역을 선형화 (paged data → linear) */
if (skb_has_shared_frag(skb) &&
__skb_linearize(skb))
return -ENOMEM;
offset = skb_checksum_start_offset(skb);
/* csum_start부터 끝까지 체크섬 계산 */
csum = skb_checksum(skb, offset,
skb->len - offset, 0);
/* 결과를 패킷 내 체크섬 필드에 기록 */
offset += skb->csum_offset;
*(__sum16 *)(skb->data + offset) = csum_fold(csum);
out_set_summed:
skb->ip_summed = CHECKSUM_NONE; /* SW에서 이미 계산 완료 */
return ret;
}
dma_map_*()이 IOVA(I/O Virtual Address)를 반환하며, NIC은 이 가상 주소로 DMA를 수행합니다. IOMMU가 없는 시스템에서는 SWIOTLB가 bounce buffer를 사용하여 물리 주소 제한(예: 32비트 DMA 한계)을 우회합니다. dma_mapping_error() 검사를 반드시 수행해야 하며, 이를 생략하면 DMA 실패 시 커널 패닉이 발생할 수 있습니다.
Pause 프레임과 흐름 제어(Flow Control)
IEEE 802.3x Pause Frame은 수신 버퍼 오버플로(Buffer Overflow)우 방지를 위한 L2 흐름 제어 메커니즘입니다.
흐름 제어 유형
| 유형 | 표준 | 동작 | 단점 |
|---|---|---|---|
| Symmetric Pause | 802.3x | 양쪽 모두 Pause 전송 가능 | 한 포트 혼잡 시 전체 링크 중단 |
| Asymmetric Pause | 802.3x | 한쪽만 Pause 전송 | 유연하지만 설정 복잡 |
| PFC (Priority Flow Control) | 802.1Qbb | 우선순위별 독립 제어 (8개 클래스) | DCB 전체 구성 필요 |
# Pause 프레임 설정
ethtool -A eth0 rx on tx on
# PFC 설정 (DCB)
mlnx_qos -i eth0 --pfc 0,0,0,1,0,0,0,0 # TC3만 PFC 활성화
ethtool -A ethX rx on tx on으로 활성화해야 합니다.
한쪽만 활성화하면 포트가 hang 상태에 빠질 수 있으므로 반드시 양단 설정을 동기화하세요.
# ICE 흐름 제어 확인 및 양단 동기화
ethtool -a eth0 # RX/TX Pause 상태 확인
ethtool -A eth0 rx on tx on # 양측 모두 활성화 필수
ECN과 DCB
ECN(Explicit Congestion Notification, RFC 3168)은 패킷 드롭 없이 혼잡을 알리는 메커니즘입니다. DCB(Data Center Bridging)는 데이터센터 이더넷에서 무손실 전송을 위한 IEEE 표준 집합입니다.
ECN 마킹 과정
송신 측이 IP 헤더의 ECN 필드를 ECT(0) 또는 ECT(1)로 설정하면, 혼잡한 스위치/라우터가 이를 CE(Congestion Experienced)로 변경합니다. 수신 측은 TCP ACK에 ECE 플래그를 설정하여 송신 측에 혼잡을 알리고, 송신 측은 CWR 플래그로 윈도우 축소를 확인합니다.
DCB 컴포넌트
| 컴포넌트 | 표준 | 기능 | 리눅스 도구 |
|---|---|---|---|
| ETS (Enhanced Transmission Selection) | 802.1Qaz | 트래픽 클래스별 대역폭 보장/분배 | mlnx_qos, lldptool |
| PFC (Priority Flow Control) | 802.1Qbb | 우선순위별 독립 흐름 제어 (8 TC) | mlnx_qos --pfc |
| CN (Congestion Notification) | 802.1Qau | 혼잡 지점에서 rate limiter 피드백 | lldptool |
| DCBX | 802.1Qaz | DCB 파라미터 자동 협상 (LLDP 확장) | lldpad, dcbtool |
# ECN 활성화 (리눅스 커널)
sysctl -w net.ipv4.tcp_ecn=1 # ECN 활성화 (요청 및 수락)
sysctl -w net.ipv4.tcp_ecn=2 # ECN 서버 모드 (수락만)
# DCB 상태 확인
mlnx_qos -i eth0
# ETS (Enhanced Transmission Selection) 대역폭 분배
mlnx_qos -i eth0 --tc_bw 10,10,10,40,10,10,5,5
# DCBX 모드 확인/설정
lldptool -ti eth0 -V IEEE-DCBX
lldptool -Ti eth0 -V ETS-CFG willing=yes
# RoCE (RDMA over Converged Ethernet)용 PFC + ECN 설정
# TC3에 PFC 활성화, ECN으로 혼잡 관리
mlnx_qos -i eth0 --pfc 0,0,0,1,0,0,0,0
sysctl -w net.ipv4.tcp_ecn=1
ICE DCB 운영 — DCBX 모드와 FW-LLDP
Intel E810(ice)은 DCBX 협상을 펌웨어(FW-LLDP) 또는 소프트웨어(SW-LLDP)에서 처리할 수 있습니다. 기본값은 FW-LLDP이며, lldpad와의 통합이 필요한 환경에서는 SW-LLDP로 전환합니다.
| 모드 | LLDP 에이전트 | 장점 | 제약 |
|---|---|---|---|
| FW-LLDP (기본) | 펌웨어가 LLDPDU 송수신 | OS 부팅 전에도 DCBX 협상 가능 | lldpad와 동시 사용 불가 |
| SW-LLDP | lldpad 데몬이 관리 | 유연한 정책 제어, 다중 NIC 통합 관리 | OS 준비 전 DCBX 미동작 |
# FW-LLDP ↔ SW-LLDP 전환 (private flag)
ethtool --set-priv-flags eth0 fw-lldp-agent off # SW-LLDP 활성화
ethtool --set-priv-flags eth0 fw-lldp-agent on # FW-LLDP 복원
# 현재 LLDP 모드 확인
ethtool --show-priv-flags eth0 | grep fw-lldp
# ICE DCB 상태 확인
lldptool -ti eth0 -V IEEE-DCBX
# TC0에 최소 대역폭 할당 (ICE 필수 조건: TC0은 항상 존재해야 함)
lldptool -Ti eth0 -V ETS-CFG tcbw=50,50,0,0,0,0,0,0 tsa=ets,ets,strict,strict,strict,strict,strict,strict up2tc=0:0,1:0,2:1,3:1,4:0,5:0,6:0,7:0
lldpad를 동시에 구동하면 LLDPDU 충돌이 발생하므로 한쪽을 반드시 비활성화하세요.
VLAN과 QinQ
IEEE 802.1Q VLAN 태깅은 하나의 물리적 이더넷 링크에서 논리적 네트워크를 분리합니다.
VLAN 태그 구조 (4바이트)
| 필드 | 비트 | 설명 |
|---|---|---|
| TPID | 16 | Tag Protocol Identifier (0x8100) |
| PCP | 3 | Priority Code Point (QoS 우선순위(Priority) 0~7) |
| DEI | 1 | Drop Eligible Indicator |
| VID | 12 | VLAN Identifier (0~4095, 유효: 1~4094) |
리눅스 VLAN 설정
# VLAN 인터페이스 생성
ip link add link eth0 name eth0.100 type vlan id 100
ip link set eth0.100 up
ip addr add 10.0.100.1/24 dev eth0.100
# QinQ (802.1ad) — 서비스 프로바이더 VLAN
ip link add link eth0 name eth0.200 type vlan proto 802.1ad id 200
ip link add link eth0.200 name eth0.200.10 type vlan id 10
# VLAN 오프로드 확인
ethtool -k eth0 | grep vlan
- RX 체크섬 오프로드 미지원 — QinQ 패킷의 하드웨어 체크섬 검증이 동작하지 않으므로 소프트웨어 검증에 의존
- VLAN stripping 비활성화 필요 — QinQ 동작을 위해
ethtool -K eth0 rxvlan off로 HW VLAN stripping을 꺼야 함 - VF QinQ 송신 조건 — VF에서 QinQ 패킷을 송신하려면 PF에서 해당 VF에 outer VLAN을 할당해야 함 (
ip link set eth0 vf 0 vlan 200 proto 802.1ad)
EEE (Energy Efficient Ethernet)
IEEE 802.3az Energy Efficient Ethernet은 트래픽이 없을 때 PHY를 저전력 유휴(LPI) 상태로 전환하여 에너지를 절약합니다.
# EEE 상태 확인
ethtool --show-eee eth0
# EEE 비활성화 (지연 민감 환경)
ethtool --set-eee eth0 eee off
# EEE 활성화 (1GbE만)
ethtool --set-eee eth0 eee on advertise 0x8
TSN (Time-Sensitive Networking)
IEEE 802.1 TSN 태스크(Task) 그룹은 이더넷 위에 확정적 지연(Deterministic Latency)과 시간 동기화를 구현하는 표준 집합입니다. 산업 자동화, 자동차, 방송 분야에서 전용 필드버스를 대체합니다.
핵심 TSN 표준
| 표준 | 이름 | 기능 |
|---|---|---|
| 802.1AS | gPTP (Generalized PTP) | 네트워크 전체 시간 동기화 (<1 μs 정밀도) |
| 802.1Qbv | Time-Aware Shaper (TAS) | 시간 기반 게이트 제어, 트래픽 클래스별 전송 시간대 할당 |
| 802.1Qbu/802.3br | Frame Preemption | 우선순위 높은 프레임이 낮은 프레임 전송을 중단 가능 |
| 802.1CB | FRER (Frame Replication and Elimination) | 프레임 복제로 무손실 전송 보장 |
| 802.1Qci | Per-Stream Filtering and Policing | 스트림별 필터링/폴리싱 |
| 802.1Qcc | Stream Reservation Protocol | 경로/대역폭 예약 관리 |
리눅스 TSN 지원
# TAS (Time-Aware Shaper) 설정 - taprio qdisc
tc qdisc replace dev eth0 parent root handle 100 taprio \
num_tc 3 \
map 2 2 1 0 2 2 2 2 2 2 2 2 2 2 2 2 \
queues 1@0 1@1 2@2 \
base-time 0 \
sched-entry S 01 300000 \
sched-entry S 02 300000 \
sched-entry S 04 400000 \
flags 0x2
# CBS (Credit-Based Shaper) - cbs qdisc
tc qdisc add dev eth0 parent 100:1 cbs \
idleslope 98688 sendslope -901312 \
hicredit 153 locredit -1389
# ETF (Earliest TxTime First) - 정밀 전송 시간 제어
tc qdisc add dev eth0 parent 100:2 etf \
clockid CLOCK_TAI delta 200000 offload
산업용 이더넷
표준 이더넷 프레임을 기반으로 실시간 산업 통신을 구현하는 프로토콜들입니다.
| 프로토콜 | 계층 | 사이클 시간 | 리눅스 지원 |
|---|---|---|---|
| PROFINET RT | L2 (EtherType 0x8892) | ~1 ms | Raw socket / AF_PACKET |
| PROFINET IRT | L2 + HW 스케줄링 | 31.25 μs | TSN + 전용 NIC |
| EtherCAT | L2 (EtherType 0x88A4) | ~50 μs | IgH EtherCAT Master |
| EtherNet/IP | TCP/UDP (CIP) | ~1 ms | 표준 소켓(Socket) API |
| Modbus/TCP | TCP (포트 502) | ~10 ms | 표준 소켓 API |
| OPC UA TSN | TSN + UDP | <1 ms | open62541 + taprio |
MACsec (IEEE 802.1AE) 계층 2 암호화(Encryption)
MACsec(Media Access Control Security, IEEE 802.1AE)는 이더넷 프레임을 링크 단위로 암호화하는 L2 보안 프로토콜입니다. IPsec(L3)이나 TLS(L4+)와 달리 이더넷 헤더 직후에서 암호화가 이루어져, ARP, DHCP, LLDP 등 모든 L2 프로토콜 트래픽을 보호합니다.
MACsec 프레임 구조
MACsec는 원본 이더넷 프레임에 SecTAG(Security Tag, 8~16바이트)와 ICV(Integrity Check Value, 8~16바이트)를 추가합니다. EtherType 0x88E5로 식별됩니다.
MKA (MACsec Key Agreement)
IEEE 802.1X-2010에 정의된 MKA(MACsec Key Agreement) 프로토콜은 링크 양단에서 암호화 키를 자동으로 교환하고 갱신합니다.
| 키 유형 | 역할 | 생성 |
|---|---|---|
| CAK (Connectivity Association Key) | 마스터 키, MKA 세션의 기반 | 사전 공유(PSK) 또는 802.1X EAP |
| CKN (CAK Name) | CAK 식별자 | CAK와 함께 설정 |
| SAK (Secure Association Key) | 실제 데이터 암호화 키 | CAK에서 KDF로 자동 파생 |
| KEK (Key Encrypting Key) | SAK 전송 시 암호화용 | CAK에서 자동 파생 |
| ICK (ICV Key) | MKPDU 무결성(Integrity) 검증 | CAK에서 자동 파생 |
MACsec 동작 모드
| 모드 | 암호화 | 무결성 | 용도 |
|---|---|---|---|
| GCM-AES-128 | O | O | 기본 모드 (대부분 HW 지원) |
| GCM-AES-256 | O | O | 높은 보안 요구 (일부 NIC) |
| GCM-AES-XPN-128/256 | O | O | 확장 PN (고속 링크, 64비트 PN) |
| Integrity Only | X | O | 무결성만 검증 (ICV만 추가) |
리눅스 MACsec 설정
# MACsec 인터페이스 생성 (PSK 모드)
ip link add link eth0 macsec0 type macsec sci 1 encrypt on
# 수신 SA 추가 (피어로부터 수신하는 키)
ip macsec add macsec0 rx sci 0x0002 sa 0 pn 1 on \
key 00 dffafc8d7b9a43d5b9a3dfbbf6a30c16
# 송신 SA 추가 (자신이 송신하는 키)
ip macsec add macsec0 tx sa 0 pn 1 on \
key 01 ead3664f508eb06c40ac7104cdae4ce2
# MACsec 인터페이스 활성화
ip link set macsec0 up
ip addr add 192.168.100.1/24 dev macsec0
# MACsec 상태 확인
ip macsec show
# wpa_supplicant MKA 자동 키 교환 (802.1X 연동)
# /etc/wpa_supplicant/wpa_supplicant-macsec.conf
# network={
# key_mgmt=IEEE8021X
# eapol_flags=0
# macsec_policy=1
# mka_cak=dffafc8d7b9a43d5b9a3dfbbf6a30c16
# mka_ckn=31323334353637383930313233343536
# mka_priority=255
# }
# HW 오프로드 확인 (지원 NIC: Intel E810, Mellanox ConnectX-6 Dx+)
ethtool -k eth0 | grep macsec
ip link add link eth0 macsec0 type macsec offload mac
offload mac 옵션으로 활성화합니다.
SR-IOV와 이더넷 가상화(Virtualization)
SR-IOV(Single Root I/O Virtualization, PCI-SIG 규격)는 하나의 물리 NIC(PF, Physical Function)에서 다수의 가상 NIC(VF, Virtual Function)를 생성하여, 가상머신(VM)이나 컨테이너에 직접 할당할 수 있게 합니다. 하이퍼바이저(Hypervisor)의 가상 스위치를 우회하므로 네이티브에 가까운 성능을 달성합니다.
SR-IOV VF 생성 및 관리
# VF 생성 (최대 수는 NIC마다 다름)
echo 4 > /sys/class/net/eth0/device/sriov_numvfs
# VF 확인
ip link show eth0
# ... vf 0 MAC aa:bb:cc:dd:ee:00, spoof checking on, link-state auto
# ... vf 1 MAC aa:bb:cc:dd:ee:01, spoof checking on, link-state auto
# VF MAC 주소 설정 (PF에서)
ip link set eth0 vf 0 mac aa:bb:cc:dd:ee:00
# VF VLAN 태그 설정
ip link set eth0 vf 0 vlan 100
# VF 대역폭 제한 (Mbps)
ip link set eth0 vf 0 max_tx_rate 1000 min_tx_rate 100
# VF Spoof 체크 비활성화 (DPDK 등 직접 MAC 설정 시)
ip link set eth0 vf 0 spoofchk off
# VF Trust 모드 (Promiscuous 허용)
ip link set eth0 vf 0 trust on
# VF를 QEMU/KVM에 직접 할당 (VFIO passthrough)
# 1. IOMMU 활성화 (커널 파라미터: intel_iommu=on iommu=pt)
# 2. VF를 vfio-pci에 바인딩
echo "0000:03:02.0" > /sys/bus/pci/drivers/ice/unbind
echo "vfio-pci" > /sys/bus/pci/devices/0000:03:02.0/driver_override
echo "0000:03:02.0" > /sys/bus/pci/drivers/vfio-pci/bind
# eSwitch 모드 전환 (Mellanox switchdev)
devlink dev eswitch set pci/0000:03:00.0 mode switchdev
devlink dev eswitch show pci/0000:03:00.0
- virtio-net (에뮬레이션): ~5 Gbps, CPU 사용률 높음, vhost-net으로 개선 가능
- macvtap: ~8 Gbps, 호스트 네트워크 스택 일부 우회
- SR-IOV VF (VFIO): ~25 Gbps (25GbE NIC), 네이티브의 95%+, CPU 오버헤드 최소
- DPDK + VF: 라인 레이트(25~100 Gbps), 유저스페이스 직접 처리, 커널 우회
switchdev 모드에서 VF representor를 OVS에 연결하여 마이그레이션을 지원하는 방식을 제공합니다. 또한 IOMMU 그룹 단위로 할당되므로, 동일 IOMMU 그룹의 모든 디바이스가 함께 할당됩니다.
DSA (Distributed Switch Architecture)
DSA는 리눅스 커널의 이더넷 스위치 칩 추상화 프레임워크입니다. SoC에 내장되거나 MDIO/SPI로 연결된 이더넷 스위치(Marvell, Microchip, Realtek 등)를 표준 net_device로 노출하여, VLAN, 브릿지, STP 등 기존 리눅스 네트워크 도구로 스위치를 제어할 수 있게 합니다.
DSA 설정 및 관리
# DSA 포트 확인
ip link show type dsa
# DSA 스위치 포트를 브릿지에 추가
ip link add br0 type bridge vlan_filtering 1
ip link set lan0 master br0
ip link set lan1 master br0
ip link set lan2 master br0
# 포트별 VLAN 설정 (HW 오프로드)
bridge vlan add dev lan0 vid 10 pvid untagged
bridge vlan add dev lan1 vid 20 pvid untagged
bridge vlan add dev lan2 vid 10
bridge vlan add dev lan2 vid 20 # trunk 포트
# FDB (포워딩 데이터베이스) 확인
bridge fdb show br br0
# DSA 태깅 프로토콜 확인
cat /sys/class/net/eth0/dsa/tagging
# Device Tree에서의 DSA 정의 예시
# &mdio {
# switch@0 {
# compatible = "marvell,mv88e6085";
# reg = <0>;
# ports {
# port@0 { label = "lan0"; };
# port@5 { label = "cpu"; ethernet = <&gmac0>; };
# };
# };
# };
net_device로 생성합니다.
Bonding과 Link Aggregation
리눅스 bonding 드라이버(drivers/net/bonding/)는 여러 물리 NIC를 하나의 논리 인터페이스로 묶어 대역폭 집약(Aggregation)과 장애 허용(Failover)을 제공합니다. IEEE 802.3ad LACP 표준을 포함하여 총 7가지 모드를 지원하며, 데이터센터와 고가용성 서버 구성에서 핵심적인 역할을 합니다.
Bonding 모드 상세
| 모드 | 이름 | 해시 | Failover | 스위치 설정 필요 | 특징 |
|---|---|---|---|---|---|
| 0 | balance-rr (Round-Robin) | 순차 분배 | 예 | 예 (EtherChannel/trunk) | 패킷 단위 순차 전송, 패킷 순서 뒤바뀜 가능 |
| 1 | active-backup | 없음 | 예 | 아니오 | 하나의 슬레이브만 활성, 나머지 대기 |
| 2 | balance-xor | XOR 해시 | 예 | 예 | src/dst MAC XOR로 슬레이브 선택, 동일 흐름은 동일 경로 |
| 3 | broadcast | 없음 (전체) | 예 | 예 | 모든 슬레이브에 동일 패킷 전송, 내결함성 극대화 |
| 4 | 802.3ad (LACP) | XOR 해시 | 예 | 예 (LACP 지원) | IEEE 표준, LACPDU 교환으로 자동 협상 |
| 5 | balance-tlb | 부하 기반 | 예 | 아니오 | TX만 부하 분산, RX는 단일 슬레이브 |
| 6 | balance-alb | 부하 기반 | 예 | 아니오 | TX+RX 모두 부하 분산, ARP 조작으로 RX 분배 |
각 모드 동작 원리
802.3ad LACP 상세
LACP(Link Aggregation Control Protocol)는 IEEE 802.3ad에 정의된 표준 링크 집약 프로토콜입니다. 두 시스템이 LACPDU(Link Aggregation Control Protocol Data Unit)를 교환하여 어떤 포트를 어떤 집약 그룹에 포함할지 자동으로 협상합니다.
LACPDU 구조
| 필드 | 크기 | 설명 |
|---|---|---|
| Subtype | 1 byte | 0x01 (LACP) |
| Version | 1 byte | 0x01 |
| Actor Info TLV | 20 bytes | System Priority, System ID, Key, Port Priority, Port ID, State |
| Partner Info TLV | 20 bytes | 파트너 시스템의 동일 정보 미러링 |
| Collector Info TLV | 16 bytes | Maximum Collector Delay |
| Terminator TLV | 2 bytes | 종료 마커 |
| Reserved | 50 bytes | 패딩 |
Activity(Active/Passive), Timeout(Short/Long), Aggregation(개별/집약 가능), Synchronization(동기 완료), Collecting(수신 허용), Distributing(송신 허용), Defaulted(기본 파트너 사용), Expired(타임아웃 발생)를 나타냅니다. lacp_rate fast 설정 시 1초 간격으로 LACPDU를 전송하고, slow(기본값)는 30초 간격입니다.
Bonding 구성
ip 명령어를 사용한 구성
# bond0 인터페이스 생성 (LACP 모드)
ip link add bond0 type bond mode 802.3ad
# LACP 파라미터 설정
ip link set bond0 type bond lacp_rate fast
ip link set bond0 type bond xmit_hash_policy layer3+4
ip link set bond0 type bond miimon 100
# 슬레이브 인터페이스 추가
ip link set eth0 down
ip link set eth0 master bond0
ip link set eth1 down
ip link set eth1 master bond0
# bond0 활성화 및 IP 할당
ip link set bond0 up
ip addr add 192.168.1.10/24 dev bond0
# 상태 확인
cat /proc/net/bonding/bond0
systemd-networkd를 사용한 영구 구성
# /etc/systemd/network/10-bond0.netdev
[NetDev]
Name=bond0
Kind=bond
[Bond]
Mode=802.3ad
LACPTransmitRate=fast
TransmitHashPolicy=layer3+4
MIIMonitorSec=100ms
# /etc/systemd/network/20-eth0.network
[Match]
Name=eth0
[Network]
Bond=bond0
# /etc/systemd/network/20-eth1.network
[Match]
Name=eth1
[Network]
Bond=bond0
# /etc/systemd/network/30-bond0.network
[Match]
Name=bond0
[Network]
Address=192.168.1.10/24
Gateway=192.168.1.1
DNS=8.8.8.8
MII 모니터링 vs ARP 모니터링
| 항목 | MII 모니터링 | ARP 모니터링 |
|---|---|---|
| 파라미터 | miimon (ms) | arp_interval (ms) + arp_ip_target |
| 감지 대상 | PHY 링크 상태 (carrier) | L3 도달성 (ARP 응답) |
| 스위치 장애 | 링크가 up이면 감지 불가 | ARP 실패로 감지 가능 |
| 케이블 단선 | 즉시 감지 | arp_interval 후 감지 |
| 권장 주기 | 100ms | 200~1000ms |
| CPU 부하 | 매우 낮음 | ARP 패킷 생성 오버헤드 |
| 주의사항 | 일부 드라이버 MII 미지원 | 대상 IP 여러 개 설정 권장 |
# MII 모니터링 설정 (100ms 간격)
ip link set bond0 type bond miimon 100
# ARP 모니터링 설정 (게이트웨이 + DNS 대상)
ip link set bond0 type bond arp_interval 500
ip link set bond0 type bond arp_ip_target 192.168.1.1,8.8.8.8
# ARP validate 모드 (mode 1에서 권장)
ip link set bond0 type bond arp_validate active
커널 소스 분석
bonding 드라이버의 핵심 함수를 살펴봅니다.
/* drivers/net/bonding/bond_main.c — bond_xmit_hash()
* 패킷을 어느 슬레이브로 보낼지 결정하는 해시 함수 */
static u32 bond_xmit_hash(struct bonding *bond,
struct sk_buff *skb)
{
struct flow_keys flow;
u32 hash;
if (bond->params.xmit_policy == BOND_XMIT_POLICY_ENCAP34 &&
skb->l4_hash)
return skb->hash;
if (bond->params.xmit_policy == BOND_XMIT_POLICY_VLAN_SRCMAC)
return bond_vlan_srcmac_hash(skb);
if (bond->params.xmit_policy == BOND_XMIT_POLICY_LAYER2 ||
!bond_flow_dissect(bond, skb, &flow))
return bond_eth_hash(skb);
/* layer2+3 또는 layer3+4: flow dissector 결과 사용 */
if (bond->params.xmit_policy == BOND_XMIT_POLICY_LAYER23)
hash = bond_l23_hash(&flow);
else
hash = bond_l34_hash(&flow);
return hash >> 1;
}
/* bond_select_active_slave() — Failover 시 새 활성 슬레이브 선택 */
static struct slave *bond_find_best_slave(struct bonding *bond)
{
struct slave *slave, *bestslave = NULL;
int mintime = bond->params.updelay;
/* 우선순위: primary_slave > 가장 먼저 올라온 슬레이브 */
bond_for_each_slave(bond, slave) {
if (slave->link == BOND_LINK_UP) {
if (bond->params.primary_slave &&
slave == bond->primary_slave)
return slave;
if (!bestslave ||
slave->jiffies_first_up < bestslave->jiffies_first_up)
bestslave = slave;
}
}
return bestslave;
}
Bonding 일반적인 문제와 해결
- mode 4에서 스위치 LACP 미설정 — 스위치 측에서도 반드시 LACP(또는 EtherChannel)를 설정해야 합니다. 미설정 시 한쪽만 집약되어 패킷 손실이 발생합니다.
- xmit_hash_policy 부적합 — 단일 src/dst 쌍이 대부분인 트래픽에서 layer2 해시를 사용하면 한 슬레이브에만 트래픽이 집중됩니다.
layer3+4로 변경하면 포트 번호까지 해시하여 더 균등하게 분산됩니다. - miimon=0 (기본값) — 모니터링이 비활성화됩니다. 반드시
miimon=100이상으로 설정하세요. - mode 6(ALB)에서 스위치 MAC 테이블 문제 — ALB는 ARP 응답을 조작하여 RX 분산을 구현하므로, 스위치의 MAC 테이블이 빈번하게 갱신됩니다. 스위치 포트 보안(Port Security)이 설정되어 있으면 차단될 수 있습니다.
- VLAN + Bonding 순서 — bonding 위에 VLAN을 구성해야 합니다(bond0.100). VLAN 위에 bonding을 구성하면 동작하지 않습니다.
# Bonding 상태 상세 확인
cat /proc/net/bonding/bond0
# LACP 파트너 정보 확인
# Aggregator ID가 동일해야 모든 슬레이브가 하나의 LAG에 포함됨
cat /proc/net/bonding/bond0 | grep -A5 "Aggregator ID"
# 슬레이브별 트래픽 분산 확인
cat /sys/class/net/bond0/bonding/slaves
cat /sys/class/net/eth0/statistics/tx_bytes
cat /sys/class/net/eth1/statistics/tx_bytes
# Failover 테스트
ip link set eth0 down # 슬레이브 강제 다운
cat /proc/net/bonding/bond0 # 활성 슬레이브 변경 확인
ip link set eth0 up # 슬레이브 복구
# Bonding 이벤트 로그 확인
dmesg | grep bond0
macvlan, macvtap, ipvlan — 가상 이더넷 인터페이스
리눅스 커널은 물리 NIC 위에 여러 유형의 가상 이더넷 인터페이스를 생성할 수 있습니다. 컨테이너 네트워킹, VM 연결, 네트워크 네임스페이스 격리 등 다양한 시나리오에서 핵심적인 역할을 합니다.
macvlan
macvlan은 하나의 물리 인터페이스 위에 고유한 MAC 주소를 가진 여러 가상 인터페이스를 생성합니다. 각 macvlan 인터페이스는 독립적인 net_device로 동작하며, 커널은 수신 패킷의 목적지 MAC을 보고 올바른 macvlan 인터페이스로 전달합니다.
macvlan 모드
| 모드 | macvlan 간 통신 | 외부 통신 | 용도 |
|---|---|---|---|
| private | 불가 (완전 격리) | 가능 | 보안이 중요한 컨테이너 격리 |
| vepa | 외부 스위치 경유만 가능 | 가능 | 스위치에서 ACL/모니터링이 필요한 경우 |
| bridge | 가능 (커널 내 브릿지) | 가능 | 컨테이너 간 통신이 빈번한 경우 |
| passthru | 해당 없음 (1:1) | 가능 | 물리 NIC를 단일 컨테이너에 전용 할당 |
| source | 허용 MAC만 | 허용 MAC만 | MAC 기반 필터링이 필요한 경우 |
# macvlan 인터페이스 생성 (bridge 모드)
ip link add macvlan0 link eth0 type macvlan mode bridge
ip addr add 192.168.1.20/24 dev macvlan0
ip link set macvlan0 up
# macvlan 인터페이스 생성 (private 모드)
ip link add macvlan1 link eth0 type macvlan mode private
# macvlan 인터페이스를 네트워크 네임스페이스로 이동
ip netns add ns1
ip link set macvlan0 netns ns1
ip netns exec ns1 ip addr add 192.168.1.20/24 dev macvlan0
ip netns exec ns1 ip link set macvlan0 up
# macvtap 생성 (VM에서 TAP 장치로 사용)
ip link add macvtap0 link eth0 type macvtap mode bridge
ip link set macvtap0 up
# QEMU에서 /dev/tapN 장치로 직접 사용 가능
ipvlan
ipvlan은 macvlan과 유사하지만, 모든 가상 인터페이스가 동일한 MAC 주소(하위 물리 인터페이스의 MAC)를 공유합니다. 대신 IP 주소로 트래픽을 분리합니다. 이는 스위치의 MAC 테이블 제한이 있거나, 802.1X 인증 환경에서 유리합니다.
| 모드 | 동작 계층 | 설명 |
|---|---|---|
| L2 | Layer 2 | macvlan bridge와 유사하게 동작. ARP/NDP를 로컬에서 처리 |
| L3 | Layer 3 | 라우팅 기반 전달. 브로드캐스트/멀티캐스트 미지원 |
| L3S | Layer 3 + netfilter | L3 모드에 conntrack/iptables 지원 추가. 컨테이너 환경 권장 |
# ipvlan L2 모드
ip link add ipvlan0 link eth0 type ipvlan mode l2
ip addr add 192.168.1.30/24 dev ipvlan0
ip link set ipvlan0 up
# ipvlan L3S 모드 (iptables 지원)
ip link add ipvlan1 link eth0 type ipvlan mode l3s
ip addr add 10.0.0.1/24 dev ipvlan1
ip link set ipvlan1 up
veth 페어와 컨테이너 네트워킹
veth(Virtual Ethernet) 페어는 두 개의 가상 이더넷 인터페이스가 가상 케이블로 연결된 형태입니다. 한쪽에 들어간 패킷은 즉시 반대쪽에서 나옵니다. 컨테이너(Docker, LXC)와 네트워크 네임스페이스(Network Namespace)를 호스트 네트워크에 연결하는 가장 기본적인 방법입니다.
# veth 페어 생성 및 네임스페이스 연결
ip link add veth-host type veth peer name veth-ns
ip netns add container1
ip link set veth-ns netns container1
# 호스트 측: 브릿지에 연결
ip link set veth-host master docker0
ip link set veth-host up
# 컨테이너 측: IP 할당 및 활성화
ip netns exec container1 ip addr add 172.17.0.10/16 dev veth-ns
ip netns exec container1 ip link set veth-ns up
ip netns exec container1 ip route add default via 172.17.0.1
커널 소스 분석
/* drivers/net/macvlan.c — macvlan_queue_xmit()
* macvlan TX 경로: lower device로 직접 전송 */
static int macvlan_queue_xmit(struct sk_buff *skb,
struct net_device *dev)
{
const struct macvlan_dev *vlan = netdev_priv(dev);
const struct macvlan_port *port = vlan->port;
/* bridge 모드: macvlan 간 직접 전달 시도 */
if (vlan->mode == MACVLAN_MODE_BRIDGE) {
const struct macvlan_dev *dest;
dest = macvlan_hash_lookup(port, eth_hdr(skb)->h_dest);
if (dest && dest->mode == MACVLAN_MODE_BRIDGE)
return macvlan_bridge_xmit(skb, dev, dest);
}
/* lower device로 직접 전송 (bridge를 경유하지 않음) */
skb->dev = vlan->lowerdev;
return dev_queue_xmit_accel(skb, NULL);
}
/* drivers/net/ipvlan/ipvlan_core.c — L2/L3 모드 TX 분기 */
static int ipvlan_xmit_mode_l2(struct sk_buff *skb,
struct net_device *dev)
{
/* L2 모드: ARP 등 브로드캐스트를 다른 ipvlan에도 전달 */
if (is_multicast_ether_addr(eth_hdr(skb)->h_dest))
ipvlan_skb_crossing_ns(skb, NULL);
skb->dev = ipvlan_get_lowerdev(dev);
return dev_queue_xmit(skb);
}
static int ipvlan_xmit_mode_l3(struct sk_buff *skb,
struct net_device *dev)
{
/* L3 모드: IP 라우팅으로 직접 전달, L2 헤더 무시 */
struct ipvl_addr *addr;
addr = ipvlan_addr_lookup(skb, ipvlan_get_port(dev), true);
if (addr)
return ipvlan_rcv_frame(addr, &skb, true);
skb->dev = ipvlan_get_lowerdev(dev);
return dev_queue_xmit(skb);
}
- macvlan — 외부 네트워크와 L2 수준으로 완전히 통합해야 할 때 (DHCP로 외부 IP 할당, 외부에서 MAC으로 직접 접근). 단, 하위 인터페이스(eth0)의 호스트 통신이 차단됩니다.
- ipvlan — 스위치 MAC 테이블 한계가 있거나, 802.1X 환경이거나, 대규모 컨테이너(수천 개)를 운용할 때. 모든 컨테이너가 동일 MAC을 공유하므로 스위치 부담이 없습니다.
- veth + bridge — NAT, iptables, 포트 매핑 등 풍부한 네트워크 기능이 필요할 때. Docker/Kubernetes의 기본 구성입니다.
XDP 상세 (Express Data Path)
XDP(Express Data Path)는 리눅스 커널 4.8+에서 도입된 고성능 프로그래밍 가능한 패킷 처리 프레임워크입니다. eBPF 프로그램을 NIC 드라이버의 수신 경로에 직접 부착하여, sk_buff 할당 이전에 패킷을 처리할 수 있습니다. 이를 통해 커널 네트워크 스택의 오버헤드를 완전히 우회하며, 단일 코어에서 10~20 Mpps의 처리량을 달성합니다.
XDP 동작 모드 비교
| 모드 | 부착 위치 | skb 할당 | 성능 | 요구 사항 | 사용 예시 |
|---|---|---|---|---|---|
| Offloaded | NIC 하드웨어 (SmartNIC) | 없음 | 최고 (라인 레이트) | SmartNIC 필요 (Netronome 등) | 하드웨어 방화벽 |
| Native | 드라이버 NAPI poll 내부 | 없음 (xdp_buff) | 매우 높음 (~20 Mpps) | 드라이버 XDP 지원 필수 | DDoS 방어, L4 LB |
| Generic | netif_receive_skb() 이후 | 있음 (skb 기반) | 낮음 (~3 Mpps) | 모든 NIC | 개발/테스트 |
xdp_buff와 xdp_frame 구조체
/* include/net/xdp.h - xdp_buff: XDP 프로그램에 전달되는 패킷 메타데이터 */
struct xdp_buff {
void *data; /* 패킷 데이터 시작점 (이더넷 헤더) */
void *data_end; /* 패킷 데이터 끝점 */
void *data_meta; /* 메타데이터 영역 (data 앞쪽) */
void *data_hard_start; /* headroom 시작점 (XDP_TX 시 활용) */
struct xdp_rxq_info *rxq; /* RX 큐 정보 (dev, queue_index) */
struct xdp_txq_info *txq; /* TX 큐 정보 (XDP_TX 시) */
u32 frame_sz; /* 프레임 버퍼 전체 크기 */
u32 flags; /* XDP 플래그 */
};
/* xdp_frame: XDP_REDIRECT 시 사용되는 프레임 메타데이터
* xdp_buff를 xdp_frame으로 변환하여 다른 CPU/디바이스로 전달 */
struct xdp_frame {
void *data;
u16 len;
u16 headroom;
u32 metasize;
u32 frame_sz;
struct xdp_mem_info mem; /* 메모리 할당자 정보 (page_pool 등) */
struct net_device *dev_rx; /* 수신 디바이스 */
u32 flags;
};
XDP 프로그램 예시: TCP 이외 패킷 드롭
/* xdp_filter.c - TCP 이외 패킷을 드롭하는 XDP 프로그램 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <arpa/inet.h>
SEC("xdp")
int xdp_tcp_filter(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* 이더넷 헤더 경계 검사 (eBPF 검증기 필수 조건) */
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_DROP;
/* IPv4가 아닌 패킷은 통과 (ARP 등) */
if (eth->h_proto != htons(ETH_P_IP))
return XDP_PASS;
/* IP 헤더 경계 검사 */
struct iphdr *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return XDP_DROP;
/* TCP(프로토콜 6)만 통과, 나머지 드롭 */
if (iph->protocol != IPPROTO_TCP)
return XDP_DROP;
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
# XDP 프로그램 컴파일 및 부착
clang -O2 -target bpf -c xdp_filter.c -o xdp_filter.o
# Native XDP 모드로 부착
ip link set dev eth0 xdpdrv obj xdp_filter.o sec xdp
# Generic XDP 모드로 부착 (드라이버 미지원 시)
ip link set dev eth0 xdpgeneric obj xdp_filter.o sec xdp
# XDP 프로그램 해제
ip link set dev eth0 xdp off
# 현재 XDP 상태 확인
ip link show dev eth0
# ... xdp/id:42 ... (XDP 프로그램 ID)
# XDP 통계 확인
bpftool prog show id 42
bpftool prog tracelog
XDP_REDIRECT와 bpf_redirect_map()
XDP_REDIRECT는 가장 강력한 XDP 액션으로, 패킷을 다른 네트워크 인터페이스(DEVMAP), 다른 CPU(CPUMAP), 또는 AF_XDP 소켓(XSKMAP)으로 전달합니다.
/* XDP_REDIRECT를 사용한 L2 포워딩 프로그램 */
struct {
__uint(type, BPF_MAP_TYPE_DEVMAP);
__uint(max_entries, 256);
__type(key, __u32);
__type(value, __u32);
} tx_port SEC(".maps");
SEC("xdp")
int xdp_redirect_prog(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_DROP;
/* MAC 주소 스왑 (반사 테스트용) */
__u8 tmp[ETH_ALEN];
__builtin_memcpy(tmp, eth->h_dest, ETH_ALEN);
__builtin_memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
__builtin_memcpy(eth->h_source, tmp, ETH_ALEN);
/* DEVMAP을 통해 다른 인터페이스로 포워딩 */
return bpf_redirect_map(&tx_port, ctx->ingress_ifindex, 0);
}
char _license[] SEC("license") = "GPL";
/* net/core/filter.c - xdp_do_redirect() 커널 내부 */
int xdp_do_redirect(struct net_device *dev,
struct xdp_buff *xdp,
struct bpf_prog *xdp_prog)
{
struct bpf_redirect_info *ri = this_cpu_ptr(&bpf_redirect_info);
enum bpf_map_type map_type = ri->map_type;
switch (map_type) {
case BPF_MAP_TYPE_DEVMAP:
case BPF_MAP_TYPE_DEVMAP_HASH:
/* 다른 net_device로 ndo_xdp_xmit() 호출 */
return dev_map_enqueue(ri->map, xdp, dev);
case BPF_MAP_TYPE_CPUMAP:
/* 다른 CPU의 per-cpu 큐에 xdp_frame 삽입 */
return cpu_map_enqueue(ri->map, xdp, dev);
case BPF_MAP_TYPE_XSKMAP:
/* AF_XDP 소켓의 RX 큐에 삽입 */
return __xsk_map_redirect(ri->map, xdp);
default:
return -EBADRQC;
}
}
/* 드라이버에서의 XDP 실행 위치 (ice 드라이버 예시):
*
* ice_clean_rx_irq()
* ├── DMA 완료 확인
* ├── xdp_buff 초기화 (data, data_end 설정)
* ├── bpf_prog_run_xdp(prog, &xdp) ← ★ XDP 프로그램 실행
* ├── switch (act) {
* │ case XDP_PASS: → build_skb() → napi_gro_receive()
* │ case XDP_TX: → ice_xmit_xdp_tx_ring()
* │ case XDP_REDIRECT: → xdp_do_redirect()
* │ case XDP_DROP: → page recycle (skb 할당 없음)
* │ }
* └── xdp_do_flush() ← 배치(batch) 리다이렉트 실행
*/
XDP 메타데이터와 BTF
XDP 프로그램은 data_meta 영역을 통해 메타데이터를 XDP_PASS로 전달된 skb에 공유할 수 있습니다. 커널 6.0+에서는 BTF(BPF Type Format) 기반 XDP 힌트(hints)가 추가되어 드라이버가 하드웨어 타임스탬프, RSS 해시 등의 정보를 XDP 프로그램에 전달할 수 있습니다.
/* XDP 메타데이터 활용: data_meta를 통한 정보 전달 */
SEC("xdp")
int xdp_with_metadata(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_meta = (void *)(long)ctx->data_meta;
/* data_meta를 확장하여 커스텀 메타데이터 저장 */
if (bpf_xdp_adjust_meta(ctx, -(int)sizeof(__u32)) < 0)
return XDP_ABORTED;
/* 메타데이터 영역에 타임스탬프 또는 마크 기록 */
data_meta = (void *)(long)ctx->data_meta;
data = (void *)(long)ctx->data;
if (data_meta + sizeof(__u32) > data)
return XDP_ABORTED;
*(__u32 *)data_meta = bpf_ktime_get_ns();
/* XDP_PASS → skb 생성 시 skb->data - skb_metadata_len(skb) 위치에서
* 이 메타데이터를 TC/eBPF 프로그램이 읽을 수 있습니다 */
return XDP_PASS;
}
i40e(Intel X710), ice(Intel E810), mlx5(Mellanox/NVIDIA ConnectX), bnxt(Broadcom), virtio_net(가상 머신), veth(컨테이너) 등이 있습니다. ip link set dev eth0 xdpdrv obj prog.o가 EOPNOTSUPP를 반환하면 해당 드라이버가 Native XDP를 지원하지 않는 것입니다. 이 경우 xdpgeneric으로 폴백할 수 있습니다.
AF_XDP와 고성능 패킷 처리
AF_XDP(eXpress Data Path 소켓)는 리눅스 커널 4.18+에서 도입된 고성능 패킷 처리 인터페이스입니다. XDP 프로그램이 NIC에서 수신한 패킷을 커널 네트워크 스택을 완전히 우회하여 유저스페이스 애플리케이션에 제로카피로 전달합니다.
AF_XDP 동작 모드
| 모드 | 성능 | 요구 사항 | 설명 |
|---|---|---|---|
| Zero-Copy | 최고 (~25 Mpps) | 드라이버 지원 필수 | NIC DMA가 UMEM에 직접 쓰기, 복사 없음 |
| Copy Mode | 중간 (~5 Mpps) | 모든 XDP 드라이버 | 커널이 RX 버퍼(Buffer)에서 UMEM으로 복사 |
| SKB Mode | 낮음 (~2 Mpps) | XDP generic | skb 할당 후 처리, 호환성 최대 |
# AF_XDP 제로카피 지원 드라이버 확인
# Intel: ice, i40e, ixgbe, igc
# Mellanox: mlx5
# Amazon: ena
# Netronome: nfp
# XDP 프로그램 로드 (xdpsock 예시)
ip link set dev eth0 xdp obj xdp_prog.o sec xdp
# AF_XDP 소켓 기본 사용법 (C 코드 개요)
/* 1. UMEM 생성 */
/* struct xsk_umem *umem;
xsk_umem__create(&umem, buffer, size, &fill, &comp, &cfg); */
/* 2. AF_XDP 소켓 생성 */
/* struct xsk_socket *xsk;
xsk_socket__create(&xsk, "eth0", queue, umem, &rx, &tx, &cfg); */
/* 3. poll() 또는 busy-poll로 패킷 수신 */
/* 4. RX Ring에서 descriptor 읽기 → UMEM 주소로 패킷 접근 */
# libbpf AF_XDP 헬퍼 (xsk.h)
# xsk_ring_prod__reserve(), xsk_ring_cons__peek()
# xsk_ring_prod__submit(), xsk_ring_cons__release()
# XDP 통계 확인
bpftool prog show
bpftool net show
# ethtool XDP 통계
ethtool -S eth0 | grep xdp
성능 튜닝
이더넷 네트워크 성능을 최적화하는 주요 커널/드라이버 파라미터입니다.
링 버퍼 최적화
# 링 버퍼 현재 크기 확인
ethtool -g eth0
# 링 버퍼 최대로 설정 (패킷 드롭 방지)
ethtool -G eth0 rx 4096 tx 4096
# 인터럽트 병합 (지연 vs 처리량 트레이드오프)
ethtool -C eth0 adaptive-rx on adaptive-tx on
adaptive-rx on을 사용하면 드라이버가 트래픽 패턴에 따라 인터럽트 병합 파라미터를 자동 조절합니다. 저부하 시 지연을 최소화하고, 고부하 시 인터럽트 수를 줄여 CPU 사용률을 낮춥니다. Intel igb/ixgbe/ice, Mellanox mlx5 등 주요 드라이버가 지원합니다.
# 멀티큐 확인
ethtool -l eth0
# 큐 수 변경
ethtool -L eth0 combined 8
IRQ 어피니티
# NIC IRQ 확인
grep eth0 /proc/interrupts
# IRQ 어피니티 자동 설정 스크립트
set_irq_affinity eth0
# 수동 설정: IRQ 42번을 CPU 0에 바인딩
echo 1 > /proc/irq/42/smp_affinity
# XPS (Transmit Packet Steering) 설정
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
sysctl 네트워크 튜닝
# 수신 버퍼
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.rmem_default=1048576
# 송신 버퍼
sysctl -w net.core.wmem_max=16777216
sysctl -w net.core.wmem_default=1048576
# 네트워크 백로그
sysctl -w net.core.netdev_max_backlog=30000
sysctl -w net.core.netdev_budget=600
# TCP 자동 튜닝
sysctl -w net.ipv4.tcp_rmem="4096 1048576 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 1048576 16777216"
이더넷 성능 벤치마크와 프로파일링
네트워크 성능을 정확하게 측정하려면 올바른 도구 선택과 방법론이 중요합니다. 처리량(Throughput), 지연 시간(Latency), 패킷 처리율(PPS)은 각각 다른 도구와 설정으로 측정해야 하며, 측정 환경의 변수를 통제하지 않으면 결과가 무의미해질 수 있습니다.
벤치마크 도구 비교
| 도구 | 측정 대상 | 프로토콜 | 동작 위치 | 특징 |
|---|---|---|---|---|
| iperf3 | 처리량 (Throughput) | TCP, UDP, SCTP | 유저스페이스 | 가장 일반적, JSON 출력 지원 |
| netperf | 처리량 + 지연 | TCP_RR, UDP_RR, TCP_STREAM | 유저스페이스 | 요청-응답 지연 측정에 강점 |
| pktgen | PPS (wire-rate) | UDP (raw) | 커널 모듈 | 커널에서 직접 패킷 생성, 최대 PPS |
| sockperf | 지연 시간 | TCP, UDP | 유저스페이스 | 마이크로초 단위 지연 히스토그램 |
| neper | 처리량 + 연결 수 | TCP, UDP | 유저스페이스 | Google 개발, 다중 흐름 테스트 |
| wrk/wrk2 | HTTP 처리량 + 지연 | HTTP/HTTPS | 유저스페이스 | 웹 서버 성능 테스트 |
iperf3 사용법
# 서버 측
iperf3 -s -p 5201
# 클라이언트: TCP 처리량 (30초, 8 병렬 스트림)
iperf3 -c 192.168.1.1 -t 30 -P 8
# 양방향 동시 테스트
iperf3 -c 192.168.1.1 -t 30 --bidir
# UDP PPS 테스트 (64바이트 패킷, 10Gbps 목표)
iperf3 -c 192.168.1.1 -u -b 10G -l 64 -t 30
# JSON 출력 (자동화용)
iperf3 -c 192.168.1.1 -t 30 -P 8 -J > result.json
# CPU 바인딩 (NUMA 노드 일치)
taskset -c 0-7 iperf3 -c 192.168.1.1 -t 30 -P 8 -A 0
netperf로 지연 시간 측정
# 서버 측
netserver -p 12865
# TCP 요청-응답 지연 (Transaction rate = 1/latency)
netperf -H 192.168.1.1 -t TCP_RR -l 30 -- -O min_latency,mean_latency,max_latency,p99_latency
# UDP 요청-응답 지연
netperf -H 192.168.1.1 -t UDP_RR -l 30
# 다양한 메시지 크기로 반복 테스트
for size in 1 64 512 1024 4096 65536; do
echo "=== Message size: $size ==="
netperf -H 192.168.1.1 -t TCP_RR -l 10 -- -r $size,$size
done
pktgen (커널 패킷 생성기)
pktgen은 커널 모듈(net/core/pktgen.c)로 구현된 패킷 생성기입니다. 유저스페이스 도구와 달리 시스템 콜 오버헤드 없이 패킷을 직접 NIC 드라이버에 전달하므로, NIC의 이론적 최대 PPS에 근접하는 성능을 측정할 수 있습니다.
# pktgen 모듈 로드
modprobe pktgen
# /proc/net/pktgen/ 인터페이스로 설정
# 스레드별 디바이스 할당 (CPU 0에서 eth0)
echo "add_device eth0" > /proc/net/pktgen/kpktgend_0
# 패킷 설정
echo "pkt_size 64" > /proc/net/pktgen/eth0
echo "count 10000000" > /proc/net/pktgen/eth0
echo "dst 192.168.1.1" > /proc/net/pktgen/eth0
echo "dst_mac AA:BB:CC:DD:EE:FF" > /proc/net/pktgen/eth0
echo "delay 0" > /proc/net/pktgen/eth0 # wire-rate
# 다중 CPU 코어 활용 (높은 PPS를 위해)
for cpu in 0 1 2 3; do
echo "add_device eth0@$cpu" > /proc/net/pktgen/kpktgend_$cpu
echo "pkt_size 64" > /proc/net/pktgen/eth0@$cpu
echo "count 0" > /proc/net/pktgen/eth0@$cpu # 무한
echo "delay 0" > /proc/net/pktgen/eth0@$cpu
echo "dst 192.168.1.1" > /proc/net/pktgen/eth0@$cpu
echo "dst_mac AA:BB:CC:DD:EE:FF" > /proc/net/pktgen/eth0@$cpu
done
# 테스트 시작
echo "start" > /proc/net/pktgen/pgctrl
# 결과 확인
cat /proc/net/pktgen/eth0
perf와 bpftrace로 프로파일링
# perf로 네트워크 스택 프로파일링 (30초)
perf record -g -F 99 -a -- sleep 30
perf report --sort=comm,dso,symbol
# 네트워크 관련 함수만 필터링
perf top -g --call-graph=fp -e cycles:k -z --no-children
# Flame Graph 생성
perf record -g -F 99 -a -- sleep 30
perf script > out.perf
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > flamegraph.svg
# bpftrace: NAPI poll 소요 시간 히스토그램
bpftrace -e '
kprobe:napi_poll { @start[tid] = nsecs; }
kretprobe:napi_poll /@start[tid]/ {
@usecs = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
'
# bpftrace: ndo_start_xmit 호출 빈도 (드라이버별)
bpftrace -e '
kprobe:dev_queue_xmit {
@tx[comm] = count();
}
interval:s:5 { print(@tx); clear(@tx); }
'
# ftrace: 특정 네트워크 함수 호출 추적
echo "napi_gro_receive" > /sys/kernel/debug/tracing/set_ftrace_filter
echo "function" > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe | head -100
25GbE NIC 전체 벤치마크 워크플로우 예시
#!/bin/bash
# 25GbE NIC 종합 벤치마크 스크립트
NIC="eth0"
SERVER="192.168.1.1"
# === Phase 1: 환경 준비 ===
echo "[1/5] 환경 준비..."
# irqbalance 중지
systemctl stop irqbalance
# NIC IRQ를 NUMA-local CPU에 고정
ethtool -L $NIC combined 8
./set_irq_affinity_cpulist.sh 0-7 $NIC
# 링 버퍼 최대화
ethtool -G $NIC rx 4096 tx 4096
# 적응형 코얼레싱 활성화
ethtool -C $NIC adaptive-rx on adaptive-tx on
# C-state 제한 (지연 최소화)
echo 1 > /dev/cpu_dma_latency &
# === Phase 2: TCP 처리량 ===
echo "[2/5] TCP 처리량 측정..."
iperf3 -c $SERVER -t 30 -P 8 -J > tcp_throughput.json
# === Phase 3: TCP 지연 시간 ===
echo "[3/5] TCP 지연 시간 측정..."
netperf -H $SERVER -t TCP_RR -l 30 -- \
-O min_latency,mean_latency,max_latency,p99_latency,throughput
# === Phase 4: UDP PPS ===
echo "[4/5] UDP PPS 측정..."
iperf3 -c $SERVER -u -b 25G -l 64 -t 30 -J > udp_pps.json
# === Phase 5: 프로파일링 ===
echo "[5/5] CPU 프로파일링..."
perf record -g -F 99 -a -o perf_network.data -- \
iperf3 -c $SERVER -t 10 -P 8
echo "완료. 결과: tcp_throughput.json, udp_pps.json, perf_network.data"
벤치마크 실수 체크리스트
- NUMA 미스매치 — NIC과 다른 NUMA 노드에서 iperf3를 실행하면 QPI/UPI 오버헤드로 처리량이 30~50% 저하됩니다.
cat /sys/class/net/eth0/device/numa_node로 NIC의 NUMA 노드를 확인하고,numactl --cpunodebind=N으로 같은 노드에서 실행하세요. - 단일 스트림으로 25G+ 측정 — 단일 TCP 연결은 CPU 코어 하나에 바인딩되므로 25G를 채울 수 없습니다.
iperf3 -P 8이상으로 다중 스트림을 사용하세요. - 워밍업 기간 무시 — TCP slow start, CPU 주파수 스케일링(Governor) 등으로 초기 수 초는 성능이 낮습니다. 최소 10초 이상 실행하고 처음 5초를 제외하세요.
- MTU 불일치 — 양단의 MTU가 다르면 프래그먼테이션이 발생하여 성능이 크게 저하됩니다.
ip link show로 양쪽 MTU를 확인하세요. - 오프로드 미확인 — TSO, GRO, checksum offload가 비활성화되어 있으면 CPU 사용량이 급증합니다.
ethtool -k eth0로 확인하세요. - TCP 버퍼 부족 — 높은 BDP(Bandwidth-Delay Product) 환경에서 TCP 버퍼가 작으면 파이프라인이 채워지지 않습니다.
sysctl net.ipv4.tcp_rmem/tcp_wmem을 확인하세요. - 백그라운드 트래픽 — SSH, DNS, NTP 등 다른 트래픽이 CPU와 NIC 큐를 공유합니다. 관리 트래픽은 별도 인터페이스로 분리하세요.
# NUMA 노드 확인 및 매칭
cat /sys/class/net/eth0/device/numa_node
# 출력: 0 → CPU 0-7이 NUMA 노드 0이면 해당 CPU 사용
lscpu | grep NUMA
# NUMA node0 CPU(s): 0-7,16-23
# NUMA-aware iperf3 실행
numactl --cpunodebind=0 --membind=0 iperf3 -c 192.168.1.1 -t 30 -P 8
# BDP (Bandwidth-Delay Product) 계산
# 25 Gbps x 0.1ms RTT = 25e9 * 0.0001 / 8 = 312,500 bytes
# TCP 버퍼는 BDP의 2배 이상 필요
sysctl -w net.ipv4.tcp_rmem="4096 524288 2097152"
sysctl -w net.ipv4.tcp_wmem="4096 524288 2097152"
sysctl -w net.core.rmem_max=2097152
sysctl -w net.core.wmem_max=2097152
FEC (Forward Error Correction)
FEC(전진 오류 정정)는 고속 이더넷(25G 이상)에서 비트 오류율(BER)을 허용 수준(10-12 이하)으로 낮추기 위해 송신 측에서 중복 데이터를 추가하고, 수신 측에서 오류를 복원하는 기술입니다. PAM4 변조를 사용하는 25G+ 이더넷에서는 FEC가 필수적입니다.
FEC 유형 비교
| FEC 유형 | 표준 | 속도 | 지연 | 정정 능력 | 오버헤드 |
|---|---|---|---|---|---|
| BaseR FEC (Firecode/FC-FEC) | 802.3 Clause 74 | 10G/25G/40G | ~50 ns | 단일 비트 버스트 (11비트) | ~2.6% |
| RS-FEC (Reed-Solomon) | 802.3 Clause 91/108 | 25G/50G/100G+ | ~100 ns | 다중 심볼 오류 (최대 ~7%) | ~2.7% |
| RS-FEC (544,514) | 802.3 Clause 119 | 50G/100G/200G/400G+ (PAM4) | ~100 ns | 더 강한 정정 (PAM4용) | ~5.8% |
| 없음 (No FEC) | - | 10G 이하 | 0 | 없음 | 0% |
FEC 설정 및 모니터링
# 현재 FEC 설정 확인
ethtool --show-fec eth0
# FEC 모드 변경
ethtool --set-fec eth0 encoding rs # RS-FEC 강제
ethtool --set-fec eth0 encoding baser # BaseR-FEC 강제
ethtool --set-fec eth0 encoding auto # 자동 협상
ethtool --set-fec eth0 encoding off # FEC 비활성화
# FEC 통계 확인 (정정/미정정 블록 수)
ethtool -S eth0 | grep -i fec
# fec_corrected_blocks: 12345 ← 정정 성공 (정상)
# fec_uncorrectable_blocks: 0 ← 미정정 (이 값이 증가하면 문제)
# FEC 미정정 블록이 증가하면:
# 1. SFP 모듈의 RX Power 확인 (ethtool -m ethX)
# 2. 광 패치 코드 교체
# 3. FEC 모드를 RS-FEC으로 변경 (더 강한 정정)
# 4. 링크 양단 FEC 모드 일치 확인
이더넷 케이블과 커넥터 가이드
물리 계층의 성능은 케이블과 커넥터에 크게 의존합니다. 올바른 케이블 선택은 안정적인 이더넷 링크의 기본입니다.
UTP 케이블 카테고리 비교
| 카테고리 | 최대 주파수 | 최대 속도 | 최대 거리 | 차폐 | 용도 |
|---|---|---|---|---|---|
| Cat5 | 100 MHz | 100 Mbps | 100m | UTP | 레거시 (사용 지양) |
| Cat5e | 100 MHz | 1 Gbps / 2.5 Gbps | 100m | UTP | 일반 사무/가정용 |
| Cat6 | 250 MHz | 5 Gbps(100m) / 10G(55m) | 55~100m | UTP/STP | 기업 환경, PoE+ |
| Cat6a | 500 MHz | 10 Gbps | 100m | F/UTP, U/FTP | 10GbE 표준, 데이터센터 |
| Cat7 | 600 MHz | 10 Gbps | 100m | S/FTP (전쌍 차폐) | EMI 환경, 산업용 |
| Cat7a | 1000 MHz | 10 Gbps (40G@50m) | 50~100m | S/FTP | 미래 대비, 특수 환경 |
| Cat8 | 2000 MHz | 25/40 Gbps | 30m | S/FTP | 데이터센터 스위치 간 |
케이블 테스트 및 진단
# 케이블 테스트 (TDR: Time Domain Reflectometry)
ethtool --cable-test eth0
# 결과 확인 (NIC 지원 필요: ice, igc, aqc111 등)
# pair A code open, length 15m
# pair B code ok
# pair C code ok
# pair D code short, length 8m
# 케이블 진단 코드 해석:
# ok — 정상 연결
# open — 단선 (선이 끊어짐)
# short — 합선 (선끼리 접촉)
# impedance-mismatch — 임피던스 불일치 (커넥터 불량)
# 광 커넥터 (SFP) 연결 상태 확인
ethtool -m eth0
# RX Power가 -30 dBm 이하이면 광 손실 과다
# 정상 범위: -1 ~ -12 dBm (모듈마다 다름)
DAC와 AOC
| 유형 | 정식 명칭 | 매체 | 최대 거리 | 소비 전력 | 용도 |
|---|---|---|---|---|---|
| DAC | Direct Attach Copper | 트윈악스 구리 | 5~7m | 매우 낮음 (~0.1W) | ToR-서버 단거리 |
| AOC | Active Optical Cable | 광섬유 (일체형) | 30~100m | ~1W | 랙 간 중거리 |
| 트랜시버+패치(Patch)코드 | SFP/QSFP + LC/MPO | 광섬유 (분리형) | 300m~40km | ~1.5W | 장거리, 유연한 구성 |
FCS와 CRC-32
이더넷 프레임의 마지막 4바이트 FCS(Frame Check Sequence)는 CRC-32 다항식으로 계산됩니다.
- 다항식:
x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1 - 검사 범위: Destination MAC ~ Payload (Preamble, SFD 제외)
- HW 오프로드: 현대 NIC은 FCS를 하드웨어에서 계산/검증,
skb->ip_summed로 커널에 알림
/* 커널 CRC-32 API */
#include <linux/crc32.h>
u32 crc = crc32_le(~0, data, len); /* Little-endian CRC-32 */
u32 fcs = ~crc; /* 최종 보수(complement) */
PTP와 하드웨어 타임스탬프
IEEE 1588 PTP(Precision Time Protocol)는 이더넷 네트워크에서 나노초 수준 시간 동기화를 달성합니다.
# PTP 하드웨어 타임스탬프 능력 확인
ethtool -T eth0
# linuxptp로 PTP 동기화
ptp4l -i eth0 -m -H # Hardware timestamping
phc2sys -s eth0 -c CLOCK_REALTIME -O 0 -m
# PHC (PTP Hardware Clock) 확인
cat /sys/class/ptp/ptp0/clock_name
WoL (Wake-on-LAN)
Wake-on-LAN은 이더넷을 통해 원격으로 시스템을 깨우는 기능입니다. 시스템이 완전히 동작하지 않는 상태여도 NIC의 일부 블록은 대기 전원으로 살아 있으며, 매직 패킷이나 드라이버가 허용한 웨이크 패턴을 감시합니다.
실무에서는 S3(대기), S4(디스크 절전), S5(소프트 오프)에서 WoL을 기대하지만, 실제 지원 범위는 메인보드 전원 설계, BIOS/UEFI 정책, NIC의 D3hot/D3cold 동작에 따라 달라집니다. 커널은 시스템을 절전 상태로 넘기기 전에 NIC 드라이버에 웨이크 패턴을 프로그래밍하고, 이후 NIC는 패턴이 일치하면 PME# 같은 웨이크 이벤트를 플랫폼에 전달합니다.
| 항목 | WoL에서 실제로 남아 있는 것 | 주의할 점 |
|---|---|---|
| 전원 상태 | S3, S4, S5에서 주로 사용 | 보드에 따라 S5 WoL을 막거나 ErP 설정으로 대기 전원을 끌 수 있습니다. |
| NIC 내부 블록 | PHY/MAC 일부와 패턴 매처 | D3cold로 너무 깊게 내려가면 링크 복구가 늦거나 WoL이 완전히 막힐 수 있습니다. |
| 웨이크 신호 | PME# 또는 펌웨어 경유 ACPI 웨이크 | 운영체제 설정만으로는 부족하고 BIOS/UEFI 허용이 함께 필요합니다. |
| 네트워크 범위 | 보통 같은 L2 브로드캐스트 도메인 | 라우터는 브로드캐스트를 기본적으로 넘기지 않으므로 중계나 포워딩 설계가 필요합니다. |
매직 패킷은 데이터 페이로드 어디에 있어도 되지만, 일반적으로는 같은 L2 세그먼트에서 브로드캐스트 주소로 보내는 방식이 가장 단순합니다. NIC는 운영체제가 거의 꺼진 상태에서도 이 패턴을 비교할 수 있도록 최소한의 수신 경로를 유지합니다.
- 펌웨어 정책: BIOS/UEFI에서 WoL이나 PCIe Power On이 꺼져 있으면 커널 설정이 맞아도 깨우지 못합니다.
- 대기 전원 차단: ErP, Deep Sleep, S5 절전 강화 옵션이 NIC의 대기 전원을 끌 수 있습니다.
- 네트워크 경로 문제: 브로드캐스트가 다른 서브넷으로 전달되지 않거나 스위치/AP가 절전 중 링크를 내릴 수 있습니다.
- 드라이버 비영속: 재부팅 또는 링크 재초기화 뒤 WoL 플래그가 초기화되면 다음 절전부터 동작하지 않습니다.
# WoL 지원 확인
ethtool eth0 | grep Wake-on
# WoL 활성화 (Magic Packet)
ethtool -s eth0 wol g
# WoL 비활성화
ethtool -s eth0 wol d
# 매직 패킷 전송 (다른 시스템에서)
wakeonlan AA:BB:CC:DD:EE:FF
디버깅과 진단
ethtool 진단
# 드라이버 정보
ethtool -i eth0
# NIC 통계 (드라이버별)
ethtool -S eth0
# 링크 상태 상세
ethtool eth0
# PHY 레지스터 덤프
ethtool -d eth0
# NIC 자가진단 (지원 시)
ethtool -t eth0 online
# 모듈(SFP) 정보
ethtool -m eth0
이더넷 오류 유형
이더넷 통신에서 발생하는 주요 오류 유형과 원인, 증상, 해결 방법을 정리합니다.
| 오류 유형 | 원인 | 증상 | 해결 |
|---|---|---|---|
| CRC 오류 | 케이블 손상, EMI, 불량 커넥터 | rx_crc_errors 증가 | 케이블 교체, 접지 확인 |
| Alignment 오류 | 바이트 경계 미정렬, PHY 문제 | rx_align_errors 증가 | PHY 칩 교체, 듀플렉스 불일치 확인 |
| Runt Frame | 충돌, 잘못된 드라이버 | 64바이트 미만 프레임 수신 | 듀플렉스 설정, 케이블 길이 확인 |
| Giant Frame | MTU 불일치, Jabber | 최대 크기 초과 프레임 | 양단 MTU/Jumbo Frame 설정 동기화 |
| Late Collision | 케이블 과장, 듀플렉스 불일치 | 512 bit time 이후 충돌 감지 | 케이블 100m 이내 확인, Full-Duplex 강제 |
| Carrier Sense 오류 | PHY 링크 불안정 | tx_carrier_errors 증가 | SFP 모듈/케이블 교체, PHY 펌웨어 업데이트 |
ethtool -S 출력에서 오류 카운터를 해석하는 예시입니다.
# NIC 오류 통계 확인
ethtool -S eth0 | grep -i error
# 주요 오류 카운터 해석
# rx_crc_errors — CRC 불일치 (물리 계층 문제)
# rx_missed_errors — 링 버퍼 부족으로 누락
# rx_length_errors — 길이 필드 불일치
# rx_over_errors — RX FIFO 오버플로우
# tx_aborted_errors — 전송 중단 (과도한 충돌)
# tx_fifo_errors — TX FIFO 언더런
# 시간 경과에 따른 오류 증가율 모니터링
watch -n 1 'ethtool -S eth0 | grep -i error'
# sysfs 오류 카운터 직접 확인
for f in /sys/class/net/eth0/statistics/*error*; do
echo "$(basename $f): $(cat $f)"
done
패킷 카운터 확인
# 인터페이스 통계
ip -s link show eth0
# 상세 오류 카운터
cat /sys/class/net/eth0/statistics/rx_errors
cat /sys/class/net/eth0/statistics/rx_dropped
cat /sys/class/net/eth0/statistics/tx_carrier_errors
# SNMP MIB 카운터
cat /proc/net/snmp | grep Ip
# 드롭 원인 추적
dropwatch -l kas
# 또는 perf를 사용
perf trace -e 'skb:kfree_skb'
패킷 캡처
# 이더넷 헤더 포함 캡처
tcpdump -i eth0 -e -nn -c 10
# VLAN 태그 포함 캡처
tcpdump -i eth0 -e vlan
# ARP만 캡처
tcpdump -i eth0 arp -e
# 특정 EtherType 캡처
tcpdump -i eth0 ether proto 0x88cc # LLDP
ethtool Netlink API
전통적으로 ethtool은 SIOCETHTOOL ioctl 인터페이스를 통해 커널과 통신했습니다. 리눅스 5.6부터는 Generic Netlink(genetlink) 기반의 새로운 인터페이스가 도입되어 확장성, 구조화된 속성, 멀티캐스트 알림(Notification) 기능이 크게 개선되었습니다.
ioctl vs Netlink 비교
ETHTOOL_MSG 메시지 유형
| 메시지 | 요청 방향 | 설명 |
|---|---|---|
ETHTOOL_MSG_STRSET_GET | User → Kernel | 문자열 집합(드라이버 이름, 통계 이름 등) 조회 |
ETHTOOL_MSG_LINKINFO_GET/SET | 양방향 | 링크 정보 (포트 유형, PHY 주소, TP MDI-X) |
ETHTOOL_MSG_LINKMODES_GET/SET | 양방향 | 지원/광고 링크 모드, 속도, 듀플렉스, Auto-Negotiation |
ETHTOOL_MSG_LINKSTATE_GET | User → Kernel | 링크 업/다운 상태, SQI(Signal Quality Index) |
ETHTOOL_MSG_RINGS_GET/SET | 양방향 | RX/TX 링 버퍼 크기 |
ETHTOOL_MSG_CHANNELS_GET/SET | 양방향 | Combined/RX/TX 채널(큐) 수 |
ETHTOOL_MSG_COALESCE_GET/SET | 양방향 | 인터럽트 병합 파라미터 |
ETHTOOL_MSG_PAUSE_GET/SET | 양방향 | Pause 프레임 설정, 통계 |
ETHTOOL_MSG_EEE_GET/SET | 양방향 | Energy Efficient Ethernet 설정 |
ETHTOOL_MSG_FEC_GET/SET | 양방향 | Forward Error Correction 모드 |
ETHTOOL_MSG_STATS_GET | User → Kernel | 표준화된 NIC 통계 (RFC 2863 기반) |
ETHTOOL_MSG_MODULE_EEPROM_GET | User → Kernel | SFP/QSFP 모듈 EEPROM 읽기 |
ETHTOOL_MSG_PSE_GET/SET | 양방향 | PoE PSE 컨트롤러 설정 |
ETHTOOL_MSG_RSS_GET | User → Kernel | RSS 해시 키/indirection 테이블 |
커널 구현 구조
net/ethtool/ 디렉터리는 기능별로 파일이 분리되어 있으며, ethnl_ops[] 디스패치 테이블이 메시지 유형별 핸들러를 관리합니다.
/* net/ethtool/netlink.c — ethnl_ops[] 디스패치 테이블 (발췌) */
static const struct ethnl_request_ops *ethnl_ops[__ETHTOOL_MSG_USER_CNT] = {
[ETHTOOL_MSG_STRSET_GET] = ðnl_strset_request_ops,
[ETHTOOL_MSG_LINKINFO_GET] = ðnl_linkinfo_request_ops,
[ETHTOOL_MSG_LINKINFO_SET] = ðnl_linkinfo_request_ops,
[ETHTOOL_MSG_LINKMODES_GET] = ðnl_linkmodes_request_ops,
[ETHTOOL_MSG_LINKMODES_SET] = ðnl_linkmodes_request_ops,
[ETHTOOL_MSG_LINKSTATE_GET] = ðnl_linkstate_request_ops,
[ETHTOOL_MSG_RINGS_GET] = ðnl_rings_request_ops,
[ETHTOOL_MSG_RINGS_SET] = ðnl_rings_request_ops,
[ETHTOOL_MSG_CHANNELS_GET] = ðnl_channels_request_ops,
[ETHTOOL_MSG_COALESCE_GET] = ðnl_coalesce_request_ops,
[ETHTOOL_MSG_PAUSE_GET] = ðnl_pause_request_ops,
[ETHTOOL_MSG_EEE_GET] = ðnl_eee_request_ops,
[ETHTOOL_MSG_FEC_GET] = ðnl_fec_request_ops,
[ETHTOOL_MSG_STATS_GET] = ðnl_stats_request_ops,
/* ... 각 메시지 유형별 request_ops 등록 */
};
/* 각 request_ops는 동일한 패턴으로 구성:
* .request_cmd = ETHTOOL_MSG_LINKSTATE_GET,
* .reply_cmd = ETHTOOL_MSG_LINKSTATE_GET_REPLY,
* .parse_request = linkstate_prepare_data,
* .prepare_data = linkstate_prepare_data,
* .reply_size = linkstate_reply_size,
* .fill_reply = linkstate_fill_reply,
*/
Netlink 인터페이스 사용 예시
# ethtool Netlink 모니터링 (실시간 이벤트 수신)
ethtool --monitor
# 출력 예시 (케이블 연결 시):
# [LINKSTATE] dev eth0 link up
# [LINKMODES] dev eth0 speed 25000Mb/s
# JSON 형식 출력 (Netlink 기반)
ethtool --json -s eth0
# genetlink raw 명령으로 링크 상태 조회
# (python3 + pyroute2 라이브러리 사용 예)
#!/usr/bin/env python3
from pyroute2 import GenericNetlinkSocket
import struct
genlsock = GenericNetlinkSocket()
genlsock.bind('ethtool', GenericNetlinkSocket.marshal)
# ETHTOOL_MSG_LINKSTATE_GET 요청
msg = genlsock.nlm_request(
msg_type='ethtool',
msg_flags=0,
cmd=6, # ETHTOOL_MSG_LINKSTATE_GET
attrs=[('ETHTOOL_A_HEADER_DEV_NAME', 'eth0')]
)
print(msg)
ethtool --debug 옵션으로 실제 사용된 인터페이스를 확인할 수 있습니다. 커널 5.17 이후로는 대부분의 ethtool 기능이 Netlink로 이전 완료되었습니다.
/* net/ethtool/linkstate.c — 링크 상태 조회 구현 */
static int linkstate_prepare_data(
const struct ethnl_req_info *req_base,
struct ethnl_reply_data *reply_base,
const struct genl_info *info)
{
struct linkstate_reply_data *data = LINKSTATE_REPDATA(reply_base);
struct net_device *dev = reply_base->dev;
/* __ethtool_get_link() → ethtool_ops->get_link() */
data->link = __ethtool_get_link(dev);
/* SQI (Signal Quality Indicator) - 자동차 이더넷 등 */
if (dev->phydev) {
data->sqi = phy_get_sqi(dev->phydev);
data->sqi_max = phy_get_sqi_max(dev->phydev);
}
return 0;
}
/* net/ethtool/linkmodes.c — 링크 모드 설정 */
static int linkmodes_set(
struct ethnl_req_info *req_base,
struct genl_info *info)
{
struct ethtool_link_ksettings ksettings;
struct net_device *dev = req_base->dev;
/* 현재 설정 가져오기 */
__ethtool_get_link_ksettings(dev, &ksettings);
/* Netlink 속성에서 변경 사항 적용 */
ethnl_update_linkmodes(&ksettings, info);
/* 드라이버에 새 설정 반영 */
return dev->ethtool_ops->set_link_ksettings(dev, &ksettings);
}
칩셋별 구현 분석 — Intel ice와 Mellanox mlx5
현대 데이터센터에서 가장 널리 사용되는 두 NIC 드라이버인 Intel ice(E810 시리즈)와 Mellanox(NVIDIA) mlx5(ConnectX 시리즈)의 커널 내부 구현을 비교 분석합니다. 두 드라이버 모두 25G/100G/200G 이상의 고속 이더넷을 지원하지만, 아키텍처적 접근이 크게 다릅니다.
Intel ice 드라이버 (E810 시리즈)
Intel E810 NIC는 drivers/net/ethernet/intel/ice/ 디렉터리에 구현되어 있습니다. AdminQ(Admin Queue)를 통한 펌웨어 통신, VSI(Virtual Station Interface) 기반 가상화, DDP(Dynamic Device Personalization)를 통한 프로토콜 파싱 커스터마이징이 특징입니다.
/* drivers/net/ethernet/intel/ice/ice_main.c
* ice_open() — 인터페이스 활성화 (ifconfig up / ip link set up) */
static int ice_open(struct net_device *netdev)
{
struct ice_netdev_priv *np = netdev_priv(netdev);
struct ice_vsi *vsi = np->vsi;
struct ice_pf *pf = vsi->back;
int err;
/* PHY 설정 복원 (속도, FEC, AN) */
err = ice_init_phy_user_cfg(pf->hw.port_info);
if (err)
return err;
/* VSI 큐 할당 및 인터럽트 설정 */
err = ice_vsi_open(vsi); /* → ice_vsi_cfg_txqs/rxqs → NAPI 등록 */
if (err)
return err;
/* AdminQ를 통해 펌웨어에 링크 활성화 요청 */
err = ice_cfg_link(pf->hw.port_info, true);
netif_carrier_on(netdev);
netif_tx_start_all_queues(netdev);
return 0;
}
/* ice_xmit_frame() — TX 경로 진입점 (ndo_start_xmit) */
static netdev_tx_t ice_xmit_frame(struct sk_buff *skb,
struct net_device *netdev)
{
struct ice_netdev_priv *np = netdev_priv(netdev);
struct ice_tx_ring *ring;
/* XPS/해시 기반 큐 선택 */
ring = np->vsi->tx_rings[skb_get_queue_mapping(skb)];
/* TX descriptor 작성 및 doorbell ring */
return ice_xmit_frame_ring(skb, ring);
}
/* ice_clean_rx_irq() — NAPI poll에서 호출되는 RX 처리 */
static int ice_clean_rx_irq(struct ice_rx_ring *rx_ring,
int budget)
{
unsigned int total_pkts = 0;
while (total_pkts < budget) {
union ice_32b_rx_flex_desc *rx_desc;
struct sk_buff *skb;
rx_desc = ICE_RX_DESC(rx_ring, rx_ring->next_to_clean);
if (!ice_test_staterr(rx_desc, ICE_RXD_DD))
break; /* Descriptor Done 비트 확인 */
skb = ice_build_skb(rx_ring, rx_desc);
ice_rx_csum(rx_ring, skb, rx_desc); /* HW 체크섬 */
ice_rx_hash(rx_ring, skb, rx_desc); /* RSS 해시 */
napi_gro_receive(rx_ring->napi, skb); /* GRO + 스택 전달 */
total_pkts++;
}
return total_pkts;
}
Mellanox mlx5 드라이버 (ConnectX 시리즈)
NVIDIA(Mellanox) ConnectX-6/7 NIC는 drivers/net/ethernet/mellanox/mlx5/에 구현되어 있습니다. WQE(Work Queue Element) 기반 TX/RX, 강력한 eSwitch(embedded Switch), TC 오프로드, RDMA/RoCE 통합이 특징입니다.
/* drivers/net/ethernet/mellanox/mlx5/core/en_tx.c
* mlx5e_xmit() — TX 진입점 */
netdev_tx_t mlx5e_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct mlx5e_priv *priv = netdev_priv(dev);
struct mlx5e_txqsq *sq;
u16 pi;
sq = priv->txq2sq[skb_get_queue_mapping(skb)];
/* WQE 슬롯 확보 */
pi = mlx5_wq_cyc_ctr2ix(&sq->wq, sq->pc);
/* WQE 구성: Control + Ethernet + Data segments */
mlx5e_sq_xmit_wqe(sq, skb, pi);
/* Doorbell: NIC에 새 WQE 알림 */
mlx5e_notify_hw(&sq->wq, sq->pc, sq->uar_map);
sq->pc++;
return NETDEV_TX_OK;
}
/* mlx5e_poll_tx_cq() — TX 완료 처리 */
static bool mlx5e_poll_tx_cq(struct mlx5e_cq *cq, int napi_budget)
{
struct mlx5_cqe64 *cqe;
while ((cqe = mlx5_cqwq_get_cqe(&cq->wq))) {
u16 wqe_counter = be16_to_cpu(cqe->wqe_counter);
/* 완료된 WQE에 연결된 sk_buff 해제 */
mlx5e_free_txqsq_descs(sq, wqe_counter);
mlx5_cqwq_pop(&cq->wq);
}
mlx5_cqwq_update_db_record(&cq->wq);
return true;
}
Intel ice vs Mellanox mlx5 비교
| 항목 | Intel ice (E810) | Mellanox mlx5 (ConnectX-6/7) |
|---|---|---|
| FW 통신 | AdminQ (비동기 메일박스) | Command Interface (동기/비동기 큐) |
| TX 방식 | 전통적 TX descriptor ring | WQE 기반 (Control+Eth+Data seg) |
| RX 방식 | Flex descriptor (32B/16B) | Striding RQ (MPWQE), 일반 RQ |
| SR-IOV VF | 최대 256 VF | 최대 1024 VF (ConnectX-7) |
| eSwitch | 기본 VF 스위칭 | 완전한 switchdev 모드, OVS 오프로드 |
| TC 오프로드 | ADQ (Application Device Queues) | TC flower → HW 필터, 체인 지원 |
| 프로토콜 커스터마이징 | DDP 프로파일 (파서 파이프라인) | Programmable parser (ConnectX-7+) |
| RDMA | iWARP (일부 모델) | RoCEv2 (기본), iWARP 옵션 |
| AF_XDP | 제로카피 지원 | 제로카피 지원 (XSK) |
| PTP | 하드웨어 타임스탬프 | 하드웨어 타임스탬프 |
| 최대 속도 | 100G (E810), 200G (E830 예정) | 400G (ConnectX-7) |
| 드라이버 크기 | ~80개 소스 파일 | ~200개 소스 파일 (core + en) |
| 핵심 파일 | ice_main.c, ice_txrx.c, ice_adminq.c | en_main.c, en_tx.c, en_rx.c, eswitch.c |
ice 드라이버는 ice_type.h에 하드웨어 레지스터/구조체가 집중되어 있고, mlx5는 mlx5_ifc.h에 펌웨어 인터페이스 정의가 자동 생성됩니다. 두 드라이버 모두 ethtool 콜백은 *_ethtool.c, 통계는 *_stats.c에 분리되어 있어 필요한 기능별로 파일을 찾기 쉽습니다.
이더넷 드라이버 작성 가이드
리눅스 이더넷 드라이버의 핵심 구조를 이해하면 NIC 관련 문제를 디버깅하고 커스텀 드라이버를 작성하는 데 큰 도움이 됩니다. 최소한의 이더넷 드라이버 스켈레톤을 통해 핵심 콜백과 초기화 흐름을 살펴봅니다.
최소 드라이버 스켈레톤
/* 최소 이더넷 드라이버 구조 (개요) */
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
struct my_priv {
struct pci_dev *pdev;
void __iomem *hw_addr; /* MMIO base */
struct napi_struct napi;
/* TX/RX descriptor rings, DMA 주소 등 */
};
/* ---- TX 경로 ---- */
static netdev_tx_t my_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct my_priv *priv = netdev_priv(dev);
/* 1. TX descriptor에 skb 매핑 */
dma_addr_t dma = dma_map_single(&priv->pdev->dev,
skb->data, skb->len, DMA_TO_DEVICE);
/* 2. Descriptor에 DMA 주소/길이 기록 */
/* 3. Doorbell (tail pointer) 업데이트 */
writel(new_tail, priv->hw_addr + TX_TAIL_REG);
return NETDEV_TX_OK;
}
/* ---- RX 경로 (NAPI poll) ---- */
static int my_poll(struct napi_struct *napi, int budget)
{
struct my_priv *priv = container_of(napi, struct my_priv, napi);
int cleaned = 0;
while (cleaned < budget) {
/* RX descriptor에서 완료된 패킷 확인 */
if (!rx_desc_done(priv))
break;
struct sk_buff *skb = napi_alloc_skb(napi, pkt_len);
/* DMA 언맵 + 데이터 복사 (또는 page flip) */
skb->protocol = eth_type_trans(skb, priv->ndev);
napi_gro_receive(napi, skb);
cleaned++;
}
if (cleaned < budget) {
napi_complete_done(napi, cleaned);
/* 인터럽트 재활성화 */
my_irq_enable(priv);
}
return cleaned;
}
/* ---- IRQ 핸들러 ---- */
static irqreturn_t my_irq(int irq, void *data)
{
struct my_priv *priv = data;
/* 인터럽트 비활성화 (NAPI가 처리) */
my_irq_disable(priv);
napi_schedule(&priv->napi);
return IRQ_HANDLED;
}
/* ---- net_device_ops ---- */
static const struct net_device_ops my_netdev_ops = {
.ndo_open = my_open, /* ip link set up */
.ndo_stop = my_close, /* ip link set down */
.ndo_start_xmit = my_xmit, /* 패킷 송신 */
.ndo_set_rx_mode = my_set_rx_mode,/* 프로미스큐어스/멀티캐스트 */
.ndo_set_mac_address = eth_mac_addr, /* 기본 MAC 변경 */
.ndo_change_mtu = my_change_mtu, /* MTU 변경 */
.ndo_get_stats64 = my_get_stats, /* 통계 */
};
/* ---- PCI probe ---- */
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *ndev;
struct my_priv *priv;
pci_enable_device(pdev);
pci_set_master(pdev);
dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
ndev = alloc_etherdev(sizeof(*priv));
priv = netdev_priv(ndev);
priv->pdev = pdev;
priv->hw_addr = pci_ioremap_bar(pdev, 0);
ndev->netdev_ops = &my_netdev_ops;
ndev->ethtool_ops = &my_ethtool_ops;
/* MAC 주소 읽기 (NIC EEPROM/레지스터에서) */
my_read_mac(priv, ndev->dev_addr);
netif_napi_add(ndev, &priv->napi, my_poll);
register_netdev(ndev);
pci_set_drvdata(pdev, ndev);
return 0;
}
- DMA 일관성:
dma_map_single()/dma_unmap_single()쌍을 반드시 맞추고, 매핑(Mapping) 실패 시dma_mapping_error()로 확인 - NAPI 원칙: IRQ 핸들러(Handler)에서는
napi_schedule()만 호출하고, 실제 패킷 처리는 poll 콜백에서 수행. budget 초과 시 poll을 재스케줄링 - 메모리 배리어(Memory Barrier): descriptor ring 업데이트 시
wmb()/rmb()로 DMA 순서 보장(Ordering) - 에러 경로: probe에서 실패 시 할당된 모든 리소스를 역순으로 해제 (goto 패턴)
- XDP 지원:
ndo_bpf,ndo_xdp_xmit콜백 구현으로 XDP 프로그램 연동
자주 하는 실수와 트러블슈팅
이더넷 환경에서 반복적으로 발생하는 문제와 해결 방법을 정리합니다.
1. 듀플렉스 불일치 (Duplex Mismatch)
late_collision, rx_crc_errors가 증가합니다.
원인: 한쪽은 Full-Duplex, 다른 쪽은 Half-Duplex로 설정된 경우. Auto-Negotiation을 한쪽만 끄면 흔히 발생합니다.
해결: 양쪽 모두
autoneg on으로 설정하거나, 양쪽 모두 동일하게 강제 설정합니다.
# 진단
ethtool eth0 | grep -E "Speed|Duplex|Auto-negotiation"
ethtool -S eth0 | grep -i "late_collision\|crc"
# 해결: 양단 Auto-Negotiation 활성화
ethtool -s eth0 autoneg on
2. MTU 불일치
ping -s 1472이 실패합니다.
원인: 경로상 한 장비의 MTU가 다른 장비보다 작고, PMTUD(Path MTU Discovery)가 차단된 경우.
해결: 경로 전체 MTU를 통일하거나, ICMP "Fragmentation Needed" 메시지가 차단되지 않았는지 확인합니다.
# 경로 MTU 확인
tracepath -n 10.0.0.1
ping -M do -s 8972 10.0.0.1 # Jumbo Frame 경로 테스트
# 모든 장비 MTU 확인
ip link show | grep mtu
3. 오프로드 문제 (체크섬/TSO 오류)
원인: TX checksum 오프로드가 활성화된 상태에서 NIC이 보내기 전의 패킷을 캡처하면 체크섬이 아직 계산되지 않아 잘못된 것처럼 보입니다. 이는 정상 동작입니다.
실제 문제인 경우: RX checksum 오류가 지속적으로 증가하면 NIC 하드웨어 또는 드라이버 버그입니다.
# tcpdump "bad checksum"이 TX 오프로드 때문인지 확인
ethtool -k eth0 | grep "tx-checksum"
# 오프로드 비활성화로 문제 격리
ethtool -K eth0 tx off rx off tso off gro off
# 문제가 사라지면 오프로드 관련 드라이버 버그
# 하나씩 다시 켜며 원인 특정
ethtool -K eth0 rx on # RX checksum
ethtool -K eth0 tx on # TX checksum
ethtool -K eth0 tso on # TSO
ethtool -K eth0 gro on # GRO
4. FEC 불일치로 링크 미연결
ethtool eth0에서 "Link detected: no".
원인: FEC 모드가 양단에서 다릅니다 (예: 한쪽 RS-FEC, 다른 쪽 No FEC).
해결: 양단 FEC 모드를 동일하게 맞추거나
auto로 설정합니다.
ethtool --show-fec eth0
ethtool --set-fec eth0 encoding auto
5. RGMII 지연 이중 적용
원인: Device Tree에서
phy-mode = "rgmii-id"로 PHY가 TX/RX 지연을 추가하는데, PHY 레지스터에도 내부 지연이 켜져 있어 이중 지연(4ns)이 됩니다.
해결: Device Tree와 PHY 레지스터 중 정확히 한 곳에서만 지연을 추가합니다.
# Device Tree 확인
cat /sys/firmware/devicetree/base/ethernet*/phy-mode
# PHY 레지스터에서 지연 설정 확인 (MDIO)
ethtool -d eth0 # 또는 phytool
# RGMII 모드 종류:
# "rgmii" — 지연 없음 (PCB 트레이스로 추가)
# "rgmii-id" — PHY가 TX+RX 지연 모두 추가
# "rgmii-txid" — PHY가 TX 지연만 추가
# "rgmii-rxid" — PHY가 RX 지연만 추가
6. 링 버퍼 부족으로 패킷 드롭
rx_missed_errors 또는 rx_no_buffer_count가 증가합니다. ifconfig에서 RX dropped 증가.
원인: RX Ring Buffer 크기가 작아서 NAPI가 처리하기 전에 새 패킷이 덮어씌워집니다.
해결: Ring Buffer를 최대로 늘리고, Adaptive Coalescing을 활성화합니다.
# 현재 Ring 크기 확인
ethtool -g eth0
# 최대로 설정
ethtool -G eth0 rx 4096 tx 4096
# Adaptive Coalescing 활성화
ethtool -C eth0 adaptive-rx on adaptive-tx on
# 드롭 원인 상세 추적
dropwatch -l kas
7. IRQ 편중 (단일 CPU 과부하)
/proc/interrupts에서 한 CPU에 NIC IRQ가 집중됩니다.
원인:
irqbalance가 NIC IRQ를 적절히 분산하지 못하거나, IRQ affinity가 설정되지 않았습니다.
해결: 수동으로 IRQ affinity를 설정하거나 드라이버 제공 스크립트를 사용합니다.
# IRQ 분포 확인
grep eth0 /proc/interrupts
# IRQ를 CPU별로 분산 (예: 8큐 NIC)
# Queue 0 → CPU 0, Queue 1 → CPU 1, ...
for i in $(seq 0 7); do
irq=$(grep "eth0-TxRx-$i" /proc/interrupts | awk '{print $1}' | tr -d :)
echo $((1 << $i)) > /proc/irq/$irq/smp_affinity
done
# 또는 Intel 제공 스크립트
set_irq_affinity eth0
8. EEE로 인한 지연 증가
원인: EEE(Energy Efficient Ethernet)의 LPI 복귀 시간이 영향을 줍니다.
해결: 지연에 민감한 환경에서는 EEE를 비활성화합니다.
ethtool --show-eee eth0
ethtool --set-eee eth0 eee off
종합 트러블슈팅 체크리스트
| 증상 | 1차 확인 | 2차 확인 | 3차 확인 |
|---|---|---|---|
| 링크 미연결 | 케이블/SFP 물리 연결 | ethtool --show-fec FEC 일치 | ethtool -s 속도/듀플렉스 강제 |
| 간헐적 끊김 | ethtool -S CRC/alignment 오류 | ethtool -m SFP RX Power | ethtool --cable-test |
| 성능 저하 | ethtool -g Ring 크기 | ethtool -c Coalescing | IRQ affinity, CPU 분산 |
| 패킷 드롭 | ethtool -S rx_missed/no_buffer | ethtool -G Ring 증가 | sysctl netdev_budget |
| 높은 CPU 사용률 | 오프로드 확인 ethtool -k | RSS 큐 수 확인 ethtool -l | GRO/TSO 활성화 |
| 지연 변동 | --show-eee EEE 확인 | Coalescing 값 조정 | NUMA/CPU pinning |
커널 소스 참조
| 파일/디렉토리 | 역할 |
|---|---|
include/uapi/linux/if_ether.h | 이더넷 상수, struct ethhdr |
net/ethernet/eth.c | eth_type_trans(), eth_header() |
include/linux/ethtool.h | ethtool_ops, 링크 설정 구조체 |
net/ethtool/ | ethtool Netlink 인터페이스 (커널 5.6+) |
include/linux/phylink.h | phylink API, phylink_mac_ops |
drivers/net/phy/ | PHY 드라이버 (Marvell, Broadcom, Realtek 등) |
drivers/net/ethernet/ | 이더넷 NIC 드라이버 (intel/, mellanox/, broadcom/ 등) |
net/8021q/ | 802.1Q VLAN 구현 |
net/sched/sch_taprio.c | TSN TAS (taprio qdisc) |
net/sched/sch_cbs.c | TSN CBS (Credit-Based Shaper) |
net/sched/sch_etf.c | TSN ETF (Earliest TxTime First) |
drivers/net/macsec.c | MACsec 소프트웨어 구현 |
include/net/macsec.h | MACsec 구조체, offload API |
net/dsa/ | DSA 프레임워크 코어 |
drivers/net/dsa/ | DSA 스위치 드라이버 (mv88e6xxx, ksz, sja1105 등) |
net/xdp/ | XDP 코어, AF_XDP 소켓 |
include/net/xdp_sock.h | AF_XDP 소켓 구조체 |
drivers/net/ethernet/intel/ice/ | Intel E810 드라이버 (SR-IOV, MACsec, PTP) |
drivers/net/ethernet/mellanox/mlx5/ | Mellanox ConnectX 드라이버 (eSwitch, SR-IOV) |
관련 문서
- MAC 주소 (MAC Address) — EUI-48 비트 구조, ARP/Neighbor, Bridge FDB, MAC 필터링
- sk_buff 자료구조 — 이더넷 프레임을 담는 핵심 커널 버퍼
- 네트워크 스택 — 전체 네트워크 스택 아키텍처와 패킷 흐름
- GSO/GRO 네트워크 오프로드 — 세그멘테이션/병합 오프로드 상세
- NAPI (New API) — 인터럽트 완화와 폴링(Polling) 기반 패킷 수신
- Network Device 드라이버 — net_device_ops, NAPI, ethtool 구현
- Bridge/VLAN/Bonding — L2 브릿지, VLAN, 링크 어그리게이션
- TC (Traffic Control) — 트래픽 셰이핑, 큐잉 디시플린
- IP 프로토콜 (IPv4/IPv6) — 이더넷 위의 L3 프로토콜