內存 “舞臺” 上,進程如何 “翩翩起舞”?
在數字世界里,計算機的每一次高效運轉都離不開內存與進程的默契配合。內存,恰似一座宏大且有序的舞臺,為進程提供了施展拳腳的空間。而進程,則如同舞臺上的舞者,它們在內存的舞臺上,遵循著一套復雜而精妙的規則,靈動地 “翩翩起舞”。
從啟動程序的那一刻起,進程便踏上了這片舞臺,開始了它與內存的互動之旅。這一過程,關乎計算機系統的高效穩定運行,也與我們日常使用的各類軟件、應用的流暢體驗緊密相連。接下來,就讓我們一同揭開進程在內存舞臺上的精彩 “舞步”,探尋它們背后的神秘機制 。
一、內存相關概述
在深入了解進程與內存的關系之前,我們先來認識一下內存這個計算機的關鍵部件。內存,也被稱為內存儲器或主存儲器,它就像是計算機的 “臨時倉庫”,在計算機運行程序時扮演著至關重要的角色。
內存是計算機中重要的部件之一,它是與CPU進行溝通的橋梁。計算機中所有程序的運行都是在內存中進行的,因此內存的性能對計算機的影響非常大。內存(Memory)也被稱為內存儲器,其作用是用于暫時存放CPU中的運算數據,以及與硬盤等外部存儲器交換的數據。只要計算機在運行中,CPU就會把需要運算的數據調到內存中進行運算,當運算完成后CPU再將結果傳送出來,內存的運行也決定了計算機的穩定運行。
內存又稱主存,是CPU能直接尋址的存儲空間,由半導體器件制成。內存的特點是存取速率快。內存是電腦中的主要部件,它是相對于外存而言的。我們平常使用的程序,如Windows操作系統、打字軟件、游戲軟件等,一般都是安裝在硬盤等外存上的,但僅此是不能使用其功能的,必須把它們調入內存中運行,才能真正使用其功能,我們平時輸入一段文字,或玩一個游戲,其實都是在內存中進行的。就好比在一個書房里,存放書籍的書架和書柜相當于電腦的外存,而我們工作的辦公桌就是內存。通常我們把要永久保存的、大量的數據存儲在外存上,而把一些臨時的或少量的數據和程序放在內存上,當然內存的好壞會直接影響電腦的運行速度。
內存就是暫時存儲程序以及數據的地方,比如當我們在使用WPS處理文稿時,當你在鍵盤上敲入字符時,它就被存入內存中,當你選擇存盤時,內存中的數據才會被存入硬(磁)盤。
內存一般采用半導體存儲單元,包括隨機存儲器(RAM),只讀存儲器(ROM),以及高速緩存(CACHE)。只不過因為RAM是其中最重要的存儲器。(synchronous)SDRAM同步動態隨機存取存儲器:SDRAM為168腳,這是目前PENTIUM及以上機型使用的內存。SDRAM將CPU與RAM通過一個相同的時鐘鎖在一起,使CPU和RAM能夠共享一個時鐘周期,以相同的速度同步工作,每一個時鐘脈沖的上升沿便開始傳遞數據,速度比EDO內存提高50%。DDR(DOUBLE DATA RATE)RAM :SDRAM的更新換代產品,他允許在時鐘脈沖的上升沿和下降沿傳輸數據,這樣不需要提高時鐘的頻率就能加倍提高SDRAM的速度。
當我們打開電腦上的某個程序,比如一款圖像處理軟件,這個程序的代碼和運行時所需的數據并不會直接從硬盤讀取然后被 CPU 處理。因為硬盤的讀取速度相對較慢,如果 CPU 直接從硬盤讀取數據,計算機的運行效率會極其低下。這時,內存就發揮了關鍵的 “中介” 作用。在程序啟動時,操作系統會將程序的代碼和初始數據從硬盤加載到內存中。內存的讀取速度比硬盤快得多,通常是硬盤的幾十倍甚至上百倍 ,這使得 CPU 能夠快速地從內存中讀取指令和數據進行處理,大大提高了計算機的運行速度。在圖像處理過程中,當我們對圖片進行裁剪、調色等操作時,相關的數據會在內存中被快速地讀取和修改,處理結果也會暫時存儲在內存中,直到我們保存圖像時,數據才會被寫入硬盤進行長期存儲。內存就像是一個高速運轉的中轉站,協調著 CPU 與硬盤之間的數據傳輸,讓計算機能夠高效地完成各種復雜的任務。
內存的內部是由各種IC電路組成的,它的種類很龐大,但是其主要分為三種存儲器:
- 只讀存儲器(ROM):ROM表示只讀存儲器(Read Only Memory),在制造ROM的時候,信息(數據或程序)就被存入并永久保存。這些信息只能讀出,一般不能寫入,即使機器停電,這些數據也不會丟失。ROM一般用于存放計算機的基本程序和數據,如BIOS ROM。其物理外形一般是雙列直插式(DIP)的集成塊。
- 隨機存儲器(RAM):隨機存儲器(Random Access Memory)表示既可以從中讀取數據,也可以寫入數據。當機器電源關閉時,存于其中的數據就會丟失。我們通常購買或升級的內存條就是用作電腦的內存,內存條(SIMM)就是將RAM集成塊集中在一起的一小塊電路板,它插在計算機中的內存插槽上,以減少RAM集成塊占用的空間。目前市場上常見的內存條有1G/條,2G/條,4G/條等。
- 高速緩沖存儲器(Cache):Cache也是我們經常遇到的概念,也就是平常看到的一級緩存(L1 Cache)、二級緩存(L2 Cache)、三級緩存(L3 Cache)這些數據,它位于CPU與內存之間,是一個讀寫速度比內存更快的存儲器。當CPU向內存中寫入或讀出數據時,這個數據也被存儲進高速緩沖存儲器中。當CPU再次需要這些數據時,CPU就從高速緩沖存儲器讀取數據,而不是訪問較慢的內存,當然,如需要的數據在Cache中沒有,CPU會再去讀取內存中的數據。
二、虛擬內存技術
當進程在內存這個 “臨時倉庫” 中運行時,虛擬內存則是背后的 “魔法”,讓進程能夠高效地使用內存資源。
2.1為什么需要使用虛擬內存
進程需要使用的代碼和數據都放在內存中,比放在外存中要快很多。問題是內存空間太小了,不能滿足進程的需求,而且現在都是多進程,情況更加糟糕。所以提出了虛擬內存,使得每個進程用于3G的獨立用戶內存空間和共享的1G內核內存空間。(每個進程都有自己的頁表,才使得3G用戶空間的獨立)這樣進程運行的速度必然很快了。而且虛擬內存機制還解決了內存碎片和內存不連續的問題。為什么可以在有限的物理內存上達到這樣的效果呢?
2.2虛擬內存詳解
虛擬內存是計算機系統內存管理的一種技術,它就像是給應用程序戴上了一副 “魔法眼鏡”,讓應用程序以為自己擁有連續可用的內存,即一個連續完整的地址空間 。但實際上,這些內存通常被分隔成多個物理內存碎片,還有部分暫時存儲在外部磁盤存儲器上。當計算機缺少運行某些程序所需的物理內存時,操作系統就會使用硬盤上的虛擬內存進行替代。這就好比你有一個小書架(物理內存),放不下所有的書(程序數據),于是你在旁邊放了一個大箱子(虛擬內存),把暫時不看的書放在箱子里,等需要的時候再拿出來。在 32 位的操作系統中,每個進程可以擁有 4GB 的虛擬內存空間,但實際的物理內存可能遠遠小于這個數字。
例如:對于程序計數器位數為32位的處理器來說,他的地址發生器所能發出的地址數目為2^32=4G個,于是這個處理器所能訪問的最大內存空間就是4G。在計算機技術中,這個值就叫做處理器的尋址空間或尋址能力。
照理說,為了充分利用處理器的尋址空間,就應按照處理器的最大尋址來為其分配系統的內存。如果處理器具有32位程序計數器,那么就應該按照下圖的方式,為其配備4G的內存:
圖片
這樣,處理器所發出的每一個地址都會有一個真實的物理存儲單元與之對應;同時,每一個物理存儲單元都有唯一的地址與之對應。這顯然是一種最理想的情況。
但遺憾的是,實際上計算機所配置內存的實際空間常常小于處理器的尋址范圍,這是就會因處理器的一部分尋址空間沒有對應的物理存儲單元,從而導致處理器尋址能力的浪費。例如:如下圖的系統中,具有32位尋址能力的處理器只配置了256M的內存儲器,這就會造成大量的浪費:
圖片
另外,還有一些處理器因外部地址線的根數小于處理器程序計數器的位數,而使地址總線的根數不滿足處理器的尋址范圍,從而處理器的其余尋址能力也就被浪費了。例如:Intel8086處理器的程序計數器位32位,而處理器芯片的外部地址總線只有20根,所以它所能配置的最大內存為1MB:
圖片
在實際的應用中,如果需要運行的應用程序比較小,所需內存容量小于計算機實際所配置的內存空間,自然不會出什么問題。但是,目前很多的應用程序都比較大,計算機實際所配置的內存空間無法滿足。
實踐和研究都證明:一個應用程序總是逐段被運行的,而且在一段時間內會穩定運行在某一段程序里。
這也就出現了一個方法:如下圖所示,把要運行的那一段程序自輔存復制到內存中來運行,而其他暫時不運行的程序段就讓它仍然留在輔存。
圖片
當需要執行另一端尚未在內存的程序段(如程序段2),如下圖所示,就可以把內存中程序段1的副本復制回輔存,在內存騰出必要的空間后,再把輔存中的程序段2復制到內存空間來執行即可:
圖片
在計算機技術中,把內存中的程序段復制回輔存的做法叫做“換出”,而把輔存中程序段映射到內存的做法叫做“換入”。經過不斷有目的的換入和換出,處理器就可以運行一個大于實際物理內存的應用程序了。或者說,處理器似乎是擁有了一個大于實際物理內存的內存空間。于是,這個存儲空間叫做虛擬內存空間,而把真正的內存叫做實際物理內存,或簡稱為物理內存。
那么對于一臺真實的計算機來說,它的虛擬內存空間又有多大呢?計算機虛擬內存空間的大小是由程序計數器的尋址能力來決定的。例如:在程序計數器的位數為32的處理器中,它的虛擬內存空間就為4GB。
可見,如果一個系統采用了虛擬內存技術,那么它就存在著兩個內存空間:虛擬內存空間和物理內存空間。虛擬內存空間中的地址叫做“虛擬地址”;而實際物理內存空間中的地址叫做“實際物理地址”或“物理地址”。處理器運算器和應用程序設計人員看到的只是虛擬內存空間和虛擬地址,而處理器片外的地址總線看到的只是物理地址空間和物理地址。
由于存在兩個內存地址,因此一個應用程序從編寫到被執行,需要進行兩次映射。第一次是映射到虛擬內存空間,第二次時映射到物理內存空間。在計算機系統中,第兩次映射的工作是由硬件和軟件共同來完成的。承擔這個任務的硬件部分叫做存儲管理單元MMU,軟件部分就是操作系統的內存管理模塊了。
在映射工作中,為了記錄程序段占用物理內存的情況,操作系統的內存管理模塊需要建立一個表格,該表格以虛擬地址為索引,記錄了程序段所占用的物理內存的物理地址。這個虛擬地址/物理地址記錄表便是存儲管理單元MMU把虛擬地址轉化為實際物理地址的依據,記錄表與存儲管理單元MMU的作用如下圖所示:
圖片
綜上所述,虛擬內存技術的實現,是建立在應用程序可以分成段,并且具有“在任何時候正在使用的信息總是所有存儲信息的一小部分”的局部特性基礎上的。它是通過用輔存空間模擬RAM來實現的一種使機器的作業地址空間大于實際內存的技術。
從處理器運算裝置和程序設計人員的角度來看,它面對的是一個用MMU、映射記錄表和物理內存封裝起來的一個虛擬內存空間,這個存儲空間的大小取決于處理器程序計數器的尋址空間。
可見,程序映射表是實現虛擬內存的技術關鍵,它可給系統帶來如下特點:
- 系統中每一個程序各自都有一個大小與處理器尋址空間相等的虛擬內存空間;
- 在一個具體時刻,處理器只能使用其中一個程序的映射記錄表,因此它只看到多個程序虛存空間中的一個,這樣就保證了各個程序的虛存空間時互不相擾、各自獨立的;
- 使用程序映射表可方便地實現物理內存的共享。
2.3虛擬地址空間布局
Linux 內核給每個進程都提供了一個獨立的虛擬地址空間,并且這個地址空間是連續的。這樣,進程就可以很方便地訪問內存,更確切地說是訪問虛擬內存。
虛擬地址空間的內部又被分為內核空間和用戶空間兩部分,不同字長(也就是單個CPU指令可以處理數據的最大長度)的處理器,地址空間的范圍也不同。比如最常見的 32 位和 64 位系統,它們的虛擬地址空間,如下所示:
圖片
通過這里可以看出,32位系統的內核空間占用 1G,位于最高處,剩下的3G是用戶空間。而 64 位系統的內核空間和用戶空間都是 128T,分別占據整個內存空間的最高和最低處,剩下的中間部分是未定義的。
進程在用戶態時,只能訪問用戶空間內存;只有進入內核態后,才可以訪問內核空間內存。雖然每個進程的地址空間都包含了內核空間,但這些內核空間,其實關聯的都是相同的物理內存。這樣,進程切換到內核態后,就可以很方便地訪問內核空間內存。
既然每個進程都有一個這么大的地址空間,那么所有進程的虛擬內存加起來,自然要比實際的物理內存大得多。所以,并不是所有的虛擬內存都會分配物理內存,只有那些實際使用的虛擬內存才分配物理內存,并且分配后的物理內存,是通過內存映射來管理的;內存映射,其實就是將虛擬內存地址映射到物理內存地址。為了完成內存映射,內核為每個進程都維護了一張頁表,記錄虛擬地址與物理地址的映射關系,如下圖所示:
圖片
頁表實際上存儲在 CPU 的內存管理單元 MMU中,這樣,正常情況下,處理器就可以直接通過硬件,找出要訪問的內存;而當進程訪問的虛擬地址在頁表中查不到時,系統會產生一個缺頁異常,進入內核空間分配物理內存、更新進程頁表,最后再返回用戶空間,恢復進程的運行。
另外,TLB(Translation Lookaside Buffer,轉譯后備緩沖器)會影響 CPU 的內存訪問性能,TLB 其實就是 MMU 中頁表的高速緩存。由于進程的虛擬地址空間是獨立的,而 TLB 的訪問速度又比 MMU 快得多,所以,通過減少進程的上下文切換,減少TLB的刷新次數,就可以提高TLB 緩存的使用率,進而提高CPU的內存訪問性能;不過要注意,MMU 并不以字節為單位來管理內存,而是規定了一個內存映射的最小單位,也就是頁,通常是 4 KB大小。這樣,每一次內存映射,都需要關聯 4 KB 或者 4KB 整數倍的內存空間。
頁的大小只有4 KB ,導致的另一個問題就是,整個頁表會變得非常大。比方說,僅 32 位系統就需要 100 多萬個頁表項(4GB/4KB),才可以實現整個地址空間的映射。為了解決頁表項過多的問題,Linux 提供了兩種機制,也就是多級頁表和大頁(HugePage)。
多級頁表就是把內存分成區塊來管理,將原來的映射關系改成區塊索引和區塊內的偏移。由于虛擬內存空間通常只用了很少一部分,那么,多級頁表就只保存這些使用中的區塊,這樣就可以大大地減少頁表的項數。
Linux用的正是四級頁表來管理內存頁,如下圖所示,虛擬地址被分為5個部分,前4個表項用于選擇頁,而最后一個索引表示頁內偏移。
圖片
大頁,就是比普通頁更大的內存塊,常見的大小有 2MB 和 1GB。大頁通常用在使用大量內存的進程上,比如Oracle、DPDK等。
通過這些機制,在頁表的映射下,進程就可以通過虛擬地址來訪問物理內存了。
以 Linux 23位系統為例,進程的虛擬地址空間布局從低到高主要包含以下幾個部分:
- LOAD Segments:這部分包含了代碼段(.text)、數據段(.data)和 BSS 段等。代碼段存儲著 CPU 執行的機器指令,它是只讀的,防止指令被其他程序修改 。數據段用于存儲初始化的全局變量和靜態變量,BSS 段則用來存放未初始化的全局變量和靜態變量。
- 堆(Heap):堆是用于保存程序運行時動態申請的內存空間的區域,比如使用malloc或new申請的內存空間就來自堆 。堆的地址空間是 “向上增加” 的,即當堆上保存的數據越多,堆的地址就越高。
- 共享庫數據:很多進程會共享一些庫文件,這些共享庫的數據就存儲在這里。通過共享庫,多個進程可以共享相同的代碼和數據,節省內存空間。
- 棧(Stack):棧主要用于保存函數的局部變量(不包括static聲明的靜態變量,靜態變量存放在數據段或 BSS 段)、參數、返回值、函數返回地址以及調用者環境信息(如寄存器值)等 。棧的內存由系統進行管理,在函數完成執行后,系統會自行釋放棧區內存,不需要用戶手動管理。整個程序的棧區大小可以由用戶自行設定,Windows 默認的棧區大小為 1M ,64 位的 Linux 默認棧大小為 10MB。
- 內核數據:這是操作系統內核使用的內存區域,用戶進程一般不能直接訪問。它包含了內核代碼、內核數據結構以及一些系統調用的相關信息。
圖片
通過這張圖可以看到,用戶空間內存,從低到高分別是五種不同的內存段:
- 只讀段,包括代碼和常量等。
- 數據段,包括全局變量等。
- 堆,包括動態分配的內存,從低地址開始向上增長。
- 文件映射段,包括動態庫、共享內存等,從高地址開始向下增長。
- 棧,包括局部變量和函數調用的上下文等。棧的大小是固定的,一般是 8 MB。
在這五個內存段中,堆和文件映射段的內存是動態分配的。比如說,使用 C 標準庫的 malloc() 或者 mmap() ,就可以分別在堆和文件映射段動態分配內存;其實64位系統的內存分布也類似,只不過內存空間要大得多。
2.4虛擬內存使用方式
進程在啟動時,操作系統會為其分配虛擬內存空間,并建立虛擬地址到物理地址的映射關系。在進程運行過程中,當需要訪問內存時,CPU 會生成虛擬地址,這個虛擬地址會經過內存管理單元(MMU)的轉換,找到對應的物理地址,然后訪問物理內存。如果所需的內存頁不在物理內存中,就會發生缺頁中斷,操作系統會從磁盤的虛擬內存中讀取相應的內存頁到物理內存中,并更新映射關系。
在 Linux 系統中,進程可以通過mmap、sbrk和brk等函數來操作虛擬內存。mmap函數用于將一個文件或者其它對象映射進內存 ,比如將共享庫映射到進程的虛擬地址空間中。sbrk和brk函數則用于改變進程數據段的大小,從而實現內存的動態分配和釋放。當malloc分配小于 128k 的內存時,會使用brk分配內存,將數據段的最高地址指針往高地址推;當malloc分配大于 128k 的內存時,會使用mmap在堆和棧之間找一塊空閑內存分配 。
malloc() 是 C 標準庫提供的內存分配函數,對應到系統調用上,有兩種實現方式,即 brk() 和 mmap()。
- 對小塊內存(小于128K),C 標準庫使用 brk() 來分配,也就是通過移動堆頂的位置來分配內存。這些內存釋放后并不會立刻歸還系統,而是被緩存起來,這樣就可以重復使用。
- 而大塊內存(大于 128K),則直接使用內存映射 mmap() 來分配,也就是在文件映射段找一塊空閑內存分配出去。
這兩種方式,自然各有優缺點:
- brk() 方式的緩存,可以減少缺頁異常的發生,提高內存訪問效率。不過,由于這些內存沒有歸還系統,在內存工作繁忙時,頻繁的內存分配和釋放會造成內存碎片。
- mmap() 方式分配的內存,會在釋放時直接歸還系統,所以每次 mmap 都會發生缺頁異常。在內存工作繁忙時,頻繁的內存分配會導致大量的缺頁異常,使內核的管理負擔增大。這也是malloc 只對大塊內存使用 mmap 的原因。
了解這兩種調用方式后,還需要清楚一點,那就是,當這兩種調用發生后,其實并沒有真正分配內存。這些內存,都只在首次訪問時才分配,也就是通過缺頁異常進入內核中,再由內核來分配內存。
整體來說,Linux 使用伙伴系統來管理內存分配。這些內存在MMU中以頁為單位進行管理,伙伴系統也一樣,以頁為單位來管理內存,并且會通過相鄰頁的合并,減少內存碎片化(比如brk方式造成的內存碎片);在用戶空間,malloc 通過 brk() 分配的內存,在釋放時并不立即歸還系統,而是緩存起來重復利用;在內核空間,Linux 則通過 slab 分配器來管理小內存。可以把slab 看成構建在伙伴系統上的一個緩存,主要作用就是分配并釋放內核中的小對象。
對內存來說,如果只分配而不釋放,就會造成內存泄漏,甚至會耗盡系統內存。所以,在應用程序用完內存后,還需要調用 free() 或 unmap(),來釋放這些不用的內存。
當然,系統也不會任由某個進程用完所有內存。在發現內存緊張時,系統就會通過一系列機制來回收內存,比如下面這三種方式:
- 回收緩存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的內存頁面;
- 回收不常訪問的內存,把不常用的內存通過交換分區直接寫到磁盤中;
- 殺死進程,內存緊張時系統還會通過 OOM(Out of Memory),直接殺掉占用大量內存的進程。
其中,第二種方式回收不常訪問的內存時,會用到交換分區(以下簡稱 Swap)。Swap 其實就是把一塊磁盤空間當成內存來用。它可以把進程暫時不用的數據存儲到磁盤中(這個過程稱為換出),當進程訪問這些內存時,再從磁盤讀取這些數據到內存中(這個過程稱為換入)。
所以,可以發現,Swap 把系統的可用內存變大了。不過要注意,通常只在內存不足時,才會發生 Swap 交換。并且由于磁盤讀寫的速度遠比內存慢,Swap 會導致嚴重的內存性能問題。
第三種方式提到的 OOM(Out of Memory),其實是內核的一種保護機制。它監控進程的內存使用情況,并且使用 oom_score 為每個進程的內存使用情況進行評分:
- 一個進程消耗的內存越大,oom_score 就越大;
- 一個進程運行占用的 CPU 越多,oom_score 就越小。
這樣,進程的 oom_score 越大,代表消耗的內存越多,也就越容易被 OOM 殺死,從而可以更好保護系統。
當然,為了實際工作的需要,管理員可以通過 /proc 文件系統,手動設置進程的 oom_adj ,從而調整進程的 oom_score;oom_adj 的范圍是 [-17, 15],數值越大,表示進程越容易被 OOM 殺死;數值越小,表示進程越不容易被 OOM 殺死,其中 -17 表示禁止OOM。
比如用下面的命令,就可以把 sshd 進程的 oom_adj 調小為 -16,這樣, sshd 進程就不容易被 OOM 殺死。
1 echo -16 > /proc/$(pidof sshd)/oom_adj
三、進程與內存的交互舞步
3.1進程啟動時的內存加載
當我們啟動一個進程時,操作系統就像是一個忙碌的 “搬運工”,開始了一系列復雜而有序的內存加載工作。以 Windows系統為例,當我們雙擊一個.exe 可執行文件時,操作系統首先會讀取該文件的頭部信息,這個頭部信息就像是一個 “導航圖”,包含了程序運行所需的各種關鍵信息,如程序的入口點、依賴的動態鏈接庫(DLL)等。
操作系統會為進程分配虛擬內存空間,這個空間就像是一個 “虛擬舞臺”,進程將在上面進行各種操作 。然后,操作系統會根據可執行文件頭部的信息,將程序的主要代碼段和初始化數據加載到虛擬內存的相應位置。這些代碼段和數據是程序啟動和運行的基礎,就像是一場演出的核心演員和基本道具。在加載代碼段時,CPU 會讀取其中的指令,開始執行程序的初始化工作,比如初始化全局變量、設置程序的運行環境等。
對于依賴的動態鏈接庫,操作系統會在內存中查找是否已經加載了這些庫。如果已經加載,就直接將庫的地址映射到進程的虛擬地址空間中,讓進程可以共享這些庫的代碼和數據 ,就像多個進程可以共用同一個舞臺道具;如果沒有加載,操作系統會從磁盤中讀取相應的動態鏈接庫文件,并將其加載到內存中,然后再進行地址映射。動態鏈接庫的使用可以節省內存空間,提高程序的可維護性和可擴展性 。許多應用程序都會依賴于系統提供的一些通用的動態鏈接庫,如 Windows 系統中的 Kernel32.dll,它提供了許多基本的操作系統功能調用。
3.2運行時內存分配
在進程運行過程中,常常需要動態分配內存來存儲一些臨時數據。以 C 語言中的malloc函數為例,它是進程運行時動態內存分配的一個典型工具。當我們調用malloc函數時,它會向操作系統申請一定大小的內存空間。
在 32 位的 Linux 系統中,malloc的內存分配機制如下:當請求的內存小于 128KB 時,malloc會使用brk系統調用,通過移動堆頂指針來分配內存 。假設堆頂指針初始指向地址 0x1000,我們調用malloc(100)申請 100 字節的內存,malloc會將堆頂指針移動到 0x1064(假設系統內存對齊為 8 字節,100 字節向上取整為 104 字節,加上一些元數據,假設為 8 字節,共 112 字節,即 0x70,所以堆頂指針移動到 0x1000 + 0x70 = 0x1070),并返回 0x1008 這個地址給用戶程序,用戶程序就可以使用這塊內存來存儲數據。
當請求的內存大于等于 128KB 時,malloc會使用mmap系統調用,在堆和棧之間的內存區域中找一塊合適的空閑內存進行分配 。mmap會在虛擬地址空間中創建一個新的映射,將磁盤上的文件或者匿名內存區域映射到進程的虛擬地址空間中。這樣,進程就可以像訪問普通內存一樣訪問這個映射區域。
在動態內存分配過程中,虛擬內存的寫時復制(Copy - on - Write,COW)策略發揮了重要作用。當一個進程通過fork系統調用創建子進程時,子進程會共享父進程的內存頁面。在子進程或父進程沒有對這些共享頁面進行寫操作之前,它們實際上共享的是相同的物理內存頁面 ,只有當其中一個進程試圖對共享頁面進行寫操作時,操作系統才會為寫操作的進程復制一份物理內存頁面,使得父子進程擁有各自獨立的物理內存頁面,這樣可以節省內存資源,提高系統的效率。
如果進程訪問的內存頁面不在物理內存中,就會發生缺頁中斷。操作系統會根據頁表信息,從磁盤的虛擬內存中找到對應的頁面,并將其加載到物理內存中 。然后,操作系統會更新頁表,將虛擬地址與新加載的物理內存頁面建立映射關系,使得進程能夠繼續訪問該內存頁面。
3.3內存回收與管理
當進程結束運行或者內存不足時,操作系統就會進行內存回收工作,以釋放內存空間供其他進程使用。當一個進程結束時,操作系統會回收該進程所占用的所有虛擬內存空間,并將這些空間標記為空閑 。操作系統會檢查進程使用的堆內存、棧內存以及其他動態分配的內存區域,將這些內存歸還給內存管理系統,就像是一場演出結束后,工作人員會將舞臺上的道具和設備清理干凈,為下一場演出做準備。
在內存不足的情況下,操作系統會采用內存置換算法來決定哪些內存頁面可以被暫時置換到磁盤上,以騰出物理內存空間。常見的內存置換算法有最近最少使用(LRU,Least Recently Used)算法 。LRU 算法的核心思想是,如果一個內存頁面在最近一段時間內沒有被訪問過,那么它在未來被訪問的可能性也較小,因此可以將其置換出去。操作系統會維護一個內存頁面的訪問時間記錄,當需要置換頁面時,選擇訪問時間最早的頁面進行置換。假設內存中有三個頁面 A、B、C,它們的訪問時間依次為 10:00、10:10、10:20,當內存不足需要置換頁面時,LRU 算法會選擇頁面 A 進行置換,因為它是最久沒有被訪問的頁面。
除了 LRU 算法,還有先進先出(FIFO,First In First Out)算法,它是將最早進入內存的頁面置換出去;時鐘(Clock)算法,它是 LRU 算法的一種近似實現,通過一個循環鏈表和一個訪問位來模擬 LRU 算法的行為 。這些算法各有優缺點,操作系統會根據具體的應用場景和系統需求選擇合適的算法,以確保系統的內存管理高效、穩定。
四、內存管理的底層奧秘
4.1MMU與地址映射
在進程使用內存的過程中,內存管理單元(MMU,Memory Management Unit)扮演著至關重要的角色,它就像是一個精準的 “翻譯官”,負責將進程的虛擬地址動態翻譯為物理地址 。
當 CPU 需要訪問內存中的數據時,它會首先產生一個虛擬地址。這個虛擬地址會被發送到 MMU。MMU 內部包含了一個高速緩存,即轉換后備緩沖器(TLB,Translation Lookaside Buffer),以及與進程相關的頁表 。TLB 中存儲了近期使用過的虛擬地址到物理地址的映射關系,就像是一個常用詞匯的快速翻譯手冊。當 MMU 接收到虛擬地址后,會首先在 TLB 中查找對應的映射關系。如果在 TLB 中命中,MMU 可以快速地獲取到對應的物理地址,從而大大提高了地址轉換的速度 。
如果在TLB中沒有命中,MMU就需要通過進程的頁表來查找映射關系。頁表是一個存儲虛擬地址與物理地址映射關系的數據結構,它就像是一本完整的翻譯詞典 。MMU會根據虛擬地址中的頁號,在頁表中查找對應的物理頁框號。找到物理頁框號后,再結合虛擬地址中的頁內偏移量,就可以計算出最終的物理地址。在 32 位的系統中,如果頁面大小為4KB,那么虛擬地址可以被劃分為20位的頁號和12位的頁內偏移量 。假設虛擬地址為 0x00401000,其中 0x0040 是頁號,0x1000 是頁內偏移量。MMU 通過頁號 0x0040 在頁表中查找對應的物理頁框號,假設找到的物理頁框號為 0x0080,那么最終的物理地址就是 0x00801000(0x0080 << 12 | 0x1000)。
4.2頁表與多級頁表
進程的頁表是管理虛擬地址與物理地址映射關系的關鍵數據結構。它就像是一個精心編排的 “映射目錄”,每個表項都記錄了一個虛擬頁到物理頁框的映射關系 。在簡單的分頁系統中,頁表可能是一個線性的數組,數組的索引是虛擬頁號,數組的值是對應的物理頁框號 。
然而,對于32位甚至64 位的地址空間來說,如果采用簡單的線性頁表,會占用大量的內存空間。在 32 位系統中,若頁面大小為4KB,進程的虛擬地址空間為4GB,那么頁表將包含 1M 個頁表項(4GB / 4KB = 1M) 。假設每個頁表項占用 4 個字節,那么僅頁表就會占用4MB 的連續內存空間,這對于內存資源來說是一種巨大的浪費。
為了解決這個問題,現代操作系統通常采用多級頁表。以二級頁表為例,它將虛擬地址空間進一步劃分。在 32 位系統中,可能將 20 位的頁號再劃分為 10 位的外層頁號和 10 位的內層頁號 。外層頁表的每個表項指向一個內層頁表,內層頁表的表項才真正記錄虛擬頁到物理頁框的映射關系 。這樣,只有當外層頁表中對應的表項被訪問時,才會加載相應的內層頁表,大大減少了內存的占用 。假設外層頁號為 0x001,內層頁號為 0x002,通過外層頁表找到對應的內層頁表,再在內層頁表中通過內層頁號 0x002 找到物理頁框號,從而實現虛擬地址到物理地址的轉換。
多級頁表不僅減少了內存占用,還支持離散存儲。由于頁表可以離散地存儲在物理內存中,不再需要連續的內存空間來存放整個頁表,提高了內存的使用效率和靈活性 。
4.3緩存機制與局部性原理
為了進一步提高內存訪問的速度,計算機系統引入了多種緩存機制,其中 CPU 緩存、Cache 和 TLB 表起著關鍵作用,它們的工作原理都基于局部性原理。
局部性原理包括時間局部性和空間局部性。時間局部性是指如果一個數據項被訪問,那么在不久的將來它很可能再次被訪問 。在一個循環結構中,循環變量和循環體內頻繁使用的數據會被多次訪問,這就體現了時間局部性。空間局部性是指如果一個數據項被訪問,那么與它相鄰的數據項很可能也會被訪問 。當我們訪問一個數組時,通常會按照順序依次訪問數組中的元素,這就利用了空間局部性。
CPU 緩存(Cache)是位于 CPU 和主存之間的高速存儲部件,它利用了局部性原理來提高內存訪問命中率。當 CPU 需要訪問內存數據時,首先會在 Cache 中查找 。如果在 Cache 中命中,CPU 可以快速地獲取數據,因為 Cache 的訪問速度比主存快得多,通常可以達到主存訪問速度的幾十倍甚至上百倍 。如果在 Cache 中沒有命中,才會訪問主存,并將主存中的數據塊加載到 Cache 中,以便后續訪問。
TLB 表作為 MMU 中的高速緩存,同樣利用了局部性原理。它緩存了近期使用過的虛擬地址到物理地址的映射關系 。當 MMU 接收到虛擬地址時,先在 TLB 中查找映射關系,如果命中,就可以快速完成地址轉換,避免了通過頁表進行查找的開銷 。由于TLB的訪問速度極快,幾乎與 CPU 的速度同步,所以 TLB 的命中率對于地址轉換的效率至關重要 。