程序員的自我修養

程序員的自我修養

在看一本很有意思的書《程序員的自我修養》,講的是鏈接、裝載與庫,大概講了操作系統層面,程序如何加載運行的。記錄以下關於操作系統的演化史。

操作系統

CPU

分時系統、多任務系統

硬盤

硬盤:硬盤有多個盤片,每個盤片分兩面,每面按照同心圓劃分爲若干個,每個磁道劃分爲若干個扇區,盤面外圍的磁道密度比內圈稀疏。現代的硬盤普遍採用LBA(logic block address)的方式來區分,所有扇區從0到開始編號,拋棄複雜的磁道、盤面的概念。

內存

傳統的內存是程序需要多少內存,就直接分配多少內存,如果內存不夠,就先把數據寫到磁盤裏面,等到要用時再讀回來。這樣的會導致三個主要問題:

  1. 地址空間不隔離,不同應用程序之間可以直接篡改其他程序的數據
  2. 內存使用效率低,頻繁的磁盤內存數據換入換出
  3. 程序運行的地址不確定,程序的重定位問題。(即指令修改完以後,目標的地址需要重新計算)

解決問題的方式就是,增加中間層,使用間接的地址訪問方法。將程序給出的地址看作是一種虛擬地址,然後通過某些映射的方法,將這個虛擬地址轉換成實際的物理地址。

內存分段

內存分段把程序所需要的內存空間大小的虛擬空間映射到某個地址空間。對於問題1,如果程序訪問的虛擬空間的地址超出範圍,硬件就會判斷這是個非法請求,拒絕這個請求訪問,並報告給操作系統或監控程序。對於問題3,因爲不在需要關注物理地址,所以無需考慮。

內存分頁

內存分段無法解決內存使用效率低的問題,主要是因爲操作數據換入換出以程序爲單位。根據程序的局部性原理,當一個程序在運行是,在某個時間段,它只是頻繁地用到了一部分數據,爲了提高效率,可以有更小粒度的內存分割和映射方法,即分頁,將地址空間人爲地等分成固定大小的頁。虛擬空間的頁爲虛擬頁,物理內存的頁爲物理頁,磁盤中的頁爲磁盤頁。當程序的虛擬頁不在內存中,在使用到虛擬頁的時候,會發生頁錯誤,需要從磁盤中讀出並存入內存,建立映射。這種映射爲MMU(Memory ManageMent Unit)實現的,一般集成在CPU內部了。同時,頁可以設置權限屬性和訪問,從而增加保護機制。

線程

多線程解決問題比如單線程的等待、交互的中斷、併發操作、多核計算機的計算能力匹配、數據共享等。當線程數量小於等於處理器數量時,線程的併發是真正的併發,不同線程運行在不同的機器上。否則,一個處理器可能以多任務形式運行多個線程。
線程私有:棧、線程局部存儲(ThreadLocal)、寄存器
線程共享:堆

線程調度狀態,根據線程的running、ready、waitting等狀態進行時間片輪轉。線程的優先級提升策略一般爲:用戶指定、根據進入等待狀態的頻繁程度提升或者降低優先級、長時間不被執行而被提升優先級。

可以使用volatile關鍵字阻止過度優化(編譯器的行爲影響)。
例如我們在單例模式中的雙重鎖檢查,c++的new其中的步驟:在內存的位置上調用構造函數和將內存地址賦值給pinst,這兩部順序是不確定的,可能出現pinst的值不是null了,然而對象的構造依然沒有構造完畢。
CPU的亂序執行能力使安全保障變得異常困難,只能用barrier,阻止CPU將該指令之前的指令交換到barrier之後。

可重入與線程安全

一個函數被重入,表示這個函數沒有執行完成,由於外部因素或內部調用,又一次進入該函數執行:

  1. 多個線程同時執行這個函數。
  2. 函數自身調用自身
Linux下的線程

Linux對於多線程的支持頗爲貧乏,Linux將所有的執行實體(無論是線程還是進程)都稱爲任務,每一個任務概念上都類似於一個單線程的進程,具有內存空間、執行實體、文件資源等,也可以共享內存空間(寫時複製----即發生修改的時候才複製內存空間)

編譯和鏈接

編譯

我們通過編譯器的構建(build)將編譯和鏈接合併到一起。這個過程被隱藏了,其實它做了預編譯(擴展宏定義、處理#inclue預編譯指令,把被包含的文件插入到該預編譯指令的位置),編譯則是把預處理完的文件進行一系列的詞法分析(將字符用有限狀態機token化分割)、語法分析(將token進行語法分析,形成以表達式爲節點的語法樹)、語義分析(靜態語義:聲明、類型轉化、匹配,動態語義:運行時出現的語義相關的問題)、優化(源碼級優化:合併運算;代碼生成器和目標代碼優化器:選擇合適的尋址方式、用位移代替乘法運算、刪除多餘好指令)形成彙編代碼文件。

鏈接

編譯後,真正的地址還沒有指定。通過符號(symbol)來表示一個地址,可以是一段函數的起始地址,也可以是一個變量的起始地址。模塊間函數調用和模塊間變量訪問都是模塊間符號的飲用,鏈接的作用就是把一些指令對其他符號的引用加以引用,鏈接的過程主要包括了地址和空間分配、符號決議、重定位等步驟。比如一個模塊調用另一個模塊的foo函數,在編譯器編譯這個模塊的時候,暫時會擱置這個指令的目標地址,等需要引用foo的時候,自動去另外一個模塊查找foo的地址,然後修正地址。
通過這個樣的鏈接過程,把每個模塊的源代碼文件經過編譯器編譯城目標文件(.o或者.obj),目標文件和庫一起鏈接形成最終可執行文件。最常見的額就是運行時庫。

目標文件

目標文件從結構上將,它是已經編譯過的可執行文件格式,只是還沒有經過鏈接的過程,其中可能有些符號或者地址還沒有被調整。

目標文件的格式

目標文件的可執行格式在Linux下是ELF可執行文件,不光可執行文件按照這種格式來存儲,動態鏈接庫和靜態鏈接庫也是按照這種格式存儲。通過file命令可以產看相應的文件格式。
目標文件總體來說,主要分爲兩種段:程序指令和程序數據。代碼段是程序指令,數據段和.bss段屬於程序數據。分段的意義是防止程序指令被修改、配合CPU的數據、指令緩存策略、共享指令可以節省大量內存。

ELF文件結構描述

包括文件頭、各個指令段、數據段、段表、字符串表、符號表等等。
文件頭定義了ELF魔數、文件機器字節長度、數據存儲方式、版本、運行平臺、ABI版本、ELF重定位類型、硬件平臺、硬件平臺入口地址、程序頭地址和長度、段表的位置和長度、短短額數量。其中魔術的最開始4個字節,第一個直接字節對應DEL控制符,後面3字節代表“E”“L”“F”三個字,第五個字節是表明文件類型,0x01表示32位,0x02表示64位,第六個字節表示字節序,是大端還是小端,第七個表示主版本號,後面使用這九個字節作爲擴展標誌。
段表描述了ELF各個段的信息,比如段名、段的長度、在文件中的偏移、讀寫權限等等。
重定位表是在鏈接器處理目標文件是,對目標文件中某些部位進行重定位,這些信息都會存放在重定位表裏。
字符串表存放普通字符串,段表字符串表存放段名。
符號表:鏈接的接口是符號,對應了一個函數或者變量。符號表結構定義了符號的類型和綁定信息,定義了所在段,符號值等等。爲了防止多模塊符號衝突問題,需要使用命名空間和符號修飾機制來避免,多態中引入函數簽名,包含函數信息,用於識別不同函數。

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