如何让SQLite启动时预分配内存?MEMSYS5实测异常排查
我正在开发一个使用SQLite的嵌入式程序,希望实现可预测的内存使用。我了解到SQLite的「Zero-malloc内存分配器」(MEMSYS5)可以让SQLite在启动时分配固定量的内存并独占使用,而非运行时动态调用malloc。
我在Ubuntu桌面环境中编译了带有-DSQLITE_ENABLE_MEMSYS5参数的最新版SQLite,编写了示例程序让SQLite预分配1GB内存后休眠15秒,同时用测试脚本通过free命令查看内存变化,但free输出中并未体现出预分配的内存,程序也没有任何错误返回。
我的测试脚本:
#!/bin/bash set -o verbose free -b ./testsqlite3 & sleep 3 ps aux | grep testsqlite3 free -b
脚本输出:
$ ./runtest.sh free -b total used free shared buff/cache available Mem: 8421400576 607322112 6244298752 28102656 1569779712 7066603520 Swap: 0 0 0 ./testsqlite3 & sleep 3 Using libsqlite version 3.40.0, sqlite3.h is version 3.40.0 Preallocating using MEMSYS5 Opened database successfully Table created successfully Sleeping for 15 secs before exiting. Call 'free -b' now. echo "bash sleep complete, confirming app still running" bash sleep complete, confirming app still running ps aux | grep testsqlite3 user1 7474 0.0 0.0 979960 2380 pts/1 S+ 12:11 0:00 ./testsqlite3 user1 7477 0.0 0.0 5116 880 pts/1 S+ 12:11 0:00 grep testsqlite3 free -b total used free shared buff/cache available Mem: 8421400576 607420416 6244200448 28102656 1569779712 7066505216 Swap: 0 0 0
示例程序testsqlite3:
#include <stdio.h> #include <stdlib.h> #include <sqlite3.h> #include <unistd.h> static int callback(void *NotUsed, int argc, char **argv, char **azColName) { int i; for(i = 0; i<argc; i++) { printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } printf("\n"); return 0; } int main(int argc, char* argv[]) { sqlite3 *db; char *zErrMsg = 0; int rc; char *sql; fprintf(stdout, "Using libsqlite version %s, sqlite3.h is version %s\n", sqlite3_libversion(), SQLITE_VERSION); fprintf(stdout, "Preallocating using MEMSYS5\n"); //int preallocateMemoryBytes = 100000000; //100MB int preallocateMemoryBytes = 1000000000; //1GB rc = sqlite3_config(SQLITE_CONFIG_HEAP, malloc(preallocateMemoryBytes), preallocateMemoryBytes, 32); if (rc != SQLITE_OK) { fprintf(stderr, "failed to config SQLITE_CONFIG_HEAP\n"); return(-1); } rc = sqlite3_initialize(); if (rc != SQLITE_OK) { fprintf(stderr, "initialize() failed\n"); return(-1); } //open DB rc = sqlite3_open("test.db", &db); if( rc ) { fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); return(-1); } else { fprintf(stdout, "Opened database successfully\n"); } sql = "CREATE TABLE IF NOT EXISTS COMPANY(" \ "ID INT PRIMARY KEY NOT NULL," \ "NAME TEXT NOT NULL," \ "AGE INT NOT NULL," \ "ADDRESS CHAR(50)," \ "SALARY REAL );"; rc = sqlite3_exec(db, sql, callback, 0, &zErrMsg); if( rc != SQLITE_OK ){ fprintf(stderr, "SQL error: %s\n", zErrMsg); sqlite3_free(zErrMsg); return(-1); } else { fprintf(stdout, "Table created successfully\n"); } fprintf(stdout, "Sleeping for 15 secs before exiting. Call 'free -b' now.\n"); usleep(15000 * 1000); sqlite3_close(db); return 0; }
问题出在两个核心点:
1. Linux下malloc的延迟分配机制
用malloc分配1GB内存时,Linux内核仅为进程预留虚拟地址空间,不会立即分配对应的物理内存。只有当进程实际写入这块内存时,内核才会真正分配物理页。这就是free命令看不到内存占用变化的根本原因——物理内存还未被实际使用。
2. SQLite MEMSYS5的内存使用逻辑
即使通过SQLITE_CONFIG_HEAP给SQLite指定了堆内存,MEMSYS5也不会立即占用整个堆,而是在SQLite执行实际操作(如创建大表、插入数据)时才从堆中分配小块内存。但你的需求是启动时立即预留物理内存,因此需要主动触发物理内存分配。
具体解决方法:
方法一:手动写入预分配内存块,触发物理内存分配
在调用sqlite3_config之前,对malloc得到的内存块进行写操作,比如用memset填充整个块:
void *heap = malloc(preallocateMemoryBytes); if (!heap) { fprintf(stderr, "malloc failed\n"); return -1; } // 填充内存,触发物理页分配 memset(heap, 0, preallocateMemoryBytes); rc = sqlite3_config(SQLITE_CONFIG_HEAP, heap, preallocateMemoryBytes, 32);
方法二:用mmap代替malloc,强制立即分配物理内存
使用mmap并指定MAP_POPULATE标志,让内核立即分配物理内存:
#include <sys/mman.h> // ... void *heap = mmap(NULL, preallocateMemoryBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0); if (heap == MAP_FAILED) { fprintf(stderr, "mmap failed\n"); return -1; } rc = sqlite3_config(SQLITE_CONFIG_HEAP, heap, preallocateMemoryBytes, 32);
注意:使用mmap分配的内存,最后需要调用munmap手动释放(SQLite不会自动释放你传入的堆内存)。
验证效果
修改后重新编译运行测试脚本,free命令的used列会明显增加1GB左右,available列对应减少,说明物理内存已经被成功预留。
内容的提问来源于stack exchange,提问作者jerkstorecalled

