共享内存就是多个进程间共同使用同一段物理内存空间,它是通过将同一段物理内存映射到不同进程的虚空间中来实现的。由于映射到不同进程的虚拟地址空间中,不同进程可以直接使用,不需要进行内存的复制,所以共享内存的效率很高。
优点:共享内存(shared memory)是最简单的最大自由度的Linux进程间通信方式之一。使用共享内存,不同进程可以对同一块内存进行读写。由于所有进程对共享内存的访问就和访问自己的内存空间一样,而不需要进行额外系统调用或内核操作,同时还避免了多余的内存拷贝,这种方式是效率最高、速度最快的进程间通信方式。
缺点:内核并不提供任何对共享内存访问的同步机制,比如同时对共享内存的相同地址进行写操作,则后写的数据会覆盖之前的数据。所以,使用共享内存一般还需要使用其他IPC机制(如信号量)进行读写同步与互斥。
原理:内核对内存的管理是以页(page)为单位的,Linux下一般一个page大小是4k。而程序本身的虚拟地址空间是线性的,所以内核管理了进程从虚拟地址空间到起对应的页的映射。创建共享内存空间后,内核将不同进程虚拟地址的映射到同一个页面。所以在不同进程中,对共享内存所在的内存地址的访问最终都被映射到同一页面。
共享内存的方式主要有四种:
System V
共享内存;
POSIX mmap
文件映射实现共享内存;
- 通过
memfd_create()
和fd
跨进程共享实现共享内存;
- 基于
dma-buf
的共享内存(多媒体、图形领域广泛使用)。
system V共享内存
基本介绍
System V
曾经也被称为AT&T System V
,是Unix
操作系统众多版本中的一支。它最初由AT&T
开发,在1983年第一次发布。一共发行了4个System V
的主要版本:版本1、2、3和4。
System V
共享内存机制为了在多个进程之间交换数据,内核专门留出了一块内存区域用于共享,共享这个内存区域的进程就只需要将该区域映射到本进程的地址空间中即可。内核直接实现了shmget/at
系统调用,最终也是靠tmpfs
来实现的。
System V
的IPC
对象有共享内存、消息队列、信号灯(量)。注意:在IPC的通信模式下,不管是共享内存、消息队列还是信号灯,每个IPC的对象都有唯一的名字,称为"键(key)"。通过"键",进程能够识别所用的对象。"键"与IPC对象的关系就如同文件名称于文件,通过文件名,进程能够读写文件内的数据,甚至多个进程能够公用一个文件。而在IPC的通信模式下,通过"键"的使用也能使得一个IPC对象能为多个进程所共用。
使用步骤
共享内存的使用过程可分为 创建->连接->使用->分离->销毁 这几步。
- 创建/打开共享内存
- 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
- 撤销共享内存的映射
- 删除共享内存对象
执行过程先调用shmget,获得或者创建一个IPC共享内存区域,并返回获得区域标识符。类似于mmap中先open一个磁盘文件返回文件标识符一样。
再调用shmat,完成获得的共享区域映射到本进程的地址空间中,并返回进程映射地址。类似与mmap函数原理。
使用完成后,调用shmdt解除共享内存区域和进程地址的映射关系。每个共享的内存区,内核维护一个struct shmid_ds信息结构,定义在sys/shm.h头文件中
相关API
1 2 3 4 | int shmget(key_t key, size_t size, int shmflg)
|
共享内存的创建使用shmget函数(shared memory get)函数。shmget
根据shm_key
创建一个大小为page_size
的共享内存空间,参数shmflag
是一系列的创建参数。如果shm_key
已经创建,使用该shm_key
会返回可以连接到该以创建共享内存的id
。
调用成功返回一个shmid
(类似打开一个或创建一个文件获得的文件描述符一样),调用失败返回-1
。
1 2 3 4 | void * shmat( int shmid, const void * shmaddr, int shmflg);
|
创建后,为了使共享内存可以被当前进程使用,必须紧接着进行连接操作。使用函数shmat(shared memory attach),参数传入通过shmget
返回的共享内存id
即可。
shmat
返回映射到进程虚拟地址空间的地址指针,这样进程就能像访问一块普通的内存缓冲一样访问共享内存。
1 | int shmdt(const void * shmadr);
|
当共享内存使用完毕后,使用函数shmdt
(shared memory detach
)进行解连接。该函数以shmat
返回的内存地址作为参数。
单个进程detach
时,并不会从内核中删除该共享内存,而是把相关shmid_ds
结构的shm_nattch
域的值减1
,当这个值为0时,内核才从物理上删除这个共享内存。即最后一个使用该共享内存的进程并detach
该共享内存后,内核将会自动销毁该共享内存自动销毁。当然,最好能显式的进行销毁,以避免不必要的共享内存资源浪费。
1 2 3 4 | int shmctl( int shmid, int cmd, struct shmid_ds * buf);
|
函数shmctl
(shared memory control
)可以返回共享内存的信息并对其进行控制。通过cmd
指定相应的控制操作,具体包括IPC_STAT
(得到共享内存的状态)IPC_SET
(改变共享内存的状态)、IPC_RMID
(删除共享内存);buf
是一个结构体指针,IPC_STAT
时,获取内存状态并存储在buf
种。如果要改变共享内存的状态,通过buf
来进行设定。
示例
writer
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | / * * * * * writer.c * * * * * * * /
int main( int argc, char * * argv)
{
int shm_id,i;
key_t key;
char buff[ 0x20 ];
char * p_map;
char * name = "./test_shm" ;
setbuf(stdin, 0 );
setbuf(stdout, 0 );
key = ftok(name, 0 );
if (key = = - 1 )
perror( "ftok error" );
shm_id = shmget(key, 4096 ,IPC_CREAT);
if (shm_id = = - 1 )
{
perror( "shmget error" );
return ;
}
p_map = (people * )shmat(shm_id,NULL, 0 );
printf( "[+] shared memory in writer's addr: %p\n" , p_map);
printf( "[+] input: " );
read( 0 , buff, 0x20 );
memcpy(p_map, buff, strlen(buff));
if (shmdt(p_map) = = - 1 )
perror( " detach error " );
}
|
reader
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | / * * * * * * * * * * reader.c * * * * * * * * * * * * /
int main( int argc, char * * argv)
{
int shm_id,i;
key_t key;
char * p_map;
char * name = "./test_shm" ;
key = ftok(name, 0 );
if (key = = - 1 ) {
perror( "ftok error" );
return - 1 ;
}
shm_id = shmget(key, 4096 ,IPC_CREAT);
if (shm_id = = - 1 )
{
perror( "shmget error" );
return - 1 ;
}
p_map = (char * )shmat(shm_id,NULL, 0 );
printf( "%s\n" ,p_map);
if (shmdt(p_map) = = - 1 ) {
perror( " detach error " );
return - 1 ;
}
}
|
我试了试,需要用root
权限运行。
运行结果:
POSIX mmap文件映射实现共享内存
POSIX
表示可移植操作系统接口(Portable Operating System Interface
,缩写为POSIX
),POSIX
标准定义了操作系统应该为应用程序提供的接口标准,是IEEE
为要在各种UNIX
操作系统上运行的软件而定义的一系列API
标准的总称,其正式称呼为IEEE 1003
,而国际标准名称为ISO/IEC 9945
。
POSIX
提供了两种在无亲缘关系进程间共享内存区的方法:
- 内存映射文件,由open函数打开,由mmap函数把所得到的描述符映射到当前进程空间地址中的一个文件。
- 共享内存区对象(shared-memory object),由shm_open函数打开一个
POSIX IPC
名字,所返回的描述符由mmap函数映射到当前进程的地址空间。
这两种共享内存区的区别在于共享的数据的载体(底层支撑对象)不一样:内存映射文件的数据载体是物理文件;共享内存区对象,也就是共享的数据载体是物理内存。共享内存,一般是指共享内存区对象,也就是共享物理内存。
posix的共享内存机制实际上在库过程中以及用户空间的其他部分被展示为完全的文件系统的调用过程,在调用完shm_open之后,需要调用mmap来将tmpfs的文件映射到地址空间,接着就可以操作这个文件了,需要注意的是,别的进程也可以操作这个文件,因此这个文件其实就是共享内存。
相关API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | int shm_open(const char * name, int oflag, mode_t mode);
/ / 创建并打开一个新的共享内存对象或者打开一个既存的共享内存对象, 与函数 open 的用法是类似的;函数返回值是一个文件描述符,会被下面的API使用。
int shm_unlink(const char * name);
/ / 删除一个共享内存对象名字。
int ftruncate( int fildes, off_t length);
/ / 设置共享内存对象的大小,新创建的共享内存对象大小为 0 。
void * mmap(void * addr, size_t len , int prot, int flags, int fildes, off_t off);
/ / 将共享内存对象映射到调用进程的虚拟地址空间。
int munmap(void * addr, size_t len );
/ / 取消共享内存对象到调用进程的虚拟地址空间的映射。
int close( int fildes);
/ / 当shm_open函数返回的文件描述符不再使用时,使用close函数关闭它。
int fstat( int fildes, struct stat * buf);
/ / 获得共享内存对象属性的stat结构体。结构体中会包含共享内存对象的大小(st_size),权限(st_mode),所有者(st_uid),归属组 (st_gid)。
int fchown( int fildes, uid_t owner, gid_t group);
/ / 改变一个共享内存对象的所有权。
int fchmod( int fildes, mode_t mode);
/ / 改变一个共享内存对象的权限。
|
示例
writer.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | int main()
{
/ * 创建共享对象,可以查看 / dev / shm目录 * /
int fd = shm_open(FILENAME, O_CREAT | O_TRUNC | O_RDWR, 0777 );
if (fd = = - 1 ) {
perror( "open failed:" );
exit( 1 );
}
/ * 调整大小 * /
if (ftruncate(fd, MAXSIZE) = = - 1 ) {
perror( "ftruncate failed:" );
exit( 1 );
}
/ * 获取属性 * /
struct stat buf;
if (fstat(fd, &buf) = = - 1 ) {
perror( "fstat failed:" );
exit( 1 );
}
printf( "the shm object size is %ld\n" , buf.st_size);
/ * 建立映射关系 * /
char * ptr = (char * )mmap(NULL, MAXSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );
if (ptr = = MAP_FAILED) {
perror( "mmap failed:" );
exit( 1 );
}
printf( "mmap %s success\n" , FILENAME);
close(fd); / * 关闭套接字 * /
/ * 写入数据 * /
char * content = "hello world" ;
strncpy(ptr, content, strlen(content));
sleep( 30 );
return 0 ;
}
|
reader.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | int main()
{
/ * 创建共享对象,可以查看 / dev / shm目录 * /
int fd = shm_open(FILENAME, O_RDONLY, 0 );
if (fd = = - 1 ) {
perror( "open failed:" );
exit( 1 );
}
/ * 获取属性 * /
struct stat buf;
if (fstat(fd, &buf) = = - 1 ) {
perror( "fstat failed:" );
exit( 1 );
}
printf( "the shm object size is %ld\n" , buf.st_size);
/ * 建立映射关系 * /
char * ptr = (char * )mmap(NULL, buf.st_size, PROT_READ, MAP_SHARED, fd, 0 );
if (ptr = = MAP_FAILED) {
perror( "mmap failed:" );
exit( 1 );
}
printf( "mmap %s success\n" , FILENAME);
close(fd); / * 关闭套接字 * /
printf( "the read msg is: %s\n" , ptr);
sleep( 30 );
return 0 ;
|
要添加-lrt
进行编译:
1 2 | gcc writer.c - lrt - o writer
gcc reader.c - lrt - o reader
|
运行结果:
memfd_create和fd跨进程共享实现共享内存
第三种是内存fd
,通过memfd_create
创建基于tmpfs
的匿名文件(返回文件描述符),再通过mmap
建立内存映射实现内存共享。
memfd_create
会创建一个匿名文件,并返回文件描述符。这个文件像普通文件一样,可以执行修改、截取、映射等操作。区别在于这个文件是存放在RAM
当中,在tmpfs
文件系统中创建。
该方法的共享内存中,存在一个问题,即如何让另外一个进程获得这个文件?因为memfd_create
创建的是匿名文件,无法在文件系统中找到相应文件(fd
),不能像其他共享内存机制一样约定好文件名进行共享,这让文件共享变得困难。
一种可行的方案是通过proc
来传递文件,进程创建的匿名文件在proc
系统中是有记录的,路径为/proc/<pid>/fd/<fd>
,创建文件的进程需要将这个路径传递给需要共享的进程,打开该路径就实现了文件共享。
相关API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int memfd_create(const char * name, unsigned int flags);
/ / 创建一个匿名内存文件并返回一个文件描述符指向它
int ftruncate( int fildes, off_t length);
/ / 设置共享内存对象的大小,新创建的共享内存对象大小为 0 。
void * mmap(void * addr, size_t len , int prot, int flags, int fildes, off_t off);
/ / 将共享内存对象映射到调用进程的虚拟地址空间。
int munmap(void * addr, size_t len );
/ / 取消共享内存对象到调用进程的虚拟地址空间的映射。
|
示例
writer.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | int main()
{
int fd = memfd_create(FILENAME, MFD_ALLOW_SEALING);
if (fd = = - 1 ) {
perror( "open failed:" );
exit( 1 );
}
/ * 调整大小 * /
if (ftruncate(fd, MAXSIZE) = = - 1 ) {
perror( "ftruncate failed:" );
exit( 1 );
}
/ * 获取属性 * /
struct stat buf;
if (fstat(fd, &buf) = = - 1 ) {
perror( "fstat failed:" );
exit( 1 );
}
printf( "[+] PID: %ld; fd: %d; /proc/%ld/fd/%d\n" , ( long ) getpid(), fd, ( long ) getpid(), fd);
printf( "[+] the shared object size is %ld\n" , buf.st_size);
/ * 建立映射关系 * /
char * ptr = (char * )mmap(NULL, MAXSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );
if (ptr = = MAP_FAILED) {
perror( "mmap failed:" );
exit( 1 );
}
printf( "[+] mmap %s success, addr: %p\n" , FILENAME, ptr);
/ * 写入数据 * /
char * content = "hello world" ;
strncpy(ptr, content, strlen(content));
sleep( 30 );
close(fd); / * 关闭套接字 * /
return 0 ;
}
|
reader.c
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | int main( int argc, char * argv[])
{
if (argc ! = 2 ) {
fprintf(stderr, "%s /proc/PID/fd/FD\n" , argv[ 0 ]);
exit(EXIT_FAILURE);
}
int fd = open (argv[ 1 ], O_RDWR);
if (fd = = - 1 ) {
perror( "open failed:" );
exit( 1 );
}
/ * 获取属性 * /
struct stat buf;
if (fstat(fd, &buf) = = - 1 ) {
perror( "fstat failed:" );
exit( 1 );
}
printf( "[+] the shared object size is %ld\n" , buf.st_size);
/ * 建立映射关系 * /
char * ptr = (char * )mmap(NULL, buf.st_size, PROT_READ, MAP_SHARED, fd, 0 );
if (ptr = = MAP_FAILED) {
perror( "mmap failed:" );
exit( 1 );
}
printf( "[+] mmap fd success, addr: %p\n" , ptr);
printf( "[+] the read msg is: %s\n" , ptr);
sleep( 30 );
close(fd); / * 关闭套接字 * /
return 0 ;
}
|
运行结果:
基于dma-buf的共享内存
dma-buf
的定义:
1 | The DMABUF framework provides a generic method for sharing buffers between multiple devices. Device drivers that support DMABUF can export a DMA buffer to userspace as a file descriptor (known as the exporter role), import a DMA buffer from userspace using a file descriptor previously exported for a different or the same device (known as the importer role), or both
|
简单地来说,dma-buf
可以实现buffer
在多个设备的共享,应用程序可以把底层驱动A
的buffer
导出到用户空间成为一个fd
,也可以把fd
导入到底层驱动B
。当然,如果进行mmap()
得到虚拟地址,CPU
也是可以在用户空间访问到已经获得用户空间虚拟地址的底层buffer
的。
因涉及到驱动,且前三种内存共享已经覆盖大部分,所以dma-buf
就不进行深入讨论。
数据同步
要注意的是共享内存本身没有提供任何同步功能。也就是说,在第一个进程结束对共享内存的写操作之前,并没有什么自动功能能够预防第二个进程开始对它进行读操作。共享内存的访问同步问题必须由程序员负责。可选的同步方式有互斥锁、条件变量、读写锁、纪录锁、信号灯。
总结
本文主要讨论了共享内存的四种方式System V
共享内存、POSIX mmap
文件映射实现共享内存、通过memfd_create()
和fd
跨进程共享实现共享内存以及基于dma-buf
的共享内存(多媒体、图形领域广泛使用),并对它们的使用进行简单的示例。
总的来说共享内存是高效快速的进程间数据同步的方式,避免了数据在用户空间以及内核空间的交换,缺点则是相关数据的同步需要由应用程序自身负责。
参考链接
- linux下的进程间通信之共享内存
- 共享内存的几点总结
- 深入理解进程间通信之共享内存
- 世上最好的共享内存
[2022冬季班]《安卓高级研修班(网课)》月薪三万班招生中~