【Linux】进程通信----管道通信

server/2024/12/23 7:44:07/

> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:理解进程通信----管道通信

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:Linux初阶

> 望小伙伴们点赞👍收藏✨加关注哟💕💕

🌟前言

当我们创建两个进程,希望它们可以相互沟通,我的内容你可以读,我的内容你可以写,这种联系我们就需要我们的管道,这个管道就可以达成两个进程或者更多进程相互通信,那这个关系在Linux中是如何实现的呢,我们又该如何理解呢?

⭐主体

学习【Linux】静态库和动态库咱们按照下面的图解:

🌙 什么是进程间通信


💫 进程间通信介绍

什么是进程间通信?

进程具有独立性,每个进程都有自己的PCB,所以进程间需要通信,并且通信的成本一定不低(通信的本质:OS需要直接或者间接给通信双方的进程提供“内存空间”,并且要通信的进程,必须看到一份公共的资源)

图解:

如何去通信

  • 采用标准的做法:System V进程间通信(聚焦在本地通信,如共享内存)、POSIX进程间通信(让通信过程可以跨主机)。
  • 采用文件的做法:管道-基于文件系统(匿名管道、命名管道)

管道通信的特点:

是面向字节流、占用内存空间、只能单向传输、有固定的大小和缓冲区等。

总结:

而我们所说的不同通信种类本质就是:上面所说的资源,是OS中的哪一个模块提供的。如文件系统提供的叫管道通信;OS对应的System V模块提供的…,(成本不低是因为我们需要让不同的进程看到同一份资源。


💫 进程间通信目的

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程)

为什么要有进程间通信?

有时候我们需要多进程协同的,完成某种业务内容。比如管道。

图解:


💫 进程间通信分类

进程间通信分类:(匿名管道和命名管道

  • 匿名管道是只能在父子进程间使用的,它通过pipe()函数创建,并返回两个文件描述符,一个用于读,一个用于写。
  • 命名管道是可以在任意进程间使用的,它通过mkfifo()或mknod()函数创建一个特殊的文件,然后通过open()函数打开,并返回一个文件描述符,用于读或写。

🌙 管道的实现和理解


💫 管道介绍

概念:

管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"

分析:

任何一个文件包括两套资源:1.file的操作方法 2.有属于自己的内核缓冲区,所以父进程和子进程有一份公共的资源:文件系统提供的内核缓冲区,父进程可以向对应的文件的文件缓冲区写入,子进程可以通过文件缓冲区读取,此时就完成了进程间通信,这种方式提供的文件称为管道文件。管道文件本质就是内存级文件,不需要IO。

两个进程如何看到同一个管道文件:fork创建子进程完成

管道创建时分别以读和写方式打开同一个文件(如果只读或者只写,子进程也只会继承只读或只写,父子双方打开文件的方式一样,无法完成单向通信);父进程创建子进程,父进程以读写打开,子进程也是以读写打开(一般而言,管道只用来进行单向数据通信);关闭父子进程不需要的文件描述符,完成通信。


💫 匿名管道

概念:

我们通过文件名区分文件,但是如果当前进程的文件没有名字,这样的内存级文件称为匿名管道。让两个进程看到同一个文件,通过父进程创建子进程,子进程继承文件地址的方式,看到同一个内存级文件,此时内存级文件没有名称就是匿名管道了。匿名管道能用来父进程和子进程之间进行进程间通信。


1.pipe

作用及其使用:

  • 创建一个管道只需要调用pipe,注意头文件,返回值,以及函数的参数。
  • 头文件为#include <unistd.h>,调用成功返回0,调用失败返回-1。参数是输出型参数。
SYNOPSIS#include <unistd.h>int pipe(int pipefd[2]);
DESCRIPTIONpipe() creates a pipe,pipefd[0]  refers  to  the  read end of the pipe.  pipefd[1] refers to the write end of the pipe.
RETURN VALUEOn success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

创建Makefile文件

mypipe:mypipe.ccg++ mypipe.cc -o mypipe.PHONY:clean
clean:rm -f mypipe

创建管道文件,打开读写端

#include <iostream>
#include <unistd.h>
#include <cassert>using namespace std;
int main()
{int fds[2];int n = pipe(fds);assert(n == 0);//0,1,2->3,4//[0]:读取  [1]:写入cout<<"fds[0]:"<<fds[0]<<endl;//3cout<<"fds[1]:"<<fds[1]<<endl;//4return 0;
}

因此,fds[0]:3代表读取,fds[1]:4代表写入;

fork子进程:

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{int fds[2];int n = pipe(fds);assert(n == 0);//forkpid_t id = fork();assert(id>=0);if(id==0){//子进程通信exit(0);}//父进程通信n = waitpid(id,nullptr,0);assert(n==id);return 0;
}

关闭父子进程不需要的文件描述符,完成通信(子进程写入,父进程读取)

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using namespace std;int main()
{int fds[2];int n = pipe(fds);assert(n == 0);//forkpid_t id = fork();assert(id>=0);if(id==0){//子进程通信:子进程进行写入,关闭读close(fds[0]);//通信const char*s = "这是子进程,正在进行通信";int cnt = 0;while(true){cnt++;char buffer[1024];snprintf(buffer,sizeof buffer,"child->parent say:%s[%d][%d]",s,cnt,getpid());//写端写满的时候,在写会阻塞,等对方进行读取write(fds[1],buffer,strlen(buffer));//系统接口sleep(1);//一秒写一次}//退出前关闭子进程close(fds[1]);exit(0);}//父进程通信:父进程进行读取,关闭写close(fds[1]);//通信while(true){char buffer[1024];//管道中如果没有数据,读端在读,默认会直接阻塞当前正在读取的进程ssize_t s = read(fds[0],buffer,sizeof(buffer)-1);if(s>0) buffer[s] = 0;cout<<"Get Message# "<<buffer<<"|mypid:"<<getpid()<<endl;}n = waitpid(id,nullptr,0);assert(n==id);//结束前关闭close(fds[0]);return 0;
}

2.读写特征

管道读写特征:

  • 1.读快写慢

分析:

  • 子进程休眠时,不在写入,父进程在读取(如果管道中没有数据,读端在读,此时默认会直接阻塞当前正在读取的进程)

代码:

#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main()
{int fds[2];int n = pipe(fds);assert(n == 0);pid_t id = fork();assert(id >= 0);if (id == 0) // 子进程{// 子进程通信,关闭子进程的读取端,即子进程进行写入close(fds[0]);const char *s = "你好,我是子进程,正在进行通信";int cnt = 0;while (1){cnt++;char buffer[1024];snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));sleep(50); // 每一秒写一次}close(fds[1]); // 退出子进程前关闭文件写入端exit(0);}// 父进程close(fds[1]); // 父进程关闭写入端,即父进程进行读取while (1){char buffer[1024];printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);printf("888888888888888888888888888888888888!!\n");if (s > 0)buffer[s] = 0;printf("Get Message : %s | mypid = %d\n", buffer, getpid());}n = waitpid(id, NULL, 0);assert(n == id);close(fds[0]); // 退出程序前,关闭读取端return 0;
}

  • 2.读慢写快

分析:

  • 读取管道的进程一直不进行读取,而写端一直在写入。写端可以向管道内写入,但是管道是固定大小的缓冲区,不断的只写不读管道会被写满。满了以后就不能再写入了,此时写端会处于阻塞状态。

文件mypipe.cc

#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>int main()
{int fds[2];int n = pipe(fds);assert(n == 0);pid_t id = fork();assert(id >= 0);if (id == 0) // 子进程{// 子进程通信,关闭子进程的读取端,即子进程进行写入close(fds[0]);const char *s = "你好,我是子进程,正在进行通信";int cnt = 0;while (1){cnt++;char buffer[1024];snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));printf("count: %d\n", cnt);}close(fds[1]); // 退出子进程前关闭文件写入端exit(0);}// 父进程close(fds[1]); // 父进程关闭写入端,即父进程进行读取while (1){sleep(50); // 父进程不读char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);printf("Get Message : %s | mypid = %d\n", buffer, getpid());}n = waitpid(id, NULL, 0);assert(n == id);close(fds[0]); // 退出程序前,关闭读取端return 0;
}

拓展:

如果休息sleep(2),这种情况,写端是将数据塞到管道内,管道读取是安装指定大小读取(并非一行一行的读取,最初安装一行来读取是因为写入的慢,一次只写一行数据,数据就被读取了)。

  • 3.写入关闭,读到0

子进程写入端关闭:

#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main()
{int fds[2];int n = pipe(fds);assert(n == 0);pid_t id = fork();assert(id >= 0);if (id == 0) // 子进程{// 子进程通信,关闭子进程的读取端,即子进程进行写入close(fds[0]);const char *s = "你好,我是子进程,正在进行通信";int cnt = 0;while (1){cnt++;char buffer[1024];snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));printf("count: %d\n", cnt);break;}close(fds[1]); // 退出子进程前关闭文件写入端exit(0);}// 父进程close(fds[1]); // 父进程关闭写入端,即父进程进行读取while (1){sleep(2); // 父进程不读char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0;printf("Get Message : %s | mypid = %d\n", buffer, getpid());}else if (s == 0) // 写入端关闭,读到文件末尾了{printf("read: %d\n", s);break; // 关闭读取端}}n = waitpid(id, NULL, 0);assert(n == id);close(fds[0]); // 退出程序前,关闭读取端return 0;
}

  • 4. 读取端关闭,写入端直接关闭

关闭读取端后,写入端就没有意义了,因此OS会给写入的进程发送信号,终止该进程。

#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main()
{int fds[2];int n = pipe(fds);assert(n == 0);pid_t id = fork();assert(id >= 0);if (id == 0) // 子进程{// 子进程通信,关闭子进程的读取端,即子进程进行写入close(fds[0]);const char *s = "你好,我是子进程,正在进行通信";int cnt = 0;while (1){cnt++;char buffer[1024];snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));printf("count: %d\n", cnt);}close(fds[1]); // 退出子进程前关闭文件写入端printf("子进程关闭写入端\n");exit(0);}// 父进程close(fds[1]); // 父进程关闭写入端,即父进程进行读取while (1){sleep(2); // 父进程不读char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0;printf("Get Message : %s | mypid = %d\n", buffer, getpid());}break; // 关闭读取端}close(fds[0]); // 退出程序前,关闭读取端printf("父进程关闭读取端\n");n = waitpid(id, NULL, 0);assert(n == id);return 0;
}

3.管道特征

1.管道的生命周期随进程,进程退出,管道释放

2.管道可以用来进行具有血缘关系的进程间通信(常用于父子通信)

3.管道是面向字节流的

4.半双工—单向通信(特殊)

5.互斥与同步机制——对共享资源进行保护的方案

💫 命名管道

概念:

我们前面已经知道:匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。那如果两个毫不相干的进程间通信交互呢?如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件

1.mkfifo

使用说明

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

在当前路径下直接创建命名管道:

mkfifo named_pipe

往管道文件写东西:


2.创建管道文件

分为三个文件:comm.hpp:公共文件(同一份资源),server.cc:读取端,clinet.cc:写入端

comm.hpp文件(同一份资源)

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <cassert>#define NAMED_PIPE "/tmp/mypipe.name"bool createFifo(const std::string &path)
{umask(0);int n = mkfifo(path.c_str(),0666);if(n==0) return true;else{std::cout<<"errno:"<<errno<<"err string:"<<strerror(errno)<<std::endl;return false;}
}

server.cc:

#include "comm.hpp"
int main()
{bool ret = createFifo(NAMED_PIPE);assert(ret);(void)ret;return 0;
}

运行:


3.删除管道文件

使用说明

unlink   注意头文件,函数的参数以及返回值这三个主要部分:

NAMEunlink - remove a directory entrySYNOPSIS#include <unistd.h>int unlink(const char *path);
RETURN VALUEUpon successful completion, 0 shall be returned. Otherwise, -1 shall be returned and errno set to indicate the error. If -1 is returned, the named file shall not be changed.

在comm.hpp中封装好删除的函数:

void removeFifo(const std::string &path)
{int n = unlink(path.c_str());assert(n==0);(void)n;//防止n没使用而警告
}

在server.cc中进行调用:

#include "comm.hpp"int main()
{bool ret = createFifo(NAMED_PIPE);assert(ret);(void)ret;removeFifo(NAMED_PIPE);return 0;
}

至此,创建和删除管道文件的操作我们实现完毕。下面进入通信环节

4.通信

说明:

其实在了解完了匿名管道之后,对于命名管道我们能够更好的理解:

client.cc(写端):

#include "comm.hpp"int main()
{int wfd = open(NAMED_PIPE,O_WRONLY);if(wfd<0) exit(1);//writechar buffer[1024];while(true){std::cout<<"Please Say:";fgets(buffer,sizeof(buffer),stdin);//if(strlen(buffer)>0) buffer[strlen(buffer)-1] = 0;ssize_t n = write(wfd,buffer,strlen(buffer));assert(n==strlen(buffer));(void)n;}close(wfd);return 0;
}

server.cc(读端):

#include "comm.hpp"int main()
{bool ret = createFifo(NAMED_PIPE);assert(ret);(void)ret;int  rfd = open(NAMED_PIPE,O_RDONLY);if(rfd<0) exit(1);//readchar buffer[1024];while(true){ssize_t s = read(rfd,buffer,sizeof(buffer)-1);if(s>0){buffer[s] = 0;std::cout<<"client->server" <<buffer<<std::endl;}else if(s==0){std::cout<<"client quit,俺也一样"<<std::endl;break;}else{std::cout<<"err string:"<<strerror(errno)<<std::endl;break;}}close(rfd);removeFifo(NAMED_PIPE);return 0;
}

进行通信:

读端多出一行空行:写端输入之后多按了回车,修改为buffer[strlen(buffer)-1] = 0;

if(strlen(buffer) > 0) buffer[strlen(buffer) - 1] = 0;

🌙 管道通信的优化

管道通信的优点有以下几点:

  • 管道通信是简单易用的,只需要使用系统调用 pipe 或 mkfifo 就可以创建一个管道文件,然后使用文件操作函数来读写数据。
  • 管道通信是安全的,匿名管道只能用于具有亲缘关系的进程间通信,命名管道可以通过文件权限来控制访问。
  • 管道通信是面向字节流的,不需要事先约定数据的格式,也不需要考虑字节序的问题。

管道通信的缺点有以下几点:

  • 管道通信是单向的,如果要实现双向通信,需要创建两个管道。
  • 管道通信是阻塞式的,如果读端没有数据可读或者写端没有空间可写,进程会被阻塞。
  • 管道通信是缓冲区有限的,如果写入数据过多而读出数据过少,会导致缓冲区满而无法继续写入。
  • 管道通信是不可靠的,如果读端或者写端被关闭,另一端可能会收到错误的信号或者返回值。

改进管道通信性能和效率的方法有以下几点:

  • 使用双向管道,可以实现两个进程之间的双向通信,而不需要创建两个单向管道。双向管道可以通过 socketpair 系统调用来创建,返回两个文件描述符,分别表示管道的两端。
  • 使用非阻塞模式,可以避免进程在读写管道时被阻塞,提高并发性能。非阻塞模式可以通过 fcntl 系统调用来设置文件描述符的 O_NONBLOCK 标志。
  • 使用自定义协议,可以根据通信的需求和场景,设计合适的数据格式和交互方式,提高数据传输的效率和可靠性。自定义协议可以包括数据包的长度、类型、校验码等信息。
  • 调整管道缓冲区的大小,可以根据数据量的大小和频率,选择合适的缓冲区大小,避免缓冲区溢出或者空闲浪费。管道缓冲区的大小可以通过 fcntl 系统调用来设置 F_SETPIPE_SZ 标志,并且可以通过 /proc/sys/fs/pipe-max-size 来修改最大容量。

🌟结束语 

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​ 


http://www.ppmy.cn/server/35567.html

相关文章

深度神经网络中的不确定性研究综述

A.单一确定性方法 对于确定性神经网络&#xff0c;参数是确定的&#xff0c;每次向前传递的重复都会产生相同的结果。对于不确定性量化的单一确定性网络方法&#xff0c;我们总结了在确定性网络中基于单一正向传递计算预测y *的不确定性的所有方法。在文献中&#xff0c;可以找…

初识JDBC

1、JDBC是什么&#xff1f; Java DataBase Connectivity(Java语言连接数据库) 2、JDBC的本质是什么&#xff1f; JDBC是SUN公司制定的一套接口(interface) java.sql.*;(这个包下有很多接口) 接口都有调用者和实现者。 面向接口调用、面向接口写实现类&#xff0c;这都属于…

docker-compose docker的批量管理工具

安装 apt-get install docker-compose -y # 查看版本 docker-compose -v设置容器自启动&#xff1a;–restartalways docker run -it -d --restartalways nginx:1.16docker-compose.yml配置文件&#xff0c;该文件的缩进必须是三个空格&#xff0c;使用的是yaml语法。 versi…

工厂模式+策略模式完成多种登录模式的实现

前提 &#xff08;简单工厂不属于设计模式&#xff0c;而是一种编程思想【抽象一层出来】&#xff09;工厂方法模式、抽象工厂模式 以上都是为了解耦&#xff0c;如果考虑多个纬度&#xff08;如需要同时考虑多种电器&#xff0c;多种品牌&#xff09;则优先考虑抽象工厂。 …

Python tutorial 2.7.13第四章(深入 Python 流程控制)

4. 深入 Python 流程控制 除了前面介绍的 while 语句&#xff0c;Python 还从其它语言借鉴了一些流程控制功能&#xff0c;并有所改变。 4.1. if 语句 也许最有名的是 if 语句。例如: >>> x int(raw_input("Please enter an integer: ")) Please enter…

自动化运维管理工具-------------Ansible

目录 一、自动化运维工具有哪些&#xff1f; 1.1Chef 1.2puppet 1.3Saltstack 二、Ansible介绍 2.1Ansible简介 2.2Ansible特点 2.3Ansible工作原理及流程 2.3.1内部流程 2.3.2外部流程 三、Ansible部署 3.1环境准备 3.2管理端安装 ansible 3.3Ansible相关文件 …

权益商城系统源码 现支持多种支付方式

简介&#xff1a; 权益商城系统源码&#xff0c;支持多种支付方式&#xff0c;后台商品管理&#xff0c;订单管理&#xff0c;串货管理&#xff0c;分站管理&#xff0c;会员列表&#xff0c;分销日志&#xff0c;应用配置。 上传到服务器&#xff0c;修改数据库信息&#xff…

链表经典面试题下

目录 如有帮助&#xff0c;还望三连支持&#xff0c;谢谢&#xff01;&#xff01;&#xff01; 题目一&#xff1a;141. 环形链表 - 力扣&#xff08;LeetCode&#xff09; 题目二&#xff1a;142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; 题目三&#xff1a;…