Notes for Advanced Linux Programming - 5. Interprocess Communication
5. Interprocess Communication
- Five types of interprocess communication:
- Shared memory permits processes to communicate by simply reading and writing to a specified memory location.
- Mapped memory is similar to shared memory, except that it is associated with a file in the filesystem.
- Pipes permit sequential communication from one process to a related process.
- FIFOs are similar to pipes, except that unrelated processes can communicate because the pipe is given a name in the filesystem.
- Sockets support communication between unrelated processes even on different computers.
5.1 Shared Memory
- Shared memory is the fastest form of interprocess communication because all processes share the same piece of memory.
- The kernel does not synchronize accesses to shared memory, you must provide your own synchronization.
- To use a shared memory segment, one process must allocate the segment.
- The process desiring to access the segment must attach the segment.
- After finishing using the segment, each process detaches the segment.
- At some point, one process must deallocate the segment.
- All shared memory segments are allocated as integral multiples of the system’s page size (4KB).
5.1.1. Allocation
- A process allocates a shared memory segment using shmget
- The first parameter is an integer key for the segment.
- Different processes can access the same shared segment by specifying the same key value.
- Using IPC_PRIVATE as the key value guarantees that a brand new memory segment is created.
- For multiple processes to use a shared segment, they must make arrangements to use the same key.
- The second parameter specifies the number of bytes in the segment.
- The third parameter is the bitwise or of flag values
- IPC_CREAT: A new segment should be created.
- IPC_EXCL: This flag is always used with IPC_CREAT. It will causes shmget to fail if a segment key is specified that already exists.
- Mode flags: This value is made of 9 bits indicating permissions granted to owner, group, and world to control access to the segment.
5.1.2. Attachment and Detachment
- To make the shared memory segment available, a process must use shmat:
- The first argument is the shared memory segment identifier SHMID returned by shmget.
- The second argument is a pointer that specifies where in your process’s address space you want to map the shared memory.
- If you specify NULL, Linux will choose an available address.
- The third argument is a flag:
- SHM_RND indicates that the address specified for the second parameter should be rounded down to a multiple of the page size.
- SHM_RDONLY indicates that the segment will be only read, not written.
- It returns the address of the attached shared segment.
- The segment can be detached using shmdt.
- Pass it the address returned by shmat.
- If the segment has been deallocated and this was the last process using it, it is removed.
- Calls to exit and any of the exec family automatically detach segments.
5.1.3. Controlling and Deallocating Shared Memory
- The shmctl call returns information about a shared memory segment and can modify it.
- The first parameter is a shared memory segment identifier.
- To obtain information about a shared memory segment, pass IPC_STAT as the second argument and a pointer to a struct shmid_ds.
- To remove a segment, pass IPC_RMID as the second argument, and pass NULL as the third argument
- Each shared memory segment should be explicitly deallocated using shmctl when you’re finished with it.
- Invoking exit and exec detaches memory segments but does not deallocate them.
#include <stdio.h>
#include <sys/shm.h>
#include <sys/stat.h>
int main ()
{
int segment_id;
char* shared_memory;
struct shmid_ds shmbuffer;
int segment_size;
const int shared_segment_size = 0x6400;
/* Allocate a shared memory segment. */
segment_id = shmget (IPC_PRIVATE, shared_segment_size, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
/* Attach the shared memory segment. */
shared_memory = (char*) shmat (segment_id, 0, 0);
printf (“shared memory attached at address %p\n”, shared_memory);
/* Determine the segment’s size. */
shmctl (segment_id, IPC_STAT, &shmbuffer);
segment_size = shmbuffer.shm_segsz;
printf (“segment size: %d\n”, segment_size);
/* Write a string to the shared memory segment. */
sprintf (shared_memory, “Hello, world.”);
/* Detach the shared memory segment. */
shmdt (shared_memory);
/* Reattach the shared memory segment, at a different address. */
shared_memory = (char*) shmat (segment_id, (void*) 0x5000000, 0);
printf (“shared memory reattached at address %p\n”, shared_memory);
/* Print out the string from shared memory. */
printf (“%s\n”, shared_memory);
/* Detach the shared memory segment. */
shmdt (shared_memory);
/* Deallocate the shared memory segment. */
shmctl (segment_id, IPC_RMID, 0);
return 0;
}
- The ipcs command provides information on interprocess communication facilities.
- Use the -m flag to obtain information about shared memory.
% ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 1627649 user 640 25600 0
- The ipcrm command can be used to remove it.
% ipcrm shm 1627649
[liuchao@localhost ~]$ ipcs
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 196608 liuchao 600 393216 2 dest
0x764867bd 65537 liuchao 600 1 0
0x2c0056d5 98306 liuchao 600 1 0
0x500e7827 131075 liuchao 600 1 0
0x20e0f21d 163844 liuchao 600 1 0
0x00000000 229381 liuchao 600 393216 2 dest
0x00000000 262150 liuchao 600 393216 2 dest
0x00000000 294919 liuchao 600 393216 2 dest
0x00000000 327688 liuchao 600 393216 2 dest
0x00000000 360457 liuchao 600 393216 2 dest
0x00000000 393226 liuchao 600 393216 2 dest
0x00000000 425995 liuchao 600 393216 2 dest
0x00000000 458764 liuchao 600 393216 2 dest
0x00000000 491533 liuchao 600 393216 2 dest
0x00000000 557070 liuchao 600 393216 2 dest
0x00000000 589839 liuchao 600 393216 2 dest
------ Semaphore Arrays --------
key semid owner perms nsems
0x59d9bc4a 0 liuchao 600 1
0x3bd464f2 32769 liuchao 600 1
------ Message Queues --------
key msqid owner perms used-bytes messages
5.2 Processes Semaphores
5.2.1. Allocation and Deallocation
- The calls semget and semctl allocate and deallocate semaphores.
- Invoke semget with a key specifying a semaphore set, the number of semaphores in the set, and permission flags as for shmget; the return value is a semaphore set identifier.
- Invoke semctl with the semaphore identifier, the number of semaphores in the set, IPC_RMID as the third argument, and any union semun value as the fourth argument (which is ignored).
- Semaphores continue to exist even after all processes using them have terminated.
- The last process to use a semaphore set must explicitly remove it.
- Allocating and Deallocating a Binary Semaphore
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
/* We must define union semun ourselves. */
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
/* Obtain a binary semaphore’s ID, allocating if necessary. */
int binary_semaphore_allocation (key_t key, int sem_flags)
{
return semget (key, 1, sem_flags);
}
/* Deallocate a binary semaphore. All users must have finished their
use. Returns -1 on failure. */
int binary_semaphore_deallocate (int semid)
{
union semun ignored_argument;
return semctl (semid, 1, IPC_RMID, ignored_argument);
}
5.2.2. Initializing Semaphores
- Initializing a Binary Semaphore
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
/* We must define union semun ourselves. */
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
/* Initialize a binary semaphore with a value of 1. */
int binary_semaphore_initialize (int semid)
{
union semun argument;
unsigned short values[1];
values[0] = 1;
argument.array = values;
return semctl (semid, 0, SETALL, argument);
}
5.2.3. Wait and Post Operations
- The semop system call support wait and post operations.
- The first parameter specifies a semaphore set identifier.
- The second parameter is an array of struct sembuf elements.
- The third parameter is the length of this array.
- The fields of struct sembuf:
- sem_num is the semaphore number in the semaphore set on which the operation is performed.
- sem_op is an integer that specifies the semaphore operation.
- If sem_op is a positive number, that number is added to the semaphore value.
- If sem_op is a negative number, the get the absolute value.
- If this would make the semaphore value negative, the call blocks until the semaphore value becomes as large as the absolute value of sem_op.
- If sem_op is zero, the operation blocks until the semaphore value becomes zero.
- sem_flg is a flag value.
- IPC_NOWAIT to prevent the operation from blocking;
- SEM_UNDO will let Linux automatically undoes the operation on the semaphore when the process exits.
- Wait and Post Operations for a Binary Semaphore
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
/* Wait on a binary semaphore. Block until the semaphore value is positive, then
decrement it by 1. */
int binary_semaphore_wait (int semid)
{
struct sembuf operations[1];
/* Use the first (and only) semaphore. */
operations[0].sem_num = 0;
/* Decrement by 1. */
operations[0].sem_op = -1;
/* Permit undo’ing. */
operations[0].sem_flg = SEM_UNDO;
return semop (semid, operations, 1);
}
/* Post to a binary semaphore: increment its value by 1.
This returns immediately. */
int binary_semaphore_post (int semid)
{
struct sembuf operations[1];
/* Use the first (and only) semaphore. */
operations[0].sem_num = 0;
/* Increment by 1. */
operations[0].sem_op = 1;
/* Permit undo’ing. */
operations[0].sem_flg = SEM_UNDO;
return semop (semid, operations, 1);
}
5.3 Mapped Memory
5.3.1. Mapping an Ordinary File
- To map an ordinary file to a process’s memory, use the mmap call.
- The first argument is the address at which you would like Linux to map the file into your process’s address space; the value NULL allows Linux to choose an available start address.
- The second argument is the length of the map in bytes.
- The third argument specifies the protection on the mapped address range. PROT_READ or PROT_WRITE or PROT_EXEC.
- The fourth argument is a flag.
- MAP_FIXED: If you specify this flag, Linux uses the address you request to map the file rather than treating it as a hint. This address must be page-aligned.
- MAP_PRIVATE: Writes to the memory range should not be written back to the attached file, but to a private copy of the file. No other process sees these writes.
- MAP_SHARED: Writes are immediately reflected in the underlying file rather than buffering writes.
- The fifth argument is a file descriptor opened to the file to be mapped.
- The last argument is the offset from the beginning of the file from which to start the map
- (mmap-write.c) Write a Random Number to a Memory-Mapped File
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#define FILE_LENGTH 0x100
/* Return a uniformly random number in the range [low,high]. */
int random_range (unsigned const low, unsigned const high)
{
unsigned const range = high - low + 1;
return low + (int) (((double) range) * rand () / (RAND_MAX + 1.0));
}
int main (int argc, char* const argv[])
{
int fd;
void* file_memory;
/* Seed the random number generator. */
srand (time (NULL));
/* Prepare a file large enough to hold an unsigned integer. */
fd = open (argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
lseek (fd, FILE_LENGTH+1, SEEK_SET);
write (fd, “”, 1);
lseek (fd, 0, SEEK_SET);
/* Create the memory mapping. */
file_memory = mmap (0, FILE_LENGTH, PROT_WRITE, MAP_SHARED, fd, 0);
close (fd);
/* Write a random integer to memory-mapped area. */
sprintf((char*) file_memory, “%d\n”, random_range (-100, 100));
/* Release the memory (unnecessary because the program exits). */
munmap (file_memory, FILE_LENGTH);
return 0;
}
- (mmap-read.c) Read an Integer from a Memory-Mapped File, and Double It
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#define FILE_LENGTH 0x100
int main (int argc, char* const argv[])
{
int fd;
void* file_memory;
int integer;
/* Open the file. */
fd = open (argv[1], O_RDWR, S_IRUSR | S_IWUSR);
/* Create the memory mapping. */
file_memory = mmap (0, FILE_LENGTH, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close (fd);
/* Read the integer, print it out, and double it. */
sscanf (file_memory, “%d”, &integer);
printf (“value: %d\n”, integer);
sprintf ((char*) file_memory, “%d\n”, 2 * integer);
/* Release the memory (unnecessary because the program exits). */
munmap (file_memory, FILE_LENGTH);
return 0;
}
5.3.2. Shared Access to a File
- Different processes can communicate using memory-mapped regions associated with the same file.
- Specify the MAP_SHARED flag so that any writes to these regions are immediately transferred to the underlying file and made visible to other processes.
- Otherwise, Linux may buffer writes before transferring them to the file.
- You can force Linux to incorporate buffered writes into the disk file by calling msync.
- The first two parameters specify a memory-mapped region.
- The third parameter can take these flag values:
- MS_ASYNC: The update is scheduled but not necessarily run before the call returns.
- MS_SYNC: The update is immediate; the call to msync blocks until it’s done.
- MS_INVALIDATE: All other file mappings are invalidated so that they can see the updated values.
msync (mem_addr, mem_length, MS_SYNC | MS_INVALIDATE);
- Specifying MAP_PRIVATE to mmap creates a copy-on-write region.
- Any write to the region is reflected only in this process’s memory; other processes that map the same file won’t see the changes.
5.4 Pipes
5.4.1. Creating Pipes
int pipe_fds[2];
int read_fd;
int write_fd;
pipe (pipe_fds);
read_fd = pipe_fds[0];
write_fd = pipe_fds[1];
5.4.2. Communication Between Parent and Child Processes
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
/* Write COUNT copies of MESSAGE to STREAM, pausing for a second between each. */
void writer (const char* message, int count, FILE* stream)
{
for (; count > 0; --count) {
/* Write the message to the stream, and send it off immediately. */
fprintf (stream, “%s\n”, message);
fflush (stream);
/* Snooze a while. */
sleep (1);
}
}
/* Read random strings from the stream as long as possible. */
void reader (FILE* stream)
{
char buffer[1024];
/* Read until we hit the end of the stream. fgets reads until either a newline or the end-of-file. */
while (!feof (stream) && !ferror (stream) && fgets (buffer, sizeof (buffer), stream) != NULL)
fputs (buffer, stdout);
}
int main ()
{
int fds[2];
pid_t pid;
/* Create a pipe. File descriptors for the two ends of the pipe are placed in fds. */
pipe (fds);
/* Fork a child process. */
pid = fork ();
if (pid == (pid_t) 0) {
FILE* stream;
/* This is the child process. Close our copy of the write end of the file descriptor. */
close (fds[1]);
/* Convert the read file descriptor to a FILE object, and read from it. */
stream = fdopen (fds[0], “r”);
reader (stream);
close (fds[0]);
}
else {
/* This is the parent process. */
FILE* stream;
/* Close our copy of the read end of the file descriptor. */
close (fds[0]);
/* Convert the write file descriptor to a FILE object, and write to it. */
stream = fdopen (fds[1], “w”);
writer (“Hello, world.”, 5, stream);
close (fds[1]);
}
return 0;
}
5.4.3. Redirecting the Standard Input, Output, and Error Streams
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main ()
{
int fds[2];
pid_t pid;
/* Create a pipe. File descriptors for the two ends of the pipe are placed in fds. */
pipe (fds);
/* Fork a child process. */
pid = fork ();
if (pid == (pid_t) 0) {
/* This is the child process. Close our copy of the write end of the file descriptor. */
close (fds[1]);
/* Connect the read end of the pipe to standard input. */
dup2 (fds[0], STDIN_FILENO);
/* Replace the child process with the “sort” program. */
execlp (“sort”, “sort”, 0);
}
else {
/* This is the parent process. */
FILE* stream;
/* Close our copy of the read end of the file descriptor. */
close (fds[0]);
/* Convert the write file descriptor to a FILE object, and write to it. */
stream = fdopen (fds[1], “w”);
fprintf (stream, “This is a test.\n”);
fprintf (stream, “Hello, world.\n”);
fprintf (stream, “My dog has fleas.\n”);
fprintf (stream, “This program is great.\n”);
fprintf (stream, “One fish, two fish.\n”);
fflush (stream);
close (fds[1]);
/* Wait for the child process to finish. */
waitpid (pid, NULL, 0);
}
return 0;
}
5.4.4. popen and pclose
- The popen call
- The first argument is executed as a shell command in a subprocess running /bin/sh.
- If the second argument is “r”, the function returns the child process’s standard output stream.
- If the second argument is “w”, the function returns the child process’s standard input stream.
- Call pclose to close a stream returned by popen.
#include <stdio.h>
#include <unistd.h>
int main ()
{
FILE* stream = popen (“sort”, “w”);
fprintf (stream, “This is a test.\n”);
fprintf (stream, “Hello, world.\n”);
fprintf (stream, “My dog has fleas.\n”);
fprintf (stream, “This program is great.\n”);
fprintf (stream, “One fish, two fish.\n”);
return pclose (stream);
}
5.4.5. FIFOs
- A first-in, first-out (FIFO) file is a pipe that has a name in the filesystem.
- FIFOs are also called named pipes.
- You can make a FIFO using the mkfifo command.
% mkfifo /tmp/fifo
% ls -l /tmp/fifo
prw-rw-rw- 1 samuel users 0 Jan 16 14:04 /tmp/fifo
- Creating a FIFO
- Create a FIFO programmatically using the mkfifo function.
- The first argument is the path at which to create the FIFO.
- The second parameter specifies the pipe’s owner, group, and world permissions.
- Accessing a FIFO
- Access a FIFO just like an ordinary file.
- To communicate through a FIFO, one program must open it for writing, and another program must open it for reading.
5.5 Sockets
- Socket: Creates a socket
- When you create a socket, specify the three socket choices:
- Namespace: use constants beginning with PF_ (protocol families). PF_LOCAL or PF_UNIX specifies the local namespace, and PF_INET specifies Internet namespaces.
- communication style: use constants beginning with SOCK_. Use SOCK_STREAM for a connection-style socket, or use SOCK_DGRAM for a datagram-style socket.
- protocol: it specifies the low-level mechanism to transmit and receive data. Because there is usually one best protocol for each such pair, specifying 0 is usually the correct protocol.
- Closes: Destroys a socket
- Connect: Creates a connection between two sockets
- To create a connection between two sockets, the client calls connect, specifying the address of a server socket to connect to.
- A client is the process initiating the connection, and a server is the process waiting to accept connections.
- Bind: Labels a server socket with an address
- Listen: Configures a socket to accept conditions
- Accept: Accepts a connection and creates a new socket for the connection
- A server’s life cycle consists of
- the creation of a connection-style socket,
- binding an address to its socket,
- listen that enables connections to the socket,
- accept incoming connections, and then closing the socket.
5.5.1. Local Namespace Sockets
- Sockets connecting processes on the same computer can use the local namespace represented by the synonyms PF_LOCAL and PF_UNIX.
- Their socket addresses, specified by filenames, are used only when creating connections.
- (socket-server.c) Local Namespace Socket Server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
/* Read text from the socket and print it out. Continue until the
socket closes. Return nonzero if the client sent a “quit”
message, zero otherwise. */
int server (int client_socket)
{
while (1) {
int length;
char* text;
/* First, read the length of the text message from the socket. If read returns zero, the client closed the connection. */
if (read (client_socket, &length, sizeof (length)) == 0)
return 0;
/* Allocate a buffer to hold the text. */
text = (char*) malloc (length);
/* Read the text itself, and print it. */
read (client_socket, text, length);
printf (“%s\n”, text);
/* Free the buffer. */
free (text);
/* If the client sent the message “quit,” we’re all done. */
if (!strcmp (text, “quit”))
return 1;
}
}
int main (int argc, char* const argv[])
{
const char* const socket_name = argv[1];
int socket_fd;
struct sockaddr_un name;
int client_sent_quit_message;
/* Create the socket. */
socket_fd = socket (PF_LOCAL, SOCK_STREAM, 0);
/* Indicate that this is a server. */
name.sun_family = AF_LOCAL;
strcpy (name.sun_path, socket_name);
bind (socket_fd, &name, SUN_LEN (&name));
/* Listen for connections. */
listen (socket_fd, 5);
/* Repeatedly accept connections, spinning off one server() to deal with each client. Continue until a client sends a “quit” message. */
do {
struct sockaddr_un client_name;
socklen_t client_name_len;
int client_socket_fd;
/* Accept a connection. */
client_socket_fd = accept (socket_fd, &client_name, &client_name_len);
/* Handle the connection. */
client_sent_quit_message = server (client_socket_fd);
/* Close our end of the connection. */
close (client_socket_fd);
} while (!client_sent_quit_message);
/* Remove the socket file. */
close (socket_fd);
unlink (socket_name);
return 0;
}
- (socket-client.c) Local Namespace Socket Client
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
/* Write TEXT to the socket given by file descriptor SOCKET_FD. */
void write_text (int socket_fd, const char* text)
{
/* Write the number of bytes in the string, including NUL-termination. */
int length = strlen (text) + 1;
write (socket_fd, &length, sizeof (length));
/* Write the string. */
write (socket_fd, text, length);
}
int main (int argc, char* const argv[])
{
const char* const socket_name = argv[1];
const char* const message = argv[2];
int socket_fd;
struct sockaddr_un name;
/* Create the socket. */
socket_fd = socket (PF_LOCAL, SOCK_STREAM, 0);
/* Store the server’s name in the socket address. */
name.sun_family = AF_LOCAL;
strcpy (name.sun_path, socket_name);
/* Connect the socket. */
connect (socket_fd, &name, SUN_LEN (&name));
/* Write the text on the command line to the socket. */
write_text (socket_fd, message);
close (socket_fd);
return 0;
}
5.5.2. Internet-Domain Sockets
- (socket-inet.c) Read from a WWW Server
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
/* Print the contents of the home page for the server’s socket. Return an indication of success. */
void get_home_page (int socket_fd)
{
char buffer[10000];
ssize_t number_characters_read;
/* Send the HTTP GET command for the home page. */
sprintf (buffer, “GET /\n”);
write (socket_fd, buffer, strlen (buffer));
/* Read from the socket. The call to read may not
return all the data at one time, so keep trying until we run out. */
while (1) {
number_characters_read = read (socket_fd, buffer, 10000);
if (number_characters_read == 0)
return;
/* Write the data to standard output. */
fwrite (buffer, sizeof (char), number_characters_read, stdout);
}
}
int main (int argc, char* const argv[])
{
int socket_fd;
struct sockaddr_in name;
struct hostent* hostinfo;
/* Create the socket. */
socket_fd = socket (PF_INET, SOCK_STREAM, 0);
/* Store the server’s name in the socket address. */
name.sin_family = AF_INET;
/* Convert from strings to numbers. */
hostinfo = gethostbyname (argv[1]);
if (hostinfo == NULL)
return 1;
else
name.sin_addr = *((struct in_addr *) hostinfo->h_addr);
/* Web servers use port 80. */
name.sin_port = htons (80);
/* Connect to the Web server */
if (connect (socket_fd, &name, sizeof (struct sockaddr_in)) == -1) {
perror (“connect”);
return 1;
}
/* Retrieve the server’s home page. */
get_home_page (socket_fd);
return 0;
}