Inter-Process Communication
Objective
- Demonstrate the ability for processes to communicate using pipe.
- Employ the
dup
anddup2
system calls in pipes. - Use file descriptors for inter-process communications.
- Perform inter-process communications using named pipes.
- Get familiar with the signal table.
- Send signals using the
signal
system call. - Demonstrate the ability of a process to ignore signals using
SIG_IGN
.
Summary
kill
mkfifo
close()
dup()
dup2()
execlp()
exit()
fork()
getlogin()
kill()
mkfifo()
open()
perror()
pipe()
read()
signal()
sleep()
strtok()
usleep()
wait()
write()
Pipes
-
A pipe is a one-way communication for sending and receiving a stream of bytes.
-
A pipe has a read end and a write end. Data written to the write end of a pipe can be read from the read end of the pipe.
-
The system call to create a pipe is called
pipe()
. -
The
pipe
system call takes an array of two integers. It fills in the array with two file descriptors that can be used for low-level I/O. For example:int pfd[2]; pipe(pfd);
-
In the above example, the first file descriptor, shown as
pfd[0]
, refers to the read end of the pipe, while the second file descriptor, shown aspfd[1]
, refers to the write end. -
A clear illustration for the pipe operation is shown as follows:
-
A single process would not use a pipe. Indeed, pipes are commonly used when two processes wish to communicate in a one-way direction. A process creating a pipe splits into two using the
fork()
system call. Consequently, a pipe opened before the fork becomes shared between the two processes. -
A clear illustration for this inter-process communication is shown as follows:
-
Now, this gives two read ends and two write ends. The read end of the pipe will not be closed until both of the read ends are closed, and the write end, also, will not be closed until both of the write ends are closed.
-
For predictable behavior, one of the processes must close its read end, and the other must close its write end. Eventually, it will become a simple pipe as illustrated below:
-
In the above illustration, suppose the parent wants to write down a pipeline to a child. The parent closes its read end, and writes into the other end. Then, the child closes its write end and reads from the other end.
-
When the processes cease communication, the parent closes its write end. This means that the child gets end-of-file (
EOF
) on its next read, and it can close its read end.
Remarks
- If a process reads from a pipe whose write end has been closed, the
read()
returns a0
, indicating end of file. - If a process reads from an empty pipe whose write end is still open, it sleeps until some input becomes available.
- If a process writes to a pipe whose write end has been closed, the write fails and the
write()
function sends aSIGPIPE
signal to the writer process. - A pipe automatically buffers the output of the writer and suspends the writer if the buffer gets full. Similarly, if a pipe is empty the reader is suspended until more output is available.
Example 1
Compile and run the following program, which creates a pipe for inter-process communication. The parent process will write a hello
word, while the child process will read it.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define SIZE 1024
int main(void) {
int pfd[2];
int pid;
char buf[SIZE];
if (pipe(pfd) == -1) {
perror("pipe failed");
exit(1);
}
if ((pid = fork()) < 0) {
perror("fork failed");
exit(2);
} else if (pid == 0) { // child
close(pfd[1]);
while ((read(pfd[0], buf, SIZE)) != 0) {
printf("Child: parent sent '%s'\n", buf);
}
close(pfd[0]);
} else { // parent
close(pfd[0]);
strcpy(buf, "hello...");
write(pfd[1], buf, strlen(buf) + 1); // +1 to include a null terminator
close(pfd[1]);
wait(NULL); // to make sure that the parent will finish after the child
}
// exit(0);
return EXIT_SUCCESS;
}
Exercise 1
Using pipes for inter-process communication, write a C program to produce a parent/child relationship in which the child process continuously displays on the screen any user string its parent process inputs to the pipe.
Duplication
dup
dup
is a system call that duplicates one file descriptor, making them aliases.dup
always uses the smallest available file descriptor.dup
requires the use of#include <unistd.h>
.- The
dup
C prototype isint dup(int fildes);
, wherefildes
is file descriptor.
Example 2
Compile and run the following program, which creates a pipe for inter-process communication to implement the command-line pipe ls | wc -l
using the dup
system call.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
int pfds[2];
pipe(pfds);
if (!fork()) {
close(1); // close normal stdout
dup(pfds[1]); // make stdout same as pfds[1]
close(pfds[0]); // we don't need this
execlp("ls", "ls", NULL);
} else {
close(0); // close normal stdin
dup(pfds[0]); // make stdin same as pfds[0]
close(pfds[1]); // we don't need this
execlp("wc", "wc", "-l", NULL);
}
return EXIT_SUCCESS;
}
Exercise 2
Write a C program in which the child process writes to a pipe all the processes running in the system, while the parent process reads from the pipe to count only those processes owned by the current logged-in user.
Hint: To get the login username, use the function char *getlogin();
which returns a pointer to a string giving a user name associated with the calling process.
dup2
dup2
is a system call similar todup
in that it duplicates one file descriptor, making them aliases, and then deletes the old file descriptor. This becomes very useful when attempting to redirect output, as it automatically takes care of closing the old file descriptor, performing the redirection in one elegant command.dup2
requires the use of#include <unistd.h>
.- The
dup2
C prototype isint dup2(int fildes, int fildes2);
, wherefildes
is file descriptor that will be changed andfildes2
is file descriptor that will be used to make the copy.
Exercise 3
Using the dup2
system call, write a C program in which the parent process prompts the user to enter a system path to list all of its contents to a pipe, and then the child process reads from the pipe to list only the directory names.
Files
-
Files can be used for inter-process communications when a connection between a file and a file descriptor is established using the
open
function. -
The
open
function creates a file descriptor that refers to the open file. -
The
open
function requires the use of#include <fcntl.h>
. -
The
open
C prototype isint open(const char *path, int oflag, mode_t mode);
, where:Parameter Description path
Points to a pathname naming the file. oflag
Constructed by a bitwise-inclusive OR
of flags from a specific list, defined in<fcntl.h>
mode
File mode bits to be applied when a new file is created. return value Returns a file descriptor value for the named file. A negative return value means that an error occurred. -
Applications must specify exactly one of the first three
oflag
values (file access modes) below. Any combination of the remaining values may also be used:Value Description O_RDONLY
Open for reading only. O_WRONLY
Open for writing only. O_RDWR
Open for reading and writing. O_APPEND
Data will be appended to the end of the file prior to each write. O_CREAT
Create the file, if it doesn’t exist.
Example 3
Compile and run the following basic program:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void) {
char *fname = "hellofiles";
int fd1 = open(fname, O_WRONLY | O_CREAT, 0644);
char *text = "Hello world!\n";
write(fd1, text, strlen(text) + 1);
close(fd1);
return EXIT_SUCCESS;
}
Example 4
Compile and run the following program, which use a file descriptor established by the open
function for inter-process communication:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
char *phrase = "Write this in your pipe and read it";
int main(void) {
if (!fork()) {
int fd = open("mypipe", O_WRONLY | O_CREAT, 0600);
write(fd, phrase, strlen(phrase) + 1);
close(fd);
} else {
wait(NULL);
char buf[100];
int fd = open("mypipe", O_RDONLY);
read(fd, buf, 100);
printf("%s\n", buf);
close(fd);
}
return EXIT_SUCCESS;
}
Exercise 4
It is possible to split a string into tokens using the built-in function strtok()
:
char *strtok(char *str, const char *delimiters);
A sequence of calls to this function splits str
into tokens, which are sequences of contiguous characters separated by any of the characters that are part of delimiters
. On a first call, the first character is used as the starting location to scan for tokens. In subsequent calls, the function expects a null pointer and uses the position right after the end of the last token as the new starting location for scanning.
char str[] = "ls -al,pwd,ps -ef,date,top.";
char *token;
token = strtok(str, ",.");
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, ",.");
}
Using files for inter-process communication, write a C program to implement any valid three commands entered by the user, with pipes connecting the commands, for example, ls | wc | tee
.
Named Pipes
- A named pipe, also known as a FIFO (First In, First Out) for its behavior, is an extension to the traditional pipe concept on Unix, and is one of the methods of inter-process communication.
- A traditional pipe is unnamed because it exists anonymously and persists only for as long as the process is running. A named pipe is system-persistent and exists beyond the life of the process and must be deleted once it is no longer being used.
- Instead of a conventional, unnamed or shell pipeline, a named pipeline makes use of the file system. On older systems, named pipes are created by the
mknod
program, usually located in the/usr/bin
directory. On modern systems, we use the standard utilitymkfifo
. - Once a named pipe is created, processes can
open()
,read()
, andwrite()
it just like any other file. Open for reading will block until a process opens it for writing, while open for writing will block until a process opens it for reading. - The named pipe can be deleted just like any file using
rm mypipe
. - From the shell:
mkfifo mypipe
. - From a C program:
mkfifo("pipe_name", permissions);
. mkfifo()
requires the use of#include <sys/stat.h>
.- The
mkfifo
C prototype isint mkfifo(const char *path, mode_t mode);
.
Example 5
Compile and run the following program, which uses a named pipe for inter-process communication:
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(void) {
mkfifo("mypipe", 0644); // create a named pipe
if (!fork()) {
system("ls > mypipe");
} else {
sleep(1);
system("wc -l < mypipe");
}
return EXIT_SUCCESS;
}
Exercise 5
- Create a named pipe with the name
pipe
:mkfifo pipe
- In one terminal, type:
ls -l > pipe
- In another terminal type:
cat < pipe
- Observe that the output of the command run on the first terminal shows up on the second one.
- Note that the order in which you run the commands, after creating the pipe, does not matter.
Signals
- Signals are software generated interrupts that are sent to a process when an event occurs.
- A process can send a signal to other processes. This is called process-to-process signalling. There are also many situations where the kernel originates a signal, such as when file size exceeds limits, when an I/O device is ready, when encountering an illegal instruction or when the user sends a terminal interrupt like
^C
or^Z
. - Linux support 64 signals of both standard and real-time signals.
- In a shell prompt, the
kill -l
command will display the signal table which shows all signals specified with signal number and corresponding signal name. - The first
31
signals are standard signals where every signal has a name starting withSIG
and is defined as a positive unique integer number. - Real-time signals range from
32
to64
. - Unlike standard signals, real-time signals have no predefined meanings: the entire set of real-time signals can be used for application-defined purposes.
Sending Signals
- A process can use the int
kill(int pid, int signal)
system call to send a signal to another process whose id is equal topid
, for example, thekill(getpid(), SIGINT)
would send the interrupt signal to the id of the calling process. This would also have a similar effect toexit()
or^C
command to the process currently being executed. - This
kill
call returns0
for a successful call or-1
to indicate an error. - All standard signals can be caught or ignored except
SIGKILL
(9
) andSIGSTOP
(19
). - Signal requires the use of
#include <signal.h>
.
Receiving Signals
When a process receives a signal, one of the following three things can happen:
- The process can execute the default action for that signal, for example, the default action for signal
SIGTERM
(15
) is to terminate the process. - The process can ignore the signal. Some signals cannot be ignored.
- The process can catch the signal and execute a special function called a signal handler.
Example 6
Compile and run the following program, which executes a default signal action using SIG_DFL
:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void) {
pid_t child_pid = fork();
if (child_pid == 0) {
// the child process
signal(SIGINT, SIG_DFL);
for (int i = 1; i <= 20000000; i++) {
if (i % 100 == 0) {
printf("I'm still alive.\n");
}
}
printf("All done! Nobody dare to stop me! \n");
} else {
// the parent process
// wait for 2 second to so that the child is executed first
usleep(2000000);
printf("Parent will send a dispatch to terminate child...\n");
// send SIGINT to the child process
kill(child_pid, SIGINT);
printf("Dispatched!\n");
}
return EXIT_SUCCESS;
}
Exercise 6
- Modify Example 6 to show that a process can ignore the received signal if it executes
SIG_IGN
. - Again, modify your program to demonstrate that
SIGKILL
andSIGSTOP
cannot be ignored.
Example 7
Compile and run the following program, which catches a signal and executes a handler:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void my_handler(int sig) {
printf("You killed me...\n");
printf("Signal no = (%d)\n", sig);
exit(1);
}
int main(void) {
pid_t pid = fork();
if (pid == 0) {
// child process
signal(SIGINT, my_handler);
while (1) {
printf("Running...\n");
}
} else {
// parent will run this block
sleep(2);
printf("Parent will send a dispatch to terminate child...\n");
kill(pid, SIGINT);
exit(0);
}
return EXIT_SUCCESS;
}
Exercise 7
Write a C program to show the case in which the parent process sends its non-zombie child process any valid standard signal requested by the user (1
to 31
) and the child process treats its receiving signal as follows:
- accepts the action for
SIGKILL
(sure kill) orSIGSTOP
(suspend) signals, - executes a non-return handler to show (signal type, date/time of signal, and
pid
of killing process) forSIGTERM
(terminate) orSIGHUP
(restart) signals, and - ignores all other signals.