比特币源码解析(16) - 可执行程序 - Bitcoind

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012183589/article/details/78061722

0x01 AppInitSanityChecks - Step 4 sanity checks

bool AppInitSanityChecks()
{
    // ********************************************************* Step 4: sanity checks

    // Initialize elliptic curve code
    std::string sha256_algo = SHA256AutoDetect();
    LogPrintf("Using the '%s' SHA256 implementation\n", sha256_algo);
    RandomInit();
    ECC_Start();
    globalVerifyHandle.reset(new ECCVerifyHandle());

    // Sanity check
    if (!InitSanityCheck())
        return InitError(strprintf(_("Initialization sanity check failed. %s is shutting down."), _(PACKAGE_NAME)));

    // Probe the data directory lock to give an early error message, if possible
    // We cannot hold the data directory lock here, as the forking for daemon() hasn't yet happened,
    // and a fork will cause weird behavior to it.
    return LockDataDirectory(true);
}

椭圆曲线参数初始化

这一步的整个函数都相对较短,代码首先根据cpuid决定使用何种版本的sha256算法,SHA256AutoDetect函数具体实现如下,

std::string SHA256AutoDetect()
{
#if defined(EXPERIMENTAL_ASM) && (defined(__x86_64__) || defined(__amd64__))
    uint32_t eax, ebx, ecx, edx;
    if (__get_cpuid(1, &eax, &ebx, &ecx, &edx) && (ecx >> 19) & 1) {
        Transform = sha256_sse4::Transform;
        assert(SelfTest(Transform));
        return "sse4";
    }
#endif

    assert(SelfTest(Transform));
    return "standard";
}

对于64位的系统通过__get_cpuid获取cpuid,然后通过ecx来获取具体芯片的feature flags,具体可以参考https://en.wikipedia.org/wiki/CPUID,ecx的第19位正好对应sse4.1的指令,其他情况就返回standard。接下来的RandomInit()同样也是根据cpuid来决定是否增加RDRAND作为额外的随机源,对于RDRAND这里https://software.intel.com/en-us/blogs/2012/11/17/the-difference-between-rdrand-and-rdseed有很好的解释,上面链接主要解释RDRANDRDSEED的却别,简单来说就是根据输出的值的作用来决定使用哪个,如果输出是作为其他PRNG(Pseudorandom number generator, 伪随机数生成器)的种子,那么就使用RDSEED;其他情况都使用RDRANDECC_Start()开始进行椭圆曲线参数初始化。比特币中所有的公钥、私钥、签名等密码学技术基本上都是使用的椭圆曲线体系。

Sanity Check

接下来的一句代码进行bitcoin运行时需要的一些基本库的完整性检测,就是调用测试程序来测试它们是否都正常工作,以确保bitcoin运行环境正常。检测的模块包括以下几个部分,

/** Sanity checks
 *  Ensure that Bitcoin is running in a usable environment with all
 *  necessary library support.
 */
bool InitSanityCheck(void)
{
    if(!ECC_InitSanityCheck()) {
        InitError("Elliptic curve cryptography sanity check failure. Aborting.");
        return false;
    }

    if (!glibc_sanity_test() || !glibcxx_sanity_test())
        return false;

    if (!Random_SanityCheck()) {
        InitError("OS cryptographic RNG sanity check failure. Aborting.");
        return false;
    }

    return true;
}
  • ECC_InitSanityCheck():椭圆曲线测试。
  • glibc_sanity_test() & glibcxx_sanity_check():glibc和glibcxx库测试。
  • Random_SanityCheck():随机数生成环境测试。

测试锁定数据目录

LockDataDirectory()的实现如下,

static bool LockDataDirectory(bool probeOnly)
{
    std::string strDataDir = GetDataDir().string();

    // Make sure only a single Bitcoin process is using the data directory.
    fs::path pathLockFile = GetDataDir() / ".lock";
    FILE* file = fsbridge::fopen(pathLockFile, "a"); // empty lock file; created if it doesn't exist.
    if (file) fclose(file);

    try {
        static boost::interprocess::file_lock lock(pathLockFile.string().c_str());
        if (!lock.try_lock()) {
            return InitError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running."), strDataDir, _(PACKAGE_NAME)));
        }
        if (probeOnly) {
            lock.unlock();
        }
    } catch(const boost::interprocess::interprocess_exception& e) {
        return InitError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running.") + " %s.", strDataDir, _(PACKAGE_NAME), e.what()));
    }
    return true;
}

函数传入一个bool型参数probeOnly,为true表示只是测试是否能进行锁定但并不实际锁定,false表示将数据目录(也就是~/.bitcoin/)进行锁定,这里的锁定是指进程间的锁定,也就是只允许当前进程访问锁定的文件,其他进程不允许访问,如果probeOnlytrue,那么在锁定之后再进行解锁。

0x02 AppInitMain Step 4a: application initialization

终于到主函数了!

创建PID文件

bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler)
{
    const CChainParams& chainparams = Params();
    // ********************************************************* Step 4a: application initialization
#ifndef WIN32
    CreatePidFile(GetPidFile(), getpid());
#endif

AppInitMain传入两个参数,threadGroupscheduler,这两个参数到传入前都是空值。然后获取chainparams,这个变量之前出现过多次,主要是包含一些共识的参数,自身是根据选择不同的网络maintestnet或者regtest来生成不同的参数。接下来对于非Windows系统创建进程的PID文件,

转载:http://www.linuxidc.com/Linux/2012-12/76950.htm

(1) pid文件的内容:pid文件为文本文件,内容只有一行, 记录了该进程的ID。 用cat命令可以看到。

(2) pid文件的作用:防止进程启动多个副本。只有获得pid文件(固定路径固定文件名)写入权限(F_WRLCK)的进程才能正常启动并把自身的PID写入该文件中。其它同一个程序的多余进程则自动退出。

通过上面文章我们知道pid文件的作用就是防止进程启动多个副本,从而打乱原有的消息传输。

限定日志大小

    if (gArgs.GetBoolArg("-shrinkdebugfile", logCategories == BCLog::NONE)) {
        // Do this first since it both loads a bunch of debug.log into memory,
        // and because this needs to happen before any other debug.log printing
        ShrinkDebugFile();
    }

-shrinkdebugfile:限制日志文件的大小,如果没有设置-debug参数,那么该变量的默认值为1.

如果设置了这个参数那么就会执行ShrinkDebugFile函数,该函数的实现位于src/util.cpp

void ShrinkDebugFile()
{
    // Amount of debug.log to save at end when shrinking (must fit in memory)
    constexpr size_t RECENT_DEBUG_HISTORY_SIZE = 10 * 1000000;
    // Scroll debug.log if it's getting too big
    fs::path pathLog = GetDataDir() / "debug.log";
    FILE* file = fsbridge::fopen(pathLog, "r");
    // If debug.log file is more than 10% bigger the RECENT_DEBUG_HISTORY_SIZE
    // trim it down by saving only the last RECENT_DEBUG_HISTORY_SIZE bytes
    if (file && fs::file_size(pathLog) > 11 * (RECENT_DEBUG_HISTORY_SIZE / 10))
    {
        // Restart the file with some of the end
        std::vector<char> vch(RECENT_DEBUG_HISTORY_SIZE, 0);
        fseek(file, -((long)vch.size()), SEEK_END);
        int nBytes = fread(vch.data(), 1, vch.size(), file);
        fclose(file);

        file = fsbridge::fopen(pathLog, "w");
        if (file)
        {
            fwrite(vch.data(), 1, nBytes, file);
            fclose(file);
        }
    }
    else if (file != nullptr)
        fclose(file);
}

我们可以看到该函数首先定义了一个变量RECENT_DEBUG_HISTORY_SIZE,大小为10MB,然后判断debug.log文件的大小,如果小于11MB,那么就不做处理;否则读取文件的最后RECENT_DEBUG_HISTORY_SIZE这么多字节,重新保存到debug.log文件中。

DebugLog显示处理

    if (fPrintToDebugLog)
        OpenDebugLog();

    if (!fLogTimestamps)
        LogPrintf("Startup time: %s\n", DateTimeStrFormat("%Y-%m-%d %H:%M:%S", GetTime()));
    LogPrintf("Default data directory %s\n", GetDefaultDataDir().string());
    LogPrintf("Using data directory %s\n", GetDataDir().string());
    LogPrintf("Using config file %s\n", GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)).string());
    LogPrintf("Using at most %i automatic connections (%i file descriptors available)\n", nMaxConnections, nFD);

fPrintToDebugLog表示是否打开debug.log文件,默认值为true,只有在bench_bitcoin.cpp中才设为falseOpenDebugLog()函数的实现如下:

static void DebugPrintInit()
{
    assert(mutexDebugLog == nullptr);
    mutexDebugLog = new boost::mutex();
    vMsgsBeforeOpenLog = new std::list<std::string>;
}

void OpenDebugLog()
{
    boost::call_once(&DebugPrintInit, debugPrintInitFlag);
    boost::mutex::scoped_lock scoped_lock(*mutexDebugLog);

    assert(fileout == nullptr);
    assert(vMsgsBeforeOpenLog);
    fs::path pathDebug = GetDataDir() / "debug.log";
    fileout = fsbridge::fopen(pathDebug, "a");
    if (fileout) {
        setbuf(fileout, nullptr); // unbuffered
        // dump buffered messages from before we opened the log
        while (!vMsgsBeforeOpenLog->empty()) {
            FileWriteStr(vMsgsBeforeOpenLog->front(), fileout);
            vMsgsBeforeOpenLog->pop_front();
        }
    }

    delete vMsgsBeforeOpenLog;
    vMsgsBeforeOpenLog = nullptr;
}

boost::call_once表示在多线程访问该语句时始终只执行一次调用的函数,其中第一个参数是被调用的函数地址,第二个参数类型为boost::once_flag,代码中使用的是debugPrintInitFlag,该变量的定义为,

static boost::once_flag debugPrintInitFlag = BOOST_ONCE_INIT;

在函数DebugPrintInit()中定义了变量mutexDebugLog,类型为boost::mutex::scoped_lock,用来保证函数中后续代码是线程安全的,scoped_lock类似于我们之前在http://blog.csdn.net/pure_lady/article/details/77675915#t3中讲过的lock_guard, 能够保证在作用域范围内是互斥访问的,离开作用域时由析构函数自动解锁。但是接下来的一段代码有些奇怪,如果按照简单的代码执行顺序来看,在DebugPrintInit()中创建了对象vMsgsBeforeOpenLog,类型为链表,这时链表肯定是空的;而到了OpenDebugLog()函数中直接有个while将vMsgsBeforeOpenLog中的字符串写入到fileout中,空链表有什么好写的?但是用gbd调试程序时,运行到这里发现vMsgsBeforeOpenLog竟然不是空的!

解释

全局搜索vMsgsBeforeOpenLog之后发现,还有个地方使用了这个变量,在src/util.cpp中的LogPrintStr()里,

// src/util.cpp line 343       
      boost::call_once(&DebugPrintInit, debugPrintInitFlag);
        boost::mutex::scoped_lock scoped_lock(*mutexDebugLog);

        // buffer if we haven't opened the log yet
        if (fileout == nullptr) {
            assert(vMsgsBeforeOpenLog);
            ret = strTimestamped.length();
            vMsgsBeforeOpenLog->push_back(strTimestamped);
        }

从上面代码可以发现这里也调用了DebugPrintInit()这个函数,所以远在OpenDebugLog()之前,vMsgsBeforeOpenLog变量就已经创建了,然后LogPrintStr在之后又被调用了很多次,每次都会往vMsgsBeforeOpenLog中写入一些东西,所以到OpenDebugLog()时该变量已经有内容了。

回到源代码,接下来几行开始打印一些提示信息,包括数据目录、最大连接数以及配置文件。

设置Cache大小

    InitSignatureCache();
    InitScriptExecutionCache();

-maxsigcachesize=<n>:限制signature cache的大小为n MiB,默认值为32.

这俩函数首先都是从命令行中获取-maxsigcachesize参数的值,然后分别通过signatureCachescriptExecutionCache中的set_bytes()函数设置最大的缓存大小。这个两个变量的类型都是Cache

,至于具体在什么时候用到还得看后面的代码。

创建Script Check Thead

    LogPrintf("Using %u threads for script verification\n", nScriptCheckThreads);
    if (nScriptCheckThreads) {
        for (int i=0; i<nScriptCheckThreads-1; i++)
            threadGroup.create_thread(&ThreadScriptCheck);
    }

nScriptCheckThreads是由参数中-par设定的,位于http://blog.csdn.net/pure_lady/article/details/77982837#t4,这段代码是根据参数来创建线程具体的线程。其中ThreadScriptCheck函数的实现如下,

void ThreadScriptCheck() {
    RenameThread("bitcoin-scriptch");
    scriptcheckqueue.Thread();
}

首先给当前线程重命名为bitcoin-scriptch,然后启动线程,线程启动的流程如下:

1 static CCheckQueue<CScriptCheck> scriptcheckqueue(128); // src/validation.cpp line 1535
2 //! Worker thread src/checkqueue.h line 136
    void Thread()
    {
        Loop();
    }
3 bool Loop(bool fMaster = false); // src/checkqueue.h line 69
4 // src/validation.cpp line 1202
  bool CScriptCheck::operator()() {
    const CScript &scriptSig = ptxTo->vin[nIn].scriptSig;
    const CScriptWitness *witness = &ptxTo->vin[nIn].scriptWitness;
    return VerifyScript(scriptSig, scriptPubKey, witness, nFlags, CachingTransactionSignatureChecker(ptxTo, nIn, amount, cacheStore, *txdata), &error);
}
5 // src/script/interpreter.cpp line 1407
  bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror);

脚本验证线程的启动步骤如上流程所示,最开始是在AppInitMain中使用threadGroup中的create_thread来创建新的线程,而每一个线程的回调函数都是ThreadScriptCheck(),在这个回调函数当中主要是通过一个CCheckQueue类型的静态变量scriptcheckqueue执行类中的Thread()成员函数,而Thread()函数又调用一个类中的Loop()私有函数,这个Loop函数就是整个script check的线程控制中心,Loop的实现如下:

    /** Internal function that does bulk of the verification work. */
    bool Loop(bool fMaster = false)
    {
        boost::condition_variable& cond = fMaster ? condMaster : condWorker;
        std::vector<T> vChecks;
        vChecks.reserve(nBatchSize);
        unsigned int nNow = 0;
        bool fOk = true;
        do {
            {
                boost::unique_lock<boost::mutex> lock(mutex);
                // first do the clean-up of the previous loop run (allowing us to do it in the same critsect)
                if (nNow) {
                    fAllOk &= fOk;
                    nTodo -= nNow;
                    if (nTodo == 0 && !fMaster)
                        // We processed the last element; inform the master it can exit and return the result
                        condMaster.notify_one();
                } else {
                    // first iteration
                    nTotal++;
                }
                // logically, the do loop starts here
                while (queue.empty()) {
                    if ((fMaster || fQuit) && nTodo == 0) {
                        nTotal--;
                        bool fRet = fAllOk;
                        // reset the status for new work later
                        if (fMaster)
                            fAllOk = true;
                        // return the current status
                        return fRet;
                    }
                    nIdle++;
                    cond.wait(lock); // wait
                    nIdle--;
                }
                // Decide how many work units to process now.
                // * Do not try to do everything at once, but aim for increasingly smaller batches so
                //   all workers finish approximately simultaneously.
                // * Try to account for idle jobs which will instantly start helping.
                // * Don't do batches smaller than 1 (duh), or larger than nBatchSize.
                nNow = std::max(1U, std::min(nBatchSize, (unsigned int)queue.size() / (nTotal + nIdle + 1)));
                vChecks.resize(nNow);
                for (unsigned int i = 0; i < nNow; i++) {
                    // We want the lock on the mutex to be as short as possible, so swap jobs from the global
                    // queue to the local batch vector instead of copying.
                    vChecks[i].swap(queue.back());
                    queue.pop_back();
                }
                // Check whether we need to do work at all
                fOk = fAllOk;
            }
            // execute work
            for (T& check : vChecks)
                if (fOk)
                    fOk = check();
            vChecks.clear();
        } while (true);
    }

分析这段代码首先要注意两个变量,第一个是std::vector<T> vChecks;中的T代表什么,从scriptcheckqueue的定义static CCheckQueue<CScriptCheck> scriptcheckqueue(128);来看,T代表的是CScriptCheck,这个CScriptCheck的定义在src/validation.h line 355处,其中的一个最主要的函数就是重载的()操作符,这可以解释上面代码的最后fOk = check();这句,我们稍后再来看这个函数的实现;第二个需要注意的变量是boost::condition_variable& cond 这个变量也是整个函数的核心 ,关于boost condition_variable可以参考http://blog.csdn.net/pure_lady/article/details/78057467#t5这篇文章,可能会更方便理解。

明白了上面两个变量再看起代码来会感觉清晰不少,这里面总共有两种角色,MasterWorker,其中Master负责统计结果,Worker负责执行具体的脚本验证。从源码的执行顺序来看,首先在AppInitMain中执行Thread()函数,也就是创建Worker,此时任务队列queue为空,所有的Worker创建后都在cond.wait(lock)处阻塞,等到有新的任务被加到queue中时,才会被唤醒批量执行任务;最后调用Wait()函数创建一个Master,等待所有的Worker执行完,统计执行结果并返回。

分析完WorkerMaster的工作流程,下面再来分析一下CScriptCheck重载的()操作符,也就是上面代码中的check()函数,它的实现如下(位于src/validation.cpp line 1202):

// src/validation.cpp line 1202
bool CScriptCheck::operator()() {
    const CScript &scriptSig = ptxTo->vin[nIn].scriptSig;
    const CScriptWitness *witness = &ptxTo->vin[nIn].scriptWitness;
    return VerifyScript(scriptSig, scriptPubKey, witness, nFlags, CachingTransactionSignatureChecker(ptxTo, nIn, amount, cacheStore, *txdata), &error);
}

其中scriptSig就是用户的签名也是解锁脚本,witness是隔离见证中从scriptSig中分离出来的,scriptPubKey也就是锁定脚本,然后调用VerifyScript来验证锁定脚本和解锁脚本是否能正确结合运行。关于脚本的验证和执行以及脚本的结构,包含的具体内容等等,我打算在后面专门再写一章来介绍比特币中的脚本系统,目前就就可当成一个封装的模块,知道它的功能就可以。

展开阅读全文

没有更多推荐了,返回首页