|
|
51CTO旗下网站
|
|
移动端

TCP/IP网络编程之进程与进程间通信

进程间通信意味着两个不同进程间可以交换数据,为了完成这一点,操作系统中应提供两个进程可以同时访问的内存空间。但我们知道,进程具有完全独立的内存结构,就连通过fork函数创建的子进程也不会和父进程共享内存,因此,进程间通信只能通过其他特殊方法完成。

作者:Hu先生Linux后台开发来源:今日头条|2019-11-08 14:47

 

进程间通信基本概念

进程间通信意味着两个不同进程间可以交换数据,为了完成这一点,操作系统中应提供两个进程可以同时访问的内存空间。但我们知道,进程具有完全独立的内存结构,就连通过fork函数创建的子进程也不会和父进程共享内存,因此,进程间通信只能通过其他特殊方法完成。

基于管道实现进程间通信

图1-1表示基于管道(PIPE)的进程间通信结构模型

TCP/IP网络编程之进程与间通信

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

从图1-1可以看到,为了完成进程间通信,需要创建管道。管道并非属于进程资源,而是和套接字一样,属于操作系统资源(也就不是fork函数的复制对象)。下面介绍创建管道函数

  1. #include 
  2. int pipe (int filedes[2]);//成功时返回0,失败时返回-1 
  • filedes[0]:通过管道接收数据时使用的文件描述符,即管道出口
  • filedes[1]:通过管道传输数据时使用的文件描述符,即管道入口

以长度为2的int数组地址值作为参数调用上述函数时,数组中存有两个文件描述符,它们将被用作管道的出口和入口。父进程调用该函数时将创建管道,同时获取对应于出入口的文件描述符,此时父进程可以读写同一管道。但父进程的目的是与子进程进行数据交换,因此需要将入口和出口中的一个文件描述符传递给子进程,如何完成传递呢?答案还是调用fork函数。

  1. pipe1.c 
  2. #include <stdio.h> 
  3. #include <unistd.h> 
  4. #define BUF_SIZE 30 
  5. int main(int argc, char *argv[]) 
  6. int fds[2]; 
  7. char str[] = "Who are you?"
  8. char buf[BUF_SIZE]; 
  9. pid_t pid; 
  10. pipe(fds); 
  11. pid = fork(); 
  12. if (pid == 0) 
  13. write(fds[1], str, sizeof(str)); 
  14. else 
  15. read(fds[0], buf, BUF_SIZE); 
  16. puts(buf); 
  17. return 0; 
  • 第12行:调用pipe函数创建管道,fds数组中保存用于I/O的文件描述符
  • 第13行:接着调用fork函数,子进程将同时拥有通过12行函数调用获取的两个文件描述符。注意!复制的并非管道,而是用于管道I/O的文件描述符。至此,父子进程同时拥有I/O文件描述符
  • 第16、20行:子进程通过第16行代码向管道传递字符串,父进程通过第20行代码从管道接收字符串

编译pipe1.c并运行

  1. # gcc pipe1.c -o pipe1 
  2. # ./pipe1 
  3. Who are you? 

上述示例中的通信方法及路径如图1-2所示,重点在于,父子进程都可以访问管道的I/O路径,但子进程仅用输入路径,父进程仅用输出路径

TCP/IP网络编程之进程与间通信

图1-2 示例pipe1.c的通信路径

以上就是管道的基本原理及通信方法,应用管道时还有一部分内容需要注意,通过双向通信示例进一步说明

通过管道进行进程间双向通信

下面创建两个进程通过一个管道进行双向数据交换的示例,其通信方式如图1-3所示

TCP/IP网络编程之进程与间通信

图1-3 双向通信模型1

从图1-3可以看出,通过一个管道可以进行双向通信,但采用这种模型需格外小心,先给出示例,稍后再讨论。

pipe2.c

  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #define BUF_SIZE 30 
  4. int main(int argc, char *argv[]) 
  5. int fds[2]; 
  6. char str1[] = "Who are you?"
  7. char str2[] = "Thank you for your message"
  8. char buf[BUF_SIZE]; 
  9. pid_t pid; 
  10. pipe(fds); 
  11. pid = fork(); 
  12. if (pid == 0) 
  13. write(fds[1], str1, sizeof(str1)); 
  14. sleep(2); 
  15. read(fds[0], buf, BUF_SIZE); 
  16. printf("Child proc output: %s \n", buf); 
  17. else 
  18. read(fds[0], buf, BUF_SIZE); 
  19. printf("Parent proc output: %s \n", buf); 
  20. write(fds[1], str2, sizeof(str2)); 
  21. sleep(3); 
  22. return 0; 
  • 第17~20行:子进程运行区域,通过第17行行传输数据,通过第19行接收数据。第18行的sleep函数至关重要,这一点稍后再讨论
  • 第24~26行:父进程的运行区域,通过第24行接收数据,这是为了接收第17行子进程传输的数据。另外通过第26行传输数据,这些数据将被第19行的子进程接收
  • 第27行:父进程先终止时会弹出命令提示符,这时子进程仍然在工作,故不会产生问题。这条语句主要是为了防止子进程终止前弹出命令提示符(故可删除)

编译pipe2.c并运行

  1. # gcc pipe2.c -o pipe2 
  2. # ./pipe2 
  3. Parent proc output: Who are you? 
  4. Child proc output: Thank you for your message 

运行结果和我们设想一致,不过如果尝试将18行的代码注释后再运行,虽然这行代码只将运行时间延迟了两秒,但一旦注释便会引发错误,是什么原因呢?

向管道传递数据时,先读的进程会把数据取走。简言之,数据进入管道后成为无主数据,也就是通过read函数先读取数据的进程将得到数据,即使该进程将数据传到了管道。因此,注释第18行将产生问题,在第19行,子进程将读回自己在第17行向管道发送的数据。结果父进程调用read函数后将无限期等待数据进入管道。

从上述示例可以看到,只用一个管道进行双向通信并非易事,为了简化在进行双向通信时,既然一个管道很难完成的任务,不如就让两个管道来一起完成?因此创建两个管道,各自负责不同的数据流动即可。其过程如图1-4所示

TCP/IP网络编程之进程与间通信

图1-4 双向通信模型2

由图1-4可知,使用两个管道可以解决单单通过一个管道来进行双向通信的麻烦,下面采用上述模型来改进pipe2.c。

pipe3.c

  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3. #define BUF_SIZE 30 
  4. int main(int argc, char *argv[]) 
  5. int fds1[2], fds2[2]; 
  6. char str1[] = "Who are you?"
  7. char str2[] = "Thank you for your message"
  8. char buf[BUF_SIZE]; 
  9. pid_t pid; 
  10. pipe(fds1), pipe(fds2); 
  11. pid = fork(); 
  12. if (pid == 0) 
  13. write(fds1[1], str1, sizeof(str1)); 
  14. read(fds2[0], buf, BUF_SIZE); 
  15. printf("Child proc output: %s \n", buf); 
  16. else 
  17. read(fds1[0], buf, BUF_SIZE); 
  18. printf("Parent proc output: %s \n", buf); 
  19. write(fds2[1], str2, sizeof(str2)); 
  20. sleep(3); 
  21. return 0; 
  • 第13行:创建两个管道
  • 第17、33行:子进程可以通过数组fds1指向的管道向父进程传输数据
  • 第18、25行:父进程可以通过数组fds2指向的管道向子进程传输数据
  • 第26行:没有太大的意义,只是为了延迟父进程终止的插入的代码

编译pipe3.c并运行

  1. # gcc pipe3.c -o pipe3 
  2. # ./pipe3 
  3. Parent proc output: Who are you? 
  4. Child proc output: Thank you for your message 

运用进程间通信

上一节学习了基于管道的进程间通信方法,接下来将其运用到网络代码中。如前所述,进程间通信与创建服务端并没有直接关联,但有助于理解操作系统。

保存消息的回声服务端

扩展TCP/IP网络编程之多进程服务端(二)这一章的echo_mpserv.c,添加将回声客户端传输的字符串按序保存到文件中。我们可以将这个任务交给另外的进程,换言之,另行创建进程,从向客户端服务的进程字符串信息。当然,该过程需要创建用于接收数据的管道。

下面给出示例,该示例可以与任意回声客户端配合运行,我们将用之前介绍过的echo_mpserv.c。

echo_storeserv.c

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <string.h> 
  4. #include <unistd.h> 
  5. #include <signal.h> 
  6. #include <sys/wait.h> 
  7. #include <arpa/inet.h> 
  8. #include <sys/socket.h> 
  9. #define BUF_SIZE 100 
  10. void error_handling(char *message); 
  11. void read_childproc(int sig); 
  12. int main(int argc, char *argv[]) 
  13. int serv_sock, clnt_sock; 
  14. struct sockaddr_in serv_adr, clnt_adr; 
  15. int fds[2]; 
  16. pid_t pid; 
  17. struct sigaction act; 
  18. socklen_t adr_sz; 
  19. int str_len, state; 
  20. char buf[BUF_SIZE]; 
  21. if (argc != 2) 
  22. printf("Usage : %s <port>\n", argv[0]); 
  23. exit(1); 
  24. act.sa_handler = read_childproc; 
  25. sigemptyset(&act.sa_mask); 
  26. act.sa_flags = 0; 
  27. state = sigaction(SIGCHLD, &act, 0); 
  28. serv_sock = socket(PF_INET, SOCK_STREAM, 0); 
  29. memset(&serv_adr, 0, sizeof(serv_adr)); 
  30. serv_adr.sin_family = AF_INET; 
  31. serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 
  32. serv_adr.sin_port = htons(atoi(argv[1])); 
  33. if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 
  34. error_handling("bind() error"); 
  35. if (listen(serv_sock, 5) == -1) 
  36. error_handling("listen() error"); 
  37. pipe(fds); 
  38. pid = fork(); 
  39. if (pid == 0) 
  40. FILE *fp = fopen("echomsg.txt""wt"); 
  41. char msgbuf[BUF_SIZE]; 
  42. int i, len; 
  43. for (i = 0; i < 10; i++) 
  44. len = read(fds[0], msgbuf, BUF_SIZE); 
  45. fwrite((void *)msgbuf, 1, len, fp); 
  46. fclose(fp); 
  47. return 0; 
  48. while (1) 
  49. adr_sz = sizeof(clnt_adr); 
  50. clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 
  51. if (clnt_sock == -1) 
  52. continue
  53. else 
  54. puts("new client connected..."); 
  55. pid = fork(); 
  56. if (pid == 0) 
  57. close(serv_sock); 
  58. while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) 
  59. write(clnt_sock, buf, str_len); 
  60. write(fds[1], buf, str_len); 
  61. close(clnt_sock); 
  62. puts("client disconnected..."); 
  63. return 0; 
  64. else 
  65. close(clnt_sock); 
  66. close(serv_sock); 
  67. return 0; 
  68. void read_childproc(int sig) 
  69. pid_t pid; 
  70. int status; 
  71. pid = waitpid(-1, &status, WNOHANG); 
  72. printf("removed proc id: %d \n", pid); 
  73. void error_handling(char *message) 
  74. fputs(message, stderr); 
  75. fputc('\n', stderr); 
  76. exit(1); 
  77. }  
  • 第47、48行:第47行创建管道,第48行创建负责保存文件的进程
  • 第49~62行:第49行创建的子进程运行区域,该区域从管道出口fds[0]读取数据并保存到文件中。另外,上述服务端并不终止运行,而是不断向客户端提供服务。因此,数据在文件中累计到一定程序即关闭文件,该过程通过第55行的循环完成
  • 第80行:第73行通过fork函数创建的所有子进程将复制第47行创建的管道的文件描述符,因此,可以通过管道入口fds[1]传递字符串信息

编译echo_storeserv.c并运行

  1. # gcc echo_storeserv.c -o echo_storeserv 
  2. # ./echo_storeserv 8500 
  3. new client connected... 
  4. new client connected... 
  5. client disconnected... 
  6. removed proc id: 8647 
  7. removed proc id: 8633 
  8. client disconnected... 
  9. removed proc id: 8644 

运行结果echo_mpclient ONE:

  1. # ./echo_mpclient 127.0.0.1 8500 
  2. Hello world! 
  3. Message from server: Hello world! 
  4. Hello Amy! 
  5. Message from server: Hello Amy! 
  6. Hello Tom! 
  7. Message from server: Hello Tom! 
  8. Hello Jack! 
  9. Message from server: Hello Jack! 
  10. Hello Rose! 
  11. Message from server: Hello Rose! 

运行结果echo_mpclient TWO:

  1. # ./echo_mpclient 127.0.0.1 8500 
  2. Hello Java! 
  3. Message from server: Hello Java! 
  4. Hello Python! 
  5. Message from server: Hello Python! 
  6. Hello Golang! 
  7. Message from server: Hello Golang! 
  8. Hello Spring! 
  9. Message from server: Hello Spring! 
  10. Hello Flask! 
  11. Message from server: Hello Flask! 

打印echomsg.txt文件

  1. # cat echomsg.txt 
  2. Hello world! 
  3. Hello Amy! 
  4. Hello Java! 
  5. Hello Python! 
  6. Hello Tom! 
  7. Hello Jack! 
  8. Hello Rose! 
  9. Hello Golang! 
  10. Hello Spring! 
  11. Hello Flask! 

如上运行结果所示,启动多个客户端向服务端传输数据时,文件中累计一定数量的字符串后(共调用十次fwrite函数),可以打开echomsg.txt存入字符串。

【编辑推荐】

  1. 网络体检技术大揭秘(上篇)
  2. 网络 | 5G那些运营商不愿说的秘密
  3. IPv4稀缺威胁互联网发展
  4. 网络体检技术大揭秘(中篇)
  5. 网络体检技术大揭秘(下篇)
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

骨干网与数据中心建设案例

骨干网与数据中心建设案例

高级网工必会
共20章 | 捷哥CCIE

384人订阅学习

中间件安全防护攻略

中间件安全防护攻略

4类安全防护
共4章 | hack_man

137人订阅学习

CentOS 8 全新学习术

CentOS 8 全新学习术

CentOS 8 正式发布
共16章 | UbuntuServer

286人订阅学习

读 书 +更多

基于Project2003的项目管理

本书的上一版本《基于Project 2002的项目管理》上市以后得到了读者的欢迎,为了更好地将Project 2003新版本的应用介绍给读者,我们重新进行...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微