Operating Systems Module 2 – Processes: creation, life cycle, signals and IPC
IPC: shared memory and message queues
How processes share memory with mmap and shm_open, keep shared data safe with process-shared locks, and pass messages by priority through queues.
What you will learn
- Set up memory that processes share, with a MAP_SHARED mapping and with shm_open, ftruncate and mmap
- Protect shared data with an atomic, a process-shared mutex or a semaphore, and explain the updates lost without one
- Predict the order in which a POSIX or System V message queue delivers messages, and compare queues with sockets
- Work around platform gaps such as the missing POSIX message queues and unnamed semaphores on macOS
Before you start
What you’ll learn · Before you start
What you will learn
- Set up memory that processes share, with a MAP_SHARED mapping and with shm_open, ftruncate and mmap
- Protect shared data with an atomic, a process-shared mutex or a semaphore, and explain the updates lost without one
- Predict the order in which a POSIX or System V message queue delivers messages, and compare queues with sockets
- Work around platform gaps such as the missing POSIX message queues and unnamed semaphores on macOS
Before you start
On this page
Two processes that each added 1 to a counter in shared memory: Memory that two or more processes map at once, so that a byte one writes is there for the others without any copy. The processes must coordinate their updates themselves. 5,000,000 times ended far short of 10,000,000 in this lesson’s recorded run. Shared memory is the fastest way for processes to exchange data, because nothing is copied, and the least forgiving, because nothing is coordinated for them either.
This lesson shares a page between two processes and loses updates on it, then fixes them three ways and names the pieces macOS leaves out. It gives the memory a name so that unrelated programs can meet, turns to message queues, which deliver whole messages in order of priority, and ends with a table that chooses between every mechanism of this module for a design of your own.
Shared memory maps one page into two processes, and nothing guards it
mmap with MAP_SHARED asks for memory whose updates “are visible to other processes mapping the same region”, in
mmap(2)’s words; with MAP_ANONYMOUS too, no file stands behind it and it starts as zeros. A mapping made before
fork is kept by the child “with the same attributes”, so a parent and its child then map the same physical page.
With MAP_PRIVATE instead, the child’s first write would get a private copy of the page, the
copy-on-write: Sharing memory pages between processes until one of them writes to a page, when the kernel copies that page for the writer. fork uses it, so the child does not copy all of its parent's memory at once. More in “Creating processes: fork, exec and wait” of fork, and the parent would never see it. Shared pages are what makes
this the fastest IPC: a byte written by one process is there for the other at once, with no system call and no copy.
The program below puts a counter in such a page and lets the parent and the child add 1 to it 5,000,000 times each, at the same moment. It does that three times: with no synchronisation at all, with an atomic add, and with a mutex that both processes can lock:
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
enum { PER_PROCESS = 5000000 };
// One page shared by a parent and its child: MAP_SHARED, so that both see
// every write. MAP_ANONYMOUS: no file behind it.
typedef struct {
atomic_int ready; // both processes start together
volatile long plain; // incremented with no synchronisation
atomic_long atomic; // incremented with atomic_fetch_add
pthread_mutex_t mutex; // a mutex both processes can lock
long locked; // incremented while holding the mutex
} Shared;
static void count(Shared *s, int how) {
atomic_fetch_add(&s->ready, 1);
while (atomic_load(&s->ready) % 2 != 0) {
}
for (int i = 0; i < PER_PROCESS; i++) {
if (how == 0) {
s->plain = s->plain + 1; // read, add, write: another process can come in
} else if (how == 1) {
atomic_fetch_add(&s->atomic, 1);
} else {
pthread_mutex_lock(&s->mutex);
s->locked++;
pthread_mutex_unlock(&s->mutex);
}
}
}
int main(void) {
Shared *s = mmap(NULL, sizeof *s, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
pthread_mutexattr_t attributes;
pthread_mutexattr_init(&attributes);
pthread_mutexattr_setpshared(&attributes, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&s->mutex, &attributes);
const char *labels[] = {"no synchronisation", "atomic_fetch_add",
"process-shared mutex"};
for (int how = 0; how < 3; how++) {
const pid_t child = fork();
count(s, how);
if (child == 0) {
_exit(0);
}
waitpid(child, NULL, 0);
}
const long want = 2L * PER_PROCESS;
for (int how = 0; how < 3; how++) {
const long got = how == 0 ? s->plain : how == 1 ? s->atomic : s->locked;
if (got == want) {
printf("%-21s %ld of %ld increments counted\n", labels[how], got, want);
} else {
printf("%-21s fewer than %ld counted: updates were lost\n", labels[how],
want);
}
}
pthread_mutex_destroy(&s->mutex);
munmap(s, sizeof *s);
return 0;
} Output
no synchronisation fewer than 10000000 counted: updates were lost atomic_fetch_add 10000000 of 10000000 increments counted process-shared mutex 10000000 of 10000000 increments counted
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined shm_counter.c -o shm_counter && ./shm_counter
The first round lost updates; the other two counted all 10,000,000. Nothing was wrong with the memory: counter + 1 is
three steps for the processor, a load, an add and a store, and the other process can run between them. This is a
A bug in which the result depends on how the steps of two processes or threads happen to interleave, such as two increments that read the same old value., the subject of the synchronisation module. Step through one collision:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- One page, two processes. mmap with MAP_SHARED and MAP_ANONYMOUS, called before fork, gives one zeroed page that both processes map after the fork. The counter in it has reached 41 at this moment.
- The parent loads. counter + 1 is three steps for the processor: load, add, store. The parent loads 41 into a register.
- The child loads. Before the parent stores its result, the child, running on another core, loads the counter too. It also gets 41.
- The parent stores. The parent adds 1 and stores 42.
- The child stores. The child adds 1 to the 41 it loaded and stores 42 as well, over the parent's 42. Two increments ran, and the counter grew by one.
- With the mutex. In the mutex round, the parent locks the process-shared mutex that lives in the same page, loads 42, stores 43 and unlocks. The child's lock call waits meanwhile.
- The child's turn. Now the child gets the lock, loads 43, stores 44 and unlocks. Both increments count, which is why the recorded mutex round reached exactly 10,000,000.
Two increments ran, and the counter grew by one, because both processes added 1 to the same 41. With hundreds of thousands of increments on two cores at once, such collisions are certain, not rare, which is why the program prints only that updates were lost, not how many: the number differs from run to run.
volatile does not make an update safe
The unlocked counter of shm_counter.c is volatile, and it still lost updates. volatile makes every load and store
happen, which is what a signal handler’s flag needs, but load, add and store remain three steps. Use an atomic
operation or a lock for data that two processes change.
Checkpoint
Check: Shared mappings and lost updates
A process-shared mutex, a semaphore or an atomic makes the updates safe
The two rounds that counted correctly used the two tools that work across processes on every system this lesson ran
on. atomic_fetch_add turns load, add and store into one indivisible operation; it is enough when the update is one
operation on one value. A pthread mutex in the shared page protects any number of steps, but only if it is made for
it: POSIX makes PTHREAD_PROCESS_PRIVATE the default, and PTHREAD_PROCESS_SHARED, set with
pthread_mutexattr_setpshared before pthread_mutex_init, lets “any thread that has access to the memory” use it.
The third classic tool, a A counter that processes or threads change with two indivisible operations: wait, which lowers it or blocks while it is 0, and post, which raises it and wakes a waiter., comes in two kinds: named ones made with sem_open,
and unnamed ones made with sem_init inside shared memory.
Not every system offers every option, and a header that declares a function does not prove that it works. This program
asks the system which parts of POSIX it claims, through the _POSIX_* constants of unistd.h, then tries each one:
#include <errno.h>
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
// What this system claims about four optional parts of POSIX (unistd.h: -1
// means not supported), and what happens when each one is tried.
int main(void) {
#if __has_include(<mqueue.h>)
printf("POSIX message queues: <mqueue.h> is there\n");
#else
printf("POSIX message queues: _POSIX_MESSAGE_PASSING %ld, and no "
"<mqueue.h> at all\n",
(long)_POSIX_MESSAGE_PASSING);
#endif
const int shm = shm_open("/lesson-probe", O_CREAT | O_RDWR, 0600);
printf("POSIX shared memory: _POSIX_SHARED_MEMORY_OBJECTS %ld, "
"shm_open %s\n",
(long)_POSIX_SHARED_MEMORY_OBJECTS, shm >= 0 ? "works" : "fails");
if (shm >= 0) {
close(shm);
shm_unlink("/lesson-probe");
}
sem_t unnamed;
errno = 0;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // as macOS marks it
const int made = sem_init(&unnamed, 1, 1);
#pragma GCC diagnostic pop
printf("unnamed semaphores: _POSIX_SEMAPHORES %ld, sem_init %s\n",
(long)_POSIX_SEMAPHORES,
made == 0 ? "works"
: errno == ENOSYS ? "fails with ENOSYS"
: "fails");
sem_t *named = sem_open("/lesson-probe-sem", O_CREAT, 0600, 1);
printf("named semaphores: sem_open %s\n",
named != SEM_FAILED ? "works" : "fails");
if (named != SEM_FAILED) {
sem_close(named);
sem_unlink("/lesson-probe-sem");
}
printf("process-shared mutexes: _POSIX_THREAD_PROCESS_SHARED %ld\n",
(long)_POSIX_THREAD_PROCESS_SHARED);
return 0;
} Output
POSIX message queues: _POSIX_MESSAGE_PASSING -1, and no <mqueue.h> at all POSIX shared memory: _POSIX_SHARED_MEMORY_OBJECTS -1, shm_open works unnamed semaphores: _POSIX_SEMAPHORES -1, sem_init fails with ENOSYS named semaphores: sem_open works process-shared mutexes: _POSIX_THREAD_PROCESS_SHARED 200112
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined posix_probe.c -o posix_probe && ./posix_probe
On this Mac, -1 means “not supported”, yet two of those options work: shm_open although
_POSIX_SHARED_MEMORY_OBJECTS is -1, and named semaphores although _POSIX_SEMAPHORES is -1. sem_init, by
contrast, is declared in the header but fails with ENOSYS, so portable code uses named semaphores or a process-shared
mutex, which macOS supports. There are no POSIX message queues at all, which the fourth section works around.
One danger is particular to locks shared between processes: a process can die while it holds one. Nobody is then
left to unlock an ordinary mutex, and the others wait for ever. POSIX’s robust mutexes, set with
pthread_mutexattr_setrobust, report the owner’s death to the next process that locks with the error EOWNERDEAD, so
it can repair the shared data before going on.
| Tool | Protects | Works across processes here |
|---|---|---|
atomic_fetch_add and other lock-free atomics |
One operation on one value | Yes, in shared memory |
A mutex with PTHREAD_PROCESS_SHARED |
Any number of steps | Yes, on macOS and Linux |
A named semaphore, sem_open |
Counts and turns | Yes, on macOS and Linux |
An unnamed semaphore, sem_init |
Counts and turns | Linux only; ENOSYS on macOS |
Key idea
Shared memory removes the copies but not the coordination: every change to shared data needs an atomic operation or a lock that both processes use, and the recorded run lost updates without one.
Checkpoint
Check: Mutexes, semaphores and atomics across processes
Named shared memory lets unrelated processes meet
An anonymous mapping reaches only processes forked after it was made. Unrelated programs meet through a name instead.
shm_open creates or opens a POSIX shared memory object by a name that starts with a slash, ftruncate gives it a
size, because “a newly created shared memory object has a length of zero”, shm_overview(7) says, and mmap maps it.
On Linux these objects live in a tmpfs file system, normally mounted at /dev/shm, and they outlive the programs that
use them until shm_unlink removes the name and every mapping is gone. This program creates an object, writes into it
and lets a reader find it by name; the reader was forked before the object existed, so it inherited nothing:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
enum { SIZE = 4096 };
int main(void) {
const char *name = "/lesson-orders";
shm_unlink(name); // in case an earlier run was stopped half way
int ready[2];
pipe(ready);
const pid_t reader = fork(); // forked first, so it inherits no mapping
if (reader == 0) {
char byte;
close(ready[1]);
if (read(ready[0], &byte, 1) != 1) { // wait until the object exists
_exit(1);
}
const int fd = shm_open(name, O_RDONLY, 0);
const char *view = mmap(NULL, SIZE, PROT_READ, MAP_SHARED, fd, 0);
printf("reader: opened %s by name and read \"%s\"\n", name, view);
fflush(stdout);
_exit(0);
}
close(ready[0]);
const int fd = shm_open(name, O_CREAT | O_EXCL | O_RDWR, 0600);
const int sized = ftruncate(fd, SIZE); // a new object is 0 bytes long
char *view = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
strcpy(view, "3 orders waiting");
printf("writer: created %s, sized it (%s) and wrote into it\n", name,
sized == 0 ? "ok" : "failed");
fflush(stdout);
if (write(ready[1], "x", 1) != 1) {
return 1;
}
waitpid(reader, NULL, 0);
errno = 0;
const int resized = ftruncate(fd, 2 * SIZE);
printf("a second ftruncate on this Mac: %s\n",
resized == 0 ? "allowed"
: errno == EINVAL ? "EINVAL, an object's size is set once"
: "another error");
char long_name[48];
snprintf(long_name, sizeof long_name, "/%040d", 7); // a 41-character name
errno = 0;
const int other = shm_open(long_name, O_CREAT | O_RDWR, 0600);
printf("a name of %zu characters: %s\n", strlen(long_name),
other >= 0 ? "accepted"
: errno == ENAMETOOLONG ? "ENAMETOOLONG"
: "another error");
if (other >= 0) {
shm_unlink(long_name);
}
munmap(view, SIZE);
close(fd);
shm_unlink(name);
errno = 0;
const int again = shm_open(name, O_RDONLY, 0);
printf("after shm_unlink, opening the name again: %s\n",
again == -1 && errno == ENOENT ? "ENOENT" : "it still exists");
return 0;
} Output
writer: created /lesson-orders, sized it (ok) and wrote into it reader: opened /lesson-orders by name and read "3 orders waiting" a second ftruncate on this Mac: EINVAL, an object's size is set once a name of 41 characters: ENAMETOOLONG after shm_unlink, opening the name again: ENOENT
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined shm_named.c -o shm_named && ./shm_named
The reader opened /lesson-orders by name and read what the writer had left there. Two lines are macOS’s own: a second
ftruncate failed with EINVAL, so an object’s size is chosen once, and a name of 41 characters was refused with
ENAMETOOLONG. Linux accepts both, so code that must run on both systems sizes each object once and keeps its names
short. After shm_unlink the name was gone, while a process that still had the memory mapped could have kept using it.
Linux also offers memfd_create, which “creates an anonymous file and returns a file descriptor that refers to it”:
memory with no name anywhere, shared by passing the descriptor over a Unix domain socket, as the previous lesson did
with a file. With seals, which the sender places and the receiver reads back with fcntl, the receiver can rely on a
buffer that the sender can no longer alter.
Checkpoint
Check: Named shared memory objects
Message queues deliver whole messages by priority
A message queue: A kernel object that holds whole messages until a receiver takes them. A POSIX queue delivers the oldest message of the highest priority first; a System V queue lets the receiver choose by type. sits between a pipe and shared memory: data is still copied through the kernel, but the kernel keeps
each message whole and orders them. A POSIX queue, opened by name with mq_open, holds at most a set number of
messages of at most a set size, and mq_receive takes “the oldest of the highest priority” messages, in POSIX’s words.
On Linux the defaults are 10 messages of 8,192 bytes, priorities run from 0 to 32,767, the queues appear under
/dev/mqueue once that file system is mounted, and a queue descriptor works with poll and epoll, mq_overview(7)
says. macOS has none of it, as the probe showed, so this model plays the rules, in 14 languages, with a queue of 4
messages of up to 16 bytes:
Python 3.14 · main.py
from dataclasses import dataclass, field
@dataclass
class Queue: # a POSIX message queue, opened with O_NONBLOCK
max_msg: int
max_size: int
messages: list[tuple[int, str]] = field(default_factory=list) # in sending order
def send(self, text: str, priority: int) -> str:
if len(text) > self.max_size:
return "EMSGSIZE: longer than the queue's message size"
if len(self.messages) == self.max_msg:
return "EAGAIN: the queue is full"
self.messages.append((priority, text))
return f"{len(self.messages)} queued"
def receive(self) -> str:
if not self.messages:
return "EAGAIN: the queue is empty"
best = 0 # the highest priority; the oldest of equal ones
for i in range(1, len(self.messages)):
if self.messages[i][0] > self.messages[best][0]:
best = i
priority, text = self.messages.pop(best)
return f'"{text}", priority {priority}'
queue = Queue(max_msg=4, max_size=16)
print("a queue of at most 4 messages of up to 16 bytes")
sends = [("job 1", 0), ("job 2", 0), ("disk full", 7), ("job 3", 0), ("job 4", 0)]
sends.append(("a report that is far too long", 0))
for text, priority in sends:
print(f'send "{text}", priority {priority}: {queue.send(text, priority)}')
for _ in range(5):
print(f"receive: {queue.receive()}") In Python: max_msg and max_size come first in the dataclass because they have no defaults, and messages.pop(best) removes the chosen message and returns it.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
// A POSIX message queue, opened with O_NONBLOCK.
class Queue {
messages = []; // [priority, text], in sending order
constructor(maxMsg, maxSize) {
this.maxMsg = maxMsg;
this.maxSize = maxSize;
}
send(text, priority) {
if (text.length > this.maxSize) return "EMSGSIZE: longer than the queue's message size";
if (this.messages.length === this.maxMsg) return "EAGAIN: the queue is full";
this.messages.push([priority, text]);
return `${this.messages.length} queued`;
}
receive() {
if (this.messages.length === 0) return "EAGAIN: the queue is empty";
let best = 0; // the highest priority; the oldest of equal ones
for (let i = 1; i < this.messages.length; i++) {
if (this.messages[i][0] > this.messages[best][0]) best = i;
}
const [[priority, text]] = this.messages.splice(best, 1);
return `"${text}", priority ${priority}`;
}
}
const queue = new Queue(4, 16);
console.log("a queue of at most 4 messages of up to 16 bytes");
const sends = [
["job 1", 0],
["job 2", 0],
["disk full", 7],
["job 3", 0],
["job 4", 0],
["a report that is far too long", 0],
];
for (const [text, priority] of sends) {
console.log(`send "${text}", priority ${priority}: ${queue.send(text, priority)}`);
}
for (let i = 0; i < 5; i++) console.log(`receive: ${queue.receive()}`); In JavaScript: splice(best, 1) removes the chosen message and returns it inside an array, which the pattern [[priority, text]] unpacks.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.ArrayList;
import java.util.List;
public class Main {
record Message(int priority, String text) {}
/** A POSIX message queue, opened with O_NONBLOCK. */
static final class Queue {
final int maxMsg;
final int maxSize;
final List<Message> messages = new ArrayList<>(); // in sending order
Queue(int maxMsg, int maxSize) {
this.maxMsg = maxMsg;
this.maxSize = maxSize;
}
String send(String text, int priority) {
if (text.length() > maxSize) {
return "EMSGSIZE: longer than the queue's message size";
}
if (messages.size() == maxMsg) {
return "EAGAIN: the queue is full";
}
messages.add(new Message(priority, text));
return messages.size() + " queued";
}
String receive() {
if (messages.isEmpty()) {
return "EAGAIN: the queue is empty";
}
int best = 0; // the highest priority; the oldest of equal ones
for (int i = 1; i < messages.size(); i++) {
if (messages.get(i).priority() > messages.get(best).priority()) {
best = i;
}
}
Message next = messages.remove(best);
return "\"" + next.text() + "\", priority " + next.priority();
}
}
public static void main(String[] args) {
Queue queue = new Queue(4, 16);
System.out.println("a queue of at most 4 messages of up to 16 bytes");
List<Message> sends =
List.of(
new Message(0, "job 1"),
new Message(0, "job 2"),
new Message(7, "disk full"),
new Message(0, "job 3"),
new Message(0, "job 4"),
new Message(0, "a report that is far too long"));
for (Message m : sends) {
String result = queue.send(m.text(), m.priority());
System.out.println("send \"" + m.text() + "\", priority " + m.priority() + ": " + result);
}
for (int i = 0; i < 5; i++) {
System.out.println("receive: " + queue.receive());
}
}
} In Java: Message is a record and List.of builds the sends; messages.remove(best) with an int argument removes by position.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <format>
#include <print>
#include <string>
#include <vector>
struct Message {
int priority;
std::string text;
};
// A POSIX message queue, opened with O_NONBLOCK.
class Queue {
public:
Queue(std::size_t max_msg, std::size_t max_size)
: max_msg_(max_msg), max_size_(max_size) {}
std::string send(const std::string &text, int priority) {
if (text.size() > max_size_)
return "EMSGSIZE: longer than the queue's message size";
if (messages_.size() == max_msg_)
return "EAGAIN: the queue is full";
messages_.push_back({priority, text});
return std::format("{} queued", messages_.size());
}
std::string receive() {
if (messages_.empty())
return "EAGAIN: the queue is empty";
std::size_t best = 0; // the highest priority; the oldest of equal ones
for (std::size_t i = 1; i < messages_.size(); ++i)
if (messages_[i].priority > messages_[best].priority)
best = i;
const Message next = messages_[best];
messages_.erase(messages_.begin() + static_cast<std::ptrdiff_t>(best));
return std::format("\"{}\", priority {}", next.text, next.priority);
}
private:
std::size_t max_msg_;
std::size_t max_size_;
std::vector<Message> messages_; // in sending order
};
int main() {
Queue queue(4, 16);
std::println("a queue of at most 4 messages of up to 16 bytes");
const std::vector<Message> sends = {
{0, "job 1"}, {0, "job 2"}, {7, "disk full"},
{0, "job 3"}, {0, "job 4"}, {0, "a report that is far too long"}};
for (const auto &[priority, text] : sends)
std::println("send \"{}\", priority {}: {}", text, priority,
queue.send(text, priority));
for (int i = 0; i < 5; ++i)
std::println("receive: {}", queue.receive());
} In C++: The chosen message is copied out before erase moves the later ones into its place, and the structured binding names the two fields of each send.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
var queue = new MessageQueue(4, 16);
Console.WriteLine("a queue of at most 4 messages of up to 16 bytes");
(string Text, int Priority)[] sends =
[
("job 1", 0), ("job 2", 0), ("disk full", 7), ("job 3", 0), ("job 4", 0),
("a report that is far too long", 0),
];
foreach (var (text, priority) in sends)
{
Console.WriteLine($"send \"{text}\", priority {priority}: {queue.Send(text, priority)}");
}
for (var i = 0; i < 5; i++)
{
Console.WriteLine($"receive: {queue.Receive()}");
}
// A POSIX message queue, opened with O_NONBLOCK.
sealed class MessageQueue(int maxMsg, int maxSize)
{
private readonly List<(int Priority, string Text)> messages = []; // in sending order
public string Send(string text, int priority)
{
if (text.Length > maxSize)
{
return "EMSGSIZE: longer than the queue's message size";
}
if (messages.Count == maxMsg)
{
return "EAGAIN: the queue is full";
}
messages.Add((priority, text));
return $"{messages.Count} queued";
}
public string Receive()
{
if (messages.Count == 0)
{
return "EAGAIN: the queue is empty";
}
var best = 0; // the highest priority; the oldest of equal ones
for (var i = 1; i < messages.Count; i++)
{
if (messages[i].Priority > messages[best].Priority)
{
best = i;
}
}
var (priority, text) = messages[best];
messages.RemoveAt(best);
return $"\"{text}\", priority {priority}";
}
} In C#: The primary constructor keeps maxMsg and maxSize in scope in every member, and the messages are named tuples, taken apart with var (priority, text).
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <stdio.h>
#include <string.h>
typedef struct {
int priority;
const char *text;
} Message;
// A POSIX message queue, opened with O_NONBLOCK.
typedef struct {
int max_msg, max_size;
Message messages[8]; // in sending order
int count;
} Queue;
static char result[96];
static const char *send_message(Queue *q, const char *text, int priority) {
if ((int)strlen(text) > q->max_size) {
return "EMSGSIZE: longer than the queue's message size";
}
if (q->count == q->max_msg) {
return "EAGAIN: the queue is full";
}
q->messages[q->count++] = (Message){priority, text};
snprintf(result, sizeof result, "%d queued", q->count);
return result;
}
static const char *receive_message(Queue *q) {
if (q->count == 0) {
return "EAGAIN: the queue is empty";
}
int best = 0; // the highest priority; the oldest of equal ones
for (int i = 1; i < q->count; i++) {
if (q->messages[i].priority > q->messages[best].priority) {
best = i;
}
}
const Message next = q->messages[best];
q->count--;
memmove(&q->messages[best], &q->messages[best + 1],
(size_t)(q->count - best) * sizeof(Message));
snprintf(result, sizeof result, "\"%s\", priority %d", next.text,
next.priority);
return result;
}
int main(void) {
Queue queue = {.max_msg = 4, .max_size = 16};
printf("a queue of at most 4 messages of up to 16 bytes\n");
const Message sends[] = {
{0, "job 1"}, {0, "job 2"}, {7, "disk full"},
{0, "job 3"}, {0, "job 4"}, {0, "a report that is far too long"}};
for (size_t i = 0; i < sizeof sends / sizeof sends[0]; i++) {
printf("send \"%s\", priority %d: ", sends[i].text, sends[i].priority);
printf("%s\n", send_message(&queue, sends[i].text, sends[i].priority));
}
for (int i = 0; i < 5; i++) {
printf("receive: %s\n", receive_message(&queue));
}
return 0;
} In C: The functions are send_message and receive_message, not mq_send and mq_receive, which <mqueue.h> declares on Linux; memmove closes the gap a message leaves.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
// A POSIX message queue, opened with O_NONBLOCK.
final class MessageQueue
{
/** @var list<array{int, string}> [priority, text], in sending order */
private array $messages = [];
public function __construct(private readonly int $maxMsg, private readonly int $maxSize) {}
public function send(string $text, int $priority): string
{
if (strlen($text) > $this->maxSize) {
return "EMSGSIZE: longer than the queue's message size";
}
if (count($this->messages) === $this->maxMsg) {
return 'EAGAIN: the queue is full';
}
$this->messages[] = [$priority, $text];
return count($this->messages) . ' queued';
}
public function receive(): string
{
if ($this->messages === []) {
return 'EAGAIN: the queue is empty';
}
$best = 0; // the highest priority; the oldest of equal ones
for ($i = 1; $i < count($this->messages); $i++) {
if ($this->messages[$i][0] > $this->messages[$best][0]) {
$best = $i;
}
}
[[$priority, $text]] = array_splice($this->messages, $best, 1);
return "\"{$text}\", priority {$priority}";
}
}
$queue = new MessageQueue(4, 16);
echo "a queue of at most 4 messages of up to 16 bytes\n";
$sends = [['job 1', 0], ['job 2', 0], ['disk full', 7], ['job 3', 0], ['job 4', 0]];
$sends[] = ['a report that is far too long', 0];
foreach ($sends as [$text, $priority]) {
echo "send \"{$text}\", priority {$priority}: {$queue->send($text, $priority)}\n";
}
for ($i = 0; $i < 5; $i++) {
echo "receive: {$queue->receive()}\n";
} In PHP: The promoted constructor parameters declare and set both limits, and array_splice removes the chosen message and numbers the list from 0 again.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
/// A POSIX message queue, opened with O_NONBLOCK.
struct Queue {
max_msg: usize,
max_size: usize,
messages: Vec<(u32, &'static str)>, // (priority, text), in sending order
}
impl Queue {
fn send(&mut self, text: &'static str, priority: u32) -> Result<String, &'static str> {
if text.len() > self.max_size {
return Err("EMSGSIZE: longer than the queue's message size");
}
if self.messages.len() == self.max_msg {
return Err("EAGAIN: the queue is full");
}
self.messages.push((priority, text));
Ok(format!("{} queued", self.messages.len()))
}
fn receive(&mut self) -> Result<String, &'static str> {
if self.messages.is_empty() {
return Err("EAGAIN: the queue is empty");
}
let mut best = 0; // the highest priority; the oldest of equal ones
for i in 1..self.messages.len() {
if self.messages[i].0 > self.messages[best].0 {
best = i;
}
}
let (priority, text) = self.messages.remove(best);
Ok(format!("\"{text}\", priority {priority}"))
}
}
fn main() {
let mut queue = Queue {
max_msg: 4,
max_size: 16,
messages: Vec::new(),
};
println!("a queue of at most 4 messages of up to 16 bytes");
let sends = [
("job 1", 0),
("job 2", 0),
("disk full", 7),
("job 3", 0),
("job 4", 0),
("a report that is far too long", 0),
];
for (text, priority) in sends {
let result = queue.send(text, priority).unwrap_or_else(String::from);
println!("send \"{text}\", priority {priority}: {result}");
}
for _ in 0..5 {
println!("receive: {}", queue.receive().unwrap_or_else(String::from));
}
} In Rust: send and receive return Result<String, &str>, which keeps an error apart from a success, and unwrap_or_else(String::from) prints whichever it is.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
// A POSIX message queue, opened with O_NONBLOCK.
class Queue {
readonly maxMsg: number;
readonly maxSize: number;
messages: [number, string][] = []; // [priority, text], in sending order
constructor(maxMsg: number, maxSize: number) {
this.maxMsg = maxMsg;
this.maxSize = maxSize;
}
send(text: string, priority: number): string {
if (text.length > this.maxSize) return "EMSGSIZE: longer than the queue's message size";
if (this.messages.length === this.maxMsg) return "EAGAIN: the queue is full";
this.messages.push([priority, text]);
return `${this.messages.length} queued`;
}
receive(): string {
if (this.messages.length === 0) return "EAGAIN: the queue is empty";
let best = 0; // the highest priority; the oldest of equal ones
for (let i = 1; i < this.messages.length; i++) {
if (this.messages[i][0] > this.messages[best][0]) best = i;
}
const [[priority, text]] = this.messages.splice(best, 1);
return `"${text}", priority ${priority}`;
}
}
const queue = new Queue(4, 16);
console.log("a queue of at most 4 messages of up to 16 bytes");
const sends: [string, number][] = [
["job 1", 0],
["job 2", 0],
["disk full", 7],
["job 3", 0],
["job 4", 0],
["a report that is far too long", 0],
];
for (const [text, priority] of sends) {
console.log(`send "${text}", priority ${priority}: ${queue.send(text, priority)}`);
}
for (let i = 0; i < 5; i++) console.log(`receive: ${queue.receive()}`); In TypeScript: messages has the type [number, string][], a list of pairs; the rest is the JavaScript version.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import (
"errors"
"fmt"
"slices"
)
type message struct {
priority int
text string
}
// queue is a POSIX message queue, opened with O_NONBLOCK.
type queue struct {
maxMsg, maxSize int
messages []message // in sending order
}
func (q *queue) send(text string, priority int) (string, error) {
if len(text) > q.maxSize {
return "", errors.New("EMSGSIZE: longer than the queue's message size")
}
if len(q.messages) == q.maxMsg {
return "", errors.New("EAGAIN: the queue is full")
}
q.messages = append(q.messages, message{priority, text})
return fmt.Sprintf("%d queued", len(q.messages)), nil
}
func (q *queue) receive() (string, error) {
if len(q.messages) == 0 {
return "", errors.New("EAGAIN: the queue is empty")
}
best := 0 // the highest priority; the oldest of equal ones
for i := 1; i < len(q.messages); i++ {
if q.messages[i].priority > q.messages[best].priority {
best = i
}
}
next := q.messages[best]
q.messages = slices.Delete(q.messages, best, best+1)
return fmt.Sprintf("%q, priority %d", next.text, next.priority), nil
}
func either(result string, err error) string {
if err != nil {
return err.Error()
}
return result
}
func main() {
q := &queue{maxMsg: 4, maxSize: 16}
fmt.Println("a queue of at most 4 messages of up to 16 bytes")
sends := []message{{0, "job 1"}, {0, "job 2"}, {7, "disk full"}, {0, "job 3"}, {0, "job 4"},
{0, "a report that is far too long"}}
for _, m := range sends {
result := either(q.send(m.text, m.priority))
fmt.Printf("send %q, priority %d: %s\n", m.text, m.priority, result)
}
for range 5 {
fmt.Printf("receive: %s\n", either(q.receive()))
}
} In Go: send and receive return a result and an error, Go's way of reporting a failure, and either picks the one to print.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
struct Message {
let priority: Int
let text: String
}
// A POSIX message queue, opened with O_NONBLOCK.
struct MessageQueue {
let maxMsg: Int
let maxSize: Int
var messages: [Message] = [] // in sending order
mutating func send(_ text: String, priority: Int) -> String {
guard text.utf8.count <= maxSize else {
return "EMSGSIZE: longer than the queue's message size"
}
guard messages.count < maxMsg else { return "EAGAIN: the queue is full" }
messages.append(Message(priority: priority, text: text))
return "\(messages.count) queued"
}
mutating func receive() -> String {
guard !messages.isEmpty else { return "EAGAIN: the queue is empty" }
var best = 0 // the highest priority; the oldest of equal ones
for i in 1..<messages.count where messages[i].priority > messages[best].priority {
best = i
}
let next = messages.remove(at: best)
return "\"\(next.text)\", priority \(next.priority)"
}
}
var queue = MessageQueue(maxMsg: 4, maxSize: 16)
print("a queue of at most 4 messages of up to 16 bytes")
let sends: [(String, Int)] = [
("job 1", 0), ("job 2", 0), ("disk full", 7), ("job 3", 0), ("job 4", 0),
("a report that is far too long", 0),
]
for (text, priority) in sends {
print("send \"\(text)\", priority \(priority): \(queue.send(text, priority: priority))")
}
for _ in 0..<5 {
print("receive: \(queue.receive())")
} In Swift: guard states each limit as the condition to go on, and text.utf8.count measures the message in bytes, as mq_send does.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
Message = Struct.new(:priority, :text)
# A POSIX message queue, opened with O_NONBLOCK.
class MessageQueue
def initialize(max_msg, max_size)
@max_msg = max_msg
@max_size = max_size
@messages = [] # in sending order
end
def send_message(text, priority)
return "EMSGSIZE: longer than the queue's message size" if text.bytesize > @max_size
return 'EAGAIN: the queue is full' if @messages.size == @max_msg
@messages << Message.new(priority, text)
"#{@messages.size} queued"
end
def receive
return 'EAGAIN: the queue is empty' if @messages.empty?
best = 0 # the highest priority; the oldest of equal ones
(1...@messages.size).each do |index|
best = index if @messages[index].priority > @messages[best].priority
end
message = @messages.delete_at(best)
"\"#{message.text}\", priority #{message.priority}"
end
end
queue = MessageQueue.new(4, 16)
puts 'a queue of at most 4 messages of up to 16 bytes'
sends = [['job 1', 0], ['job 2', 0], ['disk full', 7], ['job 3', 0], ['job 4', 0]]
sends << ['a report that is far too long', 0]
sends.each do |text, priority|
puts "send \"#{text}\", priority #{priority}: #{queue.send_message(text, priority)}"
end
5.times { puts "receive: #{queue.receive}" } In Ruby: The method is send_message, because send already exists on every Ruby object, where it calls a method by its name.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
data class Message(
val priority: Int,
val text: String,
)
// A POSIX message queue, opened with O_NONBLOCK.
class MessageQueue(
private val maxMsg: Int,
private val maxSize: Int,
) {
// In sending order.
private val messages = mutableListOf<Message>()
fun send(
text: String,
priority: Int,
): String {
if (text.length > maxSize) return "EMSGSIZE: longer than the queue's message size"
if (messages.size == maxMsg) return "EAGAIN: the queue is full"
messages.add(Message(priority, text))
return "${messages.size} queued"
}
fun receive(): String {
if (messages.isEmpty()) return "EAGAIN: the queue is empty"
// The highest priority; the oldest of equal ones.
var best = 0
for (i in 1 until messages.size) {
if (messages[i].priority > messages[best].priority) best = i
}
val (priority, text) = messages.removeAt(best)
return "\"$text\", priority $priority"
}
}
fun main() {
val queue = MessageQueue(4, 16)
println("a queue of at most 4 messages of up to 16 bytes")
val sends =
listOf(
"job 1" to 0,
"job 2" to 0,
"disk full" to 7,
"job 3" to 0,
"job 4" to 0,
"a report that is far too long" to 0,
)
for ((text, priority) in sends) {
println("send \"$text\", priority $priority: ${queue.send(text, priority)}")
}
repeat(5) { println("receive: ${queue.receive()}") }
} In Kotlin: The sends are pairs built with to, and destructuring takes each pair and each removed Message apart.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
typedef Message = ({int priority, String text});
// A POSIX message queue, opened with O_NONBLOCK.
class MessageQueue {
MessageQueue(this.maxMsg, this.maxSize);
final int maxMsg;
final int maxSize;
final List<Message> _messages = []; // in sending order
String send(String text, int priority) {
if (text.length > maxSize) {
return "EMSGSIZE: longer than the queue's message size";
}
if (_messages.length == maxMsg) {
return 'EAGAIN: the queue is full';
}
_messages.add((priority: priority, text: text));
return '${_messages.length} queued';
}
String receive() {
if (_messages.isEmpty) {
return 'EAGAIN: the queue is empty';
}
var best = 0; // the highest priority; the oldest of equal ones
for (var i = 1; i < _messages.length; i++) {
if (_messages[i].priority > _messages[best].priority) {
best = i;
}
}
final (:priority, :text) = _messages.removeAt(best);
return '"$text", priority $priority';
}
}
void main() {
final queue = MessageQueue(4, 16);
print('a queue of at most 4 messages of up to 16 bytes');
const sends = [
('job 1', 0),
('job 2', 0),
('disk full', 7),
('job 3', 0),
('job 4', 0),
('a report that is far too long', 0),
];
for (final (text, priority) in sends) {
print('send "$text", priority $priority: ${queue.send(text, priority)}');
}
for (var i = 0; i < 5; i++) {
print('receive: ${queue.receive()}');
}
} In Dart: Message is a record type, and the pattern final (:priority, :text) takes the fields out of the record that removeAt returns.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
a queue of at most 4 messages of up to 16 bytes send "job 1", priority 0: 1 queued send "job 2", priority 0: 2 queued send "disk full", priority 7: 3 queued send "job 3", priority 0: 4 queued send "job 4", priority 0: EAGAIN: the queue is full send "a report that is far too long", priority 0: EMSGSIZE: longer than the queue's message size receive: "disk full", priority 7 receive: "job 1", priority 0 receive: "job 2", priority 0 receive: "job 3", priority 0 receive: EAGAIN: the queue is empty
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 10 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 153 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 3 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 20 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 717 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 182 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 13 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
The message with priority 7 overtook the two jobs sent before it, and the jobs, all priority 0, kept their order.
The fifth send, job 4, found the queue full and got EAGAIN, as mq_send does with O_NONBLOCK, and the long report
was refused with EMSGSIZE before the queue’s fullness mattered.
System V IPC: The older family of Unix IPC: message queues, semaphore sets and shared memory segments made with msgget, semget and shmget. Its objects outlive their programs until removed., the older family of message queues, semaphore sets and shared memory segments in sysvipc(7), works on
macOS too. Its queues identify messages by a positive type instead of a priority, and msgrcv chooses by it: type 0
takes the first message of any type, a positive type the first message of that type:
#include <errno.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <unistd.h>
// A System V message: a positive type first, which msgrcv can select by.
typedef struct {
long type;
char text[32];
} Message;
int main(void) {
const int queue = msgget(IPC_PRIVATE, IPC_CREAT | 0600);
const pid_t sender = fork();
if (sender == 0) {
const Message sent[] = {{1, "job 1"}, {2, "disk full"}, {1, "job 2"}};
for (int i = 0; i < 3; i++) {
msgsnd(queue, &sent[i], sizeof sent[i].text, 0);
}
_exit(0);
}
waitpid(sender, NULL, 0);
Message got;
msgrcv(queue, &got, sizeof got.text, 2, 0); // the first message of type 2
printf("msgrcv asking for type 2: \"%s\"\n", got.text);
while (msgrcv(queue, &got, sizeof got.text, 0, IPC_NOWAIT) >= 0) {
printf("msgrcv asking for any type: type %ld, \"%s\"\n", got.type,
got.text);
}
printf("then: %s\n",
errno == ENOMSG ? "ENOMSG, the queue is empty" : "another error");
msgctl(queue, IPC_RMID, NULL); // it would outlive this program otherwise
errno = 0;
printf("after IPC_RMID, msgsnd: %s\n",
msgsnd(queue, &got, sizeof got.text, IPC_NOWAIT) == -1 &&
(errno == EINVAL || errno == EIDRM)
? "the queue is gone"
: "it still works");
return 0;
} Output
msgrcv asking for type 2: "disk full" msgrcv asking for any type: type 1, "job 1" msgrcv asking for any type: type 1, "job 2" then: ENOMSG, the queue is empty after IPC_RMID, msgsnd: the queue is gone
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined sysv_queue.c -o sysv_queue && ./sysv_queue
Asked for type 2, msgrcv skipped the job sent before it and returned disk full; asked for any type, it returned
the jobs in the order sent, then ENOMSG once the queue was empty. System V objects outlive the programs that make them
until msgctl with IPC_RMID removes them, so the program removed its queue; the ipcs and ipcrm commands list and
remove leftovers. New code usually prefers the POSIX calls or a socket, but System V IPC remains in older systems and
in exam syllabi.
Checkpoint
Check: POSIX and System V message queues
Pick the mechanism by what the data needs
Every mechanism of this module answers the same question differently: how data gets from one process to another, and what the program must then take care of. Pipes, sockets and queues copy each message into the kernel and out again, but the kernel orders it; shared memory copies nothing and orders nothing. Larger systems build on the same pieces: Android’s Binder lets a process call functions in another, and its kernel driver copies each transaction into the receiving process’s address space. Answer the four questions in the table for a design of your own:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Mechanism | Copies of the data | Unrelated processes | Whole messages | Linux and macOS | Locking left to you |
|---|---|---|---|---|---|
| Pipe | Mixed two: into the kernel, then out again | Poor only processes that inherit it | Poor a byte stream; records over PIPE_BUF can mix | Good every POSIX system | Good none; the kernel keeps the bytes in order |
| Unix domain socket | Mixed two: into the kernel, then out again | Good any process allowed to connect | Good datagram sockets keep each message whole | Good Linux and macOS alike | Good none; each connection is ordered |
| Shared memory and a lock | Good none: both processes map the same pages | Good by name with shm_open, or a passed descriptor | Mixed whatever layout you design in the pages | Good shm_open works on both; macOS names are short | Poor yours to add: the unlocked run lost updates |
| POSIX message queue | Mixed two: into the kernel, then out again | Good by name with mq_open | Good whole messages, highest priority first | Poor Linux: macOS has no mq_open at all | Good none; the kernel orders the messages |
| System V message queue | Mixed two: into the kernel, then out again | Good by key with msgget | Good whole messages, chosen by type | Mixed both, but an older API; queues outlive programs | Good none; the kernel orders the messages |
How the answers decide
- “Are the processes parent and child, or unrelated programs?” Unrelated programs: rules out Pipe. A pipe reaches only processes that inherit its descriptors; unrelated programs need a name or a socket.
- “Do you move large data, megabytes many times a second?” Yes, large and fast: adds a caveat to Pipe, Unix domain socket, POSIX message queue and System V message queue. Each message is copied into the kernel and out again; at hundreds of megabytes a second, shared memory saves both copies.
- “Do you move large data, megabytes many times a second?” No, small messages: adds a caveat to Shared memory and a lock. For small messages, the layout and locking that shared memory needs gain little; a socket is simpler and safe by default.
- “Must it run on macOS as well as Linux?” Yes: rules out POSIX message queue. macOS has no POSIX message queues: the probe found _POSIX_MESSAGE_PASSING -1 and no <mqueue.h>.
- “Does the reader need whole messages or priorities?” Yes, whole messages: adds a caveat to Pipe. A pipe is a byte stream, so records over PIPE_BUF can mix; frame them, keep them small, or use a datagram socket.
In short: a pipe for a parent and its children, a Unix domain socket for a local service with several clients, shared memory with a lock or atomics when copying is the bottleneck, and a message queue for prioritised messages on Linux, or a System V queue where both systems must run it.
Checkpoint
Check: Choosing an IPC mechanism
Practise: predict what a message queue delivers
This exercise turns the queue’s two rules, a full queue refuses a send and the oldest message of the highest priority leaves first, into a function. Its sample tests run in your browser, one of them on the model’s own five messages:
Exercise · Easy · Python
Predict the order a message queue delivers
A POSIX message queue opened with O_NONBLOCK holds at most capacity messages. A sender sends every message of sends, a list of (text, priority) pairs, in order; a send that finds the queue full is refused with EAGAIN, and that message is lost. Only then does a receiver call mq_receive until the queue is empty.
Write receive_order(sends, capacity) in order.py. It returns the texts in the order the receiver gets them: the highest priority first, and among messages of equal priority the one sent first, as POSIX specifies.
Starter code · order.py
def receive_order(sends, capacity):
"""The texts in the order mq_receive returns them, after every send of sends."""
# Replace this line with your code.
return [] The sample tests · test_order.py
from order import receive_order
def test_one_priority():
"""keeps the sending order when every priority is the same"""
assert receive_order([("a", 0), ("b", 0), ("c", 0)], 4) == ["a", "b", "c"]
def test_highest_priority_first():
"""returns the highest priority first"""
assert receive_order([("a", 0), ("b", 5), ("c", 1)], 4) == ["b", "c", "a"]
def test_oldest_of_equal_priorities():
"""returns the oldest message among those of equal priority"""
assert receive_order([("a", 3), ("b", 1), ("c", 3)], 4) == ["a", "c", "b"]
def test_full_queue_refuses():
"""refuses sends to a full queue, whatever their priority"""
assert receive_order([("a", 0), ("b", 0), ("c", 9)], 2) == ["a", "b"]
def test_the_lesson_model():
"""delivers the lesson's model sends in its recorded order"""
sends = [("job 1", 0), ("job 2", 0), ("disk full", 7), ("job 3", 0), ("job 4", 0)]
assert receive_order(sends, 4) == ["disk full", "job 1", "job 2", "job 3"] A hint
Keep the accepted messages in the order they were sent. To receive, scan for the highest priority and take the first message that has it; a strict comparison while you scan keeps the oldest of equal priorities.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Last, a self-check over the five sections. It opens with the idea that your answers on this page show to be the least secure:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
One warm-up, answered in full, comes first:
Two harder questions come from this lesson’s material: a counter in shared memory that reports fewer requests than the log, and a video pipeline that must move hundreds of megabytes a second between two processes. Start each one here, and read why every choice holds or misses:
Checkpoint
Your first decision
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Senior to staff
A capture process produces 1920 by 1080 frames of 4 bytes per pixel at 60 frames a second, and an encoder process on the same machine consumes them. Estimate the data rate, choose the IPC, and explain how the processes coordinate so that the encoder never reads a half-written frame.
Key takeaways
- A MAP_SHARED mapping puts the same physical pages into two processes, so data needs no copy, but nothing orders their updates: the unlocked counter of the recorded run lost updates.
- An atomic operation protects one update of one value; a mutex with PTHREAD_PROCESS_SHARED or a named semaphore protects longer updates; on macOS sem_init fails with ENOSYS, so portable code avoids unnamed semaphores.
- shm_open, ftruncate and mmap give shared memory a name for unrelated processes; the object starts at 0 bytes and outlives its programs until shm_unlink, and macOS sizes it once and wants short names.
- A POSIX message queue delivers whole messages, the oldest of the highest priority first, but macOS lacks it; System V queues choose messages by type and outlive programs until IPC_RMID removes them.
- Choose by the data: pipes and sockets copy but synchronise for you, shared memory copies nothing and leaves the locking to you, and queues add priorities or types.
Terms in this lesson
- message queue
- A kernel object that holds whole messages until a receiver takes them. A POSIX queue delivers the oldest message of the highest priority first; a System V queue lets the receiver choose by type.
- shared memory
- Memory that two or more processes map at once, so that a byte one writes is there for the others without any copy. The processes must coordinate their updates themselves.
- System V IPC
- The older family of Unix IPC: message queues, semaphore sets and shared memory segments made with msgget, semget and shmget. Its objects outlive their programs until removed.
References
- mmap(2): map files or devices into memory (MAP_SHARED, MAP_PRIVATE, MAP_ANONYMOUS) (Linux man-pages project (man7.org))
- shm_overview(7): overview of POSIX shared memory (Linux man-pages project (man7.org))
- POSIX.1-2024: shm_open, open a shared memory object (The Open Group and IEEE)
- POSIX.1-2024: pthread_mutexattr_getpshared, pthread_mutexattr_setpshared (The Open Group and IEEE)
- POSIX.1-2024: pthread_mutexattr_getrobust, pthread_mutexattr_setrobust (The Open Group and IEEE)
- mq_overview(7): overview of POSIX message queues (Linux man-pages project (man7.org))
- POSIX.1-2024: mq_receive, receive a message from a message queue (The Open Group and IEEE)
- POSIX.1-2024: mq_send, send a message to a message queue (The Open Group and IEEE)
- POSIX.1-2024: msgrcv, XSI message receive operation (The Open Group and IEEE)
- sysvipc(7): System V interprocess communication mechanisms (Linux man-pages project (man7.org))
- memfd_create(2): create an anonymous file (Linux man-pages project (man7.org))
- Binder overview (Android Open Source Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress