|
| 1 | +Linux 内核使用 `task_struct` 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一。该数据结构在内核文件 `include/linux/sched.h` 中定义,在Linux 3.8 的内核中,该数据结构足足有 380 行之多,在这里我不可能逐项去描述其表示的含义,本篇文章只关注该数据结构如何来组织和管理进程ID的。 |
| 2 | + |
| 3 | +## 1、**进程ID类型** |
| 4 | + |
| 5 | +要想了解内核如何来组织和管理进程ID,先要知道进程ID的类型: |
| 6 | + |
| 7 | +- **PID**:这是 Linux 中在其命名空间中唯一标识进程而分配给它的一个号码,称做进程ID号,简称PID。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值。 |
| 8 | +- **TGID**:在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程,它们处于一个线程组,该线程组的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID;线程组组长的TGID与其PID相同;一个进程没有使用线程,则其TGID与PID也相同。 |
| 9 | +- **PGID**:另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作,例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。 |
| 10 | +- **SID**:几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID。 |
| 11 | + |
| 12 | +## 2、**PID 命名空间** |
| 13 | + |
| 14 | +命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为mount命名空间、UTS命名空间、IPC命名空间、用户命名空间、PID命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。命名空间有的还有层次关系,如PID命名空间,图1 为命名空间的层次关系图。 |
| 15 | + |
| 16 | + |
| 17 | + |
| 18 | +在上图有四个命名空间,一个父命名空间衍生了两个子命名空间,其中的一个子命名空间又衍生了一个子命名空间。以PID命名空间为例,由于各个命名空间彼此隔离,所以每个命名空间都可以有 PID 号为 1 的进程;但又由于命名空间的层次性,父命名空间是知道子命名空间的存在,因此子命名空间要映射到父命名空间中去,因此上图中 level 1 中两个子命名空间的六个进程分别映射到其父命名空间的PID 号5~10。 |
| 19 | + |
| 20 | +命名空间增大了 PID 管理的复杂性,对于某些进程可能有多个PID——在其自身命名空间的PID以及其父命名空间的PID,凡能看到该进程的命名空间都会为其分配一个PID。因此就有: |
| 21 | + |
| 22 | +- **全局ID**:在内核本身和初始命名空间中唯一的ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个PID,叫全局ID,保证在整个系统中唯一。 |
| 23 | +- **局部ID**:对于属于某个特定的命名空间,它在其命名空间内分配的ID为局部ID,该ID也可以出现在其他的命名空间中。 |
| 24 | + |
| 25 | +## 3、**进程ID管理数据结构** |
| 26 | + |
| 27 | +Linux 内核在设计管理ID的数据结构时,要充分考虑以下因素: |
| 28 | + |
| 29 | +1. 如何快速地根据进程的 task_struct、ID类型、命名空间找到局部ID |
| 30 | +2. 如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct |
| 31 | +3. 如何快速地给新进程在可见的命名空间内分配一个唯一的 PID |
| 32 | + |
| 33 | +如果将所有因素考虑到一起,将会很复杂,下面将会由简到繁设计该结构。 |
| 34 | + |
| 35 | +### 3.1**一个PID对应一个task_struct** |
| 36 | + |
| 37 | +如果先不考虑进程之间的关系,不考虑命名空间,仅仅是一个PID号对应一个task_struct,那么我们可以设计这样的数据结构: |
| 38 | + |
| 39 | +```text |
| 40 | +struct task_struct { |
| 41 | + //... |
| 42 | + struct pid_link pids; |
| 43 | + //... |
| 44 | +}; |
| 45 | +
|
| 46 | +struct pid_link { |
| 47 | + struct hlist_node node; |
| 48 | + struct pid *pid; |
| 49 | +}; |
| 50 | +
|
| 51 | +struct pid { |
| 52 | + struct hlist_head tasks; //指回 pid_link 的 node |
| 53 | + int nr; //PID |
| 54 | + struct hlist_node pid_chain; //pid hash 散列表结点 |
| 55 | +}; |
| 56 | +``` |
| 57 | + |
| 58 | +每个进程的 task_struct 结构体中有一个指向 pid 结构体的指针,pid 结构体包含了 PID 号。 |
| 59 | + |
| 60 | +- pid_hash[]: 这是一个hash表的结构,根据 pid 的 nr 值哈希到其某个表项,若有多个 pid 结构对应到同一个表项,这里解决冲突使用的是散列表法。这样,就能解决开始提出的第2个问题了,根据PID值怎样快速地找到task_struct结构体: |
| 61 | + |
| 62 | +- - 首先通过 PID 计算 pid 挂接到哈希表 pid_hash[] 的表项 |
| 63 | + - 遍历该表项,找到 pid 结构体中 nr 值与 PID 值相同的那个 pid |
| 64 | + - 再通过该 pid 结构体的 tasks 指针找到 node |
| 65 | + - 最后根据内核的 container_of 机制就能找到 task_struct 结构体 |
| 66 | + |
| 67 | + |
| 68 | + |
| 69 | +- pid_map:这是一个位图,用来唯一分配PID值的结构,图中灰色表示已经分配过的值,在新建一个进程时,只需在其中找到一个为分配过的值赋给 pid 结构体的 nr,再将pid_map 中该值设为已分配标志。这也就解决了上面的第3个问题——如何快速地分配一个全局的PID。 |
| 70 | + |
| 71 | +至于上面的第1个问题就更加简单,已知 task_struct 结构体,根据其 pid_link 的 pid 指针找到 pid 结构体,取出其 nr 即为 PID 号。 |
| 72 | + |
| 73 | +### 3.2**进程ID有类型之分** |
| 74 | + |
| 75 | +如果考虑进程之间有复杂的关系,如线程组、进程组、会话组,这些组均有组ID,分别为 TGID、PGID、SID,所以原来的 task_struct 中pid_link 指向一个 pid 结构体需要增加几项,用来指向到其组长的 pid 结构体,相应的 struct pid 原本只需要指回其 PID 所属进程的task_struct,现在要增加几项,用来链接那些以该 pid 为组长的所有进程组内进程。数据结构如下: |
| 76 | + |
| 77 | +```text |
| 78 | +enum pid_type |
| 79 | +{ |
| 80 | + PIDTYPE_PID, |
| 81 | + PIDTYPE_PGID, |
| 82 | + PIDTYPE_SID, |
| 83 | + PIDTYPE_MAX |
| 84 | +}; |
| 85 | +
|
| 86 | +struct task_struct { |
| 87 | + //... |
| 88 | + pid_t pid; //PID |
| 89 | + pid_t tgid; //thread group id |
| 90 | +
|
| 91 | + struct task_struct *group_leader; // threadgroup leader |
| 92 | +
|
| 93 | + struct pid_link pids[PIDTYPE_MAX]; |
| 94 | +
|
| 95 | + //... |
| 96 | +}; |
| 97 | +
|
| 98 | +struct pid_link { |
| 99 | + struct hlist_node node; |
| 100 | + struct pid *pid; |
| 101 | +}; |
| 102 | +
|
| 103 | +struct pid { |
| 104 | + struct hlist_head tasks[PIDTYPE_MAX]; |
| 105 | + int nr; //PID |
| 106 | + struct hlist_node pid_chain; // pid hash 散列表结点 |
| 107 | +}; |
| 108 | +``` |
| 109 | + |
| 110 | +上面 ID 的类型 PIDTYPE_MAX 表示 ID 类型数目。之所以不包括线程组ID,是因为内核中已经有指向到线程组的 task_struct 指针 group_leader,线程组 ID 无非就是 group_leader 的PID。 |
| 111 | + |
| 112 | +假如现在有三个进程A、B、C为同一个进程组,进程组长为A |
| 113 | + |
| 114 | +### 3.3**增加进程PID命名空间** |
| 115 | + |
| 116 | +若在第二种情形下再增加PID命名空间,一个进程就可能有多个PID值了,因为在每一个可见的命名空间内都会分配一个PID,这样就需要改变 pid 的结构了,如下: |
| 117 | + |
| 118 | +```text |
| 119 | +struct pid |
| 120 | +{ |
| 121 | + unsigned int level; |
| 122 | + /* lists of tasks that use this pid */ |
| 123 | + struct hlist_head tasks[PIDTYPE_MAX]; |
| 124 | + struct upid numbers[1]; |
| 125 | +}; |
| 126 | +
|
| 127 | +struct upid { |
| 128 | + int nr; |
| 129 | + struct pid_namespace *ns; |
| 130 | + struct hlist_node pid_chain; |
| 131 | +}; |
| 132 | +``` |
| 133 | + |
| 134 | +在 pid 结构体中增加了一个表示该进程所处的命名空间的层次level,以及一个可扩展的 upid 结构体。对于struct upid,表示在该命名空间所分配的进程的ID,ns指向是该ID所属的命名空间,pid_chain 表示在该命名空间的散列表。 |
| 135 | + |
| 136 | +## 4、**进程ID管理函数** |
| 137 | + |
| 138 | +有了上面的复杂的数据结构,再加上散列表等数据结构的操作,就可以写出我们前面所提到的三个问题的函数了: |
| 139 | + |
| 140 | +### 4.1**获得局部ID** |
| 141 | + |
| 142 | +根据进程的 task_struct、ID类型、命名空间,可以很容易获得其在命名空间内的局部ID: |
| 143 | + |
| 144 | +1. 获得与task_struct 关联的pid结构体。辅助函数有 task_pid、task_tgid、task_pgrp和task_session,分别用来获取不同类型的ID的pid 实例,如获取 PID 的实例: |
| 145 | + **static****inline****struct** pid ***task_pid**(**struct** task_struct *****task) { **return** task**->**pids[PIDTYPE_PID].pid; } |
| 146 | + 获取线程组的ID,前面也说过,TGID不过是线程组组长的PID而已,所以: |
| 147 | + **static****inline****struct** pid ***task_tgid**(**struct** task_struct *****task) { **return** task**->**group_leader**->**pids[PIDTYPE_PID].pid; } |
| 148 | + 而获得PGID和SID,首先需要找到该线程组组长的task_struct,再获得其相应的 pid: |
| 149 | + **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; } |
| 150 | +2. 获得 pid 实例之后,再根据 pid 中的numbers 数组中 uid 信息,获得局部PID。 |
| 151 | + **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; } |
| 152 | + 这里值得注意的是,由于PID命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能,因此,函数中需要确保当前命名空间的level 小于等于产生局部PID的命名空间的level。 |
| 153 | + 除了这个函数之外,内核还封装了其他函数用来从 pid 实例获得 PID 值,如 pid_nr、pid_vnr 等。在此不介绍了。 |
| 154 | + |
| 155 | +结合这两步,内核提供了更进一步的封装,提供以下函数: |
| 156 | + |
| 157 | +```text |
| 158 | +pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); |
| 159 | +pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); |
| 160 | +pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); |
| 161 | +pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); |
| 162 | +``` |
| 163 | + |
| 164 | +从函数名上就能推断函数的功能,其实不外于封装了上面的两步。 |
| 165 | + |
| 166 | +### 4.2**查找进程task_struct** |
| 167 | + |
| 168 | +根据局部ID、以及命名空间,怎样获得进程的task_struct结构体呢?也是分两步: |
| 169 | + |
| 170 | +1. 获得 pid 实体。根据局部PID以及命名空间计算在 pid_hash 数组中的索引,然后遍历散列表找到所要的 upid, 再根据内核的 container_of 机制找到 pid 实例。代码如下: |
| 171 | + **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; } |
| 172 | +2. 根据ID类型取得task_struct 结构体。 |
| 173 | + **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; } |
| 174 | + |
| 175 | +内核还提供其它函数用来实现上面两步: |
| 176 | + |
| 177 | +```text |
| 178 | +struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns); |
| 179 | +struct task_struct *find_task_by_vpid(pid_t vnr); |
| 180 | +struct task_struct *find_task_by_pid(pid_t vnr); |
| 181 | +``` |
| 182 | + |
| 183 | +具体函数实现的功能也比较简单。 |
| 184 | + |
| 185 | +### 4.3**生成唯一的PID** |
| 186 | + |
| 187 | +内核中使用下面两个函数来实现分配和回收PID的: |
| 188 | + |
| 189 | +```text |
| 190 | +static int alloc_pidmap(struct pid_namespace *pid_ns); |
| 191 | +static void free_pidmap(struct upid *upid); |
| 192 | +``` |
| 193 | + |
| 194 | +在这里我们不关注这两个函数的实现,反而应该关注分配的 PID 如何在多个命名空间中可见,这样需要在每个命名空间生成一个局部ID,函数 alloc_pid 为新建的进程分配PID,简化版如下: |
| 195 | + |
| 196 | +```text |
| 197 | +struct pid *alloc_pid(struct pid_namespace *ns) |
| 198 | +{ |
| 199 | + struct pid *pid; |
| 200 | + enum pid_type type; |
| 201 | + int i, nr; |
| 202 | + struct pid_namespace *tmp; |
| 203 | + struct upid *upid; |
| 204 | +
|
| 205 | + tmp = ns; |
| 206 | + pid->level = ns->level; |
| 207 | + // 初始化 pid->numbers[] 结构体 |
| 208 | + for (i = ns->level; i >= 0; i--) { |
| 209 | + nr = alloc_pidmap(tmp); //分配一个局部ID |
| 210 | + pid->numbers[i].nr = nr; |
| 211 | + pid->numbers[i].ns = tmp; |
| 212 | + tmp = tmp->parent; |
| 213 | + } |
| 214 | +
|
| 215 | + // 初始化 pid->task[] 结构体 |
| 216 | + for (type = 0; type < PIDTYPE_MAX; ++type) |
| 217 | + INIT_HLIST_HEAD(&pid->tasks[type]); |
| 218 | +
|
| 219 | + // 将每个命名空间经过哈希之后加入到散列表中 |
| 220 | + upid = pid->numbers + ns->level; |
| 221 | + for ( ; upid >= pid->numbers; --upid) { |
| 222 | + hlist_add_head_rcu(&upid->pid_chain, |
| 223 | + &pid_hash[pid_hashfn(upid->nr, upid->ns)]); |
| 224 | + upid->ns->nr_hashed++; |
| 225 | + } |
| 226 | +
|
| 227 | + return pid; |
| 228 | +} |
| 229 | +``` |
| 230 | + |
| 231 | +---- |
| 232 | + |
| 233 | +版权声明:本文为知乎博主「[极致Linux内核](https://www.zhihu.com/people/linuxwang-xian-sheng)」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 |
| 234 | + |
| 235 | +原文链接:https://zhuanlan.zhihu.com/p/546814252 |
0 commit comments