內存

內存

 

1.         內存

1.               內存管理子系統導讀from aka

2.               用戶態

3.               內核頁目錄的初始化

4.               內核線程頁目錄的借用

5.               用戶進程內核頁目錄的建立

6.               內核頁目錄的同步

7.               mlock代碼分析

8.               memory.c

1.                      copy_page

2.                      clear_page_tables

3.                      oom

4.                      free_page_tables

5.                      new_page_tables

6.                      copy_one_pte

7.                      copy_pte_range

8.                      copy_pmd_range

9.                      copy_page_range

10.                  free_pte

11.                  forget_pte

12.                  zap_pte_range

13.                  zap_pmd_range

14.                  zap_page_range

15.                  zeromap_pte_range

16.                  remap_pte_range

17.                  put_dirty_page

18.                  handle_mm_fault

9.               mmap.c

10.           夥伴(buddy)算法

11.           頁目錄處理的宏

12.           MM作者的文章




內存


   
內存管理系統是操作系統中最爲重要的部分,因爲系統的物理內存總是少於系統所需要的內存數量。虛擬內存就是爲了克服這個矛盾而採用的策略。系統的虛擬內存通過在各個進程之間共享內存而使系統看起來有多於實際內存的內存容量。

    虛擬內存可以提供以下的功能:

*廣闊的地址空間。
   
系統的虛擬內存可以比系統的實際內存大很多倍。

*進程的保護。
   
系統中的每一個進程都有自己的虛擬地址空間。這些虛擬地址空間是完全分開的,這樣一個進程的運行不會影響其他進程。並且,硬件上的虛擬內存機制是被保護的,內存不能被寫入,這樣可以防止迷失的應用程序覆蓋代碼的數據。

*內存映射。
   
內存映射用來把文件映射到進程的地址空間。在內存映射中,文件的內容直接連接到進程的虛擬地址空間。

*公平的物理內存分配。
   
內存管理系統允許系統中每一個運行的進程都可以公平地得到系統的物理內存。

*共享虛擬內存。
   
雖然虛擬內存允許進程擁有自己單獨的虛擬地址空間,但有時可能會希望進程共享內存。

linux僅僅使用四個段

兩個代表 (code data/stack)是內核空間從[0xC000 0000] (3 GB)[0xFFFF FFFF] (4 GB)
兩個代表 (code data/stack)是用戶空間從[0] (0 GB) [0xBFFF FFFF] (3 GB)

                               __
   4 GB--->|                |    |
           |     Kernel     |    | 
內核空間
(Code + Data/Stack)
           |                |  __|
   3 GB--->|----------------|  __
           |                |    |
           |                |    |
   2 GB--->|                |    |
           |     Tasks      |    | 
用戶空間
(Code + Data/Stack)
           |                |    |
   1 GB--->|                |    |
           |                |    |
           |________________|  __|
0x00000000
         
內核/用戶 線性地址


linux
可以使用3層頁表映射,例如在高級的I64服務器上,但是i386體系結構下只有2層有實際意義:

   ------------------------------------------------------------------
                             
線性地址

   ------------------------------------------------------------------
        /___/                 /___/                     /_____/

     PD 偏移                 PF 偏移                  Frame 偏移
     [10 bits]              [10 bits]                 [12 bits]
          |                     |                          |
          |                     |     -----------          |
          |                     |     |  Value  |----------|---------
          |     |         |     |     |---------|   /|/    |        |
          |     |         |     |     |         |    |     |        |
          |     |         |     |     |         |    | Frame
偏移   |
          |     |         |     |     |         |   /|/             |
          |     |         |     |     |---------|<------            |
          |     |         |     |     |         |      |            |
          |     |         |     |     |         |      | x 4096     |
          |     |         |  PF
偏移
  |_________|-------            |
          |     |         |       /|/ |         |                   |
      PD
偏移
   |_________|-----   |  |         |          _________|
            /|/ |         |    |   |  |         |          |
             |  |         |    |  /|/ |         |         /|/
_____       |  |         |    ------>|_________|  
物理地址

|     |     /|/ |         |    x 4096 |         |
| CR3 |-------->|         |           |         |
|_____|         | ....... |           | ....... |
                |         |           |         |

                  頁目錄表                 頁表

                       Linux i386 分頁

    注意內核(僅僅內核)線性空間就等於內核物理空間,所以如下:

            ________________ _____
           |  
其他內核數據
|___  |  |                |
           |----------------|   | |__|                |
           |     
內核      |/  |____|  實際的其他
    |
  3 GB --->|----------------| /      |  
內核數據
     |
           |                |/ /     |                |
           |              __|_/_/____|__   Real       |
           |      Tasks     |  / /   |     Tasks      |
           |              __|___/_/__|__   Space      |
           |                |    / / |                |
           |                |     / /|----------------|
           |                |      / | 
實際內核空間
  |
           |________________|       /|________________|

                 邏輯地址                  物理地址

 

[內存實時分配]

|copy_mm
   |allocate_mm = kmem_cache_alloc
      |__kmem_cache_alloc
         |kmem_cache_alloc_one
            |alloc_new_slab
               |kmem_cache_grow
                  |kmem_getpages
                     |__get_free_pages
                        |alloc_pages
                           |alloc_pages_pgdat
                              |__alloc_pages
                                 |rmqueue
                                 |reclaim_pages

·copy_mm [kernel/fork.c]
·allocate_mm [kernel/fork.c]
·kmem_cache_alloc [mm/slab.c]
·__kmem_cache_alloc
·kmem_cache_alloc_one
·alloc_new_slab
·kmem_cache_grow
·kmem_getpages
·__get_free_pages [mm/page_alloc.c]
·alloc_pages [mm/numa.c]
·alloc_pages_pgdat
·__alloc_pages [mm/page_alloc.c]
·rm_queue
·reclaim_pages [mm/vmscan.c]

[內存交換線程kswapd]

|kswapd
   |// initialization routines
   |for (;;) { // Main loop
      |do_try_to_free_pages
      |recalculate_vm_stats
      |refill_inactive_scan
      |run_task_queue
      |interruptible_sleep_on_timeout // we sleep for a new swap request
   |}


·kswapd [mm/vmscan.c]
·do_try_to_free_pages
·recalculate_vm_stats [mm/swap.c]
·refill_inactive_scan [mm/vmswap.c]
·run_task_queue [kernel/softirq.c]
·interruptible_sleep_on_timeout [kernel/sched.c]


[
內存交換機制:出現內存不足的Exception]

| Page Fault Exception
| cause by all these conditions:
|   a-) User page
|   b-) Read or write access
|   c-) Page not present
|
|
-----------> |do_page_fault
                 |handle_mm_fault
                    |pte_alloc
                       |pte_alloc_one
                          |__get_free_page = __get_free_pages
                             |alloc_pages
                                |alloc_pages_pgdat
                                   |__alloc_pages
                                      |wakeup_kswapd // We wake up kernel thread kswapd


·do_page_fault [arch/i386/mm/fault.c]
·handle_mm_fault [mm/memory.c]
·pte_alloc
·pte_alloc_one [include/asm/pgalloc.h]
·__get_free_page [include/linux/mm.h]
·__get_free_pages [mm/page_alloc.c]
·alloc_pages [mm/numa.c]
·alloc_pages_pgdat
·__alloc_pages
·wakeup_kswapd [mm/vmscan.c]

[目錄]




內存管理子系統導讀from aka


   
我的目標是導讀,提供linux內存管理子系統的整體概念,同時給出進一步深入研究某個部分時的輔助信息(包括代碼組織,文件和主要函數的意義和一些參考文檔)。之所以採取這種方式,是因爲我本人在閱讀代碼的過程中,深感讀懂一段代碼容易,把握整體思想卻極不容易。而且,在我寫一些內核代碼時,也覺得很多情況下,不一定非得很具體地理解所有內核代碼,往往瞭解它的接口和整體工作原理就夠了。當然,我個人的能力有限,時間也很不夠,很多東西也是近期迫於講座壓力臨時學的:),內容難免偏頗甚至錯誤,歡迎大家指正。

存儲層次結構和x86存儲管理硬件(MMU

    這裏假定大家對虛擬存儲,段頁機制有一定的瞭解。主要強調一些很重要的或者容易誤解的概念。

存儲層次

    高速緩存(cache) -- 主存(main memory) --- 磁盤(disk)

    理解存儲層次結構的根源:CPU速度和存儲器速度的差距。

    層次結構可行的原因:局部性原理。

LINUX的任務:

    減小footprint,提高cache命中率,充分利用局部性。

    實現虛擬存儲以滿足進程的需求,有效地管理內存分配,力求最合理地利用有限的資源。

參考文檔:

    too little,too smallby Rik Van Riel, Nov. 27,2000.

    以及所有的體系結構教材:)


MMU
的作用

    輔助操作系統進行內存管理,提供虛實地址轉換等硬件支持。


x86
的地址

    邏輯地址: 出現在機器指令中,用來制定操作數的地址。段:偏移

    線性地址:邏輯地址經過分段單元處理後得到線性地址,這是一個32位的無符號整數,可用於定位4G個存儲單元。

    物理地址:線性地址經過頁表查找後得出物理地址,這個地址將被送到地址總線上指示所要訪問的物理內存單元。

LINUX: 儘量避免使用段功能以提高可移植性。如通過使用基址爲0的段,使邏輯地址==線性地址。


x86
的段

    保護模式下的段:選擇子+描述符。不僅僅是一個基地址的原因是爲了提供更多的信息:保護、長度限制、類型等。描述符存放在一張表中(GDTLDT),選擇子可以認爲是表的索引。段寄存器中存放的是選擇子,在段寄存器裝入的同時,描述符中的數據被裝入一個不可見的寄存器以便cpu快速訪問。(圖)P40

    專用寄存器:GDTR(包含全局描述附表的首地址),LDTR(當前進程的段描述附表首地址),TSR(指向當前進程的任務狀態段)


LINUX
使用的段:

    __KERNEL_CS 內核代碼段。範圍 0-4G。可讀、執行。DPL=0

    __KERNEL_DS:內核代碼段。範圍 0-4G。可讀、寫。DPL=0

    __USER_CS:內核代碼段。範圍 0-4G。可讀、執行。DPL=3

    __USER_DS:內核代碼段。範圍 0-4G。可讀、寫。DPL=3

    TSS(任務狀態段):存儲進程的硬件上下文,進程切換時使用。(因爲x86硬件對TSS有一定支持,所有有這個特殊的段和相應的專用寄存器。)

    default_ldt:理論上每個進程都可以同時使用很多段,這些段可以存儲在自己的ldt段中,但實際linux極少利用x86的這些功能,多數情況下所有進程共享這個段,它只包含一個空描述符。

    還有一些特殊的段用在電源管理等代碼中。

    (在2.2以前,每個進程的ldtTSS段都存在GDT中,而GDT最多只能有8192項,因此整個系統的進程總數被限制在4090左右。24裏不再把它們存在GDT中,從而取消了這個限制。)

    __USER_CS__USER_DS段都是被所有在用戶態下的進程共享的。注意不要把這個共享和進程空間的共享混淆:雖然大家使用同一個段,但通過使用不同的頁表由分頁機制保證了進程空間仍然是獨立的。


x86
的分頁機制

    x86硬件支持兩級頁表,奔騰pro以上的型號還支持Physical address Extension Mode和三級頁表。所謂的硬件支持包括一些特殊寄存器(cr0-cr4)、以及CPU能夠識別頁表項中的一些標誌位並根據訪問情況做出反應等等。如讀寫Present位爲0的頁或者寫Read/Write位爲0的頁將引起CPU發出page fault異常,訪問完頁面後自動設置accessed位等。

    linux採用的是一個體繫結構無關的三級頁表模型(如圖),使用一系列的宏來掩蓋各種平臺的細節。例如,通過把PMD看作只有一項的表並存儲在pgd表項中(通常pgd表項中存放的應該是pmd表的首地址),頁表的中間目錄(pmd)被巧妙地摺疊到頁表的全局目錄(pgd),從而適應了二級頁表硬件。

TLB

    TLB全稱是Translation Look-aside Buffer,用來加速頁表查找。這裏關鍵的一點是:如果操作系統更改了頁表內容,它必須相應的刷新TLB以使CPU不誤用過時的表項。


Cache

    Cache 基本上是對程序員透明的,但是不同的使用方法可以導致大不相同的性能。linux有許多關鍵的地方對代碼做了精心優化,其中很多就是爲了減少對cache不必要的污染。如把只有出錯情況下用到的代碼放到.fixup section,把頻繁同時使用的數據集中到一個cache行(如struct task_struct),減少一些函數的footprint,在slab分配器裏頭的slab coloring等。

    另外,我們也必須知道什麼時候cache要無效:新map/remap一頁到某個地址、頁面換出、頁保護改變、進程切換等,也即當cache對應的那個地址的內容或含義有所變化時。當然,很多情況下不需要無效整個cache,只需要無效某個地址或地址範圍即可。實際上,

    intel在這方面做得非常好用,cache的一致性完全由硬件維護。

    關於x86處理器更多信息,請參照其手冊:Volume 3: Architecture and Programming Manual


8
Linux 相關實現

    這一部分的代碼和體系結構緊密相關,因此大多位於arch子目錄下,而且大量以宏定義和inline函數形式存在於頭文件中。以i386平臺爲例,主要的文件包括:

page.h

    頁大小、頁掩碼定義。PAGE_SIZE,PAGE_SHIFTPAGE_MASK

    對頁的操作,如清除頁內容clear_page、拷貝頁copy_page、頁對齊page_align

    還有內核虛地址的起始點:著名的PAGE_OFFSET:)和相關的內核中虛實地址轉換的宏__pa__va.

    virt_to_page從一個內核虛地址得到該頁的描述結構struct page *.我們知道,所有物理內存都由一個memmap數組來描述。這個宏就是計算給定地址的物理頁在這個數組中的位置。另外這個文件也定義了一個簡單的宏檢查一個頁是不是合法:VALID_PAGE(page)。如果pagememmap數組的開始太遠以至於超過了最大物理頁面應有的距離則是不合法的。

    比較奇怪的是頁表項的定義也放在這裏。pgd_t,pmd_t,pte_t和存取它們值的宏xxx_val


pgtable.h pgtable-2level.h pgtable-3level.h

    顧名思義,這些文件就是處理頁表的,它們提供了一系列的宏來操作頁表。pgtable-2level.hpgtable-2level.h則分別對應x86二級、三級頁表的需求。首先當然是表示每級頁表有多少項的定義不同了。而且在PAE模式下,地址超過32位,頁表項pte_t64位來表示(pmd_t,pgd_t不需要變),一些對整個頁表項的操作也就不同。共有如下幾類:

    ·[pte/pmd/pgd]_ERROR 出措時要打印項的取值,64位和32位當然不一樣。
    ·set_[pte/pmd/pgd]
設置表項值
    ·pte_same
比較 pte_page pte得出所在的memmap位置
    ·pte_none
是否爲空。
    ·__mk_pte
構造pte

    pgtable.h的宏太多,不再一一解釋。實際上也比較直觀,通常從名字就可以看出宏的意義來了。pte_xxx宏的參數是pte_t,ptep_xxx的參數是pte_t *2.4 kernel在代碼的clean up方面還是作了一些努力,不少地方含糊的名字變明確了,有些函數的可讀性頁變好了。

    pgtable.h裏除了頁表操作的宏外,還有cachetlb刷新操作,這也比較合理,因爲他們常常是在頁表操作時使用。這裏的tlb操作是以__開始的,也就是說,內部使用的,真正對外接口在pgalloc.h中(這樣分開可能是因爲在SMP版本中,tlb的刷新函數和單機版本區別較大,有些不再是內嵌函數和宏了)。

pgalloc.h

    包括頁表項的分配和釋放宏/函數,值得注意的是表項高速緩存的使用:

    pgd/pmd/pte_quicklist

    內核中有許多地方使用類似的技巧來減少對內存分配函數的調用,加速頻繁使用的分配。如buffer cachebuffer_headbuffervm區域中最近使用的區域。

    還有上面提到的tlb刷新的接口

segment.h

    定義 __KERNEL_CS[DS] __USER_CS[DS]

參考:

    Understanding the Linux Kernel》的第二章給了一個對linux 的相關實現的簡要描述,

 

物理內存的管理。

    2.4中內存管理有很大的變化。在物理頁面管理上實現了基於區的夥伴系統(zone based buddy system)。區(zone)的是根據內存的不同使用類型劃分的。對不同區的內存使用單獨的夥伴系統(buddy system)管理,而且獨立地監控空閒頁等。

    (實際上更高一層還有numa支持。Numa(None Uniformed Memory Access)是一種體系結構,其中對系統裏的每個處理器來說,不同的內存區域可能有不同的存取時間(一般是由內存和處理器的距離決定)。而一般的機器中內存叫做DRAM,即動態隨機存取存儲器,對每個單元,CPU用起來是一樣快的。NUMA中訪問速度相同的一個內存區域稱爲一個Node,支持這種結構的主要任務就是要儘量減少Node之間的通信,使得每個處理器要用到的數據儘可能放在對它來說最快的Node中。2.4內核中node&#0;相應的數據結構是pg_data_t,每個node擁有自己的memmap數組,把自己的內存分成幾個zone,每個zone再用獨立的夥伴系統管理物理頁面。Numa要對付的問題還有很多,也遠沒有完善,就不多說了)

基於區的夥伴系統的設計&#0;物理頁面的管理

    內存分配的兩大問題是:分配效率、碎片問題。一個好的分配器應該能夠快速的滿足各種大小的分配要求,同時不能產生大量的碎片浪費空間。夥伴系統是一個常用的比較好的算法。(解釋:TODO)

引入區的概念是爲了區分內存的不同使用類型(方法?),以便更有效地利用它們。

    2.4有三個區:DMA, Normal, HighMem。前兩個在2.2實際上也是由獨立的buddy system管理的,但2.2中還沒有明確的zone的概念。DMA區在x86體系結構中通常是小於16兆的物理內存區,因爲DMA控制器只能使用這一段的內存。而HighMem是物理地址超過某個值(通常是約900M)的高端內存。其他的是Normal區內存。由於linux實現的原因,高地址的內存不能直接被內核使用,如果選擇了CONFIG_HIGHMEM選項,內核會使用一種特殊的辦法來使用它們。(解釋:TODO)。HighMem只用於page cache和用戶進程。這樣分開之後,我們將可以更有針對性地使用內存,而不至於出現把DMA可用的內存大量給無關的用戶進程使用導致驅動程序沒法得到足夠的DMA內存等情況。此外,每個區都獨立地監控本區內存的使用情況,分配時系統會判斷從哪個區分配比較合算,綜合考慮用戶的要求和系統現狀。2.4裏分配頁面時可能會和高層的VM代碼交互(分配時根據空閒頁面的情況,內核可能從夥伴系統裏分配頁面,也可能直接把已經分配的頁收回&#0;reclaim),代碼比2.2複雜了不少,要全面地理解它得熟悉整個VM工作的機理。

整個分配器的主要接口是如下函數(mm.h page_alloc.c)

struct page * alloc_pages(int gfp_mask, unsigned long order) 根據gftp_mask的要求,從適當的區分配2^order個頁面,返回第一個頁的描述符。

#define alloc_page(gfp_mask) alloc_pages(gfp_mask,0)

unsigned long __get_free_pages((int gfp_mask, unsigned long order) 工作同alloc_pages,但返回首地址。

#define __get_free_page(gfp_mask) __get_free_pages(gfp_mask,0)

get_free_page 分配一個已清零的頁面。

__free_page(s) free_page(s)釋放頁面(一個/多個)前者以頁面描述符爲參數,後者以頁面地址爲參數。

    關於Buddy算法,許多教科書上有詳細的描述,第六章對linux的實現有一個很好的介紹。關於zone base buddy更多的信息,可以參見Rik Van Riel 寫的" design for a zone based memory allocator"。這個人是目前linuxmm的維護者,權威啦。這篇文章有一點過時了,98年寫的,當時還沒有HighMem,但思想還是有效的。還有,下面這篇文章分析2.4的實現代碼:

http://home.earthlink.net/~jknapka/linux-mm/zonealloc.html


Slab--
連續物理區域管理

    單單分配頁面的分配器肯定是不能滿足要求的。內核中大量使用各種數據結構,大小從幾個字節到幾十上百k不等,都取整到2的冪次個頁面那是完全不現實的。2.0的內核的解決方法是提供大小爲2,4,8,16,...,131056字節的內存區域。需要新的內存區域時,內核從夥伴系統申請頁面,把它們劃分成一個個區域,取一個來滿足需求;如果某個頁面中的內存區域都釋放了,頁面就交回到夥伴系統。這樣做的效率不高。有許多地方可以改進:

    不同的數據類型用不同的方法分配內存可能提高效率。比如需要初始化的數據結構,釋放後可以暫存着,再分配時就不必初始化了。
   
內核的函數常常重複地使用同一類型的內存區,緩存最近釋放的對象可以加速分配和釋放。
   
對內存的請求可以按照請求頻率來分類,頻繁使用的類型使用專門的緩存,很少使用的可以使用類似2.0中的取整到2的冪次的通用緩存。
   
使用2的冪次大小的內存區域時高速緩存衝突的概率較大,有可能通過仔細安排內存區域的起始地址來減少高速緩存衝突。
   
緩存一定數量的對象可以減少對buddy系統的調用,從而節省時間並減少由此引起的高速緩存污染。

2.2實現的slab分配器體現了這些改進思想。

主要數據結構

接口:

kmem_cache_create/kmem_cache_destory

kmem_cache_grow/kmem_cache_reap 增長/縮減某類緩存的大小

kmem_cache_alloc/kmem_cache_free 從某類緩存分配/釋放一個對象

kmalloc/kfree 通用緩存的分配、釋放函數。

相關代碼(slab.c)

相關參考:

http://www.lisoleg.net/lisoleg/memory/slab.pdf Slab發明者的論文,必讀經典。

第六章,具體實現的詳細清晰的描述。

AKA2000年的講座也有一些大蝦講過這個主題,請訪問aka主頁:www.aka.org.cn


vmalloc/vfree &#0;
物理地址不連續,虛地址連續的內存管理

    使用kernel頁表。文件vmalloc.c,相對簡單。


2.4
內核的VM(完善中。。。)

進程地址空間管理

    創建,銷燬。

mm_struct, vm_area_struct, mmap/mprotect/munmap

page fault處理,demand page, copy on write


相關文件:

include/linux/mm.hstruct page結構的定義,page的標誌位定義以及存取操作宏定義。struct vm_area_struct定義。mm子系統的函數原型說明。

include/linux/mman.h:vm_area_struct的操作mmap/mprotect/munmap相關的常量宏定義。

memory.cpage fault處理,包括COWdemand page等。

對一個區域的頁表相關操作:

zeromap_page_range: 把一個範圍內的頁全部映射到zero_page

remap_page_range:給定範圍的頁重新映射到另一塊地址空間。

zap_page_range:把給定範圍內的用戶頁釋放掉,頁表清零。

mlock.c mlock/munlock系統調用。mlock把頁面鎖定在物理內存中。

mmap.c:mmap/munmap/brk系統調用。

mprotect.c mprotect系統調用。

    前面三個文件都大量涉及vm_area_struct的操作,有很多相似的xxx_fixup的代碼,它們的任務是修補受到影響的區域,保證vm_area_struct 鏈表正確。


交換

目的:

    使得進程可以使用更大的地址空間。同時容納更多的進程。

任務:

    選擇要換出的頁

    決定怎樣在交換區中存儲頁面

    決定什麼時候換出

kswapd內核線程:每10秒激活一次

    任務:當空閒頁面低於一定值時,從進程的地址空間、各類cache回收頁面

    爲什麼不能等到內存分配失敗再用try_to_free_pages回收頁面?原因:

    有些內存分配時在中斷或異常處理調用,他們不能阻塞

    有時候分配發生在某個關鍵路徑已經獲得了一些關鍵資源的時候,因此它不能啓動IO。如果不巧這時所有的路徑上的內存分配都是這樣,內存就無法釋放。

kreclaimd inactive_clean_list回收頁面,由__alloc_pages喚醒。

相關文件:

mm/swap.c kswapd使用的各種參數以及操作頁面年齡的函數。

mm/swap_file.c 交換分區/文件的操作。

mm/page_io.c 讀或寫一個交換頁。

mm/swap_state.c swap cache相關操作,加入/刪除/查找一個swap cache等。

mm/vmscan.c 掃描進程的vm_area,試圖換出一些頁面(kswapd)。

reclaim_page:從inactive_clean_list回收一個頁面,放到free_list

    kclaimd被喚醒後重復調用reclaim_page直到每個區的

zone->free_pages>= zone->pages_low

    page_lauder:由__alloc_pagestry_to_free_pages等調用。通常是由於freepages + inactive_clean_list的頁太少了。功能:把inactive_dirty_list的頁面轉移到inactive_clean_list,首先把已經被寫回文件或者交換區的頁面(by bdflush)放到inactive_clean_list,如果freepages確實短缺,喚醒bdflush,再循環一遍把一定數量的dirty頁寫回。

    關於這幾個隊列(active_list,inactive_dirty_list,inactive_clean_list)的邏輯,請參照:文檔:RFC: design for new VM,可以從lisoleg的文檔精華獲得。

page cachebuffer cacheswap cache

    page cache:讀寫文件時文件內容的cache,大小爲一個頁。不一定在磁盤上連續。

    buffer cache:讀寫磁盤塊的時候磁盤塊內容的cachebuffer cache的內容對應磁盤上一個連續的區域,一個buffer cache大小可能從512(扇區大小)到一個頁。

    swap cache page cache的子集。用於多個進程共享的頁面被換出到交換區的情況。

page cache buffer cache的關係

    本質上是很不同的,buffer cache緩衝磁盤塊內容,page cache緩衝文件的一頁內容。page cache寫回時會使用臨時的buffer cache來寫磁盤。

bdflush dirtybuffer cache寫回磁盤。通常只當dirtybuffer太多或者需要更多的buffer而內存開始不足時運行。page_lauder也可能喚醒它。

kupdate 定時運行,把寫回期限已經到了的dirty buffer寫回磁盤。

    2.4的改進:page cachebuffer cache耦合得更好了。在2.2裏,磁盤文件的讀使用page cache,而寫繞過page cache,直接使用buffer cache,因此帶來了同步的問題:寫完之後必須使用update_vm_cache()更新可能有的page cache2.4page cache做了比較大的改進,文件可以通過page cache直接寫了,page cache優先使用high memory。而且,2.4引入了新的對象:file address space,它包含用來讀寫一整頁數據的方法。這些方法考慮到了inode的更新、page cache處理和臨時buffer的使用。page cachebuffer cache的同步問題就消除了。原來使用inode+offset查找page cache變成通過file address space+offset;原來struct page 中的inode成員被address_space類型的mapping成員取代。這個改進還使得匿名內存的共享成爲可能(這個在2.2很難實現,許多討論過)。

虛存系統則從freeBSD借鑑了很多經驗,針對2.2的問題作了巨大的調整。

    文檔:RFC: design for new VM不可不讀。

    由於時間倉促,新vm的很多細微之處我也還沒來得及搞清楚。先大致羅列一下,以後我將進一步完善本文,爭取把問題說清楚。另外,等這學期考試過後,我希望能爲大家提供一些詳細註釋過的源代碼。

 

 

[目錄]




用戶態


   
用戶空間存取內核空間,具體的實現方法要從兩個方面考慮,先是用戶進程,需要調用mmapp來將自己的一段虛擬空間映射到內核態分配的物理內存;然後內核空間需要重新設置用戶進程的這段虛擬內存的頁表,使它的物理地址指向對應的物理內存。針對linux內核的幾種不同的內存分配方式(kmallocvmallocioremap),需要進行不同的處理。

一、Linux內存管理概述

這裏說一下我的理解,主要從數據結構說。

1、物理內存都是按順序分成一頁一頁的,每頁用一個page結構來描述。系統所有的物理頁 面的page

構描述就組成了一個數組mem_map

2、進程的虛擬地址空間用task_struct的域mm來描述,它是一個mm_struct結構,這個結構包包含了指向?

程頁目錄的指針(pgd_t * pgd)和指向進程虛擬內存區域的指針(struct vm_area_structt * mmap

3、進程虛擬內存區域具有相同屬性的段用結構vm_area_struct描述(簡稱爲VMA)。進程所所有的VMA?


樹組織。

4、每個VMA就是一個對象,定義了一組操作,可以通過這組操作來對不同類型的VMA進行不屯 的處理。

例如對vmalloc分配的內存的映射就是通過其中的nopage操作實現的。

二、mmap處理過程

當用戶調用mmap的時候,內核進行如下的處理:

1、先在進程的虛擬空間查找一塊VMA

2、將這塊VMA去映射

3、如果設備驅動程序或者文件系統的file_operations定義了mmap操作,則調用它

4、將這個VMA插入到進程的VMA鏈中

file_operations的中定義的mmap方法原型如下:
int (*mmap) (struct file *, struct vm_area_struct *);

其中file是虛擬空間映射到的文件結構,vm_area_struct就是步驟1中找到的VMA

三、缺頁故障處理過程

當訪問一個無效的虛擬地址(可能是保護故障,也可能缺頁故障等)的時候,就會產生一個個頁故障,?

統的處理過程如下:

1、找到這個虛擬地址所在的VMA

2、如果必要,分配中間頁目錄表和頁表

3、如果頁表項對應的物理頁面不存在,則調用這個VMAnopage方法,它返回物理頁面的paage描述結構

(當然這只是其中的一種情況)

4、針對上面的情況,將物理頁面的地址填充到頁表中

當頁故障處理完後,系統將重新啓動引起故障的指令,然後就可以正常訪問了

下面是VMA的方法:
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, innt

write_access);
};

其中缺頁函數nopageaddress是引起缺頁故障的虛擬地址,area是它所在的VMAwrite_acccess是存取

屬性。

三、具體實現

3.1、對kmalloc分配的內存的映射

kmalloc分配的內存,因爲是一段連續的物理內存,所以它可以簡單的在mmap例程中設置漢 頁表的物

理地址,方法是使用函數remap_page_range。它的原型如下:

int remap_page_range(unsigned long from, unsigned long phys_addr, unsigned long size,

pgprot_t prot)

其中from是映射開始的虛擬地址。這個函數爲虛擬地址空間fromfrom+size之間的範圍構栽 頁表;

phys_addr是虛擬地址應該映射到的物理地址;size是被映射區域的大小;prot是保護標誌?

remap_page_range的處理過程是對fromform+size之間的每一個頁面,查找它所在的頁目侶己 頁表(

必要時建立頁表),清除頁表項舊的內容,重新填寫它的物理地址與保護域。

remap_page_range可以對多個連續的物理頁面進行處理。<<Linux設備驅動程序>>指出,

remap_page_range只能給予對保留的頁和物理內存之上的物理地址的訪問,當對非保留的頁頁使?

remap_page_range時,缺省的nopage處理控制映射被訪問的虛地址處的零頁。所以在分配內內存後,就?

對所分配的內存置保留位,它是通過函數mem_map_reserve實現的,它就是對相應物理頁面?

PG_reserved標誌位。(關於這一點,參見前面的主題爲關於remap_page_range的疑問檔奶致郟?

因爲remap_page_range有上面的限制,所以可以用另外一種方式,就是採用和vmalloc分配檔哪 存同樣

的方法,對缺頁故障進行處理。

3.2、對vmalloc分配的內存的映射


3.2.1
vmalloc分配內存的過程

1)、進行預處理和合法性檢查,例如將分配長度進行頁面對齊,檢查分配長度是否過大?

2)、以GFP_KERNEL爲優先級調用kmalloc分配(GFP_KERNEL用在進程上下文中,所以這裏裏就限制了?

中斷處理程序中調用vmalloc)描述vmalloc分配的內存的vm_struct結構。

3)、將size加一個頁面的長度,使中間形成4K的隔離帶,然後在VMALLOC_STARTVMALLOOC_END之間

編歷vmlist鏈表,尋找一段自由內存區間,將其地址填入vm_struct結構中

4)、返回這個地址

vmalloc分配的物理內存並不連續

3.2.2、頁目錄與頁表的定義

typedef struct { unsigned long pte_low; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low)

3.2.3、常見例程:

1)、virt_to_phys():內核虛擬地址轉化爲物理地址
#define __pa(x)  ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
{
return __pa(address);
}

上面轉換過程是將虛擬地址減去3GPAGE_OFFSET=0XC000000,因爲內核空間從3G3G+實實際內存一?

映射到物理地址的0到實際內存

2)、phys_to_virt():內核物理地址轉化爲虛擬地址
#define __va(x)  ((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void * phys_to_virt(unsigned long address)
{
return __va(address);
}
virt_to_phys()
phys_to_virt()都定義在include/asm-i386/io.h

3)、#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))(內核核2.4?
   #define VALID_PAGE(page) ((page - mem_map) < max_mapnr)
(內核2.4

第一個宏根據虛擬地址,將其轉換爲相應的物理頁面的page描述結構,第二個宏判斷頁面是是不是在有?

的物理頁面內。(這兩個宏處理的虛擬地址必須是內核虛擬地址,例如kmalloc返回的地址#?

vmalloc返回的地址並不能這樣,因爲vmalloc分配的並不是連續的物理內存,中間可能有空空洞?

3.2.4vmalloc分配的內存的mmap的實現:

vmalloc分配的內存需要通過設置相應VMAnopage方法來實現,當產生缺頁故障的時候,,會調用VM

nopage方法,我們的目的就是在nopage方法中返回一個page結構的指針,爲此,需要通過過如下步驟?

1 pgd_offset_k或者 pgd_offset:查找虛擬地址所在的頁目錄表,前者對應內核空間檔男 擬地址

,後者對應用戶空間的虛擬地址
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
#define pgd_offset_k(address) pgd_offset(&init_mm, address)
對於後者,init_mm是進程0idle process)的虛擬內存mm_struct結構,所有進程的內核 頁表都一樣

。在vmalloc分配內存的時候,要刷新內核頁目錄表,2.4中爲了節省開銷,只更改了進程0檔哪 核頁目

錄,而對其它進程則通過訪問時產生頁面異常來進行更新各自的內核頁目錄

2pmd_offset:找到虛擬地址所在的中間頁目錄項。在查找之前應該使用pgd_none判斷適 否存在相

應的頁目錄項,這些函數如下:
extern inline int pgd_none(pgd_t pgd)  { return 0; }
extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
return (pmd_t *) dir;
}

3pte_offset:找到虛擬地址對應的頁表項。同樣應該使用pmd_none判斷是否存在相應檔 中間頁目

錄:
#define pmd_val(x) ((x).pmd)
#define pmd_none(x) (!pmd_val(x))
#define __pte_offset(address) /
  ((address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pmd_page(pmd) /
  ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
#define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + /
  __pte_offset(address))

4pte_presentpte_page:前者判斷頁表對應的物理地址是否有效,後者取出頁表中物物理地址對?

page描述結構
#define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE))
#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))
#define page_address(page) ((page)->virtual)

 


下面的一個DEMO與上面的關係不大,它是做這樣一件事情,就是在啓動的時候保留一段內存存,然後使?

ioremap將它映射到內核虛擬空間,同時又用remap_page_range映射到用戶虛擬空間,這樣亮 邊都能訪

問,通過內核虛擬地址將這段內存初始化串"abcd",然後使用用戶虛擬地址讀出來。

/************mmap_ioremap.c**************/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/wrapper.h>  /* for mem_map_(un)reserve */
#include <asm/io.h>          /* for virt_to_phys */
#include <linux/slab.h>   /* for kmalloc and kfree */

MODULE_PARM(mem_start,"i");
MODULE_PARM(mem_size,"i");

static int mem_start=101,mem_size=10;
static char * reserve_virt_addr;
static int major;

int mmapdrv_open(struct inode *inode, struct file *file);
int mmapdrv_release(struct inode *inode, struct file *file);
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma);

static struct file_operations mmapdrv_fops =
{
  owner:   THIS_MODULE,
  mmap:    mmapdrv_mmap,
  open:    mmapdrv_open,
  release: mmapdrv_release,
};


int init_module(void)
{
  if ( ( major = register_chrdev(0, "mmapdrv", &mmapdrv_fops) ) < 0 )
    {
      printk("mmapdrv: unable to register character device/n");
      return (-EIO);
    }
  printk("mmap device major = %d/n",major );

  printk( "high memory physical address 0x%ldM/n",
   virt_to_phys(high_memory)/1024/1024 );

  reserve_virt_addr = ioremap( mem_start*1024*1024,mem_size*1024*1024);
  printk( "reserve_virt_addr = 0x%lx/n", (unsigned long)reserve_virt_addr );
  if ( reserve_virt_addr )
    {
      int i;
      for ( i=0;i<mem_size*1024*1024;i+=4)
{
   reserve_virt_addr[i] = 'a';
   reserve_virt_addr[i+1] = 'b';
   reserve_virt_addr[i+2] = 'c';
   reserve_virt_addr[i+3] = 'd';
}
    }
  else
    {
      unregister_chrdev( major, "mmapdrv" );
      return -ENODEV;
    }

  return 0;
}

/* remove the module */
void cleanup_module(void)
{
  if ( reserve_virt_addr )
    iounmap( reserve_virt_addr );

  unregister_chrdev( major, "mmapdrv" );

  return;
}

int mmapdrv_open(struct inode *inode, struct file *file)
{
  MOD_INC_USE_COUNT;
  return(0);
}

int mmapdrv_release(struct inode *inode, struct file *file)
{
  MOD_DEC_USE_COUNT;
  return(0);
}

int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
  unsigned long offset = vma->vm_pgoff<<PAGE_SHIFT;
  unsigned long size = vma->vm_end - vma->vm_start;

  if ( size > mem_size*1024*1024 )
    {
      printk("size too big/n");
      return(-ENXIO);
    }

  offset = offset + mem_start*1024*1024;

  /* we do not want to have this area swapped out, lock it */
  vma->vm_flags |= VM_LOCKED;
  if ( remap_page_range(vma->vm_start,offset,size,PAGE_SHARED))
    {
      printk("remap page range failed/n");
      return -ENXIO;
    }

  return(0);
}


使用LDD2源碼裏面自帶的工具mapper測試結果如下:

[root@localhost modprg]# insmod mmap_ioremap.mod
mmap device major = 254
high memory physical address 0x100M
reserve_virt_addr = 0xc7038000

[root@localhost modprg]# mknod mmapdrv c 254 0

[root@localhost modprg]# ./mapper mmapdrv 0 1024 | od -Ax -t x1
mapped "mmapdrv" from 0 to 1024
000000 61 62 63 64 61 62 63 64 61 62 63 64 61 62 63 64
*
000400

[root@localhost modprg]#

 

 

 

 

[目錄]




內核頁目錄的初始化


內核頁目錄的初始化

內核頁目錄的初始化

/* swapper_pg_dir is the main page directory, address 0x00101000*/

>>> 內核頁目錄,第01項和第768767項均爲映射到物理內存0-8M的頁目錄項
>>>
其頁表的物理地址是0x001020000x00103000,即下面的pg0pg1所在的位置
>>>
(在啓動的時候,將內核映像移到0x0010000處)。
>>>
之所以第01項與第768767相同,是因爲在開啓分頁前的線性地址0-8M和開啓
>>>
分頁之後的3G-3G+8M均映射到相同的物理地址0-8M

/*
* This is initialized to create an identity-mapping at 0-8M (for bootup
* purposes) and another mapping of the 0-8M area at virtual address
* PAGE_OFFSET.
*/
.org 0x1000
ENTRY(swapper_pg_dir)
.long 0x00102007
.long 0x00103007
.fill BOOT_USER_PGD_PTRS-2,4,0
/* default: 766 entries */
.long 0x00102007
.long 0x00103007
/* default: 254 entries */
.fill BOOT_KERNEL_PGD_PTRS-2,4,0

/*
* The page tables are initialized to only 8MB here - the final page
* tables are set up later depending on memory size.
*/
>>>
下面爲物理地址0-8M的頁表項

>>>
0x40000x20002k個頁表項,映射0-8M的物理內存

.org 0x2000
ENTRY(pg0)

.org 0x3000
ENTRY(pg1)

/*
* empty_zero_page must immediately follow the page tables ! (The
* initialization loop counts until empty_zero_page)
*/

.org 0x4000
ENTRY(empty_zero_page)

>>> 進程0的頁目錄指向swapper_pg_dir
#define INIT_MM(name) /
{        /
mmap:  &init_mmap,    /
mmap_avl: NULL,     /
mmap_cache: NULL,     /
pgd:  swapper_pg_dir,   /
mm_users: ATOMIC_INIT(2),   /
mm_count: ATOMIC_INIT(1),   /
map_count: 1,     /
mmap_sem: __RWSEM_INITIALIZER(name.mmap_sem), /
page_table_lock: SPIN_LOCK_UNLOCKED,   /
mmlist:  LIST_HEAD_INIT(name.mmlist), /
}

/*
* paging_init() sets up the page tables - note that the first 8MB are
* already mapped by head.S.
*
* This routines also unmaps the page at virtual kernel address 0, so
* that we can trap those pesky NULL-reference errors in the kernel.
*/
void __init paging_init(void)
{
pagetable_init();

__asm__( "movl %%ecx,%%cr3/n" ::"c"(__pa(swapper_pg_dir)));

。。。。。。。。。。。
}


static void __init pagetable_init (void)
{
unsigned long vaddr, end;
pgd_t *pgd, *pgd_base;
int i, j, k;
pmd_t *pmd;
pte_t *pte, *pte_base;

>>> end虛擬空間的最大值(最大物理內存+3G
/*
  * This can be zero as well - no problem, in that case we exit
  * the loops anyway due to the PTRS_PER_* conditions.
  */
end = (unsigned long)__va(max_low_pfn*PAGE_SIZE);

pgd_base = swapper_pg_dir;
#if CONFIG_X86_PAE
for (i = 0; i < PTRS_PER_PGD; i++)
  set_pgd(pgd_base + i, __pgd(1 + __pa(empty_zero_page)));
#endif
>>>
內核起始虛擬空間在內核頁目錄表中的索引

i = __pgd_offset(PAGE_OFFSET);
pgd = pgd_base + i;

>>> #define PTRS_PER_PGD 1024
>>>
對頁目錄的從768項開始的每一項

for (; i < PTRS_PER_PGD; pgd++, i++) {
>>> vaddr
爲第i項頁目錄項所映射的內核空間的起始虛擬地址,PGDIR_SIZE=4M
  vaddr = i*PGDIR_SIZE;
  if (end && (vaddr >= end))
   break;
#if CONFIG_X86_PAE
  pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
  set_pgd(pgd, __pgd(__pa(pmd) + 0x1));
#else
>>>
對兩級映射機制,pmd實際上是
pgd
  pmd = (pmd_t *)pgd;
#endif
  if (pmd != pmd_offset(pgd, 0))
   BUG();

  for (j = 0; j < PTRS_PER_PMD; pmd++, j++) {
   vaddr = i*PGDIR_SIZE + j*PMD_SIZE;
   if (end && (vaddr >= end))
    break;
>>>
假如內核不支持
Page Size Extensions
   if (cpu_has_pse) {
  
。。。。。。。。。。

   }
>>>
分配內核頁表
   pte_base = pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
>>>
對每一項頁表項
   for (k = 0; k < PTRS_PER_PTE; pte++, k++) {
    vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE;
    if (end && (vaddr >= end))
     break;
>>>
將頁面的物理地址填入頁表項中
    *pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL);
   }
>>>
將頁表的物理地址填入到頁目錄項中
   set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base)));
   if (pte_base != pte_offset(pmd, 0))
    BUG();

  }
}

/*
  * Fixed mappings, only the page table structure has to be
  * created - mappings will be set by set_fixmap():

  */
vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
fixrange_init(vaddr, 0, pgd_base);

#if CONFIG_HIGHMEM
。。。。。。。。。。。。

#endif

#if CONFIG_X86_PAE
。。。。。。。。。。。。

#endif
}

 

 

[目錄]




內核線程頁目錄的借用


   
創建內核線程的時候,由於內核線程沒有用戶空間,而所有進程的內核頁目錄都是一樣的((某些情況下可能有不同步的情況出現,主要是爲了減輕同步所有進程內核頁目錄的開銷,而只是在各個進程要訪問內核空間,如果有不同步的情況,然後才進行同步處理),所以創建的內核線程的內核頁目錄總是借用進程0的內核頁目錄。

>>> kernel_thread以標誌CLONE_VM調用clone系統調用
/*
* Create a kernel thread
*/
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
long retval, d0;

__asm__ __volatile__(
  "movl %%esp,%%esi/n/t"
  "int $0x80/n/t"  /* Linux/i386 system call */
  "cmpl %%esp,%%esi/n/t" /* child or parent? */
  /* Load the argument into eax, and push it.  That way, it does
   * not matter whether the called function is compiled with
   * -mregparm or not.  */
  "movl %4,%%eax/n/t"
  "pushl %%eax/n/t"
  "call *%5/n/t"  /* call fn */
  "movl %3,%0/n/t" /* exit */
  "int $0x80/n"
  "1:/t"
  :"=&a" (retval), "=&S" (d0)
  :"0" (__NR_clone), "i" (__NR_exit),
   "r" (arg), "r" (fn),
   "b" (flags | CLONE_VM)
  : "memory");
return retval;
}

>>> sys_clone->do_fork->copy_mm
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;

。。。。。。。。

tsk->mm = NULL;
tsk->active_mm = NULL;

/*
  * Are we cloning a kernel thread?
  *
  * We need to steal a active VM for that..
  */
>>>
如果是內核線程的子線程(mm=NULL),則直接退出,此時內核線程mmactive_mm均爲爲
NULL
oldmm = current->mm;
if (!oldmm)
  return 0;

>>> 內核線程,只是增加當前進程的虛擬空間的引用計數
if (clone_flags & CLONE_VM) {
  atomic_inc(&oldmm->mm_users);
  mm = oldmm;
  goto good_mm;
}

。。。。。。。。。。

good_mm:
>>>
內核線程的mmactive_mm指向當前進程的mm_struct結構

tsk->mm = mm;
tsk->active_mm = mm;
return 0;

。。。。。。。
}

然後內核線程一般調用daemonize來釋放對用戶空間的引用:
>>> daemonize->exit_mm->_exit_mm

/*
* Turn us into a lazy TLB process if we
* aren't already..
*/
static inline void __exit_mm(struct task_struct * tsk)
{
struct mm_struct * mm = tsk->mm;

mm_release();
if (mm) {
  atomic_inc(&mm->mm_count);
  if (mm != tsk->active_mm) BUG();
  /* more a memory barrier than a real lock */
  task_lock(tsk);
>>>
釋放用戶虛擬空間的數據結構

  tsk->mm = NULL;
  task_unlock(tsk);
  enter_lazy_tlb(mm, current, smp_processor_id());

>>> 遞減mm的引用計數並是否爲0,是則釋放mm所代表的映射
  mmput(mm);
}
}

asmlinkage void schedule(void)
{
。。。。。。。。。

if (!current->active_mm) BUG();

。。。。。。。。。

prepare_to_switch();
{
  struct mm_struct *mm = next->mm;
  struct mm_struct *oldmm = prev->active_mm;
>>> mm = NULL
,選中的爲內核線程

  if (!mm) {
>>>
對內核線程,active_mm = NULL,否則一定是出錯了
   if (next->active_mm) BUG();
>>>
選中的內核線程active_mm借用老進程的active_mm
   next->active_mm = oldmm;
   atomic_inc(&oldmm->mm_count);
   enter_lazy_tlb(oldmm, next, this_cpu);
  } else {
>>> mm != NULL
選中的爲用戶進程,active_mm必須與mm相等,否則一定是出錯了

   if (next->active_mm != mm) BUG();
   switch_mm(oldmm, mm, next, this_cpu);
  }

>>> prev = NULL ,切換出去的是內核線程
  if (!prev->mm) {
>>>
設置其 active_mm = NULL
   prev->active_mm = NULL;
   mmdrop(oldmm);
  }
}

}

對內核線程的虛擬空間總結一下:
1、創建的時候:
父進程是用戶進程,則mmactive_mm均共享父進程的,然後內核線程一般調用daemonize適頭舖m
父進程是內核線程,則mmactive_mm均爲
NULL
總之,內核線程的mm = NULL;進程調度的時候以此爲依據判斷是用戶進程還是內核線程。

2、進程調度的時候
如果切換進來的是內核線程,則置active_mm爲切換出去的進程的active_mm
如果切換出去的是內核線程,則置active_mmNULL

 

 

 

[目錄]




用戶進程內核頁目錄的建立


用戶進程內核頁目錄的建立

    fork一個進程的時候,必須建立進程自己的內核頁目錄項(內核頁目錄項要
與用戶空間的的頁目錄放在同一個物理地址連續的頁面上,所以不能共享,但
所有進程的內核頁表與進程0共享?


3G
用戶,頁目錄中一項映射4M的空間(一項頁目錄1024項頁表,每項頁表對應1個頁面4K)# 即:
#define PGDIR_SHIFT 22
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)

>>> sys_fork->do_fork->copy_mm->mm_init->pgd_alloc->get_pgd_slow

#if CONFIG_X86_PAE

。。。。。。。。。。。。。

#else

extern __inline__ pgd_t *get_pgd_slow(void)
{
>>>
分配頁目錄表(包含1024項頁目錄),即爲一個進程分配的頁目錄可以映射的空間爲
10024*4M=4G
pgd_t *pgd = (pgd_t *)__get_free_page(GFP_KERNEL);

if (pgd) {
>>> #define USER_PTRS_PER_PGD (TASK_SIZE/PGDIR_SIZE)
>>> TASK_SIZE
3G大小,USER_PTRS_PER_PGD爲用戶空間對應的頁目錄項數目(
3G/4M=768?
>>>
將用戶空間的頁目錄項清空

  memset(pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
>>>
將內核頁目錄表(swapper_pg_dir)的第768項到1023項拷貝到進程的頁目錄表的第7688項到1023項中
  memcpy(pgd + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER__PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
}
return pgd;
}

#endif

 

 

 

[目錄]




內核頁目錄的同步


內核頁目錄的同步

    當一個進程在內核空間發生缺頁故障的時候,在其處理程序中,就要通過0號進程的頁目錄覽 同步本進程的內核頁目錄,實際上就是拷貝0號進程的內核頁目錄到本進程中(內核頁表與進程0共享,故不需要複製)。如下:
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
。。。。。。。。
>>>
缺頁故障產生的地址
/* get the address */
__asm__("movl %%cr2,%0":"=r" (address));

tsk = current;

/*
  * We fault-in kernel-space virtual memory on-demand. The
  * 'reference' page table is init_mm.pgd.
  */
>>>
如果缺頁故障在內核空間

if (address >= TASK_SIZE)
  goto vmalloc_fault;

。。。。。。。。。

vmalloc_fault:
{
  /*
   * Synchronize this task's top level page-table
   * with the 'reference' page table.
   */
  int offset = __pgd_offset(address);
  pgd_t *pgd, *pgd_k;
  pmd_t *pmd, *pmd_k;

  pgd = tsk->active_mm->pgd + offset;
  pgd_k = init_mm.pgd + offset;

>>> /*
>>>  * (pmds are folded into pgds so this doesnt get actually called,
>>>  * but the define is needed for a generic inline function.)
>>>  */
>>> #define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)
>>> #define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)

>>> 如果本進程的該地址的內核頁目錄不存在
  if (!pgd_present(*pgd)) {
>>>
如果進程0的該地址處的內核頁目錄也不存在,則出錯
   if (!pgd_present(*pgd_k))
    goto bad_area_nosemaphore;
>>>
複製進程0的該地址的內核頁目錄到本進程的相應頁目錄中
   set_pgd(pgd, *pgd_k);
   return;
  }
>>> extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
>>> {
>>>  return (pmd_t *) dir;
>>> }
  pmd = pmd_offset(pgd, address);
  pmd_k = pmd_offset(pgd_k, address);

>>> 對中間頁目錄,如果是兩級頁表,下面的幾步操作與上面的重複
  if (pmd_present(*pmd) || !pmd_present(*pmd_k))
   goto bad_area_nosemaphore;
  set_pmd(pmd, *pmd_k);
  return;
}


/*
* Switch to real mode and then execute the code
* specified by the code and length parameters.
* We assume that length will aways be less that 100!
*/
void machine_real_restart(unsigned char *code, int length)
{

。。。。。。。。。。。。。

/* Remap the kernel at virtual address zero, as well as offset zero
    from the kernel segment.  This assumes the kernel segment starts at
    virtual address PAGE_OFFSET. */

memcpy (swapper_pg_dir, swapper_pg_dir + USER_PGD_PTRS,
  sizeof (swapper_pg_dir [0]) * KERNEL_PGD_PTRS);


/* Make sure the first page is mapped to the start of physical memory.
    It is normally not mapped, to trap kernel NULL pointer dereferences. */

pg0[0] = _PAGE_RW | _PAGE_PRESENT;

/*
  * Use `swapper_pg_dir' as our page directory.
  */
asm volatile("movl %0,%%cr3": :"r" (__pa(swapper_pg_dir)));

 

[目錄]




mlock代碼分析


       
系統調用mlock的作用是屏蔽內存中某些用戶進程所要求的頁。
        mlock
調用的語法爲:
                int sys_mlock(unsigned long start, size_t len);
初始化爲:
        len=(len+(start &~PAGE_MASK)+ ~PAGE_MASK)&PAGE_MASK;
start &=PAGE_MASK;
其中mlock又調用do_mlock(),語法爲:
int do_mlock(unsigned long start, size_t len,int on);
初始化爲:
        len=(len+~PAGE_MASK)&PAGE_MASK;

    mlock的參數可看出,mlock對由start所在頁的起始地址開始,長度爲len(注:len=(len+(start&~PAGE_MASK)+ ~PAGE_MASK)&PAGE_MASK)的內存區域的頁進行加鎖。
    sys_mlock
如果調用成功返回,這其中所有的包含具體內存區域的頁必須是常駐內存的,或者說在調用munlock munlockall之前這部分被鎖住的頁面必須保留在內存。當然,如果調用mlock的進程終止或者調用exec執行其他程序,則這部分被鎖住的頁面被釋放。通過fork()調用所創建的子進程不能夠繼承由父進程調用mlock鎖住的頁面。
   
內存屏蔽主要有兩個方面的應用:實時算法和高度機密數據的處理。實時應用要求嚴格的分時,比如調度,調度頁面是程序執行延時的一個主要因素。保密安全軟件經常處理關鍵字節,比如密碼或者密鑰等數據結構。頁面調度的結果是有可能將這些重要字節寫到外存(如硬盤)中去。這樣一些黑客就有可能在這些安全軟件刪除這些在內存中的數據後還能訪問部分在硬盤中的數據。        而對內存進行加鎖完全可以解決上述難題。
   
內存加鎖不使用壓棧技術,即那些通過調用mlock或者mlockall被鎖住多次的頁面可以通過調用一次munlock或者munlockall釋放相應的頁面
    mlock
的返回值分析:若調用mlock成功,則返回0;若不成功,則返回-1,並且errno被置位,進程的地址空間保持原來的狀態。返回錯誤代碼分析如下:
    ENOMEM
:部分具體地址區域沒有相應的進程地址空間與之對應或者超出了進程所允許的最大可鎖頁面。
    EPERM
:調用mlock的進程沒有正確的優先權。只有root進程才允許鎖住要求的頁面。
    EINVAL
:輸入參數len不是個合法的正數。


mlock
所用到的主要數據結構和重要常量

1.mm_struct
struct mm_struct {
        int count;
        pgd_t * pgd; /*
進程頁目錄的起始地址,如圖2-3所示
*/
        unsigned long context;
        unsigned long start_code, end_code, start_data, end_data;
        unsigned long start_brk, brk, start_stack, start_mmap;
        unsigned long arg_start, arg_end, env_start, env_end;
        unsigned long rss, total_vm, locked_vm;
        unsigned long def_flags;
        struct vm_area_struct * mmap;     /*
指向vma雙向鏈表的指針
*/
        struct vm_area_struct * mmap_avl; /*
指向vma AVL樹的指針
*/
        struct semaphore mmap_sem;
}
start_code
end_code:進程代碼段的起始地址和結束地址。

start_data
end_data:進程數據段的起始地址和結束地址。
arg_start
arg_end:調用參數區的起始地址和結束地址。
env_start
env_end:進程環境區的起始地址和結束地址。
rss
:進程內容駐留在物理內存的頁面總數。


2.
虛存段(vma)數據結構:vm_area_atruct

虛存段vma由數據結構vm_area_atruct(include/linux/mm.h)描述:
struct vm_area_struct {
        struct mm_struct * vm_mm;        /* VM area parameters */
        unsigned long vm_start;
        unsigned long vm_end;
        pgprot_t vm_page_prot;
        unsigned short vm_flags;
/* AVL tree of VM areas per task, sorted by address */
        short vm_avl_height;
        struct vm_area_struct * vm_avl_left;
        struct vm_area_struct * vm_avl_right;
/* linked list of VM areas per task, sorted by address */
        struct vm_area_struct * vm_next;
/* for areas with inode, the circular list inode->i_mmap */
/* for shm areas, the circular list of attaches */
/* otherwise unused */
        struct vm_area_struct * vm_next_share;
        struct vm_area_struct * vm_prev_share;
/* more */
        struct vm_operations_struct * vm_ops;
        unsigned long vm_offset;
        struct inode * vm_inode;
        unsigned long vm_pte;                        /* shared mem */
};

vm_start;//所對應內存區域的開始地址
vm_end; //
所對應內存區域的結束地址
vm_flags; //
進程對所對應內存區域的訪問權限
vm_avl_height;//avl
樹的高度
vm_avl_left; //avl
樹的左兒子
vm_avl_right; //avl
樹的右兒子
vm_next;//
進程所使用的按地址排序的vm_area鏈表指針
vm_ops;//
一組對內存的操作
   
這些對內存的操作是當對虛存進行操作的時候Linux系統必須使用的一組方法。比如說,當進程準備訪問某一虛存區域但是發現此區域在物理內存不存在時(缺頁中斷),就激發某種對內存的操作執行正確的行爲。這種操作是空頁(nopage)操作。當Linux系統按需調度可執行的頁面映象進入內存時就使用這種空頁(nopage)操作。
   
當一個可執行的頁面映象映射到進程的虛存地址時,一組vm_area_struct結構的數據結構(vma)就會生成。每一個vm_area_struct的數據結構(vma)代表可執行的頁面映象的一部分:可執行代碼,初始化數據(變量),非初始化數據等等。Linux系統可以支持大量的標準虛存操作,當vm_area_struct數據結構(vma)一被創建,它就對應於一組正確的虛存操作。
   
屬於同一進程的vma段通過vm_next指針連接,組成鏈表。如圖2-3所示,struct mm_struct結構的成員struct vm_area_struct * mmap  表示進程的vma鏈表的表頭。
   
爲了提高對vma 查詢、插入、刪除操作的速度,LINUX同時維護了一個AVLAdelson-Velskii and Landis)樹。在樹中,所有的vm_area_struct虛存段均有左指針vm_avl_left指向相鄰的低地址虛存段,右指針vm_avl_right指向相鄰的高地址虛存段,如圖2-5struct mm_struct結構的成員struct vm_area_struct * mmap_avl表示進程的AVL樹的根,vm_avl_height表示AVL樹的高度。
   
對平衡樹mmap_avl的任何操作必須滿足平衡樹的一些規則:
Consistency and balancing rulesJ
(一致性和平衡規則):

tree->vm_avl_height==1+max(heightof(tree->vm_avl_left),heightof(
tree->vm_avl_right))
abs( heightof(tree->vm_avl_left) - heightof(tree->vm_avl_right) ) <= 1
foreach node in tree->vm_avl_left: node->vm_avl_key <= tree->vm_avl_key,        foreach node in tree->vm_avl_right: node->vm_avl_key >= tree->vm_avl_key.
       
注:其中node->vm_avl_key= node->vm_end

vma可以進行加鎖、加保護、共享和動態擴展等操作。

3.重要常量
    mlock
系統調用所用到的重要常量有:PAGE_MASKPAGE_SIZEPAGE_SHIFTRLIMIT_MEMLOCKVM_LOCKED PF_SUPERPRIV等。它們的值分別如下:
        PAGE_SHIFT                       
2                                // PAGE_SHIFT determines the page size
        PAGE_SIZE                        0x1000                        //1UL<<PAGE_SHIFT
        PAGE_MASK                        ~(PAGE_SIZE-1)        //a very useful constant variable
        RLIMIT_MEMLOCK                8                                //max locked-in-memory address space
        VM_LOCKED                        0x2000                        //8*1024=8192, vm_flags
的標誌之一。

        PF_SUPERPRIV                0x00000100                //512,

 

mlock系統調用代碼函數功能分析

下面對各個函數的功能作詳細的分析((1)和(2)在前面簡介mlock時已介紹過,並在後面有詳細的程序流程)
suser()
:如果用戶有效(即current->euid == 0        ),則設置進程標誌爲root優先權(current->flags |= PF_SUPERPRIV),並返回1;否則返回0
find_vma(struct mm_struct * mm, unsigned long addr)
:輸入參數爲當前進程的mm、需要加鎖的開始內存地址addrfind_vma的功能是在mmmmap_avl樹中尋找第一個滿足mm->mmap_avl->vm_start<=addr< mm->mmap_avl->vm_endvma,如果成功則返回此vma;否則返回空null
mlock_fixup(struct vm_area_struct * vma, unsigned long start, unsigned long end, unsigned int newflags)
:輸入參數爲vm_mmap鏈中的某個vma、需要加鎖內存區域起始地址和結束地址、需要修改的標誌(0:加鎖,1:釋放鎖)。
merge_segments(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
:輸入參數爲當前進程的mm、需要加鎖的開始內存地址start_addr和結束地址end_addrmerge_segments的功能的是盡最大可能歸併相鄰(即內存地址偏移量連續)並有相同屬性(包括vm_inode,vm_pte,vm_ops,vm_flags)的內存段,在這過程中冗餘的vm_area_structs被釋放,這就要求vm_mmap鏈按地址大小排序(我們不需要遍歷整個表,而只需要遍歷那些交叉或者相隔一定連續區域的鄰接vm_area_structs)。當然在缺省的情況下,merge_segments是對vm_mmap_avl樹進行循環處理,有多少可以合併的段就合併多少。
mlock_fixup_all(struct vm_area_struct * vma, int newflags)
:輸入參數爲vm_mmap鏈中的某個vma、需要修改的標誌(0:加鎖,1:釋放鎖)。mlock_fixup_all的功能是根據輸入參數newflags修改此vmavm_flags
mlock_fixup_start(struct vm_area_struct * vma,unsigned long end, int newflags)
:輸入參數爲vm_mmap鏈中的某個vma、需要加鎖內存區域結束地址、需要修改的標誌(0:加鎖,1:釋放鎖)。mlock_fixup_start的功能是根據輸入參數end,在內存中分配一個新的new_vma,把原來的vma分成兩個部分: new_vmavma,其中new_vmavm_flags被設置成輸入參數newflags;並且按地址(new_vma->startnew_vma->end)大小序列把新生成的new->vma插入到當前進程mmmmap鏈或mmap_avl樹中(缺省情況下是插入到mmap_avl樹中)。
       
注:vma->vm_offset+= vma->vm_start-new_vma->vm_start;
mlock_fixup_end(struct vm_area_struct * vma,unsigned long start, int newflags)
:輸入參數爲vm_mmap鏈中的某個vma、需要加鎖內存區域起始地址、需要修改的標誌(0:加鎖,1:釋放鎖)。mlock_fixup_end的功能是根據輸入參數start,在內存中分配一個新的new_vma,把原來的vma分成兩個部分:vmanew_vma,其中new_vmavm_flags被設置成輸入參數newflags;並且按地址大小序列把new->vma插入到當前進程mmmmap鏈或mmap_avl樹中。

       
注:new_vma->vm_offset= vma->vm_offset+(new_vma->vm_start-vma->vm_start);
mlock_fixup_middle(struct vm_area_struct * vma,unsigned long start, unsigned long end, int newflags)
:輸入參數爲vm_mmap鏈中的某個vma、需要加鎖內存區域起始地址和結束地址、需要修改的標誌(0:加鎖,1:釋放鎖)。mlock_fixup_middle的功能是根據輸入參數startend,在內存中分配兩個新vma,把原來的vma分成三個部分:left_vmavmaright_vma,其中vmavm_flags被設置成輸入參數newflags;並且按地址大小序列把left->vmaright->vma插入到當前進程mmmmap鏈或mmap_avl樹中。

       
注:vma->vm_offset += vma->vm_start-left_vma->vm_start;
                right_vma->vm_offset += right_vma->vm_start-left_vma->vm_start;
kmalloc()
:將在後面3.3中有詳細討論。

insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)
:輸入參數爲當前進程的mm、需要插入的vmpinsert_vm_struct的功能是按地址大小序列把vmp插入到當前進程mmmmap鏈或mmap_avl樹中,並且把vmp插入到vmp->inodei_mmap環(循環共享鏈)中。
avl_insert_neighbours(struct vm_area_struct * new_node,** ptree,** to_the_left,** to_the_right)
:輸入參數爲當前需要插入的新vma結點new_node、目標mmap_avlptree、新結點插入ptree後它左邊的結點以及它右邊的結點(左右邊結點按mmap_avl中各vma->vma_end大小排序)。avl_insert_neighbours的功能是插入新vma結點new_node到目標mmap_avlptree中,並且調用avl_rebalance以保持ptree的平衡樹特性,最後返回new_node左邊的結點以及它右邊的結點。
avl_rebalance(struct vm_area_struct *** nodeplaces_ptr, int count)
:輸入參數爲指向vm_area_struct指針結構的指針數據nodeplaces_ptr[](每個元素表示需要平衡的mmap_avl子樹)、數據元素個數countavl_rebalance的功能是從nodeplaces_ptr[--count]開始直到nodeplaces_ptr[0]循環平衡各個mmap_avl子樹,最終使整個mmap_avl樹平衡。
down(struct semaphore * sem)
:輸入參數爲同步(進入臨界區)信號量semdown的功能根據當前信號量的設置情況加鎖(阻止別的進程進入臨界區)並繼續執行或進入等待狀態(等待別的進程執行完成退出臨界區並釋放鎖)。
        down
定義在/include/linux/sched.h中:
extern inline void down(struct semaphore * sem)
{
        if (sem->count <= 0)
                __down(sem);
        sem->count--;
}
up(struct semaphore * sem)
輸入參數爲同步(進入臨界區)信號量semup的功能根據當前信號量的設置情況(當信號量的值爲負數:表示有某個進程在等待使用此臨界區 )釋放鎖。
        up
定義在/include/linux/sched.h中:
extern inline void up(struct semaphore * sem)
{
        sem->count++;
        wake_up(&sem->wait);
        }
kfree_s(a,b)
kfree_s定義在/include/linux/malloc.h中:#define kfree_s(a,b) kfree(a)。而kfree()將在後面3.3中詳細討論。
avl_neighbours(struct vm_area_struct * node,* tree,** to_the_left,** to_the_right)
:輸入參數爲作爲查找條件的vma結點node、目標mmap_avltreenode左邊的結點以及它右邊的結點(左右邊結點按mmap_avl中各vma->vma_end大小排序)。avl_ neighbours的功能是根據查找條件node在目標mmap_avlptree中找到node左邊的結點以及它右邊的結點,並返回。
avl_remove(struct vm_area_struct * node_to_delete, ** ptree)
:輸入參數爲需要刪除的結點node_to_delete和目標mmap_avlptreeavl_remove的功能是在目標mmap_avlptree中找到結點node_to_delete並把它從平衡樹中刪除,並且調用avl_rebalance以保持ptree的平衡樹特性。
remove_shared_vm_struct(struct vm_area_struct *mpnt)
:輸入參數爲需要從inode->immap環中刪除的vma結點mpntremove_shared_vm_struct的功能是從擁有vma結點mpnt inode->immap環中刪除的該結點。

 

 

 

[目錄]




memory.c


    Memory.c
中,Linux提供了對虛擬內存操作的若干函數,其中包括對虛擬頁的複製、新建頁表、清除頁表、處理缺頁中斷等等。

[目錄]




copy_page


1
static inline void copy_page(unsigned long from, unsigned long to)

    爲了節約內存的使用,在系統中,各進程通常採用共享內存,即不同的進程可以共享同一段代碼段或數據段。當某一進程發生對共享的內存發生寫操作時,爲了不影響其它進程的正常運行,系統將把該內存塊複製一份,供需要寫操作的進程使用,這就是所謂的copy-on-write機制。copy_page就是提供複製內存功能的函數,它調用C語言中標準的內存操作函數,將首地址爲from的一塊虛擬內存頁複製到首地址爲to的空間中。

 

 

[目錄]




clear_page_tables


2
void clear_page_tables(struct task_struct * tsk)
    clear_page_table
的功能是將傳入的結構tsk中的pgd頁表中的所有項都清零,同時將二級頁表所佔的空間都釋放掉。傳入clear_page_tables的是當前進程的tsk結構,取得該進程的一級頁目錄指針pgd後,採用循環的方式,調用free_one_pgd清除pgd表。表共1024項。在free_one_pgd中,實際執行的功能只調用一次free_one_pmd(在80x86中,由於硬件的限制,只有兩級地址映射,故將pmdpgd合併在一起)。在free_one_pmd中,函數調用pte_free將對應於pmd的二級頁表所佔的物理空間釋放掉(進程代碼、數據所用的物理內存在do_munmap釋放掉了)並將pmd賦值爲零。
    clear_page_table
在系統啓動一個可執行文件的映象或載入一個動態鏈接庫時被調用。在fs/exec.c中的do_load_elf_binary()do_load_aout_binary()調用flash_old_exec,後者調用exec_mmap,而exec_mmap調用clear_page_table。其主要功能是當啓動一個新的應用程序的時候,將複製的mm_struct中的頁表清除乾淨,並釋放掉原有的所有二級頁表空間。

 

 

[目錄]




oom


3
void oom(struct task_struct * task)
   
返回出錯信息。

 

 

[目錄]




free_page_tables


4
void free_page_tables(struct mm_struct * mm)
   
free_page_table中,大部分的代碼與clear_page_table中的函數一致。所不同的是,該函數在最後調用了pgd_free(page_dir),即不光釋放掉二級頁表所佔的空間,同時還釋放一級頁目錄所佔的空間。這是因爲free_page_tables__exit_mm調用,__exit_mm又被do_exit kernel/kernel.c)調用。當進程中止、系統退出或系統重起時都需要用do_exit(屬於進程管理)將所有的進程結束掉。在結束進程過程中 ,將調用free_page_table將進程的空間全部釋放掉,當然包括釋放進程一級頁目錄所佔的空間。

 

 

[目錄]




new_page_tables


5
int new_page_tables(struct task_struct * tsk)
   
該函數的主要功能是建立新的頁目錄表,它的主要流程如如下:
    ·
調用pgd_alloc()爲新的頁目錄表申請一片4K空間
    ·
將初始化進程的內存結構中從768項開始到1023項的內容複製給新的頁表(所有的進程都共用虛擬空間中 3G4G的內存,即在覈心態時可以訪問所有相同的存儲空間)。
    ·
調用宏SET_PAGE_DIRinclude/asm/pgtable.h)將進程控制塊tsk->ts->CR3的值改爲新的頁目錄表的首地址,同時將CPU中的CR3寄存器的值改爲新的頁目錄表的首地址,從而使新進程進入自己的運行空間。
    ·
tsk->mm->pgd改爲新的頁目錄表的首地址。
    ·new_page_tables
copy_mm調用,而copy_mmcopy_mm_do_fork調用,這兩個函數都在kernel/fork.c中。同時,new_page_tables也可以在exec_mmapfs/exec.c)中調用。即新的進程的產生可以通過兩種途徑,一種是fork,在程序中動態地生成新的進程,這樣新進程的頁表原始信息利用copy_mm從其父進程中繼承而得,另一種是運行一個可執行文件映象,通過文件系統中的exec.c,將映象複製到tsk結構中。兩種方法都需要調用new_page_tables爲新進程分配頁目錄表。

 

 

[目錄]




copy_one_pte


6
static inline void copy_one_pte(pte_t * old_pte, pte_t * new_pte, int cow)
   
將原pte頁表項複製到new_pte上,其流程如下:
    ·
檢測old_pte是否在內存中,如不在物理內存中,調用swap_duplicateold_pteswap file中的入口地址,將old_pte複製到內存中,同時把old_pte的入口地址賦給new_pte並返回。反之轉向3
獲取old_pte對應的物理地址的頁號。
    ·
根據頁號判斷old_pte是否爲系統保留的,如果爲系統保留的,這些頁爲所有的進程在覈心態下使用,用戶進程沒有寫的權利,則只需將old_pte指針直接轉賦給new_pte後返回。反之則該pte屬於普通內存的,則轉向4
    ·
根據傳入的C-O-W標誌,爲old_pte置寫保護標誌,如果該頁是從swap_cache中得來的,將old_pte頁置上“dirty”標誌。將old_pte賦值給new_pte
    ·
mem_map結構中關於物理內存使用進程的個數的數值count1

 

[目錄]




copy_pte_range


7
static inline int copy_pte_range(pmd_t *dst_pmd, pmd_t *src_pmd,
unsigned long address, unsigned long size, int cow)
   
通過循環調用copy_one_pte將從源src_pmd中以地址address開始的長度爲size的空間複製給dst_pmd中。如dst_pmd中還未分配地址爲address的頁表項,則先給三級頁表pte表分配4K空間。(每調用一次copy_one_pte複製4K空間。在一次copy_pte_range中最多可複製4M空間)。

 

 

[目錄]




copy_pmd_range


8
static inline int copy_pmd_range(pgd_t *dst_pgd, pgd_t *src_pgd,
unsigned long address, unsigned long size, int cow)
   
通過循環調用copy_pte_range將從源src_pgd中以地址address開始的長度爲size的空間複製給dst_pgd中。如dst_pgd中還未分配地址爲address的頁表項,則在一級(同時也是二級)頁表中給對應的pmd分配目錄項。

 

 

[目錄]




copy_page_range


9
int copy_page_range(struct mm_struct *dst, struct mm_struct *src,
                        struct vm_area_struct *vma)
   
該函數的主要功能是將某個任務或進程的vma塊複製給另一個任務或進程。其工作機制是循環調用copy_pmd_range,將vma塊中的所有虛擬空間複製到對應的虛擬空間中。在做複製之前,必須確保新任務對應的被複制的虛擬空間中必須都爲零。copy_page_rangedup_mmap()->copy_mm()->do_fork()的順序被調用(以上三個函數均在kernel/fork.c中)。當進程被創建的時候,需要從父進程處複製所有的虛擬空間,copy_page_range完成的就是這個任務。

 

 

[目錄]




free_pte


9
static inline void free_pte(pte_t page)
   
虛存頁page如在內存中,且不爲系統的保留內存,調用free_page將其釋放掉(如在系統保留區中,則爲全系統共享,故不能刪除)。
   
pageswap file中,調用swap_free()將其釋放。

 

 

[目錄]




forget_pte


10
static inline void forget_pte(pte_t page)
   
page不爲空,調用free_pte將其釋放。

 

 

[目錄]




zap_pte_range


11
static inline void zap_pte_range(pmd_t * pmd, unsigned long address,
unsigned long size)
    zap
zero all pages的縮寫。該函數的作用是將在pmd中從虛擬地址address開始,長度爲size的內存塊通過循環調用pte_clear將其頁表項清零,調用free_pte將所含空間中的物理內存或交換空間中的虛存頁釋放掉。在釋放之前,必須檢查從address開始長度爲size的內存塊有無越過PMD_SIZE.(溢出則可使指針逃出01023的區間)

 

 

[目錄]




zap_pmd_range


12
static inline void zap_pmd_range(pgd_t * dir, unsigned long address, unsigned long size)
   
函數結構與zap_pte_range類似,通過調用zap_pte_range完成對所有落在addressaddress+size區間中的所有pte的清零工作。zap_pmd_range至多清除4M空間的物理內存。

 

 

[目錄]




zap_page_range


13
int zap_page_range(struct mm_struct *mm, unsigned long address, unsigned long size)
   
函數結構與前兩個函數類似。將任務從address開始到address+size長度內的所有對應的pmd都清零。zap_page_range的主要功能是在進行內存收縮、釋放內存、退出虛存映射或移動頁表的過程中,將不在使用的物理內存從進程的三級頁表中清除。(在討論clear_page_tables時,就提到過當進程退出時,釋放頁表之前,先保證將頁表對應項清零,保證在處於退出狀態時,進程不佔用03G的空間。)

 

 

[目錄]




zeromap_pte_range


14
static inline void zeromap_pte_range(pte_t * pte, unsigned long address,
unsigned long size, pte_t zero_pte)
15
static inline int zeromap_pmd_range(pmd_t * pmd, unsigned long address,
unsigned long size, pte_t zero_pte)
16
int zeromap_page_range(unsigned long address, unsigned long size, pgprot_t prot)
   
這三個函數與前面的三個函數從結構上看很相似,他們的功能是將虛擬空間中從地址address開始,長度爲size的內存塊所對應的物理內存都釋放掉,同時將指向這些區域的pte都指向系統中專門開出的長度爲4K,全爲0的物理頁。zeromap_page_rangekernel代碼中沒有被引用,這個函數是舊版本的Linux遺留下來的,在新版本中已經被zap_page_range所替代。

 

 

[目錄]




remap_pte_range


17
static inline void remap_pte_range(pte_t * pte, unsigned long address,
unsigned long size,        unsigned long offset, pgprot_t prot)
18
static inline int remap_pmd_range(pmd_t * pmd, unsigned long address,
unsigned long size,        unsigned long offset, pgprot_t prot)
19
int remap_page_range(unsigned long from, unsigned long offset, unsigned long size,
pgprot_t prot)
   
這三個函數也同前面的函數一樣,層層調用,現僅介紹一下最後一個函數的作用。remap_page_range的功能是將原先被映射到虛擬內存地址from處的,大小爲size的虛擬內存塊映射到以偏移量offset爲起始地址的虛擬內存中,同時將原來的ptepmd項都清零。該函數也是逐級調用,在remap_pte_range中,通過set_pte將的物理頁映射到新的虛擬內存頁表項pte上。remap_page_range函數的功能與下文中的remap.c中介紹的功能相近,因此在kernel中也沒有用到。

 

 

[目錄]




put_dirty_page


20
unsigned long put_dirty_page(struct task_struct * tsk, unsigned long page,
unsigned long address)
   
將虛擬內存頁page鏈接到任務tsk中虛擬地址爲address的虛擬內存中,其主要調用的流程如下:put_dirty_page->setup_arg_page->do_load_xxx_binary(xxxaoutelf,這些函數都在fs/exec.c),它的功能是將在載入可執行文件的時候,將其相關的堆棧信息、環境變量等複製到當前進程的空間上。

 

 

[目錄]




handle_mm_fault


21
void handle_mm_fault(struct vm_area_struct * vma, unsigned long address,
int write_access)
       
用於處理ALPHA機中的缺頁中斷

 

 

[目錄]




mmap.c


   
mmap.c中,主要提供了對進程內存管理進行支持的函數,主要包括了do_mmapdo_munmap等對進程的虛擬塊堆avl數進行管理的函數。

有關avl樹的一些操作:
1
static inline void avl_neighbours (struct vm_area_struct * node, struct vm_area_struct * tree, struct vm_area_struct ** to_the_left, struct vm_area_struct ** to_the_right)
尋找avltree中的節點node的前序節點和後序節點,將結果放在指針to_the_leftto_the_right中,即使得*to_the_left->next=nodenode->next=*to_the_right。在實際搜索中,過程是找到node節點中的左節點的最右節點和右節點的最左節點,採用avl樹搜索可以提高效率。

2static inline void avl_rebalance (struct vm_area_struct *** nodeplaces_ptr, int count)
將由於插入操作或刪除操作而造成不平衡的avl樹恢復成平衡狀態。nodeplaces_ptr是指向的是需要調整的子樹的根節點,count是該子樹的高度。

static inline void avl_insert (struct vm_area_struct * new_node,
struct vm_area_struct ** ptree)
將新節點new_node插入avlptree中,並將該樹重新生成平衡avl樹。在創建avl樹時,將vma模塊不斷的插入avl樹中,構建一個大的avl樹。當進程創建時,複製父進程後需要將以雙向鏈表拷貝過來的vma鏈生成avl樹。

4static inline void avl_insert_neighbours (struct vm_area_struct * new_node, struct vm_area_struct ** ptree,        struct vm_area_struct ** to_the_left, struct vm_area_struct ** to_the_right)
將新節點new_node插入avlptree中,並將該樹重新生成平衡avl樹,同時返回該新節點的前序節點和後序節點。

5static inline void avl_remove (struct vm_area_struct * node_to_delete, struct vm_area_struct ** ptree)
將指定要刪除的節點node_to_deleteavlptree中刪除。並將該樹重新生成平衡avl樹。該函數在釋放虛存空間和歸併vma鏈表是使用。

7static void printk_list (struct vm_area_struct * vma)
8
static void printk_avl (struct vm_area_struct * tree)
9
static void avl_checkheights (struct vm_area_struct * tree)
10
static void avl_checkleft (struct vm_area_struct * tree, vm_avl_key_t key)
11
static void avl_checkright (struct vm_area_struct * tree, vm_avl_key_t key)
12
static void avl_checkorder (struct vm_area_struct * tree)
13
static void avl_check (struct task_struct * task, char *caller)
這些函數都是系統調試時用以檢測avl樹結構的正確性

14static inline int vm_enough_memory(long pages)
通過計算當前系統中所剩的空間判斷是否足夠調用。可使用的內存包括緩衝存儲器、頁緩存、主存中的空閒頁、swap緩存等。

15static inline unsigned long vm_flags(unsigned long prot, unsigned long flags)
提供宏功能將頁的保護位和標誌位合併起來。

16unsigned long get_unmapped_area(unsigned long addr, unsigned long len)
從虛擬內存address開始找到未分配的連續空間大於len的虛擬空間塊,並將該快的首地址返回。

17unsigned long do_mmap(struct file * file, unsigned long addr, unsigned long len,
        unsigned long prot, unsigned long flags, unsigned long off)
do_mmap
Linux虛擬內存管理中是一個很重要的函數,它的主要功能是將可執行文件的映象映射到虛擬內存中,或將別的進程中的內存信息映射到該進程的虛擬空間中。並將映射了的虛擬塊的vma加入到該進程的vma avl樹中。其運行的流程如下,更詳細的分析請參閱林濤同學和徐玫峯同學的報告。

檢驗給定的映射長度len是大於1頁,小於一個任務的最大長度3G且加上進程的加上偏移量off不會溢出。如不滿足則退出。
如果當前任務的內存是上鎖的,檢驗加上len後是否會超過當前進程上鎖長度的界限。如是則退出。
如果從文件映射,檢驗文件是否有讀的權限。如無在退出。
調用get_unmaped取得從地址address開始未映射的連續虛擬空間大於len的虛存塊。
如從文件映射,保證該文件控制塊有相應的映射操作。
爲映射組織該區域申請vma結構。
調用vm_enough_memory有足夠的內存。如無則釋放6中申請的vma,退出。
如果是文件映射,調用file->f_op_mmap將該文件映射如vma中。
調用insert_vm_structvma插入該進程的avl樹中。
歸併該avl樹。

18void merge_segments (struct mm_struct * mm, unsigned long start_addr,
unsigned long end_addr)
經過對進程虛擬空間不斷的映射,在進程中的vma塊有許多是可以合併的,爲了提高avl樹查找的效率,減少avl樹中不必要的vma塊,通常需要將這些塊和並,merge_segments的功能爲合併虛擬空間中從start_addrend_addr中類型相同,首尾相連的vma塊。由於只有經過增加操作採有可能合併,所有merge_segments只在do_mmapunmap_fixup中被調用。該函數的流程如下:

根據起始地址start_addr從找到第一塊滿足vm_end>start_addrvmampnt
調用avl_neighbours找到在vma雙向鏈表上與mpnt前後相連的vmaprevnext
如果prevmpnt首尾相連,且有同樣在swap file中的節點,同樣的標誌,同樣的操作等則將其合併,反之轉向6
調用avl_removempntavl樹中刪除,調整prev的結束地址和後序指針。
將結構mpnt所佔的物理空間刪除。
prev
mpntnext依次下移,如未超過end_addr則返回3

19static void unmap_fixup(struct vm_area_struct *area, unsigned long addr, size_t len)
釋放虛擬空間中的某些區域的時候,將會出現四種情況:

將整個vma釋放掉
vma的前半部分釋放掉
vma的後半部分釋放掉
vma的中間部分釋放掉
爲了正常維護vma樹,當第一種情況是,將整個vma釋放掉。同時釋放vma結構所佔的空間。第二種,釋放後半部分,修改vma的相關信息。第二種,釋放前半部分,修改vma的相關信息。第四種,由於在vma中出現了一個洞,則需增加一個vma結構描述新出現的vma塊。unmap_fixup所執行的工作就是當釋放空間時,修正對vma樹的影響。

20int do_munmap(unsigned long addr, size_t len)
do_munmap
將釋放落在從地址addr開始,長度爲len空間內的vma所對應的虛擬空間。do_munmap被系統調用sys_munmap所調用(對sys_munmap如何工作的不甚瞭解)。下面是該函數的流程:

通過find_vma根據addr找到第一塊vma->end>addrvmampnt
調用avl_neighbours找到mpnt在鏈表中的相鄰指針prevnext
將檢查中所有與虛擬空間addr~addr+len相交的vma塊放入free鏈表中。同時如果該vma鏈接在共享內存中,則將其從該環形鏈表中釋放出來。
按序搜索free鏈表,調用unmap_fixup釋放空間。
調用zap_page_range將指向釋放掉的虛擬空間中的pte頁表項清零。
調用kfree釋放mpnt結構佔用的空間。

remap.c
該文件提供了對虛擬內存重映射的若干函數。在下文中將介紹這些函數的功能,分析這些函數在虛擬內存管理中所起的作用。同時詳細介紹其中主要函數的流程。

static inline pte_t *get_one_pte(struct mm_struct *mm, unsigned long addr)
根據輸入的虛存地址返回其在虛擬內存中的對應的頁表項pte

static inline pte_t *alloc_one_pte(struct mm_struct *mm, unsigned long addr)
根據輸入的虛存地址addrpgd表中根據三級頁表映射機制找pte,如果在pgd表中無對應的項,則分配給一個pgdpmd)表項,在這個表項內分配根據addr分配pte,將pte返回。

static inline int copy_one_pte(pte_t * src, pte_t * dst)
將目的ptedst)表項中的值賦成源ptesrc)中的值,然後將源pte中的值清零,根據這函數的功能取move_one_pte更合適。

static int move_one_page(struct mm_struct *mm,
         unsigned long old_addr, unsigned long new_addr)
根據輸入的虛擬地址old_addr調用get_one_pte獲取該地址在三級頁表中的pte項,調用copy_one_pte將該pte對應的物理頁指針移到根據new_addr對應的pte項上,即在虛擬空間內移動一虛擬內存頁。

static int move_page_tables(struct mm_struct * mm,
         unsigned long new_addr, unsigned long old_addr, unsigned long len)
將虛擬地址空間中從old_addr開始的長度爲len的虛擬內存移動到以new_addr爲起始地點的的虛擬空間中,以下爲該函數的流程:

將所需移動的內存長度len賦值給偏移量offset如果offset0,結束。反之轉向2
將偏移量offset減去一個頁的長度,調用move_one_page將從old_addr+offset開始的一頁移到new_addr+offset。若發生錯誤則轉到4
如果offset不爲0,則轉向1,反之結束。
調用move_one_page將所有已移動到新地址的頁移回源地址,調用zap_page_range將從new_addr開始的移動過的頁pte清零,並返回出錯信息-1

static inline unsigned long move_vma(struct vm_area_struct * vma,
        unsigned long addr, unsigned long old_len, unsigned long new_len)
將虛存中的vmavma的起始地址爲addr,長度爲old_len的內存塊擴展爲長度爲new_len的內存塊,並在虛存中找到可容納長度爲new_len塊的連續區域,返回首地址。其工作流程如下:

給新的vma結構塊new_vma分配空間,如果不成功返回出錯信息。
調用get_unmap_areaaddr開始找到第一個未被利用的虛存空洞,空洞長度大於給定的新的虛擬內存塊的長度len,將其首地址賦給new_addr。如果未招到,則轉向9
調用move_page_tables將從addr開始的長度爲old_len的內存區域移動到以new_addr爲起始地址的虛擬空間中。
修改new_vma塊中關於起始地址,結束地址的值。
將新的new_vma塊插入到當前進程的虛擬內存所鏈成的雙向鏈表和avl樹中。
調用merge_segment將虛擬空間中地址可能連結在一起的不同的vma段連結成一個vma塊,同時刪除冗於的vma塊。
將原有空間中的從addr開始,長度爲old_len的虛擬空間釋放掉。
修改mm結構中的所有虛存的長度,返回新的起始虛擬地址new_addr
vmanew_vma釋放掉並返回出錯信息。

asmlinkage unsigned long sys_mremap(unsigned long addr,         unsigned long old_len,
                    unsigned long new_len        unsigned long flags)
sys_remap
是一個系統調用,其主要功能是擴展或收縮現有的虛擬空間。它的主要工作流程如下:

檢查addr地址是否小於4096,如小於,則非法,返回。
將原始長度old_len和需要擴展或收縮的長度new_len頁對齊。
如果有old_len>new_len,則說明是收縮空間,調用do_munmap將虛存空間中從new_lenold_len的空間釋放掉。返回收縮後的首地址addr
根據addr找到第一塊vma塊滿足vma->end > addr,檢查addr是否落在虛存的空洞中,如是,則返回出錯信息。
檢查需要擴展的內存塊是否落在該vma塊中,越界則返回出錯信息。
如果該vma是上鎖的,則檢測上鎖的內存擴展後是否越界,如是,則7返回出錯信息
檢測當前進程的虛存空間經擴展後是否超過系統給該進程的最大空間。如是,則返回出錯信息。
如果找到vma塊從addr開始到塊末尾的長度爲old_len(old_len的長度不等於new_len或該虛存是不可移動的),則轉向9,反之轉向10
檢測從跟隨找到的vma塊的未分配的空間是否大於需要擴展空間。如果大於,則直接將擴展的空間掛在找到的vma塊後,修改vma塊中相關的信息,並返回擴展後虛擬塊的首地址。如小於轉向10
如果當前虛擬塊是是不可移動的,則返回錯誤信息。反之,調用move_vma將需要擴展的虛擬塊移動可以容納其長度new_len的虛擬空間中。

 

 

[目錄]




夥伴(buddy)算法


    2.4
版內核的頁分配器引入了"頁區"(zone)結構, 一個頁區就是一大塊連續的物理頁面. Linux 2.4將整個物理內存劃分爲3個頁區, DMA頁區(ZONE_DMA), 普通頁區(ZONE_NORMAL)和高端頁區(ZONE_HIGHMEM).

    頁區可以使頁面分配更有目的性, 有利於減少內存碎片. 每個頁區的頁分配仍使用夥伴(buddy)算法.
   
夥伴算法將整個頁區劃分爲以2爲冪次的各級頁塊的集合, 相鄰的同次頁塊稱爲夥伴, 一對夥伴可以合併到更高次頁面集合中去.

下面分析一下夥伴算法的頁面釋放過程.

; mm/page_alloc.c:

#define BAD_RANGE(zone,x) (((zone) != (x)->zone) || (((x)-mem_map) offset) || (((x)-mem_map) >= (zone)->offset+(zone)->size))

#define virt_to_page(kaddr)        (mem_map + (__pa(kaddr) >> PAGE_SHIFT))
#define put_page_testzero(p)         atomic_dec_and_test(

void free_pages(unsigned long addr, unsigned long order)
{        order
是頁塊尺寸指數, 即頁塊的尺寸有(2^order)
.
        if (addr != 0)
                __free_pages(virt_to_page(addr), order);
}
void __free_pages(struct page *page, unsigned long order)
{
        if (!PageReserved(page)  put_page_testzero(page))
                __free_pages_ok(page, order);
}
static void FASTCALL(__free_pages_ok (struct page *page, unsigned long order));
static void __free_pages_ok (struct page *page, unsigned long order)
{
        unsigned long index, page_idx, mask, flags;
        free_area_t *area;
        struct page *base;
        zone_t *zone;

        if (page->buffers)
                BUG();
        if (page->mapping)
                BUG();
        if (!VALID_PAGE(page))
                BUG();
        if (PageSwapCache(page))
                BUG();
        if (PageLocked(page))
                BUG();
        if (PageDecrAfter(page))
                BUG();
        if (PageActive(page))
                BUG();
        if (PageInactiveDirty(page))
                BUG();
        if (PageInactiveClean(page))
                BUG();

        page->flags  ~((1        page->age = PAGE_AGE_START;

        zone = page->zone; page所在的頁區

        mask = (~0UL)         base = mem_map + zone->offset; 求頁區的起始頁
        page_idx = page - base;
page在頁區內的起始頁號
        if (page_idx  ~mask)
頁號必須在頁塊尺寸邊界上對齊
                BUG();
        index = page_idx >> (1 + order);
                ;
求頁塊在塊位圖中的索引, 每一索引位置代表相鄰兩個"夥伴"
        area = zone->free_area + order;
取該指數頁塊的位圖平面

        spin_lock_irqsave( flags);

        zone->free_pages -= mask; 頁區的自由頁數加上將釋放的頁數(掩碼值爲負)

        while (mask + (1                 struct page *buddy1, *buddy2;

                if (area >= zone->free_area + MAX_ORDER) 如果超過了最高次平面
                        BUG();
                if (!test_and_change_bit(index, area->map))
測試並取反頁塊的索引位
                        /*
                        * the buddy page is still allocated.
                        */
                        break;
如果原始位爲0, 則說明該頁塊原來沒有夥伴, 操作完成
                /*
                * Move the buddy up one level.
如果原始位爲1, 則說明該頁塊存在一個夥伴
                */
                buddy1 = base + (page_idx ^ -mask);
對頁塊號邊界位取反,得到夥伴的起點
                buddy2 = base + page_idx;

                if (BAD_RANGE(zone,buddy1)) 夥伴有沒有越過頁區範圍
                        BUG();
                if (BAD_RANGE(zone,buddy2))
                        BUG();

                memlist_del( 刪除夥伴的自由鏈
                mask                 area++;
求更高次位圖平面
                index >>= 1;
求更高次索引號
                page_idx  mask;
求更高次頁塊的起始頁號
        }
        memlist_add_head( + page_idx)->list, 
將求得的高次頁塊加入該指數的自由鏈

        spin_unlock_irqrestore( flags);

        /*
        * We don't want to protect this variable from race conditions
        * since it's nothing important, but we do want to make sure
        * it never gets negative.
        */
        if (memory_pressure > NR_CPUS)
                memory_pressure--;
}

 

 

 

 

[目錄]




頁目錄處理的宏


   
對於i3862級分頁機構,每個頁目錄字高20位是頁號,12位是頁屬性.
   
如果將頁目錄字的低12位屏蔽成0,整個頁目錄字就是相應頁面的物理地址,下面是常用的一些頁目錄處理的宏.

typedef struct { unsigned long pgd; } pgd_t;                 一級頁目錄字結構
typedef struct { unsigned long pmd; } pmd_t;                
中級頁目錄字結構
typedef struct { unsigned long pte_low; } pte_t;         
末級頁目錄字結構
typedef struct { unsigned long pgprot; } pgprot_t;       
頁屬性字結構

pgd_t *pgd = pgd_offset(mm_struct,addr);
       
取進程虛擬地址addr的一級頁目錄字指針,擴展爲

        ((mm_struct)->pgd + ((address >> 22)  0x3FF))

pgd_t *pgd = pgd_offset_k(addr)
       
取內核地址addr的一級頁目錄字指針,擴展爲

        (init_mm.pgd + ((address >> 22)  0x3FF));

pmd_t *pmd = pmd_offset(pgd, addr) ;
       
從一級頁目錄字指針取addr的中級頁錄字指針,2級分頁系統中,它們的值是相同的,擴展爲

        (pmd_t *)(pgd);

pte_t *pte = pte_offset(pmd, addr)
       
從中級頁目錄字指針取addr的末級頁目錄字指針,擴展爲

        (pte_t *)((pmd->pmd  0xFFFFF000) + 0xC0000000) + ((addr >> 12)  0x3FF);

struct page *page = pte_page(pte_val));
       
取末級頁目錄字pte_val的頁面映射指針,擴展爲

        (mem_map+(pte_val.pte_low >> 12))

pte_t pte_val = ptep_get_and_clear(pte);
       
取末級頁目錄字指針pte的值,並將該目錄字清零,擴展爲

        (pte_t){xchg(...

pte_t pte_val = mk_pte(page,pgprot);
       
將頁面映射指針page與頁屬性字pgprot組合成頁目錄字,擴展爲

        (pte_t) { (page - mem_map)
pte_t pte_val =  mk_pte_phys(physpage, pgprot);
       
將物理地址physpage所在頁面與頁屬性字組合成頁目錄字,擴展爲
        (pte_t) { physpage >> 12
unsigned long addr = pmd_page(pmd_val);
       
取中級頁目錄字所表示的頁目錄虛擬地址,擴展爲
        ((unsigned long) (pmd_val.pmd  0xFFFFF000 + 0xC0000000));

set_pte(pte,pte_val);
       
設置末級頁目錄字,擴展爲

        *pte = pteval;
set_pmd(pmd,pmd_val)
       
設置中級頁目錄字,擴展爲
        *pmd = pmd_val;
set_pgd(pgd,pgd_val)
       
設置一級頁目錄字,擴展爲
        *pgd = pgd_val;

 

 

 

 

[目錄]




MM作者的文章


Linux MM: design for a zone based memory allocator
Rik van Riel, July 1998

One of the biggest problems currently facing the Linux memory management subsystem is memory fragmentation. This is the result of several developments in other parts of the Linux kernel, most importantly the growth of each process'es kernel stack to 8 kB and the dynamic allocation of DMA and networking buffers. These factors, together with a general speedup of both peripheral hardware and the device drivers has lead to a situation where the currently used buddy allocator just can't cut it anymore. This white-paper is divided in 3 pieces, the problem, the solution and some actual code. I need a lot of comments and hints for possible improvement, so feel free to email them to me...

The problem
The problem is caused by the fact that memory is allocated in chunks of different sizes. For most types of usage we just allocate memory one page (4 kB on most machines) at a time, but sometimes we give out larger pieces of memory (2, 4, 8, 16 or 32 pages at once). Because of the fact that most UNIX (and Linux) machines have a completely full memory (free memory is wasted memory), it is next to impossible to free larger area's and the best we can do is be very careful not to hand out those large areas when we only need a small one.
There have been (and there are) several workarounds for this fragmentation issue; one of them (PTE chaining) even involves a physical to logical translating, almost reverse page table-like solution. With that project, we can swap out pages based on their physical address, thus force freeing that one page that blocked an entire 128 kB area. This would solve most of our problems, except when that last page is unswappable, for example a page table or a program's kernel stack. In that case, we're screwed regardlessly of what deallocation scheme we're using.

Because our inability to hand out larger chunks of memory has impact on system functionality and could even have impact on system stability it seems warranted to sacrifice a little bit of speed (the buddy system is fast!) in order to solve most of the above problems. The main problem with the current system is that it doesn't differentiate between swappable and unswappable memory, leading to a system where page tables and other cruft are scattered all over the system, making it impossible to free up one large contiguous area.

This problem is made even worse by the fact that on some architectures we can only do DMA to addresses under 16 MB and it will undoubtedly show up again in some years when we all have 16 GB of memory and try do do DMA to those oldie 32 bit PCI cards that don't support dual cycle address mode :-)

The solution
The solution is to hand out free zones of 128 kB large, and to use each zone for one type of usage only. Then we can be sure that no page tables interfere with the freeing of a zone of user memory, and we can always just free an area of memory.
In the current Linux kernel, we have the following uses for memory:

reserved memory, kernel code and statically allocated kernel structures: after system boot we never much with the layout of this memory so it's a non issue wrt. the allocator
user memory: this memory can be swapped out and/or relocated at will, it is allocated one page at a time and gives us no trouble, apart from the fact that we always need more than we have physically available; no special requirements
kernel stack: we allocate 8 kB (2 pages) of unswappable kernel stack for each process; each of those stacks needs to be physically contiguous and it needs to be in fast memory (not in uncached memory)
page tables: page directories are unswappable, page tables and (on some machines) page middle directories can be moved/swapped with great caution; the memory for these is given out one page at a time; we only look up the page tables every once in a while so speed is not very critical; when we have uncached memory, we'd rather use it for page tables than for user pages
small SLAB: SLAB memory is used for dynamic kernel data; it is allocated and freed at will, unfortunately this will is not ours but that of the (device) driver that requested the memory; speed is critical
large SLAB: the same as small SLAB, but sometimes the kernel wants large chunks (> 2 pages); we make the distinction between the two because we don't want to face hopeless fragmentation inside the SLAB zones...
DMA buffers: this memory needs to be physically below a certain boundary (16 MB for ISA DMA) and is often allocated in chunks of 32, 64 or 128 kB
For small (< 16 MB) machines, the above scheme is overkill and we treat several types of usage as one. We can, for instance, treat large SLAB and DMA the same, and small SLAB, kernel stack and page table can be allocated in the same zones too. Small slab and kernel stack will be treated the same on every machine; the distinction is only made because I want the documentation to be complete.
In addition to this, we can differentiate between 3 different kinds of memory:

DMA memory: this memory is located under the 16 MB limit and is cached by the L1 and L2 caches
'normal' memory: this memory is located above the DMA limit and is cached by the L1 and L2 caches, it can not be used for DMA buffers
slow memory: this memory is not cached or present on an add-on board, it can not be used for DMA buffers and using it for time critical kernel stack and SLAB would be disastrous for performance; we also don't want to use it for CPU intensive user applications
Since we don't want to waste the slow memory we might have, we can use that for page tables and user memory that isn't used very often. If we have user memory in slow memory and it turns out that it is used very often we can always use the swap code to relocate it to fast memory. DMA memory is scarce, so we want to allocate that only we specifically need it or when we don't have any other memory left.
This leads to the following zone allocation orders:

SLAB and kernel stack  | user memory   |  page tables   |  DMA buffers
-----------------------+---------------+----------------+-------------
normal memory          | normal memory |  slow memory   |  DMA memory
DMA memory             | slow memory   |  normal memory |
slow memory            | DMA memory    |  DMA memory    |

This means that when, for instance, we ran out of user memory and there is enough free memory available, we first try to grab a zone of 'normal memory', if that fails we look for a free area of slow memory and DMA memory is tried last.

Page allocation
For SLAB, page table and DMA memory we always try to allocate from the fullest zone available and we grab a free zone when we're out of our own memory. In order to grab the fullest zone, we keep these zones in a (partially?) sorted order. For large SLAB/DMA areas we will also want to keep in mind the sizes of the memory chunks previously allocated in this zone.
User pages are kept on a number of linked lists: active, inactive, clean and free. We allocate new pages in the inactive queue and perform allocations from the free queue first, moving to the clean queue when we're out of free pages. Inactive pages get either promoted to the active queue (when they're in heavy use) or demoted to the clean queue (when they're dirty, we have to clean them first). Pages in the clean queue are also unmapped from the page table and thus already 'halfway swapped out'. Pages only enter the free list when a program free()s pages or when we add a new zone to the user area.

In order to be able to free new zones (for when SLAB gets overly active), we need to be able to mark a relatively free zone force-freeable. Upon scanning such a page kswapd will free the page and make sure it isn't allocated again.When the PTE chaining system gets integrated into the kernel, we can just force-free a user zone with relatively few active pages when the system runs out of free zones. Until then we'll need to keep two free zones and walk the page tables to find and free the pages.

Actual code
There's not much of actual code yet but all the administrative details are ready. ALPHA status reached and the .h file is ready :)
/*
* The struct mem_zone is used to describe a 32 page memory area.
*/

struct mem_zone {
mem_zone * prev, next; /* The previous and next zone on this list */
unsigned long used; /* Used pages bitmap for SLAB, etc !!! count for user */
unsigned long flags;
};

/*
* Flags for struct_mem->flags
*/

#define ZONE_DMA 0x00000001 /* DMA memory */
#define ZONE_SLOW 0x00000002 /* uncached/slow memory */
#define ZONE_USER 0x00000004 /* usermode pages, these defines are for paranoia only */
#define ZONE_SLAB 0x00000008 /* large SLAB */
#define ZONE_STK 0x00000010 /* kernel stack and order-1 SLAB (and order-0 SLAB if there is slow memory) */
#define ZONE_PTBL 0x00000020 /* page tables and one-page SLAB (except when there is slow memory) */
#define ZONE_DMA 0x00000040 /* DMAbuffers */
#define ZONE_RECL 0x00000080 /* We are reclaiming this zone */
#define ZONE_0 0x00000100 /* loose pages allocated */
#define ZONE_1 0x00000200 /*order-1 (2^1 = 2 page)chunks allocated */
#define ZONE_2 0x00000400 /* etc... In order to help in buddy-like allocation for */
#define ZONE_3 0x00000800 /* large SLAB zones on small memory machines. */
#define ZONE_4 0x00001000
#define ZONE_5 0x00002000

/*
* Memory statistics
*/

typedef struct {
        unsigned long used;
        unsigned long free;
} zone_stats_t;

struct memstats {
        struct zone_stats_t ptbl;
        struct zone_stats_t stk;
        struct zone_stats_t slab;
        struct zone_stats_t dma;
        /* Slightly different structs for these */
        struct user {
                unsigned long active;
                unsigned long inactive;
                unsigned long clean;        /* we do lazy reclamation */
                unsigned long free;
        };
        struct free {
                unsigned long dma;        /* different memory types */
                unsigned long normal;
                unsigned long slow;
        };
        struct misc {
                unsigned long num_physpages;
                unsigned long reserved;        /* reserved pages */
                unsigned long kernel;        /* taken by static kernel stuff */
        };
};

/* This is where we find the different zones */

struct memzones {
        struct free {
                struct mem_zone dma;
                struct mem_zone normal;
                struct mem_zone slow;
        };
        struct mem_zone dma;
        struct mem_zone user;
        struct mem_zone slab;
        struct mem_zone stk;
        struct mem_zone ptbl;
};

 

發佈了21 篇原創文章 · 獲贊 3 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章