APUE读书笔记---进程间通信(IPC)之共享内存区mmap

3/8/2017来源:ASP.NET技巧人气:2070

APUE读书笔记—进程间通信(ipC)之共享内存区mmap

1. 共享内存区概述

共享内存区是可用IPC形式中最快的。一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递就不在涉及内核。

“不再涉及内核”含义:进程不再通过执行任何进入内核的系统调用来彼此传递数据。

管道、FIFO和消息队列这些IPC形式,当两个进程要交换信息时,这些信息必须经由内核传递。通过让两个或多个进程共享一个内存区,共享内存区就绕过内核,提高了速度。

2. mmap / munmap /msync 函数

2.1 mmap

mmap函数可以把一个文件或者一个POSIX共享内存区对象映射到调用进程的地址空间。

#include <sys/mman.h> void *mmap(void *addr, size_t length, int PRot, int flags, int fd, off_t offset); //返回值:若成功,返回起始地址,若出错,返回MAP_FAILED addr 可以指定文件描述符fd被映射到进程内空间的起始地址。通常指定为NULL len 是映射到调用进程地址空间的字节数,它从被映射文件开头其第offset个字节处开始算,offset通常为0 prot 指定内存映射区数据的权限
prot 说明
PROT_EXEC 数据可被执行
PROT_READ 数据可读
PROT_WRITE 数据可写
PROT_NONE 数据不可访问

- flags 必须指定MAP_SHARED或MAP_PRIVATE中的一个,并可有选择的或上MAP_FIXED,从可移植性考虑,不应该指定。

flags 说明
MAP_SHARED 变动是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化
MAP_PRIVATE 变动是私自的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化,也不会真的写到文件中去
MAP_FIXED 准确地解释addr参数

- mmap成功返回后,fd参数可以关闭,该操作对于mmap建立的映射关系没有影响。

###2.2 munmap 删除某个进程的地址空间的映射关系,调用munmap函数

#include <sys/mman.h> int munmap(void *addr, size_t length); //返回值:若成功返回0,若出错,返回-1 addr是由mmap返回的地址,length是映射区的大小。 再次访问这些地址将导致向调用进程产生一个SIGSEGV信号。

2.3 msync

当flags参数为MAP_SHARED时,如果修改了处于内存映射到某个文件的内存区中某个位置,那么内核将在稍后某个时刻相应地更新文件。调用msync函数使磁盘上的文件和内存映射区中的内容一致。

#include <sys/mman.h> int msync(void *addr, size_t length, int flags); //返回值:若成功返回0,若出错返回-1 addr和length参数通常指代内存中的整个内存映射区,不过也可以指定内存区的一个子集。 flags参数是下图常值的组合
常值 说明
MS_ASYNC 执行异步写(立即返回)
MS_SYNC 执行同步写(等待写操作完成后返回)
MS_INVALIDATE 使高速缓存的数据失效

3. 例子

#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <stdlib.h> #include <sys/mman.h> int main(int argc, char **argv) { int fd; int len; char *ptr; if(argc < 1) { printf("./a.out filename\n"); exit(1); } fd = open(argv[1], O_RDWR); if(fd < 0){ perror("open err"); exit(1); } len = lseek(fd, 0, SEEK_END); ptr = mmap(NULL, len, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0); if(ptr == MAP_FAILED){ perror("mmap err"); exit(1); } close(fd); ptr[0] = 'a'; ptr[1] = 'b'; ptr[2] = 'c'; munmap(ptr, len); return 0; }

运行结果:

➜ IPC cat hello //hello文件内存 helloworld ➜ IPC ./a.out hello ➜ IPC cat hello //内存映射更改后的文件 abcloworld

4. 对mmap()返回地址的访问

linux采用的是页式管理机制,对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。

#include "unpipc.h" int main(int argc, char *argv[]) { int fd, i; char *ptr; size_t filesize, mmapsize, pagesize; if(argc != 4) sys_err("./a.out filename filesize mmapsize\n"); filesize = atoi(argv[2]); //映射的文件大小 mmapsize = atoi(argv[3]); //映射内存的大小 fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE); lseek(fd, filesize-1, SEEK_SET); write(fd, "", 1); ptr = mmap(NULL, mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); pagesize = sysconf(_SC_PAGESIZE); printf("pagesize = %ld\n", (long)pagesize); //打印当前页大小 for(i = 0; i < max(filesize, mmapsize); i += pagesize) { printf("ptr[%d] = %d\n", i , ptr[i]); ptr[i] = 1; printf("ptr[%ld] = %d\n", i + pagesize -1, ptr[i + pagesize - 1]); ptr[i + pagesize -1] = 1; } for(int j = 0; j < 10; j++) { printf("index = %d\n", j); printf("ptr[%ld] = %d\n", i+pagesize*j-1 , ptr[i+pagesize*j-1]); } return 0; }

运行:

➜ mmap ./test foo 5000 5000 pagesize = 4096 ptr[0] = 0 ptr[4095] = 0 ptr[4096] = 0 ptr[8191] = 0 index = 0 ptr[8191] = 1 index = 1 ptr[12287] = 0 index = 2 ptr[16383] = 0 index = 3 ptr[20479] = 0 index = 4 ptr[24575] = 0 index = 5 ptr[28671] = 0 index = 6 [1] 4261 segmentation fault (core dumped) ./test foo 5000 5000 ➜ mmap ls -l foo -rw-r--r-- 1 menwen menwen 5000 3月 3 09:23 foo ➜ mmap od -b -A d foo //-b 选项指定以八进制输出个字节,-A d 选项指定以十进制输出地址 0000000 001 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 0000016 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 * 0004080 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 001 0004096 001 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 0004112 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 * 0004992 000 000 000 000 000 000 000 000 0005000

由运行结果可知:

一个页大小为4096字节 当访问到第9 (2+7=9) 个pages时,出现段错误(segmentation fault (core dumped)),因此可知,系统真正映射到进程地址空间大小为8个page。 文件大小为5000字节,需要两个pages共8192字节,内核允许我们将ptr[8191]设置为1,但是通过od命令查看,它不也不写到foo文件中,因而该文件的大小仍是5000。

接下来我们试图将指定更大的内存映射区:

➜ mmap ./test foo 5000 8193 pagesize = 4096 ptr[0] = 0 ptr[4095] = 0 ptr[4096] = 0 ptr[8191] = 0 [1] 6000 bus error (core dumped) ./test foo 5000 8193 当映射内存区大小指定为8193字节时,大于两个pages的大小,出现了总线错误( bus error (core dumped)),可以看出,内核知道被映射的底层支撑对象也就是foo文件的大小,如果超出这个大小,就会发生总线错误。

因此,超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程,如下图所示:

这里写图片描述 注:内存真正映射地址空间的大小可能和运行环境有关系,以上运行环境 64位 Ubuntu 16.04LTS ,内存12个G。