重點思考兩個問題
一段源代碼是怎么變成最后可執(zhí)行的程序的
一個進程,在內(nèi)存中是什么樣的
一.預備知識
為了協(xié)調(diào)CPU、內(nèi)存和高速的圖形設備 -> 北橋 -> 有相對低速的設備連在北橋上 -> 南橋,專門處理低速設備
CPU頻率被4GHz天花板限制,增加CPU數(shù)量 -> 對稱多處理器 -> 成本高,多處理機之間共享昂貴緩存,只多個核 ->多核處理器-
將用于管理計算機本身的軟件成為系統(tǒng)軟件
計算機軟件體系結構
- 平臺性的:操作系統(tǒng)內(nèi)核 驅動程序 運行庫 系統(tǒng)工具
- 程序開發(fā)性的:編譯器、匯編器、鏈接器等開發(fā)工具和開發(fā)庫
- 運行庫
- 操作系統(tǒng)內(nèi)核
- 硬件
將計算機上有限的物理內(nèi)存分配給多個程序使用,但問題是地址空間不隔離、內(nèi)存使用效率低、程序運行地址不確定。
加中間層的方法可以避免問題,把程序給出的地址看作是虛擬地址,通過映射,將虛擬地址轉換為實際物理地址。
內(nèi)存 -> 物理地址
分段將程序所需的內(nèi)存空間大小的虛擬地址映射到某個物理地址,程序A、B被映射到兩塊不同物理空間區(qū)域且無重疊,解決了地址空間不隔離、程序運行地址不確定的問題
根據(jù)局部性原理,程序在運行時,在某個時間段內(nèi),只是頻繁用到一小部分數(shù)據(jù),分頁將地址空間人為地等分成固定大小的頁共程序使用,提高內(nèi)存使用效率進程:所有應用程序以進程的方式運行在比操作系統(tǒng)權限更低的級別,有獨立的地址空間,進程之間地址空間相互隔離,CPU分配資源的最小單位
線程:cpu執(zhí)行任務的最小單元,線程ID + 當前指令指針PC + 寄存器集合 + 堆棧
一個進程由一個或多個線程組成,每個線程都運行在進程的上下文中,各個線程之間共享程序的內(nèi)存空間(代碼段、數(shù)據(jù)段、堆)及一些進程級資源(打開文件和信號)

線程狀態(tài):運行 就緒 等待
線程調(diào)度:
- 輪轉法:讓各個線程輪流執(zhí)行一小段時間的方法
- 優(yōu)先級調(diào)度:改變優(yōu)先級的方法:指定優(yōu)先級、根據(jù)進入等待狀態(tài)的頻繁程度提升或降級優(yōu)先級、長時間得不到執(zhí)行而提升優(yōu)先級
- 線程安全
同步:在一個線程訪問數(shù)據(jù)未結束時,其他線程不得對同一個數(shù)據(jù)進行訪問,將數(shù)據(jù)的訪問原子化
同步最常見方法:鎖,線程訪問數(shù)據(jù)或資源前先獲取鎖,并在訪問結束之后釋放鎖,鎖被占用時,獲取鎖的線程等待,直到鎖重新可用
- 信號量 標志資源占用/非占用
- 互斥量 必須由同一個線程釋放、獲取的信號量
- 臨界區(qū) 僅限于本進程,其他進程無法獲取的互斥量
編譯器優(yōu)化時,可能為了效率而交換毫不相干的兩條相鄰指令的執(zhí)行順序,使用volatile關鍵字阻止過度優(yōu)化
1.阻止編譯器為了提高速度將一個變量緩存到寄存器內(nèi)而不寫回
2.阻止編譯器調(diào)整操作volatile變量的指令順序
二.編譯和鏈接
編譯 + 鏈接 = 構建
- 預編譯
處理以‘#’開始的預編譯指令,展開宏定義 #define #if #endif
? 刪除注釋 添加行號標示 - 編譯
- 詞法分析 將源代碼字符序列分割成一系列記號
- 語法分析
用語法分析器將產(chǎn)生的記號進行語法分析,產(chǎn)生語法樹 - 語義分析
將語法樹上的類型不符的插入相應結點,做隱式轉換 -
生成中間代碼
編譯器前端 Clang 優(yōu)化部分代碼
編譯過程.png
- 匯編
編譯器后端 LLVM GCC 將匯編代碼轉變?yōu)闄C器可執(zhí)行指令 - 鏈接
將源代碼模塊獨立地編譯,然后將其組裝起來,將目標文件鏈接形成可執(zhí)行文件- 地址和空間分配
- 符號決議、地址綁定
- 重定向,確定全局變量和函數(shù)最終運行時的絕對地址
三.目標文件

.text/.data 它們在文件中和虛擬地址中分配空間
.bss僅分配虛擬地址空間
每個可重定位目標模塊m都有一個符號表,包含m所定義和引用的符號的信息,共有三種不同符號:
由m定義并能被其他模塊引用的全局符號:非靜態(tài)的C函數(shù)和全局變量
由其他模塊定義并被模塊m引用的全局符號:其他模塊中定義的非靜態(tài)C函數(shù)和全局變量
只被模塊m定義和引用的局部符號:帶static屬性的C函數(shù)和全局變量,
這些符號在模塊m中全局可見,但其他模塊不可見
其他非本地靜態(tài)變量由棧管理,鏈接器對此類不感興趣
利用 static 屬性隱藏變量和函數(shù)名字
段表:數(shù)組中每個元素都是結構體,包括段名、類型、加載地址、相對于文件頭的偏移量、段大小、鏈接信息
重定位表:需要重定位的信息
函數(shù)、變量需要獨特的符號名,防止類似的符號名沖突,C++采用命名空間的方法解決符號沖突,Objective-C 采用加前綴方式。
函數(shù)簽名:包含了一個函數(shù)的信息,包括函數(shù)名、參數(shù)類型、所屬類、名稱空間及其他信息
符號分為強符號,和弱符號。強符號不可名稱重復,弱符號(未初始化的全局變量)可以符號名相同。
對符號名的引用分為強引用和弱引用,強引用表示如果找不到符號定義會報錯,弱引用不報錯,默認為0或某個特殊值。
目標文件里面還有可能保存調(diào)試信息
可以進行設置斷點、監(jiān)視變量變化、單步行進等調(diào)試是因為編譯器將源代碼的行、函數(shù)和變量類型、結構體的定義、字符串保存在目標文件里
GCC編譯時加上 -g 參數(shù),編譯器就會加上調(diào)試信息
四.靜態(tài)鏈接
鏈接:將幾個輸入目標文件加工后合并成一個輸出文件,這個文件可被加載到內(nèi)存并執(zhí)行。
兩步鏈接:空間與地址分配 符號解析與鏈接時重定位
一種語言的開發(fā)環(huán)境會附帶語言庫,語言庫是對操作系統(tǒng)API的包裝、常用函數(shù)
靜態(tài)庫可看做一組目標文件的集合,參與編譯
鏈接控制腳本控制鏈接器的運行,將目標文件和庫文件轉化為可執(zhí)行文件
五.可執(zhí)行文件的裝載與進程
程序:靜態(tài) 預先編譯好的指令和數(shù)據(jù)集合的文件
進程:動態(tài) 程序運行時的過程
CPU位數(shù)決定了虛擬地址空間的大小 —> 硬件尋址空間大小頁映射函數(shù)將虛擬空間的各個頁映射至相應的物理空間
程序執(zhí)行時所需要的指令和數(shù)據(jù)必須在內(nèi)存中才能夠正常運行,又根據(jù)局部性原理,可將程序最常用的部分駐留在內(nèi)存中,不常用的存放在磁盤里,需要時,動態(tài)裝入
動態(tài)裝載的方法:
- 覆蓋裝入
手工將程序分成若干塊,在編寫覆蓋管理器管理模塊何時應該駐留內(nèi)存、何時應被替換掉 - 頁映射
將內(nèi)存和磁盤中的數(shù)據(jù)及指令按照“頁”為單位劃分,需要時裝入,超出范圍后,用FIFO或最少使用算法替換新頁
進程的建立:
創(chuàng)建一個獨立的虛擬地址空間
讀取可執(zhí)行文件頭,并且建立虛擬空間與可執(zhí)行文件的映射關系
將CPU的指令寄存器設置成可執(zhí)行文件的入口地址,啟動運行
進程結束后,將相關資源(進程地址空間、物理內(nèi)存、打開文件、網(wǎng)絡鏈接)都被操作系統(tǒng)關閉或收回
頁錯誤:當執(zhí)行到某個地址的指令時,發(fā)現(xiàn)頁為空(未被裝入)
段地址對齊
進程啟動前,將系統(tǒng)環(huán)境變量和程序運行參數(shù)保存到進程的虛擬空間棧中
程序庫部分會把堆棧里的初始化信息中的參數(shù)信息傳遞給main函數(shù)的argc、argv參數(shù)argc命令行參數(shù)數(shù)量、argv命令行參數(shù)字符串指針數(shù)組
六.動態(tài)鏈接
靜態(tài)鏈接把所有程序模塊都鏈接成一個單獨的可執(zhí)行文件,可能會帶來這些問題:
- 對計算機內(nèi)存和磁盤空間浪費嚴重,每個程序內(nèi)部都保留著公用庫函數(shù)
- 更新庫函數(shù)時,程序需重新鏈接
動態(tài)鏈接:把程序按照模塊拆分為各個相對獨立的部分,運行時才將他們鏈接在一起,在運行和加載時,可以被加載到任意的內(nèi)存地址,并和一個在內(nèi)存中的程序鏈接起來。
共享對象會被多個程序調(diào)用,導致其在虛擬地址空間中的位置難以確定,所以共享對象需要在裝載時重定位
但裝載時重定位會導致無法在多個進程間共享,采用地址無關代碼
將共享對象模塊中的地址引用劃分為模塊內(nèi)部引用和模塊外部引用、指令引用和數(shù)據(jù)訪問
- 對于模塊內(nèi)部的指令和數(shù)據(jù)引用,采用相對偏移調(diào)用的方法
- 對于模塊外部的指令和數(shù)據(jù)引用,采用GOT全局偏移表間接訪問
延遲綁定 Lazy Binding 當函數(shù)第一次被用到的時候才重定位,提供程序運行速度
動態(tài)鏈接器是一個特殊共享對象,不依賴與任何動態(tài)共享文件,且自己的重定位工作由自己完成
自舉:不用到任何全局和靜態(tài)變量,自己完成重定位工作

七.內(nèi)存
內(nèi)核空間(內(nèi)核使用,應用程序無法訪問) + 用戶空間
用戶空間:
棧 從高位向低地址增長,向下增長
? i386中,esp寄存器指向棧頂,ebp寄存器指向了函數(shù)活動記錄的一個固定位置
-堆棧幀 保存了一個函數(shù)調(diào)用所需要的維護信息
函數(shù)的返回地址和參數(shù)
臨時變量 函數(shù)的非靜態(tài)局部變量 編譯器自動生成的其他臨時變量
保存的上下文 在函數(shù)調(diào)用前后需要保持不變的寄存器
調(diào)用慣例:函數(shù)的調(diào)用方和被調(diào)用方對于函數(shù)如何調(diào)用的明確約定
棧上的數(shù)據(jù)在函數(shù)返回時會被釋放掉,無法將數(shù)據(jù)傳遞至函數(shù)外部堆 從低位向高地址增長(不總是向上增長)
程序可申請一段內(nèi)存,釋放申請的這個內(nèi)存
堆上的內(nèi)存管理由堆自己進行,若由操作系統(tǒng)進行,會頻繁進行系統(tǒng)調(diào)用,性能開銷較大
若一次性分給進程的空間不夠,可能出現(xiàn)系統(tǒng)調(diào)用,再申請一部分空間

代碼區(qū):存放程序的二進制代碼
常量區(qū):所有常量
全局數(shù)據(jù)區(qū):全局變量和靜態(tài)數(shù)據(jù)
可執(zhí)行文件映像:由裝載器將可執(zhí)行文件的內(nèi)存讀取或映射在這里
保留區(qū):對內(nèi)存中收到保護而禁止訪問的內(nèi)存區(qū)域總稱
?
Debug模式中,將未初始化區(qū)域都初始化為0xCC,有助于判斷一個變量是否沒有初始化,0xCCCC被當做文本就是燙,0xCDCD是屯
HotPatch 可替換函數(shù),實現(xiàn)Hook,允許用戶在某些時刻截獲特定函數(shù)的調(diào)用
八.運行庫
入口函數(shù):運行庫的一部分,一個程序的初始化和結束部分,準備好了main函數(shù)執(zhí)行所需要的環(huán)境,并且負責調(diào)用main函數(shù),這樣在main函數(shù)中才能:申請內(nèi)存、使用系統(tǒng)調(diào)用、觸發(fā)異常、訪問I/O
程序運行步驟:
1.操作系統(tǒng)創(chuàng)建進程后,把控制權交給程序入口
2.入口函數(shù)對運行庫和程序運行環(huán)境進行初始化,包括堆、I/O、線程、全局變量構造等
3.入口函數(shù)完成初始化后,調(diào)用main函數(shù),進行程序主體部分
4.main函數(shù)執(zhí)行完畢后,返回入口函數(shù),入口函數(shù)進行清理工作,包括全局變量析構、堆銷毀、關閉I/O,然后系統(tǒng)調(diào)用結束進程
環(huán)境變量:存在于系統(tǒng)中的一些公用數(shù)據(jù),如系統(tǒng)搜索路徑,當前OS版本
I/O 指代了程序與外界的交互,包括文件、管道、網(wǎng)絡、命令行、信號等FILE
運行庫:啟動與退出、標準函數(shù)、I/O、堆、語言實現(xiàn)、調(diào)試
九.系統(tǒng)調(diào)用
系統(tǒng)調(diào)用:為了讓應用程序 (運行庫) 有能力訪問系統(tǒng)資源,讓程序借助操作系統(tǒng)做一些必須由操作系統(tǒng)支持的行為,操作系統(tǒng)提供的接口
現(xiàn)代CPU可在多種截然不同的特權級別下執(zhí)行指令,分為用戶模式、內(nèi)核模式
接口的調(diào)用通過中斷實現(xiàn)從用戶模式到內(nèi)核模式的切換
上下文:操作系統(tǒng)保持跟蹤進程運行所需的所有狀態(tài)信息,包括 PC 和寄存器文件的當前值,以及主存的內(nèi)容。
在任何時刻,單處理器系統(tǒng)都只能執(zhí)行一個進程的代碼,當操作系統(tǒng)決定要把控制權從當前進程轉移到某個新進程時,就會進行上下文切換,保存當前進程的上下文,恢復新進程的上下文,然后將控制權傳遞到新進程。
課后問題及答案
源代碼是怎么變成可執(zhí)行文件的,每一步的作用是什么?
(預編譯,詞法分析,語法分析,語義分析,中間語言生成目標代碼生成,匯編,鏈接)
預編譯:展開宏,刪除注釋,標注行號
詞法分析:將代碼解析成一個個記號
語法分析:生成語法樹
語義分析:將語法樹上的類型不符的插入相應結點,做隱式轉換
中間代碼生成匯編:生成匯編語言
鏈接:將源代碼模塊獨立地編譯,然后將其組裝起來,將目標文件鏈接形成可執(zhí)行文件?應用層、API、運行庫、系統(tǒng)調(diào)用、操作系統(tǒng)內(nèi)核之間的關系是什么?
應用層通過API調(diào)用運行庫的接口,運行庫通過系統(tǒng)調(diào)用調(diào)用操作系統(tǒng)內(nèi)核?虛擬內(nèi)存空間是什么,為什么要有虛擬內(nèi)存空間?
虛擬內(nèi)存空間使得應用程序認為它擁有連續(xù)的可用的內(nèi)存(一個連續(xù)完整的地址空間),而實際上,它通常是被分隔成多個物理內(nèi)存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進行數(shù)據(jù)交換。?靜態(tài)鏈接和動態(tài)鏈接分別表示什么,大概是怎么實現(xiàn)的?
靜態(tài)鏈接:把所有程序模塊都鏈接成一個單獨的可執(zhí)行文件
動態(tài)鏈接:把程序按照模塊拆分為各個相對獨立的部分,運行時才將他們鏈接在一起?可執(zhí)行文件的結構如何?分為哪些段?
文件頭、代碼段、數(shù)據(jù)段、bss段(未初始化的全局變量)進程的內(nèi)存格局是怎樣的?
堆、棧、全局/靜態(tài)區(qū),代碼區(qū),常量區(qū)堆和棧的區(qū)別,函數(shù)調(diào)用和棧的關系
進程和線程的區(qū)別
異步和同步,串行,并發(fā),并行的區(qū)別
- 異步 并不按順序執(zhí)行,開啟新線程
- 同步 按順序執(zhí)行,不開啟新線程
- 串行 串行是指多個任務,各個任務按順序執(zhí)行,完成一個之后才能進行下一個
- 并行 同一時刻的肯定有多個任務在執(zhí)行
- 并發(fā) 同一時間間隔有多個任務在發(fā)生
多并發(fā)任務,僅多線程能加快速度么?
(不能,會變慢,有線程切換的開銷)多個線程之間可以共享那些數(shù)據(jù)?
全局變量、堆上的數(shù)據(jù)、函數(shù)里的靜態(tài)變量、程序代碼打開的文件進程之間如何通信管道?
在父子進程中單向的流動有名管道:可在無親緣關系的進程中通信信號量:控制多個進程對共享資源的訪問
消息隊列:由消息的鏈表,存放在內(nèi)核中并由消息隊列標示
共享內(nèi)存:映射一段能被其他進程所訪問的內(nèi)存
套接字:進程間通信機制介紹幾種鎖,他們的用途和區(qū)別?
//todo
我們在使用多線程的時候多個線程可能會訪問同一塊資源,這樣就很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全等問題,這時候就需要我們保證每次只有一個線程訪問這一塊資源,鎖 應運而生。

