隐藏符号可见性下 fmtlog 跨 DSO 的日志丢失与崩溃
隐藏符号可见性下 fmtlog 跨 DSO 的日志丢失与崩溃
fmtlog,DSO,符号可见性。
程序启动后会先输出两行工作线程绑核日志,接着直接 free(): invalid pointer,Aborted (core dumped)。
08:46:47.442681 worker_thread.cpp:29 INF pthread_setaffinityp success, cpu:3
08:46:47.442730 worker_thread.cpp:29 INF pthread_setaffinityp success, cpu:4
free(): invalid pointer
Aborted (core dumped)
本地开发时一切正常,只有把某个模块编译成 .so 发给别人之后才会遇到。这个问题前面还叠着另一个更烦的现象:工作线程里的 LOGI_SYNC / LOGE_SYNC 有时不会写进日志文件。
当时没继续追日志丢失,先在写入模块的后端实现里加了一行:
fmtlog::setLogFile(logFilePath.c_str());
加完之后确实能捞回一部分日志,但没有全回来,启动还多了这个崩溃。
先把这行删掉,崩溃消失,工作线程日志也跟着没了。后来又在 .so 内部单独启动了一个轮询线程,两个轮询线程同时写一个文件,输出交叉得没法看。期间还怀疑同步日志宏里调用了 fmtlog::poll(true),去找了一遍,当前版本的宏里并没有这句,去掉这个猜测后崩溃也没变化。
回头看 fmtlog 的实现,问题基本都指向 header-only 库的全局状态[1] 。
fmtlog.h 里通过 fmtlogWrapper<>::impl 保存实例,fmtlog-inl.h 里还有一份由 fmtlogDetailWrapper<>::impl 保存的日志实现。setLogFile() 最后操作的是后者:
template<int _>
void fmtlogT<_>::setLogFile(const char* filename, bool truncate) {
auto& d = fmtlogDetailWrapper<>::impl;
FILE* newFp = fopen(filename, truncate ? "w" : "a");
if (!newFp) {
std::string err = fmt::format("Unable to open file: {}: {}", filename, strerror(errno));
fmt::detail::throw_format_error(err.c_str());
}
setbuf(newFp, nullptr);
d.fpos = ftell(newFp);
closeLogFile();
d.outputFp = newFp;
d.manageFp = true;
}
当时两个编译成 .so 的模块都开了隐藏可见性:
SET_TARGET_PROPERTIES(my_target PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
)
这样一来,主程序和 .so 里的 fmtlog 弱符号不会合并[2] ,.so 会拿到自己的一份 fmtlogDetailWrapper<>::impl。主程序里初始化日志时打开文件并启动轮询线程,实际只配置了主程序的实例;.so 里的 logi() 仍然往自己的队列写,自然没有线程去消费。
这也解释了为什么 .so 里再调用一次 setLogFile() 能恢复部分日志,但最后的 free(): invalid pointer,一开始我觉得是 .so 的 setLogFile() 调用了 fclose(),恰好关掉主程序轮询线程正在写的 FILE*。后来重新看源码,closeLogFile() 操作的是当前 DSO 内的 fmtlogDetailWrapper<>::impl,按照实例已经分裂的前提,它关闭的应该也是当前实例自己的文件句柄,不能直接推出原来的 fclose/fwrite 竞态。
所以最后那次 free() 具体落在了哪里,目前还没办法从现有记录里还原出来。现在能确定的是:fmtlog 的全局状态被 DSO 边界分开了;把 hidden 去掉后,主程序配置和轮询线程能覆盖所有模块,日志也不再需要每个 .so 单独打开一次文件。
修改时先移除了两个模块的 CMake 配置里那段隐藏可见性设置,再删掉后端实现里重复的 setLogFile()。
在 CentOS 7.9 上 clean build 后重新启动,free(): invalid pointer 不再出现,两个模块的工作线程日志也都能正常进入同一个日志文件。
所以这次与其说是修好了这两处,不如说是把两份 fmtlog 合成了一份。

浙公网安备 33010602011771号