进程通信的两套标准:
system v标准下的进程通信,本地通信
posix进程通信,可以跨网络
通信的实质
是让两个进程看到同一个资源,这个资源不同,决定了不同的通信方式
父子进程指向了同一个文件,
如果是普通文件,往磁盘上写
如果是管道文件,向缓冲区中写,但是不用刷新到磁盘上
- pcb中有一个中的文件描述符数组类型,这个自定义类型就是文件结构体类型,表示文件的属性,
- 其中有个struct address_space类型结构体,这个就是缓冲区类型,定义了一个指针指向了缓冲区,
- 其中有一个inode结构体类型,表示inode的信息,其中一个成员变量有一个字段的联合体,用于表示文件是普通文件,管道文件,和字符设备就是键盘显示器这些
- 这个struct file就能找到文件对应的类型和缓冲区
Linux中的管道,文件不再是磁盘文件,读写数据的时候只在其缓冲区中读写数据。管道就是一个内存级文件,不用刷新到磁盘
pipe函数会自动将文件以读写两个方式打开两个文件描述符,并且写入到参数部分的数组中,这是输出型参数
返回0成功,-1失败
文件计数器,一方关闭,另一方会感知到
多进程编程
- 父进程以读方式和写方式同时把文件打开,就有了对文件读写的能力,子进程继承,不用再让子进程打开
- 父进程再fork创建子进程,子进程继承父进程pcb
- 再关闭父进程读,关闭子进程写,因为管道是单向通信
根据需求来关闭谁的读写
父子进程传递数据
int main()
{
// 1. 创建管道
int pipefd[2] = { 0 };
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 2. 创建子进程
pid_t id = fork();
if (id == 0)//子进程---读操作
{
close(pipefd[1]);
char buffer[1024];
while (true)
{
cout << "时间戳: " << (uint64_t)time(nullptr) << endl;
// 子进程没有带sleep,为什么子进程你也会休眠呢??
memset(buffer, 0, sizeof(buffer));
sleep(100);
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s > 0)
{
//读取成功
buffer[s] = '\0';
cout << "子进程收到消息,内容是: " << buffer << endl;
}
else if (s == 0)
{
cout << "父进程写完了,我也退出啦" << endl;
break;
}
else
{
}
}
close(pipefd[0]);
exit(0);
}
else//父进程---写操作
{
close(pipefd[0]);
const char* msg = "父进程发送信息: ";
int cnt = 0;
while (1)
{
char sendBuffer[1024];
sprintf(sendBuffer, "%s : %d", msg, cnt);
write(pipefd[1], sendBuffer, strlen(sendBuffer)); //不需要+1
cnt++;
cout << "cnt: " << cnt << endl;
}
close(pipefd[1]);
cout << "父进程写完了" << endl;
}
pid_t res = waitpid(id, nullptr, 0);
if (res > 0)
{
cout << "等待子进程成功" << endl;
}
return 0;
}
父进程带写端带sleep,子进程不带,但是子进程会休眠
父进程没有写入时,子进程在等,写入后子进程才能read到数据,子进程要根据父进程节奏进行读
读写是有一定的顺序性
管道内没有数据,read函数会被阻塞
如果写满,write函数就会阻塞
管道内自带访问控制机制就是同步互斥
管道也是资源,等待的时候就被放入管道资源等待队列中
父进程控制一堆进程,并且指定子进程完成对应的事情
- 控制一批进程
- 创建三个管道,每个进程和父进程建立管道
有多少进程就创建多少管道,父进程向管道内写特定任务,让对应子进程做不同的事情,这就是池化
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <unordered_map>
#include <ctime>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
using namespace std;
typedef void (*functor)();
vector<functor> functors; // 方法集合
unordered_map<uint32_t, string> info;
void f1()
{
cout << "处理日志任务, 执行进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n" << endl;
}
void f2()
{
cout << "备份数据任务, 执行进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n" << endl;
}
void f3()
{
cout << "处理网络连接任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n" << endl;
}
void loadFunctor()
{
info.insert({functors.size(), "处理日志的任务"});//左键代表任务编号,右值是任务名称
functors.push_back(f1);
info.insert({functors.size(), "备份数据任务"});
functors.push_back(f2);
info.insert({functors.size(), "处理网络连接的任务"});
functors.push_back(f3);
}
// int32_t: 进程pid, int32_t: 该进程对应的管道写端fd
typedef std::pair<int32_t, int32_t> elem;
int processNum = 5;
子进程执行逻辑
void work(int blockFd)
{
cout << "进程[" << getpid() << "]" << " 开始工作" << endl;
while (true)
{
uint32_t operatorCode = 0;
//将从block中读取的数据存储到第二个参数中
ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
if(s == 0)
break;
assert(s == sizeof(uint32_t));
(void)s;
if(operatorCode < functors.size()) //判断任务编号是否合法
functors[operatorCode]();//真正执行父进程派发过来的任务
}
cout << "进程[" << getpid() << "]" << " 结束工作" << endl;
}
父进程执行逻辑 - - -生成一个随机编码,写入指定的管道(也就变相的指定某个子进程)
// [子进程的pid, 子进程的管道fd]
void blanceSendTask(const vector<elem> &processFds)
{
srand((long long)time(nullptr));
while(true)
{
sleep(1);
// 选择一个进程, 选择进程是随机的,没有压着一个进程给任务
// 较为均匀的将任务给所有的子进程 --- 负载均衡
uint32_t pick = rand() % processFds.size();
// 选择一个任务
uint32_t task = rand() % functors.size();
// 把任务给一个指定的进程
write(processFds[pick].second, &task, sizeof(task));
// 打印对应的提示信息
cout << "父进程指派任务->" << info[task]
<< "给进程: " << processFds[pick].first
<< " 编号: " << pick << endl;
}
}
int main()
{
loadFunctor();
vector<elem> assignMap;
// 创建processNum个进程
for (int i = 0; i < processNum; i++)
{
int pipefd[2] = {0};
pipe(pipefd);
pid_t id = fork();
if (id == 0)//子进程区域
{
close(pipefd[1]);
work(pipefd[0]);//子进程执行管道中传过来的函数编码
close(pipefd[0]);
exit(0);
}
//父进程区域
close(pipefd[0]);
elem e(id, pipefd[1]);//进程id和对应的文件描述符
assignMap.push_back(e);
}
cout << "create all process success!" << std::endl;
// 父进程, 派发任务
blanceSendTask(assignMap);
// 回收资源
for (int i = 0; i < processNum; i++)
{
if (waitpid(assignMap[i].first, nullptr, 0) > 0)
cout << "wait for: pid=" << assignMap[i].first << " wait success!"
<< "number: " << i << "\n";
close(assignMap[i].second);
}
}
vector functors 该数组存储子进程应该做的任务
创建多进程
父进程只是在不断循环
确定给哪个进程指派,通过管道指派
由父子通信转换成兄弟通信。创建两个子进程,共享管道,关闭一端
管道的五个特征
只用于
血缘关系
单向通信,半双工的一种特殊情况
半双工:一个工作,一个停止
全双工:两个同时工作
自带同步机制,管道满,写得等。管道空,读得等——自带访问控制
面向字节流,要了解协议
管道的生命周期:管道是文件,跟随进程退出
命名管道,让无血缘进程通信
匿名管道通过子进程继承父进程做到的
命名管道,通过管道文件,文件在磁盘上有唯一的路径,通过路径找到对应的资源
以p开头的文件
匿名的是竖划线
mkfifo函数
第一个参数文件路径
第二个参数文件权限
fork函数中,父子进程通过管道通信的实质是fork会 继承 文件描述符表的特性做到的
命名管道中,两个进程打开磁盘上的文件在内存中只打开了一份,通信时候数据不会刷新到磁盘上,磁盘上的文件进行了符号处理,识别成管道文件
通过文件的路径,路径具有唯一性,多个进程可以通过路径打开同一个文件,同一个文件中的inode,就是同一个缓冲区
两个进程要通信先让他们看到同一个路径
一个进程把管道文件创建好了,另一个进程不需要创建
对端不写且退出,客户端是写端如何退出
不加1,\0是c的标准
进程虚拟地址空间本质上将进程分成了独立的进程,称为进程独立性;但是会造成进程与进程间相互协作困难
进程通信所做的事情:
- 把a进程数据给b进程;
- ab进程通过各自页表关系将内存中某一块内存映射到各自的进程虚拟地址空间,更改物理内存区域没有用,要改页表的映射关系;
进程间通信的本质:
- 为了进程之间交换数据,使用的一种计数手段,
- 进程有自己的进程虚拟地址空间,构成每一个进程的数据独立,每一个进程的数据独立会构成进程的独立性。这个特性有好有坏
常见的进程间通信的方式:
管道 | 最简单 |
---|---|
共享内存 | 无血缘关系 |
消息队列&信号量 | |
信号 | 携带数据量少,开销最小 |
本地套接字 | 网络中,最稳定却复杂 |
最大的进程间通信是网络
匿名管道:
1.管道符号:是一个竖线|
ps aux | grep xxx
- ps和grep是两个可执行程序;
- 该命令是将ps aux的输出结果通过管道给grep,作为grep程序的输入内容,grep去过滤xxx这个字符串并展示
- ps aux命令是在用户态输入,会在内核创建一个buf(缓冲区),通过缓冲区被grep程序读走
2.管道本质:
- 管道在内核中是一块缓冲区,或者称为一块内存,供不同进程进行读写的缓冲区;
- 也就是说不同进程要进行数据交互时,进程a将数据写到缓冲区中,进程b在这个缓冲区中读取
内核态:当前运行是操作系统的代码
用户态:运行的用户的代码
3.管道接口
pipe系统调用函数:
- fd[0]=3;fd[1]=4
- f[0]管道读端,f[1]管道写端
生成了两个文件描述符3和4,指向同内存空间的两端,并不是指向实质的文件
这里的闪烁是正常的,闪烁的原因有两个,一种是指向的原文件不存在,一种是指向了一块内存空间
liunx中共七种文件,有四种是不占用磁盘空间,只占用内存