关于RDMA传输的基本流量控制

Basic flow control for RDMA transfers | The Geek in the Corner (wordpress.com)

文心一言

已经介绍了使用发送/接收操作和RDMA读写操作,那么现在是一个很好的机会来结合这两种方法的元素,并讨论一般的流量控制。还会稍微谈谈RDMA带有立即数据的写操作(IBV_WR_RDMA_WRITE_WITH_IMM),并且将通过一个示例来说明这些方法,该示例使用RDMA传输命令行中指定的文件。

示例包括一个服务器和一个客户端。服务器等待来自客户端的连接。客户端在连接到服务器后主要执行两个操作:它发送要传输的文件名,然后发送文件的内容。我们不会关注建立连接的细节;这些在之前的帖子中已经讨论过了。相反,我们将专注于同步和流量控制。不过,在本文中的代码结构上做了一些调整,与之前关于RDMA读写的帖子中的结构相反——在那里,将连接管理代码分别放在client.c和server.c中,而将完成处理代码放在common.c中。而在这里,将连接管理代码集中放在common.c中,并将完成处理代码分别放在client.c和server.c中。

回到示例。有许多方法可以从客户端向服务器传输整个文件。例如:

  1. 将整个文件加载到客户端内存中,连接到服务器,等待服务器发布一系列接收操作,然后在客户端端发起一个发送操作(send)以将内容复制到服务器。
  2. 将整个文件加载到客户端内存中,注册内存,将区域详细信息传递给服务器,让服务器发起RDMA读取操作将整个文件复制到其内存中,然后将内容写入磁盘。
  3. 与上述相同,但发起RDMA写入操作以将文件内容复制到服务器内存中,然后通知它写入磁盘。
  4. 在客户端打开文件,读取一个块,等待服务器发布接收操作,然后在客户端端发布一个发送操作,并循环直到整个文件被发送。
  5. 与上述相同,但使用RDMA读取操作。
  6. 与上述相同,但使用RDMA写入操作。

将整个文件加载到内存中对于大文件来说可能不切实际,因此将跳过前三个选项。在剩下的三个选项中,将专注于使用RDMA写入操作,以便可以说明RDMA带有立即数据的写入操作的使用,这是一直想讨论的一个话题。这种操作类似于常规的RDMA写入,但发起者可以将32位值“附加”到写入操作上。与常规RDMA写入不同,RDMA带有立即数据的写入要求在目标的接收队列上发布一个接收操作。当从目标的队列中拉取完成时,该32位值将可用。

12月26日:Roland D. 相当热心地指出,iWARP适配器不支持RDMA带有立即数据的写入。我们可以重写代码以使用RDMA写入(不带立即数据)后跟一个发送操作,但这留作读者的练习。

既然我们已经决定要将文件拆分成块,并将这些块一次一个地写入服务器的内存,我们需要找到一种方法来确保我们不会比服务器能够处理的速度更快地写入块。我们将通过服务器在准备好接收数据时向客户端发送显式消息来实现这一点。另一方面,客户端将使用带有立即数据的写入来向服务器发送信号。这个过程的大致顺序如下:

  1. 服务器开始监听连接。
  2. 客户端发布一个用于流量控制消息的接收操作,并启动到服务器的连接。
  3. 服务器发布一个用于RDMA带有立即数据的写入的接收操作,并接受来自客户端的连接。
  4. 服务器向客户端发送其目标内存区域的详细信息。
  5. 客户端重新发布一个接收操作,然后通过将文件名写入服务器的内存区域来响应。立即数据字段包含文件名的长度。
  6. 服务器打开一个文件描述符,重新发布一个接收操作,然后发送一个消息,指示它已准备好接收数据。
  7. 客户端重新发布一个接收操作,从输入文件中读取一个数据块,然后将该数据块写入服务器的内存区域。立即数据字段包含该数据块的大小(以字节为单位)。
  8. 服务器将数据块写入磁盘,重新发布一个接收操作,然后发送一个消息,指示它已准备好接收数据。
  9. 重复步骤7和8,直到没有数据要发送。
  10. 客户端重新发布一个接收操作,然后向服务器的内存发起一个零字节的写入操作。立即数据字段设置为零。
  11. 服务器发送一个消息,指示已完成操作。
  12. 客户端关闭连接。
  13. 服务器关闭文件描述符。

一个图表可能会有所帮助:

查看这个序列,我们可以看到服务器只向客户端发送小消息,并且只从客户端接收RDMA写入操作。客户端只执行RDMA写入操作,并且只从服务器接收小消息。

让我们从服务器开始看起。建立连接的细节现在隐藏在rc_init()函数之后,该函数设置了各种回调函数,以及rc_server_loop()函数,它运行一个事件循环:

int main(int argc, char **argv)
{
  rc_init(
    on_pre_conn,
    on_connection,
    on_completion,
    on_disconnect);
 
  printf("waiting for connections. interrupt (^C) to exit.\n");
 
  rc_server_loop(DEFAULT_PORT);
 
  return 0;
}

回调函数的名称相当直观:on_pre_conn()在接收到连接请求但尚未接受连接时被调用,on_connection()在建立连接时被调用,on_completion()在从完成队列中拉取条目时被调用,而on_disconnect()在断开连接时被调用。

on_pre_conn()中,我们分配一个结构体来包含各种连接上下文字段(一个缓冲区来包含来自客户端的数据,一个缓冲区用于向客户端发送消息等),并发布一个接收工作请求以接收客户端的RDMA写入操作:

static void post_receive(struct rdma_cm_id *id)
{
  struct ibv_recv_wr wr, *bad_wr = NULL;
 
  memset(&wr, 0, sizeof(wr));
 
  wr.wr_id = (uintptr_t)id;
  wr.sg_list = NULL;
  wr.num_sge = 0;
 
  TEST_NZ(ibv_post_recv(id->qp, &wr, &bad_wr));
}

这里有趣的是我们设置了sg_list = NULLnum_sge = 0。传入的RDMA写请求将指定一个目标内存地址,由于这个工作请求只与传入的RDMA写请求匹配,所以我们不需要使用sg_listnum_sge来指定接收的内存位置。在连接建立后,on_connection()将内存区域的详细信息发送给客户端:

static void on_connection(struct rdma_cm_id *id)
{
  struct conn_context *ctx = (struct conn_context *)id->context;
 
  ctx->msg->id = MSG_MR;
  ctx->msg->data.mr.addr = (uintptr_t)ctx->buffer_mr->addr;
  ctx->msg->data.mr.rkey = ctx->buffer_mr->rkey;
 
  send_message(id);
}

这促使客户端开始发出RDMA写入操作,这会触发on_completion()回调函数:

static void on_completion(struct ibv_wc *wc)
{
  struct rdma_cm_id *id = (struct rdma_cm_id *)(uintptr_t)wc->wr_id;
  struct conn_context *ctx = (struct conn_context *)id->context;
 
  if (wc->opcode == IBV_WC_RECV_RDMA_WITH_IMM) {
    uint32_t size = ntohl(wc->imm_data);
 
    if (size == 0) {
      ctx->msg->id = MSG_DONE;
      send_message(id);
 
      // don't need post_receive() since we're done with this connection
 
    } else if (ctx->file_name[0]) {
      ssize_t ret;
 
      printf("received %i bytes.\n", size);
 
      ret = write(ctx->fd, ctx->buffer, size);
 
      if (ret != size)
        rc_die("write() failed");
 
      post_receive(id);
 
      ctx->msg->id = MSG_READY;
      send_message(id);
 
    } else {
      memcpy(ctx->file_name, ctx->buffer, (size > MAX_FILE_NAME) ? MAX_FILE_NAME : size);
      ctx->file_name[size - 1] = '\0';
 
      printf("opening file %s\n", ctx->file_name);
 
      ctx->fd = open(ctx->file_name, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
 
      if (ctx->fd == -1)
        rc_die("open() failed");
 
      post_receive(id);
 
      ctx->msg->id = MSG_READY;
      send_message(id);
    }
  }
}

我们在第7行检索即时数据字段,并将其从网络字节顺序转换为主机字节顺序。然后我们测试三种可能的情况:

如果size == 0,则表示客户端已完成数据写入(第9-14行)。我们用MSG_DONE来确认这一点。

如果ctx->file_name的第一个字节被设置,则表示我们已经有了文件名并且有一个打开的文件描述符(第15-30行)。我们调用write()将客户端的数据追加到我们打开的文件中,然后用MSG_READY回复,表示我们已准备好接收更多数据。

否则,我们尚未收到文件名(第30-45行)。我们从输入缓冲区中复制它,打开一个文件描述符,然后用MSG_READY回复,表示我们已准备好接收数据。

在断开连接时,在on_disconnect()中,我们关闭打开的文件描述符并整理内存注册等。服务器的操作就是这样!

在客户端,main()函数稍微复杂一些,因为我们需要将服务器主机名和端口传递给rc_client_loop()

int main(int argc, char **argv)
{
  struct client_context ctx;
 
  if (argc != 3) {
    fprintf(stderr, "usage: %s <server-address> <file-name>\n", argv[0]);
    return 1;
  }
 
  ctx.file_name = basename(argv[2]);
  ctx.fd = open(argv[2], O_RDONLY);
 
  if (ctx.fd == -1) {
    fprintf(stderr, "unable to open input file \"%s\"\n", ctx.file_name);
    return 1;
  }
 
  rc_init(
    on_pre_conn,
    NULL, // on connect
    on_completion,
    NULL); // on disconnect
 
  rc_client_loop(argv[1], DEFAULT_PORT, &ctx);
 
  close(ctx.fd);
 
  return 0;
}

我们不为连接或断开连接提供回调函数,因为这些事件对客户端来说不是特别相关。on_pre_conn()回调函数与服务器端的相当类似,除了连接上下文结构是预先分配的之外,我们发布的接收工作请求(在post_receive()中)需要一个内存区域:

static void post_receive(struct rdma_cm_id *id)
{
  struct client_context *ctx = (struct client_context *)id->context;
 
  struct ibv_recv_wr wr, *bad_wr = NULL;
  struct ibv_sge sge;
 
  memset(&wr, 0, sizeof(wr));
 
  wr.wr_id = (uintptr_t)id;
  wr.sg_list = &sge;
  wr.num_sge = 1;
 
  sge.addr = (uintptr_t)ctx->msg;
  sge.length = sizeof(*ctx->msg);
  sge.lkey = ctx->msg_mr->lkey;
 
  TEST_NZ(ibv_post_recv(id->qp, &wr, &bad_wr));
}

我们将sg_list指向一个足够大的缓冲区,以容纳一个message结构体。服务器将使用这个缓冲区来传递流控制消息。每个消息都会触发对on_completion()的调用,这是客户端执行大部分工作的地方:

static void on_completion(struct ibv_wc *wc)
{
  struct rdma_cm_id *id = (struct rdma_cm_id *)(uintptr_t)(wc->wr_id);
  struct client_context *ctx = (struct client_context *)id->context;
 
  if (wc->opcode & IBV_WC_RECV) {
    if (ctx->msg->id == MSG_MR) {
      ctx->peer_addr = ctx->msg->data.mr.addr;
      ctx->peer_rkey = ctx->msg->data.mr.rkey;
 
      printf("received MR, sending file name\n");
      send_file_name(id);
    } else if (ctx->msg->id == MSG_READY) {
      printf("received READY, sending chunk\n");
      send_next_chunk(id);
    } else if (ctx->msg->id == MSG_DONE) {
      printf("received DONE, disconnecting\n");
      rc_disconnect(id);
      return;
    }
 
    post_receive(id);
  }
}

这与上面描述的序列相匹配。send_file_name()send_next_chunk()最终都调用了write_remote()

static void write_remote(struct rdma_cm_id *id, uint32_t len)
{
  struct client_context *ctx = (struct client_context *)id->context;
 
  struct ibv_send_wr wr, *bad_wr = NULL;
  struct ibv_sge sge;
 
  memset(&wr, 0, sizeof(wr));
 
  wr.wr_id = (uintptr_t)id;
  wr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM;
  wr.send_flags = IBV_SEND_SIGNALED;
  wr.imm_data = htonl(len);
  wr.wr.rdma.remote_addr = ctx->peer_addr;
  wr.wr.rdma.rkey = ctx->peer_rkey;
 
  if (len) {
    wr.sg_list = &sge;
    wr.num_sge = 1;
 
    sge.addr = (uintptr_t)ctx->buffer;
    sge.length = len;
    sge.lkey = ctx->buffer_mr->lkey;
  }
 
  TEST_NZ(ibv_post_send(id->qp, &wr, &bad_wr));
}

这个RDMA请求与之前帖子中描述的请求在两个方面有所不同:我们将操作码(opcode)设置为IBV_WR_RDMA_WRITE_WITH_IMM,并将imm_data设置为我们缓冲区的长度。

这并不难理解,对吧?如果一切正常,你应该会看到以下情况:

ib-host-1$ ./server 
waiting for connections. interrupt (^C) to exit.
opening file test-file
received 10485760 bytes.
received 10485760 bytes.
received 5242880 bytes.
finished transferring test-file
^C
 
ib-host-1$ md5sum test-file
5815ed31a65c5da9745764c887f5f777  test-file
ib-host-2$ dd if=/dev/urandom of=test-file bs=1048576 count=25
25+0 records in
25+0 records out
26214400 bytes (26 MB) copied, 3.11979 seconds, 8.4 MB/s
 
ib-host-2$ md5sum test-file
5815ed31a65c5da9745764c887f5f777  test-file
 
ib-host-2$ ./client ib-host-1 test-file
received MR, sending file name
received READY, sending chunk
received READY, sending chunk
received READY, sending chunk
received READY, sending chunk
received DONE, disconnecting

如果你在内存注册期间遇到错误,比如以下错误,你可能需要增加你的锁定内存资源限制:

error: ctx->buffer_mr = ibv_reg_mr(rc_get_pd(), ctx->buffer, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE) failed (returned zero/null).

OpenMPI的FAQ对如何设置锁定内存限制有很好的解释。

示例代码可以在这里找到。

12月21日:更新了帖子以描述锁定内存限制错误,并更新了示例代码以:检查ibv_reg_mr()的错误;使用文件路径的basename()而不是完整路径;在open()调用中添加缺失的模式参数;在Makefile中添加缺失的库引用。感谢Matt。

10月4日:示例代码现在位于https://github.com/tarickb/the-geek-in-the-corner/tree/master/03_file-transfer。

Kimi

这篇文章讨论了使用RDMA(Remote Direct Memory Access,远程直接内存访问)传输大量数据的基本流程控制。作者Matt最近询问了发送大于或等于2GB数据的方法。作者指出,尽管存在2^31的消息大小限制,但有多种有效的方法可以处理这个问题,考虑到内存资源有限,将跳过将整个文件加载到内存中的前三个选项。作者选择了使用RDMA写入操作来说明RDMA写入即时数据(IBV_WR_RDMA_WRITE_WITH_IMM)操作的使用,这是一种与常规RDMA写入类似的操作,但发起方可以附加一个32位的值。

文章提供了一个示例,包括服务器和客户端的代码,服务器等待客户端连接,客户端在连接后发送要传输的文件名,然后发送文件内容。文章重点讨论了同步和流程控制,而不是连接建立的细节。

以下是文章的中文翻译概要:

  1. 服务器和客户端结构:服务器等待连接,客户端在连接后发送文件名和内容。
  2. 流程控制:使用RDMA写入操作和即时数据来控制数据传输速度,确保不会比服务器处理速度快。
  3. 服务器代码:隐藏了连接建立的细节,通过回调函数处理连接请求、建立、完成和断开连接。
  4. 客户端代码:比服务器复杂,需要传递服务器主机名和端口,处理接收到的消息,并执行RDMA写入操作。
  5. 写入远程数据:客户端使用RDMA写入即时数据操作发送文件名和数据块。
  6. 错误处理:如果遇到内存注册错误,可能需要增加锁定内存资源限制。

文章最后提供了示例代码的链接,并在后续更新中修正了一些错误和遗漏。

请注意,这是文章内容的概要翻译,并非逐字翻译。如果需要更详细的翻译或对特定部分有疑问,请告知。

这是一篇关于使用RDMA(Remote Direct Memory Access,远程直接内存访问)进行大文件传输的讨论。评论者Matt最近询问了如何发送较大的数据量,例如大于或等于2GB的文件。他怀疑存在一种有效的方法来处理这个问题,因为消息大小有一个2^31的限制。他指出,他没有大量的内存可用,因为它被用于其他事情。

文章中提到了多种方法来协调从客户端到服务器的整个文件传输。包括将整个文件加载到客户端内存中,然后发送到服务器;或者使用RDMA读取或写入操作。但是,加载整个文件到内存对于大文件来说可能是不切实际的,所以我们将跳过前三个选项。在剩下的三个选项中,作者选择使用RDMA写入,这样可以展示RDMA-write-with-immediate-data操作的使用,这是他一直想讨论的内容。这种操作与传统的RDMA写入类似,但发起方可以“附加”一个32位的值到写入操作中。与常规的RDMA写入不同,带有立即数据的RDMA写入需要目标在接收队列上发布接收操作。当从目标队列中提取完成时,32位的值将可用。

文章还讨论了如何处理流控制,并提供了一个示例,说明如何使用RDMA传输命令行中指定的文件。示例包括服务器和客户端。服务器等待客户端的连接。客户端在连接到服务器后主要做两件事:发送它正在传输的文件的名称,然后发送文件的内容。文章不会涉及建立连接的细节,而是将重点放在同步和流控制上。

服务器端的代码示例包括初始化、接受连接、处理完成和断开连接的回调函数。客户端代码示例包括发送文件名和文件块,以及接收服务器的流控制消息。

文章最后提供了一个示例,展示了如何使用RDMA写入和立即数据来传输文件,并说明了服务器和客户端如何通过发送和接收消息来协调文件传输的过程。

请注意,由于网络原因,我无法访问提供的GitHub链接以验证示例代码。如果你需要查看示例代码,你可以访问提供的GitHub链接。此外,RDMA write with immediate data在iWARP适配器中可能不受支持,文章建议使用RDMA write(不带立即数据)后跟一个send操作,但这留给读者作为练习。

文章最后更新了一些关于锁定内存限制错误的描述,并更新了示例代码,以检查ibv_reg_mr()的错误,使用文件路径的basename()而不是完整路径,添加了open()调用中缺少的mode参数,以及在Makefile中添加了缺失的库引用。

这篇文章讨论了使用RDMA(Remote Direct Memory Access)传输大量数据的基本流程控制。评论者Matt最近询问了关于发送较大数据量(例如大于或等于2GB的文件)的问题。作者指出,尽管存在2^31的消息大小限制,但有多种有效的方法来处理这种数据传输,尤其是在内存资源有限的情况下。

文章中提到了几种可能的数据传输方式,包括将整个文件加载到客户端内存、使用RDMA读写操作等。但考虑到内存限制,作者选择了使用RDMA写操作,并特别提到了带有立即数据的RDMA写操作(IBV_WR_RDMA_WRITE_WITH_IMM),这种操作允许发送方附加一个32位的值到写操作中,并且需要目标方在接收队列中发布接收操作。

文章详细介绍了客户端和服务器的实现方式,包括它们如何同步和控制流程。服务器等待客户端的连接请求,而客户端在连接后发送要传输的文件名,然后发送文件内容。文章重点介绍了如何使用立即数据信号服务器,以及服务器如何显式地向客户端发送消息以表示它已准备好接收数据。

以下是文章的中文翻译摘要:

Matt最近询问了关于发送较大数据量的问题,例如发送大于或等于2GB的文件。作者提供了一些指针或示例,说明了如何发送大量数据。由于存在2^31的消息大小限制,作者提出了一种结合发送/接收操作和RDMA读写操作的方法来处理流程控制,并特别介绍了带有立即数据的RDMA写操作。

文章中的示例包括一个服务器和一个客户端。服务器等待客户端的连接,客户端在连接后主要做两件事:发送它要传输的文件名,然后发送文件内容。文章没有涉及建立连接的细节,而是专注于同步和流程控制。

作者选择了使用RDMA写操作来分解文件并逐个写入服务器内存,并使用立即数据信号来确保不会比服务器处理速度更快地写入数据。服务器通过发送显式消息来告知客户端何时准备好接收数据。

文章还提供了一个流程图来说明文件传输的顺序,包括服务器和客户端如何交互,以及它们如何使用RDMA写操作和立即数据来控制数据传输。

最后,文章提供了服务器和客户端的示例代码,展示了如何实现上述流程。如果在内存注册过程中遇到错误,可能需要增加锁定内存资源的限制。

文章的示例代码可以在GitHub上找到,链接已在文章中更新。

请注意,这是一个非常技术性的文章,涉及到特定的编程概念和网络协议。如果你需要更详细的翻译或对特定部分有疑问,请告知。

这篇文章讨论了使用RDMA(Remote Direct Memory Access,远程直接内存访问)传输大量数据的基本流程控制。评论者Matt最近询问了发送较大数据量(例如大于或等于2GB的文件)的方法。文章作者指出,尽管存在2^31的消息大小限制,但有多种有效的方法可以处理这种数据传输,尤其是考虑到Matt提到可用内存有限。

文章介绍了几种可能的文件传输方式,包括将整个文件加载到客户端内存中,然后发送到服务器;或者使用RDMA读取或写入操作。作者选择了使用RDMA写入操作,因为它可以展示RDMA-write-with-immediate-data(带有立即数据的RDMA写入)操作的使用,这是一种特殊的RDMA写入操作,允许发送方附加一个32位的值。

文章接着详细描述了客户端和服务器的交互流程,包括服务器监听连接、客户端发送文件名和文件内容、同步和流程控制等。服务器和客户端都使用特定的回调函数来处理连接请求、建立连接、完成队列中的条目以及断开连接。

文章还提供了一个示例代码,展示了如何使用RDMA写入操作和立即数据来传输文件。服务器和客户端的代码都进行了简化,以便专注于流程控制和同步机制。服务器代码隐藏了连接建立的细节,并运行了一个事件循环。客户端代码则需要传递服务器主机名和端口到循环中,并在接收到服务器的内存区域细节后开始发送文件名和文件内容。

最后,文章提到如果遇到内存注册错误,可能需要增加锁定内存资源限制,并提供了OpenMPI FAQ的链接来解释如何操作。文章还提到了示例代码的位置,并感谢了Matt和其他贡献者的帮助。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《机器学习特征提取》

书籍&#xff1a;Building Feature Extraction with Machine Learning: Geospatial Applications 作者&#xff1a;Bharath.H. Aithal&#xff0c;Prakash P.S. 出版&#xff1a;CRC Press 书籍下载-《机器学习特征提取》这是一本面向专业人士和研究生的实用指南&#xff0c…

uniapp uni-popup内容被隐藏问题

今天开发新需求的时候发现uni-popup 过一会就被隐藏掉只留下遮罩(css被更改了)&#xff0c;作者进行了如下调试。 1.讲uni-popup放入其他节点内 失败&#xff01; 2.在生成dom后在打开 失败&#xff01; 3.uni-popup将该节点在包裹一层 然后将统计设置样式&#xff0c;v-if v-s…

selenium中, quit 和close的区别

close时 """ close和quit的区别 close关闭当前页 (只是关闭了当前) quit离开整个浏览器 &#xff08;走远了&#xff09; """ from selenium import webdriver import time# 创建浏览器驱动对象 from selenium.webdriver.co…

抢人!抢人!抢人! IT行业某岗位已经开始抢人了!

所谓抢滩鸿蒙&#xff0c;人才先行。鸿蒙系统火力全开后&#xff0c;抢人已成鸿蒙市场的主题词&#xff01; 智联招聘数据显示&#xff0c;春节后首周&#xff0c;鸿蒙相关职位数同比增长163%&#xff0c;是去年同期的2.6倍&#xff0c;2023年9-12月鸿蒙相关职位数同比增速为3…

深入理解C++多线程系列——线程基础

概念 在现代计算机中&#xff0c;多线程编程是一种强大的并发执行计数&#xff0c;允许多个线程在单个程序内部并行执行&#xff0c;提高程序的执行效率和响应速度。线程&#xff0c;作为CPU调度的最小单元&#xff0c;它被用来执行程序中的指令。一个线程是进程中的一个单一顺…

跨境电商测评自养号需要解决哪些问题?

现在做测评工作室这块的&#xff0c;真正有技术的每天单都做不过来&#xff0c;同样也滋生出很多找别人买个设备和账号就以为自己懂了&#xff0c;直接开始教学来割韭菜&#xff0c;很多人没接触过这行业&#xff0c;不知道里面的水很深&#xff0c;花了钱&#xff0c;却没有掌…

移动端 UI 风格,魅力无限

移动端 UI 风格&#xff0c;打造极致体验

在推荐四款软件卸载工具,让流氓软件无处遁形

Revo Uninstaller Revo Uninstaller是一款电脑软件、浏览器插件卸载软件&#xff0c;目前已经有了17年的历史了。可以扫描所有window用户卸载软件后的残留物&#xff0c;并及时清理&#xff0c;避免占用电脑空间。 Revo Uninstaller可以通过命令行卸载软件&#xff0c;可以快速…

ChatGPT-4o独家揭秘:全国一卷高考语文作文如何轻松斩获满分?

​一、2024年全国一卷高考 二、2018年全国一卷高考 三、2016年全国一卷高考 一、2024年全国一卷高考 技术进步的悖论&#xff1a;我们的问题真的在减少吗&#xff1f; 引言 随着互联网的普及和人工智能的应用&#xff0c;越来越多的问题能够快速得到解答。然而&#xff0c;这引…

msvcr120.dll丢失怎样修复?为什么msvcr120.dll文件很重要

msvcr120.dll​ 是一个属于 Microsoft Visual C 2013 Redistributable package 的动态链接库文件。这个文件对于运行使用 Visual Studio 2013 开发的应用程序是必要的&#xff0c;因为它包含了C运行时库的一部分功能&#xff0c;这些功能是标准C库中与输入/输出操作、字符串操作…

Redis客户端界面工具QuickRedis

介绍 QuickRedis 是一款 永久免费 的 Redis 可视化管理工具。它支持直连、哨兵、集群模式&#xff0c;支持亿万数量级的 key&#xff0c;还有令人兴奋的 UI。QuickRedis 支持 Windows 、 Mac OS X 和 Linux 下运行。 QuickRedis 是一个效率工具&#xff0c;当别人在努力敲命令…

RK3588+FPGA+算能BM1684X:高性能AI边缘计算盒子,应用于视频分析、图像视觉等

搭载RK3588&#xff08;四核 A76四核 A55&#xff09;&#xff0c;CPU主频高达 2.4GHz &#xff0c;提供1MB L2 Cache 和 3MB L3 &#xff0c;Cache提供更强的 CPU运算能力&#xff0c;具备6T AI算力&#xff0c;可扩展至38T算力。 产品规格 系统主控CPURK3588&#xff0c;四核…

torch.cat 与 torch.concat函数

文章目录 区别torch.cat介绍作用参数使用实例关于参数dim为None的使用 区别 先说结论&#xff1a;没有区别在功能、用法以及作用上&#xff0c;concat函数就是cat函数的别名&#xff08;官方就是这样说的&#xff09;。下面截图为证&#xff1a;   因此接下来就主要是介绍 to…

苹果手机618大降价重登销量榜首 红米K70pro为何成京东618国产手机之光

今天的618已经好几天了&#xff0c;很多买有机的已经下单&#xff0c;不出意外苹果15系列手机仍然是最卖座的手机&#xff0c;大家虽然口号喊得很响身体却是诚实的。但令人感到意外的是&#xff0c;今年618国产手机的第一把交椅确实红米K70系列&#xff0c;说好的支持华为呢&am…

哈夫曼树的创建

要了解哈夫曼树&#xff0c;可以先了解一下哈夫曼编码&#xff0c;假设我们有几个字母&#xff0c;他们的出现频率是A: 1 B: 2 C: 3 D: 4 E: 5 F: 6 G: 7。那么如果想要压缩数据的同时让访问更加快捷&#xff0c;就要让频率高的字母离根节点比较进&#xff0c;容易访问&#xf…

el-input中change事件造成的坑

el-input中change事件造成的坑 一、change事件定义二、如果仅回车时候触发 一、change事件定义 仅在输入框失去焦点或用户按下回车时触发 二、如果仅回车时候触发 <el-inputv-model.trim"questionInput"placeholder"请输入你的问题&#xff0c;按回车发送&…

vue-$set修改深层对象的值

背景&#xff1a; 点击编辑按钮&#xff0c;打开修改预算的抽屉&#xff0c;保存后更新此行数据的预算&#xff0c;以前是调接口刷新表格&#xff0c;这次的数据是由前端处理更新&#xff0c;由于数据层级比较深&#xff0c;使用$set来修改两层嵌套对象 使用方法&#xff1a; …

python - DataFrame查询数据操作

学习目标 掌握获取df一列或多列数据的方法 知道loc和iloc的区别以及使用方法 知道df的query函数的使用方法 知道isin函数的作用和使用方法 获取DataFrame子集的基本方法 1.1 从前从后获取多行数据 案例中用到的数据集在文章顶部 LJdata.csv 前景回顾 head() & tail(…

opencv实战小结-银行卡号识别

实战1-银行卡号识别 项目来源&#xff1a;opencv入门 项目目的&#xff1a;识别传入的银行卡照片中的卡号 难点&#xff1a;银行卡上会有一些干扰项&#xff0c;如何排除这些干扰项&#xff0c;并且打印正确的号码是一个问题 最终效果如上图 实现这样的功能需要以下几个步骤…

JDK7 JDK8 JDK9接口中的默认方法、静态方法、私有方法

JDK8开始之后接口新增的方法 JDK7以前&#xff1a;接口中只能定义抽象方法 JDK8的新特性&#xff1a;接口中可以定义有方法体的方法&#xff08;默认、静态&#xff09; JDK9的新特性&#xff1a;接口中可以定义私有方法 接口中的默认方法InterA package com.itheima.a06;p…