前言
前面的幾章, 我們有提到個名詞“上下文切換
”
這是個什么東西呢?
其實很好理解, 我們看一本書籍時,往往間隔的看(很少有人一天看完的吧?!), 那么如何保證自己下次是從上一次斷點處看呢? 顯而易見, 我們用書簽!
“上下文切換”就是計算機的“書簽”.
本章,我們來學(xué)習(xí)“上下文切換”原理以及如何減少“上下文切換”.
在此之前, 還得先復(fù)習(xí)下前面提到的進程和線程, 因為上下文切換的對象就是進程和線程
.
線程與進程
進程是操作系統(tǒng)的管理單位,也是系統(tǒng)分配資源的基本單位
;
線程則是進程的管理單位, 也是是CPU調(diào)度的基本單位
;
線程依托于進程, 一個進程至少包含一個線程;
線程在Linux系統(tǒng)中就是能并行運行并且與他們的父進程(創(chuàng)建他們的進程)共享
同一地址空間(一段內(nèi)存區(qū)域)和其他資源的輕量級進程
一個線程指的是進程中一個單一順序的控制流
不管是在單線程還是多線程中, 每個線程都有
- 一個程序計數(shù)器
記錄要執(zhí)行的下一條指令;
程序計數(shù)器是一個專用的寄存器,用于表明指令序列中 CPU 正在執(zhí)行的位置,存的值為正在執(zhí)行的指令的位置或者下一個將要被執(zhí)行的指令的位置,具體依賴于特定的系統(tǒng).
- 一組寄存器
保存當(dāng)前線程的工作變量
寄存器是CPU 內(nèi)部數(shù)量較少但是速度很快的內(nèi)存(與之對應(yīng)的是 CPU 外部相對較慢的 RAM 主內(nèi)存).寄存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程序運行的速度.
- 堆棧
記錄執(zhí)行歷史,其中每一幀保存了一個已經(jīng)調(diào)用但未返回的過程
上下文切換
上下文切換針對的是多任務(wù)處理的系統(tǒng)
,
多任務(wù)處理系統(tǒng)指的是同時運行兩個或多個程序的系統(tǒng)
.
在多任務(wù)處理系統(tǒng)中, CPU需要處理所有程序的操作, 當(dāng)用戶來回切換它們時, 需要記錄這些程序執(zhí)行到哪里.
上下文切換就是這樣一個過程:
允許CPU記錄并恢復(fù)各種正在運行程序的狀態(tài),使它能夠完成切換操作
多任務(wù)系統(tǒng)往往需要同時執(zhí)行多道作業(yè), 作業(yè)數(shù)往往大于機器的CPU數(shù).
然而, 一顆CPU同時只能執(zhí)行一項任務(wù), 如何讓用戶感覺這些任務(wù)正在同時進行呢?
操作系統(tǒng)的設(shè)計者 巧妙地利用了時間片輪轉(zhuǎn)
的方式:
CPU給每個任務(wù)都運行一定的時間, 然后把當(dāng)前任務(wù)的狀態(tài)保存下來,
再加載下一任務(wù)的狀態(tài)后, 繼續(xù)服務(wù)下一任務(wù).
任務(wù)的狀態(tài)保存及再加載
,這段過程就叫做上下文切換.
時間片輪轉(zhuǎn)的方式使多個任務(wù)在同一顆CPU上執(zhí)行
變成了可能.
上下文切換(有時也稱做進程切換或任務(wù)切換)是指:
CPU從一個進程或線程切換到另一個進程或線程.
上下文切換可以認為是內(nèi)核(操作系統(tǒng)的核心)在 CPU 上對于進程(或線程)進行以下的活動:
- 掛起一個進程
將這個進程在 CPU 中的狀態(tài)(上下文)存儲于內(nèi)存中的某處
- 恢復(fù)一個進程
在內(nèi)存中檢索下一個進程的上下文并將其在 CPU 的寄存器中恢復(fù)
- 跳轉(zhuǎn)到程序計數(shù)器所指向的位置
即跳轉(zhuǎn)到進程被中斷時的代碼行, 以恢復(fù)該進程
切換種類
上下文切換在不同的場合有不同的含義
上下文切換種類 | 描述 |
---|---|
線程切換 | 同一進程中的兩個線程之間的切換 |
進程切換 | 兩個進程之間的切換 |
模式切換 | 在給定線程中,用戶模式和內(nèi)核模式的切換 |
地址空間切換 | 將虛擬內(nèi)存切換到物理內(nèi)存 |
切換步驟
在上下文切換過程中, CPU會停止處理當(dāng)前運行的程序, 并保存當(dāng)前程序運行的具體位置以便之后繼續(xù)運行.
從這個角度來看, 上下文切換有點像我們同時閱讀幾本書,來回切換書本的同時, 我們需要記住每本書當(dāng)前讀到的頁碼.
在程序中, 上下文切換過程中的“頁碼”信息是保存在進程控制塊(PCB, process control block)中的, PCB還經(jīng)常被稱作“切換楨”(switchframe).
“頁碼”信息會一直保存到CPU的內(nèi)存中,直到他們被再次使用.
PCB通常是系統(tǒng)內(nèi)存占用區(qū)中的一個連續(xù)存區(qū),
它存放著操作系統(tǒng)用于描述進程情況及控制進程運行所需的全部信息.
它的作用主要是“使一個在多道程序環(huán)境下不能獨立進行的程序(含數(shù)據(jù)), 成為一個能獨立運行的基本單位, 一個能與其他進程并發(fā)執(zhí)行的進程.
或者說, 操作系統(tǒng)是根據(jù)PCB來對并發(fā)執(zhí)行的進程進行控制和管理.
舉個例子, 兩個進程A和B, 系統(tǒng)需要從A切換到B:
- 保存進程A的狀態(tài)(寄存器和操作系統(tǒng)數(shù)據(jù))燕锥;
- 更新PCB中的信息,對進程A的“運行態(tài)”做出相應(yīng)更改溺健;
- 將進程A的PCB放入相關(guān)狀態(tài)的隊列;
- 將進程B的PCB信息改為“運行態(tài)”,并執(zhí)行進程B蜘醋;
- B執(zhí)行完后,從隊列中取出進程A的PCB,恢復(fù)進程A被切換時的上下文,繼續(xù)執(zhí)行A邮府;
進程/線程上下文切換差異
線程切換和進程切換的步驟是有差異的
主要體現(xiàn)在性能和地址空間上.
- 性能上
進程的上下文切換需要兩步
- 切換頁目錄以使用新的地址空間;
- 切換內(nèi)核棧和硬件上下文溉奕;
線程的上下文切換只需一步
- 切換內(nèi)核棧和硬件上下文褂傀;
進程上下文切換比線程上下文切換多了步驟1, 所以明顯是進程切換代價大.
- 地址空間
對于Linux來說, 線程和進程的最大區(qū)別就在于地址空間.
線程的切換虛擬內(nèi)存空間依然是相同的,
而進程切換是不同的.
這兩種上下文切換的處理都是通過操作系統(tǒng)內(nèi)核來完成
的.
內(nèi)核的這種切換過程最顯著的性能損耗是將寄存器中的內(nèi)容切換出
.
一個正在執(zhí)行的進程包括程序計數(shù)器、寄存器加勤、變量的當(dāng)前值等,
這些數(shù)據(jù)都是保存在CPU的寄存器中的,并且這些寄存器只能是正在使用CPU的進程才能使用.
在進程切換時,
首先得保存上一個進程的這些數(shù)據(jù)
主要為了下次獲得CPU的使用權(quán)時,從上次的中斷處開始繼續(xù)順序執(zhí)行,
而不是返回到進程開始,否則每次進程重新獲得CPU時所處理的任務(wù)都是上一次的重復(fù),永遠也到不了進程的結(jié)束,
因為一個進程幾乎不可能執(zhí)行完所有任務(wù)后才釋放CPU
然后將本次獲得CPU的進程的這些數(shù)據(jù)裝入CPU的寄存器, 從上次斷點處繼續(xù)執(zhí)行剩下的任務(wù).
操作系統(tǒng)為了便于管理系統(tǒng)內(nèi)部進程,為每個進程創(chuàng)建了一張進程表項:
切換查看
在Linux系統(tǒng)下可以使用vmstat
命令來查看上下文切換的次數(shù)
vmstat 1指每秒統(tǒng)計一次, 其中cs列就是指上下文切換的數(shù)目.
切換原因
引起線程上下文切換的原因,主要存在三種情況如下:
- 中斷處理
在中斷處理中, 其他程序”打斷”了當(dāng)前正在運行的程序.
當(dāng)CPU接收到中斷請求時, 會在正在運行的程序和發(fā)起中斷請求的程序之間進行一次上下文切換.
中斷分為硬件中斷和軟件中斷,
軟件中斷包括因為IO阻塞仙辟、未搶到資源或者用戶代碼等原因,線程被掛起.
- 多任務(wù)處理
在多任務(wù)處理中,CPU會在不同程序之間來回切換,每個程序都有相應(yīng)的處理時間片,CPU在兩個時間片的間隔中進行上下文切換.
- 用戶態(tài)切換
對于一些操作系統(tǒng),當(dāng)進行用戶態(tài)切換時也會進行一次上下文切換,雖然這不是必須的.
對于我們經(jīng)常使用的搶占式操作系統(tǒng)
而言,引起線程上下文切換的原因大概有以下幾種:
當(dāng)前執(zhí)行任務(wù)的時間片用完之后,系統(tǒng)CPU正常調(diào)度下一個任務(wù)
當(dāng)前執(zhí)行任務(wù)碰到IO阻塞,調(diào)度器將此任務(wù)掛起,繼續(xù)下一任務(wù)
多個任務(wù)搶占鎖資源,當(dāng)前任務(wù)沒有搶到鎖資源,被調(diào)度器掛起,繼續(xù)下一任務(wù)
用戶代碼掛起當(dāng)前任務(wù),讓出CPU時間
硬件中斷
切換損耗
上下文切換會帶來直接和間接
兩種因素影響程序性能的消耗.
- 直接消耗
- CPU寄存器需要保存和加載
- 系統(tǒng)調(diào)度器的代碼需要執(zhí)行
- TLB實例需要重新加載
- CPU 的pipeline需要刷掉
- 間接消耗
- 多核的cache之間得共享數(shù)據(jù)
間接消耗對于程序的影響要看線程工作區(qū)操作數(shù)據(jù)的大小鳄梅;
如何減少切換
上下文切換會導(dǎo)致額外的開銷, 因此減少上下文切換次數(shù)便可以提高多線程程序的運行效率.
但上下文切換又分為2種:
- 讓步式上下文切換
即執(zhí)行線程主動釋放CPU,與鎖競爭嚴重程度成正比;
可通過減少鎖競爭來避免叠国;
- 搶占式上下文切換
指線程因分配的時間片用盡, 而被迫放棄CPU或者被其他優(yōu)先級更高的線程所搶占;
一般由于線程數(shù)大于CPU可用核心數(shù)引起;
可通過調(diào)整線程數(shù),適當(dāng)減少線程數(shù)來避免.
減少上下文切換的方法如下
- 無鎖并發(fā)
多線程競爭時,會引起上下文切換;
所以多線程處理數(shù)據(jù)時,可以用一些辦法來避免使用鎖;
如將數(shù)據(jù)的ID按照Hash取模分段,不同的線程處理不同段的數(shù)據(jù);
- CAS算法
Java的Atomic包使用CAS算法來更新數(shù)據(jù),而不需要加鎖戴尸;
- 最少線程
避免創(chuàng)建不需要的線程;
比如任務(wù)很少,但是創(chuàng)建了很多線程來處理,這樣會造成大量線程都處于等待狀態(tài)粟焊;
- 使用協(xié)程
在單線程里實現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個任務(wù)間的切換;
合理控制線程數(shù)目
合理設(shè)置線程數(shù)目,關(guān)鍵點是
- 盡量減少線程切換和管理的開支
要求線程數(shù)盡量少,這樣可以減少線程切換和管理的開支孙蒙;
- 最大化利用CPU
要求盡量多的線程,以保證CPU資源最大化的利用项棠;
針對不同的實際情況,我們需要采取合理的措施:
- 對于任務(wù)耗時短的情況:
要求線程盡量少,如果線程太多,有可能出現(xiàn)線程切換和管理的時間,大于任務(wù)執(zhí)行的時間, 那效率就低了;
- 對于耗時長的任務(wù):
要分是CPU任務(wù),還是IO等類型的任務(wù).
如果是CPU類型的任務(wù),線程數(shù)不宜太多挎峦;
如果是IO類型的任務(wù),線程多一些更好,可以更充分利用CPU.
- 對于高并發(fā),低耗時的情況:
建議少線程, 只要滿足并發(fā)即可;
因為上下文切換本來就多,并且高并發(fā)就意味著CPU是處于繁忙狀態(tài)的,
增加更多地線程也不會讓線程得到執(zhí)行時間片,反而會增加線程切換的開銷香追;
例如并發(fā)100,線程池可能設(shè)置為10就可以.
- 對于低并發(fā),高耗時的情況:
建議多線程, 保證有空閑線程, 接受新的任務(wù).
例如并發(fā)10,線程池可能就要設(shè)置為20;
- 對于高并發(fā)高耗時:
- 要分析任務(wù)類型坦胶;
- 增加排隊透典;
- 加大線程數(shù);