写在最前:本文大量截图内容节借鉴选自简书菜菜子_forest的比特币源码研读。(https://www.jianshu.com/u/30081a05cf95)
在上篇随笔的最后,我们代码已经阅读到了比特币各种网的设置和对应的网络参数,拿我们使用的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 }; } };
这些参数里比较重要的参数如下:
区块奖励减半间隔:consensus.nSubsidyHalvingInterval = 210000; 算力极限值:consensus.powLimit =uint256S("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); 算力修改间隔:consensus.nPowTargetSpacing = 10 * 60;即10分钟 创世块genesis = CreateGenesisBlock(1231006505, 2083236893,0x1d00ffff, 1, 50 * COIN);第一个块的奖励为50个比特币
以上这些参数都是我们经常听到的名词,如果我们想创建自己命名的数字货币,只需简单修改这些参数即可,比如把区块奖励减半间隔修改为420000或其他数,创世块中比特币奖励的50该为100或其他数。所以要创建一个自己的数字货币并不难,关键在于看其是否有应用价值。
程序在每个类的定义之后,程序也定义了对应的静态链参数对象。
static CMainParamsmainParams; staticCTestNetParams testNetParams; staticCRegTestParams regTestParams;
Params将根据用户设置的链参数名称,将对应的链参数返回给pCurrentParams,从而完成链基本参数与主要参数的实现任务。
至此,程序根据用户输入的网络类型参数完成了比特币运行网络的设置。在这段代码中,我知道了私有网络,以前听得最多的是主网和测试网,而私有网或私有链基本很少听到,在这段代码中我知道了私有链是开发团队在开发时使用的网络,因为其挖矿难度很低,很容易进行程序的调试与功能试验。进而让我明白了一些区块链项目为什么会说在XX时刻要进入测试网阶段,然后再是最终的主网运行阶段。因此区块链开发过程应该是这样的:
接下来,我们回归之前的代码研究。
初始化日志
接下来我们将进入日志打印内容基础框架设置部分,在该部分主要对日志打印的内容根据参数进行解析,确定后续运行过程中需打印的信息,其具体实现代码也很简单,具体如下:
/** * Initialize global loggers. * * Note that this is called very early in the process lifetime, so you should be * careful about what global state you rely on here. */ void InitLogging() { LogInstance().m_print_to_file = !gArgs.IsArgNegated("-debuglogfile"); LogInstance().m_file_path = AbsPathForConfigVal(gArgs.GetArg("-debuglogfile", DEFAULT_DEBUGLOGFILE)); LogInstance().m_print_to_console = gArgs.GetBoolArg("-printtoconsole", !gArgs.GetBoolArg("-daemon", false)); LogInstance().m_log_timestamps = gArgs.GetBoolArg("-logtimestamps", DEFAULT_LOGTIMESTAMPS); LogInstance().m_log_time_micros = gArgs.GetBoolArg("-logtimemicros", DEFAULT_LOGTIMEMICROS); LogInstance().m_log_threadnames = gArgs.GetBoolArg("-logthreadnames", DEFAULT_LOGTHREADNAMES); fLogIPs = gArgs.GetBoolArg("-logips", DEFAULT_LOGIPS); std::string version_string = FormatFullVersion(); #ifdef DEBUG version_string += " (debug build)"; #else version_string += " (release build)"; #endif LogPrintf(PACKAGE_NAME " version %s\n", version_string); }
日志文件的默认位置为我们设置的数据目录中,其名称为debug.log。
我们来分析其参数含义:
(1)printtoconsole
日志信息是否发送跟踪/调试信息到控制台而不是debug.log文件。我们看到其默认为false,即不打印至控制台或终端上,反之则打印。
(2)logtimestamps
该参数的含义为在日志中打印时间戳,该参数的默认值定义为静态常量DEFAULT_LOGTIMESTAMPS,该常量在src/util.h中定义,与该常量一起定义的还有本函数中使用到的另两个常量,这3个常量分别给作为函数中3个参数的默认值,其分别是:
static const bool DEFAULT_LOGTIMEMICROS = false;//按微秒格式打印日志时间 static const bool DEFAULT_LOGIPS= false; //日志打印中包含IP地址 static const bool DEFAULT_LOGTIMESTAMPS = true;//打印日志时间戳 通过名字我们可以很明显看出它们的对应关系。分别为: Logtimestamps -------> DEFAULT_LOGTIMESTAMPS fLogTimeMicros -------> DEFAULT_LOGTIMEMICROS DEFAULT_LOGIPS-------> DEFAULT_LOGIPS
从DEFAULT_LOGTIMESTAMPS的定义可以看出,日志打印中是默认打印时间戳的。
(3)logtimemicros
从DEFAULT_LOGTIMEMICROS的定义可以看出,日志打印中日志时间是按微秒格式打印的。
(4)DEFAULT_LOGIPS
从DEFAULT_LOGIPS的定义可以看出,日志打印中是默认不打印IP地址的。
在ubuntu系统中打开~/.bitcoin下的日志文件debug.log,我们可以看到其具体的日志打印输出内容如图所示。
InitParameterInteraction
InitParameterInteraction函数,通过其名称我们可以直观的理解该函数的功能是对传入参数的交互处理,也就是根据参数的信息做出相应的操作或执行相应的任务,这对于我们理解后续比特币程序运行的理解将会很关键,因为这些参数决定了其具体运行方式,所以,让我们一起认真地完成该函数的研读。
InitParameterInteraction函数的运行主要分为以下7部分:
(1)绑定并监听地址
我们首先来看该部分的实现源码:
// when specifying an explicitbinding address, you want to listen on it // even when -connect or -proxy isspecified if (IsArgSet("-bind")) { if (SoftSetBoolArg("-listen", true)) LogPrintf("%s:parameter interaction: -bind set -> setting -listen=1\n", __func__); } if (IsArgSet("-whitebind")) { if (SoftSetBoolArg("-listen", true)) LogPrintf("%s: parameter interaction: -whitebind set -> setting-listen=1\n", __func__); }
其注释的含义为:当显示指定了绑定地址后,即使指定了-connect和-proxy参数信息,程序将会接受来自外部的连接,并监听该地址。
绑定地址的方式有两种参数,分别是“-bind”和“-whitebind”,程序对这两种参数的处理方式是一致的,均通过SoftSetBoolArg函数实现“-listen”参数的设置,并将其值设置为true,表示要监听设置的外部连接IP地址。
这里要特别说明的是LogPrintf函数,因为该该函数在后续的代码中将会频繁出现,所以有必要对其进行说明。它在src/util.h中以预编译方式实现的定义,其本身不实现日志打印功能,而是通过调用LogPrintStr函数实现,该函数在src/util.cpp中进行了实现,其实现流程如图所示:
(2)连接可信节点
对连接可信节点参数的处理比较简单,其代码实现如下:
if (mapMultiArgs.count("-connect")&& mapMultiArgs.at("-connect").size() > 0) { // whenonly connecting to trusted nodes, do not seed via DNS, or listen by default if(SoftSetBoolArg("-dnsseed", false)) LogPrintf("%s: parameter interaction: -connect set -> setting-dnsseed=0\n", __func__); if(SoftSetBoolArg("-listen", false)) LogPrintf("%s: parameter interaction: -connect set -> setting-listen=0\n", __func__); }
其通过mapMultiArgs判断是否包含“-connect”参数,如果包括则将“-dnsseed(使用DNS查找节点)”和“-listen(即接受来自外部的连接,并对其进行监听)”设置为true。并进行日志打印,日志打印函数仍为LogPrintf。
这里需要注意的是,前面(1)中提到如果设置了”-bind”和”-whitebind”参数,程序将会监听指定的IP地址,即使指定了-connect和-proxy参数信息,程序将会接受来自外部的连接,并监听该地址。所以,此处代码的有效执行是在为设置”-bind”和”-whitebind”参数的情况下进行的。
(3)代理模式
设置代理参数的目的是为了保护隐私,所以此处将”-listen”、”-upnp”以及”-discover”均设置为false,也就是说比特币后台进程只使用代理提供的监听地址与端口,并且不去查找默认的监听地址。这里的upnp代表的意思是使用全局即插即用(UPNP)映射监听端口,默认不使用。
但与(2)中的说明一样,如果(1)中设置了”-bind”和”-whitebind”参数,程序将会监听指定的IP地址,即使指定了-connect和-proxy参数信息,程序将会接受来自外部的连接,并监听该地址。所以,此处代码的有效执行是在为设置”-bind”和”-whitebind”参数的情况下进行的。
(4)监听设置处理
监听设置处理代码在if (!GetBoolArg("-listen", DEFAULT_LISTEN)){}块中实现。
如果监听参数设置为false,即不实施监听则upnp(端口)、discover(自动发现默认地址)以及listenonion(匿名地址监听)均设置为false。
if (!GetBoolArg("-listen", DEFAULT_LISTEN))语句中的DEFAULT_LISTEN在src/net.h中定义。其定义默认为true,具体定义如下:
/** -listen default */ static const bool DEFAULT_LISTEN = true;
此处需要说明的是listenonion(匿名地址监听),此处设计一个通信机制的一个概念:第二代洋葱路由(onion routing),其解释如下:
Tor(The Onion Router)是第二代洋葱路由(onion routing)的一种实现,用户通过Tor可以在因特网上进行匿名交流。Tor专门防范流量过滤、嗅探分析,让用户免受其害。
最初该项目由美国海军研究实验室赞助。2004年后期,Tor成为电子前哨基金会的一个项目。2005年后期,EFF不再赞助Tor项目,但他们继续维持Tor的官方网站。
比特币程序中使用src/torcontrol.h、src/torcontrol.cpp实现了Tor的控制,这个类的实现我们将在后续文章说明。
(5)外部IP参数处理
外部IP参数处理代码在if (IsArgSet("-externalip")) {}块中实现。
如果显示指定了公共IP地址,那么bitcoind就不需要查找其他监听地址了。
(6)区块模式参数设置
在区块模式参数设置代码在if (GetBoolArg("-blocksonly",DEFAULT_BLOCKSONLY)) { }块中实现。
DEFAULT_BLOCKSONLY在Src/net.h中定义,默认值为false,具体定义如下:
/** Default for blocks only*/ static const bool DEFAULT_BLOCKSONLY = false;
如果-blocksonly设置为false,那么在参数中将GetBoolArg设置为true,那么whitelistrelay参数将设置为false,意味着在区块模式下白名单列表将失效。
(7)强制白名单节点连接参数处理
强制白名单节点连接参数处理意味着比特币网络中的信息将优先在白名单节点间传递。
AppInitBasicSetup
一、警告消息处理
警告消息处理函数实现代码很简单,只有5行代码
// ********************************************************* Step 1: setup #ifdef _MSC_VER // Turn off Microsoft heap dump noise _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); _CrtSetReportFile(_CRT_WARN, CreateFileA("NUL", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, 0)); // Disable confusing "helpful" text message on abort, Ctrl-C _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); #endif
通过#ifdef _MSC_VER,我们可以知道上面这段代码是针对微软的VS开发环境而设置的,在其他编译环境下是这段代码是不会执行的。上述这段代码中调用了2个开发环境相关的函数_CrtSetReportMode和_CrtSetReportFile。
(1)_CrtSetReportMode函数定义如下:
int _CrtSetReportMode( intreportType, intreportMode );
其包含的两个参数分别为报告类型和报告输出模式:
报告类型包含:
l_CRT_WARN:警告、消息和不需要立即关注的信息。
l_CRT_ERROR:错误、不可恢复的问题和需要立即关注的问题。
l_CRT_ASSERT:断言失败(断言表达式的计算结果为FALSE)。。
报告模式包含:
l_CRTDBG_MODE_DEBUG:将消息写入调试器的输出窗口。 l_CRTDBG_MODE_FILE:将消息写入用户提供的文件句柄。_CrtSetReportFile应调用以定义要用作目标流的特定文件。 l_CRTDBG_MODE_WNDW:创建一个消息框,以显示该消息以及Abort,Retry,和Ignore按钮。 l_CRTDBG_REPORT_MODE:返回reportMode指定reportType:1 _CRTDBG_MODE_FILE、2_CRTDBG_MODE_DEBUG、4_CRTDBG_MODE_WNDW
由上述分析可以知道当前代码中设置的报告类型为警告,报告输出方式为文件输出。
(2)_CrtSetReportFile函数定义如下:
_HFILE _CrtSetReportFile( int reportType, _HFILE reportFile );
参数reportType与_CrtSetReportMode中的参数一致,都是_CRT_WARN, _CRT_ERROR, and _CRT_ASSERT3种类型,这里就不需要赘述了。
参数reportFile为文件句柄,其对应的文件作为reportType相应消息的输出对象。
在当前代码中使用CreateFileA("NUL", GENERIC_WRITE, 0,NULL, OPEN_EXISTING, 0, 0)实现了输出文件的创建。但是其第一个参数值(文件名)为“NULL”,则说明这个文件为空,警告消息虽然说是输出至文件中,但当前为空文件,那么警告消息的输出可理解为被关闭了。这点可从代码注释找到依据:// Turn off Microsoft heap dump noise,即关闭微软内存堆快照的“噪音”,这里的噪音应该就是指此处的警告消息了。
二、abort函数调用处理
很多软件通过设置自己的异常捕获函数,捕获未处理的异常,生成报告或者日志(例如生成mini-dump文件),达到Release版本下追踪Bug的目的。但是,到了VS2005(即VC8),Microsoft对CRT(C运行时库)的一些与安全相关的代码做了些改动,典型的,例如增加了对缓冲溢出的检查。新CRT版本在出现错误时强制把异常抛给默认的调试器(如果没有配置的话,默认是Dr.Watson),而不再通知应用程序设置的异常捕获函数,这种行为主要在以下3种情况出现。
(1)调用abort函数,并且设置了_CALL_REPORTFAULT选项(这个选项在Release版本是默认设置的); (2)启用了运行时安全检查选项,并且在软件运行时检查出安全性错误,例如出现缓存溢出。(安全检查选项/GS默认也是打开的); (3)遇到_invalid_parameter错误,而应用程序又没有主动调用_set_invalid_parameter_handler设置错误捕获函数。 所以结论是,使用VS2005(VC8,代码中的宏定义为_MSC_VER >= 1400)编译的程序,许多错误都不能在SetUnhandledExceptionFilter捕获到。这是CRT相对于前面版本的一个比较大的改变,但是很遗憾,Microsoft却没有在相应的文档明确指出。
我们可以通过_set_abort_behavior(0, _WRITE_ABORT_MSG |_CALL_REPORTFAULT)解决(1),也就是abort函数异常错误捕获问题。
详细解释参考:http://blog.csdn.net/yuzhiyuxia/article/details/16889155
三、数据执行保护(DEP)功能处理
数据执行保护(DEP)的目的是为了防止病毒或其他安全威胁造成损害,Windows XP SP2、WinXP SP3, WinVista >= SP1, Win Server
2008使用了数据执行保护(DEP)功能,而GCCs winbase.h将该功能限制在_WIN32_WINNT >= 0x0601 (Windows 7)才能使用,所以在代码中强制定义了宏定义
#ifndef PROCESS_DEP_ENABLE
#define PROCESS_DEP_ENABLE 0x00000001
#endif
并且通过函数指针获取Kernel32.dll中的SetProcessDEPPolicy函数对象,实现DEP功能的开启。
typedef BOOL (WINAPI*PSETPROCDEPPOL)(DWORD);
PSETPROCDEPPOL setProcDEPPol =(PSETPROCDEPPOL)GetProcAddress(GetModuleHandleA("Kernel32.dll"),"SetProcessDEPPolicy");
if (setProcDEPPol != NULL)
setProcDEPPol(PROCESS_DEP_ENABLE);
四、初始化网络连接
SetupNetworking函数在src/util.cpp中实现,其主要实现Winsock服务的初始化,初始化的工作通过WSAStartup函数完成。这一步的执行是必须在初始化中完成的,具体原因如下:
为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。
该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的Socket的版本信息。
当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。
总得来说,Socket连接涉及的API调用,都必须在WSAStartup函数执行之后才有效,否则将无法执行网络连接操作。
五、信号处理设置
通过宏定义判断#ifndef WIN32,我们可以知道信号处理设置是针对非Windows系统的。信号处理设置代码可分为4部分来分析:
(1)文件创建权限
if (!GetBoolArg("-sysperms",false)) {
umask(077);
}
在该代码中,程序首先判断是否设置了sysperms参数,该参数的的含义为:
Create new files with system default permissions,instead of umask 077 (only effective with disabled wallet functionality) 在创建新文件时,文件权限为系统默认权限,以此来代替umask 077命令(因为umask 077只在钱包功能被禁止时才其作用)
如果设置了则返回其状态值,如果为false,则需执行umask(077)命令。
umask用于设置文件与文件夹使用权限,此处077代表---rwxrwx,表示owner没有任何权限,group和other有完全的操作权限。
(2)进程终止信号处理
进程终止信号处理代码如下所示:
// Clean shutdown on SIGTERM struct sigaction sa; //信号处理对象 sa.sa_handler = HandleSIGTERM; //进程终止信号处理句柄 sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGTERM, &sa, NULL); //终止信号处理 sigaction(SIGINT, &sa, NULL); //中断信号处理
从代码可以看到,程序首先定义了信号处理对象sa,然后对其句柄、标志和掩码赋值,最后将该信号对象传递给终止与中断信号处理函数。
这里要说明的是句柄函数HandleSIGTERM,该函数在src/init.cpp中实现,其实现代码为:
void HandleSIGTERM(int) { fRequestShutdown = true; }
很简单的一个函数,将全局变量fRequestShutdown设置为true,所有正在运行的线程将根据一定的规则停止运行。
(3)挂起信号处理
进程挂起信号处理代码如下所示:
// Reopen debug.log on SIGHUP struct sigaction sa_hup; sa_hup.sa_handler = HandleSIGHUP; //挂起信号处理句柄函数 sigemptyset(&sa_hup.sa_mask); sa_hup.sa_flags = 0; sigaction(SIGHUP, &sa_hup, NULL); //挂起信号处理
从代码可以看出挂起信号处理过程与终止信号处理过程是一样的。这里要说明的是句柄函数HandleSIGHUP,该函数在src/init.cpp中实现,其实现代码为:
static void HandleSIGHUP(int) { LogInstance().m_reopen_file = true; }
很简单的一个函数,将全局变量m_reopen_file设置为true,src/util.cpp中的LogPrintStr将重新打开调试日志打印文件。
// reopen the log file, if requested if (m_reopen_file) { m_reopen_file = false; FILE* new_fileout = fsbridge::fopen(m_file_path, "a"); if (new_fileout) { setbuf(new_fileout, nullptr); // unbuffered fclose(m_fileout); m_fileout = new_fileout; } } FileWriteStr(str_prefixed, m_fileout);
(4)管道错误处理
管道错误处理只有一行代码:
signal(SIGPIPE, SIG_IGN);
signal为信号函数,第一个参数表示需要处理的信号值(SIGPIPE,管道错误),第二个参数为处理函数或者是一个表示,此处SIG_IGN表示忽略SIGPIPE那个注册的信号。
此处需设置忽略SIGPIPE管道错误,否则客户端异常关闭时会将守护进程连带着也给关闭,影响守护进程的正常运行。
六、内存分配失败处理
内存分配失败处理函数主要由set_new_handler函数完成,该函数说明如下:
1. set_new_handler函数的作用是设置new_p指向的函数为new操作或new[]操作失败时调用的处理函数。 2.设置的处理函数可以尝试使更多空间变为可分配状态,这样新一次的new操作就可能成功。当且仅当该函数成功获得更多可用空间它才会返回;否则它将抛出bad_alloc异常(或者继承该异常的子类)或者终止程序(例如调用abort或exit)。 3.如果设置的处理函数返回了(例如,该函数成功获得了更多的可用空间),它可能将被反复调用,直到内存分配成功,或者它不再返回,或者被其它函数所替代。 4.在尚未用set_new_handler设置处理函数,或者设置的处理函数为空时,将调用默认的处理函数,该函数在内存分配失败时抛出bad_alloc异常。
从上述说明我们可以看出,set_new_handler需要一个内存分配失败后的处理函数,源码中通过new_handler_terminate()函数完成了该功能,通过new_handler_terminate函数中的注释我们可以知道其为了防止影响区块链被破坏,通过执行terminate命令,直接终止程序的方式解决内存分配失败导致的错误,并且进行日志打印。
AppInitParameterInteraction
在研读AppInitParameterInteraction的过程中,发现代码中包含了Step 2: parameter interactions和Step 3: parameter-to-internal-flags两部分,既然有Setp 2和Setp 3,那肯定有Step 1,在哪呢?按常理应该从这个函数的前面去查找Step 1。发现Step 1就在我们前一篇文章AppInitBasicSetup中包含了。从此可以很清楚地看出AppInit系列的执行顺序,中本聪对代码注释做得还是很不错的,让读者可以清晰地研读源码。既然这样,我们就跟随Step的步伐前行吧!本文主要开展Step 2: parameter interactions,应用程序参数交互源码的解读。
一、区块修剪参数处理
区块修剪参数处理额代码很简单,3行代码搞定,如图所示。
// if using block pruning, then disallow txindex if (gArgs.GetArg("-prune", 0)) { if (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX)) return InitError(_("Prune mode is incompatible with -txindex.").translated); if (!g_enabled_filter_types.empty()) { return InitError(_("Prune mode is incompatible with -blockfilterindex.").translated); } }
其修剪参数为prune,从代码可以出该参数与txindex是有冲突的,这点虽然可以从上述代码看出一些端倪,但是我们并不知道二者为什么会存在不兼容性。我们来看看prune参数与txindex参数都有什么作用!
(1)prune参数
刚开始看到prune参数,还有这段代码上面的注释我还以为是直接把区块修剪掉,但仔细一想,这肯定不对的啊,把区块都删除了那还能叫区块链吗?区块链不完整了,那我们常说的账本还能全吗?当然不行,所以带着这个疑惑,我去网上搜索了一番!功夫不负有心人,经过努力让我找到了一篇好帖子,让我对prune参数有了很好的理解。
来源:https://bitcoin.stackexchange.com/questions/36100/pruning-the-branches-in-merkle-tree/
从该页面的提问部分我知道了修剪是针对默克尔树(Merkle Tree)来说的,所以修剪一词用得很贴切,可以直观地看出prune是对树上的节点进行修剪。其修剪的对象又是谁呢?从回答中我们可以看出其修剪对象有2种:
一种是所有输出都被花费的叶子节点(交易);
另一种是节点包含的所有子节点均已被修剪。
根据以上分析我们可以看出修剪的目的是为了节省存储空间。但回答者在后面补充到比特币核心未实现此修剪功能,比特币核心针对修剪功能只以两种模式进行:
不修剪(默认选项)
不保留区块和默克尔树,只跟踪未花费输出(UTXO)和公钥脚本,但该模式不允许你帮助新节点进行同步操作。
我们从代码GetArg("-prune", 0)可以看出prune的默认值确实为不修剪,除非是在参数中设置为1,否则不进行修剪操作。该帖子是在2015年2月18日发出来的,帖子的最后说到:
Bitcoin core doesn\'t implement this yet. Pruning is likely to be implemented in 0.11(the next version).
0.11版之后的版本开始实现Prune功能了。下面我们再来看看txindex参数。
(2)txindex参数
该参数的注释我们可以从Src/init.cpp的HelpMessage(HelpMessageMode mode)函数中可以找到,当然我们也可以从bitcoind的命令行中获得其帮助信息,该帮助信息其实就是这段源码实现的,所以咱们看源码就可以了。
strUsage += HelpMessageOpt("-txindex",strprintf(_("Maintain a full transaction index, used by thegetrawtransaction rpc call (default: %u)"), DEFAULT_TXINDEX));
从注释我们可以看出txindex参数的作用是维护一个全交易索引,所以其与prune存在不兼容性问题就很明白了。如果两个都设置了,出现一个要剪、一个要全保留的,程序是会乱的,所以此时程序如果发现二者都设置了,将了会报错("Prune mode is incompatible with-txindex."),并退出程序。
二、文件描述符处理
Step 2的第二部分是对比特币核心中使用到的文件描述符进行处理,主要是处理用于连接的文件描述符数量,将其控制在一个合理的范围内。那么何为文件描述符呢?
在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。
在编写文件操作的或者网络通信的软件时,初学者一般可能会遇到“Too many open files”的问题。这主要是因为文件描述符是系统的一个重要资源,虽然说系统内存有多少就可以打开多少的文件描述符,但是在实际实现过程中内核是会做相应的处理的,一般最大打开文件数会是系统内存的10%(以KB来计算)(称之为系统级限制),查看系统级别的最大打开文件数可以使用sysctl -a| grep fs.file-max命令查看。与此同时,内核为了不让某一个进程消耗掉所有的文件资源,其也会对单个进程最大打开文件数做默认值处理(称之为用户级限制),默认值一般是1024,使用ulimit -n命令可以查看。
详细描述见:http://blog.csdn.net/cywosp/article/details/38965239
通过对代码的研读可以发现其主要是确保程序中有足够多的文件描述符,用于程序所需文件,套接字的操作。此处的代码中设置了几个重要的宏定义变量,它们分别是:
DEFAULT_MAX_PEER_CONNECTIONS:定义于src/net.h中,代表了最大可维护的节点连接数,默认为125个
static const unsigned intDEFAULT_MAX_PEER_CONNECTIONS = 125;
FD_SETSIZE:定义于src/compat.h,代表可包含的最大文件描述符的个数,默认为1024
#ifdef FD_SETSIZE
#undef FD_SETSIZE// prevent redefinition compiler warning
#endif
#define FD_SETSIZE1024 // max number of fds in fd_set
MIN_CORE_FILEDESCRIPTORS:定义于src/init.cpp中,代表了最小核心文件描述符个数,window下为0,linux下为150
#ifdef WIN32
// Win32 LevelDBdoesn\'t use filedescriptors, and the ones used for
// accessing blockfiles don\'t count towards the fd_set size limit
// anyway.
#defineMIN_CORE_FILEDESCRIPTORS 0
#else
#defineMIN_CORE_FILEDESCRIPTORS 150
#endif
MAX_ADDNODE_CONNECTIONS:定义于src/net.h中,代表了最大增加节点连接数,默认为8
/** Maximum numberof addnode outgoing nodes */
static const intMAX_ADDNODE_CONNECTIONS = 8;
至此,我们一起完成了AppInitParameterInteraction中的第一部分Step 2,区块修剪与文件描述符处理。
接下来我们继续看Step 3。
在Step 3: 内部标志(flags)参数的最开始部分,比特币核心源码主要是对标志参数的有效性进行判断,并作出警告或错误退出处理。具体处理的参数、处理顺序以及处理方法见流程图。
(1)debug标志参数:主要是标识程序是运行过程中是否输出调试信息,如果-debug=0或者设置了-nodebug参数,则关闭调试信息,并且fDebug=false;(fDebug在src/util.h中声明,在util.cpp中定义);反之如果-debug=1则输出调试信息;
(2)debugnet标志参数:从InitWarning(_("Unsupported argument -debugnet ignored, use -debug=net."));语句我们可以看出比特币程序目前已不支持debugnet参数,需用-debug=net代替;
(3)socks标志参数:从InitError(_("Unsupported argument -socks found. Setting SOCKS version isn\'t possible anymore, only SOCKS5 proxies are supported."));语句我们可以看出比特币程序目前已不支持socks参数,对于socket通讯目前只支持SOCKS5代理协议。SOCKS5代理协议又是什么呢?
百度百科的解释如下:
SOCKS5是一个代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。SOCKS5服务器通过将前端发来的请求转发给真正的目标服务器,模拟了一个前端的行为。在这里,前端和SOCKS5之间也是通过TCP/IP协议进行通讯,前端将原本要发送给真正服务器的请求发送给SOCKS5服务器,然后SOCKS5服务器将请求转发给真正的服务器。
SOCKS像一堵墙被夹在Internal服务器和客户端之间,对于出入企业网络的资讯提供流量和安全的管理。
从以上解释我们可以看出使用SOCKS代理协议的主要目的是提高网络通信的安全性。
4)tor标志参数:tor的英文全称为The Onion Router,即第二代洋葱路由(onion routing),用于匿名通信。关于tor我们在《比特币源码研读之八》中已给出了其具体解释,有兴趣的读者可以前往了解。通过其提示语句InitError(_("Unsupported argument -tor found, use -onion."));,我们可以发现目前已不支持-tor参数,使用-onion参数代替之;
(5)benchmark标志参数:通过其提示语句“InitWarning(_("Unsupported argument -benchmark ignored, use -debug=bench."));”,我们可以知道benchmark已被忽略,使用debug=bench代替之;
(6)whitelistalwaysrelay标志参数:通过其提示语句“InitWarning(_("Unsupported argument -whitelistalwaysrelay ignored, use -whitelistrelay and/or -whitelistforcerelay."));”,我们可以知道whitelistalwaysrelay也已被废弃,转而使用“-whitelistrelay、-whitelistforcerelay”两个参数之一或共同使用来代替之;whitelistrelay参数的意义是节点间的通信优先在白名单节点之接力实现;
(7)blockminsize标志参数:通过其提示语句“InitWarning("Unsupported argument -blockminsize ignored.");”,我们可以知道blockminsize参数也已被废弃,讲不能通过blockminsize参数设置区块中包含信息量的最小值。
二、交易池与区块索引检测参数
通过注释“// Checkmempool and checkblockindex default to true in regtest mode(检测交易池和区块索引,这两个参数在私有网模式(也可理解为开发模式)下默认为true,即默认执行检测)”以及后面的代码,我们可以发现checkmempool与checkblockindex在开发模式下是肯定会执行的,其目的是为了比特币在正式运行前确保交易状态与区块索引的正确性。作为开发人员,我们可以很容易理解到,检测程序来说是存在资源消耗的,从而会影响程序的运行效率,那么在测试网模式与主网模式下,这两个参数是默认为false的。
(1)交易池参数检测
我们首先来看交易池参数检测处理代码如下:
int ratio = std::min(std::max(GetArg("-checkmempool",chainparams.DefaultConsistencyChecks() ? 1 : 0), 0),1000000);
if (ratio != 0) {
mempool.setSanityCheck(1.0 / ratio);
}
上述源码中,我们首先来看mempool对象,定义于src/validation.h中,其类型为CTxMemPool类,该类在src/txmempool.h中定义,其主要用途是存储主链中发生的有效交易,这些交易也将会被打包至后续的区块中。但如下条件下的交易是不会加入到交易池中的:
1)交易所给出的交易费未达到最小交易费;
2)存在“双花”的交易,即矿池中已包含该交易;
3)非标准交易。
正因为有了上述3个条件,所以CTxMemPool会对交易进行完整性检测,检测的频率通过setSanityCheck函数来设置,该函数的实现内容如下:
void setSanityCheck(double dFrequency = 1.0) { nCheckFrequency = dFrequency * 4294967295.0; }
其中,4294967295=2^32次方-1,此处的nCheckFrequency代表交易池中所有交易的检测频率,即每nCheckFrequency隔多少个交易检测一次。此处以4294967295为基数设置检测频率。
那我们在来看传入的dFrequency值是如何计算的。在init.cpp中,dFrequency的值是1.0/ratio,ratio的计算是在如下代码实现的:
int ratio = std::min(std::max(GetArg("-checkmempool",chainparams.DefaultConsistencyChecks()? 1 : 0), 0), 1000000);
这里需要关注的是chainparams.DefaultConsistencyChecks(),chainparams是在const CChainParams& chainparams = Params();中获取,其具体实现的解读可在前文中详细了解。这里要说的是chainparams根据传入的命令参数(默认为主网),其有可能是主网、测试网或私有网3种对象之一。而这3种模式下DefaultConsistencyChecks获取的值是不一样的,在主网、测试网中其返回值为false(见Chainparams.cpp的CMainParams与CTestNetParams类中fDefaultConsistencyChecks=false),私有网的返回值为true(见Chainparams.cpp的CRegTestParams类中fDefaultConsistencyChecks=true)。即只有在私有网中才默认需要进行一致性检测。
而ratio值则是根据-checkmempool传入的参数值与1000000之间的最小值而定的,它们的比较取值用的是std::min,即ratio的值取二者中最小者,我们也可以看出其值不会超过1000000。
(2)区块索引检测
区块索引参数处理也很简单,其实现代码如下:
fCheckBlockIndex =GetBoolArg("-checkblockindex",chainparams.DefaultConsistencyChecks());
从其代码我们可以看出,只有在私有网模式下才会进行区块索引的检测,其他两个网默认是不检测的。
fCheckBlockIndex参数定义于src/validation.h中,该变量为全局变量,所以可在init.cpp中根据“checkblockindex”参数进行修改。进而在validation.cpp中的CheckBlockIndex函数中使用该变量,在这个函数中如果checkblockindex为true,将实现了区块索引信息的验证,具体验证内容我们将在后续的研读文章中详细记录。