8 Process Communication

Inter-Process Communication

Objective

  • Demonstrate the ability for processes to communicate using pipe.
  • Employ the dup and dup2 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.

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 as pfd[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

  1. If a process reads from a pipe whose write end has been closed, the read() returns a 0, indicating end of file.
  2. If a process reads from an empty pipe whose write end is still open, it sleeps until some input becomes available.
  3. If a process writes to a pipe whose write end has been closed, the write fails and the write() function sends a SIGPIPE signal to the writer process.
  4. 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.

example-01.c
#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 is int dup(int fildes);, where fildes 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.

example-02.c
#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 to dup 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 is int dup2(int fildes, int fildes2);, where fildes is file descriptor that will be changed and fildes2 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 is int open(const char *path, int oflag, mode_t mode);, where:

    ParameterDescription
    pathPoints to a pathname naming the file.
    oflagConstructed by a bitwise-inclusive OR of flags from a specific list, defined in <fcntl.h>
    modeFile mode bits to be applied when a new file is created.
    return valueReturns 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:

    ValueDescription
    O_RDONLYOpen for reading only.
    O_WRONLYOpen for writing only.
    O_RDWROpen for reading and writing.
    O_APPENDData will be appended to the end of the file prior to each write.
    O_CREATCreate the file, if it doesn’t exist.

Example 3

Compile and run the following basic program:

example-03.c
#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:

example-04.c
#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 utility mkfifo.
  • Once a named pipe is created, processes can open(), read(), and write() 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 is int mkfifo(const char *path, mode_t mode);.

Example 5

Compile and run the following program, which uses a named pipe for inter-process communication:

example-05.c
#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

  1. Create a named pipe with the name pipe: mkfifo pipe
  2. In one terminal, type: ls -l > pipe
  3. In another terminal type: cat < pipe
  4. Observe that the output of the command run on the first terminal shows up on the second one.
  5. 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 with SIG and is defined as a positive unique integer number.
  • Real-time signals range from 32 to 64.
  • 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

  1. A process can use the int kill(int pid, int signal) system call to send a signal to another process whose id is equal to pid, for example, the kill(getpid(), SIGINT) would send the interrupt signal to the id of the calling process. This would also have a similar effect to exit() or ^C command to the process currently being executed.
  2. This kill call returns 0 for a successful call or -1 to indicate an error.
  3. All standard signals can be caught or ignored except SIGKILL (9) and SIGSTOP (19).
  4. Signal requires the use of #include <signal.h>.

Receiving Signals

When a process receives a signal, one of the following three things can happen:

  1. The process can execute the default action for that signal, for example, the default action for signal SIGTERM (15) is to terminate the process.
  2. The process can ignore the signal. Some signals cannot be ignored.
  3. 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:

example-06.c
#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

  1. Modify Example 6 to show that a process can ignore the received signal if it executes SIG_IGN.
  2. Again, modify your program to demonstrate that SIGKILL and SIGSTOP cannot be ignored.

Example 7

Compile and run the following program, which catches a signal and executes a handler:

example-07.c
#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) or SIGSTOP (suspend) signals,
  • executes a non-return handler to show (signal type, date/time of signal, and pid of killing process) for SIGTERM (terminate) or SIGHUP (restart) signals, and
  • ignores all other signals.