??作者:一只大喵咪1201
??專欄:《Linux學(xué)習(xí)》
??格言:你只管努力,剩下的交給時間!
?? 頁表詳解
我們在之前一直都提到頁表,知道它的作用是將虛擬地址映射到物理地址,但是它具體怎么映射的,它的結(jié)構(gòu)是什么樣的,并沒有提及過。
char* str = "hello world";
*str = 'H';
上訴代碼,會在運行時報錯,原因是str指向的地址在字符常量區(qū),字符常量區(qū)的內(nèi)容是不允許用戶去修改的。
代碼在運行起來以后,操作系統(tǒng)是怎么知道用戶在修改字符常量區(qū)的呢?
如上圖所示的頁表示意圖,頁表中不僅右虛擬地址和物理地址的映射關(guān)系,還有是否命中,RWX權(quán)限,U/K權(quán)限等等內(nèi)容。
- U/K權(quán)限:U表示用戶(user),K表示內(nèi)核(kernal)。
- RWX權(quán)限:當前身份(用戶或者內(nèi)核)對當前地址的讀,寫執(zhí)行權(quán)限。
- 上面代碼在對srt指向的地址寫內(nèi)容時,先會經(jīng)過頁表映射到物理地址。
- 但是在頁表中發(fā)現(xiàn)這是一個寫操作,并且該地址是不允許被寫的,此時MMU就發(fā)送信號,導(dǎo)致程序報錯。
- 虛擬地址,物理地址以及屬性所在的一行,稱為條目。
仍然是這張圖,需要將這張圖分解進行講解。
物理內(nèi)存空間劃分:
以32位系統(tǒng)為例,它的物理內(nèi)存理論上有4GB大小,但是這4GB又被分成了多塊小空間。
- 被分割的小空間,每一塊大小是4KB,被叫做頁框。
物理內(nèi)存中會又很多個頁框,并且這些頁框也需要操作系統(tǒng)管理起來,采用的方式同樣是先描述,再組織。
通過一個結(jié)構(gòu)體來描述頁框
struct_Page
{
//內(nèi)存屬性--4KB
}
代碼形式如上所示,每一個頁框都會有這樣一個結(jié)構(gòu)體對象,將多個結(jié)構(gòu)體對象放在一個數(shù)組中:
struct_Page mem[];
此時就完成了先描述再組織的過程。操作系統(tǒng)中有專門的內(nèi)存管理算法,叫做伙伴系統(tǒng),有興趣的小伙伴可以自行查閱資料了解。
可執(zhí)行文件:
我們寫好的代碼會經(jīng)過編譯器的處理形成二進制可執(zhí)行文件放在磁盤中,在運行的時候加載到內(nèi)存中。
- 編譯器在處理源文件生成的二進制可執(zhí)行文件,同樣是以4KB為單位的,這4KB的數(shù)據(jù)塊被叫做頁楨。
這一切都是設(shè)計好的,所以可執(zhí)行程序在加載到內(nèi)存中的時候是以4KB為單位的,正好一個頁幀來填充一個頁框。當頁框被填充了以后,就會創(chuàng)建對應(yīng)的struct_Page結(jié)構(gòu)體對象,并且放在數(shù)組中,讓操作系統(tǒng)來管理。
??頁目錄和頁表項
再回到頁表,我們知道,每個進程對應(yīng)的虛擬地址空間大小都是4GB的,也就是有232個地址,如果每個虛擬地址在頁表中都對應(yīng)著一個物理地址:
那么頁表就會有232行,每一行都是一個條目,每個條目中不僅有物理地址,虛擬地址,還有其他屬性,假設(shè)一個條目的大小是10B,那么光頁表就有10*232=40GB,已經(jīng)超過了物理內(nèi)存的大小,所以頁表肯定不是這樣的。
- 實際上,頁表是由頁目錄和頁表項組成的。
在32位機器上,地址的大小是4G字節(jié),也就是有32個比特位:
隨便寫了一個地址,如上圖所示,一個32個比特位。
- 將32個比特位分為10個比特位,10個比特位,12個比特位,共3組。
- 之前的的頁表 = 頁目錄 + 頁表項,如上圖所示。
- 32個比特位的高10位,作為頁目錄的下標,如上圖所示的0000 0000,通過這個下標0可以訪問到頁目錄中的第一個條目。
- 頁目錄中存放的是頁表項的地址,可以通過下標找到對應(yīng)的頁表項。
10個比特位,意味著頁目錄的下標范圍是0~1023,最多是210也就是1KB個條目,大大減少了對內(nèi)存的消耗。
- 32個比特位的中間10位,作為頁表項的下標,同樣可以訪問頁表項中的條目。
- 頁表項中存放的是物理內(nèi)存中頁框的起始地址,可以通過下標找到物理內(nèi)存中對應(yīng)的頁框。
同樣,一個頁表項最多有1KB個條目,指向1KB個頁框。
- 32個比特位中的低12位,作為偏移量,在物理內(nèi)存中頁框的起始地址基礎(chǔ)上進行偏移,此時就可以得到具體數(shù)據(jù)在內(nèi)存中的地址。
- 這也是為什么頁框和頁幀的大小設(shè)置為4KB的原因,因為最低的12個比特位是212=4KB,偏移量最大就是4KB。
- 32位虛擬地址->物理地址的映射過程:
- 根據(jù)高10位下標找到頁目錄中對應(yīng)頁表項的地址
- 再根據(jù)中間10位下標找到頁表相中對應(yīng)頁框的物理地址
- 再根據(jù)低12位偏移量進行偏移找到具體的物理地址。
頁目錄和頁表項同樣是采用先描述再組織的方式被操作系統(tǒng)管理起來的,每創(chuàng)建一個進程就會有一個頁目錄,只有在目錄中存在的頁表項才會被建立。
采用這種方式,大大減少了對內(nèi)存的消耗。
如何看到地址空間和頁表:
- 地址空間是進程看到的資源窗口,每個進程都認為自己有4GB的資源。
- 頁表決定進程真正擁有的資源。
- 合理對地址空間和頁表進行資源劃分,就可以對一個進程所有的資源進行分類。
??線程
- 線程:是進程中的一個執(zhí)行流。
先將概念拋出,那么這句話到底是什么意思呢?
回憶一下,之前我們對進程的定義是:內(nèi)核數(shù)據(jù)結(jié)構(gòu) + 進程對應(yīng)的代碼和數(shù)據(jù)。
如上圖所示,此時我們創(chuàng)建了多個“子進程”。
- 新創(chuàng)建的“子進程”中的mm_struct* mm都指向父進程的虛擬地址空間。
- 也就是說,所有“子進程”和父進程共用一塊虛擬地址空間。
和父進程共用一塊虛擬地址空間的“子進程”,就叫做線程。
此時開始,我們就將帶引號的"子進程",叫做線程。
- 線程的作用:執(zhí)行進程中的一部分代碼。
線程在執(zhí)行進程的部分代碼時,有點像父子進程執(zhí)行同一份代碼中不同部分,區(qū)別在于線程和父進程使用的是同一份虛擬地址空間,而父子進程使用的是兩個獨立的虛擬地址空間。
??輕量級進程
從圖中可以看到,每個線程都也有一個task_struct結(jié)構(gòu)體對象,用來描述線程的屬性(id,狀態(tài),優(yōu)先級,上下文,棧等等)。那么線程要不要被操作系系統(tǒng)管理起來呢?
答案是要的,而且采用的方式同樣是先描述再組織,描述線程的task_struct結(jié)構(gòu)體被叫做TCB–線程控制塊,是英文Thread Contral Block的首字母。
描述好了以后同樣像PCB一樣,需要用鏈表組織起來進行管理,并也和PCB一樣,有自己的管理算法。
- 但是,TCB中的屬性和PCB幾乎一樣,管理TCB的數(shù)據(jù)結(jié)構(gòu)和算法也和PCB的一樣。
此時不僅會導(dǎo)致代碼上的冗余,而且還會增加系統(tǒng)的開銷,所以Linux并不是使用TCB管理線程的,因為這種方式比較復(fù)雜,維護起來不方便,而且運行也不是很穩(wěn)定。
- Linux中,線程是直接復(fù)用PCB的數(shù)據(jù)結(jié)構(gòu)和管理方法。
- 所以在Liux操作系統(tǒng)中,進程和線程的描述結(jié)構(gòu)體都是task_struct。
- 站在CPU的角度,它只關(guān)注task_struct。
CPU是一個被動的硬件,給它什么它就執(zhí)行什么,所以它并不會區(qū)分當前執(zhí)行的task_struct是一個進程還是一個線程,在它看來,都是進程。
之前VS現(xiàn)在:
- 之前:CPU執(zhí)行的task_struct是一個進程
- 現(xiàn)在:CPU執(zhí)行的task_struct是一個執(zhí)行流
所以說,今天CPU處理的task_struct <= 之前task_struct的含義。
- 站在內(nèi)核的角度,稱今天學(xué)習(xí)的task_struct為輕量級進程。
我們可以通過虛擬地址空間 + 頁表的方式對進程進行資源劃分,讓不同的“輕量級進程”同時執(zhí)行不同部分的代碼,所以單個“輕量級進程”的執(zhí)行粒度,一定要比之前的進程細。
- Linux內(nèi)核中并沒有線程的概念,線程是用進程PCB來模擬的。
由于Linux中,線程也是使用的PCB結(jié)構(gòu),是一種輕量化的進程,所以在Linux內(nèi)核中并不存在線程的概念,也不存在線程的結(jié)構(gòu)。
- 站在CPU的角度,每一個PCB都被稱為輕量級進程。
CPU并不關(guān)注它處理的task_struct是屬于進程還是線程,在它看來,都是進程。
- Linux線程是CPU調(diào)度的基本單位。
CPU每次都是調(diào)度一個task_struct結(jié)構(gòu)體,而這些PCB都是輕量級進程,有可能是屬于進程,也有可能是屬于線程,即使是屬于進程,也可以看作是一個線程,因為無論是進程還是線程,都是一個個的執(zhí)行流,CPU每次調(diào)度的都是一個執(zhí)行流。
- 進程的重新定義:進程是承擔分配系統(tǒng)資源的基本實體。
每創(chuàng)建一個進程,都會創(chuàng)建一個PCB,一個虛擬地址空間,一個頁表,一塊物理空間,而線程是屬于這個進程中的執(zhí)行流,它使用的是這個進程的資源。
所以此時的進程就包括因為創(chuàng)建它而產(chǎn)生的一系列開銷(PCB,虛擬地址空間,頁表,物理空間),這些都是屬于這個進程的。
- 當這個進程中的某個線程申請新資源的時候,也是以該進程的名義去申請,而不是也這個線程的名義。
講到這里,是不是覺得和之前學(xué)習(xí)的進程概念有沖突了?其實是自洽的。
- 之前我們學(xué)習(xí)的進程,每個進程只有一個執(zhí)行流。
- 而現(xiàn)在每個進程中有多個執(zhí)行流,每個執(zhí)行流都是一個線程。
一個進程內(nèi)可以有多個執(zhí)行流,這些執(zhí)行流都共用一個虛擬地址,一個頁表。
- 最初的進程執(zhí)行流被叫做主線程。
- 之后創(chuàng)建的執(zhí)行流被叫做新線程。
主線程和新線程都屬于一個進程,都是一體的,就像一個家庭中,有不同的成員,他們的工作是不同的,但是目的都是一樣的–為了這個家好。
同樣,多個線程同時工作的目的也是相同的–為了完成這個進程的任務(wù)。
??線程引入Linux
通過上面介紹,我們知道,在Linux內(nèi)核中是不存在線程這一個概念的,因為沒有TCB數(shù)據(jù)結(jié)構(gòu)以及管理算法,而我們所說的線程,都是在宏觀層面,代指所有操作系統(tǒng)。
- Linux操作系統(tǒng)中也沒有提供創(chuàng)建線程的系統(tǒng)調(diào)用。
- 無論是宏觀操作系統(tǒng),還是用戶(程序員)都只認線程的概念,但是Linux內(nèi)核中并沒有線程的概念。
我們(程序員)在編程的時候,仍然會使用線程的概念,那么我們在創(chuàng)建線程的時候,Linux內(nèi)核中是怎么創(chuàng)建出輕量級進程的呢?
- 我們在創(chuàng)建進程的時候,會調(diào)用一個線程庫,庫中再通過一些系統(tǒng)調(diào)用創(chuàng)建出輕量級進程。
這樣一來,程序員創(chuàng)建線程,Linux中創(chuàng)建輕量級進程,雙方的要求就都滿足了。
這個線程庫是所有Linux操作系統(tǒng)必須自帶的,所以也叫做原生線程庫。
??看見線程
下面我們來看看線程的樣子,創(chuàng)建線程使用到的庫函數(shù)接口是:
- pthread_t* thread:線程標識符tid,是一個輸出型參數(shù)。
- const pthread_attr_t* attr:線程屬性,當前階段一律設(shè)成nullptr。
- void* (*start_routine)(void *):是一個函數(shù)指針,線程執(zhí)行的就是該函數(shù)中的代碼。
- void* arg:是上面函數(shù)指針指向函數(shù)的形參。
- 返回值:線程創(chuàng)建成功返回0。
- 主線程執(zhí)行的任務(wù)是在一個死循環(huán)中,新線程執(zhí)行的任務(wù)也在一個死循環(huán)中。
如果只有一個執(zhí)行流的話,程序會陷入一個死循環(huán)中,另一個死循環(huán)就不會再執(zhí)行,而我們創(chuàng)建新線程就是為了讓新線程和主線程同時執(zhí)行兩個不同的死循環(huán)。
將上訴代碼進行編譯的時候,我們發(fā)現(xiàn)報錯了,編譯器不認識pthread_create函數(shù)。
- 我們創(chuàng)建新線程只能通過原生線程庫去創(chuàng)建,此時編譯器找不到原生線程庫。
- 使用-l選項指定原生線程庫pthread,之前在動態(tài)靜態(tài)庫的時候詳細介紹過如果指定動態(tài)庫。
此時我們再次編譯以后,就能夠正常運行了。
- 新線程和主線程在同時運行,并沒有陷入某一個死循環(huán)中。
??原生線程庫
查看可執(zhí)行程序的鏈接屬性??梢钥吹绞莿討B(tài)鏈接,鏈接的庫是原生線程庫,如上圖綠色框中所示。
根據(jù)線程庫的路徑去查看該路徑下的所有文件,可以看到還有靜態(tài)庫,我們使用的線程庫是一個軟鏈接文件,它所鏈接的庫才是真正的原生線程庫。
在創(chuàng)建新線程的時候,傳遞的最后一個參數(shù)作為新線程執(zhí)行函數(shù)的形參,如上圖所示。
可以看到,新線程中打印的name內(nèi)容正式在主線程中創(chuàng)建新線程時傳過來的字符串。
??LWP和PID
- 主線程和新線程在同時運行,此時存在兩個執(zhí)行流。
但是在查看該進程的時候,發(fā)現(xiàn)mythread進程只有一個,pid,ppid等值也只有一個。
- 這也證明,線程是進程中的一個執(zhí)行流,線程屬于進程的一部分。
- 給mythread進程發(fā)送9號信號,主線程和新線程都結(jié)束了。
- 所有信號針對的都是進程,而線程屬于進程。
- 當一個進程結(jié)束以后,它的所有資源都會被回收,所以線程也就不存在了。
那我們想看到線程該怎么辦呢?
- 使用指令ps -aL來查看線程。L必須大小
- 此時名字為mythread的線程有兩個,它們的PID值相同,LWP不同。
- PID:進程標識符
- LWP:輕量級進程表示符,LWP是英文Light Weight Process的首字母。
- 可以看到,第一個線程的LWP和PID是一樣的,這個線程就被叫做主線程。
- 第二個線程的LWP和PID不一樣,這個線程就被叫做新線程。
那么CPU在調(diào)度PCB的時候,根據(jù)的是LWP呢還是PID呢?
- CPU在調(diào)度PCB的時候是根據(jù)LWP為標識符表示一個特點的執(zhí)行流的。
因為CPU調(diào)度的都是輕量級進程,而每個輕量級進程也就線程的根本區(qū)別就在于LWP不同,但是不同線程的PID卻有可能相同。
- 我們之前學(xué)習(xí)的進程,它只有一個執(zhí)行流,也就是主線程,所以它的PID和LWP是相同的,即PID = LWP,我們使用哪個都無所謂。
- 而現(xiàn)在我們學(xué)習(xí)了線程,就不能再只使用PID了,而是使用LWP。
從這里可以再次看出線程是屬于進程的一部分。
線程和進程的關(guān)系如下圖:
一個框表示一個進程,一條波浪線表示一個線程。
??線程的公有資源和私有資源
公有資源:
所有線程都共享一個虛擬地址空間,一個頁表,所以進程中的絕大部分資源都是所有線程共享的,先來看看共享的情況:
寫了一個函數(shù),分別在主線程和新線程中調(diào)用這個函數(shù)。
可以看到,主線程和新線程都可以調(diào)用這個函數(shù)。
- 該進程中只有一份虛擬地址空間,該函數(shù)放在代碼段中。
- 所有線程共享一個代碼段。
- 創(chuàng)建一個全局變量,在主線程和新線程中都打印,并且使用后置++。
- 再將全局變量的地址在主線程和新線程中打印出來。
- 新線程和主線程看到的全局變量是一個,當任意一個線程改變這個變量的值時,都會影響另一個線程使用這個值。
- 主線程和新線程中,全局變量的地址是相同的,說明它們使用的是同一個全局變量。
根據(jù)上面現(xiàn)象以及分析,可以知道,數(shù)據(jù)段也是被所有線程共享的。
進程中的絕大部分資源都是和所有線程共享的。
私有資源:
因為所有線程都共享一個虛擬地址空間以及頁表,線程之間有私有資源嗎?答案肯定是有的。
- PCB屬性私有
所有線程都有各自的PCB,所以PCB中的屬性肯定是私有的,屬于各自線程。
- 上下文數(shù)據(jù)私有
CPU在調(diào)度PCB的時候,采用輪轉(zhuǎn)時間片的方式,當一個線程被換下時,該線程的上下文一定是私有的,防止被其他線程修改而導(dǎo)致恢復(fù)上下文的時候出現(xiàn)錯誤。
- 棧結(jié)構(gòu)私有
不同線程各自的臨時變量一定是私有的,而臨時變量存放在棧結(jié)構(gòu)中,所有棧也是私有的。
理解是能理解,但是都是同一塊虛擬地址空間,怎么就讓不同線程的棧結(jié)構(gòu)私有了呢?這就涉及到了原生線程庫的實現(xiàn),本喵大致說一下:
系統(tǒng)調(diào)用clone是用來創(chuàng)建子進程的,這里的子進程是輕量級進程,也就是沒有獨立的虛擬地址空間。
- clone中有一個參數(shù),如上圖中綠色框中所示,該參數(shù)就是用來自定這個子進程的棧空間的。
所以我們在使用pthread_create創(chuàng)建新線程的時候,底層會調(diào)用clone,并且會指定屬于該線程的私有棧結(jié)構(gòu)。
??線程的優(yōu)缺點
優(yōu)點:
與線程之間的切換相比,線程之間的切換需要操作系統(tǒng)做的工作要少很多。
- 進程切換:PCB切換 + 上下文切換 + 虛擬地址空間切換 + 頁表切換
- 線程切換:PCB切換 + 上下文切換
可以看到,線程切換比進程切換少了兩項。 除此之外,線程切換時cache不用太更新。
- cache:硬件緩沖區(qū),其實就是我們所說的高速緩存。
- 它存在于CPU中,速度只是比CPU慢一點,但是比內(nèi)存快很多。
- cache會根據(jù)局部性原理從內(nèi)存中拿數(shù)據(jù),尤其是使用頻率高的熱點數(shù)據(jù)會一直放在cache中。
- CPU在使用數(shù)據(jù)時,不是直接去內(nèi)存中拿,而是先去cache中拿,如果不命中,也就是不存在,cache就會將CPU所需要的數(shù)據(jù)從內(nèi)存中緩存到cache中。
- 進程間切換:不僅上面提到的四項內(nèi)容需要切換,而且cache中的內(nèi)容也需要重新緩存。
- 線程間卻換:切換PCB和上下文,但是cache中緩存的數(shù)據(jù)不需要切換。
所以線程都共用一個虛擬地址空間和一個頁表,而cache中的內(nèi)容也是根據(jù)虛擬地址和頁表緩存進來的,所以不同進程之間是可以共用的。
這樣一來,大大節(jié)省了cache從內(nèi)存中緩存數(shù)據(jù)的時間,并且也節(jié)省了操作系統(tǒng)的大量工作。
當然還有很多其他的優(yōu)點,比如:
- 創(chuàng)建一個新線程的代價要比創(chuàng)建一個新進程小得多,因為線程不會創(chuàng)建新的虛擬地址空間和頁表。
- 線程占用的資源要比進程少很多。
- 能充分利用多處理器的可并行數(shù)量。
- 在等待慢速I/O操作結(jié)束的同時,程序可執(zhí)行其他的計算任務(wù)。
- 計算密集型應(yīng)用,為了能在多處理器系統(tǒng)上運行,將計算分解到多個線程中實現(xiàn)。
- I/O密集型應(yīng)用,為了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。
- 計算密集型應(yīng)用:主要體現(xiàn)在CPU的高頻工作,如加密,解密,算法等。
- I/O密集型應(yīng)用:主要體現(xiàn)在和外設(shè)的交互上,如訪問磁盤,顯示器,網(wǎng)卡等。
上面很多線程的優(yōu)點,進程也是擁有的。
缺點:
健壯性或者魯棒性較差:
- 在新線程中,對空指針指向的地址進行寫入。
- 我們知道,這個操作在運行時肯定會出錯。
- 新線程中發(fā)送端錯誤異常,收到了11號信號SIGSEGV。
- 但是不僅新線程結(jié)束了,主線程也結(jié)束了。
線程是進程的執(zhí)行分支,線程出異常,就類似進程出異常,進而觸發(fā)信號機制,終止進程,進程終止,該進程內(nèi)的所有線程也就隨即退出。而多進程就不存在,一個進程的退出并不會影響另一個進程。
除此之外,線程還有一些其他的缺點:
- 性能損失
一個很少被外部事件阻塞的計算密集型線程往往無法與其它線程共享同一個處理器。如果計算密集型線程的數(shù)量比可用的處理器多,那么可能會有較大的性能損失,這里的性能損失指的是增加了額外的同步和調(diào)度開銷(線程切換),而可用的資源不變。
- 缺乏訪問控制
進程是訪問控制的基本粒度,在一個線程中調(diào)用某些OS函數(shù)會對整個進程造成影響。文章來源:http://www.zghlxwxcb.cn/news/detail-454450.html
- 編程難度提高
- 一般情況下,CPU有幾個核就創(chuàng)建幾個線程。
- 核:只的是一個CPU中的運算器個數(shù),即使是多核,而控制器也是一個CPU只有一個。
??總結(jié)
在這篇文章中,一定要明白線程是什么,它和進程的區(qū)別。并且要知道線程是站在宏觀操作系統(tǒng)而言的概念,而具體到Linux操作系統(tǒng)中是沒有線程這一個概念的,也沒有線程對應(yīng)的數(shù)據(jù)結(jié)構(gòu)和系統(tǒng)調(diào)用。概念上的線程和內(nèi)核中的輕量級進程是通過線程庫建立的聯(lián)系。文章來源地址http://www.zghlxwxcb.cn/news/detail-454450.html
到了這里,關(guān)于【Linux學(xué)習(xí)】多線程——頁表詳解 | 線程概念 | 線程理解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!