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

2024-03-18 17:50

本文主要是介绍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的进程设计等方面也会考虑容错机制。

这篇关于Erlang虚拟机源码阅读笔录(三)虚拟机的进程调度的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/823151

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟 开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚 第一站:海量资源,应有尽有 走进“智听

搭建Kafka+zookeeper集群调度

前言 硬件环境 172.18.0.5        kafkazk1        Kafka+zookeeper                Kafka Broker集群 172.18.0.6        kafkazk2        Kafka+zookeeper                Kafka Broker集群 172.18.0.7        kafkazk3

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

论文阅读笔记: Segment Anything

文章目录 Segment Anything摘要引言任务模型数据引擎数据集负责任的人工智能 Segment Anything Model图像编码器提示编码器mask解码器解决歧义损失和训练 Segment Anything 论文地址: https://arxiv.org/abs/2304.02643 代码地址:https://github.com/facebookresear

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

kubelet组件的启动流程源码分析

概述 摘要: 本文将总结kubelet的作用以及原理,在有一定基础认识的前提下,通过阅读kubelet源码,对kubelet组件的启动流程进行分析。 正文 kubelet的作用 这里对kubelet的作用做一个简单总结。 节点管理 节点的注册 节点状态更新 容器管理(pod生命周期管理) 监听apiserver的容器事件 容器的创建、删除(CRI) 容器的网络的创建与删除