定義:
context switch:每秒上下文切換的次數
什么是CPU上下文:
我們都知道,Linux 是一個多任務操作系統,它支持遠大於 CPU 數量的任務同時運行。當然,這些任務實際上並不是真的在同時運行,而是因為系統在很短的時間內,將 CPU 輪流分配給它們,造成多任務同時運行的錯覺。
而在每個任務運行前,CPU 都需要知道任務從哪里加載、又從哪里開始運行,也就是說,需要系統事先幫它設置好 CPU 寄存器和程序計數器(Program Counter,PC)。
CPU 寄存器,是 CPU 內置的容量小、但速度極快的內存。而程序計數器,則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。它們都是 CPU 在運行任何任務前,必須的依賴環境,因此也被叫做 CPU 上下文。
根據任務的不同,CPU的上下文切換可以分為不同的場景:
- 進程上下文切換
- 線程上下文切換(這個是我們性能測試時關注的點)
- 中斷上下文切換
為什么要關注進程的上下文切換:
進程是由內核來管理和調度的,進程的切換只能發生在內核態。所以,進程的上下文不僅包括了虛擬內存、棧、全局變量等用戶空間的資源,還包括了內核堆棧、寄存器等內核空間的狀態。
因此,進程的上下文切換就比系統調用時多了一步:在保存當前進程的內核狀態和 CPU 寄存器之前,需要先把該進程的虛擬內存、棧等保存下來;而加載了下一進程的內核態后,還需要刷新進程的虛擬內存和用戶棧。
如下圖所示,保存上下文和恢復上下文的過程並不是“免費”的,需要內核在 CPU 上運行才能完成:
每次上下文切換都需要幾十納秒到數微秒的 CPU 時間。這個時間還是相當可觀的,特別是在進程上下文切換次數較多的情況下,很容易導致 CPU 將大量時間耗費在寄存器、內核棧以及虛擬內存等資源的保存和恢復上,進而大大縮短了真正運行進程的時間。這也正是,導致平均負載升高的一個重要因素。
另外,我們知道, Linux 通過 TLB(Translation Lookaside Buffer)來管理虛擬內存到物理內存的映射關系。當虛擬內存更新后,TLB 也需要刷新,內存的訪問也會隨之變慢。特別是在多處理器系統上,緩存是被多個處理器共享的,刷新緩存不僅會影響當前處理器的進程,還會影響共享緩存的其他處理器的進程。
知道了進程上下文切換潛在的性能問題后,我們再來看,究竟什么時候會切換進程上下文:
顯然,進程切換時才需要切換上下文,換句話說,只有在進程調度的時候,才需要切換上下文。Linux 為每個 CPU 都維護了一個就緒隊列,將活躍進程(即正在運行和正在等待 CPU 的進程)按照優先級和等待 CPU 的時間排序,然后選擇最需要 CPU 的進程,也就是優先級最高和等待 CPU 時間最長的進程來運行。
進程在什么時候才會被調度到 CPU 上運行呢?
- 最容易想到的一個時機,就是進程執行完終止了,它之前使用的 CPU 會釋放出來,這個時候再從就緒隊列里,拿一個新的進程過來運行
- 為了保證所有進程可以公平調度,CPU 時間被划分為一段段的時間片,這些時間片再被輪流分配給各個進程。這樣,當某個進程的時間片耗盡了,就會被系統掛起,切換到其它正在等待 CPU 的進程運行
- 進程在系統資源不足(比如內存不足)時,要等到資源滿足后才可以運行,這個時候進程也會被掛起,並由系統調度其他進程運行
- 當進程通過睡眠函數 sleep 這樣的方法將自己主動掛起時,自然也會重新調度
- 當有優先級更高的進程運行時,為了保證高優先級進程的運行,當前進程會被掛起,由高優先級進程來運行
- 發生硬件中斷時,CPU 上的進程會被中斷掛起,轉而執行內核中的中斷服務程序
如果出現(cs)性能問題那么主要從最后五個方面排查
大體上分為兩方面:
- cpu 不足
- 進程或線程自己需要切換
針對cpu不足有兩種情況;
1.1、本進程消耗 --解決自身問題
1.2、其他進程消耗 --解決別人那問題
針對進程或線程自己需要切換:
分析自己進程或線程的問題
實例分析:
一、cpu不足
1.1情況模擬:
使用top命令查看
看到CPU被消耗光了,確實也都在用戶空間us cpu 使用vmstat 查看(CPU空閑時也差不多有2000多的切換)
從vmstat上來看,並看不到多大的CS。但是已經可以看到CPU隊列高、CPU使用率高了
使用命令pidstat -w -t -p 15288 查看
從上圖可以看到nvcswch/s已經有值了,我這里是每秒刷新一次。也就是說每個線程大概每秒被動切換了50多次。
你有沒有覺得,這個值看起來似乎並不大是不是?並且在vmstat中也沒看到多高的CS切換呀,因為在CPU空閑時也差不多有2000多的切換呀。(在我這個環境中是這樣的數值,在其他環境中,這個值會有變化。)
為什么會出現這種情況呢。因為現在CPU都被15288搶占,本來操作系統正常的CS都搶不到CPU了,只被15288里面的幾個進程消耗掉了。那正常的CS就連CPU都搶不到,當然CS也就減少了,這時整個系統其實是處在癱瘓的狀態的。
1.2情況模擬:
cpu 被其他進程消耗,導致本進程被動切換
使用top命令查看
消耗cpu的進程有兩個16550 16551
然后啟動我們自己的模擬進程
查看模擬進程的上下文切換情況:
從命令看來cs比平時低很多
看到切換了,並且切換的意願不是很強烈。被動切換
那么現在殺掉其他兩個進程 再來查看切換
是不是少了很多
二、進程或線程自己需要切換
自動切換和服務代碼有關
這里啟動了400個線程來達到預期結果。 這里可以看到sy cpu 過高 是因為模擬代碼中僅僅是sleep 如果失敗業務代碼的話就是us cpu 了
這里來看一下上下文切換情況:
這種情況是資源切換上下文的情況。
1 /* 2 * context_switch - switch to the new MM and the new thread's register state. 3 */ 4 static __always_inline struct rq * 5 context_switch(struct rq *rq, struct task_struct *prev, 6 struct task_struct *next, struct rq_flags *rf) 7 { 8 prepare_task_switch(rq, prev, next); 9 10 /* 11 * For paravirt, this is coupled with an exit in switch_to to 12 * combine the page table reload and the switch backend into 13 * one hypercall. 14 */ 15 arch_start_context_switch(prev); 16 17 /* 18 * kernel -> kernel lazy + transfer active 19 * user -> kernel lazy + mmgrab() active 20 * 21 * kernel -> user switch + mmdrop() active 22 * user -> user switch 23 */ 24 if (!next->mm) { // to kernel 25 enter_lazy_tlb(prev->active_mm, next); 26 27 next->active_mm = prev->active_mm; 28 if (prev->mm) // from user 29 mmgrab(prev->active_mm); 30 else 31 prev->active_mm = NULL; 32 } else { // to user 33 membarrier_switch_mm(rq, prev->active_mm, next->mm); 34 /* 35 * sys_membarrier() requires an smp_mb() between setting 36 * rq->curr / membarrier_switch_mm() and returning to userspace. 37 * 38 * The below provides this either through switch_mm(), or in 39 * case 'prev->active_mm == next->mm' through 40 * finish_task_switch()'s mmdrop(). 41 */ 42 switch_mm_irqs_off(prev->active_mm, next->mm, next); 43 44 if (!prev->mm) { // from kernel 45 /* will mmdrop() in finish_task_switch(). */ 46 rq->prev_mm = prev->active_mm; 47 prev->active_mm = NULL; 48 } 49 } 50 51 rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP); 52 53 prepare_lock_switch(rq, next, rf); 54 55 /* Here we just switch the register state and the stack. */ 56 switch_to(prev, next, prev); 57 barrier(); 58 59 return finish_task_switch(prev); 60 }
這里是切換上下文源碼。
- context_switch :切換上下文的方法(下面是切換上下文的三個參數)
- rq :指向切換上下文發生cpu的運行隊列
- prev:被切進程
- next:切向進程
文章摘抄自:性能分析之自願和非自願上下文切換 (
)深入理解CPU上下文切換(知乎 https://zhuanlan.zhihu.com/p/99923968)