一、共享内存简介
共享内存区是最快的IPC形式,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
即每个进程地址空间都有一个共享存储器的映射区,当这块区域都映射到相同的真正的物理地址空间时,可以通过这块区域进行数据交换,例如共享库就是这么实现的,很多进程都会使用同一个函数如printf,也许在真正的物理地址空间中只存在一份printf.o ,然后所有进程都映射到这一份printf.o 就实现了共享。
用管道或者消息队列传递数据:
用共享内存传递数据:
即使用共享内存传递数据比用消息队列和管道来说,减少了进入内核的次数,提高了效率。
二、mmap 函数
#include
功能:将文件或者设备空间映射到共享内存区。
原型 void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
参数
addr: 要映射的起始地址,通常指定为NULL,让内核自动选择
len:映射到进程地址空间的字节数
prot:映射区保护方式
flags:标志
fd:文件描述符
offset:从文件头开始的偏移量,必须是页大小的整数倍(在32位体系统结构上通常是4K)
返回值:成功返回映射到的内存区的起始地址;失败返回-1
prot 参数取值:
PROT_EXEC 表示映射的这一段可执行,例如映射共享库
PROT_READ 表示映射的这一段可读
PROT_WRITE 表示映射的这一段可写
PROT_NONE 表示映射的这一段不可访问
flag参数有很多种取值,这里只讲两种,其它取值可查看mmap(2)
MAP_SHARED 多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化。
MAP_PRIVATE 多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化,也不会真的写到文件中去。
内存映射文件示意图:
如果mmap成功则返回映射首地址,如果出错则返回常数MAP_FAILED。当进程终止时,该进程的映射内存会自动解除,也可以调用munmap解除映射:
功能:取消mmap函数建立的映射
原型 int munmap(void *addr, size_t len);
参数
addr: 映射的内存起始地址
len:映射到进程地址空间的字节数
返回值:成功返回0;失败返回-1
下面写两个程序测试一下:
mmap_write.c
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#include #include #include #include #include #include #include #include #include #include #include #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while( 0) typedef struct stu { char name[ 4]; int age; } STU; int main( int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s exit(EXIT_FAILURE); } int fd; fd = open(argv[ 1], O_CREAT | O_RDWR | O_TRUNC, 0666); if (fd == - 1) ERR_EXIT( "open"); lseek(fd, sizeof(STU) * 5 - 1, SEEK_SET); write(fd, "", 1); STU *p; p = (STU *)mmap( NULL, sizeof(STU) * 5, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (p == -1) ERR_EXIT( "mmap"); char ch = 'a'; int i; for (i = 0; i < 5; i++) { memcpy((p + i)->name, &ch, 1); (p + i)->age = 20 + i; ch++; } printf( "initialize over\n"); munmap(p, sizeof(STU) * 5); printf( "exit...\n"); return 0; } |
mmap_read.c
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#include #include #include #include #include #include #include #include #include #include #include #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while( 0) typedef struct stu { char name[ 4]; int age; } STU; int main( int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s exit(EXIT_FAILURE); } int fd; fd = open(argv[ 1], O_RDWR); if (fd == - 1) ERR_EXIT( "open"); STU *p; p = (STU *)mmap( NULL, sizeof(STU) * 5, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (p == -1) ERR_EXIT( "mmap"); int i; for (i = 0; i < 5; i++) { printf( "name = %s age = %d\n", (p + i)->name, (p + i)->age); } munmap(p, sizeof(STU) * 5); printf( "exit...\n"); return 0; } |
先运行mmap_write ,然后用od -c 查看文件内容:
mmap 编程注意点:
1、映射不能改变文件的大小;
2、可用于进程间通信的有效地址空间不完全受限于被映射文件的大小;
3、文件一旦被映射后,所有对映射区域的访问实际上是对内存区域的访问。映射区域内容写回文件时,所写内容不能超过文件的大小;
对于1,3点,将mmap_write.c 中40行以后的代码中的5改成10,即映射的内存大于文件的大小,这样写入是不会出错的,因为是向内存写入,但用od 查看时发现文件还是40 个字节,即只有前5个STU才被真正写入到了文件。
对于第2点,将mmap_write.c 和 mmap_read.c 都按上面说的更改成10,然后在mmap_write.c 中munmap 函数之前sleep(10); 先运行mmap_write,再在另一终端运行mmap_read,观察结果:
simba@ubuntu:~/Documents/code/linux_programming/UNP/system_v$ ./mmap_read test
name = a age = 20
name = b age = 21
name = c age = 22
name = d age = 23
name = e age = 24
name = f age = 25
name = g age = 26
name = h age = 27
name = i age = 28
name = j age = 29
exit...
即在mmap_write 对映射内存区域写入之后尚未取消映射时,mmap_read 也映射了test 文件,两个虚拟进程地址空间的映射区域都指向了同一块物理内存,所以也能读到write 进程对内存的修改,但进程结束后查看test 文件,还是40个字节而已。内存的映射是以页面为单位的,一般为4k,所以才有第2条的说法,其实这才是真正体现共享内存可以进程间通信的所在。
最后一点,与write 类似,将文件映射到内存后对内存进行写入,不一定会马上写回文件,有可能内核也会产生一个缓冲区,找个适当的时间内核再写回设备文件,write 之后可以调用fsync 进行同步,同样地,mmap 可以调用msync 进行同步。
参考:
《linux c 编程一站式学习》
《UNP》