計算機組成和操作系統

1 計算機系統漫遊

  1. 只由ASCII字符構成的文件稱爲文本文件,除此以外都是二進制文件。
  2. .c文件\rightarrow預處理器(cpp)\rightarrow.i文件\rightarrow編譯器(ccl)\rightarrow.s文件(彙編文件)\rightarrow彙編器(as)\rightarrow.o文件(可重定位目標程序)\rightarrow鏈接器(ld)\rightarrow可執行文件。
  3. hello程序的運行:首先shell讀取用戶的輸入,將“./hello”讀入寄存器,再保存在主存中。然後從磁盤上讀取hello文件代碼以及數據(DMA)。接着開始執行代碼,這些指令將要輸出的字符串加載到寄存器上,最後傳輸到顯示設備。
  4. 操作系統提供的抽象表示:
    1. 文件是I/O設備的抽象,文件爲應用程序提供了統一的視圖
    2. 虛擬內存是I/O+主存,虛擬內存起到每個進程獨佔主存的假象
    3. 進程是I/O+主存+處理器,進程起到每個程序獨佔硬件資源的假象
    4. 虛擬機是操作系統+I/O+主存+處理器。
  5. 虛擬地址空間從低到高地址增大。最底層是程序代碼和數據,然後是堆(運行時由malloc創建),共享庫,用戶棧(函數調用增長,函數返回收縮),內核虛擬內存(用戶不可見)
  6. amdahl定律:對系統的一部分加速時,影響程度取決於該部分的重要性和加速程度。

2 信息的表示和處理

  1. 字節(byte)作爲最小的可尋址的內存單位。字長(word size)決定了計算機的虛擬地址空間的最大大小,字長爲w位的計算機,程序最大訪問爲2w2^w個字節。
  2. 最低有效字節在低地址是小端,反之是大端。字節順序值得注意的地方:1是網絡傳輸中的統一;2是閱讀機器級代碼時候的統一。
  3. C中對於有符號數的右移是算術右移,對於無符號數的右移必須是邏輯右移。
  4. 乘法的指令週期在10個左右,而加減,移位則是1個是時鐘週期。因此編譯器會通過移位和加減法結合來代替乘法,從而得到優化。除法更慢,需要30個甚至更多的時鐘週期,對於除以2的冪的除法,可以通過右移操作來優化。
  5. 浮點數有符號位,尾數,和階碼組成。舍入的規則是,對於正好出於中間位置的數,我們向偶數舍入(由於這樣能在計算平均數的時候不出現偏差),其他時候向靠着近的舍入。從F或D向int轉的規則是向零舍入。

3 程序的機器級表示

  1. 查看二進制文件的linux命令:objdump -d filename.o。可以用–masm=intel來顯示intel標準的彙編代碼。
  2. gcc彙編代碼指令都有一個字符的後綴,表示操作數的大小。b是字節,w是字,l是雙字,q是四字。
  3. CPU中包含16個64位通用目的寄存器。他們分別由初代8086的8個寄存器ax—bp,以及後面新增的8—15構成。功能如下:16個寄存器功能
  4. 操作數的可能性分爲三種:
    1. 是立即數,$符號後面加一個數字。
    2. 是寄存器,rar_a表示寄存器,R[rar_a]表示寄存器的值。
    3. 是內存尋址,通常是MbM_b[addr]。
      內存尋址中需要注意最特殊的比例變址尋址:Imm(r_b,r_l,s) = M[Imm+R[r_b]+R[r_c].s],沒有r_b時前面逗號保留;s=1時不出現s,此時爲變址尋址;只有一個寄存器時是間接尋址。
  5. mov操作在x86中禁止直接從內存賦值到另一片內存中,必須通過寄存器做中介。movz會將剩餘位填補爲0,movs會將剩餘位填補爲源數據的最高位。
  6. %rsp始終指向棧頂元素,且棧頂元素的地址是最低的。壓入棧時,先移動棧頂指針,然後將數據壓入棧。彈出時相反。(8086是這樣,IA-32中則是直接將rsp的值壓入棧中)
  7. leaq是算數邏輯運算中唯一隻有q模式的指令。leaq S,D = D <- &S。這個指令可以被編譯器用作簡單的加法和乘法運算。
  8. 條件碼寄存器:CF進位標誌,ZF零標誌,SF符號標誌(最近得到的結果爲負),OF溢出標誌。
    符號寄存器的訪問方式:
    1. 通過set訪問
    2. 通過跳轉指令訪問,其中rep ret能夠起到讓跳轉指令不會指向ret,避免無意義的操作的作用
    3. 基於條件判斷的數據傳輸,數據更爲常用是考慮到流水線會進行分支預測,一旦預測錯誤效率會大打折扣,因此用跳轉指令效率會低。
  9. C中的switch語句在彙編中會有兩步重要的處理:
    1. 將輸入的範圍進行縮小,縮小到0-n,這樣判斷起來更便捷。
    2. 構建跳轉表,對於不存在的選項跳轉至默認的loc_def標籤,對於重複的選項跳轉至同一個標籤。
  10. 過程調用需要滿足的三個要求:
    1. 過程調用開始時,PC需要設置爲被調用程序的起始地址。
    2. 調用結束PC設置爲調用程序的下條指令地址。
    3. 可以傳遞參數和返回值,前6個參數有寄存器傳遞,超過6個通過棧傳遞。被調用程序要爲局部變量分配和釋放內存。
  11. 浮點數會存放在媒體寄存器中YMM或XMM中。一共有16個寄存器。函數通過%xmm0 ~ %xmm7 8 個寄存器傳遞浮點參數,剩下的參數通過棧傳遞。返回的浮點數通過%xmm0傳遞。單雙精度指令區別在於指令後面是ss還是sd

4 處理器體系結構

  1. 指令的字節級編碼:每條指令的第一個字節表示指令類型。高4位是代碼部分,第四位是功能部分。第二個字節表示使用到的寄存器的寄存器標識符,如果沒有用到寄存器則用F表示。
  2. 程序員可以通過狀態碼來得到程序運行的總體狀態。系統會調用異常處理函數處理碰到的異常。
  3. 數字系統三部分:
    1. 存儲位的存儲器
    2. 更新位的時鐘
    3. 計算位的組合邏輯。
  4. 處理一條指令的流程:
    取指\rightarrow譯碼(讀取最多兩個操作數和寄存器)\rightarrow執行(對於跳轉指令在這裏決定分支)\rightarrow訪問(內存交互)\rightarrow寫回(寄存器交互)\rightarrow更新PC。
  5. 流水線儘管對特定指令會有延遲,但是增加了系統的吞吐量。
    流水線的問題:由於劃分不一致,系統吞吐量受最慢的部分限制。流水線過深,收益會下降,這是因爲受到流水線寄存器固定延遲導致的。
  6. 爲了流水線的並行,需要把完成PC值的預測這一過程放在取指環節。
  7. 流水線冒險:數據冒險(一條指令用到這一條指令計算結果)和控制冒險(跳轉的指令觸發)。
    避免數據冒險的方式:
    1. 暫停。直到所有的源操作數指令完成了寫回階段,通過插入氣泡讓本來要執行的指令停下,類似於動態插入nop指令。
    2. 轉發。檢測到有未完成的讀任務時,直接將計算結果轉發到相應的指令階段。轉發在碰到前一條指令加載,後一條指令使用的情況時就會出問題,此時需要把暫停和加載結合起來,在使用階段添加氣泡,然後用轉發處理。
  8. 異常處理的原則:流水線最深的異常指令優先級最高。出現異常指令後,禁止後面的指令更新寄存器

5 優化程序性能

  1. 編譯器優化的原則:只執行安全的優化。舉例:兩個指針指向同一塊內存的情況稱爲內存別名,編譯器優化時必須考慮這個情況。
  2. 程序性能度量標準:每元素的週期數(CPE)。
  3. 消除循環的低效率。代碼移動,將需要多級計算單計算結果不變的運算移到循環外,典型的例子是判斷循環邊界條件。
  4. 減少過程調用,每次函數調用都是一筆開銷。在循環中調用函數就是一個例子。
  5. 避免不必要的內存開銷。比如在循環中每次都要讀寫內存,可以使用局部變量代替。
  6. 程序性能的兩個界限:一系列指令必須順序執行時,會有延遲界限;處理器單元的原始計算能力是吞吐量界限。
  7. 循環展開:通過在循環中增加計算次數,來減少循環輪數。
  8. 通過增加並行性來提高效率。可以打破延遲界限。
  9. 通過重新結合運算也能顯著提升效率。這個是考慮到指令發射時刻意並行完成加載和計算來實現的。
  10. 一些限制因素:
    1. 寄存器溢出。一旦超過可用寄存器數量,部分臨時變量就會保存在棧上,這會拉低效率;
    2. 分支預測錯誤和預測懲罰。
      爲了避免這個問題,需要:不要過分關心可預測的分支,寫出適合條件傳送的代碼(依賴隨機數據進行條件判斷何容易出現分支預測錯誤,這時候功能性的代碼是通過條件操作計算值來完成賦值)
  11. 程序剖析:通過在gcc中加入-pg的選項,就會在正常執行代碼後得到一個gmon.out的文件,通過gprof就可以查看函數調用時間。
    報告有兩部分
    1. 第一部分是各個函數調用花費的時間,調用次數。
    2. 第二部分是具體的函數調用與被調用情況。需要注意的是gprof對時間短於1秒的函數不夠準確,並且默認不會統計庫函數的調用。

6 存儲器層次結構

  1. 局部性:具有局部性的計算機程序傾向於訪問相同的數據項集合,或者鄰近的數據項集合。
    時間局部性:引用過的內存,會在將來不久再次被引用。
    空間局部性:引用過的內存,會在將來不久被引用到它附近的內存。
  2. SRAM比DRAM要快,SRAM多用於CPU高速緩存存儲器,一般爲幾兆。DRAM主要用於主存和圖形系統幀緩存區,一般爲幾百兆。
    SRAM將位儲存在雙穩態電路中(倒掛的鐘擺),抗干擾強,需要6個晶體管。DRAM將位儲存在電容中,抗干擾差,只需一個晶體管。
    傳統的DRAM由d個超單元組成,每個超單元由w個單元組成。數據通過pin傳入傳出。內存控制器通過行地址和列地址訪問對應超單元。
    DRAM設計成二維陣列好處是減少地址pin,壞處是增加訪問時間。
  3. 增強DRAM。快頁模式:允許在行地址不變的情況下,直接通過列地址訪問。擴展數據:列地址更加緊密。同步:前幾種均爲異步,
    同步速度更快。雙倍數據速率同步:使用兩個時鐘沿作爲控制信號,速度翻倍。
    SRAM和DRAM斷電會有數據缺失,爲易失性存儲器。ROM是非易失性存儲器。PROM,一次重編程,儲存器單元是熔絲。EPROM,石英窗光控,
    1000次。EEPROM,電子控制,10^5次重編程(閃存)。儲存在ROM中的程序稱爲固件。
  4. 磁盤容量受到記錄密度(磁道上單位長有的位數)和磁道密度(從中心出發單位長上的磁道數)影響。
    CPU通過內存映射I/O的方式來控制I/O設備。地址空間中留一部分用於I/O通信,即I/O端口。
    SSD由閃存翻譯塊和閃存(塊頁)組成。數據以頁爲單位讀寫,讀性能高於寫性能,原因是寫需要保證整個塊是被擦除過的,否則要把原先塊上
    的數據複製一個擦除過的塊上。
  5. 緩存不命中分爲:冷不命中(緩存爲空),需要執行放置策略,通常爲一個塊號映射,但是會引起衝突不命中,此時需要設定一個工作集。當
    工作集超過緩存容量,爲容量不命中。
    cache的參數:S組數,E組中行數,B塊數,m整個物理內存的字節數。當且僅當有效位置位+找到符合的塊地址纔算命中。E=1的cache稱爲直接cache。
    CPU訪問直接cache的三步驟:組選擇,行匹配,字抽取。
    全相聯高速緩存一般應用在快表上,是沒有分組的。
  6. 高速緩存友好代碼:讓最常見的情況運行的更快。儘量減少循環中的不命中數。

7 鏈接

  1. 目標文件:可重定位目標文件(編譯生成),可執行目標文件(靜態鏈接生成),共享目標文件(動態鏈接生成)。
    ELF文件中的重要的分段:.text 已經編譯的代碼段 .rodata 只讀的數據 .data 已初始化的全局和靜態變量 .bss 未初始化的全局和靜態變量 .symtab 符號表.rel.text .rel.data 重定位相關信息。
  2. 每一個可重定位模塊m都有符號表。包含:由m定義的並被其他文件引用的全局符號(非靜態函數和全局變量);由別的文件定義被m引用的外部符號;只被m定義和引用的局部符號(靜態函數和帶static的全局變量)
    符號表的參數:name是字符表中的offset;value是地址;size是大小;binding表示是本地還是全局的。每個符號被分配到一個section中,有三個例外:ABS(不該被重定位的符號)UNDEF(不在本模塊定義的符號)COMMON(未初始化的全局變量)。
  3. linux處理多重定義的三原則:不準有重複強符號,有強弱符號選強符號,重複弱符號隨機選。函數和已初始化的全局變量是強符號,未初始化的全局變量是弱符號。
  4. 重定位:合併相同的節;重定位符號引用,使之指向正確的地址。
    重定位條目:offset需要被修改的節偏移。type重定位類型,其中兩個常見的:R_X86_64_PC32,重定位PC的相對位置;R_X86_64_32,重定位絕對地址。
  5. 可執行文件相比可重定位文件,少了rel部分(已經不需要重定位),多了.init(內含_init函數初始化代碼)。
  6. 動態鏈接庫沒有真正加載代碼段和數據段,而是提供符號表和重定位信息。可以加載無需重定位的代碼稱爲位置無關代碼(PIC),PIC中數據引用是用了全局偏移量表(GOT)函數引用是用了延遲綁定。
  7. 通過readelf可以查看目標文件。幾個重要的參數:-h 文件名,查看整體信息;-t 文件名 詳細表;-s 符號表;-S 每一節的頭部。-r 顯示重定位;-d 動態表節。

8 異常控制流

  1. 處理器通過異常表處理異常。異常表起始地址放在異常表基址寄存器中。
    異常調用與過程調用的不同:異常調用返回當前指令或者下一指令的地址;會將額外的處理狀態壓入棧;壓入的是內核棧;異常調用在內核中進行,有完全權限。
  2. 異常類型:
    中斷(I/O設備的信號)
    陷阱(故意的錯誤)
    故障(可恢復)
    終止(不可恢復)
    除了中斷全爲同步。陷阱是用戶和內核之間的一個過程調用。
  3. 進程提供了兩種關係抽象:邏輯控制流(獨佔處理器),私有地址空間(獨佔內存)。
    進程的三狀態:創建,掛起,終止。
    fork函數可以用來創建進程。默認情況下,父子進程併發執行,擁有相同但是獨立的地址空間,享有copy-on-write機制,共享打開的文件。
    已經終止但是沒有被回收的進程是殭屍進程。如果父進程終止,系統會安排PID爲1的init進程來回收子進程。
    進程休眠:sleep(受制於時間),pause(受制於信號),sigsuspend(比前兩者都好,既沒有競爭,也沒有時間浪費)。
    linux shell和web服務器用fork和execve來實現交互:讀入命令行的輸入,通過parseline判斷前後臺執行,通過builtin_command函數檢查是否是內置命令,如果不是則調用execve函數來執行。
  4. 發送信號:內核檢測到系統事件或者進程調用kill函數。
    接收信號:可以忽略或者調用signal handle來捕獲信號。一個已發出但未接收的信號稱爲pending signal。每種類型至多隻有一個待處理信號(由內核維護向量)
    信號處理原則:
    1. 處理程序儘可能小而簡單
    2. 只調用異步信號安全函數(可重入,不可被信號處理程序中斷)
    3. 保存errno
    4. 保護全局數據結構,使用volatile聲明全局變量
    5. 使用sig_automatic_t聲明flag,因爲讀寫是原子操作。
  5. setjmp.h頭文件提供了兩個非本地跳轉的函數:setjmp,longjmp。預先設置好setjmp的錯誤值,當之後再調用函數中遇到錯誤,直接調用longjmp返回setjmp處進行錯誤解碼處理。

9 虛擬內存

  1. 虛擬內存被分割爲虛擬頁,在磁盤上;物理內存被分割爲頁幀,在主存上。
    三種虛擬頁:
    1. 未分配頁
    2. 已緩存在物理內存中的已分配頁
    3. 未緩存的已分配頁。
      將虛擬地址映射到物理地址的數據結構稱爲頁表。有效位用來判斷是否虛擬頁緩存到物理內存中。地址爲空則是尚未分配。
  2. 頁表會存在頁命中和缺頁異常。因此當有不命中的時候,會進行頁面調度。由於局部性是的程序會工作在一個工作集上,當程序需要頻繁換頁的時候,就是發生了抖動。
    虛擬內存應用在內存管理:由於地址空間獨立,可以簡化鏈接。簡化加載。簡化共享(操作系統內核代碼,不同的虛擬頁指向同一塊物理幀)。簡化內存分配(虛擬內存上連續,但物理內存不連續的情況)
    虛擬內存應用於內存保護:在PTE上增加關鍵字。
  3. 地址翻譯流程圖:
    地址翻譯流程
    爲了加速地址翻譯,可會在高速緩存中儲存一個TLB,儲存頁號映射。
    爲節約內存,採用多級頁表。intel corei7採用了四級頁表,分成四個9位的片段
    常見PTE字段:P是否在內存中;R/W是否有讀寫權;U/S是否是有超級權限;WT直寫還是寫回;CD能否緩存頁表;A是否被MMU訪問過(頁替換算法);D髒位,判斷是否寫過
    linux的虛擬內存結構:task_struct維護了進程的信息,其中的mm_struct維護了虛擬內存當前狀態。其中pgd指向了第一級頁表的基址,mmap指向了一個vm_area_structs的鏈表,該鏈表維護了虛擬內存的內容(起止地址,是否有讀寫權限,是否共享)
  4. linux將虛擬內存區域和磁盤上的對象關聯起來的操作稱爲內存映射。映射對象是交換空間,它限制着虛擬頁面的總數。
    mmap和munmap可以創建和刪除內存區域。在運行時動態分配是在堆上完成的。動態分配器分爲顯示分配器(C中的malloc和free,C++中的new和delete)和隱式分配器(垃圾收集)
    顯示分配器的要求:處理任意請求序列;立即響應;只用堆;對齊;不修改已分配的塊。
  5. 堆分配會導致碎片產生:
    1. 內部碎片是分了有沒有用到的
    2. 外部碎片是沒分但是不連續,沒法再次分出去。分配器需要通過顯/隱式空閒鏈表解決。
  6. C中常見內存錯誤:
    1. 間接引用壞指針
    2. 讀未初始化的內存
    3. 緩衝區溢出(典型的gets)
    4. 假設指向對象的指針和所指對象同大小
    5. 錯位覆蓋
    6. 誤操作指針(由於運算符優先級問題,建議多用括號)
    7. 誤解指針運算(指針操作是按照所指對象大小操作的,比如int指針++是直接跳過一個int數)
    8. 引用不存在的變量(典型返回指向局部變量的指針)
    9. 引用空閒堆的數據
    10. 內存泄漏(忘記釋放指針,這個可以用智能指針解決)

10 系統級I/O

  1. unix 文件基操:打開文件有描述符fd標識,每個進程默認三個文件:標準輸入(0),標準輸出(1),標準錯誤(2)。
    此後fd會遞增,關閉文件時收回;讀寫文件,正常返回字節數,出錯返回-1,讀越界返回0;定位seek;關閉文件。
  2. 通過stat(輸入文件名)或者fstat(fd)可以獲得文件元數據。其中st_size表示文件大小,st_mode表示文件類型和訪問權限。
  3. 內核用三個數據結構表示打開的文件:描述符表:每個進程獨立擁有,每個描述符指向文件表的一個表項。文件表:所有進程共享,
    每個表項有文件的起始位置,引用計數,指向v-node表的指針。v-node表:文件信息。
  4. 文件重定向函數:dup2(oldfd,newfd)。

11 網絡編程

  1. 網絡字節儲存通常是大端法。因此需要轉換。unix提供了hton,ntoh來轉換。n代表網絡,h代表主機。inet_pton inet_ntop提供了
    十進制點分IP地址和網絡流之間的轉換。
  2. 套接字:IP+端口。
    服務器端通過 socket\rightarrowbind\rightarrowlisten,收到請求後accept處理
    客戶端通過socket\rightarrowconnect發送請求。
    可以通過 getaddrinfo函數,其中host參數是主機名,service是端口號。getnameinfo則是反過來獲得host和service。getaddrinfo在設置hints時,ai_family設置IPV4,ai_socktype設置套接字類型。

12 併發編程

  1. 使用應用級併發的程序稱爲concurrent program。操作系統提供三種構造併發程序的方式:
    1. 進程。由內核維護和調度,進程間通過IPC通信。
      優點:獨立的地址空間,不會出現覆蓋。
      缺點:進程之間通信比較慢。
    2. I/O多路複用。由應用程序自己在一個進程的上下文顯示調度。
      使用select函數,要求內核掛起進程,直到一個或多個I/O事件發生,再返回控制權給應用程序。select函數將read_set中發出請求的描述符產生一個ready_set,表明哪些請求來到。通過FD_ISSET區分需要怎麼處理。
    3. 線程。一個進程內,由內核調度。
      線程上下文切換快,線程間平等無父子關係。
  2. 多線程程序內存模型:寄存器不共享,虛擬內存共享。全局變量和本地靜態變量都可以被共享。
    對於臨界區,可以通過semphore(PV操作)實現mutex。
    三類線程不安全的函數:
    1. 不保護共享變量的函數
    2. rand函數
    3. 返回值是指向靜態變量的指針的函數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章