720 likes | 899 Views
同步問題. sigang@mti.xidian.edu.cn. 引入. Vc 中 Use run-time library 選項 ︰ Single-Threaded Multi-threaded Multi-threaded DLL 分別靜態連接 LIBC.LIB 靜態連接 LIBCMT.LIB 動態連接 MSVCRT.DLL debug 和 release 版本連接到不同的庫. C Runtime Library. C Runtime Library︰ Single-threaded Multi-threaded
E N D
同步問題 sigang@mti.xidian.edu.cn
引入 • Vc中Use run-time library 選項︰ • Single-Threaded • Multi-threaded • Multi-threaded DLL • 分別靜態連接 LIBC.LIB • 靜態連接 LIBCMT.LIB • 動態連接MSVCRT.DLL • debug 和release版本連接到不同的庫
C Runtime Library • C Runtime Library︰ • Single-threaded • Multi-threaded • Single-threaded 生于1970年代。當時的硬體情況決定多任務是不可能的,多線程更是痴人說夢。 • 所以Single-threaded 使用數個全局變量和靜態變量,這不會引起什麼問題,因為只有一個線程使用它。
時間的流逝 • 歷史的車輪不會停,硬體在突飛猛進的發展,軟體也發展。 • 出現了多任務,多線程 • 使用Single-threaded庫時出現了很多問題 • 比如︰
Multi-threaded • 利用我們后面要講述的同步機製, Multi-threaded 版本的C Runtime Library出世了 • 差別︰對于一些變量,改成每個線程一個,對于一些數據架構的訪問加上了同步機製。 • Single-threaded Runtime Library 消失么? • 不會。因為同步機製的引入而引起的大小和效率問題使Multi-threaded版本僅在需要時使用
一個原始的同步問題 驅動程式中的代碼︰ static LONG lActiveRequests; NTSTATUS DispatchRead(PDEVICE_OBJECT fdo, PIRP Irp) { ++lActiveRequests; ... // process PNP request --lActiveRequests; } 兩個問題?
; ++lActiveRequests; mov eax, lActiveRequests add eax, 1 mov lActiveRequests, eax ; ++lActiveRequests; mov eax, lActiveRequests add eax, 1 mov lActiveRequests, eax 第一個問題 X86處理器生成如下的彙編代碼
解決的辦法 • 把load/add/store和load/subtract/store指令序列替換為原子指令︰ • ; ++lActiveRequests; • inc lActiveRequests • ...; • --lActiveRequests; • dec lActiveRequests • INC和DEC指令不能被中斷,但是多處理器環境中仍然是不安全的,因為這兩個指令都是由幾條微代碼(CISC的特點)實現的
最終的解決 • ; ++lActiveRequests; • lock inc lActiveRequests • ...; • --lActiveRequests; • lock dec lActiveRequests • LOCK指令前綴可以使當前執行多微碼指令的CPU鎖定匯流排,從而保證數據訪問的完整性。
第二個問題 • 一個驅動程式支持多個設備怎么辦? • 不能使用靜態變量,而要使用一個保存設備特有數據的設備擴展的一個成員。 • 這個簡單的同步問題解決了。 • 但是並不是所有的同步問題都可以這樣容易地解決
使用IRQL優先級方案來避免搶先 • 複習︰
搶先(preempt) • 什麼是搶先? • 執行在PASSIVE_LEVEL的IRQL上活動能被任何活動搶先 • 一旦CPU執行在高于PASSIVE_LEVEL的IRQL上時,該CPU上的活動僅能被擁有更高IRQL的活動搶先。
線程調度呢? • 線程調度呢?(dispatch)? • 當IRQL級高于或等于DISPATCH_LEVEL級時線程切換停止,無論當前活動的是什麼線程都將保持活動狀態直到IRQL降到DISPATCH_LEVEL級之下。 • 執行在高于或等于DISPATCH_LEVEL級上的代碼絕對不能造成頁故障。 • 系統在APC級處理頁故障
IRQL的隱含控制 • 驅動程式中一些固定的框架(在分發歷程中或者dpc調用startpacket,startnextpacket排隊請求,操作隊列,所有IRP的開始都是在StartIo例程裡面完成) • 不理解?因為系統有一個對IRQ的隱含控制 • StartIo和Dpc都營運在DISPATCH_LEVEL級,他們不會互相衝突
IRQL的明確控制 • 可以臨時提升IRQL,然後再降回到原來的IRQL • KeRaiseIrql和KeLowerIrql函數 • KIRQL oldirql; • ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); • KeRaiseIrql(DISPATCH_LEVEL, &oldirql); • ... • KeLowerIrql(oldirql);
自旋鎖 • 前面IRQL的一個問題? • 不能解決多cpu的問題。 • 自旋鎖(spin lock) 對象可以解決這個問題。 • Spin lock的所有者是cpu,不是特定的線程
什麼是自旋鎖 • 為了獲得一個自旋鎖,在某CPU上營運的代碼需先執行一個原子操作,該操作測試並設置(test-and-set)某個內存變量,由於它是原子操作,所以在該操作完成之前其它CPU不可能訪問這個內存變量。如果測試結果表明鎖已經空閒,則程式獲得這個自旋鎖並繼續執行。如果測試結果表明鎖仍被佔用,程式將在一個小的循環內重複這個“測試並設置(test-and-set)”操作,即開始“自旋”。最後,鎖的所有者透過重置該變量釋放這個自旋鎖,于是,某個等待的test-and-set操作向其調用者報告鎖已釋放。
有關自旋鎖 • 如果一個已經擁有某個自旋鎖的CPU想第二次獲得這個自旋鎖,則該CPU將死鎖(deadlock) ,自旋鎖沒有與其關聯的“使用計數器”或“所有者標識”;鎖或者被佔用或者空閒 。 • CPU在等待自旋鎖時不做任何有用的工作,僅僅是等待。所以,為了避免影響性能,應該在擁有自旋鎖時做盡量少的操作,因為此時某個CPU可能正在等待這個自旋鎖。 • 僅能在低于或等于DISPATCH_LEVEL級上請求自旋鎖,在擁有自旋鎖期間,內核將把代碼提升到DISPATCH_LEVEL級上營運。
使用自旋鎖 • 要在非分頁內存中為一個KSPIN_LOCK對象分發存儲 ,通常在設備擴展中 • 調用KeInitializeSpinLock初始化這個對對象 • 當代碼營運在低于或等于DISPATCH_LEVEL級上時獲取這個鎖,並執行需要保護的代碼,最後釋放自旋鎖 • 看一個例子︰
在DISPATCH_LEVEL獲取自選鎖 • 如果知道代碼已經處在DISPATCH_LEVEL級上 ,如DPC、StartIo,和其它執行在DISPATCH_LEVEL級上的驅動程式例程 • 可以調用兩個專用函數來操作自旋鎖 ︰ • KeAcquireSpinLockAtDpcLevel(&pdx->QLock); • ... • KeReleaseSpinLockFromDpcLevel(&pdx->QLock);
為什麼不一樣 Why? KeAcquireSpinLock可能是這樣實現的︰ //Disable thread preemption oldirql = KeRaiseIrql(DISPATCH_LEVEL) ; KeAcquireSpinLockAtDpcLevel() 所以在Dispatch level上沒有必要使用KeAcquireSpinLock
內核模式同步對象 • Windows NT提供了五種內核同步對象(Kernel Dispatcher Object)。事件(event),Semaphore(信號燈) ,Mutex(互斥) ,Timer(定時器) ,Thread(線程) • 可以用它們控制非任意線程(普通線程)的流程 ,因為這些對象和spinlock不一樣,它們不是cpu特定的,而是線程特定的。 • 下面是這五個對象的簡要比較︰
內核模式同步對象 • 在任何時刻,任何上面的對象都處于兩種狀態中的一種︰信號態或非信號態。 • 當代碼營運在某個線程的上下文中時,它可以阻塞這個線程的執行,調用KeWaitForSingleObject或KeWaitForMultipleObjects函數可以使代碼(以及背景線程)在一個或多個同步對象上等待,等待它們進入信號態
“當前”線程 • 如果在線程執行時發生了軟體或硬體中斷,那么在內核處理中斷期間,該線程仍然是“當前”線程 • 內核處理中斷開始執行時所在的上下文環境就是指這個“當前”線程的上下文 • 為了附應各種中斷,Windows NT線程調度可能會切換線程,這樣,另一個線程將成為新的“當前”線程 • 所以中斷程式中的這個上下文就是任意進程上下文(因為不確定是哪個線程的上下文)
線程阻塞的簡單規則︰ • 當我們處理某個請求時,僅能阻塞產生該請求的線程。 • 執行在高于或等于DISPATCH_LEVEL級上的代碼不能阻塞線程。
在單同步對象上等待 • KeWaitForSingleObject • ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); LARGE_INTEGER timeout; NTSTATUS status = KeWaitForSingleObject(object, WaitReason, WaitMode, Alertable, &timeout);
參數意義 • object指向要等待的對象,它應該指向一個上面表中列出的同步對象。該對象必須在非分頁內存中,例如,在設備擴展中或其它從非分頁內存池中分發的數據區 • WaitReason是一個純粹建議性的值,驅動程式應把該參數指定為Executive • WaitMode是MODE枚舉類型,該枚舉類型僅有兩個值︰KernelMode和UserMode。 • 驅動程式中Alertable參數指定為FALSE
timeout • 最後一個參數&timeout是一個64位超時值的位址,單位為100納秒。正數的超時表示一個從1601年1月1日起的絕對時間。負數代表相對于當前時間的時間間隔。 • 指定0超時將使KeWaitForSingleObject函數立即返回,返回的狀態代碼指出對象是否處于信號態。如果代碼執行在DISPATCH_LEVEL級上,則必須指定0超時,因為在這個IRQL上不允許阻塞。 • 超時參數也可以指定為NULL指針,這代表無限期等待。
返回值 • STATUS_SUCCESS,結果是所希望的,表示等待被滿足。即你調用KeWaitForSingleObject時,對象或者已經進入信號態,或者后來進入信號態。 • STATUS_TIMEOUT指出在指定的超時期限內對象未進入信號態 。如果指定0超時,則函數將立即返回。返回代碼為STATUS_TIMEOUT,代表對象處于非信號態,返回代碼為STATUS_SUCCESS,代表對象處于信號態。 • 其它兩個返回值STATUS_ALERTED和STATUS_USER_APC表示等待提前終止,對象未進入信號態 ,上面的Alertable和WaitMode參數決定在驅動程式中不會有這兩個值。
在多同步對象上等待 • ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); • LARGE_INTEGER timeout; • NTSTATUS status = KeWaitForMultipleObjects(count, objects, WaitType, WaitReason, WaitMode, Alertable, &timeout, waitblocks);
參數 • objects指向一個指針數組,每個數組元素指向一個同步對象,count是數組中指針的個數 • WaitType是枚舉類型,其值可以為WaitAll或WaitAny,它指出你是等到所有對象都進入信號態,還是只要有一個對象進入信號態就可以。 • waitblocks參數指向一個KWAIT_BLOCK架構數組,內核用這個架構數組管理等待操作。不需要初始化這些架構,內核僅需要知道這個架構數組在那裡,內核用它來記錄每個對象在等待中的狀態。 • 其餘參數與KeWaitForSingleObject中的對應參數作用相同
返回值 • 如果指定了WaitAll,則返回值STATUS_SUCCESS表示等待的所有對象都進入了信號態。 • 如果指定了WaitAny,則返回值在數值上等于進入信號態的對象在objects數組中的索引。 • 如果碰巧有多個對象進入了信號態,則返回值僅代表其中的一個,可能是第一個也可能是其它。可以認為返回值等于STATUS_WAIT_0加上數組索引。 • 可以先用NT_SUCCESS測試返回碼,然後再從其中提取數組索引︰
NTSTATUS status = KeWaitForMultipleObjects(...); if (NT_SUCCESS(status)) { ULONG iSignalled = (ULONG) status - (ULONG) STATUS_WAIT_0; ... }
內核事件(event) • 使用︰內核中經常使用,把一個特定的事件通知給一個等待中的線程。 • 下面列出了用于處理內核事件的服務函數。 • KeClearEvent 把事件設置為非信號態,不報告以前的狀態 • KeInitializeEvent 初始化事件對象KeReadStateEvent 取事件的當前狀態 • KeResetEvent 把事件設置為非信號態,返回以前的狀態 • KeSetEvent把事件設置為信號態,返回以前的狀態
使用 • 初始化一個事件對象︰KeInitializeEvent • ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); • KeInitializeEvent(event, EventType, initialstate); • event是要初始化事件對象的位址。EventType是一個枚舉值,可以為NotificationEvent或SynchronizationEvent。 • initialstate是布爾量,為TRUE表示事件的初始狀態為信號態,為FALSE表示事件的初始狀態為非信號態。
NotificationEvent和SynchronizationEvent • 通知事件(notification event)有這樣的特性,當它進入信號態后,它將一直處于信號態直到明確地把它重置為非信號態。此外,當通知事件進入信號態后,所有在該事件上等待的線程都被釋放。 • 同步事件(synchronization event),只要有一個線程被釋放,該事件就被自動重置為非信號態。
KeSetEvent • 調用KeSetEvent函數可以把事件置為信號態︰ • ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); • LONG wassignalled = KeSetEvent(event, boost, wait); • event參數指向一個事件對象,boost值用于提升等待線程的優先級。wait參數指定為FALSE。如果該事件已經處于信號態,則該函數返回非0值。如果該事件處于非信號態,則該函數返回0。
其他函數 • 調用KeReadStateEvent函數(在任何IRQL上)可以測試事件的當前狀態︰ • LONG signalled = KeReadStateEvent(event); • 返回值不為0代表事件處于信號態,為0代表事件處于非信號態。 • 調用KeResetEvent函數(在低于或等于DISPATCH_LEVEL級)可以把事件對象重置為非信號狀態並即獲得事件對象的當前狀態 • ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); • LONG signalled = KeResetEvent(event);
其他函數 • 如果對事件的上一個狀態不感興趣,可以調用KeClearEvent函數 • ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); • KeClearEvent(event); • KeClearEvent函數執行得更快,因為它不讀取事件的當前狀態而直接設置事件為非信號態。
內核信號燈 • 生產者消費者問題 • 內核信號燈是一個有同步語義的整數計數器 • 信號燈計數器為正值時代表信號態,為0時代表非信號態。計數器不能為負值。 • 釋放信號燈將使信號燈計數器增1,在一個信號燈上等待將使該信號燈計數器減1。如果計數器值被減為0,則信號燈進入非信號態,之后其它調用KeWaitXxx函數的線程將被阻塞。 • 注意如果等待線程的個數超過了計數器的值,那么並不是所有等待的線程都可以恢復營運。
服務函數 • KeInitializeSemaphore 初始化信號燈對象 • KeReadStateSemaphore 取信號燈當前狀態 • KeReleaseSemaphore 設置信號燈對象為信號態
KeInitializeSemaphore • 信號燈對象應該在PASSIVE_LEVEL級上初始化︰ • ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); • KeInitializeSemaphore(semaphore, count, limit); • semaphore參數指向一個在非分頁內存中的KSEMAPHORE對象。count是信號燈計數器的初始值,limit是計數器能達到的最大值,它必須為正數,且比count大
KeReleaseSemaphore • 可以調用KeReleaseSemaphore函數釋放信號燈︰ • ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); • LONG wassignalled = KeReleaseSemaphore(semaphore, boost, delta, wait); • 該函數把delta值加到semaphore指向的信號燈計數器上,這將把信號燈帶入信號態,並使等待線程釋放 。返回值為0代表信號燈的前一個狀態是非信號態,非0代表信號燈的前一個狀態為信號態。
boost和wait參數與在KeSetEvent函數中的作用相同 • KeReleaseSemaphore不允許把計數器的值增加到超過limit指定的值。如果這樣做,該函數根本就不調整計數器的值,它將產生一個代碼為STATUS_SEMAPHORE_LIMIT_EXCEEDED的異常。除非系統中存在捕獲該異常的處理程式,否則將導致一個bug check。
KeReadStateSemaphore • 讀取信號燈的狀態 • ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); • LONG signalled = KeReadStateSemaphore(semaphore); • 非0返回值表示信號燈處于信號態,0返回值代表信號燈為非信號態。不要把該返回值假定為計數器的當前值。
互斥對象Mutex • 互斥(mutex)就是互相排斥(mutual exclusion)的簡寫。 • 內核互斥對象為多個競爭線程串行化訪問共享資源提供了一種方法(不一定是最好的方法)。 • 如果互斥對象不被某線程所擁有,則它是信號態,反之則是非信號態。 • 內核互斥可以被遞歸獲取,即內核互斥的所有者可以調用KeWaitXxx並指定所擁有的互斥對象從而使等待立即被滿足。如果一個線程真的這樣做,它必須也要以同樣的次數釋放該互斥對象,否則該互斥對象不被認為是空閒的。
Mutex • Mutex的功能可以透過其它方法實現 • 如果需要長時間串行化訪問一個對象,應該首先考慮使用互斥(而不是倚賴提升的IRQL和自旋鎖)。 • 利用互斥對象控制資源的訪問,可以使其它線程分佈到多處理器平台上的其它CPU中營運,還允許導致頁故障的代碼仍能鎖定資源而不被其它線程訪問。
互斥對象的服務函數 • KeInitializeMutex 初始化互斥對象KeReadStateMutex 取互斥對象的當前狀態KeReleaseMutex 設置互斥對象為信號態
KeInitializeMutex • ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); • KeInitializeMutex(mutex, level); • mutex是KMUTEX對象的位址,level參數最初是用于輔助避免多互斥對象帶來的死鎖。現下,內核忽略level參數。 • 互斥對象的初始狀態為信號態,即未被任何線程擁有。KeWaitXxx調用將使調用者接管互斥對象的控制並使其進入非信號態。