개발/공부 기록

CSAPP 11장 공부 기록 2편: 소켓 fd에서 커널 버퍼, NIC, DMA까지

cedis 2026. 5. 23. 00:11

CSAPP 11장 공부 기록 2편

소켓과 커널 내부: fd에서 NIC/DMA까지

1편에서는 데이터가 컴퓨터 밖에서 frame, packet, segment로 포장되어 이동하는 그림을 잡았다. 이번 글에서는 같은 일을 컴퓨터 안쪽에서 본다. 응용 프로그램이 write(sockfd, buf, n) 한 줄을 호출하면, 운영체제와 하드웨어는 실제로 무엇을 하는가?

이번 글에서 다루는 것

  • 소켓은 물리적인 구멍이 아니라 커널의 소프트웨어 객체라는 점
  • fd, FDT, VFS, socket object가 어떻게 이어지는지
  • socket, bind, listen, accept, connect 호출 순서
  • CSAPP helper 함수 open_clientfd, open_listenfd, RIO 함수가 무엇을 자동화하는지
  • TCP와 UDP의 차이, 3-way handshake, byte stream과 datagram
  • 송신 경로: user buffer에서 kernel socket buffer, TCP/IP stack, NIC, DMA까지
  • 수신 경로: NIC DMA에서 kernel receive queue, user buffer까지
  • EOF, short count, rio_readlineb, rio_writen이 왜 필요한지

1. 질문 흐름: 소켓을 파일처럼 본다는 말에서 시작했다

처음에는 "소켓은 연결의 끝점"이라는 설명만으로는 부족했다. 끝점이 어디에 실제로 존재하는지, 랜카드에 있는 구멍인지, 커널 메모리에 있는 구조체인지, fd라는 정수는 왜 필요한지 감이 잡히지 않았다.

공부하면서 질문은 아래처럼 이어졌다.

상위 질문 하부 질문 잡은 결론
소켓도 파일이라면 fd는 무엇인가? FDT, VFS, socket object는 어디서 등장하는가? fd는 커널 객체를 찾는 정수 핸들이다.
write가 리턴하면 진짜 전송이 끝난 것인가? user buffer와 kernel buffer를 왜 나누는가? 대체로 커널 버퍼에 접수된 것이지, 선로 송신 완료가 아니다.
데이터가 RAM에 들어오면 가상 주소인가 물리 주소인가? NIC와 DMA는 CPU 대신 무엇을 하는가? NIC는 DMA로 커널이 준비한 물리 메모리 버퍼에 읽고 쓴다.
TCP가 더 까다로운데 왜 stream처럼 자유롭게 읽는가? UDP는 왜 datagram 경계를 보존하는가? TCP는 바이트 순서를 관리하므로 응용에는 물줄기처럼 보인다.

2. 소켓은 하드웨어 구멍이 아니라 커널 객체다

소켓을 처음 들으면 랜선이 꽂히는 물리 구멍처럼 느껴진다. 하지만 프로그래밍 관점에서 소켓은 운영체제 커널이 만들어 둔 논리적 엔드포인트다. 물리적인 것은 NIC이고, 소켓은 그 NIC와 커널 버퍼, 프로토콜 상태를 사용하게 해 주는 소프트웨어 객체다.

파일도 비슷하다. 사용자는 디스크 블록을 직접 들고 있지 않고 fd라는 정수만 들고 있다. 소켓도 마찬가지다. 사용자는 sockfd라는 정수만 들고, 커널은 그 정수로 실제 socket 객체와 TCP/UDP 상태를 찾아간다.

sockfd가 열어 주는 커널 객체 체인

user space
  sockfd = 3
      |
      v
per-process descriptor table(FDT)
      |
      v
struct file
      |
      v
struct socket
      |
      v
struct sock / tcp_sock / udp_sock
      |
      v
send queue, receive queue, TCP state, buffer metadata

이 구조 때문에 소켓도 read, write, close로 다룰 수 있다. 유닉스의 "모든 것은 파일이다"는 말은, 모든 장치가 진짜 디스크 파일이라는 뜻이 아니라 I/O를 fd와 read/write라는 공통 인터페이스로 추상화했다는 뜻이다.

3. fd와 FDT: 숫자 하나가 진짜 객체로 가는 길

fd는 file descriptor, 즉 파일 식별자다. 프로세스가 파일이나 소켓을 열면 커널은 작은 정수를 돌려준다. 응용 프로그램은 이후 이 숫자를 이용해 read(fd), write(fd), close(fd)를 호출한다.

이 숫자가 의미를 가지려면 어디엔가 매핑표가 있어야 한다. 그 매핑표가 프로세스별 식별자 테이블, 즉 FDT(File Descriptor Table)다.

비유

fd는 리모컨의 버튼 번호다. FDT는 내 리모컨 설명서다. 내가 3번 버튼을 누르면, 커널은 내 프로세스의 FDT 3번 칸을 보고 진짜 파일이나 소켓 객체로 찾아간다. 다른 프로세스의 3번 버튼은 전혀 다른 장치를 가리킬 수 있다.

대상 무엇인가 왜 필요한가
fd 응용 프로그램이 들고 있는 작은 정수 복잡한 커널 객체를 직접 노출하지 않기 위해
FDT 프로세스별 fd 매핑 테이블 각 프로세스가 같은 fd 번호를 독립적으로 쓰게 하기 위해
VFS 파일, 소켓, 장치를 공통 I/O 대상으로 보는 커널 계층 write 하나로 디스크 파일과 소켓을 모두 다루기 위해
v-node / i-node 파일의 메타데이터와 실제 디스크 위치를 찾는 구조 디스크 파일 I/O에서 실제 파일 정보를 추적하기 위해

소켓 fd는 디스크 파일의 v-node와 완전히 같은 것은 아니지만, VFS 관점에서는 둘 다 fd로 시작하는 I/O 대상이다. 그래서 네트워크를 "계속 갱신되는 파일 같은 I/O 대상"으로 생각하는 비유는 출발점으로 좋다. 다만 소켓은 lseek으로 되감는 파일이 아니라, 흘러오는 스트림이나 datagram을 읽고 쓰는 대상이라는 차이가 있다.

sockfd=3을 커널이 따라가는 실제 길

user code:
  write(3, buf, 100)

kernel:
  현재 프로세스의 FDT[3]
        |
        v
  struct file
        |
        v
  struct socket
        |
        v
  struct sock / tcp_sock
        |
        v
  send queue, receive queue, TCP state

응용 프로그램 입장에서는 3이라는 숫자 하나만 보인다. 하지만 커널 안에서는 그 숫자가 소켓 객체, 프로토콜 상태, 송수신 큐까지 이어지는 입구다. 그래서 fd는 단순한 번호표이면서도 커널 내부 세계로 들어가는 손잡이다.

4. 소켓 시스템 콜은 glibc 래퍼를 지나 커널로 들어간다

C 코드에서 connect를 호출한다고 해서 사용자 공간 함수 하나가 모든 일을 처리하는 것은 아니다. 보통 흐름은 glibc wrapper가 인자를 준비하고, CPU의 syscall 명령을 통해 커널 모드로 들어가며, 커널의 socket 구현이 실제 작업을 수행하는 구조다.

user program
  connect(sockfd, ...)
      |
      v
glibc wrapper
  syscall number와 인자 준비
      |
      v
syscall
  ring 3 -> ring 0
      |
      v
kernel
  fdtable에서 struct socket 찾기
  TCP면 tcp connect path
  UDP면 주소 기록 중심의 connect path
      |
      v
return to user space

같은 sendwrite처럼 보여도, 커널 내부에서는 소켓 종류에 따라 함수 포인터 테이블을 타고 TCP 전담 함수나 UDP 전담 함수로 분기된다. 그래서 소켓 인터페이스는 프로토콜 독립적인 범용 리모컨처럼 보인다.

5. 서버와 클라이언트의 소켓 호출 순서

CSAPP 11.4의 소켓 인터페이스는 함수 하나하나보다 호출 순서가 중요하다. 전화 비유로 보면 서버는 전화를 받을 매장을 열고, 클라이언트는 그 매장으로 전화를 건다.

서버 의미 클라이언트 의미
getaddrinfo 서버 주소 후보 준비 getaddrinfo 접속할 서버 주소 찾기
socket 듣기용 소켓 생성 socket 클라이언트 소켓 생성
bind IP/port를 소켓에 묶음 connect 서버에 연결 요청
listen 연결 요청 대기 상태 read/write 데이터 송수신
accept 요청 하나를 받아 통신용 connfd 생성 close 연결 종료

서버에서 특히 중요한 것은 listenfdconnfd의 분리다. listenfd는 손님을 받는 대표 전화기이고, accept가 돌려주는 connfd는 실제 손님 한 명과 대화하는 전용 전화기다.

6. CSAPP helper 함수: 복잡한 준비 과정을 접어 둔 버튼

CSAPP는 실제 코드에서 반복되는 준비 과정을 helper 함수로 묶어 둔다. 이 함수들은 원리를 숨기기 위한 것이 아니라, 매번 같은 보일러플레이트를 줄이고 예제의 핵심을 보게 하기 위한 장치다.

helper 무엇을 해 주나 내부에서 자동화하는 것
open_clientfd 클라이언트가 서버에 연결된 fd를 얻는다. getaddrinfo, socket, connect, freeaddrinfo
open_listenfd 서버가 연결 요청을 받을 listenfd를 만든다. getaddrinfo, socket, setsockopt, bind, listen
rio_writen n바이트를 끝까지 쓰려고 반복한다. short write와 interrupt 재시도 처리
rio_readlineb 버퍼링된 입력에서 한 줄을 읽는다. 내부 버퍼, 줄바꿈 탐지, read 호출 횟수 감소

중요한 점은 open_listenfdaccept까지 해 주지는 않는다는 것이다. 듣기 준비는 한 번만 하면 되지만, accept는 클라이언트가 올 때마다 반복해서 호출되어야 하기 때문이다.

7. TCP와 UDP: 둘 다 포트를 쓰지만 전략이 다르다

IP는 호스트까지 배달한다. 하지만 한 호스트 안에는 웹 서버, SSH, 게임, 브라우저 등 여러 프로세스가 동시에 있다. TCP와 UDP는 포트 번호를 사용해 어느 프로세스의 소켓으로 데이터를 넘길지 구분한다.

둘 다 IP 위에서 동작하지만, 데이터 전달 전략이 다르다.

비교 TCP UDP
연결 3-way handshake로 연결 수립 연결 없음
신뢰성 sequence, ack, 재전송, 순서 보장 보장하지 않음
전송 단위 byte stream, 경계 없음 datagram, 메시지 경계 보존
비유 번호표 붙은 등기 택배 빠르게 던지는 엽서
대표 사용 HTTP, SSH, DB 연결 DNS 기본 질의, 실시간 게임, 음성/영상 일부

TCP가 더 까다로운데 왜 stream이 가능한가?

TCP는 "상자 번호"가 아니라 바이트 위치에 가까운 sequence number로 흐름을 관리한다. 그래서 수신 커널은 도착한 segment를 순서대로 조립해 하나의 물탱크처럼 보이게 만들 수 있다. 응용 프로그램은 그 물탱크에서 10바이트를 퍼가든 100바이트를 퍼가든 자유롭다. 반면 UDP는 그런 순서 관리가 없으므로 보낸 datagram 하나를 받은 datagram 하나로 보존해야 한다.

50바이트씩 두 번 보냈을 때

TCP:
  sender: write(50B)
  sender: write(50B)

  receiver kernel:
    sequence number를 보고 100B byte stream으로 조립

  receiver app:
    read(100B) 한 번으로 받을 수도 있음
    read(30B), read(70B)처럼 나눠 받을 수도 있음

UDP:
  sender: sendto(50B datagram)
  sender: sendto(50B datagram)

  receiver app:
    recvfrom()은 datagram 단위로 받음
    첫 번째 50B와 두 번째 50B의 메시지 경계가 보존됨

8. TCP 3-way handshake: 연결을 만들고 시작 번호를 맞춘다

TCP 연결은 데이터를 보내기 전에 사전 인사를 한다. 이 과정을 3-way handshake라고 부른다. 단순히 "연결 가능?"을 묻는 것뿐 아니라, 양쪽이 sequence number를 시작할 준비를 맞추는 의미도 있다.

1. SYN: 클라이언트가 서버에게 "연결하고 싶다. 내 시작 번호는 이쪽이다"라고 보낸다.
2. SYN + ACK: 서버가 "요청을 받았다. 나도 준비됐다. 내 시작 번호는 이쪽이다"라고 답한다.
3. ACK: 클라이언트가 서버의 응답을 확인한다. 이 뒤부터 데이터가 흐른다.

클라이언트의 connect는 이 연결 수립 과정과 맞물리고, 서버의 accept는 완료된 연결을 꺼내 실제 통신용 fd를 돌려준다.

9. 송신 경로: write 한 줄 뒤에서 일어나는 일

이제 중심 질문으로 돌아오자. 응용 프로그램이 아래처럼 호출했다고 하자.

write(sockfd, buf, 100);

이 호출은 "랜선으로 100바이트를 이미 다 보냈다"는 뜻이 아니다. 보통은 "커널이 이 데이터를 전송할 대상으로 접수했다"는 의미에 가깝다.

1편에서 본 것처럼 데이터는 TCP header, IP header, Ethernet header/FCS를 얻으며 점점 커진다. 이번 편의 관심사는 그 포장 자체보다, 그 포장을 누가 하고 데이터가 user buffer에서 NIC까지 어떤 메모리 경로로 이동하는지다.

송신 파이프라인

user buffer(buf)
  -> copy_from_user
kernel socket send buffer / sk_buff
  -> TCP header 추가
TCP segment
  -> IP header 추가
IP packet
  -> Ethernet header/FCS 추가
Ethernet frame
  -> NIC TX descriptor에 물리 주소 등록
  -> MMIO doorbell
NIC
  -> DMA로 DRAM에서 frame 읽기
  -> 선로에 비트 송신
  -> TX 완료 IRQ

user buffer에서 kernel buffer로 복사하는 이유는 보안과 역할 분리 때문이다. 사용자 프로그램이 커널 메모리나 하드웨어를 직접 만지면 시스템 전체가 위험해진다. 그래서 응용 프로그램은 커널에게 시스템 콜로 부탁하고, 커널이 안전한 버퍼에 데이터를 복사한 뒤 프로토콜 처리를 맡는다.

또한 NIC가 DMA로 데이터를 읽어 가는 동안 사용자가 원래 버퍼를 바꾸거나 해제해도 전송 데이터가 흔들리지 않게 하려면, 커널이 통제하는 버퍼에 데이터를 둬야 한다.

write 리턴과 실제 전송 완료는 다른 사건이다

1. user process
   write(sockfd, buf, 100) 호출

2. kernel
   user buffer -> kernel socket buffer로 복사
   write는 여기서 100을 리턴할 수 있음

3. TCP/IP stack
   segment/packet/frame으로 포장

4. NIC driver
   TX descriptor에 sk_buff의 물리 주소를 기록
   doorbell로 NIC에게 알림

5. NIC
   DMA로 DRAM에서 frame을 읽고 선로로 송신

6. NIC -> CPU
   TX 완료 IRQ
   driver가 sk_buff를 해제하거나 재사용

택배 비유로 말하면 write 리턴은 "택배 접수 완료"에 가깝다. 택배차가 실제로 물류센터를 떠나고, 배송 완료 문자가 오는 시점은 NIC의 DMA와 TX 완료 IRQ 쪽에 더 가깝다.

10. 수신 경로: NIC가 먼저 커널 버퍼에 내려놓는다

수신은 송신의 반대 방향이다. 선로에서 비트가 들어오면 NIC가 frame을 받고, DMA로 메모리에 기록한다. 이때 NIC가 접근하는 것은 응용 프로그램의 가상 주소가 아니라 커널이 미리 준비한 물리 메모리 버퍼다.

수신 파이프라인

Ethernet signal
  -> NIC가 frame 수신
  -> DMA로 DRAM의 RX buffer에 기록
  -> IRQ 또는 polling으로 커널 처리 시작
  -> Ethernet header 확인
  -> IP header 확인
  -> TCP/UDP header 확인
  -> 해당 socket receive queue에 enqueue
  -> read(sockfd, user_buf, n)
  -> copy_to_user
user buffer

즉 "데이터가 물리 주소에 올라간 뒤 커널로 복사된다"가 아니라, NIC가 DMA로 기록하는 그 물리 메모리 자체가 커널이 관리하는 수신 버퍼에 해당한다. 이후 응용 프로그램이 read를 호출하면 커널이 데이터를 user buffer로 복사해 준다.

하드웨어는 가상 주소를 직접 이해하지 않는다. 가상 주소는 CPU와 MMU, 운영체제가 프로세스에게 제공하는 추상화다. NIC는 커널 드라이버가 준비한 DMA descriptor를 보고 물리 메모리 위치를 사용한다.

수신자가 자기 데이터인지 확인하는 계단

1. NIC / Ethernet
   dst MAC이 내 MAC인가?
   EtherType이 IPv4인가?

2. IP layer
   dst IP가 내 IP인가?
   protocol 필드가 6(TCP)인가, 17(UDP)인가?

3. TCP/UDP layer
   dst port에 해당하는 socket이 있는가?

4. Socket receive queue
   해당 소켓의 수신 큐에 payload를 쌓음

5. User process
   read() / recv()로 user buffer에 복사해 감

즉 비트 뭉치가 내 프로그램까지 바로 뛰어드는 것이 아니다. 바깥 포장지부터 MAC, IP, port를 차례대로 검사하며 "이 링크의 나인가?", "이 호스트의 나인가?", "이 프로세스의 나인가?"를 확인한다.

11. sk_buff, TX/RX ring, doorbell, IRQ는 어디까지 알아야 하나

리눅스 내부 구현을 조금 더 보면 sk_buff, TX ring, RX ring, descriptor, doorbell, IRQ 같은 말이 나온다. CSAPP의 핵심을 이해하는 데 이 이름을 모두 외울 필요는 없다. 하지만 흐름을 알면 "CPU가 데이터를 직접 다 나르지 않는다"는 말이 정확해진다.

용어 감각 주의
sk_buff 커널 안에서 packet/frame 데이터를 감싸는 상자와 메타데이터 CSAPP 본문보다 리눅스 구현에 가까운 심화 렌즈다.
TX/RX ring NIC와 드라이버가 주고받는 송수신 대기열 데이터 자체보다 버퍼 위치를 가리키는 descriptor가 중요하다.
doorbell CPU가 NIC에게 "준비됐으니 가져가라"고 알리는 신호 보통 MMIO로 장치 레지스터에 기록한다.
IRQ NIC가 CPU에게 완료나 수신을 알리는 인터럽트 고성능 환경에서는 interrupt coalescing, polling도 얽힌다.

수신 중 sk_buff는 어떻게 변하는가

처음 DMA로 들어온 상태:
  data pointer -> [Ethernet][IP][TCP][Payload]

Ethernet layer 처리 후:
                  data pointer -> [IP][TCP][Payload]

IP layer 처리 후:
                       data pointer -> [TCP][Payload]

TCP layer 처리 후:
                             data pointer -> [Payload]

socket receive queue:
  payload를 해당 socket의 read 대기 데이터로 보관

여기서 "헤더를 벗긴다"는 말을 매번 큰 메모리 복사가 일어난다는 뜻으로 이해하면 안 된다. 실제 구현에서는 sk_buff 안의 포인터와 길이 메타데이터를 조정해서, 지금 어느 계층의 헤더부터 보아야 하는지를 바꾸는 식으로 처리하는 경우가 많다. 택배 상자를 매번 새 상자에 옮겨 담는 것이 아니라, 겉포장지를 무시하고 다음 주소표를 읽는 위치로 손가락을 옮기는 느낌이다.

깊이 제한

발표나 블로그의 핵심은 "CPU가 모든 바이트를 손으로 옮기는 게 아니라, CPU는 지시와 복사를 하고 NIC가 DMA로 DRAM과 직접 데이터 이동을 한다" 정도다. PCIe, MMIO, IRQ의 아주 세부 구현은 A 파트 발표에서 방어용으로만 두는 편이 안전하다.

12. CPU, 메모리, 커널, fd 네 가지 렌즈로 같은 통신 보기

네트워크 I/O를 한 줄로 설명하면 부족하다. 같은 장면을 네 가지 렌즈로 보면 병목과 추상화가 더 잘 보인다.

렌즈 무엇을 보는가 성능에 영향 주는 것
CPU syscall, copy_from_user/to_user, checksum, TCP 상태 갱신, interrupt 처리 시스템 콜 횟수, 복사 비용, 캐시 miss, context switch
메모리 user buffer, kernel socket buffer, sk_buff, DMA buffer DRAM 대역폭, buffer 크기, 할당/해제 비용
커널 socket layer, TCP/UDP, IP, qdisc, driver 네트워크 스택 처리량, 큐잉, backpressure
fd 사용자에게 보이는 정수 핸들 열린 fd 개수, select/epoll 같은 이벤트 처리 방식

결국 네트워크 I/O는 fd로 커널에 일을 시키고, CPU가 복사와 제어를 하며, 메모리에서 버퍼가 흘러가고, 커널의 함수 체인이 NIC까지 밀어주는 과정이다.

13. Echo server: 소켓 fd로 읽고 다시 쓰는 가장 작은 서버

CSAPP의 Echo server는 네트워크 프로그래밍의 Hello World에 가깝다. 클라이언트가 한 줄을 보내면 서버가 그대로 되돌려준다. 중요한 것은 메인 루틴과 실제 상호작용 함수가 분리되어 있다는 점이다.

server main
  listenfd = Open_listenfd(port)
  while (1) {
    connfd = Accept(listenfd, ...)
    echo(connfd)
    Close(connfd)
  }

echo(connfd)
  while (Rio_readlineb(connfd, buf, MAXLINE) != 0) {
    Rio_writen(connfd, buf, strlen(buf))
  }

메인 루틴은 문지기다. 연결을 받고 닫는다. echo 함수는 실제 손님과 대화한다. 이 구조가 Tiny Web Server의 maindoit 구조로 이어진다.

14. EOF, short count, RIO: 네트워크는 디스크 파일보다 까다롭다

소켓도 파일처럼 read/write한다고 해서 디스크 파일과 완전히 같지는 않다. 네트워크에는 상대방, 지연, 버퍼 상태, 연결 종료 같은 변수가 있다.

현상 의미 대응
EOF 상대가 자기 쪽 연결을 닫아 더 이상 보낼 데이터가 없다는 신호 read가 0을 리턴하면 연결 종료로 처리
short read 요청한 바이트보다 적게 읽힘 필요한 만큼 반복해서 읽거나 줄 단위 함수 사용
short write 요청한 바이트보다 적게 커널에 들어감 rio_writen처럼 끝까지 반복
EPIPE/SIGPIPE 이미 끊긴 연결에 쓰려고 함 에러 처리와 signal 처리 필요

짧은 카운트를 숫자로 보면

read(sockfd, buf, 100)을 호출했는데
현재 커널 수신 버퍼에 20B만 있음
  -> read는 20을 리턴할 수 있음

write(sockfd, buf, 100)을 호출했는데
현재 커널 송신 버퍼 여유가 40B뿐임
  -> write는 40을 리턴할 수 있음

상대가 FIN을 보내 연결을 닫음
  -> read는 0을 리턴함

상대가 닫은 뒤 계속 write 시도
  -> EPIPE 또는 SIGPIPE 상황

HTTP request line처럼 줄 단위 텍스트를 읽을 때는 rio_readlineb가 유용하다. response body처럼 정해진 바이트 수를 끝까지 보내야 할 때는 rio_writen이 중요하다. 네트워크 fd는 계속 갱신되는 물줄기라서, 한 번의 read/write가 내가 생각한 논리 단위와 항상 일치한다고 믿으면 위험하다.

이번 글에서 기억할 것

  • 소켓은 하드웨어 구멍이 아니라 커널이 관리하는 논리적 엔드포인트다.
  • fd는 정수 핸들이고, FDT를 통해 커널의 실제 파일/소켓 객체로 이어진다.
  • open_listenfdsocket, bind, listen까지 준비하지만 accept는 반복 루프에서 직접 호출해야 한다.
  • TCP는 sequence/ack로 신뢰성과 순서를 관리하기 때문에 응용에는 byte stream처럼 보인다.
  • write 성공은 보통 선로 전송 완료가 아니라 커널 송신 버퍼 접수에 가깝다.
  • NIC는 DMA로 DRAM의 버퍼를 직접 읽고 쓰며, CPU는 주로 syscall, 복사, 제어, interrupt 처리를 맡는다.

스스로 점검

  1. listenfdconnfd는 왜 분리되는가?
  2. write(sockfd, buf, 100)가 100을 리턴했을 때, 데이터가 반드시 랜선으로 나갔다고 말할 수 없는 이유는 무엇인가?
  3. TCP는 왜 byte stream이고 UDP는 왜 datagram 경계를 보존하는가?
  4. NIC가 DMA로 접근하는 주소는 왜 사용자 가상 주소라고 보기 어려운가?

힌트: 2번은 user buffer와 kernel socket buffer의 경계를 떠올리면 된다. 3번은 TCP의 sequence number가 "상자 번호"라기보다 바이트 흐름의 위치를 관리한다는 점이 핵심이다.

다음 글 예고

다음 글에서는 이 소켓 흐름 위에 HTTP를 올린다. 브라우저가 보낸 GET /home.html HTTP/1.0을 Tiny Web Server가 어떻게 읽고, URI를 정적/동적 콘텐츠로 나누며, CGI에서는 왜 fork, dup2, execve가 등장하는지 코드 기준으로 정리한다.

한 줄 정리

소켓 I/O는 fd라는 리모컨으로 커널의 소켓 객체를 찾아가고, user/kernel buffer 사이를 복사한 뒤, TCP/IP stack과 NIC/DMA가 데이터를 실제 네트워크로 밀어내는 과정이다.