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;
}
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;
};
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;
}
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