太好了,一文就能讀懂,作業系統原理之Linux記憶體管理
真實模式和保護模式
CPU復位或者上電的時候以真實模式啟動,不能實現許可權分級,
只能訪問1M記憶體
,存在時間很短,之後載入作業系統,進入保護模式
保護模式
目的:保護程序地址空間
保護模式下,
段值變成一個索引,指向GDT的一個段描述符
8086處理器有20根地址線,但是內部暫存器是16位的
解決:物理地址 = 段地址 << 4 + 偏移地址
文章相關影片講解:
c/c++Linux後臺伺服器開發高階架構師學習影片
Linux核心記憶體管理---記憶體對映
Linux核心記憶體管理(二)
虛擬記憶體空間之VMA實戰操作
剖析Linux核心MMU機制
PS:影片相關學習文件,
點選獲取
分段機制
邏輯地址:
段地址和偏移地址
線性地址:
描述虛擬記憶體空間
物理地址
在記憶體中構建兩個儲存段基址的表,
GDT(Global Descriptor Table,全域性描述符表)
,
儲存核心程式的段基址
,由暫存器GDTR儲存GDT入口地址,
LDT(Local Descriptor Table,本地描述符表)
,由暫存器LDTR指向LDT的入口地址,儲存各個使用者程序的段基址。table中的條目儲存
段描述符
(8位元組),其中包括
段基址、段界限和許可權
相關資訊
CS、DS等段基址暫存器仍然為16位(相容以前),但改為儲存
段選擇子
,即table的
索引
,段基址儲存地址時是
真實模式
,儲存段選擇子時是
保護模式
Linux為了相容其它體系架構(這些體系都沒有分段地址模式),同時也為了減少一次地址對映,
將所有段基址都設定為0
,也就是所有程序的任一段基址都為0,所以
Linux沒必要使用LDT,僅用了GDT
linux沒有使用全部的分段功能,linux分段用來做許可權稽核,linux傾向於分頁
優點:
方便程式設計,分段保護
缺點:
物理記憶體分配比較麻煩,容易出現外碎片
分頁機制
將程式的邏輯地址空間劃分為固定大小的
頁(page)
,而物理記憶體劃分為同樣大小的
頁框(page frame)
,程式載入時,可將任意一頁放入記憶體中的任意一個頁框,這些頁框不必連續,從而實現離散分配
優點:
沒有外碎片,每個內碎片不超過頁大小,提高記憶體利用率
一個程式不必連續存放
方便改變程序佔用空間大小
頁大小是4K,並且是4K對齊的。線性地址透過分頁機制轉換成物理地址時,如果某個線性地址對應的頁不存在,那麼訪問的時候將產生一個
缺頁異常
線性地址經過分頁機制的轉換變成物理地址,其實是透過兩個表,一個是
頁目錄表PDE
,也叫一級目錄,另一個是
二級頁表PTE
。程序的虛擬地址首先透過其區域性段描述符變換為CPU整個線性地址空間中的地址,然後再使用頁目錄表PDE(一級頁表)和頁表PTE(二級頁表)對映到實際物理地址頁上。頁表中,每項的大小是32b,其中20b用來存放頁面的物理地址,12b用於存放屬性資訊。頁表含有1M個表項,每項4B。第一級表是頁目錄,存放在1頁4k頁面中,含有1K個表項。第二級是頁表,也是1K個表項
處理器會將最近經常用到的頁目錄和頁表項儲存在TLB中。
TLB就是頁表的Cache
,其中儲存了當前最可能被訪問到的頁表項,其內容是部分頁表項的一個副本。只有在TLB無法完成地址翻譯任務時,才會到記憶體中查詢頁表,這樣就減少了頁表查詢導致的處理器效能下降
當頁目錄或者頁表項被修改的時候,對應TLB中的專案也就失效了;當CR3重新載入的時候,所有的TLB都失效了,除非頁或頁表條目的G位被設定
CR3又叫PDBR,就是頁目錄暫存器。它的高20位將是頁目錄表首地址的高20位,頁目錄表的低12位會是零,也就是說,頁目錄表會是4KB對齊。類似地,PDE中的頁表基址以及PTE中的頁基址也是用高20位來表示4KB對齊的頁表和頁
分段和分頁
分段面向程式:便於編譯和進行段保護,分頁面向物理記憶體:實現離散分配,提高記憶體利用率
頁的大小固定,由系統決定,段的長度不固定,取決於程式
虛擬地址空間
對於32位系統,0-3G用於使用者空間,3G-4G用於核心空間
並非所有的虛擬記憶體都會分配物理記憶體,只有實際使用的虛擬空間才會分配物理記憶體,透過記憶體對映把虛擬記憶體地址對映到物理記憶體地址
讓物理記憶體擴充成更大的邏輯記憶體,每個程式擁有自己的地址空間,這個地址空間被分割成多個頁,這些頁被對映到物理記憶體,
但不需要對映到連續的物理記憶體,也不需要所有頁都必須在物理記憶體中
,當程式用到不在物理記憶體中的頁時,由硬體執行必要的對映,將缺失的部分裝入物理記憶體並重新執行失敗的指令
棧幀
函式的返回地址和引數
臨時變數:包括函式的非靜態區域性變數以及編譯器自動生成的其他臨時變數
儲存的上下文:包括在函式呼叫前後需要保持不變的暫存器
核心地址空間
直接對映區:線性空間中從 3G 開始最大 896M 的區間,為直接記憶體對映區
動態記憶體對映區:該區域由核心函式 vmalloc 來分配
永久記憶體對映區:該區域可訪問高階記憶體
固定對映區:該區域和 4G 的頂端只有 4k 的隔離帶,其每個地址項都服務於特定的用途,如: ACPI_BASE 等
文章福利 Linux後端開發網路底層原理知識學習提升 點選
學習資料
獲取,完善技術棧,內容知識點包括Linux,Nginx,ZeroMQ,MySQL,Redis,執行緒池,MongoDB,ZK,Linux核心,CDN,P2P,epoll,Docker,TCP/IP,協程,DPDK等等。
MMU
包含分段部件和分頁部件
CPU中含有一個被稱為記憶體管理單元(Memory Management Unit, MMU)的硬體,它的功能是將虛擬地址轉換為物理地址。MMU需要藉助存放在記憶體中的頁表來動態翻譯虛擬地址,該頁表由作業系統管理。
當程序訪問的虛擬地址在頁表中無法找到時,就會觸發缺頁異常,進入核心空間分配物理記憶體,更新程序頁表,最後返回使用者空間,繼續執行
記憶體分配與回收
malloc原理
malloc有兩種實現方式:
brk和mmap
對於
小塊記憶體(小於128K)使用brk分配
,即透過移動堆頂的位置分配記憶體
對於
大塊記憶體(大於128K)使用mmap分配
,即在檔案對映區域找一塊空閒記憶體分配出去
brk可減少缺頁異常的發生
,提高記憶體訪問效率,由於記憶體沒有歸還系統,可能會造成
記憶體碎片
mmap分配的記憶體在釋放時歸還系統,
每次mmap都會發生缺頁異常
上面兩種呼叫發生後都沒有真正分配物理記憶體,只有在首次訪問時,透過缺頁異常進入核心,才真正分配物理記憶體,建立虛擬記憶體和物理記憶體之間的對映關係
當一個程序發生缺頁中斷的時候,程序會陷入核心態,執行以下操作:
檢查要訪問的虛擬地址是否合法
查詢/分配一個物理頁
填充物理頁內容(
讀取磁碟,或者直接置0,或者啥也不幹
),
如果需要讀取磁碟,那麼該缺頁中斷就是majflt,否則就是minflt
建立對映關係(虛擬地址到物理地址)
重新執行發生缺頁中斷的那條指令
malloc一次能夠申請的最大空間是多少
kmalloc/vmalloc/malloc
kmalloc和vmalloc分配的是核心的記憶體,malloc分配的是使用者的記憶體
kmalloc保證分配的記憶體在物理上連續,vmalloc保證分配的記憶體在虛擬地址空間上連續
kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相對較大
記憶體只有在被DMA訪問的時候才需要物理上連續
vmalloc比kmalloc要慢
小物件分配記憶體的方案
在使用者空間,malloc透過brk分配的記憶體,在釋放時並不立即歸還系統,而是快取起來重複利用
在核心空間,Linux透過slab分配器來管理小記憶體。slab可認為是構建在夥伴系統上的一個快取,主要作用就是分配並釋放核心中的小物件
記憶體緊張時的解決方案
回收快取,比如使用LRU演算法,回收最近使用最少的記憶體頁面
回收不常訪問的記憶體,把不常用的記憶體透過交換分割槽直接寫到磁碟中
殺死程序,記憶體緊張時系統還會透過 OOM(Out of Memory),直接殺掉佔用大量記憶體的程序
系統對堆空間的管理方式
空閒連結串列法
就是把堆中各個空閒的塊按照連結串列的方式連線起來,當用戶請求一塊空間的時候,可以遍歷整個列表,直到找到合適大小的塊並且將它拆分;當用戶釋放空間的時候將它合併到空閒連結串列中
點陣圖法
將整個堆劃分為大量的塊(block),每個塊的大小相同,當用戶請求記憶體的時候,總是分配整數個塊的空間給使用者,第一個塊我們稱為已分配區域的頭(Head),其餘的稱為己分配區域的主體(Body),而我們可以使用一個整數陣列來記錄塊的使用情況,由於每個塊只有頭/主體/空閒三種狀態,因此僅僅需要兩位即可表示一個塊,因此稱為點陣圖
物件池
核心態和使用者態的區別
當程序執行系統呼叫而陷入核心程式碼中執行時,就進入了核心態。當程序處於核心態時,執行核心程式碼會使用核心棧,每個程序都有自己的核心棧
當程序在執行使用者自己的程式碼時,就處於使用者態
當程序正在執行使用者程式碼而突然中斷時,此時也可以認為是處於核心態
核心態與使用者態是作業系統的兩種執行級別
,Intel CPU提供Ring0 - Ring3三種級別執行模式,Ring0級別最高,Ring3級別最低。
Linux使用了Ring3級別執行使用者態,Ring0級別執行核心態,沒有使用Ring1和Ring2級別
使用者態切換到核心態的3種方式
系統呼叫
這是使用者程序主動要求切換到核心態的一種方式,系統呼叫的機制還是使用了
作業系統為使用者特別開放的一箇中斷來實現
,例如Linux的80中斷
異常
當CPU在執行執行在使用者態的程式時,發現了某些事件不可知的異常,會觸發由當前執行程序切換到處理此異常的核心相關程式中,也就到了核心態,比如缺頁異常
外圍裝置的中斷
當外圍裝置完成使用者請求的操作之後,會向CPU發出相應的中斷訊號,這時CPU會暫停執行下一條將要執行的指令轉而去執行中斷訊號的處理程式,如果先執行的指令是使用者態下的程式,那麼這個轉換的過程自然也就發生了有使用者態到核心態的切換。比如硬碟讀寫操作完成,系統會切換到硬碟讀寫的中斷處理程式中執行後續操作
具體的切換操作
最終實際完成由使用者態到核心態的切換操作都相當於執行了一箇中斷響應的過程,因為系統呼叫實際上是中斷機制實現的,而異常和中斷處理機制基本上是一樣的,使用者態切換到核心態的步驟主要包括:
從當前程序的描述符中提取其核心棧的ss0及esp0資訊
使用ss0和esp0指向的核心棧將當前程序的cs,eip,eflags,ss,esp資訊儲存起來,這個過程也完成了由使用者棧找到核心棧的切換過程,同時儲存了被暫停執行的程式的下一條指令
將先前由中斷向量檢索得到的中斷處理程式的cs,eip資訊裝入相應的暫存器,開始執行中斷處理程式,這時就轉到了核心態的程式執行了
頁面置換演算法
程式執行過程中,如果要訪問的頁面不在記憶體中,就發生缺頁中斷從而將該頁調入記憶體中。此時如果記憶體已無空閒空間,系統必須從記憶體中調出一個頁面到磁碟交換區中來騰出空間
頁面置換演算法和快取淘汰策略類似
,可以將記憶體看成磁碟的快取。在快取系統中,快取的大小有限,當有新的快取到達時,需要淘汰一部分已經存在的快取,這樣才有空間存放新的快取資料
頁面置換演算法的主要目標是使
頁面置換頻率最低
最佳置換演算法(OPT)
選擇換出的頁面是
在未來最長時間內不再被訪問的頁面
,這是一種
理論上的演算法
,因為無法知道一個頁面在未來多長時間不再被訪問
先進先出置換演算法(FIFO)
選擇換出的頁面是
最先進入記憶體的頁面
實現需要在記憶體中維護一個所有頁面的連結串列,當一個頁面被訪問時,將這個頁面插到連結串列表頭,這樣就能保證
連結串列表尾的頁面是駐留時間最長的
會將那些經常被訪問的頁面也換出
,可能出現
Belady現象
Belady現象
:採用FIFO演算法時,如果對一個程序未分配所要求的的全部頁面,有時會出現分配的頁面數增多但缺頁率反而提高的異常現象
最近最久未使用演算法(LRU)
雖然無法知道將來要使用頁面的情況,但是可以知道過去使用頁面的情況
。選擇換出的頁面是
最近最久未使用的頁面
實現需要在記憶體中維護一個所有頁面的連結串列,當一個頁面被訪問時,將這個頁面移到連結串列表頭,這樣就能保證
連結串列表尾的頁面是最近最久未訪問的
每次訪問都需要遍歷連結串列,代價高
最少使用置換演算法(LFU)
為每個頁面配置一個計數器,一旦某頁被訪問,將其計數器加1,需要選擇一頁置換時,選擇計數器值最小的頁面,即記憶體中訪問次數最少的頁面進行淘汰
LRU和LFU的區別:LRU考察的是多久未訪問,時間越短越好;而LFU考察的是訪問的次數或頻率,訪問次數越多越好
時鐘置換演算法(最近未使用演算法,NRU)
FIFO演算法的改進,避免把經常使用的頁面置換出去
簡單的CLOCK演算法為每個頁面設定一個訪問位,再將記憶體中的頁面組織成一個迴圈佇列。當某頁被訪問時,其訪問位置1。當需要淘汰一個頁面時,只需檢查頁的訪問位,如果是0,就將該頁換出,如果是1,則將它置0,暫不換出,繼續檢查下一個頁面,若第一輪掃描中所有頁面都是1,則將這些頁面的訪問位依次置0後,再進行第二輪掃描(第二輪掃描一定會有訪問位為0的頁面,因此簡單的CLOCK演算法選擇一個淘汰頁面最多會經過兩輪掃描)
改進的時鐘置換演算法
若用(訪問位,修改位)的形式表述,則第一輪:淘汰(0,0), 第二輪:淘汰(0,1),並將掃描過的頁面訪問位都置為0, 第三輪:淘汰(0,0),第四輪:淘汰(0, 1)
微核心和宏核心
宏核心/單核心:把程序、執行緒管理、記憶體管理、檔案系統,驅動,網路協議等等都整合在核心裡面,例如linux核心。
優點:
效率高
;
缺點:
穩定性差
,開發過程中的bug經常會導致整個系統掛掉。
微核心:核心中只有
最基本的排程、記憶體管理
。驅動、檔案系統等都是使用者態的守護程序去實現的。
優點:
穩定性高
,驅動等的錯誤只會導致相應程序死掉,不會導致整個系統都崩潰;
缺點:
效率低
。
缺頁異常
缺頁異常指的是當程序試圖訪問已對映在虛擬地址空間,但並未被載入在物理記憶體中的一個分頁時,由CPU所觸發的中斷。
缺頁中斷會使程序陷入核心,然後執行
以下操作
:
檢查要訪問的虛擬地址是否合法
查詢/分配一個物理頁
填充物理頁內容(讀取磁碟,或者直接置0,或者啥也不幹)
建立對映關係(虛擬地址到物理地址)
與普通的中斷的區別在於:
在指令執行期間產生和處理缺頁中斷訊號
一條指令在執行期間,可能產生多次缺頁中斷
缺頁中斷返回時,執行產生中斷的那一條指令,而一般的中斷返回時,執行下一條指令
記憶體碎片
有兩種
內部
碎片:因為所有的記憶體分配需要滿足
位元組對齊
,所以通常會多分配一點不需要的多餘記憶體空間,造成內部碎片。如:申請43 byte,因為沒有合適大小的記憶體,會分配44 byte或48 byte,就會存在1 byte或3 byte的多餘空間
外部
碎片:頻繁的分配與回收物理頁面會導致大量的、連續且小的頁面塊
夾雜
在已分配的頁面中間,從而產生外部碎片。比如有一塊共有100個單位的連續空閒記憶體空間,範圍為0~99,如果從中申請了一塊10 個單位的記憶體塊,那麼分配出來的就是0~9。這時再繼續申請一塊 5個單位的記憶體塊,這樣分配出來的就是 10~14。如果將第一塊釋放,此時整個記憶體塊只佔用了 10~14區間共 5個單位的記憶體塊。然後再申請20個單位的記憶體塊,此時只能從 15開始,分配15~24區間的記憶體塊,如果以後申請的記憶體塊都大於10個單位,那麼 0~9 區間的記憶體塊將不會被使用,變成外部碎片
夥伴演算法
就是將記憶體分成若干塊,然後儘可能以最適合的方式滿足程式記憶體需求的一種記憶體管理演算法,記憶體釋放後,檢查與該記憶體相鄰的記憶體是否是同樣大小並且同樣處於空閒的狀態,如果是,則將這兩塊記憶體合併,然後程式遞迴進行同樣的檢查。
夥伴演算法的優點是能夠
完全避免外部碎片的產生
申請時,夥伴演算法會給程式分配一個較大的記憶體空間,即保證所有大塊記憶體都能得到滿足。
夥伴演算法的缺點是
會產生內部碎片
,當分配比需求還大的記憶體空間,就會產生內部碎片。
slab allocation
的基本原理:將分配的記憶體分割成各種尺寸的塊,並把相同尺寸的塊分成組。另外分配到的記憶體
只會回收、不會釋放
,返回到對應的組,重複利用。
對從Buddy拿到的記憶體進行二次管理,以
更小的單位
進行分配和回收(注意,是回收而不是釋放),防止了空間的浪費。
讓頻繁使用的物件儘量分配在
同一塊記憶體區間
並
保留基本資料結構
,提高程式效率。
夥伴演算法與slab演算法的關係:
slab與Buddy都是記憶體分配器。
slab的記憶體來自Buddy
slab與Buddy在演算法上級別對等。Buddy把記憶體條 當作一個池子來管理,slab是把從Buddy拿到的記憶體當作一個池子來管理的。
fork/vfork
fork()會複製父程序的頁表,而vfork()不會複製,直接讓子程序共用父程序的地址空間
fork()的父子程序的執行次序不確定;vfork()保證子程序先執行,在呼叫exec或exit之前與父程序資料是共享的,在它呼叫exec或exit之後父程序才可能被排程執行,如果呼叫exec或exit之前子程序依賴於父程序的某些動作,則會導致死鎖
vfork的出現原因:在實現寫時複製之前,為了避免fork後立刻執行exec所造成的地址空間的浪費
寫時複製(Copy-On-Write, COW)
寫時複製是一種惰性最佳化方法,以減少fork時對父程序空間程序整體複製帶來的開銷
如果有多個程序要讀取同一資源的副本,那麼複製是不必要的,每個程序只要儲存一個指向這個資源的指標就可以了,這樣就存在著幻覺:每個程序好像獨佔那個資源,從而避免複製的開銷
如果一個程序要修改自己的那份資源“副本”,那麼就會複製那份資源
,並把複製的那份提供給程序,複製過程對於程序來說是透明的。這個程序就可以修改複製後的資源了,同時其他的程序仍然共享那份沒有修改過的資源
如果程序從來就不需要修改資源,則不需要進行復制。惰性演算法的好處就在於它們儘量推遲代價高昂的操作,直到必要的時刻才會去執行
在使用虛擬記憶體的情況下,寫時複製是以頁為基礎進行的
在fork()結束後,父程序和子程序都相信它們有一個自己的地址空間,但實際上它們共享了父程序的原始頁
在核心實現中,寫時複製觸發
缺頁中斷
,處理缺頁中斷的方法是對該頁進行一次透明覆制