一、Linux系統概念模型
linux操作系統是一個基於POSIX的多用戶、多任務、支持多線程的復雜系統。它的復雜程度難以想象,作為一個操作系統linux為用戶提供進程管理、內存管理、設備控制以及網絡管理等功能。要學習如此錯綜復雜的系統,最主要的是要抓住其脈絡,構建一個易於理解的模型。linux操作系統最主要的功能是管理用戶程序,為用戶程序執行提供資源,從這個角度本文描述linux系統的運行模型可以概括為如下幾個模塊:
-
存儲程序計算機。目前的大多數計算機都是馮諾依曼型計算機,計算機硬件應由運算器、存儲器、控制器、輸入設備、輸出設備5大基本類型部件組成。計算機內部采用二進制來表示指令和數據。將編好的程序和原始數據先存入存儲器中經過CPU取值分析后才能執行,這就是存儲程序的基本含義。
linux操作系統頻閉了底層硬件的復雜性,為用戶提供了簡潔、易用的系統接口;並且負責管理所有的硬件資源,采用巧妙的抽象技術支持多道程序運行以提高系統的資源利用率。
-
函數調用堆棧
無論是面向對象的程序設計方式還是面向過程的程序設計方式,函數都是程序的基本單位,函數之間的相互調用是構造復雜程序的基礎。在linux操作系統通過堆棧這一簡單的數據結構,使得函數調用的模型變得簡單易於理解。
每一個函數執行時都有自己的棧幀,棧幀內存儲局部變量、上一個棧幀的棧基址,當要調用其他函數時還會存儲調用返回的返回地址,以及被調用函數的參數列表。
-
中斷處理機制
正常情況下CPU執行的指令流是按照執行在內存的地址順序執行的,但是由於內外部事件或由程序預先安排的事件會引起的CPU暫時停止正在運行的程序,轉而為該內部或外部事件或預先安排的事件服務的程序中去,服務完畢后再返回去繼續運行被暫時中斷的程序。
中斷不光為系統處理異常事件提供了統一的實現模型,還是用戶程序獲取系統服務的入口,以及進程調度的時機。
-
系統調用服務
系統調用封裝了訪問系統資源的方式,位用戶進程與硬件設備的交互提供了一組簡單易用的接口。在日常編程中處處離不開系統調用的身影,最常見的就是通過庫函數提供的
printf
語句訪問系統輸出設備,printf
函數的API內部便封裝了訪問硬件的系統調用。當用戶態進程通過庫函數訪問系統調用時,首先會通過系統調用堆棧在用戶態堆棧保存調用函數的現場和程序斷點;然后通過中斷機制CPU進入內核態執行系統調用,系統調用執行完成后返回前操作系統會檢查中斷向量判斷是否有可執行的中斷;最后還會運行
schedule()
-
進程調度
linux操作系統是多任務系統,通過進程管理實現多個進程的公平調度盡可能提高系統的吞吐量、資源利用率。具體的,linux內核通過schedule()
函數實現進程的調度,schedule()
通過調度算法在運行隊列中找到下一個運行的進程,並把CPU分配給它。
模型驗證
一個有文件讀寫操作的用戶程序,用戶程序的代碼經過編譯、鏈接、裝載之后數據和代碼都存儲在內存中;執行時操作系統的shell調用fork
創建一個子進程,子進程再調用execve
構造用戶程序的用戶堆棧,加載函數參數與環境變量,子進程返回后從用戶程序入口開始執行;
在執行過程中如果需要調用函數,首先需要將函數參數壓棧,然后調用call
指令將返回地址壓棧,最后為被調用函數構造一個新棧幀;被調用函數執行完畢后,首先銷毀棧幀,然后將返回地址彈出到RIP
在執行過程中CPU可能會相應各種各樣的中斷事件,最常見的如時鍾中斷、read()
系統調用引發的軟中斷等。當中斷發生時CPU需要進入內核態執行中斷處理程序,與函數調用處理過程類似,首先需要進行中斷上下文的切換,不同的是需要將被中斷進程的斷點和寄存器上下文保存到進程的內核棧中;緊接着根據中斷調用號查找中斷向量表確定服務例程的地址,開始執行中斷服務程序;具體地當用戶進程執行read()
函數時,操作系統要執行中斷指令進入內核態,根據中斷描述符找到服務例程sys_read()
,將文件內容讀入到內核態再復制到用戶地址空間,完成文件讀操作。
中斷服務程序完成后,中斷返回前是操作系統進行進程調度的時機,操作系統會執行schedule()
判斷是否需要執行進程調度。若需要則根據調度算法從進程的就緒隊列中選取下一個可運行的進程,完成進程上下文的切換;若不需要,則正常的中斷返回,從被中斷函數的下一條語句繼續執行。
在上述所述的概念模型中,操作系統通過堆棧管理着用戶程序的調用關系,通過系統調用想用戶程序提供資源服務,通過中斷機制處理異常情況,通過進程調度使得整個系統能夠生生不息,延綿有序地執行下去。
二、應用程序生命周期與影響程序性能的因素
在Linux下使用GCC將源碼編譯成可執行文件的過程可以分解為4個步驟,分別是預處理、編譯、匯編和鏈接。一個簡單的hello word程序編譯過程如下:
-
預處理
預編譯過程主要處理那些源代碼中以#開始的預編譯指令,主要過程有:
-
刪除所有的
#define
,並且展開所有的宏定義; -
處理所有條件編譯指令,如#if,#ifdef等;
-
處理#include預編譯指令,將被包含的文件插入到該預編譯指令的位置。
-
-
編譯
編譯過程就是把預處理完的文件進行一系列詞法分析,語法分析,語義分析及優化后生成相應的匯編代碼文件(.s)。
-
匯編
匯編就是將匯編代碼轉變成機器可以執行的命令,生成目標文件(.o),在linux操作系統下的目標文件是ELF格式文件。ELF文件中將程序划分為不同的Section,主要有代碼段(.text),未初始化的全局變量(.bss),已初始化的全局變量(.data)以及只讀數據段(.rodata)等。
-
鏈接
鏈接就是將各種代碼和數據部分收集起來並組合成為一個單一文件的過程,經過鏈接這個文件可以被加載到內存執行。鏈接是可執行文件生產過程中最為復雜的部分,可以分為符號解析和重定位兩部分;根據鏈接時機的不同,又可分為靜態鏈接和動態鏈接。
可執行文件只有被裝載到內存以后才能被CPU執行。操作系統創建一個進程,然后裝載相應的可執行文件並且執行,整個過程可以描述如下:
-
創建一個獨立的虛擬地址空間。創建虛擬空間實際上只是分配一個頁目錄,虛擬空間到物理內存的映射關系等到后面程序發生頁錯誤的時候再進行設置。
-
讀取可執行文件頭,並且建立虛擬空間與可執行文件的映射關系。當操作系統捕獲到缺頁錯誤時,通過該映射關系就知道當前所需要的頁在可執行文件中的位置。這種映射關系是按照段進行映射的,進程虛擬空間中的一個段叫做虛擬內存區域。
-
將CPU的執行寄存器設置成可執行文件的入口地址,啟動運行。ELF文件頭中保存了入口地址,操作系統通過設置CPU指令寄存器將控制權轉交給進程,由此進程開始執行。
用戶程序執行過程中CPU按照指令的地址順序不斷取址執行,如果存在函數調用關系會在用戶態堆棧中創建新的棧幀,並跳轉執行指令;當訪問系統調用時首先在用戶態棧幀保存斷電,然后CPU通過中斷指令進入內核態執行系統調用;當進程被阻塞或執行中斷處理函數結束時,操作系統都會執行schedule()
函數判斷是否需要進行進程調度,若需要會切換進程上下文執行新的進程。事實上進程在執行結束前會不斷地被調入,調出。
經過上述分析可知影響一個應用程序運行的因素大致可以分為三種:
-
應用程序頻繁的訪問系統調用,CPU需要不斷地完成從用戶態與內核態之間的切換,降低系統性能。
-
應用程序的運算量較大,CPU過於繁忙,此時CPU性能是瓶頸。
-