我们的故事可以先从MySQL数据库说起。假设你写了一个C++程序,需要访问MySQL数据库。那么,你需要在程序开始的调用mysql_library_init方法初始化mysql代码库,然后与之相对应的在程序结束的时候调用mysql_library_end。
例如:
#include <mysql.h>
#include <stdlib.h>
int main(void) {
if (mysql_library_init(0, NULL, NULL)) {
fprintf(stderr, "could not initialize MySQL client library\n");
exit(1);
}
/* Use any MySQL API functions here */
mysql_library_end();
return EXIT_SUCCESS;
}
那么,我们能不能把mysql_library_init和mysql_library_end这两个函数用一个C++类包装起来?在这个类的构造函数里调用mysql_library_init,析构函数里调用mysql_library_end。然后定义一个该类型的全局变量,来简化这个操作?比如:
#include <mysql.h>
#include <stdlib.h>
class MysqlInit{
public:
MysqlInit(){
if (mysql_library_init(0, NULL, NULL)) {
fprintf(stderr, "could not initialize MySQL client library\n");
abort();
}
}
~MysqlInit(){
mysql_library_end();
}
};
MysqlInit mysql_init_var;
int main(void) {
/* Use any MySQL API functions here */
return 0;
}
接下来这篇文章的主要目的就是为了分析这样做会有什么问题。
首先,上述程序不能采用静态链接的方式链接到mysql library。因为mysql library自身也会使用全局变量,在`mysql_init_var`变量构造的时候,mysql library的全局变量们可能还没初始化。这就是常说的“static initialization order ‘fiasco’ (problem)”。
如果上述程序动态链接到mysql library, 粗略来看没有什么问题。操作系统在加载这个应用程序的时候,会先查看它有哪些动态库依赖。既然它知道你要使用MySQL library,它就会去先加载并初始化那个library。并且,在进程退出的时候,它也会按照相反的顺序去调用析构函数。“粗略”二字是指遇到C++11 magic statics的时候会有问题,它不遵守这个原则。后面会细说。
如果该应用程序用dlopen(或者LoadLibrary Windows API)来加载mysql library,问题就变复杂了。我们需要分两种情况讨论:
情况1. 该应用程序会正确的使用dlclose(或FreeLibrary Windows API)来在恰当的时间卸载该动态库(MySQL Library),并且dlclose(或FreeLibrary)实际执行成功了。那么这个行为是可预测的。在那一刻该动态库(MySQL Library)内部的所有全局变量都会被销毁掉,然后内存被释放掉。从此你不要再使用该动态库的任何函数(任何Mysql API)。
情况2. 该应用程序没有调用dlclose(或FreeLibrary),或者尽管调用了但是该操作是no-op。那么在Windows上主exe里面的全局变量先被析构,然后是DLL里的。Linux上是反过来,意味着你在调用mysql_library_end的时候会crash。
那么什么情况下dlclose会是no-op呢?当这个shared library可能还需要被别人用的时候。一个典型的例子是:如果你把一堆c++代码编译成一个动态库,但是像mysql一样设计一套C API并且没有使用ld的version script来限制symbols的visibility(见:
https://gcc.gnu.org/wiki/Visibility),那么这个shared library是不可被卸载的,dlclose会是no-op。
一个更复杂的问题是关于C++11引入的function local static variables(又名magic statics)。
比如:
std::mutex& GetMutex(){
static std::mutex m;
return m;
}
//XXX: DO NOT use a function local mutex to protect a global var.
static int connection_count = 0;
void* CreateConnection(...){
std::lock_guard<std::mutex> guard(GetMutex());
++connection_count;
return new DataBaseConnection();
}
void ReleaseConnection(void* p){
std::lock_guard<std::mutex> guard(GetMutex()); //XXX: the mutex object probably is already gone.
--connection_count;
delete (DataBaseConnection*)p;
}
GetMutex()函数里的那个变量m就是一个function local static variable。C++11可以保证该变量的初始化是线程安全的,这也是最简单有效的实现singleton模式的方法。它也可以被用于避免“static initialization order ‘fiasco’ (problem)”。(说到底都怪std::mutex的默认构造函数为什么不是constexpr)
好话说了这么多,下面我得开始说缺点了。在Linux上magic statics先于整个进程里的任何全局变量析构。拿上面的例子来说,如果一个函数用了GetMutex()方法,那么这个函数就不能被任何全局变量的析构函数直接或间接调用。这就意味着如果你是某个函数库的作者,你得挨个挨个的给你的APIs加注释说明哪些不能在析构函数里使用……
再拿前面那个MySQL的例子来说,当`mysql_init_var`调用mysql_library_end函数时,mysql library内部的magic statics变量已经没了,这时候mysql library就像一个已经被拆掉了一半的房子。哪些APIs还能正常工作?God Knows。另一方面,在Windows上情况却又不是这样。在Windows上DLL里的magic statics是跟着DLL里的其它全局变量一起销毁的。以上面的例子,如果你采用隐式动态连接,那么可以肯定的是在`mysql_init_var`析构的时候mysql library还是完整的。然后在主程序析构完之后mysql library开始析构它的magic statics,然后是析构它的全局变量们。这些是写开源软件的人必须面对的挑战。
好吧,听起来很复杂,那么咱们不把代码写成那样行不行?有时候还真不行。举个例子,假设我们要用C/C++写一个python的插件,然后这个C/C++代码需要访问mysql。由于python并没有对C/C++插件提供init/fini这样的hook API,我们还非得把代码写成上面那样用C++全局变量来解决问题。
不过,另一方面也有一个好消息:除了动态库被用dlclose(或LoadLibrary)显式成功卸载的例子外,就算该动态库里的全局变量已经被析构了,这些变量还在内存里,有些还是可以正常访问的。比如,整数类型的变量以及weak_ptr/unique_ptr/shared_ptr等。比如写动态库的人可以把代码写成这样:
static std::shared_ptr<int> env = std::make_shared<int>();
class ResourceHandler {
public:
ResourceHandler() { env_p = env; }
~ResourceHandler() {
if (env_p.expired()) {
std::cout << "This library is already uninitialzed. Skip destructing" << std::endl;
}
//TODO: do the real destruction work
}
private:
std::weak_ptr<int> env_p;
};
//The following two APIs are exported publicly as external symbols
void* CreateResourceHandler() { return new ResourceHandler(); }
void ReleaseResourceHandler(void* handler) { delete reinterpret_cast<ResourceHandler*>(handler); }
--
修改:snnn FROM 107.139.34.*
FROM 107.139.34.*