首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 软件管理 > 软件架构设计 >

深入懂得Lustre文件系统-第3篇 LNET:Lustre网络

2012-08-22 
深入理解Lustre文件系统-第3篇 LNET:Lustre网络LNET是Lustre Networking的缩写,是Lustre的网络子系统,负责

深入理解Lustre文件系统-第3篇 LNET:Lustre网络

    LNET是Lustre Networking的缩写,是Lustre的网络子系统,负责提供消息传递API。LNET源自于Sandia Portals,但又与之存在着差异。

3.1      结构

    LNET由两部分组成:

LNET层。它以通信API的方式,向被称为LNET客户端的高层提供一个与网络类型无关的服务。在LNET层,以LNet作为前缀的函数名是为高层提供的外部API,而以lnet作为前缀的函数则是内部函数,只能被LNET层或下层的LND层调用。LND(Lustre Network Driver)层。它实现从一般化的LNET层到特定网络驱动之间的接口。那些LNET层可以调用的函数都以lnet作为前缀。

    这里需要的注意的是,LNET既被用来表示整个网络子系统,又被用来指代LND之上的一个软件层。我们不应该混淆它们的含义。

    LNET可以以内核模块的方式运行,也可以用户库的方式运行。因而,其中的LND层可以是内核模块,也可以是用户层程序。

图 LND支持的网络类型

名字

描述

socklnd

TCP/IP sockets

{cib,open}iblnd

Topspin IB

iiblnd

Silverstorm IB

viblnd

Voltaire IB

o2iblnd

OFA IB

ptllnd

Cray Portals

ralnd

Cray RapidArray

qswlnd

Quadrics Elan

gmlnd

Myricom GM (no RDMA)

mxlnd

Myricom MX

       另外,LND还支持TCP/IP sockets、OFA IB、Cray Portals等几种用户层网络。在下面的内容中,如果没有特殊说明,都指的是内核层实现。

       LNET的结构图如下图所示:

深入懂得Lustre文件系统-第3篇 LNET:Lustre网络

图 LNET的结构

3.2      网络驱动层

    Lustre支持一系列的网络类型。对每种网络类型的支持,都是以内核模块的形式提供的。在这些内核模块的初始化函数中,都会调用lnet_register_lnd,注册一个类型为lnd_t的全局变量。这个类型为lnd_t的对象,就描述了这个驱动的全部信息。

表 lnd_t对象提供的方法

字段

描述

lnd_startup

LNET打开这类网络接口时调用

lnd_shutdown

LNET关闭这类网络接口时调用

lnd_ctl

处理来自用户空间的ioctl命令。LNET通过一个特殊的设备文件,支持许多ioctl,其中一些ioctl命令由LNET直接处理(例如,增加路由),另外一些必须传递到LND处理

lnd_send

发送正在送出的消息

lnd_recv

接收正在来到的消息

lnd_eager_recv

消息的处理由于缺乏资源(转发缓冲或额度)而被推迟,调用此函数以期释放一些资源。

lnd_notify

一个peer挂了,调用这个函数告知LNET。被LNET路由调用

lnd_query

用来查询peer是否存活

lnd_accept

接受新的连接

3.3      核心数据结构

    LNET系统的状态维持在一个类型为lnet_t的全局对象the_lnet里。

    the_lnet对象的ln_lnds字段是系统中LND的链表。在每类网络的LND模块加载时,会调用lnet_register_lnd,在这个链表中添加一个项目。而在LND模块卸载时,则调用lnet_unregister_lnd,从这个链表中删除其所对应的一项。当LNET层需要使用合适的网络时,会首先从这个链表中查找并匹配到对应的项。用这种方法,LNET提供了对多种网络类型的支持。

    the_lnet对象的ln_portals字段是一个lnet_portal_t的对象数组。在Portal RPC一章,我们认识到,正确的portal是请求能够被发送到对应服务线程的关键。每个portal都由一个类型为lnet_portal_t的对象来描述。系统中使用的所有lnet_portal_t对象都被放置在the_lnet对象的ln_portals数组中。

    lnet_portal_t对象拥有两个字段ptl_mlist和ptl_mhash。前者是一个链表,后者是一个链表数组。它们都被用来把所有属于该portal的匹配项(Matching Entry,ME)组织起来。具体使用哪一个,可以通过设置lnet_portal_t对象的ptl_options字段来决定。如果使用后者,那么在定位匹配项链表时,需要通过计算哈希值来确定使用的是ptl_mhash的哪个元素。

    匹配项通过类型为lnet_me_t的对象来描述。通过me_list字段,它被加入到对应的lnet_portal_t对象的匹配项链表中。它的me_md字段,是一个类型为lnet_libmd的对象指针,指向该匹配项管理的内存描述符(memory descriptor,MD)。匹配项还定义了64比特的匹配位(me_match_bits字段)和忽略位(me_ignore_bits字段),用来确定达到的消息是否可以使用内存描述符中的缓冲空间。

    内存描述符通过类型为lnet_libmd的对象来描述。与此相关的,还有一种lnet_md_t类型。它们共有一些字段,但lnet_libmd_t为了进行内部管理维护了更多的状态。lnet_md_t则被用在LNET客户端,即ptlrpc层,使得lnet_libmd_t对之不透明,从而避免对LNET的内部状态产生干扰。

    lnet_libmd_t对象用一个I/O向量md_iov来描述缓冲区。这个I/O向量的类型是md_iov联合体中的struct iovec类型或lnet_kiov_t类型。具体使用那一种由lnet_libmd_t对象的md_options字段来决定。一般来说,内核中使用lnet_kiov_t来描述缓冲区,而用户使用struct iovec。正如我们前面所提到的,LNET是一个用户层和内核层通用的组件。

    lnet_libmd_t对象中缓冲区的大小由md_length字段给出。md_offset字段则给出了这个缓冲区的有效偏移量。为了描述这个字段的用法,我们假设请求缓冲大小是4KB,而每个请求最多是1KB。当第一个消息到达时,这个偏移量增加到1KB,而当第二个消息到达时,偏移量设置为2KB,如此继续。因此,从本质上说,偏移量是为避免覆盖写而用来追踪写位置的。

    lnet_libmd_t对象中的另外一个重要的字段是类型为lnet_eq_t的md_eq字段。在Portal RPC一章,我们提到,消息发送/接受完成或超时,都会在内存描述符的事件队列中产生一个事件。每个事件的处理都是通过调用回调函数来完成的,而回调函数则是把消息从LNET传递到Portal RPC的关键。

    注意到,内存描述符可能被关联到某个匹配项上,也有可能没有。这一点,将在下面的内容中涉及到。

    下图给出了上面提到的几个对象之间的关系。

深入懂得Lustre文件系统-第3篇 LNET:Lustre网络

图 关于LNET的各个对象之间的关系

3.4      块传输的流程

    在Portal RPC一章,我们知道ptlrpc_main()函数是所有服务线程池共同运行的主函数。在这个函数里进行的一项重要操作就是调用ptlrpc_server_post_idle_rqbds()函数发布一组请求缓冲描述符(request buffer descriptor,简写为rqbd)。请求缓冲描述符是由类型为ptlrpc_request_buffer_desc的对象描述的。每个请求缓冲描述符发布通过ptlrpc_register_rqbd()函数来完成。在这个函数里,它进行了一系列操作,包括:

创建一个匹配项,并将这个匹配项关联到服务对应lnet_portal_t对象的匹配项链表中。这一任务由LNetMEAttach()函数完成。我们在Portal RPC一章已经知道每种服务都对应一个portal。匹配项的各个字段被设置为确定的值。例如,由于服务对请求来自何方并无限制,因此匹配项的me_match_id字段被设置为{LNET_NID_ANY, LNET_PID_ANY}。其意义是不管全局进程ID(globalprocess ID)是什么值,具体地,不管它的节点ID字段nid是什么值,也不管它的进程ID字段pid是什么值,都将请求匹配到这个匹配项。全局进程ID由类型为lnet_process_id_t的对象描述,它是LNET对端(peer)的唯一区分标识。创建一个内存描述符,并将它关联到上面提到的匹配项上。这一任务由LNetMDAttach ()函数完成。

    通过上述活动,服务器已完成了接受请求的准备。

    同样是在Portal RPC一章,我们分析到所有请求发送都是通过ptl_send_rpc()发起的。这个函数将类型为ptlrpc_request 的对象作为一个输入参数。我们假设客户端在发送请求,此时ptl_send_rpc()函数可能进行如下操作:

如果请求的块缓冲不为空,即ptlrpc_request 对象的rq_bulk不为NULL,客户端通过ptlrpc_register_bulk()发布一个块缓冲。这个函数将通用调用LNetMEAttach()和LNetMDAttach()函数。客户端通过LNetMEAttach()和LNetMDAttach()函数发布回复缓冲,这个操作是在客户端期望得到回复消息的时候才被执行(即输入参数noreply被设置为0时)。客户端通过ptl_send_buf()函数发送请求。这个函数将首先调用LNetMDBind(),然后调用LNetPut()函数。LNetMDBind()函数将会新建一个“自由漂浮”(free floating)的内存描述符,即这个新建的内存描述符没有被关联到哪个匹配项上。这些内存描述符最终将在发送完成或失败后,被LNetMDUnlink()函数释放。LNetPut()函数则负责初始化一个异步的PUT操作。我们需要注意到,LNetPut()和与之相对的LNetGet()函数都使用一个“自由漂浮”的内存描述符作为输入参数,这个内存描述符都由LNetMDBind()函数新建。

    在服务器接受到请求时,会由LNET层发起对request_in_callback()的回调。这一点在Portal RPC一章也有介绍。这个函数会唤醒等待工作的服务线程。服务线程根据请求的类型,可能会进行两个与消息传输有关的动作:

    1.       调用ptlrpc_start_bulk_transfer()函数进块传输。这个函数通常由target_bulk_io()函数调用。在发起块传输之前,还需要做两件事情:

调用ptlrpc_prep_bulk_exp()函数为块传输准备好出口。我们在Portal RPC一章了解到,一种服务既有请求portal,也有回复portal,它们都被作为服务的初始化函数ptlrpc_init_svc()的参数。现在,我们又接触到了服务的另外一种portal,那就是块传输portal,它被用在ptlrpc_prep_bulk_exp()函数的参数中。反复调用ptlrpc_prep_bulk_page()函数为块传输准备好块传输页。在这个函数中所新建的传输页,最终将被LNetMDBind()函数放入到一个“自由浮动”的内存描述符中,并通过LNetGet()函数或LNetPut()函数填充接到或发出的块数据。

    2.       调用ptlrpc_reply()函数发送回复消息。

    这些消息返回到客户端,可能引发多个回调,如client_bulk_callback()和reply_in_callback()。

    为了总结上面的分析结论,我们假设客户端想要向服务器读若干个块数据。为此,它要准备两个缓冲:一个是为块RPC提供的与块portal相关联的缓冲,一个是与恢复Portal相关联的缓冲。然后,它向服务器发送一个RPC请求,表明它想读取若干个块。接到请求后,服务器准备好块缓冲,装填好数据,然后初始化块传输。当服务器完成传输后,它通过发送一个回复来告知客户端。客户端在接收完块数据和回复消息后,会通过调用回调函数完成处理。

3.5      启动

    LNET的启动函数是LNetNIInit()。这个函数里初始化了LNET网络接口、路由等各种部分,我们这里指分析初始化网络接口,即lnet_startup_lndnis()函数。这个函数的流程如下:

    1.      调用lnet_parse_networks()函数来分析用户提供的模块参数。这些模块参数一般写在/etc/modprobe.conf文件中。通过分析这些参数,可以获得一个需要启动的网络接口列表。

    2.      对上面列表中的所有网络接口进行如下操作。

    3.      调用lnet_find_lnd_by_type()函数通过网络接口的类型查找驱动。如果没有找到驱动,可能是驱动还没有加载,所以将尝试调用request_module()函数加载模块,然后重试定位驱动。

    4.      在定位了网络接口的驱动之后,我们可以将驱动绑定到该接口。

    5.      调用驱动的lnd_startup方法,启动该网络接口。

    6.      将该网络接口加入到the_lnet全局变量的网络接口列表中。

    至此,所有使用的网络接口都被初始化好了,可以通过这些接口发送或接受数据。

3.6      发送

    所有LNET消息都是由LNetPut()函数发送的。这个函数接受的主要参数包括由lnet_handle_md_t描述的要发送的内存描述符句柄,由lnet_nid_t描述的本地接口,由lnet_process_id_t描述的目标进程ID。LNetPut()函数的流程如下:

    1.      调用lnet_msg_alloc()函数分配一个类型为lnet_msg_t 的对象消息描述符。这个消息描述符将传递给LND。该对象里包含的一个重要字段是类型为lnet_hdr_t的msg_hdr字段。hdr是消息头(header)的缩写,顾名思义,这个对象描述了消息头信息,它将最终成为传输线上的消息的一部分。

    2.      调用lnet_commit_md()函数将待发送的内存描述符与消息相关联。

    3.      填入消息细节。被填入的细节包括是消息的类型(PUT或者GET)、匹配位、portal索引、偏移量等等。在发送和接受时都填充的字段由lnet_prep_send()函数完成,因此这个函数也被LNetGet()函数调用。

    4.      填入事件信息。lnet_msg_t 对象的msg_ev字段是一个类型为lnet_event_t的对象。

    5.      调用lnet_send()函数发送这个消息。

    lnet_send()函数的流程如下:

    1.      如果消息要发送到本地,则选取直连接口作为路由节点。路由节点由类型为lnet_peer_t的对象来描述。

    2.      如果消息要发往网络上的其他节点,则需要根据目标nid选取一个最佳的路由和路由节点。路由节点的是该路由最终到达的目标节点。路由由类型为lnet_route_t的对象来描述。为了选取最佳路由,首先要从目标nid中取得远程网络,它由类型为lnet_remotenet_t的对象描述。这个对象保存了从本地到目标的所有可能的路由,因此可以根据这些路由的跳数(hop)、等待发送的字节数和可用额度(credit)来选取最佳的路由。

    3.      调用lnet_post_send_locked()函数检查额度。

    4.      调用lnet_ni_send()函数发送消息。这个函数的参数除了有被发送的消息消息描述符lnet_msg_t对象外,还有一个由lnet_ni_t对象描述的网络接口(network insterface)。lnet_ni_t对象可能直接由lnet_send()函数的输入参数lnet_nid_t决定,也可能被设定为直连接口,还可能由选取的路由决定。

    lnet_post_send_locked()函数的流程如下:

    1.      检查目标路由节点是否活跃,不活跃则返回节点无法连接的错误。

    2.      为消息分配一个路由节点发送额度。这个额度将从路由节点的发送额度中扣除。如果该额度被扣为负数,则返回重新此次操作错误。

    3.      为消息分配一个发送额度。这个额度将从路由节点网络接口(networkinterface)的发送额度中扣除。如果该额度被扣为负数,则返回重新此次操作错误。

    4.      如果lnet_post_send_locked()函数的do_send参数被使能,将调用lnet_ni_send()函数发送消息。但是在lnet_send()函数调用lnet_post_send_locked()函数时,并没有使能这个参数,而是自己调用了lnet_ni_send()函数。

    lnet_ni_send()函数的流程如下:

    1.      调用特定网络接口所对应的LND方法lnd_send。

    2.      如果该发送方法返回失败,调用lnet_finalize()函数终止这次发送。

    在以后的某一时间点,在LND发送消息结束后,将调用lnet_finalize()来告知LNET层,消息已经发送完毕。

3.6.1       套接字LND的发送

    为了继续深入分析发送消息的过程,我们假设这是一个IP网络。此时使用的是套接字LND,其lnd_send方法是ksocknal_send()。这个函数的流程如下:

    1.      调用ksocknal_alloc_tx()函数申请一个套接字发送报文,它由类型为ksock_tx_t的对象描述。

    2.      根据LNET消息填充套接字发送报文的各个字段。

    3.      调用ksocknal_launch_packet()函数发起报文的发送。

    ksocknal_launch_packet()函数的流程如下:

    1.      调用ksocknal_find_peer_locked(),通过网络接口和目标进程号查找到套接字端(peer)描述符,这个端描述符由类型为ksock_peer_t的对象描述。

    2.      如果找到对应的套接字端描述符,而且已经建立好了连接,就通过ksocknal_queue_tx_locked()把报文加入该连接的发送队列中。套接字LND是基于连接的,这种连接由类型为ksock_conn_t的对象描述。

    3.      如果没有找到对应的套接字端描述符,则尝试通过ksocknal_add_peer()添加这个端描述符。然后回到第1步重试一次。

    4.      如果套接字端描述符已存在,但是还未连接好,那么暂时先将消息报文排队到套接字端描述符的等待发送队列中。这样当建立了新连接后,从套接字端描述符的等待发送队列中将报文移至连接的发送队列里,然后将它们发送出去。

    报文的发送是一个异步的过程,ksocknal_launch_packet()函数在把报文加入加入连接的发送队列中之后就返回了,而实际进行报文的发送或接收的是一组线程名前缀为socknal_sd的内核线程。这组内核线程运行ksocknal_scheduler()函数。这个函数接受一个类型为ksock_sched_t的参数,这个参数描述了该调度器的状态。这个函数反复地通过如下流程进行消息收发:

    1.      如果调度器的接收连接链表不为空,那么调用ksocknal_process_receive()函数尝试从链表首个连接接受报文,然后把该连接放置到链表的尾部。我们可以看到,这是一个近似公平的调度方式。

    2.      如果调度器的发送连接链表不为空,那么调用ksocknal_process_transmit()函数尝试从链表首个连接发送消息,然后把该连接放置到链表的尾部。

    3.      如果此次调度没有任何发送,那么就进入睡眠,直到ksocknal_recv()函数ksocknal_queue_tx_locked()把自己唤醒。

    4.      如果该流程反复运行超过了一定次数,则调用cond_resched()函数来进行进程切换,并将运行次数清零。

    这里我们只分析发送过程。ksocknal_process_transmit()函数的流程如下:

    1.      如果消息可以通过零拷贝(zero copy,ZC)方式发送,那么将该消息(由ksock_msg_t对象描述)的ksm_zc_cookies字段将被设置,并将该报文加入套接字端描述符的零拷贝请求链表中。这个链表中的所有请求都在等待零拷贝ACK。零拷贝是套接字提供的一种发送方式,它使得可以不复制缓冲,而直接把报文发送出去。最终这个报文将通过套接字操作的sendpage方法发送。而与此相比,非零拷贝方式则是通过套接字操作的sendmsg方法发送,在这个方法中将进行一次缓冲的复制。不过,零复制也有额外开销,因此Lustre只用零拷贝方式发送长度超过一定限度的报文。

   2.      调用ksocknal_transmit()函数发送这个消息。在发送的时候,就是通过判断消息的ksm_zc_cookies字段来确定是否要进行零拷贝发送的。

3.6.2       套接字LND的零拷贝发送

   在早先的Lustre版本中(2007年之前),对零拷贝的支持需要向Linux内核打补丁。这样内核才能告知零拷贝的发起者,被发送的页可以被覆盖写了。最终,通过引入零拷贝ACK(ZC-ACK),避免了打内核补丁的需要。

   零拷贝ACK是一个空的消息,它的消息类型是KSOCK_MSG_NOOP。KSOCK_MSG_NOOP这类消息旨在套接字LND层就被处理了,LNET不知道该消息的存在。

   上面到ksocknal_process_transmit()函数在发现消息是通过零拷贝方式发送时,会把该消息放到套接字端描述符的零拷贝请求链表中保存,以等待接收到零拷贝ACK。接收者接到消息之后,会回复一个零拷贝ACK。发送者接到这个ACK之后,调用ksocknal_handle_zcack()函数将该消息从套接字端描述符的零拷贝请求链表中删除,并减少对这个消息的引用,如果引用降为零,这个消息将被ksocknal_tx_done()函数释放。一般的消息在被ksocknal_tx_done()函数释放时会调用lnet_finalize()通知LNET层,但由于零拷贝ACK的消息类型是KSOCK_MSG_NOOP,因此将不会通知到LNET层。

   套接字消息的格式如下所示:

深入懂得Lustre文件系统-第3篇 LNET:Lustre网络

3.7      接收3.7.1       套接字LND的接收

   我们这里仍以套接字LND为例,分析LNET接收的流程。前面提到,有一组线程运行着ksocknal_scheduler()函数。这些线程既进行消息的发送也进行消息的接收。在接收消息时,ksocknal_scheduler()函数调用的是ksocknal_process_receive()函数。这个函数实际上是一个根据连接的接收状态来进行运转的状态机。这个状态机可能出现以下状态:

SOCKNAL_RX_KSM_HEADER状态,表明该连接正准备接收套接字的消息头。在每次接收消息失败或完成之后,ksocknal_new_packet()函数将状态机设置为该状态。在这个状态下,状态机期望下次能一次接收到字节数等于ksock_msg_t对象的消息头大小。SOCKNAL_RX_LNET_HEADER状态,表明该连接正准备接收LNET消息头。每次状态机成功的接收了套接字消息头之后,进入这一状态。同时注意到对KSOCK_MSG_NOOP类型的消息,由于不具有LNET消息部分,因此不进入SOCKNAL_RX_LNET_HEADER状态,就直接接收成功了。在这个状态下,状态机期望下次能一次接收到字节数等于ksock_lnet_msg_t对象的大小。SOCKNAL_RX_PARSE状态,表明消息正在被lnet_parse()函数解析。在状态机成功接收到了LNET消息头,就进入这一状态。在这种状态下,会调用lnet_parse()函数。这个函数可能会调用lnet_ni_recv()函数,最终通过ksocknal_recv()函数把连接状态改变为SOCKNAL_RX_LNET_PAYLOAD。SOCKNAL_RX_LNET_PAYLOAD状态,表明正在等待接收消息的有效载荷。我们将在下面的内容中详细分析lnet_parse()函数。在这种状态下,状态机期望下次能一次接收到所有的有效载荷,这个有效载荷被放置在LNET消息头对象lnet_hdr_t的payload_length字段中,由lnet_parse()函数负责解析出来。SOCKNAL_RX_SLOP状态,表明接收消息的有效载荷出现了意外,如消息被截断或校验和出错。在这个状态下,将调用ksocknal_new_packet()函数,最终回到SOCKNAL_RX_KSM_HEADER状态3.8      基于RMDA的消息传输

   上面的分析大多以套接字LND作为例子。在套接字LND的消息接收中存在者内存到内存的一次复制。而对于任一支持RDMA的网络,例如o2ib LND,它可以使用RMDA将数据直接传输到目的内存描述符,从而避免了内存到内存的拷贝。在这里我们以o2ibLND为例分析基于基于RDMA的发送流程。

   与套接字LND类似,o2ib LND的发送方法kiblnd_send()也只是将消息准备好,放入发送队列中就返回了。但与套接字LND不同的是它要项OFED注册要发送的内存。kiblnd_send()函数在发送LNET_MSG_PUT和LNET_MSG_GET类型的消息时所进行的处理是类似的,其流程如下:

   1.      取得一个空闲的kib_tx_t类型的对象。

   2.      调用kiblnd_setup_rd_iov()函数或kiblnd_setup_rd_kiov()函数将存有有效载荷的内存描述符中的缓冲注册到OFED中去。最终这个注册动作由ib_reg_phys_mr()函数完成。在注册内存后,它取得了一个OFED内存ID,它与注册好的内存等价。

   3.      调用kiblnd_init_tx_msg()函数新建一个IBLND_MSG_PUT_REQ或IBLND_MSG_GET_REQ类型的消息,并调用kiblnd_queue_tx()函数把它加入连接的发送队列中。该消息中携带了注册好的内存ID。

   4.      调用kiblnd_launch_tx()函数。

   只将消息通过kiblnd_init_tx_msg()函数准备好,然后通过kiblnd_launch_tx()函数放入发送队列就返回了。实际的发送操作由运行kiblnd_scheduler()的一组线程名前缀为kiblnd_sd_的线程来完成。

   kiblnd_scheduler()函数反复进行如下流程:

   1.      如果类型为kib_data_t的全局变量kiblnd_data的连接链表不为空,取得链表首部的连接,并从该链接中删除。我们可以发现这里与套接字存在着一些差异。其中之一是,每个套接字调度线程独占的调度器,它由类型为ksock_sched_t的对象描述,它们各自负责一部分连接,而所有o2ib调度线程则共享一个连接链表。并且我们应该注意到,kiblnd_scheduler()函数并不会在接受或发送成功后显式地把连接放回链表尾部,而是交由kiblnd_cq_completion()函数来完成。kiblnd_cq_completion在连接创建的函数kiblnd_create_conn()中就被注册成传输完成时的调用的回调函数,这个注册动作在ib_create_cq()函数中完成。

   2.      调用ib_poll_cq()函数等待完成队列(completion queue)产生完成事件。我们知道Infiniband网络的通信是通过事件通知的方式来进行的。

   3.      如果ib_poll_cq()函数的返回值表明有事件需要处理则调用kiblnd_complete()函数处理时间。这里产生的时间既有可能是RDMA失败事件(类型为IBLND_WID_RDMA),也有可能是发送完成事件(类型为IBLND_WID_TX),还有可能是接收完成事件(类型为IBLND_WID_RX)。后两种时间分别调用kiblnd_tx_complete()函数和kiblnd_rx_complete()来进行处理。

    kiblnd_tx_complete()函数的处理比较简单,其中一个操作就是调用kiblnd_check_sends()函数把该连接的各种等待发送队列上所有消息全都通过kiblnd_post_tx_locked()函数发送出去。最终的发送由IB网提供的ib_post_send()函数接口来完成。

   我们应该注意到,kiblnd_check_sends()函数不仅会被kiblnd_tx_complete()函数调用,在包括接收完成时的其他情况下也会被调用。事实上,这个函数一有机会就会被调用,以尽快把消息发送出去。

   kiblnd_rx_complete()函数的流程如下:

   1.      调用kiblnd_unpack_msg()函数解包收到的消息。

   2.      调用kiblnd_handle_rx()函数处理收到的消息。

   kiblnd_handle_rx()函数会根据消息的类型进行处理。让我们假设这个消息的类型是IBLND_MSG_PUT_REQ,此时接收端需要准备接受一个RDMA的推送请求。在这个请求报文里面有LNET头信息,因此kiblnd_handle_rx()函数调用lnet_parse()进行解析,由报文中的匹配位匹配到本地的内存描述符,这一动作发生在lnet_parse_put()调用lnet_match_md()时。在匹配好内存描述符时,lnet_parse_put()函数会调用lnet_recv_put()来接收这个推送请求。这个函数最终调用o2ib LND的kiblnd_recv()函数。

   kiblnd_recv()函数会根据消息的不同类型进行不同的处理。我们这里只分析消息是IBLND_MSG_PUT_REQ类型的情况。此时进行的操作流程与上面提到的kiblnd_send()函数在发送LNET_MSG_GET或LNET_MSG_PUT类型的消息时所采取的操作流程非常相似,也要把内存描述符中的接收缓冲注册到OFED中去,只不过消息类型变为IBLND_MSG_PUT_ACK,而且是最后一步发生了变化。在最后一步,kiblnd_recv()不是调用kiblnd_launch_tx()函数,而是调用kiblnd_queue_tx ()函数。在kiblnd_recv()函数的最后,还将调用kiblnd_post_rx()函数。

   kiblnd_post_rx()函数首先调用ib_post_recv()函数准备接受消息,然后调用kiblnd_check_sends()函数尝试把所有该连接的消息发送出去,其中就包含了前面加入队列中的IBLND_MSG_PUT_ACK类型的消息。

   我们回到当初IBLND_MSG_PUT_REQ请求的发送方,它是RDMA推送请求的发起者,此时接到了类型为IBLND_MSG_PUT_ACK的消息。在kiblnd_handle_rx()函数里,对这个类型的消息的处理过程是:

   1.      调用kiblnd_init_rdma(),初始化RDMA的过程。

   2.      kiblnd_queue_tx_locked(),将RDMA操作通过ib_post_send()函数接口发起RDMA。

   至此,就完成了一次基于RDMA的消息传输流程。

3.9      路由

   一般来说,路由器(router)指的是一种通过转发数据包来实现网络互连的网络层设备。然而在这里的路由器指的是LNET提供的一种消息转发机制。在一个大型Lustre分布式系统中,可能存在多种类型的混合网络,这些网络中的节点可能作为Lustre的客户端或服务器。为了使得这些网络能够毫无阻碍地进行通信,LNET提供了路由功能。

   我们粗略地把Lustre网络的定义成一组可以相互之间直接通信而不需路由参与的节点群。那么LNET路由器就是具有多个网络接口,可以同时与多个Lustre网络网络通信并连接这些Lustre网络的节点。

   下图给出了一种Lustre集群的实例,这个实例中有多种类型的Lustre网络,网络之间通过LNET路由器进行通信。

深入懂得Lustre文件系统-第3篇 LNET:Lustre网络

   LNET路由是静态路由,拓扑是静态配置的,在系统初始化时就已配置分析完毕。在运行时,可以通过lctl的add_route或del_route命令更新LNET路由配置。这些命令最终会通过LNetCtl()函数调用相应的路由管理函数。我们可以发现,这种动态更新方式,与基于距离向量或者链接状态的路由存在着很大的不同。

   LNET路由转发消息的方式是先把整个消息接受完毕,然后再转发出去。我们仍以套接字LND为例分析这个过程。通过前面的分析已经知道,在ksocknal_process_receive()函数发现连接进入SOCKNAL_RX_PARSE状态时,就开始调用lnet_parse()函数。此时已经接收到了LNET的消息头,因此lnet_parse()函数可以通过消息头的相关信息知道该消息是不是最终目的地是自己,如果不是自己,那么意味着该消息需要转发到最终目的地。此时lnet_parse()会调用lnet_ni_recv()函数,指明把消息整个接收下来。在包括有效载荷在内的完整消息接收完毕后,lnet_finalize()将会被ksocknal_process_receive()函数调用。这个函数将调用lnet_complete_msg_locked()对接受完毕的消息做进一步处理。在自身是消息的LNET路由器的情况下,lnet_complete_msg_locked()函数会把这个消息转发到路由的下一跳。通过这个过程,完成了LNET路由的消息转发。

   在初始化时,LNET路由预分配了一个确定数量的路由缓冲,这个分配发生在lnet_init_rtrpools()函数中。这些路由缓冲的有效载荷大小被分为1MB(一个LNET消息可以携带的最大有效载荷量)、4KB和零,分别为别为大消息,小消息和诸如ACK等极小消息准备。

   由于路由缓冲是受限资源,为了防止单一端节点淹没路由,每个端节点(peer)都给定了一个额度。如果从一个特定端节点的请求超过了它的限额,那么它的下一个请求将不会被处理,直到路由缓冲被释放,这就是LNET层进行流控制的原理。更具体一点说,在lnet_parse()函数中,如果它调用lnet_post_routed_recv_locked()时发现额度耗尽,将会返回EAGAIN错误,该消息将被推迟处理,直到lnet_finalize()函数调用lnet_complete_msg_locked()释放额度,才重新处理这个转发消息。


本文章欢迎转载,请保留原始博客链接http://blog.csdn.net/fsdev/article

热点排行