Linux -- 进程间通信【System V共享内存】
目录
一、System V通信
二、System V共享内存
1、共享内存的介绍
2、共享内存的原理
3、共享内存的函数
3.1 shmget
3.2 shmat
3.3 shmdt
3.4 shmctl
4、shmid和key的区别
5、共享内存与管道的对比
一、System V通信
# 前面我们所探究的通信方式都是基于管道文件的,而接下来我们将谈论的是在System V标准下,进程间通信的方式。
# System V标准是在同一主机内的进程间通信方案,是站在OS层面专门为进程间通信设计的方案,其主要提供三个主流方案:system V共享内存,system V消息队列,system V信号量。
# 其中System V 共享内存和 System V 消息队列的目的在于传送数据。而 System V 信号量则是为确保进程间的同步与互斥而设计,虽然 System V 信号量看似与通信没有直接关联,但实际上它也属于通信范畴。
二、System V共享内存
1、共享内存的介绍
# 共享内存本质就是让不同进程间能够访问同一块物理空间,从而实现进程间通信。
2、共享内存的原理
# 我们之前动态库可以加载一份物理内存空间,然后通过页表映射,就可以映射到不同的进程的进程虚拟空间,所以实现不同进程的代码共享,而共享内存的原理也是类似的。
# 共享内存的实现方式是在物理内存中申请一块内存空间。接着,将这块内存空间与各个进程各自的页表分别建立映射,并在虚拟地址空间的共享区开辟空间,把虚拟地址填充到页表对应位置,从而建立虚拟地址与物理地址的对应关系。此时,不同进程便能访问同一份物理内存,此物理内存即共享内存。
# 所以我们通过一个内核结构体来描述共享内存,再由操作系统统一管理这些内核结构体,共享内存数据结构:
struct shmid_ds {struct ipc_perm shm_perm; /* operation perms */int shm_segsz; /* size of segment (bytes) */__kernel_time_t shm_atime; /* last attach time */__kernel_time_t shm_dtime; /* last detach time */__kernel_time_t shm_ctime; /* last change time */__kernel_ipc_pid_t shm_cpid; /* pid of creator */__kernel_ipc_pid_t shm_lpid; /* pid of last operator */unsigned short shm_nattch; /* no. of current attaches */unsigned short shm_unused; /* compatibility */void shm_unused2; /* ditto - used by DIPC */void shm_unused3; /* unused */
};
3、共享内存的函数
# 一般来说我们创建共享内存大致可以分为两步:第一步就是在物理内存上申请共享内存。第二步将申请到的物理内存挂接到对应的地址空间上。这两步都分别对应两个函数:shmget
与shmat
。如果想释放共享内存,步骤就刚好相反,首先第一步将共享内存与地址空间去关联,即取消映射关系,第二步将释放共享内存空间,即将物理内存归还给系统。这两步都分别对应两个函数:shmdt
与shmctl
。
3.1 shmget
#
shmget是System V IPC中创建或获取共享内存段的核心函数,其本质是向内核申请一块多进程可共同访问的物理内存区域。
- 头文件:#include <sys/ipc.h> #include <sys/shm.h>
- 函数原型:int shmget(key_t key, size_t size, int shmflg);
- 参数:第一个参数key,表示待创建共享内存在系统当中的唯一标识。第二个参数size,表示待创建共享内存的大小。第三个参数shmflg,表示创建共享内存的方式。
- 返回值:shmget调用成功,返回一个有效的共享内存标识符(用户层标识符),否则返回-1。
# 首先shmget需要传入的第一个参数key
需要通过函数ftok
获取,其原型如下:
- 头文件:#include <sys/types.h> #include <sys/ipc.h>
- 函数原型:key_t ftok(const char *pathname, int proj_id);
# 其中pathname
代表一个已存在的路径名,proj_id
代表一个项目ID
,ftok
函数可以将通过特定的算法将这两个参数转换出对应的系统标识符key
,否则返回-1。
# 值得注意的是:
- 使用
ftok
函数生成key
值可能会产生冲突,此时需要对传入ftok
函数的参数进行修改。- 如果不同进程间需要通信,需要采用同样的路径名和和项目
ID
,进而生成同一个key
值,才能找到同一个共享资源。
# 第二个参数size
一般建议是4096的整数倍,假设如果传的是4097,操作系统实际上申请的空间大小是 4096*2,虽然操作系统多申请了,但是多余的部分用户不能使用,这样就可能造成空间的浪费。
# 第三个参数shmflag
标记位常用选项有两种:
IPC_CREAT
:如果申请的共享内存不存在,就创建,存在,就获取并返回。IPC_EXCL
:如果申请的共享内存存在,就出错返回。
# IPC_CREAT | IPC_EXCL
:如果申请的共享内存不存在,就创建,存在就出错返回。这俩选项一起使用保证了,如果我们申请成功了一个共享内存,这个共享内存一定是一个新的。
#pragma once#include <iostream>
#include <stdio.h>
#include <string>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)class Shm
{
public:Shm():_shmid(gdefaultid),_size(gsize){}// 创建的一定要是一个全新的共享内存void Creat(){key_t k = ftok(pathname.c_str(), projid);if(k < 0){ERR_EXIT("ftok");exit(EXIT_FAILURE);}printf("key: 0x%x", k);_shmid = shmget(k, _size, IPC_CREAT | IPC_EXCL);if(_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d", _shmid);}~Shm(){}private:int _shmid;int _size;
};
# 并且我们也可能通过指令ipcs - m
查看相关信息。
# 其分别每一项的含义:
标题 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层 id |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
# 因为共享内存的生命周期是随内核的,所以如果不手动回收,这个共享内存就会一直存在。除了通过特定的函数外,我们也能够通过指令ipcrm -m shmid
释放指定的共享内存资源。
# 注意不能通过key删除共享内存,因为key只是给内核来区分的唯一性的。而我们的指令本质是运行在用户空间的,而用户层只能使用shmid访问共享内存,而代码级别删除共享内存的系统调用是shmctl。
3.2 shmat
# shmat函数可以将共享内存映射到进程的虚拟地址空间,使进程可访问共享数据。
- 头文件:#include <sys/types.h> #include <sys/shm.h>
- 函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
- 参数:第一个参数shmid,表示待关联共享内存的用户级标识符。第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。第三个参数shmflg,表示关联共享内存时设置的某些属性。
- 返回值:shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。否则,返回(void*)-1。
# 一般shmat函数的第三个参数传入的常用的选项有以下三种:
SHM_RDONLY
:关联共享内存后只进行只读操作。SHM_RND
:若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA
(通常为页大小)的整数倍。0
:默认为读写权限。
// Comm.hpp#pragma once#include <cstdio>
#include <cstdlib>#define ERR_EXIT(m) \
do \
{ \perror(m); \exit(EXIT_FAILURE); \
} while (0)
// Shm.hpp#pragma once#include <iostream>
#include <stdio.h>
#include <string>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>#include "Comm.hpp"const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;#define CREATER "creater"
#define USER "user"class Shm
{
private:// 创建的一定要是一个全新的共享内存void CreateHelper(int flg){printf("key: 0x%x\n", _key);// _shmid = shmget(k, _size, IPC_CREAT | IPC_EXCL | gmode);_shmid = shmget(_key, _size, flg);if (_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d\n", _shmid);}void Create(){CreateHelper(IPC_CREAT | IPC_EXCL | gmode);}void Attach(){_start_mem = shmat(_shmid, nullptr, 0);if ((long long)_start_mem < 0){ERR_EXIT("shmat");}printf("attach success\n");}void Get(){CreateHelper(IPC_CREAT);}// 共享内存通常需要在所有进程使用完毕后才能删除,而非某个进程的对象销毁时。如果在析构函数中删除,可能导致 “提前删除”(例如一个进程只是临时使用完对象,但其他进程仍在访问)。void Destroy(){// if (_shmid == gdefaultid)// return;int n = shmctl(_shmid, IPC_RMID, nullptr);if (n > 0){printf("shmctl delete shm: %d success!\n", _shmid);}else{ERR_EXIT("shmctl");}}public:Shm(const std::string &pathname, int projid, const std::string &usertype):_shmid(gdefaultid),_size(gsize),_start_mem(nullptr),_usertype(usertype){_key = ftok(pathname.c_str(), projid);if (_key < 0){ERR_EXIT("ftok");}if(_usertype == CREATER)Create();else if(_usertype == USER)Get();else{}Attach();}int Size(){return _size;}void *VirtualAddr(){printf("VirtualAddr: %p\n", _start_mem);return _start_mem;}~Shm(){// if(_usertype == CREATER)Destroy();}private:int _shmid;key_t _key;int _size;void *_start_mem;std::string _usertype;
};
// Fifo.hpp#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#include "Comm.hpp"#define PATH "."
#define FILENAME "fifo"class NamedFifo
{
public:NamedFifo(const std::string &path, const std::string &name): _path(path), _name(name){_fifoname = _path + "/" + _name;// 将文件默认掩码设置为0umask(0);// 新建管道int n = mkfifo(_fifoname.c_str(), 0666);if (n < 0){// std::cerr << "mkfifo error" << std::endl;// perror("mkfifo");// exit(1);ERR_EXIT("mkfifo");}else{std::cout << "mkfifo success" << std::endl;}}~NamedFifo(){// 删除管道文件int n = unlink(_fifoname.c_str());if (n == 0){std::cout << "remove fifo success" << std::endl;}else{// std::cout << "remove fifo failed" << std::endl;ERR_EXIT("unlink");}}private:std::string _path;std::string _name;std::string _fifoname;
};class FileOper
{
public:FileOper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_fifoname = _path + "/" + _name;}void OpenForRead(){// 打开// write方没有执行open的时候,read方就要在open内部进行阻塞,直到有人把管道文件打开了,open才会返回_fd = open(_fifoname.c_str(), O_RDONLY); // 以读方式打开命名管道文件if (_fd < 0){// std::cerr << "open fifo error" << std::endl;// return;ERR_EXIT("open");}std::cout << "open fifo success" << std::endl;}void OpenForWrite(){// write_fd = open(_fifoname.c_str(), O_WRONLY); // 以写方式打开命名管道文件if (_fd < 0){// std::cerr << "Open fifo error" << std::endl;// return;ERR_EXIT("open");}std::cerr << "Open fifo success" << std::endl;}void Wakeup(){// 写入操作char c = 'c';int n = write(_fd, &c, 1);printf("唤醒: %d\n", n);}bool Wait(){char c;int number = read(_fd, &c, 1);if(number > 0){printf("醒来: %d\n", number);return true;}elsereturn false;}void Close(){close(_fd);}~FileOper(){}private:std::string _path;std::string _name;std::string _fifoname;int _fd;
};
// server.cc#include "Shm.hpp"
#include "Fifo.hpp"int main()
{Shm shm(pathname, projid, CREATER);// 创建管道文件NamedFifo fifo(PATH, FILENAME);// 文件操作FileOper readerfile(PATH, FILENAME);readerfile.OpenForRead();char *mem = (char*)shm.VirtualAddr();while(true){if(readerfile.Wait()) // 默认会阻塞{printf("%s\n", mem);}elsebreak;}readerfile.Close();return 0;
}
// client.cc#include "Shm.hpp"
#include "Fifo.hpp"int main()
{FileOper writerfile(PATH, FILENAME);writerfile.OpenForWrite();Shm shm(pathname, projid, USER);char *mem = (char*)shm.VirtualAddr();int index = 0;for (char c = 'A'; c <= 'Z'; c++, index += 2){// 才是向共享内存里写入sleep(1);mem[index] = c;mem[index + 1] = c;sleep(1);mem[index + 2] = 0;writerfile.Wakeup();}writerfile.Close();return 0;
}
3.3 shmdt
#
将共享内存段从当前进程的地址空间分离(解除映射),但不会删除共享内存。
- 头文件:#include <sys/types.h> #include <sys/shm.h>
- 函数原型:int shmdt(const void *shmaddr);
- 参数:
shmaddr
为待去关联共享内存的起始地址,即调用shmat
函数时得到的返回值。- 返回值:
shmdt
调用成功,返回0。否则返回-1。
3.4 shmctl
#
管理共享内存段,包括删除、状态查询或权限修改。
- 头文件:#include <sys/ipc.h> #include <sys/shm.h>
- 函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 参数:第一个参数shmid,表示所控制共享内存的用户级标识符。第二个参数cmd,表示具体的控制动作。第三个参数buf,用于获取或设置所控制共享内存的数据结构。
- 返回值:shmctl调用成功,返回0。否则返回-1。
# 其中第二个选项cmd
常见有三个选项:
IPC_STAT
:获取共享内存的当前关联值,此时参数buf
作为输出型参数。IPC_SET
:在进程有足够权限的前提下,将共享内存的当前关联值设置为buf
所指的数据结构中的值.IPC_RMID
:删除共享内存段,此时buf
可以传NULL
。
4、shmid和key的区别
5、共享内存与管道的对比
# 同样是实现客户端client
与服务端server
的交互,共享内存明显会比管道通信快的多。
# 从上图观察我们就可以看出,管道通信将一个文件内容从服务端发送到客户端一共需要四次拷贝:
- 服务端将信息从输入文件复制到服务端的临时缓冲区中。
- 将服务端临时缓冲区的信息复制到管道中。
- 客户端将信息从管道复制到客户端的缓冲区中。
- 将客户端临时缓冲区的信息复制到输出文件中。
# 而如果是共享内存却只需要两次拷贝即可,大大提升效率。
- 从输入文件到共享内存。
- 从共享内存到输出文件。
# 但是共享内存也有明显的缺陷,那就是没有同步与互斥这样的保护机制。