內存的申請釋放對程序員來講就像空氣同樣天然,你幾乎不怎麼能意識到,有時你意識不到的東西卻無比重要,申請過這麼多內存,你知道申請內存時底層都發生什麼了嗎?
中國古代的神話故事一般有「三界」之說,通常指的是天、地、人三界,天界是神仙所在的地方,凡人沒法企及;人界說的是就是人間;地界說的是閻羅王所在的地方,孫悟空上天入地無所不能就是說能夠在這三界自由出入。
原來,咱們的代碼也是分三六九等的,程序運行起來後也是有「三界」之說的,程序運行起來的「三界」就是這樣的:
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語言下一樣的open函數既能在Linux下打開文件也能在Windows下打開文件的緣由。
原來,咱們分配內存時使用的malloc函數其實不是實如今操做系統裏的,而是在標準庫中實現的。
如今咱們知道了,malloc是標準庫的一部分,當咱們調用malloc時其實是標準庫在爲咱們申請內存。
這裏值得注意的是,咱們平時在C語言中使用malloc只是內存分配器的一種,實際上有不少內存分配器,像tcmalloc,jemalloc等等,它們都有各自適用的場景,對於高性能程序來講使用知足特定要求的內存分配器是相當重要的。
那麼接下來的問題就是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後開始轉入內核態,此時操做系統中的虛擬內存系統開始工做,擴大進程的堆區,注意額外擴大的這一部份內存僅僅是虛擬內存,操做系統並無爲此分配真正的物理內存app
brk執行結束後返回到malloc,從內核態切換到用戶態,malloc找到一塊合適的空閒內存後返回異步
程序員拿到新申請的內存,程序繼續函數
當有代碼讀寫新申請的內存時系統內部出現缺頁中斷,此時再次由用戶態切換到內核態,操做系統此時真正的分配物理內存,以後再次由內核態切換回用戶態,程序繼續。高併發
以上就是一次內存申請的完整過程,能夠看到一次內存申請過程是很是複雜的。
怎麼樣,程序員申請內存使用的malloc雖然表面看上去很是簡單,簡單到就一行代碼,但這行代碼背後是很是複雜的。
有的同窗可能會問,爲何咱們要理解這背後的原理呢?理解了原理後我才能知道內存申請的複雜性,對於高性能程序來說頻繁的調用malloc對系統性能是有影響的,那麼很天然的一個問題就是咱們可否避免malloc?
最後的最後,若是以爲文章對你有幫助的話,請多多分享、轉發、在看。
本文分享自微信公衆號 - 碼農的荒島求生(escape-it)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。