国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【Linux】多線程 --- 線程概念 控制 封裝

這篇具有很好參考價值的文章主要介紹了【Linux】多線程 --- 線程概念 控制 封裝。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

從前種種,譬如昨日死。從后種種,往如今日生。

【Linux】多線程 --- 線程概念 控制 封裝



一、線程概念

1.重新理解用戶級頁表

1.1 進程資源如何進行分配呢?(地址空間+頁表)

1.
首先我們來看一個現(xiàn)象,當只有第一行代碼時,編譯是能通過的,但會報warning,當加了第二行代碼時,編譯無法通過,報error。
第一行代碼能編過的原因是權限縮小,雖然ptr是可讀可寫的權限,但在指向常量字符串"hello world"之后,ptr的權限就變?yōu)榱酥蛔x,所以如果僅僅修改一下權限,g++并不會報錯,只是報個warning罷了,但當解引用ptr,將ptr指向的內(nèi)容修改為"H"字符串后,編譯器就會報錯了,因為我們說ptr的權限是只讀,因為常量字符串是不可修改的,你現(xiàn)在進行了ptr指向內(nèi)容的修改,編譯器則一定會報錯。

【Linux】多線程 --- 線程概念 控制 封裝
2.
上面的那段解釋其實是語言級別的,那憑什么ptr指向內(nèi)容一修改,g++就會報錯呢?進程就會退出呢?誰告訴進程的啊?又是誰終止進程的呢?想要解釋這些問題,語言層面是無法做到的,只有在系統(tǒng)層面才能解釋。
實際上,頁表的結構并非我們所想的那樣簡單,除了進行虛擬地址到物理地址的轉(zhuǎn)換之外,他還會記錄對應虛擬地址映射到物理地址時的權限,例如讀/寫/執(zhí)行權限,內(nèi)核/用戶權限,還包括虛擬地址是否有效命中到對應的物理地址上,等等信息都是頁表進行存儲的。
所以在解引用ptr修改其指向內(nèi)容時,底層就是ptr這個虛擬地址會經(jīng)過頁表映射,然后轉(zhuǎn)換到對應物理內(nèi)存上將ptr指向內(nèi)容進行修改,而在用戶級頁表轉(zhuǎn)換的時候,MMU發(fā)現(xiàn)ptr這個虛擬地址對應的權限是R權限,那就是只讀不能被修改,此時進程如果執(zhí)意要進行修改,那就會導致硬件MMU直接報錯,操作系統(tǒng)知曉MMU報錯后,就會給對應的進程發(fā)11號信號(Segmentation fault),當進程在合適的時候就會去處理這個信號,處理信號的默認動作就是終止當前進程!

【Linux】多線程 --- 線程概念 控制 封裝
3.
所以我們該如何理解用戶級頁表和進程地址空間呢?
從功能角度來談,進程地址空間就是進程能夠看到的資源的窗口,因為進程所占用的系統(tǒng)資源都是分配在物理內(nèi)存上的,想要訪問這些系統(tǒng)資源都需要地址空間來作為中間件去訪問。
頁表真正決定了進程實際擁有資源的情況,進程對某個資源具有什么權限?訪問此資源需要的進程級別?一個不屬于當前進程的虛擬地址,進程能否通過這個地址訪問對應物理內(nèi)存上的資源呢?這些問題都需要依靠頁表來解決!所以進程對資源的真正掌握情況是通過頁表來實現(xiàn)的!
那該如何對進程的資源進行劃分呢?合理的對地址空間+頁表進行資源劃分,我們就可以對進程的所有資源進行分類!

1.2 虛擬地址如何轉(zhuǎn)換到物理地址?(頁目錄+頁表項)

1.
我們知道頁表的作用就是幫助硬件MMU來進行虛擬地址到物理地址的轉(zhuǎn)換,如果按照我們原來理解的頁表進行推斷的話,一個地址空間有2^32次方個地址,頁表的每一個條目會將虛擬地址轉(zhuǎn)換為物理地址,假設頁表條目什么都不放,只放虛擬地址,那所有條目加起來占用的內(nèi)存就是16GB空間大小,這還僅僅是一個進程的用戶級頁表,如果一個用戶級頁表都占16GB的空間,隨便幾個進程一起跑,需要的內(nèi)存已經(jīng)非常多了,這可能嗎?當然不可能!所以實際頁表的結構并沒有以前我們所理解的那樣簡單!

【Linux】多線程 --- 線程概念 控制 封裝
2.
物理內(nèi)存也是硬件,操作系統(tǒng)既然是軟硬件資源的管理者,那操作系統(tǒng)要不要對物理內(nèi)存進行管理呢?當然要!怎么管理呢?先描述,再組織!操作系統(tǒng)在管理物理內(nèi)存時,將物理內(nèi)存劃分成了一個個大小為4KB的頁框,并為每個頁框創(chuàng)建內(nèi)核數(shù)據(jù)結構struct Page{};,并用類似于struct Page mem[ ];數(shù)組這樣的方式將每個struct Page{};結構體管理起來,而我們編寫好的程序,在編譯之后實際頁會被劃分為一個個大小為4KB的頁幀,程序加載到內(nèi)存的過程,其實就是頁幀內(nèi)容加載到頁框的過程。
加載之后,內(nèi)核此時就會創(chuàng)建對應的PCB,地址空間等一套內(nèi)核數(shù)據(jù)結構,并做好虛擬地址空間到物理內(nèi)存之間的映射關系,當然內(nèi)核不會提前把所有的虛擬到物理之間的映射工作做好,部分的映射關系可能還需要進程在啟動的時候動態(tài)的完成剩余部分的映射工作。
然后CPU調(diào)度進程的PCB,開始執(zhí)行代碼的時候,就會進行虛擬地址到物理地址之間的轉(zhuǎn)換,通過頁表來完成這個工作。虛擬地址會被劃分為10 10 12三個部分,第一個部分對應的是頁目錄,因為只有10位,所以頁目錄只需要1024個條目,每個條目對應一個虛擬地址的高10位,每個條目中又會存儲對應頁表項的地址,這個頁表項是虛擬地址的中間10位所對應的,所以也會有1024個頁表項存在,每個頁表項的地址會放到頁目錄里面,然后頁表項的每個條目又會存儲物理內(nèi)存中每個頁框的起始物理地址,虛擬地址的低12位負責干什么工作呢?他其實就是虛擬地址對應的物理頁框內(nèi)的物理地址的偏移量,即通過虛擬地址的高20位能夠確定對應的物理頁框位置,最后再通過虛擬地址的低12位進行對應物理頁框的起始地址的偏移,最終確定好虛擬地址對應的物理地址的真實位置所在?。摽虼笮?KB正好匹配虛擬地址低12位的所有排列組合,12位的排列組合最大數(shù)字正好是4096,4KB不也是4096byte的大小嗎?所以偏移之后的位置也一定在指定頁框內(nèi)部。)

【Linux】多線程 --- 線程概念 控制 封裝
3.
所以,進程在真正訪問物理內(nèi)存時,有的頁表項根本就不會用到,操作系統(tǒng)也就不會把1024個頁表項全部創(chuàng)建出來,而是進程用到哪些頁表項才會創(chuàng)建哪些頁表項,這樣就可以解決多個進程運行時連頁表都存儲不下的內(nèi)存不足的問題了,按需創(chuàng)建,而不是一股腦把所有頁表項全部創(chuàng)建出來!

4.
雖然內(nèi)存是按照一個個的字節(jié)來劃分的,但實際在訪問內(nèi)存時,是按照頁框的大小來進行訪問的,編譯器同樣也會將程序劃分為4KB大小的頁幀。
如果有老鐵想要了解內(nèi)核數(shù)據(jù)結構struct Page{}結構體,以及操作系統(tǒng)管理內(nèi)存的算法:伙伴系統(tǒng)算法,可以自己在網(wǎng)上搜一下。
其實上面這種虛擬地址到物理地址轉(zhuǎn)換的方法,遵循了x86架構尋址的一種特點:基地址+偏移量

2.Linux的輕量級進程(linux沒有線程的概念)

2.1 線程概念的引出 和 進程概念的重構

1.
線程的概念就是進程內(nèi)部的一個執(zhí)行流,這句話放到哪個操作系統(tǒng)上都沒有錯,因為這是一個宏觀層面上的概念,但正因為OS太宏觀了,進而導致概念很抽象,想要具體理解某一個概念必須落到具體的操作系統(tǒng)上,我們今天所談的多線程,只談linux這一款操作系統(tǒng)的具體實現(xiàn),不同平臺的多線程實現(xiàn)策略是不一樣的。(下面所談到的任何話題都是專屬于linux的?。?br> 先拋出一個概念,線程在進程內(nèi)運行,線程在進程的地址空間內(nèi)運行,擁有該進程的一部分資源。這句話一說可能老鐵們直接蒙蔽,線程就線程嘛,怎么還在進程里面運行呢?還在地址空間內(nèi)運行?而且擁有進程的一部分資源,這都是什么鬼?
如何看待線程在地址空間內(nèi)運行呢?實際進程就像一個封閉的屋子,線程就是在屋子里面的人,而地址空間就是一個個的窗戶,屋子外面就是進程對應的代碼和數(shù)據(jù),一個屋子里面當然可以有多個人,而且每個人都可以挑選一個窗戶看看外面的世界。

2.
在上面的例子中,每個人挑選一個窗戶實際就是將進程的資源分配給進程內(nèi)部的多個執(zhí)行流,以前fork創(chuàng)建子進程的時候,不就是將父進程的一部分代碼塊兒交給子進程運行嗎?子進程不就是一個執(zhí)行流嗎?
而今天我們所談到的線程道理也是類似,我們可以將進程的資源劃分給不同的線程,讓線程來執(zhí)行某些代碼塊兒,而線程就是進程內(nèi)部的一個執(zhí)行流。那么此時我們就可以通過地址空間+頁表的方式將進程的資源劃分給每一個線程,那么線程的執(zhí)行粒度一定比之前的進程更細!

3.
那我們在思考一下,如果linux在內(nèi)核中真的創(chuàng)建出了我們上面所談論到的線程,那么linux就一定要管理內(nèi)核中的這些線程,既然是管理,那就需要先描述,再組織,創(chuàng)建出真正的TCB結構體來描述線程,線程被創(chuàng)建的目的不就是被執(zhí)行,被CPU調(diào)度嗎?既然所有的線程都要被調(diào)度,那每個線程都應該有自己獨立的thread_id,獨立的上下文,狀態(tài),優(yōu)先級,獨立的棧(線程執(zhí)行進程中的某一個代碼塊兒)等等,那么大家不覺得熟悉嗎?單純從CPU調(diào)度的角度來看,線程和進程有太多重疊的地方了!
所以linux工程師心一橫,我們就不創(chuàng)建什么線程TCB結構體了,直接復用進程的PCB當作線程的描述結構體,用PCB來當作Linux系統(tǒng)內(nèi)部的"線程"。這么做的好處是什么呢?如果要創(chuàng)建真正的線程結構體,那就需要對其進行維護,需要和進程構建好關系,每個線程還需要和地址空間進行關聯(lián),CPU調(diào)度進程和調(diào)度線程還不一樣,操作系統(tǒng)要對內(nèi)核中大量的進程和線程做管理,這樣維護的成本太高了!不利于系統(tǒng)的穩(wěn)定性和健壯性,所以直接復用PCB是一個很好的選擇,維護起來的成本很低,因為直接復用原來的數(shù)據(jù)結構就可以實現(xiàn)線程。所以這也是linux系統(tǒng)既穩(wěn)定又高效,成為世界上各大互聯(lián)網(wǎng)公司服務器系統(tǒng)選擇的原因。(而windows系統(tǒng)內(nèi)是真正有對應的TCB結構體的,他確實創(chuàng)建出了真正的線程,所以維護起來的成本就會很高,這也是windows用的用的就卡起來,或者藍屏的原因,因為不好維護啊,實現(xiàn)的結構太復雜!代碼健壯性不高)

【Linux】多線程 --- 線程概念 控制 封裝

4.
在知道linux的線程實現(xiàn)方案之后,我們又該如何理解線程這個概念呢?現(xiàn)在PCB都已經(jīng)不表示進程了,而是代表線程。以前我們所學的進程概念是:進程的內(nèi)核數(shù)據(jù)結構+進程對應的代碼和數(shù)據(jù),但今天站在內(nèi)核視角來看,進程的概念實際可以被重構為:承擔分配系統(tǒng)資源的基本實體!進程分配了哪些系統(tǒng)資源呢?PCB+虛存+頁表+物存。所以進程到底是什么呢?其實就是紅色方框圈起來的部分,這些就是進程!
那在linux中什么是線程呢?線程是CPU調(diào)度的基本單位,也就是struct task_struct{},PCB就是線程,為進程中的執(zhí)行流!
那我們以前學習的進程概念是否和今天學習的進程概念沖突了呢?當然沒有,以前的進程也是承擔分配系統(tǒng)資源的基本實體,只不過原來的進程內(nèi)部只有一個PCB,也就是只有一個執(zhí)行流,而今天我們所學的進程內(nèi)部是有多個執(zhí)行流,多個PCB!

【Linux】多線程 --- 線程概念 控制 封裝

5.
Linux內(nèi)核中有沒有真正意義上的線程呢?沒有,linux用進程的PCB來模擬線程,是完全屬于自己實現(xiàn)的一套方案!
站在CPU的角度來看,每一個PCB,都可以稱之為輕量級進程,因為它只需要PCB即可,而進程承擔分配的資源更多,量級更重!
Linux線程是CPU調(diào)度的基本單位,進程是承擔分配系統(tǒng)資源的基本實體!
進程用來整體向操作系統(tǒng)申請資源,線程負責向進程伸手要資源。如果線程向操作系統(tǒng)申請資源,實質(zhì)上也是進程在向操作系統(tǒng)要資源,因為線程在進程內(nèi)部運行,是進程內(nèi)部的一部分!
linux內(nèi)核中雖然沒有真正意義上的線程,但雖無進程之名,卻有進程之實!
程序員(用戶)只認線程,但linux沒有線程只有輕量級進程,所以linux無法直接提供創(chuàng)建線程的系統(tǒng)調(diào)用接口,只能提供創(chuàng)建輕量級進程的接口!
用pcb模擬線程的好處是維護成本大大降低,系統(tǒng)變得更加可靠、高效、穩(wěn)定。windows操作系統(tǒng)是給老百姓用的,可用性必須要高。linux是給程序用的,必須要可靠穩(wěn)定高效。所以由于需求的不同,產(chǎn)生了不同實現(xiàn)方案的操作系統(tǒng)。

為了方便大家理解線程,下面在舉一個例子,讓大家對線程印象深刻一點。
社會分配資源時,例如房子,汽車,土地等等,都是以家庭作為基本單位的,當然一個家庭中肯定會有不同的成員,每個成員都干著不同的事情,你的父母要工作,你要上學,你的爺爺奶奶要養(yǎng)老,你的弟弟妹妹也要上學。但所有成員其實都是在共同完成一件事情,那就是讓這個家庭變得越來越好,爭取得到更為優(yōu)質(zhì)的資源,讓生活變得更美好。在上面的例子中,社會其實就是操作系統(tǒng),家庭就是進程,家庭中的每個成員就是線程。雖然每個線程做的事情是不同的,但他們其實都是為了完成同一個任務,例如一個線程在下載視頻,另一個線程在播放視頻,他們其實都是在完成下載視頻這個任務,只不過是邊下邊播罷了。

2.2 證明創(chuàng)建線程其實就是創(chuàng)建輕量級進程

1.
但怎么證明呢?你說linux中沒有線程只有輕量級進程,他就真的只有輕量級進程??!你是誰?憑什么這么說?沒有事實依據(jù)的只能稱為猜測,只有有依據(jù),他才能成為事實。
下面我們通過代碼來驗證一下。

2.
在談創(chuàng)建線程之前,我們先來回顧一下程序使用第三方動靜態(tài)庫時,編譯鏈接需要注意哪些問題。
我在這里直接說結論,具體驗證時的現(xiàn)象可以看我的另一篇文章。
如果我們使用第三方庫,并且這個第三方庫沒有安裝到系統(tǒng)里面,那么如果程序使用的是靜態(tài)庫,在編譯時需要指明頭文件的路徑,因為include包含了頭文件,但編譯器會找不到這個頭文件,需要增加-I(大寫的i)選項,指定頭文件的路徑,包含頭文件之后,程序內(nèi)部又會調(diào)用靜態(tài)庫中的實現(xiàn)方法的代碼,然后在鏈接時,鏈接器會找不到對應的靜態(tài)庫文件,也就是實現(xiàn)方法的代碼所在的文件,所以在編譯時還需要增加-L選項,指定鏈接器需要鏈接的庫文件的路徑,又由于一個路徑下可能存在多個庫文件,所以還需要增加一個-l(小寫的l)選項,指定程序要鏈接的具體的庫文件的名稱,庫文件的名稱需要去掉前綴lib和后綴.so或.a。增加這些選項之后,程序才能正常的編譯鏈接,成功運行。
如果程序使用的是動態(tài)庫,除上面所說的增加3個選項之外,還需要一些其他的工作。因為動態(tài)庫不是直接將代碼拷貝到程序中的,而是在程序運行起來的時候動態(tài)鏈接的,但當程序運行起來的時候,和編譯器就沒關系了,而是和操作系統(tǒng)與bash(我的是centos7.6)有關,所以如果你只添加那三個選項,當程序運行的時候,OS和shell會找不到動態(tài)庫文件,通常的解決方案有:將動態(tài)庫路徑添加到環(huán)境變量里,或者在/etc/ld.so.conf.d/目錄下增加配置文件,并手動調(diào)用ldconfig更新一下,或者在系統(tǒng)路徑或者當前路徑下,建立動態(tài)庫文件的軟鏈接,或者將動態(tài)庫文件路徑拷貝到系統(tǒng)路徑下,相當于安裝動態(tài)庫到系統(tǒng)路徑。大概的解決方案就是上面這四種。
gcc默認的動態(tài)鏈接只是一個建議選項,而究竟是動態(tài)鏈接還是靜態(tài)鏈接,取決于提供的庫是動態(tài)庫還是靜態(tài)庫。如果只提供動態(tài)庫,你沒帶選項,那正好就是動態(tài)鏈接。但如果編譯帶上-static選項,此時編譯鏈接是不成功的,會發(fā)生報錯,無法進行編譯鏈接!如果只提供靜態(tài)庫,你沒帶選項,那gcc也只能靜態(tài)鏈接。當然如果你帶上-static選項,那是更標準的做法。如果動靜態(tài)庫都給gcc,此時你編譯帶-static選項,那就是靜態(tài)鏈接。如果你沒帶,那就是動態(tài)鏈接。

基礎IO — 軟硬鏈接、acm時間、動靜態(tài)庫制作、動靜態(tài)鏈接、動靜態(tài)庫加載原理…

3.
pthread_create是創(chuàng)建線程的一個接口,具體使用細節(jié)看圖。線程屬性不需要管,我們也不清楚需要給線程設置什么屬性,所以傳nullptr即可。

【Linux】多線程 --- 線程概念 控制 封裝

【Linux】多線程 --- 線程概念 控制 封裝
4.
如果在編譯時不帶-lpthread選項,可以看到g++報錯pthread_create()函數(shù)未定義,其實就是因為鏈接器鏈接不上具體的動態(tài)庫,此時就可以看出來linux內(nèi)核中并沒有真正意義的線程,他無法提供創(chuàng)建線程的接口,而只能通過第三方庫libpthread.so或libpthread.a來提供創(chuàng)建線程的接口。

【Linux】多線程 --- 線程概念 控制 封裝
通過ldd選項就可以看到程序鏈接時,都鏈接了哪些動態(tài)庫,其中軟鏈接鏈接的庫就是我們的原生線程庫libpthread-2.17.so

【Linux】多線程 --- 線程概念 控制 封裝

5.
linux為了讓用戶能夠得到他想要的線程,只能通過原生線程庫來給用戶他想要的,所以在用戶和內(nèi)核之間有一個軟件層,這個軟件層負責給程序員創(chuàng)建出程序員想要的線程。除這個原生線程庫會創(chuàng)建出線程結構體外,但同時linux內(nèi)核中會通過一個叫clone的系統(tǒng)調(diào)用來對應的創(chuàng)建出一個輕量級進程,所以我們稱這個庫是用戶級線程庫,因為linux是沒有真正意義上的線程的,無法給用戶創(chuàng)建線程,只能創(chuàng)建對應的PCB,也就是輕量級進程!

【Linux】多線程 --- 線程概念 控制 封裝

2.3 線程的屬性(含面試題)

1.
下面是我們使用pthread_create創(chuàng)建線程的代碼,代碼很簡單,看起來比較多是因為我寫的注釋比較多,實際代碼很少。

【Linux】多線程 --- 線程概念 控制 封裝

2.
通過ps -aL就可以看到正在運行的線程有哪些,可以看到有兩個標識符,一個是PID,一個是LWP(light weight process),所以CPU在調(diào)度那么多的PCB時,其實是以LWP作為每個PCB的標識符,以此來區(qū)分進程中的多個輕量級進程。
主線程的PID和LWP是相同的,所以從CPU調(diào)度的角度來看,如果進程內(nèi)只有一個執(zhí)行流,那么LWP和PID標識符對于CPU來說都是等價的,但當進程內(nèi)有多個執(zhí)行流時,CPU是以LWP作為標識符來調(diào)度線程,而不是以PID來進行調(diào)度。

【Linux】多線程 --- 線程概念 控制 封裝

操作系統(tǒng)管理輕量級進程,其實是通過鏈表來進行管理的。
【Linux】多線程 --- 線程概念 控制 封裝

3.
前面說的LWP標識符是為了給CPU區(qū)分多個PCB搞出來的一種類似id的數(shù)字,而pthread_create第一個參數(shù)tid是真正的線程id,我們下意識的可能以為這個值就應該是LWP標識符的值,但實際上這個值背后隱藏著很多的知識內(nèi)容,當我們將這個tid進行格式化輸出時,我們大概可以猜到他像是一個地址!實際這個tid非常重要,他背后牽扯很多的知識內(nèi)容,但現(xiàn)在還沒到揭曉他是什么的時候,這篇文章的下面部分會具體談論這個tid究竟是什么,這里先埋一個伏筆。

【Linux】多線程 --- 線程概念 控制 封裝
可以通過snprintf將tid值格式化為十六進制的表示形式,存儲到tidbuffer里面,輸出的時候直接輸出tidbuffer指針指向的內(nèi)容即可。
【Linux】多線程 --- 線程概念 控制 封裝

4.
線程一旦被創(chuàng)建,幾乎所有的資源都是共享的!
func()是代碼中獨立的一個函數(shù)體,但主線程和新城可以同時調(diào)用這個func(),并且新線程修改全局變量g_val,主線程也能看到g_val被修改。至于原因其實非常簡單,因為一個進程中的所有線程都共享進程地址空間,地址空間中的棧,堆,已初始化/未初始化數(shù)據(jù)段,代碼段,這些區(qū)域中的資源都是共享的,每個線程都可以看到,那么任意一個線程就都可以去訪問這些資源了!
所以如果線程想要通信,那成本是要比進程間通信低很多的,由于進程具有獨立性,所以進程間通信的前提是讓不同的進程能夠看到同一份資源,看到同一份資源的成本就很大,例如之前我們所學的,通過創(chuàng)建管道或共享內(nèi)存的方式來讓進程先能夠看到同一份資源,然后才能繼續(xù)向下談通信的話題。但是今天,對于線程來說完全不需要考慮看到同一份資源這個問題,因為一個進程內(nèi)的所有線程天然的可以共享進程地址空間,你可以直接定義一個全局緩沖區(qū),一個線程往里寫,另一個線程立馬就可以從緩沖區(qū)中看到另一個線程寫的信息,所以線程通信的成本非常低!

【Linux】多線程 --- 線程概念 控制 封裝

【Linux】多線程 --- 線程概念 控制 封裝

5.
如果你細心一點,可以發(fā)現(xiàn)上面4.中的內(nèi)容,在說共享進程地址空間的段時,我故意沒有說映射段(Memory Mapping Segment),至于原因其實就是線程雖然能共享進程的絕大部分資源,但線程其實也是要有自己自己私有的資源的,映射段中存儲了線程的部分私有資源?。P于映射段,這篇文章的下面會談)
什么資源是線程應該私有的呢?這是一道經(jīng)典的面試題!
a.線程PCB的屬性,例如線程id,線程調(diào)度優(yōu)先級,線程狀態(tài)等等…(這個回答不回答不重要,重要的是回答出下面那兩點)
b.線程在被CPU調(diào)度時,也是需要進行切換的,所以,線程的上下文結構也必須是線程的私有資源。(這點可以體現(xiàn)出我們知道線程是動態(tài)的,CPU調(diào)度線程會輪換,線程會被切換上來也會被切換下去)
c.每個線程都會執(zhí)行自己的線程函數(shù),就是那個start_routine函數(shù)指針所指向的函數(shù),所以每個線程都有自己的私有棧結構。

【Linux】多線程 --- 線程概念 控制 封裝

上面的第三點其實隱藏了一些問題,我們知道進程地址空間中只有一個棧區(qū)啊,每個線程都有自己的私有棧結構,但表示棧頂和棧底的寄存器只有兩個啊,那怎么給每個線程維護其私有棧結構呢?這個話題以及映射段以及揭曉線程id都放到文章下面的同一個部分去講。

【Linux】多線程 --- 線程概念 控制 封裝

6.下面在念一些概念,稍微過一過即可
【Linux】多線程 --- 線程概念 控制 封裝

2.4 線程的優(yōu)點和缺點(線程切換更輕量化,多線程代碼健壯性較差)

1.
線程的優(yōu)點如下,其中第二點比較重要,需要單獨拿出來再談一下,其余的幾個優(yōu)點理解難度比較低,自己看一下就好。

【Linux】多線程 --- 線程概念 控制 封裝
2.
進程切換操作系統(tǒng)要保存進程的上下文結構,那線程切換操作系統(tǒng)也要保存線程的上下文結構啊,你憑什么說線程切換需要操作系統(tǒng)做的工作要少很多呢?
進程切換:要切換用戶級頁表,還要切換虛擬地址空間,要切換PCB,要切換進程的上下文結構
線程切換:要切換PCB,要切換線程的上下文結構
從需要切換的內(nèi)容來看,進程切換的代價沒比線程高多少嘛,切換個頁表,那其實就是切換一下存儲頁表地址的寄存器的內(nèi)容就OK了,切換地址空間,那就切換一下PCB,新PCB立馬指向新的地址空間,這也沒做太多工作???怎么回事呢?
實際線程切換更為輕量化的原因是和CPU的硬件級別的Cache有關!為了提升CPU讀取的效率,當CPU在讀取物理內(nèi)存中的代碼和數(shù)據(jù)時,其實并不是直接從物理內(nèi)存中讀取的,而是先將物理內(nèi)存中的代碼和數(shù)據(jù)加載到CPU中的Cache,然后再將Cache中的數(shù)據(jù)讀取到寄存器里面,CPU最終通過寄存器來開展他的調(diào)度工作。Cache的IO速度要高于內(nèi)存,低于寄存器,Cache中也有各種級別的高速緩存,例如l1 l2 l3級別。程序具有局部性原理,也就是說進程會在某一時刻訪問程序中某一固定部分的代碼,這段代碼中的數(shù)據(jù)我們稱為熱點數(shù)據(jù),進程會高頻的訪問這些熱點數(shù)據(jù),那么在加載Cache的時候,就一定會加載這些熱點數(shù)據(jù),程序中不經(jīng)常被訪問到的數(shù)據(jù)就會暫時擱在一旁,等到需要CPU調(diào)度的時候,再將他們加載到Cache里。所以,當某一個進程穩(wěn)定的在CPU上運行時,CPU中的Cache緩存的都是當前進程訪問的高頻熱點數(shù)據(jù),那如果此時要切換線程,因為線程是進程內(nèi)部的一個執(zhí)行流,所以線程在切換時,Cache里面的大部分數(shù)據(jù)都是不用被更新的,可能只需要更新一部分熱點數(shù)據(jù)即可。但如果此時切換進程,則原先CPU中Cache內(nèi)的所有熱點數(shù)據(jù)全部失效,操作系統(tǒng)需要將新的進程的熱點數(shù)據(jù)加載到Cache里面!此時相比線程切換,操作系統(tǒng)做的工作就多起來了,因為需要更新Cache里面的所有數(shù)據(jù)。一旦重新緩存數(shù)據(jù),CPU就會慢很多了

Cache 是什么?

【Linux】多線程 --- 線程概念 控制 封裝

所以線程切換更為輕量化的原因,主要是放在cache的數(shù)據(jù)更新上了,切換進程會導致cache的數(shù)據(jù)全部失效,操作系統(tǒng)需要更新所有的cache數(shù)據(jù)。
【Linux】多線程 --- 線程概念 控制 封裝

3.
多線程確實有很多的優(yōu)點,但他也有缺點,不過總體來說,線程的優(yōu)點還是要大于他的缺點的。
其中多線程代碼的健壯性降低,可以通過代碼來驗證一下。

【Linux】多線程 --- 線程概念 控制 封裝
4.
首先信號是發(fā)送給進程的,而不是發(fā)送給線程的,子進程崩了肯定不會影響父進程,因為進程之間具有獨立性,但新線程崩了會不會影響主線程呢?驗證的思路也很簡單,我們在新線程中添加訪問空指針指向地址的代碼,看是否會影響主線程的執(zhí)行!從實驗結果可以看到,當新線程執(zhí)行到訪問空指針執(zhí)行的空間時,也就是大概過了1s之后,進程直接就崩了,bash報錯:Segmentation fault,這其實就可以證明新線程崩了,會導致整個進程都崩了,進程中所有的線程(當前代碼只有主線程和新線程,你也可以多創(chuàng)建幾個線程試試)也崩了!
當某個線程崩的時候,操作系統(tǒng)會給進程發(fā)送信號,但進程中可能有多個執(zhí)行流,所以操作系統(tǒng)會給每個PCB都發(fā)送信號,每個PCB中的pending位圖都會收到對應的信號,在進程陷入內(nèi)核時,就會處理該信號,默認的處理動作就是直接終止進程,將進程中所有的執(zhí)行流全部關閉!
所以,我們稱多線程代碼的健壯性或魯棒性較差!

【Linux】多線程 --- 線程概念 控制 封裝

5.
其實還可以通過另一個視角來談論上面多線程代碼的健壯性較差的問題,當線程崩的時候,操作系統(tǒng)會給進程發(fā)送信號,本質(zhì)其實就是操作系統(tǒng)要回收進程的資源了,因為進程是承擔分配系統(tǒng)資源的基本單位,而線程用到的資源又是向進程伸手要的,如果進程占用的資源都被操作系統(tǒng)回收了的話,那線程不就沒有資源了嗎?沒有資源,線程還怎么跑啊?
所以此時就要回收所有的線程,關閉進程中的全部執(zhí)行流!一個線程崩,其他線程都會受到影響!

6.
下面是進程和線程的關系圖,在未學習系統(tǒng)知識之前,我們所寫的代碼其實都是單線程進程的代碼,但實際除單線程進程外,還有其他三種,稍微看看下面的圖就好。

【Linux】多線程 --- 線程概念 控制 封裝

二、線程控制

1.創(chuàng)建一批線程

1.
在談論創(chuàng)建一批線程之前,我們先來拓展的認識一下下面這兩個接口。
clone其實是一個創(chuàng)建linux線程的系統(tǒng)調(diào)用接口,但我們知道在linux中是沒有線程這個概念的,只有輕量級進程這個概念,所以linux中fork創(chuàng)建子進程底層調(diào)用的同樣是clone,而創(chuàng)建輕量級進程的底層系統(tǒng)調(diào)用接口也還是這個clone。因為對于linux來講,創(chuàng)建輕量級進程和創(chuàng)建線程主要區(qū)別其實就在于,創(chuàng)建出來的PCB執(zhí)行流是否要共享地址空間,如果要共享,那linux只需要創(chuàng)建PCB就可以了,這其實就是創(chuàng)建輕量級進程。如果不共享,那就不僅僅需要創(chuàng)建PCB了,還需要創(chuàng)建新的地址空間以及頁表,完成對應的映射工作等等,而這其實就是創(chuàng)建進程。
另外linux還提供了另一個接口vfork,這個進程創(chuàng)建出來的子進程和父進程是共享地址空間的,所以雖然他叫做vfork,但其實他創(chuàng)建出來的就是輕量級進程,也就是linux下的"線程",vfork創(chuàng)建出來的子進程和父進程同樣共享絕大部分資源,也契合線程的其他屬性。

【Linux】多線程 --- 線程概念 控制 封裝
2.
創(chuàng)建一個線程在線程概念部分就做過了,比較簡單沒什么含金量,所以在線程控制這里選擇創(chuàng)建一批線程,來看看多個線程下的進程運行情況。
在線程的錯誤檢查這里,并不會設置全部變量errno,道理也很簡單,線程出錯了,那其實就是進程出錯了,錯誤碼這件事不應該是我線程來搞,這是你進程的事情和我線程有什么關系?所以線程也沒有理由去設置全局變量errno,他的返回值只表示成功或錯誤,具體的返回狀態(tài),其實是要通過pthread_join來獲取的!

【Linux】多線程 --- 線程概念 控制 封裝

3.
創(chuàng)建一批線程也并不困難,我們可以搞一個vector存放創(chuàng)建出來的每個線程的tid,但從打印出來的新線程的編號可以看出來,打印的非常亂,有的編號還沒有顯示,這是為什么呢?(我們主觀認為應該是打印出來0-9編號的線程啊,這怎么打印的這么亂呢?)
其實這里就涉及到線程調(diào)度的話題了,創(chuàng)建出來的多個新線程以及主線程誰先運行,這是不確定的,這完全取決于調(diào)度器,我們事先無法預知哪個線程先運行,所以就有可能出現(xiàn),新線程一直沒有被調(diào)度,主線程一直被調(diào)度的情況,也有可能主線程的for循環(huán)執(zhí)行到i等于6或9或8的時候,新線程又被調(diào)度起來了,此時新線程內(nèi)部就會打印出創(chuàng)建成功的語句。所以打印的結果很亂,這也非常正常,因為哪個線程先被調(diào)度是不確定的!

【Linux】多線程 --- 線程概念 控制 封裝
【Linux】多線程 --- 線程概念 控制 封裝
但如果我們每創(chuàng)建出來一個新線程,我們先讓主線程sleep 1秒,等等新線程,讓新線程先運行一下,然后再繼續(xù)創(chuàng)建線程,這樣的話,打印出來的就和我們主觀想的結果一樣了,打印的就不會亂了。

【Linux】多線程 --- 線程概念 控制 封裝

2.線程的終止和等待(三種終止方式 + pthread_join()的void**retval)

1.
再談完線程的創(chuàng)建之后,那什么時候線程終止呢?所以接下來我們要談論的就是線程終止的話題,線程終止總共有三種方式,分別為return,pthread_exit,pthread_cancel
我們知道線程在創(chuàng)建的時候會執(zhí)行對應的start_routine函數(shù)指針指向的方法,所以最正常的線程終止方式就是等待線程執(zhí)行完對應的方法之后,線程自動就會退出,如果你想要提前終止線程,可以通過最常見的return的方式來實現(xiàn),線程函數(shù)的返回值為void*,一般情況下,如果不關心線程退出的情況,直接return nullptr即可。
和進程終止類似的是,除return這種方式外,原生線程庫還提供了pthread_exit接口來終止線程,接口的使用方式也非常簡單,只要傳遞一個指針即可,同樣如果你不關心線程的退出結果,那么也只需要傳遞nullptr即可。

【Linux】多線程 --- 線程概念 控制 封裝

【Linux】多線程 --- 線程概念 控制 封裝

2.
談完上面兩種線程終止的話題后,第三種終止方式我們先等會兒再說,與進程類似,進程退出之后要被等待,也就是回收進程的資源,否則會出現(xiàn)僵尸進程,僵尸的這種狀態(tài)可以通過ps指令+axj選項看到,同時會產(chǎn)生內(nèi)存泄露的問題。
線程終止同樣也需要被等待,但線程這里沒有僵尸線程這樣的概念,如果不等待線程同樣也會造成資源泄露,也就是PCB資源未被回收,線程退出的狀態(tài)我們是無法看到的,我們只能看到進程的Z狀態(tài)。
原生線程庫給我們提供了對應的等待線程的接口,其中join的第二個參數(shù)是一個輸出型參數(shù),在join的內(nèi)部會拿到線程函數(shù)的返回值,然后將返回值的內(nèi)容寫到這個輸出型參數(shù)指向的變量里面,也就是寫到我們用戶定義的ret指針變量里,通過這樣的方式來拿到線程函數(shù)的返回值。
通過bash的打印結果就可以看到,每個線程都正常的等待成功了。

【Linux】多線程 --- 線程概念 控制 封裝

3.
有些人可能覺得join的第二個參數(shù)不太好理解,所以這里在細說一下這個部分,以前如果我們想拿到一個函數(shù)中的多個返回值,但由于函數(shù)的返回值只能有一個,所以為了拿到多個返回值,我們都是在調(diào)用函數(shù)之前,定義出想要拿到的返回值的類型的變量,然后把這個變量的地址傳給需要調(diào)用的函數(shù),這樣的函數(shù)參數(shù)我們稱為輸出型參數(shù),然后在函數(shù)內(nèi)部會通過解引用輸出型參數(shù)的方式,將函數(shù)內(nèi)部的某個需要返回給外部的值拷貝到解引用后的參數(shù)里面,那其實就是修改了我們函數(shù)外部定義的變量的值。
這里不好理解的原因其實是因為二級指針,我們想要拿到的線程函數(shù)的返回值是一個指針,不再是一個變量,所以在調(diào)用join的時候,僅僅傳一級指針是不夠的,我們需要傳一級指針變量的地址,讓join內(nèi)部能解引用一級指針變量的地址,拿到外面的一級指針內(nèi)容并對其修改。

【Linux】多線程 --- 線程概念 控制 封裝
4.
我們可以做一個測試,我們將一個數(shù)字100強制轉(zhuǎn)為void*類型的指針并返回,那么pthread_join的第二個參數(shù)就應該能拿到這個返回值,所以在調(diào)用join之后,將ret指針的值強轉(zhuǎn)成long long的8字節(jié)整型,然后看看是否join能拿到線程函數(shù)的返回值。通過bash的打印結果就可以看到,確實join能依靠他的第二個參數(shù)獲取到線程函數(shù)的退出狀態(tài)。

【Linux】多線程 --- 線程概念 控制 封裝
上面的測試較為簡單,我們其實還可以讓線程函數(shù)返回一個結構體指針,看看join能否拿到結構體指針呢?通過bash的輸出結果可以看到,ThreadReturn類型的指針ret也可以拿到線程函數(shù)的返回值,線程函數(shù)的返回值也是一個ThreadReturn類型的指針,我們拿到了ret指向的exit_code和exit_result的值

下面的代碼有內(nèi)存泄露,在等待成功之后,要記得delete ret指針,否則ThreadRetuen結構體不會被回收!
【Linux】多線程 --- 線程概念 控制 封裝

5.
在了解join拿到線程函數(shù)的返回值之后,我們再來談最后一個線程終止的方式pthread_cancel,叫做線程取消。首先線程要被取消,前提一定得是這個線程是跑起來的,跑起來的過程中,我們可以選擇取消這個線程,換個說法就是中斷這個線程的運行。
如果新線程是被別的線程取消的話,則新線程的返回值是一個宏PTHREAD_CANCELED,這個宏其實就是把-1強轉(zhuǎn)成指針類型了,所以如果我們join被取消的線程,那join到的返回值就應該是-1,如果線程是正常運行結束退出的話,默認的返回值是0.
我們讓創(chuàng)建出來的每個新線程跑10s,然后在第5s的時候,主線程取消前5個線程,那么這5個線程就會被中斷,主線程阻塞式的join就會提前等待到這5個被取消的線程,并打印出線程函數(shù)的返回值,發(fā)現(xiàn)結果就是-1,再經(jīng)過5s之后,其余的5個線程會正常的退出,主線程的join會相應的等待到這5個線程,并打印出默認為0的退出結果。

【Linux】多線程 --- 線程概念 控制 封裝

【Linux】多線程 --- 線程概念 控制 封裝

3.初步認識原生線程庫(在linux環(huán)境,C++11線程庫底層封裝了POSIX線程庫)

1.
我們知道C++11也是有自己的線程庫的,C++11的線程庫是C++標準庫的一部分,它提供了一種跨平臺的線程管理接口,可以在不同的操作系統(tǒng)上使用。
在windows平臺,C++11的線程庫是基于Windows線程庫實現(xiàn)的,因此它可以直接調(diào)用Windows線程庫提供的底層線程管理接口。
在linux平臺,C++11的線程庫則需要使用linux提供的POSIX線程庫來實現(xiàn),C++11的線程庫可以使用POSIX庫來實現(xiàn)跨平臺的線程管理。
所以,在Windows平臺上,C++11的線程庫底層封裝了Windows線程庫,而在Linux平臺上,它底層封裝了POSIX線程庫(pthread)。這使得C++11的線程庫可以在不同的操作系統(tǒng)上使用,并且提供了一種跨平臺的線程管理接口。

2.
下面代碼就是C++11形式的線程管理代碼,這段代碼的好處就是它可以跨平臺運行,無論是在linux還是在windows環(huán)境下這段代碼都可以跑,因為C++11的線程庫底層封裝了各個操作系統(tǒng)的線程庫實現(xiàn),這使得我們能夠通過C++11形式的線程管理方式,寫出跨平臺的代碼,這是C++11線程庫的優(yōu)勢。
當然我們前面所寫的線程管理代碼都是用原生的POSIX線程庫寫出來的,并且是在對應的linux環(huán)境下面運行的,所以軟件層次的調(diào)用就會少很多,程序的運行效率就會高很多,這是POSIX原生線程庫的優(yōu)勢。只不過我們用原生線程庫寫出來的代碼無法跨平臺運行,只在linux環(huán)境下能跑。

【Linux】多線程 --- 線程概念 控制 封裝

4.線程的分離(若要進行分離,推薦創(chuàng)建完線程之后立馬設置分離)

1.
上面我們談過了線程終止和等待的話題,我們知道如果不等待線程的話,會造成內(nèi)存泄露的問題產(chǎn)生,所以要通過join的方式來等待線程,如果關注線程的退出狀態(tài),則可以通過join的第二個參數(shù)來拿到對應的線程函數(shù)返回值。
那如果我們根本就不想等待這個線程呢?在進程那里我們可以通過設置SIGCHLD信號處理方式為SIG_IGN的方式來讓操作系統(tǒng)自動幫我們回收子進程運行結束后的資源。或者如果進程不想阻塞式等待的話,也可以通過非阻塞式等待,以輪詢的方式來檢測子進程的狀態(tài),發(fā)現(xiàn)為Z狀態(tài)時,waitpid就會回收子進程的資源了。
但在線程這里是沒有非阻塞式等待這樣的概念的,你要么就阻塞式等待線程,要么就別等待線程!

2.
新創(chuàng)建出來的線程默認狀態(tài)是joinable的,也就是說你必須通過pthread_join去等待線程,否則就會造成內(nèi)存泄露。
但如果我們壓根就不想等待線程,那調(diào)用pthread_join就是一種負擔,這個時候我們就可以通過分離線程的手段,來告訴操作系統(tǒng),現(xiàn)在我這個線程要和進程分離了,我不再共享進程的地址空間了,我也不要進程的任何資源了,我們倆人以后就形同陌路,互不相干了!操作系統(tǒng)你現(xiàn)在就把我回收吧,我已經(jīng)和進程沒有任何關系了!
所以在設置線程為分離狀態(tài)后,操作系統(tǒng)會立即回收線程的所有資源,而不需要等待線程自動退出或者是手動來釋放資源,表示我們現(xiàn)在已經(jīng)不關心這個線程了!
joinable和detach是線程的兩個對立的狀態(tài),一個線程不能既是joinable又是分離的,并且如果線程被設置為detach,那么就不可以用join來等待線程,否則是會報錯的

3.
設置線程分離的接口pthread_detach使用起來比較簡單,這里也就不做介紹了。
設置線程為分離狀態(tài),可以是線程自己設置自己為分離狀態(tài),也可以是其他線程來設置他為分離狀態(tài)。下面代碼中新線程自己設置自己為分離狀態(tài),但實際上沒有sleep(3)這行代碼的話,可以看到運行結果是新線程正常運行,在5s之后join還等待成功了!并且沒有報錯,這是怎么回事?。?br> 其實主要原因還是在于線程調(diào)度,新線程和主線程誰先被調(diào)度運行我們是不確定的,所以就有可能出現(xiàn)新線程還未執(zhí)行pthread_detach設置自己未分離狀態(tài)之前,主線程已經(jīng)執(zhí)行到pthread_join了,已經(jīng)開始阻塞式等待新線程了!也就是說在執(zhí)行join的時候,join是不知道新線程是分離狀態(tài)的,還以為他是joinable的呢,這就會導致join函數(shù)一直阻塞式等待,在新線程退出后,join會等待成功,默認返回碼是0.
如果想要解決這種問題,主要還是得在調(diào)用join之前,讓join知道他等待的線程是分離狀態(tài)的,這樣的話join就會報錯了,所以加上sleep(3)這行代碼就是為了先讓主線程停一下,等調(diào)度器調(diào)度新線程,新線程設置自己為分離狀態(tài)之后,join就知道他等待的線程是分離狀態(tài)的了,此時join函數(shù)就會報錯,Invalid argument。

【Linux】多線程 --- 線程概念 控制 封裝
4.
除上述那種主線程等待幾秒,讓主線程知曉新線程是分離狀態(tài)的這種方法外(這種方法看起來有點挫),更為推薦的一種做法是,在創(chuàng)建新線程之后,立馬就設置新線程為分離狀態(tài),也就是讓其他線程設置新線程為分離狀態(tài),而不是讓他自己設置自己為分離狀態(tài)。
如果是這樣的話,那在新線程運行結束之后,主線程一定是知道他為分離狀態(tài)的,因為創(chuàng)建線程之后的第一步工作就是設置線程為分離狀態(tài),此時如果調(diào)用join進行等待,那就會直接報錯。所以,設置為分離之后,主線程就可以自己干自己的事情了,無須擔心創(chuàng)建出來的線程有內(nèi)存泄露的問題產(chǎn)生!

【Linux】多線程 --- 線程概念 控制 封裝

5.揭示用戶級線程tid究竟是什么?(映射段中線程庫內(nèi)的TCB的起始地址)

1.
我們知道linux中沒有真正意義上的線程,所以需要原生線程庫來提供創(chuàng)建線程的接口,那你當前的進程可能在使用原生線程庫,其他進程有沒有可能也在同時使用呢?那如果其他進程也在使用原生線程庫,原生線程庫中就會存在多個線程,那庫中的多個線程要不要被管理起來呢?當然要!管理就得先描述,再組織,那描述出來的結構體是什么呢?其實就是pthread_create接口中的第二個參數(shù)指向的聯(lián)合體,這個結構體是在庫這一軟件層面所創(chuàng)建的,但其實這只是線程的一小部分屬性,大部分的屬性都在linux的內(nèi)核中。

【Linux】多線程 --- 線程概念 控制 封裝
2.
除線程庫要在用戶層創(chuàng)建一個描述線程的數(shù)據(jù)結構外,實際操作系統(tǒng)還會給用戶層的TCB創(chuàng)建出來對應的輕量級進程內(nèi)核數(shù)據(jù)結構,進行內(nèi)核中輕量級進程的管理。
所以可以認為,線程是POSIX庫中實現(xiàn)了一部分,操作系統(tǒng)中實現(xiàn)了一部分。每當我們創(chuàng)建一個線程時,庫就要幫我們在用戶層創(chuàng)建出對應的線程控制塊TCB,來對庫中的多個線程進行管理,同時操作系統(tǒng)還要在對應的創(chuàng)建出輕量級進程。所以,Linux用戶級線程 : 內(nèi)核輕量級進程 = 1:1。
用戶關心的線程屬性在用戶級線程中,內(nèi)核提供輕量級進程(線程)的調(diào)度
內(nèi)核中創(chuàng)建輕量級進程調(diào)用的接口就是clone,它可以幫助我們創(chuàng)建出linux認為的"線程"。

【Linux】多線程 --- 線程概念 控制 封裝

3.
在知道用戶層線程和內(nèi)核輕量級進程之后,我們來詳細談一下程序是如何使用原生線程庫的。
與靜態(tài)鏈接不同的是,動態(tài)鏈接只會把可執(zhí)行程序需要用到的動態(tài)庫的庫函數(shù)的偏移地址拷貝到可執(zhí)行程序里面,動態(tài)庫中所有庫函數(shù)在動態(tài)鏈接時,都采用的是這種起始地址+偏移量的方式來進行相對編址。
然后CPU在調(diào)度可執(zhí)行程序時,從物理內(nèi)存讀取代碼時,發(fā)現(xiàn)有外部的物理地址(這個外部的物理地址就是動態(tài)鏈接時,鏈接到程序中的庫函數(shù)的偏移地址)此時CPU不會繼續(xù)執(zhí)行我們的代碼,而是轉(zhuǎn)而去加載這個物理地址所對應的動態(tài)庫!
在將磁盤上的動態(tài)庫文件加載到物理內(nèi)存的過程中,操作系統(tǒng)會讀取動態(tài)庫文件的頭部信息,確定好動態(tài)庫的大小,布局等等信息。然后操作系統(tǒng)會為該動態(tài)庫分配一段虛擬地址空間,并將動態(tài)庫文件中的代碼段、數(shù)據(jù)段等信息都映射到該虛擬地址空間中,這個區(qū)域就是夾在棧和堆之間的映射段。在映射工作完成之后,庫中函數(shù)的起始地址就立馬被確定了,通過起始地址+偏移量的方式,就可以在映射段中確定出程序所使用的庫函數(shù)代碼的具體位置,CPU就會讀取并執(zhí)行映射段中庫函數(shù)代碼,這樣動態(tài)庫就會被使用起來了。
在CPU讀取并執(zhí)行pthread_create代碼的時候,就會在映射段中創(chuàng)建每個線程的線程控制塊TCB,每個線程的基本屬性都會作為一個個的字段存放在這個TCB中,例如線程私有棧,而在Linux中,pthread_create底層會調(diào)用clone接口,clone會創(chuàng)建好線程控制塊,其中clone的第二個參數(shù)void *child_stack就是線程的私有棧的屬性。所以線程所使用的棧是在映射段中操作系統(tǒng)給分配的,而主線程所使用的棧是在棧區(qū)上操作系統(tǒng)給分配的。
而映射到映射段中的動態(tài)庫內(nèi)肯定會存在多個線程,所以線程庫會使用一個數(shù)組來管理這些TCB,而每個線程的LWP值其實就是線程對應的TCB結構體的起始地址,而我們之前一直所說的tid其實就是TCB結構體的起始地址,每個TCB都會有自己的起始地址,這樣能夠很好區(qū)分開來每個TCB的地址空間布局。
像之前所使用的join函數(shù)的第一個參數(shù),也就是tid,他就是TCB的起始地址,也就是指向TCB結構體的指針,而線程函數(shù)的返回值實際會寫到TCB結構體中的某一個字段,join函數(shù)需要tid這個地址,實際就會通過這個結構體指針從TCB中拿到表示線程函數(shù)返回值的那個字段的內(nèi)容。然后將其寫到join的第二個參數(shù) void **retval里面。
(由于程序無法直接讀取物理內(nèi)存上的代碼和數(shù)據(jù),所以需要將動態(tài)庫文件的各個段的信息全部映射到虛擬地址空間的映射段上,這樣CPU才能訪問虛擬地址空間上程序的所有代碼,包括代碼中所使用的第三方庫的代碼,因為這些數(shù)據(jù)都會被映射到虛擬地址空間上,操作系統(tǒng)會在加載動態(tài)庫的時候,完成動態(tài)庫到虛擬地址空間上映射段的映射工作)
并且我們現(xiàn)在也能回頭去理解一些東西了,例如為什么叫用戶級線程庫,當然是因為線程庫會被映射到虛擬地址空間的映射段啊,而映射段不就是在用戶空間嗎?線程庫的代碼都是跑在用戶空間的上的,所以線程庫也叫用戶級線程庫

【Linux】多線程 --- 線程概念 控制 封裝

clone的第二個參數(shù)子棧對應的就是線程的私有棧屬性。
【Linux】多線程 --- 線程概念 控制 封裝

動態(tài)庫加載原理

6.線程的局部存儲(介于全局和局部變量之間的,線程特有的一種存儲方案)

1.
接下來我們再來談另外一個話題,叫做線程的局部存儲。
我們定義一個全局變量g_val,然后讓新線程和主線程分別都打印這個全局變量的地址和值,然后新線程不斷++這個g_val的值。

【Linux】多線程 --- 線程概念 控制 封裝
當這個變量是普通的全局變量的時候,新線程修改,主線程同樣也可以看到修改后的變量的值,并且兩個線程打印出來的變量的地址也是一樣的。

【Linux】多線程 --- 線程概念 控制 封裝

當變量被__thread關鍵字修飾過后,該變量變?yōu)榫€程局部存儲的變量,每個線程都會獨立擁有該變量,所以兩個線程打印出來的地址是不一樣的,并且新線程打印的值是以1為單位逐個增加的,而主線程打印的值不會變化。由此可見這個變量確實是線程局部存儲的,每個線程都有自己獨立的這份變量。

【Linux】多線程 --- 線程概念 控制 封裝

2.
但是為什么前后打印出來的地址差別這么大呢?線程局部存儲的變量地址那么一長串,而原來的那個全局變量地址只有那一小串。
原來的地址是已初始化數(shù)據(jù)段的地址,而線程局部存儲之后,該地址變?yōu)橛成涠蔚牡刂?,我們知道地址空間從下到上地址在逐步的增加,變化也會越來越大,映射段和已初始化數(shù)據(jù)段間隔還是比較大的,所以地址的差別同樣也會很大。

3.
線程局部存儲有什么用呢?
如果你給線程定義的局部屬性不想放在堆上,也不想放在棧上,而是想在程序編譯好的時候,天然的就給每個線程獨立的分配私有的變量空間,那么你就可以使用線程局部存儲關鍵字__thread來定義每個線程獨立擁有的變量,設置線程的私有屬性。
這種局部存儲是介于全局變量和局部變量之間的一種線程特有的存儲方案!

三、線程封裝(面向?qū)ο螅?/h2>

1.組件式的封裝出一個線程類(像C++11線程庫那樣去管理線程)

1.
我們并不想暴露出線程創(chuàng)建,終止,等待,分離,獲取線程id等POSIX線程庫的接口,我們也想像C++11那樣通過面向?qū)ο蟮姆绞絹硗妫越酉聛砦覀儗OSIX線程庫的接口做一下封裝,同樣能實現(xiàn)像C++11線程庫那樣去管理我們的線程,這個類就像一個小組件似的,包含對應的.hpp文件就可以使用,使用起來很舒服。

2.
線程類需要的成員變量有執(zhí)行的函數(shù)_func,格式化后的線程名_name,線程獨立的_tid,線程函數(shù)的參數(shù)_args。其中_func我們用包裝器來實現(xiàn),這樣外部在構造線程對象的時候,就可以傳函數(shù)指針,lambda表達式,仿函數(shù)對象等等。
構造函數(shù)的參數(shù)是包裝器類型的可調(diào)用對象,以及線程函數(shù)的void *參數(shù),外加一個線程的編號,用于區(qū)分打印出來的線程。我們可以將線程名進行格式化處理存儲到buffer里面,buffer的內(nèi)容就是成員變量_name的內(nèi)容,線程編號的參數(shù)就會在這個地方用到。然后就可以調(diào)用pthread_create來創(chuàng)建出線程,并做好查錯處理。
但在調(diào)用pthread_create的時候,其實會出問題,因為第三個函數(shù)指針所指向的函數(shù)的返回值是void *,參數(shù)也是void *的。而只要我們將線程函數(shù)start_routine寫到類內(nèi),默認的形參第一個位置會有一個缺省參數(shù)this指針,所以在調(diào)用start_routine的時候,就會產(chǎn)生類型匹配錯誤的問題。我們可以通過將函數(shù)設置為靜態(tài)成員來解決這個問題,因為static修飾的類成員是沒有this指針的,這樣類型就可以匹配了。
但隨之又會引出新的問題,start_routine想要調(diào)用_func的時候,其實是調(diào)不到的,因為_func是非靜態(tài)成員變量,必須得是有this指針的非靜態(tài)成員才能調(diào)用非靜態(tài)成員_func,所以這里也會出現(xiàn)問題。一種解決方案是將_func也搞成靜態(tài)的,雖然這樣可以調(diào)用到_func,但_func就屬于整個類了,那創(chuàng)建出來的所有線程執(zhí)行的方法就都是一樣的了,這樣不太好。另一個解決方案是將start_routine直接搞成友元函數(shù)放到類外面(友元函數(shù)既可以訪問類的非靜態(tài)成員也能訪問類的靜態(tài)成員)這樣他就能訪問_func了,但這樣也不太好,因為友元會破壞類的封裝性。
現(xiàn)在我們回到最本質(zhì)的問題上來,由于start_routine是靜態(tài)成員函數(shù),沒有this指針,無法調(diào)用到_func,那我們就可以搞一個大號的結構體,讓結構體存儲this指針,也就是在調(diào)用pthread_create之前搞出Context類型的結構體,然后把這個結構體指針作為start_routine的參數(shù)傳給start_routine函數(shù),在start_routine間接的通過ctx結構體中的this指針來調(diào)用_func,但因為_func是private的,所以再增加一個Thread類成員函數(shù)run,在run中調(diào)用private修飾的_func可調(diào)用對象,這樣就可以實現(xiàn)線程的創(chuàng)建和運行了。
線程的等待也比較簡單,直接調(diào)用pthread_join即可完成線程的回收工作。析構函數(shù)什么都不用寫,因為編譯器會自動調(diào)用string類的析構函數(shù),所以不會出現(xiàn)內(nèi)存泄露的情況。

補充知識:就算我們寫的是空的析構函數(shù),線程對象銷毀時會調(diào)用這個空的析構函數(shù),編譯器還是會調(diào)用string的析構函數(shù)完成_name對象的內(nèi)存資源的回收的
【Linux】多線程 --- 線程概念 控制 封裝

3.
使用線程的時候,我們可以通過智能指針來使用,構造智能指針的時候,需要調(diào)用線程的構造函數(shù),只要調(diào)用了線程的構造函數(shù),線程就跑起來了,跑完之后,就可以通過智能指針來調(diào)用join函數(shù)完成線程資源的回收了。

【Linux】多線程 --- 線程概念 控制 封裝

下面是代碼運行結果
【Linux】多線程 --- 線程概念 控制 封裝文章來源地址http://www.zghlxwxcb.cn/news/detail-438949.html

到了這里,關于【Linux】多線程 --- 線程概念 控制 封裝的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 【探索Linux】—— 強大的命令行工具 P.19(多線程 | 線程的概念 | 線程控制 | 分離線程)

    【探索Linux】—— 強大的命令行工具 P.19(多線程 | 線程的概念 | 線程控制 | 分離線程)

    在當今信息技術日新月異的時代,多線程編程已經(jīng)成為了日常開發(fā)中不可或缺的一部分。Linux作為一種廣泛應用的操作系統(tǒng),其對多線程編程的支持也相當完善。本文將會介紹關于Linux多線程相關的知識,其中包括了線程的概念、線程控制、線程分離等方面的內(nèi)容。如果你希望

    2024年02月05日
    瀏覽(41)
  • 『Linux』第九講:Linux多線程詳解(一)_ 線程概念 | 線程控制之線程創(chuàng)建 | 虛擬地址到物理地址的轉(zhuǎn)換

    『Linux』第九講:Linux多線程詳解(一)_ 線程概念 | 線程控制之線程創(chuàng)建 | 虛擬地址到物理地址的轉(zhuǎn)換

    「前言」文章是關于Linux多線程方面的知識,講解會比較細,下面開始! 「歸屬專欄」Linux系統(tǒng)編程 「主頁鏈接」個人主頁 「筆者」楓葉先生(fy) 「楓葉先生有點文青病」「每篇一句」? 我與春風皆過客, 你攜秋水攬星河。 ——網(wǎng)絡流行語,詩詞改版 用現(xiàn)在的話來說:我不

    2024年02月04日
    瀏覽(17)
  • 【線程概念和線程控制】

    【線程概念和線程控制】

    教材觀點是這樣的: 線程是一個執(zhí)行分支,執(zhí)行力度比進程更細,調(diào)度的成本更低。 Linux內(nèi)核觀點: 進程是系統(tǒng)分配資源的基本單位,線程是CPU調(diào)度的基本單位。 這兩種說法都是正確的,但是我們究竟該如何理解線程呢? 在一個程序里的一個執(zhí)行路線就叫做線程(thread)。

    2024年02月16日
    瀏覽(19)
  • 智能家居(3)---socket網(wǎng)絡控制線程封裝

    封裝socket網(wǎng)絡線程實現(xiàn)對智能家居中各種燈光的控制 main.Pro(主函數(shù)) inputCommand.h(控制類) socketControl.c(socket)

    2024年02月13日
    瀏覽(21)
  • 智能家居(2)---串口通信(語音識別)控制線程封裝

    封裝語音線程(語音通過串口和主控設備進行交流)實現(xiàn)對智能家居中各種燈光的控制 mainPro.c(主函數(shù)) inputCommand.h(控制類) voiceControl.c(語音)

    2024年02月13日
    瀏覽(33)
  • cpp多線程(二)——對線程的控制和鎖的概念

    cpp多線程(二)——對線程的控制和鎖的概念

    這篇文章是筆者學習cpp多線程操作的第二篇筆記,沒有閱讀過第一篇的讀者可以移步此處: Cpp多線程(一)-CSDN博客 如果讀者發(fā)現(xiàn)我的文章里有問題,歡迎交流哈! ? 一、如何控制線程呢? c++11在std::this_thread名稱空間(顯然,這是一個嵌套在大名稱空間里的小名稱空間)內(nèi)

    2024年01月20日
    瀏覽(15)
  • 【Linux】線程-線程概念

    【Linux】線程-線程概念

    實際上,線程是一個進程內(nèi)部的控制序列,一個程序的一個執(zhí)行線路就是一個線程。 并且一個進程中至少有一個線程,本質(zhì)上,一個進程內(nèi)部如果有多個線程,那么這些線程實際上是指向同一塊地址空間的。而不論進程還是線程,從CPU看來都是一個PCB,只是說線程的PCB要比進

    2023年04月25日
    瀏覽(40)
  • Linux 多線程 ( 多線程概念 )

    Linux 多線程 ( 多線程概念 )

    在一個程序里的一個執(zhí)行路線叫做線程 thread ),更準確的定義為:“線程是一個進程內(nèi)部的控制序列\(zhòng)\\"。 一切進程至少有一個執(zhí)行線程。 線程在進程內(nèi)部運行,本質(zhì)上是在進程地址空間中運行。 在linux系統(tǒng)中,CPU看到的PCB比傳統(tǒng)的進程更加輕量化。 透過進程虛擬地址空間,可

    2024年02月09日
    瀏覽(34)
  • Linux之多線程(上)——Linux下的線程概念

    Linux之多線程(上)——Linux下的線程概念

    本文介紹了地址空間和二級頁表、Linux下的線程、線程的優(yōu)缺點以及線程與進程的關系等概念。 地址空間是進程能看到的資源窗口 :一個進程可以看到代碼區(qū)、堆棧區(qū)、共享區(qū)、內(nèi)核區(qū)等,大部分的資源是在地址空間上看到的。 頁表決定進程真正有用資源的情況 :進程認為

    2024年02月09日
    瀏覽(24)
  • 【關于Linux中----多線程(二)線程終止、分離與封裝、線程庫以及線程id的解析】

    【關于Linux中----多線程(二)線程終止、分離與封裝、線程庫以及線程id的解析】

    上一篇文章中已經(jīng)講述了兩種終止線程的方式,這里介紹第三種方式: 這里對上篇文章中的代碼稍作修改: 運行結果如下: 根據(jù)記過可知,線程如果是被取消的,它的退出碼是-1. 這里需要注意, 線程可以被取消的前提是該線程已經(jīng)運行起來了 。 上一篇文章中以及這篇文章

    2023年04月08日
    瀏覽(21)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包