找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3835|回复: 8

【多线程】在Windows内核中玩多线程编程

[复制链接]
发表于 2019-6-20 17:55:55 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
记得很久以前,毛利鸣人在群里问过我在Windows内核里玩多线程的事。我当时说:内核里线程概念没那么重了。
emmmm,这话该说是对是错我还说不清楚。因为在高中断请求级下,就没有线程的概念了。所谓的线程同步在高中断请求级下应该称之为多核同步。
不过在Windows内核里,玩玩多线程还是可行的,这里通过类比POSIX线程做个简单的介绍。


系统线程的创建与终止:
在Windows内核中,通过导出函数PsCreateSystemThread来实现创建系统线程。类似于POSIX线程中pthread_create函数。函数原型如下:
  1. NTSTATUS PsCreateSystemThread(OUT PHANDLE ThreadHandle,IN ULONG DesiredAccess,IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,IN HANDLE ProcessHandle OPTIONAL,OUT PCLIENT_ID ClientId OPTIONAL,IN PKSTART_ROUTINE StartRoutine,IN PVOID StartContext);
复制代码

参数介绍如下:
ThreadHandle:                用于接收被创建的线程句柄。句柄不再需要使用之后请关闭句柄。
DesiredAccess:                句柄对线程所拥有的权限,它是一个ACCESS_MASK类型的掩码。
ObjectAttributes:        用于描述线程对象句柄的属性。不可以设置为OBJ_PERMANENT, OBJ_EXCLUSIVE, 和 OBJ_OPENIF。从Windows XP起,如果该系统线程不被归入系统进程的上下文,则必须设置OBJ_KERNEL_HANDLE属性。在Windows 2000/98/Me中,该函数必须在系统进程上下文中调用。从Windows Vista起,该函数返回的句柄必然是内核句柄。
ProcessHandle:                用于指定被创建的系统线程归入的进程的句柄。该参数可选,填入NULL表示系统进程(PID=4)。
ClientId:                用于接收被创建线程的PID和TID。驱动创建线程时,应当设置为NULL。
StartRoutine:                线程起始函数地址。
StartContext:                线程函数的参数。
返回值为NTSTATUS错误码。当线程被成功创建时,函数返回STATUS_SUCCESS。
值得注意的是,ObjectAttributes是一个结构体,当指定OBJ_KERNEL_HANDLE属性时,方法如下:
  1. OBJECT_ATTRIBUTES oa;
  2. InitializeObjectAttributes(&oa,NULL,OBJ_KERNEL_HANDLE,NULL,NULL);
复制代码

线程被创建后,会执行其线程函数。线程函数的原型为:
KSTART_ROUTINE ThreadStart;
void ThreadStart(IN PVOID StartContext)
{...}
原型和POSIX线程一样,只不过没有返回值罢了。


线程完成操作时,应当主动退出线程。在POSIX线程中,通过pthread_exit函数退出线程,在Windows内核系统线程中,通过PsTerminateSystemThread函数退出线程。函数原型如下:
  1. NTSTATUS PsTerminateSystemThread(IN NTSTATUS Status);
复制代码

通过填入一个NTSTATUS的代码表示线程任务的状态。通常填入STATUS_SUCCESS表示执行成功。


多线程编程中,需要等待一个线程执行完成。在POSIX线程中,通过pthread_join函数来等待线程完成。在Windows内核中,由于我们获取了线程句柄,因此使用ZwWaitForSingleObject来等待线程完成。函数原型如下:
  1. NTSTATUS ZwWaitForSingleObject(IN HANDLE Handle,IN BOOLEAN Alertable,IN PLARGE_INTEGER Timeout OPTIONAL);
复制代码

参数介绍如下:
Handle:        要等待的对象的句柄,这里直接填入PsCreateSystemThread返回的句柄即可。
Alertable:        该等待是否可以被叫醒。用于表示其他线程是否可以在该线程陷入等待时叫醒线程退出等待。
Timeout:        等待时长。值得注意的是,单位是100纳秒,正数表示绝对系统时间(即系统中的格林尼治时间),负数表示相对于执行该函数时的时间。举例:等待5毫秒时,设置为-50000。当设置为NULL时表示无限等待。
返回值为NTSTATUS错误码,可能情况如下:
STATUS_SUCCESS:        等待成功完成,在等待过程中没有被叫醒。
STATUS_ALERTED:        等待过程中被叫醒。
STATUS_INVALID_HANDLE:        句柄不正确。
STATUS_ACCESS_DENIED:        拒绝访问。一般是PsCreateSystemThread中没有赋予SYNCHRONIZE权限。
STATUS_TIMEOUT:        等待超时。
STATUS_USER_APC:        用户态APC被插入到当前线程。
值得注意的是,当使用NT_SUCCESS宏时,STATUS_SUCCESS,STATUS_ALERTED,STATUS_TIMEOUT,STATUS_USER_APC都被视为执行成功。
等待完成后,线程终止,此时应当使用ZwClose函数关闭句柄以避免句柄泄露。


“睡觉”是一种多线程实现定时器的功能,也可以用于实现简单的自旋锁多线程同步。在用户态里可以用Sleep函数来实现睡觉。在Windows内核中,通过KeDelayExecutionThread实现睡觉。函数原型如下:
  1. NTSTATUS KeDelayExecutionThread(IN KPROCESSOR_MODE WaitMode,IN BOOLEAN Alertable,IN PLARGE_INTEGER Interval);
复制代码

参数介绍如下:
WaitMode:        用于表示等待的模式。在系统线程中,应当用KernelMode。
Alertable:        该睡觉是否可以被叫醒。
Interval:        睡觉时长,单位是100纳秒。正数表示绝对系统时间(即系统中的格林尼治时间),负数表示相对于执行该函数时的时间。
返回值如下:
STATUS_SUCCESS:        睡觉完成,没有被叫醒。
STATUS_ALERTED:        等待过程中被叫醒。
STATUS_USER_APC:        用户态APC被插入到当前线程。
之前提到过睡觉和等待可以被叫醒,叫醒线程使用ZwAlertThread函数实现,函数原型如下:
NTSTATUS ZwAlertThread(IN HANDLE ThreadHandle);
填入线程句柄来叫醒指定的线程。注意这是个非文档化函数,使用这个函数时应当自行声明。
如果线程等待和睡觉时指定不可被叫醒,则这个函数不能叫醒线程。


在多线程编程中,线程安全尤为重要。我们需要实现多线程之间的同步。在Windows内核中,有以下简单的线程同步机制:
互斥锁、推拉锁、资源锁、自旋锁。
互斥锁(Mutual Exclusion,Mutex)是一种基本的线程同步机制。任何受互斥锁保护的数据都会在获取时陷入阻塞。
在Windows内核中,可以选择内核互斥锁(Mutex),快速互斥锁(Fast Mutex)和受保护互斥锁(Guarded Mutex)。
他们的区别是:
内核互斥锁性能最差,但线程递归性获取普通互斥锁时不会发生死锁。
快速互斥锁比内核互斥锁快,但线程递归性获取快速互斥锁时会发生死锁。
受保护互斥锁比快速互斥锁拥有更好的性能,但线程递归性获取受保护互斥锁时会发生死锁。
推拉锁(Push Lock)是一种支持读写分离的线程同步机制。推拉锁通过访问数据行为,将获取锁的行为分为共享锁和独占锁。当读取数据时,应当获取共享锁。当写入数据时,应当获取独占锁。
资源锁(Resource)也是一种支持读写分离的线程同步机制。和推拉锁一样,获取锁的行为分为共享锁和独占锁。当读取数据时,应当获取共享锁。当写入数据时,应当获取独占锁。
推拉锁和资源锁的区别在于:线程递归性获取推拉锁会发生死锁,资源锁则不会。而且,推拉锁未被微软官方文档化,是一种Windows内核内部使用的锁。
自旋锁(Spin Lock)是一种高度占用CPU核心的锁,但它是唯一允许在高中断请求级下使用的线程同步机制,因此受自旋锁保护的代码应当GKD。
这些线程同步机制都是基于获取-释放的代码模型。


关于互斥锁,这里对内核互斥锁略过不谈,直接谈快速互斥锁和受保护互斥锁。
快速互斥锁变量类型为FAST_MUTEX,受保护互斥锁变量类型为KGUARDED_MUTEX。在初始化一个互斥锁时,这个互斥锁变量不能是一个驱动中的全局变量,它必须以未分页内存的方式保存变量。通过ExAllocatePool函数,填入NonPagedPool参数分配一段未分页内存。为互斥锁变量分配出内存后,才能以ExInitializeFastMutex或KeInitializeGuardedMutex初始化互斥锁。
初始化互斥锁的函数原型如下:
  1. void ExInitializeFastMutex(IN PFAST_MUTEX FastMutex);
  2. void KeInitializeGuardedMutex(IN PKGUARDED_MUTEX GuardedMutex);
复制代码

当获取互斥锁时,使用ExAcquireFastMutex或KeAcquireGuardedMutex函数。如果互斥锁已被获取,则获取互斥锁将陷入阻塞直到互斥锁被释放。
当释放互斥锁时,使用ExReleaseFastMutex或KeReleaseGuardedMutex函数。互斥锁释放时,其他获取锁的线程将停止阻塞。
如果不希望获取资源锁时发生阻塞,则使用ExTryToAcquireFastMutex或KeTryToAcquireGuardedMutex。获取成功时返回TRUE,反之则FALSE。
以上六个函数原型如下:
  1. void ExAcquireFastMutex(IN PFAST_MUTEX FastMutex);
  2. BOOLEAN ExTryToAcquireFastMutex(IN PFAST_MUTEX FastMutex);
  3. void ExReleaseFastMutex(IN PFAST_MUTEX FastMutex);
  4. void KeAcquireGuardedMutex(IN PKGUARDED_MUTEX GuardedMutex);
  5. BOOLEAN KeTryToAcquireGuardedMutex(IN PKGUARDED_MUTEX GuardedMutex);
  6. void KeReleaseGuardedMutex(IN PKGUARDED_MUTEX GuardedMutex);
复制代码



推拉锁支持读写分离,但未被文档化,这里对推拉锁做简单的文档化。
推拉锁的变量类型为EX_PUSH_LOCK,在初始化一个互斥锁时,也必须是未分页内存的方式。通过ExInitializePushLock函数对其初始化,函数原型如下:
  1. void ExInitializePushLock(OUT PEX_PUSH_LOCK PushLock);
复制代码

获取推拉锁之前,应当进入临界区,使用KeEnterCriticalRegion函数进入临界区,函数原型如下:
  1. void KeEnterCriticalRegion(void);
复制代码

获取锁有共享锁独占锁两种形式,用ExfAcquirePushLockShared获取共享锁,用ExfAcquirePushLockExclusive获取独占锁。函数原型如下:
  1. void __fastcall ExfAcquirePushLockShared(IN PEX_PUSH_LOCK PushLock);
  2. void __fastcall ExfAcquirePushLockExclusive(IN PEX_PUSH_LOCK PushLock);
复制代码

注意这里使用的是fastcall调用约定!
当获取共享锁时,如果推拉锁未被获取独占锁,则锁被立即获取,否则发生阻塞。也就是说多个线程可以同时以共享锁的方式获取一个推拉锁。
当获取独占锁时,如果推拉锁未被获取任何形式的锁,则锁被立即获取,否则发生阻塞。
在释放锁时,要对应获取方式实行释放锁,用ExfReleasePushLockShared释放共享锁,用ExfReleasePushLockExclusive释放独占锁。函数原型如下:
  1. void __fastcall ExfReleasePushLockShared(IN PEX_PUSH_LOCK PushLock);
  2. void __fastcall ExfReleasePushLockExclusive(IN PEX_PUSH_LOCK PushLock);
复制代码

释放推拉锁后,应当离开临界区,使用KeLeaveCriticalRegion函数离开临界区,函数原型如下:
  1. void KeLeaveCriticalRegion(void);
复制代码

再次重申,递归获取推拉锁会引起线程死锁!


资源锁是一种同时支持读写分离且允许递归获取的文档化的线程锁。
资源锁的变量类型为ERESOURCE,在初始化一个资源锁时,也必须是未分页内存的方式。通过ExInitializeResourceLite函数对其初始化,函数原型如下:
  1. NTSTATUS ExInitializeResourceLite(IN PERESOURCE Resource);
复制代码

获取资源锁有共享锁和独占锁两种形式,其中共享锁还能细分为插队锁和排队锁两种形式。
获取独占锁时,用ExAcquireResourceExclusiveLite函数获取独占锁,函数原型如下:
  1. BOOLEAN ExAcquireResourceExclusiveLite(IN PERESOURCE Resource,IN BOOLEAN Wait);
复制代码

当Wait参数为TRUE时,函数等到所有共享锁和独占锁被释放后进行获取,并返回TRUE。
当Wait参数为FALSE时,如果不能立即获取独占锁,返回FALSE,反之则返回TRUE。
获取插队锁时,用ExAcquireSharedStarveExclusive函数获取插队锁,函数原型如下:
  1. BOOLEAN ExAcquireSharedStarveExclusive(IN PERESOURCE Resource,IN BOOLEAN Wait);
复制代码

这里对Wait参数和返回值不再赘述,简单讲讲插队锁的特征:
如果资源没有被获取独占锁,插队锁会在队列中的独占锁之前抢占获取共享锁。
获取排队锁时,用ExAcquireResourceSharedLite函数获取排队锁,函数原型如下:
  1. BOOLEAN ExAcquireResourceSharedLite(IN PERESOURCE Resource,IN BOOLEAN Wait);
复制代码

相对于插队锁,排队锁会等待资源锁队列中的独占锁——即使资源尚未被获取独占锁,排队锁也会等待队列中排在前的独占锁完成操作。这里假设Wait参数为TRUE。
当Wait参数为FALSE时,如果不能立即获取排队锁,返回FALSE。
操作完成时,应当释放锁,无论是何种锁,释放锁时统一使用ExReleaseResourceLite函数,函数原型如下:
  1. void ExReleaseResourceLite(IN PERESOURCE Resource);
复制代码

和推拉锁一样,在获取资源锁前,应当进入临界区,在释放资源锁后,应当离开临界区。
当资源锁不再使用时,应当使用ExDeleteResourceLite删除资源锁。函数原型如下:
  1. NTSTATUS ExDeleteResourceLite(IN PERESOURCE Resource);
复制代码

当资源锁被删除后,才能释放掉资源锁的内存。


自旋锁是一种可以在高中断请求级下使用的线程同步(多核同步)机制。它不支持读写分离,且大量占用CPU资源。
自旋锁的变量类型是KSPIN_LOCK,在初始化一个自旋锁时,也必须是未分页内存的方式。通过KeInitializeSpinLock函数对其初始化,函数原型如下:
  1. void KeInitializeSpinLock(IN PKSPIN_LOCK SpinLock);
复制代码

关于自旋锁的获取,在WDK7中就有五个函数实现获取,它们分别是:
标准获取自旋锁:        void KeAcquireSpinLock(IN PKSPIN_LOCK SpinLock,OUT PKIRQL OldIrql);
在DPC级获取自旋锁:        void KeAcquireSpinLockAtDpcLevel(IN PKSPIN_LOCK SpinLock);
在DPC级试图获取自旋锁:        BOOLEAN KeTryToAcquireSpinLockAtDpcLevel(IN PKSPIN_LOCK SpinLock);
线程DPC获取自旋锁:        KIRQL KeAcquireSpinLockForDpc(IN PKSPIN_LOCK SpinLock);
提升至DPC级获取自旋锁:        KIRQL KeAcquireSpinLockRaiseToDpc(IN PKSPIN_LOCK SpinLock);
有三个函数实现释放,它们分别是:
标准释放自旋锁:        void KeReleaseSpinLock(IN PKSPIN_LOCK SpinLock,IN KIRQL NewIrql);
从DPC级释放自旋锁:        void KeReleaseSpinLockFromDpcLevel(IN PKSPIN_LOCK SpinLock);
线程DPC释放自旋锁:        void KeReleaseSpinLockForDpc(IN PKSPIN_LOCK SpinLock,IN KIRQL OldIrql);
其中KeAcquireSpinLock和KeAcquireSpinLockRaiseToDpc获取的自旋锁应当用KeReleaseSpinLock释放;
KeAcquireSpinLockAtDpcLevel和KeTryToAcquireSpinLockAtDpcLevel获取的自旋锁应当用KeReleaseSpinLockFromDpcLevel释放;
KeAcquireSpinLockForDpc获取的自旋锁应当用KeReleaseSpinLockForDpc释放。
注意KeAcquireSpinLock返回的OldIrql通常放进局部变量,虽然可以放进全局变量,但绝对不能用于两个不同的锁,否则会引起竞态条件。
在自旋锁中,不要访问分页内存,不要产生异常,并且GKD,获取后尽快释放。对在获取自旋锁时运行的代码应当做到最优优化。


本文介绍了在Windows内核中玩多线程的方法,涉及了线程的创建退出等待,睡觉与叫醒,以及四种可用于线程同步的锁:互斥锁,推拉锁,资源锁,自旋锁。
因为时间关系就不写示例代码了。以后可能会补充。
回复

使用道具 举报

发表于 2019-6-22 22:43:59 | 显示全部楼层
知识有限,还没能看懂,不过我会努力的。

点评

加油。  发表于 2020-1-27 18:58
回复 赞! 1 靠! 0

使用道具 举报

发表于 2019-6-23 02:56:13 | 显示全部楼层
这个总结太牛逼了。
回复 赞! 靠!

使用道具 举报

发表于 2020-1-27 17:07:03 | 显示全部楼层
牛,我看懂了OBJECT_ATTRIBUTES结构体和InitializeObjectAttributes宏,之前python学过多线程里面的mutex lock所以还稍微看懂一些互斥锁,最让我感觉兴奋的是在内核中的实现Sleep函数来着,由于Sleep是winsdk里面用在r3层面的api, 苦恼了好长时间才有人说内核中用KeDelayExecutionThread来实现Sleep功能,要是当初早些时间看到这个帖子就好了
回复 赞! 靠!

使用道具 举报

发表于 2020-1-27 18:55:09 | 显示全部楼层
小弟我简单根据本帖子上的api指导和win32sdk编程中的多线程程序写了一个win7 x64的互斥锁的多线程内核程序的例子,为了便于理解,小弟写了一个python的程序作为对应。
例子采用互斥锁的应用中的给共享资源(这里以一个全局变量为例)进行计数来说明互斥锁的应用,其具体原理就是本帖子中说明的堵塞和解堵塞的原理。
python程序(python 3.7 amd64)
  1. import threading
  2. import time

  3. # global parameter.
  4. g_num = 0


  5. def thread1(num):
  6.     global g_num

  7.     mutex.acquire()
  8.    
  9.     for i in range(num):
  10.         g_num += 1

  11.     mutex.release()

  12.     print("thread 1: g_num=%d" % g_num)
  13.    


  14. def thread2(num):
  15.     global g_num

  16.     mutex.acquire()
  17.    
  18.     for i in range(num):
  19.         g_num += 1

  20.     mutex.release()

  21.     print("thread 2: g_num=%d" % g_num)


  22. # Create a mutex lock, and the defaulted is unlocked.
  23. mutex = threading.Lock()


  24. def main():
  25.     th1 = threading.Thread(target=thread1, args=(10000000,))
  26.     th2 = threading.Thread(target=thread2, args=(10000000,))

  27.     th1.start()
  28.    
  29.     th2.start()

  30.     time.sleep(3)
  31.    
  32. if __name__ == '__main__':
  33.     main()
  34.    
复制代码

运行结果:
2020-01-27_185435.png

C语言 win7 x64 内核程序:
  1. #include <wdm.h>
  2. #include <windef.h>

  3. // Global parameter.
  4. unsigned long g_number = 0;

  5. void thread1(void *mutex)
  6. {
  7.         unsigned long i;
  8.         PKMUTEX pkMutex = (PKMUTEX)mutex;

  9.         KeWaitForSingleObject(pkMutex,
  10.                 Executive,
  11.                 KernelMode,
  12.                 FALSE,
  13.                 NULL);
  14.         DbgPrint("This is Thread 1...\n");

  15.         for (i = 0; i < 10000000; i++)
  16.         {
  17.                 g_number++;
  18.         }

  19.         KeReleaseMutex(pkMutex, FALSE);

  20.         // Print the result of g_number in thread 1.
  21.         DbgPrint("Thread 1:g_number = %ld\n", g_number);

  22.         PsTerminateSystemThread(STATUS_SUCCESS);
  23. }

  24. void thread2(void *mutex)
  25. {
  26.         unsigned long i;
  27.         PKMUTEX pkMutex = (PKMUTEX)mutex;

  28.         KeWaitForSingleObject(pkMutex,
  29.                 Executive,
  30.                 KernelMode,
  31.                 FALSE,
  32.                 NULL);
  33.         DbgPrint("This is Thread 2...\n");

  34.         for (i = 0; i < 10000000; i++)
  35.         {
  36.                 g_number++;
  37.         }

  38.         KeReleaseMutex(pkMutex, FALSE);

  39.         // Print the result of g_number in thread 2.
  40.         DbgPrint("Thread 2:g_number = %ld\n", g_number);

  41.         PsTerminateSystemThread(STATUS_SUCCESS);
  42. }


  43. void DriverUnload(IN PDRIVER_OBJECT pDrvObj)
  44. {
  45.         DbgPrint("DriverUnload....\n");
  46. }

  47. NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObj, PUNICODE_STRING RegistryPath)
  48. {
  49.         NTSTATUS status = STATUS_SUCCESS;
  50.         pDrvObj->DriverUnload = DriverUnload;

  51.         // TODO: mutex-lock thread test.

  52.         HANDLE hThread1, hThread2;
  53.         KMUTEX mutex;

  54.         // Create the mutex lock.
  55.         KeInitializeMutex(&mutex, 0);

  56.         // Create the thread.
  57.         PsCreateSystemThread(&hThread1,
  58.                 0,
  59.                 NULL,
  60.                 NtCurrentProcess(),
  61.                 NULL,
  62.                 thread1, // thread proc
  63.                 &mutex);

  64.         PsCreateSystemThread(&hThread2,
  65.                 0,
  66.                 NULL,
  67.                 NtCurrentProcess(),
  68.                 NULL,
  69.                 thread2, // thread2 proc
  70.                 &mutex);

  71.         void *Queue[2];
  72.         ObReferenceObjectByHandle(hThread1,
  73.                 0,
  74.                 NULL,
  75.                 KernelMode,
  76.                 &Queue[0],
  77.                 NULL);
  78.         ObReferenceObjectByHandle(hThread2,
  79.                 0,
  80.                 NULL,
  81.                 KernelMode,
  82.                 &Queue[1],
  83.                 NULL);

  84.        
  85.         KeWaitForMultipleObjects(2,
  86.                 Queue,
  87.                 WaitAll,
  88.                 Executive,
  89.                 KernelMode,
  90.                 FALSE,
  91.                 NULL,
  92.                 NULL);

  93.         // Subtract the count the object reference and
  94.         // release the system resources.
  95.         ObDereferenceObject(Queue[0]);
  96.         ObDereferenceObject(Queue[1]);

  97.         // Sleep 2 seconds, wait for the threads completely
  98.         // finish their work.
  99.         LARGE_INTEGER sleep;
  100.         sleep.QuadPart = -20 * 1000 * 1000;
  101.         KeDelayExecutionThread(KernelMode,
  102.                 FALSE,
  103.                 &sleep);

  104.         DbgPrint("The process is terminated...\n");
  105.         return status;
  106. }
复制代码

运行结果:
result.PNG
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2020-1-27 21:55:51 | 显示全部楼层
watermelon 发表于 2020-1-27 18:55
小弟我简单根据本帖子上的api指导和win32sdk编程中的多线程程序写了一个win7 x64的互斥锁的多线程内核程序 ...

随便说几句吧。
1. 在Windows内核里锁对象必须放在不可换出内存里。也就是说必须要用ExAllocatePool(WithTag)分配一段NonPagedPool给锁对象。每个初始化锁函数的文档都会提到这句话。
2. 我在帖子里没说怎么用内核互斥锁。。。
3. 创建线程没必要引用他们的线程对象,直接ZwWaitForMultipleObjects即可,这是个导出函数。
4. 你虽然记得要解引用以免对象泄漏,但你忘记关闭线程句柄了。
5. 你发这个帖子的时候python3.8.x都出了。。。
回复 赞! 靠!

使用道具 举报

发表于 2020-1-27 23:41:02 | 显示全部楼层
tangptr@126.com 发表于 2020-1-27 21:55
随便说几句吧。
1. 在Windows内核里锁对象必须放在不可换出内存里。也就是说必须要用ExAllocatePool(With ...

学习了!不过好像的确是忘记关闭hThread1和hThread2了(汗
回复 赞! 靠!

使用道具 举报

发表于 2020-1-27 23:44:12 | 显示全部楼层
tangptr@126.com 发表于 2020-1-27 21:55
随便说几句吧。
1. 在Windows内核里锁对象必须放在不可换出内存里。也就是说必须要用ExAllocatePool(With ...

是的,最新的python正式发布版本是3.8,并且刚看了一下,python3.9已经出测试版了
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-12-27 13:06 , Processed in 0.041589 second(s), 29 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表