Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Clang 21.0.0 and GCC 16.2.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Five million increments in each of two processes, three ways C · shm_counter.c
#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:

Interactive · message by message Two increments of one shared counter, and the one that is lost Step through one moment of shm_counter.c, when both processes add 1 to the counter at the same time.
Two increments of one shared counter, and the one that is lost, step 7 of 7. Messages so far: Shared page to Parent: load: 41; Shared page to Child: load: 41; Parent to Shared page: store 42; Child to Shared page: store 42; Parent to Shared page: lock, 42 to 43, unlock; Child to Shared page: lock, 43 to 44, unlock.Parentadds 1 in a loopShared pagecounter = 44Childadds 1 in a loopload: 41load: 41store 42store 42lock, 42 to 43, unlocklock, 43 to 44, unlock

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. The parent loads. counter + 1 is three steps for the processor: load, add, store. The parent loads 41 into a register.
  3. The child loads. Before the parent stores its result, the child, running on another core, loads the counter too. It also gets 41.
  4. The parent stores. The parent adds 1 and stores 42.
  5. 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.
  6. 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.
  7. 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

  1. Question 1 of 2 A parent maps a page with MAP_PRIVATE before fork, and the child writes to it. What does the parent see?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its own old value, because the child's write went to a private copy of the page

    The counter of shm_counter.c is in a MAP_SHARED page, which is why both processes' increments reach the same memory, and why they can collide.

  2. Question 2 of 2 In the scene, the counter was 41, both processes loaded it, and both stored their result. What was the counter afterwards?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 42: one of the two increments was lost

    Repeated five million times in each process, such collisions cost the unlocked round of shm_counter.c a large share of its increments, while the atomic and the mutex rounds counted every one.

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:

What this Mac claims, and what each call does C · posix_probe.c
#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

  1. Question 1 of 2 What must a program set before a pthread mutex in shared memory can be used by two processes?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The PTHREAD_PROCESS_SHARED attribute, with pthread_mutexattr_setpshared, before pthread_mutex_init

    shm_counter.c set the attribute, initialised the mutex inside the shared page, and its mutex round counted all 10,000,000 increments.

  2. Question 2 of 2 A program written on Linux uses sem_init with pshared set to 1 for a semaphore in shared memory. What happens when it runs on this lesson's Mac?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: sem_init fails with ENOSYS, because macOS does not implement unnamed semaphores; use sem_open or a process-shared mutex

    Checking the return value of every call matters most for these optional parts of POSIX, which a system may lack while still declaring the function.

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:

A shared memory object found by its name C · shm_named.c
#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

  1. Question 1 of 2 A program creates a new object with shm_open and maps 4,096 bytes of it right away, without ftruncate. What goes wrong?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The new object is 0 bytes long, so touching the mapping fails; ftruncate must give it its size first

    shm_named.c called ftruncate(fd, 4096) before mapping. On this Mac a second ftruncate then failed with EINVAL, so the size is chosen once.

  2. Question 2 of 2 A writer calls shm_unlink on its object while a reader still has it mapped. What happens?
    Show the answer: Question 2 of 2

    Answer: The name disappears at once, but the memory lives until the last process unmaps it

    shm_named.c removed its name at the end, and the next shm_open of that name failed with ENOENT, the same rule as for a file that is unlinked while open.

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:

One program · 14 languages A POSIX message queue, in a model

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

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:

A System V queue: messages chosen by type C · sysv_queue.c
#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

  1. Question 1 of 2 Messages are sent to an empty POSIX queue in this order with these priorities - a (1), b (4), c (4), d (2). In which order does mq_receive return them?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: b, c, d, a

    mq_receive removes the oldest of the highest priority messages, as the model did with disk full, priority 7, ahead of the jobs it was sent after.

  2. Question 2 of 2 A System V queue holds, in order, messages of type 1, 2 and 1. A program calls msgrcv with msgtyp 2, then twice with msgtyp 0. What does it get?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The type-2 message, then the two type-1 messages in the order they were sent

    sysv_queue.c recorded exactly this on macOS: disk full first, then job 1 and job 2, then ENOMSG for the empty queue.

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:

Interactive · answer and compare Which way should your processes talk? Answer four questions about your processes and data. The table rules out what cannot work and marks what needs care.
Are the processes parent and child, or unrelated programs?
Do you move large data, megabytes many times a second?
Must it run on macOS as well as Linux?
Does the reader need whole messages or priorities?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways for processes on one machine to exchange data, against five needs of a design
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

  1. Question 1 of 2 A camera process hands 30 frames of 8 MB each per second to an encoder process on the same Linux machine. Which mechanism fits best?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Shared memory with a lock or a ring of slots, so that no frame is copied through the kernel

    The price is the synchronisation: the camera must not overwrite a frame the encoder is still reading, which a mutex, a semaphore or a ring of slots with atomic indexes settles.

  2. Question 2 of 2 A job runner sends short jobs with priorities to a worker, and it must run on both Linux and macOS. Which mechanism works on both?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A Unix domain datagram socket, with the priority in each message

    A System V queue would also work on both systems, with types for priorities, at the cost of an older API whose queues outlive the programs that created them.

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.

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.

  1. Question 1 of 15 Why is shared memory the fastest way for two processes to exchange data?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: Both processes map the same physical pages, so data written by one is there for the other without any copy

    The same fact makes it dangerous: since no system call is involved in each access, nothing orders the processes' reads and writes unless the program does.

  2. Question 2 of 15 A program maps an anonymous page with MAP_SHARED after it has forked its child. Can the child see the page?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: No; only processes forked after the mmap inherit the mapping, so the child needs it to be made before the fork

    For processes that are not parent and child, a named object made with shm_open, or a memfd passed over a socket, is the way to share memory.

  3. Question 3 of 15 A team fixes the lost updates of an unlocked shared counter by declaring it volatile. Does that work?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: No; volatile makes each load and store happen, but load, add and store are still three steps another process can come between

    volatile is the right tool for a signal handler's flag, as the signals lesson showed, and the wrong one for data that two processes update.

  4. Question 4 of 15 When is an atomic operation enough, without a mutex?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: When the update is one operation on one value, such as adding 1 to a counter

    Updates that must keep several values consistent, such as a ring buffer's head, tail and data, need a mutex or a carefully designed lock-free protocol.

  5. Question 5 of 15 Which semaphores does this lesson's Mac support, according to posix_probe.c?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: Named semaphores from sem_open; sem_init failed with ENOSYS

    A program that must run on both systems uses sem_open, a process-shared mutex, or atomics, all of which worked in the recorded runs.

  6. Question 6 of 15 A worker process is killed while it holds the process-shared mutex in a shared page. What happens to the other workers, and what is the remedy on Linux?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: Their lock calls wait for ever, unless the mutex is robust, in which case the next one gets EOWNERDEAD and must repair the data

    A crash inside a critical section leaves both a held lock and possibly inconsistent data, so shared-memory designs either use robust mutexes or keep updates small enough to make with atomics.

  7. Question 7 of 15 Where do POSIX shared memory objects live on Linux?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: In a tmpfs file system, normally mounted at /dev/shm

    Because they outlive their creator, a program that crashes leaves its objects behind, which is why shm_named.c called shm_unlink at the start in case an earlier run had been stopped half way.

  8. Question 8 of 15 On this lesson's Mac, shm_open refused a name of 41 characters. Which error did it report?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: ENAMETOOLONG

    Portable programs keep shared memory names short and start them with a slash, the one form whose meaning POSIX defines, rather than relying on Linux's longer limit.

  9. Question 9 of 15 Two unrelated programs on Linux must share a 64 MB buffer, and you do not want a name that other users could see in /dev/shm. Which design fits?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: memfd_create in one program, then pass its descriptor to the other over a Unix domain socket and map it there

    memfd_create exists since Linux 3.17, and its file seals let the receiver rely on a buffer that the sender can no longer change.

  10. Question 10 of 15 Why does this lesson show POSIX message queues with a model instead of a program that calls mq_open?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: macOS has no POSIX message queues; the probe found _POSIX_MESSAGE_PASSING -1 and no <mqueue.h>

    On Linux the same rules hold for real queues, which mq_overview(7) lists under /dev/mqueue once the mqueue file system is mounted.

  11. Question 11 of 15 On Linux, what is the default limit on the size of one message in a new POSIX queue, in bytes?

    Type a number.

    Show the answer: Question 11 of 15

    Answer: 8192 bytes

    mq_overview(7) gives a queue opened without attributes msgsize_default, 8,192 bytes, and msg_default, 10 messages, and the ceilings msgsize_max and msg_max start at the same values. That is why queues suit small messages and shared memory suits large data.

  12. Question 12 of 15 A Linux service must take urgent requests ahead of routine ones from several local clients, and reply to each client. Why is a Unix domain socket usually a better fit than one POSIX message queue?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: A queue has one shared inbox and no way back; a socket gives each client a connection for replies and tells the server who it is

    A queue fits a one-way stream of prioritised work; a request-reply service with several clients is what sockets were made for.

  13. Question 13 of 15 How many times is each byte copied when one process sends it to another through a pipe?

    Type a number.

    Show the answer: Question 13 of 15

    Answer: 2 copies

    Once from the writer's memory into the kernel's buffer, and once from there into the reader's memory. Shared memory needs none, which matters at hundreds of megabytes a second.

  14. Question 14 of 15 Which of these leave synchronising the processes to the program itself?

    Choose every answer that is right.

    Show the answer: Question 14 of 15

    Answer:

    • An anonymous MAP_SHARED mapping between parent and child
    • Shared memory made with shm_open and mmap

    Message passing synchronises by construction, since each message is one unit the kernel hands over; sharing memory leaves that work to the program.

  15. Question 15 of 15 A log collector on Linux and macOS receives short lines from many unrelated programs, which must not mix. Which design is simplest and safe?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: A Unix domain datagram socket bound to a path, one datagram per line

    Choose the simplest mechanism that keeps the guarantees the data needs; shared memory pays off only when copying is the bottleneck.

Interview questions

One warm-up, answered in full, comes first:

  1. Fresher to mid · Warm-up

    When is shared memory a better choice than message passing between processes, and what does it cost you?

    Shared memory wins when the data is large or frequent. Both processes map the same pages, so nothing is copied, while a pipe, socket or message queue copies each message into the kernel and out again; at hundreds of megabytes a second, such as video frames, those copies dominate. The cost is coordination. The kernel orders nothing in shared memory, so every change needs an atomic operation or a lock that both processes use: in the lesson's run, two processes adding 1 to an unlocked shared counter lost updates, while an atomic add and a process-shared mutex counted all 10,000,000. A crash can leave a lock held or data half written, and the layout of the memory is a protocol the programs must agree on. For small messages, a socket or a queue is simpler and safe by default.

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

  1. Question 1 of 2 Eight worker processes count requests in a shared memory page with counter++ on a volatile long. Under load the counter shows 20 % fewer requests than the access log. What is your diagnosis?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: counter++ is a load, an add and a store, so two workers can load the same value and both store it plus one

    shm_counter.c lost updates the same way with two processes and a volatile counter, and counted all 10,000,000 with an atomic add or a process-shared mutex.

  2. Question 2 of 2 A capture process produces 1920 by 1080 frames of 4 bytes a pixel at 60 frames a second, and an encoder process on the same machine consumes them. What is the data rate, and how should the frames travel?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: About 500 MB a second, through shared memory, because a pipe or a socket would copy every byte twice

    Shared memory removes the copies and keeps the coordination: the two processes still have to hand each frame over, so that the encoder never reads one that is half written.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Eight worker processes count requests in a shared memory page with counter++ on a volatile long. Under load the counter reports 20 % fewer requests than the access log. Explain why, fix it, and say how you would prove the fix.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.