V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yueyoum
V2EX  ›  程序员

c/c++ 通过 dlopen 是不是同样可以实现热更新?

  •  
  •   yueyoum ·
    yueyoum · 2016-04-19 11:22:57 +08:00 · 12264 次点击
    这是一个创建于 3141 天前的主题,其中的信息可能已经有所发展或是发生改变。
    c/c++ 热更最常见的就是 用 lua, python 做脚本,
    但是这种方案 还要考虑到 语言交换的问题, 有时候并不是很方便。

    如果直接采用 dlopen 动态连接库, 那么 本体程序 和 库 程序之间 完全可以 传递指针,引用, 很方便。

    我也做了些实验, 本体程序注册文件修改事件, 只要库变了, 就会重新 dlopen 这个库,
    而本体的一些 逻辑 是调用的 库里的函数, 这样库变了, 本体程序的行为也就变了。

    这样不就可以热更了吗?

    但是我没搜到太多的 使用 dlopen 来热更的 资料, 难道这个办法有什么坑?
    15 条回复    2016-04-19 18:35:38 +08:00
    3dwelcome
        1
    3dwelcome  
       2016-04-19 11:54:17 +08:00 via Android
    完全可以的、 dlopen 简直就是天生的热更新插件。
    chmlai
        2
    chmlai  
       2016-04-19 12:01:28 +08:00
    二进制兼容性问题比较蛋疼吧
    owt5008137
        3
    owt5008137  
       2016-04-19 12:02:54 +08:00
    如果能解决好资源和对象的生命周期的管理问题。理论上是可以。

    Windows 下,在一个 dll 里创建的对象,不能在另一个 dll 里释放。因为 Windows 下不同 dll 都有自己的堆。如果全局变量(包括单例)在 dll 里。那就要搞死人了。

    Linux 下, so 里的全局变量(包括单例)不能有任何的启动初始化和卸载时析构(特别是用 C++的时候)。不然会对一个地址重复执行 ctor 和 dtor 。

    其他的坑当然还有,诸如符号重复导入时的问题,接口版本不一致的问题等等。

    好麻烦的说。 COM 可能是这种想法里设计的比较好的了。
    3dwelcome
        4
    3dwelcome  
       2016-04-19 12:15:52 +08:00 via Android
    Windows 下 dll 需要把项目运行期设置为 dll 共享模式、就能共用内存分配函数。只是用起来还是没有 dlopen 方便、因为缺少 rtld_global 属性、不能直接调用主程序的函数体。
    xylophone21
        5
    xylophone21  
       2016-04-19 12:19:44 +08:00
    @owt5008137 "so 里的全局变量(包括单例)不能有任何的启动初始化和卸载时析构“表示不解,除非你完全不用全局(包括静态)变量,否则加载时就一定会构造,卸载时就一定会析构,语言就是这样定义的。
    owt5008137
        6
    owt5008137  
       2016-04-19 14:23:07 +08:00
    @xylophone21 这里说的构造和析构指的是假如使用 C++的话, so 里的全局(包括静态)对象,不能有构造函数和析构函数,而不是指内存的分配操作。纯 C 或者 C++的 POD 类型的构造和析构是不会执行任何初始化操作和回收操作的,不会有问题。

    具体指不要出现类似这样的代码:

    ```
    void func() {
    static int a = 123;
    static CLASS obj(123,456);
    }
    ```

    其实这么说可能不是特别准确,因为如果全局(包括静态)对象如果只是在 so 内部使用,并不暴露给外部的话其实也并没什么大问题。这里这么说其实是设计上尽量避免问题出现的可能性。

    因为既然是使用动态库做热更新,那不可避免地会出现多个 so 之间或者 so 和二进制之间会有相同的符号(包括引入了相同的源文件或者链接了相同的.a ),那么这些符号在所有的 so 里都会尝试执行一次实例化。

    详见:
    https://www.owent.net/JqRzQ

    https://www.owent.net/Il9XS#坑二 linux 环境下共享静态库的问题
    3dwelcome
        7
    3dwelcome  
       2016-04-19 15:08:20 +08:00
    @owt5008137 为了给 dlopen 正名,我编译了一下你 blog 里的 a.h, a.cpp, b.cpp, c.cpp 文件,完全没有你说的重复两次初始化问题。

    输出结果如下:
    [root@localhost test]# ./test_b
    foo_class::foo_class(), this-> 0x600fa8
    &foo_class::_ = 0x600fa8, foo_class::_.m = 1010
    &foo_class::_ = 0x600fa8, foo_class::_.m = 1110
    foo_class::~foo_class(), this-> 0x600fa8
    [root@localhost test]#


    ------------------
    问题的症结在于,你编译 libtest_c.so 的时候,不应该用到-ltest_a ,应该用-rdynamic ,这样主程序在调用 dlopen("./libtest_c.so", RTLD_NOW|RTLD_GLOBAL)的时候,自动会通过符号找到 a.cpp 里的全局变量。
    3dwelcome
        8
    3dwelcome  
       2016-04-19 15:42:00 +08:00
    不好意思,我理解有误,那句-ltest_a 是楼上故意加上的。是为了重现 bug: 指 dlopen 的时候,载入了一个全局符号,名字和主程序里一模一样,那就会再次初始化一次。

    这特殊的情况以前还真没仔细想过,类似 windows 下的 dll, 每个模块编译后的 symbol 都是独立的,不会交叉引用,也完全不可能发生 so 这种符号名字冲突现象。
    xylophone21
        9
    xylophone21  
       2016-04-19 15:52:20 +08:00
    同意 @3dwelcome 的说法,应该属于菱形依赖+混合动态 /静态链接的坑。
    这个坑我也踩过,不过更隐蔽一些, a 模块的链接关系不是手写的,是 cmake 自动传染过来的。
    owt5008137
        10
    owt5008137  
       2016-04-19 15:52:35 +08:00
    @3dwelcome 关键不在于-rdynamic ,而是多个.so 之间或者.so 和可执行程序之间有相同的符号,最简单的构造方式就是链接相同的.a 或者编译进去相同的源文件。当项目结构复杂的时候除非强制依赖库全部用共享库,否则很难保证符号不重复。

    libtest_c.so 的时候,不-ltest_a 倒是可以,但是这里的 sample 比较简单。如果这么做的话有两个前提
    1. test_a 要编译成动态库
    2. test_b 要把 libtest_a.so dlopen 进来。(意味着大型项目中可执行程序需要手动并按顺序把所有依赖的动态库 dlopen 进来)

    如果不按上述方法做就可能碰到 https://www.owent.net/JqRzQ#comment-448 提到的问题。当然还有一种方法是链接选项里加上不裁剪未使用的符号,但是这样很会影响 LTO 。(不考虑每个模块需要手动精细地控制的情况)

    另:我 blog 里的例子, b.cpp:18 改成 void* handle = dlopen("./libtest_c.so", RTLD_NOW|RTLD_GLOBAL);
    编译选项改成:
    gcc -O0 -g -ggdb a.cpp -o libtest_a.a -c -fPIC -rdynamic
    gcc -O0 -g -ggdb b.cpp -o test_b -fPIC -ldl -L$PWD -ltest_a -lstdc++ -rdynamic
    gcc -O0 -g -ggdb c.cpp -o libtest_c.so -shared -fPIC -L$PWD -ltest_a -lstdc++ -rdynamic

    执行结果如下:
    foo_class::foo_class(), this-> 0x602068
    &foo_class::_ = 0x602068, foo_class::_.m = 1010
    foo_class::foo_class(), this-> 0x602068
    &foo_class::_ = 0x602068, foo_class::_.m = 110
    foo_class::~foo_class(), this-> 0x602068
    foo_class::~foo_class(), this-> 0x602068

    我本地环境是 CentOS 7, GCC 4.8.5
    owt5008137
        11
    owt5008137  
       2016-04-19 15:54:09 +08:00
    @3dwelcome 是的,然而等我敲完上一条回复以后才看到后一条回复
    owt5008137
        12
    owt5008137  
       2016-04-19 15:57:24 +08:00
    @xylophone21 是的。碰到同时存在动态库、静态库,然后又用像 cmake 或者 gyp 那种自动分析依赖关系的。确实容易一不留神就 GG 了。所以我现在项目里全部换成动态库了,省得再碰上这种糟心事儿。
    Monad
        13
    Monad  
       2016-04-19 16:44:26 +08:00   ❤️ 1
    换个思路,把稳定的部分和不稳定的部分分离开,然后通过共享内存队列等在进程重启后仍然能够使用的方式来进行通信。
    0987363
        14
    0987363  
       2016-04-19 17:22:48 +08:00
    以前公司做插件的时候就是 dlopen 加载插件,这样不影响其他插件
    owt5008137
        15
    owt5008137  
       2016-04-19 18:35:38 +08:00
    @Monad 我也赞同这种方法可控性更好。 TX 的 MMO 后台貌似都是这么做得
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1094 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 23:27 · PVG 07:27 · LAX 15:27 · JFK 18:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.