目录
通信
介绍
为什么要有通信
通信的本质
如何通信
管道
引入
匿名管道
原理
介绍
过程
实现 -- pipe()
函数原型
参数
返回值
模拟代码
特点
用于父子进程之间的通信
提供访问控制
缓冲区被写满时
写入规定
pipe_buf
原子性
提供面向字节流的通信服务
管道的生命周期随进程
代码中添加退出信息
单向通信 -- 半双工的特殊情况
半双工
全双工
多个进程之间通信
介绍
代码
头文件
主程序 -- 创建子进程+清理资源
主程序 -- 菜单界面+父进程派发任务
执行结果
进程通信是指不同进程之间进行信息交换和共享数据的机制和方法
包括完成:
- 在操作系统中,进程是独立运行的程序实体,每个进程拥有自己的内存空间和资源
- 但我们有时候又需要使用多个进程来协同完成某种功能
- 因此我们就需要进程通信这一概念
进程通信允许不同的进程之间进行协作和数据交换,以实现共同的目标
所以,通信的目的就是 -- 实现多进程协同
- 通信的前提:让不同进程看到同一块"内存"(特定的结构组织的)
- 只有这样,才能在它们之间传输数据,实现各种功能
- 而这块"内存"不属于任何一个进程
- 如果属于某一个进程,其他进程该如何拿到它呢?(进程具有独立性嗷)
- 因此,它是os中某一个模块提供的
- 管道 -- 基于文件系统的概念(匿名,命名)
- system V -- 多进程 -- 单机通信
- posix -- 多线程 -- 网络通信
- 共享内存(消息队列,信号量)
- 现实中的管道(天然气管道,石油管道等等),都是为了运输某种资源修建的,并且有一个出口,一个入口,进行单向运输
- 而在计算机领域,资源指的就是数据
- 为了实现通信,有设计者设计了一种单向通信的方式
- 这种方式与现实中的管道功能类似,于是取名为管道
介绍
- 管道通信 -- 进程之间通过管道进行通信
- 而管道实际上是文件
过程
当一个进程A以某种方式打开管道,实际上是创建两个文件
一个文件作为管道读端,另一个文件作为管道写端
如何让另一个进程B也可以看到这个管道呢?
函数原型
#include
int pipe(int pipefd[2]);
- 用于创建一个匿名管道
参数
- 参数pipefd是一个整型数组,包含两个文件描述符
- pipefd[0]是读端fd,pipefd[1]是写端fd
返回值
- 返回0,表示管道创建成功
- 返回-1,表示出现错误
模拟代码
#include
#include #include #include #include #include #include #include using namespace std; int main() { int pipefd[2] = {0}; // 会打开两个文件(用于读,写) int ret = pipe(pipefd); assert(ret != -1); (void)ret; // release下assert不会显示,如果不使用这个ret,会有警告 #ifdef DEBUG cout << pipefd[0] << endl; cout << pipefd[1] << endl; #endif pid_t fd = fork(); // 创建子进程,让他和父进程通信 assert(fd!=-1); if (fd == 0) { // 子进程 -- 读 close(pipefd[1]);//关掉写端 char buffer[1024];//用于存放数据 memset(buffer, 0, sizeof(buffer)); while (true){ ssize_t size = read(pipefd[0], buffer, sizeof(buffer) - 1); //为了使有地方放\0 if (size > 0) { buffer[size] = 0; //读到内容后,记得要加\0 cout << "im child " << "message: " << buffer << endl; //打印出读到的内容 } else if (size == 0) //如果为0,说明读到了文件末尾 { cout << "read end" << endl; break; } } exit(0); } // 父进程 -- 写 close(pipefd[0]);//关掉读端 string message = "im parent , im writing"; char buffer[1024]; //存放数据 int count = 0; memset(buffer, 0, sizeof(buffer)); while (true) { snprintf(buffer, sizeof(buffer) - 1, "pid:%d,%s,%d", getpid(), message.c_str(), count++); //先将要写入的内容格式化 ssize_t size = write(pipefd[1], buffer, strlen(buffer)); if (size < 0) //小于0就说明发生错误 { cout << "write fail" << endl; } sleep(1); //写入成功后,休息1s再继续写入 } pid_t flag = waitpid(fd,nullptr, 0); //等待子进程退出 if (flag <= 0) { cout << "wait fail" << endl; } assert(flag > 0); (void)flag; cout << "wait success" << endl; return 0; }
用于父子进程之间的通信
- 匿名管道的原理只适用于父子进程之间的通信
- 因为其中一个进程必须拿到另一个进程的文件描述符表,只有父子进程才能实现
- 每个进程都打开了两个文件,作为读端和写端
- 但是管道是单向通信,所以每个进程只有其中一端就行
- 所以,需要在通信前,确定好各自的分工,然后关闭不需要的文件
上面的代码中,我们让父进程在每次写入成功后停1s
可以看到是很秩序的输出信息:
说明 -> 读端会等待写端写入,而不会自己乱读,导致读一些垃圾数据啥的
缓冲区被写满时
如果父进程一直写,一直写,把我们定义的缓冲区写满了会怎么样
(让子进程睡眠20s,不让他读取数据)
// 父进程 -- 写 close(pipefd[0]); string message = "im parent , im writing"; char buffer[1024]; int count = 0; memset(buffer, 0, sizeof(buffer)); while (true) { snprintf(buffer, sizeof(buffer) - 1, "pid:%d,%s,%d", getpid(), message.c_str(), count++); ssize_t size = write(pipefd[1], buffer, strlen(buffer)); cout << count << endl; //标识它写入的次数 if (size < 0) { cout << "write fail" << endl; } // sleep(1); }
会发现父进程写入次数卡在了这里:
说明此时已经将缓冲区写满了,但它没有继续写下去
说明 -> 写端写到满时,会等待读端读出,而不会一直写入
写入规定
pipe_buf
规定了内核的管道缓冲区大小
linux下为4096字节
原子性
原子性是指一个操作要么完全执行,要么完全不执行,没有中间状态(在并发编程中被用到)
上面两个例子,就说明管道会对这两个进程进行访问控制
(没有访问控制的情况下,父子进程同时向显示器写入时,会互相干扰各自打印的信息)
提供面向字节流的通信服务
如果子进程每次读取前停几秒,会一次输出一堆信息:
说明 -> 写入次数和读入次数无关 -> 表明数据以字节流的方式存在,它是面向流式的通信服务
- 因为管道也是文件,而文件的生命周期是依靠进程的
- 一旦没有进程打开这个文件,该文件就会被销毁
代码中添加退出信息
if (fd == 0) { // 子进程 -- 读 close(pipefd[1]); char buffer[1024]; memset(buffer, 0, sizeof(buffer)); while (true) { ssize_t size = read(pipefd[0], buffer, sizeof(buffer) - 1); if (size > 0) { buffer[size] = 0; cout << "im child ," << "message: " << buffer; } else if (size == 0) //当读取完数据就退出 { cout << "read end , im quit" << endl; break; } } exit(0); } // 父进程 -- 写 close(pipefd[0]); string message = "im parent , im writing"; char buffer[1024]; int count = 0; memset(buffer, 0, sizeof(buffer)); while (true) { snprintf(buffer, sizeof(buffer) - 1, "pid:%d,%s,%d\n", getpid(), message.c_str(), count++); ssize_t size = write(pipefd[1], buffer, strlen(buffer)); //cout << count << endl; if (size < 0) { cout << "write fail" << endl; } if(count==5){ //写了五次就关闭写端 close(pipefd[1]); cout<<"parent quit success"<
0); (void)flag; cout << "wait success" << endl; return 0; } 虽然这份代码看不太出来管道是否被销毁,但大概看看知道就行
半双工
- 半双工(Half-duplex)是一种通信方式,指在通信的两端之间只能单向传输数据
- 且数据传输方向只能在发送和接收之间切换,不能同时进行发送和接收
- 在半双工通信中,通信双方交替进行数据传输
- 当一方发送数据时,另一方必须处于接收状态,以接收发送方的数据
- 一旦发送方完成数据传输,接收方可以切换为发送状态,并向发送方发送数据
全双工
- 全双工(Full-duplex)是一种通信方式,指在通信的两端可以同时进行双向数据传输,即可以同时发送和接收数据
- 在全双工通信中,通信双方可以同时进行发送和接收操作,不需要交替进行
- 两个进程都有独立的发送和接收通道,可以同时进行双向数据传输,从而实现了并行的数据交换
介绍
如果我们想要在多个进程之间通信,就可以创建多个子进程来完成
父进程通过在管道中写入命令,确定好要派任务给哪个进程,然后就美美交给子进程完成噜
头文件
#include
#include #include #include #include #include #include #include #include #include #include #include #include #define num 5 using namespace std; using func = function ; vector callbacks; //存放任务方法 unordered_map descripe; //将编号和任务名关联起来 //四种任务 void read_mysql() { cout << "process: " << getpid() << " 执行访问数据库任务" << endl; } void execule_url() { cout << "process: " << getpid() << " 执行url解析" << endl; } void cal() { cout << "process: " << getpid() << " 执行加密任务" << endl; } void save() { cout << "process: " << getpid() << " 执行访问数据持久化任务" << endl; } void load() //将任务初始化进两个容器中 { descripe.insert({callbacks.size(), "read_mysql"}); callbacks.push_back(read_mysql); for (const auto &i : descripe) { cout << i.first << " : " << i.second << endl; } descripe.insert({callbacks.size(), "execule_url"}); callbacks.push_back(execule_url); descripe.insert({callbacks.size(), "cal"}); callbacks.push_back(cal); descripe.insert({callbacks.size(), "save"}); callbacks.push_back(save); } void show_hander() //展示现在正在执行什么任务 { //cout<<"im show "<
int main()
{
vector> mapping_table; //关联 执行任务的子进程和任务编号
load(); //初始化
// 创建多个进程
for (int i = 0; i < num; i++)
{
int pipefd[2] = {0};
pipe(pipefd); // 创建管道
pid_t id = fork();
assert(id != -1);
(void)id;
if (id == 0)
{
// child
close(pipefd[1]); //关闭写端
while (true) // 等待命令+执行任务
{
bool quit = false;
int command = wait_command(pipefd[0], quit);//等待命令
if (quit) //判断是否退出
break;
if (command >= 0 && command < task_size())
{
callbacks[command]();//执行派发的任务
cout << "success" << endl;
}
else
{
cout << "非法command" << endl;
}
}
cout << "process exit : " << getpid() << endl;
exit(0);
}
// parent
close(pipefd[0]);//关闭读端
// 保存[通过哪个fd发送命令到子进程]:
mapping_table.push_back(pair(id, pipefd[1]));
}
//派发任务
// 关闭fd+进程
for (const auto &i : mapping_table)
{
close(i.second); // 关闭每个进程之间通信的写端,使子进程读到0从而退出
}
for (const auto &i : mapping_table)
{
waitpid(i.first, nullptr, 0); // 等待每个退出的子进程,回收资源
}
return 0;
}
// 派发任务,需要使每个进程都能被使用 -- 单机版的负载均衡,所以使用随机数
srand((unsigned int)time(nullptr) ^ getpid() ^ 1243225324);
cout << "****************************************" << endl;
cout << "* 1. show functions 2. send command *" << endl;
cout << "****************************************" << endl;
cout << "please select : " << endl;
while (true)
{
int flag = 0;
int select = 0, command = 0;
cin >> select;
if (select == 1)
{
show_handler();
}
else if (select == 2)
{
cout << "please enter your command : " << endl;
cin >> command; // 任务
// cout << "command: " << command << endl;
cout << endl;
int i = rand() % mapping_table.size(); //随机指派一个子进程执行任务
//派发任务后,需要叫醒对应的子进程
send_wakeup(mapping_table[i].first, mapping_table[i].second, command);
}
else
{
cout << "select error" << endl;
}
sleep(1);
while (true) //重复
{
cout << endl
<< "continue : y/n ?" << endl;
char c = 0;
cin >> c;
getchar();
if (c == 'y' || c == 'Y')
{
cout << "****************************************" << endl;
cout << "* 1. show functions 2. send command *" << endl;
cout << "****************************************" << endl;
cout << "please select : " << endl;
flag = 1;
break;
}
else if (c == 'n' || c == 'N')
{
cout << "exit" << endl;
flag = 2;
break;
}
else
{
cout << "select error , please retry" << endl;
continue;
}
getchar();
}
if (flag == 1)
{
continue;
}
else if (flag == 2)
{
break;
}
}
执行结果
运行起来就长这样,我们可以手动派发任务