linux动态链接库全局变量共享问题&DLL共享数据段
Linux写时拷贝技术(copy-on-write)进程间是相互独立的,其实完全可以看成A、B两个进程各自有一份单独的liba.so和libb.so,相应的动态库的代码段和数据段都是各个进程各自有一份的。然后在这个基础上,由于代码段是不会被修改的,所以操作系统可以采用copy on write的优化技术,让两个进程共享同一份物理内存。这是属于在不改变系统行为的基础上,为了节省内存,的优化技术。CO
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动态链接库全局变量共享
from: http://blog.csdn.net/iterzebra/article/details/6255270
注意:本文中的大部分是阅读 《程序员的自我修养》 作 者:俞甲子,石凡,潘爱民 的读书笔记。推荐大家看看这本书。
一,动态链接
操作系统将把程序依赖的目标文件全部加载到内存,如果依赖关系满足,则系统开始进行链接。链接与静态链接相似,即进行符号解析、地址重定位。
例如程序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() |
|
仅定义一个数据段还不能达到共享数据的目的,还要告诉编译器该段的属性,有两种方法可以实现该目的(其效果是相同的),一种方法是在.DEF文件中加入如下语句: SETCTIONS shareddata READ WRITE SHARED 另一种方法是在项目设置链接选项(Project Setting --〉Link)中加入如下语句: /SECTION:shareddata,rws
第一点:什么是共享数据段?为什么要用共享数据段??它有什么用途??
在Win16环境中,DLL的全局数据对每个载入它的进程来说都是相同的;而在Win32环境中,情况却发生了变化,DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。当进程在载入DLL时,操作系统自动把DLL地址映射到该进程的私有空间,也就是进程的虚拟地址空间,而且也复制该DLL的全局数据的一份拷贝到该进程空间。也就是说每个进程所拥有的相同的DLL的全局数据,它们的名称相同,但其值却并不一定是相同的,而且是互不干涉的。
因此,在Win32环境下要想在多个进程中共享数据,就必须进行必要的设置。在访问同一个Dll的各进程之间共享存储器是通过存储器映射文件技术实现的。也可以把这些需要共享的数据分离出来,放置在一个独立的数据段里,并把该段的属性设置为共享。必须给这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中。
#pragma data_seg预处理指令用于设置共享数据段。例如:
#pragma data_seg("SharedDataName") HHOOK hHook=NULL; //必须在定义的同时进行初始化!!!!#pragma data_seg()
在#pragma data_seg("SharedDataName")和#pragma data_seg()之间的所有变量将被访问该Dll的所有进程看到和共享。再加上一条指令#pragma comment(linker,"/section:.SharedDataName,rws"),[注意:数据节的名称is case sensitive]那么这个数据节中的数据可以在所有DLL的实例之间共享。所有对这些数据的操作都针对同一个实例的,而不是在每个进程的地址空间中都有一份。
当进程隐式或显式调用一个动态库里的函数时,系统都要把这个动态库映射到这个进程的虚拟地址空间里(以下简称"地址空间")。这使得DLL成为进程的一部分,以这个进程的身份执行,使用这个进程的堆栈。(这项技术又叫code Injection技术,被广泛地应用在了病毒、黑客领域!呵呵^_^)
第二点:在具体使用共享数据段时需要注意的一些问题!
Win32 DLLs are mapped into the address space of the calling process.By default, each process using a DLL has its own instance of all the DLLs global and static variables. (注意:即使是全局变量和静态变量也都不是共享的!) If your DLL needs to share data with other instances of it loaded by other applications, you can use either of the following approaches:
· Create named data sections using thedata_seg pragma.
· Use memory mapped files. See the Win32 documentation aboutmemory mapped files.
Here is an example of using the data_seg pragma:
#pragma data_seg (".myseg")
int i = 0;
char a[32] = "hello world";
#pragma data_seg()
data_seg can be used to create a new named section (.myseg in this example). The most typical usage is to call the data segment .shared for clarity. You then must specify the correct sharing attributes for this new named data section in your .def file or with the linker option/SECTION:.MYSEC,RWS. (这个编译参数既可以使用pragma指令来指定,也可以在VC的IDE中指定!)
There are restrictions to consider before using a shared data segment:
· Any variables in a shared data segmentmust be statically initialized. In the above example, i is initialized to 0 and a is 32 characters initialized to hello world.
· All shared variables are placed in the compiled DLL in the specified data segment. Very large arrays can result in very large DLLs. This is true of all initialized global variables.
· Never store process-specific information in a shared data segment. Most Win32 data structures or values (such as HANDLEs) are really valid only within the context of a single process.
· Each process gets its own address space. It is very important that pointers are never stored in a variable contained in a shared data segment. A pointer might be perfectly valid in one application but not in another.
· It is possible that the DLL itself could get loaded at a different address in the virtual address spaces of each process. It is not safe to have pointers to functions in the DLL or to other shared variables.
动态链接库中的全局变量,在进程间共享,写时拷贝
Posted inLinux/Unix,编程10 条留言
今天和同学在BBS上讨论有关动态链接库中的全局变量的问题。如果某动态库中有一个全局变量,程序a使用的这个动态库,程序b也使用了这个动态库,那么程序a和b中的全局变量是一个吗?也就是说,进程间使用动态库时,共享全局变量吗?答题是:是一个,共享,但写时拷贝,给程序员的感觉是:不共享。谢谢WWF的说明。
看看下面的测试:
testshare.h
| |
testshare.c
| |
上面的两个程序片断很简单,其中testshare.h中声明了两个函数,testshare.c中实现了这两个函数,而且定义了一个全局变量。接下来,把testshare.c编译成动态库:
| |
下面程序testshare_main1.c,使用动态库libtestshare.so,
| |
用下面的命令编译上面的程序并执行:
| |
执行程序:
| |
执行的时候,我们在两个终端里先后分次执行程序,发现程序都输出
| |
显然,后执行的程序并没有受到先执行的程序的影响。由此可见,不同的进程并不共享相同动态库中的全局变量。
另外,如果有两个动态库中有同名的全局变量,在同时使用这两个动态库时,链接器中链接写在前面的动态库中的全局变量。写在前面指的是编译命令中库排列在前的。
November 28, 2010
:-)看完了想说点什么呢?
“动态链接库中的全局变量,在进程间共享,写时拷贝”10条留言
-
wwf:2010/11/28 at 8:26 PM
恩 ,是写时复制的。之前在某论坛上见人讨论过。
-
wildpointer:2010/11/28 at 9:03 PM
不是吧。我觉得和写时复制没关系。写时复制是对共享的数据说的。比如两段代码共享一个数据,其中一段代码要对数据进行修改,这时候要把数据复制一份。
在这儿,数据根本不共享。所以我就得谈不上写时复制。-
wwf:2010/11/28 at 10:03 PM
动态库的设计目的不就是共享么?动态库应该是在加载时就将其全局数据的读写属性改为不可写,修改时要在进程中copy一份访问的全局数据,然后该进程的访问就访问自己的一份copy了吧。这样才使多个进程的对动态库的修改和访问彼此不互相影响吧。
-
wildpointer:2010/11/28 at 10:24 PM
是共享,为了共享代码。加载时,全局数据并没有共享。
当使用动态链接库的程序加载时,加载器调用动态链接器,去重定位用到的共享库的代码和数据。但与其他进程无关。 -
wildpointer:2010/11/28 at 10:38 PM
找一段文字看:《程序员的自我修养-链接,装载与库》第198页 Q&A,里面讲了些。你在学校吧?到我FTP上去下载,在ftp://ftp.wildpointer.net/courseWare/course/这个文件夹里。
-
wwf:2010/11/29 at 10:42 AM
这个我看过…
-
-
wildpointer:2010/12/23 at 12:28 PM
你是对的,写时拷贝。
-
-
-
wildpointer:2010/11/29 at 10:51 AM
哦。书上的那一段写的挺清楚的。
-
-
vfdff:2013/02/24 at 8:08 PM
共享代码也是写时复制?
-
vfdff:2013/02/24 at 8:35 PM
写时复制的一个应用是:在调试器中实现断点支持。例如:在默认情况下,代码页面在起始时都是只能执行的(即:只读的),然而,如果一个程序员在调试一个程序时设置了一个断点,则调试器必须在代码中加入一条断点指令。它是这样做的:首先将该页面的保护模式改变为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 ); //释放发送事件
}
更多推荐
所有评论(0)