containerd系统分析(一)-系统组成-CSDN博客
containerd系统分析(二)-镜像管理-CSDN博客
containerd系统分析(三)-容器创建流程分析-CSDN博客
containerd系统分析(四)-容器启动流程分析-CSDN博客
containerd系统分析(五)-网络分析-CSDN博客
containerd系统分析(六)-CRI接口-CSDN博客
在执行完创建容器的过程后,若把容器启动起来,需要执行ctr task start bb-1指令,其中bb-1为在前文系统分析(三)https://mp.csdn.net/mp_blog/creation/editor/145000596创建的容器。
执行ctr task start,实际上是执行了下面的二个步骤:
tasks.NewTask
task.Start(ctx)
若是启动了cni,还要执行网络的安装.
创建任务总结如下:这个过程实际上就是准备了runtime要执行的bundle,然后通过runc-shim通过这个bundle去创建了容器。
Stdin: filepath.Join(dir, id+"-stdin"),
Stdout: filepath.Join(dir, id+"-stdout"),
Stderr: filepath.Join(dir, id+"-stderr"),
这里的io用途是将io的内容copy到con句柄,这样tty就可以与容器交互了 func NewCreator(opts ...Opt) Creator {
streams := &Streams{}
for _, opt := range opts {
opt(streams)
}
if streams.FIFODir == "" {
streams.FIFODir = defaults.DefaultFIFODir
}
return func(id string) (IO, error) {
fifos, err := NewFIFOSetInDir(streams.FIFODir, id, streams.Terminal)
if err != nil {
return nil, err
}
if streams.Stdin == nil {
fifos.Stdin = ""
}
if streams.Stdout == nil {
fifos.Stdout = ""
}
if streams.Stderr == nil {
fifos.Stderr = ""
}
return copyIO(fifos, streams)
}
}
容器的id
通过ioCreator得到io,再由io得到req需要的参数
i, err := ioCreate(c.id)
if err != nil {
return nil, err
}
defer func() {
if err != nil && i != nil {
i.Cancel()
i.Close()
}
}()
cfg := i.Config()
request := &tasks.CreateTaskRequest{
ContainerID: c.id,
Terminal: cfg.Terminal,
Stdin: cfg.Stdin,
Stdout: cfg.Stdout,
Stderr: cfg.Stderr,
}
t := &task{
client: c.client, //复用clinet,用来和containerd通信的
io: i, //创建的io对象,用来tty输入输出的
id: c.id, //容器的id
c: c, //容器对象
}
opts := runtime.CreateOpts{
Spec: container.Spec, //容器的spec参数
IO: runtime.IO{
Stdin: r.Stdin, //请求报文的输入
Stdout: r.Stdout, //请求报文的输出
Stderr: r.Stderr,
Terminal: r.Terminal, //是否使用了terminal
},
Checkpoint: checkpointPath, //checkpoint的路径
Runtime: container.Runtime.Name, //运行时的名称
RuntimeOptions: container.Runtime.Options,
TaskOptions: r.Options, //任务options
SandboxID: container.SandboxID,
}
在(m *ShimManager) Start中首先会去创建bundle
在代码中m.root和m.state的作用是不同的.
m.root指向的是/var/lib/containerd/io.containerd.runtime.v2.task
m.state指向的是/run/containerd/io.containerd.runtime.v2.task,实际上这个路径下存的东西是最终runc要用到的
最终得到的bundle的路径是得到的bundle是目标执行的路径state的目标路径为/run/containerd/io.containerd.runtime.v2.task/{ns}/{id}
调用的代码是
func NewBundle(ctx context.Context, root, state, id string, spec typeurl.Any) (b *Bundle, err error) {
b = &Bundle{
ID: id, //容器的id
Path: filepath.Join(state, ns, id), //运行时的路径为/run/containerd/io.containerd.runtime.v2.task/{ns}/{id}
Namespace: ns, //ctr时为default,docker为moby
}
b := shimBinary(bundle, shimBinaryConfig{
runtime: runtimePath,
address: m.containerdAddress, //var/containerd/containerd.sock
ttrpcAddress: m.containerdTTRPCAddress, //var/containerd/contaierd.sock.ttrpc
schedCore: m.schedCore,
})
shim, err := b.Start(ctx, protobuf.FromAny(topts), func() {
cmd, err := client.Command(
ctx,
&client.CommandConfig{
Runtime: b.runtime, //shim插件
Address: b.containerdAddress, //监听的端口
TTRPCAddress: b.containerdTTRPCAddress,
Path: b.bundle.Path, //bundle的路径
Opts: opts,
Args: args,
SchedCore: b.schedCore,
})
out, err := cmd.CombinedOutput(),在这里运行命令并捕获输出的buff,输出的地址, unix:///run/containerd/s/随机数
创建一个ttrpc client,后面要根据这个连接向shim发送报文
shim{
bundle: b.bundle,
client: client,
}
以已创建好的shim对象构建shimTask
shimTask := newShimTask(shim)
这个shimTask的作用是封装shimtask,这里会通过shim实例取一个client,来封装一个client,用来传递报文,对容器的生命周期的管理,由这个shimtask来完成
调用代码(s *shimTask) Create(
构建的请求报文如下:
request := &task.CreateTaskRequest{
ID: s.ID(),
Bundle: s.Bundle(), //bundle路径
Stdin: opts.IO.Stdin,//tty的io输入
Stdout: opts.IO.Stdout,//输出
Stderr: opts.IO.Stderr,
Terminal: opts.IO.Terminal,.//是否使用了termial
Checkpoint: opts.Checkpoint,//checkpoint的路径
Options: protobuf.FromAny(topts), .//额外的运行时参数
}
若opts中的mount参数不为空,则要添加到rootfs中
for _, m := range opts.Rootfs {
request.Rootfs = append(request.Rootfs, &types.Mount{
Type: m.Type,
Source: m.Source,
Options: m.Options,
})
}
构建完之后,就可以发送创建任务报文了
这里的shim实际上是根据传来的容器指令,调用runc或crun等最底层的容器运行时去执行容器的相关的指令。在这里实际上是在执行容器的创建指令,实际上创建了一个进程,获得了一个PID,容器的执行环境也就创建好了。具体过程如下:
在shim侧调用创建任务的代码:
func (s *service) Create(ctx context.Context, r *taskAPI.CreateTaskRequest) (_ *taskAPI.CreateTaskResponse, err error) {
在这里主要是通过执行下面的代码进行容器的创建
container, err := runc.NewContainer(ctx, s.platform, r)
创建过程如下:
config := &process.CreateConfig{
ID: r.ID,
Bundle: r.Bundle,
Runtime: opts.BinaryName,//这一步是很关键的,这个是说明要使用的runtime具体是什么
Rootfs: mounts,
Terminal: r.Terminal,
Stdin: r.Stdin,
Stdout: r.Stdout,
Stderr: r.Stderr,
Checkpoint: r.Checkpoint,
ParentCheckpoint: r.ParentCheckpoint,
Options: r.Options,
}
p, err := newInit(
ctx,
r.Bundle,
filepath.Join(r.Bundle, "work"),
ns,
platform,
config,
opts,
rootfs,
)
func NewRunc(root, path, namespace, runtime string, systemd bool) *runc.Runc {
if root == "" {
root = RuncRoot //这个默认值是/run/containerd/runc下
}
return &runc.Runc{
Command: runtime, //这个也就是真实的用的运行时可能是runc,也可以是crun
Log: filepath.Join(path, "log.json"),
LogFormat: runc.JSON,
PdeathSignal: unix.SIGKILL,
Root: filepath.Join(root, namespace), ///run/containerd/runc/{ns}存储容器运行的state的数据
SystemdCgroup: systemd,
}
}
p := process.New(r.ID, runtime, stdio.Stdio{
Stdin: r.Stdin,
Stdout: r.Stdout,
Stderr: r.Stderr,
Terminal: r.Terminal,
})
p.Bundle = r.Bundle
p.Platform = platform
p.Rootfs = rootfs
p.WorkDir = workDir
p.IoUID = int(options.IoUid)
p.IoGID = int(options.IoGid)
p.NoPivotRoot = options.NoPivotRoot
p.NoNewKeyring = options.NoNewKeyring
p.CriuWorkPath = options.CriuWorkPath
if p.CriuWorkPath == "" {
// if criu work path not set, use container WorkDir
p.CriuWorkPath = p.WorkDir
}
func New(id string, runtime *runc.Runc, stdio stdio.Stdio) *Init {
p := &Init{
id: id,
runtime: runtime,
pausing: new(atomicBool),
stdio: stdio,
status: 0,
waitBlock: make(chan struct{}),
}
p.initState = &createdState{p: p}//在创建阶段就是createState,后面生命周期管理时会用到
return p
}
准备创建容器了
func (p *Init) Create(ctx context.Context, r *CreateConfig) error {
opts := &runc.CreateOpts{
PidFile: pidFile.Path(),//pidfile用来记录pid
NoPivot: p.NoPivotRoot,
NoNewKeyring: p.NoNewKeyring,
}
if err := p.runtime.Create(ctx, r.ID, r.Bundle, opts); err != nil {
return p.runtimeError(err, "OCI runtime create failed")
}
args := []string{"create", "--bundle", bundle}
if opts != nil {
oargs, err := opts.args() //这里会将上面所述的opts转成args作为运行的命令行参数传过去
if err != nil {
return err
}
args = append(args, oargs...)
}
cmd := r.command(context, append(args, id)...)
if cmd.Stdout == nil && cmd.Stderr == nil {
data, err := cmdOutput(cmd, true, nil)
defer putBuf(data)
if err != nil {
return fmt.Errorf("%s: %s", err, data.String())
}
return nil
}
….
status, err := Monitor.Wait(cmd, ec)
if err == nil && status != 0 {
err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status})
}
return err
在得到task对象后,通过task对象,执行容器启动运行容器中设定的运行的命令。调用代码:
if err := task.Start(ctx); err != nil
服务端的运行比较简单,根据2.3节创建task的shim的对象执行Start指令去执行要在容器中执行的命令,绑定到相关的pid上。
执行stat命令查看容器的状态
func (l *local) Start(ctx context.Context, r *api.StartRequest, _ ...grpc.CallOption) (*api.StartResponse, error) {
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)
}
}
//启动容器
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
}
根据接收的指令执行相关的容器的命令
容器启动的命令,从runc这类运行时来看,实际上是对应了容器的启动和命令的执行两者的合体。
在这里要说明的是,容器的启动实际上分成了两阶段:(1)创建了一个进程的执行环境 (2)执行要在容器执行的命令。
再者,容器的具体的执行是containerd的核心程序通过调用shim去调用runc这类runtime去执行容器。因此创建了多少个容器,实际上也就创建了多少个shim。从这点上来看,似乎并不高效。