高级语言编写kernel,鱼和熊掌不可兼得
source link: https://www.tuicool.com/articles/e26ZNvm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
本文基于OSDI18论文《The benefits and costs of writing a POSIX kernel in a high-level language》的理解整理而成。 从第1节到4节沿着论文作者思路介绍a)为什么要使用高级语言编写Kernel; b)使用Go编写Biscuit在开发效率,系统安全性和性能有哪些优缺点; c)最终Biscuit与Linux Kernel的性能对比分析。 最后第5节是本文作者对论文的总结: 基于C vs Go编写Kernel,结合安全性、性能和复杂性3方面数据,在追求安全性和系统创新前提下,Rust语言比Go更有优势。
论文:https://www.usenix.org/system/files/osdi18-cutler.pdf
ppt: https://www.usenix.org/sites/default/files/conference/protected-files/osdi18_slides_cutler.pdf
github: https://github.com/mit-pdos/biscuit
1. 问题背景
最初编写OS的语言只有汇编语言,但随着OS的复杂性增加,汇编语言慢慢暴露出它的不足。丹尼斯·里奇开发C语言,并编写Unix操作系统。从此C一战成名,几乎成为OS领域的唯一编程语言。
随着计算机硬件发展,CPU性能变强,内存容量极大提升,人们对性能的诉求通过硬件高速发展得到弥补,研究人员开发反过来思考是否可以使用安全的、高级编程语言开发操作系统。牺牲高性能,换取系统安全性和更好的开发效率。
论文作者使用Go语言设计Biscuit kernel(一个POSIX Kernel)作为案例,对比Go和C编写Kernel的开发效率,安全性和性能。
2. 动机:为什么使用高级语言编写kernel
高级语言提供较好的语法抽象、运行抽象,kernel开发更方便。高级语言比C更接近人类思考方式,易于理解。提供自动内存管理机制,让开发人员不用考虑复杂的内存管理,特别在多线程场景下对象的同步释放工作,也能避免释放后再使这个安全问题。语言支持多线程机制,让并行编程和同步更简单。
高级语言能减少CVE安全漏洞。C语言宪章思想是把自由交给程序员,相信他们知道自己在做什么事情。然而即使是非常有经验的老手,都无法避免C语言典型的缓冲区溢出,释放后使用,任意的类型强转等各种各样的问题。CVE数据库披露Linux Kernel在2017年有40个关于代码执行的安全漏洞,如果用高级语言则能彻底避免,或者部分减轻问题的影响。
高级语言让并发编程更容易。高级编程语言提供垃圾回收机制,并行编程时,无需考虑生命周期结束后多个线程之间的同步释放。
高级语言的缺点。垃圾回收减轻开发人员负担,但它不是没有代价,垃圾回收机制引入的开销和时延抖动。语言的安全性,自然在运行时增加安全检测,CPU运行开销增加。高级语言提供的runtime自身已引入一些关键性机制,比如内存管理,线程调度机制,开发人员在使用它开发kernel时,必须与这些机制兼容,造成kernel方案设计可选余地变少。
3. Biscuit设计与实现
从分析高级编程语言对Kernel的影响来说,不需要深入分析Biscuit设计与实现。所以这里只是粗略介绍Biscuit设计方案,重点体现为什么会有这样的选择,以及Go语言对Biscuit设计的影响。
3.1 Biscuit整体结构
Biscuit首先是个宏内核,提供部分POSIX接口,它架整与传统的POSIX宏内存没有太多区别,它的结构图如下:
Biscuit是POSIX内核,支持多进程和线程。 Kernel核心部分是Biscuit组件,由于采用Go语言编写,所以它之下还有Go runtime。 而Go runtime原来是运行在Linux用户态,为了弥补底层功能的缺失,论文作者实现一个比较轻量的shim层,满足Go runtime的功能依赖。
下面简单介绍Biscuit进程、线程模型,中断模型,文件系统,网络协议栈,垃圾回收器的实现方案。
3.2 进程和kernel Goroutine
Biscuit进程模型与其它POSIX内核没有太大的差异,支持fork, execve系统调用,每进程有单独的地址空间,通过硬件页表隔离,进程可以有一个或多个线程。
Biscuit用户态线程与内核态线程采用1:1的模型,即每个用户态线程都对应一个内核Goroutine(在Go运行环境和术语中,routine也称为线程)。用户态线程执行系统调用,或产生page fault后,都在陷入内核,委托对应的kernel Goroutine执行相应的系统调用和异常处理逻辑。
Biscuit的线程调度完全用go routine调度器掌管,在go runtime看来,用户态线程只是Goroutine跑在user mode而已。Go runtime的调度机制采用预抢占调度机制,在编译阶段预先生成抢占点,当Kernel Goroutine运到该抢点占时,就会发生调度。
Go提供Goroutine和调度功能,Biscuit无法逃脱Goroutine的约束。优点是快速开发Kernel功能,缺点是无法做精细化的调用策略控制和理论创新。
3.3 中断
Go runtime原先的设计是运行在用户态,没有中断概念一说,所以它在运行过程中不会关中断。所以Biscuit中断处理函数不能太复杂,比如加锁则会造成死锁。因此,Biscuit中断方案是中断线程化,设备中断触发后只是对中断线程做个标记,中断完成后才唤醒对应的中断线程(Goroutine),然后在进程上下文执行中断例程代码。
3.4 文件系统
Biscuit实现主要的POSIX文件系统访问接口,实现日志型文件系统,提供批处理能力,文件可靠性和性能有一定保证。实现ACHI磁盘驱动,该驱动使用DMA和MSI机制。
3.5 协议栈
实现TCP/IP协议栈,一款Intel PCIE网卡驱动,支持DMA和MSI机制,同时提供POSIX的socket接口。
3.6 垃圾回收
Biscuit复用Go runtime提供的垃圾回收功能,这意味着只要将一块内存给Go runtime管理,Biscuit直接向Go runtime申请各类kernel对象即可,也无须释放,只要无指针引用,垃圾回收器会在适当的时机对它做回收。借用Go语言的内存管理机制,大大简化了Biscuit的实现和代码量。
Biscuit利用Go 1.0 runtime提供的多核并行“标志-清除”垃圾回收器,减少垃圾回收过程对业务暂停时间。该时间与系统存活对象数量成正比,而与回收周期成反比。
3.7 Biscuit代码和实现
Biscuit kernel基本由Go语言编写,Go语言有27,583 行,汇编有1546,完全没有C代码,下图展示各组件的代码构成。
Biscuit实现58个系统调用,对于Linux程序nginx和redis已经够用。 Biscuit实现磁盘驱动和网卡驱动,需要访问硬件DMA地址空间,需要使用unsafe.Pointer 访问寄存器,网络报文,物理页内存,用户态内存,使用atomic package控制内存访问顺序。
Biscuit的一个设计原则是尽管不修改Go runtime代码,而runtime在运行过程加锁时并没有关闭中断,因此为了避免死锁,Biscuit在中断只是打上标记,中断退出后,再唤醒中断服务线程。
Biscuit的调度也完全由Go runtime掌管,无法实现优先级调用策略。在Goroutine上下文切换时,无法更换硬件页表,只能在返回用户前切换页表。线程在切换后,返回户用态前需要访问用户态内存时,使用软件查页表找到物理地址,然后软件权限检查,最后才内存访问。
4. 性能评估
论文作者开篇就提出使用高级语言开发kernel的好处和代价,设计Biscuit仅仅是作为实验从各维度评估Go vs C开发Kernel的好处和代价。评估分为以下几个方面:
-
Biscuit横向与其它项目相比,使用Go语言的特性分析,想说明使用高级语言能给Kernel提高开发效率
-
安全性,高级语言编写kernel能有效减少安全漏洞
-
Go语言高级特性给开发kernel带来怎样的性能耗损
4.1 Go特性在Biscuit使用情况
论文作者横向对比3个采用Go语言开发项目:Biscuit, Golang和Moby,分析项目中每1000行代码使用的特性数,结果如下图:
从图上可以看到3个项目较多使用Go的内存管理机制,避去复杂的对象生命周期管理。 使用Slice,String,Multi-return和Closure,更多像语法糖,提升代码生产率,以及减少编码出错; Defer方便处理资源释放。 Channel用于Goroutine线程同步数据。
4.2 Biscuit能减少哪些CVE漏洞
CVE数据库披露Linux Kernel社区在2017年一共有65个公共漏洞,其中11个bug不确定在Biscuit是否能避免,还有14个bug属于逻辑问题,在Biscuit也会出现。最后剩下的40个bug与内存访问相关,有释放后使用,重复释放,下标溢出访问这3类。相比之下,Biscuit比Linux Kernel有更好的安全性,因为Go语言能在运行防止不安全内存访问,即时发生运行错误,避免进一步攻击。
4.3 Biscuit性能分析
论文作者将Linux Kernel不太相关的功能组件关闭(比如cgroup,随机地址化,透明大页,零拷贝,ftrace, kprobe),让Biscuit和Linux Kernel执行路径大致相同。运行nginx和redis两个服务程序进行性能对比,结果如下:
client和server采用ping-pong测试模式时,C语言比Go性能高15%,专门针对Page fault做性能测试时,发现C比Go性能高5%。对Go语言来说,由于垃圾回收的存在,对性能影响也不可忽略,垃圾回收占CPU开销的1%~3%,致命的是它影响业务时延,造成业务单次请求的最大时延在574ms。
5. 思考和借鉴
论文作者使用Go语言开发Biscuit仅仅做为一个实验,帮助OS设计者去理解、剖析高级语言编写Kernel需要考虑哪些维度。高级编程语言,提供更好的抽象能力,类型安全和内存安全,此外像Go语言还提供很多非常强大的功能:协程、垃圾回收、多线程同步原言,大大简化kernel开发工作量。Biscuit项目引入Go语言,因其类型安全和内存安全,系统运行更安全,性能下降15%,用安全性换取性能,在安全优先的场景下是值得的。此外,Go语言提供Goroutine机制和带垃圾回收器的内存管理方式,却让Biscuit kernel在线程调度上强依赖于Go runtime的调度机制,无法做调度算法上的创新,垃圾回收的内存管理方式,让Biscuit在内核内存耗尽情况下,也大费力气解决, 完全无法掌整个kernel的核心设计,这是Go语言开发kernel的不足 。
是不是所有高级语言都与Go语言大同小异的,其实还不是,Rust语言是最近系统级编程语言的新星。Rust与Go不同,它不支持垃圾回收机制,它的设计哲学就认为垃圾回收很难做到高性能。Rust没有垃圾回收功能,并不意味内存就不安全了,它提供所有权,借用和引用机制,让开发人员编写内存安全的程序,并支多线程并发安全访问。 Rust的好处是提供类型安全和内存安全,多线程内存安全访问机制,但没有了Go的runtime的约束,让Kernel开发人员聚焦OS架构和方案创新 。
采用C语言开发Kernel,尽管可以获得很高的性能,但不可避免地引入很多安全问题。而采用像Go一样,带垃圾回收,协程调度等厚重的语言机制,尽管可以高效编写kernel,但也需要为这些机制付出代价。而Rust这类的高级语言,提供类型安全和内存安全机制,并没有runtime的束缚,让Kernel开发有更大创新空间。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK