转自:http://www.cnblogs.com/hazir/p/linux_kernel_pid.html

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,那么我们可以设计这样的数据结构:

  1. struct task_struct {
  2. //...
  3. struct pid_link pids;
  4. //...
  5. };
  6.  
  7. struct pid_link {
  8. struct hlist_node node;
  9. struct pid *pid;
  10. };
  11.  
  12. struct pid {
  13. struct hlist_head tasks; //指回 pid_link 的 node
  14. int nr; //PID
  15. struct hlist_node pid_chain; //pid hash 散列表结点
  16. };

每个进程的 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 为组长的所有进程组内进程。数据结构如下:

  1. enum pid_type
  2. {
  3. PIDTYPE_PID,
  4. PIDTYPE_PGID,
  5. PIDTYPE_SID,
  6. PIDTYPE_MAX
  7. };
  8.  
  9. struct task_struct {
  10. //...
  11. pid_t pid; //PID
  12. pid_t tgid; //thread group id
  13.  
  14. struct task_struct *group_leader; // threadgroup leader
  15.  
  16. struct pid_link pids[PIDTYPE_MAX];
  17.  
  18. //...
  19. };
  20.  
  21. struct pid_link {
  22. struct hlist_node node;
  23. struct pid *pid;
  24. };
  25.  
  26. struct pid {
  27. struct hlist_head tasks[PIDTYPE_MAX];
  28. int nr; //PID
  29. struct hlist_node pid_chain; // pid hash 散列表结点
  30. };

上面 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 的结构了,如下:

  1. struct pid
  2. {
  3. unsigned int level;
  4. /* lists of tasks that use this pid */
  5. struct hlist_head tasks[PIDTYPE_MAX];
  6. struct upid numbers[1];
  7. };
  8.  
  9. struct upid {
  10. int nr;
  11. struct pid_namespace *ns;
  12. struct hlist_node pid_chain;
  13. };

在 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 的实例:

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

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

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

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

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

    1. pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
    2. {
    3. struct upid *upid;
    4. pid_t nr = 0;
    5.  
    6. if (pid && ns->level <= pid->level) {
    7. upid = &pid->numbers[ns->level];
    8. if (upid->ns == ns)
    9. nr = upid->nr;
    10. }
    11. return nr;
    12. }

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

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

  1. pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
  2. pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
  3. pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
  4. 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 实例。代码如下:

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

    1. struct task_struct *pid_task(struct pid *pid, enum pid_type type)
    2. {
    3. struct task_struct *result = NULL;
    4. if (pid) {
    5. struct hlist_node *first;
    6. first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
    7. lockdep_tasklist_lock_is_held());
    8. if (first)
    9. result = hlist_entry(first, struct task_struct, pids[(type)].node);
    10. }
    11. return result;
    12. }

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

  1. struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns);
  2. struct task_struct *find_task_by_vpid(pid_t vnr);
  3. struct task_struct *find_task_by_pid(pid_t vnr);

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

生成唯一的PID

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

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

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

  1. struct pid *alloc_pid(struct pid_namespace *ns)
  2. {
  3. struct pid *pid;
  4. enum pid_type type;
  5. int i, nr;
  6. struct pid_namespace *tmp;
  7. struct upid *upid;
  8.  
  9. tmp = ns;
  10. pid->level = ns->level;
  11. // 初始化 pid->numbers[] 结构体
  12. for (i = ns->level; i >= 0; i--) {
  13. nr = alloc_pidmap(tmp); //分配一个局部ID
  14. pid->numbers[i].nr = nr;
  15. pid->numbers[i].ns = tmp;
  16. tmp = tmp->parent;
  17. }
  18.  
  19. // 初始化 pid->task[] 结构体
  20. for (type = 0; type < PIDTYPE_MAX; ++type)
  21. INIT_HLIST_HEAD(&pid->tasks[type]);
  22.  
  23. // 将每个命名空间经过哈希之后加入到散列表中
  24. upid = pid->numbers + ns->level;
  25. for ( ; upid >= pid->numbers; --upid) {
  26. hlist_add_head_rcu(&upid->pid_chain,
  27. &pid_hash[pid_hashfn(upid->nr, upid->ns)]);
  28. upid->ns->nr_hashed++;
  29. }
  30.  
  31. return pid;
  32. }

参考资料

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

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


邮箱:haifenglinying#yahoo.cn (#->@)

个人主页:www.hazirguo.com

Linux 内核进程管理之进程ID【转】的更多相关文章

  1. Linux 内核进程管理之进程ID 。图解

    http://www.cnblogs.com/hazir/tag/kernel/ Linux 内核进程管理之进程ID   Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数 ...

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

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

  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. C# 模拟串口发送接收

    一.准备虚拟串口驱动工具 创建俩个虚拟串口,如图: 二.创建两个控制台程序 模拟串口的发送接收数据 1. 接收数据,代码如下: //遍历串行端口名称数组 foreach (string port in ...

  2. Spark:一个高效的分布式计算系统--转

    原文地址:http://soft.chinabyte.com/database/431/12914931.shtml 概述 什么是Spark ◆ Spark是UC Berkeley AMP lab所开 ...

  3. 【bzoj2906】颜色 分块

    题目描述 给定一个长度为N的颜色序列C,对于该序列中的任意一个元素Ci,都有1<=Ci<=M.对于一种颜色ColorK来说,区间[L,R]内的权值定义为这种颜色在该区间中出现的次数的平方, ...

  4. 转---Post/Redirect/Get pattern

    今天重新认识了Post/Redirect/Get pattern, 感谢hip-hop的session, 一下帮助我理清了概念和思路. 谈到pattern,首先要清楚它为了什么而产生: PRG (参见 ...

  5. php获取字符串的编码格式的方法(函数)

    // 检测字符的编码格式 $encode = mb_detect_encoding($string, array('ASCII','UTF-8','GB2312','GBK','BIG5')); ec ...

  6. 《Node入门》读书笔记——用Node.js开发一个小应用

    Android APP的开发告一段落,一个稳定的.实现了基本功能的APP已经交付用户使用了!我和老板交流了下,接下来准备转战Node.js了,而且一部分前端的功能也要做进去!哈哈哈~~~接下来要朝一个 ...

  7. Splitter Control for Dialog

    原文链接地址:https://www.codeproject.com/Articles/595602/Splitter-Control-for-Dialog Introduction Yes, tha ...

  8. 20135239益西拉姆 Linux内核分析 汇编一个简单的c程序并分析其指令过程

    益西拉姆+<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 第一周linux内核分析 学习笔记 一.计算机 ...

  9. bzoj2396: 神奇的矩阵(矩阵乘法+随机化)

    这题n三方显然会GG... 运用矩阵乘法的性质A*B*R=A*(B*R)=C*R,于是随机化出一个一列的R,就可以把复杂度降低成n方...大概率是不会错的 #include<iostream&g ...

  10. GTY's gay friends HDU - 5172 线段树

    GTY has nn gay friends. To manage them conveniently, every morning he ordered all his gay friends to ...