4 of 4
Client Logic
msgget, atexit
Request-Reply
What This Part Covers
The client program is simpler than the server but introduces important patterns: creating a private reply queue, ensuring cleanup with atexit(), composing a request message, and correctly receiving the stream of response messages from the server.
This part also discusses limitations of the simple client and how a real production client would improve on them.
Key Terms
The first thing the client does is create its own message queue using IPC_PRIVATE. Unlike the server’s well-known queue (identified by SERVER_KEY), this queue has no fixed name — only the client knows its ID.
/* Look up the server's queue by its well-known key */
serverId = msgget(SERVER_KEY, S_IWUSR);
if (serverId == -1)
errExit("msgget - server message queue");
/* Create our own private reply queue — no other process knows it */
clientId = msgget(IPC_PRIVATE, S_IRUSR | S_IWUSR | S_IWGRP);
if (clientId == -1)
errExit("msgget - client message queue");
| Call | Key | Flags | Purpose |
|---|---|---|---|
msgget(SERVER_KEY, ...) |
SERVER_KEY |
S_IWUSR — write only |
Find the server’s existing queue. Client only needs to write to it (send requests). |
msgget(IPC_PRIVATE, ...) |
IPC_PRIVATE |
S_IRUSR|S_IWUSR|S_IWGRP |
Create a new private queue. Client reads from it; server child writes to it. |
S_IWGRP bit allows the server child process to send response messages into the client’s queue even though it has a different UID.atexit() registers a function that the C runtime calls automatically when the process exits — whether via return from main(), a call to exit(), or even abnormal termination through some signal handlers.
static int clientId; /* global so atexit handler can see it */
static void
removeQueue(void)
{
if (msgctl(clientId, IPC_RMID, NULL) == -1)
errExit("msgctl");
}
/* In main(), after creating clientId: */
if (atexit(removeQueue) != 0)
errExit("atexit");
Why is this necessary? System V message queues persist in the kernel until explicitly deleted — they are not closed like file descriptors. If the client exits without deleting its queue:
- The queue remains in the kernel’s IPC namespace.
- It occupies a slot in the system’s message queue table (limited by
MSGMNI). - Pending messages in it waste kernel memory.
- You can see leftover queues with
ipcs -qand delete them withipcrm -q <id>.
exit() from an error path, control might never reach the end of main(). atexit() ensures cleanup happens regardless of how the process exits.atexit() handlers do NOT run when the process is killed by SIGKILL or SIGSTOP, or if the process calls _exit(). For those cases, the queue leaks. This is a known limitation discussed in TLPI Exercise 46-5.The client builds a requestMsg and sends it to the server’s queue.
struct requestMsg req;
req.mtype = 1; /* Any positive value — server reads all types */
req.clientId = clientId; /* Tell server where to send the reply */
/* Copy pathname safely — prevent buffer overflow */
strncpy(req.pathname, argv[1], sizeof(req.pathname) - 1);
req.pathname[sizeof(req.pathname) - 1] = '\0'; /* ensure null termination */
/* Send request to server's queue */
if (msgsnd(serverId, &req, REQ_MSG_SIZE, 0) == -1)
errExit("msgsnd");
| Field | Value | Why |
|---|---|---|
req.mtype |
1 | Server uses msgrcv(..., 0, 0) which receives any type. Value 1 is arbitrary. |
req.clientId |
clientId | Server child will reply to this queue ID. |
req.pathname |
argv[1] | Filename to serve, safely copied with strncpy + manual null termination. |
| size arg | REQ_MSG_SIZE | Macro: sizeof(requestMsg) - sizeof(long) — excludes mtype. |
After sending the request, the client waits for responses on its private queue. The first message it receives determines what happens next:
(error text)
exit(EXIT_FAILURE)
Loop: receive more messages
→ Stop loop, exit OK
struct responseMsg resp;
ssize_t msgLen, totBytes;
/* Receive the FIRST response — might be failure or first data chunk */
msgLen = msgrcv(clientId, &resp, sizeof(resp.data), 0, 0);
if (msgLen == -1)
errExit("msgrcv");
/* Check if server reported failure */
if (resp.mtype == RESP_MT_FAILURE) {
fprintf(stderr, "%s\n", resp.data); /* print error from server */
exit(EXIT_FAILURE);
}
/* File opened OK — loop receiving data chunks until RESP_MT_END */
totBytes = msgLen;
for (;;) {
if (resp.mtype == RESP_MT_END)
break; /* transfer complete */
write(STDOUT_FILENO, resp.data, msgLen); /* print chunk to stdout */
msgLen = msgrcv(clientId, &resp,
sizeof(resp.data), 0, 0);
if (msgLen == -1)
errExit("msgrcv");
totBytes += msgLen;
}
The book explicitly notes this client does not handle several real-world failure scenarios:
| Scenario | Problem | How to Fix (Exercise 46-5) |
|---|---|---|
| Server dies mid-transfer | Client blocks forever in msgrcv() waiting for more DATA or END that never comes. |
Add a timeout using alarm() or select() with the queue, or use MSG_NOERROR + periodic polling. |
| Server never processes the request | Client blocks in msgrcv() indefinitely. |
Add a timeout before the first receive. |
| Client killed by SIGKILL | atexit() does not run — queue leaks. |
Install a handler for SIGTERM/SIGINT that calls msgctl(IPC_RMID) before exiting. |
| Multiple clients, one queue ID reuse | If clientId is reused after a crash, a new client might receive stale messages from a previous run. | Drain the queue on startup, or use a versioned message type. |
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include "svmsg_file.h"
static int clientId;
static void
removeQueue(void)
{
msgctl(clientId, IPC_RMID, NULL);
}
int
main(int argc, char *argv[])
{
struct requestMsg req;
struct responseMsg resp;
int serverId;
ssize_t msgLen, totBytes;
if (argc != 2) {
fprintf(stderr, "Usage: %s pathname\n", argv[0]);
exit(EXIT_FAILURE);
}
/* === SETUP === */
serverId = msgget(SERVER_KEY, S_IWUSR);
if (serverId == -1) exit(EXIT_FAILURE);
clientId = msgget(IPC_PRIVATE,
S_IRUSR | S_IWUSR | S_IWGRP);
if (clientId == -1) exit(EXIT_FAILURE);
atexit(removeQueue); /* ensure queue deleted on exit */
/* === SEND REQUEST === */
req.mtype = 1;
req.clientId = clientId;
strncpy(req.pathname, argv[1], sizeof(req.pathname) - 1);
req.pathname[sizeof(req.pathname) - 1] = '\0';
msgsnd(serverId, &req, REQ_MSG_SIZE, 0);
/* === RECEIVE FIRST RESPONSE === */
msgLen = msgrcv(clientId, &resp,
sizeof(resp.data), 0, 0);
if (resp.mtype == RESP_MT_FAILURE) {
fprintf(stderr, "Server error: %s\n", resp.data);
exit(EXIT_FAILURE);
}
/* === LOOP RECEIVING DATA === */
totBytes = 0;
for (;;) {
if (resp.mtype == RESP_MT_END) break;
write(STDOUT_FILENO, resp.data, msgLen);
totBytes += msgLen;
msgLen = msgrcv(clientId, &resp,
sizeof(resp.data), 0, 0);
if (msgLen == -1) exit(EXIT_FAILURE);
}
fprintf(stderr, "Received %ld bytes\n", (long)totBytes);
exit(EXIT_SUCCESS);
}
| Aspect | IPC_PRIVATE | Named Key (e.g. ftok/SERVER_KEY) |
|---|---|---|
| Discoverability | Only by sharing the returned ID explicitly | Any process that knows the key can connect |
| Use case | Private reply queues, parent-child IPC | Well-known service endpoints |
| Multiple queues | Each call creates a brand new queue | Same key always returns same queue (if it exists) |
| Cleanup | Must delete explicitly — never auto-deleted | Same — must delete explicitly |
| In this app | Client reply queues | Server request queue |
- Why does the client create its queue with
IPC_PRIVATEinstead of a fixed key? - What does
atexit()do and why is it necessary for the client’s private queue? - In what situations will the
atexit()handler NOT run? - Why does the client include
clientIdin the request message instead of using a fixed agreed-upon reply queue? - Why does the client check
mtype == RESP_MT_FAILUREon the very first received message before starting the data loop? - What happens if the client tries to receive but the server has already deleted the queue?
- Why is the
clientIdvariable declared asstaticglobal instead of local tomain()? - What is the permission
S_IWGRPfor on the client’s message queue? - The client uses
strncpyand manually sets the last byte to'\0'. Why is both necessary? - How would you add a timeout to the client’s
msgrcv()call to prevent blocking forever? - If two instances of this client run simultaneously requesting the same file, is there any conflict? Explain.
- What does
msgrcv(clientId, &resp, sizeof(resp.data), 0, 0)receive — all message types or a specific one?
Chapter 46 Complete!
See all interview questions consolidated, or go back to start.
