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

posted @ 2021-08-23 18:27  大辉哥  阅读(409)  评论(0编辑  收藏  举报