微信公众号:运维开发故事,作者;夏老师

CRI shim是什么?

实现了 CRI 接口的容器运行时通常称为 CRI shim, 这是一个 gRPC Server,监听在本地的 unix socket 上;而 kubelet 作为 gRPC 的客户端来调用 CRI 接口,来进行 Pod 和容器、镜像的生命周期管理。另外,容器运行时需要自己负责管理容器的网络,推荐使用 CNI。
kubelet 调用下层容器运行时的执行过程,并不会直接调用Docker 的 API,而是通过一组叫作 CRI(Container Runtime Interface,容器运行时接口)的 gRPC 接口来间接执行的,意味着需要使用新的连接方式与 docker 通信,为了兼容以前的版本,k8s 提供了针对 docker 的 CRI 实现,也就是 kubelet 包下的dockershim包,dockershim是一个 grpc 服务,监听一个端口供 kubelet 连接,dockershim收到 kubelet 的请求后,将其转化为 REST API 请求,再发送给docker daemon。Kubernetes 项目之所以要在 kubelet 中引入这样一层单独的抽象,当然是为了对 Kubernetes 屏蔽下层容器运行时的差异。
![image.png](https://img-blog.csdnimg.cn/img_convert/1edc2d18b36868c9614a5d4b13f413ed.png#clientId=ubfa83de4-e122-4&from=paste&height=512&id=HKTqm&margin=[object Object]&name=image.png&originHeight=1024&originWidth=2436&originalType=binary&ratio=1&size=135204&status=done&style=none&taskId=u82f88702-9519-4448-a937-872aeaa02d8&width=1218)
解决思路再次体现了《代码大全2》里提到的那句经典名言:
any problem in computer science can be sloved by another layer of indirecition。计算机科学领域的任何问题都可以通过增加一个中间层来解决,我们的 CRI shim就是加了这样一层。

CRI shim server 接口图示

**CRI 接口包括 RuntimeService 和 ImageService 两个服务,这两个服务可以在一个 gRPC server 中实现,也可以分开成两个独立服务。**目前社区的很多运行时都是将其在一个 gRPC server 里面实现。
![image.png](https://img-blog.csdnimg.cn/img_convert/88f85851df0cd60dcc2f401951a363a7.png#clientId=u6f450f7d-9368-4&from=paste&height=606&id=fnxe3&margin=[object Object]&name=image.png&originHeight=1212&originWidth=2122&originalType=binary&ratio=1&size=181649&status=done&style=none&taskId=ud6e49e52-c0c8-4e34-95e3-f2415c38710&width=1061)

ImageServiceServer 提供了 5 个接口,用于管理容器镜像。
管理镜像的 ImageService 提供了 5 个接口:

  • 查询镜像列表;
  • 拉取镜像到本地;
  • 查询镜像状态;
  • 删除本地镜像;
  • 查询镜像占用空间等。

关于容器镜像的操作比较简单,所以我们就暂且略过。接下来,我主要为你讲解一下 RuntimeService 部分。
RuntimeService 则提供了更多的接口,按照功能可以划分为四组:

  • PodSandbox 的管理接口:CRI 设计的一个重要原则,就是确保这个接口本身,只关注容器,不关注 Pod。
  • PodSandbox 是对 Kubernete Pod 的抽象,用来给容器提供一个隔离的环境(比如挂载到相同的 CGroup 下面),并提供网络等共享的命名空间。PodSandbox 通常对应到一个 Pause 容器或者一台虚拟机;
  • Container 的管理接口:在指定的 PodSandbox 中创建、启动、停止和删除容器;

![image.png](https://img-blog.csdnimg.cn/img_convert/1da4627bd83d58049d645c75d9df443d.png#clientId=u7f106c04-82a1-4&from=paste&height=238&id=Zi9ok&margin=[object Object]&name=image.png&originHeight=475&originWidth=1080&originalType=binary&ratio=1&size=331757&status=done&style=none&taskId=ue02c5d23-ed18-4c3b-98b0-b3469381a40&width=540)

  • Streaming API 接口:包括 Exec、Attach 和 PortForward 等三个和容器进行数据交互的接口,这三个接口返回的是运行时 Streaming Server 的 URL,而不是直接跟容器交互。kubelet 需要跟容器项目维护一个长连接来传输数据。这种 API,我们就称之为 Streaming API。
  • 状态接口:包括查询 API 版本和查询运行时状态。

我们通过 kubectl 命令来运行一个 Pod,那么 Kubelet 就会通过 CRI 执行以下操作:

  • 首先调用 RunPodSandbox 接口来创建一个 Pod 容器,Pod 容器是用来持有容器的相关资源的,比如说网络空间、PID空间、进程空间等资源;
  • 然后调用 CreatContainer 接口在 Pod 容器的空间创建业务容器;
  • 再调用 StartContainer 接口启动运行容器
  • 最后调用停止,销毁容器的接口为 StopContainer 与 RemoveContainer。

就完成了整个Container的生命周期。

Streaming API

CRI shim 里对 Streaming API 的实现,依赖于一套独立的 Streaming Server 机制。Streaming API 用于客户端与容器进行交互,包括 Exec、PortForward 和 Attach 等三个接口。kubelet 内置的 Docker 通过 nsenter、socat 等方法来支持这些特性,但它们不一定适用于其他的运行时,也不支持 Linux 之外的其他平台。因而,CRI 也显式定义了这些 API,并且要求容器运行时返回一个 Streaming Server 的 URL 以便 kubelet 重定向 API Server 发送过来的流式请求。
![image.png](https://img-blog.csdnimg.cn/img_convert/a31402d825d4f98d9fee08efbd0688f4.png#clientId=u6abb2437-a6fd-4&from=paste&height=348&id=dRIkw&margin=[object Object]&name=image.png&originHeight=695&originWidth=1937&originalType=binary&ratio=1&size=147078&status=done&style=none&taskId=u2727d943-eecb-43a8-b6aa-a31ad63edcb&width=968.5)

因为所有容器的流式请求都会经过 kubelet,这可能会给节点的网络流量带来瓶颈,因而 CRI 要求容器运行时启动一
个对应请求的单独的流服务器,将地址返回给 kubelet。kubelet 将这个信息再返回给 Kubernetes API Server,会直接打开与运行时提供的服务器相连的流连接,并通过它与客户端连通。
这样一个完整的 Exec 流程就如上图所示,分为多个阶段:

  • 客户端 kubectl exec -i -t …;
  • kube-apiserver 向 kubelet 发送流式请求 /exec/;
  • kubelet 通过 CRI 接口向 CRI Shim 请求 Exec 的 URL;
  • CRI Shim 向 kubelet 返回 Exec URL;
  • kubelet 向 kube-apiserver 返回重定向的响应;
  • kube-apiserver 重定向流式请求到 Exec URL,然后将 CRI Shim 内部的 Streaming Server 跟 kube-apiserver 进行数据交互,完成 Exec 的请求和响应。

也就是说 apiserver 其实实际上是跟 streaming server 交互来获取我们的流式数据的。这样一来让我们的整个 CRI Server 接口更轻量、更可靠。
注意:当然,这个 Streaming Server 本身,是需要通过使用 SIG-Node 为你维护的 Streaming API 库来实现的。并且,Streaming Server 会在 CRI shim 启动时就一起启动。此外,Stream Server 这一部分具体怎么实现,完全可以由 CRI shim 的维护者自行决定。比如,对于 Docker 项目来说,dockershim 就是直接调用 Docker 的 Exec API 来作为实现的。

CRI-containerd架构解析与主要接口解析

![image.png](https://img-blog.csdnimg.cn/img_convert/5dd816ec1950eeb29da6638d74a35d19.png#clientId=u6b46695e-eb1c-4&from=paste&height=338&id=u2e9b7200&margin=[object Object]&name=image.png&originHeight=675&originWidth=1620&originalType=binary&ratio=1&size=218172&status=done&style=none&taskId=u18bd24b2-4b67-41dc-b8ac-8b4d6d3272b&width=810)
![image.png](https://img-blog.csdnimg.cn/img_convert/8344e052008131de4eb35099df361410.png#clientId=u7f106c04-82a1-4&from=paste&height=198&id=vwJx9&margin=[object Object]&name=image.png&originHeight=395&originWidth=1080&originalType=binary&ratio=1&size=225113&status=done&style=none&taskId=uf690d137-f035-4d9a-b4c9-da3ea33587c&width=540)

整个架构看起来非常直观。这里的 Meta services、Runtime service 与 Storage service 都是 containerd 提供的接口。它们是通用的容器相关的接口,包括镜像管理、容器运行时管理等。CRI 在这之上包装了一个 gRPC 的服务。右侧就是具体的容器的实现。比如说,创建容器时就要创建具体的 runtime 和它的containerd-shim。 Container 和 Pod Sandbox组成了一个Pod。
CRI-containerd 的一个好处是,containerd 还额外实现了更丰富的容器接口,所以它可以用 containerd 提供的 ctr 工具来调用这些丰富的容器运行时接口,而不只是 CRI 接口
CRI实现了两个GRPC协议的API,提供两种服务ImageService和RuntimeService。

// grpcServices are all the grpc services provided by cri containerd.
type grpcServices interface {runtime.RuntimeServiceServerruntime.ImageServiceServer
}
// CRIService is the interface implement CRI remote service server.
type CRIService interface {Run() error// io.Closer is used by containerd to gracefully stop cri service.io.Closerplugin.ServicegrpcServices
}

CRI的实现CRIService中包含了很多重要的组件:其中最重要的是cni.CNI,用于配置容器网络。还有containerd.Client,用于连接containerd来创建容器。


// criService implements CRIService.
type criService struct {// config contains all configurations.config criconfig.Config// imageFSPath is the path to image filesystem.imageFSPath string// os is an interface for all required os operations.os osinterface.OS// sandboxStore stores all resources associated with sandboxes.sandboxStore *sandboxstore.Store// sandboxNameIndex stores all sandbox names and make sure each name// is unique.sandboxNameIndex *registrar.Registrar// containerStore stores all resources associated with containers.containerStore *containerstore.Store// containerNameIndex stores all container names and make sure each// name is unique.containerNameIndex *registrar.Registrar// imageStore stores all resources associated with images.imageStore *imagestore.Store// snapshotStore stores information of all snapshots.snapshotStore *snapshotstore.Store// netPlugin is used to setup and teardown network when run/stop pod sandbox.netPlugin cni.CNI// client is an instance of the containerd clientclient *containerd.Client// streamServer is the streaming server serves container streaming request.streamServer streaming.Server// eventMonitor is the monitor monitors containerd events.eventMonitor *eventMonitor// initialized indicates whether the server is initialized. All GRPC services// should return error before the server is initialized.initialized atomic.Bool// cniNetConfMonitor is used to reload cni network conf if there is// any valid fs change events from cni network conf dir.cniNetConfMonitor *cniNetConfSyncer// baseOCISpecs contains cached OCI specs loaded via `Runtime.BaseRuntimeSpec`baseOCISpecs map[string]*oci.Spec
}

我们知道 Kubernetes 的一个运作的机制是面向终态的,在每一次调协的循环中,Kubelet 会向 apiserver 获取调度到本 Node 的 Pod 的数据,再做一个面向终态的处理,以达到我们预期的状态。
循环的第一步,首先通过 List 接口拿到容器的状态。确保有镜像,如果没有镜像则 pull 镜像再通过 Sandbox 和 Container 接口来创建容器。
需要注意的是,我们的 CNI(容器网络接口)也是在 CRI 进行操作的,因为我们在创建 Pod 的时候需要同时创建网络资源然后注入到 Pod 中(PS:CNI包含在创建Pod 这个动作里)。接下来就是我们的容器和镜像。我们通过具体的容器创建引擎来创建一个具体的容器。
执行流程为:

  • Kubelet 通过 CRI runtime service API 调用 CRI plugin 创建 pod
  • CRI 通过 CNI 创建 pod 的网络配置和 namespace
  • CRI使用 containerd 创建并启动 pause container (sandbox container) 并且把这个 container 置于 pod 的 cgroups/namespace
  • Kubelet 接着通过 CRI image service API 调用 CRI plugin, 获取容器镜像
  • CRI 通过 containerd 获取容器镜像
  • Kubelet 通过 CRI runtime service API 调用 CRI, 在 pod 的空间使用拉取的镜像启动容器
  • CRI 通过 containerd 创建/启动 应用容器, 并且把 container 置于 pod 的 cgroups/namespace. Pod 完成启动。

ctr命令行工具初始用

打印服务端和客户端版本:

# ctr version
Client:
Version:  v1.4.4
Revision: 05f951a3781f4f2c1911b05e61c160e9c30eaa8e
Go version: go1.15.8Server:
Version:  v1.4.4
Revision: 05f951a3781f4f2c1911b05e61c160e9c30eaa8e
UUID: dee82270-b4b4-429c-befa-45df1421da7e

pull docker hub中的redis镜像:

# pull镜像
ctr images pull docker.io/library/redis:alpine3.13# 查看镜像
ctr i ls

pull私有仓库的镜像,需要使用-u :给定镜像仓库的用户名和密码:

ctr images pull -u user:password harbor.my.org/library/nginx:1.1

启动这个测试的redis容器:

ctr run -d docker.io/library/redis:alpine3.13 redis

查看容器(container和task):

ctr container ls
CONTAINER    IMAGE                                 RUNTIME
redis        docker.io/library/redis:alpine3.13    io.containerd.runc.v2ctr task ls
TASK     PID      STATUS
redis    20808    RUNNING

注意: 在containerd中,container和task是分离的,container描述的是容器分配和附加资源的元数据对象,是静态内容,task是任务是系统上一个活动的、正在运行的进程。 task应该在每次运行后删除,而container可以被多次使用、更新和查询。这点和docker中container定义是不一样的。
进入到容器中执行redis命令:

ctr task exec -t --exec-id redis-sh redis sh
/data # redis-cli
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> get k1
"v1"

查看一下系统中的进程信息:

ps -ef | grep runc | grep redis
/usr/local/containerd/bin/containerd-shim-runc-v2 -namespace default -id redis -address /run/containerd/containerd.sock

可以看出containerd中是存在namespace概念的,这样可以将不同业务和应用进行隔离,例如k8s使用containerd和直接使用ctr创建的容器可以隔离开。查看一下当前的namespace:

ctr ns ls
NAME    LABELS
default
k8s.io

不同namespace下pull的镜像也是隔离显示的,可以使用-n指定具体的namespace:

ctr -n default i ls
ctr -n k8s.io i ls

crictl命令行工具配置和初始用

crictl是k8s cri-tools的一部分,它提供了类似于docker的命令行工具,不需要kubelet就可以通过CRI跟容器运行时通信。 crictl是专门为k8s设计的,提供了Pod、容器和镜像等资源的管理命令,可以帮助用户调试容器应用或者排查异常问题。 crictl 可以用于所有实现了CRI接口的容器运行时。可以排查常见的PLEG的问题。
尝试使用ctictl命令行工具查看一下镜像,给了一个警告信息:

crictl images
WARN[0000] image connect using default endpoints: [unix:///var/run/dockershim.sock unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock]. As the default settings are now deprecated, you should set the endpoint instead.
ERRO[0002] connect endpoint 'unix:///var/run/dockershim.sock', make sure you are running as root and the endpoint has been started: context deadline exceeded
IMAGE               TAG                 IMAGE ID            SIZE

需要我们显示配置默认的endpoints:

crictl config runtime-endpoint unix:///run/containerd/containerd.sock
crictl config image-endpoint unix:///run/containerd/containerd.sock

上面的命令执行完成后将生成配置文件/etc/crictl.yaml:

runtime-endpoint: "unix:///run/containerd/containerd.sock"
image-endpoint: "unix:///run/containerd/containerd.sock"
timeout: 0
debug: false
pull-image-on-create: false
disable-pull-on-run: false

pull镜像:

crictl pull docker.io/library/redis:alpine3.13crictl images
IMAGE                                       TAG                 IMAGE ID            SIZE
docker.io/library/redis                     alpine3.13          554d20f203657       10.9MB

crictl pull的镜像实际上是在k8s.io namespace下,可以使用ctr -n k8s.io i ls查看。
crictl不能像ctr那样通过参数给定用户名和密码的方式从开启认证的私有仓库中pull镜像。需要对containerd进行配置。 containerd提供的各种功能在其内部都是通过插件实现的,可以使用ctr plugins ls查看containerd的插件:

ctr plugins lsTYPE                            ID                       PLATFORMS      STATUS
io.containerd.content.v1        content                  -              ok
io.containerd.snapshotter.v1    aufs                     linux/amd64    error
io.containerd.snapshotter.v1    btrfs                    linux/amd64    error
io.containerd.snapshotter.v1    devmapper                linux/amd64    error
io.containerd.snapshotter.v1    native                   linux/amd64    ok
io.containerd.snapshotter.v1    overlayfs                linux/amd64    ok
io.containerd.snapshotter.v1    zfs                      linux/amd64    error
io.containerd.metadata.v1       bolt                     -              ok
io.containerd.differ.v1         walking                  linux/amd64    ok
io.containerd.gc.v1             scheduler                -              ok
io.containerd.service.v1        introspection-service    -              ok
io.containerd.service.v1        containers-service       -              ok
io.containerd.service.v1        content-service          -              ok
io.containerd.service.v1        diff-service             -              ok
io.containerd.service.v1        images-service           -              ok
io.containerd.service.v1        leases-service           -              ok
io.containerd.service.v1        namespaces-service       -              ok
io.containerd.service.v1        snapshots-service        -              ok
io.containerd.runtime.v1        linux                    linux/amd64    ok
io.containerd.runtime.v2        task                     linux/amd64    ok
io.containerd.monitor.v1        cgroups                  linux/amd64    ok
io.containerd.service.v1        tasks-service            -              ok
io.containerd.internal.v1       restart                  -              ok
io.containerd.grpc.v1           containers               -              ok
io.containerd.grpc.v1           content                  -              ok
io.containerd.grpc.v1           diff                     -              ok
io.containerd.grpc.v1           events                   -              ok
io.containerd.grpc.v1           healthcheck              -              ok
io.containerd.grpc.v1           images                   -              ok
io.containerd.grpc.v1           leases                   -              ok
io.containerd.grpc.v1           namespaces               -              ok
io.containerd.internal.v1       opt                      -              ok
io.containerd.grpc.v1           snapshots                -              ok
io.containerd.grpc.v1           tasks                    -              ok
io.containerd.grpc.v1           version                  -              ok
io.containerd.grpc.v1           cri                      linux/amd64    ok

私有镜像仓库相关的配置在cri插件中,文档Configure Image Registry中包含了镜像仓库的配置。 关于私有仓库和认证信息配置示例如下,修改/etc/containerd/config.toml:

...
[plugins]
...
[plugins."io.containerd.grpc.v1.cri"]
...
[plugins."io.containerd.grpc.v1.cri".registry]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://registry-1.docker.io"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."harbor.my.org"]
endpoint = ["https://harbor.my.org"]
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.my.org".tls]
insecure_skip_verify = true
[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.my.org".auth]
username = "username"
password = "passwd"
# auth = "base64(username:password)"
...

配置完成后重启containerd,就可以使用crictl pull配置的私有仓库的镜像了:

crictl pull harbor.my.org/library/nginx:1.1

reference

https://time.geekbang.org/column/article/71499?utm_campaign=guanwang&utm_source=baidu-ad&utm_medium=ppzq-pc&utm_content=title&utm_term=baidu-ad-ppzq-title
https://blog.frognew.com/2021/04/relearning-container-02.html
https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md

[

](https://time.geekbang.org/column/article/71499?utm_campaign=guanwang&utm_source=baidu-ad&utm_medium=ppzq-pc&utm_content=title&utm_term=baidu-ad-ppzq-title)

CRI shim:kubelet怎么与容器运行时交互相关推荐

  1. 【容器运行时】一文理解 OCI、runc、containerd、docker、shim进程、cri、kubelet 之间的关系

    参考 docker,containerd,runc,docker-shim 之间的关系 Containerd shim 进程 PPID 之谜 内核大神教你从 Linux 进程的角度看 Docker R ...

  2. 课时 28:理解容器运行时接口 CRI(知谨)

    CRI 是 Kubernetes 体系中跟容器打交道的一个非常重要的部分.本文将主要分享以下三方面的内容: CRI 介绍 CRI 实现 相关工具 CRI 介绍 在 CRI 出现之前(也就是 Kuber ...

  3. 从零开始入门 K8s | 理解容器运行时接口 CRI

    作者 | 知谨 阿里云工程师 本文整理自<CNCF x Alibaba 云原生技术公开课>第 28 讲,点击直达课程页面. 关注"阿里巴巴云原生"公众号,回复关键词** ...

  4. 什么是Kubernetes的CRI - 容器运行时接口

    我们都知道Kubernetes不会直接和容器打交道,Kubernetes的使用者能接触到的概念只有pod,而pod里包含了多个容器.当我们在Kubernetes里用kubectl执行各种命令时,Kub ...

  5. 课时 30:理解 RuntimeClass 与使用多容器运行时(贾之光)

    本文将主要分享以下三方面的内容: RuntimeClass 需求来源 RuntimeClass 功能介绍 多容器运行时示例 RuntimeClass 需求来源 容器运行时的演进过程 我们首先了解一下容 ...

  6. 1.Containerd容器运行时初识与尝试

    0x00 前言简述 1.基础介绍 2.专业术语 3.架构简述 0x01 安装配置 1.Ubuntu安装Containerd.io流程 0x02 简单使用 1.镜像拉取与运行 2.创建和使用网络 3.与 ...

  7. 3.Containerd容器运行时的配置浅析与知识扩充实践

    公众号关注「WeiyiGeek」 设为「特别关注」,每天带你玩转网络安全运维.应用开发.物联网IOT学习! 本章目录: 0x00 Containerd 容器运行时配置指南 如何配置 Container ...

  8. 云原生钻石课程 | 第1课:容器运行时技术深度剖析

    点击上方"程序猿技术大咖",关注并选择"设为星标" 回复"加群"获取入群讨论资格! 本篇文章来自<华为云云原生王者之路训练营>钻 ...

  9. 部署一个 Containerd 容器运行时的 Kubernetes 集群

    前面我们介绍了 containerd 的基本使用,也了解了如何将现有 docker 容器运行时的 Kubernetes 集群切换成 containerd,接下来我们使用 kubeadm 从头搭建一个使 ...

  10. 关于容器和容器运行时的那些事

    转载本文需注明出处:微信公众号EAWorld,违者必究. 前言: 容器,容器编排,微服务,云原生,这些无疑都是当下软件开发领域里面最热门的术语.容器技术的出现并迅速的广泛应用于软件开发的各个领域里,主 ...

最新文章

  1. android stadio svn 使用技巧
  2. TypeScript学习笔记3:运算符
  3. 《Web前端工程师修炼之道(原书第4版)》——我该从哪里开始呢
  4. python3.6.3安装过程_python3.6.3安装图文教程 TensorFlow安装配置方法
  5. (扫盲)RPC远程过程调用
  6. yii model层操作总结
  7. matlab 信息融合,MSDF,matlab,多传感器信息融合
  8. 如果一切需要重学,2014年应该学哪些技术?
  9. 【网络流24题】餐巾计划问题(费用流)
  10. 关于在EF中通用方法
  11. 关于SWAT模型的一些原理(二)
  12. python做语音识别
  13. 在html创建色块,浅谈网页制作中色块使用
  14. 【论文排版术】学习笔记1
  15. 项目实训--Unity多人游戏开发(十六、草丛隐身与道具隐身)
  16. edg击败we视频_2017LPL春季赛4月8日WE VS EDG视频:EDG 2:0 WE获胜
  17. wps通过vb宏来查看文档中使用的所有字体
  18. spring-session(一)揭秘
  19. 页面跳转传参,A 页面跳转到B页面,把A页面获取的值传到B页面
  20. Selenium教程(4)操作选择框

热门文章

  1. HTTP TFP状态解释
  2. visio的替代者yEd Graph Editor
  3. [网络规划] 拓扑图绘图工具yED Graph Editor使用(持续更新)
  4. 乘风领航、耀世创新——DEFI平台Lizard打造数字金融新世界
  5. ElasticSearch:简单介绍以及使用Docker部署ElasticSearch 和 Kibana
  6. CTF-8021-题目一
  7. ArcGis基础—shapefile矢量文件与lyr图层文件之间有何区别?
  8. 百度表格识别——原理解读
  9. stm32F407控制器在驱动电机等执行机构时,ADS1256采集模块出现死机现象,问题待解决
  10. specular高光贴图