技术宅的结界

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

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 556|回复: 2
收起左侧

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

[复制链接]

32

主题

91

帖子

3316

积分

用户组: 管理员

UID
1043
精华
14
威望
104 点
宅币
2686 个
贡献
261 次
宅之契约
0 份
在线时间
538 小时
注册时间
2015-8-15
发表于 2019-6-20 17:55:55 | 显示全部楼层 |阅读模式

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

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

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


系统线程的创建与终止:
在Windows内核中,通过导出函数PsCreateSystemThread来实现创建系统线程。类似于POSIX线程中pthread_create函数。函数原型如下:
[C] 纯文本查看 复制代码
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属性时,方法如下:
[C] 纯文本查看 复制代码
OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa,NULL,OBJ_KERNEL_HANDLE,NULL,NULL);

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


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

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


多线程编程中,需要等待一个线程执行完成。在POSIX线程中,通过pthread_join函数来等待线程完成。在Windows内核中,由于我们获取了线程句柄,因此使用ZwWaitForSingleObject来等待线程完成。函数原型如下:
[C] 纯文本查看 复制代码
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实现睡觉。函数原型如下:
[C] 纯文本查看 复制代码
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初始化互斥锁。
初始化互斥锁的函数原型如下:
[C] 纯文本查看 复制代码
void ExInitializeFastMutex(IN PFAST_MUTEX FastMutex);
void KeInitializeGuardedMutex(IN PKGUARDED_MUTEX GuardedMutex);

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



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

获取推拉锁之前,应当进入临界区,使用KeEnterCriticalRegion函数进入临界区,函数原型如下:
[C] 纯文本查看 复制代码
void KeEnterCriticalRegion(void);

获取锁有共享锁独占锁两种形式,用ExfAcquirePushLockShared获取共享锁,用ExfAcquirePushLockExclusive获取独占锁。函数原型如下:
[C] 纯文本查看 复制代码
void __fastcall ExfAcquirePushLockShared(IN PEX_PUSH_LOCK PushLock);
void __fastcall ExfAcquirePushLockExclusive(IN PEX_PUSH_LOCK PushLock);

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

释放推拉锁后,应当离开临界区,使用KeLeaveCriticalRegion函数离开临界区,函数原型如下:
[C] 纯文本查看 复制代码
void KeLeaveCriticalRegion(void);

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


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

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

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

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

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

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

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


自旋锁是一种可以在高中断请求级下使用的线程同步(多核同步)机制。它不支持读写分离,且大量占用CPU资源。
自旋锁的变量类型是KSPIN_LOCK,在初始化一个自旋锁时,也必须是未分页内存的方式。通过KeInitializeSpinLock函数对其初始化,函数原型如下:
[C] 纯文本查看 复制代码
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内核中玩多线程的方法,涉及了线程的创建退出等待,睡觉与叫醒,以及四种可用于线程同步的锁:互斥锁,推拉锁,资源锁,自旋锁。
因为时间关系就不写示例代码了。以后可能会补充。

评分

参与人数 1威望 +20 宅币 +100 贡献 +50 收起 理由
美俪女神 + 20 + 100 + 50 赞!

查看全部评分

flowers for Broken spirits - a woman turned into stake will hold the world in the basin of fire.

1

主题

46

帖子

171

积分

用户组: 小·技术宅

UID
4683
精华
0
威望
0 点
宅币
125 个
贡献
0 次
宅之契约
0 份
在线时间
20 小时
注册时间
2019-2-11
发表于 2019-6-22 22:43:59 | 显示全部楼层
知识有限,还没能看懂,不过我会努力的。

36

主题

146

帖子

7193

积分

用户组: 管理员

UID
77
精华
11
威望
115 点
宅币
6630 个
贡献
132 次
宅之契约
0 份
在线时间
108 小时
注册时间
2014-2-22
发表于 2019-6-23 02:56:13 | 显示全部楼层
这个总结太牛逼了。

本版积分规则

QQ|申请友链||Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号|网站地图

GMT+8, 2019-11-18 02:02 , Processed in 0.101460 second(s), 32 queries , Gzip On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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