2.2.1 ABI版本控制
核心ABI是强版本,通过在每个程序中识别ABI的版本信息,Singularity能够为系统维护和升级提供更好的兼容性。
进程中的代码是在ABI接口汇编中被编译的,这个ABI接口汇编是在一个特定版本的命名空间中的。例如,Microsoft.Singularity.V1.Threads是一个包含线程相关功能的命名空间,它是ABI的第一个版本。进程源代码的命名空间是根据所需要的ABI版本命名的。进程二进制代码包含对特定版本ABI的元数据引用。
在安装时,仅当目标机器支持特定版本的ABI,程序才会被安装。如果是这样的话,ABI接口部件是被一个实现部件替换的,实现部件为系统内核版本提供一个特定版本ABI的进程端的实现。最简单的实现把内核ABI的运行时调用转换为对内核静态方法的直接调用。然而,一个更先进的Singularity系统能够把早期版本的命名空间变成一个包含兼容函数的库。作为另一种选择,兼容代码能够在内核中运行,因为内核能够很容易地支持特定命名空间中的多ABI实现。
内核ABI的第一个版本包含126个入口点(附录A列出了ABI中的方法)
2.2.2 调度器
Singularity支持编译时可替换调度器。我们实现了四个调度器,它们是Rialto调度器[参考32],一个不严格的多资源调度器,Round Robin调度器(Rialto调度器的一个退化版本),最小潜伏期的Round Robin调度器。
最小潜伏期的round-robin调度器是专门为大量线程进行频繁通讯而准备的,调度器维护着两个可运行线程列表。第一个列表是未阻塞列表,在这个列表中,存放的是最近变成可运行的线程。第二个列表是优先列表,在这个列表中存放着被优先占用的(pre-empted) 线程。当系统要运行下一个线程时,调度器从阻塞列表中以先进先出的顺序移出线程。如果未阻塞列表是空的,调度器会从优先列表中移出下一个线程。无论何时, 一旦一个调度计时器中断被触发,所有阻塞列表中的线程都会被全部移入优先列表的末尾,紧跟着计时器触发时正在运行的那个线程后面。阻塞列表中的第一个线程 会被调度,并且调度器计时器会被重置。
使 用两个调度列表策略的结果是对于那些刚被消息唤醒的线程很有好处,仅仅是作一点点工作,发送一个或者多个消息给其他进程,然后阻塞,并等待下一个消息,这 是进行消息循环的最常见动作。为了避免重置调度计时器所造成的性能损失,阻塞列表中的线程会继承阻塞它们的线程的调度量。当与双列表策略配合使用时,量继 承是十分有效的,因为Singularity会在仅仅394个时钟周期内从一个线程切换到另一个。
2.3 进程
Singularity系统运行在唯一的虚拟地址空间中。虚拟内存硬件用于保护页面,例如,通过映射地址空间的前16k地址来捕获空指针应用。在Singularity系 统中,地址空间按逻辑被分为这几个部分:内核对象空间、每一个进程的对象空间、通道数据交换堆。一个更容易被接受的设计思路是内存独立不变:跨对象空间指 针仅指向交换堆。特别值得注意的是,核心没有进入进程对象空间的指针,也没有一个进程有另一个进程对象的指针。该不变性保证了每一个进程能够在没有其他进 程帮助的情况下进行垃圾收集和终止操作。
内核会通过充分分配内存来创建一个进程,并从PE格式文件中加载一个可执行映像。Singularity会执行重定位和修正,包括连接内核ABI函数。内核通过创建一个在映像入口点(image’s entry point)运行的线程完成一个新的进程的启动,在这个位置的代码是可被信任的线程启动代码,它们会调用堆栈和页面管理者来初始化进程。
一 个进程通过启动一个内核页管理器来获得一个额外的地址空间,该管理器会返回一些全新的、未分配的页。这些页不需要与进程的现存地址空间相邻,因为垃圾收集 器不要求地质空间相邻,虽然他们可能存在相邻区域用于存储一些比较大的对象或数组。除了保存进程代码和堆数据的内存以外,在一个进程中,每个线程都有一个 堆栈,能够访问交换堆。
Singularity使用相连堆栈(linked stack)来自顶向下减少线程的内存,这些堆栈会根据需要添加4k或4k以上大小的非相邻块。Singularity编译器执行一个静态的内部过程分析来优化溢出测试的性能[参考51], 每个被编译器插入的检查是可信任代码,它可以访问系统数据结构、重新在进程对象空间中驻留、判断当前堆栈块的空间量。在已运行的线程压入新栈的帧之前,由 于新栈的帧可能会导致当前堆栈块的溢出,可信任的代码会调用一个内核方法,该方法可以保证中断和调用页管理器来分配新堆栈块的动作不被执行。这些代码也会 在块中初始化第一个栈的帧,这个帧位于运行模块和它的被调函数之间,用来调用块的未连接例程(segment unlink routine),这些例程会收集那些被弹出的栈块。因为所有的进程运行在x86的Ring0层,当前栈块必须总能留下足够的空间来让处理器保存一个中断或者异常帧(exception frame),这是在句柄切换到专用的中断栈之前发生的。
在Singularity中,交换堆是用于提高通讯效率的,保存在进程间传递的数据。交换堆不是收集到的垃圾,但作为替换,它是用一个引用来跟踪叫做“区域”(Regions)的内存的使用块数。一个进程可以通过一个叫做”分配器”(Allocation)的结构来访问区域。
分配器也可以驻留在交换堆中,交换堆允许它们在进程中传递,但每一个一次只能拥有并且访问一个进程。多个分配器只能以只读方式访问一个区域,并且,分配器可以拥有不同的基础(base)和绑定(bound),可以为数据提供两种不同的视图。例如,网络堆栈中的协议处理代码能够在不拷贝的情况下处理包含已封装协议头(encapsulated protocol headers)的包。一个区域跟踪指向它的分配器的数量,当该引用计数变为零时,它们会进行收集操作。Singularity编译器会通过分配器记录(Allocation Record)隐藏额外的指向级别,这是通过把强类型引用放入一个区域实现的,它还会自动生成代码来废弃一条记录。
一个进程可以创建额外的线程。运行在进程中用于创建线程对象的不可信(但已验证的)代码会用已提供的函数初始化这些线程,并且在系统运行时进程表中使用一个未使用的槽来存储线程对象。这些代码会调用ThreadHandle.Create,并传递线程表索引值给Create函数。这些内核方法会创建一个线程上下文来维持寄存器状态,整理初始化堆栈帧(initial stack frame),同时更新它的数据结构,它的返回值会返回给进程,这个进程运行时会调用ThreadHandle.Start方法来安排线程任务。当线程启动时,它会在内核和运行时代码中执行,这些代码是用来调用进程入口点的,同时在运行时进程表中传递线程索引。进程启动代码会在线程对象
中调用启动线程执行的函数。
在整个进程和线程的创建过程中,内核仅知道进程中的一个地址,即线程启动代码的地址,该地址位于进程入口点,就像内核ABI方法一样,它是不可以被重新定位的。
2.4 垃圾收集
垃圾收集是大部分安全语言所必须具有的功能之一,因为它能够防止内存收集时的错误,这些错误可能推翻安全契约。在Singularity中,内核和进程对象空间是可进行垃圾收集的。
大多数垃圾收集算法和经验指出没有一个垃圾收集器可以适用于所有的系统或应用程序代码。Singularity的架构简化了这个算法、数据结构和每个进程的垃圾收集执行,因此你可以用它来适应每个进程中的行为代码,以便在没有实现全局一致的情况下运行。Singularity有 四个方面的特性可以满足这样的需求:每个进程是一个封闭的环境,它有自己的运行时支持;指针不会越过进程或内核边界,因此收集器不需要考虑跨空间的指针; 通道中的消息不是对象,所以内存分配中的一致性仅对消息和其他在交换堆中的数据是必要的。内核控制内存页分配,它可以为资源的一致分配提供连接。
Singularity运行时系统目前支持5种类型的收集器——通用半空间收集器(generational semi-space)、通用滑动紧凑收集器(generational sliding compacting)、一个可以对前两个进行适配组合的收集器、标志清除收集器(mark-sweep)和并发标志清除收集器(concurrent mark-sweep)。我们目前对于系统代码使用的是最后一个收集器,因为它在收集时仅需要很少的停滞时间。该收集器为每个线程提供了一个隔离的自由列表,在通常情况下这会导致无法同步。在分配时,垃圾收集器会被触发,并且它将在独立集合线程(independent collection thread)中运行的,这样可以保证对象的可访问性。在收集时,收集器会暂停每个线程来扫描它的堆栈,对于典型堆栈(typical stacks),它会产生一个小于100微妙的暂停。这种收集器比非并发收集器位于更上层的位置,所以在应用中,我们建议使用简单的非并发标志收集器。
每个SIP有它自己的收集器,它们单独承担了各自对象控件中对象收集职责。从垃圾收集的观点看,当一个控制线程进入或者离开一个应用程序或内核时,在传统的垃圾收集环境中,这与处理一个调用或者从本地代码(native code)中调用回调函数是一样的。对于不同的对象空间,垃圾收集能够完全独立地安排和运行。如果一个应用程序雇佣了能够暂停整个世界(stop-the-world)的收集器,可以认为线程相对于应用程序空间而言,处于停止状态,即使它是因为内核调用而运行在内核对象空间中的也是一样。然而,线程在返回结果给应用程序进程空间时,会有一个集合的延迟,这时它是真的处于停止状态。
2.4.1 堆栈管理
在垃圾收集环境中,一个线程堆栈中包含可能成为收集器根节点的对象引用。内核级调用是在用户线程堆栈中被执行的,并且可以在堆栈中存储内核指针。一看就知道,在创建跨进程指针是违背内存独立不可变性的,而且还会导致用户级和内核级垃圾收集的混淆。
为了解决这个问题,Singularity划定了每个空间栈帧的界限,所以垃圾收集器不需要管其他空间的引用,在跨域(进程到内核或内核到进程)调用中,Singularity会在堆栈的一个特殊结构中保存调用对象保存着的寄存器,这也被认为是一种跨域调用。这些结构会标记属于每个对象空间的堆栈区域边界。而调用内核ABI时,不需要传递对象指针,所以垃圾收集器会跳过其他空间的帧。
这些边界限制符也可以很干净的终止进程。当杀死进程后,它的所有线程将被终止,内核会为每一个线程抛出一个异常,该异常会被忽略,但同时也会对进程堆栈帧进行收集操作。
2.5 通道
Singularity进程是通过在通道中发送消息实现外部通讯的,通道是双向的,它是位于两个进程之间的类型化连接。消息其实是一个标记集合的值,或者说是一个交换堆中消息块,它从一个进程发送,并由另一个进程接收。通道是通过约定(contract)实现类型化的,约定会指定一个消息格式和一个通道中有效消息的序列(见4.1节)。
通过调用一个约定的静态NewChannel方法,进城可以创建一个通道,NewChannel方法返回通道的两个端点(endpoint),两个端点的类型是不同的,一个是输出,一个是输入,这是通过输出参数(output parameters)获得的:
C1.Exp importCh;
C1.Imp exportCh;
C1.NewChannel(out importCh, out exportCh)
进 程能够在一个存在的通道中,从两个端点的任何一段向另外一段传递。接收终端的进程拥有一个到另一个进程的通道,另一个进程会响应该终端。例如,如果应用程 序的进程需要与一个系统服务通讯,应用程序会创建两个端点,并且把包含一个终端的请求发送给系统命名服务器,系统命名服务器会把该终端指向另外一个服务, 最后在进程和服务之间创建一个通道。
通道上的发送动作是异步的。A receive synchronously blocks until a specific
message arrives。 通过使用语言特性,一个线程会等待通道的消息集中的第一个消息,或者从不同通道等待消息特定集。当数据在通道上传递时,数据的拥有权会从发送进程那一方向 接收进程传递,而发送进程不会保留数据的引用。这种拥有权的不可变性是由语言和运行时系统强制执行的,用于三个目的。第一个目的是防治进程间共享,第二个 目的是通过消除指针别名使静态程序分析变得更为简单。第三个目的是通过提供消息传递文法允许复杂性实现,而这种文法是通过拷贝或指针传递实现的。
2.5.1 通道实现
通道终端 (endpoint) 和 跨值通道值传递是在交换堆中进行的。终端是不能在进程对象空间中驻留的,因为他们是在通道间传递的。类似地,一个通道上传递的数据无法驻留在对象空间中, 因为它可能违反内存独立不可变性。消息的发送者通过在接收终端中存储一个消息的指针实现拥有权的传递,存储的位置是由当前消息交换堆的状态决定的,这个方 法允许“零拷贝”输入输出堆栈( IO Stack )的实现。例如,在多通道中,通过协议堆栈和进入应用程序进程可以传递磁盘缓冲和网络包,而不是用拷贝。