![316b5fb9f3b4f9a1b071bf334fca8f68.png](https://img-blog.csdnimg.cn/img_convert/316b5fb9f3b4f9a1b071bf334fca8f68.png)
什么是classloader?
classloader直译过来是类加载器,类加载器主要用来动态加载库文件,动态加载是一种机制,计算机程序可以通过这种机制在运行时将库(或者其它二进制文件)加载到内存中,检索库中包含的函数和变量的地址,执行这些函数或访问这些变量。
跨平台的系统采用了动态加载,类加载器是JAVA的基石,需要插件(plugins)的系统也采用了动态加载,例如ROS和Apollo cyber的插件机制。
程序运行机制
程序运行一共有3种机制:
- 静态链接 (static linking)
- 动态链接 (dynamic linking)
- 动态加载(dynamic loading)
静态链接会把程序拷贝一份到可执行程序,带来的好处是不依赖任何外部程序就可以独立运行,坏处是浪费磁盘和内存空间,而动态链接和静态链接相反,动态链接是在编译时候指定链接到哪个库,在运行时查找对应的库并且加载到内存,而且不同的应用程序在内存中可以共享一份动态库,从而节省系统内存。
下面我们看一个例子,有2个文件hello.c和main.c分别如下:
#include<stdio.h>
void hello() {
printf("hello world!n");
}
我们通过命令`gcc -fPIC -shared hello.c -o libhello.so`把hello.c编译为动态库。
#include <stdio.h>
#include "hello.h"
int main() {
printf("call hello()n");
hello();
}
接下来用命令`gcc main.c -L. -lhello -o main`链接编译好的libhello库。
我们可以通过`ldd ./main`来查看运行程序依赖于哪些动态库。
(base) zero@zero-ThinkPad-E450:~/05test/class_loader$ ldd main
linux-vdso.so.1 (0x00007ffc4d19a000)
libhello.so => /lib/libhello.so (0x00007f2452dbb000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f24529ca000)
/lib64/ld-linux-x86-64.so.2 (0x00007f24531bf000)
同时也可以用`readelf -a ./main | grep interp`来查看运行时动态链接器。
![006ac0924879544c2df687c6df4b6171.png](https://img-blog.csdnimg.cn/img_convert/006ac0924879544c2df687c6df4b6171.png)
用`LD_DEBUG=libs ./main`来打开打印信息,查看main函数加载的时候如何动态找到需要的库文件。
![1547e0237de0db12bdba94676f6749ef.png](https://img-blog.csdnimg.cn/img_convert/1547e0237de0db12bdba94676f6749ef.png)
找到库文件之后,还需要动态绑定符号表,绑定符号表的过程就依赖于动态链接器,我们可以用`LD_DEBUG=bindings main`来打印绑定符号的过程。
![69bf69bbc29ece791abadd12b76b95c2.png](https://img-blog.csdnimg.cn/img_convert/69bf69bbc29ece791abadd12b76b95c2.png)
也就是说程序运行的时候通过interpreter来找到链接的动态库,然后去对应的共享库中绑定符号表,这一切完成之后进行init初始化,然后再把控制权交给main函数,main函数执行完成之后,在执行对应的fini过程。
动态加载的过程和动态库加载的过程类似。
动态加载
unix系统的动态加载接口在"dlfcn.h"中,主要有3个接口函数
- dlopen 加载动态库
- dlsym 根据符号名称获取函数实例
- dlclose 卸载动态库
dlopen
加载动态库到内存,调用的是dlopen函数,dlopen的接口如下,第一个参数是动态库的路径,第二个参数是flag,传入一些标志位
void *dlopen(const char *filename, int flag);
flag的参数有以下几种
- RTLD_LAZY // 懒加载
- RTLD_NOW // 立即加载
- RTLD_GLOBAL // 全局可见
- RTLD_LOCAL // 局部可见
- RTLD_NODELETE
- RTLD_NOLOAD
- RTLD_DEEPBIND
dlopen如果重复打开同一个共享库,第二次打开的时候返回的是之前打开的句柄,也就是说返回的是之前已经加载的共享库的地址,而不会重复加载,但是每次加载的时候共享库的引用计数会加1,当调用dlclose的时候,必须和dlopen有相同的次数,共享库才会被卸载。
动态库加载的时候,在把控制权交给main函数之前,会进行初始化init,我们可以看以下例子。在上述"hello.c"加入静态变量a,并且添加默认构造函数打印"init before main()",并且换成g++编译。
#include<stdio.h>
struct A {
A() {
printf("init before main()n");
}
};
static A a;
void hello() {
printf("hello world!n");
}
可以看到静态变量在main之前就执行默认构造函数了,之后才执行main函数中的内容。
![6b537fac122ea669b6d2a0be2ca07ed7.png](https://img-blog.csdnimg.cn/img_convert/6b537fac122ea669b6d2a0be2ca07ed7.png)
dlsym
当共享库加载到内存之后,如果需要调用共享库的函数或者创建对象,可以通过dlsym根据函数名获取函数实例。
void *dlsym(void *handle, const char *symbol);
这里的symbol就是对应的函数名,因为c++有函数重载,为了区分函数和变量,c++中编译器创建符号表的时候会对变量做name mangling,因此函数名经过name mangling之后就变成我们根本不认识的了,下面是name mangling之后的函数名称。
![f624a4aa88abf7f8c14c0922de904355.png](https://img-blog.csdnimg.cn/img_convert/f624a4aa88abf7f8c14c0922de904355.png)
如果需要通过函数名作为符号获取实例,必须用c语言来实现,因为c语言不会对进行name mangling。
dlclose
dlclose对动态库进行卸载
int dlclose(void *handle);
这里需要注意,dlclose的次数和dlopen的次数相等,共享库才会从内存中卸载。下面是一个例子。
B -> A
C -> A
先加载B,然后加载C,根据隐性依赖会加载2次A,卸载B的时候A的计数会减1,不会直接从内存中卸载(因为C还在内存,还依赖于A),但dlclose还是返回成功,只有当卸载C的时候,A的引用计数会清零,这时候A才会真正从内存中卸载。
poco库实现
poco库的动态库加载,借鉴了上述c语言不会进行name mangling,在c++中采用`extern "C"`嵌入c语言的函数,这样就可以直接通过dlsym函数名调用函数了。下面我们分析下poco库的加载过程。
![61a93c6800fbe020353a2f5c85556236.png](https://img-blog.csdnimg.cn/img_convert/61a93c6800fbe020353a2f5c85556236.png)
首先是通过宏来注册类,注册分为3个宏定义,宏定义实际上就是注册`Poco::MetaObject`到`pManifest_`,而`Poco::MetaObject`类实现了`create()`接口创建注册好的类。
#define POCO_EXPORT_CLASS(cls)
pManifest->insert(new Poco::MetaObject<cls, _Base>(#cls));
Poco::MetaObject类实现如下:
template <class C, class B>
class MetaObject: public AbstractMetaObject<B>
{
public:
MetaObject(const char* name): AbstractMetaObject<B>(name)
{
}
~MetaObject()
{
}
B* create() const // 创建注册好的类
{
return new C;
}
最后poco库的重点是用extern "C"进行函数声明。
extern "C"
{
bool POCO_LIBRARY_API pocoBuildManifest(Poco::ManifestBase* pManifest);
void POCO_LIBRARY_API pocoInitializeLibrary();
void POCO_LIBRARY_API pocoUninitializeLibrary();
}
总结下poco库的实现,是利用了c语言不会进行name mangling的特性,在loadlibrary的时候调用`pocoBuildManifest`,把对应的`Poco::MetaObject`注册到类中`pManifest_`,这样就可以通过`pManifest_`来创建注册好的对象了。
ROS实现
ROS的实现方式和poco库的实现略有差异,ROS也要对类进行注册,但是初始化的方式不一样,前面讲到过,在动态库被加载到内存,控制权交给main函数之前,会执行初始化的过程init,而ROS正是利用了这一特性,每个注册过程都会创建一个静态变量,而静态变量的初始化在类加载的时候会默认执行(因为静态变量不属于任何一个对象,因此程序加载的时候会自动创建静态变量,而这个过程在init中完成)。
知道了大概的原理,下面我们看以下ros中的具体实现。首先看注册类的宏定义。
class Duck : public Base
{
public:
virtual void saySomething() {std::cout << "Quack" << std::endl;}
};
class Cow : public Base
{
public:
virtual void saySomething() {std::cout << "Moooo" << std::endl;}
};
class Sheep : public Base
{
public:
virtual void saySomething() {std::cout << "Baaah" << std::endl;}
};
CLASS_LOADER_REGISTER_CLASS(Dog, Base)
CLASS_LOADER_REGISTER_CLASS(Cat, Base)
CLASS_LOADER_REGISTER_CLASS(Duck, Base)
上面通过`CLASS_LOADER_REGISTER_CLASS`宏定义完成注册派生类和基类。宏定义的具体实现如下
#define CLASS_LOADER_REGISTER_CLASS_INTERNAL_WITH_MESSAGE(Derived, Base, UniqueID, Message)
namespace
{
struct ProxyExec ## UniqueID
{
typedef Derived _derived;
typedef Base _base;
ProxyExec ## UniqueID()
{
class_loader::impl::registerPlugin<_derived, _base>(#Derived, #Base);
}
};
static ProxyExec ## UniqueID g_register_plugin_ ## UniqueID;
} // namespace
宏定义中就主动创建了一个静态类`g_register_plugin_ ## UniqueID`,而且动态类加载的时候会主动执行类的默认构造函数。
template<typename Derived, typename Base>
void registerPlugin(const std::string & class_name, const std::string & base_class_name)
{
// Create factory
impl::AbstractMetaObject<Base> * new_factory =
new impl::MetaObject<Derived, Base>(class_name, base_class_name);
new_factory->addOwningClassLoader(getCurrentlyActiveClassLoader());
new_factory->setAssociatedLibraryPath(getCurrentlyLoadingLibraryName());
// Add it to global factory map map
FactoryMap & factoryMap = getFactoryMapForBaseClass<Base>();
factoryMap[class_name] = new_factory;
}
这里factoryMap为一个map结构,key是派生类名称,value是`impl::MetaObject`,而这里的`impl::MetaObject`和POCO库中的一样,也实现了`Create()`接口用来创建注册好的类。
template<class C, class B>
class MetaObject : public AbstractMetaObject<B>
{
public:
MetaObject(const std::string & class_name, const std::string & base_class_name)
: AbstractMetaObject<B>(class_name, base_class_name)
{
}
B * create() const // 创建注册好的类
{
return new C;
}
};
之后就可以用注册好的`factoryMap`来创建对象了。
至此POCO库和ROS的classloader机制就分析完成了,而Apollo的classloader机制和ROS的基本类似。
动态加载的优缺点
优点
- 运行时更加灵活,可以动态的加载启动功能。
- 可以使用第三方的工具进行扩展
缺点
- 启动时间会慢20%
- 执行效率慢了5%,由于动态地址解析PIC
优秀实践
业界已经有不少c++的程序采用了动态加载机制,包括
- Apache HTTP Server
- ROS
- Apollo
- POCO
- Boost
其中COM组件的思想也是希望采用动态加载的机制实现跨平台和跨语言。
参考资料
- the_inside_story_on_shared_libraries_and_dynamic_loading
- ELF: From The Programmer's Perspective
- How To Write Shared Libraries