说明如何使用 Informix C DataBlade API 内存管理函数。
简介
UDR 不能调用标准操作系统内存分配函数,如 malloc()
、 calloc()
、 realloc()
和 free()
。 关于体系结构问题的详细说明,请参阅 对用于 DataBlade 模块的限制进行编码 技术说明。
DataBlade API 提供两个让 UDR 从 IDS 共享内存动态分配内存的函数集:
mi_alloc()
mi_zalloc()
mi_dalloc()
mi_free()
这些函数比已命名的内存函数更常用,并适合于在仅有一条 SQL 语句的上下文中执行的 UDR。 在查询完成后,服务器自动释放由 UDR 分配的内存。
下面的“ 用户函数(mi_alloc) ”一节提供了关于如何使用它们的示例。
mi_named_alloc()
mi_named_zalloc()
mi_named_get()
mi_named_free()
已命名的内存函数所分配的内存,可由多个 UDR 访问,这些 UDR 在(可能)有多条 SQL 语句的上下文中执行。 当查询完成时,内存不被释放,而是仍然保持可用。(下面的“ 如何以及何时释放内存 ”一节描述了如何释放它。) 已命名的内存可能要求您通过多线程来管理并行访问。
下面的“ 已命名的内存函数(mi_named_alloc) ”一节显示了如何使用它们,“ 管理并发性 ”一节说明了如何通过多线程管理并行访问。
本技术说明的目标是阐明何时使用用户函数以及何时使用已命名的内存函数。
因为 DataBlade API 文档只涵盖公用函数,所以本技术说明的先前版本将内存函数的类别划分为公用和半公用函数。 从 IBM Informix DataBlade API Programmer's Manual,版本 9.3 开始,这两个函数集分别被记录和标识为“用户内存”函数和“已命名的内存”函数。 本技术说明现在采用该约定。 (可以从 IBM Informix 在线文档网站 下载手册。)
如果不用 mi_* 函数会发生什么呢?
malloc()
和其它 OS 内存函数引起的问题包括:
在执行 UDR 的虚拟处理器(VP)堆上分配内存。
如果线程切换到另一个 VP,则该内存的指针将失效。
因为其它 VP 不知道该内存是用 malloc()
分配的,所以它们可能在扩展虚拟内存池时使用同一地址空间,导致重叠的内存分配。
内存没有被释放,导致内存泄漏。 例如,UDR 的返回值如果大于 4 字节就必须动态地分配。
如果您使用 mi_*
函数,那么,当不再需要返回值时服务器会自动释放它。 还有,如果在 UDR 执行期间出现任何异常,则服务器释放内存。
如果您使用 malloc()
,则在服务器使用完该值后不会释放它。而且发生异常时也不会释放它。
请参阅下面的“ 关于这个主题的更多信息 ”一节,以获得详细描述该问题的 IDN 白皮书列表。
![]() ![]() |
![]()
|
内存是如何在内部存储的
DataBlade API mi_*
内存分配函数从 IDS 共享内存分配内存。
内存持续时间
每个 mi_*
内存分配都有与之相关的持续时间。下面是关于持续时间的简明摘要。 有关更多详细信息,请参阅 IBM Informix DataBlade API Programmer's Manual,版本 9.3 中的“Managing Memory”的章节,可以从 IBM Informix 在线文档网站 下载该手册。
持续时间 | 描述 |
PER_ROUTINE | 一次 UDR 调用 |
PER_COMMAND | 子查询 |
PER_STMT_EXEC' | (9.3 以前的版本)SQL 语句( SELECT 、 INSERT 、 UPDATE 等)。 |
PER_STMT_EXEC | (9.3)当前 SQL 语句。 |
PER_STMT_PREP | (9.3)预编译的 SQL 语句。 |
PER_TRANSACTION | 当前客户机事务( BEGIN WORK 到 COMMIT/ABORT WORK ) |
PER_SESSION | 当前客户机会话 |
PER_SYSTEM | IDS 系统全局,在服务器关机之前一直保持有效 |
两个新的 9.3 持续时间 PER_STMT_EXEC
和 PER_STMT_PREP
取代了现已弃用的 PER_STATEMENT
内存持续时间。
内存池
服务器在内部通过持续时间分配内存池。
您可以用 onstat -g mem
命令监控为特定客户机会话分配了多少 DataBlade API 共享内存。
9.3 服务器的输出包括类似如下所示的行:
CMD.53.72 V ... |
前三个字符标识内存池。下面总结了持续时间代码:
RTN | PER_ROUTINE |
CMD | PER_COMMAND |
STM | PER_STATEMENT(9.3 以前的版本) |
EXE | PER_STMT_EXEC(9.3) |
PRP | PER_STMT_PREP(9.3) |
TRX | PER_TRANSACTION |
SES | PER_SESSION |
SYS | PER_SYSTEM |
以上输出中的“53”是运行查询的用户的会话标识,您可以用“ onstat -g ses 53
”找出更多关于该会话的信息。“72”是 sqlexec 线程的线程标识。
您可能注意到 9.3 onstat
输出与先前发行版的输出不同。9.3 之前版本的服务器的输出包括如下所示的行:
23.RTN.SAPI V .... |
每行都以运行查询的用户的会话标识开始(“23”),后跟持续时间代码、然后是“ SAPI
”(意思是“服务器 API (Server API)”),SAPI 由 DataBlade API mi_*
函数为其分配内存。
![]() ![]() |
![]()
|
如何以及何时释放内存
当其持续时间到期时,服务器自动释放内存。或者您可以使用适当的 mi_*
函数提前释放内存。
例如,您可以用 mi_alloc()
为 UDR 中的临时变量分配内存,然后用 mi_free()
在 UDR 返回前释放该内存。但不要释放您分配给 UDR 返回结果的内存! 当 SQL 语句中不再需要返回值时,服务器将释放该内存。
注意! | 访问持续时间已过期的内存通常导致断言失败。下面关于“ 调试问题 ”一节说明了调试断言失败的方法。 |
好的,如果您希望提前进行自己的垃圾收集,那么您需要更多上下文来理解如何以及何时释放内存,以及应该使用什么函数。下一节描述了如何分配内存以及如何指定持续时间。“ 如何(以及何时)释放内存 ”一节特别详细地讲述了如果您打算在内存持续时间到期之前释放内存,该如何(以及 何时! )做到这一点。
![]() ![]() |
![]()
|
用户函数(mi_alloc)
下面列出了用户内存函数:
mi_alloc() |
以当前持续时间分配一块内存。缺省值是 PER_ROUTINE。 |
mi_zalloc() |
与 mi_alloc() 相同,但内存已置零 — 它的行为类似于 calloc() 。 |
mi_dalloc() |
以指定的持续时间分配一块内存。 对于 UDR,您最常用的指定值是 PER_ROUTINE 或 PER_COMMAND。对于 VTI 和 VII 访问方法,开发人员可能指定 PER_STMT_EXEC(请参阅下面一节)。 |
mi_free() |
释放由 mi_alloc() 、 mi_zalloc() 或 mi_dalloc() 分配的内存。 |
mi_alloc()
、 mi_zalloc()
和 mi_dalloc()
返回指向已分配内存的指针。然后,您在该指针上执行所有操作。 关于此类内存的关键点包括:
公用函数最常用的持续时间是 PER_ROUTINE
和 PER_COMMAND
。 在查询完成后,服务器自动释放任何由 UDR 分配且您还未释放的内存。
有必要顺便提一下另一个分配内存的用户函数: mi_new_var()
分配 mi_lvarchar
结构。 有关详细信息,请参阅 用 mi_lvarchar 管理变长数据 技术说明。
示例:分配 PER_ROUTINE 内存
以下 MyLog10() UDR 的代码显示了如何使用 mi_alloc()
来为双精度返回结果分配 PER_ROUTINE
内存。
|
缺省情况下, mi_alloc()
以 PER_ROUTINE
持续时间分配内存。(可以通过调用 mi_switch_mem_duration()
来更改这个缺省值;请参阅 DataBlade API Programmer's Manual 。)
每个 UDR 调用都分配返回结果,在 UDR 完成时服务器自动释放该结果。 永远不应指望 PER_ROUTINE
内存在 UDR 返回之后仍然有效。(实际上,它在 下一个 UDR 调用之前被释放。)
UDR 可能在一个查询中被多次调用,每处理一行就调用一次。 例如,在下面的查询中, MyLog10 被调用了三次,每次用于表中的一行:
|
|
该 UDR 被调用了三次, PER_ROUTINE
三次分配了返回值,并且服务器每次都自动释放内存。
如果您觉得为每一行都分配返回值代价过高,尤其是对于为大型表执行的 UDR, 那么您是对的! 下一节研究了如何在第一次调用 UDR 时就一次性分配返回值,这样内存将对于所有返回值都保持有效,以便所有 UDR 调用能够使用它。
示例:分配 PER_COMMAND 内存
本节演示了如何修改 MyLog10() UDR 以分配 PER_COMMAND
内存,它在查询中 UDR 的所有调用期间保持有效。为了做到这一点:
我们需要标识第一次 UDR 调用,此时我们将分配 PER_COMMAND
内存。
我们需要一个地方来存储指向 PER_COMMAND
已分配内存的指针,以便后面的 UDR 调用可以取回该指针。
MI_FPARAM
结构提供了一个地方来用 mi_fp_setfuncstate()
存储指向 PER_COMMAND
内存的指针。 mi_fp_funcstate()
取回该指针,并在第一个 UDR 调用时返回 NULL。
|
VTI 和 VII 访问方法的说明
VTI / VII API 提供了特殊的描述符(类似于 MI_FPARAM
)来存储指向用户数据的指针。 这些描述符被传递到许多访问方法任务:
MI_AM_SCAN_DESC
描述符被传递到 am_beginscan
和 am_endscan
任务之间的访问方法例程。 可以用 mi_dalloc()
分配 PER_COMMAND
内存,并用 mi_scan_setuserdata()
存储指针。
MI_AM_TABLE_DESC
描述符被传递到 am_open
和 am_close
任务之间的访问方法例程。 可以用 mi_dalloc()
分配 PER_STMT_EXEC
内存并用 mi_tab_setuserdata()
在 MI_AM_TABLE_DESC
结构中存储指向该内存的指针。
如何(以及何时)释放内存
释放内存是可选的,因为当内存持续时间到期时,服务器会自动释放它。 虽然如此,但当查询正在运行时,释放不再需要的内存以减少 UDR 内存消耗是通常的做法。
以下是一些准则:
绝不要释放分配给 UDR 返回值的内存。 服务器在不再需要该内存时会释放它。
不要尝试在内存持续时间到期后释放它。
不要用 mi_free()
释放任何数据结构(如 mi_lvarchar )。 那样做只会释放主 mi_lvarchar
结构本身而不是它指向的数据容器。DataBlade 在持续时间到期和释放内存以前很可能会发生内存泄漏。
以合适的持续时间分配内存,尤其是如果您打算为以后的重用而在诸如 MI_FPARAM
之类的描述符中存储指向该内存的指针时。
例如, MI_FPARAM
的持续时间是 PER_COMMAND
。
如果分配内存时使用了太大的持续时间,则 DataBlade 很可能发生内存泄漏。因此,如果您用 mi_dalloc()
分配 PER_SESSION 内存并在 MI_FPARAM
中存储指针,那么,服务器在查询完成后会释放 MI_FPARAM
,而 PER_SESSION 内存将保持到客户机会话结束为止。
如果分配内存时使用了太小的持续时间,则可能看到断言失败。 因此,如果您分配了 PER_ROUTINE
内存并在 MI_FPARAM
中存储其指针,则内存会在一次 UDR 调用之后被释放而指针对于后续的调用不再有效。
除非分配了内存,否则就不要释放它。许多分配内存的 mi_*
函数拥有相应的释放函数,因此,显然第一个函数分配内存,也显然有某个函数可以用来释放它。 mi_row_create()
和 mi_row_free()
就是一对很好的示例。
有时 DataBlade API 函数分配内存、该内存的持续时间是多少以及可以用什么函数来提前释放它这些都不明显。 IBM Informix DataBlade API Programmer's Manual,版本 9.3 中的图 13-5( PER_ROUTINE
)、13-6( PER_COMMAND
)、13-9( PER_STMT_EXEC
)和 13-11( PER_SESSION
)总结了由 DataBlade API 函数分配的内存的持续时间。
调试问题
onmode 调试标志
您可以启用内存梳理和内存池检查来调试断言失败。 本节描述了如何启用这些检查,但它们应该仅用于开发和调试。当启用时,它们将减慢生产环境下的应用程序的速度。
用 onmode -f 0x200
启用内存梳理。 接着服务器将:
0xfe
0xfd
。
mi_free()
和 mi_var_free()
释放的内存。 启用内存池检查以捕获对已释放内存或界外位置的写操作:
onmode -f 0x1
onmode -f 0x40000
标志可以组合。例如, onmode -f 0x201
在 9.1 服务器中既启用内存梳理又启用内存池检查。 最终结果是断言失败很可能在更接近问题的地方发生。
您也可以使用 onconfig 参数来启用这些选项。例如,下列参数启用梳理:
CCFLAGS 0x200 # scribble |
应该记住在您注释掉 CCFLAGS
参数并重新启动服务器之前,调试选项一直都有效。
新的 9.3 DataBlade API 函数
9.3 服务器引入了两个新的 mi_*
函数,可以用它们来生成有辅助作用的跟踪消息:
mi_get_duration_size()
让您确定内存池的大小。 mi_get_memptr_duration()
让您确定用某个 mi_*
函数分配的内存的持续时间。
![]() ![]() |
![]()
|
已命名的内存函数(mi_named_alloc)
下面列出了已命名的内存函数:
mi_named_alloc() |
以指定的持续时间分配一个已命名的内存块。 |
mi_named_zalloc() |
与 mi_named_alloc() 相同,但内存已置零。 |
mi_named_get() |
通过提供名称和持续时间获得指向该内存的指针。 |
mi_named_free() |
释放用 mi_named_alloc() 或 mi_named_zalloc() 分配的内存。 |
已命名的内存将已分配的内存与名称和内存持续时间联系起来。 当您分配内存时,服务器在内部存储名称及其相应地址。 您总是可以通过提供名称和持续时间获得指向该内存的指针。
关于已命名内存的关键点包括:
始终可以通过内存的名称和持续时间来获取指向它的指针。
在分配内存的持续时间上下文中内存是全局的,因此可以由多个 UDR 访问,这个 UDR 在(可能)有多个查询上下文中执行,甚至可以由多个客户机会话访问它。
在内存持续时间到期以前,它一直是可访问的。
当您需要跨 UDR 高速缓存数据或在 UDR 之间共享内存时,应该使用已命名的内存。 样本用法包括:
mi_routine_exec()
反复调用的会话级高速缓存函数(MI_FUNC_DESC)描述符。请参阅 Fastpath:从 C UDR 执行 SQL 例程 。 索引方法,需要存储用于跨分段索引的索引扫描的“全局”信息。
已命名的内存例程不锁定相关内存。
mi_named_alloc()
和 mi_named_zalloc()
分配被请求的内存块,并在跟踪已命名的内存的内部散列表中插入一个用于该已命名的内存的项。作为该散列表项的一部分,它还创建一个互斥锁,稍后可以用该互斥锁使用 mi_lock_memory()
或 mi_try_lock_memory()
锁定已命名的内存。
类似地, mi_named_get()
只返回指向已命名的内存的指针;它不锁定该已命名的内存。
由 DataBlade 管理并发访问。
下一节提供了必须管理的并发性上下文问题中的已命名的内存代码示例。
![]() ![]() |
![]()
|
管理并发性
正如前面所提到的,由 DataBlade 管理并发访问。但您必须知道它何时成为问题,以及如何管理它。
并发性问题
通常,内存持续时间越长,并发性就越有可能成为问题。 因此,第一步是理解并发性在哪里以及何时成为问题。
持续时间 = PER_ROUTINE 或 PER_COMMAND
让我们假设 UDR foo() 用一个公用函数来分配 PER_ROUTINE
内存或在 MI_FPARAM
中高速缓存 PER_COMMAND
内存。
在下面的查询中,有三个 foo() 的实例,并且每个 UDR 的实例将多次调用 foo() :
select foo(column1) |
查询中的每个 foo() 实例都在其自己的上下文中执行,获得自己的 MI_FPARAM
,并分配仅对它自己可见的内存。这里不用管理并发性问题,由服务器管理并发性。
如果三个 foo() 实例都要共享数据,它们就需要分配一个持续时间至少为 PER_STMT_EXEC
的已命名的内存。
持续时间 > PER_COMMAND
让我们假定函数 foo() 也在内部管理名为 foobar 的已命名的内存。 每个 foo() 实例都设法得到已命名的内存块;如果该内存块还不存在,则该实例继续运行并分配它。给定下列查询:
select foo(column1) |
foobar
的内存持续时间确定了谁可以同步访问它:
PER_STMT_EXEC
如果 foo()
分配给 foobar
的持续时间为 PER_STMT_EXEC,那么每个在同一 SQL 语句中执行的 foo()
将访问同一个 foobar
。 在 SQL 语句完成之后释放该内存。
PER_SESSION
如果 foo()
分配给 foobar
的持续时间为 PER_SESSION,那么该客户机会话中的任何 SQL 查询都可以访问该内存。当数据库连接关闭时释放该内存。
PER_SYSTEM
如果 foo()
分配给 foobar
的持续时间为 PER_SYSTEM
,那么,任何执行 foo()
的代码都访问同一个 foobar
。 该内存对于整个 IDS 服务器来说是全局的,并且在服务器关闭之前,它不会被释放。
基本准则
正如前面的“ 已命名的内存函数(mi_name_alloc) ”一节所提到的, mi_named_alloc()
、 mi_named_zalloc()
和 mi_named_get()
只返回指向内存的指针。已命名的内存例程都不锁定相关内存。
因此,即使对于仅读取已命名的内存而不更新它的线程,最安全的方法是锁定该内存,以防它已经处于被另一个线程更新的过程中。在 DataBlade 代码一致地管理内存的前提下,一旦线程拥有了对已命名的内存的锁,就可以保证一致的读取。
但请尽可能缩短持有锁的时间。应该在严格管理的、快速代码段中使用锁定接口,这些代码段让您在修改期间保护临界区。代码应该:
mi_lock_memory()
或 mi_try_lock_memory()
锁定内存 mi_unlock_memory()
解锁内存。 这种锁定接口不适于长期持有锁。 如果您需要长时间锁定数据,应该在客户机应用程序中用 SQL 来这么做。
注意! | 别耽误时间!获取和释放锁都要快。 一旦您获取了锁,则 必须 释放它。 |
本节提供了使用 mi_lock_*
函数的样本代码。
示例:mi_lock_memory()
假定已声明了 MyInfo
结构:
typedef struct |
并假定该代码提供了初始化该结构的 C 函数:
mi_integer MyInfo_init(MyInfo *my_info); |
下面的样本代码分配已命名的内存,或者,如果已经分配了已命名的内存则获取它,然后管理针对该内存的并发读取和更新。
|
示例:mi_try_lock_memory()
以下是另一种使用 mi_try_lock_memory()
的方法,如果另一个线程拥有锁则它立即返回。如果 10 次尝试之后还不能获取锁,这个代码将返回错误。
|
释放锁定
服务器不会释放您可能已经获得的任何锁。在下列情况下,您 必须 释放所获得的任何锁:
mi_db_error_raise()
产生异常之前。 mi_*
函数在内部产生异常。 上面的示例演示了如何管理前两种情况。 第三种情况可以通过注册异常处理程序来管理, Informix DataBlade API Programmer's Manual 第 10 章对此进行了详细描述。
例如,让我们假定 上面的 GetMyInfo()
示例从查询表执行 SQL select 以将数据填入已命名的内存。 mi_exec()
是在内部产生异常的函数示例。我们应该注册一个异常回调,如果发生异常它就释放锁。 完成该任务的代码如下。
|
注意! | 如果 UDR 象上面的 C 代码那样使用 mi_exec() 执行 SQL 语句,就必须用 variant 修饰符创建该 UDR。 即使 SQL 语句是从静态查询表进行选择并返回不变的结果,它仍然必须被创建为 variant 。 |
![]() ![]() |
![]()
|
更多关于这个主题的信息
下列 IDN 技术说明提供了相关信息:
![]() ![]() |
![]()
|
词汇表
这篇技术说明中使用的首字母缩略词和词汇包括:
关于作者
![]() |
||
![]() |
Jean T. Anderson has authored this article |