Linux之进程间通信(管道)

目录

一、进程间通信

1、进程间通信的概念

2、进程间通信的目的

3、进程间通信的分类

二、管道

1、管道基本介绍

2、匿名管道

3、命名管道


一、进程间通信

1、进程间通信的概念

什么是进程间通信?

我们在学习了进程的相关知识后,知道,进程的运行是具有独立性的,每个进程都有自己独立的PCB,也都有只属于自己的地址空间,进程之间是互不干扰的。

所以说,进程之间要通信的话,难度是非常大的。

进程间通信的本质:操作系统需要直接或者间接给通信双方的进程提供一段 "内存空间",并且要让不同的进程看到同一份资源(内存空间)。而这个内存空间不应该属于任何一个进程,而应该由操作系统来维护。

所以,进程间通信的最大成本就是:操作系统在设计的原生层面,进程是互相独立的,但是现在需要让它们之间进行通信。那么应该如何让不同的进程看到同一份资源。

2、进程间通信的目的

~ 数据传输:一个进程需要将它的数据发送给另一个进程。

~ 资源共享:多个进程之间共享同样的资源。

~ 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

~ 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

3、进程间通信的分类

~ 管道:1、匿名管道pipe 2、命名管道

~ System V IPC:1、System V 消息队列 2、System V 共享内存 3、System V 信号量

~ POSIX IPC:1、消息队列 2、共享内存 3、信号量 4、互斥量 5、条件变量 6、读写锁

进程间通信的必要性:如果只是使用单进程,那么也就无法使用并发能力,更加无法实现多进程协同工作。

本篇文章我们来具体讲一讲,管道的相关知识。

二、管道

1、管道基本介绍

什么是管道?

1、管道是Unix中最古老的进程间通信的形式。

2、我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"。

管道只能进行单向通信,用来传输资源:数据。

我们按下面的步骤来分析:

1、首先,父进程分别以读和写的方式,打开了一个文件。

2、然后,父进程fork创建了一个子进程,我们知道,子进程会继承父进程,指向同一个文件。

3、我们规定,父进程对文件写入,子进程对文件读取。关闭父子进程各自不需要的功能。即:父进程关闭读的文件描述符,子进程关闭写的文件描述符。

最终,通过该文件就实现了单向通信。这就是管道。管道的本质其实就是内存级文件。所以两个进程间通信的数据,不需要刷新到磁盘里。

为什么能够这样做?

文件有属于自己的内核缓冲区,所以父进程和子进程有一份公共的资源:文件系统提供的内核缓冲区,父进程可以向对应的文件的文件缓冲区写入,子进程可以通过文件缓冲区读取,此时就完成了进程间通信。

管道一般用来进行具有血缘关系的进程(父子进程)之间的通信。

管道分为匿名管道和命名管道。下面我们就来具体讲一讲它们的内容。

2、匿名管道

通过文件名区分文件,但是如果当前进程的文件没有名字,这样的内存级文件称为匿名管道。匿名管道能用来父进程和子进程之间进行进程间通信。

匿名管道文件在磁盘上也没有实体,它只存在于内存中。

函数:pipe,调用成功返回0,调用失败返回-1。

cpp 复制代码
SYNOPSIS
       #include <unistd.h>
       int pipe(int pipefd[2]);
DESCRIPTION
    pipe() creates a pipe,pipefd[0]  refers  to  the  read end of the pipe.  pipefd[1] refers to the write end of the pipe.
RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

我们首先创建管道文件,打开读写端:在pipefd数组中,pipefd[0]代表读,pipefd[1]代表写。

cpp 复制代码
#include <iostream>
#include <cassert>
#include <unistd.h>

using namespace std;

int main()
{
    // 创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n != -1);
    (void)n;

    cout << "pipefd[0]: " << pipefd[0] << endl;
    cout << "pipefd[1]: " << pipefd[1] << endl;

    return 0;
}

接着,我们fork创建子进程, 然后,我们关闭父子进程不需要的文件描述符,完成通信框架的建立:

cpp 复制代码
#include <iostream>
#include <cassert>
#include <unistd.h>

using namespace std;

int main()
{
    // 创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n != -1);
    (void)n;

    pid_t id = fork();
    assert(id != -1);
    if(id == 0)
    {
        //子进程
        close(pipefd[1]);

        exit(0);
    }

    //父进程
    close(pipefd[0]);

    return 0;
}

最后,我们通过父进程给子进程发送消息来检测通信:

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

using namespace std;

int main()
{
    // 创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n != -1);
    (void)n;

    // 创建子进程
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        // 子进程
        close(pipefd[1]);
        char buffer[1024];
        while (true)
        {
            ssize_t mess = read(pipefd[0], buffer, sizeof(buffer));
            if (mess > 0)
            {
                buffer[mess] = 0;
                cout << "father process say: " << buffer << endl;
            }
        }

        exit(0);
    }

    // 父进程
    close(pipefd[0]);
    string message = "i am father ->";
    char buff[1024];
    int count = 0;
    while (true)
    {
        snprintf(buff, sizeof(buff), "%s[%d]: %d", message.c_str(), getpid(), count++);
        write(pipefd[1], buff, sizeof(buff));
        sleep(1);
    }

    pid_t ret = waitpid(id, nullptr, 0);
    assert(ret > 0);
    (void)ret;

    close(pipefd[1]);
    return 0;
}

管道的特点:

1、管道是用来进行具有血缘关系的进程间的通信,常用于父子进程。

2、管道能够让进程间协同,提供了访问控制。

3、管道是面向字节流的。

4、管道的生命周期随进程,进程退出,管道释放。

5、管道是单向通信,是半双工通信的特殊情况。

6、内核会对管道操作进行互斥与同步。

下面我们针对管道的特点2,来进行演示:

~ 读快写慢

其余代码不变,我们仅让父进程在每次写入后,休眠10秒再写。

我们发现,父进程休眠期间,子进程会等待父进程写入后再读取。(如果管道中没有数据,读端在读,此时默认会直接阻塞当前正在读取的进程)

~ 读慢写快

我们让父进程不停地写入。

子进程休眠5秒后再读取。

拿着管道读端不读,写端一直在写:写端往管道里写,而管道是有大小的,不断往写端写,会被写满。

管道是固定大小的缓冲区,当管道被写满,就不能再写了。此时写端会阻塞。而休眠结束后,子进程就会开始读取,这时父进程才能继续写入。

~ 关闭写入端

父进程写入端关闭:写完第一次后,父进程休眠6秒,然后结束循环,关闭写入端。

父进程作为写入的一方,关闭了写入端,子进程作为读取的一方,read会返回0,表示读到了文件的结尾。这时我们让子进程结束循环,并退出。

~ 关闭读端

管道是单向的:读端关闭,再写入就没有意义了,这时OS会终止写端,会给写进程发送信号,终止写端。

为了方便观察,我们让父进程读取,子进程写入。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	int pipefd[2] = { 0 };
	if (pipe(pipefd) < 0)
    { 
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); 
	if (id == 0){
		//child
		close(pipefd[0]); 
		const char* msg = "hello father, I am child...";
		int count = 10;
		while (count--){
			write(pipefd[1], msg, strlen(msg));
			sleep(1);
		}
		close(pipefd[1]);
		exit(0);
	}
	//father
	close(pipefd[1]); 
	close(pipefd[0]); 
	int status = 0;
	waitpid(id, &status, 0);
	printf("child get signal:%d\n", status & 0x7F); 
	return 0;
}

操作系统向子进程发送的是SIGPIPE信号将子进程终止。

3、命名管道

上面我们所讲的匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

而如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。

一个进程打开了一个文件后,Linux内核里会有文件的struct file结构。如果还有第二个进程要打开这个文件,那么只要路径相同,那么两个进程打开的文件一定是同一个且是唯一的,那么第二个进程不需要继续创建struct file对象,因为OS会识别到打开的文件被打开了。

此时两个进程就看到了同一份资源,该文件也不需要把数据刷新到磁盘上去,不需要IO。

所以说,命名管道是真实存在于系统路径下的,只是它只有属性,没有内容。它能够使用open,close函数被打开或关闭。

mkfifo:也可以作为命令直接在指定路径下创建命名管道。mkfifo 命名管道名称

cpp 复制代码
NAME
       mkfifo - make FIFOs (named pipes)

SYNOPSIS
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
RETURN VALUE
       On success mkfifo() returns 0.  In the case of an error, -1 is returned (in which case, errno is set appropriately).

下面我们就来使用一下:我们有三个文件,server.cc(读取端) ,client.cc(写入端) 是两个源文件,它们运行后会是两个不同且无关系的进程,我们来进行它们之间的通信。comm.h是一个头文件。

comm.h

cpp 复制代码
#include<iostream>
#include<string>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

using namespace std;

string ipcPath = "./fifo.ipc";
#define MODE 0666
#define SIZE 128

server.cc

cpp 复制代码
#include "comm.hpp"

int main()
{
    // 1.创建命名管道
    int ret = mkfifo(ipcPath.c_str(), MODE);
    if (ret == -1)
    {
        perror("mkfifo");
        exit(1);
    }

    // 2.正常文件操作:通信
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    char buffer[SIZE];
    while (true)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            cout << "client say :" << buffer << endl;
        }
        else if (s == 0)
        {
            cerr << "read the end of file ->  client quit, server quit too" << endl;
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

    close(fd);
    return 0;
}

client.cc

cpp 复制代码
#include "comm.hpp"

int main()
{
    // 1.打开命名管道
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if (fd < 0)
    {
        perror("open");
        exit(1);
    }

    string buffer;
    while (true)
    {
        cout << "please enter message ->" << endl;
        getline(cin, buffer);
        write(fd, buffer.c_str(), buffer.size());
    }

    close(fd);
    return 0;
}

通信演示:

相关推荐
一个网络学徒5 分钟前
MGRE综合实验
运维·服务器·网络
守望时空3311 分钟前
RustDesk搭建指南
linux
C++ 老炮儿的技术栈15 分钟前
在 Scintilla 中为 Squirrel 语言设置语法解析器的方法
linux·运维·c++·git·ubuntu·github·visual studio
墨痕砚白25 分钟前
VMware Workstation Pro虚拟机的下载和安装图文保姆级教程(附下载链接)
服务器·windows·vmware·虚拟机
白鹭1 小时前
基于LNMP架构的分布式个人博客搭建
linux·运维·服务器·网络·分布式·apache
java叶新东老师1 小时前
linux 部署 flink 1.15.1 并提交作业
linux·运维·flink
程序员JerrySUN2 小时前
Linux系统架构核心全景详解
linux·运维·系统架构
无敌的牛2 小时前
Linux文件理解,基础IO理解
linux·运维·服务器
angushine2 小时前
鲲鹏服务器logstash采集nginx日志
运维·服务器·nginx