什么是共享内存
作为Unix/Linux进程间通信最高效的方式,莫过于共享同一块内存区域进行读写操作了。共享内存(Shared Memory)相比其它进程间通信能够提供更好的安全性和使用效率,它本质上就是通过映射同一物理内存空间到不同进程从而实现通信的方式。使用共享内存可以快速地在同一主机不同进程间进行数据交换,而且能够对数据进行随机访问,所以它经常作为主机内部进程间通信的首选,但在数据同步及控制上需要借助于信号/信号量/互斥进行访问控制。
共享内存其实是占用进程的虚拟内存空间的,因为它需要将真实物理地址与虚拟内存空间进行映射,所以即使它在物理上是使用同一块地址,在使用这块共享内存的进程本身看来,它仍然是属于自己可访问地址空间上的一块线性区域。
如何使用共享内存
共享内存使用一般有以下几个步骤:创建/获取共享内存->映射共享内存->使用共享内存->释放共享内存->删除引用。下面从一个简单的例子来了解下它的使用过程。
共享内存API
共享内存涉及以下相关API调用(System V API):
System V
key_t ftok(const char *path, int id) // 创建key
int shmget(key_t key, size_t size, int shmflg) // 创建or获取共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg) // 映射共享内存
int shmdt(const void *shmaddr) // 删除共享内存引用
int shmctl(int shmid, int cmd, struct shmid_ds *buf) // 控制共享内存(获取状态,修改属性,删除)
- ftok
它可以根据传入路径及id自动生成一个key
,你可以在后续的shmget()
调用中使用这个key
用做共享内存的标识,不同进程间使用同一共享内存必须知道这个key
的。当然,你也完全可以自己定义一个key
来标识共享内存以避免路径变化时不同进程生成的key
发生不一致的坑。
- shmget
当创建或获取共享内存时,需要调用shmget()
,它接受一个共享内存标识符key
,�共享内存大小size
以及标志位shmflg
。标志位用于确定对共享内存的访问权限控制及相关操作,如IPC_CREATE
和IPC_EXCL
标识符都设置时,如果已经存在该key
关联的共享内存,errno
将直接返回EEXIST
。
- shmat
映射共享内存到当前进程的内存地址空间需要调用此函数,它接收shmid
(shmget()
返回)、shmaddr
(虚拟内存空间地址)及shmflg
(控制访问权限及相关操作)这三个参数。这里存在一个坑:如果当前进程多次调用shmat()
,并不会出现任何错误,得到的结果反而是在当前进程的虚拟内存地址空间出现多个共享内存地址映射,最终可能导致应用程序的地址空间资源耗尽,同时也可能使共享内存的引用最终无法得到正常的释放。
- shmdt
删除共享内存引用,接收参数为shmat()
返回的内存地址。
- shmctl
用于控制共享内存,接收参数为shmid
(shmget()
返回), 操作指令cmd
及虚拟内存状态buf
。操作指令有三种:查看状态IPC_STAT
,设置属性IPC_SET
和删除IPC_RMID
。
POSIX API
int shm_open(const char *name, int oflag, ...)
int shm_unlink(const char *name)
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
int munmap(void *addr, size_t len)
int msync(void *addr, size_t len, int flags)
- shm_open
创建共享内存段或连接到现有的已命名内存段,这个系统调用返回一个文件描述符,name
为这段共享内存的命名,oflag
为权限控制。
- shm_unlink
根据(shm_open()
返回的)文件描述符,删除共享内存段。实际上,这个内存段直到访问它的所有进程都退出时才会删除,这与在UNIX中删除文件很相似。但是,调用shm_unlink()
(通常由原来创建共享内存段的进程调用)之后,其他进程就无法访问这个内存段了。
- mmap
把共享内存段映射到进程的内存。这个系统调用需要shm_open()
返回的文件描述符,它返回指向内存的指针。(在某些情况下,还可以把一般文件或另一个设备的文件描述符映射到内存。)
- munmap
与mmap()
操作相反,取消共享内存映射。
- msync
用来让共享内存段与文件系统同步 — 当把文件映射到内存时,这种技术有用。
共享内存使用
这里用一个简单的例子来解释下共享内存的使用,我们将引入信号来进行同步控制,创建两个进程分别用于负责读和写的操作。
这里先定义一个数据结构
shared_data.h
#ifndef _SHARED_DATA_H_
#define _SHARED_DATA_H_
#include
#define BUFFER_SIZE 1024
typedef struct {
pid_t pid;
char buffer[BUFFER_SIZE];
} shared_data;
#endif
pid
用于记录等待信号的对象进程,buffer
用于记录数据。
基于System V API
读程序
shmread.c
#include "shmutils.h"
void handler(int signo)
{
printf("READER: get signal\n");
}
int main(int argc, char** argv)
{
bool running = true;
pid_t pid;
key_t shm_key;
if ((shm_key = ftok(".", 'm')) < 0)
{
perror("READER: call ftok failed!");
exit(EXIT_FAILURE);
}
printf("READER: shm_key: %d\n", shm_key);
signal(SIGUSR1, handler);
void* shm_p = NULL;
shared_data* shm_data = NULL;
int shm_id = shmget(shm_key, sizeof(shared_data), 0666 | IPC_CREAT | IPC_EXCL);
if (shm_id == -1)
{
if (EEXIST == errno)
{
printf("READER: share memory for key[%d] exist!\n", shm_key);
shm_id = shmget(shm_key, sizeof(shared_data), 0666);
shm_data = (shared_data*)shmat(shm_id, NULL, 0);
pid = shm_data->pid;
shm_data->pid = getpid();
kill(pid, SIGUSR1);
}
else
{
perror("READER: call shmget failed!\n");
exit(EXIT_FAILURE);
}
}
else
{
shm_p = shmat(shm_id, NULL, 0);
shm_data = (shared_data*)shm_p;
shm_data->pid = getpid();
pause(); // wait for awake
pid = shm_data->pid;
}
while (running)
{
pause();
if (strcmp(shm_data->buffer, END_STR) == 0)
{
running = false;
}
printf("READER: read from shm: %s\n", shm_data->buffer);
kill(pid, SIGUSR1);
}
shmdt(shm_p);
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
写程序
shmwrite.c
#include "shmutils.h"
void handler(int signo)
{
printf("WRITER: get signal\n");
}
int main(int argc, char** argv)
{
bool running = true;
key_t shm_key;
if ((shm_key = ftok(".", 'm')) < 0)
{
perror("WRITER: call ftok failed!");
exit(EXIT_FAILURE);
}
printf("WRITER: shm_key: %d\n", shm_key);
signal(SIGUSR1, handler);
pid_t pid;
void* shm_p = NULL;
shared_data* shm_data = NULL;
int shm_id = shmget(shm_key, sizeof(shared_data), 0666 | IPC_CREAT | IPC_EXCL);
if (shm_id == -1)
{
if (errno == EEXIST)
{
printf("WRITER: share memory for key[%d] exist!\n", shm_key);
shm_id = shmget(shm_key, sizeof(shared_data), 0666);
shm_data = (shared_data*)shmat(shm_id, NULL, 0);
pid = shm_data->pid;
shm_data->pid = getpid();
kill(pid, SIGUSR1);
}
else
{
perror("WRITER: call shmget failed!\n");
exit(EXIT_FAILURE);
}
}
else
{
shm_p = shmat(shm_id, NULL, 0);
shm_data = (shared_data*)shm_p;
shm_data->pid = getpid();
pause();
pid = shm_data->pid;
}
while (running)
{
printf("WRITER: write to shm: ");
fgets(shm_data->buffer, BUFFER_SIZE, stdin);
kill(pid, SIGUSR1);
if (strcmp(shm_data->buffer, END_STR) == 0)
{
running = false;
break;
}
pause();
}
shmdt(shm_p);
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
执行结果
▶ ./shmread &
[1] 78735
READER: shm_key: 1829132911
▶ ./shmwrite
WRITER: shm_key: 1829132911
WRITER: share memory for key[1829132911] exist!
READER: get signal
WRITER: write to shm: Hello, reader!
READER: get signal
READER: read from shm: Hello, reader!
WRITER: get signal
WRITER: write to shm: Do you know who I am?
READER: get signal
READER: read from shm: Do you know who I am?
WRITER: get signal
WRITER: write to shm: end
READER: get signal
READER: read from shm: end
[1] + 78735 done ./shmread
基于POSIX API
读程序
shmread.c
#include "shmutils.h"
void handler(int signo)
{
printf("READER: get signal\n");
}
int main(int argc, char** argv)
{
bool running = true;
pid_t pid;
signal(SIGUSR1, handler);
void* shm_p = NULL;
shared_data* shm_data = NULL;
int shm_id = shm_open(IPC_SHM_NAME, O_CREAT | O_RDWR | O_EXCL, 0666);
if (shm_id == -1)
{
if (EEXIST == errno)
{
printf("READER: share memory for key[%s] exist!\n", IPC_SHM_NAME);
shm_id = shm_open(IPC_SHM_NAME, O_RDWR, 0666);
ftruncate(shm_id, sizeof(shared_data));
shm_data = (shared_data*)mmap(0, sizeof(shared_data), PROT_READ | PROT_WRITE, MAP_SHARED, shm_id, 0);
pid = shm_data->pid;
shm_data->pid = getpid();
kill(pid, SIGUSR1);
}
else
{
perror("READER: call shm_open failed!\n");
exit(EXIT_FAILURE);
}
}
else
{
ftruncate(shm_id, sizeof(shared_data));
shm_data = (shared_data*)mmap(0, sizeof(shared_data), PROT_READ | PROT_WRITE, MAP_SHARED, shm_id, 0);
shm_data->pid = getpid();
}
while (running)
{
printf("READER: wait for awake...\n");
pause();
pid = shm_data->pid;
shm_data->pid = getpid();
printf("READER: awake from %0x.\n", pid);
if (strcmp(shm_data->buffer, END_STR) == 0)
{
running = false;
}
printf("READER: read from shm: %s\n", shm_data->buffer);
sleep(1);
kill(pid, SIGUSR1);
}
munmap(shm_p, sizeof(shared_data));
shm_unlink(IPC_SHM_NAME);
return 0;
}
写程序
shmwrite.c
#include "shmutils.h"
void handler(int signo)
{
printf("WRITER: get signal\n");
}
int main(int argc, char** argv)
{
bool running = true;
signal(SIGUSR1, handler);
pid_t pid;
void* shm_p = NULL;
shared_data* shm_data = NULL;
int shm_id = shm_open(IPC_SHM_NAME, O_CREAT | O_RDWR | O_EXCL, 0666);
if (shm_id == -1)
{
if (errno == EEXIST)
{
printf("WRITER: share memory for key[%s] exist!\n", IPC_SHM_NAME);
shm_id = shm_open(IPC_SHM_NAME, O_RDWR, 0666);
ftruncate(shm_id, sizeof(shared_data));
shm_data = (shared_data*)mmap(0, sizeof(shared_data), PROT_WRITE, MAP_SHARED, shm_id, 0);
pid = shm_data->pid;
shm_data->pid = getpid();
}
else
{
perror("WRITER: call shm_open failed!\n");
exit(EXIT_FAILURE);
}
}
else
{
ftruncate(shm_id, sizeof(shared_data));
shm_data = (shared_data*)mmap(0, sizeof(shared_data), PROT_READ, MAP_SHARED, shm_id, 0);
shm_data->pid = getpid();
}
while (running)
{
printf("WRITER: write to shm: ");
fflush(stdout);
fgets(shm_data->buffer, BUFFER_SIZE, stdin);
sleep(1);
kill(pid, SIGUSR1);
if (strcmp(shm_data->buffer, END_STR) == 0)
{
running = false;
break;
}
printf("WRITER: wait for awake...\n");
pause();
pid = shm_data->pid;
shm_data->pid = getpid();
printf("WRITER: awake from %0x.\n", pid);
}
munmap(shm_p, sizeof(shared_data));
shm_unlink(IPC_SHM_NAME);
return 0;
}
运行结果在这里就不再描述了。
共享内存进阶
System V API
查看共享内存
查看共享内存可以使用ipcs -m
即可,比如我们运行./shread
时,查看共享内存看到的信息:
▶ ipcs -m
IPC status from as of Sun Aug 6 14:41:33 CST 2017
T ID KEY MODE OWNER GROUP
Shared Memory:
m 851968 0x6d065a6f --rw-rw-rw- wangqun staff
删除共享内存
删除共享内存可以使用ipcrm -m $ID
来操作,比如我们想删除我们查询到的�ID为851968的共享内存,直接使用ipcrm -m 851968
即可。
配置共享内存参数
在Linux主机上,共享内存大小及数量都是可以通过内核参数进行控制的,这些参数都在/proc/sys/kernel
下。
- SHMMAX: 设置共享内存的最大值
- SHMMNI: 设置共享内存的最大使用数量
- SHMALL: 设置系统一次可用的共享内存总量,该值至少为
ceil(SHMMAX/PAGE_SIZE)
POSIX API
使用POSIX API进行共享内存申请时,不受内参数限制,只受物理内存限制,所以相比System V API少了使用上的限制,但如果想要查询共享内存的信息就不如System V API方便了。
使用文件进行mmap
在使用mmap()
时,其实我们是可以使用文件fd
来进行mmap
的,这样我们的共享内存数据是可以同步到文件中实现持久化的目的。
全部测试代码可以在我的GitHub上找到。
原文链接:https://wangqun.info/2017/08/06/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1-%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/