1. 基本概念
- 共享内存是最快的IPC,一旦内存区映射进程序的地址空间,进程间的数据传递就不在需要涉及内核,只是再共享内存区内存取数据需要用到同步,同步的方法有:互斥锁、条件变量、读写锁、记录锁和信号量。(同步可能需要较长时间)
- 将文件映射进程序时,这时的映射文件共享内存呈随文件系统的持续性。
- 和管道、FIFO、消息队列读写数据时的区别
1)管道(随进程)、FIFO(随进程)、消息队列(随内核或文件系统)读写数据时(消息队列可以通过内存映射实现、这时不涉及内核)。
2)共享内存读写数据 – 无需内核
2.API
2.1 内存映射文件: mmap
、munmap
和msync
函数
1) mmap
#include <sys/mman.h>
// addr为fd被映射入程序的起始地址,通常为NULL,这样内核会自己选择起始地址
// len:程序被映射的地址长度,映射的内容从fd的offset开始len长
// prot: PROT_READ 数据可读, PROT_WRITE数据可写, PROT_EXEC数据可执行,PROT_NONE数据不可访问
// flags: MAP_PRIVATED 程序改变这段映射区内容不影响其他程序,MAP_SHARED 变动影响映射区底层数据结构,MAP_FIXED
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
// 成功返回被映射区的起始地址,失败则返回MAP_FAILED
父子进程之间共享内存区的方法之一是父进程在调用fork前先指定MAP_SHARED调用mmap。
#include <semaphore.h>
#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
// 将想要共享的数据结构放入文件中,父进程将文件映射进内存后FORK,子进程同样拥有该共享内存
int main()
{
sem_t *sem = sem_open("/tmp1212.test", O_CREAT | O_EXCL, FILE_MODE, 1);
if (sem == SEM_FAILED) {
printf("sem_open fail\n");
return -1;
}
sem_unlink("/tmp1212.test");
// fork之前加载至共享内存区域, 借助外界内存
int count = 0;
int fd = open("/tmp/test1213.txt", O_RDWR | O_CREAT, FILE_MODE);
// 1. 将想要的数据结构写入文件
write(fd, &count, sizeof(int));
// 2. 将文件映射到内存: 第一个参数为NULL,由内核选择开始地址,
void *ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
// 子父进程的ptr虽然不相同,但是指向的内容相同,都是共享内存区
if (fork() == 0) {
for (int i = 0; i < 100; i++) {
sem_wait(sem);
printf("child:%d\n", (*((int *)ptr))++); // 子进程的ptr
sem_post(sem);
}
return 0;
}
for (int i = 0; i < 200; i++) {
sem_wait(sem);
printf("father:%d\n", (*((int *)ptr))++); // 父进程的ptr
sem_post(sem);
}
return 0;
}
2) munmap
#include <sys/man.h>
int munmap(void *addr, size_t len); // addr是mmap返回的地址
// 成功返回0,失败返回-1
再次使用munmap
后的映射内存,则会产生SIGSEGV
信号。
3) msyn
#include <sys/man.h>
// flags: MS_ASYNC异步写,由内核排入写队列,立即返回 MS_SYNC同步写,写操作完成后返回,MS_INVALIDATE;
// addr和len为映射起始位置和长度,通常为整个映射区。
int msync(void *addr, size_t len, int flags); // 成功返回0, 失败返回-1
当 使用MAP_SHARED
方式映射文件至内存区后,如果内存区的内容发生变化,内核会随后将变化写回文件,如果需要立马写回文件,调用msync
。
#include <semaphore.h>
#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
// 将起同步作用的信号量和共享值count放到一个数据结构助攻,再将数据结构放入文件中,父进程将文件映射进内存后FORK,子进程同样拥有该共享内存
struct myStruct{
sem_t sem;
int count;
} myStruct;
int main()
{
// fork之前加载至共享内存区域, 借助外界内存
int fd = open("/tmp/test1213_2.txt", O_RDWR | O_CREAT, FILE_MODE);
// 1. 将想要的数据结构写入文件
write(fd, &myStruct, sizeof(myStruct));
// 2. 将文件映射到内存: 第一个参数为NULL,由内核选择开始地址,
struct myStruct *ptr = (struct myStruct *)mmap(NULL, sizeof(myStruct), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
// 3. 将共享内存中的数据初始化
ptr->count = 0;
sem_init(&ptr->sem, 1, 1); // sem_t, shared_flag,value
// 子父进程的ptr虽然不相同,但是指向的内容相同,都是共享内存区
if (fork() == 0) {
for (int i = 0; i < 100; i++) {
sem_wait(&ptr->sem);
printf("child:%d\n", (ptr->count)++); // 子进程的ptr
sem_post(&ptr->sem);
}
return 0;
}
for (int i = 0; i < 200; i++) {
sem_wait(&ptr->sem);
printf("father:%d\n", (ptr->count)++); // 父进程的ptr
sem_post(&ptr->sem);
}
return 0;
}
注意:
1)4.4BSD提供匿名内存映射
上述的内容**都需要在文件系统中创建一个额外文件**,在文件中保存数据结构,再将文件映射进程序内存中。 4.4bsd中将mmap的flags指定为MAP_SHARED|MAP_ANON, fd 指定为-1,offset将忽略,这样内存初始化为0
2)读取共享内存时注意:
A. 内核保护时以页为单位的,如果一页为4096,写入5000个字节,可以读取两页也就是8192个字节,超过8192则会产生SIGSEGV信号。
B. 当mmap内存超过文件大小时,如mmap10000,文件5000个字节,则访问0-8192都是合法的,8192~10000则会产生SIGBUS信号,大于10000则会产生的4096倍内存就会产生SIGSEGV信号
2.2 Posix 共享内存: shm_open
, shm_unlink
-
POSIX共享内存区
1)内存映射文件: 由open
打开文件,再由mmap
把得到的文件描述符映射到进程中。
2)共享内存区对象:由shm_open
打开一个Posix IPC名字
(也可以是文件系统中的一个路径名),返回描述符给mmap使用。 -
注意:
编译的时候需要加 -lrt
-
共享内存区对象的操作:
#include <sys/mman.h>
// oflag: O_CREAT,O_RDONLY,O_WRONLY,O_RDWR,O_EXCL mode权限
int shm_open(const char *name, int oflag, mode_t mode); // 成功返回非负的描述符,失败返回-1
int shm_unlink(const char *name); // 成功返回0, 失败返回-1; 删除一个共享内存区名字
- 对描述符的操作,指定新创建的共享内存区对象的大小。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int ftruncate(int fd, off_t len); // 指定创建新的共享内存对象大小,或修改老的共享内存对象大小。
int fstat(int fd, struct stat *buf); // 获取该fd对象的属性。
- demo
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <semaphore.h>
#include <fcntl.h>
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
// 共享内存区 基本使用
int main()
{
// 1. shm_open
int fd = shm_open("/test1214.txt", O_CREAT | O_RDWR, FILE_MODE);
if (fd == -1) {
printf("shm_open fail\n");
return -1;
}
// 2. ftruncate
if (ftruncate(fd, 1000) != 0) {
printf("ftruncate fail\n");
return -1;
}
// 3. mmap
void *ptr = nullptr;
ptr = mmap(NULL, 1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
printf("mmap fail\n");
return -1;
}
// 关闭fd
close(fd);
// 向共享内存区写数据
*((int *)ptr) = 1;
// . shm_unlink
/*
if (shm_unlink("/test1214.txt") !=0) {
printf("shm_unlink fail\n");
return -1;
}
*/
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <semaphore.h>
#include <fcntl.h>
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
// 共享内存区映射到不同程序的地址空间
int main()
{
char str[] = "/test1214.txt.2";
// 1. shm_open fd1
int fd1 = shm_open(str, O_RDWR | O_CREAT, FILE_MODE);
if (fd1 == -1) {
printf("shm_open fd1 fail\n");
return -1;
}
// 2. ftruncate fd1
if (ftruncate(fd1, sizeof(int)) != 0) {
printf("ftruncate fail\n");
return -1;
}
// 3. open fd2
int fd2 = open("test1214.txt.22", O_CREAT | O_RDONLY, FILE_MODE);
if (fd2 == -1) {
printf("open fd2 fail\n");
close(fd2);
return -1;
}
struct stat stat; // #include <sys/stat.h>
fstat(fd2, &stat);
// 4. fork
pid_t childPid;
if ((childPid = fork()) == 0) {
int *ptr2 = (int *)mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd2, 0);
int *ptr1 = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
printf("child : shm ptr2 = %p, ptr1 = %p\n", ptr2, ptr1);
sleep(2);
printf("ptr1 int = %d\n", *ptr1);
return 0;
}
printf("begin\n");
int *ptr4 = (int *)mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd2, 0);
int *ptr3 = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
printf("father: shm ptr4 = %p, ptr3 = %p\n", ptr4, ptr3);
*ptr3 = 233;
waitpid(childPid, NULL, 0); //#include <sys/wait.h>
return 0;
}
总结:基本套路
服务端
1. shm_unlink(IPC name) 删除可能仍然存在的共享内存区对象
2. shm_opem((IPC name) 创建或打开一个共享内存区对象,返回描述符
3. ftruncate 对返回的描述符做长度处理
4. mmap 将这个对象映入进程序空间
5. close 关闭这个描述符
客户端
1. shm_open 打开一个共享内存区对象,返回描述符
2. mmap 将这个对象映入进程序空间
3. close 关闭这个描述符
2.3 Posix共享内存特点
Posix共享内存对象
用来在无关进程间共享一块内存区域而无需创建一个底层的磁盘文件,shm_open
在基于内存的文件系统中创建一个文件用来给mmap
提供文件描述符,注意基于内存与基于磁盘创建的文件不同点,基于内存具有内核持续性,基于磁盘具有文件持续性。