当前位置: 首页 > news >正文

Linux学习:进程通信(管道)

目录

  • 1. 前言:进程通信
  • 2. 匿名管道的原理
  • 3. 匿名管道的系统调用接口
  • 4. 匿名管道的四种情况与五种特性
    • 4.1 管道的四种情况
    • 4.2 管道的五种特性
    • 4.3 命令行上的匿名管道
  • 5. 命名管道
    • 5.1 命名管道的原理
    • 5.2 命名管道的使用
    • 5.3 命名管道使用示例

1. 前言:进程通信

  • 什么是进程间的通信
      两个进程之间可以直接进行"数据"的直接传递吗,这显然是不行的,显然会违背进程本身的独立性。可是,进程在创建子进程的过程中,子进程不是会继承父进程内核数据结构中的各种信息吗,父进程正可以通过这一方式实现对子进程数据的传递。虽然父子进程数据与代码共享,会写时拷贝,但它们之间也是独立的(拥有独立的内核数据结构)。单次数据的传递不能被称之为通信,而且也没有实际的应用价值,一个进程能把自己的数据交给另一个进程,反之亦然,多次可重复双向的数据交换我们才称之为进程间的通信

  • 进程间通信的价值和意义
    1、 通过数据的传输,实现多进程并发(多个进程去协同,共同完成一些事情)
    2、 资源共享(应用于大型联机游戏,地图数据/资源共享)
    3、 通知事件(告知其他进程发生了某些时间或状态变化)
    4、 进程控制(通过一个进程去对其他的进程进行控制操作)

  • 技术上如何实现进程间的通信
    在这里插入图片描述
      进程之间通信的本质就是使得不同的进程都看到同一份资源,可是,进程本身具有独立性,因此,进程间不可能直接去其他进程的内存空间中获取数据。但数据的通信必须要有一块双方共享的用来进行数据交换的空间(一块内存空间)。这块用来数据交换的共享空间不能由通信双方中的任何一方进程去提供,所以,就只能由操作系统提供一块这样的内存空间。
      这块操作系统提供的"空间",其有着多种不同的样式,根据样式的不同其对应的通信方式也不同。现还常用的两套进程间的通信标准分别为:
    SYSTEM V IPC(IPC:进程间通信):管道、共享内存、消息队列、信号量。
    POSIX IPC:共享内存、消息队列、信号量、互斥量、条件变量、互斥锁。
      SYSTEM V只能用于主机内通信,而后者不仅仅支持主机内通信,同时也支持网络通信。上世纪之前的计算机技术前沿即使各种不同的通信方案,现如今很多的都是以前不同的实验室创造的,越优秀的方案就越容易称为主流的标准,当自己获得行业内制定标准的权力无疑伴随的利益是十分巨大的,同时,不优秀的不完善的技术都会随着时间的推移淘汰掉。

2. 匿名管道的原理

在这里插入图片描述

  进程用读"r"方式与写"w"方式分别打开一次同一个文件,再创建一个此进程的子进程。这个子进程会将父进程的文件描述符表拷贝一份,但操作系统认为不必要再创建一份重复的资源,其不允许资源的浪费。所以,不会再打开文件创建一份管理文件的内核数据结构,而是直接进行浅拷贝使用同一个struct file。创建成功后,父进程关闭写模式文件fd,子进程关闭读模式文件fd。这样,只要父子进程分别从这个指向的文件读写,如此便实现了两个进程间的通信。
  既然,struct file允许多个进程的指针指向,那为什么父进程最开始就要按照分别读写模式打开同一个文件呢。这是为了后续父子进程通过分别关闭读端,写端的方式去控制数据的读写方向。上述这种基于文件的,让不同进程看到同一份资源的通信方式,就叫做管道。管道只能被设计为单向通信的模式(半双工),这种通信方式的设计初衷就是为了实现单向通信,管道的名称本身就证明了这一点。

3. 匿名管道的系统调用接口

  进程间的通信只是需要一块共享的内存空间,采用文件来实现的管道通信方式。其中的文件只是为了充当一个共享空间,所以并不需要将写入文件的内容刷新到磁盘上。因此,其实没有必要真的去打开一个磁盘文件,而是只需要创建一个内存级的文件即可。这个文件没有文件名与路径且再磁盘中不存在,这个匿名文件就是这里的管道。系统提供了专门的系统调用接口,如下:

int pipe(int pipe[2]);

  pipe方法会将打开的管道文件读写端的文件fd写入参数数组中,0号下标的位置存入读端(“r”),1号下标的位置存入写端(“w”)。当顺利创建打开管道文件时,函数返回值为0,当调用失败时,函数返回值为-1,并会将错误码写入头文件<errno.h>头文件中的全局变量errno中
在这里插入图片描述
  使用pipe系统调用接口创建管道的方式,只能用于带有"血缘关系"之间的进程通信,父进程打开管道文件的读写端。
在这里插入图片描述
  而后,创建子进程并继承父进程的文件描述符表中的内容。
在这里插入图片描述
  最后,根据设定的数据流通方向,父子进程再分别关闭对应的读端或写端,这样就完成了管道的创建。这种管道文件我们称之为匿名管道,其没有对应的系统路径和文件名。

4. 匿名管道的四种情况与五种特性

4.1 管道的四种情况

  • 子进程写的慢,父进程一直读:管道内没有数据,子进程不关闭自己的写端文件fd,父进程就要阻塞等待直至管道内有数据,而后进行读取。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>

void writer(int wfd)
{
    char* str = "hello father! this is chlid";
    char buffer[128];
    int pid = getpid();
    int cnt = 0;

    while(1)
    {
        //1. 写端写的慢,读端正常读取
        snprintf(buffer, sizeof(buffer), "%s, pid = %d, count = %d\n", str, pid, cnt++);
        write(wfd, buffer, sizeof(buffer));

        sleep(5);
    }
}

void reader(int rfd)
{
    char buffer[1024];
    while(1)
    {
        //1. 写慢读快
        size_t n = read(rfd, buffer, sizeof(buffer) - 1);
        (void) n;
        printf("father get a message: %s", buffer);
        sleep(1);
}

int main()
{
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret < 0) return 1;

    printf("pipefd[0] = %d, pipefd[1] = %d\n", pipefd[0]/*read*/, pipefd[1]/*write*/);

    pid_t fd = fork();

    if(fd == 0)//子进程
    {
        //child 1
        close(pipefd[0]);

        writer(pipefd[1]);

        exit(0);
    }
    else
    {
        //father 0
        close(pipefd[1]);

        reader(pipefd[0]);
    }

    wait(NULL);//回收子进程

    return 0;
}

在这里插入图片描述

  • 子进程一直写,父进程不读:管道内部被写满,读端不关闭自己的文件fd,写端写满管道之后就进行阻塞等待。(一种同步机制)
void writer(int wfd)
{
    char* str = "hello father! this is chlid";
    char buffer[128];
    int pid = getpid();
    int cnt = 0;

    while(1)
    {
        //2. 写端一直写,读端不读(计算管道大小)
        char c = 'A';
        write(wfd, &c, 1);

        cnt++;
        printf("cnt = %d\n", cnt);
        sleep(1);
    }
}

void reader(int rfd)
{
    char buffer[1024];
    while(1)
    {
		//2. 一直写不读     	
        sleep(100);
    }
}

在这里插入图片描述
  在Ubuntu 20.04版本Linux操作系统中,管道文件的大小为65536字节(即64KB),查看当前系统版本信息(指令: cat /etc/lsb-release)。系统中存在着一个宏PIPE_BUF,这个宏的值是4096,单位为字节(4KB)。当进程间使用管道通信时,一次交互的数据大小不大于4096Bytes,我们就称之为原子的,此次通信的数据保证是安全的,而当一次通信的数据大小大于这个值,则就不保证数据传输的安全了。查看PIPE_BUF的大小,在7号手册中查找(指令:man 7 pipe),在手册中查找关键字,指令:/关键字
在这里插入图片描述

  • 写端关闭,读端一直读:当写端进程关闭之后,读端进程会继续读取管道内的数据。当管道内数据被读取完成之后,再次尝试读取read函数的返回值为0;正常读取时,read函数的返回值为读取到的字节数;当读取失败时,read函数的返回值小于0。(子进程关闭,父进程还在读,子进程会进入僵尸状态< defunct >)
void writer(int wfd)
{
    char* str = "hello father! this is chlid";
    char buffer[128];
    int pid = getpid();
    int cnt = 0;

    while(1)
    {
        //3. 写端关闭,读端一直读
         sleep(1);

         char c = 'A';
         write(wfd, &c, 1);

         cnt++;
         printf("cnt = %d\n", cnt);
         if(cnt == 10) 
             break;
    }
    
    //3.
    close(wfd);
}

void reader(int rfd)
{
    char buffer[1024];
    while(1)
    {
        //3. 一直读,写关闭
         size_t n = read(rfd, buffer, sizeof(buffer) - 1);

         if(n > 0)
             printf("father get a message: %s, n: %ld\n", buffer, n);
         else if(n == 0)
         {
             printf("read pipe done, read file done!\n");
             break;
         }
         else
             break;
    }

    //3.
    close(rfd);
    printf("read endpoint close!\n");
}

在这里插入图片描述

  • 读端关闭,写端一直写:操作系统会直接终止写端的进程,通过信号13 SIGPIPE杀掉进程,父进程wait获得子进程的退出信号。当文件没有指向时,操作系统会自动关闭对应文件,当父子进程关闭之后,操作系统会自动将管道文件关闭。
void writer(int wfd)
{
    char* str = "hello father! this is chlid";
    char buffer[128];
    int pid = getpid();
    int cnt = 0;

    while(1)
    {
        //4. 读端关闭,写端一直写
         sleep(1);
         char c = 'A';
         write(wfd, &c, 1);
         cnt++;
         printf("cnt: %d\n", cnt);
    }
}

void reader(int rfd)
{
    char buffer[1024];
    int cnt = 10;//4.
    while(1)
    {
        //4. 读关闭,一直写
         size_t n = read(rfd, buffer, sizeof(buffer) - 1);

         if(n > 0)
             printf("father get a message: %s, n: %ld\n", buffer, n);
         else if(n == 0)
         {
             printf("read pipe done, read file done!\n");
             break;
         }
         else
             break;

         cnt--;
         if(cnt == 0) 
         	break;
    }

    // 4.
     close(rfd);
     printf("read endpoint close!\n");
}

int main()
{
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret < 0) return 1;

    printf("pipefd[0] = %d, pipefd[1] = %d\n", pipefd[0]/*read*/, pipefd[1]/*write*/);

    pid_t fd = fork();

    if(fd == 0)//子进程
    {
        //child 1
        close(pipefd[0]);

        writer(pipefd[1]);

        exit(0);
    }
    else
    {
        //father 0
        close(pipefd[1]);

        reader(pipefd[0]);
    }
	
	//4.
    int status = 0;
    int rid = waitpid(fd, &status, 0);
    if(rid == fd)
    {
        printf("exit code: %d, exit signal: %d\n", WEXITSTATUS(status), status&0x7F);
    }

    return 0;
}

在这里插入图片描述

4.2 管道的五种特性

  • 自带同步机制(读端会等待写端)
  • 匿名管道只能作用有血缘关系进程之间
  • 管道时面向字节流的(写多少次与一次性读取的数据量没有关系)
  • 父子进程同时退出后,管道会自动释放,文件的生命周期随进程
  • 管道只能单向通信(半双工的一种特殊情况,半双工:一次只能有一个人发言)

4.3 命令行上的匿名管道

  • 指令范式:
指令:指令1 | 指令2

将指令1的输出信息当作指令2中的输入信息,将指令1进程的标准输出改为标准输入,指令2进程正常从标准输入中获取信息。

  • 指令示例:
#统计一共有多少个.h、.c文件
find ~ -type f -name "*.h" -o -name "*.c" | wc -l# -name: 名字 -o: or

#下面管道链接的三个进程是兄弟进程的关系
sleep 1000 | sleep 2000 | sleep 3000# bash会对指令进行扫面,提前创建两个管道

5. 命名管道

5.1 命名管道的原理

  匿名管道只能用于拥有血缘关系得进程间的通信,当有两个毫无关系的进程需要进程通信时,操作系统提供了另一种新的管道通信方式,本身拥有名字的管道,即命名管道。
在这里插入图片描述
  两个进程分别用读写模式打开同一个对应的管道文件,其中文件本身的内容属性不需要出现两次,所以两个struct file结构体会进行文件属性信息的共享。因为管道文件只是被用来作为进程间通信的桥梁,其中内容没有永久性存储的意义,其内容不需要持久化刷新到磁盘上。

5.2 命名管道的使用

  命名管道需要我们通过系统指令mkfifo 管道名称(first in first out)去手动创建,管道具有先进先出的特性其实可以是做是一个队列。进程间通过管道通信时,对于命名管道的使用与查找是通过其路径 + 文件名的方式实现的。除开指令创建命名管道之外,还有通过代码来使用系统调用的方式去创建命名管道,系统调用接口具体,如下:

//参数1:创建管道文件的名称 参数2:打开管道文件的模式
//返回值:当创建成功时返回0,创建失败时返回-1
int mkfifo(const char* pathname, mode_t mode);

5.3 命名管道使用示例

Comm.h

#ifndef COMM_H
#define COMM_H
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <cerrno>
#include <unistd.h>
#include <fcntl.h>

using namespace std;

#define Path "./fifo"
#define Mode 0666

class Fifo
{
public:
	Fifo(const string& path)
		:_path(path)
	{
		umask(0);
		
		int n = mkfifo(_path.c_str(), Mode);
		if(n < 0)
		{
			cerr << "mkfifo failed " << errno << " errstring: " << strerror(errno) << endl;
		}
	}
	
	~Fifo()
	{
		int n = unlink(_path.c_str());
		if(n < 0)
		{
			cerr << "unlink failed " << errno << " errstring: " << strerror(errno) << endl;
		}
	}
private:
	string _path;
};
#endif

PipeServer.cc(读)

#include "Comm.h"

int main()
{	
	Fifo fifo(Path);

	int rfd = open(Path, O_RDONLY);
	if(rfd < 0)
	{
		cerr << "read open failed " << errno << " errstring: " << strerror(errno) << endl;
		return 1;
	}

	//写端未打开,读端open会阻塞住,直至写端打开后open才会返回
	cout << "open success" << endl;

	char buf[1024];
	while(true)
	{
		int n = read(rfd, buf, sizeof(buf) - 1);
		if(n > 0)
		{
			buf[n] = 0;
			cout << "Client say: " << buf << endl;
		}
		else if(n == 0)
		{
			cout << "server quit me too!" << endl;
			break;
		}
		else
		{
			cerr << "read failed " << errno << " errstring: " << strerror(errno) << endl;
			break;
		}
	}

	close(rfd);
	
	return 0;
}

PipeClient.cc(写)

#include "Comm.h"

int main()
{
	//打开
	int wfd = open(Path, O_WRONLY);
	if(wfd < 0)
	{
		cerr << "write open failed " << errno << " errstring: " << strerror(errno) << endl;
		return 1;
	}

	//写入数据
	string inbuf;
	while(true)
	{
		cout << "Please Enter: ";
		getline(cin, inbuf);
		int n = write(wfd, inbuf.c_str(), inbuf.size());
		if(inbuf == "quit")
		{	
			cout << "PipeServer quit!" << endl;
			break;	
		}
		
		if(n < 0)
		{
			cerr << "write failed " << errno << " errstring: " << strerror(errno) << endl;
			break;
		}
	}
	
	close(wfd);

	return 0;
}

makefile

.PHONY:all
all:pipeserver pipeclient

pipeserver:PipeServer.cc
	g++ -o $@ $^ -std=c++11
pipeclient:PipeClient.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -rf pipeserver pipeclient fifo

相关文章:

  • HarmonyOS:GridObjectSortComponent(两个Grid之间网格元素交换)
  • 微软下一个大更新:Windows 11 25H2或已在路上!
  • CSS(八)
  • Linux笔记---动静态库(使用篇)
  • 全书测试:《C++性能优化指南》
  • 如何在 Postman 中配置并发送 JSON 格式的 POST 请求?
  • ‌国产芯片解析:龙迅HDMI发射机系列产品详解
  • 【C++】内存模型分析
  • Cherry Studio开源程序 是一个支持多个LLM提供商的桌面客户端。支持 deepseek-r1,可在 Windows、Mac 和 Linux 上使用
  • 数据库基础知识点(系列六)
  • 遍历整个列表
  • 天梯赛测试题2(L1答案及其解析)
  • .netCore的winform程序如何调用webapi
  • 软考笔记——软件工程基础知识
  • 未来技术的发展趋势与影响分析
  • dji飞行控制
  • AOA(到达角度)与TOA(到达时间)两个技术的混合定位,MATLAB例程,自适应基站数量,三维空间下的运动轨迹,滤波使用UKF(无迹卡尔曼滤波)
  • 7.5 窗体事件
  • [学成在线]07-视频转码
  • 链表-LeetCode
  • 蔡建忠已任昆山市副市长、市公安局局长
  • 《风林火山》千呼万唤始出来,戛纳首映后口碑崩盘?
  • 混乱的5天:俄乌和谈如何从充满希望走向“卡壳”
  • 上海率先推进生物制品分段生产试点,这款国产1类创新药获批上市
  • 钕铁硼永磁材料龙头瞄准人形机器人,正海磁材:已向下游客户完成小批量供货
  • 《求是》杂志发表习近平总书记重要文章《锲而不舍落实中央八项规定精神,以优良党风引领社风民风》