Linux内核中namespace之PID namespace

时间:2021-03-16 23:31:14

前面看了LInux PCI设备初始化,看得有点晕,就转手整理下之前写的笔记,同时休息一下!!~(@^_^@)~


这片文章是之前写的,其中参考了某些大牛们的博客!!

PID框架的设计

一个框架的设计会考虑很多因素,相信分析过Linux内核的读者来说会发现,内核的大量数据结构被哈希表和链表链接起来,最最主要的目的就是在于查找。可想而知一个好的框架,应该要考虑到检索速度,还有考虑功能的划分。那么在PID框架中,需要考虑以下几个因素.

  • 如何通过task_struct快速找到对应的pid

  • 如何通过pid快速找到对应的task_struct

  • 如何快速的分配一个唯一的pid

这些都是PID框架设计的时候需要考虑的一些基本的因素。也正是这些因素将PID框架设计的愈加复杂。

原始的PID框架

先考虑的简单一点,一个进程对应一个pid

struct task_struct {

  .....

  pid_t pid;

  .....

}

引入hlist和pid位图

struct task_struct *pidhash[PIDHASH_SZ];

这样就很方便了,再看看PID框架设计的一些因素是否都满足了,如何分配一个唯一的pid呢,连续递增?,那么前面分配的进程如果结束了,那么分配的pid就需要回收掉,直到分配到PID的最大值,然后从头再继续。好吧,这或许是个办法,但是是不是需要标记一下那些pid可用呢?到此为此这看起来似乎是个解决方案,但是考虑到这个方案是要放进内核,开发linux的那帮家伙肯定会想近一切办法进行优化的,的确如此,他们使用了pid位图,但是基本思想没有变,同样需要标记pid是否可用,只不过使用pid位图的方式更加节约内存.想象一下,通过将每一位设置为0或者是1,可以用来表示是否可用,第1位的0和1用来表示pid为1是否可用,以此类推.到此为此一个看似还不错的pid框架设计完成了,下图是目前整个框架的整体效果.

Linux内核中namespace之PID namespace

用上面大牛的引言来引入今天的主题:

其实PID namespace带来的好处远不止于此,其中最重要的就是对轻量级虚拟化的支持。当前炙手可热的docker就是基于Linux 内核中命名空间的原理。有了PID命名空间,我们就可以保证进程的隔离性,至少在进程的角度,可以按照namespace的方式去组织,不同namespace的进程互不干扰。这也是笔者要分析下底层虚拟化支持的初衷之一!

言归正传:

引入进程PID命名空间后的PID框架

随着内核不断的添加新的内核特性,尤其是PID Namespace机制的引入,这导致PID存在命名空间的概念,并且命名空间还有层级的概念存在,高级别的可以被低级别的看到,这就导致高级别的进程有多个PID,比如说在默认命名空间下,创建了一个新的命名空间,占且叫做level1,默认命名空间这里称之为level0,在level1中运行了一个进程在level1中这个进程的pid为1,因为高级别的pid namespace需要被低级别的pid namespace所看见,所以这个进程在level0中会有另外一个pid,为xxx.套用上面说到的pid位图的概念,可想而知,对于每一个pid namespace来说都应该有一个pidmap,上文中提到的level1进程有两个pid一个是1,另一个是xxx,其中pid为1是在level1中的pidmap进行分配的,pid为xxx则是在level0的pidmap中分配的. 下面这幅图是整个pidnamespace的一个框架

Linux内核中namespace之PID namespace

可以看到这里出现了一个层次结构,即一个命名空间可以有其子命名空间,子命名空间中的进程同样会出现在父命名空间中,只是进程ID是不同的。看PID的结构:

 struct pid
{
atomic_t count;
unsigned int level;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct rcu_head rcu;
struct upid numbers[];
};

count表示这个PID的引用计数,同一个PID结构可以为多个进程所共享;

level表示该PID所在的层级;

tasks是一个HASH数组,每一项都是一个链表头。分别是PID链表头,进程组ID表头,会话ID表头;

rcu用于保护指针引用;

numbers是一个UPID数组,记录对应层级的命名空间中的UPID,所以可以想到,该PID处于第几层,那么这个数组应该有几项(当然都是从0开始)。

看下UPID结构:

 struct upid {
/* Try to keep pid_chain in the same cacheline as nr for find_vpid */
int nr;
struct pid_namespace *ns;
struct hlist_node pid_chain;
};

UPID相比之下就简单的多,或者说这才是真正的PID。

nr表示ID号;

namespace指向该UPID所在命名空间的namespace结构;

pid_chain是一个链表,系统会把UPID 的nr和namespace的ns经过某种hash,得到在一个全局的Hash数组中的下标,此UPID便会加入到对应下标的链表中。

看下进程中对于PID是如何应用的:

 struct task_struct{
...
struct pid_link pids[PIDTYPE_MAX];
...
}

进程中包含一个pid_link结构的数组,我们还是先看一个pid_link结构:

 struct pid_link
{
struct hlist_node node;
struct pid *pid;
};

node作为一个节点加入到所有引用同一PID结构的进程链表中;

pid指向该进程引用的PID结构。

也许到这里还是显得关系有点紊乱,那么看一下下面的图:

Linux内核中namespace之PID namespace

没错,这个图是我画的!!哈哈,可花了我不少时间了,希望对大家理解进程结构和PID以及UPID之间的关系有所帮助。贴出这个图突然发现没有什么好解释的了,各种关系图中已经表明。不过需要注意的是最下面的UPID我仅仅用一个框框代表了,其实是Numbers指向UPID结构,而让UPID加入到链表中的是结构中的pid_chain;还有一个问题就是UPID那一组链表并不是表示同一PID下的所有UPID,根据内核源代码,这里只是处理冲突的一种方式,而这里画成链表仅仅是为了表示这里是以链表存在的。

下面说下PID位图:

每一个namespace都对应一个map,用于分配pid号。这里包含两个成员,nr_free表示可用的ID号数量,第二个是一个指向一个页面的指针。这么设计有其自身的合理性。默认情况下page指向一个页面,而一个页面是4kb的大小,即4096个字节,也就是4096*8bit,每一位表示一个pid号,一个页面就可以表示32768个进程ID,普通情况下绝对是够了,即使特殊情况不够,那么还可以动态扩展,这就是其合理性所在。

 struct pidmap {
atomic_t nr_free;
void *page;
};

这里是pid_namespace结构:

 struct pid_namespace {
struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES];//命名空间对应的PID位图
int last_pid;//上次分配的PID,便于下次分配
unsigned int nr_hashed;
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;
#endif
#ifdef CONFIG_BSD_PROCESS_ACCT
struct bsd_acct_struct *bacct;
#endif
struct user_namespace *user_ns;
struct work_struct proc_work;
kgid_t pid_gid;
int hide_pid;
int reboot; /* group exit code if this pidns was rebooted */
unsigned int proc_inum;
};

下一节会结合LInux内核源代码分析下PID的具体分配情况