进程是资源封装的单位,内存就是进程所封装的资源的一种。一般情况下,进程间的内存是相互隔离的,也就是说一个进程不能访问另一个进程的内存。如果一个进程想要访问另一个进程的内存,那么必须要进过内核这个桥梁,这就是共享内存。
在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 shared
memory 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 size
of 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 touching
shared 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 now
access the modified data in shared memory */
if (sem_post(&shmp->sem2) == -1)
errExit("sem_post");
/* Unlink the shared memory object. Even if the peer process
is still using the object, this is okay. The object will
be 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 it
into 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 accessing
the 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;
}