map_normfile1 首先打开或创建一个文件,并把文件的长度设置为 5 个 people 结构大小。然后从 mmap()的返回地址开始,设置了 10 个 people 结构。然后,进程睡眠 10 秒钟,等待其他进程映射同一个文件,最后解除映射。 map_normfile2.c 只是简单的映射一个文件, 并以 people 数据结构的格式从 mmap()返回的 地址处读取 10 个 people 结构,并输出读取的值,然后解除映射。 分别把两个程序编译成可执行文件 map_normalfile1 和 map_normalfile2 后, 在一个终端上 先运行./map_normalfile2 /tmp/test_shm,程序输出结果如下: initialize over umap ok 在 map_normalfile1 输出 initialize over 之后,输出 umap ok 之前,在另一个终端上运行 map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理 后的结果): name: b age 20; age 24; name: g age 25; age 29; name: h age 26; name: I age 27; name: j age 28; name: k name: c age 21; name: d age 22; name: e age 23; name: f 在 map_normalfile1 输出 umap ok 后,运行 map_normalfile2 则输出如下结果: name: b age 20; age 24; name: age 0; age 0; name: age 0; name: age 0; name: age 0; name: name: c age 21; name: d age 22; name: e age 23; name: f 从程序的运行结果中可以得出的结论 1、 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的 大小; 2、 可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受 限于文件大小。打开文件被截短为 5 个 people 结构大小,而在 map_normalfile1 中初始化 了 10 个 people 数据结构,在恰当时候(map_normalfile1 输出 initialize over 之后,输出 umap ok 之前)调用 map_normalfile2 会发现 map_normalfile2 将输出全部 10 个 people 结构的值,后面将给出详细讨论。 注:在 linux 中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内 核也会为映射分配一个页面大小的内存。 当被映射文件小于一个页面大小时, 进程可以对从 mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外 的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有 效地址空间大小不会超过文件大小及一个页面大小的和。 3、 文件一旦被映射后,调用 mmap()的进程对返回地址的访问是对某一内存区域的访问, 暂时脱离了磁盘上文件的影响。所有对 mmap()返回地址空间的操作只在内存中有意义,只 有在调用了 munmap()后或者 msync()时,才把内存中的相应内容写回磁盘文件,所写内容 仍然不能超过文件的大小。 范例 2:父子进程通过匿名映射实现共享内存 #include #include #include #include typedef struct{ char name[4]; int age; }people; main(int argc, char** argv) { int i; people *p_map; char temp; p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS,-1,0); if(fork() == 0) { sleep(2); for(i = 0;i<5;i++) printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age); (*p_map).age = 100; munmap(p_map,sizeof(people)*10); //实际上,进程终止时,会自动解除映射。 exit(); } temp = 'a'; for(i = 0;i<5;i++) { temp += 1; memcpy((*(p_map+i)).name, &temp,2); (*(p_map+i)).age=20+i; } sleep(5); printf( "parent read: the first people,s age is %d\n",(*p_map).age ); printf("umap\n"); munmap( p_map,sizeof(people)*10 ); printf( "umap ok\n" ); } 考察程序的输出结果,体会父子进程匿名共享内存: child read: the 1 people's age is 20 child read: the 2 people's age is 21 child read: the 3 people's age is 22 child read: the 4 people's age is 23 child read: the 5 people's age is 24 parent read: the first people,s age is 100 umap umap ok 回页首 四、对 mmap()返回地址的访问 前面对范例运行结构的讨论中已经提到, linux 采用的是页式管理机制。 对于用 mmap()映射 普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由 mmap()的 len 参数指 定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址 大小取决于文件被映射部分的大小。 简单的说, 能够容纳文件被映射部分大小的最少页面个 数决定了进程从 mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大 小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明: 注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文 件的偏移部分,一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例: #include #include #include #include typedef struct{ char name[4]; int }people; main(int argc, char** argv) { int fd,i; int pagesize,offset; people *p_map; age; pagesize = sysconf(_SC_PAGESIZE); printf("pagesize is %d\n",pagesize); fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777); lseek(fd,pagesize*2-100,SEEK_SET); write(fd,"",1); offset = 0; p_map = //此处 offset = 0 编译成版本 1;offset = pagesize 编译成版本 2 (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset); close(fd); for(i = 1; i<10; i++) { (*(p_map+pagesize/sizeof(people)*i-2)).age = 100; printf("access page %d over\n",i); (*(p_map+pagesize/sizeof(people)*i-1)).age = 100; printf("access page %d edge over, now begin to access page %d\n",i, i+1); (*(p_map+pagesize/sizeof(people)*i)).age = 100; printf("access page %d over\n",i+1); } munmap(p_map,sizeof(people)*10); } 如程序中所注释的那样, 把程序编译成两个版本, 两个版本主要体现在文件被映射部分的大 小不同。文件的大小介于一个页面与两个页面之间(大小为:pagesize*2-99),版本 1 的 被映射部分是整个文件, 版本 2 的文件被映射部分是文件大小减去一个页面后的剩余部分, 不到一个页面大小(大小为:pagesize-99)。程序中试图访问每一个页面边界,两个版本都 试图在进程空间中映射 pagesize*3 的字节数。 版本 1 的输出结果如下: pagesize is 4096 access page 1 over access page 1 edge over, now begin to access page 2 access page 2 over access page 2 over access page 2 edge over, now begin to access page 3 Bus error 页面 //被映射文件在进程空间中覆盖了两个页面,此时,进程试图访问第三个 版本 2 的输出结果如下: pagesize is 4096 access page 1 over access page 1 edge over, now begin to access page 2 Bus error 页面 //被映射文件在进程空间中覆盖了一个页面,此时,进程试图访问第二个 结论:采用系统调用 mmap()实现进程间通信是很方便的,在应用层上接口非常简洁。内部 实现机制区涉及到了 linux 存储管理以及文件系统等方面的内容,可以参考一下相关重要数 据结构来加深理解。在本专题的后面部分,将介绍系统 v 共享内存的实现。 系统调用 mmap()通过映射一个普通文件实现共享内存。 系统 V 则是通过映射特殊文件系统 shm 中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系 统 shm 中的一个文件(这是通过 shmid_kernel 结构联系起来的),后面还将阐述。 1、系统 V 共享内存原理 进程间需要共享的数据被放在一个叫做 IPC 共享内存区域的地方,所有需要访问该共享区 域的进程都要把该共享区域映射到本进程的地址空间中去。系统 V 共享内存通过 shmget 获得或创建一个 IPC 共享内存区域,并返回相应的标识符。 内核在保证 shmget 获得或创建 一个共享内存区,初始化该共享内存区相应的 shmid_kernel 结构注同时,还将在特殊文件 系统 shm 中,创建并打开一个同名文件,并在内存中建立起该文件的相应 dentry 及 inode 结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一 切都是系统调用 shmget 完成的。 注:每一个共享内存区都有一个控制结构 struct shmid_kernel,shmid_kernel 是共享内存 区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下: struct shmid_kernel /* private to the kernel */ { struct kern_ipc_perm struct file * int unsigned long unsigned long time_t time_t time_t pid_t pid_t }; shm_perm; shm_file; id; shm_nattch; shm_segsz; shm_atim; shm_dtim; shm_ctim; shm_cprid; shm_lprid; 该结构中最重要的一个域应该是 shm_file, 它存储了将被映射文件的地址。 每个共享内存区 对象都对应特殊文件系统 shm 中的一个文件,一般情况下,特殊文件系统 shm 中的文件是 不能用 read()、write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址 空间后,可直接采用访问内存的方式对其访问。 这里我们采用[1]中的图表给出与系统 V 共享内存相关数据结构: 正如消息队列和信号灯一样,内核通过数据结构 struct ipc_ids shm_ids 维护系统中的所有 共享内存区域。上图中的 shm_ids.entries 变量指向一个 ipc_id 结构数组,而每个 ipc_id 结 构数组中有个指向 kern_ipc_perm 结构的指针。到这里读者应该很熟悉了,对于系统 V 共 享内存区来说,kern_ipc_perm 的宿主是 shmid_kernel 结构,shmid_kernel 是用来描述一 个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在 shmid_kernel 结构的 file 类型指针 shm_file 指向文件系统 shm 中相应的文件,这样,共享内存区域就与 shm 文件系统中的文件对应起来。 在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用 shmat()完成此项 功能。由于在调用 shmget()时,已经创建了文件系统 shm 中的一个同名文件与共享内存区 域相对应,因此,调用 shmat()的过程相当于映射文件系统 shm 中的同名文件过程,原理与 mmap()大同小异。 回页首 2、系统 V 共享内存 API 对于系统 V 共享内存,主要有以下几个 API:shmget()、shmat()、shmdt()及 shmctl()。 #include #include shmget()用来获得共享内存区域的 ID,如果不存在指定的共享区域就创建相应的区域。 shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享 区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。