从零开始写 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,!gccgo

package 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:  128959393
    fmt.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种namespace
	char *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系统调用,进入对应namespace
		if (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 []string
       for _, 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) {
	// 根据传进来的容器名获取对应的PID
	pid, 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.Stdin
	cmd.Stdout = os.Stdout
	cmd.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.Info
	if 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.Stdin
    cmd.Stdout = os.Stdout
    cmd.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  COMMAND
    1 root      0:00 top
    6 root      0:00 sh
    7 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}

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

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

相关文章

在线音乐网站的设计与实现

在线音乐网站的设计与实现 摘 要 在社会和互联网的快速发展中&#xff0c;音乐在人们生活中也产生着很大的作用。音乐可以使我们紧张的神经得到放松&#xff0c;有助于开启我们的智慧&#xff0c;可以辅助治疗&#xff0c;达到药物无法达到的效果&#xff0c;所以利用现代科学…

优秀Burp插件 提取JS、HTML中URL插件

Burp Js Url Finder 攻防演练过程中&#xff0c;我们通常会用浏览器访问一些资产&#xff0c;但很多接口/敏感信息隐匿在html、JS文件中&#xff0c;通过该Burp插件我们可以&#xff1a; 1、发现通过某接口可以进行未授权/越权获取到所有的账号密码 2、发现通过某接口可以枚举用…

STM32的GPIO端口的八种模式解析

目录 STM32的GPIO端口的八种模式解析 一、上拉输入模式 二、下拉输入模式 三、浮空输入模式 四、模拟输入模式 五、推挽输出模式 六、开漏输出模式 七、复用推挽输出模式 八、复用开漏输出模式 STM32的GPIO端口的八种模式解析 在学习STM32的过程中&#xff0c;GPIO端口…

【YUV】YUV图像全面详解(一)——格式详解

文章目录 一、前言二、YUV 介绍三、YUV 优点四、YUV 采样格式五、YUV 存储格式六、具体分类详解 一、前言 视频采集芯片输出的码流一般都是 YUV 格式数据流&#xff0c;后续视频处理也是对 YUV 数据流进行编码和解析。所以&#xff0c;了解 YUV 数据流对做视频领域的人而言&am…

【web开发网页制作】html+css家乡长沙旅游网页制作(4页面附源码)

家乡长沙网页制作 涉及知识写在前面一、网页主题二、网页效果Page1、主页Page2、历史长沙Page3、著名人物Page4、留言区 三、网页架构与技术3.1 脑海构思3.2 整体布局3.3 技术说明书 四、网页源码HtmlCSS 五、源码获取5.1 获取方式 作者寄语 涉及知识 家乡长沙网页制作&#x…

学习Rust的第5天:控制流

Control flow, as the name suggests controls the flow of the program, based on a condition. 控制流&#xff0c;顾名思义&#xff0c;根据条件控制程序的流。 If expression If表达式 An if expression is used when you want to execute a block of code if a condition …

如何试用 Ollama 运行本地模型 Mac M2

首先下载 Ollama https://github.com/ollama/ollama/tree/main安装完成之后&#xff0c;启动 ollma 对应的模型&#xff0c;这里用的是 qwen:7b ollama run qwen:7b命令与模型直接交互 我的机器配置是M2 Pro/ 32G&#xff0c;运行 7b 模型毫无压力&#xff0c;而且推理时是用…

C语言案例——输出以下图案(两个对称的星型三角形)

目录 图片代码 图片 代码 #include<stdio.h> int main() {int i,j,k;//先输出上半部图案for(i0;i<3;i){for(j0;j<2-i;j)printf(" ");for(k0;k<2*i;k)printf("*");printf("\n");}//再输出下半部分图案for(i0;i<2;i){for(j0;j&…

《R语言与农业数据统计分析及建模》学习——R基础包的函数

1、R的基础包 基础包是R语言的核心组成部分&#xff0c;构建了R语言的基本功能框架。是R语言默认的安装包&#xff0c;不需要额外安装&#xff0c;使用时无需加载。 2、常用函数及其功能 &#xff08;1&#xff09;数据处理函数&#xff1a;unique()、sort()、subset() # 获…

LRTimelapse for Mac:专业延时摄影视频制作利器

LRTimelapse for Mac是一款专为Mac用户设计的延时摄影视频制作软件&#xff0c;它以其出色的性能和丰富的功能&#xff0c;成为摄影爱好者和专业摄影师的得力助手。 LRTimelapse for Mac v6.5.4中文激活版下载 这款软件提供了直观易用的界面&#xff0c;用户可以轻松上手&#…

OpenHarmony、HarmonyOS和Harmony NEXT 《我们不一样》

1. OpenHarmony 定义与地位&#xff1a;OpenHarmony是鸿蒙系统的底层内核系统&#xff0c;集成了Linux内核和LiteOS&#xff0c;为各种设备提供统一的操作系统解决方案。 开源与商用&#xff1a;OpenHarmony是一个开源项目&#xff0c;允许开发者自由访问和使用其源代码&#…

# Nacos 服务发现-Spring Cloud Alibaba 综合架构实战(五) -实现 gateway 网关。

Nacos 服务发现-Spring Cloud Alibaba 综合架构实战&#xff08;五&#xff09; -实现 gateway 网关。 1、什么是网关&#xff1f; 原来的单体架构&#xff0c;所有的服务都是本地的&#xff0c;UI 可以直接调用&#xff0c;现在按功能拆分成独立的服务&#xff0c;跑在独立的…

Kafka、RabbitMQ、Pulsar、RocketMQ基本原理和选型

Kafka、RabbitMQ、Pulsar、RocketMQ基本原理和选型 1. 消息队列1.1 消息队列使用场景1.2. 消息队列模式1.2.1 点对点模式&#xff0c;不可重复消费1.2.2 发布/订阅模式 2. 选型参考2.1. Kafka2.1.1 基本术语2.1.2. 系统框架2.1.3. Consumer Group2.1.4. 存储结构2.1.5. Rebalan…

【深度学习】执行wandb sync同步命令报错wandb: Network error (SSLError), entering retry loop

执行wandb sync同步命令报错wandb: Network error (SSLError), entering retry loop 在代码中设置wandb offline的命令 os.environ["WANDB_API_KEY"] "API keys" os.environ["WANDB_MODE"] "offline"日志文件生成后&#xff0c;使…

十大排序——7.希尔排序

下面我们来看一下希尔排序 目录 1.介绍 2.代码实现 3.总结与思考 1.介绍 希尔排序是插入排序的一种优化&#xff0c;可以理解为是一种分组的插入排序。 希尔排序的要点&#xff1a; 简单来说&#xff0c;就是分组实现插入&#xff0c;每组元素的间隙称为gap&#xff0c;…

【Git】常用命令速查

目录 一、创建版本 二、修改和提交 三、查看提交历史 四、撤销 五、分支与标签 六、合并与衍合 七、远程操作 一、创建版本 命令简要说明注意事项git clone <url>克隆远程版本库 二、修改和提交 命令简要说明注意事项 三、查看提交历史 命令简要说明注意事项 …

论文解读:(MoCo)Momentum Contrast for Unsupervised Visual Representation Learning

文章汇总 参数的更新&#xff0c;指encoder q的参数&#xff0c;为encoder k&#xff0c;sampling&#xff0c;monentum encoder 的参数。 值得注意的是对于(b)、(c)这里反向传播只更新&#xff0c;的更新只依赖于。 对比学习如同查字典 考虑一个编码查询和一组编码样本是字典…

负载均衡集群——LVS

目录 1.LVS简介 2.LVS体系结构 3.LVS相关术语 4. LVS工作模式 5. LVS调度算法 6.LVS集群介绍 6.1 LVS-DR模式 6.2 LVS – NAT 模式 6.3 LVS – TUN 模式 7.LVS 集群构建 7.1 LVS/NAT 模式配置 实验操作步骤 步骤 1 Nginx1 和 Nginx2 配置 步骤 2 安装和配置 LVS …

Visual Studio 2019 社区版下载

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

ISP图像处理pipeline简介1

ISP 是什么&#xff1f; ISP (Image Signal Processor)&#xff0c;图像信号处理器&#xff0c;是用于摄影和视频处理的一种专用芯片。它是用来干什么的呢&#xff1f;简单说就是用来将图像传感器&#xff08;CCD, CMOS&#xff09;信号转化成可视的信号的功能&#xff0c;这里…