PID:这是 Linux 中在其命名空间中唯一标识进程而分配给它的一个号码,称做进程ID号,简称PID。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值。
TGID:在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程,它们处于一个线程组,该线程组的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID;线程组组长的TGID与其PID相同;一个进程没有使用线程,则其TGID与PID也相同。
PGID:另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作,例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。
SID:几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID。
命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为mount命名空间、UTS命名空间、IPC命名空间、用户命名空间、PID命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。命名空间有的还有层次关系,如PID命名空间,图1 为命名空间的层次关系图。
上图有四个命名空间,图中已经表明的映射关系。
命名空间增大了 PID 管理的复杂性,对于某些进程可能有多个PID——在其自身命名空间的PID以及其父命名空间的PID,凡能看到该进程的命名空间都会为其分配一个PID。因此就有:
全局ID:在内核本身和初始命名空间中唯一的ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个PID,叫全局ID,保证在整个系统中唯一。
局部ID:对于属于某个特定的命名空间,它在其命名空间内分配的ID为局部ID,该ID也可以出现在其他的命名空间中。
我们要解决的问题:
1.如何根据task_struct ID类型,命名空间找到局部ID?
2.如何根据局部ID,命名空间,ID找到对应的task_struct?
3.如何给一个新的进程找到一个可分配的进程ID?
我们先来看简单问题:一个PID对应一个task_struct:
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 散列表结点
};
每个进程的 task_struct 结构体中有一个指向 pid 结构体的指针,pid 结构体包含了 PID 号。结构示意图如图2
当我们拿到一个进程的PID的时候,我们如何得到它的task_struct?
1.首先计算这个PID的hash,然后在pid_hash[]中寻找这个PID的结构体,如果冲突,则使用拉链法往下继续寻找。
2.找到后,使用该pid结构体的tasks指针找到node,图中的虚线部分
3.使用内核的container_of机制找到task_struct结构体。
此时,需要在pid_link结构体中增加几项,用来指向组长的pid结构体,相应的pid结构体也需要链接那些以该pid为组长的进程,数据结构如下:
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 散列表结点
};
上面 ID 的类型 PIDTYPE_MAX 表示 ID 类型数目。之所以不包括线程组ID,是因为内核中已经有指向到线程组的 task_struct 指针 group_leader,线程组 ID 无非就是 group_leader 的PID。
假如现在有三个进程A、B、C为同一个进程组,进程组长为A,这样的结构示意图如图3。
上图中,还是和没有进程组ID等区别主要在:
每个task_struct增加了指向其组长pid的指针,组长pid中增加了指向组员进程的pid_link指针。
若在第二种情形下再增加PID命名空间,一个进程就可能有多个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;
};
在 pid 结构体中增加了一个表示该进程所处的命名空间的层次level,以及一个可扩展的 upid 结构体。对于struct upid,表示在该命名空间所分配的进程的ID,ns指向是该ID所属的命名空间,pid_chain 表示在该命名空间的散列表。
举例来说,在level 2 的某个命名空间上新建了一个进程,分配给它的 pid 为45,映射到 level 1 的命名空间,分配给它的 pid 为 134;再映射到 level 0 的命名空间,分配给它的 pid 为289,对于这样的例子,如图4所示为其表示:
图中关于如果分配唯一的 PID 没有画出,但也是比较简单,与前面两种情形不同的是,这里分配唯一的 PID 是有命名空间的容器的,在PID命名空间内必须唯一,但各个命名空间之间不需要唯一。
至此,已经与 Linux 内核中数据结构相差不多了。