动态绑定class_ROS class loader动态加载原理剖析

316b5fb9f3b4f9a1b071bf334fca8f68.png

什么是classloader?

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

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

程序运行机制

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

  1. 静态链接 (static linking)
  2. 动态链接 (dynamic linking)
  3. 动态加载(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
系统默认动态连接器

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

1547e0237de0db12bdba94676f6749ef.png
查找动态库

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

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

dlsym

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

void *dlsym(void *handle, const char *symbol);

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

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

如果需要通过函数名作为符号获取实例,必须用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

首先是通过宏来注册类,注册分为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的基本类似。

动态加载的优缺点

优点

  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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Cartographer ROS 是一个 SLAM(Simultaneous Localization and Mapping,即同时位与地图构建)系统,它使用 Google 的 Cartographer 库来构建 2D 和 3D 地图。它可以通过激光雷达或 RGB-D 摄像头获取传感器数据,执行实时位和地图构建,并提供了一些有用的工具来可视化和分析地图。 Cartographer ROS 的工作原理如下: 1. 数据采集:Cartographer ROS 可以接收来自激光雷达或 RGB-D 摄像头的传感器数据。传感器数据是用 ROS 消息格式发送的。 2. 传感器数据预处理:Cartographer ROS 可以对传感器数据进行预处理,例如去除不必要的噪声、滤波、畸变校正等。 3. 实时位:Cartographer 使用高效的算法来实现实时位,它会估计机器人在已知地图中的位置。这个过程涉及使用激光雷达或 RGB-D 摄像头的数据来匹配已知地图中的特征点,并使用优化算法来估计机器人的位置。 4. 地图构建:一旦机器人的位置被估计出来,Cartographer 就会使用机器人的传感器数据来更新地图。这个过程涉及将传感器数据转换为地图中的特征点、线、面等,并使用优化算法来拟合这些特征点,从而生成高质量的地图。 5. 地图优化:Cartographer ROS 还提供了一些工具来优化生成的地图。例如,可以使用图像处理技术来去除地图中的噪声、缩小地图的尺寸、合并相似的特征等。 6. 地图发布:Cartographer ROS 最终会将生成的地图以 ROS 消息的形式发布出去,这样其他 ROS 节点就可以使用这个地图进行导航、路径规划等操作。 总之,Cartographer ROS 是一个非常强大和灵活的 SLAM 系统,它可以用于各种机器人平台和应用场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值