🚀 更多关于 Windows Internals、调试、逆向和出海相关内容请访问我的个人博客: FrendGuo's Blog

【译】调查并确定 Windows 运行速度变慢问题

我最近注意到,我的 Windows 7 家用笔记本电脑运行速度比平时要慢。系统似乎在磁盘 I/O 上花了很多时间。当我查看任务管理器时,我发现笔记本电脑的缓存磁盘数据量大约只有平时的 五分之一,但问题的原因并不明显。

我的工作之一是性能专家——要调查各种日常或深层次的性能问题。所以当我意识到我的笔记本出现了严重且系统性的问题时,我集中精力,拿出所有专业工具进行调查,最终找到了罪魁祸首。

你即将读到的这个故事完全真实。唯一被改动的是一些缓存、可用和空闲内存的精确数值,以保护“无辜者”。

提前剧透:我从 Costco 买的那台 Western Digital My Book Live 备份驱动器是罪魁祸首。

再多剧透一点:在我发表这篇文章九天之后,Western Digital 联系了我,并非常认真地对待这个问题。大约四个月后,他们发布了修复补丁。这个故事的结局很美好,而且你可以通过博客来改变世界。


缓存的意义

想要理解这个谜团,首先得理解 Windows 磁盘缓存的重要性。在计算机里拥有大量内存是很重要的,不仅能让你同时运行很多程序,而不必动用页面文件,而且还能让更多的数据缓存到内存里。硬盘由于依赖物理旋转盘片,是计算机里速度最慢的组件之一,而笔记本硬盘则是硬盘中最慢的类型。Windows 会把硬盘中的数据缓存到内存中,以便能瞬间访问这些数据,而不是每次都要花上几毫秒来读取。缓存的数据越多,你对磁盘的访问就越少,计算机速度也就越快。正因为如此,Windows 的磁盘缓存几乎可以说“内存越多越好”。我工作的机器有 32 GiB(GiB 指二进制 1024 的幂的单位),这意味着我通常能缓存超过 24 GiB 的磁盘数据,这让我能在不同的程序和不同的项目之间切换时,最大程度地减少磁盘 I/O。

从 Windows Vista 开始,Windows 引入了一些只为将磁盘缓存填满而生的系统机制,它会根据日志判断你下一步最可能需要哪些数据,然后提前加载。比如 SuperFetch 会监控你的使用模式,在你启动应用程序时提前预取数据。系统空闲时也会进行预取。

这些功能在 Windows Vista 刚推出时让人们感到困惑。用户抱怨 Vista 占用大量内存,很“笨重”,因为在任务管理器里看到几乎没有空闲内存。然而事实证明,这其实是好事。空闲内存完全是浪费,一点用都没有。一台运行良好的 Windows 机器应该几乎没有空闲内存,而有大量被用来做磁盘缓存的内存。缓存的内存是有用的,因为它能让系统运行得更快,而且当需要时,缓存内存可以随时转变成空闲内存来供程序分配。


健康的机器

当 Windows 正常运行时,任务管理器(通过右键点击任务栏来打开)在物理内存统计信息中应该显示类似右图的内容:大量的 cached(缓存)内存,大量的 available(可用)内存,以及很少的 free(空闲)内存。如果 available 内存很低(低于几千 MiB),就值得考虑增加更多内存,以便 Windows 有更多空间来运行程序并缓存数据。如果 free 内存很高(高于几百 MiB),那么要么 Windows 还没来得及往磁盘缓存中填充数据,要么就是哪里出了问题。


不健康的机器

在我的笔记本上,任务管理器的“性能”选项卡看起来是这样的:

上图中紫色圈出的区域显示:我有很多可用内存,但磁盘缓存比预期要小(对于一台 8 GiB 的机器来说),而且空闲内存明显偏高。我的 8 GiB 笔记本通常能缓存 4-5 GiB 的数据,但我注意到问题发生时,它只能缓存 1-2 GiB 左右的数据。

查看内存使用历史,则能看到为什么缓存内存低、空闲内存高。

上图中红色圈起的部分显示了三次短暂的内存使用峰值。每一处峰值都对应了一个瞬时的 4 GiB 内存分配。Windows 为满足其中一次分配,把 4 GiB 有用数据从文件缓存中腾空,最后就变成了浪费的空闲内存。

任务管理器帮助我了解了问题的大概结构,但并不能找到罪魁祸首。它的“峰值工作集(Peak Working Set)”一列里没有显示任何可能导致这类峰值的进程。这时候我就要使用最喜欢的系统分析工具了。


用 xperf 进行调查

我使用了我所知最强大的性能调查工具:ETW tracing(也就是 xperf),来自 Windows Performance Toolkit(WPT)。这是微软免费提供的一套工具,可以从 CPU 使用率、磁盘 I/O、内存分配等各个角度来分析 Windows 的性能。xperf 使用起来并不容易,但对于熟练用户来说,它能提供对 Windows 性能几乎无限的洞察力。

我决定使用新的 Windows Performance Recorder UI(wprui)来记录追踪数据。我勾选了所有和内存分配相关的选项,并让它把日志记录到内存中(目前我会使用 UIforETW,在“Settings”对话框中勾选“VirtualAlloc stacks always”,在“Extra kernel flags”里选“POOL”,并在“Extra kernel stackwalks”里选“PoolAlloc”)。在这种模式下,wprui 或 UIforETW 会持续监控你的系统,将详细的性能信息写入到内存中的循环缓冲区里。当你发现性能问题时,点击“Save”按钮,就能把缓冲区保存到磁盘上。也就是说,它是一种“事后”分析器,你可以随时记录最后约 30 秒的系统活动。我一般会让这种内存记录模式一直开着,这样我随时可以捕捉罕见的性能问题,而不用去想如何复现。当下图截图时,我已经让 wprui 连续运行了快一周,但它的“Time”字段每天会重置。


抓个正着

内存峰值大多出现在随机时刻,很容易错过,所以我一直没能捕捉到想要的追踪记录。不过我最后发现,每次我的笔记本从待机恢复时,都会出现一次内存峰值。这样一来就简单多了。只要在 wprui 开始记录后,让笔记本休眠并恢复,登录后立刻点击“Save”就行。搞定!然后我把记录加载到 WPA(Windows Performance Analyzer)里,查看和内存相关的异常。这一步有时候需要耐心,因为你并不知道应该看哪里,也不知道具体要找什么,但有时当你看到正确的图表时,问题会非常明显。看看下面这张图,能看到那个极其明显的内存峰值吗?

到目前为止,我们看到的还只是跟任务管理器类似的信息,但 xperf 的强大之处在于可以深入得多。这个追踪记录包含所有池(pool)内存分配事件的调用栈。只要选中合适的时间范围(从峰值中段到峰值前),我们就能得到一个汇总表。通过合理设置汇总表列,按大小排序,再深入到 AIFO 类型(即“在选定时间范围内分配,但未在范围内释放”),我们很快就找到了那个导致问题的 4,294,967,296 字节的分配(也就是 4 GiB)。而且我们还拿到了调用栈。

最初看到这个调用栈时,我以为是 Windows 的 bug。因为我最近才装了一些 Windows 更新,我猜可能是哪个补丁导致了这个问题。结果我猜错了,但需要再深入一些才能明白。

从调用栈可以看到,FstubAllocateDiskInformation 这个函数做了两次 ExAllocatePoolWithTag 调用,其中一次分配了大量内存。现在是时候看看 FstubAllocateDiskInformation 做了什么。


真相在代码里

要查看 FstubAllocateDiskInformation 这段代码,最简单的方法是使用 Local Kernel Debugging。通过它可以让 windbg 获取内核的内存快照,而无需真正中断内核执行。它对调试功能有局限,比如不能设置断点或者单步调试,但查看反汇编已经够用了,而且比在两台机器之间用电缆做实时内核调试更简单。

很显然,这里需要用 uf 命令来反汇编此函数。下面是反汇编后的结果,我稍作了格式化,并用粗体标出了几个关键的指令,并加上了一些注释:

lkd> uf ntkrnlmp!FstubAllocateDiskInformation
nt!FstubAllocateDiskInformation:
mov qword ptr [rsp+8],rbx
mov qword ptr [rsp+10h],rbp
mov qword ptr [rsp+18h],rsi
push rdi
sub rsp,20h
mov rsi,rdx
mov rbp,rcx
mov r8d,42747346h ; pool tag of FstB
mov edx,40h ; allocation size
xor ecx,ecx ; flags
call nt!ExAllocatePoolWithTag
test rax,rax
je Exit1

nt!FstubAllocateDiskInformation+0x35:
lea rdx,[rax+10h]
mov rcx,rbp
mov rbx,rax
call nt!FstubGetDiskGeometry
test eax,eax
js Exit2

nt!FstubAllocateDiskInformation+0x4c:
mov ecx,dword ptr [rbx+24h]
test ecx,ecx ; compare SectorSize to zero
je Exit3

nt!FstubAllocateDiskInformation+0x57:
mov rax,qword ptr [rbx+28h]
test rax,rax
je Exit3

nt!FstubAllocateDiskInformation+0x64:
xor edx,edx
mov dword ptr [rbx+8],ecx
mov r8d,42747346h ; pool tag of FstB
div rax,rcx
mov rdx,rcx ; SectorSize/allocation size
mov ecx,4 ; flags
mov qword ptr [rbx+38h],rax
mov qword ptr [rbx],rbp
call nt!ExAllocatePoolWithTag
test rax,rax
je Exit4

你不需要仔细理解所有代码才能跟上后面的逻辑。代码中有两次调用 ExAllocatePoolWithTag,一次是 0x40 字节,另一次则是可变大小。两次调用都使用了 pool tag 42747346h(对应 “FstB”)。要做完整分析还要查看输入参数类型及其布局,我也去 reactos 网站上看了他们的源码版本,以确认我对这段汇编的解读没错。关键行可以这样概括:

FstubGetDiskGeometry(DiskInformation); 
...
if (DiskInformation->SectorSize != 0) 
	ExAllocatePoolWithTag(DiskInformation->SectorSize);

换句话说,FstubAllocateDiskInformation 调用了 FstubGetDiskGeometry(参见 FstubGetDiskGeometry),然后如果 SectorSize 不为 0,就分配 SectorSize 字节的内存。

我稍微停顿了一下,去 验证 主硬盘的扇区大小:

C:>fsutil fsinfo ntfsinfo c: 
NTFS Volume Serial Number : 0x00b8457cb8457168 
Version : 3.1 
Number Sectors : 0x0000000048ad8ae2 
Total Clusters : 0x000000000915b15c 
Free Clusters : 0x00000000016ac4a5 
Total Reserved : 0x0000000000000780 
Bytes Per Sector : 512 
Bytes Per Physical Sector : 512 
...

随后,我猛地醒悟到几周前,我购买并使用了一个备份驱动器,当我第一次使用它时,它安装了一个驱动。这个驱动正是导致所有问题的根源。这正好和我开始出现性能问题的时间相吻合,也和我安装那些 Windows 更新的时间差不多。


问题解决!

我打开设备管理器,找到了问题驱动,把它卸载,然后甚至不用重启电脑,我的笔记本就恢复正常了。无论是休眠还是恢复,都不再出现内存峰值,我的磁盘缓存也不会再被清空,倍感欣慰。现在我这台笔记本有 8 GiB 内存的真正表现了,而不再像只有 4 GiB 那样。

我并不是驱动开发专家——我的领域在用户态——但我能想象大概发生了什么。这个驱动好像在处理 IOCTL_DISK_GET_DRIVE_GEOMETRY 请求时,要返回一个 DISK_GEOMETRY 结构。但如果某些信息不可用,该怎么做呢?或许可以返回一个错误代码,也可能填一个全是零的结构,但这个驱动却把所有字段——包括 BytesPerSector——都设成了 0xFFFFFFFF。由于 SectorSize 是无符号数,加上内存分配会向上取整,所以最终导致分配了 4 GiB 内存。这个错误不算太难犯,但 Western Digital 的开发者们确实应该注意到它带来的严重问题。

Windows 内核只检查了 BytesPerSector 是否等于 0,却没有检查它是否在某个合理的范围(也可参见 KB2510009)内。也就是说,内核少了一道保护,否则本可以避免此问题。

如果你拥有这台设备,我建议你把它退掉。不过,这可能还不够。我在离那个备份驱动器很远、并且没有连接它的情况下,也会出现这个问题。所以很可能需要打开设备管理器,手动卸载驱动。这个我也不完全肯定。

我还建议你联系 Western Digital,要求他们放出修复补丁。


后记

发现问题后,我原本只想把这个驱动器退掉,但在那之前,我特意确保我机器上装了最新的驱动,以及 NAS 固件上的所有更新,以保证测试结果准确。随后我清空了 NAS 里的数据,并格式化了硬盘,“Full Factory Restore” 选项会安全地擦除磁盘,这对于退回一个曾经存过我整机备份的硬盘至关重要。然后,我卸载了驱动,确认问题解决。

再一次,ETW 让我用科学的方式调查并解决了一个神秘问题。我希望能有更多的开发者运用“基于证据的方法”来调查性能问题,而不是依靠猜测。

我同情那些普通消费者,他们可能会很随意地买下这个设备,也不太可能想到把性能问题和新买的备份硬盘联系起来。

我通过一些私人渠道联系了微软,他们确认相关团队已经知晓这个问题。我在 2012 年 8 月 29 日通过普通的支持渠道联系了 Western Digital,但他们一直没回复——后来他们的一位工程师看到了这篇文章,并在 2012 年 9 月 7 日(也就是九天之后)通过技术支持渠道回复了我。


其他故事

如果你喜欢这篇文章,也许你也会喜欢我用 xperf 进行的其他取证式性能调查:

  1. Xperf and Visual Studio: The Case of the Breakpoint Hangs
  2. PowerPoint Poor Performance Problem(以及 Xperf for Excess CPU Consumption 这篇更详细的教程,它探讨了同一个问题)
  3. Faster Fractals Through Better Scheduling —— 调查并优化 Fractal eXtreme 中的一些低效点
  4. Xperf Wait Analysis–Finding Idle Time —— 分析 Visual Studio 中三秒卡顿的教程
  5. Windows Live Photo Gallery—Poor Performance Peculiarities —— 调查并解决放大图像时出现的短暂卡顿
  6. Fixing another Photo Gallery performance bug —— 调查并解决启动时的卡顿问题

如果你想学着自己用 xperf 做调查,可参考 ETW central

关于 brucedawson

我是 Google 的一名程序员,主要工作是做优化和可靠性开发。让代码跑得快 10 倍比什么都开心,除非能同时消灭大量 bug。我也玩单轮车、打冰球(ice hockey)和轮椅冰球(sled hockey),还会玩杂耍(juggle)。有时我会纠结这个博客究竟该不该叫 randomutf-8。了解更多可看我在 Twitter 上的2010 年代回顾

以上翻译仅供参考,原文信息请以英文原版为准。

原文链接:https://randomascii.wordpress.com/2012/09/04/windows-slowdown-investigated-and-identified/

posted @   frendguo  阅读(72)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek-R1本地部署如何选择适合你的版本?看这里
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
· 我们是如何解决abp身上的几个痛点
· 普通人也能轻松掌握的20个DeepSeek高频提示词(2025版)
点击右上角即可分享
微信分享提示