Solaris 内核内存 (kmem) 分配器提供了一组强大的调试功能,可以简化内核崩溃转储的分析。本章将讨论这些调试功能以及专门为分配器设计的 MDB dcmd 和 walker。Bonwick(请参见相关书籍和文章)概述了分配器本身的原理。有关分配器数据结构的定义,请参阅头文件 <sys/kmem_impl.h>。可以在产品化的系统中启用 kmem 调试功能以增强问题分析,或者在开发系统中启用它以协助调试内核软件和设备驱动程序。
本指南反映了 Solaris 10 实现;此信息对于过去或将来的发行版可能不相关、不正确或不适用,因为它反映的是当前的内核实现。它未定义任何类型的公共接口。所提供的有关内核内存分配器的所有信息在将来的 Solaris 发行版中可能会更改。
本节说明如何获取崩溃转储样例以及如何调用 MDB 对其进行检查。
内核内存分配器包含许多高级调试功能,但是由于这些功能可能会导致性能下降,因此缺省情况下并未启用。为了理解本指南中的示例,您应该启用这些功能。应仅在测试系统中启用这些功能,因为它们可能会导致性能下降或暴露潜在的问题。
分配器的调试功能由 kmem_flags 可调参数控制。首先,请确保正确设置了 kmem_flags:
# mdb -k > kmem_flags/X kmem_flags: kmem_flags: f
如果未将 kmem_flags 设置为 'f',则应该将以下行:
set kmem_flags=0xf
添加至 /etc/system,然后重新引导系统。系统重新引导时,请确认是否已将 kmem_flags 设置为 'f'。将此系统恢复为用于生产之前,请记住要删除对 /etc/system 的修改。
下一步是确保正确配置了崩溃转储。首先,请确认是否将 dumpadm 配置为保存内核崩溃转储并启用了 savecore。 有关崩溃转储参数的更多信息,请参见 dumpadm(1M)。
# dumpadm Dump content: kernel pages Dump device: /dev/dsk/c0t0d0s1 (swap) Savecore directory: /var/crash/testsystem Savecore enabled: yes
接下来,使用 reboot(1M) 的 '-d' 标志重新引导系统,这将强制内核崩溃并保存崩溃转储。
# reboot -d Sep 28 17:51:18 testsystem reboot: rebooted by root panic[cpu0]/thread=70aacde0: forced crash dump initiated at user request 401fbb10 genunix:uadmin+55c (1, 1, 0, 6d700000, 5, 0) %l0-7: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 ...
系统重新引导时,请确保崩溃转储已成功:
$ cd /var/crash/testsystem $ ls bounds unix.0 unix.1 vmcore.0 vmcore.1
如果转储目录中缺少该转储,可能是由于分区的空间不足。 可以释放空间并以超级用户身份手动运行 savecore(1M),以便随后保存转储。如果转储目录包含多个崩溃转储,则刚创建的转储将是具有最新修改时间的 unix.[n] 和 vmcore.[n] 对。
现在,请对创建的崩溃转储运行 mdb 并检查其状态:
$ mdb unix.1 vmcore.1 Loading modules: [ unix krtld genunix ip nfs ipc ] > ::status debugging crash dump vmcore.1 (32-bit) from testsystem operating system: 5.10 Generic (sun4u) panic message: forced crash dump initiated at user request
在本指南提供的示例中,使用的是来自 32 位内核的崩溃转储。 此处提供的所有方法都适用于 64 位内核,并且已仔细区分了指针(其大小在 32 位和 64 位系统中不同)与固定大小的数量(相对于内核数据模型不变)。
UltraSPARC 工作站用于生成所提供的示例。您得到的结果可能随所使用系统的体系结构和型号的不同而不同。
内核内存分配器的作用是将虚拟内存中的区域分配给其他内核子系统(它们通常称为客户机)。本节说明了分配器操作的基础知识,并介绍了本指南中稍后使用的一些术语。
内核内存分配器的作用域是虚拟内存中组成内核堆的缓冲区集。 这些缓冲区将会分组为具有相同大小和用途的各缓冲区集(称为高速缓存)。 每个高速缓存都包含一组缓冲区。 其中的一些缓冲区当前空闲,这意味着尚未将其分配给分配器的任何客户机。 其余的缓冲区已分配,这意味着已将指向该缓冲区的指针提供给分配器的客户机。 如果分配器的所有客户机中都没有指向已分配的缓冲区的指针,则认为此缓冲区发生了泄漏,因为无法将其释放。 泄漏的缓冲区表明错误的代码正在浪费内核资源。
kmem 事务是指缓冲区在已分配状态和空闲状态之间的转换。 分配器可以在每个事务执行过程中验证缓冲区的状态是否有效。此外,分配器还包含用于记录事务的工具,以便进行事后检查。
与标准 C 库的 malloc(3C) 函数不同,内核内存分配器可以阻塞(或休眠),一直等到有足够的虚拟内存可用于满足客户机的请求为止。 这一行为由 kmem_alloc(9F) 的 'flag' 参数控制。调用设置了 KM_SLEEP 标志的 kmem_alloc(9F) 决不会失败;它将永远阻塞,等待资源变为可用。
内核内存分配器会将其管理的内存分为一组高速缓存。 所有分配都是从这些高速缓存(通过 kmem_cache_t 数据结构表示)实现的。每个高速缓存都具有固定的缓冲区大小,表示该高速缓存可提供的最大分配空间。 每个高速缓存都具有一个指明其管理的数据类型的字符串名称。
一些内核内存高速缓存具有特殊用途,并会进行初始化以便仅分配特定种类的数据结构。 "thread_cache" 即是此类高速缓存的一个示例,它仅分配 kthread_t
类型的结构。 这些高速缓存中的内存通过 kmem_cache_alloc() 函数分配给客户机,并且通过 kmem_cache_free() 函数释放。
kmem_cache_alloc() 和 kmem_cache_free() 不是公共的 DDI 接口。请勿编写依赖于这些接口的代码,因为将来的 Solaris 发行版中可能会更改或删除这些接口。
名称以 "kmem_alloc_" 开头的高速缓存可实现内核的常规内存分配方案。 这些高速缓存为 kmem_alloc(9F) 和 kmem_zalloc(9F) 的客户机提供内存。 其中的每个高速缓存都满足大小介于此类高速缓存的缓冲区大小和第二小的高速缓存的缓冲区大小之间的请求。 例如,内核具有 kmem_alloc_8 和 kmem_alloc_16 高速缓存。 在这种情况下,kmem_alloc_16 高速缓存可处理大小为 9-16 个字节内存的所有客户端请求。 请记住,无论客户端请求的大小是多少,kmem_alloc_16 高速缓存中每个缓冲区的大小均为 16 个字节。对于大小为 14 个字节的请求,所得到缓冲区中有两个字节是未使用的,因为该请求是从 kmem_alloc_16 高速缓存得到满足的。
最后一组高速缓存是指由内核内存分配器在内部使用以对其自身进行记录的高速缓存。 这包括名称以 "kmem_magazine_" 或 "kmem_va_"、kmem_slab_cache、kmem_bufctl_cache 等开头的那些高速缓存。
本节说明如何查找和检查内核内存高速缓存。通过发出 ::kmastat 命令,可以了解系统中的各种 kmem 高速缓存。
> ::kmastat cache buf buf buf memory alloc alloc name size in use total in use succeed fail ------------------------- ------ ------ ------ --------- --------- ----- kmem_magazine_1 8 24 1020 8192 24 0 kmem_magazine_3 16 141 510 8192 141 0 kmem_magazine_7 32 96 255 8192 96 0 ... kmem_alloc_8 8 3614 3751 90112 9834113 0 kmem_alloc_16 16 2781 3072 98304 8278603 0 kmem_alloc_24 24 517 612 24576 680537 0 kmem_alloc_32 32 398 510 24576 903214 0 kmem_alloc_40 40 482 584 32768 672089 0 ... thread_cache 368 107 126 49152 669881 0 lwp_cache 576 107 117 73728 182 0 turnstile_cache 36 149 292 16384 670506 0 cred_cache 96 6 73 8192 2677787 0 ...
如果运行 ::kmastat,则可以了解“正常”系统的信息。 这将有助于发现系统中正在泄漏内存的过大高速缓存。 根据所运行的系统和正在运行的进程数等因素,::kmastat 的结果将有所不同。
列出各种 kmem 高速缓存的另一种方法是使用 ::kmem_cache 命令:
> ::kmem_cache ADDR NAME FLAG CFLAG BUFSIZE BUFTOTL 70036028 kmem_magazine_1 0020 0e0000 8 1020 700362a8 kmem_magazine_3 0020 0e0000 16 510 70036528 kmem_magazine_7 0020 0e0000 32 255 ... 70039428 kmem_alloc_8 020f 000000 8 3751 700396a8 kmem_alloc_16 020f 000000 16 3072 70039928 kmem_alloc_24 020f 000000 24 612 70039ba8 kmem_alloc_32 020f 000000 32 510 7003a028 kmem_alloc_40 020f 000000 40 584 ...
此命令非常有用,因为它可将高速缓存名称映射到地址,并为 FLAG 列中的每个高速缓存提供调试标志。 必须要了解的是,分配器对调试功能的选择是基于每个高速缓存从这组标志派生而来的。 这些是在创建高速缓存时与全局 kmem_flags 变量一起设置的。在系统运行的同时设置 kmem_flags 不会影响调试行为,但会影响随后创建的高速缓存(这种情况在引导后很少发生)。
接下来,请直接使用 MDB 的 kmem_cache walker 遍历 kmem 高速缓存的列表:
> ::walk kmem_cache 70036028 700362a8 70036528 700367a8 ...
这将产生对应于内核中每个 kmem 高速缓存的指针的列表。 要了解有关特定高速缓存的信息,请应用 kmem_cache 宏:
> 0x70039928$<kmem_cache 0x70039928: lock 0x70039928: owner/waiters 0 0x70039930: flags freelist offset 20f 707c86a0 24 0x7003993c: global_alloc global_free alloc_fail 523 0 0 0x70039948: hash_shift hash_mask hash_table 5 1ff 70444858 0x70039954: nullslab 0x70039954: cache base next 70039928 0 702d5de0 0x70039960: prev head tail 707c86a0 0 0 0x7003996c: refcnt chunks -1 0 0x70039974: constructor destructor reclaim 0 0 0 0x70039980: private arena cflags 0 104444f8 0 0x70039994: bufsize align chunksize 24 8 40 0x700399a0: slabsize color maxcolor 8192 24 32 0x700399ac: slab_create slab_destroy buftotal 3 0 612 0x700399b8: bufmax rescale lookup_depth 612 1 0 0x700399c4: kstat next prev 702c8608 70039ba8 700396a8 0x700399d0: name kmem_alloc_24 0x700399f0: bufctl_cache magazine_cache magazine_size 70037ba8 700367a8 15 ...
用于调试的重要字段包括 'bufsize'、'flags' 和 'name'。kmem_cache 的名称(在本示例中为 "kmem_alloc_24")指明了它在系统中的用途。 Bufsize 表示此高速缓存中每个缓冲区的大小;在本示例中,高速缓存用于进行大小为 24 或更小的分配。 'flags' 指明了为此高速缓存启用的调试功能。 可以找到在 <sys/kmem_impl.h> 中列出的调试标志。 在本示例中 'flags' 为 0x20f,即 KMF_AUDIT | KMF_DEADBEEF | KMF_REDZONE | KMF_CONTENTS | KMF_HASH。 本文档将在后续几节中说明每个调试功能。
如果有兴趣查看特定高速缓存中的缓冲区,可以直接遍历该高速缓存中已分配和已释放的缓存区:
> 0x70039928::walk kmem 704ba010 702ba008 704ba038 702ba030 ... > 0x70039928::walk freemem 70a9ae50 70a9ae28 704bb730 704bb2f8 ...
MDB 提供了一种为 kmem walker 提供高速缓存地址的快捷方式:此方式会为每个 kmem 高速缓存提供特定的 walker,并且 walker 与高速缓存同名。例如:
> ::walk kmem_alloc_24 704ba010 702ba008 704ba038 702ba030 ... > ::walk thread_cache 70b38080 70aac060 705c4020 70aac1e0 ...
现在,您已经知道如何迭代内核内存分配器的内部数据结构以及如何检查 kmem_cache 数据结构的最重要成员。
分配器的主要调试功能之一是它包括了用于快速识别数据损坏的算法。当检测到损坏时,分配器会导致系统立即出现故障。
本节介绍分配器如何识别数据损坏;您必须了解这一点才能调试这些问题。内存误用通常分为以下种类:
写入超出了缓冲区的结尾
访问未初始化的数据
继续使用已释放的缓冲区
损坏内核内存
阅读接下来的三节时,请牢记这些问题。 它们将有助于了解分配器的设计,并使您可以更有效地诊断问题。
如果在 kmem_cache 的 flags 字段中设置了 KMF_DEADBEEF (0x2) 位,则分配器会尝试通过将特殊模式写入所有已释放缓冲区中,从而使内存损坏易于检测。 此模式即是 0xdeadbeef。 由于典型的内存区域同时包含已分配的内存和已释放的内存,因此每种块的各个节将是分散的;以下是来自 "kmem_alloc_24" 高速缓存的一个示例:
0x70a9add8: deadbeef deadbeef 0x70a9ade0: deadbeef deadbeef 0x70a9ade8: deadbeef deadbeef 0x70a9adf0: feedface feedface 0x70a9adf8: 70ae3260 8440c68e 0x70a9ae00: 5 4ef83 0x70a9ae08: 0 0 0x70a9ae10: 1 bbddcafe 0x70a9ae18: feedface 139d 0x70a9ae20: 70ae3200 d1befaed 0x70a9ae28: deadbeef deadbeef 0x70a9ae30: deadbeef deadbeef 0x70a9ae38: deadbeef deadbeef 0x70a9ae40: feedface feedface 0x70a9ae48: 70ae31a0 8440c54e
从 0x70a9add8 开始的缓冲区是使用 0xdeadbeef 模式填充的,这就直接表明了缓冲区当前是空闲的。 另一个空闲缓冲区从 0x70a9ae28 开始;一个已分配的缓冲区介于它们之间,位于 0x70a9ae00。
您可能已经注意到此内存区域布局中有一些空洞,3 个 24 字节区域应该仅占用 72 字节的内存,而不是此处显示的 120 字节。 此差异将在下一节Redzone(禁区): 0xfeedface中进行说明。
模式 0xfeedface 频繁地在以上缓冲区中出现。 此模式称为 "redzone" 指示器。 通过此模式,分配器(以及调试问题的程序员)可以确定“错误”代码是否超出了缓冲区的边界。 redzone 后面是一些其他信息。 该数据的内容取决于其他因素(请参见内存分配日志记录)。 redzone 及其后缀统称为 buftag 区域。 图 9–1 概述了此信息。
如果在高速缓存中设置了 KMF_AUDIT、KMF_DEADBEEF 或 KMF_REDZONE 标志中的任何一个,则 buftag 会附加到该高速缓存的每个缓冲区。 buftag 的内容取决于是否设置了 KMF_AUDIT。
现在,可以轻易将上述内存区域分解为不同的缓冲区:
0x70a9add8: deadbeef deadbeef \ 0x70a9ade0: deadbeef deadbeef +- User Data (free) 0x70a9ade8: deadbeef deadbeef / 0x70a9adf0: feedface feedface -- REDZONE 0x70a9adf8: 70ae3260 8440c68e -- Debugging Data 0x70a9ae00: 5 4ef83 \ 0x70a9ae08: 0 0 +- User Data (allocated) 0x70a9ae10: 1 bbddcafe / 0x70a9ae18: feedface 139d -- REDZONE 0x70a9ae20: 70ae3200 d1befaed -- Debugging Data 0x70a9ae28: deadbeef deadbeef \ 0x70a9ae30: deadbeef deadbeef +- User Data (free) 0x70a9ae38: deadbeef deadbeef / 0x70a9ae40: feedface feedface -- REDZONE 0x70a9ae48: 70ae31a0 8440c54e -- Debugging Data
在位于 0x70a9add8 和 0x70a9ae28 的空闲缓冲区中,redzone 是使用 0xfeedfacefeedface 填充的。这是确定缓冲区是否空闲的便利方法。
在从 0x70a9ae00 开始的已分配缓冲区中,情况是不同的。请回想一下分配器基础知识,有两种分配类型:
1) 客户机使用 kmem_cache_alloc() 请求的内存,在这种情况下所请求缓冲区的大小等于高速缓存的 bufsize。
2) 客户机使用 kmem_alloc (9F) 请求的内存,在这种情况下所请求缓冲区的大小小于或等于高速缓存的 bufsize。 例如,对 20 个字节的请求将从 kmem_alloc_24 高速缓存得到满足。 分配器会通过在紧邻客户机数据之后放置一个标记(即 redzone 字节)来强制设置缓冲区边界:
0x70a9ae00: 5 4ef83 \ 0x70a9ae08: 0 0 +- User Data (allocated) 0x70a9ae10: 1 bbddcafe / 0x70a9ae18: feedface 139d -- REDZONE 0x70a9ae20: 70ae3200 d1befaed -- Debugging Data
位于 0x70a9ae18 的 0xfeedface 后跟一个 32 位的字,其中包含的内容看似一个随机值。 此数字实际上是缓冲区大小的编码表示形式。 要对此数字进行解码并获得已分配缓冲区的大小,请使用以下公式:
size = redzone_value / 251
因此,在本示例中,
size = 0x139d / 251 = 20 bytes.
这表明所请求缓冲区的大小为 20 个字节。 分配器将执行此解码操作,同时会发现 redzone 字节应该位于偏移量为 20 的位置。redzone 字节是十六进制模式 0xbb,如预期的那样存在于 0x729084e4 (0x729084d0 + 0t20)。
图 9–3 说明了此内存布局的常规形式。
如果分配大小等于高速缓存的 bufsize,则 redzone 字节会覆写 redzone 本身的第一个字节,如图 9–4 中所示。
此覆写操作会导致 redzone 的第一个 32 位字为 0xbbedface 或 0xfeedfabb,具体取决于系统运行的硬件的字节存储顺序。
为什么分配大小以该方式进行编码? 要对大小进行编码,分配器可使用公式(251 * 大小 + 1)。对大小进行解码时,整数除法将废弃余数 '+1'。 但是,加上 1 是有价值的,因为分配器可以通过测试(大小 % 251 == 1)是否成立来检查大小是否有效。 这样,分配器可防止损坏 redzone 字节索引。
您可能想知道在用 redzone 字节覆写字中的第一个字节之前,地址 0x729084d4 上的可疑 0xbbddcafe 是什么。 它是 0xbaddcafe。如果在高速缓存中设置了 KMF_DEADBEEF 标志,则使用 0xbaddcafe 模式填充已分配但未初始化的内存。 分配器执行分配时,会循环通过缓冲区的各个字并验证每个字是否包含 0xdeadbeef,然后使用 0xbaddcafe 填充该字。
系统可能会发出以下故障消息:
panic[cpu1]/thread=e1979420: BAD TRAP: type=e (Page Fault) rp=ef641e88 addr=baddcafe occurred in module "unix" due to an illegal access to a user address
在这种情况下,导致故障的地址是 0xbaddcafe: 出现故障的线程访问了一些从未初始化的数据。
内核内存分配器会对应于之前所述的失败模式发出故障消息。 例如,系统可能会发出以下故障消息:
kernel memory allocator: buffer modified after being freed modification occurred at offset 0x30
由于分配器会尝试验证是否使用 0xdeadbeef 填充了不确定的缓冲区,因此能够检测到此情况。如果偏移的位置为 0x30,则不符合此条件。 由于此条件表明内存损坏,因此分配器会导致系统出现故障。
以下是另一个故障消息示例:
kernel memory allocator: redzone violation: write past end of buffer
由于分配器会尝试验证 redzone 字节 (0xbb) 是否处于它通过 redzone 大小编码所确定的位置,因此能够检测到此情况。 它无法在正确的位置找到签名字节。 由于这表明内存损坏,因此分配器会导致系统出现紧急情况。 其他的分配器故障消息将在稍后讨论。
本节说明内核内存分配器的日志记录功能以及如何使用它们调试系统崩溃。
如前所述,每个 buftag 的后半部分包含了有关对应缓冲区的额外信息。 此数据中一部分是调试信息,一部分是分配器的专用数据。 尽管此辅助数据可以采用几种不同的形式,但是它统称为“缓冲区控制”或 bufctl 数据。
不过,分配器需要知道缓冲区的 bufctl 指针是否有效,因为该指针也可能由于异常代码而损坏。 分配器通过存储其辅助指针和该指针的编码版本,然后交叉检查这两个版本来确认该指针的完整性。
如图 9–5 中所示,这些指针是 bcp(缓冲区控制指针)和 bxstat(缓冲区控制 XOR 状态)。 分配器会对 bcp 和 bxstat 进行排列,以便表达式 bcp XOR bxstat 等于已知值。
这两个指针中的一个或两个损坏时,分配器可以轻易检测到此类损坏,并会导致系统出现紧急情况。分配缓冲区后,bcp XOR bxstat = 0xa110c8ed ("allocated")。缓冲区空闲时,bcp XOR bxstat = 0xf4eef4ee ("freefree")。
您可能会发现重新检查检查已释放的缓冲区: 0xdeadbeef中的示例会有助于确认那里显示的 buftag 指针是否一致。
分配器找到损坏的 buftag 时,会导致系统出现紧急情况,并生成与以下内容类似的消息:
kernel memory allocator: boundary tag corrupted bcp ^ bxstat = 0xffeef4ee, should be f4eef4ee
请记住,如果 bcp 已损坏,仍可通过采用 bxstat XOR 0xf4eef4ee 或 bxstat XOR 0xa110c8ed(取决于缓冲区是已分配的还是空闲的)的值来对其值进行检索。
bufctl
指针包含在 buftag 区域中的缓冲区控制 (bufctl) 指针可以具有不同的含义,具体取决于高速缓存的 kmem_flags。 需要特别注意的是,KMF_AUDIT 标志的设置情况不同,具体的行为也会有所不同:如果未设置 KMF_AUDIT 标志,则内核内存分配器会为每个缓冲区分配一个 kmem_bufctl_t 结构。 此结构包含有关每个缓冲区的一些最少记帐信息。 如果已 设置了 KMF_AUDIT 标志,则分配器会改为分配 kmem_bufctl_audit_t(kmem_bufctl_t 的扩展版本)。
本节假定已设置了 KMF_AUDIT 标志。对于未设置此位的高速缓存,可用的调试信息量会减少。
kmem_bufctl_audit_t(简称 bufctl_audit)包含有关此缓冲区中执行的最后一个事务的其他信息。以下示例说明了如何应用 bufctl_audit 宏检查审计记录。 所示的缓冲区是检测内存损坏中使用的示例缓冲区:
> 0x70a9ae00,5/KKn 0x70a9ae00: 5 4ef83 0 0 1 bbddcafe feedface 139d 70ae3200 d1befaed
使用如上所述的方法可以很容易地看到 0x70ae3200 指向 bufctl_audit 记录:它是 redzone 后面的第一个指针。 要检查它所指向的 bufctl_audit 记录,请应用 bufctl_audit 宏:
> 0x70ae3200$<bufctl_audit 0x70ae3200: next addr slab 70378000 70a9ae00 707c86a0 0x70ae320c: cache timestamp thread 70039928 e1bd0e26afe 70aac4e0 0x70ae321c: lastlog contents stackdepth 7011c7c0 7018a0b0 4 0x70ae3228: kmem_zalloc+0x30 pid_assign+8 getproc+0x68 cfork+0x60
'addr' 字段是对应于此 bufctl_audit 记录的缓冲区的地址。 以下是原始地址:0x70a9ae00。'cache' 字段是指已分配此缓冲区的 kmem_cache。可以使用 ::kmem_cache dcmd 对其进行检查,如下所示:
> 0x70039928::kmem_cache ADDR NAME FLAG CFLAG BUFSIZE BUFTOTL 70039928 kmem_alloc_24 020f 000000 24 612
'timestamp' 字段表示执行此事务的时间。此时间的表示方式与 gethrtime(3C) 相同。
'thread' 是指向线程的指针,该线程在此缓冲区中执行了最后一个事务。 'lastlog' 和 'contents' 指针指向分配器的事务日志中的位置。 这些日志将在分配器日志记录工具中详细讨论。
通常,bufctl_audit 提供的最有用的信息段是事务发生时记录的栈跟踪。 在这种情况下,事务是在执行 fork(2) 的过程中调用的分配。
本节介绍用于执行高级内存分析(包括查找内存泄漏和数据损坏原因)的工具。
对于启用了完整 kmem 调试功能集的内核崩溃转储,::findleaks dcmd 提供了提供了强大高效的内存泄漏检测。 首次执行 ::findleaks 时会处理转储中的内存泄漏(这可能需要几分钟),然后根据分配栈跟踪合并泄漏。 findleaks 报告会针对所识别的每个内存泄漏显示一个 bufctl 地址和最顶层的栈帧:
> ::findleaks CACHE LEAKED BUFCTL CALLER 70039ba8 1 703746c0 pm_autoconfig+0x708 70039ba8 1 703748a0 pm_autoconfig+0x708 7003a028 1 70d3b1a0 sigaddq+0x108 7003c7a8 1 70515200 pm_ioctl+0x187c ------------------------------------------------------ Total 4 buffers, 376 bytes
使用 bufctl 指针可以通过应用 bufctl_audit 宏获取分配的完整栈反向跟踪:
> 70d3b1a0$<bufctl_audit 0x70d3b1a0: next addr slab 70a049c0 70d03b28 70bb7480 0x70d3b1ac: cache timestamp thread 7003a028 13f7cf63b3 70b38380 0x70d3b1bc: lastlog contents stackdepth 700d6e60 0 5 0x70d3b1c8: kmem_alloc+0x30 sigaddq+0x108 sigsendproc+0x210 sigqkill+0x90 kill+0x28
程序员通常可以使用 bufctl_audit 信息和分配栈跟踪快速找到泄漏给定缓冲区的代码路径。
尝试诊断内存损坏问题时,应该知道其他哪些内核实体包含特定指针的副本。这一点非常重要,因为这可以表明哪个线程在被释放后访问了数据结构。 另外,还可以更轻松地了解哪些内核实体正在共享特定(有效)数据项的信息。 ::whatis 和 ::kgrep dcmd 可以用于回答这些问题。 可以对相关值应用 ::whatis:
> 0x705d8640::whatis 705d8640 is 705d8640+0, allocated from streams_mblk
在本示例中,表明 0x705d8640 是指向 STREAMS mblk 结构的指针。 要查看整个分配树,请改用 ::whatis -a:
> 0x705d8640::whatis -a 705d8640 is 705d8640+0, allocated from streams_mblk 705d8640 is 705d8000+640, allocated from kmem_va_8192 705d8640 is 705d8000+640 from kmem_default vmem arena 705d8640 is 705d2000+2640 from kmem_va vmem arena 705d8640 is 705d2000+2640 from heap vmem arena
这表明分配也会在 kmem_va_8192 高速缓存(即面向 kmem_va vmem 块的 kmem 高速缓存)中进行。 它还显示了 vmem 分配的完整栈。
kmem 高速缓存和 vmem 块的完整列表通过 ::kmastat dcmd 显示。 可以使用 ::kgrep 查找包含指向此 mblk 的指针的其他内核地址。这说明了系统中内存分配的分层性质;通常,可以根据最具体 kmem 高速缓存的名称确定给定地址所引用的对象类型。
> 0x705d8640::kgrep 400a3720 70580d24 7069d7f0 706a37ec 706add34
并通过再次应用 ::whatis 对其进行检查:
> 400a3720::whatis 400a3720 is in thread 7095b240's stack > 706add34::whatis 706add34 is 706add20+14, allocated from streams_dblk_120
在这里一个指针位于已知内核线程的栈上,另一个指针 mblk 位于对应的 STREAMS dblk 结构的内部。
MDB 的 ::kmem_verify dcmd 在运行时可实现大多数 kmem 分配器实现的检查。可以调用 ::kmem_verify 以便扫描每个具有相应 kmem_flags 的 kmem 内存,或者检查特定的高速缓存。
以下是使用 ::kmem_verify 确定问题的示例:
> ::kmem_verify Cache Name Addr Cache Integrity kmem_alloc_8 70039428 clean kmem_alloc_16 700396a8 clean kmem_alloc_24 70039928 1 corrupt buffer kmem_alloc_32 70039ba8 clean kmem_alloc_40 7003a028 clean kmem_alloc_48 7003a2a8 clean ...
在这里可以很容易地看到 kmem_alloc_24 高速缓存包含 ::kmem_verify 所确认的问题。使用显式的高速缓存参数,::kmem_verify dcmd 可以提供有关该问题的更详细信息:
> 70039928::kmem_verify Summary for cache 'kmem_alloc_24' buffer 702babc0 (free) seems corrupted, at 702babc0
下一步是检查 ::kmem_verify 确认已损坏的缓冲区:
> 0x702babc0,5/KKn 0x702babc0: 0 deadbeef deadbeef deadbeef deadbeef deadbeef feedface feedface 703785a0 84d9714e
::kmem_verify 标记此缓冲区的原因此时非常明显:缓冲区中的第一个字(位于 0x702babc0)可能应该使用 0xdeadbeef 模式而不是 0 填充。 此时,为此缓冲区检查 bufctl_audit 可能产生有关最近向缓冲区写入了哪些代码的线索,指明释放此缓冲区的位置和时间。
此情况下的另一种有用方法是使用 ::kgrep 在地址空间中搜索对地址 0x702babc0 的引用,以便发现哪些线程或数据仍然包含对此已释放数据的引用。
如果为高速缓存设置了 KMF_AUDIT,则内核内存分配器会对记录其最近活动历史的日志进行维护。此事务日志记录的是 bufctl_audit 记录。如果同时设置了 KMF_AUDIT 和 KMF_CONTENTS 标志,则分配器会生成一个内容日志,其中记录了已分配和已释放缓冲区的部分实际内容。 内容日志的结构和用法不在本文档的讨论范围之内。本节将讨论事务日志。
MDB 提供了用于显示事务日志的多种工具。最简单的工具是 ::walk kmem_log,用于将日志中的事务作为一系列 bufctl_audit_t 指针进行列显:
> ::walk kmem_log 70128340 701282e0 70128280 70128220 701281c0 ... > 70128340$<bufctl_audit 0x70128340: next addr slab 70ac1d40 70bc4ea8 70bb7c00 0x7012834c: cache timestamp thread 70039428 e1bd7abe721 70aacde0 0x7012835c: lastlog contents stackdepth 701282e0 7018f340 4 0x70128368: kmem_cache_free+0x24 nfs3_sync+0x3c vfs_sync+0x84 syssync+4
查看整个事务日志的更好方法是使用 ::kmem_log 命令:
> ::kmem_log CPU ADDR BUFADDR TIMESTAMP THREAD 0 70128340 70bc4ea8 e1bd7abe721 70aacde0 0 701282e0 70bc4ea8 e1bd7aa86fa 70aacde0 0 70128280 70bc4ea8 e1bd7aa27dd 70aacde0 0 70128220 70bc4ea8 e1bd7a98a6e 70aacde0 0 701281c0 70d03738 e1bd7a8e3e0 70aacde0 ... 0 70127140 70cf78a0 e1bd78035ad 70aacde0 0 701270e0 709cf6c0 e1bd6d2573a 40033e60 0 70127080 70cedf20 e1bd6d1e984 40033e60 0 70127020 70b09578 e1bd5fc1791 40033e60 0 70126fc0 70cf78a0 e1bd5fb6b5a 40033e60 0 70126f60 705ed388 e1bd5fb080d 40033e60 0 70126f00 705ed388 e1bd551ff73 70aacde0 ...
::kmem_log 的输出按时间标记进行降序排列。ADDR 列是对应于该事务的 bufctl_audit 结构;BUFADDR 指向实际缓冲区。
这些数字表示(分配和释放的)缓冲区中的事务。如果特定的缓冲区损坏,则在事务日志中找到该缓冲区,然后确定在其他哪些事务中涉及执行事务的线程可能会有所帮助。 这有助于对分配(或释放)缓冲区前后所发生的事件的顺序有一个完整的了解。
可以使用 ::bufctl 命令过滤遍历事务日志的输出。 ::bufctl -a 命令用于按缓冲区地址过滤事务日志中的缓冲区。 以下是对缓冲区 0x70b09578 进行过滤的示例:
> ::walk kmem_log | ::bufctl -a 0x70b09578 ADDR BUFADDR TIMESTAMP THREAD CALLER 70127020 70b09578 e1bd5fc1791 40033e60 biodone+0x108 70126e40 70b09578 e1bd55062da 70aacde0 pageio_setup+0x268 70126de0 70b09578 e1bd52b2317 40033e60 biodone+0x108 70126c00 70b09578 e1bd497ee8e 70aacde0 pageio_setup+0x268 70120480 70b09578 e1bd21c5e2a 70aacde0 elfexec+0x9f0 70120060 70b09578 e1bd20f5ab5 70aacde0 getelfhead+0x100 7011ef20 70b09578 e1bd1e9a1dd 70aacde0 ufs_getpage_miss+0x354 7011d720 70b09578 e1bd1170dc4 70aacde0 pageio_setup+0x268 70117d80 70b09578 e1bcff6ff27 70bc2480 elfexec+0x9f0 70117960 70b09578 e1bcfea4a9f 70bc2480 getelfhead+0x100 ...
本示例说明一个特定的缓冲区可以在许多事务中使用。
请记住,kmem 事务日志是内核内存分配器生成的事务不完整记录。会根据需要删除日志中的较旧项,以便保持日志大小不变。
::allocdby 和 ::freedby dcmd 提供了汇总与特定线程关联的事务的便利方法。 以下示例列出了线程 0x70aacde0 最近执行的分配:
> 0x70aacde0::allocdby BUFCTL TIMESTAMP CALLER 70d4d8c0 e1edb14511a allocb+0x88 70d4e8a0 e1edb142472 dblk_constructor+0xc 70d4a240 e1edb13dd4f allocb+0x88 70d4e840 e1edb13aeec dblk_constructor+0xc 70d4d860 e1ed8344071 allocb+0x88 70d4e7e0 e1ed8342536 dblk_constructor+0xc 70d4a1e0 e1ed82b3a3c allocb+0x88 70a53f80 e1ed82b0b91 dblk_constructor+0xc 70d4d800 e1e9b663b92 allocb+0x88
通过检查 bufctl_audit 记录,可以了解特定线程最近的活动。