多线程安全

总结就是

DISPATCH_LEVEL:SpinLock

APC/PASSIVE:
互斥:ERESOURCE/FAST_MUTEX
同步:KEVENT/KSEMAPHORE

R3/R0同步通讯:

KEVENT

常见函数的线程

调用源 运行环境 原因
DriverEntry,DriverUnload 单线程 这两个函数由系统进程的单一线程调用,不会出现多线程同时调用的情况
各种分发函数 多线程 分发函数不会和DriverEntry并发,但是可能和DriverUnload并发
完成函数 多线程 完成例程函数随时被未知的线程调用
各种NDIS回调函数 多线程 和完成函数相同

PsCreateSystemThread

PsCreateSystemThread是 Windows 操作系统内核中的一个应用程序编程接口(API)。它主要用于在系统进程(System Process)的环境中创建一个新的线程。

1
2
3
4
5
6
7
8
9
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:用于指定对新创建线程的访问权限。例如,可以指定THREAD_ALL_ACCESS来获取对线程的完全访问权限,包括读取和修改线程的上下文、终止线程等操作。
  • ObjectAttributes:这是一个可选参数,用于指定新线程对象的属性。它是一个指向OBJECT_ATTRIBUTES结构的指针。如果设置为NULL,系统将使用默认的属性来创建线程对象。
  • ProcessHandle:也是一个可选参数。它用于指定新线程所属的进程句柄。如果设置为NULL,新线程将在系统进程(System Process)中创建。
  • ClientId:这是一个输出参数,用于接收新创建线程的客户端 ID。客户端 ID 是一个在系统范围内唯一标识线程的标识符,它包含了进程 ID 和线程 ID。
  • StartRoutine:这是一个非常关键的参数,它是一个指向线程起始例程的指针。这个例程将在新创建的线程中执行,它定义了线程的主要功能。例如,可以在这里编写代码来执行一些后台任务,如数据收集、设备监控等。
  • StartContext:这个参数用于向线程起始例程传递一个上下文参数。起始例程可以根据这个参数来获取一些初始化信息,从而正确地执行任务。

互斥体和事件的区别

此前一直对互斥体和事件有什么分别都是很懵逼的,今天查阅了相关资料,怕忘记赶紧记录一下

首先事事件能做到的事,互斥体做不到的事

场景1:

起两个线程,要轮流执行,此时就要使用事件

之前我天真的以为,用互斥体也可以实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <windows.h>
#include <iostream>
HANDLE hMutex;
int g_Max = 10;
int g_Number = 0;
//生产者线程函数
DWORD WINAPI ThreadProduct(LPVOID pM)
{
for (int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
WaitForSingleObject(hMutex, INFINITE);
g_Number = 1;
DWORD id = GetCurrentThreadId();
printf("生产者%d将数据%d放入缓冲区\n", id, g_Number);
ReleaseMutex(hMutex);
}
return 0;
}
//消费者线程函数
DWORD WINAPI ThreadConsumer(LPVOID pM)
{
for (int i = 0; i < g_Max; i++)
{
//互斥的访问缓冲区
WaitForSingleObject(hMutex, INFINITE);
g_Number = 0;
DWORD id = GetCurrentThreadId();
printf("----消费者%d将数据%d放入缓冲区\n", id, g_Number);
ReleaseMutex(hMutex);
}
return 0;
}

int main(int argc, char* argv[])
{
//创建一个互斥体
hMutex = CreateMutex(NULL, FALSE, NULL);

HANDLE hThread[2];

hThread[0] = ::CreateThread(NULL, 0, ThreadProduct, NULL, 0, NULL);
hThread[1] = ::CreateThread(NULL, 0, ThreadConsumer, NULL, 0, NULL);

WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);

//销毁
CloseHandle(hMutex);

return 0;
}

虽然大部分情况是顺序的,但是多运行几次还是出现了这样的画面

1730898412831

出现这个的原因还得是因为线程释放了互斥体,但是由于时间片又在这个线程手里,马上又去获取了互斥体,所以造成以上的错误

但是事件却不会出现这个情况,应该是操作系统调度机制的问题,这里就不去深究了,反正确实会出现这个结果,所以遇到顺序执行的,用事件就对了

场景2:

假设有多个线程,它们在某个时刻需要同时开始工作,但是工作可以在同一个条件下开始。比如,当某个资源加载完毕后,所有的工作线程都可以同时开始工作。

此时,用事件能做到,But互斥体做不到,下面是实现的代码

注意CreateEvent(NULL, TRUE, FALSE, NULL); ,第二个参数 如果此参数为 TRUE,则函数将创建手动重置事件对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <windows.h>
#include <iostream>
#include <thread>

HANDLE eventHandle; // 用于线程同步的事件

void worker(int id) {
std::cout << "Thread " << id << " is waiting for the signal.\n";
WaitForSingleObject(eventHandle, INFINITE); // 等待事件信号
std::cout << "Thread " << id << " is now working.\n";
}

int main() {
eventHandle = CreateEvent(NULL, TRUE, FALSE, NULL); // 手动重置事件

std::thread threads[5];

// 启动五个线程
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(worker, i);
}

std::cout << "All threads are now waiting for the signal.\n";
Sleep(1000); // 假设做一些准备工作

std::cout << "Triggering the event to allow all threads to work...\n";
SetEvent(eventHandle); // 设置事件,唤醒所有线程

for (int i = 0; i < 5; ++i) {
threads[i].join();
}

// 释放资源
CloseHandle(eventHandle);

return 0;
}

事件对象在此场景下充当了一个信号源,所有线程都等待这个事件。它们会在事件信号被设置后同时开始工作。

SetEvent 方法设置事件为有信号状态,使得所有等待的线程可以同时被唤醒。

这在互斥体中做不到。互斥体是用来保证一个线程独占资源的,无法用于多个线程等待同一个信号并同时开始工作。

互斥体能做到,但是事件做不到

场景:

互斥体允许一个线程多次获取同一个互斥体,而不会死锁,这是事件对象做不到的。你可以通过互斥体的递归锁定来在递归函数中多次获得锁,而不需要担心死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <windows.h>
#include <iostream>

HANDLE hMutex;

void RecursiveFunction(int count)
{
// 递归调用前获取互斥体
WaitForSingleObject(hMutex, INFINITE);

// 模拟一些操作
std::cout << "Entering function with count: " << count << std::endl;

// 递归调用
if (count > 0)
{
RecursiveFunction(count - 1);
}

// 退出递归时释放互斥体
std::cout << "Exiting function with count: " << count << std::endl;
ReleaseMutex(hMutex);
}

int main()
{
// 创建互斥体对象(没有指定为递归,Windows默认是递归互斥体)
hMutex = CreateMutex(NULL, FALSE, NULL);

// 调用递归函数
RecursiveFunction(3);

// 清理
CloseHandle(hMutex);

return 0;
}

内核事件对象常用API

1.KeInitializeEvent

功能:初始化 KEVENT 对象。

1
2
3
4
5
VOID KeInitializeEvent(
IN OUT PRKEVENT Event,
IN EVENT_TYPE Type,
IN BOOLEAN State
);

Event: 指向 KEVENT 对象的指针。

Type: 指定事件类型,通常为 NotificationEvent(自动重置事件)或 SynchronizationEvent(手动重置事件)。

State: 初始状态,TRUE 表示事件对象初始时为已信号状态,FALSE 为未信号状态。

2. KeSetEvent

功能:将 KEVENT 对象的状态设置为 已信号(signaled),通知等待该事件的线程继续执行。

3.KeClearEvent

功能:将 KEVENT 对象的状态设置为 无信号(non-signaled),即清除事件的信号。

4. KeWaitForSingleObject

功能:等待一个内核对象(如 KEVENT)进入已信号状态,阻塞当前线程,直到事件发生。

5. KeWaitForMultipleObjects

功能:等待多个内核对象中的任意一个进入已信号状态。

6. KeResetEvent

功能:与 KeClearEvent 类似,KeResetEvent 也将 KEVENT 对象的状态设置为无信号。

利用事件进行R0 R3通讯

一般来说,会用DeviceIoControl进行通讯,但是用轮询的方式太消耗资源,所以我们一般创建一个同步对象,这样三环阻塞,让出CPU,等待同步对象激活,唤醒三环,这样可以提高CPU的利用率

代码示例

驱动代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//头文件
#pragma once
extern "C"
{
#include <ntifs.h>
#include <ntddk.h>
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath);
VOID DriverUnloadRoutine(IN PDRIVER_OBJECT DriverObject);
}



//.cpp文件
#include "header.h"
#define DebugPrint(...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,__VA_ARGS__)

UNICODE_STRING EventName;

PDEVICE_OBJECT g_pDeviceObject = NULL;

#define CTRLCODE_BASE 0x8000

#define MYCTRL_CODE(i) \
CTL_CODE(FILE_DEVICE_UNKNOWN,CTRLCODE_BASE + i, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_PROCWATCH MYCTRL_CODE(0)


typedef struct _DEVICE_EXTENSION
{
HANDLE hProcessHandle; // 事件对象句柄
PKEVENT ProcessEvent; // 用户和内核通信的事件对象指针
HANDLE hParentId; // 在回调函数中保存进程信息
HANDLE hProcessId;
BOOLEAN bCreate;
} DEVICE_EXTENSION, * PDEVICE_EXTENSION;


DEVICE_EXTENSION Device_extension = { 0 };

VOID InitEvent(DEVICE_EXTENSION* pDeviceExtension)
{
RtlInitUnicodeString(&EventName, L"\\BaseNamedObjects\\ProcEvent");
pDeviceExtension->ProcessEvent = IoCreateNotificationEvent(&EventName, &pDeviceExtension->hProcessHandle);
if (pDeviceExtension->ProcessEvent)
{
DebugPrint("创建对象成功!");
}
else
{
DebugPrint("创建对象失败!");
}
KeClearEvent(pDeviceExtension->ProcessEvent); // 设置为非受信状态
}


VOID ProcessCreateMon(IN HANDLE hParentId, IN HANDLE PId, IN BOOLEAN bCreate)
{
// 获得DEVICE_EXTENSION结构
PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)g_pDeviceObject->DeviceExtension;

// 保存信息
deviceExtension->hParentId = hParentId;
deviceExtension->hProcessId = PId;
deviceExtension->bCreate = bCreate;

// 触发事件,通知应用程序
KeSetEvent(deviceExtension->ProcessEvent, 0, FALSE);

}


//处理设备对象操作
NTSTATUS CommonDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DeviceObject;
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0L;
IoCompleteRequest(Irp, 0);
return Irp->IoStatus.Status;
}

typedef struct _ProcMonData
{
HANDLE hParentId;
HANDLE hProcessId;
BOOLEAN bCreate;
}ProcMonData, * PProcMonData;

NTSTATUS IoctrlDispatch(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
pDeviceObject;

NTSTATUS ntStatus = STATUS_SUCCESS;
PVOID pUserBuffer = NULL;
ULONG ulInputSize = 0;
ULONG ulOutputSize = 0;
PIO_STACK_LOCATION pIrpStack = NULL;
ULONG ulIoCtrlCode = 0;
PProcMonData pProcMonData = NULL;
PDEVICE_EXTENSION pDeviceExtension = NULL;

pIrpStack = IoGetCurrentIrpStackLocation(pIrp);

pUserBuffer = pIrp->AssociatedIrp.SystemBuffer;

pProcMonData = (PProcMonData)pUserBuffer;

ulIoCtrlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
ulInputSize = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
ulOutputSize = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;

switch (ulIoCtrlCode)
{
case IOCTL_PROCWATCH:

pDeviceExtension = (PDEVICE_EXTENSION)g_pDeviceObject->DeviceExtension;

pProcMonData->bCreate = pDeviceExtension->bCreate;
pProcMonData->hParentId = pDeviceExtension->hParentId;
pProcMonData->hProcessId = pDeviceExtension->hProcessId;
ulOutputSize = sizeof(DEVICE_EXTENSION);
break;

default:
ntStatus = STATUS_INVALID_PARAMETER;
ulOutputSize = 0;
break;
}

pIrp->IoStatus.Status = ntStatus;
pIrp->IoStatus.Information = ulOutputSize;
// 获得DEVICE_EXTENSION结构
PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)g_pDeviceObject->DeviceExtension;
KeClearEvent(deviceExtension->ProcessEvent);
DebugPrint("ProcessCreateMon triggered event: ParentId = %p, ProcessId = %p, bCreate = %d\n", pProcMonData->hParentId, pProcMonData->hProcessId, pProcMonData->bCreate);
IoCompleteRequest(pIrp, IO_NO_INCREMENT);

return ntStatus;
}


NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DebugPrint(("Create!"));
KdBreakPoint();



// 驱动程序卸载例程&注册例程
DriverObject->DriverUnload = DriverUnloadRoutine;
//创建设备
NTSTATUS status;
PDEVICE_OBJECT DeviceObject = NULL;
UNICODE_STRING DeviceName;
RtlInitUnicodeString(&DeviceName, L"\\Device\\MyDevice");
status = IoCreateDevice(
DriverObject, // 驱动程序对象
0, // 设备扩展大小
&DeviceName, // 设备名称
FILE_DEVICE_UNKNOWN, // 设备类型
0, // 设备特征
FALSE, // 非独占设备
&DeviceObject // 返回的设备对象指针
);
DeviceObject->Flags |= DO_BUFFERED_IO;
g_pDeviceObject = DeviceObject;


if (!NT_SUCCESS(status))
{
KdPrint(("Failed to create device: %X\n", status));
return status;
}
KdPrint(("Device created successfully\n"));

UNICODE_STRING symbolicLink;
RtlInitUnicodeString(&symbolicLink, L"\\??\\MyDevice_Link");
status = IoCreateSymbolicLink(&symbolicLink, &DeviceName);
if (!NT_SUCCESS(status))
{
KdPrint(("Failed to create device: %X\n", status));
IoDeleteDevice(DriverObject->DeviceObject);
return status;
}
DebugPrint(("Device created successfully\n"));


InitEvent(&Device_extension);
DeviceObject->DeviceExtension = &Device_extension;


status = PsSetCreateProcessNotifyRoutine(ProcessCreateMon, FALSE);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(DriverObject->DeviceObject);
DebugPrint("PsSetCreateProcessNotifyRoutine()\n");
return status;
}


for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
DriverObject->MajorFunction[i] = CommonDispatch;
}
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoctrlDispatch;



return STATUS_SUCCESS;
}



VOID DriverUnloadRoutine(IN PDRIVER_OBJECT DriverObject)
{
if (DriverObject->DeviceObject != NULL)
{
PsSetCreateProcessNotifyRoutine(ProcessCreateMon, TRUE);
UNICODE_STRING symbolicLink = RTL_CONSTANT_STRING(L"\\??\\MyDevice_Link");
IoDeleteSymbolicLink(&symbolicLink);
IoDeleteDevice(DriverObject->DeviceObject);
}

DbgPrint("Driver unloaded\n");
}

三环代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

#include <iostream>

#include <windows.h>
#include <psapi.h>
#pragma comment(lib, "Psapi.lib")
using namespace std;

#define CTRLCODE_BASE 0x8000
#define MYCTRL_CODE(i) \
CTL_CODE(FILE_DEVICE_UNKNOWN,CTRLCODE_BASE + i, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_PROCWATCH MYCTRL_CODE(0)

typedef struct _ProcMonData
{
HANDLE hParentId;
HANDLE hProcessId;
BOOLEAN bCreate;
}ProcMonData, * PProcMonData;


void PrintProcessStatus(ProcMonData procmondata)
{
if (procmondata.bCreate == TRUE)
{
printf("有进程创建,PID:%llu\n", procmondata.hProcessId);
}
else
{
printf("有进程退出,PID:%llu\n", procmondata.hProcessId);
}
}



int main()
{
HANDLE hEvent = OpenEvent(SYNCHRONIZE, FALSE, L"Global\\ProcEvent");
if (hEvent == NULL)
{
printf("Failed to open event: %lu\n", GetLastError());
system("pause");
return 1;
}

HANDLE hDevice = CreateFile(L"\\\\.\\MyDevice_Link", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("can't open the device!\n");
printf("Failed to open device: %lu\n", GetLastError());
system("pause");
return 0;
}



// 等待事件被信号
while (1)
{
printf("等待事件\n");
WaitForSingleObject(hEvent, INFINITE);
printf("获得事件\n");
DWORD dwBytesReturned;

ProcMonData procmondata = { 0 };

BOOL bResult = DeviceIoControl(
hDevice,
IOCTL_PROCWATCH,
nullptr,
NULL,
&procmondata,
sizeof(ProcMonData) + 1,
&dwBytesReturned,
NULL
);

printf("%llu %llu %llu\n", procmondata.bCreate, procmondata.hParentId, procmondata.hProcessId);
printf("收到%d个字节\n", dwBytesReturned);
PrintProcessStatus(procmondata);
procmondata = { 0 };

}



// 关闭句柄
CloseHandle(hEvent);
system("pause");
return 0;
}

效果图

1730979529633

ERESOURCE(读写共享锁)

共享/独占锁概念

基于windows api实现的共享锁/独占锁 - mthoutai - 博客园

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#ifdef __cplusplus
extern "C" {
#endif
struct SELock //Shared & Exclusive lock
{
RTL_CRITICAL_SECTION sec_shared, sec_exclusive; //对加锁代码本身进行临界区控制
HANDLE exclusive_evt;
HANDLE shared_evt;
volatile long shared_count;
};

//初始化一个SE锁
_inline void InitializeSELock(SELock* lock)
{
InitializeCriticalSection(&lock->sec_shared);
InitializeCriticalSection(&lock->sec_exclusive);
lock->exclusive_evt = CreateEventW(NULL, TRUE, TRUE, NULL);
lock->shared_evt = CreateEventW(NULL, TRUE, TRUE, NULL);
lock->shared_count = 0;
}

//清理一个SE锁
_inline void DeleteSELock(SELock* lock)
{
DeleteCriticalSection(&lock->sec_shared);
DeleteCriticalSection(&lock->sec_exclusive);
CloseHandle(lock->exclusive_evt);
CloseHandle(lock->shared_evt);
lock->shared_count = 0;
}

//请求共享锁,用于读
_inline void AcquireSELockShared(SELock* lock)
{
EnterCriticalSection(&lock->sec_exclusive);
EnterCriticalSection(&lock->sec_shared);
WaitForSingleObject(lock->exclusive_evt, INFINITE); //等待独占锁
++lock->shared_count;
if (lock->shared_count)
ResetEvent(lock->shared_evt); //打开共享锁
LeaveCriticalSection(&lock->sec_shared);
LeaveCriticalSection(&lock->sec_exclusive);
}

//释放共享锁
_inline void ReleaseSELockShared(SELock* lock)
{
EnterCriticalSection(&lock->sec_shared);
--lock->shared_count;
if (!lock->shared_count)
SetEvent(lock->shared_evt); //关闭共享锁
LeaveCriticalSection(&lock->sec_shared);
}

//请求独占锁
_inline void AcquireSELockExclusive(SELock* lock)
{
EnterCriticalSection(&lock->sec_exclusive);
WaitForSingleObject(lock->exclusive_evt, INFINITE); //等待独占锁
WaitForSingleObject(lock->shared_evt, INFINITE); //等待共享锁
ResetEvent(lock->exclusive_evt); //打开独占锁
LeaveCriticalSection(&lock->sec_exclusive);
}

//释放独占锁
_inline void ReleaseSELockExclusive(SELock* lock)
{
SetEvent(lock->exclusive_evt); //关闭独占锁
}


#ifdef __cplusplus
}
#endif

相关API:

ExAcquireResourceSharedLite

该函数用于获取共享资源锁。共享资源锁允许多个线程并发访问,但在该资源上不允许进行写操作。

函数声明:

1
2
3
4
VOID ExAcquireResourceSharedLite(
_Inout_ PERSOURCE Resource,
_In_ BOOLEAN Wait
);
  • 参数
    • Resource:指向 ERESOURCE 对象的指针。
    • Wait:如果为 TRUE,线程将在无法获取资源时等待。如果为 FALSE,则不会阻塞线程,若资源不可用,函数将立即返回。
  • 功能:请求共享资源锁。多个线程可以同时获取共享资源,但不能进行修改操作,通常用于读操作。

示例:

1
ExAcquireResourceSharedLite(&Resource, TRUE);  // 请求共享资源并等待

ExAcquireResourceExclusiveLite

该函数用于获取独占资源锁。独占资源锁只允许一个线程访问资源,其他线程必须等待。通常用于写操作。

函数声明:

1
2
3
4
VOID ExAcquireResourceExclusiveLite(
_Inout_ PERSOURCE Resource,
_In_ BOOLEAN Wait
);
  • 参数
    • Resource:指向 ERESOURCE 对象的指针。
    • Wait:如果为 TRUE,线程将在无法获取资源时等待。如果为 FALSE,则不会阻塞线程,若资源不可用,函数将立即返回。
  • 功能:请求独占资源锁。只有一个线程可以获取独占资源锁,因此适合用于写操作。

示例:

1
ExAcquireResourceExclusiveLite(&Resource, TRUE);  // 请求独占资源并等待

ExAcquireSharedStarveExclusive

该函数获取共享资源锁,但会优先等待并抢占任何当前的独占锁请求。它允许共享访问,但如果有独占锁的请求,它会“饿死”独占锁的请求,直到所有共享锁请求释放。

函数声明:

1
2
3
4
VOID ExAcquireSharedStarveExclusive(
_Inout_ PERSOURCE Resource,
_In_ BOOLEAN Wait
);
  • 参数
    • Resource:指向 ERESOURCE 对象的指针。
    • Wait:如果为 TRUE,线程将在无法获取资源时等待。如果为 FALSE,则不会阻塞线程,若资源不可用,函数将立即返回。
  • 功能:请求共享资源锁。该函数的行为是”优先共享”,即如果当前有独占锁的请求,它会“饿死”独占锁,直到共享锁释放。

示例:

1
ExAcquireSharedStarveExclusive(&Resource, TRUE);  // 请求共享资源,并优先共享

ExAcquireSharedWaitForExclusive

该函数类似于 ExAcquireSharedStarveExclusive,但是它会在获取共享锁之前等待独占锁的请求。也就是说,线程在获取共享锁之前会先等到所有独占锁请求都释放,这有助于防止过多的线程在进行写操作时产生不一致的状态。

函数声明:

1
2
3
4
VOID ExAcquireSharedWaitForExclusive(
_Inout_ PERSOURCE Resource,
_In_ BOOLEAN Wait
);
  • 参数
    • Resource:指向 ERESOURCE 对象的指针。
    • Wait:如果为 TRUE,线程将在无法获取资源时等待。如果为 FALSE,则不会阻塞线程,若资源不可用,函数将立即返回。
  • 功能:请求共享资源锁,并且会在获取共享锁之前等待独占锁请求。

示例:

1
ExAcquireSharedWaitForExclusive(&Resource, TRUE);  // 请求共享资源并等待独占锁释放

总结:这四个 API 的区别

  1. ExAcquireResourceSharedLite:请求共享锁,允许多个线程并发读取资源。
  2. ExAcquireResourceExclusiveLite:请求独占锁,确保只有一个线程能够访问资源,通常用于写操作。
  3. ExAcquireSharedStarveExclusive:请求共享锁,并优先等待并阻止独占锁请求。适用于“共享优先”的情况。(读优先)
  4. ExAcquireSharedWaitForExclusive:请求共享锁,但在获取共享锁之前会等待所有的独占锁释放。(写优先)

在微软的官方文档我们可以知道:

ExAcquireResourceExclusiveLite 函数 (wdm.h) - Windows drivers | Microsoft Learn

在调用此例程之前,必须禁用正常的内核 APC 传递。 通过调用 KeEnterCriticalRegion 禁用正常的内核 APC 传递。 在释放资源之前,传递必须保持禁用状态,此时可以通过调用 KeLeaveCriticalRegion 重新启用它。 有关详细信息,请参阅 禁用 APC

所以在使用这些 ERESOURCE(读写共享锁) API之前,我们需要使用KeEnterCriticalRegion KeLeaveCriticalRegion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
VOID MyFunction()
{
ERESOURCE Resource;
ExInitializeResource(&Resource);

// 禁用内核 APC 传递
KeEnterCriticalRegion();

// 获取共享资源
ExAcquireResourceSharedLite(&Resource, TRUE);

// 执行需要保护的操作
// 例如:对共享资源的读取操作

// 释放共享资源
ExReleaseResourceLite(&Resource);

// 重新启用 APC 传递
KeLeaveCriticalRegion();
}

FAST_MUTEX

1731246105883

常用的 FAST_MUTEX API

  • **ExInitializeFastMutex**:初始化 FAST_MUTEX 变量。
  • **ExAcquireFastMutex**:获取 FAST_MUTEX
  • **ExReleaseFastMutex**:释放 FAST_MUTEX
  • **ExTryToAcquireFastMutex**:尝试获取 FAST_MUTEX(如果无法获取,则立即返回)。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FAST_MUTEX FastMutex;

// 初始化FAST_MUTEX
ExInitializeFastMutex(&FastMutex);

void SomeFunction()
{
// 获取锁
ExAcquireFastMutex(&FastMutex);

// 进入临界区
// 执行需要同步的操作
...

// 释放锁
ExReleaseFastMutex(&FastMutex);
}

KSEMAPHORE

KSEMAPHORE 适用于以下情况:

  1. 限制资源的并发访问数量:当多个线程想访问一个资源(比如缓冲区或设备)时,使用 KSEMAPHORE 可以确保同一时刻只能有固定数量的线程进入该资源。
  2. 管理资源池:比如在驱动程序中,控制同时可以访问设备的请求数量,防止系统负载过高。
  3. 生产者-消费者模型:在有多个生产者和消费者的情况下,信号量可以用于控制资源的可用数量,避免资源竞争。

KSEMAPHORE 的初始化和使用方法

1
2
3
4
5
VOID KeInitializeSemaphore(
PKSEMAPHORE Semaphore,
LONG Count,
LONG Limit
);

Semaphore:指向信号量对象的指针。

Count:初始化的计数值,表示当前可以访问资源的线程数。

Limit:信号量计数器的上限值,表示最大允许同时访问资源的线程数。

示例:初始化信号量,限制最多3个线程同时访问

1
2
KSEMAPHORE Semaphore;
KeInitializeSemaphore(&Semaphore, 3, 3);

获取信号量 调用 KeWaitForSingleObject 获取信号量。当信号量计数大于 0 时,当前线程可以继续执行,计数值会减 1;如果计数为 0,线程会等待直到有其他线程释放信号量。

1
2
3
4
5
6
7
NTSTATUS status = KeWaitForSingleObject(
&Semaphore, // 指向信号量对象
Executive, // 等待的原因
KernelMode, // 等待模式
FALSE, // 警报状态
NULL // 超时值(NULL 表示无限等待)
);

释放信号量 当线程完成对资源的访问后,调用 KeReleaseSemaphore 释放信号量,并将计数值加 1。

1
2
3
4
5
6
7
LONG PreviousCount;
KeReleaseSemaphore(
&Semaphore, // 指向信号量对象
IO_NO_INCREMENT, // 优先级增量
1, // 增加的计数
FALSE // 当前线程不立即让出CPU
);xxxxxxxxxx LONG PreviousCount;KeReleaseSemaphore(   &Semaphore,           // 指向信号量对象   IO_NO_INCREMENT,       // 优先级增量   1,                     // 增加的计数   FALSE                 // 当前线程不立即让出CPU);复制代码

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
KSEMAPHORE Semaphore;

VOID InitializeResourceControl()
{
// 初始化信号量,最多允许3个线程同时访问资源
KeInitializeSemaphore(&Semaphore, 3, 3);
}

VOID AccessResource()
{
// 获取信号量
NTSTATUS status = KeWaitForSingleObject(&Semaphore, Executive, KernelMode, FALSE, NULL);

if (NT_SUCCESS(status)) {
// 在临界区内访问资源
// ...

// 释放信号量
KeReleaseSemaphore(&Semaphore, IO_NO_INCREMENT, 1, FALSE);
}
}

SPIN_LOCK

自旋锁适用于短时间的临界区操作,特别是以下场景:

  1. 小代码段:操作简单且执行时间短的临界区。
  2. 高优先级代码:中断处理程序等无法进行线程休眠的场合。
  3. 高性能要求:希望避免进入等待队列和上下文切换开销的情况。

API

1
2
3
4
5
KeInitializeSpinLock
KeAcquireSpinLock
KeReleaseSpinLock
KeAcquireSpinLockAtDpcLevel
KeReleaseSpinLockFromDpcLevel
  1. 初始化自旋锁

在使用自旋锁前,必须初始化它。

1
2
3
4
5
6
KSPIN_LOCK SpinLock;

VOID InitializeLock()
{
KeInitializeSpinLock(&SpinLock);
}

2. 获取自旋锁

获取自旋锁会阻止其他线程访问该资源。获取时可以保存当前 IRQL 值,以便后续恢复。

1
2
KIRQL oldIrql;
KeAcquireSpinLock(&SpinLock, &oldIrql);

注意:获取自旋锁时会提升当前处理器的 IRQL 到 DISPATCH_LEVEL,确保在持有锁期间不会被中断打断。

3.释放自旋锁

完成对共享资源的操作后,释放自旋锁并恢复原来的 IRQL。

1
KeReleaseSpinLock(&SpinLock, oldIrql);

高级用法:在 DPC 级别操作自旋锁

有时在 DPC 级别下使用自旋锁,可以使用 KeAcquireSpinLockAtDpcLevelKeReleaseSpinLockFromDpcLevel 函数来避免多余的 IRQL 处理。

1
2
3
KeAcquireSpinLockAtDpcLevel(&SpinLock);
// 临界区操作
KeReleaseSpinLockFromDpcLevel(&SpinLock);

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
KSPIN_LOCK SpinLock;

VOID Initialize()
{
KeInitializeSpinLock(&SpinLock);
}

VOID AccessResource()
{
KIRQL oldIrql;

// 获取自旋锁
KeAcquireSpinLock(&SpinLock, &oldIrql);

// 临界区操作(需短小且高效)
// ...

// 释放自旋锁并恢复原来的 IRQL
KeReleaseSpinLock(&SpinLock, oldIrql);
}

死锁

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。

例如现在有A,B两把锁,1,2线程想要去获取这俩锁
现在线程1有A锁,线程2有B锁,然后线程1又想去获取B锁,线程2想去获取A锁,于是就造成了死锁

死锁产生的四个必要条件
(1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

(2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

(3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

(4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

windbg分析锁:3:25:00