【清理栈是干什么的?】
程序一般提供两种错误处理机制,通过返回值判断和异常处理。通过返回值判断是程序正常执行流程中,对错误的处理方式;而异常处理是程序执行过程出现异常时,处理错误的方式。清理栈是Symbian下的异常处理机制,结合TRAP/Leave,保证程序出现异常时,已经申请的资源得以释放。
编程者如果认为某个函数的执行过程可能发生异常(调用到的代码可能调用User::Leave,或者自己编写的代码可能调用User::Leave),需要对这个函数进行TRAP,同时保证这个函数中申请的资源都被正确的放入清理栈。当程序发生异常时(调用User::Leave),清理栈中的资源在离开TRAP范围之前,会得以释放。
其实好的编程习惯是,任何时候你都应该把申请的资源正确的放入清理栈,即使你的代码中没有处理TRAP/Leave。因为上层的函数可能处理了TRAP/Leave,如果资源都被正确的放入了清理栈,上层函数处理TRAP/Leave后继续执行,可以保证没有资源泄漏。如果你申请的资源没有放入清理栈,上层函数处理TRAP/Leave后继续执行,就已经有了资源泄漏了。
Symbian有这样的编程规范,把可能Leave的函数以L或LC结尾命名,但是这并不是强制的。你不能假设没有以L或LC结尾命名的函数,就不会Leave。
【如何使用清理栈?】
清理栈的使用本身很简单,但是为了保持本文的完整性,我们在这里还是给出样例。
首先,新建的线程默认是没有清理栈的,如果你需要使用清理栈,需要建立清理栈,如下。CTrapCleanup::New函数中会自动把线程的当前清理栈设为最近New的清理栈,需要被管理的资源都被放在最近创建的清理栈中。如果你在下一层的函数中再次调用CTrapCleanup::New创建清理栈,线程的当前清理栈会被设为新建的清理栈,新建的清理栈中会保存旧的清理栈的指针,新建的清理栈析构时,会把线程清理栈恢复为旧的清理栈。
GLDEF_C TInt E32Main()
{
// Create cleanup stack
__UHEAP_MARK;
CTrapCleanup* cleanup = CTrapCleanup::New();
TRAPD(mainError, DoStartL());
delete cleanup;
__UHEAP_MARKEND;
return KErrNone;
}
下面是使用清理栈的代码,新建的CActiveScheduler被放入清理栈,然后调用了可能Leave的MainL函数。如果MainL中发生了Leave,则err不会为KErrNone,然后调用User::Leave(err)。发生Leave后,最后一句CleanupStack::PopAndDestroy(scheduler)就不会被调用到了,程序会进行调用栈的回滚,直到找到上一级的TRAP,在回滚的过程中,scheduler会被清理栈释放。如果MainL没有发生Leave,最后一句CleanupStack::PopAndDestroy(scheduler);会被调用到,scheduler会被从清理栈中弹出并且释放。
CActiveScheduler* scheduler = new (ELeave) CActiveScheduler();
CleanupStack::PushL(scheduler);
CActiveScheduler::Install(scheduler);
TRAPD(err, MainL());
if (err != KErrNone)
{
User::Leave(err);
}
CleanupStack::PopAndDestroy(scheduler);
程序发生Leave后,如果在调用栈回滚过程中,一直都没有找到TRAP,最后线程会crash。
【清理栈是怎样运作的?】
Kernal Package中的文件kernel/eka/euser/cbase/ub_cln.cpp包含了清理栈用户态的实现代码。下图是Symbian清理栈用户态相关实现类的结构。用户一般通过CleanupStack提供的静态函数进行操作,但是真正功能主要由线程的TTrapHandler* iTrapHandler;提供。每个线程都有一个当前TrapHandler指针,保存在DThread成员变量TTrapHandler* iTrapHandler;(或者用户态的线程私有数据,取决于编译时的宏定义)。
(以上图片摘自博客 http://blog.sina.com.cn/zixieqiangwei)
我们先来看看CTrapCleanup::New的实现,代码如下(文件kernel/eka/euser/cbase/ub_cln.cpp中)。这个函数中除了新建CTrapCleanup外,还新建了CCleanup::New,并且把CCleanup保存到了iHandler的成员变量中,最后把新建的iHandler保存到了县城私有数据,同时在成员变量iOldHandler中保存了旧的handler。CCleanup负责保存需要释放的资源指针,并在必要的时候释放。需要把资源指针放入CCleanup时,可以首先从线程私有数据中拿到TrapHandler,接着从其成员得到CCleanup的指针,就可以调用函数了。CCleanup初始分配了KCleanupInitialSlots=8个资源项的数组,一般来说足够了,除非你的调用嵌套非常深。需要注意的是,CCleanup并不是直接保存资源指针,而是以TCleanupStackItem的形式保存,TCleanupStackItem中包括了资源释放的函数指针,这样可以以任意方式释放资源。
EXPORT_C CTrapCleanup *CTrapCleanup::New()
{
CTrapCleanup *pT=new CTrapCleanup;
if (pT!=NULL)
{
CCleanup *pC=CCleanup::New();
if (pC!=NULL)
{
pT->iHandler.iCleanup=pC;
pT->iOldHandler=User::SetTrapHandler(&pT->iHandler);
}
else
{
delete pT;
pT=NULL;
}
}
return(pT);
}
接着我们来看看发生Leave时的动作,代码在文件kernel/eka/euser/us_trp.cpp中。 实现代码分了使用用户态线程私有数据保存TrapHandler,和使用内核线程对象成员变量保存TrapHandler的两种情况,为了分析简单,我们只考虑使用内核线程对象成员变量保存的情况,代码简化如下。与Exec::LeaveStart对应的内核处理函数是ExecHandler::LeaveStart,在文件kernel/eka/kernel/scodeseg.cpp中,并没有复杂的操作,仅仅是设置线程正在Leave的标志位,然后返回TrapHandler。接着调用TrapHandler::Leave,这是关键过程,对应实现代码在文件kernel/eka/euser/cbase/ub_cln.cpp的TCleanupTrapHandler::Leave函数中,直接调用iCleanup->PopAndDestroyAll();释放了所有资源(这里的iCleanup就是前面CTrapCleanup::New时创建的CCleanup对象)。释放了资源后,下面的代码接着throw了XLeaveException,并且呆了aReason,然后会被TRAP中的catch抓住。
EXPORT_C void User::Leave(TInt aReason)
{
TTrapHandler* pH = Exec::LeaveStart();
if (pH)
pH->Leave(aReason); // causes things on the cleanup stack to be cleaned up
throw XLeaveException(aReason);
}
【是否所有的异常都可以被TRAP?】
答案:否。
Symbian的TRAP/Leave是基于C++ try/catch实现的。查看TRAP的源码(如下),我们可以发现,TRAP宏定义中只是catch了XLeaveException,对于其他的exception调用了User::Invariant。User::Invariant最终调用了Panic,最终会调用TheCurrentThread->Die(EExitPanic,aReason,aCategory);,如果线程是主线程,程序就退出了。
#define 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只能catch到Leave产生的exception,不能catch其他exception。
【Panic是什么?】
Panic也是一种异常,它是系统代码认为已经发生了无法挽回的错误,必须让线程、进程甚至系统crash。它与TRAP/Leave基本上毫无关系,Panic的代码执行路径与TRAP不同,无法被TRAP到。
【还有其他异常么?】
答案:有。
例如,我们都知道,任何系统都可能发生除0错误,这也是异常。这些异常也是TRAP无法catch的。对于这一类的异常,每个线程都有exception handler,保存在成员变量DThread的成员变量TExceptionHandler iExceptionHandler;中。发生此类异常后,系统会判断当前线程能否处理,如果不能处理则会crash线程。线程默认的exception handler是NULL,你可以写自己的exception handler,然后设置到线程数据中去。可以被线程的exception handler处理的异常如下:
enum TExcType
{
EExcGeneral=0, /// EExcIntegerDivideByZero=1, ///
EExcSingleStep=2, ///
EExcBreakPoint=3, /// EExcIntegerOverflow=4, ///
EExcBoundsCheck=5, ///
EExcInvalidOpCode=6, ///
EExcDoubleFault=7, /// EExcStackFault=8, ///
EExcAccessViolation=9, ///
EExcPrivInstruction=10, ///
EExcAlignment=11, ///
EExcPageFault=12, ///
EExcFloatDenormal=13, ///
EExcFloatDivideByZero=14, ///
EExcFloatInexactResult=15, ///
EExcFloatInvalidOperation=16, ///
EExcFloatOverflow=17, ///
EExcFloatStackCheck=18, ///
EExcFloatUnderflow=19, ///
EExcAbort=20, ///
EExcKill=21, ///
EExcUserInterrupt=22, ///
EExcDataAbort=23, ///
EExcCodeAbort=24, ///
EExcMaxNumber=25, ///
EExcInvalidVector=26, ///
};
Symbian的TRAP/Leave机制是利用try/catch实现的简易的异常处理机制,基本的逻辑就是保存资源指针,异常的时候释放,并且产生可以被抓住的XLeaveException。为什么不直接使用try/catch?或许是因为早期时候Symbian并不支持C++ try/catch,毕竟Symbian出来的时候C++还没有标准化呢。关于Symbian下try/catch的实现,那又是一部分新的内容了,如果你有兴趣详解,希望能够分享。