리눅스 네트워크 스택 — 소켓에서 NIC까지 패킷의 여정
애플리케이션에서
send()를 호출하면 데이터가 어떤 경로로 네트워크 카드까지 도달할까요? 리눅스 네트워크 스택의 전체 흐름을 이해하면 네트워크 성능 튜닝과 트러블슈팅의 기반을 갖출 수 있습니다.
전체 구조
애플리케이션 (유저 공간)
│ send() / recv()
├─────────────────────── 시스템 콜 경계
│ 소켓 계층 (Socket Layer)
│ │
│ 프로토콜 계층 (TCP/UDP/IP)
│ │
│ 네트워크 디바이스 계층
│ │
│ 디바이스 드라이버
│ │
├─────────────────────── HW/SW 경계
NIC (네트워크 카드)
송신 과정 (send)
1. 애플리케이션: send(socket, data, len, 0)
│
2. 소켓 계층:
│ - 소켓 버퍼(sk_buff) 생성
│ - 데이터를 커널 버퍼에 복사
│
3. TCP 계층:
│ - 세그먼트 분할 (MSS 크기)
│ - TCP 헤더 추가 (시퀀스 번호, 체크섬 등)
│ - 재전송 큐에 사본 저장
│ - 혼잡 제어 윈도우 확인
│
4. IP 계층:
│ - IP 헤더 추가 (출발/목적 IP)
│ - 라우팅 테이블 조회 → 출력 인터페이스 결정
│ - 필요 시 IP 단편화
│ - Netfilter (iptables) 처리
│
5. 이더넷 계층:
│ - ARP로 MAC 주소 확인
│ - 이더넷 헤더 추가
│
6. 디바이스 드라이버:
│ - TX 큐에 패킷 추가
│ - DMA로 NIC에 전송
│
7. NIC: 물리적 전송
수신 과정 (recv)
1. NIC: 패킷 수신 → DMA로 링 버퍼에 저장
│
2. 인터럽트 + NAPI:
│ - 하드웨어 인터럽트 발생
│ - NAPI(New API)로 폴링 모드 전환 (인터럽트 폭주 방지)
│ - 여러 패킷을 한 번에 처리
│
3. 네트워크 계층:
│ - 이더넷 헤더 파싱
│ - IP 헤더 확인, 체크섬 검증
│ - Netfilter 처리
│
4. TCP 계층:
│ - TCP 헤더 파싱
│ - 시퀀스 번호 확인, ACK 생성
│ - 수신 버퍼에 데이터 저장
│ - 소켓에 데이터 도착 알림
│
5. 소켓 계층:
│ - 수신 버퍼에서 데이터 대기
│
6. 애플리케이션: recv()로 데이터 읽기
- 커널 버퍼 → 유저 버퍼 복사
sk_buff (소켓 버퍼)
리눅스 네트워크 스택의 핵심 자료구조입니다. 패킷 하나를 표현합니다.
sk_buff 구조:
┌─────────────────────────────┐
│ head ─────────────────────→ │ 버퍼 시작
│ data ────────→ ┌──────────┐│
│ │ 이더넷 헤더│ │
│ │ IP 헤더 ││
│ │ TCP 헤더 ││
│ │ 페이로드 ││
│ tail ────────→ └──────────┘│
│ end ──────────────────────→ │ 버퍼 끝
└─────────────────────────────┘
각 계층은 헤더를 추가/제거할 때 data/tail 포인터만 이동합니다 (복사 없이).
NAPI (New API)
고속 네트워크에서 인터럽트 기반 → 폴링 기반 으로 전환하여 성능을 높이는 메커니즘입니다.
기존: 패킷마다 인터럽트 → 10Gbps에서 초당 수백만 번 인터럽트 → CPU 과부하
NAPI:
1. 첫 패킷: 인터럽트 발생
2. 인터럽트 비활성화
3. 폴링 모드: 링 버퍼에서 패킷을 모아서 처리
4. 패킷이 없으면 다시 인터럽트 모드로
성능 관련 커널 파라미터
# 소켓 버퍼 크기
cat /proc/sys/net/core/rmem_max # 수신 버퍼 최대
cat /proc/sys/net/core/wmem_max # 송신 버퍼 최대
# TCP 버퍼 자동 튜닝
cat /proc/sys/net/ipv4/tcp_rmem # min default max
cat /proc/sys/net/ipv4/tcp_wmem
# 네트워크 백로그
cat /proc/sys/net/core/netdev_max_backlog # NIC → 커널 큐 크기
cat /proc/sys/net/core/somaxconn # listen() 백로그
# RSS (Receive Side Scaling)
cat /proc/interrupts | grep eth0 # NIC 인터럽트의 CPU 분산 확인
핵심 포인트
- send()가 반환되었다고 전송 완료가 아님: 커널 버퍼에 복사되었을 뿐
- NAPI의 필요성: 고속 네트워크에서 인터럽트 폭주(interrupt storm) 방지
- zero-copy: sendfile(), splice()로 커널-유저 간 불필요한 복사 제거
- sk_buff 포인터 조작: 각 계층이 헤더를 추가/제거할 때 메모리 복사 없이 포인터만 이동
정리
리눅스 네트워크 스택은 소켓-TCP-IP-디바이스 드라이버-NIC의 계층 구조입니다. 각 계층이 sk_buff를 통해 패킷을 전달하고, NAPI로 고속 처리를 지원합니다. 이 흐름을 이해하면 네트워크 성능 문제의 원인을 파악할 수 있습니다.