Lab 1II Report
Report Subject: OS Experiment - Lab 3
Student ID: 20138380130
Student Name: Khan Md Shahedul Islam
Data:2021/11/6
Computer Operating System Experiment
Laboratory 3
Inter-Process Communication
Objective:
In computer science, Inter-process communication (IPC) is a
mechanism which allows processes to communicate with each other and
synchronize their actions. IPC provides a mechanism to allow the
processes to manage shared data. The objective of this lab is to help you
internalize a couple of important facts about IPC in Linux.
Including:
Shared memory
Pipes
Equipment:
VirtualBox with Ubuntu Linux
Methodology:
Program and answer all the questions in this lab sheet.
1 Inter-Process communication
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. An independent process is not affected by the
execution of other processes while a co-operating process can be affected
by other executing processes. Though one can think that those processes,
which are running independently, will execute very efficiently, in reality,
there are many situations when co-operative nature can be utilized for
increasing computational speed, convenience and modularity. Inter
process communication (IPC) is a mechanism which allows processes to
communicate with each other and synchronize their actions.
Cooperating processes require an inter-process communication (IPC)
mechanism that will allow them to exchange data and information. There
are two fundamental models of inter-process communication: shared
memory and message passing. In the shared-memory model, a region of
memory that is shared by cooperating processes is established. Processes
can then exchange information by reading and writing data to the shared
region. In the message-passing model, communication takes place by
means of messages exchanged between the cooperating processes such as
pipes, message queue, and sockets.
2 Experiments
2.1 Experiment 1: shared memory
communication
Inter-process communication using shared memory requires
communicating processes to establish a region of shared memory.
Typically, a shared-memory region resides in the address space of the
process creating the shared-memory segment. Other processes that wish
to communicate using this shared-memory segment must attach it to their
address space.
These are steps for shared memory communication:
• Create or open a shared memory object with shm_open(). A file
descriptor will be returned if shm_open() creates a shared memory
object successfully.
• Set the shared memory object size with ftruncate().
• Map the shared memory object into the current address space with
mmap() and MAP_SHARED.
• Read/write the shared memory.
• Unmap the shared memory with munmap().
• Close the shared memory object with close().
• Delete the shared memory object with shm_unlink().
• Control operations on the shared memory segment (shmctl())
POSIX provides five entry points to create, map, synchronize, and
undo shared memory segments:
shm_open(): Creates a shared memory region or attaches to an existing, named region. This
system call returns a file descriptor.
shm_unlink(): Deletes a shared memory region given a file descriptor (returned
from shm_open()). The region is not actually removed until all processes accessing the
region exit, much like any file in UNIX. However, once shm_unlink() is called (typically
by the originating process), no other processes can access the region.
mmap(): Maps a shared memory region into the process’s memory. This system call requires
the file descriptor from shm_open() and returns a pointer to memory. (In some cases, you
can also map a file descriptor to a plain file or another device into memory. A discussion of those
options is beyond the scope of this introduction; consult the mmap() documentation for your
operating system for specifics.)
munmap(): The inverse of mmap().
msync(): Used to synchronize a shared memory segment with the file system — a technique
useful when mapping a file into memory.
A program shown in text book (page 133) use the producer-
consumer model in implementing shared memory. The producer
establishes a shared memory object and writes to shared memory, and the
consumer reads from shared memory.
Requirements:
You need to write codes that implements a parent and child process
that communicates via a shared memory object.
1) Parent process creates a shared memory
2) Parent process spawn child process
3) Child process runs and write a “greeting to parent!” message to
shared memory
4) Parent process waits child process to terminate, if the exit code == 0,
then read the message from shared memory.
Compile the producer and consumer by:
$gcc -o shm-posix-producer shm-posix-producer.c –lrt
$gcc -o shm-posix-consumer shm-posix-consumer.c –lrt
The producer process calls shm_open to create and open the shared
memory object in O_CREAT|O_RDWR mode. Return its descriptor
shm_fd. After adjusting the size of the shared memory object shm_fd to
the specified size, call mmap to map it to the shared memory area pointed
to by ptr, and the mapping flag Consistent with the flag when the object is
opened, and then use ptr to perform operations on shared memory
objects.
The producer's operation on the shared memory object is to write the
string name and message0 together into the common memory object. The
consumer program can directly access the shared memory object and
output the content written by the producer after calling shm_open and
mmap.
Running the programs:
./shm-posix-producer
./shm-posix-consumer
The producer and consumer processes in the program run sequentially.
From the program structure, the producer first writes data to the shared
memory area, and then the consumer utputs the data in the shared
memory area. If the producer keeps inputting data into the shared
memory while the producer is running, the consumer can automatically
display the content after each input.
Questions:
1. On Linux, all shared memory objects can be found in /dev/shm. Can
you find the shared memory objects that you created?
Solution:
Shared Memory Objects and Linux Kernels
On Linux, all shared memory objects can be found in /dev/shm. You
may list them with ls -l /dev/shm. You may also remove a shared
memory object with rm /dev/shm/[name]. This is handy when you are
debugging your program.
2.2 Experiment 2: ordinary pipe communication
Ordinary pipes allow two processes to communicate in standard
producer– consumer fashion: the producer writes to one end of the pipe
(the write-end) and the consumer reads from the other end (the read-end).
As a result, ordinary pipes are unidirectional, allowing only one-way
communication. If two-way communication is required, two pipes must
be used, with each pipe sending data in a different direction. We next
illustrate constructing ordinary pipes on Linux systems. In the example,
one process writes the message Greetings to the pipe, while the other
process reads this message from the pipe. An ordinary pipe cannot be
accessed from outside the process that created it. Typically, a parent
process creates a pipe and uses it to communicate with a child process
that it creates via fork().
On Linux systems, ordinary pipes are constructed using the function
pipe(int fd[ ])
This function creates a pipe that is accessed through the int fd[] file
descriptors: fd[0] is the read-end of the pipe, and fd[1] is the write-end.
In order to send structured data through a pipe, you have to serialize
it to push it from the sending to receiving process as a stream of bytes.
For instance, imagine you are sending a struct type: you have to turn the
struct into a sequence of bytes in order to send them over the pipe.
When you send two different instances of serialized data structures
over a serial channel such as a Linux pipe, you have to be able to
distinguish where one datum ends and the next one begins. What you use
as the “marker” or “sentinel value” that distinguishes the two instances is
up to you to define.
1) Write an ordinary pipe between parent process and child process.
Copy the pipes-test.c program from the text book (Page 143) to
another file named pipes.c. Now, modify your new program so that the
parent process read to the pipe content from child process when child
process executed commands.
The program first creates a pipe, and then creates a child process of
the current process through fork(). Then each process closes the file
descriptors that are not needed for the read and write pipes. The child
process executes the "ls -a" command under the current path, and writes
the command execution output to the pipe by copying the pipe write
descriptor fd[1] to standard output; the parent process reads the pipe data
and displays it through fd[0] .
Every process in Linux is provided with three standard file
descriptor including standard input, output and error files. By default:
Standard Input is the keyboard, abstracted as a file to make it
easier to write shell scripts.
Standard Output is the shell window or the terminal from which
the script runs, abstracted as a file to again make writing scripts &
program easier
Standard error is the same as standard output: the shell window or
terminal from which the script runs.
A file descriptor is simply a number that refers to an open file. By
default, file descriptor 0 (zero) refers to the standard input & often
abbreviated as stdin. File descriptor 1 refers to standard output (stdout)
and file descriptor 2 refers to standard error (stderr).
You can use dup(2) function to duplicate a file descriptor with the
pipe write descriptor fd[1] by using function dup2 in order to relocate the
standard output.
// dup2() makes newfd be the
copy of oldfd, closing newfd
first if necessary
int dup2(int oldfd, int newfd);
pipes.c
// Khan Md Shahedul Islam
//2018380130
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1
int main(void) {
char write_msg[BUFFER_SIZE] = "Greetings";
char read_msg[BUFFER_SIZE];
int fd[2];
pid_t pid;
char EoF = '\0';
if (pipe(fd) == -1) {
fprintf(stderr, "Pipe failed");
return 1;
}
pid = fork();
//Pipe(fd);
if (pid < 0) {
fprintf(stderr, "Fork failed");
return 1;
}
if (pid > 0) {
//parent process
close(fd[READ_END]);
//printf("%s", write_msg);
for (int i = 0; i < strlen(write_msg); i++) {
write(fd[WRITE_END], &write_msg[i], 1);
}
write(fd[WRITE_END], &EoF, 1);
close(fd[WRITE_END]);
}
else {
//child process
close(fd[WRITE_END]);
printf("read \n");
for (int j = 0; j < strlen(write_msg); j++) {
if (read(fd[READ_END], &read_msg[j], BUFFER_SIZE) >= 0){
read(fd[READ_END], &read_msg[j], BUFFER_SIZE);
printf("%c", read_msg[j]);
}
}
close(fd[READ_END]);
}
return 0;
}
2) Write a two-way ordinary pipe
Copy your pipes-test.c program to another file named upper.c.
Modify your program so that it defines two pipes: one for communication
from parent to child and another for communication from child to parent.
Make sure to close the correct ends of each pipe. Ultimately, your goal is
to have two pipes, one in each direction, so that you have bi-directional
communication between the two processes. It helps a lot to use names for
the pipes file descriptors that indicate their direction. For instance: p-to-c
and c-to-p tell us the processes that the pipes interconnect and the
direction of the flow of information.
This new version of the program must behave as follows.
The parent sends a message to the child (byte-by-byte or line) and
when it is done, it enters a loop to read characters from the pipe
coming from the child (also byte-by-byte), terminating the loop on
the receipt of EOF.
The child receives the message (also byte-by-byte or line),
printing each character to standard out as it arrives. For each
character received, the child converts it to uppercase (using
toupper(3) function) and sends it back to the parent using your
second pipe. As the parent reads the characters received from the
child, it prints them to standard out.
Make sure to reason carefully about when the write ends of the two
pipes should be closed, so that your processes can terminate their loops
gracefully and reach their termination state when appropriate. Also, make
sure to use the wrappers defined previously (Fork, Pipe, Read, and
Write).
upper.c
//Khan Md Shahedul Islam
//2018380130
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <ctype.h>
#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1
int main(void) {
char write_msg[BUFFER_SIZE] = "Greetings";
char read_msg[BUFFER_SIZE];
int p2c[2];
int c2p[2];
pid_t pid;
char EoF = '\0';
int status;
if (pipe(p2c) == -1) {
fprintf(stderr, "Pipe failed");
return 1;
}
if (pipe(c2p) == -1) {
fprintf(stderr, "Pipe failed");
return 1;
}
pid = fork();
//Pipe(fd);
if (pid < 0) {
fprintf(stderr, "Fork failed");
return 1;
}
if (pid > 0) {
//parent process
close(p2c[READ_END]);
close(c2p[WRITE_END]);
//printf("%s", write_msg);
for (int i = 0; i < strlen(write_msg); i++) {
write(p2c[WRITE_END], &write_msg[i], BUFFER_SIZE);
}
write(p2c[WRITE_END], &EoF, BUFFER_SIZE);
close(p2c[WRITE_END]);
wait(&status);
//printf("we made it here\n");
for (int c = 0; c < strlen(write_msg); c++) {
//printf("c");
read(c2p[READ_END], &read_msg[c], BUFFER_SIZE);
printf("%c", read_msg[c]);
}
printf("\n");
close(c2p[READ_END]);
}
else {
//child process
close(p2c[WRITE_END]);
close(c2p[READ_END]);
const char *filler;
printf("read\n");
for (int j = 0; j < strlen(write_msg); j++) {
//printf("a");
read(p2c[READ_END], &read_msg[j], BUFFER_SIZE);
filler = toupper(read_msg[j]);
printf("%c", read_msg[j]);
write(c2p[WRITE_END], &filler, BUFFER_SIZE);
}
close(p2c[READ_END]);
printf("\n");
//write(c2p[WRITE_END], &EoF, 1);
close(c2p[WRITE_END]);
}
return 0;
}
Questions:
1 Does the pipe allow bidirectional communication, or is
communication unidirectional in an ordinary pipe?
Solution:
Ordinary pipes are unidirectional, allowing only one-way
communication.
Ordinary pipes allow two processes to communicate in
standard producer– consumer fashion: the producer writes to one end
of the pipe (the write-end) and the consumer reads from the other end
(the read-end).
If bidirectional communications are needed, then a second pipe is
required.
2 If two-way communication is allowed, is it half duplex (data can
travel only one way at a time) or full duplex (data can travel in both
directions at the same time)?
Solution:
If two-way communication is required, two pipes must be used, with
each pipe sending data in a different direction. Pipes are half duplex
Half-duplex and Full-duplex Transmission
Although FIFOs allow bidirectional communication, only half-duplex
transmission is permitted.
If data must travel in both directions, two FIFOs are typically used.
3 Must a relationship (such as parent–child) exist between the
communicating processes in an ordinary pipe?
Solution:
yes
2.3 Experiment 3: Named pipe communication
We used one pipe for one-way communication and two pipes for bi-
directional communication. We can use single named pipe that can be
used for two-way communication (communication between the server
and the client, plus the client and the server at the same time) as Named
Pipe supports bi-directional communication. Named pipes provide a
much more powerful communication tool. Communication can be
bidirectional, and no parent–child relationship is required.
A named pipe, however, can last as long as the system is up, beyond
the life of the process. It can be deleted if no longer used. Usually a
named pipe appears as a file and generally processes attach to it for inter-
process communication. A FIFO file is a special kind of file on the local
storage which allows two or more processes to communicate with each
other by reading/writing to/from this file.
A FIFO special file is entered into the filesystem by calling mkfifo()
in C. Once we have created a FIFO special file in this way, any process
can open it for reading or writing, in the same way as an ordinary file.
However, it has to be open at both ends simultaneously before you can
proceed to do any input or output operations on it.
The following steps outline how to use a named pipe from Linux
programs:
Create a named pipe using the mkfifo() function. Only one of the
processes that use the named pipe needs to do this.
Access the named pipe using the appropriate I/O method such as
open().
Communicate through the pipe with another process using file I/O
functions:
Write data to the named pipe.
Read data from the named pipe.
Close the named pipe.
Creating a FIFO file: In order to create a FIFO file, a function calls i.e.
mkfifo is used.
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char
*pathname, mode_t mode);
mkfifo() makes a FIFO
special file with name
pathname. mode specifies the
FIFO's permissions.
Pathname specifies the name
of the new FIFO.
mode specifies which access
modes the FIFO has when it is
created. Refer to the
documentation for stat() for a
discussion of access mode
flags. You can use 0666 (read
and write allowed for
everyone)
Requriments:
You need to write two programmers which follow rules below.
Step 1: Create two processes, one is client and another one is server.
Step 2: Server process performs the following:
Creates a named pipe (using library function mkfifo()) with
name “fifo” in /tmp directory, if not created.
Opens the named pipe for read and write purposes.
Here, created FIFO with permissions of read and write for
Owner.
Waits infinitely for a message from the client.
If the message received from the client is not “end”, prints the
message and reverses the string. The reversed string is sent
back to the client. If the message is “end”, closes the fifo and
ends the process.
Step 3: Client process performs the following:
Opens the named pipe for read and write purposes.
Accepts string from the user.
Checks, if the user enters “end” or other than “end”. Either
way, it sends a message to the server. However, if the string is
“end”, this closes the FIFO and also ends the process.
If the message is sent as not “end”, it waits for the message
(reversed string) from the client and prints the reversed string.
Repeats infinitely until the user enters the string “end”
Now, let's take a look at the sample code of the FIFO
server.
// 2018380130
// Khan Md Shahedul Islam
// fifo_Server_2way.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO_FILE "/tmp/fifo_twoway"
void reverse_string(char *);
int main() {
int fd;
char readbuf[80];
char end[10];
int to_end;
int read_bytes;
/* Create the FIFO if it does not exist */
mkfifo(FIFO_FILE, S_IFIFO|0640);
strcpy(end, "end");
fd = open(FIFO_FILE, O_RDWR);
while(1) {
read_bytes = read(fd, readbuf, sizeof(readbuf));
readbuf[read_bytes] = '\0';
printf("FIFOSERVER: Received string: \"%s\" and length is %d\n",
readbuf, (int)strlen(readbuf));
to_end = strcmp(readbuf, end);
if (to_end == 0) {
close(fd);
break;
reverse_string(readbuf);
printf("FIFOSERVER: Sending Reversed String: \"%s\" and length is
%d\n", readbuf, (int) strlen(readbuf));
write(fd, readbuf, strlen(readbuf));
/*
sleep - This is to make sure other process reads this, otherwise this
process would retrieve the message
*/
sleep(2);
}
return 0;
void reverse_string(char *str) {
int last, limit, first;
char temp;
last = strlen(str) - 1;
limit = last/2;
first = 0;
while (first < last) {
temp = str[first];
str[first] = str[last];
str[last] = temp;
first++;
last--;
return;
Compilation and Execution Results
FIFOSERVER: Received string: "LINUX IPCs" and length
is 10
FIFOSERVER: Sending Reversed String: "sCPI XUNIL" and
length is 10
FIFOSERVER: Received string: "Inter Process
Communication" and length is 27
FIFOSERVER: Sending Reversed String: "noitacinummoC
ssecorP retnI" and length is 27
FIFOSERVER: Received string: "end" and length is 3
Let's have a look at the sample code of the FIFO client
below:-
// 2018380130
//2018380130
// fifo_Client_2way.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO_FILE "/tmp/fifo_twoway"
int main() {
int fd;
int end_process;
int stringlen;
int read_bytes;
char readbuf[80];
char end_str[5];
printf("FIFO_CLIENT: Send messages, infinitely, to end enter \"end\"\n");
fd = open(FIFO_FILE, O_CREAT|O_RDWR);
strcpy(end_str, "end");
while (1) {
printf("Enter string: ");
fgets(readbuf, sizeof(readbuf), stdin);
stringlen = strlen(readbuf);
readbuf[stringlen - 1] = '\0';
end_process = strcmp(readbuf, end_str);
//printf("end_process is %d\n", end_process);
if (end_process != 0) {
write(fd, readbuf, strlen(readbuf));
printf("FIFOCLIENT: Sent string: \"%s\" and string length is %d\n",
readbuf, (int)strlen(readbuf));
read_bytes = read(fd, readbuf, sizeof(readbuf));
readbuf[read_bytes] = '\0';
printf("FIFOCLIENT: Received string: \"%s\" and length is %d\n", readbuf,
(int)strlen(readbuf));
} else {
write(fd, readbuf, strlen(readbuf));
printf("FIFOCLIENT: Sent string: \"%s\" and string length is %d\n",
readbuf, (int)strlen(readbuf));
close(fd);
break;
}
}
return 0;
}
Compilation and Execution Results
FIFO_CLIENT: Send messages, infinitely, to end enter
"end"
Enter string: LINUX IPCs
FIFOCLIENT: Sent string: "LINUX IPCs" and string
length is 10
FIFOCLIENT: Received string: "sCPI XUNIL" and length
is 10
Enter string: Inter Process Communication
FIFOCLIENT: Sent string: "Inter Process
Communication" and string length is 27
FIFOCLIENT: Received string: "noitacinummoC ssecorP
retnI" and length is 27
Enter string: end
FIFOCLIENT: Sent string: "end" and string length is 3
Questions:
1 What are the advantages of using named pipe?
Solution:
A major advantage of using named pipes is that they provide a
useful way to send one-line requests to an OpenEdge background
session running a message handler procedure. Multiple users can
send requests through the same named pipe and each request is
removed from the pipe as it is received. In addition, the message
handler procedure can loop indefinitely looking for input because it
blocks (waits) until there is something to read. Finally, output
through named pipes is more efficient than writing a complete
response to an ordinary file, closing the file, and then informing the
recipient that the results are available. The receiving process can
read the result through a named pipe as soon as it is written.
There is another advantage of named pipes: you can use them
across different systems. Suppose you want real-time
communication of two process running on different machines.
Then share a folder between the two, put your FIFO onto the
folder, and off you go. It is considerably easier than transforming
an application designed to work on files into a service listening on
a port.
A disadvantage of named pipes is that multiple processes cannot
use a single named pipe to send or receive multi-line messages,
unless you define a more complex protocol to control message
interleaving. Also, although synchronizing named pipe input and
output is helpful in some situations, it is a problem in others. For
example, if the message handler procedure running in the
background OpenEdge session starts returning results to an output
named pipe, and for some reason the requestor is not ready to read
the results, the message handler cannot move on to read the next
request.