文章目录
- 命名管道
- 原理
- 命令创建命名管道
- 函数创建命名管道
- 共享内存
- 原理
- shmget
- FIOK
- 代码应用:
- prems
- nattch
命名管道
用于两个毫无关系的进程间的通信。
原理
Linux文件的路径是多叉树,故文件的路径是唯一的。
让内核缓冲区不用刷新到磁盘中,一旦刷新就拖慢了操作系统。所以磁盘中有个特殊文件,在内存中写入不会刷新到磁盘,让两个进程在内存中通信,该文件叫做命名管道。(命名:有路径就有名字;管道:用于内存通信)
命令创建命名管道
可以直接使用系统命令创建命名管道
p打头的文件就是管道文件
echo是进程,cat也是进程。两个进程基于管道实现了通信。写入的时候,管道的大小依旧为0。
函数创建命名管道
返回值:成功返回0,失败返回-1
删除指定路径的文件:unlink
返回值:成功返回0,失败返回-1
代码如下:
client.cc 负责写:
#include "namedPipe.hpp"
// client 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;
getline(std::cin, message);
fifo.WriteNamedpipe(message);
}
}
return 0;
}
namePipe.hpp:
#pragma once
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.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(comm_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 creat Named pipe" << std::endl;
}
}
bool OpenForRead()
{
return OpenNamedPipe(Read);
}
bool OpenForWrite()
{
return OpenNamedPipe(Write);
}
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)
{
sleep(5);
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; //身份(user/creater)
int _fd; //文件描述符
};
server.cc 读端:
#include "namedPipe.hpp"
// server read:管理命名管道的生命周期(创建与删除)
int main()
{
NamePiped fifo(comm_path, Creater);
if(fifo.OpenForRead())
{
std::cout << "server open named pipe done" << std::endl;
while (true)
{
std::string message;
int res = fifo.ReadNamedpipe(&message);
if (res > 0)//读内容
{
std::cout << "Client send >" << message << std::endl;
}
else if(res == 0)//写端关闭
{
std::cout << "Client quit" << std::endl;
break;
}
else//出错
{
std::cout << "fifo.ReadNamedpipe default" << std::endl;
break;
}
}
}
return 0;
}
运行:
光运行./server(读端),不运行写端会创建出管道,但读没有打印出server open named pipe done。说明没有打开管道。
然后运行了./client(写端)才会打开管道。说明对于读端而言,如果我们打开文件,但还没有写端,就会阻塞在open调用中,直到对方打开。简称进程同步。
相反如果读端关闭,写端还在写,写端就会收到SIGPIPE信号,让写端进程直接退出
共享内存
匿名管道和命名管道就是复用文件的代码。
有人从零开始写本地通信方案的代码:System V IPC 有三种方式通信
因为只能本地通信,且和文件的整合度不高,所以目前这种方案已经快被淘汰了。
原理
假设A进程在物理内存中创建一段内存空间,然后在A进程的地址空间中的共享区申请一片空间,再把虚拟地址与共享内存的映射关系填入页表。拥有映射关系后就可以往创建的内存中写入。B进程的虚拟地址也通过页表与创建的内存映射。这样进程A与进程B看到同一块资源,上面的技术叫做共享内存。
补充:
1.以上操作都是由操作系统做的,所以操作系统肯定提供给用户系统调用了。
2.AB进程通信,CD进程也要通信,所以共享进程在系统中可以存在很多份,且功能也不一样。操作系统就要对共享内存做管理!先描述,再组织。类似struct Shm的结构体。
综上所述:共享内存 = 共享内存空间(数据) + 共享内存属性
shmget
创建共享内存的函数调用:IPC_CREAT用于获取;IPC_CREAT | IPC_EXCL用于创建。
key比较特殊,是标识共享内存的唯一性字段,用于寻找共享空间。
综上:用户设置key值,AB两个进程就能看见同一块共享空间。
key值是给用户看的,后面会说shmid是给操作系统看的,所以操作系统不创建key值。
但是又不建议用户自己设置key,容易冲突,就给用户提供了函数ftok,由一些算法形成的随机数。
FIOK
同一个pathname,同一个proj_id,就能由算法形成同一个key。
返回值:成功后返回的是key的值;失败后返回-1
代码应用:
先验证共享内存的几个特性:
共享内存,不随着进程的结束释放。
Shm.hpp
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cerrno>
#define gShmSize 4096
const std::string gpathname = "/root/test/shm";
const int gproj_id = 0x66;
std::string ToHex(key_t k)
{
char buffer[128];
snprintf(buffer,sizeof(buffer),"0x%x",k);
return buffer;
}
key_t GetCommKey(const std::string pathname, int proj_id)
{
key_t res = ftok(pathname.c_str(), proj_id);
if (res < 0)
{
perror("create ftok failing");
}
return res;
}
int GetShm(int key)
{
int shmid = shmget(key,gShmSize,IPC_CREAT | IPC_EXCL);
if(shmid < 0)
{
perror("shmget fail");
}
return shmid;
}
#endif
server.cc
#include "Shm.hpp"
int main()
{
key_t k = GetCommKey(gpathname,gproj_id);
std::cout << "key: " << ToHex(k) << std::endl;
int shmid = GetShm(k);
std::cout << "shmid:" << shmid << std::endl;
return 0;
}
运行两次,发现第二次并没有创建,并显示已经存在,说明共享内存并没有像子进程一样运行完就被父进程或系统回收。
把创建共享内存改成获取共享内存:
int GetShm(int key)
{
int shmid = shmget(key,gShmSize,IPC_CREAT);
if(shmid < 0)
{
perror("shmget fail");
}
return shmid;
}
发现获取的话可以一直获取。
如果不释放共享内存,就会一直存在,生命周期随内核。
查共享内存:
ipcs -m
key:属于用户形成,内核使用的一个字段,用户不能使用key来进行shm管理。
shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的id值。
删共享内存用命令shmid删除:
ipcrm -m [shmid]
也可以用操作系统提供的函数:shmctl删除。cmd删除是IPC_RMID,就是位图。
返回值:移植成功,返回0;失败,返回-1
综上所述:创建共享内存,使用后删除的代码就能写出来了:
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cerrno>
#define gCreater 1
#define gUser 2
#define gShmSize 4096
const std::string gpathname = "/root/test/shm";
const int gproj_id = 0x66;
class Shm
{
private:
key_t GetCommKey()
{
key_t key = ftok(_pathname.c_str(), _proj_id);
if (key < 0)
{
perror("create ftok failing");
}
return key;
}
int GetShmHelper(key_t key, int size, int flag)
{
int shmid = shmget(key, size, flag);
if (shmid < 0)
{
perror("shmget fail");
}
return shmid;
}
public:
Shm(const std::string pathname, const int proj_id, int who)
: _pathname(pathname), _proj_id(proj_id), _who(who)
{
_key = GetCommKey();
if (_who == gCreater)
{
CreaterGetShmid();
}
else if (_who == gUser)
{
UserGetShmid();
}
std::cout << "shmid: " << _shmid << std::endl;
std::cout << "key: " << ToHex(_key) << std::endl;
}
~Shm()
{
if (_who == gCreater)
{
int res = shmctl(_shmid, IPC_RMID, nullptr);
if (res < 0)
std::cout << "shmctl fail " << std::endl;
else
std::cout << "shmid remove done " << std::endl;
}
}
std::string ToHex(key_t k)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", k);
return buffer;
}
bool CreaterGetShmid()
{
if (_who == gCreater)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL);
sleep(10); //为了创建完看见删除的效果
if (_shmid > 0)
return true;
}
std::cout << "Create shmid succeed" << std::endl;
return false;
}
bool UserGetShmid()
{
if (_who == gUser)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT);
if (_shmid > 0)
return true;
}
std::cout << "Get shmid succeed" << std::endl;
return false;
}
private:
key_t _key;
int _shmid;
int _who;
const std::string _pathname;
const int _proj_id;
};
#endif
创建出共享内存后10秒钟后删除
prems
perms是共享内存的权限:如果创建的时候,加上权限,perms的值会更改。
bool CreaterGetShmid()
{
if (_who == gCreater)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid > 0)
return true;
}
std::cout << "Create shmid succeed" << std::endl;
return false;
}
nattch
nattch是该共享内存挂接的数量。
挂接的函数:shmat(at有attach的意思)
返回值:一旦挂接成功,返回地址空间中共享内存的起始地址。(跟malloc返回值类似)
挂接代码:
std::string RoleToString(int who)
{
if (who == gCreater)
return "Creater";
else if (who == gUser)
return "gUser";
else
return "None";
}
// 挂接
void *AttachShm()
{
void *shmaddr = shmat(_shmid, nullptr, 0);
if (shmaddr == nullptr)
{
perror("shmat");
}
std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;
return shmaddr;
}
一个进程挂接上了为1,两个进程挂接上了为2。
去掉进程与共享内存的关联的函数:shmdt
代码如下:
void DetachShm(void *shmaddr)
{
if(shmaddr == nullptr) return;
shmdt(shmaddr);
std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;
}
效果:0-》1-》0
上面都是准备工作:下面开始通信。
只运行读端:
现象:发现不等写端写入,读端一直在读。
读端和写端都运行:
现象:写端2秒写一次,读端1秒读1次,数据就有重复。(相比较通道读完数据就没有了)
读端和写端都运行,然后把写端关闭后再次运行写端:
现象:读端重新读取
综上所述:与管道不一样,共享内存不提供保护机制。
缺点:会造成数据不一致问题(在纸上写字,写一半被别人拿走了)
优点:共享内存收所有进程IPC,速度最快的。因为共享内存大大减少了数据的拷贝次数。