#include <unistd.h>
int pipe(int fd[2]);
// Returns: 0 if OK, –1 on error
After calling pipe()
:
fd[0]
is opened for reading, fd[1]
is opening for writing
man 7 pipe
.After calling pipe()
and then fork()
:
Recall from L03-file-io: when forking, all open file descriptors are “dup’d”. Child gets a reference to the read and write ends of the pipe.
Diagram is slightly misleading: pipes are only half-duplex (one-way communication). You can only do one of the following:
fd[1]
, child reads from fd[0]
fd[1]
, parent reads from fd[0]
You can’t use pipe as a full-duplex (two-way communication) channel, e.g.:
fd[1]
, and then reads from fd[0]
expecting to block
until child writes something, it actually ends up just reading back what it just
wrote.Since pipe is only half-duplex, close()
unused ends of pipe after forking
depending on who you want to read/write. e.g., where child reads and parent
writes:
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], ...)
}
Note dependence on fork()
for sharing ends of pipe via dup’d file descriptors.
This form of IPC only works for related processes (e.g. parent-child)
connect2
demo: how shell stitches together two processes when you run a
pipeline: p1 | p2
.
dup2()
: allows you to target a specific newfd
to copy
oldfd
into. If newfd
is taken, atomically close()
s newfd
before copying
oldfd
into it.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
int fd[2];
pid_t pid1, pid2;
// Split arguments ["cmd1", ..., "--", "cmd2", ...] into
// ["cmd1", ...] and ["cmd2", ...]
char **argv1 = argv + 1; // argv for the first command
char **argv2; // argv for the second command
for (argv2 = argv1; *argv2; argv2++) {
if (strcmp(*argv2, "--") == 0) {
*argv2++ = NULL;
break;
}
}
if (*argv1 == NULL || *argv2 == NULL) {
fprintf(stderr, "%s\n", "separate two commands with --");
exit(1);
}
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);
// Unreachable
}
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);
// Unreachable
}
// Parent does not need either end of the pipe
close(fd[0]);
close(fd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
return 0;
}
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
// Returns: 0 if OK, –1 on error
mkfifo()
: create a new named pipe on the filesystem
Afterwards, use file I/O syscalls to interact with special pipe file. Shares semantics with unnamed pipe – still half-duplex.
Unlike unnamed pipe, FIFO can be used for IPC between unrelated processes because they can use the filesystem as a rendezvous point. Both processes just need to know the path to the FIFO.
Semaphore: fundamentally, just an integer value mainly manipulated by two methods.
Increment: increase the integer
V()
, up, sem_post()
Decrement: wait until value > 0, then decrease the integer value
P()
, down, sem_wait()
Initial value affects semaphore semantics!
Binary semaphore (a.k.a. lock): initial value is 1. Protects one resource.
sem_wait()
. Value decremented to 0.sem_post()
to release the resource. Value incremented to 1.Resource is limited to 1 user at a time. Concurrent access while resource is locked sees value of 0, blocks, and is woken up when value is incremented back to 1.
Counting semaphore: initial value is N > 1
. Protects N
resources.
sem_wait()
. Value is decremented by 1.sem_post()
to release the resource. Value incremented by 1.Since there are N
resources, concurrent access is only blocked after N
concurrent accesses (i.e. when the value hits 0). N + 1
th concurrent access
sees value of 0, blocks, is woken up when the value becomes positive again (i.e.
some users posted the semaphore).
Ordering semaphore: take advantage of blocking semantic to implement “events”. e.g.:
sem = 0 // initial value is 0
P1: 1 -> 2 -> sem_wait() -> 4 -> 5
P2: A -> B -> C -> D -> sem_post()
P1 completes tasks 1-2 then blocks until P2 completes tasks A-D before moving on to tasks 4 and 5. P1 has to wait until P2 increments the semaphore value.
Initializing and destroying unnamed POSIX semaphores:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
// Returns: 0 if OK, –1 on error
int sem_destroy(sem_t *sem);
// Returns: 0 if OK, –1 on error
sem_t *sem
: pointer to shared semaphore object. Declared by user and
initialized/destroyed by API.
int pshared
: If semaphore is meant to be shared by processes, pass in non-zero
value. Otherwise, (e.g. threads), pass in 0.
unsigned int value
: Initial value for the semaphore
If unnammed semaphore is to be shared by related processes, where should semaphore be declared?
fork()
because of dup()
ing semantics.mmap()
below on discussion of “shared memory”.Creating, opening, closing, and removing named POSIX semaphores:
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ...
/* mode_t mode, unsigned int value */ );
// Returns: Pointer to semaphore if OK, SEM_FAILED on error
int sem_close(sem_t *sem);
// Returns: 0 if OK, –1 on error
int sem_unlink(const char *name);
// Returns: 0 if OK, –1 on error
Similar semantics to file API syscalls (recall L03-file-io).
Named semaphores meant to be used by unrelated processes – use semaphore name as “redezvous” point.
/dev/shm
Decrement the value of semaphores:
#include <semaphore.h>
#include <time.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
// Both return: 0 if OK, –1 on error
int sem_timedwait(sem_t *restrict sem,
const struct timespec *restrict tsptr);
// Returns: 0 if OK, –1 on error
Blocking semantics:
sem_trywait()
does NOT block, returns immediately if semaphore value is 0.sem_wait()
blocks until semaphore value is positive
errno
to EINTR
if interrupted by a signalsem_timedwait()
blocks until it times out or semaphore value is positive,
whichever happens first
sem_timedwait()
be safely implemented using SIGALRM
? Recall L04-signals.Increment the value of semaphores:
#include <semaphore.h>
int sem_post(sem_t *sem);
// Returns: 0 if OK, –1 on error
Using the file I/O syscalls can be annoying. Consider the example of opening a file with O_RDWR
:
lseek()
read()
to copy contents out of kernel to userspace bufferwrite()
to copy contents out of userspace buffer into kernelAlternative: map region of file into your virtual address space!
Memory-mapped region is backed by disk. That is, updates to the memory-mapped region go to memory first, then (eventually) flushed to disk
Furthermore, mappings can be private or shared.
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off);
// Returns: starting address of mapped region if OK, MAP_FAILED on error
Note-worthy parameters:
void *addr
: Virtual address to place the mapping at. Prefer to pass NULL
and
let mmap()
decide for you (address is the return value).int prot
: Protection of the mapped region (read, write, exec, none)int flag
: Visibility (shared/private) + other modifiersint fd
: file descriptor attached to file we want to mapMapping a file with MAP_SHARED
is a form of IPC for unrelated processes
Sometimes we want to map memory that is not backed by a file (kinda like malloc()
):
fd = -1
and flag = MAP_ANON | ...
However, mapping visibility makes this more powerful than malloc()
! Consider a process that creates an anonymous memory and then fork()
s. We know that child will inherit all of the parent’s memory mappings, but…
MAP_PRIVATE
: child gets its own indepdendent copy of the mapping (like malloc()
)MAP_SHARED
: child shares memory mapping with parent, both see each other’s updatesMapping some anonymous memory with MAP_SHARED
is a form of IPC for related processes
fork()
facilitates the sharing, like for pipe()
Example: counter.c
– note that unnamed semaphore is placed in shared memory
so both parent and child have access to it.
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LOOPS 2059
struct counter {
sem_t sem;
int cnt;
};
static struct counter *counter = NULL;
static void inc_loop() {
for (int i = 0; i < LOOPS; i++) {
sem_wait(&counter->sem);
// Not an atomic operation, needs lock!
// 1) Load counter->cnt into tmp
// 2) Increment tmp
// 3) Store tmp into counter->cnt
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, sizeof(struct counter),
// Region is readable and writable
PROT_READ | PROT_WRITE,
// Want to share anonymous mapping with forked child
MAP_SHARED | MAP_ANON,
/*fd=*/-1, // No associated file
/*offset=*/0);
assert(counter != MAP_FAILED);
// Mapping is already zero-initialized.
assert(counter->cnt == 0);
sem_init(&counter->sem, /*pshared=*/1, /*value=*/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, LOOPS * 2);
sem_destroy(&counter->sem);
assert(munmap(counter, sizeof(struct counter)) == 0);
return 0;
}
Skim briefly just to appreciate how great POSIX IPC is :^)
They share common naming and interface scheme:
XSI Message queues
int msgget(key_t key, int flag);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
XSI Semaphores
int semget(key_t key, int nsems, int flag);
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */ );
int semop(int semid, struct sembuf semoparray[], size_t nops);
XSI Shared memory
int shmget(key_t key, size_t size, int flag);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
void *shmat(int shmid, const void *addr, int flag);
int shmdt(const void *addr);
And they all suck…
Hard to clean-up because there is no reference counting
Hard to use
They have been widely used for lack of alternatives. Fortunately we do have alternatives these days:
Instead of XSI message queues, use:
man 7 mq_overview
)Instead of XSI semaphores, use:
Instead of XSI shared memory, use:
mmap()
Last updated: 2023-02-02