77%的Linux運維都不懂的內核問題

 

前言node

以前在實習時,聽了 OOM 的分享以後,就對 Linux 內核內存管理充滿興趣,可是這塊知識很是龐大,沒有必定積累,不敢寫下,擔憂誤人子弟,因此通過一個一段時間的積累,對內核內存有必定了解以後,今天才寫下這篇博客,記錄以及分享。mysql

【OOM - Out of Memory】內存溢出linux

內存溢出的解決辦法:redis

一、等比例縮小圖片算法

二、對圖片採用軟引用,及時進行 recycle( ) 操做。sql

三、使用加載圖片框架處理圖片,如專業處理圖片的 ImageLoader 圖片加載框架,還有XUtils 的 BitMapUtils 來處理。數組

這篇文章主要是分析了單個進程空間的內存佈局與分配,是從全局的視角分析下內核對內存的管理; 

緩存

下面主要從如下方面介紹 Linux 內存管理:服務器

  • 進程的內存申請與分配;框架

  • 內存耗盡以後 OOM;

  • 申請的內存都在哪?

  • 系統回收內存;


一、進程的內存申請與分配

以前有篇文章介紹 hello world 程序是如何載入內存以及是如何申請內存的,我在這,再次說明下:一樣,仍是先給出進程的地址空間,我以爲對於任何開發人員這張圖是必須記住的,還有一張就是操做 disk ,memory 以及 cpu cache 的時間圖。

當咱們在終端啓動一個程序時,終端進程調用 exec 函數將可執行文件載入內存,此時代碼段,數據段,bbs 段,stack 段都經過 mmap 函數映射到內存空間,堆則要根據是否有在堆上申請內存來決定是否映射。

exec 執行以後,此時並未真正開始執行進程,而是將 cpu 控制權交給了動態連接庫裝載器,由它來將該進程須要的動態連接庫裝載進內存。以後纔開始進程的執行,這個過程能夠經過 strace 命令跟蹤進程調用的系統函數來分析。

這是我上篇博客認識 pipe 中的程序,從這個輸出過程,能夠看出和我上述描述的一致。

當第一次調用 malloc 申請內存時,經過系統調用 brk 嵌入到內核,首先會進行一次判斷,是否有關於堆的 vma,若是沒有,則經過 mmap 匿名映射一塊內存給堆,並創建 vma 結構,掛到 mm_struct 描述符上的紅黑樹和鏈表上。

而後回到用戶態,經過內存分配器(ptmaloc,tcmalloc,jemalloc)算法將分配到的內存進行管理,返回給用戶所須要的內存。

若是用戶態申請大內存時,是直接調用 mmap 分配內存,此時返回給用戶態的內存仍是虛擬內存,直到第一次訪問返回的內存時,才真正進行內存的分配。

其實經過 brk 返回的也是虛擬內存,可是通過內存分配器進行切割分配以後(切割就必須訪問內存),全都分配到了物理內存

當進程在用戶態經過調用 free 釋放內存時,若是這塊內存是經過 mmap 分配,則調用 munmap 直接返回給系統。

不然內存是先返回給內存分配器,而後由內存分配器統一返還給系統,這就是爲何當咱們調用 free 回收內存以後,再次訪問這塊內存時,可能不會報錯的緣由。

固然,當整個進程退出以後,這個進程佔用的內存都會歸還給系統。

二、內存耗盡以後OOM

在實習期間,有一臺測試機上的 mysql 實例常常被 oom 殺死,OOM(out of memory)即爲系統在內存耗盡時的自我拯救措施,他會選擇一個進程,將其殺死,釋放出內存,很明顯,哪一個進程佔用的內存最多,即最可能被殺死,但事實是這樣的嗎?

今天早上去上班,恰好碰到了一塊兒 OOM,忽然發現,OOM 一次,世界都安靜下來了,哈哈,測試機上的 redis 被殺死了。 



OOM 關鍵文件 oom_kill.c,裏面介紹了當內存不夠時,系統如何選擇最應該被殺死的進程,選擇因素有挺多的,除了進程佔用的內存外,還有進程運行的時間,進程的優先級,是否爲 root 用戶進程,子進程個數和佔用內存以及用戶控制參數 oom_adj 都相關。

當產生 oom 以後,函數 select_bad_process 會遍歷全部進程,經過以前提到的那些因素,每一個進程都會獲得一個 oom_score 分數,分數最高,則被選爲殺死的進程。

咱們能夠經過設置 /proc//oom_adj 分數來干預系統選擇殺死的進程。

這是內核關於這個oom_adj調整值的定義,最大能夠調整爲15,最小爲-16,若是爲-17,則該進程就像買了vip會員同樣,不會被系統驅逐殺死了,所以,若是在一臺機器上有跑不少服務器,且你不但願本身的服務被殺死的話,就能夠設置本身服務的 oom_adj 爲-17。

固然,說到這,就必須提到另外一個參數 /proc/sys/vm/overcommit_memory,man proc 說明以下: 

意思就是當 overcommit_memory 爲0時,則爲啓發式oom,即當申請的虛擬內存不是很誇張的大於物理內存,則系統容許申請,可是當進程申請的虛擬內存很誇張的大於物理內存,則就會產生 OOM。

例如只有8g的物理內存,而後 redis 虛擬內存佔用了24G,物理內存佔用3g,若是這時執行 bgsave,子進程和父進程共享物理內存,可是虛擬內存是本身的,即子進程會申請24g的虛擬內存,這很誇張大於物理內存,就會產生一次OOM。

當 overcommit_memory 爲1時,則永遠都容許 overmemory 內存申請,即無論你多大的虛擬內存申請都容許,可是當系統內存耗盡時,這時就會產生oom,即上述的redis例子,在 overcommit_memory=1 時,是不會產生oom 的,由於物理內存足夠。

當 overcommit_memory 爲2時,永遠都不能超出某個限定額的內存申請,這個限定額爲 swap+RAM* 係數(/proc/sys/vm/overcmmit_ratio,默認50%,能夠本身調整),若是這麼多資源已經用光,那麼後面任未嘗試申請內存的行爲都會返回錯誤,這一般意味着此時無法運行任何新程序

以上就是 OOM 的內容,瞭解原理,以及如何根據本身的應用,合理的設置OOM。

三、系統申請的內存都在哪?

咱們瞭解了一個進程的地址空間以後,是否會好奇,申請到的物理內存都存在哪了?可能不少人以爲,不就是物理內存嗎?

我這裏說申請的內存在哪,是由於物理內存有分爲cache和普通物理內存,能夠經過 free 命令查看,並且物理內存還有分 DMA,NORMAL,HIGH 三個區,這裏主要分析cache和普通內存。

經過第一部分,咱們知道一個進程的地址空間幾乎都是 mmap 函數申請,有文件映射和匿名映射兩種。

3.1 共享文件映射

咱們先來看下代碼段和動態連接庫映射段,這兩個都是屬於共享文件映射,也就是說由同一個可執行文件啓動的兩個進程是共享這兩個段,都是映射到同一塊物理內存,那麼這塊內存在哪了?我寫了個程序測試以下:

 

咱們先看下當前系統的內存使用狀況:

當我在本地新建一個1G的文件:

dd if=/dev/zero of=fileblock bs=M count=1024

而後調用上述程序,進行共享文件映射,此時內存使用狀況爲: 

咱們能夠發現,buff/cache 增加了大概1G,所以咱們能夠得出結論,代碼段和動態連接庫段是映射到內核cache中,也就是說當執行共享文件映射時,文件是先被讀取到 cache 中,而後再映射到用戶進程空間中。

3.2 私有文件映射段

對於進程空間中的數據段,其必須是私有文件映射,由於若是是共享文件映射,那麼同一個可執行文件啓動的兩個進程,任何一個進程修改數據段,都將影響另外一個進程了,我將上述測試程序改寫成匿名文件映射: 

 

在執行程序執行,須要先將以前的 cache 釋放掉,不然會影響結果

echo 1 >> /proc/sys/vm/drop_caches

接着執行程序,看下內存使用狀況: 

從使用前和使用後對比,能夠發現 used 和 buff/cache 分別增加了1G,說明當進行私有文件映射時,首先是將文件映射到 cache 中,而後若是某個文件對這個文件進行修改,則會從其餘內存中分配一塊內存先將文件數據拷貝至新分配的內存,而後再在新分配的內存上進行修改,這也就是寫時複製。

這也很好理解,由於若是同一個可執行文件開啓多個實例,那麼內核先將這個可執行的數據段映射到 cache,而後每一個實例若是有修改數據段,則都將分配一個一塊內存存儲數據段,畢竟數據段也是一個進程私有的。

經過上述分析,能夠得出結論,若是是文件映射,則都是將文件映射到 cache 中,而後根據共享仍是私有進行不一樣的操做。

3.3 私有匿名映射

像 bbs 段,堆,棧這些都是匿名映射,由於可執行文件中沒有相應的段,並且必須是私有映射,不然若是當前進程 fork 出一個子進程,那麼父子進程將會共享這些段,一個修改都會影響到彼此,這是不合理的。

ok,如今我把上述測試程序改爲私有匿名映射 

這時再來看下內存的使用狀況 

咱們能夠看到,只有 used 增長了1G,而 buff/cache 並無增加;說明,在進行匿名私有映射時,並無佔用 cache,其實這也是有道理,由於就只有當前進程在使用這塊這塊內存,沒有必要佔用寶貴的 cache。

3.4 共享匿名映射

當咱們須要在父子進程共享內存時,就能夠用到 mmap 共享匿名映射,那麼共享匿名映射的內存是存放在哪了?我繼續改寫上述測試程序爲共享匿名映射 。

 

這時來看下內存的使用狀況:

從上述結果,咱們能夠看出,只有buff/cache增加了1G,即當進行共享匿名映射時,這時是從 cache 中申請內存,道理也很明顯,由於父子進程共享這塊內存,共享匿名映射存在於 cache,而後每一個進程再映射到彼此的虛存空間,這樣便可操做的是同一塊內存。

四、系統回收內存

當系統內存不足時,有兩種方式進行內存釋放,一種是手動的方式,另外一種是系統本身觸發的內存回收,先來看下手動觸發方式。

4.1 手動回收內存

手動回收內存,以前也有演示過,即

echo 1 >> /proc/sys/vm/drop_caches

咱們能夠在 man proc 下面看到關於這個的簡介 

從這個介紹能夠看出,當 drop_caches 文件爲1時,這時將釋放 pagecache 中可釋放的部分(有些 cache 是不能經過這個釋放的),當 drop_caches 爲2時,這時將釋放 dentries 和 inodes 緩存,當 drop_caches 爲3時,這同時釋放上述兩項。

關鍵還有最後一句,意思是說若是 pagecache 中有髒數據時,操做 drop_caches 是不能釋放的,必須經過 sync 命令將髒數據刷新到磁盤,才能經過操做 drop_caches 釋放 pagecache。

ok,以前有提到有些pagecache是不能經過drop_caches釋放的,那麼除了上述提文件映射和共享匿名映射外,還有有哪些東西是存在pagecache了?

4.2 tmpfs

咱們先來看下 tmpfs ,tmpfs 和 procfs,sysfs 以及 ramfs 同樣,都是基於內存的文件系統,tmpfs 和 ramfs 的區別就是 ramfs 的文件基於純內存的,和 tmpfs 除了純內存外,還會使用 swap 交換空間,以及 ramfs 可能會把內存耗盡,而 tmpfs 能夠限定使用內存大小,能夠用命令 df -T -h 查看系統一些文件系統,其中就有一些是 tmpfs,比較出名的是目錄 /dev/shm

tmpfs 文件系統源文件在內核源碼 mm/shmem.c,tmpfs實現很複雜,以前有介紹虛擬文件系統,基於 tmpfs 文件系統建立文件和其餘基於磁盤的文件系統同樣,也會有 inode,super_block,identry,file 等結構,區別主要是在讀寫上,由於讀寫才涉及到文件的載體是內存仍是磁盤。

而 tmpfs 文件的讀函數 shmem_file_read,過程主要爲經過 inode 結構找到 address_space 地址空間,其實就是磁盤文件的 pagecache,而後經過讀偏移定位cache 頁以及頁內偏移。

這時就能夠直接從這個 pagecache 經過函數 __copy_to_user 將緩存頁內數據拷貝到用戶空間,當咱們要讀物的數據不pagecache中時,這時要判斷是否在 swap 中,若是在則先將內存頁 swap in,再讀取。

tmpfs 文件的寫函數 shmem_file_write,過程主要爲先判斷要寫的頁是否在內存中,若是在,則直接將用戶態數據經過函數__copy_from_user拷貝至內核pagecache中覆蓋老數據,並標爲 dirty。

若是要寫的數據再也不內存中,則判斷是否在swap 中,若是在,則先讀取出來,用新數據覆蓋老數據並標爲髒,若是即不在內存也不在磁盤,則新生成一個 pagecache 存儲用戶數據。

由上面分析,咱們知道基於 tmpfs 的文件也是使用 cache 的,咱們能夠在/dev/shm上建立一個文件來檢測下:

看到了吧,cache 增加了1G,驗證了 tmpfs 的確使用的 cache 內存。

其實 mmap 匿名映射原理也是用了 tmpfs,在 mm/mmap.c->do_mmap_pgoff 函數內部,有判斷若是 file 結構爲空以及爲 SHARED 映射,則調用 shmem_zero_setup(vma) 函數在 tmpfs 上用新建一個文件 

這裏就解釋了爲何共享匿名映射內存初始化爲0了,可是咱們知道用 mmap 分配的內存初始化爲0,就是說 mmap 私有匿名映射也爲0,那麼體如今哪了?

這個在 do_mmap_pgoff 函數內部可沒有體現出來,而是在缺頁異常,而後分配一種特殊的初始化爲0的頁。

那麼這個 tmpfs 佔有的內存頁能夠回收嗎? 

 

 

也就是說 tmpfs 文件佔有的 pagecache 是不能回收的,道理也很明顯,由於有文件引用這些頁,就不能回收。

4.3 共享內存

posix 共享內存其實和 mmap 共享映射是同一個道理,都是利用在 tmpfs 文件系統上新建一個文件,而後再映射到用戶態,最後兩個進程操做同一個物理內存,那麼 System V 共享內存是否也是利用 tmpfs 文件系統了?

咱們能夠跟蹤到下述函數 

這個函數就是新建一個共享內存段,其中函數

shmem_kernel_file_setup

就是在 tmpfs 文件系統上建立一個文件,而後經過這個內存文件實現進程通訊,這我就不寫測試程序了,並且這也是不能回收的,由於共享內存ipc機制生命週期是隨內核的,也就是說你建立共享內存以後,若是不顯示刪除的話,進程退出以後,共享內存仍是存在的。

以前看了一些技術博客,說到 Poxic 和 System V 兩套 ipc 機制(消息隊列,信號量以及共享內存)都是使用 tmpfs 文件系統,也就是說最終內存使用的都是 pagecache,可是我在源碼中看出了兩個共享內存是基於 tmpfs 文件系統,其餘信號量和消息隊列還沒看出來(有待後續考究)。

posix 消息隊列的實現有點相似與 pipe 的實現,也是本身一套 mqueue 文件系統,而後在 inode 上的 i_private 上掛上關於消息隊列屬性 mqueue_inode_info,在這個屬性上,內核2.6時,是用一個數組存儲消息,而到了4.6則用紅黑樹了存儲消息(我下載了這兩個版本,具體何時開始用紅黑樹,沒深究)。

而後兩個進程每次操做都是操做這個 mqueue_inode_info 中的消息數組或者紅黑樹,實現進程通訊,和這個 mqueue_inode_info 相似的還有 tmpfs 文件系統屬性shmem_inode_info 和爲epoll服務的文件系統 eventloop,也有一個特殊屬性struct eventpoll,這個是掛在 file 結構的 private_data 等等。

說到這,能夠小結下,進程空間中代碼段,數據段,動態連接庫(共享文件映射),mmap 共享匿名映射都存在於 cache 中,可是這些內存頁都有被進程引用,因此是不能釋放的,基於 tmpfs 的 ipc 進程間通訊機制的生命週期是隨內核,所以也是不能經過 drop_caches 釋放。

雖然上述說起的cache不能釋放,可是後面有提到,當內存不足時,這些內存是能夠 swap out 的。

所以 drop_caches 能釋放的就是當從磁盤讀取文件時的緩存頁以及某個進程將某個文件映射到內存以後,進程退出,這時映射文件的的緩存頁若是沒有被引用,也是能夠被釋放的。

4.4 內存自動釋放方式

當系統內存不夠時,操做系統有一套自我整理內存,並儘量的釋放內存機制,若是這套機制不能釋放足夠多的內存,那麼只能 OOM 了。

以前在說起 OOM 時,說道 redis 由於 OOM 被殺死,以下: 

第二句後半部分,

total-vm:186660kB, anon-rss:9388kB, file-rss:4kB

把一個進程內存使用狀況,用三個屬性進行了說明,即全部虛擬內存,常駐內存匿名映射頁以及常駐內存文件映射頁。

其實從上述的分析,咱們也能夠知道一個進程其實就是文件映射和匿名映射:

  • 文件映射:代碼段,數據段,動態連接庫共享存儲段以及用戶程序的文件映射段;

  • 匿名映射:bbs段,堆,以及當 malloc 用 mmap 分配的內存,還有mmap共享內存段;

其實內核回收內存就是根據文件映射和匿名映射來進行的,在 mmzone.h 有以下定義: 

 

LRU_UNEVICTABLE 即爲不可驅逐頁 lru,個人理解就是當調用 mlock 鎖住內存,不讓系統 swap out 出去的頁列表。

簡單說下 linux 內核自動回收內存原理,內核有一個 kswapd 會週期性的檢查內存使用狀況,若是發現空閒內存定於 pages_low,則 kswapd 會對 lru_list 前四個 lru 隊列進行掃描,在活躍鏈表中查找不活躍的頁,並添加不活躍鏈表。

而後再遍歷不活躍鏈表,逐個進行回收釋放出32個頁,知道 free page 數量達到 pages_high,針對不一樣的頁,回收方式也不同。

固然,當內存水平低於某個極限閾值時,會直接發出內存回收,原理和 kswapd 同樣,可是此次回收力度更大,須要回收更多的內存。

文件頁:

  1. 若是是髒頁,則直接回寫進磁盤,再回收內存。

  2. 若是不是髒頁,則直接釋放回收,由於若是是io讀緩存,直接釋放掉,下次讀時,缺頁異常,直接到磁盤讀回來便可,若是是文件映射頁,直接釋放掉,下次訪問時,也是產生兩個缺頁異常,一次將文件內容讀取進磁盤,另外一次與進程虛擬內存關聯。

匿名頁: 由於匿名頁沒有回寫的地方,若是釋放掉,那麼就找不到數據了,因此匿名頁的回收是採起 swap out 到磁盤,並在頁表項作個標記,下次缺頁異常在從磁盤 swap in 進內存。

swap 換進換出實際上是很佔用系統IO的,若是系統內存需求忽然間迅速增加,那麼cpu 將被io佔用,系統會卡死,致使不能對外提供服務,所以系統提供一個參數,用於設置當進行內存回收時,執行回收 cache 和 swap 匿名頁的,這個參數爲:

意思就是說這個值越高,越可能使用 swap 的方式回收內存,最大值爲100,若是設爲0,則儘量使用回收 cache 的方式釋放內存。

五、總結

這篇文章主要是寫了 linux 內存管理相關的東西:

首先是回顧了進程地址空間;

其次當進程消耗大量內存而致使內存不足時,咱們能夠有兩種方式:第一是手動回收 cache;另外一種是系統後臺線程 swapd 執行內存回收工做。

最後當申請的內存大於系統剩餘的內存時,這時就只會產生 OOM,殺死進程,釋放內存,從這個過程,能夠看出系統爲了騰出足夠的內存,是多麼的努力啊。

做者:羅道文的私房菜

原文連接:http://luodw.cc/2016/08/13/linux-cache/

相關文章
相關標籤/搜索