Linux网络编程 - 在服务器端运用进程间通信之管道(pipe)

一 进程间通信的基本概念

1.1 对进程间通信的基本理解

进程间通信(Inter Process Communication,简称 IPC)

进程间通信意味着两个不同进程间可以交换数据,为了实现这一点,操作系统内核需要提供两个进程可以同时访问的内存空间,即在内核中开辟一块缓冲区。整个数据交换过程如下图所示:

图1  进程间通信

从上图 1-1 可以看出,只要有两个进程可以同时访问的内存空间,就可以通过此空间交换数据。但我们知道,进程具有完全独立的内存结构,就连通过 fork 函数创建的子进程也不会与其父进程共享内存空间。因此,进程间通信只能在操作系统内核区开辟这种共享内存缓冲区。

拓展》关于进程间通信的机制请参见下面博文链接

Linux进程之进程间通信

二 Linux 的管道(pipe)

2.1 管道的基本概念

管道(pipe) 也称为匿名管道,是Linux下最常见的进程间通信方式之一,它是在两个进程之间实现一个数据流通的通道。

基于管道的进程间通信结构模型如下图2所示。

图2  基于管道的进程间通信模型

         为了完成进程间通信,需要创建管道。管道并非属于进程的资源,而是和套接字一样,属于操作系统(也就不是 fork 函数的复制对象)。所以,两个进程通过操作系统内核提供的内存空间进行通信。

2.2 管道的特点

Linux 的管道具有以下特点:

  • 管道没有名字,所以也称为匿名管道。
  • 管道是半双工的通信方式,数据只能向一个方向流动;需要双向通信时,需要建立起两个管道。(缺点1
  • 管道只能用在父子进程或兄弟进程之间(即具有亲缘关系的进程)。(缺点2
  • 管道单独构成一种独立的文件系统,管道对于管道两端的进程而言,就是一个文件,但它不是普通文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。
  • 数据的读出和写入:一个进程向管道中写入的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
  • 管道的缓冲区是有限的(管道只存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
  • 管道中所传递的数据是无格式的字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式。例如,多少字节算作一个消息(或命令、记录等)。

2.3 管道的实现方法

        当一个进程创建一个管道时,Linux 系统内核为使用该管道准备了两个文件描述符:一个用于管道的输入(即进程写操作),也就是在管道中写入数据;另一个用于管道的输出(即进程读操作),也就是从管道中读出数据,然后对这两个文件描述符调用正常的系统调用(write、read函数),内核利用这种抽象机制实现了管道这一特殊操作。如下图 3 所示。

图3  管道的结构
  •  管道结构的说明

fd0:从管道中读出数据时使用的文件描述符,即管道出口,用于进程的读操作(read),称为读管道文件描述符。

fd1:向管道中写入数据时使用的文件描述符,即管道入口,用于进程的写操作(write),称为写管道文件描述符。

        如果一个管道只与一个进程相联系,可以实现进程自身内部的通信,这个一般用在进程内线程间的通信(自己遇到过)。

        通常情况下,一个创建管道的进程接着就会创建子进程,由于子进程是复制父进程所有资源创建出的进程,因此子进程将从父进程那里继承到读写管道的文件描述符,这样父子进程间的通信管道就建立起来了。如下图 4 所示。

图4  父进程与子进程之间的管道

《父子进程管道半双工通信说明》

  • 父进程的 fd[0] = 子进程的 f[0],即表示这两个文件描述符都是标识同一个管道的出口端。
  • 父进程的 fd[1] = 子进程的 f[1],即表示这两个文件描述符都是标识同一个管道的入口端。

《父子进程数据传输方向》

父进程 —> 子进程的数据传输方向:父进程的 fd[1] —> 管道 —> 子进程的 fd[0]

子进程 —> 父进程的数据传输方向:子进程的 fd[1] —> 管道 —> 父进程的 fd[0]

        例如,数据从父进程传输给子进程时,则父进程关闭读管道的文件描述符 fd[0],子进程关闭写管道的文件描述符 fd[1],这样就建立了从父进程到子进程的通信管道,如下图 5 所示。

图5  从父进程到子进程的管道

 2.4 管道的读写操作规则

        在建立了一个管道之后即可通过相应的文件 I/O 操作函数(例如 read、write 等)来读写管道,以完成数据的传递过程。

        需要注意的是由于管道的一端已经关闭,在进行相应的操作时,需要注意以下三个要点:

  • 如果从一个写描述符(fd[1])关闭的管道中读取数据,当读完所有的数据后,read 函数返回0,表明已到达文件末尾。严格地说,只有当没有数据继续写入后,才可以说到达了完末尾,所以应该分清楚到底是暂时没有数据写入,还是已经到达文件末尾,如果是前者,读进程应该等待。若为多进程写、单进程读的情况将更加复杂。
  • 如果向一个读描述符(fd[0])关闭的管道中写数据,就会产生 SIGPIPE 信号。不管是否忽略这个信号,还是处理它,write 函数都将返回 -1。
  • 常数 PIPE_BUF 规定了内核中管道缓冲的大小,所以在写管道中要注意一点。一次向管道中写入 PIPE_BUF 或更少的字节数据时,不会和其他进程写入的内容交错;反之,当存在多个写管道的进程时,向其中写入超过 PIPE_BUF 个字节数据时,将会产生内容交错现象,即覆盖了管道中的已有数据。

三 管道的操作

3.1 管道的创建

Linux 内核提供了函数 pipe 用于创建一个管道,对其标准调用格式说明如下:

  • pipe() — 创建一个匿名管道。
#include <unistd.h>

int pipe(int pipefd[2]);

/*参数说明
pipefd[2]: 长度为2的文件描述符整型数组
pipefd[0]: 是管道读出端的文件描述符,也就是说pipefd[0]只能为读操作打开。
pipefd[1]: 是管道写入端的文件描述符,也就是说pipefd[1]只能为写操作打开。
*/

//返回值: 成功时返回0,失败时返回-1。

【编程实例】使用 pipe 函数创建管道。在一个进程中使用管道的示例。

  • 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};  //写缓冲区
    char read_buf[BUF_SIZE] = {0};   //读缓冲区
    
    if(pipe(fd) < 0)                 //创建管道
    {
        printf("create pipe error!\n");
        exit(1);
    }
    printf("write data to pipe: ");
    fgets(write_buf, BUF_SIZE, stdin);  //从控制台输入一行字符串
    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(fd[1]);                    //关闭管道的写入端文件描述符
    return 0;
}
  • 运行结果

$ 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

注意》在关闭一个管道时,必须对管道的两端都执行 close 操作,也就是说要对管道的两个文件描述符都进行 close 操作。

3.2 通过管道实现进程间通信

        当父进程调用 pipe 函数时将创建管道,同时获取对应于管道出入口两端的文件描述符,此时父进程可以读写同一管道,也就是本示例程序中那样。但父进程的目的通常是与子进程进行数据交换,因此需要将管道入口或出口中的其中一个文件描述符传递给子进程。如何传递呢?答案就是调用 fork 函数。

  • 在父子进程中使用管道的详细步骤

1、在父进程中调用 pipe 函数创建一个管道。

2、在父进程中调用 fork 函数创建一个子进程。

3、在父进程中关闭不使用的管道一端的文件描述符,然后调用对应的写操作函数,例如 write,将对应的数据写入管道。

4、在子进程中关闭不使用的管道一端的文件描述符,然后调用对应的读操作函数,例如 read,将对应的数据从管道中读出。

5、在父子进程中,调用 close 函数,关闭管道的文件描述符。

【编程实例】在父子进程中使用管道。在父进程中创建一个管道,并调用 fork 函数创建一个子进程,父进程将一行字符串数据写入管道,在子进程中,从管道读出这个字符串并打印出来。

  • 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){                      //创建一个管道,两个文件描述符存入fds数组中
        printf("pipe() error!\n");
        exit(1);
    }
    if((pid = fork()) < 0){                 //创建一个子进程   
        printf("fork() error!\n");
        exit(1);
    }
    else if(pid > 0)                        //父进程执行区域
    {
        printf("Parent Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);
        close(fds[0]);                      //关闭父进程的管道读出端描述符
        fgets(buf, BUF_SIZE, stdin);        //终端输入一行字符串数据 
        write(fds[1], buf, strlen(buf));    //向管道写入数据
    }
    else                                    //子进程执行区域
    {
        printf("Child Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);
        close(fds[1]);                      //关闭子进程的管道写入端描述符
        len = read(fds[0], buf, BUF_SIZE);  //从管道中读出字符串数据
        buf[len] = '\0';
        printf("%s", buf);
        close(fds[0]);                      //关闭子进程的管道读出端描述符
    }
    
    close(fds[1]);                          //关闭父进程的管道写入端描述符
    return 0;
}
  • 运行结果

$ 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?

代码说明

  • 第14行:在父进程中调用 pipe 函数创建管道,fds 数组中保存用于读写 I/O 的文件描述符。
  • 第18行:接着调用 fork 函数。子进程将同时拥有通过第14行 pipe 函数调用获取的2个文件描述符,从上面的运行结果可以验证这一点。注意!复制的并非管道,而是用于管道 I/O 的文件描述符。至此,父子进程同时拥有管道 I/O 的文件描述符。
  • 第27、33行:父进程通过第27行代码,向管道写入字符串;子进程通过第33行代码,从管道接收字符串。
  • 第36、39行:第36行代码,子进程结束运行前,关闭管道的读出端文件描述符;第39行代码,父进程(也是主进程)结束运行前,关闭管道的写入端文件描述符。
  • 在兄弟进程中使用管道

        在兄弟进程中使用管道进行数据通信的方法和在父子进程中类似,只是将对管道进行操作的两个进程更换为兄弟进程即可,在父进程中则关闭该管道的 I/O 文件描述符。

编程实例】值兄弟进程中使用管道的应用实例。首先在主进程(也就是父进程)中创建一个管道和两个子进程,然后在第1个子进程中将一个字符串通过管道发送给第2个子进程,第2个子进程从管道中读出数据,然后将该数据输出到屏幕上。

  • 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){                      //创建一个管道,两个文件描述符存入fds数组中
        printf("pipe() error!\n");
        exit(1);
    }
    printf("Parent Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);

    if((pid1 = fork()) < 0){                 //创建子进程1
        printf("fork() error!\n");
        exit(1);
    }
    else if(pid1 == 0)                       //子进程1执行区域
    {
        printf("Child1 Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);
        close(fds[0]);                       //关闭子进程1的管道读出端描述符
        fgets(buf, BUF_SIZE, stdin);         //从终端中输入字符串数据
        write(fds[1], buf, strlen(buf));     //向管道写入数据
        close(fds[1]);                       //关闭子进程1的管道写入端描述符
        exit(1);
    }
    
    if((pid2 = fork()) < 0){                 //创建子进程2
        printf("fork() error!\n");
        exit(1);
    }
    else if(pid2 == 0)                       //子进程2执行区域
    {
        printf("Child2 Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);
        close(fds[1]);                      //关闭子进程2的管道写入端描述符
        len = read(fds[0], buf, BUF_SIZE);  //从管道中读出字符串数据
        buf[len] = '\0';
        printf("%s", buf);
        close(fds[0]);                      //关闭子进程2的管道读出端描述符
        exit(2);
    }
    else                                    //父进程执行区域
    {
        int proc_num = 2;                   //子进程个数为2
        while(proc_num)
        {
            while((pid = waitpid(-1, &status, WNOHANG)) == 0)  //等待子进程结束
            {
                continue;
            }
            if(pid == pid1){                                   //结束的是子进程1
                printf("Child1 proc eixt, pid=%d\n", pid1);
                proc_num--;
            }
            else if(pid == pid2){                              //结束的是子进程2
                printf("Child2 proc eixt, pid=%d\n", pid2);
                proc_num--;
            }
            if(WIFEXITED(status))                              //获取子进程退出时的状态返回值
                printf("Child proc send %d\n", WEXITSTATUS(status));
        }
    }

    close(fds[0]);                      //关闭父进程的管道读出端描述符
    close(fds[1]);                      //关闭父进程的管道写入端描述符
    return 0;
}
  • 运行结果

$ 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

代码说明

  • 第54、58、62行:在父进程中调用 waitpid 函数,等待子进程的终止,如果没有终止的子进程也不会进入阻塞状态,而是返回0。当子进程1结束运行时,函数返回该子进程的进程ID,执行第58行的代码;同理,当子进程2结束运行时,函数返回该子进程的进程ID,执行第62行的代码。

3.3 通过管道实现进程间双向通信

下面创建2个进程和1个管道进行双向数据交换的示例,其通信方式如下图6所示。

图6  管道双向通信模型1

 从图6可以看出,通过一个管道可以进行双向数据通信。但采用这种模型时需格外注意。先给出示例,稍后再分析讨论。

  • 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)  //子进程区域
    {
        write(fds[1], str1, sizeof(str1));       //向管道写入字符串str1
        sleep(2);                                //让子进程暂停2秒
        read(fds[0], buf, BUF_SIZE);             //从管道读出数据
        printf("Child proc output: %s\n", buf);  //打印从管道读出的字符串
    }
    else          //父进程区域
    {
        read(fds[0], buf, BUF_SIZE);              //从管道读出数据
        printf("Parent porc output: %s\n", buf);
        write(fds[1], str2, sizeof(str2));        //向管道写入字符串str2
        sleep(3);                                 //让父进程暂停3秒
    }
    return 0;
}
  • 运行结果

$ gcc pipe_duplex.c -o pipe_duplex

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

        运行结果和我们预想的一样:子进程向管道中写入字符串 str1,父进程从管道中读出该字符串;父进程向管道中写入字符串 str2,子进程从管道中读出该字符串。如果我们将第 27 行的代码注释掉,运行结果会是怎样呢?

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

从上面的运行结果和进程状态可以看出,进程 pipe_duplex 陷入了 死锁状态(<defunct>),产生的原因是什么呢?

向管道中传递数据时,先读的进程会把管道中的数据取走。

        数据进入管道后成为无主数据。也就是通过 read 函数先读取数据的进程将得到数据,即使该进程将数据传到了管道。因此,注释掉第 27 行代码将产生问题。在第 28 行,子进程将读回自己在第 26 行向管道发送的数据。结果,父进程调用 read 函数后将无限期等待数据进入管道,导致进程陷入死锁。

        从上述示例中可以看到,只用一个管道进行进程间的双向通信并非易事。为了实现这一点,程序需要预测并控制运行流程,这在每种系统中都不同,可以视为不可能完成的任务。既然如此,该如何进行双向通信呢?

创建两个管道。

        非常简单,一个管道无法完成双向通信任务,因此需要创建两个管道,各自负责不同的数据流动方向即可。其过程如下图 7 所示。

图7  双向通信模型2

         由上图 7 可知,使用两个管道可以避免程序流程的不可预测或不可控制因素。下面采用上述模型改进 pipe_duplex.c 程序。

  • 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);   //创建管道1
    if(ret < 0)
    {
        perror("pipe() error");
        exit(1);
    }
    
    ret = pipe(fds2);   //创建管道2
    if(ret < 0)
    {
        perror("pipe() error");
        exit(1);
    }
    
    pid = fork();
    if(pid == 0)  //子进程区域
    {
        write(fds1[1], str1, sizeof(str1));      //向管道1写入字符串str1
        read(fds2[0], buf, BUF_SIZE);            //从管道2读出数据
        printf("Child proc output: %s\n", buf);  //打印从管道读出的字符串
    }
    else          //父进程区域
    {
        read(fds1[0], buf, BUF_SIZE);             //从管道1读出数据
        printf("Parent porc output: %s\n", buf);
        write(fds2[1], str2, sizeof(str2));       //向管道2写入字符串str2
        sleep(3);                                 //让父进程暂停3秒
    }
    return 0;
}
  • 运行结果

$ gcc pipe_duplex2.c -o pipe_duplex2

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

  • 程序说明

1、子进程 ——> 父进程:通过数组 fds1 指向的管道1进行数据交互。

2、父进程 ——> 子进程:通过数组 fds2 指向的管道2进行数据交互。

四 在网络编程中运用管道实现进程间通信

上一节我们学习了基于管道的进程间通信方法,接下来将其运用到网络编程代码中。

4.1 保存消息的回声服务器端

下面我们扩展上一篇博文中的服务器端程序 echo_mpserv.c,添加如下功能:

将回声客户端传输的字符串按序保存到文件中。

        我们将这个功能任务委托给另外的进程。换言之,另行创建进程,从向客户端提供服务的进程读取字符串信息。这就涉及到进程间通信的问题。为此,我们可以使用上面讲过的管道来实现进程间通信过程。下面给出示例程序。该示例可以与任意回声客户端配合运行,但我们将使用前一篇博文中介绍过的 echo_mpclient.c

提示】服务器端程序 echo_mpserv.c 和 客户端程序 echo_mpclient.c,请参见下面的博文链接获取。

Linux网络编程 - 多进程服务器端(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;    //服务器端地址信息变量
    struct sockaddr_in clnt_adr;    //客户端地址信息变量
    int fds[2];                     //管道两端的文件描述符
    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);
    }
    
    //初始化sigaction结构体变量act
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, NULL);     //注册SIGCHLD信号的信号处理函数
    
    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-添加将接收到的字符串数据保存到文件中的功能代码
    pipe(fds);
    pid = fork();   //创建子进程1
    if(pid == 0)    //子进程1运行区域
    {
        FILE *fp = fopen("echomsg.txt", "wt");
        char msgbuf[BUF_SIZE];
        int i, len;
        
        for(i=0; i<10; i++)    //累计10次后关闭文件
        {
            len = read(fds[0], msgbuf, BUF_SIZE);  //从管道读出字符串数据
            fwrite(msgbuf, 1, len, fp);            //将msgbuf缓冲区数据写入打开的文件中
        }
        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();   //创建子进程2
        if(pid == -1)
        {
            close(clnt_sock);
            continue;
        }
        else if(pid == 0)  //子进程2运行区域
        {
            close(serv_sock);
            while((str_len=read(clnt_sock, buf, BUF_SIZE)) != 0)
            {
                write(clnt_sock, buf, str_len);    //接收客户端发来的字符串
                write(fds[1], buf, str_len);       //向管道写入字符串数据
            }
            
            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(fds[0]);      //关闭管道的读出端
    close(fds[1]);      //关闭管道的写入端

    return 0;
}

void read_childproc(int sig)
{
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);   //等待子进程退出
    printf("remove proc id: %d\n", pid);
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 代码说明
  • 第55、56行:第55行创建管道,第56行创建负责保存数据到文件中的子进程。
  • 第57~72行:这部分代码是第56行创建的子进程运行区域。该代码执行区域从管道出口端 fds[0] 读取数据并保存到文件中。另外,上述服务器端并不终止运行,而是不断向客户端提供服务。因此,数据在文件中累计到一定程度即关闭文件,该过程通过第63行的 for 循环完成。
  • 第99行:第87行通过 fork 函数创建的子进程将复制第55行创建的管道的文件描述符数组 fds。因此,可以通过管道入口端 fds[1] 向管道传递字符串数据。
  • 运行结果
  • 服务器端: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

  • 客户端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]$

  • 客户端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]$

  • 查看 echomsg.txt 文件内容

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

提示》观察示例 echo_storeserv.c 后,可以发现在 main 函数中,代码内容太长,有点影响代码阅读和理解。我们其实可以尝试针对一部分功能以函数为模块单位重构代码,有兴趣的话,可以试一试,让代码结构更加紧凑、美观。

五 多进程并发服务器端总结

        前面我们已经实现了多进程并发服务器端模型,但它只是并发服务器模型中的其中之一。如果我们有如下的想法:

我想利用进程和管道编写聊天室程序,使多个客户端进行对话,应该从哪着手呢?

        若想仅用进程和管道构建具有复杂功能的服务器端,程序员需要具备熟练的编程技术和经验。因此,初学者应用该模型扩展程序并非易事,希望大家不要过于拘泥。以后要说明的另外两种并发服务器端模型在功能上更加强大,同时更容易实现我们的想法。

        在实际网络编程开发项目中,几乎不会用到多进程并发服务器端模型,因为它并不是一种高效的并发服务器模型,不适合实际应用场景。即使我们在实际开发项目中不会利用多进程模型构建服务器端,但这些内容我们还是有必要学习和掌握的。

        最后跟大家分享一句他人的一条学习编程经验之谈:“即使开始时只需学习必要部分,但最后也会需要掌握所有的内容。

提示》另外两种比较高效的并发服务器端模型为:I/O 复用、多线程服务器端。

六 习题

1、什么是进程间通信?分别从概念上和内存的角度进行说明。

:从概念上讲,进程间通信是指两个进程之间交换数据的过程。从内存的角度上讲,就是两个进程共享的内存,通过这个共享的内存区域,可以进行数据交换,而这个共享的内存区域是在操作系统内核区中开辟的。

2、进程间通信需要特殊的IPC机制,这是由操作系统提供的。进程间通信时为何需要操作系统的帮助?

:两个进程之间要想交换数据,需要一块共享的内存,但由于每个进程的地址空间都是相互独立的,因此需要操作系统的帮助。也就是说,两个进程共享的内存空间必须由操作系统来提供。

3、“管道”是典型的IPC技术。关于管道,请回答如下问题。

a. 管道是进程间交换数据的路径。如何创建该路径? 由谁创建?

b. 为了完成进程间通信,2个进程需同时连接管道。那2个进程如何连接到同一管道?

c. 管道允许进行2个进程间的双向通信。双向通信中需要注意哪些内容?

  • a:在父进程(或主进程)中调用 pipe 函数创建管道。实际管道的创建主体是操作系统,管道不是属于进程的资源,而是属于操作系统的资源。
  • b:pipe 函数通过传入参数返回管道的出入口两端的文件描述符。当调用 fork 函数创建子进程时,这两个文件描述符会被复制到子进程中,因此,父子进程可以同时访问同一管道。
  • c:数据进入管道后就变成了无主数据。因此,只要有数据流入管道,任何进程都可以读取数据。因此,要合理安排管道中数据的写入和读出顺序。

4、编写示例复习IPC技术,使2个进程相互交换3次字符串。当然,这两个进程应具有父子关系,各位可指定任意字符串。

:问题剖析:两个父子进程要互相交换数据,可以通过管道方式实现进程间通信,而通过创建两个管道可以实现进程间的双向通信。我们假设是子进程先向父进程发送消息,然后父进程回复消息,如此往复3次后结束运行。

  • 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);     //创建管道1
    pipe(fds2);     //创建管道2
    
    pid = fork();   //创建子进程
    if(pid == 0)    //子进程执行区域
    {
        for(i=0; i<N; i++)
        {
            printf("Child send message: ");
            fgets(buf, BUF_SIZE, stdin);
            write(fds1[1], buf, strlen(buf));           //向管道1中写入字符串
            len = read(fds2[0], buf, BUF_SIZE);         //从管道2中读出字符串
            buf[len] = '\0';                            //添加字符串结束符'\0'
            printf("Child recv message: %s\n", buf);
        }
        close(fds1[0]); close(fds1[1]);
        close(fds2[0]); close(fds2[1]);
        return 1;
    }
    else            //父进程执行区域
    {
        for(i=0; i<N; i++)
        {
            len = read(fds1[0], buf, BUF_SIZE);         //从管道1中读出字符串
            buf[len] = '\0';                            //添加字符串结束符'\0'
            printf("Parent recv message: %s", buf);           
            printf("Parent resp message: ");
            fgets(buf, BUF_SIZE, stdin);
            write(fds2[1], buf, strlen(buf));           //向管道2中写入字符串
        }
    }
    close(fds1[0]); close(fds1[1]);
    close(fds2[0]); close(fds2[1]);
    return 0;
}
  • 运行结果

$ 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]$

参考

《TCP-IP网络编程(尹圣雨)》第11章 - 进程间通信

《Linux C编程从基础到实践(程国钢、张玉兰)》第9章 - Linux的进程同步机制——管道和IPC

《TCP/IP网络编程》课后练习答案第一部分11~14章 尹圣雨

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值