【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 pipefd2);

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

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

答:就创建两个管道。

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

答: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 pipefd2); --- 参数int pipefd2,属于输出型参数,表示管道的输入或输出的端口

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

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

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言