[C++]《Windows核心編程》讀書筆記
這篇筆記是我在讀《Windows核心編程》第5版時做的記錄和總結(部分章節是第4版的書),沒有摘抄原句,包含了很多我個人的思考和對實現的推斷,因此不少條款和Windows實際機制可能有出入,但應該是合理的。開頭幾章由於我追求簡潔,往往是很多單獨的字句,后面的內容更為連貫。
海量細節。
第1章 錯誤處理
1. GetLastError返回的是最后的錯誤碼,即更早的錯誤碼可能被覆蓋。
2. GetLastError可能用於描述成功的原因(CreatEvent)。
3. VS監視窗口err,hr。
4. FormatMessage。
5. SetLastError。
第2章 字符和字符串處理
1. ANSI版本的API全部是包裝Unicode版本來的,在傳參和返回是多了一次編碼轉換。
2. MS的C庫的ANSI和Unicode版本之間也是沒有互相調用關系的,沒有編碼轉換開銷。
3. 寬字符函數:_tcscpy,_tcscat,_tcslen。
4. UNICODE宏是Windows API使用的,而MS的C庫中,對於非標准的東西用_前綴區分,所以_UNICODE宏是MS的C API使用的。
5. MS提供的避免緩沖區溢出攻擊的函數在<StrSafe.h>文件中,包括StringCbCat和StringCchCat等函數(其中Cb表示Count of Byte,Cch表示Count of Character,都用於表示衡量目標緩沖大小的單位);另外<TChar.h>中有_tcscpy_s等_s后綴的函數。在源串過短時,<StrSafe.h>的函數截斷,<TChar.h>的函數斷言。
6. 要想接管CRT的錯誤處理(比如assert),使用_set_invalid_parameter_handler設置自己的處理函數,然后使用_CrtSetReportMode(_CRT_ASSERT, 0);來禁止CRT彈出對話框。
7. Windows也提供了字符串處理函數,但lstrcat、lstrcpy(針對T字符的)已經過時了,因為沒考慮緩沖區溢出攻擊。考慮使用StrFormatKBSize、StrFormatByteSize、CompareString(有很多比較選項)、CompareStringOrdinal(相當於_tcscmp)。
8. GetThreadLocale返回線程的語言信息:LCID(Locale ID),供很多函數使用(包括使用CompareString針對語言來比較的時候)。
9. 寬字節轉多字節WideCharToMultiByte,反之MultiByteToWideChar。其中,在寬字節轉多字節的時候,如果有Unicode字符在多字節編碼中沒有對應項,那寬字節會被替換成參數lpDefaultChar,並且lpUsedDefaultChar會被標記為TRUE。當用這兩個函數計算結果串的大小時,返回的是字符數。
10. IsTextUnicode。
第3章 內核對象
1. 簡單區分內核對象和其他對象的方法:創建需要安全信息的多半是內核對象。
2. 每個進程有一個內核對象表,表的每一項是一個簡單結構,包括真實內核對象地址和訪問權限等。用戶代碼持有的內核對象句柄其實是對象表中對應項的索引。因此如果CloseHandle關閉一個對象后沒有清空變量,且在對象表的同樣位置恰好又創建了一個新的內核對象,對之前沒清空的無效變量的訪問會造成bug。(比如對同一個句柄多調用了一次CloseHandle導致另一個內核對象被關閉。)
3. 進程退出時,會釋放各種內存、內核對象、GDI對象等。
4. 跨進程使用內核對象的理由:跨進程傳輸:用文件映像對象實現共享內存、郵件槽和命名管道實現數據通信、信號量和互斥量進行同步等。
5. 跨進程使用內核對象的三種方式:對象句柄繼承、命名內核對象、復制對象句柄。
6. 對象句柄繼承:創建內核對象的時候可以指定SECURITY_ATTRIBUTES. bInheritHandle表示可繼承(任何時候可以使用SetHandleInformation修改可繼承性等屬性),創建子進程時指定CreateProcess的參數bInheritHandles為TRUE,則子進程從父進程的對象表中拷貝所有可繼承的對象到自己的對象表的相同表位置中(並增加引用計數),因為表項結構被完全拷貝且內核對象實際地址在地址空間后2G的內核地址段中,所以拷貝過來的表項完全有效,進而父子進程的可繼承內核對象的句柄值完全相同,於是只要以任何方式將要繼承的對象的句柄值跨進程交給子進程(創建子進程時的命令行參數、環境變量、共享內存、消息等手段),則后者可以使用。
7. 命名內核對象:要訪問已經存在的命名內核對象,可以使用CreateXXX或者OpenXXX,后者在對象不存在的時候返回NULL。如果打開了一個已經存在的命名對象,在打開時為API指定的對象名以外的參數被忽略。注意,一個進程打開同一對象兩次,除了增加引用兩次外,返回的句柄值是不同的,需要分別關閉一次,即打開和關閉完全對稱(很合理的行為)。在Vista及以上的系統,對象名可以包括在命名空間下,避免被低授權用戶訪問。
8. 復制對象句柄:DuplicateHandle。
第4章 進程
1. 進程是執行文件的運行時形態。包括兩部分:內核數據(對應內核對象)、地址空間(包括執行文件代碼和棧堆等動態內存)。
2. 把VC的“系統-子系統”值刪除掉,即不指定控制台或GUI,則編譯器會根據代碼中存在main或者WinMain來自動選擇子系統(這里不談Unicode了),很方便。
3. 啟動程序:根據子系統執行mainCRTStartup/WinMainCRTStartup,在該函數中干幾件事(1)准備命令行和環境變量(用於char *argv[]和char *env[])(2)初始化CRT的全局變量(包括_osver、_winmajor、_winver、__argc、_environ等)(3)初始化CRT運行庫的內存分配(malloc、free)、IO函數等(4)初始化全局對象調用C++構造函數。
4. 退出程序:main返回后mainCRTStartup會調用exit,exit干以下幾件事:(1)執行通過_onexit注冊的函數(2)執行全局對象的C++析構函數(通過atexit注冊的)(3)判斷_CrtDumpMemoryLeaks設置的內存泄漏檢測標志,嘗試檢測內存泄漏(4)調用ExitProcess。
5. HINSTANCE和HMOUDLE完全相同,都是表示映像文件加載到內存后的基址(鏈接器中可以配置)。GetModuleHandle傳入文件名可以獲得模塊基址;傳入NULL可以得到執行文件的HINSTANCE(即使調用者位於某個模塊中同樣返回應用程序基址);GetModuleHandleEx可以根據函數地址得到模塊基址
6. 訪問環境變量:char *env[]參數、GetEnvironmentStrings、GetEnvironmentVariable、ExpandEnvironmentStrings(將一個使用了類似”%USERPROFILE%”環境變量的字符串中的變量替換成值)。
7. 系統環境變量:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Enviroment。用戶環境變量:HKEY_CURRENT_USER\Enviroment。
8. 修改環境變量后可以通知相關的系統窗口(如控制面板等):SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM) “Enviroment”)。
9. 可以設置特定線程在一個CPU核心集合上執行。
10. SetErrorMode。設置該進程如何響應各種錯誤。
11. 關於相對路徑:在通過GetEnvironmentStrings返回的環境變量中,有一部分不是真正的環境變量,比如“=C:=C:\Windows”“ =F:=F:\Projects\Test05”,他們表示一種進程相關配置“本進程在特定驅動器下對應的當前文件夾”。一個進程除了有以上配置外,還有一個當前驅動器,最終GetCurrentDirectory返回的當前路徑就是當前驅動器+當前驅動器對應的當前文件夾。使用SetCurrentDirectory會改變該驅動器的當前文件夾,還會改變進程的當前驅動器(但這個API的改變並不會在GetEnvironmentStrings上體現出來,使用C函數_chdir可以同時改變兩者,故C函數更優)。進程剛啟動時,如果不考慮從父進程繼承的環境,則只有進程當前驅動有當前文件夾,其他驅動都無配置。使用相對路徑訪問文件的時候,其絕對路徑可以用GetFullPathName得到。”文件名”這樣的相對路徑的絕對路徑是GetCurrentDirectory() + “文件名”;”驅動器盤符:文件名”(注意不是”驅動符:/文件名”)這樣的相對路徑的絕對路徑就是”該驅動器的當前文件夾”(如果無配置,則是根目錄) + “文件名”。
看如下代碼:
_chdir("D:/Downloads"); // 修改D:的當前路徑為Downloads,且進程當前驅動器為D:
_chdir("F:/Projects"); // 修改F:的當前路徑為Projects,且進程當前驅動器為F:
std::ofstream("1.txt"); // 當前驅動器是F:,所以絕對路徑是F:/Projects/1.txt
std::ofstream("d:1.txt"); // D:的當前路徑是Downloads,所以絕對路徑是D:/Downloads/1.txt
這種行為從cmd的cd命令也可以看得出點端倪。
歸納:相對路徑訪問文件的時候,首先將相對路徑展開成絕對路徑,使用GetFullPathName,后者分兩步:首先判斷是否包含驅動器(以X:開頭),如果沒有,則在開頭添加進程當前驅動器;然后檢查是否以”X:/”開頭,如果沒有,則將”X:”展開成”X:/” + “對應驅動的當前文件夾”。兩步過后得到絕對路徑。
12. GetVersionEx獲取系統版本信息。VerifyVersionInfo檢測當前系統是否滿足版本需要。
13. CreateProcess的參數:關於lpApplicationName和lpCommandLine,有兩種用法:(1)前者指定應用程序路徑,后者指定參數(第一個參數前面要有一個空格,似乎底層會直接連接兩個串)(2)前者為NULL,后者指定路徑和參數,空格隔開。常用第二種方法。注意,lpCommandLine中由於是用空格分隔參數的,所以對其中含有空格的路徑一定要用內層引號括起來。另外CreateProcessW有一個奇怪的行為,它會修改參數lpCommandLine(似乎只在lpApplicationName為空的時候會修改),所以使用Unicode版本的時候傳入的該參數不能是常字符串(如L”Nodepad 1.txt”),而應該另外准備緩沖傳給該API供其修改,因為ANSI版本是調用Unicode版本的且在編碼轉換的時候內置了緩沖,所以CreateProcessA的lpCommandLine參數可以是常串(最終API會修改轉換編碼的臨時緩沖)。默認情況下,CUI的CUI型子進程會和父進程共享控制台,在參數dwCreationFlags中添加DETACHED_PROCESS或CREATE_NEW_CONSOLE標志可以阻止這種行為。在dwCreationFlags中添加CREATE_NEW_PROCESS_GROUP標志,可以控制進程組的組織,用戶按下Ctrl+C的時候同一進程組的所有進程得到通知。lpEnvironment指定為NULL的時候,底層為用GetEnvironmentStrings來填充。lpCurrentDirectory為NULL的時候,子進程繼承父進程的當前目錄。lpStartupInfo不能為空,至少要初始化結構為0並將cb賦為sizeof。使用STARTUPINFOEX結構作為lpStartupInfo參數,還可以具體指定子進程要繼承哪些父進程的可繼承內核對象(即使bInheritHandles參數為FALSE)。
14. cmd進程輸入命令行前顯示的路徑,就是其當前路徑(GetCurrentDirectory)。在CreateProcess時,cmd沒有設置子進程當前路徑,而資源管理器將路徑設置成子進程鏡像目錄。因為cmd的子進程會繼承cmd的當前路徑(lpCurrentDirectory為空的結果),因此最好在用cmd啟動程序的時候先將cmd的當前路徑設置為新進程的鏡像路徑。
15. 進程和線程結束后,句柄對象被標記為激活, WaitForSingleObject會返回。
16. CreateProcess后,可以使用WaitForInputIdle或類似函數來等待新進程初始化環境完畢開始運行。
17. WoW64:Windows 32 On Windows 64。所有64位windows運行着這個虛擬機,用來執行32位程序。判斷一個32位程序是否是運行在64位系統的32位虛擬機中:IsWow64Process。
18. 父進程創建子進程時使用的lpStartupInfo,在子進程中可以使用GetStartupInfo來查詢。
19. 創建一個子進程時,進程和主線程本身的存在就有了引用1,而調用CreateProcess的父進程又會有他們的引用所以計數到了2。要完全銷毀進程和線程,需要計數為0,所以除了需要進程本身結束外,引用的該進程的其他線程也要釋放引用。當然,CreateProcess過后父進程馬上CloseHandle並不會結束子進程,只是釋放自己的引用,使其計數為1,這是正常的行為。要確保某個進程或線程不被銷毀,不調用CloseHandle即可。如果進程本身已經退出了,但還有其他進程引用它,則它的地址空間被回收,只有內核對象還存在(比如這時再對句柄使用API查看內存,則內存信息為空),這也是為什么可以查看已經退出的進程的退出碼的原因(退出碼保存在內核對象中)。
20. 進程和線程的ID位於同一個系統頂層名空間。即任意進程的任意線程ID絕不可能和任意進程ID相同。這個ID會被系統循環利用。
21. GetProcessIDOfThread。
22. 進程只有在它所有線程都結束后才會結束。ExitProcess會殺死所有線程,所以可以直接結束進程,在主線程中調用ExitThread只會結束主線程(即,主線程創建一個死循環線程后自己_exitthreadex,這個進程不會退出。)。main返回后CRT調用exit后者再調用ExitProcess,所以在main中return可以直接結束進程。
23. 通過ExitProcess或ExitThread(單線程時)結束進程,由於這些API比CRT更底層,他們只能保證正確的釋放Windows資源(內存、內核對象引用),並不保證釋放C++資源(CRT底層資源、全局對象的析構函數),故一定要從main中返回自然的結束進程(其他原因在后面章節說明)。TerminateProcess也出於相同的原因應該避免使用。
24. CreateProcess創建的子進程會繼承父進程的Security Token權限,而ShellExecuteEx可以提高子進程的權限(令lpVerb參數為”runas”)。資源管理器使用前者創建子進程,所以通過它開打的程序都具有和資源管理器相同的權限。
25. 關於Vista及更高系統的UAC(User Account Control):Vista以前的系統如果以管理員賬號登陸,資源管理器(Explorer)會獲得一個管理員權限的Security Token,然后從資源管理器打開的子進程都會繼承這個最高權限,這種行為非常危險。Vista以后,即使以管理員賬號登陸,資源管理器仍然只持有一個一般權限的Token(Filtered Token),子進程如果想提升權限,有兩種途徑:(1)用戶“以管理員身份運行”啟動該進程(2)子進程自己提出請求要求用戶提升權限(子進程是安裝程序、或者子進程配置有.manifest文件說明權限需求)。另外,在很多軟件中出現有小盾牌圖標的按鈕,也是要求提高權限,點擊過后會結束當前進程,重啟一個高權限進程(如資源管理器中“顯示所有用戶的進程”按鈕)。其實這三種提高權限都是父進程調用了ShellExecuteEx。
26. IsUserAnAdmin判斷當前用戶是否是管理員。在Vista及以上的系統中,即使是管理員,進程也有可能因為篩選Token而不具備最高權限。
27. 枚舉所有進程:Process32First、Process32Next、EnumProcesses。
28. 可以從HMOUDLE中讀取IMAGE_DOS_HEADER和IMAGE_NT_HEADERS,進而從這些PE頭中取得模塊的推薦加載地址等信息。
29. PEB(Process Enviroment Block)包含了進程的啟動命令行、當前路徑等數據。該字段可以通過NtQueryInformationProcess的PROCESS_BASIC_INFORMATION參數取得。
30. 可以通過WinDbg的dt命令,查看一些結構的具體成員布局,如PEB等。
31. Windows完整性機制(Windows Integrity Mechanism):這是UAC之外的另一套安全機制,Windows通過在系統訪問控制表(SACL, System Access Control List)中增加訪問控制項(ACE, Access Control Entry)實現,每一種受保護的資源都有對應的完整性級別(Integrity Level),每個進程都有一個基於Token計算的完整性級別,如果進程的級別小於資源的級別,則不能訪問資源。提升Token權限之前的進程級別為中,提升后為高,而像IE這樣可以能執行網絡代碼的進程為低。可以通過GetTokenInfomation查看一些和完整性級別相關的策略。窗口系統也根據完整性級別,拒絕低級別者向高級別使用PostMessage、SendMessage等API。
32. Vista以上有一些進程是特殊的受保護進程,ToolHelp API對他們無效,因此無法查看進程信息。
33. GetProcessTime查看進程時間,GetProcessIoCounters查看IO次數。
34. GetProcessImageFileName返回內核格式的文件名。
第5章 作業
1. Job(作業),也就是進程組的概念,添加進同一個作業的進程能夠通過作業內核對象來集中控制,設置一些額外的屬性等。添加進一個作業就不能再移出。
2. IsProcessInJob、CreateJobObject、OpenJobObject。
3. 作業內核對象在它內部的所有進程都結束后才會被銷毀。
4. 細節:當客戶的作業句柄變量都被關閉后,即使作業對象還存在(因為進程沒有全部結束),也不能再通過作業名打開作業再操作了。
5. Vista以上,通過任務管理器創建的進程,都被添加進了一個獨立的作業;從命令行(cmd)創建的進程則不然。
6. 能夠對作業添加的限制:基本限制(限制進程時間、優先級、物理內存占用等)、擴展限制(基礎限制之上,還能限制內存使用總量,以及查看峰值內存使用)、UI限制(限制關機/重啟、訪問剪切板、切換桌面、改變顯示器設置、訪問作業外進程的句柄等)、安全限制(安全限制一旦設置,則不能修改)。SetInformationJobObject、QueryInformationJobObject用於設置和查詢限制。
7. AssignProcessToJobObject添加進程到作業。
8. 父進程位於某一作業中,子進程創建后也自動加入同一作業。除非作業的基本限制中包含JOB_OBJECT_LIMIT_BREAKAWAY_OK(允許進程時脫離作業),並且CreateProcess時指定CREATE_BREAKAWAY_FROM_JOB標記。
9. TerminateJobObject強制結束作業,同時結束作業內所有進程(等價於對作業內每個進程調TerminateProcess)。
10. QueryInformationJobObject除了查看作業限制外,也可以查看作業信息,包括總進程數、活躍進程數、總時間、總IO次數、進程ID列表等。
11. 作業結束后(所有內部進程結束),內核對象處於激活態,WaitForSingleObject返回。
12. 作業通知機制:將作業對象和IO完成端口綁定,作業中的事件(進程結束、時間到期、內存達到限制等)將通過完成端口事件來通知。
第6章 線程基礎
1. 像進程一樣,線程在數據上也分為兩個部分:線程內核對象(包括統計信息)、棧。(進程的兩個部分是,內核對象和地址空間)。---
2. 比起ExitThread和TerminateThread,應該讓線程的主函數返回來結束線程,否則一些棧對象不能正常析構(這里不再考慮CRT函數)。
3. 在C/C++編程中不要使用CreateThread、ExitThread,應該使用編譯器廠商提供的包裝函數,如MS的_beginthreadex、_endthreadex。因為使用前者,C/C++的CRT不能正常初始化和釋放線程相關資源(C/C++中有一些全局變量如errno和一些有內部狀態的函數strtok、asctime都需要通過TLS來正確實現,畢竟C庫函數的誕生早於多線程)。事實上,如果在C/C++中使用了CreateThread和EndThread,部分有內部狀態的函數還是可以正常使用的,因為這些函數內部會嘗試取得TLS,發現還未分配的話會自動分配,CRT的Dll版本庫也會在得到線程退出通知時嘗試釋放TLS,只是因為這份TLS是中途分配的信息不夠全面,部分狀態函數還是會有問題,因此在C/C++中還是要盡量使用后者。
4. 線程棧最大為CreateThread的dwStackSize參數和/STACK鏈接選項(VC中默認為1MB)兩者中的較大值。
5. TerminateThread的一些細節:該函數是異步的,函數返回時,線程還沒有結束,需要WaitForSingleObject;DllMain不會收到被Terminate線程的結束通知。
6. 只有當線程函數結束(正常返回或Exit掉)后,該線程的棧空間才會被回收(也就是說TerminateThread函數剛返回時被殺死線程棧空間還在,直到線程對象處於激活態)。
7. 對進程中的各個線程來說,ExitProcess和TerminateProcess都將導致對線程的TerminateThread調用,因此進程的main函數結束前,盡量確保工作線程都正常退出。
8. 大部分的資源都是進程相關的,窗口句柄和hook句柄是線程相關的,線程退出時會釋放他們(在C/C++中還有CRT的TLS變量)。
9. GetCurrentProcess、GetCurrentThread返回的都是偽句柄,如果想要把這個句柄保存下來在其他線程、進程中使用的話,是有歧義的,可以用保存ID來代替,如果一定要保存句柄的話,兩種方法:(1)DuplicateHandle(2)先GetCurrentThreadID,再OpenThread。
第7章 線程調度、優先級和關聯性
1. Windows線程調度的時間間隔(發生上下文切換的時間片)大概是15毫秒(GetSystemTimeAdjustment的lpTimeIncrement參數)。
2. 每個線程都有一個掛起計數,當計數非0的時候,該線程不參與線程調度。CreateThread、CreateProcess傳入特定的參數可以使計數初始化為1。SuspendThread可以增加計數,ResumeThread可以減少計數,兩者都返回新的掛起計數。顯然線程無法對自身調用ResumeThread。
3. 調試進程的WaitForDebugEvent返回后,被調試進程的所有線程被掛起,直到調試進程調用ContinueDebugEvent。
4. Sleep的休眠時間可能不精確,取決於線程調度時間片大小(一般是15毫秒左右)以及其他線程的運行情況。
5. Sleep(0)和SwitchToThread的區別在於:如果存在另一個更低優先級的線程,前者不會將CPU讓出,而后者會。即如果存在多個線程,SwitchThread總是讓出CPU。
6. YieldProcessor用於支持超線程技術的CPU切換超線程。
7. GetThreadTimes、GetProcessTimes返回指定線程或進程的內核代碼時間和用戶代碼時間(兩者都是絕對的CPU執行代碼時間,不包括調度過程中的中斷時間以及主動的Sleep或者Wait時間)。因此在對代碼段計時的時候,使用GetThreadTimes明顯優於GetTickCount等,因為后者得出的時間包括了其他線程的時間片。
8. 用於計時,最基本的有clock、GetTickCount、timeGetTime等;為了地提高精度,可以使用QueryPerformanceCounter;為了去掉因線程調度中斷的時間和Sleep、Wait的時間,可以使用GetThreadTimes、GetProcessTimes等。在Vista以上的系統中,有新的機制,可以使用ReadTimeStampCounter(對應GetTickCount)、QueryThreadCycleTime(不考慮中斷休眠,對應GetThreadTimes)、QueryProcessCycleTime等。對於沒有考慮線程調度影響的函數,可以先用SetThreadPriority提高優先級盡量獨占時間片。應該確保每次調用QueryPerformanceCounter的時候在同一CPU核心上,使用SetThreadAffinityMask。
9. 線程上下文(CONTEXT)保存在線程的內核對象數據中,主要包括線程相關的CPU寄存器狀態等。上下文有兩份,分別記錄內核和用戶模式,GetThreadContext只能返回用戶模式上下文,在調用該函數前應該確保用戶上下文不再改變了,即線程正處於內核態或者雖然在用戶態但已經調用過SuspendThread。
10. 先SuspendThread、再SetThreadContext改變線程上下文,可以改變執行流等,一般用於調試器 “跳到指定位置執行” 的功能等。
11. 高優先級線程可以被調度時(沒有Sleep、Wait等),低優先級線程得不到時間片;即使低優先級線程正在執行,一旦有高優先級線程可以調度,前者會被中斷並讓出CPU資源。
12. SetPriorityClass設置進程的優先級類,SetThreadPriority設置線程的相對優先級(相對於進程優先級類),二者共同決定線程的實際優先級(這個映射根據Windows版本不同而異,是一個0~31的整數,用戶不可訪問)。將線程的實際優先級設置為最高(31)是危險的,因為它將搶占系統資源,導致IO不能響應等。
13. 當線程有IO事件或消息到來時,操作系統會暫時提高線程的優先級;或者線程可調度但長時間(數秒)都得不到時間片的時候,系統也會暫時提高線程優先級。可以設置是否允許系統自動提升優先級:SetProcessPriorityBoost、SetThreadPriorityBoost。
14. 特定類型計算機的幾個相關CPU核心之間可以共享內存緩存等,因此Windows支持設置線程關聯CPU核心SetProcessAffinityMask、SetThreadAffinityMask。當然這組API也可以用於為特定線程提供專用CPU資源以提高性能。子進程默認繼承父進程的核心關聯設置。
15. SetThreadIdealProcessor設置線程最多可以使用的閑置CPU數量。該設置會覆蓋AffinityMask。
16. 進程的默認AffinityMask可以在鏡像文件頭中設置(因為沒有鏈接選項只有手工寫文件):ImageLoad->GetImageConfigInformation->ilcd.ProcessAffinityMask->SetImageConfigInformation->ImageUnload。
第8章 用戶模式下的線程同步
1. Interlocked系列函數:InterlockedIncrement(對應++)、InterlockedExchangeAdd(對應+=)、InterlockedExchange(對應=)、InterlockedCompareExchange(cas)。
2. _aligned_malloc可以指定分配內存的對齊邊界。
3. spinlock(自旋鎖)是CAS的應用。使用自旋鎖的時候因為有while(true) { …; Sleep(0); }這樣的循環,因此線程優先級不能太高,使用SetThreadPriorityBoost來禁用優先級提升,避免被自動提升后不會讓出CPU(或者使用SwitchToThread)。自旋鎖適用於單個線程不會占用資源太久的情況(因為一個線程占有資源期間,其他線程在循環檢測浪費CPU)。
4. CAS(InterlockedCompareExchange)必須是原語!必須!用C++編寫的CAS是不行的。
5. InitializeSListHead、InterlockedPushEntrySList、QueryDepthSList等API可以以Interlocked的方式操作一個單鏈表。
6. CacheLine:是Cache和內存通信的基本單位,可能是32/64字節等,CPU讀寫內存的時候會先將對應的CacheLine加載進Cache,修改完成后Flush到內存上。因此數據組織為CacheLine Size對齊、以及將只讀和讀寫數據分別組織到不同的CacheLine都能提高效率。多個CPU(或者具有獨立Cache的多個CPU核心)訪問同一地址時,該地址附近的數據會被多個Cache映射成各自的CacheLine,如果其中某個CPU修改了其CacheLine的數據,該CPU會通知其他CPU更新各自的CacheLine,這種行為會影響性能,故盡量避免跨線程共享數據以及利用AffinityMask盡量使用同一個CPU。
7. GetLogicalProcessorInformation提供CPU描述信息(比如能夠查詢到包括4個CPU核心,3級Cache,1、2級Cache為各個核心獨有,3級Cache為共享Cache,其Cache Line Size為64字節等)。
8. 所有線程都處於等待狀態數分鍾后,電源管理器介入。
9. volatile的作用:編譯器不會將變量優化成寄存器變量,即每次讀寫都會訪問內存。對struct應用該關鍵字會影響每個字段。
10. CRITICAL_SECTION內部記錄了擁有訪問權的線程以及引用次數。TryEnterCriticalSection如果返回TRUE,則已經增加了計數需要對稱調用LeaveCriticalSection。
11. CRITICAL_SECTION在實現上結合了spinlock(自旋鎖),調用EnterCriticalSection時發現資源正被占用需要切換到內核態休眠之前(切換到內核態開銷很大,高達數千CPU周期),可以嘗試進行一定次數的循環判斷。使用InitializeCriticalSectionAndSpinCount可以啟用結合自旋鎖功能(作為參考,用於保護進程堆的CS的SpinCount為4000),使用SetCriticalSectionSpinCount可以修改旋轉次數。當SpinCount為1的時候,關鍵段內部用於休眠和喚醒的事件對象會第一時間創建,而不是等到EnterCriticalSection的時候才創建。建議總是啟用自旋鎖。
12. Slim Reader/Writer Lock是性能比關鍵段更好的選擇,相比后者,它的缺陷是不能遞歸加鎖、且沒有TryLock。InitializeSRWLock、AcquireSRWLockShared(申請讀鎖)、AcquireSRWLockExclusive(申請寫鎖)。
13. 在都能完成任務的情況下,性能從高到底依次是:無鎖、volatile、Interlocked、SRW、CRITICAL_SECTION、內核對象(因為切換到內核態開銷很大)。
14. SleepConditionVariableCS、SleepConditionVariableSRW用法:已經獲得鎖(CS、SRW)的線程開始在一個ConditionVariable對象上睡眠,同時釋放鎖;如果其他線程Wakeup這個ConditionVariable對象,則函數返回TRUE,且再度獲得鎖;如果超時,返回FALSE,不會獲得鎖。應用:消費者獲得鎖后發現沒有產品於是開始休眠等待生產者產出產品后喚醒。
15. 技巧:按資源的邏輯個數而不是對象個數來組織鎖;需要加多層鎖的時候,總是按固定順序,比如按鎖的地址大小來依次加鎖,避免死鎖;通過拷貝資源等方式來減小鎖粒度。
第9章 用內核對象進行線程同步
1. 內核對象用於線程同步更靈活比如可以設置等待時間以及跨進程等,但開銷更大(需要切換到內核模式)。
2. 內核對象中都有一個表示觸發狀態的BOOLEAN值。
3. 進程和線程對象在結束前是非觸發,結束后是觸發狀態,其他時候不會再改變。
4. 文件對象有正在處理的異步IO請求時處於非觸發,其他時候觸發。
5. 控制台輸入句柄在沒有輸入的時候非觸發。
6. 內核對象觸發后,Wait在上面的線程被喚醒,決定哪一個線程首先被喚醒的規則基本上就是等待順序的先入先出,和線程的優先級等無關。
7. PulseEvent會在Event對象上產生一個觸發脈沖。近似於SetEvent(h);ResetEvent(h);兩句。
8. WaitableTimer在平時處於非觸發,第一次時間到或者之后周期性時間到都會處於觸發狀態。另外在SetWaitableTimer的時候可以傳入回調指定在觸發的時候往APC(Asynchronous Procedure Call)隊列中加入回調,但必須定時器觸發時線程正處於Alertable(使用SleepEx等帶Ex的API)狀態下才會入隊列(避免因為回調處理太慢及其他因素導致過量入隊)。一般定時器的APC和WaitFor兩種模式不混用。SetWaitableTimer指定第一次的時間時,正數表示絕對時間(SystemTimeToFileTime得到),負數表示相對時間。每次調用SetWaitableTimer會自動取消上次調用的設置,故兩次調用間不必CancelWaitableTimer。該定時器和基於消息的SetTimer定時器建議適時選用。
9. Semaphore的當前計數非0時處於觸發。ReleaseSemaphore增加計數發現達到最大時會返回FALSE,WaitFor減少計數到0的時候會休眠。
10. Mutex和CriticalSection在使用上完全相同,都記錄了Owner線程和遞歸次數。由於CriticalSection和Mutex記錄了Owner線程,因此需要該線程來釋放計數,如果在計數減少到0前線程退出了,則同步對象處於Abandoned(遺棄)狀態。對於Abandoned的情況,系統能檢測到發生在Mutex上的問題,並在底層自動釋放計數,只是WaitFor會返回WAIT_ABANDONED表示Mutex對象的計數是由系統自動回收的,該Mutex保護的資源可能處在未定義狀態。而CS的計數不會被自動釋放,一旦Abandoned則CS永遠的失效了。
11. WaitForInputIdle:進程中創建第一個窗口的線程的消息隊列中沒有需要處理的輸入消息后返回。
12. MsgWaitForMultipleObjects:等待的內核對象觸發后或者線程的消息隊列中有相應消息后返回。
13. SignalObjectAndWait增加一個對象計數的同時原子地等待另一個對象。能夠增加計數的對象只限於Event(SetEvent)、Mutex(ReleaseMutex)、Semaphore(ReleaseSemaphore),而等待的對象類型不限。使用:客戶端填充好請求於是通知服務端准備處理並等待服務端處理完畢。
14. 在Vista以上可以通過WCT(等待鏈遍歷,Wait Chain Traversal)相關API來追蹤死鎖。OpenThreadWaitChainSession、GetThreadWaitChain。
第10章 同步設備I/O與異步設備I/O
1. 打開設備的方式:文件-CreateFile,參數時路徑名或UNC路徑名。目錄-CreateFile,參數為路徑名或UNC路徑名,另外指定FILE_FLAG_BACKUP_SEMANTICS允許改變目錄屬性。邏輯磁盤驅動器-CreateFile,參數為””” \\.\x:”,打開后可以格式化和檢測大小等。物理磁盤驅動器-CreateFile,參數為””” \\.\PHYSICALDRIVEx”,(其中x為012等)。串口-CreateFile,參數為”” COMx”。並口-CreateFile,參數為”” LPTx”。郵件槽服務器-CreateMailSlot,參數為”\\.\mailslot\abcd”。郵件槽客戶端-CreateFile,參數為””\\serverName\mailslot\abcd””。命名管道服務器-CreateNamedPipe,參數為”\\.\pipe\abcd “。命名管道客戶端-CreateFile,參數為””\\serverName\pipe\abcd “。匿名管道-CreatePipe。套接字-Socket、accept、AcceptEx。控制台-CreateConsoleScreenBuffer、GetStdHandle。前面的設備路徑規則:””””\\服務器\設備”,其中如果在本機的話,服務器就是”” .”。
2. SetCommConfig可以設置串口波特率等屬性。
3. SetMailSlotInfo可以設置超時。
4. 一般用CloseHandle關閉設備。closesocket關閉套接字。
5. GetFileType可以返回設備的類型:FILE_TYPE_DISK-磁盤文件;FILE_TYPE_CHAR-字符文件,包括控制台和打印機等;FILE_TYPE_PIPE-命名管道或匿名管道。
6. 多次CreateFile打開同一個文件得到的是不同的內核對象,各自維護自己的文件指針等數據; DuplicateHandle得到的多個句柄仍然標志的是同一個對象。
7. CreateFile的dwShareMode參數:0表示獨占,如果文件已經被打開,則本次打開失敗;如果本次打開成功,在關閉前不能在其他地方打開同一個文件。FILE_SHARE_READ,如果本次打開前已經有寫句柄,本次打開失敗;如果本次打開成功,在關閉前在其他地方不能打開寫句柄。FILE_SHARE_WRITE也類似。FILE_SHARE_DELETE表示,如果本次打開成功,其他地方又刪除了文件,則刪除時只是打上刪除標記,待這里的句柄關閉后才真正刪除。
8. CreateFile的dwFlagsAndAttributes參數:(1)關於內置緩沖。內置緩沖至少有兩個作用,首先,加速,頻繁的小字節塊訪問會被緩沖為少數大字節塊的設備讀寫;其次,最底層設備訪問需要按一定的字節塊對齊(文件無緩沖讀寫需要按磁盤扇區大小對齊),緩沖屏蔽了這個限制,方便上層使用。FILE_FLAG_NO_BUFFERING,底層不提供緩沖,需要上層自己提供緩沖,緩沖區首地址、文件讀寫偏移/指針、讀寫字節數三者都必須按磁盤扇區大小對齊(扇區大小可以通過GetDiskFreeSpace獲得,比如512字節)。文件太大有可能打開失敗,也需要指定這個標記。當有緩沖時,FILE_FLAG_SEQUENTIAL_SCAN承諾會連續訪問(不會用SetFilePointer),因此底層可以嘗試緩沖更多連續內容;FILE_FLAG_RANDOM_ACESS表示會隨機訪問,因此底層會盡量不要緩沖太多(緩沖的作用還剩下避免要求扇區對齊)。FILE_FLAG_WRITE_THROUGH,表示寫文件不使用緩沖,這樣避免在數據Flush到文件前對象就被非法關閉導致數據丟失。(2)其他標志。(1)FILE_FLAG_DELETE_ON_CLOSE,關閉文件的時候刪除,適合臨時文件。FILE_FLAG_OVERLAPPED異步IO。
9. CreateFile的dwFlagsAndAttributes參數:只在創建文件的時候有效,用於指定ARCHIVE、ENCRYPTED(加密)、HIDDEN、READONLY、SYSTEM、TEMPORARY等屬性
10. CreateFile的hFileTemplate參數:只在創建新文件時有效,傳入另一個文件句柄的話,系統會忽略dwFlagsAndAttributes參數和直接使用該句柄對應的dwFlagsAndAttributes。
11. FILE_ATTRIBUTE_TEMPORARY和FILE_FLAG_DELETE_ON_CLOSE標記結合適用於臨時文件,前者會讓系統盡量將文件維護在內存而不是磁盤中,后者會在關閉句柄時刪除文件。
12. 獲取文件大小:GetFileSizeEx、GetCompressedFileSize(尤其針對壓縮屬性的文件)分別返回邏輯大小和磁盤上的實際大小。
13. SetFilePointerEx可以超出文件實際大小,超出后,除非寫文件或者SetEndOfFile否則文件不會變大。
14. SetEndOfFile是減小文件的唯一手段。
15. FlushFileBuffers。
16. 在Vista以上,可以用CancelSynchronousIo來中止一個線程的同步IO。
17. 異步IO的實際訪問設備順序不一定和請求順序(API調用順序)相同(比如驅動會根據磁盤磁頭位置選擇先處理距離最近的IO請求)。
18. 對異步IO的文件發出IO請求有可能是同步操作,因為可能數據正好在底層緩沖中可以立即完成。
19. 關於取消異步IO請求:(1)CancelIo取消調用線程在指定設備上的異步IO請求。(2)線程結束會取消該線程的所有異步請求。(3)關閉設備會取消所有該設備的請求。(4)CancelIoEx能取消調用線程以外線程在指定設備上的特定請求。(5)CancelIoEx能取消特定設備的所有請求。
20. OVERLAPPED結構的Internal表示錯誤碼,InternalHigh表示傳輸的字節。由於異步IO跟文件指針無關(文件指針來不及修改),所以偏移存儲在該結構中。
21. GetOverlappedResult函數實現為,訪問結構的Internal、InternalHigh字段,另外如果結構的hEvent為空嘗試Wait設備否則Wait事件(函數參數bWait為TRUE的時候)。
22. QueueUserAPC向線程的APC隊列拋出一個用戶自定義函數。
23. QueueUserWorkItem向線程池拋出任務。
24. 異步IO有四種方式得到完畢通知:(1)設備內核對象觸發。(2)OVERLAPPED的hEvent內核對象觸發。(3)APC回調(ReadFileEx)。(4)IO完成端口。
25. 異步IO-設備內對象觸發:對FILE_FLAG_OVERLAPPED的文件使用ReadFile,將OVERLAPPED的hEvent設置為空,IO完成時設備句柄將觸發,因此只能同時進行一次IO(瓶頸)。可以一個線程請求,另一線程響應完成。
26. 異步IO-事件內核對象的觸發:將OVERLAPPED的hEvent設置為事件以獲得通知。可以用SetFileCompletionNotificationModes來避免IO完成時去觸發設備對象。可以一個線程請求,另一線程響應完成。
27. 異步IO-APC隊列:ReadFileEx后使用SleepEx等讓線程進入Alertable狀態。同一個線程發出請求和響應完成(瓶頸)。
28. 異步IO-IO完成端口:步驟(1)CreateIoComplitionPort創建完成端口,指定活躍線程數(建議為CPU核心數)。(2)用CreateIoComplitionPort向完成端口添加異步設備。(3)創建完成端口服務線程(建議為CPU核心*2個,或者動態估計),初始化后使用GetQueuedCompletionStatus使線程和完成端口綁定並休眠。(4)執行異步IO,IO完成后底層會用PostQueuedCompletionStatus令正在GetQueuedCompletionStatus上休眠的服務線程蘇醒響應。細節:可以在OVERLAPPED的hEvent指定一個值為hEvent | 1的數,令IO完成后不發出完成通知(即不Post)。可以使用GetQueuedCompletionStatusEx來一次響應多個請求。完成端口服務線程中,使用GetQueuedCompletionStatus休眠的線程叫等待線程,從GetQueued…返回的線程叫釋放線程(活躍線程),活躍線程如果因其他原因(如Sleep、Wait)再掛起叫暫停線程,完成端口能夠檢測到各個線程的數量,會控制GetQueuedCompletionStatus的返回以使活躍線程盡量逼近創建完成端口時指定的數目。默認情況下異步IO即使同步完成,也會Post…,可以使用SetFileCompletionNotificationModes來禁用Post…。對於完成事件的響應是先入先出的,但服務線程的激活卻是后入先出的(盡量激活相同線程,其他線程長期休眠其棧內存可以換出到頁面文件提高性能)。
第11章 線程池的使用(第4版)
1. MessageBox彈出的對話框是可用修改的,FindWindow找到后,0x0000ffff是靜態文本框的控件ID等,因此很容易實現倒計時自動關閉的消息框。
2. 從win2000開始提供的線程池主要有4種用法:(1)異步調用函數(QueueUserWorkItem)。(2)定時器回調(CreateTimerQueueTimer)。(3)內核對象觸發后回調(RegisterWaitForSingleObject)。(4)內置IOCP實現(BindIoCompletionCallback)。
3. 線程池模塊下有幾種底層線程:(1)可變數量的長任務線程,用於執行標記為WT_EXECUTELONGFUNCTION的長時間回調。(2)1個Timer線程。所有CreateTimerQueueTimer調用都被轉發為在Timer線程上創建以APC方式通知的WaitableTimer,這個線程除了刪除和創建WaitableTimer外,就是在Alertable態下休眠等待定時器的APC。由於這個線程一旦創建就貫穿進程生命期不會銷毀,因此WT_EXECUTEINPERSISTENTTHREAD標志的線程池回調也由本線程執行。(3)多個Wait線程。服務於RegisterWaitForSingleObject,每個線程用WaitForMultipleObjects等待最多63(MAXIMUM_WAIT_OBJECTS減去一個用於維護對象數組的工作對象)個內核對象,對象觸發后執行回調。(4)可變數量的IO線程。由於發出異步IO請求(ReadFileEx)后,一旦請求線程結束,請求將被撤銷,因此請求被驅動執行完畢之前IO請求線程一定要存在,而線程池內的線程大都會根據CPU繁忙情況動態創建和刪除,因此線程池中有一部分線程被賦予了特殊行為,他們會檢測自己執行回調時發出的異步IO請求是否完成,如果沒有,就不會結束運行,這些追蹤自身發起的異步IO請求執行情況的特殊線程叫做IO線程。因此只能在線程池的IO線程上執行異步IO調用。(5)可變數量的非IO線程。線程池內部實現了一個IO完成端口,服務於BindIoCompletionCallback,其中IOCP的服務線程(在GetQueuedCompletionStatus上休眠)由於數量會根據CPU情況動態調整,不應用於執行異步IO,故叫非IO線程。
4. 四種用法中,如果Flags參數指定的回調執行線程與默認線程不符,底層可以使用QueueUserWorkItem來切換線程。比如CreateTimerQueueTimer用法的默認線程肯定是Timer線程,發現WT_EXECUTELONGFUNCTION標記后,使用Queue…來切換到專門執行長任務的線程避免阻塞Timer線程影響定時器功能。
5. 用法1-異步函數調用:QueueUserWorkItem 。Flags參數為0(WT_EXECUTEDEFAULT)的時候回調交給非IO線程執行(通過PostQueuedCompletionStatus通知非IO線程)。還可以指定WT_EXECUTEINIOTHREAD交給IO線程、指定WT_EXECUTEINPERSISTENTTHREAD交給Timer線程、指定WT_EXECUTELONGFUNCTION交給長任務線程等。
6. 用法2-定時器回調:CreateTimerQueue-創建專用TimerQueue。DeleteTimerQueueEx-刪除專用TimerQueue,參數CompletionEvent是用於接受刪除Queue完畢通知的事件對象,如果設置為NULL表示不接受通知,設置為INVALID_HANDLE_VALUE表示阻塞等待刪除完成。注意不能在Timer線程上的回調中以INVALID_HANDLE_VALUE為參數調用DeleteTimerQueueEx,因為后者實現為向Timer線程拋出一個要求維護Timer列表的APC,在線程的APC回調中拋出新的APC並且還阻塞等待,結果就是死鎖。CreateTimerQueueTimer-創建具體的Timer對象,TimerQueue參數指定為NULL表示在默認的Queue上創建對象,適用於Timer對象不多的用法。使用WT_EXECUTEINTIMERTHREAD標記即要求在Timer線程上執行回調,因不必切換線程效率較高,注意回調不能過長影響Timer線程的功能。ChangeTimerQueueTimer-改變Timer對象的一些參數。DeleteTimerQueueTimer-刪除Timer對象,注意使用INVALID_HANDLE_VALUE參數造成死鎖的可能。
7. 用法3-等待內核對象觸發回調:RegisterWaitForSingleObject-在內核對象觸發或超時后執行回調。標記WT_EXECUTEINWAITTHREAD表示在Wait線程上執行,效率較高。WT_EXECUTEONLYONCE只執行一次回調,適用於進程/線程句柄這種觸發后不再重置的對象。PulseEvent的脈沖可能不會被Wait線程檢測到(線程剛好在干其他事)。UnregisterWaitEx-取消回調,注意INVALID_HANDLE_VALUE參數可能的死鎖。
8. 用法4-內置IOCP實現:BindIoCompletionCallback。將異步IO設備和內置的IO完成端口管理起來,異步完成后執行回調。標志只能為0,默認在非IO線程(IOCP的服務線程)上執行,如果需要切換線程,手工QueueUserWorkItem。
