QUESTION
EPOLLOUT이 오면 TCP connect는 성공한 것인가?
nonblocking socket의 connect가 EINPROGRESS를 반환하면 handshake가 진행 중이다. 완료되면 socket은 writable/error event를 낼 수 있지만 성공과 실패 모두 wakeup 원인이므로 SO_ERROR를 읽어 최종 결과를 확인한다.
connect timeout은 kernel TCP retransmission 전체를 기다릴지 application deadline에서 중단할지 정책 문제다. epoll_wait 상대 timeout을 EINTR마다 초기화하지 않고 monotonic deadline을 유지한다.
STRUCTURE
구조 그림
POLLOUT은 성공 판정이 아니다. 각 상태에서 SO_ERROR를 읽어 connection object의 최종 상태를 확정한다.
CALL PATH
호출 흐름
writable event를 결과가 아니라 상태를 다시 검사하라는 신호로 본다. SO_ERROR를 읽은 뒤에만 connection state를 established/failed로 확정한다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| net/ipv4/tcp_ipv4.c | tcp_v4_connect() | route, local port, SYN, TCP state 시작 |
| net/core/sock.c | sock_getsockopt() | SO_ERROR 값을 읽고 clear하는 semantics |
| net/socket.c | sock_poll() | socket state를 poll readiness mask로 변환 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 nb_connect.c -o nb_connect01#define _GNU_SOURCE
02#include <arpa/inet.h>
03#include <errno.h>
04#include <poll.h>
05#include <stdio.h>
06#include <stdlib.h>
07#include <sys/socket.h>
08#include <unistd.h>
09
10int main(int argc, char **argv)
11{
12 if (argc != 3)
13 return 2;
14 int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
15 struct sockaddr_in peer = {
16 .sin_family = AF_INET,
17 .sin_port = htons((unsigned short)strtoul(argv[2], NULL, 10))
18 };
19 if (fd < 0 || inet_pton(AF_INET, argv[1], &peer.sin_addr) != 1)
20 return 1;
21
22 int rc = connect(fd, (struct sockaddr *)&peer, sizeof(peer));
23 if (rc < 0 && errno != EINPROGRESS)
24 return 1;
25 if (rc < 0) {
26 struct pollfd pfd = { .fd = fd, .events = POLLOUT };
27 if (poll(&pfd, 1, 3000) != 1)
28 return 1;
29 int error = 0;
30 socklen_t length = sizeof(error);
31 if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) < 0 || error != 0) {
32 errno = error;
33 perror("connect completion");
34 return 1;
35 }
36 }
37 puts("connected");
38 close(fd);
39 return 0;
40}
CODE NOTES
코드 조각별 설명
SOCK_NONBLOCK | SOCK_CLOEXECconnect와 동시에 nonblocking/CLOEXEC 상태를 갖는 fd를 만들어 다른 thread와의 fcntl race를 피한다.
errno != EINPROGRESS즉시 완료되지 않았지만 정상적으로 진행 중인 경우만 event wait 단계로 넘긴다. EALREADY/EISCONN 처리도 재호출 state machine에 포함한다.
.events = POLLOUTconnect 완료 시 send 가능 또는 pending error 때문에 writable/error 상태가 된다.
SO_ERRORsocket에 저장된 pending error를 읽고 지운다. 0이면 connect 성공, 양수 errno이면 실패다.
errno = errorSO_ERROR는 getsockopt 반환 errno가 아니라 output integer이므로 perror에 전달하려면 errno에 복사한다.
DETAILS
세부 동작
여러 address를 순차로만 시도하면 느리다
getaddrinfo 결과의 IPv6가 경로 문제로 timeout된 뒤 IPv4를 시도하면 사용자 지연이 커진다. Happy Eyeballs 계열은 address family를 staggered parallel connect하고 첫 성공을 선택한다.
각 candidate socket과 timer를 독립 state로 관리하고 loser fd를 닫는다.
deadline과 fd readiness를 함께 관리한다
poll 3000ms 예제는 signal EINTR 시 남은 시간을 계산하지 않는다. 실제 loop는 CLOCK_MONOTONIC absolute deadline을 만들어 epoll timerfd와 connection event를 함께 처리한다.
timeout 시 close로 handshake를 취소하고 connection object generation을 갱신한다.
connect 성공 뒤에도 protocol 준비가 남는다
TCP established는 TLS handshake나 application greeting 완료가 아니다. state machine을 CONNECTING, TLS_HANDSHAKE, READY처럼 분리하고 각 단계의 read/write interest를 갱신한다.
EPOLLOUT은 send buffer 여유가 있는 동안 계속 level-ready일 수 있으므로 보낼 data가 없으면 관심을 제거한다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
connecting socket | socket/connect에서 생기고 성공/실패/timeout close까지 유지된다 | peer, deadline, state |
sk_err | 비동기 network 오류에서 설정되고 SO_ERROR read가 소비한다 | errno value, clear semantics |
event registration | EINPROGRESS 뒤 추가되고 completion 판정 뒤 관심 mask가 바뀐다 | POLLOUT/ERR, generation |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| EPOLLOUT인데 send 실패 | connect error를 SO_ERROR로 미확인 | getsockopt 결과 |
| timeout이 길어짐 | EINTR마다 상대 timeout 재사용 | monotonic deadline |
| IPv6 문제로 전체 접속 지연 | address 직렬 fallback | candidate별 connect timeline |
LAB
직접 확인
- listener가 있는 port와 닫힌 port에 각각 연결해 SO_ERROR 0과 ECONNREFUSED를 비교한다.
- 방화벽로 drop되는 address에 application 1초 deadline을 적용해 kernel 기본 retransmission보다 먼저 취소한다.
- getaddrinfo 결과 여러 개를 nonblocking으로 동시에 관리하는 작은 Happy Eyeballs connector를 구현한다.
./nb_connect 127.0.0.1 8080strace -e trace=socket,connect,poll,getsockopt,close ./nb_connect 127.0.0.1 8080PRIMARY REFERENCES