各類緩存(一)

對操做系統中的各類緩存進行一下梳理: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)回寫修改的數據:

內核同時提供了以下幾個同步方案:

  • 幾個專門的內核守護進程在後臺運行,稱爲pdflush,它們將週期性激活而不考慮頁緩存中當前的狀況,這些守護進程掃描緩存中的頁,將超出必定時間沒有與底層塊設備同步的頁寫回。
  • pdflush的第二種運做模式是:若是緩存中修改的數據項數目在短時間內顯著增長,則由內核激活pdflush
  • 提供相關的系統調用。可由用戶或應用程序通知內核寫回全部未同步的數據,如sync調用

一般,修改文件或其餘按頁緩存的對象時,只會修改頁的一部分。爲節省時間,內核在寫操做期間,將緩存中的每一頁劃分爲較小的單位,稱爲緩衝區。在同步數據時,內核能夠將回寫操做限制於那些實際發生了修改的較小單位上。

(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 */
    }

到這裏也能夠看到,共享映射的狀況下就是將文件內容複製到頁緩存,而後創建進程虛擬內存到頁緩存的映射關係,達到用戶進程經過操做虛擬內存來操做文件的效果。

 

三、塊緩存

這塊之後看到了再加上來吧。

相關文章
相關標籤/搜索