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

2024-05-14 17:08

本文主要是介绍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 100int 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 100int 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 100int 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){                 //创建子进程1printf("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){                 //创建子进程2printf("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;                   //子进程个数为2while(proc_num){while((pid = waitpid(-1, &status, WNOHANG)) == 0)  //等待子进程结束{continue;}if(pid == pid1){                                   //结束的是子进程1printf("Child1 proc eixt, pid=%d\n", pid1);proc_num--;}else if(pid == pid2){                              //结束的是子进程2printf("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 100int 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));       //向管道写入字符串str1sleep(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));        //向管道写入字符串str2sleep(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 100int 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);   //创建管道1if(ret < 0){perror("pipe() error");exit(1);}ret = pipe(fds2);   //创建管道2if(ret < 0){perror("pipe() error");exit(1);}pid = fork();if(pid == 0)  //子进程区域{write(fds1[1], str1, sizeof(str1));      //向管道1写入字符串str1read(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写入字符串str2sleep(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 1024void 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结构体变量actact.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();   //创建子进程1if(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;}elseprintf("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();   //创建子进程2if(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        3int main(int argc, char *argv[])
{int fds1[2], fds2[2];pid_t pid;char buf[BUF_SIZE] = {0};int i, len;pipe(fds1);     //创建管道1pipe(fds2);     //创建管道2pid = 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章 尹圣雨

这篇关于Linux网络编程 - 在服务器端运用进程间通信之管道(pipe)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/989342

相关文章

Linux磁盘分区、格式化和挂载方式

《Linux磁盘分区、格式化和挂载方式》本文详细介绍了Linux系统中磁盘分区、格式化和挂载的基本操作步骤和命令,包括MBR和GPT分区表的区别、fdisk和gdisk命令的使用、常见的文件系统格式以... 目录一、磁盘分区表分类二、fdisk命令创建分区1、交互式的命令2、分区主分区3、创建扩展分区,然后

Linux中chmod权限设置方式

《Linux中chmod权限设置方式》本文介绍了Linux系统中文件和目录权限的设置方法,包括chmod、chown和chgrp命令的使用,以及权限模式和符号模式的详细说明,通过这些命令,用户可以灵活... 目录设置基本权限命令:chmod1、权限介绍2、chmod命令常见用法和示例3、文件权限详解4、ch

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

Linux使用nohup命令在后台运行脚本

《Linux使用nohup命令在后台运行脚本》在Linux或类Unix系统中,后台运行脚本是一项非常实用的技能,尤其适用于需要长时间运行的任务或服务,本文我们来看看如何使用nohup命令在后台... 目录nohup 命令简介基本用法输出重定向& 符号的作用后台进程的特点注意事项实际应用场景长时间运行的任务服

什么是cron? Linux系统下Cron定时任务使用指南

《什么是cron?Linux系统下Cron定时任务使用指南》在日常的Linux系统管理和维护中,定时执行任务是非常常见的需求,你可能需要每天执行备份任务、清理系统日志或运行特定的脚本,而不想每天... 在管理 linux 服务器的过程中,总有一些任务需要我们定期或重复执行。就比如备份任务,通常会选在服务器资

Linux限制ip访问的解决方案

《Linux限制ip访问的解决方案》为了修复安全扫描中发现的漏洞,我们需要对某些服务设置访问限制,具体来说,就是要确保只有指定的内部IP地址能够访问这些服务,所以本文给大家介绍了Linux限制ip访问... 目录背景:解决方案:使用Firewalld防火墙规则验证方法深度了解防火墙逻辑应用场景与扩展背景:

Linux下MySQL8.0.26安装教程

《Linux下MySQL8.0.26安装教程》文章详细介绍了如何在Linux系统上安装和配置MySQL,包括下载、解压、安装依赖、启动服务、获取默认密码、设置密码、支持远程登录以及创建表,感兴趣的朋友... 目录1.找到官网下载位置1.访问mysql存档2.下载社区版3.百度网盘中2.linux安装配置1.

C#如何优雅地取消进程的执行之Cancellation详解

《C#如何优雅地取消进程的执行之Cancellation详解》本文介绍了.NET框架中的取消协作模型,包括CancellationToken的使用、取消请求的发送和接收、以及如何处理取消事件... 目录概述与取消线程相关的类型代码举例操作取消vs对象取消监听并响应取消请求轮询监听通过回调注册进行监听使用Wa

C#反射编程之GetConstructor()方法解读

《C#反射编程之GetConstructor()方法解读》C#中Type类的GetConstructor()方法用于获取指定类型的构造函数,该方法有多个重载版本,可以根据不同的参数获取不同特性的构造函... 目录C# GetConstructor()方法有4个重载以GetConstructor(Type[]

Linux使用粘滞位 (t-bit)共享文件的方法教程

《Linux使用粘滞位(t-bit)共享文件的方法教程》在Linux系统中,共享文件是日常管理和协作中的常见任务,而粘滞位(StickyBit或t-bit)是实现共享目录安全性的重要工具之一,本文将... 目录文件共享的常见场景基础概念linux 文件权限粘滞位 (Sticky Bit)设置共享目录并配置粘