本文主要是介绍Linux网络编程 - 在服务器端运用进程间通信之管道(pipe),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一 进程间通信的基本概念
1.1 对进程间通信的基本理解
进程间通信(Inter Process Communication,简称 IPC)
进程间通信意味着两个不同进程间可以交换数据,为了实现这一点,操作系统内核需要提供两个进程可以同时访问的内存空间,即在内核中开辟一块缓冲区。整个数据交换过程如下图所示:
从上图 1-1 可以看出,只要有两个进程可以同时访问的内存空间,就可以通过此空间交换数据。但我们知道,进程具有完全独立的内存结构,就连通过 fork 函数创建的子进程也不会与其父进程共享内存空间。因此,进程间通信只能在操作系统内核区开辟这种共享内存缓冲区。
《拓展》关于进程间通信的机制请参见下面博文链接
Linux进程之进程间通信
二 Linux 的管道(pipe)
2.1 管道的基本概念
管道(pipe) 也称为匿名管道,是Linux下最常见的进程间通信方式之一,它是在两个进程之间实现一个数据流通的通道。
基于管道的进程间通信结构模型如下图2所示。
为了完成进程间通信,需要创建管道。管道并非属于进程的资源,而是和套接字一样,属于操作系统(也就不是 fork 函数的复制对象)。所以,两个进程通过操作系统内核提供的内存空间进行通信。
2.2 管道的特点
Linux 的管道具有以下特点:
- 管道没有名字,所以也称为匿名管道。
- 管道是半双工的通信方式,数据只能向一个方向流动;需要双向通信时,需要建立起两个管道。(缺点1)
- 管道只能用在父子进程或兄弟进程之间(即具有亲缘关系的进程)。(缺点2)
- 管道单独构成一种独立的文件系统,管道对于管道两端的进程而言,就是一个文件,但它不是普通文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。
- 数据的读出和写入:一个进程向管道中写入的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
- 管道的缓冲区是有限的(管道只存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
- 管道中所传递的数据是无格式的字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式。例如,多少字节算作一个消息(或命令、记录等)。
2.3 管道的实现方法
当一个进程创建一个管道时,Linux 系统内核为使用该管道准备了两个文件描述符:一个用于管道的输入(即进程写操作),也就是在管道中写入数据;另一个用于管道的输出(即进程读操作),也就是从管道中读出数据,然后对这两个文件描述符调用正常的系统调用(write、read函数),内核利用这种抽象机制实现了管道这一特殊操作。如下图 3 所示。
- 管道结构的说明
fd0:从管道中读出数据时使用的文件描述符,即管道出口,用于进程的读操作(read),称为读管道文件描述符。
fd1:向管道中写入数据时使用的文件描述符,即管道入口,用于进程的写操作(write),称为写管道文件描述符。
如果一个管道只与一个进程相联系,可以实现进程自身内部的通信,这个一般用在进程内线程间的通信(自己遇到过)。
通常情况下,一个创建管道的进程接着就会创建子进程,由于子进程是复制父进程所有资源创建出的进程,因此子进程将从父进程那里继承到读写管道的文件描述符,这样父子进程间的通信管道就建立起来了。如下图 4 所示。
《父子进程管道半双工通信说明》
- 父进程的 fd[0] = 子进程的 f[0],即表示这两个文件描述符都是标识同一个管道的出口端。
- 父进程的 fd[1] = 子进程的 f[1],即表示这两个文件描述符都是标识同一个管道的入口端。
《父子进程数据传输方向》
父进程 —> 子进程的数据传输方向:父进程的 fd[1] —> 管道 —> 子进程的 fd[0]
子进程 —> 父进程的数据传输方向:子进程的 fd[1] —> 管道 —> 父进程的 fd[0]
例如,数据从父进程传输给子进程时,则父进程关闭读管道的文件描述符 fd[0],子进程关闭写管道的文件描述符 fd[1],这样就建立了从父进程到子进程的通信管道,如下图 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可以看出,通过一个管道可以进行双向数据通信。但采用这种模型时需格外注意。先给出示例,稍后再分析讨论。
- 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 可知,使用两个管道可以避免程序流程的不可预测或不可控制因素。下面采用上述模型改进 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 procChild 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, tooChild 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)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!