計算機操作系統知識集合

主要來自小林coding

硬件結構

cpu位寬

如果用 32 位 CPU 去加和兩個 64 位大小的數字,就需要把這 2 個 64 位的數字分成 2 個低位 32 位數字和 2 個高位 32 位數字來計算,先加個兩個低位的 32 位數字,算出進位,然后加和兩個高位的 32 位數字,最后再加上進位,就能算出結果了,可以發現 32 位 CPU 并不能一次性計算出加和兩個 64 位數字的結果。

對于 64 位 CPU 就可以一次性算出加和兩個 64 位數字的結果,因為 64 位 CPU 可以一次讀入 64 位的數字,并且 64 位 CPU 內部的邏輯運算單元也支持 64 位數字的計算。

但是并不代表 64 位 CPU 性能比 32 位 CPU 高很多,很少應用需要算超過 32 位的數字,所以如果計算的數額不超過 32 位數字的情況下,32 位和 64 位 CPU 之間沒什么區別的,只有當計算超過 32 位數字的情況下,64 位的優勢才能體現出來。

另外,32 位 CPU 最大只能操作 4GB 內存,就算你裝了 8 GB 內存條,也沒用。而 64 位 CPU 尋址范圍則很大,理論最大的尋址空間為 2^64。

存儲器分層

寄存器和cache

我們可以把 CPU 比喻成我們的大腦,大腦正在思考的東西,就好比 CPU 中的寄存器,處理速度是最快的,但是能存儲的數據也是最少的,畢竟我們也不能一下同時思考太多的事情,除非你練過。

我們大腦中的記憶,就好比 CPU Cache,中文稱為 CPU 高速緩存,處理速度相比寄存器慢了一點,但是能存儲的數據也稍微多了一些。

CPU Cache 通常會分為 L1、L2、L3 三層,其中 L1 Cache 通常分成「數據緩存」和「指令緩存」,L1 是距離 CPU 最近的,因此它比 L2、L3 的讀寫速度都快、存儲空間都小。我們大腦中短期記憶,就好比 L1 Cache,而長期記憶就好比 L2/L3 Cache。

CPU Cache 用的是一種叫 SRAM(Static Random-Access Memory,靜態隨機存儲器) 的芯片。

SRAM 之所以叫「靜態」存儲器,是因為只要有電,數據就可以保持存在,而一旦斷電,數據就會丟失了。

在 SRAM 里面,一個 bit 的數據,通常需要 6 個晶體管,所以 SRAM 的存儲密度不高,同樣的物理空間下,能存儲的數據是有限的,不過也因為 SRAM 的電路簡單,所以訪問速度非常快。

CPU 的高速緩存,通常可以分為 L1、L2、L3 這樣的三層高速緩存,也稱為一級緩存、二級緩存、三級緩存。

寄存器和 CPU Cache 都是在 CPU 內部,跟 CPU 挨著很近,因此它們的讀寫速度都相當的快,但是能存儲的數據很少,畢竟 CPU 就這么丁點大。
在這里插入圖片描述
寄存器的訪問速度非常快,一般要求在半個 CPU 時鐘周期內完成讀寫,CPU 時鐘周期跟 CPU 主頻息息相關,比如 2 GHz 主頻的 CPU,那么它的時鐘周期就是 1/2G,也就是 0.5ns(納秒)。

CPU 處理一條指令的時候,除了讀寫寄存器,還需要解碼指令、控制指令執行和計算。如果寄存器的速度太慢,則會拉長指令的處理周期,從而給用戶的感覺,就是電腦「很慢」。

L1 高速緩存的訪問速度幾乎和寄存器一樣快,通常只需要 2~4 個時鐘周期,而大小在幾十 KB 到幾百 KB 不等。

L2 高速緩存同樣每個 CPU 核心都有,但是 L2 高速緩存位置比 L1 高速緩存距離 CPU 核心 更遠,它大小比 L1 高速緩存更大,CPU 型號不同大小也就不同,通常大小在幾百 KB 到幾 MB 不等,訪問速度則更慢,速度在 10~20 個時鐘周期。

L3 高速緩存通常是多個 CPU 核心共用的,位置比 L2 高速緩存距離 CPU 核心 更遠,大小也會更大些,通常大小在幾 MB 到幾十 MB 不等,具體值根據 CPU 型號而定。

訪問速度相對也比較慢一些,訪問速度在 20~60個時鐘周期。

我們執行一條命令,實際上在寄存器中是這樣的:
在這里插入圖片描述

內存

內存用的芯片和 CPU Cache 有所不同,它使用的是一種叫作 DRAM (Dynamic Random Access Memory,動態隨機存取存儲器) 的芯片。

相比 SRAM,DRAM 的密度更高,功耗更低,有更大的容量,而且造價比 SRAM 芯片便宜很多。

DRAM 存儲一個 bit 數據,只需要一個晶體管和一個電容就能存儲,但是因為數據會被存儲在電容里,電容會不斷漏電,所以需要「定時刷新」電容,才能保證數據不會被丟失,這就是 DRAM 之所以被稱為「動態」存儲器的原因,只有不斷刷新,數據才能被存儲起來。

DRAM 的數據訪問電路和刷新電路都比 SRAM 更復雜,所以訪問的速度會更慢,內存速度大概在 200~300 個 時鐘周期之間。

如何寫出讓cpu跑的更快的代碼

CPU Cache 的數據是從內存中讀取過來的,它是以一小塊一小塊讀取數據的,而不是按照單個數組元素來讀取數據的,在 CPU Cache 中的,這樣一小塊一小塊的數據,稱為 Cache Line(緩存塊)。
你可以在你的 Linux 系統,用下面這種方式來查看 CPU 的 Cache Line,你可以看我服務器的 L1 Cache Line 大小是 64 字節,也就意味著 L1 Cache 一次載入數據的大小是 64 字節。
比如,有一個 int array[100] 的數組,當載入 array[0] 時,由于這個數組元素的大小在內存只占 4 字節,不足 64 字節,CPU 就會順序加載數組元素到 array[15],意味著 array[0]~array[15] 數組元素都會被緩存在 CPU Cache 中了,因此當下次訪問這些數組元素時,會直接從 CPU Cache 讀取,而不用再從內存中讀取,大大提高了 CPU 讀取數據的性能。

如何提升數據緩存的命中率?

假設要遍歷二維數組,有以下兩種形式,雖然代碼執行結果是一樣,但你覺得哪種形式效率最高呢?為什么高呢?
在這里插入圖片描述
經過測試,形式一 array[i][j] 執行時間比形式二 array[j][i] 快好幾倍。

之所以有這么大的差距,是因為二維數組 array 所占用的內存是連續的,比如長度 N 的值是 2 的話,那么內存中的數組元素的布局順序是這樣的:
在這里插入圖片描述
形式一用 array[i][j] 訪問數組元素的順序,正是和內存中數組元素存放的順序一致。當 CPU 訪問 array[0][0] 時,由于該數據不在 Cache 中,于是會「順序」把跟隨其后的 3 個元素從內存中加載到 CPU Cache,這樣當 CPU 訪問后面的 3 個數組元素時,就能在 CPU Cache 中成功地找到數據,這意味著緩存命中率很高,緩存命中的數據不需要訪問內存,這便大大提高了代碼的性能。

而如果用形式二的 array[j][i] 來訪問,則訪問的順序就是:
在這里插入圖片描述
你可以看到,訪問的方式跳躍式的,而不是順序的,那么如果 N 的數值很大,那么操作 array[j][i] 時,是沒辦法把 array[j+1][i] 也讀入到 CPU Cache 中的,既然 array[j+1][i] 沒有讀取到 CPU Cache,那么就需要從內存讀取該數據元素了。很明顯,這種不連續性、跳躍式訪問數據元素的方式,可能不能充分利用到了 CPU Cache 的特性,從而代碼的性能不高。

如何提升指令緩存的命中率?

提升數據的緩存命中率的方式,是按照內存布局順序訪問,那針對指令的緩存該如何提升呢?

我們以一個例子來看看,有一個元素為 0 到 100 之間隨機數字組成的一維數組:
在這里插入圖片描述
接下來,對這個數組做兩個操作:
在這里插入圖片描述
第一個操作,循環遍歷數組,把小于 50 的數組元素置為 0;
第二個操作,將數組排序;
那么問題來了,你覺得先遍歷再排序速度快,還是先排序再遍歷速度快呢?

在回答這個問題之前,我們先了解 CPU 的分支預測器。對于 if 條件語句,意味著此時至少可以選擇跳轉到兩段不同的指令執行,也就是 if 還是 else 中的指令。那么,如果分支預測可以預測到接下來要執行 if 里的指令,還是 else 指令的話,就可以「提前」把這些指令放在指令緩存中,這樣 CPU 可以直接從 Cache 讀取到指令,于是執行速度就會很快。

當數組中的元素是隨機的,分支預測就無法有效工作,而當數組元素都是是順序的,分支預測器會動態地根據歷史命中數據對未來進行預測,這樣命中率就會很高。

因此,先排序再遍歷速度會更快,這是因為排序之后,數字是從小到大的,那么前幾次循環命中 if < 50 的次數會比較多,于是分支預測就會緩存 if 里的 array[i] = 0 指令到 Cache 中,后續 CPU 執行該指令就只需要從 Cache 讀取就好了。

如果你肯定代碼中的 if 中的表達式判斷為 true 的概率比較高,我們可以使用顯示分支預測工具,比如在 C/C++ 語言中編譯器提供了 likely 和 unlikely 這兩種宏,如果 if 條件為 ture 的概率大,則可以用 likely 宏把 if 里的表達式包裹起來,反之用 unlikely 宏。
在這里插入圖片描述
實際上,CPU 自身的動態分支預測已經是比較準的了,所以只有當非常確信 CPU 預測的不準,且能夠知道實際的概率情況時,才建議使用這兩種宏。

如何提升多核 CPU 的緩存命中率?

在單核 CPU,雖然只能執行一個線程,但是操作系統給每個線程分配了一個時間片,時間片用完了,就調度下一個線程,于是各個線程就按時間片交替地占用 CPU,從宏觀上看起來各個線程同時在執行。

而現代 CPU 都是多核心的,線程可能在不同 CPU 核心來回切換執行,這對 CPU Cache 不是有利的,雖然 L3 Cache 是多核心之間共享的,但是 L1 和 L2 Cache 都是每個核心獨有的,如果一個線程在不同核心來回切換,各個核心的緩存命中率就會受到影響,相反如果線程都在同一個核心上執行,那么其數據的 L1 和 L2 Cache 的緩存命中率可以得到有效提高,緩存命中率高就意味著 CPU 可以減少訪問 內存的頻率。

當有多個同時執行「計算密集型」的線程,為了防止因為切換到不同的核心,而導致緩存命中率下降的問題,我們可以把線程綁定在某一個 CPU 核心上,這樣性能可以得到非常可觀的提升。

在 Linux 上提供了 sched_setaffinity 方法,來實現將線程綁定到某個 CPU 核心這一功能。

CPU 緩存一致性

小林里面內容比較多 以后用到再看吧

需要注意的是
寫直達 保持內存與 Cache 一致性最簡單的方式是,把數據同時寫入內存和 Cache 中,這種方法稱為寫直達(Write Through)。
既然寫直達由于每次寫操作都會把數據寫回到內存,而導致影響性能,于是為了要減少數據寫回內存的頻率,就出現了寫回(Write Back)的方法。

在寫回機制中,當發生寫操作時,新的數據僅僅被寫入 Cache Block 里,只有當修改過的 Cache Block「被替換」時才需要寫到內存中,減少了數據寫回內存的頻率,這樣便可以提高系統的性能。

中斷是什么?

先來看看什么是中斷?在計算機中,中斷是系統用來響應硬件設備請求的一種機制,操作系統收到硬件的中斷請求,會打斷正在執行的進程,然后調用內核中的中斷處理程序來響應請求。

這樣的解釋可能過于學術了,容易云里霧里,我就舉個生活中取外賣的例子。

小林中午搬完磚,肚子餓了,點了份白切雞外賣,這次我帶閃了,沒有被某團大數據殺熟。雖然平臺上會顯示配送進度,但是我也不能一直傻傻地盯著呀,時間很寶貴,當然得去干別的事情,等外賣到了配送員會通過「電話」通知我,電話響了,我就會停下手中地事情,去拿外賣。

這里的打電話,其實就是對應計算機里的中斷,沒接到電話的時候,我可以做其他的事情,只有接到了電話,也就是發生中斷,我才會停下當前的事情,去進行另一個事情,也就是拿外賣。

從這個例子,我們可以知道,中斷是一種異步的事件處理機制,可以提高系統的并發處理能力。

操作系統收到了中斷請求,會打斷其他進程的運行,所以中斷請求的響應程序,也就是中斷處理程序,要盡可能快的執行完,這樣可以減少對正常進程運行調度地影響。

而且,中斷處理程序在響應中斷時,可能還會「臨時關閉中斷」,這意味著,如果當前中斷處理程序沒有執行完之前,系統中其他的中斷請求都無法被響應,也就說中斷有可能會丟失,所以中斷處理程序要短且快。

還是回到外賣的例子,小林到了晚上又點起了外賣,這次為了犒勞自己,共點了兩份外賣,一份小龍蝦和一份奶茶,并且是由不同地配送員來配送,那么問題來了,當第一份外賣送到時,配送員給我打了長長的電話,說了一些雜七雜八的事情,比如給個好評等等,但如果這時另一位配送員也想給我打電話。

很明顯,這時第二位配送員因為我在通話中(相當于關閉了中斷響應),自然就無法打通我的電話,他可能嘗試了幾次后就走掉了(相當于丟失了一次中斷)。

#什么是軟中斷?
前面我們也提到了,中斷請求的處理程序應該要短且快,這樣才能減少對正常進程運行調度地影響,而且中斷處理程序可能會暫時關閉中斷,這時如果中斷處理程序執行時間過長,可能在還未執行完中斷處理程序前,會丟失當前其他設備的中斷請求。

那 Linux 系統為了解決中斷處理程序執行過長和中斷丟失的問題,將中斷過程分成了兩個階段,分別是「上半部和下半部分」。

上半部用來快速處理中斷,一般會暫時關閉中斷請求,主要負責處理跟硬件緊密相關或者時間敏感的事情。
下半部用來延遲處理上半部未完成的工作,一般以「內核線程」的方式運行。
前面的外賣例子,由于第一個配送員長時間跟我通話,則導致第二位配送員無法撥通我的電話,其實當我接到第一位配送員的電話,可以告訴配送員說我現在下樓,剩下的事情,等我們見面再說(上半部),然后就可以掛斷電話,到樓下后,在拿外賣,以及跟配送員說其他的事情(下半部)。

這樣,第一位配送員就不會占用我手機太多時間,當第二位配送員正好過來時,會有很大幾率撥通我的電話。

再舉一個計算機中的例子,常見的網卡接收網絡包的例子。

網卡收到網絡包后,通過 DMA 方式將接收到的數據寫入內存,接著會通過硬件中斷通知內核有新的數據到了,于是內核就會調用對應的中斷處理程序來處理該事件,這個事件的處理也是會分成上半部和下半部。

上部分要做的事情很少,會先禁止網卡中斷,避免頻繁硬中斷,而降低內核的工作效率。接著,內核會觸發一個軟中斷,把一些處理比較耗時且復雜的事情,交給「軟中斷處理程序」去做,也就是中斷的下半部,其主要是需要從內存中找到網絡數據,再按照網絡協議棧,對網絡數據進行逐層解析和處理,最后把數據送給應用程序。

所以,中斷處理程序的上部分和下半部可以理解為:

上半部直接處理硬件請求,也就是硬中斷,主要是負責耗時短的工作,特點是快速執行;
下半部是由內核觸發,也就說軟中斷,主要是負責上半部未完成的工作,通常都是耗時比較長的事情,特點是延遲執行;
還有一個區別,硬中斷(上半部)是會打斷 CPU 正在執行的任務,然后立即執行中斷處理程序,而軟中斷(下半部)是以內核線程的方式執行,并且每一個 CPU 都對應一個軟中斷內核線程,名字通常為「ksoftirqd/CPU 編號」,比如 0 號 CPU 對應的軟中斷內核線程的名字是 ksoftirqd/0

不過,軟中斷不只是包括硬件設備中斷處理程序的下半部,一些內核自定義事件也屬于軟中斷,比如內核調度等、RCU 鎖(內核里常用的一種鎖)等。

為什么 0.1 + 0.2 不等于 0.3 ?

補碼

如果負數不是使用補碼的方式表示,則在做基本對加減法運算的時候,還需要多一步操作來判斷是否為負數,如果為負數,還得把加法反轉成減法,或者把減法反轉成加法,這就非常不好了,畢竟加減法運算在計算機里是很常使用的,所以為了性能考慮,應該要盡量簡化這個運算過程。

而用了補碼的表示方式,對于負數的加減法操作,實際上是和正數加減法操作一樣的

精度

在計算機中 0.1 + 0.2 并不等于完整的 0.3。

這主要是因為有的小數無法可以用「完整」的二進制來表示,所以計算機里只能采用近似數的方式來保存,那兩個近似數相加,得到的必然也是一個近似數。

我們二進制只能精準表達 2 除盡的數字 1/2, 1/4, 1/8,但是對于 0.1(1/10) 和 0.2(1/5),在二進制中都無法精準表示時,需要根據精度舍入。

我們人類熟悉的十進制運算系統,可以精準表達 2 和 5 除盡的數字,例如 1/2, 1/4, 1/5(0.2), 1/8, 1/10(0.1)。

當然,十進制也有無法除u盡的地方,例如 1/3, 1/7,也需要根據精度舍入。

內核結構

對于內核的架構一般有這三種類型:
宏內核,包含多個模塊,整個內核像一個完整的程序;
微內核,有一個最小版本的內核,一些模塊和服務則由用戶態管理;。
混合內核,是宏內核和微內核的結合體,內核中抽象出了微內核的概念,也就是內核中會有一個小型的
內核,其他模塊就在這個基礎上搭建,整個內核是個完整的程序:
Linux 的內核設計是采用了宏內核,Window 的內核設計則是采用了混合內核。
這兩個操作系統的可執行文件格式也不一樣, Linux 可執行文件格式叫作 ELF,Windows 可執行文件格式叫作 PE。

內存管理

為什么要有虛擬內存

是什么

如果你是電子相關專業的,肯定在大學里搗鼓過單片機。

單片機是沒有操作系統的,所以每次寫完代碼,都需要借助工具把程序燒錄進去,這樣程序才能跑起來。

另外,單片機的 CPU 是直接操作內存的「物理地址」。

在這里插入圖片描述

在這種情況下,要想在內存中同時運行兩個程序是不可能的。如果第一個程序在 2000 的位置寫入一個新的值,將會擦掉第二個程序存放在相同位置上的所有內容,所以同時運行兩個程序是根本行不通的,這兩個程序會立刻崩潰。

操作系統是如何解決這個問題呢?

這里關鍵的問題是這兩個程序都引用了絕對物理地址,而這正是我們最需要避免的。

我們可以把進程所使用的地址「隔離」開來,即讓操作系統為每個進程分配獨立的一套「虛擬地址」,人人都有,大家自己玩自己的地址就行,互不干涉。但是有個前提每個進程都不能訪問物理地址,至于虛擬地址最終怎么落到物理內存里,對進程來說是透明的,操作系統已經把這些都安排的明明白白了。

操作系統會提供一種機制,將不同進程的虛擬地址和不同內存的物理地址映射起來。

如果程序要訪問虛擬地址的時候,由操作系統轉換成不同的物理地址,這樣不同的進程運行的時候,寫入的是不同的物理地址,這樣就不會沖突了。

虛擬內存的作用:

第一,虛擬內存可以使得進程對運行內存超過物理內存大小,因為程序運行符合局部性原理,CPU 訪問內存會有很明顯的重復訪問的傾向性,對于那些沒有被經常使用到的內存,我們可以把它換出到物理內存之外,比如硬盤上的 swap 區域。
第二,由于每個進程都有自己的頁表,所以每個進程的虛擬內存空間就是相互獨立的。進程也沒有辦法訪問其他進程的頁表,所以這些頁表是私有的,這就解決了多進程之間地址沖突的問題和進程內存獨立性安全的問題。
第三,頁表里的頁表項中除了物理地址之外,還有一些標記屬性的比特,比如控制一個頁的讀寫權限,標記該頁是否存在等。在內存訪問方面,操作系統提供了更好的安全性。
第四、通過虛擬內存的映射,內核空間和用戶空間某些時候比如io可以映射同一片內存,避免了拷貝復制的耗時操作(具體看零拷貝那里)

內存分配管理

程序員阿沛寫的比小林好,主要來自阿沛
https://mp.weixin.qq.com/s?__biz=MzUxMjczODMyMA==&mid=2247484085&idx=1&sn=ead5eb84f73b2260437eb21650509241&chksm=f8bda2f4d9572c3b05cc05f3bd864f57bba41d11826f92bb6ad36a80ab28210259f7cc5dbe99&mpshare=1&scene=24&srcid=0501FQLJ9FombY8gSMZTXYm5&sharer_shareinfo=c31674ae5ee2fe805e76ca9a9b932af5&sharer_shareinfo_first=c31674ae5ee2fe805e76ca9a9b932af5#rd

存在的問題

內存碎片 分外部碎片和內部碎片 后面會講

內存的連續分配管理

連續分配是指,整個進程在物理內存中是連續存儲的,分配給進程的內存空間是連續的內存空間。連續分配策略分為單一連續分配、固定分區分配和動態分區分配3種。

  • 單一連續分配是指將內存中整個用戶區空間分配給1個用戶程序,無需對用戶區再分區,內存中只能有一道用戶程序,用戶程序獨占整個用戶區空間。
  • 固定分區分配是指在進程裝入內存之前,就將整個用戶區分為多個分區,進程裝入內存時選擇一個大小大于等于該進程所需內存的分區給該進程。
  • 動態分區分配:進程裝入內存時才對內存分區,分區的大小就是進程所需的所有內存大小。

固定分區分配和動態分區分配都是一個進程只能占用一個分區,不能占用多個分區。

單一連續分配

單一連續分配是指將內存中整個用戶區空間分配給1個用戶程序,無需對用戶區再分區,內存中只能有一道用戶程序,用戶程序獨占整個用戶區空間。

在這里插入圖片描述

優點:

實現簡單;

無外部碎片;

如果用戶區內存不足以裝下整個進程,可以采用覆蓋技術擴充內存;

不一定需要采取內存保護。

缺點:

內存只裝入一個進程的信息,內部碎片可能極大,內存利用率極低。

無法實現進程并發,并發率低。

固定分區分配

固定分區分配是指整個用戶空間劃分為若干個大小固定的分區,每個分區中只裝入一道作業,進程裝入內存時選擇一個大小大于等于該進程所需內存的分區給該進程。

按照分區大小是否相等,又有兩種分法:
在這里插入圖片描述
固定分區分配如何實現內存分配和回收?

操作系統需要建立一個分區分配表的數據結構,來實現各個分區的分配與回收。每個表項對應一個分區,通常按分區大小排列。每個表項包括對應分區的 大小、起始地址、狀態(是否已分配)。可以使用數組或者鏈表在邏輯上實現這個分區分配表。

在這里插入圖片描述
優點:實現簡單,無外部碎片。

缺點:

a. 當用戶程序太大時,可能所有的分區都不能滿足需求,此時不得不采用覆蓋技術來解決,但這又會降低性能;

b. 會產生大量內部碎片,內存利用率低。

c.難以合理預先劃分分區, 分區個數劃分少了意味著物理內存中能裝入的進程個數少了,降低并發度。分區個數劃分多了,每個分區大小就小了,就會造成大進程無法裝下任何一個分區。

動態分區分配

這種分配方式不會預先劃分內存分區,而是在進程裝入內存時, 根據進程的大小動態地建立分區,并使分區的大小正好適合進程的需要。因此動態分區的大小是不固定的。

在這里插入圖片描述
動態分區分配可以使用空閑分區表或者空閑分區鏈管理,表項 中包含分區號、 分區大小、分區 起始地址等信息。
在這里插入圖片描述
如果回收分區的時候,待回收的分區和其他空閑分區相連,需要合并這些分區的表項為一個表項。

動態分區分配的過程如下圖所示,在一開始給多個進程分配分區時,這些進程在內存中是緊挨著的,隨著多次回收和分配,進程在內存中會變成離散分布,并可能出現大量外部碎片。動態分區不會產生內部碎片,因為每個分區大小都等于該進程所需內存大小。
在這里插入圖片描述

此時可以用緊湊技術解決,將離散的進程變成在內存中緊密相連。

緊湊技術是指將所有進程占用的分區移動到一端使其緊湊的挨在一起,空閑區留在另一端。如下圖所示,原本所有碎片分區都容納不下進程5,但移動后所有碎片分區相鄰融合為一個連續的空閑區就能放下進程5:
在這里插入圖片描述

緊湊技術要將進程在內存中的物理位置“搬家”,因此需要在分區表對所有進程與地址有關的項修改,例如基址寄存器、程序計數器等,還會造成內存數據大量拷貝。因此緊湊會花費大量CPU時間。

緊縮的時機:當所有空閑分區的大小不滿足要裝入內存的進程大小時發生。

動態分區的優點:

分區大小可變,進程在內存的起始位置可變,空閑分區可合并,更靈活,內存利用率更高;

無內部碎片;

缺點:

有外部碎片,外部碎片過多就可能觸發“緊湊”,導致系統開銷變大;

一個進程需要占用連續的內存空間,相比于離散分配而言,內存利用率低,這點是靜態分區和單一連續分配都有的問題,而離散分區管理可以解決該問題;

動態分區分配算法

動態分區分配算法研究的是,內存中有空閑分區時,動態分區分配法優先選擇哪個分區給進程。動態分區算法分為首次適應算法、最佳適應算法、最壞適應算法和鄰近適應算法。

首次適應算法

從低地址開始查找分區,找到第一個能滿足待裝入進程大小的空閑分區。

如何實現:空閑分區在空閑分區鏈中以地址遞增的次序排列。每次分配內存時順序查找空閑分區鏈(或空閑分區 表),找到大小能滿足要求的第一個空閑分區。

缺點:低地址空閑區會產生很多外部碎片,查找空閑區鏈表時會重復遍歷低地址的外部碎片分區,增加了查找的開銷。

最佳適應算法

從所有空閑分區中選擇滿足待裝入進程大小的最小空閑分區,目的是預留大分區給大進程。

如何實現:空閑分區在空閑分區鏈中按容量遞增次序排序。每次分配內存時順序查找空閑分區鏈(或空閑分區表),找到大小能滿足要求的第一個空閑分區。
在這里插入圖片描述
缺點:每次都選最小的分區進行分配,會留下越來越多的、小到難以利用的外部碎片。

最壞適應法

從所有空閑分區中選擇滿足待裝入進程大小的最大空閑分區。目的是盡可能避免生成外部碎片。

如何實現:空閑分區在空閑分區鏈中按容量降序排序實現。
在這里插入圖片描述

缺點:每次都選最大的分區進行分配,可以讓分配后留下的空閑區更大,避免外部碎片。但是會導致較大的連續空閑區被迅速用完,“大進程”到達就沒有內存分區可用了。

鄰近適應算法

該算法按照首次適應算法的邏輯查找,但下次查找按上次查找結束的位置開始檢索,目的是解決首次適應算法查找時間遞增的缺點。

如何實現:空閑分區在空閑分區鏈中以地址遞增的順序排列,排成一個循環鏈表。使用一個頭指針指向上次分配內存時查找結束的位置,下次從該位置開始查找空閑分區鏈,找到大小能滿足要求的第一個空閑分區。

在這里插入圖片描述

缺點:進程裝入時,頭指針之前的空閑分區(低地址的分區)即使有足夠空間分配給該進程,系統還是會選擇頭指針之后的空閑分區來分配。因此該算法會導致高地址和低地址分區可無大分區可用。

四種算法中,首次適應算法的效果反而最好。首次適應算法和鄰近適應算法的優點是分配分區后,無需移動鏈表節點,而最佳適應和最壞適應算法則需要移動鏈表節點。鄰近適應算法的查找效率最高。

在這里插入圖片描述

連續分配的方式分配內存的最大缺點在于會產生很多內存碎片,內存利用率低,而產生很多內存碎片的根本原因在于必須為進程分配連續的內存空間,雖然可以通過緊湊技術解決,但是需要花費很多CPU時間。

為什么連續存儲會造成很多內存碎片?

因為對于較大的進程,系統要挑選大塊的空閑空間給進程,而很小塊的空閑空間可能連較小的進程都無法容納而永遠無法被利用,就變成了內存碎片。進程換入和換出的次數多了之后就會產生很多碎片。

離散分配則允許為進程分配分散的內存塊,即一個進程可以分布在不相鄰的多個小分區中。離散分配管理分為分頁存儲、分段存儲和段頁式存儲3種。

內存的離散分配管理

頁式存儲

頁式存儲又稱分頁存儲,是指在物理上將內存空間分為多個大小相等的物理塊(又叫內存塊),在邏輯上將進程的邏輯地址空間分為多個頁面(又叫邏輯頁),每個頁面和物理塊一樣大。

操作系統分配與進程占用頁面數量相同的物理塊給該進程,該進程的頁面可以離散的存放在內存的塊中,與內存塊一一對應,每個進程會維護一個邏輯頁和物理塊映射關系的頁表。

在這里插入圖片描述

頁面是邏輯空間單位,塊是物理空間單位。頁面的編號叫頁號,內存塊的編號叫塊號,編號和頁號都是從0開始。頁面大小由硬件決定,一般為2的若干次冪,有些是512B,有些是4K等,不同機器的頁面大小不同。

  • 邏輯地址的構成

一個邏輯地址可能是32位或者64位。一般來說,32位的邏輯地址的前 20 位 就是頁號,后12位就是頁內偏移,也就是說邏輯地址是由 頁號 和 頁內偏移 組成的。這里有個通用的結論:頁面大小 是 2 ^ k 情況下,頁號 是 邏輯地址的前 32 - k 位,頁內偏移 是 邏輯地址的末尾 k 位。

在這里插入圖片描述
頁號P表示這個邏輯地址位于邏輯空間的第幾個頁,頁內偏移W表示這個邏輯地址位于頁面的第幾個字節。

  • 分頁存儲如何實現地址轉換

分頁存儲采用動態重定位,即裝入時不進行地址轉換,運行指令時才進行地址轉換。指令中的地址是邏輯地址。邏輯地址轉為物理地址的過程如下:

在這里插入圖片描述

例如,程序中某個數據的邏輯地址L為80,頁面大小p為50,頁號塊號映射關系為 塊號數 = 頁號數+10,求物理地址(這里為了方便計算所以用公式表示映射關系,實際上頁號與塊號的映射關系需要查詢頁表得出)。

頁號P = floor(80 / 50) = 1

頁內偏移i = 80 % 50 = 30

塊號B = 1 + 10 = 11

物理地址S = 11 * 50 + 30 = 580

頁表

頁表是操作系統為每個進程創建的進程頁號與內存塊號的映射關系表。

進程的頁表保存在內存中,而頁表在內存的指針存在PCB內。

在這里插入圖片描述

需要注意:

  1. 一個進程都有屬于自己的頁表,進程的每個頁面對應一個頁表項,每個頁表項的大小是相同的。

  2. 每個頁表項由“頁號”和“塊號”組成,但頁號是“隱含”的,不占存儲空間,頁表實際只保存了塊號。

  3. 如果不采用多級頁表,則一個頁表是連續存放在內存中的。

一個頁表項的大小取決于內存最多有多少個物理塊。

假設某系統物理內存大小為 4GB,頁面大小為 4KB,則 4GB 的內存總共會被切分為 40G / 4K = 2^32 / 2^12 = 2^20個內存塊,因此內存塊號的范圍應該是 0 ~ 2^20 -1,內存塊號至少要用 20 bit 來表示,即頁表項至少需要3B的大小才能保存下最大的塊號(3*8=24bit)

  • 基本地址變換機構

基本地址變換機構是幫助系統將邏輯地址轉換為物理地址的硬件設施。這些硬件中最重要的是頁表寄存器(PTR)。頁表寄存器存放頁表在內存中的起始地址F 和頁表長度(頁表項個數)M。

進程未執行時,頁表的始址 和 頁表長度 放在進程控制塊(PCB)中,當進程被調度時,操作系統內核會把它們load到頁表寄存器中。下圖是CPU將 程序計數器 中指令的邏輯地址 轉為 物理地址 的過程。

在這里插入圖片描述

從上面的過程中我們注意兩點:

1、頁式管理的地址是一維的,因為只需向系統提供一個 邏輯地址 系統就能算出物理地址。

2、CPU根據邏輯地址訪問某個指令或數據時會發生2次內存IO,一次是根據頁號+頁表地址查頁表,一次是實際訪問目標指令或數據。

為了讓CPU根據邏輯地址尋址可以更快,計算機會使用一種叫快表的硬件把訪問過的頁表項緩存到CPU中,如果下次根據頁號命中快表中的頁表項,則可以避免查頁表的過程,減少1次內存IO。

在介紹快表之前需要先介紹局部性原理,局部性原理分為時間局部性和空間局部性。

  • 局部性原理

時間局部性:如果執行了程序中的某條指令,那么不久后這條指令很可能會再次被執行;如果某個數據被訪問過,不久之后該數據很可能會再次被訪問。(典型的例子就是循環)

空間局部性:一旦程序訪問了某個存儲單元,在不久之后,其附近的存儲單元也很有可能被訪問。

時間局部性強調對同一個指令或數據的重復訪問。空間局部性強調對鄰近指令或數據的訪問。

例如下面這段代碼:

int i = 0;
int a[100];
while(i < 100){
a[i] = i;
i++;
}

while代碼塊內的重復執行就是時間局部性的體現,會重復訪問相同的指令和 i 這個數據;數組a的訪問就是空間局部性的體現,a[099]是連續存儲的,對a[0]a[99]的訪問很可能是訪問同一個頁面。

快表 TLB

多級頁表雖然解決了空間上的問題,但是虛擬地址到物理地址的轉換就多了幾道轉換的工序,這顯然就降低了這倆地址轉換的速度,也就是帶來了時間上的開銷。
程序是有局部性的,即在一段時間內,整個程序的執行僅限于程序中的某一部分。相應地,執行所訪問的存儲空間也局限于某個內存區域。
我們就可以利用這一特性,把最常訪問的幾個頁表項存儲到訪問速度更快的硬件,于是計算機科學家們,就在 CPU 芯片中,加入了一個專門存放程序最常訪問的頁表項的 Cache,這個 Cache 就是 TLB(Translation Lookaside Buffer) ,通常稱為頁表緩存、轉址旁路緩存、快表等。

快表(TLB)本質上是一種被稱為聯想寄存器的硬件,用來緩存CPU最近訪問過的若干個頁表項,可以加快地址轉換的速度。訪問快表的速度遠高于訪問內存的速度。

根據時間和空間局部性,CPU根據邏輯地址查找物理塊時,可能多次查詢不同邏輯地址都會查到同一個頁表項和同一個物理塊,因此快表正是利用了局部性原理優化了地址轉換的過程。而局部行原理也可以使快表的命中率高達90%。下圖為添加了快表之后的地址變換機構。
在這里插入圖片描述
① CPU給出邏輯地址,由硬件算得頁號、頁內偏移量,將頁號與快表中的所有頁號進行比較(遍歷所有頁號是O(n)復雜度,但該過程快到耗時可以忽略不計,因為這是高速緩存)。

② 如果頁號命中快表則從快表讀出對應的塊號,塊號+頁內偏移量拼接得到物理地址,訪問該物理地址對應的內存單元。若快表命中則訪問某個物理地址僅需一次訪存即可。

③ 如果沒有命中快表,則需要訪問內存中的頁表,將查到的頁表項存入快表, 以便后面可能發生的再次訪問。但若快表已滿,則必須按照一定的算法對舊的頁表項進行替換。若快表未命中,則訪問某個邏輯地址需要兩次訪存。

多級頁表

簡單的分頁有什么缺陷嗎?
有空間上的缺陷。
因為操作系統是可以同時運行非常多的進程的,那這不就意味著頁表會非常的龐大。
在 32 位的環境下,虛擬地址空間共有 4GB,假設一個頁的大小是 4KB(2^12),那么就需要大約 100 萬 (2^20) 個頁,每個「頁表項」需要 4 個字節大小來存儲,那么整個 4GB 空間的映射就需要有 4MB 的內存來存儲頁表。
這 4MB 大小的頁表,看起來也不是很大。但是要知道每個進程都是有自己的虛擬地址空間的,也就說都有自己的頁表。
那么,100 個進程的話,就需要 400MB 的內存來存儲頁表,這是非常大的內存了,更別說 64 位的環境了。
你可能會問,分了二級表,映射 4GB 地址空間就需要 4KB(一級頁表)+ 4MB(二級頁表)的內存,這樣占用空間不是更大了嗎?
當然如果 4GB 的虛擬地址全部都映射到了物理內存上的話,二級分頁占用空間確實是更大了,但是,我們往往不會為一個進程分配那么多內存。
其實我們應該換個角度來看問題,還記得計算機組成原理里面無處不在的局部性原理么?
每個進程都有 4GB 的虛擬地址空間,而顯然對于大多數程序來說,其使用到的空間遠未達到 4GB,因為會存在部分對應的頁表項都是空的,根本沒有分配,對于已分配的頁表項,如果存在最近一定時間未訪問的頁表,在物理內存緊張的情況下,操作系統會將頁面換出到硬盤,也就是說不會占用物理內存。
如果使用了二級分頁,一級頁表就可以覆蓋整個 4GB 虛擬地址空間,但如果某個一級頁表的頁表項沒有被用到,也就不需要創建這個頁表項對應的二級頁表了,即可以在需要時才創建二級頁表。做個簡單的計算,假設只有 20% 的一級頁表項被用到了,那么頁表占用的內存空間就只有 4KB(一級頁表) + 20% * 4MB(二級頁表)= 0.804MB,這對比單級頁表的 4MB 是不是一個巨大的節約?
那么為什么不分級的頁表就做不到這樣節約內存呢?
我們從頁表的性質來看,保存在內存中的頁表承擔的職責是將虛擬地址翻譯成物理地址。假如虛擬地址在頁表中找不到對應的頁表項,計算機系統就不能工作了。所以頁表一定要覆蓋全部虛擬地址空間,不分級的頁表就需要有 100 多萬個頁表項來映射,而二級分頁則只需要 1024 個頁表項(此時一級頁表覆蓋到了全部虛擬地址空間,二級頁表在需要時創建)。
我們把二級分頁再推廣到多級頁表,就會發現頁表占用的內存空間更少了,這一切都要歸功于對局部性原理的充分應用。
對于 64 位的系統,兩級分頁肯定不夠了,就變成了四級目錄,分別是:
全局頁目錄項 PGD(Page Global Directory);
上層頁目錄項 PUD(Page Upper Directory);
中間頁目錄項 PMD(Page Middle Directory);
頁表項 PTE(Page Table Entry);

采用單級頁表會有兩個問題。

1、頁表必須連續存放在內存,當頁表很大時,頁表需要占用多個連續的內存塊。而從上面的內容我們知道連續存儲會造成內存碎片。對于該問題,系統采用多級頁表來解決。

2、根據局部性原理,進程一段時間內可能只訪問特定的幾個頁面,因此沒有必要讓整個頁表常駐內存。對于該問題,系統采用虛擬內存技術解決。

舉個例子,某32位計算機系統按字節編址,采用分頁存儲管理,頁面大小P=4KB,頁表項長度L= 4B。那么內存存儲一個頁表需要耗費 1024 個頁面。已知頁面大小是12位。(下面給出計算過程,不感興趣的朋友可以跳過)

計算過程:

最大頁號N = 2 ^ (32-12) = 2^20

頁表總大小S = 4 * N = 2 ^ 22

內存存儲一個頁表所需頁面個數 = S / P = 2^22 / 2^12 = 2^10 = 1024 個頁面

這就意味著每個進程的頁表就可能占用連續1024個物理塊。為了解決頁表的存儲需要連續占用多個物理塊這個問題,操作系統使用兩級頁表。

操作系統將原本的單級頁表劃分為多個頁,每頁包含若干個表項,一個單級頁表被分散存儲在內存中。再用一個作為目錄的頁表記錄這些離散的單級頁表的頁與其對應的塊號,目錄頁表就是一個一級頁表,或稱為頁目錄表,原本的單級頁表變成多個二級頁表。一級頁表的一個表項代表一個二級頁表的位置。如下如所示:

在這里插入圖片描述

地址轉換過程:

1、使用二級頁表時,系統會將一個邏輯地址按照位數解釋為3個部分:一級頁號、二級頁號和數據所在的頁內偏移。

2、從PCB 中讀出頁目錄表的起始地址,再根據一級頁號和頁目錄表的起始地址在內存查詢頁目錄表,找到下一級頁表在內存中的存放位置。

3、根據二級頁號和二級頁表的始址在內存中查二級頁表,找到最終想訪問的內存塊號。

4、根據目標內存塊號 + 頁內偏移得到物理地址。

注意兩個細節:

1、對于一個更長的邏輯地址,意味著系統的頁面數量上限更多,系統可能使用三級或四級頁表才能實現頁表離散存儲。非頂級頁表的個數可以有多個,但每個頁表的大小不能超過一個頁面;頂級頁表的個數只能有一個,只占用1個頁面。根據這個原則可以判斷出一個系統需要采用幾級頁表。

2、無論是多少級頁表,進程的PCB只記錄頂級頁表的起始指針。因此n級頁表下,CPU根據邏輯地址訪問目標值需要進行 n+1 次內存訪問。

  • 頁式存儲下的內部碎片

考慮一種情況,系統的頁面大小是4K,一個進程大小為49K,會占用13個頁面,但是最后一個頁面只用了1K,剩余3K沒有被該進程使用,也不會被其他進程使用,于是會造成3K的內存碎片。

因此頁式存儲不會產生外部碎片,但會產生內部碎片,而且每個進程產生的碎片大小范圍為 0~頁面大小p,可以認為每個進程平均產生 p/2 字節的內部碎片大小。

  • 頁面尺寸

頁面尺寸(也就是頁面大小)是影響分頁管理中內存使用效率的重要因素。

如果頁面尺寸太大,就會出現越大的內存碎片;

如果頁面太小,同一個進程所需的頁面數會越多,該進程的頁表占用的空間越多,而且會造成多級頁表,每多一級頁表,CPU根據邏輯地址找到目標值就需要多一次內存IO。

段式存儲

通常用戶程序由若干個邏輯模塊組成,例如一個C程序中有一個主函數main,它調用子函數f1~f3,又調用標準函數庫的printf和scanf,每一個函數都是一個獨立的邏輯結構,都應該有自己獨立的內存空間,每個內存空間的地址從0開始。

段式存儲(又叫做分段存儲)是指將進程按獨立的邏輯結構劃分為若干個段,每個段都被分配連續的內存空間,段與段之間離散存儲。

在這里插入圖片描述
用戶程序進行編譯時,編譯程序會為該程序的各個邏輯結構構建段,例如在編譯程序中會為下面內容建立單獨的段:

1.全局變量

2.函數調用棧,用來放參數和返回地址

3.每個函數的代碼部分

4.每個函數的局部變量

一個進程的程序段和數據段不止一個,而是程序中有多少個獨立的邏輯模塊就會產生多少個段。

例如進程A包含 main()、f()和g()這3個函數,其中 main()調用了f()和g(),此時進程A會產生3個程序段存放 main()/f()/g() 對應的機器指令,產生3個數據段存放 main()/f()/g() 生成的臨時數據。而且這些段相互獨立,相互離散。

如果main()先調用 f() 后調用 g() ,那么f() 和 g()的數據段不會同時出現在內存中,而是f()的數據段先被回收再創建g()的數據段,但 main()的數據段和f()的數據段肯定是同時在內存中的。

分段存儲中,每個段都有一個段名和段號。和分頁存儲不同的是,每個頁的長度是相同的,由操作系統決定;而每個段的長度是不同的,是由模塊內的代碼量和產生的數據量決定。

分段系統的邏輯地址結構由段號(也就是段名)和段內地址(段內偏移量)所組成。段號的位數決定了每個進程最多可以分幾個段,段內地址位數決定了每個段的最大長度是多少。分段的用戶進程地址空間是二維的,程序員在標識一個地址時,既要給出段名,也要給出段內地址。

在這里插入圖片描述

和頁表類似,為了方便系統找到進程的某個段在物理內存中的起始地址,系統為分段存儲管理下的每個進程建立了一個段表。如下所示:

在這里插入圖片描述

1、每個段對應一個段表項,其中記錄了該段在內存中的起始位置(又稱 “基址”)和段的長度(要記錄長度是因為每個段的長度不同,而頁的長度都相同)。

2、各個段表項的長度是相同的。段表項長度取決于段長的位數和段基址的位數(即操作系統位數)。由于段表項長度相同,因此段號是隱含的,不占存儲空間。

  • 分段存儲的地址變換

在這里插入圖片描述
①根據邏輯地址得到 段號、段內地址。

②判斷段號是否越界。若S≥M,說明CPU正訪問一個不存在于該進程的段,產生越界 中斷,否則繼續執行。

③根據段表基址和段號查詢段表,找到 對應的段表項,段 表項的存放地址為 F+S*段表項長度。得到段基址b

④檢查段內地址是否超過 段長。若W≥C,說明CPU正訪問一個該段不存在的地址,產生越 界中斷,否則繼續執行。

⑤根據段基址b和段內偏移W計算得到物理地址。

⑥訪問目標 內存單元。

分段系統中也可以引入快表機構,將近期訪問過的段表項放到快表中,這樣可以少一次訪問,加快地址變換速度。

分段和分頁管理對比

1、從頁和段的含義

分頁的主要目的是為了實現進程在內存的離散分配,提高內存利用率。分頁僅僅是系統管理上的需要,完全是系統行為,對用戶是不可見的。

分段的主要目的是更好地滿足用戶需求。一個段通常包含著一個邏輯模塊的信息,分段對用戶是可見的。

2、從頁和段的長度

頁的大小固定且由系統決定。段的長度卻不固定,決定于用戶編寫的程序。

3、分段比分頁更容易實現程序的共享和保護。

因為我們共享程序具體是共享一個獨立的邏輯模塊,而分段存儲管理是對獨立的邏輯模塊分配連續內存空間,系統只需將共享的這段代碼在內存中的地址添加到要引用這段代碼的進程中的段表中即可。

在這里插入圖片描述
而如果在分頁存儲管理中共享,一個邏輯模塊的內容可能分在在不同的頁面中。假如該邏輯模塊橫跨11個頁,就需要將這些頁面的頁號塊號映射都添加到共享進程們的頁表中。而且一個頁可能既有共享模塊的內容也有非共享模塊的內容,如果讓共享進程們的頁表指向這個頁,就可能訪問到不可共享的代碼和數據。

在這里插入圖片描述
簡單來說:在分頁系統中實現頁的共享較為困難,因為分頁是對連續的進程邏輯空間等分,被共享的程序不一定剛好分在一個或多個完整的頁面。此時就會出現一個頁面既有共享程序又有不能共享的該進程自己的私有數據,因此頁式存儲不利于共享。

PS:關于共享程序

不能被修改的代碼稱為純代碼或可重入代碼,這樣的代碼是可以安全的被并行執行的,只有這樣的代碼可以被多進程或多線程共享,而可修改的代碼是不能共享的。

以函數為例,如果一個函數是可重入的,則該函數不能含有全局變量,只能包含調用者提供的參數,和函數自己產生的局部變量,可重入函數也不能調用其他不可重入的函數;

如下所示:

int g_var = 1;

int f()
{
g_var = g_var + 2;
return g_var;
}

int g()
{
return f() + 2;
}

f() 和 g() 都是不可重入函數,因為他們包含了一個全局變量 g_var,如果多進程共享f()和g()會導致g_var變量因為多進程并發的異步性而被改亂。

int f(int i)
{
return i + 2;
}

int g(int i)
{
return f(i) + 2;
}

這兩個函數才是可重入函數。多進程只能共享f() 和 g() 的代碼段,但是不會共享f()和g()的數據段。f() 和 g()運行過程中產生的數據分別保存在各自進程自己在運行f()/g()時創建的數據段中。例如 進程A和進程B共享 f() ,進程A運行f()時系統會為其創建一個數據段A1用來保存f()產生的臨時變量i,而且數據段A1和進程A運行自身其他邏輯模塊產生的數據段是相互獨立的,離散的。

在這里插入圖片描述

段頁式管理

段頁式管理將進程按邏輯模塊分段,再將各段分頁 ,再將物理空間分為大小相同的內存塊 ,把進程各頁面分別裝入各內存塊中。

段頁式存儲同時繼承了分頁管理和分段管理的優點,分段方便用戶進程按以邏輯模塊為單位規劃和運行,段內分頁使一個段可以離散存儲,提高了內存利用率,避免外部碎片。
在這里插入圖片描述
段頁式系統的邏輯地址結構由段號、頁號、頁內地址(頁內偏移量)組成。
在這里插入圖片描述
“分段”對用戶是可見的,程序員知道程序中有多少個段,因為它親手實現了這些邏輯模塊。而將各段“分頁”對用戶是不可見的,系統會根據段內地址自動劃分頁號 和頁內偏移量。段頁式管理中,一個進程對應一個段表和多個頁表,一個段對應一個頁表。

在這里插入圖片描述

每個段對應一個段表項,每個段表項由段號、段對應的頁表長度、頁表存放的塊號(頁表起始地址)組成。每個頁面對應一個頁表項,每個頁表項由頁號、塊號組成。

段頁式存儲地址變換的過程
在這里插入圖片描述
無論是段式存儲、頁式存儲還是段頁式存儲,其地址變換都發生在進程運行時,即CPU執行指令時發生的。

linux內存布局與管理

用戶空間和系統空間

在這里插入圖片描述

內存滿了,會發生什么?

更多看小林
在這里插入圖片描述
在這里插入圖片描述

  • 文件頁(File-backed Page):內核緩存的磁盤數據(Buffer)和內核緩存的文件數據(Cache)都叫作文件頁。大部分文件頁,都可以直接釋放內存,以后有需要時,再從磁盤重新讀取就可以了。而那些被應用程序修改過,并且暫時還沒寫入磁盤的數據(也就是臟頁),就得先寫入磁盤,然后才能進行內存釋放。所以,回收干凈頁的方式是直接釋放內存,回收臟頁的方式是先寫回磁盤后再釋放內存。
  • 匿名頁(Anonymous Page):這部分內存沒有實際載體,不像文件緩存有硬盤文件這樣一個載體,比如堆、棧數據等。這部分內存很可能還要再次被訪問,所以不能直接釋放內存,它們回收的方式是通過 Linux 的 Swap 機制,Swap 會把不常訪問的內存先寫到磁盤中,然后釋放這些內存,給其他更需要的進程使用。再次訪問這些內存時,重新從磁盤讀入內存就可以了。

如何保護一個進程不被 OOM 殺掉呢?

在系統空閑內存不足的情況,進程申請了一個很大的內存,如果直接內存回收都無法回收出足夠大的空閑內存,那么就會觸發 OOM 機制,內核就會根據算法選擇一個進程殺掉。

Linux 到底是根據什么標準來選擇被殺的進程呢?這就要提到一個在 Linux 內核里有一個 oom_badness() 函數,它會把系統中可以被殺掉的進程掃描一遍,并對每個進程打分,得分最高的進程就會被首先殺掉。

進程得分的結果受下面這兩個方面影響:

第一,進程已經使用的物理內存頁面數。
第二,每個進程的 OOM 校準值 oom_score_adj。它是可以通過 /proc/[pid]/oom_score_adj 來配置的。我們可以在設置 -1000 到 1000 之間的任意一個數值,調整進程被 OOM Kill 的幾率。
函數 oom_badness() 里的最終計算方法是這樣的:

// points 代表打分的結果
// process_pages 代表進程已經使用的物理內存頁面數
// oom_score_adj 代表 OOM 校準值
// totalpages 代表系統總的可用頁面數
points = process_pages + oom_score_adj*totalpages/1000

用「系統總的可用頁面數」乘以 「OOM 校準值 oom_score_adj」再除以 1000,最后再加上進程已經使用的物理頁面數,計算出來的值越大,那么這個進程被 OOM Kill 的幾率也就越大。

每個進程的 oom_score_adj 默認值都為 0,所以最終得分跟進程自身消耗的內存有關,消耗的內存越大越容易被殺掉。我們可以通過調整 oom_score_adj 的數值,來改成進程的得分結果:

如果你不想某個進程被首先殺掉,那你可以調整該進程的 oom_score_adj,從而改變這個進程的得分結果,降低該進程被 OOM 殺死的概率。
如果你想某個進程無論如何都不能被殺掉,那你可以將 oom_score_adj 配置為 -1000。
我們最好將一些很重要的系統服務的 oom_score_adj 配置為 -1000,比如 sshd,因為這些系統服務一旦被殺掉,我們就很難再登陸進系統了。

但是,不建議將我們自己的業務程序的 oom_score_adj 設置為 -1000,因為業務程序一旦發生了內存泄漏,而它又不能被殺掉,這就會導致隨著它的內存開銷變大,OOM killer 不停地被喚醒,從而把其他進程一個個給殺掉。

「在 4GB 物理內存的機器上,申請 8G 內存會怎么樣?」

存在比較大的爭議,有人說會申請失敗,有的人說可以申請成功。

這個問題在沒有前置條件下,就說出答案就是耍流氓。這個問題要考慮三個前置條件:

操作系統是 32 位的,還是 64 位的?
申請完 8G 內存后會不會被使用?
操作系統有沒有使用 Swap 機制?
更多看小林

預讀失效和緩存污染——如何改進 LRU 算法

Linux 的 Page Cache 和 MySQL 的 Buffer Pool 的大小是有限的,并不能無限的緩存數據,對于一些頻繁訪問的數據我們希望可以一直留在內存中,而一些很少訪問的數據希望可以在某些時機可以淘汰掉,從而保證內存不會因為滿了而導致無法再緩存新的數據,同時還能保證常用數據留在內存中。

要實現這個,最容易想到的就是 LRU(Least recently used)算法。

LRU 算法一般是用「鏈表」作為數據結構來實現的,鏈表頭部的數據是最近使用的,而鏈表末尾的數據是最久沒被使用的。那么,當空間不夠了,就淘汰最久沒被使用的節點,也就是鏈表末尾的數據,從而騰出內存空間。

因為 Linux 的 Page Cache 和 MySQL 的 Buffer Pool 緩存的基本數據單位都是頁(Page)單位,所以后續以「頁」名稱代替「數據」。

傳統的 LRU 算法的實現思路是這樣的:

當訪問的頁在內存里,就直接把該頁對應的 LRU 鏈表節點移動到鏈表的頭部。
當訪問的頁不在內存里,除了要把該頁放入到 LRU 鏈表的頭部,還要淘汰 LRU 鏈表末尾的頁。
比如下圖,假設 LRU 鏈表長度為 5,LRU 鏈表從左到右有編號為 1,2,3,4,5 的頁。
在這里插入圖片描述
如果訪問了 3 號頁,因為 3 號頁已經在內存了,所以把 3 號頁移動到鏈表頭部即可,表示最近被訪問了。
在這里插入圖片描述
而如果接下來,訪問了 8 號頁,因為 8 號頁不在內存里,且 LRU 鏈表長度為 5,所以必須要淘汰數據,以騰出內存空間來緩存 8 號頁,于是就會淘汰末尾的 5 號頁,然后再將 8 號頁加入到頭部。
在這里插入圖片描述

傳統的 LRU 算法并沒有被 Linux 和 MySQL 使用,因為傳統的 LRU 算法無法避免下面這兩個問題:

預讀失效導致緩存命中率下降;
緩存污染導致緩存命中率下降;

預讀失效,怎么辦?

Linux 操作系統為基于 Page Cache 的讀緩存機制提供預讀機制,一個例子是:

應用程序只想讀取磁盤上文件 A 的 offset 為 0-3KB 范圍內的數據,由于磁盤的基本讀寫單位為 block(4KB),于是操作系統至少會讀 0-4KB 的內容,這恰好可以在一個 page 中裝下。
但是操作系統出于空間局部性原理(靠近當前被訪問數據的數據,在未來很大概率會被訪問到),會選擇將磁盤塊 offset [4KB,8KB)、[8KB,12KB) 以及 [12KB,16KB) 都加載到內存,于是額外在內存中申請了 3 個 page;

上圖中,應用程序利用 read 系統調動讀取 4KB 數據,實際上內核使用預讀機制(ReadaHead) 機制完成了 16KB 數據的讀取,也就是通過一次磁盤順序讀將多個 Page 數據裝入 Page Cache。

這樣下次讀取 4KB 數據后面的數據的時候,就不用從磁盤讀取了,直接在 Page Cache 即可命中數據。因此,預讀機制帶來的好處就是減少了 磁盤 I/O 次數,提高系統磁盤 I/O 吞吐量。

MySQL Innodb 存儲引擎的 Buffer Pool 也有類似的預讀機制,MySQL 從磁盤加載頁時,會提前把它相鄰的頁一并加載進來,目的是為了減少磁盤 IO。

預讀失效會帶來什么問題?
如果這些被提前加載進來的頁,并沒有被訪問,相當于這個預讀工作是白做了,這個就是預讀失效。

如果使用傳統的 LRU 算法,就會把「預讀頁」放到 LRU 鏈表頭部,而當內存空間不夠的時候,還需要把末尾的頁淘汰掉。

如果這些「預讀頁」如果一直不會被訪問到,就會出現一個很奇怪的問題,不會被訪問的預讀頁卻占用了 LRU 鏈表前排的位置,而末尾淘汰的頁,可能是熱點數據,這樣就大大降低了緩存命中率 。

#如何避免預讀失效造成的影響?
我們不能因為害怕預讀失效,而將預讀機制去掉,大部分情況下,空間局部性原理還是成立的。

要避免預讀失效帶來影響,最好就是讓預讀頁停留在內存里的時間要盡可能的短,讓真正被訪問的頁才移動到 LRU 鏈表的頭部,從而保證真正被讀取的熱數據留在內存里的時間盡可能長。

那到底怎么才能避免呢?

Linux 操作系統和 MySQL Innodb 通過改進傳統 LRU 鏈表來避免預讀失效帶來的影響,具體的改進分別如下:

Linux 操作系統實現兩個了 LRU 鏈表:活躍 LRU 鏈表(active_list)和非活躍 LRU 鏈表(inactive_list);
MySQL 的 Innodb 存儲引擎是在一個 LRU 鏈表上劃分來 2 個區域:young 區域 和 old 區域。
這兩個改進方式,設計思想都是類似的,都是將數據分為了冷數據和熱數據,然后分別進行 LRU 算法。不再像傳統的 LRU 算法那樣,所有數據都只用一個 LRU 算法管理。

接下來,具體聊聊 Linux 和 MySQL 是如何避免預讀失效帶來的影響?

Linux 是如何避免預讀失效帶來的影響?

Linux 操作系統實現兩個了 LRU 鏈表:活躍 LRU 鏈表(active_list)和非活躍 LRU 鏈表(inactive_list)。

  • active list 活躍內存頁鏈表,這里存放的是最近被訪問過(活躍)的內存頁;
  • inactive list 不活躍內存頁鏈表,這里存放的是很少被訪問(非活躍)的內存頁;

有了這兩個 LRU 鏈表后,預讀頁就只需要加入到 inactive list 區域的頭部,當頁被真正訪問的時候,才將頁插入 active list 的頭部。如果預讀的頁一直沒有被訪問,就會從 inactive list 移除,這樣就不會影響 active list 中的熱點數據。

接下來,給大家舉個例子。

假設 active list 和 inactive list 的長度為 5,目前內存中已經有如下 10 個頁:
在這里插入圖片描述
現在有個編號為 20 的頁被預讀了,這個頁只會被插入到 inactive list 的頭部,而 inactive list 末尾的頁(10號)會被淘汰掉。
在這里插入圖片描述
即使編號為 20 的預讀頁一直不會被訪問,它也沒有占用到 active list 的位置,而且還會比 active list 中的頁更早被淘汰出去。

如果 20 號頁被預讀后,立刻被訪問了,那么就會將它插入到 active list 的頭部, active list 末尾的頁(5號),會被降級到 inactive list ,作為 inactive list 的頭部,這個過程并不會有數據被淘汰。
在這里插入圖片描述
MySQL 是如何避免預讀失效帶來的影響?

MySQL 的 Innodb 存儲引擎是在一個 LRU 鏈表上劃分來 2 個區域,young 區域 和 old 區域。

young 區域在 LRU 鏈表的前半部分,old 區域則是在后半部分,這兩個區域都有各自的頭和尾節點,如下圖:
在這里插入圖片描述

young 區域與 old 區域在 LRU 鏈表中的占比關系并不是一比一的關系,而是 63:37(默認比例)的關系。

劃分這兩個區域后,預讀的頁就只需要加入到 old 區域的頭部,當頁被真正訪問的時候,才將頁插入 young 區域的頭部。如果預讀的頁一直沒有被訪問,就會從 old 區域移除,這樣就不會影響 young 區域中的熱點數據。

接下來,給大家舉個例子。

假設有一個長度為 10 的 LRU 鏈表,其中 young 區域占比 70 %,old 區域占比 30 %。
在這里插入圖片描述
現在有個編號為 20 的頁被預讀了,這個頁只會被插入到 old 區域頭部,而 old 區域末尾的頁(10號)會被淘汰掉。

在這里插入圖片描述
如果 20 號頁一直不會被訪問,它也沒有占用到 young 區域的位置,而且還會比 young 區域的數據更早被淘汰出去。

如果 20 號頁被預讀后,立刻被訪問了,那么就會將它插入到 young 區域的頭部,young 區域末尾的頁(7號),會被擠到 old 區域,作為 old 區域的頭部,這個過程并不會有頁被淘汰。

在這里插入圖片描述

緩存污染,怎么辦?

#什么是緩存污染?
雖然 Linux (實現兩個 LRU 鏈表)和 MySQL (劃分兩個區域)通過改進傳統的 LRU 數據結構,避免了預讀失效帶來的影響。

但是如果還是使用「只要數據被訪問一次,就將數據加入到活躍 LRU 鏈表頭部(或者 young 區域)」這種方式的話,那么還存在緩存污染的問題。

當我們在批量讀取數據的時候,由于數據被訪問了一次,這些大量數據都會被加入到「活躍 LRU 鏈表」里,然后之前緩存在活躍 LRU 鏈表(或者 young 區域)里的熱點數據全部都被淘汰了,如果這些大量的數據在很長一段時間都不會被訪問的話,那么整個活躍 LRU 鏈表(或者 young 區域)就被污染了。

#緩存污染會帶來什么問題?
緩存污染帶來的影響就是很致命的,等這些熱數據又被再次訪問的時候,由于緩存未命中,就會產生大量的磁盤 I/O,系統性能就會急劇下降。

我以 MySQL 舉例子,Linux 發生緩存污染的現象也是類似。

當某一個 SQL 語句掃描了大量的數據時,在 Buffer Pool 空間比較有限的情況下,可能會將 Buffer Pool 里的所有頁都替換出去,導致大量熱數據被淘汰了,等這些熱數據又被再次訪問的時候,由于緩存未命中,就會產生大量的磁盤 I/O,MySQL 性能就會急劇下降。

注意, 緩存污染并不只是查詢語句查詢出了大量的數據才出現的問題,即使查詢出來的結果集很小,也會造成緩存污染。

比如,在一個數據量非常大的表,執行了這條語句:

select * from t_user where name like “%xiaolin%”;
可能這個查詢出來的結果就幾條記錄,但是由于這條語句會發生索引失效,所以這個查詢過程是全表掃描的,接著會發生如下的過程:

從磁盤讀到的頁加入到 LRU 鏈表的 old 區域頭部;
當從頁里讀取行記錄時,也就是頁被訪問的時候,就要將該頁放到 young 區域頭部;
接下來拿行記錄的 name 字段和字符串 xiaolin 進行模糊匹配,如果符合條件,就加入到結果集里;
如此往復,直到掃描完表中的所有記錄。
經過這一番折騰,由于這條 SQL 語句訪問的頁非常多,每訪問一個頁,都會將其加入 young 區域頭部,那么原本 young 區域的熱點數據都會被替換掉,導致緩存命中率下降。那些在批量掃描時,而被加入到 young 區域的頁,如果在很長一段時間都不會再被訪問的話,那么就污染了 young 區域。

舉個例子,假設需要批量掃描:21,22,23,24,25 這五個頁,這些頁都會被逐一訪問(讀取頁里的記錄)。
在這里插入圖片描述
在批量訪問這些頁的時候,會被逐一插入到 young 區域頭部。

在這里插入圖片描述
可以看到,原本在 young 區域的 6 和 7 號頁都被淘汰了,而批量掃描的頁基本占滿了 young 區域,如果這些頁在很長一段時間都不會被訪問,那么就對 young 區域造成了污染。

如果 6 和 7 號頁是熱點數據,那么在被淘汰后,后續有 SQL 再次讀取 6 和 7 號頁時,由于緩存未命中,就要從磁盤中讀取了,降低了 MySQL 的性能,這就是緩存污染帶來的影響。

#怎么避免緩存污染造成的影響?
前面的 LRU 算法只要數據被訪問一次,就將數據加入活躍 LRU 鏈表(或者 young 區域),這種 LRU 算法進入活躍 LRU 鏈表的門檻太低了!正式因為門檻太低,才導致在發生緩存污染的時候,很容就將原本在活躍 LRU 鏈表里的熱點數據淘汰了。

所以,只要我們提高進入到活躍 LRU 鏈表(或者 young 區域)的門檻,就能有效地保證活躍 LRU 鏈表(或者 young 區域)里的熱點數據不會被輕易替換掉。

Linux 操作系統和 MySQL Innodb 存儲引擎分別是這樣提高門檻的:

Linux 操作系統:在內存頁被訪問第二次的時候,才將頁從 inactive list 升級到 active list 里。
MySQL Innodb:在內存頁被訪問第二次的時候,并不會馬上將該頁從 old 區域升級到 young 區域,因為還要進行停留在 old 區域的時間判斷:
如果第二次的訪問時間與第一次訪問的時間在 1 秒內(默認值),那么該頁就不會被從 old 區域升級到 young 區域;
如果第二次的訪問時間與第一次訪問的時間超過 1 秒,那么該頁就會從 old 區域升級到 young 區域;
提高了進入活躍 LRU 鏈表(或者 young 區域)的門檻后,就很好了避免緩存污染帶來的影響。

在批量讀取數據時候,如果這些大量數據只會被訪問一次,那么它們就不會進入到活躍 LRU 鏈表(或者 young 區域),也就不會把熱點數據淘汰,只會待在非活躍 LRU 鏈表(或者 old 區域)中,后續很快也會被淘汰。

總結

傳統的 LRU 算法法無法避免下面這兩個問題:

  • 預讀失效導致緩存命中率下降;
  • 緩存污染導致緩存命中率下降;

為了避免「預讀失效」造成的影響,Linux 和 MySQL 對傳統的 LRU 鏈表做了改進:

Linux 操作系統實現兩個了 LRU 鏈表:活躍 LRU 鏈表(active list)和非活躍 LRU 鏈表(inactive list)。
MySQL Innodb 存儲引擎是在一個 LRU 鏈表上劃分來 2 個區域:young 區域 和 old 區域。

但是如果還是使用「只要數據被訪問一次,就將數據加入到活躍 LRU 鏈表頭部(或者 young 區域)」這種方式的話,那么還存在緩存污染的問題。

為了避免「緩存污染」造成的影響,Linux 操作系統和 MySQL Innodb 存儲引擎分別提高了升級為熱點數據的門檻:

Linux 操作系統:在內存頁被訪問第二次的時候,才將頁從 inactive list 升級到 active list 里。
MySQL Innodb:在內存頁被訪問第二次的時候,并不會馬上將該頁從 old 區域升級到 young 區域,因為還要進行停留在 old 區域的時間判斷:
如果第二次的訪問時間與第一次訪問的時間在 1 秒內(默認值),那么該頁就不會被從 old 區域升級到 young 區域;
如果第二次的訪問時間與第一次訪問的時間超過 1 秒,那么該頁就會從 old 區域升級到 young 區域;
通過提高了進入 active list (或者 young 區域)的門檻后,就很好了避免緩存污染帶來的影響。

進程管理

并發和并行有什么區別?

在這里插入圖片描述

程序 進程的區別

在這里插入圖片描述

進程

狀態

在這里插入圖片描述

進程的控制結構PCB

在操作系統中,是用進程控制塊(process control block,PCB)數據結構來描述進程的。

PCB 是進程存在的唯一標識,這意味著一個進程的存在,必然會有一個 PCB,如果進程消失了,那么 PCB 也會隨之消失。

PCB 具體包含什么信息呢?

  • 進程描述信息:
    • 進程標識符:標識各個進程,每個進程都有一個并且唯一的標識符;
    • 用戶標識符:進程歸屬的用戶,用戶標識符主要為共享和保護服務;
  • 進程控制和管理信息:
    • 進程當前狀態,如 new、ready、running、waiting 或 blocked 等;
    • 進程優先級:進程搶占 CPU 時的優先級;
  • 資源分配清單:
    • 有關內存地址空間或虛擬地址空間的信息,
    • 所打開文件的列表
    • 所使用的 I/O 設備信息。
  • CPU 相關信息:CPU 中各個寄存器的值,當進程被切換時,CPU 的狀態信息都會被保存在相應的 PCB 中,以便進程重新執行時,能從斷點處繼續執行。

可見,PCB 包含信息還是比較多的。

每個 PCB 是如何組織的呢?

通常是通過鏈表的方式進行組織,把具有相同狀態的進程鏈在一起,組成各種隊列。比如:

  • 將所有處于就緒狀態的進程鏈在一起,稱為就緒隊列;
  • 把所有因等待某事件而處于等待狀態的進程鏈在一起就組成各種阻塞隊列;
  • 另外,對于運行隊列在單核 CPU 系統中則只有一個運行指針了,因為單核 CPU 在某個時間,只能運行一個程序。

進程的上下文切換

各個進程之間是共享 CPU 資源的,在不同的時候進程之間需要切換,讓不同的進程可以在 CPU 執行,那么這個一個進程切換到另一個進程運行,稱為進程的上下文切換。

在詳細說進程上下文切換前,我們先來看看 CPU 上下文切換

大多數操作系統都是多任務,通常支持大于 CPU 數量的任務同時運行。實際上,這些任務并不是同時運行的,只是因為系統在很短的時間內,讓各個任務分別在 CPU 運行,于是就造成同時運行的錯覺。

任務是交給 CPU 運行的,那么在每個任務運行前,CPU 需要知道任務從哪里加載,又從哪里開始運行。

所以,操作系統需要事先幫 CPU 設置好 CPU 寄存器和程序計數器。

CPU 寄存器是 CPU 內部一個容量小,但是速度極快的內存(緩存)。我舉個例子,寄存器像是你的口袋,內存像你的書包,硬盤則是你家里的柜子,如果你的東西存放到口袋,那肯定是比你從書包或家里柜子取出來要快的多。

再來,程序計數器則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。

所以說,CPU 寄存器和程序計數是 CPU 在運行任何任務前,所必須依賴的環境,這些環境就叫做 CPU 上下文。

既然知道了什么是 CPU 上下文,那理解 CPU 上下文切換就不難了。

CPU 上下文切換就是先把前一個任務的 CPU 上下文(CPU 寄存器和程序計數器)保存起來,然后加載新任務的上下文到這些寄存器和程序計數器,最后再跳轉到程序計數器所指的新位置,運行新任務。

系統內核會存儲保持下來的上下文信息,當此任務再次被分配給 CPU 運行時,CPU 會重新加載這些上下文,這樣就能保證任務原來的狀態不受影響,讓任務看起來還是連續運行。

上面說到所謂的「任務」,主要包含進程、線程和中斷。所以,可以根據任務的不同,把 CPU 上下文切換分成:進程上下文切換、線程上下文切換和中斷上下文切換。

進程的上下文切換到底是切換什么呢?

進程是由內核管理和調度的,所以進程的切換只能發生在內核態。

所以,進程的上下文切換不僅包含了虛擬內存、棧、全局變量等用戶空間的資源,還包括了內核堆棧、寄存器等內核空間的資源。

通常,會把交換的信息保存在進程的 PCB,當要運行另外一個進程的時候,我們需要從這個進程的 PCB 取出上下文,然后恢復到 CPU 中,這使得這個進程可以繼續執行,如下圖所示:
在這里插入圖片描述
大家需要注意,進程的上下文開銷是很關鍵的,我們希望它的開銷越小越好,這樣可以使得進程可以把更多時間花費在執行程序上,而不是耗費在上下文切換。

發生進程上下文切換有哪些場景?

為了保證所有進程可以得到公平調度,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流分配給各個進程。這樣,當某個進程的時間片耗盡了,進程就從運行狀態變為就緒狀態,系統從就緒隊列選擇另外一個進程運行;
進程在系統資源不足(比如內存不足)時,要等到資源滿足后才可以運行,這個時候進程也會被掛起,并由系統調度其他進程運行;
當進程通過睡眠函數 sleep 這樣的方法將自己主動掛起時,自然也會重新調度;
當有優先級更高的進程運行時,為了保證高優先級進程的運行,當前進程會被掛起,由高優先級進程來運行;
發生硬件中斷時,CPU 上的進程會被中斷掛起,轉而執行內核中的中斷服務程序;
以上,就是發生進程上下文切換的常見場景了。

線程

為什么使用線程?

我們舉個例子,假設你要編寫一個視頻播放器軟件,那么該軟件功能的核心模塊有三個:

從視頻文件當中讀取數據;
對讀取的數據進行解壓縮;
把解壓縮后的視頻數據播放出來;
對于單進程的實現方式,我想大家都會是以下這個方式:

在這里插入圖片描述

對于單進程的這種方式,存在以下問題:

播放出來的畫面和聲音會不連貫,因為當 CPU 能力不夠強的時候,Read 的時候可能進程就等在這了,這樣就會導致等半天才進行數據解壓和播放;
各個函數之間不是并發執行,影響資源的使用效率;
那改進成多進程的方式:

在這里插入圖片描述

對于多進程的這種方式,依然會存在問題:

進程之間如何通信,共享數據?
維護進程的系統開銷較大,如創建進程時,分配資源、建立 PCB;終止進程時,回收資源、撤銷 PCB;進程切換時,保存當前進程的狀態信息;
那到底如何解決呢?需要有一種新的實體,滿足以下特性:

實體之間可以并發運行;
實體之間共享相同的地址空間;
這個新的實體,就是線程( Thread ),線程之間可以并發運行且共享相同的地址空間。

什么是線程?

線程是進程當中的一條執行流程。

同一個進程內多個線程之間可以共享代碼段、數據段、打開的文件等資源,但每個線程各自都有一套獨立的寄存器和棧,這樣可以確保線程的控制流是相對獨立的。

在這里插入圖片描述
線程的優缺點?

線程的優點:

一個進程中可以同時存在多個線程;
各個線程之間可以并發執行;
各個線程之間可以共享地址空間和文件等資源;

線程的缺點:

當進程中的一個線程崩潰時,會導致其所屬進程的所有線程崩潰(這里是針對 C/C++ 語言,Java語言中的線程奔潰不會造成進程崩潰,具體分析原因可以看這篇:線程崩潰了,進程也會崩潰嗎? (opens new window))。

舉個例子,對于游戲的用戶設計,則不應該使用多線程的方式,否則一個用戶掛了,會影響其他同個進程的線程。

線程與進程的比較

更多看那篇單獨的博客

線程與進程的比較如下:

進程是資源(包括內存、打開的文件等)分配的單位,線程是 CPU 調度的單位;
進程擁有一個完整的資源平臺,而線程只獨享必不可少的資源,如寄存器和棧;
線程同樣具有就緒、阻塞、執行三種基本狀態,同樣具有狀態之間的轉換關系;
線程能減少并發執行的時間和空間開銷;
對于,線程相比進程能減少開銷,體現在:

  • 線程的創建時間比進程快,因為進程在創建的過程中,還需要資源管理信息,比如內存管理信息、文件管理信息,而線程在創建的過程中,不會涉及這些資源管理信息,而是共享它們;
  • 線程的終止時間比進程快,因為線程釋放的資源相比進程少很多;
    同一個進程內的線程切換比進程切換快,因為線程具有相同的地址空間(虛擬內存共享),這意味著同一個進程的線程都具有同一個頁表,那么在切換的時候不需要切換頁表。而對于進程之間的切換,切換的時候要把頁表給切換掉,而頁表的切換過程開銷是比較大的;
  • 由于同一進程的各線程間共享內存和文件資源,那么在線程之間數據傳遞的時候,就不需要經過內核了,這就使得線程之間的數據交互效率更高了;

所以,不管是時間效率,還是空間效率線程比進程都要高。

線程的上下文切換

在前面我們知道了,線程與進程最大的區別在于:線程是調度的基本單位,而進程則是資源擁有的基本單位。

所以,所謂操作系統的任務調度,實際上的調度對象是線程,而進程只是給線程提供了虛擬內存、全局變量等資源。

對于線程和進程,我們可以這么理解:

當進程只有一個線程時,可以認為進程就等于線程;
當進程擁有多個線程時,這些線程會共享相同的虛擬內存和全局變量等資源,這些資源在上下文切換時是不需要修改的;
另外,線程也有自己的私有數據,比如棧和寄存器等,這些在上下文切換時也是需要保存的。

線程上下文切換的是什么?

這還得看線程是不是屬于同一個進程:

當兩個線程不是屬于同一個進程,則切換的過程就跟進程上下文切換一樣;
當兩個線程是屬于同一個進程,因為虛擬內存是共享的,所以在切換時,虛擬內存這些資源就保持不動,只需要切換線程的私有數據、寄存器等不共享的數據;
所以,線程的上下文切換相比進程,開銷要小很多。

進程間有哪些通信方式

更多看小林
在這里插入圖片描述

多線程沖突了怎么辦?

更多看小林

進程與線程區別

結合另一篇專門的博客看

怎么避免死鎖

看另一篇博客

進程調度/頁面置換/磁盤調度算法

看小林

文件系統全家桶

看小林和之前寫的博客

設備管理

看小林

IO相關

看另一篇博客

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/81205.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/81205.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/81205.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

電機常用易混淆概念說明(伺服、舵機、多輪)

1. 概述 基礎動力需求 &#xff1a;普通電機&#xff08;如水泵、風扇&#xff09;。 高精度控制 &#xff1a;優先伺服系統或伺服電機&#xff08;如數控機床&#xff09;。 微型化場景 &#xff1a;舵機&#xff08;如遙控模型&#xff09;。 移動底盤 &#xff1a;單舵輪成…

進程與線程:04 內核線程

內核級線程概述 上一講我們學習了用戶級線程&#xff0c;了解了其切換和創建方式。用戶級線程切換核心在于從一個棧變為兩個棧&#xff0c;每個線程有自己的棧和線程控制塊&#xff08;tcb&#xff09;&#xff0c;切換時先切換tcb再切換棧&#xff0c;創建時將切換的pc指針放…

信息系統項目管理師-軟考高級(軟考高項)???????????2025最新(六)

個人筆記整理---僅供參考 第六章項目管理概論 6.1PMBOK的發展 6.2項目基本要素 組織過程資產指的是項目上的&#xff0c;國產數據庫的使用----安保和安全指的是環境因素 6.3項目經理的角色 6.4價值驅動的項目管理知識體系

[藍橋杯 2023 國 Python B] 劃分 Java

import java.util.*;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int[] arr new int[41];int sum 0;for (int i 1; i < 40; i) {arr[i] sc.nextInt();sum arr[i];}sc.close();int target sum / 2; // 最接近的兩…

Redis05-進階-主從

零、文章目錄 Redis05-進階-主從 1、搭建主從架構 &#xff08;1&#xff09;概述 單節點Redis的并發能力是有上限的&#xff0c;要進一步提高Redis的并發能力&#xff0c;就需要搭建主從集群&#xff0c;實現讀寫分離。 &#xff08;2&#xff09;集群概況 我們搭建的主從…

小結:ipsec-ike

IPSec 手動配置與自動配置&#xff08;IKE動態協商&#xff09; 手動配置IPSec 邏輯圖 #mermaid-svg-eNMnNEwnoTjF8fkV {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-eNMnNEwnoTjF8fkV .error-icon{fill:#552222;}…

瀟灑郎: 100% 成功搭建Docker私有鏡像倉庫并管理、刪除鏡像

1、Registry Web管理界面 2、拉取Registry-Web鏡像 創建配置文件 tee /opt/zwx-registry/web-config.yml <<-EOF registry:url: http://172.28.73.90:8010/v2name: registryreadonly: falseauth:enabled: false EOF 拉取docker-registry-web鏡像并綁定Registry倉庫 …

《機器學習中的過擬合與模型復雜性:理解與應對策略》

《機器學習中的過擬合與模型復雜性&#xff1a;理解與應對策略》 摘要 在機器學習中&#xff0c;過擬合是模型在訓練數據上表現良好但在新數據上泛化能力差的現象。本文深入探討了過擬合與模型復雜性之間的關系&#xff0c;分析了復雜模型導致過擬合的原因&#xff0c;并介紹…

linux中sigint和sigterm的區別

SIGINT 和 SIGTERM 是在 Unix 及類 Unix 系統&#xff08;包括 Linux&#xff09;中用于進程間通信的信號&#xff0c;它們都可以用于請求進程終止&#xff0c;區別如下&#xff1a; 1、信號編號與定義 在信號機制里&#xff0c;每個信號都有對應的編號&#xff0c;這便于系統…

一套SaaS ERP管理系統源碼,支持項目二開商用,SpringBoot+Vue+ElementUI+UniAPP

ERP管理系統源碼&#xff0c;一款適用于小微企業的SaaS ERP管理系統源碼, 采用最新的技術棧開發(SpringBootVueElementUIUniAPP)&#xff0c;讓企業簡單上云。 專注于小微企業的應用需求&#xff0c;如企業基本的進銷存、詢價&#xff0c;報價, 采購、銷售、MRP生產制造、品質…

2025 新生 DL-FWI 培訓

摘要: 本貼給出 8 次討論式培訓的提綱, 每次培訓 1 小時. 1. Basic concepts 主動學習: 提問, 理解, 繼續追問. 通過不斷迭代, 逐步提升問題的質量, 加深理解. 1.1 Seismic exploration 問 DeepSeek (下同): 為什么進行地震勘探? 問: 地震勘探一般的深度是多少? 1.2 Sesmi…

mac電腦pytest生成測試報告

時隔了好久再寫代碼&#xff0c;感覺我之前的積累都白費了&#xff0c;全部忘記了&#xff0c;看來每一步都有記錄對于我來說才是最好的。 最近又要重新搞接口自動化&#xff0c;然而是在mac電腦&#xff0c;對于我長期使用windows的人來說真的是個考驗&#xff0c;對此次過程…

神經輻射場(NeRF)技術解析:3D重建與虛擬世界的未來

神經輻射場&#xff08;NeRF&#xff09;技術解析&#xff1a;3D重建與虛擬世界的未來 ——從算法突破到元宇宙基礎設施的演進之路 摘要 本文通過算法演進圖譜、訓練流程解析、PyTorch代碼實戰及產業應用洞察&#xff0c;構建從學術創新到工程落地的完整技術框架。實驗數據顯…

ES搜索知識

GET /categories/1/10?name手機 // 按名稱過濾 GET /categories/1/10?type電子產品 // 按類型過濾 GET /categories/1/10?name手機&type電子產品 // 組合過濾 查詢參數 ApiOperation(value "獲取商品分類分頁列表")GetMapping("{page}/{limit}")…

【Docker】Docker拉取部分常用中間件

一、拉取MySQL 這里以Docker拉取MySQL5.7為例 #拉取鏡像 docker pull mysql:5.7 docker run -d --name oj-mysql -p 3306:3306 -e "TZAsia/Shanghai" -e "MYSQL_ROOT_PASSWORD123456" mysql:5.7 -e 參數用于設置容器內的環境變量。TZ 是用于設置時區的環…

在 Ubuntu 上離線安裝 ClickHouse

在 Ubuntu 上離線安裝 ClickHouse 的步驟如下: 一.安裝驗證 # 檢查服務狀態 sudo systemctl status clickhouse-server #刪除默認文件 sudo rm /etc/clickhouse-server/users.d/default-password.xml # 使用客戶端連接 clickhouse-client --password

Linux 部署以paddle Serving 的方式部署 PaddleOCR CPU版本

強烈建議您在Docker內構建Paddle Serving&#xff0c;更多鏡像請查看Docker鏡像列表。 提示-1&#xff1a;Paddle Serving項目僅支持Python3.6/3.7/3.8/3.9&#xff0c;接下來所有的與Python/Pip相關的操作都需要選擇正確的Python版本。 提示-2&#xff1a;以下示例中GPU環境均…

AOSP Android14 Launcher3——Launcher的狀態介紹LauncherState類

Launcher3中有一個跟Launcher狀態相關的類&#xff0c;叫LauncherState LauncherState 是 Launcher3 中定義各種用戶界面狀態的抽象基類。你可以把它想象成一個狀態機&#xff0c;定義了 Launcher 可能處于的不同視覺和交互模式&#xff0c;例如主屏幕、所有應用列表、最近任務…

鴻蒙NEXT開發動畫(方塊動畫旋轉)

1.創建空白項目 2.Page文件夾下面新建Spin.ets文件&#xff0c;代碼如下&#xff1a; /*** SpinKit 風格的旋轉加載動畫組件。** component* param spinSize - 動畫容器大小&#xff08;必須為正數&#xff09;* param spinColor - 動畫顏色&#xff08;支持資源引用&#xf…

深入解析Java架構師面試:從核心技術到AI應用

深入解析Java架構師面試&#xff1a;從核心技術到AI應用 在互聯網大廠的Java求職者面試中&#xff0c;技術深度和項目經驗是成功的關鍵。本文以嚴肅的面試官與資深Java架構師馬架構&#xff08;擁有十年研發及架構設計經驗&#xff09;之間的對話為背景&#xff0c;詳細展示了…