系统调用的开销

几乎所有OS的都是用了基于exception,同步的系统调用机制,利用处理器提供的exception功能进行用户到内核态的转换,并且让用户程序在系统调用结束返回后才继续执行。这种机制给性能带来了不小损失:

  • 直接原因:特权级切换,保存上下文,陷入异常这一套流程带来的开销。
  • 间接原因:执行上下文的切换对处理器的状态造成了“污染”。所谓“污染”,即分支预测的结果可能失效,TLB和cache可能需要被刷新,等等。

这就导致指令执行的速度(IPC)在系统调用结束后相当长的时间内都低于系统调用前的水平。通过测试可以看出,在系统调用频率较高的情况下,性能损失主要由于直接原因;在系统调用频率中等的情况下,间接原因是导致性能损失的主要原因。

Exception-less 系统调用

无异常的异步系统调用让用户态程序不用再同步地等待返回结果,带来了两个好处:

  • System call batching,在一次mode switch中可以处理多个系统调用,提高了时间局部性。
  • Core specialization,处理系统调用的核心可以和发起系统调用的用户态程序所在核心不同,从而保护了用户态和内核态的CPU状态,提高了空间局部性。

设计

  1. 用户态和内核态之间共享一系列memory page,称为syscall page,其每一条目对应一次系统调用。应用程序通过普通的内存操作将系统调用号、参数等信息写入某一空闲条目,标记其状态为submitted。kernel负责处理系统调用,完成后将返回值存入该条目,同时将状态设为done。
  2. 内核中有一系列syscall thread,它们只运行在内核态,唯一工作就是处理系统调用请求。它们只在用户线程无法取得进展(即等待系统调用)时被唤醒执行。

实现

作者在Linux中基于以上思想实现了FlexSC。

  • 包含flexsc_register()flexsc_wait()两个系统调用,它们本身是基于异常的系统调用,避免了bootstrap problem。

    • 进程在使用exception-less系统调用前,需要调用flexsc_register()完成初始化操作。主要操作包括在用户地址空间中映射一张或多张syscall page,以及为syscall page中的每一entry创建syscall thread。
    • 当线程因等待系统调用结果而无法取得进展时,它应该调用flexsc_wait()通知kernel。kernel应让用户线程睡眠,并唤醒其进程的syscall thread处理系统调用,当所有系统调用请求都被处理过,并且其中至少有一个系统调用完成(被处理过且未完成==因等待资源而阻塞),kernel就会唤醒用户线程。flexsc_wait()的设计是因为在异步系统调用下,操作系统无从得知用户程序何时无法取得进展从而需要睡眠(对同步syscall,可以在IO阻塞时令用户线程睡眠),因此必须让用户程序通知内核。
  • 理想情况下(并且是单核情况),每个进程只需要一个syscall thread,它不会睡眠,而是负责处理所在进程的所有系统调用请求。如果某一请求因等待IO而阻塞,它就去执行下一个请求,并在IO就绪是得到通知。

    鉴于实现难度,在实际实现中,为每个进程创建了一批syscall thread,数目等于syscall page能容纳的entry总数,以备所有系统调用都被阻塞的最坏情况。如果系统调用需要阻塞,该syscall thread就陷入睡眠;否则它会继续执行下一个系统调用。在任何时刻,同一进程同一核心上最多只有一个syscall thread处于可运行状态。

  • FlexSC为syscall thread实现了一个调度器,对于多核情形,syscall thread被允许调度到预先指定的处理器集合中的任何一个上执行。调度器如果发现当前选中的核心上已经在跑一个syscall thread了,就会选择下一个核心。这种情况下,一个进程最多有ncore个syscall thread在并发执行,需要对syscall page上锁。这就是一个进程需要多个syscall page的原因,这样提高了多核情况下多个syscall thread并发执行的效率。

FlexSC-Threads

作者提供了FlexSC-Threads库,可以让使用传统基于异常系统调用的应用程序不经任何修改,转而使用exception-less系统调用。它的主要思想是让M个用户态线程(协程)复用N个kernel可见的(用户态)线程,其中M远大于N。

  1. 在libc外面作为wrapper,所有通过libc的系统调用改为通过FlexSC-Threads。
  2. 对每个进程每个核心创建一个kernel-visible thread,绑定到该核心上运行。
  3. 用户态线程发起系统调用时,FlexSC-Threads(在用户态)完成exception-less 系统调用,让其阻塞,并切换到另外一个就绪的用户态线程。
  4. 如果没有就绪的用户态线程,FlexSC-Threads检查syscall page,唤醒那些等待的系统调用已经完成的用户态线程。
  5. 如果所有用户态线程的系统调用都在pending,FlexSC-Threads调用flexsc_wait()让整个kernel-visible线程睡眠。

作为优化,可以让每个kernel-visible thread拥有私有的syscall page,它们只被一个核心访问,不需要并发安全。

因为每个用户态线程都是独立推进的,它们提出的系统调用不需要按序执行,这就留下了给这些系统调用的执行(syscall threads)做调度的设计空间。比如用户态线程在Core 0上执行,而syscall threads可能在Core 1上执行它们的系统调用请求,这就保证了user和kernel都有更好的空间局部性。