Study · 운영체제 · 주고받기 · 네트워크

패킷 하나가 계층을 오르내리는 동안 같은 것이 계속 전달된다

장면

랜선에 도착한 바이트가 프로그램의 read 까지 오는 동안 몇 개의 층을 지날까?

$ ss -nt | grep 9098      # 보낸 쪽은 다 보냈는데 받는 쪽이 아직 안 읽는 중
State  Recv-Q  Send-Q  Local Address:Port    Peer Address:Port
ESTAB  127663  0       127.0.0.1:9098        127.0.0.1:40908
ESTAB  0       72337   127.0.0.1:40908       127.0.0.1:9098
# 같은 연결이 두 줄로 나온다 — 위가 받는 쪽, 아래가 보내는 쪽이다
# 127663 은 커널이 받아 뒀는데 프로그램이 아직 안 읽은 바이트 수다
# 72337 은 보내는 쪽 커널이 아직 상대에게 못 보낸 바이트 수다
# 40908 은 그때 붙은 임시 포트라 다시 재면 달라진다

아래 재생기의 ▶를 누르면 한 스텝씩 진행된다. 금색 테두리가 지금 이야기하는 곳이고, 캡션이 무슨 일이 일어났는지 말해 준다. 굵게 밑줄 친 말은 누르면 뜻이 나온다. 빨간 딱지는 그때 벌어지는 일의 이름이고, 캡션 바로 위 회색 글자는 지금 스텝이 속한 묶음의 이름이다.

카드 아래 회색 줄은 그 일이 실제로 적혀 있는 소스의 파일 이름 : 줄 번호와 함수 이름이다. 글 안에 (:669)처럼 줄 번호만 적힌 것은 바로 앞에 적은 파일의 그 줄이라는 뜻이다. 캡션 한가운데 박힌 긴 영어 이름과 괄호 속 파일 · 줄 번호도 같다 — 건너뛰고 한글만 읽어도 뜻이 통하게 써 두었다.

장면 · 패킷 하나가 계층을 오르내리는 동안 같은 것이 계속 전달된다 0 / 11

▶ 를 누르면 랜선에 도착한 바이트가 프로그램의 read() 까지 오는 동안 지나는 계층을 따라간다. 받은 바이트는 「누가 어디로 보냈는지 적은 앞부분(머리말) + 진짜 내용(알맹이)」으로 되어 있고, 계층이란 그 머리말을 하나씩 벗기며 위로 올리는 단계들이다 — 랜 카드 · IP · TCP · 소켓, 이렇게 넷을 지난다. 층마다 머리말을 하나씩 떼는데, 「뗀다」가 실제로 무슨 뜻인지 — 옮겨 적는 것인지 표시만 옮기는 것인지 — 가 이 장면의 요지다.

키보드 · ← → 한 스텝 · Space 재생/멈춤 · Home 처음

머리말은 아래 세 층에서 하나씩 떼지만 알맹이를 옮겨 담지는 않는다. 층을 오가는 동안 전달되는 것은 sk_buff 하나다.

어떻게 확인했나

소스 어디를 보고 적었는지와 무엇을 돌려서 쟀는지를 남긴 자리다. 처음 읽을 때는 건너뛰어도 된다.

  • sk_buff는 복사되지 않는다 — 층 사이로 넘기는 것은 skb 포인터 하나다(ip_protocol_deliver_rcu가 net/ipv4/ip_input.c:207에서 tcp_v4_rcv에 넘긴다). 머리말을 떼는 __skb_pull(include/linux/skbuff.h:2836)의 본문은 skb->len -= len;과 return skb->data += len;뿐이라 memcpy가 없다.
  • 머리말을 떼는 자리는 세 곳이고 전부 같은 방식이다 — 이더넷은 eth_skb_pull_mac(include/linux/etherdevice.h:644), IP는 ip_local_deliver_finish(net/ipv4/ip_input.c:238), TCP는 tcp_rcv_established(net/ipv4/tcp_input.c:6634). 버퍼 자체는 head~end 그대로 있고 data 표시만 움직인다(include/linux/skbuff.h:1092–1095).
  • 랜 카드에서 소켓 버퍼까지의 차례 — netif_receive_skb(net/core/dev.c:6516) → ip_packet_type.func = ip_rcv(net/ipv4/af_inet.c:1876) → ip_rcv(net/ipv4/ip_input.c:603) → ip_local_deliver(250) → tcp_v4_rcv(net/ipv4/tcp_ipv4.c:2070) → __inet_lookup_skb(2114)로 소켓을 찾고 → tcp_v4_do_rcv(2248) → tcp_queue_rcv(net/ipv4/tcp_input.c:5532) → tcp_add_receive_queue(include/net/tcp.h:802)의 __skb_queue_tail(&sk->sk_receive_queue, skb).
  • 바이트를 진짜로 옮겨 적는 곳은 한 군데다 — 사용자 공간으로 넘길 때 tcp_recvmsg가 부르는 skb_copy_datagram_msg(net/ipv4/tcp.c:2840)다.
  • 소켓이 잠겨 있으면 곧장 큐에 못 들어간다 — sock_owned_by_user이면 tcp_add_backlog(net/ipv4/tcp_ipv4.c:1901)로 소켓마다 따로 있는 임시 대기 줄(이름은 같은 백로그지만 16번의 그 상한과는 다른 것이다)에 잠시 들렀다 간다(net/ipv4/tcp_ipv4.c:2250).
  • 받는 쪽이 안 읽는 동안 200,000 바이트를 보냈더니 ss -nt 가 받는 쪽 Recv-Q 127,663 · 보내는 쪽 Send-Q 72,337 로 갈라 보여 줬다. 여기 Recv-Q 는 바이트 수다 — 16번의 LISTEN 줄에서 같은 칸이 기다리는 연결 개수였던 것과 다르다. 둘을 더하면 정확히 200,000 이다 — 같은 바이트를 커널이 양쪽에 나눠 들고 있다.
‹ 이전 목차로 ›