標籤:資料 設定 roc 結構 process 方便 http 函數參數 class
1.進程有兩個組成部分,一個進程核心對象和一個地址空間。線程也有兩個組成部分:
- 一個是線程的核心對象,作業系統用它管理線程。系統還用核心對象來存放線程統計資訊的地方。
- 一個線程棧,用於維護線程執行時所需的所有函數參數和局部變數。
2.線程要在其進程的地址空間內執行代碼和處理資料,假如一個進程上下文中有兩個以上的線程運行,這些線程將共用一個地址空間。這些線程可以執行同樣的代碼,可以處理相同的資料。此外,這些線程還共用核心物件控點,因為控制代碼表是針對每一個進程的。
3.為一個進程建立一個虛擬地址空間需要大量系統資源,系統會發生大量的記錄活動,這需要用到大量記憶體,而且由於.exe和.dll檔案要載入到一個地址空間,所有還需要用到檔案資源。而線程只有一個核心對象和一個棧,幾乎不涉及記錄活動,所有不需要佔用多少記憶體。
4.線程函數終止返回時,用於線程棧的記憶體也會被釋放,線程核心對象的使用計數會遞減,變為0時銷毀。
5.系統從進程的地址空間中分配記憶體給線程棧使用,新線程在與負責建立的那個線程在相同的進程上下文中運行。因此,新線程可以訪問進程核心對象的所有控制代碼、進程中的所有記憶體以及同一個進程中其他所有線程的棧。
6.調用CreateThread時,預定的地址空間容量設定了棧空間的上限,這樣才能捕獲代碼中的無窮遞迴bug。否則系統會將進程的所有地址空間分配殆盡,並為線程棧調撥大量實體儲存體。
7.線程可以通過以下4種方法來終止運行。
- 線程函數返回(強烈推薦)
- 線程通過調用ExitThread函數“殺死”自己(避免使用這種方法)
- 同一個進程或另一個進程中的線程調用TerminateThread函數(避免使用)
- 包含線程的進程終止運行(避免使用)
8.讓線程函數返回,可以確保以下正確的應用程式清理工作都得以執行。
- 線程函數中建立的所有C++對象都通過其解構函式被正確銷毀。
- 作業系統正確釋放線程棧使用的記憶體。
- 作業系統把線程的退出嗎(線上程的核心對象中維護)設為線程函數的傳回值。
- 系統遞減少線程的核心對象的使用計數。
9.ExitThread函數將終止線程的運行,並導致作業系統清理該線程使用的所有作業系統資源。但是C/C++資源(如C++類對象)不會被銷毀。
10.ExitThread是殺死主調線程,而TerminateThread能殺死任何線程。TerminateThread是非同步,返回時並不能保證線程以及被終止。而且使用這個函數時,除非擁有此線程的進程終止運行,否則系統不會銷毀這個線程的堆棧。
11.動態連結程式庫DLL通常會線上程終止運行時收到通知,但是如果調用TerminateThread殺死線程,則DLL不會收到這個通知。
12.線程終止運行時,會發生下面這些事情
- 線程擁有的所有使用者物件控制代碼會被釋放。在Windows中,大多數對象都是由包含了“建立這些對象的線程”的進程擁有。但一個線程有兩個使用者物件:視窗和掛鈎。一個線程終止運行時,系統會自動銷毀由線程建立或安裝的任何視窗,並卸載由線程建立或安裝的任何掛鈎,其他對象只有在擁有線程的進程終止時才會被銷毀。
- 線程的退出碼從STILL_ACTIVE變成傳給iExitThread或TerminateThread的代碼。
- 線程核心對象的狀態變為觸發狀態。
- 如果線程時進程中的最後一個活動線程,系統認為進程也終止了。
- 線程核心對象的使用計數遞減1。
13.線程終止運行時,其關聯的線程對象不會自動釋放,除非對這個對象的所有未結束的引用都被關閉了。
14.一旦線程不再運行,系統中就沒有別的線程再用該線程的控制代碼了,但是其他線程可以調用GetExitCodeThread來檢查線程是否終止運行,以及其結束代碼。
15.對CreateThread函數的一個調用導致系統建立了一個線程核心對象。該對象最初的使用計數為2.其他屬性也被初始化。一旦建立了核心對象,系統就分配記憶體,供線程的堆棧使用。此記憶體是從進程的地址空間記憶體配置的,因為線程沒有自己的地址空間。然後系統將pvParam和pfnStartAddr參數寫入線程棧的第一個和第二個值。
16.每個線程都有其自己的一組CPU寄存器,稱為線程的上下文。上下文反映了當線程上一次執行時,線程的CPU寄存器的狀態。線程的CPU寄存器全部儲存再一個CONTEXT結構中。CONTEXT結構儲存線上程核心對象中,
17.指令寄存器和棧指標寄存器時線程上下文中最重要的兩個寄存器。記住,線程始終在進程的上下文中運行,所以,這兩個地址標識的記憶體都位於線程所在進程的地址空間中。當線程核心對象被初始化的時候,CONTEXT結構的堆棧指標寄存器被設為pfnStartAddr線上程堆棧中的地址。而指令寄存器被設為RtlUserThreadStart函數的地址。
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam){ __try { ExitThread((pfnStartAddr)(pvParam)); } __except (UnhandledExceptionFilter(GetExceptionInformation())) { ExitProcess(GetExceptionCode()); } // NOTE: we never get here}
18.RtlUserThreadStart實際就是線程開始執行的地方。兩個參數是由作業系統將值顯示地寫入線程堆棧,而不是從另一個函數調用的。新線程執行RtlUserThreadStart函數的時候,將會發生以下事情:
- 圍繞線程函數,會設定一個結構化異常處理幀。這樣一來,線程執行期間所產生的任何異常都能得到系統的預設處理。
- 系統調用線程函數,把傳給CreateThread函數的pvParam參數傳給它。
- 線程函數返回時,RtlUserThreadStart調用ExitThread,將你的線程函數的傳回值傳給它。線程核心對象的使用計數遞減,而後線程停止執行。
- 如果線程產生一個未被處理的異常,RtlUserThreadStart函數所設定的SEH幀會處理這個異常。通常,這意味者系統會向使用者顯示一個訊息框,而且當使用者關閉此訊息框時,RtlUserThreadStart會調用ExitProcess來終止整個進程,而不只是終止有問題的線程。
19. RtlUserThreadStart內,線程會調用ExitThread或者ExitProcess。這意味著線程永遠不能退出此函數。
20.當RtlUserThreadStart調用你的線程函數時,它會將線程函數的返回地址壓入堆棧,使線程函數知道在何處返回。但是,RtlUserThreadStart函數是不允許返回的。如果它沒有在強行“殺死”線程的前提下嘗試返回,幾乎肯定會引起訪問違規,因為線程堆棧上沒有函數返回地址,RtlUserThreadStart將嘗試返回某個隨機的記憶體位置。
21.一個進程的主線程初始化時,其指令指標會被設為同一個未文檔化的函數RtlUserThreadStart,當RtlUserThreadStart開始執行時,它會調用C/C++運行庫的啟動代碼,後者初始化繼而調用你的_tmain或_tWinMain函數。你的進入點函數返回時,C/C++運行時啟動代碼會調用ExitProcess。所以對於C/C++應用程式來說,主線程永遠不會返回到RtlUserThreadStart函數。
22.VS附帶了4個C/C++運行庫用於機器碼的開發,還有兩個庫向,Microsoft.NET的託管環境。注意,所有這些庫都支援對線程開發,不再有單獨的一個C/C++庫專門針對單線程開發。
23.由於標準C運行庫是在1970年左右發明的,很久以後才在作業系統上出現線程的概念,所以多線程應用程式使用C運行庫會有問題,如設定errno等全域變數。
24.建立新線程時,一定不要叫用作業系統的CreateThread函數。相反,必須調用C/C++運行庫函數_beginthreadex。
25.對於_beginthreadex函數,需要重點關注以下幾點:
- 每個線程都有自己的專用_tiddata記憶體塊,它們是從C/C++運行庫的堆上分配的。
- 傳給_beginthreadex的線程函數的地址儲存在_tiddata記憶體塊中。
- _beginthreadex確實會在內部調用CreateThread,因為作業系統只知道用這種方式來建立一個新線程。
- CreateThread函數被調用時,傳給它的函數地址是_threadstartex(而非pfnStartAddr)。另外,參數地址是_tiddata結構的地址,而非pvParam。
- 如果一切順利,會返回線程的控制代碼,就像CreateThread那樣。任何操作失敗,會返回0。
26.關於threadstartex函數,以及其重點:
- 新的線程首先執行RtlUserThreadStart,然後再跳轉到_threadstartex。
- threadstartex唯一的參數就是新線程的_tiddata記憶體塊的地址。
- TlsSetValue是一個作業系統函數,它將一個值與主調線程關聯起來,這就是所謂的線程局部儲存。threadstartex函數將_tiddata記憶體塊與建立線程關聯起來。
- 在無參數的輔助函數_callthreadstartex中,有一個SEH幀,它將預期要執行的線程函數包圍起來。這個幀處理著與運行庫有關的許多事情,比如執行階段錯誤(如拋出未被捕捉的C++異常),和C/C++運行庫的signal函數。這一點相當重要,如果用CreateThread函數建立了一個線程,然後調用C/C++運行庫的signal函數,那麼signal函數不能正常工作。
- 預期要執行的線程函數會被調用,並向其傳遞預期的參數。
- 線程函數的傳回值被認為時線程的結束代碼。但是注意callthreadstartex不是簡單的返回到_threadstartex,繼而到RtlUserThreadStart,如果時那樣的話,線程會終止運行,其結束代碼也會被正確設定,但線程的_tiddata記憶體塊不會被銷毀。這會導致應用程式出現記憶體流失。為了防止這個問題,_threadstartex調用了_endthreadex,並向其傳遞結束代碼。
27.對於_endthreadex函數,需要注意以下幾點:
- C運行庫的_getptd_noexit函數在內部叫用作業系統的TlsGetValue函數,後者擷取主調線程的tiddata記憶體塊地址。
- 然後,_endthreadex將此資料區塊釋放,並叫用作業系統的ExitThread函數來實際地銷毀線程。當然,它會傳遞並正確設定結束代碼。
28.當一個線程調用一個需要_tiddata結構地C/C++運行庫函數時:
- 首先,C/C++運行庫函數嘗試取得線程資料區塊的地址(通過調用TlsGetValue)。
- 如果NULL被作為_tiddata塊的地址返回,表明主調線程沒有與之關聯的_tiddata塊。在這個時候,C/C++運行庫函數會為主調線程分配並初始化一個_tiddata塊。
- 然後,這個塊會與線程關聯(通過TlsSetValue),而且只要線程還在運行,這個塊就會一直存在並與線程關聯。
- 現在C/C++運行庫函數可以使用線程的_tiddata塊,以後調用的任何C/C++運行庫函數也都可以使用。
29.對於上述過程事實上,問題還是有的,假如線程使用了C/C++運行庫的signal函數,則整個進程都會終止,因為結構化異常處理(SEH)幀沒有就緒,從而導致記憶體流失。第二個問題是,假如線程不是通過調用_endthreadex來終止的,資料庫就不能被銷毀,從而導致記憶體流失。
30.當模組串連到C/C++運行庫的DLL版本時,這個庫會線上程終止時收到一個DLL_THREAD_DETACH通知,並會釋放_tiddata塊,可以防止_tiddata塊的泄漏,但是還是盡量避免使用。
31._endthread函數是無參的,意味者線程的結束代碼被寫入程式碼為0,而且它在調用ExitThread前,會調用CloseHandle,向其傳入新線程的控制代碼。
32.Windows提供了一些函數來方便線程引用它的進程核心對象或者它自己的線程核心對象:GetCurrentProcess() GetCurrentThread()。這兩個函數都返回到主調線程的進程核心對象或線程核心對象的一個偽控制代碼。調用這兩個函數,不會影響進程核心對象或線程核心對象的使用計數。調用CloseHandle會忽略此調用,並返回FALSE。注意,偽控制代碼是一個指向當前線程的控制代碼,即發出函數調用的那個線程。可以用DuplicateHandle轉換成真正的控制代碼。
33.
Windows Internals 筆記——線程