linux命名空间(namespace)学习(三)

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 为命名空间的层次关系图。

图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怎么索引到其下所有的线程,进程,组中所有的进程?

以上两点的解决方案如下图:

加入TGID/PGID/SID管理的PID管理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表索引到我们要找的进程的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命名空间内必须唯一,但各个命名空间之间不需要唯一。
以上摘自:https://www.cnblogs.com/hazir/p/linux_kernel_pid.html

猜你喜欢

转载自blog.csdn.net/weixin_37867857/article/details/84889559