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

Linux入门:匿名管道命名管道

目录

一.进程间通信介绍

一).进程间通信的目的

二).进程间通信分类

二.管道

三.匿名管道

一).基本介绍

二).用 fork 来共享管道原理

三).文件描述符角度理解管道

四).内核角度理解管道

五).匿名管道读写规则

六).匿名管道特点

七).匿名管道模拟实现进程池

四.命名管道

一).命名管道操作

二).匿名管道与命名管道的区别

三).命名管道的打开规则

四).命名管道的实例代码

1.用命名管道实现文件拷贝

2.用命名管道实现server&client通信



一.进程间通信介绍

一).进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另⼀个进程的所有陷入和异常,并能够及时知道它的状态改变。

二).进程间通信分类

  • 管道:匿名管道(pipe),命名管道
  • System V IPC:System V消息队列,System V共享内存,System V信号量
  • POSIX IPC:消息队列,,共享内存,信号量,互斥量,条件变量,读写锁

进程间通信的本质是:不同的进程看到同一份资源(内存),然后才有通信的条件。

资源由OS提供,使用系统调用。


二.管道

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

上图表示,将who进程输出的结果给到 wc -l 进程进行处理,然后输出对应输出。其中 “|” 就是管道。who 命令显示关于当前在本地系统上的所有登录用户的信息,而 wc -l 统计文本文件的行数


三.匿名管道

一).基本介绍

匿名管道是用于具有血缘关系间进程通信的一种方式,常用与父子进程。匿名管道也是一种内存级的文件,所以当进程打开该管道时,也会有对应的文件描述符

pipefd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端

返回值:成功返回0,失败返回错误代码

一般的进程的文件描述符0,1,2分别代表的是标准输入,标准输出,标准错误。所以调用pipe函数的进程给匿名管道分配的文件描述符一般为3,4,。3是读端,4是写端

下面的代码展示了管道的用法:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main()
{int fds[2];char buf[100];int len;if(pipe(fds)==-1) perror("make pipe"),exit(1);//从标准输入读取信息while(fgets(buf,100,stdin)){len=strlen(buf);//将信息写到管道中if(write(fds[1],buf,len)!=len){perror("write to pipe");break;}memset(buf,0x00,sizeof(buf));//从管道读取信息if((len=read(fds[0],buf,100))==-1){perror("read form pipe");break;}//将信息写到标准输出if(write(1,buf,len)!=len){perror("write to stdout");break;}}
}

二).用 fork 来共享管道原理

通过3步形成共享管道

1. 创建管道文件

2. 创建子进程

3. 关闭不需要的读写端,形成通信信道

下面代码:子进程向管道文件中不断写入数据,然后父进程读取子进程写入的数据输出到标准输出中:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>void ChildWrite(int wfd)
{char buffer[1024];int cnt = 0;while(true){snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);write(wfd, buffer, strlen(buffer));sleep(1);}
}void FatherRead(int rfd)
{char buffer[1024];while(true){buffer[0] = 0;ssize_t n = read(rfd, buffer, sizeof(buffer)-1);if (n > 0){buffer[n] = 0;std::cout << "child say: " << buffer << std::endl;}}
}int main()
{//1. 创建管道文件int fds[2] = {0};int i = pipe(fds);if (i < 0){std::cout << "创建管道失败" << std::endl;return 1;}// 2. 创建子进程pid_t id = fork();if (id == 0){// child// 子进程关闭读端close(fds[0]);ChildWrite(fds[1]);}//3. 关闭不需要的读写端,形成通信信道//父进程关闭写端close(fds[1]);FatherRead(fds[0]);waitpid(id, nullptr, 0);close(fds[0]);return 0;
}

三).文件描述符角度理解管道

父进程中创建管道文件,给其分配两个文件描述符,然后父进程中在创建一个子进程,子进程的文件描述符和父进程相同。分别关闭父进程的读端和子进程的写端,就能形成一个父进程写,子进程读的通信信道。

四).内核角度理解管道

看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”

五).匿名管道读写规则

1.当没有数据可读时(写慢,读快)

  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

2.当管道满的时候(写快,读慢)

  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

3.如果所有管道写端对应的文件描述符被关闭,则read返回0,表示文件结尾(写关,读继续)

4.如果所有管道读端对应的文件描述符被关闭,则write操作会产生异常信号13SIGPIPE,进而可能导致write进程退出(读关闭,写继续)

5.当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性

6.当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性

六).匿名管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道
  • 管道提供流式服务(面向字节流的
  • 进程退出,管道释放,所以管道的生命周期随进程
  • 内核会对管道操作进行同步与互斥,自带同步机制
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

七).匿名管道模拟实现进程池

模拟实现目标:父进程是写端,子进程是读端,父进程通过管道给子进程发送任务码,然后子进程解析父进程发来的任务码执行相应的任务。

我们要知道,父进程创建子进程时,子进程是继承父进程的文件描述符表的。

第一次父进程创建管道文件时,占用3,4 文件描述符,但是父进程为写端,所以关闭3号文件描述符,而子进程1关闭4号文件描述符;第二次创建管道文件时,因为4号文件描述符指向了管道文件1的写端,所以父进程占用3,5文件描述符,这是父进程3号文件描述符指向管道文件2的读端,4号文件描述指向管道文件1的写端,5号文件描述指向管道文件2的写端,创建子进程2时,子进程继承父进程的文件描述符表,所以将不要的文件描述符关闭之后,父进程的4,5号文件描述符分别指向管道文件1的写端和管道文件2的写端,但是子进程2的文件描述符表中,3号文件描述符指向管道文件2的读端,而4号文件描述符指向管道文件1的写端。同理,子进程3也一样。

代码模拟实现:

ProcessPool.hpp是将.h文件和.cpp文件结合起来,直接在文件中声明并定义类和函数了。这个文件主要包含Channel类和ChannelManager类和ProcessPool类。Channel类主要是对进程池中的每一个信道的信息进行描述。ChannelManager类是将每一个信道统一管理起来的类。ProcessPool类是进程池类,里面包含了进程池中通道的个数,ChannelManager对象以及管理分配给子进程的任务对象TaskManager类对象。

//Process.hpp#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__#include <iostream>
#include <cstdlib> 
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include "Task.hpp"//  描述一个信道的类,从父进程视角看,只需要看到一个信道中管道的写端描述符和对应子进程的pid
class Channel
{
public:Channel(int fd, pid_t id) : _wfd(fd), _subid(id){_name = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid);}~Channel(){}void Send(int code){int n = write(_wfd, &code, sizeof(code));(void)n; // ?}void Close(){close(_wfd);}void Wait(){pid_t rid = waitpid(_subid, nullptr, 0);(void)rid;}int Fd() { return _wfd; }pid_t SubId() { return _subid; }std::string Name() { return _name; }private:
// 一个信道的写端文件描述符,子进程pid以及名字int _wfd;pid_t _subid;std::string _name;
};//  一个管理进程池的类,管理信道
class ChannelManager
{
public:ChannelManager() : _next(0){}void Insert(int wfd, pid_t subid){_channels.emplace_back(wfd, subid);}Channel &Select(){auto &c = _channels[_next];_next++;_next %= _channels.size();return c;}void PrintChannel(){for (auto &channel : _channels){std::cout << channel.Name() << std::endl;}}void StopSubProcess(){for (auto &channel : _channels){channel.Close();std::cout << "关闭: " << channel.Name() << std::endl;}}void WaitSubProcess(){for (auto &channel : _channels){channel.Wait();std::cout << "回收: " << channel.Name() << std::endl;}}~ChannelManager() {}private:std::vector<Channel> _channels;int _next;
};//  描述一个进程池的类,父子进程与管道形成信道
class ProcessPool
{
public://  初始化进程池进程个数以及注册各种任务ProcessPool(int num) : _process_num(num){_tm.Register(PrintLog);_tm.Register(Download);_tm.Register(Upload);}//  子进程进行读任务void Work(int rfd){while (true){int code = 0;ssize_t n = read(rfd, &code, sizeof(code));if (n > 0){if (n != sizeof(code)){continue;}std::cout << "子进程[" << getpid() << "]收到一个任务码: " << code << std::endl;_tm.Excute(code);}else if (n == 0){std::cout << "子进程退出" << std::endl;break;}else{std::cout << "读取错误" << std::endl;break;}}}//  创建并启动进程池bool Start(){for (int i = 0; i < _process_num; i++){// 1. 创建管道int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0){return false;}// 2. 创建子进程pid_t subid = fork();if (subid < 0){return false;}//  3. 关闭不需要的文件描述符else if (subid == 0){// 子进程close(pipefd[1]);Work(pipefd[0]); close(pipefd[0]);exit(0);}else{// 父进程close(pipefd[0]); _cm.Insert(pipefd[1], subid);}}return true;}void Debug(){_cm.PrintChannel();}void Run(){// 1. 选择一个任务int taskcode = _tm.Code();// 2. 选择一个信道[子进程],负载均衡的选择一个子进程,完成任务auto &c = _cm.Select();std::cout << "选择了一个子进程: " << c.Name() << std::endl;// 2. 发送任务c.Send(taskcode);std::cout << "发送了一个任务码: " << taskcode << std::endl;}void Stop(){// 关闭父进程所有的wfd即可_cm.StopSubProcess();// 回收所有子进程_cm.WaitSubProcess();}~ProcessPool(){}private:ChannelManager _cm;int _process_num;Taskmanager _tm;
};#endif

Task.hpp中定义了TaskManager类,用于管理子进程需要执行的任务。

//Task.hpp#pragma once//用于管理子进程需要执行的任务
#include <iostream>
#include <vector>
#include <ctime>typedef void (*task_t)();void PrintLog()
{std::cout << "打印日志的任务" << std::endl;
}void Download()
{std::cout << "下载的任务" << std::endl;
}void Upload()
{std::cout << "上传的任务" << std::endl;
}class Taskmanager
{
public:Taskmanager(){srand(time(nullptr));}//注册任务void Register(task_t t){_tasks.push_back(t);}int Code(){return rand()%_tasks.size();}void Excute(int code){if (code >= 0 && code < _tasks.size()){_tasks[code]();}}private:std::vector<task_t> _tasks;
};

main.cc中是整个进程池运行的主要流程,一是创建出进程池对象ProcessPool,二是启动并创建进程池,三是给进程池中的子进程派发任务并执行,四是关闭指向子进程的文件描述符,子进程退出之后,通过waitpid回收子进程。

//main.cc#include "Process.hpp"const int gdefaultnum = 5;int main()
{// 创建进程池对象ProcessPool pp(gdefaultnum);// 启动进程池pp.Start();// 自动派发任务int cnt = 10;while(cnt--){pp.Run();sleep(1);}// 回收,结束进程池pp.Stop();return 0;
}

代码结果:

可以看到,这里循环分配任务结束之后,父进程关闭了指向管道文件写端的文件描述符,然后子进程从后到前依次退出,最后回收子进程程序结束。

从上述结果可以看到,通信信道是依次关闭的,子进程也是依次回收的,但是子进程退出的顺序是反的。这是为什么呢?

因为管道的生命周期是随进程的,面对下面图片这个问题,前面的管道文件的写端,除了会被父进程的文件描述符指向外,都会被它的弟弟进程的文件描述符指向,所以,当父进程关闭指向管道文件的文件描述符之后,前面管道文件都有弟弟进程的文件描述符指向,所以进程还是会阻塞在read函数那等待输入,无法关闭。

但是最后一个管道文件的写端则没有后面进程的文件描述符指向,所以最后一个子进程的read函数返回0,然后子进程退出,当该子进程退出之后,它的上一个子进程对应的管道文件的写端也就没有进程指向,所以上一个子进程也接着退出,这样依次,从后到前,所以子进程依次退出,所以也就出现倒序退出。

除了倒着关闭还有什么方法吗?

有的兄弟,有的。

我们可以主动关闭继承下来的写端,在代码上加上下面代码。

子进程的_cm也是继承自父进程,当父进程对_cm进行插入时,发生写时拷贝,所以后面的子进程中的_cm里面,只有指向哥哥进程对应的管道文件写端的文件描述符,所以可以实现在创建子进程的时候就可以进行关闭

重新运行就可以发现:


四.命名管道

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

一).命名管道操作

  • 创建管道:
    mkfifo filename

  • 删除管道:
    rm filename 或 unlink filename
  • 程序中创建命名管道

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>int main()
{mkfifo("pipe", 0666);return 0;
}

二).匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义

三).命名管道的打开规则

  •         如果当前打开操作是为读而打开FIFO时:阻塞直到有相应进程为写而打开该FIFO。
  •         如果当前打开操作是为写而打开FIFO时:阻塞直到有相应进程为读而打开该FIFO

四).命名管道的实例代码

1.用命名管道实现文件拷贝

代码通过命名管道实现了一个从文件中读取内容,然后写入到中


//read.cc -- 读取文件,写入命名管道中#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>//  封装一下错误退出的宏
#define ERR_EXIT(m) \
do \
{   \perror(m);  \exit(EXIT_FAILURE);  \
} while(0)//  读取文件,写入命令管道
int main()
{//  1. 创建命名管道文件fifoint ret = mkfifo("fifo", 0664);if (ret < 0){ERR_EXIT("mkfifo");}//  2. 以读方式打开待读文件int infd = open("te", O_RDONLY);if (infd == -1)ERR_EXIT("open");//  3. 以写方式打开命名管道fifoint outfd;outfd = open("fifo", O_WRONLY);if (outfd == -1)ERR_EXIT("open");//  4.从待读文件中读取数据,写入管道文件中char buf[1024];int n;while((n = read(infd, buf, 1024)) > 0){write(outfd, buf, n);}close(infd);close(outfd);return 0;
}
//write.cc -- 读取管道文件,写入目标文件中#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define ERR_EXIT(m) \
do \
{   \perror(m); \exit(EXIT_FAILURE); \
}while(0)//读取管道文件,写入文件中
int main()
{umask(0);//  1. 以读的方式打开管道文件fifoint infd = open("fifo", O_RDONLY, 0666);if (infd == -1)ERR_EXIT("open");//  2. 以写方式打开目标文件textint outfd = open("text", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (outfd == -1){ERR_EXIT("open");}//  3.从管道文件fifo中读数据并写入到目标文件中char buf[1024];int n;while((n = read(infd, buf, 1024)) > 0){write(outfd, buf, n);}close(infd);close(outfd);// 删除管道文件unlink("fifo");return 0;
}

2.用命名管道实现server&client通信

server作为读端,将client写入到管道文件的数据读出,显示到显示器上

// comm.hpp -- 用于描述管道文件的类和用于描述对管道文件操作的类
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>//设置为当前路径
#define PATH "."
#define NAME "fifo"#define ERR_EXIT(m)         \do                      \{                       \perror(m);          \exit(EXIT_FAILURE); \}while (0)class namefifo
{
public:namefifo(const std::string path, const std::string name): _path(path), _name(name){// 创建管道文件_filename = _path + "/" + _name;umask(0);int n = mkfifo(_filename.c_str(), 0664);if (n < 0)ERR_EXIT("mkfifo");else{std::cout << "mkfifo success" << std::endl;}}~namefifo(){int n = unlink(_filename.c_str());// 删除成功if (n == 0){ERR_EXIT("unlink");}else    // 删除失败{std::cout << "remove fifo failed" << std::endl;}}private:std::string _path;std::string _name;std::string _filename;
};class namefile
{
public:namefile(const std::string path, const std::string name): _path(path), _name(name), _fd(-1){_filename = _path + "/" + _name;}void OpenForRead(){_fd = open(_filename.c_str(), O_RDONLY);if (_fd < 0)ERR_EXIT("open");else if (_fd > 0){std::cout << "open success" << std::endl;}else{}}void OpenForWrite(){_fd = open(_filename.c_str(), O_WRONLY);if (_fd < 0)ERR_EXIT("open");else if (_fd > 0){std::cout << "open success" << std::endl;}else{}}//  服务端进行读void Read(){char buf[1024];int n;while (true){n = read(_fd, buf, sizeof(buf) - 1);buf[n] = 0;if (n > 0){std::cout << "Client say # " << buf << std::endl;}else if (n == 0){std::cout << "Client quit! me too !" << std::endl;break;}else{// TODO}}}//  客户端进行写void Write(){std::string message;while (true){std::cin >> message;int n = 0;write(_fd, message.c_str(), message.size());}}void Close(){close(_fd);}private:std::string _path;std::string _name;std::string _filename;int _fd;
};
// client.cc#include "comm.hpp"int main()
{namefile fo(PATH, NAME);fo.OpenForWrite();fo.Write();fo.Close();return 0;
}
// server.cc#include "comm.hpp"int main()
{namefifo nf(PATH, NAME);namefile fo(PATH, NAME);fo.OpenForRead();fo.Read();fo.Close();return 0;
}

http://www.dtcms.com/a/521989.html

相关文章:

  • 网站制作标准福田网站建设乐云seo
  • 群晖做网站网站维护大概要多久
  • 网站下做二级域名wordpress存储插件
  • 凡科网站手机投票怎么做网站项目策划方案
  • 乐器产品主要在什么网站做推广做网站常用软件
  • 淄博张店做网站的公司广州网站建设外贸
  • 深圳网站建设制作设计公司大学生创业50个小案例
  • 网站用户注册怎么做wordpress模板打开慢
  • 宝塔织梦网站建设免费网店代运营
  • 模板做网站影响seo泰安建设厅网站
  • 天津企业模板建站嘉兴互联网公司
  • 云服务器能否虚拟多个ip,一个云服务器怎么弄多个ip?
  • 创建一个网站 站点根文件夹为百度ai智能搜索引擎
  • S7-200 SMART CPU通信全解析:工业自动化的无缝连接之道
  • 从化网站制作互联网公司十大排名
  • 平原做网站看上去高端的网站
  • 已经有了网站怎么做推广wordpress系统
  • qq头像网站源码花店网站建设课程设计
  • 外贸网站设计师网站在百度的标头不对
  • 做淘宝素材网站哪个好用英雄联盟手游小程序被投诉
  • 正能量晚上看的网站2021自动app优化
  • C++仿muduo库高并发服务器项目:Socket模块
  • 湖南网站优化上海网站设计
  • 电子商务网站的功能有哪些哪个网站做团购要求低点
  • 淄博网站定制网站制作出租
  • 做家具的企业网站wordpress搭建方案
  • 苏州网站建设专家郑州网站建设技术支持
  • STM32学习和实践笔记(43):CAN通信实验(1)
  • 企业网站模板 优帮云百度搜索排名购买
  • 编程项目实例网站61源码网