TCP/IP网络编程_第11章进程间通信

在这里插入图片描述

11.1 进程间通信的基本概念

进程间通信(Inter Process Communication) 意味着这个不同进程间可以交换数据, 为了完成这一点, 操作系统中应提供两个进程可以同时访问内存空间.

对进程间通信的基本理解

理解好进程间通信并没有想象中那么难, 进程A和进程B之间的如下谈话内容就是一种进程间通信规则.
在这里插入图片描述
也就是说, 进程A通过bread将自己的状态通知给了进程B, 进程B通过变量bread 听到了进程进程A的话. 因此, 只要有两个进程可以同时访问的内存空间, 就可以通过此空间交换数据. 但正如第10章所讲, 进程具有完全独立的内存结构. 就连通过 fork 函数创建的子进程也不会与父进程共享内存空间. 因此, 进程间通信只能通过其他特殊方法完成.

各位应该已经明白进程间通信及其无法简单实现的原因, 下面正式介绍进程间通信方法.

通过管道实现进程间通信

图11-1 表示基于管道(PIPE)的进程间通信结构模型.
在这里插入图片描述
从图11-1 中可以看到, 为了完成进程间通信, 需要创建管道. 管道并非属于进程的资源, 而是和套接字一样, 属于操作系统(也就不是fork函数的复制对象). 所以, 两个进程通过操作系统提供的内存空间进行通信. 下面介绍创建管道的函数.
在这里插入图片描述
以长度为2的int 数组地址值作为参数调用的上述函数时, 数组中存有两个文件描述符, 它们将被用作管道的出口和入口. 父进程调用该函数时将创建管道, 同时获取对应出入口的文件描述符, 此时父进程可以读写同一管道(相信大家也做过这样的实验). 但父进程的目的是与子进程进行数据交换, 因此需要将入口或出口中的1个文件描述符传递给子进程. 如何完成传递呢? 答案就是调用 fork 函数. 通过下列示例进行演示.

#include <stdio.h>
#include <unistd.h>

#define BUF_SIZE 30

int main(int argc, char *argv[])
{
    int fds[2];
    char str[] = "Who are you?";
    char buf[BUF_SIZE];
    pid_t pid;

    
    pipe(fds); /* 第14行: 调用pipe函数创建管道, fds数组中保存用于I/O的文件描述符 */
    pid = fork(); /* 第15行: 接着调用fork函数. 子进程将同时拥有通过第14行函数获取的2个文件描述符. 注意! 复制的并非管道, 而是用于管道I/O的文件描述符. 至此, 父子进程同时拥有I/O文件描述符 */
    if (pid == 0)
    {
        write(fds[1], str, sizeof(str)); /* 第18 22行: 子进程通过第18行代码向管道传递字符串. 父进程通过第22行代码从管道接收字符串 */
    }
    else
    {
        read(fds[0], buf, BUF_SIZE);
        puts(buf);
    }
    
    return 0;
}

运行结果;
在这里插入图片描述
上述示例中的通信方式及路径如图11-2所示. 重点在于, 父子进程都可以访问管道的I/O路径, 但子进程仅用输入路径, 父进程仅用输出路径.
在这里插入图片描述
以上就是管道的基本原理及通信方法. 应用管道时还有一部分内容需要注意, 通过双向通信实例进一步说明.

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

下面创建2个进程通过1个管道进行双向数据交换的实例, 其通信方式如图11-3所示.
在这里插入图片描述
从图11-3可以看出, 通过1个管道可以进行双向通信. 但采用这种模型是需格外注意. 先给出示例, 稍后再讨论.

#include <stdio.h>
#include <unistd.h>

#define BUF_SIZE 30

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;

    pipe(fds);
    pid = fork();
    if (pid == 0)
    {
        write(fds[1], str1, sizeof(str1)); /* 18-21行: 子进程运行区域. 通过第18行传输数据, 第20行接收数据. 需要特别关注第19行的sleep函数. 关于这一点稍后再讨论, 希望各位自己思考其含义 */
        sleep(2);
        read(fds[0], buf, BUF_SIZE);
        printf("Child proc output: %s \n", buf);
    }
    else
    {
        read(fds[0], buf, BUF_SIZE); /* 25-28行 父进程运行区域. 通过第25行接收数据, 这是为了接收第18行的子进程传输的数据. 另外, 通过第27行传输数据, 这些数据被第20行的子进程接收 */
        printf("Parent proc output: %s \n", buf);
        write(fds[1], str2, sizeof(str2));
        sleep(3); /* 父进程先终止时会弹出命令提示符. 这时子进程仍在工作, 故不会产生问题. 这条语句主要是为了防止子进程终止前弹出命令提示符(故可删除). 注释这条代码后再运行程序, 各位就会明白我的意思. */
    }
    
    return 0;
}

运行结果:
在这里插入图片描述
运行结果应该和大家的预想一致. 这次注释第18行代码后再运行(务必亲自动手操作). 虽然这行代码只将运行时间延迟2秒, 但已引发运行错误. 产生原因是什么呢?
在这里插入图片描述
简言之, 数据进入管道后成为无主数据. 也就是通过read函数先读取数据的进程将得到数据, 即使该进程将数据传到了管道. 因此, 注释第18行将产生问题. 在第19行, 子进程将读回自己在第17行向管道发送的数据. 结果, 父进程调用read 函数后将无限等待数据进入管道.

从上述实例中可以看出, 只用1个管道进行双向通信并非易事. 为了实现这一点, 程序需要预测并控制运行流程, 这在每种系统中都不同, 可以视为不可能完成的任务. 既然如此, 该如何进行双向通信呢?
在这里插入图片描述
非常简单, 1个通道无法完成双向通信任务, 因此需要创建2个管道, 各自负责不同的数据流动即可. 其过程如图11-4所示.
在这里插入图片描述
由图11-4可知, 使用2个管道可以避免程序的预测或控制. 下面采用上述模型改进pipe2.c

#include <stdio.h>
#include <unistd.h>

#define BUF_SIZE 30

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;

    pipe(fds1), pipe(fds2); /* 创建两个管道. */
    pid = fork();
    if (pid == 0)
    {
        write(fds1[1], str1, sizeof(str1)); /* 第18 24行: 子进程可以通过数组fds1指向的管道向父进程传输数据. */
        read(fds2[0], buf, BUF_SIZE); /* 第19 26行: 父进程可以通过数组fds2指向的管道向子进程发送数据. */
        printf("Child proc output: %s \n", buf);
    }
    else
    {
        read(fds1[0], buf, BUF_SIZE);
        printf("Parent proc output: %s \n", buf);
        write(fds2[1], str2, sizeof(str2));
        sleep(3); /* 第27行: 没有太大的意义, 只是为了延迟父进程终止而插入的代码 */
    }
    
    return 0;
}

运行结果:
在这里插入图片描述

11.2 运用进程间通信

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

保存消息的回声服务器端

下面扩展第10章的echo_mpserv.c, 添加如下功能:
在这里插入图片描述
我希望将该任务委托给另外的进程. 换言之, 另行创建进程, 从向客户端提供服务的进程读取字符串信息. 当然, 该过程中需要创建用于接收数据的管道.

下面给出示例. 该示例可以与任意回声客户端配合运行, 但我们将用第10章介绍过的 echo_mpclient.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 100

void error_handling(char *message);
void read_childproc(int sig);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    int fds[2];

    pid_t pid;
    struct sigaction act;
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];
    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }

    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, 0);

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    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");
    }

    pipe(fds); /* 第53 54行: 第54行创建负责保存文件的进程 */
    pid = fork(); 
    if (pid == 0) /* 第55行-68行: 第54行创建的子进程运行区域. 该区域从管道出口fds[0]读取并保存到文件中. 另外, 上述服务器端并不终止运行, 而是不断向客户端提供服务. 因此, 数据在文件中积累到一定程度即关闭文件, 该过程通过第61行的循环完成. */
    {
        FILE * fp = fopen("echomsg.txt", "wt");
        char msgbuf[BUF_SIZE];
        int i, len;

        for (i=0; i<10; i++)
        {
            len = read(fds[0], msgbuf, BUF_SIZE);
            fwrite((void*)msgbuf, 1, len, fp);
        }

        fclose(fp);
        return 0;
    }

    while (1)
    {
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
        if (clnt_sock == -1)
        {
            continue;
        }
        else
        {
            puts("new client connected...");
        }
        
        pid = fork();
        if (pid == 0)
        {
            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); /* 第71行: 第84行通过fork 函数创建的所有子进程将复制第53行创建的管道的文件描述符. 因此, 可以通过管道入口fds[1]传递字符串信息. */
            }

            close(clnt_sock);
            puts("client disconnected...");
            return 0;
        }
        else
        {
            close(clnt_sock);
        }
    }

    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

void read_childproc(int sig)
{
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);
    printf("removed proc id: %d \n", pid);
}

运行结果:
服务器端:
在这里插入图片描述
客户端:
在这里插入图片描述
如上运行结果所示, 希望各位也启动多个客户端向服务端传递字符串. 文件中积累一定数量的字符串后(共10次的fwrite函数调用完成后), 可以打开echomsg.txt 验证保存的字符串.
在这里插入图片描述

如果想构建更大型的程序

前面已经讲解了并服务器的第一种实现模型, 但各位或许有如下想法:
在这里插入图片描述
若想仅用进程和管道构建具有复杂功能的服务器端, 程序员需要具备熟练的编程技能和经验. 因此, 初学者应用模型扩展程序并非易事, 希望大家不要过于拘泥. 以后要说明的另外两种模型功能上更加强大, 同时更容易实现我们的想法.
在这里插入图片描述
我曾被多次问及此类问题. 各位通过第10章内容理解了操作系统的基本内容, 同时也是学习线程必备的前期知识–进程. 而且掌握了多进程代码的基本分析能力. 即使各位不会亲自利用多进程构建服务器, 但这些都值得学习. 可以将我的个人经验告诉所有读者朋友:
在这里插入图片描述

结语:

你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/

时间: 2020-06-04

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值