前言
很早之前就在構(gòu)思這篇文章的主題沛申,進(jìn)程線程可以說是操作系統(tǒng)基礎(chǔ)劣领,看過很多關(guān)于這方面知識的文章都是純理論講述,編程新手有些難以下咽铁材。
于是寫下這篇文章尖淘,用圖解的形式帶你學(xué)習(xí)和掌握進(jìn)程、線程著觉、協(xié)程村生,文字力求簡單明了,對于復(fù)雜概念做到一個概念一張圖解饼丘,即使你是編程小白也能看的明明白白梆造,媽媽再也不用擔(dān)心你的學(xué)習(xí)。
Go 基礎(chǔ)教程系列教程更新已接近尾聲葬毫,對 Go 語言學(xué)習(xí)感興趣但還沒看過的的同學(xué)镇辉,可以在公眾號歷史文章查看。Go 基礎(chǔ)教程接下來會進(jìn)入并發(fā)編程和 Go 協(xié)程部分贴捡,為了更好的理解這部分內(nèi)容忽肛,帶大家先了解 Linux 系統(tǒng)基礎(chǔ)和進(jìn)程、線程以及協(xié)程的差異與特點(diǎn)烂斋。
在操作系統(tǒng)課程的學(xué)習(xí)中屹逛,很多人對進(jìn)程線程有大體的認(rèn)識,但操作系統(tǒng)教材更偏向于理論敘述汛骂,本文會結(jié)合 Linux 系統(tǒng)實(shí)現(xiàn)分析罕模,更加印象深刻。
同時帘瞭,大部分人都接觸進(jìn)程和線程比較多淑掌,對協(xié)程知之甚少,然而最近協(xié)程并發(fā)編程技術(shù)火熱起來蝶念,希望讀完本文你對協(xié)程也有一個基本的了解抛腕。
話不多說,我們馬上進(jìn)入本文的學(xué)習(xí)媒殉。
進(jìn)程
關(guān)于進(jìn)程和內(nèi)存管理我之前有一篇文章單獨(dú)講解過担敌,這里再挑選一部分和本文相關(guān)的內(nèi)容學(xué)習(xí),溫故而知新廷蓉。
首先還是說下「程序」的概念全封,程序是一些保存在磁盤上的指令的有序集合,是靜態(tài)的。進(jìn)程是程序執(zhí)行的過程刹悴,包括了動態(tài)創(chuàng)建给猾、調(diào)度和消亡的整個過程,進(jìn)程是程序資源管理的最小單位颂跨。
進(jìn)程與資源
那么進(jìn)程都管理哪些資源呢敢伸? 通常包括內(nèi)存資源、IO資源恒削、信號處理等部分池颈。
篇幅有限著重說一下內(nèi)存管理,進(jìn)程運(yùn)行起來必然會涉及到對內(nèi)存資源的管理钓丰。內(nèi)存資源有限躯砰,操作系統(tǒng)采用虛擬內(nèi)存技術(shù),把進(jìn)程虛擬地址空間劃分成用戶空間和內(nèi)核空間携丁。
地址空間
4GB
的進(jìn)程虛擬地址空間被分成兩部分:用戶空間和內(nèi)核空間
用戶空間
用戶空間按照訪問屬性一致的地址空間存放在一起的原則琢歇,劃分成 5
個不同的內(nèi)存區(qū)域。 訪問屬性指的是“可讀梦鉴、可寫李茫、可執(zhí)行等 。
-
代碼段
代碼段是用來存放可執(zhí)行文件的操作指令肥橙,可執(zhí)行程序在內(nèi)存中的鏡像魄宏。代碼段需要防止在運(yùn)行時被非法修改,所以只準(zhǔn)許讀取操作存筏,它是不可寫的宠互。
-
數(shù)據(jù)段
數(shù)據(jù)段用來存放可執(zhí)行文件中已初始化全局變量,換句話說就是存放程序靜態(tài)分配的變量和全局變量椭坚。
-
BSS段
BSS
段包含了程序中未初始化的全局變量予跌,在內(nèi)存中bss
段全部置零。 -
堆
heap
堆是用于存放進(jìn)程運(yùn)行中被動態(tài)分配的內(nèi)存段善茎,它的大小并不固定券册,可動態(tài)擴(kuò)張或縮減。當(dāng)進(jìn)程調(diào)用malloc等函數(shù)分配內(nèi)存時巾表,新分配的內(nèi)存就被動態(tài)添加到堆上(堆被擴(kuò)張)汁掠;當(dāng)利用free等函數(shù)釋放內(nèi)存時略吨,被釋放的內(nèi)存從堆中被剔除(堆被縮減)
-
棧
stack
棧是用戶存放程序臨時創(chuàng)建的局部變量集币,也就是函數(shù)中定義的變量(但不包括
static
聲明的變量,static意味著在數(shù)據(jù)段中存放變量)翠忠。除此以外鞠苟,在函數(shù)被調(diào)用時,其參數(shù)也會被壓入發(fā)起調(diào)用的進(jìn)程棧中,并且待到調(diào)用結(jié)束后当娱,函數(shù)的返回值也會被存放回棧中吃既。由于棧的先進(jìn)后出特點(diǎn),所以棧特別方便用來保存/恢復(fù)調(diào)用現(xiàn)場跨细。從這個意義上講鹦倚,我們可以把堆棧看成一個寄存冀惭、交換臨時數(shù)據(jù)的內(nèi)存區(qū)震叙。
上述幾種內(nèi)存區(qū)域中數(shù)據(jù)段、BSS
段散休、堆通常是被連續(xù)存儲在內(nèi)存中媒楼,在位置上是連續(xù)的,而代碼段和棧往往會被獨(dú)立存放戚丸。堆和棧兩個區(qū)域在 i386
體系結(jié)構(gòu)中棧向下擴(kuò)展划址、堆向上擴(kuò)展,相對而生限府。
你也可以再 linux 下用size
命令查看編譯后程序的各個內(nèi)存區(qū)域大卸岵:
[lemon ~]# size /usr/local/sbin/sshd
text data bss dec hex filename
1924532 12412 426896 2363840 2411c0 /usr/local/sbin/sshd
內(nèi)核空間
在 x86 32
位系統(tǒng)里,Linux 內(nèi)核地址空間是指虛擬地址從 0xC0000000
開始到 0xFFFFFFFF
為止的高端內(nèi)存地址空間胁勺,總計 1G
的容量拂共, 包括了內(nèi)核鏡像、物理頁面表姻几、驅(qū)動程序等運(yùn)行在內(nèi)核空間 宜狐。
線程
線程是操作操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。線程被包含在進(jìn)程之中蛇捌,是進(jìn)程中的實(shí)際運(yùn)作單位抚恒,一個進(jìn)程內(nèi)可以包含多個線程,線程是資源調(diào)度的最小單位络拌。
線程資源和開銷
同一進(jìn)程中的多條線程共享該進(jìn)程中的全部系統(tǒng)資源俭驮,如虛擬地址空間,文件描述符文件描述符和信號處理等等春贸。但同一進(jìn)程中的多個線程有各自的調(diào)用棧混萝、寄存器環(huán)境、線程本地存儲等信息萍恕。
線程創(chuàng)建的開銷主要是線程堆棧的建立逸嘀,分配內(nèi)存的開銷。這些開銷并不大允粤,最大的開銷發(fā)生在線程上下文切換的時候崭倘。
線程分類
還記得剛開始我們講的內(nèi)核空間和用戶空間概念嗎翼岁?線程按照實(shí)現(xiàn)位置和方式的不同,也分為用戶級線程和內(nèi)核線程司光,下面一起來看下這兩類線程的差異和特點(diǎn)琅坡。
用戶級線程
實(shí)現(xiàn)在用戶空間的線程稱為用戶級線程。用戶線程是完全建立在用戶空間的線程庫残家,用戶線程的創(chuàng)建榆俺、調(diào)度、同步和銷毀全由用戶空間的庫函數(shù)完成坞淮,不需要內(nèi)核的參與谴仙,因此這種線程的系統(tǒng)資源消耗非常低,且非常的高效碾盐。
特點(diǎn)
- 用戶線級線程只能參與競爭該進(jìn)程的處理器資源晃跺,不能參與全局處理器資源的競爭。
- 用戶級線程切換都在用戶空間進(jìn)行毫玖,開銷極低掀虎。
- 用戶級線程調(diào)度器在用戶空間的線程庫實(shí)現(xiàn),內(nèi)核的調(diào)度對象是進(jìn)程本身付枫,內(nèi)核并不知道用戶線程的存在烹玉。
缺點(diǎn)
- 如果觸發(fā)了引起阻塞的系統(tǒng)調(diào)用的調(diào)用,會立即阻塞該線程所屬的整個進(jìn)程阐滩。
- 系統(tǒng)只看到進(jìn)程看不到用戶線程二打,所以只有一個處理器內(nèi)核會被分配給該進(jìn)程 ,也就不能發(fā)揮多核 CPU 的優(yōu)勢 掂榔。
內(nèi)核級線程
內(nèi)核級線程是指继效,內(nèi)核線程建立和銷毀都是由操作系統(tǒng)負(fù)責(zé)、通過系統(tǒng)調(diào)用完成的装获,內(nèi)核維護(hù)進(jìn)程及線程的上下文信息以及線程切換瑞信。
特點(diǎn)
- 內(nèi)核級線級能參與全局的多核處理器資源分配,充分利用多核 CPU 優(yōu)勢穴豫。
- 每個內(nèi)核線程都可被內(nèi)核調(diào)度凡简,因?yàn)榫€程的創(chuàng)建、撤銷和切換都是對內(nèi)核管理的精肃。
- 一個內(nèi)核線程阻塞與他同屬一個進(jìn)程的線程仍然能繼續(xù)運(yùn)行秤涩。
缺點(diǎn)
- 內(nèi)核級線程切換開銷較大。因?yàn)槊看尉€程切換都要從用戶態(tài)切換到內(nèi)核態(tài)司抱, 代價要比用戶級線程大很多筐眷。
- 線程表是存放在操作系統(tǒng)固定的表格空間或者堆棧空間里状植,所以內(nèi)核級線程的數(shù)量是有限的浊竟。
Linux 線程實(shí)現(xiàn)
Linux
并沒有為線程準(zhǔn)備特定的數(shù)據(jù)結(jié)構(gòu)怨喘,因?yàn)?Linux只有task_struct
這一種描述進(jìn)程的結(jié)構(gòu)體津畸。在內(nèi)核看來只有進(jìn)程而沒有線程振定,線程調(diào)度時也是當(dāng)做進(jìn)程來調(diào)度的。Linux所謂的線程其實(shí)是與其他進(jìn)程共享資源的輕量級進(jìn)程肉拓。
為什么說是輕量級呢后频?在于它只有一個最小的執(zhí)行上下文和調(diào)度程序所需的統(tǒng)計信息,它只帶有進(jìn)程執(zhí)行相關(guān)的信息暖途,與父進(jìn)程共享進(jìn)程地址空間 卑惜。
輕量級進(jìn)程
輕量級線程 Light-weight Process
簡稱LWP
,是一種由內(nèi)核支持的用戶線程驻售,每一個輕量級進(jìn)程都與一個特定的內(nèi)核線程關(guān)聯(lián)露久。
它是基于內(nèi)核線程的高級抽象,系統(tǒng)只有先支持內(nèi)核線程才能有 LWP
欺栗。每一個進(jìn)程有一個或多個 LWPs
毫痕,每個LWP
由一個內(nèi)核線程支持,在這種實(shí)現(xiàn)的操作系統(tǒng)中 LWP
就是用戶線程迟几。
輕量級進(jìn)程最早在Linux
內(nèi)核 2.0.x
版本就已實(shí)現(xiàn)消请,應(yīng)用程序通過一個統(tǒng)一的 clone()
系統(tǒng)調(diào)用接口,用不同的參數(shù)指定創(chuàng)建的進(jìn)程是輕量進(jìn)程還是普通進(jìn)程类腮。
特點(diǎn)和缺點(diǎn)
由于輕量輕量級進(jìn)程基于內(nèi)核線程實(shí)現(xiàn)臊泰,因此它的特點(diǎn)和缺點(diǎn)就是內(nèi)核線程的缺點(diǎn),這里不再贅述蚜枢。
查看 LWP 信息
輕量級線程也沒什么神秘的缸逃,還記得我在這篇文章《資深程序員總結(jié):分析Linux進(jìn)程的6個方法,我全都告訴你》教你的方法嗎厂抽?我們用 Linux 的 pstack
命令可以查看進(jìn)程的輕量級線程 LWP 信息察滑。下圖的黃色字體就是打印出的輕量級線程 ID ,以及該線程的調(diào)用堆棧信息修肠,從最新的棧幀開始往下排列贺辰。
用法示例: pstack pid
協(xié)程
協(xié)程的知名度好像不是很高,在以前我們談?wù)摳卟l(fā)嵌施,大部分人都知道利用多線程和多進(jìn)程部署服務(wù)饲化,提高服務(wù)性能,但一般不會提到協(xié)程吗伤。其實(shí)協(xié)程的概念出來的比線程還早吃靠,只不過最近才被人們更多的提起。
協(xié)程之所以最近被大家熟知足淆,個人覺得是 Python
和 Go
從語言層面提供了對協(xié)程更好的支持巢块,尤其是以 Goroutine
為代表的 Go 協(xié)程實(shí)現(xiàn)礁阁,很大程度上降低了協(xié)程使用門檻,可以說是后起之秀了族奢!
why 協(xié)程
當(dāng)今無數(shù)的 Web 服務(wù)和互聯(lián)網(wǎng)服務(wù)姥闭,本質(zhì)上大部分都是 IO 密集型服務(wù),什么是 IO 密集型服務(wù)越走?意思是處理的任務(wù)大多是和網(wǎng)絡(luò)連接或讀寫相關(guān)的高耗時任務(wù)棚品,高耗時是相對 CPU 計算邏輯處理型任務(wù)來說,兩者的處理時間差距不是一個數(shù)量級的廊敌。
IO 密集型服務(wù)的瓶頸不在 CPU 處理速度铜跑,而在于盡可能快速的完成高并發(fā)、多連接下的數(shù)據(jù)讀寫骡澈。
以前有兩種解決方案:
如果用多線程锅纺,高并發(fā)場景的大量 IO 等待會導(dǎo)致多線程被頻繁掛起和切換,非常消耗系統(tǒng)資源肋殴,同時多線程訪問共享資源存在競爭問題囤锉。
如果用多進(jìn)程,不僅存在頻繁調(diào)度切換問題疼电,同時還會存在每個進(jìn)程資源不共享的問題嚼锄,需要額外引入進(jìn)程間通信機(jī)制來解決。
協(xié)程出現(xiàn)給高并發(fā)和 IO 密集型服務(wù)開發(fā)提供了另一種選擇蔽豺。
當(dāng)然区丑,世界上沒有技術(shù)銀彈,在這里我想把協(xié)程這把鑰匙交到你手中修陡,但是它也不是萬能鑰匙沧侥,最好的解決方案是貼合自身業(yè)務(wù)類型做出最優(yōu)選擇,不一定就選擇一種模型魄鸦,有時候是幾種模型的組合宴杀,比如多線程搭配協(xié)程是常見的組合。
什么是協(xié)程
那什么是協(xié)程呢拾因?協(xié)程 Coroutines
是一種比線程更加輕量級的微線程旺罢。類比一個進(jìn)程可以擁有多個線程,一個線程也可以擁有多個協(xié)程绢记,因此協(xié)程又稱微線程和纖程扁达。
可以粗略的把協(xié)程理解成子程序調(diào)用,每個子程序都可以在一個單獨(dú)的協(xié)程內(nèi)執(zhí)行蠢熄。
調(diào)度開銷
線程是被內(nèi)核所調(diào)度跪解,線程被調(diào)度切換到另一個線程上下文的時候,需要保存一個用戶線程的狀態(tài)到內(nèi)存签孔,恢復(fù)另一個線程狀態(tài)到寄存器叉讥,然后更新調(diào)度器的數(shù)據(jù)結(jié)構(gòu)窘行,這幾步操作設(shè)計用戶態(tài)到內(nèi)核態(tài)轉(zhuǎn)換,開銷比較多图仓。
協(xié)程的調(diào)度完全由用戶控制罐盔,協(xié)程擁有自己的寄存器上下文和棧,協(xié)程調(diào)度切換時透绩,將寄存器上下文和棧保存到其他地方翘骂,在切回來的時候壁熄,恢復(fù)先前保存的寄存器上下文和棧帚豪,直接操作用戶空間棧,完全沒有內(nèi)核切換的開銷草丧。
動態(tài)協(xié)程棧
協(xié)程擁有自己的寄存器上下文和棧狸臣,協(xié)程調(diào)度切換時將寄存器上下文和棧保存下來,在切回來的時候昌执,恢復(fù)先前保存的寄存器的上下文和棧烛亦。
Goroutine 是 Golang 的協(xié)程實(shí)現(xiàn)。Goroutine 的棧只有 2KB大小懂拾,而且是動態(tài)伸縮的煤禽,可以按需調(diào)整大小,最大可達(dá) 1G 相比線程來說既不浪費(fèi)又靈活了很多岖赋,可以說是相當(dāng)?shù)膎ice了檬果!
線程也都有一個固定大小的內(nèi)存塊來做棧,一般會是 2MB 大小唐断,線程棧會用來存儲線程上下文信息选脊。2MB 的線程棧和協(xié)程棧相比大了很多。
協(xié)程實(shí)現(xiàn)
Python協(xié)程實(shí)現(xiàn)
正如剛才所寫的代碼示例脸甘,python 2.5
中引入 yield/send
表達(dá)式用于實(shí)現(xiàn)協(xié)程恳啥,但這種通過生成器的方式使用協(xié)程不夠優(yōu)雅。
python 3.5
之后引入async/await
丹诀,簡化了協(xié)程的使用并且更加便于理解钝的。
Go語言協(xié)程實(shí)現(xiàn)
Golang 在語言層面實(shí)現(xiàn)了對協(xié)程的支持,Goroutine
是協(xié)程在 Go 語言中的實(shí)現(xiàn)铆遭, 在 Go 語言中每一個并發(fā)的執(zhí)行單元叫作一個 Goroutine
硝桩,Go 程序可以輕松創(chuàng)建成百上千個協(xié)程并發(fā)執(zhí)行。
Go 協(xié)程調(diào)度器有三個重要數(shù)據(jù)結(jié)構(gòu):
G 表示
Goroutine
疚脐,它是一個待執(zhí)行的任務(wù)亿柑;M 表示操作系統(tǒng)的線程,它由操作系統(tǒng)的調(diào)度器調(diào)度和管理棍弄;
P 表示處理器
Processor
望薄,它可以被看做運(yùn)行在線程上的本地調(diào)度器疟游;
Go 調(diào)度器最多可以創(chuàng)建 10000 個線程,但可以通過設(shè)置 GOMAXPROCS
變量指能夠正常運(yùn)行的運(yùn)行痕支, 這個變量的默認(rèn)值 等于 CPU 個數(shù)颁虐,也就是線程數(shù)等于 CPU 核數(shù),這樣不會觸發(fā)操作系統(tǒng)的線程調(diào)度和上下文切換卧须,所有的調(diào)度由 Go 語言調(diào)度器觸發(fā)另绩,都是在用戶態(tài),減少了非常多的調(diào)用開銷花嘶。
總結(jié)
這篇文章講解和對比了進(jìn)程笋籽、線程的概念,同時通過進(jìn)程窺探到操作系統(tǒng)內(nèi)存管理的冰山一角椭员,另外還講解了具體到 Linux 系統(tǒng)下線程的實(shí)現(xiàn)現(xiàn)狀车海,順勢引出了輕量級進(jìn)程的概念。最后著重說明了大部分同學(xué)不太了解的協(xié)程隘击,通過對比不同的服務(wù)模型侍芝,帶你了解協(xié)程的特點(diǎn)。
說明一下埋同,文中的圖片都是我手繪的州叠,原圖我沒打水印方便閱讀,不過微信發(fā)出去之后會對圖片壓縮和打上水印凶赁,如果你想要下載高清原圖咧栗,留著時不時溫故知新,我也樂意無償提供原圖給你下載哟冬,在公眾號「后端技術(shù)學(xué)堂」回復(fù)「進(jìn)程」即可獲取楼熄,對知識的理解有時候真的是一圖勝千言,也是我文章的價值所在浩峡。
再聊兩句
最近公眾號改版了可岂,我發(fā)的文章可能不能出現(xiàn)在你的消息列表中,不敢稱自己是小號主翰灾,對于我這種 mini 號主影響還是很大的缕粹,寫公眾號文章的都知道,發(fā)出去的文章都想讓更多人看到纸淮,才有機(jī)會得到持續(xù)的正向反饋平斩,激勵我持續(xù)創(chuàng)作和分享。
所以咽块,如果覺得文章寫的還行绘面,對你有點(diǎn)幫助,就動動手指點(diǎn)個「在看」順手加個「星標(biāo)」讓我們還能及時再見。
感謝各位的閱讀揭璃,文章的目的是分享對知識的理解晚凿,技術(shù)類文章我都會反復(fù)求證以求最大程度保證準(zhǔn)確性,若文中出現(xiàn)明顯紕漏也歡迎指出瘦馍,我們一起在探討中學(xué)習(xí)歼秽。
今天的技術(shù)分享就到這里,我們下期再見情组。