技术交流

好好学习,天天向上。

0%

进程的表示

[toc]

进程的表示

task_struct和ID基础概念

  • 在linux中,线程和进程在内核中都是用task_struct结构体来表示的,只是当一组task_struct运行在一个进程中时,内核让它们共享某些资源(比如用于管理内存的struct mm)。每个task_sturct都有自己的PID(process id),当然task_struct作为线程时,PID也就是其TID(thread id)
  • 运行在同一个进程中的task_struct,有统一个线程组ID(TGID),如果进程没有使用线程,则其PID和TGID相同。线程组中的主线程被称作组长(group leader)。所有线程的task_struct的group_leader成员会指向组长task_struct
  • 独立进程可以合并成进程组(使用setpgrp系统调用),在终端中用管道执行一组命令时就是这种情况。进程组成员的task_struct的pgrp属性值都是相同的,即进程组组长的PID。进程组简化了向组的所有成员发送信号的操作
1
2
3
4
5
6
7
8
9
// 在task_struct有分别表示PID和TGID的变量,也就是pid和tgid。当考虑
// namespace时,这两个变量一般用于全局namespace,子namespace的情况会比较复杂
<sched.h>
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
}

namespace

linux支持一个叫namespace的特性,其核心是期望将某些资源和进程做隔离,例如隔离文件系统、隔离IPC信息或者隔离网络。隔离的效果就是,某些资源对某一组进程可见,对另一组进程不可见;不同namespace中的进程所执行的动作,也是互相不可见的。在内核中,隔离是通过各种namespace实现的。namespace可以嵌套,也就是namespace中可以创建自己的子namespace。

很显然,为了对资源进行隔离,PID也是需要做隔离的(否则,不同namespace中的进程可以看到其他进程)。内核采用如下逻辑做PID隔离,子namespace中的PID信息各自独立,互相不可见,但是父namespace对子namespace中的PID信息完全可见;子namespace中的某个进程,除了在子namespace中有自己的PID外,还要在其所有的上层namespace中都映射一个PID。

image-20211204192907811

在namespace架构下PID的表示方法

涉及的数据结构

在namespace结构下,为了描述PID信息,内核总共涉及了以下数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* 
* PID在内核中的内部表示以及PID在namespace下的特殊表示
*/

// 位于<pid.h>,struct pid是进程PID在内核的内部表示
struct pid // pid
{
atomic_t count;
unsigned int level; // 描述进程在命名空间层次结构中的深度
// 也是numbers长度,
// 也是描述有多少个namespace可以看到该数据结构
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX]; // 这里是PIDTYPE_MAX个hash表每种表对应一组进程,以及这组进程和该pid值的关联关系。
// 上面挂的是和这个pid绑定的进程信息,进程可能是通过各种角度和这个pid绑定的
struct rcu_head rcu;
struct upid numbers[1]; // 特定的命名空间可见的信息,为什么书中说这个地方可以有n+1项呢,因为当做变长数数组用的
};
// 位于<pid.h>,该结构体表示进程在特定namespace下可见的信息
struct upid {
int nr; // 在命名空间下的pid值
struct pid_namespace *ns; // 指向对应的命名空间
struct hlist_node pid_chain; // 用于将upid挂在pid_hash表上,find_pid_ns中会用
};


/*
* struct pid_link以及该数据结构和task_struct的关系
*/

// 位于<sched.h>,在task_struct中,有PIDTYPE_MAX个pid_link
struct task_struct {
...
struct pid_link pids[PIDTYPE_MAX];
...
}
// 位于<pid.h>,描述内核纳入管理的三种PID类型,或者也是一个task_struct的PID的几种不同身份
enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,
};
// 位于<pid.h>,pid_link的作用就是使task_struct可以挂到不同的hash表上
struct pid_link
{
struct hlist_node node; // 用于把进程放到 struct pid的hash表中
struct pid *pid; // 指向进程的 struct pid
};

引入struct pid

内核在对进程的管理过程中,需要解决以下几个问题:

  • 通过一个ID值来找到一个进程的task_struct
  • 快速的为新创建进程分配一个可用的ID值,ID值是可以重复使用的;
  • 前两个操作在所有可以看到目标进程的namespace都可以执行,而且进程ID管理必须满足namespace的嵌套设计;

为了应对复杂的管理需求,内核用struct pid表示一个进程PID信息。内核代码对struct pid有如下注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
* What is struct pid?
*
* A struct pid is the kernel's internal notion of a process identifier.
* It refers to individual tasks, process groups, and sessions. While
* there are processes attached to it the struct pid lives in a hash
* table, so it and then the processes that it refers to can be found
* quickly from the numeric pid value. The attached processes may be
* quickly accessed by following pointers from struct pid.
*
* struct pid是内核对pid的内部表示,其指向一个独立的task、进程组或者会话。
* 当有进程和某个struct pid关联上的时候,struct pid 可以挂在一个hash表上,
* 这样通过值就是可快速找到struct pid进而找到对应的进程。
*
* Storing pid_t values in the kernel and referring to them later has a
* problem. The process originally with that pid may have exited and the
* pid allocator wrapped, and another process could have come along
* and been assigned that pid.
*
* 如果仅在内核中保存一个ID值来引用task_struct,这种方式有一些问题。拥有某个ID值的进程
* 可能exit,然后pid重新将该ID值分配给新创建的进程,内核可能会混淆通过该ID值查询到的task_struct
* 是属于就进程还是新创建的进程
*
* Referring to user space processes by holding a reference to struct
* task_struct has a problem. When the user space process exits
* the now useless task_struct is still kept. A task_struct plus a
* stack consumes around 10K of low kernel memory. More precisely
* this is THREAD_SIZE + sizeof(struct task_struct). By comparison
* a struct pid is about 64 bytes.
* 上面的内容大概是说,通过保存task_struct指针来引用用户进程,会造成资源浪费问题。因为内核在某些情况下有
* 查询已经exit进程的ID的必要,如果为了查询其ID就保留其task_struct的话,就显得太浪费了。
*
* Holding a reference to struct pid solves both of these problems.
* It is small so holding a reference does not consume a lot of
* resources, and since a new struct pid is allocated when the numeric pid
* value is reused (when pids wrap around) we don't mistakenly refer to new
* processes.
* 通过保存一个struct pid指针的形式可以解决上述问题。当一个ID值被重用的时候,可以新创建一个struct pid
* 首先它足够小,不会消耗过多的资源(已经退出的进程可以保留其struct pid)。其次,通过ID值查询task_struct
* 内核不会混淆具体引用的是旧进程还是新进程了。
*/

各个数据结构的联系

下图描述的是前述各个数据结构之间的逻辑联系。首先task_sturctk可以和struct_pid因为某种做绑定。例如,可能这个struct pid就是属于task_struct的PID表述,则可用PID_TYPE_PID做绑定。又或者这个struct pid的拥有者是task_struct的group leader,则可用PID_TYPE_PGID绑定。当task_struct和struct pid绑定的时候,struct pid会通过task_struct的struct pid_link pids成员将task_struct连接到struct pid的struct hlist_head tasks链表上。当然,整个过程都是区分绑定类型的。

struct pid中包含一个struct upid numbers[1]数组,虽然其规格为1,但是由于位于结构体末尾,其长度可以根据实际内存区域做变长数组用。struct pid的 unsigned int level成员记录了具体的struct upid numbers成员有效个数。struct upid numbers中的成员用于命名空间,从上到下,每一级一个。每个struct upid numbers成员既会被hash到一个全局hash表中,hash过程会用到struct upid中的nr值和namespace指针,这样可以实现与之相反的查找过程。struct upid中的nr值也就是某个task_struct在具体命名空间中的ID值。

image-20211204220023985

管理代码说明

  • task_struct可以通过某个方式和task_pid绑定
1
2
3
4
5
6
7
8
int fastcall attach_pid(struct task_struct *task, enum pid_type type, struct pid *pid)
{
struct pid_link *link;
link = &task->pids[type];
link->pid = pid;
hlist_add_head_rcu(&link->node, &pid->tasks[type]);
return 0;
}
  • 内核提供了一组辅助函数,可以找到目标进程对应的struct pid,有了struct pid就可以通过container_of找到目标task_struct 了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static inline struct pid *task_pid(struct task_struct *task)
{
return task->pids[PIDTYPE_PID].pid;
}

static inline struct pid *task_tgid(struct task_struct *task)
{
return task->group_leader->pids[PIDTYPE_PID].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;
}
  • 找到某个进程在某个namespace中的唯一ID
1
2
3
4
5
6
7
8
9
10
11
12
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;
}
  • 通过ID值和类型,从某个namespace中找到目标进程的task_struct
1
2
3
4
struct task_struct *find_task_by_pid_type_ns(int type, int nr, struct pid_namespace *ns)
{
return pid_task(find_pid_ns(nr, ns), type);
}
  • 下面为linux为新进程创建struct pid的裁剪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
for (i = ns->level; i >= 0; i--) {
nr = alloc_pidmap(tmp);
...
pid->numbers[i].nr = nr;
pid->numbers[i].ns = tmp;
tmp = tmp->parent;
}
pid->level = ns->level;
...
// 起始于建立进程的命名空间,一直到初始的全局命名空间,内核会为此间的每个命名空间分别创
// 建一个局部PID。包含在struct pid中的所有upid都用重新生成的PID更新其数据。每个upid实例都
// 必须置于PID散列表中:

for (i = ns->level; i >= 0; i--) {
upid = &pid->numbers[i];
hlist_add_head_rcu(&upid->pid_chain,
&pid_hash[pid_hashfn(upid->nr, upid->ns)]);
}
...
return pid;
}