转载请注明出处,并保留以上所有对文章内容、图片、表格的来源的描述。

 

一、Linux Namespace

        Linux Namespace是Linux提供的一种OS-level virtualization的方法。目前在Linux系统上实现OS-level virtualization的系统有Linux VServer、OpenVZ、LXC Linux Container、Virtuozzo等,其中Virtuozzo是OpenVZ的商业版本。以上种种本质来说都是使用了Linux Namespace来进行隔离。

        那么究竟什么是Linux Namespace?Linux很早就实现了一个系统调用chroot,该系统调用能够为进程提供一个限制的文件系统。虽然文件系统的隔离要比单纯的chroot复杂的多,但是至少chroot提供了一种简单的隔离模式:chroot内部的文件系统无法访问外部的内容。Linux Namespace在此基础上,提供了对UTS、IPC、mount、PID、network的隔离机制,例如对不同的PID namespace中的进程无法看到彼此,而且每个PID namespace中的进程PID都是单独制定的。这一点对OS-level Virtualization非常有用,这是因为:对于不同的Linux运行环境中,都有一个init进程,其PID=0,由于不同的PID namespace中都可以指定自己的0号进程,所以可以通过该技术来进行PID环境的隔离。

        OS-level Virtualization相比其他的虚拟化技术更加轻量级。

        Linux在使用Namespace的时候,需要显式的在配置中指定将那些Namespace的支持编译到内核中。

二、数据结构

        我们可以在struct task_struct中找到对应的namespace结构。

<sched.h>

1 struct task_struct { 
2 ... 
3     struct nsproxy *nsproxy; 
4 ... 
5 };

        nsproxy就是每个进程自己的namespace,结构如下:

<nsproxy.h>

1 struct nsproxy { 
2     atomic_t count; 
3     struct uts_namespace *uts_ns; 
4     struct ipc_namespace *ipc_ns; 
5     struct mnt_namespace *mnt_ns; 
6     struct pid_namespace *pid_ns; 
7     struct net          *net_ns; 
8 }; 
9 extern struct nsproxy init_nsproxy;

 

        具体的参考后文,可以看到有一个init_nsproxy,这个全局初始化使用的nsproxy,也是init进程使用的nsproxy。

三、UTS namespace

        我们在这里只介绍两种namespace:UTS namespace和PID namespace,是因为这两种namespace比较有代表性。UTS namespace很简单,也没有树形或者很复杂的结构。struct uts_namespace结构如下:

<utsname.h>

1 struct uts_namespace { 
2     struct kref kref; 
3     struct new_utsname name; 
4     struct user_namespace *user_ns; 
5 }; 
6 extern struct uts_namespace init_uts_ns;

 

        可以看到uts_namespace中只关心utsname域(name)和user_namespace域(user_ns)。老版本的Linux user_namespace是在nsproxy中的,新版本放在了uts_namespace中。这部分的结构如下:

85677792[4]

        需要说明的是,这张图是比较老的Linux Kernel,其中user_namespace还在nsproxy结构中。对于uts_namespace,由于不需要维护像pid_namespace那样复杂的树状结构和复杂的搜索需求,所以对于每个uts_namespace只需要维护一个实例就可以了。

四、进程的若干个ID的意义
  • PID:Process ID,进程ID,即进程的唯一标识
  • TGID:处于某个线程组中的所有进程都有统一的线程组ID(Thread Group IP,TGID)。线程可以用clone加CLONE_THREAD来创建。线程组中的主进程成为group leader,可以通过线程组中任何线程的的task_struct->group_leader成员获得。
  • 独立进程可以合并成进程组(使用setpgrp系统调用)。进程组成员的task_struct->pgrp属性值都是相同的(PGID),即进程组组长的PID。用管道连接的进程在一个进程组中。
  • 几个进程组可以合并成一个会话。会话中所有进程都有同样的SID(Session ID,会话ID),保存在task_struct->session中。SID可以通过setsid系统调用设置。
五、PID namespace

        task_struct中有几个成员是与PID有关的,如下:

<sched.h>

1 struct task_struct{ 
2 ... 
3     pid_t pid; 
4     pid_t tgid; 
5 ... 
6     struct pid_link pids[PIDTYPE_MAX]; 
7 ... 
8 }

 

        对于所有的进程来说,都有两种ID(包含PID、TGID、PGRP、SID):一个是全局的ID,保存在task_struct->pid中;另一个是局部的ID,即属于某个特定的命名空间的ID。对于task_struct->pids数组,数组编号的定义如下:

<pid.h>

1 enum pid_type 
2 { 
3     PIDTYPE_PID, 
4     PIDTYPE_PGID, 
5     PIDTYPE_SID, 
6     PIDTYPE_MAX 
7 };

 

        可以看到这里只定义了PID、PGID和SID,至于TGID(线程组ID)无非是线程组组长的ID,所以就不需要用特殊的结构来组织了。

        下面给出struct pid_link的结构:

<pid.h>

1 struct pid_link 
2 { 
3     struct hlist_node node; 
4     struct pid *pid; 
5 };

 

        pid_link包含两个成员,node用来组织struct task_struct和struct pid的关系,后面会降到,pid则指向对应的struct pid结构。可以看到,给出指定的task_struct,可以通过task_struct->pids[pid_type]->pid来找到对应的pid结构。下面给出struct pid结构定义:

<pid.h>

1 struct pid 
2 { 
3     atomic_t count; 
4     unsigned int level; 
5     /* lists of tasks that use this pid */ 
6     struct hlist_head tasks[PIDTYPE_MAX]; 
7     struct rcu_head rcu; 
8     struct upid numbers[1]; 
9 };

 

        其中count表示该pid结构被引用的次数,level表示该pid结构对应多少层的namespace,tasks则反向指向与该pid相连的task_struct,即上文所述pid_link->node就是组织在pid->tasks中的,rcu是将所有struct pid组织起来的辅助结构,numbers成员中存储的是struct upid结构,该结构是pid与pid_namespace相关联的结构。这里需要注意,虽然numbers成员数组大小只有1,其实这种定义只是说明了:struct pid中的numbers事实上是一个数组,其长度至少为1(即只有一层namespace的情况);对于pid属于多层namespace的情况,该数组是可以动态扩张的。struct pid定义如下:

<pid.h>

1 struct upid { 
2     /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ 
3     int nr; 
4     struct pid_namespace *ns; 
5     struct hlist_node pid_chain; 
6 };

 

        nr表示ID的数值,我们使用ls命令得到的进程的各种ID就是保存在这里,ns存储的是指向namespace的结构。此外,所有的upid都保存在一个散列表中,通过upid->pid_chain组织。散列表的表头定义在:

<kernel/pid.c>

1 static struct hlist_head *pidhash;

 

        对于upid指向的pid_namespace,定义如下:

<pid_namespace.h>

 1 struct pid_namespace { 
 2     struct kref kref; 
 3     struct pidmap pidmap[PIDMAP_ENTRIES]; 
 4     int last_pid; 
 5     struct task_struct *child_reaper; 
 6     struct kmem_cache *pid_cachep; 
 7     unsigned int level; 
 8     struct pid_namespace *parent; 
 9 #ifdef CONFIG_PROC_FS 
10     struct vfsmount *proc_mnt; 
11 #endif 
12 #ifdef CONFIG_BSD_PROCESS_ACCT 
13     struct bsd_acct_struct *bacct; 
14 #endif 
15 };

        其中,kref保存实例被引用的次数;pidmap是一个位图,保存该namespace中pid的分配情况;last_pid保存上一个分配的pid;对于每个namespace来说,都有一个进程来扮演Linux中init进程的角色,对所有的僵死进程执行wait4操作,child_reaper指向的就是这个进程(一般来说都是当前namespace中的0号进程);pid_cachep是一个kmem_cache对象,用来加速pid的分配,具体可以参考“Linux的物理内存管理”;level表示该namespace在整个命名空间的层次;parent表示该namespace的父namespace;proc_mnt表示在/proc文件系统的显示结构;bacct保存BSD进程加速的结构。后两者我们在这里不涉及。

        整个数据结构的组织如下图:

3710358[4]

        可以看到图分为四个部分:

  • 左下角的部分是PID namespace自己的组织结构,即是一个树形的结构。struct pid_namespace自成体系,除了child_reaper之外并没有更多和外部的联系。
  • 右下角是所有的struct upid组成的散列表,通过pidhash为表头的散列表组织,便于对upid的查找。
  • 右上角是使用pid的struct task_struct,不同的进程可能对应到同一个struct pid,例如同一个进程组的task_struct->pid[PIDTYPE_PGID],即同一进程组进程的进程组ID是相同的,指向同一个pid实例。
  • 中间处于最核心地位的是struct pid,可以看到struct pid可以通过其task成员数组来找到所有指向该pid的pid_link结构,从而找到对应的task_struct;此外struct pid还可以通过其numbers成员数组来获得该struct pid所属的不同层次的pid_namespace,二者的连接通过struct upid建立。

        总结来说,struct task_struct和其对应的struct pid通过struct pid_link连接,每个struct pid实例通过struct upid来和对应不同层次的struct pid_namespace连接。真正的PID值保存在struct upid结构中,这也是为什么需要将struct upid通过pidhash组织起来的原因。

六、getpid系统调用路径

        getpid调用到系统调用sys_getpid,定义在timer.c中的SYSCALL_DEFINE0(getpid),之后的调用路径为:

 

SYSCALL_DEFINE0(getpid)->task_tgid_vnr(current)->pid_vnr()->pid_nr_ns()

 

        最终获得当前进程所属命名空间看到的线程组leader的局部pid。可以通过上述路径的代码来得到一个简单的pid_namespace使用实例。由于代码很简单,在这里就不详细说明了。

七、Kernel中pid_namespace相关的函数
  • void attach_pid(struct task_struct *task, enum pid_type type, struct pid *pid);

    将新申请的struct pid实例连接到指定的task。

  • static inline struct pid *task_pid(struct task_struct *task);
    获得与task_struct相关联的PID的struct pid实例。
  • static inline struct pid *task_pgrp(struct task_struct *task);
    获得与task_struct相关联的PGRP的struct pid实例。
  • pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns);
    获得struct pid之后,从其numbers数组中的upid得到指定ns中的pid数值。
  • pid_t pid_vnr(struct pid *pid);
    获得struct pid实例在当前进程所属pid_namespace中的pid数值。
  • static inline pid_t pid_nr(struct pid *pid);
    获得struct pid全局的pid数值。
  • static inline pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
    pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

    static inline pid_t task_pgrp_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

    static inline pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
    分别表示获得进程在对应namespace中的PID/TGID/PGRP/SID的值。

  • struct pid *find_pid_ns(int nr, struct pid_namespace *ns);
    通过PID数值和指定namespace找到对应的struct pid结构。该函数的实现以来于pidhash组织的散列表。

  • find_task_by_pid_type_ns:通过pid数值、type、namespace找到对应的struct task_struct。
    find_task_by_pid_ns:通过pid数值和进程namespace找到对应的struct task_struct。
    find_task_by_vpid:根据pid数值在当前进程所处namespace(即局部的数字pid)来找到对应的进程。
    find_task_by_pid:根据pid数值在全局的namespace来找到对应的进程

  • struct pid *alloc_pid(struct pid_namespace *ns);
    在指定namespace中分配一个struct pid结构。

        我们来详细分析一下alloc_pid代码:

<pid.c>

 1 struct pid *alloc_pid(struct pid_namespace *ns) 
 2 { 
 3     struct pid *pid; 
 4     enum pid_type type; 
 5     int i, nr; 
 6     struct pid_namespace *tmp; 
 7     struct upid *upid; 
 8     // 在ns->pid_cachep指定的cache中分配struct pid 
 9     pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); 
10     if (!pid) 
11         goto out; 
12     // 对指定namespace中每一层 
13     // 通过alloc_pidmap函数分配在该层namespace对应的pid数值 
14     // 保存在upid的numbers数组中 
15     tmp = ns; 
16     for (i = ns->level; i >= 0; i--) { 
17         nr = alloc_pidmap(tmp); 
18         if (nr < 0) 
19             goto out_free; 
20         pid->numbers[i].nr = nr; 
21         pid->numbers[i].ns = tmp; 
22         tmp = tmp->parent; 
23     } 
24     // get_pid_ns增加对应ns的引用数 
25     get_pid_ns(ns); 
26     // 设置struct pid的层次 
27     pid->level = ns->level; 
28     // 设置struct pid的引用数 
29     atomic_set(&pid->count, 1); 
30     // 初始化pid->tasks数组 
31     for (type = 0; type < PIDTYPE_MAX; ++type) 
32         INIT_HLIST_HEAD(&pid->tasks[type]); 
33     // 将struct pid中numbers数组保存的upid添加到pidhash中 
34     upid = pid->numbers + ns->level; 
35     spin_lock_irq(&pidmap_lock); 
36     for ( ; upid >= pid->numbers; --upid) 
37         hlist_add_head_rcu(&upid->pid_chain, 
38             &pid_hash[pid_hashfn(upid->nr, upid->ns)]); 
39     spin_unlock_irq(&pidmap_lock); 
40 // 成功返回 
41 out: 
42     return pid; 
43 // 错误处理 
44 out_free: 
45     while (++i <= ns->level) 
46         free_pidmap(pid->numbers + i); 
47     kmem_cache_free(ns->pid_cachep, pid); 
48     pid = NULL; 
49     goto out; 
50 }

 

 

        文中图片来自《深入Linux内核架构》,代码来自Kernel-3.2.0-rc1。转载请注明出处。

 

posted on 2013-12-25 20:21  xelatex  阅读(3911)  评论(0编辑  收藏  举报