一文帶你吃透作業系統

qi66發表於2023-03-14


文章字數大約1.9萬字,閱讀大概需要65分鐘,建議收藏後慢慢閱讀!!!

1. 程式、執行緒管理

  1. 程式和執行緒基礎知識

    程式:程式是系統進行資源分配和排程的一個獨立單位,是系統中的併發執行的單位。

    執行緒:執行緒是程式的一個實體,也是 CPU 排程和分派的基本單位,它是比程式更小的能獨立執行的基本單位,有時又被稱為輕權程式或輕量級程式。

    1. 程式

      執行中的程式,就被稱為「程式」(Process)。

      在一個程式的活動期間至少具備三種基本狀態,即執行狀態、就緒狀態、阻塞狀態。

      • 執行狀態(Running):該時刻程式佔用 CPU;
      • 就緒狀態(Ready):可執行,由於其他程式處於執行狀態而暫時停止執行;
      • 阻塞狀態(Blocked):該程式正在等待某一事件發生(如等待輸入/輸出操作的完成)而暫時停止執行,這時,即使給它CPU控制權,它也無法執行;

      當然,程式還有另外兩個基本狀態:

      • 建立狀態(new):程式正在被建立時的狀態;
      • 結束狀態(Exit):程式正在從系統中消失時的狀態;

      掛起狀態可以分為兩種:

      • 阻塞掛起狀態:程式在外存(硬碟)並等待某個事件的出現;
      • 就緒掛起狀態:程式在外存(硬碟),但只要進入記憶體,即刻立刻執行;

      PCB 程式控制塊 是程式存在的唯一標識

      PCB 具體包含什麼資訊呢?

      程式描述資訊:

      • 程式識別符號:標識各個程式,每個程式都有一個並且唯一的識別符號;
      • 使用者識別符號:程式歸屬的使用者,使用者識別符號主要為共享和保護服務;

      程式控制和管理資訊:

      • 程式當前狀態,如 new、ready、running、waiting 或 blocked 等;
      • 程式優先順序:程式搶佔 CPU 時的優先順序;

      資源分配清單:

      • 有關記憶體地址空間或虛擬地址空間的資訊,所開啟檔案的列表和所使用的 I/O 裝置資訊。

      CPU 相關資訊:

      • CPU 中各個暫存器的值,當程式被切換時,CPU 的狀態資訊都會被儲存在相應的 PCB 中,以便程式重新執行時,能從斷點處繼續執行。
    2. 執行緒

      執行緒是程式當中的一條執行流程。

      同一個程式內多個執行緒之間可以共享程式碼段、資料段、開啟的檔案等資源,但每個執行緒各自都有一套獨立的暫存器和棧,這樣可以確保執行緒的控制流是相對獨立的。

      執行緒的實現

      主要有三種執行緒的實現方式:

      • 使用者執行緒(*User Thread*):在使用者空間實現的執行緒,不是由核心管理的執行緒,是由使用者態的執行緒庫來完成執行緒的管理;
      • 核心執行緒(*Kernel Thread*):在核心中實現的執行緒,是由核心管理的執行緒;
      • 輕量級程式(*LightWeight Process*):在核心中來支援使用者執行緒;

      第一種關係是多對一的關係,也就是多個使用者執行緒對應同一個核心執行緒

      第二種是一對一的關係,也就是一個使用者執行緒對應一個核心執行緒

      第三種是多對多的關係,也就是多個使用者執行緒對應到多個核心執行緒

      使用者執行緒的整個執行緒管理和排程,作業系統是不直接參與的,而是由使用者級執行緒庫函式來完成執行緒的管理,包括執行緒的建立、終止、同步和排程等。

      執行緒的優點:

      • 一個程式中可以同時存在多個執行緒;
      • 各個執行緒之間可以併發執行;
      • 各個執行緒之間可以共享地址空間和檔案等資源;

      執行緒的缺點:

      • 當程式中的一個執行緒崩潰時,會導致其所屬程式的所有執行緒崩潰(這裡是針對 C/C++ 語言,Java語言中的執行緒奔潰不會造成程式崩潰

      核心執行緒是由作業系統管理的,執行緒對應的 TCB 自然是放在作業系統裡的,這樣執行緒的建立、終止和管理都是由作業系統負責。

  2. 程式/執行緒上下文切換

    1. 程式

      一個程式切換到另一個程式執行,稱為程式的上下文切換

      程式的上下文切換到底是切換什麼呢?

      程式是由核心管理和排程的,所以程式的切換隻能發生在核心態。

      所以,程式的上下文切換不僅包含了虛擬記憶體、棧、全域性變數等使用者空間的資源,還包括了核心堆疊、暫存器等核心空間的資源。

      通常,會把交換的資訊儲存在程式的 PCB,當要執行另外一個程式的時候,我們需要從這個程式的 PCB 取出上下文,然後恢復到 CPU 中,這使得這個程式可以繼續執行

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

      • 為了保證所有程式可以得到公平排程,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流分配給各個程式。這樣,當某個程式的時間片耗盡了,程式就從執行狀態變為就緒狀態,系統從就緒佇列選擇另外一個程式執行;
      • 程式在系統資源不足(比如記憶體不足)時,要等到資源滿足後才可以執行,這個時候程式也會被掛起,並由系統排程其他程式執行;
      • 當程式透過睡眠函式 sleep 這樣的方法將自己主動掛起時,自然也會重新排程;
      • 當有優先順序更高的程式執行時,為了保證高優先順序程式的執行,當前程式會被掛起,由高優先順序程式來執行;
      • 發生硬體中斷時,CPU 上的程式會被中斷掛起,轉而執行核心中的中斷服務程式;
    2. 執行緒

      1. 執行緒上下文切換的是什麼?

        這還得看執行緒是不是屬於同一個程式:

        1. 當兩個執行緒不是屬於同一個程式,則切換的過程就跟程式上下文切換一樣;
        2. 當兩個執行緒是屬於同一個程式,因為虛擬記憶體是共享的,所以在切換時,虛擬記憶體這些資源就保持不動,只需要切換執行緒的私有資料、暫存器等不共享的資料

        所以,執行緒的上下文切換相比程式,開銷要小很多。

  3. 程式/執行緒間通訊方式

    程式間通訊(IPC,InterProcess Communication)是指在不同程式之間傳播或交換資訊。IPC 的方式通常有管道(包括無名管道和命名管道)、訊息佇列、訊號量、共享儲存、Socket、Streams 等。其中 Socket 和 Streams 支援不同主機上的兩個程式 IPC。

    管道

    1. 它是半雙工的,具有固定的讀端和寫端;
    2. 它只能用於父子程式或者兄弟程式之間的程式的通訊;
    3. 它可以看成是一種特殊的檔案,對於它的讀寫也可以使用普通的 read、write 等函式。但是它不是普通的檔案,並不屬於其他任何檔案系統,並且只存在於記憶體中。

    命名管道

    1. FIFO 可以在無關的程式之間交換資料,與無名管道不同;
    2. FIFO 有路徑名與之相關聯,它以一種特殊裝置檔案形式存在於檔案系統中。

    訊息佇列

    1. 訊息佇列,是訊息的連結表,存放在核心中。一個訊息佇列由一個識別符號 ID 來標識;
    2. 訊息佇列是面向記錄的,其中的訊息具有特定的格式以及特定的優先順序;
    3. 訊息佇列獨立於傳送與接收程式。程式終止時,訊息佇列及其內容並不會被刪除;
    4. 訊息佇列可以實現訊息的隨機查詢,訊息不一定要以先進先出的次序讀取,也可以按訊息的型別讀取。

    訊號量

    1. 訊號量(semaphore)是一個計數器。用於實現程式間的互斥與同步,而不是用於儲存程式間通訊資料;
    2. 訊號量用於程式間同步,若要在程式間傳遞資料需要結合共享記憶體;
    3. 訊號量基於作業系統的 PV 操作,程式對訊號量的操作都是原子操作;
    4. 每次對訊號量的 PV 操作不僅限於對訊號量值加 1 或減 1,而且可以加減任意正整數;
    5. 支援訊號量組。

    共享記憶體

    1. 共享記憶體(Shared Memory),指兩個或多個程式共享一個給定的儲存區;
    2. 共享記憶體是最快的一種 IPC,因為程式是直接對記憶體進行存取。

    Socket通訊

    前面說到的通訊機制,都是工作於同一臺主機,如果要與不同主機的程式間通訊,那麼就需要 Socket 通訊了。Socket 實際上不僅用於不同的主機程式間通訊,還可以用於本地主機程式間通訊,可根據建立 Socket 的型別不同,分為三種常見的通訊方式,一個是基於 TCP 協議的通訊方式,一個是基於 UDP 協議的通訊方式,一個是本地程式間通訊方式。

    以上,就是程式間通訊的主要機制了。你可能會問了,那執行緒通訊間的方式呢?

    同個程式下的執行緒之間都是共享程式的資源,只要是共享變數都可以做到執行緒間通訊,比如全域性變數,所以對於執行緒間關注的不是通訊方式,而是關注多執行緒競爭共享資源的問題,訊號量也同樣可以線上程間實現互斥與同步:

    • 互斥的方式,可保證任意時刻只有一個執行緒訪問共享資源;
    • 同步的方式,可保證執行緒 A 應線上程 B 之前執行;
  4. 執行緒、程式崩潰發生什麼

    一般來說如果執行緒是因為非法訪問記憶體引起的崩潰,那麼程式肯定會崩潰,為什麼系統要讓程式崩潰呢,這主要是因為在程式中,各個執行緒的地址空間是共享的,既然是共享,那麼某個執行緒對地址的非法訪問就會導致記憶體的不確定性,進而可能會影響到其他執行緒,這種操作是危險的,作業系統會認為這很可能導致一系列嚴重的後果,於是乾脆讓整個程式崩潰

    崩潰機制

    1. CPU 執行正常的程式指令
    2. 呼叫 kill 系統呼叫向程式傳送訊號
    3. 程式收到作業系統發的訊號,CPU 暫停當前程式執行,並將控制權轉交給作業系統
    4. 呼叫 kill 系統呼叫向程式傳送訊號(假設為 11,即 SIGSEGV,一般非法訪問記憶體報的都是這個錯誤)
    5. 作業系統根據情況執行相應的訊號處理程式(函式),一般執行完訊號處理程式邏輯後會讓程式退出

    注意上面的第五步,如果程式沒有註冊自己的訊號處理函式,那麼作業系統會執行預設的訊號處理程式(一般最後會讓程式退出),但如果註冊了,則會執行自己的訊號處理函式,這樣的話就給了程式一個垂死掙扎的機會,它收到 kill 訊號後,可以呼叫 exit() 來退出,但也可以使用 sigsetjmp,siglongjmp 這兩個函式來恢復程式的執行

  5. 守護程式、殭屍程式和孤兒程式

    守護程式

    指在後臺執行的,沒有控制終端與之相連的程式。它獨立於控制終端,週期性地執行某種任務。Linux的大多數伺服器就是用守護程式的方式實現的,如web伺服器程式http等

    建立守護程式要點:

    (1)讓程式在後臺執行。方法是呼叫fork()產生一個子程式,然後使父程式退出。

    (2)呼叫setsid()建立一個新對話期。控制終端、登入會話和程式組通常是從父程式繼承下來的,守護程式要擺脫它們,不受它們的影響,方法是呼叫setsid()使程式成為一個會話組長。setsid()呼叫成功後,程式成為新的會話組長和程式組長,並與原來的登入會話、程式組和控制終端脫離。

    (3)禁止程式重新開啟控制終端。經過以上步驟,程式已經成為一個無終端的會話組長,但是它可以重新申請開啟一個終端。為了避免這種情況發生,可以透過使程式不再是會話組長來實現。再一次透過fork()建立新的子程式,使呼叫fork的程式退出。

    (4)關閉不再需要的檔案描述符。子程式從父程式繼承開啟的檔案描述符。如不關閉,將會浪費系統資源,造成程式所在的檔案系統無法卸下以及引起無法預料的錯誤。首先獲得最高檔案描述符值,然後用一個迴圈程式,關閉0到最高檔案描述符值的所有檔案描述符。

    (5)將當前目錄更改為根目錄。

    (6)子程式從父程式繼承的檔案建立遮蔽字可能會拒絕某些許可權。為防止這一點,使用unmask(0)將遮蔽字清零。

    (7)處理SIGCHLD訊號。對於伺服器程式,在請求到來時往往生成子程式處理請求。如果子程式等待父程式捕獲狀態,則子程式將成為殭屍程式(zombie),從而佔用系統資源。如果父程式等待子程式結束,將增加父程式的負擔,影響伺服器程式的併發效能。在Linux下可以簡單地將SIGCHLD訊號的操作設為SIG_IGN。這樣,子程式結束時不會產生殭屍程式。

    孤兒程式

    如果父程式先退出,子程式還沒退出,那麼子程式的父程式將變為init程式。(注:任何一個程式都必須有父程式)。

    一個父程式退出,而它的一個或多個子程式還在執行,那麼那些子程式將成為孤兒程式。孤兒程式將被init程式(程式號為1)所收養,並由init程式對它們完成狀態收集工作。

    殭屍程式

    如果子程式先退出,父程式還沒退出,那麼子程式必須等到父程式捕獲到了子程式的退出狀態才真正結束,否則這個時候子程式就成為殭屍程式。

    設定殭屍程式的目的是維護子程式的資訊,以便父程式在以後某個時候獲取。這些資訊至少包括程式ID,程式的終止狀態,以及該程式使用的CPU時間,所以當終止子程式的父程式呼叫wait或waitpid時就可以得到這些資訊。如果一個程式終止,而該程式有子程式處於殭屍狀態,那麼它的所有殭屍子程式的父程式ID將被重置為1(init程式)。繼承這些子程式的init程式將清理它們(也就是說init程式將wait它們,從而去除它們的殭屍狀態)。

    如何避免殭屍程式

    • 透過signal(SIGCHLD, SIG_IGN)通知核心對子程式的結束不關心,由核心回收。如果不想讓父程式掛起,可以在父程式中加入一條語句:signal(SIGCHLD,SIG_IGN);表示父程式忽略SIGCHLD訊號,該訊號是子程式退出的時候向父程式傳送的。
    • 父程式呼叫wait/waitpid等函式等待子程式結束,如果尚無子程式退出wait會導致父程式阻塞。waitpid可以透過傳遞WNOHANG使父程式不阻塞立即返回。
    • 如果父程式很忙可以用signal註冊訊號處理函式,在訊號處理函式呼叫wait/waitpid等待子程式退出。
    • 透過兩次呼叫fork。父程式首先呼叫fork建立一個子程式然後waitpid等待子程式退出,子程式再fork一個孫程式後退出。這樣子程式退出後會被父程式等待回收,而對於孫子程式其父程式已經退出所以孫程式成為一個孤兒程式,孤兒程式由init程式接管,孫程式結束後,init會等待回收。

    第一種方法忽略SIGCHLD訊號,這常用於併發伺服器的效能的一個技巧因為併發伺服器常常fork很多子程式,子程式終結之後需要伺服器程式去wait清理資源。如果將此訊號的處理方式設為忽略,可讓核心把殭屍子程式轉交給init程式去處理,省去了大量殭屍程式佔用系統資源。

  6. 程式和執行緒的比較

    執行緒是排程的基本單位,而程式則是資源擁有的基本單位

    執行緒與程式的比較如下:

    • 程式是資源(包括記憶體、開啟的檔案等)分配的單位,執行緒是 CPU 排程的單位;
    • 程式擁有一個完整的資源平臺,而執行緒只獨享必不可少的資源,如暫存器和棧;
    • 執行緒同樣具有就緒、阻塞、執行三種基本狀態,同樣具有狀態之間的轉換關係;
    • 執行緒能減少併發執行的時間和空間開銷;

    對於,執行緒相比程式能減少開銷,體現在:

    • 執行緒的建立時間比程式快,因為程式在建立的過程中,還需要資源管理資訊,比如記憶體管理資訊、檔案管理資訊,而執行緒在建立的過程中,不會涉及這些資源管理資訊,而是共享它們;
    • 執行緒的終止時間比程式快,因為執行緒釋放的資源相比程式少很多;
    • 同一個程式內的執行緒切換比程式切換快,因為執行緒具有相同的地址空間(虛擬記憶體共享),這意味著同一個程式的執行緒都具有同一個頁表,那麼在切換的時候不需要切換頁表。而對於程式之間的切換,切換的時候要把頁表給切換掉,而頁表的切換過程開銷是比較大的;
    • 由於同一程式的各執行緒間共享記憶體和檔案資源,那麼線上程之間資料傳遞的時候,就不需要經過核心了,這就使得執行緒之間的資料互動效率更高了;

    所以,不管是時間效率,還是空間效率執行緒比程式都要高。

    1、執行緒啟動速度快,輕量級

    2、執行緒的系統開銷小

    3、執行緒使用有一定難度,需要處理資料一致性問題

    4、同一執行緒共享的有堆、全域性變數、靜態變數、指標,引用、檔案等,而獨自佔有棧

    1. 程式是資源分配的最小單位,而執行緒是 CPU 排程的最小單位;
    2. 建立程式或撤銷程式,系統都要為之分配或回收資源,作業系統開銷遠大於建立或撤銷執行緒時的開銷;
    3. 不同程式地址空間相互獨立,同一程式內的執行緒共享同一地址空間。一個程式的執行緒在另一個程式內是不可見的;
    4. 程式間不會相互影響,而一個執行緒掛掉將可能導致整個程式掛掉;

2. 記憶體管理

  1. 實體地址、邏輯地址、虛擬記憶體的概念

    1. 實體地址:它是地址轉換的最終地址,程式在執行時執行指令和訪問資料最後都要透過實體地址從主存中存取,是記憶體單元真正的地址。
    2. 邏輯地址:是指計算機使用者看到的地址。例如:當建立一個長度為 100 的整型陣列時,作業系統返回一個邏輯上的連續空間:指標指向陣列第一個元素的記憶體地址。由於整型元素的大小為 4 個位元組,故第二個元素的地址時起始地址加 4,以此類推。事實上,邏輯地址並不一定是元素儲存的真實地址,即陣列元素的實體地址(在記憶體條中所處的位置),並非是連續的,只是作業系統透過地址對映,將邏輯地址對映成連續的,這樣更符合人們的直觀思維。
    3. 虛擬記憶體:是計算機系統記憶體管理的一種技術。它使得應用程式認為它擁有連續的可用的記憶體(一個連續完整的地址空間),而實際上,它通常是被分隔成多個實體記憶體碎片,還有部分暫時儲存在外部磁碟儲存器上,在需要時進行資料交換。
  2. 虛擬記憶體有什麼好處

    • 第一,虛擬記憶體可以使得程式對執行記憶體超過實體記憶體大小,因為程式執行符合區域性性原理,CPU 訪問記憶體會有很明顯的重複訪問的傾向性,對於那些沒有被經常使用到的記憶體,我們可以把它換出到實體記憶體之外,比如硬碟上的 swap 區域。
    • 第二,由於每個程式都有自己的頁表,所以每個程式的虛擬記憶體空間就是相互獨立的。程式也沒有辦法訪問其他程式的頁表,所以這些頁表是私有的,這就解決了多程式之間地址衝突的問題。
    • 第三,頁表裡的頁表項中除了實體地址之外,還有一些標記屬性的位元,比如控制一個頁的讀寫許可權,標記該頁是否存在等。在記憶體訪問方面,作業系統提供了更好的安全性。
  3. 記憶體管理

    記憶體管理的概念就是作業系統對記憶體的劃分和動態分配。

    記憶體管理功能:

    記憶體空間的分配與回收:由作業系統完成主儲存器空間的分配和管理,是程式設計師擺脫儲存分配的麻煩,提高程式設計效率。
    地址轉換:將邏輯地址轉換成相應的實體地址。
    記憶體空間的擴充:利用虛擬儲存技術或自動覆蓋技術,從邏輯上擴充主存。
    儲存保護:保證各道作業在各自的儲存空間內執行,互不干擾。
    建立程式首先要將程式和資料裝入記憶體。將使用者源程式變為可在記憶體中執行的程式,通常需要以下幾個步驟:

    編譯:由編譯程式將使用者原始碼編譯成若干目標模組(把高階語言翻譯成機器語言)
    連結:由連結程式將編譯後形成的一組目標模組及所需的庫函式連線在一起,形成一個完整的裝入模組(由目標模組生成裝入模組,連結後形成完整的邏輯地址)
    裝入:由裝入程式將裝入模組裝入記憶體執行,裝入後形成實體地址
    程式的連結有以下三種方式:

    靜態連結:在程式執行之前,先將各目標模組及它們所需的庫函式連線成一個完整的可執行檔案(裝入模組),之後不再拆開。
    裝入時動態連結:將各目標模組裝入記憶體時,邊裝入邊連結的連結方式。
    執行時動態連結:在程式執行中需要該目標模組時,才對它進行連結。其優點是便於修改和更新,便於實現對目標模組的共享。
    記憶體的裝入模組在裝入記憶體時,有以下三種方式:

    重定位:根據記憶體的當前情況,將裝入模組裝入記憶體的適當位置,裝入時對目標程式中的指令和資料的修改過程稱為重定位。

    靜態重定位:地址的變換通常是在裝入時一次完成的。一個作業裝入記憶體時,必須給它分配要求的全部記憶體空間,若沒有足夠的記憶體,則不能裝入該作業。此外,作業一旦裝入記憶體,整個執行期間就不能在記憶體中移動,也不能再申請記憶體空間。
    動態重定位:需要重定位暫存器的支援。可以將程式分配到不連續的儲存區中;在程式執行之前可以只裝入它的部分程式碼即可投入執行,然後在程式執行期間,根據需要動態申請分配記憶體。
    記憶體分配前,需要保護作業系統不受使用者程式的影響,同時保護使用者程式不受其他使用者程式的影響。記憶體保護可採取如下兩種方法:

    在CPU中設定一對上、下限暫存器,存放使用者作業在主存中的上限和下限地址,每當CPU要訪問一個地址時,分別和兩個暫存器的值相比,判斷有無越界。
    採用重定位暫存器(或基址暫存器)和界地址暫存器(又稱限長儲存器)來實現這種保護。重定位暫存器包含最小的實體地址值,界地址暫存器含邏輯地址的最大值。每個邏輯地址值必須小於界地址暫存器;記憶體管理機構動態得將邏輯地址與界地址暫存器進行比較,若未發生地址越界,則加上重定位暫存器的值後對映成實體地址,再送交記憶體單元。

  4. 常見的記憶體分配方式

    (1) 從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數,static變數。

    (2) 在棧上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內建於處理器的指令集中,效率很高,但是分配的記憶體容量有限。

    (3) 從堆上分配,亦稱動態記憶體分配。程式在執行的時候用malloc或new申請任意多少的記憶體,程式設計師自己負責在何時用free或delete釋放記憶體。動態記憶體的生存期由我們決定,使用非常靈活,但問題也最多。

    常見記憶體分配記憶體錯誤

    (1)記憶體分配未成功,卻使用了它。

    (2)記憶體分配雖然成功,但是尚未初始化就引用它。

    (3)記憶體分配成功並且已經初始化,但操作越過了記憶體的邊界。

    (4)忘記了釋放記憶體,造成記憶體洩露。

    (5)釋放了記憶體卻繼續使用它。常見於以下有三種情況:

    • 程式中的物件呼叫關係過於複雜,實在難以搞清楚某個物件究竟是否已經釋放了記憶體,此時應該重新設計資料結構,從根本上解決物件管理的混亂局面。
    • 函式的return語句寫錯了,注意不要返回指向“棧記憶體”的“指標”或者“引用”,因為該記憶體在函式體結束時被自動銷燬。
    • 使用free或delete釋放了記憶體後,沒有將指標設定為NULL。導致產生“野指標”。
  5. malloc如何分配記憶體

    從作業系統層面上看,malloc是透過兩個系統呼叫來實現的: brk和mmap

    • brk是將程式資料段(.data)的最高地址指標向高處移動,這一步可以擴大程式在執行時的堆大小
    • mmap是在程式的虛擬地址空間中尋找一塊空閒的虛擬記憶體,這一步可以獲得一塊可以操作的堆記憶體。

    通常,分配的記憶體小於128k時,使用brk呼叫來獲得虛擬記憶體,大於128k時就使用mmap來獲得虛擬記憶體。

    程式先透過這兩個系統呼叫獲取或者擴大程式的虛擬記憶體,獲得相應的虛擬地址,在訪問這些虛擬地址的時候,透過缺頁中斷,讓核心分配相應的實體記憶體,這樣記憶體分配才算完成。

  6. 如何避免預讀失效和快取汙染

    預讀失效

    這些被提前載入進來的頁,並沒有被訪問,相當於這個預讀工作是白做了,這個就是預讀失效

    傳統的 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 區域)的門檻後,就很好了避免快取汙染帶來的影響。

  7. 實體記憶體管理

    作業系統實體記憶體管理主要包括程式裝入、交換技術、連續分配管理方式和非連續分配管理方式(分頁、分段、段分頁)。

    連續分配管理方式

    連續記憶體分配
    記憶體碎片
    當給程式分配空間時,可能會出現一些無法被利用的空閒碎片空間。
    1.外部碎片:分配單元之間無法被利用的記憶體碎片
    2.內部碎片:分配給任務記憶體大小大於任務所需記憶體大小時,多出來的記憶體碎片。

    分割槽的動態分配
    連續記憶體分配情況:將應用程式從硬碟載入到記憶體中,給記憶體分配一塊記憶體。應用程式執行訪問資料,資料的連續記憶體空間。

    記憶體分配演算法:
    具體問題具體分析適配演算法。

    1. 首次適配:
      定義:使用第一塊記憶體大小大於需求大小的可用空閒塊
      實現方法:需要有一個按地址排序的空閒塊列表。尋找第一個滿足記憶體需求的空閒塊。對於回收,要考慮空閒塊與相鄰空閒塊合併問題。
      優點:簡單,易於產生更大的空閒塊,想著地址空間結尾分配
      缺點:產生外部碎片,具有不確定性

    2. 最優適配:
      定義:尋找整個空閒塊中,最滿足分配請求的空閒塊,記憶體差值最小。
      實現方法:需要有一個按尺寸排序的空閒塊列表,尋找最滿足分配的記憶體塊。對於回收,要考慮空閒塊與相鄰空閒塊合併問題。
      優點:避免分割大空閒塊,最小化外部碎片的產生,簡單
      缺點:外部碎片很細,有很多微小碎片,不利於後續分配管理。

    3. 最差適配:
      定義:找到差距最大的記憶體空閒塊。
      實現:需要有一個按尺寸排序的空閒塊列表,尋找差距最大的記憶體塊。對於回收,要考慮空閒塊與相鄰空閒塊合併問題。
      優點:避免產生微小外部碎片,分配效率高,適用於分配中大快
      缺點:對於大塊請求帶來一定影響

    減少記憶體碎片方法

    1. 緊緻:壓縮式碎片整理
      調整執行程式的位置。
      1.重定位的時機。不能在程式執行時進行,可以在程式等待時複製。
      2.記憶體複製開銷很大。

    2. swaping:交換式碎片整理
      把硬碟作為一個備份。把等待的程式包括資料(主存)挪到硬碟上。當硬碟上程式需要執行時再複製到主存上。
      1.交換那個程式,減小開銷
      2.交換的時機

    非連續記憶體分配

    連續記憶體分配和非連續記憶體分配
    連續記憶體分配缺點:1.分配給一個程式的實體記憶體是連續的,記憶體利用率較低,由外碎片內碎片的問題。
    非連續記憶體分配優點:1.程式實體地址是非連續的 2.更好的記憶體利用和管理 3.允許共享程式碼與資料 4.支援動態載入和動態連結
    非連續記憶體分配缺點:建立虛擬地址到實體地址的對映,軟體開銷太大,可以用硬體支援
    -》硬體兩種管理方案:分段和分頁
    分段
    分段地址空間
    對於一段程式記憶體可以分為:程式(主程式+子程式+共享庫)+變數(棧、堆、共享資料段)
    分段:更好的分離與共享,將邏輯地址空間分散到多個實體地址空間
    邏輯地址是連續的,將具有不同功能的對映到物理空間中,這些段大小不一,位置不一

    硬體實現分段定址機制
    一維邏輯地址有不同段組成,首先將邏輯地址分為兩段:段定址(段號)+段偏移定址(addr)
    透過段號在段表找到邏輯記憶體的段起始地址,看段起始地址是否滿足段大小限制,不滿足返回記憶體異常,滿足將邏輯地址加偏移量是實體地址。

    段表:
    1.儲存邏輯地址段段號到實體地址段號之間的對映關係
    2.儲存段大小,起始地址
    段表的建立:作業系統在定址前建立。

    分頁
    分頁地址空間
    需要頁號和頁地址偏移。相比分段,分頁頁幀大小固定不變。
    可以劃分實體記憶體至固定大小的幀,將邏輯地址的頁也劃分至相同記憶體大小。大小是2的冪。
    建立方案
    頁幀(Frame):實體記憶體被分割為大小相等的幀
    一個記憶體實體地址是一個二元組(f,o)
    實體地址 = 2^S*f+o
    f是幀號(F位,2F個幀),o是幀內偏移(S位,每幀2S位元組),

    頁(Page):邏輯地址空間分割為大小相等的頁
    頁內偏移大小 = 幀內偏移大小(頁幀大小和頁大小一致)
    頁號大小和幀號大小可能不一致

    一個邏輯地址是一個二元組(p,o)
    邏輯地址 = 2^S*p+o
    p:頁號(P位,2P個頁),o:頁內偏移(S位,每頁2S個位元組)

    頁定址機制
    CPU定址(邏輯地址),邏輯地址包含兩部分(p,o),首先把p(頁號)作為索引,再加上頁表基址查頁表(pagetable)中對應幀號(實體地址中f),知道幀號加上頁內偏移就是實體地址(f,o)。

    頁表:以頁號為索引的對應的幀號(實體地址),為此需要知道頁表的基址位置(頁號從哪個地址開始查)。
    頁表的建立:作業系統初始化時,enable分頁機制前就需要建立好

    分頁與分段:
    分頁:相比分段,分頁頁記憶體固定,導致頁內偏移大小範圍是固定的。不需要想分段一樣考慮分頁大小不一致的問題。
    邏輯地址和實體地址空間
    1.總邏輯頁大小和總物理幀大小不一致,一般邏輯地址空間大於實體地址空間。
    2.邏輯地址空間連續,實體地址空間不連續。減少內外碎片。
    頁表
    頁表結構
    頁表是個陣列,索引是頁號,對應的陣列項內容裡有幀號。

    分頁機制效能問題
    1.時間開銷:訪問一個記憶體單元需要兩次記憶體訪問

    頁表不能放在CPU裡,只能放在記憶體裡,CPU先做記憶體定址找頁表基址,再進行頁表訪問,進行兩次記憶體訪問,訪問速度很慢

    2空間代價:頁表佔用空間

    1.64位計算機,每頁1KB,頁表大小?2^54個數的頁表,很大
    2.多個程式有多個頁表

    解決方法
    時間上:快取 ——快表,TLB,Translation Look-aside Buffer
    TLB:

    位於CPU中的一塊快取區域,存放常用的頁號-幀號對,採用關聯記憶體的方式實現,具有快速訪問功能。
    -CPU定址時會先透過頁號在TLB查詢,看是否存在頁號的Key,對應得到幀號,進而得到實體地址(減少對實體地址的訪問)。TLB未命中,把該項更新到TLB中(x86CPU這個過程是硬體完成,對於mps是作業系統完成)。
    編寫程式時,把訪問的地址寫在一個頁號裡。
    空間上:間接訪問(多級頁表機制),以時間換空間
    二級頁表:

    對於邏輯地址(p1,p2,o)
    CPU定址時先透過p1查詢一級頁表,一級頁表中存放的是二級頁表的p2的起始地址,再在二級頁表對應起始地址查詢偏移p2,對應存放的就是幀號。提高時間開銷,但一級頁表中不存在頁表項就不需要佔用二級頁表項的記憶體,節省空間。
    多級頁表

    頁號分為K個部分,建立頁表“樹”

  8. 快表

    快表,又稱聯想暫存器(TLB) ,是一種訪問速度比記憶體快很多的高速緩衝儲存器,用來存放當前訪問的若干頁表項,以加速地址變換的過程。與此對應,記憶體中的頁表常稱為慢表。

    地址變換過程 訪問一個邏輯地址的訪存次數
    基本地址變換機構 ①算頁號、頁內偏移量 ②檢查頁號合法性 ③查頁表,找到頁面存放的記憶體塊號 ④根據記憶體塊號與頁內偏移量得到實體地址 ⑤訪問目標記憶體單元 兩次訪存
    具有快表的地址變換機構 ①算頁號、頁內偏移量 ②檢查頁號合法性 ③查快表。若命中,即可知道頁面存放的記憶體塊號,可直接進行⑤;若未命中則進行④ ④查頁表,找到頁面存放的記憶體塊號,並且將頁表項複製到快表中 ⑤根據記憶體塊號與頁內偏移量得到實體地址 ⑥訪問目標記憶體單元 快表命中,只需一次訪存 快表未命中,需要兩次訪存
  9. 記憶體交換技術

    交換(對換)技術的設計思想:記憶體空間緊張時,系統將記憶體中某些程式暫時換出外存,把外存中某些已具備執行條件的程式換入記憶體(程式在記憶體與磁碟間動態排程)

    換入:把準備好競爭CPU執行的程式從輔存移到記憶體。 換出:把處於等待狀態(或CPU排程原則下被剝奪執行權力)的程式從記憶體移到輔存,把記憶體空間騰出來。

    交換時機:記憶體交換通常在許多程式執行且記憶體吃緊時進行,而系統負荷降低就暫停。例如:在發現許多程式執行時經常發生缺頁,就說明記憶體緊張,此時可以換出一些程式;如果缺頁率明顯下降,就可以暫停換出。

    關鍵點

    1. 交換需要備份儲存,通常是快速磁碟,它必須足夠大,並且提供對這些記憶體映像的直接訪問。
    2. 為了有效使用CPU,需要每個程式的執行時間比交換時間長,而影響交換時間的主要是轉移時間,轉移時間與所交換的空間記憶體成正比。
    3. 如果換出程式,比如確保該程式的記憶體空間成正比。
    4. 交換空間通常作為磁碟的一整塊,且獨立於檔案系統,因此使用就可能很快。
    5. 交換通常在有許多程式執行且記憶體空間吃緊時開始啟動,而系統負荷降低就暫停。
    6. 普通交換使用不多,但交換的策略的某些變種在許多系統中(如UNIX系統)仍然發揮作用。
  10. 分頁與分段的區別

    1. 段是資訊的邏輯單位,它是根據使用者的需要劃分的,因此段對使用者是可見的 ;頁是資訊的物理單位,是為了管理主存的方便而劃分的,對使用者是透明的;
    2. 段的大小不固定,有它所完成的功能決定;頁大大小固定,由系統決定;
    3. 段向使用者提供二維地址空間;頁向使用者提供的是一維地址空間;
    4. 段是資訊的邏輯單位,便於儲存保護和資訊的共享,頁的保護和共享受到限制。

3. 程式排程演算法

  1. 程式排程演算法詳細介紹

    選擇一個程式執行這一功能是在作業系統中完成的,通常稱為排程程式scheduler)。

    排程時機

    ​ 在程式的生命週期中,當程式從一個執行狀態到另外一狀態變化的時候,其實會觸發一次排程。

    比如,以下狀態的變化都會觸發作業系統的排程:

    • 從就緒態 -> 執行態:當程式被建立時,會進入到就緒佇列,作業系統會從就緒佇列選擇一個程式執行;
    • 從執行態 -> 阻塞態:當程式發生 I/O 事件而阻塞時,作業系統必須選擇另外一個程式執行;
    • 從執行態 -> 結束態:當程式退出結束後,作業系統得從就緒佇列選擇另外一個程式執行;

    因為,這些狀態變化的時候,作業系統需要考慮是否要讓新的程式給 CPU 執行,或者是否讓當前程式從 CPU 上退出來而換另一個程式執行。

    另外,如果硬體時鐘提供某個頻率的週期性中斷,那麼可以根據如何處理時鐘中斷 ,把排程演算法分為兩類:

    • 非搶佔式排程演算法挑選一個程式,然後讓該程式執行直到被阻塞,或者直到該程式退出,才會呼叫另外一個程式,也就是說不會理時鐘中斷這個事情。
    • 搶佔式排程演算法挑選一個程式,然後讓該程式只執行某段時間,如果在該時段結束時,該程式仍然在執行時,則會把它掛起,接著排程程式從就緒佇列挑選另外一個程式。這種搶佔式排程處理,需要在時間間隔的末端發生時鐘中斷,以便把 CPU 控制返回給排程程式進行排程,也就是常說的時間片機制

    排程原則

    • CPU 利用率:排程程式應確保 CPU 是始終匆忙的狀態,這可提高 CPU 的利用率;
    • 系統吞吐量:吞吐量表示的是單位時間內 CPU 完成程式的數量,長作業的程式會佔用較長的 CPU 資源,因此會降低吞吐量,相反,短作業的程式會提升系統吞吐量;
    • 週轉時間:週轉時間是程式執行+阻塞時間+等待時間的總和,一個程式的週轉時間越小越好;
    • 等待時間:這個等待時間不是阻塞狀態的時間,而是程式處於就緒佇列的時間,等待的時間越長,使用者越不滿意;
    • 響應時間:使用者提交請求到系統第一次產生響應所花費的時間,在互動式系統中,響應時間是衡量排程演算法好壞的主要標準。

    排程演算法

    ​ 排程演算法是指:根據系統的資源分配策略所規定的資源分配演算法。常用的排程演算法有:先來先服務排程演算法、時間片輪轉排程法、短作業優先排程演算法、最短剩餘時間優先、高響應比優先排程演算法、優先順序排程演算法等等。

    • 先來先服務排程演算法

    先來先服務排程演算法是一種最簡單的排程演算法,也稱為先進先出或嚴格排隊方案。當每個程式就緒後,它加入就緒佇列。當前正執行的程式停止執行,選擇在就緒佇列中存在時間最長的程式執行。該演算法既可以用於作業排程,也可以用於程式排程。先來先服務比較適合於常作業(程式),而不利於段作業(程式)。

    • 時間片輪轉排程演算法

    時間片輪轉排程演算法主要適用於分時系統。在這種演算法中,系統將所有就緒程式按到達時間的先後次序排成一個佇列,程式排程程式總是選擇就緒佇列中第一個程式執行,即先來先服務的原則,但僅能執行一個時間片。

    • 短作業優先排程演算法

    短作業優先排程演算法是指對短作業優先排程的演算法,從後備佇列中選擇一個或若干個估計執行時間最短的作業,將它們調入記憶體執行。 短作業優先排程演算法是一個非搶佔策略,他的原則是下一次選擇預計處理時間最短的程式,因此短程式將會越過長作業,跳至佇列頭。

    • 最短剩餘時間優先排程演算法

    最短剩餘時間是針對最短程式優先增加了搶佔機制的版本。在這種情況下,程式排程總是選擇預期剩餘時間最短的程式。當一個程式加入到就緒佇列時,他可能比當前執行的程式具有更短的剩餘時間,因此只要新程式就緒,排程程式就能可能搶佔當前正在執行的程式。像最短程式優先一樣,排程程式正在執行選擇函式是必須有關於處理時間的估計,並且存在長程式飢餓的危險。

    • 高響應比優先排程演算法

    高響應比優先排程演算法主要用於作業排程,該演算法是對 先來先服務排程演算法和短作業優先排程演算法的一種綜合平衡,同時考慮每個作業的等待時間和估計的執行時間。在每次進行作業排程時,先計算後備作業佇列中每個作業的響應比,從中選出響應比最高的作業投入執行。

    • 優先順序排程演算法

    優先順序排程演算法每次從後備作業佇列中選擇優先順序最髙的一個或幾個作業,將它們調入記憶體,分配必要的資源,建立程式並放入就緒佇列。在程式排程中,優先順序排程演算法每次從就緒佇列中選擇優先順序最高的程式,將處理機分配給它,使之投入執行。

4. 磁碟排程演算法

  1. 磁碟排程演算法詳細介紹

    常見的磁碟排程演算法有:

    • 先來先服務演算法
    • 最短尋道時間優先演算法
    • 掃描演算法
    • 迴圈掃描演算法
    • LOOK 與 C-LOOK 演算法

    先來先服務

    ​ 先來先服務(First-Come,First-Served,FCFS),顧名思義,先到來的請求,先被服務。

    最短尋道時間優先

    ​ 最短尋道時間優先(Shortest Seek First,SSF)演算法的工作方式是,優先選擇從當前磁頭位置所需尋道時間最短的請求

    掃描演算法

    ​ 最短尋道時間優先演算法會產生飢餓的原因在於:磁頭有可能再一個小區域內來回得移動。

    ​ 為了防止這個問題,可以規定:磁頭在一個方向上移動,訪問所有未完成的請求,直到磁頭到達該方向上的最後的磁軌,才調換方向,這就是掃描(*Scan*)演算法

    ​ 這種演算法也叫做電梯演算法,比如電梯保持按一個方向移動,直到在那個方向上沒有請求為止,然後改變方向。

    迴圈掃描演算法

    ​ 掃描演算法使得每個磁軌響應的頻率存在差異,那麼要最佳化這個問題的話,可以總是按相同的方向進行掃描,使得每個磁軌的響應頻率基本一致。

    ​ 迴圈掃描(Circular Scan, CSCAN )規定:只有磁頭朝某個特定方向移動時,才處理磁軌訪問請求,而返回時直接快速移動至最靠邊緣的磁軌,也就是復位磁頭,這個過程是很快的,並且返回中途不處理任何請求,該演算法的特點,就是磁軌只響應一個方向上的請求。

    LOOK 與 C-LOOK演算法

    ​ 掃描演算法和迴圈掃描演算法,都是磁頭移動到磁碟「最始端或最末端」才開始調換方向。

    那這其實是可以最佳化的,最佳化的思路就是磁頭在移動到「最遠的請求」位置,然後立即反向移動。

    針對 SCAN 演算法的最佳化叫 LOOK 演算法,它的工作方式,磁頭在每個方向上僅僅移動到最遠的請求位置,然後立即反向移動,而不需要移動到磁碟的最始端或最末端,反向移動的途中會響應請求

    針對C-SCAN 演算法的最佳化叫 C-LOOK,它的工作方式,磁頭在每個方向上僅僅移動到最遠的請求位置,然後立即反向移動,而不需要移動到磁碟的最始端或最末端,反向移動的途中不會響應請求

5. 頁面置換演算法

  1. 頁面置換演算法詳細介紹

    請求調頁,也稱按需調頁,即對不在記憶體中的“頁”,當程式執行時要用時才調入,否則有可能到程式結束時也不會調入。而記憶體中給頁面留的位置是有限的,在記憶體中以幀為單位放置頁面。為了防止請求調頁的過程出現過多的記憶體頁面錯誤(即需要的頁面當前不在記憶體中,需要從硬碟中讀資料,也即需要做頁面的替換)而使得程式執行效率下降,我們需要設計一些頁面置換演算法,頁面按照這些演算法進行相互替換時,可以儘量達到較低的錯誤率。常用的頁面置換演算法如下:

    • 先進先出置換演算法(FIFO)

    先進先出,即淘汰最早調入的頁面。

    • 最佳置換演算法(OPT)

    選未來最遠將使用的頁淘汰,是一種最優的方案,可以證明缺頁數最小。

    • 最近最久未使用(LRU)演算法

    即選擇最近最久未使用的頁面予以淘汰

    • 時鐘(Clock)置換演算法

    時鐘置換演算法也叫最近未用演算法 NRU(Not RecentlyUsed)。該演算法為每個頁面設定一位訪問位,將記憶體中的所有頁面都透過連結指標鏈成一個迴圈佇列。

6. 網路系統

  1. 什麼是零複製

    為了提高檔案傳輸的效能,於是就出現了零複製技術,它透過一次系統呼叫(sendfile 方法)合併了磁碟讀取與網路傳送兩個操作,降低了上下文切換次數。另外,複製資料都是發生在核心中的,天然就降低了資料複製的次數。

    Kafka 和 Nginx 都有實現零複製技術,這將大大提高檔案傳輸的效能。

    零複製技術是基於 PageCache 的,PageCache 會快取最近訪問的資料,提升了訪問快取資料的效能,同時,為了解決機械硬碟定址慢的問題,它還協助 I/O 排程演算法實現了 IO 合併與預讀,這也是順序讀比隨機讀效能好的原因。這些優勢,進一步提升了零複製的效能。

  2. I/O多路複用

    既然為每個請求分配一個程式/執行緒的方式不合適,那有沒有可能只使用一個程式來維護多個 Socket 呢?答案是有的,那就是 I/O 多路複用技術。

    一個程式雖然任一時刻只能處理一個請求,但是處理每個請求的事件時,耗時控制在 1 毫秒以內,這樣 1 秒內就可以處理上千個請求,把時間拉長來看,多個請求複用了一個程式,這就是多路複用,這種思想很類似一個 CPU 併發多個程式,所以也叫做時分多路複用。

    我們熟悉的 select/poll/epoll 核心提供給使用者態的多路複用系統呼叫,程式可以透過一個系統呼叫函式從核心中獲取多個事件

    select/poll/epoll 是如何獲取網路事件的呢?在獲取事件時,先把所有連線(檔案描述符)傳給核心,再由核心返回產生了事件的連線,然後在使用者態中再處理這些連線對應的請求即可。

  3. select/poll/epoll

    select 和 poll 並沒有本質區別,它們內部都是使用「線性結構」來儲存程式關注的 Socket 集合。

    在使用的時候,首先需要把關注的 Socket 集合透過 select/poll 系統呼叫從使用者態複製到核心態,然後由核心檢測事件,當有網路事件產生時,核心需要遍歷程式關注 Socket 集合,找到對應的 Socket,並設定其狀態為可讀/可寫,然後把整個 Socket 集合從核心態複製到使用者態,使用者態還要繼續遍歷整個 Socket 集合找到可讀/可寫的 Socket,然後對其處理。

    很明顯發現,select 和 poll 的缺陷在於,當客戶端越多,也就是 Socket 集合越大,Socket 集合的遍歷和複製會帶來很大的開銷,因此也很難應對 C10K。

    epoll 是解決 C10K 問題的利器,透過兩個方面解決了 select/poll 的問題。

    • epoll 在核心裡使用「紅黑樹」來關注程式所有待檢測的 Socket,紅黑樹是個高效的資料結構,增刪改一般時間複雜度是 O(logn),透過對這棵黑紅樹的管理,不需要像 select/poll 在每次操作時都傳入整個 Socket 集合,減少了核心和使用者空間大量的資料複製和記憶體分配。
    • epoll 使用事件驅動的機制,核心裡維護了一個「連結串列」來記錄就緒事件,只將有事件發生的 Socket 集合傳遞給應用程式,不需要像 select/poll 那樣輪詢掃描整個集合(包含有和無事件的 Socket ),大大提高了檢測的效率。

    而且,epoll 支援邊緣觸發和水平觸發的方式,而 select/poll 只支援水平觸發,一般而言,邊緣觸發的方式會比水平觸發的效率高。

  4. 高效能網路模式:Reactor和Proactor

    常見的 Reactor 實現方案有三種。

    第一種方案單 Reactor 單程式 / 執行緒,不用考慮程式間通訊以及資料同步的問題,因此實現起來比較簡單,這種方案的缺陷在於無法充分利用多核 CPU,而且處理業務邏輯的時間不能太長,否則會延遲響應,所以不適用於計算機密集型的場景,適用於業務處理快速的場景,比如 Redis(6.0之前 ) 採用的是單 Reactor 單程式的方案。

    第二種方案單 Reactor 多執行緒,透過多執行緒的方式解決了方案一的缺陷,但它離高併發還差一點距離,差在只有一個 Reactor 物件來承擔所有事件的監聽和響應,而且只在主執行緒中執行,在面對瞬間高併發的場景時,容易成為效能的瓶頸的地方。

    第三種方案多 Reactor 多程式 / 執行緒,透過多個 Reactor 來解決了方案二的缺陷,主 Reactor 只負責監聽事件,響應事件的工作交給了從 Reactor,Netty 和 Memcache 都採用了「多 Reactor 多執行緒」的方案,Nginx 則採用了類似於 「多 Reactor 多程式」的方案。

    Reactor 可以理解為「來了事件作業系統通知應用程式,讓應用程式來處理」,而 Proactor 可以理解為「來了事件作業系統來處理,處理完再通知應用程式」。

    因此,真正的大殺器還是 Proactor,它是採用非同步 I/O 實現的非同步網路模型,感知的是已完成的讀寫事件,而不需要像 Reactor 感知到事件後,還需要呼叫 read 來從核心中獲取資料。

    不過,無論是 Reactor,還是 Proactor,都是一種基於「事件分發」的網路程式設計模式,區別在於 Reactor 模式是基於「待完成」的 I/O 事件,而 Proactor 模式則是基於「已完成」的 I/O 事件。

  5. 一致性雜湊介紹

    一致性雜湊是指將「儲存節點」和「資料」都對映到一個首尾相連的雜湊環上,如果增加或者移除一個節點,僅影響該節點在雜湊環上順時針相鄰的後繼節點,其它資料也不會受到影響。

    但是一致性雜湊演算法不能夠均勻的分佈節點,會出現大量請求都集中在一個節點的情況,在這種情況下進行容災與擴容時,容易出現雪崩的連鎖反應。

    為了解決一致性雜湊演算法不能夠均勻的分佈節點的問題,就需要引入虛擬節點,對一個真實節點做多個副本。不再將真實節點對映到雜湊環上,而是將虛擬節點對映到雜湊環上,並將虛擬節點對映到實際節點,所以這裡有「兩層」對映關係。

    引入虛擬節點後,可以會提高節點的均衡度,還會提高系統的穩定性。所以,帶虛擬節點的一致性雜湊方法不僅適合硬體配置不同的節點的場景,而且適合節點規模會發生變化的場景。

7. 鎖

  1. 什麼是死鎖和產生死鎖原因

    死鎖,是指多個程式在執行過程中因爭奪資源而造成的一種僵局,當程式處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。 如下圖所示:如果此時有一個執行緒 A,已經持有了鎖 A,但是試圖獲取鎖 B,執行緒 B 持有鎖 B,而試圖獲取鎖 A,這種情況下就會產生死鎖。

    產生死鎖原因

    ​ 由於系統中存在一些不可剝奪資源,而當兩個或兩個以上程式佔有自身資源,並請求對方資源時,會導致每個程式都無法向前推進,這就是死鎖。

    • 競爭資源

    例如:系統中只有一臺印表機,可供程式 A 使用,假定 A 已佔用了印表機,若 B 繼續要求印表機列印將被阻塞。

    系統中的資源可以分為兩類:

    1. 可剝奪資源:是指某程式在獲得這類資源後,該資源可以再被其他程式或系統剝奪,CPU 和主存均屬於可剝奪性資源;
    2. 不可剝奪資源,當系統把這類資源分配給某程式後,再不能強行收回,只能在程式用完後自行釋放,如磁帶機、印表機等。
    • 程式推進順序不當

    例如:程式 A 和 程式 B 互相等待對方的資料。

    死鎖產生的必要條件?

    1. 互斥條件:程式要求對所分配的資源進行排它性控制,即在一段時間內某資源僅為一程式所佔用。
    2. 請求和保持條件:當程式因請求資源而阻塞時,對已獲得的資源保持不放。
    3. 不剝奪條件:程式已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。
    4. 環路等待條件:在發生死鎖時,必然存在一個程式–資源的環形鏈。
  2. 如何避免死鎖

    預防死鎖、避免死鎖、檢測死鎖、解除死鎖

    預防死鎖

    1. 破壞請求條件:一次性分配所有資源,這樣就不會再有請求了;
    2. 破壞請保持條件:只要有一個資源得不到分配,也不給這個程式分配其他的資源:
    3. 破壞不可剝奪條件:當某程式獲得了部分資源,但得不到其它資源,則釋放已佔有的資源;
    4. 破壞環路等待條件:系統給每類資源賦予一個編號,每一個程式按編號遞增的順序請求資源,釋放則相反。

    解除死鎖

    1. 資源剝奪:掛起某些死鎖程式,並搶佔它的資源,將這些資源分配給其他死鎖程式(但應該防止被掛起的程式長時間得不到資源);
    2. 撤銷程式:強制撤銷部分、甚至全部死鎖程式並剝奪這些程式的資源(撤銷的原則可以按程式優先順序和撤銷程式代價的高低進行);
    3. 程式回退:讓一個或多個程式回退到足以避免死鎖的地步。程式回退時自願釋放資源而不是被剝奪。要求系統保持程式的歷史資訊,設定還原點。
  3. 什麼是悲觀鎖、樂觀鎖

    悲觀鎖做事比較悲觀,它認為多執行緒同時修改共享資源的機率比較高,於是很容易出現衝突,所以訪問共享資源前,先要上鎖

    樂觀鎖做事比較樂觀,它假定衝突的機率很低,它的工作方式是:先修改完共享資源,再驗證這段時間內有沒有發生衝突,如果沒有其他執行緒在修改資源,那麼操作完成,如果發現有其他執行緒已經修改過這個資源,就放棄本次操作

    可見,樂觀鎖的心態是,不管三七二十一,先改了資源再說。另外,你會發現樂觀鎖全程並沒有加鎖,所以它也叫無鎖程式設計

    互斥鎖、自旋鎖、讀寫鎖,都是屬於悲觀鎖。

  4. 哲學家進餐問題

    生產者-消費者問題描述:

    • 生產者在生成資料後,放在一個緩衝區中;
    • 消費者從緩衝區取出資料處理;
    • 任何時刻,只能有一個生產者或消費者可以訪問緩衝區;

    我們對問題分析可以得出:

    • 任何時刻只能有一個執行緒操作緩衝區,說明操作緩衝區是臨界程式碼,需要互斥
    • 緩衝區空時,消費者必須等待生產者生成資料;緩衝區滿時,生產者必須等待消費者取出資料。說明生產者和消費者需要同步

    那麼我們需要三個訊號量,分別是:

    • 互斥訊號量 mutex:用於互斥訪問緩衝區,初始化值為 1;
    • 資源訊號量 fullBuffers:用於消費者詢問緩衝區是否有資料,有資料則讀取資料,初始化值為 0(表明緩衝區一開始為空);
    • 資源訊號量 emptyBuffers:用於生產者詢問緩衝區是否有空位,有空位則生成資料,初始化值為 n (緩衝區大小);

    如果消費者執行緒一開始執行 P(fullBuffers),由於訊號量 fullBuffers 初始值為 0,則此時 fullBuffers 的值從 0 變為 -1,說明緩衝區裡沒有資料,消費者只能等待。

    接著,輪到生產者執行 P(emptyBuffers),表示減少 1 個空槽,如果當前沒有其他生產者執行緒在臨界區執行程式碼,那麼該生產者執行緒就可以把資料放到緩衝區,放完後,執行 V(fullBuffers) ,訊號量 fullBuffers 從 -1 變成 0,表明有「消費者」執行緒正在阻塞等待資料,於是阻塞等待的消費者執行緒會被喚醒。

    消費者執行緒被喚醒後,如果此時沒有其他消費者執行緒在讀資料,那麼就可以直接進入臨界區,從緩衝區讀取資料。最後,離開臨界區後,把空槽的個數 + 1。

  5. 生產者、消費者問題

    哲學家就餐的問題描述:

    • 5 個老大哥哲學家,閒著沒事做,圍繞著一張圓桌吃麵;
    • 巧就巧在,這個桌子只有 5 支叉子,每兩個哲學家之間放一支叉子;
    • 哲學家圍在一起先思考,思考中途餓了就會想進餐;
    • 這些哲學家要兩支叉子才願意吃麵,也就是需要拿到左右兩邊的叉子才進餐;
    • 吃完後,會把兩支叉子放回原處,繼續思考;

    一、讓偶數編號的哲學家「先拿左邊的叉子後拿右邊的叉子」,奇數編號的哲學家「先拿右邊的叉子後拿左邊的叉子」。

    在 P 操作時,根據哲學家的編號不同,拿起左右兩邊叉子的順序不同。另外,V 操作是不需要分支的,因為 V 操作是不會阻塞的。

    二、用一個陣列 state 來記錄每一位哲學家的三個狀態,分別是在進餐狀態、思考狀態、飢餓狀態(正在試圖拿叉子)。

    那麼,一個哲學家只有在兩個鄰居都沒有進餐時,才可以進入進餐狀態。

    上面的程式使用了一個訊號量陣列,每個訊號量對應一位哲學家,這樣在所需的叉子被佔用時,想進餐的哲學家就被阻塞。

  6. 讀者寫者問題

    讀者只會讀取資料,不會修改資料,而寫者即可以讀也可以修改資料。

    讀者-寫者的問題描述:

    • 「讀-讀」允許:同一時刻,允許多個讀者同時讀
    • 「讀-寫」互斥:沒有寫者時讀者才能讀,沒有讀者時寫者才能寫
    • 「寫-寫」互斥:沒有其他寫者時,寫者才能寫

    使用訊號量的方式來嘗試解決:

    • 訊號量 wMutex:控制寫操作的互斥訊號量,初始值為 1 ;
    • 讀者計數 rCount:正在進行讀操作的讀者個數,初始化為 0;
    • 訊號量 rCountMutex:控制對 rCount 讀者計數器的互斥修改,初始值為 1;

    這種實現,是讀者優先的策略,因為只要有讀者正在讀的狀態,後來的讀者都可以直接進入,如果讀者持續不斷進入,則寫者會處於飢餓狀態。

    那既然有讀者優先策略,自然也有寫者優先策略:

    • 只要有寫者準備要寫入,寫者應儘快執行寫操作,後來的讀者就必須阻塞;
    • 如果有寫者持續不斷寫入,則讀者就處於飢餓;

    在方案一的基礎上新增如下變數:

    • 訊號量 rMutex:控制讀者進入的互斥訊號量,初始值為 1;
    • 訊號量 wDataMutex:控制寫者寫操作的互斥訊號量,初始值為 1;
    • 寫者計數 wCount:記錄寫者數量,初始值為 0;
    • 訊號量 wCountMutex:控制 wCount 互斥修改,初始值為 1;

    注意,這裡 rMutex 的作用,開始有多個讀者讀資料,它們全部進入讀者佇列,此時來了一個寫者,執行了 P(rMutex) 之後,後續的讀者由於阻塞在 rMutex 上,都不能再進入讀者佇列,而寫者到來,則可以全部進入寫者佇列,因此保證了寫者優先。

    同時,第一個寫者執行了 P(rMutex) 之後,也不能馬上開始寫,必須等到所有進入讀者佇列的讀者都執行完讀操作,透過 V(wDataMutex) 喚醒寫者的寫操作。

    既然讀者優先策略和寫者優先策略都會造成飢餓的現象,那麼我們就來實現一下公平策略。

    公平策略:

    • 優先順序相同;
    • 寫者、讀者互斥訪問;
    • 只能一個寫者訪問臨界區;
    • 可以有多個讀者同時訪問臨界資源;

8. 作業系統知識點

  1. 對併發和並行的理解

    1. 並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔發生;
    2. 並行是在不同實體上的多個事件,併發是在同一實體上的多個事件;
  2. 什麼是使用者態和核心態

    使用者態和核心態是作業系統的兩種執行狀態。

    • 核心態:處於核心態的 CPU 可以訪問任意的資料,包括外圍裝置,比如網路卡、硬碟等,處於核心態的 CPU 可以從一個程式切換到另外一個程式,並且佔用 CPU 不會發生搶佔情況,一般處於特權級 0 的狀態我們稱之為核心態。
    • 使用者態:處於使用者態的 CPU 只能受限的訪問記憶體,並且不允許訪問外圍裝置,使用者態下的 CPU 不允許獨佔,也就是說 CPU 能夠被其他程式獲取。
  3. 兩大區域性性原理是什麼

    主要分為時間區域性性和空間區域性性

    時間區域性性:如果執行了程式中的某條指令,那麼不久後這條指令很有可能再次執行;如果某個資料被訪問過,不久之後該資料很可能再次被訪問。(因為程式中存在大量的迴圈)

    空間區域性性:一旦程式訪問了某個儲存單元,在不久之後,其附近的儲存單元也很有可能被訪問。(因為很多資料在記憶體中都是連續存放的,並且程式的指令也是順序地在記憶體中存放的)

  4. 異常和中斷是什麼,有什麼區別

    中斷

    當我們在敲擊鍵盤的同時就會產生中斷,當硬碟讀寫完資料之後也會產生中斷,所以,我們需要知道,中斷是由硬體裝置產生的,而它們從物理上說就是電訊號,之後,它們透過中斷控制器傳送給CPU,接著CPU判斷收到的中斷來自於哪個硬體裝置(這定義在核心中),最後,由CPU傳送給核心,有核心處理中斷。

    異常

    CPU處理程式的時候一旦程式不在記憶體中,會產生缺頁異常;當執行除法程式時,當除數為0時,又會產生除0異常。所以,大家也需要記住的是,異常是由CPU產生的,同時,它會傳送給核心,要求核心處理這些異常

    相同點

    • 最後都是由CPU傳送給核心,由核心去處理
    • 處理程式的流程設計上是相似的

    不同點

    • 產生源不相同,異常是由CPU產生的,而中斷是由硬體裝置產生的
    • 核心需要根據是異常還是中斷呼叫不同的處理程式
    • 中斷不是時鐘同步的,這意味著中斷可能隨時到來;異常由於是CPU產生的,所以它是時鐘同步的
    • 當處理中斷時,處於中斷上下文中;處理異常時,處於程式上下文中
  5. 原子操作

    處理器使用基於對快取加鎖或匯流排加鎖的方式來實現多處理器之間的原子操作。首先處理器會自動保證基本的記憶體操作的原子性。處理器保證從系統記憶體中讀取或者寫入一個位元組是原子的,意思是當一個處理器讀取一個位元組時,其他處理器不能訪問這個位元組的記憶體地址。Pentium 6和最新的處理器能自動保證單處理器對同一個快取行裡進行16/32/64位的操作是原子的,但是複雜的記憶體操作處理器是不能自動保證其原子性的,比如跨匯流排寬度、跨多個快取行和跨頁表的訪問。但是,處理器提供匯流排鎖定和快取鎖定兩個機制來保證複雜記憶體操作的原子性。

    (1)使用匯流排鎖保證原子性 第一個機制是透過匯流排鎖保證原子性。

    所謂匯流排鎖就是使用處理器提供的一個LOCK#訊號,當一個處理器在匯流排上輸出此訊號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔共享記憶體。

    (2)使用快取鎖保證原子性 第二個機制是透過快取鎖定來保證原子性。

  6. 伺服器高併發解決方案

    • 應用資料與靜態資源分離 將靜態資源(圖片,影片,js,css等)單獨儲存到專門的靜態資源伺服器中,在客戶端訪問的時候從靜態資源伺服器中返回靜態資源,從主伺服器中返回應用資料。
    • 客戶端快取 因為效率最高,消耗資源最小的就是純靜態的html頁面,所以可以把網站上的頁面儘可能用靜態的來實現,在頁面過期或者有資料更新之後再將頁面重新快取。或者先生成靜態頁面,然後用ajax非同步請求獲取動態資料。
    • 叢集和分散式 (叢集是所有的伺服器都有相同的功能,請求哪臺都可以,主要起分流作用)
      (分散式是將不同的業務放到不同的伺服器中,處理一個請求可能需要使用到多臺伺服器,起到加快請求處理的速度。)
      可以使用伺服器叢集和分散式架構,使得原本屬於一個伺服器的計算壓力分散到多個伺服器上。同時加快請求處理的速度。
    • 反向代理 在訪問伺服器的時候,伺服器透過別的伺服器獲取資源或結果返回給客戶端。
  7. 抖動你知道是什麼嗎?它也叫顛簸現象

    剛剛換出的頁面馬上又要換入記憶體,剛剛換入的頁面馬上又要換出外存,這種頻繁的頁面排程行為稱為抖動,或顛簸。產生抖動的主要原因是程式頻繁訪問的頁面數目高於可用的物理塊數(分配給程式的物理塊不夠)

    為程式分配的物理塊太少,會使程式發生抖動現象。為程式分配的物理塊太多,又會降低系統整體的併發度,降低某些資源的利用率 為了研究為應該為每個程式分配多少個物理塊,Denning 提出了程式工作集” 的概念

相關文章