`
LeslieWei
  • 浏览: 13587 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Erlang虚拟机源码阅读笔录(三)虚拟机的进程调度

阅读更多

3. Erlang虚拟机的调度

在这一部分中我们来重点分析Erlang虚拟机的调度策略。

由第一分部的介绍可以得知,在ERTS_SMP模式中,erl_start()函数在创建好第一个进程后最后调用的两个函数分别为erts_start_schedulers()erts_sys_main_thread()。在单核模式下,erl_start()函数在调用set_main_stack_size()进行一些栈区设置后调用了process_main()函数,然后进入了单核模式下的进程切换调度。这里我们重点讨论在ERTS_SMP模式下Erlang虚拟机的调度策略。

进入erts_start_schedulers()函数的定义(该函数的定义在otp_src_R15B02/erts/emulator/beam/erl_process.c文件中)erts_start__schedulers()函数通过前面early_init()函数获取的cpu数目来为每个cpu创建一个调度线程,然后创建一个附属线程。调度线程的入口函数是sched_thread_func(),这个函数的定义也在erl_process.c文件中,该函数设置调度器初始化时使用的一系列回调函数,然后进行初始化并等待其他调度线程完成初始化,然后每个调度线程最终也会调用process_main()。而process_main()函数在整个erlang虚拟机的运行过程中会被调用两次,第一次调用是进行emulator的初始化工作,首先被init_emulator()调用,init_emulator()又被erl_init()调用,erl_init的调用关系已经在第一部分中被说明,在该次调用中process_main()在进行一些指令集所需的寄存器参数定义后跳转到init_emulator程序段中,如图3.1




 
 

 

3.1

       在这部分代码中,process_main()首先初始化了emulator调用的error_handler和一些其他的BIF的入口函数,然后初始化了所有BIF的函数导出表,当这些工作都完成后,process_main()函数的第一次调用结束并return;如图3.2所示:




 
 
3.2

process_main()函数的第二次调用是被sched_thread_func()调用,在这次调用中process_main()函数进行真正的调度工作,这部分的代码十分的长,大概有4000多行的代码量,因此这个函数是调度的核心,在这次调用中process_main()将进入一个死循环而永远不会返回,因此在真正的调度工作中,process_main()函数没有返回值。

process_main()函数最先调用的是schedule()函数,schedule()通过传入的进程信息c_p和进程已经执行了的reds来调度选择下一个要执行的进程(有可能还是原来的那个c_p,也可能会发生切换)

schedule()函数的定义和process_main()位于同一个文件,该函数首先通过传递的进程信息获取到对应的调度器(如果c_p = NULL,说明是进行第一次调度,schedule()通过调用erts_get_scheduler_data()从异步队列里获取一个调度器,并且获取与调度器绑定的运行队列,如果c_p != NULL,说明不是第一次调度,那么直接从进程中获取与之绑定的调度器和运行队列)。获取了调度器后,schedule()将计算每次reds的增量以及整个队列的reds增量,然后通过profile_runnable_proc()函数来分析当前的执行进程是继续执行还是需要被调度。

profile_runnable_proc()分析好进程的执行情况后会设置c_pstatus,接着进入一个swith(p->status)的程序段中。如果当前进程执行状态经过分析变成退出状态,将调用handle_pending_exit()进行进程的善后处理并退出。除此之外即为reds用完但是进程尚未退出的情况,调度器会将当前进程调用handle_pending_suspend()函数进行挂起。

接下来调度器需要选择一个新的进程用于执行,首先,schedule()函数会先调用check_balance(rq)检查该调度器绑定的runqueue是否是平衡状态,如果进程数的低于或者高于了迁移的阀值,就调用immigrate()函数进行迁移,Erlang的进程共有四个优先级,如下所示:

/* process priorities */

#define PRIORITY_MAX          0

#define PRIORITY_HIGH         1

#define PRIORITY_NORMAL       2

#define PRIORITY_LOW          3

#define ERTS_NO_PROC_PRIO_LEVELS      4

其中PRIORITY_MAXPRIORITY_HIGH各有一个优先级队列,PRIORITY_NORMALPRIORITY_LOW共用一个优先级队列,一般情况下,PRIORITY_LOW优先级进程只有在调度特定个数的PRIORITY_NORMAL后才会被调度,这种机制保证了PRIORITY_NORMAL优先级高于PRIORITY_LOW优先级被执行,但在某些情况下会引起优先级反转。

每个runqueue和一个scheduler进行绑定,每个runqueue又包含了三个优先级队列:一个PRIORITY_MAX队列,一个PRIORITY_HIGH队列,一个PRIORITY_NORMALPRIORITY_LOW共用的混合队列,在进行任务迁移的时候,immigrate()中有个for结构,分别对每种优先级队列进行任务迁移。关于时间片调度算法和Erlang任务迁移算法,这里不做详细说明,请参见论文:Characterizing the Scalability of Erlang VM on Many-core Processors。平衡运行队列中的进程数量后检查运行队列的状态,如果运行队列状态变为ERTS_QUNQ_FLG_SUSPENDED,将调用suspend_scheduler()函数将当前的调度器挂起。如果调度器未被挂起,接着检查runqueue是否有任务,如果没有任务,将调用empty_runq()清除runqueue的状态标志,然后调用try_steal_tast()函数拉取任务,如果拉取成功则调用goto continue_check_activites_to_run跳转到上一个检查点,重新检查runqueue的运行状态,然后按状态调度runqueue。如果没有拉取成功,则调用scheduler_wait()等待系统IO任务,scheduler_wait()函数将调用erl_sys_schedule()阻塞在系统IO上,其实erl_sys_schedule()unix系列平台下的定义就是一个poll模型的封装,定义如下:

void

erl_sys_schedule(int runnable)

{

#ifdef ERTS_SMP

    ERTS_CHK_IO(!runnable);

#else

    ERTS_CHK_IO(runnable ? 0 : !check_children());

#endif

    ERTS_SMP_LC_ASSERT(!erts_thr_progress_is_blocking());

    (void) check_children();

}

 

如果runqueuelen != 0,那么scheduler先调度runqueue中的PRIORITY_MAX优先队列(通常为系统任务),然后调度PRIORITY_HIGH优先级队列(通常为port任务),然后调度一般任务。当选择好一个任务进行调度后,schedule()函数返回选择的新进程的指针给process_main()

这时,process_main()函数通过schedule()计算得到了下一个需要执行的process的指针,接下来需要将进程的中断现场恢复到当前的上下文环境中,首先将进程的寄存器参数恢复到每个寄存器中,然后调用SET_I(c_p->i)这个宏定义,来设置register变量II代表的是下一条即将执行的Erlang虚拟机指令threaded-code。然后用next指针指向I寄存器中保存的threaded-code地址,经过一系列的跟踪调用后调用Goto(next)这个宏来执行下一条threaded-code

threaded-code指令段的定义在process_main()中,是以宏OpCase(OpCode)开始的程序段,经过调度的进程后调用Goto(next)将跳转到具体的threaded-code段中继续执行。这里以send原语来举例说明,当Erlang进程调用Pid ! MsgObj原语的时候,将跳转到lb_send这一threaded-code段中,在这个内建函数中首先计算进程的reds消耗(c_p->fcalls = FCALLS - 1),然后将寄存器r0的值赋给了reg[0]register变量r0此时保存的值为要发送到的进程idx(1)寄存器保存的是要发送的消息体的地址,然后将调用erl_send()。消息在进程间发送的形式和在不同node上的发送形式不同,进程间,是通过Erlang自定义的消息队列,每个进程都有一个公有的消息队列和私有消息队列,消息队列位于进程的堆空间中。当erl_send()执行结束后,lb_send接着执行PreFetch(0 next)这个宏结构,这个宏结构将I指向的地址做+1操作后赋予next指针,然后对进程的消息队列进行检查,调用erts_gc_after_bif_call对发送的消息数据进行内存回收,然后调用goto find_func_info跳转到该程序段中,在该程序段中对执行现场进行错误检查,并将handle_error()结果返回给register变量I,然后跳转到post_error_handling,在这段程序中,如果I == 0,说明执行成功,没有错误产生,将调用goto do_schedule进行跳转,至此一次完整的调度执行流程就完成了,程序将进入下一次调度。如果I != 0那说明指令执行失败,产生了错误,将调用erts_garbage_collect()对执行过程中产生的堆数据进行回收,然后调用Goto(*I)跳转到具体的错误处理函数中。

 

总结:在Erlang虚拟机的指令集(threaded-code)中提供了sendreceive原语,所以erlang语言屏蔽了网络编程开发和进程通信等很多琐碎的问题,更利于高效率的开发出分布式结构的程序。sendreceive原语的提供,为进程间消息通信机制也提供了基础支持。

Erlang虚拟机有自己的指令集系统,更方便的设计出适合分布式程序的调度算法和调度粒度。

Erlang虚拟机对系统IO的响应采用了事件驱动模式,这是一种异步模式,而不是同步模式,而Erlang语言中所涉及到的同步语法都是使用异步模式模拟的(即在用户态模式下由虚拟机进行阻塞操作,而不是由操作系统内核来进行同步调用的阻塞操作),虽然逻辑编写会变得更复杂,但这种模式对繁重的IO处理有很大的性能提升,所以Erlang不会出现某个调度线程由于调度内核函数而阻塞在内核态中不能及时切换到用户态。

Erlang虚拟机的每一条虚拟机指令(threaded-code)的执行都会进行错误检查,使erlang程序的容错性更高,也为Erlang的容错性提供了最底层的支持,同时Erlang的进程设计等方面也会考虑容错机制。

  • 大小: 10.2 KB
  • 大小: 256.7 KB
分享到:
评论
4 楼 LeslieWei 2013-05-13  
yueyemaitian 写道
LeslieWei 写道
yueyemaitian 写道
进程的栈如何备份恢复呢,比如说a调用了b,b调用了c,c代码中发送了个消息,发送消息之后被调度走了,这个栈怎么备份的呢,里边的局部变量如何处理的?下次被调度到cpu执行的时候,这些又是如何恢复的呢?

erlang虚拟机的进程切换类似于操作系统的进程管理,每个进程有自己的堆栈,以及进程控制块,在进程切换的时候,erlang虚拟机会保存中断现场,也就是各寄存器的值,以及next指针的值,保存在控制块里面,这个和操作系统的原理是一样的,所以我没有详细阐述现场保存和恢复。

有计划写篇文章分析下erlang 进程的切换么,求布道

这篇文章就是讲erlang进程调度和切换的,只是不是十分详细,更详细的我不打算写了,我对erlang虚拟机也没有太过深入的分析。
3 楼 yueyemaitian 2013-05-13  
LeslieWei 写道
yueyemaitian 写道
进程的栈如何备份恢复呢,比如说a调用了b,b调用了c,c代码中发送了个消息,发送消息之后被调度走了,这个栈怎么备份的呢,里边的局部变量如何处理的?下次被调度到cpu执行的时候,这些又是如何恢复的呢?

erlang虚拟机的进程切换类似于操作系统的进程管理,每个进程有自己的堆栈,以及进程控制块,在进程切换的时候,erlang虚拟机会保存中断现场,也就是各寄存器的值,以及next指针的值,保存在控制块里面,这个和操作系统的原理是一样的,所以我没有详细阐述现场保存和恢复。

有计划写篇文章分析下erlang 进程的切换么,求布道
2 楼 LeslieWei 2013-05-11  
yueyemaitian 写道
进程的栈如何备份恢复呢,比如说a调用了b,b调用了c,c代码中发送了个消息,发送消息之后被调度走了,这个栈怎么备份的呢,里边的局部变量如何处理的?下次被调度到cpu执行的时候,这些又是如何恢复的呢?

erlang虚拟机的进程切换类似于操作系统的进程管理,每个进程有自己的堆栈,以及进程控制块,在进程切换的时候,erlang虚拟机会保存中断现场,也就是各寄存器的值,以及next指针的值,保存在控制块里面,这个和操作系统的原理是一样的,所以我没有详细阐述现场保存和恢复。
1 楼 yueyemaitian 2013-05-09  
进程的栈如何备份恢复呢,比如说a调用了b,b调用了c,c代码中发送了个消息,发送消息之后被调度走了,这个栈怎么备份的呢,里边的局部变量如何处理的?下次被调度到cpu执行的时候,这些又是如何恢复的呢?

相关推荐

    Erlang虚拟机内存管理

    Erlang核心开发者Lukas Larsson在2014年3月份Erlang Factory上的一个演讲详细介绍了Erlang内存体系的原理以及调优案例 根据siyao zheng博客上听写的资源进行的翻译,大致只翻译了80%但核心部分已经完整,希望对大家...

    hex, Erlang虚拟机的软件包管理器.zip

    hex, Erlang虚拟机的软件包管理器 十六进制 Hex是Erlang虚拟机的软件包管理器。这个项目目前提供了与混合。tcm 工具构建的任务。有关安装说明和其他文档,请参阅 hex.pm 。在本地安装十六进制以进行开发: mix ...

    erlang 程序设计 源码

    erlang 程序设计 源码 erlang 程序设计 源码 erlang 程序设计 源码erlang 程序设计 源码

    erlang 部分源码

    erlang 部分源码, 虽然不是很全,但是大部分都有。 erlang 部分源码

    英雄远征erlang源码

    英雄远征服务器端erlang源码,用mysql数据库

    基于Erlang VM的语言

    基于Erlang VM的语言基于Erlang基于Erlang VM的语言 VM的语言基于Erlang VM的语言

    erlang21.0源码

    erlang21.0源码。! erlang21.0源码。!erlang21.0源码。!erlang21.0源码。!

    Erlang编程指南

    Erlang是运行于虚拟机的解释性语言,但是现在也包含有乌普萨拉大学高性能Erlang计划(HiPE)开发的本地代码编译器,自R11B-4版本开始,Erlang也开始支持脚本式解释器。在编程范型上,Erlang属于多重范型编程语言,...

    erlang server源码

    erlang server源码 一个聊天室服务器源码

    英雄远征erlang服务器源码含数据库

    erlang具有良好的高并发性 支持热更新 本代码适合初学者更快的上手

    远古封神Server(erlang源码)+文档+mongodb数据库

    远古封神Server(erlang源码)+文档+mongodb数据库 远古封神Server(erlang源码)+文档+mongodb数据库

    erlang 聊天室源码

    erlang 聊天室源代码,初学者项目,花了两周时间完成,其中设计参考的是著名聊天软件rabbitMQ 的部分模块

    erlang入门级练习:LeetCode OJ问题的部分erlang 源码

    我自己在新学erlang,在LeetCode OJ上找了题目练习,题目很适合新手熟悉语言,但是LeetCode OJ里面只有几门主流语言的答案,下面是已完成的erlang源代码,后续有空再做其他问题续传,题目包含:(源码开头都有题目...

    erlang源码包

    erlang源码安装包,此安装包是linux环境下的;erlang的版本是5.10.3

    erlang聊天室源码

    附有需求文档,设计文档,以及一些说明文档,简单的聊天室功能,适合初学者阅读,是本人学习erlang过程中写的,参照了rabbitMQ的核心模块

    Erlang进程模型学习PPT

    Erlang进程模型特点 一个进程可以创建数以万计的轻量级进程 每个轻量级进程仅仅完成单一功能 一个复杂的功能可以由多个轻量级进程协同完成 当遇到file或socket等阻塞的io时, 调用的轻量级进程被阻塞,整个进程不...

    远古封神Server(erlang源码)

    远古封神Server(erlang源码)+文档+mongodb数据库

    openpoker源码 erlang写的网游服务器

    openpoker源码 erlang写的网游服务器源码 OpenPoker是一个大型多人扑克网游,内建支持了容错能力,负载平衡和无限制的规模大小。

Global site tag (gtag.js) - Google Analytics