自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

神秘!申請內(nèi)存時底層發(fā)生了什么?

存儲 存儲軟件
內(nèi)存的申請釋放對程序員來說就像空氣一樣自然,你幾乎不怎么能意識到,有時你意識不到的東西卻無比重要,申請過這么多內(nèi)存,你知道申請內(nèi)存時底層都發(fā)生什么了嗎?

[[376556]]

內(nèi)存的申請釋放對程序員來說就像空氣一樣自然,你幾乎不怎么能意識到,有時你意識不到的東西卻無比重要,申請過這么多內(nèi)存,你知道申請內(nèi)存時底層都發(fā)生什么了嗎?

大家都喜歡聽故事,我們就從神話故事開始吧。

三界

中國古代的神話故事通常有“三界”之說,一般指的是天、地、人三界,天界是神仙所在的地方,凡人無法企及;人界說的是就是人間;地界說的是閻羅王所在的地方,孫悟空上天入地?zé)o所不能就是說可以在這三界自由出入。有的同學(xué)可能會問,這和計算機(jī)有什么關(guān)系呢?原來,我們的代碼也是分三六九等的,程序運(yùn)行起來后也是有“三界”之說的,程序運(yùn)行起來的“三界”就是這樣的:

x86 CPU提供了“四界”:0,1,2,3,這幾個數(shù)字其實就是指CPU的幾種工作狀態(tài),數(shù)字越小表示CPU的特權(quán)越大,0號狀態(tài)下CPU特權(quán)最大,可以執(zhí)行任何指令,數(shù)字越大表示CPU特權(quán)越小,3號狀態(tài)下CPU特權(quán)最小,不能執(zhí)行一些特權(quán)指令。一般情況下系統(tǒng)只使用0和3,因此確切的說是“兩界”,這兩界可不是說天、地,這兩界指的是“用戶態(tài)(3)”以及“內(nèi)核態(tài)(0)”,接下來我們看看什么是內(nèi)核態(tài)、什么是用戶態(tài)。

 

內(nèi)核態(tài)

什么是內(nèi)核態(tài)?當(dāng)CPU執(zhí)行操作系統(tǒng)代碼時就處于內(nèi)核態(tài),在內(nèi)核態(tài)下CPU可以執(zhí)行任何機(jī)器指令、訪問所有地址空間、不受限制的訪問任何硬件,可以簡單的認(rèn)為內(nèi)核態(tài)就是“天界”,在這里的代碼(操作系統(tǒng)代碼)無所不能。

 

用戶態(tài)

什么是用戶態(tài)?當(dāng)CPU執(zhí)行我們寫的“普通”代碼(非操作系統(tǒng)、驅(qū)動程序員)時就處于用戶態(tài),粗糙的劃分方法就是除了操作系統(tǒng)之外的代碼,就像我們寫的HelloWorld程序。用戶態(tài)就好比“人界”,在用戶態(tài)我們的代碼處處受限,不能直接訪問硬件、不能訪問特定地址空間,否則神仙(操作系統(tǒng))直接將你kill掉,這就是著名的Segmentation fault、不能執(zhí)行特權(quán)指令,等等。

關(guān)于這一部分的詳細(xì)講解,請參見《深入理解操作系統(tǒng)》系列文章。

 

跨界

孫悟空神通廣大,一個跟斗就能從人間跑到天上去罵玉帝老兒,程序員就沒有這個本領(lǐng)了。普通程序永遠(yuǎn)也去不了內(nèi)核態(tài),只能以通信的方式從用戶態(tài)往內(nèi)核態(tài)傳遞信息。操作系統(tǒng)為普通程序員留了一些特定的暗號,這些暗號就和普通函數(shù)一樣,程序員通過調(diào)用這些暗號就能向操作系統(tǒng)請求服務(wù)了,這些像普通函數(shù)一樣的暗號就被稱為系統(tǒng)調(diào)用,System Call,通過系統(tǒng)調(diào)用我們可以讓操作系統(tǒng)代替我們完成一些事情,像打開文件、網(wǎng)絡(luò)通信等等。

你可能有些疑惑,什么,還有系統(tǒng)調(diào)用這種東西,為什么我沒調(diào)用過也可以打開文件、進(jìn)行網(wǎng)絡(luò)通信?

 

標(biāo)準(zhǔn)庫

雖然我們可以通過系統(tǒng)讓操作系統(tǒng)替我們完成一些特定任務(wù),但這些系統(tǒng)調(diào)用都是和操作系統(tǒng)強(qiáng)相關(guān)的,Linux和Windows的系統(tǒng)調(diào)用就完全不同。如果你直接使用系統(tǒng)調(diào)用的話,那么Linux版本的程序就沒有辦法在Windows上運(yùn)行,因此我們需要某種標(biāo)準(zhǔn),該標(biāo)準(zhǔn)對程序員屏蔽底層差異,這樣程序員寫的程序就無需修改的在不同操作系統(tǒng)上運(yùn)行了。在C語言中,這就是所謂的標(biāo)準(zhǔn)庫。注意,標(biāo)準(zhǔn)庫代碼也是運(yùn)行在用戶態(tài)的,并不是神仙(操作系統(tǒng)),一般來說,我們調(diào)用標(biāo)準(zhǔn)庫去打開文件、網(wǎng)絡(luò)通信等等,標(biāo)準(zhǔn)庫再根據(jù)操作系統(tǒng)選擇對應(yīng)的系統(tǒng)調(diào)用。從分層的角度看,我們的程序一般都是這樣的漢堡包類型:

最上層是應(yīng)用程序,應(yīng)用程序一般只和標(biāo)準(zhǔn)庫打交道(當(dāng)然,我們也可以繞過標(biāo)準(zhǔn)庫),標(biāo)準(zhǔn)庫通過系統(tǒng)調(diào)用和操作系統(tǒng)交互,操作系統(tǒng)管理底層硬件。這就是為什么在C語言下同樣的open函數(shù)既能在Linux下打開文件也能在Windows下打開文件的原因。說了這么多,這和malloc又有什么關(guān)系呢?

 

主角登場

原來,我們分配內(nèi)存時使用的malloc函數(shù)其實不是實現(xiàn)在操作系統(tǒng)里的,而是在標(biāo)準(zhǔn)庫中實現(xiàn)的。

現(xiàn)在我們知道了,malloc是標(biāo)準(zhǔn)庫的一部分,當(dāng)我們調(diào)用malloc時實際上是標(biāo)準(zhǔn)庫在為我們申請內(nèi)存。這里值得注意的是,我們平時在C語言中使用malloc只是內(nèi)存分配器的一種,實際上有很多內(nèi)存分配器,像tcmalloc,jemalloc等等,它們都有各自適用的場景,對于高性能程序來說使用滿足特定要求的內(nèi)存分配器是至關(guān)重要的。那么接下來的問題就是malloc又是怎么工作的呢?

 

malloc是如何工作的

實際上你可以把malloc的工作理解為去停車場找停車位,停車場就是一片malloc持有的內(nèi)存,可用的停車位就是可供malloc支配的空閑內(nèi)存,停在停車場占用的車位就是已經(jīng)分配出去的內(nèi)存,特殊點(diǎn)在于停在該停車場的車寬度大小不一,malloc需要回答這樣一個問題:當(dāng)有一輛車來到停車場后該停到哪里?通過上面的類比你應(yīng)該能大體理解工作原理了,具體分析詳見《自己動手實現(xiàn)一個malloc內(nèi)存分配器》。但是,請注意,上面這篇文章并不是故事的全部,在這篇文章中有一個問題我們故意忽略了,這個問題就是如果內(nèi)存分配器中的空閑內(nèi)存塊不夠用了該怎么辦呢?在上面這篇文章中我們總是假定自己實現(xiàn)的malloc總能找到一塊空閑內(nèi)存,但實際上并不是這樣的。

內(nèi)存不夠該怎么辦?

讓我們再來看一下程序在內(nèi)存中是什么樣的:

我們已經(jīng)知道了,malloc管理的是堆區(qū),注意,在堆區(qū)和棧區(qū)之間有一片空白區(qū)域,這片空白區(qū)域的目的是什么呢?原來,棧區(qū)其實是可以增長的,隨著調(diào)用深度的增加,相應(yīng)的棧區(qū)占用的內(nèi)存也會增加,關(guān)于棧區(qū)這一主題,你可以參考《函數(shù)運(yùn)行時在內(nèi)存中是什么樣子》這篇文章。棧區(qū)的增長就需要占用原來的空白區(qū)域。相應(yīng)的,堆區(qū)也可以增長:

堆區(qū)增長后占用的內(nèi)存就會變多,這就解決了內(nèi)存分配器空閑內(nèi)存不足的問題,那么很自然的,malloc該怎樣讓堆區(qū)增長呢?原來malloc內(nèi)存不足時要向操作系統(tǒng)申請內(nèi)存,操作系統(tǒng)才是真大佬,malloc不過是小弟,對每個進(jìn)程,操作系統(tǒng)(類Unix系統(tǒng))都維護(hù)了一個叫做brk的變量,brk發(fā)音break,這個brk指向了堆區(qū)的頂部。

將brk上移后堆區(qū)增大,那么我們該怎么樣讓堆區(qū)增大呢?這就涉及到我們剛提到的系統(tǒng)調(diào)用了。

 

向操作系統(tǒng)申請內(nèi)存

操作系統(tǒng)專門提供了一個叫做brk的系統(tǒng)調(diào)用,還記得剛提到堆的頂部吧,這個brk()系統(tǒng)調(diào)用就是用來增加或者減小堆區(qū)的。

實際上不只brk系統(tǒng)調(diào)用,sbr、mmap系統(tǒng)調(diào)用也可以實現(xiàn)同樣的目的,mmap也更為靈活,但該函數(shù)并不是本文重點(diǎn),就不在這里詳細(xì)討論了?,F(xiàn)在我們知道了,如果malloc自己維護(hù)的內(nèi)存空間不足將通過brk系統(tǒng)調(diào)用向操作系統(tǒng)申請內(nèi)存。這樣malloc就可以把這些從操作系統(tǒng)申請到的內(nèi)存當(dāng)做新的空閑內(nèi)存塊分配出去。

 

看起來已經(jīng)講完的故事

現(xiàn)在我就可以簡單總結(jié)一下了,當(dāng)我們申請內(nèi)存時,經(jīng)歷這樣幾個步驟:

程序調(diào)用malloc申請內(nèi)存,注意malloc實現(xiàn)在標(biāo)準(zhǔn)庫中

malloc開始搜索空閑內(nèi)存塊,如果能找到一塊大小合適的就分配出去,前兩個步驟都是發(fā)生在用戶態(tài)

如果malloc沒有找到空閑內(nèi)存塊那么就像操作系統(tǒng)發(fā)出請求來增大堆區(qū),這是通過系統(tǒng)調(diào)用brk(sbrk、mmap也可以)實現(xiàn)的,注意,brk是操作系統(tǒng)的一部分,因此當(dāng)brk開始執(zhí)行時,此時就進(jìn)入內(nèi)核態(tài)了。brk增大進(jìn)程的堆區(qū)后返回,malloc的空閑內(nèi)存塊增加,此時malloc又一次能找到合適的空閑內(nèi)存塊然后分配出去。

故事就到這里了嗎?

 

冰山之下

實際上到目前為止,我們接觸到的僅僅是冰山一角。

我們看到的冰山是這樣的:我們向malloc申請內(nèi)存,malloc內(nèi)存不夠時向操作系統(tǒng)申請內(nèi)存,之后malloc找到一塊空閑內(nèi)存返回給調(diào)用者。但是,你知道嗎,上述過程根本就沒有涉及到哪怕一丁點(diǎn)物理內(nèi)存!!!我們確實向malloc申請到內(nèi)存了,malloc不夠也確實從操作系統(tǒng)申請到內(nèi)存了,但這些內(nèi)存都不是真的物理內(nèi)存,NOT REAL。實際上,進(jìn)程看到的內(nèi)存都是假的,是操作系統(tǒng)給進(jìn)程的一個幻象,這個幻象就是由著名的虛擬內(nèi)存系統(tǒng)來維護(hù)的,我們經(jīng)常說的這張圖就是進(jìn)程的虛擬內(nèi)存。

所謂虛擬內(nèi)存就是假的、不是真正的物理內(nèi)存,虛擬內(nèi)存是給進(jìn)程用的,操作系統(tǒng)維護(hù)了虛擬內(nèi)存到物理內(nèi)存的映射,當(dāng)malloc返回后,程序員申請到的內(nèi)存就是虛擬內(nèi)存。

注意,此時操作系統(tǒng)根本就沒有真正的分配物理內(nèi)存,程序員從malloc拿到的內(nèi)存目前還只是一張空頭支票。

那么這張空頭支票什么時候才能兌現(xiàn)呢?也就是什么時候操作系統(tǒng)才會真正的分配物理內(nèi)存呢?

答案是當(dāng)我們真正使用這段內(nèi)存時,當(dāng)我們真正使用這段內(nèi)存時,這時會產(chǎn)生一個缺頁錯誤,操作系統(tǒng)捕捉到該錯誤后開始真正的分配物理內(nèi)存,操作系統(tǒng)處理完該錯誤后我們的程序才能真正的讀寫這塊內(nèi)存。

這里只是簡略的提到了虛擬內(nèi)存,實際上虛擬內(nèi)存是當(dāng)前操作系統(tǒng)內(nèi)部極其重要的一部分,關(guān)于虛擬內(nèi)存的工作原理將在《深入理解操作系統(tǒng)》系列文章中詳細(xì)討論。

 

完整的故事

現(xiàn)在,這個故事就可以完整講出來了,當(dāng)我們調(diào)用malloc申請內(nèi)存時:

  1. malloc開始搜索空閑內(nèi)存塊,如果能找到一塊大小合適的就分配出去
  2. 如果malloc找不到一塊合適的空閑內(nèi)存,那么調(diào)用brk等系統(tǒng)調(diào)用擴(kuò)大堆區(qū)從而獲得更多的空閑內(nèi)存
  3. malloc調(diào)用brk后開始轉(zhuǎn)入內(nèi)核態(tài),此時操作系統(tǒng)中的虛擬內(nèi)存系統(tǒng)開始工作,擴(kuò)大進(jìn)程的堆區(qū),注意額外擴(kuò)大的這一部分內(nèi)存僅僅是虛擬內(nèi)存,操作系統(tǒng)并沒有為此分配真正的物理內(nèi)存
  4. brk執(zhí)行結(jié)束后返回到malloc,從內(nèi)核態(tài)切換到用戶態(tài),malloc找到一塊合適的空閑內(nèi)存后返回
  5. 程序員拿到新申請的內(nèi)存,程序繼續(xù)
  6. 當(dāng)有代碼讀寫新申請的內(nèi)存時系統(tǒng)內(nèi)部出現(xiàn)缺頁中斷,此時再次由用戶態(tài)切換到內(nèi)核態(tài),操作系統(tǒng)此時真正的分配物理內(nèi)存,之后再次由內(nèi)核態(tài)切換回用戶態(tài),程序繼續(xù)。

以上就是一次內(nèi)存申請的完整過程,可以看到一次內(nèi)存申請過程是非常復(fù)雜的。

 

總結(jié)

 

怎么樣,程序員申請內(nèi)存使用的malloc雖然表面看上去非常簡單,簡單到就一行代碼,但這行代碼背后是非常復(fù)雜的。有的同學(xué)可能會問,為什么我們要理解這背后的原理呢?理解了原理后我才能知道內(nèi)存申請的復(fù)雜性,對于高性能程序來講頻繁的調(diào)用malloc對系統(tǒng)性能是有影響的,那么很自然的一個問題就是我們能否避免malloc?這個問題我們將在接下來的文章中講解。希望本篇對大家理解內(nèi)存分配的底層原理有所幫助。

 

責(zé)任編輯:武曉燕 來源: 碼農(nóng)的荒島求生
相關(guān)推薦

2019-11-12 14:41:41

Redis程序員Linux

2020-08-20 11:50:31

語言類型轉(zhuǎn)換代碼

2021-11-23 23:31:43

C語言數(shù)據(jù)類型系統(tǒng)

2023-03-31 08:12:30

操作系統(tǒng)nanosleep信號

2021-06-30 06:02:38

MySQL SQL 語句數(shù)據(jù)庫

2020-08-17 12:47:07

Mozilla裁員瀏覽器

2022-12-13 10:59:47

devtoolMemory

2010-02-07 09:00:29

AndroidLinux Kerne

2019-08-26 09:35:25

命令ping抓包

2021-04-11 10:40:16

Git軟件開發(fā)

2015-07-03 09:27:43

網(wǎng)絡(luò)閏秒

2020-09-01 11:40:01

HTTPJavaTCP

2023-08-29 16:26:20

Linux命令行

2019-09-16 17:16:29

Hadoop數(shù)據(jù)湖數(shù)據(jù)結(jié)構(gòu)

2021-12-16 15:58:48

Linux內(nèi)存微軟

2022-06-03 08:12:52

InnoDB插入MySQL

2017-09-06 16:20:51

2022-05-31 13:58:09

MySQL查詢語句

2023-11-02 08:00:00

ClickHouse數(shù)據(jù)庫

2022-05-26 23:36:36

SQLMySQL數(shù)據(jù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號