容器启动流程(containerd 和 runc)

启动流程

containerd 作为一个 api 服务,提供了一系列的接口供外部调用,比如创建容器、删除容器、创建镜像、删除镜像等等。使用 docker 和 ctr 等工具,都是通过调用 containerd 的 api 来实现的。
kubelet 通过 cri 调用 containerd 和这些不一样,后续我会介绍到。

containerd 创建容器流程如下:

  1. 接收到 api 请求,通过调用 containerd-shim-runc-v2 调用 runc 创建容器,主要是做解压文件和准备环境的工作。
  2. 接收到 api 请求,创建一个 task,task 是一个容器的抽象,包含了容器的所有信息,比如容器的 id、容器的状态、容器的配置等等。
  3. containerd 启动一个 containerd-shim-runc-v2 进程。
  4. containerd-shim-runc-v2 进程 在启动一个 containerd-shim-runc-v2 进程,然后第一个 containerd-shim-runc-v2 进程退出。
  5. containerd 通过 IPC 通信,让第二个 containerd-shim-runc-v2 启动容器。
  6. containerd-shim-runc-v2 进程通过调用 runc start 启动容器。
  7. runc 会调用 runc init 启动容器的 init 进程。
  8. runc init 进程会调用 unix.Exec 的方式,替换自己的进程,启动容器的第一个进程。这个进程既是容器的启动命令,也是容器的 pid 1 进程。完成之后,runc create 进程退出。

这样 containerd-shim-runc-v2 的父进程就是 init 进程(1),而 init 进程的父进程是 containerd-shim-runc-v2 进程,这样就形成了一个进程树。

我通过 docker 启动一个容器,示例一下:

docker run -d --rm -it docker.m.daocloud.io/ubuntu:22.10 sleep 3000ps -ef|grep "sleep 3000"
root       15042   15021  0 22:02 pts/0    00:00:00 sleep 3000ps -ef|grep "15021"
root       15021       1  0 22:02 ?        00:00:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 4346ca602cd85d35b0a4a81762be6142bc6a2222f859f4af47563992efc3c59c -address /run/containerd/containerd.sock
root       15042   15021  0 22:02 pts/0    00:00:00 sleep 3000

可以看到我们的结论是正确的。

疑问解答

1.为什么要创建两个 containerd-shim 不嫌麻烦吗?

因为 第一个 containerd-shim 会在创建完第二个 containerd-shim 后退出,而作为第一个进程子进程的第二个 containerd-shim 会成为孤儿进程,这样就会被 init 进程接管,而和 containerd 本身脱离了关系。

2.为什么要想法设法把 containerd-shim 挂在 init 进程下面,而不是 containerd?

为了保证稳定性和独立性。这样做可以确保即使 containerd 崩溃或重启,由 containerd-shim 管理的容器进程仍然可以继续运行,不受影响。此外,这种设计还有助于更好地管理资源和防止资源泄露。

3.为什么 runc start 进程退出了 runc init 进程(用户进程)没有变成 init 的子进程 而是containerd-shim的子进程?

因为 containerd-shim 做了 unix 的 PR_SET_CHILD_SUBREAPER 调用, 这个系统调用大概作用为 当这个进程的子子孙孙进程变成孤儿进程的时候,这个进程会接管这个孤儿进程,而不是 init 进程接管。

架构图

代码分析

containerd api 注册 代码分析

var register = struct {
   
   
	sync.RWMutex
	r plugin.Registry
}{
   
   }

type Registry []*Registration

type Registration struct {
   
   
	// Type of the plugin
	Type Type
	// ID of the plugin
	ID string
	// Config specific to the plugin
	Config interface{
   
   }
	// Requires is a list of plugins that the registered plugin requires to be available
	Requires []Type

	// InitFn is called when initializing a plugin. The registration and
	// context are passed in. The init function may modify the registration to
	// add exports, capabilities and platform support declarations.
	InitFn func(*InitContext) (interface{
   
   }, error)

	// ConfigMigration allows a plugin to migrate configurations from an older
	// version to handle plugin renames or moving of features from one plugin
	// to another in a later version.
	// The configuration map is keyed off the plugin name and the value
	// is the configuration for that objects, with the structure defined
	// for the plugin. No validation is done on the value before performing
	// the migration.
	ConfigMigration func(context.Context, int, map[string]interface{
   
   }) error
}

通过 init 把接口注册进去 比如 task api 注册

代码位置 : services/tasks/local.go

func init() {
   
   
	registry.Register(&plugin.Registration{
   
   
		Type:     plugins.ServicePlugin,
		ID:       services.TasksService,
		Requires: tasksServiceRequires,
		Config:   &Config{
   
   },
		InitFn:   initFunc,
	})

	timeout.Set(stateTimeout, 2*time.Second)
}

func initFunc(ic *plugin.InitContext) (interface{
   
   }, error) {
   
   
	config := ic.Config.(*Config)

	v2r, err := ic.GetByID(plugins.RuntimePluginV2, "task")
	if err != nil {
   
   
		return nil, err
	}

	m, err := ic.GetSingle(plugins.MetadataPlugin)
	if err != nil {
   
   
		return nil, err
	}

	ep, err := ic.GetSingle(plugins.EventPlugin)
	if err != nil {
   
   
		return nil, err
	}

	monitor, err := ic.GetSingle(plugins.TaskMonitorPlugin)
	if err != nil {
   
   
		if !errors.Is(err, plugin.ErrPluginNotFound) {
   
   
			return nil, err
		}
		monitor = runtime.NewNoopMonitor()
	}

	db := m.(*metadata.DB)
	l := &local{
   
   
		containers: metadata.NewContainerStore(db),
		store:      db.ContentStore(),
		publisher:  ep.(events.Publisher),
		monitor:    monitor.(runtime.TaskMonitor),
		v2Runtime:  v2r.(runtime.PlatformRuntime),
	}

	v2Tasks, err := l.v2Runtime.Tasks</
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值