• namaspace之pid namespace


    认识Namespace

    namespace 是 Linux 内核用来隔离内核资源的方式。通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。linux 内核提供的 namespace 技术为 docker 等容器技术的出现和发展提供了基础条件。

    Linux 现有的namespace 有7种:

    namespace隔离内容
    Cgroup CLONE_NEWCGROUP Cgroup root directory
    IPC CLONE_NEWIPC System V IPC, POSIX消息队列
    Network CLONE_NEWNET 网络设备、栈、端口等
    Mount CLONE_NEWNS 挂载点
    PID CLONE_NEWPID 进程ID
    User CLONE_NEWUSER 用户和组ID
    UTS CLONE_NEWUTS 主机名和NIS域名

    其中Cgroup namespace在4.6的内核中才实现。本文只介绍pid namespace。文章写的比较繁琐,还请大家耐心看到后面( ^_^ ) ~~~~

    Namespace操作函数

    和namespace相关的函数只有三个,如下所示:

    一、clone: 创建一个新的进程并把他放到新的namespace中。

    int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg); 

    其中:flags用于指定一个或者多个上面的CLONE_NEW*宏定义(当然也可以包含跟namespace无关的flags,多个flags 用|进行分隔),这样就会创建一个或多个新的不同类型的namespace,并把新创建的子进程加入新创建的这些namespace中。

    二、setns: 将当前进程加入到已有的namespace中

    int setns(int fd, int nstype); 

    其中:

    • fd:指向/proc/[pid]/ns/目录里相应namespace对应的文件,表示要加入哪个namespace
    • nstype:指定namespace的类型(上面的任意一个CLONE_NEW*),具体分为两种情况:1. 如果当前进程不能根据fd得到它的类型,如fd由其他进程创建,并通过UNIX domain socket传给当前进程,那么就需要通过nstype来指定fd指向的namespace的类型。2. 如果进程能根据fd得到namespace类型,比如这个fd是由当前进程打开的,那么nstype设置为0即可。

    三、unshare: 使当前进程退出指定类型的namespace,并加入到新创建的namespace(相当于创建并加入新的namespace)。

    int unshare(int flags); 

    其中:flags用于指定一个或者多个上面的CLONE_NEW*宏定义(当然也可以包含跟namespace无关的flags,多个flags 用|进行分隔),这样就会创建一个或多个新的不同类型的namespace,并把新创建的子进程加入新创建的这些namespace中。 clone和unshare的区别

    clone和unshare的功能都是创建并加入新的namespace, 他们的区别是:

    • unshare是使当前进程加入新的namespace。
    • clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变。

    pid namespace有什么用?

    PID Namespace对进程PID重新标号,即不同的Namespace下的进程可以有同一个PID。

    内核为所有的PID Namespace维护了一个树状结构,最顶层的是系统初始化创建的,被称为Root Namespace,由它创建的新的PID Namespace成为它的Child namespace,原先的PID Namespace成为新创建的Parent Namespace,这种情况下不同的PID Namespace形成一个等级体系:父节点可以看到子节点中的进程,可以通过信号对子节点的进程产生影响,反过来子节点无法看到父节点PID Namespace里面的进程。下面用一个图描述容器、进程pid、pid namespace关系:

    PID namesapce 对容器类应用特别重要, 可以实现容器内进程的暂停/恢复等功能,还可以支持容器在跨主机的迁移前后保持内部进程的 PID 不发生变化。

    pid namespace 特性

    1、进程所属的 PID namespace 在它创建的时候就确定了,不能更改,所以调用 unshare 和 nsenter 等命令后,原进程还是属于老的 PID namespace,新 fork 出来的进程才属于新的 PID namespace;

    2、PID namespace 可以嵌套;

    3、PID namespace 中的 init 进程。当一个进程的父进程退出后,该进程就变成了孤儿进程。孤儿进程会被当前 PID namespace 中 PID 为 1 的进程接管,而不是被最外层的系统级别的 init 进程接管。


    下面从kernel源码中了解下pid namespace的原理和用法。所有kernel源码均来自linux 4.18.0。

    核心数据结构

    1、内核使用struct pid_namespace 结构体描述进程号命名空间:

    //include/linux/pid_namespace.h
    struct pid_namespace {
        struct kref kref;
        struct idr idr;
        struct rcu_head rcu;
        unsigned int pid_allocated;
        struct task_struct *child_reaper;
        struct kmem_cache *pid_cachep;
        unsigned int level;
        struct pid_namespace *parent;
    #ifdef CONFIG_PROC_FS
        struct vfsmount *proc_mnt;
        struct dentry *proc_self;
        struct dentry *proc_thread_self;
    #endif
    #ifdef CONFIG_BSD_PROCESS_ACCT
        struct fs_pin *bacct;
    #endif
        struct user_namespace *user_ns;
        struct ucounts *ucounts;
        struct work_struct proc_work;
        kgid_t pid_gid;
        int hide_pid;
        int reboot;        /* group exit code if this pidns was rebooted */
        struct ns_common ns;
    } __randomize_layout;

    2、内核将所有的namespace封装成struct nsproxy ,struct pid_namespace就在该结构体中:

    //include/linux/nsproxy.h
    struct nsproxy {
        atomic_t count;
        struct uts_namespace *uts_ns;
        struct ipc_namespace *ipc_ns;
        struct mnt_namespace *mnt_ns;
        struct pid_namespace *pid_ns_for_children;
        struct net              *net_ns;
        struct cgroup_namespace *cgroup_ns;
    };

    3、命名空间是进程资源隔离技术,自然是要放在进程描述符中,下面截取struct task_struct部分:

    struct task_struct {
        ... ...
            struct fs_struct *fs;
        struct files_struct *files;
        struct nsproxy *nsproxy;
        ... ...
    };

    至此内核中 pid namespace组织形式有了初步认识,下面介绍下pid namespace的初始化和相关内核API。

    调用过程分析

    【1】系统 init进程的pid namespace

    每一个进程都有自己的namespace(struct nsproxy),可以看做是这个进程自己的“地盘”。进程默认都是共享init进程的namespace,即系统“默认”的根命名空间(目录树、pid等)。pid_namespace按层次组织成一棵树,init进程pid namespace是树的根,对应全局变量 init_pid_ns:

    注意,创建进程时,从进程所属的pid_namespace到init_pid_ns 都会分配进程号!

    init进程的pid_namespace是在内核初始化阶段创建的。在x86体系结构上,kernel将init进程namespace数据放在了“.data”段上,下面是截取的部分源码:

    • 上面提到的 init_pid_ns 统一放在init进程全局变量init_nsproxy中:
    struct nsproxy init_nsproxy = {
        .count                        = ATOMIC_INIT(1),
        .uts_ns                        = &init_uts_ns,
    #if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
        .ipc_ns                        = &init_ipc_ns,
    #endif
        .mnt_ns                        = NULL,
        .pid_ns_for_children        = &init_pid_ns, //init进程的pid_namespace
    #ifdef CONFIG_NET
        .net_ns                        = &init_net,
    #endif
    #ifdef CONFIG_CGROUPS
        .cgroup_ns                = &init_cgroup_ns,
    #endif
    };

    init进程的进程描述符是静态定义的,init_nsproxy就放在其中:

    struct task_struct
    … …
        .fs                = &init_fs,
        .files                = &init_files,
        .signal                = &init_signals,
        .sighand        = &init_sighand,
        .nsproxy        = &init_nsproxy, //命名空间代理
    … …
    }

    通过链接脚本,可以看到init进程静态定义的进程描述符放在内核数据段:

    //include/asm-generic/vmlinux.lds.h
    #define INIT_TASK_DATA(align)                                                
        . = ALIGN(align);                                                
        __start_init_task = .;                                                
        init_thread_union = .;                                                
        init_stack = .;                                                        
        KEEP(*(.data..init_task))                                        
        KEEP(*(.data..init_thread_info))                                
        . = __start_init_task + THREAD_SIZE;                                
    __end_init_task = .;
    
    //arch/x86/kernel/vmlinux.lds.S
    SECTIONS
    {
        … …
        /* init_task */
        INIT_TASK_DATA(THREAD_SIZE)
        … …
    }

    【2】子进程的pid_namespace

    进程创建时函数调用栈如下(包含pid namespace创建):

    sys_fork / sys_vfork / sys_clone / kernel_thread
    └→ _do_fork
          └→ copy_process
              └→ copy_namespaces
                   └→ create_new_namespaces
                            └→ copy_pid_ns

    在内核中进程创建核心函数是copy_process(),新建pid namespace的核心函数是copy_pid_ns()。下面依次分析。

    • 首先在进程创建时,会判断是否需要新建pid namespace。copy_process()函数调用copy_namespaces() 函数处理子进程namespace相关,下面截取该函数部分:
    int copy_namespaces(unsigned long flags, struct task_struct *tsk)
    {
        struct nsproxy *old_ns = tsk->nsproxy;
        struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);
        struct nsproxy *new_ns;
    
        if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
                      CLONE_NEWPID | CLONE_NEWNET |
                      CLONE_NEWCGROUP)))) {
            get_nsproxy(old_ns); //如果flags中没有CONE_NEW相关新建namespace标记,则继承父进程的namespace
            return 0;
        }
            ... ...
        new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs); //否则创建新的namespace
            ... ...
    }

    这里“get_nsproxy(old_ns) ”比较简单,增加父进程namespace引用计数,大部分子进程都是继承init进程的namespace。下面重点看下如何创建新的namespace。create_new_namespaces() 函数中调用copy_pid_ns() 来新建pid namespace,他的核心是create_pid_namespace() ,源码节选:

    static struct pid_namespace *create_pid_namespace(struct user_namespace *user_ns,
        struct pid_namespace *parent_pid_ns)
    {
        struct pid_namespace *ns;
        unsigned int level = parent_pid_ns->level + 1;  //新创建的pid_namespace在树中新一层
            … …
        ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL);
        if (ns == NULL)
            goto out_dec;
            … …
        ns->pid_cachep = create_pid_cachep(level); //struct pid结构体高速缓存
        ns->ns.ops = &pidns_operations;
    
        kref_init(&ns->kref);
        ns->level = level;
        ns->parent = get_pid_ns(parent_pid_ns);    //父pid_namespace
        ns->user_ns = get_user_ns(user_ns);
        ns->ucounts = ucounts;
        ns->pid_allocated = PIDNS_ADDING;    //初始化新pid_namespace pid计数
        INIT_WORK(&ns->proc_work, proc_cleanup_work);
            … …
    }

    【3】创建子进程pid

    如何用标识一个进程呢?对于进程id,虽然用户空间使用一个正整数来表示各种IDs,但是对于内核,我们需要使用“pid namespace,ID number”这样的二元组来表示。因为同样的进程在不同的 PID namespace 中拥有不同的 PID。linux内核使用struct pid结构体来标识进程:

    struct pid
    {
        atomic_t count;
        unsigned int level;   //该进程所属的pid_ns的level,也就表示了这个pid对象在多少个pid namespace中可见。
        /* lists of tasks that use this pid */
        struct hlist_head tasks[PIDTYPE_MAX]; //使用该pid结构体的进程描述符集合
        struct rcu_head rcu;
        struct upid numbers[1];  //存储每层的pid信息的变成数组,长度就是上面的level
    };
    struct upid {
        int nr; //该层pid ns 的PID值
        struct pid_namespace *ns; //该层pid ns结构体地址
    };

    关于标识进程的各个ID详解(pid/tid/tgid/sid等),感兴趣的可以参考《Linux系统如何标识进程?》一文。

    在进程创建核心函数copy_process中,通过alloc_pid 函数为子进程申请一个“struct pid”:

    //copy_process()    
    if (pid != &init_struct_pid) { //pid指针是函数入参,普通进程创建时调用的__do_fork中该参数为NULL
            pid = alloc_pid(p->nsproxy->pid_ns_for_children);
            if (IS_ERR(pid)) {
                retval = PTR_ERR(pid);
                goto bad_fork_cleanup_thread;
            }
        }

    pid指针是函数入参,普通进程创建时调用的__do_fork函数中,pid参数为NULL,因此一般进程都会调用“alloc_pid”创建新的struct pid:

    struct pid *alloc_pid(struct pid_namespace *ns)  //参数是新进程pid ns,返回值是申请的pid结构体
    {
        struct pid *pid;
        enum pid_type type;
        int i, nr;
        struct pid_namespace *tmp;
        struct upid *upid;
        int retval = -ENOMEM;
    
        pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);  //pid_ns 初始化时赋值了pid缓存
        if (!pid)
            return ERR_PTR(retval);
    
        tmp = ns;
        pid->level = ns->level;  //pid成员level记录新进程pid_ns的level,即进程ns的编号
    
        for (i = ns->level; i >= 0; i--) {    //遍历所有父pid_ns,pid结构体内需要记录每层pid_ns分配的pid值
            int pid_min = 1;     //如果进程的pid_ns是新创建的,则pid值从1开始
    … …
            nr = idr_alloc_cyclic(&tmp->idr, NULL, pid_min,
                      pid_max, GFP_ATOMIC);
            spin_unlock_irq(&pidmap_lock);
            idr_preload_end();
    … …
            pid->numbers[i].nr = nr;    //在pid结构体中记录每层pid_ns分配的pid值
            pid->numbers[i].ns = tmp;    //记录每层pid_ns分配同时,记录pid值对应进程的pid_ns地址
            tmp = tmp->parent;
        }
    … …
    
        upid = pid->numbers + ns->level;    //更新每层pid_ns的pid已使用值:pid_allocated
    … …
        for ( ; upid >= pid->numbers; --upid) {
            /* Make the PID visible to find_pid_ns. */
            idr_replace(&upid->ns->idr, pid, upid->nr);
            upid->ns->pid_allocated++;  //更新这层pid_ns的pid已使用值,即直接加一即可
        }
        spin_unlock_irq(&pidmap_lock);
    
        return pid;
    … …
    }

    【4】保存进程ID信息

    在解析完子进程pid创建后,下面看看如何保存到进程描述符(task_struct)中。再回到进程创建copy_process函数中,有两处和保存进程ID信息相关:

    //in copy_process func:
        p->pid = pid_nr(pid);   //获取global pid_ns中的pid,即第0层
            … …
            init_task_pid(p, PIDTYPE_PID, pid);   //将struct pid 结构地址保存到进程描述符中
    • 首先task_struct->pid中存的是global pid namespace中的PID value,因为对于内核来说,它只需要看全局的pid namespace即可(init进程),里面包含系统全局进程的global PID。
    • 第二个函数init_task_pid 和相关结构体如下:
    static inline void
    init_task_pid(struct task_struct *task, enum pid_type type, struct pid *pid)
    {
         task->pids[type].pid = pid;
    }
    
    struct task_struct
    {
            … …
        struct pid_link        pids[PIDTYPE_MAX];
            … …
    }
    enum pid_type
    {
        PIDTYPE_PID,
        PIDTYPE_PGID,
        PIDTYPE_SID,
        PIDTYPE_MAX,
        /* only valid to __task_pid_nr_ns() */
        __PIDTYPE_TGID
    };
    struct pid_link
    {
        struct hlist_node node;
        struct pid *pid;
    };

    我们知道在linux中并不区分进程和线程,都是用task_struct来抽象,只不过支持多线程的进程是由一组task_struct来抽象。struct pid 结构可能被多个进程共享(比如表示pgid时),为了既能方便从task struct快速找到对应的struct pid,又能方便从struct pid能够遍历所有使用该pid的task,内核设计了 struct pid_link 来保存各个ID对应的struct pid 结构地址。

    【5】获取进程PID value

    linux内核提供三个标准API,用于获取进程PID value:

    1. pid_nr(): 获取全局pid_ns pid value,即第0 level,来自init namespace
    static inline pid_t pid_nr(struct pid *pid)
    {
        pid_t nr = 0;
        if (pid)
            nr = pid->numbers[0].nr;
        return nr;
    }

    2. pid_vnr() :获取当前pid_ns pid value,即进程当前pid namespace

    pid_t pid_vnr(struct pid *pid)
    {
        return pid_nr_ns(pid, task_active_pid_ns(current));
    }
    EXPORT_SYMBOL_GPL(pid_vnr);

    3. pid_nr_ns() :获取指定ns 中的pid value

    pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
    {
        struct upid *upid;
        pid_t nr = 0;
    
        if (pid && ns->level <= pid->level) {
            upid = &pid->numbers[ns->level];
            if (upid->ns == ns)
                nr = upid->nr;
        }
        return nr;
    }
    EXPORT_SYMBOL_GPL(pid_nr_ns);
    • 内核空间系统管理只需要关注“默认”的根命名空间中的PID value即可,因此调用pid_nr在task_struct->pid中缓存的PID value ,称为global PID
    • 用户空间运用namespace进程资源隔离,因此用户空间获取进程PID 的系统调用getpid需要关注pid namespace。相关系统调用源码如下:
    SYSCALL_DEFINE0(getpid)
    {
        return task_tgid_vnr(current);
    }
    static inline pid_t task_tgid_vnr(struct task_struct *tsk)
    {
        return __task_pid_nr_ns(tsk, __PIDTYPE_TGID, NULL);
    }
    pid_t __task_pid_nr_ns(struct task_struct *task, enum pid_type type,
        struct pid_namespace *ns)
    {
        if (!ns)
            ns = task_active_pid_ns(current);
            … …
        nr = pid_nr_ns(rcu_dereference(task->pids[type].pid), ns);
        }
        rcu_read_unlock();
    
        return nr;
    }

    因此用户空间获取的是进程当前pid namespace里的pid value,称为virtual PID

    【6】根据pid获取pid_namespace

    函数ns_of_pid 用于根据pid获取pid_namespace:

    static inline struct pid_namespace *ns_of_pid(struct pid *pid)
    {
        struct pid_namespace *ns = NULL;
        if (pid)
            ns = pid->numbers[pid->level].ns;
        return ns;
    }

     docker namespace原理:https://blog.csdn.net/zhonglinzhang/article/details/64441263

  • 相关阅读:
    Docker 给 故障停掉的 container 增加 restart 参数
    使用docker化的nginx 反向代理 docker化的GSCloud 的方法
    apache benchmark 的简单安装与测试
    mysql5.7 的 user表的密码字段从 password 变成了 authentication_string
    Windows 机器上面同时安装mysql5.6 和 mysql5.7 的方法
    python4delphi 安装
    见证下神奇的时刻
    windows下面安装Python和pip终极教程
    python如何安装pip和easy_installer工具
    Tushare的安装
  • 原文地址:https://www.cnblogs.com/dahuige/p/15177213.html
Copyright © 2020-2023  润新知