成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Linux內核內存管理:核心技術與優化策略

系統 Linux
THP(Transparent Huge Page,透明大頁)是 Linux 內核中的一項內存管理優化技術,它主要用于處理大內存頁面的分配和管理,旨在提高內存訪問效率,特別是對于那些需要頻繁訪問大量內存的應用程序 。

在 Linux 系統中,內存管理堪稱內核的核心功能之一,其運作機制復雜且精妙。Linux 采用虛擬內存技術,為進程構建獨立的地址空間,借內存管理單元(MMU)將虛擬地址精準映射至物理地址,既保障進程間內存隔離,又防止相互干擾。物理內存管理上,Linux 以分頁機制為基,將內存切為固定大小頁(常見 4KB) ,由伙伴系統算法主導分配與回收。通過合并、分割內存頁,伙伴系統有效減少內存碎片,提升內存利用率。同時,slab 分配器專注于小內存塊分配,進一步優化內存使用。

當物理內存捉襟見肘,頁面替換算法粉墨登場。Linux 常用 “最近最少使用”(LRU)算法,依據程序訪問局部性原理,淘汰長時間未訪問頁面,保障系統關鍵數據駐留內存;在優化策略方面,調整內核參數如vm.swappiness,可調控系統對交換空間的依賴程度;啟用大頁技術,能減少頁表項數量,降低內存管理開銷,大幅提升內存訪問效率,尤其適用于對內存要求嚴苛的應用程序。

一、Linux內存管理概述

Linux 內存管理機制的設計遵循著一系列理念,這些理念是其高效運行和廣泛適應性的基石,涵蓋了效率、公平性、可擴展性等多個關鍵方面。

在效率層面,Linux 內存管理致力于實現快速的內存分配與釋放。以伙伴系統(Buddy System)為例,這是 Linux 內核用于管理物理內存的一種重要機制。它將內存按照 2 的冪次方大小進行劃分,形成不同大小的內存塊。當一個進程請求內存時,伙伴系統能迅速找到合適大小的內存塊并分配給它;當內存被釋放時,伙伴系統又能高效地將相鄰的空閑內存塊合并成更大的內存塊,從而減少內存碎片的產生。這種基于 2 的冪次方的分配策略,就像一個精密的齒輪系統,各個部分緊密配合,大大提高了內存分配和回收的效率。

再比如,在內存訪問的優化上,Linux 采用了高速緩存機制。高速緩存(Cache)就像是一個靠近 CPU 的小型快速存儲器,它存儲了頻繁訪問的內存數據和指令。當 CPU 需要訪問內存時,首先會在高速緩存中查找,如果找到所需數據,就可以直接讀取,大大縮短了訪問時間。這就好比我們在閱讀一本厚厚的書籍時,如果將經常查閱的內容記錄在一個便于翻閱的筆記本上,每次查找時就無需在整本書中逐頁尋找,從而節省了大量時間。Linux 通過合理地組織高速緩存,使得內存訪問的命中率大幅提高,進而提升了系統整體的運行效率。

公平性也是 Linux 內存管理設計中不可或缺的一環。在多任務環境下,每個進程都需要獲得合理的內存資源,以保證它們能夠正常運行。Linux 通過內存分配策略來確保公平性。例如,在進程啟動時,系統會根據其需求為其分配一定的內存空間,并且在運行過程中,會根據進程的優先級和實際內存使用情況,動態地調整內存分配。這就如同在一場比賽中,每個選手都有公平的機會獲得比賽資源,而裁判會根據選手的表現和需求,合理地分配比賽時間和場地等資源。

Linux 內存管理還注重對不同類型進程的公平對待。無論是前臺交互進程,還是后臺服務進程,都能在內存分配上得到合理的保障。前臺交互進程通常對響應速度要求較高,因為它們直接與用戶進行交互,如用戶在使用圖形界面的應用程序時,希望操作能夠得到即時反饋。Linux 會優先為這類進程分配足夠的內存,以確保它們能夠快速響應用戶的操作。而后臺服務進程雖然對響應速度的要求相對較低,但它們在系統中承擔著重要的服務任務,如網絡服務、文件服務等。Linux 也會根據它們的實際需求,為其分配穩定的內存資源,保證這些服務的正常運行。

隨著計算機技術的不斷發展,系統的規模和應用場景日益復雜,可擴展性成為 Linux 內存管理設計必須考慮的因素。Linux 內存管理機制具備良好的可擴展性,能夠適應不同硬件平臺和應用需求的變化。在硬件平臺方面,無論是小型嵌入式設備,還是大型服務器集群,Linux 都能有效地管理內存。對于嵌入式設備,由于其硬件資源有限,內存管理需要更加精細和高效,以充分利用有限的內存空間。Linux 通過精簡內存管理算法和數據結構,減少內存開銷,滿足嵌入式設備的需求。而對于大型服務器集群,內存管理需要處理海量的內存資源和高并發的內存訪問請求。Linux 采用分布式內存管理策略,將內存管理任務分布到各個節點上,提高系統的整體性能和可擴展性。

在面對不斷涌現的新應用需求時,Linux 內存管理也能夠靈活調整。例如,隨著大數據和人工智能技術的發展,應用程序對內存的需求呈現出多樣化和動態化的特點。一些大數據處理應用需要處理海量的數據,這就要求內存管理能夠支持大規模內存的分配和高效利用。Linux 通過引入新的內存分配算法和技術,如大頁內存(Huge Page)機制,滿足這類應用對內存的特殊需求。大頁內存可以提供更大的內存頁,減少頁表項的數量,從而降低內存管理的開銷,提高大數據處理的效率。

二、物理內存與虛擬內存

2.1物理內存的管理

物理內存是計算機系統中實際存在的內存,它由計算機硬件直接管理,通常由 DRAM 芯片組成,是計算機系統中最快的存儲器 ,其大小通常是固定的,取決于計算機硬件的配置。在 Linux 內核中,物理內存被劃分為一個個固定大小的頁框(Page Frame),每個頁框通常為 4KB(也有其他大小,如 2MB、1GB 的大頁)。內核通過頁框號(Page Frame Number,PFN)來管理這些頁框,就像給圖書館的每一本書都編上了唯一的編號,方便查找和管理。

物理內存的作用至關重要,它是程序運行和數據存儲的直接場所。當程序運行時,其代碼和數據會被加載到物理內存中,CPU 直接從物理內存中讀取指令和數據進行處理。比如,當我們啟動一個文本編輯器時,編輯器的程序代碼會被加載到物理內存的某個區域,我們在編輯器中輸入的文本數據也會存儲在物理內存中,這樣 CPU 才能快速地對這些數據進行處理,實現文本的編輯、保存等操作。

2.2虛擬內存的奧秘

虛擬內存,簡單來說,是一種內存管理技術,它為每個進程提供了一個獨立的、連續的地址空間,讓進程誤以為自己擁有一塊完整且足夠大的內存空間 ,而無需關心實際物理內存的具體布局和大小限制。這就好比你擁有一個超大的虛擬倉庫,你可以隨意規劃貨物的擺放位置,而不用擔心倉庫空間不夠。

虛擬內存的主要作用之一是實現內存地址轉換。在 Linux 系統中,每個進程都有自己的虛擬地址空間,這個空間通過頁表(Page Table)與物理內存進行映射。頁表就像是一本地址翻譯字典,負責將進程使用的虛擬地址翻譯成實際的物理地址。

當程序運行時,它所訪問的內存地址都是虛擬地址。例如,當程序需要讀取某個變量的值時,它會給出一個虛擬地址。CPU 首先會根據這個虛擬地址中的頁號(Page Number)在頁表中查找對應的物理頁框號(Page Frame Number)。如果頁表中存在這個映射關系(即頁表項有效),CPU 就可以通過物理頁框號和虛擬地址中的頁內偏移(Offset)計算出實際的物理地址,從而訪問到物理內存中的數據。

但如果頁表中沒有找到對應的映射關系(即發生缺頁異常,Page Fault),系統會認為這個虛擬頁還沒有被加載到物理內存中。此時,操作系統會介入,從磁盤的交換區(Swap Area)或者文件系統中找到對應的物理頁,并將其加載到物理內存中,同時更新頁表,建立虛擬地址與物理地址的映射關系。之后,程序就可以通過新建立的映射關系訪問到數據了。

為了更直觀地理解,我們可以把虛擬內存想象成一個圖書館的目錄系統。每個進程就像是一個讀者,擁有自己的目錄(虛擬地址空間)。當讀者想要查找某本書(訪問數據)時,會先在自己的目錄中找到對應的條目(虛擬地址),然后通過這個條目去書架(物理內存)上找到實際的書。如果書架上沒有這本書(缺頁異常),圖書館管理員(操作系統)就會從倉庫(磁盤)中把書取出來放到書架上,并更新目錄(頁表),以便下次讀者能更快地找到這本書。

例如:對于程序計數器位數為32位的處理器來說,他的地址發生器所能發出的地址數目為2^32=4G個,于是這個處理器所能訪問的最大內存空間就是4G。在計算機技術中,這個值就叫做處理器的尋址空間或尋址能力。

照理說,為了充分利用處理器的尋址空間,就應按照處理器的最大尋址來為其分配系統的內存。如果處理器具有32位程序計數器,那么就應該按照下圖的方式,為其配備4G的內存:

這樣,處理器所發出的每一個地址都會有一個真實的物理存儲單元與之對應;同時,每一個物理存儲單元都有唯一的地址與之對應。這顯然是一種最理想的情況。

但遺憾的是,實際上計算機所配置內存的實際空間常常小于處理器的尋址范圍,這是就會因處理器的一部分尋址空間沒有對應的物理存儲單元,從而導致處理器尋址能力的浪費。例如:如下圖的系統中,具有32位尋址能力的處理器只配置了256M的內存儲器,這就會造成大量的浪費:

另外,還有一些處理器因外部地址線的根數小于處理器程序計數器的位數,而使地址總線的根數不滿足處理器的尋址范圍,從而處理器的其余尋址能力也就被浪費了。例如:Intel8086處理器的程序計數器位32位,而處理器芯片的外部地址總線只有20根,所以它所能配置的最大內存為1MB:

在實際的應用中,如果需要運行的應用程序比較小,所需內存容量小于計算機實際所配置的內存空間,自然不會出什么問題。但是,目前很多的應用程序都比較大,計算機實際所配置的內存空間無法滿足。

實踐和研究都證明:一個應用程序總是逐段被運行的,而且在一段時間內會穩定運行在某一段程序里。

這也就出現了一個方法:如下圖所示,把要運行的那一段程序自輔存復制到內存中來運行,而其他暫時不運行的程序段就讓它仍然留在輔存。

當需要執行另一端尚未在內存的程序段(如程序段2),如下圖所示,就可以把內存中程序段1的副本復制回輔存,在內存騰出必要的空間后,再把輔存中的程序段2復制到內存空間來執行即可:

在計算機技術中,把內存中的程序段復制回輔存的做法叫做“換出”,而把輔存中程序段映射到內存的做法叫做“換入”。經過不斷有目的的換入和換出,處理器就可以運行一個大于實際物理內存的應用程序了。或者說,處理器似乎是擁有了一個大于實際物理內存的內存空間。于是,這個存儲空間叫做虛擬內存空間,而把真正的內存叫做實際物理內存,或簡稱為物理內存。

那么對于一臺真實的計算機來說,它的虛擬內存空間又有多大呢?計算機虛擬內存空間的大小是由程序計數器的尋址能力來決定的。例如:在程序計數器的位數為32的處理器中,它的虛擬內存空間就為4GB。

可見,如果一個系統采用了虛擬內存技術,那么它就存在著兩個內存空間:虛擬內存空間和物理內存空間。虛擬內存空間中的地址叫做“虛擬地址”;而實際物理內存空間中的地址叫做“實際物理地址”或“物理地址”。處理器運算器和應用程序設計人員看到的只是虛擬內存空間和虛擬地址,而處理器片外的地址總線看到的只是物理地址空間和物理地址。

由于存在兩個內存地址,因此一個應用程序從編寫到被執行,需要進行兩次映射。第一次是映射到虛擬內存空間,第二次時映射到物理內存空間。在計算機系統中,第兩次映射的工作是由硬件和軟件共同來完成的。承擔這個任務的硬件部分叫做存儲管理單元MMU,軟件部分就是操作系統的內存管理模塊了。

在映射工作中,為了記錄程序段占用物理內存的情況,操作系統的內存管理模塊需要建立一個表格,該表格以虛擬地址為索引,記錄了程序段所占用的物理內存的物理地址。這個虛擬地址/物理地址記錄表便是存儲管理單元MMU把虛擬地址轉化為實際物理地址的依據,記錄表與存儲管理單元MMU的作用如下圖所示:

綜上所述,虛擬內存技術的實現,是建立在應用程序可以分成段,并且具有“在任何時候正在使用的信息總是所有存儲信息的一小部分”的局部特性基礎上的。它是通過用輔存空間模擬RAM來實現的一種使機器的作業地址空間大于實際內存的技術。

從處理器運算裝置和程序設計人員的角度來看,它面對的是一個用MMU、映射記錄表和物理內存封裝起來的一個虛擬內存空間,這個存儲空間的大小取決于處理器程序計數器的尋址空間。

可見,程序映射表是實現虛擬內存的技術關鍵,它可給系統帶來如下特點:

  • 系統中每一個程序各自都有一個大小與處理器尋址空間相等的虛擬內存空間;
  • 在一個具體時刻,處理器只能使用其中一個程序的映射記錄表,因此它只看到多個程序虛存空間中的一個,這樣就保證了各個程序的虛存空間時互不相擾、各自獨立的;
  • 使用程序映射表可方便地實現物理內存的共享。

三、深度剖析核心技術

3.1分頁:虛擬內存的基石

在 Linux 的世界里,內存分頁機制就像是一位有條不紊的大管家,精心管理著系統的內存資源。簡單來說,內存分頁機制就是把物理內存和虛擬內存分割成固定大小的小塊,這些小塊被稱作 “頁” ,每個頁的大小一般為 4KB 或者 8KB。就好比你有一個巨大的倉庫(內存),為了更好地管理里面的貨物(數據),你把倉庫劃分成了一個個大小相同的小隔間(頁)。

(1)什么是分頁機制

分頁機制是 80x86 內存管理機制的第二部分。它在分段機制的基礎上完成虛擬地址到物理地址的轉換過程。分段機制把邏輯地址轉換成線性地址,而分頁機制則把線性地址轉換成物理地址。分頁機制可用于任何一種分段模型。處理器分頁機制會把線性地址空間劃分成頁面,然后這些線性地址空間頁面被映射到物理地址空間的頁面上。分頁機制的幾種頁面級保護措施,可和分段機制保護措施或用或替代分段機制的保護措施。

(2)分頁機制如何啟用

在我們進行程序開發的時候,一般情況下,是不需要管理內存的,也不需要操心內存夠不夠用,其實,這就是分頁機制給我們帶來的好處。它是實現虛擬存儲的關鍵,位于線性地址與物理地址之間,在使用這種內存分頁管理方法時,每個執行中的進程(任務)可以使用比實際內存容量大得多的連續地址空間。而且當系統內存實際上被分成很多凌亂的塊時,它可以建立一個大而連續的內存空間的映象,好讓程序不用操心和管理這些分散的內存塊。

分頁機制增強了分段機制的性能。頁地址變換是建立在段變換基礎之上的。因為,段管理機制對于Intel處理器來說是最基本的,任何時候都無法關閉。所以即使啟用了頁管理功能,分段機制依然是起作用的,段部件也依然工作。

分頁只能在保護模式(CR0.PE = 1)下使用。在保護模式下,是否開啟分頁,由 CR0. PG 位(位 31)決定:

  • 當 CR0.PG = 0 時,未開啟分頁,線性地址等同于物理地址;
  • 當 CR0.PG = 1 時,開啟分頁。

(3)分頁機制線性地址到物理地址轉換過程

80x86使用 4K 字節固定大小的頁面,每個頁面均是 4KB,并且對其于 4K 地址邊界處。這表示分頁機制把 2^32字節(4GB)的線性地址空間劃分成 2^20(1M = 1048576)個頁面。分頁機制通過把線性地址空間中的頁面重新定位到物理地址空間中進行操作。由于 4K 大小的頁面作為一個單元進行映射,并且對其于 4K 邊界,因此線性地址的低 12 位可做為頁內偏移地量直接作為物理地址的低 12 位。分頁機制執行的重定向功能可以看作是把線性地址的高 20 位轉換到對應物理地址的高 20 位。

線性到物理地址轉換功能,被擴展成允許一個線性地址被標注為無效的,而非要讓其產生一個物理地址。以下兩種情況一個頁面可以被標注為無效的:

  • 操作系統不支持的線性地址。
  • 對應的虛擬內存系統中的頁面在磁盤上而非在物理內存中。

在第一中情況下,產生無效地址的程序必須被終止,在第二種情況下,該無效地址實際上是請求 操作系統虛擬內存管理器 把對應的頁面從磁盤加載到物理內存中,以供程序訪問。因為無效頁面通常與虛擬存儲系統相關,因此它們被稱為不存在頁面,由頁表中稱為存在的屬性來確定。

當使用分頁時,處理器會把線性地址空間劃分成固定大小的頁面(4KB),這些頁面可以映射到物理內存中或磁盤存儲空間中,當一個程序引用內存中的邏輯地址時,處理器會把該邏輯地址轉換成一個線性地址,然后使用分頁機制把該線性地址轉換成對應的物理地址。

如果包含線性地址的頁面不在當前物理內存中,處理器就會產生一個頁錯誤異常。頁錯誤異常處理程序就會讓操作系統從磁盤中把相應頁面加載到物理內存中(操作過程中可能會把物理內存中不同的頁面寫到磁盤上)。當頁面加載到物理內存之后,從異常處理過程的返回操作會使異常的指令被重新執行。處理器把用于線性地址轉換成物理地址和用于產生頁錯誤的信息包含在存儲與內存中的頁目錄與頁表中。

(4)分頁機制與分段機制的不同

分頁與分段的最大的不同之處在于分頁使用了固定長度的頁面。段的長度通常與存放在其中的代碼或數據結構有相同的長度。與段不同,頁面有固定的長度。如果僅使用分段地址轉換,那么存儲在物理內存中的一個數據結構將包含其所有的部分。如果使用了分頁,那么一個數據結構就可以一部分存儲與物理內存中,而另一部分保存在磁盤中。

為了減少地址轉換所要求的總線周期數量,最近訪問的頁目錄和頁表會被存放在處理器的一個叫做轉換查找緩沖區(TLB)的緩沖器件中。TLB 可以滿足大多數讀頁目錄和頁表的請求而無需使用總線周期。只有當 TLB 中不包含所要求的頁表項是才會出現使用額外的總線周期從內存讀取頁表項。通常在一個頁表項很長時間沒有訪問過時才會出現這種情況。

3.2交換空間:內存不足時的后盾

首先呢,提一個概念,交換空間(swap space),這個大家應該不陌生,在重裝系統的時候,會讓你選擇磁盤分區,就比如說一個硬盤分幾個部分去管理。其中就會分一部分磁盤空間用作交換,叫做swap space。其實就是一段臨時存儲空間,內存不夠用的時候就用它了,雖然它也在磁盤中,但省去了很多的查找時間啊。當發生進程切換的時候,內存與交換空間就要發生數據交換一滿足需求。所以啊,進程的切換消耗是很大的,這也說明了為什么自旋鎖比信號量效率高的原因。

那么我們的程序里申請的內存的時候,linux內核其實只分配一個虛擬內存( 線性地址),并沒有分配實際的物理內存。只有當程序真正使用這塊內存時,才會分配物理內存。這就叫做延遲分配和請頁機制。釋放內存時,先釋放線性區對應的物理內存,然后釋放線性區;"請頁機制"將物理內存的分配延后了,這樣是充分利用了程序的局部性原來,節約內存空間,提高系統吞吐;就是說一個函數可能只在物理內存中呆了一會,用完了就被清除出去了,雖然在虛擬地址空間還在。(不過虛擬地址空間不是事實上的存儲,所以只能說這個函數占據了一段虛擬地址空間,當你訪問這段地址時,就會產生缺頁處理,從交換區把對應的代碼搬到物理內存上來)

Swap 交換機制是 Linux 虛擬內存管理的另一個重要組成部分。簡單來說,Swap 是磁盤上的一塊區域,當物理內存不足時,系統會將一部分暫時不用的內存頁面(Page)交換到 Swap 空間中,騰出物理內存給更需要的進程使用 。當被交換出去的頁面再次被訪問時,系統會將其從 Swap 空間換回到物理內存中。

Swap 交換機制的工作原理涉及到內存回收和頁面置換算法。當系統內存緊張時,內核會啟動內存回收機制,掃描內存中的頁面,選擇一些不常用或最近最少使用的頁面進行回收。如果這些頁面是匿名頁面(沒有關聯到文件的內存頁面,如進程的堆和棧空間),就會被交換到 Swap 空間中;如果是文件映射頁面(關聯到文件的內存頁面,如共享庫、文件緩存等),則會根據情況進行處理,臟頁面(被修改過的頁面)會被寫回文件,干凈頁面(未被修改過的頁面)可以直接釋放。

Swap 交換機制對系統性能有著重要的影響。當 Swap 使用頻繁時,說明物理內存不足,系統需要頻繁地在物理內存和 Swap 空間之間交換頁面,這會導致磁盤 I/O 增加,系統性能下降 。因為磁盤的讀寫速度遠遠低于內存,過多的 Swap 操作會使系統變得遲緩。因此,在實際應用中,需要合理配置 Swap 空間的大小,并密切關注系統的內存使用情況,避免 Swap 過度使用。

例如,可以通過調整/proc/sys/vm/swappiness參數來控制系統對 Swap 的使用傾向,swappiness的值范圍是 0 - 100,表示系統將內存頁面交換到 Swap 空間的傾向程度,值越大表示越傾向于使用 Swap 。一般來說,對于內存充足的系統,可以將swappiness設置為較低的值,如 10 或 20,以減少不必要的 Swap 操作;對于內存緊張的系統,可以適當提高swappiness的值,但也要注意不要過高,以免嚴重影響性能。

3.3內存映射:高效 I/O 的秘訣

內存映射,英文名為 Memory - mapped I/O,從字面意思理解,就是將磁盤文件的數據映射到內存中。在 Linux 系統中,這一機制允許進程把一個文件或者設備的數據關聯到內存地址空間,使得進程能夠像訪問內存一樣對文件進行操作 。

舉個簡單的例子,假設有一個文本文件,通常我們讀取它時,會使用read函數,數據從磁盤先讀取到內核緩沖區,再拷貝到用戶空間。而內存映射則直接在進程的虛擬地址空間中為這個文件創建一個映射區域,進程可以直接通過指針訪問這個映射區域,就好像文件數據已經在內存中一樣,大大簡化了文件操作的流程 。

內存映射的工作原理涉及到虛擬內存、頁表以及文件系統等多個方面的知識。當進程調用mmap函數進行內存映射時,大致會經歷以下幾個關鍵步驟 :

  1. 虛擬內存區域創建:系統首先在進程的虛擬地址空間中尋找一段滿足要求的連續空閑虛擬地址,然后為這段虛擬地址分配一個vm_area_struct結構,這個結構用于描述虛擬內存區域的各種屬性,如起始地址、結束地址、權限等,并將其插入到進程的虛擬地址區域鏈表或樹中 。就好比在一片空地上,規劃出一塊特定大小和用途的區域,并做好標記。
  2. 地址映射建立:通過待映射的文件指針,找到對應的文件描述符,進而鏈接到內核 “已打開文件集” 中該文件的文件結構體。再通過這個文件結構體,調用內核函數mmap,定位到文件磁盤物理地址,然后通過remap_pfn_range函數建立頁表,實現文件物理地址和進程虛擬地址的一一映射關系 。這一步就像是在規劃好的區域和實際的文件存儲位置之間建立起一條通道,讓數據能夠順利流通。不過,此時只是建立了地址映射,真正的數據還沒有拷貝到內存中 。
  3. 數據加載(缺頁異常處理):當進程首次訪問映射區域中的數據時,由于數據還未在物理內存中,會觸發缺頁異常。內核會捕獲這個異常,然后在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有找到,則調用nopage函數把所缺的頁從磁盤裝入到主存中 。這個過程就像是當你需要使用某個物品,但它不在身邊,你就需要去存放它的地方把它取回來。之后,進程就可以對這片主存進行正常的讀或寫操作,如果寫操作改變了數據內容,系統會在一定時間后自動將臟頁面回寫臟頁面到對應磁盤地址,完成寫入到文件的過程 。當然,也可以調用msync函數來強制同步,讓數據立即保存到文件里 。

mmap內存映射的實現過程,總的來說可以分為三個階段:

①進程啟動映射過程,并在虛擬地址空間中為映射創建虛擬映射區域

  • 進程在用戶空間調用庫函數mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
  • 在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續的虛擬地址
  • 為此虛擬區分配一個vm_area_struct結構,接著對這個結構的各個域進行了初始化
  • 將新建的虛擬區結構(vm_area_struct)插入進程的虛擬地址區域鏈表或樹中

②調用內核空間的系統調用函數mmap(不同于用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關系

  • 為映射分配了新的虛擬地址區域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護著和這個已打開文件相關各項信息。
  • 通過該文件的文件結構體,鏈接到file_operations模塊,調用內核函數mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數。
  • 內核mmap函數通過虛擬文件系統inode模塊定位到文件磁盤物理地址。
  • 通過remap_pfn_range函數建立頁表,即實現了文件地址和虛擬地址區域的映射關系。此時,這片虛擬地址并沒有任何數據關聯到主存中。

③進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝

注:前兩個階段僅在于創建虛擬區間并完成地址映射,但是并沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時。

  • 進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常。
  • 缺頁異常進行一系列判斷,確定無非法操作后,內核發起請求調頁過程。
  • 調頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有則調用nopage函數把所缺的頁從磁盤裝入到主存中。
  • 之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間后系統會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程。
  • 注:修改過的臟頁面并不會立即更新回文件中,而是有一段時間的延遲,可以調用msync()來強制同步, 這樣所寫的內容就能立即保存到文件里了。

四、內存管理機制的具體實現

4.1slab 分配器:小內存的管理者

slab分配器是Linux內核中用于管理小內存對象的一種高效內存分配機制,主要用于管理那些頻繁分配和釋放、大小相對固定的小內存對象,其設計目的是為了解決伙伴系統在分配小內存時存在的內存浪費和分配效率低的問題 。

slab 分配器的工作機制基于對象復用和緩存技術。它預先分配一組相同大小的內存塊,將這些內存塊組成一個緩存(Cache),每個緩存專門用于存儲特定類型的對象。當內核需要分配一個小內存對象時,首先會在對應的緩存中查找空閑的內存塊。如果緩存中有空閑塊,就直接從緩存中分配,避免了通過伙伴系統進行內存分配的開銷。當對象不再使用時,將其釋放回緩存中,而不是立即歸還給伙伴系統,以便后續再次使用。

例如,在 Linux 內核中,進程描述符(struct task_struct)是一種頻繁使用的小內存對象。slab 分配器會為進程描述符創建一個專門的緩存,在系統啟動時,預先分配一定數量的內存塊,這些內存塊的大小剛好適合存儲進程描述符。當創建一個新進程時,內核直接從這個緩存中獲取一個空閑的內存塊,用于存儲新進程的描述符。當進程結束時,對應的內存塊被釋放回緩存中,等待下一次使用。

①創建slab緩存區

該函數創建一個slab緩存(后備高速緩沖區),它是一個可以駐留任意數目全部同樣大小的后備緩存。其原型如下:

struct kmem_cache *kmem_cache_create(const char *name, size_t size, \
                   size_t align, unsigned long flags,\
                   void (*ctor)(void *, struct kmem_cache *, unsigned long),\
                   void (*dtor)(void *, struct kmem_cache *, unsigned ong)));

其中:

  • name:創建的緩存名;
  • size:可容納的緩存塊個數;
  • align:后備高速緩沖區中第一個內存塊的偏移量(一般置為0);
  • flags:控制如何進行分配的位掩碼,包括 SLAB_NO_REAP(即使內存緊缺也不自動收縮這塊緩存)、SLAB_HWCACHE_ALIGN ( 每 個 數 據 對 象 被 對 齊 到 一 個 緩 存 行 )、SLAB_CACHE_DMA(要求數據對象在 DMA 內存區分配)等);
  • ctor:是可選的內存塊對象構造函數(初始化函數);
  • dtor:是可選的內存對象塊析構函數(釋放函數)。

②分配slab緩存函數

一旦創建完后備高速緩沖區后,就可以調用kmem_cache_alloc()在緩存區分配一個內存塊對象了,其原型如下:

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

cachep指向開始分配的后備高速緩存,flags與傳給kmalloc函數的參數相同,一般為GFP_KERNEL。

③釋放slab緩存

該函數釋放一個內存塊對象:

void *kmem_cache_free(struct kmem_cache *cachep, void *objp);

④銷毀slab緩存

與kmem_cache_create對應的是銷毀函數,釋放一個后備高速緩存:

int kmem_cache_destroy(struct kmem_cache *cachep);

它必須等待所有已經分配的內存塊對象被釋放后才能釋放后備高速緩存區。

⑤slab緩存使用舉例

創建一個存放線程結構體(struct thread_info)的后備高速緩存,因為在linux中涉及頻繁的線程創建與釋放,如果使用__get_free_page()函數會造成內存的大量浪費,效率也不高。所以在linux內核的初始化階段就創建了一個名為thread_info的后備高速緩存,代碼如下:

/* 創建slab緩存 */
static struct kmem_cache *thread_info_cache;
thread_info_cache = kmem_cache_create("thread_info", sizeof(struct thread_info), \
                    SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL);

/* 分配slab緩存 */
struct thread_info *ti;
ti = kmem_cache_alloc(thread_info_cache, GFP_KERNEL);

/* 使用slab緩存 */
...
/* 釋放slab緩存 */
kmem_cache_free(thread_info_cache, ti);
kmem_cache_destroy(thread_info_cache);

這種機制在管理小對象時具有明顯的優勢。一方面,它減少了內存碎片的產生。由于緩存中的內存塊大小固定,且專門用于存儲特定類型的對象,避免了因不同大小的內存需求而導致的內存碎片化問題。另一方面,通過對象復用,減少了內存分配和釋放的次數,從而提高了內存分配的效率。因為從緩存中分配和釋放內存塊的操作比通過伙伴系統進行內存分配和釋放要快得多,減少了系統調用和內存管理的開銷,使得內核在處理大量小內存對象的分配和釋放時能夠更加高效地運行 。

4.2buddy 系統:大塊內存的管家

buddy 系統是 Linux 內核中用于管理物理內存的一種重要機制,主要負責大塊連續內存的分配和回收,其目標是高效地分配和回收連續的物理內存頁,減少外部碎片的產生,確保系統能夠滿足對大塊連續內存的需求 。

Linux 便是采用這著名的伙伴系統算法來解決外部碎片的問題。把所有的空閑頁框分組為 11 塊鏈表,每一塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512 和 1024 個連續的頁框。對1024 個頁框的最大請求對應著 4MB 大小的連續RAM 塊。每一塊的第一個頁框的物理地址是該塊大小的整數倍。例如,大小為 16個頁框的塊,其起始地址是 16 * 2^12 (2^12 = 4096,這是一個常規頁的大小)的倍數。

下面通過一個簡單的例子來說明該算法的工作原理:

假設要請求一個256(129~256)個頁框的塊。算法先在256個頁框的鏈表中檢查是否有一個空閑塊。如果沒有這樣的塊,算法會查找下一個更大的頁塊,也就是,在512個頁框的鏈表中找一個空閑塊。如果存在這樣的塊,內核就把512的頁框分成兩等分,一般用作滿足需求,另一半則插入到256個頁框的鏈表中。

如果在512個頁框的塊鏈表中也沒找到空閑塊,就繼續找更大的塊——1024個頁框的塊。如果這樣的塊存在,內核就把1024個頁框塊的256個頁框用作請求,然后剩余的768個頁框中拿512個插入到512個頁框的鏈表中,再把最后的256個插入到256個頁框的鏈表中。如果1024個頁框的鏈表還是空的,算法就放棄并發出錯誤信號。

①相關數據結構

#define MAX_ORDER 11

struct zone {
  ……
	struct free_area	free_area[MAX_ORDER];
	……
} 
struct free_area {
	struct list_head	free_list;
	unsigned long		nr_free;//該組類別塊空閑的個數
};

Zone結構體中的free_area數組的第k個元素,它保存了所有連續大小為2^k的空閑塊,具體是通過將連續頁的第一個頁插入到free_list中實現的,連續頁的第一個頁的頁描述符的private字段表明改部分連續頁屬于哪一個order鏈表。

②伙伴算法系統初始化

Linux內核啟動時,伙伴算法還不可用,linux是通過bootmem來管理內存,在mem_init中會把bootmem位圖中空閑的內存塊插入到伙伴算法系統的free_list中。

調用流程如下:

mem_init----->__free_all_bootmem()—>free_all_bootmem()>free_all_bootmem_core(NODE_DATA(0))–>free_all_bootmem_core(pgdat)

//利用free_page 將頁面分給伙伴管理器
free_all_bootmem
    return(free_all_bootmem_core(NODE_DATA(0)));  //#define NODE_DATA(nid)		(&contig_page_data)
        bootmem_data_t *bdata = pgdat->bdata;
        page = virt_to_page(phys_to_virt(bdata->node_boot_start));
		idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);
		map = bdata->node_bootmem_map;

		for (i = 0; i < idx; ) 
			unsigned long v = ~map[i / BITS_PER_LONG];
			//如果32個頁都是空閑的
			if (gofast && v == ~0UL)
				count += BITS_PER_LONG;
				__ClearPageReserved(page);
				order = ffs(BITS_PER_LONG) - 1;
				//設置32個頁的引用計數為1
				set_page_refs(page, order)

				//一次性釋放32個頁到空閑鏈表
				__free_pages(page, order);
									__free_pages_ok(page, order);
					list_add(&page->lru, &list);
					//page_zone定義如下return zone_table[page->flags >> NODEZONE_SHIFT];
					//接收一個頁描述符的地址作為它的參數,它讀取頁描述符的flags字段的高位,并通過zone_table數組來確定相應管理區描述符的地址,最終將頁框回收到對應的管理區中
					free_pages_bulk(page_zone(page), 1, &list, order);
				i += BITS_PER_LONG;
				page += BITS_PER_LONG;

			//這32個頁中,只有部分是空閑的	
			else if (v)
				for (m = 1; m && i < idx; m<<=1, page++, i++)
					if (v & m)
						count++;
						__ClearPageReserved(page);
						set_page_refs(page, 0);
						//釋放單個頁
						__free_page(page);
			else
				i+=BITS_PER_LONG;
				page += BITS_PER_LONG;

		//釋放內存分配位圖本身		
		page = virt_to_page(bdata->node_bootmem_map);
		for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++)
			__ClearPageReserved(page);
			set_page_count(page, 1);
			__free_page(page);

③伙伴算法系統分配空間

page = __rmqueue(zone, order);
	//從所請求的order開始,掃描每個可用塊鏈表進行循環搜索。
    for (current_order = order; current_order < MAX_ORDER; ++current_order)
        area = zone->free_area + current_order;
        if (list_empty(&area->free_list))
            continue; 
        page = list_entry(area->free_list.next, struct page, lru);  
		//首先在空閑塊鏈表中刪除第一個頁框描述符。
        list_del(&page->lru);
		//清楚第一個頁框描述符的private字段,該字段表示連續頁框屬于哪一個大小的鏈表
        rmv_page_order(page);
        area->nr_free--;
        zone->free_pages -= 1UL << order;
		//如果是從更大的order鏈表中申請的,則剩下的要重新插入到鏈表中
        return expand(zone, page, order, current_order, area);
            unsigned long size = 1 << high;
            while (high > low)
                area--;
                high--;
                size >>= 1; 
				//該部分連續頁面插入到對應的free_list中
                list_add(&page[size].lru, &area->free_list); 
                area->nr_free++;
				//設置該部分連續頁面的order
                set_page_order(&page[size], high);
                    page->private = order;
                    __SetPagePrivate(page);
                         __set_bit(PG_private, &(page)->flags)
            return page;

④伙伴算法系統回收空間

free_pages_bulk
	//linux內核將空間分為三個區,分別是ZONE_DMA、ZONE_NORMAL、ZONR_HIGH,zone_mem_map字段就是指向該區域第一個頁描述符
    struct page *base = zone->zone_mem_map;
    while (!list_empty(list) && count--)  
        page = list_entry(list->prev, struct page, lru);
        list_del(&page->lru);
        __free_pages_bulk
            int order_size = 1 << order;
			//該段空間的第一個頁的下標
            page_idx = page - base; 
            zone->free_pages += order_size;
			//最多循環10 - order次。每次都將一個塊和它的伙伴進行合并。
            while (order < MAX_ORDER-1)
				//尋找伙伴,如果page_idx=128,order=4,則buddy_idx=144
                buddy_idx = (page_idx ^ (1 << order)); 
                buddy = base + buddy_idx;
                /**
                 * 判斷伙伴塊是否是大小為order的空閑頁框的第一個頁。
                 * 首先,伙伴的第一個頁必須是空閑的(_count == -1)
                 * 同時,必須屬于動態內存(PG_reserved被清0,PG_reserved為1表示留給內核或者沒有使用)
                 * 最后,其private字段必須是order
                 */
                if (!page_is_buddy(buddy, order))
                    break;
                list_del(&buddy->lru);
                area = zone->free_area + order;
				//原先所在的區域空閑頁減少
                area->nr_free--;   
                rmv_page_order(buddy);
                    __ClearPagePrivate(page);
                    page->private = 0;
                page_idx &= buddy_idx;
                order++;

			/**
			 * 伙伴不能與當前塊合并。
			 * 將塊插入適當的鏈表,并以塊大小的order更新第一個頁框的private字段。
			 */
            coalesced = base + page_idx;
            set_page_order(coalesced, order);
            list_add(&coalesced->lru, &zone->free_area[order].free_list);
            zone->free_area[order].nr_free++;

4.3THP:大內存頁面的優化器

THP(Transparent Huge Page,透明大頁)是 Linux 內核中的一項內存管理優化技術,它主要用于處理大內存頁面的分配和管理,旨在提高內存訪問效率,特別是對于那些需要頻繁訪問大量內存的應用程序 。

THP 的原理是將多個連續的物理內存頁合并成一個大的內存頁,通常大小為 2MB 或 1GB,而不是使用傳統的 4KB 小頁。這樣做的好處是減少了頁表項的數量,因為一個大頁只需要一個頁表項來映射,而多個小頁則需要多個頁表項。減少頁表項不僅節省了內存空間,還提高了地址轉換的速度,因為在進行虛擬地址到物理地址的轉換時,查找一個頁表項比查找多個頁表項要快。此外,大頁還能提高 TLB(Translation Lookaside Buffer,轉換后備緩沖器)的命中率,TLB 是一種高速緩存,用于存儲最近訪問的頁表項。由于大頁減少了頁表項的數量,使得 TLB 能夠緩存更多的有效映射,從而減少了 TLB 缺失的次數,進一步提高了內存訪問的速度 。

例如,對于一個需要頻繁訪問大量內存的數據庫應用程序,使用 THP 可以顯著提高其性能。假設該數據庫應用程序需要訪問 1GB 的內存,如果使用傳統的 4KB 小頁,需要 262144 個頁表項來映射;而使用 2MB 的大頁,只需要 512 個頁表項。這樣,不僅減少了頁表項占用的內存空間,還加快了地址轉換的速度,提高了數據庫的讀寫性能 。

THP 適用于那些具有良好內存訪問局部性的應用程序,即應用程序在一段時間內主要訪問內存的某個局部區域。對于這類應用程序,使用大頁可以充分發揮 THP 的優勢,提高內存訪問效率。然而,對于內存訪問比較分散的應用程序,THP 可能并不適用,因為大頁的分配可能會導致內存碎片問題,反而降低系統性能 。

// mm/khugepaged.c
static void collapse_huge_page(struct mm_struct *mm, unsigned long address)
{
    // 1. 檢查是否可合并
    if (!khugepaged_scan_pmd(mm, vma, addr))
        return;

    // 2. 分配2MB大頁
    huge_page = alloc_pages(HPAGE_PMD_ORDER, ...);

    // 3. 復制小頁內容
    copy_page_to_huge_page(huge_page, pages, address);

    // 4. 替換頁表項
    set_pmd_at(mm, address, pmd, pmd_mkhuge(pfn_pmd(pfn, prot)));
}

大頁的三大陷阱:

內存浪費:小內存應用被迫使用2MB大頁

# 查看大頁內存碎片
$ grep Huge /proc/meminfo
AnonHugePages: 102400 kB
HugePages_Free: 512  # 空閑大頁

延遲波動:khugepaged合并操作引入不可預測延遲

// 合并操作可能阻塞
down_write(&mm->mmap_sem); // 獲取寫鎖

OOM風險:大頁不可交換,加劇內存壓力

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2025-01-02 11:06:22

2023-05-08 12:03:14

Linux內核進程

2011-06-15 14:45:26

上網行為管理技術

2024-03-08 10:50:44

Spring技術應用程序

2022-05-07 14:31:46

物聯網

2025-04-25 08:05:00

IP地址CIDRVLSM

2025-04-03 07:00:00

2020-11-08 16:16:12

Linux硬盤RAID

2023-02-26 23:13:24

存儲LinuxRAID

2009-06-15 17:54:50

Java核心技術

2022-05-09 08:21:29

Spring微服務Sentinel

2017-03-08 10:06:11

Java技術點注解

2009-06-26 16:01:39

EJB組織開發EJB容器EJB

2023-06-14 08:49:22

PodKubernetes

2016-11-15 14:33:05

Flink大數據

2018-05-18 09:07:43

Linux內核內存

2023-10-12 19:41:55

2010-09-02 10:56:37

IOS軟件備份

2020-01-15 10:29:29

區塊鏈架構模型

2025-04-28 00:55:00

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 91在线资源 | 久久精品av| 久久一级免费视频 | 亚洲国产一区在线 | 久久久久91 | 成人亚洲一区 | 久久综合99| 国产美女自拍视频 | 日韩精品一区中文字幕 | 国产精品一区二区欧美黑人喷潮水 | 性色av香蕉一区二区 | 天天弄 | 国产免费一区二区三区最新6 | 国产精品欧美一区喷水 | 国产免费一区 | 亚洲精品乱码久久久久久久久 | 国产免费视频 | 亚洲一区二区在线 | 欧美大片一区二区 | 综合久久一区 | 国产麻豆乱码精品一区二区三区 | 免费观看的av | 国产色婷婷精品综合在线手机播放 | 成人精品高清 | 91精品国产91久久久久久最新 | 国产在线一区二区三区 | 美女福利网站 | 久久99国产精品久久99果冻传媒 | 久久免费大片 | 久久久久亚洲视频 | 成人精品一区二区 | 超碰人人91| 亚洲视频中文字幕 | 国产高清免费视频 | 日本中文字幕一区 | 亚洲综合二区 | 国产黄色小视频在线观看 | 极情综合网 | 国产成人精品一区二区三区 | 中文字幕在线播放第一页 | 亚洲二区在线 |