QUESTION
accept가 반환한 fd는 listening fd와 어떤 상태를 공유하는가?
listen fd는 local endpoint와 pending connection queue를 소유하고 새 연결을 받는 역할만 한다. accept는 연결 하나의 새 struct socket/file/fd를 만들어 반환하며 이후 byte stream 상태는 accepted fd에 있다.
listener를 close해도 이미 accepted된 connection은 유지된다. 반대로 accepted fd를 close해도 listener와 다른 connection에는 영향을 주지 않는다. fd ownership을 종류별로 나눠야 shutdown이 단순해진다.
STRUCTURE
구조 그림
handshake 진행 중 request와 handshake 완료 후 accept 대기 connection은 다른 queue에 있다. accept는 별도 connected socket fd를 만든다.
CALL PATH
호출 흐름
SYN 처리 queue와 accept 완료 queue, listening socket과 connected socket을 각각 다른 칸으로 그린다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| net/socket.c | __sys_socket(), do_accept() | socket 객체를 file/fd로 노출하고 accepted fd 설치 |
| net/ipv4/af_inet.c | inet_bind(), inet_listen() | IPv4 local address와 passive state 설정 |
| net/ipv4/inet_connection_sock.c | inet_csk_accept() | accept queue에서 completed child socket 분리 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 tcp_server.c -o tcp_server01#define _GNU_SOURCE
02#include <arpa/inet.h>
03#include <netinet/in.h>
04#include <stdio.h>
05#include <stdlib.h>
06#include <sys/socket.h>
07#include <unistd.h>
08
09int main(int argc, char **argv)
10{
11 if (argc != 2)
12 return 2;
13 int listener = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
14 int one = 1;
15 setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
16 struct sockaddr_in address = {
17 .sin_family = AF_INET,
18 .sin_port = htons((unsigned short)strtoul(argv[1], NULL, 10)),
19 .sin_addr = { .s_addr = htonl(INADDR_LOOPBACK) }
20 };
21 if (listener < 0 || bind(listener, (struct sockaddr *)&address, sizeof(address)) < 0 ||
22 listen(listener, 128) < 0)
23 return 1;
24
25 int client = accept4(listener, NULL, NULL, SOCK_CLOEXEC);
26 if (client < 0)
27 return 1;
28 char buffer[1024];
29 ssize_t n = read(client, buffer, sizeof(buffer));
30 if (n > 0)
31 write(client, buffer, (size_t)n);
32 shutdown(client, SHUT_WR);
33 close(client);
34 close(listener);
35 return n < 0;
36}
CODE NOTES
코드 조각별 설명
SOCK_STREAM | SOCK_CLOEXECstream socket file을 만들 때 exec 상속을 막는다. nonblocking server라면 SOCK_NONBLOCK도 생성/accept 단계에서 원자적으로 적용한다.
SO_REUSEADDRbind 전에 local address reuse 정책을 설정한다. SO_REUSEPORT와 목적이 다르며 여러 listener 분산은 별도다.
htonl(INADDR_LOOPBACK)외부 interface가 아니라 loopback에만 bind해 예제 노출 범위를 제한한다.
listen(listener, 128)passive state로 바꾸고 completed connection queue 상한의 힌트를 준다. 실제 상한은 kernel 설정과 protocol 상태에 영향을 받는다.
accept4(listenerlistener에서 연결 하나를 꺼내 별도 fd를 만든다. listener file status flag가 새 fd에 그대로 상속된다고 가정하지 않는다.
DETAILS
세부 동작
backlog 하나로 queue 전체를 설명할 수 없다
TCP는 handshake 진행 중 request queue와 handshake가 끝나 accept를 기다리는 queue를 구분한다. syncookies, tcp_max_syn_backlog, somaxconn, application accept 속도가 서로 다른 병목을 만든다.
ss -lnt와 kernel counter로 현재 queue를 확인한다.
accept도 반복 가능한 실패가 있다
nonblocking listener는 queue가 비면 EAGAIN을 반환한다. signal이면 EINTR, network pending error가 새 socket에 나타날 수 있다. edge-triggered listener는 EAGAIN까지 accept loop를 돌린다.
EMFILE 상황에서는 listener가 readable 상태로 계속 남아 busy loop가 될 수 있어 reserve fd 같은 복구 전략을 둔다.
close와 shutdown은 다르다
shutdown(SHUT_WR)은 FIN을 보내 더 이상 쓰지 않되 peer data를 계속 읽는 half-close를 만든다. close는 fd 참조를 놓고 마지막 socket reference 정책에 따라 protocol 종료가 진행된다.
SO_LINGER는 close blocking/RST 의미를 바꿀 수 있으므로 default를 모른 채 설정하지 않는다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
listening socket | socket-bind-listen에서 준비되고 listener close에서 passive endpoint가 끝난다 | local addr, state, accept queue |
accepted socket | accept에서 생기고 connection close/timewait protocol을 거친다 | 4-tuple, send/receive queue |
socket fd | sock_alloc_file/fd_install에서 생기고 close에서 file ref를 놓는다 | CLOEXEC, NONBLOCK, owner |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| connection은 오는데 accept 없음 | fd limit 또는 event loop stall | ss queue, EMFILE log, loop latency |
| 재시작 bind EADDRINUSE | 다른 listener 또는 address policy | ss -lptn, reuse option, namespace |
| client 마지막 data 손실 | HUP/close에서 receive drain 누락 | read 0 전 buffer 처리 |
LAB
직접 확인
- nc 127.0.0.1 8080으로 연결해 echo와 half-close를 확인한다.
- 여러 client를 listener가 accept하기 전에 연결해 ss -lnt의 Recv-Q 변화를 본다.
- accept4에 SOCK_NONBLOCK을 추가하고 epoll edge-triggered accept loop를 구현한다.
./tcp_server 8080strace -e trace=socket,setsockopt,bind,listen,accept4,read,write,shutdown,close ./tcp_server 8080PRIMARY REFERENCES