美文网首页
namespaces 学习笔记2:uts ns 源码实现

namespaces 学习笔记2:uts ns 源码实现

作者: 董泽润 | 来源:发表于2019-10-09 11:20 被阅读0次

    TL;DR 最近想看 docker 相关的实现,自然涉及底层 namespace, 所以边做实验边看源码,感兴趣的先看耗子叔的文章

    测试案例

    uts namespace 主要是用来隔离主机名,但不只这一个功能。先上小例子,也是来自耗子叔

    #define _GNU_SOURCE
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <stdio.h>
    #include <sched.h>
    #include <signal.h>
    #include <unistd.h>
    #include <sys/mount.h>
    
    /* 定义一个给 clone 用的栈,栈大小1M */
    #define STACK_SIZE (1024 * 1024)
    static char container_stack[STACK_SIZE];
    
    char* const container_args[] = {
        "/bin/bash",
        NULL
    };
    
    int container_main(void* arg)
    {
        printf("Container - inside the container!\n");
        sethostname("my-container",10); /* 设置hostname */
        /* 直接执行一个shell,以便我们观察这个进程空间里的资源是否被隔离了 */
        execv(container_args[0], container_args);
        printf("Something's wrong!\n");
        return 1;
    }
    
    int main()
    {
        printf("Parent - start a container!\n");
        /* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
        int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD|CLONE_NEWUTS, NULL);
        /* 等待子进程结束 */
        waitpid(container_pid, NULL, 0);
        printf("Parent - container stopped!\n");
        return 0;
    }
    

    和上一篇测试 mount ns 不同,本次 flags 只设置了 CLONE_NEWUTS, 然后在子进程中调用 sethostname 设置容器的主机名。

    root@iZhp36ik63t96xhzjh00ujZ:~# gcc uts.c && ./a.out
    Parent - start a container!
    Container - inside the container!
    root@my-contain:~# pwd
    /root
    root@my-contain:~# exit
    exit
    Parent - container stopped!
    root@iZhp36ik63t96xhzjh00ujZ:~#
    root@iZhp36ik63t96xhzjh00ujZ:~#
    root@iZhp36ik63t96xhzjh00ujZ:~# pwd
    /root
    

    编绎并运行,可以看到容器内主机名己变,退出后宿主机主机名并没有改变。

    创建 uts namespace

    struct ns_common {
        atomic_long_t stashed;
        const struct proc_ns_operations *ops;
        unsigned int inum;
    };
    
    struct new_utsname {
        char sysname[__NEW_UTS_LEN + 1];
        char nodename[__NEW_UTS_LEN + 1];
        char release[__NEW_UTS_LEN + 1];
        char version[__NEW_UTS_LEN + 1];
        char machine[__NEW_UTS_LEN + 1];
        char domainname[__NEW_UTS_LEN + 1];
    };
    
    struct uts_namespace {
        struct kref kref; // 引用计数
        struct new_utsname name;
        struct user_namespace *user_ns;
        struct ucounts *ucounts;
        struct ns_common ns;
    } __randomize_layout;
    

    核心结构体是 uts_namespace,创建的过程和前文讲的 clone 调用相关,来自 copy_utsname. 另外可以看到 new_utsname 构构体内容是 uts ns 隔离的所有内容,不仅有主机名。

    struct uts_namespace *copy_utsname(unsigned long flags,
        struct user_namespace *user_ns, struct uts_namespace *old_ns)
    {
        struct uts_namespace *new_ns;
    
        BUG_ON(!old_ns);
        get_uts_ns(old_ns);
    
        if (!(flags & CLONE_NEWUTS))
            return old_ns;
    
        new_ns = clone_uts_ns(user_ns, old_ns);
    
        put_uts_ns(old_ns);
        return new_ns;
    }
    

    如果没有参数 CLONE_NEWUTS, 那就使用旧的 uts, 否则调用 clone_uts_ns 复制一份。

    static struct uts_namespace *clone_uts_ns(struct user_namespace *user_ns,
                          struct uts_namespace *old_ns)
    {
        struct uts_namespace *ns;
        struct ucounts *ucounts;
        int err;
    
        err = -ENOSPC;
        ucounts = inc_uts_namespaces(user_ns);
        if (!ucounts)
            goto fail;
    
        err = -ENOMEM;
        ns = create_uts_ns();
        if (!ns)
            goto fail_dec;
    
        err = ns_alloc_inum(&ns->ns);
        if (err)
            goto fail_free;
    
        ns->ucounts = ucounts;
        ns->ns.ops = &utsns_operations;
    
        down_read(&uts_sem);
        memcpy(&ns->name, &old_ns->name, sizeof(ns->name));
        ns->user_ns = get_user_ns(user_ns);
        up_read(&uts_sem);
        return ns;
        ......
    }
    

    这块代码更简单,主要是设置 uts ns 的回调函数结构体 utsns_operations,然后再将 old_ns.new_utsname 复制给新的 uts_ns,此段函数就完成了。

    设置主机名

    当容器启动后,uts namespace 己经隔离了,此时再设置主机名会与宿主机互不影响。先看下 hostname 系统调用

    ~# strace hostname
    ......
    uname({sysname="Linux", nodename="my-contain", ...}) = 0
    fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0
    write(1, "my-contain\n", 11my-contain
    )            = 11
    exit_group(0)                           = ?
    +++ exited with 0 +++
    

    省去无关信息,原来 hostname 是调用 uname 来获取的主机名等所有信息

    SYSCALL_DEFINE1(uname, struct old_utsname __user *, name)
    {
        struct old_utsname tmp;
    
        if (!name)
            return -EFAULT;
    
        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
        if (copy_to_user(name, &tmp, sizeof(tmp)))
            return -EFAULT;
    
        if (override_release(name->release, sizeof(name->release)))
            return -EFAULT;
        if (override_architecture(name))
            return -EFAULT;
        return 0;
    }
    
    static inline struct new_utsname *utsname(void)
    {
        return &current->nsproxy->uts_ns->name;
    }
    

    代码也非常简单,通过 utsname 获取当前进程的 uts namespace, 然后把 utsname 结构体复制一份到用户空间。后面两个 override 忽略,为了兼容老的版本。其中 old_utsnamenew_utsname 虽然是两个结构体,但是字段长度大小是一样的,只是为了去掉 hardcode 重新定义一个。

    小结

    现在看 uts ns 是最简单的一个,比较容易理解。

    相关文章

      网友评论

          本文标题:namespaces 学习笔记2:uts ns 源码实现

          本文链接:https://www.haomeiwen.com/subject/tqewpctx.html