Chapter 80: IPC: Unix Domain Sockets for Local Inter-Process Comm.

Chapter Objectives

Upon completing this chapter, you will be able to:

  • Understand the fundamental principles of the Berkeley Sockets API and its role in Inter-Process Communication (IPC).
  • Implement stream-based (SOCK_STREAM) and datagram-based (SOCK_DGRAM) communication using the AF_UNIX address family.
  • Utilize socketpair() to create an efficient, bidirectional communication channel between related processes, such as a parent and child.
  • Design, build, and debug client-server applications on a Raspberry Pi 5 that communicate locally using Unix domain sockets.
  • Explain the security implications of filesystem-based sockets and configure appropriate permissions.
  • Implement the advanced technique of passing open file descriptors between processes to build more secure and modular applications.

Introduction

In the complex ecosystem of an embedded Linux system, individual processes rarely work in isolation. A well-designed system is often a collection of specialized, single-purpose programs that must collaborate to achieve a larger goal. A process managing a temperature sensor, for example, needs a way to report its readings to a separate process that controls a cooling fan. A web-based user interface must communicate with a backend service that configures system settings. This collaboration requires robust, efficient, and secure mechanisms for Inter-Process Communication (IPC).

While the Linux kernel offers a rich variety of IPC mechanisms (such as pipes, FIFOs, message queues, and shared memory), Unix domain sockets stand out for their power and versatility. They extend the familiar networking paradigm of sockets—typically associated with TCP/IP communication over Ethernet or Wi-Fi—to the local machine. By using the same API, developers can create high-performance communication channels that are managed by the kernel but are often addressed through the filesystem. This approach provides a remarkable combination of performance, flexibility, and security that is highly relevant in embedded systems.

This chapter delves into the world of local IPC using the AF_UNIX socket family. We will explore how to build both connection-oriented (stream) and connectionless (datagram) services, much like you would with network sockets, but without the overhead of network protocols. We will begin with socketpair(), a simple yet powerful tool for setting up communication between a parent and child process. We will then build full client-server applications on the Raspberry Pi 5, demonstrating how unrelated processes can discover and communicate with each other. Finally, we will uncover one of the most compelling features of Unix domain sockets: the ability to pass open file descriptors from one process to another, a cornerstone of secure, privilege-separated designs. By the end of this chapter, you will have the practical skills to implement sophisticated, multi-process applications on your embedded Linux device.

Technical Background

The Sockets API: A Universal Interface for Communication

The concept of a socket emerged from the Berkeley Software Distribution (BSD) of Unix in the early 1980s as a unifying abstraction for communication. The goal was to create a single Application Programming Interface (API) that could handle different communication protocols and address types, whether they were for networking across a continent or communicating between processes on the same machine. This powerful idea was so successful that it became the de facto standard, adopted by virtually every modern operating system, including Linux.

A socket can be thought of as an endpoint for communication. Just as you plug a cable into a physical socket in a wall, a process creates a software socket to send or receive data. The elegance of the Sockets API lies in its layered design. The core API calls—socket()bind()connect()send()recv()—are generic. The specific behavior of these calls is determined by the parameters you provide when you first create the socket, namely the domaintype, and protocol.

The domain (also called the address family) specifies the communication context. For networking, you would use AF_INET for IPv4 or AF_INET6 for IPv6. For the local, on-device communication that is the focus of this chapter, we use the AF_UNIX domain (also known as AF_LOCAL). This tells the kernel that the communication will be contained entirely within the operating system, without involving any network hardware or protocols like TCP/IP.

The type determines the semantics of the communication. The two most common types are:

  • SOCK_STREAM: This provides a reliable, connection-oriented, bidirectional stream of bytes. It behaves like a telephone call; a connection must be established before any data is sent, and the data arrives in the same order it was sent, without errors or duplication. The kernel handles all the underlying mechanics of ensuring reliability and flow control. This type corresponds to the TCP protocol in the networking world.
  • SOCK_DGRAM: This provides a connectionless, unreliable datagram service. It behaves like sending a letter through the postal service. Each message, or datagram, is a self-contained packet that is sent independently. There is no initial connection, and the kernel does not guarantee that datagrams will arrive in order, or even that they will arrive at all. This lack of overhead makes it faster but places the burden of ensuring reliability on the application. This type corresponds to the UDP protocol in the networking world.

When working with AF_UNIX, the communication is mediated entirely by the Linux kernel. This makes it significantly faster and more efficient than network sockets, as there is no need to construct network packets, perform checksums, or interact with network interface card drivers. Data is essentially copied directly from the sending process’s buffer to the receiving process’s buffer within the kernel’s memory space.

Addressing in the AF_UNIX Domain

Unlike network sockets that are identified by an IP address and port number, AF_UNIX sockets are typically identified by a path in the filesystem. When a server process wants to create a listening socket, it calls the bind() system call with a specific path, for example, /tmp/myserver.sock. This creates a special file of type “socket” at that location. When a client process wants to connect to this server, it uses the same filesystem path.

This filesystem-based addressing is both simple and powerful. It makes service discovery trivial; a client only needs to know a well-defined path to find the server. It also means that standard Unix file permissions can be used to control access to the socket. By setting the owner, group, and permissions on the socket file, a developer can precisely control which users or groups are allowed to connect to the service. This is a significant security advantage.

However, this also introduces a responsibility. The socket file is not automatically removed when the server process terminates. If a server crashes and is restarted, its attempt to bind() to the same path will fail with an “Address already in use” error because the old socket file still exists. Therefore, well-behaved server applications must always unlink() (remove) the socket file before calling bind(), and should ideally install a signal handler to clean up the file upon exit.

The socketpair() System Call: A Shortcut for Related Processes

While the standard client-server model is powerful for unrelated processes, there is a common scenario where a simpler mechanism is desirable: communication between a parent process and a child it creates via fork(). For this, the socketpair() system call is an elegant and highly efficient solution.

A single call to socketpair() creates a pair of connected, unnamed AF_UNIX sockets. It takes the domain, type, and protocol as arguments and returns two file descriptors, one for each end of the bidirectional channel. Because the sockets are already connected, there is no need for bind()listen()accept(), or connect(). They are also “unnamed” because they do not exist in the filesystem, avoiding the need for cleanup.

graph TD
    subgraph Parent Process
        A[Start] --> B("call socketpair()");
        style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
        style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff

        B --> C{"sv[0], sv[1]<br>Connected Pair Created"};
        style C fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff

        C --> D("call fork()");
        style D fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
    end

    subgraph Fork Creates Two Processes
        D -- "pid > 0 (Parent)" --> E;
        D -- "pid == 0 (Child)" --> F;

        subgraph Parent Logic
            E("close sv[1]");
            style E fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff
        end

        subgraph Child Logic
            F("close sv[0]");
            style F fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff
        end
    end

    E --> G("(Use sv[0])");
    F --> H("(Use sv[1])");

    G <--> H;

    linkStyle 7 stroke:#10b981,stroke-width:3px,fill:none,stroke-dasharray: 5 5;

    I[Bidirectional Channel Established];
    style I fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
    G --> I;
    H --> I;

After a fork(), the parent and child processes both inherit the pair of file descriptors. By convention, the parent closes one end of the pair and the child closes the other. This leaves a clean, private, and fully functional communication channel between them. If a SOCK_STREAM pair is created, they have a reliable, stream-based channel. If SOCK_DGRAM is used, they can exchange messages. This mechanism is incredibly efficient as it requires minimal setup and is handled entirely within the kernel.

The Communication Flow: SOCK_STREAM vs. SOCK_DGRAM

Understanding the sequence of system calls is crucial for implementing socket-based IPC.

For a connection-oriented SOCK_STREAM server, the flow is as follows:

  1. socket(): The server creates a socket, specifying AF_UNIX and SOCK_STREAM.
  2. unlink(): The server defensively removes any old socket file that might exist at the desired path.
  3. bind(): The server associates the newly created socket with a filesystem path (its address). This creates the socket file on disk.
  4. listen(): The server tells the kernel that it is ready to accept incoming connections. It also specifies a “backlog,” which is the maximum number of pending connections the kernel should queue up if the server is busy.
  5. accept(): This is a blocking call. The server process goes to sleep, waiting for a client to connect. When a client connects, accept() wakes up and returns a new file descriptor. This new descriptor is for the actual communication with that specific client. The original listening socket remains open and can be used to accept more connections. This allows a server to handle multiple clients simultaneously.

For a SOCK_STREAM client:

  1. socket(): The client creates a socket with the same domain and type.
  2. connect(): The client attempts to establish a connection to the server’s address (the filesystem path). This is a blocking call that completes when the server accept()s the connection.Once the connection is established, both client and server can use send() and recv() (or the more general read() and write()) on their respective connected file descriptors to communicate.

For a connectionless SOCK_DGRAM server, the flow is simpler:

  1. socket(): The server creates a socket, specifying AF_UNIX and SOCK_DGRAM.
  2. unlink() and bind(): The server binds the socket to a filesystem path, just like the stream server.
  3. recvfrom(): The server calls recvfrom(), which blocks until a datagram arrives. This single call receives a message and also identifies the “address” of the sender, which can be used to send a reply. There is no listen() or accept().

For a SOCK_DGRAM client:

  1. socket(): The client creates a datagram socket.
  2. sendto(): The client sends a message using sendto(), specifying the data, its length, and the server’s address. No connect() is needed. The client can immediately send another datagram to a different server using the same socket if desired.
sequenceDiagram
    actor Client
    actor Server

    participant Client
    participant Server

    Server->>Server: socket(AF_UNIX, SOCK_STREAM)
    Server->>Server: unlink(PATH)
    Server->>Server: bind(fd, PATH)
    Server->>Server: listen(fd)

    loop Wait for Connection
        Server->>Server: accept(fd)
    end

    Client->>Client: socket(AF_UNIX, SOCK_STREAM)
    Client->>Server: connect(fd, PATH)
    Server-->>Client: Connection Established

    Client->>Server: write("Hello Server!")
    Server->>Server: read("Hello Server!")
    Server->>Client: write("Message Received!")
    Client->>Client: read("Message Received!")

    Client->>Client: close()
    Server->>Server: close()

Ancillary Data and Passing File Descriptors

Perhaps the most sophisticated feature of AF_UNIX sockets is the ability to pass ancillary data alongside the normal byte stream. This is a special, out-of-band communication channel managed by the kernel. The most powerful use of this feature is passing open file descriptors from one process to another.

This is not simply sending the integer value of a file descriptor. Sending the number 5 from one process to another is meaningless, as file descriptor tables are private to each process. Instead, this mechanism involves a special type of message (SCM_RIGHTS) that instructs the kernel to duplicate the file descriptor in the receiving process. The receiving process gets a brand new file descriptor number, but it points to the same underlying open file description in the kernel as the sender’s descriptor.

This technique is the foundation of many secure system designs. A common pattern is to have a privileged process that starts at boot. This process has the authority to open sensitive resources (like hardware devices or configuration files). It can then spawn less-privileged worker processes. When a worker needs access to a resource, it asks the privileged parent, which opens the resource and passes the file descriptor back to the worker. The worker process now has access to that specific resource for the lifetime of the descriptor, but it never had the privileges to open the resource itself. This adheres to the principle of least privilege, dramatically reducing the system’s attack surface. This is accomplished using the sendmsg() and recvmsg() system calls, which are more complex than send() and recv() as they can handle these special control messages.

Practical Examples

This section provides hands-on examples for the Raspberry Pi 5. You will need a standard Raspbian/Debian installation with the build-essential package to compile the C code. All commands should be run in a terminal.

Example 1: socketpair() for Parent-Child Communication

This example demonstrates the simplest way to establish a bidirectional communication channel between a parent and a child process. The parent will send a string to the child, and the child will convert it to uppercase and send it back.

Code Snippet: socketpair_demo.c

C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <ctype.h>

#define MSG "Hello, child process!"
#define BUFFER_SIZE 64

int main() {
    // 1. Create a socket pair for communication
    // sv[0] will be used by the parent, sv[1] by the child
    int sv[2]; 
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        perror("socketpair");
        exit(EXIT_FAILURE);
    }

    // 2. Fork the process
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) { // --- Child Process ---
        // 3a. Child closes the parent's end of the socket
        close(sv[0]);
        printf("[Child]  Socket pair created. Child is ready.\n");

        char buffer[BUFFER_SIZE];
        ssize_t num_bytes;

        // 4a. Read message from parent
        num_bytes = read(sv[1], buffer, BUFFER_SIZE - 1);
        if (num_bytes == -1) {
            perror("child read");
            exit(EXIT_FAILURE);
        }
        buffer[num_bytes] = '\0'; // Null-terminate the string
        printf("[Child]  Received from parent: '%s'\n", buffer);

        // 5a. Process the data (convert to uppercase)
        for (int i = 0; i < num_bytes; i++) {
            buffer[i] = toupper(buffer[i]);
        }

        // 6a. Send the processed data back to the parent
        printf("[Child]  Sending back: '%s'\n", buffer);
        if (write(sv[1], buffer, strlen(buffer)) == -1) {
            perror("child write");
            exit(EXIT_FAILURE);
        }

        // 7a. Child closes its socket and exits
        close(sv[1]);
        exit(EXIT_SUCCESS);

    } else { // --- Parent Process ---
        // 3b. Parent closes the child's end of the socket
        close(sv[1]);
        printf("[Parent] Socket pair created. Parent is ready.\n");

        char buffer[BUFFER_SIZE];
        ssize_t num_bytes;

        // 4b. Send a message to the child
        printf("[Parent] Sending to child: '%s'\n", MSG);
        if (write(sv[0], MSG, strlen(MSG)) == -1) {
            perror("parent write");
            exit(EXIT_FAILURE);
        }

        // 5b. Wait for the response from the child
        num_bytes = read(sv[0], buffer, BUFFER_SIZE - 1);
        if (num_bytes == -1) {
            perror("parent read");
            exit(EXIT_FAILURE);
        }
        buffer[num_bytes] = '\0';
        printf("[Parent] Received from child: '%s'\n", buffer);

        // 6b. Parent closes its socket and waits for child to terminate
        close(sv[0]);
        wait(NULL); // Clean up the zombie process
        printf("[Parent] Child has terminated. Exiting.\n");
    }

    return 0;
}

Build and Run Steps

  1. Save the code: Save the code above into a file named socketpair_demo.c.
  2. Compile: Open a terminal on your Raspberry Pi 5 and compile the code using GCC.
    gcc -o socketpair_demo socketpair_demo.c
  3. Run: Execute the compiled program.
    ./socketpair_demo

Expected Output

The output shows the clear, sequential communication between the two processes.

Plaintext
[Parent] Socket pair created. Parent is ready.
[Parent] Sending to child: 'Hello, child process!'
[Child]  Socket pair created. Child is ready.
[Child]  Received from parent: 'Hello, child process!'
[Child]  Sending back: 'HELLO, CHILD PROCESS!'
[Parent] Received from child: 'HELLO, CHILD PROCESS!'
[Parent] Child has terminated. Exiting.

Example 2: AF_UNIX SOCK_STREAM Client-Server

This example demonstrates a more general client-server model where two unrelated processes communicate. The server will wait for a connection, and the client will connect to it to send a message.

File Structure

Plaintext
unix_stream_ipc/
├── stream_server.c
├── stream_client.c
└── Makefile

Code Snippet: stream_server.c

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/stream_socket.sock"
#define BUFFER_SIZE 128

int main() {
    int server_fd, client_fd;
    struct sockaddr_un server_addr, client_addr;
    socklen_t client_len;

    // 1. Create the server socket
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. Unlink socket path to ensure we can bind
    unlink(SOCKET_PATH);

    // 3. Configure server address
    memset(&server_addr, 0, sizeof(struct sockaddr_un));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    // 4. Bind the socket to the address
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_un)) == -1) {
        perror("bind");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 5. Listen for incoming connections
    if (listen(server_fd, 5) == -1) { // Backlog of 5
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on %s\n", SOCKET_PATH);

    // 6. Accept a connection
    client_len = sizeof(struct sockaddr_un);
    client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
    if (client_fd == -1) {
        perror("accept");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server accepted a connection.\n");

    // 7. Read data from the client
    char buffer[BUFFER_SIZE];
    ssize_t num_bytes = read(client_fd, buffer, BUFFER_SIZE - 1);
    if (num_bytes > 0) {
        buffer[num_bytes] = '\0';
        printf("Server received: '%s'\n", buffer);
        // Echo back to client
        write(client_fd, "Message received!", 17);
    }

    // 8. Clean up
    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH); // Remove the socket file

    return 0;
}

Code Snippet: stream_client.c

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/stream_socket.sock"
#define BUFFER_SIZE 128

int main() {
    int client_fd;
    struct sockaddr_un server_addr;

    // 1. Create client socket
    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (client_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. Configure server address
    memset(&server_addr, 0, sizeof(struct sockaddr_un));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    // 3. Connect to the server
    if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_un)) == -1) {
        perror("connect");
        close(client_fd);
        exit(EXIT_FAILURE);
    }

    printf("Client connected to server.\n");

    // 4. Send data to the server
    const char *msg = "Hello from the stream client!";
    write(client_fd, msg, strlen(msg));
    printf("Client sent: '%s'\n", msg);

    // 5. Receive echo from server
    char buffer[BUFFER_SIZE];
    ssize_t num_bytes = read(client_fd, buffer, BUFFER_SIZE - 1);
    if (num_bytes > 0) {
        buffer[num_bytes] = '\0';
        printf("Client received echo: '%s'\n", buffer);
    }

    // 6. Clean up
    close(client_fd);

    return 0;
}

Build and Run Steps

1. Create files: Save the two C files and create a simple Makefile.

Makefile
# Makefile
CC=gcc
CFLAGS=-Wall -Wextra -std=c11

all: stream_server stream_client

stream_server: stream_server.c
	$(CC) $(CFLAGS) -o stream_server stream_server.c

stream_client: stream_client.c
	$(CC) $(CFLAGS) -o stream_client stream_client.c

clean:
	rm -f stream_server stream_client /tmp/stream_socket.sock

Compile: Run make in the unix_stream_ipc directory.

Bash
make

Run: You need two terminals for this.

In Terminal 1, start the server:

Bash
./stream_server


The server will print Server is listening on /tmp/stream_socket.sock and wait.

While the server is running, in Terminal 2, run the client:

Bash
./stream_client

Expected Output

  • Terminal 1 (Server):Server is listening on /tmp/stream_socket.
    sock Server accepted a connection.
    Server received: 'Hello from the stream client!'
  • Terminal 2 (Client):Client connected to server.
    Client sent: 'Hello from the stream client!'
    Client received echo: 'Message received!'

Tip: After starting the server, you can use ls -l /tmp/stream_socket.sock in another terminal to see the socket file. The first character of the permissions will be s, indicating a socket.

Example 3: AF_UNIX SOCK_DGRAM Client-Server

This example modifies the previous one to use datagrams. Notice the absence of listenaccept, and connect. Communication is message-based.

Code Snippet: dgram_server.c

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/dgram_socket.sock"
#define BUFFER_SIZE 128

int main() {
    int server_fd;
    struct sockaddr_un server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUFFER_SIZE];

    server_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (server_fd == -1) { perror("socket"); exit(EXIT_FAILURE); }

    unlink(SOCKET_PATH);

    memset(&server_addr, 0, sizeof(struct sockaddr_un));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_un)) == -1) {
        perror("bind"); close(server_fd); exit(EXIT_FAILURE);
    }

    printf("DGRAM Server is waiting for messages on %s\n", SOCKET_PATH);

    client_len = sizeof(struct sockaddr_un);
    ssize_t num_bytes = recvfrom(server_fd, buffer, BUFFER_SIZE - 1, 0,
                                 (struct sockaddr *)&client_addr, &client_len);

    if (num_bytes == -1) {
        perror("recvfrom");
    } else {
        buffer[num_bytes] = '\0';
        printf("Server received %zd bytes: '%s'\n", num_bytes, buffer);
    }

    close(server_fd);
    unlink(SOCKET_PATH);
    return 0;
}

Code Snippet: dgram_client.c

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/dgram_socket.sock"

int main() {
    int client_fd;
    struct sockaddr_un server_addr;
    const char *msg = "A single datagram message.";

    client_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (client_fd == -1) { perror("socket"); exit(EXIT_FAILURE); }

    memset(&server_addr, 0, sizeof(struct sockaddr_un));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    printf("Client sending datagram...\n");
    if (sendto(client_fd, msg, strlen(msg), 0,
               (struct sockaddr *)&server_addr, sizeof(struct sockaddr_un)) == -1) {
        perror("sendto");
    }

    close(client_fd);
    return 0;
}

Build and Run Steps

  1. Compile: Use gcc to compile both files.gcc -o dgram_server dgram_server.c
    gcc -o dgram_client dgram_client.c
  2. Run: Use two terminals, just like the stream example.
    • Terminal 1: ./dgram_server
    • Terminal 2: ./dgram_client

Expected Output

  • Terminal 1 (Server):DGRAM Server is waiting for messages on /tmp/dgram_socket.sock Server received 25 bytes: 'A single datagram message.'
  • Terminal 2 (Client):Client sending datagram...

Common Mistakes & Troubleshooting

Developing with Unix domain sockets is generally straightforward, but a few common issues can trip up even experienced developers. Understanding these pitfalls can save hours of debugging.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Stale Socket File Server fails to start with a bind error: Address already in use. Solution: Always call unlink(SOCKET_PATH) before bind() in the server code. This removes the old socket file left over from a previous run or crash. Also, consider adding a signal handler to clean up the socket on exit.
Server Not Ready Client fails with a connect error: Connection refused. Solution: Ensure the server process is running before the client. Verify the server has successfully called bind() and listen(). Double-check that the socket path is identical in both client and server code.
Incorrect Permissions bind() or connect() fails with a Permission denied error. Solution: Check the permissions of the directory containing the socket. The server process needs write access to create the socket file. If the client runs as a different user, it needs read/write access to the socket file itself. Use chmod() after binding or place the socket in a shared directory like /tmp.
Assuming Full Reads (SOCK_STREAM) A read() or recv() call returns fewer bytes than expected, leading to corrupted or incomplete messages. Solution: Never assume a single read will get the whole message. Streams can be chunked. Place your read call in a loop that continues until the total expected number of bytes has been received. A common pattern is to send a fixed-size header indicating the message length first.
Small Buffer (SOCK_DGRAM) Messages are silently truncated. The receiver gets an incomplete datagram without an error. Solution: The receiving buffer for recvfrom() must be large enough to hold the largest possible datagram. Client and server should agree on a maximum message size. Use the MSG_TRUNC flag to detect if a message was larger than the buffer.
Path Length Exceeded bind() or connect() fails with an obscure error, sometimes EINVAL. Solution: The path for a Unix socket stored in struct sockaddr_un has a limited size (typically 108 bytes). Ensure your socket path is well under this limit. Keep paths short and place them in conventional locations like /tmp/ or /var/run/.

Exercises

  1. Duplex socketpair() Communication:
    • Objective: Modify the socketpair_demo.c example to be fully duplex.
    • Guidance: After the initial exchange, have the parent read a line of text from the user’s standard input and send it to the child. The child should reverse the string and send it back. The parent then prints the reversed string. The loop should continue until the user types “exit”.
    • Verification: The parent and child should be able to exchange multiple messages back and forth within the same program execution.
  2. Datagram System Logger:
    • Objective: Create a simple system logging service using SOCK_DGRAM sockets.
    • Guidance:
      • The logger_server should create a datagram socket at /tmp/logger.sock. It should loop indefinitely, receiving messages and printing them to its standard output with a timestamp.
      • The log_client should be a command-line tool that takes a string as an argument (e.g., ./log_client "System rebooting now.") and sends it as a datagram to the server.
    • Verification: You should be able to run the server in one terminal and then run the client multiple times from another terminal, seeing each message appear in the server’s output.
  3. Multi-Client Stream Server:
    • Objective: Extend the stream_server.c example to handle multiple clients concurrently.
    • Guidance: Inside the main while loop, after accept() returns a new client connection, the server should fork() a new child process. The child process will be responsible for handling all communication with that specific client (reading their message and echoing a reply), and then exiting. The parent process should close the client file descriptor and immediately loop back to accept() to wait for the next connection.
    • Verification: Start the server. Then, from several different terminals, run the client simultaneously. Each client should successfully connect and complete its transaction.
  4. Remote Command Execution Shell:
    • Objective: Build a simple remote shell using a SOCK_STREAM server.
    • Guidance:
      • The server listens on a socket. When a client connects, it reads a command string (e.g., “ls -l /home/pi”).
      • The server should use the popen() function to execute the command. popen() runs a command and returns a FILE* that you can read from to get the command’s output.
      • The server reads the output from popen() and sends it back to the client over the socket.
      • The client reads the result from the socket and prints it to the screen.
    • Verification: The client should be able to send various shell commands and see the correct output, as if it had run them locally.
  5. Secure File Opener Service:
    • Objective: Implement a service that uses file descriptor passing for secure access.
    • Guidance: This is an advanced exercise.
      • The secure_opener server runs as a privileged user (or with permissions to access a specific file, e.g., /var/log/secure.log). It creates a SOCK_STREAM socket.
      • The log_reader client runs as a non-privileged user. It connects to the server and sends a request message (e.g., “GET_LOG_FD”).
      • The server receives the request, opens the secure log file for reading, and uses sendmsg() with SCM_RIGHTS to pass the open file descriptor to the client.
      • The client uses recvmsg() to receive the file descriptor. It then uses this new local descriptor to read the contents of the log file and print them.
    • Verification: The log_reader client, despite not having direct permissions to open /var/log/secure.log, should be able to display its contents by using the file descriptor provided by the server.

Summary

  • Unix Domain Sockets (AF_UNIX) provide a powerful IPC mechanism that uses the familiar Sockets API for efficient communication between processes on the same machine.
  • Communication is significantly faster than network sockets as it avoids the overhead of network stacks and hardware, with data being copied directly within the kernel.
  • SOCK_STREAM sockets provide a reliable, connection-oriented service, analogous to TCP. The flow involves socketbindlistenaccept on the server, and socketconnect on the client.
  • SOCK_DGRAM sockets provide a connectionless, message-oriented service, analogous to UDP. The server uses socketbindrecvfrom, and the client uses socketsendto.
  • socketpair() is a highly efficient shortcut for creating a connected pair of sockets, perfect for bidirectional communication between a parent and child process after a fork().
  • AF_UNIX sockets are typically addressed by filesystem paths, which allows standard file permissions to be used as a security mechanism to control access.
  • A key advanced feature is the ability to pass open file descriptors between processes using sendmsg() and recvmsg(), enabling secure designs based on the principle of least privilege.

Further Reading

  1. The Single UNIX Specification (POSIX.1-2017): The official standard for socket functions.
  2. Linux Manual Pages: The man pages provide detailed, Linux-specific information.
    • man 7 unix: An excellent overview of the AF_UNIX domain.
    • man 2 socketpair: Details on the socketpair system call.
    • man 3p popen: Information on the popen function for executing commands.
  3. “UNIX Network Programming, Volume 1: The Sockets Networking API, 3rd Edition” by W. Richard Stevens, Bill Fenner, and Andrew M. Rudoff: This is the definitive reference for all things related to sockets programming. Chapter 15 is dedicated to Unix Domain Sockets.
  4. “The Linux Programming Interface” by Michael Kerrisk: An exhaustive guide to the Linux kernel and glibc APIs. Chapter 57 provides a thorough treatment of Unix Domain Sockets.
  5. Beej’s Guide to Unix IPC: A more approachable, tutorial-style guide that covers various IPC mechanisms, including sockets. https://beej.us/guide/bgipc/
  6. Raspberry Pi Documentation: Official hardware and software documentation for the Raspberry Pi platform. https://www.raspberrypi.com/documentation/

Leave a Comment

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

Scroll to Top