《重要》内核下各种同步处理方法(自旋锁、信号灯、互斥体…)

1.在支持多线程的操作系统下,有些函数会出现不可重入的现象。所谓“可重入”是指函数的执行结果不和执行顺序有关。反之如果执行结果和执行顺序有关,则称这个函数是“不可重入”的。

 

2.Windows将中断的概念进行了扩展,提出一个中断请求级(IRQL)的概念。其中规定了32个中断请求级别,分别是0~2级别为软件中断,3~31级为硬件中断,其中数字从0~31,优先级别逐次递增。

image

在内核模式下,可以通过调用KeGetCurrentIrql内核函数来得到当前的IRQL级别。

 

3.线程运行在PASSIVE_LEVEL,这个时候操作系统随时可能将当前线程切换到别的线程,但是如果提升IRQL到DISPATCH_LEVEL级别,这时候不会出现线程切换。这是一种很常用的同步处理机制,但这种方法只能使用于单CPU的系统。对于多CPU的系统,需要采用别的同步处理机制。

 

4.读取不在物理内存中的分页内存时,会引发一个页故障,从而执行异常的处理函数重新将磁盘的文件内容交换到物理内存中。页故障允许出现在PASSIVE_LEVEL级别的程序中,但如果在DISPATCH_LEVEL或者更高级别IRQL的程序中会带来系统崩溃。对于等于或高于DISPATCH_LEVEL级别的程序不能使用分页内存,必须使用非分页内存。驱动程序的StartIO例程、DPC例程、中断服务例程都运行在DISPATCH_LEVEL或者更高IRQL。因此,在这些例程中不能使用分页内存,否则会导致系统崩溃。

 

5.控制IRQL提升与降低:KeRaiseIrql/KeLowerIrql

 

6.自旋锁:不同于线程中的等待时间。在线程中如果等待某个事件,操作系统会使这个线程进入休眠状态,CPU会运行其他线程,而自旋锁原理则不同,他不会切换到别的线程,而是一直让这个线程“自旋”。因此,对于自旋锁占用时间不宜过长,否则会导致申请自旋锁的其他线程处于自旋,这会浪费CPU宝贵的时间。在单CPU的系统中,获取自旋锁只是将当前的IRQL从PASSVIE_LEVEL级别提升到DISPATCH_LEVEL级别。驱动程序必须在低于或者等于DISPATCH_LEVEL的IRQL级别中使用自旋锁。

注意:如果在DISPATHC_LEVEL级别申请自旋锁,那么不会改变IRQL级别。这时,申请和释放自旋锁可以简单的使用KeAcquireSpinLockAtDpcLevelKeReleaseSpinLockFromDpcLevel内核函数。

KeInitializeSpinLock         //初始化自旋锁

KeAcquireSpinLock          //获取自旋锁

KeReleaseSpinLock         //释放自旋锁

 

7.用户模式的等待

WaitForSingleObject
WaitForMultipleObjects

 

8.用户模式的事件

CreateEvent

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
                      // pointer to security attributes
BOOL bManualReset,  // flag for manual-reset event
BOOL bInitialState, // flag for initial state
LPCTSTR lpName      // pointer to event-object name
);

bManualReset表示创建的事件是否是手动模式,当事件处于激发状态后,需要手动设置才能回到未激发状态。如果是自动模式,当事件处于激发状态后,遇到任意一个等待(如WaitForSingleObject),则自动变为未激发状态。

示例代码:

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
#include <stdio.h>
#include <process.h>
#include <windows.h>

UINT WINAPI ThreadProc( LPVOID lpParameter )
{
         printf("Enter Second Thread!\n");
         HANDLE hEvent = *(PHANDLE)lpParameter;
         Sleep(1000);
         printf("Leave Second Thread!\n");
         //激发事件
         SetEvent(hEvent);
         return 0;
}

int main(void)
{
         printf("Enter Main Thread!\n");
         //定义一个自动重置的,未激发的事件对象
         HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
         HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, &hEvent, 0, NULL);
         //等待该事件激发
         WaitForSingleObject(hEvent, INFINITE);
         printf("Leave Main Thread!\n");
         return 0;

}

9.用户模式的信号灯

信号灯内部有个计数器,可以理解信号灯内部有N个灯泡,如果有一个灯泡亮着,就代表信号灯处于激发状态,如果全部熄灭,就代表信号灯处于未激发状态。

创建信号灯:

HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,// pointer to security attributes
  LONG lInitialCount,  // initial count
  LONG lMaximumCount,  // maximum count
  LPCTSTR lpName       // pointer to semaphore-object name
);
增加信号灯的计数器:
BOOL ReleaseSemaphore(
  HANDLE hSemaphore,   // handle to the semaphore object
  LONG lReleaseCount,  // amount to add to current count
  LPLONG lpPreviousCount   // address of previous count
);
 
对信号灯执行一次等待操作,就会减少一个计数,就相当于熄灭一个灯泡。当计数为0时,也就是所有灯泡都熄灭时,当前线程进入睡眠状态,直到信号灯变成激发状态或者超时。

示例代码:

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
#include <windows.h>
#include <stdio.h>
#include <process.h>

UINT WINAPI ThreadProc(LPVOID lpParam)
{
         printf("Enter Second Thread!\n");
         HANDLE hSemaphore = *(PHANDLE)lpParam;
         Sleep(5000);
         printf("Leave Second Thread!\n");
         //将信号灯计数加1,使之成为激发状态
         ReleaseSemaphore(hSemaphore, 1, NULL);
         return 0;
}

int main(void)
{
         printf("Enter Main Thread!\n");
         //创建信号灯
         HANDLE hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
         //此时信号灯计数为2,处于激发状态
         WaitForSingleObject(hSemaphore, INFINITE);
         //此时信号灯计数为1,处于激发状态
         WaitForSingleObject(hSemaphore, INFINITE);
         //创建新线程
         HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, &hSemaphore, 0, NULL);
         //此时信号灯计数为0,处于未激发状态,为了等待信号灯,线程被挂起
         WaitForSingleObject(hSemaphore, INFINITE);
         printf("Leave Main Thread!\n");
         return 0;
}

10.用户模式的互斥体

获得了互斥体之后,同一个线程中可以递归获得互斥体。所谓递归获得互斥体就是得到互斥体的线程还可以再次获得这个互斥体,或者说互斥体对于已经获得互斥体的线程不产生”互斥”关系。

创建互斥体:

HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
                       // pointer to security attributes
BOOL bInitialOwner,  // flag for initial ownership
LPCTSTR lpName       // pointer to mutex-object name
       );

释放互斥体:

BOOL ReleaseMutex(
  HANDLE hMutex   // handle to mutex object
);

示例代码:

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
#include <windows.h>
#include <process.h>
#include <stdio.h>
 
UINT WINAPI ThreadProc1(LPVOID lparam)
{
         HANDLE hMutex = *(PHANDLE)lparam;
         //等待互斥体
         WaitForSingleObject(hMutex, INFINITE);
         //对于同一个线程,已经获得了互斥体,还可以多次获取
         WaitForSingleObject(hMutex, INFINITE);
         printf("Enter  Thread1!\n");
         Sleep(2000);
         printf("Leave  Thread1!\n");
         //释放互斥体
         ReleaseMutex(hMutex);
         return 0;
}

UINT WINAPI ThreadProc2(LPVOID lparam)
{      
         HANDLE hMutex = *(PHANDLE)lparam;
         WaitForSingleObject(hMutex, INFINITE);
         printf("Enter  Thread2!\n");
         Sleep(2000);
         printf("Leave  Thread2!\n");
         ReleaseMutex(hMutex);
         return 0;

}

int main(void)
{
         printf("Enter Main Thread!\n");
         //创建互斥体
         HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
         HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, ThreadProc1, &hMutex, 0, NULL);
         HANDLE hThread2 = (HANDLE)_beginthreadex(NULL, 0, ThreadProc2, &hMutex, 0, NULL);
         Sleep(6000);
         printf("Leave Main Thread!\n");
         return 0;
}

11.等待线程完成

还有一种同步对象,就是线程对象。每个线程同样有两个状态,激发状态和未激发状态。当线程在运行中的时候,是未激发状态。当线程终止后,线程处于激发状态。可以用WaitFor*函数对线程句柄进行等待。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <windows.h>
#include <stdio.h>
#include <process.h>

UINT WINAPI ThreadProc(LPVOID lpParam)
{
         printf("Enter ThreadProc!\n");
         Sleep(5000);
         printf("Leave ThreadProc!\n");
         return 0;
}

int main(void)
{
         //创建两个字线程
         HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, 0, NULL);
         HANDLE hThread2 = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, 0, NULL);
         //主线程等待两个字线程结束
         HANDLE hThread[2] = {hThread1, hThread2};
         WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
         return 0;
}

12.内核模式下的等待

NTSTATUS
  KeWaitForSingleObject(
    IN PVOID  Object,
    IN KWAIT_REASON  WaitReason,
    IN KPROCESSOR_MODE  WaitMode,
    IN BOOLEAN  Alertable,
    IN PLARGE_INTEGER  Timeout  OPTIONAL
    );
Object:是一个同步对象的指针,注意这里不是句柄
WaitReason:等待的原因,一般为Executive
WaitMode:等待模式,一般为KernelMode
Alertable:指明等待是否为“警惕”的,一般为FALSE
Timeout:等待时间,如果为NULL,就表示无限期等待,直到同步对象变为激发态

KeWaitForMultipleObjects

 

13.内核模式下开启多线程

内核函数PsCreateSystemThread负责创建新线程。该函数可以创建两种线程,一种是用户线程,它属于当前进程中的线程。例如,如果IRP_MJ_READ的派遣函数中调用了PsCreateSystemThread,那么新创建的线程属于调用ReadFile的进程。另一种是系统线程,系统线程不属于当前用户进程,而是属于系统进程,一般PID为4,名字为“System”的进程。

NTSTATUS
  PsCreateSystemThread(
    OUT PHANDLE  ThreadHandle, //新创建的线程句柄
    IN ULONG  DesiredAccess, //创建的权限
    IN POBJECT_ATTRIBUTES  ObjectAttributes  OPTIONAL,//线程的属性,一般设为NULL
    IN HANDLE  ProcessHandle  OPTIONAL,//指定创建用户线程还是系统线程。如果为NULL,则为系统进程,如果该值是一个进程句柄,则新创建的线程属于这个指定的进程。DDK提供的NTCurrentProcess可以得到当前进程的句柄。
    OUT PCLIENT_ID  ClientId  OPTIONAL,
    IN PKSTART_ROUTINE  StartRoutine,//新线程的运行地址
    IN PVOID  StartContext //新线程接收的参数
    );
注意:在内核模式下创建的线程是无法自动退出的,必须使用PsTerminateSystemThread强制结束线程。

 

14.内核模式下的事件对象

在内核中,用KEVENT数据结构表示一个事件对象。在使用前,需要进行初始化。
VOID
  KeInitializeEvent(
    IN PRKEVENT  Event, //要初始化的KEVENT结构对象
    IN EVENT_TYPE  Type, //事件的类型。
    IN BOOLEAN  State //如果为真,初始为激发态
   );
Type:事件的类型,有两类,一类是“通知事件”,对应参数为NotificationEvent,当事件变为激发态时,需要手动将其该为未激发状态。另一类为“同步事件”,对应参数为SynchronizationEvent,当事件为激发态时,如果遇到KeWaitFor*的函数,事件对象则自动变为未激发状态。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VOID SystemThread(IN PVOID lpParam)
{
         KdPrint(("进入系统线程函数!\n"));
         KEVENT kEvent = *(PKEVENT)lpParam;
         KeSetEvent(&kEvent, IO_NO_INCREMENT, FALSE);
         KdPrint(("离开系统线程函数!\n"));
         PsTerminateSystemThread(STATUS_SUCCESS);
}
 
VOID CreateSystemThread()
 {
         KdPrint(("进入主线程!\n"));
         HANDLE hSystemThread;
         KEVENT kEvent;
         //初始化Event
         KeInitializeEvent(&kEvent, SynchronizationEvent, FALSE);
         //创建线程
         NTSTATUS status = PsCreateSystemThread(&hSystemThread, 0, NULL, NULL, NULL,
 SystemThread, &kEvent);
         KeWaitForSingleObject(&kEvent, Executive, KernelMode, FALSE, NULL);
         ZwClose(hSystemThread);
         KdPrint(("离开主线程!\n"));
 }

15.驱动程序与应用程序交互事件对象

应用程序中创建的事件和在内核模式下创建的事件对象,本质上是同一个东西。在用户模式下,它用句柄代表,在内核模式下,它用KEVENT数据结构代表。
在应用程序中,所有内核对象都不会被用户看到,用户看到的只是代表内核对象的对象句柄。
我们要将用户模式下创建的事件传递给驱动程序,可以用DeviceIoControl API函数。DDK提供了内核函数将句柄转化为指针,该函数如下:
NTSTATUS
  ObReferenceObjectByHandle(
    IN HANDLE  Handle,
    IN ACCESS_MASK  DesiredAccess,
    IN POBJECT_TYPE  ObjectType  OPTIONAL,
    IN KPROCESSOR_MODE  AccessMode,
    OUT PVOID  *Object,
    OUT POBJECT_HANDLE_INFORMATION  HandleInformation  OPTIONAL
    );
ObReferenceObjectByHandle函数在得到指针的同时,会为对象的指针维护一个计数。每次调用ObReferenceObjectByHandle函数时会使计数加1.因此为了计数平衡,在使用完ObReferenceObjectByHandle函数后,需要调用如下函数:
VOID
  ObDereferenceObject(
    IN PVOID  Object
    );
ObDereferenceObject函数使计数减一。

示例代码:

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
用户模式下代码:
#include <windows.h>
#include <stdio.h>
#include <winioctl.h>
#include "../NT_Driver/ioctl.h"
 
int main(void)
{
    HANDLE hFile = CreateFile("\\\\.\\HelloDDK", 0, 0,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
       printf("error:%d", GetLastError());
       return -1;
    }
    printf("创建Event!\n");
    //创建一个自动重置的,初始为未激发的事件对象
    HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    DWORD dwRet;
    //调用DeviceIoControl把事件句柄传进内核
    DeviceIoControl(hFile, CLT_TRANSMIT_EVENT,
&hEvent, sizeof(hEvent), NULL, 0, &dwRet, NULL);
    //等待事件被激发
    WaitForSingleObject(hEvent, INFINITE);
    printf("程序退出!\n");
    return 0;
}
 
内核代码:
NTSTATUS FuckDeviceControl(IN PDEVICE_OBJECT pDeviceObj,
IN PIRP pIrp)
{
    KdPrint(("进入IRP_MJ_DEVICE_CONTROL处理函数!\n"));
   
    NTSTATUS status = STATUS_SUCCESS;
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
    ULONG inLength =
              stack->Parameters.DeviceIoControl.InputBufferLength;
    ULONG outLength =
           stack->Parameters.DeviceIoControl.OutputBufferLength;
    ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
 
    switch(code)
    {
       case CLT_TRANSMIT_EVENT:
       {
           //获得传递进来的事件句柄
HANDLE hEvent =
                  *(PHANDLE)pIrp->AssociatedIrp.SystemBuffer;
           PKEVENT pkEvent;
           //把句柄转化为KEvent结构
           status = ObReferenceObjectByHandle(hEvent,
              EVENT_MODIFY_STATE, *ExEventObjectType, KernelMode,
              (PVOID*)&pkEvent, NULL);
           //使事件激发
           KeSetEvent(pkEvent, IO_NO_INCREMENT, FALSE);
           //递减计数
           ObDereferenceObject(pkEvent);
           break;
       }
       default:
           status = STATUS_INVALID_VARIANT;
    }
 
    pIrp->IoStatus.Information = outLength;
    pIrp->IoStatus.Status = status;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    KdPrint(("离开IR_MJ_DEVICE_CONTROL处理函数!\n"));
 
    return STATUS_SUCCESS;
}

16.驱动程序与驱动程序交互对象

有时候,还需要驱动程序与驱动程序之间交互事件对象。例如,驱动程序A的某个派遣函数要与驱动程序B的派遣函数进行同步,就需要两个驱动程序间交互事件对象。

最简单的方法是驱动程序B创建一个有名字的事件对象,这样,驱动程序A就可以根据名字找到事件对象的指针。

创建有名字的事件可以通过IoCreateSynchronizationEventIoCreateNotificationEvent函数。为了进一步得到事件内核对象的指针,可以使用前面介绍的ObReferenceObjectByHandle函数。

17.内核模式下的信号灯

在内核模式下,信号灯对象用KSEMAPHORE数据结构表示。在使用信号灯对象钱,需要对其进行初始化(KeInitializeSemaphore

KeReadStateSemaphore函数可以读取信号灯当前计数。

释放信号灯会增加信号灯计数,它对应内核函数KeReleaseSemaphore函数。

示例代码:

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
void UserThreadProc(IN PVOID lpParam)
{
    KdPrint(("进入新建的线程!\n"));
    //得到信号灯
    KSEMAPHORE kSemaphore = *(PKSEMAPHORE)lpParam;
    //增加信号灯计数
    KeReleaseSemaphore(&kSemaphore, IO_NO_INCREMENT, 1, FALSE);
    KdPrint(("离开新建的线程!\n"));
PsTerminateSystemThread(STATUS_SUCCESS);
}
 
VOID Test()
{
    KdPrint(("进入主线程!\n"));
    HANDLE hUserThread;
    ULONG count;
    KSEMAPHORE kSemaphore;
    //初始化信号灯
    KeInitializeSemaphore(&kSemaphore, 2, 2);
    //两次等待,用信号灯处于未激发状态
    KeWaitForSingleObject(&kSemaphore, Executive, KernelMode,
FALSE, NULL);
    KeWaitForSingleObject(&kSemaphore, Executive, KernelMode,
FALSE, NULL);
    //读取现有的信号灯计数,为0
    count = KeReadStateSemaphore(&kSemaphore);
    KdPrint(("信号灯计数为:%d\n", count));
    //创建新线程
    NTSTATUS status = PsCreateSystemThread(&hUserThread,
THREAD_ALL_ACCESS, NULL, NtCurrentProcess(), NULL,
UserThreadProc, &kSemaphore);
    //等待信号灯被激发
    KeWaitForSingleObject(&kSemaphore, Executive, KernelMode,
 FALSE, NULL);
    KdPrint(("离开主线程!\n"));
}

18.内核模式下的互斥体

互斥体在内核模式下的结构体为KMUTEX,使用前需要进行初始化:(KeInitializeMutex

释放互斥体使用KeReleaseMutex内核函数:

示例代码:

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
void UserThreadProc1( IN PVOID lpParam)
{
    //获得互斥体
    PKMUTEX pMutex = (PKMUTEX)lpParam;
    //等待互斥体
    KeWaitForSingleObject(pMutex, Executive, KernelMode,
FALSE, NULL);
    KdPrint(("进入新建的线程1!\n"));
    //强迫停止50ms
    KeStallExecutionProcessor(50);
    KdPrint(("离开新建的线程1!\n"));
    //释放互斥体
    KeReleaseMutex(pMutex, FALSE);
    PsTerminateSystemThread(STATUS_SUCCESS);
}
 
void UserThreadProc2( IN PVOID lpParam)
{
    PKMUTEX pMutex = (PKMUTEX)lpParam;
    KeWaitForSingleObject(pMutex, Executive, KernelMode,
FALSE, NULL);
    KdPrint(("进入新建的线程2!\n"));
    KeStallExecutionProcessor(50);
    KdPrint(("离开新建的线程2!\n"));
    KeReleaseMutex(pMutex, FALSE);
    PsTerminateSystemThread(STATUS_SUCCESS);
}
 
VOID Test()
{
    HANDLE hUserThread1, hUserThread2;
    KMUTEX kMutex;
    //初始化互斥体
    KeInitializeMutex(&kMutex, 0);
    //创建两个新线程
    NTSTATUS status = PsCreateSystemThread(&hUserThread1, 0,
NULL, NtCurrentProcess(), NULL, UserThreadProc1, &kMutex);
    status = PsCreateSystemThread(&hUserThread2, 0, NULL,
               NtCurrentProcess(), NULL, UserThreadProc2, &kMutex);
   
    PVOID ThreadArray[2];
    //把线程句柄转换为可以等待的指针
    ObReferenceObjectByHandle(hUserThread1, 0, NULL,
KernelMode, &ThreadArray[0], NULL);
    ObReferenceObjectByHandle(hUserThread2, 0, NULL,
KernelMode, &ThreadArray[1], NULL);
    //等待2个新建线程执行完毕
    KeWaitForMultipleObjects(2, ThreadArray, WaitAll,
                         Executive, KernelMode, FALSE, NULL, NULL);
    //减少引用计数
    ObDereferenceObject(ThreadArray[0]);
    ObDereferenceObject(ThreadArray[1]);
    //关闭线程句柄
    ZwClose(hUserThread1);
    ZwClose(hUserThread2);
}

19.快速互斥体

快速互斥体是DDK提供的另外一种内核同步对象,他的特征类似于前面介绍的普通互斥体。他们两的作用完全一样,之所以被称为是快速互斥体,是因为它执行的速度比普通的互斥体速度快(这里指的是获取和释放的速度)。然而,快速互斥体对象不能被递归获取。

普通互斥体在内核中用KMUTEX数据结构表示,而快速互斥体在内核中用FAST_MUTEX数据结构描述。

除此之外,对快速互斥体的初始化,获取和释放对应的内核函数也和普通互斥体不同。

初始化:
VOID
  ExInitializeFastMutex(
    IN PFAST_MUTEX  FastMutex
    );
获取:
VOID
  ExAcquireFastMutex(
    IN PFAST_MUTEX  FastMutex
    );
释放:
VOID
  ExReleaseFastMutex(
    IN PFAST_MUTEX  FastMutex
    );

示例代码:

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
void UserThreadProc1( IN PVOID lpParam)
{
    PFAST_MUTEX pFastMutex = (PFAST_MUTEX)lpParam;
    ExAcquireFastMutex(pFastMutex);
    KdPrint(("进入新建的线程1!\n"));
    KeStallExecutionProcessor(50);
    KdPrint(("离开新建的线程1!\n"));
    ExReleaseFastMutex(pFastMutex);
    PsTerminateSystemThread(STATUS_SUCCESS);
}
 
void UserThreadProc2( IN PVOID lpParam)
{
    //获得快速互斥体
    PFAST_MUTEX pFastMutex = (PFAST_MUTEX)lpParam;
    ExAcquireFastMutex(pFastMutex);
    KdPrint(("进入新建的线程2!\n"));
    //强迫停止50ms
    KeStallExecutionProcessor(50);
    KdPrint(("离开新建的线程2!\n"));
    //释放互斥体
    ExReleaseFastMutex(pFastMutex);
    PsTerminateSystemThread(STATUS_SUCCESS);
}
 
void Test()
{
    HANDLE hUserThread1, hUserThread2;
    //初始化快速互斥体
    FAST_MUTEX fastMutex;
    ExInitializeFastMutex(&fastMutex);
    //创建两个新线程
    NTSTATUS status = PsCreateSystemThread(&hUserThread1, 0,
NULL, NtCurrentProcess(), NULL, UserThreadProc1, &fastMutex);
    status = PsCreateSystemThread(&hUserThread2, 0, NULL,
           NtCurrentProcess(), NULL, UserThreadProc2, &fastMutex);
   
    PVOID ThreadArray[2];
    //把线程句柄转换为可以等待的指针
    ObReferenceObjectByHandle(hUserThread1, 0, NULL,
KernelMode, &ThreadArray[0], NULL);
    ObReferenceObjectByHandle(hUserThread2, 0, NULL,
KernelMode, &ThreadArray[1], NULL);
    //等待2个新建线程执行完毕
    KeWaitForMultipleObjects(2, ThreadArray, WaitAll,
              Executive, KernelMode, FALSE, NULL, NULL);
    //减少引用计数
    ObDereferenceObject(ThreadArray[0]);
    ObDereferenceObject(ThreadArray[1]);
    //关闭线程句柄
    ZwClose(hUserThread1);
    ZwClose(hUserThread2);  
}

20.使用自旋锁进行同步

示例代码:

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
定义设备扩展:
typedef struct _DEVICE_EXTENSION {
    PDEVICE_OBJECT pDevice;
    UNICODE_STRING ustrDeviceName;  //设备名称
    UNICODE_STRING ustrSymLinkName; //符号链接名
    KSPIN_LOCK My_SpinLock;            //自旋锁
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
 
在DriverEntry中初始化自旋锁:
PDEVICE_EXTENSION pDevExt =
                  (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
……
KeInitializeSpinLock(&pDevExt->My_SpinLock);
 
IRP_MJ_DEVICE_CONTROL派遣函数:
NTSTATUS FuckTest(IN PDEVICE_OBJECT pDevObj,
                IN PIRP pIrp)
{
    //为了避免多个派遣函数并行运行,所以进行同步
    //对DeviceIoControl调用来源自用户线程,因此处于PASSIVE_LEVEL。
    ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
    KdPrint(("进入IRP_MJ_DEVICE_CONTROL处理函数!\n"));
   
    PDEVICE_EXTENSION pdx =
                  (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    KIRQL oldIrql;
    //请求获得自旋锁
    KeAcquireSpinLock(&pdx->My_SpinLock, &oldIrql);
 
    KdPrint(("获得自旋锁!\n"));
    //获得自旋锁后,IRQL提升至DISPATCH_LEVEL
    ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
    KdPrint(("IRQL提升至DISPATCH_LEVEL!\n"));
 
    pIrp->IoStatus.Information = 0;
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    //释放自旋锁
    KeReleaseSpinLock(&pdx->My_SpinLock, oldIrql);
    KdPrint(("释放互斥锁!\n"));
    KdPrint(("离开IRP_MJ_DEVICE_CONTROL处理函数!\n"));
 
    return STATUS_SUCCESS;
}
 
测试程序代码:
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include <process.h>
#include "../Driver/ioctl.h"
 
UINT WINAPI ThreadProc1(LPVOID lpParam)
{
    HANDLE hFile = *(PHANDLE)lpParam;
    BOOL bRet = DeviceIoControl(hFile, TEST_CTL, NULL,
0, NULL, 0, NULL, NULL);
    return 0;
}
 
int main(void)
{
    HANDLE hFile = CreateFile("\\\\.\\HelloDDK",
           GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
       printf("打开设备失败!\n");
       return -1;
    }
 
    HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0,
ThreadProc1, &hFile, 0, 0);
    HANDLE hThread2 = (HANDLE)_beginthreadex(NULL, 0,
ThreadProc1, &hFile, 0, 0);
    HANDLE hThread[2] = {hThread1, hThread2};
 
    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
 
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    CloseHandle(hFile);
    return 0;
}

21.使用互锁操作进行同步

DDK提供了两类互锁操作来提供简单的同步处理。一类是InterlockedXX函数,另一类是ExInterlockedXX函数。

其中InterlockedXX函数不是通过自旋锁实现的,内部不会提升IRQL,因此既可以操作分页数据又可以操作非分页数据。而ExInterlockedXX函数是通过自旋锁实现的,在使用的时候需要我们提供一个自旋锁。内部依靠这个自旋锁实现同步,因此它不能操作分页内存。

本文链接:http://www.alonemonkey.com/kernel-synchronization.html