微吼云上線多路互動(dòng)直播服務(wù) 加速多場景互動(dòng)直播落地
670
2025-04-01
文章目錄
其他文章
虛擬內(nèi)存
內(nèi)存分段
內(nèi)存分頁
簡單分頁
多級(jí)頁表
頁表緩存TLB(Translation Lookaside Buffer)
Linux內(nèi)存管理
其他文章
操作系統(tǒng)——概述
操作系統(tǒng)——內(nèi)存管理
操作系統(tǒng)——進(jìn)程和線程
操作系統(tǒng)——進(jìn)程間通信
操作系統(tǒng)——文件系統(tǒng)
操作系統(tǒng)——設(shè)備管理
操作系統(tǒng)——網(wǎng)絡(luò)系統(tǒng)
虛擬內(nèi)存
如果你是電子相關(guān)專業(yè)的,肯定在大學(xué)里搗鼓過單片機(jī)。
單片機(jī)是沒有操作系統(tǒng)的,所以每次寫完代碼,都需要借助工具把程序燒錄進(jìn)去,這樣程序才能跑起來。
另外,單片機(jī)的 CPU 是直接操作內(nèi)存的「物理地址」。
在這種情況下,要想在內(nèi)存中同時(shí)運(yùn)行兩個(gè)程序是不可能的。如果第一個(gè)程序在 2000 的位置寫入一個(gè)新的值,將會(huì)擦掉第二個(gè)程序存放在相同位置上的所有內(nèi)容,所以同時(shí)運(yùn)行兩個(gè)程序是根本行不通的,這兩個(gè)程序會(huì)立刻崩潰。
操作系統(tǒng)如何解決這個(gè)問題呢?
這里關(guān)鍵的問題是這兩個(gè)程序都引用了絕對物理地址,而這正是我們最需要避免的。
我們可以把進(jìn)程所使用的地址「隔離」開來,即讓操作系統(tǒng)為每個(gè)進(jìn)程分配獨(dú)立的一套「虛擬地址」,人人都有,大家自己玩自己的地址就行,互不干涉。但是有個(gè)前提每個(gè)進(jìn)程都不能訪問物理地址,至于虛擬地址最終怎么落到物理內(nèi)存里,對進(jìn)程來說是透明的,操作系統(tǒng)已經(jīng)把這些都安排的明明白白了。
操作系統(tǒng)會(huì)提供一種機(jī)制,將不同進(jìn)程的虛擬地址和不同內(nèi)存的物理地址映射起來。
如果程序要訪問虛擬地址的時(shí)候,由操作系統(tǒng)轉(zhuǎn)換成不同的物理地址,這樣不同的進(jìn)程運(yùn)行的時(shí)候,寫入的是不同的物理地址,這樣就不會(huì)沖突了。
于是,這里就引出了兩種地址的概念:
我們程序所使用的內(nèi)存地址叫做虛擬內(nèi)存地址(Virtual Memory Address)
實(shí)際存在硬件里面的空間地址叫物理內(nèi)存地址(Physical Memory Address)。
操作系統(tǒng)引入了虛擬內(nèi)存,進(jìn)程持有的虛擬地址會(huì)通過 CPU 芯片中的內(nèi)存管理單元(MMU)的映射關(guān)系,來轉(zhuǎn)換變成物理地址,然后再通過物理地址訪問內(nèi)存,如下圖所示:
操作系統(tǒng)如何管理虛擬地址和物理地址之間的關(guān)系呢?
主要有兩種方式,分別是內(nèi)存分段和內(nèi)存分頁,分段是比較早提出的,我們先來看看內(nèi)存分段。
內(nèi)存分段
程序是由若干個(gè)邏輯分段組成的,如可由代碼分段、數(shù)據(jù)分段、棧段、堆段組成。不同的段是有不同的屬性的,所以就用分段(Segmentation)的形式把這些段分離出來。
分段機(jī)制下,虛擬地址和物理地址如何映射?
分段機(jī)制下的虛擬地址由兩部分組成,段選擇子和段內(nèi)偏移量。
段選擇子就保存在段寄存器里面。段選擇子里面最重要的是段號(hào),用作段表的索引。段表里面保存的是這個(gè)段的基地址、段的界限和特權(quán)等級(jí)等。
虛擬地址中的段內(nèi)偏移量應(yīng)該位于 0 和段界限之間,如果段內(nèi)偏移量是合法的,就將段基地址加上段內(nèi)偏移量得到物理內(nèi)存地址。
在上面了,知道了虛擬地址是通過段表與物理地址進(jìn)行映射的,分段機(jī)制會(huì)把程序的虛擬地址分成 4 個(gè)段,每個(gè)段在段表中有一個(gè)項(xiàng),在這一項(xiàng)找到段的基地址,再加上偏移量,于是就能找到物理內(nèi)存中的地址,如下圖:
如果要訪問段 3 中偏移量 500 的虛擬地址,我們可以計(jì)算出物理地址為,段 3 基地址 7000 + 偏移量 500 = 7500。
分段的辦法很好,解決了程序本身不需要關(guān)心具體的物理內(nèi)存地址的問題,但它也有一些不足之處:
第一個(gè)就是內(nèi)存碎片的問題。
第二個(gè)就是內(nèi)存交換的效率低的問題。
接下來,說說為什么會(huì)有這兩個(gè)問題。
我們來看這樣一個(gè)例子。我現(xiàn)在手頭的這臺(tái)電腦,有 1GB 的內(nèi)存。我們先啟動(dòng)一個(gè)圖形渲染程序,占用了 512MB 的內(nèi)存,接著啟動(dòng)一個(gè) Chrome 瀏覽器,占用了 128MB 內(nèi)存,再啟動(dòng)一個(gè) Python 程序,占用了 256MB 內(nèi)存。這個(gè)時(shí)候,我們關(guān)掉 Chrome,于是空閑內(nèi)存還有 1024 - 512 - 256 = 256MB。按理來說,我們有足夠的空間再去裝載一個(gè) 200MB 的程序。但是,這 256MB 的內(nèi)存空間不是連續(xù)的,而是被分成了兩段 128MB 的內(nèi)存。因此,實(shí)際情況是,我們的程序沒辦法加載進(jìn)來。
當(dāng)然,這個(gè)我們也有辦法解決。解決的辦法叫內(nèi)存交換(Memory Swapping)。
我們可以把 Python 程序占用的那 256MB 內(nèi)存寫到硬盤上,然后再從硬盤上讀回來到內(nèi)存里面。不過讀回來的時(shí)候,我們不再把它加載到原來的位置,而是緊緊跟在那已經(jīng)被占用了的 512MB 內(nèi)存后面。這樣,我們就有了連續(xù)的 256MB 內(nèi)存空間,就可以去加載一個(gè)新的 200MB 的程序。如果你自己安裝過 Linux 操作系統(tǒng),你應(yīng)該遇到過分配一個(gè) swap 硬盤分區(qū)的問題。這塊分出來的磁盤空間,其實(shí)就是專門給 Linux 操作系統(tǒng)進(jìn)行內(nèi)存交換用的。
虛擬內(nèi)存、分段,再加上內(nèi)存交換,看起來似乎已經(jīng)解決了計(jì)算機(jī)同時(shí)裝載運(yùn)行很多個(gè)程序的問題。不過,你千萬不要大意,這三者的組合仍然會(huì)遇到一個(gè)性能瓶頸。硬盤的訪問速度要比內(nèi)存慢很多,而每一次內(nèi)存交換,我們都需要把一大段連續(xù)的內(nèi)存數(shù)據(jù)寫到硬盤上。所以,如果內(nèi)存交換的時(shí)候,交換的是一個(gè)很占內(nèi)存空間的程序,這樣整個(gè)機(jī)器都會(huì)顯得卡頓。
為了解決內(nèi)存分段的內(nèi)存碎片和內(nèi)存交換效率低的問題,就出現(xiàn)了內(nèi)存分頁。
內(nèi)存分頁
分段的好處就是能產(chǎn)生連續(xù)的內(nèi)存空間,但是會(huì)出現(xiàn)內(nèi)存碎片和內(nèi)存交換的空間太大的問題。
要解決這些問題,那么就要想出能少出現(xiàn)一些內(nèi)存碎片的辦法。另外,當(dāng)需要進(jìn)行內(nèi)存交換的時(shí)候,讓需要交換寫入或者從磁盤裝載的數(shù)據(jù)更少一點(diǎn),這樣就可以解決問題了。這個(gè)辦法,也就是內(nèi)存分頁(Paging)。
分頁是把整個(gè)虛擬和物理內(nèi)存空間切成一段段固定尺寸的大小。這樣一個(gè)連續(xù)并且尺寸固定的內(nèi)存空間,我們叫頁(Page)。在 Linux 下,每一頁的大小為 4KB。
虛擬地址與物理地址之間通過頁表來映射,如下圖:
頁表實(shí)際上存儲(chǔ)在 CPU 的內(nèi)存管理單元 (MMU) 中,于是 CPU 就可以直接通過 MMU,找出要實(shí)際要訪問的物理內(nèi)存地址。
而當(dāng)進(jìn)程訪問的虛擬地址在頁表中查不到時(shí),系統(tǒng)會(huì)產(chǎn)生一個(gè)缺頁異常,進(jìn)入系統(tǒng)內(nèi)核空間分配物理內(nèi)存、更新進(jìn)程頁表,最后再返回用戶空間,恢復(fù)進(jìn)程的運(yùn)行。
分頁怎么解決分段的內(nèi)存碎片、內(nèi)存交換效率低的問題?
由于內(nèi)存空間都是預(yù)先劃分好的,也就不會(huì)像分段會(huì)產(chǎn)生間隙非常小的內(nèi)存,這正是分段會(huì)產(chǎn)生內(nèi)存碎片的原因。而采用了分頁,那么釋放的內(nèi)存都是以頁為單位釋放的,也就不會(huì)產(chǎn)生無法給進(jìn)程使用的小內(nèi)存。
如果內(nèi)存空間不夠,操作系統(tǒng)會(huì)把其他正在運(yùn)行的進(jìn)程中的「最近沒被使用」的內(nèi)存頁面給釋放掉,也就是暫時(shí)寫在硬盤上,稱為換出(Swap Out)。一旦需要的時(shí)候,再加載進(jìn)來,稱為換入(Swap In)。所以,一次性寫入磁盤的也只有少數(shù)的一個(gè)頁或者幾個(gè)頁,不會(huì)花太多時(shí)間,內(nèi)存交換的效率就相對比較高。
更進(jìn)一步地,分頁的方式使得我們在加載程序的時(shí)候,不再需要一次性都把程序加載到物理內(nèi)存中。我們完全可以在進(jìn)行虛擬內(nèi)存和物理內(nèi)存的頁之間的映射之后,并不真的把頁加載到物理內(nèi)存里,而是只有在程序運(yùn)行中,需要用到對應(yīng)虛擬內(nèi)存頁里面的指令和數(shù)據(jù)時(shí),再加載到物理內(nèi)存里面去。
分頁機(jī)制下,虛擬地址和物理地址是如何映射的?
在分頁機(jī)制下,虛擬地址分為兩部分,頁號(hào)和頁內(nèi)偏移。頁號(hào)作為頁表的索引,頁表包含物理頁每頁所在物理內(nèi)存的基地址,這個(gè)基地址與頁內(nèi)偏移的組合就形成了物理內(nèi)存地址,見下圖。
總結(jié)一下,對于一個(gè)內(nèi)存地址轉(zhuǎn)換,其實(shí)就是這樣三個(gè)步驟:
把虛擬內(nèi)存地址,切分成頁號(hào)和偏移量;
根據(jù)頁號(hào),從頁表里面,查詢對應(yīng)的物理頁號(hào);
直接拿物理頁號(hào),加上前面的偏移量,就得到了物理內(nèi)存地址。
下面舉個(gè)例子,虛擬內(nèi)存中的頁通過頁表映射為了物理內(nèi)存中的頁,如下圖:
簡單分頁
簡單的分頁有什么缺陷呢?
有空間上的缺陷。
因?yàn)椴僮飨到y(tǒng)是可以同時(shí)運(yùn)行非常多的進(jìn)程的,那這不就意味著頁表會(huì)非常的龐大。
在 32 位的環(huán)境下,虛擬地址空間共有 4GB,假設(shè)一個(gè)頁的大小是 4KB(2^12),那么就需要大約 100 萬 (2^20) 個(gè)頁,每個(gè)「頁表項(xiàng)」需要 4 個(gè)字節(jié)大小來存儲(chǔ),那么整個(gè) 4GB 空間的映射就需要有 4MB 的內(nèi)存來存儲(chǔ)頁表。
這 4MB 大小的頁表,看起來也不是很大。但是要知道每個(gè)進(jìn)程都是有自己的虛擬地址空間的,也就說都有自己的頁表。
那么,100 個(gè)進(jìn)程的話,就需要 400MB 的內(nèi)存來存儲(chǔ)頁表,這是非常大的內(nèi)存了,更別說 64 位的環(huán)境了。
多級(jí)頁表
要解決上面的問題,就需要采用的是一種叫作多級(jí)頁表(Multi-Level Page Table)的解決方案。
在前面我們知道了,對于單頁表的實(shí)現(xiàn)方式,在 32 位和頁大小 4KB 的環(huán)境下,一個(gè)進(jìn)程的頁表需要裝下 100 多萬個(gè)「頁表項(xiàng)」,并且每個(gè)頁表項(xiàng)是占用 4 字節(jié)大小的,于是相當(dāng)于每個(gè)頁表需占用 4MB 大小的空間。
我們把這個(gè) 100 多萬個(gè)「頁表項(xiàng)」的單級(jí)頁表再分頁,將頁表(一級(jí)頁表)分為 1024 個(gè)頁表(二級(jí)頁表),每個(gè)表(二級(jí)頁表)中包含 1024 個(gè)「頁表項(xiàng)」,形成二級(jí)分頁。如下圖所示:
你可能會(huì)問,分了二級(jí)表,映射 4GB 地址空間就需要 4KB(一級(jí)頁表)+ 4MB(二級(jí)頁表)的內(nèi)存,這樣占用空間不是更大了嗎?
當(dāng)然如果 4GB 的虛擬地址全部都映射到了物理內(nèi)上的,二級(jí)分頁占用空間確實(shí)是更大了,但是,我們往往不會(huì)為一個(gè)進(jìn)程分配那么多內(nèi)存。
其實(shí)我們應(yīng)該換個(gè)角度來看問題,還記得計(jì)算機(jī)組成原理里面無處不在的局部性原理么?
每個(gè)進(jìn)程都有 4GB 的虛擬地址空間,而顯然對于大多數(shù)程序來說,其使用到的空間遠(yuǎn)未達(dá)到 4GB,因?yàn)闀?huì)存在部分對應(yīng)的頁表項(xiàng)都是空的,根本沒有分配,對于已分配的頁表項(xiàng),如果存在最近一定時(shí)間未訪問的頁表,在物理內(nèi)存緊張的情況下,操作系統(tǒng)會(huì)將頁面換出到硬盤,也就是說不會(huì)占用物理內(nèi)存。
如果使用了二級(jí)分頁,一級(jí)頁表就可以覆蓋整個(gè) 4GB 虛擬地址空間,但如果某個(gè)一級(jí)頁表的頁表項(xiàng)沒有被用到,也就不需要?jiǎng)?chuàng)建這個(gè)頁表項(xiàng)對應(yīng)的二級(jí)頁表了,即可以在需要時(shí)才創(chuàng)建二級(jí)頁表。做個(gè)簡單的計(jì)算,假設(shè)只有 20% 的一級(jí)頁表項(xiàng)被用到了,那么頁表占用的內(nèi)存空間就只有 4KB(一級(jí)頁表) + 20% * 4MB(二級(jí)頁表)= 0.804MB
,這對比單級(jí)頁表的 4MB 是不是一個(gè)巨大的節(jié)約?
那么為什么不分級(jí)的頁表就做不到這樣節(jié)約內(nèi)存呢?我們從頁表的性質(zhì)來看,保存在內(nèi)存中的頁表承擔(dān)的職責(zé)是將虛擬地址翻譯成物理地址。假如虛擬地址在頁表中找不到對應(yīng)的頁表項(xiàng),計(jì)算機(jī)系統(tǒng)就不能工作了。所以頁表一定要覆蓋全部虛擬地址空間,不分級(jí)的頁表就需要有 100 多萬個(gè)頁表項(xiàng)來映射,而二級(jí)分頁則只需要 1024 個(gè)頁表項(xiàng)(此時(shí)一級(jí)頁表覆蓋到了全部虛擬地址空間,二級(jí)頁表在需要時(shí)創(chuàng)建)。
我們把二級(jí)分頁再推廣到多級(jí)頁表,就會(huì)發(fā)現(xiàn)頁表占用的內(nèi)存空間更少了,這一切都要?dú)w功于對局部性原理的充分應(yīng)用。
對于 64 位的系統(tǒng),兩級(jí)分頁肯定不夠了,就變成了四級(jí)目錄,分別是:
全局頁目錄項(xiàng) PGD(Page Global Directory);
上層頁目錄項(xiàng) PUD(Page Upper Directory);
中間頁目錄項(xiàng) PMD(Page Middle Directory);
頁表項(xiàng) PTE(Page Table Entry);
頁表緩存TLB(Translation Lookaside Buffer)
多級(jí)頁表雖然解決了空間上的問題,但是虛擬地址到物理地址的轉(zhuǎn)換就多了幾道轉(zhuǎn)換的工序,這顯然就降低了這倆地址轉(zhuǎn)換的速度,也就是帶來了時(shí)間上的開銷。
程序是有局部性的,即在一段時(shí)間內(nèi),整個(gè)程序的執(zhí)行僅限于程序中的某一部分。相應(yīng)地,執(zhí)行所訪問的存儲(chǔ)空間也局限于某個(gè)內(nèi)存區(qū)域。
我們就可以利用這一特性,把最常訪問的幾個(gè)頁表項(xiàng)存儲(chǔ)到訪問速度更快的硬件,于是計(jì)算機(jī)科學(xué)家們,就在 CPU 芯片中,加入了一個(gè)專門存放程序最常訪問的頁表項(xiàng)的 Cache,這個(gè) Cache 就是 TLB(Translation Lookaside Buffer) ,通常稱為頁表緩存、轉(zhuǎn)址旁路緩存、快表等。
在 CPU 芯片里面,封裝了內(nèi)存管理單元(Memory Management Unit)芯片,它用來完成地址轉(zhuǎn)換和 TLB 的訪問與交互。
有了 TLB 后,那么 CPU 在尋址時(shí),會(huì)先查 TLB,如果沒找到,才會(huì)繼續(xù)查常規(guī)的頁表。
TLB 的命中率其實(shí)是很高的,因?yàn)槌绦蜃畛TL問的頁就那么幾個(gè)。
Linux內(nèi)存管理
邏輯地址和線性地址:
程序所使用的地址,通常是沒被段式內(nèi)存管理映射的地址,稱為邏輯地址;
通過段式內(nèi)存管理映射的地址,稱為線性地址,也叫虛擬地址;
邏輯地址是「段式內(nèi)存管理」轉(zhuǎn)換前的地址,線性地址則是「頁式內(nèi)存管理」轉(zhuǎn)換前的地址。
Linux 內(nèi)存主要采用的是頁式內(nèi)存管理,但同時(shí)也不可避免地涉及了段機(jī)制。
這主要是上面 Intel 處理器發(fā)展歷史導(dǎo)致的,因?yàn)?Intel X86 CPU 一律對程序中使用的地址先進(jìn)行段式映射,然后才能進(jìn)行頁式映射。既然 CPU 的硬件結(jié)構(gòu)是這樣,Linux 內(nèi)核也只好服從 Intel 的選擇。
但是事實(shí)上,Linux 內(nèi)核所采取的辦法是使段式映射的過程實(shí)際上不起什么作用。也就是說,“上有政策,下有對策”,若惹不起就躲著走。
Linux 系統(tǒng)中的每個(gè)段都是從 0 地址開始的整個(gè) 4GB 虛擬空間(32 位環(huán)境下),也就是所有的段的起始地址都是一樣的。這意味著,Linux 系統(tǒng)中的代碼,包括操作系統(tǒng)本身的代碼和應(yīng)用程序代碼,所面對的地址空間都是線性地址空間(虛擬地址),這種做法相當(dāng)于屏蔽了處理器中的邏輯地址概念,段只被用于訪問控制和內(nèi)存保護(hù)。
我們再來瞧一瞧,Linux 的虛擬地址空間是如何分布的?
在 Linux 操作系統(tǒng)中,虛擬地址空間的內(nèi)部又被分為內(nèi)核空間和用戶空間兩部分,不同位數(shù)的系統(tǒng),地址空間的范圍也不同。比如最常見的 32 位和 64 位系統(tǒng),如下所示:
通過這里可以看出:
32 位系統(tǒng)的內(nèi)核空間占用 1G,位于最高處,剩下的 3G 是用戶空間;
64 位系統(tǒng)的內(nèi)核空間和用戶空間都是 128T,分別占據(jù)整個(gè)內(nèi)存空間的最高和最低處,剩下的中間部分是未定義的。
再來說說,內(nèi)核空間與用戶空間的區(qū)別:
進(jìn)程在用戶態(tài)時(shí),只能訪問用戶空間內(nèi)存;
只有進(jìn)入內(nèi)核態(tài)后,才可以訪問內(nèi)核空間的內(nèi)存;
雖然每個(gè)進(jìn)程都各自有獨(dú)立的虛擬內(nèi)存,但是每個(gè)虛擬內(nèi)存中的內(nèi)核地址,其實(shí)關(guān)聯(lián)的都是相同的物理內(nèi)存。這樣,進(jìn)程切換到內(nèi)核態(tài)后,就可以很方便地訪問內(nèi)核空間內(nèi)存。
接下來,進(jìn)一步了解虛擬空間的劃分情況,用戶空間和內(nèi)核空間劃分的方式是不同的,內(nèi)核空間的分布情況就不多說了。
我們看看用戶空間分布的情況,以 32 位系統(tǒng)為例,我畫了一張圖來表示它們的關(guān)系:
通過這張圖你可以看到,用戶空間內(nèi)存,從低到高分別是 7 種不同的內(nèi)存段:
程序文件段,包括二進(jìn)制可執(zhí)行代碼;
已初始化數(shù)據(jù)段,包括靜態(tài)常量;
未初始化數(shù)據(jù)段,包括未初始化的靜態(tài)變量;
堆段,包括動(dòng)態(tài)分配的內(nèi)存,從低地址開始向上增長;
文件映射段,包括動(dòng)態(tài)庫、共享內(nèi)存等,從低地址開始向上增長(跟硬件和內(nèi)核版本有關(guān))
棧段,包括局部變量和函數(shù)調(diào)用的上下文等。棧的大小是固定的,一般是 8 MB。當(dāng)然系統(tǒng)也提供了參數(shù),以便我們自定義大??;
在這 7 個(gè)內(nèi)存段中,堆和文件映射段的內(nèi)存是動(dòng)態(tài)分配的。比如說,使用 C 標(biāo)準(zhǔn)庫的 malloc() 或者 mmap() ,就可以分別在堆和文件映射段動(dòng)態(tài)分配內(nèi)存。
Linux 單片機(jī) 虛擬化
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。