【操作系統】CSAPP學習筆記

CSAPP學習筆記

前言

在閱讀本書前,最好先了解一下書本的結構,然後根據結構,網上查查網評。最好能找到一些最佳閱讀技巧。可以給自己定一個大一點的目標,比如,期望讀完這本書,可以自己設計一個操作系統。而不是僅僅學會給代碼調優(這個目的會讓你走火入魔)。不要企圖或者期待這本書給你帶來很多開箱即用的最佳實踐。你不是在刷算法題。

閱讀時的模式:明白技術點所要解決的問題,以及如何解決的。(問題驅動閱讀)

瞭解書的目錄:

## 第一部分 程序結構和執行
  - 數據類型
  - 程序指令,系統是如何執行程序指令操作數據的
  - CPU結構,CPU是怎麼工作的?
  - 談談花裏花俏的程序性能優化(其實還不如學算法優化實在)
  - 存儲,瞭解數據可以存在操作系統的什麼地方,CPU?內存?硬盤?
前言:這一章核心,計算機怎麼處理數據,計算機怎麼處理指令,計算機怎麼存儲數據。簡而言之,就是操作系統自身是怎麼工作。

## 第二部分 在系統上運行程序
  - 鏈接,怎麼從文件種將雜七雜八的代碼組裝起來(製作圖靈機的紙帶?)
  - 異常處理,如果用戶輸入垃圾代碼,操作系統怎麼擦屁股,收拾爛攤子?
  - 操作系統,怎麼提供自己的存儲空間給用戶呢?(聊聊地主怎麼分地皮)

前言:程序是操作系統提供用戶執行作業的接口。瞭解操作系統是怎麼提供這個接口給用戶。簡而言之,就是了解操作系統怎麼對提供服務,對就是你想的那種服務。側重於異常和存儲分配(指令這些就不管啦,CPU內部封裝的,對用戶不可見)。

## 第三部分 程序間的交互和通信
 - 同一個操作系統,進程怎麼聊
 - 不同操作系統,進程怎麼聊
 - 操作系統是一個單例,所以怎麼處理多個進程資源複用。

前言:系統是一個房子,程序就是住戶,怎麼通信和共同生活,也是一個問題。

本書分爲3大部分:講講操作系統怎麼工作,講講用戶怎麼讓操作系統工作,講講怎麼讓操作系統有條不紊的完成用戶的多個任務。

所以整本書的最終目的就是,搞清楚,怎麼讓CPU,內存,硬盤,幫用戶做很多工作。

簡化後的目錄:

一,操作系統
	- 數據
	- 指令
    - 存儲
    - 
二,程序接口
	- 編譯
	- 異常
	- 內存

三,進程通信
	- 系統IO
	- 網絡編程
	- 併發


一,操作系統

如果想要做操作系統,這一部分的知識就是入門材料了。

1.數據

簡單快讀

  • 瞭解位運算法則,與或非異或
  • Integer最高位存放負數,其他位正常表示。(這個簡單)
  • double浮點提供了高11位用於記錄指數階級。52位記錄尾數,以及高於尾數後丟精度的特性。(根據IEEE規則,float也一樣)
  • 有符號比無符號表示法,在正數值空間少一半。
  • 補碼技術可以使得CPU只用加法器就能進行減法運算。
  • 計算機怎麼計算乘除法呢?
  • 左移帶不帶符號呢?假如帶符號位一起移動,符號最高位會消失,java中不給帶符號位移動,所以<<<是無效操作。
  • 正數帶不帶符號移動,都一個樣子。源碼等於補碼。
  • 負數的話,不帶符號好說。不帶符號,就是和整數一樣。
  • 帶了符號就不好說了。因爲他是以補碼的形式移動的。而且-1怎麼右移都是-1.因爲負數的補碼和原碼不一樣。
  • 時刻注意溢出,計算機運算不是數學運算,隨時準備溢出。
  • 浮點數的出現是爲了用小空間表示大數而設計的,這個做法就是隻記錄大數的前幾位,以及他的數量級,卻不精確的記錄所有位,因爲這個成本太高了。比如記錄太陽到地球的距離,如果要記錄精確記錄每一位,不要說計算機,用全地球的木材做成草稿紙都不夠寫。
  • 浮點數怎麼掉精度呢?
  • 補碼技術怎麼代替減法器呢?
  • 什麼是規範值,什麼是非規範值?什麼是特殊值?他們的意義是什麼?
  • 浮點數的加法乘法。
  • 浮點的四捨五入也是和常量不一樣,比如2.885會得到2.88.而2.895卻是2.90
  • 認識什麼是大端數,什麼是小端數。我們平常看到的數是都是大端數,計算機CPU比較特殊,採用小端數。互聯網屬於多屬於應用層的產品,所以也理應用了大端數(小端數太奇葩了)。

2.指令

瞭解概念

2.1 彙編語言操作對象-寄存器,指令棧

  • 馮諾依曼體把數據和指令混合存儲再內存中,CPU怎麼區分的?
  • CPU結構,除了寄存器,PC,加法器,還有什麼?CPU的本質功能是什麼?
  • CPU怎麼定位數據?
  • CPU的奇淫巧計----棧與隊列?
  • 內存怎麼存數據和指令?
  • 計算機高度抽象後是什麼,CPU,內存,程序又在其中充當什麼角色。
  • 機器碼纔是計算機的指令和數據的集合。彙編碼和C語言是接口用戶,這就意味着,需要在接口進行預處理,因此,我們定義,【 語法 + 編譯工具】 = 編程語言。
  • 從CPU接口的角度上來說,機器碼和C語言都是一樣的,面向用戶的程序接口。只是友好度不一樣,但都是給用戶操作CPU用的語言。
  • 我們把 【操作符 + 數據 = 指令】 並把指令存放在內存,這就是所謂的馮諾依曼體。CPU執行的指令本身包含了數據。指令在內存看來就是數據了,但是對於CPU來說又是一個操作命令。因爲馮諾依曼體在物理層不區分數據和操作。
  • PC指針,執行了CPU將要執行的命令,所以我們可以理解爲CPU本質就是一個不停讀取PC的while循環。
    PC象徵着無盡的命令。CPU象徵無盡的循環,其次寄存器可以看成PC的附屬品,用於寄放數據。

2.2 指令的種類

  • 而PC的命令有1個典型的:修改命令,一切加減乘除和跳轉命令都是修改操作。CPU爲了提高修改的性能,
    發明了幾個專用器件,比如加法器,條件跳轉器等等。
  • 我們沒有辦法用一條指令完成內存間的數據交換。所有指令一定要寄存器參與,因爲CPU只能接觸寄存器做數據修改。
  • 大於,等於,小於,(溢出).是CPU專門爲條件if語句設計的器件,可以理解爲【比較器】,就好像加法器一樣。
  • 所有的switch,while,if,等於條件比較有關的都會由比較器來實現。

3.存儲

瞭解結構

3.1 物理知識

  • 硬盤雖然是舊社會的產品,但確實一種很耐用很實在的東西,尤其是微服務和大數據橫行,有必要認真學習。
  • 靜態RAM和動態RAM的區別,動態RAM需要經常刷新且便宜。但他們都是內存
  • ROM用來寫驅動和BIOS(系統引導程序)。
  • 硬盤的核心部件,磁頭,主軸,磁盤。
  • 機械硬盤有許多片磁盤(platter)組成,每一片磁盤有兩面;每一面由一圈圈的磁道(track)組成,而每個磁道會被分隔成不同的扇區(sector)。這裏概念層層遞進,可以結合下圖仔細辨析清楚。
  • 最小存儲單位是某一個磁道上的某一個扇區。
  • 存儲座標:磁盤片,片的面,磁道,扇區。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ogaDAsod-1588815445133)(:/076d018da076413398072526001b4a70)]

  • 外磁道比內磁道能存更多數據。(像廢話)
  • 容量 Capacity = 每個扇區的字節數(bytes/sector) x 磁道上的平均扇區數(avg sectors/track) x 磁盤一面的磁道數(tracks/surface) x 磁盤的面數(surfaces/platter) x 硬盤包含的磁盤數(platters/disk)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gtBlVhJj-1588815445143)(:/ee8c55044e7542358d12fe96c3e8709d)]

  • 假設我們現在已經從藍色區域讀取完了數據,接下來需要從紅色區域讀,首先需要尋址,把讀取的指針放到紅色區域所在的磁道,然後等待磁盤旋轉,旋轉到紅色區域之後,纔可以開始真正的數據傳輸過程。

  • 總的訪問時間 Taccess = 尋址時間 Tavg seek + 旋轉時間 Tavg rotation + 傳輸時間 Tavg transfer

    • 尋址時間 Tavg seek 因爲物理規律的限制,一般是 3-9 ms
    • 旋轉延遲 Tavg rotation 取決於硬盤具體的轉速,一般來說是 7200 RPM
    • 傳輸時間 Tavg tranfer 就是需要讀取的扇區數目
  • 磁盤訪問大部分時間花在尋址上,讀取扇區是很快的過程。

  • 尋址包括了,選盤面,選磁道,和轉盤。轉盤一般是4ms,選盤是4ms,選磁道也是4ms.

  • 硬盤比 SRAM 慢 40,000 倍,比 DRAM 慢 2500 倍。

  • 固態硬盤是閃存,他的分級爲:塊–》頁。一頁爲512k,寫入時頁爲單位,刷存時塊爲單位。刷存影響壽命。

  • 在硬件上看來,所有的設備都是連接在一條總線上,由CPU通過信號驅動。CPU的信號可用觸達任何存儲模塊。但是數據的提交過程卻時層層上報的,也就是說,硬盤的數據需要加載到內存,才能被CPU讀取到cache(儘管有總線,但無法直接傳,因爲IO速度差太大)。

3.2 系統軟件知識:

  • 關於計算機程序的幾個現象:
    • 時間局部性,如果一個數據剛被訪問,那麼他很可能會再被訪問,所以計算機到處是緩存。
    • 空間局部性,如果一個數據被訪問了,那麼他周圍的數據也很可能被方位,順序訪問。
    • 指令局部性,大部分的程序指令都不會到處goto,不會瞎跳。
  • CPU的緩存有寄存器和L1級高速緩存和L2高速緩存(其實是SRAM)。
  • 但是CPU還有一個很特別的寄存器,叫TLB,虛擬地址緩存。這個東西可以不要,但是由於操作系統大多是通過虛擬地址加頁基準地址來尋址的,所以TLB存放頁基準地址可以加速內存地址翻譯速度,所以可以加速地址翻譯。
  • 另外一個事實,低級存儲的都把數據緩存在高一級的存儲空間裏。
  • 一個有趣的事實:網絡通信IO緩存比硬盤慢,所以大部分網絡數據都是緩存在硬盤裏的。然後再按需要讀取到內存提供給CPU。(比如瀏覽器的頁面緩存)
  • 但是,在互聯網時間,大併發高速網絡IO需求背景,如果數據傳輸要經過硬盤才能讀取是相當慢。因此直接內存代替了硬盤上的網絡緩存,【零拷貝技術】孕育而生。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-182SpaCj-1588815445146)(:/ce42c1c91ad747ce87a62af2212b89b7)]

通過這種圖,我們可以知道:

  • 內存纔是操作系統級別的,高速緩存cache等是編譯器和硬件級別的。
  • 怎麼纔可以讓緩存利用率最大呢?除了操作系統給我們做了足夠保證,我們程序該如何優化呢?
  • 緩存失效的原因是什麼?未命中!爲什麼未命中?緩存爲空,or 緩存數據不完整不合法 or 訪問的數據大於緩存。
  • 程序是操作系統的概念。內核程序也是程序。任何計算機至少都有一個程序在運行,那就是操作系統的內核程序。

3.3 再聊一聊硬件知識–緩存級別

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ngdleEly-1588815445148)(:/a4ee83cd3060425590453c1ea362c538)]

  • 從這個圖可以看出,CPU其實只需要寄存器組和運算單元就可以工作了。高速緩存SRAM只是作爲加速器緩存來使用。

  • 所有的數據最終都是來源於總線接口,總線的數據來源於內存空間。

  • 這不是重點,重點是有沒有發現這個當先流行的web服務架構一毛一樣。總線接口等價於數據庫IO接口,高速緩存相當於Redis等NoSQL內存存儲模塊,寄存器就是本機內存和硬盤了。算術邏輯單元就是我們的服務工作線程。

  • 講這個不是爲了要用類似的思維去學習他,而是想說,不妨感受一下高速緩存的結構和意義,就像感受redis的奇妙一樣。

  • 高速緩存的結構是類似完全二叉樹的樹形結構,第一層是集合,第二層是行,第三層是塊。塊纔是真正存儲數據的節點。

  • 通過一個集合都只有一行數據。等價於沒有集合的概念(或者沒有行的概念),這樣的話,可以看做一行有很多塊。只需要給出行座標(集合座標)以及塊座標即可定位。標準的地址是要給出集合座標,行座標,塊座標纔可以的。

  • CPU怎麼將寫入內存?先寫緩存,再寫內存,需要寫內存,比較慢。先寫緩存,等緩存失效了再同步回內存,這種寫入塊,但是需要處理緩存內容過期問題(併發問題)。

  • 緩存的和內存誰是主,是一個很重要的話題。

  • 緩存讀寫策略有哪些?什麼場景用什麼策略呢?

緩存寫入miss的策略
在寫入 miss 的時候,同樣有兩種方式:

Write-allocate: 載入到緩存中,並更新緩存(如果之後還需要對其操作,這個方式就比較好)
No-write-allocate: 直接寫入到內存中,不載入到緩存
這四種策略通常的搭配是:

Write-through + No-write-allocate
Write-back + Write-allocate

3.4 進程的內存結構

  • 在內存裏,數據可以是【操作符+數據】也可以只是【數據】。這是馮諾依曼體的特點。

  • 程序內存空間從邏輯上可以分爲:棧,堆,代碼區。在程序被加載的時候就會分配程序空間。

  • 那麼問題來了,程序的內存空間是按照什麼規則分配的?可以動態擴大或者縮小麼?

  • 按進程位單位分配的,其中棧是固定的,分配時映射了內存,堆是動態的,用的時候做映射,其他代碼段,Data段,text段,庫段時固定的。堆段時可以無限擴展的( 堆分爲2,存放小對象的brk,和大對象的內存映射段)。

  • 一個程序可以分配多少內存?答案是:操作系統的全部可用內存,因爲操作系統分配給進程的內存是虛擬內存,或者說是邏輯內存,在進程看來,整個系統的內存都是他們可以用的。比如操作系統有4G內存,那麼每個進程都可以拿到4G內存。

3.5 堆空間內存映射

  • 僅當進程對內存進行讀寫的那一次,纔會觸發物理內存映射,將內存頁與虛擬內存綁定(物理內存是按頁分配的)

  • 每觸發一次物理內存與虛擬內存的映射,下一個進程的可用內存都會減少。所以越往後面,新進程的可用內存就越少。除非有舊內存釋放空間(接觸物理內存頁與虛擬內存的綁定)

  • 根據上面的推理,可用內存大小是動態變化(堆棧的可用大小是動態變化的)。

  • 棧有空間限制,堆沒有。堆有多少就可以申請到多少。

  • 棧的數據是有固定生命週期的,從函數創建到函數結束。堆的數據生命週期不固定。

  • 頁中斷是爲了讓物理內存與虛擬內存建立聯繫。堆內存分配會觸發昂貴的頁中斷,而棧不會,棧空間在進程初始化就分配好,並做好了頁映射。

  • 由於堆分配需要進行頁中斷,成本高,所以建議一次性分配多點堆空間(比如1G哈哈哈)。不要每次分配幾個字節。(這是理論上來說,實際上,內存管理器預料到會這樣,所以給瞭解決方案。)

  • 當然爲了適應玩家系統分配幾個字節小內存的愛好,內存管理器,會爲進程分配一個專門存放小內存的頁,這個頁不會隨便回收,而是當空閒區比較大的時候纔會回收頁。所以並不會經常觸發頁中斷。

  • 我們把小內存映射頁叫heap,把大塊內存映射區叫,內存映射段 Memory Mapping Segment。

  • 什麼是Segment?進程講虛擬內存空間劃分的各種區域,這些區域就叫Segment,比如stack Segment 棧空間,head Segment,memory mapping Segment. data Segment, text Segment

3.6 堆空間大小約束

  • linux系統提供了很多函數給我們提前限制各個segment的大小上限。防止進程胡作非爲,影響核心進程。
  • 有幾種角度來設計內存限制:對用於分配內存的malloc函數進行限制,對程序訪問的地址,在系統級別做限制,程序自身做檢測限制。 分別是,系統讀寫限制,程序自身業務限制。

總結

操作系統的核心知識是進程管理和內存管理。進程管理涉及對算力的管理,內存管理涉及對存儲的管理。進程對於操作系統,就是算力和存儲的集合。重點關注【緩存局部性原理的利用】

二、程序接口

這一章主要是講操作系統怎麼抽象出進程的概念。

1.編譯

認真閱讀,接口產品設計,程序(進程的抽象)
- 關鍵詞:編譯,彙編,鏈接
- 

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vCHPqZhe-1588815445149)(:/4da3af334c1048838d2dfb860e4615f5)]

  • 運行時包括了堆棧段,讀寫的代碼區包括字符串,靜態變量之類的,只讀的大多存放方法代碼。
  • 靜態庫,是函數方法和變量的集合,在程序鏈接執行的時候會被加載到代碼段中。
  • 動態庫,也叫共享庫,爲了減少庫的反覆加載浪費內存,會將其封裝爲一個共享的方法集合,只會在內存加載一份。所有可執行程序共享。
  • 代碼在彙編成可執行對象後,將代碼分爲:只讀代碼段,已初始化數據,未初始化數據。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fxfcZAP5-1588815445151)(:/a8ff84645f0e419a979a188f6883d5a8)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3wOT457x-1588815445153)(:/e933d0ef75db48d3b1a5a8fef9535e7d)]

2.異常

認真閱讀,表面上講異常,其實是講操作系統怎麼實現CPU共享。而下一張則是講操作系統的內存共享
  • 進程可能是計算機系統中最偉大的抽象。進程這個概念背後,其實隱藏着一整套系統級機制,從進程切換、用戶態與內核態的轉換到系統實時響應各種事件,都離不開一個相當熟悉又陌生的概念——異常。

  • 計算機啓動後就會不停的讀取內核程序的指令並不停的執行指令。

  • 除此之外,計算機還會做一件重要的事情那就是跳轉

    • 在程序內跳轉,就是分支goto切換。
    • 在程序外跳轉,就是函數調用和返回。(函數是爲模塊化而生的抽象概念,是對程序的抽象。)
    • 中斷跳轉(異常中斷,用戶退出中斷,定時器中斷。),稱之爲異常控制流(exceptional control flow)
  • 異常控制流不止是異常,他的定義包含了用戶中斷和定時器中斷。

  • 我們常說的Exception,大多由硬件和操作系統觸發。

  • 進程的切換也是異常控制流ECF的結果,進程的切換實現了對CPU的共享,該中斷由硬件和操作系統協助完成。

  • 信號,只是操作系統的一種實現。

  • 什麼是非本地跳轉呢?非本地跳轉(Nonlocal Jumps)。

  • 異常控制流只是一種機構上的概念嗎?還是說是由於操作系統實現的一種機制?我傾向認爲是前者。

  • 異常控制流,可以從硬件到操作系統,也可以從操作系統到用戶程序,可以說無處不在。

  • 計算機從控制權的角度分爲3層,硬件,操作系統,用戶程序。最終的控制權位於操作系統,硬件作爲服務組件爲操作系統提高平臺和必要的中斷,操作系統監督程序運行。

  • 我們常說異常Exception,就是由硬件或者操作系統自身觸發,並利用操作系統擁有的最高控制權反映到用戶程序的執行流程中。

  • 系統會通過異常表(Exception Table)來確定跳轉的位置,每種事件都有對應的唯一的異常編號,發生對應異常時就會調用對應的異常處理代碼。

    • 這一點跟JVM很像。如果單單從指令跳轉的的角度來看,異常表和我們平常寫的switch case 語句一樣,區別就是這個函數調用的返回時從系統返回,還是由用戶自己的程序返回。
    • 除此之外,還有一種異步異常,即從系統突然中斷觸發的,這個時候用戶指令會被掛起,操作系統重新掌控CPU,執行對應的中斷程序,結束後返回用戶空間。
      比如操作系統接受到IO中斷,需要講網絡IO的數據存放到硬盤空間,此時就會臨時徵用CPU資源。
  • 系統調用走的其實時異常控制流程,因爲他一旦用戶程序調用了系統函數,就會把CPU控制權交回給了操作系統。等系統執行完中斷指令後纔會返回用戶空間。

    • (看起來很複雜跳來跳去的,但是站在CPU的角度,就是在一直while循環,偶爾進行跳轉。)
    • 觸發中斷的動作可以是用戶程序主動調用系統函數. 也可以是操作系統,強行終止用戶程序運行,跳轉到自身中斷程序。
    • 這就意味着用戶程序,其實在操作系統的監督下運行的,操作系統隨時都可以往用戶程序指令集中間插入任何goto指令。
    • 所有的指令在CPU看來就像是隊裏中的流水。而操作系統就像流程線上的管理員,指令就是流水線上的商品。

科普一個概念,程序理論上可以訪問所有的內存空間地址(包括虛擬內存)。
一旦用戶往內存地址上寫入數據,操作系統就會將與內存地址所在的內存頁與用戶進程綁定,表示這個頁已經被某個進程映射綁定了。
內存頁相對於進程來說就是一種相互競爭的物理資源。是操作系統提供給用戶程序的資源。但是對於底層硬件來說,則是一種邏輯資源,頁內存可以是硬盤,也可以是內存條或者是閃存。
如果頁內存是硬盤資源,那麼當用戶訪問頁內存的時候操作系統就可以將頁數據從硬盤加載到真正的內存提供給用戶進程,這個過程對用戶進程是不可見的。
提處頁內存的目的有兩個:
1.使得內存資源可以按批分配,提高資源的利用效率。
2.對物理資源進行虛擬,解除物理存儲與用戶數據的耦合關係,使得用戶內存數據可以存放到硬盤,實現虛擬內存技術。
3.進一步適配各種奇奇怪怪的內存廠商標準。(也許是吧)

  • 進程纔是程序(指令和數據)的真正運行實例。
  • 操作系統管理進程上下文切換,讓多個進程分時共用CPU,讓每個進程都感覺在獨佔CPU。
  • 操作系統管理內存映射關係,在硬件層實現內存頁虛擬(頁置換算法是虛擬技術的一個組成),在系統層實現頁映射算法,讓每一個進程都感覺在獨佔內存條。
  • 操作系統代理了CPU硬件和內存硬件的管理,使得用戶編程正常情況不再需要關心,如何讓多進程共享CPU和內存的問題。
  • 在用戶看來一臺就好像只跑了一個進程一樣,不需要擔心自己的程序是否會影響到其他進程。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cxEyDV6g-1588815445156)(:/67a4a0ee316f4ac581c55bcc549988c3)]

  • 頁錯誤page fault 有哪些情況?內存頁未被置換到內存中。被訪問地址未發生頁映射綁定(未分配內存)。頁錯誤會切換到內核,內核會對頁錯誤進行檢查,如果只是未置換,則嘗試置換,置換失敗就拋異常(很少見),如果是其它情況就只能拋異常了。
  • segmentation fault 段錯誤,當程序訪問的地址屬於segmentation未分配的或者未映射的頁內存。
  • segmentation fault 屬於頁錯誤的一種。
  • 內核會維護着一個進程隊列,每一個進程對象都會保存着寄存器上下文,當cpu發生上下文切換的時候,進程的寄存器上下文就會被換入cpu中,在進程看來這就像獨佔cpu一樣。因爲進程是操作系統虛擬的概念,可以認爲一個進程就是一個虛擬的計算機。所有進程共享一臺物理硬件。
  • 內核會代理進程指令的指向過程,這就意味着進程指令會是不是的被內核注入中斷指令。比如,用於進程調度的中斷指令。具體過程如下圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Mf0XF0Py-1588815445157)(:/c0a8e011101543a58926b7f5d12390ed)]

  • 如果站在cpu的角度看來就是這樣的一堆指令:a進程指令-》切換寄存器指令-》b進程指令-》切換寄存器指令-》a進程指令。切換寄存器指令就是內核代碼了。

  • 我們說的指令重排,其實就是內核往進程代碼注入內核指令的行爲。

  • 一旦發生系統調用或者硬件中斷,就會觸發內核上下文切換。

  • 我剛說了進程可以理解爲是虛擬計算機,那麼進程的幾種狀態是不是和虛擬機可以對應上呢?答案是肯定的,我會會創建虛擬機new,啓動虛擬機ready,運行虛擬機作業running,暫停虛擬機作業stop, 重啓虛擬機作業resume,終止虛擬機terminated.

  • 而終止虛擬機的幾種方法有,直接斷電,等於發送kill信號。調用exit退出函數,等價於點擊關閉系統按鈕,最優雅的動作。或者程序執行完所有代碼(理論上不可能發送。)

  • 說一個有趣的函數fork,fork可以字面上一顆創建子進程的函數,但是這樣理解是膚淺的。對,就是膚淺的。如果學習了操作系統後,我們應該從操作系統的本質去理解,操作系統是一個資源管理系統,操作系統的服務對象是進程,操作系統的資源分爲共享資源和私有資源。共享資源就是系統裏存儲的外設數據,比如文件描述符(字節序),或者可以籠統的理解爲內核級別的資源,這種資源fork不會進行復制,另外一種資源則是進程獨佔的資源具體爲“堆棧segment,data text segment, 庫代碼segment,bss segment,以及最重要的進程寄存器上下文”.這些資源會被複制一份。而頁表映射綁定關係也會被複制一份,但是默認情況下不會觸發頁表重新映射,除非發生了寫衝突,就會把原頁表拷貝一份並重新綁定頁內存。所以fork函數就是複製進程獨佔資源的函數,唯一不一樣的進程獨佔資源就是fork函數返回寄存器值不一樣,父進程的fork返回值是子進程的pid號,而子進程的fork函數返回值寄存器值是0.

  • 當fork函數執行完後,兩個進程的pc指針都指向fork函數這個內核函數的返回處。並各自開始執行自己的業務。

  • 信號是一種內核自己發明的異常控制流,每一個進程都有內核默認的信號處理函數,比如kill -9的終止信號,默認的內核處理函數就是終止進程。又其它進程發送給另一種進程(這個信號的發送者需要有足夠的系統權限)

  • 信號作爲內核進程提供給進程的通信方式,由此可見,所有利用信號進行的進程通信都需要有內核進程進行協調,內核進程就充當了代理或者是中介的角色。

  • 信號是一種異步的通訊機制,就類似消息隊列,a進程發送給b進程,信號消息會先暫存在系統的內核進程等待發送,這就意味着,信號有一種交pending的狀態(內核是用set來暫存信號,所以信號不會重複)。接受者可以暫時阻塞一些特定信號(又進程的啓動者對指定阻塞哪些信號),這就好比接受者在系統內核有多個快遞箱,一些快遞箱可以暫存不處理的信號,另一些快遞箱則是存放相對實時的信號。

  • 相比,硬件中斷和系統中斷,這兩種異常控制流而言,系統內核發明的信號異常控制流就沒那麼偉大了,信號僅僅爲了進程通訊,而硬件中斷和系統中斷則是爲了實現cpu共享和協調io等外設。

  • 進程組這個概念目前看來只是爲了批量維護進程。比如可以給進程組id發送信號,這樣就等於批量發送信號給進程組內的所有進程。

  • ctrl+z是發送掛起信號給當前前臺進程。並不會終止進程。

  • ctrl+c是發送stop信號給當前前臺進程。

  • kill 是用來發送信號的系統調用

  • 每一個信號都是對應在一個狀態字的一個比特位。換句話,每一個比特位都對應一箇中斷函數。

  • 每個信號的中斷函數是可以被重寫的(重定向函數句柄),通過signal系統調用來重寫。

  • 操作系統的所有進程代碼都是被內核進程注入監控指令的,以至於每個用戶進程隔一段時間就會去執行內核進程的指令,看看內核進程這個頂級上司有何指示,沒指示就繼續做自己的事情。這樣的機制就是用來實現,操作系統信號中斷等異常控制流的。(有了這樣的機制,進程就像是操作系統的租戶。)
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HPvjccY6-1588815445158)(:/3b056fb188264506bce9fac0e95836bf)]

  • 最有趣的是操作系統內核進程指令除了會注入到用戶進程意外,還會注入到自己的內核調用。有了這個滿世界都是注入指令的代碼,就是隨時隨地中斷任何程序,包括自己。比如,信號處理器在處理中斷信號的時候也可以被其它信號給再次中斷。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sOs1ZmeF-1588815445160)(:/9a56c2f4938f4384884f9a30124504cd)]

  • 信號處理器相對你內核進程來說是異步的,那就意味着要面向併發編程的數據同步問題,在操作系統裏叫“異步信號安全”(我們在JVM裏叫線程安全),這裏的安全手段也和大部分的計算機併發問題處理沒什麼區別,比如
    • 對共享變量採用volatile,確保內存可見性和讀取一致性(應該也具備防指令重排的作用),
    • 採用局部變量而非全局變量,從而避免資源共享。
    • 對異步函數(信號處理函數),聲明原子性,具體就是在執行函數期間屏蔽其它中斷信號。
    • 避免長時間複雜作業
    • 避免使用非安全的內核調用(大部分內核調用都是併發不安全的,畢竟併發問題是罕見的場景)。
  • Posix 標準指定了 117 個異步信號安全(async-signal-safe)的函數,這個就類似Java的JUC庫了
  • 說一個看起來雞肋的知識,“非本地跳轉”其實就是跨函數跳轉,比如我們在遞歸函數棧時,可以不用按照函數棧一步一步回溯,而是直接從棧頂直接回到棧底的main函數,感覺沒什麼應用場景。着實雞肋。

3.內存

認真閱讀,資源管理算法
  • 瞭解虛擬內存管理器,核心是圍繞“內存頁關聯表”的緩存和翻譯。
  • 瞭解“多級內存頁關聯表“是怎麼利用搜索樹算法加速查詢。
  • 瞭解動態內存分配的過程(本質上就是學習“內存頁關聯表”的工作過程)
  • 外部碎片是內存管理器對物理內存的碎片管理,看似來似乎沒什麼可展開學習的。
  • 內部碎片,其設計頁內對象空間管理。常常與垃圾回收器掛鉤,尤其是在JVM中垃圾回收器是重頭。
  • 瞭解內存異常,stackoverflow,segmentfault.
  • 不同進程訪問同一個地址,他們的虛地址一樣嗎?一樣的,因爲他們有各自的頁表。A進程訪問第0頁第5個偏移,查A的頁表,會被映射到第6頁物理內存的第5個偏移,而B進程訪問第0頁第5個偏移,會查B的頁表,會被映射到第17頁物理內存的第5個偏移。(這段話的關鍵詞是,每個進程都有各自的頁表)。確定這樣的理解對嗎?對的。
  • 緩存的兩種寫策略,write-back,換出的時候寫回(懶寫)write-through,改緩存的時候及時寫入(餓寫)。
  • 虛擬內存實際上是操作系統對內存的重定義,操作系統不再把物理內存(內存條DRAM)看作是直接可用的內存,而是將磁盤看作內存空間,而DRAM被當作是內存空間的緩存空間。當需要訪問虛擬內存條的時候,就會查詢頁表,如果頁表有記錄,則表面硬盤裏的內存數據被緩存在DRAM中那麼直接訪問即可,如果頁表中不存在改內存數據的緩存,則觸發異常中斷,執行頁置換算法,將硬盤的內存數據調入DRAM中,中斷結束就繼續剛剛的虛擬內存訪問流程。
  • 因此虛擬內存技術可以看作是對物理內存的代理技術,而被代理的對象是進程,不是操作系統,這就意味着,只要尋址空間允許(即便物理內存只有1G),你也可以創建幾十萬個進程,並且還能保證每個進程有3G的可用內存。
  • 由於局部性原理的存在,這項代理技術非常有存在的價值。
  • 問一個問題,“給定4G的物理內存DRAM,和一個1T的硬盤,並且聲明操作系統內核進程最多隻用1G物理內存。試問有沒有可能創建10個用戶進程,約定每個用戶進程都會申請2G的內存空間?” 答案是可能,因爲現代操作系統將物理內存DRAM看作是cache,真正的內存空間位於磁盤等外設上,因此只要確保磁盤空間足夠容納每個進程獨佔的虛擬內存空間,那麼就可以提供超過物理內存上線的進程數,並且還能保證每個進程擁有好幾G的獨佔的虛擬內存空間。(而最終大的進程上限只能由操作系統本事進行軟限制了,硬件可以說是無法限制進程的個數的。)
  • 頁表會限制內存塊的“讀、寫、執行”的權限。
  • 給定一個固定規格的空間,固定空間內的碎片空間成爲內部碎片,固定空間所在的外部空間碎片成爲外部碎片。
  • 內存分配算法和內存回收算法都是進程在自己動態內存空間的管理算法。
  • 寫入超出堆棧緩衝區的內存會怎麼?(緩衝區溢出攻擊)
    • 如果寫入的內存是堆棧段內,則會擾亂堆棧內其它數據值。
    • 如果超過堆棧段,則會出現segmentfault.
    • 問題是萬一緩衝區位於寄存器附近,且沒有報segmentfault,就會導致PC執行了攻擊者預先留下的代碼空間,如果這段代碼是系統調用代碼,就很有可能使得攻擊者獲得shell權限,萬一被攻擊的程序是root權限的程序,那麼就等於攻擊者獲得了root權限的shell了。所以建議大家不要隨意用root執行程序
  • 工作集,WorkingSet 就是隻某一個進程某一段時間內會用到的內存頁總和(包括了共享內存和獨佔內存),這個概念提出有利於我們統計物理內存DRAM的使用情況。我們把每一個進程的工作集加起來就是認爲這是物理內存DRAM的實際使用情況了。(非工作集的內存數據在硬盤交換區裏躺着。)
  • linux swap值是所有進程共用的swap值上限?還是單個進程能用的swap值上限?根據實踐經驗,我覺得是前者,因爲我們平常free -h查看swap空間使用情況是,返回的時候並沒說某一個進程用了多少。而是直接現實一個使用量。
  • swappiness可以優化linux對swap空間的使用策略,0~100,爲100表示儘可能的用磁盤,爲0表示儘可能用物理內存。

三、進程通信

這一張講用戶進程怎麼實現業務。

1.系統IO

認真閱讀,進程協作,學習文件系統
- 在linux裏,【所有東西都是文件】(這句話有待考證)
- 文件可以看成是字節序
- 標準c文件接口都帶緩存。
- 而unixI/O的文件訪問接口open,close,read,write都不帶緩存。
- open可以理解爲調用內核函數初始化文件上下文,close就等於調用內核函數關閉文件上下文。
- read則是向設備發送字節序複製指令。
- 文件頭有幾個重要數據,type,inode號,inode引用次數。
- 同一個文件系統inode號表示同一個文件,inode引用次數是專門爲硬鏈接設計的字段,用於表示該文件有多少個硬鏈接。
- 只有硬鏈接變成0才允許被刪除。(一個inode可以有多個硬鏈接)
- 軟件其實是一種文件類型,因爲他有不一樣的inode號,文件系統是根據inode來區分是否爲同一個文件。
- 目錄也是一種文件,他的文件內容保持着上一個目錄的指針,以及文件索引列表。
- 每一個由Linux shell打開的進程,都會打開3個系統資源文件,即,標準輸入流0.標準輸出流1,標準錯誤流2,0、1、2 分別是這3個文件的文件描述符fd。
- write和read這兩個UNIX IO接口的最小事務粒度是1個字符,即,要麼讀寫一個字符,要麼讀寫0個字符,沒有半個字符的說法。

- 文件的數據結構很複雜,由多個文件元數據組成,其中與inode有關的元數據如下:
	struct stat
{
    dev_t           st_dev;     // Device
    ino_t           st_ino;     // inode
    mode_t          st_mode;    // Protection & file type
    nlink_t         st_nlink;   // Number of hard links
    uid_t           st_uid;     // User ID of owner
    gid_t           st_gid;     // Group ID of owner
    dev_t           st_rdev;    // Device type (if inode device)
    off_t           st_size;    // Total size, in bytes
    unsigned long   st_blksize; // Blocksize for filesystem I/O
    unsigned long   st_blocks;  // Number of blocks allocated
    time_t          st_atime;   // Time of last access
    time_t          st_mtime;   // Time of last modification
    time_t          st_ctime;   // Time of last change
}


  • 其中 st_ino,st_mode,st_nlink 就是我們剛剛說的inode號,文件類型,inode硬鏈接數。
  • 當有進程打開文件時,內核會把文件索引對象(即文件屬性)中的引用計數器+1,進程自己也有一個索引表記錄了當前進程打開了什麼文件。
  • 子進程所謂的繼承父進程的文件索引表,實際上就是拷貝了一份文件描述符表過來而已。
  • C標準庫 IO 會用流的形式打開文件(這一點和UNIX IO不一樣,區別在於流)
  • 什麼是流,流(stream)實際上是文件描述符和緩衝區(buffer)在內存中的抽象。
  • 也就說,帶緩衝區的方式打開文件,就稱之爲流,分爲輸入流,輸出流。
  • 如果用 Unix I/O 的方式來進行調用,是非常昂貴的,比如說 read 和 write 因爲需要內核調用,需要大於 10000 個時鐘週期。
  • 簡單的說就是read和write是內核接口,需要從用戶態指令切換到內核態指令,需要切換寄存器上下文很浪費時間。
  • 如果爲了減少來回切換用戶態到內核態,採用批量調用的方法,減少內核調用次數。先把內核緩存在用戶內存,後來在一次性刷入內核態。
  • 說說java,java更噁心,需要把數據流寫入到JVM的堆,然後再刷回用戶態堆內存,然後再刷回內核態內存,需要花費兩次字符串複製。因此JAVA採用直接內存技術可以優化性能,即,將數據直接寫入用戶態緩存,然後再刷回內核態,從而減少了一次內存複製的過程。
  • Unix I/O 中的方法都是異步信號安全(async-signal-safe)的,也就是說,可以在信號處理器中調用。
  • 什麼是異步信號安全呢?就是在調用UnixIO的時候允許被其他中斷信號打斷,之後再繼續IO也不會造成異常丟數據或者死鎖,這種不會獨佔CPU。
  • 標準 C I/O 如果用來處理網絡IO,記得及時flush緩衝區,要不然信息會丟失,其次就是標準C IO不是異步信號安全的。
  • 異步信號安全,分爲2種,1.可重入安全,2.互斥其他信號安全。
  • 儘管標準 C I/O有互斥鎖對buffer進行保護,但是他互斥不了信號中斷,因此信號中斷,如果再次調用同一個函數就會形成相互等待的死鎖場景。
  • 互斥鎖不是內核層面的,所以無法阻塞中斷信號。
  • 所以不要在中斷函數裏調用一些標準C的東西就是這個道理,畢竟中斷函數是內核級別的函數。

2.網絡編程

認真閱讀,爲互聯網打基礎,其實對於看過tcp協議和計算機網絡的人來說,這一章完全可以不讀的了。

3.併發

認真閱讀,爲高併發打基礎
  • 介紹了3種最簡的併發模型,多進程併發IO,基於事件單進程IO,基於多線程IO併發。
  • 其中多線程IO模型可以理解爲共享動態內存但是不共享寄存器的IO模型。
  • 基於事件IO模型,其原理則是利用操作系統IO時延遠大於CPU業務處理時延(想象一下一個Byte傳播需要1ms,cpu可以執行多少指令)。
    顯然cpu大部分時間處於空閒狀態,尤其是當我們網絡請求的事務都是短作業的時候尤爲明顯(即服務器每個請求處理速度特別快)。
    正因爲我們大部分的業務都是短作業的,所以沒必要用併發。換而言之,我們把所有請求發給一個進程即可。
    而要實現這個技術關鍵點在於理解【linux套接字】的定義,套接字包括了【發送方IP,發送方端口,請求方IP,請求方端口】
    多進程模型裏,一個套接字對應一個進程。而基於時間的模型,我們讓一個進程維護一個套接字數組,並且開啓一箇中斷監聽。
    一旦數組中任意一個套接字緩存接收到字節序,則喚醒業務進程處理收到的字節序。所有套接字的字節序是穿插着排隊被處理的。
    • 總結一下,就是基於事件,監聽,觀察。
  • 大部分生產環境都是結合這3個核心組合而成,現代操作系統,常常用事件IO和多線程集合。
  • POSIX Threads是一個線程庫,他提供了多線程。
  • POSIX API 中大致共有 100 個函數調用,全都以 pthread_ 開頭。
  • 我們通過學習 POSIX API可以瞭解一個線程庫可以劃分成哪些模塊。
  • POSIX API提供功能如下:
    • 最基本的線程創建和終止,線程啓動API。這個等同於和進程管理一樣。
    • 提供多進程也有的線程同步功能,比如,join等待(和wait差不多)
    • 還有多進程所沒有的,同步工具:Mutex,條件變量,讀寫鎖。
  • Mutex,條件變量,讀寫鎖同步3個工具對應着3種應用場景。
  • 除了POSIX的同步功能,還有一種作爲補充的第三方同步工具Semaphore API用於實現其他場景。
  • 每個線程有單獨的線程上下文(線程 ID,棧,棧指針,PC,條件碼,GP 寄存器)
  • 所有線程共享進程資源,除了棧空間和CPU寄存器數據。
  • volatile 關鍵字聲明,在C語言裏表示寫入採用write-through策略。(犧牲緩存性能獲得可見性)
  • 臨界區塊是一種利用同步工具實現的一個概念,即確保一段指令只能被一個線程執行,實現同步效果。
  • 消費者和生產者的應用場景很多,其中視頻幀的生成和渲染就是一種基於消費生產模型實現的。其中幀就是信息載體。
  • Reentrant Functions 是線程安全函數非常重要的子集,其表現爲,函數被中斷後,
    再回來原函數執行,其上下文沒有被改變。(他不是通過臨界區模型,而是純代碼實現,即不fang’w全局變量)
  • 阿姆達爾定律 amdahl’s law指,將以一個作業分爲可並行邏輯和不可並行邏輯,然後來考慮採用多核並行後的價值。從而指導我們考察業務場景是否採用並行計算。
  • 超線程,指線程的分發和CPU綁定之間又加了一層代理,從而實現一個核心可以執行多個線程?有待研究。

四、總結:

CSAPP,第一部分講了數據類型和彙編指令簡單知識,基本上就是在可怕邏輯上的hello world入門了。第二部

,重點講了操作系統怎麼利用中斷實現多進程共享算力,以及怎麼定義進程虛擬內存和實現。第三部分講了進程通信技術。

從目錄結構上看,整本書核心就是講進程,可見對於操作系統來說,進程的重要性。甚至我們可以說一切都是爲了
講明白進程而作的鋪墊。

整本書就是講:硬件怎麼支持實現操作系統,操作系統怎麼支持用戶進程,用戶進程怎麼支持業務實現(從計算機的架構看來,業務的本質就是IO,計算只是爲了更好的IO)。

引用聲明:

  • https://wdxtub.com/csapp/thin-csapp-3/2016/04/16/ 小土刀博客
  • https://www.jianshu.com/p/9a3720912164 簡書,進程和線程的內存組織結構
  • 《深入理解計算機系統》作者: Randal E.Bryant / David O’Hallaron
  • https://zh.wikipedia.org/wiki/%E9%98%BF%E5%A7%86%E8%BE%BE%E5%B0%94%E5%AE%9A%E5%BE%8B 阿姆達爾定律
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章