system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
共享内存示意图
通过上面的图,我们不难想到,共享内存肯定有下面的操作:
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 */ };
共享内存生命周期
system V下的共享内存,生命周期是随内核的,如果不显式删除,只能通过kernel(OS)重启来解决。
共享内存函数
shmget函数
功能:用来创建共享内存 原型 int shmget(key_t key, size_t size, int shmflg); 参数 key:这个共享内存段名字 size:共享内存大小,最好是设置成页(4KB)的整数倍,如果我们申请的不是页的整数倍该怎么办呢(以4097为例)?实际操作系统会为我们申请两个page(8194字节),但是我们可以实际使用的共享内存大小只有4097个空间 shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的 shmflg: IPC_CREAT:创建共享内存,如果已经存在了,就获取,如果不存在就创建(设置为0默认行为就是IPC_CREAT) IPC_EXCL:不单独使用,必须和IPC_CREAT配合(按位或),如果不存在指定的共享内存,就创建,如果存在,就出错返回,作用:如果shmget函数调用成功,一定是一个全新的共享内存 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
注意:
当只有IPC_CREAT选项打开时,不管是否已经存在该块共享内存,则都返回该共享内存的ID,若不存在则创建共享内存
当只有IPC_EXCL选项打开时,不管有没有该共享内存,shmget()都返回-1
所以当IPC_CREAT | IPC_EXCL时, 如果没有该块共享内存,则创建,并返回共享内存ID。若已有该块共享内存,则返回-1;
问:标识共享内存是否存在的标识符存在哪?是怎么进行管理起来的?我们如何知道共享内存是否存在?
答:标识共享内存是否存在的标识符存在于内核中。内核会帮我们维护共享内存的结构,通过结构体来存储共享内存的各种属性(具体结构如上面的共享内存的数据结构所示)。下面是用户层面的数据结构(通过查找比对下面的key就可以判断共享内存是否存在):
注意:这个标识共享内存的唯一值是由用户提供的。为什么一定要由用户提供?进程间通信的前提:不同的进程能够看到同一份资源。共享内存在内核中,让不同的进程看到同一份共享内存,做法是:让他们能够看到同一份key。
补充知识:操作系统把内存分为很多4KB的页,比如4GB的内存,操作系统会将其划分为220个页,然后通过类似数组的方式对整个内存空间进行管理:
struct page//每个页 { //page的属性 } struct page mem[2^20];
ftok函数
参数说明:
id:由自己来进行设置,一般是0~255中的一个数。
path:一个文件路径,一般是当前文件的路径。
作用:转换一个文件路径和一个id标识符形成一个唯一的数字,我们用其来形成key。
shmctl函数
功能:用于控制共享内存 原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf); 参数 shmid:由shmget返回的共享内存标识码 cmd:将要采取的动作(有三个可取值) buf:指向一个保存着共享内存的模式状态和访问权限的数据结构 返回值:成功返回0;失败返回-1
命令 | 说明 |
---|---|
IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
shmat/shmdt函数
shmat函数
功能:将共享内存段连接到进程地址空间 原型 void *shmat(int shmid, const void *shmaddr, int shmflg); 参数 shmid: 共享内存标识 shmaddr:指定我们想要挂接的地址(我们一般将其设置为nullptr) shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY(一般默认设置为0,以读写方式) 返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1
说明:
shmaddr为NULL,核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。 shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离 原型 int shmdt(const void *shmaddr); 参数 shmaddr: 由shmat所返回的指针 返回值:成功返回0;失败返回-1 注意:将共享内存段与当前进程脱离不等于删除共享内存段
共享内存相关的命令行操作
查看共享内存
ipcs -m:查看当前的共享内存
使用举例:
perms:该共享内存的权限(类似文件一样,有权限的,如果该共享内存的权限是0,那么就没有人或者进程能够去读取他) nattch:挂接该共享内存的进程数
删除共享内存
ipcrm -m [shmid]:删除共享内存
使用举例:
代码练习
Comm.hpp文件
#pragma once #include<iostream> #include<sys/types.h> #include<sys/ipc.h> #include<cstdlib> #include<cstring> #include<cerrno> #include<unistd.h> #include<sys/shm.h> #include<sys/stat.h> #include<fcntl.h> #include<cassert> #include"Log.hpp" using namespace std; #define PATH_NAME "/home/ljg/linux_for_practice/2022_11_4/" #define PROJ_ID 0x666 #define SHM_SIZE 4096 #define FIFO_FILE ".fifo" #define READER O_RDONLY #define WRITER O_WRONLY key_t CreatKey() { key_t key = ftok(PATH_NAME, PROJ_ID); if(key < 0) { std::cerr << "fork:" << strerror(errno) << std::endl; exit(1); } return key; } void CreatFifo() { umask(0); if(mkfifo(FIFO_FILE, 0666) < 0) { Log() << strerror(errno) << endl; exit(2); } } int Open(const std::string &filename, int flags) { return open(filename.c_str(), flags); } int Wait(int fd) { uint32_t values = 0; ssize_t s = read(fd, &values, sizeof(uint32_t)); return s; } int Signal(int fd) { uint32_t cmd = 1; write(fd, &cmd, sizeof(cmd)); } int Close(int fd, const std::string& filename) { close(fd); unlink(filename.c_str()); }
IpcShmSer.cpp
#include"Comm.hpp" #include"Log.hpp" using namespace std; const int flags = IPC_CREAT | IPC_EXCL;//创建全新的共享内存 int main() { CreatFifo(); cout << "open begin" << endl; int fd = Open(FIFO_FILE, READER); cout << "open end" << endl; assert(fd >= 0); key_t key = CreatKey(); Log() << "key:" << key << endl; int shmid = shmget(key, SHM_SIZE, flags | 0666); Log() << "create shm begin, shmid:" << shmid << endl; if(shmid < 0) { Log() << "shmget:" << strerror(errno) << endl; return 2; } Log() << "create shm success, shmid:" << shmid << endl; //用它 //1.将共享内存和自己的进程产生关联 char* str = (char*)shmat(shmid, nullptr, 0); Log() << "attach shm success" << endl; sleep(5); while(true) { //让读端进行等待 if(Wait(fd) <= 0) { break; } printf("%s\n", str); sleep(1); } Log() << "deattach shm sucess" << endl; shmdt(str); //删它 shmctl(shmid, IPC_RMID, nullptr); Log() << "delete shmid success" << endl; Close(fd, FIFO_FILE); return 0; }
IpcShmCli.cpp文件
#include "Comm.hpp" #include "Log.hpp" using namespace std; int main() { int fd = Open(FIFO_FILE, WRITER); assert(fd >= 0); //创建相同的key值 key_t key = CreatKey(); Log() << "key:" << key << endl; int shmid = shmget(key, SHM_SIZE, IPC_CREAT); if (shmid < 0) { Log() << "shmget:" << strerror(errno) << endl; return 2; } //挂接 char* str = (char*)shmat(shmid, nullptr, 0); sleep(5); //使用共享内存 while(true) { printf("Please Enter#"); fflush(stdout); ssize_t s = read(0, str, SHM_SIZE);//将输入的数据写到共享内存中 if(s > 0) { str[s] = '\0'; } Signal(fd); } shmdt(str); return 0; }
Log.hpp文件
#pragma once #include<iostream> #include<ctime> std::ostream& Log() { std::cout << "For Debug | " << "timestamp:" << (uint64_t)time(nullptr) << " "; return std::cout; }
Makefile文件
.PHONY:all all:IpcShmCli IpcShmSer IpcShmCli:IpcShmCli.cpp g++ -o $@ $^ -std=c++11 IpcShmSer:IpcShmSer.cpp g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm IpcShmCli IpcShmSer
扩展
mmap
将进程中的个区域和特定的文件建立关联和映射。
1、mmap将一个文件或者其它对象映射进内存。mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。
2、两个进程可以通过映射普通文件实现共享内存通信。
3、在网络下载中,可以通过映射整个文件,使文件减少一次从内核态到用户态的拷贝,从而加快传输的效率。
一些概念了解
临界资源:能够被多个进程看到的公共资源就是临界资源。
如果没有对临界资源进行任何保护,对于临界资源的访问,双方进程在进行访问的时候,就都是乱序的,可能会因为读写交叉而导致的各种乱码和访问控制方面的问题。(比如两个进程通过printf向显示器上打印资源)
临界区:对多个进程而言,访问临界资源的代码,就是临界区。(比如printf代码,read代码,write代码等等)
对于共享内存来说,共享内存就是临界资源,两个进程中对共享内存进行读写的代码部分就是临界区。
原子性:一件事情,要么没做,要么做完了,没有中间状态,我们称其为原子性。
从代码层面上来说:cnt++、++cnt、x = y(读取y至寄存器,再把该值写入x)都不是原子操作。
只有x=1是原子操作(直接将一个常量值加载到内存中)。
互斥:任何时刻,只允许一个进程,访问临界资源。
system V消息队列
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
特性方面
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
命令行操作:
ipcs -q:查看消息队列 ipcrm -q [msid]:删除消息队列
system V信号量
本质
信号量的本质就是一个计数器,我们想要访问临界资源,就要先访问信号量,当然,信号量本身也是一种临界资源。
注意:信号量所对应的操作是原子的,这是由系统所封装好的。
函数操作
信号量的获取:
信号量的控制:
信号量的加减操作:
命令行操作
ipcs -s ipcrm -s [semid]
共享内存/消息队列/信号量函数和命令行对比
函数对比
共享内存 | 消息队列 | 信号量 | |
---|---|---|---|
获取 | shmget() | msgget() | semget() |
控制 | shmctl() | msgctl() | semctl |
操作 | shmat/shmdt | msgsnd/msgrcv | semop |
命令行操作对比
ipcs -m/-q/-s ipcrm -m/-q/-s