關(guān)于進(jìn)程和線程,大家總是說(shuō)的一句話是“進(jìn)程是操作系統(tǒng)分配資源的最小單元,線程是操作系統(tǒng)調(diào)度的最小單元”。這句話理論上沒(méi)問(wèn)題,我們來(lái)看看什么是所謂的“資源”呢。
什么是計(jì)算機(jī)資源
經(jīng)典的馮諾依曼結(jié)構(gòu)把計(jì)算機(jī)系統(tǒng)抽象成 CPU + 存儲(chǔ)器 + IO,那么計(jì)算機(jī)資源無(wú)非就兩種:
1. 計(jì)算資源
2. 存儲(chǔ)資源
CPU是計(jì)算單元,單純從CPU的角度來(lái)說(shuō)它是一個(gè)黑盒,它只對(duì)輸入的指令和數(shù)據(jù)進(jìn)行計(jì)算,然后輸出結(jié)果,它不負(fù)責(zé)管理計(jì)算哪些”指令和數(shù)據(jù)“。 換句話說(shuō)CPU只提供了計(jì)算能力,但是不負(fù)責(zé)分配計(jì)算資源。
計(jì)算資源是操作系統(tǒng)來(lái)分配的,也就是常說(shuō)的操作系統(tǒng)的調(diào)度模塊,由操作系統(tǒng)按照一定的規(guī)則來(lái)分配什么時(shí)候由誰(shuí)來(lái)獲得CPU的計(jì)算資源,比如分時(shí)間片
存儲(chǔ)資源就是內(nèi)存,磁盤這些存儲(chǔ)設(shè)備的資源。在這篇計(jì)算機(jī)底層知識(shí)拾遺(一)理解虛擬內(nèi)存機(jī)制 我們說(shuō)了操作系統(tǒng)使用了虛擬內(nèi)存機(jī)制來(lái)管理存儲(chǔ)器,從緩存原理的角度來(lái)說(shuō),把內(nèi)存作為磁盤的緩存。進(jìn)程是面向磁盤的,為什么這么說(shuō)呢,進(jìn)程表示一個(gè)運(yùn)行的程序,程序的代碼段,數(shù)據(jù)段這些都是存放在磁盤中的,在運(yùn)行時(shí)加載到內(nèi)存中。所以虛擬內(nèi)存面向的是磁盤,虛擬頁(yè)是對(duì)磁盤文件的分配,然后被緩存到物理內(nèi)存的物理頁(yè)中。
所以存儲(chǔ)資源是操作系統(tǒng)由虛擬內(nèi)存機(jī)制來(lái)管理和分配的。進(jìn)程應(yīng)該是操作系統(tǒng)分配存儲(chǔ)資源的最小單元。
再來(lái)看看線程,理論上說(shuō)Linux內(nèi)核是沒(méi)有線程這個(gè)概念的,只有內(nèi)核調(diào)度實(shí)體(Kernal Scheduling Entry, KSE)這個(gè)概念。Linux的線程本質(zhì)上是一種輕量級(jí)的進(jìn)程,是通過(guò)clone系統(tǒng)調(diào)用來(lái)創(chuàng)建的。何謂“輕量級(jí)”會(huì)在后面細(xì)說(shuō)。進(jìn)程是一種KSE,線程也是一種KSE。所以“線程是操作系統(tǒng)調(diào)度的最小單元”這句話沒(méi)問(wèn)題。
什么是進(jìn)程
進(jìn)程是對(duì)計(jì)算機(jī)的一種抽象,
1. 進(jìn)程表示一個(gè)邏輯控制流,就是一種計(jì)算過(guò)程,它造成一個(gè)假象,好像這個(gè)進(jìn)程一直在獨(dú)占CPU資源
2. 進(jìn)程擁有一個(gè)獨(dú)立的虛擬內(nèi)存地址空間,它造成一個(gè)假象,好像這個(gè)進(jìn)程一致在獨(dú)占存儲(chǔ)器資源
這張圖是進(jìn)程的虛擬內(nèi)存地址空間的分配模型圖,可以看到進(jìn)程的虛擬內(nèi)存地址空間分為用戶空間和內(nèi)核空間。用戶空間從低端地址往高端地址發(fā)展,內(nèi)核空間從高端地址往低端地址發(fā)展。用戶空間存放著這個(gè)進(jìn)程的代碼段和數(shù)據(jù)段,以及運(yùn)行時(shí)的堆和用戶棧。堆是從低端地址往高端地址發(fā)展,棧是從高端地址往低端地址發(fā)展。
內(nèi)核空間存放著內(nèi)核的代碼和數(shù)據(jù),以及內(nèi)核為這個(gè)進(jìn)程創(chuàng)建的相關(guān)數(shù)據(jù)結(jié)構(gòu),比如頁(yè)表數(shù)據(jù)結(jié)構(gòu),task數(shù)據(jù)結(jié)構(gòu),area區(qū)域數(shù)據(jù)結(jié)構(gòu)等等。
從文件IO的角度來(lái)說(shuō),Linux把一切IO都抽象成了文件,比如普通文件IO,網(wǎng)絡(luò)IO,統(tǒng)統(tǒng)都是文件,利用open系統(tǒng)調(diào)用返回一個(gè)整數(shù)作為文件描述符file descriptor,進(jìn)程可以利用file descriptor作為參數(shù)在任何系統(tǒng)調(diào)用中表示那個(gè)打開(kāi)的文件。內(nèi)核為進(jìn)程維護(hù)了一個(gè)文件描述符表來(lái)保持進(jìn)程所有獲得的file descriptor。
每調(diào)用一次open系統(tǒng)調(diào)用內(nèi)核會(huì)創(chuàng)建一個(gè)打開(kāi)文件open file的數(shù)據(jù)結(jié)構(gòu)來(lái)表示這個(gè)打開(kāi)的文件,記錄了該文件目前讀取的位置等信息。打開(kāi)文件又唯一了一個(gè)指針指向文件系統(tǒng)中該文件的inode結(jié)構(gòu)。inode記錄了該文件的文件名,路徑,訪問(wèn)權(quán)限等元數(shù)據(jù)。
操作操作系統(tǒng)用了3個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)為每個(gè)進(jìn)程管理它打開(kāi)的文件資源
fork系統(tǒng)調(diào)用
操作系統(tǒng)利用fork系統(tǒng)調(diào)用來(lái)創(chuàng)建一個(gè)子進(jìn)程。fork所創(chuàng)建的子進(jìn)程會(huì)復(fù)制父進(jìn)程的虛擬地址空間。
要理解“復(fù)制”和“共享”的區(qū)別,復(fù)制的意思是會(huì)真正在物理內(nèi)存復(fù)制一份內(nèi)容,會(huì)真正消耗新的物理內(nèi)存。共享的意思是使用指針指向同一個(gè)地址,不會(huì)真正的消耗物理內(nèi)存。理解這兩個(gè)概念的區(qū)別很重要,這是進(jìn)程和線程的根本區(qū)別之一。
那么有人問(wèn)了如果我父進(jìn)程占了1G的物理內(nèi)存,那么fork會(huì)再使用1G的物理內(nèi)存來(lái)復(fù)制嗎,相當(dāng)于一下用了2G的物理內(nèi)存?
答案是早期的操作系統(tǒng)的確是這么干的,但是這樣性能也太差了,所以現(xiàn)代操作系統(tǒng)使用了 寫時(shí)復(fù)制Copy on write的方式來(lái)優(yōu)化fork的性能,fork剛創(chuàng)建的子進(jìn)程采用了共享的方式,只用指針指向了父進(jìn)程的物理資源。當(dāng)子進(jìn)程真正要對(duì)某些物理資源寫操作時(shí),才會(huì)真正的復(fù)制一塊物理資源來(lái)供子進(jìn)程使用。這樣就極大的優(yōu)化了fork的性能,并且從邏輯來(lái)說(shuō)子進(jìn)程的確是擁有了獨(dú)立的虛擬內(nèi)存空間。
fork不只是復(fù)制了頁(yè)表結(jié)構(gòu),還復(fù)制了父進(jìn)程的文件描述符表,信號(hào)控制表,進(jìn)程信息,寄存器資源等等。它是一個(gè)較為深入的復(fù)制。
從邏輯控制流的角度來(lái)說(shuō),fork創(chuàng)建的子進(jìn)程開(kāi)始執(zhí)行的位置是fork函數(shù)返回的位置。這點(diǎn)和線程是不一樣的,我們知道Java中的Thread需要寫run方法,線程開(kāi)始后會(huì)從run方法開(kāi)始執(zhí)行。
既然我們知道了內(nèi)核為進(jìn)程維護(hù)了這么多資源,那么當(dāng)內(nèi)存進(jìn)行進(jìn)程調(diào)度時(shí)進(jìn)行的進(jìn)程上下文切換就容易理解了,一個(gè)進(jìn)程運(yùn)行要依賴這么些資源,那么進(jìn)程上下文切換就要把這些資源都保存起來(lái)寫回到內(nèi)存中,等下次這個(gè)進(jìn)程被調(diào)度時(shí)再把這些資源再加載到寄存器和高速緩存硬件。
進(jìn)程上下文切換保存的內(nèi)容有:
頁(yè)表 -- 對(duì)應(yīng)虛擬內(nèi)存資源
文件描述符表/打開(kāi)文件表 -- 對(duì)應(yīng)打開(kāi)的文件資源
寄存器 -- 對(duì)應(yīng)運(yùn)行時(shí)數(shù)據(jù)
信號(hào)控制信息/進(jìn)程運(yùn)行信息
進(jìn)程間通信
虛擬內(nèi)存機(jī)制為進(jìn)程管理存儲(chǔ)資源帶來(lái)了種種好處,但是它也給進(jìn)程帶來(lái)了一些小麻煩,我們知道每個(gè)進(jìn)程擁有獨(dú)立的虛擬內(nèi)存地址空間,看到一樣的虛擬內(nèi)陸址空間視圖,所以對(duì)不同的進(jìn)程來(lái)說(shuō),一個(gè)相同的虛擬地址意味著不同的物理地址。我們知道CPU執(zhí)行指令時(shí)采用了虛擬地址,對(duì)應(yīng)一個(gè)特定的變量來(lái)說(shuō),它對(duì)應(yīng)著一個(gè)特定的虛擬地址。這樣帶來(lái)的問(wèn)題就是兩個(gè)進(jìn)程不能通過(guò)簡(jiǎn)單的共享變量的方式來(lái)進(jìn)行進(jìn)程間通信,也就是說(shuō)進(jìn)程不能通過(guò)直接共享內(nèi)存的方式來(lái)進(jìn)行進(jìn)程間通信,只能采用信號(hào),管道等方式來(lái)進(jìn)行進(jìn)程間通信。這樣的效率肯定比直接共享內(nèi)存的方式差
什么是線程
上面說(shuō)了一堆內(nèi)核為進(jìn)程分配了哪些資源,我們知道進(jìn)程管理了一堆資源,并且每個(gè)進(jìn)程還擁有獨(dú)立的虛擬內(nèi)存地址空間,會(huì)真正地?fù)碛歇?dú)立與父進(jìn)程之外的物理內(nèi)存。并且由于進(jìn)程擁有獨(dú)立的內(nèi)存地址空間,導(dǎo)致了進(jìn)程之間無(wú)法利用直接的內(nèi)存映射進(jìn)行進(jìn)程間通信。
并發(fā)的本質(zhì)是在時(shí)間上重疊的多個(gè)邏輯流,也就是說(shuō)同時(shí)運(yùn)行的多個(gè)邏輯流。并發(fā)編程要解決的一個(gè)很重要的問(wèn)題就是對(duì)資源的并發(fā)訪問(wèn)的問(wèn)題,也就是共享資源的問(wèn)題。而兩個(gè)進(jìn)程恰恰很難在邏輯上表示共享資源。
線程解決的最大問(wèn)題就是它可以很簡(jiǎn)單地表示共享資源的問(wèn)題,這里說(shuō)的資源指的是存儲(chǔ)器資源,資源最后都會(huì)加載到物理內(nèi)存,一個(gè)進(jìn)程的所有線程都是共享這個(gè)進(jìn)程的同一個(gè)虛擬地址空間的,也就是說(shuō)從線程的角度來(lái)說(shuō),它們看到的物理資源都是一樣的,這樣就可以通過(guò)共享變量的方式來(lái)表示共享資源,也就是直接共享內(nèi)存的方式解決了線程通信的問(wèn)題。而線程也表示一個(gè)獨(dú)立的邏輯流,這樣就完美解決了進(jìn)程的一個(gè)大難題。
從存儲(chǔ)資源的角度理解了線程之后,就不難理解計(jì)算資源的分配了。從計(jì)算資源的角度來(lái)說(shuō),對(duì)內(nèi)核而言,進(jìn)程和線程沒(méi)有什么區(qū)別,所以內(nèi)核用內(nèi)核調(diào)度實(shí)體(KSE)來(lái)表示一個(gè)調(diào)度的單元。
clone系統(tǒng)調(diào)用
在Linux系統(tǒng)中,線程是使用clone系統(tǒng)調(diào)用,clone是一個(gè)輕量級(jí)的fork,它提供了一系列的參數(shù)來(lái)表示線程可以共享父類的哪些資源,比如頁(yè)表,打開(kāi)文件表等等。我們上面說(shuō)過(guò)了共享和復(fù)制的區(qū)別,共享只是簡(jiǎn)單地用指針指向同一個(gè)物理地址,不會(huì)在父進(jìn)程之外開(kāi)辟新的物理內(nèi)存。
clone系統(tǒng)調(diào)用可以指定創(chuàng)建的線程開(kāi)始執(zhí)行代碼位置,也就是Java中的Thread類的run方法。
Linux內(nèi)核只提供了clone這個(gè)系統(tǒng)調(diào)用來(lái)創(chuàng)建類似線程的輕量級(jí)進(jìn)程的概念。C語(yǔ)言利用了Pthreads庫(kù)來(lái)真正創(chuàng)建了線程這個(gè)數(shù)據(jù)結(jié)構(gòu)。Linux采用了1:1的模型,即C語(yǔ)言的Pthreads庫(kù)創(chuàng)建的線程實(shí)體1:1對(duì)應(yīng)著內(nèi)核創(chuàng)建的一個(gè)KSE。Pthreads運(yùn)行在用戶空間,KSE運(yùn)行在內(nèi)核空間。
既然線程共享了進(jìn)程的資源,那么線程的上下文切換就好理解了。對(duì)操作系統(tǒng)來(lái)說(shuō),它看到要被調(diào)度進(jìn)來(lái)的線程和剛運(yùn)行的線程是同一個(gè)進(jìn)程的,那么線程的上下文切換只需要保存線程的一些運(yùn)行時(shí)的數(shù)據(jù),比如
線程的id
寄存器中的值
棧數(shù)據(jù)
而不需要像進(jìn)程上下文切換那樣要保存頁(yè)表,文件描述符表,信號(hào)控制數(shù)據(jù)和進(jìn)程信息等數(shù)據(jù)。頁(yè)表是一個(gè)很重的資源,我們之前說(shuō)過(guò),如果采用一級(jí)頁(yè)表的結(jié)構(gòu),那么32位機(jī)器的頁(yè)表要達(dá)到4MB的物理空間。 所以線程上下文切換是很輕量級(jí)的。
進(jìn)程采用父子結(jié)構(gòu),init進(jìn)程是最頂端的父進(jìn)程,其他進(jìn)程都是從init進(jìn)程派生出來(lái)的。這樣就很容易理解進(jìn)程是如何共享內(nèi)核的代碼和數(shù)據(jù)的了。
而線程采用對(duì)等結(jié)構(gòu),即線程沒(méi)有父子的概念,所有線程都屬于同一個(gè)線程組,線程組的組號(hào)等于第一個(gè)線程的線程號(hào)。
我們來(lái)看看Java的線程到底是如何實(shí)現(xiàn)的。Java語(yǔ)言層面提供了java.lang.Thread這個(gè)類來(lái)表示Java語(yǔ)言層面的線程,并提供了run方法表示線程運(yùn)行的邏輯控制流。
我們知道JVM是C++/C寫的,JVM本身利用了Pthreads庫(kù)來(lái)創(chuàng)建操作系統(tǒng)的線程。JVM還要支持Java語(yǔ)言創(chuàng)建的線程的概念。
聊聊JVM(五)從JVM角度理解線程 這篇已經(jīng)說(shuō)了從JVM的角度如何理解線程。 JVM提供了JavaThread類來(lái)對(duì)應(yīng)Java語(yǔ)言的Thread,即Java語(yǔ)言中創(chuàng)建一個(gè)java.lang.Thread對(duì)象,JVM會(huì)相應(yīng)的在JVM中創(chuàng)建一個(gè)JavaThread對(duì)象。同時(shí)JVM還創(chuàng)建了一個(gè)OSThread類來(lái)對(duì)應(yīng)用Pthreads創(chuàng)建的底層操作系統(tǒng)的線程對(duì)象。
構(gòu)建并發(fā)程序可以基于進(jìn)程也可以線程,
比如Nginx就是基于進(jìn)程構(gòu)建并發(fā)程序的。而Java天生只支持基于線程的方式來(lái)構(gòu)建并發(fā)程序。
最后再總結(jié)一下 進(jìn)程VS 線程
參考資料
《深入理解計(jì)算機(jī)系統(tǒng)》
《Linux系統(tǒng)編程手冊(cè)》
聯(lián)系客服