前言
LINUX完全注釋中的一段話
當一個進程在執行時,CPU的所有寄存器中的值、進程的狀態以及堆棧中的內容被稱 為該進程的上下文。當內核需要切換到另一個進程時,它需要保存當前進程的 所有狀態,即保存當前進程的上下文,以便在再次執行該進程時,能夠必得到切換時的狀態執行下去。在LINUX中,當前進程上下文均保存在進程的任務數據結 構中。在發生中斷時,內核就在被中斷進程的上下文中,在內核態下執行中斷服務例程。但同時會保留所有需要用到的資源,以便中繼服務結束時能恢復被中斷進程 的執行.
進程上下文切換
Linux 按照特權等級,把進程的運行空間分為內核空間和用戶空間,分別對應着下圖中。CPU特權等級的Ring0 和 Ring3。

- 內核空間(Ring 0)具有最高權限,可以直接訪問所有資源
- 用戶空間(Ring 3)只能訪問受限資源,不能直接訪問內存等硬件設備,必須通過系統調用陷入到內核中,才能訪問這些特權資源。
換個角度看,也就是說,進程即可以在用戶空間運行,又可以在內核空間中運行。進程在用戶空間運行是,被稱為進程的用戶態,而陷入內核空間的時候,被稱為進程的內核態。
從用戶態到內核態的轉變,需要通過系統調用
來完成,比如當我們查看文件內容時,就需要多次系統調用來完成:首先調用open()打開文件,然后調用read()讀取文件內容,並調用write()將內容寫到標准輸出,最后再調用close()關閉文件。
那么,系統調用的過程有沒有發生CPU上下文切換呢?答案是肯定的。
CPU寄存器里原來用戶態的指令位置,需要先保存起來。接着,為了執行內核態代碼,CPU寄存器需要更新為內核態指令的新位置。最后才是跳轉到內核態運行內核任務。
而系統調用結束后,CPU寄存器需要恢復原來保存的用戶態,然后再切換到用戶空間,繼續運行進程,所以一次系統調用的過程,其實是發生了兩次CPU上下文切換。
不過,需要注意的是,系統調用過程中,並不會涉及到虛擬內存等進程用戶態的資源,也不會切換進程。這跟我們通常所說的進程上下文切換是不一樣的。
- 進程上下文切換,是指從一個進程切換到另一個進程運行。
- 而系統調用過程中一直是同一個進程在運行。
所以,系統調用過程通常稱為特權模式切換
,而不是上下文切換
。但實際上系統調用過程中,CPU的上下文切換還是無法避免的。
進程上下文切換跟系統調用又有什么區別呢?
- 進程是由內核來管理和調度的,進程的切換只能發生在內核態。所以,進程的上下文不僅包含了虛擬內存、棧。全局變量等用戶空間的資源,還包含了內核堆棧、寄存器等內核空間狀態。因此,進程的上下文切換就比系統調用多了一步:在保存當前進程的內核狀態和CPU寄存器之前,需要先把該進程的虛擬內存、棧等保存下來;而加載了下一進程的內核態后,還需要刷新新進程的虛擬內存和用戶棧。
2. Linux通過TLB(Translation Lookaside Buffer)來管理虛擬內存到物理內存的映射關系。當虛擬內存更新后,TLB也需要刷新,內存的訪問也會隨之變慢。特別是在多處理器系統上,緩存是被多個處理器共享的,刷新緩存不僅會影響當前處理器的進程,還會影響共享緩存的其他處理器的進程。
顯然,進程切換時才需要切換上下文,換句話說,只有在進程調度的時候,才需要切換上下文。Linux為每個CPU都維護了一個就緒隊列,將獲取進程(即正在運行和等待CPU的進程)按照優先級和等待CPU的時間排序,然后選擇最需要CPU的進程,也就是優先級最高和等待CPU時間最長的進程來運行。
進程在什么時候才會被調度到CPU上運行:
- 為了保證所有進程可以得到公平調度,CPU時間片被划分為一段段的時間片,這些時間片再被輪流分配給各個進程。這樣,當某個進程的時間片耗盡了,就會被系統掛起,切換到其他正在等待CPU的進程運行。
- 進程在系統資源不足(比如內存不足)時,需要等到資源滿足后才可以運行,這個時候進程也會被掛起,並由系統調度其他進程運行。
- 當進程通過隨眠函數sleep這樣的方法將自己主動掛起時,自然也會重新調度。
- 當有優先級更高的進程運行時,為了保證高優先級進程的運行,當前進程會被掛起,由高優先級的進程來運行。
- 當發生硬件中斷時,CPU上的進程會被中斷掛起,轉而執行內核中中斷服務程序。
上下文切換機制分析
linux中進程調度時, 內核在選擇新進程之后進行搶占時, 通過context_switch完成進程上下文切換.
/* * context_switch - switch to the new MM and the new thread's register state. */ static __always_inline struct rq * context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next) { struct mm_struct *mm, *oldmm; /* 完成進程切換的准備工作 */ prepare_task_switch(rq, prev, next); mm = next->mm; oldmm = prev->active_mm; /* * For paravirt, this is coupled with an exit in switch_to to * combine the page table reload and the switch backend into * one hypercall. */ arch_start_context_switch(prev); /* 如果next是內核線程,則線程使用prev所使用的地址空間 * schedule( )函數把該線程設置為懶惰TLB模式 * 內核線程並不擁有自己的頁表集(task_struct->mm = NULL) * 它使用一個普通進程的頁表集 * 不過,沒有必要使一個用戶態線性地址對應的TLB表項無效 * 因為內核線程不訪問用戶態地址空間。 */ if (!mm) /* 內核線程無虛擬地址空間, mm = NULL*/ { /* 內核線程的active_mm為上一個進程的mm * 注意此時如果prev也是內核線程, * 則oldmm為NULL, 即next->active_mm也為NULL */ next->active_mm = oldmm; /* 增加mm的引用計數 */ atomic_inc(&oldmm->mm_count); /* 通知底層體系結構不需要切換虛擬地址空間的用戶部分 * 這種加速上下文切換的技術稱為惰性TBL */ enter_lazy_tlb(oldmm, next); } else /* 不是內核線程, 則需要切切換虛擬地址空間 */ switch_mm(oldmm, mm, next); /* 如果prev是內核線程或正在退出的進程 * 就重新設置prev->active_mm * 然后把指向prev內存描述符的指針保存到運行隊列的prev_mm字段中 */ if (!prev->mm) { /* 將prev的active_mm賦值和為空 */ prev->active_mm = NULL; /* 更新運行隊列的prev_mm成員 */ rq->prev_mm = oldmm; } /* * Since the runqueue lock will be released by the next * task (which is an invalid locking op but in the case * of the scheduler it's an obvious special-case), so we * do an early lockdep release here: */ lockdep_unpin_lock(&rq->lock); spin_release(&rq->lock.dep_map, 1, _THIS_IP_); /* Here we just switch the register state and the stack. * 切換進程的執行環境, 包括堆棧和寄存器 * 同時返回上一個執行的程序 * 相當於prev = witch_to(prev, next) */ switch_to(prev, next, prev); /* switch_to之后的代碼只有在 * 當前進程再次被選擇運行(恢復執行)時才會運行 * 而此時當前進程恢復執行時的上一個進程可能跟參數傳入時的prev不同 * 甚至可能是系統中任意一個隨機的進程 * 因此switch_to通過第三個參數將此進程返回 */ /* 路障同步, 一般用編譯器指令實現 * 確保了switch_to和finish_task_switch的執行順序 * 不會因為任何可能的優化而改變 */ barrier(); /* 進程切換之后的處理工作 */ return finish_task_switch(prev); }
switch_mm(): 把虛擬內存從一個進程映射切換到新進程中,switch_mm更換通過task_struct->mm描述的內存管理上下文, 該工作的細節取決於處理器, 主要包括加載頁表, 刷出地址轉換后備緩沖器(部分或者全部), 向內存管理單元(MMU)提供新的信息
switch_to():從上一個進程的處理器狀態切換到新進程的處理器狀態。這包括保存、恢復棧信息和寄存器信息,switch_to切換處理器寄存器的呢內容和內核棧(虛擬地址空間的用戶部分已經通過switch_mm變更, 其中也包括了用戶狀態下的棧, 因此switch_to不需要變更用戶棧, 只需變更內核棧),
線程上下文切換
線程與進程最大的區別在與,線程是調度的基本單位,而進程則是資源擁有的基本單位
。說白了,所謂內核中的任務調用,實際上的調度對象是線程;而進程只是給線程提供了虛擬內存、全局變量等資源。所以,對於現場和進程,我們可以這么理解:
- 當進程只有一個線程時,可以認為進程就等於線程。
- 當進程擁有多個線程時,這些線程會共享相同的虛擬內存和全局變量等資源。這些資源在上下文切換時是不需要修改的。
- 另外,線程也有自己的私有數據,比如棧和寄存器等,這些在上下文切換時也是需要保存的。
這么一來,線程的上下文切換其實就可以分為兩種情況:
第一種,前后倆個線程屬於不同進程,此時,由於資源不共享,所以切換過程就跟進程上下文切換是一樣的。
第二種,前后兩個線程屬於同一個進程,此時,應為虛擬內存是共享的,所以在切換時,虛擬內存這些資源就保持不動,只需要切換線程的私有數據,寄存器等不共享的數據。
到這里你應該也發現了,雖然同為上下文切換,但同進程內的線程切換,要比多進程間切換消耗更少的資源,而這,也正是多線程代替多進程的一個優勢。
中斷上下文切換
除了前面兩種上下文切換,還有一個場景也會也換CPU上下文,那就是中斷。
為了快速響應硬件的時間,中斷處理會打斷進程的正常調度和執行
,轉而調用中斷處理程序,響應設備時間。而在打斷其他進程時,就需要將進程當前的狀態保存下來,這樣在中斷結束后,進程仍然可以從原來的狀態恢復運行。
跟進程上下文不同,中斷上下文切換並不涉及到進程的用戶態。所以,即便中斷打斷了一個正處於用戶態的進程,也不需要保存和恢復這個進程的虛擬內存、全局變量等用戶態資源。中斷上下文,其實只包括內核態中斷服務程序所必須的狀態,包括CPU寄存器、內核堆棧、硬件中斷等參數等。
對同一個CPU來說,中斷處理比進程擁有更高的優先級,所以中斷上下文切換並不會與進程上下文切換同時發生。同樣的道理,由於中斷會大段正常進程的調度和執行,所以大部分中斷處理程序都短小精悍,以便盡可能快的執行結束。
另外,跟進程上下文切換一樣,中斷上下文切換也需要消耗CPU,切換次數過多也會耗費大量的CPU,甚至嚴重降低系統的整體性能。所以,當你發現中斷次數過多時,就需要注意去排查它是否會給你的系統帶來嚴重的性能問題。
中斷上下文切換過程
1)CPU 對中斷的硬件處理
CPU 從中斷控制器取得中斷向量
根據中斷向量從 IDT 中找到對應的中斷門
根據中斷門,找到中斷處理程序
在進入中斷處理程序前,需要將堆棧切換到內核堆棧。也就是將 TSS 中的 SS0、ESP0裝入SS、ESP
然后將原來的用戶空間堆棧(SS, ESP)、EFLAGS、返回地址(CS, EIP)壓入新的堆棧。
以上這一系列動作都由硬件完成
最后,才進入中斷處理程序,接下來,由 linux 內核處理
2)Linux 內核對中斷的處理
common_interrupt:
SAVE_ALL
pushl $ret_from_intr
SYMBOL_NAME_STR(call_do_IRQ):
jmp SYMBOL_NAME_STR(do_IRQ);
保存中斷來源號
調用 SAVE_ALL,保存各種寄存器
將 DS、ES 指向 __KERNEL_DS
將返回地址 ret_from_intr入棧
調用 do_IRQ進行中斷處理
中斷處理完畢,返回到 ret_from_intr
3)ret_from_intr
ENTRY(ret_from_intr) GET_CURRENT(%ebx) movl EFLAGS(%esp),%eax //Linux只采用兩種運行級別,系統為0,用戶為 3,所以,如果CS的最低兩位為非0,那就說明中斷發生於用戶空間。
//如果中斷發生於系統空間,控制就直接轉移到restore_all,
//而如果發生於用戶 空間,則轉移到ret_with_reschedule。
//在restore_all中恢復1中保存的寄存器,隨后調用iret恢復EIP、CS、 EFLAGS返回到中斷發生時的狀態。 movb CS(%esp),%al testl $(VM_MASK | 3),%eax //檢測中斷前夕寄存器EFLAGS的高6位和代碼段寄存器CS的內容,來判斷中斷前夕CPU是否運行於VM86模式、用戶空間還是系統空 間 jne ret_with_reschedule //如果發現當前進程的need_resched==1,則會調用schedule; // 如果發現還有待需要處理的軟中斷,則會調用do_softirq; jmp restore_all
RESOTRE_ALL 和 SAVE_ALL 是相反的操作,將堆棧中的寄存器恢復
最后,調用 iret 指令 ,將處理權交給 CPU
4)iret 指令使 CPU 從中斷返回
此時,系統空間的堆棧和CPU在第1步處理完之后,交給 linux 內核時的情形是一樣的,也就是保存着用戶空間的返回地址(CS、EIP)、EFLAGS、用戶空間的堆棧(SS、ESP)。
CPU將 CS、EIP、EFLAGS 、SS、ESP恢復,從而返回到用戶空間。
怎么查看系統的上下文切換情況
系統性能分析工具: vmstat 主要用來分析系統的內存使用情況,也常用來分析 CPU 上下文切換和中斷的次數。
1 G480:~$ vmstat 5
2 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
3 r b swpd free buff cache si so bi bo in cs us sy id wa st
4 0 0 0 1801536 108208 2065472 0 0 61 2870 433 1283 9 3 80 8 0
cs(context switch)是每秒上下文切換的次數。
in(interrupt)則是每秒中斷的次數。
r(Running or Runnable)是就緒隊列的長度,也就是正在運行和等待 CPU 的進程數。
b(Blocked)則是處於不可中斷睡眠狀態的進程數。
pidstat查看每個進程的詳細情況:
1 G480:~$ pidstat -w 5
2 Linux 5.4.0-9-generic (G480) 2020年02月14日 _x86_64_ (4 CPU)
3
4 10時26分52秒 UID PID cswch/s nvcswch/s Command 5 10時26分57秒 0 1 20.52 0.00 systemd 6 10時26分57秒 0 10 0.60 0.00 ksoftirqd/0 7 10時26分57秒 0 11 25.90 0.00 rcu_sched 8 10時26分57秒 0 12 0.20 0.00 migration/0 9 10時26分57秒 0 17 0.20 0.00 migration/1
cswch: 表示每秒自願上下文切換(voluntary context switches)的次數
nvcswch: 表示每秒非自願上下文切換(non voluntary context switches)的次數
所謂自願上下文切換,是指進程無法獲取所需資源,導致的上下文切換。比如說, I/O、內存等系統資源不足時,就會發生自願上下文切換。
而非自願上下文切換,則是指進程由於時間片已到等原因,被系統強制調度,進而發生的上下文切換。比如說,大量進程都在爭搶 CPU 時,就容易發生非自願上下文切換。
案例分析
sysbench:多線程的基准測試工具,模擬context switch
終端1:sysbench --threads=10 --max-time=300 threads run
終端2:vmstat 1:sys列占用84%說明主要被內核占用,ur占用16%;r就緒隊列8;in中斷處理1w,cs切換139w==>等待進程過多,頻繁上下文切換,內核cpu占用率升高
終端3:pidstat -w -u 1:sysbench的cpu占用100%(-wt發現子線程切換過多),其他進程導致上下文切換
watch -d cat /proc/interupts :查看另一個指標中斷次數,在/proc/interupts中讀取,發現重調度中斷res變化速度最快
總結:cswch過多說明資源IO問題,nvcswch過多說明調度爭搶cpu過多,中斷次數變多說明cpu被中斷程序調用
工具
系統負載 : uptime ( watch -d uptime)看三個階段平均負載
系統整體情況 : mpstat (mpstat -p ALL 3) 查看 每個cpu當前的整體狀況,可以重點看用戶態、內核態、以及io等待三個參數
系統整體的平均上下文切換情況 : vmstat (vmstat 3) 可以重點看 r (進行或等待進行的進程)、b (不可中斷進程/io進程) 、in (中斷次數) 、cs(上下文切換次數)
查看詳細的上下文切換情況 : pidstat (pidstat -w(進程切換指標)/-u(cpu使用指標)/-wt(線程上下文切換指標)) 注意看是自願上下文切換、還是被動上下文切換
io使用情況 : iostat
模擬場景工具 :
stress : 模擬進程 、 io
sysbench : 模擬線程數
總結
自願上下文切換變多了,說明進程都在等待資源,有可能發生了 I/O 等其他問題;非自願上下文切換變多了,說明進程都在被強制調度,也就是都在爭搶 CPU,說明 CPU 的確成了瓶頸;中斷次數變多了,說明 CPU 被中斷處理程序占用,還需要通過查看 /proc/interrupts 文件來分析具體的中斷類型。
stress和sysbench兩個工具在壓測過程中的對比發現:
stress基於多進程的,會fork多個進程,導致進程上下文切換,導致us開銷很高;
sysbench基於多線程的,會創建多個線程,單一進程基於內核線程切換,導致sy的內核開銷很高
...................................................................................
極客時間倪鵬飛《linux性能優化實踐》專欄筆記
https://blog.csdn.net/vividonly/article/details/6607811