【Linux系统编程】进程间通信(管道:匿名管道、命名管道、实战练习)

知其然,知其所以然

什么是进程间通信:

进程间通信是不同进程间交换信息的一种机制。进程可能在同一台计算机上,也可能在网络中的不同计算机上。那我们为什么要有这种机制:

为什么进程间要通信:

①数据共享:多个进程需要访问共享数据,通过通信来保证数据的一致性和有效性

②同步和协调:在并发程序中,某些进程必须等待其他进程完成某项任务,以确保程序的正确性。

③资源管理:在有限的资源(如内存、文件、设备等)下,进程需要协调使用这些资源,避免竞争和冲突。

④模块化设计:通过将功能分散到多个程序中,可以提高程序的可管理性和可拓展性,但分散到多个程序中,势必要求多个进程能够访问同一资源、数据。

进程间通信的方式有:管道(Pipes)、消息队列(Message Queues)、共享内存(Shared Memory)、信号(Signals)、套接字(Sockets)。其中套接字虽然可以在同一台计算机上的进程间使用,但主要是用于网络上不同计算机之间的通信,本节我们呢不涉及该模块的内容。

通信方式--管道

用于一对进程之间进行数据流转,常用于父进程与子进程之间的通信

管道是Unix中最古老的进程间通信的形式,我们把:从一个进程连接到另一个进程的数据流称为一个“管道”。例如:我们之间使用的管道符,统计登录用户的个数:who | wc -l

其中,who命令和wc命令是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”里,wc进程再通过标准输入从“管道”中读取数据,至此,便完成了数据的传输,进而完成数据的进一步加工处理。

一般来讲,管道的特征是单向通信(实现方式为关闭不用的文件描述符)。管道有读端和写端 

在C编程中的应用:

管道实际上分为两种:匿名管道和命名管道

在此之前,我们想要进行通信,我们能想到的只有通过文件来传递信息。

int main(int argc, char* argv[])
{
	int fd_w=open("./test.txt",O_WRONLY| O_CREAT| O_TRUNC ,0644);//写端
	int fd_r=open("./test.txt",O_RDONLY);//读端

	pid_t fpid=fork();//创建子进程
	if(fpid<0)exit(-1);//创建子进程失败
	else if(fpid==0){//子进程
		close(fd_r);//关闭读端
		// char buf_w[1024];//写入缓冲区
		// memset(buf_w,'\0',sizeof(buf_w));
		char *str="hello world";
		int ret = write(fd_w,str,strlen(str));
		if(ret<0)perror("write err");
	}
	else if(fpid>0){//父进程
		close(fd_w);//关闭写端
		char buf_r[1024];//读出缓冲区
		memset(buf_r,'\0',sizeof(buf_r));
		int cnt=0;
		sleep(1);//等待子进程写入
		while( cnt = read(fd_r,buf_r,sizeof(buf_r)) ){
			if(cnt<0)perror("read err");
			else printf("sucess: %s\n",buf_r);
		}
	}
	return 0;
}

在该示例中,我们将文件使用两种方式打开,分别使用两个文件描述符管理,在父子进程中分别只保留一个。然后一个进程进行写入操作,另一个进程进行读出操作,为了保证正确写入,我们使用sleep函数进行手动强制等待。但是这个方法局限性太大,不知道具体的执行时间,所以这里我们也可以使用进程等待进行优化(将sleep(1)换成wait(NULL)即可)。父子间能直接进行通信的本质是使用了公共的资源-文件描述符,然后通过文件来进行数据的通信。

匿名管道

--父子进程间的通信管道

匿名管道用于进程间通信,且仅限于本地关联进程之间的通信,通常也就是所谓的父子。

进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让父子进程先看到一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。

这里,父子进程看到的同一份文件资源是由操作系统维护的,所以父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。

管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。

创建匿名管道:int pipe(int pipefd[2]);

int main(int argc, char* argv[])
{
    int fd[2]; pipe(fd);
	int fd_w=fd[1];//写端
	int fd_r=fd[0];//读端
    
	pid_t fpid=fork();//创建子进程
	if(fpid<0)exit(-1);//创建子进程失败
	else if(fpid==0){//子进程
		close(fd_r);//关闭读端
		// char buf_w[1024];//写入缓冲区
		// memset(buf_w,'\0',sizeof(buf_w));
		char *str="hello world";
		int ret = write(fd_w,str,strlen(str));
		if(ret<0)perror("write err");
	}
	else if(fpid>0){//父进程
		close(fd_w);//关闭写端
		char buf_r[1024];//读出缓冲区
		memset(buf_r,'\0',sizeof(buf_r));
		int cnt=0;
		sleep(1);//等待子进程写入
		while( cnt = read(fd_r,buf_r,sizeof(buf_r)) ){
			if(cnt<0)perror("read err");
			else printf("sucess: %s\n",buf_r);
		}
	}
	return 0;
}

我们发现,这种方式和上面的文件通信的方式非常的像完全可以说是一个模子里刻出来的。是的,管道的本质也是文件通信,只不过二者有着本质的区别:管道文件实际上不是一个磁盘文件,是一个内存文件。

从管道写端写入的数据会被存到内核缓冲,直到从管道的读端被读取。而管道是有大小的,这就意味着,管道可以被存满。①如果管道已满,写端进程会阻塞在write上,读端read返回成功时才可以继续write写入。②如果管道没满(存在数据),write写入,read读出正常进行。③管道中不存在数据,也就是read先于write执行,那么read时会阻塞。

练习:使用管道实现ls / | wc -l

int main(){
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret<0)perror("create pipe err");

    pid_t fpid=fork();
    if(fpid<0)perror("fork err");
    else if(fpid==0){//子进程
        close(pipefd[0]);
        dup2(pipefd[1],STDOUT_FILENO);//将管道的写端 重定向到输出端
        execlp("ls","ls","/",NULL);
    }
    else if(fpid>0){//父进程
        close(pipefd[1]);
        dup2(pipefd[0],STDIN_FILENO);//将管道的读端 重定向到输入端
        execlp("wc","wc","-l",NULL);
    }
}

命名管道

匿名管道只能用于具有亲缘关系的进程之间的通信。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。

如果要实现两个毫不相干的进程之间的通信,可以使用民命管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,也是因此使得两个不同的进程都可以找到这个管道,但是这个映像的大小永远为0,因为命名管道和匿名管道一样,不会将通信数据刷新到磁盘中。

创建命名管道文件:使用mkfifo命令。

然后创建两个.c文件,执行其中一个达到写入的目的,执行另一个达到读出的目的。

//write:写入端
int main(int argc, char* argv[])
{
	int fd=open("./fifo",O_WRONLY);
	if(fd<0)perror("open err");

	char *str="hello world";
	write(fd,str,strlen(str));
	return 0;
}
//read:读取端
int main(int argc, char* argv[])
{
	int fd = open("./fifo", O_RDONLY);
	if (fd < 0)perror("open err");

	char buf[1024];int cnt=0;
	memset(buf,'\0',sizeof(buf));
	while(cnt=read(fd,buf,sizeof(buf))){
		if(buf<0)perror("read err");
		else printf("%s",buf);
	}
	printf("\n");
	return 0;
}

执行时的注意点:

1.读进程打开FIFO,没有打开写进程时:

如果文件属性有O_NONBLOCK,返回成功,否则阻塞直到有写进程打开FIFO

2.写进程打开FIFO,没有打开读进程时:

如果文件属性有O_NONBLOCK,返回失败,否则阻塞直到有读进程打开FIFO

3.正常情况:

先打开写进程,此时处于第二种情况下的阻塞情景

后打开读进程,此时写进程返回成功,读进程读取全部内容后返回成功

练习:两个程序使用fifo互发消息聊天

阶段1.单向发送和接收固定的一条消息

//send.c:发送端
int main(int argc, char* argv[])
{
	int fd=open("./fifo",O_WRONLY);
	if(fd<0)perror("open err");

	char *str="hello world";
	write(fd,str,strlen(str));
	return 0;
}
//receive.c:接收端
int main(int argc, char* argv[])
{
	int fd = open("./fifo", O_RDONLY);
	if (fd < 0)perror("open err");

	char buf[1024];int cnt=0;
	memset(buf,'\0',sizeof(buf));
	while(cnt=read(fd,buf,sizeof(buf))){
		if(buf<0)perror("read err");
		else printf("%s",buf);
	}
	printf("\n");
	return 0;
}

阶段2.单向发送和接收终端输入的一条消息

//send:发送端
int main(int argc, char* argv[])
{
	int fd_send=open("./fifo",O_WRONLY);
	if(fd_send < 0)perror("open err");

	char send_msg[1024];
	memset(send_msg,'\0',sizeof(send_msg));
	int count = read(STDIN_FILENO,send_msg,sizeof(send_msg));
	write(fd_send,send_msg,count);
	
	return 0;
}
//receive:接收端
int main(int argc, char* argv[])
{
	int fd_receive = open("./fifo", O_RDONLY);
	if (fd_receive < 0)perror("open err");

	char buf[1024];int cnt=0;
	memset(buf,'\0',sizeof(buf));
	while(cnt=read(fd_receive,buf,sizeof(buf))){
		if(buf<0)perror("read err");
		else printf("%s",buf);
	}
	return 0;
}

阶段3.单向循环发送和接收终端输入的消息

//send:发送端
int main(int argc, char* argv[])
{
	int fd_send=open("./fifo",O_WRONLY);
	if(fd_send<0)perror("open err");
	
	
	while(1){
		char send_msg[1024];int cnt=0;
		memset(send_msg,'\0',sizeof(send_msg));

		cnt = read(STDIN_FILENO,send_msg,sizeof(send_msg));
		write(fd_send,send_msg,cnt);
	}
	return 0;
}
//receive:接收端
int main(int argc, char* argv[])
{
	int fd_receive = open("./fifo", O_RDONLY);
	if (fd_receive < 0)perror("open err");


	while(1){
		char receive_msg[1024];int cnt=0;
		memset(receive_msg,'\0',sizeof(receive_msg));

		while(cnt=read(fd_receive,receive_msg,sizeof(receive_msg))){
			if(receive_msg < 0)perror("read err");
			else write(STDOUT_FILENO,receive_msg,cnt);
		}
	}
	return 0;
}

阶段4.双向循环发送和接收终端输入的消息

int main(int argc, char* argv[])
{
    //send端: (如果是receive端,打开的管道交换一下即可)
	int fd_send=open("./fifo_1to2",O_WRONLY);if(fd_send<0)perror("open err");
	int fd_receive = open("./fifo_2to1", O_RDONLY);if (fd_receive < 0)perror("open err");

	pid_t fpid=fork();if(fpid<0)perror("fork err");
	else if(fpid==0){//子进程--发送
		close(fd_receive);//只发送不接收
		
		while(1){
			char send_msg[1024];int cnt=0;
			memset(send_msg,'\0',sizeof(send_msg));

			cnt = read(STDIN_FILENO,send_msg,sizeof(send_msg));
			write(fd_send,send_msg,cnt);
		}

		close(fd_send);//完事结束发送端
	}
	else if(fpid>0){//父进程--接收
		close(fd_send);//只接受不发送
		
		while (1){
			char receive_msg[1024];int cnt = 0;
			memset(receive_msg, '\0', sizeof(receive_msg));

			while (cnt = read(fd_receive, receive_msg, sizeof(receive_msg))){
				if (receive_msg < 0)perror("read err");
				else write(STDOUT_FILENO, receive_msg, cnt);
			}
		}

		close(fd_receive);//完事关闭接收端
	}
	return 0;
}

感谢大家!!! 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

K8S ReplicaSet 控制器

一、理论介绍 今天我们来实验 ReplicaSet 控制器&#xff08;也叫工作负载&#xff09;。官网描述如下&#xff1a; 1、是什么&#xff1f; ReplicaSet 副本集&#xff0c; 维护一组稳定的副本 Pod 集合。 2、为什么需要&#xff1f; 解决 pod 被删除了&#xff0c;不能自我恢…

【C语言】自定义类型讲解

文章目录 一、前言二、结构体2.1 概念2.2 定义2.2.1 通常情况下的定义2.2.2 匿名结构体 2.3 结构体的自引用和嵌套2.4 结构体变量的定义与初始化2.5 结构体的内存对齐2.6 结构体传参2.7 结构体实现位段 三、枚举3.1 概念3.2 定义3.3 枚举的优点3.3.1 提高代码的可读性3.3.2 防止…

VUE2双向绑定的原理

文章目录 VUE2双向绑定的原理1. 什么是双向绑定2. 双向绑定的原理2.1 ViewModel的重要作用2.2 双向绑定的流程 3. 双向绑定的实现3.1 data响应化处理3.2 Compile编译3.3 依赖收集 VUE2双向绑定的原理 1. 什么是双向绑定 讲双向绑定先讲单项绑定&#xff0c;啥叫单项绑定&…

入行FPGA设计工程师需要提前学习哪些内容?

FPGA作为一种灵活可编程的硬件平台&#xff0c;广泛应用于嵌入式系统、通信、数据处理等领域。很多人选择转行FPGA设计工程师&#xff0c;但对于新手来说&#xff0c;可能在学习过程中会遇到一些迷茫和困惑。为了帮助大家更好地准备&#xff0c;本文将详细介绍入行FPGA设计工程…

Mac M1 ComfyUI 中 AnyText插件安装问题汇总?

Q1&#xff1a;NameError: name ‘PreTrainedTokenizer’ is not defined ? 该项目最近更新日期为2024年12月&#xff0c;该时间段的transformers 版本由PyPI 上的 transformers 页面 可知为4.47.1. A1: transformers 版本不满足要求&#xff0c;必须降级transformors &#…

深度学习 Pytorch 神经网络的学习

本节将从梯度下降法向外拓展&#xff0c;介绍更常用的优化算法&#xff0c;实现神经网络的学习和迭代。在本节课结束将完整实现一个神经网络训练的全流程。 对于像神经网络这样的复杂模型&#xff0c;可能会有数百个 w w w的存在&#xff0c;同时如果我们使用的是像交叉熵这样…

Java 大视界 -- 深度洞察 Java 大数据安全多方计算的前沿趋势与应用革新(52)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Docker使用指南(二)——容器相关操作详解(实战案例教学,创建/使用/停止/删除)

目录 1.容器操作相关命令​编辑 案例一&#xff1a; 案例二&#xff1a; 容器常用命令总结&#xff1a; 1.查看容器状态&#xff1a; 2.删除容器&#xff1a; 3.进入容器&#xff1a; 二、Docker基本操作——容器篇 1.容器操作相关命令 下面我们用两个案例来具体实操一…

【C++】STL——list的使用

目录 &#x1f495;1.带头双向链表List &#x1f495;2.list用法介绍 &#x1f495;3.list的初始化 &#x1f495;4.size函数与resize函数 &#x1f495;5.empty函数 &#x1f495;6.front函数与back函数 &#x1f495;7.push_front,push_back,pop_front,pop_back函数…

Java面试题集合篇5:10道基础面试题

文章目录 前言41、多线程使用 ArrayList42、List 和 Set 区别43、HashSet 实现原理44、HashSet检查重复和保证数据不可重复45、BlockingQueue46、Map接口46.1、HashMap实现原理46.2、HashMap在JDK1.7和JDK1.8中不同点46.3、JDK1.7 VS JDK1.8 比较 47、HashMap的put方法流程48、…

控件【QT】

文章目录 控件QWidgetenabledgeometrysetGeometry qrcwindowOpacityQPixmapfonttoolTipfocusPolicystyleSheetQPushButtonRadio ButtionCheck Box显示类控件QProgressBarcalendarWidget 控件 Qt中已经提供了很多内置的控件了(按钮,文本框,单选按钮,复选按钮&#xff0c;下拉框…

docker pull Error response from daemon问题

里面填写 里面解决方案就是挂代理。 以虚拟机为例&#xff0c;将宿主机配置端口设置&#xff0c;https/http端口设为7899 配置虚拟机的http代理&#xff1a; vim /etc/systemd/system/docker.service.d/http-proxy.conf里面填写&#xff0c;wq保存 [Service] Environment…

linux 进程补充

环境变量 基本概念 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如&#xff1a;我们在编写C/C代码的时候&#xff0c;在链接的时候&#xff0c;从来不知道我们的所链接的动态静态库在哪 里&#xff0c;但是照样可以链接成功&#…

一文解释pytorch 中的 squeeze() 和 unsqueeze()函数(全网最详细版)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;零基础入门PyTorch框架_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 …

QT:对象树

1.概念 Qt 中的对象树是一种以树形结构组织 Qt 对象的方式。当创建一个QObject&#xff08;Qt 中大多数类的基类&#xff09;或其派生类的对象时&#xff0c;可以为其指定一个父对象&#xff08;parent&#xff09;。这个对象就会被添加到其父对象的子对象列表中&#xff0c;形…

labview通过时间计数器来设定采集频率

在刚接触labview的时候&#xff0c;笔者通常用定时里的等待函数来实现指令的收发&#xff0c;但是当用到的收发消息比较多时就出现了卡顿&#xff0c;卡死的情况&#xff0c;这是因为当用队列框架时&#xff0c;程序卡在了其中的一个分支里&#xff0c;等通过相应的延时后才可以…

2024最新前端面试题(附答案及解析)

文章目录 HTML篇1、HTML5有哪些新特性&#xff1f;2、介绍下 BFC 及其应用3、内元素和块级元素的区别&#xff1f;4、Doctype作用&#xff1f;标准模式与混杂模式如何区分&#xff1f;5、引入样式时&#xff0c;link和import的区别&#xff1f;6、介绍一下你对浏览器内核的理解…

Linux:文件系统(软硬链接)

目录 inode ext2文件系统 Block Group 超级块&#xff08;Super Block&#xff09; GDT&#xff08;Group Descriptor Table&#xff09; 块位图&#xff08;Block Bitmap&#xff09; inode位图&#xff08;Inode Bitmap&#xff09; i节点表&#xff08;inode Tabl…

Java基础面试题50题

1&#xff0c;""空字符串的作用 package com.neuedu.nineteen;public class Test {public static void main(String[] args) {String s"";for (char i a; i < d; i) {ssi;//输出abc // sis;//输出cba}System.out.println(s);} }如题所示&…

【技海登峰】Kafka漫谈系列(二)Kafka高可用副本的数据同步与选主机制

【技海登峰】Kafka漫谈系列(二)Kafka高可用副本的数据同步与选主机制 一. 数据同步 在之前的学习中有了副本Replica的概念,解决了数据备份的问题。我们还需要面临一个设计难题即:如何处理分区中Leader与Follwer节点数据同步不匹配问题所带来的风险,这也是保证数据高可用的…