一 进程间通信的基本概念

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)相关推荐

  1. Linux网络编程 - 多线程服务器端的实现(1)

    引言 本来,线程在 Windows 中的应用比在 Linux 平台中的应用更广泛.但 Web 服务的发展迫使 UNIX 系列的操作系统开始重视线程.由于 Web 服务器端协议本身具有的特点,经常需要同 ...

  2. Linux网络编程 - 多进程服务器端(1)

    一 进程概念及应用 利用之前学习到的内容,我们可以构建按序向第一个客户端到第一百个客户端提供服务的服务器端.当然,第一个客户端不会抱怨服务器端,但如果每一个客户端的平均服务时间为 0.5秒,则第100 ...

  3. Linux网络编程--进程间通信(一)

    进程间通信简介(摘自<Linux网络编程>p85) AT&T 在 UNIX System V 中引入了几种新的进程通讯方式,即消息队列( MessageQueues),信号量( s ...

  4. alin的学习之路(Linux网络编程:一)(网络模型、帧格式、socket套接字、服务器端实现)

    alin的学习之路(Linux网络编程:一)(网络模型.帧格式.socket套接字.服务器端实现) 1. 协议 协议是一组规则,规定了如何发送数据.通信的双发都需要遵守该规则 2. 网络分层结构模型 ...

  5. linux网络编程常用函数详解与实例(socket--bind--listen--accept)

    常用的网络命令: netstat 命令netstat是用来显示网络的连接,路由表和接口统计等网络的信息.netstat有许多的选项我们常用的选项是 -an 用来显示详细的网络状态.至于其它的选项我们可 ...

  6. Linux网络编程基础

    2019独角兽企业重金招聘Python工程师标准>>> (一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍 客户端和服务端 网络程序和普通的程序有一个最大的 ...

  7. Linux网络编程——千峰物联网笔记

    B站视频:千峰物联网学科linux网络编程 网址:https://www.bilibili.com/video/BV1RJ411B761?p=1 目录 第一章:计算机网络概述 1.1计算机网络发展简史 ...

  8. Linux网络编程基础知识

    Linux网络编程基础知识 1. 协议的概念 1.1 什么是协议 1.2 典型协议 2 网络应用程序设计模式 2.1 C/S模式 2.2 B/S模式 2.3 优缺点 3 分层模型 3.1 OSI七层模 ...

  9. Linux网络编程——socket、bind、listen、accpet、connect、read和write

    Linux网络编程 基础理论 1.TCP/UDP/端口号 2.字节序 一.socket服务器与客户端的开发步骤 二.具体使用步骤 1.socket(创建连接协议) 2.bind(地址准备好) 3.li ...

  10. Linux网络编程入门

    (一)Linux网络编程--网络知识介绍 linux网络编程--网络知识介绍 客户端和服务端          网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. ...

最新文章

  1. ElasticSearch_查询过滤排序
  2. arm linux中添加开机启动
  3. 编程软件python下载怎么读-使用最方便的计算机编程软件,Python下载使用完美教程...
  4. 软件工程概论个人作业02(四则运算2)
  5. python3基础知识三
  6. ant之property关键字
  7. JAVA浮点数浮点数转换成人民币读法
  8. 综述:关系抽取,挑战与机遇并存!
  9. caffe 连接 matlab2016b
  10. 中国科学院计算机研究生面试,19年中科院信工所考研经验分享【初试+复试】
  11. Yolo 一小时学会基本操作
  12. Aqara首次发布会三大预测 全屋智能生活体验再次升级
  13. Comet OJ - 2019国庆欢乐赛 D1 入学考试 (简单版)
  14. 植树问题(python)
  15. 【python】控制鼠标定时移动 防止屏幕锁定 并生成可执行文件exe
  16. context是什么意思
  17. linux解压tar后缀的包,解压tar包的linux命令的详细说明
  18. 小k java_leetcode——面试题 17.14. 最小K个数 (java快速排序)
  19. 《机器学习》第四章 人工神经网络 深度学习启蒙篇
  20. iview select多选下拉 鼠标移出自动收回

热门文章

  1. win7网上邻居_CentOS7 Linux访问Win7的共享文件夹
  2. oracle中db_create_file_dest参数
  3. gis与一般计算机应用系统有哪些异同,gis概论各章练习题..doc
  4. 【jvm】8-垃圾回收
  5. Unrecognized Windows Sockets error: 10106: create解决方案
  6. 一图秒懂!“天使投资、VC、PE、A轮、B轮、C轮融资”的关系
  7. cordova不是内部或外部命令的解决方案
  8. 100%概率与任意好友获取QQ幸运字符的方法
  9. 实现拖拉机发牌程序——界面版python
  10. leetcode之动态规划刷题总结1(Java)