通信方式:命名管道
一、命名管道
1. 命名管道的原理
有了匿名管道,理解命名管道就非常简单了。
对于普通文件而言,两个进程打开同一个文件,OS是不会将文件加载两次的,这两个进程都会指向同一个文件,那么,也就享有同一份 inode 和 文件缓冲区
。这在一定层面上也是实现了进程之间共享数据。但是,普通文件是需要向磁盘上进行刷新的。
所以,才有了一种特殊的文件:管道,管道(包含匿名管道和命名管道)是不需要向磁盘上进行刷新的,命名管道是文件系统中的一种特殊类型文件。管道就是解决进程之间共享数据的问题的。OS会为每个进程分配一个 struct file 结构体的,但是它们指向同一个 struct pipe_inode_info ,这个结构体里包含环形缓冲区,这就保证了不同的进程之间可以访问同一份数据
。
2. 命名管道的操作
//命令
mkfifo //创建命名管道
可以看到,fifo 文件的大小是 0,这也说明了数据是不会刷新到磁盘上的。
//系统调用
//mode代表权限
//成功返回0,失败返回-1
int mkfifo(const char* pathname, mode_t mode);//创建命名管道
//成功返回0,失败返回-1
int unlink(const char* pathname); //删除命名管道
3. 命名管道的实现
.
client.cpp
#include "NamePipe.hpp"int main()
{NamePipe name_pipe(fifoname);name_pipe.OpenForWrite();std::string line;while (true){std::cout << "Please Enter# ";std::getline(std::cin, line);name_pipe.Write(line);}name_pipe.Close();return 0;
}
.
common.hpp
#ifndef __COMMON_HPP__
#define __COMMON_HPP__#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>std::string fifoname = "fifo";
mode_t mode = 0666;#define SIZE 128#endif
.
NamePipe.hpp
#include "common.hpp"const int gdefaultfd = -1;
class NamePipe
{
public:NamePipe(std::string &name, int fd = gdefaultfd) : _name(name), _fd(fd){}bool Create(){// server 创建管道int n = mkfifo(fifoname.c_str(), mode);if (n < 0){perror("mkfifo fail");return false;}return true;}bool OpenForRead(){// 命名管道的操作特点:打开一端,另一端没打开的时候,open会阻塞_fd = open(fifoname.c_str(), O_RDONLY);if (_fd < 0){perror("server open fail");return false;}return true;}bool OpenForWrite(){_fd = open(fifoname.c_str(), O_WRONLY);if (_fd < 0){perror("client open fail");return false;}return true;}bool Read(std::string *out){char buffer[SIZE] = {0};ssize_t num = read(_fd, buffer, sizeof(buffer) - 1);if (num > 0){buffer[num] = 0;// std::cout 也有缓冲区,需要刷新*out = buffer;}else if (num == 0){// client quit,read读到文件末尾,返回0return false;}else{return false;}return true;}void Write(const std::string &in){write(_fd, in.c_str(), in.size());}void Close(){close(_fd);}void Remove(){unlink(fifoname.c_str()); // 删除管道文件}~NamePipe(){}private:std::string _name;int _fd;
};
.
server.hpp
#include "NamePipe.hpp"int main()
{NamePipe name_pipe(fifoname);name_pipe.Create();name_pipe.OpenForRead();std::cout << "open file success" << std::endl;std::string message;while (true){bool res = name_pipe.Read(&message);if(!res)break;std::cout << "Client Say@ " << message << std::endl;}name_pipe.Close();name_pipe.Remove();return 0;
}
.
Makefile
.PHONY:all
all:client serverclient:client.cppg++ -o $@ $^ -g -std=c++11
server:server.cppg++ -o $@ $^ -g -std=c++11.PHONY:clean
clean:rm -f clientrm -f server
命名管道主要解决毫无关系的进程之间,进行文件级进程通信。
匿名管道和命名管道剩下的特点是一样的。
server 和 client 之所以能够看到同一份资源,是因为它们打开的是同一份文件,该文件路径相同,inode相同,所以能够共享数据
。
看到这里,大家应该明白了,匿名管道和命名管道是非常相似的,匿名管道是内存级文件,是内存模拟出来的,而命名管道是磁盘上的一种特殊文件类型,是有名字的。正是因为有名字,所以才叫做命名管道
。
二、System V共享内存
1. System V的原理
还记得动态库的加载吗?进程会把自己依赖的动态库加载到物理内存里,通过页表建立虚拟地址和物理地址的映射关系,而动态库就被映射到了虚拟地址空间中的共享区中
。
那么,一个进程需要的动态库会被映射到自己的共享区中,另一个进程也需要这个动态库呢?那么,这个进程也会把动态库映射到自己的共享区中,OS不会将这个动态库加载两次,两个进程用的是同一个动态库
。这从某种意义上来说,不就是OS给动态库开辟了一块物理内存,这块物理内存被映射到了进程虚拟地址空间中的共享区上吗。这不就是两个进程之间共享内存了嘛!
所以,我们想实现共享内存,就必须
1.创建共享内存
。
2.建立虚拟地址和物理地址的映射关系
。
3.删除共享内存
。
动态库的详细加载过程请看这篇文章:从ELF到进程间通信:剖析Linux程序的加载与交互机制。
假设进程A,进程B之间通过共享内存通信,如果进程C,D也需要通信呢,许多进程之间都需要共享内存通信呢?那么,是不是会有许多共享内存,OS要不要进行管理呢?肯定是要的。先描述再组织。
2. System V的操作
//key表示共享内存段的唯一键值(类似文件名)
//size是共享内存段的大小,一般建议是4KB的整数倍
//shmflg 创建方式和访问权限
//成功返回共享内存标识符,失败返回-1(类似于文件描述符fd)
//shmflg的选项,介绍两个
//IPC_CREAT,单独使用,共享内存不存在,则创建,已存在,直接获取
//IPC_EXCL,不能单独使用
//一起使用,共享内存不存在,则创建,已存在,返回出错
int shmget(key_t key, size_t size, int shmflg)//创建或获取一个共享内存
//这两个参数是可以随便写的
//成功返回 key,失败返回-1
key_t ftok(const char* pathname, int proj_id)//形成唯一的键值,传入shmget函数中
注:共享内存的生命周期不随进程,随内核
。
所以,共享内存需要我们自己手动释放资源
。可以使用以下命令查看共享内存的信息以及删除共享内存。
ipcs -m //查看所有共享内存段的信息
ipcrm -m shmid //删除指定的共享内存段
可以看到,第一次共享内存是创建出来的,第二次就创建失败,这也证明了共享内存的生命周期不随进程。当删除了指定的共享内存,就可以再次创建共享内存了。
但这种删除共享内存的方式还是太麻烦了,我们希望进程结束时就删除掉共享内存。
文件 = 文件属性 + 文件内容
。
共享内存 = 内存块 + 共享内存的 struct(shm的属性)
。
//shmid就是共享内存标识符
//cmd可以使用IPC_RMID,表示立即删除
//buf设置为NULL
//0表示成功, -1表示失败
int shmctl(int shmid, int cmd, struct shmid_ds* buf); //对共享内存的属性做操作
//shmaddr 设置为 nullptr,让内核自动选择映射的虚拟地址
//shmflg 设置为 0,缺省,继承共享内存的权限
//成功了返回共享内存映射到当前进程虚拟地址空间的具体地址,失败返回-1
void* shmat(int shmid, const void* shmaddr, int shmflg); //将共享内存与虚拟地址进行映射
这是为什么呢?要想挂接成功,就必须对共享内存设置权限才可以
。
可以看到,映射成功了,并且共享内存有了权限,nattch 挂接的进程数量也变为了 1。
在删除共享内存之前,虚拟地址空间依然和共享内存有着映射关系,但这并不是我们想要的,所以,在删除共享内存之前,需要先将虚拟地址空间和共享内存去关联,然后再删除共享内存
。
//成功返回0,失败返回-1
int shmdt(const void* shmaddr); //将共享内存与虚拟地址空间进行去关联
我们说过,OS内会有许多共享内存,所以对于共享内存是需要进行管理的,在用户层面上提供了一种结构体来描述共享内存。我们来看一下。
我们可以获取里面的属性看一看。不过在此之前,需要了解 shmctl
函数的一个选项。
共享内存的特征总结:
1.生命周期随内核
。
2.共享内存是 IPC 中速度最快的(因为共享内存写入数据和读取数据不需要使用系统调用,只需要使用指针就可以完成,系统调用也是有成本的)
。
3.共享内存没有同步互斥机制,来对多个进程的访问进行协同
。
3. 共享内存的实现
.
client.cpp
#include "Shm.hpp"int main()
{SharedMemory shm;shm.Get();// sleep(5);shm.Attach();// sleep(5);// 通信shm.SetZero();char ch = 'A';int cnt = 10;while(cnt--){std::cout << "client 开始写入" << std::endl;shm.AddChar(ch);ch++;sleep(1);}shm.Detach();// sleep(5);return 0;
}
.
Shm.hpp
#ifndef __SHM_HPP__
#define __SHM_HPP__#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>#define gdefaultsize 4096std::string gpathname = ".";
int gproj_id = 0x66;class SharedMemory
{
private:bool CreateHelper(int flags){// 形成唯一的键值_key = ftok(gpathname.c_str(), gproj_id);if (_key < 0){perror("ftok fail");return false;}printf("形成键值成功:0x%x\n", _key);// 获取共享内存_shmid = shmget(_key, _size, flags); // 创建全新的共享内存if (_shmid < 0){perror("shmget fail");return false;}printf("shmid:%d\n", _shmid);return true;}public:SharedMemory(size_t size = gdefaultsize) : _key(0), _size(size), _shmid(-1),_start_addr(nullptr), _windex(0), _rindex(0),_num(nullptr), _data_start(nullptr){}bool Create(){return CreateHelper(IPC_CREAT | IPC_EXCL | 0666);}bool Get(){return CreateHelper(IPC_CREAT);}bool RemoveShm(){// 删除共享内存int n = shmctl(_shmid, IPC_RMID, NULL);if (n < 0){perror("shmctl fail");return false;}std::cout << "删除shm成功" << std::endl;return true;}bool Attach(){// 将共享内存映射到虚拟地址空间中//共享内存需要权限才能挂接在指定的进程_start_addr = shmat(_shmid, nullptr, 0);if ((long long)_start_addr == -1){perror("shmat fail");return false;}_num = (int *)_start_addr;_data_start = (char *)_start_addr + sizeof(int);std::cout << "共享内存映射到进程的虚拟地址空间中" << std::endl;return true;}bool Detach(){// 共享内存与虚拟地址空间进行去关联int n = shmdt(_start_addr);if (n < 0){perror("Detach fail");return false;}std::cout << "虚拟地址空间与共享内存进行去关联" << std::endl;return true;}void SetZero(){*_num = 0;}void AddChar(char ch){if (*_num == _size - sizeof(int))return;((char *)_data_start)[_windex++] = ch;_data_start[_windex] = 0;_windex %= (_size - sizeof(int));(*_num)++;// std::cout << "Debug: " << _data_start[_windex - 1] << " _num = " << *_num << std::endl;}void PopChar(char *out){// std::cout << " _num = " << *_num;if (*_num == 0)return;*out = _data_start[_rindex++];_rindex %= (_size - sizeof(int));(*_num)--;// std::cout << "client read: " << _data_start[_rindex - 1] << std::endl;}void PrintAttr(){struct shmid_ds ds;int n = shmctl(_shmid, IPC_STAT, &ds);if(n < 0){perror("PrintAttr shmctl fail");return;}printf("key:0x%x\n", ds.shm_perm.__key);printf("shm_atime:%ld\n",ds.shm_atime);printf("shm_nattch:%ld\n",ds.shm_nattch);}int GetCount(){return *_num;}~SharedMemory(){}private:key_t _key;size_t _size;int _shmid;void *_start_addr;int *_num;char *_data_start;int _windex;int _rindex;
};#endif
.
server.cpp
#include "Shm.hpp"int main()
{SharedMemory shm;shm.Create();// sleep(5);shm.Attach();shm.PrintAttr();sleep(2);// 通信char ch;while (true){if(!shm.GetCount()) break;shm.PopChar(&ch);printf("server getchar:%c\n", ch);sleep(1);}shm.Detach();// sleep(5);shm.RemoveShm();// sleep(5);return 0;
}
.
Makefile
.PHONY:all
all:client serverclient:client.cppg++ -o $@ $^ -g -std=c++11
server:server.cppg++ -o $@ $^ -g -std=c++11.PHONY:clean
clean:rm -f clientrm -f server
今天的文章分享到此结束,觉得不错的给个一键三连吧。