LinuxPID命名空间学习
通过对于前两节的学习我们知道Linux内核使用task_struct结构来表示和管理进程,这个数据结构里面存放了很多有关于PID如何管理的数据,可以这么说,Linux内核所有有关进程管理的数据结构都和此数据结构有关。该数据结构存放在include/linux/sched.h头文件里,并且这个数据结构比较大,就不一一列举了。实际上一一列举也没有什么意思,因这个数据结构过于庞大导致列举出来连笔者都感到头大----可以举出这么一个例子:这个数据结构占用内核1.7K的空间,可想有多么庞大。但是不需要担心,可以把此数据结构分块划分的话,就简单很多了。话不多说我会根据如下来结构体系来介绍:
1. 进程类型
2. PID的命名空间;
3.管理 PID有关的数据结构
进程类型
进程如何划分呢?我们可以把进程划分为如下四种类型:
1.普通PID:
这是Linux对于每一个进程都使用的划分,每一个进程都分配给一个PID,每一个PID都对应一个task_struct,每一个task_struct对应着相应的命名空间,和PID类型(先做了解)。
2.TGID:
线程组ID,这个是线程的定义,这个定义在clone时候使用CLONE_THREAD函数调用的时候进行:在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程,它们处于一个线程组,该线程组的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID;线程组组长的TGID与其PID相同;一个进程没有使用线程,则其TGID与PID也相同。
3.PGID:
另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作,例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。
4.SID:
几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID。
PID命名空间
命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为mount命名空间、UTS命名空间、IPC命名空间、用户命名空间、PID命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。命名空间有的还有层次关系,如PID命名空间,图1 为命名空间的层次关系图。
在上图有四个命名空间,一个父命名空间衍生了两个子命名空间,其中的一个子命名空间又衍生了一个子命名空间。以PID命名空间为例,由于各个命名空间彼此隔离,所以每个命名空间都可以有 PID 号为 1 的进程;但又由于命名空间的层次性,父命名空间是知道子命名空间的存在,因此子命名空间要映射到父命名空间中去,因此上图中 level 1 中两个子命名空间的六个进程分别映射到其父命名空间的PID 号5~10。
命名空间增大了 PID 管理的复杂性,对于某些进程可能有多个PID——在其自身命名空间的PID以及其父命名空间的PID,凡能看到该进程的命名空间都会为其分配一个PID。因此就有:
全局ID:
在内核本身和初始命名空间中具有唯一的ID表示唯一的进程。内核中的init进程就是初始命名空间。系统为每一个进程分配了ID号来标示不同的进程,无论是在一级命名空间还是在二级命名空间中的进程,都在初始命名空间进程中都申请了一个ID号用于管理。这样在父命名空间中就可以看到子命名空间中的进程了。
局部ID
也可以说是子命名空间中看到的ID号,这个ID只能在子命名空间中有用,在父命名空间中没有作用。
进程ID管理数据结构
Linux 内核在设计管理ID的数据结构时,要充分考虑以下因素:
1.如何快速地根据进程的 task_struct、ID类型、命名空间找到局部ID
2.如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct
3.如何快速地给新进程在可见的命名空间内分配一个唯一的 PID
如果将所有因素考虑到一起,将会很复杂,下面将会由简到繁设计该结构。
介绍的结构如下:
1. 一个PID只对应一个task_struct结构;
2. 加入TGID/PGID/SID管理的PID管理;
3. 加入命名空间的PID管理
一个PID对应一个task_struct结构
如果不考虑一个进程对应的TGID/PGID/SID,也不考虑一个进程对应的命名空间设计,我们可以对于进程的数据结构进行如下设计:
一个进程对应了一个task_struct结构,其中每一个PID 中的nr表示PID号即为进程号,PID 结构中的pid_chain代表PID散列表的节点。
上述设计核心思想如下:
1.一个task_struct 中存放着pid_link结构体,指向struct pid结构。
2.PID结构里面存放着PID 号(即为nr),也存放着指向pid_link的指针和PID散列表的节点的节点。
3.每一个PID的申请和释放都是通过pid_hash(PID散列表)和pid_map来进行管理的。
4.对于每一个PID的查找也是通过pid_hash来管理的;
数据结构如下:
struct task_struct {
//...
struct pid_link pids;
//...
};
struct pid_link {
struct hlist_node node;
struct pid *pid;
};
struct pid {
struct hlist_head tasks; //指回 pid_link 的 node
int nr; //PID
struct hlist_node pid_chain; //pid hash 散列表结点
};
上述两个主要的数据结构还有两个没有介绍:
pid_hash[]:是PID的hash散列表,用于管理和查找pid结构,主要通过pid号来进行关键索引找到PID结构。然后找到task_struct结构,主要查找有一下四步:
1. 通过PID号,索引pid_hash[],找到struct pid的pid_chain结构;
2. 通过pid_chain找到struct pid结构;
3. struct pid结构中有tasks指向,task_struct->plink.node;
4. 通过container_of查找到struct task_struct结构;
pid_map:PID位图表示,主要用于申请和释放未使用的PID号,这样不用遍历pid_hash结构也能够找到未使用的PID号。
加入TGID/PGID/SID管理的PID管理
加入TGID/PGID/SID管理的PID管理稍微比较复杂一点,在上述基础上我们知道struct pid结构可以索引到task_struct结构,但是如何加入TGID/PGID/SID的管理呢?可以从如下角度考虑下问题:
1. 如何通过进程本身的task_struct 索引到自身的所属TGID/PGID/SID的struct PID 结构?
2. 一个线程组或者一个进程组或者一个组里面的线程主ID或者进程组ID或者组ID怎么索引到其下所有的线程,进程,组中所有的进程?
以上两点的解决方案如下图:
ruc
对于上述的两点疑问可以做如下解答:
1. 在task_struct结构里面增加struct pid_link数组到至少四个,第一个索引自身进程的struct pid结构,第二个索引线程组ID,第三个索引PGID,第四个索引SID。可以通过一次索引查询到自身task_struct的PID,TGID,PGID,SID结构的task_struct结构。
2. 每一个自身的进程的PID结构把tasks数组增加至至少四个,第一个索引自身的task_struct ,第二个索引以自己为主进程的下面挂载多少个线程的task_struct,第三个索引以自己为主进程下面挂载多少个gid..... 其中hlist_head tasks[index]以struct task_struct->pid_link[index].hlist_node为节点.
数据结构设计如下:
enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};
struct task_struct {
//...
pid_t pid; //PID
pid_t tgid; //thread group id
struct task_struct *group_leader; // threadgroup leader
struct pid_link pids[PIDTYPE_MAX];
//...
};
struct pid_link {
struct hlist_node node;
struct pid *pid;
};
struct pid {
struct hlist_head tasks[PIDTYPE_MAX];
int nr; //PID
struct hlist_node pid_chain; // pid hash 散列表结点
};
**
增加PID命名空间的PID表示结构
**
再回到PID命名空间的讨论范畴,通过本文中对于PID命名空间的介绍我们知道每一个命名空间中其PID分配是相对独立的,在父命名空间中可以看到子命名空间中的进程,父命名空间中看到的进程号是父命名空间分配的,子命名空间中看到的进程号是子命名空间分配的。
可能会有一下疑问:
1. 子命名空间中的进程怎么会索引到父命名空间中?
2. 子命名空间中怎么会感知父命名空间的存在呢?
3. 父命名空间如何会知道子命名空间中的进程呢?
4. 父命名空间中的进程如何给子命名空间中的进程分配PID呢?
为了回答以上问题,我们先从第四个问题开始讨论:
为了使父命名空间给子命名空间中的进程分配进程号,Linux内核在命名空间设计中把pid_map结构放入到命名空间中结构,这样每一个父命名空间中的结构就能够给子命名空间中的进程分配ID了,如下所示:
struct pid_namespace {
struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES];
int last_pid;
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;
#endif
#ifdef CONFIG_BSD_PROCESS_ACCT
struct bsd_acct_struct *bacct;
#endif
};
以上数据结构还回答了我们第二个问题:子命名空间怎么感知父命名空间的存在
父命名空间如何会知道子命名空间中的进程呢?
我们知道同一个父命名空间中的进程ID和子命名空间中的ID互相不影响,而且敷命名空间的和子命名空间是不相同的,这样我们就可以在设计数据结构的时候把两者设计在一起,只要找到设计的数据结构就可以通过pid_hash得到struct upid结构。
上面提到的pid_hash索引的功能有所变化,之前提交通过pid_hash表通过PID值可以索引到struct pid结构,但是现在我们通过hash表先索引到struct upid结构,再通过upid结构和namespace的level值所引到pid结构。进一步所引到task_struct结构
如下所示:
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;
};
以下图片会帮助我们回答以上所有问题,举例来说,在level 2 的某个命名空间上新建了一个进程,分配给它的 pid 为45,映射到 level 1 的命名空间,分配给它的 pid 为 134;再映射到 level 0 的命名空间,分配给它的 pid 为289,对于这样的例子,如图4所示为其表示:
图中关于如果分配唯一的 PID 没有画出,但也是比较简单,与前面两种情形不同的是,这里分配唯一的 PID 是有命名空间的容器的,在PID命名空间内必须唯一,但各个命名空间之间不需要唯一。
数据结构设计如下:
struct pid
{
unsigned int level;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct upid numbers[1];
};
struct upid {
int nr;
struct pid_namespace *ns;
struct hlist_node pid_chain;
};