linux动态链接库全局变量共享问题&DLL共享数据段
Linux写时拷贝技术(copy-on-write)
进程间是相互独立的,其实完全可以看成A、B两个进程各自有一份单独的liba.so和libb.so,相应的动态库的代码段和数据段都是各个进程各自有一份的。
然后在这个基础上,由于代码段是不会被修改的,所以操作系统可以采用copy on write的优化技术,让两个进程共享同一份物理内存。这是属于在不改变系统行为的基础上,为了节省内存,的优化技术。
COW技术初窥:
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?
在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
在网上看到还有个细节问题就是,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
COW详述:
现在有一个父进程P1,这是一个主体,那么它是有灵魂也就身体的。现在在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部分,相应的,内核要为这四个部分分配各自的物理块。即:正文段块,数据段块,堆块,栈块。至于如何分配,这是内核去做的事,在此不详述。
1. 现在P1用fork()函数为进程创建一个子进程P2,
内核:
(1)复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。
(2)为这四个部分分配物理块,P2的:正文段->PI的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段->P2自己的数据段块(为其分配对应的块),堆->P2自己的堆块,栈->P2自己的栈块。如下图所示:同左到右大的方向箭头表示复制内容。
2. 写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
3. vfork():这个做法更加火爆,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间,当然了,这种做法就顺水推舟的共享了父进程的物理空间
通过以上的分析,相信大家对进程有个深入的认识,它是怎么一层层体现出自己来的,进程是一个主体,那么它就有灵魂与身体,系统必须为实现它创建相应的实体, 灵魂实体与物理实体。这两者在系统中都有相应的数据结构表示,物理实体更是体现了它的物理意义。以下援引LKD
传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—举例来说,fork()后立即调用exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。这里补充一点:Linux COW与exec没有必然联系
PS:实际上COW技术不仅仅在Linux进程上有应用,其他例如C++的String在有的IDE环境下也支持COW技术,即例如:
string str1 = "hello world";string str2 = str1;
之后执行代码:
str1[1]='q';str2[1]='w';
在开始的两个语句后,str1和str2存放数据的地址是一样的,而在修改内容后,str1的地址发生了变化,而str2的地址还是原来的,这就是C++中的COW技术的应用,不过VS2005似乎已经不支持COW。
第一部分:
linux动态链接库全局变量共享
注意:本文中的大部分是阅读 《程序员的自我修养》 作 者:俞甲子,石凡,潘爱民 的读书笔记。推荐大家看看这本书。
一,动态链接
操作系统将把程序依赖的目标文件全部加载到内存,如果依赖关系满足,则系统开始进行链接。链接与静态链接相似,即进行符号解析、地址重定位。
例如程序program1和program2都依赖于lib.o,而在运行program1的时候,lib.o已经被加载,那么在运行program2的时候,系统不需要加载lib.o,而只是将program2和lib.o进行链接。
这样不仅仅节省内存,还减少了内存物理页面的换入换出,增加CPU缓存命中。
动态链接的另外一个特点是程序运行时可以动态选择加载各种程序模块。这个优点即人们制作程序的插件(Plug-in)。
例如一个公司制定的产品,并制定了接口。其他公司按照这些借口编写符合要求的动态链接库,程序可以动态载入这些开发的模块,程序运行时动态的链接,拓展程序的功能。
动态链接也增加了程序兼容性,比如不同操作系统的库都提供了printf,在该库之上的代码,可以跨不同操作系统。
二,动态链接的实现
动态链接使用的物件,理论上是可以是目标文件的,但是实际上动态连接库与目标文件稍有差别。
动态链接涉及运行时链接,需要操作系统支持,一些存储管理,共享内存、进程线程机制,在动态链接下,也会与静态链接不同。Linux下,ELF动态链接文件称作DSO(动态共享对象),Windows下,一般为DLL。
Linux下常用C语言运行库为glibc,其动态链接库形式版本在/lib目录下的libc.so。程序加载时,动态链接器将所有动态连接库装载到进程地址空间,将程序中未决议符号绑定到相应的动态链接库,进行重定位工作。
由于每次加载需要动态的链接,所以性能有损失,采取延迟绑定(Lazy Binding)可以对其进行优化。
三,一个例子
Program1.c:
#include "Lib.h"
int main(){
foobar(1);
return 0;
}
Program2.c:
#include "Lib.h"
int main(){
foobar(2);
return 0;
}
Lib.h:
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif
lib.c:
#include <stdio.h>
void foobar(int i)
{
printf("Printing from Lib.so %d/n",i);
sleep(-1);
}
使用如下编译 gcc -fPIC -shared -o Lib.so Lib.c
编译链接program1和program2:
gcc -o Program1 Program1.c ./Lib.so
gcc -o Program2 Program2.c ./Lib.so
在静态链接器链接program1和program2的过程中,它必须知道foobar这个函数的性质。如果是静态目标模块中的函数,那么其必须进行地址重定位,如果是动态链接模块中的,则标记为动态链接符号,在装载时进行重定位。
那么静态链接器如何知道该符号是动态链接符号呢?传入的./Lib.so 文件中包含完整的符号信息,静态链接器以及装载时的动态链接器都是通过其中的符号信息获知这些信息的。这样静态链接器将foobar这个函数符号标识为动态链接符号。
开启Program1,使用cat /proc/进程ID/maps 查看其进程映射:
00400000-00401000 r-xp 00000000 ca:02 399066 /root/mylinuxc/Program1
00600000-00601000 r--p 00000000 ca:02 399066 /root/mylinuxc/Program1
00601000-00602000 rw-p 00001000 ca:02 399066 /root/mylinuxc/Program1
7fa557326000-7fa55747a000 r-xp 00000000 ca:02 651529 /lib64/libc-2.11.1.so
7fa55747a000-7fa55767a000 ---p 00154000 ca:02 651529 /lib64/libc-2.11.1.so
7fa55767a000-7fa55767e000 r--p 00154000 ca:02 651529 /lib64/libc-2.11.1.so
7fa55767e000-7fa55767f000 rw-p 00158000 ca:02 651529 /lib64/libc-2.11.1.so
7fa55767f000-7fa557684000 rw-p 00000000 00:00 0
7fa557684000-7fa557685000 r-xp 00000000 ca:02 399064 /root/mylinuxc/Lib.so
7fa557685000-7fa557884000 ---p 00001000 ca:02 399064 /root/mylinuxc/Lib.so
7fa557884000-7fa557885000 r--p 00000000 ca:02 399064 /root/mylinuxc/Lib.so
7fa557885000-7fa557886000 rw-p 00001000 ca:02 399064 /root/mylinuxc/Lib.so
7fa557886000-7fa5578a5000 r-xp 00000000 ca:02 651522 /lib64/ld-2.11.1.so
7fa557a70000-7fa557a73000 rw-p 00000000 00:00 0
7fa557aa2000-7fa557aa4000 rw-p 00000000 00:00 0
7fa557aa4000-7fa557aa5000 r--p 0001e000 ca:02 651522 /lib64/ld-2.11.1.so
7fa557aa5000-7fa557aa6000 rw-p 0001f000 ca:02 651522 /lib64/ld-2.11.1.so
7fa557aa6000-7fa557aa7000 rw-p 00000000 00:00 0
7fff5c82c000-7fff5c841000 rw-p 00000000 00:00 0 [stack]
7fff5c9b2000-7fff5c9b3000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
libc-2.11.1.so是c语言运行库。
ld-2.11.1.so这个共享目标文件其实是Linux下的动态链接器,系统执行program1之前,会将控制权交给动态链接器,它将完成所有动态链接工作,然后把控制权交给program1。
使用readelf -l查看Lib.so:
Elf file type is DYN (Shared object file)
Entry point 0x570
There are 7 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000734 0x0000000000000734 R E 200000
LOAD 0x0000000000000e18 0x0000000000200e18 0x0000000000200e18
0x0000000000000208 0x0000000000000218 RW 200000
DYNAMIC 0x0000000000000e40 0x0000000000200e40 0x0000000000200e40
0x0000000000000190 0x0000000000000190 RW 8
NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
0x0000000000000024 0x0000000000000024 R 4
GNU_EH_FRAME 0x00000000000006e0 0x00000000000006e0 0x00000000000006e0
0x0000000000000014 0x0000000000000014 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 8
GNU_RELRO 0x0000000000000e18 0x0000000000200e18 0x0000000000200e18
0x00000000000001e8 0x00000000000001e8 R 1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
01 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .ctors .dtors .jcr .dynamic .got
发现其加载地址为0x0000000000000000,其实是7fa557684000,此时的值是因为共享库文件的装载地址在链接时不能确定,而是在动态链接器加载时才得以确定的。所以在动态连接库文件中是无效的0x0000000000000000。
早期的系统,采用的是对于某些共享库,指定其装载地址的方法。比如上边的Lib.so,可以制定其装载地址为7fa557684000。这样可能引发地址的冲突。比如另外的Lib2.so也是指定的7fa557684000。这种方式称为静态共享库(Static shared object)。静态共享库除了地址冲突,还会因为预先留有的空间的限制,导致新版本的库的大小必须有制约。
为了解决这个问题,必须保证共享库能够在任意装载时确定的地址装载。
解决这种问题,自然想到可以利用静态链接中的地址重定位方法。自然,这里应该是在装载而不是链接时进行重定位,即装载时重定位。
例如,foobar的地址相对给库文件起始的偏移为0x100,该库文件在装载时的初始地址确定为0x10000000,则foobar的地址装载后会是0x100000100,装载器遍历整个重定位表,对重定位表中记录的有对foobar地址引用的地方,全部重新改写为0x100000100。
这个过程称为 装载时重定位,Windows下称为基址重置(Rebasing)。
但是为了解决这个问题我们想的这个类似静态链接的方法,对于要在进程间进行共享对象的动态库文件,却不适用。因为该方法,在重定位的时候,必须修改代码段中的对foobar的引用地址为新的值,而这个值是进程相关的,一旦一个进程修改为自己用的值后,其他进程就无法使用该共享对象了。
对于上述库文件中的可修改数据部分,因为每个进程都存在它的一个副本,所以可以采用上述办法。
Linux的GCC中的不使用-fPIC而仅仅使用-shared就是产生装载时重定位的代码。
其中的-fPIC的意义能解决装载时重定位将导致指令部分无法进程间共享的问题,其实现的基本思想是把指令中需要进行重定位修改的那部分分离出来,跟数据部分一起,使得每个进程都有一个副本。这种技术即地址无关代码技术。
四,地址无关代码
产生地址无关代码其实对于现在机器,并不麻烦。首先我们看看哪些需要地址重定位,把可以不需要地址重定位的,就用不需要地址重定位的方法实现。那些必须要重定位的,就采用一种将其地址存储到一个数据结构中,而该数据结构放置到数据段,代码段通过该数据结构间接访问该地址的方法。由于数据段每个进程都有一份副本,所以该代码(动态链接库)是可以进程共享的。
模块中的地址引用方式,可以按照是否跨模块分成:模块内和模块外引用;按照不同引用方式可以划分为指令引用和数据访问。分类具体如下:
1,模块内函数调用
2,模块内数据访问
3,模块外函数调用
4,模块外数据访问
例如代码:
static int a;
extern int b;
extern void ext();
void bar()
{
a=1;
b=2;
}
void foo()
{
bar();
ext();
}
bar的反汇编代码:
编译器在编译上述pic.c代码时,并不能确定b、ext是模块内部还是模块外部的。因为extern意味着在别的目标文件,但是有可能别的目标文件和自身产生的目标文件是同一个共享库中的,所以是一个模块的。编译器将所有不确定的当作模块外部函数和变量处理。MSVC编译器提供了__declspec(dllimport)拓展,制定一个符号是模块内部还是模块外部的。
对于类型1,由于他们调用者和被调用者相对位置固定,采取相对地址调用即可。或者基于寄存器的相对调用。因此,对于这种指令,其实不需要重定位。这样产生的汇编代码,只要调用者和被调用者的相对地址不变,则总是有效的。
对于类型2,采用相对寻址也可解决。即对于数据的访问,采取相对访问这个数据的指令的地址,来寻址的方式,由于该数据和访问它的指令的相对地址不变,所以不需要重定位了。当然,目前一般都是相对下一条指令的地址来访问数据。那么如何获取下一条指令的地址呢?编译后的汇编码可以看到,其实程序会先调用__i686.get_pc_thunk.cx函数,该函数将返回地址的值放到ecx寄存器(本质上是通过eip寄存器的值,因为eip即下一条指令地址),然后通过ecx和预先指令中寻址带有的偏移量,即可获取当前数据存在哪了。(其实相对当前指令寻址也是同样道理,只是有点麻烦,相对当前指令需要将之前(也就是当前指令)的eip保存)。
假设加载到0x10000000,那么,a的地址就是(0x10000000+0x454)+0x118c+0x28。(0x10000000+0x454)即下一条指令的地址。0x118c+0x28是a相对于该指令的偏移。
之所以 <__i686.get_pc_thunk.cx> 是 mov (%esp) %ecx是因为在调用之前,调用函数将把下一条指令地址压栈,所以%esp即调用函数的下一条地址。 [e01]
对于类型3,必然需要重定位。ELF的做法,是将其他模块的全局变量的地址存储到数据段里的全局偏移表(Global Offset Table,GOT)中。例如变量b,程序找到GOT,获知b的目标地址,然后再去访问。链接器在装载模块的时候,会将该GOT表进行正确的填充。GOT在数据段,保证了多个进程有自己的副本。GOT自己本身也要是地址无关的,不能因为加载地址不同,而需要对GOT的访问也进行重定位,那样就不能多进程共享了。
GOT本身的地址无关是通过与模块内部数据访问类似的方法:编译的时候确定GOT与当前指令的偏移。那么在指定指令的时候,获取该指令的地址PC,再加上偏移量即可得到GOT的位置。然后再根据变量在GOT中的位置,获取变量的地址。
上述bar()访问b,假设加载到0x10000000,则b的地址在GOT的位置为(0x10000000+0x454+0x118c)+(-8)=0x100015d8(-8的补码是0xfffffff8)。(0x10000000+0x454+0x118c)是GOT表的地址,-8是b的地址在GOT表中的偏移量。
假设0x10000000是当前段加载的地址,而0x454则是call指令的下一条,也就是add指令的地址。此时的ecx即该值,之后ecx被加上0x118c,得到了GOT表的地址。此时的ecx即GOT表地址,+0xfffffff8的位置是存储的b的地址。
使用objdump -h 查看GOT的位置,如果要查看动态加载库的定位项目
b的偏移是000015d8,这个值是相对于模块的,而不是GOT表,因此跟我们使用相对指令的偏移得出的结果地址0x100015d8,减去首址0x10000000得到的结果15d8一致。
对于类型4,类似于类型3的处理方法,采用GOT表。例如调用ext()函数:
call 494 <__i686.get_pc_thunk.cx>
add $0x118c,%ecx
mov 0xffffffffc(%ecx),%eax
call *(%eax)
也是得到PC,然后加上偏移得到GOT中的偏移,最后使用间接调用。
其实ELF采用了一种更为复杂和精巧的方法,因为上述这种方法简单,但是存在性能问题。
这样,对于四种类型,我们对应的采取方法,使得代码达到地址无关:
指令跳转 数据访问
模块内部: 相对跳转和调用 相对地址访问
模块外部: 间接跳转和调用(GOT) 间接访问(GOT)
-fPIC和-fpic区别在于-fpic产生的代码小,执行速度快。但是-fpic在某些平台上会有限制,因为地址无关代码是硬件平台相关的。比如全局符号的数量、代码长度等。-fPIC则没有这种限制。
五,查看是否是PIC的
readelf -d foo.so|grep TEXTREL
如果上述命令有输出,则不是PIC的,否则就是。PIC的动态链接库不会含有任何代码段重定位表,TEXTREL即代码段重定位表。
六,PIE
地址无关技术也可以用在可执行文件上,这种为Position-Independent Executable。使用参数为-fPIE和-fpie。
七,可执行文件中对外部数据的访问
通过上边的描述,我们知道,对于动态链接库,它的符号,如果是对模块内部的数据访问和函数调用,则使用相对地址访问的方式,这样就不需要进行地址重定位了,因为代码中含有获取下一个指令地址的指令,而又有相对下一条指令地址的偏移量,通过指令地址和偏移量访问数据或者进行函数调用。由于不需要重定位,因此多个进程可以共享该动态链接库。对于模块外部的数据访问和函数调用,则采用了GOT表的方法,将需要访问的模块外部数据和函数,使用GOT表做记录,在进行动态加载的时候,改写GOT表中符号的对应地址。而对GOT表本身的访问则采用类似模块内部数据访问的方法,因为GOT表与加载地址的偏移(实现上是采用与指令的相对偏移)可以在编译的时候确定。使得对GOT表的访问具有代码无关性。这样由于进程都有自己的GOT的副本,使得多个进程可以在加载重定位的时候,修改自己的GOT表而不影响别的进程。
对于可执行文件,以external声明的全局变量可能是来自本模块的其他目标文件或者其他模块。可执行文件中,对于模块内的符号引用和模块外的符号引用,由于无法编译时确定,都作为模块外符号处理。
对于可执行文件中访问共享对象文件中的全局变量符号的问题,如果也采用上述的PIC机制,则会如下处理:在生成的代码中,采用相对于GOT表的地址偏移的寻址方式。则访问该全局变量的时候,需要首先获取PC的值,然后加上该偏移获取到GOT表的位置,再加上在GOT表的偏移获取该变量的地址在GOT表中存储的位置,然后获取到该变量的地址(改写GOT表中全局变量的地址是在动态链接库被运行时加载的时候填写的),之后进行访问。
由于可执行文件编译产生的代码,不采用如同上述的PIC机制,即不采用相对下一条指令的地址的偏移来寻找GOT表,进而寻找数据地址的方式,而是依然采用与普通数据一样的方式,即绝对地址访问,因此,可执行文件中的全局变量符号的地址,必须在进行编译链接的时候可以决定出来。而实际上,由于定义在其他模块的全局变量的地址,如果其他模块采用的是动态链接的方式,那么这个地址必然是不能在编译链接的时候决议出来的,而是只有在加载时,获知了模块加载地址,才能通过变量与模块加载地址的偏移获知变量的地址,因此,可执行文件采用了如下机制,使得编译链接时,可以不知道变量的地址,也可以正常进行:在bss段中分配该变量,重定位表中的类型为COPY。
例如:
external int global;
int foo()
{
global=1;
}
int main(){
}
将上述编译链接成为可执行文件,使用objdump -R 查看重定位表,发现global类型为“COPY”,而不是像函数访问一样,是JUMP_SLOT等,而且,其是存放在bss段的,而不是在got表中。
这样,如果加载模块后,必然在加载的模块中(数据段)也有该变量的副本,产生矛盾。实际上,ELF在编译共享库的时候,都将把全局变量当作模块外引用,使用GOT表访问,即使明确知道该变量是自己模块的(例如就在该目标文件中)。这样,如果运行时动态加载的时候,发现可执行文件中也有该变量,则会统一在GOT表中重定位填充为可执行文件bss段中该变量副本的地址。如果在共享库中对该变量进行了初始化,动态装载器还得负责将初始化的值拷贝到可执行文件bss中该变量的副本位置。如果可执行文件中没有该变量,则GOT表中重定位后,指向自己模块内的该变量。这样就意味着对模块内的变量访问,也采用了GOT表。也就是或,对于共享库中的全局对象,无论是否是内部的,还是无法决定是否是内部的,都得作为外部模块访问那样,使用GOT表进行访问。
问题:
共享对象lib.so中的全局变量 G,进程A和B都使用了lib.so,那么A改变G的时候,是否影响进程B中的G?
回答:
不会。因为G其实是存储在bss中的,bss类似数据段,每个进程都有自己的副本。这样看起来,共享库的全局变量与程序内部全局变量没有区别,因为都是数据段(或bss段),都会有自己的副本。如果想通过全局变量进行进程间通信,可以采用"共享数据段"技术,使得不同进程访问同一个全局变量。而对于一个进程,如果想让变量不被多个线程共享,即多个线程拥有自己数据段的副本,可以采用“线程局部存储”技术。
第二部分:
DLL共享数据段
在主文件中,用#pragma data_seg建立一
个新的数据段并定义共享数据,其具体格式为:
#pragma data_seg("shareddata") //名称可以
//自己定义,但必须与下面的一致。
HWND sharedwnd=NULL;//共享数据
#pragma data_seg()
|
|
在Win16环境中,DLL的全局数据对每个载入它的进程来说都是相同的;而在Win32环境中,情况却发生了变化,DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。当进程在载入DLL时,操作系统自动把DLL地址映射到该进程的私有空间,也就是进程的虚拟地址空间,而且也复制该DLL的全局数据的一份拷贝到该进程空间。也就是说每个进程所拥有的相同的DLL的全局数据,它们的名称相同,但其值却并不一定是相同的,而且是互不干涉的。
int i = 0;
char a[32] = "hello world";
#pragma data_seg()
今天和同学在BBS上讨论有关动态链接库中的全局变量的问题。如果某动态库中有一个全局变量,程序a使用的这个动态库,程序b也使用了这个动态库,那么程序a和b中的全局变量是一个吗?也就是说,进程间使用动态库时,共享全局变量吗?答题是:是一个,共享,但写时拷贝,给程序员的感觉是:不共享。谢谢WWF的说明。
看看下面的测试:
testshare.h
1 2 3 4 5 |
|
testshare.c
1 2 3 4 5 6 7 8 9 10 11 |
|
上面的两个程序片断很简单,其中testshare.h中声明了两个函数,testshare.c中实现了这两个函数,而且定义了一个全局变量。接下来,把testshare.c编译成动态库:
1 |
|
下面程序testshare_main1.c,使用动态库libtestshare.so,
1 2 3 4 5 6 7 8 9 10 |
|
用下面的命令编译上面的程序并执行:
1 |
|
执行程序:
1 |
|
执行的时候,我们在两个终端里先后分次执行程序,发现程序都输出
1 2 |
|
显然,后执行的程序并没有受到先执行的程序的影响。由此可见,不同的进程并不共享相同动态库中的全局变量。
另外,如果有两个动态库中有同名的全局变量,在同时使用这两个动态库时,链接器中链接写在前面的动态库中的全局变量。写在前面指的是编译命令中库排列在前的。
:-)看完了想说点什么呢?
“动态链接库中的全局变量,在进程间共享,写时拷贝”10条留言
-
-
不是吧。我觉得和写时复制没关系。写时复制是对共享的数据说的。比如两段代码共享一个数据,其中一段代码要对数据进行修改,这时候要把数据复制一份。
在这儿,数据根本不共享。所以我就得谈不上写时复制。 -
哦。书上的那一段写的挺清楚的。
-
-
共享代码也是写时复制?
-
写时复制的一个应用是:在调试器中实现断点支持。例如:在默认情况下,代码页面在起始时都是只能执行的(即:只读的),然而,如果一个程序员在调试一个程序时设置了一个断点,则调试器必须在代码中加入一条断点指令。它是这样做的:首先将该页面的保护模式改变为PAGE_EXECUTE_READWRITE,然后改变指令流。因为代码页面是所映射的内存区的一部分,所以内存管理器为设置了断点的那个进程创建一份私有拷贝,同时其它进程仍然使用原先未经修改的代码页面。
写时复制是“延迟计算(lazy evaluation)”这一计算技术(evaluation technique)的一个例子,内存管理器广泛地使用了延迟计算的技术。延迟计算使得只有当绝对需要时才执行一个昂贵的操作——如果该操作从来也不需要的话,则它不会浪费任何一点时间。
POSIX子系统利用写时复制来实现fork函数,当一个UNIX应用程序调用fork函数来创建另一个进程时,新进程所做的第一件事是调用exec函数,用一个可执行程序来重新初始化它的地址空间。 在fork中,新进程不是拷贝整个地址空间,而是通过将页面标记为写时复制的方式,与父进程共享这些页面。如果子进程在这些页面中写入数据了,则生成一份进程私有的拷贝。如果没有写操作,则2个进程继续共享页面,不会执行拷贝动作。不管怎么样,内存管理器只拷贝一个进程试图要写入数据的那些页面,而不是整个地址空间。
你好,看了你写的"VC++ DLL编程深入浅出",特别有收获。 只是有个地方我老搞不明白,就是用DLL导出全局变量时,指定了.lib的路径(#pragma comment(lib,"dllTest.lib")),那么.dll的文件的路径呢,我尝试着把.dll文件移到别的地方程序就无法正常运行了,请问.dll在这里怎么指定。
希望您能在百忙中抽空给我解答一下,不胜感激!
一位编程爱好者
回答:
Windows按下列顺序搜索DLL:
(1)当前进程的可执行模块所在的目录;
(2)当前目录;
(3)Windows 系统目录,通过GetSystemDirectory 函数可获得此目录的路径;
(4)Windows 目录,通过GetWindowsDirectory 函数可获得此目录的路径;
(5)PATH 环境变量中列出的目录。
因此,隐式链接时,DLL文件的路径不需要指定也不能指定,系统指定按照1~5的步骤寻找DLL,但是对应的.lib文件却需要指定路径;如果使用Windows API函数LoadLibrary动态加载DLL,则可以指定DLL的路径。
你好,我是一位C++初学者,我在PCONLINE看了教学之后,受益不浅。我想问一下能否在DLL里使用多线程?MSDN上用#using <mscorlib.dll>这个指令之后实现了多线程,不过好象不支持DLL..
请问有什么办法支持制作多线程DLL??能否给一个源码来?
回答:
在DLL中可以处理多线程,WIN32对于多线程的支持是操作系统本身提供的一种能力,并不在于用户编写的是哪一类程序。即便是一个控制台程序,我们都可以使用多线程:
#include <stdio.h>
#include <windows.h>
void ThreadFun(void)
{
while(1)
{
printf( "this is new thread/n" );
Sleep( 1000 );
}
}
int main()
{
DWORD threadID;
CreateThread( NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFun, NULL, 0, &threadID );
while(1)
{
printf( "this is main thread/n" );
Sleep( 1000 );
}
}
观察程序运行的结果为在控制台窗口上交替输出this is main thread、this is new thread。
我们来看下面的一个多线程DLL的例子。
DLL程序提供一个接口函数SendInit,在此接口中启动发送线程SendThreadFunc,在这个线程的对应工作函数中我们使用原始套接字socket发送报文。参考微软出版的经典书籍《Windows核心编程》,我们发现,不宜在DLL被加载的时候(即进程绑定时)启动一个新的线程。
这个线程等待一个CEvent事件(用于线程间通信),应用程序调用DLL中的接口函数SendMsg( InterDataPkt sendData )可以释放此事件。下面是相关的源代码:
(1)发送报文线程入口函数
///////////////////////////////////////////////////////////////////////////
//函数名:SendThreadFunc
//函数功能:发送报文工作线程入口函数,使用UDP协议
////////////////////////////////////////////////////////////////////////////
DWORD WINAPI SendThreadFunc( LPVOID lpvThreadParm )
//提示:对于线程函数应使用WINAPI声明,WINAPI被宏定义为__stdcall
{
/* 创建socket */
sendSock = socket ( AF_INET, SOCK_DGRAM, 0 );
if ( sendSock == INVALID_SOCKET )
{
AfxMessageBox ( "Socket创建失败" );
closesocket ( recvSock );
}
/* 获得目标节点端口与地址 */
struct sockaddr_in desAddr;
desAddr.sin_family=AF_INET;
desAddr.sin_port=htons( DES_RECV_PORT ); //目标节点接收端口
desAddr.sin_addr.s_addr = inet_addr( DES_IP );
/* 发送数据 */
while(1)
{
WaitForSingleObject( hSendEvent, 0xffffffffL );//无限等待事件发生
ResetEvent( hSendEvent );
sendto( sendSock, (char *)sendSockData.data, sendSockData.len, 0, (struct sockaddr*)&desAddr, sizeof(desAddr) );
}
return -1;
}
(2)MFC规则DLL的InitInstance函数
/////////////////////////////////////////////////////////////////////////////
// CMultiThreadDllApp initialization
BOOL CMultiThreadDllApp::InitInstance()
{
if ( !AfxSocketInit() ) //初始化socket
{
AfxMessageBox( IDP_SOCKETS_INIT_FAILED );
return FALSE;
}
return TRUE;
}
(3)启动发送线程
////////////////////////////////////////////////////////////////////////////////
//函数名:SendInit
//函数功能:DLL提供给应用程序调用接口,用于启动发送线程
/////////////////////////////////////////////////////////////////////////////
void SendInit(void)
{
hSendThread = CreateThread( NULL, 1000, SendThreadFunc, this, 1, &uSendThreadID );
}
(4)SendMsg函数
////////////////////////////////////////////////////////////////////////////////
//函数名:SendMsg
//函数功能:DLL提供给应用程序调用接口,用于发送报文
/////////////////////////////////////////////////////////////////////////////
extern "C" void WINAPI SendMsg( InterDataPkt sendData )
{
sendSockData = sendData;
SetEvent( hSendEvent ); //释放发送事件
}
以上程序仅仅是一个简单的例子,其实在许多工程应用中,我们经常看到这样的处理方式。这个DLL对用户而言仅仅使一个简单的接口函数SendMsg,对调用它的应用程序屏蔽了多线程的技术细节。与之类似,MFC提供的CSocket类在底层自己采用了多线程机制,所以使我们免去了对多线程的使用。
您好,看了您的DLL文章,发现导出函数可以直接用_declspec(dllexport)声明或在.def文件中定义,变量的导出也一样。我想知道类是否也可以在.def文件中导出?您的文章中只讲了在类前添加_declspec(dllexport)导出类的方法。请您指教!
回答:
一般我们不采用.def文件导出类,但是这并不意味着类不能用.def文件导出类。
使用Depends查看连载2的"导出类"例程生成的DLL,我们发现其导出了如图21的众多"怪"symbol,这些symbol都是经过编译器处理的。因此,为了以.def文件导出类,我们必须把这些"怪"symbol全部导出,实在是不划算啊!所以对于类,我们最好直接以_declspec(dllexport)导出。
图1 导出类时导出的symbol
您好,看了您的DLL文章,知道怎么创建DLL了,但是面对一个具体的工程,我还是不知道究竟应该把什么做成DLL?您能给一些这方面的经验吗?
回答:
DLL一般用于软件模块中较固定、较通用的可以被复用的模块,这里有一个非常好的例子,就是豪杰超级解霸。梁肇新大师把处理视频和音频的算法模块专门做成了两个DLL,供超级解霸的用户界面GUI程序调用,实在是DLL设计的模范教程。所谓"万变不离其宗",超级解霸的界面再cool,用到的还是那几个DLL!具体请参考《编程高手箴言》一书。
您好,您的DLL文章讲的都是Windows的,请问Linux操作系统上可以制作DLL吗?如果能,和Windows有什么不一样?谢谢!
回答:
在Linux操作系统中,也可以采用动态链接技术进行软件设计,但与Windows下DLL的创建和调用方式有些不同。
Linux操作系统中的共享对象技术(Shared Object)与Windows里的DLL相对应,但名称不一样,其共享对象文件以.so作为后缀。与Linux共享对象技术相关的一些函数如下:
(1)打开共享对象,函数原型:
//打开名为filename共享对象,并返回操作句柄;
void *dlopen (const char *filename, int flag);
(2)取函数地址,函数原型:
//获得接口函数地址
void *dlsym(void *handle, char *symbol);
(3)关闭共享对象,函数原型:
//关闭指定句柄的共享对象
int dlclose (void *handle);
//共享对象操作函数执行失败时,返回出错信息
const char *dlerror(void);
从这里我们分明看到Windows API――LoadLibrary、FreeLibrary和GetProcAddress的影子!又一个"万变不离其宗"!
本系列文章的连载暂时告一段落,您可以继续给笔者发送email(mailto:[email protected])讨论DLL的编程问题。对于文中的错误和纰漏,也热诚欢迎您指正。
c++共享库问题
1创建和使用静态库创建一个静态库是相当简单的。通常使用 ar 程序把一些目标文件(.o)组合在一起,
成为一个单独的库,然后运行 ranlib,以给库加入一些索引信息。
2创建和使用共享库
特殊的编译和连接选项
-D_REENTRANT 使得预处理器符号 _REENTRANT 被定义,这个符号**一些宏特性。
-fPIC 选项产生位置独立的代码。由于库是在运行的时候被调入,因此这个
选项是必需的,因为在编译的时候,装入内存的地址还不知道。如果
不使用这个选项,库文件可能不会正确运行。
-shared 选项告诉编译器产生共享库代码。
-Wl,-soname -Wl 告诉编译器将后面的参数传递到连接器。而 -soname 指定了
共享库的 soname。
# 可以把库文件拷贝到 /etc/ld.so.conf 中列举出的任何目录中,并以
root 身份运行 ldconfig;或者
# 运行 export LD_LIBRARY_PATH='pwd',它把当前路径加到库搜索路径中去。
1.7.9 使用高级共享库特性
1. ldd 工具
ldd 用来显示执行文件需要哪些共享库,共享库装载管理器在哪里找到了需要的共享库.
2. soname
共享库的一个非常重要的,也是非常难的概念是 soname——简写共享目标名(short for shared object name)。这是一个为共享库(.so)文件而内嵌在控制数据中的名字。如前面提到的,每一个程序都有一个需要使用的库的清单。这个清单的内容是一系列库的 soname,如同 ldd 显示的那样,共享库装载器必须找到这个清单。
soname 的关键功能是它提供了兼容性的标准。当要升级系统中的一个库时,并且新库的 soname 和老的库的 soname 一样,用旧库连接生成的程序,使用新的库依然能正常运行。这个特性使得在 Linux 下,升级使用共享库的程序和定位错误变得十分容易。
在 Linux 中,应用程序通过使用 soname,来指定所希望库的版本。库作者也可以通过保留或者改变 soname 来声明,哪些版本是相互兼容的,这使得程序员摆脱了共享库版本冲突问题的困扰。
查看/usr/local/lib 目录,分析 MiniGUI 的共享库文件之间的关系
3. 共享库装载器
当程序被调用的时候,Linux 共享库装载器(也被称为动态连接器)也自动被调用。它的作用是保证程序所需要的所有适当版本的库都被调入内存。共享库装载器名字是 ld.so 或者是 ld-linux.so,这取决于 Linux libc 的版本,它必须使用一点外部交互,才能完成自己的工作。然而它接受在环境变量和配置文件中的配置信息。
文件 /etc/ld.so.conf 定义了标准系统库的路径。共享库装载器把它作为搜索路径。为了改变这个设置,必须以 root 身份运行 ldconfig 工具。这将更新 /etc/ls.so.cache 文件,这个文件其实是装载器内部使用的文件之一。
可以使用许多环境变量控制共享库装载器的操作(表1-4+)。
表 1-4+ 共享库装载器环境变量
变量 含义
LD_AOUT_LIBRARY_PATH 除了不使用 a.out 二进制格式外,与 LD_LIBRARY_PATH 相同。
LD_AOUT_PRELOAD 除了不使用 a.out 二进制格式外,与 LD_PRELOAD 相同。
LD_KEEPDIR 只适用于 a.out 库;忽略由它们指定的目录。
LD_LIBRARY_PATH 将其他目录加入库搜索路径。它的内容应该是由冒号
分隔的目录列表,与可执行文件的 PATH 变量具有相同的格式。
如果调用设置用户 ID 或者进程 ID 的程序,该变量被忽略。
LD_NOWARN 只适用于 a.out 库;当改变版本号是,发出警告信息。
LD_PRELOAD 首先装入用户定义的库,使得它们有机会覆盖或者重新定义标准库。
使用空格分开多个入口。对于设置用户 ID 或者进程 ID 的程序,
只有被标记过的库才被首先装入。在 /etc/ld.so.perload 中指定
了全局版本号,该文件不遵守这个限制。
4. 使用 dlopen
另外一个强大的库函数是 dlopen()。该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。比如 Apache Web 服务器利用这个函数在运行过程中加载模块,这为它提供了额外的能力。一个配置文件控制了加载模块的过程。这种机制使得在系统中添加或者删除一个模块时,都不需要重新编译了。
可以在自己的程序中使用 dlopen()。dlopen() 在 dlfcn.h 中定义,并在 dl 库中实现。它需要两个参数:一个文件名和一个标志。文件名可以是我们学习过的库中的 soname。标志指明是否立刻计算库的依赖性。如果设置为 RTLD_NOW 的话,则立刻计算;如果设置的是 RTLD_LAZY,则在需要的时候才计算。另外,可以指定 RTLD_GLOBAL,它使得那些在以后才加载的库可以获得其中的符号。
当库被装入后,可以把 dlopen() 返回的句柄作为给 dlsym() 的第一个参数,以获得符号在库中的地址。使用这个地址,就可以获得库中特定函数的指针,并且调用装载库中的相应函数。
恩 ,是写时复制的。之前在某论坛上见人讨论过。