本文档的Copyleft归wwwlkk所有,使用GPL发布,可以自由拷贝、转载,转载时请保持文档的完整性,严禁用于任何商业用途。
E-mail: [email protected]
来源: http://blog.csdn.net/wwwlkk
1. 文件映射的页框是磁盘文件高速缓存中的页框,内核线程pdflush会将页框中的内容回写进磁盘, 如果是私有映射类型,将会进行写时复制。而IPC共享内存映射的是一种特殊文件系统中的文件高速缓存,它没有相应的磁盘映像。
2. IPC共享内存只存在于内存中,系统重新启动,数据将会丢失。而文件共享映射会将数据写回磁盘。
3. IPC共享内存的大小是在创建的时候指定,而且大小不能改变,而文件在创建时大小为0,此时还不能建立映射,文件的大小会间接的决定映射区的大小。例如文件的大小是123,而要求映射的区域大小是4096*2,但实际只会分配4096的映射空间,此时引用4096以后的线性空间将引起缺页异常。
4. 当第一次读取共享内存时IPC共享内存对象将分配一个新的页框,而文件映射分配新页框的同时会将磁盘中的数据写入新页框。
5. IPC共享内存不需要写回磁盘操作,完全是为共享内存而设计,所以使用效率会更高。
6. IPC共享内存对象必须调用shmctl()显示的撤销,否则会一直保留着,使用key或者id号定位一个共享内存对象,key和id号的对应关系并不是固定的。例如,第一次使用key建立一个共享内存对象为shm1对应的id为id1,之后系统重新启动,然后再使用key建立一个共享内存对象shm2,对应的id是id2,此时id2和id1是不同的。而文件映射使用相同的路径将会定位相同的磁盘文件。
总结:IPC共享内存和文件映射的实现机制是一样的,文件映射的目的是加快对文件的读写速度,而IPC共享内存就是为了共享内存而设计的,所以效率会高一些。
1. 建立一个线性区对象struct vm_area_struct 并加入进程的内存描述符current->mm中。
函数mmap()和shmat()就是用于建立并注册线性区对象,这个对象中的struct file *vm_file指向映射文件的文件对象,vm_page_prot是线性区中页框的访问许可权。但此时并未修改进程的页表,而是注册相应的缺页异常回调函数,注册在对象的vm_ops。
2. 当进程第一次访问共享内存区时,由于相应的页表还未填写,将产生缺页异常,并根据线性地址找到对应的线性区对象,然后调用前边注册过的缺页异常回调函数,并根据vm_file文件对象和vm_page_prot的信息来填写相应的页表项,最后重新执行产生缺页异常的代码。
说明:文件映射和IPC共享内存映射的物理页框都是磁盘文件的页高速缓存中的,IPC共享内存使用一种特殊文件系统,这个文件系统并没有对应的磁盘映像,只是复用了文件系统的框架。更详细的内容参见后边的五,六,七节。
下面3,4节是《UNIX环境高级编程》对文件映射和IPC共享内存的讲解,已经说明的很详细了,我在它的基础上附加了一些内核实现原理的说明,实现原理说明部分放在括号内。
存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。
为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的。
#include <sys/types.h>
#include <sys/mman.h>
void * mmap(void addr, size_t len, int prot, int flag, int fd, off_t off) ;
返回:若成功则为映射区的起始地址,若出错则为- 1
addr参数用于指定映射存储区的起始地址。通常将其设置为0,这表示由系统选择该映射区的起始地址。此函数的返回地址是:该映射区的起始地址。fd指定要被映射文件的描述符(fd用于定位是哪个磁盘文件的页高速缓存)。在映射该文件到一个地址空间之前,先要打开该文件。len是映射的字节数。off是要映射字节在文件中的起始位移量(下面将说明对off值有某些限制)。
在说明其余参数之前,先看一下存储映射文件的基本情况。图12 - 12显示了一个存储映射文件。
在此图中,“起始地址”是mmap的返回值。在图中,映射存储区位于堆和栈之间:这属于实现
细节,各种实现之间可能不同。
prot参数说明映射存储区的保护要求。见表12 - 8。
对于映射存储区所指定的保护要求与文件的open方法匹配。例如,若该文件是只读打开的,那么对映射存储区就不能指定PROT _WRITE。(对存储映射区的保护是通过设置页表项的保护标志来实现的,如果页表项的read/write标志位为0,说明页是只读的,如果进程试图修改页的内容,将产生段错误,这些保护方案都是由CPU硬件控制的)
flag参数影响映射存储区的多种属性:
• MAP_FIXED 返回值必须等于addr。因为这不利于可移植性,所以不鼓励使用此标志。如果未指定此标志,而且addr非0,则内核只把addr视为何处设置映射区的一种建议。通过将addr指定为0可获得最大可移植性。
• MAP_SHARED 这一标志说明了本进程对映射区所进行的存储操作的配置。此标志指定存储操作修改映射文件—也就是,存储操作相当于对该文件write。(这里映射的页是包含在文件的页高速缓存中,用户态进程在读写磁盘的时候,内核先在页高速缓存中增加一个新页,将所请求的磁盘块写入新页,用户态进程从页高速缓存中取出数据。如果要写入数据,也是要添加一个页将磁盘中的数据写入该页,然后再将数据写入该页,内核会在一定的时机对磁盘进行更新。)
(以上的页高速缓存是组织在inode的i_mmaping对象中,对于一个磁盘文件唯一对应一个磁盘inode,每个磁盘inode也唯一对应一个内核inode,也就是,每个磁盘文件只有一个页高速缓存,如果两个进程映射的是同一个文件的页高数缓存,则它们共享相同的物理页)
• MAP_PRIVATE 本标志说明,对映射区的存储操作导致创建该映射文件的一个副本。所有后来对该映射区的存访都是存访该副本,而不是原始文件。(这里内核用到了写时复制技术,在相应的页表项中设置写时复制标志,当进程试图修改该页,内核将会产生缺页异常,内核就把该页框进行复制,并在进程页表中用复制的页来替换原来的页框,显然这个新的页框已经不在页高速缓存中了,对页框的内容进行修改将不会写回文件,其它进程将无法共享这个页框。如果本进程还未进行写复制,而其它进程修改了页的内容,本进程是可以获得更新后的数据)
因为映射文件的起动位移量受系统虚存页长度的限制,那么如果映射区的长度不是页长度的整数倍时,将如何呢?假定文件长12字节,系统页长为512字节,则系统通常提供512字节的映射区,其中后500字节被设0。可以修改这50字节,但任何变动都不会在文件中反映出来。(这是由于内核分配线性区和分配物理内存都是以页为单位)
与映射存储区相关有两个信号: SIGSEGV和SIGBUS。信号SIGSEGV通常用于指示进程试图存取它不能存取的存储区。如果进程企图存数据到用mmap指定为只读的映射存储区,那么也产生此信号。如果存取映射区的某个部分,而在存取时这一部分已不存在,则产生SIGBUS信号。例如,用文件长度映射一个文件,但在存访该映射区之前,另一个进程已将该文件截短。此时,如果进程企图存取对应于该文件尾端部分的映射区,则接收到SIGBUS信号。(对信号的实现机制有待进一步分析)
在fork之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是该地址空间中的一部分),但是由于同样的理由,exec后的新程序则不继承此存储映射区。(关闭文件描述符也不影响存储映射区,磁盘文件的页高速缓存并不会因为进程的撤销而撤销,如果有足够的空闲内存,页高速缓存中的页将长期存在,使其它进程再使用该页时不再访问磁盘。)
进程终止时,或调用了munmap之后,存储映射区就被自动去除。关闭文件描述符fd并不解除映射区。(关闭存储映射区,只是撤销进程页表中的相应目录项,并不影响页高速缓存。)
#include <sys/types.h>
#include <sys/mman.h>
int munmap(void addr,size_t len) ;
返回:若成功则为0,若出错则为- 1
munmap 并不影响被映射的对象—也就是说,调用munmap并不使映射区的内容写到磁盘文件上。对于MAP_SHARED区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。(pdflush内核线程用于刷新脏页)
例子程序:
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(int argc, char *argv[])
{
int fd, i, counter;
pid_t pid;
char *area = NULL;
if((fd = open("test", O_RDWR) )<= 0)
printf("open error/n");
area = (char *)mmap(0, sizeof(long), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
printf("area:%p/n", area);
close(fd);
*(area + 1) = 'c';
}
msync函数的使用
原型:
#include <sys/mman.h>
int msync(const void *start, size_t length, int flags);
msync函数用来把映像的文件写入磁盘。调用msync可以用对内存中的映像的更新写入一个被映像的文件,被强行写入到磁盘的内存取从start指定的地址开始,写入length个字节的数据。
flags可以是下面的一个值或多个的逻辑“或”:
1、MS_ASYNC 调度一次写入操作然后返回
2、MS_SYNC 在msync返回前写入数据
3、MS_INVALIDATE 让映像到同一文件的映像无效,以便用新数据更新它们
(MS_INVALIDATE的作用是使映射的页高速缓存中的内容无效,重新从磁盘写入数据到映射的页高速缓存。
可以使用MS_INVALIDATE来测试内核是否进行页高速缓存数据的回写磁盘操作,测试过程:写一个字符到映射区,然后使用MS_INVALIDATE使映射区的数据失效,并从磁盘写入数据,从测试结果看字符会被写入磁盘,也就是说内核几乎在对映射区进行写入操作的同时就进行了回写磁盘操作)
mprotect函数的使用
mprotect函数修改在内存映像上的保护模式。
函数原型:
#include <unistd.h>
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);
mprotect把自start开始的内存区的保护模式修改为prot指定的值,如果执行成功返回0,如果执行失败,mprotect返回-1,并且设置errno变量。
port可以是PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)、PROT_NONE(不可访问)中的一个或多个
(这里只是修改本进程页表中的访问控制标志,并不涉及物理页,对其它进程没有影响)
锁定内存
原型:
#include <sys/mman.h>
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指出被加锁或解锁的内存区,len指出加锁或解锁的内存区大小。flags的值可以是MCL_CURRENT和MCL_FUTURE之一或者两个都有。MCL_CURRENT在调用返回前请求锁住所有内存页面,MCL_FUTURE指出锁住所有增加到进程的地址空间的内存页面。
(mlock的操作结果是vma的属性,VM_LOCKED. 作用是被lock的内存不参加swap,保证一直存在于内存中.内核中具体的策略执行可以看函数 swap_out_vma.当使用exec的时候,lock失效. fork的子进程也不继承此属性.
对于实时进程和安全程序,此调用很有意义.对于加密程序,密码不会被dump到磁盘上. 但是lock并不能阻止系统休眠的时候内存被存储到磁盘.
mlock,munlock的操作不可堆叠. 多次调用mlock的一段内存也会被一次unlock操作解锁.
mlock/munlock指定的地址会被round down到一个page的边界.)
mremap函数的使用
mremap函数用于改变一个被映像的文件大小。
原型:
#include <sys/mman.h>
void *mremap(void *old_addr, size_t old_len, size_t new_len, unsigned long flags);
mremap用指定的flags把地址在old_addr的内存映像大小从old_len调整为new_len,flags如果为MREMAP_MAYMOVE则调整此内存映像的地址。成功返回新地址,失败返回NULL。
(内核要做事情是:改变线性区对象的长度。
内核会检查是否可以直接扩大或者缩小线性区的大小,如果线性对象相邻的线性空间已经被使用了,此时将没法扩大了,如果此时设置了MREMAP_MAYMOVE标志,将会重新分配一块新的线性空间,显然这个空间的起始地址已经改变)
IPC共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种IPC。使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。若服务器将数据放入共享存储区,则在服务器做完这一操作之前,客户机不应当去取这些数据。通常,信号量被用来实现对共享存储存取的同步。
调用的第一个函数通常是shmget,它获得一个共享存储标识符。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
如果参数key为 IPC_PRIVATE。则会建立新的共享内存对象其大小由size(单位字节Byte)指定,如果key不为IPC_PRIVATE,并且存在键值为key的共享内存对象,则返回所关联的id号,如果不存在键值为key的共享内存对象,那么系统会视参数shmflg是否有IPC_CREAT来决定是否新建一个共享内存对象。(每个共享内存对象都对应一个目录对象和一个inode对象,每个inode对象都包含一个address_space i_mapping对象,但是这些对象并没有磁盘映像,而是为了可以重复利用文件映射中提供的代码,每个进程映射的物理页就存储在i_mapping对象里。)
(以上是一种特殊的文件系统,它并没有挂载在某个目录下,只是为了方便实现共享内存)
size是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。实际的大小是((bytes进位到4096整数倍)/4096 + 4) * 4096。(因为线性区和物理内存的分配都是以页为单位)
shmflg主要和一些标志有关,其中有效的包括IPC_CREAT和IPC_EXCL,它们的功能与open()的O_CREAT和O_EXCL相当。
IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。
IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
如果单独使用IPC_CREAT,shmget()函数要么返回一个已经存在的共享内存的操作符,要么返回一个新建的共享内存的标识符。如果将IPC_CREAT和IPC_EXCL标志一起使用,shmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,或者返回-1。IPC_EXEL标志本身并没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。
返回值
成功返回共享内存的标识符;不成功返回-1,errno储存错误原因。
EINVAL 参数size小于SHMMIN或大于SHMMAX。
EEXIST 预建立key所致的共享内存,但已经存在。
EIDRM 参数key所致的共享内存已经删除。
ENOSPC 超过了系统允许建立的共享内存的最大值(SHMALL )。
ENOENT 参数key所指的共享内存不存在,参数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; /* 最后一次附加这个共享内存的时间 */
__kernel_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 DIPC */
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; //建立共享内对象的组识别码
__kernel_mode_t mode; //这个共享内存的读写权限(可以修改)
unsigned short seq; //序号
};
shmctl函数对共享存储段执行多种操作。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds * buf) ;
返回:若成功则为0,若出错则为- 1
cmd参数指定下列5种命令中一种,使其在shmid指定的段上执行。
• IPC_STAT 对此段取shmid_ds结构,并存放在由buf指向的结构中。
• IPC_SET按buf指向的结构中的值设置与此段相关结构中的下列三个字段:(只能修改这3个字段)shm_perm.uid、shm_perm.gid以及shm_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.cuid或shm_perm.uid的进程;另一种是具有超级用户特权的进程。
• IPC_RMID 从系统中删除该共享存储段。因为每个共享存储段有一个连接计数(shm_nattch在shmid_ds结构中),所以除非使用该段的最后一个进程终止或与该段脱接,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除,所以不能再用shmat与该段连接。此命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.cuid或shm_perm.uid的进程;另一种是具有超级用户特权的进程。
• SHM_LOCK锁住共享存储段。此命令只能由超级用户执行。
• SHM_UNLOCK解锁共享存储段。此命令只能由超级用户执行。
一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, void *addr, int flag) ;
返回:若成功则为指向共享存储段的指针,若出错则为- 1。
共享存储段连接到调用进程的哪个地址上与addr参数以及在flag中是否指定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的倍数。
除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不用指定共享段所连接到的地址。所以一般应指定addr为0,以便由内核选择地址。
如果在f l a g中指定了SHM_RDONLY位,则以只读方式连接此段。否则以读写方式连接此段。(在进程页表项中设置只读标志,试图修改该页时将产生缺页异常,这些都是由CPU的页寻址硬件控制的)
shmat的返回值是该段所连接的实际地址,如果出错则返回-1。
当对共享存储段的操作已经结束时,则调用shmdt脱接该段。注意,这并不从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用shmctl(带命令IP C_RMID)特地删除它。(连接是进程将共享内存的物理页加入进程页表,脱离是从页表中撤销该物理页的信息,并不改变实际的物理页)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(void * addr);
返回:若成功则为0,若出错则为- 1
addr参数是以前调用shmat时的返回值。
下面是和IPC共享内存有关的内核参数,可以修改的。
其中shmall是所有共享内存段可以使用的最大页个数
shmmni是一个共享内存段的最小字节数
shmmax是一个共享内存段的最大字节数
下面是显示系统中已近建立的共享内存对象
其中bytes是申请共享内存时使用的大小参数,实际的大小是((bytes进位到4096整数倍)/4096 + 4) * 4096。
nattch是附加此共享内存对象的进程数。
例子程序:
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#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: %d/n", shmid);
shmaddr = (char *)shmat(shmid, NULL, 0);
*(shmaddr + 4095*3) = 'c';
shmdt(shmaddr);
}
使用ipcs –m 查看建立的共享内存对象:
要理解IPC共享内存和文件映射的实现机制,先要理解什么是共享内存,共享内存实现的基本原理是什么。
CPU要访问某块内存,必须要获得内存的物理地址。
CPU集成有寻址硬件,会根据机器语言指令中提供的地址,执行地址转换,获得的物理地址。
CPU有两种转换模式:1.实模式 2.保护模式。
实模式下的物理地址 = 线性地址。
保护模式下的物理地址 = 线性地址通过分页机制转化为物理地址。
启动保护模式:把CPU控制寄存器CR0中的最高位置1。
CPU保护模式寻址方式:
图1 分页机制寻址
说明:CR3控制寄存器的值是物理地址。由于寻找的是页框的物理地址,所以CR3,页目录和页表中存储的物理地址后12位都为0。也就是这12位的空间不存储物理地址,而是用于访问控制(可读/可写/CPU特权级别),指示对应的页是否存在等作用。
在转换过程中,出现以下情况之一将会引起也异常:
(1) 涉及的页目录表内的表项或页表内的表项中的P=0,即涉及到的页不在内存;
(2) 违反也保护属性的规定而对页进行访问。
注意:从2.6.11版本开始,采用了四级分页模型,但基本原理是一样的。
每个进程的CR3的值是不同,进程切换时保存或者恢复CR3的值。只要设置了CR3的值CPU将自动进行寻址。
要将某个物理页加入进程的地址空间,要做的事情就是将物理页的物理地址填写进程页目录表内的表项和页表内的表项。
共享内存实现原理就是:将相同的物理页加入不同进程的地址空间。
显然进程中要加入一块物理页,就必须对应一块线性空间,于是就要先申请一块线性空间,然后根据这块线性空间填写页目录表内的表项和页表内的表项。(这里可以说明为什么不同进程中的不同线性地址可以对应相同的物理地址)。
由于linux会先分配线性空间,页表的修改会推后进行。Linux通过vm_area_struct对象实现线性区,当产生缺页异常时,会根据vm_area_struct对象来修改页表,然后重新执行产生缺页异常的代码。
总的来说,内核实际要做的事情是很多的,但是,内核也提供了很多接口,所以我们要做的事情还是比较少的。
以上就是共享内存实现的基本原理,下面分析一下IPC共享内存和内存映射实现机制。实际上IPC共享内存的实现是基于内存映射,原因是:内存映射提供了一些接口,基于内存映射来实现IPC共享内存可以复用这些代码。但是,两者最终的实现原理还是修改页表。
先看一下内核是如何组织共享内存对象的,如图2所示:
图2 共享内存对象组织
其中struct shmid_kernel就是一个共享内存对象,使用id radix tree来组织所有的共享内存对象。使用id号查找一个共享内存对象。
我们现在最关心的问题是:如何根据struct shmid_kernel结构获得对象所拥有的物理内存。
是根据file->f_path.dentry->d_inode
struct address_space *mapping = inode->i_mapping
mapping存储着共享内存拥有的物理页,如图3所示:
图3 共享内存物理页存储方式
其中page_tree用于存储物理页,每个节点的值类型是struct page *
每个共享内存对象对应一个inode对象,这个对象是被多个进程共享,也就是说,进程是通过inode对象获得物理页。这里借用了文件映射的框架,i_mmaping对象也就是文件的页高速缓存。
进程映射共享内存区域的过程:
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. 当第一次访问共享内存块时,由于相应的页表项还未填写,将产生缺页异常,内核根据产生异常的线性地址找到对应的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 << PAGE_CACHE_SHIFT) >= i_size_read(inode))
return VM_FAULT_SIGBUS;
error = shmem_getpage(inode, vmf->pgoff, &vmf->page, SGP_CACHE, &ret);
if (error)
return ((error == -ENOMEM) ? VM_FAULT_OOM : VM_FAULT_SIGBUS);
return ret | VM_FAULT_LOCKED;
}
本函数的功能是:根据产生缺页异常的线性地址找到对应的物理页,并将这个物理页加入页表。
以上说明的也就是内存映射实现机制,现在做个总结:
一个磁盘文件唯一对应一个磁盘inode,每个磁盘inode也唯一对应一个内核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 = file->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_mapping;获得物理页。
从以下函数看出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结构,通过inode->i_mapping来访问。address_space结构中维护了一棵radix树,用于磁盘高速缓存的内存页面就挂在这棵树上。打开这个文件的每个进程都共用同一份页高速缓存。
如果是私有映射类型则相应的页表项会设置写时复制,也就是,当进程试图修改一个私有映射内存的页时,内核将会产生缺页异常,内核就把该页框进行复制,并在进程页表中用复制的页来替换原来的页框,显然这个新的页框已经不在页高速缓存中了,对页框的内容进行修改将不会写回文件,其它进程将无法共享这个页框。
pdflush内核线程用于刷新脏页。
注意:如果还没修改私有映射内存的页,也就是还未进行写时复制,则对应的页框还是页高速缓存中的页,如果其他进程修改了这个页框的数据,本进程还是可以读取修改后的数据。