目录
一.共享内存介绍
(一).什么是共享内存
(二).共享内存优点
(三).共享内存缺点
二.共享内存使用
(一).创建—shmget
①key
②size
③shmflg
④返回值
(二).连接—shmat
(三).分离—shmdt
(四).销毁—shmctl
(五).查看—ipcs
(六).删除—ipcrm
(七).读取与写入
三.共享内存与访问控制
(一).添加访问控制
(二).可能的陷阱
共享内存本质上就是内存中的一块区域,用于进程间通信使用。该内存空间由操作系统分配与管理。与文件系统类似的是,操作系统在管理共享内存时,不仅仅有内存数据块,同时还会创建相应结构体来记录该共享内存属性,以便于管理。
因此,共享内存不只有一份,可以根据需求申请多个。
进程之间进行通信的时候,会获取 到共享内存的地址,写端进程写入数据,读端进程通过直接访问内存完成数据读取。
相比于管道而言,共享内存不仅能够用于非父子进程之间的通信,而且访问数据的速度也比管道要快。这得益于通信直接访问内存,而管道则需要先通过操作系统访问文件再获得内存数据。
用于进程间通信时,共享内存本身不支持阻塞等待操作。这是因为当读端读取数据后,数据并不会在内存中清空。因此读端和写端可以同时访问内存空间,即全双工。因为共享内存本质是进程直接访问内存,无法主动停止读取,如果读端不加以限制,那么将持续读取数据。同理,写端也会持续写入数据。换句话说,共享内存本身没有访问控制。
想要使用共享内存首先要建立共享内存。
shmget会根据key值创建一个共享内存,因此当创建多个共享内存时,每一个key值要独一无二。
获得key值可以使用库函数ftok专门获取一个独一无二的key_t类型值。
参数pathname为路径,必须是真实存在且可以访问的路径。
参数proj_id是int类型数字,且必须传入非零值。
成功返回key_t值,失败返回-1。
ftok函数内部会根据路径和proj_id通过算法生成一个独一无二的key_t返回值。
多进程通信时,需要通信双方使用同一个key值,因此双方使用的ftok参数应该一致。
该参数用于确定共享内存大小。
一般而言是4096的整数倍,因为内存的块的大小就是4KB即4096B。因此即便我们需要的空间大小不是块大小的整数倍,操作系统实际上也还是分配块的倍数个。但在使用时,那些超过size大小的多余分配空间不能访问。
该参数用于确定共享内存属性。
使用上为:标志位 | 内存权限
标志位参数有两种:IPC_CREAT、IPC_EXCL
常用使用方式有两种:
方式 | 含义 |
---|---|
shmget(..., IPC_CREAT | 权限) | 创建失败不报错返回已有shmid |
shmget(..., IPC_CREAT | IPC_EXCL | 权限) | 创建失败报错返回-1 |
值得注意PC_EXCL无法单独使用。
通常情况下在多进程通信时,创建方使用IPC_CREAT | IPC_EXCL,接收方使用0即可。
返回值为int类型,称为shmid。每一个共享内存都会有一个shmid,用于连接与分离时传递参数。
创建共享内存后还不能直接使用,需要找到内存地址后才能使用,即连接。
shmid即shmget返回值。
shmaddr用于确定将共享内存挂在进程虚拟地址哪个位置,一般填nullptr即可代表让内核自己确定位置。
shmflg用于确定挂接方式,一般填0。
连接成功返回共享内存在进程中的起始地址,失败返回-1。
当使用完毕后,需要分离挂接的共享内存。
shmaddr与shmat的相同,为共享内存在进程中地址位置,一般填nullptr。
分离成功返回0,失败返回-1。
该接口本身用于控制共享内存,可用于销毁。
shmid不再介绍,cmd传入IPC_RMID,buf传nullptr。
成功返回0,失败返回-1。
该指令为系统指令。
使用时可以查看当前全部共享内存。
ipcs -m
通过指定共享内存shmid,进行删除。
ipcrm -m [shmid]
调用shmat后会返回一个地址,读端直接读取该地址数据,写端直接向该地址写入即可。
//读端, 将共享内存数据读取到文件,此处为显示器文件
char* p = (char*)shmat(...);
write(1, p, sizeof p);
//写端,将文件中数据写入共享内存,此处为键盘文件
char* p = (char*)shmat(...);
read(0, p, 4096);
通过博客第一部分我们知道,共享内存不支持访问控制,那么我们可不可以添加访问控制给共享内存呢——完全可以。
方式是借用命名管道的访问控制,即阻塞。
首先我们有如下代码,该代码是读端一直读取写端数据,直到写端输入quit为止。
//写端
int main()
{
key_t key = ftok(".", 131);
int shmid = shmget(key, 4096, IPC_CREAT|0660);//获取shmid
char* p = (char*)shmat(shmid, nullptr, 0);//连接
while(1){
ssize_t s = read(0, p, 4096);//写入shm
p[s - 1] = 0;
assert(s > 0);
(void)s;
}
shmdt(p);//分离
return 0;
}
//读端
int main()
{
key_t key = ftok(".", 131);
int shmid = shmget(key, 4096, IPC_CREAT|IPC_EXCL|0660);//创建
char* p = (char*)shmat(shmid, nullptr, 0);//连接
while(1){
assert(p != nullptr);
if(strcmp(p, "quit") == 0)break;
printf("%s\n", p);//读取shm中数据
sleep(1);
}
shmdt(p);//分离
shmctl(shmid, IPC_RMID, nullptr);//销毁
return 0;
}
但是因为共享内存无法访问控制,读端会一直读取数据,即便我们添加sleep函数也不能从根本解决问题。
解决方式是,在读端和写端分别加上管道的读端和写端。因为我们知道管道读端在读取到来自写端的数据前会阻塞,因此,将管道读端放在共享内存读端之前,将管道写端放在共享内存写端之后。
这样一来,当shm写端写入数据后会触发管道写端写数据,当管道写端写入数据后,管道读端才会停止阻塞,进而执行shm读端。
图例如下:
//写端
int main()
{
key_t key = ftok(".", 131);
int shmid = shmget(key, 4096, IPC_CREAT|0660);//获取shmid
char* p = (char*)shmat(shmid, nullptr, 0);//连接
int fd = open(..., O_WRONLY);//打开命名管道
while(1){
ssize_t s = read(0, p, 4096);//写入shm
p[s - 1] = 0;
assert(s > 0);
(void)s;
char i[4] = { 0 };
write(fd, i, sizeof i);//写入管道
}
shmdt(p);//分离
close(fd);
return 0;
}
//读端
int main()
{
int i = mkfifo(PATH_FIFO, 0660);//创建管道
assert(i >= 0);
key_t key = ftok(".", 131);
int shmid = shmget(key, 4096, IPC_CREAT|IPC_EXCL|0660);//创建
char* p = (char*)shmat(shmid, nullptr, 0);//连接shm
int fd = open(..., O_RDONLY);//连接管道
while(1){
char buf[4];
read(fd, buf, sizeof buf);//管道等待读取,阻塞
assert(p != nullptr);
if(strcmp(p, "quit") == 0)break;
printf("%s\n", p);//读取shm中数据
sleep(1);
}
shmdt(p);//分离
shmctl(shmid, IPC_RMID, nullptr);//销毁
close(fd);
return 0;
}
在添加访问控制时,会有一个可能的陷阱,就是命名管道可不可以在创建shm之前打开(open)呢?
不可以,因为打开管道要求读端和写端同时打开才能继续,否则就会阻塞。
如果阻塞的是写端还好,当读端创建完shm后写端创建失败返回shmid,但是如果阻塞的是读端,那么写端创建shm后,读端创建时因为加上IPC_EXCL的缘故,失败返回-1,之后shmat也失败返回nullptr,进而读端获取到的地址是空。
简单模块注意封装,复杂模块注意分层——未名
如有错误,敬请斧正