SQlite源码分析

配置

1 可选的底层内存分配器
SQLite源代码包含几种不同的内存分配器模块,可以在编译时选择,或在启动时作为一个有限的扩展。


(1)缺省内存分配器
缺省情况下,SQLite使用C标准库中的malloc(), realloc()和free()例程来分配内存。实现中还对这些例程做一层薄的包装以提供一个memsize()函数返回一个现存分配的大小。memsize()也能精确地跟踪未归还内存的字节数,它能确定当一个分配被释放时有多少字节从未归还内存中移除。缺省内存分配器中的memsize()实现是在每个malloc()请求上多分配额外8个字节作为头部,并把分配的大小保存到这个8字节的头部。
在大多数应用程序中我们都建议使用缺省内存分配器,如果没有使用可选内存分配器的强制性需求,使用缺省的即可。

(2)调试内存分配器
如果SQLite使用SQLITE_MEMDEBUG编译时选项来编译,则使用一个不同的、对系统malloc(), realloc()和free()进行重型包装的内存分配器。重型包装器对每个分配请求多分配100字节的额外空间,用来在分配的末尾放置哨兵值。当一个分配被释放时,检查这些哨兵值以确保SQLite内核没有超出缓冲区的两端。当系统库来自GLIBC时,重型包装器也会使用GNU backtrace()函数来检查栈,并记录malloc()调用的祖先函数。当运行测试套件时,重型包装器还会记录当前测试用例的名称。这两个特性对跟踪内存泄漏是非常有用的。 重型包装器只用于SQLite的测试、分析和调试。它有显著的性能和内存开销,一般不用在最终产品中。


(3)零分配内存分配器 当SQLite使用SQLITE_ENABLE_MEMSYS5选项编译时,会包含一个不使用malloc()的可选内存分配器。SQLite开发者称它为"memsys5"。即使被包含在版本中,缺省情况下memsys5也是被禁用的。应用程序必须在启动时调用下面的SQLite接口:
sqlite3_config(SQLITE_CONFIG_HEAP, pBuf, szBuf, mnReq);
其中pBuf指向一个大的连续的内存块,SQLite使用它满足所有的内存分配需要。pBuf也可以指向一个静态数组或一段从其他应用程序特定机制获取的内存。szBuf为pBuf内存的字节数。mnReq为一次分配的最小字节数。任何对sqlite3_malloc(N)的调用,当N小于mnReq时会向上舍入到mnReq。nmReq必须是2的幂。稍后我们会看到mnReq参数在Robson证明中对于减小n值和最小内存需求是至关重要的。 memsys5分配器被设计用于嵌入式系统中,当然没有任何限制规定不得用于工作站上。szBuf通常在几百KB到几十MB之间,取决于系统需要和内存预算。memsys5使用的算法可以概括为“2的幂,首次命中”。所有内存分配请求的大小都舍入到2的幂,分配时使用pBuf中第一个足够大的空闲内存块。使用伙伴系统来合并相邻的空闲内存块。如果使用得当,本算法可以为避免内存碎片和内在崩溃提供数学保证(参考下面描述)。


(4)实验性内存分配器
从名称"memsys5"可以看出,可能还有其他可选的内存分配器,的确如此。缺省内存分配器称为"memsys1",调试内存分配器称为"memsys2"。如果SQLite使用SQLITE_ENABLE_MEMSYS3编译,则另外一个零分配内存分配器memsys3包含到版本中。它与memsys5类似,必须调用sqlite3_config(SQLITE_CONFIG_HEAP,...)来激活。Memsys3使用内存缓冲区来满足所有的内存分配需要。它与memsys5的区别是使用不同的内存分配算法,这个算法在实践中看起来工作得很好,但不能为避免内存碎片和内存崩溃提供数学保证。Memsys3是memsys5的前任,SQLite开发者相信memsys5比memsys3更好,所有需要零分配内存分配器的应用程序应该使用memsys5而不是memsys3。Memsys3在将来的SQLite版本中可能会移除。


(5)应用程序定义的内存分配器
应用程序可以在启动时提供自己的内存分配器给SQLite。为了让SQLite使用新的内存分配器,应用程序要调用: sqlite3_config(SQLITE_CONFIG_MALLOC, pMem); 其中pMem指向一个sqlite3_mem_methods对象,这个结构定义了应用程序特定的内存分配器接口。sqlite3_mem_methods对象只是包含一系列函数指针的结构,指向自定义的各种内存分配函数。在多线程应用程序中,当且仅当激活SQLITE_CONFIG_MEMSTATUS时sqlite3_mem_methods才是串行化的。如果SQLITE_CONFIG_MEMSTATUS禁用,sqlite3_mem_methods中的方法就需要自己来关注串行化。


(6)内存分配器覆盖层
应用程序可在SQLite内核和底层内存分配器之间插入覆盖层。例如,OOM测试逻辑通过使用覆盖层可以模拟内存分配失败的情形。覆盖层使用以下接口来创建: sqlite3_config(SQLITE_CONFIG_GETMALLOC, pOldMem); 该接口获取现存内存分配器的指针,并保存它以用来进行实际的内存分配。然后通过使用类似的sqlite3_config(SQLITE_CONFIG_MALLOC,...)把覆盖层被插入到现存内存分配器的地方。
(7)空操作内存分配器
如果SQLite使用SQLITE_ZERO_MALLOC选项来编译,缺省内存分配器将会被忽略,由一个桩内存分配器代替,它不会分配任何内存。任何对桩分配器的调用将返回没有可用内存的报告。这种空操作内存分配器只是作为一个占位符,以便SQLite能链接一些不使用malloc(), free()或realloc()的自定义内存分配器。带SQLITE_ZERO_MALLOC选项编译的应用程序在使用SQLite之前,需要使用sqlite3_config(),结合SQLITE_CONFIG_MALLOC或SQLITE_CONFIG_HEAP来指定新的可选内存分配器。

2 临时内存

SQLite偶尔需要一大块的“临时”内存来执行一些临时的计算。
例如,当重新平衡一棵B-Tree时,需要使用临时内存。这些临时内存通常在10KB左右,用于一个单一的、短暂的函数调用。在早期的SQLite版本中,临时内存从处理器栈中获取,这在拥有大容量栈的工作站上可以很好地工作。但在只有小容量处理器栈(通常4K或8K)的嵌入式系统中,从栈中申请一个大的缓冲区会引起问题。因此,SQLite被修改为从堆上获取临时内存。临时内存分配器的设置方法如下: sqlite3_config(SQLITE_CONFIG_SCRATCH, pBuf, sz, N); 其中pBuf指向一段连续的内存,SQLite用它来进行临时内存分配。这段连续内存至少要有sz*N字节的大小,"sz"参数是每次临时内存分配的最大字节数,N是同时进行临时内存分配的最大次数。"sz"参数值应该为最大数据库页面的6倍左右,N应该为系统中运行线程数量的2倍左右。没有线程会一次请求超过两次的临时内存分配,因此N的值能确保足够的临时内存分配。如果临时内存设置没有提供足够的内存,SQLite将退回到使用正常的内存分配器来进行临时内存分配。缺省的设置是sz=0, N=0,表示使用正常的内存分配器作为缺省行为


3 页缓存内存
在很多应用程序中,SQLite的数据库页缓存子系统会更频繁地使用动态内存分配,甚至频繁程度超过其他子系统的10倍。SQLite可以配置成从一个独立的、槽大小固定的内存池中进行页缓存内存的分配。这有两个优点:


(1)因为所有的分配是同样的大小,内存分配器可能工作的更快,分配器无需合并相邻空闲槽或查找大小合适的槽。所有未分配的内存槽存储在一个链表中,分配时摘下链表中的第一个内存槽,释放时直接把内存槽添加到链表的头部。
(2)由于只有一种分配大小,Robson证明中的n参数为1,并且分配器需要的整个内存空间(N)恰好等于使用的最大内存(M)。没有额外的内存碎片开销,因此减少了内存需求量。这对页缓存内存特别重要,因为SQLite的大部分内存需求都来自于页缓存。
页缓存内存分配器缺省是禁用了,应用程序可在启动时打开它: sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N); 其中pBuf指向一段连续的内存,SQLite用它来进行页缓存内存的分配。这段连续内存至少要有sz*N字节的大小,"sz"参数是每次页缓存内存分配的字节数,N是可以进行分配的最大次数。如果SQLite需要超过sz个字节的页缓存内存,或者需要超过N块的分配,则退回到使用通常的内存分配器。


4 后备内存分配器
SQLite数据库连接会进行许多小的、短期的内存分配。当用sqlite3_prepare_v2()编译SQL语句时这种情况最常见。这些小的内存分配用来存储诸如表名和列名、解析树结点、单独的查询结果、和B-Tree游标对象。这会导致频繁地调用malloc()和free(),用掉分配给SQLite的大部分CPU时间片。
后备内存池的大小有一个全局的缺省值,但可以配置成不同的值。只要在启动时使用以下接口: sqlite3_config(SQLITE_CONFIG_LOOKASIDE, sz, cnt); 其中sz为每个后备槽的字节数,缺省为100字节。cnt是每个数据库连接的后备内存槽总个数,缺省值为500个槽。显然每个数据库连接的后备内存大小为szcnt字节,缺省为50KB。注意这些缺省值针对SQLite 3.6.1,在将来版本中有可能会改变。 一个单独数据库连接的后备池可以更改,使用下面的调用: sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, pBuf, sz, cnt); 其中pBuf指向后备内存池空间。如果pBuf为NULL,SQLite将使用sqlite3_malloc()来获取需要的内存池空间。sz和cnt是每个后备槽的大小和槽的总数。如果pBuf不是NULL,则必须指向至少szcnt字节的空间。 数据库连接上的后备内存池配置只有在内存还没分配出去的情况下才能更改。因此,配置的设置应该在用sqlite3_open()(或其他创建函数)创建数据库连接后,而在执行任何SQL语句前立刻进行。
5 内存状态

缺省情况下,SQLite统计它的内存使用情况。这些统计可以确定一个应用程序真正需要多少内存,也可以用在高可靠系统中以确定内存使用是否即将关闭或超过Robson证明的限制,从而导致内存分配子系统崩溃。许多内存统计是全局的,因此必须要互斥锁来串行化。统计缺省是打开的,但是可以禁用它,这样在内存分配或释放是可以避免使用互斥锁,因此节省开销。调用下面的接口: sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);
"onoff"参数为true时激活内存统计跟踪,为false时禁用内存统计跟踪。如果统计是激活的,可以使用下面的例程来访问它们: sqlite3_status(verbsqlite3_status(verb, &current, &highwater, resetflag); "verb"参数确定访问哪个统计信息,有各种各样的verb动词定义,可参考http://sqlite.org/c3ref/c_status_malloc_count.html#sqlitestatusmemoryused。当前选择的值会写入到current整型参数中,历史最高值会写入到highwater参数中。如果resetflag为true,则在调用返回时high-water标志会重置为当前选择的值。 对于单个数据库连接的统计,有不同的接口: sqlite3_db_status(db, verb, &current, &highwater, resetflag); 这个接口功能类似,只不过多一个数据库连接参数,并且返回的是这个连接的内存统计信息,而不是整个SQLite库。sqlite3_db_status()接口当前只识别一个动词SQLITE_DBSTATUS_LOOKASIDE_USED,在将来可能会识别更多的动词。 每个连接的统计不使用全局变量,因此不需要互斥锁来访问和更新。即使SQLITE_CONFIG_MEMSTATUS关闭,每个连接的内存统计也会继续进行。
设置内存使用限制
sqlite3_soft_heap_limit64()接口用来设置通用内存分配器可分配的内存堆总量上限。如果分配的内存超过了这个弱的堆限制,SQLite将在继续分配请求之前释放缓存的内存。弱的堆限制机制只在内存统计激活的情况下才工作,并且如果编译时使用SQLITE_ENABLE_MEMORY_MANAGEMENT,它能获得最好的工作性能。
弱的堆限制之所以称为“弱(soft)”的,是因为如果SQLite不能释放足够的辅助内存来满足这个限制,它会继续分配额外的内存,并超过这个限制。这是基于使用额外内存比完全失败更好的理论。在SQLite 3.6.1中,弱的堆限制只能应用在通用内存分配器中,它不能和临时内存分配器、页缓存内存分配器、或后备内存分配器交互。在将来版本中会解决这个问题。