神秘!申請內存時底層發生了什么?
內存的申請釋放對程序員來說就像空氣一樣自然,你幾乎不怎么能意識到,有時你意識不到的東西卻無比重要,申請過這么多內存,你知道申請內存時底層都發生什么了嗎?
大家都喜歡聽故事,我們就從神話故事開始吧。
三界
中國古代的神話故事通常有“三界”之說,一般指的是天、地、人三界,天界是神仙所在的地方,凡人無法企及;人界說的是就是人間;地界說的是閻羅王所在的地方,孫悟空上天入地無所不能就是說可以在這三界自由出入。有的同學可能會問,這和計算機有什么關系呢?原來,我們的代碼也是分三六九等的,程序運行起來后也是有“三界”之說的,程序運行起來的“三界”就是這樣的:
x86 CPU提供了“四界”:0,1,2,3,這幾個數字其實就是指CPU的幾種工作狀態,數字越小表示CPU的特權越大,0號狀態下CPU特權最大,可以執行任何指令,數字越大表示CPU特權越小,3號狀態下CPU特權最小,不能執行一些特權指令。一般情況下系統只使用0和3,因此確切的說是“兩界”,這兩界可不是說天、地,這兩界指的是“用戶態(3)”以及“內核態(0)”,接下來我們看看什么是內核態、什么是用戶態。
內核態
什么是內核態?當CPU執行操作系統代碼時就處于內核態,在內核態下CPU可以執行任何機器指令、訪問所有地址空間、不受限制的訪問任何硬件,可以簡單的認為內核態就是“天界”,在這里的代碼(操作系統代碼)無所不能。
用戶態
什么是用戶態?當CPU執行我們寫的“普通”代碼(非操作系統、驅動程序員)時就處于用戶態,粗糙的劃分方法就是除了操作系統之外的代碼,就像我們寫的HelloWorld程序。用戶態就好比“人界”,在用戶態我們的代碼處處受限,不能直接訪問硬件、不能訪問特定地址空間,否則神仙(操作系統)直接將你kill掉,這就是著名的Segmentation fault、不能執行特權指令,等等。
關于這一部分的詳細講解,請參見《深入理解操作系統》系列文章。
跨界
孫悟空神通廣大,一個跟斗就能從人間跑到天上去罵玉帝老兒,程序員就沒有這個本領了。普通程序永遠也去不了內核態,只能以通信的方式從用戶態往內核態傳遞信息。操作系統為普通程序員留了一些特定的暗號,這些暗號就和普通函數一樣,程序員通過調用這些暗號就能向操作系統請求服務了,這些像普通函數一樣的暗號就被稱為系統調用,System Call,通過系統調用我們可以讓操作系統代替我們完成一些事情,像打開文件、網絡通信等等。
你可能有些疑惑,什么,還有系統調用這種東西,為什么我沒調用過也可以打開文件、進行網絡通信?
標準庫
雖然我們可以通過系統讓操作系統替我們完成一些特定任務,但這些系統調用都是和操作系統強相關的,Linux和Windows的系統調用就完全不同。如果你直接使用系統調用的話,那么Linux版本的程序就沒有辦法在Windows上運行,因此我們需要某種標準,該標準對程序員屏蔽底層差異,這樣程序員寫的程序就無需修改的在不同操作系統上運行了。在C語言中,這就是所謂的標準庫。注意,標準庫代碼也是運行在用戶態的,并不是神仙(操作系統),一般來說,我們調用標準庫去打開文件、網絡通信等等,標準庫再根據操作系統選擇對應的系統調用。從分層的角度看,我們的程序一般都是這樣的漢堡包類型:
最上層是應用程序,應用程序一般只和標準庫打交道(當然,我們也可以繞過標準庫),標準庫通過系統調用和操作系統交互,操作系統管理底層硬件。這就是為什么在C語言下同樣的open函數既能在Linux下打開文件也能在Windows下打開文件的原因。說了這么多,這和malloc又有什么關系呢?
主角登場
原來,我們分配內存時使用的malloc函數其實不是實現在操作系統里的,而是在標準庫中實現的。
現在我們知道了,malloc是標準庫的一部分,當我們調用malloc時實際上是標準庫在為我們申請內存。這里值得注意的是,我們平時在C語言中使用malloc只是內存分配器的一種,實際上有很多內存分配器,像tcmalloc,jemalloc等等,它們都有各自適用的場景,對于高性能程序來說使用滿足特定要求的內存分配器是至關重要的。那么接下來的問題就是malloc又是怎么工作的呢?
malloc是如何工作的
實際上你可以把malloc的工作理解為去停車場找停車位,停車場就是一片malloc持有的內存,可用的停車位就是可供malloc支配的空閑內存,停在停車場占用的車位就是已經分配出去的內存,特殊點在于停在該停車場的車寬度大小不一,malloc需要回答這樣一個問題:當有一輛車來到停車場后該停到哪里?通過上面的類比你應該能大體理解工作原理了,具體分析詳見《自己動手實現一個malloc內存分配器》。但是,請注意,上面這篇文章并不是故事的全部,在這篇文章中有一個問題我們故意忽略了,這個問題就是如果內存分配器中的空閑內存塊不夠用了該怎么辦呢?在上面這篇文章中我們總是假定自己實現的malloc總能找到一塊空閑內存,但實際上并不是這樣的。
內存不夠該怎么辦?
讓我們再來看一下程序在內存中是什么樣的:
我們已經知道了,malloc管理的是堆區,注意,在堆區和棧區之間有一片空白區域,這片空白區域的目的是什么呢?原來,棧區其實是可以增長的,隨著調用深度的增加,相應的棧區占用的內存也會增加,關于棧區這一主題,你可以參考《函數運行時在內存中是什么樣子》這篇文章。棧區的增長就需要占用原來的空白區域。相應的,堆區也可以增長:
堆區增長后占用的內存就會變多,這就解決了內存分配器空閑內存不足的問題,那么很自然的,malloc該怎樣讓堆區增長呢?原來malloc內存不足時要向操作系統申請內存,操作系統才是真大佬,malloc不過是小弟,對每個進程,操作系統(類Unix系統)都維護了一個叫做brk的變量,brk發音break,這個brk指向了堆區的頂部。
將brk上移后堆區增大,那么我們該怎么樣讓堆區增大呢?這就涉及到我們剛提到的系統調用了。
向操作系統申請內存
操作系統專門提供了一個叫做brk的系統調用,還記得剛提到堆的頂部吧,這個brk()系統調用就是用來增加或者減小堆區的。
實際上不只brk系統調用,sbr、mmap系統調用也可以實現同樣的目的,mmap也更為靈活,但該函數并不是本文重點,就不在這里詳細討論了。現在我們知道了,如果malloc自己維護的內存空間不足將通過brk系統調用向操作系統申請內存。這樣malloc就可以把這些從操作系統申請到的內存當做新的空閑內存塊分配出去。
看起來已經講完的故事
現在我就可以簡單總結一下了,當我們申請內存時,經歷這樣幾個步驟:
程序調用malloc申請內存,注意malloc實現在標準庫中
malloc開始搜索空閑內存塊,如果能找到一塊大小合適的就分配出去,前兩個步驟都是發生在用戶態
如果malloc沒有找到空閑內存塊那么就像操作系統發出請求來增大堆區,這是通過系統調用brk(sbrk、mmap也可以)實現的,注意,brk是操作系統的一部分,因此當brk開始執行時,此時就進入內核態了。brk增大進程的堆區后返回,malloc的空閑內存塊增加,此時malloc又一次能找到合適的空閑內存塊然后分配出去。
故事就到這里了嗎?
冰山之下
實際上到目前為止,我們接觸到的僅僅是冰山一角。
我們看到的冰山是這樣的:我們向malloc申請內存,malloc內存不夠時向操作系統申請內存,之后malloc找到一塊空閑內存返回給調用者。但是,你知道嗎,上述過程根本就沒有涉及到哪怕一丁點物理內存!!!我們確實向malloc申請到內存了,malloc不夠也確實從操作系統申請到內存了,但這些內存都不是真的物理內存,NOT REAL。實際上,進程看到的內存都是假的,是操作系統給進程的一個幻象,這個幻象就是由著名的虛擬內存系統來維護的,我們經常說的這張圖就是進程的虛擬內存。
所謂虛擬內存就是假的、不是真正的物理內存,虛擬內存是給進程用的,操作系統維護了虛擬內存到物理內存的映射,當malloc返回后,程序員申請到的內存就是虛擬內存。
注意,此時操作系統根本就沒有真正的分配物理內存,程序員從malloc拿到的內存目前還只是一張空頭支票。
那么這張空頭支票什么時候才能兌現呢?也就是什么時候操作系統才會真正的分配物理內存呢?
答案是當我們真正使用這段內存時,當我們真正使用這段內存時,這時會產生一個缺頁錯誤,操作系統捕捉到該錯誤后開始真正的分配物理內存,操作系統處理完該錯誤后我們的程序才能真正的讀寫這塊內存。
這里只是簡略的提到了虛擬內存,實際上虛擬內存是當前操作系統內部極其重要的一部分,關于虛擬內存的工作原理將在《深入理解操作系統》系列文章中詳細討論。
完整的故事
現在,這個故事就可以完整講出來了,當我們調用malloc申請內存時:
- malloc開始搜索空閑內存塊,如果能找到一塊大小合適的就分配出去
- 如果malloc找不到一塊合適的空閑內存,那么調用brk等系統調用擴大堆區從而獲得更多的空閑內存
- malloc調用brk后開始轉入內核態,此時操作系統中的虛擬內存系統開始工作,擴大進程的堆區,注意額外擴大的這一部分內存僅僅是虛擬內存,操作系統并沒有為此分配真正的物理內存
- brk執行結束后返回到malloc,從內核態切換到用戶態,malloc找到一塊合適的空閑內存后返回
- 程序員拿到新申請的內存,程序繼續
- 當有代碼讀寫新申請的內存時系統內部出現缺頁中斷,此時再次由用戶態切換到內核態,操作系統此時真正的分配物理內存,之后再次由內核態切換回用戶態,程序繼續。
以上就是一次內存申請的完整過程,可以看到一次內存申請過程是非常復雜的。
總結
怎么樣,程序員申請內存使用的malloc雖然表面看上去非常簡單,簡單到就一行代碼,但這行代碼背后是非常復雜的。有的同學可能會問,為什么我們要理解這背后的原理呢?理解了原理后我才能知道內存申請的復雜性,對于高性能程序來講頻繁的調用malloc對系統性能是有影響的,那么很自然的一個問題就是我們能否避免malloc?這個問題我們將在接下來的文章中講解。希望本篇對大家理解內存分配的底層原理有所幫助。