面试必会系列 - 4.1 程序员必须掌握的:计算机组成、操作系统知识点汇总
本文已收錄至 Github(MD-Notes),若博客中圖片模糊或打不開,可以來我的 Github 倉庫,包含了完整圖文:https://github.com/HanquanHq/MD-Notes,涵蓋了互聯網大廠面試必問的知識點,講解透徹,長期更新中,歡迎一起學習探討 ~
更多內容,可以訪問:
面試必會系列專欄:https://blog.csdn.net/sinat_42483341/category_10300357.html
操作系統系列專欄:https://blog.csdn.net/sinat_42483341/category_10519484.html
目錄
- 計組、操作系統
- CPU電路原理
- CPU的基本組成
- 超線程
- 計算機的組成
- 存儲器的層次結構
- 多核CPU
- 緩存行對齊偽共享問題
- 緩存行大小
- Intel CPU 緩存一致性協議
- 其他 CPU 緩存一致性協議
- 非同一訪問內存 NUMA
- 操作系統啟動過程
- 匯編語言(機器語言)的執行過程
- 名詞
- 常識
- 吞吐量:單位時間可以讀取的數據
- 總線的分類
- 進程、線程
- 進程
- 1、僵尸進程
- 2、孤兒進程
- 線程
- 思考:java線程是用戶級線程還是內核級線程?
- 協程(纖程)
- 纖程
- 纖程的優勢
- 原語
- 中斷、系統調用
- 用戶態、內核態
- mmap
- 內存管理
- LRU算法(解決內存不夠用的問題)
- 虛擬內存
- 缺頁中斷
- Linux 內核同步機制
- 關于同步理論的一些基本概念
- Linux 內核同步常用方法
- Linux VFS 虛擬文件系統
- 硬鏈接、軟鏈接
- 文件描述符 fd
- PageCache 頁緩存
- 為什么 Java 程序員不要使用直接 IO,而要使用 Buffered 形式的IO?
- Linux
- Linux 目錄樹
- Linux 文件類型
- Linux 常用命令
- 進程相關
- 網絡相關
計組、操作系統
匯編和操作系統代碼,又是不同層面的東西。匯編是針對CPU編程。系統內核方法,是針對操作系統編程。
CPU電路原理
CPU的基本組成
PC -> Program Counter 程序計數器 (記錄當前指令地址)
Registers -> 暫時存儲CPU計算需要用到的數據
ALU -> Arithmetic & Logic Unit 運算單元
CU -> Control Unit 控制單元
MMU -> Memory Management Unit 內存管理單元
超線程
一個ALU對應多個Registers
切換線程的時候不需要進行上下文切換,只需要切換到對應的Registers即可
計算機的組成
存儲器的層次結構
添加緩存,是為了解決 CPU 到不同部件的速度差別過大的問題
多核CPU
不同的核心有各自獨立的 L1,L2,但共享 L3 級緩存。為什么是3級?這是工業上的經驗值。
按塊讀取:根據程序局部性原理,提高效率,充分發揮總線CPU針腳一次性讀取更多數據的能力
讀取數據是通過 DMA 硬件進行的,直接內存訪問,用來做數據的搬運。通過協處理器,而不需要走CPU。偏向硬件,驅動開發的人員會接觸。
緩存行對齊偽共享問題
緩存行大小
緩存行越大,局部性空間效率越高,但讀取時間慢;緩存行越小,局部性空間效率越低,但讀取時間快。取一個折中值,目前 Intel CPU 緩存行64字節
Intel CPU 緩存一致性協議
怎么保證數據的一致性?Intel MESI 緩存一致性協議,數據四種狀態:
-
M: 被修改(Modified)
該緩存行只被緩存在該CPU的緩存中,并且是被修改過的(dirty),即與主存中的數據不一致,該緩存行中的內存需要在未來的某個時間點(允許其它CPU讀取請主存中相應內存之前)寫回(write back)主存。當被寫回主存之后,該緩存行的狀態會變成獨享(exclusive)狀態。
-
E: 獨享的(Exclusive)
該緩存行只被緩存在該CPU的緩存中,它是未被修改過的(clean),與主存中數據一致。該狀態可以在任何時刻當有其它CPU讀取該內存時變成共享狀態(shared)。同樣地,當CPU修改該緩存行中內容時,該狀態可以變成Modified狀態。
-
S: 共享的(Shared)
該狀態意味著該緩存行可能被多個CPU緩存,并且各個緩存中的數據與主存數據一致(clean),當有一個CPU修改該緩存行中,其它CPU中該緩存行可以被作廢(變成無效狀態(Invalid))。
-
I: 無效的(Invalid)
該緩存是無效的(可能有其它CPU修改了該緩存行)。
其工作原理是,當CPU1在獲取計算機內存i時,這是CPU2也可以獲取這個變量i。當CPU1對它取回的變量進行操作后。CPU1的i會被標記為被修改的(Modified),他需要將i寫入主內存將i標記為獨享狀態(Exclusive),這時CPU1會立即將i這個變量寫入計算機內存。這時計算機內存中的i會被標記為共享的(Shared)同時CPU2中的i會被標記為無效的(Invalid),然后CPU2就只能重新從計算機內存中獲取新的i變量。然后在進行CPU2中i的操作。
其他 CPU 緩存一致性協議
其他廠商的 CPU 也有緩存一致性協議,例如 MSI,MESI,MOSI,Synapse,Firefly,Dragon 等等。
盡管有緩存一致性協議,有些無法被緩存的數據,或者跨越多個緩存行的數據,依然必須使用總線鎖。
一種情況,假設有兩個線程想要修改的數據,位于同一個緩存行 cache line,你改的時候要通知我,我改的時候要通知你,于是因為緩存行的存在,相比沒有緩存行而言,反而效率會比較低。
于是誕生了一種編程模式:緩存行對齊,對于某些高競爭級別訪問的數據,為了保證不發生偽共享,可以采用緩存行對齊的編程方式,即,使用多個 long 類型的變量,將數據前后緩存行撐起來。例如:單機最快的內存消息隊列 disruptor,核心是一個 ring buffer,它的 cursor 前后分別放了 7個 long 類型數字。
JDK7中,很多采用long padding提高效率
JDK8,加入了@Contended注解(實驗)需要加上:JVM -XX:-RestrictContended
非同一訪問內存 NUMA
UMA:同一內存訪問。多個CPU通過一條總線,訪問同一個內存。
現在很多服務器的架構是使用NUMA的,因為UMA不以拓展:隨著CPU的數量增多,許多時間被浪費在CPU爭搶內存資源上。
NUMA:非同一訪問內存。每個CPU有自己專屬的內存,CPU對于自己插槽上的內存訪問是有優先級的。
ZGC 垃圾回收器可以做到 NUMA aware,如果探測到計算機實現了NUMA的話,分配內存會優先分配該線程所在CPU的最近內存。
操作系統啟動過程
通電 -> bios uefi 工作 -> 自檢 -> 到硬盤固定位置加載 bootloader -> 讀取可配置信息 -> CMOS
bootloader 在硬盤上的位置是固定的,放在硬盤的第一個扇區上
CMOS 用來存儲可以配置的信息,需要通電才能存儲信息,例如開機密碼。主板上有塊電池給它通電。
操作系統的第一條指令在硬盤中也是固定的
匯編語言(機器語言)的執行過程
匯編語言的本質:機器語言的助記符 其實它就是機器語言
計算機通電 -> CPU讀取內存中程序(電信號輸入)->時鐘發生器不斷震蕩通斷電 ->推動CPU內部一步一步執行(執行多少步取決于指令需要的時鐘周期)->計算完成->寫回(電信號)->寫給顯卡輸出(sout,或者圖形)
名詞
異地持久化
常識
秒 > 毫秒 > 微妙 > 納秒
磁盤尋址:毫秒級別(磁頭移動等)
內存尋址:納秒級別(磁盤、內存差距10W倍)
吞吐量:單位時間可以讀取的數據
磁盤IO:百兆級別(SATA3 幾百MB,PCI-E GB級別)
總線的分類
數據總線、地址總線、控制總線
進程、線程
進程是操作系統分配資源的基本單位,線程是操作系統執行調度的基本單位。
進程
- 進程 是處于執行期的程序以及相關資源的總稱,分配了獨立的內存空間
- 所有的進程都是 PID 為 1 的 init 進程的后代
- 內核把進程的列表放在任務隊列 task list 中,鏈表中的每一項都是 進程描述符
- 進程描述符包含:它打開的文件、進程的地址空間、掛起的信號、進程的狀態等
- 進程狀態
- 運行:進程是可執行的
- 可中斷:正在睡眠,等待某些條件的達成,可以因為收到信號而提前被喚醒
- 不可中斷:即使收到信號,也不會被喚醒
- 被其他進程跟蹤:例如通過 ptrace 對調試程序進行跟蹤
- 停止:進程沒有投入運行,也不能投入運行
- 進程的創建
- Linux 中,父進程通過 fork() 創建子進程,fork() 內部調用的是 clone()
- Linux 的 fork() 使用 寫時拷貝 頁實現
- 進程的調度
- Linux 使用 CFS(Completely Fair Scheduler 完全公平調度)
- 確保給每個進程公平的 處理器使用比,按 優先級 分配 時間片的比例,記錄每個進程的執行時間,如果有一個進程執行時間不到他應該分配的比例(比如給了你50%,你只用了10%),就優先執行這個進程作為補償。例如,一個是用戶鼠標移動優先級高,另一個是 IO 密集型優先級低
- 用 紅黑樹 維護 可運行進程隊列,每次運行紅黑樹中 最左邊葉子結點 代表的進程
- 分為實時進程(始終優先于普通進程)、普通進程(根據 nice 值分配時間片)
- Linux 使用 CFS(Completely Fair Scheduler 完全公平調度)
1、僵尸進程
在大多數情況下,少量僵尸進程的存在,對操作系統沒有太大影響,它基本不再占用任何資源了,它在內存中唯一占用的資源就是PCB了。僵尸進程的產生原因,有可能父進程是一個daemon進程,C程序開發人員在父進程沒有釋放子進程,這時會出現僵尸進程。可以使用內核中的wait函數,釋放僵尸進程。
2、孤兒進程
孤兒進程產生的原因:子進程還活著,父進程掛了。孤兒進程和孤兒線程沒有太大區別,因為在Linux中,進程和線程沒有太大區別。孤兒進程的影響:影響不大。
線程
- 線程是在進程中活動的對象
- 內核調度的對象是 線程,而不是 進程
- Linux 把所有的線程都當做進程來實現,線程僅僅被視為一個與其他進程共享某些資源的進程,只不過和其他線程共享同一個主進程中的內存空間 ——《Linux內核設計與實現》
- 線程共享進程的內存空間,沒有自己獨立的內存空間
思考:java線程是用戶級線程還是內核級線程?
java 線程不是純用戶級線程:java中有個 fork join 框架,利用多處理技術進行 maprudce 的工作,證明了內核是可以感知到用戶線程的存在,因此才會將多個線程調度到多個處理器中。
java應用程序中的某個線程阻塞,是不會引起整個進程的阻塞。
java 線程不是純內核級線程:如果使用純粹的內核級線程,那么有關線程的所有管理工作都是內核完成的,用戶程序中沒有管理線程的代碼。顯然,java線程庫提供了大量的線程管理機制,因此java線程絕不是純粹的內核級線程。
馬士兵:“ JVM 線程和內核中的線程是 1:1 的關系”
綜上,java線程是混合型的線程模型,一般而言,是通過lwp將用戶級線程映射到內核線程中。
補充:《現代操作系統》P79:“Java 支持用戶級線程”
協程(纖程)
JVM(HotSpot)和操作系統的線程是一一對應的:JVM空間的一個線程,對應操作系統中的一個線程,這是重量級線程。
操作系統可以開啟的線程是有限的(1萬個已經很卡了),而纖程是用戶空間的,不需要與內核打交道,輕量級,切換速度快,可以啟動幾萬個,甚至幾十萬個都沒有問題。
纖程
纖程可以被理解為:線程里的線程,在JVM層級用軟件進行內部的線程協調。
纖程是被 JVM 管理,運行在用戶態,操作和調度不經過操作系統內核。
關于纖程的實現:纖程可以分配自己獨立的纖程棧,相當于自己實現了一個小小的操作系統級別的調度程序。
Java 通過 Quaser 類庫可以支持纖程,但目前還不是很成熟,據說如果大規模應用在生產級別的話,還是有一些小bug存在的。
纖程的優勢
1、占有資源很少。操作系統啟動一個線程,占用 1M 的內存空間,而啟動一個纖程只需要 4K 空間
2、由于輕量級,切換簡單
3、由于輕量級,可以啟動幾十萬個纖程都沒有問題。
原語
- 是一種特殊的程序,運行在 內核態
- 處于操作系統最底層,是最接近硬件的部分
- 這種程序的運行具有原子性,只能一氣呵成,不可被中斷
- 運行時間較短、調用頻繁
- 原語采用“關中斷指令”和“開中斷指令”實現
中斷、系統調用
系統調用表及源碼鏈接 https://filippo.io/linux-syscall-table/
系統調用 是用戶空間訪問內核的唯一手段,除 異常 和 陷入 外,它是內核的唯一合法入口。
Linux 的系統調用,作為 C 庫的一部分提供,用戶空間進程 用 系統調用號 來指明執行哪個系統調用。
進行系統調用的步驟:
用戶態、內核態
Intel CPU 分為三級:ring 0,1,2,3三個級別
Linux 只使用了 0,3 兩個級別。
Linux 內核是運行在 ring 0 級別的,能訪問所有指令;應用程序只能訪問 Ring 3 級。
用戶空間的應用程序運行在 ring 3 級別,某些指令是不能被訪問的,想要讀硬盤、寫硬盤、寫網卡等,對于系統的關鍵訪問,需要通過操作系統內核,因此需要轉換為內核態。正是因為這樣的分層,使得現在的Linux,Windows非常健壯,不會輕易被程序搞崩。
Linux 內核提供了 200 多個系統調用,例如 sendfile read write pthread fork等對外暴露的函數。
內核空間:只有內核能訪問的空間。用戶是不可能將這塊內存干掉的。
用戶空間:用戶可以直接操作的空間。
mmap
mmap將 用戶的線性地址空間 直接映射到了 內核的 pagecache 地址,如果是臟的需要寫的話,依然受pagecache 的影響,才能最終刷寫到磁盤中去。
內存管理
DOS時代 - 同一時間只能有一個進程在運行(也有一些特殊算法可以支持多進程)
windows9x - 多個進程裝入內存存在的問題:
- 內存不夠用
- 互相打擾
為了解決這兩個問題,誕生了現在的內存管理系統:使用虛擬地址、分頁裝入、軟硬件結合尋址。
將內存分頁(因為內存不夠用),內存中分成固定大小的頁框4K,把硬盤上的程序也分成4K大小的塊。另外維護一個頁框page frame,用到哪一塊,就將哪一塊加載進內存中。
例如,執行QQ.exe時,把它的頁表記錄下來,執行時,用到頁表中的哪一頁,就將這頁加載進內存中。
在加載的過程中,如果內存已經滿了,會把最不常用的一塊放到swap分區, 把最新的一塊加載進來,這個就是著名的LRU算法。這就是交換分區的由來。
LRU算法(解決內存不夠用的問題)
LeetCode 146 題,頭條要求15分鐘內手撕,阿里去年也要求手撕。
只允許使用 HashMap 實現 LRU 算法,哈希表(保證查找操作O(1)) + 雙向鏈表 (保證排序操作和新增操作 O(1)))
- LRU (Least recently used) 最近最少使用,如果數據最近時間被訪問過,那么將來被訪問的幾率也更高。
- LFU (Least frequently used) 最不經常使用,如果一個數據在最近一段時間內使用次數很少,那么在將來一段時間內被使用的可能性也很小。
虛擬內存
以DOS Win31 …這類系統為例,A進程、B進程是可以互相操作內存的。為了保證互不影響,讓進程工作在虛擬空間。在程序中用到的空間地址不再是直接的物理地址,而是虛擬的地址,這樣,A進程永遠不可能訪問到B進程的空間。
**虛擬空間多大呢?**虛擬空間的大小就是尋址空間,要看操作系統是多少位的。例如,64位系統的虛擬空間是2^64,32為系統的虛擬空間是2^32。虛擬空間比物理空間大很多 ,單位是 byte
**為什么使用虛擬內存?**站在虛擬的角度,進程是獨享整個系統 + CPU
**地址是怎么映射的?**內存映射:偏移量 + 段的基地址 = 線性地址 (虛擬空間)
線性地址,是通過 OS + MMU(Memory Management Unit內存管理單元)來映射到真正的物理地址。只有操作系統內核知道虛擬內存中地址對應的真正的物理地址,應用程序是不知道的,這樣保證了系統的安全。
缺頁中斷
進程P1,P2,P3,P4都認為自己是獨占整個內核的,實際上是共享操作系統內核。
MMU給每一個進程分配他們的內存資源。
如果內存裝滿了,使用LRU算法將最不常使用的頁放入硬盤的交換空間中。
在執行一條指令時,如果發現需要用到頁在內存中沒有,那么停止該指令的執行,并產生一個缺頁異常(中斷),由內核處理并加載,之后,原先引起的異常的指令就可以繼續執行,而不再產生異常。
Linux 內核同步機制
關于同步理論的一些基本概念
- 臨界區(critical area): 訪問或操作共享數據的代碼段
簡單理解:synchronized大括號中部分(原子性) - 競爭條件(race conditions)兩個線程同時擁有臨界區的執行權
- 數據不一致:data unconsistency 由競爭條件引起的數據破壞
- 同步(synchronization)避免race conditions
- 鎖:鎖是完成同步的手段(門鎖,門后是臨界區,只允許一個線程存在)
上鎖解鎖必須具備原子性(如果連上鎖的過程都會被打斷的話,就沒有什么意義了) - 原子性(象原子一樣不可分割的操作)
- 有序性(禁止指令重排)
- 可見性(一個線程內的修改,另一個線程可見)
互斥鎖 排他鎖 共享鎖 分段鎖
Linux 內核同步常用方法
了解一下Linux內核實現的一些同步方法,了解一些關于線程控制的理論知識,可以去印證Java層面的同步知識。
用的話要用C語言去調用。
相當于:讀的時候是共享鎖,寫的時候是排他鎖
是一個重量級鎖,線程會進入wait,適合長時間持有的鎖情況
(多個寫,可以分段寫,比較少用)(分段鎖)
vfork() 在子進程結束時通過完成變量叫醒父進程 類似于(Latch)
在網絡編程中有完成端口(Completion Port)
序列計數器(從0開始,寫時增加(+1),寫完釋放(+1),讀線程不對計數器做任何操作)
寫的時候,不妨礙讀線程:
如果讀線程發現是偶數,說明在讀的時候沒有任何人改變過。
如果讀線程發現是奇數,說明你讀到的可能是中間狀態,你可以選擇繼續自旋,等待值變為偶數。
讀前讀后序列一樣,說明沒有寫線程打斷。
Linux VFS 虛擬文件系統
Virtual Filesystem Switch,虛擬文件系統,是一個目錄樹。樹上不同的節點可映射到物理的文件地址,可掛載。
相當于一個解耦層,在具體的文件系統之上抽象的一層,為能夠給各種文件系統提供一個通用的接口,使上層的應用程序能夠使用通用的接口訪問不同文件系統、不同的驅動。
硬鏈接、軟鏈接
- 硬鏈接
- 兩個變量名指向了同一個物理位置,硬鏈接的文件擁有相同的 inode,操作系統是靠inode來區分文件的。2個inode相同的文件,代表它們是一個文件。
- 如果刪掉了其中一個文件,另外一方還能找到這個文件。相當于只是刪除了一個引用。除非你把硬鏈接和源文件都刪除, 這個文件才被刪除。
- 軟鏈接
- 軟鏈接是兩個獨立的文件,相當于創建了一個“快捷方式”
- 文件的共享用戶只有該文件的 路徑名,只有文件擁有者才擁有 指向其索引節點的指針
- 當符號鏈接被刪除時,并不會影響源文件。但是當源文件被刪除時,符號鏈接就找不到源文件了,會標紅報錯。
- 軟鏈接有著 自己的 inode 號 以及用戶數據塊。
- 軟鏈接可跨文件系統,硬鏈接不行
- 共性
- 無論是硬鏈接還是軟連接,如果修改任意一方,另外一個文件也會看到這個變化。
文件描述符 fd
任何程序都有:0 標準輸入,1 標準輸出, 2 報錯輸出
-
lsof 列出系統中打開的文件
lsof -op $$看見當前進程的文件描述符的細節,包括偏移量、指針等等
-
/proc/$$/fd是當前程序的所有的文件描述符
PageCache 頁緩存
PageCache 通常 4K,本來是用來優化IO的性能(優先走內存),但它的缺點是刷寫硬盤不及時,在突然關機或異常斷電時,有丟失數據的可能
為什么 Java 程序員不要使用直接 IO,而要使用 Buffered 形式的IO?
一次寫一個緩沖區大小,減少調用 write 的次數,只不過是每一次寫入的數據量比較大。減少了用戶態到內核態的來回切換帶來的性能損耗。
- ByteBuffer
- allocate將字節數組分配到了堆上,是 JVM 堆內 的線性地址空間
- allocateDirect將字節數組分配到 JVM 的堆外 內存中,是 C 語言可以直接訪問的。
操作系統沒有絕對的數據可靠性。為什么要設計 pagecache,是為了減少硬件的IO的調用,想要優先使用內存,這樣能夠提速。如果追求性能,就要在一致性、可靠性之間做出權衡。
從大方面來看,在現在的分布式系統當中,如果你追求數據存儲的可靠性(保持緩存和磁盤的強一致,對于每一次對數據的微小改變,都要去刷寫磁盤),仍然避免不了單點故障的問題。單點故障會讓你為了保持強一致而耗費的能損耗一毛錢收益都沒有。
這就是為什么我們使用主從復制、主備HA
這就是為什么 Kafka,ES 都有副本的概念,而副本是從 socket 得到的。副本又分為同步的異步的區別,這些都是后話了,我們以后再講…
Linux
Linux 目錄樹
常見目錄說明:
- /bin: 存放二進制可執行文件(ls、cat、mkdir等),常用命令一般都在這里;
- /etc: 存放系統管理和配置文件;
- /home: 存放所有用戶文件的根目錄,是用戶主目錄的基點,比如用戶user的主目錄就是/home/user,可以用~user表示;
- /usr : 用于存放系統應用程序;
- /opt: 額外安裝的可選應用程序包所放置的位置。一般情況下,我們可以把tomcat等都安裝到這里;
- /proc: 虛擬文件系統目錄,是系統內存的映射。可直接訪問這個目錄來獲取系統信息;
- /root: 超級用戶(系統管理員)的主目錄(特權階級o);
- /sbin: 存放二進制可執行文件,只有root才能訪問。這里存放的是系統管理員使用的系統級別的管理命令和程序。如ifconfig等;
- /dev: 用于存放設備文件;
- /mnt: 系統管理員安裝臨時文件系統的安裝點,系統提供這個目錄是讓用戶臨時掛載其他的文件系統;
- /boot: 存放用于系統引導時使用的各種文件;
- /lib : 存放著和系統運行相關的庫文件 ;
- /tmp: 用于存放各種臨時文件,是公用的臨時文件存儲點;
- /var: 用于存放運行時需要改變數據的文件,也是某些大文件的溢出區,比方說各種服務的日志文件(系統啟動日志等。)等;
- /lost+found: 這個目錄平時是空的,系統非正常關機而留下“無家可歸”的文件(windows下叫什么.chk)就在這里。
Linux 文件類型
Linux支持很多文件類型,其中非常重要的文件類型有: 普通文件,目錄文件,鏈接文件,設備文件,管道文件,Socket套接字文件等。
Linux 常用命令
進程相關
ps -ef / ps -aux: 查看當前系統正在運行進程,展示格式不同。如果想查看特定的進程,使用:ps aux | grep redis (查看包括redis字符串的進程),也可使用 pgrep redis
kill -9 進程pid: 殺死進程(-9 表示強制終止)
網絡相關
網絡通信命令:
- 查看當前系統的網卡信息:ifconfig / ip -a
- 查看當前系統的端口使用:netstat -an
總結
以上是生活随笔為你收集整理的面试必会系列 - 4.1 程序员必须掌握的:计算机组成、操作系统知识点汇总的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java多线程:示例代码
- 下一篇: 操作系统例题:某文件系统中,针对每个文件