作者简介

杨子夜,Intel存储软件开发工程师,主要从事SPDK软件开发工作。

随着由NVM Express 发布新闻宣告发布了 NVMe/TCPtransport [1][2](NVMe-oF TP 8000 TCP Transport)的spec,SPDK 也基于master branch (https://github.com/SPDK) 发布了patch[3]进行对这个transport的支持。在这篇文章中我们会简要介绍一下为什么需要NVMe/TCP transport,并且对SPEC做简单介绍, 然后介绍SPDK library中对于NVMe/TCP transport的支持情况, 包括现状、发展蓝图以及一些简单的使用方法。

1.  NVMe/TCP transport spec的诞生

近日NVM Express (NVMe 协议标准工作组) 发布了NVMe_TP_8000这个SPEC文档。这个SPEC从2017年7月19日开始起草,历时16个月的时间,终于发布了第一个版本。这个SPEC 弥补了NVMe over Fabrics的一些短板。由于NVMe的初衷是提供一个协议用于高速访问本地的PCIe SSD (NVMe over PCIe), 去除SCSI 层,使用简化的并发协议,以提高性能。在本地高速访问协议出现以后,很自然地有用户希望NVMe可以进行远程访问(Remote Access)。就像SCSI 协议在以太网协议上扩展成为iSCSI协议,NVMe 协议也在Fabrics, 包括InfiniBand, Fibre Channel, Ethernet 上扩展成为了NVMe over Fabrics (简称NVMe-oF)协议,如图 1所示。为了保证NVMe低延迟的属性在fabrics 上继续延续,通过远程Fabrics的访问必须继续高效。于是最开始的NVMe-oF 协议制定了基于RDMA (Remote Direct Memory access) 协议的一个transport。 RDMA协议非常高效,对于极端情况,在fabrics上的延迟的开销会非常小。比如通过NVMe  协议对于本地盘进行随机读,对于(4KB大小,qd=1)的随机读的latency在几十微秒左右,那么在RDMA的transport上只会增加同样数量级的时延,这样NVMe协议的高效访问对于远程依然有效,NVMe协议的初衷依然得以保存和延续。于是用户可以使用各种Fabrics(例如Inifiniband 或者Ethernet), 只要该transport能够支持NVMe-oF协议中规定的使用RDMA的交互协议, 当然基于FC Transport的有专门的FC-NVMe 管理,定义在INCITS 540 Fibre Channel中。

图 1 NVMe over Fabrics 架构

但是从去年开始,标准组织又开始制定基于TCP的transport。众所周知,和RDMA协议相比,使用TCP 协议会破坏NVMe 协议设计的初衷,主要原因是使用TCP 协议会带来远高于本地PCIe访问的网络延迟,使得NVMe协议低延迟的目标遭到破坏。不过笔者认为有以下4个原因(仅供参考)促使了NVMe/TCP Transport的诞生:

1. NVMe 虚拟化的出现。 在NVMe虚拟化实现的前提下,NVMe-oF target那端并不一定需要真实的NVMe 设备,可以是由分布式系统抽象虚拟出来的一个虚拟NVMe 设备,为此未必继承了物理NVMe设备的高性能的属性 。那么在这一前提下,使用低速的TCP协议也未尝不可。

2. 向后兼容性。NVMe-oF协议,在某种程度上希望替换掉iSCSI 协议(iSCSI最初的协议是RFC3720,有很多扩展)。iSCSI协议只可以在以太网上运行,对于网卡没有太多需求,并不需要网卡一定支持RDMA。 当然如果能支持RDMA, 则可以使用iSER协议,进行数据传输的CPU 资源卸载(workload offloading)。 但是NVMe-oF 协议一开始没有TCP的支持。于是当用户从iSCSI向NVMe-oF 转型的时候,很多已有的网络设备无法使用。这样会导致NVMe-oF协议的接受度下降。在用户不以性能为首要考量的前提下,显然已有NVMe-oF协议对硬件的要求,会给客户的转型造成障碍,使得用户数据中心的更新换代不能顺滑地进行。

3. TCP offloading。虽然TCP协议在很大程度上会降低性能,但是TCP也可以使用offloading,或者使用Smart NIC,或者FPGA。那么潜在的性能损失,可以得到一定的弥补。那么提供一个TCP协议的transport,是可行的。总的来说短期有性能损失,长期来讲协议对硬件的要求降低,性能可以改进。为此总的来讲,接受度会得到提升。

4. 和Software RoCE的比较。在没有TCP transport的时候,用户在不具备RDMA网卡设备的时候。如果要进行NVMe-oF的测试,需要通过Software RoCE,把网络设备模拟成一个具有RDMA功能的设备,然后进行相应的测试。其真实实现是通过内核的相应模块,实际UDP 包来封装模拟RDMA协议。有了TCP transport协议,则没有这么复杂,用户可以采用更可靠的TCP协议来进行NVMe-oF的一些相关测试。 从测试部署来讲更加简单,有效。

2. NVMe/TCCP transport 协议简要介绍

NVMe/TCP transport 的协议,在一定程度上借鉴了iSCSI的协议,例如iSCSI数据读写的传输协议 。这个不太意外,因为有些协议的指定参与者,也是iSCSI协议的指定参与者。另外iSCSI协议的某些部分确实写得很好。 但是NVMe/TCP  和iSCSI协议相比更加简单,可以说是取其精华。 接下来我们先看一些TCP transport协议的定义,然后再看一下基于TCP transport的交互过程。

NVMe/TCP PDU的定义

图 2 和图3给出了NVMe/TCP PDU  (Protocol Data Unit)的定义,一共包括5个部分:HDR (= CH + PSH), HDGST, PAD, DATA, DDGST。HDR由CH + PSH 组成。这个 NVME PDU 被封装在一个TCP PDU 中, 也可以被分割在不同TCP PDU的Payload中。对于这个点,不需要使用NVMe/TCP transport的编程人员考虑。

 1 . HDR:  PDU header的头部定义。 由CH (Common header,通用头部) 和 PSH (PDU specific header)组成。

 a.  其中CH固定长度8个bytes,并且定义了以下的数据字段,以byte为单位。

i.     PDU-type (offset=0, len=1): PDU的类型。一共有9种不同类型的PDU header。

ii.     Flags (offset=1, len=1: 不同类型的PDU 有不同类型的标志位。

iii.     Header Length (hlen: offset=2, len=1): PDU type的长度包括 header digest的长度。

iv.     PDU Data Offset (PDO: offset=3, len=1): 如果有数据字段,这个值代表数了数据 (Data, 如果存在的话) 在整个PDU中的offset的起始值,这个值最大不超过128 。

v.     PDU Length (PLEN: offset=4, len=4): 整个PDU的长度,包括CH + PSH  + HDGST(可选)  + PAD(可选) + DATA(可选) + DDGST (可选)。

b. PSH:不同类型的PDU,请参考TP  8000SPEC( [2] 中ZIP包解压后名字叫做“NVMe-oF - TP 8000 TCP Transport 2018.11.13-Ratified”的文件)中描述的每个PDU的定义。

2 . HDGST: Header Digest.  Header的摘要,其固定长度是4 bytes,header digest是可选的。 如果target 和host 协商需要使用Header Digest,相应的PDU 才会有。

3 . PAD:主要用于数据的Padding。根据Spec的定义,Data在整个PDU中的最大的offload起始值是128,所以data padding的长度是有限。填充长度是根据CPDA(Controller PDU Data Align, target决定,当然是双方已经沟通好的值) 或者HPDA (Host PDU Data Align, 由host那段决定,当然是双方已经沟通好的值) 的值,构建出一个不超过128长度大小的值A,再减去(HDR 的Len以及可能的HDGST的长度 , 计为B)。 如果这个值(A - B)是负,则填充长度为0。 一般情况下,PAD的填充的长度是0, 也就是说未必有padding 的数据。

4 .DATA. PDU中带有的数据,未必一定携带。Data的数据一般包含在C2HData或者H2CData中。当然在CapsuleCmd,H2CTermReq, ,2HTermReq 类型的PDU中也会包含。对于这些类型的PDU,Data size的长度可能会有最大长度的限制。

 5 . DDGST:是对持有PDU中持有数据的哈希值,其长度为4。Data Digest 也是可选的,只有target 和host协商使用Data Digest,相应的PDU才会有。

在9种不同类型的PDU 中,  Host端使用以下4种:

1)  Initialize Connection Request PDU (ICReq): 类型代号0x0

2)  Host to Controller Terminate Connection Request PDU (H2CTermReq):类型代号是0x2

3)  Command Capsule PDU (CapsuleCmd): 类型代号是 0x4

4)   Host To Controller Data Transfer PDU (H2CData):类型代码是0x6

Target 端使用的PDU类型:

1) Initialize Connection Response PDU (ICResp):类型代号 0x1

2) Controller to Host Terminate Connection Request PDU (C2HTermReq): 类型代码0x3

3) Response Capsule PDU (CapsuleResp):类型代号0x5

4) Controller To Host Data Transfer PDU (C2HData):类型代号0x7

5) Ready To Transfer PDU (R2T): 类型代号0x9

这9种PDU的类型不同,他们有以下的异同:

hlen:由于PSH长度不一样,所以HDR的长度(hlen)也不一样,但是hlen最大不会超过128。

HDGST:  ICReq, H2CTermReq, ICResp 和C2HTermReq这4种类型的PDU是必然没有header digest。

DDGST:  ICReq, H2CTermReq, ICResp, C2HTermreq,

CapsuleResp以及R2T 这六种类型的PDU 是必然没有data digest的。

图2 NVMe/TCP PDU的定义

图3 NVMe/TCP PDU header的定义

基于NVMe /TCP transport的交互

使用TCP transport,  target 和host 需要建立相应的TCP 连接。然后NVMe协议中的每一对qpair (由submission queue, 和completion queue组成,  可以是admin qpair 或者是I/O qpair) 会被映射到一个TCP 连接上。举个例子,如果host和target了建立了1个admin qpair,3个I/O qpair, 那么就会有4个TCP连接。总的来讲,通过TCP transport 建立一个连接的简要流程如下:

Target 端监听到一个TCP 端口上,等到host的连接。

Host 通过TCP 请求发起一个连接,连接建立后。发送ICReq PDU, 然后等待Target端的反应。

Target端接收数据,解析PDU ,判断是否是ICReq,如果请求内容正确,则返回ICResp PDU,另外把这个连接设置为active状态,进入这个状态中,意味着不能再接收ICResp PDU。如果在解析过程中出现错误,则返回C2HTermReq, 同时target进入等待时间 (一般为设置为30 秒)。 如果host 没有关闭连接(指收到相应的数据包),则会主动关闭连接。

Host 端接收数据,解析PDU,判断是否是ICResp PDU在过程中,出现错误则返回H2CTermReq,然后关闭连接。再收到正确的ICResp PDU后,标记连接的状态是ACTIVE。以后再收到ICResp,则判断为错误状态。如果收到ICResp内容正确,Host会封装上层发送的NVMe command,封装成为CapsuleCmd,  发送出去。

Target端接收数据,发现是CapsuleCmd PDU后,会提取其中NVMe command (命令为64bytes)进行执行。

a.  如果从host传送过来的PDU已经包含数据 (指InCapsuleData),或者不需要数据,则直接交给NVMe-oF/NVMe 协议层处理; 如果需要数据(没有InCapsuleData, 说明这个NVMe command是写操作),但是没有发送过来,Target端就发送R2T PDU。 Host接收到R2T PDU后, 如果包解析正确,则会发送一个或者多个H2C PDU,然后host等待新的PDU 包过来。 Target收到所有H2C PDU后,获得数据后,则交给NVMe-oF/NVMe 层执行。

b.  在target端的NVMe-oF/NVMe层执行,最终会返回。如果是操作有数据返回(说明从host发过来的NVMe command的是读操作),则会返回若干个C2H PDU给host 端,然后发送CapsuleResp(封装16bytes的NVMe response命令), 其实这个回复是可选的, 如果NVMe SQ 的控制流被禁止了。 如果返回不需要数据,则直接发送CapsuleResp。

c. Host 接收相应的数据,要么是若干个C2H PDU + 可选的CapsuleResp, 要么是单独的CapsuleResp.

总的来讲,a、b、c 步骤完成了一轮从host 端发送封装NVMe command -> CaspsuleCmd PDU,然后最终target 端返回相应数据,以及CapsuleResp(对于读NVMe command非必需)的操作。当然在实际实现过程中,有相应的优化手段,远远没有这么简单。

3. 基于SPDK的NVMe-oF TCP 的transport 的实现以及使用介绍

SPDK  TCP transport的现状和细节

SPDK 库支持NVMe/TCP transport的代码主要分为以下几块:

·头文件的定义: 主要位于spdk/include/spdk/nvme_spec.h, spdk/incude/spdk_internel/nvme_tcp.h

·Host端的实现:主要位于 spdk/lib/nvme 目录,主要实现在nvme_tcp.c文件中。

·Target端的实现: 主要位于spdk/lib/nvmf目录,主要实现在tcp.c文件中。

对于 SPDK第一版本的TCP transport实现,主要目标是基本功能的支持,  包括和kernel 实现的交互性。当然由于kernel版本也刚发布,可能会存在一些问题,比如SPDK 实现的bug,或者kernel实现的bug,为此还处于继续测试和集成的阶段。

另外在TCP transport的实现中,SPDK的target和host 端的TCP transport的PDU接收采用了同样的状态机的逻辑:

状态1:<接收 PDU CH>。 一旦接受满8个byte,则判断是否是个合法PDU。如果是则跳到状态2, 否则跳到状态4。

状态2:<接收 PSH + HDGST(可选) + PAD(可选)>; 如果接收的数据没问题 ,那么有两个可能: (a)需要继续接受数据,则跳到状态3;(b)执行相关NVMe-oF/NVMe 层的逻辑,并且跳到状态1,可以接续接收包。如果接收的数据有问题,则跳到状态4。

状态3: <接收 DATA +  DDGST(可选)>:接收相应的数据。数据正确,则执行相应的NVMe-oF/NVMe层的逻辑,PDU接收逻辑跳转到状态1, 否则进入状态4。

状态4:< 错误状态>。发送 termreq命令。 Target 发送C2HTermreq, host发送H2CTERMReq,同时停止接收处理后续的数据。

SPDK对于NVMe/TCP transport的一些Roadmap

1. 功能的继续完善:继续遵循NVM Express 制定的有关TCP transport的SPEC。不仅确保了SPDK target 和host的交互性正常,另外要继续保证和Kernel 实现的交互性正常。

2.  性能优化,可能有以下几个方面:

a. 优化已有的TCP transport中的代码。

b. 采用用户态的TCP 协议栈  + 用户态的网卡驱动,比如集成VPP来优化TCP Transport, 就像iSCSI target可以整合VPP一样。

c. 后续可能会使用hardware的offloading,诸如FPGA, Smart NIC。这个可能需要设计offloading的接口,来适配各种offloading的引擎。

测试基于TCP transport的SPDK NVMe-oF

总的来讲,在SPDK 中使用NVMe/TCP transport比较简单。和使用RDMA transport相比,只要在原有的命令中把RDMA 替换成为 TCP 即可,以下是一些参考。

测试 SPDK NVMe-oF target

SPDK Target 端使用如下的配置(作为参考)

1. 下载支持TCP的branch([3]),编译后,启动SPDK NVMe-oF target。

例如使用命令:./app/nvmf/nvmf_tgt.

2.  在另外一个Shell窗口执行以下RPC 命令:

# 创建一个大小是64M,每个块大小是512的malloc bdev,命名为Malloc0

scripts/rpc.py construct_malloc_bdev -b Malloc0 64 512

#创建一个TCP transport

scripts/rpc.py nvmf_create_transport -t TCP -p 4

#创建一个 NVMe-oF subsystem

scripts/rpc.py nvmf_subsystem_create nqn.2016-06.io.spdk:cnode1 -a -s SPDK00000000000001

#在这个subsystem的中添加Malloc0,作为相应的subsystem

scripts/rpc.py nvmf_subsystem_add_ns nqn.2016-06.io.spdk:cnode1 Malloc0

#在命名为nqn.2016-06.io.spdk:cnode1的subsystem 上创建一个监听socket,其中IP 地址是192.168.0.128 IP,TCP端口是4420

scripts/rpc.py nvmf_subsystem_add_listener nqn.2016-06.io.spdk:cnode1 -t tcp -a 192.168.0.128 -s 4420

Host 端的测试方法(作为参考)

1.  使用SPDK的自带程序测试基于SPDK的NVMe-oF target.

a.  使用 perf 程序:./spdk/examples/nvme/perf/perf -r 'trtype:TCP adrfam:IPv4 traddr: 192.168.0.128 trsvcid:4420' -s 512 -q 4 -o 4096 –w randrw -M 50 -t 10

b.  使用identify 程序: ./spdk/examples/nvme/identify/identify -r 'trtype:TCP adrfam:IPv4 traddr:192.168.0.128 trsvcid:4420'

2.   使用kernel host测试SPDK的NVMe-oF target.

a. ‍下载已经支持NVMe TCP transport的Linux Kernel [4]。 根据已有Linux kernel 系统的配置, 编译相应内核以及相关模块,替换掉已有的kernel。

b. 如果对应的nvme_tcp 编译为可加载的模块,则在shell中运行以下命令:

modprobe nvme_tcp‍

c.  然后在shell中运行以下命令:

nvme connect -t tcp -n "nqn.2016-06.io.spdk:cnode1" -a 192.168.0.128 -s 4420

d.  如果连结成功,然后使用fdisk -l命令就可以看到一个新的NVMe相关的盘。比如/dev/nvme0n1。那么接着就可以使用fio 命令对发现的新盘进行相关的性能测试。

测试SPDK NVMe-oF Host

Kernel Target 端使用如下的配置(作为参考)

1. 可以采用以下的script, 进行简单配置。命名为add.sh。

modprobe null_blk nr_devices=1

modprobe nvmet

# 插入NVMe-tcp的模块

modprobe nvmet-tcp

mkdir /sys/kernel/config/nvmet/subsystems/mysubsystem

cd /sys/kernel/config/nvmet/subsystems/mysubsystem/

echo 1 > attr_allow_any_host

mkdir namespaces/10

cd namespaces/10/

echo -n /dev/nullb0 > device_path

echo 1 > enable

mkdir /sys/kernel/config/nvmet/ports/1

cd /sys/kernel/config/nvmet/ports/1

echo  $1 > addr_traddr

echo tcp > addr_trtype

echo $2 > addr_trsvcid

echo ipv4 > addr_adrfam

ln -s /sys/kernel/config/nvmet/subsystems/mysubsystem /sys/kernel/config/nvmet/ports/1/subsystems/mysubsystem

然后在shell中执行:

#配置IP地址是192.168.0.128,端口是4420监听端口transport

./add.sh  192.168.0.128 4420

2. 当然后续要清理的时候可以执行以下命名为delete.sh, 内容如下:

rm -rf /sys/kernel/config/nvmet/ports/1/subsystems/mysubsystem

echo 0 > /sys/kernel/config/nvmet/subsystems/mysubsystem/namespaces/10/enable

echo -n 0 > /sys/kernel/config/nvmet/subsystems/mysubsystem/namespaces/10/device_path

rmdir --ignore-fail-on-non-empty /sys/kernel/config/nvmet/subsystems/mysubsystem/namespaces/10

rmdir --ignore-fail-on-non-empty /sys/kernel/config/nvmet/subsystems/mysubsystem

rmdir --ignore-fail-on-non-empty /sys/kernel/config/nvmet/ports/1

rmmod nvmet-tcp

rmmod nvmet

rmmod null_blk

备注:add.sh和delete.sh 仅供参考。具体配置Linux kernel NVMe-oF Target请参考官方文档

Host 端的测试方法(作为参考)

a.使用 perf 程序:

./spdk/examples/nvme/perf/perf -r 'trtype:TCP adrfam:IPv4 traddr: 192.168.0.128 trsvcid:4420' -s 512 -q 4 -o 4096 –w randrw -M 50 -t 10

b.使用identify 程序:

./spdk/examples/nvme/identify/identify -r 'trtype:TCP adrfam:IPv4 traddr:192.168.0.128 trsvcid:4420'

备注:如果在Target端安装的Linux kernel 中的NVMe/TCP不支持header摘要(HDGST)或者 Data摘要(DDGST), 那么使用SPDK host的时候,需要在一开始发送ICReq PDU中,把相关Digest的bit清零。目前我们还没提供较为简易的使用SPDK NVMe-oF host的配置(之后的Patch会提供),但是可以参考Reference中的Patch [5]。

参考文献

[1] "WelcomeNVMe™/TCP to the NVMe-oF™ Family of Transports," [Online]. Available:https://nvmexpress.org/welcome-nvme-tcp-to-the-nvme-of-family-of-transports/.

[2] "NVMe/TCPTransport Binding specification," [Online]. Available:https://nvmexpress.org/wp-content/uploads/NVM-Express-over-Fabrics-1.0-Ratified-TPs.zip.

[3] "TheSPDK NVMe/TCP support," [Online]. Available:https://review.gerrithub.io/#/c/spdk/spdk/+/425191/.

[4] "TheLinux Kernel NVMe/TCP support," [Online]. Available:http://git.infradead.org/nvme.git/shortlog/refs/heads/nvme-tcp.

[5] "SPDKtemporary Patch for using SPDK host testing Linux TCP tranposrt based NVMe-oFtarget," [Online]. Available:https://review.gerrithub.io/#/c/spdk/spdk/+/427189/.

1

END

1

推荐阅读 

送!京东电子卡

活动规则:带文字将本文转发至朋友圈,并将截图发至公众号后台,小编将随机抽取四位幸运读者:

一等奖1名,送50元京东电子卡

二等奖1名,送30元京东电子卡

三等奖2名,送10元京东电子卡

领取方式:后台发送卡号及卡密。

最新!SPDK宣布在NVMe-oF Fabrics中支持TCP transport相关推荐

  1. 深入剖析NVMe Over Fabrics

    前两篇,我们探讨了: <详解:什么是NVMe?> <Why NVMe?> 本篇我们将探讨: 1.NVMe Over Fabrics是什么?Fabrics包括哪几类? 2.为什么 ...

  2. NVMe Over Fabrics架构概述

    NVMe Over Fabrics使用RDMA或光纤通道(FC)架构等Fabric技术取代PCIe传输.如图所示,除了基于RDMA架构的传输包括以太网(ROCE),InfiniBand和iWARP,当 ...

  3. 【转】SPDK 助力加速 NVMe 硬盘

    https://software.intel.com/zh-cn/articles/accelerating-your-nvme-drives-with-spdk 简介 存储性能开发套件 (SPDK) ...

  4. 谷歌浏览器最新版本存放插件文件在电脑中位置路径

    描述:谷歌浏览器最新版本存放插件文件在电脑中位置路径 步骤: C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Profile 1\Extensio ...

  5. [转]使用 .NET Framework 2.0 在您的应用程序中支持证书

    NET 安全 使用 .NET Framework 2.0 在您的应用程序中支持证书 Dominick Baier 本文讨论: Windows 证书存储区 .NET 中的证书类 验证.SSL.Web 服 ...

  6. java8返回单个号码_如何在单个API中支持Java 6、8、9

    java8返回单个号码 借助jOOQ 3.7,我们终于添加了对Java 8功能的正式支持. 这为许多不错的改进打开了大门,例如: 创建结果流 try (Stream<Record2<Str ...

  7. 如何在单个API中支持Java 6、8、9

    借助jOOQ 3.7,我们终于添加了对Java 8功能的正式支持. 这为许多不错的改进打开了大门,例如: 创建结果流 try (Stream<Record2<String, String& ...

  8. 在 VMware ESXi 5.5 和 6.0.x 中支持大于 2 TB 的虚拟机磁盘 (2058287)

    Purpose 免责声明:本文为 Support for virtual machine disks larger than 2 TB in VMware ESXi 5.5 and 6.0.x (20 ...

  9. 最新ChatGPT商业运营版网站源码+支持AI绘画+支持用户会员套餐+邀请分佣功能+支持后台一键更新+网站后台管理+永久更新!

    最新ChatGPT商业运营版网站源码+支持AI绘画+支持用户会员套餐+邀请分佣功能+支持后台一键更新+网站后台管理+永久更新! AI付费创作系统: 如果后续程序有新版,直接在后台一键更新即可! 程序完 ...

最新文章

  1. Maven实战:Maven生命周期
  2. [译]LINT TO SQL 介绍(数据库查询) - Part.3
  3. 肝!用 Python 加密文件
  4. Angular Shadow Root DOM的一些API
  5. sharding-jdbc学习
  6. RTL8201网络芯片讲解
  7. 什么是区块链――区块链的分布式数据库、共识机制
  8. layer关闭当前窗口并刷新父窗口
  9. Atitit.Gui按钮与面板---项目规模的评估----文件数统计,结构,代码行数,每类型文件行数.
  10. 算法导论第三版 第30章习题答案
  11. MySQL 主从幂等复制slave_exec_mode=IDEMPOTENT
  12. 实验六系统安装与简单配置(Linux基础教程 第2版)虚拟机环境下安装龙蜥操作系统(Anolis OS) 8.4
  13. 利用轻量级js插件Beer Slider实现新老图片的实时对比
  14. XC3101锂电池充电IC(耐高压,带OVP)
  15. 并查集算法 | Union-Find Algorithm
  16. 第六十四章 Caché 函数大全 $STACK 函数
  17. 【LeetCode】1823. 找出游戏的获胜者 Find the Winner of the Circular Game
  18. Unity Failed to load ‘Sssets/Plugins/xxx.dll with error 找不到指定的模块
  19. 【C++从0到1】7.C++中标识符的命名
  20. 「 工业缺陷检测深度学习方法」最新2022研究综述

热门文章

  1. Xmind 8 pro 软件破解版
  2. 3ds max渲图自从使用了云渲染,不可能在自己渲图了
  3. Final发布中间产物
  4. 爬虫总结(二)-- scrapy
  5. 戒指在不同手指的意义
  6. oracle实例恢复 redo,ORACLE不完全恢复之current或active状态redo损坏(二)
  7. SAP Scripting Tracker基本使用技巧
  8. 【ARM学习笔记】ARM Cortex -A7 EPIT定时器
  9. 2.2 DP: Value Iteration Gambler‘s Problem
  10. python 拼多多抢券_拼多多满减优惠 AC代码 python