在操做系統課程的學習中,不少人對進程線程有大致的認識,但操做系統教材更偏向於理論敘述,本文會結合 Linux 系統實現分析,更加印象深入。python
同時,大部分人都接觸進程和線程比較多,對協程知之甚少,然而最近協程併發編程技術火熱起來,但願讀完本文你對協程也有一個基本的瞭解。linux
話很少說,咱們立刻進入本文的學習。程序員
首先仍是說下「程序」的概念,程序是一些保存在磁盤上的指令的有序集合,是靜態的。進程是程序執行的過程,包括了動態建立、調度和消亡的整個過程,進程是程序資源管理的最小單位。面試
那麼進程都管理哪些資源呢?一般包括內存資源、IO資源、信號處理等部分。編程
程序和進程網絡
篇幅有限着重說一下內存管理,進程運行起來必然會涉及到對內存資源的管理。內存資源有限,操做系統採用虛擬內存技術,把進程虛擬地址空間劃分紅用戶空間和內核空間。數據結構
4GB序的進程虛擬地址空間被分紅兩部分:用戶空間和內核空間多線程
用戶空間內核空間併發
用戶空間按照訪問屬性一致的地址空間存放在一塊兒的原則,劃分紅 5個不一樣的內存區域。訪問屬性指的是「可讀、可寫、可執行等 。ssh
代碼段代碼段是用來存放可執行文件的操做指令,可執行程序在內存中的鏡像。代碼段須要防止在運行時被非法修改,因此只准許讀取操做,它是不可寫的。
數據段數據段用來存放可執行文件中已初始化全局變量,換句話說就是存放程序靜態分配的變量和全局變量。
BSS段BSS段包含了程序中未初始化的全局變量,在內存中 bss 段所有置零。
對 heap堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
棧 stack棧是用戶存放程序臨時建立的局部變量,也就是函數中定義的變量(但不包括 static 聲明的變量,static意味着在數據段中存放變量)。除此之外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,而且待到調用結束後,函數的返回值也會被存放回棧中。因爲棧的先進後出特色,因此棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。
上述幾種內存區域中數據段、BSS 段、堆一般是被連續存儲在內存中,在位置上是連續的,而代碼段和棧每每會被獨立存放。堆和棧兩個區域在 i386 體系結構中棧向下擴展、堆向上擴展,相對而生。
程序內存分段
你也能夠在 linux 下用size 命令查看編譯後程序的各個內存區域大小:
[lemon ~]# size /usr/local/sbin/sshd
text data bss dec hex filename
1924532 12412 426896 2363840 2411c0 /usr/local/sbin/sshd
在 x86 32 位系統裏,Linux 內核地址空間是指虛擬地址從 0xC0000000 開始到 0xFFFFFFFF 爲止的高端內存地址空間,總計 1G 的容量, 包括了內核鏡像、物理頁面表、驅動程序等運行在內核空間 。
內核空間地址映射
線程是操做操做系統可以進行運算調度的最小單位。線程被包含在進程之中,是進程中的實際運做單位,一個進程內能夠包含多個線程,線程是資源調度的最小單位。
多線程程序模型
同一進程中的多條線程共享該進程中的所有系統資源,如虛擬地址空間,文件描述符文件描述符和信號處理等等。但同一進程中的多個線程有各自的調用棧、寄存器環境、線程本地存儲等信息。
線程建立的開銷主要是線程堆棧的創建,分配內存的開銷。這些開銷並不大,最大的開銷發生在線程上下文切換的時候。
線程切換
還記得剛開始咱們講的內核空間和用戶空間概念嗎?線程按照實現位置和方式的不一樣,也分爲用戶級線程和內核線程,下面一塊兒來看下這兩類線程的差別和特色。
實如今用戶空間的線程稱爲用戶級線程。用戶線程是徹底創建在用戶空間的線程庫,用戶線程的建立、調度、同步和銷燬全由用戶空間的庫函數完成,不須要內核的參與,所以這種線程的系統資源消耗很是低,且很是的高效。
用戶線級線程只能參與競爭該進程的處理器資源,不能參與全局處理器資源的競爭。
用戶級線程切換都在用戶空間進行,開銷極低。
用戶級線程調度器在用戶空間的線程庫實現,內核的調度對象是進程自己,內核並不知道用戶線程的存在。
用戶線程圖解
若是觸發了引發阻塞的系統調用的調用,會當即阻塞該線程所屬的整個進程。
系統只看到進程看不到用戶線程,因此只有一個處理器內核會被分配給該進程 ,也就不能發揮多久 CPU 的優點 。
內核線程創建和銷燬都是由操做系統負責、經過系統調用完成,內核維護進程及線程的上下文信息以及線程切換。
內核級線級能參與全局的多核處理器資源分配,充分利用多核 CPU 優點。
每一個內核線程均可被內核調度,由於線程的建立、撤銷和切換都是由內核管理的。
一個內核線程阻塞與他同屬一個進程的線程仍然能繼續運行。
內核線程圖解
內核級線程調度開銷較大。調度內核線程的代價可能和調度進程差很少昂貴,代價要比用戶級線程大不少。
線程表是存放在操做系統固定的表格空間或者堆棧空間裏,因此內核級線程的數量是有限的。
Linux 並無爲線程準備特定的數據結構,由於 Linux只有task_struct這一種描述進程的結構體。在內核看來只有進程而沒有線程,線程調度時也是當作進程來調度的。Linux所謂的線程實際上是與其餘進程共享資源的輕量級進程。
爲何說是輕量級呢?在於它只有一個最小的執行上下文和調度程序所需的統計信息,它只帶有進程執行相關的信息,與父進程共享進程地址空間 。
輕量級線程 Light-weight Process簡稱LWP ,是一種由內核支持的用戶線程,每個輕量級進程都與一個特定的內核線程關聯。
它是基於內核線程的高級抽象,系統只有先支持內核線程纔能有 LWP。每個進程有一個或多個 LWPs ,每一個LWP 由一個內核線程支持,在這種實現的操做系統中 LWP 就是用戶線程。
輕量級進程
輕量級進程最先在Linux 內核 2.0.x 版本就已實現,應用程序經過一個統一的 clone() 系統調用接口,用不一樣的參數指定建立的進程是輕量進程仍是普通進程。
因爲輕量輕量級進程基於內核線程實現,所以它的特色和缺點就是內核線程的缺點,這裏再也不贅述。
輕量級線程也沒什麼神祕的,還記得我在這篇文章《資深程序員總結:分析Linux進程的6個方法,我全都告訴你》教你的方法嗎?咱們用 Linux 的 pstack 命令能夠查看進程的輕量級線程 LWP 信息。下圖的黃色字體就是打印出的輕量級線程 ID ,以及該線程的調用堆棧信息,從最新的棧幀開始往下排列。
用法示例:pstack pid
pstack查看lwp
協程的知名度好像不是很高,在之前咱們談論高併發,大部分人都知道利用多線程和多進程部署服務,提升服務性能,但通常不會提到協程。其實協程的概念出來的比線程還早,只不過最近才被人們更多的提起。
協程之因此最近被你們熟知,我的以爲是 Python 和 Go 從語言層面提供了對協程更好的支持,尤爲是以 Goroutine 爲表明的 Go 協程實現,很大程度上下降了協程使用門檻,能夠說是後起之秀了!
當今無數的 Web 服務和互聯網服務,本質上大部分都是 IO 密集型服務,什麼是 IO 密集型服務?意思是處理的任務大可能是和網絡鏈接或讀寫相關的高耗時任務,高耗時是相對 CPU 計算邏輯處理型任務來講,二者的處理時間差距不是一個數量級的。
IO 密集型服務的瓶頸不在 CPU 處理速度,而在於儘量快速的完成高併發、多鏈接下的數據讀寫。
之前有兩種解決方案:
若是用多線程,高併發場景的大量 IO 等待會致使多線程被頻繁掛起和切換,很是消耗系統資源,同時多線程訪問共享資源存在競爭問題。
若是用多進程,不只存在頻繁調度切換問題,同時還會存在每一個進程資源不共享的問題,須要額外引入進程間通訊機制來解決。
協程出現給高併發和 IO 密集型服務開發提供了另外一種選擇。
固然,世界上沒有技術銀彈。在這裏我想把協程這把鑰匙交到你手中,可是它也不是萬能鑰匙,最好的解決方案是貼合自身業務類型作出最優選擇,不必定就選擇一種模型,有時候是幾種模型的組合,好比多線程搭配協程是常見的組合。
那什麼是協程呢?攜程 Coroutines 是一種比線程更加輕量級的微線程。類比一個進程能夠擁有多個線程,一個線程也能夠擁有多個協程,所以協程又稱爲線程和線程。
協程圖解
能夠粗略的把協程理解成子程序調用,每一個子程序均可以在一個單獨的協程內執行。
協程子程序模型
線程是被內核所調度,線程被調度切換到另外一個線程上下文的時候,須要保存一個用戶線程的狀態到內存,恢復另外一個線程狀態到寄存器,而後更新調度器的數據結構,這幾步操做設計用戶態到內核態轉換,開銷比較多。
線程切換
協程的調度徹底由用戶控制,協程擁有本身的寄存器上下文和棧,協程調度切換時,將寄存器上下文和棧保存到其餘地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操做用戶空間棧,徹底沒有內核切換的開銷。
協程切換
協程擁有本身的寄存器上下文和棧,協程調度切換時將寄存器上下文和棧保存下來,在切回來的時候,恢復先前保存的寄存器的上下文和棧。
Goroutine 是 Golang 的協程實現。Goroutine 的棧只有 2KB大小,並且是動態伸縮的,能夠按需調整大小,最大可達 1G 相比線程來講既不浪費又靈活了不少,能夠說是至關的nice了!
線程也都有一個固定大小的內存塊來作棧,通常會是 2MB 大小,線程棧會用來存儲線程上下文信息。2MB 的線程棧和協程棧相比大了不少。
線程和協程棧對比
python 2.5 中引入 yield/send 表達式用於實現協程,但這種經過生成器的方式使用協程不夠優雅。
python 3.5 以後引入async/await ,簡化了協程的使用而且更加便於理解。
Golang 在語言層面實現了對協程的支持,Goroutine 是協程在 Go 語言中的實現, 在 Go 語言中每個併發的執行單元叫做一個 Goroutine ,Go 程序能夠輕鬆建立成百上千個協程併發執行。
Go 協程調度器有三個重要數據結構:
G 表示 Goroutine ,它是一個待執行的任務;
M 表示操做系統的線程,它由操做系統的調度器調度和管理;
P 表示處理器 Processor,它能夠被看作運行在線程上的本地調度器;
協商調度
Go 調度器最多能夠建立 10000 個線程,但能夠經過設置 GOMAXPROCS 變量指可以正常運行的線程數, 這個變量的默認值 等於 CPU 個數,也就是線程數等於 CPU 核數,這樣不會觸發操做系統的線程調度和上下文切換,全部的調度由 Go 語言調度器觸發,都是在用戶態,減小了很是多的調用開銷。
這篇文章講解和對比了進程、線程的概念,同時經過進程窺探到操做系統內存管理的冰山一角,另外還講解了具體到 Linux 系統下線程的實現現狀,順勢引出了輕量級進程的概念。最後着重說明了大部分同窗不太瞭解的協程,經過對比不一樣的服務模型,帶你瞭解協程的特色。