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)처럼 줄 번호만 적힌 것은 바로 앞에 적은 파일의 그 줄이라는 뜻이다. 캡션 한가운데 박힌 긴 영어 이름과 괄호 속 파일 · 줄 번호도 같다 — 건너뛰고 한글만 읽어도 뜻이 통하게 써 두었다.
▶ 를 누르면 랜선에 도착한 바이트가 프로그램의 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 이다 — 같은 바이트를 커널이 양쪽에 나눠 들고 있다.
항목 지도목차 ›
- 계정과 권한
- 1 로그인 — 로그인하면 무슨 일이 일어나나
- 2 계정 만들기 — 계정을 만들면 어디에 무엇이 쓰이나
- 3 권한 주기 — chmod 가 바꾸는 것은 어디에 있나
- 4 권한 검사 — 파일을 열 때 권한을 언제 보나
- 파일과 디렉터리
- 5 파일 만들기 — 파일을 만들면 무엇이 생기나
- 6 디렉터리 — 디렉터리를 만들면 무엇이 생기나
- 7 파일 읽기 — 파일을 읽으면 어디를 거치나
- 8 파일 지우기 — 파일을 지우면 정말 사라지나
- 프로세스
- 9 명령 실행 — 명령 하나를 치면 무슨 일이 벌어지나
- 10 파일 디스크립터 — 프로세스가 파일을 열면 무엇이 늘어나나
- 11 CPU 나누기 — CPU 하나를 여럿이 어떻게 나눠 쓰나
- 메모리
- 12 메모리 요청 — 메모리를 달라고 하면 바로 주나
- 13 페이지 폴트 — 처음 건드릴 때 무슨 일이 일어나나
- 14 OOM — 메모리가 모자라면 누가 죽나
- 네트워크
- 15 소켓 만들기 — 소켓을 만들면 무엇을 받나
- 16 연결 받기 — 연결이 들어오면 누가 먼저 받나
- 17 주고받기 — 데이터가 오갈 때 무엇이 움직이나