驱动程序运行在系统的内核地址空间,而所有进程共享这2GB的虚拟地址空间,所以绝大多数驱动程序是运行在多线程环境中,有的时候需要对程序进行同步处理,使某些操作是严格串行化的,这就要用到同步的相关内容。
异步是指两个线程各自运行互不干扰,而当某个线程运行取决与另一个线程,也就是要在线程之间进行串行化处理时就需要同步机制。
中断请求级别
在进行I/O操作时会产生中断,以便告知CPU当前I/O操作已完成,此时CPU会停下手头的工作,来处理这个中断请求,在Windows操作系统中,分为硬件中断和软件中断。并且将这些中断映射为不同级别的中断请求级。硬件中断是由硬件产生的中断,软件中断是由int指令产生的。在传统的PC中,一般可以接收16种中断信号,每个信号对应一个中断号。硬件中断分为可屏蔽中断和不可屏蔽中断。可屏蔽中断是由可编程中断控制器(PIC)产生,这是一个硬件设备。在后面的PC机中采用了高级可编程中断控制器(APIC)代替。
在APIC中将中断扩展为24个,每个都有对应的优先级,一般正在运行的线程可以被中断打断,进入中断处理程序,当优先级高的中断来临时处在低优先级的中断也会被打断。在Windows中中断请求级别有32个,但是在编程或者在MSDN上只需要关心两类级别,PASSIVE_LEVEL:用户级别,这个中断级别最低。DISPATCH_LEVEL:级别相对较高。在运用内核函数时可以查看MSDN,运行在低优先级的函数不能进行高优先级的一些操作。下面是一些常用函数的优先级
函数 | 优先级 |
---|---|
DriverEntry AddDevice DriverUnload等函数 | PASSIVE_LEVEL |
各种分发派遣函数 | PASSIVE_LEVEL |
完成函数 | DISPATCH_LEVEL |
NDIS回调函数 | DISPATCH_LEVEL |
在内核模式中可以调用KeGetCurrentIrql得到当前的IRQL
需要注意的是,线程优先级只针对于应用程序,只有在IRQL处于PASSIVE_LEVEL级别才有意义。当线程运行在PASSIVE_LEVEL级别的时候可以进行线程切换,而当IRQL提升到DISPATCH_LEVEL,就不再出现线程切换
PASSIVE_LEVEL是应用层的中断级别,可以有线程切换,处在这个IRQL下的程序是位于进程上下文,可以进行线程的切换休眠等操作,而处于DISPACTH_LEVEL的程序属于中断上下文,CPU会一直执行这个环境下的代码,没有线程切换,不能进行线程的休眠操作,否则,一旦休眠则没有线程能够唤醒。
在内存的使用上,PASSIVE_LEVEL级别的程序可以使用分页内存,一旦发生缺页中断,系统可以进行线程切换,切换到其他进程,将缺页装载在内存,但是在DISPATCH_LEVEL没有线程切换,一旦发生缺页中断就会导致系统崩溃,所以DISPATCH_LEVEL只能使用非分页内存。
我们可以在程序中手动提升和降低当前的IRQL。
VOID
KeRaiseIrql(
IN KIRQL NewIrql, //新IRQL
OUT PKIRQL OldIrql//当前的IRQL
);
VOID
KeLowerIrql(
IN KIRQL NewIrql //新IRQL
);
自旋锁
自旋锁是一种同步机制,他能保证某个资源只被一个线程所拥有。
在初始化自旋锁的时候,处于解锁状态,这个时候线程可以获取自旋锁并访问同步资源,一旦有一个线程获取到自旋锁,必须等到它释放以后,才能被其他线程获取。自旋锁被锁上之后当切换到另外的线程时,线程会不停的询问是否可以获取自旋锁。此时线程处于空转的情况,白白浪费了CPU资源,所以一般要慎用自旋锁
使用方法
自旋锁用结构体KSPIN_LOCK来表示
使用自旋锁的时候需要对其进行初始化,初始化可以使用函数KeInitializeSpinLock,一般在驱动加载函数DriverEntry或者AddDevice函数中初始化自旋锁。
VOID
KeInitializeSpinLock(
IN PKSPIN_LOCK SpinLock
);
申请自旋锁可以使用函数KeAcquireSpinLock。
VOID
KeAcquireSpinLock(
IN PKSPIN_LOCK SpinLock,
OUT PKIRQL OldIrql //自旋锁以前所处的IRQL
);
释放自旋锁可以使用函数KeReleaseSpinLock
内核模式下线程的创建
在内核模式中线程使用PsCreateSystemThread;该函数的原型如下:
NTSTATUS
PsCreateSystemThread(
OUT PHANDLE ThreadHandle, //线程的句柄指针,这个参数作为一个输出参数
IN ULONG DesiredAccess,//新线程的权限,在驱动中这个值一般给0
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,//线程的属性,一般给NULL
IN HANDLE ProcessHandle OPTIONAL,//该线程所属的进程句柄,如果给NULL表示创建一个系统进程的线程
OUT PCLIENT_ID ClientId OPTIONAL,//指向客户结构的一个指针,在驱动中这个值一般给NULL
IN PKSTART_ROUTINE StartRoutine,//新线程的函数地址
IN PVOID StartContext//线程函数的参数
);
第4个参数表示创建线程的类型,如果给NULL则表示创建一个系统线程,否则表示将创建一个用户线程,DDK提供了一个宏NtCurrentThread()来获取当前进程的句柄,这个当前进程表示的是像驱动发送IRP请求的进程的句柄。
获取进程名
在XP中EPROCESS结构的0X174偏移位置记录着线程名,我们可以使用IoGetCurrentProcess()函数来获取当前进程的EPROCESS结构,这样我们 可以利用这样的代码来获取进程名:
PEPROCESS pEprocess = IoGetCurrentProcess();
ASSERT(NULL != pEprocess);
DbgPrint("the process name is %S
", (PTSTR)((ULONG)pEprocess + 0x174));
下面是一个使用线程的例子
VOID MyProcessThread(PVOID pContext)
{
//获取当前发送IRP请求的线程名
PEPROCESS pCurrProcess = IoGetCurrentProcess();
PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174);
// UNREFERENCED_PARAMETER(pContext);
DbgPrint("MyProcessThread Current Process %s
", pProcessName);
PsTerminateSystemThread(0);
}
VOID SystemThread(PVOID pContext)
{
//获取系统进程名
PEPROCESS pCurrProcess = IoGetCurrentProcess();
PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174);
// UNREFERENCED_PARAMETER(pContext);
DbgPrint("MyProcessThread Current Process %s
", pProcessName);
PsTerminateSystemThread(0);
}
VOID CreateThread_Test()
{
HANDLE hSysThread = NULL;
HANDLE hMyProcThread = NULL;
NTSTATUS status;
//创建系统进程
status = PsCreateSystemThread(&hSysThread, 0, NULL, NULL, NULL, SystemThread, NULL);
//创建用户进程
status = PsCreateSystemThread(&hMyProcThread, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread, NULL);
}
内核模式下的同步对象
内核模式下的同步对象与应用层的大致相同,所以理解了应用的线程同步对象,那么内核层的也很好理解
内核模式下的等待函数
内核模式下的等待函数是KeWaitForSingleObject 和 KeWaitForMultipleObjects,一个是用来等待单个事件,一个是用来等待多个事件。
NTSTATUS
KeWaitForSingleObject(
IN PVOID Object, /第一个参数是一个指向同步对象的指针
IN KWAIT_REASON WaitReason,//第二个参数是等待原因,在驱动中这个值应该被设置为Executive
IN KPROCESSOR_MODE WaitMode,//等待模式,处在低优先级的驱动应该将这个值设置为KernelMode
IN BOOLEAN Alertable,//是否是警惕的
IN PLARGE_INTEGER Timeout OPTIONAL//等待时间,如果是正数则表示从1601年1月1日到现在的时间如果是负数则表示从现在算起的时间,单位是100ns
);
函数如果是等待到了对应的事件则返回STATUS_SUCCESS如果是由于等待时间到了,则返回STATUS_TIMEOUT
内核模式下的事件对象
在内核中用KEVENT来表示一个事件对象,在使用事件对象时需要对其进行初始化,使用函数KeInitializeEvent
VOID
KeInitializeEvent(
IN PRKEVENT Event, //事件对象的指针
IN EVENT_TYPE Type, //事件类型,一般分为两种:NotificationEvent 通知事件和同步事件SynchronizationEvent
IN BOOLEAN State//是否是激发状态
);
所谓的激发状态就是有信号状态,没有线程拥有这个事件。在这个状态下其他线程中的等待函数可以等到这个事件
这两种类型的事件对象的区别在于如果是通知事件需要程序员手动的更改事件的状态,如果是同步事件,在等待函数等到这个事件对象后会自动将这个对象设置为无信号状态
可以使用函数KeSetEvent设置事件为有信号,这样其他线程的等待函数就可以等到这个事件
LONG
KeSetEvent(
IN PRKEVENT Event, //事件对象的指针
IN KPRIORITY Increment,//被唤起的线程将以何种优先级执行,这个参数与IoCompleteRequest的第二个参数含义相同
IN BOOLEAN Wait //一般给FALSE
);
下面是这个它的使用例子
VOID Event_Test()
{
KEVENT keEvent;
HANDLE hThread;
//初始化事件对象,并设置为无信号
KeInitializeEvent(&keEvent, NotificationEvent, FALSE);
//创建新线程,将事件对象传入线程函数中,新线程将会设置事件对象为有状态
PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, EventThread, &keEvent);
if(NULL == hThread)
{
DbgPrint("Create Event Thread Error!
");
return;
}
KeWaitForSingleObject(&keEvent, Executive, KernelMode, FALSE, NULL);
}
VOID EventThread(PVOID pContext)
{
PKEVENT pEvent = (PKEVENT)pContext;
DbgPrint("This is Event Thread
");
KeSetEvent(pEvent, IO_NO_INCREMENT, FALSE);
PsTerminateSystemThread(0);
}
驱动程序与应用程序交互事件对象
本质上用户层和内核层的事件对象是同一个东西,在用户层用句柄代替,看不到它的具体结构,在内核层是一个KEVENT,能知道它的具体数据成员。
我们可以先在应用层创建一个事件对象的句柄,然后通过DeviceIoControl传到应用层,然后利用函数ObReferenceObjectByHandle将这个句柄转化为对应的事件对象,在利用这个函数转化成功后会将事件对象的计数 + 1所以在使用完后应该调用函数ObDereferenceObject使计数减1
NTSTATUS
ObReferenceObjectByHandle(
IN HANDLE Handle, //用户层传下来的内核对象句柄
IN ACCESS_MASK DesiredAccess, //访问权限对于同步事件一般给EVENT_MODIFY_STATE
IN POBJECT_TYPE ObjectType OPTIONAL,//转化何种类型的内核结构
IN KPROCESSOR_MODE AccessMode,//模式,一般有KernelMode和UserMode
OUT PVOID *Object,//对应结构的指针
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL//这个参数在内核模式下为NULL
);
第三个参数根据转化的内核结构的不同可以有下面的结构。
参数值 | 对应的结构 |
---|---|
*IoFileObjectType | PFILE_OBJECT |
*ExEventObjectType | PKEVENT |
*PsProcessType | PEPROCESS或者PKPROCESS |
*PsThreadType | PETHREAD或者PKTHREAD |
下面是内核层的例子
else if(IOCTL_TRANS_EVENT == pIrps->Parameters.DeviceIoControl.IoControlCode)
{
//接收从应用层下发的下来的事件句柄
hEvent = *(PHANDLE)(Irp->AssociatedIrp.SystemBuffer);
if(NULL == hEvent)
{
DbgPrint("Invalied Handle
");
goto __RET;
}
status = ObReferenceObjectByHandle(hEvent, EVENT_MODIFY_STATE, *ExEventObjectType, KernelMode, &pkEvent, NULL);
if(!NT_SUCCESS(status))
{
//失败
DbgPrint("Translate Event Error
");
goto __RET;
}
//将事件设置为有信号
KeSetEvent(pkEvent, IO_NO_INCREMENT, FALSE);
//引用计数 -1
ObDereferenceObject(pkEvent);
}
驱动程序与驱动程序交互事件对象
在内核驱动中可以通过给某个内核对象创建一个命名对象,然后在另一个驱动中通过名字来获取这个对象,然后操作它来实现两个驱动之间的内核对象的通讯,针对事件对象来说,要实现两个驱动交互事件对象,通过这样几步:
1. 在驱动A中调用IoCreateNotificationEvent或者IoCreateSynchronizationEvent来创建一个通知事件对象或者同步事件对象
2. 在驱动B中调用 IoCreateNotificationEvent或者IoCreateSynchronizationEvent获取已经有名字的内核对象的句柄
3. 在驱动B中调用ObReferenceObjectByHandle根据上面两个函数返回的句柄来获取A中的事件对象,并操作它
4. 操作完成后调用ObDereferenceObject解引用
PKEVENT
IoCreateNotificationEvent(
IN PUNICODE_STRING EventName,
OUT PHANDLE EventHandle
);
如果指定名称的事件存在那么将会通过EventHandle来返回这个事件对象的句柄,如果不存在则会创建一个事件并通过返回值直接返回这个事件对象的结构指针,需要注意的是这个名字必须以L”BaseNamedObjects” 开头另外不能在DriverEntry中等待过长时间,否则会造成系统蓝屏
内核模式下的信号量
在操作系统相关的书籍中但凡说到线程的同步问题就会涉及到信号量,当多个线程共享一个公共资源时在某一时刻只能有一个线程在运行,这个时候一般用事件对象控制,而当多个线程共享多个公共资源时,可以有多个线程同时在运行,这个时候就可以用信号量,可以把信号量想象成一个盒子,里面有多盏灯,当只要有一盏灯是亮的,就有线程可以执行,每当有一个线程在访问共享资源时,亮灯的数量-1,当线程不再访问共享资源时,亮灯的数目 +1而当灯全部熄灭时就不再允许线程访问。当盒子中只有一盏灯的时候,就相当于一个互斥体
信号量的初始化函数为KeInitializeSemaphore
VOID
KeInitializeSemaphore(
IN PRKSEMAPHORE Semaphore,//将要被初始化的信号量的指针
IN LONG Count,//当前信号量中有多少个灯亮
IN LONG Limit//总共有多少灯
);
释放信号量会增加信号灯计数。对应的函数是KeReleaseSemaphore。可以利用这个函数指定增量值,获得的信号灯可以使用Wait函数等待如果获得就熄灭一盏灯,否则就陷入等待。。利用函数KeReadStateSemaphore可以得到当前有多少盏灯是亮的
下面是使用的例子
VOID Semaphore_Test()
{
KSEMAPHORE keSemaphore;
HANDLE hThread;
NTSTATUS status = STATUS_SUCCESS;
ULONG uCount = 0;//当前有多少盏灯亮着
//初始化,使其里面有两盏灯,两盏灯全亮
KeInitializeSemaphore(&keSemaphore, 2, 2);
if(!NT_SUCCESS(status))
{
DbgPrint("Initialize Semaphore Error
");
return;
}
//当前有多少盏灯亮着
uCount = KeReadStateSemaphore(&keSemaphore);
DbgPrint("the count = %ul", uCount);
//函数会成功返回并熄灭一盏灯
KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0);
uCount = KeReadStateSemaphore(&keSemaphore);
DbgPrint("the count = %ul", uCount);
//函数会成功返回并熄灭一盏灯
KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0);
uCount = KeReadStateSemaphore(&keSemaphore);
DbgPrint("the count = %ul", uCount);
//创建新线程
PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, SemaphoreThread, &keSemaphore);
//这时没有灯亮,函数会陷入等待状态
KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0);
}
VOID SemaphoreThread(PVOID pContext)
{
//线程函数
PKSEMAPHORE pkeSemaphore = (PKSEMAPHORE)pContext;
DbgPrint("Entry My Thread
");
//点亮其中的一盏灯
KeReleaseSemaphore(pkeSemaphore, IO_NO_INCREMENT, 1, FALSE);
//结束线程
PsTerminateSystemThread(0);
}
内核模式下的互斥体
互斥体在内核结构中的定义为KMUTEX使用前需要使用函数KeInitializeMutex进行初始化
VOID
KeInitializeMutex(
IN PRKMUTEX Mutex,
IN ULONG Level//系统保留参数一般给0
);
初始化之后就可以使用Wait系列的函数进行等待,一旦函数返回,那么该线程就拥有了该互斥体,线程可以调用函数KeReleaseMutex来主动释放互斥体
LONG
KeReleaseMutex(
IN PRKMUTEX Mutex,
IN BOOLEAN Wait
);
与同步对象相比,互斥体可以在某个线程中递归获取,这个时候每当获取一次,那么它被引用的次数也将加1,在释放时,被引用多少次就应该释放多少次,只有当计数为0时才能被其他线程获取
互锁操作进行同步
互锁操作就是定义了一个原子操作,当原子操作没有完成时,线程是不允许切换的,系统会保证原子操作要么都完成了,要么都没有完成。
在Windows中为一些常用的操作定义了一组互锁操作函数