Windows服务与异步I/O技术详解
1. Windows服务基础
1.1 服务控制与状态
服务可接受并处理特定的控制代码,这些代码通过服务控制处理程序进行处理。例如,在后续示例中会用到一些特定的值,这些值需通过按位“或”操作进行组合。服务在注册处理程序并设置状态后,可进行自身初始化并再次设置状态。如在某些转换场景中,当套接字初始化完成且服务器准备好接受客户端时,需将状态设置为相应值。
1.2 服务控制处理程序
服务控制处理程序是一个回调函数,其形式如下:该处理程序由服务控制管理器(SCM)在与主程序相同的线程中调用,通常以
switch
语句编写。处理程序的参数
control
表示SCM发送的实际控制信号,共有14种可能的值,示例中关注的5种控制值如下:
- 部分控制值在表13 - 3中提及;
- 用户定义的值在特定范围内也是允许的,但本示例未使用;
-
dwEventType
通常为0,非零值用于设备管理,超出了本文的范围;
-
lpEventData
提供某些事件所需的额外数据;
-
lpContext
是注册处理程序时传递的用户定义数据。
1.3 事件日志记录
由于服务通常在无用户交互的情况下运行,直接显示状态消息并不合适。早期的一些服务会创建控制台、消息框或窗口进行用户交互,但这些技术在Vista和NT6之后已不再可用。解决方案是将事件记录到日志文件或使用Windows事件日志功能,这些事件可在控制面板的管理工具中的事件查看器中查看。即将介绍的示例(Program 13 - 2)会将重要的服务事件和错误记录到日志文件,同时有一个练习要求修改程序以使用Windows事件。
1.4 服务“包装器”示例
Program 13 - 2可将任意
server
转换为服务。转换过程依赖于前面描述的任务,现有服务器代码(即旧的
server
函数)从
ServiceMain
函数作为线程或进程调用,因此该代码本质上是现有服务器程序的包装器。命令行选项
-standalone
指定程序作为独立程序运行,可能用于调试。若没有该选项,则会调用
StartServiceCtrlDispatcher
。此外,还添加了一个日志文件,为简单起见,文件名是硬编码的,服务会将重要事件记录到该文件。以下是相关代码示例:
DispTable [] = {
"SockSrv", serverSK,
"NPSrv", serverNP,
NULL, NULL
};
main ()
{
StartServiceCtrlDispatcher(DispTable);
}
serverSK (argc, argv [])
{
RegisterServiceCtrlHandler(HandlerSK);
SetServiceStatus ();
...
Service-Specific code
...
}
HandlerSK (control)
{
switch (control) ...
}
1.5 简单服务的运行
Run 13 - 2a展示了
sc
命令工具创建、启动、查询、停止和删除服务的过程,只有管理员才能执行这些步骤。Run 13 - 2b展示了日志文件的内容。
1.6 管理Windows服务
编写好服务后,需将其置于SCM的控制之下,SCM可启动、停止和控制服务。除了使用
sc
和服务管理工具,还可通过编程方式管理服务。以下是管理服务的主要步骤:
1.
打开SCM
:需要一个以“管理员”身份运行的单独进程来创建服务,第一步是打开SCM,获取一个句柄,以便后续创建服务。参数说明如下:
-
lpMachineName
:若SCM在本地计算机上,则为
NULL
,也可访问网络机器上的SCM;
-
lpDatabaseName
:通常为
NULL
;
-
dwDesiredAccess
:通常为
SC_MANAGER_ALL_ACCESS
,也可指定更有限的访问权限。
2.
创建和删除服务
:调用
CreateService
注册服务,新服务会在注册表中相应位置进行登记,但不要直接操作注册表来绕过
CreateService
。
CreateService
的参数说明如下:
-
SC_HANDLE schSCManager
:从
OpenSCManager
获取的句柄;
-
LPCTSTR lpServiceName
:用于后续引用服务的名称,是
StartServiceCtrlDispatcher
调用的调度表中指定的逻辑服务名称之一;
-
LPCTSTR lpDisplayName
:在服务管理工具中向用户显示的服务名称;
-
DWORD dwDesiredAccess
:可以是
SERVICE_ALL_ACCESS
或其他值的组合;
-
DWORD dwServiceType
:值如Table 13 - 1所示;
-
DWORD dwStartType
:指定服务的启动方式,示例中使用
SERVICE_DEMAND_START
,其他值如
SERVICE_BOOT_START
、
SERVICE_SYSTEM_START
和
SERVICE_AUTO_START
有不同的启动时机;
-
LPCTSTR lpBinaryPathName
:服务的可执行文件的完整路径,
.exe
扩展名是必需的,若路径包含空格,需使用引号;
- 其他参数指定账户名称和密码、组合服务的组以及多个相互依赖服务的依赖关系。
可以使用
ChangeServiceConfig
和
ChangeServiceConfig2
更改现有服务的配置参数,还可使用
OpenService
获取命名服务的句柄,使用
DeleteService
从SCM中注销服务,使用
CloseServiceHandle
关闭句柄。
3.
启动服务
:创建服务后,服务并未运行,需通过指定从
OpenService
获取的句柄以及服务主函数期望的命令行参数来启动
StartService
函数。
4.
控制服务
:通过告诉SCM使用指定的控制调用服务的控制处理程序来控制服务。示例中感兴趣的
dwControl
值如下:
- 部分特定值;
- 用户指定的特定范围内的值;
- 其他命名值用于通知服务启动值已更改或绑定相关的更改;
-
SERVICE_CONTROL_INTERROGATE
:告诉服务使用
SetServiceStatus
报告其状态,但由于SCM会定期接收更新,其用途有限;
-
lpServiceStatus
:指向一个
SERVICE_STATUS
结构,用于接收当前状态。
5.
查询服务状态
:可使用
QueryServiceStatus
在
SERVICE_STATUS
结构中获取服务的当前状态。调用
QueryServiceStatus
从SCM获取当前状态信息,与使用
SERVICE_CONTROL_INTERROGATE
控制代码调用
ControlService
有所不同,前者是让服务更新SCM,而不是应用程序。
1.7 服务操作和管理总结
图13 - 1展示了SCM与服务以及服务控制程序之间的关系。服务必须向SCM注册,所有对服务的命令都通过SCM传递。以下是相关代码示例:
Service Control Manager (SCM)
YourService.exe
DispTable [] = {
"SockSrv", serverSK,
"NPSrv", serverNP,
NULL, NULL
};
main ()
{
StartServiceCtrlDispatcher(DispTable);
}
serverSK (argc, argv [])
{
RegisterServiceCtrlHandler(HandlerSK);
SetServiceStatus ();
...
Service-Specific code
...
}
HandlerSK (control)
{
switch (control) ...
}
...
OpenSCManager ();
CreateService(YourService.exe);
StartService (argc, argv[]);
ControlService ();
...
ServiceShell
1.8 服务控制外壳示例
可以通过管理工具中的服务图标、Windows命令
sc
或在应用程序中控制服务。示例
ServiceControl
(Program 13 - 3)是对之前示例(Program 6 - 3)的修改,旨在展示如何从程序中控制服务,但它不能替代
sc
或服务管理工具。Run 13 - 3展示了
ServiceControl
的操作。
1.9 与服务共享内核对象
服务和应用程序可能会共享内核对象,例如服务可能使用命名互斥体保护用于与应用程序通信的共享内存区域,文件映射也是共享内核对象。由于应用程序和服务在不同的安全上下文中运行,即使不需要保护,使用
NULL
安全属性指针创建和/或打开共享内核对象也是不够的,至少需要一个非自由裁量的访问控制列表,即应用程序和服务需要使用非
NULL
安全属性结构。此外,若服务在系统账户下运行,可能会在访问其他机器上的资源(如共享文件)时遇到困难。
1.10 服务调试注意事项
服务应持续运行,因此必须尽可能可靠且无缺陷。虽然可以将服务附加到调试器并使用事件日志跟踪服务操作,但这些技术在服务部署后最为合适。在初始开发和调试期间,利用Program 13 - 2中介绍的服务包装器通常更容易,该包装器允许根据命令行选项作为服务或独立应用程序运行。以下是调试服务的步骤:
1. 将“预服务”版本开发为独立程序,例如某些程序就是这样开发的;
2. 使用事件日志或日志文件对程序进行检测;
3. 当程序被认为准备好作为服务部署时,不使用
-standalone
命令行选项运行,使其作为服务运行;
4. 对服务进行额外测试至关重要,以检测额外的逻辑错误和安全问题,因为服务可能在系统账户下运行,不一定能访问用户对象,独立版本可能无法检测到此类问题;
5. 可使用日志文件或事件日志中的信息进行正常事件和小型维护调试,状态信息也有助于确定服务器的健康状况和缺陷症状;
6. 若需要进行大量维护,可以使用
-standalone
选项作为正常应用程序进行调试。
2. Windows异步I/O概述
2.1 为什么需要异步I/O
输入和输出操作相比其他处理操作本质上较慢,原因如下:
- 随机访问设备(如磁盘)的磁道和扇区寻道时间会导致延迟;
- 物理设备与系统内存之间相对较慢的数据传输速率会导致延迟;
- 使用文件服务器、存储区域网络等进行网络数据传输时会出现延迟。
之前示例中的所有I/O操作都是线程同步的,即整个线程会等待I/O操作完成。本文将介绍线程如何在不等待操作完成的情况下继续执行,即线程可以执行异步I/O,同时还会介绍可等待计时器,以及在理解标准异步I/O后如何使用I/O完成端口,这对于构建能够支持大量客户端而无需为每个客户端创建线程的可扩展服务器非常有用。
2.2 实现异步I/O的三种技术
在Windows中,有三种实现异步I/O的技术,它们在启动I/O操作的方法和确定操作完成的方法上有所不同:
1.
多线程I/O
:进程或一组进程中的每个线程执行正常的同步I/O,但其他线程可以继续执行。例如,第11章中的线程服务器在命名管道上使用多线程I/O,Program 7 - 1管理对多个文件的并发I/O,这些现有程序通过多线程I/O实现了一种异步I/O形式。
2.
重叠I/O(带等待)
:线程在发出读取、写入或其他I/O操作后继续执行,当线程在继续执行之前需要I/O结果时,它会等待文件句柄或
OVERLAPPED
结构中指定的事件。后续示例将使用此技术实现文件转换(简化的凯撒密码,首次在第2章使用),以说明顺序文件处理,该示例是Program 2 - 3的修改版本。
3.
带完成例程的重叠I/O(或“扩展I/O”或“可警告I/O”)
:当I/O操作完成时,系统会在线程中调用指定的完成例程回调函数。“扩展I/O”这个术语容易记住,因为它需要扩展函数(如
ReadFileEx
和
WriteFileEx
)。
“重叠I/O”和“扩展I/O”用于后两种技术,它们是重叠I/O的两种形式,区别在于Windows指示操作完成的方式。需要注意的是,重叠I/O和扩展I/O可能比较复杂,在Windows XP上很少能带来显著的性能提升,线程通常可以克服这些问题,因此一些读者可能希望跳过相关部分,直接阅读可等待计时器和I/O完成端口的部分,但异步I/O概念在新旧技术中都有应用,学习这些技术是有价值的。此外,异步过程调用(APC)操作与扩展I/O非常相似,两种重叠I/O技术的一个重要优点是可以取消未完成的I/O操作,便于清理。NT6(包括Windows 7)在性能方面是个例外,NT6的扩展和重叠I/O与简单的顺序I/O相比提供了良好的性能。
2.3 异步I/O技术对比
| 技术名称 | 启动I/O操作方法 | 确定操作完成方法 | 适用场景 |
|---|---|---|---|
| 多线程I/O | 每个线程执行正常同步I/O | 其他线程继续执行 | 适用于已有多线程架构,可通过多线程实现并发I/O |
| 重叠I/O(带等待) | 线程发出I/O操作后继续执行 | 等待文件句柄或指定事件 | 适用于需要顺序处理文件,且可在需要结果时等待的场景 |
| 带完成例程的重叠I/O | 发出I/O操作 | 系统调用完成例程回调函数 | 适用于需要在I/O完成时自动执行特定操作的场景 |
2.4 异步I/O操作流程
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(选择异步I/O技术):::process
B --> C{是否为多线程I/O}:::decision
C -->|是| D(每个线程执行同步I/O):::process
C -->|否| E{是否为重叠I/O(带等待)}:::decision
E -->|是| F(线程发出I/O操作后继续执行):::process
E -->|否| G(发出I/O操作,设置完成例程):::process
F --> H(需要结果时等待文件句柄或事件):::process
G --> I(系统调用完成例程):::process
D --> J([结束]):::startend
H --> J
I --> J
综上所述,Windows服务和异步I/O技术为开发者提供了强大的工具,用于构建高效、可靠的应用程序。通过合理使用这些技术,可以提高系统的性能和可扩展性,满足不同场景下的需求。在实际应用中,需要根据具体情况选择合适的技术和方法,并注意相关的调试和安全问题。
3. 重叠I/O技术详解
3.1 重叠I/O的基本原理
重叠I/O允许线程在发起I/O操作后继续执行其他任务,而无需等待I/O操作完成。这种技术通过
OVERLAPPED
结构来跟踪I/O操作的状态。当线程发起一个I/O操作时,它会将
OVERLAPPED
结构传递给相应的I/O函数,系统会在后台处理该操作,并在操作完成时通知线程。
3.2 重叠I/O示例:文件转换
以下是一个使用重叠I/O实现文件转换(简化的凯撒密码)的示例代码:
// 示例代码框架,具体实现需根据实际情况完善
#include <windows.h>
#include <stdio.h>
#define BUFFER_SIZE 1024
// 凯撒密码转换函数
void caesarCipher(char* buffer, int length, int shift) {
for (int i = 0; i < length; i++) {
if (isalpha(buffer[i])) {
if (isupper(buffer[i])) {
buffer[i] = ((buffer[i] - 'A' + shift) % 26) + 'A';
} else {
buffer[i] = ((buffer[i] - 'a' + shift) % 26) + 'a';
}
}
}
}
int main() {
HANDLE hInFile, hOutFile;
OVERLAPPED olIn = {0}, olOut = {0};
char buffer[BUFFER_SIZE];
DWORD bytesRead, bytesWritten;
// 打开输入文件
hInFile = CreateFile("input.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (hInFile == INVALID_HANDLE_VALUE) {
printf("无法打开输入文件\n");
return 1;
}
// 打开输出文件
hOutFile = CreateFile("output.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
if (hOutFile == INVALID_HANDLE_VALUE) {
printf("无法打开输出文件\n");
CloseHandle(hInFile);
return 1;
}
// 循环读取和转换文件内容
while (ReadFile(hInFile, buffer, BUFFER_SIZE, &bytesRead, &olIn)) {
if (bytesRead == 0) {
break;
}
// 执行凯撒密码转换
caesarCipher(buffer, bytesRead, 3);
// 写入转换后的数据
if (!WriteFile(hOutFile, buffer, bytesRead, &bytesWritten, &olOut)) {
printf("写入文件出错\n");
break;
}
}
// 关闭文件句柄
CloseHandle(hInFile);
CloseHandle(hOutFile);
return 0;
}
3.3 重叠I/O的关键步骤
-
创建文件句柄
:使用
CreateFile函数以FILE_FLAG_OVERLAPPED标志打开文件,这样可以启用重叠I/O功能。 -
初始化
OVERLAPPED结构 :在发起I/O操作前,需要初始化OVERLAPPED结构,确保其成员变量(如hEvent)被正确设置。 -
发起I/O操作
:使用
ReadFile或WriteFile等函数发起I/O操作,并传递OVERLAPPED结构。 -
等待操作完成
:可以使用
WaitForSingleObject等函数等待OVERLAPPED结构中的事件对象,以确保I/O操作完成。 - 处理结果 :在I/O操作完成后,处理读取或写入的数据。
3.4 重叠I/O的优缺点
| 优点 | 缺点 |
|---|---|
| 提高线程的并发性能,允许线程在I/O操作期间执行其他任务 |
实现较为复杂,需要处理
OVERLAPPED
结构和事件等待
|
| 可以取消未完成的I/O操作,便于资源清理 | 在Windows XP上性能提升不明显 |
4. 带完成例程的重叠I/O(扩展I/O)
4.1 扩展I/O的工作机制
带完成例程的重叠I/O(扩展I/O)在I/O操作完成时,系统会自动调用指定的完成例程回调函数。这种方式使得线程可以在I/O操作完成时立即处理结果,而无需显式等待。
4.2 扩展I/O示例代码
// 示例代码框架,具体实现需根据实际情况完善
#include <windows.h>
#include <stdio.h>
#define BUFFER_SIZE 1024
// 完成例程回调函数
VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped) {
if (dwErrorCode == 0) {
printf("I/O操作完成,传输了 %d 字节\n", dwNumberOfBytesTransfered);
} else {
printf("I/O操作出错,错误代码: %d\n", dwErrorCode);
}
}
int main() {
HANDLE hFile;
OVERLAPPED ol = {0};
char buffer[BUFFER_SIZE];
DWORD bytesRead;
// 打开文件
hFile = CreateFile("test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("无法打开文件\n");
return 1;
}
// 发起异步读取操作
if (!ReadFileEx(hFile, buffer, BUFFER_SIZE, &ol, FileIOCompletionRoutine)) {
printf("发起读取操作出错\n");
CloseHandle(hFile);
return 1;
}
// 模拟其他任务
SleepEx(INFINITE, TRUE);
// 关闭文件句柄
CloseHandle(hFile);
return 0;
}
4.3 扩展I/O的关键要点
-
使用扩展函数
:如
ReadFileEx和WriteFileEx,这些函数支持完成例程回调。 -
定义完成例程
:完成例程必须符合特定的函数原型,接受
DWORD dwErrorCode、DWORD dwNumberOfBytesTransfered和LPOVERLAPPED lpOverlapped等参数。 -
启用可警告状态
:使用
SleepEx等函数使线程进入可警告状态,以便系统能够调用完成例程。
4.4 扩展I/O与重叠I/O的比较
| 比较项 | 重叠I/O(带等待) | 带完成例程的重叠I/O(扩展I/O) |
|---|---|---|
| 操作完成通知方式 | 等待文件句柄或事件 | 系统调用完成例程 |
| 代码复杂度 | 相对较低,主要处理等待逻辑 | 相对较高,需要定义完成例程 |
| 适用场景 | 需要明确等待I/O结果的场景 | 需要在I/O完成时立即处理结果的场景 |
5. 可等待计时器与I/O完成端口
5.1 可等待计时器
可等待计时器允许线程在指定的时间点或时间间隔后执行特定的操作。它可以用于定时任务、周期性任务等场景。以下是一个简单的可等待计时器示例:
// 示例代码框架,具体实现需根据实际情况完善
#include <windows.h>
#include <stdio.h>
// 计时器回调函数
VOID CALLBACK TimerCallback(PVOID lpParameter, BOOLEAN TimerOrWaitFired) {
printf("计时器触发\n");
}
int main() {
HANDLE hTimer;
LARGE_INTEGER dueTime;
// 创建计时器
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
if (hTimer == NULL) {
printf("创建计时器失败\n");
return 1;
}
// 设置计时器触发时间
dueTime.QuadPart = -10000000; // 1秒后触发
if (!SetWaitableTimer(hTimer, &dueTime, 0, TimerCallback, NULL, FALSE)) {
printf("设置计时器失败\n");
CloseHandle(hTimer);
return 1;
}
// 等待计时器触发
WaitForSingleObject(hTimer, INFINITE);
// 关闭计时器句柄
CloseHandle(hTimer);
return 0;
}
5.2 I/O完成端口
I/O完成端口是一种高效的I/O处理机制,特别适用于需要处理大量并发I/O操作的场景,如可扩展服务器。其工作原理是将多个I/O操作的完成通知集中到一个完成端口,线程可以从该端口获取完成的I/O操作并进行处理。
5.3 I/O完成端口的使用步骤
-
创建完成端口
:使用
CreateIoCompletionPort函数创建一个完成端口对象。 -
将文件句柄与完成端口关联
:使用
CreateIoCompletionPort函数将文件句柄与完成端口关联起来,以便将该句柄上的I/O操作完成通知发送到完成端口。 - 发起I/O操作 :使用重叠I/O或扩展I/O发起I/O操作。
-
从完成端口获取完成的I/O操作
:使用
GetQueuedCompletionStatus函数从完成端口获取完成的I/O操作信息,并进行相应处理。
5.4 I/O完成端口的优势
- 高效处理大量并发I/O :通过完成端口集中处理I/O完成通知,减少线程上下文切换开销。
- 可扩展性强 :能够轻松应对大量客户端的并发请求,适用于构建高性能服务器。
综上所述,Windows服务、异步I/O、可等待计时器和I/O完成端口等技术为开发者提供了丰富的工具和方法,用于构建高效、可靠的应用程序。在实际开发中,需要根据具体需求选择合适的技术,并注意相关的实现细节和性能优化。通过合理运用这些技术,可以显著提升系统的性能和可扩展性,满足不同场景下的应用需求。
超级会员免费看
2592

被折叠的 条评论
为什么被折叠?



