进程间通信(一)【管道通信(下)】

目录

  • 3. 编码通信
    • 3.1 管道的四种情况
    • 3.2 管道的大小
    • 3.3 总结管道的五个特征
  • 4. 管道的应用场景
    • 4.1 命令行中的管道
    • 4.2 进程池中的管道

3. 编码通信

// 创建管道文件的系统调用
// pipefd:输出型参数,将以读写方式分别打开的文件的文件描述符带出,供用户使用。
// pipefd[0]: 读端下标	pipefd[1]: 写端下标
//创建成功返回0,否则返回-1,并且设置对应的errno
int pipe(int pipefd[2]);
RETURN VALUEOn success, zero is returned.  On error, -1 is returned, and errno is set appropriately.
// 输出信息格式化后,从原来想显示器文件写入转变为像某一个字符串写入,并且设置写入的大小
// 与 sprintf 相比更加安全的写入
int snprintf(char *str, size_t size, const char *format, ...);
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <cstring>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;#define N 2
#define M 1024void Write(int wfd)
{string s = "I am a child process!";pid_t self = getpid();int number = 0;char buffer[M];while (1){snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);// 发送信息给父进程// strlen(buffer) 不需要 + 1 带上 \0,因为 \0 是 C/C++ 的语法规定// wrtie 向管道文件写入,管道文件没有这个规定,只写入数据内容即可。write(wfd, buffer, strlen(buffer)); sleep(1);}
}void Reader(int rfd)
{char buffer[M];while(1){// read 返回实际读取的字符个数ssize_t n = read(rfd, buffer, sizeof(buffer));if(n > 0){// 读取到的字符串没有带 \0, 因此补上。buffer[n] = '\0';cout << "father process(" << getpid() << ") get the message: " << buffer << endl;}// TODO}
}int main()
{int pipefd[N];int n = pipe(pipefd);if (n < 0) return 1;// father->r, child->w// pipefd[0]: 读端下标	pipefd[1]: 写端下标pid_t id = fork();if (id < 0) return 2;else if (id == 0){// child processclose(pipefd[0]); // 子进程关闭读端的文件描述符Write(pipefd[1]); // 子进程写close(pipefd[1]);exit(0);}// father processclose(pipefd[1]); // 父进程先关闭写端的文件描述符Reader(pipefd[0]); // 父进程读pid_t returnId = waitpid(id, nullptr, 0);   // 等待回收子进程if(returnId < 0) return 1;close(pipefd[0]);return 0;
}

在这里插入图片描述

父子进程间的通信,其中子进程要写入数据给父进程读取,依旧上述理论知识,进程间通信就是创建一个管道文件,然后向管道文件写入数据即可,而管道文件也是文件,因此只要是文件,直接调用 write 系统调用写入即可,然后父进程再调 read 读取就可以完成进程间的通信了。

其中,buffer 数组的本质就是用户缓冲区,子进程先将数据写入到用户缓冲区,再调 write 将用户缓冲区刷到内核中(即文件缓冲区,文件对象也是内核的数据结构);父进程就先把文件缓冲区的数据拷贝到应用层,即用户缓冲区(buffer)。所以进程间的通信,是需要操作系统做为中介的,通信的数据都要经过操作系统,因为进程是互相独立的,谁也无法直接访问谁。

3.1 管道的四种情况

  • 读写端正常,管道如果为空,读端阻塞

    上述代码中只是对子进程的写入操作做了 sleep,父进程并没有做休眠,但为什么父进程读取从子进程传过来的数据时,也是一秒一秒的读,父进程没有休眠,不应该是一直读取,然后读完打印,再读取。。。。这样的现象吗??

    父子在对同一个资源进行访问,是进程通信的前提,但引入一个方案往往会携带一个新的问题产生,这个资源是被多执行流共享的,难免出现访问冲突的问题。假设子进程写入一条数据后 sleep(50),但父进程还是不管不顾的从文件缓冲区中读取数据,可能读取到的都是垃圾数据,总不能都打印出来让用户自己去甄别这些数据的真实性吧。而当一方向文件做读取的同时,另一方如果也在做写入操作,极大困难会导致数据的覆盖等问题,这就是访问冲突的问题,因此父子进程是会进程协同的,同步与互斥的,以达到包含管道文件数据的安全。

    所以当子进程写入完后休眠了,父进程就是要阻塞等待子进程,直到子进程做下一次写入,父进程再开始第二遍的读取。

    如果反过来呢,子进程无休眠的一直写入数据,但父进程每隔五秒做一次数据读取,是什么现象呢??

  • 读写端正常,管道如果被写满,写端阻塞

    void Write(int wfd)
    {......while (1){snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);write(wfd, buffer, strlen(buffer)); cout << number << "\n";}
    }
    void Reader(int rfd)
    {char buffer[M];while(1){sleep(5);ssize_t n = read(rfd, buffer, sizeof(buffer));......// TODO}
    }
    

    在这里插入图片描述

    现象:因为读端每读一次就要休眠 5 秒,写端一直在往管道中写入数据,所以管道的一下子就写满了,管道被写满之后写端也就阻塞等待着读端读取数据,而不会一直覆盖式的继续写入,因此这个现象也告诉我们,管道是有大小的。

    我们还看到了,写端写入的数据是一行一行的字符串数据,但是读端读取到的是一大坨的字符串,这是因为不管写端是一次性、还是分批的将数据写入管道,读端都会一次性把管道的数据读出来(如果用户缓冲区足够大的前提下)。在父进程的视角,它无法知道子进程写入的是一行一行的字符串数据,在父进程的视角上只有一个一个的字符,因为管道是面向字节流的。

    而对于读端从管道中读取出来的数据是乱的,这个问题是需要用户层去解决的,所以这就需要用户层在通信时制定一些通信协议,比如规定一次写入的字符串数据的大小,然后读端读取数据时也得按这个大小为单位进行读取。

  • 读端正常读,写端关闭,读端读到文件结尾,读端不阻塞。(即子进程先退出,父进程继续读数据)

    void Write(int wfd)
    {string s = "I am a child process!";int number = 0;while (1){sleep(1);char ch = 'c';write(wfd, s.c_str(), s.size());number++;cout << "写入数据:" << s.c_str() << "\n";if(number >= 5) break;  // 子进程退出}
    }
    void Reader(int rfd)
    {char buffer[M];while(1){ssize_t n = read(rfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = '\0';cout << "father process(" << getpid() << ") get the message: " << buffer << endl;}cout << "读取的实际字符:" << n << '\n';
    }
    

    在这里插入图片描述

    现象:子进程正常写入时,父进程正常读取,当子进程退出后,父进程并且没有阻塞,而是一直继续读取,只不过实际读取的字节数为 0(即 read 的返回值,读取成功时返回实际读取字节个数,0 表示读到文件结尾)。

    所以当读端正常读取、写端关闭的情况,读端把数据读完之后,就不再继续读了,而写端关闭后,读端不会处于阻塞状态!

    所以当这种情况,正确的代码处理应该是,当父进程读取到文件结尾后,跳出循环,回收子进程,最后自己退出。

    void Reader(int rfd)
    {char buffer[M];while(1){ssize_t n = read(rfd, buffer, sizeof(buffer));if(n > 0)	// TODOelse if(n == 0) break;  // 写端关闭后,读端读到文件尾后,不再继续读取else break;}
    }
    
  • 写端正常,读端关闭

    在做这种情况的实验之前,我们可以简单预测一下它的结果。读端都关闭了,还有必要继续写吗??写给谁看呢??所以读端如果关闭了,写端有非常大概率直接就不写了,而写端存在的意义就是写数据,你都不写了,你这个进程还留着干嘛,浪费我操作系统的资源吗??诸如闲置的进程被操作系统挂起、父子进程的写时拷贝、还有缺页中断式的惰性加载,这些都足以证明操作系统是不会做低效、浪费任何资源等类似的工作的。如果做了,那就是操作系统有 bug!

    所以如果读端被关闭,写端即没有存在的意义,一场再浩大的演唱会,失去了观众,也就失去了存在的意义。没有进程读 取,写端还继续写入数据,那么也一样要占用调度资源、cpu 资源、内存资源,因此操作系统就会通过信号杀掉正在写入的这种进程。(为什么是通过信号?进程的创建、终止 讲进程终止时我们曾谈过,任何形式使进程异常退出的本质都是进程收到了某种信号!)

    接下来,我们要验证上述猜想,并且顺便把让子进程终止的信号打印出来。

    int main()
    {// TODO.....pid_t id = fork();if (id < 0) return 1;else if (id == 0){// TODO.....Write(pipefd[1]); // 子进程写exit(0);}Reader(pipefd[0]);  // 父进程读5sclose(pipefd[0]);   // 关闭读端// TODO.....return 0;
    }
    

    在这里插入图片描述

    现象:前面 5 秒钟,子进程正常写、父进程正常读,5秒后,父进程直接把读端的文件描述符关了,不读了!子进程立马被操作系统杀掉,变为僵尸状态,等待父进程回收。

3.2 管道的大小

# 可查看当前系统内核允许一个进程打开的最大文件数、还有管道的容量大小等信息。ulimit -a		

在这里插入图片描述

根据系统查看的信息,512 bytes * 8 = 4kb,即创建出来的管道的大小就是 4kb,但其实通过代码层面去验证,发现是这个大小是有的问题的。

int pipefd[N];
int n = pipe(pipefd);
int number = 0;
char ch = 'c';
write(pipefd[1], &ch, 1);
number++;
cout << number << "\n";

在这里插入图片描述

每次向管道写入一个字符,然后计数统计的结果一共是向管道写入了 65536 bytes 的数据,65536 bytes = 64 kb,所以这是系统信息的错误吗??

其实也不是系统提供的信息有错,只不过是内核版本更新时做了优化,man 7 pipe 查看管道的容量说明

在这里插入图片描述
在 linux 内核 2.6.11 之前,管道的大小是与系统的页大小一样的(即 4kb),但在 2.6.11 之后的版本,管道的大小为 65536 bytes,即我们代码层面验证的结果。

在这里插入图片描述

同时,管道还规定数据的原子性,即一次读取的数据不能小于 PIPE_BUF(即4kb)。

什么是原子性?假设子进程要向管道写入字符串 “hello world”,并且规定以 “hello world” 这一个字符串的大小为一个单位,因此当子进程写入数据时,刚写完 hello,world 这个单词还没写,父进程看到管道中有数据了,也不能进行读取,因为规定了一次读取就要读 “hello world” 大小这么多的数据,这就是管道的原子性。当写入数据大小没超过 4kb 时,即便另一方进程看到管道中有数据了,也无法进行读取,要么等写入端停止写入,要么等数据超过 4kb 时才能进行读取。

所以 ulimit -a 查看到的管道的大小,可以理解为一次读取的最小单位,即 PIPE_BUF 的大小。

3.3 总结管道的五个特征

  • 具有血缘关系的进程才能进行进程间通信
  • 管道只能单向通信
  • 父子进程是会进程协同的,同步与互斥的,以此来达到保护管道文件的数据安全
  • 管道是面向字节流的
  • 管道是基于文件的,而文件的生命周期是随进程的!(当父子进程进行管道通信时,一方都只保留一端,所以对于 struct file w 或者 struct file r ,在文件对象内部的引用计数都为1,因此当正在通信的进程全部退出后,对应的 struct file 一端的引用计数就会变为 0,引用计数为 0 时操作系统就会释放文件对象及其数据,因此管道文件也就被关闭了)

4. 管道的应用场景

4.1 命令行中的管道

这篇文章讲的基于文件的通信方式,即匿名管道,在我们命令行中就经常使用到,比如

cat test.txt | head -10 | tail -5

上面的这条指令,在命令行解释中,会先创建出两个管道文件;而每一部分的指令,执行起来都会创建为一个进程,并且它们都是父进程 bash 的子进程,具有血缘关系,因此可以进行管道通信。该指令在通信时的本质,就是将文件的标准输出做重定向到管道的写端,接着由另一个进程从管道中读取数据,也即从标准输入重定向到管道的读端,以此类推。而诸如这类命令行指令,使用的就是 匿名管道

4.2 进程池中的管道

为了方便大家理解什么是进程池,我以内存池为例,我们平时在语言中向操作系统 malloc / new 申请的内存,其实都是在所谓的内存池中拿的内存,并不是直接从操作系统那拿的。如果频繁的向操作系统申请内存,会导致程序的运行效率和响应速度不高(操作系统可是很忙的,不仅要管理上百进程、文件等内核结构,还要管理硬件),而不管是 malloc 还是 new,底层一定都是调用的系统调用,只要是系统调用,就有性能消耗。有了内存池,可以一次性向操作系统申请多一些的内存,后续当用户 malloc/new 时,直接从内存池中拿就行了,不用了再释放回去,释放回去后也还是回到内存池,而不会回到操作系统中,这样也可以循环利用这些内存,大大减少了向操作系统申请内存的频率,程序的响应速度也就提高了。

所以,进程池也是类似于内存池的一种理念,池化技术都是为了提高程序响应速度、减少访问操作系统的次数。而创建子进程的最终目的都是为了帮助用户执行任务、解决需求的。如果没有进程池,当来一个任务时就 fork() 一次,任务执行完毕后再 waitpid 回收子进程,后续来任务再创建,这样的话,程序的响应速度就不高(频繁的调用系统调用)。因此进程池的本质也就是提前创建好一批子进程,等待用户调度分配任务即可。

所以管道在进程池中的应用就在于,因为创建了一批子进程,因此父进程需要管理控制子进程,那么就需要与子进程进行通信,而通信的本质就是进程双方能够看到一块共同的 “资源”,即管道文件。建立通信后,父进程可以通过发送 “信号” 等行为向子进程分配任务。

因此能够确定的是,每一个子进程与父进程之间都要创建一个管理来建立父子进程间的通信信道,然后父进程对管道做写入,子进程从管道中读取父进程分配的任务等信息。而当父进程没有向管道中写入数据时,子进程就阻塞在管道的读端,等待父进程分配任务。并且,为了防止上述案例 “父进程毫无协议的写数据,子进程也毫无规律的读取,读取到的数据就乱成一团的问题”,我们在编码实现上,人为约定好一次读写,都以 4bytes 为单位,不可多读,也不可多写。

关于管道的一个应用场景 ----- 进程池的代码案例 管道在进程池中的应用案例。


如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1549269.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

2025 年 IT 前景:机遇与挑战并存,人工智能和云计算成重点

云计算de小白 投资人工智能&#xff1a;平衡潜力与实用性 到 2025 年&#xff0c;人工智能将成为 IT 支出的重要驱动力&#xff0c;尤其是在生成式人工智能领域。人工智能的前景在于它有可能彻底改变业务流程、增强决策能力并开辟新的收入来源。然而&#xff0c;现实情况更加微…

突发:OpenAI o1颠覆了人类,o1为什么超越了人类,sam万字长文解读

要点速读 2024 年 9 月 12 日&#xff0c;OpenAI 发布了其最新的人工智能模型——o1&#xff08;Learning to Reason with LLMs[1]&#xff09;&#xff0c;这是一款经过强化学习训练的大型语言模型&#xff0c;能够执行复杂的推理任务。相比于此前的 GPT-4o&#xff08;GPT-4…

上交所服务器崩溃:金融交易背后的技术隐患暴露杭州BGP高防服务器43.228.71.X

一、上交所宕机事件始末 2024 年 9 月 27 日&#xff0c;上交所交易系统突发崩溃&#xff0c;这一事件犹如一颗巨石投入平静的湖面&#xff0c;引起了轩然大波。当天上午&#xff0c;众多投资者反馈券商交易出现延迟问题&#xff0c;随后上交所发布了《关于股票竞价交易出现异常…

【中医智慧解糖忧】血糖高?中医调理有妙招,自然平衡血糖不是梦!

在快节奏的现代生活中&#xff0c;高血糖已成为困扰许多人的健康难题。面对这一挑战&#xff0c;许多人第一时间想到的是西医的药物治疗&#xff0c;却往往忽略了中医这一博大精深的宝库。事实上&#xff0c;中医以其独特的理论体系和丰富的实践经验&#xff0c;在调理血糖方面…

C++里的随机数

想用C做最基础的猜数字,肯定少不了随机数; srand(unsigned(time(NULL))); rand() //是生成一个随机数 rand()%1001//就是一个从一到一百的随机数 合体: #include <iostream> #include <cstdlib> #include <time.h> int main() { int g 0; while (g < …

NSSCTF [HDCTF 2023]easy_re

文件有壳 先用upx脱壳 upx -d 文件地址 将文件拖入IDA shiftF12查看可疑字符串 先进入主函数查看 继续跟进function函数 发现这就是一个base64解码 void __cdecl func(char *x, char *y) {unsigned __int8 *v3; // 用于暂存字符的指针unsigned __int8 v4; // 用于暂存单个字符…

MyBatis——Plus——入门

常用注解 MyBatis——Plus怎么知道他是访问哪张表 常用配置

前端常用动画 直接可以用的代码加详细流程和案例 能应付90%的开发场景

前端项目&#xff0c;特别是Toc的项目&#xff0c;一定少不了各种动效和动画效果。 葫芦七兄弟&#xff1a; CSS 动画 优点&#xff1a;兼容性强&#xff1b;浏览器针对的流畅度优化&#xff1b;语法简单&#xff1b;某些属性&#xff08;如 transform 和 opacity&#xff09;…

带您了解《人工智能机器视觉应用工程师》

人工智能机器视觉应用是指利用人工智能技术和机器视觉技术相结合&#xff0c;使机器能够像人类一样通过视觉感知和理解环境&#xff0c;从而实现各种应用。随着人工智能技术的不断发展&#xff0c;机器视觉应用在各个领域得到了广泛应用。 在工业制造领域&#xff0c;人工智能机…

电商系统之链动2+1模式开发

在电商领域&#xff0c;创新的商业模式是推动市场增长与用户粘性的关键。链动21模式&#xff0c;作为一种基于社交裂变的分销策略&#xff0c;以其独特的团队构建与激励机制&#xff0c;在电商系统中展现出巨大的潜力。从程序员的技术角度出发&#xff0c;本文将深入探讨链动21…

汇编语言 访问CMOS RAM并打印时间(未完)

题目:以"年/月/日 时:分:秒"的格式,显示当前的日期,时间 提示:在此代码的基础上加以改造 assume cs:code code segment start:mov al,9 ;年out 70h,al ;传入9号单元的地址in al,71h ;取9号单元的内容&#xff0c;高4位为十位、低4位为各位mov ah,almov cl,4shr ah,…

最详细!适合AI大模型零基础入门的学习路线+学习方法+学习资料,全篇干货,建议收藏!

前言 随着ChatGPT的横空出世&#xff0c;大模型时代正式来临。千亿甚至万亿参数的大模型陆续出现&#xff0c;各大企业、高校纷纷推出自己的大模型&#xff0c;这标志着通用智能时代的到来。对于零基础的初学者来说&#xff0c;如何快速入门AI大模型&#xff0c;抓住这个时代的…

别再使用[]来获取字典的值了,来尝试一下这些方法

字典 在Python中&#xff0c;字典&#xff08;Dictionary&#xff09;是一种非常灵活的数据结构&#xff0c;用于存储键值对&#xff08;key-value pairs&#xff09;。每个键都是唯一的&#xff0c;并且与某个值相关联。字典是Python中处理映射关系&#xff08;即一个键对应一…

数据结构与算法——Java实现 22.有效的括号

目录 22. 有效的括号 思路 接口 数组实现类 有效的括号 力扣 直到有一天&#xff0c;我不会再问离开的人为什么 —— 24.9.28 22. 有效的括号 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。…

【网站推荐】favicon图标生成

在制作网站的过程中&#xff0c;图标和 Favicon 是不可或缺的一部分。Favicon 是浏览器标签、书签和其他地方显示的小图标&#xff0c;它不仅可以增强网站的品牌识别度&#xff0c;还能提升用户体验。一个精美的 Favicon 可以让您的网站在众多标签中脱颖而出&#xff0c;吸引用…

信息学奥赛复赛复习06-CSP-J2020-02直播获奖-向上取整、向下取整、整数除法、最大值、最小值、计数排序

PDF文档回复:20240928 1 2020 CSP-J 题目1 优秀的拆分 [题目描述] NOI2130 即将举行。为了增加观赏性&#xff0c;CCF 决定逐一评出每个选手的成绩&#xff0c;并直播即时的获奖分数线。本次竞赛的获奖率为 w%&#xff0c;即当前排名前 w% 的选手的最低成绩就是即时的分数线 …

集合框架 - Map双列集合

01 概述 02 常用方法 03 遍历方式 【快捷键】&#xff1a;ctrlaltv 【说明】&#xff1a;Map.Entry<xx,xx>中&#xff0c;Entry是Map集合中的一个接口&#xff0c;但接口是不能创建对象的&#xff0c;它底层是通过使用Entry的实现类对象来封装键值对数据的。 【说明】&a…

LLM大模型学习:致AI新手掌握这些经验,助你少走三年弯路!

这篇文章&#xff0c;我将结合自己在大模型领域的经验&#xff0c;给大家详细聊聊新人应该如何转行大模型赛道&#xff1f; 比如大模型都有哪些方向&#xff1f;各方向的能力要求和岗位匹配&#xff1f;新手转行大模型常踩的坑和常见的误区&#xff1f;以及入行大模型最顺滑的…

代码为笔,合作作墨,共绘共赢画卷———未来之窗行业应用跨平台架构

合作共赢&#xff0c;代码同创&#xff0c;成就非凡 一、资源整合方面 1.1. 技术资源共享 - 不同的合作伙伴可能在技术领域各有所长。例如&#xff0c;一方可能擅长前端用户界面设计&#xff0c;具有丰富的交互设计经验&#xff0c;能够打造出美观、易用的预订界面&#xff…

Hadoop三大组件之MapReduce(一)

Hadoop之MapReduce 1. MapReduce是什么 MapReduce是一个分布式运算程序的编程框架&#xff0c;旨在帮助用户开发基于Hadoop的数据分析应用。它的核心功能是将用户编写的业务逻辑代码与自带的默认组件整合&#xff0c;形成一个完整的分布式运算程序&#xff0c;并并发运行在一…