1 概述
Pid namespace是对进程pid的容器虚拟化,从pid的维度实现容器间的隔离。即在一个容器中只能看到属于该pidns的pid,从而在某种程度上实现了进程间的隔离。
在容器中只能看到容器内的pid,但在宿主机上可以看到所有进程的pid,所以从看到的pid号的角度,这是有层级关系的,对应的,pidns在实现上也是有层级关系的,在高层次pidns中可以看到低层次pidns的信息,反之则不行。一个简单的示意图如下所示:
通过箭头连接的,表示同一个进程在不同的pidns中的pid表示。
说明:文本的代码基于Linux 3.4。
2 Pidns的管理
2.1 关键数据结构
通过上面的描述,在引入pidns后,要获取一个指定的进程,除了通过pid号外,还必须指明pidns,这样才可以确定唯一的进程。Pidns是如何管理来实现这一目的的,这主要通过pid_namespace结构体实现。
structpid_namespace {
struct kref kref; // 引用计数
struct pidmap pidmap[PIDMAP_ENTRIES]; // pid分配的bitmap,为1表示已分配
int last_pid; // 记录上次分配的pid,默认当前分配的pid=last_pid+1
unsigned int nr_hashed;
struct task_struct *child_reaper; // 父进程结束后,需要该child_reaper进程对其托管
struct kmem_cache *pid_cachep; // 用于分配pid结构的slab缓存
unsigned int level; // 记录该pidns的深度
struct pid_namespace *parent; // 父pidns
#ifdefCONFIG_PROC_FS
struct vfsmount *proc_mnt;
#endif
#ifdefCONFIG_BSD_PROCESS_ACCT
struct bsd_acct_struct *bacct;
#endif
struct work_struct proc_work;
gid_t pid_gid;
int hide_pid;
int reboot; /* group exit code if this pidns was rebooted */
};
这里比较重要的成员变量就是pidmap,它表示在该pidns中pid的分配情况,每pidns中独立的pidmap就决定了在一个pidns中,进程的pid是可以重新从1开始计数的。这是一种典型的位图的应用,在32位linux中一页的大小是 4k=4*1028*8位=32768位,所以一个page可以存储32768个bit位,而linux中默认pid的最大值也是32768,所以正好可以用一个page来表示进程pid的分配情况。
struct pidmap {
atomic_t nr_free; // 该bitmap中还有多少位为0,也就是还能分配多少pid
void *page; // 存储该bitmap的page
};
但是如果系统中最大的pid超过了32768,那个一个page就无法表示所有的pid分配情况了,所以在pid_namespace中,pidmap是一个数组,就是为了出现这种情况时,可以用多个page来存储pid的位图。
一个进程对应一个task struct,但是这个进程在多个pidns中可以看到不同的pid,对于这些pid的管理,主要通过两个结构体来实现。
struct pid
{
atomic_t count;
unsigned int level; // 这个pid的深度
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX]; // 使用这个pid的进程链表
struct rcu_head rcu;
struct upid numbers[1]; // 这个pid在不同命名空间中的显示
};
这里最重要的成员变量就是numbers数组,它表示一个进程在每个namespace里的id,这里的id就是getpid()所得到的值。Numbers[0]表示最顶层的namespace,level=0,numbers[1]表示level=1的namespace,依此类推。
这里numbers数组的长度为1,但是因为这是结构体的最后一个成员,所以通过在扩展结构体的存储空间大小,就可以无限制的根据需要扩展数组的长度,这种根据需要动态设置数组长度,在linux内核中是一种比较常见的方式。因为这里有一个成员level表示pid的深度,所以这里numbers数组的实际长度就会是level。
另外,tasks[PIDTYPE_MAX]链表头也是比较容易产生困惑的地方。根据上面的理解,我们不难得出这样的分析:内核中的进程通过唯一的task_struct标示,但这个进程可以在不同的pidns中体现为不同的pid,这些pid用pid结构体来管理,所以一个进程对应唯一的一个pid结构体。但是为什么一个pid结构体会被多个进程使用呢?这实际上是内核中一些机制带来的副作用:父进程fork出子线程,然后子线程去调用exec,在这调用exec函数的过程中,首先子线程发信号使得父进程停止,子线程去attach父进程pid结构,最后再release父进程,在段代码中,父进程和子线程会共用一个pid结构。因为有这种可能性的存在,所以pid结构体也不得不处理这样的情况。
再来看下upid的结构,
struct upid {
/* Try to keep pid_chain in the samecacheline as nr for find_vpid */
int nr; //pid的数值
struct pid_namespace *ns; // 所在命名空间
struct hlist_node pid_chain; // 链表节点
};
Upid的结构很简单,这里n