作業系統常用知識總結!
計算機結構
現代計算機模型是基於-「馮諾依曼計算機模型」
計算機在執行時,先從記憶體中取出第一條指令,透過控制器的譯碼,按指令的要求,從儲存器中取出資料進行指定的運算和邏輯操作等加工,然後再按地址把結果送到記憶體中去,接下來,再取出第二條指令,在控制器的指揮下完成規定操作,依此進行下去。直至遇到停止指令
程式與資料一樣存貯,按程式編排的順序,一步一步地取出指令,自動地完成指令規定的操作是計算機最基本的工作模型
「計算機五大核心組成部分」
控制器:是整個計算機的中樞神經,其功能是對程式規定的控制資訊進行解釋,根據其要求進行控制,排程程式、資料、地址,協調計算機各部分工作及記憶體與外設的訪問等。
運算器:運算器的功能是對資料進行各種算術運算和邏輯運算,即對資料進行加工處理。
儲存器:儲存器的功能是儲存程式、資料和各種訊號、命令等資訊,並在需要時提供這些資訊。
輸入:輸入裝置是計算機的重要組成部分,輸入裝置與輸出裝置合你為外部裝置,簡稱外設,輸入裝置的作用是將程式、原始資料、文字、字元、控制命令或現場採集的資料等資訊輸入到計算機。
❝常見的輸入裝置有鍵盤、滑鼠器、光電輸入機、磁帶機、磁碟機、光碟機等。
❞
輸出:輸出裝置與輸入裝置同樣是計算機的重要組成部分,它把外算機的中間結果或最後結果、機內的各種資料符號及文字或各種控制訊號等資訊輸出出來,微機常用的輸出裝置有顯示終端CRT、印表機、鐳射印字機、繪圖儀及磁帶、光碟機等。
「計算機結構分成以下 5 個部分:」
輸入裝置;輸出裝置;記憶體;中央處理器;匯流排。
記憶體
在馮諾依曼模型中,程式和資料被儲存在一個被稱作記憶體的線性排列儲存區域。
儲存的資料單位是一個二進位制位,英文是 bit,最小的儲存單位叫作位元組,也就是 8 位,英文是 byte,每一個位元組都對應一個記憶體地址。
記憶體地址由 0 開始編號,比如第 1 個地址是 0,第 2 個地址是 1, 然後自增排列,最後一個地址是記憶體中的位元組數減 1。
我們通常說的記憶體都是隨機存取器,也就是讀取任何一個地址資料的速度是一樣的,寫入任何一個地址資料的速度也是一樣的。
CPU
馮諾依曼模型中 CPU 負責控制和計算,為了方便計算較大的數值,CPU 每次可以計算多個位元組的資料。
如果 CPU 每次可以計算 4 個 byte,那麼我們稱作 32 位 CPU;
如果 CPU 每次可以計算 8 個 byte,那麼我們稱作 64 位 CPU。
這裡的 32 和 64,稱作 CPU 的位寬。
「為什麼 CPU 要這樣設計呢?」
因為一個 byte 最大的表示範圍就是 0~255。
比如要計算 20000*50
,就超出了byte 最大的表示範圍了。
因此,CPU 需要支援多個 byte 一起計算,當然,CPU 位數越大,可以計算的數值就越大,但是在現實生活中不一定需要計算這麼大的數值,比如說 32 位 CPU 能計算的最大整數是 4294967295,這已經非常大了。
「控制單元和邏輯運算單元」
CPU 中有一個控制單元專門負責控制 CPU 工作;還有邏輯運算單元專門負責計算。
「暫存器」
CPU 要進行計算,比如最簡單的加和兩個數字時,因為 CPU 離記憶體太遠,所以需要一種離自己近的儲存來儲存將要被計算的數字。
這種儲存就是暫存器,暫存器就在 CPU 裡,控制單元和邏輯運算單元非常近,因此速度很快。
常見的暫存器種類:
通用暫存器,用來存放需要進行運算的資料,比如需要進行加和運算的兩個資料。 程式計數器,用來儲存 CPU 要執行下一條指令所在的記憶體地址,注意不是儲存了下一條要執行的指令,此時指令還在記憶體中,程式計數器只是儲存了下一條指令的地址。 指令暫存器,用來存放程式計數器指向的指令,也就是指令本身,指令被執行完成之前,指令都儲存在這裡。
多級快取
現代CPU為了提升執行效率,減少CPU與記憶體的互動(互動影響CPU效率),一般在CPU上整合了多級快取架構
「CPU快取」即高速緩衝儲存器,是位於CPU與主記憶體間的一種容量較小但速度很高的儲存器
由於CPU的速度遠高於主記憶體,CPU直接從記憶體中存取資料要等待一定時間週期,Cache中儲存著CPU剛用過或迴圈使用的一部分資料,當CPU再次使用該部分資料時可從Cache中直接呼叫,減少CPU的等待時間,提高了系統的效率,具體包括以下幾種:
「L1-Cache」
L1- 快取在 CPU 中,相比暫存器,雖然它的位置距離 CPU 核心更遠,但造價更低,通常 L1-Cache 大小在幾十 Kb 到幾百 Kb 不等,讀寫速度在 2~4 個 CPU 時鐘週期。
「L2-Cache」
L2- 快取也在 CPU 中,位置比 L1- 快取距離 CPU 核心更遠,它的大小比 L1-Cache 更大,具體大小要看 CPU 型號,有 2M 的,也有更小或者更大的,速度在 10~20 個 CPU 週期。
「L3-Cache」
L3- 快取同樣在 CPU 中,位置比 L2- 快取距離 CPU 核心更遠,大小通常比 L2-Cache 更大,讀寫速度在 20~60 個 CPU 週期。
L3 快取大小也是看型號的,比如 i9 CPU 有 512KB L1 Cache;有 2MB L2 Cache; 有16MB L3 Cache。
當 CPU 需要記憶體中某個資料的時候,如果暫存器中有這個資料,我們可以直接使用;如果暫存器中沒有這個資料,我們就要先查詢 L1 快取;L1 中沒有,再查詢 L2 快取;L2 中沒有再查詢 L3 快取;L3 中沒有,再去記憶體中拿。
「總結:」
儲存器儲存空間大小:記憶體>L3>L2>L1>暫存器;
儲存器速度快慢排序:暫存器>L1>L2>L3>記憶體;
安全等級
「CPU執行安全等級」
CPU有4個執行級別,分別為:
ring0,ring1,ring2,ring3
ring0只給作業系統用,ring3誰都能用。
ring0是指CPU的執行級別,是最高階別,ring1次之,ring2更次之……
系統(核心)的程式碼執行在最高執行級別ring0上,可以使用特權指令,控制中斷、修改頁表、訪問裝置等等。
應用程式的程式碼執行在最低執行級別上ring3上,不能做受控操作。
如果要做,比如要訪問磁碟,寫檔案,那就要透過執行系統呼叫(函式),執行系統呼叫的時候,CPU的執行級別會發生從ring3到ring0的切換,並跳轉到系統呼叫對應的核心程式碼位置執行,這樣核心就為你完成了裝置訪問,完成之後再從ring0返回ring3。
這個過程也稱作使用者態和核心態的切換。
區域性性原理
在CPU訪問儲存裝置時,無論是存取資料抑或存取指令,都趨於聚集在一片連續的區域中,這就被稱為區域性性原理
「時間區域性性(Temporal Locality):」
如果一個資訊項正在被訪問,那麼在近期它很可能還會被再次訪問。
比如迴圈、遞迴、方法的反覆呼叫等。
「空間區域性性(Spatial Locality):」
如果一個儲存器的位置被引用,那麼將來他附近的位置也會被引用。
比如順序執行的程式碼、連續建立的兩個物件、陣列等。
程式的執行過程
程式實際上是一條一條指令,所以程式的執行過程就是把每一條指令一步一步的執行起來,負責執行指令的就是 CPU 了。
「那 CPU 執行程式的過程如下:」
第一步,CPU 讀取程式計數器的值,這個值是指令的記憶體地址,然後 CPU 的控制單元操作地址匯流排指定需要訪問的記憶體地址,接著通知記憶體裝置準備資料,資料準備好後透過資料匯流排將指令資料傳給 CPU,CPU 收到記憶體傳來的資料後,將這個指令資料存入到指令暫存器。 第二步,CPU 分析指令暫存器中的指令,確定指令的型別和引數,如果是計算型別的指令,就把指令交給邏輯運算單元運算;如果是儲存型別的指令,則交由控制單元執行; 第三步,CPU 執行完指令後,程式計數器的值自增,表示指向下一條指令。這個自增的大小,由 CPU 的位寬決定,比如 32 位的 CPU,指令是 4 個位元組,需要 4 個記憶體地址存放,因此程式計數器的值會自增 4;
簡單總結一下就是,一個程式執行的時候,CPU 會根據程式計數器裡的記憶體地址,從記憶體裡面把需要執行的指令讀取到指令暫存器裡面執行,然後根據指令長度自增,開始順序讀取下一條指令。
CPU 從程式計數器讀取指令、到執行、再到下一條指令,這個過程會不斷迴圈,直到程式執行結束,這個不斷迴圈的過程被稱為 「CPU 的指令週期」。
匯流排
CPU 和記憶體以及其他裝置之間,也需要通訊,因此我們用一種特殊的裝置進行控制,就是匯流排。
地址匯流排,用於指定 CPU 將要操作的記憶體地址; 資料匯流排,用於讀寫記憶體的資料; 控制匯流排,用於傳送和接收訊號,比如中斷、裝置復位等訊號,CPU 收到訊號後自然進行響應,這時也需要控制匯流排;
當 CPU 要讀寫記憶體資料的時候,一般需要透過兩個匯流排:
首先要透過地址匯流排來指定記憶體的地址; 再透過資料匯流排來傳輸資料;
輸入、輸出裝置
輸入裝置向計算機輸入資料,計算機經過計算,將結果透過輸出裝置向外界傳達。
如果輸入裝置、輸出裝置想要和 CPU 進行互動,比如說使用者按鍵需要 CPU 響應,這時候就需要用到控制匯流排。
基礎知識
中斷
「中斷的型別」
按照中斷的觸發方分成同步中斷和非同步中斷;
根據中斷是否強制觸發分成可遮蔽中斷和不可遮蔽中斷。
中斷可以由 CPU 指令直接觸發,這種主動觸發的中斷,叫作同步中斷。
❝同步中斷有幾種情況。
❞
比如系統呼叫,需要從使用者態切換核心態,這種情況需要程式觸發一箇中斷,叫作陷阱(Trap),中斷觸發後需要繼續執行系統呼叫。
還有一種同步中斷情況是錯誤(Fault),通常是因為檢測到某種錯誤,需要觸發一箇中斷,中斷響應結束後,會重新執行觸發錯誤的地方,比如後面我們要學習的缺頁中斷。
最後還有一種情況是程式的異常,這種情況和 Trap 類似,用於實現程式丟擲的異常。
另一部分中斷不是由 CPU 直接觸發,是因為需要響應外部的通知,比如響應鍵盤、滑鼠等裝置而觸發的中斷,這種中斷我們稱為非同步中斷。
CPU 通常都支援設定一箇中斷遮蔽位(一個暫存器),設定為 1 之後 CPU 暫時就不再響應中斷。
對於鍵盤滑鼠輸入,比如陷阱、錯誤、異常等情況,會被臨時遮蔽。
但是對於一些特別重要的中斷,比如 CPU 故障導致的掉電中斷,還是會正常觸發。
「可以被遮蔽的中斷我們稱為可遮蔽中斷,多數中斷都是可遮蔽中斷。」
核心態和使用者態
「什麼是使用者態和核心態」
Kernel 執行在超級許可權模式下,所以擁有很高的許可權。
按照許可權管理的原則,多數應用程式應該執行在最小許可權下。
因此,很多作業系統,將記憶體分成了兩個區域:
核心空間(Kernal Space),這個空間只有核心程式可以訪問;
使用者空間(User Space),這部分記憶體專門給應用程式使用。
使用者空間中的程式碼被限制了只能使用一個區域性的記憶體空間,我們說這些程式在使用者態 執行。
核心空間中的程式碼可以訪問所有記憶體,我們稱這些程式在核心態 執行。
❝按照級別分:
❞
當程式執行在0級特權級上時,就可以稱之為執行在核心態
當程式執行在3級特權級上時,就可以稱之為執行在使用者態
執行在使用者態下的程式不能直接訪問作業系統核心資料結構和程式。
當我們在系統中執行一個程式時,大部分時間是執行在使用者態下的,在其需要作業系統幫助完成某些它沒有權力和能力完成的工作時就會切換到核心態(比如操作硬體)
「這兩種狀態的主要差別」
處於使用者態執行時,程式所能訪問的記憶體空間和物件受到限制,其所處於佔有的處理器是可被搶佔的
處於核心態執行時,則能訪問所有的記憶體空間和物件,且所佔有的處理器是不允許被搶佔的。
「為什麼要有使用者態和核心態」
由於需要限制不同的程式之間的訪問能力,防止他們獲取別的程式的記憶體資料,或者獲取外圍裝置的資料,併傳送到網路
「使用者態與核心態的切換」
所有使用者程式都是執行在使用者態的,但是有時候程式確實需要做一些核心態的事情, 例如從硬碟讀取資料,或者從鍵盤獲取輸入等,而唯一可以做這些事情的就是作業系統,所以此時程式就需要先作業系統請求以程式的名義來執行這些操作
「使用者態和核心態的轉換」
❝系統呼叫
❞
使用者態程式透過系統呼叫申請使用作業系統提供的服務程式完成工作,比如fork()實際上就是執行了一個建立新程式的系統呼叫
而系統呼叫的機制其核心還是使用了作業系統為使用者特別開放的一箇中斷來實現,例如Linux的int 80h中斷
「舉例:」
如上圖所示:核心程式執行在核心態(Kernal Mode),使用者程式執行在使用者態(User Mode)。
當發生系統呼叫時,使用者態的程式發起系統呼叫,因為系統呼叫中牽扯特權指令,使用者態程式許可權不足,因此會中斷執行,也就是 Trap(Trap 是一種中斷)。
發生中斷後,當前 CPU 執行的程式會中斷,跳轉到中斷處理程式,核心程式開始執行,也就是開始處理系統呼叫。
核心處理完成後,主動觸發 Trap,這樣會再次發生中斷,切換回使用者態工作。
❝異常
❞
當CPU在執行執行在使用者態下的程式時,發生了某些事先不可知的異常,這時會觸發由當前執行程式切換到處理此異常的核心相關程式中,也就轉到了核心態,比如缺頁異常
❝外圍裝置的中斷
❞
當外圍裝置完成使用者請求的操作後,會向CPU發出相應的中斷訊號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷訊號對應的處理程式,如果先前執行的指令是使用者態下的程式,那麼這個轉換的過程自然也就發生了由使用者態到核心態的切換
比如硬碟讀寫操作完成,系統會切換到硬碟讀寫的中斷處理程式中執行後續操作等
執行緒
執行緒:系統分配處理器時間資源的基本單元,是程式執行的最小單位
執行緒可以看做輕量級的程式,共享記憶體空間,每個執行緒都有自己獨立的執行棧和程式計數器,執行緒之間切換的開銷小。
在同一個程式(程式)中有多個執行緒同時執行(透過CPU排程,在每個時間片中只有一個執行緒執行)
程式可以透過 API 建立使用者態的執行緒,也可以透過系統呼叫建立核心態的執行緒。
使用者態執行緒
使用者態執行緒也稱作使用者級執行緒,作業系統核心並不知道它的存在,它完全是在使用者空間中建立。
使用者級執行緒有很多優勢,比如:
管理開銷小:建立、銷燬不需要系統呼叫。
切換成本低:使用者空間程式可以自己維護,不需要走作業系統排程。
但是這種執行緒也有很多的缺點:
與核心協作成本高:比如這種執行緒完全是使用者空間程式在管理,當它進行 I/O 的時候,無法利用到核心的優勢,需要頻繁進行使用者態到核心態的切換。
執行緒間協作成本高:設想兩個執行緒需要通訊,通訊需要 I/O,I/O 需要系統呼叫,因此使用者態執行緒需要額外的系統呼叫成本。
無法利用多核優勢:比如作業系統排程的仍然是這個執行緒所屬的程式,所以無論每次一個程式有多少使用者態的執行緒,都只能併發執行一個執行緒,因此一個程式的多個執行緒無法利用多核的優勢。
作業系統無法針對執行緒排程進行最佳化:當一個程式的一個使用者態執行緒阻塞(Block)了,作業系統無法及時發現和處理阻塞問題,它不會更換執行其他執行緒,從而造成資源浪費。
核心態執行緒
核心態執行緒也稱作核心級執行緒(Kernel Level Thread),這種執行緒執行在核心態,可以透過系統呼叫創造一個核心級執行緒。
核心級執行緒有很多優勢:
可以利用多核 CPU 優勢:核心擁有較高許可權,因此可以在多個 CPU 核心上執行核心執行緒。
作業系統級最佳化:核心中的執行緒操作 I/O 不需要進行系統呼叫;一個核心執行緒阻塞了,可以立即讓另一個執行。
當然核心執行緒也有一些缺點:
建立成本高:建立的時候需要系統呼叫,也就是切換到核心態。
擴充套件性差:由一個核心程式管理,不可能數量太多。
切換成本較高:切換的時候,也同樣存在需要核心操作,需要切換核心態。
「使用者態執行緒和核心態執行緒之間的對映關係」
如果有一個使用者態的程式,它下面有多個執行緒,如果這個程式想要執行下面的某一個執行緒,應該如何做呢?
❝這時,比較常見的一種方式,就是將需要執行的程式,讓一個核心執行緒去執行。
❞
畢竟,核心執行緒是真正的執行緒,因為它會分配到 CPU 的執行資源。
如果一個程式所有的執行緒都要自己排程,相當於在程式的主執行緒中實現分時演算法排程每一個執行緒,也就是所有執行緒都用作業系統分配給主執行緒的時間片段執行。
❝這種做法,相當於作業系統排程程式的主執行緒;程式的主執行緒進行二級排程,排程自己內部的執行緒。
❞
這樣操作劣勢非常明顯,比如無法利用多核優勢,每個執行緒排程分配到的時間較少,而且這種執行緒在阻塞場景下會直接交出整個程式的執行許可權。
由此可見,使用者態執行緒建立成本低,問題明顯,不可以利用多核。
核心態執行緒,建立成本高,可以利用多核,切換速度慢。
因此通常我們會在核心中預先建立一些執行緒,並反覆利用這些執行緒。
協程
協程,是一種比執行緒更加輕量級的存在,協程不是被作業系統核心所管理,而完全是由程式所控制(也就是在使用者態執行)。
這樣帶來的好處就是效能得到了很大的提升,不會像執行緒切換那樣消耗資源。
「子程式」
或者稱為函式,在所有語言中都是層級呼叫,比如A呼叫B,B在執行過程中又呼叫了C,C執行完畢返回,B執行完畢返回,最後是A執行完畢。
所以子程式呼叫是透過棧實現的,一個執行緒就是執行一個子程式。
子程式呼叫總是一個入口,一次返回,呼叫順序是明確的。
「協程的特點在於是一個執行緒執行,那和多執行緒比,協程有何優勢?」
極高的執行效率:因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的效能優勢就越明顯; 不需要多執行緒的鎖機制:因為只有一個執行緒,也不存在同時寫變數衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多。
執行緒安全
如果你的程式碼所在的程式中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。
如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。
程式
在系統中正在執行的一個應用程式;程式一旦執行就是程式;是資源分配的最小單位。
在作業系統中能同時執行多個程式;
開機的時候,磁碟的核心映象被匯入記憶體作為一個執行副本,成為核心程式。
程式可以分成「使用者態程式和核心態程式」兩類,使用者態程式通常是應用程式的副本,核心態程式就是核心本身的程式。
如果使用者態程式需要申請資源,比如記憶體,可以透過系統呼叫向核心申請。
每個程式都有獨立的記憶體空間,存放程式碼和資料段等,程式之間的切換會有較大的開銷;
「分時和排程」
每個程式在執行時都會獲得作業系統分配的一個時間片段,如果超出這個時間,就會輪到下一個程式(執行緒)執行。
❝注意,現代作業系統都是直接排程執行緒,不會排程程式。
❞
「分配時間片段」
如下圖所示,程式 1 需要 2 個時間片段,程式 2 只有 1 個時間片段,程式 3 需要 3 個時間片段。
因此當程式 1 執行到一半時,會先掛起,然後程式 2 開始執行;程式 2 一次可以執行完,然後程式 3 開始執行,不過程式 3 一次執行不完,在執行了 1 個時間片段後,程式 1 開始執行;就這樣如此週而復始,這個就是分時技術。
建立程式
使用者想要建立一個程式,最直接的方法就是從命令列執行一個程式,或者雙擊開啟一個應用,但對於程式設計師而言,顯然需要更好的設計。
首先,應該有 API 開啟應用,比如可以透過函式開啟某個應用;
另一方面,如果程式設計師希望執行完一段代價昂貴的初始化過程後,將當前程式的狀態複製好幾份,變成一個個單獨執行的程式,那麼作業系統提供了 fork 指令。
也就是說,每次 fork 會多創造一個克隆的程式,這個克隆的程式,所有狀態都和原來的程式一樣,但是會有自己的地址空間。
如果要創造 2 個克隆程式,就要 fork 兩次。
❝那如果我就是想啟動一個新的程式呢?
❞
作業系統提供了啟動新程式的 API。
如果我就是想用一個新程式執行一小段程式,比如說每次服務端收到客戶端的請求時,我都想用一個程式去處理這個請求。
如果是這種情況,建議你不要單獨啟動程式,而是使用執行緒。
因為程式的建立成本實在太高了,因此不建議用來做這樣的事情:要建立條目、要分配記憶體,特別是還要在記憶體中形成一個個段,分成不同的區域。所以通常,我們更傾向於多建立執行緒。
不同程式語言會自己提供建立執行緒的 API,比如 Java 有 Thread 類;go 有 go-routine(注意不是協程,是執行緒)。
程式狀態
「建立狀態」
程式由建立而產生,建立程式是一個非常複雜的過程,一般需要透過多個步驟才能完成:如首先由程式申請一個空白的程式控制塊(PCB),並向PCB中填寫用於控制和管理程式的資訊;然後為該程式分配執行時所必須的資源;最後,把該程式轉入就緒狀態並插入到就緒佇列中
「就緒狀態」
這是指程式已經準備好執行的狀態,即程式已分配到除CPU以外所有的必要資源後,只要再獲得CPU,便可立即執行,如果系統中有許多處於就緒狀態的程式,通常將它們按照一定的策略排成一個佇列,該佇列稱為就緒佇列,有執行資格,沒有執行權的程式
「執行狀態」
這裡指程式已經獲取CPU,其程式處於正在執行的狀態。對任何一個時刻而言,在單處理機的系統中,只有一個程式處於執行狀態而在多處理機系統中,有多個程式處於執行狀態,既有執行資格,又有執行權的程式
「阻塞狀態」
這裡是指正在執行的程式由於發生某事件(如I/O請求、申請緩衝區失敗等)暫時無法繼續執行的狀態,即程式執行受到阻塞,此時引起程式排程,作業系統把處理機分配給另外一個就緒的程式,而讓受阻的程式處於暫停的狀態,一般將這個暫停狀態稱為阻塞狀態
「終止狀態」
程式間通訊IPC
每個程式各自有不同的使用者地址空間,任何一個程式的全域性變數在另一個程式中都看不到,所以程式之間要交換資料必須透過核心,在核心中開闢一塊緩衝區,程式1把資料從使用者空間拷到核心緩衝區,程式2再從核心緩衝區把資料讀走,核心提供的這種機制稱為程式間通訊
「管道/匿名管道」
管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道。
只能用於父子程式或者兄弟程式之間(具有親緣關係的程式);
單獨構成一種獨立的檔案系統:管道對於管道兩端的程式而言,就是一個檔案,但它不是普通的檔案,它不屬於某種檔案系統,而是自立門戶,單獨構成一種檔案系統,並且只存在與記憶體中。
資料的讀出和寫入:一個程式向管道中寫的內容被管道另一端的程式讀出,寫入的內容每次都新增在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出資料。
「有名管道(FIFO)」
匿名管道,由於沒有名字,只能用於親緣關係的程式間通訊。
為了克服這個缺點,提出了有名管道(FIFO)。
有名管道不同於匿名管道之處在於它提供了一個路徑名與之關聯,以有名管道的檔案形式存在於檔案系統中,這樣,即使與有名管道的建立程式不存在親緣關係的程式,只要可以訪問該路徑,就能夠彼此透過有名管道相互通訊,因此,透過有名管道不相關的程式也能交換資料。
「訊號」
訊號是Linux系統中用於程式間互相通訊或者操作的一種機制,訊號可以在任何時候發給某一程式,而無需知道該程式的狀態。
如果該程式當前並未處於執行狀態,則該訊號就有核心儲存起來,知道該程式回覆執行並傳遞給它為止。
如果一個訊號被程式設定為阻塞,則該訊號的傳遞被延遲,直到其阻塞被取消是才被傳遞給程式。
「訊息佇列」
訊息佇列是存放在核心中的訊息連結串列,每個訊息佇列由訊息佇列識別符號表示。
與管道(無名管道:只存在於記憶體中的檔案;命名管道:存在於實際的磁碟介質或者檔案系統)不同的是訊息佇列存放在核心中,只有在核心重啟(即作業系統重啟)或者顯示地刪除一個訊息佇列時,該訊息佇列才會被真正的刪除。
另外與管道不同的是,訊息佇列在某個程式往一個佇列寫入訊息之前,並不需要另外某個程式在該佇列上等待訊息的到達
「共享記憶體」
使得多個程式可以直接讀寫同一塊記憶體空間,是最快的可用IPC形式,是針對其他通訊機制執行效率較低而設計的。
為了在多個程式間交換資訊,核心專門留出了一塊記憶體區,可以由需要訪問的程式將其對映到自己的私有地址空間,程式就可以直接讀寫這一塊記憶體而不需要進行資料的複製,從而大大提高效率。
由於多個程式共享一段記憶體,因此需要依靠某種同步機制(如訊號量)來達到程式間的同步及互斥。
共享記憶體示意圖:
一旦這樣的記憶體對映到共享它的程式的地址空間,這些程式間資料傳遞不再涉及到核心,換句話說是程式不再透過執行進入核心的系統呼叫來傳遞彼此的資料。
「訊號量」
訊號量是一個計數器,用於多程式對共享資料的訪問,訊號量的意圖在於程式間同步。
為了獲得共享資源,程式需要執行下列操作:
建立一個訊號量:這要求呼叫者指定初始值,對於二值訊號量來說,它通常是1,也可是0。
等待一個訊號量:該操作會測試這個訊號量的值,如果小於0,就阻塞,也稱為P操作。
掛出一個訊號量:該操作將訊號量的值加1,也稱為V操作。
「套接字(Socket)」
套接字是一種通訊機制,憑藉這種機制,客戶/伺服器(即要進行通訊的程式)系統的開發工作既可以在本地單機上進行,也可以跨網路進行。也就是說它可以讓不在同一臺計算機但透過網路連線計算機上的程式進行通訊。
訊號
訊號是程式間通訊機制中唯一的非同步通訊機制,可以看作是非同步通知,通知接收訊號的程式有哪些事情發生了。
也可以簡單理解為訊號是某種形式上的軟中斷
可執行kill -l
檢視Linux支援的訊號列表:
kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
「幾個常用的訊號:」
訊號 | 描述 |
---|---|
SIGHUP | 當使用者退出終端時,由該終端開啟的所有程式都會接收到這個訊號,預設動作為終止程式。 |
SIGINT | 程式終止(interrupt)訊號, 在使用者鍵入INTR字元(通常是Ctrl+C )時發出,用於通知前臺程式組終止程式。 |
SIGQUIT | 和SIGINT 類似, 但由QUIT字元(通常是Ctrl+\ )來控制,程式在因收到SIGQUIT 退出時會產生core 檔案, 在這個意義上類似於一個程式錯誤訊號。 |
SIGKILL | 用來立即結束程式的執行,本訊號不能被阻塞、處理和忽略。 |
SIGTERM | 程式結束(terminate)訊號, 與SIGKILL 不同的是該訊號可以被阻塞和處理。通常用來要求程式自己正常退出。 |
SIGSTOP | 停止(stopped)程式的執行. 注意它和terminate以及interrupt的區別:該程式還未結束, 只是暫停執行,本訊號不能被阻塞, 處理或忽略 |
程式同步
「臨界區」
透過對多執行緒的序列化來訪問公共資源或一段程式碼,速度快,適合控制資料訪問
優點:保證在某一時刻只有一個執行緒能訪問資料的簡便辦法
缺點:雖然臨界區同步速度很快,但卻只能用來同步本程式內的執行緒,而不可用來同步多個程式中的執行緒
「互斥量」
為協調共同對一個共享資源的單獨訪問而設計的
互斥量跟臨界區很相似,比臨界區複雜,互斥物件只有一個,只有擁有互斥物件的執行緒才具有訪問資源的許可權
優點:使用互斥不僅僅能夠在同一應用程式不同執行緒中實現資源的安全共享,而且可以在不同應用程式的執行緒之間實現對資源的安全共享
「訊號量」
為控制一個具有有限數量使用者資源而設計,它允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目,互斥量是訊號量的一種特殊情況,當訊號量的最大資源數=1就是互斥量了
訊號量(Semaphore)是一個整型變數,可以對其執行 down 和 up 操作,也就是常見的 P 和 V 操作
「down」 : 如果訊號量大於 0 ,執行 -1 操作;如果訊號量等於 0,程式睡眠,等待訊號量大於 0; 「up」 :對訊號量執行 +1 操作,喚醒睡眠的程式讓其完成 down 操作。
down 和 up 操作需要被設計成原語,不可分割,通常的做法是在執行這些操作的時候遮蔽中斷。
如果訊號量的取值只能為 0 或者 1,那麼就成為了 「互斥量(Mutex)」 ,0 表示臨界區已經加鎖,1 表示臨界區解鎖。
「事件」
用來通知執行緒有一些事件已發生,從而啟動後繼任務的開始
優點:事件物件透過通知操作的方式來保持執行緒的同步,並且可以實現不同程式中的執行緒同步操作
「管程」
管程有一個重要特性:在一個時刻只能有一個程式使用管程。
程式在無法繼續執行的時候不能一直佔用管程,否則其它程式永遠不能使用管程。
管程引入了 「條件變數」 以及相關的操作:「wait()」 和 「signal()」 來實現同步操作。
對條件變數執行 wait() 操作會導致呼叫程式阻塞,把管程讓出來給另一個程式持有。
signal() 操作用於喚醒被阻塞的程式。
使用訊號量機制實現的生產者消費者問題需要客戶端程式碼做很多控制,而管程把控制的程式碼獨立出來,不僅不容易出錯,也使得客戶端程式碼呼叫更容易。
上下文切換
對於單核單執行緒CPU而言,在某一時刻只能執行一條CPU指令。
上下文切換(Context Switch)是一種將CPU資源從一個程式分配給另一個程式的機制。
從使用者角度看,計算機能夠並行執行多個程式,這恰恰是作業系統透過快速上下文切換造成的結果。
「在切換的過程中,作業系統需要先儲存當前程式的狀態(包括記憶體空間的指標,當前執行完的指令等等),再讀入下一個程式的狀態,然後執行此程式。」
程式排程演算法
「先來先服務排程演算法」
該演算法既可用於作業排程,也可用於程式排程,當在作業排程中採用該演算法時,每次排程都是從後備作業佇列中選擇一個或多個最先進入該佇列的作業,將它們調入記憶體,為它們分配資源、建立程式,然後放入就緒佇列
「短作業優先排程演算法」
從後備佇列中選擇一個或若干個估計執行時間最短的作業,將它們調入記憶體執行
「時間片輪轉法」
每次排程時,把CPU分配給隊首程式,並令其執行一個時間片,時間片的大小從幾ms到幾百ms,當執行的時間片用完時,由一個計時器發出時鐘中斷請求,排程程式便據此訊號來停止該程式的執行,並將它送往就緒佇列的末尾
然後,再把處理機分配給就緒佇列中新的隊首程式,同時也讓它執行一個時間片,這樣就可以保證就緒佇列中的所有程式在一給定的時間內均能獲得一時間片的處理機執行時間
「最短剩餘時間優先」
最短作業優先的搶佔式版本,按剩餘執行時間的順序進行排程,當一個新的作業到達時,其整個執行時間與當前程式的剩餘時間作比較。
如果新的程式需要的時間更少,則掛起當前程式,執行新的程式。否則新的程式等待。
「多級反饋佇列排程演算法」:
前面介紹的幾種程式排程的演算法都有一定的侷限性,如「短程式優先的排程演算法,僅照顧了短程式而忽略了長程式」,多級反饋佇列排程演算法既能使高優先順序的作業得到響應又能使短作業迅速完成,因而它是目前「被公認的一種較好的程式排程演算法」,UNIX 作業系統採取的便是這種排程演算法。
❝舉例:
❞
多級佇列,就是多個佇列執行排程,先考慮最簡單的兩級模型
上圖中設計了兩個優先順序不同的佇列,從下到上優先順序上升,上層佇列排程緊急任務,下層佇列排程普通任務。
只要上層佇列有任務,下層佇列就會讓出執行許可權。
低優先順序佇列可以考慮搶佔 + 優先順序佇列的方式實現,這樣每次執行一個時間片段就可以判斷一下高優先順序的佇列中是否有任務。
高優先順序佇列可以考慮用非搶佔(每個任務執行完才執行下一個)+ 優先順序佇列實現,這樣緊急任務優先順序有個區分,如果遇到十萬火急的情況,就可以優先處理這個任務。
上面這個模型雖然解決了任務間的優先順序問題,但是還是沒有解決短任務先行的問題,可以考慮再增加一些佇列,讓級別更多。
❝比如下圖這個模型:
❞
緊急任務仍然走高優佇列,非搶佔執行。
普通任務先放到優先順序僅次於高優任務的佇列中,並且只分配很小的時間片;如果沒有執行完成,說明任務不是很短,就將任務下調一層。
下面一層,最低優先順序的佇列中時間片很大,長任務就有更大的時間片可以用。
透過這種方式,短任務會在更高優先順序的佇列中執行完成,長任務優先順序會下調,也就類似實現了最短作業優先的問題。
實際操作中,可以有 n 層,一層層把大任務篩選出來,最長的任務,放到最閒的時間去執行,要知道,大部分時間 CPU 不是滿負荷的。
「優先順序排程」
為每個流程分配優先順序,首先執行具有最高優先順序的程式,依此類推,具有相同優先順序的程式以 FCFS 方式執行,可以根據記憶體要求,時間要求或任何其他資源要求來確定優先順序。
守護程式
守護程式是脫離於終端並且在後臺執行的程式,脫離終端是為了避免在執行的過程中的資訊在終端上顯示,並且程式也不會被任何終端所產生的終端資訊所打斷。
守護程式一般的生命週期是系統啟動到系統停止執行。
Linux系統中有很多的守護程式,最典型的就是我們經常看到的服務程式。
當然,我們也經常會利用守護程式來完成很多的系統或者自動化任務。
孤兒程式
父程式早於子程式退出時候子程式還在執行,子程式會成為孤兒程式,Linux會對孤兒程式的處理,把孤兒程式的父程式設為程式號為1的程式,也就是由init程式來託管,init程式負責子程式退出後的善後清理工作
殭屍程式
子程式執行完畢時發現父程式未退出,會向父程式傳送 SIGCHLD 訊號,但父程式沒有使用 wait/waitpid 或其他方式處理 SIGCHLD 訊號來回收子程式,子程式變成為了對系統有害的殭屍程式
子程式退出後留下的程式資訊沒有被收集,會導致佔用的程式控制塊PCB不被釋放,形成殭屍程式,程式已經死去,但是程式資源沒有被釋放掉
「問題及危害」
如果系統中存在大量的殭屍程式,他們的程式號就會一直被佔用,但是系統所能使用的程式號是有限的,系統將因為沒有可用的程式號而導致系統不能產生新的程式
任何一個子程式(init除外)在exit()之後,並非馬上就消失掉,而是留下一個稱為殭屍程式(Zombie)的資料結構,等待父程式處理,這是每個子程式在結束時都要經過的階段,如果子程式在exit()之後,父程式沒有來得及處理,這時用ps命令就能看到子程式的狀態是Z。
如果父程式能及時處理,可能用ps命令就來不及看到子程式的殭屍狀態,但這並不等於子程式不經過殭屍狀態
產生殭屍程式的元兇其實是他們的父程式,殺掉父程式,殭屍程式就變為了孤兒程式,便可以轉交給 init 程式回收處理
死鎖
「產生原因」
系統資源的競爭:系統資源的競爭導致系統資源不足,以及資源分配不當,導致死鎖。
程式執行推進順序不合適:程式在執行過程中,請求和釋放資源的順序不當,會導致死鎖。
「發生死鎖的四個必要條件」
互斥條件:一個資源每次只能被一個程式使用,即在一段時間內某資源僅為一個程式所佔有,此時若有其他程式請求該資源,則請求程式只能等待
請求與保持條件:程式已經保持了至少一個資源,但又提出了新的資源請求時,該資源已被其他程式佔有,此時請求程式被阻塞,但對自己已獲得的資源保持不放
不可剝奪條件:程式所獲得的資源在未使用完畢之前,不能被其他程式強行奪走,即只能由獲得該資源的程式自己來釋放(只能是主動釋放)
迴圈等待條件: 若干程式間形成首尾相接迴圈等待資源的關係
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖
「只要我們破壞其中一個,就可以成功避免死鎖的發生」
其中,互斥這個條件我們沒有辦法破壞,因為我們用鎖為的就是互斥
對於佔用且等待這個條件,我們可以一次性申請所有的資源,這樣就不存在等待了。 對於不可搶佔這個條件,佔用部分資源的執行緒進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源,這樣不可搶佔這個條件就破壞掉了。 對於迴圈等待這個條件,可以靠按序申請資源來預防,所謂按序申請,是指資源是有線性順序的,申請的時候可以先申請資源序號小的,再申請資源序號大的,這樣線性化後自然就不存在迴圈了。
「處理方法」
主要有以下四種方法:
鴕鳥策略 死鎖檢測與死鎖恢復 死鎖預防,破壞4個必要條件 死鎖避免,銀行家演算法
「鴕鳥策略」
把頭埋在沙子裡,假裝根本沒發生問題。
因為解決死鎖問題的代價很高,因此鴕鳥策略這種不採取任務措施的方案會獲得更高的效能。
當發生死鎖時不會對使用者造成多大影響,或發生死鎖的機率很低,可以採用鴕鳥策略。
「死鎖檢測」
不試圖阻止死鎖,而是當檢測到死鎖發生時,採取措施進行恢復。
每種型別一個資源的死鎖檢測
每種型別多個資源的死鎖檢測
「死鎖恢復」
利用搶佔恢復 利用回滾恢復 透過殺死程式恢復
哲學家進餐問題
五個哲學家圍著一張圓桌,每個哲學家面前放著食物。
哲學家的生活有兩種交替活動:吃飯以及思考。
當一個哲學家吃飯時,需要先拿起自己左右兩邊的兩根筷子,並且一次只能拿起一根筷子。
如果所有哲學家同時拿起左手邊的筷子,那麼所有哲學家都在等待其它哲學家吃完並釋放自己手中的筷子,導致死鎖。
哲學家進餐問題可看作是併發程式併發執行時處理共享資源的一個有代表性的問題。
「為了防止死鎖的發生,可以設定兩個條件:」
必須同時拿起左右兩根筷子; 只有在兩個鄰居都沒有進餐的情況下才允許進餐。
銀行家演算法
銀行家演算法的命名是它可以用了銀行系統,當不能滿足所有客戶的需求時,銀行絕不會分配其資金。
當新程式進入系統時,它必須說明其可能需要的每種型別資源例項的最大數量這一數量不可以超過系統資源的總和。
當使用者申請一組資源時,系統必須確定這些資源的分配是否處於安全狀態,如何安全,則分配,如果不安全,那麼程式必須等待指導某個其他程式釋放足夠資源為止。
「安全狀態」
在避免死鎖的方法中,允許程式動態地申請資源,但系統在進行資源分配之前,應先計算此次資源分配的安全性,若此次分配不會導致系統進入不安全狀態,則將資源分配給程式;否則,令程式等待
因此,避免死鎖的實質在於:系統在進行資源分配時,如何使系統不進入不安全狀態
Fork函式
fork
函式用於建立一個與當前程式一樣的子程式,所建立的子程式將複製父程式的程式碼段、資料段、BSS段、堆、棧等所有使用者空間資訊,在核心中作業系統會重新為其申請一個子程式執行的位置。
fork
系統呼叫會透過複製一個現有程式來建立一個全新的程式,新程式被存放在一個叫做任務佇列的雙向迴圈連結串列中,連結串列中的每一項都是型別為task_struct
的程式控制塊PCB
的結構。
每個程式都由獨特換不相同的程式識別符號(PID),透過getpid()
函式可獲取當前程式的程式識別符號,透過getppid()
函式可獲得父程式的程式識別符號。
一個現有的程式可透過呼叫fork
函式建立一個新程式,由fork
建立的新程式稱為子程式child process
,fork
函式被呼叫一次但返回兩次,兩次返回的唯一區別是子程式中返回0而父程式中返回子程式ID。
「為什麼fork
會返回兩次呢?」
因為複製時會複製父程式的堆疊段,所以兩個程式都停留在fork
函式中等待返回,因此會返回兩次,一個是在父程式中返回,一次是在子程式中返回,兩次返回值是不一樣的。
在父程式中將返回新建子程式的程式ID 在子程式中將返回0 若出現錯誤則返回一個負數
因此可以透過fork
的返回值來判斷當前程式是子程式還是父程式。
「fork執行執行流程」
當程式呼叫fork
後控制轉入核心,核心將會做4件事兒:
分配新的記憶體塊和核心資料結構給子程式 將父程式部分資料結構內容(資料空間、堆疊等)複製到子程式 新增子程式到系統程式列表中 fork
返回開始排程器排程
「為什麼pid
在父子程式中不同呢?」
其實就相當於連結串列,程式形成了連結串列,父程式的pid
指向子程式的程式ID,因此子程式沒有子程式,所以PID為0,這裡的pid
相當於連結串列中的指標。
裝置管理
磁碟排程演算法
讀寫一個磁碟塊的時間的影響因素有:
旋轉時間 尋道時間實際的資料傳輸時間
其中,尋道時間最長,因此磁碟排程的主要目標是使磁碟的平均尋道時間最短。
❝先來先服務 FCFS, First Come First Served
❞
按照磁碟請求的順序進行排程,優點是公平和簡單,缺點也很明顯,因為未對尋道做任何最佳化,使平均尋道時間可能較長。
❝最短尋道時間優先,SSTF, Shortest Seek Time First
❞
優先排程與當前磁頭所在磁軌距離最近的磁軌, 雖然平均尋道時間比較低,但是不夠公平,如果新到達的磁軌請求總是比一個在等待的磁軌請求近,那麼在等待的 磁軌請求會一直等待下去,也就是出現飢餓現象,具體來說,兩邊的磁軌請求更容易出現飢餓現象。
❝電梯演算法,SCAN
❞
電梯總是保持一個方向執行,直到該方向沒有請求為止,然後改變執行方向, 電梯演算法(掃描演算法)和電梯的執行過程類似,總是按一個方向來進行磁碟排程,直到該方向上沒有未完成的磁碟 請求,然後改變方向,因為考慮了移動方向,因此所有的磁碟請求都會被滿足,解決了 SSTF 的飢餓問題
記憶體管理
「邏輯地址和實體地址」
我們程式設計一般只有可能和邏輯地址打交道,比如在 C 語言中,指標裡面儲存的數值就可以理解成為記憶體裡的一個地址,這個地址也就是我們說的邏輯地址,邏輯地址由作業系統決定。
實體地址指的是真實實體記憶體中地址,更具體一點來說就是記憶體地址暫存器中的地址,實體地址是記憶體單元真正的地址。
編譯時只需確定變數x存放的相對地址是100 ( 也就是說相對於程式在記憶體中的起始地址而言的地址)。
CPU想要找到x在記憶體中的實際存放位置,只需要用程式的起始地址+100即可。
相對地址又稱邏輯地址,絕對地址又稱實體地址。
「記憶體管理有哪幾種方式」
「塊式管理」:將記憶體分為幾個固定大小的塊,每個塊中只包含一個程式,如果程式執行需要記憶體的話,作業系統就分配給它一塊,如果程式執行只需要很小的空間的話,分配的這塊記憶體很大一部分幾乎被浪費了,這些在每個塊中未被利用的空間,我們稱之為碎片。 「頁式管理」:把主存分為大小相等且固定的一頁一頁的形式,頁較小,相對相比於塊式管理的劃分力度更大,提高了記憶體利用率,減少了碎片,頁式管理透過頁表對應邏輯地址和實體地址。
「段式管理」: 頁式管理雖然提高了記憶體利用率,但是頁式管理其中的頁實際並無任何實際意義, 段式管理把主存分為一段段的,每一段的空間又要比一頁的空間小很多 ,段式管理透過段表對應邏輯地址和實體地址。 「段頁式管理機制:「段頁式管理機制結合了段式管理和頁式管理的優點,簡單來說段頁式管理機制就是把主存先分成若干段,每個段又分成若干頁,也就是說」段頁式管理機制」中段與段之間以及段的內部的都是離散的。
虛擬地址
現代處理器使用的是一種稱為**虛擬定址(Virtual Addressing)**的定址方式
「使用虛擬定址,CPU 需要將虛擬地址翻譯成實體地址,這樣才能訪問到真實的實體記憶體。」
實際上完成虛擬地址轉換為實體地址轉換的硬體是 CPU 中含有一個被稱為**記憶體管理單元(Memory Management Unit, MMU)**的硬體
「為什麼要有虛擬地址空間」
沒有虛擬地址空間的時候,「程式都是直接訪問和操作的都是實體記憶體」。
但是這樣有什麼問題?
使用者程式可以訪問任意記憶體,定址記憶體的每個位元組,這樣就很容易破壞作業系統,造成作業系統崩潰。 想要同時執行多個程式特別困難,比如你想同時執行一個微信和一個 QQ 音樂都不行,為什麼呢?舉個簡單的例子:微信在執行的時候給記憶體地址 1xxx 賦值後,QQ 音樂也同樣給記憶體地址 1xxx 賦值,那麼 QQ 音樂對記憶體的賦值就會覆蓋微信之前所賦的值,這就造成了微信這個程式就會崩潰。
「透過虛擬地址訪問記憶體有以下優勢:」
程式可以使用一系列相鄰的虛擬地址來訪問實體記憶體中不相鄰的大記憶體緩衝區。 程式可以使用一系列虛擬地址來訪問大於可用實體記憶體的記憶體緩衝區。 不同程式使用的虛擬地址彼此隔離,一個程式中的程式碼無法更改正在由另一程式或作業系統使用的實體記憶體。
「MMU如何把虛擬地址翻譯成實體地址的」
對於每個程式,記憶體管理單元MMU都為其儲存一個頁表,該頁表中存放的是虛擬頁面到物理頁面的對映。
每當為一個虛擬頁面尋找到一個物理頁面之後,就在頁表裡增加一條記錄來保留該對映關係,當然,隨著虛擬頁面進出實體記憶體,頁表的內容也會不斷更新變化。
虛擬記憶體
很多時候我們使用點開了很多佔記憶體的軟體,這些軟體佔用的記憶體可能已經遠遠超出了我們電腦本身具有的實體記憶體
透過 「虛擬記憶體」 可以讓程式可以擁有超過系統實體記憶體大小的可用記憶體空間。
另外,虛擬記憶體為每個程式提供了一個一致的、私有的地址空間,它讓每個程式產生了一種自己在獨享主存的錯覺(每個程式擁有一片連續完整的記憶體空間),這樣會更加有效地管理記憶體並減少出錯。
「虛擬記憶體」是計算機系統記憶體管理的一種技術,我們可以手動設定自己電腦的虛擬記憶體
「虛擬記憶體的重要意義是它定義了一個連續的虛擬地址空間」,並且 「把記憶體擴充套件到硬碟空間」
「虛擬記憶體的實現有以下三種方式:」
「請求分頁儲存管理」 :請求分頁是目前最常用的一種實現虛擬儲存器的方法,請求分頁儲存管理系統中,在作業開始執行之前,僅裝入當前要執行的部分段即可執行,假如在作業執行的過程中發現要訪問的頁面不在記憶體,則由處理器通知作業系統按照對應的頁面置換演算法將相應的頁面調入到主存,同時作業系統也可以將暫時不用的頁面置換到外存中。 「請求分段儲存管理」 :請求分段儲存管理方式就如同請求分頁儲存管理方式一樣,在作業開始執行之前,僅裝入當前要執行的部分段即可執行;在執行過程中,可使用請求調入中斷動態裝入要訪問但又不在記憶體的程式段;當記憶體空間已滿,而又需要裝入新的段時,根據置換功能適當調出某個段,以便騰出空間而裝入新的段。 「請求段頁式儲存管理」
不管是上面那種實現方式,我們一般都需要:
❝一定容量的記憶體和外存:在載入程式的時候,只需要將程式的一部分裝入記憶體,而將其他部分留在外存,然後程式就可以執行了;
❞
缺頁中斷
如果「需執行的指令或訪問的資料尚未在記憶體」(稱為缺頁或缺段),則由處理器通知作業系統將相應的頁面或段「調入到記憶體」,然後繼續執行程式;
在分頁系統中,一個虛擬頁面既有可能在實體記憶體,也有可能儲存在磁碟上。
如果CPU發出的虛擬地址對應的頁面不在實體記憶體,就將產生一個缺頁中斷,而缺頁中斷服務程式負責將需要的虛擬頁面找到並載入到記憶體。
缺頁中斷的處理步驟如下,省略了中間很多的步驟,只保留最核心的幾個步驟:
頁面置換演算法
當發生缺頁中斷時,如果當前記憶體中並沒有空閒的頁面,作業系統就必須在記憶體選擇一個頁面將其移出記憶體,以便為即將調入的頁面讓出空間。
用來選擇淘汰哪一頁的規則叫做頁面置換演算法,我們可以把頁面置換演算法看成是淘汰頁面的規則
「OPT 頁面置換演算法(最佳頁面置換演算法)」 :該置換演算法所選擇的被淘汰頁面將是以後永不使用的,或者是在最長時間內不再被訪問的頁面,這樣可以保證獲得最低的缺頁率,但由於人們目前無法預知程式在記憶體下的若千頁面中哪個是未來最長時間內不再被訪問的,因而該演算法無法實現,一般作為衡量其他置換演算法的方法。
「FIFO(First In First Out) 頁面置換演算法(先進先出頁面置換演算法)」 : 總是淘汰最先進入記憶體的頁面,即選擇在記憶體中駐留時間最久的頁面進行淘汰。
「LRU (Least Currently Used)頁面置換演算法(最近最久未使用頁面置換演算法)」 :LRU演算法賦予每個頁面一個訪問欄位,用來記錄一個頁面自上次被訪問以來所經歷的時間 T,當須淘汰一個頁面時,選擇現有頁面中其 T 值最大的,即最近最久未使用的頁面予以淘汰。
「LFU (Least Frequently Used)頁面置換演算法(最少使用頁面置換演算法)」 : 該置換演算法選擇在之前時期使用最少的頁面作為淘汰頁。
區域性性原理
區域性性原理是虛擬記憶體技術的基礎,正是因為程式執行具有區域性性原理,才可以只裝入部分程式到記憶體就開始執行。
區域性性原理表現在以下兩個方面:
「時間區域性性」 :如果程式中的某條指令一旦執行,不久以後該指令可能再次執行;如果某資料被訪問過,不久以後該資料可能再次被訪問,產生時間區域性性的典型原因,是由於在程式中存在著大量的迴圈操作。 「空間區域性性」 :一旦程式訪問了某個儲存單元,在不久之後,其附近的儲存單元也將被訪問,即程式在一段時間內所訪問的地址,可能集中在一定的範圍之內,這是因為指令通常是順序存放、順序執行的,資料也一般是以向量、陣列、表等形式簇聚儲存的。
時間區域性性是透過將近來使用的指令和資料儲存到「快取記憶體儲存器」中,並使用快取記憶體的層次結構實現。
空間區域性性通常是使用較大的快取記憶體,並將預取機制整合到快取記憶體控制邏輯中實現。
頁表
作業系統將虛擬記憶體分塊,每個小塊稱為一個頁(Page);真實記憶體也需要分塊,每個小塊我們稱為一個 Frame。
Page 到 Frame 的對映,需要一種叫作頁表的結構。
上圖展示了 Page、Frame 和頁表 (PageTable)三者之間的關係。
Page 大小和 Frame 大小通常相等,頁表中記錄的某個 Page 對應的 Frame 編號。
頁表也需要儲存空間,比如虛擬記憶體大小為 10G, Page 大小是 4K,那麼需要 10G/4K = 2621440 個條目。
如果每個條目是 64bit,那麼一共需要 20480K = 20M 頁表,作業系統在記憶體中劃分出小塊區域給頁表,並負責維護頁表。
「頁表維護了虛擬地址到真實地址的對映。」
每次程式使用記憶體時,需要把虛擬記憶體地址換算成實體記憶體地址,換算過程分為以下 3 個步驟:
透過虛擬地址計算 Page 編號;
查頁表,根據 Page 編號,找到 Frame 編號;
將虛擬地址換算成實體地址。
多級頁表
引入多級頁表的主要目的是為了避免把全部頁表一直放在記憶體中佔用過多空間,特別是那些根本就不需要的頁表就不需要保留在記憶體中
「一級頁表:」
假如實體記憶體中一共有1048576個頁,那麼頁表就需要總共就是1048576 * 4B = 4M
。
也就是說我需要4M連續的記憶體來存放這個頁表,也就是一級頁表。
隨著虛擬地址空間的增大,存放頁表所需要的連續空間也會增大,在作業系統記憶體緊張或者記憶體碎片較多時,這無疑會帶來額外的開銷。
頁表定址是用暫存器來確定一級頁表地址的,所以一級頁表的地址必須指向確定的物理頁,否則就會出現錯誤,所以如果用一級頁表的話,就必須把全部的頁表都載入進去。
「二級頁表:」
而使用二級頁表的話,只需要載入一個頁目錄表(一級頁表),大小為4K,可以管理1024個二級頁表。
可能你會有疑問,這1024個二級頁表也是需要記憶體空間的,這下反而需要4MB+4KB的記憶體,反而更多了。
其實二級頁表並不是一定要存在記憶體中的,記憶體中只需要一個一級頁表地址存在存器即可,二級頁表可以使用缺頁中斷從外存移入記憶體。
「多級頁表屬於時間換空間的典型場景」
快表
為了解決虛擬地址到實體地址的轉換速度,作業系統在「頁表方案」基礎之上引入了「快表」來加速虛擬地址到實體地址的轉換
我們可以把快表理解為一種特殊的「高速緩衝儲存器(Cache)」,其中的內容是頁表的一部分或者全部內容,作為頁表的 Cache,它的作用與頁表相似,但是提高了訪問速率,由於採用頁表做地址轉換,讀寫記憶體資料時 CPU 要訪問兩次主存,有了快表,有時只要訪問一次高速緩衝儲存器,一次主存,這樣可加速查詢並提高指令執行速度。
「使用快表之後的地址轉換流程是這樣的:」
根據虛擬地址中的頁號查快表; 如果該頁在快表中,直接從快表中讀取相應的實體地址; 如果該頁不在快表中,就訪問記憶體中的頁表,再從頁表中得到實體地址,同時將頁表中的該對映表項新增到快表中; 當快表填滿後,又要登記新頁時,就按照一定的淘汰策略淘汰掉快表中的一個頁。
記憶體管理單元
在 CPU 中一個小型的裝置——記憶體管理單元(MMU)
當 CPU 需要執行一條指令時,如果指令中涉及記憶體讀寫操作,CPU 會把虛擬地址給 MMU,MMU 自動完成虛擬地址到真實地址的計算;然後,MMU 連線了地址匯流排,幫助 CPU 操作真實地址。
在不同 CPU 的 MMU 可能是不同的,因此這裡會遇到很多跨平臺的問題。
解決跨平臺問題不但有繁重的工作量,更需要高超的程式設計技巧。
動態分割槽分配演算法
記憶體分配演算法,大體來說分為:「連續式分配 與 非連續式分配」
連續式分配就是把所以要執行的程式 「完整的,有序的」 存入記憶體,連續式分配又可以分為「固定分割槽分配 和 動態分割槽分配」
非連續式分配就是把要執行的程式按照一定規則進行拆分,顯然這樣更有效率,現在的作業系統通常也都是採用這種方式分配記憶體
所謂動態分割槽分配,就是指「記憶體在初始時不會劃分割槽域,而是會在程式裝入時,根據所要裝入的程式大小動態地對記憶體空間進行劃分,以提高記憶體空間利用率,降低碎片的大小」
動態分割槽分配演算法有以下四種:
❝首次適應演算法(First Fit)
❞
空閒分割槽以地址遞增的次序連結,分配記憶體時順序查詢,找到大小滿足要求的第一個空閒分割槽就進行分配
❝鄰近適應演算法(Next Fit)
❞
又稱迴圈首次適應法,由首次適應法演變而成,不同之處是分配記憶體時從上一次查詢結束的位置開始繼續查詢
❝最佳適應演算法(Best Fit)
❞
空閒分割槽按容量遞增形成分割槽鏈,找到第一個能滿足要求的空閒分割槽就進行分配
❝最壞適應演算法(Next Fit)
❞
又稱最大適應演算法,空閒分割槽以容量遞減的次序連結,找到第一個能滿足要求的空閒分割槽(也就是最大的分割槽)就進行分配
「總結」
首次適應不僅最簡單,通常也是最好最快,不過首次適應演算法會使得記憶體低地址部分出現很多小的空閒分割槽,而每次查詢都要經過這些分割槽,因此也增加了查詢的開銷。
鄰近演算法試圖解決這個問題,但實際上,它常常會導致在記憶體的末尾分配空間分裂成小的碎片,它通常比首次適應演算法結果要差。
最佳適應演算法導致大量碎片,最壞適應演算法導致沒有大的空間。
記憶體覆蓋
覆蓋與交換技術是在程式用來擴充記憶體的兩種方法。
早期的計算機系統中,主存容量很小,雖然主存中僅存放一道使用者程式,但是儲存空間放不下使用者程式的現象也經常發生,這一矛盾可以用覆蓋技術來解決。
「覆蓋的基本思想是:」
由於程式執行時並非任何時候都要訪問程式及資料的各個部分(尤其是大程式),因此可以把使用者空間分成一個固定區和若干個覆蓋區。
將經常活躍的部分放在固定區,其餘部分按呼叫關係分段。
首先將那些即將要訪問的段放入覆蓋區,其他段放在外存中,在需要呼叫前,系統再將其調入覆蓋區,替換覆蓋區中原有的段。
覆蓋技術的特點是打破了必須將一個程式的全部資訊裝入主存後才能執行的限制,但當同時執行程式的程式碼量大於主存時仍不能執行。
記憶體交換
「交換的基本思想」
把處於等待狀態(或在CPU排程原則下被剝奪執行權利)的程式從記憶體移到輔存,把記憶體空間騰出來,這一過程又叫換出;
把準備好競爭CPU執行的程式從輔存移到記憶體,這一過程又稱為換入。
❝例如,有一個CPU釆用時間片輪轉排程演算法的多道程式環境。
❞
時間片到,記憶體管理器將剛剛執行過的程式換出,將另一程式換入到剛剛釋放的記憶體空間中。
同時,CPU排程器可以將時間片分配給其他已在記憶體中的程式。
每個程式用完時間片都與另一程式交換。
理想情況下,記憶體管理器的交換過程速度足夠快,總有程式在記憶體中可以執行。
❝交換技術主要是在不同程式(或作業)之間進行,而覆蓋則用於同一個程式或程式中。
❞
由於覆蓋技術要求給出程式段之間的覆蓋結構,使得其對使用者和程式設計師不透明,所以對於主存無法存放使用者程式的矛盾
現代作業系統是透過虛擬記憶體技術來解決的,覆蓋技術則已成為歷史;而交換技術在現代作業系統中仍具有較強的生命力。
常見面試題
「程式、執行緒的區別」
作業系統會以程式為單位,分配系統資源(CPU時間片、記憶體等資源),程式是資源分配的最小單位。
排程:執行緒作為CPU排程和分配的基本單位,程式作為擁有資源的基本單位;
併發性:不僅程式之間可以併發執行,同一個程式的多個執行緒之間也可併發執行;
❝擁有資源:
❞
程式是擁有資源的一個獨立單位,執行緒不擁有系統資源,但可以訪問隸屬於程式的資源。
程式所維護的是程式所包含的資源(靜態資源), 如:地址空間,開啟的檔案控制程式碼集,檔案系統狀態,訊號處理handler等;
執行緒所維護的執行相關的資源(動態資源),如:執行棧,排程相關的控制資訊,待處理的訊號集等;
❝系統開銷:
❞
在建立或撤消程式時,由於系統都要為之分配和回收資源,導致系統的開銷明顯大於建立或撤消執行緒時的開銷。
但是程式有獨立的地址空間,一個程式崩潰後,在保護模式下不會對其它程式產生影響,而執行緒只是一個程式中的不同執行路徑。
執行緒有自己的堆疊和區域性變數,但執行緒之間沒有單獨的地址空間,一個程式死掉就等於所有的執行緒死掉,所以多程式的程式要比多執行緒的程式健壯,但在程式切換時,耗費資源較大,效率要差一些。
「一個程式可以建立多少執行緒」
理論上,一個程式可用虛擬空間是2G,預設情況下,執行緒的棧的大小是1MB,所以理論上最多隻能建立2048個執行緒。
如果要建立多於2048的話,必須修改編譯器的設定。
在一般情況下,你不需要那麼多的執行緒,過多的執行緒將會導致大量的時間浪費線上程切換上,給程式執行效率帶來負面影響。
「外中斷和異常有什麼區別」
外中斷是指由 CPU 執行指令以外的事件引起,如 I/O 完成中斷,表示裝置輸入/輸出處理已經完成,處理器能夠傳送下一個輸入/輸出請求,此外還有時鐘中斷、控制檯中斷等。
而異常時由 CPU 執行指令的內部事件引起,如非法操作碼、地址越界、算術溢位等。
「解決Hash衝突四種方法」
開放定址法
開放定址法就是一旦發生了衝突,就去尋找下一個空的雜湊地址,只要雜湊表足夠大,空的雜湊地址總能找到,並將記錄存入。
鏈地址法
將雜湊表的每個單元作為連結串列的頭結點,所有雜湊地址為i的元素構成一個同義詞連結串列。即發生衝突時就把該關鍵字鏈在以該單元為頭結點的連結串列的尾部。
再雜湊法
當雜湊地址發生衝突用其他的函式計算另一個雜湊函式地址,直到衝突不再產生為止。
建立公共溢位區
將雜湊表分為基本表和溢位表兩部分,發生衝突的元素都放入溢位表中。
「分頁機制和分段機制有哪些共同點和區別」
共同點
分頁機制和分段機制都是為了提高記憶體利用率,較少記憶體碎片。 頁和段都是離散儲存的,所以兩者都是離散分配記憶體的方式。但是,每個頁和段中的記憶體是連續的。
區別
頁的大小是固定的,由作業系統決定;而段的大小不固定,取決於我們當前執行的程式。 分頁僅僅是為了滿足作業系統記憶體管理的需求,而段是邏輯資訊的單位,在程式中可以體現為程式碼段,資料段,能夠更好滿足使用者的需要。 分頁是一維地址空間,分段是二維的。
「介紹一下幾種典型的鎖」
❝讀寫鎖
❞
可以同時進行多個讀 寫者必須互斥(只允許一個寫者寫,也不能讀者寫者同時進行) 寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)
❝互斥鎖
❞
一次只能一個執行緒擁有互斥鎖,其他執行緒只有等待
互斥鎖是在搶鎖失敗的情況下主動放棄CPU進入睡眠狀態直到鎖的狀態改變時再喚醒,而作業系統負責執行緒排程,為了實現鎖的狀態發生改變時喚醒阻塞的執行緒或者程式,需要把鎖交給作業系統管理,所以互斥鎖在加鎖操作時涉及上下文的切換。
互斥鎖實際的效率還是可以讓人接受的,加鎖的時間大概100ns左右,而實際上互斥鎖的一種可能的實現是先自旋一段時間,當自旋的時間超過閥值之後再將執行緒投入睡眠中,因此在併發運算中使用互斥鎖(每次佔用鎖的時間很短)的效果可能不亞於使用自旋鎖
❝條件變數
❞
互斥鎖一個明顯的缺點是他只有兩種狀態:鎖定和非鎖定。
而條件變數透過允許執行緒阻塞和等待另一個執行緒傳送訊號的方法彌補了互斥鎖的不足,他常和互斥鎖一起使用,以免出現競態條件。
當條件不滿足時,執行緒往往解開相應的互斥鎖並阻塞執行緒然後等待條件發生變化。
一旦其他的某個執行緒改變了條件變數,他將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒。
總的來說「互斥鎖是執行緒間互斥的機制,條件變數則是同步機制。」
❝自旋鎖
❞
如果進執行緒無法取得鎖,進執行緒不會立刻放棄CPU時間片,而是一直迴圈嘗試獲取鎖,直到獲取為止。
如果別的執行緒長時期佔有鎖,那麼自旋就是在浪費CPU做無用功,但是自旋鎖一般應用於加鎖時間很短的場景,這個時候效率比較高。
雖然它的效率比互斥鎖高,但是它也有些不足之處:
自旋鎖一直佔用CPU,在未獲得鎖的情況下,一直進行自旋,所以佔用著CPU,如果不能在很短的時間內獲得鎖,無疑會使CPU效率降低。 在用自旋鎖時有可能造成死鎖,當遞迴呼叫時有可能造成死鎖。
「如何讓程式後臺執行」
1.命令後面加上&即可,實際上,這樣是將命令放入到一個作業佇列中了
2.ctrl + z 掛起程式,使用jobs檢視序號,在使用bg %序號後臺執行程式
3.nohup + &,將標準輸出和標準錯誤預設會被重定向到 nohup.out
檔案中,忽略所有結束通話(SIGHUP)訊號
nohup ping
4.執行指令前面 + setsid,使其父程式變成init程式,不受SIGHUP訊號的影響
[root@pvcent107 ~]# setsid ping
[root@pvcent107 ~]# ps -ef |grep
root 31094 1 0 07:28 ? 00:00:00 ping
root 31102 29217 0 07:29 pts/4 00:00:00 grep
上例中我們的程式 ID(PID)為31094,而它的父 ID(PPID)為1(即為 init 程式 ID),並不是當前終端的程式 ID。
❝5.將命令+ &放在()括號中,也可以是程式不受HUP訊號的影響
❞
[root@pvcent107 ~]# (ping )
「異常和中斷的區別」
❝中斷
❞
當我們在敲擊鍵盤的同時就會產生中斷,當硬碟讀寫完資料之後也會產生中斷,所以,我們需要知道,中斷是由硬體裝置產生的,而它們從物理上說就是電訊號,之後,它們透過中斷控制器傳送給CPU,接著CPU判斷收到的中斷來自於哪個硬體裝置(這定義在核心中),最後,由CPU傳送給核心,有核心處理中斷。
下面這張圖顯示了中斷處理的流程:
❝異常
❞
CPU處理程式的時候一旦程式不在記憶體中,會產生缺頁異常;當執行除法程式時,當除數為0時,又會產生除0異常。
「異常是由CPU產生的,同時,它會傳送給核心,要求核心處理這些異常」
下面這張圖顯示了異常處理的流程:
❝相同點
❞
最後都是由CPU傳送給核心,由核心去處理 處理程式的流程設計上是相似的
❝不同點
❞
產生源不相同,異常是由CPU產生的,而中斷是由硬體裝置產生的 核心需要根據是異常還是中斷呼叫不同的處理程式 中斷不是時鐘同步的,這意味著中斷可能隨時到來;異常由於是CPU產生的,所以它是時鐘同步的 當處理中斷時,處於中斷上下文中;處理異常時,處於程式上下文中
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2924571/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 作業系統相關知識總結作業系統
- jvm、gc、作業系統等基礎知識總結JVMGC作業系統
- 作業系統知識點作業系統
- 作業系統總結作業系統
- docker常用知識點總結Docker
- mysql 常用知識點總結MySql
- RabbitMQ 常用知識點總結MQ
- # Redis 常用知識總結(一)Redis
- 作業系統常見知識點作業系統
- 《作業系統》分析與總結作業系統
- 作業系統寫題總結作業系統
- 作業系統概念知識點複習作業系統
- MySQL 常用易混淆知識點總結MySql
- 作業系統知識回顧(4)-死鎖作業系統
- ES6常用知識點總結(上)
- ES6常用知識點總結(下)
- 5萬字、97 張圖總結作業系統核心知識點作業系統
- 作業系統(二):作業系統結構作業系統
- [作業系統]程式基礎知識記錄(上)作業系統
- 【知識分享】伺服器作業系統有哪些伺服器作業系統
- 架構知識體系總結架構
- jQuery常用的一些知識點總結jQuery
- c語言常用小知識點總結1C語言
- 【分享篇】常用的八個Linux作業系統彙總!Linux作業系統
- [MongoDB知識體系] 一文全面總結MongoDB知識體系MongoDB
- [Redis知識體系] 一文全面總結Redis知識體系Redis
- 408 知識點筆記——作業系統(檔案系統、裝置管理)筆記作業系統
- Redis知識體系總結(2021版)Redis
- Java知識體系總結(2021版)Java
- MySQL基礎知識和常用命令總結MySql
- 【乾貨分享】常用的八個Linux作業系統彙總!Linux作業系統
- 作業系統結構作業系統
- JavaWeb基礎知識總結:如何系統學習spring boot?JavaWebSpring Boot
- 收藏!系統運維中網路知識實用總結運維
- servlet知識總結Servlet
- Cookie知識總結(-)Cookie
- MySQL知識總結MySql
- 知識點總結