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 theAF_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 domain, type, 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:
socket()
: The server creates a socket, specifyingAF_UNIX
andSOCK_STREAM
.unlink()
: The server defensively removes any old socket file that might exist at the desired path.bind()
: The server associates the newly created socket with a filesystem path (its address). This creates the socket file on disk.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.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:
socket()
: The client creates a socket with the same domain and type.- 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:
socket()
: The server creates a socket, specifyingAF_UNIX
andSOCK_DGRAM
.unlink()
andbind()
: The server binds the socket to a filesystem path, just like the stream server.recvfrom()
: The server callsrecvfrom()
, 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 nolisten()
oraccept()
.
For a SOCK_DGRAM
client:
socket()
: The client creates a datagram socket.sendto()
: The client sends a message usingsendto()
, specifying the data, its length, and the server’s address. Noconnect()
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
#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
- Save the code: Save the code above into a file named
socketpair_demo.c
. - Compile: Open a terminal on your Raspberry Pi 5 and compile the code using GCC.
gcc -o socketpair_demo socketpair_demo.c
- Run: Execute the compiled program.
./socketpair_demo
Expected Output
The output shows the clear, sequential communication between the two processes.
[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
unix_stream_ipc/
├── stream_server.c
├── stream_client.c
└── Makefile
Code Snippet: stream_server.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
#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
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.
make
Run: You need two terminals for this.
In Terminal 1, start the server:
./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:
./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 bes
, indicating a socket.
Example 3: AF_UNIX SOCK_DGRAM
Client-Server
This example modifies the previous one to use datagrams. Notice the absence of listen
, accept
, and connect
. Communication is message-based.
Code Snippet: dgram_server.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
#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
- Compile: Use
gcc
to compile both files.gcc -o dgram_server dgram_server.c
gcc -o dgram_client dgram_client.c
- Run: Use two terminals, just like the stream example.
- Terminal 1:
./dgram_server
- Terminal 2:
./dgram_client
- Terminal 1:
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.
Exercises
- 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.
- Objective: Modify the
- 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.
- The
- 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.
- Objective: Create a simple system logging service using
- Multi-Client Stream Server:
- Objective: Extend the
stream_server.c
example to handle multiple clients concurrently. - Guidance: Inside the main
while
loop, afteraccept()
returns a new client connection, the server shouldfork()
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 toaccept()
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.
- Objective: Extend the
- 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 aFILE*
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.
- Objective: Build a simple remote shell using a
- 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 aSOCK_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()
withSCM_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.
- The
- 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 involvessocket
,bind
,listen
,accept
on the server, andsocket
,connect
on the client.SOCK_DGRAM
sockets provide a connectionless, message-oriented service, analogous to UDP. The server usessocket
,bind
,recvfrom
, and the client usessocket
,sendto
.socketpair()
is a highly efficient shortcut for creating a connected pair of sockets, perfect for bidirectional communication between a parent and child process after afork()
.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()
andrecvmsg()
, enabling secure designs based on the principle of least privilege.
Further Reading
- The Single UNIX Specification (POSIX.1-2017): The official standard for socket functions.
- Linux Manual Pages: The
man
pages provide detailed, Linux-specific information.man 7 unix
: An excellent overview of theAF_UNIX
domain.man 2 socketpair
: Details on thesocketpair
system call.man 3p popen
: Information on thepopen
function for executing commands.
- “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.
- “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.
- Beej’s Guide to Unix IPC: A more approachable, tutorial-style guide that covers various IPC mechanisms, including sockets. https://beej.us/guide/bgipc/
- Raspberry Pi Documentation: Official hardware and software documentation for the Raspberry Pi platform. https://www.raspberrypi.com/documentation/