Linux network programming - using the pipe of interprocess communication on the server side

A basic concept of interprocess communication

1.1 basic understanding of interprocess communication

Inter Process Communication (IPC)

Interprocess communication means that two different processes can exchange data. In order to achieve this, the operating system kernel needs to provide memory space that two processes can access at the same time, that is, open up a buffer in the kernel. The whole data exchange process is shown in the figure below:

Figure 1} interprocess communication

As can be seen from Figure 1-1 above, as long as there is a memory space that can be accessed by two processes at the same time, data can be exchanged through this space. But we know that the process has a completely independent memory structure, and even the child process created through the fork function will not share memory space with its parent process. Therefore, interprocess communication can only open up this shared memory buffer in the operating system kernel.

See the blog link below for the mechanism of interprocess communication in expansion

Interprocess communication of Linux processes

II. Pipe of Linux

2.1 basic concept of pipeline

Pipe, also known as anonymous pipe, is one of the most common ways of inter process communication under Linux. It is a channel for data flow between two processes.

The inter process communication structure model based on pipeline is shown in Figure 2-2 below.

Figure 2. Interprocess communication model based on pipeline

To complete interprocess communication, you need to create a pipeline. The pipeline does not belong to the resource of the process, but, like the socket, belongs to the operating system (that is, it is not the copy object of the fork function). Therefore, the two processes communicate through the memory space provided by the operating system kernel.

2.2 characteristics of pipeline

Linux pipeline has the following characteristics:

  • A pipe has no name, so it is also called an anonymous pipe.
  • Pipeline is a half duplex communication mode, and data can only flow in one direction; When two-way communication is required, two pipelines need to be established. (disadvantage 1)
  • Pipes can only be used between parent-child processes or sibling processes (that is, processes with kinship). (disadvantage 2)
  • The pipeline alone constitutes an independent file system. For the processes at both ends of the pipeline, the pipeline is a file, but it is not an ordinary file. It does not belong to a file system, but a self-contained portal. It constitutes a file system alone and exists only in memory.
  • Read and write of data: the content written by a process to the pipeline is read by the process at the other end of the pipeline. The content written is added to the end of the pipeline buffer every time, and the data is read from the head of the buffer every time.
  • The buffer of the pipeline is limited (the pipeline only exists in memory, and a page size is allocated to the buffer when the pipeline is created).
  • The data transmitted in the pipeline is an unformatted byte stream, which requires that the reader and writer of the pipeline must agree on the data format in advance. For example, how many bytes count as a message (or command, record, etc.).

2.3 implementation method of pipeline

When a process creates a pipeline, the Linux system kernel prepares two file descriptors for using the pipeline: one for pipeline input (i.e. process write operation), that is, writing data in the pipeline; The other is used for the output of the pipeline (i.e. process read operation), that is, to read data from the pipeline, and then call the normal system calls (write and read functions) for the two file descriptors. The kernel uses this abstract mechanism to realize the special operation of the pipeline. As shown in Figure 2-3 below.

Figure 3. Structure of pipeline
  • Description of pipe structure

fd0: the file descriptor used when reading data from the pipeline, i.e. the pipeline exit, is used for the read operation (read) of the process, which is called the read pipeline file descriptor.

fd1: the file descriptor used when writing data to the pipeline, that is, the pipeline entry, is used for the write operation of the process, which is called the write pipeline file descriptor.

If a pipeline is only associated with one process, it can realize the internal communication of the process itself. This is generally used for the communication between processes within the process (I have encountered it).

Usually, a process that creates a pipeline will then create a child process. Since the child process is a process created by copying all the resources of the parent process, the child process will inherit the file descriptor of the read-write pipeline from the parent process, so that the communication pipeline between the parent and child processes is established. As shown in Figure 2-4 below.

Figure 4} pipeline between parent and child processes

Half duplex communication description of parent-child process pipeline

  • fd[0] of the parent process = f[0] of the child process, which means that both file descriptors identify the outlet of the same pipe.
  • fd[1] of the parent process = f[1] of the child process, which means that both file descriptors identify the entry end of the same pipeline.

Data transmission direction of parent-child process

Parent process - > data transmission direction of child process: fd[1] of parent process - > pipeline - > fd[0] of child process

Child process - > data transmission direction of parent process: fd[1] of child process - > pipeline - > fd[0] of parent process

For example, when data is transferred from the parent process to the child process, the parent process closes the file descriptor fd[0] of the read pipeline and the child process closes the file descriptor fd[1] of the write pipeline, thus establishing a communication pipeline from the parent process to the child process, as shown in Figure 2-5 below.

Figure 5} pipeline from parent process to child process

2.4 read and write operation rules of pipeline

After establishing a pipeline, you can read and write the pipeline through the corresponding file I/O operation functions (such as read, write, etc.), and the data transfer process has been completed.

It should be noted that since one end of the pipe has been closed, the following three points should be paid attention to during corresponding operations:

  • If you read data from a pipeline closed by a write descriptor (fd[1]), after reading all the data, the read function returns 0, indicating that the end of the file has been reached. Strictly speaking, it can be said that the end is reached only when there is no data to continue writing, so it should be clear whether there is no data to write temporarily or the end of the file has been reached. If the former, the reading process should wait. The situation of multi process writing and single process reading will be more complex.
  • If data is written to a pipeline closed by a read descriptor (fd[0]), a SIGPIPE signal is generated. The write function returns - 1 regardless of whether the signal is ignored or processed.
  • Constant PIPE_BUF specifies the size of pipeline buffer in the kernel, so you should pay attention to it in writing pipeline. Write pipe to pipe at a time_ BUF or less byte data will not be interleaved with the contents written by other processes; Conversely, when there are multiple processes that write pipes, more than pipe is written to them_ When buf bytes of data, content interleaving will occur, that is, it covers the existing data in the pipeline.

III. operation of pipeline

3.1 pipe creation

The Linux kernel provides the function pipe to create a pipe. The standard calling format is described as follows:

  • pipe() - create an anonymous pipe.
#include <unistd.h>

int pipe(int pipefd[2]);

/*Parameter description
pipefd[2]: Integer array of file descriptors with length 2
pipefd[0]: Is the file descriptor of the pipeline readout end, that is, pipefd[0] can only be opened for read operations.
pipefd[1]: Is the file descriptor of the pipeline write side, that is, pipefd[1] can only be opened for write operations.
*/

//Return value: 0 for success and - 1 for failure.

[programming example] use the pipe function to create a pipe. An example of using a pipe in a process.

  • pipe.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUF_SIZE 100

int main(int argc, char *argv[])
{
    int fd[2];
    char write_buf[BUF_SIZE] = {0};  //Write buffer
    char read_buf[BUF_SIZE] = {0};   //Read buffer
    
    if(pipe(fd) < 0)                 //Create pipe
    {
        printf("create pipe error!\n");
        exit(1);
    }
    printf("write data to pipe: ");
    fgets(write_buf, BUF_SIZE, stdin);  //Enter a line of string from the console
    write(fd[1], write_buf, sizeof(write_buf));
    read(fd[0], read_buf, sizeof(write_buf));
    
    printf("read data from pipe: %s", read_buf);
    printf("pipe read_fd: %d, write_fd: %d\n", fd[0], fd[1]);
    
    close(fd[0]);                    //Close the readout file descriptor of the pipeline
    close(fd[1]);                    //Close the write side file descriptor of the pipeline
    return 0;
}
  • Operation results

$ gcc pipe.c -o pipe
$ ./pipe
write data to pipe: This is a test!
read data from pipe: This is a test!
pipe read_fd: 3, write_fd: 4

Note: when closing a pipeline, you must close both ends of the pipeline, that is, close both file descriptors of the pipeline.

3.2 inter process communication through pipeline

When the parent process calls the pipe function, it will create a pipe and obtain the file descriptors corresponding to both ends of the pipe inlet and outlet. At this time, the parent process can read and write the same pipe, that is, in this example program. However, the purpose of the parent process is usually to exchange data with the child process, so one of the file descriptors in the pipe entry or exit needs to be passed to the child process. How to deliver? The answer is to call the fork function.

  • Detailed steps for using pipes in parent-child processes

1, call the pipe function in the parent process to create a pipeline.

2, call the fork function in the parent process to create a child process.

3, close the file descriptor at one end of the pipe that is not used in the parent process, and then call the corresponding write operation function, such as write, to write the corresponding data to the pipeline.

4, close the file descriptor at one end of the pipe that is not used in the child process, then call the corresponding read operation function, such as read, to read the corresponding data from the pipeline.

5, in the father child process, call the close function to close the file descriptor of the pipeline.

[programming example] use pipes in parent-child processes. Create a pipeline in the parent process and call the fork function to create a child process. The parent process writes a line of string data to the pipeline. In the child process, read the string from the pipeline and print it.

  • pipe_fatherson.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUF_SIZE 100

int main(int argc, char *argv[])
{
    int fds[2], len;
    pid_t pid;
    char buf[BUF_SIZE];
    
    if(pipe(fds) < 0){                      //Create a pipeline and store two file descriptors in the fds array
        printf("pipe() error!\n");
        exit(1);
    }
    if((pid = fork()) < 0){                 //Create a child process   
        printf("fork() error!\n");
        exit(1);
    }
    else if(pid > 0)                        //Parent process execution area
    {
        printf("Parent Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);
        close(fds[0]);                      //Close the pipe readout descriptor of the parent process
        fgets(buf, BUF_SIZE, stdin);        //The terminal inputs a line of string data 
        write(fds[1], buf, strlen(buf));    //Write data to pipeline
    }
    else                                    //Subprocess execution area
    {
        printf("Child Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);
        close(fds[1]);                      //Close the pipeline write side descriptor of the child process
        len = read(fds[0], buf, BUF_SIZE);  //Read string data from pipe
        buf[len] = '\0';
        printf("%s", buf);
        close(fds[0]);                      //Close the pipeline readout descriptor of the child process
    }
    
    close(fds[1]);                          //Close the pipe writer descriptor of the parent process
    return 0;
}
  • Operation results

$ gcc pipe_fatherson.c -o pipe_fatherson
[wxm@centos7 pipe]$ ./pipe_fatherson
Parent Proc, fds[0]=3, fds[1]=4
Child Proc, fds[0]=3, fds[1]=4
Who are you?
Who are you?

Code description

  • The fourteenth line: in the parent process, call the pipe function to create the pipeline, and the fds array saves the file descriptors for reading and writing I/O.
  • Line 18: next call the fork function. The child process will have two file descriptors obtained through the pipe function call on line 14 at the same time. This can be verified from the above running results. be careful! Instead of the pipe, the file descriptor used for pipe I/O is copied. At this point, both parent and child processes have file descriptors for pipeline I/O.
  • Lines 27 and 33: the parent process writes a string to the pipeline through the code in line 27; The subprocess receives a string from the pipeline through line 33.
  • Lines 36 and 39: Code in line 36: close the readout end file descriptor of the pipeline before the subprocess ends running; In line 39, close the write side file descriptor of the pipeline before the parent process (also the main process) ends running.
  • Using pipes in sibling processes

The method of using the pipeline for data communication in the brother process is similar to that in the parent-child process, except that the two processes operating on the pipeline can be replaced by the brother process, and the I/O file descriptor of the pipeline is closed in the parent process.

[programming instance] the application instance of the pipeline used in the value brother process. First, create a pipeline and two child processes in the main process (that is, the parent process), and then send a string to the second child process through the pipeline in the first child process. The second child process reads data from the pipeline, and then outputs the data to the screen.

  • pipe_brother.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define BUF_SIZE 100

int main(int argc, char *argv[])
{
    int fds[2], len, status;
    pid_t pid, pid1, pid2;
    char buf[BUF_SIZE];
    
    if(pipe(fds) < 0){                      //Create a pipeline and store two file descriptors in the fds array
        printf("pipe() error!\n");
        exit(1);
    }
    printf("Parent Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);

    if((pid1 = fork()) < 0){                 //Create child process 1
        printf("fork() error!\n");
        exit(1);
    }
    else if(pid1 == 0)                       //Subprocess 1 execution area
    {
        printf("Child1 Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);
        close(fds[0]);                       //Close the pipeline readout descriptor of subprocess 1
        fgets(buf, BUF_SIZE, stdin);         //Input string data from terminal
        write(fds[1], buf, strlen(buf));     //Write data to pipeline
        close(fds[1]);                       //Close the pipeline write side descriptor of subprocess 1
        exit(1);
    }
    
    if((pid2 = fork()) < 0){                 //Create child process 2
        printf("fork() error!\n");
        exit(1);
    }
    else if(pid2 == 0)                       //Subprocess 2 execution area
    {
        printf("Child2 Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);
        close(fds[1]);                      //Close the pipeline write end descriptor of child process 2
        len = read(fds[0], buf, BUF_SIZE);  //Read string data from pipe
        buf[len] = '\0';
        printf("%s", buf);
        close(fds[0]);                      //Close the pipeline readout descriptor of subprocess 2
        exit(2);
    }
    else                                    //Parent process execution area
    {
        int proc_num = 2;                   //The number of child processes is 2
        while(proc_num)
        {
            while((pid = waitpid(-1, &status, WNOHANG)) == 0)  //Wait for the child process to end
            {
                continue;
            }
            if(pid == pid1){                                   //The end is subprocess 1
                printf("Child1 proc eixt, pid=%d\n", pid1);
                proc_num--;
            }
            else if(pid == pid2){                              //Child process 2 ends
                printf("Child2 proc eixt, pid=%d\n", pid2);
                proc_num--;
            }
            if(WIFEXITED(status))                              //Gets the status return value when the child process exits
                printf("Child proc send %d\n", WEXITSTATUS(status));
        }
    }

    close(fds[0]);                      //Close the pipe readout descriptor of the parent process
    close(fds[1]);                      //Close the pipe writer descriptor of the parent process
    return 0;
}
  • Operation results

$ gcc pipe_brother.c -o pipe_brother
[wxm@centos7 pipe]$ ./pipe_brother
Parent Proc, fds[0]=3, fds[1]=4
Child1 Proc, fds[0]=3, fds[1]=4
Child2 Proc, fds[0]=3, fds[1]=4
Hello,I`m your brother!
Hello,I`m your brother!
Child1 proc eixt, pid=4679
Child proc send 1
Child2 proc eixt, pid=4680
Child proc send 2

Code description

  • The fifty-fourth, fifty-eighth, sixty-second line: call the waitpid function in the parent process, wait for the termination of the child process, and if the child process does not terminate, it will not enter the blocking state, but return to 0. When the sub process 1 finishes running, the function returns the process ID of the sub process and executes the code in line 58; Similarly, when child process 2 ends running, the function returns the process ID of the child process and executes the code in line 62.

3.3 realize two-way communication between processes through pipeline

The following is an example of two processes and one pipeline for two-way data exchange. Its communication mode is shown in Figure 6 below.

Figure 6. Pipeline two-way communication model 1

As can be seen from Fig. 6, two-way data communication can be carried out through one pipeline. However, special attention should be paid to using this model. Examples are given first, and then analyzed and discussed later.

  • pipe_duplex.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define BUF_SIZE 100

int main(int argc, char *argv)
{
    int fds[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message";
    char buf[BUF_SIZE];
    pid_t pid, ret;
    
    ret = pipe(fds);
    if(ret < 0)
    {
        perror("pipe() error");
        exit(1);
    }
    
    pid = fork();
    if(pid == 0)  //Child process area
    {
        write(fds[1], str1, sizeof(str1));       //Writes the string str1 to the pipeline
        sleep(2);                                //Pause the child process for 2 seconds
        read(fds[0], buf, BUF_SIZE);             //Read data from pipe
        printf("Child proc output: %s\n", buf);  //Print the string read from the pipe
    }
    else          //Parent process area
    {
        read(fds[0], buf, BUF_SIZE);              //Read data from pipe
        printf("Parent porc output: %s\n", buf);
        write(fds[1], str2, sizeof(str2));        //Writes the string str2 to the pipeline
        sleep(3);                                 //Pause the parent process for 3 seconds
    }
    return 0;
}
  • Operation results

$ gcc pipe_duplex.c -o pipe_duplex

$ ./pipe_duplex
Parent porc output: Who are you?
Child proc output: Thank you for your message

The running result is the same as we expected: the child process writes the string str1 to the pipeline, and the parent process reads the string from the pipeline; The parent process writes the string str2 to the pipeline, and the child process reads the string from the pipeline. What would happen if we commented out the code on line 27?

$ ./pipe_duplex
Child proc output: Who are you?

From the above running results and process status, we can see that the process pipe_duplex is in a deadlock state (< defunct >), what is the reason?

"When passing data to the pipeline, the first reading process will take the data from the pipeline."

After entering the pipeline, the data becomes no master data. That is, the process that reads the data first through the read function will get the data, even if the process transmits the data to the pipeline. Therefore, commenting out line 27 will cause problems. In line 28, the child process will read back the data it sent to the pipeline in line 26. As a result, the parent process will wait indefinitely for data to enter the pipeline after calling the read function, resulting in a deadlock.

As can be seen from the above example, it is not easy to use only one pipe for two-way communication between processes. In order to achieve this, the program needs to predict and control the running process, which is different in each system and can be regarded as an impossible task. In that case, how to conduct two-way communication?

Create two pipes

Very simple, one pipeline cannot complete the two-way communication task, so you need to create two pipelines, each responsible for different data flow directions. The process is shown in Figure 7 below.

Fig. 7 two way communication model 2

As can be seen from Figure 7 above, the use of two pipelines can avoid unpredictable or uncontrollable factors of program flow. The above model is used to improve pipe_duplex.c procedure.

  • pipe_duplex2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define BUF_SIZE 100

int main(int argc, char *argv)
{
    int fds1[2], fds2[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message";
    char buf[BUF_SIZE];
    pid_t pid, ret;
    
    ret = pipe(fds1);   //Create pipe 1
    if(ret < 0)
    {
        perror("pipe() error");
        exit(1);
    }
    
    ret = pipe(fds2);   //Create pipe 2
    if(ret < 0)
    {
        perror("pipe() error");
        exit(1);
    }
    
    pid = fork();
    if(pid == 0)  //Child process area
    {
        write(fds1[1], str1, sizeof(str1));      //Write string str1 to pipeline 1
        read(fds2[0], buf, BUF_SIZE);            //Read data from pipe 2
        printf("Child proc output: %s\n", buf);  //Print the string read from the pipe
    }
    else          //Parent process area
    {
        read(fds1[0], buf, BUF_SIZE);             //Read data from pipe 1
        printf("Parent porc output: %s\n", buf);
        write(fds2[1], str2, sizeof(str2));       //Write string str2 to pipeline 2
        sleep(3);                                 //Pause the parent process for 3 seconds
    }
    return 0;
}
  • Operation results

$ gcc pipe_duplex2.c -o pipe_duplex2

$ ./pipe_duplex2
Parent porc output: Who are you?
Child proc output: Thank you for your message

  • Program description

1. Child process - > parent process: data interaction through pipeline 1 pointed to by array fds1.

2. Parent process - > child process: data interaction through pipeline 2 pointed to by array fds2.

Fourth, use pipeline to realize interprocess communication in network programming

In the previous section, we learned the inter process communication method based on pipeline, and then applied it to network programming code.

4.1 echo server for saving messages

Let's extend the server-side program echo in the previous blog post_ mpserv. c. Add the following functions:

Save the strings transmitted by the echo client to a file in order

We delegate this functional task to another process. In other words, a separate process is created to read string information from the process providing services to the client. This involves the problem of inter process communication. To this end, we can use the pipes mentioned above to realize the inter process communication process. An example program is given below. This example can work with any echo client, but we will use the echo described in the previous blog post_ mpclient. c.

[prompt] server program echo_mpserv.c and client program echo_mpclient.c. See the blog link below.

Linux network programming - multi process server side (2)

  • echo_storeserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/wait.h>

#define BUF_SIZE 1024

void read_childproc(int sig);
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr;    //Server side address information variable
    struct sockaddr_in clnt_adr;    //Client address information variable
    int fds[2];                     //File descriptors at both ends of the pipeline
    socklen_t clnt_adr_sz;
    
    pid_t pid;
    struct sigaction act;
    char buf[BUF_SIZE] = {0};
    int str_len, state;
    
    if(argc!=2) {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }
    
    //Initialize the sigaction struct variable act
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, NULL);     //Register the signal processing function of SIGCHLD signal
    
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock==-1)
        error_handling("socket() error");
    
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
        error_handling("bind() error");
    
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");

    //@override - add the function code to save the received string data to a file
    pipe(fds);
    pid = fork();   //Create child process 1
    if(pid == 0)    //Subprocess 1 running area
    {
        FILE *fp = fopen("echomsg.txt", "wt");
        char msgbuf[BUF_SIZE];
        int i, len;
        
        for(i=0; i<10; i++)    //Close the file after accumulating 10 times
        {
            len = read(fds[0], msgbuf, BUF_SIZE);  //Read string data from pipe
            fwrite(msgbuf, 1, len, fp);            //Writes msgbuf buffer data to an open file
        }
        fclose(fp);
        close(fds[0]);
        close(fds[1]);
        return 1;
    }
    
    while(1)
    {
        clnt_adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        if(clnt_sock == -1)
        {
            continue;
        }
        else
            printf("New client connected from address[%s:%d], conn_id=%d\n", inet_ntoa(clnt_adr.sin_addr), 
                    ntohs(clnt_adr.sin_port), clnt_sock);

        
        pid = fork();   //Create child process 2
        if(pid == -1)
        {
            close(clnt_sock);
            continue;
        }
        else if(pid == 0)  //Subprocess 2 running area
        {
            close(serv_sock);
            while((str_len=read(clnt_sock, buf, BUF_SIZE)) != 0)
            {
                write(clnt_sock, buf, str_len);    //Receive string from client
                write(fds[1], buf, str_len);       //Write string data to pipeline
            }
            
            printf("client[%s:%d] disconnected, conn_id=%d\n", inet_ntoa(clnt_adr.sin_addr), 
                    ntohs(clnt_adr.sin_port), clnt_sock);

            close(clnt_sock);
            close(fds[0]);
            close(fds[1]);
            return 2;
        }
        else
        {
            printf("New child proc ID: %d\n", pid);
            close(clnt_sock);
        }
    }

    close(serv_sock);   //Close the listening socket on the server side
    close(fds[0]);      //Close the readout end of the pipe
    close(fds[1]);      //Close the write end of the pipe

    return 0;
}

void read_childproc(int sig)
{
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);   //Wait for the child process to exit
    printf("remove proc id: %d\n", pid);
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • Code description
  • Lines 55 and 56: Line 55 creates a pipeline, and line 56 creates a sub process responsible for saving data to a file.
  • Lines 57-72: this part of the code is the sub process running area created in line 56. The code execution area reads data from the pipe outlet fds[0] and saves it to a file. In addition, the above server does not terminate the operation, but continuously provides services to the client. Therefore, when the data accumulates in the file to a certain extent, the file is closed, and the process is completed through the for loop on line 63.
  • Line 99: in line 87, the subprocess created through the fork function will copy the file descriptor array fds of the pipeline created in line 55. Therefore, string data can be passed to the pipeline through the pipeline inlet fds[1].
  • Operation results
  • Server side: echo_storeserv.c

$ gcc echo_storeserv.c -o storeserv
[wxm@centos7 echo_tcp]$ ./storeserv 9190
New client connected from address[127.0.0.1:60534], conn_id=6
New child proc ID: 5589
New client connected from address[127.0.0.1:60536], conn_id=6
New child proc ID: 5592
remove proc id: 5586
client[127.0.0.1:60534] disconnected, conn_id=6
remove proc id: 5589
client[127.0.0.1:60536] disconnected, conn_id=6
remove proc id: 5592

  • Client 1: echo_mpclient.c

$ ./mpclient 127.0.0.1 9190
Connected...........
One
Message from server: One
Three
Message from server: Three
Five
Message from server: Five
Seven
Message from server: Seven
Nine
Message from server: Nine
Q

[wxm@centos7 echo_tcp]$

  • Client 2: echo_mpclient.c

$ ./mpclient 127.0.0.1 9190
Connected...........
Two
Message from server: Two
Four
Message from server: Four
Six
Message from server: Six
Eight
Message from server: Eight
Ten
Message from server: Ten
Q
[wxm@centos7 echo_tcp]$

  • View echomsg Txt file content

[wxm@centos7 echo_tcp]$ cat echomsg.txt
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
[wxm@centos7 echo_tcp]$

Tip observation example echo_storeserv.c, you can find that the code content in the main function is too long, which affects the reading and understanding of the code. In fact, we can try to reconstruct the code with functions as modules for some functions. If you are interested, you can try to make the code structure more compact and beautiful.

Five multi process concurrent server-side summary

We have implemented the multi process concurrent server-side model, but it is only one of the concurrent server models. If we have the following ideas:

"I want to use processes and pipes to write chat room programs to make multiple clients talk. Where should I start?"

If you want to build a server with complex functions only with processes and pipelines, programmers need to have skilled programming technology and experience. Therefore, it is not easy for beginners to apply the model extension program. I hope you don't be too rigid. The other two concurrent server-side models to be explained later are more powerful and easier to implement our ideas.

In the actual network programming development project, the multi process concurrent server-side model is rarely used, because it is not an efficient concurrent server model and is not suitable for practical application scenarios. Even if we don't use the multi process model to build the server-side in the actual development project, we still need to learn and master these contents.

Finally, I would like to share with you another experience in learning programming: "even if you only need to learn the necessary parts at the beginning, you will need to master all the contents in the end."

Tips: the other two more efficient concurrent server-side models are I/O reuse and multithreaded server-side.

Six exercises

1. What is interprocess communication? It is explained from the perspective of concept and memory.

A: conceptually, interprocess communication refers to the process of exchanging data between two processes. From the perspective of memory, it is the memory shared by two processes. Through this shared memory area, data exchange can be carried out, and this shared memory area is opened up in the kernel area of the operating system.

2. Interprocess communication requires a special IPC mechanism, which is provided by the operating system. Why do you need the help of the operating system when communicating between processes?

A: to exchange data between two processes, you need a piece of shared memory, but because the address space of each process is independent of each other, you need the help of the operating system. That is, the memory space shared by the two processes must be provided by the operating system.

3. "Pipeline" is a typical IPC technology. Please answer the following questions about pipelines.

a. A pipeline is a path for exchanging data between processes. How do I create this path? Who created it?

b. In order to complete the inter process communication, two processes need to be connected to the pipeline at the same time. How do the two processes connect to the same pipe?

c. The pipeline allows two-way communication between two processes. What should we pay attention to in two-way communication?

  • A: call the pipe function to create the pipe in the parent process (or main process). The creation subject of the actual pipeline is the operating system. The pipeline is not a resource belonging to the process, but a resource belonging to the operating system.
  • b: The pipe function returns the file descriptors at both ends of the inlet and outlet of the pipe by passing in parameters. When the fork function is called to create a child process, the two file descriptors will be copied to the child process, so the parent and child processes can access the same pipe at the same time.
  • c: When the data enters the pipeline, it becomes unowned data. Therefore, any process can read data as long as there is data flowing into the pipeline. Therefore, it is necessary to reasonably arrange the writing and reading order of data in the pipeline.

4. Write examples to review IPC technology, so that two processes exchange strings with each other three times. Of course, these two processes should have a parent-child relationship, and you can specify any string.

A: Problem Analysis: two parent-child processes need to exchange data with each other. Inter process communication can be realized through pipelines, and two-way communication between processes can be realized by creating two pipelines. Let's assume that the child process sends a message to the parent process first, and then the parent process replies to the message. In this way, it repeats three times and ends the operation.

  • pipe_procipc.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUF_SIZE 30
#define N        3

int main(int argc, char *argv[])
{
    int fds1[2], fds2[2];
    pid_t pid;
    char buf[BUF_SIZE] = {0};
    int i, len;
    
    pipe(fds1);     //Create pipe 1
    pipe(fds2);     //Create pipe 2
    
    pid = fork();   //Create child process
    if(pid == 0)    //Subprocess execution area
    {
        for(i=0; i<N; i++)
        {
            printf("Child send message: ");
            fgets(buf, BUF_SIZE, stdin);
            write(fds1[1], buf, strlen(buf));           //Write string to pipeline 1
            len = read(fds2[0], buf, BUF_SIZE);         //Read string from pipe 2
            buf[len] = '\0';                            //Add string terminator '\ 0'
            printf("Child recv message: %s\n", buf);
        }
        close(fds1[0]); close(fds1[1]);
        close(fds2[0]); close(fds2[1]);
        return 1;
    }
    else            //Parent process execution area
    {
        for(i=0; i<N; i++)
        {
            len = read(fds1[0], buf, BUF_SIZE);         //Read string from pipe 1
            buf[len] = '\0';                            //Add string terminator '\ 0'
            printf("Parent recv message: %s", buf);           
            printf("Parent resp message: ");
            fgets(buf, BUF_SIZE, stdin);
            write(fds2[1], buf, strlen(buf));           //Write string to pipeline 2
        }
    }
    close(fds1[0]); close(fds1[1]);
    close(fds2[0]); close(fds2[1]);
    return 0;
}
  • Operation results

$ gcc pipe_procipc.c -o pipe_procipc
[wxm@centos7 pipe]$ ./pipe_procipc
Child send message: Hi,I`m child proc
Parent recv message: Hi,I`m child proc
Parent resp message: Hi,I`m parent proc
Child recv message: Hi,I`m parent proc

Child send message: Nice to meet you
Parent recv message: Nice to meet you
Parent resp message: Nice to meet you, too
Child recv message: Nice to meet you, too

Child send message: Good bye!
Parent recv message: Good bye!
Parent resp message: Bye bye!
Child recv message: Bye bye!

[wxm@centos7 pipe]$

reference resources

Chapter 10 of TCP-IP network programming (Yin Shengyu) - interprocess communication

Chapter 9 of Linux C programming from foundation to practice (Cheng guogang and Zhang Yulan) - Linux Process Synchronization Mechanism - pipeline and IPC

Answers to after class exercises of TCP/IP Network Programming Part I Chapter 11 ~ 14 Yin Shengyu

Keywords: Linux

Added by en on Mon, 10 Jan 2022 13:10:21 +0200