1
3dwelcome 2016-04-19 11:54:17 +08:00 via Android
完全可以的、 dlopen 简直就是天生的热更新插件。
|
2
chmlai 2016-04-19 12:01:28 +08:00
二进制兼容性问题比较蛋疼吧
|
3
owt5008137 2016-04-19 12:02:54 +08:00
如果能解决好资源和对象的生命周期的管理问题。理论上是可以。
Windows 下,在一个 dll 里创建的对象,不能在另一个 dll 里释放。因为 Windows 下不同 dll 都有自己的堆。如果全局变量(包括单例)在 dll 里。那就要搞死人了。 Linux 下, so 里的全局变量(包括单例)不能有任何的启动初始化和卸载时析构(特别是用 C++的时候)。不然会对一个地址重复执行 ctor 和 dtor 。 其他的坑当然还有,诸如符号重复导入时的问题,接口版本不一致的问题等等。 好麻烦的说。 COM 可能是这种想法里设计的比较好的了。 |
4
3dwelcome 2016-04-19 12:15:52 +08:00 via Android
Windows 下 dll 需要把项目运行期设置为 dll 共享模式、就能共用内存分配函数。只是用起来还是没有 dlopen 方便、因为缺少 rtld_global 属性、不能直接调用主程序的函数体。
|
5
xylophone21 2016-04-19 12:19:44 +08:00
@owt5008137 "so 里的全局变量(包括单例)不能有任何的启动初始化和卸载时析构“表示不解,除非你完全不用全局(包括静态)变量,否则加载时就一定会构造,卸载时就一定会析构,语言就是这样定义的。
|
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 环境下共享静态库的问题 |
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 里的全局变量。 |
8
3dwelcome 2016-04-19 15:42:00 +08:00
不好意思,我理解有误,那句-ltest_a 是楼上故意加上的。是为了重现 bug: 指 dlopen 的时候,载入了一个全局符号,名字和主程序里一模一样,那就会再次初始化一次。
这特殊的情况以前还真没仔细想过,类似 windows 下的 dll, 每个模块编译后的 symbol 都是独立的,不会交叉引用,也完全不可能发生 so 这种符号名字冲突现象。 |
9
xylophone21 2016-04-19 15:52:20 +08:00
同意 @3dwelcome 的说法,应该属于菱形依赖+混合动态 /静态链接的坑。
这个坑我也踩过,不过更隐蔽一些, a 模块的链接关系不是手写的,是 cmake 自动传染过来的。 |
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 |
11
owt5008137 2016-04-19 15:54:09 +08:00
@3dwelcome 是的,然而等我敲完上一条回复以后才看到后一条回复
|
12
owt5008137 2016-04-19 15:57:24 +08:00
@xylophone21 是的。碰到同时存在动态库、静态库,然后又用像 cmake 或者 gyp 那种自动分析依赖关系的。确实容易一不留神就 GG 了。所以我现在项目里全部换成动态库了,省得再碰上这种糟心事儿。
|
13
Monad 2016-04-19 16:44:26 +08:00 1
换个思路,把稳定的部分和不稳定的部分分离开,然后通过共享内存队列等在进程重启后仍然能够使用的方式来进行通信。
|
14
0987363 2016-04-19 17:22:48 +08:00
以前公司做插件的时候就是 dlopen 加载插件,这样不影响其他插件
|
15
owt5008137 2016-04-19 18:35:38 +08:00
@Monad 我也赞同这种方法可控性更好。 TX 的 MMO 后台貌似都是这么做得
|