内核编程与应用程序编程一个很重要的不同点在于:应用编程中,多数情况下只需考虑单线程就可以;在内核编程中,绝大多数情况下所写的代码都位于多线程环境中。
获得当前“滴答数”:
获得系统日前和时间往往是为了写日志,获得启动毫秒数则很适合用来做一个随机数的种子。有时也使用时间相关的函数来寻找程序的性能瓶颈。
在Win32开发中,我们使用GetTickCount()函数来返回系统自启动之后经历的毫秒数。在驱动开发中,对应的函数是KeQueryTickCount():
VOID KeQueryTickCount(
OUT PLARGE_INTEGER TickCount
);
需要注意的是,返回到TickCount中的并不是一个简单的毫秒数,而是一个“滴答”数,这个数在不同硬件环境中是不一样的,所以,我们需要结合使用另一个函数:
ULONG KeQueryTimeIncrement();
下面的代码可以求得实际的毫秒数:
void ASCEGetTickCount(PULONG msce)
{
LARGE_INTEGER tickCount;
ULONG asceInc = KeQueryTimeIncrement();
KeQueryTickCount(&tickCount);
tickCount.QuadPart *= asceInc;
tickCount.QuadPart /= 10000;
*msce = tickCount.LowPart;
}
获得当前系统时间:
使用KeQuerySystemTime()函数得到的当前时间是格林威治时间,接着使用ExSystemTimeToLocalTime()函数转换成当地时间:
VOID KeQuerySystemTime(
OUT PLARGE_INTEGER CurrentTime
);
VOID ExSystemTimeToLocalTime(
IN PLARGE_INTEGER SystemTime,
OUT PLARGE_INTEGER LocalTime
);
由于这两个函数使用的时间是长长整型,不便于阅读,因此,需通过函数RtlTimeToTimeFields转换成TIME_FIELDS:
VOID RtlTimeToTimeFields(
IN PLARGE_INTEGER Time,
IN PTIME_FIELDS TimeFields
);
下面函数返回当前时间:
PWCHAR ASCECurTimeStr()
{
LARGE_INTEGER snow, now;
TIME_FIELDS nowFIelds;
static WCHAR timeStr[32] = {0};
//获得标准时间
KeQuerySystemTime(&snow);
//转换为当地时间
ExSystemTimeToLocalTime(&snow, &now);
//转换为人们可以理解的时间格式
RtlTimeToTimeFields(&now, &nowFields);
//打印到字符串中
RtlStringCchPrintfW(
timeStr,
32*2,
L”%4d-%2d-%2d %2d-%2d-%2d”,
nowFields.Year, nowFields.Month, nowFields.Day,
nowFields.Hour, nowFields.Minute, nowFields.Second
);
return timeStr;
}
注意:timeStr是静态变量,因此这个函数不具备多线程安全性。
使用定时器:
Windows应用程序编程中,当需要定时执行任务时,经常使用SetTimer()函数。在驱动开发中,我们可以使用KeSetTimer():
BOOLEAN KeSetTimer(
IN PKTIMER Timer, //定时器
IN LARGE_INTEGER DueTime, //延后执行的时间
IN PKDPC Dpc OPTIONAL //要执行的回调函数
);
其中定时器Timer和回调函数Dpc必须初始化,下面代码可以初始化一个Timer:
KTIMER asceTimer;
KeInitializeTimer(&asceTimer);
而初始化Dpc的函数原型如下:
VOID KeInitializeDpc(
IN PRKDPC Dpc,
IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext
);
其中,PKDEFERRED_ROUTINE这个函数指针类型指向的函数类型是:
VOID CustomDpc(
IN struct _KDPC *Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
我们只需要关注DeferredContext,这个参数是KeInitializeDpc调用时传入的参数,用来提供给CustomDpc被调用的时候,让用户传入一些参数。
注意:这是一个“延时执行”的过程,而不是一个定时执行的过程。如果想定时反复执行必须在每次CustomDpc函数被调用时,再次调用KeSetTimer,以此保证下次还可以执行。注意:CustomDpc运行在Dispatch中断级。
《补充中断级知识》:内核代码始终运行在某个“中断级”上,代码并非从头到尾连续执行,而是在某些执行过程中随时可能被打断:异常发生、中断发生、线程切换等。高中断级上运行的代码不会被低中断级上运行的代码中断,主要的中断级从高到低是:Dispatch>APC>Passive。
为了完全实现定时器的功能,我们最后自己封装一些结构:
//内部时钟结构
typedef struct ASCE_TIMER_
{
KDPC dpc;
KTIMER timer;
PKDEFERRED_ROUTINE func;
PVOID privateContext; //私有上下文
}ASCE_TIMER, *PASCE_TIMER;
//初始化这个结构
void ASCETimerInit(PASCE_TIMER timer, PKDEFERRED_ROUTINE func)
{
//请注意,我们将回调函数上下文参数设置为timer
KeInitializeDpc(&timer->dpc, ASCEOnTimer, timer);
timer->func = func;
KeInitializeTimer(&timer->timer);
}
//让这个结构中的回调函数在毫秒后开始执行
BOOLEAN ASCESetTimer(PASCE_TIMER timer, ULONG msce, PVOID context)
{
LARGE_INTEGER due;
//注意时间单位的转换,msce是毫秒
due.QuadPart = -10000*msce;
//用户私有上下文
timer->privateContext = context;
return KeSetTimer(&timer->timer, due, &timer->dpc);
}
//停止执行
VOID ASCEDestroyTimer(PASCE_TIMER timer)
{
KeCancelTimer(&timer->timer);
}
在回调函数ASCEOnTimer中要获得上下文,必须从timer->privateContext中获得,此外,在ASCEOnTimer中还需要再次调用ASCESetTimer(),保证下次依然得到执行:
VOID ASCEOnTimer(
IN struct _KDPC *Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
)
{
//这里传入的上下文是timer结构,用来下次再启动延时调用
PASCE_TIMER timer = (PASCE_TIMER)DeferredContext;
//获得用户上下文
PVOID asceContext = timer->privateContext;
//这里做OnTimer要执行的任务
…-…-
//再次调用,我们假设每秒执行以此
ASCESetTimer(timer, 1000, asceContext);
}