目录
进程间通信介绍
进程间通信目的
进程间通信发展
进程间通信分类
管道(基于文件)
System V IPC(基于本地通信,不能跨网络)
POSIX IPC
管道
什么是管道
匿名管道
匿名管道的原理
任何进程通信的手段
用fork来共享管道原理
站在文件描述符角度-深度理解管道
站在内核角度-管道本质
编辑
pipe函数
pipe2
进程通信的步骤
第一步,创建管道
第二步,创建子进程
第三步,关闭不需要的fd
第四步,开始通信
管道的特点
管道的四种场景
基于匿名管道设计一个小的进程池
命名管道
使用命令创建命名管道
创建一个命名管道
mkfifo
mkfifo函数的返回值:
命名管道的打开规则
命名管道的应用——客户端和服务端的通信
匿名管道与命名管道的区别
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
管道
System V进程间通信
POSIX进程间通信
匿名管道pipe
命名管道
System V 消息队列
System V 共享内存
System V 信号量
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
a.想办法,先让不同的进程,看到同一份资源
b.让一方写入,一方读取,完成通信过程,至于通信目的与后续工作,要结合具体场景
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
——创建子进程的时候fork子进程,只会复制进程相关的数据结构对象 ,不会复制父进程曾经打开的文件对象
现象:这就是为什么fork之后,父子进程都printf,cout,都会向同一个显示器终端打印数据的原因
#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
1、当没有数据可读时:
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
2、当管道满的时候:O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
用管道通信,大致分为四个步骤
查看一下管道文件是否创建成功
我们一般把pipefd[0]作为读端,pipefd[1]作为写端——0对应嘴巴用来读,1对应笔用来写
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 让不同的进程看到同一份资源!!!!
// 任何一种任何一种进程间通信中,一定要 先 保证不同的进程之间看到同一份资源
int pipefd[2] = {0};
//1. 创建管道
int n = pipe(pipefd);
if(n < 0)
{
std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;
return 1;
}
std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // 读端, 0->嘴巴->读书
std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // 写端, 1->笔->写东西的
//2. 创建子进程
pid_t id = fork();
assert(id != -1); //正常应该用判断,我这里就断言:意料之外用if,意料之中用assert
if(id == 0)// 子进程
{
//3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[0]);
//4. 开始通信 -- 结合某种场景
const std::string namestr = "hello ,我是子进程";
int cnt = 1;
char buffer[1024];
while(true)
{
snprintf(buffer, sizeof buffer, "%s, 计数器: %d, 我的PID: %d\n", namestr.c_str(), cnt++, getpid());
write(pipefd[1], buffer, strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
//3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[1]);
//4. 开始通信 -- 结合某种场景
char buffer[1024];
while(true)
{
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = '\0';
std::cout << "我是父进程, child give me message: " << buffer << std::endl;
}
}
close(pipefd[0]);
return 0;
}
这要一个简单的管道通信就写好了
这里有一个细节,在子进程写的时候每写一次sleep一秒
1.单向通信
2.管道的本质是文件,因为fd的生命周期随进程,管道的生命周期是随进程的
3.管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用与父子通信——pipe打开管道,并不清楚管道的名字,匿名管道
4.在管道通信中,写入的次数,和读取的次数不是严格匹配的,读写次数的多少没有强相关--表现--字节流
5.具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信--自带同步机制
1.如果我们read读取完毕了所有的管道数据,如果对方不发,我们只能等待
2.如果我们write端将管道写满了,我们还能写吗?不能
3.如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾
4.写端一直写,读端关闭,会发生什么呢?没有意义OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程!OS会通过信号来终止进程 13)SIGPIPE
父进程向子进程写入特定的消息,唤醒子进程,甚至让子进程定向的执行某周任务
创建进程部分
void createProcesses(vector *end_points)
{
vector fds;
for (int i = 0; i < gnum; i++)
{
// 1.1 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 1.2 创建进程
pid_t id = fork();
assert(id != -1);
// 一定是子进程
if (id == 0)
{
for(auto &fd : fds) close(fd);
// std::cout << getpid() << " 子进程关闭父进程对应的写端:";
// for(auto &fd : fds)
// {
// std::cout << fd << " ";
// close(fd);
// }
// std::cout << std::endl;
// 1.3 关闭不要的fd
close(pipefd[1]);
// 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
// 1.3.1 输入重定向,可以不做
dup2(pipefd[0], 0);
// 1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
// 一定是父进程
// 1.3 关闭不要的fd
close(pipefd[0]);
// 1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
在创建管道for循环中,在下一次循环管道的文件描述符会覆盖掉,作为父进程怎么知道哪一个进程对应哪一个管道呢?
所以定义一个类(或者结构体)来管理创建创建出来的子进程和管道,先描述在组织,
父进程需要的是有哪些子进程,哪些管道,子进程和管道的关系
再用一个vector容器来管理所有这个类
class EndPoint
{
public:
pid_t _child_id;//子进程的pid
int _write_fd;//向哪一个管道里写
public:
EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
{
}
~EndPoint()
{
}
};
子进程要执行的方法
void WaitCommand()
{
while (true)
{
int command = 0;
int n = read(0, &command, sizeof(int));
if (n == sizeof(int))
{
t.Execute(command);
}
else if (n == 0)
{
std::cout << "父进程让我退出,我就退出了: " << getpid() << std::endl;
break;
}
else
{
break;
}
}
}
总代码
Task.hpp
#pragma once
#include
#include
#include
// typedef std::function func_t;
typedef void (*fun_t)(); //函数指针
void PrintLog()
{
std::cout << "pid: "<< getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}
void InsertMySQL()
{
std::cout << "执行数据库任务,正在被执行..." << std::endl;
}
void NetRequest()
{
std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}
//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2
class Task
{
public:
Task()
{
funcs.push_back(PrintLog);
funcs.push_back(InsertMySQL);
funcs.push_back(NetRequest);
}
void Execute(int command)
{
if(command >= 0 && command < funcs.size()) funcs[command]();
}
~Task()
{}
public:
std::vector funcs;
};
ctrlProcess.cc
#include
#include
#include
#include
#include
#include "Task.hpp"
using namespace std;
const int gnum = 5;//表示未来一共想要创建几个子进程
Task t;
class EndPoint
{
public:
pid_t _child_id;
int _write_fd;
public:
EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
{
}
~EndPoint()
{
}
};
// 子进程要执行的方法
void WaitCommand()
{
while (true)
{
int command = 0;
int n = read(0, &command, sizeof(int));
if (n == sizeof(int))
{
t.Execute(command);
}
else if (n == 0)
{
break;
}
else
{
break;
}
}
}
void createProcesses(vector *end_points)
{
for (int i = 0; i < gnum; i++)
{
// 1.1 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 1.2 创建进程
pid_t id = fork();
assert(id != -1);
// 一定是子进程
if (id == 0)
{
// 1.3 关闭不要的fd
close(pipefd[1]);
// 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
// 1.3.1 输入重定向
dup2(pipefd[0], 0);
// 1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
// 一定是父进程
// 1.3 关闭不要的fd
close(pipefd[0]);
// 1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
}
}
int main()
{
// 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
vector end_points;
createProcesses(&end_points);
// 2. 我们的得到了什么?end_points
int num = 0;
while(true)
{
//1. 选择任务
int command = COMMAND_LOG;
//2. 选择进程
int index = rand()%end_points.size();
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
sleep(1);
}
return 0;
}
匿名管道只能用于具有血缘关系的进程,如父子进程来进行进程间通信,如果我想要让毫不相干的两个进程来进程通信呢?——这里就可以用到命名管道了
----------------------------------------------------
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
我们可以使用mkfifo
命令创建一个命名管道。
这里我从另一端输入,从另一端读取,这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
如果两个不相干的进程打开同一个文件,要不要再打开一个stuct file 结构体?
答案是不用,操作系统只会将指向改文件的引用计数ret加一
而且用管道文件写入读取不会进行刷盘,也没有对应的data block
如何保证两个毫不相关的进程,看到的是同一个文件并打开?
文件是有唯一性的,用路径表示
让不同的进程通过文件路径+文件名看到同一个文件,并打开——就是看到了同一资源——具备了进程间通信的前提
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限
若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask
函数将文件默认掩码设置为0
umask(0); //将文件默认掩码设置为0
命名管道创建成功,返回0。
命名管道创建失败,返回-1。
1、如果当前打开操作是为读而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功。
2、如果当前打开操作是为写而打开FIFO时。O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。
注意这里的权限变成了664,但是我给的权限是666,这里是因为没有设置权限掩码
再重新创建文件
服务端
第一步,创建管道
第二步,让服务端直接开启管道文件
第三步,正常通信
第四步,关闭不需要的fd
客户端
打开服务端,会发现server会卡在这里,原因客户端没有打开命名管道而导致的阻塞
继续打开客户端
这里简单的通信就做好了,但是这里服务端退出并不会自动把管道文件删除,在下次启动的时候就会报错,所以要对这里进行一些优化,用unlink函数
这里直接给出最后的全部代码(不用回车就能实现字符输入)
client.cc
#include
#include
#include
#include
#include
#include
#include
#include
#include
// #include
#include "comm.hpp"
int main()
{
//1. 不需创建管道文件,我只需要打开对应的文件即可!
int wfd = open(fifoname.c_str(), O_WRONLY);
if(wfd < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
return 1;
}
// 可以进行常规通信了
char buffer[NUM];
while(true)
{
// std::cout << "请输入你的消息# ";
// char *msg = fgets(buffer, sizeof(buffer), stdin);
// assert(msg);
// (void)msg;
// int c = getch();
// std::cout << c << std::endl;
// if(c == -1) continue;
system("stty raw");
int c = getchar();
system("stty -raw");
//std::cout << c << std::endl;
//sleep(1);
//buffer[strlen(buffer) - 1] = 0;
// abcde\n\0
// 012345
//if(strcasecmp(buffer, "quit") == 0) break;
ssize_t n = write(wfd, (char*)&c, sizeof(char));
assert(n >= 0);
(void)n;
}
close(wfd);
return 0;
}
server.cc
#include
#include
#include
#include
#include
#include
#include
#include "comm.hpp"
//少年们, 我刚刚写了一个基于匿名管道的进程池
// 可不可以把它改成使用命名管道呢??
int main()
{
// 1. 创建管道文件,我们今天只需要一次创建
umask(0); //这个设置并不影响系统的默认配置,只会影响当前进程
int n = mkfifo(fifoname.c_str(), mode);
if(n != 0)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 1;
}
std::cout << "create fifo file success" << std::endl;
// 2. 让服务端直接开启管道文件
int rfd = open(fifoname.c_str(), O_RDONLY);
if(rfd < 0 )
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 2;
}
std::cout << "open fifo success, begin ipc" << std::endl;
// 3. 正常通信
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = 0;
//std::cout << "client# " << buffer << std::endl;
printf("%c", buffer[0]);
fflush(stdout);
}
else if(n == 0)
{
std::cout << "client quit, me too" << std::endl;
break;
}
else
{
std::cout << errno << " : " << strerror(errno) << std::endl;
break;
}
}
// 关闭不要的fd
close(rfd);
unlink(fifoname.c_str());
return 0;
}
comm.hpp
#pragma once
#include
#include
#define NUM 1024
const std::string fifoname = "./fifo";
uint32_t mode = 0666;
服务端和客户端之间的退出关系
当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。
当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义