Docker基础之runc初始化命令

一、initCommand

下面,我们进入runc的初始化指令的源码介绍:

var initCommand = cli.Command{
	//现在已经进入之前clone出来的一个namespace隔离的子进程在子进程中调用`runc init`来进行初始化设置
	Name:  "init",
	Usage: `initialize the namespaces and launch the process (do not call it outside of runc)`,
	Action: func(context *cli.Context) error {
		factory, _ := libcontainer.New("")
		//runc create的时候会调用到这里
		if err := factory.StartInitialization(); err != nil {
			// 当错误被发送回父进程时,不需要记录或将其写入标准错误,因为父进程将处理此错误
			os.Exit(1)
		}
		panic("libcontainer: container init failed to exec")
	},
}

二、factory.StartInitialization

1、Init进程通过管道pipe来读取父进程传送过来的信息
2、调用func newContainerInit(),生成一个type linuxStandardInit struct对象
3、执行linuxStandardInit.Init()

// StartInitialization通过从父进程打开管道fd来加载容器,以读取配置和状态。这是reexec的低级实现细节,不应该在外部使用
func (l *LinuxFactory) StartInitialization() (err error) {
	var pipefd, rootfd int
	for _, pair := range []struct {
		k string
		v *int
	}{
		{"_LIBCONTAINER_INITPIPE", &pipefd},
		{"_LIBCONTAINER_STATEDIR", &rootfd},
	} {
		s := os.Getenv(pair.k)
		i, err := strconv.Atoi(s)
		if err != nil {
			return fmt.Errorf("unable to convert %s=%s to int", pair.k, s)
		}
		*pair.v = i
	}
	var (
		//Init进程通过管道来读取父进程传送过来的信息
		pipe = os.NewFile(uintptr(pipefd), "pipe")
		it   = initType(os.Getenv("_LIBCONTAINER_INITTYPE"))
	)
	//清理当前进程的环境为了清理任何特定于libcontainer的env变量
	os.Clearenv()

	var i initer
	defer func() {
		// 在初始化容器的init时发生了错误,将它以initError的形式发送回父进程。如果容器初始化成功,系统调用。Exec将不会返回,因此这个延迟函数将永远不会被调用。
		if _, ok := i.(*linuxStandardInit); ok {
			//  Synchronisation only necessary for standard init.
			if werr := utils.WriteJSON(pipe, syncT{procError}); werr != nil {
				panic(err)
			}
		}
		if werr := utils.WriteJSON(pipe, newSystemError(err)); werr != nil {
			panic(err)
		}
		// ensure that this pipe is always closed
		pipe.Close()
	}()
	defer func() {
		if e := recover(); e != nil {
			err = fmt.Errorf("panic from initialization: %v, %v", e, string(debug.Stack()))
		}
	}()
	i, err = newContainerInit(it, pipe, rootfd)
	if err != nil {
		return err
	}
	return i.Init()
}

1、newContainerInit
runc create {} 时的t类型是 initStandard

func newContainerInit(t initType, pipe *os.File, stateDirFD int) (initer, error) {
	var config *initConfig
	//从pipe中解析出config信息
	if err := json.NewDecoder(pipe).Decode(&config); err != nil {
		return nil, err
	}
	if err := populateProcessEnvironment(config.Env); err != nil {
		return nil, err
	}
	//`runc create {}` 时的t类型是  initStandard
	switch t {
	case initSetns:
		return &linuxSetnsInit{
			config: config,
		}, nil
	case initStandard:
		return &linuxStandardInit{
			pipe:       pipe,
			parentPid:  syscall.Getppid(),
			config:     config,
			stateDirFD: stateDirFD,
		}, nil
	}
	return nil, fmt.Errorf("unknown init type %q", t)
}

三、linuxStandardInit构造函数

  • 定义
type linuxStandardInit struct {
	pipe       io.ReadWriteCloser
	parentPid  int
	stateDirFD int
	config     *initConfig
}

1、linuxStandardInit.Init
看看其Init()函数,这是本文的重点函数。 分析其流程如下:
1、 前面部分主要是进行参数设置和状态检查等
2、 exec.LookPath(l.config.Args[0])在当前系统的PATH中寻找 cmd 的绝对路径。这个cmd就是config.json中声明的用户希望执行的初始化命令。
3、 以"只写" 方式打开fifo管道,形成阻塞。等待另一端有进程以“读”的方式打开管道。
4、 如果单独执行runc create命令,到这里就会发生阻塞。 后面将是等待runc start以只读的方式打开FIFO管道,阻塞才会消除 ,本进程(Init进程)才会继续后面的流程。
5、 阻塞清除后,Init进程会根据config配置初始化seccomp,并调用syscall.Exec执行cmd。 系统调用syscall.Exec(),执行用户真正希望执行的命令,用来覆盖掉PID为1的Init进程。 至此,在容器内部PID为1的进程才是用户希望一直在前台执行的进程

func (l *linuxStandardInit) Init() error {
	if !l.config.Config.NoNewKeyring {
		ringname, keepperms, newperms := l.getSessionRingParams()
		// 不继承父类的秘钥
		sessKeyId, err := keys.JoinSessionKeyring(ringname)
		if err != nil {
			return err
		}
		// 使会话密钥环可搜索
		if err := keys.ModKeyringPerm(sessKeyId, keepperms, newperms); err != nil {
			return err
		}
	}
	var console *linuxConsole
	if l.config.Console != "" {
		console = newConsoleFromPath(l.config.Console)
		if err := console.dupStdio(); err != nil {
			return err
		}
	}
	if console != nil {
		if err := system.Setctty(); err != nil {
			return err
		}
	}
	//配置容器内部的网络
	if err := setupNetwork(l.config); err != nil {
		return err
	}
	//配置容器内部的路由
	if err := setupRoute(l.config.Config); err != nil {
		return err
	}
	//检查selinux是否处于enabled状态
	label.Init()
	// 如果设置了mount namespace,则调用setupRootfs在新的mount namespace中配置设备、挂载点以及文件系统。
	if l.config.Config.Namespaces.Contains(configs.NEWNS) {
		if err := setupRootfs(l.config.Config, console, l.pipe); err != nil {
			return err
		}
	}
	//配置hostname、apparmor、processLabel、sysctl、readonlyPath、maskPath。这些对容器启动本身没有太多影响
	if hostname := l.config.Config.Hostname; hostname != "" {
		if err := syscall.Sethostname([]byte(hostname)); err != nil {
			return err
		}
	}
	if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil {
		return err
	}
	if err := label.SetProcessLabel(l.config.ProcessLabel); err != nil {
		return err
	}

	for key, value := range l.config.Config.Sysctl {
		if err := writeSystemProperty(key, value); err != nil {
			return err
		}
	}
	for _, path := range l.config.Config.ReadonlyPaths {
		if err := remountReadonly(path); err != nil {
			return err
		}
	}
	for _, path := range l.config.Config.MaskPaths {
		if err := maskPath(path); err != nil {
			return err
		}
	}
	// 获取父进程的退出信号量
	pdeath, err := system.GetParentDeathSignal()
	if err != nil {
		return err
	}
	if l.config.NoNewPrivileges {
		if err := system.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
			return err
		}
	}
	// 通过管道与父进程进行同步,先发出procReady再等待procRun
	if err := syncParentReady(l.pipe); err != nil {
		return err
	}
	// 如果没有NoNewPrivileges, seccomp是一个有特权的操作,所以我们需要在删除功能之前执行此操作;否则,在execve之前尽可能晚地执行,以便在execve之后尽可能少地发生系统调用。
	if l.config.Config.Seccomp != nil && !l.config.NoNewPrivileges {
		// 初始化seccomp
		if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
			return err
		}
	}
	//调用finalizeNamespace根据config配置将需要的特权capabilities加入白名单,设置user namespace,关闭不需要的文件描述符。
	if err := finalizeNamespace(l.config); err != nil {
		return err
	}
	//恢复parent进程的death信号量并检查当前父进程pid是否为我们原来记录的。
	if err := pdeath.Restore(); err != nil {
		return err
	}
	if syscall.Getppid() != l.parentPid {
		return syscall.Kill(syscall.Getpid(), syscall.SIGKILL)
	}
	//在当前系统的PATH中寻找 cmd 的绝对路径
	name, err := exec.LookPath(l.config.Args[0])
	if err != nil {
		return err
	}
	// 与父进程之间的同步已经完成,关闭pipe,pipe是一个匿名管道(类似于go中的有容量的channel),匿名管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信。能把两个不相关的进程联系起来,FIFO就像一个公共通道,解决了不同进程之间的“代沟”。普通的无名管道只能让相关的进程进行沟通(比如父shell和子shell之间)
	l.pipe.Close()
		func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error)
	*/
	fd, err := syscall.Openat(l.stateDirFD, execFifoFilename, os.O_WRONLY|syscall.O_CLOEXEC, 0)
	if err != nil {
		return newSystemErrorWithCause(err, "openat exec fifo")
	}
	if _, err := syscall.Write(fd, []byte("0")); err != nil {
		return newSystemErrorWithCause(err, "write 0 exec fifo")
	}

	if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
		if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
			return newSystemErrorWithCause(err, "init seccomp")
		}
	}
	if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
		return newSystemErrorWithCause(err, "exec user process")
	}
	return nil
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要在 Docker 中使用达梦数据库进行命令初始化,需要按照以下步骤进行操作。 首先,我们需要先构建一个支持达梦数据库的 Docker 镜像。可以通过在 Dockerfile 中使用适当的基础镜像和达梦数据库安装程序来构建镜像。 在 Dockerfile 中,可以使用以下命令下载并安装达梦数据库: ``` RUN wget -P /tmp https://website/dm-install.tar.gz RUN tar -xzf /tmp/dm-install.tar.gz -C /tmp RUN cd /tmp/dm-install && ./install.sh --silent ``` 接下来,使用以下命令构建 Docker 镜像: ``` docker build -t dm-db . ``` 完成构建后,可以使用以下命令创建一个 Docker 容器: ``` docker run -d --name dm-container dm-db ``` 此时,Docker 容器以后台模式运行,并且达梦数据库已成功安装。 接下来,我们可以使用以下命令进入 Docker 容器的命令行: ``` docker exec -it dm-container bash ``` 进入容器后,可以使用以下命令启动达梦数据库命令行: ``` dmdosql dbname ``` 这将启动达梦数据库的命令行界面,其中 "dbname" 是要初始化的数据库的名称。 接下来,在达梦数据库命令行中,通过执行适当的 SQL 命令初始化数据库。例如,可以创建表、插入数据等。 完成数据库初始化后,可以通过退出达梦数据库命令行和容器的命令行来退出容器。 以上就是使用 Docker 和达梦数据库进行命令初始化的步骤。通过构建支持达梦数据库的 Docker 镜像,创建容器并进入容器的命令行,然后在达梦数据库命令行中执行相应的 SQL 命令,即可完成数据库初始化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绝域时空

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值