上文书说到区块链的存储方式,并结合了EOSIO进行分析,其中也提到了使用CLion调试EOS的方法。本文将继续深入细致地展开对加载了mongo_db_plugin的nodeos的调试过程以及心得。
关键字:源码分析,Debug EOS,nodeos,mongo_db_plugin,CLion,C++,boost::asio::signal_set,queue
本文涉及的环境:clang-6.0, clang++-6.0, GDB Debugger, make 4.1, mongodb-linux-x86_64-3.6.3, boost 1.67.0
调试EOS: nodeos关于EOS的调试环境的搭建这里不再赘述了,下文开始针对nodeos程序进行调试。
(一)CMakeList.txtnodeos开始运行前,要先使用项目的总CmakeList.txt配置,这里我配置了boost库的位置,如果你配置了boost的环境变量可以跳过这里。
set( BOOST_ROOT "/home/evsward/opt/boost")这个文件中有很多的set语句,这些语句都是开关,或者路径,或者全局变量,是配置各个子CMakeList.txt而用的。
include 语句是为runtime引入相关依赖库。
add_subdirectory语句设置了子目录程序。
install语句是将相关命令安装到指定位置以供runtime后续使用。
总的CMakeList文件介绍完了,下面会执行到nodeos目录下的CMakeList.txt文件:
add_executable( nodeos main.cpp )语句设定了nodeos程序执行入口。
set, configure_file, include, install 等都是为runtime准备的环境相关的。
重点语句target_link_libraries,这里定义了链runtime环境需要启动的plugin。(注意记住这个顺序)
(二)static register plugin我们打开每一个plugin的cpp文件,会发现有一个static的register方法的调用。这里会首先执行按上面plugin定义的顺序中第一个login_plugin,它的static语句如下:
static appbase::abstract_plugin& _login_plugin = app().register_plugin<login_plugin>();
执行此语句时,会先执行app(),这是application单例方法。
(三)applicationapplication是nodeos的执行者,上面调用的app函数:
application& application::instance() { static application _app; return _app; } application& app() { return application::instance(); }application与plugin拥有相同的实现函数,而由于它作为执行者、统筹者的存在,它会安排调用所有plugin,例如set_program_options。
执行app()以后获取到了application的实例,然后调用了register_plugin函数,通过模板类(泛型类)携带了login_plugin的类型。register_plugin函数是模板函数,定义在application.hpp文件中。
application.hpp 中定义了私有的内存变量
map<string, std::unique_ptr<abstract_plugin>> plugins;
abstract_plugin是所有plugin的基类,它定义了虚函数,需要继承它的子类去实现。他们与application的关系是:
abstract_plugin=>plugin(对基类的虚函数进一步使用,由application定义管理)=>各个plugin
template<typename Plugin> auto& register_plugin() { auto existing = find_plugin<Plugin>(); // 从plugins寻找该plugin是否已注册。 if(existing) return *existing; // 如果已注册了直接返回plugin的实例 auto plug = new Plugin(); // 创建该未注册plugin的实例 plugins[plug->name()].reset(plug); // 先插入到上面定义的内存变量plugins plug->register_dependencies();// 注册该plugin的依赖plugins,每个plugin内部都会调用APPBASE_PLUGIN_REQUIRES((chain_plugin))来注册自己依赖的别的plugin。 return *plug; // 返回plugin的实例 } (四)main.cpp->main()在编译runtime环境结束以后,进入入口函数main(),
int main(int argc, char** argv)
main函数的参数就是调用命令nodeos的通过--加入的参数,我们可以通过nodeos的Edit Configuration来调整。其中argc是个数,argv是参数的值,是一个数组类型。如下图:
我们接着来看main函数,它的函数体是通过app()对application单例进行的设置,包括版本号、data路径、config路径,然后是对于application实例内部方法的调用:
initialize<chain_plugin, http_plugin, net_plugin, producer_plugin>
startup()
exec()
main函数执行了内部函数initialize_logging()还通过ilog打印了日志,输出了版本号以及eosio root路径地址。
由于main函数是入口函数,上面也介绍了它主要是对application实例的使用,以及一些异常处理等,接下来会逐一进行debug跟踪分析。
(五)initialize plugin这个初始化函数是一个模板函数,模板类参数是plugin基类,在main函数调用该函数时传入了基本的插件依赖(这些是不需要我们在config中配置的,是链启动的基础插件):chain_plugin, http_plugin, net_plugin, producer_plugin。下面来看initialize函数在application头文件中的声明:
/** * @brief 查看 --plugin(存在于命令行或者config配置文件中)调用这些plugin的 initialize方法。 * * @tparam Plugin plugin的列表用来初始化,即使在config中没有配置的但被其他plugin所依赖的plugin,都可以统一使用该模板类没有影响。 * @return true:plugin初始化完成,false:出现异常。 */ template<typename... Plugin> bool initialize(int argc, char** argv) { return initialize_impl(argc, argv, {find_plugin<Plugin>()...}); // ...是可变参数的语法,上面通过main函数的调用,我们传入了多个plugin。 }实现类initialize_impl的内容较多,不粘贴源码,直接总结一下:
(1)set_program_options()函数application.cpp文件中的set_program_options()函数是用来生成初始化的config.ini文件内容以及nodeos命令行--help的输出内容。