QUESTION
When EPOLLOUT arrives, has TCP connect succeeded?
When connect on a nonblocking socket returns EINPROGRESS, the handshake is in progress. On completion, the socket can report writable/error events, but both success and failure are wakeup causes, so read SO_ERROR to determine the final result.
A connect timeout is a policy choice between waiting for all kernel TCP retransmissions and stopping at an application deadline. Preserve a monotonic deadline instead of resetting a relative epoll_wait timeout after every EINTR.
STRUCTURE
Structure diagram
POLLOUT is not a success decision. Read SO_ERROR in each state to finalize the connection object's result.
CALL PATH
Call path
Treat a writable event as a request to recheck state, not as the result. Mark connection state established or failed only after reading SO_ERROR.
This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.
SOURCE COORDINATES
Linux 6.18.37 LTS source locations
Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.
| File | Function / structure | What to inspect |
|---|---|---|
| net/ipv4/tcp_ipv4.c | tcp_v4_connect() | Begin route lookup, local-port selection, SYN, and TCP state |
| net/core/sock.c | sock_getsockopt() | Semantics that read and clear SO_ERROR |
| net/socket.c | sock_poll() | Convert socket state to a poll readiness mask |
COMPLETE PROGRAM
Complete runnable example
The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.
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
Code notes
SOCK_NONBLOCK | SOCK_CLOEXECCreates an fd with nonblocking and CLOEXEC already set at connect time, avoiding an fcntl race with other threads.
errno != EINPROGRESSOnly the normal in-progress result moves to event waiting. Handling EALREADY/EISCONN also belongs in a state machine that may call connect again.
.events = POLLOUTWhen connect completes, the socket becomes writable/error-ready because sending is possible or an error is pending.
SO_ERRORReads and clears the pending error stored on the socket. 0 means connect succeeded; a positive errno means failure.
errno = errorSO_ERROR is an output integer, not errno from the getsockopt return; copy it to errno before passing it to perror.
DETAILS
Detailed behavior
Trying several addresses only in sequence is slow
If IPv6 from getaddrinfo times out because of a path problem before IPv4 is attempted, user-visible delay grows. Happy Eyeballs-family algorithms stagger parallel connects across address families and select the first success.
Maintain each candidate socket and timer as independent state, and close loser fds.
Manage deadlines and fd readiness together
The poll 3000ms example does not compute remaining time after signal EINTR. A production loop establishes an absolute CLOCK_MONOTONIC deadline and handles an epoll timerfd with connection events.
At timeout, close the socket to cancel the handshake and increment the connection-object generation.
Protocol setup remains after connect succeeds
TCP established does not mean a TLS handshake or application greeting is complete. Split the state machine into CONNECTING, TLS_HANDSHAKE, READY, and similar states, updating read/write interest at each stage.
EPOLLOUT can remain level-ready while the send buffer has capacity, so remove the interest when there is no data to send.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
connecting socket | Created by socket/connect and retained until close after success/failure/timeout | peer, deadline, state |
sk_err | Set by an asynchronous network error and consumed by reading SO_ERROR | errno value, clear semantics |
event registration | Added after EINPROGRESS and changed after the completion decision | POLLOUT/ERR, generation |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| send fails despite EPOLLOUT | The connect error was not checked through SO_ERROR | Inspect the getsockopt result |
| The timeout grows longer | A relative timeout is reused after every EINTR | monotonic deadline |
| An IPv6 problem delays the entire connection | Addresses fall back serially | Inspect the connect timeline for each candidate |
LAB
Verify it yourself
- Connect once to a port with a listener and once to a closed port, and compare SO_ERROR 0 with ECONNREFUSED.
- Apply a 1-second application deadline to an address dropped by a firewall and cancel before the kernel's default retransmission completes.
- Implement a small Happy Eyeballs connector that manages several getaddrinfo results nonblockingly at the same time.
./nb_connect 127.0.0.1 8080strace -e trace=socket,connect,poll,getsockopt,close ./nb_connect 127.0.0.1 8080PRIMARY REFERENCES