1. ID类型
进程 ID 是由内核分配给进程用于进程在其命名空间中唯一标识。该号码被称作进程 ID 号,简称 PID 。
每个进程除 PID 以外还有其他的 ID。在 linux 中有以下几种ID:
<pid.h> enum pid_type { PIDTYPE_PID, PIDTYPE_TGID, PIDTYPE_PGID, PIDTYPE_SID, PIDTYPE_MAX, };
PID : 进程IDT
TGID : 线程组中的所有线程都有的统一的线程组ID
PGID : 进程组中进程组组长的PID
SID : 会话中所有进程都相同的会话ID
一个进程的 ID 可能并不只有以上4个 ID ,因为内核的命名空间管理从而增加了进程 ID 管理的复杂性。同一个进程在不同的命名空间中有些 ID 值可能并不相同。所以还需要以全局 ID 和局部 ID 来区分。
全局 ID : 在内核本身和初始命名空间中的唯一 ID 号。
局部 ID :属于某个特定命名空间,不具备全局有效性。
全局 PID 和 TGID 直接保存在 task_struct 中的 pid 和 tgid 中。局部 PID 则保存在 thread_pid 所指向的结构体中。 其他 ID 通过 在task_struct 中的 signal 指针下可以找到。
<sched.h> struct task_struct { ... pid_t pid; pid_t tgid; ... struct pid *thread_pid; struct hlist_node pid_links[PIDTYPE_MAX]; struct list_head thread_group; struct list_head thread_node; ... }
接下来就来了解下局部 ID 结构和保存方式。
2. 局部 ID 与命名空间
既然局部 ID 是为了反应当前命名空间中的 ID 号。那么局部 ID 和命名空间就存在一个一一对应的关系。而且父命名空间也可以看到子命名空间中的 ID 值,所以内核要管理和查询不同命名空间中局部 ID 。前面我们已经看到内核将不同类型的 ID 都存放在 struct pid 结构体中,并通过指针的方式去访问。那我们先来看一下 struct pid 结构体。
<pid.h> struct pid { atomic_t count; unsigned int level; /* lists of tasks that use this pid */ struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1]; };
level 为命名空间深度。我们前面提到过子命名空间的进程在父命名空间可见。所以我们需要知道当前进程的命名空间深度。
而后面 struct upid numvers[] 结构体数组中存放了 level 个成员。每个成员对于了当前 level 的 ID 和命名空间信息。这里结果体声明看到只有一个。但其位置在结构体尾部其他数组成员可以通过分配足够的内存来访问。 整个关系结构如下图。
3. ID 与命名空间转化
既然有这么多的 ID 和命名空间。内核自然也要提供通过 ID 找命名空间或者通过命名空间找 ID 的方法。先看 task 到特定命名空间获取特定 ID 的方法。
<sched.h> // 检查任务结构是否过时 static inline int pid_alive(const struct task_struct *p) { return p->thread_pid != NULL; } //下面函数实现都是调用的同一个函数所以可以而且通过名字和传参也可以知道其含义 //所以只简单列出 static inline pid_t task_pgrp_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); // return __task_pid_nr_ns(tsk, PIDTYPE_PGID, ns); static inline pid_t task_pgrp_vnr(struct task_struct *tsk); // return __task_pid_nr_ns(tsk, PIDTYPE_PGID, NULL); static inline pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns) // return __task_pid_nr_ns(tsk, PIDTYPE_SID, ns); static inline pid_t task_session_vnr(struct task_struct *tsk) // return __task_pid_nr_ns(tsk, PIDTYPE_SID, NULL); static inline pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns) // return __task_pid_nr_ns(tsk, PIDTYPE_TGID, ns); static inline pid_t task_tgid_vnr(struct task_struct *tsk) // return __task_pid_nr_ns(tsk, PIDTYPE_TGID, NULL); //获取 task 父进程指定命名空间的 pid static inline pid_t task_ppid_nr_ns(const struct task_struct *tsk, struct pid_namespace *ns) //应该是在初始命名空间中获取 task 父进程的 pid static inline pid_t task_ppid_nr(const struct task_struct *tsk) { return task_ppid_nr_ns(tsk, &init_pid_ns); } /* 过时, 勿用 */ static inline pid_t task_pgrp_nr(struct task_struct *tsk) <pid.c> //这就是前面的许多函数封装的原型 //主要目的是获取 task 中制定命名空间制定类型的 ID。当 ns 为 NULL 时默认为当前命名空间 pid_t __task_pid_nr_ns(struct task_struct *task, enum pid_type type, struct pid_namespace *ns)
接下来我们来看下从数字 id 到 struct pid。
<pid.c> //其实核心就是通过 idr 机制通过 id 和命名空间去找到对应的 struct pid struct pid *find_pid_ns(int nr, struct pid_namespace *ns) { return idr_find(&ns->idr, nr); } //作用类似,返回当前命名空间 struct pid *find_vpid(int nr);
既然可以从数字 id 找到 struct pid 那么想找 task_struct 只要通过 pid 上的 tasks[PIDTYPE_MAX] 链表头就可以找到对应的 task 。其中的 rcu 机制暂时略过, rcu 主要用于 SMP 处理器的 CPU 之前同步问题。
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, pid_links[(type)]); } return result; } //自然内核也就提供了通过 pid 和 命名空间来找 task_struct 的方法。 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);
唯一 id 生成自然也是使用 idr 机制,此处暂不做详细说明。通过 idr 获得唯一的 id( id 循环使用)。