很早就聽(tīng)過(guò)這本書(shū)的介紹辉词,每次想靜下心來(lái)研讀的時(shí)候總會(huì)被一些瑣事打斷。這段時(shí)間比較空閑繁调,正好把這把咀嚼一下骆莹。
這本書(shū)詳細(xì)描述了Windows和Linux操作系統(tǒng)各自的可執(zhí)行文件颗搂、目標(biāo)文件格式。
- C/C++代碼如何被編譯成目標(biāo)文件及程序在目標(biāo)文件中文件存儲(chǔ)幕垦。
- 目標(biāo)文件如何被鏈接器鏈接到一起丢氢,并且形成可執(zhí)行文件的。
- 目標(biāo)文件在鏈接時(shí)符號(hào)處理智嚷、重定位及地址分配如何進(jìn)行
- 可執(zhí)行文件如何被裝載并且執(zhí)行
- .....
- 什么是堆卖丸,什么是棧
- ......
下面開(kāi)始探索之旅吧!
操作系統(tǒng)基礎(chǔ)回顧
溫故而知新盏道,可以為師
下圖為全文的導(dǎo)圖
組成部分
計(jì)算機(jī)三個(gè)最為重要的部件:中央處理器CPU稍浆、內(nèi)存、I/O控制芯片猜嘱。
早起計(jì)算機(jī)因?yàn)楹诵念l率不高所以設(shè)備都是連在同一條總線上衅枫,并且每個(gè)設(shè)備都還有一個(gè)響應(yīng)的I/O控制器。
后來(lái)核心頻率提升朗伶,內(nèi)存跟不上CPU的速度弦撩,于是產(chǎn)生了與內(nèi)存頻率一致的系統(tǒng)總線,CPU采用倍頻的方式與系統(tǒng)總線通信论皆。再到后來(lái)出現(xiàn)了北橋芯片(協(xié)調(diào)高速芯片)益楼,南橋芯片(協(xié)調(diào)低速設(shè)備)
北橋芯片:協(xié)調(diào)CPU猾漫、內(nèi)存、高速的圖形設(shè)備长酗,以便高速的交換數(shù)據(jù)
南橋芯片:協(xié)調(diào)磁盤、USB颖变、鍵盤蛙粘、鼠標(biāo)等慢速設(shè)備
上圖可見(jiàn)邢笙,位于中間的是連接所有高速芯片的北橋(PCI),他的左邊是CPU丈积,右邊是內(nèi)存筐骇。
系統(tǒng)軟件可以分為兩塊,一塊是平臺(tái)性的江滨,比如操作系統(tǒng)內(nèi)核铛纬,驅(qū)動(dòng)程序,運(yùn)行庫(kù)和數(shù)以千計(jì)的系統(tǒng)工具唬滑;另一塊是用于程序開(kāi)發(fā)的告唆,比如編譯器、匯編器晶密、鏈接器擒悬、開(kāi)發(fā)庫(kù)和開(kāi)發(fā)工具。這里主要介紹的是鏈接器及庫(kù)相關(guān)的內(nèi)容稻艰。
無(wú)論是在計(jì)算機(jī)軟件體系還是硬件體系有一句至理名言:計(jì)算機(jī)科學(xué)領(lǐng)域的任何問(wèn)題都可以通過(guò)一個(gè)間接的中間層來(lái)解決懂牧。基本上這菊花概括了所有的設(shè)計(jì)要點(diǎn)尊勿。(想想我們平時(shí)所用的什么架構(gòu)僧凤、模式都是這個(gè)道理)
可以把這種方式叫做層次體系,相鄰的層次通過(guò)定義接口實(shí)現(xiàn)通訊元扔,一般是下面那層是接口的提供者躯保,上層使用接口(對(duì)應(yīng)到軟件開(kāi)發(fā)中也就是面向接口編程,這套在Java中的Spring框架體現(xiàn)得淋漓盡致)澎语。接口都是被精心設(shè)計(jì)過(guò)的途事,盡量保證穩(wěn)定不變,這樣對(duì)上層屏蔽了具體實(shí)現(xiàn)咏连,任何一層次能都可以被修改或者被替換盯孙。
整體體系結(jié)構(gòu)依賴關(guān)系:上次應(yīng)用層程序——》操作系統(tǒng)應(yīng)用程序編程接口——》運(yùn)行庫(kù)——》系統(tǒng)調(diào)用接口(以軟件終端的方式)——》硬件接口
- 運(yùn)行庫(kù)API:Linux下的GLibc庫(kù)提供的POSIX的API,Window運(yùn)行庫(kù)提供Windows API祟滴,比如Win32
- 系統(tǒng)調(diào)用接口:系統(tǒng)調(diào)用接口在實(shí)現(xiàn)中以一般軟件中斷的方式提供振惰,Linux中以0x80號(hào)中斷作為系統(tǒng)調(diào)用接口,Winodows以0x2E作為系統(tǒng)調(diào)用接口
硬件的接口定義決定操作系統(tǒng)內(nèi)核垄懂,確定驅(qū)動(dòng)程序如何操作硬件骑晶,如何與硬件通信痛垛,這種接口叫做硬件規(guī)格,硬件廠商負(fù)責(zé)提供硬件規(guī)格桶蛔,操作系統(tǒng)及驅(qū)動(dòng)程序的開(kāi)發(fā)者通過(guò)閱讀硬件規(guī)格匙头,文檔來(lái)編寫(xiě)。
操作系統(tǒng)主要是提供抽象的接口以及管理硬件資源(大學(xué)的時(shí)候老師講過(guò))仔雷。
CPU進(jìn)化過(guò)程:CPU只能運(yùn)行一個(gè)程序——》多道程序(監(jiān)控CPU是否空閑)——》分時(shí)系統(tǒng)(每個(gè)程序都有機(jī)會(huì)運(yùn)行一小段時(shí)間蹂析,任何一個(gè)死循環(huán)都可能導(dǎo)致死機(jī))——》多任務(wù)系統(tǒng)(操作系統(tǒng)管理硬件資源,運(yùn)行在硬件保護(hù)的級(jí)別碟婆。)
多任務(wù)系統(tǒng)中所有應(yīng)用程序都以進(jìn)程的方式運(yùn)作电抚,但是比操作系統(tǒng)的權(quán)限更低,每個(gè)進(jìn)程有自己獨(dú)立的地址空間竖共,進(jìn)程之前的地址空間相互隔離蝙叛。CPU由操作系統(tǒng)統(tǒng)一分配,根據(jù)進(jìn)程優(yōu)先級(jí)調(diào)度公给,如果進(jìn)程占用CPU太久的時(shí)間則會(huì)被暫停借帘,進(jìn)而分配給其他等待運(yùn)行的進(jìn)程。分配給每個(gè)進(jìn)程的時(shí)間都很短淌铐,CPU在多個(gè)進(jìn)程間快速切換肺然,造成了多個(gè)進(jìn)程同時(shí)運(yùn)行的假象。
內(nèi)存腿准、分段狰挡、分頁(yè)(這個(gè)很重要!)
早期計(jì)算機(jī)中释涛,程序是直接運(yùn)行在物理內(nèi)存的。比如計(jì)算機(jī)128M倦沧,程序A10M唇撬,程序B100M,程序C20M展融。那么最直接的就是將內(nèi)存的前10M給A窖认,10M-110M給B。但是這種方式有很多問(wèn)題:
- 地址空間不隔離(非常危險(xiǎn)):直接訪問(wèn)物理地址告希,惡意程序很容易改寫(xiě)其他程序的內(nèi)存數(shù)據(jù)扑浸;除此之外如果程序有bug,同樣會(huì)出現(xiàn)不小心改了其他程序的數(shù)據(jù)現(xiàn)象燕偶。這樣就非常不安全喝噪,穩(wěn)定
- 內(nèi)存使用率低:比如上面例子想要運(yùn)行C,這個(gè)時(shí)候內(nèi)存空間已經(jīng)不足了指么,這個(gè)時(shí)候需要把其中部分?jǐn)?shù)據(jù)寫(xiě)到磁盤上酝惧,等到要用的時(shí)候在讀回來(lái)榴鼎。由于程序空間是連續(xù)的,將A寫(xiě)到磁盤還是不夠用還需要將B程序到磁盤晚唇。中間有大量數(shù)據(jù)的換入換出
- 程序運(yùn)行地址不確定:程序每次載入運(yùn)行的時(shí)候巫财,需要從內(nèi)測(cè)中分配一塊足夠大的空間,但是這個(gè)無(wú)內(nèi)置是不確定會(huì)給編寫(xiě)程序帶來(lái)麻煩哩陕。因?yàn)榫帉?xiě)程序的時(shí)候平项,訪問(wèn)數(shù)據(jù)、指令的目標(biāo)地址很多是固定的悍及,需要重定位闽瓢。
以上幾個(gè)問(wèn)題通過(guò)增加了一個(gè)中間層解決,也就是間接訪問(wèn)地址并鸵。將程序給出的地址看做是虛擬地址鸳粉,然后通過(guò)映射關(guān)系,將虛擬地址轉(zhuǎn)換為物理地址园担。只要能夠管理好映射過(guò)程就能保證程序之間的內(nèi)存區(qū)域不會(huì)重疊届谈,達(dá)到地址空間隔離的效果。——虛擬內(nèi)存
分段用來(lái)解決前面提到的地址空間不隔離和程序運(yùn)行地址不確定弯汰〖枭剑基本思路是把一段與程序所需要的內(nèi)存空間大小的虛擬空間映射到某個(gè)地址空間,比如A程序10M咏闪,0X00000000到0X00A00000的10M虛擬空間曙搬,然后物理地址分配同樣大小的區(qū)域,比如0x00100000到0x00B00000鸽嫂。然后將這兩塊相同大小的地址做空間一一映射纵装,操作系統(tǒng)提供這個(gè)映射函數(shù),實(shí)際由硬件轉(zhuǎn)換据某,后面會(huì)提到具體是由MMU(內(nèi)存管理單元)實(shí)現(xiàn)橡娄。
比如A中訪問(wèn)0x00001000,CPU會(huì)將這個(gè)二地址轉(zhuǎn)換為實(shí)際物理地址的0X00101000.
異常情況處理:雖然分段做到了地址隔離癣籽,A和B沒(méi)有任何重疊挽唉,但是如果A訪問(wèn)虛擬地址空間超過(guò)了0x00A00000范圍,那么硬件就會(huì)判斷這是一個(gè)非法訪問(wèn)筷狼,拒絕這地址請(qǐng)求瓶籽,并將這個(gè)請(qǐng)求報(bào)告給操作系統(tǒng)或監(jiān)控程序,讓它們進(jìn)行下一步處理埂材,一般就是產(chǎn)生異常塑顺。
分段也做到了程序不需要重定位,對(duì)程序而言不需要關(guān)系物理地址變化俏险,只需要安裝從地址0X00000000到0X00A00000l來(lái)編寫(xiě)程序茬暇。但是還是沒(méi)有解決內(nèi)存使用效率的問(wèn)題首昔。如果內(nèi)存不足還是會(huì)造成很大的換入換出(以程序?yàn)閱挝唬6冗€是太大)
根據(jù)程序局部性原理糙俗,當(dāng)程序運(yùn)行時(shí)勒奇,某個(gè)時(shí)間段內(nèi),它只會(huì)頻繁的使用到一小部分?jǐn)?shù)據(jù)巧骚,也就是說(shuō)很多數(shù)據(jù)其實(shí)并不會(huì)被使用到赊颠,于是粒度更小的內(nèi)存分隔和映射方法孕育而生,那就是分頁(yè)劈彪。
分頁(yè)是把地址空間人為的分成固定大小的頁(yè)竣蹦,頁(yè)大小范圍由硬件決定,其次由操作系統(tǒng)最終確定頁(yè)大小沧奴。
舉個(gè)例子:
把進(jìn)程的虛擬空間地址以頁(yè)分隔痘括,數(shù)據(jù)和代碼也裝載到內(nèi)存,不常用的放到磁盤保存滔吠,需要的時(shí)候在從磁盤中讀取處理纲菌。默認(rèn)情況下虛擬也大小為4k。
- 有兩個(gè)進(jìn)程Process1疮绷、Process2
- Process1中虛擬頁(yè)VP0翰舌、VP1、VP7映射到物理頁(yè)P(yáng)P0冬骚、PP2椅贱、PP3
- Process2中虛擬頁(yè)VP0、VP2只冻、VP3庇麦、VP7映射到物理頁(yè)P(yáng)P5、PP0喜德、PP1女器、PP3
- 虛擬VP2、VP3頁(yè)面保存在磁盤頁(yè)中的DP0住诸、DP1,并不在內(nèi)存中涣澡,當(dāng)進(jìn)程需要這兩個(gè)也的時(shí)候贱呐,硬件會(huì)捕獲這個(gè)信息,就是所謂的頁(yè)錯(cuò)誤入桂,然后操作系統(tǒng)接管進(jìn)程奄薇,負(fù)責(zé)將VP2、VP3從磁盤中載入到內(nèi)存抗愁。
- 可以看到到物理頁(yè)中的PP0馁蒂、PP3在進(jìn)程Process1呵晚、Process2都有虛擬頁(yè)映射到其中。這樣就實(shí)現(xiàn)了內(nèi)存共享
虛擬內(nèi)存實(shí)現(xiàn)需要依賴硬件支持沫屡,不同CPU處理方式不同饵隙,但是所有的硬件都采用MMU(內(nèi)存管理單元)部件來(lái)進(jìn)行頁(yè)映射管理。MMU將CPU發(fā)出的虛擬地址轉(zhuǎn)換為物理地址沮脖。
多線程
線程被稱為輕量級(jí)進(jìn)程金矛,程序執(zhí)行流的最小單元。線程由線程ID勺届,當(dāng)前指令指針驶俊,寄存器和堆棧組成。進(jìn)程由多個(gè)線程組成免姿,各個(gè)線程之間共享內(nèi)存空間(代碼段饼酿、數(shù)據(jù)段、堆)及進(jìn)程級(jí)別的資源(打開(kāi)文件和信號(hào))胚膊。
進(jìn)程故俐、線程關(guān)系圖:
線程訪問(wèn)非常自由,可以訪問(wèn)進(jìn)程內(nèi)存中的所有數(shù)據(jù)澜掩,甚至包含其他線程轉(zhuǎn)給你的堆棧(如果知道其他線程的堆棧地址购披,但是很少見(jiàn)),線程也擁有自己的私有存儲(chǔ)空間肩榕。
- 棧:雖然不是被其他線程完全無(wú)法訪問(wèn)刚陡,但是一般可以認(rèn)為是私有數(shù)據(jù)
- 線程局部存儲(chǔ)(TLS):操作系統(tǒng)為線程單獨(dú)提供的私有空間,但是很有限
- 寄存器:包括PC株汉,寄存器是執(zhí)行流的基本數(shù)據(jù)筐乳,為線程私有。
可以總結(jié)如下表
線程總是并發(fā)
的執(zhí)行乔妈,線程數(shù)小于等于處理器數(shù)量蝙云,線程的并發(fā)才是真真的并發(fā)(同一時(shí)刻,多個(gè)進(jìn)行)路召,不同線程運(yùn)行在不同的處理器上勃刨;當(dāng)大于處理器數(shù)量,此時(shí)會(huì)出現(xiàn)一個(gè)處理器運(yùn)行多個(gè)線程的情況股淡。
單處理器情況下身隐,多線程并發(fā)是一種模擬出來(lái)的狀態(tài),操作系統(tǒng)讓多線程輪流執(zhí)行唯灵,每次僅僅執(zhí)行很小一段時(shí)間贾铝,這稱作線程調(diào)度。
線程調(diào)度至少有三個(gè)狀態(tài):
- 運(yùn)行:線程正在執(zhí)行。
- 就緒:線程可以立刻運(yùn)行垢揩,但CPU被其他線程占用玖绿。
- 等待:線程在等待某一個(gè)事件(比如:I/O或者同步)發(fā)生,無(wú)法執(zhí)行叁巨。
運(yùn)行狀態(tài)下的線程執(zhí)行時(shí)間叫做時(shí)間片斑匪,
- 時(shí)間片用盡了就進(jìn)入就緒狀態(tài);——用盡
- 如果時(shí)間片用盡之前線程就開(kāi)始等待某事件俘种,則會(huì)就進(jìn)入等待狀態(tài)秤标;——事件
- 線程離開(kāi)運(yùn)行態(tài),操作系統(tǒng)就會(huì)調(diào)度其他就緒的線程執(zhí)行宙刘;
- 在等待狀態(tài)的線程等待的事件發(fā)生之后苍姜,線程就進(jìn)入就緒狀態(tài);
狀態(tài)切換如下圖:
主流的調(diào)度策略雖然不同但是都有優(yōu)先級(jí)調(diào)度及輪轉(zhuǎn)法悬包。
- 輪轉(zhuǎn)法:讓各個(gè)線程輪流執(zhí)行一小段時(shí)間的方法衙猪,線程之間交錯(cuò)執(zhí)行
- 優(yōu)先級(jí)調(diào)度:確定線程按照什么順序輪流執(zhí)行難,線程都有自己的優(yōu)先級(jí)布近,具有高優(yōu)先級(jí)的會(huì)更早的執(zhí)行垫释,低優(yōu)先級(jí)的需要等待沒(méi)有高優(yōu)先級(jí)線程存在的時(shí)候才執(zhí)行。比如Linux中就是通過(guò)pthread來(lái)實(shí)現(xiàn)撑瞧,iOS中也是
線程可以只定義優(yōu)先級(jí)棵譬,系統(tǒng)也會(huì)根據(jù)線程執(zhí)行狀態(tài)改變線程的優(yōu)先級(jí)。頻繁進(jìn)入等待狀態(tài)的線程(處理I/O)比頻繁進(jìn)行大量計(jì)算(每次把時(shí)間片用盡的線程)有更多的機(jī)會(huì)執(zhí)行预伺,因?yàn)轭l繁等待的線程只占用很少的時(shí)間片订咸,CPU喜歡先執(zhí)行簡(jiǎn)單的。
如果一個(gè)線程一直都得不到執(zhí)行酬诀,這就是餓死現(xiàn)象脏嚷。優(yōu)先級(jí)較低,總有較高優(yōu)先級(jí)在占用CPU瞒御,為了避免這種現(xiàn)象父叙,調(diào)度系統(tǒng)會(huì)逐步提升等待時(shí)間過(guò)長(zhǎng)卻得不到機(jī)會(huì)執(zhí)行的線程優(yōu)先級(jí)。
優(yōu)先級(jí)改變觸發(fā)條件:
- 用戶指定優(yōu)先級(jí)
- 根據(jù)線程進(jìn)入等待狀態(tài)的頻率提升或降低優(yōu)先級(jí)
- 長(zhǎng)時(shí)間得不到執(zhí)行而被提升優(yōu)先級(jí)
搶占:線程在用盡時(shí)間片之后被強(qiáng)制剝奪執(zhí)行的權(quán)利進(jìn)入就緒狀態(tài)肴裙,這個(gè)過(guò)程叫做搶占趾唱,這樣的線程就是搶占線程。目前基本所有的線程都是搶占式的蜻懦。
不可搶占線程:也就是線程不可被搶占甜癞,只有線程主動(dòng)發(fā)出放棄執(zhí)行的命令,主動(dòng)進(jìn)入就緒態(tài)阻肩,而不靠時(shí)間片才會(huì)空出當(dāng)前占用的資源。
不可搶占線程觸動(dòng)放棄情況:
- 線程視圖等待某事件
- 線程主動(dòng)放棄時(shí)間片
雖然不可搶占線程可以避免一些因?yàn)閾屨季€程里調(diào)度時(shí)機(jī)不確定而產(chǎn)生的線程安全問(wèn)題,但是現(xiàn)在非搶占式線程很少烤惊。
Linux多線程
Linux內(nèi)核中并不存在真正的線程概念乔煞,Linux將所有的執(zhí)行實(shí)體(線程還是進(jìn)程都一樣)都稱為任務(wù),每一個(gè)任務(wù)都類似于一個(gè)單線程的進(jìn)程柒室,具有內(nèi)存空間渡贾,執(zhí)行實(shí)體,文件資源等雄右。
進(jìn)程:不同任務(wù)之間可以選擇共享內(nèi)存空間空骚,共享同一個(gè)內(nèi)存空間的多任務(wù)構(gòu)成一個(gè)進(jìn)程,這些任務(wù)也就是這個(gè)進(jìn)程里面的線程擂仍。
Linux創(chuàng)建一個(gè)新的任務(wù)的方式:
fork函數(shù)產(chǎn)生一個(gè)和當(dāng)前進(jìn)程完全一樣的新進(jìn)程囤屹,fork產(chǎn)生的新任務(wù)速度非常快逢渔,因?yàn)椴粫?huì)復(fù)制原任務(wù)的內(nèi)存空間肋坚,而是共享一個(gè)寫(xiě)時(shí)復(fù)制的內(nèi)存空間。
寫(xiě)時(shí)復(fù)制就是指兩個(gè)任務(wù)可以同時(shí)自由得讀取內(nèi)存肃廓,但任意一個(gè)任務(wù)要對(duì)內(nèi)存修改智厌,內(nèi)存就會(huì)賦值一份給修改方使用。fork只能產(chǎn)生本任務(wù)的鏡像盲赊,所以需要用exec才能啟動(dòng)新的任務(wù)铣鹏。
線程安全(開(kāi)發(fā)中經(jīng)常遇到的)
多線程程序中,可訪問(wèn)的全局變量及堆數(shù)據(jù)隨時(shí)都可能被其他線程改變哀蘑,由此產(chǎn)生了的線程安全诚卸。
線程安全的根本原因是同時(shí)寫(xiě)一個(gè)共享數(shù)據(jù),每個(gè)線程都有自己的寄存器递礼。
計(jì)算機(jī)中單條指令在執(zhí)行的時(shí)候不會(huì)被打斷惨险,所以在執(zhí)行單條指令的時(shí)候不會(huì)存在線程安全問(wèn)題,稱為具有原子性脊髓;常見(jiàn)的自增++操作辫愉,因?yàn)樽栽?+操作會(huì)被匯編為多條指令,所以在執(zhí)行自增的時(shí)候可能被打斷将硝,去執(zhí)行其他代碼恭朗,不具有原子性,就有可能造成線程安全的問(wèn)題
一個(gè)例子:
其中的i為共享變量依疼。
在計(jì)算機(jī)中會(huì)按照下面的步驟
- 讀取i到某個(gè)寄存器X(每個(gè)線程有自己的寄存器)
- X++
- 將X的內(nèi)容返回給i(也就是從寄存器取值然后送入內(nèi)存)
由于1痰腮、2線程是并發(fā)執(zhí)行,隱藏坑出現(xiàn)如下的執(zhí)行序列(每個(gè)線程有自己獨(dú)立寄存器)
如果按照正常邏輯來(lái)看i的值應(yīng)該為1律罢。但是通過(guò)上面的執(zhí)行順序最后的結(jié)果是0膀值,所以出現(xiàn)了問(wèn)題棍丐。實(shí)際上可能會(huì)是0或者1或者2。i的最終值取決于最終是從哪個(gè)線程中賦值的沧踏,也就是最終是由哪個(gè)線程的寄存器回寫(xiě)到內(nèi)存中的歌逢。
同步與鎖
所謂同步就是指在一個(gè)線程訪問(wèn)數(shù)據(jù)未結(jié)束的時(shí)候其他線程不能對(duì)同一數(shù)據(jù)訪問(wèn)。最常見(jiàn)的同步方式就是使用鎖翘狱,分為加鎖和解鎖過(guò)程秘案,在加鎖之后其他線程需要等待解鎖之后才能訪問(wèn)資源。
其次還有二元信號(hào)量潦匈,也即是占有和非占有兩個(gè)狀態(tài)阱高,如果允許對(duì)個(gè)線程并發(fā)訪問(wèn)資源,多遠(yuǎn)信號(hào)量簡(jiǎn)稱信號(hào)量茬缩。給定一個(gè)初始值為N的信號(hào)量則允許N個(gè)線程并發(fā)訪問(wèn)赤惊。具體來(lái)講:
- 信號(hào)量值減去1
- 如果信號(hào)量值小于0則進(jìn)入等待狀態(tài),否則繼續(xù)執(zhí)行寒屯。訪問(wèn)完資源之后荐捻,線程釋放信號(hào)量
- 將信號(hào)量加1
- 如果信號(hào)量的值大于1,則喚醒一個(gè)等待中的線程
互斥量和二元信號(hào)量類似寡夹,資源同一時(shí)刻只能一個(gè)線程訪問(wèn)处面,和信號(hào)量不同的是,信號(hào)量在整個(gè)系統(tǒng)可以被任意線程獲取并釋放菩掏,而互斥量只能在哪個(gè)線程獲取的魂角,也就只能在獲取的那個(gè)線程釋放,跨線程釋放是無(wú)效的智绸∫熬荆——任意線程
臨界區(qū)是比互斥量更加嚴(yán)格的同步方式,臨界區(qū)與信號(hào)量大而區(qū)別在于瞧栗,互斥量和幸好量在系統(tǒng)的任何進(jìn)程里都是可見(jiàn)的斯稳,也即是一個(gè)進(jìn)程創(chuàng)建了互斥量或信號(hào)量,其他進(jìn)程視圖去獲取該鎖是合法的迹恐,臨界區(qū)作用范圍僅僅限于本進(jìn)程挣惰,其他進(jìn)程無(wú)法獲取該鎖,除此之外臨界區(qū)和互斥量具有相同的性質(zhì)殴边≡髅——任意進(jìn)程
條件變量同樣是一個(gè)同步的手段,作用類似于柵欄锤岸。線程對(duì)條件變量有兩種操作方式竖幔,一種是條件變量可以被多個(gè)線程等待;另一種是線程可以喚醒條件變量是偷,此時(shí)所有等待此條件變量的線程都會(huì)被喚醒拳氢。也就是條件變量可以讓許多線程一起等待某個(gè)事件的發(fā)生募逞。當(dāng)事件發(fā)生時(shí)(條件變量被喚醒),線程繼續(xù)執(zhí)行——柵欄馋评、多個(gè)線程
函數(shù)被重入表示這個(gè)函數(shù)沒(méi)有執(zhí)行完成凡辱,又進(jìn)入 了該函數(shù)的執(zhí)行。在多線程同時(shí)執(zhí)行這個(gè)函數(shù)或者函數(shù)自身調(diào)用自己就會(huì)出現(xiàn)重入現(xiàn)象栗恩。如果函數(shù)被重入之后不會(huì)產(chǎn)生任何不良后果函數(shù)被稱為具有可重入性。
比如:
類似這樣的函數(shù)洪燥,沒(méi)有使用任何局部靜態(tài)或全局的變量磕秤、沒(méi)有依賴調(diào)用方提供的參數(shù)、不調(diào)用任何不可重入的函數(shù)捧韵,是線程安全的市咆。
在開(kāi)發(fā)中,有時(shí)候即使使用了鎖也不一定能保證線程安全再来。這是因?yàn)槁浜蟮木幾g技術(shù)無(wú)法滿足增長(zhǎng)的并發(fā)需求蒙兰。這樣會(huì)導(dǎo)致很多看似不合理的情況!
比如:
從代碼上來(lái)講有了lock和unlock的包含芒篷,x++的是原子性的搜变,那么值似乎必然是2,但是如果編譯器為了提高x的訪問(wèn)速度针炉,把x放到某個(gè)寄存器里挠他,我們知道不同線程的寄存器是獨(dú)立的,因此Thread1先獲得鎖篡帕,則程序執(zhí)行可能會(huì)出現(xiàn)如下情況
可見(jiàn)這樣并不能達(dá)到線程安全殖侵,原因就是在R1++之后沒(méi)有立即回寫(xiě)到內(nèi)存中,而是緩存在寄存器中镰烧,過(guò)了一段時(shí)間才回寫(xiě)到內(nèi)存中拢军。**
另一個(gè)例子
邏輯上講r1、r2至少有一個(gè)為1怔鳖,不可能同時(shí)為0茉唉。但是這種同時(shí)為0的情況確實(shí)存在。因?yàn)镃PU有動(dòng)態(tài)調(diào)度的特性败砂,在執(zhí)行程序的時(shí)候?yàn)榱颂岣咝视锌赡芙粨Q指令的順序赌渣,同樣編譯器在優(yōu)化的時(shí)候也可能為了效率而交換毫不相干的相鄰指令如x=1、r1=y的執(zhí)行順序昌犹。
上面的代碼可能就變成了:
這個(gè)時(shí)候就可能出現(xiàn)r1=r2=0坚芜,關(guān)鍵詞volatile(C語(yǔ)言中)可以阻止這種過(guò)度優(yōu)化。它可以實(shí)現(xiàn)兩件事:
- 阻止編譯器為了提高速度將一個(gè)變量緩存在寄存器內(nèi)不回寫(xiě)到內(nèi)存中斜姥。
- 阻止編譯器調(diào)整操作volatile變量的指令順序鸿竖。
CPU動(dòng)態(tài)調(diào)度換序
CPU動(dòng)態(tài)調(diào)用換序也算是過(guò)度優(yōu)化的范疇沧竟。
源代碼:
從邏輯上講是沒(méi)有問(wèn)題的,函數(shù)返回時(shí)缚忧,Pinst總是指向一個(gè)有效的對(duì)象悟泵,同時(shí)也加了鎖。其實(shí)有問(wèn)題闪水,主要是因?yàn)镃PU亂序執(zhí)行糕非,C++中的new包含了兩個(gè)步驟
- 分配內(nèi)存
- 調(diào)用構(gòu)造函數(shù)
那么Pinst = new T其實(shí)包含了三個(gè)步驟
- 分配內(nèi)存
- 調(diào)用構(gòu)造函數(shù)
- 將內(nèi)存地址復(fù)制給PInst
其中2和3順序可以顛倒。產(chǎn)生的問(wèn)題就是這個(gè)時(shí)候PInst雖然不是NULL球榆,但還沒(méi)構(gòu)造完成朽肥,這個(gè)時(shí)候如果另一個(gè)線程滴啊用了GetInstance,那么就會(huì)直接返回PInst持钉,但是這個(gè)地址是并沒(méi)有構(gòu)造完全的對(duì)象地址衡招。如果后面使用了這個(gè)沒(méi)有被構(gòu)造完成的毒性地址,那么會(huì)發(fā)生異趁壳浚現(xiàn)象
所以需要阻止CPU換序始腾,但是并不存在這樣的方法。而是通過(guò)一條 指令解決空执,指令叫做barrier浪箭,barrier會(huì)阻止CPU將之前的指令交換到barrier之后,相當(dāng)于一個(gè)柵欄辨绊。
改進(jìn)后的代碼如下
通過(guò)barrier保證了對(duì)象構(gòu)造一定是在barrier之前完成的山林,那么PInst被復(fù)制的時(shí)候?qū)ο笫峭暾摹?/p>
這種情況什么時(shí)候會(huì)出現(xiàn),目前我也沒(méi)遇到過(guò)類似的場(chǎng)景邢羔。
多線程內(nèi)部
這里涉及到內(nèi)核線程與用戶線驼抹,一些輕量級(jí)的線程,對(duì)用戶而言如果有三個(gè)線程同時(shí)執(zhí)行拜鹤,對(duì)內(nèi)核而言可能就只有一個(gè)線程框冀。
一對(duì)一的線程模型最為簡(jiǎn)單,一個(gè)用戶線程對(duì)應(yīng)一個(gè)內(nèi)核線程(但是內(nèi)核態(tài)的線程不一定在用戶太有對(duì)應(yīng)的線程)敏簿。這樣方式實(shí)現(xiàn)了真真的并發(fā)明也,一個(gè)線程阻塞不會(huì)應(yīng)影響其他線程。但是也有缺點(diǎn):
- 許多操作系統(tǒng)限制了內(nèi)核線程的數(shù)量惯裕,一次一對(duì)一的線程會(huì)讓用戶線程受到限制
- 許多操作系統(tǒng)內(nèi)核線程調(diào)度時(shí)温数,上下文切換開(kāi)銷比較大,導(dǎo)致用戶線程的執(zhí)行效率下降
除了一對(duì)一還有多對(duì)一的線程模型
多對(duì)一將多個(gè)用戶線程映射到一個(gè)內(nèi)核線程上蜻势,線程之間由用戶態(tài)的代碼來(lái)驚醒撑刺,因此對(duì)弈一對(duì)一的線程模型而言,多對(duì)一在線程切換的時(shí)候就快很多握玛。多對(duì)一的問(wèn)題:
- 最大問(wèn)題就是一旦其中一個(gè)用戶線程被阻塞了够傍,那么所有的線程都無(wú)法繼續(xù)執(zhí)行甫菠,因此內(nèi)核里面的線程也被阻塞;最大的好處就是線程的上下文切換和無(wú)限制的線程數(shù)量
多對(duì)多的線程模型結(jié)合了一對(duì)一冕屯,多對(duì)一的特點(diǎn)寂诱。將多個(gè)用戶線程映射到不知一個(gè)內(nèi)核線程上。
一個(gè)線程阻塞并不會(huì)影響其他線程安聘,而且也沒(méi)有用戶線程數(shù)量的限制痰洒。
小結(jié)
第一篇就到這里了,這一節(jié)主要是復(fù)習(xí)操作系統(tǒng)層面的知識(shí)浴韭。其中比較難(現(xiàn)在也沒(méi)弄明白)就是CPU的動(dòng)態(tài)調(diào)度带迟。不知道有沒(méi)有大咖懂的。
擴(kuò)展閱讀
下面這幾篇都講得比較深入囱桨,比如關(guān)于虛擬地址晦溪、進(jìn)程益缠、線程等买决∏垂牛可以看一看腐宋!
Linux虛擬地址空間布局
Linux 中的各種棧:進(jìn)程棧 線程棧 內(nèi)核棧 中斷棧
Linux下Fork與Exec使用
Linux虛擬地址空間布局以及進(jìn)程棧和線程椇斗ぃ總結(jié)