Interprocess communication
Required reading: Chapter 13 and 21
Overview
Address spaces isolate programs from each other. However, often a
program needs to communicate with another program in another address
space (e.g., sending a message to a server, the output of one program
is the input to another program, or to coordinate execution order).
Lecture today is about various approaches to communication between
programs.
Interprocess communication involves two operations:
- Moving data from one address space to another
- Transfering control from one address space to another
Both operations have to be done in controled manner: we don't want the
sender to write in an arbitrary place in the receiver's address space
or jump to an arbitrary location in the receiver's address space.
How can processes communicate in UNIX v6?
- signals; moves limited amount of information, but transfers
control. receiver sets a signal handler for a particular signal (an
integer) and the sender sends a signal using the kill system call,
which will cause the receiver handler in the receiver's address space
to be invoked.
- pipes; moves data and supports sequence coordination. pipes work
between programs that have a common ancestor.
- files; moves data persistently (one program writes a file, another
one opens it), but doesn't include control transfer.
V6 code examples
- Signals. Type "man 3 signal" to see how a programmer can use
signals on today's unixes. Conceptually, it is identical to what v6
provides, but it is better implemented. v6 signals have some serious
drawbacks that makes them akward to use as a general IPC mechanism.
The sender sets a flag in the receiver's proc structure and
increase the priority of the receiver. The receiver checks the flag,
and deliver the signal. Why does the receiver deliver the signal
instead of the sender? (Answer: The receiver's stack may be swapped
out; thus, we must give the scheduler a chance first to load the
receiver.) Why is the flag stored in the proc strucure instead of the
u structure? (Answer: the u structure may be swapped out.)
- ssig. set the handler for signal a.
- kill. look for the destination process. if the destination
process has a signal pending, overwrite it with this one. Thus, the
signals in v6 may be lost, which makes programming with signals
difficult. Current UNIXes avoid this problem by having a queue of
signals to be delivered.
- psignal. set flag in destination's proc structure. should raise
priority of destination process, but code contains a bug (it uses
p_stat instead of p_prio). if in sleep, make destination process
runnable.
- issig. check whether current process has received a signal.
Where issig() called from? (Answer: sleep, trap, clock)
- psig. deliver the signal to user space. the kernel modifies the
trap/interrupt frame and the user stack; Line 4055 through line 4061
is the essense of signals.
- The code modifies the user stack so that
after the signal handler runs, it returns to the saved pc in the
trap/interrupt frame; this pc corresponds to the next instruction that
must be executed after the signal handler has completed.
- The code
also modifies the user stack to save the saved PSW, because the signal
handler is likely to modify the condition codes in PSW; thus, PSW
needs to be restored to its saved value when the signal handler
completes.
- From the fact that both PC and PSW are put on the user
stack, and both need to be restored after signal handler is done,
we can know that the signal handler returns with the rtt instruction
instead of the rts instruction. rtt restores both PS and PSW from top
of stack, while rts only restores PC from stack.
- signal reentrance. psig sets u_signal[0] to zero so that user
signal handler won't be interrupted by another signal. the signal
handler has to indicate that it is ready for a new signal by calling
ssig again. this is true for all signals, except SIGINS and SIGTRC.
If a second signal would come in before the handler called ssig,
however, the second signal would terminate process! Current UNIXes
avoid this problem.
- Pipes connect one programs output to another programs input. A
pipe is a FIFO character array. A pipe holds limited amount of bytes.
If the pipe is full, the kernel blocks the writer until there is
space. If the pipe empty, the kernel blocks the reader until the
writer writes new data to the pipe.
int fd[2];
if (pipe(fd) < 0) panic ("error");
if ((pid = fork ()) == 0) {
close (1);
tmp = dup (fd[1]); // 1 is the write end; tmp will be 1
close (fd[0]); // close read end
exec (command1, args1, 0);
} else if (pid > 0) {
close (0);
tmp = dup (fd[0]); // 0 is the read end; tmp will be 0
close (fd[1]); // close write end
exec (command2, args2, 0);
} else {
printf ("Unable to fork\n");
}
- pipe. one inode shared by two file descriptor entries. i_size1 is
amount of data in the pipe that is unread. f_offset[1] how many bytes
we have read.
- readp. if there is data read it, otherwise wait till writer writes
(after checking that a writer exists and waking it up). if reader,
reads data, wakeup writer (if it is waiting).
- writep. if there is space write it, otherwise wait till reader
reads (after checking that a reader exists and waking it up). if
writes writes data, wakeup reader (if it is waiting).