在 .NET 中创建系统监控服务






4.89/5 (17投票s)
演示了一个可配置的 Windows 服务,
引言
几年前,我发布了文章系统监视器的实现,它在一个 Windows 窗体中显示计算机资源使用情况,类似于 Windows 任务管理器中的性能页面。有时,您可能不想一直在 UI 中监视您的系统。您可能需要在后台记录系统数据,以监视记录,并在必要时触发一些操作。这是我们可以作为 Windows 服务实现的另一种应用程序。
Windows 服务是一个长时间运行的可执行文件,它在自己的 Windows 会话中运行。该服务可以在启动时自动启动,可以暂停、恢复和重新启动,而无需任何用户界面。Windows 服务非常适合不干扰用户在计算机上工作的长时间周期性运行任务。例如,监视本地系统资源或从远程单元收集和分析数据。
您可以将服务创建为 Microsoft Visual Studio 项目,定义代码以控制发送的命令和对命令采取的操作。然后,您可以使用服务控制管理器来启动、停止、暂停、恢复和配置您的服务。与其他应用程序不同,服务可执行文件必须先安装才能工作。您必须为服务创建一个安装组件,该组件安装并注册服务,并使用服务控制管理器创建其条目。有关详细信息,请参阅MSDN:Windows 服务应用程序。
几乎所有应用程序都要求接收一些输入来为任务准备数据和状态。应用程序通常需要显示其输出来指示结果或异常。由于 Windows 服务不是交互式的,因此在服务中弹出的对话框无法看到,并且可能会使其停止响应。通常,服务的运行状态可以记录到 Windows 事件查看器中,但此类日志非常有限,并且不容易检索。因此,问题是如何为输入数据准备 Windows 服务以及如何接收其结果输出。本文将通过演示如何实现以下功能,向您展示一个系统监控服务的示例:
- 使用
XmlSerializer
通过输入配置服务行为, - 在检查周期内模拟本地系统和远程主机上的任务,
- 将服务轮询结果记录到每周生成的日志文件中,
- 使用性能计数器和 WMI 检索 CPU、内存和磁盘使用情况,
- 处理服务的启动、停止、暂停和恢复事件。
如您所见,这是一个真实而完整的 Windows 服务,具有简化但有意义的操作。通过这个服务骨架,您可以在配置中添加您的任务设置,替换您的业务逻辑,并格式化您预期或警报消息的输出。它应该更容易增强和扩展以供您使用。
使用系统监控服务
让我们通过几个步骤来演示示例。首先,您可以下载并运行上面演示安装链接中提供的 SysMonServiceSetup.msi。安装后,您可以在默认文件夹中找到复制的服务可执行文件,即 C:\Program Files (x86)\Dings Code Office\System Monitor Service

此时,您只能看到 SysMonService.exe 和 SysMonService.InstallState,第二个是在安装时生成的。XML 和日志文件将在您启动 SysMonService
后生成。现在打开“管理工具”中的“服务”小程序,您可以看到创建的名为“系统监视器演示”的服务条目。然后单击“启动”链接以启动 SysMonService

现在,当您回到系统监控服务文件夹时,您将找到配置文件 SysMonService.xml 和日志文件 SysMonService1Jan2012.log。让我们阅读 SysMonService.xml,它包含您第一次启动服务时生成的默认输入数据

第一部分是主机数组。为了模拟网络通信,我只是获取主机名并收集每个主机的 IP 地址。第二部分用于检查本地计算机的 CPU、内存和磁盘空间使用情况。您可以打开或关闭单个设备并设置其百分比阈值。第三部分是毫秒级的时间控制,用于设置检查周期的间隔、服务启动延迟和停止超时。日志文件 SysMonService1Jan2012.log 包含与上述任务对应的输出。以下是一个周期摘录:
====================================================
1/2/2012 3:51:26 PM, SysMonService Thread Started
Info: Service Configuration created
Thread Proc called
1/2/2012 3:51:31 PM, Timer Proc Started ... ...
------ Check Remote Hosts ------
Check www.google.com, Host name:www.l.google.com
IP: (74.125.224.212) (74.125.224.209) (74.125.224.211) (74.125.224.210) (74.125.224.208)
Check www.microsoft.com, Host name:lb1.www.ms.akadns.net
IP: (207.46.19.254)
Check www.cnn.com, Host name:www.cnn.com
IP: (157.166.255.19) (157.166.226.25) (157.166.226.26) (157.166.255.18)
----- Check Local Computer -----
CPU Used 3.00%
Physical Memory 40.43% (2.35 GB/5.80 GB) Over Threshold(30)
Virtual Memory 42.61% (4.94 GB/11.60 GB) Over Threshold(40)
Disk Space:
C: 30.43% (136.75 GB/449.47 GB)
D: 85.58% (13.69 GB/16.00 GB) Over Threshold(50)
1/2/2012 3:51:41 PM, Timer Proc Ended ... ...
---------------------------------------------------
正如预期,我们收到了 Google、Microsoft 和 CNN 的主机名和 IP。我们显示本地资源并根据阈值检查其使用情况。任务每 2 分钟(120000 毫秒)执行一次。
接下来,我们尝试更改 SysMonService.xml 中的设置以查看差异。例如,进行以下更改:

这次我创建了两个主机,一个是 ABC,另一个是无效 URI。我禁用了虚拟内存轮询并将磁盘空间阈值重置为 90%。稍等片刻,重新打开 SysMonService1Jan2012.log,日志将如下所示:
---------------------------------------------------
1/2/2012 6:06:27 PM, Timer Proc Started ... ...
------ Check Remote Hosts ------
Check www.abc.com, Host name:abc.com
IP: (199.181.132.250)
Check invalidHost, Error: No such host is known
----- Check Local Computer -----
CPU Used 10.00%
Physical Memory 40.97% (2.38 GB/5.80 GB) Over Threshold(30)
Disk Space:
C: 30.42% (136.75 GB/449.47 GB)
D: 85.58% (13.69 GB/16.00 GB)
1/2/2012 6:06:30 PM, Timer Proc Ended ... ...
---------------------------------------------------
输出看起来与我们更改的 XML 设置一致。但有一件事并没有立即生效。请注意,我将检查周期设置为 1 分钟(60000 毫秒)。当您检查日志文件时,仍然会看到 2 分钟的间隔。这是为什么呢?因为我还没有在计时器过程中更改间隔(尽管我可以)。您必须通过在服务小程序中使用“暂停”和“恢复”来重置计时器,以重新启动线程过程。我稍后会讨论这个问题。
最后,您可以在“控制面板”的“程序和功能”中找到卸载 SysMonService
的条目,该条目也在安装时创建。

此时,您可能会想到如何在 C# 中使用各种组件来实现该服务。我将在以下部分重点介绍这些内容。但首先,我假设您了解如何在 Visual Studio 2010 中创建带有安装组件的 Windows 服务项目。如果不是,MSDN 提供了一个简洁且易于理解的演练:在组件设计器中创建 Windows 服务应用程序以供阅读和实践。我在这里不会讨论服务的创建和安装。
可配置的服务设置
可配置的 XML 文件基于 ServiceConfig.cs 中的以下结构:
public class ServiceConfig
{
[XmlAttribute()]
public string ServiceName;
public int TimeCheckCycle { get; set; }
public int TimeStartDelay { get; set; }
public int TimeStopTimeout { get; set; }
public string[] Hosts;
public Usage[] Usages;
}
public enum DeviceType { CPU, PhysicalMemory, VirtualMemory, DiskSpace };
public class Usage
{
[XmlAttribute()]
public DeviceType DeviceID;
[XmlAttribute()]
public bool Enable;
[XmlAttribute()]
public double Threshold;
}
ServiceConfig
类相应地表示 SysMonService.xml 中的 XML 项。我调用 _getXmlConfig()
函数来访问 XML 设置。如果 SysMonService.xml 不存在,我将创建一个默认文件,否则只检索其内容。这使得您可以在服务运行时在轮询周期之间配置 XML 项。显然,任何更改都应遵循 ServiceConfig
设计的规范。以下 _getXmlConfig()
由服务线程过程和计时器过程调用。
// Get the configurations to prepare timer
internal ServiceConfig _getXmlConfig()
{
string path = Assembly.GetExecutingAssembly().Location;
int pos = path.IndexOf(".exe");
path = path.Substring(0, pos) + ".xml";
XmlSerializer x = new XmlSerializer(typeof(ServiceConfig));
if (!File.Exists(path)) // Create XML at the first time
{
_config = new ServiceConfig
{
ServiceName = Log.Instance.ModuleName,
TimeStartDelay = 2000, TimeCheckCycle = 120000, TimeStopTimeout = 5000,
Hosts = new string[] {
"www.google.com", "www.microsoft.com", "www.cnn.com" }
};
// DeviceType { CPU, PhysicalMemory, VirtualMemory, DiskSpace };
double threshold = 10;
Array ary = Enum.GetValues(typeof(DeviceType));
_config.Usages = new Usage[ary.Length];
foreach (DeviceType value in ary)
{
_config.Usages[(int)value]
= new Usage { DeviceID = value, Enable = true,
Threshold = threshold += 10 };
}
TextWriter w = new StreamWriter(path);
x.Serialize(w, _config);
w.Close();
Log.Instance.WriteLine("Info: Service Configuration created");
}
else // XML configurations Exist
{
try
{
TextReader r = new StreamReader(path);
_config = x.Deserialize(r) as ServiceConfig;
r.Close();
Log.Instance.WriteLine("Info: Service Configuration retrieved");
}
catch (Exception e)
{
Log.Instance.WriteLine("Error in XmlSerializer TextReader: " + e.Message);
}
}
return _config;
}
现在我想引导您了解 ServiceThread.cs 中 ServiceThread
类中的线程过程。
public class ServiceThread
{
public void Start(string action)
{
Log.Instance.OpenLog();
Log.Instance.WriteLine("===================================================");
Log.Instance.WriteLine(Log.Instance.ModuleName + " Thread " +action, true);
_thread = new Thread(ThreadProc);
_config = _getXmlConfig();
_thread.Start(this);
}
public void Stop(string action)
{
_myTimer.Dispose();
Log.Instance.WriteLine(Log.Instance.ModuleName + " Thread " + action
+(_thread.Join(_config.TimeStopTimeout) ? " OK" : " Timeout"), true);
_config = null;
Log.Instance.Close();
}
// Get the configurations to prepare timer
internal ServiceConfig _getXmlConfig()
{
// ... See above
}
static void ThreadProc(object o)
{
Log.Instance.WriteLine("Thread Proc called");
ServiceConfig cfg = (o as ServiceThread)._config;
ServiceWorker sw = new ServiceWorker();
_myTimer = new Timer(sw.TimerProc, o, cfg.TimeStartDelay, cfg.TimeCheckCycle);
}
Thread _thread;
ServiceConfig _config;
static Timer _myTimer;
}
ServiceThread
有三个成员对象:_thread
、_config
和 _myTimer
,并提供 Start()
和 Stop()
方法。启动服务时,我创建一个线程,获取配置,并触发 ThreadProc()
,在那里我将所有服务任务委托给 ServiceWorker
对象。为此,我将 ServiceWorker
的 TimerProc()
传递给 _myTimer
,并使用 XML 时间设置初始化 _myTimer
。在 Stop()
中,我简单地等待 _config.TimeStopTimeout
,以使 _thread
结束,此处不关心同步机制。
独立定义服务任务
软件开发的基本原则之一是将业务逻辑与应用程序逻辑分离。如前所述,我将 ServiceWorker
类定义为一个高内聚的业务单元,它与服务应用程序松散耦合。因此,在服务端,与模块化的 ServiceWorker
中执行的任务没有任何关系。如有必要,当引入更多复杂性(例如数据库和通信)时,ServiceWorker
可以放在 DLL 中。目前,我只放了一些小东西来使这个演示有意义。
internal class ServiceWorker
{
// This method is called by the timer delegate.
internal void TimerProc(Object o)
{
Log.Instance.WriteLine("Timer Proc Started ... ...", true);
ServiceConfig cfg = (o as ServiceThread)._getXmlConfig();
string s = "";
Log.Instance.WriteLine("------ Check Remote Hosts ------");
foreach (string host in cfg.Hosts)
{
try
{
IPHostEntry hostInfo = Dns.GetHostEntry(host);
Thread.Sleep(1000);
Log.Instance.WriteLine
("Check " +host +", Host name:" + hostInfo.HostName);
s = "";
foreach (IPAddress ip in hostInfo.AddressList)
s += "(" +ip.ToString() +") ";
Log.Instance.WriteLine("IP: " + s);
}
catch (Exception e)
{
Log.Instance.WriteLine("Check " +host + ", Error: " + e.Message);
}
}
Log.Instance.WriteLine("----- Check Local Computer -----");
foreach (Usage u in cfg.Usages)
{
if (!u.Enable)
continue;
switch (u.DeviceID)
{
case DeviceType.CPU:
double d = _SysData.GetProcessorData();
Log.Instance.WriteLine("CPU Used " + d.ToString("F") + "%"
+ (d >= u.Threshold ? " Over Threshold
(" + u.Threshold + ")" : ""));
break;
case DeviceType.DiskSpace:
Log.Instance.WriteLine("Disk Space:");
foreach (SysValues v in _SysData.GetDiskSpaces())
LogSysValueWithUsage(v, u);
break;
case DeviceType.PhysicalMemory:
LogSysValueWithUsage(_SysData.GetPhysicalMemory(), u);
break;
case DeviceType.VirtualMemory:
LogSysValueWithUsage(_SysData.GetVirtualMemory(), u);
break;
}
}
Log.Instance.WriteLine("Timer Proc Ended ... ...", true);
Log.Instance.WriteLine("---------------------------------------------------");
Log.Instance.Close();
} // TimerProc
void LogSysValueWithUsage(SysValues val, Usage usage)
{
double d = 100 * val.Used / val.Total;
string s = (d >= usage.Threshold ?
" Over Threshold(" + usage.Threshold + ")" : "");
Log.Instance.WriteLine(val.DeviceID + " " + d.ToString("F") + "% ("
+ FormatBytes(double.Parse(val.Used.ToString())) + "/"
+ FormatBytes(double.Parse(val.Total.ToString())) + ")" + s);
}
enum Unit { B, KB, MB, GB, TB, ER }
string FormatBytes(double bytes)
{
int unit = 0;
while (bytes > 1024)
{
bytes /= 1024;
++unit;
}
return bytes.ToString("F") +" "+ ((Unit)unit).ToString();
}
SystemData _SysData = new SystemData();
}
在 TimerProc()
中,第一个任务(检查远程主机)很简单,只是通过调用 Dns.GetHostEntry()
收集主机名和 IP。第二个任务(检查本地计算机)涉及性能计数器和 WMI。同样,为了逻辑分离,我在 SysData.cs 中创建了另一个类 SystemData
,以封装检索 CPU、内存和磁盘使用情况所需的方法。此类的部分内容借鉴自我的系统监控器实现。为简单起见,我不会再详细讨论它。您可以阅读 SysData.cs 获取详细信息。
请注意,在辅助函数 LogSysValueWithUsage()
中,我检查了使用阈值。如果系统值较高,我会在日志文件中做出指示。在实际应用中,您可能会做得更多,例如写入数据库或发送警报消息。
服务日志记录
我们已经在代码中看到了很多 Log.Instance.WriteLine()
调用。Log
对象显示在 ServiceLog.cs 中:
class Log
{
Log() { } // Constructor is 'private'
public static Log Instance
{
get
{ // Lazy initialization, this singleton is not thread safe.
if (_instance == null)
_instance = new Log();
return _instance;
}
}
public void OpenLog()
{
// Retrieve the module path
string path = Assembly.GetExecutingAssembly().Location;
int pos = path.IndexOf(".exe");
path = path.Substring(0, pos);
pos = path.LastIndexOf('\\');
ModuleName = path.Substring(pos + 1);
// Get the week of the year
DateTimeFormatInfo dfi = DateTimeFormatInfo.CurrentInfo;
DateTime dt = DateTime.Now;
int week = dfi.Calendar.GetWeekOfYear
(dt, dfi.CalendarWeekRule, dfi.FirstDayOfWeek);
// Create one log file per week
path += week + dt.ToString("MMMyyyy") + ".log";
FileMode fm = File.Exists(path) ? FileMode.Append : FileMode.CreateNew;
_fileStream = new FileStream(path, fm, FileAccess.Write, FileShare.Read);
_streamWriter = new StreamWriter(_fileStream);
}
public void WriteLine(string LogMsg, bool timeStamp=false)
{
if (_streamWriter == null) // Lazy initialization,
OpenLog();
_streamWriter.BaseStream.Seek(0, SeekOrigin.End);
string time = "";
if (timeStamp)
time = DateTime.Now.ToString("G") + ", ";
_streamWriter.WriteLine(time + LogMsg);
}
public void Close()
{
// Cleanup for _streamWriter and _fileStream
if (_streamWriter != null)
{
_streamWriter.Close();
_streamWriter = null;
}
if (_fileStream != null)
{
_fileStream.Close();
_fileStream = null;
}
}
public string ModuleName { get; set; }
static Log _instance;
FileStream _fileStream;
StreamWriter _streamWriter;
}
如您所见,Log
对象是一个单例。它不是线程安全的,但对于此演示来说足够了。值得一提的是 OpenLog()
方法。在 OpenLog()
中,我获取一年中的周数并构造当前日志名称。如果此文件存在,则追加日志;否则,创建一个新的每周日志文件进行写入。这使得一年大约有 52 个文件,以避免在一个文件中记录大量日志。请注意,OpenLog()
在服务启动时被调用。但是,如果 _streamWriter
为 null
,它也会在 Log
的 WriteLine()
中每次被调用。这是一个延迟初始化,发生在 TimerProc()
中一个新的检查周期开始时。为了避免日志文件在周期之间保持打开,我在 TimerProc()
结束时调用 Log
的 Close()
,这将 _streamWriter
设置为 null
作为标志。
处理服务事件
最后,让我们回到服务应用程序本身来处理服务事件。在这个服务中,我特意通过设置 CanPauseAndContinue
为 true
来启用“暂停”和“恢复”命令。因此,在您启动服务后,打开服务“属性”对话框,您可以看到“暂停”按钮已启用,如果您暂停它,“恢复”按钮将启用。

我们的 SysMonService
类,即主服务引擎,全权负责处理以下四个事件:
public partial class SysMonService : ServiceBase
{
public SysMonService()
{
InitializeComponent();
this.AutoLog = false;
this.CanPauseAndContinue = true;
_paused = false;
}
protected override void OnStart(string[] args)
{
// to Application event logs
EventLog.WriteEntry("Dings System Monitor Started.");
_paused = false;
_serviceThread = new ServiceThread();
_serviceThread.Start("Started");
}
protected override void OnStop()
{
// to Application event logs
EventLog.WriteEntry("Dings System Monitor Stopped.");
if (!_paused)
_serviceThread.Stop("Stopped");
}
protected override void OnPause()
{
EventLog.WriteEntry("Dings System Monitor Paused.");
_serviceThread.Stop("Paused");
_paused = true;
}
protected override void OnContinue()
{
EventLog.WriteEntry("Dings System Monitor Resumed.");
_paused = false;
_serviceThread = new ServiceThread();
_serviceThread.Start("Resumed");
}
ServiceThread _serviceThread;
bool _paused;
}
如上所示,我只是使用 ServiceBase
的 EventLog
对象,而没有定义自定义的 EventLog
对象(尽管我可以)。我禁用了服务的 AutoLog
并调用了 EventLog
的 WriteEntry()
。这样,当您暂停此服务时,可以在事件查看器中看到事件日志信息。

由于我们之前已经描述了 ServiceThread
类,我只需要创建一个这样的对象成员 _serviceThread
,并通过调用 Start()
和 Stop()
将所有工作委托给它。此外,我只需重用此逻辑来处理 OnPause()
中的暂停和 OnContinue()
中的恢复。这里要注意两点:我使用 _paused
标志来避免在用户在服务已暂停时停止服务时重复调用 Stop()
。其次,我将一个动作字符串传递给 Start()
和 Stop()
,以区分我们自己的日志文件中的调用上下文。请注意,我只在 ThreadProc()
中初始化时间间隔,因此 SysMonService.xml 中 TimeCheckCycle
的任何更改都不会生效,直到服务重新启动或恢复,这回答了上一节中的问题。
我实现暂停和恢复处理程序的一个原因是用于服务调试。从 MSDN 如何:调试 Windows 服务应用程序中,我们知道调试 OnStart()
方法并不容易。但是,您可以在 OnPause()
和 OnContinue()
中捕获断点而没有问题。
摘要
使用 Microsoft Visual Studio 或 Microsoft .NET Framework SDK,我们可以轻松地创建一个服务及其安装组件。但实际的服务设计、编程和调试则有点挑战性。本文主要侧重于服务 IO 的具体细节,并提供了一个 C# 示例演示。该示例可以作为初学者的教程,因为我介绍了一些 .NET 编程技巧并进行了详细解释。根据我的经验,Windows 服务开发确实是一个有趣的领域,但需要与其他应用程序不同的思维方式。
我没有提及的一个有用工具是 ServiceController
类,它可以在您自己的应用程序中用于控制和访问服务,而不是依赖于服务控制管理器来启动和停止。这可以成为另一篇文章的主题。目前,我只是在源代码中包含了一个控制台程序 GetSystemMonitorService
,它简单地使用 ServiceController
显示此演示服务的一些属性。

历史
- 2012 年 1 月 9 日 -- 发布原始版本