內存工做原理

Reference:https://time.geekbang.org/column/article/74272算法

 

內存

內存主要用來存儲系統和應用程序的指令、數據、緩存等。緩存

 

內存映射

一般所說的內存容量,好比筆記本電腦的8GB內存,其實指的是物理內存。物理內存也稱爲主存,大多數計算機用的主存都是動態隨機訪問內存(DRAM)。只有內核才能夠直接訪問物理內存。ssh

Linux 內核給每一個進程都提供了一個獨立的虛擬地址空間,而且這個地址空間是連續的。這樣,進程就能夠很方便地訪問內存,更確切地說是訪問虛擬內存。ide

虛擬地址空間的內部又被分爲內核空間和用戶空間兩部分,不一樣字長(也就是單個CPU指令能夠處理數據的最大長度)的處理器,地址空間的範圍也不一樣。好比最多見的 32 位和 64 位系統,它們的虛擬地址空間,以下所示:函數

經過這裏能夠看出,32位系統的內核空間佔用 1G,位於最高處,剩下的3G是用戶空間。而 64 位系統的內核空間和用戶空間都是 128T,分別佔據整個內存空間的最高和最低處,剩下的中間部分是未定義的。工具

進程在用戶態時,只能訪問用戶空間內存;只有進入內核態後,才能夠訪問內核空間內存。雖然每一個進程的地址空間都包含了內核空間,但這些內核空間,其實關聯的都是相同的物理內存。這樣,進程切換到內核態後,就能夠很方便地訪問內核空間內存。性能

既然每一個進程都有一個這麼大的地址空間,那麼全部進程的虛擬內存加起來,天然要比實際的物理內存大得多。因此,並非全部的虛擬內存都會分配物理內存,只有那些實際使用的虛擬內存才分配物理內存,而且分配後的物理內存,是經過內存映射來管理的。url

內存映射,其實就是將虛擬內存地址映射到物理內存地址。爲了完成內存映射,內核爲每一個進程都維護了一張頁表,記錄虛擬地址與物理地址的映射關係,以下圖所示:spa

頁表實際上存儲在 CPU 的內存管理單元 MMU中,這樣,正常狀況下,處理器就能夠直接經過硬件,找出要訪問的內存。
而當進程訪問的虛擬地址在頁表中查不到時,系統會產生一個缺頁異常,進入內核空間分配物理內存、更新進程頁表,最後再返回用戶空間,恢復進程的運行。.net

另外,TLB(Translation Lookaside Buffer,轉譯後備緩衝器)會影響 CPU 的內存訪問性能,TLB 其實就是 MMU 中頁表的高速緩存。因爲進程的虛擬地址空間是獨立的,而 TLB 的訪問速度又比 MMU 快得多,因此,經過減小進程的上下文切換,減小TLB的刷新次數,就能夠提升TLB 緩存的使用率,進而提升CPU的內存訪問性能。

不過要注意,MMU 並不以字節爲單位來管理內存,而是規定了一個內存映射的最小單位,也就是頁,一般是 4 KB大小。這樣,每一次內存映射,都須要關聯 4 KB 或者 4KB 整數倍的內存空間。

頁的大小隻有4 KB ,致使的另外一個問題就是,整個頁表會變得很是大。比方說,僅 32 位系統就須要 100 多萬個頁表項(4GB/4KB),才能夠實現整個地址空間的映射。爲了解決頁表項過多的問題,Linux 提供了兩種機制,也就是多級頁表和大頁(HugePage)。

多級頁表就是把內存分紅區塊來管理,將原來的映射關係改爲區塊索引和區塊內的偏移。因爲虛擬內存空間一般只用了不多一部分,那麼,多級頁表就只保存這些使用中的區塊,這樣就能夠大大地減小頁表的項數。

Linux用的正是四級頁表來管理內存頁,以下圖所示,虛擬地址被分爲5個部分,前4個表項用於選擇頁,而最後一個索引表示頁內偏移。

大頁,就是比普通頁更大的內存塊,常見的大小有 2MB 和 1GB。大頁一般用在使用大量內存的進程上,好比 Oracle、DPDK等。
經過這些機制,在頁表的映射下,進程就能夠經過虛擬地址來訪問物理內存了。

 

虛擬內存空間分佈

最上方的內核空間不用多講,下方的用戶空間內存,其實又被分紅了多個不一樣的段。以32 位系統爲例,以下圖:

經過這張圖能夠看到,用戶空間內存,從低到高分別是五種不一樣的內存段。

  1. 只讀段,包括代碼和常量等。
  2. 數據段,包括全局變量等。
  3. 堆,包括動態分配的內存,從低地址開始向上增加。
  4. 文件映射段,包括動態庫、共享內存等,從高地址開始向下增加。
  5. 棧,包括局部變量和函數調用的上下文等。棧的大小是固定的,通常是 8 MB。

在這五個內存段中,堆和文件映射段的內存是動態分配的。好比說,使用 C 標準庫的 malloc() 或者 mmap() ,就能夠分別在堆和文件映射段動態分配內存。
其實64位系統的內存分佈也相似,只不過內存空間要大得多。

 

內存分配與回收

malloc() 是 C 標準庫提供的內存分配函數,對應到系統調用上,有兩種實現方式,即 brk() 和 mmap()。

  • 對小塊內存(小於128K),C 標準庫使用 brk() 來分配,也就是經過移動堆頂的位置來分配內存。這些內存釋放後並不會馬上歸還系統,而是被緩存起來,這樣就能夠重複使用。
  • 而大塊內存(大於 128K),則直接使用內存映射 mmap() 來分配,也就是在文件映射段找一塊空閒內存分配出去。

這兩種方式,天然各有優缺點。

  • brk() 方式的緩存,能夠減小缺頁異常的發生,提升內存訪問效率。不過,因爲這些內存沒有歸還系統,在內存工做繁忙時,頻繁的內存分配和釋放會形成內存碎片。
  • mmap() 方式分配的內存,會在釋放時直接歸還系統,因此每次 mmap 都會發生缺頁異常。在內存工做繁忙時,頻繁的內存分配會致使大量的缺頁異常,使內核的管理負擔增大。這也是malloc 只對大塊內存使用 mmap 的緣由。

瞭解這兩種調用方式後,還須要清楚一點,那就是,當這兩種調用發生後,其實並無真正分配內存。這些內存,都只在首次訪問時才分配,也就是經過缺頁異常進入內核中,再由內核來分配內存。

總體來講,Linux 使用夥伴系統來管理內存分配。這些內存在MMU中以頁爲單位進行管理,夥伴系統也同樣,以頁爲單位來管理內存,而且會經過相鄰頁的合併,減小內存碎片化(好比brk方式形成的內存碎片)。

在用戶空間,malloc 經過 brk() 分配的內存,在釋放時並不當即歸還系統,而是緩存起來重複利用。
在內核空間,Linux 則經過 slab 分配器來管理小內存。能夠把slab 當作構建在夥伴系統上的一個緩存,主要做用就是分配並釋放內核中的小對象。

對內存來講,若是隻分配而不釋放,就會形成內存泄漏,甚至會耗盡系統內存。因此,在應用程序用完內存後,還須要調用 free() 或 unmap(),來釋放這些不用的內存。

固然,系統也不會任由某個進程用完全部內存。在發現內存緊張時,系統就會經過一系列機制來回收內存,好比下面這三種方式:

  • 回收緩存,好比使用 LRU(Least Recently Used)算法,回收最近使用最少的內存頁面;
  • 回收不常訪問的內存,把不經常使用的內存經過交換分區直接寫到磁盤中;
  • 殺死進程,內存緊張時系統還會經過 OOM(Out of Memory),直接殺掉佔用大量內存的進程。

其中,第二種方式回收不常訪問的內存時,會用到交換分區(如下簡稱 Swap)。Swap 其實就是把一塊磁盤空間當成內存來用。它能夠把進程暫時不用的數據存儲到磁盤中(這個過程稱爲換出),當進程訪問這些內存時,再從磁盤讀取這些數據到內存中(這個過程稱爲換入)。
因此,能夠發現,Swap 把系統的可用內存變大了。不過要注意,一般只在內存不足時,纔會發生 Swap 交換。而且因爲磁盤讀寫的速度遠比內存慢,Swap 會致使嚴重的內存性能問題。

第三種方式提到的 OOM(Out of Memory),實際上是內核的一種保護機制。它監控進程的內存使用狀況,而且使用 oom_score 爲每一個進程的內存使用狀況進行評分:

  • 一個進程消耗的內存越大,oom_score 就越大;
  • 一個進程運行佔用的 CPU 越多,oom_score 就越小。

這樣,進程的 oom_score 越大,表明消耗的內存越多,也就越容易被 OOM 殺死,從而能夠更好保護系統。

固然,爲了實際工做的須要,管理員能夠經過 /proc 文件系統,手動設置進程的 oom_adj ,從而調整進程的 oom_score。
oom_adj 的範圍是 [-17, 15],數值越大,表示進程越容易被 OOM 殺死;數值越小,表示進程越不容易被 OOM 殺死,其中 -17 表示禁止OOM。

好比用下面的命令,就能夠把 sshd 進程的 oom_adj 調小爲 -16,這樣, sshd 進程就不容易被 OOM 殺死。

1 echo -16 > /proc/$(pidof sshd)/oom_adj

 

小結

  • 對普通進程來講,它能看到的實際上是內核提供的虛擬內存,這些虛擬內存還須要經過頁表,由系統映射爲物理內存。
  • 當進程經過 malloc() 申請內存後,內存並不會當即分配,而是在首次訪問時,才經過缺頁異常陷入內核中分配內存。
  • 因爲進程的虛擬地址空間比物理內存大不少,Linux 還提供了一系列的機制,應對內存不足的問題,好比緩存的回收、交換分區 Swap 以及OOM 等。
  • 當須要瞭解系統或者進程的內存使用狀況時,能夠用 free 和 top 、ps 等性能工具。它們都是分析性能問題時最經常使用的性能工具。
相關文章
相關標籤/搜索