作为一个被Symbian开发折磨过的人,当得知Symbian开源时,忍不住马上跑到developer.symbian.org上瞧一瞧,看看自己具体是怎样被折磨的。看了一段时间之后,想想还是把有些东西记录下来,为了加深理解同时也为了能够与人交流探讨。
我们首先从Symbian OS API调用过程说起。如果你只想了解一下Symbian OS API调用的大概过程,那么你可以看 Symbian OS Internals 第五章 Kernel_Services 的 Flow of Execution in an Executive Call 这一小节。如果你想跟着Symbian OS源码看其具体过程,请继续往下看。
首先,请不要混淆OS API和User Library
OS API是OS对外暴露的最基本的接口,数目不多,但是是最基本的必须的接口集合。例如,Exec::SessionSend()、Exec::ChunkBase()等。
User Library一般是在OS API基础上进行封装,比较丰富,使用方便,便于编程者调用。例如,RFile、RThread和RProcess等都属于User Library,它们都是基于OS API进行了封装。
Symbian OS没有文件操作/窗口管理等OS API,只有文件操作/窗口管理的User Library
每个OS都会提供非常多的OS API,大体可以分为内存操作API、进程/线程创建及管理API、进程/线程同步API、进程/线程通信API等等。
需要注意的是Symbian OS下并没有诸如文件操作OS API,窗口管理OS API等。下图是 Symbian OS 的结构,Symbian OS 属于hybird 结构。只有kernal、driver等在内核层,其他的系统服务,如文件服务,窗口服务,网络服务等都是在用户层以Server process的形式提供。诸如RFile、RWindowGroup等User Library的实现主要用到的是进程间通信的OS API。程序进行文件/窗口操作时,RFile/RWindowGroup会把请求参数封装成一定格式后,通过调用进程间通信的OS API,把参数放到内核中。file server/window server程序同样调用进程间通信的OS API,从内核中获得请求,然后处理,并通过进程间通信返回处理结果。
(注:图片摘自Symbian OS Internals第一章 )
Symbian OS API调用的大体过程
1. 程序调用User Library函数或者直接调用某些OS API
2. User Library代码会对用户输入的参数做一定处理和封装,然后调用OS API
3. OS API的调用主要包括设置参数到CPU寄存器,然后执行swi(软中断)ARM指令 (用户态 )
==================================================================================
4. swi指令会触发软中断,CPU切换到内核模式(ARM的Supervisor mode,stack同时也切换),并调用软中断的中断服务函数(内核态 )
5. 软中断服务函数首先会检查当前调用是否为fast OS API调用,如果是,首先关闭所有中断,然后从NThread.iFastExecTable中找到对应的入口并调用,最后返回到用户态
6. 如果软件中服务函数发现是slow OS API 调用,那么首先会进行线程同步锁的检查,符合条件时调用NThread.iSlowExecTable中对应的函数,不符合条件则等待。调用完NThread.iSlowExecTable中的函数后,还会进行线程同步锁状态的更新,也可能重新调度线程。操作完成后返回用户态。
==================================================================================
7. 软中断处理完成后,CPU切换回用户状态,用户程序可以处理API调用结果(如果本次OS API调用返回) (用户态 )
Symbian OS API调用的关键点
【Symbian OS API代码是怎样组织的?】
Symbian OS API的接口(用户态与内核态的接口)代码主要由一个API定义文件execs.txt和API定义处理脚本genexec.pl组成。genexec.pl会根据API定义文件生成API 调用码,用户态调用OS API的函数,并声明内核态的OS API实现函数。有了这些文件,User Library就可以直接封装生成的用户态接口函数,提供方便使用的库;而内核需要实现OS API实现函数列表中的所有函数。
在Symbian代码的Kernel Package(FCL/sf/os/kernelhwsrv)中,文件 kernel/eka/kernel/execs.txt 列出了Symbian OS所有的API。你可以查看该文件顶端的注释,注释解释了下面文件内容是怎样定义Symbian OS API的各项属性的。我们举一个例子来说明。
fast {
name = TrapHandler
return = TTrapHandler*
}
以上代码定义了一个fast Symbian OS API,API的名字是TrapHandler,返回值的类型是TTrapHandler*。我们大概可以推断出,这个API是返回线程当前的trap handler。当函数出现Leave时,TrapHandler会进行异常处理。关于TrapHandler,同样也有下面的OS API定义。
fast {
name = SetTrapHandler
return = TTrapHandler*
arg1 = TTrapHandler*
}
在相同目录下,文件genexec.pl是一个perl脚本文件,它的作用是把上面的execs.txt作为输入,然后输出以下三个文件。该脚本文件的参数请查看文件头中的注释,如果要弄清楚具体处理过程,请仔细研究脚本文件。
- enum.h 定义了各个OS API的调用码,swi产生API调用软中断时,OS API调用码是必须参数,软中断处理函数根据调用码找到对应的OS API函数地址。下面是截取的enum.h文件中的一段,第一个枚举定义是fast OS API调用码的定义,第二个枚举定义是slow OS API调用码的定义。
-
enum TFastExecDispatch
{
EFastExecWaitForAnyRequest = 0,
EFastExecHeap = 1,
EFastExecHeapSwitch = 2,
EFastExecPushTrapFrame = 3,
EFastExecPopTrapFrame = 4,
EFastExecActiveScheduler = 5,
EFastExecSetActiveScheduler = 6,
…
}; -
enum TExecDispatch
{
EExecObjectNext = 0,
EExecChunkBase = 1,
EExecChunkSize = 2,
EExecChunkMaxSize = 3,
EExecHandleAttributes = 4,
EExecTickCount = 5,
…
};
-
- user.h 声明并定义了用户态调用OS API的函数。下面是从文件中截取的一段,第一部分是函数声明,第二部分是函数定义。
-
class Exec
{
public:
static void WaitForAnyRequest();
static RAllocator* Heap();
static RAllocator* HeapSwitch(RAllocator*);
static TTrapHandler* PushTrapFrame(TTrap*);
static TTrap* PopTrapFrame();
static CActiveScheduler* ActiveScheduler();
static void SetActiveScheduler(CActiveScheduler*);
...
} - 用户态OS API调用函数定义
-
__EXECDECL__ void Exec::WaitForAnyRequest()
{
FAST_EXEC0(EFastExecWaitForAnyRequest);
}
__EXECDECL__ RAllocator* Exec::Heap()
{
FAST_EXEC0(EFastExecHeap);
} - 以上代码中用到了一些宏,例如FAST_EXEC0, FAST_EXEC1等,这些宏定义在文件kernel/eka/include/u32exec.h中,以下是两个示例。
#define FAST_EXEC0(n) __DISPATCH((n)|EXECUTIVE_FAST)
#define FAST_EXEC1(n) __DISPATCH((n)|EXECUTIVE_FAST) -
#define FAST_EXEC0(n) __DISPATCH((n)|EXECUTIVE_FAST)
#define FAST_EXEC1(n) __DISPATCH((n)|EXECUTIVE_FAST)#define __DISPATCH(n)
asm("swi %a0" : : "i" (n)); /
__JUMP(,lr);通过以上代码,我们可以看出,OS API的用户接口函数其实都很简单,基本都是用统一的宏来实现,最主要的代码是swi中断。
-
- kernel.h 声明了内核态对应OS API实现函数。下面是从文件截取的部分,第一部分是内核实现函数的声明,
-
class ExecHandler {
public:
static RAllocator* Heap();
static RAllocator* HeapSwitch(RAllocator*);
static TTrapHandler* PushTrapFrame(TTrap*);
static TTrap* PopTrapFrame();
static CActiveScheduler* ActiveScheduler();
static void SetActiveScheduler(CActiveScheduler*);
...
}
-
FAST_EXEC_BEGIN
DECLARE_WORD(FAST_EXEC_COUNT)
DECLARE_FUNC(ExecHandler::Heap)
DECLARE_FUNC(ExecHandler::HeapSwitch)
...
SLOW_EXEC_END
-
#define DECLARE_FUNC(f) TLinAddr(&f),
#define DECLARE_FAST_EXEC_INVALID DECLARE_FUNC(InvalidFastExec)
#define SLOW_EXEC_BEGIN /
GLDEF_D const TUint32 EpocSlowExecTable[] = /
{
#define SLOW_EXEC_END /
0 /
};
- 从以上代码我们可以看出,genexec.pl会生成内核态的OS API实现函数数组,数组的第一项是API的总个数,后面各项为OS API实现函数地址,软中断处理OS API请求时,会从数组中找到对应的处理函数地址。
-
【为什么要在User mode和supervisor mode之间切换?】
稍微了解OS的都知道,OS API调用过程中由用户态切换到内核态最主要的原因,是内核态可以访问更多的资源。很多数据例如进程信息只有在内核才能访问,诸如此类的受保护信息都存储在受保护的内存区域,用户态是没有权限访问的。通过这种机制可以保护重要的信息,防止OS重要信息被病毒或者写得很烂的程序修改。
Symbian OS下内核数据的保护依赖ARM提供的硬件保护机制。ARM硬件保护机制中最主要的是内存保护机制,这个依赖于MMU。MMU会对内存分区域进行管理,每个区域有对应的访问权限: read/write? 另外还会对内存区域分不同的域,某些域可以自由访问,而某些域只能在特权模式下访问。关于MMU管理内存的细节,这需要另外一篇很长的文章来介绍,这里并不详述。
【如何在User mode和supervisor mode之间切换?】
ARM支持多种模式,User mode是非特权模式,其他模式都是特权模式。(个人觉得多模式的好处主要是提高处理硬件中断等事件的效率,因为每种模式下都有自己特有的寄存器,可以减少处理过程中的现场保护动作)ARM模式是由机器状态字中对应的位控制的,不同的值表示不同的模式。机器状态字中的这些位是受保护的,只有在特权模式下才能自由修改。另外外部事件,例如硬件中断、数据异常或软中断命令都会导致ARM切换到对应的处理模式。
对于每种外部事件,ARM都有对应的处理函数。ARM检测到这些事件后,会到其地址空间的起始部分的中断向量表中找到对应处理函数的起始地址。swi事件的处理函数也必须被正确的放到对应的位置。在Kernel Package中的文件kernel/eka/include/nkern/arm/entry.h中,你可以看到下面这样一段。这一段的注释和命令都要求,这段代码生成的二进制代码必须被放在ARM地址空间的起始位置。看下代码发现,起始位置是几条跳转指令,后面定义了条状到的标签例如__ArmVectorSwi ,发生对应事件之后会先到中断向量,然后跳转到对应标签,执行对应处理函数。与Symbian OS API相关的函数ArmVectorSwi 的实现部分在文件kernel/eka/nkern/arm/victors.cia
/* NOTE: We must ensure that this code goes at the beginning of the kernel image. */
__NAKED__ void __this_must_go_at_the_beginning_of_the_kernel_image()
{
asm("ldr pc, __reset_vector "); // 00 = Reset vector
asm("ldr pc, __undef_vector "); // 04 = Undefined instruction vector
asm("ldr pc, __swi_vector "); // 08 = SWI vector
asm("ldr pc, __pabt_vector "); // 0C = Prefetch abort vector
asm("ldr pc, __dabt_vector "); // 10 = Data abort vector
asm("ldr pc, __unused_vector "); // 14 = unused
…
asm("__reset_vector:");
asm(".word __ArmVectorReset ");
asm("__undef_vector:"); <
__DECLARE_UNDEFINED_INSTRUCTION_HANDLER;
asm("__swi_vector:");
asm(".word __ArmVectorSwi ");
asm("__pabt_vector:");
__DECLARE_PREFETCH_ABORT_HANDLER;
asm("__dabt_vector:");
__DECLARE_DATA_ABORT_HANDLER;
asm("__unused_vector:");
…
就像前面所说,User mode到supervisor mode的切换,靠swi指令实现。而进入supervisor mode之后,由于已经是特权模式了,可以自由改变ARM的工作模式,所以OS API处理完成后,直接改变机器状态字中的模式位,然后跳转就回到了User mode。需要注意的是,ARM并没有类似与X86下的iret指令,从中断返回直接改变机器状态字中的模式位,然后跳转即可。
本文只是介绍Symbian OS API的调用过程,因此并不详细介绍ARM CPU的中断体系及对应处理过程。如果你对这部分感兴趣,可以看ARM CPU相关的书籍。 Symbian OS Internals 第六章 Interrupts and Exceptions 大概介绍了Symbian中如何使用ARM中断,但是并没有详细介绍ARM中断。
【什么是FastExceCall和SlowExceCall?】
我们从上面OS API相关的代码中可以看到,Symbian OS API分FastExceCall和SlowExceCall。从名字就可以看出,两者的主要却别在于响应速度,FastExceCall的处理速度更快一些,不过FastExceCall主要是一些完成简单功能的OS API,完成复杂功能的OS API一定是SlowExceCall。
FastExceCall和SlowExceCall处理过程的差别有哪些呢?第一是完成操作的复杂性,FastExceCall完成的工作会更简单;第二是FastExecCall的处理过程中,中断可能被关闭,另外不会执行DFC(delayed function call)。如果你想了解两者处理过程差异更详细的信息,请参考Symbian OS Internals 第五章 Kernel_Services 的 Slow and fast executive call compared 这一小节
【软中断服务函数做了什么?】
软中断服务函数就是上面提到过的文件kernel/eka/nkern/arm/victors.cia中的__ArmVectorSwi函数。(C代码中有下划线,是为了符合汇编代码和C代码链接的文件约定)。软中断服务函数的大体处理过程是,根据调用码中的是否是FastExecCall的位确认fast/slow call类型然后分别处理。FastExceCall的处理相对简单,SlowExceCall的处理过程比较麻烦。这些麻烦的过程主要是进行调度方面的事情,超出本文内容,不详述。fast/slow call处理过程中的共同工作是,根据OS API调用码,从OS API列表中找到对应的内核处理函数地址,然后跳转执行。另外还有处理完成后改变机器状态字中的模式控制位,然后返回。
需要注意的是中断服务函数__ArmVectorSwi中使用的OS API列表并不是全局的FastExecCall和SlowExceCall列表,而是NThread.iFastExecTable。这是因为每个线程在创建时,会初始化自己的OS API列表。不过目前来说,线程创建时都是用全局的FastExecCall和SlowExceCall列表初始化对应的成员变量iFastExecTable和iSlowExecTable。线程初始化的代码在kernel/eka/kernel/sthread.cpp的DThread::DoCreate函数中,其中相关代码如下:
ni.iAttributes=0; // overridden if necessary by memory model
ni.iHandlers = &EpocThreadHandlers;
ni.iFastExecTable=(const SFastExecTable*)EpocFastExecTable;
ni.iSlowExecTable=(const SSlowExecTable*)EpocSlowExecTable;
ni.iParameterBlock=(const TUint32*)&aInfo;
跟着code走,Symbian OS API调用实例分析
相信本文上面的内容,已经比较详细的描述了Symbian OS API调用的详细过程。如果你觉得哪部分不够详细,欢迎拍砖。下面我们还是以实际代码为例,实际跟着代码走一遍Symbian OS API调用的具体过程。
1. 从用户编写的代码开始,如下。我们分析newChunk.Base()的处理过程。newChunk.Base()属于上文中所说的User Library,它是封装API实现的。
RChunk newChunk=0;
newChunk=OpenGlobal(_L(*SharedChunk),ETrue);
TUint* base=0;
base=newChunk.Base();
2. RChunk的实现代码在文件kernel/eka/euser/us_exec.cpp中,RChunk::Base()的实现代码如下,调用了OS API的用户态接口函数return(Exec::ChunkBase(iHandle))
EXPORT_C TUint8 * RChunk::Base() const
/**
Gets a pointer to the base of the chunk's reserved region.
@return A pointer to the base of the chunk's reserved region.
*/
{
return(Exec::ChunkBase(iHandle));
}
3.在文件kernel/eka/kernel/execs.txt中我们可以找到OS API ChunkBase对应的描述信息,如下,是一个SlowExceCall,返回参数为TUint8*类型,会传入类型为chunk的handle
slow {
name = ChunkBase
return = TUint8*
handle = chunk
}
同时也可以在生成的user.h中找到对应的OS API用户态接口函数,如下
__EXECDECL__ TUint8* Exec::ChunkBase(TInt)
{
SLOW_EXEC1(EExecChunkBase);
}
另外,也可以在生成的kernel.h中找到对应的OS API内核态处理函数声明及API数组中对应的项,如下
class ExecHandler
{
public:
...
static TUint8* ChunkBase(DChunk*);
DECLARE_FLAGS_FUNC(0|EF_C|EF_R|EF_P|(EChunk+1), ExecHandler::ChunkBase)
4. 有了以上的代码以及我们之前关于OS API调用过程的说明,我们已经可以从用户最初的调用走到OS API内核实现函数了,只是还没有给出OS API内核实现函数的具体实现。genexec.pl生成的kernel.h只包含了OS API内核实现的声明,并不包括实现,实现需要另外的代码写好。ExecHandler::ChunkBase对应的实现函数在文件kernel/eka/kernel/sexec.cpp中,代码如下。关于这个OS API具体实现的解释,这里就不给出了。
TUint8 *ExecHandler::ChunkBase(DChunk* aChunk)
//
// Return the address of the base of the Chunk.
//
{ __KTRACE_OPT(KEXEC,Kern::Printf("Exec::ChunkBase"));
return (TUint8*)aChunk->Base(&Kern::CurrentProcess());
}
5. 上面列出了RChunk::ChunkBase完整实现过程的代码,结合前文给出的Symbian OS API过程,相信你已经可以弄清楚完整过程了。