MiKTeX 中的 FNDB(FileName DataBase) 是一个核心组件,它是专门设计用于高效文件搜索的索引数据库。使得在庞大的 TEXMF 目录树中快速定位文件成为可能,显著提升了 TeX 文档编译和包管理的效率。
- 提供快速文件搜索:通过内存中的哈希表实现毫秒级查找
- 减少磁盘 I/O:避免昂贵的文件系统遍历
- 支持复杂搜索模式:集成递归目录匹配算法
- 自动维护更新:通过更改文件机制保持同步
- 跨平台一致性:在不同操作系统上提供统一的搜索体验
一、FNDB 的基本概念
1.1 定义
FNDB 是 MiKTeX 的文件名数据库,它预先扫描整个 TEXMF 目录树,构建文件索引,以提供快速的文件查找功能。
1.2 解决的问题
在没有 FNDB 的情况下,每次文件搜索都需要:
- 遍历复杂的目录结构
- 进行大量的磁盘 I/O 操作
- 处理平台特定的文件系统特性
FNDB 通过预构建索引来解决这些问题。
二、修改代码
2.1 读fndb文件时,不再使用内存映射方式,而是普通读写
要简化代码,在要用的代码中,就这地方在使用内存映射,而且内存映射模块中还要使用TryLock/UnlockFile,后者也是要去掉的。
为可以不让Core/Fndb目录下那几个修改调用接口,实现一个假的MemoryMappedFile,针对此接口,不支持任何和写有关的操作。
主要要思路时,roseMemoryMappedFile::Open时,打开fndb文件,并读出内存,在此函数,文件就关闭。后面就是对这内存的访问。roseMemoryMappedFile::Close执行的是释放这内存。
2.2. 针对内存中某个tex根目录的FileNameDatabase对象,app运行期间只维护一个,即只创建一次,只销毁一次。
以要生成pdflatex.fmt为例,先后要经过三个模块,官方mitex就是三个exe。
miktex -> miktex_makefmt -> miktex_pdftex
三个都要使用<mktex>/sandbox这个fndb,意味着以<mktex>/sandbox为根目录的FileNameDatabase对象会被创建三个。
<miktex>/Libraries/MiKTeX/Core/Session\texmfroot.cpp
------
namespace {
static std::map<std::string, std::shared_ptr<FileNameDatabase> > fndbs;
}增加个全局变量fndbs,根目录时<mktex>/sandbox时,第一次发现fndbs没有,就创建FileNameDatabase,以pair(<mktex>/sandbox, FileNameDatabase)加入fndbs。后绪要<mktex>/sandbox时,从fndbs取出就行。
当fndb文件存在,但不是有效时,会原版miktex会抛出异常。像长度不足文件头时,抛出"Not a file name database file (wrong size)."。一旦发fndb文件无效,需删除这文件。一是避够后面又复制这无用流程,二是这文件要是在着,miktex后面Fndb::Add会抛出“internal error”。
2.3 fndb文件+fndb.log = FileNameDatabase机制,不在使用fndb.log
要简化代码,上面去掉fndb文件使用内存映射后,在用TryLock/UnlockFile的剩和fndb.log相关了,至此可以彻底没有TryLock/UnlockFile。
fndb文件+fndb.log = FileNameDatabase
在这个等试中,左侧是fndb文件系统中,右侧是内存中。修改后,fndb文件 = FileNameDatabase。修改这个须要清楚原版用的fndb.log机制,这是一个生成pdflatex.fmt,并加入fndb流程。
- (makefmt)生成了文件pdftexconfig.tex,要写入fndb,发现fndb文件没有,于是调用FndbManager::Create创建fndb文件。
- {makefmt}在FndbManager::Create,生成fndb文件,并删除fndb.log后,会调用SessionImpl::RecordMaintenance(),要向mitex.ini中Core块写入LastUserMaintenance=1765332190,会调用Fndb::FileExist判断是否存在“miktex/config\miktex.ini”这个文件。这时要用到FileNameDatabase对象,发现没有,于是FileNameDatabase对象。——FndbManager::Create,1)生成fndb文件,2)因为生成的fndb文件已包括目录树下所有文件,会删除fndb.log。3)创建FileNameDatabase对象,对象中fileNames是fndb文件中的那些文件。有ApplyChangeFile操作,但fndb.log就没有,啥有没做。
- (makefmt)Fndb::Add的后半段,执行FileNameDatabase::Add。把pdftexconfig.tex加入到FileNameDatabase对象,但此文件已在之前由fndb文件得到的fileNames,不需要加入,于是此个FileNameDatabase::Add只是生成一个空的fndb.log。一旦有操做,那会以“+”的方式向fndb.log增加一些记录。
- (pdftex)(还没有生成pdflatex.fmt)会遇到一次需要查此个fndb的操作,于是调用FileNameDatabase::Search,Search会调用ApplyChangeFile()。fndb.log文件是空,值newChangeFileSize是0,不执行合并。
- (pdftex)生成pdflatex.fmt,此时是放在tmp目录。
- (makefmt)把pdflatex.log从tmp目录复制到miktex\log\makefmt\pdflatex目录,并要改名为2025-12-10-10-00-56.log。复制执行的是File::Copy操作,设置了UpdateFndb标记。也就是说,除了复制文件,还执行Fndb::Add。后者会执行FileNameDatabase::InsertRecord,把2025-12-10-10-00-56.log加入fndb哈希表,并把这条加入文件以“+”的方法写到fndb.log。加入靠的不是通过合并log文件,而是Fndb::add,通过log文件加入时,加入到FileNameDatabase,调用的是FastInsertRecord。
- 把pdflatex.fmt从tmp目录复制到miktex\data\le\pdftex目录。复制执行的是File::Copy操作,设置了UpdateFndb标记。也就是说,除了复制文件,还执行Fndb::Add。后者会执行FileNameDatabase::InsertRecord,把pdflatex.fmt加入fndb哈希表,并把这条加入文件以“+”的方法写到fndb.log。
- (miktex)使用此个fndb,也就都有fndb.log、pdflatex.fmt了。
- 至此在内存中,FileNameDatabase已加入了2025-12-10-10-00-56.log、pdflatex.fmt这两个文件。在文件上,fndb文件没变,但fndb.log有了两条“+”记录。这样在下次创建的FileNameDatabase,那它读的是fndb文件+fndb.log,那就是完整的了。
修改思路:在修改fndb.log文件地方,重新生成一个fndb文件,这个fdnb文件包括了此次新加入的2025-12-10-10-00-56.log,而fndb.log则保持空。
void FileNameDatabase::Add(const vector<Fndb::Record>& records)
原版将新增文件加入fndb.log是在上面这个FileNameDatabase::Add,参数records就是要加的文件。就在此文件,改为和fndb.log无关,而是调用mmap->fndb_add修改fndb文件。
void* roseMemoryMappedFile::fndb_add(const std::vector<trecord>& records)
内存映射接口增加fndb_add,专门用于向fndb文件增加文件。类似于实现fndb.log中的“+”。一旦FileNameDatabase有增加文件,磁盘上的fndb文件会随即被修改。
对FileNameDatabase有减少文件,即fndg.log中的“-”,目前没实现。等将来有这个须要了,再写相关代码。
2.4 确保fndb文件存在
即使没有fndb文件,过程中也须要查找文件,但还是可能不会生成fndb文件的,像已生成pdflatex.fmt后,执行miktex_pdftex把一个tex生成pdf。
void Fndb::ensure_file_exists()
{
static bool called = false;
if (called) {
return;
}
MiKTeX::Util::PathName pathOut(miktex_sandbox_dir + "/miktex/config/pdflatex.ini");
Fndb::Add({ {pathOut} });
}新增一个ensure_file_existes,在须要确保生成的fndb文件的地方调用这个函数。Fndb::Add在发现pathOut文件所在fndb没有对应fndb文件时,就会健这文件。参数pathOut须满足两个要求。
- 是在tex根目录是<miktex>/sandbox下的文件。
- 这文件肯定已存在fndb。避免第二次调用ensure_file_exists时,让FileNameDatabase::Add是等同啥也不做。当然,上面已有called,第二次ensure_file_exists已不会Fndb:Add。
何时调用ensure_file_exists?——在Application::Init调用ConfigureLogging()之前。这里要注意一条规则:在调用Fndb::Add({ {pathOut} });”时,fndb文件如果存在,那必须有效,否则抛出“internal error”。这就导致一个问题,如果一开始fndb文件存在、但无效,那在Fndb::Add前,得把这文件删除。借用的时机是,ConfigureLogging()之前,SessionImpl::GetConfigValue会去发现这文件无效,然后删除它。
二、FNDB 的架构设计
2.1 核心数据结构
class FileNameDatabase {
private:
PathName rootDirectory; // 数据库覆盖的根目录
// 主要索引结构:文件名 → 文件记录集合
FileNameHashTable fileNames;
// 可能的辅助索引
DirectoryHashTable directories;
FileSizeIndex sizeIndex;
};
2.2 哈希表结构
// 文件名哈希表示例
unordered_multimap<FileNameKey, FileRecord> fileNames;
// 哈希键生成
FileNameKey MakeKey(const PathName& fileName) {
// 通常生成大小写不敏感的哈希键
string lowerName = Utils::ToLower(fileName.ToString());
return HashFunction(lowerName);
}
// 文件记录结构
struct FileRecord {
PathName relativeDirectory; // 相对于根目录的路径
FileInfo info; // 文件元数据
time_t lastModified; // 最后修改时间
size_t fileSize; // 文件大小
};
三、FNDB 的构建过程
3.1 数据库创建
void BuildFileNameDatabase(const PathName& rootDir) {
FileNameDatabase fndb(rootDir);
// 递归扫描目录树
ScanDirectoryTree(rootDir, "", fndb);
// 优化索引结构
fndb.Optimize();
// 写入磁盘
fndb.SaveToFile(GetFNDBPath(rootDir));
}
3.2 目录扫描算法
void ScanDirectoryTree(const PathName& baseDir,
const string& relativePath,
FileNameDatabase& fndb) {
auto lister = DirectoryLister::Open(baseDir, nullptr);
DirectoryEntry entry;
while (lister->GetNext(entry)) {
if (entry.isDirectory) {
// 递归扫描子目录
string newRelativePath = relativePath + "/" + entry.name;
ScanDirectoryTree(baseDir / entry.name, newRelativePath, fndb);
} else {
// 添加文件到数据库
FileRecord record;
record.relativeDirectory = relativePath;
record.info = GetFileInfo(baseDir / entry.name);
record.lastModified = entry.lastWriteTime;
record.fileSize = entry.fileSize;
fndb.AddFile(entry.name, record);
}
}
}
四、FNDB 文件格式
4.1 磁盘存储结构
FNDB 通常存储为二进制文件,结构如下:
[文件头] - 魔数(标识文件类型) - 版本号 - 根目录路径 - 记录数量 - 创建时间戳 [文件名索引区] - 哈希表桶数量 - 每个桶的偏移量 [数据记录区] - 文件记录序列化数据 [字符串池] - 重复使用的路径字符串
4.2 文件位置
// 用户级别的 FNDB PathName userFndbPath = GetUserDataRoot() / "miktex/data/le" / "miktex.fndb"; // 系统级别的 FNDB PathName systemFndbPath = GetCommonDataRoot() / "miktex/data/le" / "miktex.fndb";
五、FNDB 的搜索机制
5.1 核心搜索函数
bool FileNameDatabase::Search(const PathName& relativePath,
const string& pathPattern,
bool all,
vector<Fndb::Record>& result) {
// 1. 分解路径为目录和文件名
PathName dir = relativePath.GetDirectoryName();
PathName fileName = relativePath.GetFileName();
// 2. 在哈希表中查找文件名
auto range = fileNames.equal_range(MakeKey(fileName));
// 3. 对每个匹配的文件,检查目录是否匹配模式
for (auto it = range.first; it != range.second; ++it) {
if (Match(pathPattern, it->second.relativeDirectory.ToString())) {
// 构建完整路径并添加到结果
PathName fullPath = rootDirectory;
fullPath /= it->second.relativeDirectory;
fullPath /= fileName;
result.push_back({fullPath, it->second.info});
if (!all) break;
}
}
return !result.empty();
}
5.2 搜索性能优势
对比分析:
| 搜索方式 | 时间复杂度 | 磁盘 I/O | 内存使用 |
| 文件系统搜索 | O(总文件数) | 高 | 低 |
| FNDB 搜索 | O(匹配文件数) | 无(内存中) | 中等 |
实际性能差异:
- 文件系统搜索:可能需要秒级时间
- FNDB 搜索:通常毫秒级完成
六、FNDB 在 MiKTeX 工作流中的集成
6.1 TeX 编译过程中的文件查找
bool SessionImpl::FindFile(const string& fileName,
FileType fileType,
PathName& resultPath) {
// 首先尝试 FNDB 搜索
if (fndb != nullptr) {
vector<Fndb::Record> results;
if (fndb->Search(fileName, GetPatternsForType(fileType), false, results)) {
resultPath = results[0].path;
return true;
}
}
// 回退到文件系统搜索
return FindFileInFileSystem(fileName, fileType, resultPath);
}
6.2 包管理器集成
class PackageManager {
FileNameDatabase* fndb;
public:
vector<PathName> ListPackageFiles(const string& packageName) {
vector<Fndb::Record> results;
string pattern = GetPackageInstallPath(packageName) + "//";
// 使用 FNDB 快速获取包的所有文件
fndb->Search("*", pattern, true, results);
vector<PathName> files;
for (const auto& record : results) {
files.push_back(record.path);
}
return files;
}
};
七、FNDB 的维护和更新
7.1 自动更新机制
void FileNameDatabase::CheckAndUpdate() {
time_t lastUpdate = GetLastUpdateTime();
time_t lastMaintenance = session->GetLastMaintenanceTime();
if (lastMaintenance > lastUpdate) {
// 系统维护后需要更新
Rebuild();
} else {
// 检查更改文件
if (ChangeFileExists() && ChangeFileIsNewer()) {
ApplyChanges();
}
}
}
7.2 更改文件机制
MiKTeX 使用更改文件来记录文件系统的变化:
void ApplyChangeFile() {
PathName changeFile = GetChangeFilePath();
if (File::Exists(changeFile)) {
vector<ChangeRecord> changes = ReadChangeFile(changeFile);
for (const auto& change : changes) {
if (change.type == ChangeType::Added) {
AddFileToDatabase(change.filePath);
} else if (change.type == ChangeType::Deleted) {
RemoveFileFromDatabase(change.filePath);
} else if (change.type == ChangeType::Modified) {
UpdateFileInDatabase(change.filePath);
}
}
// 删除已处理的更改文件
File::Delete(changeFile);
}
}
八、FNDB 的高级特性
8.1 多根目录支持
class FileNameDatabaseManager {
vector<unique_ptr<FileNameDatabase>> databases;
public:
bool SearchAcrossAll(const PathName& relativePath,
const string& pattern,
vector<Fndb::Record>& results) {
for (const auto& fndb : databases) {
if (fndb->Search(relativePath, pattern, true, results)) {
if (!all) return true;
}
}
return !results.empty();
}
};
8.2 查询优化
// 使用统计信息优化搜索
class OptimizedFileNameDatabase : public FileNameDatabase {
FileNameStatistics stats;
public:
bool SmartSearch(const PathName& relativePath,
const string& pattern,
vector<Fndb::Record>& results) {
// 基于使用频率调整搜索顺序
auto searchOrder = stats.GetSearchOrder(pattern, relativePath);
// ...
}
};
8.3 缓存机制
// 缓存热门搜索的结果
class CachedFileNameDatabase : public FileNameDatabase {
mutable unordered_map<SearchKey, vector<Fndb::Record>> cache;
public:
bool Search(const PathName& relativePath,
const string& pattern,
bool all,
vector<Fndb::Record>& results) override {
SearchKey key = MakeSearchKey(relativePath, pattern, all);
auto it = cache.find(key);
if (it != cache.end()) {
results = it->second;
return !results.empty();
}
// 执行实际搜索并缓存结果
bool found = FileNameDatabase::Search(relativePath, pattern, all, results);
cache[key] = results;
return found;
}
};
九、FNDB 的实际使用示例
示例1:查找文档类文件
void FindDocumentClass() {
FileNameDatabase fndb(GetTeXMFRoot());
vector<Fndb::Record> results;
// 搜索 article 文档类
if (fndb.Search("article.cls", "tex//latex//", false, results)) {
PathName articlePath = results[0].path;
cout << "Found: " << articlePath << endl;
}
}示例2:获取文件信息
void GetFileStatistics() {
FileNameDatabase fndb(GetTeXMFRoot());
vector<Fndb::Record> results;
// 获取所有 .tex 文件
fndb.Search("*.tex", "tex//", true, results);
size_t totalSize = 0;
for (const auto& record : results) {
totalSize += record.info.fileSize;
}
cout << "Total .tex files: " << results.size() << endl;
cout << "Total size: " << totalSize << " bytes" << endl;
}
示例3:验证包完整性
bool VerifyPackage(const string& packageName) {
FileNameDatabase fndb(GetTeXMFRoot());
vector<Fndb::Record> results;
// 检查包是否包含必要的文件类型
vector<string> requiredFiles = {"*.sty", "*.tex", "*.doc"};
for (const auto& pattern : requiredFiles) {
string searchPattern = "tex//" + packageName + "//" + pattern;
if (!fndb.Search(pattern, searchPattern, false, results)) {
return false; // 缺少必需文件
}
}
return true;
}
十、FNDB 的管理命令
10.1 命令行工具
MiKTeX 提供 initexmf 命令管理 FNDB:
# 更新 FNDB initexmf --update-fndb # 重建 FNDB initexmf --rebuild-fndb # 显示 FNDB 统计信息 initexmf --report-fndb # 从 FNDB 中查找文件 initexmf --find-file=article.cls
10.2 编程接口
// 在代码中管理 FNDB
void MaintainFNDB() {
FileNameDatabase::RebuildAll(); // 重建所有 FNDB
// 或者针对特定根目录
FileNameDatabase fndb(texmfRoot);
fndb.Rebuild();
}
十一、性能监控和调优
11.1 监控指标
struct FNDBStatistics {
size_t totalFiles; // 总文件数
size_t totalDirectories; // 总目录数
size_t memoryUsage; // 内存使用量
double averageSearchTime; // 平均搜索时间
size_t cacheHitRate; // 缓存命中率
};
11.2 性能调优
void TuneFNDBPerformance() {
// 调整哈希表大小以减少冲突
fndb.SetHashTableSize(optimalSize);
// 启用压缩以减少内存使用
fndb.EnableCompression(true);
// 调整缓存策略
fndb.SetCacheSize(cacheSize);
}
十二、故障排除
12.1 常见问题
FNDB 过时
// 症状:找不到新安装的文件 // 解决方案:更新 FNDB initexmf --update-fndb
FNDB 损坏
// 症状:搜索返回错误结果或崩溃 // 解决方案:重建 FNDB initexmf --rebuild-fndb
权限问题
// 症状:无法更新 FNDB
// 解决方案:以管理员权限运行
RunAsAdministrator("initexmf --update-fndb");
12.2 调试信息
void DebugFNDB() {
// 启用详细日志
trace_fndb->SetLevel(TraceLevel::Debug);
// 搜索时会输出详细日志
fndb.Search("test.tex", "tex//", false, results);
}