一、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
}