Socket · Linux userspace / kernel ABI

The socket, bind, listen, and accept lifecycle

Treats listening and accepted sockets as different kernel objects, and distinguishes the bound address, two queues, close, and half-close.

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

What state does an fd returned by accept share with the listening fd?

A listening fd owns a local endpoint and pending-connection queues, and exists only to receive new connections. accept creates and returns a new struct socket/file/fd for one connection; subsequent byte-stream state belongs to that accepted fd.

Closing the listener does not end already accepted connections. Conversely, closing an accepted fd does not affect the listener or other connections. Separating fd ownership by kind simplifies shutdown.

Structure diagram

Figure 1. Two queues owned by a listening socket and an accepted socket
SYN request queue
10.0.0.2 SYN-RECV10.0.0.3 SYN-RECVfree slot
accept queue
conn A ESTABLISHEDconn B ESTABLISHEDbacklog slot
listener
local :8080TCP_LISTENaccept wait queue
accepted fd table
fd 6 → conn Afd 7 → conn Blistener fd 3 remains separate

A request with a handshake in progress and a connection awaiting accept after handshake completion occupy different queues. accept creates a separate connected-socket fd.

Call path

Figure 2. From userspace code to observable results
socket unbound endpoint
bind local address
listen passive + backlog
accept4 create connected fd
shutdown/close close each connection

Draw the queue for connections processing SYN, the queue for completed connections awaiting accept, the listening socket, and connected sockets in separate boxes.

Figure 3. Major points along the kernel-internal path
__sys_socket socket/file/fd
inet_bind port hash
inet_listen TCP_LISTEN
inet_csk_accept accept queue
sock_alloc_file new fd install

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.

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.

FileFunction / structureWhat to inspect
net/socket.c __sys_socket(), do_accept() Expose a socket object as a file/fd and install the accepted fd
net/ipv4/af_inet.c inet_bind(), inet_listen() Set an IPv4 local address and passive state
net/ipv4/inet_connection_sock.c inet_csk_accept() Detach a completed child socket from the accept queue

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.

Buildcc -std=c17 -Wall -Wextra -O2 tcp_server.c -o tcp_server
01#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

Source line 13SOCK_STREAM | SOCK_CLOEXEC

Prevents exec inheritance when the stream socket file is created. A nonblocking server should also apply SOCK_NONBLOCK atomically during creation/accept.

Source line 15SO_REUSEADDR

Sets the local-address reuse policy before bind. This differs in purpose from SO_REUSEPORT; distribution across several listeners is separate.

Source line 19htonl(INADDR_LOOPBACK)

Limits example exposure by binding only to loopback rather than an external interface.

Source line 22listen(listener, 128)

Enters passive state and supplies a hint for the completed-connection queue limit. The effective limit also depends on kernel settings and protocol state.

Source line 25accept4(listener

Removes one connection from the listener and creates a separate fd. Do not assume listener file status flags are inherited unchanged by the new fd.

Detailed behavior

01

One backlog value cannot describe the entire queueing system

TCP distinguishes a request queue for handshakes in progress from a queue of completed handshakes awaiting accept. Syncookies, tcp_max_syn_backlog, somaxconn, and application accept rate create different bottlenecks.

Inspect current queues with ss -lnt and kernel counters.

02

accept also has retryable failures

A nonblocking listener returns EAGAIN when its queue is empty. A signal can produce EINTR, and a pending network error can appear on the new socket. An edge-triggered listener repeats accept until EAGAIN.

Under EMFILE the listener can remain readable and form a busy loop, so define a recovery strategy such as a reserve fd.

03

close and shutdown differ

shutdown(SHUT_WR) sends FIN and creates a half-close: no more writes, while peer data can still be read. close drops an fd reference, and protocol termination proceeds according to the last-socket-reference policy.

SO_LINGER can change close blocking and RST semantics, so do not set it without understanding the default.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
listening socketPrepared by socket-bind-listen; the passive endpoint ends when the listener closeslocal addr, state, accept queue
accepted socketCreated by accept and passes through connection close/TIME-WAIT protocol4-tuple, send/receive queue
socket fdCreated by sock_alloc_file/fd_install and drops its file reference on closeCLOEXEC, NONBLOCK, owner

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
Connections arrive but accept returns noneAn fd limit or stalled event loopss queue, EMFILE log, loop latency
A restart bind returns EADDRINUSEAnother listener or the address policyss -lptn, reuse option, namespace
The client's final data is lostReceive draining was omitted on HUP/closeProcess buffered data before read returns 0

Verify it yourself

  1. Connect with nc 127.0.0.1 8080 and inspect echo plus half-close behavior.
  2. Connect several clients before the listener accepts and observe changes in Recv-Q from ss -lnt.
  3. Add SOCK_NONBLOCK to accept4 and implement an epoll edge-triggered accept loop.
Run./tcp_server 8080
Tracestrace -e trace=socket,setsockopt,bind,listen,accept4,read,write,shutdown,close ./tcp_server 8080

Primary sources