← System Programming DUJINLABS.COM

Socket · Linux userspace / kernel ABI

nonblocking connect와 SO_ERROR

EINPROGRESS 이후 writability가 연결 성공만을 뜻하지 않는 이유와 getsockopt(SO_ERROR)로 완료 상태를 소비하는 절차를 다룹니다.

Series
31 / 37
Build
cc -std=c17 -Wall -Wextra -O2 nb_connect.c -o nb_connect
Run
./nb_connect 127.0.0.1 8080
Kernel
Linux 6.18.37 LTS

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을 유지한다.

구조 그림

그림 1. nonblocking connect 상태와 확인해야 할 값
관찰socket stateSO_ERROR다음 처리 connect() = 0ESTABLISHED0바로 protocol 단계errno=EINPROGRESSSYN-SENT아직 미정POLLOUT/ERR 대기POLLOUT완료 또는 실패반드시 읽기0이면 성공POLLERR/HUPpending errorECONNREFUSED 등fd close/retrydeadline 만료아직 진행 중상관없음close로 취소

POLLOUT은 성공 판정이 아니다. 각 상태에서 SO_ERROR를 읽어 connection object의 최종 상태를 확정한다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
socket nonblock CLOEXEC 포함
connect 0 또는 EINPROGRESS
epoll POLLOUT 완료 가능성
SO_ERROR 0/ECONNREFUSED 등
connected I/O interest 변경

writable event를 결과가 아니라 상태를 다시 검사하라는 신호로 본다. SO_ERROR를 읽은 뒤에만 connection state를 established/failed로 확정한다.

그림 3. 커널 내부에서 지나가는 주요 지점
tcp_v4_connect SYN 전송/state
sk_sleep poll wait queue
handshake/error sk_state/sk_err
sock_poll writable/error
SO_ERROR error read-and-clear

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

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로 변환

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 nb_connect.c -o nb_connect
01#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}

코드 조각별 설명

실제 코드 14행SOCK_NONBLOCK | SOCK_CLOEXEC

connect와 동시에 nonblocking/CLOEXEC 상태를 갖는 fd를 만들어 다른 thread와의 fcntl race를 피한다.

실제 코드 23행errno != EINPROGRESS

즉시 완료되지 않았지만 정상적으로 진행 중인 경우만 event wait 단계로 넘긴다. EALREADY/EISCONN 처리도 재호출 state machine에 포함한다.

실제 코드 26행.events = POLLOUT

connect 완료 시 send 가능 또는 pending error 때문에 writable/error 상태가 된다.

실제 코드 31행SO_ERROR

socket에 저장된 pending error를 읽고 지운다. 0이면 connect 성공, 양수 errno이면 실패다.

실제 코드 32행errno = error

SO_ERROR는 getsockopt 반환 errno가 아니라 output integer이므로 perror에 전달하려면 errno에 복사한다.

세부 동작

01

여러 address를 순차로만 시도하면 느리다

getaddrinfo 결과의 IPv6가 경로 문제로 timeout된 뒤 IPv4를 시도하면 사용자 지연이 커진다. Happy Eyeballs 계열은 address family를 staggered parallel connect하고 첫 성공을 선택한다.

각 candidate socket과 timer를 독립 state로 관리하고 loser fd를 닫는다.

02

deadline과 fd readiness를 함께 관리한다

poll 3000ms 예제는 signal EINTR 시 남은 시간을 계산하지 않는다. 실제 loop는 CLOCK_MONOTONIC absolute deadline을 만들어 epoll timerfd와 connection event를 함께 처리한다.

timeout 시 close로 handshake를 취소하고 connection object generation을 갱신한다.

03

connect 성공 뒤에도 protocol 준비가 남는다

TCP established는 TLS handshake나 application greeting 완료가 아니다. state machine을 CONNECTING, TLS_HANDSHAKE, READY처럼 분리하고 각 단계의 read/write interest를 갱신한다.

EPOLLOUT은 send buffer 여유가 있는 동안 계속 level-ready일 수 있으므로 보낼 data가 없으면 관심을 제거한다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
connecting socketsocket/connect에서 생기고 성공/실패/timeout close까지 유지된다peer, deadline, state
sk_err비동기 network 오류에서 설정되고 SO_ERROR read가 소비한다errno value, clear semantics
event registrationEINPROGRESS 뒤 추가되고 completion 판정 뒤 관심 mask가 바뀐다POLLOUT/ERR, generation

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
EPOLLOUT인데 send 실패connect error를 SO_ERROR로 미확인getsockopt 결과
timeout이 길어짐EINTR마다 상대 timeout 재사용monotonic deadline
IPv6 문제로 전체 접속 지연address 직렬 fallbackcandidate별 connect timeline

직접 확인

  1. listener가 있는 port와 닫힌 port에 각각 연결해 SO_ERROR 0과 ECONNREFUSED를 비교한다.
  2. 방화벽로 drop되는 address에 application 1초 deadline을 적용해 kernel 기본 retransmission보다 먼저 취소한다.
  3. getaddrinfo 결과 여러 개를 nonblocking으로 동시에 관리하는 작은 Happy Eyeballs connector를 구현한다.
실행./nb_connect 127.0.0.1 8080
추적strace -e trace=socket,connect,poll,getsockopt,close ./nb_connect 127.0.0.1 8080

원문