socketpair(): A Pre-Connected Pair of Sockets
socketpair() creates two already-connected UNIX domain sockets in a single call. Think of it as a bidirectional pipe. Unlike pipe() which is unidirectional, both ends of a socket pair can send and receive.
The typical use case is in a parent-child relationship: the parent calls socketpair(), then fork()s. The child inherits both file descriptors, the parent closes one and the child closes the other, giving each a private channel to communicate through.
Key Terms
#include <sys/socket.h>
/* Creates a pair of connected sockets */
int socketpair(int domain, /* Must be AF_UNIX */
int type, /* SOCK_STREAM or SOCK_DGRAM */
int protocol, /* Must be 0 */
int sv[2]); /* Output: two file descriptors */
/* Returns 0 on success, -1 on error */
/* sv[0] and sv[1] are connected to each other */
/* Writing to sv[0] can be read from sv[1], and vice versa */
Important rules:
domainmust beAF_UNIXβ socketpair works only for local socketstypecan beSOCK_STREAMorSOCK_DGRAM- The two sockets are indistinguishable β either end can send or receive
- No binding is needed β these sockets have no address in the file system
- Commonly used with
fork()for parent-child communication
Process A end
Process B end
write(sv[0], “hello”) β read(sv[1]) returns “hello”
write(sv[1], “world”) β read(sv[0]) returns “world”
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/wait.h>
int main(void)
{
int sv[2];
pid_t pid;
char buf[128];
ssize_t n;
/* Create the socket pair BEFORE forking */
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
perror("socketpair");
exit(EXIT_FAILURE);
}
/* sv[0] and sv[1] are now connected to each other */
printf("sv[0]=%d, sv[1]=%d\n", sv[0], sv[1]);
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
/* CHILD: use sv[1], close sv[0] */
close(sv[0]);
/* Read message from parent */
n = read(sv[1], buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Child received: %s\n", buf);
}
/* Send reply */
const char *reply = "Hello from child!";
write(sv[1], reply, strlen(reply));
close(sv[1]);
exit(EXIT_SUCCESS);
} else {
/* PARENT: use sv[0], close sv[1] */
close(sv[1]);
/* Send message to child */
const char *msg = "Hello from parent!";
write(sv[0], msg, strlen(msg));
/* Read reply from child */
n = read(sv[0], buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Parent received: %s\n", buf);
}
close(sv[0]);
waitpid(pid, NULL, 0);
}
return 0;
}
Expected output:
sv[0]=3, sv[1]=4
Child received: Hello from parent!
Parent received: Hello from child!
| Feature | pipe() | socketpair() |
|---|---|---|
| Direction | One-way only | Bidirectional (full duplex) |
| For unrelated processes | No (only parent-child) | No (only parent-child) |
| Portability | All UNIX/POSIX | All POSIX (AF_UNIX) |
| Pass file descriptors | No | Yes (via SCM_RIGHTS) |
| Number of FDs created | 2 (one per direction) | 2 (both bidirectional) |
| Datagram mode | No | Yes (SOCK_DGRAM) |
| shutdown() half-close | No | Yes |
Rule of thumb: If you just need simple parent-child communication in one direction, use pipe() (simpler, slightly less overhead). If you need bidirectional communication or want to pass file descriptors, use socketpair().
This implements the sequence number server (originally in Section 44.8 for FIFOs) using UNIX domain stream sockets. Each client request gets the next number in a global sequence.
Sequence number server (seqnum_sv.c):
/* seqnum_sv.c β sequence number server using UNIX domain stream sockets */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdint.h>
#define SOCK_PATH "/tmp/seqnum_sv"
#define BACKLOG 5
/* Protocol: client sends how many numbers it wants (uint32_t)
* server replies with the starting number (uint32_t) */
int main(void)
{
struct sockaddr_un addr;
int sfd, cfd;
uint32_t seq_num = 0; /* Next sequence number to allocate */
uint32_t req, resp;
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
perror("socket"); exit(EXIT_FAILURE);
}
remove(SOCK_PATH);
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) == -1) {
perror("bind"); exit(EXIT_FAILURE);
}
if (listen(sfd, BACKLOG) == -1) {
perror("listen"); exit(EXIT_FAILURE);
}
printf("Sequence number server listening on %s\n", SOCK_PATH);
for (;;) {
cfd = accept(sfd, NULL, NULL);
if (cfd == -1) {
perror("accept");
continue;
}
/* Read the client's request: how many sequence numbers do you want? */
if (read(cfd, &req, sizeof(req)) != sizeof(req)) {
fprintf(stderr, "Failed to read request\n");
close(cfd);
continue;
}
/* Reply: starting number of the allocated block */
resp = seq_num;
seq_num += req;
printf("Client requests %u numbers: allocating %u..%u\n",
req, resp, seq_num - 1);
if (write(cfd, &resp, sizeof(resp)) != sizeof(resp)) {
fprintf(stderr, "Failed to write response\n");
}
close(cfd);
}
unlink(SOCK_PATH);
return 0;
}
Sequence number client (seqnum_cl.c):
/* seqnum_cl.c β sequence number client */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdint.h>
#define SOCK_PATH "/tmp/seqnum_sv"
int main(int argc, char *argv[])
{
struct sockaddr_un addr;
int sfd;
uint32_t req, resp;
unsigned long count;
if (argc != 2) {
fprintf(stderr, "Usage: %s <count>\n", argv[0]);
exit(EXIT_FAILURE);
}
count = strtoul(argv[1], NULL, 10);
req = (uint32_t)count;
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
perror("socket"); exit(EXIT_FAILURE);
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
if (connect(sfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) == -1) {
perror("connect"); exit(EXIT_FAILURE);
}
/* Send the number of sequence numbers we want */
if (write(sfd, &req, sizeof(req)) != sizeof(req)) {
fprintf(stderr, "Failed to write request\n");
exit(EXIT_FAILURE);
}
/* Receive the starting number */
if (read(sfd, &resp, sizeof(resp)) != sizeof(resp)) {
fprintf(stderr, "Failed to read response\n");
exit(EXIT_FAILURE);
}
printf("PID %ld: allocated sequence numbers %u..%u\n",
(long)getpid(), resp, resp + req - 1);
close(sfd);
return 0;
}
Test with multiple clients simultaneously:
# Terminal 1
./seqnum_sv
# Terminal 2 β multiple clients at once
./seqnum_cl 3 &
./seqnum_cl 5 &
./seqnum_cl 2 &
wait
UNIX domain sockets are a fast, reliable IPC mechanism for processes on the same machine. They use the socket API but with AF_UNIX address family. Addresses are file system paths or abstract names (Linux only).
SOCK_STREAM β byte stream, connection-oriented. Server: socketβbindβlistenβaccept. Client: socketβconnect. Uses read()/write().
SOCK_DGRAM β message-oriented, connectionless. Reliable on UNIX domain (unlike UDP). Uses sendto()/recvfrom(). Client usually needs to bind().
struct sockaddr_un with sun_family = AF_UNIX and sun_path holding the path (up to 108 chars on Linux). Must unlink() old socket files before bind(). Use SUN_LEN() for the address length (not for abstract sockets).
Socket file permissions (via chmod() and umask) control who can connect. After accept(), use SO_PEERCRED to get the peer’s PID/UID/GID. For datagrams, use SCM_CREDENTIALS ancillary data with sendmsg()/ recvmsg().
Set sun_path[0] = '\0', put name in sun_path[1..]. Calculate addrlen as offsetof(sun_path) + 1 + strlen(name). No file system entry, auto-cleanup on close. Scoped to network namespace. View with ss -xl.
Creates two pre-connected, bidirectional UNIX domain sockets in one call. Commonly used for parent-child IPC after fork(). Parent uses sv[0], child uses sv[1]. Like a bidirectional pipe. Can carry ancillary data (file descriptors).
sleep() between reads). Demonstrate that the sender eventually blocks rather than losing messages.
Key insight: UNIX domain datagrams are reliable. When the receiver’s buffer fills up, the kernel blocks the sender β it does not drop messages. This is fundamentally different from UDP.
/* Slow receiver demonstrating sender blocking */
/* In the receiver: */
while ((n = recvfrom(sfd, buf, sizeof(buf), 0, NULL, NULL)) > 0) {
printf("Received message\n");
sleep(1); /* Read one message per second β slower than sender */
}
/* The sender will block at sendto() when the buffer fills up */
/* Kernel socket receive buffer default: ~100-200 KB */
/* With 100-byte messages, buffer holds ~1000-2000 messages */
us_xfr_sv.c and us_xfr_cl.c to use the Linux abstract namespace. The solution is shown in Part 5 (ch57-5-abstract-namespace.html). Key changes:
- Remove all
remove()/unlink()calls - Set
sun_path[0] = '\0', place name insun_path[1..] - Calculate
addrlenmanually, not withSUN_LEN()
Key advantage over FIFOs: With stream sockets, each client gets its own connection (accepted via accept()). The server can handle multiple clients correctly because requests and replies are separate per-connection. With a FIFO, you need careful protocol design to route replies back to the right client.
sendto(A), what happens?
Answer: Socket C’s sendto() will receive ECONNREFUSED. Once a UNIX domain datagram socket is connected, the kernel filters incoming messages β only datagrams from the connected address (B) are accepted. Messages from any other source are rejected with an error sent back to the sender.
/* Test program for Exercise 57-4 */
/* socket A = /tmp/sockA, connected to /tmp/sockB */
/* socket C = /tmp/sockC, tries to send to /tmp/sockA */
/* C's sendto() to A will fail with ECONNREFUSED because A is connected to B */
ssize_t n = sendto(sfd_c, "test", 4, 0,
(struct sockaddr *)&addr_a, SUN_LEN(&addr_a));
if (n == -1) {
if (errno == ECONNREFUSED)
printf("Got ECONNREFUSED β A is connected to B, not accepting from C\n");
else
perror("sendto");
}
socketpair() creates two pre-connected UNIX domain sockets in a single call. Both descriptors are full-duplex β each end can both send and receive. pipe() creates two file descriptors where only pfd[1] (write end) can write and pfd[0] (read end) can read β it is unidirectional. For bidirectional parent-child communication, socketpair() is more convenient. Additionally, socket pairs can carry ancillary data (like file descriptors via SCM_RIGHTS), while pipes cannot.
Process supervisors like inetd, web servers (Apache’s prefork model), and shell implementations use socketpair() for parent-child coordination. A notable example is the OpenSSH ssh-agent: the agent creates a socket pair before forking, and uses it to pass file descriptors (the agent’s listening socket) to the child shell. Another example is any process that wants to run a helper subprocess and communicate with it bidirectionally without needing a named path.
Yes. With SOCK_DGRAM, the pair behaves like two connected datagram sockets. Each write(sv[0], ...) produces a discrete message that must be read with a single read(sv[1], ...). Message boundaries are preserved β unlike SOCK_STREAM where data is a byte stream. The main reason to choose SOCK_DGRAM with socketpair is when you need message framing without adding a length header to your protocol.
(1) UNIX domain socket concepts and why they are used (AF_UNIX, sockaddr_un). (2) Stream sockets (SOCK_STREAM): server/client setup, concurrent server with fork, partial read/write loops. (3) Datagram sockets (SOCK_DGRAM): sendto/recvfrom, reliability, sender blocking behavior. (4) Socket permissions: file permissions on socket path, SO_PEERCRED, SCM_CREDENTIALS, sendmsg()/recvmsg(). (5) Abstract namespace: Linux-only, sun_path[0]='\0', auto-cleanup, network namespace scoping. (6) socketpair(): pre-connected pair, parent-child IPC, vs pipe comparison.
108 bytes total in sun_path[], including the null terminator. So the usable path is at most 107 characters. This is shorter than PATH_MAX (4096) on Linux. If you need a longer socket name, you either have to restructure your directory layout or use the abstract namespace (where the name can use up to 107 bytes after the leading null byte).
D-Bus (Desktop Bus) is the main IPC mechanism for Linux desktop applications (GNOME, KDE, systemd). It uses UNIX domain stream sockets for communication between the D-Bus daemon and client processes. The daemon creates a listening socket (typically at an abstract name like @/tmp/dbus-XXXXXX on Linux). After a client connects, the daemon uses SO_PEERCRED to authenticate the client’s UID and PID, then applies policy rules to decide which operations the client is allowed to perform. This is a textbook example of combining UNIX socket credentials with application-level authorization.
Chapter 57 Complete!
Next: Chapter 58 β Sockets: Fundamentals of TCP/IP Networks
