从源码解析Containerd容器启动流程

从源码解析Containerd容器启动流程

本文从源码的角度分析containerd容器启动流程以及相关功能的实现。
本篇containerd版本为v1.7.9
更多文章访问 https://www.cyisme.top

本文从ctr run命令出发,分析containerd的容器启动流程。

在这里插入图片描述

ctr命令

查看文件cmd/ctr/commands/run/run.go

// cmd/ctr/commands/run/run.go
var Command = cli.Command{
    // 省略其他代码
	Action: func(context *cli.Context) error {
		// 省略其他代码
        // 获取grpc客户端
		client, ctx, cancel, err := commands.NewClient(context)
		if err != nil {
			return err
		}
		defer cancel()
        // 创建容器(基本信息)
		container, err := NewContainer(ctx, client, context)
		if err != nil {
			return err
		}
        // 创建任务
        task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...)
		if err != nil {
			return err
		}
		// 省略其他代码 
        // 用于阻塞进程,等待容器退出
		var statusC <-chan containerd.ExitStatus
		if !detach {
            // 清理容器网络
			defer func() {
				if enableCNI {
					if err := network.Remove(ctx, commands.FullID(ctx, container), ""); err != nil {
						logrus.WithError(err).Error("network review")
					}
				}
				task.Delete(ctx)
			}()
            // 等待容器退出
			if statusC, err = task.Wait(ctx); err != nil {
				return err
			}
		}
		// 创建容器网络
		if enableCNI {
            // nspath /proc/%d/ns/net
			netNsPath, err := getNetNSPath(ctx, task)
			if err != nil {
				return err
			}

			if _, err := network.Setup(ctx, commands.FullID(ctx, container), netNsPath); err != nil {
				return err
			}
		}
        // 启动任务(启动容器)
		if err := task.Start(ctx); err != nil {
			return err
		}
        // 如果是后台(detach)运行,直接返回
		if detach {
            // detach运行的任务,containerd不会主动进行数据清理
			return nil
		}
        // 前台运行时, 判断是否开启交互终端
		if tty {
			if err := tasks.HandleConsoleResize(ctx, task, con); err != nil {
				logrus.WithError(err).Error("console resize")
			}
		} else {
			sigc := commands.ForwardAllSignals(ctx, task)
			defer commands.StopCatch(sigc)
		}
        // 等待容器退出
		status := <-statusC
		code, _, err := status.Result()
		if err != nil {
			return err
		}
        // 非detach模式,会执行清理
        // 清理任务
		if _, err := task.Delete(ctx); err != nil {
			return err
		}
		if code != 0 {
			return cli.NewExitError("", int(code))
		}
		return nil
	},
}

创建容器

containerd中,创建容器实际为创建一个container对象,该对象包含容器的基本信息,如idimagerootfs等。

// cmd/ctr/commands/run/run.go:162
// client, ctx, cancel, err := commands.NewClient(context)
// if err != nil {
//     return err
// }
// defer cancel()
// 创建容器(基本信息)
// container, err := NewContainer(ctx, client, context)
// if err != nil {
//     return err
// }
//
// cmd/ctr/commands/run/run_unix.go:88
func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) {
    // 省略其他代码

	if config {
		cOpts = append(cOpts, containerd.WithContainerLabels(commands.LabelArgs(context.StringSlice("label"))))
		opts = append(opts, oci.WithSpecFromFile(context.String("config")))
	} else {
		// 省略其他代码

		if context.Bool("rootfs") {
			rootfs, err := filepath.Abs(ref)
			if err != nil {
				return nil, err
			}
			opts = append(opts, oci.WithRootFSPath(rootfs))
			cOpts = append(cOpts, containerd.WithContainerLabels(commands.LabelArgs(context.StringSlice("label"))))
		} else {
			// 省略其他代码
            // 解压镜像
			if !unpacked {
				if err := image.Unpack(ctx, snapshotter); err != nil {
					return nil, err
				}
			}
			// 省略其他代码
		}
		// 省略其他代码
        // 特权模式判断
		privileged := context.Bool("privileged")
		privilegedWithoutHostDevices := context.Bool("privileged-without-host-devices")
		if privilegedWithoutHostDevices && !privileged {
			return nil, fmt.Errorf("can't use 'privileged-without-host-devices' without 'privileged' specified")
		}
		if privileged {
			if privilegedWithoutHostDevices {
				opts = append(opts, oci.WithPrivileged)
			} else {
				opts = append(opts, oci.WithPrivileged, oci.WithAllDevicesAllowed, oci.WithHostDevices)
			}
		}
		// 省略其他代码
        // 参数生成
	}
    // 省略其他代码
    // 创建容器
	return client.NewContainer(ctx, id, cOpts...)
}

解压镜像

ctr run命令执行时,强制要求镜像存在。不存在则会退出命令。

镜像存在时,会根据镜像的layer信息,解压镜像到指定目录,生成快照数据。具体流程可以看《Containerd Snapshots功能解析》这篇文章。这里不再赘述。

// image.go:339
func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error {
    // 省略其他代码
}

创建容器

创建容器完成后,此时容器为一条记录,并没有真正调用oci runtime进行创建,也没有真实运行。 具体流程可以看《Containerd Container管理功能解析》这篇文章。 这里不再赘述。

// client.go:280
func (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) {
    // 省略其他代码
}

创建任务

taskcontainerd中真正运行的对象,它包含了容器的所有信息,如rootfsnamespace进程等。

ctr 本地准备阶段

// task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...)
// if err != nil {
//     return err
// }
// cmd/ctr/commands/task/task_unix.go:71
func NewTask(ctx gocontext.Context, client *containerd.Client, container containerd.Container, checkpoint string, con console.Console, nullIO bool, logURI string, ioOpts []cio.Opt, opts ...containerd.NewTaskOpts) (containerd.Task, error) {
    // 获取checkpoint信息
    // checkpoint需要criu支持
	if checkpoint != "" {
		im, err := client.GetImage(ctx, checkpoint)
		if err != nil {
			return nil, err
		}
		opts = append(opts, containerd.WithTaskCheckpoint(im))
	}
    // 获取目标容器信息
	spec, err := container.Spec(ctx)
	if err != nil {
		return nil, err
	}
    // 省略其他代码
    // io创建,用于输出容器日志等终端输出
	var ioCreator cio.Creator
	if con != nil {
		if nullIO {
			return nil, errors.New("tty and null-io cannot be used together")
		}
		ioCreator = cio.NewCreator(append([]cio.Opt{cio.WithStreams(con, con, nil), cio.WithTerminal}, ioOpts...)...)
	}
    // 省略其他代码
    // 创建task
	t, err := container.NewTask(ctx, ioCreator, opts...)
	if err != nil {
		return nil, err
	}
	stdinC.closer = func() {
		t.CloseIO(ctx, containerd.WithStdinCloser)
	}
	return t, nil
}
// container.go:210
func (c *container) NewTask(ctx context.Context, ioCreate cio.Creator, opts ...NewTaskOpts) (_ Task, err error) {
    // 省略其他代码
    // 获取容器信息
	r, err := c.get(ctx)
	if err != nil {
		return nil, err
	}
    // 处理快照信息
	if r.SnapshotKey != "" {
		if r.Snapshotter == "" {
			return nil, fmt.Errorf("unable to resolve rootfs mounts without snapshotter on container: %w", errdefs.ErrInvalidArgument)
		}

		// get the rootfs from the snapshotter and add it to the request
		s, err := c.client.getSnapshotter(ctx, r.Snapshotter)
		if err != nil {
			return nil, err
		}
        // 获取挂载位置
		mounts, err := s.Mounts(ctx, r.SnapshotKey)
		if err != nil {
			return nil, err
		}
		spec, err := c.Spec(ctx)
		if err != nil {
			return nil, err
		}
        // 处理挂载信息
		for _, m := range mounts {
			if spec.Linux != nil && spec.Linux.MountLabel != "" {
				context := label.FormatMountLabel("", spec.Linux.MountLabel)
				if context != "" {
					m.Options = append(m.Options, context)
				}
			}
            // 快照的挂载信息,最终会添加到容器的根文件系统中
            // 根文件系统容器不可更改
			request.Rootfs = append(request.Rootfs, &types.Mount{
				Type:    m.Type,
				Source:  m.Source,
				Target:  m.Target,
				Options: m.Options,
			})
		}
	}
	// 省略其他代码
	t := &task{
        // grpc客户端
		client: c.client,
        // io信息, 用于处理终端数据
		io:     i,
        // 容器id
		id:     c.id,
        // 容器对象
		c:      c,
	}
	// grpc请求containerd, 创建task
	response, err := c.client.TaskService().Create(ctx, request)
	if err != nil {
		return nil, errdefs.FromGRPC(err)
	}
    // shim进程id
	t.pid = response.Pid
	return t, nil
}

containerd grpc阶段

c.client.TaskService().Create(ctx, request)会以grpc方式调用containerd

// services/tasks/local.go:166
func (l *local) Create(ctx context.Context, r *api.CreateTaskRequest, _ ...grpc.CallOption) (*api.CreateTaskResponse, error) {
    // 省略其他代码
    // 获取容器信息
	container, err := l.getContainer(ctx, r.ContainerID)
	if err != nil {
		return nil, errdefs.ToGRPC(err)
	}
	checkpointPath, err := getRestorePath(container.Runtime.Name, r.Options)
	if err != nil {
		return nil, err
	}
	// jump get checkpointPath from checkpoint image
	if checkpointPath == "" && r.Checkpoint != nil {
		// checkpioint相关,需要criu支持,这里省略
	}
	opts := runtime.CreateOpts{
		Spec: container.Spec,
		IO: runtime.IO{
            // 终端信息, 实际为系统中的一个文件
            // 如:/run/containerd/fifo/1096067688/redis6-stdin
			Stdin:    r.Stdin,
			Stdout:   r.Stdout,
			Stderr:   r.Stderr,
			Terminal: r.Terminal,
		},
        // 一些runtime配置
		Checkpoint:     checkpointPath,
		Runtime:        container.Runtime.Name,
		RuntimeOptions: container.Runtime.Options,
		TaskOptions:    r.Options,
		SandboxID:      container.SandboxID,
	}
	// 省略其他代码
    // 获取runtime
	rtime, err := l.getRuntime(container.Runtime.Name)
	if err != nil {
		return nil, err
	}
    // 获取任务信息,实际是获取shim相关信息
    // 这里实际是为了判断任务是否存在
	_, err = rtime.Get(ctx, r.ContainerID)
	if err != nil && !errdefs.IsNotFound(err) {
		return nil, errdefs.ToGRPC(err)
	}
	if err == nil {
		return nil, errdefs.ToGRPC(fmt.Errorf("task %s: %w", r.ContainerID, errdefs.ErrAlreadyExists))
	}
    // 创建任务
	c, err := rtime.Create(ctx, r.ContainerID, opts)
	if err != nil {
		return nil, errdefs.ToGRPC(err)
	}
	labels := map[string]string{"runtime": container.Runtime.Name}
    // 将提供的容器添加到监视器中
	if err := l.monitor.Monitor(c, labels); err != nil {
		return nil, fmt.Errorf("monitor task: %w", err)
	}
    // 在当前返回时,这个pid对应着 runc init进程
    // 后续会随着容器内进程的启动,pid变为对应着容器内的进程
	pid, err := c.PID(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get task pid: %w", err)
	}
	return &api.CreateTaskResponse{
		ContainerID: r.ContainerID,
		Pid:         pid,
	}, nil
}
启动shim进程

任务创建会启动shim进程,shim会与oci runtime交互,完成容器的创建。

shim进程是一个短暂的进程,它的生命周期与容器一致。它的主要作用是与oci runtime交互,完成容器的创建。

可以理解为,shim进程是oci runtime的代理。
shim有v1和v2两个版本,当前containerd版本使用v2。

// 创建任务
// c, err := rtime.Create(ctx, r.ContainerID, opts)
// if err != nil {
//     return nil, errdefs.ToGRPC(err)
// }
// runtime/v2/manager.go:420
func (m *TaskManager) Create(ctx context.Context, taskID string, opts runtime.CreateOpts) (runtime.Task, error) {
    // 启动shim进程
	shim, err := m.manager.Start(ctx, taskID, opts)
	if err != nil {
		return nil, fmt.Errorf("failed to start shim: %w", err)
	}

	// 获取shim客户端
	shimTask, err := newShimTask(shim)
	if err != nil {
		return nil, err
	}
    // 通知对应的oci runtime创建容器
    // 这个函数逻辑比较简单,省略函数解析
	t, err := shimTask.Create(ctx, opts)
	if err != nil {
		// 创建失败会清理shim相关信息
        // 此处省略
		return nil, fmt.Errorf("failed to create shim task: %w", err)
	}

	return t, nil
}
// m.manager.Start(ctx, taskID, opts)
// runtime/v2/manager.go:184
func (m *ShimManager) Start(ctx context.Context, id string, opts runtime.CreateOpts) (_ ShimInstance, retErr error) {
    // 省略其他代码

	if opts.SandboxID != "" {
		// 省略其他代码
        // 如果绑定了sandbox,直接获取shim信息,不再创建新的shim
		shim, err := loadShim(ctx, bundle, func() {})
		if err != nil {
			return nil, fmt.Errorf("failed to load sandbox task %q: %w", opts.SandboxID, err)
		}
        // 添加shim信息
		if err := m.shims.Add(ctx, shim); err != nil {
			return nil, err
		}

		return shim, nil
	}
    // 启动shim进程
	shim, err := m.startShim(ctx, bundle, id, opts)
	if err != nil {
		return nil, err
	}
	defer func() {
		if retErr != nil {
			m.cleanupShim(ctx, shim)
		}
	}()
    // 添加shim信息
	if err := m.shims.Add(ctx, shim); err != nil {
		return nil, fmt.Errorf("failed to add task: %w", err)
	}

	return shim, nil
}

这个阶段完成后,使用runc命令可以看见一个状态为created的容器
在这里插入图片描述

创建网络

task准备好之后, 如果容器需要网络,ctr会调用cni插件,创建容器网络。

// if enableCNI {
//     netNsPath, err := getNetNSPath(ctx, task)
//     
//     if err != nil {
//         return err
//     }
// 
//     if _, err := network.Setup(ctx, commands.FullID(ctx, container), netNsPath); err != nil {
//         return err
//     }
// }
// 这里不赘述,项目地址:
// https://github.com/containerd/go-cni
func (c *libcni) Setup(ctx context.Context, id string, path string, opts ...NamespaceOpts) (*Result, error) {
	if err := c.Status(); err != nil {
		return nil, err
	}
	ns, err := newNamespace(id, path, opts...)
	if err != nil {
		return nil, err
	}
	result, err := c.attachNetworks(ctx, ns)
	if err != nil {
		return nil, err
	}
	return c.createResult(result)
}

启动任务

启动任务本质是启动容器。启动容器就比较简单了,因为前面的工作都已经完成了,这里只需要调用oci runtimestart接口,就可以完成容器的启动。

ctr 本地准备阶段

// if err := task.Start(ctx); err != nil {
//     return err
// }
// task.go:215
func (t *task) Start(ctx context.Context) error {
    // grpc调用containerd
	r, err := t.client.TaskService().Start(ctx, &tasks.StartRequest{
		ContainerID: t.id,
	})
	if err != nil {
		if t.io != nil {
			t.io.Cancel()
			t.io.Close()
		}
		return errdefs.FromGRPC(err)
	}
	t.pid = r.Pid
	return nil
}

containerd grpc阶段

// services/tasks/local.go:258
func (l *local) Start(ctx context.Context, r *api.StartRequest, _ ...grpc.CallOption) (*api.StartResponse, error) {
    // 获取task信息
	t, err := l.getTask(ctx, r.ContainerID)
	if err != nil {
		return nil, err
	}
	p := runtime.Process(t)
	if r.ExecID != "" {
		if p, err = t.Process(ctx, r.ExecID); err != nil {
			return nil, errdefs.ToGRPC(err)
		}
	}
    // 启动
    // start函数最终会调用shim客户端,由shim进程去启动容器
    // 这里函数逻辑比较简单,不对函数展开分析
	if err := p.Start(ctx); err != nil {
		return nil, errdefs.ToGRPC(err)
	}
    // 获取容器状态
	state, err := p.State(ctx)
	if err != nil {
		return nil, errdefs.ToGRPC(err)
	}
	return &api.StartResponse{
		Pid: state.Pid,
	}, nil
}

当这个阶段完成后,可以看见容器的状态变为running。容器启动完成
在这里插入图片描述

总结

  1. taskcontainerd中真正运行的对象,它包含了容器的所有信息,如rootfs、namespace、进程等。创建task时,会启动shim进程。
  2. shim进程是一个短暂的进程,它的生命周期与容器一致。它的主要作用是与oci runtime交互,完成容器的创建。
  3. 容器的网络配置是在task创建之后,由ctr调用cni插件完成的。

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

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

相关文章

高档建筑覆膜板,胶水足表面光滑

在建筑材料行业&#xff0c;选择高质量的建筑覆膜板至关重要。贵港市能强优品木业是专业从事建筑覆膜板生产销售25年的源头工厂。这家工厂一直以来致力于生产出色的覆膜板&#xff0c;以确保建筑物外观精美&#xff0c;持久耐用。 无论是商业大楼还是家庭住宅&#xff0c;外墙装…

气膜建筑助力体育场馆快速普及

传统的室内体育馆投入资金庞大&#xff0c;建设强度高&#xff0c;建设周期漫长。而气膜体育馆的出现&#xff0c;不仅显著降低了建设成本和缩短了建设周期&#xff0c;更符合节能环保的需求&#xff0c;成为推动场馆快速普及的创新建筑形式。 对于校园设施的建设而言&#xff…

可以免费使用的Axure在线版来了

Axure作为一种功能强大的原型设计工具&#xff0c;一直受到设计师的青睐。然而&#xff0c;其高昂的价格可能成为一个门槛&#xff0c;限制了一些设计师的选择。但不用担心&#xff0c;现在有一个免费的Axure在线工具即时设计&#xff0c;功能更完整&#xff0c;更划算&#xf…

『 MySQL数据库 』插入查询结果

文章目录 &#x1f39f;️ 前言&#x1f39f;️ 创建一张结构相同的表&#x1f39f;️ 表内插入查询结果&#x1f3ab; 对表内数据进行去重&#x1f3ab; 配合ORDER BY排序后以及LIMIT分页对数据进行插入 &#x1f39f;️ 前言 在MySQL数据库中不仅可以直接根据字段类型等对数据…

信而泰 SSL测试方法介绍

[本文介绍在ALPS平台上进行SSL测试的内容和方法] 什么是SSL SSL全称是Secure Sockets Layer&#xff0c;指安全套接字协议&#xff0c;为基于TCP的应用层协议提供安全连接&#xff1b;SSL介于TCP/IP协议栈的第四层和第五层之间&#xff0c;广泛用于电子商务、网上银行等。 SSL…

String你知道多少细节(含面试题)

1 字符串初始化 常见的初始化方式有以下3种 public static void main(String[] args) {String s1 "abc";System.out.println(s1);String s2 new String("abc");System.out.println(s2);char[] s3 {a,b,c};System.out.println(s3);} 【注意】 1.Strin…

【带头学C++】----- 八、C++面向对象编程 ---- 8.10 函数的默认参数

8.10 函数的默认参数 C在声明函数原型的时可为一个或者多个参数指定默认(缺省)的参数值&#xff0c;当函数调用的时候如果没有指定这个值&#xff0c;编器会自动用默认值代替。 通过为函数参数指定默认值&#xff0c;可以在调用函数时省略相应的参数&#xff0c;而该参数将使用…

自定义Windows服务启动失败

文章目录 自定义Windows服务启动失败报错内容解决方案管理员身份运行cmd进入到InstallUtil.exe的路径&#xff0c;使用cd命令。使用InstallUtil.exe工具安装服务。 自定义Windows服务启动失败 报错内容 “无法从命令行或调试器启动服务&#xff0c;必须首先安装Windows服务(使…

TikTok区块链实践:数字社交媒体的去中心化未来

随着区块链技术的日渐成熟&#xff0c;数字社交媒体行业也在探索如何整合区块链&#xff0c;以推动去中心化发展。在这一潮流中&#xff0c;TikTok作为全球领先的短视频平台&#xff0c;积极实践区块链技术&#xff0c;探索数字社交媒体的未来。本文将深入探讨TikTok的区块链实…

PHP项目用docker一键部署

公司新项目依赖较多&#xff0c;扩展版本参差不一&#xff0c;搭建环境复杂缓慢&#xff0c;所以搭建了一键部署的功能。 docker-compose build 构建docker docker-compose up 更新docker docker-compose up -d 后台运行docker docker exec -it docker-php-1 /bin/bas…

第13关 解决K8s中Ingress Nginx控制器无法获取真实客户端IP的问题

------> 课程视频同步分享在今日头条和B站 大家好&#xff0c;我是博哥爱运维。 这节课带大家探索并分享最全面的解决在使用Kubernetes&#xff08;K8s&#xff09;和Ingress-Nginx-Controller中无法获取客户端真实IP问题的视频教程&#xff0c;帮助你快速理解并解决这一问…

idea 导入外部包 打包失败

一、在项目中引入jar包 二、pom文件添加 引入外部包 <!--应用第三方包监听文件--><dependency><groupId>jnotify</groupId><artifactId>jnotify</artifactId><version>0.94.0</version><scope>system</scope><…

统信桌面版arm系统安装火狐浏览器和浏览器驱动

一、系统信息 二、下载浏览器和驱动 1、浏览器 https://security.debian.org/debian-security/pool/updates/main/f/firefox-esr/firefox-esr_115.5.0esr-1~deb10u1_arm64.deb 2、驱动 https://github.com/mozilla/geckodriver/releases geckodriver-v0.33.0-linux-aarch6…

如何通过nginx进行反向代理

简单介绍 正向代理 正向代理服务器是一个位于客户端和原始服务器(origin server)之间的服务器&#xff0c;为了能够从原始服务器取得内容&#xff0c;客户端向代理发送一个请求并指定目标(原始服务器)&#xff0c;然后代理向原始服务器转交请求并将获得的内容返回给客户端。正向…

短视频账号矩阵系统开发--saas源头技术开发(手机版)

目前PC端网页版基本上已经很倦市场了&#xff0c;所以在这种情况下 &#xff0c;我们已经专注开发短视频矩阵系统pc版3年了&#xff0c;目前我们这边核心技术优势就是都是自己一手搭建开发的并且我们的剪辑算法也是自己一手源头开发的&#xff0c;剪辑成本后期运营成本低&#…

关于MySQL的66个问题

SQL基础掌握不错的小伙伴可以跳过这一部分。当然&#xff0c;可能会现场写一些SQL语句&#xff0c;SQ语句可以通过牛客、LeetCode、LintCode之类的网站来练习。 1. 什么是内连接、外连接、交叉连接、笛卡尔积呢&#xff1f; 内连接&#xff08;inner join&#xff09;&#xf…

初刷leetcode题目(10)——数据结构与算法

&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️Take your time ! &#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️…

数电票试点扩围至36个省市区 百望云解决方案助力企业数电升级

今日&#xff0c;国家税务总局西藏自治区税务局发布《关于开展全面数字化的电子发票试点工作的公告》&#xff0c;于12月1日起正式实施。自2021年12月1日上海、广东、内蒙古三地拉开数电票试点的序幕&#xff0c;两年时间内&#xff0c;36个省市区&#xff08;含计划单列市&…

Java高级技术(反射:获取类的成员变量)

一&#xff0c;获取类的成员变量的常用方法 二&#xff0c;案例 三&#xff0c;赋值&#xff0c;取值的常用方法 四&#xff0c; 案例 五&#xff0c;获取类的成员方法的常用方法 六&#xff0c;案例 七&#xff0c;执行 八&#xff0c;案例

软著项目推荐 深度学习图像风格迁移 - opencv python

文章目录 0 前言1 VGG网络2 风格迁移3 内容损失4 风格损失5 主代码实现6 迁移模型实现7 效果展示8 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习图像风格迁移 - opencv python 该项目较为新颖&#xff0c;适合作为竞赛课题…