0% found this document useful (0 votes)
10 views12 pages

Producer-Consumer C Program Guide

The document outlines three programming assignments involving inter-process communication (IPC) in C. The first assignment focuses on a producer-consumer problem using shared memory, the second involves creating a command-input and execute-command system using pipes, and the third simulates a game with a parent process and child processes using signals. Each assignment requires the implementation of specific C programs and adherence to detailed instructions regarding process management and communication.

Uploaded by

vobigo8622
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
10 views12 pages

Producer-Consumer C Program Guide

The document outlines three programming assignments involving inter-process communication (IPC) in C. The first assignment focuses on a producer-consumer problem using shared memory, the second involves creating a command-input and execute-command system using pipes, and the third simulates a game with a parent process and child processes using signals. Each assignment requires the implementation of specific C programs and adherence to detailed instructions regarding process management and communication.

Uploaded by

vobigo8622
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Q1] Shared Memory :

One producer and n consumers share a memory M[] capable of storing two int variables.
The producer generates items (random integers) in M[1] for a predetermined number t of times. For
each item generated, the producer specifies in M[0] the consumer (an integer in the range 1, 2, 3, . .
. , n) for which the item written in M[1] is meant. The designated consumer reads M[1], and sets
M[0] to 0, indicating that the item is consumed. After all of the t items are generated and consumed,
the producer writes –1 to M[0]. After reading this special value of M[0], each consumer prints some
aggregate information, and terminates. Finally, the producer terminates too after printing some
aggregate information.

In order to implement this set of actions, write a C program prodcons.c. The parent process (call it
P) plays the role of the producer. P reads n (the number of consumers) and t (the number of items to
be produced) from the user or as command-line arguments. Then, P creates a shared-memory
segment M capable of storing two int variables. P also initializes M[0] to 0 (implying that no item is
available for consumption at the beginning). P then forks n child processes C1, C2, . . . , Cn which play
the roles of the n consumers. These child processes (or consumers) are numbered 1, 2, . . . , n. After
this, P goes to a production loop, and each Ci goes to a consumption loop. The loops run until all of
the t items are produced and consumed. These loops work as follows.

Production Loop :

For each i = 1, 2, . . . , t, the producer P (parent in our case) generates a random 3-digit int value item
and a random consumer c in the range 1, 2, . . . , n. P waits (busy wait) until M[0] becomes 0. When
M[0] becomes 0, P sets M[0] to c and M[1] to item (in that order). An optional delay (you can use
usleep()) between setting M[0] and setting M[1] should be used if a compile-time macro SLEEP is set.

After producing t items, P waits (busy wait) until M[0] becomes 0 (that is, the last item is consumed
by the designated consumer child). P then writes –1 to M[0], and waits until all of the n child
processes terminate. P then prints, for each consumer c, the count of items produced for c, and the
sum of these items.

P finally removes the shared-memory segment M, and exits.

Consumption Loop :

The c-th consumer waits until M[0] becomes c or –1. If M[0] becomes c, the consumer reads M[1] as
the next item meant for it. When M[0] becomes –1, the consumption loop is broken. The number of
items read by the consumer and the sum of these items are then printed, and the child process
terminates.
Compile Time Tags :

The default behavior of your program should be to print only an initial message and the final
statistics. If the compile-time flag VERBOSE is set, then the production and the consumption of each
item should also be printed (see the Sample Output). Another compile-time flag SLEEP (already
mentioned above) dictates whether there is no delay between the setting of M[0] and the setting of
M[1] by the producer (this should be the default behavior if the flag is not set) or there is a small
delay (of 1–10 microseconds) between these two assignments. This delay simulates preemption of P
(which would otherwise be very difficult to reproduce), and highlights the necessity of
synchronization for this producer-consumer problem

Submit a single C source file prodcons.c.


Q2 . Pipes

In this assignment, you write a single C program CSE.c. Compile the program to an executable file
called CSE. The program deals with three initial processes called C, S, and E. They work as follows.

Supervisor (S):

This is the parent process. This process creates the necessary pipe(s), and then forks two child
processes henceforth referred to as the “First Child” and the “Second Child”, respectively. Each of
these child processes opens an xterm to run ./CSE itself for interacting with the user.

Command-input child (C):

This child keeps on reading lines of commands from the user in its own xterm, and sending the
commands verbatim to the other child.

Execute-command child (E):

This child keeps on reading the commands sent by the other child, and executing them in the
foreground (by forking child processes) in its own xterm. The parent S initiates First Child in the C
mode and the Second Child in the E mode. It then waits until both the child processes terminate.

Each command supplied by the user can be one of the following:

1. A standard Linux command (like ls, ps, who, cat) with any number of command-line parameters
but without pipes (like |) or redirections (like >, >>, and <).

2. The special command exit that terminates both the child processes.

3. Another special command swaprole that swaps the roles (C and E) of the two child processes. That
is, when the user types this command (without any arguments) to the C child, the C child sends this
command verbatim to the E child. After this, the C child switches to the E (execute-command) mode,
and the E child switches to the C (command-input) mode. This command may be supplied by the
user any number of times in the appropriate C windows.

All of the processes C, S, and E run the same executable file ./CSE. If ./CSE is run without any
commandline arguments, it is the parent process (S). After the parent forks the First Child in C mode
or the Second Child in E mode, each child execs xterm which in turn runs ./CSE with appropriate
command-line arguments, so that each child knows its role (mode) along with other relevant
information (like pipe fd’s).

Follow the instructions given below in order to implement CSE.c.

• An xterm can be opened with a customized title to run an executable file (in our case, ./CSE) as

xterm -T "Customized Title" -e ./CSE arg1 arg2 arg3 . . .

• Child C reads user commands from its stdin. It sends the command to Child E via a pipe. Child E
reads each command from the pipe, and forks a (grand)child for executing the command (and itself
waits until the grandchild terminates). The grandchild prints the output of the command to the
stdout of E. This pipe must be created by the parent S, and the corresponding file descriptors must
be sent to the child processes as command-line arguments. Two child processes cannot themselves
establish an unnamed pipe between them. Do not involve the parent S in any child-to-child
communication.

• The stdout of C should be dup-ed as the write-end of the pipe. Likewise, the stdin of E should be
dup-ed as the read-end of the pipe. This allows the two child processes to use high-level I/O
primitives like scanf, printf, fgets, fputs, . . . This however prevents the Child C from printing a
prompt like Enter Command> to the terminal. Use the un-dup-ed stderr for that purpose. This is not
the intended use of stderr, but nobody will mind.

• A swaprole command will necessitate the restoring of the original stdin or stdout. This can be
achieved by maintaining copies of the original stdin and of the original stdout (can be done by dup
once at the beginning).

• Use fflush() whenever it is necessary to flush an output buffer (like stdout) immediately.

There are two more problems to solve. The solutions are outlined below.

1. Suppose that the user supplies the command swaprole in the C window. Immediately after this
input, this situation may happen: C writes the command to the pipe, switches role to E, and reads
from the pipe, before the earlier E gets a chance to read from the pipe. This should not happen. Do
not use sleep to delay the earlier C (and give the earlier E time to read from the pipe). Use another
pipe instead (to be created by S before forking).

2. Suppose that the user supplies an interactive command. When E lets this command get executed
by a (grand)child process, the stdin of E and of the grandchild is the pipe’s read end. That is, the
inputs for the interactive command should come from the other window C. This can be unimaginably
bad (for example, you will go mad if you open a text editor in one window and type in another
window). To solve this problem, note that the (interactive) command is not executed by E itself, so E
can afford to continue with the dup-ed definition of stdin. The grandchild which actually does the
running of the command should restore the original stdin for taking user inputs in the E window
itself. This strategy will also relieve E and the grandchild from any contention over user inputs. That’s
all indeed. Submit the single C source file CSE.c
Q3 . IPC using Signals

Think of n children C1, C2, C3, . . . , Cn standing in a circle, and playing a game with the parent P
standing at the center of the circle. P throws a ball to the children in a circular sequence. If the child
(say, Ci) to which the ball is thrown can catch the ball, then Ci continues to play. If Ci misses the ball,
then Ci goes out of the game. After each throw, the ball comes back to P who then throws the ball to
the next (in the circular order) child who is not yet out of the game. Eventually, n – 1 children miss
and go out of the game. The remaining child wins the game.

You need to implement this game as a multi-process application, where the processes can
communicate with one another by sending signals. You write two programs parent.c and child.c to
simulate the working of the parent and each child process. Suppose that these two programs are
compiled to the executable files parent and child. The program parent (which simulates P) is run
with one command-line argument n (the number of child processes). P creates n child processes Ci
which exec child i for i = 1, 2, 3, . . . , n. P also writes, in a text file [Link], the child count n and
the PIDs of the n child processes created by P. Each child waits for some time (like one second) for P
to finish writing to [Link]. After this wait, each child reads n and the n PIDs from the text file
[Link]. After the child creation, P waits for some time (like two seconds) so that each child
process gets time to read this text file. These waits may be implemented by sleep() or usleep(), but
after this, no waiting based on these functions will be allowed.

The parent P starts the game by sending SIGUSR2 to C1. P then enters a loop which continues until
only one child is left as the player. Each child Ci, on the other hand, enters an infinite loop. The body
of each loop should contain the single system call pause() which lets the calling process wait until it
receives a signal (avoid busy waits). The game of throwing balls and catching/missing throws will be
implemented by sending and handling signals. In this assignment, we use the three signals SIGUSR1,
SIGUSR2, and SIGINT only.

For a child process Ci, receiving SIGUSR2 implies that a throw is made to it. It then randomly decides
whether it catches the ball (with probability 0.8) or misses the ball (with probability 0.2). If Ci can
catch the ball, it sends SIGUSR1 to P. If Ci fails to catch the ball, it sends SIGUSR2 to P. Depending on
the type of the signal received from the child Ci (to which the throw is made), the parent knows
whether that child continues to play the game or is out of the game. P records this information.

After the outcome of a throw is recorded as explained above, P initiates a printing of the current
status of all the n players. Since P has all the necessary information, it can do that printing itself.
However, as a part of this assignment, this printing should be done by the child processes. This is
achieved by sending SIGUSR1 to the child processes in turn. Recall that to a child process, SIGUSR2
means that a throw is made to it. On the other hand, the reception of SIGUSR1 initiates that child to
print its current status. The possible status of a child are PLAYING (written as ....), CATCHMADE
(written as CATCH), CATCHMISSED (written as MISS), and OUTOFGAME (written as blank). See the
sample at the end to know the format of printing. P initiates the printing process by sending
SIGUSR1 to C1. For each i < n, Ci prints its status, and then sends SIGUSR1 to the next child Ci + 1.
The last child Cn prints its status, but does not send SIGUSR1 to any other process. However, Cn
takes part in the synchronization activity in a different manner. Until all the child processes finish
writing their status, the parent P must wait before it can make the throw to the next playing child.
But you do not know many synchronization primitives at this moment, so let this wait be
accomplished by waitpid(). Before sending SIGUSR1 to C1, P forks a dummy child process D and
writes the PID of D in a text file [Link]. P then waits until D exits. When Cn is done printing
its status, it reads the PID of D from the file [Link], and sends SIGINT to D. Write dummy.c
(the code for D) that enters an infinite loop of pause() at the beginning of its main().

When D exits, P wakes up, and works out the next playing child process Cnext to which the throw is
to be made. Note that P maintains the information of the status of all child processes. P should also
keep track of the child process to which the last throw is made. So P can determine Cnext easily. P
then sends SIGUSR2 to Cnext , and the game continues as explained above.

After n – 1 child processes miss throws, the parent sends SIGINT to all the n child processes. Only the
last playing child process prints a happy message, and exits (see the format in the sample). The other
processes exit without printing anything.

Notice that the sequencing of the throw-and-print cycle must be implemented only by signals (and
by the waitpid() in one case). No other synchronization mechanism is allowed. You may use
fflush(stdout); to avoid garbled output. But do not use any sleep or usleep calls (except only at the
very beginning, that is, before the game starts).

You may use the following makefile.

Submit a zip/tar/tgz archive containing the files parent.c, child.c, dummy.c, and makefile.

Common questions

Powered by AI

In the CSE program, the swaprole command allows the two child processes to exchange their roles: one as command-input (C) and the other as execute-command (E). This is achieved by Child C sending the swaprole command to Child E, and both children swapping their operational modes. Proper swapping is ensured by maintaining separate pipes for communication and ensuring each child appropriately dup's stdin and stdout for the role switch. The parent S ensures pipes are shared correctly to prevent errors during the transition, and synchronization problems are managed by creating additional pipes if necessary. The careful management of pipe file descriptors helps prevent any race conditions or incorrect input/output handling during this swap .

Challenges arise in the CSE program when executing interactive commands because the stdin of the executing process, E, is connected to the pipe’s read end. This setup can lead to unintended input routes if interactive programs require user input directly from the terminal. To address this, the grandchild process executing an interactive command restores the original stdin, allowing it to accept input from its associated terminal rather than the pipe. By duplicating the original stdin and restoring it during execution, the program avoids input misdirection, alleviating interaction issues .

In the CSE program, command executions and role swaps are handled by ensuring that two child processes communicate directly using unnamed pipes created by the parent S. The parent does not supervise this communication directly. Instead, stdin and stdout are dup’ed as the ends of the pipe for the two children, allowing them to handle command input and execution independently. To handle a role swap without error, synchronization is ensured by maintaining copies of the original stdin and stdout, which are used when the roles are swapped through the swaprole command. The use of fflush() ensures immediate buffer flushing, preventing any delays in interprocess communication .

In the CSE program, role switching is managed by leveraging dup'ed pipes for stdin and stdout, allowing direct communication between the two child processes. When a swaprole command is issued, the roles of command-input (C) and execute-command (E) are swapped by altering the pipes’ connections, restoring their original states, which have been saved using dup, allowing smooth role transition. This setup ensures commands are relayed correctly, and processes execute the intended roles without error. Managing pipes effectively with proper cleanup and restoration of file descriptors is central to ensuring seamless role transition and execution of commands .

Signals in the ball game simulation are utilized to facilitate the coordination of actions among processes. The parent P uses signals like SIGUSR2 to indicate a ball throw to a child, and SIGUSR1 for reporting a child's status. Each child, upon receiving SIGUSR2, randomly decides if it catches the ball or not, sending SIGUSR1 or SIGUSR2 to the parent correspondingly. This allows the parent to monitor who remains in the game. The advantage of using signals is that they provide an asynchronous communication mechanism that is efficient and suitable for lightweight process coordination without the overhead of other IPC methods like message-passing queues or shared memory locks .

The dummy child process in the ball game simulation plays a critical role for synchronization. Before initiating a status print cycle, the parent forks a dummy process D and uses its termination as a synchronization point. The last child in a cycle, after printing its status, sends SIGINT to D, triggering its termination. The parent, which waits for D's exit using waitpid(), uses the exit as a signal that the print cycle is complete before proceeding to the next game round. This method leverages a dummy process to establish a synchronization barrier between print cycles without using complex synchronization primitives .

The compile-time flag VERBOSE, when set, modifies the program to print detailed logging of each production and consumption activity, helping developers verify that items are correctly managed. Meanwhile, SLEEP introduces a delay (1-10 microseconds) between setting M[0] and M[1] by the producer, simulating preemption to demonstrate the necessity of synchronization mechanisms. Both compile-time flags are essential for debugging and understanding the timing and synchronization details within the implementation. They provide flexibility in testing different scenarios that can affect consumer-producer interactions and aid in identifying synchronization issues .

The shared memory model ensures that items are not overwritten by using M[0] as a control flag and M[1] as the storage for the actual data. The control structure dictates that the producer first waits until M[0] is 0, indicating that the previous item has been consumed before writing a new item to M[1]. M[0] is then set to the intended consumer's ID. This design guarantees that data integrity is preserved by only allowing new data to be written once the old data has been successfully processed, effectively coordinating the producer's and consumer's access states .

The producer-consumer model described addresses synchronization challenges primarily through the use of shared memory and careful control of access to this memory. The producer waits until the consumer has signaled that it has consumed the current item by setting M[0] to 0 before it writes a new item. This is a busy-waiting mechanism to ensure that the producer does not overwrite existing data before it is consumed. The optional delay controlled by the SLEEP macro simulates preemption, demonstrating the need for synchronization. The use of compile-time flags like VERBOSE and SLEEP helps demonstrate and test these synchronization behaviors .

Busy waiting in the producer-consumer model is used by the producer to continuously check the value of M[0] until it reads 0, indicating the buffer is clear for a new item. While this ensures immediate action once a consumer completes its task, it holds the CPU in a loop, potentially leading to wasted processing resources and energy, particularly in systems with many concurrent tasks. This can degrade the overall system performance due to excessive CPU usage for looping instead of yielding to other processes, highlighting a trade-off between simplicity of implementation and system efficiency .

You might also like