多进程引用的动态链接库中的全局变量问题现有liba.so中定义一个全局变量char buf;
libb.so 中的函数print会将buf进行输出。
进程A和进程B使用-lb进行编译链接,并在进程A和B中分别对buf进行初始化,如strcpy(buf, "A"); strcpy(buf, "B");
进程A和进程B在初始化后分别通过dlopen的方式加载liba.so并调用其中的print函数。
输出结果是A中输出A,B中输出B(当然A在初始化buf后sleep一段时间再调用print函数的,而B初始化后直接调用print,这样保证如果只有一份buf则两者输出结果肯定是后初始化的那个值)。
这是否能确定不同进程加载的动态链接库中的全局变量分别有一份拷贝,即使共享库在内存中只有一份,但是全局变量还是会有多份的,共享库共享的只是代码部分?
对这种应用方式不太确定。不知是否是这样,测试程序看来是没问题。
参考地址:
http://www.linux
forum.net/forum/showflat.php?Cat=&Board=linuxK&Number=659336&page=&view=&sb=&o=&vc=1
http://wenda.tianya.cn/wenda/thread?tid=2b1380592c693106原帖由 THEBEST 于 2009-11-28 18:08 发表 http://bbs2.chinaunix.net/images/common/back.gif
现有liba.so中定义一个全局变量char buf;
libb.so 中的函数print会将buf进行输出。
进程A和进程B使用-lb进行编译链接,并在进程A和B中分别对buf进行初始化,如strcpy(buf, "A"); strcpy(buf, "B");
...
简单的说,你的结论是对的。
虽然我没有看过加载器的代码,但我看过内核动态加载module的代码,它们的原理是一样的,所以我推测加载器会是这样做的:
1. 将代码段以SHARED模式mmap代码段,于是所有application共享一份代码段
2.以PRIVATE模式mmap数据段,于是当有application试图修改数据段时,就会在它自己的地址空间内生成一份拷贝
LZ可以试验将这个全局变量初始化,然后A进程起来后先打印,然后修改,修改完毕后再起B进程http://www.faminorson.com,再打印。B进程打印的值应该任然是初始化的那个值。我想这个测试跟我A修改再sleep再启B测试的效果是一样的。
那就是动态库中的全局变量在不同的执行程序中是独立的。
还有一个问题是为什么libb.so中可以使用liba.so中的定义的全局变量但却不能使用a.c中定义的全局变量。a.c 直接编译成执行程序。动态库中使用的符号会去该应用加载的所有动态库中查找但却不会在可执行程序本身里面查找?进程间是相互独立的,其实完全可以看成A、B两个进程各自有一份单独的liba.so和libb.so,相应的动态库的代码段和数据段都是各个进程各自有一份的。
然后在这个基础上,由于代码段是不会被修改的,所以操作系统可以采用copy on write的优化技术http://www.torxmg.com/,让两个进程共享同一份物理内存。这是属于在不改变系统行为的基础上,为了节省内存,的优化技术。原帖由 THEBEST 于 2009-11-29 01:43 发表http://bbs2.chinaunix.net/images/common/back.gif
我想这个测试跟我A修改再sleep再启B测试的效果是一样的。
那就是动态库中的全局变量在不同的执行程序中是独立的。
还有一个问题是为什么libb.so中可以使用liba.so中的定义的全局变量但却不能使用a.c中定义 ...
>>还有一个问题是为什么libb.so中可以使用liba.so中的定义的全局变量但却不能使用a.c中定义
这是可以的,只不过在链接你的a.c的时候需要一些额外的选项。不同的操作系统是不同的,我记得Linux上是-Wl,--export-dynamic
为什么动态库中的全局变量是可以用的?
之前在linux下做过一个测试:
写一个so,该so中有一个全局变量。so中的代码在运行时会修改该全局变量的值。然后,有多个程序都需要该so,而且这些应用程序都启动了。此时,很显然so只被加载了一份,那么,当这么多程序在运行调用该so时,该so中的全局变量的值会被覆盖来覆盖去么?
答案是不会。这是测试的答案。
现在知道原理了,尽管这是windows via C/C++中解释的windows的做法,但是我想linux也是这么类似处理的。
windows 使用memory map来加载exe和dll。当一个exe/dll有多个instance要启动时,实际在windows paging file(包括RAM和swap文件)中,exe和牵涉到的dll,只有一份。这样可以节省内存使用,也可以提高性能。
也就是说,如果是exe,虽然每个instance都有自己独立的地址空间,但是地址空间映射到storage的时候,他们映射的都是同样的地方。这样就带来问题了:exe/dll中的全局变量和静态变量怎么办?每个instance都有可能会修改这些变量。
windows 的做法是,该存放全局变量和静态变量的page,设定copy on write protect attribute。所以,当任何一个线程尝试修改这些page中的内容时,windows负责分配一个新的page出来,然后修改该线程的地址空间,将 这个新分配的page的地址设置上去,从此以后,该线程修改这个全局变量或是静态变量,操作的就是这个新分配的page了。这样,多个实例就不会出现全局 变量或静态变量互相覆盖的问题了。
参考windows via C/C++ P593,有详细说明
如果某动态库中有一个全局变量,程序a使用的这个动态库,程序b也使用了这个动态库,那么程序a和b中的全局变量是一个吗?也就是说,进程间使用动态库时,共享全局变量吗?答题是:是一个,共享,但写时拷贝,给程序员的感觉是:不共享。
写时复制的一个应用是:在调试器中实现断点支持。例如:在默认情况下,代码页面在起始时都是只能执行的(即:只读的),然而,如果一个程序员在调试一个程序时设置了一个断点,则调试器必须在代码中加入一条断点指令。它是这样做的:首先将该页面的保护模式改变为PAGE_EXECUTE_READWRITE,然后改变指令流。因为代码页面是所映射的内存区的一部分,所以内存管理器为设置了断点的那个进程创建一份私有拷贝,同时其它进程仍然使用原先未经修改的代码页面。
写时复制是“延迟计算(lazy evaluation)”这一计算技术(evaluation technique)的一个例子,内存管理器广泛地使用了延迟计算的技术。延迟计算使得只有当绝对需要时才执行一个昂贵的操作——如果该操作从来也不需要的话,则它不会浪费任何一点时间。
POSIX子系统利用写时复制来实现fork函数,当一个UNIX应用程序调用fork函数来创建另一个进程时,新进程所做的第一件事是调用exec函数,用一个可执行程序来重新初始化它的地址空间。 在fork中,新进程不是拷贝整个地址空间,而是通过将页面标记为写时复制的方式,与父进程共享这些页面。如果子进程在这些页面中写入数据了,则生成一份进程私有的拷贝。如果没有写操作,则2个进程继续共享页面,不会执行拷贝动作。不管怎么样,内存管理器只拷贝一个进程试图要写入数据的那些页面,而不是整个地址空间。
主程序存取动态链接库里的全局变量
动态链接库是否能存取主程序的全局变量,主程序是否能存取动态链接库里定义的全局变量都是可以通过链接指令改变此行为。
例如动态库里定义全局变量int i, 在主程序里申明extern int i。 则,主程序存取的就是动态库里定义的i。在所有的UNIX平台上这是默认的行为(注意:实际定义i的 模块的点o文件,必须同时链接到动态库和主程序上,否则链接报错)。
如果不想让主程序能存取动态库里的全局变量,则在链接动态连接库的时候,给gcc传入-Wl,-Bsymbolic即可。
动态连接库存取主程序里定义的全局变量
在linux上,链接主程序的时候,使用参数-Wl,--export-dynamic
在AIX上,使用'deferred imports' 并且 enable 'runtime linking'.
在Linux中动态库的确给程序带来了良好的扩充性,并减少了内存的使用量,但这是有代价的。例如:
1 2 3 4 5 6 |
|
我们知道printf是在glibc中定义的,如果不适用动态库,而是将glibc静态链接到进程中的话,那么printf函数的地址在编译时就是已知的了,使用很简单的依据地址转移,就可以进行函数调用。
可是如果采用动态库的话,在程序编译阶段,编译器就无法得知printf的函数地址,因为动态库加载的内存地址时随机的。那么对于动态库的情况,针对printf是如何寻址的呢?
在程序运行时,当调用printf的时候,程序会将处理权交给linker,由其负责在执行文件以及其连接的动态库中查找printf的函数地址。由于linker不知道printf具体是在哪个动态库,所以它将在整个执行文件和动态库的范围内查找。更糟糕的是在C++程序中,符号的命名是类名+函数名,这导致在做字符串比较时,往往直到字符串的结尾才能获得结果。
这也就导致了在进程启动过程中,符号查找占据了一大部分时间。在Linux的KDE进程启动中,符号查找甚至占据了进程启动80%的时间。
因此就针对上述的情况,有以下优化解决方案:
1、减少导出符号的数量
在动态库编译和生成时,默认所有的函数和全局变量都是导出的。而实际上有很多函数只是动态库内部使用,通过去掉那些动态库中不必要的导出符号,从而减少动态库在做链接时所查找的符号数量,可以加快动态库链接的速度。
可以使用ld的ld --retain-symbols-file --version-script两个选项实现。写一个导出符号文件,如symbol 指定你只导出的函数,如 func1。使用 ld 的--retain-symbols-file 参数可以在 static section 里取消 func1 以外的所有函数。这时你用 readelf 看编译好后的 .so 文件 static section里没有了,使用 nm 看 .so文件它无法查出导出函数。但这并不完全。因为在 dynamic section 里还是会看到所有符号被导出。如果想在 dynsym section 里也不让他导出的话,需要再编写一个 script文件,指定 global 与 local 在 global 中指定你要导出的函数,简单的格式如下:
VERSION{
VER_1.0{
global: 导出函数名;
local: *;
};
}
再在 ld 时用 --version-script 选项来 load 你的文件。都完事后再使用 readelf 观察static 与dynamic section 发现只导出了你指定的函数名即符号。
例:
ld -shared --retain-symbols-file 符号文件 --version-script 脚本文件 -o 动态库文件。so filename
2、减少符号的长度
3、使用prelink
在这里另外在提一个问题,很有趣的东西。
gcc -fvisibility=hidden 只在链接时传入的.c文件起作用,对.o文件不其作用;
比如test.c test1.c,使用以下命令:
gcc -shared -fvisibility=hidden -otest.so test.c test1.c
和命令
gcc -c test.c test1.c
gcc -shared -fvisibility=hidden -otest.so test.o test1.o
生成的test.so中的对应可见性是不一样的,使用“readelf -s test.so”查看发现:
第一个达到预期目的,即将两个.c文件中的functions设为HIDDEN,
而第2个则不行,-fvisibility=hidden不起作用;
再用gcc -shared -fvisibility=hidden -o test.so test.o test1.c
生成的so,则可发现test1.c中的函数为HIDDEN,但test.o中的函数仍为DEFAULT;