全部博文(237)
分类: C/C++
2011-01-24 12:11:42
从表面看起来清除栈的概念还是很容易理解的,使用起来也是比较方便。市面上关于Symbian的大部分书籍都对Symbian清除栈的使用有详细的介绍。可是,到现在,很少有资料详细涉及Symbian清除栈工作原理。我们接触最多的资料一般是《Symbian OS Explained Effective C++ Programming for Smartphones》这本书,但是这本书似乎也是给人一种戛然而止的感觉,使我们只能局限于Symbian应用程序的 开发,却不能深入了解Symbian操作系统本身。于是,我想利用业余时间和更多的人一起探讨一下Symbian清除栈的工作原理。当然,也不仅限于此, 后续我还会写更多的文章和大家一起探讨Symbian的核心工作原理,甚至一起对Symbian的内核进行分析。我希望能够吸引更多的人参与 Symbian的学习和研究过程,来共同壮大Symbian开发阵营。
下面我们来探讨Symbian清除栈的工作原理,这是我写的第一篇关于Symbian操作系统工作原理的文章,所以还是有一些表述上的问题,希望大家多多给出意见。另外,也希望读者有一定的Symbian和C++基础,否则还是会影响阅读的。
清除栈实际上是一种半自动的内存回收机制,Symbian为了达到这个目的,做了很多工作,甚至付出了不少代价。与之相关的有清除栈本身的框架、TRAP宏及Leave机制。本文先从Symbian清除栈框架介绍,如果阅读过《Symbian OS Explained Effective C++ Programming for Smartphones》这本书的话,可以直接从本文图1以下的位置开始阅读。
1.1.1. 清除栈的框架:
清除栈保存了在发生异常退出时会被销毁的对象的指针,而这些对象是由TRAP宏来标定为不同的异常退出等级,即TRAP宏是可以嵌套的,每一级嵌套的TRAP宏之内如果发生异常退出,则只有该TRAP宏内推入清除栈的对象才会被销毁,下面的代码说明了这个问题:
CServer2* NewServerL(const TDesC& aServerName)
{
// Install the active scheduler;
CActiveScheduler* scheduler = new(ELeave) CActiveScheduler;
CleanupStack::PushL(scheduler);
CActiveScheduler::Install(scheduler);
CSmallServServer* server = CSmallServServer::NewLC();
User::LeaveIfError(User::RenameThread(aServerName));
RProcess::Rendezvous(KErrNone);
TRAPD(r, server->StartL(aServerName);
if (r != KErrNone)
{
//…
}
CleanupStack::Pop(2, scheduler);
return static_cast(server);
}
TInt ThreadStart()
{
__UHEAP_MARK;
TInt err = KErrNone;
// set cleanup stack manually
CTrapCleanup* cleanup = CTrapCleanup::New();
CServer2* server = NULL;
if (cleanup)
{
TRAP(err, server = NewServerL(KServer));
}
else
{
err = KErrNoMemory;
}
if (err == KErrNone)
{
// start the active Scheduler
CActiveScheduler::Start();
User::InfoPrint(_L("after"));
}
delete server;
delete CActiveScheduler::Current();
delete cleanup;
__UHEAP_MARKEND;
return err;
}
在 NewServerL函数中有TRAP宏存在,如果该宏的中StartL函数发生异常退出,则NewServerL已经推入清除栈的scheduler 和server并不受到任何影响,但如果NewServerL函数中User::LeaveIfError产生异常退出,则已经推入清除栈的 scheduler和server将由清除栈自动析构。
那么清除栈到底如何实现这些功能呢?我们先来看看清除栈的创建过程,在一般的GUI 应用程序中,我们通常不关心清除栈的创建,可以直接使用,这是因为 GUI的框架已经为我们创建好了。但有时候我们需要手动创建清除栈,比如创建一个新的非GUI进程,并且这个进程中的主线程必须用到清除栈。因 此,Symbian提供了CTrapCleanup类用于清除栈的初始化。
我们继续从上面代码中来看看清除栈是如何初始化的,可以看出上面这段代码是一个单独的进程启动过程,其中有这样一段代码。
CTrapCleanup* cleanup = CTrapCleanup::New();
…
delete cleanup;
经典的清除栈框架对这段代码的描述是这样,当创建CTrapCleanup类对象的时候,CTrapCleanup::New()中发生以下事件:
1.线程当前的异常处理程序被保存起来。
2.在CTrapCleanup对象中创建一个名为iHandler的TCleanTrapHandler类对象(它持有一个包含实际清除栈实现代码的CCleanup对象)。
3.调用User;;SetTrapHandler(),将TCleanupTrapHanlder对象作为线程中新的异常处理程序。
当 调用CleanupStack::PushL()或CleanupStack::Pop()时,这些静态函数会调用 User::TrapHandler()来获取已经安装TCleanupTrapHandler对象,从而可以访问CCleanup对象(见图 1)。正如前面所说,CCleanup对象是实现清除栈代码的核心类,它真正的负责CleanupStack类中静态函数的实现。
现在我们来看CCleanup及其他一些辅助类如何实现清除栈的功能,以CleanupStack类中的三个推入函数为例来逐渐揭开清除栈的真相,这三个函数在Symbian应用开发过程中是再熟悉不过的了,分别是:
IMPORT_C static void PushL(TCleanupItem anItem);
IMPORT_C static void PushL(TAny* aPtr);
IMPORT_C static void PushL(CBase* aPtr);
这三个函数将分别调用CCleanup对象中的以下三个函数:
EXPORT_C void CCleanup::PushL(TCleanupItem anItem)
{
…// check consistency
iNext->Set(anItem);
iNext++;
if (iNext + 1 >= iTop)
{
…// reallocate memory to cleanup stack
}
}
EXPORT_C void CCleanup::PushL(TAny* aPtr)
{
PushL(TCleanupItem(User::Free, aPtr);
}
EXPORT_C void CCleanup::PushL(CBase* anObject)
{
PushL(TCleanupItem(TCleanupOperation(doDelete), anObject);
}
可 以看到在CCleanup对象中也有类似的三个PushL重载函数,我们先来看看CCleanup::PushL(TCleanupItem anItem)函数,因为CCleanup::PushL(TAny* aPtr)和CCleanup::PushL(CBase* anObject)在内部实际上也是在调用它。
首先,该函数内部的iNext和iTop是用来控制清除栈指针的,这和数据结构中栈的处理是一样的,我们来看看e32base.h中iNext和iTop的定义:
//Pointer to the top of the cleanup stack.
TCleanupStackItem* iTop;
//Pointer to the next availaible slot in the cleanup stack.
TCleanupStackItem* iNext;
请 注意注释部分,iNext原来就是指向清除栈的下一个空闲槽,该空闲槽的类定义是TCleanupStackItem,且该类对象的集合组成清除栈。那么 CCleanup::PushL(TCleanupItem anItem)函数就是在清除栈中将所传过来的对象放入空闲槽(Set(anItem))并且“推入”清除栈(iNext++),即让iNext指向清除 栈下一个空闲槽。
现在我们已经知道在Push过程中清除栈是如何被控制的,但是用户所传过来的对象是如何保存的呢,这就需要 TCleanupItem类,该类是CCleanup::PushL(TCleanupItem anItem) 函数的参数类型,并且被放入清除栈的空闲槽(Set(anItem)),我们继续从e32base.h寻找TCleanupItem类的定义,很幸运,发 现它是这样定义的:
typedef void (*TCleanupOperation)(TAny*);
class TCleanupItem
{
public:
inline TCleanupItem(TCleanupOperation anOperation);
inline TCleanupItem(TCleanupOperation anOperation,TAny* aPtr);
private:
TCleanupOperation iOperation;
TAny* iPtr;
friend class TCleanupStackItem;
};
原来TCleanupItem将用户要推入清除栈的对象及其销毁处理的方法分别放入iPtr和iOperation函数指针中,在发生异常退出或是CleanupStack::PopAndDestroy就可以将所指向的对象“就地处理”了(后面还会详细介绍)。
说 到这里,对清除栈的Push机制,我们就有了一个比较完整的了解,再来看CCleanup::PushL(TAny* aPtr)就比较容易理解了。该函数将User::Free作为对象销毁的处理方法和对象的指针一起放入TCleanupItem对象并推入清除栈的空闲 槽,在发生异常退出或是CleanupStack::PopAndDestroy时就可以调用User::Free来释放TAny*指向对象的内存空间 了。
同理,CCleanup::PushL(CBase* anObject)函数也是这样处理的,该函数将doDelete函数指针放入TCleanupItem对象并推入清除栈的空闲槽,在发生异常退出或是 CleanupStack::PopAndDestroy时就可以调用doDelete来释放CBase*指向对象的内存空间了。那么doDelete又 是如何处理的呢?有些读者可能已经猜出十之八九。不过,为了表示清楚,还是列出来:
LOCAL_C void doDelete(CBase* aPtr)
{
delete aPtr;
}
原来就是这样简单,因为CBase的析构函数是虚函数,从CBase继承下来的C类,只要简单的delete就可以!
我们再看看CleanupStack::PopAndDestroy()或异常退出时是如何删除清除栈对象的,iNext会调用成员函数Cleanup(),该函数会执行下面一行代码:
(*iOperation)(iPtr);
其中iOperation的定义为
typedef void (*TCleanupOperation)(TAny*);
TCleanupOperation iOperation;
可以看出,iOperation指向的函数被调用,比如说,对于CBase继承下来的C类,就会调用刚讲过的doDelete,对于TAny*就会调用User::Free()来直接释放内存,以此类推。
1.1.2. TRAP宏
这是Symbian SDK中关于TRAP宏的定义:
TRAP(_r,_s){ \
TInt& __rref = _r; \
__rref = 0; \
{ TRAP_INSTRUMENTATION_START; } \
try { \
__WIN32SEHTRAP \
TTrapHandler* ____t = User::MarkCleanupStack(); \
_s; \
User::UnMarkCleanupStack(____t); \
{ TRAP_INSTRUMENTATION_NOLEAVE; } \
__WIN32SEHUNTRAP \
} \
catch (XLeaveException& l) \
{ \
__rref = l.GetReason(); \
{ TRAP_INSTRUMENTATION_LEAVE(__rref); } \
} \
catch (...) \
{ \
User::Invariant(); \
} \
{ TRAP_INSTRUMENTATION_END; } \
}
这段代码看起来似乎并不重要,但就是这段看似不起眼的代码决定了Symbian整个清除栈及其异常退出的框架。
先来看Symbian是如何实现TRAP嵌套与清除栈挂钩的:
TTrapHandler* ____t = User::MarkCleanupStack(); \
_s; \
User::UnMarkCleanupStack(____t); \
这里User::MarkCleanupStack()实际上还是求助于CCleanup对象的代码实现,它调用CCleanup::NextLevel()函数。
EXPORT_C void CCleanup::NextLevel()
{
…
iNext->MarkLevel();
iNext++;
…
}
看 起来似乎有点让人费解,怎么又在操作iNext?其实,MarkLevel()函数会将iNext指向的下一个清除栈空闲槽 TCleanupStackItem对象设定为一个标记,该标记将不再会放入任何用户推入对象,仅仅作为标记,然后iNext指向再下一个清除栈空闲槽, 于是新的用户推入对象只会放入该标记之上
同样,User::UnMarkCleanupStack()实际上也是求助于CCleanup 对象的代码实现,它调用 CCleanup::PreviousLevel()函数,这个函数就是将iNext减一,位置回退一位并将标志所占空闲槽改为可以推入对象的空闲槽,如 下所示:
EXPORT_C void CCleanup::PreviousLevel()
{
…
--iNext;
if (!iNext->IsLevelMarker())
{
Panic(ENotEmpty);
}
…
}
这 里有人就要问了,为什么iNext需要判断是否为标记?而且如果不是,竟然会将整个程序Panic!那是什么原因造成这么严重的错误?其实,这就是防止 Symbian开发新手经常犯的错误,即只Push对象进入清除栈却没有及时的将其Pop出清除栈。因此,TRAP宏执行到这里时就会检查清除栈该标记以 上部分是否还有未清除的对象,如果有,就说明有对象忘Pop了,于是整个程序便Panic。需要提醒的是,实际清除栈的代码远比上面描述的要复杂,其中还 有许多问题需要考虑,在此就不一一描述了。
我们再来看Symbian的异常退出(Leave),异常退出机制也是清除栈实现的重要组成部分,经常使用的异常退出函数如下:
IMPORT_C static void Leave(TInt aReason);
IMPORT_C static void LeaveNoMemory();
IMPORT_C static TInt LeaveIfError(TInt aReason);
IMPORT_C static TAny* LeaveIfNull(TAny* aPtr);
这些函数都实现了如下代码:
TTrapHandler* t = GetTrapHandler();
if (t)
t->Leave(aReason);
throw XLeaveException(aReason);
TTrapHandler对象的Leave函数最终又是求助于CCleanup对象的代码,CCleanup则是这样实现的,即清除该TRAP宏中所有已经推入的用户对象:
while(!(--iNext)->IsLevelMarker())
{
iNext->Cleanup();
}
可以看到,如果iNext指向的不是标记,则循环进行对象的清除工作。
到此为止,似乎清除栈的相关内容我们已经介绍完了,但还有一些问题需要解释,我们看到在异常退出的那些函数里,有这样一行代码:
throw XLeaveException(aReason);
而在TRAP宏里有这样的catch模块:
catch (XLeaveException& l) \
{ \
__rref = l.GetReason(); \
{ TRAP_INSTRUMENTATION_LEAVE(__rref); } \
}
原 来Symbian在实现TRAP和异常退出时,还是使用的标准C++异常机制。那么就有人要问了,为什么不让Symbian应用开发人员使用呢?这可能 有历史遗留方面的原因,但有一个理由很明显,如果让Symbian应用开发人员使用标准C++异常机制,一旦发生标准C++异常,谁来清除清除栈内的对象 呢?显然,不能让开发人员来维护这件事情,而使用TRAP宏就可以在发生异常时强制清除所需要清除的对象。所以,Symbian为清除栈真是费尽了心机 啊。
chinaunix网友2011-03-09 11:57:19
很好的, 收藏了 推荐一个博客,提供很多免费软件编程电子书下载: http://free-ebooks.appspot.com