小程序动态class_ROS class loader动态加载原理剖析

1e70407333d2d6b2177751e81c686571.png

什么是classloader?

classloader直译过来是类加载器,类加载器主要用来动态加载库文件,动态加载是一种机制,计算机程序可以通过这种机制在运行时将库(或者其它二进制文件)加载到内存中,检索库中包含的函数和变量的地址,执行这些函数或访问这些变量。

跨平台的系统采用了动态加载,类加载器是JAVA的基石,需要插件(plugins)的系统也采用了动态加载,例如ROS和Apollo cyber的插件机制。

程序运行机制

程序运行一共有3种机制:

  1. 静态链接 (static linking)
  2. 动态链接 (dynamic linking)
  3. 动态加载(dynamic loading)

静态链接会把程序拷贝一份到可执行程序,带来的好处是不依赖任何外部程序就可以独立运行,坏处是浪费磁盘和内存空间,而动态链接和静态链接相反,动态链接是在编译时候指定链接到哪个库,在运行时查找对应的库并且加载到内存,而且不同的应用程序在内存中可以共享一份动态库,从而节省系统内存。

下面我们看一个例子,有2个文件hello.c和main.c分别如下:

#include

我们通过命令`gcc -fPIC -shared hello.c -o libhello.so`把hello.c编译为动态库。

#include 

接下来用命令`gcc main.c -L. -lhello -o main`链接编译好的libhello库。

我们可以通过`ldd ./main`来查看运行程序依赖于哪些动态库。

(

同时也可以用`readelf -a ./main | grep interp`来查看运行时动态链接器。

b1ad89f16d709a2d0705f33a35f67b8f.png
系统默认动态连接器

用`LD_DEBUG=libs ./main`来打开打印信息,查看main函数加载的时候如何动态找到需要的库文件。

0eeee05817d12d1a153574b41bba6b72.png
查找动态库

找到库文件之后,还需要动态绑定符号表,绑定符号表的过程就依赖于动态链接器,我们可以用`LD_DEBUG=bindings main`来打印绑定符号的过程。

efdc8c83e74316727bfd4621c07bf775.png
绑定符号表

也就是说程序运行的时候通过interpreter来找到链接的动态库,然后去对应的共享库中绑定符号表,这一切完成之后进行init初始化,然后再把控制权交给main函数,main函数执行完成之后,在执行对应的fini过程

动态加载的过程和动态库加载的过程类似。

动态加载

unix系统的动态加载接口在"dlfcn.h"中,主要有3个接口函数

  • dlopen 加载动态库
  • dlsym 根据符号名称获取函数实例
  • dlclose 卸载动态库

dlopen

加载动态库到内存,调用的是dlopen函数,dlopen的接口如下,第一个参数是动态库的路径,第二个参数是flag,传入一些标志位

void 

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

可以看到静态变量在main之前就执行默认构造函数了,之后才执行main函数中的内容。

9cc9e9ceb1d247411b196a1686f8c7b6.png

dlsym

当共享库加载到内存之后,如果需要调用共享库的函数或者创建对象,可以通过dlsym根据函数名获取函数实例。

void 

这里的symbol就是对应的函数名,因为c++有函数重载,为了区分函数和变量,c++中编译器创建符号表的时候会对变量做name mangling,因此函数名经过name mangling之后就变成我们根本不认识的了,下面是name mangling之后的函数名称。

e44cb43daad3dfaaa9ab2450448c81ce.png
routing模块name mangling之后的名称

如果需要通过函数名作为符号获取实例,必须用c语言来实现,因为c语言不会对进行name mangling。

dlclose

dlclose对动态库进行卸载

int 

这里需要注意,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库的加载过程。

29ead34478e13ec19378118ab56121e7.png

首先是通过宏来注册类,注册分为3个宏定义,宏定义实际上就是注册`Poco::MetaObject`到`pManifest_`,而`Poco::MetaObject`类实现了`create()`接口创建注册好的类。

#define POCO_EXPORT_CLASS(cls) 

Poco::MetaObject类实现如下:

template 

最后poco库的重点是用extern "C"进行函数声明。

extern 

总结下poco库的实现,是利用了c语言不会进行name mangling的特性,在loadlibrary的时候调用`pocoBuildManifest`,把对应的`Poco::MetaObject`注册到类中`pManifest_`,这样就可以通过`pManifest_`来创建注册好的对象了。

ROS实现

ROS的实现方式和poco库的实现略有差异,ROS也要对类进行注册,但是初始化的方式不一样,前面讲到过,在动态库被加载到内存,控制权交给main函数之前,会执行初始化的过程init,而ROS正是利用了这一特性,每个注册过程都会创建一个静态变量,而静态变量的初始化在类加载的时候会默认执行(因为静态变量不属于任何一个对象,因此程序加载的时候会自动创建静态变量,而这个过程在init中完成)。

知道了大概的原理,下面我们看以下ros中的具体实现。首先看注册类的宏定义。

class 

上面通过`CLASS_LOADER_REGISTER_CLASS`宏定义完成注册派生类和基类。宏定义的具体实现如下

#define CLASS_LOADER_REGISTER_CLASS_INTERNAL_WITH_MESSAGE(Derived, Base, UniqueID, Message) 

宏定义中就主动创建了一个静态类`g_register_plugin_ ## UniqueID`,而且动态类加载的时候会主动执行类的默认构造函数。

template

这里factoryMap为一个map结构,key是派生类名称,value是`impl::MetaObject`,而这里的`impl::MetaObject`和POCO库中的一样,也实现了`Create()`接口用来创建注册好的类。

template

之后就可以用注册好的`factoryMap`来创建对象了。

至此POCO库和ROS的classloader机制就分析完成了,而Apollo的classloader机制和ROS的基本类似。

动态加载的优缺点

优点

  1. 运行时更加灵活,可以动态的加载启动功能。
  2. 可以使用第三方的工具进行扩展

缺点

  1. 启动时间会慢20%
  2. 执行效率慢了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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值