erlang是怎么调度的

来源:互联网 发布:淘宝买家可以贷款吗 编辑:程序博客网 时间:2024/05/16 19:07
    原创翻译文章,转载请注明出处http://blog.csdn.net/erlib 作者Sunface

 

   在这篇文章中我会说明为什么erlang和其他绝大数语言的runtime是不同的,还会说明为什么它会为了更低延迟而牺牲一点高吞吐量。

   erlang跟其他语言Runtime不一样的地方在于它的目标是不同的,这就是为什么Erlang在少数进程的情况下表现的不如其他语言但是在很多进程的情况下表现的很好。

   不时的都会有人问erlang是如何调度的,这篇文章是调度原理的缩简版本,主要是描述erlang如何操作它的进程的。注意这里我使用的是erlang R15规范,如果你是来自未来的读者,那么也不用担心 ———虽然可能会改变很多,但是事情往往朝着更好的地方发展。

 

    对于操作系统而言,erlang一般都是每个CPU内核都有一个线程在运行,每个线程都有一个调度器,这是为了确保机器的所有CPU内核都能为erlang系统工作,CPU内核会通过+sbt flag绑定到调度器上,这就表明调度器不会在一个内核跳到另外一个内核(0它只会工作在现代操作系统中),这个说明erlang系统知道各个处理器的排列和内在关系,这是很重要的,因为缓存和miration times等原因,在很多情况下+sbt flag可以提升你的系统的运行速度,在有些时候甚至会提升很多很多。

    +A flag 为异步线程池定义了多个异步线程,这个线程池可以被drivers使用,用来阻塞某个操作,而调度器在其中一个线程池被阻塞的时候就依然能做一些有用的工作。最值得注意的是线程池是被文件驱动用来加速文件I/O操作,而不是网络I/O。

     上面的描述其实是一个粗浅的对操作系统内核的描述,我们仍然需要提出erlang进程的概念,当你调用spawn的时候,新进程就被创立了,被分配自己的用户态控制块,这通常是

600+bytes,对于32位和64位系统分配的空间也是不同的,可运行的进程被放置在调度器的队列中,稍后当进程获得时间片的时候就会运行。

   在深入单个调度器前,需要先说明下migration是怎么工作的,在有些情况下(Every once in a while)进程通过调度器的migration是一个非常复杂(intricate)的过程,探索(heuristic)的目的是为了让多个调度器负载均衡,这样所有的内核都可以完全得到利用(utilize).但是算法同样需要考虑是否有足够的工作来另外启动一个新的调度器,如果没有,那最好让调度器保持关闭因为线程并没有事情做,反之也意味着内核可以进入power save mode,erlang会帮你节省power :P.在脱离工作后,调度器也可以悄悄的工作。

   重要:在R15中,调度器是通过"lagged" fashion启动和关闭的,这说明了erlang/OTP 认为启动或者停止一个调度器是非常昂贵的所有只有在需要的时候才这么做。假设调度器目前没有任务做,它将继续运行(spin)一小会儿希望能有新的任务到来而不是立刻进入睡眠,如果新任务来了,它会很快就被处理。另一个方面,你不能使用工具例如top(1)或者OS kernel来度量你的系统的运行效率,你必须用erlang系统的内部方法来度量,很多人不正确的认为R15比R14烂就是因为这个原因。

 

   每个调度者运行着两类作业: 进程作业和端口作业。它们运行的时候也带优先级,类似于操作系统内核。我们可以将某个进程标识为高优先级、低优先级或其它优先级。进程作业执行的是进程。端口作业处理的是端口。Erlang中的“端口”是系统与外界通信的机制,文件、网络socket、通往其它程序的管道,都是端口。程序员可以通过加入“端口驱动”,以支持新类型的端口,不过那需要写C代码。调度者还可以轮询网络socket,从中读取数据。

   普通进程和端口进程都有2000个削减运算(reduction budget),任何系统操作都会消耗这个预算, 包括循环调用,内置函数调用,垃圾回收,ets的读取,发送消息(接受者的邮箱信息越多,发送的成本越高).这真是非常普遍的(pervasive). 即便erlang的正则表达式是用C写的,  长时间运行它依然对你不利并会有线占用消耗几个限制次数. port也是. 在ports上做io操作消耗限制,发送分布消息也是. 大量的限制消耗以保证任何进程都有一定的运行次数.

   由于以上的原因,我认为Erlang是真正执行抢占式多任务处理的语言之一,也是正确理解软实时概念的语言之一。同时,Erlang对时延看得比吞吐量更重,这在编程语言里也是不多见的。 更精确地来说,抢占,指的是调度者可以强制让某个任务停止执行。所有基于协作的语言和系统,包括Python、Node.js、LWT(Ocaml)等等,都无法做到这一点。更有趣的是,即使Go(golang.org)和Haskell(GHC)也不完全是抢占式的。Go只在通信时切换上下文,因此只需一个密集的循环即可独占某个核心。GHC则是在内存分配时切换(在Haskell程序中十分常见)。这些系统的问题在于,对核心的独占会影响整个系统的时延,大家可以想象一下在这些语言里执行数组操作的情形。

  Erlang是为保证低时延和软实时而精心(meticulous)打造的。2000的递减量非常短,因此会导致大量的上下文切换。而且,将需要长时间运行的BIF打碎成中型运算,也非常昂贵。但这保证了Erlang系统在加入更多任务时,性能的下降是平稳的。在Ericsson这样需要低时延的公司里,Erlang系统是无可替代的。不能指望换成一种 面向吞吐量的语言,还能得到这么低的时延。你得自己去搞了。坦白地说,如果你确实需要低时延,那么不选择Erlang真的是很奇怪。

 [1] 《多核心处理器中Erlang虚拟机可扩展性的特征》  http://kth.diva-portal.org/smash/record.jsf?searchId=2&pid=diva2:392243
[2]  http://en.wikipedia.org/wiki/Preemption_(computing)
[3]  http://en.wikipedia.org/wiki/Real-time_computing

[n1] 进程堆是独立进程的,因此一个进程并不能影响其它进程的 GC时间。 
[n2] 本节也说明了为什么我们必须避免长时间运行的NIFS。他们不是每个默认抢占,也不是他们触发减数计数器。因此,他们会使系统延迟。 
[n3] 设想一个单核的情况,多核依靠着核心数量近似的消化”这些问题,但问题仍然存在。

原创粉丝点击