OverView
Erlang外部调用的几种方式:
外部接入(OS进程级):
- Ports: 用C实现的可执行程序,以Port的方式与Erlang交互。
- C Nodes: 用C模拟Erlang Node行为实现的可执行程序。
- Jinterface: Java和Erlang的通讯接口。
- Network: 通过自定义序列化格式与Erlang节点网络交互,如bert-rpc
内部接入(和虚拟机在同一个OS进程内):
- BIF: Erlang大部分BIF用C实现,如erlang:now,lists:reverse等
- Port Driver: 以链接库方式将Port嵌入虚拟机,也叫Linkin Driver
- NIF: 虚拟机直接调用C原生代码
下面主要理解常用的三种:Ports, Port Driver, NIF。
Ports
图一. Ports 通信模型
Port是连接外部程序进程和Erlang虚拟机的桥梁,外部进程通过标准输入输出与Erlang虚拟机交互,并运行于独立的地址空间。
从操作系统的角度看,外部程序和Erlang虚拟机都是独立允许的进程,只不过外部程序的标准输入输出与Erlang虚拟机对接在了一起而已。因此外部程序可以通过read(0, req_buf, len)
来获取虚拟机发出的指令,也可通过write(1, ack_buf, len)
来发出响应。当外部程序崩溃了,Erlang虚拟机可以检测到,可以选择重启等对应策略。由于两者在不同的地址空间,通过标准IO交互,因此外部程序的崩溃不会影响到Erlang虚拟机本身的正常运行。
每个Port都有一个owner进程,通常为创建Port的进程,当owner进程终止时,Port也将被自动关闭。Ports使用示例参考Ports。
Port的优势在于隔离性和安全性,因为外部程序的任何异常都不会导致虚拟机崩溃,并且Erlang层通过receive
来实现同步调用等待外部程序响应时,是不会影响Erlang虚拟机调度的。至于Port的缺点,主要是效率低,由于传递的是字节流数据,因此需要对数据进行序列化反序列化,Erlang本身针对C和Java提供了对应的编解码库ei和Jinterface。
Port Driver
图二. Port Driver 通信模型
从Erlang层来看,端口驱动和普通端口所体现的行为模式一样,收发消息,注册名字,并且共用一套Port API。但是端口驱动本身是作为一个链接库运行于Erlang虚拟机中的,也就是和Erlang虚拟机共享一个操作系统进程。
Port Driver分为静态链接和动态链接两种,前者和虚拟机一起编译,在虚拟机启动时被加载,后者通过动态链接库的方式嵌入到虚拟机。出于灵活性和易用性的原因,通常使用后者。
虚拟机和Port Driver的交互方式与Port一样,Port和Port Driver在Erlang层表现的语义一致。
Port Driver通过一个driver_entry结构体与虚拟机交互,该结构体注册了driver针对各种虚拟机事件的响应函数。skynet挂接service的思想大概也继承于此。driver_entry结构体主要成员如下:
typedef struct erl_drv_entry {
// 当链接库被加载(erl_ddll:load_driver/2)时调用,同一个链接库的多个driver实例来说,只调用一次
int (*init)(void);
// 当Erlang层调用erlang:open_port/2时调用,每个driver实例执行一次
ErlDrvData (*start)(ErlDrvPort port, char *command);
// 当Port Driver被关闭(erlang:port_close/1,owner进程终止,虚拟机停止等)时执行
void (*stop)(ErlDrvData drv_data);
// 收到Erlang进程发来的消息(Port ! {PortOwner, {command, Data}} or erlang:port_command(Port, Data))
void (*output)(ErlDrvData drv_data, char *buf, ErlDrvSizeT len);
// 用于基于事件的异步Driver 通过erl_driver:driver_select函数进行事件(socket,pipe,Event等)监听
void (*ready_input)(ErlDrvData drv_data, ErlDrvEvent event);
void (*ready_output)(ErlDrvData drv_data, ErlDrvEvent event);
// Driver名字 用于open_port/2
char *driver_name;
// 当Driver被卸载时调用(erl_ddll:unload_driver/1),和init对应。仅针对动态链接Driver
void (*finish)(void);
// 被erlang:port_control/3(类似ioctl)触发
ErlDrvSSizeT (*control)(ErlDrvData drv_data, unsigned int command,
char *buf, ErlDrvSizeT len,
char **rbuf, ErlDrvSizeT rlen);
// Driver定义的超时回调,通过erl_driver:driver_set_timer设置
void (*timeout)(ErlDrvData drv_data);
// output的高级版本,通过ErlIOVec避免了数据拷贝,更高效
void (*outputv)(ErlDrvData drv_data, ErlIOVec *ev);
// 用于基于线程池的异步Driver(erl_driver:driver_async) 当线程池中的的任务执行完成时,由虚拟机调度线程回调该函数
void (*ready_async)(ErlDrvData drv_data, ErlDrvThreadData thread_data);
// 当Driver即将关闭时,在stop之前调用 用于清理Driver队列中的数据(?)
void (*flush)(ErlDrvData drv_data);
// 被erlang:port_call/3触发 和port_control类似,但使用ei库编码ETerm
ErlDrvSSizeT (*call)(ErlDrvData drv_data, unsigned int command,
char *buf, ErlDrvSizeT len,
char **rbuf, ErlDrvSizeT rlen, unsigned int *flags);
// Driver 监听的进程退出信号(erl_driver:driver_monitor_process)
void (*process_exit)(ErlDrvData drv_data, ErlDrvMonitor *monitor);
} ErlDrvEntry;
该结构体比较复杂,主要原因是Erlang Port Driver支持多种运行方式:
- 运行于虚拟机调度线程的基本模式
- 基于select事件触发的异步Driver
- 基于异步线程池的异步Driver
三种模式的示例参考Port Driver,How to Implement a Driver,Driver API接口文档:erl_driver。Erlang虚拟机提供的异步线程池可通过+A
选项设置。
端口驱动的主要优势是效率高,但是缺点是链入的动态链接库本身出现内测泄露或异常,将影响虚拟机的正常运行甚至导致虚拟机崩溃。将外部模块的问题带入了虚拟机本身。对于耗时较长或阻塞的任务,应该通过异步方式设计,避免影响虚拟机调度。
NIF
NIF是Erlang调用C代码最简单高效的方案,对Erlang层来说,调用NIF就像调用普通函数一样,只不过这个函数是由C实现的。NIF是同步语义的,运行于调度线程中,无需上下文切换,因此效率很高。但也引出一个问题,对于执行时间长的NIF,在NIF返回之前,调度线程不能做别的事情,影响了虚拟机的公平调度,甚至会影响调度线程之间的协作。因此NIF是把双刃剑,在使用的时候要尤其小心。
Erlang建议的NIF执行时间不要超过1ms,针对于执行时间长的NIF,有如下几种方案:
- 分割任务,将单次长时间调用切分为多次短时间调用,再合并结果。这种方案显然不通用
- 让NIF参与调度。在NIF中恰当时机通过
enif_consume_timeslice
汇报消耗的时间片,让虚拟机确定是否放弃控制权并通过返回值通知NIF(做上下文保存等) - 使用脏调度器,让NIF在非调度线程中执行
Erlang默认并未启用脏调度器,通过--enable-dirty-schedulers
选项重新编译虚拟机可打开脏调度器,目前脏调度器只能被NIF使用。
关于脏调度器,NIF测试与调优,参考:
- siyao blog
- nifwait
- bitwise(其中的PDF质量很高)
Port Driver和NIF与虚拟机调度密切相关,想要在实践中用好它们,还是要加深对Erlang虚拟机调度的理解,如公平调度,进程规约,调度器协同等。再来理解异步线程池,脏调度器的存在的意义以及适用场景。另外,Port Driver和NIF还有一种用法是自己创建新的线程或线程池(Driver和NIF也提供了线程操作API),我们项目组也这么用过,这基本是费力不讨好的一种方案,还极易出错。