從系統的角度分析影響程序執行性能的因素

一、精簡的Linux系統概念模型概述

操作系統是管理計算機硬件與軟件資源的計算機程序一般由內核、shell 和應用程序組成。核心是內核,控制着計算機系統上的所有硬件和軟件,在必要時分配硬件,並根據需要執行軟件。內核主要負責進程管理、內存管理、文件系統等。

​ 進程管理模塊主要是對進程使用的處理機進行管理和控制。Linux以進程作爲系統資源分配的基本單位,並採用動態優先級的進程高級算法,保證各個進程使用處理機的合理性。使用的調度策略有:先進先出的實時進程、時間片輪轉的實時進程、普通的分時進程

​ 內存管理模塊採用了虛擬存儲機制,實現對多進程的存儲管理,使得每個進程都有各自互不干涉的進程地址空間。

​ 文件系統模塊採用了虛擬文件系統(VFS),屏蔽了各種文件系統的差別,爲處理各種不同的文件系統提供了統一的接口,支持多種不同的物理文件系統。同時,Linux把各種硬件設備看作一種特殊的文件來處理,用管理文件的方法管理設備,非常方便、有效。

二、進程管理

進程的描述

在Linux內核中用一個數據結構struct task_struct來描述進程,其中state是進程狀態, stack是堆棧等,通過如圖所示的進程描述符的結構示意圖從總體上看清structtask_struct的結構關係。

 

 

 

進程狀態

操作系統原理中的進程有就緒態、運行態、阻塞態這 3 種基本狀態,實際的 Linux 內核管理的進程狀 態與這 3 個狀態是很不一樣的。如圖所示爲 Linux 內核管理的進程狀態轉換圖。


 

 

 其中當使用 fork()系統調用來創建一個新進程時,新進程的狀態是 TASK_RUNNING(就 緒態,但是沒有在運行)。當調度器選擇這個新創建的進程運行時,新創建的進程就切換 到運行態,它也是 TASK_RUNNING。也就是說,在 Linux 內核中,當進程 是 TASK_RUNNING 狀態時,它是可運行的,也就是就緒態,是否在運行取決於它有沒 有獲得 CPU 的控制權,也就是說這個進程有沒有在 CPU 中實際執行。如果在 CPU 中實 際執行了,進程狀態就是運行態;如果被內核調度出去了,在等待隊列裏就是就緒態。

進程調度

Linux內核通過schedule函數實現進程調度,schedule函數負責在運行隊列中選擇一個進程,然後把它切換到CPU上執行。調用schedule函數的時機主要分爲兩類:

(1)中斷處理過程中的進程調度時機,中斷處理過程中會在適當的時機檢測need_resched標記,決定是否調用schedule()函數。比如在系統調用內核處理函數執行完成後且系統調用返回之前就會檢測need_resched標記決定是否調用schedule()函數。

(2)內核線程主動調用schedule(),如內核線程等待外設或主動睡眠等情形下,或者在適當的時機檢測need_resched標記,決定是否主動調用schedule函數。

進程的切換

爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復執行以前掛起的某個進程。這種行爲被稱爲進程切換、任務切換或進程上下文切換。在進程切換時,需要保存當前進程的所有信息,如用戶地址空間、控制信息、進程的CPU上下文,和相關寄存器的值。schedule()函數選擇一個新的進程來運行,並調用context_switch進行上下文的切換。context_switch()首先調用switch_mm切換地址空間,然後調用switch_to()進行CPU上下文切換。

三、內核態和用戶態

Linux 操作系統採用 0 和 3 兩個特權級別,分別對應內核 態和用戶態,區分方法就是 CS:EIP 的指向範圍,在內核態時,CS:EIP 的值可以是任意的地址,在 32 位的 x86 機器上有 4GB 的進程地址空間,內核態下的這 4GB 的地址空間全都可以訪問。但是在用戶態時,只能訪問 0x00000000~0xbfffffff 的地址空間,0xc0000000 以上的地址空間只能在內核態下訪問。

四、中斷

  發生從用戶態到內核態的切換,一般存在以下三種情況:

  1)庫函數、系統調用或者shell命令。

  2)異常事件: 當CPU正在執行運行在用戶態的程序時,突然發生某些預先不可知的異常事件,這個時候就會觸發從當前用戶態執行的進程轉向內核態執行相關的異常事件,典型的如缺頁異常。

  3)外圍設備的中斷:當外圍設備完成用戶的請求操作後,會像CPU發出中斷信號,此時,CPU就會暫停執行下一條即將要執行的指令,轉而去執行中斷信號對應的處理程序,如果先前執行的指令是在用戶態下,則自然就發生從用戶態到內核態的轉換。

  總而言之就是通過中斷。Linux 下系統調用通過 int 0x80 中斷完成,中斷保 存了用戶態 CS:EIP 的值,以及當前的堆棧段寄存器的棧頂,將 EFLAGS 寄存器的當前的 值保存到內核堆棧裏,同時把當前的中斷信號或者是系統調用的中斷服務程序的入口加載到 CS:EIP 裏,把當前的堆棧段 SS:ESP 也加載到 CPU 裏,這些都是由中斷信號或者是 int 指令來完成的。完成後,當前 CPU 在執行下一條指令時就已經開始執行中斷處理程序的入 口了,這時對堆棧的操作已經是內核堆棧操作了,之前的 SAVE_ALL 就是內核代碼,完成 中斷服務,發生進程調度。如果沒有發生進程調度,就直接 restore_all 恢復中斷現場,然 後 iret 返回到原來的狀態;如果發生了進程調度,當前的這些狀態都會暫時地保存在系統 內核堆棧裏,當下一次發生進程調度有機會再切換回當前進程時,就會接着把 restore_all 和 iret 執行完,這樣中斷處理過程就執行完了

五、文件系統

文件是具有符號名的、在邏輯上具有完整意義的一組相關信息項的有序序列。文件系統,就是操作系統中實現文件統一管理的一組軟件、被管理的文件以及爲實施文件管理所需要的一些數據結構的總稱。要實現操作系統對其他各種不同文件系統的支持,就要將對各種不同文件系統的操作和管理納入到一個統一的框架中。對用戶程序隱去各種不同文件系統的實現細節,爲用戶程序提供一個統一的、抽象的、虛擬的文件系統界面,這就是所謂的虛擬文件系統VFS。通常,虛擬文件系統分爲三個層次:

第一層爲文件系統接口層,如open/write/close等系統調用接口。

第二層爲VFS接口層。該層有兩個接口:一個是與用戶的接口;一個是與特定文件系統的接口。VFS與用戶的接口將所有對文件的操作定向到相應的特定文件系統函數上。VFS與特定文件系統的接口主要是通過VFS-operations實現。

第三層是具體文件系統層,提供具體文件系統的結構和實現,包括網絡文件系統,如NFS。

文件打開和讀寫的流程:

1. 進程X讀寫一個文件首先要使用open系統調用打開這個文件,根據給出的文件路徑獲取它在哪個存儲設備上(FAT32格式的u盤、NTFS格式的磁盤等等),可以找到該設備對應的設備文件的文件控制塊inode,inode裏有對應的設備號,根據設備號可以找到對應的驅動程序。在內核初始化時已經將各個設備的設備驅動程序註冊到內核,併爲他們生成對應的設備文件,設備文件向上提供統一的接口如open()、read()等,方便調用。而open系統調用會新建一個file結構,並在進程X的進程打開文件表的fd數組裏找到空閒的一項,指向剛創建的file結構,然後返回fd數組的下標。假如要訪問的文件存儲在FAT32格式的塊設備上,程序員寫的驅動程序包括具體的針對FAT32的讀寫打開關閉等函數,他們已經被註冊到內核,通過設備號就能找到對應的驅動程序,然後用具體的操作函數來初始化設備文件的文件控制塊inode節點裏的cdev,再用inode節點的cdev初始化系統文件打開表file結構裏的file_operations。以上都是open系統調用內核乾的事,直觀上來說就是內核向用戶態返回了一個整數(fd數組的下標)。

2. 進程X使用read系統調用讀這個文件,就會根據參數:fd數組的下標 找到fd數組的對應項,進而找到指向第1步創建的file結構的指針,進而找到這個file結構,進而找到file結構裏的file_operations裏的具體的read函數來讀取文件,這是一個從抽象到具體的過程。最後系統調用返回告訴用戶態進程X讀操作成功與否。

3. 進程X使用write系統調用寫數據到這個文件,就會根據參數:fd數組的下標 找到fd數組的對應項,進而找到指向第1步創建的file結構的指針,進而找到這個file結構,進而找到file結構裏的file_operations裏的具體的write函數來寫文件,這也是一個從抽象到具體的過程。最後系統調用返回告訴用戶態進程X寫操作成功與否。

六、影響應用程序性能表現的因素

第一個終端裏運行 sysbench ,模擬系統多線程調度的瓶頸:

# 以10個線程運行5分鐘的基準測試,模擬多線程切換的問題
sysbench --threads=10 --max-time=300 threads run

在第二個終端運行 vmstat ,觀察上下文切換情況:

# 每隔1秒輸出1組數據(需要Ctrl+C才結束)
vmstat 1
 
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----
r b swpd free buff cache si so bi bo in cs us sy id wa s
6 0 0 6487428 118240 1292772 0 0 0 0 9019 1398830 16 84 0
8 0 0 6487428 118240 1292772 0 0 0 0 10191 1392312 16 84

cs 列:上下文切換次數驟然上升到了 139 萬
r 列:就緒隊列的長度已經到了 8,遠遠超過了系統 CPU 的個數 2,所以肯定會有大量的 CPU 競爭
us(user)和 sy(system)列:這兩列的 CPU 使用率加起來上升到了 100%,其中sy 列高達 84%
in 列:中斷次數也上升到了 1 萬左右,說明中斷處理也是個潛在的問題。

綜合這幾個指標可以知道,系統的就緒隊列過長,也就是正在運行和等待 CPU 的進程數過多,導致了大量的上下文切換,而上下文切換又導致了系統 CPU 的佔用率升高。

在第三個終端再用 pidstat 來看一下, CPU 和進程上下文切換的情況:

# 每隔1秒輸出1組數據(需要 Ctrl+C 才結束)
# -w參數表示輸出進程切換指標,而-u參數則表示輸出CPU使用指標
pidstat -w -u 1
 
08:06:33      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
08:06:34        0     10488   30.00  100.00    0.00    0.00  100.00     0  sysbench
08:06:34        0     26326    0.00    1.00    0.00    0.00    1.00     0  kworker/u4:2
 
08:06:33      UID       PID   cswch/s nvcswch/s  Command
08:06:34        0         8     11.00      0.00  rcu_sched
08:06:34        0        16      1.00      0.00  ksoftirqd/1
08:06:34        0       471      1.00      0.00  hv_balloon
08:06:34        0      1230      1.00      0.00  iscsid
08:06:34        0      4089      1.00      0.00  kworker/1:5
08:06:34        0      4333      1.00      0.00  kworker/0:3
08:06:34        0     10499      1.00    224.00  pidstat
08:06:34        0     26326    236.00      0.00  kworker/u4:2
08:06:34     1000     26784    223.00      0.00  sshd

從 pidstat 的輸出可以發現,CPU 使用率的升高果然是 sysbench 導致的,它的 CPU 使用率已經達到了 100%。但上下文切換則是來自其他進程,包括非自願上下文切換頻率最高的 pidstat ,以及自願上下文切換頻率最高的內核線程 kworker 和 sshd。

另外可以看到,pidstat 輸出的上下文切換次數,加起來也就幾百,比 vmstat 的 139 萬明顯小了太多。

Linux 調度的基本單位實際上是線程,而場景 sysbench 模擬的也是線程的調度問題,那麼,是不是 pidstat 忽略了線程的數據呢?通過運行 man pidstat ,pidstat 默認顯示進程的指標數據,加上 -t 參數後,纔會輸出線程的指標。

在第三個終端裏再加上 -t 參數,重試一下看看:

# 每隔1秒輸出一組數據(需要 Ctrl+C 才結束)
# -wt 參數表示輸出線程的上下文切換指標
pidstat -wt 1
 
08:14:05      UID      TGID       TID   cswch/s nvcswch/s  Command
...
08:14:05        0     10551         -      6.00      0.00  sysbench
08:14:05        0         -     10551      6.00      0.00  |__sysbench
08:14:05        0         -     10552  18911.00 103740.00  |__sysbench
08:14:05        0         -     10553  18915.00 100955.00  |__sysbench
08:14:05        0         -     10554  18827.00 103954.00  |__sysbench

現在就能看到雖然 sysbench 進程(也就是主線程)的上下文切換次數看起來並不多,但它的子線程的上下文切換次數卻有很多。上下文切換罪魁禍首,還是過多的sysbench 線程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章