如何在.NET应用程序中限制I/O操作?

16
我正在开发一个应用程序(.NET 4.0, C#),它需要:
1. 扫描文件系统。
2. 打开和读取一些文件。
该应用程序将在后台运行,对磁盘使用率的影响应该很小。如果用户正在执行其常规任务并且磁盘使用率很高,该应用程序不应该打扰他们。反之,如果没有人使用磁盘,该应用程序可以更快地运行。
主要问题是,由于使用 API(mapi32.dll) 读取文件,我不知道实际的 I/O 操作量和大小。如果我要求 API 执行某些操作,我不知道它读取了多少字节来处理我的响应。
所以问题是,如何监控和管理磁盘使用情况?包括文件系统扫描和文件读取...
检查标准性能监视器工具使用的性能计数器?还是其他方法?

该服务是否具有管理员权限或以普通用户帐户运行?(物理磁盘的性能计数器需要管理员权限)。根据答案,可能会有另一种方法。 - Neil Fenwick
是的,它将拥有管理员权限。 - Anatoly
5个回答

20
使用 System.Diagnostics.PerformanceCounter 类,连接到与正在索引的驱动器相关的 PhysicalDisk 计数器。
以下是一些代码示例,尽管它目前是硬编码到"C:"驱动器,但您需要将“C:”更改为您的进程正在扫描的任何驱动器。(这只是大致的示范代码,用于说明性能计数器的存在 - 不要将其视为提供准确信息 - 应仅用作指南。按照自己的目的进行更改)
观察 % 空闲时间 计数器,该计数器指示驱动器在多大程度上在执行操作。 0%空闲 表示磁盘正在忙碌,但不一定意味着它无法传输更多数据。
% 空闲时间当前磁盘队列长度 结合起来,这将告诉您驱动器是否变得如此繁忙,以至于无法处理所有的数据请求。通常情况下,任何超过 0 的值都表示驱动器可能已经非常忙碌,而任何超过 2 的值都表示驱动器已经完全饱和了。这些规则对SSD和HDD都适用。
此外,您读取的任何值都是某个时间点上的瞬时值。您应该对一些结果进行一个运行平均数,例如每100毫秒读取一次,并在使用结果的信息做出决策之前对5个读数求平均数(即等待计数器稳定后再进行下一次IO请求)。
internal DiskUsageMonitor(string driveName)
{

    // Get a list of the counters and look for "C:"

    var perfCategory = new PerformanceCounterCategory("PhysicalDisk");
    string[] instanceNames = perfCategory.GetInstanceNames();

    foreach (string name in instanceNames)
    {
        if (name.IndexOf("C:") > 0)
        {
            if (string.IsNullOrEmpty(driveName))
               driveName = name;
        }
    }


    _readBytesCounter = new PerformanceCounter("PhysicalDisk", 
                                               "Disk Read Bytes/sec", 
                                               driveName);

    _writeBytesCounter = new PerformanceCounter("PhysicalDisk", 
                                                "Disk Write Bytes/sec", 
                                                driveName);

    _diskQueueCounter = new PerformanceCounter("PhysicalDisk", 
                                               "Current Disk Queue Length", 
                                               driveName);

    _idleCounter = new PerformanceCounter("PhysicalDisk",
                                          "% Idle Time", 
                                          driveName);
    InitTimer();
}

internal event DiskUsageResultHander DiskUsageResult;

private void InitTimer()
{
    StopTimer();
    _perfTimer = new Timer(_updateResolutionMillisecs);
    _perfTimer.Elapsed += PerfTimerElapsed;
    _perfTimer.Start();
}

private void PerfTimerElapsed(object sender, ElapsedEventArgs e)
{
    float diskReads = _readBytesCounter.NextValue();
    float diskWrites = _writeBytesCounter.NextValue();
    float diskQueue = _diskQueueCounter.NextValue();
    float idlePercent = _idleCounter.NextValue();

    if (idlePercent > 100)
    {
        idlePercent = 100;
    }

    if (DiskUsageResult != null)
    {
        var stats = new DiskUsageStats
                        {
                                DriveName = _readBytesCounter.InstanceName,
                                DiskQueueLength = (int)diskQueue,
                                ReadBytesPerSec = (int)diskReads,
                                WriteBytesPerSec = (int)diskWrites,
                                DiskUsagePercent = 100 - (int)idlePercent
                        };
        DiskUsageResult(stats);
    }
}

非常好的答案!我也是想用同样的计数器,但错过了System.Diagnostics命名空间中相关的类。这个例子非常有用。现在我更清楚地看到了解决方案。这种方法符合我的期望。非常感谢!!! - Anatoly

3

思考一下:如果有其他遵循相同(或类似)策略的进程,会发生什么情况?在“空闲时间”中哪一个进程会运行?其他进程能有机会利用空闲时间吗?

显然,除非有某些公认的操作系统机制来公平地分配空闲时间内的资源,否则无法正确执行。在Windows中,通过调用SetPriorityClass来完成此操作。

这篇关于Vista中I/O优先级的文档似乎意味着IDLE_PRIORITY_CLASS实际上不会降低I/O请求的优先级(但会降低进程的调度优先级)。Vista为此添加了新的值PROCESS_MODE_BACKGROUND_BEGINPROCESS_MODE_BACKGROUND_END

在C#中,您通常可以使用Process.PriorityClass属性设置进程优先级。不过,Vista的新值不可用,因此您必须直接调用Windows API函数。您可以像这样进行操作:

[DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool SetPriorityClass(IntPtr handle, uint priorityClass);

const uint PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000;

static void SetBackgroundMode()
{
   if (!SetPriorityClass(new IntPtr(-1), PROCESS_MODE_BACKGROUND_BEGIN))
   {
      // handle error...
   }
}

我没有测试上面的代码。不要忘记它只能在Vista或更高版本上运行。您需要使用Environment.OSVersion检查早期操作系统并实施回退策略。


2
SetPriorityClass 需要一个句柄,Process.GetCurrentProcess().Id 是进程 ID,所以这样是行不通的。幸运的是,您可以简单地将 new IntPtr(-1) 传递给 SetPriorityClass,因为它是当前进程的伪句柄。通过这些更改,它可以工作。此外,您应该检查 SetPriorityClass 的返回值,以查看它是否真正起作用。 - CodesInChaos

3
很久以前,微软研究部门发表了一篇论文(很抱歉我不记得URL了)。
据我回忆,该程序最初只执行了非常少的“工作项”。
他们测量了每个“工作项”所需的时间。
运行一段时间后,他们可以计算出系统负载下“工作项”的速度。
从那时起,如果“工作项”速度很快(例如没有其他程序员发出请求),他们会发出更多请求,否则他们将减少请求。
基本思想是:“如果它们在拖慢我的速度,那么我一定在拖慢它们的速度,因此如果我的速度被拖慢,就要做更少的工作”。

1
请参考这个问题这个问题以获取相关查询。我建议采用简单的解决方案,定期查询当前磁盘和CPU使用率%,并仅在它们低于定义的阈值时继续执行当前任务。只需确保您的工作可以轻松地分解为任务,并且每个任务可以轻松高效地启动/停止。

谢谢你提供第二个链接。 我想要的是计数器,但我忘记了管理包装器。 - Anatoly

0

检查屏幕保护程序是否正在运行?这是用户离开键盘的一个很好的指示。


该应用程序将部署为Windows服务。 - Anatoly
同时,多个用户可以同时登录。至于我,我通常将屏幕保护程序关闭 ;) 所以不确定这是否是一个好主意。 - Anatoly
顺便说一下,即使没有人登录,任何其他服务都可以使用磁盘。我无法影响它们的性能。 - Anatoly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接