Erlang/OTP 17.0-rc1 新引入的"脏调度器"浅析
最近在做一些和 NIF 有关的事情,看到 OTP 团队发布的 17 rc1 引入了一个新的特性“脏调度器”,为的是解决 NIF 运行时间过长耗死调度器的问题。本文首先简单介绍脏调度器机制的用法,然后简要分析虚拟机中的实现原理,最后讨论了一下脏调度器的局限性。
脏调度器机制的用法
了解 NIF 的同学都知道,在 Erlang 虚拟机的层面,NIF 调用是不会被抢占的,在执行 NIF 的时候调度器线程的控制权完全被 NIF 调用接管,因此除非 NIF 调用的代码主动交出控制权,否则调度器线程会一直执行 NIF 调用的代码。这实际上变成了协程式的调度,因此运行时间过长的 NIF 会影响其所在的调度器上的所有其他进程的调度。
之前对于这种长时运行 NIF 的一种解决方法是可以使用官方提供的 enif_consume_timeslice 调用,这种方法还是要让 NIF 代码自己在恰当的地方调用这个 api,然后根据 enif_consume_timeslice 返回的结果判断是否需要放弃控制权,因此实际上还是协程的模式。协程式调度和抢占式调度混合在一起本来就是坏味道,如果通过判断发现已经用完时间片,程序员必须自己手工保存断点以及下一次恢复断点;而且这里还要自己估计时间片,把 timeslice 和虚拟机中本来就很模糊的规约(reduction)混在一起,味道也不好闻。
那么 R17 通过引入“脏调度器”从一定程度上解决了这个问题。脏调度器本质上和普通调度器是一样的,也是运行在虚拟机中的调度器线程,但是这种调度器专门运行长时运行的 NIF,R17 允许将长时运行的 NIF 直接丢到脏调度器上去跑。通过调用 enif_schedule_dirty_nif 将需要长时运行的 NIF 函数丢到脏调度器上。长时运行的函数返回的时候要调用 enif_schedule_dirty_nif_finalizer 函数,表示从脏调度器返回到了普通调度器。
下面看一个简单的例子,比如下面这个简单霸道的 NIF:
static ERL_NIF_TERM
io_work(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
int i;
int Number;
enif_get_int(env, argv[], &Number);
for (i = ; i < ; ++i) {
sleep();
printf("nif process number %d\n", Number);
}
return enif_make_atom(env, "ok");
}
io_work 函数显然会运行很长时间(远长于官方文档建议的 1ms)。
利用 R17 新引入的脏调度器,这个 NIF 可以这么写:
#include "erl_nif.h"
#include <unistd.h>
#include <stdio.h> static int
load(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info)
{
return ;
} static ERL_NIF_TERM
dirty_io_work(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
int i;
int Number;
enif_get_int(env, argv[], &Number);
for (i = ; i < ; ++i) {
sleep();
printf("nif process number %d\n", Number);
}
return enif_schedule_dirty_nif_finalizer(env,
enif_make_atom(env, "ok"),
enif_dirty_nif_finalizer);
} static ERL_NIF_TERM call_dirty_io_work(
ErlNifEnv* env,
int argc,
const ERL_NIF_TERM argv[])
{
return enif_schedule_dirty_nif(env,
ERL_NIF_DIRTY_JOB_IO_BOUND,
dirty_io_work, argc, argv);
} static ErlNifFunc io_nif_funcs[] =
{
{"call_dirty_io_work", , call_dirty_io_work}
}; ERL_NIF_INIT(io_nif, io_nif_funcs, load, NULL, NULL, NULL)
这段代码将长时运行的工作放在 dirty_io_work 函数中,Erlang 模块调用 call_dirty_io_work 函数,这个函数转而调用 enif_schedule_dirty_nif 函数,将 dirty_io_work 函数传入,call_dirty_io_work 立即返回,dirty_io_work 函数进入脏调度器等待调度执行。dirty_io_work 函数在返回的时候调用 enif_schedule_dirty_nif_finalizer 将实际的结果返回给原调用者。
enif_schedule_dirty_nif() 函数还接受一个参数 type,表示要调度的 NIF 的类型:CPU 密集型或 IO 密集型。后面可以看出,根据不同的类型,NIF 会被不同类型的脏调度器调用。
下面简单分析一下脏调度器机制的工作原理。
工作原理浅析
OTP 团队实现的脏调度器机制实际上很简单,脏调度器是普通调度器之外的调度器线程。从位于 erts/emulator/beam/erl_nif.c 的 enif_schedule_dirty_nif 函数开始:
这个函数设置当前进程的标志 ERTS_PSFLG_DIRTY_CPU_PROC 或 ERTS_PSFLG_DIRTY_IO_PROC,说明当前进程应该在某种脏调度器上执行。然后一次性将当前进程的规约配额全部清零,说明这个进程很快就要让出调度器了。最后就是设置了一些和虚拟机代码相关的状态,改变虚拟机的执行状态。注意最后将 proc->freason 设置为 TRAP,之后虚拟机会利用到这个标志,虽然最后 return 了一个 THE_NON_VALUE,但是放心,最后返回到原调用者的不会是这个值。
enif_schedule_dirty_nif 函数返回之后会返回上一级调用的函数,通常就是被调用的 NIF,例如上面例子中的 call_dirty_io_work,后者返回之后会将控制权返回给虚拟机,那么返回的位置必然是虚拟机(即一个调度器线程)中处理 call_nif 指令的位置:
NIF 返回到虚拟机中的时候返回到上图中 1 的位置,然后在使用脏调度器的时候,2 中的 if 条件会满足,因此这里设置了一些代码相关的东西,最后跳转到
正常 NIF 的情况下,会满足 if 的第一个条件:设置返回结果,设置下一条指令并跳转执行。但是对于使用脏调度器的情况,满足的是 else if 的条件,这里执行的就不是下一条指令了,而是当前进程中设置的 c_p->i,指令,这个指令是之前在 enif_schedule_dirty_nif 函数中设置的,实际上就是表示执行要被脏调度的那个 NIF 函数的指令。因此执行到上图中的 3472 行的 Dispatch() 之前的时候,当前进程的状态是:
- 进程上下文已经准备好要执行 call_nif 调用另一个 NIF 了
- 当前的规约配额已经归零
- 设置了 ERTS_PSFLG_DIRTY_CPU_PROC 或 ERTS_PSFLG_DIRTY_IO_PROC 标志,表示进程需要在脏调度器上执行
好了,接下来就是 Dispatch() 了,分发下一条要执行的指令。在 Dispatch() 的时候,发现当前线程的规约配额用完,所以准备调度下一个进程。调度下一个进程的时候得把当前进程调度出吧,这时流程就进入了庞杂的调度函数:位于 erts/emulator/beam/erl_process.c 的 schedule() 函数。schedule() 函数调用 schedule_out_process() 函数处理的是调度出一个进程需要的操作。schedule_out_process() 函数会通过 check_enqueue_in_prio_queue() 函数判断是否需要转移进程所在的队列。check_enqueue_in_prio_queue() 函数中有一个判断:
根据进程的ERTS_PSFLG_DIRTY_CPU_PROC 或 ERTS_PSFLG_DIRTY_IO_PROC 状态打上新的标签:ERTS_PSFLG_DIRTY_CPU_PROC_IN_Q 或 ERTS_PSFLG_DIRTY_IO_PROC_IN_Q,表示进程应该进入 CPU 密集型的脏调度器运行队列或 IO 密集型的脏调度器运行队列。回到 schedule_out_process() 函数:
在 1 处,根据进程的标签判断要进入的队列,ERTS_DIRTY_CPU_RUNQ 和 ERTS_DIRTY_IO_RUNQ 宏分别表示 CPU 密集型任务的队列和 IO 密集型任务的队列。虽然虚拟机中可以有多个 CPU 密集型脏调度器和 IO 密集型脏调度器,但是两类队列分别只有一个,即一类脏调度器分享同一个队列。由于脏调度器通常运行长时间的任务,因此访问运行队列的频次要低得多,所以可以只共享一个队列。为此,ErtsRunQueue_ 数据结构中还增加了一个字段 sleepers 用于表示睡了这个队列的调度器列表。最后,在 2 处,当前进程被加入了相应的脏运行队列,如果脏调度器在睡觉的话,此时会被唤醒。
下面兵分两路,一路是“当前进程”原来所在的普通调度器,另一路是某一个被唤醒的脏调度器。
在普通调度器中,之前的“当前进程”被调度出了,那么这个调度器可以像以前一样,挑选下一个正常的进程继续执行。后面还有一个判断:
如果发现有需要脏调度器的进程遗留在普通调度器的队列中的时候,则忽略这个进程。以上两个方面保证了普通调度器能够正常运行,不会被长时运行的 NIF 耗死。
另一路就进入脏调度器了。脏调度器实际上复用的就是普通调度器的代码,即位于 erts/emulator/beam/beam_emu.c 中的 process_main() 函数。这一路的操作和正常调度器的操作一样,从运行队列中取出要执行的进程。当然就是之前 enqueue 进去的那个“当前进程”了。记得我们之前提到的这个进程的状态:要执行一条 call_nif 指令。那么脏调度器拿到这个进程之后,首先开始分发指令就是 call_nif 指令,之后执行这条指令,执行的时候正常调用之前指定的 NIF。不论这个 NIF 运行时间有多长有多复杂,都没关系,因为在独立的调度器,也就是操作系统进程中运行,操作系统可以保证其他普通调度器线程被调度执行,而其他普通调度器线程也能正常运行。
脏调度器运行完长 NIF 之后,通过 enif_schedule_dirty_nif_finalizer() 函数执行上述相反的过程,把“当前进程”变成普通进程,丢回普通调度器执行。
综上可以看出,脏调度器机制简单地说就是提供了一个场所让运行时间很长的进程运行,在这个环境中,进程可以自由运行,不会被抢占。
根据 github 上的 commit 记录:
Currently only NIFs are able to access dirty scheduler
functionality. Neither drivers nor BIFs currently support dirty
schedulers. This restriction will be addressed in the future.
目前只有 NIF 能访问脏调度器,未来会允许 BIF 和驱动也能访问脏调度器。因此可以想象未来也许会出现不会被抢占的普通 Erlang 进程。
局限性
这个简单的实现目前有一定的局限性。首先,目前虽然区分了 CPU 密集型计算和 I/O 密集型计算的脏调度器,但是这两个调度器运行的代码完全是一样的,只是用了不同的运行队列而已,调度策略都是一样的。但是这反映了未来的一个优化方向。
此外,从以上对脏调度器原理的浅析,我们可以发现脏调度器有一点过去 batch 操作系统的感觉,脏调度器必须完整处理完一个任务才会处理下一个任务。因此,如果当前所有脏调度器都被占用满了,那么新的脏任务就不能及时得到调度。下面用一个例子来演示。
这个例子的 NIF 部分就是本文最开始部分的那个 C 语言代码。略去 NIF 调用的桩,下面是主模块的代码:
-module(io_nif_test).
-export([start/0, heart/0]). start() ->
io:format("Starting heartbeat.~n", []),
{ok, _} = timer:apply_interval(500, io_nif_test, heart, []),
timer:sleep(500),
io:format("Stress dirty io schedulers~n", []),
NormalCount = erlang:system_info(schedulers),
DirtyIOCount = erlang:system_info(dirty_io_schedulers),
io:format("There are ~w normal schedulers and ~w dirty io schedulers~n",
[NormalCount, DirtyIOCount]),
IOProcessCount = DirtyIOCount + 1,
io:format("Create ~w IO NIF processes~n", [IOProcessCount]),
lists:foreach(
fun(Number) -> spawn(fun() -> io_nif:call_dirty_io_work(Number) end) end,
lists:seq(1, IOProcessCount)
),
receive
stop_me -> ok
end,
timer:sleep(1000). heart() ->
io:format("Tick~n", []).
这段代码的主进程每半秒钟会心跳一下,我们可以通过心跳看出调度器是不是卡死了。然后根据 io 脏调度器的数目,创建多于一个这个数目的 NIF 进程,也就是说会有一个 NIF 进程得不到及时调度。下面将普通调度器和 io 脏调度器都设置为 1 的运行结果:
erl +S 1:1 +SDio 1 -noshell -s io_nif_test start -s init stop
Starting heartbeat.
Stress dirty io schedulers
There are 1 normal schedulers and 1 dirty io schedulers
Tick
Create 2 IO NIF processes
Tick
Tick
nif process number 2
Tick
Tick
nif process number 2
Tick
Tick
nif process number 2
Tick
Tick
nif process number 2
Tick
Tick
nif process number 2
Tick
Tick
nif process number 1
Tick
Tick
nif process number 1
Tick
Tick
nif process number 1
Tick
Tick
nif process number 1
Tick
Tick
nif process number 1
Tick
Tick
Tick
Tick
Tick
Tick
Tick
Tick
果然,进程 2 全部执行完之后才轮到进程 1 执行。将 io 脏调度器设置为 2:
/Users/zhengsyao/programs/ErlangInstall/otp_17rc1/bin/erl +S 1:1 +SDio 2 -noshell -s io_nif_test start -s init stop
Starting heartbeat.
Stress dirty io schedulers
There are 1 normal schedulers and 2 dirty io schedulers
Tick
Create 3 IO NIF processes
Tick
Tick
nif process number 3
nif process number 2
Tick
Tick
nif process number 3
nif process number 2
Tick
Tick
nif process number 3
nif process number 2
Tick
Tick
nif process number 3
nif process number 2
Tick
Tick
nif process number 3
nif process number 2
Tick
Tick
nif process number 1
Tick
Tick
nif process number 1
Tick
Tick
nif process number 1
Tick
Tick
nif process number 1
Tick
Tick
nif process number 1
Tick
Tick
Tick
2 个 io 脏调度器,创建了 3 个 nif 进程。进程 3 和 2 立即都得到了调度,而进程 1 则在其中一个进程运行完之后得到了调度。
Erlang/OTP 17.0-rc1 新引入的"脏调度器"浅析的更多相关文章
- CentOS 6.5安装Erlang/OTP 17.0
CentOS 6.5安装Erlang/OTP 17.0 作者:chszs,转载需注明.博客主页:http://blog.csdn.net/chszs Erlang眼下已经是Fedora和Debian/ ...
- Erlang/OTP设计原则(文档翻译)
http://erlang.org/doc/design_principles/des_princ.html 图和代码皆源自以上链接中Erlang官方文档,翻译时的版本为20.1. 这个设计原则,其实 ...
- Erlang OTP gen_event
转自:http://www.myexception.cn/program/1569725.html Erlang OTP gen_event (0) 原英文文档:http://www.erlang.o ...
- 关于 Swift 2.0 - 语言新特性与革新
随着刚刚结束的 WWDC 2015 苹果发布了一系列更新,这其中就包括了令人振奋的 Swift 2.0. 这是对之前语言特性的一次大幅的更新,加入了很多实用和方便的元素,下面我们就一起来看看这次更新都 ...
- nexus 3.17.0 简单说明
nexus 在6.24 发布了3.17.0 ,同时包含了好多新的特性 以下为一些主要变动: routing rules 可以增强repo 的安全 apt repo 格式的支持 可以方便的为ubuntu ...
- Erlang调度器细节探析
Erlang调度器细节探析 Erlang的很多基础特性使得它成为一个软实时的平台.其中包括垃圾回收机制,详细内容可以参见我的上一篇文章Erlang Garbage Collection Details ...
- Android 7.0 PopupWindow 又引入新的问题,Google工程师也不够仔细么
Android7.0 PopupWindow的兼容问题 Android7.0 中对 PopupWindow 这个常用的控件又做了一些改动,修复了以前遗留的一些问题的同时貌似又引入了一些问题,本文通 ...
- 理解Erlang/OTP Supervisor
http://www.cnblogs.com/me-sa/archive/2012/01/10/erlang0030.html Supervisors are used to build an hie ...
- 工作流引擎 Flowable 6.0.0.RC1 release,完全兼容Activi
Flowable 6.0.0.RC1 release,第一个可流动的6引擎版本(6.0.0.RC1). Flowable 6.0.0.RC1 relase新增加的功能以及特色: 包重命名为org.Fl ...
随机推荐
- jQuery中的Sizzle引擎分析
我分析的jQuery版本是1.8.3.Sizzle代码从3669行开始到5358行,将近2000行的代码,这个引擎的版本还是比较旧,最新的版本已经到v2.2.2了,代码已经超过2000行了.并且还有个 ...
- 重温Servlet学习笔记--request对象
request和response是一对搭档,一个负责请求一个负责响应,都是Servlet.service()方法的参数,response的知识点前面梳理过了,这里只说一下request,在客户端发出每 ...
- SQL Server 2014里的性能提升
在这篇文章里我想小结下SQL Server 2014引入各种惊艳性能提升!! 缓存池扩展(Buffer Pool Extensions) 缓存池扩展的想法非常简单:把页文件存储在非常快的存储上,例如S ...
- RECONFIGURE语句会清空计划缓存么?
几个星期前,有个网友问我一个非常有趣的问题:RECONFIGURE语句会清空计划缓存么?通常我对这个问题的答案是简单的是,但慢慢的我找出了真正的答案是“看情况啦”.我们来看下它,为什么“它看情况”. ...
- 开发漫谈:千万别说你不了解Docker!
1dotCloud到Docker:低调奢华有内涵 写在前面:放在两年前,你不认识Docker情有可原.但如果现在你还这么说,不好意思,我只能说你OUT了.你最好马上get起来,因为有可能你们公司很 ...
- BF算法与KMP算法
BF(Brute Force)算法是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符:若不相等,则比较S的 ...
- jQuery模拟打字逐字输出代码
效果查看:http://hovertree.com/texiao/jquery/70/ jQuery键盘打出逐字逐句显示特效,逐字逐句显示文字 还可以设置每个文字随机颜色: http://hovert ...
- [WCF编程]12.事务:事务协议与管理器
一.事务协议 总体来说,WCF开发人员不需要涉及事务协议与管理器.我们应该依赖WCF来选择相应的事务协议和管理器,重点关注业务逻辑的实现. WCF是根据事务范围里的参与个体来选择事务管理协议的.事务管 ...
- 谈一谈.net析构函数对垃圾回收的影响
之前忘了说了 代码都是在Release模式下运行的,现在补充上. 这里说析构函数,其实并不准确,应该叫Finalize函数,Finalize函数形式上和c++的析构函数很像 ,都是(~ClassNam ...
- C#怎样保证弹出窗体是唯一并居中显示
Winform窗体中,假如我从Form1窗体要弹出Form2窗体,写法是这样的: Form2 f2 = new Form2(); f2.Show(); 1.如何使窗体打开时居中显示 //初始化默认窗体 ...