《Linux共享内存实例及文件映射编程及实现原理.rtf》由会员分享,可在线阅读,更多相关《Linux共享内存实例及文件映射编程及实现原理.rtf(11页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、目录(一)IPC 共享内存和文件映射的区别1(二)共享内存实现流程总结1(三)存储映射 I/O(包含实现原理说明)2文件映射 API 补充4(四)IPC 共享存储(包含实现原理说明)6(五)共享内存实现基本原理10(六)IPC 共享内存实现机制11(七)文件映射的实现机制13(一)IPC 共享内存和文件映射的区别1.文件映射的页框是磁盘文件高速缓存中的页框,内核线程 pdflush 会将页框中的内容回写进磁盘,如果是私有映射类型,将会进行写时复制。而 IPC 共享内存映射的是一种特殊文件系统中的文件高速缓存,它没有相应的磁盘映像。2.IPC 共享内存只存在于内存中,系统重新启动,数据将会丢失。
2、而文件共享映射会将数据写回磁盘。3.IPC 共享内存的大小是在创建的时候指定,而且大小不能改变,而文件在创建时大小为 0,此时还不能建立映射,文件的大小会间接的决定映射区的大小。例如文件的大小是123,而要求映射的区域大小是 4096*2,但实际只会分配 4096 的映射空间,此时引用 4096以后的线性空间将引起缺页异常。4.当第一次读取共享内存时 IPC 共享内存对象将分配一个新的页框,而文件映射分配新页框的同时会将磁盘中的数据写入新页框。5.IPC 共享内存不需要写回磁盘操作,完全是为共享内存而设计,所以使用效率会更高。6.IPC 共享内存对象必须调用 shmctl()显示的撤销,否则会
3、一直保留着,使用 key 或者id 号定位一个共享内存对象,key 和 id 号的对应关系并不是固定的。例如,第一次使用 key建立一个共享内存对象为 shm1 对应的 id 为 id1,之后系统重新启动,然后再使用 key 建立一个共享内存对象 shm2,对应的 id 是 id2,此时 id2 和 id1 是不同的。而文件映射使用相同的路径将会定位相同的磁盘文件。总结:IPC 共享内存和文件映射的实现机制是一样的,文件映射的目的是加快对文件的读写速度,而 IPC 共享内存就是为了共享内存而设计的,所以效率会高一些。(二)共享内存实现流程总结1.建立一个线性区对象 struct vm_area
4、_struct 并加入进程的内存描述符 current-mm中。函数 mmap()和 shmat()就是用于建立并注册线性区对象,这个对象中的 struct file*vm_file 指向映射文件的文件对象,vm_page_prot 是线性区中页框的访问许可权。但此时并未修改进程的页表,而是注册相应的缺页异常回调函数,注册在对象的 vm_ops。2.当进程第一次访问共享内存区时,由于相应的页表还未填写,将产生缺页异常,并根据线性地址找到对应的线性区对象,然后调用前边注册过的缺页异常回调函数,并根据vm_file 文件对象和 vm_page_prot 的信息来填写相应的页表项,最后重新执行产生缺
5、页异常的代码。说明:文件映射和 IPC 共享内存映射的物理页框都是磁盘文件的页高速缓存中的,IPC共享内存使用一种特殊文件系统,这个文件系统并没有对应的磁盘映像,只是复用了文件系统的框架。更详细的内容参见后边的五,六,七节。下面 3,4 节是UNIX 环境高级编程对文件映射和 IPC 共享内存的讲解,已经说明的很详细了,我在它的基础上附加了一些内核实现原理的说明,实现原理说明部分放在括号内。(三)存储映射 I/O(包含实现原理说明)存储映射 I/O 使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件
6、。这样,就可以在不使用 read 和 write 的情况下执行 I/O。为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap 函数实现的。#include#include void*mmap(void addr,size_t len,int prot,int flag,int fd,off_t off);返回:若成功则为映射区的起始地址,若出错则为-1addr 参数用于指定映射存储区的起始地址。通常将其设置为 0,这表示由系统选择该映射区的起始地址。此函数的返回地址是:该映射区的起始地址。fd 指定要被映射文件的描述符(fd 用于定位是哪个磁盘文件的页高速缓存)
7、。在映射该文件到一个地址空间之前,先要打开该文件。len 是映射的字节数。off 是要映射字节在文件中的起始位移量(下面将说明对off 值有某些限制)。在说明其余参数之前,先看一下存储映射文件的基本情况。图 12-12 显示了一个存储映射文件。在此图中,“起始地址”是 mmap 的返回值。在图中,映射存储区位于堆和栈之间:这属于实现细节,各种实现之间可能不同。prot 参数说明映射存储区的保护要求。见表 12-8。对于映射存储区所指定的保护要求与文件的 open 方法匹配。例如,若该文件是只读打开的,那么对映射存储区就不能指定 PROT _WRITE。(对存储映射区的保护是通过设置页表项的保护
8、标志来实现的,如果页表项的 read/write 标志位为 0,说明页是只读的,如果进程试图修改页的内容,将产生段错误,这些保护方案都是由 CPU 硬件控制的)flag 参数影响映射存储区的多种属性:?MAP_FIXED 返回值必须等于 addr。因为这不利于可移植性,所以不鼓励使用此标志。如果未指定此标志,而且 addr 非 0,则内核只把 addr 视为何处设置映射区的一种建议。通过将 addr 指定为 0 可获得最大可移植性。?MAP_SHARED 这一标志说明了本进程对映射区所进行的存储操作的配置。此标志指定存储操作修改映射文件也就是,存储操作相当于对该文件 write。(这里映射的页
9、是包含在文件的页高速缓存中,用户态进程在读写磁盘的时候,内核先在页高速缓存中增加一个新页,将所请求的磁盘块写入新页,用户态进程从页高速缓存中取出数据。如果要写入数据,也是要添加一个页将磁盘中的数据写入该页,然后再将数据写入该页,内核会在一定的时机对磁盘进行更新。)(以上的页高速缓存是组织在 inode 的 i_mmaping 对象中,对于一个磁盘文件唯一对应一个磁盘 inode,每个磁盘 inode 也唯一对应一个内核 inode,也就是,每个磁盘文件只有一个页高速缓存,如果两个进程映射的是同一个文件的页高数缓存,则它们共享相同的物理页)?MAP_PRIVATE 本标志说明,对映射区的存储操作
10、导致创建该映射文件的一个副本。所有后来对该映射区的存访都是存访该副本,而不是原始文件。(这里内核用到了写时复制技术,在相应的页表项中设置写时复制标志,当进程试图修改该页,内核将会产生缺页异常,内核就把该页框进行复制,并在进程页表中用复制的页来替换原来的页框,显然这个新的页框已经不在页高速缓存中了,对页框的内容进行修改将不会写回文件,其它进程将无法共享这个页框。如果本进程还未进行写复制,而其它进程修改了页的内容,本进程是可以获得更新后的数据)因为映射文件的起动位移量受系统虚存页长度的限制,那么如果映射区的长度不是页长度的整数倍时,将如何呢?假定文件长 12 字节,系统页长为 512 字节,则系统
11、通常提供 512字节的映射区,其中后 500 字节被设 0。可以修改这 50 字节,但任何变动都不会在文件中反映出来。(这是由于内核分配线性区和分配物理内存都是以页为单位)与映射存储区相关有两个信号:SIGSEGV 和 SIGBUS。信号 SIGSEGV 通常用于指示进程试图存取它不能存取的存储区。如果进程企图存数据到用 mmap 指定为只读的映射存储区,那么也产生此信号。如果存取映射区的某个部分,而在存取时这一部分已不存在,则产生 SIGBUS信号。例如,用文件长度映射一个文件,但在存访该映射区之前,另一个进程已将该文件截短。此时,如果进程企图存取对应于该文件尾端部分的映射区,则接收到 SI
12、GBUS 信号。(对信号的实现机制有待进一步分析)在 fork 之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是该地址空间中的一部分),但是由于同样的理由,exec 后的新程序则不继承此存储映射区。(关闭文件描述符也不影响存储映射区,磁盘文件的页高速缓存并不会因为进程的撤销而撤销,如果有足够的空闲内存,页高速缓存中的页将长期存在,使其它进程再使用该页时不再访问磁盘。)进程终止时,或调用了 munmap 之后,存储映射区就被自动去除。关闭文件描述符 fd 并不解除映射区。(关闭存储映射区,只是撤销进程页表中的相应目录项,并不影响页高速缓存。)#include#includ
13、e int munmap(void addr,size_t len);返回:若成功则为 0,若出错则为-1munmap 并不影响被映射的对象也就是说,调用 munmap 并不使映射区的内容写到磁盘文件上。对于 MAP_SHARED 区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。(pdflush 内核线程用于刷新脏页)例子程序:#include#include#include#include#include#include int main(int argc,char*argv)int fd,i,counter;pid_t pid;char*area=NULL;if(fd=open
14、(test,O_RDWR)=0)printf(open errorn);area=(char*)mmap(0,sizeof(long),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);printf(area:%pn,area);close(fd);*(area+1)=c;文件映射 API 补充msync 函数的使用原型:?#include?int msync(const void*start,size_t length,int flags);msync 函数用来把映像的文件写入磁盘。调用 msync 可以用对内存中的映像的更新写入一个被映像的文件,被强行写入到磁盘的
15、内存取从 start 指定的地址开始,写入 length 个字节的数据。flags 可以是下面的一个值或多个的逻辑“或”:?1、MS_ASYNC 调度一次写入操作然后返回?2、MS_SYNC 在 msync 返回前写入数据?3、MS_INVALIDATE 让映像到同一文件的映像无效,以便用新数据更新它们(MS_INVALIDATE 的作用是使映射的页高速缓存中的内容无效,重新从磁盘写入数据到映射的页高速缓存。可以使用 MS_INVALIDATE 来测试内核是否进行页高速缓存数据的回写磁盘操作,测试过程:写一个字符到映射区,然后使用 MS_INVALIDATE 使映射区的数据失效,并从磁盘写入数
16、据,从测试结果看字符会被写入磁盘,也就是说内核几乎在对映射区进行写入操作的同时就进行了回写磁盘操作)mprotect 函数的使用 mprotect 函数修改在内存映像上的保护模式。函数原型:?#include?#include?int mprotect(const void*start,size_t len,int prot);mprotect 把自 start 开始的内存区的保护模式修改为 prot 指定的值,如果执行成功返回 0,如果执行失败,mprotect 返回-1,并且设置 errno 变量。port 可以是 PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXE
17、C(可执行)、PROT_NONE(不可访问)中的一个或多个(这里只是修改本进程页表中的访问控制标志,并不涉及物理页,对其它进程没有影响)锁定内存原型:?#include int mlock(const void*start,size_t len);?int munlock(void*start,size_t len);?int mlockall(int flags);?int munlockall(void);以 4 上个函数是对指定的内存映像加锁和解锁,其中 mlockall 的 flags包括 MCL_CURRENT 和 MCL_FUTURE。只有 root 权限才能使用它们。start
18、指出被加锁或解锁的内存区,len 指出加锁或解锁的内存区大小。flags 的值可以是 MCL_CURRENT 和 MCL_FUTURE之一或者两个都有。MCL_CURRENT 在调用返回前请求锁住所有内存页面,MCL_FUTURE 指出锁住所有增加到进程的地址空间的内存页面。(mlock 的操作结果是 vma 的属性,VM_LOCKED.作用是被 lock 的内存不参加 swap,保证一直存在于内存中.内核中具体的策略执行可以看函数 swap_out_vma.当使用 exec 的时候,lock 失效.fork 的子进程也不继承此属性.对于实时进程和安全程序,此调用很有意义.对于加密程序,密码不
19、会被 dump 到磁盘上.但是 lock 并不能阻止系统休眠的时候内存被存储到磁盘.mlock,munlock 的操作不可堆叠.多次调用 mlock 的一段内存也会被一次 unlock 操作解锁.mlock/munlock 指定的地址会被 round down 到一个 page 的边界.)mremap 函数的使用 mremap 函数用于改变一个被映像的文件大小。原型:?#include?void*mremap(void*old_addr,size_t old_len,size_t new_len,unsigned long flags);mremap 用指定的 flags 把地址在 old_a
20、ddr 的内存映像大小从 old_len 调整为new_len,flags 如果为 MREMAP_MAYMOVE 则调整此内存映像的地址。成功返回新地址,失败返回 NULL。(内核要做事情是:改变线性区对象的长度。内核会检查是否可以直接扩大或者缩小线性区的大小,如果线性对象相邻的线性空间已经被使用了,此时将没法扩大了,如果此时设置了 MREMAP_MAYMOVE 标志,将会重新分配一块新的线性空间,显然这个空间的起始地址已经改变)(四)IPC 共享存储(包含实现原理说明)IPC 共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种 IPC。使
21、用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。若服务器将数据放入共享存储区,则在服务器做完这一操作之前,客户机不应当去取这些数据。通常,信号量被用来实现对共享存储存取的同步。调用的第一个函数通常是 shmget,它获得一个共享存储标识符。#include#include int shmget(key_t key,size_t size,int shmflg);如果参数 key 为 IPC_PRIVATE。则会建立新的共享内存对象其大小由 size(单位字节Byte)指定,如果 key 不为 IPC_PRIVATE,并且存在键值为 key 的共享内存对象,则返回所关联的 id 号,
22、如果不存在键值为 key 的共享内存对象,那么系统会视参数 shmflg 是否有IPC_CREAT 来决定是否新建一个共享内存对象。(每个共享内存对象都对应一个目录对象和一个 inode 对象,每个 inode 对象都包含一个 address_space i_mapping 对象,但是这些对象并没有磁盘映像,而是为了可以重复利用文件映射中提供的代码,每个进程映射的物理页就存储在 i_mapping 对象里。)(以上是一种特殊的文件系统,它并没有挂载在某个目录下,只是为了方便实现共享内存)size 是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。实际的大小是((bytes 进位到 4
23、096 整数倍)/4096+4)*4096。(因为线性区和物理内存的分配都是以页为单位)shmflg 主要和一些标志有关,其中有效的包括 IPC_CREAT 和 IPC_EXCL,它们的功能与open()的 O_CREAT 和 O_EXCL 相当。IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。如果单独使用 IPC_CREAT,shmget()函数要么返回一个已经存在的共享内存的操作符,要么返回一个新建的共享内存的标识符。如果将 IPC_CREAT 和 IPC_EXCL 标志一起使用,s
24、hmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,或者返回-1。IPC_EXEL 标志本身并没有太大的意义,但是和 IPC_CREAT 标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。返回值成功返回共享内存的标识符;不成功返回-1,errno 储存错误原因。EINVAL 参数 size 小于 SHMMIN 或大于 SHMMAX。EEXIST 预建立 key 所致的共享内存,但已经存在。EIDRM 参数 key 所致的共享内存已经删除。ENOSPC 超过了系统允许建立的共享内存的最大值(SHMALL)。ENOENT 参数 key 所指的共享内存不存在,参数
25、shmflg 也未设 IPC_CREAT 位。EACCES 没有权限。ENOMEM 核心内存不足。struct shmid_ds shmid_ds 数据结构表示每个新建的共享内存。当 shmget()创建了一块新的共享内存后,返回一个可以用于引用该共享内存的 shmid_ds 数据结构的标识符。include/linux/shm.h struct shmid_ds struct ipc_perm shm_perm;/*operation perms*/int shm_segsz;/*共享内存的大小*/_kernel_time_t shm_atime;/*最后一次附加这个共享内存的时间*/_ke
26、rnel_time_t shm_dtime;/*最后一次分离这个共享内存的时间*/_kernel_time_t shm_ctime;/*最后一次改变这个共享内存结构的时间*/_kernel_ipc_pid_t shm_cpid;/*建立这个共享内存的进程识别码*/_kernel_ipc_pid_t shm_lpid;/*最后一个操作共享内存的进程识别码*/unsigned short shm_nattch;/*附加这个共享内存的进程个数*/unsigned short shm_unused;/*compatibility*/void *shm_unused2;/*ditto-used by D
27、IPC*/void *shm_unused3;/*unused*/;struct ipc_perm 对于每个 IPC 对象,系统共用一个 struct ipc_perm 的数据结构来存放权限信息,以确定一个 ipc 操作是否可以访问该 IPC 对象。struct ipc_perm _kernel_key_t key;/共享内存对象的 key _kernel_uid_t uid;/共享内存所属的用户识别码(可以修改)_kernel_gid_t gid;/共享内存所属的组识别码(可以修改)_kernel_uid_t cuid;/建立共享内对象的用户识别码 _kernel_gid_t cgid;/建
28、立共享内对象的组识别码 _kernel_mode_t mode;/这个共享内存的读写权限(可以修改)unsigned short seq;/序号;shmctl 函数对共享存储段执行多种操作。#include#include#include int shmctl(int shmid,int cmd,struct shmid_ds*buf);返回:若成功则为 0,若出错则为-1cmd 参数指定下列 5 种命令中一种,使其在 shmid 指定的段上执行。?IPC_STAT 对此段取 shmid_ds 结构,并存放在由 buf 指向的结构中。?IPC_SET 按 buf 指向的结构中的值设置与此段相关
29、结构中的下列三个字段:(只能修改这 3 个字段)shm_perm.uid、shm_perm.gid 以及 shm_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户 ID 等于 shm_perm.cuid 或 shm_perm.uid 的进程;另一种是具有超级用户特权的进程。?IPC_RMID 从系统中删除该共享存储段。因为每个共享存储段有一个连接计数(shm_nattch 在 shmid_ds 结构中),所以除非使用该段的最后一个进程终止或与该段脱接,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除,所以不能再用 shmat 与该段连接。此命令只能由下列
30、两种进程执行:一种是其有效用户 ID 等于shm_perm.cuid 或 shm_perm.uid 的进程;另一种是具有超级用户特权的进程。?SHM_LOCK 锁住共享存储段。此命令只能由超级用户执行。?SHM_UNLOCK 解锁共享存储段。此命令只能由超级用户执行。一旦创建了一个共享存储段,进程就可调用 shmat 将其连接到它的地址空间中。#include#include#include void*shmat(int shmid,void*addr,int flag);返回:若成功则为指向共享存储段的指针,若出错则为-1。共享存储段连接到调用进程的哪个地址上与 addr 参数以及在 fla
31、g 中是否指定 SHM_RND位有关。(1)如果 addr 为 0,则此段连接到由内核选择的第一个可用地址上。(2)如果 addr 非 0,并且没有指定 SHM_RND,则此段连接到 addr 所指定的地址上。(3)如果 addr 非 0,并且指定了 SHM_RND,则此段连接到(addr(addr mod SHMLBA))所表示的地址上。SHM_RND 命令的意思是:取整。SHMLBA 的意思是:低边界地址倍数,它总是 2 的乘方。该算式是将地址向下取最近 1 个 SHMLBA 的倍数。除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不用指定共享段所连接到的地址。所以一般应指
32、定 addr 为 0,以便由内核选择地址。如果在 f l a g 中指定了 SHM_RDONLY 位,则以只读方式连接此段。否则以读写方式连接此段。(在进程页表项中设置只读标志,试图修改该页时将产生缺页异常,这些都是由 CPU 的页寻址硬件控制的)shmat 的返回值是该段所连接的实际地址,如果出错则返回1。当对共享存储段的操作已经结束时,则调用 shmdt 脱接该段。注意,这并不从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用 shmctl(带命令 IP C_RMID)特地删除它。(连接是进程将共享内存的物理页加入进程页表,脱离是从页表中撤销该物理页的信
33、息,并不改变实际的物理页)#include#include#include int shmdt(void*addr);返回:若成功则为 0,若出错则为-1addr 参数是以前调用 shmat 时的返回值。下面是和 IPC 共享内存有关的内核参数,可以修改的。其中 shmall 是所有共享内存段可以使用的最大页个数shmmni 是一个共享内存段的最小字节数shmmax 是一个共享内存段的最大字节数下面是显示系统中已近建立的共享内存对象其中 bytes 是申请共享内存时使用的大小参数,实际的大小是((bytes 进位到 4096 整数倍)/4096+4)*4096。nattch 是附加此共享内存对
34、象的进程数。例子程序:#include#include#include#include#include#include#include#define KEY 4#define SIZE 4096*3 int main(int argc,char*argv)int shmid=0,ret=0;char*shmaddr=0;struct shmid_ds buf;shmid=shmget(KEY,SIZE,IPC_CREAT|SHM_R);printf(id:%dn,shmid);shmaddr=(char*)shmat(shmid,NULL,0);*(shmaddr+4095*3)=c;shmd
35、t(shmaddr);使用 ipcs m 查看建立的共享内存对象:要理解 IPC 共享内存和文件映射的实现机制,先要理解什么是共享内存,共享内存实现的基本原理是什么。(五)共享内存实现基本原理CPU 要访问某块内存,必须要获得内存的物理地址。CPU 集成有寻址硬件,会根据机器语言指令中提供的地址,执行地址转换,获得的物理地址。CPU 有两种转换模式:1.实模式 2.保护模式。实模式下的物理地址=线性地址。保护模式下的物理地址=线性地址通过分页机制转化为物理地址。启动保护模式:把 CPU 控制寄存器 CR0 中的最高位置 1。CPU 保护模式寻址方式:图 1 分页机制寻址说明:CR3 控制寄存器
36、的值是物理地址。由于寻找的是页框的物理地址,所以 CR3,页目录和页表中存储的物理地址后 12 位都为 0。也就是这 12 位的空间不存储物理地址,而是用于访问控制(可读/可写/CPU 特权级别),指示对应的页是否存在等作用。在转换过程中,出现以下情况之一将会引起也异常:涉及的页目录表内的表项或页表内的表项中的 P=0,即涉及到的页不在内存;违反也保护属性的规定而对页进行访问。注意:从 2.6.11 版本开始,采用了四级分页模型,但基本原理是一样的。每个进程的 CR3 的值是不同,进程切换时保存或者恢复 CR3 的值。只要设置了 CR3 的值CPU 将自动进行寻址。要将某个物理页加入进程的地址
37、空间,要做的事情就是将物理页的物理地址填写进程页目录表内的表项和页表内的表项。共享内存实现原理就是:将相同的物理页加入不同进程的地址空间。显然进程中要加入一块物理页,就必须对应一块线性空间,于是就要先申请一块线性空间,然后根据这块线性空间填写页目录表内的表项和页表内的表项。(这里可以说明为什么不同进程中的不同线性地址可以对应相同的物理地址)。由于 linux 会先分配线性空间,页表的修改会推后进行。Linux 通过 vm_area_struct 对象实现线性区,当产生缺页异常时,会根据 vm_area_struct 对象来修改页表,然后重新执行产生缺页异常的代码。总的来说,内核实际要做的事情是
38、很多的,但是,内核也提供了很多接口,所以我们要做的事情还是比较少的。以上就是共享内存实现的基本原理,下面分析一下 IPC 共享内存和内存映射实现机制。实际上 IPC 共享内存的实现是基于内存映射,原因是:内存映射提供了一些接口,基于内存映射来实现 IPC 共享内存可以复用这些代码。但是,两者最终的实现原理还是修改页表。(六)IPC 共享内存实现机制先看一下内核是如何组织共享内存对象的,如图 2 所示:图 2 共享内存对象组织其中 struct shmid_kernel 就是一个共享内存对象,使用 id radix tree 来组织所有的共享内存对象。使用 id 号查找一个共享内存对象。我们现在
39、最关心的问题是:如何根据 struct shmid_kernel 结构获得对象所拥有的物理内存。是根据 file-f_path.dentry-d_inodestruct address_space*mapping=inode-i_mappingmapping 存储着共享内存拥有的物理页,如图 3 所示:图 3 共享内存物理页存储方式其中 page_tree 用于存储物理页,每个节点的值类型是 struct page*每个共享内存对象对应一个 inode 对象,这个对象是被多个进程共享,也就是说,进程是通过 inode 对象获得物理页。这里借用了文件映射的框架,i_mmaping 对象也就是文件
40、的页高速缓存。进程映射共享内存区域的过程:1.需要申请一块线性地址空间,也就是生成一个 vm_area_struct 对象,并将对象加入到自己的地址空间,当此时并不修改进程页表,而是把 struct file 对象加入到 vm_area_struct对象中,执行以下代码:vma-vm_file=file;get_file(file);error=file-f_op-mmap(file,vma);注意:这里的 file 是根据 shm_file 生成的一个新的对象,相当于 shm_file 的复制。2.当第一次访问共享内存块时,由于相应的页表项还未填写,将产生缺页异常,内核根据产生异常的线性地址
41、找到对应的 vm_area_struct 对象,最后将执行以下函数:static int shmem_fault(struct vm_area_struct*vma,struct vm_fault*vmf)struct inode*inode=vma-vm_file-f_path.dentry-d_inode;int error;int ret;if(loff_t)vmf-pgoff=i_size_read(inode)return VM_FAULT_SIGBUS;error=shmem_getpage(inode,vmf-pgoff,&vmf-page,SGP_CACHE,&ret);if(
42、error)return(error=-ENOMEM)?VM_FAULT_OOM:VM_FAULT_SIGBUS);return ret|VM_FAULT_LOCKED;本函数的功能是:根据产生缺页异常的线性地址找到对应的物理页,并将这个物理页加入页表。以上说明的也就是内存映射实现机制,现在做个总结:调用 do_mmap()函数生成并注册一个线性对象,同时也注册缺页异常回掉函数。当第一次访问共享内存空间时,产生缺页异常,将会调用先前注册的回调函数,在回调函数中将相应的物理页加入进程的页表。(七)文件映射的实现机制一个磁盘文件唯一对应一个磁盘 inode,每个磁盘 inode 也唯一对应一个内核
43、 inode,每个内核 inode 都包含一个页高速缓存对象 struct address_space i_mmap,建立一个文件映射的步骤如下:第一步和 IPC 共享内存一样还是生成并注册一个线性对象,同时也注册缺页异常回掉函数。第二步当产生缺页异常时会执行先前注册的回调函数,在回调函数中将请求的物理页的物理地址写入进程页表项,并重新执行产生缺页异常的代码。下面以 EXT3 文件系统为例子:注册回调函数:int generic_file_mmap(struct file*file,struct vm_area_struct*vma)struct address_space*mapping=f
44、ile-f_mapping;if(!mapping-a_ops-readpage)return-ENOEXEC;file_accessed(file);vma-vm_ops=&generic_file_vm_ops;vma-vm_flags|=VM_CAN_NONLINEAR;return 0;const struct vm_operations_struct generic_file_vm_ops=.fault=filemap_fault,;filemap_fault()回调函数被多种文件系统使用。在函数内总是从 struct address_space*mapping=file-f_map
45、ping;获得物理页。从以下函数看出 file-f_mapping 指向 inode 的 i_mapping 对象。struct file*alloc_file(struct path*path,fmode_t mode,const struct file_operations*fop)。file-f_path=*path;file-f_mapping=path-dentry-d_inode-i_mapping;。每个磁盘文件都唯一对应一个内核 inode 对象,如果多个进程同时打开同一个磁盘文件,inode 对象将被多个进程共享。每个文件的 inode 包含了一个 address_space
46、 结构,通过inode-i_mapping 来访问。address_space 结构中维护了一棵 radix 树,用于磁盘高速缓存的内存页面就挂在这棵树上。打开这个文件的每个进程都共用同一份页高速缓存。如果是私有映射类型则相应的页表项会设置写时复制,也就是,当进程试图修改一个私有映射内存的页时,内核将会产生缺页异常,内核就把该页框进行复制,并在进程页表中用复制的页来替换原来的页框,显然这个新的页框已经不在页高速缓存中了,对页框的内容进行修改将不会写回文件,其它进程将无法共享这个页框。pdflush 内核线程用于刷新脏页。注意:如果还没修改私有映射内存的页,也就是还未进行写时复制,则对应的页框还是页高速缓存中的页,如果其他进程修改了这个页框的数据,本进程还是可以读取修改后的数据。