本节书摘来自异步社区出版社《C++多线程编程实战》一书中的第2章,第2.10节,作者: 【黑山共和国】Milos Ljumovic(米洛斯 留莫维奇),更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.10 在内核实现线程
整个内核就是一个进程,许多系统(内核)线程在其上下文中运行。内核有一个线程表,跟踪该系统中所有的线程。
内核维护这个传统的进程表以跟踪进程。那些可以阻塞线程的函数调用可作为系统调用执行,这比执行系统过程的代价更高。当线程被阻塞时,内核必须运行其他线程。当线程被毁坏时,则被标记为不可运行。但是,它的内核数据结构不会受到影响。然后在创建新的线程时,旧的线程将被再次激活,回收资源以备后用。当然,也可以回收用户级线程,但如果线程管理开销非常小,就没必要这样做。
准备就绪
下面的示例要求安装WinDDK(Driver Development Kit,驱动程序开发工具包),详情请参阅附录。成功安装WinDDK后,运行Visual Studio。
操作步骤
1. 创建一个新的Win32应用程序项目,并命名为KernelThread
。
2. 打开【解决方案资源管理器】,并添加一个新的头文件,命名为ThreadApp.h
。打开ThreadApp.h
,并输入下面的代码:
#include <windows.h>
#include <tchar.h>
#define DRIVER_NAME TEXT( "TestDriver.sys" )
#define DRIVER_SERVICE_NAME TEXT( "TestDriver" )
#define Message(n) MessageBox(0, TEXT(n), \
TEXT("Test Driver Info"), 0)
BOOL StartDriver(LPTSTR szCurrentDriver);
BOOL StopDriver(void);```
3.现在,打开【解决方案资源管理器】,并添加一个新的源文件,命名为`ThreadApp.cpp`。打开`ThreadApp.cpp`,并输入下面的内容:
include "ThreadApp.h"
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR szCommandLine, int iCmdShow)
{
StartDriver(DRIVER_NAME);
ShellAbout(0, DRIVER_SERVICE_NAME, TEXT(""), NULL);
StopDriver();
return 0;
}
BOOL StartDriver(LPTSTR szCurrentDriver)
{
HANDLE hFile = 0;
DWORD dwReturn = 0;
SC_HANDLE hSCManager = { 0 };
SC_HANDLE hService = { 0 };
SERVICE_STATUS ServiceStatus = { 0 };
TCHAR szDriverPath[MAX_PATH] = { 0 };
GetSystemDirectory(szDriverPath, MAX_PATH);
TCHAR szDriver[MAX_PATH + 1];
ifdef _UNICODE
wsprintf(szDriver, L"\drivers\%ws", DRIVER_NAME);
else
sprintf(szDriver, "\drivers\%s", DRIVER_NAME);
endif
_tcscat_s(szDriverPath, (_tcslen(szDriver) + 1) * sizeof(TCHAR),
szDriver);
BOOL bSuccess = CopyFile(szCurrentDriver, szDriverPath, FALSE);
if (bSuccess == FALSE)
{
Message("copy driver failed");
return bSuccess;
}
hSCManager = OpenSCManager(NULL, NULL,
SC_MANAGER_CREATE_SERVICE);
if (hSCManager == 0)
{
Message("open sc manager failed!");
return FALSE;
}
hService = CreateService(hSCManager, DRIVER_SERVICE_NAME,
DRIVER_SERVICE_NAME, SERVICE_START | DELETE | SERVICE_STOP,
SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE,
szDriverPath, NULL, NULL, NULL, NULL, NULL);
if (hService == 0)
{
hService = OpenService(hSCManager, DRIVER_SERVICE_NAME,
SERVICE_START | DELETE | SERVICE_STOP);
Message("create service failed!");
}
if (hService == 0)
{
Message("open service failed!");
return FALSE;
}
BOOL startSuccess = StartService(hService, 0, NULL);
if (startSuccess == FALSE)
{
Message("start service failed!");
return startSuccess;
}
CloseHandle(hFile);
return TRUE;
}
BOOL StopDriver(void)
{
SC_HANDLE hSCManager = { 0 };
SC_HANDLE hService = { 0 };
SERVICE_STATUS ServiceStatus = { 0 };
TCHAR szDriverPath[MAX_PATH] = { 0 };
GetSystemDirectory(szDriverPath, MAX_PATH);
TCHAR szDriver[MAX_PATH + 1];
ifdef _UNICODE
wsprintf(szDriver, L"\drivers\%ws", DRIVER_NAME);
else
sprintf(szDriver, "\drivers\%s", DRIVER_NAME);
endif
_tcscat_s(szDriverPath, (_tcslen(szDriver) + 1) * sizeof(TCHAR),
szDriver);
hSCManager = OpenSCManager(NULL, NULL,
SC_MANAGER_CREATE_SERVICE);
if (hSCManager == 0)
{
return FALSE;
}
hService = OpenService(hSCManager, DRIVER_SERVICE_NAME,
SERVICE_START | DELETE | SERVICE_STOP);
if (hService)
{
ControlService(hService, SERVICE_CONTROL_STOP,
&ServiceStatus);
DeleteService(hService);
CloseServiceHandle(hService);
BOOL ifSuccess = DeleteFile(szDriverPath);
return TRUE;
}
return FALSE;
}`
4.现在,打开【解决方案资源管理器】,创建一个新的空Win32控制台项目,并命名为DriverApp
。
5.添加一个新的头文件,命名为DriverApp.h
,并输入以下代码:
#include <ntddk.h>
DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD OnUnload;```
6.打开【解决方案资源管理器】,在`DriverApp`项目下,添加一个新的源文件,命名为`DriverApp.cpp`。打开`DriverApp.cpp`,并输入以下代码:
include "DriverApp.h"
VOID ThreadStart(PVOID lpStartContext)
{
PKEVENT pEvent = (PKEVENT)lpStartContext;
DbgPrint("Hello! I am kernel thread. My ID is %u. Regards..",
(ULONG)PsGetCurrentThreadId());
KeSetEvent(pEvent, 0, 0);
PsTerminateSystemThread(STATUS_SUCCESS);
}
NTSTATUS DriverEntry(PDRIVER_OBJECT theDriverObject, PUNICODE_STRING
theRegistryPath)
{
HANDLE hThread = NULL;
NTSTATUS ntStatus = 0;
OBJECT_ATTRIBUTES ThreadAttributes;
KEVENT kEvent = { 0 };
PETHREAD pThread = 0;
theDriverObject->DriverUnload = OnUnload;
DbgPrint("Entering KERNEL mode..");
InitializeObjectAttributes(&ThreadAttributes, NULL, OBJ_KERNEL_HANDLE,
NULL, NULL);
__try
{
KeInitializeEvent(&kEvent, SynchronizationEvent, 0);
ntStatus = PsCreateSystemThread(&hThread, GENERIC_ALL,
&ThreadAttributes, NULL, NULL, (PKSTART_ROUTINE)&ThreadStart,
&kEvent);
if (NT_SUCCESS(ntStatus))
{
KeWaitForSingleObject(&kEvent, Executive, KernelMode, FALSE,
NULL);
ZwClose(hThread);
}
else
{
DbgPrint("Could not create system thread!");
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("Error while creating system thread!");
}
return STATUS_SUCCESS;
}
VOID OnUnload(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Leaving KERNEL mode..");
}`
示例分析
首先,创建了一个Win32应用程序,仅作为演示用。我们只想把驱动程序加载至内核中,还没有UI,甚至没有消息循环。然后,程序将显示ShellAbout
对话框,这仅仅是为了让用户有时间阅读DbgView
输出(欲详细了解DbgView
,请参阅附录)。在用户关闭ShellAbout
对话框后,程序将卸载驱动程序,应用程序结束。
我们创建的Win32应用程序只能加载和卸载驱动程序,所以不做进一步解释了。现在,来看DriverApp
项目。为编译驱动程序设置好Visual Studio
后(请查阅附录了解详细的Visual Studio
的编译设置),我们声明了下面两个主例程,每个驱动程序都必须在DriverApp.h
头文件中:
DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD OnUnload;```
这两个例程是驱动程序入口点和驱动程序的卸载例程。我们将使用驱动程序入口点初始化一个线程对象,并启动内核线程。新创建的线程将只写入一条显示它唯一标识符的消息,然后立刻返回。要说明的是,深入探讨和开发内核超出了本书讨论的范围,我们在这里浅尝辄止。因为驱动程序被编译为/TC2,我们必须确保在执行第一条命令之前已经声明了所有变量。如下代码所示:
HANDLE hThread = NULL;
NTSTATUS ntStatus = 0;
OBJECT_ATTRIBUTES ThreadAttributes;
KEVENT kEvent = { 0 };
PETHREAD pThread = 0;`
然后,还必须设置卸载例程:
`
theDriverObject->DriverUnload = OnUnload;`
另外,在创建内核线程之前,要用InitializeObjectAttributes
例程初始化ThreadAttribute
对象:
`
InitializeObjectAttributes(&ThreadAttributes, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);`
内核开发必须执行得非常谨慎,哪怕是一丁点儿错误都会导致蓝屏死机BSOD
)或机器崩溃。为此,我们使用_ try - _except块,它与我们熟悉的try-catch
块稍有不同。
在内核中创建句柄和在用户空间中创建句柄不同。KeWaitForSingleObject
例程无法使用PsCreateSystemThread
返回的句柄。我们要在KeWaitForSingleObject
返回时在线程中初始化一个触发的事件(事件将在第3章中详细介绍)。PsCreateSystemThread
例程必须与ZwClose
例程成对调用。调用ZwClose
关闭内核句柄和防止内存泄漏。
最后,我们要实现PKSTART_ROUTINE
或线程的开始地址,线程的指令从这里开始执行。下面是一个示例:
`
VOID (__stdcall* KSTART_ROUTINE)( PVOID StartContext );`
我们已经通过PsCreateSystemThread
的最后一个参数传递了一个指向KEVENT的指针。现在,使用DbgPrint
把相应的线程ID写入消息供用户阅读。然后,设置一个事件,以便相应的KeWaitForSingleObject
调用可以返回并安全地退出驱动程序。确保PsTerminateSystemThread
没有返回。内核将在卸载驱动程序时清理线程对象。
更多讨论
虽然内核线程能解决一些问题,但也不是万能的。例如,当多线程进程创建其他多线程进程时会发生什么情况?应该创建与旧进程的线程一样多的新进程,还是创建只有一个线程的新进程?在多数情况下,这取决于你下一步打算用进程做什么。如果要启动一个新程序,也许应该创建只有一个线程的进程。但如果是继续执行,也许应该创建具有同样数量线程的进程才对。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。