- .NET 4.0面向對象編程漫談:應用篇
- 金旭亮
- 9905字
- 2019-01-01 12:19:59
15.1 操作系統的進程與線程管理
本書的讀者應該都是Windows的“專家級”用戶,一定很清楚Windows可以同時運行多個程序,比如在使用瀏覽器上網沖浪的同時,使用多線程下載工具從互聯網上下載文件,或者同時打開一個MP3播放器播放音樂……
從操作系統的角度來看,這些正在運行的程序都是“進程(Process)”。
15.1.1 進程與程序
程序并行執行的功能由操作系統提供,操作系統使用“進程”將正在執行的不同應用程序相互隔離開來,以免“城門失火”(一個程序的崩潰),“殃及池魚”(其他程序也被影響)。
嚴格地說,進程(Process)是一個具有一定獨立功能的程序在一個數據集合上的一次動態執行過程。
這個定義太“理論化”了,用一句通俗的話取代它。
所謂“進程”,可以簡單地理解成“一個正在運行的程序”。
程序與進程的區別可以用圖形象地表達出來(見圖15-1)。
如圖15-1所示,程序源代碼編譯后會生成一個可執行文件,其中包含可以運行的機器指令(或虛擬機支持的指令)。此文件通常保存于計算機所配備的磁盤上。
當程序運行時,操作系統從磁盤上裝入此文件,在內存中分配一塊區域用于保存程序中的指令和數據,這塊區域就是這一程序所使用的內存空間,被稱為進程的“地址空間(Address Space)”。在32位的Windows操作系統中,進程的地址空間有2GB大小,這就是說:一個進程可以使用2GB大小的“虛擬”內存,注意,這是“虛擬”的,并不代表安裝于某臺計算機上的真實的物理內存數量。在后面的小節中還將對進程的內存分配方式作更詳細的介紹。
將可執行文件裝入內存之后,程序才能運行。

圖15-1 進程與程序
15.1.2 操作系統的進程管理
每個正在運行的程序都對應著一個獨立的進程,當這些程序裝入內存開始執行時,操作系統會為每個進程創建好相關的數據結構(其實可將其看成是一張大表,保存了操作系統用于管理進程的各種相關控制信息)。
由于操作系統可以同時裝入多個程序,為此必須有一種方法來保證這些同時運行的程序彼此不會相互影響,不會由于一個程序出現異常而直接影響其他程序甚至是操作系統的正常運行。
位于操作系統核心的“進程管理”模塊負責管理并行執行的多個程序。
操作系統的用戶模式與核心模式
為了避免應用程序有意無意地修改操作系統核心數據,Windows設計了兩種代碼運行環境:用戶模式(User Mode,也可被翻譯為“用戶態”)和核心模式(Kernel Mode,也可被翻譯為“核心態”)。
普通的應用程序運行于用戶模式中,而操作系統的關鍵代碼(比如負責分配與回收內存、創建和銷毀進程等功能的代碼)運行于核心模式下。
運行于核心模式下的代碼可以訪問所有的系統內存和執行所有的CPU指令,用戶模式下運行的代碼則只擁有“有限的權限”,不能“為所欲為”,比如不能訪問某些內存單元。
當用戶模式下的應用程序需要訪問系統核心數據時,它必須發出一個“系統調用”提出訪問核心數據的申請,操作系統內核在接到此申請之后,由運行于核心模式下的代碼去訪問這些數據,然后將結果再“轉發”給處于用戶模式下的代碼。
在Windows中,“系統調用”主要指Win32 API中的特定函數,所以,Windows應用程序通過調用Win32 API函數來實現從“用戶模式”到“核心模式”的轉換。
句柄與系統核心對象
位于操作系統內核中,僅允許運行于“核心模式”下的代碼訪問的數據被稱為“核心對象(Kernel Object)”。核心對象所包容的數據通常都是操作系統正常運行所必需的,比如用于管理進程的進程表。
提示
這里所談到的操作系統“核心對象”是借用面向對象理論中的“對象”概念,因為兩者都可以看成是對數據的一種封裝。但要注意,面向對象理論中的“對象”擁有一些操作系統“核心對象”所不具備的特征,比如支持多態、擁有構造函數和析構函數等。
出于易于理解的目的,可以將系統核心對象簡單地類比于C語言中的結構體(struct)變量。
操作系統在運行時,會在系統核心不斷地創建和銷毀“核心對象”,為了便于跟蹤和訪問這些對象,操作系統為這些對象分配了標識,這是一個32位的整數,被稱為“句柄(Handle)”。許多Win 32 API函數通過句柄來定位所要訪問的系統核心對象。
提示
句柄實際上是一個整數值,是相對于進程的。它實際上代表的是進程句柄表中的索引,在進程句柄表的對應行中,保存的是核心對象真正的地址。后文馬上就會介紹進程相關的數據結構。
舉個例子,請看以下Win32 API函數WaitForSingleObject的聲明:
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
可以看到此函數的第1個參數就是一個句柄。當運行于“用戶模式”下的應用程序調用此函數時,它會阻塞等待,直到此句柄所標識的系統核心對象的狀態轉換為“signaled”狀態,為了避免無限期等待可能帶來的問題,可以使用此函數的第2個參數指定一個最長的等待時間。
另外,還有不少的Win32 API函數可以創建系統核心對象,這些函數通常會將所創建的系統核心對象的句柄返回給應用程序,之后應用程序就可以通過這個句柄來訪問核心對象。例如Win32 API函數CreateThread可用于創建一個線程,函數返回線程對象的句柄給應用程序:
HANDLE WINAPI CreateThread(……);
操作系統采用引用計數法來決定銷毀核心對象的時機。
當一個線程調用某個Win32 API函數創建一個核心對象之后,此核心對象的初始引用計數為1,應用程序代碼中對其句柄的每次引用都會導致引用計數加1。作為一個編程原則,線程用完核心對象之后須關閉句柄(可以通過調用Win32 API中的CloseHandle函數關閉句柄),這時,核心對象的引用計數值減1,當其值減為0時,操作系統銷毀這一核心對象,并回收其占用的各種資源。
當線程調用CloseHandle函數關閉核心對象時,線程所屬進程的句柄表中相應的內容被清空,從而應用程序將無法再訪問此核心對象。但要注意,這時核心對象是否被銷毀則取決于其引用計數。因為完全有可能另一個進程也在使用同一個核心對象,因此其引用計數不會為0。
如果使用Visual C++開發Windows應用程序,則上述工作需要軟件工程師的深度參與,因為創建一個“普通對象”和創建一個“核心對象”的代碼是有很大差別的。
幸運的是,在.NET托管環境中,.NET應用程序對“普通對象”和“核心對象”不加區分,使用new關鍵字就可以創建任何一種類型的對象,而對象的銷毀工作由CLR負責。
交叉鏈接
Windows定義了許多種不同用途的核心對象,在第17章《線程同步與并發訪問共享資源》中介紹的許多用于線程同步的對象都是對操作系統核心對象的一個封裝,比如互斥信號量對象(Mutex)其實就是對Windows核心對象Mutex的一個托管封裝。通過這種封裝,.NET程序員就可以使用C#的new關鍵字在應用程序中直接創建一個Mutex對象,無須寫代碼去調用Win32 API中的相關函數。
與進程相關的數據結構
在Windows中,操作系統為每個進程創建的“大表”稱為“EPROCESS”,這是一個復雜的數據結構,包含有相當多的數據項(見圖15-2,其中僅繪出了與應用軟件開發工程師關系密切的數據項)。

圖15-2 EPROCESS中的數據項
如圖15-2所示,進程的管理信息主要由EPROCESS結構表達,EPROCESS有一個數據項保存了一個指針,引用位于用戶地址空間中的“進程環境塊(Process Environment)”。
進程環境塊中也有很多數據項,其中與應用軟件工程師直接相關的就是“線程局部存儲區(Thread Local Storage,TLS)”和“進程堆(Process Heap)”。
線程局部存儲區用于保存線程的“私有”數據,此數據僅供線程自己訪問,謝絕其他人的“拜訪”。
而“進程堆”讀者應該很熟悉了,當使用new關鍵字創建一個引用類型的對象時,此對象的數據就保存在與此進程相關聯的“堆”中。
現在回過頭來再看看位于系統地址空間中的EPROCESS結構。其中最引人注目的是第一項“核心進程塊(Kernel Process Block)”,有時又將它稱為“進程控制塊(Process Control Block,PCB)”,Windows使用KPROCESS結構來表示它,這也是一個很復雜的數據結構,其中保存了進程所創建的所有線程清單、進程當前狀態、所占用的處理器時間等信息,操作系統根據這些信息進行線程調度,即分配CPU給特定的線程運行。
EPROCESS結構中另一個很重要的信息就是“句柄表(Handle Table)”。句柄表中的每一項都引用著一個操作系統核心對象。進程每創建一個核心對象,就會在此句柄表中保存此核心對象的句柄,而進程關閉一個句柄時,此句柄表中的對應行被清空。
當進程銷毀時,它的句柄表中引用的所有核心對象的引用計數都會減1。
擴充閱讀
非托管應用程序的“資源泄露”問題
在非托管應用程序中,如果程序員創建了一個核心對象,卻忘記了及時關閉其句柄,那么在進程的整個生命周期中,此核心對象將始終存在,并占用寶貴的系統資源。類似地,如果程序員在進程堆中創建了一個普通對象,忘記及時銷毀它,這兩種現象都被稱為“資源泄露”。
在非托管應用程序中解決資源泄露的基本策略是“成對編程”,比如調用Win32 API函數創建了一個核心對象,用完之后馬上就調用CloseHandle函數關閉它。在C++程序中使用new關鍵字創建一個對象,用完之后應馬上使用delete關鍵字銷毀它。
需要指出的是,“資源泄露”只發生于進程還“活著”的時刻,如果進程被中止了,操作系統會將它所引用的所有核心對象引用計數減1,并回收整個進程堆,“資源泄露”也就不可能再發生了。
那如何判斷進程是“活著”還是“死了”?
很簡單:只要進程所創建的所有線程還有一個“活著”,進程就“活著”,它引發“資源泄露”的可能性就始終存在。
進程的創建與運行
在Windows中,使用Win32 API中的CreateProcess函數創建一個進程,應用程序發出對CreateProcess函數的調用指令之后,操作系統其實是啟動了一個復雜的處理流程,簡單地說,首先是打開可執行程序文件讀取相關信息,然后創建與進程相關的系統核心對象,初始化進程核心對象的各個屬性,緊接著為其創建第一個線程(稱為“主線程(Main Thread)”),然后主線程執行應用程序的入口函數,至此整個進程創建完成并且投入了運行。
可以通過Windows任務管理器來查看正在運行的進程信息。從“查看”菜單中選擇“選擇列…”命令,可以顯示進程的其他信息,這些信息主要來自每個進程所對應的EPROCESS結構(見圖15-3)。

圖15-3 操作系統正在運行的進程
進程資源分配
操作系統除了要為進程創建多個系統核心對象,還要為進程分配它可以使用的內存空間。這一內存區域其內存單元地址的總和被稱為“進程的地址空間”。這一地址空間依據其用途,又可以分為不同的子區域,請看圖15-4。

圖15-4 Windows進程的地址空間
圖15-4所示為Windows Server 2003(32位系統)中默認的進程地址空間分配方式。從地址“00000000”到“7FFF FFFF”一共2GB的空間為用戶進程空間;從“80000000”到“FFFF FFFF”為操作系統占用的系統空間,也有2GB,一般情況下應用程序不能直接訪問這部分空間。
因此,每個32位的Windows應用程序都可以訪問多達2GB的用戶進程空間。
計算機所擁有的內存分為兩種類型,一種是“物理內存”,在“實體上”體現為內存條(目前常見的內存條規格有512MB、1GB、2GB等),計算機主板中插上的多個內存條容量之和,就是此計算機所擁有的“物理內存”。
另一部分是“虛擬內存(Virtual Memory)”,它是進程所能訪問的所有內存單元的地址的集合,這其中包括了物理內存。虛擬內存很大,前面說過,在Windows NT平臺上,應用程序可以使用高達2GB的內存單元。
需要注意的是,計算機中真正意義上的內存是指“物理內存”,其容量是很有限的,比如某臺PC機只安裝有一條512MB的內存,那怎么說一個應用程序可以使用多達2GB的內存單元?多出來的內存從何而來?
答案是這部分內存可以從硬盤中得來。
Windows操作系統在硬盤上專門劃分出一塊區域,這部分區域被操作系統模擬成與物理內存一樣的“存儲區”,稱為“虛擬內存分頁文件”,應用程序可以用同樣的代碼訪問“真正的”物理內存和用硬盤空間“虛擬”出來的內存空間,程序員只需指定一個內存單元地址,操作系統會根據這個地址自動判斷這些數據是在物理內存中還是在虛擬內存分頁文件中,如果此地址對應的數據已在物理內存中,則可以直接訪問,否則操作系統會將要訪問的數據從虛擬內存分頁文件讀入物理內存。在這個數據的“換入”過程中,有可能還同時發生一個“換出”過程,即操作系統將物理內存中一些暫時不用的數據保存到硬盤上的虛擬內存分頁文件中,以挪出空間裝入進程所需要的數據。
這里面的關鍵就是:
只有保存于物理內存中的數據才能被CPU處理。
提示
在Windows操作系統中,硬盤根目錄下通常有一個隱藏的系統文件pagefile.sys,它就是Windows的虛擬內存分頁文件,其容量高達數百兆字節甚至是上吉(G)字節。
為了提高效率,操作系統將所有內存進行分頁,每頁包容若干個內存單元,內存數據的換入和換出以頁為基本單位。
當進程需要訪問的頁不在物理內存時,操作系統引發一個“缺頁中斷”,有一個專門的操作系統線程負責將新頁調入物理內存,同時將部分舊的頁面換出到虛擬內存分頁文件中。
為了實現虛擬地址與物理地址的對應,操作系統設置了一個進程頁表(EPROCESS塊中有一個數據項指向此頁表),當進程按虛擬地址訪問內存時,操作系統查此頁表即可確定要訪問的頁是否在物理內存中(可參看圖15-4)。
操作系統這種對內存的管理方式稱為“虛擬頁式存儲管理”。
15.1.3 進程中的線程
除了內存,CPU也是一個非常重要的資源,操作系統是一個“冷血的資本家”,它要“盡量榨取CPU的每一點時間”,不能讓CPU光耗電而不干活。
各種進程要完成的任務是不同的,有些任務需要占用CPU進行大量的計算,比如正在加密或解密一大批的數據,或者正在進行某個復雜的數學運算;而有些任務則很少需要CPU的參與,比如將一個文件從磁盤的某個位置復制到另一個位置;還有些任務必須等待某些條件滿足之后才開始執行(典型實例是Windows的自動系統更新機制),因此操作系統必須能區分開這些情況:
當一個進程正在等待某個條件滿足時,讓其他的進程使用CPU。
從前面介紹的內容可知,操作系統會為進程分配大量的資源。因此如果操作系統直接以進程作為調度CPU運行的基本單位,那么當進程投入運行和退出運行時,必然要花費大量的計算資源在進程運行環境的切換上。這會嚴重地影響操作系統的性能。為了避免出現這種情況,操作系統將進程切分為多個“線程(Thread)”,雖然線程也需要有一個運行環境,但這個運行環境往往只涉及到當前一些寄存器的值,數據量小,切換速度快得多。
按照線程而不是進程來分配CPU,這是Windows操作系統所采用的標準方法。
一個進程可以劃分為多個線程,也可以只有一個線程。
線程是CPU調度的基本單位,它擁有一個線程標識(Thread ID),一組CPU寄存器,兩個堆棧(Thread Stack)和一個專有的線程局部存儲區(Thread Local Storage,TLS)。
屬于同一個進程的線程共享進程所擁有的資源。
只有一個線程的進程稱為“單線程”進程,擁有多個線程的進程稱為“多線程”進程。對應地,將運行時只有一個線程的程序稱為“單線程”程序,運行時擁有多個線程的程序稱為“多線程”程序。
關于線程與進程,有以下結論:
進程是系統分配各種資源(比如內存)的單位,而線程則是操作系統分配CPU(即處理機調度)的基本單位。
舉個例子,假設目前操作系統中只有兩個進程在運行,進程A創建了10個線程,而另一個進程B只有2個線程,則操作系統會按照12個線程來分配CPU時間,這樣一來,A進程有機會獲得CPU的機率就大于B進程。
線程的最大特點是它們是可以并行執行的。就是說,在某個時間段內,可以同時有多個線程在運行。
擁有兩個以上CPU的計算機的線程的確是并行執行的,但在只有一個CPU的計算機中,由于CPU一次只能執行一個線程的代碼,所以操作系統使用一種“時間片輪轉”的方法來制造一個線程“同時”運行的“假象”(見圖15-5)。
如圖15-5所示,操作系統將時間分成非常短的小時間片,并輪流為每個線程分配一個時間片。當前執行的線程在其時間片結束時被掛起,操作系統選擇另一個線程運行。

圖15-5 操作系統使用“時間片”來給線程分配CPU
為保證被中斷的線程能在以后重新投入運行,并且能在原先工作的基礎上“繼續”,在剝奪一個線程的CPU使用權時,必須將它的運行環境保存起來。
線程的運行環境被稱為“線程上下文(Thread Context)”,包括為使線程在線程的所屬進程地址空間中繼續執行所需的所有信息,例如線程的CPU寄存器組、線程堆棧和線程局部存儲區。
當從一個線程切換到另一個線程時,操作系統將保存被換出線程的線程上下文,并重新加載投入運行線程的線程上下文。
時間片的長度取決于操作系統和處理器。由于每個時間片都很小,因此即使只有一個處理器,多個線程看起來似乎也是在同時執行。此即單處理器操作系統多線程管理的“宏觀上并行,微觀上串行”實現機理。
由于各個線程可能處理不同類型的工作,而這些工作的重要性又不一樣,因此,操作系統根據線程處理工作的重要性為其設定了優先級,并為每個優先級立一個線程隊列。當一個線程正在運行時,如果有高優先級的線程需要使用CPU,則當前線程在完成當前時間片的運行之后,會被剝奪CPU的使用權,讓高優先級的線程投入運行。
15.1.4 CLR如何管理進程與線程
.NET是一個托管的運行環境,在其上運行的進程稱為“托管進程(Managed Process)”,而在托管進程中創建的線程自然就是“托管線程(Managed Thread)”了。
對應地,操作系統直接創建和管理的進程和線程被稱為“本地進程(Native Process)”和“本地線程(Native Thread)”。前一小節介紹的實際上都是本地進程和本地線程。
.NET Framework引入了面向對象的思想,用類來表示進程和線程。
Process類用于代表托管進程,它實際封裝的是操作系統的本地進程,每一個托管進程對象都對應著一個操作系統真實的本地進程。
Thread類用于表示托管線程,每個Thread對象都代表著一個托管線程,而每個托管線程都對應著一個函數(稱為“線程函數”),托管線程的執行過程就是線程函數代碼的執行過程,線程函數代碼執行完,線程也就執行完了。
提示
Thread類與操作系統真實的本地線程不是一一對應關系,它所代表的是一個“邏輯線程”,由CLR負責創建與管理。.NET Framework中另有一個ProcessThread類用于表示操作系統中真實的本地線程。
在操作系統中,線程直接運行于進程內部,而在.NET托管環境下,托管線程與托管進程之間還有一個中間層次,叫做“應用程序域(Application Domain)”。
讀者可通過一個實例來直觀地了解一下進程與線程,請看示例項目ProcessInfo(見圖15-6)。

圖15-6 “進程信息查看器”示例
如圖15-6所示,示例程序在左邊列表框中列出了所有的正在運行的進程,從中選擇一個進程,會在右邊的文本框中顯示出進程的相關信息。
下面以本節示例程序ProcessInfo為“標本”,逐個介紹它運行時所對應的ProcessInfo進程基本信息,可以讓讀者對操作系統如何管理進程有一個直觀的了解。
進程的標識
進程的唯一標識符(Id):2344
關聯進程的本機句柄(Handle):604
打開的句柄數(HandleCount):145
關聯進程的基本優先級(BasePriority):8
每一個進程都有一個唯一的標識符,它是一個int類型的整數,由Process類的Id屬性給出,當此進程消亡后,此標識符可以被其他進程所使用,但在任何時候,都只有一個進程擁有此標識符。
Handle是本進程句柄,此句柄實際上引用的是本進程所對應的操作系統核心對象。
進程的運行信息
進程啟動的時間(StartTime):2009/8/8 16:33:40
進程正在其上運行的計算機名稱(MachineName):.
進程的主窗口標題(MainWindowTitle):進程信息查看器
進程主窗口的窗口句柄(MainWindowHandle):983476
進程的用戶界面當前是否響應(Responding):True
進程的終端服務會話標識符(SessionId):1
進程終止時是否應激發Exited事件(EnableRaisingEvents):False
擁有圖形界面的程序都有一個主窗體,此主窗體也對應著一個句柄,當主窗體關閉時,進程也隨之結束。主窗體一般由主線程負責創建。
提示
在Windows Forms應用程序的Main方法體內,向Application.Run方法中傳入的窗體對象就是主窗體。
進程的內存分配情況
進程的虛擬內存大小(VirtualMemorySize):154288128
峰值虛擬內存大小(PeakVirtualMemorySize):158666752
VirtualMemorySize信息表明當前進程所使用的虛擬內存大小。操作系統將每個進程的虛擬地址空間映射到物理內存中加載的頁面,或者映射到磁盤上虛擬內存分頁文件中存儲的頁面。
PeakVirtualMemorySize信息表明進程啟動以來該進程使用的最大虛擬內存的大小。
進程允許的最大工作集大小(MaxWorkingSet):1413120
進程允許的最小工作集大小(MinWorkingSet):204800
進程的峰值工作集大小(PeakWorkingSet):13635584
物理內存使用情況(WorkingSet):13635584
“進程的工作集”是物理內存中當前對該進程可直接訪問的內存頁的集合。這些內存頁常駐內存,可供應用程序使用,而不會觸發缺頁中斷。
MaxWorkingSet和MinWorkingSet兩項信息表明了進程所能存取的最大和最小內存頁數量。每次創建進程資源時,系統都會保留等于該進程最小工作集大小的內存量。虛擬內存管理器會嘗試在進程處于活動狀態時至少保留最小的常駐內存量,但決不會保留超過MaxWorkingSet值的物理內存量。
PeakWorkingSet項信息則表明自啟動進程以來該進程使用的最大工作集內存大?。ㄟ@部分內存現在有一部分可能在硬盤上)。
WorkingSet項信息表明進程占用的物理內存數量。
在本示例中,可以看到ProcessInfo進程占用了13635584字節的物理內存空間(前面介紹的VirtualMemorySize屬性表明進程使用的虛擬內存為154288128字節,兩個數字之間有差額,表明此進程的一部分內存頁被放到了虛擬內存分頁文件中)。
分頁的系統內存大小(PagedSystemMemorySize):217612
分配給此進程的未分頁的系統內存大小(NonpagedSystemMemorySize):7680
分頁的內存大小(PagedMemorySize):13672448
專用內存大小(PrivateMemorySize):13672448
操作系統的內存從用途上又分為兩類:
一部分是供操作系統自身使用的,稱為“系統內存(System Memory)”,分為分頁池內存和非分頁池內存兩部分。非分頁池的內存是連續的一塊內存,不分頁,“被鎖定”在物理內存中。
另一部分是供應用程序使用的。
上述信息表明,ProcessInfo當前在系統內存分頁池中占用了217612字節,在非分頁池中占用了7680字節。
同時,ProcessInfo進程還使用了13672448字節的分頁內存,而且這些內存還都是供此進程“獨自享用”的(因為PagedMemorySize=PrivateMemorySize),不與其他進程共享。
進程的執行時間
可安排此進程中的線程在其上運行的處理器(ProcessorAffinity):3
進程的特權處理器時間(PrivilegedProcessorTime):00:00:00.1404009
進程的總的處理器時間(TotalProcessorTime):00:00:00.3120020
進程的用戶處理器時間(UserProcessorTime):00:00:00.1716011
進程最終必須在CPU上執行,而一個進程的指令代碼可分為兩類:一類是由程序員編寫的完成某種工作的代碼,操作系統在“用戶模式”下運行這部分代碼;另一類則稱為“特權指令”,由操作系統提供。程序員在代碼中通過“系統調用”來請求操作系統執行這些指令,這些指令往往需要訪問操作系統的核心對象,并使操作系統進入“核心模式”。
CPU執行第一部分“用戶模式”代碼所花費的時間就是UserProcessorTime這一項提供的信息,而CPU執行第二部分即操作系統特定功能所花費的時間就是PrivilegedProcessorTime這一項提供的信息。兩者相加,即為CPU執行進程所花費的時間。
注意“ProcessorAffinity”這個屬性,中文譯為“處理器親和性”,其意思是指此進程可以在哪個CPU上運行。
現在的個人電腦通常都是雙核的,默認情況下進程平等地看待這兩個CPU,它所包容的線程可以運行于任何一個CPU上。然而,在某些情況下,可能希望進程只運行于特定的CPU之上,圖15-7所示的計算機中擁有8個CPU,分置于兩個主板之上,可以看到每4個CPU共享同一物理內存。

圖15-7 一個多處理器的計算機系統
很明顯,如果能限制某一進程只能在某塊主板上的4個CPU上運行,而不是將其分散到位于兩個主板上的8個CPU上運行,這就避免了在不同主板物理內存間傳送數據的開銷,有可能獲得更佳的性能。
那么,如何設置進程只能運行于特定的CPU之上?
其實很簡單,直接給ProcessorAffinity賦值就行了。將ProcessorAffinity值轉換為二進制,從右到左,每個CPU都對應一個位,當希望進程運行于此CPU時,將相應的位置1。請看表15-1。
表15-1 ProcessorAffinity值
查看表15-1,再對照ProcessInfo示例程序的輸出,就很清楚了:ProcessInfo示例程序中的所有線程可以自由地在本機所擁有的兩個CPU上運行。
進程中裝載的模塊信息
進程中要執行的代碼可來自于多個軟件組件,如圖15-8所示,ProcessInfo進程共加載了一個EXE文件(即ProcessInfo.exe文件自身)和多個DLL(比如ntdll.dll和MSCOREE.DLL)。這些文件被稱為“模塊(Module)”。

圖15-8 模塊信息
可以在示例程序中選擇一個模塊,下部的文本框中即顯示出相關的模塊信息,比如MSCOREE.DLL模塊的信息如下:
模塊的完整路徑(FileName):D:\Windows\SYSTEM32\MSCOREE.DLL
加載模塊所需內存量(ModuleMemorySize):294912
加載模塊的內存起始地址(BaseAddress):1893138432
系統加載和運行模塊時運行的函數的內存地址(EntryPointAddress):1893150444
進程中啟動的線程信息
示例程序還可以查看特定進程啟動的所有線程信息(見圖15-9)。

圖15-9 進程啟動的線程
以正在運行的ProcessInfo進程為例,它啟動了5個線程,其中線程3016的信息如下:
線程的基本優先級(BasePriority):8
線程的當前優先級(CurrentPriority):10
是否讓操作系統自動調整線程優先級(PriorityBoostEnabled):True
線程的優先級別(PriorityLevel):Normal
在操作系統內核中運行代碼所用的時間(PrivilegedProcessorTime):00:00:00
操作系統調用的、啟動此線程的函數的內存地址(StartAddress):2006614552
操作系統啟動該線程的時間(StartTime):2009/8/8 18:55:45
此線程的當前狀態(ThreadState):Wait
此線程使用處理器的時間總量(TotalProcessorTime):00:00:00
關聯的線程在應用程序內(不是在操作系統內核)運行代碼所用的時間(UserProcessorTime):00:00:00
線程等待的原因(WaitReason):UserRequest
有關線程的內容,將在后續的小節中詳細介紹,此處不再贅述。
ProcessInfo示例技術要點
在示例項目ProcessInfo中,進程和線程信息的獲取主要是通過訪問Process、ProcessModule和ProcessThread三個類的屬性實現的,代碼很簡單,請讀者自行閱讀項目源碼了解相關的技術細節。
擴充閱讀
神秘的Idle進程
當使用ProcessInfo工具查看正在運行的進程時,會發現有一個名為Idle的進程,ProcessInfo無法列出其詳細信息。這說明這一進程是一個特殊的進程。
我們知道,進程是正在執行的程序,因此一個進程應該對應著相應的可執行文件,但事實上并不存在Idle這一進程,也找不到和Idle進程相關聯的可執行文件。
事實上,Idel進程是一些進程信息查看工具(比如“Windows任務管理器”,或者是.NET基類庫中的Process組件)僅僅是出于方便管理角度而“創建”的,它是一個“虛進程”,而且它在不同的進程信息查看工具中顯示的名字可能還會不一樣。這個“虛進程”可以看成是操作系統空閑線程的容器。所謂“空閑線程(Idle Thread)”,就是指CPU“什么也不干”的情況,由于CPU“什么也不干”,所以空閑線程也是“虛”的,它只是代表了未被真實進程所占用的CPU使用率。
有意思的是:Windows 7的“Windows任務管理器”中不再顯示這個進程,但早期版本的“Windows任務管理器”中始終可以看到這一進程的蹤跡。
- Getting Started with Gulp(Second Edition)
- Apache Oozie Essentials
- PHP+MySQL網站開發技術項目式教程(第2版)
- 游戲程序設計教程
- Oracle BAM 11gR1 Handbook
- 數據結構(C語言)
- OpenShift在企業中的實踐:PaaS DevOps微服務(第2版)
- ArcGIS By Example
- SharePoint Development with the SharePoint Framework
- Java EE企業級應用開發教程(Spring+Spring MVC+MyBatis)
- Spring+Spring MVC+MyBatis從零開始學
- MongoDB Cookbook(Second Edition)
- Java EE架構設計與開發實踐
- Python物理建模初學者指南(第2版)
- Java RESTful Web Service實戰