0. 用C写的内联驱动的port driver运行时嵌入到Eralng虚拟机中,就像是Erlang的一部分,而外部接口是在一个独立的操作系统进程中运行的。
内联驱动的优点是高效,缺点是写的不好会连累Erlang虚拟机,把后者崩溃掉。所以,很危险要小心。
一个比较好的参考例子是
bfile,bfile实际上是对c的标准文件操作(如fopen(), fread()等)做了包装。
用erl_ddll:loaded_drivers().可以看到缺省情况下erl提供的常见driver:
> erl_ddll:loaded_drivers().
{ok,["efile","tcp_inet","udp_inet","zlib_drv",
"ram_file_drv","tty_sl","GDAL_drv"]}
写driver时可以参考一下。
注意driver中的函数都是静态的,静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。
C语言中定义静态函数的好处:
静态函数会被自动分配在一个一直使用的存储区,直到退出应用程序实例,避免了调用函数时压栈出栈,速度快很多。
关键字“static”,译成中文就是“静态的”,所以内部函数又称静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。 使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名,因为同名也没有关系。
1. Port Driver共享库的装载与卸载
装载(erl_ddll:load(Path,LibName))指定库文件所在目录和库名字(库文件的名字与ErlDrvEntry结构driver_name字段的名字一致),装载时会自动执行我们在ErlDrvEntry结构init字段中指定的函数(如果指定了),我们可以在这个函数中为driver做一些准备工作;
卸载(erl_ddll:unload(Name)时只需指定要卸载的库名,即ErlDrvEntry结构中的driver_name(也是上面erl_ddll:loaded_drivers列出的库名),如果ErlDrvEntry结构体中指定了finish字段的函数,那么卸载时会自动执行此函数,可以用此函数做一些库的清理工作
Port Driver共享库在Erlang运行时中的存在与转载库的进程有关,如果装载库的进程(即调用erl_ddll:load的进程死掉了,这个进程装载的库也会自动被卸载。可以自己验证一下:
erl_ddll:loaded_drivers().
spawn(fun() ->erl_ddll:load(".", 'YOUR_drv'), timer:sleep(5000) end).
erl_ddll:loaded_drivers().
五秒钟后再
erl_ddll:loaded_drivers().
所以,照看好你的装载共享库的进程,不要让它意外退出了。
2. 端口(Port)的打开与关闭
用open_port启动一个端口时会自动执行ErlDrvEntry结构体中start字段指定的函数;
Port关闭时(如调用unlink关闭)是会指定执行ErlDrvEntry 结构体stop字段指定的函数。
在开启port直到关闭的这段作用域内,我们可能需要共享一些数据以方便对port的操作。该函数会返回一个指针,这个指针是一个ErlDrvData数据类型,实际上指向一个用户自定义的结构体,这个ErlDrvData数据(也就是那个指针)又称为port handle,某种意义上这个指针代表被打开的port,因此在driver的其它回调函数中,ErlDrvData数据(也就是那个指针)总是传入的第一个参数。
例如bfile包装了底层对文件的读写操作,底层实际上是以文件句柄的方式操作文件,每开启一个bfile 端口,就是对应着一个文件的打开(得到一个文件句柄),以后通过端口对文件的操作都是通过该句柄进行的,因此bfile的c实现中需要记录打开的对应文件句柄。这个文件句柄可以在端口打开时记录在ErlDrvPort结构中,以后每次对端口操作时就可以从ErlDrvPort结构体中得到port对应的文件句柄了。
在
端口连接进程(即调用open_port打开端口的进程)死掉后,端口会自动关闭。
也可以自己主动关闭端口
unlink(Port),
catch erlang:port_close(Port).
3. 当在Erlang上调用erlang:port_control时,对应的C driver的control函数将被调用,执行的结果通过connect函数的输出参数rbuf传回给erlang emulator,erl_interface中的ei库会将数据编码成erlang的二进制term,因此在erlang一端可以调用binary_to_term将结果转成term。显然,在C driver中为输出而给rbuf分配的内存应该由erlang emulator负责释放。
4. C程序的编译,连接的时候要小心别遗漏某些库,不然编译连接都通过了,库文件也生成了,运行时却出问题;例如,有一次程序中拼写错误调用了一个不存在的函数,但是编译连接都通过,只是在加载的时候才给出错误。
5. C++的内联驱动
DRIVER_INIT应该声明为extern "C" 的:
extern "C" DRIVER_INIT(MY_drv)
更多的例子参考
generic erlang port driver,这个项目提供了一个框架,方便了C/C++的外部端口以及内联驱动程序的编写。
6. 内存泄漏的问题
自己用C写内联驱动程序最可能出现的问题就是内存泄漏了,我在linux下使用了一个比较土的检测方法:用一个脚本每十秒钟检测一下erlang虚拟机的内存,具体是调用了ps -o rss命令进行的(单位是KB),这个命令可能不够精确,但也够用了。检测数据都写入erl_mem.log文本文件中,最后一列即是erlang运行时的内存:
# ERLPID=`pgrep -f 'beam'`; while [ 1 ] ; do MEM=`ps -o rss= -p $ERLPID`; echo -e "`date`\t`date +%s`\t$MEM\t"; sleep 10; done | tee -a erl_mem.log
如果发现内存数量只升不降多半是出现内存泄漏了。
运行这个脚本的机器上不要同时运行两个erlang运行时。
7. 想在C driver里用erl_interface库函数解析二进制数据时出现问题,函数erl_init(NULL, 0)不调用还好,一旦出现,即使没有执行到,控制台上就会大约无穷的:
(no error logger present) error: "erts_poll_wait() failed: einval (22)\n"
真邪门
erl_interface库一般用于外联驱动,或者写C node,为这些程序提供erlang term数据结构的转换支持,这些程序的特点是都独立于Erlang运行,而内联驱动的程序是作为Erlang自身的一部分运行,也许和这个有关,待证实
查官方的例子(在erts/example/目录下)可以看到,写内联驱动的driver进行erlang term的转换时没有用erl_interface,而是使用的ei进行term的编码和解码,ei的抽象程度要比erl_interface低,但是至少提供了将C语言的数据结构转换成对应的Erlang term结构的一个包装。一个生成二元tuple的例子:
ei_x_buff x; // 将用来表示Erlang term的C数据结构
ei_x_new_with_version(&x);
ei_x_encode_tuple_header(&x, 2); //这是一个二元的tuple
ei_x_encode_atom(&x, "ok"); // tuple的第一个元素是一个atom,值为ok
ei_x_encode_string(&x, "abcdef"); // 第二个元素是字符串abcdef
生成了由ei_x_buff 表示的term,
ErlDrvBinary* bin = driver_alloc_binary(x.index);
memcpy(&bin->orig_bytes[0], x.buff, x.index);
从ei_x_buff提取出二进制binary,在erlang上调用binary_to_term即可得到成Erlang term,最后得到tuple
{ok, "abcdef"}
port driver的官方文档是ERTS User‘s Guide的
第6章 How to implement a driver
如果driver中的计算比较耗时,可能会阻塞erlang emulator,别的Erlang进程就不能执行了,这是不可接受的,解决办法是实现一个异步driver,在6.5和6.6节对此有介绍