8 Interprocess Communication

3 minute read

Published:

Pipes

Unnamed pipe

No file name, just an fd

#include <unistd.h>
int pipe(int fd[2]);

Kernel prepares a pipe structure, connecting fd[0] (read) with fd[1] (write) After fork(), parent and child will share the same pipe.

  • Pipe is one-way traffic (half duplex)
  • Need to close() unused ends of pipe
int fd[2];
pipe(fd);

if (fork() == 0) {
    close(fd[1]);  // close unused write end
    read(fd[0], ...);
} else {
    close(fd[0]);  // close unused read end
    write(fd[1], ...)
}

connect2

How a shell would stitch together two processes for a pipeline (E. p1 | p2 becomes ./connect2 p1 -- p2)

int dup2(int oldfd, int newfd);

If newfd is open, dup2() will close it first, and duplicate oldfd to newfd

  • Atomic operation
  1. Creates two child processes
  2. Stitch the IO together
pipe(fd);

if ((pid1 = fork()) == 0) {
	close(fd[0]);   // Close read end of pipe
	dup2(fd[1], 1); // Redirect stdout to write end of pipe
	close(fd[1]);   // stdout already writes to pipe, close spare fd
	execvp(*argv1, argv1);   // fds preserved
}

if ((pid2 = fork()) == 0) {
	close(fd[1]);   // Close write end of pipe
	dup2(fd[0], 0); // Redirect stdin from read end of pipe
	close(fd[0]);   // stdin already reads from pipe, close spare fd
	execvp(*argv2, argv2);
}

// Parent does not need either end of the pipe
close(fd[0]);
close(fd[1]); 

Named pipes

If something’s unnamed, you can only use it between related processes

#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);

Blocked in open() until someone else opens the other end

  • Fine for multiple processes to R/W
  • But at least one reader and one writer.
  • Analogous to unnamed pipe (read() will block for empty unnamed pipe, same for write)

Memory mapped IO

Memory Mapping `mmap()

  • Shared, file-backed: can link unrelated processes
    • Avoid physical file with shm_open() (shared mem open)
    • Will create a “fake file” under /dev/shm (an in-memory filesystem)
  • Shared, anonymous: Must be related, no actual file involved
  • Private, file-backed: private copy of the file in memory, not written back to file
  • Private, anonymous: Just allocate memory (E. malloctopus)

POSIX Semaphore

Also available in unnamed and Named

  • For multiple processes (instead of thread), semaphore object needs to reside in a file for every process to see it

Create a multi-process semaphore embedded in some other structure (counter), and then putting it into a shared anonymous memory region

struct counter {   // semaphore
    sem_t sem;
    int cnt;
};
static struct counter *counter = NULL;

// multiple processes will run this loop
static void inc_loop() {
    for (int i = 0; i < 1000; i++) {
        sem_wait(&counter->sem);
        counter->cnt++;
        sem_post(&counter->sem);
    }
}

int main(int argc, char **argv) {
    // Create a shared anonymous memory mapping, set global pointer to it
    counter = mmap(/*addr=*/NULL, /*let mmap decide*/
				   sizeof(struct counter),
                   // Region is readable and writable
                   PROT_READ | PROT_WRITE,
                   // Want to share anonymous mapping with forked child
                   MAP_SHARED | MAP_ANONYMOUS,
                   /*fd=*/-1,  // No associated file (anony)
                   /*offset=*/0);
    assert(counter != MAP_FAILED);
    assert(counter->cnt == 0);  // Mapping is already zero-initialized.

    sem_init(&counter->sem, /*pshared=*/1, /*value=(binary)*/1);

    pid_t pid;
    if ((pid = fork()) == 0) {
        inc_loop();
        return 0;
    }

    inc_loop();
    waitpid(pid, NULL, 0);

    printf("Total count: %d, Expected: %d\n", counter->cnt, 1000 * 2);

    sem_destroy(&counter->sem);
    munmap(counter, sizeof(struct counter));  // aka free
}

Unnamed semaphore are just sem_t objects passed to sem_init() and sem_destroy()

How about named semaphore? Passed to sem_open(), and it gives back a pointer to a newly created named sem_t object file.

  • Necessary for unrelated processes