這篇筆記是我在讀《Windows核心編程》第5版時作的記錄和總結(部分章節是第4版的書),沒有摘抄原句,包含了不少我我的的思考和對實現的推斷,所以很多條款和Windows實際機制可能有出入,但應該是合理的。開頭幾章因爲我追求簡潔,每每是不少單獨的字句,後面的內容更爲連貫。編程
海量細節。windows
第1章 錯誤處理數組
1. GetLastError返回的是最後的錯誤碼,即更早的錯誤碼可能被覆蓋。緩存
2. GetLastError可能用於描述成功的緣由(CreatEvent)。安全
3. VS監視窗口err,hr。服務器
4. FormatMessage。網絡
5. SetLastError。多線程
第2章 字符和字符串處理app
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,一、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。