進程是操作系統的偉大發明之一,對應用程序屏蔽了CPU調度、內存管理等硬件細節,而抽象出一個進程的概念,讓應用程序專心于實現自己的業務邏輯既可,而且在有限的CPU上可以“同時”進行許多個任務。但是它為用戶帶來方便的同時,也引入了一些額外的開銷。如下圖,在進程運行中間的時間里,雖然CPU也在忙于干活,但是卻沒有完成任何的用戶工作,這就是進程機制帶來的額外開銷。
圖1.jpg
在進程A切換到進程B的過程中,先保存A進程的上下文,以便于等A恢復運行的時候,能夠知道A進程的下一條指令是啥。然后將要運行的B進程的上下文恢復到寄存器中。這個過程被稱為上下文切換。上下文切換開銷在進程不多、切換不頻繁的應用場景下問題不大。但是現在linux操作系統被用到了高并發的網絡程序后端服務器。在單機支持成千上萬個用戶請求的時候,這個開銷就得拿出來說道說道了。因為用戶進程在請求redis、MySQL數據等網絡IO阻塞掉的時候,或者在進程時間片到了,都會引發上下文切換。
圖2.png
一個簡單的進程上下文切換開銷測試實驗
廢話不多說,我們先用個實驗測試一下,到底一次上下文切換需要多長的CPU時間!實驗方法是創建兩個進程并在它們之間傳送一個令牌。其中一個進程在讀取令牌時就會引起阻塞。另一個進程發送令牌后等待其返回時也處于阻塞狀態。如此往返傳送一定的次數,然后統計他們的平均單次切換時間開銷。
具體的實驗代碼參見test04
# gcc main.c -o main # ./main./main Before Context Switch Time1565352257 s, 774767 us After Context SWitch Time1565352257 s, 842852 us
每次執行的時間會有差異,多次運行后平均每次上下文切換耗時3.5us左右。當然了這個數字因機器而異,而且建議在實機上測試。
前面我們測試系統調用的時候,最低值是200ns。可見,上下文切換開銷要比系統調用的開銷要大。系統調用只是在進程內將用戶態切換到內核態,然后再切回來,而上下文切換可是直接從進程A切換到了進程B。顯然這個上下文切換需要完成的工作量更大。
進程上下文切換開銷都有哪些
那么上下文切換的時候,CPU的開銷都具體有哪些呢?開銷分成兩種,一種是直接開銷、一種是間接開銷。
直接開銷就是在切換時,cpu必須做的事情,包括:
- 1、切換頁表全局目錄
- 2、切換內核態堆棧
- 3、切換硬件上下文(進程恢復前,必須裝入寄存器的數據統稱為硬件上下文)
- ip(instruction pointer):指向當前執行指令的下一條指令
- bp(base pointer): 用于存放執行中的函數對應的棧幀的棧底地址
- sp(stack poinger): 用于存放執行中的函數對應的棧幀的棧頂地址
- cr3:頁目錄基址寄存器,保存頁目錄表的物理地址
- ......
- 4、刷新TLB
- 5、系統調度器的代碼執行
間接開銷主要指的是雖然切換到一個新進程后,由于各種緩存并不熱,速度運行會慢一些。如果進程始終都在一個CPU上調度還好一些,如果跨CPU的話,之前熱起來的TLB、L1、L2、L3因為運行的進程已經變了,所以以局部性原理cache起來的代碼、數據也都沒有用了,導致新進程穿透到內存的IO會變多。 其實我們上面的實驗并沒有很好地測量到這種情況,所以實際的上下文切換開銷可能比3.5us要大。
想了解更詳細操作過程的同學請參考《深入理解Linux內核》中的第三章和第九章。
一個更為專業的測試工具-lmbench
lmbench用于評價系統綜合性能的多平臺開源benchmark,能夠測試包括文檔讀寫、內存操作、進程創建銷毀開銷、網絡等性能。使用方法簡單,但就是跑有點慢,感興趣的同學可以自己試一試。
這個工具的優勢是是進行了多組實驗,每組2個進程、8個、16個。每個進程使用的數據大小也在變,充分模擬cache miss造成的影響。我用他測了一下結果如下:
------------------------------------------------------------------------- Host OS 2p/0K 2p/16K 2p/64K 8p/16K 8p/64K 16p/16K 16p/64K ctxsw ctxsw ctxsw ctxsw ctxsw ctxsw ctxsw --------- ------------- ------ ------ ------ ------ ------ ------- ------- bjzw_46_7 Linux 2.6.32- 2.7800 2.7800 2.7000 4.3800 4.0400 4.75000 5.48000
lmbench顯示的進程上下文切換耗時從2.7us到5.48之間。
線程上下文切換耗時
前面我們測試了進程上下文切換的開銷,我們再繼續在Linux測試一下線程。看看究竟比進程能不能快一些,快的話能快多少。
在Linux下其實本并沒有線程,只是為了迎合開發者口味,搞了個輕量級進程出來就叫做了線程。輕量級進程和進程一樣,都有自己獨立的task_struct進程描述符,也都有自己獨立的pid。從操作系統視角看,調度上和進程沒有什么區別,都是在等待隊列的雙向鏈表里選擇一個task_struct切到運行態而已。只不過輕量級進程和普通進程的區別是可以共享同一內存地址空間、代碼段、全局變量、同一打開文件集合而已。
同一進程下的線程之所有getpid()看到的pid是一樣的,其實task_struct里還有一個tgid字段。 對于多線程程序來說,getpid()系統調用獲取的實際上是這個tgid,因此隸屬同一進程的多線程看起來PID相同。
我們用一個實驗來測試一下test06。其原理和進程測試差不多,創建了20個線程,在線程之間通過管道來傳遞信號。接到信號就喚醒,然后再傳遞信號給下一個線程,自己睡眠。 這個實驗里單獨考慮了給管道傳遞信號的額外開銷,并在第一步就統計了出來。
# gcc -lpthread main.c -o main 0.508250 4.363495
每次實驗結果會有一些差異,上面的結果是取了多次的結果之后然后平均的,大約每次線程切換開銷大約是3.8us左右。從上下文切換的耗時上來看,Linux線程(輕量級進程)其實和進程差別不太大。
Linux相關命令
既然我們知道了上下文切換比較的消耗CPU時間,那么我們通過什么工具可以查看一下Linux里究竟在發生多少切換呢?如果上下文切換已經影響到了系統整體性能,我們有沒有辦法把有問題的進程揪出來,并把它優化掉呢?
# vmstat 1 procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 0 595504 5724 190884 0 0 295 297 0 0 14 6 75 0 4 5 0 0 593016 5732 193288 0 0 0 92 19889 29104 20 6 67 0 7 3 0 0 591292 5732 195476 0 0 0 0 20151 28487 20 6 66 0 8 4 0 0 589296 5732 196800 0 0 116 384 19326 27693 20 7 67 0 7 4 0 0 586956 5740 199496 0 0 216 24 18321 24018 22 8 62 0 8
或者是
# sar -w 1 proc/s Total number of tasks created per second. cswch/s Total number of context switches per second. 11:19:20 AM proc/s cswch/s 11:19:21 AM 110.28 23468.22 11:19:22 AM 128.85 33910.58 11:19:23 AM 47.52 40733.66 11:19:24 AM 35.85 30972.64 11:19:25 AM 47.62 24951.43 11:19:26 AM 47.52 42950.50 ......
上圖的環境是一臺生產環境機器,配置是8核8G的KVM虛機,環境是在Nginx+fpm的,fpm數量為1000,平均每秒處理的用戶接口請求大約100左右。其中cs列表示的就是在1s內系統發生的上下文切換次數,大約1s切換次數都達到4W次了。粗略估算一下,每核大約每秒需要切換5K次,則1s內需要花將近20ms在上下文切換上。要知道這是虛機,本身在虛擬化上還會有一些額外開銷,而且還要真正消耗CPU在用戶接口邏輯處理、系統調用內核邏輯處理、以及網絡連接的處理以及軟中斷,所以20ms的開銷實際上不低了。
那么進一步,我們看下到底是哪些進程導致了頻繁的上下文切換?
# pidstat -w 1 11:07:56 AM PID cswch/s nvcswch/s Command 11:07:56 AM 32316 4.00 0.00 php-fpm 11:07:56 AM 32508 160.00 34.00 php-fpm 11:07:56 AM 32726 131.00 8.00 php-fpm ......
由于fpm是同步阻塞的模式,每當請求Redis、Memcache、Mysql的時候就會阻塞導致cswch/s自愿上下文切換,而只有時間片到了之后才會觸發nvcswch/s非自愿切換。可見fpm進程大部分的切換都是自愿的、非自愿的比較少。
如果想查看具體某個進程的上下文切換總情況,可以在/proc接口下直接看,不過這個是總值。
grep ctxt /proc/32583/status voluntary_ctxt_switches: 573066 nonvoluntary_ctxt_switches: 89260
本節結論
上下文切換具體做哪些事情我們沒有必要記,只需要記住一個結論既可,測得作者開發機上下文切換的開銷大約是2.7-5.48us左右,你自己的機器可以用我提供的代碼或工具進行一番測試。
lmbench相對更準確一些,因為考慮了切換后Cache miss導致的額外開銷。
個人公眾號“開發內功管理”,打通理論與實踐的任督二脈。
參考文獻
- 進程上下文切換,殘酷的性能殺手
- 測試上下文切換開銷
- 進程上下文切換導致Load過高
- CPU上下文切換的次數和時間
- Linux操作系統測試工具
- lmbench官方文檔
- lmbench安裝與使用