Interprocess communication - Pipeline

Introduction to interprocess communication

Purpose of interprocess communication

  • Data transfer: one process needs to send its data to another process
  • Resource sharing: multiple processes share the same resources.
  • Notification event: a process needs to send a message to another process or group of processes to notify it (them) of an event (such as notifying the parent process when the process terminates).
  • Process control: some processes want to fully control the execution of another process (such as the Debug process). At this time, the control process wants to be able to intercept all traps and exceptions of another process and know its state changes in time.

Interprocess communication development

  • The Conduit
  • System V interprocess communication
  • POSIX interprocess communication

Interprocess communication classification

The Conduit

  • Anonymous pipe
  • name pipes

System V IPC

  • System V message queuing
  • System V shared memory
  • System V semaphore

POSIX IPC

  • Message queue
  • Shared memory
  • Semaphore
  • mutex
  • Conditional variable
  • Read write lock

To make two different processes communicate, the premise is to let the two processes see the same resource

The Conduit

What is a pipe

  • Pipeline is the oldest form of interprocess communication in Unix.
  • We call a data flow from one process to another a "pipe"

Anonymous Pipe

#include <unistd.h>
function:Create an anonymous pipe
 Function prototype
int pipe(int fd[2]);
parameter
fd: File descriptor array,among fd[0]Indicates the reading end, fd[1]Indicates the write end
 Return value:0 is returned for success and 0 is returned for failure-1

Example code

//Example: read data from the keyboard, write to the pipeline, read the pipeline, and write to the screen
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main( void )
{
    int fds[2];
    char buf[100];
    int len;
    
    if ( pipe(fds) == -1 )//Create anonymous pipe
    	perror("make pipe"),exit(1);
    
    // read from stdin
    while ( fgets(buf, 100, stdin) ) //Loop write data to buf
    {
        len = strlen(buf);
        
        // write into pipe
        //Write first and then judge
        if (write(fds[1], buf, len) != len) //When the number of characters actually written to the pipeline is not equal to the buf length, an error is written
        {
            perror("write to pipe");
            break;
        }
        
        memset(buf, 0x00, sizeof(buf));//Set the contents of buf to 0
        
        // read from pipe
        if ( (len=read(fds[0], buf, 100)) == -1 ) //Read before Judge
        {
            perror("read from pipe");
            break;
        }
        
        // write to stdout
        if ( write(1, buf, len) != len ) //Write first and then judge
        {
            perror("write to stdout");
            break;
        }
        
    }
}

Using fork to share pipeline principle

From the perspective of file descriptor - deep understanding of pipeline

Because our initial process opens three files: standard input, standard output and standard error, all subsequent processes open these three files by default

Why aren't the files private? Because the file does not belong to the process

From the perspective of kernel - Pipeline essence

f_op is the function pointer we talked about before.

The figure simplifies the understanding. The inode pointer is not in the struct file

So, look at the pipeline, just as you look at the file! The use of pipes is consistent with files, which caters to the "Linux everything is a file idea".

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0)
int main(int argc, char *argv[])
{ 	
    int pipefd[2]; 
    if (pipe(pipefd) == -1) 
        ERR_EXIT("pipe error");
    
    pid_t pid;
	pid = fork();
    if (pid == -1)
    	ERR_EXIT("fork error");
    
    if (pid == 0) //Subprocess
    {
        close(pipefd[0]);//Close the reader
        
        //Write to pipe
        write(pipefd[1], "hello", 5);
        //Close the write side to reduce the occupation of file descriptors
        close(pipefd[1]);
        exit(EXIT_SUCCESS);
    }
    
    //The parent process closes the write side
    close(pipefd[1]);
    
    char buf[10] = {0};
    //Read data from pipe
    read(pipefd[0], buf, 10);
    printf("buf=%s\n", buf);
    
    return 0;
}

Pipeline read / write rules

  1. If the writer does not close the file descriptor or write, the reader may be blocked for a long time (there may be data in the pipeline at the beginning, so it will not be blocked immediately)

    Example:

    #include<stdio.h>
    #include <unistd.h>
    #include<string.h>
    
    
    int main()
    {
      int pi[2] = {0};
      if (pipe(pi) < 0)//Failed to create anonymous pipeline
      {
        perror("pipe");
        return 1;
      }
      
      int pid = fork();
      if (pid < 0)
      {
        perror("fork");
        return 1;
      }
      
      //Let the child process write and the parent process read  
      if (pid == 0)//Subprocess
      {
        //For fear of mistakes, another interface is used, so the read of the child process is turned off
        close(pi[0]);
    
        const char* buf = "I am a child!\n";
    
        while (1)
        {
          write(pi[1], buf, strlen(buf));
          sleep(3);//The child process writes every 3s
        
        }
      }
      else//Parent process, read only 
      {
        close(pi[1]);
        char buf[64];
        while (1)
        {
          ssize_t s = read(pi[0], buf, sizeof(buf) - 1);
          
          if (s > 0)
          {
            buf[s] = 0;
          }
    
          printf("father get info from pipe:%s\n", buf);
        
        } 
      }
    
      return 0;
    }
    
    

    Because the child process writes to the pipeline every 3s, while the parent process reads the pipeline data continuously, the parent process will block for a period of time:

    Therefore, if we close the file descriptor of the child process, the parent process will be waiting all the time, that is, it will be blocked for a long time.

  2. When we write, if the write conditions are not met (the pipeline is full and the reading speed of the reader is too slow), the writer will block

    #include<stdio.h>
    #include <unistd.h>
    #include<string.h>
    
    
    int main()
    {
      int pi[2] = {0};
      if (pipe(pi) < 0)//Failed to create anonymous pipeline
      {
        perror("pipe");
        return 1;
      }
      
      int pid = fork();
      if (pid < 0)
      {
        perror("fork");
        return 1;
      }
      
      //Let the child process write and the parent process read  
      if (pid == 0)//Subprocess
      {
        //For fear of mistakes, another interface is used, so turn off the reading of the child process
        close(pi[0]);
    
        const char* buf = "I am a child!\n";
        
        int count = 0;
        while (1)
        {
          
          write(pi[1], buf, strlen(buf));
          printf("CHILD:%d\n", count++);
        }
      }
      else//Parent process, read only 
      {
        close(pi[1]);
        char buf[64];
        while (1)
        {
          ssize_t s = read(pi[0], buf, sizeof(buf) - 1);
          sleep(1);//The parent process reads every 1s
    
          if (s > 0)
          {
            buf[s] = 0;
          }
            
          printf("father get info from pipe:%s\n", buf);
        
        } 
      }
    
      return 0;
    }
    

    It can be seen that the child process writes suddenly at the beginning, while the parent process reads data every 1s, and reads data from the parent process, and the child process does not write again because the pipeline is full and the writing conditions are not met, so the writing cannot be completed.

  3. If the writer closes the file descriptor, the reader will read 0 when reading the pipeline, that is, the read function returns 0

    #include<stdio.h>
    #include <unistd.h>
    #include<string.h>
    
    
    int main()
    {
      int pi[2] = {0};
      if (pipe(pi) < 0)//Failed to create anonymous pipeline
      {
        perror("pipe");
        return 1;
      }
      
      int pid = fork();
      if (pid < 0)
      {
        perror("fork");
        return 1;
      }
      
      //Let the child process write and the parent process read  
      if (pid == 0)//Subprocess
      {
        //For fear of mistakes, another interface is used, so the read of the child process is turned off
        close(pi[0]);
    
        const char* buf = "I am a child!\n";
        
        int count = 0;
        while (1)
        {
          if (count == 5)//The child process writes only 5 times
            close(pi[1]);
    
          write(pi[1], buf, strlen(buf));
          ++count;
        }
      }
      else//Parent process, read only 
      {
        close(pi[1]);
        char buf[64];
        while (1)
        {
          ssize_t s = read(pi[0], buf, sizeof(buf) - 1);
          sleep(1);//The parent process sleeps for 1s and reads again
    
          if (s > 0)
          {
            buf[s] = 0;
            
          	printf("father get info from pipe:%s\n", buf);
          }
          printf("father exit return : %d\n", s);
        } 
    
      }
    
      return 0;
    }
    
    

    The child process writes only 5 times without sleep, while the parent process reads once in 1s of sleep. When the child process stops writing, the data read by the parent process from the pipeline is 0.

  4. If the reader is closed, the writer process may be killed directly

    #include<stdio.h>
    #include <unistd.h>
    #include<string.h>
    #include<stdilb.h>
    
    int main()
    {
      int pi[2] = {0};
      if (pipe(pi) < 0)//Failed to create anonymous pipeline
      {
        perror("pipe");
        return 1;
      }
      
      int pid = fork();
      if (pid < 0)
      {
        perror("fork");
        return 1;
      }
      
      //Let the child process write and the parent process read  
      if (pid == 0)//Subprocess
      {
        //For fear of mistakes, another interface is used, so the read of the child process is turned off
        close(pi[0]);
    
        const char* buf = "I am a child!\n";
        
        int count = 0;
        while (1)
        {
    		
          write(pi[1], buf, strlen(buf));
        	printf("CHILD:%d\n", count++);
        }
    
        exit(2);
      }
      else//Parent process, read only 
      {
        close(pi[1]);
        char buf[64];
        int count = 0;
        while (1)
        {
          if (5 == count++)
          {   
              close(pi[0]);
          	  break;
          }
          ssize_t s = read(pi[0], buf, sizeof(buf) - 1);
          sleep(1);
    
          if (s > 0)
          {
            buf[s] = 0;
          	printf("father get info from pipe:%s\n", buf);
          }
    
        } 
    
      }
    
      return 0;
    }
    
    

    Preliminary observation:

We can see that after the parent process reads 5 times, both the parent and child processes end, and the child process does not continue printing, but exits.

Instead of letting the parent process end, we use the script to observe the status of the parent-child process:

shell script:

   while :; do ps axj |grep mypipe | grep -v grep; echo "######################################"; sleep 1; done

effect:

You can see that the child process is actually in a zombie state, that is, it exits.

In fact, it was killed by the operating system, because no one in the pipeline had read the data, and the sub process was wasting resources by writing data into the pipeline. The operating system did not allow this behavior to happen, so it killed the sub process.

Since the child process is killed, you need to recycle it with the parent process and get its signal

Modify the code to make the parent process wait and get the signal:

You can see that the exit signal of the child process is 13

Let's use kill -l to see kill's command 13:

As the name suggests, this signal is used to kill pipeline related processes.

Modification code:

   #include<stdio.h>
   #include <unistd.h>
   #include<string.h>
   #include<stdlib.h>
   #include<sys/wait.h>
   
   int main()
   {
     int pi[2] = {0};
     if (pipe(pi) < 0)//Failed to create anonymous pipeline
     {
       perror("pipe");
       return 1;
     }
     
     int pid = fork();
     if (pid < 0)
     {
       perror("fork");
       return 1;
     }
     
     //Let the child process write and the parent process read  
     if (pid == 0)//Subprocess
     {
       //For fear of mistakes, another interface is used, so the read of the child process is turned off
       close(pi[0]);
   
       const char* buf = "I am a child!\n";
       
       int count = 0;
       while (1)
       {
   
         write(pi[1], buf, strlen(buf));
         printf("CHILD:%d\n", count++);
       }
   
       exit(2);
     }
     else//Parent process, read only 
     {
       close(pi[1]);
       char buf[64];
       int count = 0;
       while (1)
       {
         if (5 == count++)
         { 
           close(pi[0]);
           break;
         }
         
         ssize_t s = read(pi[0], buf, sizeof(buf) - 1);
         sleep(1);
   
         if (s > 0)
         {
           buf[s] = 0;
           printf("father get info from pipe:%s\n", buf);
       
         }
         
       } 
   
         //The parent process gets the exit signal of the child process
         int status = 0;
         waitpid(pid, &status, 0);
         printf("child exited, father gets the signal:%d\n", status & 0x7f);
     }
   
     return 0;
   }
   

In the | pipeline in the command line, there is brotherhood between processes

We use the sleep 2000 | sleep 1000 command to run, and then copy a session to view their process pid information:

  • When no data is readable

    • O_NONBLOCK disable: the read call is blocked, that is, the process suspends execution until data arrives.
    • O_NONBLOCK enable: the read call returns - 1, and the errno value is EAGAIN.
  • When the pipe is full

    • O_NONBLOCK disable: the write call blocks until a process reads the data
    • O_NONBLOCK enable: the call returns - 1, and the errno value is EAGAIN
  • If the file descriptors corresponding to all pipeline write ends are closed, read returns 0

  • If the file descriptors corresponding to all pipeline readers are closed, the write operation will generate the signal SIGPIPE, which may lead to the write process
    sign out

  • When the amount of data to be written is not greater than pipe_ When buf, linux will guarantee the atomicity of writing.

  • When the amount of data to be written is greater than pipe_ When buf, linux will no longer guarantee the atomicity of writes.

Pipeline characteristics

  • It can only be used for communication between processes with common ancestors (processes with kinship); Typically, a pipeline is created by a process
    After that, the process calls fork, and then the pipeline can be applied between the parent and child processes.
  • Pipeline provides streaming services
  • Generally speaking, the process exits and the pipeline is released, so the pipeline life cycle changes with the process
  • Generally speaking, the kernel will synchronize and mutually exclusive pipeline operations
  • The pipeline is half duplex, and the data can only flow in one direction; When both parties communicate, two pipelines need to be established

Anonymous pipes have brotherhood between processes

name pipes

  • One limitation of pipeline application is that it can only communicate between processes with a common ancestor (kinship).
  • If we want to exchange data between unrelated processes, we can use FIFO files to do this, which is often called named pipes.
  • Named pipes are a special type of file

Create a named pipe

  • Named pipes can be created from the command line by using the following command:

    $ mkfifo filename
    
  • Named pipes can also be created from the program. The related functions are:

    int mkfifo(const char *filename, mode_t mode);
    //filename is the pipe name
    
  • To create a named pipe:

    int main(int argc, char *argv[])
    {
        mkfifo("p2", 0644);
        return 0;
    }
    

The difference between anonymous pipes and named pipes

  • Anonymous pipes are created and opened by the pipe function.
  • The named pipe is created by the mkfifo function and opened with open
  • The only difference between FIFO (named pipe) and pipe (anonymous pipe) is that they are created and opened in different ways, but once these works are completed
    After they become, they have the same meaning

Opening rules for named pipes

  • If the current open operation is to open FIFO for reading

    • O_NONBLOCK disable: blocks the FIFO until a corresponding process opens it for writing
    • O_NONBLOCK enable: returns success immediately
  • If the current open operation is to open FIFO for writing

    • O_NONBLOCK disable: blocks until a corresponding process opens the FIFO for reading
    • O_NONBLOCK enable: failure is returned immediately with error code ENXIO

Using named pipes to realize server client communication

makefile

# ll
total 12
-rw-rw-r-- 1 ysj ysj 568 Oct  6 01:18 client.c
-rw-rw-r-- 1 ysj ysj 138 Oct  6 01:07 makefile
-rw-rw-r-- 1 ysj ysj 662 Oct  6 01:15 server.c

# cat Makefile
.PHONY:all
all:server client 

server:server.c
	gcc $^ -o $@
client:client.c
	gcc $^ -o $@

.PHONY:clean
clean:
	rm -f server client fifo

server.c

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

#define FIFE_FIFO "fifo"

int main()
{
  umask(0);
  mkfifo(FIFE_FIFO, 0666);
  
  int rfd = open(FIFE_FIFO, O_RDONLY);
  if (rfd < 0)
  {
    perror("open");
    return 1;
  }
  
  char buf[64];

  while (1)
  {
    buf[0] = 0;
    printf("p")
    ssize_t s = read(rfd, buf, sizeof(buf) - 1);
    if (s > 0)
    {
        buf[s - 1]= 0;
        printf("client say:# %s\n", buf);
        
    }
    else if(s == 0) 
    {
      printf("exit,too\n");
      break; 
    }
    else {
      perror("read");
      return 1;
    }
    
  }

  close(rfd);
  return 0;
}

client.c

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

#define FIFE_FIFO "fifo"

int main()
{
  
  int wfd = open(FIFE_FIFO, O_WRONLY);
  if (wfd < 0)
  {
    perror("open");
    return 1;
  }
  
  char buf[64];

  while (1)
  {
    printf("Please Enter:# ");
    fflush(stdout);
      
    ssize_t s = read(0, buf, sizeof(buf) - 1);
    if (s > 0)
    {
      buf[s]= 0;
      write(wfd, buf, strlen(buf));
    }
    else 
    {
      printf("exit\n");
      break; 
    }
    
  }

  close(wfd);
  return 0;
}

result:

Why didn't you print please wait... At first?
Because in the server, the reader is blocked at the open when it is not opened. When the client starts running and the writer is opened, the server ends blocking and starts running before printing please wait

servet has nothing to do with client, so named pipes are different from anonymous pipes in this regard:

Keywords: C Linux Operating System

Added by Petsmacker on Mon, 24 Jan 2022 14:44:36 +0200