前言
在操作系統(tǒng)課程的學(xué)習(xí)中,很多人對(duì)進(jìn)程線程有大體的認(rèn)識(shí),但操作系統(tǒng)教材更偏向于理論敘述,本文會(huì)結(jié)合 Linux 系統(tǒng)實(shí)現(xiàn)分析,更加印象深刻。
同時(shí),大部分人都接觸進(jìn)程和線程比較多,對(duì)協(xié)程知之甚少,然而最近協(xié)程并發(fā)編程技術(shù)火熱起來,希望讀完本文你對(duì)協(xié)程也有一個(gè)基本的了解。
話不多說,我們馬上進(jìn)入本文的學(xué)習(xí)。
進(jìn)程
關(guān)于進(jìn)程和內(nèi)存管理我之前有一篇文章單獨(dú)講解過,感興趣的同學(xué)點(diǎn)這里《別再說你不懂Linux內(nèi)存管理了,10張圖給你安排的明明白白!》 這里再挑選一部分和本文相關(guān)的內(nèi)容學(xué)習(xí),溫故而知新。
首先還是說下「程序」的概念,程序是一些保存在磁盤上的指令的有序集合,是靜態(tài)的。進(jìn)程是程序執(zhí)行的過程,包括了動(dòng)態(tài)創(chuàng)建、調(diào)度和消亡的整個(gè)過程,進(jìn)程是程序資源管理的最小單位。
進(jìn)程與資源
那么進(jìn)程都管理哪些資源呢?通常包括內(nèi)存資源、IO資源、信號(hào)處理等部分。
程序和進(jìn)程
篇幅有限著重說一下內(nèi)存管理,進(jìn)程運(yùn)行起來必然會(huì)涉及到對(duì)內(nèi)存資源的管理。內(nèi)存資源有限,操作系統(tǒng)采用虛擬內(nèi)存技術(shù),把進(jìn)程虛擬地址空間劃分成用戶空間和內(nèi)核空間。
地址空間
4GB 的進(jìn)程虛擬地址空間被分成兩部分:用戶空間和內(nèi)核空間
用戶空間內(nèi)核空間
用戶空間
用戶空間按照訪問屬性一致的地址空間存放在一起的原則,劃分成 5個(gè)不同的內(nèi)存區(qū)域。訪問屬性指的是“可讀、可寫、可執(zhí)行等 。
代碼段
代碼段是用來存放可執(zhí)行文件的操作指令,可執(zhí)行程序在內(nèi)存中的鏡像。代碼段需要防止在運(yùn)行時(shí)被非法修改,所以只準(zhǔn)許讀取操作,它是不可寫的。
數(shù)據(jù)段
數(shù)據(jù)段用來存放可執(zhí)行文件中已初始化全局變量,換句話說就是存放程序靜態(tài)分配的變量和全局變量。
BSS段
BSS段包含了程序中未初始化的全局變量,在內(nèi)存中 bss 段全部置零。
堆 heap
堆是用于存放進(jìn)程運(yùn)行中被動(dòng)態(tài)分配的內(nèi)存段,它的大小并不固定,可動(dòng)態(tài)擴(kuò)張或縮減。當(dāng)進(jìn)程調(diào)用malloc等函數(shù)分配內(nèi)存時(shí),新分配的內(nèi)存就被動(dòng)態(tài)添加到堆上(堆被擴(kuò)張);當(dāng)利用free等函數(shù)釋放內(nèi)存時(shí),被釋放的內(nèi)存從堆中被剔除(堆被縮減)
棧 stack
棧是用戶存放程序臨時(shí)創(chuàng)建的局部變量,也就是函數(shù)中定義的變量(但不包括 static 聲明的變量,static意味著在數(shù)據(jù)段中存放變量)。除此以外,在函數(shù)被調(diào)用時(shí),其參數(shù)也會(huì)被壓入發(fā)起調(diào)用的進(jìn)程棧中,并且待到調(diào)用結(jié)束后,函數(shù)的返回值也會(huì)被存放回棧中。由于棧的先進(jìn)后出特點(diǎn),所以棧特別方便用來保存/恢復(fù)調(diào)用現(xiàn)場(chǎng)。從這個(gè)意義上講,我們可以把堆棧看成一個(gè)寄存、交換臨時(shí)數(shù)據(jù)的內(nèi)存區(qū)。
上述幾種內(nèi)存區(qū)域中數(shù)據(jù)段、BSS 段、堆通常是被連續(xù)存儲(chǔ)在內(nèi)存中,在位置上是連續(xù)的,而代碼段和棧往往會(huì)被獨(dú)立存放。堆和棧兩個(gè)區(qū)域在 i386 體系結(jié)構(gòu)中棧向下擴(kuò)展、堆向上擴(kuò)展,相對(duì)而生。
程序內(nèi)存分段
你也可以再 linux 下用size 命令查看編譯后程序的各個(gè)內(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)存地址空間,總計(jì) 1G 的容量, 包括了內(nèi)核鏡像、物理頁面表、驅(qū)動(dòng)程序等運(yùn)行在內(nèi)核空間 。
內(nèi)核空間地址映射
線程
線程是操作操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。線程被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位,一個(gè)進(jìn)程內(nèi)可以包含多個(gè)線程,線程是資源調(diào)度的最小單位。
多線程程序模型
線程資源和開銷
同一進(jìn)程中的多條線程共享該進(jìn)程中的全部系統(tǒng)資源,如虛擬地址空間,文件描述符文件描述符和信號(hào)處理等等。但同一進(jìn)程中的多個(gè)線程有各自的調(diào)用棧、寄存器環(huán)境、線程本地存儲(chǔ)等信息。
線程創(chuàng)建的開銷主要是線程堆棧的建立,分配內(nèi)存的開銷。這些開銷并不大,最大的開銷發(fā)生在線程上下文切換的時(shí)候。
線程切換
線程分類
還記得剛開始我們講的內(nèi)核空間和用戶空間概念嗎?線程按照實(shí)現(xiàn)位置和方式的不同,也分為用戶級(jí)線程和內(nèi)核線程,下面一起來看下這兩類線程的差異和特點(diǎn)。
用戶級(jí)線程
實(shí)現(xiàn)在用戶空間的線程稱為用戶級(jí)線程。用戶線程是完全建立在用戶空間的線程庫,用戶線程的創(chuàng)建、調(diào)度、同步和銷毀全由用戶空間的庫函數(shù)完成,不需要內(nèi)核的參與,因此這種線程的系統(tǒng)資源消耗非常低,且非常的高效。
特點(diǎn)
用戶線級(jí)線程只能參與競(jìng)爭(zhēng)該進(jìn)程的處理器資源,不能參與全局處理器資源的競(jìng)爭(zhēng)。
用戶級(jí)線程切換都在用戶空間進(jìn)行,開銷極低。
用戶級(jí)線程調(diào)度器在用戶空間的線程庫實(shí)現(xiàn),內(nèi)核的調(diào)度對(duì)象是進(jìn)程本身,內(nèi)核并不知道用戶線程的存在。
用戶線程圖解
缺點(diǎn)
如果觸發(fā)了引起阻塞的系統(tǒng)調(diào)用的調(diào)用,會(huì)立即阻塞該線程所屬的整個(gè)進(jìn)程。
系統(tǒng)只看到進(jìn)程看不到用戶線程,所以只有一個(gè)處理器內(nèi)核會(huì)被分配給該進(jìn)程 ,也就不能發(fā)揮多核 CPU 的優(yōu)勢(shì) 。
內(nèi)核級(jí)線程
內(nèi)核線程建立和銷毀都是由操作系統(tǒng)負(fù)責(zé)、通過系統(tǒng)調(diào)用完成,內(nèi)核維護(hù)進(jìn)程及線程的上下文信息以及線程切換。
特點(diǎn)
內(nèi)核級(jí)線級(jí)能參與全局的多核處理器資源分配,充分利用多核 CPU 優(yōu)勢(shì)。
每個(gè)內(nèi)核線程都可被內(nèi)核調(diào)度,因?yàn)榫€程的創(chuàng)建、撤銷和切換都是由內(nèi)核管理的。
一個(gè)內(nèi)核線程阻塞與他同屬一個(gè)進(jìn)程的線程仍然能繼續(xù)運(yùn)行。
內(nèi)核線程圖解
缺點(diǎn)
內(nèi)核級(jí)線程調(diào)度開銷較大。調(diào)度內(nèi)核線程的代價(jià)可能和調(diào)度進(jìn)程差不多昂貴,代價(jià)要比用戶級(jí)線程大很多。
線程表是存放在操作系統(tǒng)固定的表格空間或者堆棧空間里,所以內(nèi)核級(jí)線程的數(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)度時(shí)也是當(dāng)做進(jìn)程來調(diào)度的。Linux所謂的線程其實(shí)是與其他進(jìn)程共享資源的輕量級(jí)進(jìn)程。
為什么說是輕量級(jí)呢?在于它只有一個(gè)最小的執(zhí)行上下文和調(diào)度程序所需的統(tǒng)計(jì)信息,它只帶有進(jìn)程執(zhí)行相關(guān)的信息,與父進(jìn)程共享進(jìn)程地址空間 。
輕量級(jí)進(jìn)程
輕量級(jí)線程 Light-weight Process簡(jiǎn)稱LWP ,是一種由內(nèi)核支持的用戶線程,每一個(gè)輕量級(jí)進(jìn)程都與一個(gè)特定的內(nèi)核線程關(guān)聯(lián)。
它是基于內(nèi)核線程的高級(jí)抽象,系統(tǒng)只有先支持內(nèi)核線程才能有 LWP。每一個(gè)進(jìn)程有一個(gè)或多個(gè) LWPs ,每個(gè)LWP 由一個(gè)內(nèi)核線程支持,在這種實(shí)現(xiàn)的操作系統(tǒng)中 LWP 就是用戶線程。
輕量級(jí)進(jìn)程
輕量級(jí)進(jìn)程最早在Linux 內(nèi)核 2.0.x 版本就已實(shí)現(xiàn),應(yīng)用程序通過一個(gè)統(tǒng)一的 clone() 系統(tǒng)調(diào)用接口,用不同的參數(shù)指定創(chuàng)建的進(jìn)程是輕量進(jìn)程還是普通進(jìn)程。
特點(diǎn)和缺點(diǎn)
由于輕量輕量級(jí)進(jìn)程基于內(nèi)核線程實(shí)現(xiàn),因此它的特點(diǎn)和缺點(diǎn)就是內(nèi)核線程的缺點(diǎn),這里不再贅述。
查看 LWP 信息
輕量級(jí)線程也沒什么神秘的,還記得我在這篇文章《資深程序員總結(jié):分析Linux進(jìn)程的6個(gè)方法,我全都告訴你》教你的方法嗎?我們用 Linux 的 pstack 命令可以查看進(jìn)程的輕量級(jí)線程 LWP 信息。下圖的黃色字體就是打印出的輕量級(jí)線程 ID ,以及該線程的調(diào)用堆棧信息,從最新的棧幀開始往下排列。
用法示例:pstack pid
pstack查看lwp
協(xié)程
協(xié)程的知名度好像不是很高,在以前我們談?wù)摳卟l(fā),大部分人都知道利用多線程和多進(jìn)程部署服務(wù),提高服務(wù)性能,但一般不會(huì)提到協(xié)程。其實(shí)協(xié)程的概念出來的比線程還早,只不過最近才被人們更多的提起。
協(xié)程之所以最近被大家熟知,個(gè)人覺得是 Python 和 Go 從語言層面提供了對(duì)協(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)的高耗時(shí)任務(wù),高耗時(shí)是相對(duì) CPU 計(jì)算邏輯處理型任務(wù)來說,兩者的處理時(shí)間差距不是一個(gè)數(shù)量級(jí)的。
IO 密集型服務(wù)的瓶頸不在 CPU 處理速度,而在于盡可能快速的完成高并發(fā)、多連接下的數(shù)據(jù)讀寫。
以前有兩種解決方案:
如果用多線程,高并發(fā)場(chǎng)景的大量 IO 等待會(huì)導(dǎo)致多線程被頻繁掛起和切換,非常消耗系統(tǒng)資源,同時(shí)多線程訪問共享資源存在競(jìng)爭(zhēng)問題。
如果用多進(jìn)程,不僅存在頻繁調(diào)度切換問題,同時(shí)還會(huì)存在每個(gè)進(jìn)程資源不共享的問題,需要額外引入進(jìn)程間通信機(jī)制來解決。
協(xié)程出現(xiàn)給高并發(fā)和 IO 密集型服務(wù)開發(fā)提供了另一種選擇。
當(dāng)然,世界上沒有技術(shù)銀彈。在這里我想把協(xié)程這把鑰匙交到你手中,但是它也不是萬能鑰匙,最好的解決方案是貼合自身業(yè)務(wù)類型做出最優(yōu)選擇,不一定就選擇一種模型,有時(shí)候是幾種模型的組合,比如多線程搭配協(xié)程是常見的組合。
什么是協(xié)程
那什么是協(xié)程呢?協(xié)程 Coroutines 是一種比線程更加輕量級(jí)的微線程。類比一個(gè)進(jìn)程可以擁有多個(gè)線程,一個(gè)線程也可以擁有多個(gè)協(xié)程,因此協(xié)程又稱微線程和纖程。
協(xié)程圖解
可以粗略的把協(xié)程理解成子程序調(diào)用,每個(gè)子程序都可以在一個(gè)單獨(dú)的協(xié)程內(nèi)執(zhí)行。
協(xié)程子程序模型
調(diào)度開銷
線程是被內(nèi)核所調(diào)度,線程被調(diào)度切換到另一個(gè)線程上下文的時(shí)候,需要保存一個(gè)用戶線程的狀態(tài)到內(nèi)存,恢復(fù)另一個(gè)線程狀態(tài)到寄存器,然后更新調(diào)度器的數(shù)據(jù)結(jié)構(gòu),這幾步操作設(shè)計(jì)用戶態(tài)到內(nèi)核態(tài)轉(zhuǎn)換,開銷比較多。
線程切換
協(xié)程的調(diào)度完全由用戶控制,協(xié)程擁有自己的寄存器上下文和棧,協(xié)程調(diào)度切換時(shí),將寄存器上下文和棧保存到其他地方,在切回來的時(shí)候,恢復(fù)先前保存的寄存器上下文和棧,直接操作用戶空間棧,完全沒有內(nèi)核切換的開銷。
協(xié)程切換
動(dòng)態(tài)協(xié)程棧
協(xié)程擁有自己的寄存器上下文和棧,協(xié)程調(diào)度切換時(shí)將寄存器上下文和棧保存下來,在切回來的時(shí)候,恢復(fù)先前保存的寄存器的上下文和棧。
Goroutine 是 Golang 的協(xié)程實(shí)現(xiàn)。Goroutine 的棧只有 2KB大小,而且是動(dòng)態(tài)伸縮的,可以按需調(diào)整大小,最大可達(dá) 1G 相比線程來說既不浪費(fèi)又靈活了很多,可以說是相當(dāng)?shù)膎ice了!
線程也都有一個(gè)固定大小的內(nèi)存塊來做棧,一般會(huì)是 2MB 大小,線程棧會(huì)用來存儲(chǔ)線程上下文信息。2MB 的線程棧和協(xié)程棧相比大了很多。
線程和協(xié)程棧對(duì)比
協(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 ,簡(jiǎn)化了協(xié)程的使用并且更加便于理解。
Go語言協(xié)程實(shí)現(xiàn)
Golang 在語言層面實(shí)現(xiàn)了對(duì)協(xié)程的支持,Goroutine 是協(xié)程在 Go 語言中的實(shí)現(xiàn), 在 Go 語言中每一個(gè)并發(fā)的執(zhí)行單元叫作一個(gè) Goroutine ,Go 程序可以輕松創(chuàng)建成百上千個(gè)協(xié)程并發(fā)執(zhí)行。
Go 協(xié)程調(diào)度器有三個(gè)重要數(shù)據(jù)結(jié)構(gòu):
G 表示 Goroutine ,它是一個(gè)待執(zhí)行的任務(wù);
M 表示操作系統(tǒng)的線程,它由操作系統(tǒng)的調(diào)度器調(diào)度和管理;
P 表示處理器 Processor,它可以被看做運(yùn)行在線程上的本地調(diào)度器;
協(xié)程調(diào)度
Go 調(diào)度器最多可以創(chuàng)建 10000 個(gè)線程,但可以通過設(shè)置 GOMAXPROCS 變量指能夠正常運(yùn)行的線程數(shù), 這個(gè)變量的默認(rèn)值 等于 CPU 個(gè)數(shù),也就是線程數(shù)等于 CPU 核數(shù),這樣不會(huì)觸發(fā)操作系統(tǒng)的線程調(diào)度和上下文切換,所有的調(diào)度由 Go 語言調(diào)度器觸發(fā),都是在用戶態(tài),減少了非常多的調(diào)用開銷。
總結(jié)
這篇文章講解和對(duì)比了進(jìn)程、線程的概念,同時(shí)通過進(jìn)程窺探到操作系統(tǒng)內(nèi)存管理的冰山一角,另外還講解了具體到 Linux 系統(tǒng)下線程的實(shí)現(xiàn)現(xiàn)狀,順勢(shì)引出了輕量級(jí)進(jìn)程的概念。最后著重說明了大部分同學(xué)不太了解的協(xié)程,通過對(duì)比不同的服務(wù)模型,帶你了解協(xié)程的特點(diǎn)。
? ? ? ?責(zé)任編輯:pj
評(píng)論