欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

02 线程的等待与唤醒

程序员文章站 2022-05-04 17:53:48
...

1、等待与唤醒机制

在Windows中,一个线程可以通过等待一个或多个可等待对象,从而进入等待状态,另一些线程可以在某些时刻唤醒等待这些对象的其他线程。
02 线程的等待与唤醒
2、可等待对象

在Windbg中查看如下结构

Windbg指令 名称
dt _KPROCESS 进程
dt _KTHREAD 线程
dt _KTIMER 定时器
dt _KSEMAPHORE 信号量
dt _KEVENT 事件
dt _KMUTANT 互斥体
dt _FILE_OBJECT 文件
kd> dt _KPROCESS
ntdll!_KPROCESS
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 ProfileListHead  : _LIST_ENTRY
   +0x018 DirectoryTableBase : [2] Uint4B
   +0x020 LdtDescriptor    : _KGDTENTRY
   +0x028 Int21Descriptor  : _KIDTENTRY
   +0x030 IopmOffset       : Uint2B
   +0x032 Iopl             : UChar
   +0x033 Unused           : UChar
   +0x034 ActiveProcessors : Uint4B
   +0x038 KernelTime       : Uint4B
   +0x03c UserTime         : Uint4B
   +0x040 ReadyListHead    : _LIST_ENTRY
   +0x048 SwapListEntry    : _SINGLE_LIST_ENTRY
   +0x04c VdmTrapcHandler  : Ptr32 Void
   +0x050 ThreadListHead   : _LIST_ENTRY
   +0x058 ProcessLock      : Uint4B
   +0x05c Affinity         : Uint4B
   +0x060 StackCount       : Uint2B
   +0x062 BasePriority     : Char
   +0x063 ThreadQuantum    : Char
   +0x064 AutoAlignment    : UChar
   +0x065 State            : UChar
   +0x066 ThreadSeed       : UChar
   +0x067 DisableBoost     : UChar
   +0x068 PowerState       : UChar
   +0x069 DisableQuantum   : UChar
   +0x06a IdealNode        : UChar
   +0x06b Flags            : _KEXECUTE_OPTIONS
   +0x06b ExecuteOptions   : UChar

kd> dt _KMUTANT
nt!_KMUTANT
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 MutantListEntry  : _LIST_ENTRY
   +0x018 OwnerThread      : Ptr32 _KTHREAD
   +0x01c Abandoned        : UChar
   +0x01d ApcDisable       : UChar

查看这些结构体,发现除了_FILE_OBJECT第一个成员不是_DISPATCHER_HEADER外,其他的第一个成员都是_DISPATCHER_HEADER

kd> dt _FILE_OBJECT
ntdll!_FILE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT
   +0x008 Vpb              : Ptr32 _VPB
   +0x00c FsContext        : Ptr32 Void
   +0x010 FsContext2       : Ptr32 Void
   +0x014 SectionObjectPointer : Ptr32 _SECTION_OBJECT_POINTERS
   +0x018 PrivateCacheMap  : Ptr32 Void
   +0x01c FinalStatus      : Int4B
   +0x020 RelatedFileObject : Ptr32 _FILE_OBJECT
   +0x024 LockOperation    : UChar
   +0x025 DeletePending    : UChar
   +0x026 ReadAccess       : UChar
   +0x027 WriteAccess      : UChar
   +0x028 DeleteAccess     : UChar
   +0x029 SharedRead       : UChar
   +0x02a SharedWrite      : UChar
   +0x02b SharedDelete     : UChar
   +0x02c Flags            : Uint4B
   +0x030 FileName         : _UNICODE_STRING
   +0x038 CurrentByteOffset : _LARGE_INTEGER
   +0x040 Waiters          : Uint4B
   +0x044 Busy             : Uint4B
   +0x048 LastLock         : Ptr32 Void
   +0x04c Lock             : _KEVENT
   +0x05c Event            : _KEVENT
   +0x06c CompletionContext : Ptr32 _IO_COMPLETION_CONTEXT

但是其在5C位置由一个_KEVENT

kd> dt _KEVENT
ntdll!_KEVENT
   +0x000 Header           : _DISPATCHER_HEADER

正常的可等待对象都是以_DISPATCHER_HEADER开头,但是Windows希望某些对象也可以是可等待对象,所以在其结构体内部嵌入了_DISPATCHER_HEADER结构体,所以只要结构体中包含_DISPATCHER_HEADER我们就可以将其看成是可等待对象。

3、可等待对象的差异

WaitForSingleObject(3环)–>NtWaitForSingleObject(内核)–>KeWaitForSigleObject(内核)

NtWaitForSingleObject执行流程

<1>通过3环用户提供的句柄,找到等待对象的内核地址

<2>如果是_DISPATCHER_HEADER开头的直接使用

<3>如果不是_DISPATCHER_HEADER开头的则找到其嵌入位置

4、一个线程等待一个对象

当一个线程只等待一个对象时线程跟对象建立联系的方式
02 线程的等待与唤醒

实验代码:

#include <stdio.h>
#include <windows.h>

HANDLE hEvent[2];

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	WaitForSingleObject(hEvent[0], -1);
	printf("ThreadProc函数开始执行\n");
	return 0;
}

int main()
{
	hEvent[0] = ::CreateEvent(NULL, TRUE, FALSE, NULL);
	::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL);
	getchar();
	return 0;
}

首先,我们运行程序,然后切换到Windbg中查找我们的运行的进程。

指令:!process 0 0
02 线程的等待与唤醒
然后观察一下这个进程的情况:

kd> !process 866fc990  
Failed to get VadRoot
PROCESS 866fc990  SessionId: 0  Cid: 0590    Peb: 7ffd7000  ParentCid: 00d4
    DirBase: 0ee86000  ObjectTable: e1042580  HandleCount:  34.
    Image: WaitForObject.exe
    VadRoot 00000000 Vads 0 Clone 0 Private 45. Modified 0. Locked 0.
    DeviceMap e1b180b0
    Token                             e238f708
    ElapsedTime                       00:33:59.734
    UserTime                          00:00:00.015
    KernelTime                        00:00:00.000
    QuotaPoolUsage[PagedPool]         0
    QuotaPoolUsage[NonPagedPool]      0
    Working Set Sizes (now,min,max)  (187, 50, 345) (748KB, 200KB, 1380KB)
    PeakWorkingSetSize                187
    VirtualSize                       8 Mb
    PeakVirtualSize                   8 Mb
    PageFaultCount                    183
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      54

        THREAD 868cab98  Cid 0590.05d4  Teb: 7ffdf000 Win32Thread: 00000000 WAIT: (WrLpcReply) UserMode Non-Alertable
            868cad8c  Semaphore Limit 0x1
        Waiting for reply to LPC MessageId 0000bc13:
        Current LPC port e104e3d8
        Not impersonating
        DeviceMap                 e1b180b0
        Owning Process            00000000       Image:         
        Attached Process          866fc990       Image:         WaitForObject.exe
        Wait Start TickCount      63232          Ticks: 130536 (0:00:33:59.625)
        Context Switch Count      29             IdealProcessor: 0             
        UserTime                  00:00:00.000
        KernelTime                00:00:00.000
*** WARNING: Unable to verify timestamp for ntoskrnl.exe
*** ERROR: Module load completed but symbols could not be loaded for ntoskrnl.exe
        Win32 Start Address nt_400000 (0x00401570)
        Stack Init ed997000 Current ed996b94 Base ed997000 Limit ed994000 Call 00000000
        Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 0 PagePriority 0
        Kernel stack not resident.
*** ERROR: Module load completed but symbols could not be loaded for Hookport.sys
*** ERROR: Module load completed but symbols could not be loaded for intelppm.sys
        ChildEBP RetAddr  
        ed996bac 804dd0f7 nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
        ed996bb8 804dd143 nt!KiSwapThread+0x46 (FPO: [0,0,0])
        ed996be0 8056efe6 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
        ed996c94 f759438e nt!NtRequestWaitReplyPort+0x63d (FPO: [Non-Fpo])
WARNING: Stack unwind information not available. Following frames may be wrong.
        ed996d50 804df7ec Hookport+0x338e
        ed996d50 7c92e514 nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ ed996d64)
        0012fce4 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])

        THREAD 86951580  Cid 0590.03f4  Teb: 7ffde000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
            8698aee8  NotificationEvent
        Not impersonating
        DeviceMap                 e1b180b0
        Owning Process            00000000       Image:         
        Attached Process          866fc990       Image:         WaitForObject.exe
        Wait Start TickCount      63232          Ticks: 130536 (0:00:33:59.625)
        Context Switch Count      3              IdealProcessor: 0             
        UserTime                  00:00:00.000
        KernelTime                00:00:00.000
        Win32 Start Address nt_400000 (0x00401005)
        Stack Init ed98f000 Current ed98eca0 Base ed98f000 Limit ed98c000 Call 00000000
        Priority 11 BasePriority 8 PriorityDecrement 2 IoPriority 0 PagePriority 0
        Kernel stack not resident.
        ChildEBP RetAddr  
        ed98ecb8 804dd0f7 nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
        ed98ecc4 804dd143 nt!KiSwapThread+0x46 (FPO: [0,0,0])
        ed98ecec 80567310 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
        ed98ed50 804df7ec nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
        ed98ed50 7c92e514 nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ ed98ed64)
        0062ff44 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])

同样我们可以在线程等待链表中找到该线程

接下来我们看一下线程结构体

dt _KTHREAD 86951580

kd> dt _KTHREAD 86951580
ntdll!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 MutantListHead   : _LIST_ENTRY [ 0x86951590 - 0x86951590 ]
   +0x018 InitialStack     : 0xed98f000 Void
   +0x01c StackLimit       : 0xed98c000 Void
   +0x020 Teb              : 0x7ffde000 Void
   +0x024 TlsArray         : (null) 
   +0x028 KernelStack      : 0xed98eca0 Void
   +0x02c DebugActive      : 0 ''
   +0x02d State            : 0x5 ''
   +0x02e Alerted          : [2]  ""
   +0x030 Iopl             : 0 ''
   +0x031 NpxState         : 0xa ''
   +0x032 Saturation       : 0 ''
   +0x033 Priority         : 11 ''
   +0x034 ApcState         : _KAPC_STATE
   +0x04c ContextSwitches  : 3
   +0x050 IdleSwapBlock    : 0 ''
   +0x051 VdmSafe          : 0 ''
   +0x052 Spare0           : [2]  ""
   +0x054 WaitStatus       : 0n0
   +0x058 WaitIrql         : 0 ''
   +0x059 WaitMode         : 1 ''
   +0x05a WaitNext         : 0 ''
   +0x05b WaitReason       : 0x6 ''
   +0x05c WaitBlockList    : 0x869515f0 _KWAIT_BLOCK
   +0x060 WaitListEntry    : _LIST_ENTRY [ 0x0 - 0x8055b108 ]
   +0x060 SwapListEntry    : _SINGLE_LIST_ENTRY
   +0x068 WaitTime         : 0xf700
   +0x06c BasePriority     : 8 ''
   +0x06d DecrementCount   : 0x10 ''
   +0x06e PriorityDecrement : 2 ''
   +0x06f Quantum          : 16 ''
   +0x070 WaitBlock        : [4] _KWAIT_BLOCK
   +0x0d0 LegoData         : (null) 
   +0x0d4 KernelApcDisable : 0
   +0x0d8 UserAffinity     : 1
   +0x0dc SystemAffinityActive : 0 ''
   +0x0dd PowerState       : 0 ''
   +0x0de NpxIrql          : 0 ''
   +0x0df InitialNode      : 0 ''
   +0x0e0 ServiceTable     : 0x8055b320 Void
   +0x0e4 Queue            : (null) 
   +0x0e8 ApcQueueLock     : 0
   +0x0f0 Timer            : _KTIMER
   +0x118 QueueListEntry   : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x120 SoftAffinity     : 1
   +0x124 Affinity         : 1
   +0x128 Preempted        : 0 ''
   +0x129 ProcessReadyQueue : 0 ''
   +0x12a KernelStackResident : 0 ''
   +0x12b NextProcessor    : 0 ''
   +0x12c CallbackStack    : (null) 
   +0x130 Win32Thread      : (null) 
   +0x134 TrapFrame        : 0xed98ed64 _KTRAP_FRAME
   +0x138 ApcStatePointer  : [2] 0x869515b4 _KAPC_STATE
   +0x140 PreviousMode     : 1 ''
   +0x141 EnableStackSwap  : 0x1 ''
   +0x142 LargeStack       : 0 ''
   +0x143 ResourceIndex    : 0 ''
   +0x144 KernelTime       : 0
   +0x148 UserTime         : 0
   +0x14c SavedApcState    : _KAPC_STATE
   +0x164 Alertable        : 0 ''
   +0x165 ApcStateIndex    : 0 ''
   +0x166 ApcQueueable     : 0x1 ''
   +0x167 AutoAlignment    : 0x4 ''
   +0x168 StackBase        : 0xed98f000 Void
   +0x16c SuspendApc       : _KAPC
   +0x19c SuspendSemaphore : _KSEMAPHORE
   +0x1b0 ThreadListEntry  : _LIST_ENTRY [ 0x866fc9e0 - 0x868cad48 ]
   +0x1b8 FreezeCount      : 0 ''
   +0x1b9 SuspendCount     : 0 ''
   +0x1ba IdealProcessor   : 0 ''
   +0x1bb DisableBoost     : 0 ''

当前线程就是通过5C位置的_KWAIT_BLOCK跟被等待线程建立联系的。

kd> dt _KWAIT_BLOCK 0x869515f0 
ntdll!_KWAIT_BLOCK
   +0x000 WaitListEntry    : _LIST_ENTRY [ 0x8698aef0 - 0x8698aef0 ]
   +0x008 Thread           : 0x86951580 _KTHREAD //指明等待块时谁的
   +0x00c Object           : 0x8698aee8 Void //被等待对象的地址
   +0x010 NextWaitBlock    : 0x869515f0 _KWAIT_BLOCK //等待块链表,只有一个等待对象指向自己
   +0x014 WaitKey          : 0 //等待索引
   +0x016 WaitType         : 1 //等待的时候一个对象符合条件就执行时这里是1,如果需要全部等待对象都符合时才执行这里就是0

5、一个线程等待多个对象
02 线程的等待与唤醒
实验代码:

#include <stdio.h>
#include <windows.h>

HANDLE hEvent[2];

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	::WaitForMultipleObjects(2, hEvent, FALSE, -1);
	printf("ThreadProc函数开始执行\n");
	return 0;
}

int main()
{
	DWORD ThreadID;
	hEvent[0] = ::CreateEvent(NULL, TRUE, FALSE, NULL);
	hEvent[1] = ::CreateEvent(NULL, TRUE, FALSE, NULL);
	::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, (LPDWORD)&ThreadID);
	getchar();
	return 0;
}

重复上面的步骤进行观察

kd> dt _KWAIT_BLOCK 0x86ac72c8 
ntdll!_KWAIT_BLOCK
   +0x000 WaitListEntry    : _LIST_ENTRY [ 0x869e0610 - 0x869e0610 ]
   +0x008 Thread           : 0x86ac7258 _KTHREAD
   +0x00c Object           : 0x869e0608 Void
   +0x010 NextWaitBlock    : 0x86ac72e0 _KWAIT_BLOCK
   +0x014 WaitKey          : 0
   +0x016 WaitType         : 1
kd> dt _KWAIT_BLOCK 0x86ac72e0 
ntdll!_KWAIT_BLOCK
   +0x000 WaitListEntry    : _LIST_ENTRY [ 0x86960928 - 0x86960928 ]
   +0x008 Thread           : 0x86ac7258 _KTHREAD
   +0x00c Object           : 0x86960920 Void
   +0x010 NextWaitBlock    : 0x86ac72c8 _KWAIT_BLOCK
   +0x014 WaitKey          : 1
   +0x016 WaitType         : 1

6、等待网
02 线程的等待与唤醒
总结:

等待中的线程一定在等待链表中(KiWaitListHead),同时也一定在这张网上(KTHREAD + 5C的位置不为空);

线程通过调用WaitForSingleObject/WaitForMultipleObjects将将自己挂到这张网上;

线程什么时候再执行取决于其他何时调用相关函数,等待对象不同,调用函数不同