One interesting area of POSIX programming that we did not have a chance to cover earlier in the semester is inter-process communication. When we learned about processes it was primarily focused on the idea that processes are completely separate, and coordinate only through simple interactions, such as a shell waiting for a child process to exit. However, there are times when we want processes to be slightly less separate than what we’ve seen before.
Today’s in-class exercise will introduce three mechanisms—signals, pipes, and shared memory—that make it possible for processes to communicate and coordinate in useful ways.
There is no provided starter code for this exercise, but you will want to start with the examples shown inline below.
You are welcome to borrow a Makefile from another exercise or lab to set up your work for today’s exercise.
We first looked at signals in the virtual memory lab.
In that case, the operating system reports a segmentation fault to the running process by sending it the SIGSEGV signal.
Under normal circumstances you cannot do anything to fix a segmentation fault in a signal handler, but you were able to use the signal in combination with virtual memory operations to implement copy-on-write behavior.
Signals can be used for more than just sending information from the OS to a running process;
users can send signals;
typing ctrl + C sends the SIGINT signal, which causes programs to exit by default.
Processes can also send signals to other processes, which we will use today.
When we used signals before, we treated them as asynchronous events.
Using the sigaction function, we were able to set up a signal handler that will run in response to a signal.
This is useful if you want to handle a signal regardless of what your program is doing, but it doesn’t work well if you want your program to wait for a signal at some defined point in the code.
Instead of writing signal handlers, we’ll instead use sigwait to wait for signals.
This is relatively straightforward, but we need to specifically disable signal handlers for any signals we will wait for, otherwise the handler may run.
For many signals the default handler will exit the program, which is probably not what we want.
The example program wait_sigint.c below shows how you can use sigwait and associated functions to wait for the SIGINT signal.
Instead of terminating the program, sending this signal will allow the program to continue.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main() {
// Create an empty signal set
sigset_t signals;
if(sigemptyset(&signals)) {
perror("sigemptyset failed");
exit(2);
}
// Add SIGINT to the signal set
if(sigaddset(&signals, SIGINT)) {
perror("sigaddset failed");
exit(2);
}
// Mask the signals in our set so they do not run signal handlers
if(sigprocmask(SIG_BLOCK, &signals, NULL)) {
perror("sigprocmask failed");
exit(2);
}
// Loop ten times
for(int i=0; i<10; i++) {
// Wait for a signal in the set
int signal;
if(sigwait(&signals, &signal)) {
perror("sigwait failed");
exit(2);
}
// Print a message
printf(" %d: Received signal %d\n", i, signal);
}
return 0;
}
Compile this program and run it as in the example below.
The example output includes the funny ^C character, which is what appears when you type ctrl + C.
$ clang -o wait_sigint wait_sigint.c
$ ./wait_sigint
^C 0: Received signal 2
^C 1: Received signal 2
^C 2: Received signal 2
^C 3: Received signal 2
^C 4: Received signal 2
^C 5: Received signal 2
^C 6: Received signal 2
^C 7: Received signal 2
^C 8: Received signal 2
^C 9: Received signal 2
Verify that your program is working and then move on to the next part.
In addition to the keyboard shortcuts, you can send signals to a process using the kill command.
The example run below shows how you can start wait_sigint in the background, then send it a signal.
Note that the bash variable $! holds the process ID of the most recent background process.
$ ./wait_sigint &
[1] 92657
$ kill -s INT $!
0: Received signal 2
$ kill -s INT $!
1: Received signal 2
...
In addition to SIGINT, you can send other signals using the kill command.
What do you expect to happen if you send the program a different signal using the command kill -s ALRM $!?
Make a prediction and then run the program to check it.
Copy the previous program to a new source file, wait_signals.c, and modify it to respond to three different signals: SIGINT, SIGALRM, and SIGUSR1.
The program should wait for any of these signals, print the name of the signal it received, then repeat this process nine additional times as in the original example.
You may need to refer to the man pages for sigaddset and sigwait.
Here is an example of how your program should run:
$ ./wait_signals &
[1] 64237
$ kill -s INT $!
0: Received signal SIGINT
$ kill -s ALRM $!
1: Received signal SIGALRM
$ kill -s USR1 $!
2: Received signal SIGUSR1
$ kill -s INT $!
3: Received signal SIGINT
...
Complete your implementation before moving on to the next part.
So far we’ve only seen ways to send signals from the shell, but if we hope to use signals to allow processes to communicate we will need to send signals from C code.
Luckily the process is very similar.
The POSIX function int kill(pid_t pid, int sig) works just like the command line call;
you simply call this function with a process ID and signal number, and the OS will send the signal.
Using the man page for kill (run man 2 kill to get the page for the function instead of the command-line program), write a simple program to send a signal repeatedly.
Your program should run with the following arguments: ./send_signal <pid> <signal> <N>.
This invocation should send the specified signal n times to the specified process.
Here is an example run with the signal waiting program from earlier.
$ ./wait_signals &
$ ./send_signals $! SIGINT 3
0: Received signal SIGINT
1: Received signal SIGINT
2: Received signal SIGINT
$ ./send_signals $! SIGALRM 5
3: Received signal SIGALRM
4: Received signal SIGALRM
5: Received signal SIGALRM
6: Received signal SIGALRM
7: Received signal SIGALRM
$ ./send_signals $! SIGUSR1 1
8: Received signal SIGUSR1
$ ./send_signals $! SIGUSR1 1
9: Received signal SIGUSR1
Your program only needs to handle the three signals from earlier: SIGINT, SIGALRM, and SIGUSR1.
If the program is run with any other signal name you can simply exit with an error.
Don’t forget to check error returns from the kill function.
So far we’ve only seen mechanisms for sending a signal all by itself;
often we want processes to communicate by sharing data in some way.
There is a limited feature for signals that allows you to send a small amount of data, although we won’t explore it in this exercise.
If you are curious, you can read about sigqueue and sigwaitinfo, which make it possible to send a single integer value along with a signal.
Most interesting cases for inter-process communication require more than simple signals because processes need to share data, not just send notifications when events occur. One mechanism that allows data sharing between processes is the POSIX pipe. A pipe is like a file, but it is not stored in the filesystem. Instead, a pipe has two endpoints; any value written into one end of the pipe can be read out the other end.
The simplest use for a pipe is to send data between a parent and child process;
the parent typically creates the pipe before calling fork;
after calling fork, both processes will have access to the pipe using the file descriptors created before fork.
The simple_pipe.c program below shows how a parent process can send messages to a child process.
In this case, messages are structs that contain coordinates, though any binary data could be sent over a pipe, including text.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
struct coord {
float x;
float y;
float z;
};
int main(int argc, char** argv) {
// Create an array to hold pipe file descriptors
int pipefds[2];
// Create a pipe
if(pipe(pipefds)) {
perror("pipe failed");
exit(2);
}
// At this point, pipefds[0] holds the output end of the pipe and pipefds[1] holds the input end. Split them out for easier code reading
int from_parent = pipefds[0];
int to_child = pipefds[1];
// Now call fork to create a child process
pid_t child_pid = fork();
if(child_pid == -1) {
perror("fork failed");
exit(2);
} else if(child_pid == 0) {
// In the child process
// Close the end of the pipe the child does not use
close(to_child);
// Make space for a coordinate value
struct coord c;
// Now read in a loop
while(read(from_parent, &c, sizeof(struct coord)) == sizeof(struct coord)) {
// Display the value received
printf("Child: Received coordinate <%.2f, %.2f, %.2f>\n", c.x, c.y, c.z);
}
// Close the pipe output
close(from_parent);
} else {
// In the parent process
// Close the end of the pipe the parent does not use
close(from_parent);
// Send ten structs to the child process
for(int i=0; i<10; i++) {
// Create a struct value to send over the pipe
struct coord c;
c.x = i;
c.y = i*2;
c.z = 3*i/2;
// Print the value we're sending
printf("Parent: Sending coordinate <%.2f, %.2f, %.2f>\n", c.x, c.y, c.z);
// Send the struct
if(write(to_child, &c, sizeof(struct coord)) != sizeof(struct coord)) {
perror("Failed to write to pipe");
exit(2);
}
// Pause for half a second
usleep(500000);
}
// Close the pipe input
close(to_child);
}
return 0;
}
When you build and run this sample program you should see the following output:
$ clang -o simple_pipe simple_pipe.c
$ ./simple_pipe
Parent: Sending coordinate <0.00, 0.00, 0.00>
Child: Received coordinate <0.00, 0.00, 0.00>
Parent: Sending coordinate <1.00, 2.00, 1.00>
Child: Received coordinate <1.00, 2.00, 1.00>
Parent: Sending coordinate <2.00, 4.00, 3.00>
Child: Received coordinate <2.00, 4.00, 3.00>
Parent: Sending coordinate <3.00, 6.00, 4.00>
Child: Received coordinate <3.00, 6.00, 4.00>
Parent: Sending coordinate <4.00, 8.00, 6.00>
Child: Received coordinate <4.00, 8.00, 6.00>
Parent: Sending coordinate <5.00, 10.00, 7.00>
Child: Received coordinate <5.00, 10.00, 7.00>
Parent: Sending coordinate <6.00, 12.00, 9.00>
Child: Received coordinate <6.00, 12.00, 9.00>
Parent: Sending coordinate <7.00, 14.00, 10.00>
Child: Received coordinate <7.00, 14.00, 10.00>
Parent: Sending coordinate <8.00, 16.00, 12.00>
Child: Received coordinate <8.00, 16.00, 12.00>
Parent: Sending coordinate <9.00, 18.00, 13.00>
Child: Received coordinate <9.00, 18.00, 13.00>
There is a pause inserted in the parent process to make it easier to see that the two processes are communicating, although this is not necessary for the program to work.
Once you’ve read through and run the simple example, move on to the next step.
If you want to use pipes to send data in two directions you will need to create two pipes: one to send from parent to child, and another to send from child to parent. Modify the example program above to use a second pipe. The child process should receive a coordinate as in the original program, compute the magnitude of the coordinate it received, and then send that magnitude back to the parent, where it should be displayed. Recall that the magnitude of a three-dimensional vector \(<x, y, x>\) is \(\sqrt{x^2 + y^2 + z^2}\). The output of your completed program should look something like this:
$ ./two_way_pipe
Parent: Sending coordinate <0.00, 0.00, 0.00>
Child: Received coordinate <0.00, 0.00, 0.00>
Child: Sending magnitude 0.00
Parent: Received magnitude 0.00
Parent: Sending coordinate <1.00, 2.00, 1.00>
Child: Received coordinate <1.00, 2.00, 1.00>
Child: Sending magnitude 2.45
Parent: Received magnitude 2.45
Parent: Sending coordinate <2.00, 4.00, 3.00>
Child: Received coordinate <2.00, 4.00, 3.00>
Child: Sending magnitude 5.39
Parent: Received magnitude 5.39
Parent: Sending coordinate <3.00, 6.00, 4.00>
Child: Received coordinate <3.00, 6.00, 4.00>
Child: Sending magnitude 7.81
Parent: Received magnitude 7.81
Parent: Sending coordinate <4.00, 8.00, 6.00>
Child: Received coordinate <4.00, 8.00, 6.00>
Child: Sending magnitude 10.77
Parent: Received magnitude 10.77
Parent: Sending coordinate <5.00, 10.00, 7.00>
Child: Received coordinate <5.00, 10.00, 7.00>
Child: Sending magnitude 13.19
Parent: Received magnitude 13.19
Parent: Sending coordinate <6.00, 12.00, 9.00>
Child: Received coordinate <6.00, 12.00, 9.00>
Child: Sending magnitude 16.16
Parent: Received magnitude 16.16
Parent: Sending coordinate <7.00, 14.00, 10.00>
Child: Received coordinate <7.00, 14.00, 10.00>
Child: Sending magnitude 18.57
Parent: Received magnitude 18.57
Parent: Sending coordinate <8.00, 16.00, 12.00>
Child: Received coordinate <8.00, 16.00, 12.00>
Child: Sending magnitude 21.54
Parent: Received magnitude 21.54
Parent: Sending coordinate <9.00, 18.00, 13.00>
Child: Received coordinate <9.00, 18.00, 13.00>
Child: Sending magnitude 23.96
Parent: Received magnitude 23.96
Make sure you are computing the magnitude in the child process and sending it back to the parent process, not simply printing the magnitude in the child.
Your magnitude value should be a float, so make sure you write and read the appropriate number of bytes when transferring this value back to the parent process.
One of the most interesting use cases for the pipe function is to set up chains of processes that communicate in sequence.
To run these chains in the shell you use the | (pipe) character.
The idea is that the standard output of one command becomes the standard input of the next.
For this part of today’s exercise you will use pipes to perform a simpler form of output capture from a running process.
Write a simple command line tool reverse <cmd ...>, which runs the specified command but captures its output and prints each line in reverse.
You will need to use fork to create a child process and exec to run the specified command in that child process.
Just before calling exec, you can use the dup2 function to replace the standard out file descriptor (which is always set to the STDOUT_FILENO constant) with the input end of a pipe.
At this point, anything the child process prints will be sent through the pipe instead of to the terminal.
Meanwhile, the parent process should read from the pipe until it sees a full line, then print that line in reverse.
Here are a few simple runs of commands with and without the reverse program that you should be able to handle:
$ whoami
curtsinger
$ reverse whoami
regnistruc
$ hostname
turing
$ reverse hostname
gnirut
$ ps
PID TTY TIME CMD
16532 pts/2 00:00:00 ps
27024 pts/2 00:00:00 bash
$ reverse ps
DMC EMIT YTT DIP
sp 00:00:00 2/stp 29761
hsab 00:00:00 2/stp 42072
Remember to handle errors when reading from the pipe (this will tell you when the process has exited), and be careful about buffer sizes when reading a full line.
A third mechanism that processes can use to communicate is shared memory.
Shared memory is exactly what it sounds like;
two processes can set up a range of their address space to be directly shared with another process or collection of processes.
To do this, programs generally use either mmap with the MAP_SHARED flag, which keeps the same memory in place after a fork call, or they explicitly attach shared memory regions to their address spaces using shm_open and the associated functions.
Programming in this model is very much like thread programming, although processes do not share all of their memory, just the parts that are explicitly shared.
If you have time during class or want to explore shared memory on your own, the following exercises should help get you started:
mmapUse mmap to create a region of shared memory using the MAP_SHARED flag.
After that point, call fork to create a child process.
In the parent, write some values into shared memory after calling fork.
In the child, sleep for a short time to allow the parent to write, then print out the values from shared memory to confirm it really is shared.
Note: this is not a good way to synchronize concurrent accesses to shared memory!
We can use sempahores across processes to synchronize accesses to shared memory, or just to make a process wait.
Create a shared memory region using mmap, but before calling fork initialize a semaphore in the shared memory.
This semaphore will be used across processes, so make sure you initialize it with the right parameters (check the manpage).
Call fork one or more times to create processes with access to the shared memory.
You can use the semaphore stored in shared memory to reimplement the tick/tock example from class earlier this semester, or try something else interesting with the shared semaphore.
You can also use a mutex across processes, as long as the mutex itself is stored in shared memory.
You will need to initialize the mutex with pthread_mutex_init using the attributes parameter to allow cross-process use;
the mutex might work without this, but the behavior is undefined and is unlikely to work across different systems.
Try using the mutex to protect a data structure that two processes access. A fixed-size buffer stored in the shared memory region would be easiest to implement, but you could also implement a linked list as long as you place the nodes in the shared memory region (otherwise the other process won’t be able to access them).
You can also use condition variables across processes using an attribute at initialization time.
Sometimes we want two unrelated processes to share memory, or we want to establish shared memory between two processes after they are already running.
The two primary ways to do this are with the shm_* family of shared memory functions, and with mmapped files.
The shm_* functions allow you to create special shared memory “files” that a process can open and attach to gain access to a shared memory region.
Using mmap backed by a file also allows two processes to share memory;
to do this, you create a file, use truncate to set its length, and then mmap it using the MAP_SHARED flag.
A second process opens the file and mmaps it with MAP_SHARED.
The virtual address regions returned by mmap in both processes will refer to the same underlying memory.