linux下共享内存的3种使用方式

news/2024/9/23 5:06:14/

进程是资源封装的单位,内存就是进程所封装的资源的一种。一般情况下,进程间的内存是相互隔离的,也就是说一个进程不能访问另一个进程的内存。如果一个进程想要访问另一个进程的内存,那么必须要进过内核这个桥梁,这就是共享内存。

linux中,共享内存有3种方式,分别是POSIX接口,mmap以及system V风格的接口。本文分别介绍这3种共享内存的使用方式。在3种方式中,POSIX接口简洁易用,是最常使用的;system V易用性不是很好,很少使用。

另外,在工作中,共享内存作为一种进程间通信的方式,我们很轻易就能想到它的优点:减少拷贝次数。但是一项技术有优点,同时也必然有局限性,共享内存的局限性就是,在使用的时候往往需要在进程间做同步,进程间同步也会带来性能上的损耗。

1POSIX

1.1/dev/shm

POSIX共享内存接口使用了/dev/shm临时文件系统。

在介绍POSIX共享内存接口之前,有必要先了解linux下的临时文件系统/dev/shm,从名字也能看出来,这个文件系统是专门用作共享内存的。从mount显示的信息中可以看出来,临时文件系统tmpfs被mount到了/dev/shm下。

这是一个临时文件系统,同时也是一个内存文件系统,也就是说在这个文件系统上创建的文件,都是保存在内存中的,而不是保存在磁盘上。可想而知,性能会比较高。可以像普通文件系统一样使用临时文件系统,打开、读写、关闭、删除、拷贝等操作和普通文件是一样的。但是要注意,临时文件系统是保存在内存中的,机器重启之后不再存在。

/dev/shm默认大小是机器物理内存的一半:

使用df -i /dev/shm可以查看默认的inode个数:

修改大小和inode个数,比如我想将大小修改为4G,inode个数修改为1000,使用如下命令进行修改,可以看到,修改是生效的。

1.2example

如下例子是linux文档中的例子,通过man shm_open可以看到这个例子。从例子的实现可以看出来,共享内存依赖项有两个:一个是/dev/shm临时文件系统,一个是mmap。mmap本身就是一种共享内存的方式。所以说POSIX共享内存和mmap并不是完全割裂的,前者依赖于后者。其实我们也可以完全不使用shm_open、shm_unlink接口,而是直接使用mmap,在/dev/shm下创建共享内存也是可以的。

pshm_ucase_bounce.c和pshm_ucase_send.c中分别创建共享内存,大小是struct shmbuf的大小。后者向内存中写hello,前者将hello改成大写的,然后后者打印数据。

pshm_ucase.h:

#include <sys/mman.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \} while (0)#define BUF_SIZE 1024   /* Maximum size for exchanged string *//* Define a structure that will be imposed on the sharedmemory object */struct shmbuf {sem_t  sem1;            /* POSIX unnamed semaphore */sem_t  sem2;            /* POSIX unnamed semaphore */size_t cnt;             /* Number of bytes used in 'buf' */char   buf[BUF_SIZE];   /* Data being transferred */
};

pshm_ucase_bounce.c:

#include <ctype.h>
#include "pshm_ucase.h"int
main(int argc, char *argv[])
{if (argc != 2) {fprintf(stderr, "Usage: %s /shm-path\n", argv[0]);exit(EXIT_FAILURE);}char *shmpath = argv[1];/* Create shared memory object and set its size to the sizeof our structure */printf("size:%d\n", sizeof(struct shmbuf));int fd = shm_open(shmpath, O_CREAT | O_EXCL | O_RDWR,S_IRUSR | S_IWUSR);if (fd == -1)errExit("shm_open");if (ftruncate(fd, sizeof(struct shmbuf)) == -1)errExit("ftruncate");/* Map the object into the caller's address space */struct shmbuf *shmp = mmap(NULL, sizeof(*shmp),PROT_READ | PROT_WRITE,MAP_SHARED, fd, 0);if (shmp == MAP_FAILED)errExit("mmap");/* Initialize semaphores as process-shared, with value 0 */if (sem_init(&shmp->sem1, 1, 0) == -1)errExit("sem_init-sem1");if (sem_init(&shmp->sem2, 1, 0) == -1)errExit("sem_init-sem2");/* Wait for 'sem1' to be posted by peer before touchingshared memory */if (sem_wait(&shmp->sem1) == -1)errExit("sem_wait");/* Convert data in shared memory into upper case */for (int j = 0; j < shmp->cnt; j++)shmp->buf[j] = toupper((unsigned char) shmp->buf[j]);/* Post 'sem2' to tell the to tell peer that it can nowaccess the modified data in shared memory */if (sem_post(&shmp->sem2) == -1)errExit("sem_post");/* Unlink the shared memory object. Even if the peer processis still using the object, this is okay. The object willbe removed only after all open references are closed. */shm_unlink(shmpath);exit(EXIT_SUCCESS);
}

pshm_ucase_send.c:

#include <string.h>
#include "pshm_ucase.h"int
main(int argc, char *argv[])
{if (argc != 3) {fprintf(stderr, "Usage: %s /shm-path string\n", argv[0]);exit(EXIT_FAILURE);}char *shmpath = argv[1];char *string = argv[2];size_t len = strlen(string);if (len > BUF_SIZE) {fprintf(stderr, "String is too long\n");exit(EXIT_FAILURE);}/* Open the existing shared memory object and map itinto the caller's address space */int fd = shm_open(shmpath, O_RDWR, 0);if (fd == -1)errExit("shm_open");struct shmbuf *shmp = mmap(NULL, sizeof(*shmp),PROT_READ | PROT_WRITE,MAP_SHARED, fd, 0);if (shmp == MAP_FAILED)errExit("mmap");/* Copy data into the shared memory object */shmp->cnt = len;memcpy(&shmp->buf, string, len);/* Tell peer that it can now access shared memory */if (sem_post(&shmp->sem1) == -1)errExit("sem_post");/* Wait until peer says that it has finished accessingthe shared memory */if (sem_wait(&shmp->sem2) == -1)errExit("sem_wait");/* Write modified data in shared memory to standard output */write(STDOUT_FILENO, &shmp->buf, len);write(STDOUT_FILENO, "\n", 1);exit(EXIT_SUCCESS);
}

2mmap

mmap在linux中是经常使用的,mmap不仅仅可以用来共享内存,当我们使用malloc申请内存时,默认情况下如果申请的内存大于128K,那么底层便会使用mmap来从系统申请内存;mmap同样也可以将系统的设备内存映射到用户态。

void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

MAP_SHARED共享,一个进程修改之后,另一个进程能看到。如果我们要使用共享内存,那么需要设置这个标志
MAP_PRIVATE

私有,也就是不共享,即使两个进程使用mmap映射的是同一个文件,偏移量都是一样的,那么一个进程的修改,另一个进程也看不到。

从下边的注释可以看到,设置MAP_PRIVATE,使用copy on write机制,当一个进程要写的时候,进程内会拷贝一份。另外,私有的情况下,数据最终会不会保存到文件中,是不确定的。

MAP_ANONYMOUS匿名映射,mmap的倒数第二个参数是一个fd,如果要进行文件映射,那么需要首先打开这个文件,再使用mmap进行映射;如果是匿名映射,那么就不需要指定fd,将fd设置为-1即可。

匿名映射常常用于父子进程间的内存共享,文件映射常常用于没有父子关系的进程间的内存共享。 如果我们想要内存中的内容会保存到一个文件中,并且开机之后还能够使用,那么就必须使用共享和文件映射的方式。

2.1文件映射

文件映射就是要基于文件系统的一个文件来映射,第一节中的POSIX接口,就是使用的临时文件系统中的文件进行映射。如下两个文件aa.c和bb.c,编译之后先运行aa,再运行bb,可以看到aa写的数据,bb能够读到;bb写的数据,aa也能读到。

aa.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>int main() {const char *filename = "./shared_file.txt";const size_t length = 100;int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0666);ftruncate(fd, length);char *shared_mem = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);close(fd);if (shared_mem == MAP_FAILED) {perror("mmap");return 1;}strcpy(shared_mem, "Hello from aa!");printf("after aa write\n");sleep(5);printf("aa read:%s\n", shared_mem);munmap(shared_mem, length);return 0;
}

bb.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>int main() {const char *filename = "./shared_file.txt";const size_t length = 100;int fd = open(filename, O_RDWR, 0666);char *shared_mem = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);close(fd);if (shared_mem == MAP_FAILED) {perror("mmap");return 1;}printf("bb read:%s\n", shared_mem);strcpy(shared_mem, "Hello from bb!");munmap(shared_mem, length);return 0;
}

2.2匿名映射

匿名映射可以用于父子进程间的内存共享,如下是一个例子。

①父进程中首先向共享内存中写"Hello from parent!"

②父进程调用fork创建子进程

③子进程读取内存中的内容,然后向内存中写数据“Hello from child!”

④父进程等待子进程退出,然后读取共享内存中的数据

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>int main() {// 创建匿名共享内存size_t length = 100;char *shared_mem = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);if (shared_mem == MAP_FAILED) {perror("mmap");return 1;}strcpy(shared_mem, "Hello from parent!");pid_t pid = fork();  // 创建子进程if (pid == 0) {// 子进程printf("child read:%s\n", shared_mem);strcpy(shared_mem, "Hello from child!");printf("child write:%s\n", shared_mem);} else {// 父进程wait(NULL);  // 等待子进程结束printf("parent read:%s\n", shared_mem);  // 读取共享内存内容}munmap(shared_mem, length);  // 解除映射return 0;
}

运行结果如下,从结果可以看出来,子进程写的数据,父进程可以读出来,说明内存在父子进程之间是共享的。

如果把mmap中的标志MAP_SHARED改为MAP_PRIVATE,那么内存在父子进程间是不共享的。子进程写数据之后,父进程也看不到,父进程中读出来还是“Hello from parent!”。这个时候内存在父进程和子进程中各有一份。MAP_PRIVATE下,使用写时拷贝的机制,只有写的时候,才会分配一份内存,读的时候不会,所以在子进程中一开始读内存中数据的时候还能看到一开始父进程写的数据。

2.3查看文件映射和匿名映射

/pro/pid/maps中显式了进程的内存映射。

只有在共享映射的时候,才能在maps文件中看到对应的映射,私有映射的时候看不到。

如下是文件映射,可以看到shared_file.txt映射的内存范围。

如下是匿名映射,可以看到匿名映射映射的是/dev/zero。

2.4mmap使用注意问题

2.4.1offset应为PAGE_SIZE的整数倍

mmap的最后一个形参offset,需要是页大小的整数倍,页大小通过sysconf(_SC_PAGE_SIZE)来获取。如果offset不是页的整数倍,那么会返回错误Invalid argument。本人测试,无论是文件映射还是匿名映射,MAP_SHARED还是MAP_PRIVATE,这条限制都存在。

2.4.2文件映射时,映射的内存不能超过文件的大小

当使用文件映射时,如果映射的内存的大小大于文件本身的大小,那么在调用mmap时并不会返回错误,但是在写数据的时候会出现段错误。

如下代码,如果shared_file.txt是不存在的,那么调用open的时候会创建该文件,默认大小是0。不调用ftruncate,直接调用mmap,映射的内存大小是100,这个时候mmap不会返回错误,但是在写内存的时候会出现段错误。所以在使用mmap映射文件的时候,必须要保证offset+length的数据不超过文件的大小才可以。ftruncate可以设置文件的大小。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>int main() {const char *filename = "./shared_file.txt";const size_t length = 100;int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0666);// ftruncate(fd, length);char *shared_mem = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);close(fd);if (shared_mem == MAP_FAILED) {perror("mmap");return 1;}strcpy(shared_mem, "Hello from aa!");printf("after aa write\n");sleep(5);printf("aa read:%s\n", shared_mem);munmap(shared_mem, length);return 0;
}

3system V

systemV类型的共享内存,要使用4个api。

①首先要使用ftok来获取一个key

②使用shmget获取一个id

③使用shmat获取共享内存的地址

④共享内存使用完毕之后使用shmdt删除共享内存

writer.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>#define SHM_SIZE 100  // 共享内存大小int main() {key_t key = ftok("./shmfile", 65);  // 创建一个唯一的键int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);  // 获取共享内存段if (shmid < 0) {perror("shmget");return 1;}char *str = (char *)shmat(shmid, NULL, 0);  // 将共享内存附加到当前进程地址空间if (str == (char *)(-1)) {perror("shmat");return 1;}// 向共享内存写入数据strcpy(str, "Hello from writer!");printf("Writer wrote: %s\n", str);sleep(20);shmdt(str);  // 解除共享内存return 0;
}

reader.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>#define SHM_SIZE 100  // 共享内存大小int main() {key_t key = ftok("./shmfile", 65);  // 创建一个唯一的键int shmid = shmget(key, SHM_SIZE, 0666);  // 获取共享内存段if (shmid < 0) {perror("shmget");return 1;}char *str = (char *)shmat(shmid, NULL, 0);  // 将共享内存附加到当前进程地址空间if (str == (char *)(-1)) {perror("shmat");return 1;}// 读取共享内存内容printf("Reader read: %s\n", str);shmdt(str);  // 解除共享内存return 0;
}

如果共享内存只是在父子进程间共享,那么不需要使用ftok通过文件来获取一个key,在使用shmget的时候,直接使用IPC_PRIVATE即可。如下是一个例子,在父子进程间共享内存。

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>#define SHM_SIZE 100  // 共享内存大小int main() {int shmid = shmget(IPC_PRIVATE, SHM_SIZE, 0666 | IPC_CREAT);  // 获取共享内存段if (shmid < 0) {perror("shmget");exit(1);}// 创建子进程pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);}if (pid > 0) {// 父进程char *str = (char *)shmat(shmid, NULL, 0);  // 将共享内存附加到父进程if (str == (char *)(-1)) {perror("shmat");exit(1);}// 向共享内存写入数据strcpy(str, "Hello from parent!");printf("Parent wrote: %s\n", str);sleep(4);printf("Parent read: %s\n", str);shmdt(str);  // 解除共享内存wait(NULL);  // 等待子进程结束} else {// 子进程sleep(1);  // 等待父进程写入数据char *str = (char *)shmat(shmid, NULL, 0);  // 将共享内存附加到子进程if (str == (char *)(-1)) {perror("shmat");exit(1);}// 读取共享内存内容printf("Child read: %s\n", str);strcpy(str, "Hello from child!");printf("child wrote: %s\n", str);shmdt(str);  // 解除共享内存}// 删除共享内存段if (pid > 0) {shmctl(shmid, IPC_RMID, NULL);  // 只有父进程删除共享内存}return 0;
}


http://www.ppmy.cn/news/1529164.html

相关文章

C++中string类的模拟实现

目录 1.string类的结构 2.默认成员函数 2.1.默认构造函数 2.2拷贝构造函数 2.3赋值运算符重载 2.4析构函数 3.迭代器(Iterators) 4.string类的空间操作(Capacity) 4.1size() 4.2capacity() 4.3clear() 4.4reserve() 5.元素访问(Element access) 6.string类的修…

LeetCode_sql_day31(1384.按年度列出销售总额)

目录 描述 1384.按年度列出销售总额 数据准备 分析 法一 法二 代码 总结 描述 1384.按年度列出销售总额 Product 表&#xff1a; ------------------------ | Column Name | Type | ------------------------ | product_id | int | | product_name | var…

NodeJs文档

文件操作 // 1. 导入fs模块 const fs require(fs)文件写入 //异步写入 // fs.writeFile(文件名&#xff0c; 待写入的数据&#xff0c; 选项设置&#xff08;可选&#xff09;&#xff0c; 回调函数) fs.writeFile(./座右铭.txt, 三人行&#xff0c;必有我师傅, err > {/…

MySQL函数介绍--日期与时间函数(二)

我相信大家在学习各种语言的时候或多或少听过我们函数或者方法这一类的名词&#xff0c;函数在计算机语言的使用中可以说是贯穿始终&#xff0c;那么大家有没有思考过到底函数是什么&#xff1f;函数的作用又是什么呢&#xff1f;我们为什么要使用函数&#xff1f;其实&#xf…

C++基础知识7 list

list 1. list的介绍及使用1.1 list的介绍1.2 list的使用1.2.1 list的构造1.2.2 list iterator的使用1.2.3 list capacity1.2.4 list element access1.2.5 list modifiers1.2.6 list的迭代器失效 2.1 模拟实现list 1. list的介绍及使用 1.1 list的介绍 1.2 list的使用 1.2.1 l…

Qt中的延时

单次触发延时 单次触发延时是指定时器在指定的延时后触发一次&#xff0c;然后自动停止。这种方式非常适合只需要延时执行一次操作的场景。 #include <QTimer> #include <QObject>class MyClass : public QObject {Q_OBJECT public:MyClass() {QTimer::singleSho…

MyBatis XML映射文件编写【后端 18】

MyBatis XML映射文件编写 MyBatis 是一个优秀的持久层框架&#xff0c;它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解用于配置和原始映射&#xff0c;将接口和 Java 的 POJOs …

Centos7.9 使用 Kubeadm 自动化部署 K8S 集群(一个脚本)

文章目录 一、环境准备1、硬件准备&#xff08;虚拟主机&#xff09;2、操作系统版本3、硬件配置4、网络 二、注意点1、主机命名格式2、网络插件 flannel 镜像拉取2.1、主机生成公私钥2.2、为啥有 Github 还用 Gitee2.3、将主机公钥添加到 Gitee2.3.1、复制主机上的公钥2.3.2、…