socketpair() & Chapter Summary Connected Pairs, Exercises & Complete Review

 

socketpair() & Chapter Summary
Part 6 of 6 β€” Connected Pairs, Exercises & Complete Review
πŸ“‚ File 6 of 6
πŸ”— socketpair()
πŸ“– TLPI Β§57.7

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

socketpair() bidirectional pipe parent-child IPC full duplex AF_UNIX SOCK_STREAM SOCK_DGRAM sequence number server

The socketpair() System Call
#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:

  • domain must be AF_UNIX β€” socketpair works only for local sockets
  • type can be SOCK_STREAM or SOCK_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

socketpair() β€” how the two ends connect:

sv[0]
Process A end
⇄ kernel buffer
sv[1]
Process B end

write(sv[0], “hello”) β†’ read(sv[1]) returns “hello”
write(sv[1], “world”) β†’ read(sv[0]) returns “world”

Basic socketpair() Example
#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!

socketpair() vs pipe(): When to Use Which
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().

Exercise 57-3: Sequence Number Server with UNIX Stream Sockets

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

Chapter 57 Complete Summary
πŸ“Œ Core Concept

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).

πŸ“Œ Two Types

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().

πŸ“Œ Address Structure

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).

πŸ“Œ Permissions & Credentials

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().

πŸ“Œ Abstract Namespace (Linux-only)

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.

πŸ“Œ socketpair()

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).

Chapter Exercises (from TLPI)
Exercise 57-1 β€” Demonstrate sender blockingWrite a sender that sends UNIX domain datagrams as fast as possible. Write a receiver that reads slowly (with a 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 */
Exercise 57-2 β€” Abstract namespace rewriteRewrite 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 in sun_path[1..]
  • Calculate addrlen manually, not with SUN_LEN()
Exercise 57-3 β€” Sequence number serverReimplement the Section 44.8 sequence-number server using UNIX domain stream sockets instead of FIFOs. Full solution shown above in this file.

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.

Exercise 57-4 β€” Connected datagram socket behaviorIf socket A is connected to socket B, and socket C tries to 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");
}

🎯 Final Interview Questions β€” socketpair() & Overall Review
Q1. What does socketpair() do and how is it different from pipe()?

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.

Q2. Give a real-world use case for socketpair().

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.

Q3. Can you use socketpair() with SOCK_DGRAM? What would be different?

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.

Q4. Name the 6 key topics covered in Chapter 57.

(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.

Q5. What is the maximum path length for a UNIX domain socket on Linux?

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).

Q6. How does D-Bus use UNIX domain sockets?

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

← Part 5: Abstract Namespace Back to Part 1

Leave a Reply

Your email address will not be published. Required fields are marked *