關於進程和線程,你們老是說的一句話是「進程是操做系統分配資源的最小單元,線程是操做系統調度的最小單元」。這句話理論上沒問題,咱們來看看什麼是所謂的「資源」呢。html
什麼是計算機資源java
經典的馮諾依曼結構把計算機系統抽象成 CPU + 存儲器 + IO,那麼計算機資源無非就兩種:node
1. 計算資源編程
2. 存儲資源緩存
CPU是計算單元,單純從CPU的角度來講它是一個黑盒,它只對輸入的指令和數據進行計算,而後輸出結果,它不負責管理計算哪些」指令和數據「。 換句話說CPU只提供了計算能力,可是不負責分配計算資源。網絡
計算資源是操做系統來分配的,也就是常說的操做系統的調度模塊,由操做系統按照必定的規則來分配何時由誰來得到CPU的計算資源,好比分時間片數據結構
存儲資源就是內存,磁盤這些存儲設備的資源。在這篇計算機底層知識拾遺(一)理解虛擬內存機制 咱們說了操做系統使用了虛擬內存機制來管理存儲器,從緩存原理的角度來講,把內存做爲磁盤的緩存。進程是面向磁盤的,爲何這麼說呢,進程表示一個運行的程序,程序的代碼段,數據段這些都是存放在磁盤中的,在運行時加載到內存中。因此虛擬內存面向的是磁盤,虛擬頁是對磁盤文件的分配,而後被緩存到物理內存的物理頁中。併發
因此存儲資源是操做系統由虛擬內存機制來管理和分配的。進程應該是操做系統分配存儲資源的最小單元。函數
再來看看線程,理論上說Linux內核是沒有線程這個概念的,只有內核調度實體(Kernal Scheduling Entry, KSE)這個概念。Linux的線程本質上是一種輕量級的進程,是經過clone系統調用來建立的。何謂「輕量級」會在後面細說。進程是一種KSE,線程也是一種KSE。因此「線程是操做系統調度的最小單元」這句話沒問題。性能
什麼是進程
進程是對計算機的一種抽象,
1. 進程表示一個邏輯控制流,就是一種計算過程,它形成一個假象,好像這個進程一直在獨佔CPU資源
2. 進程擁有一個獨立的虛擬內存地址空間,它形成一個假象,好像這個進程一致在獨佔存儲器資源
這張圖是進程的虛擬內存地址空間的分配模型圖,能夠看到進程的虛擬內存地址空間分爲用戶空間和內核空間。用戶空間從低端地址往高端地址發展,內核空間從高端地址往低端地址發展。用戶空間存放着這個進程的代碼段和數據段,以及運行時的堆和用戶棧。堆是從低端地址往高端地址發展,棧是從高端地址往低端地址發展。
內核空間存放着內核的代碼和數據,以及內核爲這個進程建立的相關數據結構,好比頁表數據結構,task數據結構,area區域數據結構等等。
從文件IO的角度來講,Linux把一切IO都抽象成了文件,好比普通文件IO,網絡IO,通通都是文件,利用open系統調用返回一個整數做爲文件描述符file descriptor,進程能夠利用file descriptor做爲參數在任何系統調用中表示那個打開的文件。內核爲進程維護了一個文件描述符表來保持進程全部得到的file descriptor。
每調用一次open系統調用內核會建立一個打開文件open file的數據結構來表示這個打開的文件,記錄了該文件目前讀取的位置等信息。打開文件又惟一了一個指針指向文件系統中該文件的inode結構。inode記錄了該文件的文件名,路徑,訪問權限等元數據。
操做操做系統用了3個數據結構來爲每一個進程管理它打開的文件資源
fork系統調用
操做系統利用fork系統調用來建立一個子進程。fork所建立的子進程會複製父進程的虛擬地址空間。
要理解「複製」和「共享」的區別,複製的意思是會真正在物理內存複製一分內容,會真正消耗新的物理內存。共享的意思是使用指針指向同一個地址,不會真正的消耗物理內存。理解這兩個概念的區別很重要,這是進程和線程的根本區別之一。
那麼有人問了若是我父進程佔了1G的物理內存,那麼fork會再使用1G的物理內存來複制嗎,至關於一下用了2G的物理內存?
答案是早期的操做系統的確是這麼幹的,可是這樣性能也太差了,因此現代操做系統使用了 寫時複製Copy on write的方式來優化fork的性能,fork剛建立的子進程採用了共享的方式,只用指針指向了父進程的物理資源。當子進程真正要對某些物理資源寫操做時,纔會真正的複製一塊物理資源來供子進程使用。這樣就極大的優化了fork的性能,而且從邏輯來講子進程的確是擁有了獨立的虛擬內存空間。
fork不僅是複製了頁表結構,還複製了父進程的文件描述符表,信號控制表,進程信息,寄存器資源等等。它是一個較爲深刻的複製。
從邏輯控制流的角度來講,fork建立的子進程開始執行的位置是fork函數返回的位置。這點和線程是不同的,咱們知道Java中的Thread須要寫run方法,線程開始後會從run方法開始執行。
既然咱們知道了內核爲進程維護了這麼多資源,那麼當內存進行進程調度時進行的進程上下文切換就容易理解了,一個進程運行要依賴這麼些資源,那麼進程上下文切換就要把這些資源都保存起來寫回到內存中,等下次這個進程被調度時再把這些資源再加載到寄存器和高速緩存硬件。
進程上下文切換保存的內容有:
頁表 -- 對應虛擬內存資源
文件描述符表/打開文件表 -- 對應打開的文件資源
寄存器 -- 對應運行時數據
信號控制信息/進程運行信息
進程間通訊
虛擬內存機制爲進程管理存儲資源帶來了種種好處,可是它也給進程帶來了一些小麻煩,咱們知道每一個進程擁有獨立的虛擬內存地址空間,看到同樣的虛擬內地址空間視圖,因此對不一樣的進程來講,一個相同的虛擬地址意味着不一樣的物理地址。傷感的句子咱們知道CPU執行指令時採用了虛擬地址,對應一個特定的變量來講,它對應着一個特定的虛擬地址。這樣帶來的問題就是兩個進程不能經過簡單的共享變量的方式來進行進程間通訊,也就是說進程不能經過直接共享內存的方式來進行進程間通訊,只能採用信號,管道等方式來進行進程間通訊。這樣的效率確定比直接共享內存的方式差
什麼是線程
上面說了一堆內核爲進程分配了哪些資源,咱們知道進程管理了一堆資源,而且每一個進程還擁有獨立的虛擬內存地址空間,會真正地擁有獨立與父進程以外的物理內存。而且因爲進程擁有獨立的內存地址空間,致使了進程之間沒法利用直接的內存映射進行進程間通訊。
併發的本質是在時間上重疊的多個邏輯流,也就是說同時運行的多個邏輯流。併發編程要解決的一個很重要的問題就是對資源的併發訪問的問題,也就是共享資源的問題。而兩個進程偏偏很難在邏輯上表示共享資源。
線程解決的最大問題就是它能夠很簡單地表示共享資源的問題,這裏說的資源指的是存儲器資源,資源最後都會加載到物理內存,一個進程的全部線程都是共享這個進程的同一個虛擬地址空間的,也就是說從線程的角度來講,它們看到的物理資源都是同樣的,這樣就能夠經過共享變量的方式來表示共享資源,也就是直接共享內存的方式解決了線程通訊的問題。而線程也表示一個獨立的邏輯流,這樣就完美解決了進程的一個大難題。
從存儲資源的角度理解了線程以後,就不難理解計算資源的分配了。從計算資源的角度來講,對內核而言,進程和線程沒有什麼區別,因此內核用內核調度實體(KSE)來表示一個調度的單元。
clone系統調用
在Linux系統中,線程是使用clone系統調用,clone是一個輕量級的fork,淘寶開店教程它提供了一系列的參數來表示線程能夠共享父類的哪些資源,好比頁表,打開文件表等等。咱們上面說過了共享和複製的區別,共享只是簡單地用指針指向同一個物理地址,不會在父進程以外開闢新的物理內存。
clone系統調用能夠指定建立的線程開始執行代碼位置,也就是Java中的Thread類的run方法。
Linux內核只提供了clone這個系統調用來建立相似線程的輕量級進程的概念。C語言利用了Pthreads庫來真正建立了線程這個數據結構。Linux採用了1:1的模型,即C語言的Pthreads庫建立的線程實體1:1對應着內核建立的一個KSE。Pthreads運行在用戶空間,KSE運行在內核空間。
既然線程共享了進程的資源,那麼線程的上下文切換就好理解了。對操做系統來講,它看到要被調度進來的線程和剛運行的線程是同一個進程的,那麼線程的上下文切換隻須要保存線程的一些運行時的數據,好比
線程的id
寄存器中的值
棧數據
而不須要像進程上下文切換那樣要保存頁表,文件描述符表,信號控制數據和進程信息等數據。頁表是一個很重的資源,咱們以前說過,若是採用一級頁表的結構,那麼32位機器的頁表要達到4MB的物理空間。 有一種沉默是感懷因此線程上下文切換是很輕量級的。
進程採用父子結構,init進程是最頂端的父進程,其餘進程都是從init進程派生出來的。這樣就很容易理解進程是如何共享內核的代碼和數據的了。
而線程採用對等結構,即線程沒有父子的概念,全部線程都屬於同一個線程組,線程組的組號等於第一個線程的線程號。
咱們來看看Java的線程究竟是如何實現的。Java語言層面提供了java.lang.Thread這個類來表示Java語言層面的線程,並提供了run方法表示線程運行的邏輯控制流。
咱們知道JVM是C++/C寫的,JVM自己利用了Pthreads庫來建立操做系統的線程。JVM還要支持Java語言建立的線程的概念。
聊聊JVM(五)從JVM角度理解線程 這篇已經說了從JVM的角度如何理解線程。 JVM提供了JavaThread類來對應Java語言的Thread,即Java語言中建立一個java.lang.Thread對象,JVM會相應的在JVM中建立一個JavaThread對象。同時JVM還建立了一個OSThread類來對應用Pthreads建立的底層操做系統的線程對象。
構建併發程序能夠基於進程也能夠線程,
好比Nginx就是基於進程構建併發程序的。而Java天生只支持基於線程的方式來構建併發程序。
最後再總結一下 進程VS 線程
1. 進程採用fork建立,線程採用clone建立2. 進程fork建立的子進程的邏輯流位置在fork返回的位置,線程clone建立的KSE的邏輯流位置在clone調用傳入的方法位置,好比Java的Thread的run方法位置3. 進程擁有獨立的虛擬內存地址空間和內核數據結構(頁表,打開文件表等),當子進程修改了虛擬頁以後,會經過寫時拷貝建立真正的物理頁。線程共享進程的虛擬地址空間和內核數據結構,共享一樣的物理頁4. 多個進程通訊只能採用進程間通訊的方式,好比信號,管道,而不能直接採用簡單的共享內存方式,緣由是每一個進程維護獨立的虛擬內存空間,因此每一個進程的變量採用的虛擬地址是不一樣的。多個線程通訊就很簡單,直接採用共享內存的方式,由於不一樣線程共享一個虛擬內存地址空間,變量尋址採用同一個虛擬內存5. 進程上下文切換須要切換頁表等重量級資源,線程上下文切換隻須要切換寄存器等輕量級數據6. 進程的用戶棧獨享棧空間,線程的用戶棧共享虛擬內存中的棧空間,沒有進程高效7. 一個應用程序能夠有多個進程,執行多個程序代碼,多個線程只能執行一個程序代碼,共享進程的代碼段8. 進程採用父子結構,線程採用對等結構