最后我想讨论我们在最开始问过的一个问题,你应该在一个新内核中使用高级编程语言吗?
与其直接回答这个问题,我在这页有一些我们的结论和一些考虑。或许你们该回退一步,并问自己,你们更喜欢哪种方式?你们是喜欢像在实验中用C写XV6,还是喜欢使用类似Golang的高级编程语言。更具体的说,你们更想避免哪类Bug?或许在这节课的过程中想想你们遇到过什么Bug?我想听听你们的体验,你们是怎么想的?切换到高级编程语言会不会改变你们的体验?
一些学生介绍自己的体验,有说C好的,有说C不好的,略过。
当然,我们不会将XV6改成Golang或者任何高级编程语言。具体原因刚刚一些同学已经提到了,Golang还是隐藏了太多细节,这门课的意义在于理解系统调用接口到CPU之间的所有内容。举个例子,Golang隐藏了线程,我们并不想隐藏线程,我们想要向你解释线程是如何实现的。所以接下几年,这门课程还是会使用C语言。
但是如果你要实现一个新的内核,并且目标不是教育你的学生有关内核的知识,目标是写一个安全的高性能内核。你可以从我们的研究中得出一些结论:
- 如果性能真的至关重要,比如说你不能牺牲15%的性能,那么你应该使用C。
- 如果你想最小化内存使用,你也应该使用C。
- 如果安全更加重要,那么应该选择高级编程语言。
- 或许在很多场景下,性能不是那么重要,那么使用高级编程语言实现内核是非常合理的选择。
Cody、Robert和我在实现这个项目的过程中学到的一件事情是,任何一种编程语言就是编程语言,你可以用它来实现内核,实现应用程序,它并不会阻止你做什么事情。
学生提问:我很好奇你们是怎么实现的Biscuit,你们直接在硬件上运行的Go runtime,具体是怎么启动的?
Frans教授:这里有一层中间层设置好了足够的硬件资源,这样当Go runtime为heap请求内存时,我们就可以响应。这是Go runtime依赖的一个主要内容。
(中间一些无关问题跳过)
学生提问:我知道你们实现了一些Go runtime会调用的接口,因为你们现在自己在实现内核,所以没有现成的接口可以使用。你们是全用汇编实现的这些接口吗?还是说有些还是用Golang实现,然后只在必要的时候用汇编?
Frans教授:这就是Biscuit中1500行汇编代码的原因,它会准备好一切并运行Go runtime。有一些我们可以用C来实现,但是我们不想这么做,我们不想使用任何C代码,所以我们用汇编来实现。并且很多场景也要求用汇编,因为这些场景位于启动程序。
我们的确写了一些Go代码运行在程序启动的最开始,这些Go代码要非常小心,并且不做内存分配。我们尽可能的用Golang实现了,我需要查看代码才能具体回答你的问题,你也可以查看git repo。
学生提问:我有个不相关的问题,Golang是怎么实现的goroutine,使得它可以运行成百上千个goroutine,因为你不可能运行成百上千个线程,对吧?
Frans教授:运行线程的主要问题是需要分配Stack,而Go runtime会递增的申请Stack,并在goroutine运行时动态的增加Stack。这就是Prologue代码的作用。当你执行函数调用时,如果没有足够的Stack空间,Go runtime会动态的增加Stack。而在线程实现中,申请线程空间会是一种更重的方法,举个例子在Linux中,对应的内核线程也会被创建。
学生提问:goroutine的调度是完全在用户空间完成的吗?
Frans教授:大部分都在用户空间完成。Go runtime会申请m个内核线程,在这之上才实现的的Go routine。所有的Go routine会共享这些内核线程。人们也通过C/C++实现了类似的东西。
学生提问:C是一个编译型语言,所以它可以直接变成汇编或者机器语言,它可以直接运行在CPU上,所以对于XV6来说就不用加中间层代码。但是我理解Golang也是一种编译型语言,所以它也会变成汇编语言,那么为什么还要中间层(位于机器和Go runtime之间)?XV6有这样的中间层吗?为什么有一些事情不能直接编译后运行在CPU上?
Frans教授:好问题。Go runtime提供了各种你在XV6中运行C时所没有的功能。Go runtime提供了线程,提供了调度器,提供了hashtable,提供了GC。举个例子,为了支持GC,需要一个heap来申请内存,通常是向底层的操作系统来申请内存作为heap。这里说的中间层Go runtime需要用来完成工作的相应功能(比如说响应内存申请)。
学生提问:我们不能直接将runtime编译到机器代码吗?
Frans教授:Runtime会被编译到机器码,但是当你运行Go代码时,有一部分程序是要提前运行的,这部分程序需要在那。即使C也有一个小的runtime,比如printf就是C runtime的中间层的一部分,或者字符串处理也是C runtime的一部分,它们也会被编译。C runtime有一些函数,但是这个runtime是如此之小,不像Go runtime需要支持许多Go程序所依赖的功能。
学生提问:看起来这里的中间层像是一个mini的系统层,它执行了一些底层的系统功能。
Frans教授:是的,或许一种理解中间层的方法是,XV6也有一个非常非常小的中间层。当它启动的时候,它做的第一件事情是分配一些Stack这样你才能调用C的main函数。你可以认为这一小段代码是针对XV6的中间层。一旦你执行了这些指令,你就在C代码中了,然后一切都能愉快的运行。Go runtime的中间层稍微要大一些,因为有一些功能需要被设置好,之后Go runtime才能愉快的运行。