存档

2010年4月 的存档

深入浅出Win32多线程程序设计之线程通信

2010年4月28日
深入浅出Win32多线程程序设计之线程通信已关闭评论

简介
线程之间通信的两个基本问题是互斥和同步。
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的操作系统资源(指的是广义的"资源",而不是Windows的.res文件,譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
线程互斥是一种特殊的线程同步。
实际上,互斥和同步对应着线程间通信发生的两种情况:
(1)当有多个线程访问共享资源而不使资源被破坏时;
(2)当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。
在WIN32中,同步机制主要有以下几种:
(1)事件(Event);
(2)信号量(semaphore);
(3)互斥量(mutex);
(4)临界区(Critical section)。
全局变量
因为进程中的所有线程均可以访问所有的全局变量,因而全局变量成为Win32多线程通信的最简单方式。例如:

int var; //全局变量
UINT ThreadFunction(LPVOIDpParam)
{
var = 0;
while (var < MaxValue)
{
//线程处理
::InterlockedIncrement(long*) &var);
}
return 0;
}
请看下列程序:
int globalFlag = false;
DWORD WINAPI ThreadFunc(LPVOID n)
{
Sleep(2000);
globalFlag = true;
return 0;
}
int main()
{
HANDLE hThrd;
DWORD threadId;
hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
if (hThrd)
{
printf("Thread launched\n");
CloseHandle(hThrd);
}
while (!globalFlag)
;
printf("exit\n");
}

上述程序中使用全局变量和while循环查询进行线程间同步,实际上,这是一种应该避免的方法,因为:
(1)当主线程必须使自己与ThreadFunc函数的完成运行实现同步时,它并没有使自己进入睡眠状态。由于主线程没有进入睡眠状态,因此操作系统继续为它调度C P U时间,这就要占用其他线程的宝贵时间周期;
(2)当主线程的优先级高于执行ThreadFunc函数的线程时,就会发生 globalFlag永远不能被赋值为true的情况。因为在这种情况下,系统决不会将任何时间片分配给ThreadFunc线程。
事件
事件(Event)是WIN32提供的最灵活的线程间同步方式,事件可以处于激发状态(signaled or true)或未激发状态(unsignal or false)。根据状态变迁方式的不同,事件可分为两类:
(1)手动设置:这种对象只可能用程序手动设置,在需要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。
(2)自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。
创建事件的函数原型为:

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
// SECURITY_ATTRIBUTES结构指针,可为NULL
BOOL bManualReset,
// 手动/自动
// TRUE:在WaitForSingleObject后必须手动调用ResetEvent清除信号
// FALSE:在WaitForSingleObject后,系统自动清除事件信号
BOOL bInitialState, //初始状态
LPCTSTR lpName //事件的名称
);

使用"事件"机制应注意以下事项:
(1)如果跨进程访问事件,必须对事件命名,在对事件命名的时候,要注意不要与系统命名空间中的其它全局命名对象冲突;
(2)事件是否要自动恢复;
(3)事件的初始状态设置。
由于event对象属于内核对象,故进程B可以调用 OpenEvent函数通过对象的名字获得进程A中event对象的句柄,然后将这个句柄用于ResetEvent、SetEvent和 WaitForMultipleObjects等函数中。此法可以实现一个进程的线程控制另一进程中线程的运行,例如:

HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);

 

临界区
定义临界区变量

CRITICAL_SECTION gCriticalSection;

通常情况下,CRITICAL_SECTION结构体应该被定义为全局变量,以便于进程中的所有线程方便地按照变量名来引用该结构体。
初始化临界区

VOID WINAPI InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向程序员定义的CRITICAL_SECTION变量
);

该函数用于对pcs所指的CRITICAL_SECTION结构体进行初始化。该函数只是设置了一些成员变量,它的运行一般不会失败,因此它采用了 VOID类型的返回值。该函数必须在任何线程调用EnterCriticalSection函数之前被调用,如果一个线程试图进入一个未初始化的 CRTICAL_SECTION,那么结果将是很难预计的。
删除临界区

VOID WINAPI DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一个不再需要的CRITICAL_SECTION变量
);

进入临界区

VOID WINAPI EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一个你即将锁定的CRITICAL_SECTION变量
);

离开临界区

VOID WINAPI LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一个你即将离开的CRITICAL_SECTION变量
);

使用临界区编程的一般方法是:

void UpdateData()
{
EnterCriticalSection(&gCriticalSection);
…//do something
LeaveCriticalSection(&gCriticalSection);
}

关于临界区的使用,有下列注意点:
(1)每个共享资源使用一个CRITICAL_SECTION变量;
(2)不要长时间运行关键代码段,当一个关键代码段长时间运行时,其他线程就会进入等待状态,这会降低应用程序的运行性能;
(3)如果需要同时访问多个资源,则可能连续调用EnterCriticalSection;
(4)Critical Section不是OS核心对象,如果进入临界区的线程"挂"了,将无法释放临界资源。这个缺点在Mutex中得到了弥补。
互斥
互斥量的作用是保证每次只能有一个线程获得互斥量而得以继续执行,使用CreateMutex函数创建:

HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
// 安全属性结构指针,可为NULL
BOOL bInitialOwner,
//是否占有该互斥量,TRUE:占有,FALSE:不占有
LPCTSTR lpName
//信号量的名称
);

Mutex是核心对象,可以跨进程访问,下面的代码给出了从另一进程访问命名Mutex的例子:

HANDLE hMutex;
hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"mutexName");
if (hMutex){


else{

}

相关API:

BOOL WINAPI ReleaseMutex(
HANDLE hMutex
);

使用互斥编程的一般方法是:

void UpdateResource()
{
WaitForSingleObject(hMutex,…);
…//do something
ReleaseMutex(hMutex);
}

互斥(mutex)内核对象能够确保线程拥有对单个资源的互斥访问权。互斥对象的行为特性与临界区相同,但是互斥对象属于内核对象,而临界区则属于用户方式对象,因此这导致mutex与Critical Section的如下不同:
(1) 互斥对象的运行速度比关键代码段要慢;
(2) 不同进程中的多个线程能够访问单个互斥对象;
(3) 线程在等待访问资源时可以设定一个超时值。
下图更详细地列出了互斥与临界区的不同:

信号量
信号量是维护0到指定最大值之间的同步对象。信号量状态在其计数大于0时是有信号的,而其计数是0时是无信号的。信号量对象在控制上可以支持有限数量共享资源的访问。
信号量的特点和用途可用下列几句话定义:
(1)如果当前资源的数量大于0,则信号量有效;
(2)如果当前资源数量是0,则信号量无效;
(3)系统决不允许当前资源的数量为负值;
(4)当前资源数量决不能大于最大资源数量。
创建信号量

HANDLE CreateSemaphore (
PSECURITY_ATTRIBUTE psa,
LONG lInitialCount, //开始时可供使用的资源数
LONG lMaximumCount, //最大资源数
PCTSTR pszName);

释放信号量
通过调用 ReleaseSemaphore函数,线程就能够对信标的当前资源数量进行递增,该函数原型为:

BOOL WINAPI ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount, //信号量的当前资源数增加lReleaseCount
LPLONG lpPreviousCount
);

打开信号量
和其他核心对象一样,信号量也可以通过名字跨进程访问,打开信号量的API为:

HANDLE OpenSemaphore (
DWORD fdwAccess,
BOOL bInherithandle,
PCTSTR pszName
);

互锁访问
当必须以原子操作方式来修改单个值时,互锁访问函数是相当有用的。所谓原子访问,是指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源。
请看下列代码:

int globalVar = 0;
DWORD WINAPI ThreadFunc1(LPVOID n)
{
globalVar++;
return 0;
}
DWORD WINAPI ThreadFunc2(LPVOID n)
{
globalVar++;
return 0;
}

运行 ThreadFunc1和ThreadFunc2线程,结果是不可预料的,因为globalVar++并不对应着一条机器指令,我们看看 globalVar++的反汇编代码:

00401038 mov eax,[globalVar (0042d3f0)]
0040103D add eax,1
00401040 mov [globalVar (0042d3f0)],eax

在"mov eax,[globalVar (0042d3f0)]" 指令与"add eax,1" 指令以及"add eax,1" 指令与"mov [globalVar (0042d3f0)],eax"指令之间都可能发生线程切换,使得程序的执行后globalVar的结果不能确定。我们可以使用 InterlockedExchangeAdd函数解决这个问题:

int globalVar = 0;
DWORD WINAPI ThreadFunc1(LPVOID n)
{
InterlockedExchangeAdd(&globalVar,1);
return 0;
}
DWORD WINAPI ThreadFunc2(LPVOID n)
{
InterlockedExchangeAdd(&globalVar,1);
return 0;
}

InterlockedExchangeAdd保证对变量globalVar的访问具有"原子性"。互锁访问的控制速度非常快,调用一个互锁函数的 CPU周期通常小于50,不需要进行用户方式与内核方式的切换(该切换通常需要运行1000个CPU周期)。
互锁访问函数的缺点在于其只能对单一变量进行原子访问,如果要访问的资源比较复杂,仍要使用临界区或互斥。
可等待定时器
可等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象。它们通常用来在某个时间执行某个操作。
创建可等待定时器

HANDLE CreateWaitableTimer(
PSECURITY_ATTRISUTES psa,
BOOL fManualReset,//人工重置或自动重置定时器
PCTSTR pszName);

设置可等待定时器
可等待定时器对象在非激活状态下被创建,程序员应调用 SetWaitableTimer函数来界定定时器在何时被激活:

BOOL SetWaitableTimer(
HANDLE hTimer, //要设置的定时器
const LARGE_INTEGER *pDueTime, //指明定时器第一次激活的时间
LONG lPeriod, //指明此后定时器应该间隔多长时间激活一次
PTIMERAPCROUTINE pfnCompletionRoutine,
PVOID PvArgToCompletionRoutine,
BOOL fResume);

取消可等待定时器

BOOl Cancel WaitableTimer(
HANDLE hTimer //要取消的定时器
);

打开可等待定时器
作为一种内核对象,WaitableTimer也可以被其他进程以名字打开:

HANDLE OpenWaitableTimer (
DWORD fdwAccess,
BOOL bInherithandle,
PCTSTR pszName
);

实例
下面给出的一个程序可能发生死锁现象:

#include <windows.h>
#include <stdio.h>
CRITICAL_SECTION cs1, cs2;
long WINAPI ThreadFn(long);
main()
{
long iThreadID;
InitializeCriticalSection(&cs1);
InitializeCriticalSection(&cs2);
CloseHandle(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFn, NULL, 0,&iThreadID));
while (TRUE)
{
EnterCriticalSection(&cs1);
printf("\n线程1占用临界区1");
EnterCriticalSection(&cs2);
printf("\n线程1占用临界区2");
printf("\n线程1占用两个临界区");
LeaveCriticalSection(&cs2);
LeaveCriticalSection(&cs1);
printf("\n线程1释放两个临界区");
Sleep(20);
};
return (0);
}
long WINAPI ThreadFn(long lParam)
{
while (TRUE)
{
EnterCriticalSection(&cs2);
printf("\n线程2占用临界区2");
EnterCriticalSection(&cs1);
printf("\n线程2占用临界区1");
printf("\n线程2占用两个临界区");
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs2);
printf("\n线程2释放两个临界区");
Sleep(20);
};
}

运行这个程序,在中途一旦发生这样的输出:
线程1占用临界区1
线程2占用临界区2

线程2占用临界区2
线程1占用临界区1

线程1占用临界区2
线程2 占用临界区1

线程2占用临界区1
线程1占用临界区2
程序就"死"掉了,再也运行不下去。因为这样的输出,意味着两个线程相互等待对方释放临界区,也即出现了死锁。
如果我们将线程2的控制函数改为:

long WINAPI ThreadFn(long lParam)
{
while (TRUE)
{
EnterCriticalSection(&cs1);
printf("\n线程2占用临界区1");
EnterCriticalSection(&cs2);
printf("\n线程2占用临界区2");
printf("\n线程2占用两个临界区");
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs2);
printf("\n线程2释放两个临界区");
Sleep(20);
};
}

再次运行程序,死锁被消除,程序不再挡掉。这是因为我们改变了线程2中获得临界区1、2的顺序,消除了线程1、2相互等待资源的可能性。
由此我们得出结论,在使用线程间的同步机制时,要特别留心死锁的发生。

IT技术 , , , , ,

中国穷人到底有多穷?

2010年4月27日
中国穷人到底有多穷?已关闭评论

中国实行新扶贫标准后,中国的贫困人口将由目前 1479万增至4300多万,“开发式扶贫”如何与农村生存保障有效衔接,成为具有“中国特色”的一项尖锐的社会问题。
中国“绝对贫穷”不足786元有1479万人口;“相对贫困”不超过1067元有4300万人口;若是按联合国人均不足1美元(年收入365美元、约合2200—2400中元每一年计算)为贫困,那么中国贫困人口则可能突破1.5亿人口。经过 30年的改革开放,中国贫富悬殊依然巨大。2007年中国平安保险股份有限公司董事长马明哲年薪高达6616万元,而年不足每天1美元(年收入365美元)的人口则超过1亿。
2009年中国调整贫困统计标准。中国政府将取消现行“绝对贫困”人口与“相对贫困”人口的区分,但也没有与国际社会、联合国统计贫困数据相接轨。中国实行新扶贫标准后,中国的贫困人口将由目前1479万增至4300多万,“开发式扶贫”如何与农村生存保障有效衔接,成为具有“中国特色”的一项尖锐的社会问题。

新标准解决老问题?
据中国国务院扶贫办官员确认,在即将到来的2009年,中国政府将启用新的扶贫标准,取消现行“绝对贫困人口”与“相对贫困人口”的区分。至此,实行了近30年的中国贫困人口将由现由1479万增至4300多万人口。所谓贫困标准,是指在一定时间、空间和社会发展阶段条件下,维持人们基本生存所必需消费的物品和服务的最低费用,也被称为中国称之“贫困线”。这个贫困线于改革初期,凡是人均年收入低于该“贫困线”的人口才能被称之为“贫困人口”,才能够享受一定政策保护。
在中国,有两个贫困线,一是绝对贫困线,低于该标准的就是所谓“赤贫人口”;二是相对“贫困线”,指高于绝对贫困线、但收入依然较低的人口,所谓“低收入人口”。据中国国家统计局数据,2008年中国的绝对贫困线为人均年收入786元,“相对贫困线”为人均年收入786—1067元。由于中国早期制订了非常低的贫困标准,使得中国的贫困人口规模看上去较小。但是,随着生活成本的提高,该扶贫标准已显得不合时宜,也遭到国际组织和社会学者的严重质疑。2008年中共十七届三中全会明确,要“推进农村扶贫开发,实行新的扶贫标准”。

09贫困新标准
从2009年始,中国的绝对贫困标准和相对贫困标准将合二为一,将把现行低收入标准作为新的扶贫标准。但即是新标准年收入1067中元(约为150美元),与联合国每天1美元的标准依然差距巨大。在世界银行年度《发展报告》统计公告150多个国家中:年人均国民总收入不足150美元的国家,只有最贫穷排在首位的布隆迪(100美元)、刚果金(120美元)等,而排在第3位的埃塞俄比亚、马拉维都超过了160美元,中国的贫困标准显然非常之低。据资料显示,中国2009年将新扶贫标准在1067元基础上,根据贫困地区的物价指数调整,有望调整为超过1100元。国务院扶贫办统计,截至2007年底,人均年收入低于1067元的农村人口共有4320万人。
中国自1985年起启动政府扶贫计划,并把绝对贫困线确定为人均年收入200元以下,而低收入标准则定为201—399元,约占当时农民年人均收入的一半。随后,根据农村物价指数的变化,贫困线每年都会微调。但这个贫困标准不失为全球之最。
据《中国统计年鉴2008》显示,1985年的中国的绝对贫困线为206元,1986年为 213元,1987年为227元。十年后的2007年,中国的绝对贫困线为785元,1067元则作为低收入群体的标准。据国务院扶贫办数据,中国农村的绝对贫困人口从1978年的2.5亿,下降到2007年底的1479万,占农村总人口的比重从1978年的30.7%下降到2007年的1.6%。与绝对贫困人口的大幅减少不同,20多年来中国贫困线的调整可谓“步履迟缓”。据了解,从1987年到2007年,中国的绝对贫困线提高额不足600元,而中国中央国有企业、中国平安保险股份有限公司董事长马明哲2007年年薪高达6616万元之最,最高与最低收入者相差高达1万多倍。
中国的“贫困标准”低于每天1美元与国际通行“贫困”标准、差距巨大。实行新扶贫标准后,中国的贫困标准依然非常偏低。若按世界银行每天人均1美元为贫困标准计算,那么中国贫困人口可能超过1.5亿左右,占中国总13亿人口的近10%。

因为贫困人口,中国中央财政每年都要拨出专项资金,于是数字当然越小就付出金钱更少。 1986年,以上一年农民人均纯收入低于150元为标准,中国选取了331个县作为“国家级重点贫困县”;1996年经过调整后,在全国确定了592个农民人均纯收入低于400元的县为“国家重点贫困县”。21世纪以来,中国中央财政扶贫资金逐年增加,到2008年中央财政投入每年167亿用于扶贫开发项目。

中国难题:“扶贫”“低保”

实行新的扶贫标准后,“扶贫”与“低保”衔接成为中国大发展的当务之急,这是全球金融危机之后、2008年中国经济“保8”稳定的基石,但这个“基石”显得过于太低、几乎无法“稳定”中国的高速发展。

事实上,扶贫和低保都是为了保障贫困公民的基本生活。目前负责农村低保的民政部和主管扶贫的国务院扶贫办,正在研究、协商如何界定各自的扶助对象,并确保有效衔接。1997年9月,中国决定建立城镇低保制度。但直到十年后的2007年,中国才决定在全国范围建立农村低保制度,当年中央财政投入30亿元,2008年该投入为90亿元。截至2007年底,全国农村低保对象已达3451.9万人。
2007年,中国各级政府对城市低保的投入为275亿元,其中中央财政投入160亿元。这远远高于对农村低保的投入。由于中央对城乡低保投入相差悬殊,也遭到学者和公众的诸多批评。

按照经济发展学理论,既然有了农村低保制度,就不应该再有所谓贫困人口。但是,由于中国政府体制改革严重滞后,“国民待遇”(指“城镇人口”与“农村人口”的同等待遇)成“站起来”的中国人几乎是难以逾越的世纪大鸿沟,福利政策的制定和执行分散在不同政府部门,根源难以协调,这使得“扶贫”与“低保”对象存在众多交叉又冲突重合。执行新的扶贫标准后,如何实现扶贫与低保的衔接,节省财政资源,成为决策者和公众关注的重点。

据联合国2008年最新公的有关中国最新人类发展报告称,1985年,中国城镇居民收入是占总人口60%的农村居民的1.9倍。截至2007年,这一数字扩大至3.3倍,如果考虑到基本公共服务的不平等,甚至可达五至六倍。该报告称,在2006 年,中国最富有10%的城镇家庭可支配收入是最贫穷10%家庭的九倍多。据亚洲发展银行推算,自20世纪90年代初,三分之二亚洲发展中国家的基尼系数都有所增长,该系数被普遍用于衡量社会不平等状态。如果收入完全平均分配,该系数为零;如果所有收入归于一人,该系数则为一。中国的基尼系数在20世纪70 年代曾为0.30左右,而到2005年已约达0.45,而2007、2008年中国的基尼系数会继续走高。
有观点认为“低保”与“扶贫”主要是区域性差异的,前者针对贫困地区和失业人口,后者对象主要以农村贫困户为主。这两项国策制度的衔接,有利于扶贫部门明确具体的扶贫对象。但中国长期以来制定的“贫困”标准太低,让一些长期的贫困地区可能永远也没有发展致富的机会;中国扶贫主要是政府以财政资源,参与项目开发,使有能力的农民跳出贫困怪圈,而低保则主要是对老弱病残等无劳动能力的人直接给予救济,两者瞄准的目标不同,有关部门应明确各自的责任。更有观点认为:2008年末,中国中央政府4万亿、地方政府18万亿举世大投资,将使中国“扶贫”与 “低保”拉开更大的差距,而中国西部一些地方都在“脱贫”“扶贫”中苦苦挣扎,而持续“特殊政策”地区却更加高收入高高在上。
作者: 巩胜利 会员
《国情内参》首席研究员 中国国际战略研究网专家,中国经贸研究会特约研究员,四川大学锦江学院客座教授,财经、社会类评论家。是从事国际、中国问题研究的著名独立学者。

网络转载 ,

Win32环境下动态链接库(DLL)编程原理

2010年4月22日
Win32环境下动态链接库(DLL)编程原理已关闭评论

  比较大应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作。其中可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用。在构造软件系统时,如果将所有模块的源代码都静态编译到整个应用程序EXE文件中,会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间,程序运行时也会消耗较大的内存空间,造成系统资源的浪费;另一个缺点是,在编写大的EXE程序时,在每次修改重建时都必须调整编译所有源代码,增加了编译过程的复杂性,也不利于阶段性的单元测试。   Windows系统平台上提供了一种完全不同的较有效的编程和运行环境,你可以将独立的程序模块创建为较小的DLL(Dynamic Linkable Library)文件,并可对它们单独编译和测试。在运行时,只有当EXE程序确实要调用这些DLL模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了EXE文件的大小和对内存空间的需求,而且使这些DLL模块可以同时被多个应用程序使用。Microsoft Windows自己就将一些主要的系统功能以DLL模块的形式实现。例如IE中的一些基本功能就是由DLL文件实现的,它可以被其它应用程序调用和集成。   一般来说,DLL是一种磁盘文件(通常带有DLL扩展名),它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分。如果与其它DLL之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。DLL模块中包含各种导出函数,用于向外界提供服务。Windows 在加载DLL模块时将进程函数调用与DLL文件的导出函数相匹配。   在Win32环境中,每个进程都复制了自己的读/写全局变量。如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段。DLL模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。   DLL现在越来越容易编写。Win32已经大大简化了其编程模式,并有许多来自AppWizard和MFC类库的支持。   一、导出和导入函数的匹配   DLL文件中包含一个导出函数表。这些导出函数由它们的符号名和称为标识号的整数与外界联系起来。函数表中还包含了DLL中函数的地址。当应用程序加载DLL模块时时,它并不知道调用函数的实际地址,但它知道函数的符号名和标识号。动态链接过程在加载的DLL模块时动态建立一个函数调用与函数地址的对应表。如果重新编译和重建DLL文件,并不需要修改应用程序,除非你改变了导出函数的符号名和参数序列。   简单的DLL文件只为应用程序提供导出函数,比较复杂的DLL文件除了提供导出函数以外,还调用其它DLL文件中的函数。这样,一个特殊的DLL可以既有导入函数,又有导入函数。这并不是一个问题,因为动态链接过程可以处理交叉相关的情况。   在DLL代码中,必须像下面这样明确声明导出函数: __declspec(dllexport) int MyFunction(int n);   但也可以在模块定义(DEF)文件中列出导出函数,不过这样做常常引起更多的麻烦。在应用程序方面,要求像下面这样明确声明相应的输入函数: __declspec(dllimport) int MyFuncition(int n);   仅有导入和导出声明并不能使应用程序内部的函数调用链接到相应的DLL文件上。应用程序的项目必须为链接程序指定所需的输入库(LIB文件)。而且应用程序事实上必须至少包含一个对DLL函数的调用。   二、与DLL模块建立链接   应用程序导入函数与DLL文件中的导出函数进行链接有两种方式:隐式链接和显式链接。所谓的隐式链接是指在应用程序中不需指明DLL文件的实际存储路径,程序员不需关心DLL文件的实际装载。而显式链接与此相反。   采用隐式链接方式,程序员在建立一个DLL文件时,链接程序会自动生成一个与之对应的LIB导入文件。该文件包含了每一个DLL导出函数的符号名和可选的标识号,但是并不含有实际的代码。LIB文件作为DLL的替代文件被编译到应用程序项目中。当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与 LIB文件中导出符号相匹配,这些符号或标识号进入到生成的EXE文件中。LIB文件中也包含了对应的DLL文件名(但不是完全的路径名),链接程序将其存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows根据这些信息发现并加载DLL,然后通过符号名或标识号实现对DLL函数的动态链接。   显式链接方式对于集成化的开发语言(例如VB)比较适合。有了显式链接,程序员就不必再使用导入文件,而是直接调用 Win32 的LoadLibary函数,并指定DLL的路径作为参数。LoadLibary返回HINSTANCE参数,应用程序在调用 GetProcAddress函数时使用这一参数。GetProcAddress函数将符号名或标识号转换为DLL内部的地址。假设有一个导出如下函数的 DLL文件: extern “C” __declspec(dllexport) double SquareRoot(double d);   下面是应用程序对该导出函数的显式链接的例子: typedef double(SQRTPROC)(double); HINSTANCE hInstance; SQRTPROC* pFunction; VERIFY(hInstance=::LoadLibrary(“c:\\winnt\\system32\\mydll.dll”)); VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,”SquareRoot”)); double d=(*pFunction)(81.0);//调用该DLL函数   在隐式链接方式中,所有被应用程序调用的DLL文件都会在应用程序EXE文件加载时被加载在到内存中;但如果采用显式链接方式,程序员可以决定DLL文件何时加载或不加载。显式链接在运行时决定加载哪个DLL文件。例如,可以将一个带有字符串资源的DLL模块以英语加载,而另一个以西班牙语加载。应用程序在用户选择了合适的语种后再加载与之对应的DLL文件。   三、使用符号名链接与标识号链接   在Win16环境中,符号名链接效率较低,所有那时标识号链接是主要的链接方式。在Win32环境中,符号名链接的效率得到了改善。Microsoft现在推荐使用符号名链接。但在MFC库中的DLL版本仍然采用的是标识号链接。一个典型的MFC程序可能会链接到数百个MFC DLL函数上。采用标识号链接的应用程序的EXE文件体相对较小,因为它不必包含导入函数的长字符串符号名。  四、编写DllMain函数   DllMain函数是DLL模块的默认入口点。当Windows加载DLL模块时调用这一函数。系统首先调用全局对象的构造函数,然后调用全局函数DLLMain。DLLMain函数不仅在将DLL链接加载到进程时被调用,在DLL模块与进程分离时(以及其它时候)也被调用。下面是一个框架DLLMain函数的例子。 HINSTANCE g_hInstance; extern “C” int APIENTRY DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved) { if(dwReason==DLL_PROCESS_ATTACH) { TRACE0(“EX22A.DLL Initializing!\n”); //在这里进行初始化 } else if(dwReason=DLL_PROCESS_DETACH) { TRACE0(“EX22A.DLL Terminating!\n”); //在这里进行清除工作 } return 1;//成功 }   如果程序员没有为DLL模块编写一个DLLMain函数,系统会从其它运行库中引入一个不做任何操作的缺省DLLMain函数版本。在单个线程启动和终止时,DLLMain函数也被调用。正如由dwReason参数所表明的那样。   五、模块句柄   进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识。进程自己还有一个HINSTANCE句柄。所有这些模块句柄都只有在特定的进程内部有效,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这个两种类型可以替换使用。进程模块句柄几乎总是等于0x400000,而DLL模块的加载地址的缺省句柄是0x10000000。如果程序同时使用了几个DLL模块,每一个都会有不同的HINSTANCE值。这是因为在创建DLL文件时指定了不同的基地址,或者是因为加载程序对DLL代码进行了重定位。 模块句柄对于加载资源特别重要。Win32 的FindResource函数中带有一个HINSTANCE参数。EXE和DLL都有其自己的资源。如果应用程序需要来自于DLL的资源,就将此参数指定为DLL的模块句柄。如果需要EXE文件中包含的资源,就指定EXE的模块句柄。   但是在使用这些句柄之前存在一个问题,你怎样得到它们呢?如果需要得到EXE模块句柄,调用带有Null参数的Win32函数GetModuleHandle;如果需要DLL模块句柄,就调用以DLL文件名为参数的Win32函数GetModuleHandle。   六、应用程序怎样找到DLL文件   如果应用程序使用LoadLibrary显式链接,那么在这个函数的参数中可以指定DLL文件的完整路径。如果不指定路径,或是进行隐式链接,Windows将遵循下面的搜索顺序来定位DLL:   1. 包含EXE文件的目录,   2. 进程的当前工作目录,   3. Windows系统目录,   4. Windows目录,   5. 列在Path环境变量中的一系列目录。   这里有一个很容易发生错误的陷阱。如果你使用VC++进行项目开发,并且为DLL模块专门创建了一个项目,然后将生成的DLL文件拷贝到系统目录下,从应用程序中调用DLL模块。到目前为止,一切正常。接下来对DLL模块做了一些修改后重新生成了新的DLL文件,但你忘记将新的DLL文件拷贝到系统目录下。下一次当你运行应用程序时,它仍加载了老版本的DLL文件,这可要当心!   七、调试DLL程序   Microsoft 的VC++是开发和测试DLL的有效工具,只需从DLL项目中运行调试程序即可。当你第一次这样操作时,调试程序会向你询问EXE文件的路径。此后每次在调试程序中运行DLL时,调试程序会自动加载该EXE文件。然后该EXE文件用上面的搜索序列发现DLL文件,这意味着你必须设置Path环境变量让其包含DLL文件的磁盘路径,或者也可以将DLL文件拷贝到搜索序列中的目录路径下。

技术开发 , ,