ARM64 OS Handbook
🔍

Chapter 29: IPC

What You Will Learn in This Chapter
  • Why processes need Inter-Process Communication (IPC)
  • IPC mechanisms: pipes, sockets, shared memory, message queues, signals
  • How pipes work (the simplest IPC mechanism)
  • Shared memory and memory-mapped files
  • How the kernel implements message passing
  • Our IPC implementation

29.1 Why IPC?

Processes have isolated address spaces (Chapter 23). A process cannot directly access another process's memory. Inter-Process Communication (IPC) provides mechanisms for processes to exchange data and coordinate.

Common IPC mechanisms:

MechanismData FlowPerformanceUse Case
PipeUnidirectional byte streamMediumShell pipelines (ls | grep)
SocketBidirectional byte streamMediumNetwork communication, local IPC
Shared MemoryDirect memory accessHighLarge data sharing, databases
Message QueueStructured messagesLow-MediumMicroservices, event-driven systems
SignalAsynchronous notificationLowProcess control (kill, SIGINT)
FIFO (named pipe)Unidirectional, file-system basedMediumUnrelated processes

29.2 Pipes

A pipe is a unidirectional byte stream with a read end and a write end. Data written to the write end is buffered by the kernel and can be read from the read end. Pipes are created with the pipe() system call.

/* Pipe data structure (kernel-internal) */
#define PIPE_BUFFER_SIZE 4096

struct pipe {
    char buffer[PIPE_BUFFER_SIZE];
    int read_pos;
    int write_pos;
    int readers;       /* Number of open read ends */
    int writers;       /* Number of open write ends */
    struct semaphore empty;  /* Counts empty space */
    struct semaphore full;   /* Counts data bytes */
    struct semaphore mutex;  /* Protects buffer */
};

/* Create a pipe */
int sys_pipe(int fd[2]) {
    struct pipe *p = kmalloc(sizeof(struct pipe));
    p->read_pos = p->write_pos = 0;
    p->readers = 1;
    p->writers = 1;
    sem_init(&p->empty, PIPE_BUFFER_SIZE);
    sem_init(&p->full, 0);
    sem_init(&p->mutex, 1);

    /* Create file descriptors for read and write ends */
    fd[0] = alloc_fd(p, FD_TYPE_PIPE_READ);
    fd[1] = alloc_fd(p, FD_TYPE_PIPE_WRITE);
    return 0;
}

/* Write to a pipe */
ssize_t pipe_write(struct pipe *p, const void *buf, size_t count) {
    size_t written = 0;
    while (written < count) {
        sem_wait(&p->empty);
        sem_wait(&p->mutex);

        size_t space = PIPE_BUFFER_SIZE - ((p->write_pos - p->read_pos) & (PIPE_BUFFER_SIZE - 1));
        size_t chunk = min(count - written, space);
        memcpy(&p->buffer[p->write_pos & (PIPE_BUFFER_SIZE - 1)], buf + written, chunk);
        p->write_pos += chunk;
        written += chunk;

        sem_signal(&p->mutex);
        sem_signal(&p->full);
    }
    return written;
}

/* Read from a pipe */
ssize_t pipe_read(struct pipe *p, void *buf, size_t count) {
    sem_wait(&p->full);
    sem_wait(&p->mutex);

    size_t available = p->write_pos - p->read_pos;
    size_t chunk = min(count, available);
    memcpy(buf, &p->buffer[p->read_pos & (PIPE_BUFFER_SIZE - 1)], chunk);
    p->read_pos += chunk;

    sem_signal(&p->mutex);
    sem_signal(&p->empty);
    return chunk;
}

29.3 Shared Memory

Shared memory is the fastest IPC mechanism. The kernel maps the same physical pages into the address spaces of multiple processes. Once mapped, processes can read and write directly without system call overhead.

/* Shared memory segment */
struct shm_segment {
    int id;
    size_t size;
    uint64_t *pages;      /* Array of physical page addresses */
    int refcount;         /* Number of processes mapping this segment */
};

/* Create a shared memory segment */
int sys_shm_create(size_t size) {
    struct shm_segment *seg = kmalloc(sizeof(struct shm_segment));
    seg->id = allocate_shm_id();
    seg->size = size;
    seg->refcount = 1;

    int num_pages = (size + PAGE_SIZE - 1) / PAGE_SIZE;
    seg->pages = kmalloc(num_pages * sizeof(uint64_t));
    for (int i = 0; i < num_pages; i++) {
        seg->pages[i] = (uint64_t)page_alloc();
    }

    return seg->id;
}

/* Map a shared memory segment into the current process */
void *sys_shm_map(int shm_id) {
    struct shm_segment *seg = find_shm(shm_id);
    if (!seg) return NULL;

    /* Find a free virtual address range */
    uint64_t va = find_free_va(seg->size);
    struct pcb *proc = get_current_process();

    int num_pages = (seg->size + PAGE_SIZE - 1) / PAGE_SIZE;
    for (int i = 0; i < num_pages; i++) {
        map_page(proc->page_table, va + i * PAGE_SIZE, seg->pages[i], 1, 1);
    }

    seg->refcount++;
    return (void *)va;
}

29.4 Message Queues

Message queues allow processes to exchange structured messages. Each message has a type and data. Messages are buffered by the kernel and delivered in order.

/* Message queue */
#define MSG_MAX_SIZE 256
#define MSG_QUEUE_MAX 64

struct message {
    long type;
    char data[MSG_MAX_SIZE];
    size_t size;
};

struct msg_queue {
    struct message msgs[MSG_QUEUE_MAX];
    int head, tail, count;
    struct semaphore mutex;
    struct semaphore send_slot;   /* Counts available send slots */
    struct semaphore recv_slot;   /* Counts available messages */
};

/* Send a message */
int sys_msgsnd(int qid, const void *msgp, size_t size, long type) {
    struct msg_queue *q = get_msg_queue(qid);
    sem_wait(&q->send_slot);
    sem_wait(&q->mutex);

    q->msgs[q->head].type = type;
    memcpy(q->msgs[q->head].data, msgp, size);
    q->msgs[q->head].size = size;
    q->head = (q->head + 1) % MSG_QUEUE_MAX;
    q->count++;

    sem_signal(&q->mutex);
    sem_signal(&q->recv_slot);
    return 0;
}

29.5 Signals

Signals are asynchronous notifications sent to a process. The kernel delivers signals by setting a pending flag and delivering them when the process returns from kernel mode. Common signals: SIGKILL (9), SIGSEGV (11), SIGTERM (15).

/* Signal delivery on return to user space */
void deliver_signals(struct pcb *proc) {
    for (int sig = 0; sig < MAX_SIGNALS; sig++) {
        if (proc->pending_signals & (1 << sig)) {
            proc->pending_signals &= ~(1 << sig);

            if (proc->signal_handlers[sig] == SIG_IGN) continue;
            if (proc->signal_handlers[sig] == SIG_DFL) {
                /* Default action: terminate, stop, or ignore */
                if (sig == SIGKILL || sig == SIGSEGV) {
                    sys_exit(128 + sig);
                }
                continue;
            }

            /* Set up user-space signal handler frame on user stack */
            setup_signal_frame(proc, sig, proc->signal_handlers[sig]);
            break; /* Deliver one signal at a time */
        }
    }
}

29.6 Our Implementation

Our kernel implements these IPC mechanisms:

  • Pipes: unidirectional byte streams with semaphore synchronization
  • Shared memory: shm_create, shm_map, shm_unmap system calls
  • Message queues: fixed-size message buffers with type-based receive
  • Signals: 31 standard signals, user-registerable handlers, signal frame setup
  • Unix domain sockets (future): for local socketpair communication

All IPC objects are reference-counted and freed when the last user unmaps/closes them. The kernel provides a unified IOCTL-like interface for IPC: ipc_call(cmd, ...) dispatches to the appropriate subsystem.

29.7 Exercises

Exercise 1: Pipe Echo

Write a program using fork() and pipe() where the parent sends "hello" to the child and the child prints it.

Exercise 2: Shared Memory Counter

Create a shared memory segment with a counter. Launch two processes that each increment the counter 10,000 times. Use a semaphore in shared memory for synchronization.

29.8 Summary

IPC allows isolated processes to communicate. Pipes provide unidirectional byte streams, shared memory offers the highest performance for bulk data exchange, message queues deliver structured messages, and signals deliver asynchronous notifications. Our kernel implements all four mechanisms, each optimized for its specific use case. IPC is fundamental to Unix philosophy: small programs that communicate through pipes and other channels to accomplish complex tasks.