System-V 共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
共享内存概念
共享内存是一种进程间通信(IPC)机制,允许多个进程访问同一块物理内存区域。而这块区域是由操作系统开辟的,因为进程间具有独立性,所以这块内存不是由某个进程创建的。
如图所示,多个进程可以同时访问共享内存区域。这意味着不同的进程可以读取和写入相同的数据,从而实现进程间的数据共享和协作。
现在我们就来学习一下如何创建并使用一个共享内存。
创建共享内存—shm
shmget
功能:用来创建共享内存。头文件:<sys/ipc.h>
和<sys/shm.h>
。
原型:int shmget(key_t key, size_t size, int shmflg);
参数:
key
: 用于标识要创建或获取的共享内存段。
key
是一个数字。它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。- 第一个进程可以通过
key
创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了; - 对于一个已经创建好的共享内存,key在共享内存的描述对象当中。
size
:共享内存的大小,单位是字节。
共享内存的大小是以4kb为基本单位开辟的,也就是4096个字节。所以,设置shm大小的时候最好以4096的整数倍开辟空间。就算你要开辟一个字节大小的空间,那么系统也会给开辟4096个字节,但是你只能使用1个字节的空间,多的无法使用。shmflg
:指定共享内存的访问权限和其它选项。shmflg
常用的选项有2个,本质是一个位图。IPC_CREAT
:如果创建的共享内存不存在,就直接创建;如果存在就直接获取。IPC_EXCL
:此标志与IPC_CREAT
一起使用,以确保此调用创建区段。 如果该 segment 已存在,则调用失败。(此标志不可单独使用)
当然也可以跟文件的读写权限,直接将权限值按照八进制按位或到第三个参数中即可,例如:IPC_CREAT|IPC_EXCL|0666
,意思是如果共享内存不存在就直接创建,并且读写权限是0666。
返回值:创建成功的话返回一个int类型,这个整型叫做shmid
,用来标识唯一的共享内存;创建失败则返回-1
,并设置一个错误码。
ftok
说了这么多,关键点就在于如何获取到key
,因此我们还需要了解一个函数ftok
。这个函数可以帮助我们生成key
。
功能:通过一个路径名和项目的唯一id来生成一个System V ipc
的key。
头文件:<sys/types.h>
和<sys/ipc.h>
函数原型:key_t ftok(const char *pathname, int proj_id);
pathname
:路径名称。可以自己指定。proj_id
:项目的唯一id。
返回值:创建成功返回一个key_t
类型的值,创建失败返回-1,并设置一个错误码。
所以只需要传入一个路径和一个数字,ftok
就会生成一个key
。
示例:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
using namespace std;int main()
{key_t key = ftok("/home/HJW/linux-learning/System_V", 1);cout << key << endl;return 0;
}
key值已经被创建出来了,需要注意的是path
必须是一个存在的路径,只有这样才能展现出唯一性,ftok
会利用path和传入的第二个参数使用某种算法来得到key
值。需要进程间通信的双方,只需要事先约定好这个path
以及第二个整型,就可以利用ftok
产生相同的key
值,进而访问同一块共享内存了。
然后我们来对shmget
进行使用。
int main()
{key_t key = ftok("/home/HJW/linux-learning/System_V", 10);int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);cout << key << " " << shmid << endl;return 0;
}
上面的代码中,就是一个简单的创建共享内存的过程,参数就不再进行解释了,直接看结果。
我们可以通过ipcs -m
来查看建立的共享内存。
可以看到,我们创建的共享内存,key=0x0a010d99
转换成16进制就是167841177,shmid=1
,拥有者就是自己,初始权限perm=666
,大小是4096字节,nattch
是连接这个共享内存的连接数,这个我们一会在讲。
建立完成之后,进程已经结束了,但是我们创建的共享内存还在。共享内存的生命周期是随内核的!也就是说用户不主动关闭,共享内存就会一直存在,除非内核重启(用户主动释放)
所以我们想要删除这个共享内存,有2种方式可以删除:指令方式/接口方式。
通过指令方式删除掉话,我们需要使用ipcrm -m shmid
的方式,其中shmid
就是我们刚才看到的shmid。还要其它的关闭方法,可以自己使用man
手册进行查看。
如果我们继续执行刚才的代码,可以看到shmid为-1,说明创建失败,所以需要先删除之前创建的共享内存。
shmctl
这个函数的作用是控制共享内存的各种属性。
头文件:<sys/ipc.h>和<sys/shm.h>
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
:由shmget
返回的共享内存标识码cmd
:将要采取的动作(有三个可取值)
命令 | 说明 |
---|---|
IPC_STAT | 把shmid_ds 结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds 数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
buf
:指向一个保存着共享内存的模式状态和访问权限的数据结构
上面的知识点全部围绕着一个展开的,那就是shmid_ds
,所以我们先来学习这个结构体。
struct shmid_ds {struct ipc_perm shm_perm; /* Ownership and permissions */size_t shm_segsz; /* Size of segment (bytes) */time_t shm_atime; /* Last attach time */time_t shm_dtime; /* Last detach time */time_t shm_ctime; /* Creation time/time of lastmodification via shmctl() */pid_t shm_cpid; /* PID of creator */pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */shmatt_t shm_nattch; /* No. of current attaches */...
};struct ipc_perm {key_t __key; /* Key supplied to shmget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */unsigned short __seq; /* Sequence number */
};
这个结构体中存储的是一个共享内存的基本信息。
返回值:成功时返回0,失败时返回-1,并设置错误码。
下面我就针对第二个参数cmd
来进行代码示例:
IPC_STAT
int main()
{key_t key = ftok("/home/HJW/linux-learning/System_V", 10);int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);struct shmid_ds shm;// 使用 shmctl 函数获取共享内存段的状态信息并存入 shm 结构体中shmctl(shmid, IPC_STAT, &shm); // 输出共享内存段的上次访问时间(atime)cout << "atime:" << shm.shm_atime << endl;// 输出共享内存段的上次删除时间(dtime)cout << "dtime" << shm.shm_dtime << endl;// 输出共享内存段的上次状态改变时间(ctime)cout << "ctime:" << shm.shm_ctime << endl;// 输出创建共享内存段的进程 ID(cpid)cout << "cpid:" << shm.shm_cpid << endl;return 0;
}
IPC_SET
int main()
{key_t key = ftok("/home/HJW/linux-learning/System_V", 10);int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);struct shmid_ds shm;shmctl(shmid, IPC_STAT, &shm);cout << "atime:" << shm.shm_atime << endl;shm.shm_atime = 6;// 修改shm信息shmctl(shmid, IPC_SET, &shm);// 重新获取shm信息shmctl(shmid, IPC_STAT, &shm);cout << "atime:" << shm.shm_atime << endl;return 0;
}
开始时,我们把共享内存的相关信息存储到了结构体shm
当中,然后把结构体的shm_atime
设置为6。
第二次通过shmctl
把修改后的状态信息写回到共享内存段中,此时的第二个参数就是IPC_SET
。
第三次调用就把共享内存的信息同步到shm
中,最后输出结果,验证是否修改成功。
IPC_RMID
这个就是通过接口来删除共享内存段,所以第3个参数我们设置成空指针即可。
int main()
{shmctl(2, IPC_RMID, nullptr);return 0;
}
此时我们就成功删除shmid
为2的共享内存了。
挂接共享内存
我们讲完了如何创建一个共享内存之后,接下来就要把2个不同的进程挂接起来,这样才能实现2个进程之间的通信。
shmat
功能:将共享内存连接到进程地址空间。
头文件:<sys/types.h>和<sys/shm.h>
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
:共享内存的标识id。shmaddr
:指定连接的地址。一般来说我们不会主动指定地址,这个参数传入nullptr即可,操作系统会自动的帮我们选择合适的地址连接。shmflg
:挂接共享内存的模式。
传入0,以读写的方式挂接共享内存;传入SHM_RDONLY
,以只读的方式挂接;传入SHM_RND
时,
SHM_RND
表示连接的地址需是页对齐的整数倍地址。
shmaddr
为NULL,核心自动选择一个地址
shmaddr
不为NULL且shmflg
无SHM_RND
标记,则以shmaddr
为连接地址。
shmaddr
不为NULL且shmflg
设置了SHM_RND
标记,则连接的地址会自动向下调整为SHMLBA
的整数倍。公式:shmaddr -(shmaddr % SHMLBA)
shmflg=SHM_RDONLY
,表示连接操作用来只读共享内存
返回值:挂接成功,返回挂接后共享内存的地址;挂接失败。返回(void *)-1
使用shmat
连接共享内存后,进程可以像访问普通内存一样访问共享内存段中的数据。
shmdt
既然能挂接,那么也能取消挂接共享内存,只需要使用shmdt
接口即可。
功能:将共享内存段与当前进程脱离。
头文件:<sys/types.h>
和<sys/shm.h>
函数原型:int shmdt(const void *shmaddr);
需要把挂接共享内存的地址传入到shmdt
中,就可以取消挂接。
返回值:成功返回0;失败返回-1。
注意:将共享内存段与当前进程脱离不等于删除共享内存段。
接下来我们就通过2个进程来实现共享内存的挂接和取消。processa
负责发送消息,processb
负责接收消息。
processa
进程:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
using namespace std;int main()
{key_t key = ftok("/home/HJW/linux-learning/System_V", 10);int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);char *ptr = (char *)shmat(shmid, nullptr, 0);for (int ch = 'A'; ch <= 'Z'; ch++){ptr[ch - 'A'] = ch;sleep(1);}shmctl(shmid, IPC_RMID, nullptr);return 0;
}
processa
进程通过ftok
生成key
,然后使用shmget
接口生成一个共享内存,再用shmat
将共享内存挂接到进程地址空间,此时ptr指针就指向这个共享内存了,最后在把26个字母写入到共享内存当中,然后使用shmctl
关闭共享内存。
processb
进程:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
using namespace std;int main()
{key_t key = ftok("/home/HJW/linux-learning/System_V", 10);int shmid = shmget(key, 4096, IPC_CREAT);char *ptr = (char *)shmat(shmid, nullptr, 0);while (true){cout << ptr << endl;sleep(5);}return 0;
}
processb
进程的ftok
参数和另一个进程一样,所以生成的key值都是一样的。随后通过shmget
获得共享内存的shmid
,这个共享内存是由A
维护的,因此B只需要拿到shmid
即可,共享内存的创建和销毁都由A来控制。
随后B进程在一个循环中,每隔五秒读取一次共享内存。结果如下: