从零开始写 Docker(十一)---实现 mydocker exec 进入容器内部

mydocker-exec.png

本文为从零开始写 Docker 系列第十一篇,实现类似 docker exec 的功能,使得我们能够进入到指定容器内部。


完整代码见:https://github.com/lixd/mydocker
欢迎 Star

推荐阅读以下文章对 docker 基本实现有一个大致认识:

  • 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
  • 基于 namespace 的视图隔离:探索 Linux Namespace:Docker 隔离的神奇背后
  • 基于 cgroups 的资源限制
    • 初探 Linux Cgroups:资源控制的奇妙世界
    • 深入剖析 Linux Cgroups 子系统:资源精细管理
    • Docker 与 Linux Cgroups:资源隔离的魔法之旅
  • 基于 overlayfs 的文件系统:Docker 魔法解密:探索 UnionFS 与 OverlayFS
  • 基于 veth pair、bridge、iptables 等等技术的 Docker 网络:揭秘 Docker 网络:手动实现 Docker 桥接网络

开发环境如下:

root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04
Codename:	focal
root@mydocker:~# uname -r
5.4.0-74-generic

注意:需要使用 root 用户

1. 概述

上一篇已经实现了mydocker logs 命令,可以查看容器日志了。本篇主要实现 mydocker exec,让我们可以直接进入到容器内部,查看容器内部的文件、调试应用程序、执行命令等等。

下面这篇文章分析了 Docker 是如何使用 Linux Namespace 来实现视图隔离的,那么 mydocker exec 也是需要在 Namespace 上做文章。

[探索 Linux Namespace:Docker 隔离的神奇背后]

2. 核心原理

docker exec 实则是将当前进程添加到指定容器对应的 namespace 中,从而可以看到容器中的进程信息、网络信息等。

因此我们的 mydocker exec 具体实现包括两部分:

  • 根据容器 ID 找到对应 PID,然后找到 Namespace
  • 将当前进程切换到对应 Namespace

setns

将进程加入到对应的 Namespace 很简单,Linux提供了 setns 系统调用给我们使用。

setns 是一个系统调用,可以根据提供的 PID 再次进入到指定的 Namespace 中。它需要先打开/proc/[pid/ns/文件夹下对应的文件,然后使当前进程进入到指定的 Namespace 中。

但是用 Go 来实现则存在一个致命问题:setns 调用需要单线程上下文,而 GoRuntime 是多线程的

准确的说是 MountNamespace。

Linux 的 Namespace 是一种资源隔离机制,它允许将一组进程的视图隔离到系统的不同部分,比如 PID Namespace、Network Namespace 等。

setns 系统调用允许进程加入(或重新进入)到指定的 Namespace 中。由于 Namespace 涉及到整个进程的资源隔离,因此需要在进程的上下文中执行,以确保进程及其所有线程都在相同的 Namespace 中

Go Runtime 是多线程的,这意味着 Go 程序通常会有多个线程在同时运行。这种多线程模型与 setns 调用所需的单线程上下文不兼容。

Goroutine 会随机在底层 OS 线程之间切换,而不是固定在某个线程,因此在 Go 中执行 setns 不能准确的知道是操作到哪个线程了,结果是不确定的,因此需要特殊处理。

这个问题对 Go 本身来说没有太好的解决办法,#14163 是 Github 上对一些解决方案的讨论,不过最终还是被拒绝了。

不过好消息是 C 语言可以通过 gcc 的 扩展 attribute((constructor)) 来实现程序启动前执行特定代码,因此 Go 就可以通过 cgo 嵌入 这样的一段 C 代码来完成 runtime 启动前执行特定的 C 代码。

runC 中的 nsenter 也是借助 cgo 实现的。

具体代码如下:

//go:build linux && !gccgo
// +build linux,!gccgopackage nsenter/*
#cgo CFLAGS: -Wall
extern void nsexec();
void __attribute__((constructor)) init(void) {// something
}
*/
import "C"

这段代码就会在 Go Runtime 启动前执行这里定义的 init() 函数,我们只需要把 setns 的调用放在这个 init 方法中即可。

cgo

cgo 是一个很炫酷的功能,允许 Go 程序去调用 C 的函数与标准库。你只需要以一种特殊的方式在 Go 的源代码里写出需要调用的 C 的代码,cgo 就可以把你的 C 源码文件和 Go 文件整合成一个包。

下面举一个最简单的例子,在这个例子中有两个函数一Random 和 Seed,在
它们里面调用了 C 的 random 和 srandom 函数。

package main/*
#include <stdlib.h>
*/
import "C"
import ("fmt"
)func main() {Seed(123)// Output:Random:  128959393fmt.Println("Random: ", Random())
}// Seed 初始化随机数产生器
func Seed(i int) {C.srandom(C.uint(i))
}// Random 产生一个随机数
func Random() int {return int(C.random())
}

这段代码导入了一个叫 C 的包,但是你会发现在 Go 标准库里面并没有这个包,那是因为这根本就不是一个真正的包,而只是 Cgo 创建的一个特殊命名空间,用来与 C 的命名空间交流。

这两个函数都分别调用了 C 里面的 random 和 uint 函数,然后对它们进行了类型转换。这就实现了 Go 代码里面调用 C 的功能。

3. 实现

首先,自然是需要在 C 中实现 setns 核心逻辑,根据 PID 实现 Namespace 切换。

其次,由于使用 C 的 constructor 方式,以 init 形式执行的 setns 这段代码,意味这,执行任何 mydocker 命令的时候这段代码都会执行,因此需要限制,只有 mydocker exec 时才切换 Namespace。

大致流程如下图所示:

mydocker-exec-process.png

setns

setns 的 C 实现具体如下:

package nsenter/*
#define _GNU_SOURCE
#include <unistd.h>
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>__attribute__((constructor)) void enter_namespace(void) {// 这里的代码会在Go运行时启动前执行,它会在单线程的C上下文中运行char *mydocker_pid;mydocker_pid = getenv("mydocker_pid");if (mydocker_pid) {fprintf(stdout, "got mydocker_pid=%s\n", mydocker_pid);} else {fprintf(stdout, "missing mydocker_pid env skip nsenter");// 如果没有指定PID就不需要继续执行,直接退出return;}char *mydocker_cmd;mydocker_cmd = getenv("mydocker_cmd");if (mydocker_cmd) {fprintf(stdout, "got mydocker_cmd=%s\n", mydocker_cmd);} else {fprintf(stdout, "missing mydocker_cmd env skip nsenter");// 如果没有指定命令也是直接退出return;}int i;char nspath[1024];// 需要进入的5种namespacechar *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };for (i=0; i<5; i++) {// 拼接对应路径,类似于/proc/pid/ns/ipc这样sprintf(nspath, "/proc/%s/ns/%s", mydocker_pid, namespaces[i]);int fd = open(nspath, O_RDONLY);// 执行setns系统调用,进入对应namespaceif (setns(fd, 0) == -1) {fprintf(stderr, "setns on %s namespace failed: %s\n", namespaces[i], strerror(errno));} else {fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]);}close(fd);}// 在进入的Namespace中执行指定命令,然后退出int res = system(mydocker_cmd);exit(0);return;
}
*/
import "C"

为什么要这么写,前面 setns 部分已经解释了,这里简单提一下,这里主要使用了构造函数,然后导入了 C 模块,一旦这个包被引用,它就会在所有 Go Runtime 启动之前执行,这样就避免了 Go 多线程导致的无法执行 setns 的问题。

即:这段程序执行完毕后,Go 程序才会执行。

同时,为了避免执行其他命令的时候这段 setns 的逻辑影响到其他功能,因此,在这段 C 代码前面一开始的位置就添加了环境变量检测,没有对应的环境变量时,就直接退出。

    mydocker_cmd = getenv("mydocker_cmd");if (mydocker_cmd) {// fprintf(stdout, "got mydocker_cmd=%s\n", mydocker_cmd);} else {// fprintf(stdout, "missing mydocker_cmd env skip nsenter");// 如果没有指定命令也是直接退出return;}

对于不使用 exec 功能的 Go 代码,只要不设置对应的环境变量,这段 C 代码就不会运行,这样就不会影响原来的逻辑。

注意:只有在你的 Go 应用程序中注册、导入了这个包,才会调用这个构造函数
就像这样:

import (_ "mydocker/nsenter"
)

使用 cgo 我们无法直接获取传递给程序的参数,可用的做法是,通过 go exec 创建一个自身运行进程,然后通过传递环境变量的方式,传递给 cgo 参数值。

体现在 runc 中就是 runc create → runc init ,runc 中有很多细节,他通过环境变量传递 netlink fd,然后进行通信。

execCommand

在 main_command.go 中增加一个 execCommand,具体如下:

var execCommand = cli.Command{Name:  "exec",Usage: "exec a command into container",Action: func(context *cli.Context) error {// 如果环境变量存在,说明C代码已经运行过了,即setns系统调用已经执行了,这里就直接返回,避免重复执行if os.Getenv(EnvExecPid) != "" {log.Infof("pid callback pid %v", os.Getgid())return nil}// 格式:mydocker exec 容器名字 命令,因此至少会有两个参数if len(context.Args()) < 2 {return fmt.Errorf("missing container name or command")}containerName := context.Args().Get(0)// 将除了容器名之外的参数作为命令部分var commandArray []stringfor _, arg := range context.Args().Tail() {commandArray = append(commandArray, arg)}ExecContainer(containerName, commandArray)return nil},
}

然后添加到 main 函数中去:

func main(){// 省略其他内容app.Commands = []cli.Command{initCommand,runCommand,commitCommand,listCommand,logCommand,execCommand,}
}

这里主要是将获取到的容器名和需要的命令处理完成后,交给下面的函数,下面看一下 ExecContainer 的实现。

ExecContainer

exec 命令核心实现就是 ExecContainer 方法。

// nsenter里的C代码里已经出现mydocker_pid和mydocker_cmd这两个Key,主要是为了控制是否执行C代码里面的setns.
const (EnvExecPid = "mydocker_pid"EnvExecCmd = "mydocker_cmd"
)func ExecContainer(containerId string, comArray []string) {// 根据传进来的容器名获取对应的PIDpid, err := getPidByContainerId(containerId)if err != nil {log.Errorf("Exec container getContainerPidByName %s error %v", containerId, err)return}cmd := exec.Command("/proc/self/exe", "exec")cmd.Stdin = os.Stdincmd.Stdout = os.Stdoutcmd.Stderr = os.Stderr// 把命令拼接成字符串,便于传递cmdStr := strings.Join(comArray, " ")log.Infof("container pid:%s command:%s", pid, cmdStr)_ = os.Setenv(EnvExecPid, pid)_ = os.Setenv(EnvExecCmd, cmdStr)if err = cmd.Run(); err != nil {log.Errorf("Exec container %s error %v", containerId, err)}
}

首先是通过ContainerId 找到进程 PID,具体实现如下:

因为之前已经记录了容器信息,因此这里直接读取对应文件就可以找到了。

func getPidByContainerId(containerId string) (string, error) {// 拼接出记录容器信息的文件路径dirPath := fmt.Sprintf(container.InfoLocFormat, containerId)configFilePath := path.Join(dirPath, container.ConfigName)// 读取内容并解析contentBytes, err := os.ReadFile(configFilePath)if err != nil {return "", err}var containerInfo container.Infoif err = json.Unmarshal(contentBytes, &containerInfo); err != nil {return "", err}return containerInfo.Pid, nil
}

然后则是通过 exec 简单 fork 出了一个进程,并把这个进程的标准输入输出都绑定到宿主机的 stdin、stdout、stderr 上。

    cmd := exec.Command("/proc/self/exe", "exec")cmd.Stdin = os.Stdincmd.Stdout = os.Stdoutcmd.Stderr = os.Stderr// 把命令拼接成字符串,便于传递cmdStr := strings.Join(comArray, " ")

最关键的是设置环境变量的这两句:

    _ = os.Setenv(EnvExecPid, pid)_ = os.Setenv(EnvExecCmd, cmdStr)

设置了这两个环境变量,于是在新的进程里,前面的 nsenter 部分的 C 代码就会执行到 setns 部分逻辑,从而将进程加入到对应的 Namespace 中进行操作了。

C 代码中根据环境变量拿到 PID 和要执行的命令,首先根据 PID 找到对应 Namespace,然后将当前进程加入到该 Namespace 然后执行具体命令。

这也是 mydocker exec 命令要实现的效果。

而执行其他命令时,由于没有指定这两个环境变量,因此那段 C 代码不会执行到 setns 这里。

这时应该就可以明白前面一段 C 代码的意义了 。

mydocker_pid = getenv("mydocker_pid");
if (mydocker_pid) {// fprintf(stdout, "got mydocker_pid=%s\n", mydocker_pid);
} else {// 如果没有指定PID就不需要继续执行,直接退出return;
}

执行 exec 命令就会设置这两个环境变量,那么问题来了,执行 exec 之后环境变量就已经存在了,C 代码也运行了,那么再次执行 exec 命令岂不是会重复执行 setns 系统调用?

为了避免重复执行,在 execCommand 中加了如下判断:如果对应环境变量已经存在了就直接返回,啥也不执行。

因为环境变量存在就代表着 C 代码执行了,即setns系统调用执行了,也就是当前已经在这个 namespace 里了。

var execCommand = cli.Command{Name:  "exec",Usage: "exec a command into container",Action: func(context *cli.Context) error {// 如果环境变量存在,说明C代码已经运行过了,即setns系统调用已经执行了,这里就直接返回,避免重复执行if os.Getenv(EnvExecPid) != "" {log.Infof("pid callback pid %v", os.Getgid())return nil}// 省略其他内容},
}

至此, mydocker exec 命令实现就完成了,核心就是 setns 系统调用

4. 测试

首先编译最新的 mydocker,然后启动一个后台容器,这里直接把 name 指定为 test,方便观察。

这里要运行交互式命令,例如 top,保证容器能在后台一直运行。

root@mydocker:~/feat-exec/mydocker# go build  .
root@mydocker:~/feat-exec/mydocker# ./mydocker run -d -name test top
{"level":"info","msg":"createTty false","time":"2024-01-30T09:48:33+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-01-30T09:48:33+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-30T09:48:33+08:00"}
{"level":"error","msg":"mkdir dir /root/merged error. mkdir /root/merged: file exists","time":"2024-01-30T09:48:33+08:00"}
{"level":"error","msg":"mkdir dir /root/upper error. mkdir /root/upper: file exists","time":"2024-01-30T09:48:33+08:00"}
{"level":"error","msg":"mkdir dir /root/work error. mkdir /root/work: file exists","time":"2024-01-30T09:48:33+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-30T09:48:33+08:00"}
{"level":"info","msg":"command all is top","time":"2024-01-30T09:48:33+08:00"}

然后查看容器 ID

root@mydocker:~/feat-exec/mydocker# ./mydocker ps
ID           NAME        PID         STATUS      COMMAND     CREATED
2147624410   test        180358      running     top         2024-01-30 09:48:33

然后执行 exec 命令并指定 Id 为 2147624410 进入该容器

root@mydocker:~/feat-exec/mydocker# ./mydocker exec 2147624410 sh
{"level":"info","msg":"container pid:180358 command:sh","time":"2024-01-30T09:48:42+08:00"}
got mydocker_pid=180358
got mydocker_cmd=sh
setns on ipc namespace succeeded
setns on uts namespace succeeded
setns on net namespace succeeded
setns on pid namespace succeeded
setns on mnt namespace succeeded
/ # ps -e
PID   USER     TIME  COMMAND1 root      0:00 top6 root      0:00 sh7 root      0:00 ps -e

在容器内部执行 ps -ef 可以发现 PID 为 1 的进程为 top,这也就意味着已经成功进入到了容器内部。

说明我们的 mydocker exec 命令实现是成功了。

5. 小结

本篇主要实现 mydocker exec 命令,和 docker 实现基本类似,通过 setns 系统调用将当前进程加入到容器所在 Namespace 即可。

比较关键的一点在于,Go Runtime 是多线程的,和 setns 冲突,因此需要使用 Cgo 以constructor 方式在 Go Runtime 启动之前执行 setns 调用。

最后就是根据是否存在指定环境变量来防止重复执行。


**【从零开始写 Docker 系列】**持续更新中,搜索公众号【探索云原生】订阅,文章。



完整代码见:https://github.com/lixd/mydocker
欢迎关注~

相关代码见 feat-exec 分支,测试脚本如下:

需要提前在 /root 目录准备好 busybox.tar 文件,具体见第四篇第二节。

# 克隆代码
git clone -b feat-exec https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 
./mydocker run -d -name c1 top
# 查看容器 Id
./mydocker ps
# 根据 Id 执行 exec 进入对应容器
./mydocker exec ${containerId}

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

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

相关文章

STM32 F103 C8T6开发笔记14:与HLK-LD303-24G测距雷达通信

今日尝试配通STM32 F103 ZET6与HLK-LD303-24G测距雷达的串口通信解码 文章提供测试代码...... 目录 HLK-LD303-24G测距雷达外观&#xff1a; 线路连接准备&#xff1a; 定时器与串口配置准备&#xff1a; 定时器2的初始化&#xff1a; 串口1、2初始化&#xff1a; 串口1、2自定…

C++从入门到精通——类和对象(下篇)

1. 再谈构造函数 1.1 构造函数体赋值 在创建对象时&#xff0c;编译器通过调用构造函数&#xff0c;给对象中各个成员变量一个合适的初始值。 class Date { public:Date(int year, int month, int day){_year year;_month month;_day day;} private:int _year;int _mont…

Web3.0与AI的交融:开启智能互联网新时代

目前有140 多个 Web3 AI 概念项目&#xff0c;覆盖了基础设施、数据、预测市场、计算与算力、教育、DeFi & 跨链、安全、NFT & 游戏 & 元宇宙、搜索引擎、社交 & 创作者经济、AI 聊天机器人、DID & 消息传递、治理、医疗、交易机器人等诸多方向。持续关注…

C++笔记:类和对象

类和对象 认识类和对象 先来回忆一下C语言中的类型和变量&#xff0c;类型就像是定义了数据的规则&#xff0c;而变量则是根据这些规则来实际存储数据的容器。类是我们自己定义的一种数据类型&#xff0c;而对象则是这种数据类型的一个具体实例。类就可以理解为类型&#xff0c…

怎么用手机远程控制电脑 远程控制怎么用

怎么用手机远程控制电脑&#xff1a;远程控制怎么用 在这个科技日新月异的时代&#xff0c;远程控制电脑已经成为了很多人的需求。有时&#xff0c;我们可能在外出时突然需要访问家中的电脑&#xff0c;或者在工作中需要远程操控办公室的电脑。这时&#xff0c;如果能用手机远…

JavaEE:JVM

基本介绍 JVM&#xff1a;Java虚拟机&#xff0c;用于解释执行Java字节码 jdk&#xff1a;Java开发工具包 jre&#xff1a;Java运行时环境 C语言将写入的程序直接编译成二进制的机器语言&#xff0c;而java不想重新编译&#xff0c;希望能直接执行。Java先通过javac把.java…

Visual Studio 2019 社区版下载

一、网址 https://learn.microsoft.com/zh-cn/visualstudio/releases/2019/release-notes#start-window 二、选择这个即可

【Java EE】关于Spring MVC 响应

文章目录 &#x1f38d;返回静态页面&#x1f332;RestController 与 Controller 的关联和区别&#x1f334;返回数据 ResponseBody&#x1f38b;返回HTML代码片段&#x1f343;返回JSON&#x1f340;设置状态码&#x1f384;设置Header&#x1f338;设置Content-Type&#x1f…

【单例模式】饿汉式、懒汉式、静态内部类--简单例子

单例模式是⼀个单例类在任何情况下都只存在⼀个实例&#xff0c;构造⽅法必须是私有的、由⾃⼰创建⼀个静态变量存储实例&#xff0c;对外提供⼀个静态公有⽅法获取实例。 目录 一、单例模式 饿汉式 静态内部类 懒汉式 反射可以破坏单例 道高一尺魔高一丈 枚举 一、单例…

自学Java的第二十四次笔记

一,方法重载 1.基本介绍 java 中允许同一个类中&#xff0c;多个同名方法的存在&#xff0c;但要求 形参列表不一致&#xff01; 比如&#xff1a; System.out.println(); out 是 PrintStream 类型 2.重载的好处 1) 减轻了起名的麻烦 2) 减轻了记名的麻烦 3.快速入门案…

【中间件】ElasticSearch简介和基本操作

一、简介 Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎&#xff0c;支持各种数据类型&#xff0c;包括文本、数字、地理、结构化、非结构化 ,可以让你存储所有类型的数据&#xff0c;能够解决不断涌现出的各种用例。其构成如下&#xff1a; 说明&#xff1…

Python基于深度学习的车辆特征分析系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

ARMv8-A架构下的外部debug模型之外部调试事件(external debug events)概述

外部调试器与处理器之间的握手与external debug events 一&#xff0c;External Debug的使能二&#xff0c;外部调试器和CPU之间的握手三&#xff0c;外部调试事件 External debug events1. External debug request event2. Halt instruction debug event3. Halting step debug…

天池酒瓶瑕疵检测数据集分析及完整baseline

以下内容为还没思路的小伙伴牵个头提供一个demo,大佬勿喷,线上成绩0.7,留空间给小伙伴们发挥自己的力量 ps:markdown不怎么熟悉,代码中如有明显缩进问题,自行斟酌改正,编辑好几次都改不过来,请原谅.... 数据分析瑕疵大类: 瓶盖瑕疵、标贴瑕疵、喷码瑕疵、瓶身瑕疵、酒液瑕疵瑕…

hadoop编程之工资序列化排序

数据集展示 7369SMITHCLERK79021980/12/17800207499ALLENSALESMAN76981981/2/201600300307521WARDSALESMAN76981981/2/221250500307566JONESMANAGER78391981/4/22975207654MARTINSALESMAN76981981/9/2812501400307698BLAKEMANAGER78391981/5/12850307782CLARKMANAGER78391981/…

Spectral Adversarial MixUp for Few-Shot Unsupervised Domain Adaptation论文速读

文章目录 Spectral Adversarial MixUp for Few-Shot Unsupervised Domain Adaptation摘要方法Domain-Distance-Modulated Spectral Sensitivity (DoDiSS&#xff09;模块Sensitivity-Guided Spectral Adversarial Mixup (SAMix)模块 实验结果 Spectral Adversarial MixUp for F…

Python也可以合并和拆分PDF,批量高效!

PDF是最方便的文档格式&#xff0c;可以在任何设备原样且无损的打开&#xff0c;但因为PDF不可编辑&#xff0c;所以很难去拆分合并。 知乎上也有人问&#xff0c;如何对PDF进行合并和拆分&#xff1f; 看很多回答推荐了各种PDF编辑器或者网站&#xff0c;确实方法比较多。 …

k-means聚类算法的MATLAB实现及可视化

K-means算法是一种无监督学习算法&#xff0c;主要用于数据聚类。其工作原理基于迭代优化&#xff0c;将数据点划分为K个集群&#xff0c;使得每个数据点都属于最近的集群&#xff0c;并且每个集群的中心&#xff08;质心&#xff09;是所有属于该集群的数据点的平均值。以下是…

LabVIEW变速箱自动测试系统

LabVIEW变速箱自动测试系统 在农业生产中&#xff0c;采棉机作为重要的农用机械&#xff0c;其高效稳定的运行对提高采棉效率具有重要意义。然而&#xff0c;传统的采棉机变速箱测试方法存在测试效率低、成本高、对设备可能产生损害等问题。为了解决这些问题&#xff0c;开发了…

深度学习 Lecture 8 决策树

一、决策树模型&#xff08;Decision Tree Model) 椭圆形代表决策节点&#xff08;decison nodes)&#xff0c;矩形节点代表叶节点&#xff08;leaf nodes)&#xff0c;方向上的值代表属性的值&#xff0c; 构建决策树的学习过程&#xff1a; 第一步&#xff1a;决定在根节点…