系统结构 第一部分
系统结构 本讲分析 DLLs 内存管理 线程和进程
系统结构 Symbian OS高层简单概述 这些电话 它所谓被称为是“开放”的平台是因为,除了生产商内置的应用程序 它是为高级移动电话设计的基于开放标准的一个多任务操作系统 这些电话 具有负责的图形用户界面(GUI),以及许多内置的使用GUI的应用程序 例如,消息通讯和日历 它所谓被称为是“开放”的平台是因为,除了生产商内置的应用程序 用户可以安装其他程序,例如游戏、企业应用程序(如发送email)或者工具软件
系统结构 Symbian OS授权给世界领先的手持设备制造商 Symbian OS 具有一个灵活的体系结构 Arima, BenQ, Fujitsu, Lenovo, LG Electronics, Motorola, Mitsubishi, Nokia, Panasonic, Samsung, Sharp and Sony Ericsson Symbian OS 具有一个灵活的体系结构 允许在核心操作系统上面运行不同的用户界面 Symbian OS 用户界面(UIs)包括 Nokia的S60 和Series 80 平台, NTT DoCoMo的FOMA以及索尼爱立信的UIQ
系统结构 EKA1 和 EKA2 指Symbian OS内核的不同版本 EKA 表示“EPOC内核体系结构(EPOC Kernel Architecture)“ (Symbian OS 以前叫“EPOC”,较早前叫“EPOC32”) EKA1是32为内核,最早在1997的Psion Serial 5中发布 EKA2 在以后的幻灯片中讨论
系统结构 EKA2 首先在Symbian OS version 8.0b中引入 但是直到version 8.1b才发布到手机产品中 在日本MOAP 2.0的FOMA 902i系列手机中可以找到 是Symbian32位内核的第二代 与EKA1在本质上的很大不同 为内核及用户态线程提供硬实时保证
Symbian OS中的DLLs 知道和理解多态接口(polymorphic interface)和共享库(静态)DLL的特点 知道UID2 值被用于区别静态和多态DLL以及不同的插件(plug-in)类型 对于共享库,理解必须导出那些函数,如果其他二进制组件要想访问它们的话 知道Symbian OS不允许通过名字进行库查找,而只能通过序号
共享库和多态接口DLLs 动态链接库 有主要有两种类型DLL DLLs 是编译过C++代码库 可以被加载进运行的进程 在一个现有线程的上下文中 有主要有两种类型DLL 共享库 (静态接口) DLLs 多态接口(插件) DLLs
共享库DLLs 一个共享库DLL 这种DLL的例子是 一个共享库 实现可有其他库或者EXE使用的库代码 共享库的文件扩展名是dll 用户库EUser.dll 文件系统库EFsrv.dll 一个共享库 根据模块定义(.def)文件导出API函数 可以有多个导出函数 每个函数都是DLL的入口
共享库DLLs 一个共享库发布 When executable code that uses the library runs 当使用库的可执行代码运行时 Symbian OS加载器加载它所链接的所有共享库,以及这些共享库所要求的其他 DLLs 这是个递归操作直到可执行程序所有的需要的共享代码都被加载
多态接口DLLs 一个多态接口DLL 它可能具有.dll 文件扩展名 例如 实现一个独立定义的抽象接口 例如由框架定义的接口 它可能具有.dll 文件扩展名 但是它通常使用不同的扩展名以便进一步标识DLL的不同性质 例如 .fsy 表示文件系统插件 .prt 表示协议模块插件 文件系统和套接字在稍后讨论 ASD Primer ... .fsy for a file system plug-in (see Chapter 12 - Filesystem), or .prt for a protocol module plug-in (see Chapter 13 - Sockets)
多态接口DLLs 多态接口DLLs 被用作插件 它们具有单个入口的”门(gate)“函数或”工厂(factory)“函数 它们被用于 它将实例化实现了接口的具体类 它们被用于 提供一个固定接口的多个不同实现(插件) 它们被动态加载 典型得由框架加载 ASD Primer ... .fsy for a file system plug-in (see Chapter 12), or .prt for a protocol module plug-in (see Chapter 13)
ECOM插件 从Symbian OS v7.0 以后 ECOM 是实现接口的通用框架 以及用于查找和加载实现接口的那些插件 许多Symbian OS 框架要求其插件被写成ECOM插件 而不是加载多态接口DLL插件的私有类型的框架 例如,识别器框架
ECOM 插件 使用ECOM 使得每个框架代为寻找和加载合适的插件 所以使得设计和实现新服务或者特性变得更加简单 而不是由自己去执行这个任务
DLLs使用的UIDs UIDs被用来标识文件类型 一个UID 是全球唯一的32位标识值 相应的应用程序关联的数据文件 一个UID 是全球唯一的32位标识值 Symbian OS 使用三个UID的组合来唯一标识一个二进制执行 程序 DLLs使用的三个UID 值如下...
UID1 UID1 是一个系统级的标识符 对于共享库 区别EXEs 和DLLs 该值从来不需要显式声明 由Symbian构建工具通过MMP文件指定的targettype 来决定 对于共享库 指定的targettype 应该是DLL UID1 = KDynamicLibraryUid = 0x10000079 See primer: The targettype keyword and the build tools are discussed in more detail in Chapter 14
UID1 (续) 对于多态ECOM插件DLLs 其他多态非ECOM 插件DLL 目标类型 targettype是PLUGIN 或者ECOMIIC (对Symbian OS v9.0以前的版本) 其他多态非ECOM 插件DLL 目标类型 FSY (文件系统插件) PRT (协议模块插件). targettype 关键字和构建工作稍后讨论
UID2 UID2 区分共享库DLLs 和多态接口DLLs 多态接口DLLs 用UID2 值致命其类型 共享库总是KSharedLibraryUid (0x1000008d) 多态接口DLLs 用UID2 值致命其类型 例如套接字服务器协议模块的UID2 值是0x1000004A
UID3 UID3 被用于唯一性的标识一个组件 Symbian 通过一个中心数据库管理UID的分配 开发者必须进行Symbian签名以请求UIDs Symbian Signed 的更多内容后面介绍
EXEs UID1 值由targettype EXE 语句设置成 (KExecutableImageUid=0x1000007a) UID2 对EXE 无用 可以不用指定 或者显式的设置为KNullUid (=0) UID3 在Symbian OS v9 及后续版本UID3 应当被设置成一个唯一值 以作为二进制代码的安全标识符 Symbian OS v9 以前的版本不需指定 Chapter 15 in the Primer for Platform security
从 DLL中导出函数 一个共享库DLL 通过导出其函数提供对其APIs的访问 被另一个DLL或者XE使用,被编译到一个单独的库组件 通过创建一个.lib文件,导出使得函数对其他模块是”公开的“(“public” ) 库包含了要被调用代码链接使用的导出表
从 DLL中导出函数 将被导出的函数 客户端代码将包含该都文件 相应的函数实现 应当在类定义的头文件中用宏IMPORT_C 标记 有效的将每个函数”导入“到其模块中 当要使用它时 相应的函数实现 应该在实现它的.cpp文件中使用宏EXPORT_C 作为前缀
从 DLL中导出函数 IMPORT_C 和EXPORT_C 的使用: class CMyExample : public CSomeBase { public: IMPORT_C static CMyExample* NewL(); IMPORT_C void Foo(); ... }; EXPORT_C CMyExample* CMyExample::NewL() {...} EXPORT_C void CMyExample::Foo()
从 DLL中导出函数 关于那些函数应当被导出的规则如下: 内联(Inline)函数绝对不要导,因为没有必要这样做 这就是为什么: IMPORT_C 和EXPORT_C 宏添加函数到导出表,以使其对于链接了库的组件是可以访 问的 但是一个内联函数的代码已经对于调用者是可以访问的,因为它们是在头文件内声明的 所以编译器在解释inline 指令时,在任何调用它的客户端代码中直接加入内联代码 没有需要导出它 Inline directive is a “hint” to the compiler expand the function and not use the standard function call mechanism.
从 DLL中导出函数 只有那些在DLL外使用的函数应当用IMPORT_C 和EXPORT_C 进行导出 如果函数对于类是私有的 它永远不可能被客户端代码访问 导出它未必会在模块定义文件(.def)中的导出列表中添加它
从 DLL中导出函数 所有的虚函数都应被导出 所有具有虚函数的类 这样虚函数表 不管是公开的,保护的还是私有的 因为它们能够被派生类在其他的代码模块中重新实现 所有具有虚函数的类 也必须导出构造函数 哪怕它是空的 这样虚函数表 通过访问基类构造函数被正确生成
通过序号和通过名字查找 DLL 代码的大小是经过优化的 在绝大多数操作系统中,为了加载动态库,一个DL的入口可能: 以便节省ROM 和RAM 空间 在绝大多数操作系统中,为了加载动态库,一个DL的入口可能: 或者是通过字符串匹配它们的名字来鉴别——通过名字查找 或者是通过它们在模块定义文件中导出的顺序——通过序号查找 Symbian OS 不提供通过名字查找 因为这会增加DLL的大小 保存所有导出文件的名称是对有限的ROM和RAM空间的浪费
通过序号和通过名字查找 Symbian OS 只使用通过名字链接 例如 二进制兼容将在后续讲义中讨论 这对二进制兼容有重大影响 在DLL的多个发布版本间,序号必须没有修改 例如 Code which links against a library and uses an exported function with a specific ordinal number in an early version of the library 链接了库代码,它并使用了具有早期版本库中特定序号的导出函数 将不能调用新版本库的那个函数,如果函数的序号改变了的话 二进制兼容将在后续讲义中讨论 Binary compatibility is discussed further in Chapter 16 of the ASD Primer
注意 有种类型的虚函数不应从DLL导出,即纯虚函数 因为纯虚函数通常没有实现代码 所有没有代码供导出
可写静态数据 知道可写静态数据在EKA1的DLL中是不允许的,在EKA2中也是不鼓励 知道从DLL中移除可写静态数据的基本移植策略
对可写静态数据的支持 Symbian OS支持在EXE中使用全局可写静态数据 在所有的版本和手持设备上 在包含EKA1的Symbian OS中( Symbian OS versions 8.1a, 8.0a 或者更 早的版本) 在DLL中不能使用可写静态数据 这是因为DLL具有分离程序代码和只读数据区域 但是没有可写数据的区域
DLL中支持可写静态数据的Symbian OS 版本 包含EKA2的Symbian OS版本(Symbian OS 8.0b,8.1a,9.0以及更高 版本) 现在支持在DLL中使用可写静态数据 但是这仍然是不推荐的 因为从内存使用看它的代价很大 并且Symbian OS模拟器的支持也有限 Symbian 推荐只在万不得已的使用使用它 例如当移植为其他平台上编写的使用了大量可写静态数据的代码
GUI应用程序中的可写静态代码 在EKA1 中 在EKA2 中 所有的GUI应用程序都构建成DLL 应用程序代码不能使用可写静态或者全局数据 应用程序现在被构建成EXE,因此这不再是一个问题 可修改全局或静态数据在EXE中已经是被允许的
DLL中支持可写静态数据的Symbian OS 版本 应用程序二进制类型 v6.1 — v8.0a (含), v8.1a (EKA1) 硬件构建不支持 (无法兼容) DLL — 不允许可写静态数据 v8.0b, v8.1b, v9.0 及更高 (EKA2) 支持,但是不推荐 — 只有有限的模拟器支持,内存使用上也很低效 EXE — 可以使用可写静态数据
在DLL中如何使用可写静态数据 为了能够在EKA2中使用全局可写静态数据 PETRAN ... 必须在DLL的MMP文件中添加关键字EPOCALLOWDLLDATA 在使用EKA1的Symbian OS中不使用该关键字 当为手机硬件构建DLL代码时,工具链会返回一个错误 PETRAN ...
避免DLL中使用可写静态数据的方法 1. 线程本地存储 它可以通过如下方法访问 线程本地存储是一个32位指针 1. 线程本地存储 一种替代可写静态数据的方法叫线程本地存储(TLS) 它可以通过如下方法访问 Symbian OS 8.1b以前的版本使用类Dll 8.1b,9.0及后续版本使用类UserSvr 线程本地存储是一个32位指针 每个线程可以不同,它可用来指向一个对象,该对象模拟了全局可写静态数据 所有的全局数据必须组合到单个的对象中 And allocated on the heap when the thread is created 当创建线程时在堆中分配该对象
线程本地存储 函数Dll::SetTls() 或 UserSvr::DllSetTls() 被用于保存对象指针 线程本地存储指针 函数Dll::Tls() 和UserSvr::DllTls() 被用于访问全局对象 在线程销毁时 该数据也被销毁
避免DLL中使用可写静态数据的方法 2. 客户端-服务器框架 一个普遍的移植策略是将代码包装在一个Symbian服务器中 2. 客户端-服务器框架 Symbian OS 支持在EXE中使用可写全局数据 一个普遍的移植策略是将代码包装在一个Symbian服务器中 它是一个EXE 以客户端接口方式导出API
避免DLL中使用可写静态数据的方法 3. 将全局变量嵌入到类中 如果是少量代码,将绝大多数全局数据转移到类中是可能的 3. 将全局变量嵌入到类中 如果是少量代码,将绝大多数全局数据转移到类中是可能的 数据就可以作为函数参数在对象和函数间传递
定义可写静态数据 全局可写静态数据是任何进程都可以修改的变量 实际上这意味着任何全局范围的数据都被定义在以下列表之外 它在进程的声明周期中都存在 实际上这意味着任何全局范围的数据都被定义在以下列表之外 函数 结构或类 函数范围的静态变量
定义可写静态数据 唯一可以在DLL中使用全局数据是 所以以下定义是可以接受的: 内建类型的全局常量 或者无构造函数的类的全局常量 所以以下定义是可以接受的: static const TUid KUidFooDll = { 0xF000C001 }; static const TInt KMinimumPasswordLength = 6;
定义可写静态数据 以下定义是不可用的,因为它们含有非平凡的类构造函数 即, 这些对象必须在运行时构造 static const TPoint KGlobalStartingPoint(50, 50); static const TChar KExclamation(’!’); // 下面的文字类是要被废弃的 static const TPtrC KDefaultInput =_L(""); See the lecture on descriptors for information about the _L literal macro
定义可写静态数据 对象的内存是代码中预分配的,但是它不会被真正的初始化并保持不变 这样,在构建时每个都创建一个非常量的全局对象 而是到运行构造函数之后 这样,在构建时每个都创建一个非常量的全局对象 将导致为手机硬件的构建失败 除非在DLL的MMP文件中加入了关键字EPOCALLOWDLLDATA
定义可写静态数据 下面对象也是非常量的 虽然由ptr指向的数据是常量 指针本身不是常量: 可以通过将指针改成常量对上面语句进行更正 // 可写静态数据! static const TText* ptr = (const TText*)"data"; static const TText* const ptr = (const TText*)"data";
注意 在EKA1中 模拟器可以使用底层的Windows DLL机制提供每个限于单个进程的DLL数据 如果非常量的全局数据被不小心使用了,在模拟器构建时不会被发现 当PETRAN工具在为硬件平台构建时遇到它,就会导致失败
在ROM和RAM中执行 认识关于Symbian OS在ROM和RAM中执行DLL和EXE的基本陈述的正确性
ROM和RAM中的EXEs 在目标硬件上 基于ROM的EXEs 可执行代码可以在生产时被构建到手机的只读存储(ROM)上, 也能以后安装到手机上,可以使安装到手机的内部存储器或者可移动存储器,如记忆棒 或MMC 基于ROM的EXEs 可以认为能直接在ROM上执行 这意味着程序代码和只读数据(如文字描述符)直接从ROM中读取 在RAM中只分配一个独立的数据区域用于数据读写
ROM和RAM中的EXEs 如果安装EXE(而不是直接构建到ROM中) 如果EXE的第二个副本被启动 完全在RAM上执行 它有为程序代码和只读静态数据分配的一个区域 另一个单独的区域用于静态数据的读写 如果EXE的第二个副本被启动 只读区域是共享 为读写数据分配一个新的区域
ROM和RAM中的DLL ROM 中的DLLs 从RAM 运行 DLLs 使用引用计数 不会被加载到内存中 在ROM上的固定地址就地执行 加载到特定地址 该地址只有在加载时才能决定 使用引用计数 只有当没有任何组件使用DLL时,它们才允许被卸载
ROM和RAM中的DLL 从RAM加载DLL Symbian OS 不同与简单的将DLL存储到内部驱动(RAM)上 通过修正重定位信息,为执行做准备
ROM和RAM中的DLL 从ROM 执行的DLLs 被固定在一个地址上 与DLL 相比 缺少重定位信息意味着DLL 不能从ROM复制 因此不需要重定位 与DLL 相比 为了占用更少的ROM空间 Symbian OS在构建ROM时,剔除了重定位信息 缺少重定位信息意味着DLL 不能从ROM复制 然后保存到RAM中并从中执行
ROM和RAM中的DLL 对于两种DLL (静态类和多态接口插件) 如果多个线程或进程同时使用一个DLL 代码段是共享的 如果多个线程或进程同时使用一个DLL 将访问相同的程序代码副本 在内存的相同位置 随后加载的希望使用DLL的进程或线程 被DLL加载器修正以使用相同的副本
线程和进程 认识Symbian OS中关于线程和进程的基本描述的正确性 认识同步原语RMutex, RCriticalSection and RSemaphore 的角色和特征
线程 线程 在Symbian OS应用程序创建多个线程并行执行是可能的 但是在很多情况 是执行的基本单元 构成了多任务的基础——允许多个代码序列同时执行(或者看起来是这样) 在Symbian OS应用程序创建多个线程并行执行是可能的 但是在很多情况 使用活动对象更合适 因为它们对Symbian OS上的事件驱动多任务进行了优化 See the active objects lectures for more about why they are generally preferred to multi-threaded code
线程 RHandleBase RThread 用于操作线程的类是 RThread RThread 类型的对象描述了一个线程的句柄 线程本身是内核对象
线程 RThread 的基类是RHandleBase 类RThread 定义了很多函数用于创建线程 线程并不包含在独立的可执行文件中 它封装了一个通用句柄的行为 RHandleBase作为基类使用贯穿整个Symbian OS 用以标识另一个对象的句柄 通常是内核对象 类RThread 定义了很多函数用于创建线程 线程并不包含在独立的可执行文件中 而是在其父进程中执行 每个线程具有一个独立的执行流
RThread::Create() 方法 下面是一个 RThread::Create() 方法(有很多重载方法): 每个线程创建函数 接收一个表示新线程唯一名称的描述符 一个指向线程开始执行的函数的指针 一个指向传递给函数的数据的指针 一个用于定义线程栈空间大小的值,缺省是8 KB TInt Create(const TDesC &aName, TThreadFunction aFunction, TInt aStackSize, TInt aHeapMinSize, TInt aHeapMaxSize, TAny *aPtr, TOwnerType aType=EOwnerProcess)
Thread Creation 线程被创建是 函数Create() 例如 处于挂起状态 其执行通过调用RThread::Resume() 开始 函数Create() Is overloaded to offer various options associated with the thread heap 被重载以提供线程堆的不同选项 例如 它的最大和最小大小 是否共享其创建的线程堆,或者在所属进程中是由一个特定的堆
线程堆 缺省的,每个Symbian OS线程 对则可以从最小扩展到最大值 当线程具有自己的堆是 具有自己的独立堆和栈 栈大小受限与RThread::Create()设置的参数 对则可以从最小扩展到最大值 当线程具有自己的堆是 栈和堆被分配在相同的内存片中
线程标识 当线程被创建时,系统为其分配一个唯一性的线程标识 如果知道一个现有线程的TThreadId 值 作为替代 以TThreadId 对象的形式由RThread 的Id() 函数返回 如果知道一个现有线程的TThreadId 值 可以被传递给RThread::Open() 用来打开该线程的句柄 作为替代 线程的唯一名称也可作为参数传递用以打开线程句柄
线程调度 线程是抢先式调度的 如果两个或更多的线程具有相同的优先级 线程的优先级是一个数值 运行的线程可以移除 当前运行的线程是准备运行的线程中优先级最高的 如果两个或更多的线程具有相同的优先级 它们采用基于轮盘的时间片调度 线程的优先级是一个数值 数值越大——优先级越高 运行的线程可以移除 通过在线程句柄对象上调用Suspend() 可以通过调用Resume()重新被调度执行
线程终止 线程可以永久性终 在EKA1上 通过调用Kill(TInt aReason) 或 Terminate(TInt aReason) 这些方法用于手动停止一个线程 如果要终止线程以突出程序错误可以使用Panic() 在EKA1上 线程必须调用SetProtected() 以防止其他进程的线程获取其句柄 通过调用Suspend(), Panic(), Kill() 或 Terminate()终止它
线程安全 EKA2 的安全模式保证 函数 这也是可能的 线程已经是被保护免遭运行在其他进程中的线程访问的 冗余的SetProtected() 方法已经被移除 一个线程不能停止其他进程中的另一个线程 函数 Suspend(), Terminate(), Kill() 或 Panic() 在EKA2 中仍然保存 一个线程仍然可以对自己使用这些方法 或对同一进程中的其他线程使用 但是不能用于不同进程的线程 这也是可能的 服务器对一个有错误行为的客户端线程产生一个致命错误 通过调用RMessagePtr2::Panic()
线程终止 如果进程中的主线程 对于次线程 被下列终止方法的任何一个结束 Suspend(), Terminate(), Kill() or Panic() 这个线程也会终止 对于次线程 它通过在进程中调用RThread::Create()创建 如果该线程终止了 进程本身并不会停止运行
线程死亡通知 用于在一个线程死亡时接收通知 当线程终止时请求完成 如果取消了通知请求 提交线程终止时予以通知的请求 通过调用RThread::Logon(TRequestStatus &aStatus) TRequestStatus 是一个完成信号量 当线程终止时请求完成 aStatus 包含了线程的结束值 如果取消了通知请求 通过调用RThread::LogonCancel() aStatus 会包含 KErrCancel
线程终止 线程句柄也提供给出关联线程结束状态详细信息的函数 TExitType RThread::ExitType() 允许调用者区分正常终止和致命错误 TInt RThread::ExitReason() 获得线程结束的特定原因 TExitCategoryName RThread::ExitCategory() 获得线程结束关联的策略名称
线程通知 线程集合点请求( thread rendezvous request )也能被创建 该请求以以下集中方式完成: 用以支持正确顺序的同步,例如数据操作 通过调用同步函数RThread::Rendezvous() 该请求以以下集中方式完成: 当线程再一次调用RThread::Rendezvous(TInt aReason) 时 如果通过调用RThread::RendezvousCancel()将已激活的请求取消 如果线程退出或者发生致命错误 More details in the ASD primer and Symbian OS SDK
用于同步的内核对象 除了使用RThread::Rendezvous(), Symbian OS 提供了用 于几个标识线程同步的内核对象类 信号量( Semaphore) 互斥体(mutex) 临界区(critical section)
信号量 信号量 创建和访问信号量 全局的信号量 本地信号量 能用于从一个线程向另一个线程发送信号 或者用于保存共享资源使其避免同时被多个线程访问 创建和访问信号量 通过句柄类RSemaphore 全局的信号量 可以比系统中任何进程创建、打开和使用 本地信号量 限于单个进程中的所有线程
信号量 信号量可被用于 限制共享资源的并发访问 要么是一个时间单个线程访问 或者是具有一定限制的多个访问
互斥体 一个互斥体 用于保存共享资源 所有同一时间只能由一个线程访问 类RMutex 用于创建和访问全局及本地互斥体
临界区 一个临界区 是一个代码区域,它不能被多个线程同时进入 一个例子是操作全局静态数据的代码 因为如果多个线程同时改变数据的话会有出现问题
类RCriticalSection 类RCriticalSection RCriticalSection 对象 进程中只允许一个线程进入被控制的代码段 强制其他想要访问临界区的线程等待,直到第一个线程从临界区退出 RCriticalSection 对象 总是进程的本地对象 一个临界区不能用于对不同进程的线程之间共享的资源进行访问控制, 而应当使用互斥体或信号量
进程
注意 关于用户端或者用户模式操作的提示 关于Symbian OS内核和内存管理模式更加深入的信息,请参见: 用户端应用程序有EUser.dll处理 它为用户端组件调用系统或核心函数 内核函数有时需要特权态 关于Symbian OS内核和内存管理模式更加深入的信息,请参见: Smartphone Operating System Concepts with Symbian OS By Michael J. Jipping ISBN 978-0-470-03449-1
进程 一个Symbian OS进程 是一个具有自己的数据区、栈和堆的可执行单元 By default a process is given 8 KB of stack and 1 MB of heap 缺省情况下进程具有8K的栈和1M的堆 有时被当成一个保护单元
进程 Symbian OS同时可以有许多是活动的 缺省地 把扩同一个进程的多个副本 进程具有私有的地址空间 用户端进程不能直接访问属于另一个用户端进程的内存 缺省地 一个进程包含单个执行线程——主线程 可以通过前面描述的方法创建的额外的线程
进程 当从一个线程切换到另一个线程的会发生上下文切换 线程间切换 进程上下文切换 只要一个线程被调度执行而变成活动状态就会发生上下文切换 在不同的进程间进行切换要比同一个进程的线程间切换其代价更大 进程上下文切换 需要两个进程的数据区被内存管理单元(MMU)重新映射
进程 RHandleBase RProcess 用来操作进程的类是RProcess 方法RProcess::Create() 可被用来启动一个新的命名进程 方法RProcess::Open() 可被用来打开一个进程的句柄 由名称或者进程标识(TProcessId)来识别 RProcess RHandleBase
进程 有多种方法来停止进程 方法Resume() 注意没有RProcess::Suspend() 函数 与RThread 类似 将进程的第一个线程标志成可以执行的 注意没有RProcess::Suspend() 函数 因为进程不会被调度 线程构成了执行的基本单元,并且运行在一个进程受保护的地址空间内
进程 在Windows上 在EKA1上 在EKA2 上 模拟器运行在一个称为EPOC.exe的win32进程中 其中每个Symbian OS 进程是以独立的线程运行的 在EKA1上 Windows 上对进程的模拟是不完全的 RProcess::Create() 返回 KErrNotFound 在EKA2 上 这已经被移除了 Symbian OS 仍然运行在单个进程中 但是模拟器得到了增强... RProcess::Create() 被翻译成去创建一个新的 Win32 线程
系统结构: 第一部分 Symbian OS中的DLLs 可写静态数据 ROM 和RAM中的可执行代码 线程和进程
系统结构 第二部分
系统结构 本讲分析 进程见通信 (IPC) 识别器(Recognizers) 致命错误和断言(assertions)
进程间通信(IPC) 知道Symbina OS上首选的IPC机制 (客户端-服务器, 发布和订阅,消息队列), 明 白在给定场景那种机制是最合适的 理解使用发布订阅机制来获取和订阅系统端的属性变化,包括平台安全在保护属性免 遭而已修改中所扮演的角色
客户端-服务器 客户端-服务器框架 客户端连接服务器 基于会话的通信 是Symbian OS中进程间通信的通用形式 客户端-服务器框架将在后续章节详细介绍 客户端连接服务器 以便为将来所有的通信建立一个会话(session) 一个会话包括了客户端请求和服务器应答,它们由内核进行转发 基于会话的通信 保证所有的客户端在服务器错误或关闭时得到通知 如果发生了错误,所有的服务器资源都应该清除 或者当客户端断开链接或死亡时
现在不用担心细节,因为客户端-服务器体系结构将在后面讲义中深入探讨 用户端 内核端 客户端 服务器 由内核中转 基于会话的客户端能具有多个会话 序列化请求 CSession2 DSession CServer2 RServer2 DServer
客户端-服务器 客户端-服务器通信范式 有一些限制: 让很多客户端可靠的并发访问一个服务或共享资源 服务器相应的序列化和中转对服务的访问 客户端必须知道哪个服务器提供它们需要的服务 客户端和服务器之间必须维护一个持久的会话 不适合事件多播(服务器发起的对多个客户端的“广播”)
进程间通信(IPC) 为了客服这些限制 以提供另外的IPC 机制: 发布与订阅及消息队列 扩展了Symbian OS version 8.0 在本章描述 Symbian OS Internals has more on Shared buffer i/o. See also the data sharing booklet from Symbian Press http://developer.symbian.com/main/learning/press/books/pdf/data_sharing_tips.pdf
进程间通信(IPC) 共享缓冲区I/O 它被用于 不被讨论因为它主要是为设备驱动程序开发者设计的 它被用于 允许设备驱动器及其客户端能够访问同一个内存区域 而不需要中断处理时对事件进行复制 Symbian OS Internals has more on Shared buffer i/o
发布和订阅 发布和订阅机制 发布和订阅 属性变化 发布者和订阅者 提供异步的多播事件通知 线程见无连接通信 提供一种途径用以定义和发布被称为“属性”的系统全局变量的变化 属性变化 可以被异步通知到(“发布”)不止一个感兴趣(“订阅”)的端 发布者和订阅者 可以动态的加入和退出而不需要任何连接的建立和断开 It is worth spending a little some time going through RProperty class in the SDK
发布和订阅 订阅者 发布者 属性 P1 发布者/订阅者 用户端 内核端 获得 设置 P2 P3
发布和订阅 属性是数据值 订阅者 它们只需要知道: 它们由64位整数唯一标识 这一标识是发布者和订阅者之间必须共享的唯一信息 (典型的,通过公共头文件) 没有必要为一个属性提供接口或函数 订阅者 不需要知道哪个组件在发布一个属性 它们只需要知道: 发布和订阅的API 它们感兴趣的属性的标识
发布和订阅 RHandleBase RProperty 发布和订阅API 一个属性的标识由两部分组成: 由RProperty 类提供 类别 – 由一个标准UID定义,它指明了属性属于哪个类别 键 – 它唯一的标识了特定类别中的一个属性 其值依赖于类别的键是如何枚举的 RProperty RHandleBase
发布和订阅 一个属性保存了一个数据变量,其可以是 一个线程可以扮演的角色是 一个32位整数 或者一个字节数组(描述符) 最大长度为512 字节 Unicode 文本(也是最多512 字节) 或者最大达65,536字节的大字节数组 一个线程可以扮演的角色是 发布者或订阅者
发布和订阅 任何线程都能定义属性 一旦属性被定义 通过调用RProperty::Define() 来创建一个变量 并指明其类型和访问控制 它就存在于内核中知道它被显式的删除或者系统重启 属性的生存期与定义它的线程或进程无关
发布和订阅 属性可以被发布或获取 在EKA2中 利用以前连接的句柄 或者通过为每个调用指定属性的标识 连接到一个已有句柄的好处是它有一个确定的有限的执行时间 这使得它适合高优先级的实时任务
发布和订阅 发布属性 当一个属性被发布时 通过调用RProperty::Set() 这将自动的为属性写入一个新的值 保证来自多个线程的访问被正确处理 当一个属性被发布时 所有激活的订阅都被完成 即使属性值实际上并没有改变 这使得可以用属性作为简单的广播通知
发布和订阅 要发布一个属性 客户端必须通过连接它注册兴趣 调用异步方法 RProperty::Subscribe()
发布和订阅 在下列状态下会发生通知: 客户端注册对属性的兴趣A client registers its interest in the property 通过连接它RProperty::Attach() – 并在结果句柄上调用Subscribe(), 其中传入一个TRequestStatus 引用 当一个新值发布时,客户端通过TRequestStatus对象上的信号得到通知,该信号指明了Subscribe() 请求已 经完成 客户端通过调用RProperty::Get()获得更新属性的值 客户端可以通过再次调用Subscribe()重新提交对属性改变通知的请求
发布和订阅 一个属性没有必要定义 这不是一个编程错误: 在它被访问之前 这被认为是 “懒定义” 一个属性在它被定义前被发布 这被认为是 “投机发布” 连接到一个未定义的属性不一定是一个错误
发布和订阅 Subscribe() 对一个未定义属性的请求不会完成直到: 要么属性被定义和发布 要么订阅者通过调用RProperty::Cancel()取消了请求
发布和订阅 需要使用发布和订阅机制,当一个组件需要即时提供或消费瞬时信息 时 一个典型的例子 例如 给于或者来自未知数目和类型的感兴趣的组件 而仍然要保持和它们的分离 一个典型的例子 是设备无线电状态改变的通知 例如 飞行模式 蓝牙打开或关闭 WiFi 开或者关
发布和订阅与平台安全 在Symbian OS v9的安全平台上 相应的 属性也必须被定义 为了保证进程隔离以便一个进程不能干扰另一个进程的属性 属性类型的UID应该匹配定义进程的安全标识(secure identifier) 相应的 调用RProperty::Define()的进程 必须具有WriteDeviceData 能力 属性也必须被定义 具有用TSecurityPolicy 对象定义的安全策略
发布和订阅与平台安全 对于发布属性值的进程,下面列表的事项是需要 对订阅属性的进程,下面事项也是需要的 能力(capabilities) (和/与) 商家标识(vendor identifier ) (和/与r) 安全标识(secure identifier) 对订阅属性的进程,下面事项也是需要的 更多关于平台安全和能力的介绍在后续讲义中 Platform security is covered in chapter 15 of the ASD primer.
发布和订阅与平台安全 举例 接收对一个属性的订阅之前 要检查属性创建时定义的安全策略 如果检查失败,订阅请求完成,返回参数为KErrPermissionDenied
消息队列 与客户端-服务器IPC面向连接的本质相比 消息队列 消息被发送 消息队列 (RMsgQueue) 提供端到端(peer-to-peer)以及多对多(many-to-many)的通 信机制 消息队列 提供一种发送数据(消息)到感兴趣的一方的方法 而不需要知道线程是否在侦听 或者接收者的标识 消息被发送 到队列而不是某个特定的接收者 单个队列可以被许多读者或写者共享
消息队列 消息队列句柄 DMsgQueue 队列 用户端 内核端 消息队列句柄1 消息队列句柄2 线程1 线程2 线程 3
消息队列 一个消息 一个队列 是至于队列中供分发和接收的对象 队列通常为给定类型的消息创建 是创建来处理固定长度消息的 消息必须是四个字节的倍数
消息队列 消息队列的大小 例如它能容纳的消息或槽位最大数目 是在队列创建时就固定的 队列中消息的最大尺寸 仅仅受限于系统资源
消息队列 一个消息队列 消息队列是一种数据传送机制: 在一个进程中 允许两个或多个线程进行通信而不需要相互建立连接 在运行在不同进程的线程之间 (使用全局队列,它是被命名过的并且对所有进程可见) 在同一个进程内的线程之间(使用局部队列,它对其他进程不可见...) 在一个进程中 消息可以指向映射到该进程的内存,也能用于在线程间传递描述和指针
消息队列 消息队列允许 发布和订阅 消息队列 从发送者到接收者“发射后不管”的IPC 使其很适合事件通知 对本质上是瞬时的状态变化通知比较有效 消息队列 对于允许信息在发送者生存期之外还能发送是有用的
消息队列 一个很好的使用消息队列的例子: 但是 中心日志子系统能使用消息队列来接收多个线程的消息 这些线程可能是也可能不是还在运行,当消息被读取和处理时 但是 消息和队列都不是持久的 它们在最后一个队列句柄 关闭时会被清除
识别器 正确认识Symbian OS中识别器的角色
识别器 识别器 直到Symbian OS v9.1 在后续Symbian OS版本中,它已经改为使用ECOM 是使用框架接口DLL的很好例子 加载识别器的框架由应用程序框架服务器 提供(Apparc) 直到Symbian OS v9.1 Apparc 实现它自己定制的识别器插件加载 在后续Symbian OS版本中,它已经改为使用ECOM
识别器 当文件系统中的一个文件需要与一个应用程序关联时 识别器不处理数据 Apparc 打开该文件, 从文件开始处读取一些数据到一个缓冲区 它然后调用依次调用系统中每个识别器的DoRecognizeL() 传入它读到缓冲区中的数据 如果一个差价识别了传入的数据,它就范围数据的类型 (MIME type) 识别器不处理数据 它们仅仅尝试识别数据的类型 所以数据能够传入到能够最好使用它的应用程序中
识别器 插件识别器框架 所有的数据识别器 允许开发者创建附加的数据识别器 通过安装它们把它们加入到系统中 必须实现由CApaDataRecognizerType定义的多态接口 该接口具有三个虚函数...
DoRecognizeL() DoRecognizeL() 每个实现 和一个值,用来指示信心等级,从: 执行数据识别 该函数不是纯虚函数,但是必须实现 每个实现 应该设置一个值,指明它认为该属于哪种MIME类型 和一个值,用来指示信心等级,从: ECertain – 数据明确的是某个特定的数据类型,到 ENotRecognized – 不能识别数据
SupportedDataTypeL() 返回识别器能够识别的MIME 类型 所有识别器插件都必须实现这个纯虚函数 每个识别器对SupportedDataTypeL() 的实现 由识别器框架调用 在系统中所有的识别被调用之后 以建立一个系统能识别的类型列表
PreferredBufSize() PreferredBufSize() 指明传递给Specifies the size in bytes of the buffer passed to DoRecognizeL() 该函数是识别器的工作函数,它不是纯虚函数,但是必须实现
致命错误和断言 知道传递给User::Panic() 的参数类型,理解如何使其具有意义 理解在调试代码中使用_ASSERT_DEBUG 语句来检测编程错误的,通过在代码执行 流中插入致命异常 认识到 __ASSERT_ALWAYS 应当节省使用,因为它在发布的代码中也会测试语句 并在断言失败时引起代码致命错误
致命错误 当一个线程发生了致命错误 致命错误用于 不能从致命错误恢复 它停止运行 以一种最显著的方式突出编程错误 通过执行运行该线程以确保代码得到修改 而不是继续运行从而导致潜在的严重问题 不能从致命错误恢复 不像异常退出——致命错误不能被捕捉 致命错误是终点
致命错误 如果一个进程主线程中出现了致命错误 如果在第二线程中出现了致命错误 如果线程被认为是系统线程 这极少发生 整个进程将会终止 只关闭该线程 如果线程被认为是系统线程 即——对系统运行是必须的 该线程的致命错误会导致电话重启 这极少发生 因为Symbian OS上运行的系统线程代码是成熟的,经过良好测试的
致命错误 在电话硬件上 在模拟器调试构建中 开发者可以使用调试器 以及在Windows模拟器上的发布构建中 一个致命错误的最终结果要么是重启,要么是弹出“应用程序关闭”的消息框 在模拟器调试构建中 一个致命错误会在调试器上设置一个断点 即 “即时” 调试 开发者可以使用调试器 浏览整个调用栈以查看致命错误发生的地方 从而检查相应对象和变量的状态
致命错误 调用静态函数User::Panic() 在EKA2上 在EKA1 上 对当前运行的线程引发一个致命错误 一个线程可以对同一进程中的其他任何线程引发致命错误 通过获得RThread 句柄并用其调用RThread::Panic() 在EKA1 上 这个函数可以用对任何进程中不受保护的线程引发致命错误 这被EKA2 认为是不安全的
致命错误 EKA2的唯一场合 ... 是一个服务器线程对一个有错误行为的客户端引发致命错误 一个运行在用户进程中的线程能够对其他进程的另一个线程引发致命异常 是一个服务器线程对一个有错误行为的客户端引发致命错误 通过调用 RMessagePtr2::Panic() 方法
致命错误 User::Panic() 和 RThread::Panic() 有两个参数: 不需要跟踪到调试器中 一个致命错误类型字符串 一个整型错误码,它可以是任何数值,正数、零或者负数 不需要跟踪到调试器中 这些值对于开发者来说应当已经足以确定致命错误发生的原因
致命错误 致命错误字符串 致命错误只应被用作一种开发周期中去除编程错误的方法 应当很短且并且对于开发者而不是对用户具有描述性,——因为用户永远不会看见它们 致命错误只应被用作一种开发周期中去除编程错误的方法 例如通过在断言声明中使用它们 引起致命错误不能被认为是正确调试软件的有用功能
致命错误 下面是使用致命错误的不好例子,它向用户指出问题: 下面是使用致命错误的好例子 用以向开发者突出编程错误,它了调用Foo库的Bar类,传入不合法的参数 开发者能够明确那个方法被不正确的使用了,然后修正问题: _LIT(KTryDifferentMMC, "File was not found, try selecting another"); User::Panic(KTryDifferentMMC, KErrNotFound); // 没有用! _LIT(KFooDllBarAPI, "Foo.dll, Bar::ConstructL"); User::Panic(KFooDllBarAPI, KErrArgument);
致命错误 Symbian OS 细节可以在每个SDK的Symbian OS Library 中找到 有一系列具有良好文档描述的致命错误类别,例如: KERN-EXEC E32USER-CBASE ALLOC USER 以及相关联的错误值 细节可以在每个SDK的Symbian OS Library 中找到
断言 断言被用于 典型的 有一个专门针对调试构建的断言宏 同时用于调试和发布构建的宏 检查那些假定代码正确的假设 例如——期望的对象的状态,函数参数或者返回值 典型的 断言对语句进行评价 如果是失败,就挂起代码的执行 有一个专门针对调试构建的断言宏 __ASSERT_DEBUG 同时用于调试和发布构建的宏 __ASSERT_ALWAYS
断言 断言宏测试一条语句 该函数不是一定要硬编码为致命错误 如果评价结果是失败,它就调用传递给宏的第二个参数所指定的函数 但是不能仅仅是返回一个错误码或者异常退出它,而总是应当终止正在运行的代码并 标志处失败 最好产生致命异常
断言 断言帮助检测 在错误发生之前停止代码是有有意的 无效的状态或者错误的程序逻辑,这样可以修正代码 而不是返回一个错误值,因为它更容易跟踪bug
断言 在代码的发布构建中使用断言应当被仔细考虑 断言语句在大小和速度上都有开销 如果断言失败——它会用致命错误导致代码终止 结果是造成很差的用户体验
断言 这是如何使用调试断言宏的一个例子: void CTestClass::EatPies(TInt aCount) { #ifdef _DEBUG _LIT(KMyPanicDescriptor, "CTestClass::EatPies"); #endif __ASSERT_DEBUG((aCount>=0), User::Panic(KMyPanicDescriptor, KErrArgument)); ... // 使用 aCount }
断言 对于一个类或者代码模块更普遍的是定义: 例如 一个引发致命错的函数 一个致命错误类别字符串 一组致命错误枚举值 下面的枚举可以添加到CTestClass 中 这样可以不污染全局命名空间 enum TTestClassPanic { EEatPiesInvalidArgument, // 传递给EatPies()无效参数 ... // 传递给断言的枚举值 // 按照CTestClass 方法的顺序 };
Assertions 定义一个致命错误函数 要么作为类的成员 要么作为一个静态函数,它也位于实现类的文件中 EatPies() 中的断言可以这样写: static void CTestClass::Panic(TInt aCategory) { _LIT(KTestClassPanic, "CTestClass"); User::Panic(KTestClassPanic, aCategory); } void CTestClass::EatPies(TInt aCount) { __ASSERT_DEBUG((aCount>=0), Panic(EEatPiesInvalidArgument)); ... // Use aCount }
断言 使用一个可确定的致命错误描述符以及针对不同断言条件的枚举 值的好处 这对于调用使用了给定库的代码是部分有用的 是其可追溯性 因为开发者不能访问库的源代码 而只能访问头文件
断言 如果致命错误字符串是清楚和唯一的 开发者就应当能够定位引发致命错误的类 使用致命错误类别枚举来找到关联的失败 它被命名并有文档解释为什么断言失败
断言 具有副作用的代码 在调试模拟能够按照预期很好工作的代码 不应在断言语句中调用 但是在发布构建中,由于断言语句被预编译器移除 而其中可能潜在的包含编程逻辑的关键步骤 (这样可能会导致发布版本的代码存在错误)
断言 而不是使用 “浓缩的” 语句 // Bad use of assertions! 语句应当独立执行 再将它们的返回值传递到断言宏中 // Bad use of assertions! __ASSERT_DEBUG(FunctionReturningTrue(), Panic(EUnexpectedReturnValue)); __ASSERT_DEBUG(++index<=KMaxValue, Panic(EInvalidIndex));
致命错误,断言和异常退出 异常退出 例如 不可能 可以在异常条件下合理的发生 例如 内存耗尽 磁盘空间不足 或者缺少通讯链接 不可能 阻止一个异常退出的发生 代码应该实现优雅的恢复策略 总是使用TRAP 语句捕捉异常退出 See the earlier lectures on leaves and the cleanup stack for more about leaves on Symbian OS
致命错误,断言和异常退出 编程错误 (“bugs”) 可能有以下原因引起: 这些是持久的和不能恢复的错误 处理这个的机制是 矛盾的假设 未预料到的设计错误 真正实现的错误 这些是持久的和不能恢复的错误 应该由程序员检测并修改 而不是在运行时处理 处理这个的机制是 使用断言声明 当检测到错误时,它们用一个致命错误终止代码流的执行 致命错误不能被捕捉,也不能被温和的处理 程序员必须解决这个问题
System Structure : Part Two 进程间通信(IPC) 识别器 致命错误和断言