We now consider an alternative form of inter-process cooperation known as message passing.
One motivation for this is that cooperation schemes based on shared memory are only viable when the cooperating processes run on the same CPU, or on tightly-coupled CPU's that share some physical memory.
Many applications of concurrency call for cooperation between processes located on physically remote systems. In such cases, their cooperation must be accomplished by sending messages back and forth over some kind of communication link.
Even when the physical configuration of the system does allow for cooperating by means of shared variables, some systems still use message passing, because it is simpler to implement and makes the cooperation independent of the physical configuration of the system.
Example: All Unix systems support inter-process communication using messages, but most do not allow processes to share memory.
Example: One of the design goals of Ada was to support concurrent processing. Though Ada allows shared variables, its basic cooperation mechanism is based on message-passing.
The basic idea is this: the operating system makes the following primitive operations available to processes:
send destination-specifier message
receive [source-specifier] message
Message is usually a sequence of bytes of arbitrary length whose interpretation is determined by the cooperating processes (i.e. the operating system simply passes the bytes from one process to the other.)
The send operation must specify somehow where the message is to go; but different systems do this in different ways.
The receive operation may or may not permit the receiver to specify that it only wants to receive from a particular source.
The designer of a message passing system must consider quite a number of basic questions, including the following:
How does a sender specify a destination?
Some systems set up a communication channel between processes, so that the sender specifies the destination by channel. (This is analogous to the idea of opening a file by name and then doing all further IO operations by specifying the channel that was named in the open call.)
Other systems require the sender to specify the name or process id of the intended recipient.
If communication channels are used, are they tied to two specific processes?
Most systems allow several different processes to send messages on the same channel.
Some systems allow several different processes to receive from the same channel. In this case, the sender may not necessarily know which process will actually get the message. This is fine if all possible recipients provide the same basic service.
If several processes can receive from the same channel, another question that must be answered is: Does each message go to all processes connected to that channel or only to the first receiver? (The answer is usually the latter.)
Can several messages be queued in one channel or for one process? If so, is there any guarantee as to the order in which they will be delivered to subsequent receiver operations (FIFO)?
Is the sender of a message forced to wait until the receiver has responded before proceeding?
Some systems require this; others make it an option.
If the sender waits, does it wait until the receiver gets the message, or does it wait further until the receiver sends some sort of reply back? (Some systems provide both options.)
If a process tries to do a receive when no messages are available, does it wait until a message arrives?
Most systems allow a receiver to specify that it wants to continue execution of no messages are waiting. In this case, it will have to try the receive operation again later.
Some systems provide a facility whereby an incoming message can interrupt the execution of the receiver to notify it that a message is waiting.
If a receiver can specify a channel on which it wants to receive a message, can it specify some sort of list of channels, such that if a message is pending on any of them it will get it?
If a receiver can specify several different possible channels in a receive operation, is there any guarantee as to which channel it will receive from if messages are pending on several of them?
Example: VMS Mailboxes:
Mailboxes have names of the form MBAn where n is the logical unit number. The name is assigned by the system. The creator of a mailbox can also give it an additional "logical" name, which is placed in a table where other processes can look it up and get the corresponding "physical" name of the mailbox.
Two system service allow a process to associate itself with a mailbox:
$CREMBX - creates a mailbox and returns its name
$ASSIGN - attaches to an existing mailbox specified by physical or logical name.
Any number of processes can send to a given mailbox, and any number can read from a given mailbox (though usually there is only one reader). The same protection mechanism that is used for files is used to allow the creator of a mailbox to control who may read or write it.
Send and receive are done by using the regular IO system services to write or read a mailbox.
Ordinarily, an attempt to put a message in a mailbox results in the sender waiting until the message is received; but this can be overridden. (If the sender requires a reply back, this must be handled as a separate message going the other way, usually through a different mailbox.)
An attempt to receive a message ordinarily results in a wait if none is there; this too can be overridden.
Multiple messages can be queued in one mailbox, and will be received in the order in which they were sent.
Each is received by only one process.
Note that if a process does a write to a mailbox and (without waiting) turns around and reads the same mailbox it will get its own message back. For this reason, communicating processes may use a pair of mailboxes - one for each direction of information flow.
There is no way for a receiver to specify a list of mailboxes - it must try each one in turn.
If it wishes, a receiver may request the operating system to trigger a procedure (called an Asynchronous System Trap or AST Procedure) within the receiver when a message arrives for a given mailbox.
Example: Berkeley Sockets
The facility we will discuss here originated with the Berkeley version of Unix but is now found on most Unix systems. (Example program.)
The facility is called a socket. Sockets can be used for interprocess communication on the same system and also to send messages over a network to processes on another system.
All Unix systems support a facility called pipes. We briefly discussed pipes before when we talked about how Unix shells can set up command pipelines. (Example program.) On Berkeley Unix, pipelines are implemented using sockets. However sockets provide much greater functionality than pipes do.
The socket facility supports a variety of protocols regarding message format, etc. We will describe only one - in which messages are arbitrary-sized packets of bytes.
The following system services are provided for accessing sockets:
socketpair() creates a pair of connected sockets.
What is sent on one end comes out the other end and vice versa. (This contrasts with VMS mailboxes. A process can write to its end of a pair of connected sockets and then read from the same socket without getting its own message back.)
Normally cooperating processes are set up by a series of fork() operations from a common parent. In this case, the parent will normally create the socket pairs as it creates the processes, and the children will inherit them from the parent.
Alternately, individual sockets may be created by the socket() service and connected via the connect() service. In this case, the bind() and getsockname() services may be used to give a socket a name and to find out the name of a socket of interest. Depending on the protocol in use, multiple connections to the same socket are possible.
In the simplest approach to using sockets (using the stream protocol), data is sent and received as a stream of bytes. This means that no internal record is kept in the socket as to where one message ends and another begins; thus, the receiver must know how long an incoming message is before it can receive it correctly. (This is normally handled by agreement between the sender and receiver.)
The send() and recv() services transmit data over sockets. Send() does not wait until the message is received (but may wait if there is no room for additional data in the stream); recv() may or may not wait if no data is available, depending on the setting of options on the socket.
The select() service can be used to find out which sockets from a list specified by the caller currently have messages pending (if any). The recv() service is then used to actually get the individual messages one at a time in whatever order the receiver wishes to.
Example: The river crossers program: crossers.p.
$Id: concurrency8.html,v 1.4 1998/03/03 23:42:05 senning Exp $
These notes were written by Prof. R. Bjork of Gordon College. In February 1998 they were edited and converted into HTML by J. Senning of Gordon College.