你一定要知道的計算機底層知識

前言

雖然在程序員的職業生涯中,計算機底層知識可能很少直接涉及,但並不意味着這部分知識不重要。

對於計算機底層實現的深入瞭解,能幫助你瞭解計算機的運行原理,更好地設計高效的架構,並且有助於調試、判斷錯誤。特別地,對於多線程的理解尤爲重要:現今的程序架構都需要併發處理,如何協調不同線程之間的分工協作,避免死鎖、同步出錯等問題,是程序員應當具備的技能。對於後端工程師而言,良好的操作系統基礎知識更是深刻理解並實現複雜分佈式系統的前提條件。

進程 vs.線程


進程( process)與線程( thread)最大的區別是:進程擁有自己的地址空間,某進程內的線程對於其他進程不可見,即進程 A 不能通過傳地址的方式直接讀寫進程 B 的存儲區域。進程之間的通信需要通過進程間通信( Inter-Process Communication, IPC)。與之相對的,同一進程的各線程間之間可以直接通過傳遞地址或全局變量的方式傳遞信息。

此外,進程作爲操作系統中擁有資源和獨立調度的基本單位,可以擁有多個線程。通常操作系統中運行的一個程序就對應一個進程。在同一進程中,線程的切換不會引起進程切換。在不同進程中進行線程切換,如從一個進程內的線程切換到另一個進程中的線程時,會引起進程切換。相比進程切換,線程切換的開銷要小很多。線程於進程相互結合能夠提高系統的運行效率。

線程可以分爲兩類:

一類是用戶級線程( user level thread)。對於這類線程,有關線程管理的所有工作都由應用程序完成,內核意識不到線程的存在。在應用程序啓動後,操作系統分配給該程序一個進程號,以及其對應的內存空間等資源。應用程序通常先在一個線程中運行,該線程稱爲主線程。在其運行的某個時刻,可以通過調用線程庫中的函數創建一個在相同進程中運行的新線程。 用戶級線程的好處是非常高效,不需要進入內核空間,但併發效率不高。

另一類是內核級線程( kernel level thread)。對於這類線程,有關線程管理的所有工作由內核完成,應用程序沒有進行線程管理的代碼,只能調用內核線程的接口。內核維護進程及其內部的每個線程,調度也由內核基於線程架構完成。內核級線程的好處是,內核可以將不同線程更好地分配到不同的 CPU,以實現真正的並行計算。

事實上,在現代操作系統中,往往使用組合方式實現多線程,即線程創建完全在用戶空間中完成,並且一個應用程序中的多個用戶級線程被映射到一些內核級線程上,相當於是一種折中方案。


上下文切換

對於單核單線程 CPU 而言,在某一時刻只能執行一條 CPU 指令。上下文切換( Context Switch)是一種將 CPU 資源從一個進程分配給另一個進程的機制。從用戶角度看,計算機能夠並行運行多個進程,這恰恰是操作系統通過快速上下文切換造成的結果。在切換的過程中,操作系統需要先存儲當前進程的狀態(包括內存空間的指針,當前執行完的指令等等),再讀入下一個進程的狀態,然後執行此進程。


系統調用

系統調用( System Call)是程序向系統內核請求服務的方式。可以包括硬件相關的服務(例如,訪問硬盤等),或者創建新進程,
調度其他進程等。系統調用是程序和操作系統之間的重要接口。

 

Semaphore/Mutex

當用戶創立多個線程/進程時,如果不同線程/進程同時讀寫相同的內容,則可能造成讀寫錯誤,或者數據不一致。此時,需要通過加
鎖的方式,控制核心區域( critical section)的訪問權限。對於semaphore 而言,在初始化變量的時候可以控制允許多少個線程/進
程同時訪問一個核心區域,其他的線程/進程會被堵塞,直到有人解鎖。 Mutex 相當於只允許一個線程/進程訪問的 semaphore。此外,根據實際需要,人們還實現了一種讀寫鎖( read-write lock),它允許同時存在多個讀取者( reader),但任何時候至多隻有一個寫入者( writer),且不能與讀取者共存。

 

死鎖

在引入鎖的同時,我們遇到了一個新的問題:死鎖( Deadlock)。

死鎖是指兩個或多個線程/進程之間相互阻塞,以至於任何一個都不能繼續運行,因此也不能解鎖其他線程/進程。例如,線程 A 佔有 lockA,並且嘗試獲取 lock B;而線程 2 佔有 lock B,嘗試獲取 lock A。此時,兩者相互阻塞,都無法繼續運行。產生死鎖的 4 個條件概括如下(只有當 4 個條件同時滿足時纔會產生死鎖):

  1. 互斥條件:一個資源每次只能被一個進程使用,即在一段時間內某 資源僅爲一個進程所佔有。此時若有其他進程請求該資源,則請求進程只能等待。
  2. 請求與保持條件:進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他進程佔有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。
  3. 不可剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能 由獲得該資源的進程自己來釋放(只能是主動釋放)。
  4. 循環等待條件: 若干進程間形成首尾相接循環等待資源的關係

 

生產者消費者


生產者消費者模型是一種常見的通信模型:生產者和消費者共享一個數據管道,生產者將數據寫入 buffer,消費者從另一頭讀取數
據。對於數據管道,需要考慮爲空和溢出的情況。同時,通常還需要將這部分共享內存用 mutex 加鎖。在只有一個生產者一個消費者的情況下,可以設計無鎖隊列( lockless queue),線程安全地直接讀寫數據。

 

進程間通信
 

在介紹進程的時候,我們提起過一個進程不能直接讀寫另一個進程的數據,兩者之間的通信需要通過進程間通信進行。進程通信的方
式通常遵從生產者消費者模型,需要實現數據交換和同步兩大功能。
( 1)共享內存( Shared-memory) + semaphore
       不同進程通過讀寫操作系統中特殊的共享內存進行數據交換,進程之間用 semaphore 實現同步。
( 2)信息傳遞( Message passing)
      進程在操作系統內部註冊一個端口,並且監測有沒有數據,其他進程直接寫數據到該端口。該通信方式更加接近於網絡通信方式。事實上, 網絡通信也是一種 IPC,只是進程分佈在不同機器上而已。


邏輯地址/物理地址/虛擬內存
 

所謂的邏輯地址,是指計算機用戶(例如程序開發者)看到的地址。例如,當創建一個長度爲 100 的整型數組時,操作系統返回一個
邏輯上的連續空間:指針指向數組第一個元素的內存地址。由於整型元素的大小爲 4 個字節,故第二個元素的地址時起始地址加 4,以此類推。事實上,邏輯地址並不一定是元素存儲的真實地址,即數組元素的物理地址(在內存條中所處的位置),物理地址並不是連續的,只不過操作系統通過地址映射,將邏輯地址映射成連續的,這樣更符合人們的直觀思維。

另一個重要概念是虛擬內存。操作系統讀寫內存的速度可以比讀寫磁盤的速度快幾個量級。但是,內存價格也相對較高,不能大規模
擴展。於是,操作系統可以將部分不太常用的數據移出內存,存放到價格相對較低的磁盤緩存,以實現內存擴展。操作系統還可以通過算法預測哪部分存儲到磁盤緩存的數據需要進行讀寫,提前把這部分數據讀回內存。虛擬內存空間相對磁盤而言要小很多,因此,即使搜索虛擬內存空間也比直接搜索磁盤要快。唯一慢於磁盤的可能是,內存、虛擬內存中都沒有所需要的數據,最終還需要從硬盤中直接讀取。這就是爲什麼內存和虛擬內存中需要存儲會被重複讀寫的數據,否則就
失去了緩存的意義。

現 代 計 算 機 中 有 一 個 專 門 的 轉 譯 緩 衝 區 ( Translation Lookaside Buffer, TLB),用來實現虛擬地址到物理地址的快速轉換。與內存/虛擬內存相關的還有以下兩個概念:

      ( 1) Resident Set
       當一個進程在運行的時候,操作系統不會一次性加載進程的所有數據到內存,只會加載一部分正在用,以及預期要用的數據。其他數據可能存儲在虛擬內存,交換區和硬盤文件系統上。被加載到內存的部分就是 resident set。

      ( 2) Thrashing
       由於 resident set 包含預期要用的數據,理想情況下,進程運行過程中用到的數據都會逐步加載進 resident set。但事實往往並非如此:每當需要的內存頁面( page)不在 resident set 中時,操作系統必須從虛擬內存或硬盤中讀數據,這個過程被稱爲內存頁面錯誤( page faults)。當操作系統需要花費大量時間去處理頁面錯誤的情況就是 thrashing。
 

文件系統

UNIX 風格的文件系統利用樹形結構管理文件。每個節點有多個指針,指向下一層節點或者文件的磁盤存儲位置。文件節點還附有文件的操作信息( metadata),包括修改時間、訪問權限等。用戶的訪問權限通過訪問控制表( Access Control List)和能力表( Capability List)實現。前者從文件角度出發,標註了每個用戶可以對該文件進行何種操作。後者從用戶角度出發,標註了某用
戶可以以什麼權限操作哪些文件。

UNIX 的文件權限分爲讀、寫和執行,用戶組分爲文件擁有者、組和所有用戶。可以通過命令對三組用戶分別設置權限。

 

實時 vs.分時操作系統

操作系統可以分爲實時操作系統( Real-Time System),和分時操作系統( Sharing Time System)。通常計算機採用的是分時,即多個進程/用戶之間共享 CPU,從形勢上實現多任務。各個用戶/進程之間的調度並非精準度特別高,如果一個進程被鎖住,可以給它分配更多的時間。而實時操作系統則不同,軟件和硬件必須遵從嚴格的deadline,超過時限的進程可能直接被終止。在這樣的操作系統中,每次加鎖都需要仔細考慮。

 

編譯器

對於高級語言來說,代碼需要通過編譯才能夠運行。編譯通過編譯器( Compiler)實現,是一個將程序源代碼轉換成二進制機器碼的
過程。計算機可以直接執行二進制代碼。在編譯的過程中,編譯器需要進行詞法分析( Lexical Analysis)、解析( Parsing)和過渡代碼生成( Intermediate Code Generation)。編譯器的好壞可以直接影響最終代碼的執行效率。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章