使用libibverbs构建RDMA应用

本文是对论文Dissecting a Small InfiniBand Application Using the Verbs API所做的中英文对照翻译

Dissecting a Small InfiniBand Application Using the Verbs API

             Gregory Kerr∗
College of Computer and Information ScienceNortheastern UniversityBoston, MAkerrg@ccs.neu.edu

Abstract | 摘要

InfiniBand is a switched fabric interconnect. The InfiniBand specification does not define an API. However the OFED package, libibverbs, has become the default API on Linux and Solaris systems. Sparse documentation exists for the verbs API. The simplest InfiniBand program provided by OFED, ibv_rc_pingpong, is about 800 lines long. The semantics of using the verbs API for this program is not obvious to the first time reader. This paper will dissect the ibv_rc_pingpong program in an attempt to make clear to users how to interact with verbs. This work was motivated by an ongoing project to include direct InfiniBand support for the DMTCP checkpointing package.
InfiniBand是一种基于交换结构的网络互连方式。InfiniBand标准并没有定义API。然而,OFED软件包(libibverbs)已经成为了Linux和Solaris系统默认的API。一些稀疏的文档存在于verbs API中。OFED提供了一个最简单的IB程序(ibv_rc_pingpong),约800行代码。在这个程序中,使用的API的语义对第一次接触verbs的读者来说并不那么容易理解。本文将剖析ibv_rc_pingpong程序,以帮助读者弄清楚如何与verbs打交道。之所以写作本文,是因为一个正在进行的项目,该项目将在DMTCP检查点软件包中包含对IB的直接支持。

1 Introduction | 概述

The program ibv_rc_pingpong can be found at openfabrics.org, under the "examples/" directory of the OFED tarball. The source code used for this document is from version 1.1.4. The ibv_rc_pingpong program sets up a connection between two nodes running InfiniBand adapters and transfers data. Let's begin by looking at the program in action. In this paper, I will refer to two nodes: client and server. There are various command line flags that may be set when running the program. It is important to note that the information contained within this document is based on the assumption that the program has been run with no command line flags configured. Configuring these flags will alter much of the program's behavior.
程序ibv_rc_pingpong可以在penfabrics.org上找到,位于OFED压缩包的"examples/"目录下面。本文使用的源代码版本是1.1.4。程序ibv_rc_pingpong在两个具有IB适配器的节点之间建立连接并进行数据传输。让我们从看程序代码开始。在本文中,我将提及两个结点:client和server。当运行程序的时候,可以设置各种命令行标志。值得注意的是,本文中包含的信息基于一个假定,即该程序在运行时没有配置命令行标志。如果配置这些标志的话,将改变程序的许多行为。

Since both nodes run the same executable, the "client" is the instance that is launched with a hostname as an argument. The LID, QPN, and PSN will be explained later.
由于两个节点都运行相同的可执行文件,"client"是一个使用主机名作为参数的启动的实例。有关LID、QPN和PSN将稍后做解释。

复制代码

[user@server]$ ibv_rc_pingpong
local address:  LID 0x0008, QPN 0x580048, PSN 0x2a166f, GID ::
remote address: LID 0x0003, QPN 0x580048, PSN 0x5c3f21, GID ::
8192000 bytes in 0.01 seconds = 5167.64 Mbit/sec
1000 iters in 0.01 seconds = 12.68 usec/iter[user@client]$ ibv_rc_pingpong server
local address:  LID 0x0003, QPN 0x580048, PSN 0x5c3f21, GID ::
remote address: LID 0x0008, QPN 0x580048, PSN 0x2a166f, GID ::
8192000 bytes in 0.01 seconds = 5217.83 Mbit/sec
1000 iters in 0.01 seconds = 12.56 usec/iter

复制代码

Before we delve into the actual code, please look at a list of all verbs API functions which will be used for our purposes. I encourage the reader to pause and read the man page for each of these.
在深入研究实际代码之前,请看一下我们将用到的verbs API列表。读者朋友不妨暂停一下,先阅读一下这些API的man页面。

复制代码

 1 ibv_get_device_list(3) 2 ibv_open_device(3) 3 ibv_alloc_pd(3)4 ibv_reg_mr(3)5 ibv_create_cq(3) 6 ibv_create_qp(3) 7 ibv_modify_qp(3)8 ibv_post_recv(3) 9 ibv_post_send(3) 
10 ibv_poll_cq(3)
11 ibv_ack_cq_events(3)

复制代码

2 Layers | 分层

There are multiple drivers, existing in kernel and userspace, involved in a connection. See Figure 2a. To explain it simply, much of the connection setup work goes through the kernel driver, as speed is not a critical concern in that area.
在内核和用户空间,有多个驱动参与连接。请参见图2a。简单地说,大部分连接安装工作都是通过内核驱动完成,因为速度对建立连接来说并不关键。

The user space drivers are involved in function calls such as ibv_post_send and ibv_post_recv. Instead of going through kernel space, they interact directly with the hardware by writing to a segment of mapped memory. Avoiding kernel traps is one way to decrease the overall latency of each operation.
用户空间设备驱动参与到函数调用之中,例如ibv_post_send和ibv_post_recv。它们不经过内核空间,而是通过对内存映射段执行写操作来直接与硬件打交道。避免陷入内核是降低单个操作的总时延的一种方法。

3 Remote Direct Memory Access | RDMA(远程直接内存访问)

One of the key concepts in InfiniBand is Remote Direct Memory Access (RDMA). This allows a node to directly access the memory of another node on the subnet, without involving the remote CPU or software layers.
IB的核心概念之一就是RDMA(远程直接内存访问)。这允许一个结点直接访问子网内的另一个结点的系统内存,而不需要远端CPU或者软件层的干预。

Remember the key concepts of Direct Memory Access (DMA) as illustrated by Figure 2b.
图2b演示了DMA(直接内存访问)的核心概念。

In the DMA, the CPU sends a command to the hardware to begin a DMA operation. When the operation finishes, the DMA hardware raises an interrupt with the CPU, signaling completion. The RDMA concept used in InfiniBand is similar to DMA, except with two nodes accessing each other's memory; one node is the sender and one is the receiver.
在DMA中,CPU给硬件发送一个开始DMA操作的命令。当操作完成后,DMA硬件引发一个中断高速CPU, DMA操作已经完成。用在InfiniBand中RDMA概念与DMA类似,两个节点相互访问对方的系统内存,一个结点为sender(发送方), 另一个结点为receiver(接收方)。

Figure 3 illustrates an InfiniBand connection. In this case the DMA Hardware is the Host Channel Adapter (HCA), and the two HCAs are connected, through a switch, to each other. The HCA is InfiniBand's version of a network card; it is the hardware local to each node that facilitates communications. This allows an HCA on one node to use another HCA to perform DMA operations on a remote node.
图3演示了IB连接。在这个示例中,DMA硬件是HCA(主机通道适配器)。两个结点各有一个HCA,通过交换机互联。HCA就是InfiniBand的网卡。它是每个结点用来通信的硬件。这允许一个节点上的HCA使用另一个结点上的HCA来在远程节点上执行DMA操作。

4 Overview | 概要

The ibv_rc_pingpong program does the following.
程序ibv_rc_pingpong做了如下6件事情。

  1. Reserves memory from the operating system for sending and receiving data 在操作系统中申请内存,为发送和接收数据做准备
  2. Allocates resources from the verbs API 从verbs API中申请资源
  3. Uses a TCP socket to exchange InfiniBand connection information 使用TCP socket交换IB连接信息
  4. Creates a connection between two InfiniBand ports 在两个IB端口中创建一个连接
  5. Transfers data over the connection 在连接上传输数据
  6. Acknowledges the successful completion of the transfer 在传输成功完成后给一个ACK


5 Data Transfer Modes | 数据传输模型

The InfiniBand specification states four different connection types: Reliable Connection (RC), Unreliable Connection (UC), Reliable Datagram (RD), Unreliable Datagram (UD). This program, ibv_rc_pingpong uses a simple RC model. RD is not supported by current hardware.
InfiniBand技术规范定义了四种不同的连接类型:可靠连接(RC)、不可靠连接(UC)、可靠数据报(RD)和不可靠数据报(UD)。ibv_rc_pingpong程序使用的是简单的可靠连接(RC)。目前硬件不支持可靠数据报(RD)。

The difference between reliable and unreliable is predictable -- in a reliable connection data is transferred in order and guaranteed to arrive. In an unreliable connection neither of those guarantees is made.
可靠与不可靠的区别在于是否可预测 -- 在可靠连接中,数据按顺序到达并保证能够到达。在不可靠连接中,这两项都保证不了。

A connection type is an association strictly between two hosts. In a datagram, a host is free to communicate with any other host on the subnet.
连接类型是主机之间的严格关联。在数据报中,主机可以自由地与子网上的任何其他主机进行通信。

6 Queue Based Model | 基于队列的模型

The InfiniBand hardware processes requests from the client software through requests, which are placed into queues. To send messages between nodes, each node must have at minimum three queues: a Send Queue (SQ), Receive Queue (RQ), and Completion Queue (CQ).
IB硬件处理来自client的请求是通过把请求放置到队列上。在两个结点之间发送数据,每一个结点至少包含3个队列:发送队列(SQ)、接收队列(RQ)和完成队列(CQ)。

In a reliable connection, used in the ibv_rc_pingpong program, queue pairs on two distinct hosts compromise an end-to-end context. They send messages to each other, and only each other. This paper restricts itself to this mode.
在用于ibv_rc_pingpong程序的可靠连接中,两个独立的主机之间的QP(队列对)协商一个端到端的上下文。他们互相传递信息,而且只在彼此之间。本文的讨论仅限于这一模式。

The queues themselves exist on the HCA. However the libibverbs will return to the user a data structure which corresponds with the QP. While the library will create the QP, the user assumes the responsibility of "connecting" the QP with the remote node. This is generally done by opening an out-of-band socket connection, trading the identification numbers for the queues, and then updating the hardware with the information.
队列本身存在于HCA硬件上。libibverbs将返回一个跟QP一致关联的数据结构给用户。QP的创建由libibverbs函数库负责,但连接远端结点的QP则由用户自己负责。通常的实现是打开一个带外socket连接,为队列交换标识符,然后把信息更新到硬件上。

More recently, librdma_cm (an OFED library for connection management) allows a user to create and connect QPs through library calls reminiscent of POSIX sockets. Those calls are outside the scope of this document.
最近,librdma_cm(OFED针对连接管理发布的函数库)允许用户创建和连接QP, 通过调用POSIX socket。这些调用不在本文的讨论范围之内。

6.1 Posting Work Requests to Queues | 给队列里放置工作请求

To send and receive data in the InfiniBand connection (end-to-end context), work requests, which become Work Queue Entries (WQE, pronounced "wookie") are posted to the appropriate queue. These work requests point to lists of scatter/gather elements (each element has an address and size associated with it). This is a means of writing to and reading from buffers which are non-contiguous in memory.
在InfiniBand连接(即是端到端的上下文)中发送和接收数据,工作请求(即工作队列元素,WQE,发音为wookie)被放置到相应的队列中。这些工作请求指向一个分散/聚合元素(SGE)的列表(每个元素都有一个虚拟内存地址和与之相关联的缓冲区大小)。这是一种在非连续的内存缓冲区中写入和读取数据的方法。

The memory buffers must be registered with the hardware; that process is explained later. Memory buffers must be posted to the receive queue before the remote host can post any sends. The ibv_rc_pingpong program posts numerous buffers to the receive queue at the beginning of execution, and then repopulates the queue as necessary. A receive queue entry is processed when the remote host posts a send operation.
内存缓冲区必须在硬件中注册,注册过程稍后予以解释。在远程主机放置任何发送请求到发送队列之前,内存缓冲区必须在放置到接收队列。ibv_rc_pingpong程序在开始执行的时候放置大量的缓存区到发送队列,然后在必要的时候重新填充队列。当远程主机放置一个发送操作的时候,接收队列条目将被处理。

When the hardware processes the work request, a Completion Queue Entry (CQE, pronounced "cookie") is placed on the CQ. There is a sample of code showing how to handle completion events in ibv_ack_cq_events(3).
当硬件处理一个工作请求(WR)的时候,一个完成队列条目(CQE,发音为cookie)被放置到完成队列(CQ)中。在ibv_ack_cq_events(3)中,有样本代码演示如何处理完成事件。

7 Connecting the Calls | 连接使用的函数调用

The table below which the function calls used in ibv_rc_pingpong to create a connection, and the order in which they are called.
在下表中,列出了ibv_rc_pingpong创建一个连接使用的函数以及调用顺序。

This table introduces the resources which are allocated in the process of creating a connection. These resources will be explained in detail later.
上面的表格也介绍了在创建一个连接中分配的资源,对这些资源稍后做解释。

8 Allocating Resources | 分配资源

8.1 Creating a Context | 创建上下文

The first function call to the verbs API made by the ibv_rc_pingpong source code is here:

619         dev_list = ibv_get_device_list(NULL);

As the man page states, this function returns a list of available HCAs.
ibv_rc_pingpong源代码调用的第一个verbs API就是ibv_get_device_list(), 该函数在手册里有明确说明,返回一个可用的HCA列表。

The argument to the function is an optional pointer to an int, which the library uses to specify the size of the list.
该函数的参数是一个可选的执行int的指针,用来指定列表的长度。

Next it populates the pingpong_context structure with the function pp_init_ctx.
接下来,在函数pp_init_ctx填充pingpong_context结构体。

The pingpong_context structure wraps all the resources associated with a connection into one unit.
pingpong_context结构体包装了与连接相关联的所有分配的资源。

复制代码

/* Listing 1: struct pingpong context */59 struct pingpong_context {
60         struct ibv_context      *context;
61         struct ibv_comp_channel *channel;
62         struct ibv_pd           *pd;
63         struct ibv_mr           *mr;
64         struct ibv_cq           *cq;
65         struct ibv_qp           *qp;
66         void                    *buf;
67         int                      size;
68         int                      rx_depth;
69         int                      pending;
70         struct ibv_port_attr     portinfo;
71 };

复制代码

/* Listing 2:  Initializing the struct pingpong context */643         ctx = pp_init_ctx(ib_dev, size, rx_depth, ib_port, use_event, !servername);

The ib_dev argument is a struct device * and comes from dev_list. The argument size specifies the size of the message to be sent (4096 bytes by default), rx_depth sets the number of receives to post at a time, ib_port is the port of the HCA and use_event specifies whether to sleep on CQ events or poll for them.
参数ib_dev来自于dev_list, 类型为struct device *。参数size指定发送的消息长度(默认4096字节),rx_depth设置一次放置到接收队列的接收请求的个数(即接收队列深度),ib_port是HCA的port,use_event指定是在CQ事件上睡眠或还是进行轮询。

The function pp_init_ctx first allocates a buffer of memory, which will be used to send and receive data. Note that the buffer is memalign-ed to a page, since it is pinned (see section 8.3 for a definition of pinning).
函数pp_init_ctx首先分配内存缓冲区,用于发送和接收数据。注意缓冲区是按页对齐的,由于该缓冲区是需要被凝固的(有关pinning的定义,请参见8.3节)。

复制代码

/* Listing 3: Allocating a Buffer */320         ctx->buf = memalign(page_size, size);
321         if (!ctx->buf) {
322                 fprintf(stderr, "Couldn't allocate work buf.\n");
323                 return NULL;
324         }
325
326         memset(ctx->buf, 0x7b + is_server, size);

复制代码

Next the ibv_context pointer is populated with a call to ibv_open_device. The ibv_context is a structure which encapsulates information about the device opened for the connection.
接下来,ibv_context指针被赋值,通过调用ibv_open_device()。ibv_context是一个结构体,封装了为建立连接而打开的设备信息。

复制代码

/* Listing 4: Opening a Context */328         ctx->context = ibv_open_device(ib_dev);
329         if (!ctx->context) {
330                 fprintf(stderr, "Couldn't get context for %s\n",
331                         ibv_get_device_name(ib_dev));
332                 return NULL;
333         }

复制代码

From the "infiniband/verbs.h" header, the struct ibv_context is as follows:
结构体ibv_context来自头文件"infiniband/verbs.h",如下所示:

复制代码

/* Listing 5: struct ibv_context */766 struct ibv_context {
767         struct ibv_device      *device;
768         struct ibv_context_ops  ops;
769         int                     cmd_fd;
770         int                     async_fd;
771         int                     num_comp_vectors;
772         pthread_mutex_t         mutex;
773         void                   *abi_compat;
774         struct ibv_more_ops     *more_ops;
775 };

复制代码

The struct ibv_device * is a pointer to the device opened for this connection. The struct ibv_context_ops ops field contains function pointers to driver specific functions, which the user need not access directly.
结构ibv_device *是一个指向为建立连接而打开的设备的指针。结构ibv_context_ops ops域包含了一系列函数指针,这些函数指针执行驱动程序的具体功能,用户不需要直接访问这些函数指针。

8.2 Protection Domain | 保护域

After the device is opened and the context is created, the program allocates a protection domain.
在设备被打开和上下文创建完毕之后,程序分配一个保护域(PD)。

复制代码

/* Listing 6: Opening a Protection Domain */344         ctx->pd = ibv_alloc_pd(ctx->context);
345         if (!ctx->pd) {
346                 fprintf(stderr, "Couldn't allocate PD\n");
347                 return NULL;
348         }

复制代码

A protection domain, according to the InfiniBand specification, allows the client to control which remote computers can access its memory regions during InfiniBand sends and receives.
根据IB的技术规范,保护域(PD)允许cient在IB发送和接收过程中,控制它的内存区域,该内存取可以被远程电脑访问。

The protection domain mostly exists on the hardware itself. Its user-space data structure is sparse:
保护域(PD)主要存在于硬件本身。其用户空间数据结构实为稀疏:

复制代码

/* Listing 7: struct ibv_pd from "infiniband/verbs.h" */308 struct ibv_pd {
309         struct ibv_context     *context;
310         uint32_t                handle;
311 };

复制代码

8.3 Memory Region | 内存区域

The ibv_rc_pingpong program next registers one memory region with the hardware.
接下来ibv_rc_pingpong程序注册一段内存区域(MR),该区域将被硬件访问。

When the memory region is registered, two things happen. The memory is pinned by the kernel, which prevents the physical address from being swapped to disk. On Linux operating systems, a call to mlock is used to perform this operation. In addition, a translation of the virtual address to the physical address is given to the HCA.
当内存区域(MR)注册了,有两件事情将发生。(首先,) 内存被内核锁定,防止物理地址(内存里存放的数据)被交换到硬盘上。在Linux操作系统中,使用mlock调用来执行这一操作。其次,(HCA驱动)将虚拟内存地址转换为物理内存地址,然后将这一对应关系交给HCA硬件去使用。

复制代码

/* Listing 8: Registering a Memory Region */
350         ctx->mr = ibv_reg_mr(ctx->pd, ctx->buf, size, IBV_ACCESS_LOCAL_WRITE);
351         if (!ctx->mr) {
352                 fprintf(stderr, "Couldn't register MR\n");
353                 return NULL;
354         }

复制代码

The arguments are the protection domain with which to associate the memory region, the address of the region itself, the size, and the flags. The options for the flags are defined in "infiniband/verbs.h".
参数是与MR相关联的PD, MR本身的地址,大小和标志。标志选项的定义在文件"infiniband/verbs.h"里。

复制代码

/* Listing 9: Access Flags */300 enum ibv_access_flags {
301         IBV_ACCESS_LOCAL_WRITE          = 1,
302         IBV_ACCESS_REMOTE_WRITE         = (1<<1),
303         IBV_ACCESS_REMOTE_READ          = (1<<2),
304         IBV_ACCESS_REMOTE_ATOMIC        = (1<<3),
305         IBV_ACCESS_MW_BIND              = (1<<4)
306 };

复制代码

When the memory registration is complete, an lkey field or Local Key is created. According to the InfiniBand Technical Specification the lkey is used to identify the appropriate memory addresses and provide authorization to access them.
当内存注册(MR)完成的时候,lkey(或Local Key)字段就创建好了。根据IB的技术规范,lkey用来确定合适的内存地址和提供访问授权。

8.4 Completion Queue | 完成队列

The next part of the connection is the completion queue (CQ), where work completion queue entries are posted. Please note that you must create the CQ before the QP. As stated previously, ibv_ack_cq_events(3) has helpful examples of how to manage completion events.
建立连接的接下来的一部分是创建完成队列(CQ),工作完成条目(CQE)被放置到完成队列(CQ)上。请注意,必须在创建QP之前创建CQ。如何管理完成事件,前面提及的ibv_ack_cq_events(3)中例子可供参考。

复制代码

/* Listing 10: Creating a CQ */356         ctx->cq = ibv_create_cq(ctx->context, rx_depth + 1, NULL,
357                                 ctx->channel, 0);
358         if (!ctx->cq) {
359                 fprintf(stderr, "Couldn't create CQ\n");
360                 return NULL;
361         }

复制代码

8.5 Queue Pairs | 队列对

Communication in InfiniBand is based on the concept of queue pairs. Each queue pair contains a send queue and a receive queue, and must be associated with at least one completion queue. The queues themselves exist on the HCA. A data structure containing a reference to the hardware queue pair resources is returned to the user.
在IB中,通信是基于队列对(QP)的概念而实现。每一个队列对(QP)包含有一个发送队列(SQ)和接收队列(RQ),并且必须与至少一个完成队列(CQ)相关联(换言之,一个QP中的SQ和RQ可以关联到同一个CQ上)。这些队列(SQ, RQ和CQ)都存在于HCA硬件上。一个包含有对硬件QP资源的引用的数据结构,被返回给用户使用。

First, look at the code to create a QP.
首先,让我们看看创建一个QP的源代码。

复制代码

/* Listing 11: Creating a QP */364                 struct ibv_qp_init_attr attr = {
365                         .send_cq = ctx->cq,
366                         .recv_cq = ctx->cq,
367                         .cap     = {
368                                 .max_send_wr  = 1,
369                                 .max_recv_wr  = rx_depth,
370                                 .max_send_sge = 1,
371                                 .max_recv_sge = 1
372                         },
373                         .qp_type = IBV_QPT_RC
374                 };
375
376                 ctx->qp = ibv_create_qp(ctx->pd, &attr);
377                 if (!ctx->qp)  {
378                         fprintf(stderr, "Couldn't create QP\n");
379                         return NULL;
380                 }

复制代码

Notice that a data structure which defines the initial attributes of the QP must be given as an argument. There are a few other elements in the data structure, which are optional to define.
注意,定义QP初始属性的数据结构必须作为一个参数传递。在这个数据结构中,有一些元素是可选的。

The first two elements, send_cq and recv_cq, associate the QP with a CQ as stated earlier. The send and receive queue may be associated with the same completion queue.
前两个元素是send_cq和recv_cq,(如前面所说)是与QP相关联的完成队列(CQ)。发送队列和接收队列可能关联到同一个完成队列上。

The cap field points to a struct ibv_qp_cap and specifies how many send and receive work requests the queues can hold. The max_{send, recv}_sge field specifies the maximum number of scatter/gather elements that each work request will be able to hold. A scatter gather element is used in a direct memory access (DMA) operation, and each SGE points to a buffer in memory to be used in the read or write. In this case, the attributes state that only one buffer may be pointed to at any given time.
字段cap指向一个结构体struct ibv_qp_cap, 指定可以容纳的发送和接收工作请求的个数。max_{send, recv}_sge字段指定了每一个工作请求能够容纳的最大的SGE数目。一个SGE用来做DMA操作,每一个SGE指向一个可用于读/写的内存缓冲区。在这个例子中,属性状态说明了在任何给定的时间内仅指向一个缓冲区。

The qp_type field specifies what type of connection is to be used, in this case a reliable connection.
qp_type字段指定了使用的连接类型,在这里是可靠连接(RC)。

Now the queue pair has been created. It must be moved into the initialized state, which involves a library call. In the initialized state, the QP will silently drop any incoming packets and no work requests can be posted to the send queue.
现在QP已经创建好了。必须通过库函数调用将它的状态设置为初始化状态。在初始化状态下,任何传入的数据包将被QP悄悄地丢弃,并且工作请求不能够被放置到发送队列上。

复制代码

/* Listing 12: Setting QP to INIT */384                 struct ibv_qp_attr attr = {
385                         .qp_state        = IBV_QPS_INIT,
386                         .pkey_index      = 0,
387                         .port_num        = port,
388                         .qp_access_flags = 0
389                 };
390
391                 if (ibv_modify_qp(ctx->qp, &attr,
392                                   IBV_QP_STATE              |
393                                   IBV_QP_PKEY_INDEX         |
394                                   IBV_QP_PORT               |
395                                   IBV_QP_ACCESS_FLAGS)) {
396                         fprintf(stderr, "Failed to modify QP to INIT\n");
397                         return NULL;
398                 }

复制代码

The third argument to ibv_modify_qp is a bitmask stating which options should be configured. The flags are specified in enum ibv_qp_attr_mask in "infiniband/verbs.h".
ibv_modify_qp()的第3个参数是一个位掩码,说明应该配置的选项。flags在头文件"infiniband/verbs.h"的枚举体ibv_qp_attr_mask中定义。

At this point the ibv_rc_pingpong program posts a receive work request to the QP.
在这里,ibv_rc_pingpong程序放置一个接收工作请求到QP上。

650         routs = pp_post_recv(ctx, ctx->rx_depth);

Look at the definition of pp_post_recv.
看一下pp_post_recv的定义。

复制代码

/* Listing 13: Posting Recv Requests */444 static int pp_post_recv(struct pingpong_context *ctx, int n)
445 {
446         struct ibv_sge list = {
447                 .addr   = (uintptr_t) ctx->buf,
448                 .length = ctx->size,
449                 .lkey   = ctx->mr->lkey
450         };
451         struct ibv_recv_wr wr = {
452                 .wr_id      = PINGPONG_RECV_WRID,
453                 .sg_list    = &list,
454                 .num_sge    = 1,
455         };
456         struct ibv_recv_wr *bad_wr;
457         int i;
458
459         for (i = 0; i < n; ++i)
460                 if (ibv_post_recv(ctx->qp, &wr, &bad_wr))
461                         break;
462
463         return i;
464 }

复制代码

The ibv_sge list is the list pointing to the scatter/gather elements (in this case, a list of size 1). To review, the SGE is a pointer to a memory region which the HCA can read to or write from.
ibv_sge列表是指向SGE数组的列表(在这里,列表长度为1)。SGE指向一个内存区域,该区域能被HCA读写。

Next is the ibv_recv_wr structure. The first field, wr_id, is a field set by the program to identify the work request. This is needed when checking the completion queue elements; it specifies which work request completed.
下一个结构体是ibv_recv_wr。第一个字段wr_id由应用程序设置,以标识对应的WR。在检查完成队列元素时需要wr_id,它指定了哪一个WR已经完成了。

The work request given to ibv_post_recv is actually a linked list, of length 1.
传给ibv_post_recv()的WR实际上是一个长度为1的链表。

复制代码

/* Listing 14: Linked List */451         struct ibv_recv_wr wr = {
452                 .wr_id      = PINGPONG_RECV_WRID,
453                 .sg_list    = &list,
454                 .num_sge    = 1,
455         };

复制代码

If one of the work requests fails, the library will set the bad_wr pointer to the failed wr in the linked list.
如果一个WR执行失败了,那么库函数就将bad_wr指向在此链表中失败的那个wr

Receive buffers must be posted before any sends. It is common practice to loop over the ibv_post_recv call to post numerous buffers at the beginning of execution. Eventually these buffers will be used up; internal flow control must be implemented by the applications to ensure that sends are not posted without corresponding receives.
接收缓冲区必须在发送之前放置到SQ上。 通常的做法是在执行开始的时候循环调用ibv_post_recv()将多个buffer放置到接收队列上。最终这些buffer将被全部消耗掉。应用程序必须实现内部的流量控制,以确保在远端没有准备好接收的情况下不放置任何发送请求到SQ上。

8.6 Connecting | 连接

The next step occurs in pp_client_exch_dest and pp_server_exch_dest. The QPs need to be configured to point to a matching QP on a remote node. However, the QPs currently have no means of locating each other. The processes open an out-of-band TCP socket and transmit the needed information. That information, once manually communicated, is given to the driver and then each side's QP is configured to point at the other. (The OFED librdma_cm library is an alternative to explicit out-of-band TCP.)
下一步发生在pp_client_exch_dest和pp_server_exch_dest中。QP需要配置一下,指向远端结点的QP。然而,目前QP没有定位对方的方法。进程打开带外TCP socket并传输所需要的信息。这些信息传递给驱动,然后每一方的QP就被配置为指向另一方的QP。(使用OFED的librdma_cm库,可以用来替代显式的TCP带外数据。)

So what information needs to be exchanged/configured? Mainly the LID, QPN, and PSN. The LID is the "Local Identifier" and it is a unique number given to each port when it becomes active. The QPN is the Queue Pair Number, and it is the identifier assigned to each queue on the HCA. This is used to specify to what queue messages should be sent. Finally, the destinations must share their PSNs.
那么,哪些信息需要交换和配置? LID, QPN和PSN。 LID是本地ID的缩写,当一个port变成活跃状态的时候,port就被分配了一个独一无二的数字。QPN是QP Number的缩写,是在HCA上分配给每一个队列的标识符。QPN用来指定消息发送到哪个队列上去。最后,目标必须共享它们的PSN。

The PSN stands for Packet Sequence Number. In a reliable connection it is used by the HCA to verify that packets are coming in order and that packets are not missing. The initial PSN, for the first packet, must be specified by the user code. If it is too similar to a recently used PSN, the hardware will assume that the incoming packets are stale packets from an old connection and reject them.
PSN代表的是包序列号。在可靠连接中,HCA用PSN来保证一个个数据包是有序到达的而且没有丢包。第一个数据包最初的PSN,必须由用户代码指定。如果PSN与最近使用的一个PSN很相似的话,硬件就假定传入的数据包是一个陈腐的包,来自一个旧连接,然后予以拒绝。

The GID, seen in the code sample below, is a 128-bit unicast or multicast identifier used to identify an endport. The link layer specifies which interconnect the software is running on; there are other interconnects that OFED supports, though that is not within the scope of this paper.
在下面的代码示例中,GID是一个128位的单播或者多播的标识符,用来标识一个终端端口。链路层指定了软件在哪种互联协议上运行。OFED还支持除IB之外的其他互联协议,那些协议不在本论文的讨论范围之内。

Within pp_connect_ctx the information, once transmitted, is used to connect the QPs into an end-to-end context.
在pp_connect_ctx之中的信息,一旦传输完成,就用于连接QP对到一个端到端的上下文中。

复制代码

/* Listing 15: Setting Up Destination Information */665         my_dest.lid = ctx->portinfo.lid;
666         if (ctx->portinfo.link_layer == IBV_LINK_LAYER_INFINIBAND && !my_dest.lid) {
667                 fprintf(stderr, "Couldn't get local LID\n");
668                 return 1;
669         }
670
671         if (gidx >= 0) {
672                 if (ibv_query_gid(ctx->context, ib_port, gidx, &my_dest.gid)) {
673                         fprintf(stderr, "Could not get local gid for gid index %d\n", gidx);
674                         return 1;
675                 }
676         } else
677                 memset(&my_dest.gid, 0, sizeof my_dest.gid);
678
679         my_dest.qpn = ctx->qp->qp_num;
680         my_dest.psn = lrand48() & 0xffffff;

复制代码

The my_dest data structure is filled and then transmitted via TCP. Figure 4 illustrates this data transfer.
数据结构my_dest被填充,然后通过TCP传送。图4显示了这一数据传送。

8.6.1 Modifying QPs | 修改QP

Look at the attributes given to the ibv_modify_qp call.
让我们看一看传递给ibv_modify_qp()调用的属性。

复制代码

/* Listing 16: Moving QP to Ready to Recv */84         struct ibv_qp_attr attr = {85                 .qp_state               = IBV_QPS_RTR,86                 .path_mtu               = mtu,87                 .dest_qp_num            = dest->qpn,88                 .rq_psn                 = dest->psn,89                 .max_dest_rd_atomic     = 1,90                 .min_rnr_timer          = 12,91                 .ah_attr                = {92                         .is_global      = 0,93                         .dlid           = dest->lid,94                         .sl             = sl,95                         .src_path_bits  = 0,96                         .port_num       = port97                 }98         };
...
106         if (ibv_modify_qp(ctx->qp, &attr,
107                           IBV_QP_STATE              |
108                           IBV_QP_AV                 |
109                           IBV_QP_PATH_MTU           |
110                           IBV_QP_DEST_QPN           |
111                           IBV_QP_RQ_PSN             |
112                           IBV_QP_MAX_DEST_RD_ATOMIC |
113                           IBV_QP_MIN_RNR_TIMER)) {
114                 fprintf(stderr, "Failed to modify QP to RTR\n");
115                 return 1;
116         }

复制代码

As you can see .qp_state is set to IBV_QPS_RTR, or Ready-To-Receive. The three fields swapped over TCP, the PSN, QPN, and LID, are now given to the hardware. With this information, the QPs are registered with each other by the hardware, but are not ready to begin exchanging messages. The min_rnr_timer is the time, in seconds, between retries before a timeout occurs.
正如你所看到的,.qp_state被设置为IBV_QPS_RTR或Ready-To-Receive(接收就绪)。通过TCP交换到的三个字段(PSN, QPN和LID),现在传给硬件。有了这些信息,彼此的QP被注册到对方的硬件上,但还没有为开始消息交换准备就绪。min_rnr_timer是重试的时间间隔(以秒为单位),在发生超时的时候。

The QP must be moved into the Ready-To-Send state before the "connection" process is complete.
在连接过程完成之前,QP状态必须被改变到Ready-To-Send(发送就绪)状态。

复制代码

/* Listing 17: Moving QP to Ready to Send */118         attr.qp_state       = IBV_QPS_RTS;
119         attr.timeout        = 14;
120         attr.retry_cnt      = 7;
121         attr.rnr_retry      = 7;
122         attr.sq_psn         = my_psn;
123         attr.max_rd_atomic  = 1;
124         if (ibv_modify_qp(ctx->qp, &attr,
125                           IBV_QP_STATE              |
126                           IBV_QP_TIMEOUT            |
127                           IBV_QP_RETRY_CNT          |
128                           IBV_QP_RNR_RETRY          |
129                           IBV_QP_SQ_PSN             |
130                           IBV_QP_MAX_QP_RD_ATOMIC)) {
131                 fprintf(stderr, "Failed to modify QP to RTS\n");
132                 return 1;
133         }

复制代码

The attr used to move the QP into IBV_QPS_RTS is the same attr used in the previous call. There is no need to zero out the structure because the bitmask, given as the third argument, specifies which fields should be set.
用来将QP状态改变到IBV_QPS_RTS的attr跟前面调用使用的attr是一模一样的。没有必要将数据结构初始化为0, 因为第三个参数bitmask指定了哪些字段需要被设置。

After the QP is moved into the Ready-To-Send state, the connection (end-to-end context) is ready.
当QP状态处于Ready-to-Send的时候,端到端的连接就准备好了。

8.7 Sending Data | 数据发送

Since the server already posted receive buffers, the client will now post a "send" work request.
既然服务器端已经放置了接收缓冲区,那么客户端即将放置一个“发送”工作请求。

复制代码

/* Listing 18: Client Posting Send */468         struct ibv_sge list = {
469                 .addr   = (uintptr_t) ctx->buf,
470                 .length = ctx->size,
471                 .lkey   = ctx->mr->lkey
472         };
473         struct ibv_send_wr wr = {
474                 .wr_id      = PINGPONG_SEND_WRID,
475                 .sg_list    = &list,
476                 .num_sge    = 1,
477                 .opcode     = IBV_WR_SEND,
478                 .send_flags = IBV_SEND_SIGNALED,
479         };
480         struct ibv_send_wr *bad_wr;
481
482         return ibv_post_send(ctx->qp, &wr, &bad_wr);

复制代码

The wr_id is an ID specified by the programmer to identify the completion notification corresponding with this work request. In addition, the flag IBV_SEND_SIGNALED sets the completion notification indicator. According to ibv_post_send(3), it is only relevant if the QP is created with sq_sig_all = 0.
wr_id是由程序员指定的ID,用来识别与这个WR相对应的完成通知。另外,标志IBV_SEND_SIGNALED设置了完成通知指示灯。根据ibv_post_send(3),只有当QP创建的时候设置了sq_sig_all等于0时才相关。

8.8 Flow Control | 流量控制

Programmers must implement their own flow control when working with the verbs API. Let us examine the flow control used in ibv_rc_pingpong. Remember from earlier that a client cannot post a send if its remote node does not have a buffer waiting to receive the data.
当使用verbs API的时候,程序员必须自己实现流量控制。让我们看看ibv_rc_pingpong里的流量控制。记住我们在前面强调的,如果远端结点没有准备好等待接收数据的缓冲区,client就不能够发送数据。

Flow control must be used to ensure that receivers do not exhaust their supply of posted receives. Furthermore, the CQ must not overflow. If the client does not pull CQEs off the queue fast enough, the CQ is thrown into an error state, and can no longer be used.
使用流量控制是必须的,以确保接收端不会耗尽它所提供的接收资源。此外,CQ必须不能够溢出。如果client不能足够快地把CQE从CQ上拉取下来的话,那么CQ就会陷入错误状态以致于不能再被使用。

You can see at the top of the loop, which will send/recv the data, that ibv_rc_pingpong tracks the send and recv count.
可以看到,发送/接收数据位于循环的顶部,ibv_rc_pingpong跟踪了发送和接收的计数器。

/* Listing 19: Flow Control */717         rcnt = scnt = 0;
718         while (rcnt < iters || scnt < iters) {

Now the code will poll the CQ for two completions; a send completion and a receive completion.
现在,代码将为发送完成(SC)和接收完成(RC)轮询CQ。

复制代码

/* Listing 20: Polling the CQ */745                         do {
746                                 ne = ibv_poll_cq(ctx->cq, 2, wc);
747                                 if (ne < 0) {
748                                         fprintf(stderr, "poll CQ failed %d\n", ne);
749                                         return 1;
750                                 }
751
752                         } while (!use_event && ne < 1);

复制代码

The use_event variable specifies whether or not the program should sleep on CQ events. By default, ibv_rc_pingpong will poll. Hence the while-loop. On success, ibv_poll_cq returns the number of completions found.
变量use_event指定是否在CQ时间上进行sleep。默认地,ibv_rc_pingpong进行轮询。因此使用了while循环。轮询成功后,ibv_poll_cq()返回工作完成(WC)的数量。

Next, the program must account for how many sends and receives have been posted.
接下来,程序必须统计放置的发送请求和接受工作请求的个数。

复制代码

/* Listing 21: Flow Control Accounting */762                                 switch ((int) wc[i].wr_id) {
763                                 case PINGPONG_SEND_WRID:
764                                         ++scnt;
765                                         break;
766
767                                 case PINGPONG_RECV_WRID:
768                                         if (--routs <= 1) {
769                                                 routs += pp_post_recv(ctx, ctx->rx_depth - routs);
770                                                 if (routs < ctx->rx_depth) {
771                                                         fprintf(stderr,
772                                                                 "Couldn't post receive (%d)\n",
773                                                                 routs);
774                                                         return 1;
775                                                 }
776                                         }
777
778                                         ++rcnt;
779                                         break;
780
781                                 default:
782                                         fprintf(stderr, "Completion for unknown wr_id %d\n",
783                                                 (int) wc[i].wr_id);
784                                         return 1;
785                                 }

复制代码

The ID given to the work request is also given to its associated work completion, so that the client knows what WQE the CQE is associated with. In this case, if it finds a completion for a send event, it increments the send counter and moves on.
给WR使用的ID也被给到相关联的WC上,于是client就知道CQE是关联到那一个WQE上。在这个案例中,如果为一个发送事件找到了一个工作完成,就增加发送计数器然后继续。

The case for PINGPONG_RECV_WRID is more interesting, because it must make sure that receive buffers are always available. In this case the routs variable indicates how many recv buffers are available. So if only one buffer remains available, ibv_rc_pingpong will post more recv buffers. In this case, it calls pp_post_recv again, which will post another 500 (by default). After that it increments the recv counter.
PINGPONG_RECV_WRID的案例更有趣,因为它必须确保接收缓冲区总是可用的。在这种情况下,变量routs表示有多少接收缓冲区可用。所以,如果只有一个缓冲仍然可用,ibv_rc_pingpong将会放置更多的接收缓冲区。在这种情况下,它要求pp_post_recv再放置500(默认值)个接收缓冲区,然后增加recv计数器。

Finally, if more sends need to be posted, the program will post another send before continuing the loop.
最后,如果需要放置更多的发送请求,程序将在继续循环之前放置另一个发送请求。

复制代码

/* Listing 22: Posting Another Send */787                                 ctx->pending &= ~(int) wc[i].wr_id;
788                                 if (scnt < iters && !ctx->pending) {
789                                         if (pp_post_send(ctx)) {
790                                                 fprintf(stderr, "Couldn't post send\n");
791                                                 return 1;
792                                         }
793                                         ctx->pending = PINGPONG_RECV_WRID |
794                                                        PINGPONG_SEND_WRID;
795                                 }

复制代码

8.9 ACK

The ibv_rc_pingpong program will now ack the completion events with a call to ibv_ack_cq_events. To avoid races, the CQ destroy operation will wait for all completion events returned by ibv_get_cq_event to be acknowledged. The call to ibv_ack_cq_events must take a mutex internally, so it is best to ack multiple events at once.
程序ibv_rc_pingpong将调用ibv_ack_cq_events()对完成的事件进行确认。为了避免竞态,CQ的destroy操作将等待所有完成事件并确认。调用ibv_ack_cq_events()必须持有内部的互斥锁,所以最好是一次确认多个事件。

816         ibv_ack_cq_events(ctx->cq, num_cq_events);

As a reminder, ibv_ack_cq_events(3) has helpful sample code.
友情提醒一下,ibv_ack_cq_events(3)有提供帮助性的示例代码。

9 Conclusion | 结束语

InfiniBand is the growing standard for supercomputer interconnects, even appearing in departmental clusters. The API is complicated and sparsely documented, and the sample program provided by OFED, ibv_rc_pingpong, does not fully explain the functionality of the verbs. This paper will hopefully enable the reader to better understand the verbs interface.
在超级计算机互联标准中,IB正在蓬勃生长,已经出现在集群中。由于verbs的API较为复杂,而且文档记录比较稀疏。OFED提供的示例程序ibv_rc_pingpong并不能完全解释verbs的功能。本文希望读者在阅读之后,能够更好地理解verbs的用户接口。

10 Acknowledgements | 致谢

Gene Cooperman (Northeastern University) and Jeff Squyres (Cisco) contributed substantially to the organization, structure, and content of this document. Jeff also took the time to discuss the details of InfiniBand with me. Josh Hursey (Oak Ridge National Laboratory) shared his knowledge of InfiniBand with me along the way. Roland Dreier (PureStorage) pointed out, and corrected, a mistake in my explanation of acks.
东北大学的Gene Cooperman和思科的Jeff Squyres对此文档的结构,内容做出了重大贡献。Jeff还花了不少时间和我讨论IB的细节。橡树岭国家实验室的John Hursey跟我分享了有关IB的知识。Pure存储的Roland Dreier指出和纠正了我在解释ACK时存在的错误。

References | 参考文献

  • [1] Jason Ansel, Gene Cooperman, and Kapil Arya. DMTCP: Scalable user-level transparent checkpointing for cluster computations and the desktop. In Proc. of IEEE International Parallel and Distributed Processing Symposium (IPDPS-09, systems track). IEEE Press, 2009. published on CD; software available at http://dmtcp.sourceforge.net.
  • [2] InfiniBand Trade Assocation. InfiniBand Architecture Specification Volume 1, Release 1.2.1, November 2007. http://www.infinibandta.org/content/pages.php? pg=technology_download.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/296240.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HbnnMall电子商城系统介绍(功能与技术栈)

今天在看我个人网站上的文章时&#xff0c;看到了曾经在2020年自己开发的电商系统。那时我已经入职小米有一段时间了&#xff0c;基本已经对各个业务线&#xff0c;各种业务知识有了系统性的了解和学习&#xff0c;所以想自己动手写一个电商系统&#xff0c;以便进一步提高自己…

ElasticSearch7.8的下载与安装和Kibana 7.8.0工具使用安装

1、ElasticSearch7.8.0下载 elasticsearch: 官方下载地址&#xff1a;https://www.elastic.co/cn/downloads/elasticsearch 链接: https://pan.baidu.com/s/1wAKQoB3nhLhcnBlPfVOLxQ 提取码: t83n kibana: 链接: https://pan.baidu.com/s/156aD9zDdvUv8LFgDEIPoSw 提取码:…

云存储中常用的相同子策略的高效、安全的基于属性的访问控制的论文阅读

参考文献为2022年发表的Efficient and Secure Attribute-Based Access Control With Identical Sub-Policies Frequently Used in Cloud Storage 动机 ABE是实现在云存储中一种很好的访问控制手段&#xff0c;但是其本身的计算开销导致在实际场景中应用收到限制。本论文研究了…

(2024)Ubuntu源码安装多个版本的opencv并切换使用

本人工作会用到x86_64的opencv和aarch64的opencv&#xff0c;所以写下来备忘自用 一、源码编译安装 依赖库安装&#xff1a; sudo apt-get install build-essential libgtk2.0-dev libgtk-3-dev libavcodec-dev libavformat-dev libjpeg-dev libswscale-dev libtiff5-dev o…

上位机图像处理和嵌入式模块部署(qmacvisual图像清晰度)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 做过isp的同学都知道&#xff0c;图像处理里面有一个3A&#xff0c;即自动曝光、自动白平衡和自动对焦。其中自动对焦这个&#xff0c;就需要用输入…

HarmonyOS NEXT应用开发之PersistentStorage:持久化存储UI状态

前两个小节介绍的LocalStorage和AppStorage都是运行时的内存&#xff0c;但是在应用退出再次启动后&#xff0c;依然能保存选定的结果&#xff0c;是应用开发中十分常见的现象&#xff0c;这就需要用到PersistentStorage。 PersistentStorage是应用程序中的可选单例对象。此对…

Vue的学习之旅-part1

Vue的学习之旅-part1 vue介绍vue读音编程范式ES6中不用var声明变量vue的声明、初始化传参使用data中数据时要用this指向 vue中的语法糖MVVM在Vue中&#xff0c; MVVM的各层的对应位置 方法、函数的不同之处 vue介绍 vue读音 Vue 读作 /vju:/ 不要读成v u e Vuex 的x读作叉 不…

Redis高可用技术

一.Redis高可用介绍&#xff1a; 高可用是指&#xff1a;服务器正常访问的时间 衡量的标准是&#xff1a;在多长时间内可以提供正常服务99.9%、99.99%、99.999%等等 但是在Redis语境中&#xff0c; 高可用的含义似乎要宽泛一些&#xff0c;除了保证提供正常服务(如主从分离、…

IntelliJ IDEA中文---强化智能编码与重构,提升开发效率

IntelliJ IDEA 2023是一款功能强大的集成开发环境&#xff08;IDE&#xff09;&#xff0c;专为Java开发人员设计。它支持智能代码编辑、自动补全和重构&#xff0c;帮助开发者提高编码效率。同时&#xff0c;内置了丰富的调试工具&#xff0c;支持断点调试和变量监视&#xff…

C语言--文件操作

1.标准流 • stdin - 标准输⼊流&#xff0c;在⼤多数的环境中从键盘输⼊&#xff0c;scanf函数就是从标准输⼊流中读取数据。 • stdout - 标准输出流&#xff0c;⼤多数的环境中输出⾄显⽰器界⾯&#xff0c;printf函数就是将信息输出到标准输出 流中。 • stderr - 标…

丰诺畅机电科技将莅临2024年第13届生物发酵展

参展企业介绍 无锡丰诺畅机电科技有限公司&#xff0c;是一家分离设备专业制造公司&#xff0c;集开发、设计、制造、销售、服务于一体;具有专业的生产技术&#xff0c;先进的生产工艺&#xff0c;精良的制造设备&#xff0c;完善的检测手段;为满足不同用户的过滤需求&#xf…

HTTP/UDP/TCP/IP网络协议

文章目录 计算机网络基础HTTPUDPTCP连接管理(三次握手/四次挥手)TCP可靠传输(确认答应)超时重传滑动窗口流量控制拥塞控制延时应答捎带应答粘包问题其他 IP数据链路层MUT 相关问题TCP会粘包、UDP永远不会粘包 学习博客 计算机网络基础 OSI模型定义了网络互连的七层框架&#x…

esp32控制舵机---待完善

舵机有三个引脚&#xff0c;分别是电源、电源GND和信号线。如下图所示&#xff1a; ESP32-WROOM-32E的引脚的定义如下&#xff1a; 图来自乐鑫官网:ESP32-DevKitC V4 入门指南 - ESP32 - — ESP-IDF 编程指南 v5.2.1 文档 硬件连接图&#xff1a; 待补充

013——超声波模块驱动开发(基于I.MX6uLL与SR04)

目录 一、 模块介绍 1.1 产品特色 1.2 产品实物图 1.3 接口定义 1.4 测距调节 1.5 模块工作原理 1.6 注意 二、 编码思路 三、 驱动程序 四、 应用程序 五、 Makefile 六、 其它及实验 一、 模块介绍 超声波测距模块是利用超声波来测距。模块先发送超声波&#xf…

边缘计算盒子与云计算:谁更适合您的业务需求?

边缘计算盒子和云计算&#xff0c;这两个概念听起来可能有点复杂&#xff0c;但其实它们就是两种不同的数据处理方式。那谁更适合您的业务需求呢&#xff1f;咱们来详细说说。 边缘计算盒子&#xff0c;就像是个小型的数据处理中心&#xff0c;放在离你业务现场比较近的地方。它…

WPS二次开发专题:如何获取应用签名SHA256值

作者持续关注WPS二次开发专题系列&#xff0c;持续为大家带来更多有价值的WPS开发技术细节&#xff0c;如果能够帮助到您&#xff0c;请帮忙来个一键三连&#xff0c;更多问题请联系我&#xff08;QQ:250325397&#xff09; 在申请WPS SDK授权版时候需要开发者提供应用包名和签…

美创科技获浙江省网络空间安全协会多项荣誉认可

4月2日&#xff0c;浙江省网络空间安全协会第二届会员大会第一次会议在杭州隆重召开&#xff0c;近180家会员单位代表、数十位特邀专家、嘉宾莅临现场。浙江省委网信办副主任马晓军出席会议并致辞&#xff0c;本次大会由协会秘书长吴铤主持。 凝心聚力&#xff0c;继往开来&…

SAP:无法在插件模式 HTTP 中处理消息 E ** xxx

问题描述&#xff1a;利用post方式接口&#xff0c;返回信息为 无法在插件模式 HTTP 中处理消息 E ** xxx ,如何排查是因为什么问题导致的&#xff1f; 解决方法&#xff1a; 事务码&#xff1a;SE91, 输入消息类&#xff0c;消息编号&#xff0c;点击显示&#xff0c;查看报…

【NLP笔记】LLM应用之AI Agent LangChain实战

文章目录 AI Agent概述LangChain实战构建prompt模版LLM调用调用HuggingFace开源大模型&#xff08;在线&#xff09;调用HuggingFace开源大模型&#xff08;本地&#xff09;调用文心一言 ChainsSingle ChainSequential ChainSimple Sequential ChainComplex Sequential Chain …

【重学C语言】三、C语言最简单的程序

【重学C语言】三、C语言最简单的程序 最简单的程序头文件使用尖括号 < >使用双引号 ""区别与注意事项示例 主函数认识三个错误 常量和变量常量ASCII 码表转义字符 关键字数据类型关键字存储类关键字修饰符关键字控制流程关键字函数相关关键字其他关键字 变量变…