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;
}


