您的位置:首页 > 其它

共享内存与mmap那点东西

2016-03-30 19:56 253 查看
共享内存是一种IPC形式,与其它IPC机制如管道、消息队列等相比,数据不必在进程与内核间多次交换,进程间通信的速度更快。当共享内存区映射到共享它的进程的地址空间时,再加以一些同步控制,这些进程就可以进行数据传送了。mmap函数提供了内存映射功能,可以把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间,下面首先介绍mmap的用法。

1、mmap

include <sys/mman.h>
void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);


mmap可使用普通文件以提供内存映射IO,或者是特殊文件以提供匿名内存映射,还可以使用shm_open以提供无亲缘关系进程间的Posix共享内存区,成功时返回指向映射区域的指针,失败时返回MAP_FAILED,即((void*)-1),并设置相应的errno。成功返回后,文件描述字fd可关闭,不影响后续共享内存的使用。

参数addr指定了文件描述字fd映射到进程地址空间的起始地址,不过这个参数一般为NULL,为空时地址由内核来选择。

参数length指定了fd映射到进程地址空间的字节数,字节计数从fd的offset处开始算起,offset为从fd文件开头的字节偏移量。

参数prot指定了内存映射区的读写权限,有几个常值:PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE,分别表示可执行、可读、可写、不可访问,前三个可使用按位或符号拼起来使用。

参数flags指定了内存映射区的共享模式,必须从MAP_SHARED与MAP_PRIVATE两者之间选择一个,前者表示当前进程修改的内存映射区对其它进程是可见的,也确实修改了文件fd并影响到其它进程,后者则恰好相反,表示当前进程修改的内存映射区对其它进程是不可见的,并没有修改文件fd也不会影响其它进程。flags除了这两个值之外,还有一些其它的可选值,用按位或符号连起来即可,不过考虑到移植安全,并不使用MAP_FIXED,参数addr也指定为NULL。

mmap用以内存映射,解除一个进程的的内存映射关系使用munmap。

int munmap(void *addr, size_t length);


参数addr为mmap返回的地址,length为映射区的大小。成功时返回0,失败时返回-1,并设置相应的errno。

对于mmap函数的flags参数,为MAP_PRIVATE时,调用进程对共享内存作的修改都会被丢弃掉;为MAP_SHARED时,内核维护共享内存与文件fd的内容一致,但有时候我们要手动同步共享内存与文件fd的内容,这个工作由msync函数来完成。

int msync(void *addr, size_t length, int flags);


msync成功时返回0,失败时返回-1,并设置相应的errno。参数addr为mmap返回的地址,length即映射区的大小,flags是MS_ASYNC、MS_SYNC、MS_INVALIDATE中的集合,但前两个不可同时指定。MS_ASYNC表示异步写,函数立即返回;MS_SYNC表示同步写,同步完成后函数才返回;MS_INVALIDATE表示与同一个文件fd映射的其它内存区域的原有数据将无效,以使它们立即更新。

下面以例子说明mmap的用法。

例一:fork

// increment.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int g_count = 0;

int main(int argc, char **argv)
{
int i, nloop;
if (2 != argc) {
printf("usage: %s <#loops>\n", argv[0]);
exit(EXIT_FAILURE);
}
nloop = atoi(argv[1]);
setbuf(stdout, NULL);
if (fork() == 0) { // child
for (i = 0; i < nloop; ++i) {
printf("child: %d\n", g_count++);
}
exit(EXIT_SUCCESS);
}
for (i = 0; i < nloop; ++i) { // parent
printf("parent: %d\n", g_count++);
}
exit(EXIT_SUCCESS);
}


例一是一个计数程序,全局计数变量g_count初始值为0,计数次数nloop从命令行指定,我们都知道fork的子进程不会继承父进程的地址空间,如这里的g_count,而是一份独立的拷贝,父、子进程分别对g_count计数nloop次,并输出计数结果,程序运行结果如下,可以看出父、子进程对g_count的操作并没有互相影响。

$ gcc -o test increment.c
$ ./test 10
parent: 0
parent: 1
parent: 2
parent: 3
parent: 4
parent: 5
parent: 6
parent: 7
parent: 8
parent: 9
child: 0
child: 1
child: 2
child: 3
child: 4
child: 5
child: 6
child: 7
child: 8
child: 9


例二:mmap sem

// increment2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define SEM_NAME "/semtest"

int main(int argc, char **argv)
{
int fd, i, nloop, zero = 0;
int *ptr;
sem_t *sem;
if (3 != argc) {
printf("usage: %s <pathname> <#loops>\n", argv[0]);
exit(EXIT_FAILURE);
}
nloop = atoi(argv[2]);
// open file
if ((fd = open(argv[1], O_RDWR | O_CREAT, FILE_MODE)) == -1) {
printf("open error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// initialize fd to 0
if (write(fd, &zero, sizeof(int)) == -1) {
printf("write error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// map into memory
if ((ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) {
printf("mmap error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// close file
if (close(fd) == -1) {
printf("close error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// create semaphore and initialize to 1
if ((sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, FILE_MODE, 1)) == SEM_FAILED) {
printf("sem_open error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// unlink semaphore
if (sem_unlink(SEM_NAME) == -1) {
printf("sem_unlink error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// stdout is unbuffered
setbuf(stdout, NULL);
if (fork() == 0) { // child
for (i = 0; i < nloop; ++i) {
if (sem_wait(sem) == -1) {
printf("sem_wait child: error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("child: %d\n", (*ptr)++);
if (sem_post(sem) == -1) {
printf("sem_post child error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}
for (i = 0; i < nloop; ++i) { // parent
if (sem_wait(sem) == -1) {
printf("sem_wait parent error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("parent: %d\n", (*ptr)++);
if (sem_post(sem) == -1) {
printf("sem_post parent error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}


例二在例一的基础上增加了mmap内存映射以在进程间共享数据,因为涉及进程间的数据同步,所以还增加了Posix信号灯sem。程序中,信号灯是个有名信号灯,名字为“/semtest”,mmap所需的共享文件fd从命令行指定,打开这个文件后,初始化为0以提供计数功能,而不是例一中的一个全局变量用来计数,接着使用mmap完成内存映射,mmap的参数指定了文件读写权限和进程间共享模式,随后关闭文件,内存映射完成后关闭文件是没有影响的。关于sem信号灯,打开时初始化为1,接着调用sem_unlink,这个对后续信号灯的使用并没有影响,因为内核维护着信号灯的引用计数,只有当最后一个sem_close调用或者程序结束后信号灯才会从系统中移除。最后同样是父、子进程对计数值的操作,操作对象是mmap返回的地址,因为mmap指定了MAP_SHARED,所以进程间互相影响也确实修改了共享文件中的内容,所以计数期间加了信号灯sem_wait/sem_post以完成同步,避免冲突,否则计数结果就会出错。程序运行结果如下,可以看出父、子进程实现了计数共享。

$ gcc -o test -pthread increment2.c
$ ./test count 10
parent: 0
parent: 1
child: 2
child: 3
child: 4
child: 5
child: 6
child: 7
child: 8
child: 9
child: 10
child: 11
parent: 12
parent: 13
parent: 14
parent: 15
parent: 16
parent: 17
parent: 18
parent: 19


使用file命令查看共享文件count的类型为data数据文件。

$ file count
$ count: data


例三:sem_init

// increment3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

struct shared
{
sem_t sem;
int count;
} shared;

int main(int argc, char **argv)
{
int fd, i, nloop;
struct shared *ptr;
if (3 != argc) {
printf("usage: %s <pathname> <#loops>\n", argv[0]);
exit(EXIT_FAILURE);
}
nloop = atoi(argv[2]);
// open file
if ((fd = open(argv[1], O_RDWR | O_CREAT, FILE_MODE)) == -1) {
printf("open error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// initialize fd to 0
if (write(fd, &shared, sizeof(struct shared)) == -1) {
printf("write error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// map into memory
if ((ptr = mmap(NULL, sizeof(struct shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) {
printf("mmap error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// close file
if (close(fd) == -1) {
printf("close error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// initialize semaphore that is shared between processes
if (sem_init(&ptr->sem, 1, 1) == -1) {
printf("sem_init error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// stdout is unbuffered
setbuf(stdout, NULL);
if (fork() == 0) { // child
for (i = 0; i < nloop; ++i) {
if (sem_wait(&ptr->sem) == -1) {
printf("sem_wait child: error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("child: %d\n", ptr->count++);
if (sem_post(&ptr->sem) == -1) {
printf("sem_post child error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}
for (i = 0; i < nloop; ++i) { // parent
if (sem_wait(&ptr->sem) == -1) {
printf("sem_wait parent error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("parent: %d\n", ptr->count++);
if (sem_post(&ptr->sem) == -1) {
printf("sem_post parent error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}


例二中的信号灯是个有名信号灯,信号灯还可以是基于内存映射的匿名信号灯,为此例三添加了shard结构体,成员变量为一个信号灯和一个计数器,mmap映射长度为这个结构体的长度,调用sem_init初始化信号灯的值为1,其第二个参数为1,目的是保证信号灯同步在进程间使用而非同一进程的不同线程间,其它用法不变,程序运行结果一样。

例四:MAP_ANONYMOUS

// increment_map_anon.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define SEM_NAME "/semtest"

int main(int argc, char **argv)
{
int i, nloop;
int *ptr;
sem_t *sem;
if (2 != argc) {
printf("usage: %s  <#loops>\n", argv[0]);
exit(EXIT_FAILURE);
}
nloop = atoi(argv[1]);
// anonymous mmap
if ((ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)) == MAP_FAILED) {
printf("mmap error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// create, initialize, and unlink semaphore
if ((sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, FILE_MODE, 1)) == SEM_FAILED) {
printf("sem_open error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
if (sem_unlink(SEM_NAME) == -1) {
printf("sem_unlink error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// stdout is unbuffered
setbuf(stdout, NULL);
if (fork() == 0) { // child
for (i = 0; i < nloop; ++i) {
if (sem_wait(sem) == -1) {
printf("sem_wait child: error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("child: %d\n", (*ptr)++);
if (sem_post(sem) == -1) {
printf("sem_post child error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}
for (i = 0; i < nloop; ++i) { //parent
if (sem_wait(sem) == -1) {
printf("sem_wait parent error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("parent: %d\n", (*ptr)++);
if (sem_post(sem) == -1) {
printf("sem_post parent error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}


例二、例三中的mmap是对一个共享文件的内存映射,为此首先要打开一个文件,不过这并不是必需的,mmap同样也提供了匿名内存映射,不用基于一个确实存在的文件来实现映射,这时mmap的flags为MAP_SHARED | MAP_ANONYMOUS,fd和offset被忽略,不过fd一般为-1,offset为0。例四在例二的基础上作了修改,通过mmap的参数变更,使用匿名内存映射,程序运行结果相同。

除了匿名内存映射之外,有时候我们还可以打开一个特殊的文件“/dev/zero”,将这个文件描述字fd作为mmap的参数也是可以的,任何从这个fd读取的数据都为0,写往这个fd的数据也都被丢弃,也可以完成进程间的数据共享。

mmap映射长度——

使用mmap映射一个普通文件时,还有一点值得注意,那就是mmap的第二个参数,即mmap的映射长度,上面的例子中mmap的映射长度等于共享文件的大小,然而它们可以不相等。

2、Posix共享内存区

mmap函数中的参数fd,可以是open函数打开的一个内存映射文件,还可以是shm_open函数打开的一个共享内存区对象,相关函数如下。

#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);


shm_open创建或打开一个共享内存区对象,创建成功后共享内存区对象的位置在/dev/shm目录下,shm_unlink移除一个共享内存区对象,链接时使用“-lrt”选项。

#include <unistd.h>
#include <sys/stat.h>
int ftruncate(int fd, off_t length);
int fstat(int fd, struct stat *buf);


ftruncate设置一个文件的长度,参数fd为shm_open的返回值,fstat获取一个文件fd的状态。

下面以一个例子说明shm_open等函数的用法。

// server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <semaphore.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

struct shmstruct // struct stored in shared memory
{
int count;
};

sem_t *sem; // pointer to named semaphore

int main(int argc, char **argv)
{
int fd;
struct shmstruct *ptr;
if (3 != argc) {
printf("usage: %s <shm_name> <sem_name>\n", argv[0]);
exit(EXIT_FAILURE);
}
shm_unlink(argv[1]); // unlink if used, OK if failed
// open shm
if ((fd = shm_open(argv[1], O_RDWR | O_CREAT | O_EXCL, FILE_MODE)) == -1) {
printf("shm_open error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// set size
if (ftruncate(fd, sizeof(struct shmstruct)) == -1) {
printf("ftruncate error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// mmap shm
if ((ptr = mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) {
printf("mmap error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// close shm
if (close(fd) == -1) {
printf("close error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
sem_unlink(argv[2]); // unlink if used, OK if failed
// open sem
if ((sem = sem_open(argv[2], O_CREAT | O_EXCL, FILE_MODE, 1)) == SEM_FAILED) {
printf("sem_open error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// close sem
if (sem_close(sem) == -1) {
printf("sem_close error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}


// client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <semaphore.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

struct shmstruct // struct stored in shared memory
{
int count;
};

sem_t *sem; // pointer to named semaphore

int main(int argc, char **argv)
{
int fd, i, nloop;
pid_t pid;
struct shmstruct *ptr;
if (4 != argc) {
printf("usage: %s <shm_name> <sem_name> <#loops>\n", argv[0]);
exit(EXIT_FAILURE);
}
nloop = atoi(argv[3]);
// open shm
if ((fd = shm_open(argv[1], O_RDWR, FILE_MODE)) == -1) {
printf("shm_open error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// mmap shm
if ((ptr = mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) {
printf("mmap error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// close shm
if (close(fd) == -1) {
printf("close error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// open sem
if ((sem = sem_open(argv[2], 0)) == SEM_FAILED) {
printf("sem_open error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
pid = getpid();
for (i = 0; i < nloop; ++i) {
if (sem_wait(sem) == -1) {
printf("sem_wait error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("pid %ld: %d\n", (long)pid, ptr->count++);
if (sem_post(sem) == -1) {
printf("sem_post error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}


例子中,仍然是一个计数程序,不过共享内存区的同步是在没有亲缘关系的进程间完成的。计数变量count放到了shmstruct结构中,同步机制使用Posix信号灯sem。server.c的作用是创建一个共享内存区供其它进程使用,其大小为shmstruct结构体的大小,接着创建一个有名信号灯并初始化为1。client.c的作用是计数,使用server.c已经创建好的共享内存区和信号灯,输出当前进程号和计数值,运行结果如下。

$ gcc -o server server.c -pthread -lrd
$ gcc -o client client.c -pthread -lrd
$ ./server shm sem
$ ./client shm sem 10 & ./client shm sem 10
[1] 8423
pid 8423: 0
pid 8423: 1
pid 8423: 2
pid 8423: 3
pid 8423: 4
pid 8423: 5
pid 8423: 6
pid 8423: 7
pid 8423: 8
pid 8423: 9
pid 8424: 10
pid 8424: 11
pid 8424: 12
pid 8424: 13
pid 8424: 14
pid 8424: 15
pid 8424: 16
pid 8424: 17
pid 8424: 18
pid 8424: 19
[1]+  已完成               ./c shm sem 10


可以看出,open与shm_open的用法类似,只不过shm_open创建的共享内存区对象位于虚拟文件系统中,在Linux上的位置是“/dev/shm”,通过file命令查看一下例子中创建的shm的类型,data类型,查看其内容可使用od命令。

$ file /dev/shm/shm
/dev/shm/shm: data
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: