Study · 운영체제 · 연결 받기 · 네트워크

연결은 프로그램이 받기 전에 커널이 이미 다 만들어 둔다

장면

accept() 가 돌려주는 연결은 그 순간 만들어진 것일까, 아니면 이미 만들어져 기다리고 있던 것일까?

$ ss -lnt
State   Recv-Q  Send-Q  Local Address:Port
LISTEN  0       128           0.0.0.0:22
# ss 는 지금 열려 있는 연결을 보여 주는 명령이다
# -lnt 는 「듣고 있는 것만(l) · 이름으로 바꾸지 말고 번호 그대로(n) · TCP 만(t)」이라는 뜻이다
# 0.0.0.0 은 이 기계의 어느 주소로 와도 받는다는 뜻이고, 22 는 ssh 가 쓰는 포트 번호다
# Recv-Q 와 Send-Q 는 LISTEN 줄에서 뜻이 뒤집힌다 — 이름만 보고 읽으면 틀린다. 장면 마지막 스텝에서 푼다

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

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

장면 · 연결은 프로그램이 받기 전에 커널이 이미 다 만들어 둔다 0 / 11

▶ 를 누르면 바깥에서 온 첫 인사가 서버 프로그램의 accept() 에 닿기까지를 따라간다. accept() 가 연결을 만드는 함수처럼 보이지만, 그 순간 커널이 하는 일은 이미 다 만들어 둔 것을 줄에서 꺼내 주는 것뿐이라는 게 이 장면의 요지다.

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

연결은 프로그램이 받기 전에 커널이 이미 다 만들어 둔다. accept 는 연결을 만드는 일이 아니라 쌓여 있는 줄에서 하나를 꺼내 가는 일이다.

어떻게 확인했나

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

  • accept 큐는 struct inet_connection_sock의 icsk_accept_queue 필드다(include/net/inet_connection_sock.h:84). 타입은 struct request_sock_queue(include/net/request_sock.h:177)이고, 그 안의 rskq_accept_head·rskq_accept_tail(185–186)이 외줄을 이룬다. listen이 reqsk_queue_alloc으로 비운다(net/ipv4/inet_connection_sock.c:1343).
  • SYN 큐는 그와 다른 줄이다 — tcp_conn_request가 inet_csk_reqsk_queue_hash_add(net/ipv4/tcp_input.c:7782)로 request_sock을 해시 표에 걸어 둔다(net/ipv4/inet_connection_sock.c:1176 → inet_ehash_insert 1154). accept 큐에 들어가는 것은 악수가 끝난 뒤다.
  • 악수는 accept() 전에 커널이 끝낸다 — 마지막 ACK을 tcp_check_req(net/ipv4/tcp_minisocks.c:687)가 확인해 syn_recv_sock(934)으로 새 struct sock을 만들고, inet_csk_reqsk_queue_add(net/ipv4/inet_connection_sock.c:1393)가 큐 꼬리에 붙인다. inet_csk_accept(649)는 reqsk_queue_remove로 꺼내기만 한다.
  • ss -lnt의 두 칸은 이 줄의 값이다 — tcp_diag_get_info(net/ipv4/tcp_diag.c:26)가 LISTEN일 때 Recv-Q에 sk_ack_backlog(지금 쌓인 개수), Send-Q에 sk_max_ack_backlog(상한)를 넣는다. 상한은 __inet_listen_sk(net/ipv4/af_inet.c:206)가 적고, __sys_listen_socket(net/socket.c:1993)이 그 전에 somaxconn으로 깎는다.
  • 넘치면 SYN을 대답 없이 버린다 — sk_acceptq_is_full(include/net/sock.h:1102)이 참이면 tcp_conn_request가 LISTENOVERFLOWS를 세고 goto drop한다(net/ipv4/tcp_input.c:7657).
  • 백로그 3 으로 듣고 다섯 개를 붙였다. ss -lnt 의 Send-Q 는 줄곧 3(상한)이고 Recv-Q 가 0 에서 4 로 늘었으며, 다섯 번째 연결은 Connection timed out 으로 실패했다. 상한이 3 인데 4 까지 쌓이는 것은 sk_acceptq_is_full 의 비교가 「크거나 같다」가 아니라 「크다」라서다 — 상한과 같은 수까지는 받는다. accept 를 한 번 하니 Recv-Q 가 3 으로 줄었다 — 큐에서 하나를 꺼내 간 것이다.
‹ 이전 다음 항목 ›