【C/C++】动态加载(dlopen)和直接链接 库的区别
动态加载(dlopen)方式加载库和直接链接库 有什么区别
动态加载(dlopen 方式)和直接链接库是程序使用外部库的两种不同方式,主要区别体现在加载时机、灵活性、依赖管理等方面,具体如下:
直接链接库 vs. 动态加载 (dlopen) 对比表
| 特性维度 | 直接链接库 | 动态加载 (dlopen) |
|---|---|---|
| 1. 加载时机 | 在程序启动时由操作系统自动加载。 | 在程序运行时通过代码手动加载。 |
| 2. 灵活性 | 低。库在编译时确定,路径和名称固定,更换库通常需重新编译。 | 高。可根据配置、用户输入等动态选择加载不同的库,支持热更新。 |
| 3. 依赖管理 | 强依赖。程序启动时,所有被链接的库必须存在,否则启动失败。 | 弱依赖。程序启动时不依赖目标库,库不存在时可进行错误处理,程序健壮性更强。 |
| 4. 内存占用 | 启动时即加载所有依赖库,内存占用相对固定。 | 按需加载,未使用的库不占用内存,有利于减少内存占用。 |
| 5. 符号解析 | 在编译链接阶段完成,符号缺失会导致编译错误。 | 在运行时通过 dlsym 查找,符号缺失需在代码中判断,否则导致运行时错误。 |
| 6. 适用场景 | 核心库、标准库、依赖关系固定的常用库。 | 插件系统、按需加载的功能模块、 |
1. 加载时机
- 直接链接库:在程序编译链接阶段就会将库文件(静态库
.a或动态库.so/.dll)的信息编入可执行文件中。程序启动时,操作系统会自动加载所需的库到内存中,完成符号解析。 - 动态加载(
dlopen):库的加载推迟到程序运行时。程序通过dlopen函数手动指定库文件路径,在需要使用库中功能时才加载,加载后通过dlsym获取符号(函数 / 变量)地址来调用。
2. 灵活性
- 直接链接库:编译时必须明确知道要链接的库,且库的路径和名称在编译后固定。如果需要更换库,通常需要重新编译链接。
- 动态加载:运行时可以根据条件(如配置文件、用户输入)动态选择加载不同的库,甚至可以在程序运行中卸载(
dlclose)并重新加载库,无需重新编译程序。
3. 依赖管理
- 直接链接库:程序启动时必须能找到依赖的库,否则会启动失败(如 Linux 中报
error while loading shared libraries)。 - 动态加载:程序启动时不依赖目标库,即使库不存在,也可以通过错误处理(如判断
dlopen返回值)让程序继续运行,提高了程序的健壮性。
4. 内存占用
- 直接链接库:所有依赖的库在程序启动时即被加载(动态库会被映射到内存,静态库则被合并到可执行文件)。
- 动态加载:只在需要时加载库,未使用的库不会占用内存,适合功能模块较多且不常同时使用的场景(如插件系统)。
5. 符号解析
- 直接链接库:编译链接时会进行符号检查,若库中缺少程序调用的符号,会直接报错。
- 动态加载:符号解析在运行时进行,编译阶段无法检查符号是否存在,需要在代码中通过
dlsym调用后判断是否成功,否则可能导致运行时错误(如段错误)。
6. 适用场景
- 直接链接库:适用于依赖关系固定、启动时就需要加载的核心库(如标准库、常用工具库)。
- 动态加载:适用于插件系统(如浏览器插件、IDE 插件)、按需加载的功能模块、热更新场景,或需要在运行时选择不同实现的场景。
示例对比
直接链接:
// 编译时需链接 math 库(-lm) #include <math.h> int main() {double result = sqrt(2.0); // 直接调用库函数return 0; }动态加载(
dlopen):#include <dlfcn.h> #include <stdio.h> int main() {// 运行时加载 math 库void* handle = dlopen("libm.so", RTLD_LAZY);if (!handle) {fprintf(stderr, "dlopen error: %s\n", dlerror());return 1;}// 获取 sqrt 函数地址double (*sqrt_func)(double) = dlsym(handle, "sqrt");if (!sqrt_func) {fprintf(stderr, "dlsym error: %s\n", dlerror());dlclose(handle);return 1;}double result = sqrt_func(2.0); // 调用动态获取的函数dlclose(handle); // 卸载库return 0; }
总结来说,直接链接库更简单、安全,适合固定依赖;
动态加载更灵活,适合按需加载或动态扩展,但需要手动处理加载、符号解析和错误检查。
ncclResult_t buildIbvSymbols(struct ncclIbvSymbols* ibvSymbols) {static void* ibvhandle = NULL;void* tmp;void** cast;ibvhandle=dlopen("libibverbs.so", RTLD_NOW);if (!ibvhandle) {ibvhandle=dlopen("libibverbs.so.1", RTLD_NOW);if (!ibvhandle) {INFO(NCCL_INIT, "Failed to open libibverbs.so[.1]");goto teardown;}}#define LOAD_SYM(handle, symbol, funcptr) do { \cast = (void**)&funcptr; \tmp = dlvsym(handle, symbol, IBVERBS_VERSION); \if (tmp == NULL) { \WARN("dlvsym failed on %s - %s version %s", symbol, dlerror(), IBVERBS_VERSION); \goto teardown; \} \*cast = tmp; \} while (0)// Attempt to load a specific symbol version - fail silently
#define LOAD_SYM_VERSION(handle, symbol, funcptr, version) do { \cast = (void**)&funcptr; \*cast = dlvsym(handle, symbol, version); \} while (0)LOAD_SYM(ibvhandle, "ibv_get_device_list", ibvSymbols->ibv_internal_get_device_list);LOAD_SYM(ibvhandle, "ibv_free_device_list", ibvSymbols->ibv_internal_free_device_list);LOAD_SYM(ibvhandle, "ibv_get_device_name", ibvSymbols->ibv_internal_get_device_name);LOAD_SYM(ibvhandle, "ibv_open_device", ibvSymbols->ibv_internal_open_device);LOAD_SYM(ibvhandle, "ibv_close_device", ibvSymbols->ibv_internal_close_device);LOAD_SYM(ibvhandle, "ibv_get_async_event", ibvSymbols->ibv_internal_get_async_event);LOAD_SYM(ibvhandle, "ibv_ack_async_event", ibvSymbols->ibv_internal_ack_async_event);LOAD_SYM(ibvhandle, "ibv_query_device", ibvSymbols->ibv_internal_query_device);LOAD_SYM(ibvhandle, "ibv_query_port", ibvSymbols->ibv_internal_query_port);LOAD_SYM(ibvhandle, "ibv_query_gid", ibvSymbols->ibv_internal_query_gid);LOAD_SYM(ibvhandle, "ibv_query_qp", ibvSymbols->ibv_internal_query_qp);LOAD_SYM(ibvhandle, "ibv_alloc_pd", ibvSymbols->ibv_internal_alloc_pd);LOAD_SYM(ibvhandle, "ibv_dealloc_pd", ibvSymbols->ibv_internal_dealloc_pd);LOAD_SYM(ibvhandle, "ibv_reg_mr", ibvSymbols->ibv_internal_reg_mr);// Cherry-pick the ibv_reg_mr_iova2 API from IBVERBS 1.8LOAD_SYM_VERSION(ibvhandle, "ibv_reg_mr_iova2", ibvSymbols->ibv_internal_reg_mr_iova2, "IBVERBS_1.8");// Cherry-pick the ibv_reg_dmabuf_mr API from IBVERBS 1.12LOAD_SYM_VERSION(ibvhandle, "ibv_reg_dmabuf_mr", ibvSymbols->ibv_internal_reg_dmabuf_mr, "IBVERBS_1.12");LOAD_SYM(ibvhandle, "ibv_dereg_mr", ibvSymbols->ibv_internal_dereg_mr);LOAD_SYM(ibvhandle, "ibv_create_cq", ibvSymbols->ibv_internal_create_cq);LOAD_SYM(ibvhandle, "ibv_destroy_cq", ibvSymbols->ibv_internal_destroy_cq);LOAD_SYM(ibvhandle, "ibv_create_qp", ibvSymbols->ibv_internal_create_qp);LOAD_SYM(ibvhandle, "ibv_modify_qp", ibvSymbols->ibv_internal_modify_qp);LOAD_SYM(ibvhandle, "ibv_destroy_qp", ibvSymbols->ibv_internal_destroy_qp);LOAD_SYM(ibvhandle, "ibv_fork_init", ibvSymbols->ibv_internal_fork_init);LOAD_SYM(ibvhandle, "ibv_event_type_str", ibvSymbols->ibv_internal_event_type_str);LOAD_SYM_VERSION(ibvhandle, "ibv_query_ece", ibvSymbols->ibv_internal_query_ece, "IBVERBS_1.10");LOAD_SYM_VERSION(ibvhandle, "ibv_set_ece", ibvSymbols->ibv_internal_set_ece, "IBVERBS_1.10");return ncclSuccess;teardown:
……if (ibvhandle != NULL) dlclose(ibvhandle);return ncclSystemError;
}
rccl中verbs是动态加载的,所以当使用mlx5中的接口时,如果没有在编译文件中添加连接mlx5的选项就会报错:
ncclResult_t buildIbvSymbols(struct ncclIbvSymbols* ibvSymbols) {static void* ibvhandle = NULL;void* tmp;void** cast;ibvhandle=dlopen("libibverbs.so", RTLD_NOW);if (!ibvhandle) {ibvhandle=dlopen("libibverbs.so.1", RTLD_NOW);if (!ibvhandle) {INFO(NCCL_INIT, "Failed to open libibverbs.so[.1]");goto teardown;}}
