天天看點

Linux記憶體管理{轉}

Linux記憶體管理{轉}

摘要:本章首先以應用程式開發者的角度審視linux的程序記憶體管理,在此基礎上逐漸深入到核心中讨論系統實體記憶體管理和核心記憶體的使用方法。力求從外到内、水到渠成地引導大家分析linux的記憶體管理與使用。在本章最後,我們給出一個記憶體映射的執行個體,幫助網友們了解核心記憶體管理與使用者記憶體管理之間的關系,希望大家最終能駕馭linux記憶體管理。

Linux記憶體管理{轉}

記憶體管理一向是所有作業系統書籍不惜筆墨重點讨論的内容,無論市面上或是網上都充斥着大量涉及記憶體管理的教材和資料。是以,我們這裡所要寫的linux記憶體管理采取避重就輕的政策,從理論層面就不去班門弄斧,贻笑大方了。我們最想做的和可能做到的是從開發者的角度談談對記憶體管理的了解,最終目的是把我們在核心開發中使用記憶體的經驗和對linux記憶體管理的認識與大家共享。

當然,這其中我們也會涉及到一些諸如段頁等記憶體管理的基本理論,但我們的目的不是為了強調理論,而是為了指導了解開發中的實踐,是以僅僅點到為止,不做深究。

遵循“理論來源于實踐”的“教條”,我們先不必一下子就鑽入核心裡去看系統記憶體到底是如何管理,那樣往往會讓你陷入似懂非懂的窘境(我當年就犯了這個錯誤!)。是以最好的方式是先從外部(使用者程式設計範疇)來觀察程序如何使用記憶體,等到大家對記憶體的使用有了較直覺的認識後,再深入到核心中去學習記憶體如何被管理等理論知識(由表及裡)。最後再通過一個執行個體程式設計将所講内容融會貫通。

毫無疑問,所有程序(執行的程式)都必須占用一定數量的記憶體,它或是用來存放從磁盤載入的程式代碼,或是存放取自使用者輸入的資料等等。不過程序對這些記憶體的管理方式因記憶體用途不一而不盡相同,有些記憶體是事先靜态配置設定和統一回收的,而有些卻是按需要動态配置設定和回收的。

對任何一個普通程序來講,它都會涉及到5種不同的資料段。稍有程式設計知識的朋友都能想到這幾個資料段中包含有“程式代碼段”、“程式資料段”、“程式堆棧段”等。不錯,這幾種資料段都在其中,但除了以上幾種資料段之外,程序還另外包含兩種資料段。下面我們來簡單歸納一下程序對應的記憶體空間中所包含的5種不同的資料區。

代碼段:代碼段是用來存放可執行檔案的操作指令,也就是說是它是可執行程式在記憶體中的鏡像。代碼段需要防止在運作時被非法修改,是以隻準許讀取操作,而不允許寫入(修改)操作——它是不可寫的。

堆(heap):堆是用于存放程序運作中被動态配置設定的記憶體段,它的大小并不固定,可動态擴張或縮減。當程序調用malloc等函數配置設定記憶體時,新配置設定的記憶體就被動态添加到堆上(堆被擴張);當利用free等函數釋放記憶體時,被釋放的記憶體從堆中被剔除(堆被縮減)

棧:棧是使用者存放程式臨時建立的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味着在資料段中存放變量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的程序棧中,并且待到調用結束後,函數的傳回值也會被存放回棧中。由于棧的先進先出特點,是以棧特别友善用來儲存/恢複調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時資料的記憶體區。

上述幾種記憶體區域中資料段、bss和堆通常是被連續存儲的——記憶體位置上是連續的,而代碼段和棧往往會被獨立存放。有趣的是,堆和棧兩個區域關系很“暧昧”,他們一個向下“長”(i386體系結構中棧向下、堆向上),一個向上“長”,相對而生。但你不必擔心他們會碰頭,因為他們之間間隔很大(到底大到多少,你可以從下面的例子程式計算一下),絕少有機會能碰到一起。

下圖簡要描述了程序記憶體區域的分布:

Linux記憶體管理{轉}

如下再分享一個靓圖:

Linux記憶體管理{轉}
Linux記憶體管理{轉}

它的結果如下:

Linux記憶體管理{轉}

利用size指令也可以看到程式的各段大小,比如執行size example會得到

Linux記憶體管理{轉}

但這些資料是程式編譯的靜态統計,而上面顯示的是程序運作時的動态值,但兩者是對應的。

通過前面的例子,我們對程序使用的邏輯記憶體分布已先睹為快。這部分我們就繼續進入作業系統核心看看,程序對記憶體具體是如何進行配置設定和管理的。

1.     程序空間位址如何管理?

2.     程序位址如何映射到實體記憶體?

3.     實體記憶體如何被管理?

以及由上述問題引發的一些子問題。如系統虛拟位址分布;記憶體配置設定接口;連續記憶體配置設定與非連續記憶體配置設定等。

在讨論程序空間細節前,這裡先要澄清下面幾個問題:

l         第一、4g的程序位址空間被人為的分為兩個部分——使用者空間與核心空間。使用者空間從0到3g(0xc0000000),核心空間占據3g到4g。使用者程序通常情況下隻能通路使用者空間的虛拟位址,不能通路核心空間虛拟位址。隻有使用者程序進行系統調用(代表使用者程序在核心态執行)等時刻可以通路到核心空間。

l         第二、使用者空間對應程序,是以每當程序切換,使用者空間就會跟着變化;而核心空間是由核心負責映射,它并不會跟着程序改變,是固定的。核心空間位址有自己對應的頁表(init_mm.pgd),使用者程序各自有不同的頁表。

l         第三、每個程序的使用者空間都是完全獨立、互不相幹的。不信的話,你可以把上面的程式同時運作10次(當然為了同時運作,讓它們在傳回前一同睡眠100秒吧),你會看到10個程序占用的線性位址一模一樣。

程序記憶體管理的對象是程序線性位址空間上的記憶體鏡像,這些記憶體鏡像其實就是程序使用的虛拟記憶體區域(memory region)。程序虛拟空間是個32或64位的“平坦”(獨立的連續區間)位址空間(空間的具體大小取決于體系結構)。要統一管理這麼大的平坦空間可絕非易事,為了友善管理,虛拟空間被劃分為許多大小可變的(但必須是4096的倍數)記憶體區域,這些區域在程序線性位址中像停車位一樣有序排列。這些區域的劃分原則是“将通路屬性一緻的位址空間存放在一起”,所謂通路屬性在這裡無非指的是“可讀、可寫、可執行等”。

如果你要檢視某個程序占用的記憶體區域,可以使用指令cat /proc/<pid>/maps獲得(pid是程序号,你可以運作上面我們給出的例子——./example &;pid便會列印到螢幕),你可以發現很多類似于下面的數字資訊。

由于程式example使用了動态庫,是以除了example本身使用的的記憶體區域外,還會包含那些動态庫使用的記憶體區域(區域順序是:代碼段、資料段、bss段)。

我們下面隻抽出和example有關的資訊,除了前兩行代表的代碼段和資料段外,最後一行是程序使用的棧空間。

-------------------------------------------------------------------------------

Linux記憶體管理{轉}

每行資料格式如下:

(記憶體區域)開始-結束通路權限  偏移  主裝置号:次裝置号 i節點  檔案

注意,你一定會發現程序空間隻包含三個記憶體區域,似乎沒有上面所提到的堆、bss等,其實并非如此,程式記憶體段和程序位址空間中的記憶體區域是種模糊對應,也就是說,堆、bss、資料段(初始化過的)都在程序空間中由資料段記憶體區域表示。

在linux核心中對應程序記憶體區域的資料結構是: vm_area_struct, 核心将每個記憶體區域作為一個單獨的記憶體對象管理,相應的操作也都一緻。采用面向對象方法使vma結構體可以代表多種類型的記憶體區域--比如記憶體映射檔案或程序的使用者空間棧等,對這些區域的操作也都不盡相同。

下圖反映了程序位址空間的管理模型:

Linux記憶體管理{轉}

程序的位址空間對應的描述結構是“記憶體描述符結構”,它表示程序的全部位址空間,——包含了和程序位址空間有關的全部資訊,其中當然包含程序的記憶體區域。

建立程序fork()、程式載入execve()、映射檔案mmap()、動态記憶體配置設定malloc()/brk()等程序相關操作都需要配置設定記憶體給程序。不過這時程序申請和獲得的還不是實際記憶體,而是虛拟記憶體,準确的說是“記憶體區域”。程序對記憶體區域的配置設定最終都會歸結到do_mmap()函數上來(brk調用被單獨以系統調用實作,不用do_mmap()),

核心使用do_mmap()函數建立一個新的線性位址區間。但是說該函數建立了一個新vma并不非常準确,因為如果建立的位址區間和一個已經存在的位址區間相鄰,并且它們具有相同的通路權限的話,那麼兩個區間将合并為一個。如果不能合并,那麼就确實需要建立一個新的vma了。但無論哪種情況, do_mmap()函數都會将一個位址區間加入到程序的位址空間中--無論是擴充已存在的記憶體區域還是建立一個新的區域。

同樣,釋放一個記憶體區域應使用函數do_ummap(),它會銷毀對應的記憶體區域。

    從上面已經看到程序所能直接操作的位址都為虛拟位址。當程序需要記憶體時,從核心獲得的僅僅是虛拟的記憶體區域,而不是實際的實體位址,程序并沒有獲得實體記憶體(實體頁面——頁的概念請大家參考硬體基礎一章),獲得的僅僅是對一個新的線性位址區間的使用權。實際的實體記憶體隻有當程序真的去通路新擷取的虛拟位址時,才會由“請求頁機制”産生“缺頁”異常,進而進入配置設定實際頁面的例程。

該異常是虛拟記憶體機制賴以存在的基本保證——它會告訴核心去真正為程序配置設定實體頁,并建立對應的頁表,這之後虛拟位址才實實在在地映射到了系統的實體記憶體上。(當然,如果頁被換出到磁盤,也會産生缺頁異常,不過這時不用再建立頁表了)

這裡我們需要說明在記憶體區域結構上的nopage操作。當通路的程序虛拟記憶體并未真正配置設定頁面時,該操作便被調用來配置設定實際的實體頁,并為該頁建立頁表項。在最後的例子中我們會示範如何使用該方法。

雖然應用程式操作的對象是映射到實體記憶體之上的虛拟記憶體,但是處理器直接操作的卻是實體記憶體。是以當應用程式通路一個虛拟位址時,首先必須将虛拟位址轉化成實體位址,然後處理器才能解析位址通路請求。位址的轉換工作需要通過查詢頁表才能完成,概括地講,位址轉換需要将虛拟位址分段,使每段虛位址都作為一個索引指向頁表,而頁表項則指向下一級别的頁表或者指向最終的實體頁面。

Linux記憶體管理{轉}

     上面的過程說起來簡單,做起來難呀。因為在虛拟位址映射到頁之前必須先配置設定實體頁——也就是說必須先從核心中擷取空閑頁,并建立頁表。下面我們介紹一下核心管理實體記憶體的機制。

鑒于上述需求,核心配置設定實體頁面時為了盡量減少不連續情況,采用了“夥伴”關系來管理空閑頁面。夥伴關系配置設定算法大家應該不陌生——幾乎所有作業系統方面的書都會提到,我們不去詳細說它了,如果不明白可以參看有關資料。這裡隻需要大家明白linux中空閑頁面的組織和管理利用了夥伴關系,是以空閑頁面配置設定時也需要遵循夥伴關系,最小機關隻能是2的幂倍頁面大小。核心中配置設定空閑頁面的基本函數是get_free_page/get_free_pages,它們或是配置設定單頁或是配置設定指定的頁面(2、4、8…512頁)。

 注意:get_free_page是在核心中配置設定記憶體,不同于malloc在使用者空間中配置設定,malloc利用堆動态配置設定,實際上是調用brk()系統調用,該調用的作用是擴大或縮小程序堆空間(它會修改程序的brk域)。如果現有的記憶體區域不夠容納堆空間,則會以頁面大小的倍數為機關,擴張或收縮對應的記憶體區域,但brk值并非以頁面大小為倍數修改,而是按實際請求修改。是以malloc在使用者空間配置設定記憶體可以以位元組為機關配置設定,但核心在内部仍然會是以頁為機關配置設定的。

   另外,需要提及的是,實體頁在系統中由頁結構struct page描述,系統中所有的頁面都存儲在數組mem_map[]中,可以通過該數組找到系統中的每一頁(空閑或非空閑)。而其中的空閑頁面則可由上述提到的以夥伴關系組織的空閑頁連結清單(free_area[max_order])來索引。

Linux記憶體管理{轉}

slab

    所謂尺有所長,寸有所短。以頁為最小機關配置設定記憶體對于核心管理系統中的實體記憶體來說的确比較友善,但核心自身最常使用的記憶體卻往往是很小(遠遠小于一頁)的記憶體塊——比如存放檔案描述符、程序描述符、虛拟記憶體區域描述符等行為所需的記憶體都不足一頁。這些用來存放描述符的記憶體相比頁面而言,就好比是面包屑與面包。一個整頁中可以聚集多個這些小塊記憶體;而且這些小塊記憶體塊也和面包屑一樣頻繁地生成/銷毀。

slab技術不但避免了記憶體内部分片(下文将解釋)帶來的不便(引入slab配置設定器的主要目的是為了減少對夥伴系統配置設定算法的調用次數——頻繁配置設定和回收必然會導緻記憶體碎片——難以找到大塊連續的可用記憶體),而且可以很好地利用硬體緩存提高通路速度。

    slab并非是脫離夥伴關系而獨立存在的一種記憶體配置設定方式,slab仍然是建立在頁面基礎之上,換句話說,slab将頁面(來自于夥伴關系管理的空閑頁面連結清單)撕碎成衆多小記憶體塊以供配置設定,slab中的對象配置設定和銷毀使用kmem_cache_alloc與kmem_cache_free。

kmalloc

slab配置設定器不僅僅隻用來存放核心專用的結構體,它還被用來處理核心對小塊記憶體的請求。當然鑒于slab配置設定器的特點,一般來說核心程式中對小于一頁的小塊記憶體的請求才通過slab配置設定器提供的接口kmalloc來完成(雖然它可配置設定32 到131072位元組的記憶體)。從核心記憶體配置設定的角度來講,kmalloc可被看成是get_free_page(s)的一個有效補充,記憶體配置設定粒度更靈活了。

有興趣的話,可以到/proc/slabinfo中找到核心執行現場使用的各種slab資訊統計,其中你會看到系統中所有slab的使用資訊。從資訊中可以看到系統中除了專用結構體使用的slab外,還存在大量為kmalloc而準備的slab(其中有些為dma準備的)。

核心非連續記憶體配置設定(vmalloc)

夥伴關系也好、slab技術也好,從記憶體管理理論角度而言目的基本是一緻的,它們都是為了防止“分片”,不過分片又分為外部分片和内部分片之說,所謂内部分片是說系統為了滿足一小段記憶體區(連續)的需要,不得不配置設定了一大區域連續記憶體給它,進而造成了空間浪費;外部分片是指系統雖有足夠的記憶體,但卻是分散的碎片,無法滿足對大塊“連續記憶體”的需求。無論何種分片都是系統有效利用記憶體的障礙。slab配置設定器使得一個頁面内包含的衆多小塊記憶體可獨立被配置設定使用,避免了内部分片,節約了空閑記憶體。夥伴關系把記憶體塊按大小分組管理,一定程度上減輕了外部分片的危害,因為頁框配置設定不在盲目,而是按照大小依次有序進行,不過夥伴關系隻是減輕了外部分片,但并未徹底消除。你自己比劃一下多次配置設定頁面後,空閑記憶體的剩餘情況吧。

是以避免外部分片的最終思路還是落到了如何利用不連續的記憶體塊組合成“看起來很大的記憶體塊”——這裡的情況很類似于使用者空間配置設定虛拟記憶體,記憶體邏輯上連續,其實映射到并不一定連續的實體記憶體上。linux核心借用了這個技術,允許核心程式在核心位址空間中配置設定虛拟位址,同樣也利用頁表(核心頁表)将虛拟位址映射到分散的記憶體頁上。以此完美地解決了核心記憶體使用中的外部分片問題。核心提供vmalloc函數配置設定核心虛拟記憶體,該函數不同于kmalloc,它可以配置設定較kmalloc大得多的記憶體空間(可遠大于128k,但必須是頁大小的倍數),但相比kmalloc來說,vmalloc需要對核心虛拟位址進行重映射,必須更新核心頁表,是以配置設定效率上要低一些(用空間換時間)

與使用者程序相似,核心也有一個名為init_mm的mm_strcut結構來描述核心位址空間,其中頁表項pdg=swapper_pg_dir包含了系統核心空間(3g-4g)的映射關系。是以vmalloc配置設定核心虛拟位址必須更新核心頁表,而kmalloc或get_free_page由于配置設定的連續記憶體,是以不需要更新核心頁表。

Linux記憶體管理{轉}

vmalloc配置設定的核心虛拟記憶體與kmalloc/get_free_page配置設定的核心虛拟記憶體位于不同的區間,不會重疊。因為核心虛拟空間被分區管理,各司其職。程序空間位址分布從0到3g(其實是到page_offset, 在0x86中它等于0xc0000000),從3g到vmalloc_start這段位址是實體記憶體映射區域(該區域中包含了核心鏡像、實體頁面表mem_map等等)比如我使用的系統記憶體是64m(可以用free看到),那麼(3g——3g+64m)這片記憶體就應該映射到實體記憶體,而vmalloc_start位置應在3g+64m附近(說"附近"因為是在實體記憶體映射區與vmalloc_start期間還會存在一個8m大小的gap來防止躍界),vmalloc_end的位置接近4g(說"接近"是因為最後位置系統會保留一片128k大小的區域用于專用頁面映射,還有可能會有高端記憶體映射區,這些都是細節,這裡我們不做糾纏)。

Linux記憶體管理{轉}

上圖是記憶體分布的模糊輪廓

   由get_free_page或kmalloc函數所配置設定的連續記憶體都陷于實體映射區域,是以它們傳回的核心虛拟位址和實際實體位址僅僅是相差一個偏移量(page_offset),你可以很友善的将其轉化為實體記憶體位址,同時核心也提供了virt_to_phys()函數将核心虛拟空間中的實體映射區位址轉化為實體位址。要知道,實體記憶體映射區中的位址與核心頁表是有序對應的,系統中的每個實體頁面都可以找到它對應的核心虛拟位址(在實體記憶體映射區中的)。

而vmalloc配置設定的位址則限于vmalloc_start與vmalloc_end之間。每一塊vmalloc配置設定的核心虛拟記憶體都對應一個vm_struct結構體(可别和vm_area_struct搞混,那可是程序虛拟記憶體區域的結構),不同的核心虛拟位址被4k大小的空閑區間隔,以防止越界——見下圖)。與程序虛拟位址的特性一樣,這些虛拟位址與實體記憶體沒有簡單的位移關系,必須通過核心頁表才可轉換為實體位址或實體頁。它們有可能尚未被映射,在發生缺頁時才真正配置設定實體頁面。

Linux記憶體管理{轉}

這裡給出一個小程式幫助大家認清上面幾種配置設定函數所對應的區域。

#include<linux/module.h>

#include<linux/slab.h>

#include<linux/vmalloc.h>

unsigned char *pagemem;

unsigned char *kmallocmem;

unsigned char *vmallocmem;

int init_module(void)

{

 pagemem = get_free_page(0);

 printk("<1>pagemem=%s",pagemem);

 kmallocmem = kmalloc(100,0);

 printk("<1>kmallocmem=%s",kmallocmem);

 vmallocmem = vmalloc(1000000);

 printk("<1>vmallocmem=%s",vmallocmem);

}

void cleanup_module(void)

 free_page(pagemem);

 kfree(kmallocmem);

 vfree(vmallocmem);

記憶體映射(mmap)是linux作業系統的一個很大特色,它可以将系統記憶體映射到一個檔案(裝置)上,以便可以通過通路檔案内容來達到通路記憶體的目的。這樣做的最大好處是提高了記憶體通路速度,并且可以利用檔案系統的接口程式設計(裝置在linux中作為特殊檔案處理)通路記憶體,降低了開發難度。許多裝置驅動程式便是利用記憶體映射功能将使用者空間的一段位址關聯到裝置記憶體上,無論何時,隻要記憶體在配置設定的位址範圍内進行讀寫,實際上就是對裝置記憶體的通路。同時對裝置檔案的通路也等同于對記憶體區域的通路,也就是說,通過檔案操作接口可以通路記憶體。linux中的x伺服器就是一個利用記憶體映射達到直接高速通路視訊卡記憶體的例子。

熟悉檔案操作的朋友一定會知道file_operations結構中有mmap方法,在使用者執行mmap系統調用時,便會調用該方法來通過檔案通路記憶體——不過在調用檔案系統mmap方法前,核心還需要處理配置設定記憶體區域(vma_struct)、建立頁表等工作。對于具體映射細節不作介紹了,需要強調的是,建立頁表可以采用remap_page_range方法一次建立起所有映射區的頁表,或利用vma_struct的nopage方法在缺頁時現場一頁一頁的建立頁表。第一種方法相比第二種方法簡單友善、速度快, 但是靈活性不高。一次調用所有頁表便定型了,不适用于那些需要現場建立頁表的場合——比如映射區需要擴充或下面我們例子中的情況。

我們這裡的執行個體希望利用記憶體映射,将系統核心中的一部分虛拟記憶體映射到使用者空間,以供應用程式讀取——你可利用它進行核心空間到使用者空間的大規模資訊傳輸。是以我們将試圖寫一個虛拟字元裝置驅動程式,通過它将系統核心空間映射到使用者空間——将核心虛拟記憶體映射到使用者虛拟位址。從上一節已經看到linux核心空間中包含兩種虛拟位址:一種是實體和邏輯都連續的實體記憶體映射虛拟位址;另一種是邏輯連續但非實體連續的vmalloc配置設定的記憶體虛拟位址。我們的例子程式将示範把vmalloc配置設定的核心虛拟位址映射到使用者位址空間的全過程。

程式裡主要應解決兩個問題:

第一是如何将vmalloc配置設定的核心虛拟記憶體正确地轉化成實體位址?

因為記憶體映射先要獲得被映射的實體位址,然後才能将其映射到要求的使用者虛拟位址上。我們已經看到核心實體記憶體映射區域中的位址可以被核心函數virt_to_phys轉換成實際的實體記憶體位址,但對于vmalloc配置設定的核心虛拟位址無法直接轉化成實體位址,是以我們必須對這部分虛拟記憶體格外“照顧”——先将其轉化成核心實體記憶體映射區域中的位址,然後在用virt_to_phys變為實體位址。

轉化工作需要進行如下步驟:

a)         找到vmalloc虛拟記憶體對應的頁表,并尋找到對應的頁表項。

b)        擷取頁表項對應的頁面指針

c)        通過頁面得到對應的核心實體記憶體映射區域位址。

如下圖所示:

Linux記憶體管理{轉}

第二是當通路vmalloc配置設定區時,如果發現虛拟記憶體尚未被映射到實體頁,則需要處理“缺頁異常”。是以需要我們實作記憶體區域中的nopaga操作,以能傳回被映射的實體頁面指針,在我們的執行個體中就是傳回上面過程中的核心實體記憶體映射區域中的位址。由于vmalloc配置設定的虛拟位址與實體位址的對應關系并非配置設定時就可确定,必須在缺頁現場建立頁表,是以這裡不能使用remap_page_range方法,隻能用vma的nopage方法一頁一頁的建立。

程式組成

map_driver.c,它是以子產品形式加載的虛拟字元驅動程式。該驅動負責将一定長的核心虛拟位址(vmalloc配置設定的)映射到裝置檔案上。其中主要的函數有——vaddress_to_kaddress()負責對vmalloc配置設定的位址進行頁表解析,以找到對應的核心實體映射位址(kmalloc配置設定的位址);map_nopage()負責在程序通路一個目前并不存在的vma頁時,尋找該位址對應的實體頁,并傳回該頁的指針。

test.c 它利用上述驅動子產品對應的裝置檔案在使用者空間讀取讀取核心記憶體。結果可以看到核心虛拟位址的内容(ok!),被顯示在了螢幕上。

執行步驟

編譯map_driver.c為map_driver.o子產品,具體參數見makefile

加載子產品 :insmod map_driver.o

生成對應的裝置檔案

1 在/proc/devices下找到map_driver對應的裝置命和裝置号:grep mapdrv /proc/devices

2 建立裝置檔案mknod  mapfile c 254 0  (在我的系統裡裝置号為254)

    利用maptest讀取mapfile檔案,将取自核心的資訊列印到螢幕上。

(mmap.tar木有下載下傳到)

繼續閱讀