【Linux】进程间通信(万字详解) —— 上篇

news2024/11/16 13:33:39

🎇Linux:


  • 博客主页:一起去看日落吗
  • 分享博主的在Linux中学习到的知识和遇到的问题
  • 博主的能力有限,出现错误希望大家不吝赐教
  • 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。

在这里插入图片描述

🍁 🍃 🍂 🌿


目录

  • 🍁1. 进程间通信的介绍
    • 🍂1.1 进程间通信的概念
    • 🍂1.2 进程间通信的目的
    • 🍂1.3 进程间通信的本质
    • 🍂1.4 进程间通信的分类
  • 🍁2. 管道
    • 🍂2.1 什么是管道
    • 🍂2.2 匿名管道
      • 🍃2.2.1 匿名管道的原理
      • 🍃2.2.2 pipe函数
      • 🍃2.2.3 匿名管道使用步骤
      • 🍃2.2.4 管道读写规则
      • 🍃2.2.5 管道的特点
      • 🍃2.2.6 管道的四种特殊情况
      • 🍃2.2.7 管道的大小
    • 🍂2.3 命名管道
      • 🍃2.3.1 命名管道的原理
      • 🍃2.3.2 使用命令创建命名管道
      • 🍃2.3.3 创建一个命名管道
      • 🍃2.3.4 命名管道的打开规则
      • 🍃2.3.5 用命名管道实现serve&client通信
      • 🍃2.3.6 用命名管道实现派发计算任务
      • 🍃2.3.7 用命名管道实现进程遥控
      • 🍃2.3.8 用命名管道实现文件拷贝
      • 🍃2.3.8 命名管道和匿名管道的区别
      • 🍃2.3.9 命令行当中的管道

🍁1. 进程间通信的介绍

🍂1.1 进程间通信的概念

进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。


🍂1.2 进程间通信的目的

  • 数据传输: 一个进程需要将它的数据发送给另一个进程。
  • 资源共享: 多个进程之间共享同样的资源。
  • 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
  • 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

🍂1.3 进程间通信的本质

进程间通信的本质就是,让不同的进程看到同一份资源。

由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。

各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。

在这里插入图片描述

因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。


🍂1.4 进程间通信的分类

管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

🍁2. 管道

🍂2.1 什么是管道

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

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

在这里插入图片描述


🍂2.2 匿名管道

🍃2.2.1 匿名管道的原理

匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。

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

在这里插入图片描述

注意:

  • 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
  • 管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。

🍃2.2.2 pipe函数

pipe函数用于创建匿名管道,pip函数的函数原型如下:

int pipe(int pipefd[2])

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:

数组元素含义
pipefd[0]管道读端的文件描述符
pipefd[1]管道写端的文件描述符

pipe函数调用成功时返回0,调用失败时返回-1。


🍃2.2.3 匿名管道使用步骤

在这里插入图片描述
在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:

  • 父进程调用pipe函数创建管道

在这里插入图片描述

  • 父进程创建子进程

在这里插入图片描述

  • 父进程关闭写端,子进程关闭读端

在这里插入图片描述
注意:

  • 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
  • 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。

我们可以站在文件描述符的角度再来看看这三个步骤:

在这里插入图片描述

在以下代码当中,子进程向匿名管道当中写入10行数据,父进程从匿名管道当中将数据读出。

//child->write, father->read                                                                                                                                                                                                                                                                                                                                                                                                                                                        
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	int fd[2] = { 0 };
	if (pipe(fd) < 0){ //使用pipe创建匿名管道
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); //使用fork创建子进程
	if (id == 0){
		//child
		close(fd[0]); //子进程关闭读端
		//子进程向管道写入数据
		const char* msg = "hello father, I am child...";
		int count = 10;
		while (count--){
			write(fd[1], msg, strlen(msg));
			sleep(1);
		}
		close(fd[1]); //子进程写入完毕,关闭文件
		exit(0);
	}
	//father
	close(fd[1]); //父进程关闭写端
	//父进程从管道读取数据
	char buff[64];
	while (1){
		ssize_t s = read(fd[0], buff, sizeof(buff));
		if (s > 0){
			buff[s] = '\0';
			printf("child send to father:%s\n", buff);
		}
		else if (s == 0){
			printf("read file end\n");
			break;
		}
		else{
			printf("read error\n");
			break;
		}
	}
	close(fd[0]); //父进程读取完毕,关闭文件
	waitpid(id, NULL, 0);
	return 0;
}

在这里插入图片描述


🍃2.2.4 管道读写规则

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将不再保证写入的原子性。


🍃2.2.5 管道的特点

1. 管道内部自带同步与互斥机制。

我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。

临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。

为了避免这些问题,内核会对管道操作进行同步与互斥:

  • 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
  • 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。

也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。


2. 管道的生命周期随进程。

管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。


3. 管道提供的是流式服务

对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:

  • 流式服务: 数据没有明确的分割,不分一定的报文段。
  • 数据报服务: 数据有明确的分割,拿数据按报文段拿。

4. 管道是半双工通信的

在数据通信中,数据在线路上的传送方式可以分为以下三种:

  • 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  • 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  • 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。

在这里插入图片描述


🍃2.2.6 管道的四种特殊情况

在使用管道时,可能出现以下四种特殊情况

  1. 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
  2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
  3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
  4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。

我们可以通过以下代码看看情况四中,子进程退出时究竟是收到了什么信号

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	int fd[2] = { 0 };
	if (pipe(fd) < 0){ //使用pipe创建匿名管道
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); //使用fork创建子进程
	if (id == 0){
		//child
		close(fd[0]); //子进程关闭读端
		//子进程向管道写入数据
		const char* msg = "hello father, I am child...";
		int count = 10;
		while (count--){
			write(fd[1], msg, strlen(msg));
			sleep(1);
		}
		close(fd[1]); //子进程写入完毕,关闭文件
		exit(0);
	}
	//father
	close(fd[1]); //父进程关闭写端
	close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
	int status = 0;
	waitpid(id, &status, 0);
	printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
	return 0;
}

在这里插入图片描述
在这里插入图片描述
由此可知,当发生情况四时,操作系统向子进程发送的是SIGPIPE信号将子进程终止的。


🍃2.2.7 管道的大小

管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?

  • 方法一:man

我们先使用uname -r命令,查看自己使用的Linux版本。

在这里插入图片描述

然后man 7 pipe 中查看pipe capacity

根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。

在这里插入图片描述


  • 方法二:ulimit
ulimit -a 

在这里插入图片描述
根据显示,管道的最大容量是 512 × 8 = 4096 512\times8=4096 512×8=4096 字节。


  • 方法三:暴力自测

若是读端进程一直不读取管道当中的数据,写端进程一直向管道写入数据,当管道被写满后,写端进程就会被挂起。据此,我们可以写出以下代码来测试管道的最大容量。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
	int fd[2] = { 0 };
	if (pipe(fd) < 0){ //使用pipe创建匿名管道
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); //使用fork创建子进程
	if (id == 0){
		//child 
		close(fd[0]); //子进程关闭读端
		char c = 'a';
		int count = 0;
		//子进程一直进行写入,一次写入一个字节
		while (1){
			write(fd[1], &c, 1);
			count++;
			printf("%d\n", count); //打印当前写入的字节数
		}
		close(fd[1]);
		exit(0);
	}
	//father
	close(fd[1]); //父进程关闭写端

	//父进程不进行读取

	waitpid(id, NULL, 0);
	close(fd[0]);
	return 0;
}

在这里插入图片描述可以看到,在读端进程不进行读取的情况下,写端进程最多写65536字节的数据就被操作系统挂起了,也就是说,我当前Linux版本中管道的最大容量是65536字节。


🍂2.3 命名管道

🍃2.3.1 命名管道的原理

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

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

注意:

  • 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。

  • 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。


🍃2.3.2 使用命令创建命名管道

我们可以使用mkfifo命令创建一个命名管道。

在这里插入图片描述
p代表该文件是命名管道文件

使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。

现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。

在这里插入图片描述
当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。


🍃2.3.3 创建一个命名管道

在程序中创建命名管道使用mkfifo函数

int mkfifo(const char *pathname, mode_t mode);

mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)

mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。

若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

mkfifo函数的返回值

  • 命名管道创建成功,返回0。
  • 命名管道创建失败,返回-1。

🍃2.3.4 命名管道的打开规则

1. 如果当前打开操作是为读而打开FIFO时。

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
  • O_NONBLOCK enable:立刻返回成功。

2. 如果当前打开操作是为写而打开FIFO时。

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

🍃2.3.5 用命名管道实现serve&client通信

实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。

//server.cc
#include "comm.hpp"

int main()
{
	umask(0); //将文件默认掩码设置为0
	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
		perror("mkfifo");
		return 1;
	}
	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
	if (fd < 0){
		perror("open");
		return 2;
	}
	char msg[128];
	while (1){
		msg[0] = '\0'; //每次读之前将msg清空
		//从命名管道当中读取信息
		ssize_t s = read(fd, msg, sizeof(msg)-1);
		if (s > 0){
			msg[s] = '\0'; //手动设置'\0',便于输出
			printf("client# %s\n", msg); //输出客户端发来的信息
		}
		else if (s == 0){
			printf("client quit!\n");
			break;
		}
		else{
			printf("read error!\n");
			break;
		}
	}
	close(fd); //通信完毕,关闭命名管道文件
	return 0;
}

而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。

客户端的代码如下:

//client.cc
#include "comm.hpp"

int main()
{
	int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
	if (fd < 0){
		perror("open");
		return 1;
	}
	char msg[128];
	while (1){
		msg[0] = '\0'; //每次读之前将msg清空
		printf("Please Enter# "); //提示客户端输入
		fflush(stdout);
		//从客户端的标准输入流读取信息
		ssize_t s = read(0, msg, sizeof(msg)-1);
		if (s > 0){
			msg[s - 1] = '\0';
			//将信息写入命名管道
			write(fd, msg, strlen(msg));
		}
	}
	close(fd); //通信完毕,关闭命名管道文件
	return 0;
}

对于如何让客户端和服务端使用同一个命名管道文件,这里我们可以让客户端和服务端包含同一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。

共用头文件的代码如下:

//comm.hpp
#pragma once

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>

#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道

代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。

在这里插入图片描述
接着再将客户端也运行起来,此时我们从客户端写入的信息被客户端写入到命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。

在这里插入图片描述

服务端和客户端之间的退出关系

当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。

在这里插入图片描述
当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。

通信是在内存当中进行的

若是我们只让客户端向管道写入数据,而服务端不从管道读取数据,那么这个管道文件的大小会不会发生变化呢?

//server.cc
#include "comm.hpp"

int main()
{
	umask(0); //将文件默认掩码设置为0
	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
		perror("mkfifo");
		return 1;
	}
	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
	if (fd < 0){
		perror("open");
		return 2;
	}
	while (1){
		//服务端不读取管道信息
	}
	close(fd); //通信完毕,关闭命名管道文件
	return 0;
}

可以看到,尽管服务端不读取管道当中的数据,但是管道当中的数据并没有被刷新到磁盘,使用ll命令看到命名管道文件的大小依旧为0,也就说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。

在这里插入图片描述


🍃2.3.6 用命名管道实现派发计算任务

需要注意的是两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行某些处理的。

这里我们以客户端向服务端派发计算任务为例,客户端通过管道向服务端发送双操作数的计算请求,服务端接收到客户端的信息后需要计算出相应的结果。

这里我们无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。

//server.cc
#include "comm.hpp"

int main()
{
	umask(0); //将文件默认掩码设置为0
	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
		perror("mkfifo");
		return 1;
	}
	int fd = open(FILE_NAME, O_RDONLY); //打开命名管道文件
	if (fd < 0){
		perror("open");
		return 2;
	}
	char msg[128];
	while (1){
		msg[0] = '\0'; //每次读之前将msg清空
		//从命名管道当中读取信息
		ssize_t s = read(fd, msg, sizeof(msg)-1);
		if (s > 0){
			msg[s] = '\0'; //手动设置'\0',便于输出
			printf("client# %s\n", msg);
			//服务端进行计算任务
		    const char* lable = "+-*/%";
			char* p = msg;
			int flag = 0;
			while (*p){
				switch (*p){
				case '+':
					flag = 0;
					break;
				case '-':
					flag = 1;
					break;
				case '*':
					flag = 2;
					break;
				case '/':
					flag = 3;
					break;
				case '%':
					flag = 4;
					break;
				}
				p++;
			}
			char* data1 = strtok(msg, "+-*/%");
			char* data2 = strtok(NULL, "+-*/%");
			int num1 = atoi(data1);
			int num2 = atoi(data2);
			int ret = 0;
			switch (flag){
			case 0:
				ret = num1 + num2;
				break;
			case 1:
				ret = num1 - num2;
				break;
			case 2:
				ret = num1 * num2;
				break;
			case 3:
				ret = num1 / num2;
				break;
			case 4:
				ret = num1 % num2;
				break;
			}
			printf("%d %c %d = %d\n", num1, lable[flag], num2, ret); //打印计算结果
		}
		else if (s == 0){
			printf("client quit!\n");
			break;
		}
		else{
			printf("read error!\n");
			break;
		}
	}
	close(fd); //通信完毕,关闭命名管道文件
	return 0;
}

此时服务端接收到客户端的信息后,需要进行的处理动作就不是将其打印到显示器了,而是需要将信息经过进一步的处理,从而得到相应的结果。
在这里插入图片描述


🍃2.3.7 用命名管道实现进程遥控

比较有意思的是,我们可以通过一个进程来控制另一个进程的行为,比如我们从客户端输入命令到管道当中,再让服务端将管道当中的命令读取出来并执行。

下面我们只实现了让服务端执行不带选项的命令,若是想让服务端执行带选项的命令,可以对管道当中获取的命令进行解析处理。这里的实现非常简单,只需让服务端从管道当中读取命令后创建子进程,然后再进行进程程序替换即可。

这里也无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。

//server.cc
#include "comm.hpp"

int main()
{
	umask(0); //将文件默认掩码设置为0
	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
		perror("mkfifo");
		return 1;
	}
	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
	if (fd < 0){
		perror("open");
		return 2;
	}
	char msg[128];
	while (1){
		msg[0] = '\0'; //每次读之前将msg清空
		//从命名管道当中读取信息
		ssize_t s = read(fd, msg, sizeof(msg)-1);
		if (s > 0){
			msg[s] = '\0'; //手动设置'\0',便于输出
			printf("client# %s\n", msg);
			if (fork() == 0){
				//child
				execlp(msg, msg, NULL); //进程程序替换
				exit(1);
			}
			waitpid(-1, NULL, 0); //等待子进程
		}
		else if (s == 0){
			printf("client quit!\n");
			break;
		}
		else{
			printf("read error!\n");
			break;
		}
	}
	close(fd); //通信完毕,关闭命名管道文件
	return 0;
}

此时服务端接收到客户端的信息后,便进行进程程序替换,进而执行客户端发送过来的命令。
在这里插入图片描述


🍃2.3.8 用命名管道实现文件拷贝

生成一个txt文件,然后进行拷贝

在这里插入图片描述

我们要做的就是,让客户端将file.txt文件通过管道发送给服务端,在服务端创建一个file-bat.txt文件,并将从管道获取到的数据写入file-bat.txt文件当中,至此便实现了file.txt文件的拷贝。

在这里插入图片描述

//server.cc
#include "comm.hpp"

int main()
{
	umask(0); //将文件默认掩码设置为0
	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
		perror("mkfifo");
		return 1;
	}
	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
	if (fd < 0){
		perror("open");
		return 2;
	}
	//创建文件file-bat.txt,并以写的方式打开该文件
	int fdout = open("file-bat.txt", O_CREAT | O_WRONLY, 0666);
	if (fdout < 0){
		perror("open");
		return 3;
	}
	char msg[128];
	while (1){
		msg[0] = '\0'; //每次读之前将msg清空
		//从命名管道当中读取信息
		ssize_t s = read(fd, msg, sizeof(msg)-1);
		if (s > 0){
			write(fdout, msg, s); //将读取到的信息写入到file-bat.txt文件当中
		}
		else if (s == 0){
			printf("client quit!\n");
			break;
		}
		else{
			printf("read error!\n");
			break;
		}
	}
	close(fd); //通信完毕,关闭命名管道文件
	close(fdout); //数据写入完毕,关闭file-bat.txt文件
	return 0;
}

而客户端需要做的就是,以写的方式打开这个已经存在的命名管道文件,再以读的方式打开file.txt文件,之后需要做的就是将file.txt文件当中的数据读取出来并写入管道当中即可。

//client.cc
#include "comm.hpp"

int main()
{
	int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
	if (fd < 0){
		perror("open");
		return 1;
	}
	int fdin = open("file.txt", O_RDONLY); //以读的方式打开file.txt文件
	if (fdin < 0){
		perror("open");
		return 2;
	}
	char msg[128];
	while (1){
		//从file.txt文件当中读取数据
		ssize_t s = read(fdin, msg, sizeof(msg));
		if (s > 0){
			write(fd, msg, s); //将读取到的数据写入到命名管道当中
		}
		else if (s == 0){
			printf("read end of file!\n");
			 break;
		}
		else{
			printf("read error!\n");
			break;
		}
	}
	close(fd); //通信完毕,关闭命名管道文件
	close(fdin); //数据读取完毕,关闭file.txt文件
	return 0;
}

在这里插入图片描述
file.txt 和 file-bat.txt 文件当中的内容相同,拷贝文件成功。
在这里插入图片描述

使用管道实现文件的拷贝有什么意义?

因为这里是使用管道在本地进行的文件拷贝,所以看似没什么意义,但我们若是将这里的管道想象成“网络”,将客户端想象成“shell”,再将服务端想象成“centos服务器”。那我们此时实现的就是文件上传的功能,若是将方向反过来,那么实现的就是文件下载的功能。


🍃2.3.8 命名管道和匿名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,由open函数打开。
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。

🍃2.3.9 命令行当中的管道

创建一个text.txt文件

在这里插入图片描述
我们可以利用管道(“|”)同时使用cat命令和grep命令,进而实现文本过滤。

在这里插入图片描述

由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。

下面通过管道(“|”)连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。

在这里插入图片描述

而它们的父进程实际上就是命令行解释器,这里为bash

在这里插入图片描述
也就是说,由管道(“|”)连接起来的各个进程是有亲缘关系的,它们之间互为兄弟进程。

现在我们已经知道了,若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。


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

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

相关文章

谷粒学院——第九章、阿里云视频点播

阿里云视频点播 开通 地址&#xff1a; 上传测试 开通以后&#xff0c;点击控制台&#xff0c;然后选择音/视频&#xff1a; 注意&#xff1a;先点击启用存储地址再上传。 添加转码模版&#xff1a; 开发文档 官方地址&#xff1a;https://help.aliyun.com/p…

批量统计不同块的数量

CAD收集块的数量一般采用FI等命令&#xff0c;或者使用天正等软件&#xff0c;这些方法或多或少都存在某些问题。这时就可以编写插件满足不同场景的使用。已应用到实际工作中。 一、界面及其功能 采用c#制作cad插件&#xff0c;框选待统计范围&#xff0c;直接输出到表格中&…

数据库连接超时的处理

报错信息&#xff1a;Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:695)at com.zaxxer.hikari.pool.H…

Java高手速成 | 数据库实训:图书馆管理系统建模

图书馆管理系统是常见的管理信息系统&#xff0c;考虑到系统的推广性&#xff0c;本系统采用SQL SERVER2000作为数据库。并且采用PowerDesigner进行数据建模&#xff0c;从而自动生成sql脚本。 01、数据库概念设计 1. 数据库表设计 管理员表admin&#xff1a;管理员编号&am…

[JavaEE] volatile与wait和notify

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录 一.volatile 关键字. 1.volatile 能保证内存可见性…

12个爆款 Java 开源项目

1JavaGuidehttps://github.com/Snailclimb/JavaGuide Star 10503【Java学习面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。2symphonyhttps://github.com/b3log/symphony Star 6664一款用 Java 实现的现代化社区&#xff08;论坛/BBS/社交网络/博客&#xff09;平台…

17-Golang中的包

Golang中的包基本介绍包的三大作用相关说明包的注意事项和细节说明基本介绍 1.在实际的开发中&#xff0c;我们往往需要在不同的文件中&#xff0c;去调用其他文件的定义的幻术&#xff0c;比如main.go中&#xff0c;去使用utils.go文件中的函数2.包的本质就是创建不同的文件夹…

vue简单的数据传输

很久没有水文了&#xff0c;最近又得了新冠才好&#xff0c;学习也没什么进度&#xff0c;先复习下之前的组件的数据传输吧&#xff01; props传值 这个很简单就是在组件标签上转递数据&#xff0c;值得注意的是如果不使用v-bind:&#xff08;&#xff1a;&#xff09;,转递的…

任务二:Web隐藏信息获取

任务二:Web隐藏信息获取 任务环境说明: 服务器场景名:web20200604服务器场景用户名:未知通过本地PC中渗透测试平台Kali使用Nmap扫描目标靶机HTTP服务子目录,将扫描子目录命令所需参数及第四条扫描结果关键目录以&符号拼接,作为Flag提交(例:-p 22&/root/); …

【信息学CSP-J近16年历年真题64题】真题练习与解析 第11题之纪念品

纪念品 描述 小伟突然获得一种超能力,他知道未来 T 天 N 种纪念品每天的价格。某个纪念品 的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。 每天,小伟可以进行以下两种交易无限次: 任选一个纪念品,若手上有足够金币,以当日价格购买该…

Android---DrawerLayout + NavigationView

现在 Android Studio 已经直接提供左滑菜单功能&#xff0c;只需要在创建新项目时选择 Navigation Drawer Activity 就可以直接创建一个有左滑菜单功能的 APP。 目录 DrawerLayout NavigationView android:src 与 app:srcCompat fitsSystemWindows DrawerLayout …

数据报告重要的是业务看得懂

一、前言上篇文章我们从指标库的角度梳理了指标是如何计算出来的&#xff0c;确保业务人员有数可寻&#xff0c;但对于业务人员而言&#xff0c;并不是所有的指标都看得懂&#xff0c;也不是所有的指标都需要理解&#xff0c;笔者见过太多不知所云的数据报告&#xff0c;各种模…

Maven高级-分模块拆分

Maven高级 分模块开发与设计 聚合 继承 属性 版本管理 资源配置 多环境开发配置 跳过测试 私服 分模块开发与设计 拷贝原始项目中对应的相关内容到ssm_pojo模块中 ​ 实体类&#xff08;User&#xff09; ​ 配置文件&#xff08;无&#xff09; User.java package com.i…

S32K144-S32DS 导入/编译/烧录 遇到的问题

找到你开发套件软件安装包的路径&#xff0c;里面有四个例子&#xff0c;任选一个&#xff1b;不要忘记复制到工作区 问题一&#xff1a;修改电机库的路径 问题二&#xff1a;freemaster路径问题 编译还是报错&#xff0c;好像还必须安装FreeMaster 安装之后&#xff0c;还是这…

电子招标采购系统源码:构建高效智能数字化采购

过去几十年&#xff0c;公用事业行业发生了重大变化。能源需求的转变导致企业利润率的波动&#xff0c;但不是运营成本的波动。 许多公用事业公司通过后勤部门流程自动化来削减成本&#xff0c;比如招采流程自动化。 在招采活动中&#xff0c;人工招采会产生盲点。由于公共事业…

【MySQL】MySQL视图原理与实战(MySQL专栏启动)

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;专注于研究 Java/Liunx内核/C及汇编/计算机底层原理/源码&#xff0c;就职于大型金融公司后端高级工程师&#xff0c;擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。 &#x1f4…

ArcGIS基础实验操作100例--实验51CAD转要素类

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验51 CAD转要素类 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&#…

多线程之线程安全问题

1.线程安全示例 class Count{int a 0;public void add(){a;} } public class ThreadDemo8 {public static void main(String[] args) {Count count new Count();Thread t1 new Thread(()->{for (int i 0; i < 5_0000; i) {count.add();}});Thread t2 new Thread(()…

这款企业报表工具给你灵活的数据查询体验

随着对BI应用程度的加深&#xff0c;用户需要连接和管理的数据越来越多&#xff0c;也越来越复杂。电子表格软件支持丰富的数据源接入&#xff0c;但一般并不能直接使用接入的业务库直接进行数据分析。所以在报表开发前的取数过程&#xff0c;把需要的数据整合成一个数据集合&a…

Ajax技术

全局刷新和局部刷新 在B/S 结构的项目中&#xff0c; 浏览器端负责把用户的请求和参数通过网络发送给服务器,服务端负责接收请求&#xff0c;并将处理的结果返回给浏览器。浏览器端负责展示响应结果给用户。 全局刷新 整个浏览器被新的数据覆盖。在网络中传输大量的数据。浏…