Pid
Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一。
该数据结构在内核文件include/linux/sched.h中定义,在目前最新的 Linux-4.5(截至目前的日期为 2016-05-11)的内核中,该数据结构足足有 380 行之多,在这里我不可能逐项去描述其表示的含义,本篇文章只关注该数据结构如何来组织和管理进程 ID 的。
进程 ID 概述
进程 ID 类型
要想了解内核如何来组织和管理进程 ID,先要知道进程 ID 的类型:
内核中进程 ID 的类型用pid_type来描述,它被定义在include/linux/pid.h中
|
|
- PID 内核唯一区分每个进程的标识
pid 是 Linux 中在其命名空间中唯一标识进程而分配给它的一个号码,称做进程 ID 号,简称 PID。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的 PID 值
这个 pid 用于内核唯一的区分每个进程
注意它并不是我们用户空间通过 getpid( )所获取到的那个进程号,至于原因么,接着往下看
- TGID 线程组(轻量级进程组)的 ID 标识
在一个进程中,如果以 CLONE_THREAD 标志来调用 clone 建立的进程就是该进程的一个线程(即轻量级进程,Linux 其实没有严格的进程概念),它们处于一个线程组,
该线程组的所有线程的 ID 叫做 TGID。处于相同的线程组中的所有进程都有相同的 TGID,但是由于他们是不同的进程,因此其 pid 各不相同;线程组组长(也叫主线程)的 TGID 与其 PID 相同;一个进程没有使用线程,则其 TGID 与 PID 也相同。
- PGID
另外,独立的进程可以组成进程组(使用 setpgrp 系统调用),进程组可以简化向所有组内进程发送信号的操作
例如用管道连接的进程处在同一进程组内。进程组 ID 叫做 PGID,进程组内的所有进程都有相同的 PGID,等于该组组长的 PID。
- SID
几个进程组可以合并成一个会话组(使用 setsid 系统调用),可以用于终端程序设计。会话组中所有进程都有相同的 SID,保存在 task_struct 的 session 成员中
PID 命名空间
pid 命名空间概述
命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为 mount 命名空间、UTS 命名空间、IPC 命名空间、用户命名空间、PID 命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。
关于命名空间的详细信息,请参见
命名空间有的还有层次关系,如 PID 命名空间
在上图有四个命名空间,一个父命名空间衍生了两个子命名空间,其中的一个子命名空间又衍生了一个子命名空间。以 PID 命名空间为例,由于各个命名空间彼此隔离,所以每个命名空间都可以有 PID 号为 1 的进程;但又由于命名空间的层次性,父命名空间是知道子命名空间的存在,因此子命名空间要映射到父命名空间中去,因此上图中 level 1 中两个子命名空间的六个进程分别映射到其父命名空间的 PID 号 5~10。
局部 ID 和全局 ID
命名空间增加了 PID 管理的复杂性。
回想一下,PID 命名空间按层次组织。在建立一个新的命名空间时,该命名空间中的所有 PID 对父命名空间都是可见的,但子命名空间无法看到父命名空间的 PID。但这意味着某些进程具有多个 PID,凡可以看到该进程的命名空间,都会为其分配一个 PID。 这必须反映在数据结构中。我们必须区分局部 ID和全局 ID
全局 PID 和 TGID 直接保存在task_struct中,分别是 task_struct 的 pid和 tgid成员:
- 全局 ID 在内核本身和初始命名空间中唯一的 ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个 PID,叫全局 ID,保证在整个系统中唯一。
- 局部 ID 对于属于某个特定的命名空间,它在其命名空间内分配的 ID 为局部 ID,该 ID 也可以出现在其他的命名空间中。
|
|
两项都是 pid_t 类型,该类型定义为__kernel_pid_t,后者由各个体系结构分别定义。通常定义为 int,即可以同时使用 232 个不同的 ID。
会话 session 和进程 group 组 ID 不是直接包含在 task_struct 本身中,但保存在用于信号处理的结构中。
task_ struct->signal->__session 表示全局 SID,
而全局 PGID 则保存在 task_struct->signal->__pgrp。
辅助函数 set_task_session 和 set_task_pgrp 可用于修改这些值。
除了这两个字段之外,内核还需要找一个办法来管理所有命名空间内部的局部量,以及其他 ID(如 TID 和 SID)。这需要几个相互连接的数据结构,以及许多辅助函数,并将在下文讨论。
下文我将使用 ID 指代提到的任何进程 ID。在必要的情况下,我会明确地说明 ID 类型(例如,TGID,即线程组 ID)。
一个小型的子系统称之为 PID 分配器(pid allocator)用于加速新 ID 的分配。此外,内核需要提供辅助函数,以实现通过 ID 及其类型查找进程的 task_struct 的功能,以及将 ID 的内核表示形式和用户空间可见的数值进行转换的功能。
PID 命名空间数据结构 pid_namespace
在介绍表示 ID 本身所需的数据结构之前,我需要讨论 PID 命名空间的表示方式。我们所需查看的代码如下所示:
pid_namespace的定义在include/linux/pid_namespace.h中
命名空间的结构如下
|
|
我们这里只关心其中的 child_reaper,level 和 parent 这三个字段
| 字段 | 描述 |
|---|---|
| kref | 表示指向 pid_namespace 的个数 |
| pidmap | pidmap 结构体表示分配 pid 的位图。当需要分配一个新的 pid 时只需查找位图,找到 bit 为 0 的位置并置 1,然后更新统计数据域(nr_free) |
| last_pid | 用于 pidmap 的分配。指向最后一个分配的 pid 的位置。(不是特别确定) |
| child_reaper | 指向的是当前命名空间的 init 进程,每个命名空间都有一个作用相当于全局 init 进程的进程 |
| pid_cachep | 域指向分配 pid 的 slab 的地址。 |
| level | 代表当前命名空间的等级,初始命名空间的 level 为 0,它的子命名空间 level 为 1,依次递增,而且子命名空间对父命名空间是可见的。从给定的 level 设置,内核即可推断进程会关联到多少个 ID。 |
| parent | 指向父命名空间的指针 |
实际上 PID 分配器也需要依靠该结构的某些部分来连续生成唯一 ID,但我们目前对此无需关注。我们上述代码中给出的下列成员更感兴趣。
每个 PID 命名空间都具有一个进程,其发挥的作用相当于全局的 init 进程。init 的一个目的是对孤儿进程调用 wait4,命名空间局部的 init 变体也必须完成该工作。
pid 结构描述
pid 与 upid
PID 的管理围绕两个数据结构展开:
- struct pid是内核对 PID 的内部表示,
- struct upid则表示特定的命名空间中可见的信息。
两个结构的定义在include/linux/pid.h中
|
|
| 字段 | 描述 |
|---|---|
| nr | 表示 ID 具体的值 |
| ns | 指向命名空间的指针 |
| pid_chain | 指向 PID 哈希列表的指针,用于关联对于的 PID |
所有的 upid 实例都保存在一个散列表中,稍后我们会看到该结构。
|
|
| 字段 | 描述 |
|---|---|
| count | 是指使用该 PID 的 task 的数目; |
| level | 表示可以看到该 PID 的命名空间的数目,也就是包含该进程的命名空间的深度 |
| tasks[PIDTYPE_MAX] | 是一个数组,每个数组项都是一个散列表头,分别对应以下三种类型 |
| numbers[1] | 一个 upid 的实例数组,每个数组项代表一个命名空间,用来表示一个 PID 可以属于不同的命名空间,该元素放在末尾,可以向数组添加附加的项。 |
tasks 是一个数组,每个数组项都是一个散列表头,对应于一个 ID 类型,PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID( PIDTYPE_MAX 表示 ID 类型的数目)这样做是必要的,因为一个 ID 可能用于几个进程。所有共享同一给定 ID 的 task_struct 实例,都通过该列表连接起来。
这个枚举常量 PIDTYPE_MAX,正好是 pid_type 类型的数目,这里 linux 内核使用了一个小技巧来由编译器来自动生成 id 类型的数目
此外,还有两个结构我们需要说明,就是 pidmap 和 pid_link
- pidmap 当需要分配一个新的 pid 时查找可使用 pid 的位图,其定义如下
- 而 pid_link 则是 pid 的哈希表存储结构
pidmap 用于分配 pid 的位图
|
|
| 字段 | 描述 |
|---|---|
| nr_free | 表示还能分配的 pid 的数量 |
| page | 指向的是存放 pid 的物理页 |
pidmap[PIDMAP_ENTRIES]域表示该 pid_namespace 下 pid 已分配情况
pid_link 哈希表存储
pids[PIDTYPE_MAX]指向了和该 task_struct 相关的 pid 结构体。 pid_link 的定义如下
|
|
task_struct 中进程 ID 相关数据结构
task_struct 中的描述符信息
|
|
| 字段 | 描述 |
|---|---|
| pid | 指该进程的进程描述符。在 fork 函数中对其进行赋值的 |
| tgid | 指该进程的线程描述符。在 linux 内核中对线程并没有做特殊的处理,还是由 task_struct 来管理。所以从内核的角度看, 用户态的线程本质上还是一个进程。对于同一个进程(用户态角度)中不同的线程其 tgid 是相同的,但是 pid 各不相同。 主线程即 group_leader(主线程会创建其他所有的子线程)。如果是单线程进程(用户态角度),它的 pid 等于 tgid。 |
| group_leader | 除了在多线程的模式下指向主线程,还有一个用处, 当一些进程组成一个群组时(PIDTYPE_PGID), 该域指向该群组的 leader |
| nsproxy | 指针指向 namespace 相关的域,通过 nsproxy 域可以知道该 task_struct 属于哪个 pid_namespace |
对于用户态程序来说,调用 getpid()函数其实返回的是 tgid,因此线程组中的进程 id 应该是是一致的,但是他们 pid 不一致,这也是内核区分他们的标识
- 多个 task_struct 可以共用一个 PID
- 一个 PID 可以属于不同的命名空间
- 当需要分配一个新的 pid 时候,只需要查找 pidmap 位图即可
那么最终,linux 下进程命名空间和进程的关系结构如下:
可以看到,多个 task_struct 指向一个 PID,同时 PID 的 hash 数组里安装不同的类型对 task 进行散列,并且一个 PID 会属于多个命名空间。
内核是如何设计 task_struct 中进程 ID 相关数据结构的
本部内容较多的采用了Linux 内核进程管理之进程 ID
Linux 内核在设计管理 ID 的数据结构时,要充分考虑以下因素:
- 如何快速地根据进程的 task_struct、ID 类型、命名空间找到局部 ID
- 如何快速地根据局部 ID、命名空间、ID 类型找到对应进程的 task_struct
- 如何快速地给新进程在可见的命名空间内分配一个唯一的 PID
如果将所有因素考虑到一起,将会很复杂,下面将会由简到繁设计该结构。
一个 PID 对应一个 task 时的 task_struct 设计
一个 PID 对应一个 task_struct如果先不考虑进程之间的关系,不考虑命名空间,仅仅是一个 PID 号对应一个 task_struct,那么我们可以设计这样的数据结构
|
|
每个进程的 task_struct 结构体中有一个指向 pid 结构体的指针,pid 结构体包含了 PID 号。
结构示意图如图
如何快速地根据局部 ID、命名空间、ID 类型找到对应进程的 task_struct
图中还有两个结构上面未提及:
- 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
- pid_map
这是一个位图,用来唯一分配 PID 值的结构,图中灰色表示已经分配过的值,在新建一个进程时,只需在其中找到一个为分配过的值赋给 pid 结构体的 nr,再将 pid_map 中该值设为已分配标志。这也就解决了上面的第 3 个问题——如何快速地分配一个全局的 PID
如何快速地根据进程的 task_struct、ID 类型、命名空间找到局部 ID
至于上面的*第 1 个问题就更加简单,已知 task_struct 结构体,根据其 pid_link 的 pid 指针找到 pid 结构体,取出其 nr 即为 PID 号。
带进程 ID 类型的 task_struct 设计
如果考虑进程之间有复杂的关系,如线程组、进程组、会话组,这些组均有组 ID,分别为 TGID、PGID、SID,所以原来的 task_struct 中 pid_link 指向一个 pid 结构体需要增加几项,用来指向到其组长的 pid 结构体,相应的 struct pid 原本只需要指回其 PID 所属进程的 task_struct,现在要增加几项,用来链接那些以该 pid 为组长的所有进程组内进程。数据结构如下:
定义在http://lxr.free-electrons.com/source/include/linux/sched.h#L1389
|
|
上面 ID 的类型 PIDTYPE_MAX 表示 ID 类型数目。之所以不包括线程组 ID,是因为内核中已经有指向到线程组的 task_struct 指针 group_leader,线程组 ID 无非就是 group_leader 的 PID。
假如现在有三个进程 A、B、C 为同一个进程组,进程组长为 A,这样的结构示意图如图
关于上图有几点需要说明:
图中省去了 pid_hash 以及 pid_map 结构,因为第一种情况类似;
进程 B 和 C 的进程组组长为 A,那么 pids[PIDTYPE_PGID] 的 pid 指针指向进程 A 的 pid 结构体;
进程 A 是进程 B 和 C 的组长,进程 A 的 pid 结构体的 tasks[PIDTYPE_PGID] 是一个散列表的头,它将所有以该 pid 为组长的进程链接起来。
再次回顾本节的三个基本问题,在此结构上也很好去实现。
进一步增加进程 PID 命名空间的 task_struct 设计
若在第二种情形下再增加 PID 命名空间
一个进程就可能有多个 PID 值了,因为在每一个可见的命名空间内都会分配一个 PID,这样就需要改变 pid 的结构了,如下:
|
|
在 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 内核中数据结构相差不多了。
进程 ID 管理函数
有了上面的复杂的数据结构,再加上散列表等数据结构的操作,就可以写出我们前面所提到的三个问题的函数了:
pid 号到 struct pid 实体
很多时候在写内核模块的时候,需要通过进程的 pid 找到对应进程的 task_struct,其中首先就需要通过进程的 pid 找到进程的 struct pid, 然后再通过 struct pid 找到进程的 task_struct
我知道的实现函数有三个。
|
|
find_pid_ns 获得 pid 实体的实现原理,主要使用哈希查找。内核使用哈希表组织 struct pid,每创建一个新进程,给进程的 struct pid 都会插入到哈希表中,这时候就需要使用进程 的进程 pid 和命名 ns 在哈希表中将相对应的 struct pid 索引出来,现在可以看下 find_pid_ns 的传入参数,也是通过 nr 和 ns 找到 struct pid。
根据局部 PID 以及命名空间计算在 pid_hash 数组中的索引,然后遍历散列表找到所要的 upid, 再根据内核的 container_of 机制找到 pid 实例。
代码如下:
|
|
而另外两个函数则是对其进行进一步的封装,如下
|
|
三者之间的调用关系如下
由图可以看出,find_pid_ns 是最终的实现,find_vpid 是使用 find_pid_ns 实现的,find_get_pid 又是由 find_vpid 实现的。
由原代码可以看出 find_vpid 和 find_pid_ns 是一样的,而 find_get_pid 和 find_vpid 有一点差异,就是使用 find_get_pid 将返回的 struct pid 中的字段 count 加 1,而 find_vpid 没有加 1。
获得局部 ID
根据进程的 task_struct、ID 类型、命名空间,可以很容易获得其在命名空间内的局部 ID
获得与 task_struct 关联的 pid 结构体。辅助函数有 task_pid、task_tgid、task_pgrp 和 task_session,分别用来获取不同类型的 ID 的 pid 实例,如获取 PID 的实例:
|
|
获取线程组的 ID,前面也说过,TGID 不过是线程组组长的 PID 而已,所以:
|
|
而获得 PGID 和 SID,首先需要找到该线程组组长的 task_struct,再获得其相应的 pid:
|
|
获得 pid 实例之后,再根据 pid 中的 numbers 数组中 uid 信息,获得局部 PID。
|
|
这里值得注意的是,由于 PID 命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能,因此,函数中需要确保当前命名空间的 level 小于等于产生局部 PID 的命名空间的 level。
除了这个函数之外,内核还封装了其他函数用来从 pid 实例获得 PID 值,如 pid_nr、pid_vnr 等。在此不介绍了。 结合这两步,内核提供了更进一步的封装,提供以下函数:
|
|
从函数名上就能推断函数的功能,其实不外于封装了上面的两步。
根据 PID 查找进程 task_struct
- 根据 PID 号(nr 值)取得 task_struct 结构体
- 根据 PID 以及其类型(即为局部 ID 和命名空间)获取 task_struct 结构体
如果根据的是进程的 ID 号,我们可以先通过 ID 号(nr 值)获取到进程 struct pid 实体(局部 ID),然后根据局部 ID、以及命名空间,获得进程的 task_struct 结构体
可以使用 pid_task 根据 pid 和 pid_type 获取到进程的 task
|
|
那么我们根据 pid 号查找进程 task 的过程就成为
|
|
内核还提供其它函数用来实现上面两步:
|
|
由于 linux 进程是组织在双向链表和红黑树中的,因此我们通过遍历链表或者树也可以找到当前进程,但是这个并不是我们今天的重点
生成唯一的 PID
内核中使用下面两个函数来实现分配和回收 PID 的:
|
|
在这里我们不关注这两个函数的实现,反而应该关注分配的 PID 如何在多个命名空间中可见,这样需要在每个命名空间生成一个局部 ID,函数 alloc_pid 为新建的进程分配 PID,简化版如下:
|
|
参考资料
-
No backlinks found.