虛擬內(nèi)存

本文轉(zhuǎn)載自 https://juejin.im/post/59f8691b51882534af254317

參考:https://juejin.im/post/59f8691b51882534af254317

1、cpu運(yùn)行過(guò)程中為什么可以動(dòng)態(tài)尋址?

處理器的尋址方式就是如何獲取程序運(yùn)行過(guò)程中操作數(shù)來(lái)源問(wèn)題、一般操作數(shù)來(lái)源于存儲(chǔ)器。這是指的是內(nèi)存。

java代碼被編譯成匯編代碼之后、就會(huì)將代碼中的變量轉(zhuǎn)化為當(dāng)前進(jìn)程所在的虛擬內(nèi)存的地址信息,當(dāng)操作系統(tǒng)調(diào)度到該代碼執(zhí)行的時(shí)候、代碼被裝載進(jìn)入CPU寄存器的時(shí)候、代碼中數(shù)據(jù)的尋址地址是已經(jīng)確定的。所以CPU在代碼 執(zhí)行過(guò)程中是已知具體代碼所引用的數(shù)據(jù)在內(nèi)存中的物理地址的。

內(nèi)存通常被組織為一個(gè)由M個(gè)連續(xù)的字節(jié)大小的單元組成的數(shù)組,每個(gè)字節(jié)都有一個(gè)唯一的物理地址(Physical Address PA),作為到數(shù)組的索引。CPU訪問(wèn)內(nèi)存最簡(jiǎn)單直接的方法就是使用物理地址,這種尋址方式被稱為物理尋址。

現(xiàn)代處理器使用的是一種稱為虛擬尋址(Virtual Addressing)的尋址方式。使用虛擬尋址,CPU需要將虛擬地址翻譯成物理地址,這樣才能訪問(wèn)到真實(shí)的物理內(nèi)存。

2、內(nèi)存的定義

內(nèi)存通常被組織為一個(gè)由M個(gè)連續(xù)的字節(jié)大小的單元組成的數(shù)組,每個(gè)字節(jié)都有一個(gè)唯一的物理地址(Physical Address PA),作為到數(shù)組的索引。CPU訪問(wèn)內(nèi)存最簡(jiǎn)單直接的方法就是使用物理地址,這種尋址方式被稱為物理尋址。

3、CPU尋址的過(guò)程如圖所示

虛擬內(nèi)存尋址

由程序產(chǎn)生的地址被稱為虛擬地址,它們構(gòu)成了一個(gè)虛擬地址空間。在使用虛擬存儲(chǔ)器的情況下,虛擬地址不是被直接送到內(nèi)存總線上,而且是被送到內(nèi)存管理單元(Memory Management Unt,MMU),MMU把虛擬地址映射為物理內(nèi)存地址。

虛擬尋址需要硬件與操作系統(tǒng)之間互相合作。CPU中含有一個(gè)被稱為內(nèi)存管理單元(Memory Management Unit, MMU)的硬件,它的功能是將虛擬地址轉(zhuǎn)換為物理地址。MMU需要借助存放在內(nèi)存中的頁(yè)表來(lái)動(dòng)態(tài)翻譯虛擬地址,該頁(yè)表由操作系統(tǒng)管理。

4、頁(yè)表

虛擬內(nèi)存空間被組織為一個(gè)存放在硬盤(pán)上的M個(gè)連續(xù)的字節(jié)大小的單元組成的數(shù)組,每個(gè)字節(jié)都有一個(gè)唯一的虛擬地址,作為到數(shù)組的索引(這點(diǎn)其實(shí)與物理內(nèi)存是一樣的)。

  操作系統(tǒng)通過(guò)將虛擬內(nèi)存分割為大小固定的塊來(lái)作為硬盤(pán)和內(nèi)存之間的傳輸單位,這個(gè)塊被稱為虛擬頁(yè)(Virtual Page, VP),每個(gè)虛擬頁(yè)的大小為P=2^p字節(jié)。物理內(nèi)存也會(huì)按照這種方法分割為物理頁(yè)(Physical Page, PP),大小也為P字節(jié)。

CPU在獲得虛擬地址之后,需要通過(guò)MMU將虛擬地址翻譯為物理地址。而在翻譯的過(guò)程中還需要借助頁(yè)表,所謂頁(yè)表就是一個(gè)存放在物理內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),它記錄了虛擬頁(yè)與物理頁(yè)的映射關(guān)系。

頁(yè)表是一個(gè)元素為頁(yè)表?xiàng)l目(Page Table Entry, PTE)的集合,每個(gè)虛擬頁(yè)在頁(yè)表中一個(gè)固定偏移量的位置上都有一個(gè)PTE。下面是PTE僅含有一個(gè)有效位標(biāo)記的頁(yè)表結(jié)構(gòu),該有效位代表這個(gè)虛擬頁(yè)是否被緩存在物理內(nèi)存中。

[圖片上傳失敗...(image-b6af28-1539909202897)]

虛擬頁(yè)VP0、VP4VP6、VP7被緩存在物理內(nèi)存中,虛擬頁(yè)VP2VP5被分配在頁(yè)表中,但并沒(méi)有緩存在物理內(nèi)存,虛擬頁(yè)VP1VP3還沒(méi)有被分配。

在進(jìn)行動(dòng)態(tài)內(nèi)存分配時(shí),例如malloc()函數(shù)或者其他高級(jí)語(yǔ)言中的new關(guān)鍵字,操作系統(tǒng)會(huì)在硬盤(pán)中創(chuàng)建或申請(qǐng)一段虛擬內(nèi)存空間,并更新到頁(yè)表(分配一個(gè)PTE,使該P(yáng)TE指向硬盤(pán)上這個(gè)新創(chuàng)建的虛擬頁(yè))?!就鞪ava中NIO的ByteBuffer調(diào)用allocate分配內(nèi)存也是同理,這就叫做內(nèi)存分配不是在物理內(nèi)存上分配、而是在分配一塊虛擬內(nèi)存出來(lái)、等真正使用的時(shí)候操作系統(tǒng)調(diào)入物理內(nèi)存 ****https://www.zhihu.com/question/48960471/answer/122540835****】

由于CPU每次進(jìn)行地址翻譯的時(shí)候都需要經(jīng)過(guò)PTE,所以如果想控制內(nèi)存系統(tǒng)的訪問(wèn),可以在PTE上添加一些額外的許可位(例如讀寫(xiě)權(quán)限、內(nèi)核權(quán)限等),這樣只要有指令違反了這些許可條件,CPU就會(huì)觸發(fā)一個(gè)一般保護(hù)故障,將控制權(quán)傳遞給內(nèi)核中的異常處理程序。一般這種異常被稱為“段錯(cuò)誤(Segmentation Fault)”。

5、頁(yè)命中

[圖片上傳失敗...(image-ebc73a-1539909202897)]

如上圖所示,MMU根據(jù)虛擬地址在頁(yè)表中尋址到了PTE4,該P(yáng)TE的有效位為1,代表該虛擬頁(yè)已經(jīng)被緩存在物理內(nèi)存中了,最終MMU得到了PTE中的物理內(nèi)存地址(指向PP 1)。

6、缺頁(yè)

[圖片上傳失敗...(image-510867-1539909202897)]

如上圖所示,MMU根據(jù)虛擬地址在頁(yè)表中尋址到了PTE 2,該P(yáng)TE的有效位為0,代表該虛擬頁(yè)并沒(méi)有被緩存在物理內(nèi)存中。虛擬頁(yè)沒(méi)有被緩存在物理內(nèi)存中(緩存未命中)被稱為缺頁(yè)

當(dāng)CPU遇見(jiàn)缺頁(yè)時(shí)會(huì)觸發(fā)一個(gè)缺頁(yè)異常,缺頁(yè)異常將控制權(quán)轉(zhuǎn)向操作系統(tǒng)內(nèi)核,然后調(diào)用內(nèi)核中的缺頁(yè)異常處理程序,該程序會(huì)選擇一個(gè)犧牲頁(yè),如果犧牲頁(yè)已被修改過(guò),內(nèi)核會(huì)先將它復(fù)制回硬盤(pán)(采用寫(xiě)回機(jī)制而不是直寫(xiě)也是為了盡量減少對(duì)硬盤(pán)的訪問(wèn)次數(shù)),然后再把該虛擬頁(yè)覆蓋到犧牲頁(yè)的位置,并且更新PTE。

當(dāng)缺頁(yè)異常處理程序返回時(shí),它會(huì)重新啟動(dòng)導(dǎo)致缺頁(yè)的指令,該指令會(huì)把導(dǎo)致缺頁(yè)的虛擬地址重新發(fā)送給MMU。由于現(xiàn)在已經(jīng)成功處理了缺頁(yè)異常,所以最終結(jié)果是頁(yè)命中,并得到物理地址。

這種在硬盤(pán)和內(nèi)存之間傳送頁(yè)的行為稱為頁(yè)面調(diào)度(paging):頁(yè)從硬盤(pán)換入內(nèi)存和從內(nèi)存換出到硬盤(pán)。當(dāng)缺頁(yè)異常發(fā)生時(shí),才將頁(yè)面換入到內(nèi)存的策略稱為按需頁(yè)面調(diào)度(demand paging),所有現(xiàn)代操作系統(tǒng)基本都使用的是按需頁(yè)面調(diào)度的策略。

虛擬內(nèi)存跟CPU高速緩存(或其他使用緩存的技術(shù))一樣依賴于局部性原則。雖然處理缺頁(yè)消耗的性能很多(畢竟還是要從硬盤(pán)中讀?。?,而且程序在運(yùn)行過(guò)程中引用的不同虛擬頁(yè)的總數(shù)可能會(huì)超出物理內(nèi)存的大小,但是局部性原則保證了在任意時(shí)刻,程序?qū)②呄蛴谠谝粋€(gè)較小的活動(dòng)頁(yè)面(active page)集合上工作,這個(gè)集合被稱為工作集(working set)。根據(jù)空間局部性原則(一個(gè)被訪問(wèn)過(guò)的內(nèi)存地址以及其周邊的內(nèi)存地址都會(huì)有很大幾率被再次訪問(wèn))與時(shí)間局部性原則(一個(gè)被訪問(wèn)過(guò)的內(nèi)存地址在之后會(huì)有很大幾率被再次訪問(wèn)),只要將工作集緩存在物理內(nèi)存中,接下來(lái)的地址翻譯請(qǐng)求很大幾率都在其中,從而減少了額外的硬盤(pán)流量。

如果一個(gè)程序沒(méi)有良好的局部性,將會(huì)使工作集的大小不斷膨脹,直至超過(guò)物理內(nèi)存的大小,這時(shí)程序會(huì)產(chǎn)生一種叫做抖動(dòng)(thrashing)的狀態(tài),頁(yè)面會(huì)不斷地?fù)Q入換出,如此多次的讀寫(xiě)硬盤(pán)開(kāi)銷(xiāo),性能自然會(huì)十分“恐怖”。所以,想要編寫(xiě)出性能高效的程序,首先要保證程序的時(shí)間局部性與空間局部性。

7、多級(jí)頁(yè)表 多級(jí)頁(yè)表是如何節(jié)約內(nèi)存的  多級(jí)頁(yè)面占用內(nèi)存分析

  通過(guò)一個(gè)頂級(jí)頁(yè)表為真正有用的頁(yè)表提供索引,這是我所理解的二級(jí)頁(yè)表的本質(zhì)**

我們目前為止討論的只是單頁(yè)表,但在實(shí)際的環(huán)境中虛擬空間地址都是很大的(一個(gè)32位系統(tǒng)的地址空間有2^32 = 4GB,更別說(shuō)64位系統(tǒng)了)。在這種情況下,使用一個(gè)單頁(yè)表明顯是效率低下的并且也是很浪費(fèi)內(nèi)存空間的。

 常用方法是使用層次結(jié)構(gòu)的頁(yè)表。假設(shè)我們的環(huán)境為一個(gè)32位的虛擬地址空間,它有如下形式:

  • 虛擬地址空間被分為4KB的頁(yè),每個(gè)PTE都是4字節(jié)。

  • 內(nèi)存的前2K個(gè)頁(yè)面分配給了代碼和數(shù)據(jù)。

  • 之后的6K個(gè)頁(yè)面還未被分配。

  • 再接下來(lái)的1023個(gè)頁(yè)面也未分配,其后的1個(gè)頁(yè)面分配給了用戶棧。

下圖是為該虛擬地址空間構(gòu)造的二級(jí)頁(yè)表層次結(jié)構(gòu)(真實(shí)情況中多為四級(jí)或更多),一級(jí)頁(yè)表(1024個(gè)PTE正好覆蓋4GB的虛擬地址空間,同時(shí)每個(gè)PTE只有4字節(jié),這樣一級(jí)頁(yè)表與二級(jí)頁(yè)表的大小也正好與一個(gè)頁(yè)面的大小一致都為4KB)的每個(gè)PTE負(fù)責(zé)映射虛擬地址空間中一個(gè)4MB的片(chunk),每一片都由1024個(gè)連續(xù)的頁(yè)面組成。二級(jí)頁(yè)表中的每個(gè)PTE負(fù)責(zé)映射一個(gè)4KB的虛擬內(nèi)存頁(yè)面。

[圖片上傳失敗...(image-ae2ac9-1539909202897)]

8、地址翻譯的過(guò)程

從形式上來(lái)說(shuō),地址翻譯是一個(gè)N元素的虛擬地址空間中的元素和一個(gè)M元素的物理地址空間中元素之間的映射。

下圖為MMU利用頁(yè)表進(jìn)行尋址的過(guò)程:

image

頁(yè)表基址寄存器(PTBR)指向當(dāng)前頁(yè)表。一個(gè)n位的虛擬地址【也就是CPU的尋址地址】包含兩個(gè)部分,一個(gè)p位的虛擬頁(yè)面偏移量(Virtual Page Offset, VPO)和一個(gè)(n - p)位的虛擬頁(yè)號(hào)(Virtual Page Number, VPN)。

MMU根據(jù)VPN來(lái)選擇對(duì)應(yīng)的PTE,例如VPN 0代表PTE 0、VPN 1代表PTE 1....因?yàn)槲锢眄?yè)與虛擬頁(yè)的大小是一致的,所以物理頁(yè)面偏移量(Physical Page Offset, PPO)與VPO是相同的。那么之后只要將PTE中的物理頁(yè)號(hào)(Physical Page Number, PPN)與虛擬地址中的VPO串聯(lián)起來(lái),就能得到相應(yīng)的物理地址。

多級(jí)頁(yè)表的地址翻譯也是如此,只不過(guò)因?yàn)橛卸鄠€(gè)層次,所以VPN需要分成多段。假設(shè)有一個(gè)k級(jí)頁(yè)表,虛擬地址會(huì)被分割成k個(gè)VPN和1個(gè)VPO,每個(gè)VPN i都是一個(gè)到第i級(jí)頁(yè)表的索引。為了構(gòu)造物理地址,MMU需要訪問(wèn)k個(gè)PTE才能拿到對(duì)應(yīng)的PPN。

image

9、TLB

頁(yè)表是被緩存在內(nèi)存中的,盡管內(nèi)存的速度相對(duì)于硬盤(pán)來(lái)說(shuō)已經(jīng)非??炝耍cCPU還是有所差距。為了防止每次地址翻譯操作都需要去訪問(wèn)內(nèi)存,CPU使用了高速緩存與TLB來(lái)緩存PTE。

在最糟糕的情況下(不包括缺頁(yè)),MMU需要訪問(wèn)內(nèi)存取得相應(yīng)的PTE,這個(gè)代價(jià)大約為幾十到幾百個(gè)周期,如果PTE湊巧緩存在L1高速緩存中(如果L1沒(méi)有還會(huì)從L2中查找,不過(guò)我們忽略多級(jí)緩沖區(qū)的細(xì)節(jié)),那么性能開(kāi)銷(xiāo)就會(huì)下降到1個(gè)或2個(gè)周期。然而,許多系統(tǒng)甚至需要消除即使這樣微小的開(kāi)銷(xiāo),TLB由此而生。

image

LB(Translation Lookaside Buffer, TLB)被稱為翻譯后備緩沖器或翻譯旁路緩沖器,它是MMU中的一個(gè)緩沖區(qū),其中每一行都保存著一個(gè)由單個(gè)PTE組成的塊。用于組選擇和行匹配的索引與標(biāo)記字段是從VPN中提取出來(lái)的,如果TLB中有T = 2^t個(gè)組,那么TLB索引(TLBI)是由VPN的t個(gè)最低位組成的,而TLB標(biāo)記(TLBT)是由VPN中剩余的位組成的。

下圖為地址翻譯的流程(TLB命中的情況下):

image
  • 第一步,CPU將一個(gè)虛擬地址交給MMU進(jìn)行地址翻譯。

  • 第二步和第三步,MMU通過(guò)TLB取得相應(yīng)的PTE。

  • 第四步,MMU通過(guò)PTE翻譯出物理地址并將它發(fā)送給高速緩存/內(nèi)存。

  • 第五步,高速緩存返回?cái)?shù)據(jù)到CPU(如果緩存命中的話,否則還需要訪問(wèn)內(nèi)存)。

當(dāng)TLB未命中時(shí),MMU必須從高速緩存/內(nèi)存中取出相應(yīng)的PTE,并將新取得的PTE存放到TLB(如果TLB已滿會(huì)覆蓋一個(gè)已經(jīng)存在的PTE)。

image

10、Linux中的虛擬內(nèi)存系統(tǒng)

Linux為每個(gè)進(jìn)程維護(hù)了一個(gè)單獨(dú)的虛擬地址空間。虛擬地址空間分為內(nèi)核空間與用戶空間,用戶空間包括代碼、數(shù)據(jù)、堆、共享庫(kù)以及棧,內(nèi)核空間包括內(nèi)核中的代碼和數(shù)據(jù)結(jié)構(gòu),內(nèi)核空間的某些區(qū)域被映射到所有進(jìn)程共享的物理頁(yè)面。Linux也將一組連續(xù)的虛擬頁(yè)面(大小等于內(nèi)存總量)映射到相應(yīng)的一組連續(xù)的物理頁(yè)面,這種做法為內(nèi)核提供了一種便利的方法來(lái)訪問(wèn)物理內(nèi)存中任何特定的位置。

image

Linux將虛擬內(nèi)存組織成一些區(qū)域(也稱為段)的集合,區(qū)域的概念允許虛擬地址空間有間隙。一個(gè)區(qū)域就是已經(jīng)存在著的已分配的虛擬內(nèi)存的連續(xù)片(chunk)。例如,代碼段、數(shù)據(jù)段、堆、共享庫(kù)段,以及用戶棧都屬于不同的區(qū)域,每個(gè)存在的虛擬頁(yè)都保存在某個(gè)區(qū)域中,而不屬于任何區(qū)域的虛擬頁(yè)是不存在的,也不能被進(jìn)程所引用。

內(nèi)核為系統(tǒng)中的每個(gè)進(jìn)程維護(hù)一個(gè)單獨(dú)的任務(wù)結(jié)構(gòu)(task_struct)。任務(wù)結(jié)構(gòu)中的元素包含或者指向內(nèi)核運(yùn)行該進(jìn)程所需的所有信息(PID、指向用戶棧的指針、可執(zhí)行目標(biāo)文件的名字、程序計(jì)數(shù)器等)。

image

  • mm_struct:描述了虛擬內(nèi)存的當(dāng)前狀態(tài)。pgd指向一級(jí)頁(yè)表的基址(當(dāng)內(nèi)核運(yùn)行這個(gè)進(jìn)程時(shí),pgd會(huì)被存放在CR3控制寄存器,也就是頁(yè)表基址寄存器中),mmap指向一個(gè)vm_area_structs的鏈表,其中每個(gè)vm_area_structs都描述了當(dāng)前虛擬地址空間的一個(gè)區(qū)域。

  • vm_starts:指向這個(gè)區(qū)域的起始處。

  • vm_end:指向這個(gè)區(qū)域的結(jié)束處。

  • vm_prot:描述這個(gè)區(qū)域內(nèi)包含的所有頁(yè)的讀寫(xiě)許可權(quán)限。

  • vm_flags:描述這個(gè)區(qū)域內(nèi)的頁(yè)面是與其他進(jìn)程共享的,還是這個(gè)進(jìn)程私有的以及一些其他信息。

  • vm_next:指向鏈表的下一個(gè)區(qū)域結(jié)構(gòu)。

11、內(nèi)存映射 nmap的概念理解 java內(nèi)存映射文件

內(nèi)存映射、java讀取磁盤(pán)文件、必須每次調(diào)用操作系統(tǒng)提供的底層標(biāo)準(zhǔn)IO系統(tǒng)調(diào)用函數(shù) read()、write(),此時(shí)調(diào)用此函數(shù)的進(jìn)程(在JAVA中即java進(jìn)程)由當(dāng)前的用戶態(tài)切換到內(nèi)核態(tài),然后OS的內(nèi)核代碼負(fù)責(zé)將相應(yīng)的文件數(shù)據(jù)讀取到內(nèi)核的IO緩沖區(qū),(這里的內(nèi)核IO緩沖區(qū)是進(jìn)程虛擬內(nèi)存的內(nèi)核部分?jǐn)?shù)據(jù)、映射到同一片物理內(nèi)存當(dāng)中),然 后再把數(shù)據(jù)從內(nèi)核IO緩沖區(qū)拷貝到進(jìn)程的私有地址空間中去,這樣便完成了一次IO操作

Linux通過(guò)將一個(gè)虛擬內(nèi)存區(qū)域與一個(gè)硬盤(pán)上的文件關(guān)聯(lián)起來(lái),以初始化這個(gè)虛擬內(nèi)存區(qū)域的內(nèi)容,這個(gè)過(guò)程稱為內(nèi)存映射(memory mapping)。這種將虛擬內(nèi)存系統(tǒng)集成到文件系統(tǒng)的方法可以簡(jiǎn)單而高效地把程序和數(shù)據(jù)加載到內(nèi)存中。

一個(gè)區(qū)域可以映射到一個(gè)普通硬盤(pán)文件的連續(xù)部分,例如一個(gè)可執(zhí)行目標(biāo)文件。文件區(qū)(section)被分成頁(yè)大小的片,每一片包含一個(gè)虛擬頁(yè)的初始內(nèi)容。由于按需頁(yè)面調(diào)度的策略,這些虛擬頁(yè)面沒(méi)有實(shí)際交換進(jìn)入物理內(nèi)存,直到CPU引用的虛擬地址在該區(qū)域的范圍內(nèi)。如果區(qū)域比文件區(qū)要大,那么就用零來(lái)填充這個(gè)區(qū)域的余下部分。

  一個(gè)區(qū)域也可以映射到一個(gè)匿名文件,匿名文件是由內(nèi)核創(chuàng)建的,包含的全是二進(jìn)制零。當(dāng)CPU第一次引用這樣一個(gè)區(qū)域內(nèi)的虛擬頁(yè)面時(shí),內(nèi)核就在物理內(nèi)存中找到一個(gè)合適的犧牲頁(yè)面,如果該頁(yè)面被修改過(guò),就先將它寫(xiě)回到硬盤(pán),之后用二進(jìn)制零覆蓋犧牲頁(yè)并更新頁(yè)表,將這個(gè)頁(yè)面標(biāo)記為已緩存在內(nèi)存中的。

簡(jiǎn)單的來(lái)說(shuō):普通文件映射就是將一個(gè)文件與一塊內(nèi)存建立起映射關(guān)系,對(duì)該文件進(jìn)行IO操作可以繞過(guò)內(nèi)核直接在用戶態(tài)完成(用戶態(tài)在該虛擬地址區(qū)域讀寫(xiě)就相當(dāng)于讀寫(xiě)這個(gè)文件)。匿名文件映射一般在用戶空間需要分配一段內(nèi)存來(lái)存放數(shù)據(jù)時(shí),由內(nèi)核創(chuàng)建匿名文件并與內(nèi)存進(jìn)行映射,之后用戶態(tài)就可以通過(guò)操作這段虛擬地址來(lái)操作內(nèi)存了。匿名文件映射最熟悉的應(yīng)用場(chǎng)景就是動(dòng)態(tài)內(nèi)存分配(malloc()函數(shù))。

Linux很多地方都采用了“懶加載”機(jī)制,自然也包括內(nèi)存映射。不管是普通文件映射還是匿名映射,Linux只會(huì)先劃分虛擬內(nèi)存地址。只有當(dāng)CPU第一次訪問(wèn)該區(qū)域內(nèi)的虛擬地址時(shí),才會(huì)真正的與物理內(nèi)存建立映射關(guān)系。

  只要虛擬頁(yè)被初始化了,它就在一個(gè)由內(nèi)核維護(hù)的交換文件(swap file)之間換來(lái)?yè)Q去。交換文件又稱為交換空間(swap space)或交換區(qū)域(swap area)。swap區(qū)域不止用于頁(yè)交換,在物理內(nèi)存不夠的情況下,還會(huì)將部分內(nèi)存數(shù)據(jù)交換到swap區(qū)域(使用硬盤(pán)來(lái)擴(kuò)展內(nèi)存)。

11、匿名映射(malloc分配內(nèi)存的原理)

    在內(nèi)核里,用戶空間的進(jìn)程要訪問(wèn)內(nèi)存或磁盤(pán)里的數(shù)據(jù)要通過(guò)映射的方式將內(nèi)存的物理地址和用戶空間的虛擬地址聯(lián)系起來(lái).用戶通過(guò)訪問(wèn)這樣的虛擬地址就可以訪問(wèn)到實(shí)際的物理地址,也就是實(shí)際的物理內(nèi)存.映射在實(shí)現(xiàn)虛擬地址到物理地址中扮演重要角色. 內(nèi)核中映射分為文件映射和匿名映射.

  文件映射就是磁盤(pán)中的數(shù)據(jù)通過(guò)文件系統(tǒng)映射到內(nèi)存再通過(guò)文件映射映射到虛擬空間.這樣,用戶就可以在用戶空間通過(guò)open ,read, write 等函數(shù)區(qū)操作文件內(nèi)容.

  匿名映射就是用戶空間需要分配一定的物理內(nèi)存來(lái)存儲(chǔ)數(shù)據(jù),這部分內(nèi)存不屬于任何文件,內(nèi)核就使用匿名映射將內(nèi)存中的某段物理地址與用戶空間(用戶進(jìn)程的虛擬空間)一一映射,這樣用戶就可用直接操作虛擬地址來(lái)范圍這段物理內(nèi)存.至于實(shí)際的代碼,文件映射的操作就是: open,read,write,close,mmap... 操作的虛擬地址都屬于文件映射.malloc 分配的虛擬地址屬于匿名映射

Linux很多地方都采用了“懶加載”機(jī)制,自然也包括內(nèi)存映射。不管是普通文件映射還是匿名映射,Linux只會(huì)先劃分虛擬內(nèi)存地址。只有當(dāng)CPU第一次訪問(wèn)該區(qū)域內(nèi)的虛擬地址時(shí),才會(huì)真正的與物理內(nèi)存建立映射關(guān)系。

  只要虛擬頁(yè)被初始化了,它就在一個(gè)由內(nèi)核維護(hù)的交換文件(swap file)之間換來(lái)?yè)Q去。交換文件又稱為交換空間(swap space)或交換區(qū)域(swap area)。swap區(qū)域不止用于頁(yè)交換,在物理內(nèi)存不夠的情況下,還會(huì)將部分內(nèi)存數(shù)據(jù)交換到swap區(qū)域(使用硬盤(pán)來(lái)擴(kuò)展內(nèi)存)。

CPU 第一次訪問(wèn)匿名映射的空間,所謂的訪問(wèn)就是讀或者寫(xiě)這部分區(qū)域.根據(jù) glibc 也就是用戶空間對(duì) malloc 分配的內(nèi)存,如果不進(jìn)行實(shí)際的訪問(wèn),內(nèi)核只分配虛擬地址,而不將虛擬地址通過(guò)匿名映射 與物理地址一一對(duì)應(yīng).只有 CPU 實(shí)際訪問(wèn)了這些區(qū)域,才真正發(fā)生匿名映射,也才發(fā)生缺頁(yè)處理. CPU 第一次引用匿名區(qū)域時(shí),由于匿名虛擬區(qū)還沒(méi)有和實(shí)際的物理區(qū)域映射,內(nèi)核就會(huì)發(fā)生一個(gè)缺頁(yè)錯(cuò)誤.于是內(nèi)核就會(huì) 通過(guò)一些策略,從 swap 區(qū)獲得一塊物理內(nèi)存頁(yè)與之映射.這樣虛擬地址就和物理地址掛鉤,也就會(huì)生成相應(yīng)的頁(yè)表, 頁(yè)表相關(guān)信息會(huì)被存儲(chǔ)到 TLB 或 CACHE 中以假設(shè)虛擬地址到物理地址的轉(zhuǎn)換.

內(nèi)核為了管理內(nèi)存,會(huì)將物理內(nèi)存分成 4K 大小的數(shù)據(jù)塊進(jìn)行管理,如果這塊物理頁(yè)已經(jīng)分配出去了,這樣的統(tǒng)計(jì)計(jì)數(shù)就會(huì)加 1, 其狀態(tài)就會(huì)變成使用中,其也會(huì)被加入到正在使用物理頁(yè)鏈表中.由于該頁(yè)已經(jīng)通過(guò)匿名映射分配出去了.

12、swap區(qū)域

swap 區(qū)叫做交換分區(qū),是 Linux 采取的一種內(nèi)存策略,通過(guò)把磁盤(pán)的內(nèi)存當(dāng)做物理內(nèi)存來(lái)用.這樣做的目的就是增大了物理內(nèi)存的大小,其明顯的缺點(diǎn)就是速度沒(méi)有物理內(nèi)存快.

swap 區(qū)域一般分作兩部分,一部分位于物理內(nèi)存,叫做 swapcache 區(qū)域,另外一部分位于磁盤(pán)上.

這里涉及的到換頁(yè),簡(jiǎn)單來(lái)說(shuō)就是把不經(jīng)常使用的匿名頁(yè)幀置換到磁盤(pán)上.注意,內(nèi)存存在兩種映射: 匿名映射和文件映射,文件映射還和虛擬文件系統(tǒng)有關(guān),它有自己的文件緩存和緩沖,文件映射的物理頁(yè)幀是不會(huì)來(lái)自 swap區(qū)域 所以 swap 區(qū)域就是匿名映射的物理頁(yè)幀.這個(gè)區(qū)域由內(nèi)核創(chuàng)建并管理.

13、共享對(duì)象 詳細(xì)解釋

虛擬內(nèi)存系統(tǒng)為每個(gè)進(jìn)程提供了私有的虛擬地址空間,這樣可以保證進(jìn)程之間不會(huì)發(fā)生錯(cuò)誤的讀寫(xiě)。但多個(gè)進(jìn)程之間也含有相同的部分,例如每個(gè)C程序都使用到了C標(biāo)準(zhǔn)庫(kù),如果每個(gè)進(jìn)程都在物理內(nèi)存中保持這些代碼的副本,那會(huì)造成很大的內(nèi)存資源浪費(fèi)。

內(nèi)存映射提供了共享對(duì)象的機(jī)制,來(lái)避免內(nèi)存資源的浪費(fèi)。一個(gè)對(duì)象被映射到虛擬內(nèi)存的一個(gè)區(qū)域,要么是作為共享對(duì)象,要么是作為私有對(duì)象的。

如果一個(gè)進(jìn)程將一個(gè)共享對(duì)象映射到它的虛擬地址空間的一個(gè)區(qū)域內(nèi),那么這個(gè)進(jìn)程對(duì)這個(gè)區(qū)域的任何寫(xiě)操作,對(duì)于那些也把這個(gè)共享對(duì)象映射到它們虛擬內(nèi)存的其他進(jìn)程而言,也是可見(jiàn)的。相對(duì)的,對(duì)一個(gè)映射到私有對(duì)象的區(qū)域的任何寫(xiě)操作,對(duì)于其他進(jìn)程來(lái)說(shuō)是不可見(jiàn)的。一個(gè)映射到共享對(duì)象的虛擬內(nèi)存區(qū)域叫做共享區(qū)域,類(lèi)似地,也有私有區(qū)域。

為了節(jié)約內(nèi)存,私有對(duì)象開(kāi)始的生命周期與共享對(duì)象基本上是一致的(在物理內(nèi)存中只保存私有對(duì)象的一份副本),并使用寫(xiě)時(shí)復(fù)制的技術(shù)來(lái)應(yīng)對(duì)多個(gè)進(jìn)程的寫(xiě)沖突。

image

只要沒(méi)有進(jìn)程試圖寫(xiě)它自己的私有區(qū)域,那么多個(gè)進(jìn)程就可以繼續(xù)共享物理內(nèi)存中私有對(duì)象的一個(gè)單獨(dú)副本。然而,只要有一個(gè)進(jìn)程試圖對(duì)私有區(qū)域的某一頁(yè)面進(jìn)行寫(xiě)操作,就會(huì)觸發(fā)一個(gè)保護(hù)異常。在上圖中,進(jìn)程B試圖對(duì)私有區(qū)域的一個(gè)頁(yè)面進(jìn)行寫(xiě)操作,該操作觸發(fā)了保護(hù)異常。異常處理程序會(huì)在物理內(nèi)存中創(chuàng)建這個(gè)頁(yè)面的一個(gè)新副本,并更新PTE指向這個(gè)新的副本,然后恢復(fù)這個(gè)頁(yè)的可寫(xiě)權(quán)限。

還有一個(gè)典型的例子就是fork()函數(shù),該函數(shù)用于創(chuàng)建子進(jìn)程。當(dāng)fork()函數(shù)被當(dāng)前進(jìn)程調(diào)用時(shí),內(nèi)核會(huì)為新進(jìn)程創(chuàng)建各種必要的數(shù)據(jù)結(jié)構(gòu),并分配給它一個(gè)唯一的PID。為了給新進(jìn)程創(chuàng)建虛擬內(nèi)存,它復(fù)制了當(dāng)前進(jìn)程的mm_structvm_area_struct和頁(yè)表的原樣副本。并將兩個(gè)進(jìn)程的每個(gè)頁(yè)面都標(biāo)為只讀,兩個(gè)進(jìn)程中的每個(gè)區(qū)域都標(biāo)記為私有區(qū)域(寫(xiě)時(shí)復(fù)制)。

這樣,父進(jìn)程和子進(jìn)程的虛擬內(nèi)存空間完全一致,只有當(dāng)這兩個(gè)進(jìn)程中的任一個(gè)進(jìn)行寫(xiě)操作時(shí),再使用寫(xiě)時(shí)復(fù)制來(lái)保證每個(gè)進(jìn)程的虛擬地址空間私有的抽象概念。

13、動(dòng)態(tài)內(nèi)存分配

雖然可以使用內(nèi)存映射(mmap()函數(shù))來(lái)創(chuàng)建和刪除虛擬內(nèi)存區(qū)域來(lái)滿足運(yùn)行時(shí)動(dòng)態(tài)內(nèi)存分配的問(wèn)題。然而,為了更好的移植性與便利性,還需要一個(gè)更高層面的抽象,也就是動(dòng)態(tài)內(nèi)存分配器(dynamic memory allocator)。

動(dòng)態(tài)內(nèi)存分配器維護(hù)著一個(gè)進(jìn)程的虛擬內(nèi)存區(qū)域,也就是我們所熟悉的“堆(heap)”,內(nèi)核中還維護(hù)著一個(gè)指向堆頂?shù)闹羔榖rk(break)。動(dòng)態(tài)內(nèi)存分配器將堆視為一個(gè)連續(xù)的虛擬內(nèi)存塊(chunk)的集合,每個(gè)塊有兩種狀態(tài),已分配和空閑。已分配的塊顯式地保留為供應(yīng)用程序使用,空閑塊則可以用來(lái)進(jìn)行分配,它的空閑狀態(tài)直到它顯式地被應(yīng)用程序分配為止。已分配的塊要么被應(yīng)用程序顯式釋放,要么被垃圾回收器所釋放。

image

本文只講解動(dòng)態(tài)內(nèi)存分配的一些概念,關(guān)于動(dòng)態(tài)內(nèi)存分配器的實(shí)現(xiàn)已經(jīng)超出了本文的討論范圍。如果有對(duì)它感興趣的同學(xué),可以去參考dlmalloc的源碼,它是由Doug Lea(就是寫(xiě)Java并發(fā)包的那位)實(shí)現(xiàn)的一個(gè)設(shè)計(jì)巧妙的內(nèi)存分配器,而且源碼中的注釋十分多。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 初遇簡(jiǎn)書(shū),俺關(guān)注散文。 如那首歌唱的那樣——這些年過(guò)得不好不壞,只是好像少了一個(gè)人存在…… 天天在公眾號(hào)...
    o那么大氣o閱讀 349評(píng)論 0 1
  • 寒冬暖陽(yáng),窗外金錢(qián)松的葉子在陣陣晨風(fēng)的吹弄下前赴后繼的被拍打?yàn)⒙?,一片,兩片…? 我小的時(shí)候,是一...
    舊故事里的風(fēng)雨聲閱讀 410評(píng)論 0 0
  • 當(dāng)Xcode更新到9.3后,提交代碼時(shí)發(fā)現(xiàn)project.xcworkspace文件下多了IDEWorkspace...
    Coder_Cat閱讀 2,218評(píng)論 0 1

友情鏈接更多精彩內(nèi)容