首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

6.如何为Erlang分发实现替代载体 | 6. How to Implement an Alternative Carrier for the Erlang Distribution

本节描述如何为Erlang分发实现替代载波协议。该分发通常由TCP/IP承载。这里解释了用另一种协议替换TCP/IP的方法。

本节是对uds_dist内核应用程序中的示例应用程序%28examples目录%29。大uds_dist应用程序在Unix域套接字上实现分发,并且是为Sun Solaris 2操作环境编写的。然而,这些机制是通用的,适用于任何运行Erlang的操作系统。C代码不能移植的原因是简单的可读性。

这部分是很久以前写的。其中大部分仍然有效,但从那以后有些事情发生了变化。最值得注意的是驱动程序接口。这里介绍的驱动程序的文档已经做了一些更新,但还可以做更多的工作,并计划在未来进行。鼓励读者阅读erl_driverdriver_entry还有文件。

6.1游戏攻略

为了实现Erlang分发的新载体,主要步骤如下。

写一个Erlang司机

首先,该协议必须对Erlang机器可用,这涉及到编写Erlang驱动程序。端口程序不能使用,需要Erlang驱动程序。Erlang司机可以是:

  • 静态地链接到模拟器,当使用Erlang的开源发行版时,可以选择
  • 动态加载到erlang机器地址空间,如果要使用erlang的预编译版本,这是唯一的选择。

写一个Erlang司机并不容易。当数据被发送到驱动程序时,驱动程序被写成Erlang模拟器调用的回调函数,或者驱动程序在文件描述符上有可用的任何数据。当驱动程序回调例程在Erlang机器的主线程中执行时,回调函数可以不执行任何阻塞活动。回调仅用于设置用于等待和/或读取/写入可用数据的文件描述符。所有的I/O必须是非阻塞的。然而,驱动程序回调是按顺序执行的,为什么可以在例程中安全地更新全局状态。

为驱动程序编写Erlang接口

当驱动程序实现时,最好为驱动程序编写Erlang接口,以便能够分别测试驱动程序的功能。然后,分发模块可以使用此接口,该模块将从net_kernel...

最简单的途径是模仿inetinet_tcp接口,但这些模块中的功能不多需要实现。在示例应用程序中,只实现了几个常用的接口,并且它们都得到了很大的简化。

编写分发模块

当协议通过驱动程序和Erlang接口模块可用于Erlang时,可以编写一个分发模块。分发模块是一个具有定义明确的回调的模块,很像gen_server(虽然没有检查回调的编译器支持)。该模块实现:

  • 查找其他节点的细节(也就是说,epmd或者类似的东西)
  • 创建监听端口(或类似)
  • 连接到其他节点
  • 执行握手/cookie验证

但是有一个实用模块,dist_util,它完成了握手、饼干、计时器和滴答的大部分艰苦工作。使用dist_util使得实现分发模块更加容易,这在示例应用程序中是这样做的。

创建启动脚本

最后一步是创建引导脚本,使协议实现在启动时可用。可以通过在所有系统运行时启动发行版来调试该实现,但在实际系统中,发行版将很早启动,为什么需要启动脚本和一些命令行参数。

这一步还意味着接口模块和分布模块中的Erlang代码可以在启动阶段运行。特别是,不能在application模块或任何未在启动时加载的模块进行调用。也就是说,只有KernelSTDLIB和应用程序本身可以使用。

6.2司机

虽然Erlang驱动程序一般都超出了本节的范围,但简要介绍似乎已经到位。

一般司机

Erlang驱动程序是用C%28或汇编程序%29编写的本机代码模块,它充当了一些特殊操作系统服务的接口。这是一种通用的机制,在Erlang模拟器中用于所有类型的I/O。可以在运行时将Erlang驱动程序动态链接%28或加载%29到Erlang模拟器,方法是使用erl_ddllErlang模块。然而,OTP中的一些驱动程序是静态地链接到运行时系统的,但这更多地是一种优化,而不是一种必要的优化。

驱动程序数据类型和驱动程序编写器可用的函数在头文件中定义。erl_driver.h位于Erlang%27中,包括目录。见erl_driver详细说明哪些功能可用的文档。

在编写驱动程序以使Erlang可以使用通信协议时,您应该知道关于该特定协议的所有值得了解的内容。所有操作必须是非阻塞的,所有可能的情况都要在驱动程序中考虑。一个不稳定的驱动程序将影响和/或崩溃整个Erlang运行时系统。

模拟器在以下情况下调用驱动程序:

  • 当驱动程序加载时。这个回调ErlDrvEntry函数必须有一个特殊的名字,并且通过返回一个指向一个结构体的指针来通知模拟器什么样的回调函数将被正确地填充(见下文)。
  • 打开驱动程序的端口时(通过open_port来自Erlang 的调用)。这个例程是设置内部数据结构并返回一个不透明的类型数据实体ErlDrvData,这是一个足够容纳指针的数据类型。这个函数返回的指针是关于这个特定端口的所有其他回调的第一个参数。它通常被称为端口句柄。模拟器只存储句柄,并且永远不会尝试解释它,为什么它几乎可以是任何东西(任何不大于指针的东西),并且如果它是指针,可以指向任何东西。通常这个指针指的是一个结构体,它保存有关特定端口的信息,就像它在例子中所做的那样。
  • Erlang进程向端口发送数据时。数据作为字节缓冲区到达,解释没有定义,但取决于实现者。这个回调没有给调用者返回任何信息,回答以消息的形式发送给调用者(使用driver_output所有驱动程序可用的例程)。如下所述,还有一种以同步方式与司机交谈的方式。可以有一个额外的回调函数来处理碎片数据(以深度io列表形式发送)。该接口以适合于Unix的形式获取数据,writev而不是在单个缓冲区中。分发驱动程序不需要实现这样的回调,所以我们不会。
  • 当一个文件描述符被发信号输入时。当模拟器检测到文件描述符上的驱动程序使用该接口标记为要进行监视的输入时,会调用此回调函数driver_select。驱动程序选择的机制可以通过driver_select在需要读取时调用来读取非阻塞文件描述符,然后在此回调中进行读取(可以读取时)。典型的场景是driver_select当Erlang进程读取操作时调用该方法,并且当文件描述符中有数据可用时,此例程发送答案。
  • 当文件描述符发出输出信号时。此回调的调用方式与前面的类似,但当写入文件描述符时是可能的。通常的情况是Erlang命令在文件描述符上写入,驱动程序调用driver_select当描述符准备好输出时,将调用此回调,驱动程序可以尝试发送输出。队列可以参与这些操作,并且驱动程序编写器可以使用方便的队列例程。
  • 当端口关闭时,要么通过Erlang进程,要么由调用driver_failure_XXX例行公事。这个例程是清理连接到一个特定端口的所有东西。当其他回调调用driver_failure_XXX例程,这个例程立即被调用。发出错误的回调例程不能更多地使用端口的数据结构,因为这个例程肯定释放了所有相关数据并关闭了所有文件描述符。但是,如果使用了驱动程序编写器可用的队列实用程序,则此例程为调用,直到队列为空。
  • 当Erlang进程调用erlang:port_control/3,这是驱动程序的同步接口。控制接口用于设置驱动程序选项、更改端口状态等。这个接口在这个例子中经常使用。
  • 计时器过期的时候。驱动程序可以使用该函数设置计时器。driver_set_timer当这些计时器过期时,将调用特定的回调函数。在本例中不使用计时器。
  • 当整个司机卸货的时候。驱动程序分配的每个资源都将被释放。分布式驱动程序的数据结构Erlang分发所使用的驱动程序是实现一个可靠的、顺序维护的、面向可变长度数据包的协议。所有的纠错、重发和这些需要在驱动程序或底层通信协议中实现。如果该协议是面向流的%28 AS,则TCP/IP和我们的流Unix域套接字%29都是如此,则需要一些打包机制。我们将使用简单的方法,使包含包长度为大端32位整数的头部为四个字节。由于Unix域套接字只能在同一台机器上的进程之间使用,所以我们不需要在某些特殊的endian中对整数进行编码,但是无论如何我们还是会这样做的,因为在大多数情况下,您需要这样做。Unix域套接字是可靠的和订单维护,所以我们不需要实现重发和这样的驱动程序。我们通过声明原型并填写一个静态的,开始编写示例unix域套接字驱动程序。ErlDrvEntry结构:( 1) #include <stdio.h> ( 2) #include <stdlib.h> ( 3) #include <string.h> ( 4) #include <unistd.h> ( 5) #include <errno.h> ( 6) #include <sys/types.h> ( 7) #include <sys/stat.h> ( 8) #include <sys/socket.h> ( 9) #include <sys/un.h> (10) #include <fcntl.h> (11) #define HAVE_UIO_H (12) #include "erl_driver.h" (13) /* (14) ** Interface routines (15) */ (16) static ErlDrvData uds_start(ErlDrvPort port, char *buff); (17) static void uds_stop(ErlDrvData handle); (18) static void uds_command(ErlDrvData handle, char *buff, int bufflen); (19) static void uds_input(ErlDrvData handle, ErlDrvEvent event); (20) static void uds_output(ErlDrvData handle, ErlDrvEvent event); (21) static void uds_finish(void); (22) static int uds_control(ErlDrvData handle, unsigned int command, (23) char* buf, int count, char** res, int res_size); (24) /* The driver entry */ (25) static ErlDrvEntry uds_driver_entry = { (26) NULL, /* init, N/A */ (27) uds_start, /* start, called when port is opened */ (28) uds_stop, /* stop, called when port is closed */ (29) uds_command, /* output, called when erlang has sent */ (30) uds_input, /* ready_input, called when input (31) descriptor ready */ (32) uds_output, /* ready_output, called when output (33) descriptor ready */ (34) "uds_drv", /* char *driver_name, the argument (35) to open_port */ (36) uds_finish, /* finish, called when unloaded */ (37) NULL, /* void * that is not used (BC) */ (38) uds_control, /* control, port_control callback */ (39) NULL, /* timeout, called on timeouts */ (40) NULL, /* outputv, vector output interface */ (41) NULL, /* ready_async callback */ (42) NULL, /* flush callback */ (43) NULL, /* call callback */ (44) NULL, /* event callback */ (45) ERL_DRV_EXTENDED_MARKER, /* Extended driver interface marker */ (46) ERL_DRV_EXTENDED_MAJOR_VERSION, /* Major version number */ (47) ERL_DRV_EXTENDED_MINOR_VERSION, /* Minor version number */ (48) ERL_DRV_FLAG_SOFT_BUSY, /* Driver flags. Soft busy flag is (49) required for distribution drivers */ (50) NULL, /* Reserved for internal use */ (51) NULL, /* process_exit callback */ (52) NULL /* stop_select callback */ (53) };在第1-10行中,包括驱动程序所需的操作系统头。由于此驱动程序是为Solaris编写的,因此我们知道uio.h存在。所以预处理变量HAVE_UIO_H可以在此之前定义erl_driver.h列在第12行。对…的定义HAVE_UIO_H会使Erlang%27s驱动队列中使用的I/O向量与操作系统相对应,这非常方便。在第16-23行中,不同的回调函数被声明为%28“前向声明”%29。驱动程序结构类似于静态链接的驱动程序和动态加载的驱动程序。但是,有些字段将保留为空%28,即在不同类型的驱动程序中初始化为空%29。第一个字段%28init函数指针%29在动态加载的驱动程序中始终为空白,请参见第26行。NULL在第37行中,该字段将不再使用,而是为了向后兼容性而保留。此驱动程序中不使用计时器,为什么不需要对计时器进行回调。大outputv字段%28行40%29可用于实现类似于unix的接口writev用于输出。Erlang运行时系统以前不能使用outputv对于发行版,但可以从ERTS 5.7.2开始。由于此驱动程序是在ERTS 5.7.2之前编写的,因此它不使用outputv回调。使用outputv回调是首选的,因为它减少了数据的复制。%28我们将在驱动程序内部使用分散/收集I/O。从ERTS 5.5.3开始,通过版本控制和传递功能信息的可能性扩展了驱动程序接口。第48行存在能力标志。来自ERTS 5.7.4旗ERL_DRV_FLAG_SOFT_BUSY对于要由发行版使用的驱动程序,则需要。软繁忙标志意味着驱动程序可以处理对outputoutputv回调,尽管它已经标记自己为繁忙。这一直是发行版使用的驱动程序的要求,但以前没有关于此的功能信息。想了解更多信息。见erl_driver:set_busy_port()29%。此驱动程序是在运行时系统支持SMP之前编写的。驱动程序仍将在运行时系统中运行,支持SMP,但性能将受到驱动程序使用的驱动程序锁的锁争用。这可以通过重新编写代码来缓解,这样驱动程序的每个实例都可以安全地并行执行。当实例安全地并行执行时,启用驱动程序上特定实例的锁定是安全的。这是通过传递ERL_DRV_FLAG_USE_PORT_LOCKING作为司机旗。这是留给读者的练习。因此,定义的回调如下:uds_start必须启动端口的数据。我们在这里不创建任何套接字,只初始化数据结构。uds_stop当端口关闭时调用。uds_command处理来自Erlang的消息。消息可以是要发送的普通数据,也可以是给驱动程序的更微妙的指令。此函数主要用于数据抽取。uds_input当从套接字读取某些内容时调用。uds_output当可以写入套接字时调用。uds_finish当驱动程序卸载时调用。发行驱动程序永远不会被卸载,但我们包括了这一点,以确保完整性。能够自我清理总是一件好事。uds_controlerlang:port_control/3回调,它在这个实现中经常使用。该驱动程序实现的端口以两种主要模式运行,名为commanddata.在command模式,只有被动读写方式gen_tcp:recv/gen_tcp:send%29可以完成。在分发握手期间,端口处于此模式。连接结束后,端口切换到data模式和所有数据立即读取并传递给Erlang模拟器。在data模式,没有数据到达uds_command将被解释,只在套接字上打包和发送。大uds_control回调在这两种模式之间进行切换。当net_kernel通知不同的子系统连接即将启动,端口将接受要发送的数据。但是,端口不应该接收任何数据,以避免数据在每个内核子系统准备处理之前从另一个节点到达。第三种模式,名为intermediate,用于这个中间阶段。枚举是为不同类型的端口定义的:( 1) typedef enum { ( 2) portTypeUnknown, /* An uninitialized port */ ( 3) portTypeListener, /* A listening port/socket */ ( 4) portTypeAcceptor, /* An intermediate stage when accepting ( 5) on a listen port */ ( 6) portTypeConnector, /* An intermediate stage when connecting */ ( 7) portTypeCommand, /* A connected open port in command mode */ ( 8) portTypeIntermediate, /* A connected open port in special ( 9) half active mode */ (10) portTypeData /* A connected open port in data mode */ (11) } PortType; 不同的类型如下:portTypeUnknown端口打开时具有的类型,但不绑定到任何文件描述符。portTypeListener连接到侦听套接字的端口。这个端口不做太多的工作,在这个套接字上没有数据泵,但是当一个人试图在端口上做一个接受时,读取数据是可用的。portTypeAcceptor此端口表示接受操作的结果。当您想要从侦听套接字接受时,就会创建它,并将其转换为portTypeCommand当接受成功的时候。portTypeConnector非常类似于portTypeAcceptor,连接操作请求和套接字连接到另一端的接收信道之间的中间阶段。连接套接字时,端口开关类型为portTypeCommand...portTypeCommand中的连接套接字%28或接受的套接字%29command前面提到的模式。portTypeIntermediate连接插座的中间阶段。此套接字不需要处理输入。portTypeData通过端口抽运数据的模式,而uds_command例行公事把每一个电话都看作是一个需要发送的电话。在这种模式下,所有可用的输入在到达套接字时都会被读取并发送到Erlang,就像在活动模式中的gen_tcp插座。我们研究港口所需的状态。注意,并非所有字段都用于所有类型的端口。使用联合可以节省一些空间,但这会使代码中出现多个间接方向,因此这里使用一个结构来处理所有类型的端口,以提高可读性:( 1) typedef unsigned char Byte; ( 2) typedef unsigned int Word; ( 3) typedef struct uds_data { ( 4) int fd; /* File descriptor */ ( 5) ErlDrvPort port; /* The port identifier */ ( 6) int lockfd; /* The file descriptor for a lock file in ( 7) case of listen sockets */ ( 8) Byte creation; /* The creation serial derived from the ( 9) lock file */ (10) PortType type; /* Type of port */ (11) char *name; /* Short name of socket for unlink */ (12) Word sent; /* Bytes sent */ (13) Word received; /* Bytes received */ (14) struct uds_data *partner; /* The partner in an accept/listen pair */ (15) struct uds_data *next; /* Next structure in list */ (16) /* The input buffer and its data */ (17) int buffer_size; /* The allocated size of the input buffer */ (18) int buffer_pos; /* Current position in input buffer */ (19) int header_pos; /* Where the current header is in the (20) input buffer */ (21) Byte *buffer; /* The actual input buffer */ (22) } UdsData; 此结构用于所有类型的端口,尽管某些字段对某些类型没有用处。内存消耗最少的解决方案是将此结构安排为结构的一个联合。但是,在这样的结构中,代码中访问字段的多个间接方向会使代码过于混乱,而不是一个示例。结构中的字段如下:fd与端口关联的套接字的文件描述符。port此结构对应的端口标识符。它是大多数人所需要的。driver_XXX从驱动程序到模拟器的调用。lockfd如果套接字是侦听套接字,我们将使用单独的%28正则%29文件用于两个目的:
  • 我们希望有一种不提供竞争条件的锁定机制,确保另一个Erlang节点使用我们需要的侦听套接字名称,或者该文件仅保留在前面的%28crb%29会话中。
  • 我们储存creation文件中的序列号。大creation是一个数字,用于在同名的不同Erlang模拟器的不同实例之间进行更改,以便将来自一个模拟器的进程标识符发送到具有相同分发名的新模拟器时无效。创建可以从0到3%,282位%29,并且存储在发送到另一个节点的每个进程标识符中。

在基于tcp分发的系统中,此数据保存在Erlang端口mapper守护进程%28epmd%29,当分布式节点启动时会与其联系。锁文件和UDS侦听套接字%27s名称的约定消除了对epmd当使用此分发模块时。UDS总是被限制在一个主机上,为什么避免端口映射器很容易。

creation

侦听套接字的创建号,它的计算值为%28-锁文件+1%29 rem 4中的值。这个创建值也被写回锁文件中,以便下次对模拟器的调用在文件中找到我们的值。

type

端口的当前类型/状态,可以是上面声明的值之一。

name

套接字文件%28的名称--路径前缀删除了%29,这允许删除%28unlink当套接字关闭时,%29。

sent

通过套接字发送的字节数。这可以包装,但是对于发行版来说没有问题,因为Erlang发行版只关心这个值是否已经更改。%28 Erlangnet_kernelticker通过调用驱动程序来获取该值,这是通过erlang:port_control/3常规%29

received

从套接字读取%28接收%29的字节数,使用的方式类似于sent...

partner

指向另一个端口结构的指针,它要么是该端口接受连接的侦听端口,要么相反。“伙伴关系”总是双向的。

next

指向所有端口结构的链接列表中的下一个结构的指针。此列表在接受连接和卸载驱动程序时使用。

buffer_size%2A%2A,%2A%2Abuffer_pos%2A%2A,%2A%2Aheader_pos%2A%2A,%2A%2Abuffer

用于输入缓冲的数据。有关输入缓冲的详细信息,请参阅目录中的源代码。kernel/examples这肯定超出了本节的范围。

分发驱动程序实现的选定部分

这里没有完整地介绍分发驱动程序的实现,也没有解释有关缓冲和其他与驱动程序编写无关的事情的细节。同样,UDS协议的一些特性也没有详细解释。选择的协议并不重要。

驱动程序回调例程的原型可在erl_driver.h头文件。

驱动程序初始化例程为%28---通常是%29---使用宏声明,以使驱动程序更容易在不同操作系统%28和系统%29版本之间进行移植。这是唯一的例程,必须有一个明确的名称。所有其他回调都通过驱动程序结构实现。要使用的宏名为DRIVER_INIT并将驱动程序名作为参数:

代码语言:javascript
复制
(1) /* Beginning of linked list of ports */
(2) static UdsData *first_data;

(3) DRIVER_INIT(uds_drv)
(4) {
(5)     first_data = NULL;
(6)     return &uds_driver_entry;
(7) }      

例程初始化单个全局数据结构,并返回指向驱动程序条目的指针。当erl_ddll:load_driver是从Erlang打来的。

uds_start从Erlang打开端口时调用例程。在这种情况下,我们只分配一个结构并初始化它。创建实际的套接字留给uds_command例行公事。

代码语言:javascript
复制
( 1) static ErlDrvData uds_start(ErlDrvPort port, char *buff)
( 2) {
( 3)     UdsData *ud;
( 4)     
( 5)     ud = ALLOC(sizeof(UdsData));
( 6)     ud->fd = -1;
( 7)     ud->lockfd = -1;
( 8)     ud->creation = 0;
( 9)     ud->port = port;
(10)     ud->type = portTypeUnknown;
(11)     ud->name = NULL;
(12)     ud->buffer_size = 0;
(13)     ud->buffer_pos = 0;
(14)     ud->header_pos = 0;
(15)     ud->buffer = NULL;
(16)     ud->sent = 0;
(17)     ud->received = 0;
(18)     ud->partner = NULL;
(19)     ud->next = first_data;
(20)     first_data = ud;
(21)     
(22)     return((ErlDrvData) ud);
(23) }      

初始化每个数据项,以便在关闭新创建的端口%28而没有任何相应的套接字%29时不会出现问题。当open_port({spawn, "uds_drv"},[])是从Erlang打来的。

uds_command例程是当Erlang进程向端口发送数据时调用的例程。当端口位于command模式和在端口处于data模式:

代码语言:javascript
复制
( 1) static void uds_command(ErlDrvData handle, char *buff, int bufflen)
( 2) {
( 3)     UdsData *ud = (UdsData *) handle;

( 4)     if (ud->type == portTypeData || ud->type == portTypeIntermediate) {
( 5)         DEBUGF(("Passive do_send %d",bufflen));
( 6)         do_send(ud, buff + 1, bufflen - 1); /* XXX */
( 7)         return;
( 8)     } 
( 9)     if (bufflen == 0) {
(10)         return;
(11)     }
(12)     switch (*buff) {
(13)     case 'L':
(14)         if (ud->type != portTypeUnknown) {
(15)             driver_failure_posix(ud->port, ENOTSUP);
(16)             return;
(17)         }
(18)         uds_command_listen(ud,buff,bufflen);
(19)         return;
(20)     case 'A':
(21)         if (ud->type != portTypeUnknown) {
(22)             driver_failure_posix(ud->port, ENOTSUP);
(23)             return;
(24)         }
(25)         uds_command_accept(ud,buff,bufflen);
(26)         return;
(27)     case 'C':
(28)         if (ud->type != portTypeUnknown) {
(29)             driver_failure_posix(ud->port, ENOTSUP);
(30)             return;
(31)         }
(32)         uds_command_connect(ud,buff,bufflen);
(33)         return;
(34)     case 'S':
(35)         if (ud->type != portTypeCommand) {
(36)             driver_failure_posix(ud->port, ENOTSUP);
(37)             return;
(38)         }
(39)         do_send(ud, buff + 1, bufflen - 1);
(40)         return;
(41)     case 'R':
(42)         if (ud->type != portTypeCommand) {
(43)             driver_failure_posix(ud->port, ENOTSUP);
(44)             return;
(45)         }
(46)         do_recv(ud);
(47)         return;
(48)     default:
(49)         return;
(50)     }
(51) }      

命令例程接受三个参数;为端口返回的句柄uds_start,它是指向内部端口结构、数据缓冲区和数据缓冲区长度的指针。缓冲区是从Erlang%28a字节列表%29转换为字节%29的C数组%28的数据。

例如,如果Erlang发送列表[$a,$b,$c]到港口,bufflen变量是3buff变量包含{'a','b','c'}%28NONULL终止%29。通常第一个字节用作操作码,至少在端口位于command模式%29。操作码的定义如下:

'L'<socket name>

使用指定的名称在套接字上创建和侦听。

'A'<listen number as 32-bit big-endian>

从由指定标识号标识的侦听套接字接受。属性检索标识号。uds_control例行公事。

'C'<socket name>

连接到名为<socket name>...

'S'<data>

发送数据<data>关于已连接/接受的套接字%28incommand模式%29。当数据离开此进程时,将确认发送。

'R'

接收一包数据。

命令中的“一包数据”'R'可以解释如下。这个驱动程序总是发送带有一个4字节头的数据,其中包含一个大端32位整数,表示数据包中数据的长度。不需要不同的数据包大小或某种流模式,因为此驱动程序仅用于分发。当UDS套接字是主机本地的时,为什么头字在大端编码?编写分发驱动程序是一种很好的做法,因为在实践中分发通常跨越主机边界。

在第4-8行中,端口位于data模式或intermediate模式和其余例程处理不同的命令。例程使用driver_failure_posix()例程来报告错误%28,例如,请参见第15%行29。注意,故障例程调用uds_stop例程,它将删除内部端口数据。手柄%28和铸造手柄ud%29因此无效指针在...之后driver_failure打电话,我们应该立即返回运行时系统将向所有链接进程发送退出信号。

uds_input例程时,当以前传递给文件描述符的文件描述符上的数据可用时,将调用driver_select例行公事。这通常发生在发出读命令而没有可用数据时。大do_recv例行公事如下:

代码语言:javascript
复制
( 1) static void do_recv(UdsData *ud)
( 2) {
( 3)     int res;
( 4)     char *ibuf;
( 5)     for(;;) {
( 6)         if ((res = buffered_read_package(ud,&ibuf)) < 0) {
( 7)             if (res == NORMAL_READ_FAILURE) {
( 8)                 driver_select(ud->port, (ErlDrvEvent) ud->fd, DO_READ, 1);
( 9)             } else {
(10)                 driver_failure_eof(ud->port);
(11)             }
(12)             return;
(13)         }
(14)         /* Got a package */
(15)         if (ud->type == portTypeCommand) {
(16)             ibuf[-1] = 'R'; /* There is always room for a single byte 
(17)                                opcode before the actual buffer 
(18)                                (where the packet header was) */
(19)             driver_output(ud->port,ibuf - 1, res + 1);
(20)             driver_select(ud->port, (ErlDrvEvent) ud->fd, DO_READ,0);
(21)             return;
(22)         } else {
(23)             ibuf[-1] = DIST_MAGIC_RECV_TAG; /* XXX */
(24)             driver_output(ud->port,ibuf - 1, res + 1);
(25)             driver_select(ud->port, (ErlDrvEvent) ud->fd, DO_READ,1);
(26)         }
(27)     }
(28) }      

例程尝试读取数据,直到读取数据包或buffered_read_package例程返回NORMAL_READ_FAILURE%28是模块内部定义的常量,这意味着读取操作将导致EWOULDBLOCK29%。如果端口在command模式下,读取一个包时停止读取。如果端口在data模式下,将继续读取,直到套接字缓冲区为空%28读失败%29。如果无法读取更多的数据,并且需要更多的数据,则%28--当套接字位于data模式%29,driver_select被调用以使uds_input当有更多数据可供读取时,请调用回调。

当港口在data模式下,所有数据都以适合发行版的格式发送到Erlang。实际上,原始数据永远不会到达任何Erlang进程,而是将由模拟器本身翻译/解释,然后以正确的格式传递给正确的进程。在当前的模拟器版本中,接收到的数据将被标记为单个字节为100。这就是宏DIST_MAGIC_RECV_TAG被定义为。将来可以改变分布中数据的标记。

uds_input例程处理其他输入事件%28,如非阻塞。accept%29,但最重要的是通过调用do_recv*

代码语言:javascript
复制
( 1) static void uds_input(ErlDrvData handle, ErlDrvEvent event)
( 2) {
( 3)     UdsData *ud = (UdsData *) handle;

( 4)     if (ud->type == portTypeListener) {
( 5)         UdsData *ad = ud->partner;
( 6)         struct sockaddr_un peer;
( 7)         int pl = sizeof(struct sockaddr_un);
( 8)         int fd;

( 9)         if ((fd = accept(ud->fd, (struct sockaddr *) &peer, &pl)) < 0) {
(10)             if (errno != EWOULDBLOCK) {
(11)                 driver_failure_posix(ud->port, errno);
(12)                 return;
(13)             }
(14)             return;
(15)         }
(16)         SET_NONBLOCKING(fd);
(17)         ad->fd = fd;
(18)         ad->partner = NULL;
(19)         ad->type = portTypeCommand;
(20)         ud->partner = NULL;
(21)         driver_select(ud->port, (ErlDrvEvent) ud->fd, DO_READ, 0);
(22)         driver_output(ad->port, "Aok",3);
(23)         return;
(24)     }
(25)     do_recv(ud);
(26) }      

重要行是函数中的最后一行:do_read调用例程来处理新输入。剩下的函数处理侦听套接字上的输入,这意味着可以对套接字执行接受操作,这也是一个读取事件。

输出机制与输入类似。大do_send例行公事如下:

代码语言:javascript
复制
( 1) static void do_send(UdsData *ud, char *buff, int bufflen) 
( 2) {
( 3)     char header[4];
( 4)     int written;
( 5)     SysIOVec iov[2];
( 6)     ErlIOVec eio;
( 7)     ErlDrvBinary *binv[] = {NULL,NULL};

( 8)     put_packet_length(header, bufflen);
( 9)     iov[0].iov_base = (char *) header;
(10)     iov[0].iov_len = 4;
(11)     iov[1].iov_base = buff;
(12)     iov[1].iov_len = bufflen;
(13)     eio.iov = iov;
(14)     eio.binv = binv;
(15)     eio.vsize = 2;
(16)     eio.size = bufflen + 4;
(17)     written = 0;
(18)     if (driver_sizeq(ud->port) == 0) {
(19)         if ((written = writev(ud->fd, iov, 2)) == eio.size) {
(20)             ud->sent += written;
(21)             if (ud->type == portTypeCommand) {
(22)                 driver_output(ud->port, "Sok", 3);
(23)             }
(24)             return;
(25)         } else if (written < 0) {
(26)             if (errno != EWOULDBLOCK) {
(27)                 driver_failure_eof(ud->port);
(28)                 return;
(29)             } else {
(30)                 written = 0;
(31)             }
(32)         } else {
(33)             ud->sent += written;
(34)         }
(35)         /* Enqueue remaining */
(36)     }
(37)     driver_enqv(ud->port, &eio, written);
(38)     send_out_queue(ud);
(39) }      

此驱动程序使用writev系统调用将数据发送到套接字上。一种组合writev并且驱动程序输出队列非常方便。安ErlIOVec结构包含SysIOVec%28,相当于struct iovec中定义的结构uio.h...ErlIOVec还包含一个ErlDrvBinary指针,长度与I/O向量中的缓冲区数相同。您可以使用它为驱动程序中的队列“手动”分配二进制文件,但是这里的二进制数组中填充了NULL值%28行7%29。然后,运行时系统会在下列情况下分配自己的缓冲区:driver_enqv称为%28行37%29。

例程构建一个I/O向量,其中包含头字节和缓冲区%28,操作码已被删除,缓冲器长度被输出例程%29减少。如果队列为空,则直接将数据写入套接字%28,或至少尝试将数据写入%29。如果留下任何数据,则将其存储在队列中,然后尝试发送队列%28行38%29。当消息完全传递到%28行22%29时,将发送确认。大send_out_queue如果发送完成,则发送确认。如果端口在command模式下,Erlang代码会序列化发送操作,这样每次只能有一个数据包等待发送。因此,只要队列为空,就可以发送确认。

send_out_queue例行公事如下:

代码语言:javascript
复制
( 1) static int send_out_queue(UdsData *ud)
( 2) {
( 3)     for(;;) {
( 4)         int vlen;
( 5)         SysIOVec *tmp = driver_peekq(ud->port, &vlen);
( 6)         int wrote;
( 7)         if (tmp == NULL) {
( 8)             driver_select(ud->port, (ErlDrvEvent) ud->fd, DO_WRITE, 0);
( 9)             if (ud->type == portTypeCommand) {
(10)                 driver_output(ud->port, "Sok", 3);
(11)             }
(12)             return 0;
(13)         }
(14)         if (vlen > IO_VECTOR_MAX) {
(15)             vlen = IO_VECTOR_MAX;
(16)         } 
(17)         if ((wrote = writev(ud->fd, tmp, vlen)) < 0) {
(18)             if (errno == EWOULDBLOCK) {
(19)                 driver_select(ud->port, (ErlDrvEvent) ud->fd, 
(20)                               DO_WRITE, 1);
(21)                 return 0;
(22)             } else {
(23)                 driver_failure_eof(ud->port);
(24)                 return -1;
(25)             }
(26)         }
(27)         driver_deq(ud->port, wrote);
(28)         ud->sent += wrote;
(29)     }
(30) }      

我们只需从队列%28中选择一个I/O向量,它是整个队列的一个SysIOVec29%。如果I/O向量太长,则为%28IO_VECTOR_MAX定义为16%29,向量长度减少%28行15%29,否则writev调用%28行17%29失败。尝试写入,并且任何写入的内容都已被排除队列%28行27%29。如果写入失败EWOULDBLOCK%28请注意,所有套接字都处于非阻塞模式%29,driver_select被调用以使uds_output当有空间再次写入时,将调用例程。

我们继续尝试写入,直到队列为空或写入块。

上面的例程从uds_output例行公事:

代码语言:javascript
复制
( 1) static void uds_output(ErlDrvData handle, ErlDrvEvent event)
( 2) {
( 3)    UdsData *ud = (UdsData *) handle;
( 4)    if (ud->type == portTypeConnector) {
( 5)        ud->type = portTypeCommand;
( 6)        driver_select(ud->port, (ErlDrvEvent) ud->fd, DO_WRITE, 0);
( 7)        driver_output(ud->port, "Cok",3);
( 8)        return;
( 9)    }
(10)    send_out_queue(ud);
(11) }      

例程很简单:它首先处理的事实是,输出选择将涉及连接%28和连接阻塞%29的事务中的套接字。如果套接字处于连接状态,它只发送输出队列。这个例程被调用时,可以写到有输出队列的套接字上,所以没有问题要做什么。

驱动程序实现了一个控制接口,这是一个同步接口,当Erlang调用时调用它。erlang:port_control/3.只有此接口才能控制驱动程序在data模式。它可以用下列操作码调用:

'C'

设置端口command模式。

'I'

设置端口intermediate模式。

'D'

设置端口data模式。

'N'

获取侦听端口的标识号。此标识号用于对驱动程序的接受命令中。它作为一个大端32位整数返回,这是侦听套接字的文件标识符。

'S'

获取统计信息,即接收到的字节数、发送的字节数和输出队列中挂起的字节数。当分发版检查连接是否活动时使用此数据,%28勾选%29。统计信息以三个32位大数整数的形式返回。

'T'

发送一条滴答信息,这是长度为0的数据包。当端口在data模式,因此发送数据的命令不能使用%28,而且它忽略了command模式%29。当没有其他通信量时,这是由滴答器用来发送虚拟数据的。

注:重要的是,发送滴答的接口不是阻塞的。此实现使用erlang:port_control/3,这不会阻止呼叫者。如果erlang:port_command使用,使用erlang:port_command/3并通过[force]作为选项列表;否则,调用方可以在繁忙的端口上无限期地被阻塞,并阻止系统关闭一个不能正常工作的连接。

'R'

获取侦听套接字的创建号,该套接字用于挖掘锁文件中存储的数字,以区分同名Erlang节点的调用。

控制接口获得一个缓冲区来返回它的值,但是如果提供的缓冲区太小,则可以自由地分配它自己的缓冲区。大uds_control守则如下:

代码语言:javascript
复制
( 1) static int uds_control(ErlDrvData handle, unsigned int command, 
( 2)                        char* buf, int count, char** res, int res_size)
( 3) {
( 4) /* Local macro to ensure large enough buffer. */
( 5) #define ENSURE(N)                               \
( 6)    do {                                         \
( 7)        if (res_size < N) {                      \
( 8)            *res = ALLOC(N);                     \
( 9)        }                                        \
(10)    } while(0)

(11)    UdsData *ud = (UdsData *) handle;

(12)    switch (command) {
(13)    case 'S':
(14)        {
(15)            ENSURE(13);
(16)            **res = 0;
(17)            put_packet_length((*res) + 1, ud->received);
(18)            put_packet_length((*res) + 5, ud->sent);
(19)            put_packet_length((*res) + 9, driver_sizeq(ud->port));
(20)            return 13;
(21)        }
(22)    case 'C':
(23)        if (ud->type < portTypeCommand) {
(24)            return report_control_error(res, res_size, "einval");
(25)        }
(26)        ud->type = portTypeCommand;
(27)        driver_select(ud->port, (ErlDrvEvent) ud->fd, DO_READ, 0);
(28)        ENSURE(1);
(29)        **res = 0;
(30)        return 1;
(31)    case 'I':
(32)        if (ud->type < portTypeCommand) {
(33)            return report_control_error(res, res_size, "einval");
(34)        }
(35)        ud->type = portTypeIntermediate;
(36)        driver_select(ud->port, (ErlDrvEvent) ud->fd, DO_READ, 0);
(37)        ENSURE(1);
(38)        **res = 0;
(39)        return 1;
(40)    case 'D':
(41)        if (ud->type < portTypeCommand) {
(42)            return report_control_error(res, res_size, "einval");
(43)        }
(44)        ud->type = portTypeData;
(45)        do_recv(ud);
(46)        ENSURE(1);
(47)        **res = 0;
(48)        return 1;
(49)    case 'N':
(50)        if (ud->type != portTypeListener) {
(51)            return report_control_error(res, res_size, "einval");
(52)        }
(53)        ENSURE(5);
(54)        (*res)[0] = 0;
(55)        put_packet_length((*res) + 1, ud->fd);
(56)        return 5;
(57)    case 'T': /* tick */
(58)        if (ud->type != portTypeData) {
(59)            return report_control_error(res, res_size, "einval");
(60)        }
(61)        do_send(ud,"",0);
(62)        ENSURE(1);
(63)        **res = 0;
(64)        return 1;
(65)    case 'R':
(66)        if (ud->type != portTypeListener) {
(67)            return report_control_error(res, res_size, "einval");
(68)        }
(69)        ENSURE(2);
(70)        (*res)[0] = 0;
(71)        (*res)[1] = ud->creation;
(72)        return 2;
(73)    default:
(74)        return report_control_error(res, res_size, "einval");
(75)    }
(76) #undef ENSURE
(77) }      

ENSURE%28行5-10%29用于确保缓冲区足够大以满足应答。我们打开命令并采取行动。我们总是在端口上读取SELECT ACTIVEdata模式%28通过调用do_recv在第45%行29,但我们关闭读选择intermediatecommand模式%28行27和36%29。

驱动程序的其余部分或多或少是特定于UDS的,而不是一般感兴趣的。

6.3加起来

若要测试发行版,请将net_kernel:start/1函数可以使用。它很有用,因为它在运行中的系统上启动发行版,在那里可以执行跟踪/调试。大net_kernel:start/1例程将列表作为它的单个参数。列表中的第一个元素是节点名%28,而不是“@hostname”%29作为一个原子。第二种%28和最后一种%29元素是原子之一。shortnameslongnames在例子中,shortnames是首选。

net_kernel若要找出要使用哪个分发模块,请使用命令行参数。-proto_dist被使用了。它后面跟着一个或多个发行模块名称,带有后缀“[医]“删除”,也就是说,uds_dist作为分发模块指定为-proto_dist uds...

如果没有epmd%28 tcp端口mapper守护进程%29,还使用命令行选项。-no_epmd将被指定,这使得Erlang跳过epmd启动,既作为操作系统进程,也作为Erlang同上。

必须在引导时知道分发模块所在目录的路径。这可以通过指定-pa <path>在命令行上或通过构建包含用于分发协议的应用程序的引导脚本。占28%uds_dist协议,只有uds_dist应用程序需要添加到脚本中。%29

如果指定了上述所有内容,并且-sname <name>在命令行中存在标志。

例1:

代码语言:javascript
复制
$ erl -pa $ERL_TOP/lib/kernel/examples/uds_dist/ebin -proto_dist uds -no_epmd
Erlang (BEAM) emulator version 5.0 
 
Eshell V5.0  (abort with ^G)
1> net_kernel:start([bing,shortnames]).
{ok,<0.30.0>}
(bing@hador)2>

例2:

代码语言:javascript
复制
$ erl -pa $ERL_TOP/lib/kernel/examples/uds_dist/ebin -proto_dist uds \
-no_epmd -sname bong
Erlang (BEAM) emulator version 5.0 
 
Eshell V5.0  (abort with ^G)
(bong@hador)1>

ERL_FLAGS环境变量可用于将复杂的参数存储在:

代码语言:javascript
复制
$ ERL_FLAGS=-pa $ERL_TOP/lib/kernel/examples/uds_dist/ebin \
-proto_dist uds -no_epmd
$ export ERL_FLAGS
$ erl -sname bang
Erlang (BEAM) emulator version 5.0 
 
Eshell V5.0  (abort with ^G)
(bang@hador)1>

ERL_FLAGS不应包括节点名称。

代码语言:txt
复制
 ? 2010–2017 Ericsson AB

根据ApacheLicense,版本2.0获得许可。

扫码关注腾讯云开发者

领取腾讯云代金券

http://www.vxiaotou.com