关于原子表

原子表是一个系统定义的表,用于存储字符串和相应的标识符。 应用程序将字符串放在原子表中,并接收一个名为原子的 16 位整数,该整数可用于访问字符串。 已放置在原子表中的字符串称为原子名称

系统提供了许多原子表。 每个原子表都有不同的用途。 例如,动态数据交换 (DDE) 应用程序使用全局原子表与其他应用程序共享项名称和主题名称字符串。 DDE 应用程序将全局原子传递到其合作伙伴应用程序,而不是传递实际字符串。 合作伙伴使用原子从原子表获取字符串。

应用程序可以使用本地原子表来存储其自己的项名称关联。

系统使用应用程序无法直接访问的原子表。 但是,应用程序将在调用各种函数时使用这些原子。 例如,已注册剪贴板格式存储在系统使用的内部原子表中。 应用程序使用 RegisterClipboardFormat 函数将原子添加到此原子表。 此外,已注册类存储在系统使用的内部原子表中。 应用程序使用 RegisterClassRegisterClassEx 函数将原子添加到此原子表。

以下是本节中要讨论的主题。

全局原子表

全局原子表适用于所有应用程序。 当应用程序将字符串放在全局原子表中时,系统会生成在整个系统中唯一的原子。 具有原子的任何应用程序都可以通过查询全局原子表来获取其标识的字符串。

若应用程序定义用于与其他应用程序共享数据的专用 DDE 数据格式,则应在全局原子表中放置格式名称。 此方法可防止与系统或其他应用程序定义的格式名称冲突,并使消息或格式的标识符(原子)可用于其他应用程序。

用户原子表

除了全局原子表外,用户原子表也是另一个系统原子表,也是在所有进程中共享。 用户原子表用于少量 win32k 内部方案;例如,windows 模块名称、win32k 中的已知字符串、OLE 格式等。尽管应用程序不直接与用户原子表交互,但它们会调用多个 API(例如 RegisterClassRegisterWindowMessageRegisterClipboardFormat),这些 API 会将条目添加到用户原子表。 UnregisterClass 可以删除 RegisterClass 添加的条目。 但是,在会话结束之前,不会删除 RegisterWindowMessageRegisterClipboardFormat 添加的条目。 如果用户原子表没有更多空间,并且传入的字符串尚未在表中,则调用将失败。

原子表大小

许多关键 API(包括 CreateWindow)都依赖于用户原子。 因此,用户原子表中的空间耗尽将导致严重问题:例如,所有应用程序可能都无法启动。 下面是一些建议,以确保应用程序高效利用原子表,并保留应用程序和系统的可靠性和性能:

  1. 应限制应用对用户原子表的使用。 可在用户原子表中使用 RegisterClassRegisterWindowMessageRegisterClipboardFormat 等 AIP 存储唯一字符串,而其他应用可全局使用用户原子表全局来使用字符串注册窗口类。 如果可能,应使用 AddAtom/DeleteAtom 将字符串存储在本地原子表中;如果需要原子跨进程,应使用 GlobalAddAtom/GlobalDeleteAtom

  2. 如果担心应用程序导致用户原子表出现问题,可以通过连接内核调试器并在调用 UserAddAtomEx (bae1 win32kbase!UserAddAtomEx /p <eprocess> "kc10;g") 时中断进程来调查根本原因。 在调用堆栈上查找 user32!,以查看正在调用的 API。 该方法类似于标识全局原子表泄漏中介绍的全局原子表问题检测。 转储用户原子表内容的另一种方法是在 0xC000 到 0xFFFF 的可能原子范围上调用 GetClipboardFormatName。 如果原子总数在应用程序正在运行时稳定增加或在关闭应用时未返回基线,则表示存在问题。

本地原子表

应用程序可以使用本地原子表高效地管理仅在应用程序中使用的大量字符串。 这些字符串和关联的原子仅适用于创建表的应用程序。

在许多结构中需要相同字符串的应用程序可以使用本地原子表来减少内存使用量。 应用程序可以将字符串放置在原子表中,并在结构中包含生成的原子,而不是将字符串复制到每个结构中。 这样,一个字符串在内存中仅显示一次,但可以在应用程序中使用多次。

在搜索特定字符串时,应用程序还可以使用本地原子表节省时间。 若要执行搜索,应用程序只需将搜索字符串放在原子表中,并将生成的原子与相关结构中的原子进行比较。 比较原子通常比比较字符串快。

原子表作为哈希表实现。 默认情况下,本地原子表对其哈希表使用 37 个存储桶。 但是,可以调用 InitAtomTable 函数来更改使用的存储桶数。 但是,如果应用程序调用 InitAtomTable,则必须在调用任何其他原子管理函数之前执行此操作。

原子类型

应用程序可以创建两种类型的原子:字符串原子和整数原子。 整数原子和字符串原子的值不会重叠,因此可以在同一代码块中同时使用这两种类型的原子。

多个函数接受字符串或原子作为参数。 将原子传递给这些函数时,应用程序可以使用 MAKEINTATOM 宏将原子转换为函数可以使用的形式。

以下几个部分介绍了原子类型。

字符串原子

当应用程序将以 null 结尾的字符串传递到 GlobalAddAtomAddAtomGlobalFindAtomFindAtom 函数时,它们将因此收到字符串原子(16 位整数)。 字符串原子具有以下属性:

  • 字符串原子的值在 0xC000 (MAXINTATOM) 到 0xFFFF 的范围内。
  • 在原子表中搜索原子名称时,大小写并不重要。 此外,整个字符串必须在搜索操作中匹配;不执行子字符串匹配。
  • 与字符串原子关联的字符串的大小不能超过 255 字节。 此限制适用于所有原子函数。
  • 引用计数与每个原子名称相关联。 每次将原子名称添加到表中时,都将递增计数,而每次从表中删除原子名称时,都将递减计数。 这样可以防止同一字符串原子的不同用户销毁彼此的原子名称。 当原子名称的引用计数等于零时,系统会从表中删除原子和原子名称。

整数原子

整数原子与字符串原子存在以下不同之处:

  • 整数原子的值在 0x0001 到 0xBFFF (MAXINTATOM– 1) 的范围内。
  • 整数原子的字符串表示形式为 #dddd,其中 dddd 表示的值是十进制数字。 将忽略前导零。
  • 没有与整数原子关联的引用计数或存储开销。

原子创建和使用计数

应用程序可调用 AddAtom 函数来创建本地原子;并且可调用 GlobalAddAtom 函数来创建全局原子。 这两个函数都需要指向字符串的指针。 系统会在相应的原子表中搜索字符串,并将相应的原子返回给应用程序。 对于字符串原子,如果字符串已驻留在原子表中,则系统在此过程中将递增字符串的引用计数。

用于添加相同原子名称的重复调用会返回相同的原子。 如果在调用 AddAtom 时表中不存在原子名称,则将原子名称添加到表中,并返回一个新原子。 如果是字符串原子,则也将其引用计数设置为 1。

当应用程序不再需要使用本地原子时,应用程序应调用 DeleteAtom 函数;当不再需要全局原子时,应调用 GlobalDeleteAtom 函数。 对于字符串原子,其中任一函数都会将相应原子的引用计数减少 1。 当引用计数达到零时,系统会从表中删除原子名称。

只要字符串原子的引用计数大于零,字符串原子的原子名称就会保留在全局原子表中,即使将其放置在表中的应用程序终止也是如此。 当关联的应用程序终止时,将销毁本地原子表,而不考虑表中原子的引用计数。

原子表查询

应用程序可以使用 FindAtomGlobalFindAtom 函数来确定特定字符串是否已在原子表中。 这些函数会在原子表中搜索指定的字符串,如果字符串存在,则返回对应的原子。

应用程序可以使用 GetAtomNameGlobalGetAtomName 函数从原子表中检索原子名称字符串,前提是应用程序具有与所查找的字符串对应的原子。 这两个函数都会将指定原子的原子名称字符串复制到缓冲区,并返回复制的字符串的长度。 GetAtomName 会从本地原子表检索原子名称字符串,而 GlobalGetAtomName 会从全局原子表检索原子名称字符串。

原子字符串格式

AddAtomGlobalAddAtomFindAtomGlobalFindAtom 函数将采用指向以 null 结尾的字符串的指针。 应用程序可以通过以下一种方式指定此指针。

字符串格式 说明
#dddd 指定为十进制字符串的整数。 用于创建或查找整数原子。
字符串原子名称 一个字符串原子名称。 用于将字符串原子名称添加到原子表和接收返回的原子。