Solaris 模块调试器指南

第 9 章 使用内核内存分配器进行调试

Solaris 内核内存 (kmem) 分配器提供了一组强大的调试功能,可以简化内核崩溃转储的分析。本章将讨论这些调试功能以及专门为分配器设计的 MDB dcmd 和 walker。Bonwick(请参见相关书籍和文章)概述了分配器本身的原理。有关分配器数据结构的定义,请参阅头文件 <sys/kmem_impl.h>。可以在产品化的系统中启用 kmem 调试功能以增强问题分析,或者在开发系统中启用它以协助调试内核软件和设备驱动程序。


注意 –

本指南反映了 Solaris 10 实现;此信息对于过去或将来的发行版可能不相关、不正确或不适用,因为它反映的是当前的内核实现。它未定义任何类型的公共接口。所提供的有关内核内存分配器的所有信息在将来的 Solaris 发行版中可能会更改。


入门:创建崩溃转储样例

本节说明如何获取崩溃转储样例以及如何调用 MDB 对其进行检查。

设置 kmem_flags

内核内存分配器包含许多高级调试功能,但是由于这些功能可能会导致性能下降,因此缺省情况下并未启用。为了理解本指南中的示例,您应该启用这些功能。应仅在测试系统中启用这些功能,因为它们可能会导致性能下降或暴露潜在的问题。

分配器的调试功能由 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 并检查其状态:

$ 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_8kmem_alloc_16 高速缓存。 在这种情况下,kmem_alloc_16 高速缓存可处理大小为 9-16 个字节内存的所有客户端请求。 请记住,无论客户端请求的大小是多少,kmem_alloc_16 高速缓存中每个缓冲区的大小均为 16 个字节。对于大小为 14 个字节的请求,所得到缓冲区中有两个字节是未使用的,因为该请求是从 kmem_alloc_16 高速缓存得到满足的。

最后一组高速缓存是指由内核内存分配器在内部使用以对其自身进行记录的高速缓存。 这包括名称以 "kmem_magazine_" 或 "kmem_va_"、kmem_slab_cachekmem_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 数据结构的最重要成员。

检测内存损坏

分配器的主要调试功能之一是它包括了用于快速识别数据损坏的算法。当检测到损坏时,分配器会导致系统立即出现故障。

本节介绍分配器如何识别数据损坏;您必须了解这一点才能调试这些问题。内存误用通常分为以下种类:

阅读接下来的三节时,请牢记这些问题。 它们将有助于了解分配器的设计,并使您可以更有效地诊断问题。

检查已释放的缓冲区: 0xdeadbeef

如果在 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中进行说明。


Redzone(禁区): 0xfeedface

模式 0xfeedface 频繁地在以上缓冲区中出现。 此模式称为 "redzone" 指示器。 通过此模式,分配器(以及调试问题的程序员)可以确定“错误”代码是否超出了缓冲区的边界。 redzone 后面是一些其他信息。 该数据的内容取决于其他因素(请参见内存分配日志记录)。 redzone 及其后缀统称为 buftag 区域。 图 9–1 概述了此信息。

图 9–1 Redzone

通过上下文说明的图形。

如果在高速缓存中设置了 KMF_AUDITKMF_DEADBEEFKMF_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

在位于 0x70a9add80x70a9ae28 的空闲缓冲区中,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

位于 0x70a9ae180xfeedface 后跟一个 32 位的字,其中包含的内容看似一个随机值。 此数字实际上是缓冲区大小的编码表示形式。 要对此数字进行解码并获得已分配缓冲区的大小,请使用以下公式:

size = redzone_value / 251

因此,在本示例中,

size = 0x139d / 251 = 20 bytes.

这表明所请求缓冲区的大小为 20 个字节。 分配器将执行此解码操作,同时会发现 redzone 字节应该位于偏移量为 20 的位置。redzone 字节是十六进制模式 0xbb,如预期的那样存在于 0x729084e4 (0x729084d0 + 0t20)

图 9–2 kmem_alloc(9F) 缓冲区样例

此图形描述 kmem_alloc 缓冲区样例。redzone 字节、未初始化的数据和调试数据均已标记。

图 9–3 说明了此内存布局的常规形式。

图 9–3 Redzone 字节

此图形说明了在用户数据区域结尾后面写入的 redzone 字节。redzone 字节是通过解码索引确定的。

如果分配大小等于高速缓存的 bufsize,则 redzone 字节会覆写 redzone 本身的第一个字节,如图 9–4 中所示。

图 9–4 Redzone 开头的 Redzone 字节

通过上下文说明的图形。

此覆写操作会导致 redzone 的第一个 32 位字为 0xbbedface0xfeedfabb,具体取决于系统运行的硬件的字节存储顺序。


注意 –

为什么分配大小以该方式进行编码? 要对大小进行编码,分配器可使用公式(251 * 大小 + 1)。对大小进行解码时,整数除法将废弃余数 '+1'。 但是,加上 1 是有价值的,因为分配器可以通过测试(大小 % 251 == 1)是否成立来检查大小是否有效。 这样,分配器可防止损坏 redzone 字节索引。


未初始化的数据: 0xbaddcafe

您可能想知道在用 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 数据完整性

如前所述,每个 buftag 的后半部分包含了有关对应缓冲区的额外信息。 此数据中一部分是调试信息,一部分是分配器的专用数据。 尽管此辅助数据可以采用几种不同的形式,但是它统称为“缓冲区控制”或 bufctl 数据。

不过,分配器需要知道缓冲区的 bufctl 指针是否有效,因为该指针也可能由于异常代码而损坏。 分配器通过存储其辅助指针该指针的编码版本,然后交叉检查这两个版本来确认该指针的完整性。

图 9–5 中所示,这些指针是 bcp(缓冲区控制指针)和 bxstat(缓冲区控制 XOR 状态)。 分配器会对 bcp 和 bxstat 进行排列,以便表达式 bcp XOR bxstat 等于已知值。

图 9–5 buftag 中的额外调试数据

图形由上下文说明。

这两个指针中的一个或两个损坏时,分配器可以轻易检测到此类损坏,并会导致系统出现紧急情况。分配缓冲区后,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 0xf4eef4eebxstat XOR 0xa110c8ed(取决于缓冲区是已分配的还是空闲的)的值来对其值进行检索。

bufctl 指针

包含在 buftag 区域中的缓冲区控制 (bufctl) 指针可以具有不同的含义,具体取决于高速缓存的 kmem_flags。 需要特别注意的是,KMF_AUDIT 标志的设置情况不同,具体的行为也会有所不同:如果设置 KMF_AUDIT 标志,则内核内存分配器会为每个缓冲区分配一个 kmem_bufctl_t 结构。 此结构包含有关每个缓冲区的一些最少记帐信息。 如果 设置了 KMF_AUDIT 标志,则分配器会改为分配 kmem_bufctl_audit_tkmem_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 结构的内部。

使用 ::kmem_verify 查找损坏的缓冲区

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_AUDITKMF_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 记录,可以了解特定线程最近的活动。