对操做系统中的各类缓存进行一下梳理:html
(一)高速缓冲存储器cachenode
一、cache的工做原理linux
高速缓冲存储器利用程序访问的局部性原理,把程序中正在使用的部分存放在一个高速的、容量较小的cache中,使CPU的访存操做大多数针对cache进行,从而使程序的执行速度大大提升。算法
当CPU发出读请求时,若是访存地址在cache中命中,就将此地址转换成cache地址,直接对cache进行读操做,与主存无关;若是cache不命中,则仍需访问主存,将此字所在的块一次从主存调入cache中。若此时cache已满,则需根据某种替换算法(如最久未使用算法(LRU)、先进先出算法(FIFO)、最近最少使用算法(LFU)、非最近使用算法(NMRU)等),用这个块替换掉cache中原来的某块信息。值得注意的是,CPU与cache之间的数据交换以字为单位,而cache和主存之间的数据交换则是以cache块为单位的。缓存
CPU对cache的写入更改了cache的内容,故需选择某种策略使得cache内容和主存内容保持一致,主要为:安全
全写法(cache-through):当CPU对cache写命中时,必须把数据同时写入cache和主存。当某一块须要替换时,没必要把这一块写回主存,将新调入的块直接覆盖便可。这种方法实现简单,能随时保持主存数据的正确性,缺点是增长了访存次数,下降了cache的效率。数据结构
写回法(cache-back):当CPU对cache写命中时,只修改cache的内容,而不当即写入主存,只有当此块被换出时才写回主存。这种方法减小了访存次数,但存在不一致的隐患。采用这种策略时,每一个cache行必须设置一个标志位(脏位),以反映此块是否被CPU修改过。架构
若是写不命中,还需考虑是否调块至cache的问题,非写分配法只写入内存,不进行调块,非写分配法一般与全写法合用。写分配法除了要写入主存外,还要将该块从主存调入至cache,一般与写回法合用。app
下图转载自: https://blog.csdn.net/wangwei222/article/details/79748597 框架
二、cache和主存的映射方式
(1)直接映射: 主存数据块只能装入cache中的惟一位置。若这个位置已有内容,则产生块冲突,原来的块被无条件地替换出去(无需使用替换算法)。直接映射实现简单,但不够灵活。直接映射的关系可定义为:
j= i mod c // j为cache块号,i为主存块号,c是cache中的总块数
下图转载自: https://www.cnblogs.com/yutingliuyl/p/6773684.html
(2)全相联映射:能够把主存数据块放入cache的任何位置,比较灵活,冲突几率低,但地址变换速度慢,实现成本高,查找时需所有遍历一遍。下图中左边为主存,右边为cache
(3)组相联映射:将cache分红大小相同的组,主存中的一个数据块能够装入到一个组内的任何一个位置,即组间采起直接映射,组内采用全相联,每组有多少块就称为几路组相联,可用下式表示:
j = i mod q // j为缓存块号, i为主存块号, q为cache组数,当q为1时为全相联映射, q为cache块数时为直接映射
图源: http://www.javashuo.com/article/p-eyooszcj-br.html
最后贴几个关于cache的层级:细说Cache-L1/L2/L3 为何CPU缓存会分为一级缓存L一、L二、L3 CPU缓存刷新
从L1到L3,容量愈来愈大,速度愈来愈慢,L1更加注重速度, L2和L3则更加注重节能和容量。
关于缓存行:CPU cache和缓存行 CPU cache
(二)地址转换后援缓冲器TLB
下述内容参考:http://www.javashuo.com/article/p-bhkumgza-cp.html
上述cache为在获得物理地址以后,对访问内存的一个加速,而系统虚拟地址须要经过页表转换为物理地址,页表通常都很大,而且存放在内存中,因此处理器引入MMU后,读取指令、数据须要访问两次内存:首先经过查询页表获得物理地址,而后访问该物理地址读取指令、数据。为了减小由于MMU致使的处理器性能降低,引入了TLB,TLB是Translation Lookaside Buffer的简称,可翻译为“地址转换后援缓冲器”,也可简称为“快表”。简单地说,TLB就是页表的Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。只有在TLB没法完成地址翻译任务时,才会到内存中查询页表(还有的说是查找时快表和慢表,即放在主存中的页表同时进行,若快表中有此逻辑页号,则将其对应的物理页号送入主存地址寄存器,并使慢表的查找做废),这样就减小了页表查询致使的处理器性能降低。
TLB中的项由两部分组成:标识和数据。标识中存放的是虚地址的一部分,而数据部分中存放物理页号、存储保护信息以及其余一些辅助信息。虚地址与TLB中项的映射方式有三种:全关联方式、直接映射方式、分组关联方式。直接映射方式是指每个虚拟地址只能映射到TLB中惟一的一个表项。假设内存页大小是8KB,TLB中有64项,采用直接映射方式时的TLB变换原理以下图所示:
由于页大小是8KB,因此虚拟地址的0-12bit做为页内地址偏移。TLB表有64项,因此虚拟地址的13-18bit做为TLB表项的索引。假如虚拟地址的13-18bit是1,那么就会查询TLB的第1项,从中取出标识,与虚拟地址的19-31位做比较,若是相等,表示TLB命中,反之,表示TLB失靶。TLB失靶时,能够由硬件将须要的页表项加载入TLB,也可由软件加载,具体取决于处理器设计。TLB命中时,此时翻译获得的物理地址就是TLB第1项中的标识(即物理地址13-31位)与虚拟地址0-12bit的结合。在地址翻译的过程当中还会结合TLB项中的辅助信息判断是否发生违反安全策略的状况,好比:要修改某一页,但该页是禁止修改的,此时就违反了安全策略,会触发异常。
TLB表项更新能够有TLB硬件自动发起,也能够有软件主动更新
1. TLB miss发生后,CPU从RAM获取页表项,会自动更新TLB表项
2. TLB中的表项在某些状况下是无效的,好比进程切换,更改内核页表等,此时CPU硬件不知道哪些TLB表项是无效的,只能由软件在这些场景下,刷新TLB。
在linux kernel软件层,提供了丰富的TLB表项刷新方法,可是不一样的体系结构提供的硬件接口不一样。好比x86_32仅提供了两种硬件接口来刷新TLB表项:
1. 向cr3寄存器写入值时,会致使处理器自动刷新非全局页的TLB表项
2. 在Pentium Pro之后,invlpg汇编指令用来使指定线性地址的单个TLB表项无效。
TLB内部存放的基本单位是TLB表项,TLB容量越大,所能存放的TLB表项就越多,TLB命中率就越高,可是TLB的容量是有限的。目前 Linux内核默认采用4KB大小的小页面,若是一个程序使用512个小页面,即2MB大小,那么至少须要512个TLB表项才能保证不会出现 TLB Miss的状况。可是若是使用2MB大小的大页,那么只须要一个TLB表项就能够保证不会出现 TLB Miss的状况。对于消耗内存以GB为单位的大型应用程序,还可使用以1GB为单位的大页,从而减小 TLB Miss的状况。
(三)页缓存和块缓存
上述缓存机制为所需数据已经在物理内存中,而页缓存和块缓存则是在进行磁盘io时使用的。
(1)页缓存:针对以页为单位的全部操做,页缓存实际上负责了块设备的大部分缓存工做。页缓存的任务在于得到一些物理内存页,以加速在块设备上按页为单位执行的操做。
(2)块缓存:以块为操做单位,在进行io操做时,存取的单位是设备的各个块而不是整个内存页,尽管也长度对全部文件系统是相同的,可是块长度取决于特定的文件系统或其设置,于是块缓存必须可以处理不一样长度的块。
一、页缓存
此部份内容来自:https://blog.csdn.net/gdj0001/article/details/80136364 及《深刻Linux内核架构》
页缓存是Linux内核一种重要的磁盘高速缓存,以页为大小进行数据缓存,它将磁盘中最经常使用和最重要的数据存放到部分物理内存中,使得系统访问块设备时能够直接从主存中获取块设备数据,而不需从磁盘中获取数据。在大多数状况下,内核在读写磁盘时都会使用页缓存。内核在读文件时,首先在已有的页缓存中查找所读取的数据是否已经存在。若是该页缓存不存在,则一个新的页将被添加到缓存中,而后用从磁盘读取的数据填充它。若是当前物理内存足够空闲,那么该页将长期保留在缓存中,使得其余进程再使用该页中的数据时再也不访问磁盘。写操做与读操做时相似,直接在页缓存中修改数据,可是页缓存中修改的数据(该页此时被称为Dirty Page)并非立刻就被写入磁盘,而是延迟几秒钟,以防止进程对该页缓存中的数据再次修改。
(1)管理和查找缓存的页:
Linux使用基数树来管理页缓存中包含的页,基数树是不平衡的,在树的不一样分支之间可能有任意数目的高度差。树自己由两种不一样的数据结构组成,叶子是page结构的实例,源代码中叶子使用的是void指针,意味着基数树还能够用于其余目的。树的结点具有两种搜索标记,两者用于指定给定页当前是不是脏的,或该页是否正在向底层块设备回写。标记不只对叶节点设置,还一直向上设置到根节点。若是某个层次n+1的结点设置了某个标记,其在层次n的父节点也会得到该标记。使内核能够判断,在某个范围内是否有一页或多页设置了某个标记位。
(2)回写修改的数据:
内核同时提供了以下几个同步方案:
一般,修改文件或其余按页缓存的对象时,只会修改页的一部分。为节省时间,内核在写操做期间,将缓存中的每一页划分为较小的单位,称为缓冲区。在同步数据时,内核能够将回写操做限制于那些实际发生了修改的较小单位上。
(3)地址空间address_space
为管理能够按整页处理和缓存的各类不一样对象,内核使用了address_space地址空间来抽象,将内存中的页和特定的块设备(或任何其余系统单元或系统单元的一部分)关联起来。每一个地址空间都有一个“宿主”做为其数据来源,大多数状况下宿主都是表示一个文件的iNode,由于全部现存的iNode都关联到其超级块,内核只须要扫描全部超级块的链表,并跟随相关的iNode便可得到被缓存页的列表。
地址空间实现了内存页面和后备存储器两个单元之间的一种转换机制:
内核中每一个存放数据的物理页帧都对应一个管理结构体struct page,以下:
struct page { /*flags描述page当前的状态和其余信息,如当前的page是不是脏页PG_dirty;是不是最新的已经同步到后备存储的页PG_uptodate; 是否处于lru链表上等*/ unsigned long flags; /*_count:引用计数,标识内核中引用该page的次数,若是要操做该page,引用计数会+1,操做完成以后-1。当该值为0时,表示没有引用该page的位置,因此该page能够被解除映射,这在内存回收的时候是有用的*/ atomic_t _count; /*mapcount:页表被映射的次数,也就是说page同时被多少个进程所共享,初始值为-1,若是只被一个进程的页表映射了,该值为0。
注意区分_count和_mapcount,_mapcount表示的是被映射的次数,而_count表示的是被使用的次数;被映射了不必定被使用,可是被使用以前确定要先被映射*/ atomic_t _mapcount; unsigned long private;//私有数据指针 /*_mapping有三种含义: a.若是mapping = 0,说明该page属于交换缓存(swap cache); 当须要地址空间时会指定交换分区的地址空间swapper_space; b.若是mapping != 0, bit[0] = 0, 说明该page属于页缓存或者文件映射,mapping指向文件的地址空间address_space; c.若是mapping != 0, bit[0] !=0 说明该page为匿名映射,mapping指向struct anon_vma对象;*/ struct address_space *mapping; pgoff_t index; //在页缓存中的索引 struct list_head lru;//当page被用户态使用或者是当作页缓存使用的时候,将该page连入zone中的lru链表,供内存回收使用 void* virtual; };
page结构体中index和mapping分别为在页缓存中的索引和指向页所述地址空间的指针,在系统须要判断一个页是否已经缓存时,使用函数find_get_page(), 该函数根据mapping和index进行查找。而页是属于文件的,文件中的位置是按字节偏移量指定的,而非页缓存中的偏移量,二者之间的转换经过以下实现:
index = ppos >> PAGE_CACHE_SHIFT //ppos是文件的字节偏移量,而index是页缓存中对应的偏移量
Linux使用基数树管理页缓存中的页,一个文件在内存中具备惟一的inode结构标识,inode结构中有该文件所属的设备及其标识符,于是,根据一个inode可以肯定其对应的后备设备。address_space将文件在物理内存中的页缓存和文件及其后备设备关联起来,能够说address_space结构体是将页缓存和文件系统关联起来的桥梁,其组成以下:
struct address_space { struct inode* host;/*指向与该address_space相关联的inode节点,inode节点与address_space之间是一一对应关系*/ struct radix_tree_root page_tree;/*全部页造成的基数树根节点*/ spinlock_t tree_lock;/*保护page_tree的自旋锁*/ unsigned int i_map_writable;/*VM_SHARED的计数*/ struct prio_tree_root i_mmap; struct list_head i_map_nonlinear; spinlock_t i_map_lock;/*保护i_map的自旋锁*/ atomic_t truncate_count;/*截断计数*/ unsigned long nrpages;/*页总数*/ pgoff_t writeback_index;/*回写的起始位置*/ struct address_space_operation* a_ops;/*操做表*/ unsigned long flags;/*gfp_mask掩码与错误标识*/ struct backing_dev_info* backing_dev_info;/*预读信息*/ spinlock_t private_lock;/*私有address_space锁*/ struct list_head private_list;/*私有address_space链表*/ struct address_space* assoc_mapping;/*相关的缓冲*/ }
一个文件inode对应一个地址空间address_space,而一个address_space对应一个页缓存基数树。address_space中host成员指向其全部者的iNode结点,page_tree为该inode结点对应文件的全部页的基数树,i_mmap为与该地址空间相关联的全部进程的虚拟地址区间vm_area_struct所对应的整个进程地址空间mm_struct造成的优先查找树的根节点;vm_area_struct中若是有后备存储,则存在prio_tree_node结构体,经过该prio_tree_node和prio_tree_root结构体,构成了全部与该address_space相关联的进程的一棵优先查找树,便于查找全部与该address_space相关联的进程,页缓存、文件和进程的关系以下:
二、关于mmap
说到页缓存再说一下mmap的问题,一直看到说mmap文件映射可实现像操做普通内存同样操做文件,这就让我想mmap映射的文件到底有没有映射进内存,答案应该是有的,用户进程应该不会直接操做磁盘上的文件数据,仍是须要将其拷贝至内存中。那么第二个问题就是,都说普通文件读写须要复制两次,而内存映射文件mmap只需复制一次,至于缘由,看到的解释不少是这样的:普通的读写文件为:进程调用read或是write后会陷入内核,由于这两个函数都是系统调用,进入系统调用后,内核开始读写文件,假设内核在读取文件,内核首先把文件读入本身的内核空间,读完以后进程在内核回归用户态,内核把读入内核内存的数据再copy进入进程的用户态内存空间。实际上咱们同一份文件内容至关于读了两次,先读入内核空间,再从内核空间读入用户空间。而mmap的做用是将进程的虚拟地址空间和文件在磁盘的位置作一一映射,作映射以后,读写文件虽然一样是调用read和write可是触发的机制已经不同了,mmap会返回来一个指针,指向进程逻辑地址空间的一个位置。这个时候的过程是这样的,首先read会改写为读内存操做,读内存的时候,系统发现该地址对应的物理内存是空的,触发缺页机制,而后再经过mmap创建的映射关系,从硬盘上将文件读入物理内存。也就是说mmap把文件直接映射到了用户空间,没有经历内核空间。
对于上述解释,以前一直在想普通的文件读写为何要把数据从内核空间拷贝至用户空间,而不直接拷贝到用户空间映射的物理页面,想说是系统调用在内核处理,内核很天然地就把数据拷贝到其对应的物理页面了,而后因为用户空间不能访问内核空间的数据(针对内核保护,因此用户空间的虚拟地址应该不能直接映射到内核地址所映射的物理页面),因此须要把数据拷贝至用户空间,那么mmap也在内核实现,为何能够直接把数据拷贝到用户空间映射的物理页面而不通过内核空间呢。
而后这两天看到了页缓存,又看到另外一个对mmap的解释:全部的文件内容的读取(不管一开始是命中页缓存仍是没有命中页缓存)最终都是直接来源于页缓存。当将数据从磁盘复制到页缓存以后,还要将页缓存的数据经过CPU复制到read调用提供的缓冲区中,这就是普通文件IO须要的两次复制数据复制过程。其中第一次是经过DMA的方式将数据从磁盘复制到页缓存中,本次过程只须要CPU在一开始的时候让出总线、结束以后处理DMA中断便可,中间不须要CPU的直接干预,CPU能够去作别的事情;第二次是将数据从页缓存复制到进程本身的的地址空间对应的物理内存中,这个过程当中须要CPU的全程干预,浪费CPU的时间和额外的物理内存空间。普通文件io后进程地址空间对应以下,图源水印:
而经过内存映射IO---mmap,进程不但能够直接操做文件对应的物理内存,减小从内核空间到用户空间的数据复制过程,同时能够和别的进程共享页缓存中的数据,达到节约内存的做用。当映射一个文件到内存中的时候,内核将虚拟地址直接映射到页缓存中。若是文件的内容不在物理内存中,操做系统不会将所映射的文件部分的所有内容直接拷贝到物理内存中,而是在使用虚拟地址访问物理内存的时候经过缺页异常将所须要的数据调入内存中。若是文件自己已经存在于页缓存中,则再也不经过磁盘IO调入内存。若是采用共享映射的方式,那么数据在内存中的布局以下图所示:
按照上述解释,全部文件内容读取都是来源于页缓存,那么mmap也就不是像第一种解释所说的将文件内容映射至用户空间对应的物理内存(其实要是说用户空间映射的物理内存就是页缓存的话也能够这么说),而是将文件内容拷贝至页缓存,而后用户空间虚拟地址直接映射到缓存页对应的物理地址。那么问题就是:既然均可以直接映射到页缓存了,为何普通的文件读写非要进行将数据从页缓存拷贝到用户空间映射的物理页面的过程呢,对此我认为是:因为页缓存的大小老是有限的,而其中缓存的通常而言都是最近读写的文件内容,那么若是全部文件io都映射到页缓存的话,缓存的换入换出确定十分频繁,这样的话就违背他做为缓存的原本目的了,所以须要将其拷贝至其余内存,即用户空间对应的物理页面中存放,这样下一次有新的文件读入时能够有位置将其加入缓存中,而以前用户空间读入的文件内容也存放在内存中,减小页缓存的换入换出。同时考虑到复制消耗的CPU及内存,又经过 mmap的方式提供用户空间对页缓存的直接映射。上述仅为我我的的理解与想法,若是看到这篇博客的人有不一样的想法,还请告知。
今天看了源码和使用以后,从使用的角度进行了思考:咱们使用mmap的时候,系统会首先分配一段虚拟内存,而且在该过程当中创建了vma和映射文件的关联,向上返回虚拟内存的首地址,此时尚未创建和物理内存的映射关系。等到第一次访问虚拟地址时,发生缺页异常,将对应的文件内容读取到页缓存,并创建虚拟内存和页缓存的映射关系,如此只有一次将文件数据读取页缓存的过程。然而在使用read系统调用时,咱们通常会先分配一个缓冲区buffer用于存放读取的文件内容,而后调用read系统调用,这里涉及到另外一个问题:我在程序里动态分配的这个buffer此时是否是已经分配了物理内存,即动态内存什么时候分配相应的物理内存(也是第一次访问的时候?),若是在执行系统调用时buffer已经创建了物理映射,read系统调用相似地读取文件内容到页缓存中,因为buffer已经创建了到物理内存的映射,此时没法也不必将其再映射到页缓存中,那么就须要将数据从页缓存拷贝至buffer已经映射的物理内存中,这样就多了一次将数据从内核空间拷贝至用户空间的过程。
为了了解mmap的方式,去看了下mmap部分的源码实现, 部份内容参考:https://blog.csdn.net/SweeNeil/article/details/83685812 mmap系列及 https://www.cnblogs.com/jikexianfeng/articles/5647994.html
源码基于3.10.1
系统调用mmap在内核中的实现为:
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len, unsigned long, prot, unsigned long, flags, unsigned long, fd, off_t, offset) { if (offset & ((1 << PAGE_SHIFT) - 1)) //判断offset是否对齐到页大小 return -EINVAL; return sys_mmap_pgoff(addr, len, prot, flags, fd, offset >> PAGE_SHIFT); }
能够看到mmap又调用了mmap_pgoff:
SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len, unsigned long, prot, unsigned long, flags, unsigned long, fd, unsigned long, pgoff) { struct file *file = NULL; unsigned long retval = -EBADF; if (!(flags & MAP_ANONYMOUS)) {//文件映射 audit_mmap_fd(fd, flags); if (unlikely(flags & MAP_HUGETLB)) return -EINVAL; file = fget(fd); //获取file结构体 if (!file) goto out; if (is_file_hugepages(file)) len = ALIGN(len, huge_page_size(hstate_file(file))); //调整len } else if (flags & MAP_HUGETLB) {//匿名映射大页内存 struct user_struct *user = NULL; struct hstate *hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & SHM_HUGE_MASK); if (!hs) return -EINVAL; len = ALIGN(len, huge_page_size(hs)); /* * VM_NORESERVE is used because the reservations will be * taken when vm_ops->mmap() is called * A dummy user value is used because we are not locking * memory so no accounting is necessary */ file = hugetlb_file_setup(HUGETLB_ANON_FILE, len, VM_NORESERVE, &user, HUGETLB_ANONHUGE_INODE, (flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK); if (IS_ERR(file)) return PTR_ERR(file); } flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE); retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);//最终调用该函数完成映射 if (file) fput(file); out: return retval; }
mmap_pgoff完成部分准备工做,而后调用函数vm_mmap_pgoff,该函数对进程的虚拟地址空间进行映射:
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long pgoff) { unsigned long ret; struct mm_struct *mm = current->mm; unsigned long populate; ret = security_mmap_file(file, prot, flag);//selinux相关 if (!ret) { down_write(&mm->mmap_sem); ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff, &populate);//主要函数 up_write(&mm->mmap_sem); if (populate) mm_populate(ret, populate); } return ret; }
而vm_mmap_pgoff主要调用的是do_mmap_pgoff,这个函数的代码比较长就不贴出来了,主要就是使用函数get_unmapped_area获取未映射地址,而后调用mmap_region进行映射。
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags) { unsigned long (*get_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); unsigned long error = arch_mmap_check(addr, len, flags); if (error) return error; /* Careful about overflows.. */ if (len > TASK_SIZE) return -ENOMEM; get_area = current->mm->get_unmapped_area;//匿名映射直接调用该函数获取虚拟内存空间 if (file && file->f_op && file->f_op->get_unmapped_area) get_area = file->f_op->get_unmapped_area;//文件映射则使用文件的相关哈数获取虚拟内存空间 addr = get_area(file, addr, len, pgoff, flags); if (IS_ERR_VALUE(addr)) return addr; if (addr > TASK_SIZE - len) return -ENOMEM; if (addr & ~PAGE_MASK) return -EINVAL; addr = arch_rebalance_pgtables(addr, len); error = security_mmap_addr(addr); return error ? error : addr; }
能够看到在get_unmapped_area中主要进行了匿名映射与文件映射的区分。若是为匿名映射,直接使用mm->get_unmapped_are直接获取虚拟内存空间(VMA),若是为文件映射,那么从file->f_op->get_unmapped_area中获取虚拟内存空间,可是经过看源码能够发现ext2_file_operation或ext3_file_operation都没有定义get_unmapped_area的方法,所以仍是调用内存描述符中的get_unmapped_area,该方法根据不一样架构调用arch_get_unmapped_area()函数,在arch_get_unmapped_area函数中当addr非空,表示指定了一个特定的优先选用地址,内核会检查该区域是否与现存区域重叠,由find_vma函数完成查找功能。当addr为空或是指定的优先地址不知足分配条件时,内核必须遍历进程中可用的区域,设法找到一个大小适当的空闲区域,由vm_unmapped_area函数作实际的工做,看一下vm_unmapped_area:
static inline unsigned long vm_unmapped_area(struct vm_unmapped_area_info *info) { if (!(info->flags & VM_UNMAPPED_AREA_TOPDOWN)) return unmapped_area(info); else return unmapped_area_topdown(info); }
该函数根据标志位的不一样调用了不一样的unmapped_area函数,他们的区别在于unmapped_area函数完成从低地址向高地址建立新的映射,而unmapped_area_topdown函数完成从高地址向低地址建立新的映射。unmapped_area函数代码就不贴上来了,该函数先检查进程虚拟地址空间中可用于映射空间的边界,不知足要求返回错误代号到上层应用程序。当知足时,执行如下操做,以找到最小的空闲的虚拟地址空间知足此次分配请求,便于两个相邻的vma区合并:
在while循环中 unmapped_area具体步骤以下:
1. 从vma红黑树的根开始遍历
2. 若当前结点有左子树则遍历其左子树,不然指向其右孩子。
3. 当某结点rb_subtree_gap多是最后一个知足分配请求的空隙时,遍历结束。 rb_subtree_gap是当前结点与其前驱结点之间空隙 和 当前结点其左右子树中的结点间的最大空隙的最大值。
4. 检测这个结点,判断这个结点与其前驱结点之间的空隙是否知足分配请求。知足则跳出循环。
5. 不知足分配请求时,指向其右孩子,判断其右孩子的rb_subtree_gap是否知足当前请求。
6. 知足则返回到2。不知足,回退其父结点,返回到4
unmapped_area完成以后仍是回到函数do_mmap_pgoff,get_unmapped_area的一系列操做只是返回了新线性区的地址,而后do_mmap_pgoff会对get_unmapped_area返回的addr进行校验,若是addr不知足页对齐,那么说明get_unmapped_area函数返回的是一个错误码,直接将这个错误码返回便可。若是addr正常,则调用calc_vm_prot_bits函数将mmap系统调用的prot 参数合并到内部使用的 vm_flags 中,以后判断是否为文件映射,如果则获取文件的inode结点,当打开一个文件以后,系统就会以inode来识别这个文件。对vm_flags的一系列设置以后,最终调用mmap_region:
if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) { if (do_munmap(mm, addr, len)) return -ENOMEM; goto munmap_back; }
mmap_region调用find_vma_links函数肯定处于新区间以前的线性区对象的位置,以及在红-黑树中新线性区的位置。同时find_vma_link函数也检查是否还存在与新区建重叠的线性区。若是这样就调用do_munmap函数删除新的区间,而后重复判断。检查无误后,再检查内存的可用性,可用就继续往下经过vma_merge函数返回了一个vma:
/* * Can we just expand an old mapping? */ vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL); if (vma) goto out;
调用vma_merge检查前一个线性区是否能够以这样的方式进行扩展来包含新的区间。同时须要保证,前一个线性区必须与在vm_flags局部变量中存放的那些线性区具备彻底相同的标志。若是前一个线性区能够扩展,那么vm_merge函数就会试图把它与随后的线性区进行合并,若是合并成功就直接跳转到out。若是合并不成功,就继续往下执行,调用kmem_cache_zalloc函数,其中调用了slab分配函数为新的线性区分配一个vm_area_struct结构,以后开始对vma进行赋值,初始化新的线性区对象。
vma->vm_file = get_file(file); error = file->f_op->mmap(file, vma); if (error) goto unmap_and_free_vma;
而后判断是否为文件映射,如果则获取文件描述符,将其赋给vm_file,若是是匿名映射,则vm_file设为dev/zero,这样不须要对全部页面进行提早置0,只有当访问到某具体页面的时候才会申请一个0页。
mmap_region以后就是调用vma_link把新的线性区插入到线性区链表的红黑树中,最后将内存描述符的total_vm字段中的进程地址空间大小进行了增长,最后返回addr。
函数的大体流程就是这样,最后咱们重点仍是关注文件映射,上面能够看到文件映射调用了file->f_op->mmap,以ext3文件系统为例,最终会调用generic_file_mmap.
const struct file_operations ext3_file_operations = { .llseek = generic_file_llseek, .read = do_sync_read, .write = do_sync_write, .aio_read = generic_file_aio_read, .aio_write = generic_file_aio_write, .unlocked_ioctl = ext3_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext3_compat_ioctl, #endif .mmap = generic_file_mmap, .open = dquot_file_open, .release = ext3_release_file, .fsync = ext3_sync_file, .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, };
看一下generic_file_mmap:
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; return 0; }
这里首先获取文件f_mapping指向的地址空间,而后判断其readpage是否为空,若空则返回错误码,由于以后须要使用该函数读取真正的文件内容,因此不能为空。这里的mapping->a_ops就是文件address_space对特定地址空间的操做函数指针,仍是以ext3文件系统为例,具体的函数赋值为:
static const struct address_space_operations ext3_writeback_aops = { .readpage = ext3_readpage, .readpages = ext3_readpages, .writepage = ext3_writeback_writepage, .write_begin = ext3_write_begin, .write_end = ext3_writeback_write_end, .bmap = ext3_bmap, .invalidatepage = ext3_invalidatepage, .releasepage = ext3_releasepage, .direct_IO = ext3_direct_IO, .migratepage = buffer_migrate_page, .is_partially_uptodate = block_is_partially_uptodate, .error_remove_page = generic_error_remove_page, };
函数generic_file_mmap最后还将该vma对应的操做赋值为generic_file_vm_ops:
const struct vm_operations_struct generic_file_vm_ops = { .fault = filemap_fault, .page_mkwrite = filemap_page_mkwrite, .remap_pages = generic_file_remap_pages, };
mmap文件映射的流程大体就是这样,主要就是分配一个可用的vma,而后将其和文件对应,这里尚未真正地将文件的内容映射进内存。等到真正读取内容的时候发生缺页异常,内核调用函数do_page_fault(/arch/x86/mm/fault.c)来处理,所需处理的状况比较复杂,这里我仍是重点关注文件映射部分的缺页异常,其余部分暂不涉及。
该函数进一步调用__do_page_fault,若是异常并不是出如今中断期间,也有相关的上下文,则内核检查进程的地址空间是否包含异常地址所在的区域:
vma = find_vma(mm, address);
find_vma用于完成上述工做,在内核发现地址有效或无效时,会分别跳转到good_area和bad_area,当找到异常地址所在的区域后,还需根据权限判断当前的访问是否有效,如error_code是不可写/不可执行的错误,但addr所属vma线性区自己就不可写/不可执行,那么就直接返回,由于问题根本不是缺页,而是访问无效,以后调用handle_mm_fault负责校订缺页异常。
fault = handle_mm_fault(mm, vma, address, flags);
handle_mm_fault不依赖底层体系结构,其在内存管理的框架下独立于系统实现。该函数确认在各级页目录中,通向对应于异常地址的页表项的各个页目录项都存在,不存在则分配。最后调用函数handle_pte_fault分析缺页异常的缘由:
int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, pte_t *pte, pmd_t *pmd, unsigned int flags) { pte_t entry; spinlock_t *ptl; entry = *pte; //pte为指向相关页表项(pte_t)的指针,此处得到页表项 if (!pte_present(entry)) {//页不在物理内存中 if (pte_none(entry)) {/*没有对应的页表项,则内核必须从头开始加载该页,对匿名映射称之为按需分配,对基于文件的映射称为按需调页。若是vm_ops为空,则必须使用do_anonymous_page返回一个匿名页*/ if (vma->vm_ops) {//若是该vma的操做函数集合实现了fault函数,说明是文件映射而不是匿名映射,将调用do_linear_fault分配物理页 if (likely(vma->vm_ops->fault)) return do_linear_fault(mm, vma, address, pte, pmd, flags, entry); } return do_anonymous_page(mm, vma, address, pte, pmd, flags);//匿名映射分配物理页 } if (pte_file(entry))//检查页表项是否属于非线性映射 return do_nonlinear_fault(mm, vma, address, pte, pmd, flags, entry); return do_swap_page(mm, vma, address, pte, pmd, flags, entry);//该页已换出,从交换区换入 } //下方是关于写时复制的处理,此处不贴出
以前在处理mmap系统调用时,经过vma->vm_ops = &generic_file_vm_ops给vma->vm_ops进行赋值,所以直接调用函数do_linear_fault进行处理,该函数在转换一些参数以后,将工做委托给__do_fault:
ret = vma->vm_ops->fault(vma, &vmf);
__do_fault中调用定义好的fault函数,将所需的文件数据读入到内存页,从前面mmap系统调用可知vma->vm_ops->fault赋值为filemap_fault,该函数首先去页缓存中查找所需的页是否存在,因为如今刚完成mmap,对应的页确定不在页缓存中,所以调用函数page_cache_read
no_cached_page: /* * We're only likely to ever get here if MADV_RANDOM is in * effect. */ error = page_cache_read(file, offset);
page_cache_read分配一个页面,将其加入页缓存,并调用mapping->a_ops->readpage读取数据,以ext3文件系统为例,mapping->a_ops->readpage赋值为ext3_readpage,该函数基于io调度将文件内容读取到分配的缓存页中。
static int page_cache_read(struct file *file, pgoff_t offset) { struct address_space *mapping = file->f_mapping; struct page *page; int ret; do { page = page_cache_alloc_cold(mapping); if (!page) return -ENOMEM; ret = add_to_page_cache_lru(page, mapping, offset, GFP_KERNEL); if (ret == 0) ret = mapping->a_ops->readpage(file, page); else if (ret == -EEXIST) ret = 0; /* losing race to add is OK */ page_cache_release(page); } while (ret == AOP_TRUNCATED_PAGE); return ret; }
这样页缓存中就有了该文件内容对应的页,将其赋值给vmf->page,返回至__do_fault,以后的操做流程以下:
mmap系统调用的flags参数有几个选项,其中MAP_SHARED表示建立一个共享映射的区域,多个进程能够经过共享映射方式来映射一个文件,这样其余进程也能够看到映射内容的改变,修改后的内容会同步到磁盘文件中;MAP_PRIVATE则是建立一个私有的写时复制的映射。多个进程能够经过私有映射的方式来映射一个文件,这样其余进程不会看到映射内容的改变,修改后的内容也不会同步到磁盘文件中;MAP_ANONYMOUS建立一个匿名映射,即没有关联到文件的映射,还有其余参数此处再也不说明。回到函数__do_fault,若是是对私有映射的写访问,那么须要拷贝该页的一个副本,这样的话其实就仍是两次复制操做。
但若是是共享映射的话,则调用vma->vm_ops->page_mkwrite通知进程地址空间page变为可写,一个页面变成可写,那么进程有可能须要等待这个page的内容回写成功。
page = vmf.page; if (flags & FAULT_FLAG_WRITE) {//写访问 if (!(vma->vm_flags & VM_SHARED)) {//私有映射 page = cow_page; anon = 1; copy_user_highpage(page, vmf.page, address, vma); __SetPageUptodate(page); } else {//共享映射 /* * If the page will be shareable, see if the backing * address space wants to know that the page is about * to become writable */ if (vma->vm_ops->page_mkwrite) { int tmp; unlock_page(page); vmf.flags = FAULT_FLAG_WRITE|FAULT_FLAG_MKWRITE; tmp = vma->vm_ops->page_mkwrite(vma, &vmf); if (unlikely(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))) { ret = tmp; goto unwritable_page; } if (unlikely(!(tmp & VM_FAULT_LOCKED))) { lock_page(page); if (!page->mapping) { ret = 0; /* retry the fault */ unlock_page(page); goto unwritable_page; } } else VM_BUG_ON(!PageLocked(page)); page_mkwrite = 1; } } }
以后判断该异常地址对应的硬件页表项pte的内容是否与以前一致,若和orig_pte不同说明期间有人修改了pte,那么须要释放page。若是一致,则将其加入进程的页表,再合并到逆向映射数据结构中。在完成这些操做以前,使用flush_icache_page更新缓存,确保页的内容在用户空间可见。以后根据读写权限产生页表项,根据映射类型集成到逆向映射,最后更新处理器的MMU,由于页表已经修改。
/* Only go through if we didn't race with anybody else... */ if (likely(pte_same(*page_table, orig_pte))) {//判读一致性 flush_icache_page(vma, page); entry = mk_pte(page, vma->vm_page_prot);//产生指向只读页的页表项 if (flags & FAULT_FLAG_WRITE) entry = maybe_mkwrite(pte_mkdirty(entry), vma);//设置为可写 if (anon) { inc_mm_counter_fast(mm, MM_ANONPAGES); page_add_new_anon_rmap(page, vma, address);//将匿名页集成到逆向映射 } else { inc_mm_counter_fast(mm, MM_FILEPAGES); page_add_file_rmap(page);//将文件映射的页集成到逆向映射 if (flags & FAULT_FLAG_WRITE) { dirty_page = page; get_page(dirty_page); } } set_pte_at(mm, address, page_table, entry);//填充页表项 /* no need to invalidate: a not-present page won't be cached */ update_mmu_cache(vma, address, page_table);//更新mmu } else { if (cow_page) mem_cgroup_uncharge_page(cow_page); if (anon) page_cache_release(page);//若是不一致则释放 else anon = 1; /* no anon but release faulted_page */ }
到这里也能够看到,共享映射的状况下就是将文件内容复制到页缓存,而后创建进程虚拟内存到页缓存的映射关系,达到用户进程经过操做虚拟内存来操做文件的效果。
三、块缓存
这块之后看到了再加上来吧。