(25)Linux IPC 进程间通信&&系统调用:pipe接口

一、进程间通信(IPC)

1、为什么要进程间通信?

我们在之前讲过 "进程之间是具有独立性" 的,如果进程间想交互数据,成本会非常高!

因为独立性之本质即 "封闭",进程们你封闭你的我封闭我的,那么进程间的交流可谓是窒碍难行。

进程间的通信说白了就是 "数据交互",我们需要多进程进行协同处理一件事情。

  • 刚才说的是宏观上的概念,下面我们来看看具体的、为什么要进行通信:
  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享资源
  • 通知事件:一个进程需要向另一个或一组进程发送讯息,通知它 (它们) 发生了某种事件(比如进程终止时要通知父进程)
  • 进程控制:有些进程希望完全控制另一个进程的执行(如 debug 进程),此时控制进程希望能够拦截另一个进程的所有陷阱和异常,并能够及时知道它的状态改变,属于 "闭环控制"。

进程间通信的必要性:

  • 单进程的,那么也就无法使用并发能力,更加无法实现多进程协同
  • 传输数据,同步执行流,消息通知等

2、程间通信的技术背景

  • 1.进程是具有独立性的。虚拟地址空间+页表 保证进程运行的独立性(进程内核数据结构+进程的代码和数据)
  • 2.通信成本会比较高!

3、进程间通信的本质理解

  • 1.进程间通信的前提,首先需要让不同的进程看到同一·块"内存"(特定的结构组织的)
  • 2.所以你所谓的进程看到同一块"内存",属于哪一个进程呢?不能隶属于任何一个进程,而应该更强谢共享

进程间通信的方式 也有一些标准

1.Linux原生能提供 - 管道-->匿名 命名

2.Systemy ---多进程 -- 单机通信

共享内存

消息队列(不常用)

信号量(不讲 - 原理)

3.posix-- 多线程----网络通信

标准更多在我们使用者看来,都是接口上具有一定的规律

二、管道(PIPE)

1、何为管道?

何为管道?管道是 系统中最古老的 IPC 形式,

将一个进程连接到另一个进程的数据流称为管道 (Pipe)。

下面我们先来讲解 匿名管道 (Anonymous Pipe) !

2、匿名管道(Anonymous Pipe)

匿名管道是计算机进程间的一种 单工 先进先出通信机制,全双工通信 通常需要两个匿名管道。

举个例子: 假设内存中有两个独立的进程 ,我们想让 之间进行进程间通信。

* 令 先把数据拷贝到磁盘上,再让 去读取该数据,如下图所示:

​我们可以通过这个例子明白一个道理:通信之前,要让不同的进程看到同一份资源。

现阶段我们要学的进程间通信,不是如何通信,而是先去关注它们是如何看到同一份资源的。

那么在进程通信之前,如何做到让进程 "先看到同一份资源" 呢?

资源的不同,决定了不同种类的通信方式! 而管道,就是提供共享资源的一种手段。

我们知道,文件在内存和磁盘之间来回切换是非常耗时的,因此进程间通信大多都是内存级别的。

即在内存内部重建一块 "小区域" 进行通信,示意图如下:

对我们来说,我们 echo 一个 hello,写到文件中,实际上这就算通信了

但是我们要讨论的不是这种通信!我们讨论的是内存级的通信!

3、管道通信的原理

我们在前几章中学了文件描述符 (fd) 的知识点,我们将其系统中存在的匿名管道相结合:

首先,一个进程维护自己进程对应的文件描述符表file_struct,而 file_struct 中有对应的数组。

数字里存的是 struct file* fd_array[],这里面存的就是打开文件的文件指针。

其中 0,1,2 被默认占用,这个在之前我们也做过讲解,对应 stdin, stdout, stdin,这里不再赘述。

如果我们今天打开一个文件,OS 为了管理文件,需要将磁盘中的文件的属性信息加载到内存里。

对该文件形成 struct file,包含了文件的所有属性,对应了文件的:

  • ① 操作方法
  • ② file 自己内部的缓冲区

如果我们让该进程 fork 创建一个子进程,

在做拷贝时是不需要将 struct file 本身给子进程拷贝一份的。

创建子进程 task_struct 和 file_struct 是需要被拷贝的,但是 struct file 是不需要的。

"创建进程,和我文件有什么关系?"

这也就是为什么我们创建 fork 子进程之后,让父子打印时,都会像同一个显示器打印的原因。

**结论:**struct file 一定能找到对应缓冲区的操作方法和 file 自己内部缓冲区。

4、管道通信的特点

如何做到让不同的进程,看到同一份资源的呢?---fork让子进程继承的能够让具有血缘关系的进程进行进程间通信- 常用于父子进程

输出型参数,期望通过调用它,得到被打的文件fd

特点:

  1. 管道是用来进行具有血缘关系的进程进性进程间通信-- 常用于父子通信
  2. 管道具有通过让进程间协同,提供了访问控制!
  3. 管道提供的是面向流式的通信服务 -- 面向字节流 -- 协议
  4. 管道是基丁文件的,文件的生命周期是随进程的,管道的生命周期是随进程的!
  5. 管道是单向通信的 ,就是半双工通信的一种特殊情况

其中上面第二点理解:

  • 写快,读慢,写满不能在写了
  • 写慢,读快,管道没有数据的时候,读必须等待
  • 写关,读0,标识读到了文件结尾
  • 读关,写继续写,0S终止写进程

管道是一个文件 - -读取 -- 具有访问控制

显示器也是一个文件,父子同时往显示器写入的时候,有没有说·个会等另·个的情况呢?

--缺乏访问控制

5、 系统调用:pipe 接口

Linux 给我们提供了 pipe 接口,只需调一下 pipe 就会在底层自动把文件以读和写的方式打开。

你会得到两个 fd,并且会被写进 pipefd[2] 数组中:

cpp 复制代码
#include <unistd.h>
int pipe(int pipefd[2]);   // 数组中分别存储第一次 O_RDONLY 和 O_WRONLY

你可以理解为:pipe 内部封装了 open,并且它 open 了两次:

  • 第一次 open:以 O_RDONLY 读的方式打开
  • 第二次 open:以 O_WRONLY 写的方式打开

最后,把读写 fd 分别放在 pipefd 数组的 0 下标和 1 下标中,这就帮你创建了一个共享文件。

并且别忘了 pipe 可是系统调用,创建文件时就在内核中将文件类型初始化 i_pipe,

让它指向的是一个管道文件,指向管道信息,也就不用和磁盘产生关联了。

当父进程没有写入的时候,子进程在等,所以父进程写入之后,

子进程才能 read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主。

思考:父进程和子进程读写的时候(向显示器写入也是文件),是有一定顺序性的。父子进程各自 printf 的时候,会有顺序吗?

答案是不会。管道内部没有数据,reader 就必须阻塞等待(read),管道内部如果数据被写满,此时 writer 就必须阻塞等待(write),等管道有数据。

完全乱序的地方就是缺乏访问控制,管道内部自带访问控制机制。

最后帮助大家理解管道,准备了一个程序,可以自行尝试理解:

makefile:

cpp 复制代码
mypipe:mypipe.cc
	g++ -o $@ $^ -std=c++11 #-DDEBUG
.PHONY:clean
clean:
	rm -f mypipe

mypipe.cc:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

// 为什么不定义全局buffer来进行通信呢?? 因为有写时拷贝的存在,无法更改通信!

int main()
{
    // 1. 创建管道
    int pipefd[2] = {0}; // pipefd[0(嘴巴,读书)]: 读端 , pipefd[1(钢笔,写)]: 写端
    int n = pipe(pipefd);
    assert(n != -1); // debug assert, release assert
    (void)n;

#ifdef DEBUG
    cout << "pipefd[0]: " << pipefd[0] << endl; // 3
    cout << "pipefd[1]: " << pipefd[1] << endl; // 4
#endif
    // 2. 创建子进程
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        //子进程 - 读
        // 3. 构建单向通信的信道,父进程写入,子进程读取
        // 3.1 关闭子进程不需要的fd
        close(pipefd[1]);
        char buffer[1024 * 8];
        while (true)
        {
            // sleep(20);
            // 写入的一方,fd没有关闭,如果有数据,就读,没有数据就等
            // 写入的一方,fd关闭, 读取的一方,read会返回0,表示读到了文件的结尾!
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                buffer[s] = 0;
                cout << "child get a message[" << getpid() << "] Father# " << buffer << endl;
            }
            else if(s == 0)
            {
                cout << "writer quit(father), me quit!!!" << endl;
                break;
            }
        }
        // close(pipefd[0]);
        exit(0);
    }
    //父进程 - 写
    // 3. 构建单向通信的信道
    // 3.1 关闭父进程不需要的fd
    close(pipefd[0]);
    string message = "我是父进程,我正在给你发消息";
    int count = 0;
    char send_buffer[1024 * 8];
    while (true)
    {
        // 3.2 构建一个变化的字符串
        snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
                 message.c_str(), getpid(), count++);
        // 3.3 写入
        write(pipefd[1], send_buffer, strlen(send_buffer));
        // 3.4 故意sleep
        sleep(1);
        cout << count << endl;
        if (count == 5){
            cout << "writer quit(father)" << endl;
            break;
        }
    }
    close(pipefd[1]);
    pid_t ret = waitpid(id, nullptr, 0);
    cout << "id : " << id << " ret: " << ret <<endl;
    assert(ret > 0); 
    (void)ret;

    return 0;
}

运行结果:

感谢阅读!!!!!

相关推荐
对你无可奈何4 分钟前
关于Ubuntu的 update造成的内核升级
运维·服务器·ubuntu
qq_3129201127 分钟前
Nginx限流与防爬虫与安全配置方案
运维·爬虫·nginx·安全
GanGuaGua33 分钟前
Linux系统:线程的互斥和安全
linux·运维·服务器·c语言·c++·安全
lsnm42 分钟前
【LINUX网络】IP——网络层
linux·服务器·网络·c++·网络协议·tcp/ip
全糖去冰吃不了苦43 分钟前
ELK 集群部署实战
运维·jenkins
不掰手腕1 小时前
在UnionTech OS Server 20 (统信UOS服务器版) 上离线安装PostgreSQL (pgsql) 数据库
linux·数据库·postgresql
Lynnxiaowen1 小时前
今天继续昨天的正则表达式进行学习
linux·运维·学习·正则表达式·云计算·bash
努力学习的小廉2 小时前
深入了解linux系统—— POSIX信号量
linux·运维·服务器
刘一说2 小时前
CentOS部署ELK Stack完整指南
linux·elk·centos
从零开始的ops生活2 小时前
【Day 50 】Linux-nginx反向代理与负载均衡
linux·nginx