實驗總結分析報告:從系統的角度分析影響程式執行效能的因素
1、請您根據本課程所學內容總結梳理出一個精簡的Linux系統概念模型,最大程度統攝整頓本課程及相關的知識資訊,模型應該是邏輯上可以運轉的、自洽的,並舉例某一兩個具體例子(比如讀寫檔案、分配記憶體、使用I/O驅動某個硬體等)納入模型中驗證模型。
2、然後將一個應用程式放入該系統模型中系統性的梳理影響應用程式效能表現的因素,並說明原因。
3、產出要求是發表一篇部落格文章,長度不限,1要簡略,2是重點,只談自己的思考和梳理,嚴禁引用任何資料(包括本課程的資料)造成文章虛長。
SA20225205 黃興宇
1 Linux系統模型簡述
Linux模型的主要模組分以下幾個部分:儲存管理、程式管理、檔案系統、中斷、系統呼叫等。Linux的各個模組之間相互依賴,共同完成作業系統的各項基本的功能和對系統的管理工作,對底層來說,與硬體互動管理所有的硬體資源;對上層來說,為使用者程式(應用程式)提供一個良好的執行環境。Linux的整體架構如下圖。核心向上為使用者提供系統呼叫介面,向下呼叫硬體服務介面。其自身實現了上文提到的程式管理等功能,在核心外和提供瞭如shell、編譯器、直譯器、庫函式等基礎設施。
1.1 程式管理
1.1.1 程式的描述
在Linux中每一個程式都由task_struct資料結構來定義,就是通常說的PCB,它是程式存在的標誌。當我們呼叫fork()時,系統會為我們產生一個task_struct結構。然後從父程式那裡繼承一些資料,並把新的程式插入到程式樹中,以待進行程式管理。以往各個程式的task_struct存放在它們核心棧的尾端,由於現在使用了slab分配器動態生成task_struct,所以只需要在棧底建立一個thread_info,該結構體中task指標指向的就是task_struct程式描述符。
實際的task_struct約有1.7KB大小,包含以下資訊:
1、程式標識PID
2、程式的狀態:有TASK_RUNNING(正在執行或正在等待)、TASK_INTERRUPTIBLE(被阻塞且可中斷)、TASK_UNINTERRUPTIBLE(被阻塞且不可中斷)、TASK_STOPPED(停止)、TASK_ZOMBIE(僵死,程式終止但資源未回收)、TASK_DEAD(父程式wait()系統呼叫回收子程式全部資源)、TASK_SWAPPING(換入換出)等。
3、排程資訊和策略
3、程式的通訊狀況IPC
4、因為要插入程式樹,要有指向父子兄弟的指標
5、時間資訊:計算好執行的之間,以便CPU程式排程
6、佔有的資源:如開啟的檔案
7、程式上下文和核心上下文
8、處理器上下文
9、記憶體資訊
1.1.2 程式的建立
init_task為第一個程式(0號程式)的程式描述符結構體變數,他的初始化是通過硬編碼方式固定下來的。除此之外,所有其他的程式初始化都是通過fork()系統調複製父程式的方式初始化的(Linux使用clone()系統呼叫實現的fork())。這裡新建一個子程式的主要工作就是新建一個PCB,當然需要修改必要的資訊,還要有自己的資料空間和使用者堆疊等。
也就是說,在fork()函式之前需要確認核心中有足夠的資源來完成。如果資源滿足要求,則核心slab分配器在相應的緩衝區中構造子程式的程式控制塊,並將父程式控制塊中的全部成員都複製到子程式的控制塊,然後再把子程式控制塊必須的私有資料改成子程式的資料。當fork()返回到使用者空間之前,向子程式的核心棧中壓入返回值0,而向父程式核心堆疊壓入子程式的pid。最後進行一次程式排程,決定是執行子程式還是父程式。
為了能夠使子程式和父程式執行不同的程式碼(子程式從當前父程式執行處開始執行),在fork()之後應該根據fork()返回值使用分支結構來組成程式程式碼。
int main(void)
{
pid_t pid;
pid = fork();
if(pid < 0){
... //列印fork()失敗資訊
}else if(pid == 0){
... //子程式程式碼
}else{
... //父程式程式碼
}
return 0;
}
另一種使子程式執行不同程式碼的方法是execv()系統呼叫,子程式呼叫execv()後,系統會立即為子程式分配私有程式記憶體空間,並把函式引數path所指定的可執行檔案載入該空間中,從此子程式也成為一個真正的程式。
與一般函式不同,exec族函式執行成功後一般不會返回撥用點,因為它執行了一個新的程式,程式的程式碼段、資料段、和堆疊都被新的資料取代。只有呼叫失敗,才會返回一個-1,從原程式的呼叫點接著執行。
Linux採用了寫時拷貝,即fork()不復制父程式的程式碼區,而是使用指標共享一份拷貝。這樣做的好處就是,如果子程式呼叫execv()拷貝了其它程式碼段,就能避免父程式程式碼段無效的複製;如果依然使用父程式程式碼,也可以在父或子程式任一方寫入時,再為子程式拷貝。
int execv(const char* path, char* const argv[]);
int main(void)
{
pid_t pid;
if(!(pid=fork())){
execv("./hello.o",NULL);//可執行檔案./hello可以編寫一個.c程式再進行編譯獲得
}else {
printf("my pif is %d\n", getpid());
}
return 0;
}
1號和2號程式的建立是start_kernel初始化到最後由test_init通過kernel_thread建立了兩個核心執行緒:一個是kernel_init,把使用者態的程式init啟動起來,是所有使用者程式的祖先;另一個是kthreadd核心執行緒,kthreadd是所有核心執行緒的祖先,負責管理所有核心執行緒。
1.1.3 程式的排程
Linux的程式排程使用了剝奪方式,剝奪原則有優先權原則、短程式、優先原則、時間片原則。程式的排程演算法有FIFO(非剝奪)、最短CPU執行期優先、優先權排程、時間片輪轉。
排程時機
1、程式狀態發生變化時
2、當前程式時間片用完時
3、程式從系統返回到使用者態時
4、中斷處理後,程式返回到使用者態時
程式切換
1、切換頁全域性目錄以安裝一個新的地址空間
2、切換核心態堆疊和硬體上下文
1.1.4 程式的終止
殭屍程式
當子程式終止,OS釋放掉其資源。但是由於父程式未呼叫wait(),所以它位於今稱表中的條目還存在,這樣的程式被稱為殭屍程式。
孤兒程式
如果父程式沒有呼叫wait()就終止,以至於子程式稱為孤兒程式處於殭屍態。Linux對此情況的處理是,任選一個程式作為其父程式,如init程式。init程式定期呼叫wait(),以便定期釋放掛在其下的殭屍程式。
1.2 記憶體管理
記憶體管理用於確保所有程式能夠安全地共享機器主記憶體區,同時,記憶體管理模組還支援虛擬記憶體管理方式,使得 Linux 支援程式使用比實際記憶體空間更多的記憶體容量。並可以利用檔案系統把暫時不用的記憶體資料塊會被交換到外部儲存裝置上去,當需要時再交換回來。Linux採用虛擬地址,在 32 位 Linux 系統上每個程式有4GB 的程式地址空間,在使用者態下,只能訪問 0x00000000~0xbfffffff 的地址空間,而核心態下可以訪問全部空間。linux的地址分為邏輯地址、線性地址和實體地址。邏輯地址和線性地址在 32 位和 64 位上目前都是虛擬地址,需要依次經過分段對映和分頁對映最後才轉換成實體地址。這個對映計算地址的過程一般由 CPU 內部的 MMU(記憶體管理單元)負責把虛擬地址轉換為實體地址。
1.3 裝置管理
裝置驅動程式是一個軟體層,該軟體層使硬體響應預定義好的程式設計介面,我們已經熟悉了這種介面,它由一組控制裝置的VFS函式(open,read,lseek,ioctl等)組成,這些函式實際實現由裝置驅動程式全權負責。
1.4 檔案系統
檔案系統模組用於支援對外部裝置的驅動和儲存。虛擬檔案系統模組通過向所有的外部儲存裝置提供一個通用的檔案介面,隱藏了各種硬體裝置的不同細節,從而提供並支援與其他作業系統相容的多種檔案系統格式。VFS所提供的這些統一的API,再經過系統呼叫包裝一下,使用者空間就可以經過SCI的系統呼叫來操作不同的檔案系統。
1.5 中斷和系統呼叫
中斷是計算機的三大法寶之一,分外部中斷(硬體中斷)和內部中斷(軟體中斷),其中軟體系統呼叫作為一種特殊的中斷,就是利用陷阱(trap)這種軟體中斷式主動從使用者態進入核心態的。
中斷執行過程:
確定中斷向量,利用idtr找到中斷入口地址,確定特權級是否匹配,是否需要變更堆疊段,然後在棧中儲存eflags、cs和eip的內容(儲存在被中斷程式的核心棧中),如果異常產生一個硬體出錯碼,則將它儲存在棧中,然後裝載cs和eip,執行中斷處理程式。
中斷返回:
用儲存在棧中的值裝載cs、eip和eflags暫存器。如果一個硬體出錯碼曾被壓入棧中,那麼彈出這個硬體出錯碼,檢查中斷前是否在核心態,如果不是,從棧中裝載ss和esp回退到使用者態。
其中系統呼叫機制如下圖:
Linux系統呼叫總體上可以劃分為核心態和使用者態,也可以說是核心和應用程式,系統呼叫就是提供給使用者程式的一組可以訪問核心的介面。普通的函式呼叫是通過將引數壓棧的方式傳遞的。系統呼叫從使用者態切換到核心態,在使用者態只能訪問到使用者堆疊,在核心態可以訪問核心和使用者堆疊。
系統呼叫的過程:
1、使用者執行 int $0X80 或者 syscall 觸發系統呼叫
2、利用暫存器儲存現場,其中eax中存放系統呼叫號
3、CPU切換到核心態執行system_call()彙編程式碼,此時需要從eax暫存器中獲取系統呼叫號,核心才知道執行哪個系統呼叫
4、執行完後回覆現場
5、系統呼叫返回
2 具體例項
這裡描述讀寫檔案的過程。在讀寫檔案之前,必須使用oepn開啟一個檔案,開啟檔案首先open會執行到C庫,C庫裡有INT $0x80指令,然後在中斷向量表中找到128項,中斷向量表裡有中斷描述符,可以找到中斷處理程式入口,第128項是系統呼叫處理函式,進入系統呼叫處理函式,儲存現場,系統呼叫號儲存eax中,根據系統呼叫號執行系統呼叫表中對那一項的函式。系統呼叫表是相應的函式指標,這裡會執行sys_open。sys_open進行命令查詢,找到檔案控制塊,根據不同檔案型別,呼叫檔案開啟函式,檔案開啟函式會建立一個系統檔案開啟表file,file的很多內容來自檔案控制塊,填完之後,程式也有一個程式檔案開啟表,這個結構裡面有fd陣列,fd陣列是指標,找個空閒的,把它指向之前已經建立的file結構,最後返回那一項的索引號,即fd。
當程式使用read系統呼叫讀這個檔案,就會根據fd陣列的下標 找到fd陣列的對應項,然後找到指向之前建立的file結構的指標,再找到這個file結構,最後找到file結構裡的file_operations裡的具體的read函式來讀取檔案。write系統呼叫原理類似。
3 分析Linux應用程式效能影響因素
在分析linux程式效能影響因素時需要先確定程式的型別。主要有以下兩種:
cpu密集型:例如web伺服器像nginx node.js需要CPU進行批處理和數學計算都屬於此型別
io密集型:例如資料庫常見的mysql,大量消耗記憶體和儲存系統,對CPU和網路要求不高,這種應用使用CPU來發起IO請求,然後進入sleep狀態。
確定了應用型別就開始分析有哪些情況能影響效能:
大量的網頁請求會填滿執行佇列、大量的上下文切換,中斷。
大量的磁碟請求。
網路卡大量的吞吐。
以及記憶體耗盡等。
歸結起來就是4個方面cpu memory i/o和 network。
這裡我選擇了nginx來分析效能影響因素,通過在容器內搭建nginx,然後用另一個終端進行大量請求測試發現CPU使用率很高,但是看下面的程式的CPU使用率好像很正常。
這裡使用top和pidstat命令來定位問題,發現task任務數量不正常,想到可能是短時間的應用導致的問題,如下面的兩個:
(1)應用裡直接呼叫了其他二進位制程式,這些程式通常執行時間比較短,通過top等工具發現不了。
(2)應用本身在不停地崩潰重啟,而啟動過程的資源初始化,很可能會佔用很多CPU資源。
另外通過對nginx大量請求測試可能會出現丟包問題。可以總結為以下幾個方面:
在兩臺 VM 連線之間,可能會發生傳輸失敗的錯誤,比如網路擁塞、線路錯誤等;
在網路卡收包後,環形緩衝區可能會因為溢位而丟包;
在鏈路層,可能會因為網路幀校驗失敗、QoS 等而丟包;
在 IP 層,可能會因為路由失敗、組包大小超過 MTU 等而丟包;
在傳輸層,可能會因為埠未監聽、資源佔用超過核心限制等而丟包;
在套接字層,可能會因為套接字緩衝區溢位而丟包;
在應用層,可能會因為應用程式異常而丟包;
總之,對於具體的應用程式,我們可以從CPU ,記憶體, I/O以及網路等幾個方面去分析影響效能的因素,利用效能測試工具去定位和解決問題。