http://www.cnblogs.com/hazir/tag/kernel/

Linux 内核进程管理之进程ID

 

Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一。该数据结构在内核文件 include/linux/sched.h 中定义,在Linux 3.8 的内核中,该数据结构足足有 380 行之多,在这里我不可能逐项去描述其表示的含义,本篇文章只关注该数据结构如何来组织和管理进程ID的。

进程ID类型

要想了解内核如何来组织和管理进程ID,先要知道进程ID的类型:

  • 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。

PID 命名空间

命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为mount命名空间、UTS命名空间、IPC命名空间、用户命名空间、PID命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。命名空间有的还有层次关系,如PID命名空间,图1 为命名空间的层次关系图。

图1 命名空间的层次关系

在上图有四个命名空间,一个父命名空间衍生了两个子命名空间,其中的一个子命名空间又衍生了一个子命名空间。以PID命名空间为例,由于各个命名空间彼此隔离,所以每个命名空间都可以有 PID 号为 1 的进程;但又由于命名空间的层次性,父命名空间是知道子命名空间的存在,因此子命名空间要映射到父命名空间中去,因此上图中 level 1 中两个子命名空间的六个进程分别映射到其父命名空间的PID 号5~10。

命名空间增大了 PID 管理的复杂性,对于某些进程可能有多个PID——在其自身命名空间的PID以及其父命名空间的PID,凡能看到该进程的命名空间都会为其分配一个PID。因此就有:

  • 全局ID:在内核本身和初始命名空间中唯一的ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个PID,叫全局ID,保证在整个系统中唯一。
  • 局部ID:对于属于某个特定的命名空间,它在其命名空间内分配的ID为局部ID,该ID也可以出现在其他的命名空间中。

进程ID管理数据结构

Linux 内核在设计管理ID的数据结构时,要充分考虑以下因素:

  1. 如何快速地根据进程的 task_struct、ID类型、命名空间找到局部ID
  2. 如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct
  3. 如何快速地给新进程在可见的命名空间内分配一个唯一的 PID

如果将所有因素考虑到一起,将会很复杂,下面将会由简到繁设计该结构。

一个PID对应一个task_struct

如果先不考虑进程之间的关系,不考虑命名空间,仅仅是一个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。

图2 一个task_struct对应一个PID

图中还有两个结构上面未提及:

  • pid_hash[]: 这是一个hash表的结构,根据 pid 的 nr 值哈希到其某个表项,若有多个 pid 结构对应到同一个表项,这里解决冲突使用的是散列表法。这样,就能解决开始提出的第2个问题了,根据PID值怎样快速地找到task_struct结构体:

    • 首先通过 PID 计算 pid 挂接到哈希表 pid_hash[] 的表项
    • 遍历该表项,找到 pid 结构体中 nr 值与 PID 值相同的那个 pid
    • 再通过该 pid 结构体的 tasks 指针找到 node
    • 最后根据内核的 container_of 机制就能找到 task_struct 结构体
  • pid_map:这是一个位图,用来唯一分配PID值的结构,图中灰色表示已经分配过的值,在新建一个进程时,只需在其中找到一个为分配过的值赋给 pid 结构体的 nr,再将pid_map 中该值设为已分配标志。这也就解决了上面的第3个问题——如何快速地分配一个全局的PID。

至于上面的第1个问题就更加简单,已知 task_struct 结构体,根据其 pid_link 的 pid 指针找到 pid 结构体,取出其 nr 即为 PID 号。

进程ID有类型之分

如果考虑进程之间有复杂的关系,如线程组、进程组、会话组,这些组均有组ID,分别为 TGID、PGID、SID,所以原来的 task_struct 中pid_link 指向一个 pid 结构体需要增加几项,用来指向到其组长的 pid 结构体,相应的 struct pid 原本只需要指回其 PID 所属进程的task_struct,现在要增加几项,用来链接那些以该 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。

图3 增加ID类型的结构

关于上图有几点需要说明:

  • 图中省去了 pid_hash 以及 pid_map 结构,因为第一种情况类似;
  • 进程B和C的进程组组长为A,那么 pids[PIDTYPE_PGID] 的 pid 指针指向进程A的 pid 结构体;
  • 进程A是进程B和C的组长,进程A的 pid 结构体的 tasks[PIDTYPE_PGID] 是一个散列表的头,它将所有以该pid 为组长的进程链接起来。

再次回顾本节的三个基本问题,在此结构上也很好去实现。

增加进程PID命名空间

若在第二种情形下再增加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所示为其表示:

图4 增加PID命名空间之后的结构图

图中关于如果分配唯一的 PID 没有画出,但也是比较简单,与前面两种情形不同的是,这里分配唯一的 PID 是有命名空间的容器的,在PID命名空间内必须唯一,但各个命名空间之间不需要唯一。

至此,已经与 Linux 内核中数据结构相差不多了。

进程ID管理函数

有了上面的复杂的数据结构,再加上散列表等数据结构的操作,就可以写出我们前面所提到的三个问题的函数了:

获得局部ID

根据进程的 task_struct、ID类型、命名空间,可以很容易获得其在命名空间内的局部ID:

  1. 获得与task_struct 关联的pid结构体。辅助函数有 task_pid、task_tgid、task_pgrp和task_session,分别用来获取不同类型的ID的pid 实例,如获取 PID 的实例:

    static inline struct pid *task_pid(struct task_struct *task)
    {
    return task->pids[PIDTYPE_PID].pid;
    }

    获取线程组的ID,前面也说过,TGID不过是线程组组长的PID而已,所以:

    static inline struct pid *task_tgid(struct task_struct *task)
    {
    return task->group_leader->pids[PIDTYPE_PID].pid;
    }

    而获得PGID和SID,首先需要找到该线程组组长的task_struct,再获得其相应的 pid:

    static inline struct pid *task_pgrp(struct task_struct *task)
    {
    return task->group_leader->pids[PIDTYPE_PGID].pid;
    } static inline struct pid *task_session(struct task_struct *task)
    {
    return task->group_leader->pids[PIDTYPE_SID].pid;
    }
  2. 获得 pid 实例之后,再根据 pid 中的numbers 数组中 uid 信息,获得局部PID。

    pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
    {
    struct upid *upid;
    pid_t nr = 0; if (pid && ns->level <= pid->level) {
    upid = &pid->numbers[ns->level];
    if (upid->ns == ns)
    nr = upid->nr;
    }
    return nr;
    }

    这里值得注意的是,由于PID命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能,因此,函数中需要确保当前命名空间的level 小于等于产生局部PID的命名空间的level。
    除了这个函数之外,内核还封装了其他函数用来从 pid 实例获得 PID 值,如 pid_nr、pid_vnr 等。在此不介绍了。

结合这两步,内核提供了更进一步的封装,提供以下函数:

pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

从函数名上就能推断函数的功能,其实不外于封装了上面的两步。

查找进程task_struct

根据局部ID、以及命名空间,怎样获得进程的task_struct结构体呢?也是分两步:

  1. 获得 pid 实体。根据局部PID以及命名空间计算在 pid_hash 数组中的索引,然后遍历散列表找到所要的 upid, 再根据内核的 container_of 机制找到 pid 实例。代码如下:

    struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
    {
    struct hlist_node *elem;
    struct upid *pnr; //遍历散列表
    hlist_for_each_entry_rcu(pnr, elem,
    &pid_hash[pid_hashfn(nr, ns)], pid_chain) //pid_hashfn() 获得hash的索引
    if (pnr->nr == nr && pnr->ns == ns) //比较 nr 与 ns 是否都相同
    return container_of(pnr, struct pid, //根据container_of机制取得pid 实体
    numbers[ns->level]); return NULL;
    }
  2. 根据ID类型取得task_struct 结构体。

    struct task_struct *pid_task(struct pid *pid, enum pid_type type)
    {
    struct task_struct *result = NULL;
    if (pid) {
    struct hlist_node *first;
    first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
    lockdep_tasklist_lock_is_held());
    if (first)
    result = hlist_entry(first, struct task_struct, pids[(type)].node);
    }
    return result;
    }

内核还提供其它函数用来实现上面两步:

struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns);
struct task_struct *find_task_by_vpid(pid_t vnr);
struct task_struct *find_task_by_pid(pid_t vnr);

具体函数实现的功能也比较简单。

生成唯一的PID

内核中使用下面两个函数来实现分配和回收PID的:

static int alloc_pidmap(struct pid_namespace *pid_ns);
static void free_pidmap(struct upid *upid);

在这里我们不关注这两个函数的实现,反而应该关注分配的 PID 如何在多个命名空间中可见,这样需要在每个命名空间生成一个局部ID,函数 alloc_pid 为新建的进程分配PID,简化版如下:

struct pid *alloc_pid(struct pid_namespace *ns)
{
struct pid *pid;
enum pid_type type;
int i, nr;
struct pid_namespace *tmp;
struct upid *upid; tmp = ns;
pid->level = ns->level;
// 初始化 pid->numbers[] 结构体
for (i = ns->level; i >= 0; i--) {
nr = alloc_pidmap(tmp); //分配一个局部ID
pid->numbers[i].nr = nr;
pid->numbers[i].ns = tmp;
tmp = tmp->parent;
} // 初始化 pid->task[] 结构体
for (type = 0; type < PIDTYPE_MAX; ++type)
INIT_HLIST_HEAD(&pid->tasks[type]); // 将每个命名空间经过哈希之后加入到散列表中
upid = pid->numbers + ns->level;
for ( ; upid >= pid->numbers; --upid) {
hlist_add_head_rcu(&upid->pid_chain,
&pid_hash[pid_hashfn(upid->nr, upid->ns)]);
upid->ns->nr_hashed++;
} return pid;
}

参考资料

  • 深入Linux 内核架构(以前不觉得这本书写得多好,现在倒发现还不错,本文很多都是照抄上面的)
  • 周徐达师弟的PPT(让我受益匪浅的一次讨论,周由浅入深告诉我们该数据结构是如何设计出来的,本文主思路就是按照该PPT,在此 特别感谢!)

本博客的内容如果没有标注转载字样,均属个人原创!欢迎学习交流,如果觉得有价值,欢迎转载,转载请注明出处,谢谢!

Linux 内核进程管理之进程ID 。图解的更多相关文章

  1. Linux 内核进程管理之进程ID

    Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一.该数据结构 ...

  2. Linux 内核进程管理之进程ID【转】

    转自:http://www.cnblogs.com/hazir/p/linux_kernel_pid.html Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构, ...

  3. Linux内核进程管理

    介绍: 在Linux的内核的五大组成模块中,进程管理模块时很重要的一部分.它尽管不像内存管理.虚拟文件系统等模块那样复杂.也不像进程间通信模块那样条理化,但作为五大内核模块之中的一个,进程管理对我们理 ...

  4. 【Android手机测试】linux内存管理 -- 一个进程占多少内存?四种计算方法:VSS/RSS/PSS/USS

    在Linux里面,一个进程占用的内存有不同种说法,可以是VSS/RSS/PSS/USS四种形式,这四种形式首字母分别是Virtual/Resident/Proportional/Unique的意思. ...

  5. Linux内存管理 一个进程究竟占用多少空间?-VSS/RSS/PSS/USS

    关键词:VSS.RSS.PSS.USS._mapcount.pte_present.mem_size_stats. 在Linux里面,一个进程占用的内存有不同种说法,可以是VSS/RSS/PSS/US ...

  6. Linux基础命令---查找进程id

    pidof pidof可以查找指定名称的进程的pid,将结果送到标准输出.pidof有两种返回值:0,找到至少一个进程:1,没有找到进程.pidof实际上与killall5相同:程序根据调用它的名称进 ...

  7. linux权限管理之进程掩码

    进程掩码 mask umask ======================================================== 文件权限管理之: 进程umask进程 新建文件.目录的 ...

  8. Linux服务器管理: 系统管理:进程文件信息lsof

    lsof命令 列出进程打开或使用的文件信息 [root@loclahost/]#lsof [选项] 选项: -c 字符串: 只列出以字符串开头的进程打开的文件 -u 用户名: 只列出某个用户的进程打开 ...

  9. 查看linux端口对应的进程id

    例如:查看占用4040端口的进程 ss -lptn 'sport = :4040'

随机推荐

  1. ubuntu使用中的一些问题

    ubuntu如何修改计算机名? sudo gedit /etc/hostname 如何查看ubuntu操作系统位数 方法1: #查看long的位数,返回32或64 getconf LONG_BIT 方 ...

  2. WCF - Creating WCF Service

    http://www.tutorialspoint.com/wcf/wcf_creating_service.htm Creating a WCF service is a simple task u ...

  3. HTML本地测试成功后上传博客注意事项

    需要注意不要跟博客已经存在的样式(CSS)或功能(JavaScript)起冲突 功能名一定不要一样 样式名尽量不一样 如果样式名一样,存在属性名的对应属性值尽量跟博客内相同

  4. POJ 3159 Candies 解题报告(差分约束 Dijkstra+优先队列 SPFA+栈)

    原题地址:http://poj.org/problem?id=3159 题意大概是班长发糖果,班里面有不良风气,A希望B的糖果不比自己多C个.班长要满足小朋友的需求,而且要让自己的糖果比snoopy的 ...

  5. JS模块化规范CommonJS,AMD,CMD

    模块化是软件系统的属性,这个系统被分解为一组高内聚,低耦合的模块.理想状态下我们只需要完成自己部分的核心业务逻辑代码,其他方面的依赖可以通过直接加载被人已经写好模块进行使用即可.一个模块化系统所必须的 ...

  6. epub显示特殊字体

    You need to open the ePub in an archive program (they are just ZIP files) and add an XML file to the ...

  7. epub3 in action: epub3文件格式简介

    epub3文件就是一个符合epub3标准,以epub为扩展名的zip压缩文件.epub3标准则是基于html5.css3.svg等web标准以及mathML等来展示内容.下图就是一个简单epub3文件 ...

  8. 用友U8账套的建立

      第1步点击开始菜单进入系统管理模块   第2步点击系统菜单下的注册   第3步弹出登录系统对话框,操作员输入admin点确定   第4步点击权限菜单下的用户   第5步进入用户管理窗口,点击工具栏 ...

  9. XML常用颜色值

    <?xml version="1.0" encoding="utf-8" ?> <resources> <color name=& ...

  10. jquery 列求和

    列求和 var m = 0; $('#tb tr').each(function () { //td:eq(3)从0开始计数 $(this).find('td:eq(3)').each(functio ...