比特币学习笔记(四)---解读入口部分源码

我个人目前使用的源码版本是最新的开发版master,介于0.18-0.19版本之间,如果有人发现代码不一致,可自行寻找其它版本代码解析文章查看。

回归正题,开始看源码,我们从程序bitcoind开始看起。

bitcoind的入口函数是这么写的。

int main(int argc, char* argv[])
{
#ifdef WIN32
    util::WinCmdLineArgs winArgs;
    std::tie(argc, argv) = winArgs.get();
#endif
    SetupEnvironment();

    // Connect bitcoind signal handlers
    noui_connect();

    return (AppInit(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE);
}

WIN平台下的ifdef暂且忽略,剩下的就是SetupEnvironment(),noui_connect(),AppInit()这三个函数。

SetupEnvironment

首先来看SetupEnvironment,代码具体实现如下

void SetupEnvironment()
{
#ifdef HAVE_MALLOPT_ARENA_MAX
    // glibc-specific: On 32-bit systems set the number of arenas to 1.          这里判断如果是32位系统(sizeof(void*) == 4),即只分配一个arena内存区,防止虚拟地址空间
    // By default, since glibc 2.10, the C library will create up to two heap    过度使用。自从glibc 2.10版本后,针对每个核心,C标准库都会默认创建两个堆内存区。这是公
    // arenas per core. This is known to cause excessive virtual address space   认会导致内存地址空间过度使用的问题。
    // usage in our usage. Work around it by setting the maximum number of       科普:arena:可以理解为一个较大且连续的内存分配区,需要手动来管理。
    // arenas to 1.                                                                 科普2:64位系统一个指针的长度为8字节
    if (sizeof(void*) == 4) {
        mallopt(M_ARENA_MAX, 1);
    }
#endif
    // On most POSIX systems (e.g. Linux, but not BSD) the environment's locale  在大部分POSIX系统(比如Linux,但非BSD),环境的locale即系统区域设置可能会无效,假如当前的本地化设置无效,将抛出该运行时异常,同时在捕获后将该值设置为“C”。
    // may be invalid, in which case the "C" locale is used as fallback.         Tips:locale决定当前程序所使用的语言编码、日期格式、数字格式及其它与区域有关的设置,详情参考:http://blog.csdn.net/haiross/article/details/45074355
#if !defined(WIN32) && !defined(MAC_OSX) && !defined(__FreeBSD__) && !defined(__OpenBSD__)
    try {
        std::locale(""); // Raises a runtime error if current locale is invalid
    } catch (const std::runtime_error&) {
        setenv("LC_ALL", "C", 1);
    }
#elif defined(WIN32)
    // Set the default input/output charset is utf-8
    SetConsoleCP(CP_UTF8);
    SetConsoleOutputCP(CP_UTF8);
#endif
    // The path locale is lazy initialized and to avoid deinitialization errors        路径区域设置是采用懒初始化,即延迟初始化的方式,并且可以避免多线程环境下的析构过程错
    // in multithreading environments, it is set explicitly by the main thread.        误,通过主线程来显式设置。一个dummy locale可以用来提取fs::path使用的内部默认locale(路
    // A dummy locale is used to extract the internal default locale, used by        径),然后就可以显式设置该路径。
    // fs::path, which is then used to explicitly imbue the path.                    imbue:该函数将要设置的loc值与流和该流相关的流缓冲区(如果有的话)关联起来,作为新的系统区域设置对象。详情参考:http://www.cplusplus.com/reference/ios/ios/imbue/
    std::locale loc = fs::path::imbue(std::locale::classic());
#ifndef WIN32
    fs::path::imbue(loc);
#else
    fs::path::imbue(std::locale(loc, new std::codecvt_utf8_utf16<wchar_t>()));
#endif
}

英文部分我已经做了简单翻译,并且加了注释,总之该函数设置了堆内存区、loc本地化参数等信息。

noui_connect

接下来我们来看noui_connect()这个函数,其代码如下:

void noui_connect()
{
    noui_ThreadSafeMessageBoxConn = uiInterface.ThreadSafeMessageBox_connect(noui_ThreadSafeMessageBox);
    noui_ThreadSafeQuestionConn = uiInterface.ThreadSafeQuestion_connect(noui_ThreadSafeQuestion);
    noui_InitMessageConn = uiInterface.InitMessage_connect(noui_InitMessage);
}

这段代码一点注释都没有,很多人看到这里的时候估计会跟我一样一脸懵逼,实际上在仔细跟下代码后会发现,这其实就是注册三个消息的回调函数。

其中noui_ThreadSafeMessageBox为消息弹出框提示消息,noui_ThreadSafeQuestion为用户询问的交互消息,noui_InitMessage为程序初始化过程中的消息。

在这里,uiInterface是在srcui_interface.h文件的最后声明的,该变量为全局变量:

extern CClientUIInterface uiInterface;

uiInterface是在srcui_interface.cpp文件里面定义的:

CClientUIInterface uiInterface;

中间的ThreadSafeMessageBox、ThreadSafeQuestion、InitMessage是定义在ui_interface.h文件中的三个信号量boost::signals2::signal

signals2基于Boost的另一个信号库signals,实现了线程安全的观察者模式。在signals2库中,其被称为信号/插槽机制(signals and slots,另有说法为时间处理机制,event/event handler),一个信号关联了多个插槽,当信号发出时,所有关联它的插槽都会被调用。

三个信号量的定义内容如下:

Path: bitcoinsrcui_interface.h

#define ADD_SIGNALS_DECL_WRAPPER(signal_name, rtype, ...)                                  
    rtype signal_name(__VA_ARGS__);                                                        
    using signal_name##Sig = rtype(__VA_ARGS__);                                           
    boost::signals2::connection signal_name##_connect(std::function<signal_name##Sig> fn);

    /** Show message box. */
    ADD_SIGNALS_DECL_WRAPPER(ThreadSafeMessageBox, bool, const std::string& message, const std::string& caption, unsigned int style);

    /** If possible, ask the user a question. If not, falls back to ThreadSafeMessageBox(noninteractive_message, caption, style) and returns false. */
    ADD_SIGNALS_DECL_WRAPPER(ThreadSafeQuestion, bool, const std::string& message, const std::string& noninteractive_message, const std::string& caption, unsigned int style);

    /** Progress message during initialization. */
    ADD_SIGNALS_DECL_WRAPPER(InitMessage, void, const std::string& message);

以第一个为例,宏

ADD_SIGNALS_DECL_WRAPPER

里面的bool表示返回值是布尔类型,形参分别为std::string&类型的message和std::string&类型的caption以及无符号int类型的style。回到src oui.cpp中,对比下槽函数的定义是一样的:

bool noui_ThreadSafeMessageBox(const std::string& message, const std::string& caption, unsigned int style)

tips: 当进行信号和槽函数定义,即使用SIGNAL()和SLOT()时只能用变量类型,不能出现变量名。

connect函数

connect()函数是signal的插槽管理操作函数,它把插槽连接到信号上,相当于为信号(事件)增加了一个处理器。

连接成功则返回一个connection对象,可对连接进行断开、重连、测试连接状态等操作。

参考:http://blog.csdn.net/zengraoli/article/details/9697841

说下信号和槽的概念,信号:一般是用来传参或者是一种逻辑的调用者。槽:是用来接收并处理信号的函数,完成信号想要实现的功能。

举个例子,比如你在网页上点击提交按钮的时候就触发一个信号,该操作信号就会被传递到所有绑定该信号的控件上,接着由对应的控件函数进行响应,完成提交的功能。

AppInit

最后我们来看Appinit函数,以下是函数的代码。

//////////////////////////////////////////////////////////////////////////////
//
// Start
//
static bool AppInit(int argc, char* argv[])
{
    InitInterfaces interfaces;
    interfaces.chain = interfaces::MakeChain();

    bool fRet = false;

    util::ThreadRename("init");

    //
    // Parameters
    //
    // If Qt is used, parameters/bitcoin.conf are parsed in qt/bitcoin.cpp's main()
    SetupServerArgs();
    std::string error;
    if (!gArgs.ParseParameters(argc, argv, error)) {
        return InitError(strprintf("Error parsing command line arguments: %s
", error));
    }

    // Process help and version before taking care about datadir
    if (HelpRequested(gArgs) || gArgs.IsArgSet("-version")) {
        std::string strUsage = PACKAGE_NAME " Daemon version " + FormatFullVersion() + "
";

        if (gArgs.IsArgSet("-version"))
        {
            strUsage += FormatParagraph(LicenseInfo()) + "
";
        }
        else
        {
            strUsage += "
Usage:  bitcoind [options]                     Start " PACKAGE_NAME " Daemon
";
            strUsage += "
" + gArgs.GetHelpMessage();
        }

        tfm::format(std::cout, "%s", strUsage.c_str());
        return true;
    }

    try
    {
        if (!CheckDataDirOption()) {
            return InitError(strprintf("Specified data directory "%s" does not exist.
", gArgs.GetArg("-datadir", "")));
        }
        if (!gArgs.ReadConfigFiles(error, true)) {
            return InitError(strprintf("Error reading configuration file: %s
", error));
        }
        // Check for -testnet or -regtest parameter (Params() calls are only valid after this clause)
        try {
            SelectParams(gArgs.GetChainName());
        } catch (const std::exception& e) {
            return InitError(strprintf("%s
", e.what()));
        }

        // Error out when loose non-argument tokens are encountered on command line
        for (int i = 1; i < argc; i++) {
            if (!IsSwitchChar(argv[i][0])) {
                return InitError(strprintf("Command line contains unexpected token '%s', see bitcoind -h for a list of options.
", argv[i]));
            }
        }

        // -server defaults to true for bitcoind but not for the GUI so do this here
        gArgs.SoftSetBoolArg("-server", true);
        // Set this early so that parameter interactions go to console
        InitLogging();
        InitParameterInteraction();
        if (!AppInitBasicSetup())
        {
            // InitError will have been called with detailed error, which ends up on console
            return false;
        }
        if (!AppInitParameterInteraction())
        {
            // InitError will have been called with detailed error, which ends up on console
            return false;
        }
        if (!AppInitSanityChecks())
        {
            // InitError will have been called with detailed error, which ends up on console
            return false;
        }
        if (gArgs.GetBoolArg("-daemon", false))
        {
#if HAVE_DECL_DAEMON
#if defined(MAC_OSX)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
            tfm::format(std::cout, PACKAGE_NAME " daemon starting
");

            // Daemonize
            if (daemon(1, 0)) { // don't chdir (1), do close FDs (0)
                return InitError(strprintf("daemon() failed: %s
", strerror(errno)));
            }
#if defined(MAC_OSX)
#pragma GCC diagnostic pop
#endif
#else
            return InitError("-daemon is not supported on this operating system
");
#endif // HAVE_DECL_DAEMON
        }
        // Lock data directory after daemonization
        if (!AppInitLockDataDirectory())
        {
            // If locking the data directory failed, exit immediately
            return false;
        }
        fRet = AppInitMain(interfaces);
    }
    catch (const std::exception& e) {
        PrintExceptionContinue(&e, "AppInit()");
    } catch (...) {
        PrintExceptionContinue(nullptr, "AppInit()");
    }

    if (!fRet)
    {
        Interrupt();
    } else {
        WaitForShutdown();
    }
    Shutdown(interfaces);

    return fRet;
}

我们先来看开头几句,0.18版本的代码和早期版本的代码不同,一上来主要做了以下几件事情(看注释)

    //创建一个区块实例,这个区块实例在后面会继续初始化
    InitInterfaces interfaces;
    interfaces.chain = interfaces::MakeChain();

    bool fRet = false;
    //重命名线程
    util::ThreadRename("init");

    //
    // Parameters
    //
    // 如果使用了QT, parameters/bitcoin.conf已经在 qt/bitcoin.cpp's main()中进行了序列化,否则将在这里进行第一次初始化
    SetupServerArgs();

然后是跟以往版本代码一样的参数解析ParseParmeters

AppInit函数中执行的第一个函数为ParseParameters,通过其字面意思我们可以看出其主要功能为解析外部传入的参数。其实现代码位于src/util.cpp中。

(1)互斥锁

该函数的第一行代码为LOCK(cs_args);LOCK和cs_args都没有在该函数中定义,因此我们就需要查找其定义所在,以便我们理解其真正含义。通过查找cs_args定义位于src/util.cpp的上方,其定义为:

CCriticalSection cs_args;

CCriticalSection又为何物,有过线程编程经验的应该知道Critical Section为线程中的访问临界资源,多个线程必须互斥地对它进行访问,即保证在该代码后面的全局变量在程序运行过程中不会被其他线程对其后的变量进行篡改。那么CCriticalSection类在哪定义呢?通过查找发现其在src/sync.h中实现了定义,从定义可以看出该类继承自boost::recursive_mutex互斥类。

class CCriticalSection : publicAnnotatedMixin

{

public:

~CCriticalSection() {

DeleteLock((void*)this);

}

};

了解了cs_args为互斥类对象,我们再来看LOCK函数,其又为何物呢?我们再次来到src/sync.h,可以找到其定义如下。

#define LOCK(cs) CCriticalBlock PASTE2(criticalblock,__COUNTER__)(cs, #cs, __FILE__, __LINE__)

通过其定义,我们可以看出LOCK并不是一个单独的函数,而是一个宏定义,与前面的CCriticalSection对象结合实现对包含代码在各线程中进行互斥加锁处理,防止后续代码中涉及的全局变量被不同线程抢夺。

(2)参数解析

bool ArgsManager::ParseParameters(int argc, const char* const argv[], std::string& error)
{
    LOCK(cs_args);
    m_override_args.clear();

    for (int i = 1; i < argc; i++) {
        std::string key(argv[i]);
        if (key == "-") break; //bitcoin-tx using stdin
        std::string val;
        size_t is_index = key.find('=');
        if (is_index != std::string::npos) {
            val = key.substr(is_index + 1);
            key.erase(is_index);
        }
#ifdef WIN32
        key = ToLower(key);
        if (key[0] == '/')
            key[0] = '-';
#endif

        if (key[0] != '-')
            break;

        // Transform --foo to -foo
        if (key.length() > 1 && key[1] == '-')
            key.erase(0, 1);

        const unsigned int flags = FlagsOfKnownArg(key);
        if (flags) {
            if (!InterpretOption(key, val, flags, m_override_args, error)) {
                return false;
            }
        } else {
            error = strprintf("Invalid parameter %s", key.c_str());
            return false;
        }
    }

    // we do not allow -includeconf from command line, so we clear it here
    auto it = m_override_args.find("-includeconf");
    if (it != m_override_args.end()) {
        if (it->second.size() > 0) {
            for (const auto& ic : it->second) {
                error += "-includeconf cannot be used from commandline; -includeconf=" + ic + "
";
            }
            return false;
        }
    }
    return true;
}

在这段代码中,比特币最新源码抛弃了过去使用两个map进行参数存储的做法,而是改为了使用m_override_args这一个map对参数进行KV式存储,这个map的定义我们可以在system.h中找到

std::map<std::string, std::vector<std::string>> m_override_args GUARDED_BY(cs_args);

在使用这个变量时,程序对其使用clear方法进行了清空操作。

 m_override_args.clear();

随后通过for循环实现对所有参数进行逐个解析,获取参数及其值。

参数处理

获取了参数之后,接下来我们来进行下一步,参数处理

    // Process help and version before taking care about datadir
    if (HelpRequested(gArgs) || gArgs.IsArgSet("-version")) {
        std::string strUsage = PACKAGE_NAME " Daemon version " + FormatFullVersion() + "
";

        if (gArgs.IsArgSet("-version"))
        {
            strUsage += FormatParagraph(LicenseInfo()) + "
";
        }
        else
        {
            strUsage += "
Usage:  bitcoind [options]                     Start " PACKAGE_NAME " Daemon
";
            strUsage += "
" + gArgs.GetHelpMessage();
        }

        tfm::format(std::cout, "%s", strUsage.c_str());
        return true;
    }

这段代码的注释的含义为:在处理数据目录操作前,先完成版本与帮助命令的处理。所以,通过这段代码,比特币后台进程将可根据用户输入相应参数给出对应的程序版本与帮助信息。

比如,当我们输入version参数的时候

 而当我们输入--help或者-h参数的时候

 看完了这两个特殊参数处理,我们接下来继续向下看

    try
    {
        if (!CheckDataDirOption()) {
            return InitError(strprintf("Specified data directory "%s" does not exist.
", gArgs.GetArg("-datadir", "")));
        }
        if (!gArgs.ReadConfigFiles(error, true)) {
            return InitError(strprintf("Error reading configuration file: %s
", error));
        }
        // Check for -testnet or -regtest parameter (Params() calls are only valid after this clause)
        try {
            SelectParams(gArgs.GetChainName());
        } catch (const std::exception& e) {
            return InitError(strprintf("%s
", e.what()));
        }

        // Error out when loose non-argument tokens are encountered on command line
        for (int i = 1; i < argc; i++) {
            if (!IsSwitchChar(argv[i][0])) {
                return InitError(strprintf("Command line contains unexpected token '%s', see bitcoind -h for a list of options.
", argv[i]));
            }
        }

        // -server defaults to true for bitcoind but not for the GUI so do this here
        gArgs.SoftSetBoolArg("-server", true);

这段代码主要在处理配置文件的相关参数。

其中CheckDataDirOption是根据参数检测配置文件是否存在,如果存在,gArgs.ReadConfigFiles则会去具体读取配置文件里的参数,SelectParams(gArgs.GetChainName())会根据网络参数(main,regtest,test)进行对应的参数检测以及初始化。

就拿我们使用的testnet来说,其初始化部分如下:

/**
 * Testnet (v3)
 */
class CTestNetParams : public CChainParams {
public:
    CTestNetParams() {
        strNetworkID = "test";
        consensus.nSubsidyHalvingInterval = 210000;
        consensus.BIP16Exception = uint256S("0x00000000dd30457c001f4095d208cc1296b0eed002427aa599874af7a432b105");
        consensus.BIP34Height = 21111;
        consensus.BIP34Hash = uint256S("0x0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8");
        consensus.BIP65Height = 581885; // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6
        consensus.BIP66Height = 330776; // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182
        consensus.CSVHeight = 770112; // 00000000025e930139bac5c6c31a403776da130831ab85be56578f3fa75369bb
        consensus.SegwitHeight = 834624; // 00000000002b980fcd729daaa248fd9316a5200e9b367f4ff2c42453e84201ca
        consensus.powLimit = uint256S("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
        consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks
        consensus.nPowTargetSpacing = 10 * 60;
        consensus.fPowAllowMinDifficultyBlocks = true;
        consensus.fPowNoRetargeting = false;
        consensus.nRuleChangeActivationThreshold = 1512; // 75% for testchains
        consensus.nMinerConfirmationWindow = 2016; // nPowTargetTimespan / nPowTargetSpacing
        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28;
        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = 1199145601; // January 1, 2008
        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = 1230767999; // December 31, 2008

        // The best chain should have at least this much work.
        consensus.nMinimumChainWork = uint256S("0x00000000000000000000000000000000000000000000007dbe94253893cbd463");

        // By default assume that the signatures in ancestors of this block are valid.
        consensus.defaultAssumeValid = uint256S("0x0000000000000037a8cd3e06cd5edbfe9dd1dbcc5dacab279376ef7cfc2b4c75"); //1354312

        pchMessageStart[0] = 0x0b;
        pchMessageStart[1] = 0x11;
        pchMessageStart[2] = 0x09;
        pchMessageStart[3] = 0x07;
        nDefaultPort = 18333;
        nPruneAfterHeight = 1000;
        m_assumed_blockchain_size = 30;
        m_assumed_chain_state_size = 2;

        genesis = CreateGenesisBlock(1296688602, 414098458, 0x1d00ffff, 1, 50 * COIN);
        consensus.hashGenesisBlock = genesis.GetHash();
        assert(consensus.hashGenesisBlock == uint256S("0x000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"));
        assert(genesis.hashMerkleRoot == uint256S("0x4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"));

        vFixedSeeds.clear();
        vSeeds.clear();
        // nodes with support for servicebits filtering should be at the top
        vSeeds.emplace_back("testnet-seed.bitcoin.jonasschnelli.ch");
        vSeeds.emplace_back("seed.tbtc.petertodd.org");
        vSeeds.emplace_back("seed.testnet.bitcoin.sprovoost.nl");
        vSeeds.emplace_back("testnet-seed.bluematt.me"); // Just a static list of stable node(s), only supports x9

        base58Prefixes[PUBKEY_ADDRESS] = std::vector<unsigned char>(1,111);
        base58Prefixes[SCRIPT_ADDRESS] = std::vector<unsigned char>(1,196);
        base58Prefixes[SECRET_KEY] =     std::vector<unsigned char>(1,239);
        base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x35, 0x87, 0xCF};
        base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94};

        bech32_hrp = "tb";

        vFixedSeeds = std::vector<SeedSpec6>(pnSeed6_test, pnSeed6_test + ARRAYLEN(pnSeed6_test));

        fDefaultConsistencyChecks = false;
        fRequireStandard = false;
        m_is_test_chain = true;


        checkpointData = {
            {
                {546, uint256S("000000002a936ca763904c3c35fce2f3556c559c0214345d31b1bcebf76acb70")},
            }
        };

        chainTxData = ChainTxData{
            // Data from rpc: getchaintxstats 4096 0000000000000037a8cd3e06cd5edbfe9dd1dbcc5dacab279376ef7cfc2b4c75
            /* nTime    */ 1531929919,
            /* nTxCount */ 19438708,
            /* dTxRate  */ 0.626
        };
    }
};

在这个类里面,设置了大量参数的初始值。

而至于

// -server defaults to true for bitcoind but not for the GUI so do this here
        gArgs.SoftSetBoolArg("-server", true);

英文注释已经很明确说明了,bitcoind的时候,server参数默认为true

原文地址:https://www.cnblogs.com/lsm19870508/p/11498866.html