Home | | Multi - Core Architectures and Programming | Multiprocess Programming

Chapter: Multicore Application Programming For Windows, Linux, and Oracle Solaris : Using POSIX Threads

Multiprocess Programming

It is very easy to start multiple processes and have these processes load common initialization parameters from a file or from the command line to start communicating. However, it is often useful to do this programmatically.

Multiprocess Programming

 

An alternative to multithreaded applications is multiprocess applications, as suggested in Chapter 1, “Hardware, Processes, and Threads.” The main advantage of multiprocess pro-gramming is that a failure of one process does not cause all the processes to die, and as a result, it might be possible to recover from such failures.

 

It is very easy to start multiple processes and have these processes load common initialization parameters from a file or from the command line to start communicating. However, it is often useful to do this programmatically.

 

The UNIX model of process creation is the Fork-Exec model. The fork() call makes a child process that receives an exact duplicate of the parent’s memory. The exec() call replaces the current process with a new executable. The calls often go together so that one application calls fork() to create a new process; then the child process immediately calls exec() to replace itself with a new executable.

 

The fork() call is interesting because both the child and parent processes will return from this call. The only difference is the return value of the call. The child process will return with a value of zero, and the parent process will return with the process ID of the child process.

 

The code in Listing 5.55 uses fork() to create a new child process. The child process will execute a sleep command with a parameter of 10 so that it will sleep for ten sec-onds. The parent process will wait for the child to terminate and then report the exit sta-tus of the child process. The execl() call is used to execute the sleep command. The execl() call takes the path to the executable plus the arguments to be passed to that executable, and the first argument should be the name of the executable itself.

 

Listing 5.55   Using Fork to Create a Child Process

#include <unistd.h> #include <stdio.h> #include <sys/wait.h>

 

int main()

 

{

 

int status;

 

pid_t f = fork();

 

if ( f == 0 )

 

{ /* Child process */

 

execl( "/usr/bin/sleep", "/usr/bin/sleep", "10" );

 

}

 

else

 

{

 

waitpid( f, &status, 0 );

 

printf( "Status = %i\n", status );

 

}

 

}

If the fork() call is not followed by an exec() call, we have two copies of the same process. The process state, up to the point at which the fork call was made, is duplicated in both processes. We will see how this can be useful in the following sections on setting up communications between multiple processes.

 

Sharing Memory Between Processes

 

Usually different processes share nothing; however, it is possible to set up multiple processes to share the same memory. The shm_open() call creates a shared memory seg-ment. This call takes a name for the segment, a size, and a set of flags for the created shared memory, together with the permission bits. The return value from the call is a file descriptor of the shared memory segment.

 

The name is a string with the first character as /, and no subsequent slashes. The flags used by the call are familiar from creating files. O_RDONLY will create a read-only seg-ment. O_RDWR will create a segment that permits reading and writing. O_CREAT will create the segment if it does not exist or return a handle to it if it does exist. O_EXCL will return an error if the segment already exists. The final parameters are the file access permissions.

 

The memory segment will be created, if it does not already exist, with a size of 0 bytes. The size of the reserved segment can be set with a call to ftruncate(), passing the file descriptor of the segment together with the requested size.

 

Once the segment exists, the process can attach to it with a call to mmap(). Table 5.1 shows the parameters to mmap.

 

Table 5.1      Parameters Passed to mmap()


The return value of the call to mmap() is a pointer to the shared memory segment. Once the process has finished with the shared memory segment, it can unmap it from its address space using the munmap() call, which takes a pointer to the memory region together with its size as parameters.

The shared memory can be removed from the system with a call to shm_unlink(), which takes the name for the shared region originally given to shm_open(). This causes the shared memory to be deleted when the last process detaches from it.

 

Listing 5.56 shows an example of creating using and deleting shared memory. When compiling this code on Solaris, it is necessary to define at least _POSIX_C_SOURCE= 199309L in order to get the header files to define shm_open() and shm_unlink().

 

Listing 5.56   Creating, Using, and Deleting Shared Memory

#include <sys/mman.h> #include <fcntl.h> #include <unistd.h>

 

int main()

 

{

 

int handle = shm_open( "/shm", O_CREAT|O_RDWR, 0777 ); ftruncate( handle, 1024*1024*sizeof(int) );

 

char * mem = (char*) mmap( 0, 1024*1024*sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED, handle, 0 );

 

for( int i=0; i<1024*1024; i++ ) { mem[i] = 0; }

 

munmap( mem, 1024*1024*sizeof(int) );

 

shm_unlink( "/shm" );

 

}

One use for shared memory is as a location for placing mutexes shared between processes. Listing 5.57 illustrates how a process can form a child process and share a mutex with the child process.

 

Listing 5.57   Sharing a Mutex Between Processes

#include <sys/mman.h> #include <sys/wait.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <pthread.h>

 

int main()

 

{

 

pthread_mutex_t * mutex; pthread_mutexattr_t attributes; pthread_mutexattr_init( &attributes );

pthread_mutexattr_setpshared( &attributes, PTHREAD_PROCESS_SHARED );

 

int handle = shm_open( "/shm", O_CREAT|O_RD_WR, 0777 );

 

ftruncate( handle, 1024*sizeof(int) );

 

char * mem = mmap( 0, 1024*sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED, handle,0 );

 

mutex = (pthread_mutex_t*)mem;

 

pthread_mutex_init( mutex, &attributes );

 

pthread_mutexattr_destroy( &attributes );

 

int ret = 0;

 

int * pcount = (int*)( mem + sizeof(pthread_mutex_t) ); *pcount = 0;

 

pid_t pid = fork(); if (pid == 0)

{  /* Child process */

 

pthread_mutex_lock( mutex );

 

(*pcount)++;

 

pthread_mutex_unlock( mutex );

 

ret = 57;

 

}

 

else

 

{

 

int status;

 

waitpid( pid, &status, 0 );

 

printf( "Child returned %i\n", WEXITSTATUS(status) );

 

pthread_mutex_lock( mutex );

 

(*pcount)++;

 

pthread_mutex_unlock( mutex );

 

printf( "Count = %i\n", *pcount ); pthread_mutex_destroy( mutex );

}

 

munmap( mem, 1024*sizeof(int) ); shm_unlink( "/shm" );

return ret;

 

}

The first thing the parent process does is to set up a mutex that is shared between the parent and child processes. Once this is complete, the parent process forks a child process. The parent process then waits for the child to complete.

 

When the child process is forked, it receives a copy of the memory of the parent process. This gives it access to the shared memory segment as well as the mutex and vari-ables contained in the shared memory segment. The child process acquires the mutex, increments the shared variable, and releases the mutex before unmapping and unlinking the shared memory segment and exiting.

Once the child process has exited, the parent process continues to execute, obtaining the return value of the child process from the call to waitpid(). The macro WEXITSTATUS converts the exit status from waitpid() into the return value from the child process.

The parent process also acquires the mutex and increments the shared variable before releasing the mutex. It then prints the value of the shared variable, which has the value two, indicating that both the parent and the child process incremented it. The final actions of the parent process are to destroy the mutex and then unmap and unlink the shared memory segment.

 

One thing to pay attention to is the alignment of objects created in shared memory segments. Depending on the operating system, there may be constraints on the align-ment. For example, Solaris requires that mutexes are aligned on 8-byte boundaries.

 

Sharing Semaphores Between Processes

 

As discussed in the section on semaphores, it is very easy to create a named semaphore that is shared between multiple processes. Listing 5.58 shows a parent process creating a child process. Both the parent and child process open the same semaphore, and this sem-aphore is used to ensure that the child process completes before the parent process.

 

Listing 5.58   Sharing a Named Semaphore

#include <unistd.h> #include <stdio.h> #include <semaphore.h>

 

int main()

 

{

 

int status; pid_t f = fork(); sem_t * semaphore;

semaphore = sem_open( "/my_semaphore", O_CREAT, 0777, 1 );

 

if ( f == 0 )

 

{ /* Child process */

 

printf( "Child process completed\n" ); sem_post( semaphore );

sem_close( semaphore );

 

}

 

else

 

{

 

sem_t * semaphore;

 

sem_wait( semaphore );

 

printf( "Parent process completed\n" );

 

sem_close( semaphore );

 

sem_unlink( "/my_semaphore" );

 

}

 

}


Message Queues

 

Message queues are a method of passing messages between threads or processes. Messages can be placed into a queue, and they will be read out of the queue in a prioritized first-in, first-out order.

 

To attach to a message queue, the mq_open() function needs to be called, which will return a handle to the message queue. This takes a minimum of the name of the message queue and some flags. The name of the message queue should start with a / and then be up to 13 further characters with no additional / characters. The flags need to be one of O_RDONLY, O_WRONLY, or O_RDWR to produce a read-only message queue, a write-only message queue, or a read-write message queue.

 

The open specified in this way will open an existing message queue or fail if the message queue does not exist. If the additional flag O_CREAT is passed, then the call will create the message queue if it does not already exist. The additional flag O_EXCL can be passed if the call to open the queue should succeed only if the message queue does not exist. If the flag O_CREAT is passed, the call to mq_open() requires two more parameters, a mode setting that is used to set the access permissions for the message queue and a pointer to the attributes for the message queue; if this pointer is null, default values are used for the message queue attributes.

 

The attributes for a message queue are held in an mq_attr structure, which contains the fields mq_maxmsg, which is the maximum number of messages that can be held in the message queue, and mq_msgsize, which is the maximum size of the messages that can be stored in the message queue.

 

The other flag that can be passed to mq_open() is O_NONBLOCK. If this flag is set, any attempts to write to a full message queue or read from an empty one will fail and return immediately. The default is for the thread to block until the message queue has space for sending an additional message, or has a message in it.

 

The function mq_close(), which takes the handle to a message queue as a parame-ter, will close the connection to the message queue. The message queue will continue to exist. To remove the message queue from the system, it is necessary to call mq_unlink(), which takes the name of the message queue as a parameter. After a call to mq_unlink(), the message queue will be removed once all current references to the message queue have been closed.

 

The code in Listing 5.59 demonstrates creating and deleting a message queue.

 

Listing 5.59   Opening and Closing a Message Queue

#include <mqueue.h>

 

int main()

 

{

 

mq_attr attr;

 

mqd_t mqueue;

attr.mq_maxmsg = 1000;

 

attr.mq_msgsize = 500;

 

mqueue = mq_open( "/messages", O_RDWR+O_CREAT, 0777, &attr );

 

...

 

mq_close( mqueue ); mq_unlink( "/messages" );

}

 

 

Message queues are prioritized first-in, first-out (FIFO) queues. Messages are added to the queue using the call mq_send() and received from the queue using the call mq_receive(). If the message queue has been created with the O_NONBLOCK attribute, then these functions will return immediately whether or not they were successful. There are versions of these functions available, which will return after a timeout if they are unsuccessful; these are mq_timedsend() and mq_timedreceive(), which take an absolute time, and mq_reltimedsend_np() and mq_reltimedreceive_np(), which take a relative time.

 

The parameters to mq_send() are the message queue, a pointer to a buffer contain-ing the message, the size of the message, and a priority. Messages with a higher priority will be placed before messages with a lower priority and after messages with the same or higher priorities. The message is copied from the buffer into the queue. The call to mq_send() will fail if the message size is greater than the mq_msgsize attribute for the queue.

 

The call to mq_receive() takes the message queue, a pointer to a buffer where the message can be copied, the size of the buffer, and either a null pointer or a pointer to an unsigned int where the priority of the message will be written. If the size of the buffer is smaller than the size of the message, then the call will fail.

 

Note that sending messages requires at least two copy operations to be performed on the message—once to copy it into the queue and once to copy it out of the queue. Hence, it is advantageous to send short messages and perhaps use shared memory to pass longer items of information.

 

Listing 5.60 shows an example of sending messages between a parent and child process. On Solaris, the code will need to be linked with the real-time extensions library using the compiler flag -lrt.

 

Listing 5.60   Passing Messages Between a Parent Process and a Child Process

#include <unistd.h> #include <stdio.h> #include <mqueue.h> #include <string.h>

 

int main()

 

{

 

int status; pid_t f = fork();

 

if ( f == 0 )

 

{ /* Child process */ mqd_t * queue; char message[20];

 

queue = mq_open( "/messages", O_WRONLY+O_CREAT, 0777, 0 ); strncpy( message, "Hello", 6 );

 

printf( "Send message %s\n", message ); mq_send( queue, message, strlen(message)+1, 0 ); mq_close( queue );

 

printf( "Child process completed\n" );

 

}

 

else

 

{

 

mqd_t * queue; char message[2000];

queue = mq_open( "/messages", O_RDONLY+O_CREAT, 0777, 0 );

 

mq_receive( queue, message, 2000, 0 );

 

printf( "Receive message %s\n", message );

 

mq_close( queue );

 

mq_unlink( "/messages" );

 

printf( "Parent process completed\n" );

 

}

 

}

Both the child and the parent process open the message queue with the O_CREAT flag, meaning that the queue will be created if it does not already exist. The parent process then waits for a message from the child. The child sends a message and then closes its connection to the message queue. The parent receives the message and then closes the connection to the message queue and deletes the queue.

 

Pipes and Named Pipes

 

A pipe is a connection between two processes. It can be either an anonymous pipe between two processes or a named pipe, which uses an entity in the file system for communication between processes or threads. The pipe is a streamed first-in, first-out structure.

 

A named pipe is created by a call to pipe() typically before the child process forks. The pipe call creates two file descriptors, one for reading from the pipe and a second for writing into the pipe. After the fork, both the parent and child inherit both file descrip-tors. Typically one pipe would be used for unidirectional communication between the parent and the child.

 

Reading and writing to pipes can use the functions that take file descriptors as parameters, such as read and write.

 

Listing 5.61 shows an example of using an anonymous pipe to communicate between a child and parent process.

Listing 5.61 Using an Anonymous Pipe to Communicate Between a Parent and Child Process

#include <unistd.h> #include <stdio.h>

 

int main()

 

{

 

int status; int pipes[2];

pipe( pipes );

 

pid_t f = fork(); if ( f == 0 )

 

{ /* Child process */ close( pipes[0] );

 

write( pipes[1], "a", 1 ); printf( "Child sent 'a'\n" ); close( pipes[1] );

}

 

else

 

{

 

char buffer[11];

 

close( pipes[1] );

 

int len = read( pipes[0], buffer, 10 );

 

buffer[len] = 0;

 

printf( "Parent received %s\n", buffer );

 

close (pipes[0] );

 

}

 

return 0;

 

}

The code creates file descriptors for the two pipes before forking. The parent closes the descriptor indicted by pipes[1] and then waits to receive data from pipes[0]. The child process closes the descriptor pipes[0] and then sends a character to pipes[1] to be read by the parent process. The child process then closes their copy of the write file descriptor. The parent process prints the character sent by the child before closing the pipe and exiting.

 

Named pipes are created using a call to mknod(), which takes the path to the file that is to be used as the identifier for the pipe; the mode, which is S_IFIFO for a named pipe; and the access permissions for the file. The two processes can then call open() to open the file and treat the returned handles in the same way as before. Once the processes have finished with the named pipe, it can be removed by calling unlink(). The code in Listing 5.62 implements the same parent and child communication as Listing 5.61, this time using named pipes instead of anonymous pipes.

Listing 5.62  Parent and Child Process Communicating Using Named Pipes

#include <unistd.h> #include <stdio.h> #include <sys/stat.h> #include <fcntl.h>

int main()

 

{

 

int status;

 

mknod( "/tmp/pipefile", S_IFIFO|S_IRUSR|S_IWUSR, 0 );

 

pid_t f = fork(); if ( f == 0 )

{ /* Child process */

 

int mypipe = open( "/tmp/pipefile", O_WRONLY ); write( mypipe, "a", 1 );

 

printf( "Child sent 'a'\n" ); close( mypipe );

 

}

 

else

 

{

 

int mypipe = open( "/tmp/pipefile", O_RDONLY );

 

char buffer[11];

 

int len = read( mypipe, buffer, 10 );

 

buffer[len] = 0;

 

printf( "Parent received %s\n", buffer );

 

close( mypipe );

 

}

 

unlink( "/tmp/pipefile" );

 

return 0;

}

The parent process calls mknod() to create the pipe and then forks. Both the child and parent processes open the pipe—the child for writing and the parent for reading. The child writes into the pipe, closes the file descriptor, unlinks the pipe, and then exits. The parent process reads from the pipe before it too closes it, unlinks it, and exits.

 

Using Signals to Communicate with a Process

 

Signals are used extensively in UNIX and UNIX-like operating systems. Pressing ^C on a terminal keyboard to stop an application actually results in the SIGKILL signal being sent to that process. It is relatively straightforward to set up a signal handler for the vari-ous signals that might be sent to a process. It is not possible to install a handler for SIGKILL, but many of the other signals can be intercepted and handled.

 

A signal handler is installed by calling signal() with the signal number and the rou-tine that should handle the signal. A signal can be sent by calling kill() with the PID of the process that the signal should go to and the signal number. Listing 5.63 shows an example of this.

 

Listing 5.63   Sending and Receiving a Signal

#include <signal.h> #include <stdio.h> #include <unistd.h>

 

void hsignal( int signal )

 

{

 

}

 

int main()

 

{

 

signal( SIGRTMIN+4, hsignal );

 

kill( getpid(), SIGRTMIN+4 );

 

return 0;

 

}

The example installs a handler for the signal SIGRTMIN+4. The values SIGRTMIN and SIGRTMAX are system-specific values that represent the range of values that are reserved for user-defined signals.

 

It is tempting to imagine that the code in Listing 5.64 would be appropriate for a sig-nal handler. The problem with this code is that the function printf() is not guaranteed to be signal-safe. That is, if the application happened to be performing a printf() call when the signal arrived, it would not be safe to call the printf() in the signal handler.

 

Listing 5.64   Unsafe Signal Handler Code

void hsigseg( int signal )

 

{

 

printf( "Got signal\n" );

 

}

The POSIX guarantees that a set of function calls are async-signal-safe, in particular that the write() call can be used in a signal handler, so the code in Listing 5.65 can be used.

 

Listing 5.65   Printing Output in a Signal Handler

#include <stdio.h> #include <unistd.h>

void hsignal( int signal )

{

 

write( 1, "Got signal\n", 11 );

 

}

Running this on an Ubuntu system produces the expected output shown in Listing 5.66.

 

Listing 5.66   Output Printed by Signal Handler

$ gcc signal.c

 

$ ./a.out

Got signal

Sometimes, the code already performs an operation on receiving a particular signal, but it is desired to add an additional handler. What should be done is to add a signal handler to the chain and then call the default one.

 

The function that allows us to create a chain of signal handlers is sigaction(). This takes a signal number and two sigaction structures. The first sigaction structure contains information about the new signal handler, while the second returns information about the existing signal handler.

 

The code in Listing 5.67 installs a new handler for the SIGPROF signal but stores the details of the old handler. Then, when a signal is received, the new handler does its pro-cessing and then installs the old handler to perform the default action. The handler structure has an sa_sigaction field, which indicates the routine to be called in the event of a signal arriving. It also has an sa_mask field, which sets the list of signals that are to be blocked while this signal is handled. The other field of interest is sa_flags, which allows tuning of the behavior of the signal handler.

 

Listing 5.67   Chaining Signal Handlers

#include <signal.h> #include <unistd.h>

 

struct sigaction oldhandler;

 

void hsignal( int signal, siginfo_t* info, void* other )

 

{

 

write( 1, "Got signal\n", 11 ); if (oldhandler.sa_sigaction)

{

 

oldhandler.sa_sigaction( signal, info, other );

 

}

}

int main()

{

 

struct sigaction newhandler; newhandler.sa_sigaction = hsignal; newhandler.sa_flags = 0; sigemptyset( &newhandler.sa_mask );

sigaction( SIGPROF, &newhandler, &oldhandler ); kill( getpid(), SIGPROF );

 

}

Signals can also be used for communicating between processes. Listing 5.68 demon-strates a parent process sending a signal to a child process.

 

Listing 5.68   Parent Process Signaling to a Child Process

#include <unistd.h> #include <stdio.h> #include <signal.h> #include <sys/wait.h>

 

volatile int go = 0;

 

void handler( int sig )

 

{

 

go = 1;

 

write( 1, "Signal arrived\n", 16 );

 

}

 

int main()

 

{

 

signal( SIGRTMIN+4, handler );

 

pid_t f = fork(); if ( f == 0 )

 

{ /* Child process */ while ( !go ){}

printf( "Child completed\n" );

 

}

 

else

 

{

 

kill( f, SIGRTMIN+4 );

 

waitpid( f, 0, 0 );

 

printf( "Parent completed\n" );

 

}

 

}

Compiling and running the code produces the output shown in Listing 5.69.

 

Listing 5.69  Output from Parent Process Communicating with Child Process

$ gcc sigchild.c

$ ./a.out

Signal arrived

Child completed

Parent completed

 

It might be sufficient to signal another process, but it is more useful to be able to pass data between the processes. It is possible to do this using sigaction(), as the code in Listing 5.70 demonstrates. The code also uses sigqueue() to send the signal containing the data; on Solaris, this is found in the real-time extensions library, and the application requires linking with -lrt.

 

Listing 5.70   Using Signals to Transfer Information

#include <unistd.h> #include <stdio.h> #include <signal.h> #include <sys/wait.h>

 

volatile int go = 0;

 

struct sigaction oldhandler;

 

void handler( int sig, siginfo_t *info, void *context )

 

{

 

go = (int)info->si_value.sival_int;

 

write( 1, "Signal arrived\n", 16 );

 

}

 

int main()

 

{

 

struct sigaction newhandler;

newhandler.sa_sigaction = handler;

newhandler.sa_flags = SA_SIGINFO;

 

sigemptyset( &newhandler.sa_mask );

 

sigaction( SIGRTMIN+4, &newhandler, &oldhandler );

 

pid_t f = fork(); if ( f == 0 )

 

{ /* Child process */ while ( !go ){}

printf( "Child completed go=%i\n", go );

 

}

 

else

 

{

union sigval value;

 

value.sival_int = 7;

 

sigqueue( f, SIGRTMIN+4, value );

 

waitpid( f, 0, 0 );

 

printf( "Parent completed\n" );

 

}

 

}

 

The signal handler is set up with sa_flags including SA_SIGINFO. This flag causes the signal handler to receive the siginfo_t data. If the flag is not specified, the signal handler will not receive the data. The signal is sent by the parent process by calling sigqueue(). This takes a sigval union as a parameter, and the code sets the integer field of this union to the value seven. When the child process receives the signal, it can extract the value of this field from the sigval union passed into it.

 

Compiling and running the code produces the output shown in Listing 5.71.

 

Listing 5.71   Parent Process Sending Information to the Child Process

$ gcc sigchild2.c

 

$ ./a.out

 

Signal arrived

 

Child completed go=7

 

Parent completed

The parent process creates a child process and then sends a SIGRTMIN+4 signal to the child. The child process loops until the variable go becomes nonzero. When the child process receives the signal, it sets the variable go to be nonzero, and this enables the process to print a message and exit. In the meantime, the parent process has been waiting for the child process to exit. When the child process does eventually exit, the parent process prints a message and also exits.

 

One significant problem with using signals is that they can disrupt a system call that the thread is making at the time the signal is received. If this happens, the system call will set errno to the value EINTR indicating that the call should be retried. However, the exact behavior is system dependent and should be carefully explored before relying on signals.


Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail
Multicore Application Programming For Windows, Linux, and Oracle Solaris : Using POSIX Threads : Multiprocess Programming |


Privacy Policy, Terms and Conditions, DMCA Policy and Compliant

Copyright © 2018-2024 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.