FNDB(FileName DataBase)文件名数据库

MiKTeX 中的 FNDB(FileName DataBase) 是一个核心组件,它是专门设计用于高效文件搜索的索引数据库。使得在庞大的 TEXMF 目录树中快速定位文件成为可能,显著提升了 TeX 文档编译和包管理的效率。

  1. 提供快速文件搜索:通过内存中的哈希表实现毫秒级查找
  2. 减少磁盘 I/O:避免昂贵的文件系统遍历
  3. 支持复杂搜索模式:集成递归目录匹配算法
  4. 自动维护更新:通过更改文件机制保持同步
  5. 跨平台一致性:在不同操作系统上提供统一的搜索体验

 

一、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流程。

  1. (makefmt)生成了文件pdftexconfig.tex,要写入fndb,发现fndb文件没有,于是调用FndbManager::Create创建fndb文件。
  2. {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就没有,啥有没做。
  3. (makefmt)Fndb::Add的后半段,执行FileNameDatabase::Add。把pdftexconfig.tex加入到FileNameDatabase对象,但此文件已在之前由fndb文件得到的fileNames,不需要加入,于是此个FileNameDatabase::Add只是生成一个空的fndb.log。一旦有操做,那会以“+”的方式向fndb.log增加一些记录。
  4. (pdftex)(还没有生成pdflatex.fmt)会遇到一次需要查此个fndb的操作,于是调用FileNameDatabase::Search,Search会调用ApplyChangeFile()。fndb.log文件是空,值newChangeFileSize是0,不执行合并。
  5. (pdftex)生成pdflatex.fmt,此时是放在tmp目录。
  6. (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。
  7. 把pdflatex.fmt从tmp目录复制到miktex\data\le\pdftex目录。复制执行的是File::Copy操作,设置了UpdateFndb标记。也就是说,除了复制文件,还执行Fndb::Add。后者会执行FileNameDatabase::InsertRecord,把pdflatex.fmt加入fndb哈希表,并把这条加入文件以“+”的方法写到fndb.log。
  8. (miktex)使用此个fndb,也就都有fndb.log、pdflatex.fmt了。
  9. 至此在内存中,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须满足两个要求。

  1. 是在tex根目录是<miktex>/sandbox下的文件。
  2. 这文件肯定已存在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);
}

全部评论: 0

    写评论: