System V Message Queues — File Server/Client Application

 

Interview Questions
System V Message Queues — File Server/Client Application
Chapter 46 · TLPI · EmbeddedPathashala
Questions
40+
Topics
4 Areas
Format
Click to Reveal
Level
Intermediate

How to Use This Page

All interview questions from this chapter are collected here with model answers. Click any question to reveal the answer. Use this page for quick revision before interviews. Questions cover four topic areas: Architecture, Concurrent Server, Server Request Handling, and Client Design.

Quick Revision — Key Facts at a Glance
API / Concept What It Does Key Point
msgget(SERVER_KEY, ...) Get existing server queue by fixed key Key must match on both sides
msgget(IPC_PRIVATE, ...) Create brand new unnamed queue Must pass ID out-of-band
msgsnd(qid, &msg, size, flags) Send message to queue size excludes mtype
msgrcv(qid, &msg, maxsize, type, flags) Receive message from queue type=0 means any type
msgctl(qid, IPC_RMID, NULL) Delete a message queue Required — queues persist until deleted
atexit(removeQueue) Register cleanup on exit Does NOT run on SIGKILL
waitpid(-1, NULL, WNOHANG) Reap any finished child, non-blocking Must loop — one call per child
SA_RESTART Auto-restart slow syscalls after signal Not all syscalls honor it
EINTR System call interrupted by signal Retry the call
_exit() Exit without flushing stdio buffers Use in forked child
RESP_MT_FAILURE Server could not open file First response to check
RESP_MT_DATA A chunk of file data May arrive multiple times
RESP_MT_END End of transfer Zero-length mtext

Section 1 — Architecture & Design (Click to Expand)

7 questions on application design, message types, and IPC_PRIVATE.

Q1. Why does each client use IPC_PRIVATE instead of a fixed key for its reply queue?
IPC_PRIVATE creates a unique, unnamed queue that only the creating process knows about. If all clients used a fixed key, they would all get the same queue and would steal each other’s response messages. With IPC_PRIVATE, each client gets a unique queue ID which it passes to the server inside the request — guaranteeing that only that client receives the responses.
Q2. What are the three response message types and when is each sent?
RESP_MT_FAILURE: Sent when the server cannot open the requested file. Contains a short error string.
RESP_MT_DATA: Sent for each chunk of file data. May be sent multiple times for large files.
RESP_MT_END: Sent with zero data length after all data has been sent. Signals to the client that the transfer is complete.
Q3. Why does the client embed its queue ID inside the request message instead of using a fixed reply queue?
Because the server needs to know where to send the response. The client’s queue is created with IPC_PRIVATE — no fixed address — so the ID must be communicated dynamically. Embedding clientId in the request is the standard request-reply pattern in SysV MQ applications.
Q4. What is the difference between SERVER_KEY and IPC_PRIVATE?
SERVER_KEY is a fixed integer key (like a port number) that any process can use to locate the server’s queue with msgget(SERVER_KEY, ...). IPC_PRIVATE is a special value (0) that tells the kernel to create a brand-new queue with no key — only the creating process gets the returned ID. There is no way to look up an IPC_PRIVATE queue by name.
Q5. Why must the client explicitly delete its queue? Why doesn’t it auto-close like a file descriptor?
System V IPC objects (message queues, semaphores, shared memory) persist in the kernel until explicitly deleted with msgctl(IPC_RMID) or until the system reboots. Unlike file descriptors, there is no reference counting — closing a process does not automatically remove the queue. If not deleted, the queue occupies a slot in the system’s message queue table (limited by MSGMNI) indefinitely.
Q6. How does the server distinguish between multiple simultaneous clients?
The server does not need to distinguish clients in its request queue — it accepts any message regardless of type (msgrcv(..., 0, 0)). Each request contains the client’s unique queue ID. After forking, each server child sends responses to its specific client’s queue using req->clientId. The clients are separated by having different reply queue IDs, not by message type.
Q7. What problem would arise if all clients shared a single response queue?
Multiple clients would receive each other’s response messages. A message meant for Client A might be picked up by Client B. Even if you tried to use mtype to separate them, two clients could pick the same type value, causing races. The IPC_PRIVATE per-client queue design cleanly avoids this.

Section 2 — Concurrent Server, fork() & Signals (Click to Expand)

11 questions on fork, SIGCHLD, zombies, SA_RESTART, and EINTR.

Q8. What is a zombie process and why is it dangerous in a long-running server?
A zombie is a process that has exited but whose parent has not yet called wait()/waitpid() to collect its exit status. It holds no resources except a PID slot in the process table. In a server that forks a child per request without reaping, thousands of zombies accumulate, eventually exhausting all available PIDs. fork() then fails with EAGAIN, crashing the server.
Q9. Why does the SIGCHLD handler use a while loop instead of calling waitpid() once?
Multiple children can exit in rapid succession. Because POSIX signals are not queued (only one pending signal of a given type at a time), if three children die while the handler is running, only one SIGCHLD is delivered. A single waitpid() call would only reap one child, leaving the other two as zombies. The while loop continues until waitpid() returns 0 (no more children have exited), ensuring all are reaped.
Q10. What is the purpose of WNOHANG in waitpid(-1, NULL, WNOHANG)?
WNOHANG makes waitpid() return immediately (with 0) if no child has exited, instead of blocking. Without it, the signal handler would block waiting for a child — but signal handlers should complete quickly and never block. The loop exits cleanly when waitpid() returns 0.
Q11. Why does the grimReaper handler save and restore errno?
Signal handlers can interrupt any point in the main program — including between an instruction that sets errno and the next instruction that checks it. waitpid() itself may set errno (e.g. to ECHILD). If we don’t save and restore errno, the interrupted code in the main loop might see a corrupted error value, causing incorrect behavior.
Q12. What does SA_RESTART do and why does the server also check for EINTR?
SA_RESTART tells the kernel to automatically restart certain “slow” system calls (those that can block) after a signal handler returns, instead of failing with EINTR. However, not all system calls are restarted on all Linux versions or in all situations. msgrcv() in particular may or may not restart. The explicit EINTR check in the loop is belt-and-suspenders — it handles the cases where auto-restart doesn’t happen.
Q13. Why does the child call _exit() instead of exit()?
After fork(), the child has copies of the parent’s stdio buffers. exit() flushes all stdio buffers before terminating. Flushing the parent’s buffered output from the child would cause duplicate output. _exit() terminates immediately without touching stdio buffers, leaving the parent’s buffers intact.
Q14. How does the child know which client to serve — it was never told explicitly after fork()?
The child inherits a copy of the parent’s stack via fork(). The req variable (holding the request message with the client’s pathname and queue ID) is on the parent’s stack. After forking, the child has its own copy of that stack and thus a copy of req, ready to use.
Q15. What is the difference between a zombie and an orphan process?
A zombie is a child that has exited but the parent hasn’t called wait() yet — it’s dead but still in the process table.
An orphan is a child whose parent died while the child was still running. The kernel re-parents it to init (PID 1), which periodically calls wait(), so orphans do not become permanent zombies.
Q16. Why is a concurrent server preferred over an iterative server for this file-serving application?
An iterative server serves one request at a time. If a client requests a large file, the server spends a long time reading and sending it — blocking all other clients from being served. With a concurrent server, a child process handles each request independently, so large-file requests do not block small-file requests. The parent immediately becomes available for the next client.
Q17. What would happen if the server removed its queue before all children finished sending?
The children would still hold the inherited queue ID and would still be able to send to the client queues (the client queues, not the server queue). The server queue is only used for incoming requests. Deleting the server queue stops new requests from arriving but does not affect in-progress responses. However, if the server called msgctl(IPC_RMID) on the server queue before all clients have sent their requests, those clients’ msgsnd() calls would fail with EINVAL.
Q18. What file descriptors does the server child inherit and should it close any of them?
The child inherits copies of all open file descriptors from the parent — stdin, stdout, stderr, and any others. In this application the child doesn’t use stdin/stdout/stderr for its main work (it uses message queues). In a production server, the child should close file descriptors it doesn’t need to avoid resource leaks and accidental file descriptor inheritance across exec() calls.

Section 3 — Server Request Handling / serveRequest() (Click to Expand)

10 questions on file open, data chunking, message sizing, and error paths.

Q19. Why does msgsnd() use numRead as the size argument, not RESP_MSG_SIZE?
read() may return fewer bytes than RESP_MSG_SIZE — especially on the last chunk of a file or when reading special files. Using numRead ensures only the actual data bytes are sent. Using RESP_MSG_SIZE would send uninitialized garbage bytes from the buffer after the actual data.
Q20. What does a zero-length msgsnd() call mean in this application?
It sends the RESP_MT_END message — a message with an mtype of RESP_MT_END and zero bytes of data. This is the sentinel that tells the client “all file data has been sent, you can stop receiving.” The zero length is valid in SysV MQ; the message is received successfully but msgrcv() returns 0.
Q21. Why is the mtype field NOT counted in the size argument to msgsnd()/msgrcv()?
The mtype field is part of the kernel’s message metadata — it is used for routing (selective receive by type). The size argument only describes the user data payload (everything after mtype). This is a fundamental SysV MQ API convention. Macros like REQ_MSG_SIZE = sizeof(struct requestMsg) - sizeof(long) encode this correctly.
Q22. The server does not handle read() or msgsnd() errors during transfer. Why and what are the consequences?
Once data transfer begins, the only communication channel is the client’s queue. If msgsnd() fails, that channel is broken — there is no other way to notify the client. The server simply stops sending. The consequence is that the client never receives RESP_MT_END and blocks forever in msgrcv(). This is a known limitation. Fix: add a timeout on the client, or include an error-reporting queue in the protocol.
Q23. How many messages does the server send for a 25,000-byte file if RESP_MSG_SIZE is 8192?
ceil(25000 / 8192) = 4 DATA messages (8192 + 8192 + 8192 + 424 bytes) plus 1 END message = 5 total messages. The last data message carries only 424 bytes, not 8192.
Q24. What would happen if the server sent all file data in one huge message?
SysV message queues have a maximum message size limit (controlled by MSGMAX, typically 8192 bytes on Linux). Sending a message larger than this causes msgsnd() to fail with EINVAL. This is exactly why the server reads and sends in chunks of at most RESP_MSG_SIZE.
Q25. If the client’s queue is full when the server tries to send a data message, what happens?
By default (flags = 0 in msgsnd()), msgsnd() blocks until there is space in the queue. The server child waits. In this application, the client is also reading from the queue simultaneously, so it frees up space. If you passed IPC_NOWAIT, msgsnd() would fail immediately with EAGAIN.
Q26. Why does the server child call exit(EXIT_FAILURE) on the failure path, not _exit()?
The book uses exit(EXIT_FAILURE) on the error path. Since the child hasn’t done any stdio buffering work (it just opened a file and called snprintf), there is nothing harmful about calling exit() here. The _exit() is specifically important for the normal work-done path to prevent double-flushing of parent stdio buffers. On the error path the child is done anyway.
Q27. What system limits govern the maximum message size and queue capacity?
MSGMAX: maximum size of a single message (default 8192 bytes on Linux).
MSGMNB: maximum total bytes in a single queue (default 16384 bytes).
MSGMNI: maximum number of message queues system-wide.
These can be read/changed via /proc/sys/kernel/msgmax etc.
Q28. If the file server is running as root and the requested file has mode 000, what happens?
Root bypasses normal read permission checks — open() will succeed. The server will read and send the file contents even though normal users cannot read it directly. This is a security concern in real servers — the server should check the requesting user’s permissions, not just try to open the file with its own (root) privileges.

Section 4 — Client Design (Click to Expand)

12 questions on IPC_PRIVATE, atexit, request composition, and response handling.

Q29. In what situations will the atexit() handler NOT run?
atexit() handlers do NOT run when: the process receives SIGKILL or SIGSTOP (which cannot be caught or ignored), the process calls _exit() directly, or the process is terminated by a hardware fault (e.g. SIGSEGV by default). For SIGTERM and SIGINT, if the process doesn’t install a handler that calls exit(), the queue will also leak.
Q30. Why is clientId declared as a global static variable instead of local to main()?
The removeQueue() function registered with atexit() needs to access clientId. atexit() handlers take no arguments and have no return value. The only way to share state between main() and an atexit() handler is via a global (or static file-scope) variable.
Q31. What is the purpose of S_IWGRP on the client’s message queue?
The server process may run under a different UID than the client. S_IWGRP grants write permission to processes in the same group as the client queue’s owner. This allows the server child (which may have a different UID) to call msgsnd() on the client’s queue to deliver responses.
Q32. The client uses strncpy and then manually sets the last byte to ‘\0’. Why are both necessary?
strncpy(dst, src, n) copies at most n bytes. If src is longer than n, it copies n bytes without a null terminator. The manual dst[n-1] = '\0' ensures the string is always null-terminated even when truncation occurs. Without it, the server could receive a non-null-terminated pathname and crash or open the wrong file.
Q33. How would you add a timeout to the client’s msgrcv() to prevent blocking forever?
One approach: use alarm(timeout_seconds) before the msgrcv() call and install a SIGALRM handler. When the alarm fires, msgrcv() returns -1 with errno == EINTR — the client can then treat this as a timeout. Another approach (Linux-specific): use IPC_NOWAIT with polling in a loop, sleeping between attempts.
Q34. msgrcv(clientId, &resp, sizeof(resp.data), 0, 0) — what does the type argument 0 mean?
A type argument of 0 in msgrcv() means “receive the first message in the queue regardless of its type.” The client does not filter by type here — it receives whatever arrives first (which could be RESP_MT_FAILURE, RESP_MT_DATA, or RESP_MT_END) and checks resp.mtype after receiving.
Q35. If two instances of the client run simultaneously requesting the same file, is there any conflict?
No conflict. Each client instance creates its own IPC_PRIVATE queue with a unique ID. Each server child sends responses to its respective client’s queue. The two transfers are completely independent and do not interfere with each other.
Q36. What happens if the server crashes after the client sends the request but before it sends any response?
The client blocks indefinitely in msgrcv() waiting for a response that will never come. The simple client has no timeout mechanism to detect this. The server queue is deleted when the server exits (if it calls msgctl(IPC_RMID)), but this does not unblock the client’s msgrcv() on its private queue.
Q37. Why does the client check for RESP_MT_FAILURE only on the first received message?
The server sends RESP_MT_FAILURE only if it cannot open the file — and if it can’t open the file, it exits immediately after sending the failure message. So the failure message is always the first and only message. Once the client receives the first message and it’s not RESP_MT_FAILURE, the file is being served successfully and subsequent messages will only be DATA or END.
Q38. How can you list all SysV message queues on a Linux system and delete leaked ones?
List all queues: ipcs -q
Delete a specific queue: ipcrm -q <msqid>
Delete by key: ipcrm -Q <key>
You can also read /proc/sysvipc/msg for a machine-readable list.
Q39. Compare SysV message queues with POSIX message queues — when would you choose each?
SysV MQ: Older API; integer keys; kernel persistence; identified by integer ID; no file-descriptor interface; not pollable with select/epoll.
POSIX MQ: Newer API (mq_open()); named like files (/myqueue); returns a file descriptor — can use with select()/epoll(); priority-ordered delivery; more portable.
Choose SysV for legacy compatibility. Choose POSIX for new code — especially if you need to integrate with select/poll event loops.
Q40. What is the IPC_EXCL flag used for in msgget() and when would you use it?
IPC_EXCL combined with IPC_CREAT means “create only if it doesn’t already exist — fail with EEXIST if it does.” The server uses IPC_CREAT | IPC_EXCL to create its queue. This prevents accidentally connecting to a stale queue from a previous server run. If the old queue still exists (e.g. server crashed without cleanup), the new server startup will fail, alerting the admin.

Spot the Bug — Quick Code Quizzes

Find what is wrong in each snippet:

Bug 1:

/* SIGCHLD handler */
static void grimReaper(int sig) {
    waitpid(-1, NULL, WNOHANG); /* reap one child */
}
Bug: Only calls waitpid() once. If multiple children exit simultaneously, only one is reaped. The others become permanent zombies. Fix: Put it in a while loop.

Bug 2:

/* Server SIGCHLD handler */
static void grimReaper(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0)
        continue;
    /* errno not saved/restored */
}
Bug: Missing savedErrno = errno before the loop and errno = savedErrno after. waitpid() modifies errno — the interrupted main-loop code may see wrong error values.

Bug 3:

/* Client sending request */
strncpy(req.pathname, argv[1], sizeof(req.pathname));
msgsnd(serverId, &req, REQ_MSG_SIZE, 0);
Bug: strncpy copies up to n bytes. If argv[1] is exactly sizeof(pathname) characters long, no null terminator is written. Fix: copy sizeof - 1 bytes and manually set req.pathname[sizeof-1] = '\0'.

Chapter 46 Complete — Topics Covered
SysV MQ overview IPC_PRIVATE vs named key REQUEST/RESPONSE protocol Concurrent server with fork() Zombie processes SIGCHLD + waitpid loop SA_RESTART + EINTR _exit() vs exit() serveRequest() function RESP_MT_FAILURE/DATA/END Zero-length END message atexit() cleanup Client receive loop Known limitations

Leave a Reply

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