1、概述
共享内存区是可用IPC形式中最快的。一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递就不再涉及内核。然而往该共享内存区存放信息或从中取走信息的进程间通常需要某种形式的同步。
2、mmap、munmap 和 msync 函数
(mmap性能总结 https://blog.csdn.net/qq_33611327/article/details/81738195)
mmap函数把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间。使用该函数有三个目的:
- 使用普通文件以提供内存映射I/0;
- 使用特殊文件以提供匿名内存映射;
- 使用shm_open以提供无亲缘关系进程间的Posix共享内存区。
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
// 返回:若成功则为被映射区的起始地址,若出错则为MAP_FAILED
其中addr可以指定描述符fd应被映射到的进程内空间的起始地址。它通常被指定为一个空指针,这样告诉内核自己去选择起始地址。无论哪种情况下,该函数的返回值都是描述符fd所映射到内存区的起始地址。
len是映射到调用进程地址空间中的字节数,它从被映射文件开头起第offser个字节处开始算。offsert通常设置为0。图12-6展示了这个映射关系。
内存映射区的保护由prof参数指定,它使用图12-7中的常值。该参数的常见值是代表读写访问的PROT_READ | PROT_WRITE。
flags使用图12-8中的常值指定。MAP_SHARED或MAP_PRIVATE这两个标志必须指定一个,并可有选择地或上MAP_FIXED。如果指定了MAP_PRIVATE,那么调用进程对被映射数据所作的修改只对该进程可见,而不改变其底层支撑对象(或者是一个文件对象,或者是一个共享内存区对象)。如果指定了MAP_SHARED,那么调用进程对被映射数据所作的修改对于共享该对象的所有进程都可见,而且确实改变了其底层支撑对象。
父子进程之间共享内存区的方法之一是,父进程在调用fork前先指定MAP_SHARED调用mmap。Posix.1保证父进程中的内存映射关系存留到子进程中。而且父进程所作的修改子进程能看到,反过来也一样。
mmap成功返回后, fd参数可以关闭。该操作对于由mmap建立的映射关系没有影响。
为从某个进程的地址空间删除一个映射关系,我们调用munmap。
#include <sys/mman.h>
int munmap(void *addr, size_t len);
// 返回:若成功则为0,若出错则为-1
在图12-6中,内核的虚拟内存算法保持内存映射文件(一般在硬盘上)与内存映射区(在内存中)的同步,前提是它是一个MAP_SHARED内存区。这就是说,如果我们修改了处于内存映射到某个文件的内存区中某个位置的内容,那么内核将在稍后某个时刻相应地更新文件。然而有时候我们希望确信硬盘上的文件内容与内存映射区中的内容一致,于是调用msync来执行这种同步。
#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
// 返回:若成功则为0,若出错则为-1
其中addr和len参数通常指代内存中的整个内存映射区,不过也可以指定该内存区的一个子集。flags参数是图12-9中所示各常值的组合。
MS_ASYNC和MS_SYNC这两个常值中必须指定一个,但不能都指定。它们的差别是,一旦写操作已由内核排入队列, MS_ASYNC即返回,而MS_SYNC则要等到写操作完成后才返回。如果还指定了MS_INVALIDATE,那么与其最终副本不一致的文件数据的所有内存中副本都失效。后续的引用将从文件中取得数据。
3、实例:在内存映射文件中给计数器持续加 1
父子进程共享一个内存区用于存放计数器。使用一个内存映射文件:open文件之后调用mmap把它映射到调用进程地址空间的某个文件。
// incr2.c
#include "unpipc.h"
#define SEM_NAME "mysem"
int main(int argc, char **argv)
{
int fd, i, nloop, zero = 0;
int *ptr;
sem_t *mutex;
if (argc != 3)
err_quit("usage: incr2 <pathname> <#loops>");
nloop = atoi(argv[2]);
/* open file, initialize to 0, map into memory */
fd = Open(argv[1], O_RDWR | O_CREAT, FILE_MODE);
Write(fd, &zero, sizeof(int));
ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
Close(fd);
/* create, initialize, and unlink semaphore */
mutex = sem_open(SEM_NAME, O_CREAT | O_EXCL, FILE_MODE, 1);
sem_unlink(SEM_NAME);
setbuf(stdout, NULL); /* stdout is unbuffered */
if (Fork() == 0) /* child */
{
for (i = 0; i < nloop; i++)
{
sleep(rand()%2);
Sem_wait(mutex);
printf("child: %d\n", (*ptr)++);
Sem_post(mutex);
}
printf("child end\n");
exit(0);
}
/* parent */
for (i = 0; i < nloop; i++)
{
sleep(rand()%2);
Sem_wait(mutex);
printf("parent: %d\n", (*ptr)++);
Sem_post(mutex);
}
sleep(3);
printf("parent end\n");
exit(0);
}
执行结果:
将上面程序改为使用一个Posix基于内存的信号量,而不是一个Posix有名信号量,并把该信号量存放在共享内存区中。
#include "unpipc.h"
struct shared
{
sem_t mutex; /* the mutex: a Posix memory-based semaphore */
int count; /* and the counter */
} shared;
int main(int argc, char **argv)
{
int fd, i, nloop;
struct shared *ptr;
if (argc != 3)
err_quit("usage: incr3 <pathname> <#loops>");
nloop = atoi(argv[2]);
/* open file, initialize to 0, map into memory */
fd = Open(argv[1], O_RDWR | O_CREAT, FILE_MODE);
Write(fd, &shared, sizeof(struct shared));
ptr = Mmap(NULL, sizeof(struct shared), PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
Close(fd);
/* initialize semaphore that is shared between processes */
Sem_init(&ptr->mutex, 1, 1);
setbuf(stdout, NULL); /* stdout is unbuffered */
if (Fork() == 0) /* child */
{
for (i = 0; i < nloop; i++)
{
sleep(rand()%2);
Sem_wait(&ptr->mutex);
printf("child: %d\n", ptr->count++);
Sem_post(&ptr->mutex);
}
printf("child end\n");
exit(0);
}
/* parent */
for (i = 0; i < nloop; i++)
{
sleep(rand()%2);
Sem_wait(&ptr->mutex);
printf("parent: %d\n", ptr->count++);
Sem_post(&ptr->mutex);
}
sleep(3);
printf("parent end\n");
exit(0);
}
执行结果:
4、BSD匿名内存映射
上面两个例子程序工作正确,然而我们不得不在文件系统中创建一个文件(其名字由命令行参数给出),调用open,然后往该文件中write一些0以初始化它。如果调用mmap的目的是提供一个将穿越fork由父子进程共享的映射内存区,那么我们可以简化上述情形,具体方法取决于实现。
(1) 4.4BSD提供匿名内存映射(anonymous memory mapping),它彻底避免了文件的创建和打开。其办法是把mmap的lags参数指定成MAP_SHARED | MAP_ANON,把fd参数指定为-1。offser参数则被忽略。这样的内存区初始化为0。我们将在图12-14中给出这种内存映射的一个例子。
(2) SVR4提供/dev/zero设备文件,我们open它之后可在mmap调用中使用得到的描述符。从该设备读时返回的字节全为0,写往该设备的任何字节则被丢弃。我们将在图12-15中给出使用该设备进行内存映射的一个例子。
图12-14给出了改用4.4BSD匿名内存映射后。我们不再open一个文件。在调用mmap时指定了MAP_ANON标志,并置第五个参数(描述符)为-1。
// incr_map_anon.c
#include "unpipc.h"
#define SEM_NAME "mysem"
/* include diff */
int main(int argc, char **argv)
{
int i, nloop;
int *ptr;
sem_t *mutex;
if (argc != 2)
err_quit("usage: incr_map_anon <#loops>");
nloop = atoi(argv[1]);
/* map into memory */
ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANON, -1, 0);
/* end diff */
/* create, initialize, and unlink semaphore */
mutex = sem_open(SEM_NAME, O_CREAT | O_EXCL, FILE_MODE, 1);
Sem_unlink(SEM_NAME);
setbuf(stdout, NULL); /* stdout is unbuffered */
if (Fork() == 0) /* child */
{
for (i = 0; i < nloop; i++)
{
sleep(rand()%2);
Sem_wait(mutex);
printf("child: %d\n", (*ptr)++);
Sem_post(mutex);
}
printf("child end\n");
exit(0);
}
/* parent */
for (i = 0; i < nloop; i++)
{
sleep(rand()%2);
Sem_wait(mutex);
printf("parent: %d\n", (*ptr)++);
Sem_post(mutex);
}
sleep(3);
printf("parent end\n");
exit(0);
}
5、SVR4 /dev/zero 内存映射
open文件/dev/zero后把得到的描述符用于mmap调用中。这样的映射保证内存映射区被初始化为0。
#include "unpipc.h"
#define SEM_NAME "mysem"
/* include diff */
int main(int argc, char **argv)
{
int fd, i, nloop;
int *ptr;
sem_t *mutex;
if (argc != 2)
err_quit("usage: incr_dev_zero <#loops>");
nloop = atoi(argv[1]);
fd = Open("/dev/zero", O_RDWR);
ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
Close(fd);
mutex = Sem_open(SEM_NAME, O_CREAT | O_EXCL, FILE_MODE, 1);
Sem_unlink(SEM_NAME);
setbuf(stdout, NULL);
if (Fork() == 0) /* child */
{
for (i = 0; i < nloop; i++)
{
sleep(rand()%2);
Sem_wait(mutex);
printf("child: %d\n", (*ptr)++);
Sem_post(mutex);
}
printf("child end\n");
exit(0);
}
/* parent */
for (i = 0; i < nloop; i++)
{
sleep(rand()%2);
Sem_wait(mutex);
printf("parent: %d\n", (*ptr)++);
Sem_post(mutex);
}
sleep(3);
printf("parent end\n");
exit(0);
}
执行结果:
6、小结
共享内存区是可用IPC形式中最快的,因为共享内存区中的单个数据副本对于共享该内存区的所有线程或进程都是可用的。然而为协调共享该内存区的各个线程或进程,通常需要某种形式的同步。
本章集中于mmap函数以及普通文件的内存映射,因为这是有亲缘关系的进程间共享内存空间的一种方法。一旦内存映射了一个文件,我们就不再使用read,write和lseek来访问该文件,而只是存取已由mmap映射到该文件的内存位置。把显式的文件I/O操作变换成存取内存单元往往能够简化我们的程序,有时候还能改善性能。
如果设置共享内存区的目的是为了穿越某个后续的fork在父子进程间共享它,那么通过使用匿名内存映射可简化其步骤,这样就不需要创建一个待映射的普通文件。这里或者涉及MAP_ANON这个新标志(适用于源自Berkeley的内核),或者涉及/dev/zero设备文件的映射(适用于源自SVR4的内核)。