【Linux的文件篇章 - 管道文件】

Linux学习笔记---013

Linux的管道文件

前言:

前篇开始进行了解学习Linux的磁盘文件等相关知识内容,接下来学习关于Linux的管道文件、共享内存、消息队列和信号量的基本知识,深入地了解这个强大的开源操作系统。
/知识点汇总/

1、进程间通信

1.1、进程为什么要通信?

进程间也是需要某种协同的,如何协同的前提条件就是通信。
数据是有类别的,通知就绪的,单纯的要传递的信息,以及控制信息。

事实:进程是具有独立性的。

进程 = 内核的数据结构 + 代码和数据

1.2、进程如何通信?

a、进程间通信,成本可能会稍微高一些。(因为进程是独立的)

比如:进程a把数据给进程b,进程具有独立性,所以数据无法直接传递的。(父子进程fork的方式,只是处于只读,传递信息和一直可以传递信息是有区别的,所以frok是处于可以传递信息,但不能一直传递,因为是基于写时拷贝的)

b、进程间通信的前提:先让不同的进程,看到同一份(操作系统的)资源("一段内存")

因为进程a和进程b,进程间具有独立性,相互之间的空间和数据等资源无法共享,就通过操纵系统实现,让它们能够在"一段内存中交换和访问数据"。

那么操作系统怎么知道什么时候创建共享区域呢?

1.一定是某一个进程先需要通信,让OS创建一个共享资源。

2.OS必须提供很多的系统调用。 -- OS创建的共享资源不同,系统调用接口不同 ---- 进程间通信的方式就会存在不同。

1.3、进程通信的方式?

a、存在一定约定的标准(专利)
b、消息队列、共享内存、信号量

直接复用内核代码直接通信呢?

进程间独立,对于文件系统无关。引出管道

1.命名管道

2.匿名管道

2、匿名管道

2.1、理解一种现象

为什么父子进程会向同一个显示器终端打印数据。

因为父子进程中,子进程会继承父进程的文件描述符表,进而指向同一个显示器文件,用同一个进程inode,也就把数据写入同一个缓冲区里,所以系统刷新时,就刷新到同一个显示器中。

进程默认会打开三个标准输入/输出:0,1,2怎么做到的呢?

都属于bash的子进程,所以是bash打开了。

进程默认也就打开了,我们只要约定好即可。

close():为什么我们子进程主动close(0/1/2),不影响父进程继续使用显示器文件呢?

本质是由于,之前了解到的引用计数,通过引用计数能够知道有多少文件指针指向它,那么就通过引用计数的指针依次释放指定的次数。

2.2、基本概念和管道原理

那么在通过操作系统,基于文件系统上,在内存中建立的"一段共享内存"就称为管道资源。 -- 管道文件

注意:

1.管道只允许单向通信 --- 半双工通信

2.管道与文件的操作区别,就在于不用刷新到磁盘了。

既然父子进程会关闭不需要的fd,那么为什么在创建父子进程时,要默认打开呢?可以选择不关闭吗?

答:为了让子进程继承下去(父进程只有,那么子进程就只有读,父进程有读/写,那么子进程就继承读/写)。

可以不关闭,建议关闭,防止误读或误写,以及系统资源的浪费。

既然管道不用再刷新到磁盘中,那么需要重新设计通信接口吗?

答:创建管道的系统调用,底层实际就是open,只是不用了磁盘部分。

int pipe(int pipefd[2]);

不需要文件路径和文件名,其次也被称为匿名文件 -- 匿名管道

管道只能实现单向通信。我实际就想要实现双向通信呢?

答:就创建两个管道。

为什么管道是单向通信的呢?

答:a.方便复用代码,减少开发成本。

b.数据易混淆,涉及数据的区分等复杂的操作,所以不采用双向,只需要满足传输数据。单向即可满足。

3、管道的使用

3.1、代码样例

测试代码: 子进程交给父进程的通信

cpp 复制代码
//管道的使用

#include <iostream>
#include <unistd.h>
//c++版本的errno.h,和c++版本的string.h
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string>

using namespace std;

const int Size = 1024;

string getOtherMessage()
{
	static int cnt = 0;
	string messageid = to_string(cnt);
	cnt++;
	pid_t self_id getpid();
	string  stringpid = to_string(self_id);
	//拼接
	string message = "messageid: ";
	message += messageid;
	message += " my pid is : ";
	message += stringpid;

	return message;
}

//子进程写入
void SubProcessWrite(int wfd)
{
	string message = "father,I am your son process!";
	while (true)
	{
		string info = message + getOtherMessage();//拼接得到,子进程写入管道的信息
		write(wfd, info.c_str(), info.size());//写入管道时,用的是系统调用write,没有写入'\0',也没有必要不使用时一同写入'\0'
		sleep(5);

		//情况2:管道满64kb,ubantu 20.02版本
		char c = 'A';
		write(wfd, &c, 1);
		cout << "pipesize" << ++pipesize << endl;
		break;
	}

	cout << "child quit ..." << endl;
}

//父进程读取
void FatherProcessRead(int rfd)
{
	char inbuffer[Size];//
	while (true)
	{
		//sleep(5);
		ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);//因为没有写入'\0',所以sizeof读取到时要减1
		if (n > 0)
		{
			inbuffer[n] = 0;//所以需要时,要手动添加'\0'
			cout << "father get message" << inbuffer << endl;
		}

		cout << "father get return val: " << n << endl;
	}
}

int main()
{
	//1.创建管道
	int pipefd[2];
	int n = pipe(pipefd);//pipe的参数属于输出型参数,rfd和wfd
	if (n != 0)
	{
		cerr << "errno:" << errno << ": " << "srrstring : " << strerror(errno) << endl;
		return 1;
	}

	//打印文件描述符,预测是3和4,因为文件描述符默认代开三个0,1,2.
	cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;
	sleep(1);
	//得到的是管道的读写端:
	//pipefd[0] --> 0 -->r
	//pipefd[1] --> 1 -->w

	//2.创建子进程
	//3.关闭不需要的文件描述符(以子进程写,父进程读为例)
	pid_t id = fork();
	if (id == 0)
	{
		cout << "子进程关闭不需要的fd,准备开始发消息" << endl;
		sleep(1);
		//子进程 -- write
		//....

		//关闭不需要的文件描述符
		close(pipefd[0]); // 关闭读
		//写入
		SubProcessWrite(pipefd[1]);
		close(pipefd[1]); // 写完后,关闭写
		exit(0);
	}

	cout << "父进程关闭不需要的fd,准备开始收消息" << endl;
	sleep(1);
	//父进程 -- read
	//....
	//关闭不需要的文件描述符
	close(pipefd[1]); // 关闭写
	//读取
	FatherProcessRead(pipefd[0]);
	close(pipefd[0]); // 读完后,关闭读

	//到此仍然没有进行通信,只是在建议一个共享的内存空间 --管道
	//即:让不同的进程看到同一块资源。
	//以上那么多操作,也就说明了进程间的通信是需要一定的成本的。(因为进程间具有独立性)

	//4.进程间通信
	//SubProcessWrite() 
	//FatherProcessRead()

	//5.防止僵尸进程
	pid_t rid = waitpid(id, nullptr, 0);
	if (rid > 0)
	{
		cout << "wait child process done" << endl;
	}

	return 0;
}

3.2、如何使用管道通信呢?

int pipe(int pipefd[2]); --- 参数int pipefd[2],属于输出型参数,表示管道的输入或输出的端口

既然管道也是文件,那么文件的操作依然通用于管道文件。

read / write

根据之前的知识,知道的fork之后,子进程是拿到父进程的数据的,是属于通信吗?

严格意义上讲并不是属于通信,对于子进程来讲,它是只读的(无法修改,无法阻止接收通信,只能父进程交给子进程),完全是由父进程决定得到的资源,是单向的数据。

所以再结合写时拷贝,对方是看不见通信信息的。

所以简单的通过全局变量的缓冲区,使得双方获取对方数据是行不通的。

代码验证,测试代码:

cpp 复制代码
#include <iostream>
#include <unistd.h>
//c++版本的errno.h,和c++版本的string.h
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string>

using namespace std;

const int Size = 1024;

string getOtherMessage()
{
	static int cnt = 0;
	string messageid = to_string(cnt);
	cnt++;
	pid_t self_id getpid();
	string  stringpid = to_string(self_id);
	//拼接
	string message = "messageid: ";
	message += messageid;
	message += " my pid is : ";
	message += stringpid;

	return message;
}

//子进程写入
void SubProcessWrite(int wfd)
{
	string message = "father,I am your son process!";
	char c = 'A';
	while (true)
	{
		//情况5:
		cerr << " ++++++++++++++++++++++ " << endl;
		string info = message + getOtherMessage();//拼接得到,子进程写入管道的信息
		write(wfd, info.c_str(), info.size());//写入管道时,用的是系统调用write,没有写入'\0',也没有必要不使用时一同写入'\0'
		//sleep(5);
		cerr << info << endl;
		//情况2:管道满64kb,ubantu 20.02版本
		//	write(wfd, &c, 1);
		//	cout << "pipesize" << ++pipesize << "write charctor" << c << endl;
		//	c++;
		//	if (c == 'G') break;
		//	sleep(1);
	}

	cout << "child quit ..." << endl;
}

//父进程读取
void FatherProcessRead(int rfd)
{
	char inbuffer[Size];//
	while (true)
	{
		sleep(2);
		cout << " -------------- " << endl;
		ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);//因为没有写入'\0',所以sizeof读取到时要减1
		if (n > 0)
		{
			inbuffer[n] = 0;//所以需要时,要手动添加'\0'
			cout << "father get message" << inbuffer << endl;
		}
		else if (n == 0)
		{
			cout << "client quit,father get return vsl: " << n << " father quit too!" << endl;
			break;
		}
		else if (n < 0)
		{
			cerr << "read error" << endl;
			break;
		}
		//情况:5
		sleep(1);
		break;
	}
}

int main()
{
	//1.创建管道
	int pipefd[2];
	int n = pipe(pipefd);//pipe的参数属于输出型参数,rfd和wfd
	if (n != 0)
	{
		cerr << "errno:" << errno << ": " << "srrstring : " << strerror(errno) << endl;
		return 1;
	}

	//打印文件描述符,预测是3和4,因为文件描述符默认代开三个0,1,2.
	cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;
	sleep(1);
	//得到的是管道的读写端:
	//pipefd[0] --> 0 -->r
	//pipefd[1] --> 1 -->w

	//2.创建子进程
	//3.关闭不需要的文件描述符(以子进程写,父进程读为例)
	pid_t id = fork();
	if (id == 0)
	{
		cout << "子进程关闭不需要的fd,准备开始发消息" << endl;
		sleep(1);
		//子进程 -- write
		//....

		//关闭不需要的文件描述符
		close(pipefd[0]); // 关闭读
		//写入
		SubProcessWrite(pipefd[1]);
		close(pipefd[1]); // 写完后,关闭写
		exit(0);
	}

	cout << "父进程关闭不需要的fd,准备开始收消息" << endl;
	sleep(1);
	//父进程 -- read
	//....
	//关闭不需要的文件描述符
	close(pipefd[1]); // 关闭写
	//读取
	FatherProcessRead(pipefd[0]);
	cout << "5s, father close rfd" << endl;
	sleep(5);
	close(pipefd[0]); // 读完后,关闭读

	//到此仍然没有进行通信,只是在建议一个共享的内存空间 --管道
	//即:让不同的进程看到同一块资源。
	//以上那么多操作,也就说明了进程间的通信是需要一定的成本的。(因为进程间具有独立性)

	//4.进程间通信
	//SubProcessWrite() 
	//FatherProcessRead()
	
	//5.防止僵尸进程
	int status = 0;
	pid_t rid = waitpid(id, nullptr, 0);
	if (rid > 0)
	{
		cout << "wait child process done,exit sig: " << (status&0x7f) << endl;
		cout << "wait child process done,exit code(ign): " << ((status>>8)&0xFF) << endl;
	}

	return 0;
}

3.3、管道的4种情况

1.可能会存在被多个进程同时访问的情况(并发),数据都不一致问题。

2.如果管道内部是空的 && write fd 没有关闭,读取条件不具备,读进程会被阻塞 --- wait --- 读取条件具备再写入数据。

3.管道被写满了 && read fd 不读且没有关闭,管道被写满,写进程会被阻塞(管道被写满 -- 写条件不具备) -- wait --- 写条件具备 --》读取数据,管道一直在读 && 写端关闭了wfd,读端read返回值读到了0,表示读到了文件结尾。

5.rfd直接关闭,写端wfd一直再进行写入?处于水管出口堵塞了,还一直灌水,属于无用功。对于操作系统不会做这种浪费时间浪费空间的事情,没有意义。操作系统会直接杀掉马,这种坏管道。

所以对于此类出异常的管道,操作系统会主动发送信号,kill SIGPIPE杀掉该管道。

3.4、管道的5种特征

1.匿名管道:只限于具有血缘关系的进程之间,进行通信,常用于父子进程之间的通信。(因为父子进程有一个"天生的"前提条件:都能看到同一份(操作系统的)资源("一段内存"))

2.管道内部,自带进程之间同步的机制。(多执行流执行代码时,具有明显的顺序性)

3.管道文件的生命周期是随进程的。

4.管道文件在通信的时候,是面向字节流的(有些挑战)

面向字节流最典型的特点就是:

write的次数与读取的次数不是一一匹配的。

5.管道通信的模式,是一种特殊的半双工模式。

补充:PIPE_BUF

因为管道通信属于特殊的半双工,所以有关于管道大小的两点:

1.写入的字节大小小于PIPE_BUF的大小时,会被认为是原子的,也就是小于规定的范围的或者说属于一个单元的,即这种情况下是安全的,不会出现写到一半被读取走。

2.PIPE_BUF的大小通常是512byte,而Linux中是4096byte.

3.5、管道的应用场景

1.命令行中的|,就是匿名管道的应用。

2.进程池

比如提前创建fork一批子进程,有任务就通过每一个管道,对接每一个子进程;
从而父进程对接每一个管道的写端,每一个子进程对应与其对应的读端。

这种提前创建好进程的方式就是进程池,大大节约了成本,使其不用单独创建单独的进程了,直接通过各个管道派遣任务就行了。
并且管道里没有数据时,各个子进程(work进程)就处于阻塞等待,等待分配的任务;

所以父进程(master)向哪一个管道写入,就会唤醒哪一个进程来处理任务。(进程间+管道就处于的概念就是,进程的协同)

其中,父进程最好要将任务均衡的划分给每一个子进程,就称为负载均衡。

测试代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <string>
#include <unistd.h>
#include <vector>
#include "Task.hpp"
#include <sys/wait.h>

using namespace std;

class Channel
{
public:
	Channel(int wfd, pid_t id, const string &name)
		:_wfd(wfd), _subprocessid(id),_name(name)
	{}
	int GetWfd()
	{
		return _wfd;
	}
	pid_t GetProcessId()
	{
		return _subprocessid;
	}
	string GetName()
	{
		return _name;
	}
	void CloseChannel()
	{
		close(_wfd);
	}
	void Wait()
	{
		pid_t rid = waitpid(_subprocessid,nullptr,0);
		if (rid > 0)
		{
			cout << "wait " << rid << " success" << endl;
		}
	}
	~Channel()
	{}
private:
	int _wfd;
	pid_t _subprocessid;
	string _name;
};

void work(int rfd)
{
	while (true)
	{
		int command = 0;
		int n = read(rfd, &command, sizeof(command));
		if (n == sizeof(int))
		{
			cout << "pid is: " << getpid() << " handler task" << endl;
			ExcuteTask(command);
		}
		else if (n == 0)
		{
			cout << "sub process: " << getpid() << " quit" << endl;
			break;
		}
	}
}


//创建信道和子进程
/**/
void test_pipepool(int argc, char* argv[])
{
	if (argc != 2)
	{
		cerr << "Usage: " << argv[0] << " processnum" << endl;
		return ;
	}
	int num = stoi(argv[1]);
	vector<Channel> channels;

	//创建信道和子进程
	for (int i = 0; i < num; i++)
	{
		//1.创建管道
		int pipefd[2] = { 0 };
		int n = pipe(pipefd);
		if (n < 0) exit(1);
		//3.创建子进程
		pid_t id = fork();
		if (id == 0)
		{
			//child --- read 处理任务
			close(pipefd[1]);//关闭写端
			work(pipefd[0]);
			close(pipefd[0]);//读完读端
			exit(0);
		}
		//3.构建一个channel1名称
		string channel_name = "Channel-" + to_string(i);
		//father --- write
		close(pipefd[0]);//关闭读端
		//a、子进程的Pid, b、父进程关心的管道的写端
		channels.push_back(Channel(pipefd[1], id, channel_name));
	}

	//for test
	for (auto& channel : channels)
	{
		cout << "==========================" << endl;
		cout << channel.GetName() << endl;
		cout << channel.GetProcessId() << endl;
		cout << channel.GetWfd() << endl;
	}
}

//优化
/**/
//形参类型和命名规范
//const   ---> 只读型参数
//const & --> 输出型参数
//& --->  输入输出型参数
//* ---> 输出型参数
void CreateChannelAndSub(int num, vector<Channel>* channels)
{
	//创建信道和子进程
	for (int i = 0; i < num; i++)
	{
		//1.创建管道
		int pipefd[2] = { 0 };
		int n = pipe(pipefd);
		if (n < 0) exit(1);
		//3.创建子进程
		pid_t id = fork();
		if (id == 0)
		{
			//child --- read 处理任务
			close(pipefd[1]);//关闭写端
			work(pipefd[0]);
			close(pipefd[0]);//读完读端
			exit(0);
		}
		//3.构建一个channel1名称
		string channel_name = "Channel-" + to_string(i);
		//father --- write
		close(pipefd[0]);//关闭读端
		//a、子进程的Pid, b、父进程关心的管道的写端
		channels->push_back(Channel(pipefd[1], id, channel_name));
	}

}

//轮询方案
int NextChannel(int channelnum)
{
	static int next = 0;
	int channel = next;
	next++;
	next %= channelnum;
	return channel;
}

void SendTaskCommand(Channel& channel, int taskcommand)
{
	write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}

//优化1
void test_pipepool2(int argc, char* argv[])
{
	if (argc != 2)
	{
		cerr << "Usage: " << argv[0] << " processnum" << endl;
		return ;
	}
	int num = stoi(argv[1]);
	LoadTask();//装载任务

	vector<Channel> channels;
	//1.创建信道和子进程
	CreateChannelAndSub(num, &channels);

	//2.通过channel控制子进程
	while (true)
	{
		sleep(1);
		//a、选择一个任务
		int taskcommand = SelectTask();
		//b、选择一个信道和进程
		int channel_index = NextChannel(channels.size());
		//c、发送任务
		SendTaskCommand(channels[channel_index], taskcommand);
		cout << "-----------------" << endl;
		cout << "taskcommand: " << taskcommand << " channel: " << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << endl;
	}

	//3.回收管道和子进程
}

//优化2
void ctrlProcessOnce(vector<Channel>& channels)
{
	sleep(1);
	//a、选择一个任务
	int taskcommand = SelectTask();
	//b、选择一个信道和进程
	int channel_index = NextChannel(channels.size());
	//c、发送任务
	SendTaskCommand(channels[channel_index], taskcommand);
	cout << "-----------------" << endl;
	cout << "taskcommand: " << taskcommand << " channel: " << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << endl;
}

void ctrlProcess(vector<Channel>& channels, int times = -1)
{
	if (times > 0)
	{
		while (times--)
		{
			ctrlProcessOnce(channels);
		}
	}
	else
	{
		while (true)
		{
			ctrlProcessOnce(channels);
		}
	}
}

void CleanUpChannel(vector<Channel>& channels)
{
	//关闭管道
	for (auto& channel : channels)
	{
		channel.CloseChannel();
	}
	//注意:防止僵尸进程
	//回收子进程
	for (auto& channel : channels)
	{
		channel.Wait();
	}
}

void test_pipepool3(int argc, char* argv[])
{
	if (argc != 2)
	{
		cerr << "Usage: " << argv[0] << " processnum" << endl;
		return;
	}
	int num = stoi(argv[1]);
	LoadTask();//装载任务

	vector<Channel> channels;
	//1.创建信道和子进程
	CreateChannelAndSub(num, &channels);

	//2.通过channel控制子进程
	ctrlProcess(channels);

	//3.回收管道和子进程
	//a、关闭所有的写端,返回值为0 --》子进程就自动退出,最后回收即可
	//b、回收子进程
	CleanUpChannel(channels);
}


//先描述,再组织
int main(int argc, char* argv[])
{
	//创建信道和子进程
	test_pipepool(argc, argv);
	//优化
	test_pipepool2(argc, argv);
	//优化
	test_pipepool3(argc, argv);
	return 0;
}

通过函数指针数组管理任务码,分配子进程完成任务功能.

可规定一个固定长度的4字节数组下标,写和读都以4字节为单位识别。 -- 任务码

测试代码:

.hpp默认属于开源程序,因为声明和定义是写在一起的

cpp 复制代码
//.hpp默认属于开源程序,因为声明和定义是写在一起的
#pragma once

#include <iostream>
#include <ctime>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;

#define TaskNum 3

typedef void (*task_t)();//task_t 函数指针

void Print()
{
	cout << "T am a print task" << endl;
}

void Douwnload()
{
	cout << "T am a download task" << endl;
}

void Flush()
{
	cout << "T am a flush task" << endl;
}

task_t tasks[TaskNum];

void LoadTask()
{
	srand(time(nullptr) ^ getpid());
	tasks[0] = Print;
	tasks[1] = Douwnload;
	tasks[2] = Flush;
}

void ExcuteTask(int number)
{
	if (number < 0 || number > 2)
		return;
	tasks[number]();
}

int SelectTask()
{
	return rand() % TaskNum;
}

//回调函数 --- work本质也是任务
void work()
{
	while (true)
	{
		int command = 0;
		int n = read(0, &command, sizeof(command));//重定向到标准输入去读取了
		if (n == sizeof(int))
		{
			cout << "pid is: " << getpid() << " handler task" << endl;
			ExcuteTask(command);
		}
		else if (n == 0)
		{
			cout << "sub process: " << getpid() << " quit" << endl;
			break;
		}
	}
}

4、命名管道

4.1、原理

两个进程毫无关系怎么建立通信呢?

通过命名管道,一方写另一方读

那么怎么保证两个不相关的进程,能够准确打开同一个文件呢?

答:每一个文件都有一个唯一路径(具有唯一性)

mkfifo命令

用于创建一个命名管道 mkfifo myfifo得到一个p管道文件

建立一次进程通信:

echo "hello named pipe" >myfifo

cat myfifo

循环通信:

while :;do sleep 1;echo "hello named pipe" >>myfifo; done

cat < myfifo

4.2、创建命名管道函数的使用

创建管道文件

#include <sys/types.h>

#include <sys/stat.h>

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

删除指定的管道文件

#include <unistd.h>

int unlink(const char* pathname);

测试代码:
client.cc

cpp 复制代码
#include "namedPipe.hpp"

// write
int main()
{
    NamePiped fifo(comm_path, User);
    if (fifo.OpenForWrite())
    {
        std::cout << "client open namd pipe done" << std::endl;
        while (true)
        {
            std::cout << "Please Enter> ";
            std::string message;
            std::getline(std::cin, message);
            fifo.WriteNamedPipe(message);
        }
    }

    return 0;
}

server.cc

cpp 复制代码
#include "namedPipe.hpp"

// server read: 管理命名管道的整个生命周期
int main()
{
    NamePiped fifo(comm_path, Creater);
    // 对于读端而言,如果我们打开文件,但是写还没来,我会阻塞在open调用中,直到对方打开
    // 进程同步
    if (fifo.OpenForRead())
    {
        std::cout << "server open named pipe done" << std::endl;

        sleep(3);
        while (true)
        {
            std::string message;
            int n = fifo.ReadNamedPipe(&message);
            if (n > 0)
            {
                std::cout << "Client Say> " << message << std::endl;
            }
            else if (n == 0)
            {
                std::cout << "Client quit, Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout << "fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }

    return 0;
}

namedPipe.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string comm_path = "./myfifo";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096

class NamePiped
{
private:
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

public:
    NamePiped(const std::string& path, int who)
        : _fifo_path(path), _id(who), _fd(DefaultFd)
    {
        if (_id == Creater)
        {
            int res = mkfifo(_fifo_path.c_str(), 0666);//创建命名管道,并配置权限
            if (res != 0)
            {
                perror("mkfifo");
            }
            std::cout << "creater create named pipe" << std::endl;
        }
    }
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    bool OpenForWrite()
    {
        return OpenNamedPipe(Write);
    }
    // const &: const std::string &XXX
    // *      : std::string * //输出型
    // &      : std::string & //输入输出型
    int ReadNamedPipe(std::string* out)
    {
        char buffer[BaseSize];
        int n = read(_fd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }
    int WriteNamedPipe(const std::string& in)
    {
        return write(_fd, in.c_str(), in.size());
    }
    ~NamePiped()
    {
        if (_id == Creater)
        {
            int res = unlink(_fifo_path.c_str());
            if (res != 0)
            {
                perror("unlink");
            }
            std::cout << "creater free named pipe" << std::endl;
        }
        if (_fd != DefaultFd) close(_fd);
    }

private:
    const std::string _fifo_path;
    int _id;
    int _fd;
};

5、system V的共享内存

共享内存是最快的IPC形式、一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不在涉及到内核,也就是说进程不再通过执行进入内核的系统调用来传递彼此的数据。

5.1、原理

1.所有说到的操作都是由OS完成的

2.OS提供上面1,2步骤的系统调用,供用户进程A,B来进行调用 -- 系统调用

3.AB,CD,EF...共享内存在系统中可以同时存在多份,每份可不同个数,不同对的进程同时进行通信。

4.OS注定了要对共享内存进行管理,--》先描述再组织 --》共享内存,不是简单的一段内存空间,也要有描述并管理共享内存的数据结构匹配的算法。

5.共享内存 = 内存空间(放数据) + 共享内存的属性

5.2、代码理解

测试代码:

shm目录 -- 共享内存
Shm.hpp

cpp 复制代码
#ifndef __SHM_HPP__
#define __SHM_HPP__

#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

const int gCreater = 1;
const int gUser = 2;
const std::string gpathname = "/home/whb/code/111/code/lesson22/4.shm";
const int gproj_id = 0x66;
const int gShmSize = 4097; // 4096*n

class Shm
{
private:
    key_t GetCommKey()
    {
        key_t k = ftok(_pathname.c_str(), _proj_id);
        if (k < 0)
        {
            perror("ftok");
        }
        return k;
    }
    int GetShmHelper(key_t key, int size, int flag)
    {
        int shmid = shmget(key, size, flag);
        if (shmid < 0)
        {
            perror("shmget");
        }

        return shmid;
    }
    std::string RoleToString(int who)
    {
        if (who == gCreater)
            return "Creater";
        else if (who == gUser)
            return "gUser";
        else
            return "None";
    }
    void* AttachShm()//挂接
    {
        if (_addrshm != nullptr)
            DetachShm(_addrshm);
        void* shmaddr = shmat(_shmid, nullptr, 0);
        if (shmaddr == nullptr)
        {
            perror("shmat");
        }
        std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;
        return shmaddr;
    }
    void DetachShm(void* shmaddr)
    {
        if (shmaddr == nullptr)
            return;
        shmdt(shmaddr);
        std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;
    }

public:
    Shm(const std::string& pathname, int proj_id, int who)
        : _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr)
    {
        _key = GetCommKey();
        if (_who == gCreater)
            GetShmUseCreate();
        else if (_who == gUser)
            GetShmForUse();
        _addrshm = AttachShm();

        std::cout << "shmid: " << _shmid << std::endl;
        std::cout << "_key: " << ToHex(_key) << std::endl;
    }
    ~Shm()
    {
        if (_who == gCreater)
        {
            int res = shmctl(_shmid, IPC_RMID, nullptr);
        }
        std::cout << "shm remove done..." << std::endl;
    }

    std::string ToHex(key_t key)
    {
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "0x%x", key);
        return buffer;
    }
    bool GetShmUseCreate()
    {
        if (_who == gCreater)
        {
            _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
            if (_shmid >= 0)
                return true;
            std::cout << "shm create done..." << std::endl;
        }
        return false;
    }
    bool GetShmForUse()
    {
        if (_who == gUser)
        {
            _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);
            if (_shmid >= 0)
                return true;
            std::cout << "shm get done..." << std::endl;
        }
        return false;
    }
    void Zero()
    {
        if (_addrshm)
        {
            memset(_addrshm, 0, gShmSize);
        }
    }

    void* Addr()
    {
        return _addrshm;
    }

    void DebugShm()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if (n < 0) return;
        std::cout << "ds.shm_perm.__key : " << ToHex(ds.shm_perm.__key) << std::endl;
        std::cout << "ds.shm_nattch: " << ds.shm_nattch << std::endl;
    }

private:
    key_t _key;
    int _shmid;

    std::string _pathname;
    int _proj_id;

    int _who;
    void* _addrshm;
};

#endif

shmnamedPipe.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string comm_path = "./myfifo";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096

class NamePiped
{
private:
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

public:
    NamePiped(const std::string& path, int who)
        : _fifo_path(path), _id(who), _fd(DefaultFd)
    {
        if (_id == Creater)
        {
            int res = mkfifo(_fifo_path.c_str(), 0666);
            if (res != 0)
            {
                perror("mkfifo");
            }
            std::cout << "creater create named pipe" << std::endl;
        }
    }
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    bool OpenForWrite()
    {
        return OpenNamedPipe(Write);
    }
    // const &: const std::string &XXX
    // *      : std::string *
    // &      : std::string & 
    int ReadNamedPipe(std::string* out)
    {
        char buffer[BaseSize];
        int n = read(_fd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }
    int WriteNamedPipe(const std::string& in)
    {
        return write(_fd, in.c_str(), in.size());
    }
    ~NamePiped()
    {
        if (_id == Creater)
        {
            int res = unlink(_fifo_path.c_str());
            if (res != 0)
            {
                perror("unlink");
            }
            std::cout << "creater free named pipe" << std::endl;
        }
        if (_fd != DefaultFd) close(_fd);
    }

private:
    const std::string _fifo_path;
    int _id;
    int _fd;
};

shmclient.cc

cpp 复制代码
#include "Shm.hpp"
#include "shmnamedPipe.hpp"

int main()
{
    // 1. 创建共享内存

    Shm shm(gpathname, gproj_id, gUser);
    shm.Zero();
    char* shmaddr = (char*)shm.Addr();
    sleep(3);

    // 2. 打开管道
    NamePiped fifo(comm_path, User);
    fifo.OpenForWrite();

    // 当成string
    char ch = 'A';
    while (ch <= 'Z')
    {
        shmaddr[ch - 'A'] = ch;

        std::string temp = "wakeup";
        std::cout << "add " << ch << " into Shm, " << "wakeup reader" << std::endl;
        fifo.WriteNamedPipe(temp);
        sleep(2);
        ch++;
    }
    return 0;
}

shmserver.cc

cpp 复制代码
include "Shm.hpp"
#include "shmnamedPipe.hpp"

int main()
{
    // 1. 创建共享内存
    Shm shm(gpathname, gproj_id, gCreater);
    char* shmaddr = (char*)shm.Addr();

    shm.DebugShm();

    // // 2. 创建管道
    // NamePiped fifo(comm_path, Creater);
    // fifo.OpenForRead();

    // while(true)
    // {
    //     // std::string temp;
    //     // fifo.ReadNamedPipe(&temp);

    //     std::cout << "shm memory content: " << shmaddr << std::endl;
    // }

    sleep(5);

    return 0;
}

5.3、共享内存的理解

申请一个systeam V版本的动态内存

int shmget(key_t key,size_t size,int shmflg);
参数:

1.size_t size --- 创建的共享内存大小

2.int shmflg --- 标志位(常用IPC_CREAT 和 IPC_EXEL) -- 可以位图的形式传参

IPC_CREAT:如果你要创建的共享内存不存在,就创建,如果存在,获取该共享内存并返回。(总能获取到)

IPC_EXEL:单独使用没有意义,只有和IPC_CREAT组合使用才有意义。

IPC_CREAT | IPC_EXEL:如果你要创建的共享内存不存在,就创建,否则出错返回。(获取的是全新的shm)

3.key_t key --- 由用户自定义的key值,设置为唯一标识符,只要具备唯一性即可

a、key_t key是什么?是由用户自定义的key值,用于设置为唯一标识符

b、为什么?因为让不同的进程看到同一个共享内存

c、怎么办?利用ftok随机设置key值,便于用户使用
返回值:

返回唯一的标识符

#include <sys/typse.h>

#include <sys/ipc.h>

key_t ftok(const char* pathname, int proj_id);

我们怎么确定,OS内的共享内存是否存在了呢?

答:struct Shm中会有一个标识共享内存的唯一性标识符

能让OS自动生成,标识符呢?

答:不能, 共享内存,不随着进程的结束而自动释放,需要手动释放。(或系统调用释放)

共享内存的生命周期: 共享内存生命周期随内核,文件生命周期随进程

查看共享内存的信息:

ipcs -m

删除/释放指定的共享内存:

ipcrm -m shmid(返回给用户的标识符)

补充:IPC的知识

key VS shmid

key:属于用户形成的,属于内核使用的一个特定字段,具有唯一性,用户不能使用key来对shm进行管理,内核进行区分shm的唯一性(struct file*)

shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的id值(fd).

5.4、共享内存的相关接口

a、shmctl

#include <sys/ipc.h>

#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmod_ds *buf);

功能:

共享内存的控制,增删改查...

参数:

int shmid:内核给用户返回的id值

int cmd:对共享内存要执行的操作(常用IPC_RMID,删除/释放当前共享内存)

struct shmod_ds *buf:共享内存结构体的属性成员

b、shmat

#include <sys/typse.h>

#include <sys/shm.h>

void* shmat(int shmid, const void* shmaddr, int shmflg);

功能:将对应的地址空间挂接到共享内存中。

返回值:

地址空间中,共享内存的起始地址

c、shmdt

int shmdt(const void* shmaddr);

功能:取消挂接关联

6、消息队列

6.1、基本概念

一个进程,向另一个进程发送有类型的数据块的方式。结合之前的理解,msg_queue自带属性信息。

消息队列的生命周期也是随内核的,不随进程。

6.2、涉及的常用接口

消息队列常用接口

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgget (key_t key, int msgflg);

key_t ftok (const char* pathname,int proj_id);

int msgget (key_t key, int msgflg);

int msgctl (int msgid,int cmd,struct msgid_ds* buf);

int msgsnd (int msgid, const void* msgp, size_t msgsz, int msgflg);

int mshrcv(int msgid, void* msgp,size_t msgsz, long msgtyp,int msgflg);

7、信号量

7.1、5个概念

1.多个执行流(进程)都能看到的一份资源,称为共享资源

2.被保护起来的资源 -- 称为临界资源 -- 同步和互斥

3.互斥:任何时刻只能有一个进程访问共享资源。

4.资源 --- 要被程序员访问 -- 资源被访问,简单理解就是就是通过代码访问,代码 = 访问共享资源的代码(临界区) + 不访问共享资源的代码(非临界区)

5.所谓的对共享资源进行保护 -- 临界资源 -- 本质是对访问共享资源的代码进行代码。

7.2、对于信号量的理论理解

临界区 <=加锁/解锁=> 非临界区

1.用于保护临界资源,本质是一个计数器。

信号量的计数数量,标志对共享资源的预定机制。 担心超出资源量的个数,管理属性资源总数,限制资源不被多余预定。

类比:电影院系统

电影院:共享资源(临界资源)

买票:申请信号量

票数:信号量的初始值

申请信号量的本质:就是对公共资源的一种预定机制

申请信号量

访问共享资源

释放信号量

对共享资源整体的使用,其实不就是资源只有一个么?

1/0,二元信号量,互斥

既然信号量是一个计数器,可以使用一个全局的变量(如:gcongt)来充当对共享资源的保护吗?

答:不能,

1.因为全局变量不能被所有进程能够看到。

2.并且gcount++,不是原子的。

7.3、原子操作

所以IPC信号量:

1.与共享内存一样,使不同的进程之间都能看到同一个信号量(计数器),控制不同进程的同步或互斥

2.意味着信号量本身也属于共享资源

3.既然本身也属于临界资源,却要保护别的临界资源安全,前提是不是需要自己肯定是安全的呢?---提出原子操作

4.允许用户一次性申请多个信号量集 -- 用数组来维护的。

步骤:

1.申请信号量

2.访问公共资源(共享内存)

3.释放信号量

对信号量(计数器)的操作,就被设置为原子操作:

P和V操作 -- 安全的 -- 原子性

-- ---》本身是安全的 P

++ ---》本身是安全的 V

7.4、常用信号量的指令

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semget (key_t key,int nsems, int semflg);

int semctl (int semid,int semnum,int cmd,...);

int semop (int semid,struct sembuf* sops,size_t nsops);
查看信号量指令:
incs -s
删除指定信号量:
ipcrn -s semid

相关推荐
IT利刃出鞘37 分钟前
SecureCRT--使用sftp上传和下载文件
linux·运维·服务器
踩着阴暗的自己向上爬4 小时前
Day05-04-持续集成总结
linux·运维·ci/cd
qyhua5 小时前
Linux内网端口转公网端口映射
linux·运维·服务器
j.king6 小时前
开源GTKSystem.Windows.Forms框架让C# winform支持跨平台运行
linux·c#·gtk
stackY、7 小时前
【Linux】:程序地址空间
linux·算法
R语言爱好者8 小时前
如何查看程序是否在运行-Linux
linux
逆风就重开8 小时前
数据埋点从入门到了解
大数据·linux·前端·数据库·数据仓库
掘根8 小时前
【Linux】压缩命令——gzip,bzip2,xz
大数据·linux·运维
可爱的小小小狼9 小时前
linux的服务管理
linux·运维·服务器
TMS320VC5257H9 小时前
ffmpeg在powershell和ubuntu终端下的不同格式
linux·ubuntu·ffmpeg