Inter-Process Communication in Linux PDF
Inter-Process Communication in Linux PDF
com
A guide to inter-process
communication in Linux
About Opensource.com
What is Opensource.com?
Marty Kalin
Twitter: @kalin_martin
Introduction
Introduction 5
Chapters
Shared storage 6
Using pipes and message queues 12
Sockets and signals 19
Wrapping up this guide 24
Write for Us 25
Introduction
This guide is about
the following IPC mechanisms:
interprocess communication (IPC) in Linux. The
guide uses code examples in C to clarify
• Shared files
• Shared memory (with semaphores)
• Pipes (named and unnamed)
• Message queues
• Sockets
• Signals
I’ll introduce you to some core concepts before moving on to the first two of these mech-
anisms: shared files and shared memory.
Core concepts
A process is a program in execution, and each process has its own address space, which
comprises the memory locations that the process is allowed to access. A process has
one or more threads of execution, which are sequences of executable instructions: a
single-threaded process has just one thread, whereas a multi-threaded process has more
than one thread. Threads within a process share various resources, in particular, address
space. Accordingly, threads within a process can communicate straightforwardly through
shared memory, although some modern languages (e.g., Go) encourage a more disci-
plined approach such as the use of thread-safe channels. Of interest here is that different
processes, by default, do not share memory.
There are various ways to launch processes that then communicate, and two ways
dominate in the examples that follow:
• A terminal is used to start one process, and perhaps a different terminal is used to
start another.
• The system function fork is called within one process (the parent) to spawn another
process (the child).
The first examples take the terminal approach. The code examples [1] are available in a
ZIP file on my website.
Links
[1] h
ttp://condor.depaul.edu/mkalin
Shared
storage
Learn how processes synchronize with each
other in Linux.
Programmers are
all too familiar with file access, including
#include <stdio.h>
#include <stdlib.h>
arise: the producer and the consumer might lock.l_whence = SEEK_SET; /* base for seek offsets */
lock.l_start = 0; /* 1st byte in file */
access the file at exactly the same time,
lock.l_len = 0; /* 0 here means 'until EOF' */
thereby making the outcome indeterminate.
lock.l_pid = getpid(); /* process id */
To avoid a race condition, the file must be
locked in a way that prevents a conflict be-
int fd; /* file descriptor to identify a file within a process */
tween a write operation and any another
if ((fd = open(FileName, O_RDWR | O_CREAT, 0666)) < 0) /* -1 signals an error */
operation, whether a read or a write. The report_and_exit("open failed...");
locking API in the standard system library
can be summarized as follows: if (fcntl(fd, F_SETLK, &lock) < 0) /** F_SETLK doesn't block, F_SETLKW does **/
• A producer should gain an exclusive lock report_and_exit("fcntl failed to get lock...");
on the file before writing to the file. An ex- else {
clusive lock can be held by one process write(fd, DataString, strlen(DataString)); /* populate data file */
at most, which rules out a race condition fprintf(stderr, "Process %d has written to data file...\n", lock.l_pid);
because no other process can access the }
file until the lock is released.
• A consumer should gain at least a shared /* Now release the lock explicitly. */
lock.l_type = F_UNLCK;
lock on the file before reading from the file.
if (fcntl(fd, F_SETLK, &lock) < 0)
Multiple readers can hold a shared lock at
report_and_exit("explicit unlocking failed...");
the same time, but no writer can access
a file when even a single reader holds a
close(fd); /* close the file: would unlock if needed */
shared lock.
return 0; /* terminating the process would unlock as well */
A shared lock promotes efficiency. If one }
process is just reading a file and not
changing its contents, there is no reason to prevent other • If the producer gains the lock, the program writes two text
processes from doing the same. Writing, however, clearly records to the file.
demands exclusive access to a file. • After writing to the file, the producer changes the lock
The standard I/O library includes a utility function named structure’s l_type field to the unlock value:
fcntl that can be used to inspect and manipulate both exclusive
and shared locks on a file. The function works through a file lock.l_type = F_UNLCK;
descriptor, a non-negative integer value that, within a process,
identifies a file. (Different file descriptors in different processes and calls fcntl to perform the unlocking operation. The
may identify the same physical file.) For file locking, Linux pro- program finishes up by closing the file and exiting (see
vides the library function flock, which is a thin wrapper around Example 2).
fcntl. The first example uses the fcntl function to
expose API details (see Example 1). Example 2. The consumer program
The main steps in the producer program above #include <stdio.h>
can be summarized as follows: #include <stdlib.h>
• The program declares a variable of type struct #include <fcntl.h>
flock, which represents a lock, and initializes the #include <unistd.h>
structure’s five fields. The first initialization:
#define FileName "data.dat"
if (fcntl(fd, F_SETLK, &lock) < 0) lock.l_type = F_RDLCK; /* prevents any writing during the reading */
if (fcntl(fd, F_SETLK, &lock) < 0)
tries to lock the file exclusively, checking wheth- report_and_exit("can't get a read-only lock...");
er the call succeeded. In general, the fcntl func-
/* Read the bytes (they happen to be ASCII codes) one at a time. */
tion returns -1 (hence, less than zero) to indicate
int c; /* buffer for read bytes */
failure. The second argument F_SETLK means
while (read(fd, &c, 1) > 0) /* 0 signals EOF */
that the call to fcntl does not block: the function write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */
returns immediately, either granting the lock or in-
dicating failure. If the flag F_SETLKW (the W at /* Release the lock explicitly. */
the end is for wait) were used instead, the call to lock.l_type = F_UNLCK;
fcntl would block until gaining the lock was pos- if (fcntl(fd, F_SETLK, &lock) < 0)
sible. In the calls to fcntl, the first argument fd is report_and_exit("explicit unlocking failed...");
The consumer program is more complicated than neces- cess involves reading or writing. As always, programming
sary to highlight features of the locking API. In particular, the comes with tradeoffs. The next example has the upside of
consumer program first checks whether the file is exclusively IPC through shared memory, rather than shared files, with a
locked and only then tries to gain a shared lock. The relevant corresponding boost in performance.
code is:
Shared memory
lock.l_type = F_WRLCK; Linux systems provide two separate APIs for shared mem-
... ory: the legacy System V API and the more recent POSIX
fcntl(fd, F_GETLK, &lock); /*
sets lock.l_type to F_UNLCK if no one. These APIs should never be mixed in a single appli-
write lock */ cation, however. A downside of the POSIX approach is
if (lock.l_type != F_UNLCK) that features are still in development and dependent upon
report_and_exit("file is still write locked..."); the installed kernel version, which impacts code portabil-
ity. For example, the POSIX API, by default, implements
The F_GETLK operation specified in the fcntl call checks shared memory as a memory-mapped file: for a shared
for a lock, in this case, an exclusive lock given as F_WRLCK memory segment, the system maintains a backing file with
in the first statement above. If the specified lock does not corresponding contents. Shared memory under POSIX
exist, then the fcntl call automatically changes the lock type can be configured without a backing file, but this may im-
field to F_UNLCK to indicate this fact. If the file is exclusively pact portability. My example uses the POSIX API with a
locked, the consumer terminates. (A more robust version of backing file, which combines the benefits of memory ac-
the program might have the consumer sleep a bit and try cess (speed) and file storage (persistence).
again several times.) The shared-memory example has two programs, named
If the file is not currently locked, then the consumer tries memwriter and memreader, and uses a semaphore to co-
to gain a shared (read-only) lock (F_RDLCK). To shorten ordinate their access to the shared memory. Whenever
the program, the F_GETLK call to fcntl could be dropped shared memory comes into the picture with a writer, whether
because the F_RDLCK call would fail if a read-write lock in multi-processing or multi-threading, so does the risk of a
already were held by some other process. Recall that a memory-based race condition; hence, the semaphore is used
read-only lock does prevent any other process from writing to coordinate (synchronize) access to the shared memory.
to the file, but allows other processes to
read from the file. In short, a shared lock Example 3. Source code for the memwriter process
can be held by multiple processes. After /** Compilation: gcc -o memwriter memwriter.c -lrt -lpthread **/
gaining a shared lock, the consumer pro- #include <stdio.h>
gram reads the bytes one at a time from #include <stdlib.h>
the file, prints the bytes to the standard #include <sys/mman.h>
output, releases the lock, closes the file, #include <sys/stat.h>
and terminates. #include <fcntl.h>
Here is the output from the two pro- #include <unistd.h>
grams launched from the same terminal #include <semaphore.h>
with % as the command line prompt: #include <string.h>
#include "shmem.h"
% ./producer
Process 29255 has written to data file...
void report_and_exit(const char* msg) {
perror(msg);
% ./consumer
exit(-1);
Now is the winter of our discontent
}
Made glorious summer by this sun of York
int main() {
In this first code example, the data shared
int fd = shm_open(BackingFile, /* name from smem.h */
through IPC is text: two lines from Shake-
O_RDWR | O_CREAT, /* read/write, create if needed */
speare’s play Richard III. Yet, the shared
AccessPerms); /* access permissions (0644) */
file’s contents could be voluminous, arbi-
if (fd < 0) report_and_exit("Can't open shared mem segment...");
trary bytes (e.g., a digitized movie), which
makes file sharing an impressively flexible
ftruncate(fd, ByteSize); /* get the bytes */
IPC mechanism. The downside is that file
access is relatively slow, whether the ac-
The memwriter program should be started first in its own • The memwriter then calls the mmap function:
terminal. The memreader program then can be started
(within a dozen seconds) in its own terminal. The output caddr_t memptr = mmap(NULL, /* let system pick where to
from the memreader is: put segment */
ByteSize, /* how many bytes */
This is the way the world ends... PROT_READ | PROT_WRITE, /* access
protections */
Each source file has documentation at the top explaining the MAP_SHARED, /* mapping visible to other
link flags to be included during compilation. processes */
Let’s start with a review of how semaphores work as a fd, /* file descriptor */
synchronization mechanism. A general semaphore also is 0); /* offset: start at 1st byte */
called a counting semaphore, as it has a value (typically
initialized to zero) that can be incremented. Consider a to get a pointer to the shared memory. (The memreader
shop that rents bicycles, with a hundred of them in stock, makes a similar call.) The pointer type caddr_t starts
with a program that clerks use to do the rentals. Every with a c for calloc, a system function that initializes dy-
time a bike is rented, the semaphore is incremented by namically allocated storage to zeroes. The memwriter
one; when a bike is returned, the semaphore is decre- uses the memptr for the later write operation, using the
mented by one. Rentals can continue until the value hits library strcpy (string copy) function.
100 but then must halt until at least one
bike is returned, thereby decrementing Example 3. Source code for the memwriter process (continued)
the semaphore to 99. caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
A binary semaphore is a special case ByteSize, /* how many bytes */
requiring only two values: 0 and 1. In PROT_READ | PROT_WRITE, /* access protections */
this situation, a semaphore acts as a MAP_SHARED, /* mapping visible to other processes */
mutex: a mutual exclusion construct.
fd, /* file descriptor */
The shared-memory example uses a
0); /* offset: start at 1st byte */
semaphore as a mutex. When the sema-
if ((caddr_t) -1 == memptr) report_and_exit("Can't get segment...");
phore’s value is 0, the memwriter alone
can access the shared memory. After
fprintf(stderr, "shared mem address: %p [0..%d]\n", memptr, ByteSize - 1);
writing, this process increments the
fprintf(stderr, "backing file: /dev/shm%s\n", BackingFile );
semaphore’s value, thereby allowing the
memreader to read the shared memory
/* semaphore code to lock the shared mem */
(see Example 3).
sem_t* semptr = sem_open(SemaphoreName, /* name */
Here’s an overview of how the memwrit-
O_CREAT, /* create the semaphore */
er and memreader programs communi-
cate through shared memory: AccessPerms, /* protection perms */
instead. The MAP_SHARED flag indicates that the allocated The memreader, like the memwriter, accesses the sema-
memory is shareable among processes, and the last argu- phore through its name in a call to sem_open. But the mem-
ment (in this case, zero) means that the offset for the shared reader then goes into a wait state until the memwriter incre-
memory should be the first byte. The size argument speci- ments the semaphore, whose initial value is 0:
fies the number of bytes to be allocated (in this case, 512),
and the protection argument indicates that the shared mem- if (!sem_wait(semptr)) { /* wait until semaphore != 0 */
ory can be written and read.
When the memwriter program executes successfully, the Once the wait is over, the memreader reads the ASCII bytes
system creates and maintains the backing file; on my sys- from the shared memory, cleans up, and terminates.
tem, the file is /dev/shm/shMemEx, with shMemEx as my The shared-memory API includes operations explicitly to
name (given in the header file shmem.h) for the shared stor- synchronize the shared memory segment and the backing
age. In the current version of the memwriter and memreader file. These operations have been omitted from the example
programs, the statement: to reduce clutter and keep the focus on the memory-sharing
and semaphore code.
shm_unlink(BackingFile); /* removes backing file */ The memwriter and memreader programs are likely to exe-
cute without inducing a race condition even if the semaphore
removes the backing file. If the unlink statement is omitted, code is removed: the memwriter creates the shared memory
then the backing file persists after the program terminates. segment and writes immediately to it; the memreader cannot
even access the shared memory until this has
Example 4. Source code for the memreader process (continued) been created. However, best practice requires
if ((caddr_t) -1 == memptr) report_and_exit("Can't access segment..."); that shared-memory access is synchronized
whenever a write operation is in the mix, and
/* create a semaphore for mutual exclusion */ the semaphore API is important enough to be
sem_t* semptr = sem_open(SemaphoreName, /* name */ highlighted in a code example.
O_CREAT, /* create the semaphore */
AccessPerms, /* protection perms */ Wrapping up
0); /* initial value */ The shared-file and shared-memory exam-
if (semptr == (void*) -1) report_and_exit("sem_open"); ples show how processes can communicate
through shared storage, files in one case and
/* use semaphore as a mutex (lock) by waiting for writer to increment it */ memory segments in the other. The APIs for
if (!sem_wait(semptr)) { /* wait until semaphore != 0 */ both approaches are relatively straightfor-
int i; ward. Do these approaches have a common
for (i = 0; i < strlen(MemContents); i++) downside? Modern applications often deal with
write(STDOUT_FILENO, memptr + i, 1); /* one byte at a time */ streaming data, indeed, with massively large
sem_post(semptr);
streams of data. Neither the shared-file nor the
}
shared-memory approaches are well suited for
massive data streams. Channels of one type or
/* cleanup */
another are better suited. Part 2 thus introduc-
munmap(memptr, ByteSize);
es channels and message queues, again with
close(fd);
code examples in C.
sem_close(semptr);
unlink(BackingFile);
return 0;
}
In the contrived example, the sleep process does not parent in case of failure. In the pipeUN example, the
write any bytes to the channel but does terminate after call is:
about five seconds, which sends an end-of-stream mark-
er to the channel. In the meantime, the echo process im- pid_t cpid = fork(); /* called in parent */
mediately writes the greeting to the standard output (the
screen) because this process does not read any bytes The returned value is stored, in this example, in the variable
from the channel, so it does no waiting. Once the sleep cpid of integer type pid_t. (Every process has its own pro-
and echo processes terminate, the unnamed pipe—not cess ID, a non-negative integer that identifies the process.)
used at all for communication—goes away and the com- Forking a new process could fail for several reasons, includ-
mand line prompt returns. ing a full process table, a structure that the system maintains
Here is a more useful example using two unnamed pipes. to track processes. Zombie processes, clarified shortly, can
Suppose that the file test.dat looks like this: cause a process table to fill if these are not harvested.
• If the fork call succeeds, it thereby spawns (creates) a One more aspect of the program needs clarification: the
new child process, returning one value to the parent but a call to the wait function in the parent code. Once spawned,
different value to the child. Both the parent and the child a child process is largely independent of its parent, as even
process execute the same code that follows the call to the short pipeUN program illustrates. The child can execute
fork. (The child inherits copies of all the variables declared arbitrary code that may have nothing to do with the parent.
so far in the parent.) In particular, a successful call to fork However, the system does notify the parent through a sig-
returns: nal—if and when the child terminates.
• Zero to the child process What if the parent terminates before the child? In this
• The child’s process ID to the parent case, unless precautions are taken, the child becomes and
• An if/else or equivalent construct typically is used after a remains a zombie process with an entry in the process table.
successful fork call to segregate code meant for the par- The precautions are of two broad types. One precaution is
ent from code meant for the child. In this example, the con- to have the parent notify the system that the parent has no
struct is: interest in the child’s termination:
If forking a child succeeds, the pipeUN program proceeds as wait(NULL); /* called in parent */
follows. There is an integer array:
This call to wait means wait until the termination of any child
int pipeFDs[2]; /* two file descriptors */ occurs, and in the pipeUN program, there is only one child pro-
cess. (The NULL argument could be replaced with the address
to hold two file descriptors, one for writing to the pipe and of an integer variable to hold the child’s exit status.) There is a
another for reading from the pipe. (The array element more flexible waitpid function for fine-grained control, e.g., for
pipeFDs[0] is the file descriptor for the read end, and the specifying a particular child process among several.
array element pipeFDs[1] is the file descriptor for the write The pipeUN program takes another precaution. When the
end.) A successful call to the system pipe function, made parent is done waiting, the parent terminates with the call to
immediately before the call to fork, populates the array with the regular exit function. By contrast, the child terminates
the two file descriptors: with a call to the _exit variant, which fast-tracks notification
of termination. In effect, the child is telling the system to noti-
if (pipe(pipeFDs) < 0) report_and_exit("pipeFD"); fy the parent ASAP that the child has terminated.
If two processes write to the same unnamed pipe, can the
The parent and the child now have copies of both file de- bytes be interleaved? For example, if process P1 writes:
scriptors, but the separation of concerns pattern means that
each process requires exactly one of the descriptors. In this foo bar
example, the parent does the writing and the child does
the reading, although the roles could be reversed. The first to a pipe and process P2 concurrently writes:
statement in the child if-clause code, therefore, closes the
pipe’s write end: baz baz
close(pipeFDs[WriteEnd]); /* called in child code */ to the same pipe, it seems that the pipe contents might be
something arbitrary, such as:
and the first statement in the parent else-clause code closes
the pipe’s read end: baz foo baz bar
close(pipeFDs[ReadEnd]); /* called in parent code */ The POSIX standard ensures that writes are not interleaved
so long as no write exceeds PIPE_BUF bytes. On Linux sys-
The parent then writes some bytes (ASCII codes) to the un- tems, PIPE_BUF is 4,096 bytes in size. My preference with
named pipe, and the child reads these and echoes them to pipes is to have a single writer and a single reader, thereby
the standard output. sidestepping the issue.
% mkfifo tester ## creates a backing file named tester close(fd); /* close pipe: generates end-of-stream marker */
% cat tester ## type the pipe's contents to stdout unlink(pipeName); /* unlink from the implementing file */
The fifoWriter program above can be printf("%i ints sent to the pipe.\n", MaxLoops * ChunkSize * IntsPerChunk);
summarized as follows:
return 0;
• The program creates a named pipe for
}
writing:
The system reclaims the backing file once every process const char* file = "./fifoChannel";
connected to the pipe has performed the unlink operation. int fd = open(file, O_RDONLY);
In this example, there are only two such processes: the
fifoWriter and the fifoReader, both of which do an unlink The file opens as read-only.
operation. • T
he program then goes into a potentially infinite loop, try-
The two programs should be executed in different termi- ing to read a 4-byte chunk on each iteration. The read call:
nals with the same working directory. However, the fifoW-
riter should be started before the fifoReader, as the former ssize_t count = read(fd, &next, sizeof(int));
creates the pipe. The fifoReader then accesses the already
created named pipe (see Example 3). r eturns 0 to indicate end-of-stream, in which case the
The fifoReader program above can be summarized as fol- fifoReader breaks out of the loop, closes the named
lows: pipe, and unlinks the backing file before terminating.
• Because the fifoWriter creates the named pipe, the fifoRe- • After reading a 4-byte integer, the fifoReader checks
ader needs only the standard call open to access the pipe whether the number is a prime. This represents the busi-
through the backing file: ness logic that a production-grade reader might perform on
the received bytes. On a sample run, there
Example 3. The fifoReader program were 37,682 primes among the 768,000 in-
#include <stdio.h> tegers received.
#include <stdlib.h> On repeated sample runs, the fifoReader
#include <string.h> successfully read all of the bytes that the
#include <fcntl.h> fifoWriter wrote. This is not surprising. The
#include <unistd.h> two processes execute on the same host,
taking network issues out of the equation.
unsigned is_prime(unsigned n) { /* not pretty, but efficient */
Named pipes are a highly reliable and ef-
if (n <= 3) return n > 1;
if (0 == (n % 2) || 0 == (n % 3)) return 0;
ficient IPC mechanism and, therefore, in
wide use.
unsigned i; Here is the output from the two programs,
for (i = 5; (i * i) <= n; i += 6) each launched from a separate terminal but
if (0 == (n % i) || 0 == (n % (i + 2))) return 0; with the same working directory:
+-+ +-+ +-+ +-+ the message queue allows other retrieval orders. For ex-
sender--->|3|--->|2|--->|2|--->|1|--->receiver ample, the messages could be retrieved by the receiver in
+-+ +-+ +-+ +-+ the order 3-2-1-2.
The mqueue example consists of two programs, the send-
Of the four messages shown, the one labeled 1 is at the er that writes to the message queue and the receiver that
front, i.e., closest to the receiver. Next come two messag- reads from this queue. Both programs include the header file
es with label 2, and finally, a message labeled 3 at the queue.h shown below in Example 4:
back. If strict FIFO behavior were in play, then the mes- The header file defines a structure type named queued-
sages would be received in the order 1-2-2-3. However, Message, with payload (byte array) and type (integer)
fields. This file also defines symbolic constants
Example 4. The header file queue.h (the #define statements), the first two of which
#define ProjectId 123 are used to generate a key that, in turn, is used
#define PathName "queue.h" /* any existing, accessible file would do */ to get a message queue ID. The ProjectId can
#define MsgLen 4 be any positive integer value, and the PathName
#define MsgCount 6 must be an existing, accessible file—in this case,
the file queue.h. The setup statements in both the
typedef struct {
sender and the receiver programs are:
long type; /* must be of type long */
char payload[MsgLen + 1]; /* bytes in the message */
key_t key = ftok(PathName, ProjectId); /*
generate key
} queuedMessage;
*/
int qid = msgget(key, 0666 | IPC_CREAT); /*
use key
Example 5. The message sender program to get
#include <sys/ipc.h>
#include <sys/msg.h> The ID qid is, in effect, the counterpart of a file
#include <stdlib.h> descriptor for message queues (see Example 5).
#include <string.h> The sender program above sends out six mes-
#include "queue.h" sages, two each of a specified type: the first mes-
sages are of type 1, the next two of type 2, and
void report_and_exit(const char* msg) {
the last two of type 3. The sending statement:
perror(msg);
exit(-1); /* EXIT_FAILURE */
msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT);
}
% ./sender % ./receiver
msg1 sent as type 1 msg5 received as type 3
msg2 sent as type 1 msg1 received as type 1
msg3 sent as type 2 msg3 received as type 2
msg4 sent as type 2 msg2 received as type 1
msg5 sent as type 3 msg6 received as type 3
msg6 sent as type 3 msg4 received as type 2
Example 6. The message receiver program The output above shows that the sender and
#include <stdio.h> the receiver can be launched from the same
#include <sys/ipc.h> terminal. The output also shows that the mes-
#include <sys/msg.h> sage queue persists even after the sender
#include <stdlib.h> process creates the queue, writes to it, and
#include "queue.h" exits. The queue goes away only after the re-
ceiver process explicitly removes it with the
void report_and_exit(const char* msg) {
call to msgctl:
perror(msg);
exit(-1); /* EXIT_FAILURE */
} if (msgctl(qid, IPC_RMID, NULL) < 0) /* remove
queue */
int main() {
key_t key= ftok(PathName, ProjectId); /* key to identify the queue */ Wrapping up
if (key < 0) report_and_exit("key not gotten...");
The pipes and message queue APIs are fun-
damentally unidirectional: one process writes
int qid = msgget(key, 0666 | IPC_CREAT); /* access if created already */
if (qid < 0) report_and_exit("no access to queue...");
and another reads. There are implementa-
tions of bidirectional named pipes, but my
int types[] = {3, 1, 2, 1, 3, 2}; /* different than in sender */ two cents is that this IPC mechanism is at
int i; its best when it is simplest. As noted earlier,
for (i = 0; i < MsgCount; i++) { message queues have fallen in popularity—
queuedMessage msg; /* defined in queue.h */ but without good reason; these queues are
if (msgrcv(qid, &msg, sizeof(msg), types[i], MSG_NOERROR | IPC_NOWAIT) < 0)
yet another tool in the IPC toolbox. Part 3
puts("msgrcv trouble...");
completes this quick tour of the IPC toolbox
printf("%s received as type %i\n", msg.payload, (int) msg.type);
} with code examples of IPC through sockets
and signals.
/** remove the queue **/
if (msgctl(qid, IPC_RMID, NULL) < 0) /* NULL = 'no flags' */ Links
report_and_exit("trouble removing queue..."); [1] http://man7.org/linux/man-pages/man2/
mq_open.2.html
return 0;
[2] http://man7.org/linux/man-pages/man2/
}
mq_open.2.html#ATTRIBUTES
/* bind the server's local address in memory */ The socket call in full is:
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr)); /* clear the bytes */
int sockfd = socket(AF_INET, /* versus AF_LOCAL */
saddr.sin_family = AF_INET; /* versus AF_LOCAL */
saddr.sin_addr.s_addr = htonl(INADDR_ANY); /* host-to-network endian */ SOCK_STREAM, /*
reliable,
saddr.sin_port = htons(PortNumber); /* for listening */ bidirectional */
0); /*
system picks
if (bind(fd, (struct sockaddr *) &saddr, sizeof(saddr)) < 0) protocol (TCP) */
report("bind", 1); /* terminate */
The first argument is the socket’s file descriptor and Example 2. The socket client
the second specifies how many client connections #include <string.h>
can be accommodated before the server issues a #include <stdio.h>
connection refused error on an attempted connec- #include <stdlib.h>
tion. (MaxConnects is set to 8 in the header file #include <unistd.h>
#include <sys/types.h>
sock.h.)
#include <sys/socket.h>
The accept call defaults to a blocking wait: the #include <arpa/inet.h>
server does nothing until a client attempts to connect #include <netinet/in.h>
and then proceeds. The accept function returns -1 to #include <netinet/tcp.h>
indicate an error. If the call succeeds, it returns anoth- #include <netdb.h>
er file descriptor—for a read/write socket in contrast #include "sock.h"
nated code), with SIGSTOP (pause) and SIGKILL (termi- Signals can arise in user interaction. For example, a
nate immediately) as the two notable exceptions. Symbolic user hits Ctrl+C from the command line to terminate a pro-
constants such as SIGKILL have integer values, in this gram started from the command-line; Ctrl+C generates a
case, 9. SIGTERM signal. SIGTERM for terminate, unlike SIGKILL,
can be either blocked or handled. One pro-
Example 3. The graceful shutdown of a multi-processing system cess also can signal another, thereby mak-
#include <stdio.h> ing signals an IPC mechanism.
#include <signal.h> Consider how a multi-processing applica-
#include <stdlib.h> tion such as the Nginx web server might be
#include <unistd.h> shut down gracefully from another process.
#include <sys/wait.h>
The kill function:
void graceful(int signum) {
printf("\tChild confirming received signal: %i\n", signum); in
t kill(pid_t pid, int signum);
puts("\tChild about to terminate gracefully..."); /* declaration */
sleep(1);
puts("\tChild terminating now...");
can be used by one process to terminate
_exit(0); /* fast-track notification of parent */
}
another process or group of processes. If
the first argument to function kill is greater
void set_handler() { than zero, this argument is treated as the pid
struct sigaction current; (process ID) of the targeted process; if the
sigemptyset(¤t.sa_mask); /* clear the signal set */
argument is zero, the argument identifies the
current.sa_flags = 0; /* enables setting sa_handler, not sa_action */
group of processes to which the signal send-
current.sa_handler = graceful; /* specify a handler */
sigaction(SIGTERM, ¤t, NULL); /* register the handler */ er belongs.
} The second argument to kill is either a
standard signal number (e.g., SIGTERM
void child_code() { or SIGKILL) or 0, which makes the call to
set_handler();
signal a query about whether the pid in the
while (1) { /** loop until interrupted **/ first argument is indeed valid. The graceful
sleep(1); shutdown of a multi-processing application
puts("\tChild just woke up, but going back to sleep."); thus could be accomplished by sending a
} terminate signal—a call to the kill function
}
with SIGTERM as the second argument—to
void parent_code(pid_t cpid) {
the group of processes that make up the ap-
puts("Parent sleeping for a time..."); plication. (The Nginx master process could
sleep(5); terminate the worker processes with a call to
kill and then exit itself.) The kill function, like
/* Try to terminate child. */
so many library functions, houses power and
if (-1 == kill(cpid, SIGTERM)) {
perror("kill");
flexibility in a simple invocation syntax (see
exit(-1); Example 3).
} The shutdown program above simulates
wait(NULL); /** wait for child to terminate **/ the graceful shutdown of a multi-processing
puts("My child terminated, about to exit myself...");
system, in this case, a simple one consisting
}
of a parent process and a single child pro-
int main() { cess. The simulation works as follows:
pid_t pid = fork(); • The parent process tries to fork a child. If
if (pid < 0) { the fork succeeds, each process executes
perror("fork"); its own code: the child executes the func-
return -1; /* error */
tion child_code, and the parent executes
}
if (0 == pid) the function parent_code.
child_code(); • The child process goes into a potential-
else ly infinite loop in which the child sleeps
parent_code(pid); for a second, prints a message, goes
return 0; /* normal */
back to sleep, and so on. It is precisely
}
a SIGTERM signal from the parent that
causes the child to execute the signal-handling callback puts("Parent sleeping for a time...");
function graceful. The signal thus breaks the child pro- sleep(5);
cess out of its loop and sets up the graceful termination of if (-1 == kill(cpid, SIGTERM)) {
both the child and the parent. The child prints a message ...
before terminating.
• The parent process, after forking the child, sleeps for If the kill call succeeds, the parent does a wait on the
five seconds so that the child can execute for a while; child’s termination to prevent the child from becoming a
of course, the child mostly sleeps in this simulation. The permanent zombie; after the wait, the parent exits.
parent then calls the kill function with SIGTERM as the • The child_code function first calls set_handler and then
second argument, waits for the child to terminate, and goes into its potentially infinite sleeping loop. Here is the
then exits. set_handler function for review:
Here is the output from a sample run:
void set_handler() {
% ./shutdown struct sigaction current; /* current setup */
Parent sleeping for a time... sigemptyset(¤t.sa_mask); /* clear the signal set */
Child just woke up, but going back to sleep. current.sa_flags = 0; /*
for setting sa_handler,
Child just woke up, but going back to sleep. not sa_action */
Child just woke up, but going back to sleep. current.sa_handler = graceful; /* specify a handler */
Child just woke up, but going back to sleep. sigaction(SIGTERM, ¤t, NULL); /* register the handler */
Child confirming received signal: 15 ## SIGTERM is 15 }
Child about to terminate gracefully...
Child terminating now... The first three lines are preparation. The fourth statement sets
My child terminated, about to exit myself... the handler to the function graceful, which prints some mes-
sages before calling _exit to terminate. The fifth and last state-
For the signal handling, the example uses the sigaction ment then registers the handler with the system through the
library function (POSIX recommended) rather than the call to sigaction. The first argument to sigaction is SIGTERM
legacy signal function, which has portability issues. Here for terminate, the second is the current sigaction setup, and
are the code segments of chief interest: the last argument (NULL in this case) can be used to save a
• If the call to fork succeeds, the parent executes the previous sigaction setup, perhaps for later use.
parent_code function and the child executes the child_ Using signals for IPC is indeed a minimalist approach, but
code function. The parent waits for five seconds before a tried-and-true one at that. IPC through signals clearly be-
signaling the child: longs in the IPC toolbox.
Write for Us
In 2010, Red Hat CEO Jim Whitehurst announced the launch of Opensource.com
in a post titled Welcome to the conversation on Opensource.com. He explained,
“This site is one of the ways in which Red Hat gives something back to the open
source community. Our desire is to create a connection point for conversations
about the broader impact that open source can have—and is having—even beyond
the software world.” he wrote, adding, “All ideas are welcome, and all participants
are welcome. This will not be a site for Red Hat, about Red Hat. Instead, this will be
a site for open source, about the future.”
More than 60% of our content is contributed by members of open source communities,
and additional articles are written by the editorial team and other Red Hat contributors.
A small, international team of staff editors and Community Moderators work closely
with contributors to curate, polish, publish, and promote open source stories from
around the world.
Would you like to write for us? Send pitches and inquiries to [email protected].