比特币源码剖析(十五)
本篇主要分析 Step 9: data directory maintenance 第九步数据目录维护的详细过程。
源码剖析
3.11.9.第九步,数据目录维护。这部分代码实现在“init.cpp”文件的 AppInit2(…) 函数中。
bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) // 3.11.程序初始化,共 12 步
{
...
// ********************************************************* Step 9: data directory maintenance // 若是裁剪模式且关闭了再索引选项,则进行 blockstore 的裁剪
// if pruning, unset the service bit and perform the initial blockstore prune // 如果正在修剪,
// after any wallet rescanning has taken place. // 在任何钱包再扫描发生后,取消设置服务位并执行初始区块存储修剪。
if (fPruneMode) { // 裁剪标志,默认为 false
LogPrintf("Unsetting NODE_NETWORK on prune mode\n");
nLocalServices &= ~NODE_NETWORK; // 取消设置本地服务中的 NODE_NETWORK
if (!fReindex) { // 若再索引标志关闭
uiInterface.InitMessage(_("Pruning blockstore...")); // 开始修剪区块存储
PruneAndFlush(); // 设置修剪标志并刷新磁盘上的链状态
}
}
...
}
若满足条件,开启了裁剪模式且关闭了再索引选项,则进行数据目录中区块的修剪。 这里调用 PruneAndFlush() 来设置修剪标志并刷新磁盘上的链状态。 该函数声明在“main.h”文件中。
/** Flush all state, indexes and buffers to disk. */
void FlushStateToDisk(); // 刷新全部状态,索引和缓冲到磁盘。
/** Prune block files and flush state to disk. */
void PruneAndFlush(); // 修建区块文件并刷新状态到磁盘
实现在“main.cpp”文件中,没有入参。
/**
* Update the on-disk chain state.
* The caches and indexes are flushed depending on the mode we're called with
* if they're too large, if it's been a while since the last write,
* or always and in all cases if we're in prune mode and are deleting files.
*/ // 更新磁盘上的链状态。缓存和索引根据我们调用的模式刷新,如果它们太大,或经上次写入已有一段时间,或总在所有情况下,我们处于修剪模式并正在删除文件。
bool static FlushStateToDisk(CValidationState &state, FlushStateMode mode) {
const CChainParams& chainparams = Params();
LOCK2(cs_main, cs_LastBlockFile);
static int64_t nLastWrite = 0;
static int64_t nLastFlush = 0;
static int64_t nLastSetChain = 0;
std::set<int> setFilesToPrune; // 修剪的文件集合
bool fFlushForPrune = false;
try {
if (fPruneMode && fCheckForPruning && !fReindex) {
FindFilesToPrune(setFilesToPrune, chainparams.PruneAfterHeight());
fCheckForPruning = false;
if (!setFilesToPrune.empty()) {
fFlushForPrune = true;
if (!fHavePruned) {
pblocktree->WriteFlag("prunedblockfiles", true);
fHavePruned = true;
}
}
}
int64_t nNow = GetTimeMicros();
// Avoid writing/flushing immediately after startup. // 避免在启动后立刻写入/刷新。
if (nLastWrite == 0) {
nLastWrite = nNow;
}
if (nLastFlush == 0) {
nLastFlush = nNow;
}
if (nLastSetChain == 0) {
nLastSetChain = nNow;
}
size_t cacheSize = pcoinsTip->DynamicMemoryUsage(); // 获取动态内存用量
// The cache is large and close to the limit, but we have time now (not in the middle of a block processing). // 缓存很大并接近极限,但我们现在有时间(不再块处理的中间)。
bool fCacheLarge = mode == FLUSH_STATE_PERIODIC && cacheSize * (10.0/9) > nCoinCacheUsage;
// The cache is over the limit, we have to write now. // 缓存超过极限,我们现在写入。
bool fCacheCritical = mode == FLUSH_STATE_IF_NEEDED && cacheSize > nCoinCacheUsage;
// It's been a while since we wrote the block index to disk. Do this frequently, so we don't need to redownload after a crash. // 这需要一段时间,因为我们写区块索引到磁盘。经常这么做,所以我们不需要在崩溃后重新下载
bool fPeriodicWrite = mode == FLUSH_STATE_PERIODIC && nNow > nLastWrite + (int64_t)DATABASE_WRITE_INTERVAL * 1000000;
// It's been very long since we flushed the cache. Do this infrequently, to optimize cache usage. // 这会花很长时间,因为我们刷新了缓存。不常这样做,以优化缓存使用。
bool fPeriodicFlush = mode == FLUSH_STATE_PERIODIC && nNow > nLastFlush + (int64_t)DATABASE_FLUSH_INTERVAL * 1000000;
// Combine all conditions that result in a full cache flush. // 合并所有导致完整缓存刷新的条件。
bool fDoFullFlush = (mode == FLUSH_STATE_ALWAYS) || fCacheLarge || fCacheCritical || fPeriodicFlush || fFlushForPrune;
// Write blocks and block index to disk. // 写入区块和区块索引到磁盘。
if (fDoFullFlush || fPeriodicWrite) {
// Depend on nMinDiskSpace to ensure we can write block index // 基于 nMinDiskSpace 以确保我们能写入区块索引
if (!CheckDiskSpace(0)) // 检查当前的磁盘空间
return state.Error("out of disk space");
// First make sure all block and undo data is flushed to disk. // 首次确保全部区块和恢复数据被刷新到磁盘
FlushBlockFile(); // 刷新区块文件
// Then update all block file information (which may refer to block and undo files). // 然后更新全部区块文件信息(可能参照区块和恢复文件)。
{
std::vector<std::pair<int, const CBlockFileInfo*> > vFiles; // <脏掉的文件号,区块文件信息指针>
vFiles.reserve(setDirtyFileInfo.size());
for (set<int>::iterator it = setDirtyFileInfo.begin(); it != setDirtyFileInfo.end(); ) {
vFiles.push_back(make_pair(*it, &vinfoBlockFile[*it]));
setDirtyFileInfo.erase(it++);
}
std::vector<const CBlockIndex*> vBlocks; // 区块索引列表
vBlocks.reserve(setDirtyBlockIndex.size());
for (set<CBlockIndex*>::iterator it = setDirtyBlockIndex.begin(); it != setDirtyBlockIndex.end(); ) { // 遍历脏掉的区块索引集合
vBlocks.push_back(*it);
setDirtyBlockIndex.erase(it++);
}
if (!pblocktree->WriteBatchSync(vFiles, nLastBlockFile, vBlocks)) { // 批量写入同步
return AbortNode(state, "Files to write to block index database");
}
}
// Finally remove any pruned files // 最终移除所有修剪的文件
if (fFlushForPrune)
UnlinkPrunedFiles(setFilesToPrune);
nLastWrite = nNow;
}
// Flush best chain related state. This can only be done if the blocks / block index write was also done. // 刷新最佳链相关的状态。该操作仅在区块/区块索引写入完成后执行。
if (fDoFullFlush) {
// Typical CCoins structures on disk are around 128 bytes in size. // 磁盘上的 CCoins 典型结构约 128 字节。
// Pushing a new one to the database can cause it to be written // 将推送一个新数据到数据库可能导致
// twice (once in the log, and once in the tables). This is already // 它被写入 2 次(一次在日志中,一次在表中)。
// an overestimation, as most will delete an existing entry or // 这已经是高估了,因为大多数人会删除现有条目或覆盖现有条目。
// overwrite one. Still, use a conservative safety factor of 2. // 仍使用保守的安全系数 2。
if (!CheckDiskSpace(128 * 2 * 2 * pcoinsTip->GetCacheSize()))
return state.Error("out of disk space");
// Flush the chainstate (which may refer to block index entries). // 刷新链状态(可参考区块索引条目)。
if (!pcoinsTip->Flush())
return AbortNode(state, "Failed to write to coin database");
nLastFlush = nNow;
}
if (fDoFullFlush || ((mode == FLUSH_STATE_ALWAYS || mode == FLUSH_STATE_PERIODIC) && nNow > nLastSetChain + (int64_t)DATABASE_WRITE_INTERVAL * 1000000)) {
// Update best block in wallet (so we can detect restored wallets). // 在钱包中更新最佳区块(以至于我们能检测到恢复的钱包)。
GetMainSignals().SetBestChain(chainActive.GetLocator());
nLastSetChain = nNow;
}
} catch (const std::runtime_error& e) {
return AbortNode(state, std::string("System error while flushing: ") + e.what());
}
return true; // 本地化状态成功返回 true
}
void FlushStateToDisk() {
CValidationState state;
FlushStateToDisk(state, FLUSH_STATE_ALWAYS);
}
void PruneAndFlush() {
CValidationState state;
fCheckForPruning = true; // 全局修剪标志置为 true
FlushStateToDisk(state, FLUSH_STATE_NONE); // 刷新磁盘上的链状态
}
3.11.10.第九步,数据目录维护。这部分代码实现在“init.cpp”文件的 AppInit2(…) 函数中。
bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) // 3.11.程序初始化,共 12 步
{
...
// ********************************************************* Step 10: import blocks // 导入区块数据
if (mapArgs.count("-blocknotify")) // 1.若注册了区块通知的命令
uiInterface.NotifyBlockTip.connect(BlockNotifyCallback); // 连接区块通知回调函数
uiInterface.InitMessage(_("Activating best chain..."));
// scan for better chains in the block chain database, that are not yet connected in the active best chain // 扫描区块链数据库中的最佳链,这些链还没连接到激活的最佳链
CValidationState state;
if (!ActivateBestChain(state, chainparams)) // 2.激活最佳链,并获取验证状态
strErrors << "Failed to connect best block";
std::vector<boost::filesystem::path> vImportFiles; // 导入文件列表(存放导入区块文件的路径)
if (mapArgs.count("-loadblock")) // 导入区块文件选项
{
BOOST_FOREACH(const std::string& strFile, mapMultiArgs["-loadblock"]) // 3.遍历指定的区块文件
vImportFiles.push_back(strFile); // 放入文件列表
}
threadGroup.create_thread(boost::bind(&ThreadImport, vImportFiles)); // 4.创建一个用于导入区块线程
if (chainActive.Tip() == NULL) { // 至少创世区块要加载完成
LogPrintf("Waiting for genesis block to be imported...\n");
while (!fRequestShutdown && chainActive.Tip() == NULL) // 必须保证至少加载创世区块
MilliSleep(10); // 否则睡 10ms 等待导入区块线程完成工作
}
...
}
1.调用区块通知回调函数,注册并运行指定命令。
2.激活当前最佳区块链,并获取验证状态。
3.遍历指定的区块文件路径,加入导入文件列表。
4.创建一个线程,用于导入指定的区块文件。
1.调用 uiInterface.NotifyBlockTip.connect(BlockNotifyCallback) 注册区块通知回调函数 BlockNotifyCallback。 该函数实现在“init.cpp”文件中,入参为:初始化标志,区块索引。
static void BlockNotifyCallback(bool initialSync, const CBlockIndex *pBlockIndex)
{
if (initialSync || !pBlockIndex)
return;
std::string strCmd = GetArg("-blocknotify", ""); // 获取指定的命令
boost::replace_all(strCmd, "%s", pBlockIndex->GetBlockHash().GetHex()); // 替换最佳区块哈希的 16 进制形式
boost::thread t(runCommand, strCmd); // thread runs free // 传入命令,运行处理命令线程
}
这里创建了一个局部线程对象来执行传入的命令。 线程函数 runCommand 声明在“util.h”文件中。
void runCommand(const std::string& strCommand); // 运行 shell 命令
实现在“util.cpp”文件中,入参为:待执行的命令。
void runCommand(const std::string& strCommand)
{
int nErr = ::system(strCommand.c_str()); // 执行命令 bash 命令
if (nErr)
LogPrintf("runCommand error: system(%s) returned %d\n", strCommand, nErr);
}
这里直接进行系统调用执行传入的命令。
4.调用 threadGroup.create_thread(boost::bind(&ThreadImport, vImportFiles)) 在线程组中创建一个线程,用于导入指定的区块文件到内存。 线程函数 ThreadImport 实现在“init.cpp” 文件中,入参为:指定的待导入去快文件路径列表。
void ThreadImport(std::vector<boost::filesystem::path> vImportFiles) // 导入区块线程处理函数
{
const CChainParams& chainparams = Params(); // 获取区块链参数
RenameThread("bitcoin-loadblk"); // 重命名为加载区块线程
// -reindex // 再索引选项
if (fReindex) {
CImportingNow imp; // 创建导入对象,把导入标志置为 true
int nFile = 0; // 文件序号从 0 开始
while (true) { // 循环加载区块
CDiskBlockPos pos(nFile, 0); // 创建区块文件位置对象
if (!boost::filesystem::exists(GetBlockPosFilename(pos, "blk"))) // 判断该文件是否存在
break; // No block files left to reindex // 若没有剩余区块文件用于再索引,则跳出
FILE *file = OpenBlockFile(pos, true); // 若该文件存在,则打开
if (!file) // 若文件打开失败
break; // This error is logged in OpenBlockFile // 记录错误信息到日志
LogPrintf("Reindexing block file blk%05u.dat...\n", (unsigned int)nFile);
LoadExternalBlockFile(chainparams, file, &pos); // 加载外部的区块文件
nFile++; // 文件号加 1
}
pblocktree->WriteReindexing(false); // 写入再索引标志
fReindex = false; // 再索引置为 false
LogPrintf("Reindexing finished\n");
// To avoid ending up in a situation without genesis block, re-try initializing (no-op if reindexing worked): // 为避免在没有创世区块的情况下结束,再次尝试初始化(若再索引起作用了,则无操作):
InitBlockIndex(chainparams); // 初始化区块索引数据库
}
// hardcoded $DATADIR/bootstrap.dat // 硬编码的 $DATADIR/bootstrap.dat
boost::filesystem::path pathBootstrap = GetDataDir() / "bootstrap.dat"; // 路径拼接
if (boost::filesystem::exists(pathBootstrap)) { // 若该文件存在
FILE *file = fopen(pathBootstrap.string().c_str(), "rb"); // 以 2 进制只读模式打开文件
if (file) {
CImportingNow imp;
boost::filesystem::path pathBootstrapOld = GetDataDir() / "bootstrap.dat.old";
LogPrintf("Importing bootstrap.dat...\n");
LoadExternalBlockFile(chainparams, file); // 加载外部的区块文件
RenameOver(pathBootstrap, pathBootstrapOld); // 重命名文件,增加 .old 后缀
} else {
LogPrintf("Warning: Could not open bootstrap file %s\n", pathBootstrap.string());
}
}
// -loadblock= // 导入区块选项
BOOST_FOREACH(const boost::filesystem::path& path, vImportFiles) { // 遍历待导入的去快文件路径列表
FILE *file = fopen(path.string().c_str(), "rb"); // 以二进制只读模式打开
if (file) {
CImportingNow imp;
LogPrintf("Importing blocks file %s...\n", path.string());
LoadExternalBlockFile(chainparams, file); // 加载外部区块文件到内存
} else {
LogPrintf("Warning: Could not open blocks file %s\n", path.string());
}
}
if (GetBoolArg("-stopafterblockimport", DEFAULT_STOPAFTERBLOCKIMPORT)) { // 导入区块后停止
LogPrintf("Stopping after block import\n");
StartShutdown(); // 关闭客户端
}
}