文章目录
- 1. 前言
- 2. 进程 PID 相关数据结构
- 3. 进程 PID 的构建
- 3.1 第一个进程 PID 构建
- 3.2 第二个进程 PID 的构建过程
- 3.2.1 从当前进程复制进程 PID 信息
- 3.2.2 创建每进程的 PID 管理数据 (`struct pid`) 并初始化
- 3.2.3 绑定进程和其相关的 PID 管理数据
- 3.3 进程的 PID 建立过程一般化
- 4. 进程 PID 管理相关接口
- 5. 进程 PID 的层级结构
- 6. 命名空间观察工具
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 进程 PID 相关数据结构
/* include/linux/pid_namespace.h */
/* 进程 PID 命名空间对象 */
struct pid_namespace {
...
struct pidmap pidmap[PIDMAP_ENTRIES]; /* 空闲 PID 管理位图 */
...
struct kmem_cache *pid_cachep; /* 当前层级 struct pid 对象分配缓存,alloc_pid() 从中分配 struct pid 对象 */
unsigned int level; /* PID 命名空间 层级编号,从 0 开始编号 */
struct pid_namespace *parent; /* 父级 pid_namespace (@level - 1) */
...
};
/* include/linux/nsproxy.h */
/* 命名空间代理对象: 包含指向各类型命名空间对象的指针 */
struct nsproxy {
...
struct pid_namespace *pid_ns_for_children; /* 进程关联的 PID 命名空间 */
...
};
enum pid_type
{
PIDTYPE_PID, /* 进程 PID */
PIDTYPE_PGID, /* 进程组 ID(进程组 领头进程的 PID) */
PIDTYPE_SID, /* session ID */
PIDTYPE_MAX,
/* only valid to __task_pid_nr_ns() */
/*
* 线程组 ID(线程组 group leader 进程的 PIDTYPE_PID 类型 PID),
* 可通过进程所在 线程组的 group leader 进程 task_struct::group_leader
* 的 task_struct::pids[PIDTYPE_PID] 信息获取,所以无需在进程中维护一个
* __PIDTYPE_TGID 的 pid 信息,这也是 __PIDTYPE_TGID 定义在 PIDTYPE_MAX
* 之后的原因。
* 但这一点,在更新版本的内核中已经有所变化。
*/
__PIDTYPE_TGID
};
/* include/linux/pid.h */
struct upid {
/* Try to keep pid_chain in the same cacheline as nr for find_vpid */
int nr; /* getpid(), gettid() 等 API 返回的值,来自于这里 */
struct pid_namespace *ns; /* 关联的 PID 命名空间 */
struct hlist_node pid_chain; /* 用于挂接到全局 PID 哈希表 pid_hash[] */
};
struct pid {
...
unsigned int level; /* 所属 pid_namespace 的层级编号。level 从 0 开始编号 */
/*
* 使用当前 struct pid 的 任务列表:
* (1) tasks[PIDTYPE_PID]
* 使用当前 struct pid 作为 PID 的进程列表。
* (2) tasks[PIDTYPE_PGID]
* 使用当前 struct pid 作为 PGID 的进程列表。
* 同一 pid_namespace(即同一层级) 内的所有进程共享 pid_namespace 内
* 首进程的 struct pid 。
* (3) tasks[PIDTYPE_SID]
* 使用当前 struct pid 作为 SID 的进程列表。
* 同一 pid_namespace(即同一层级)内的所有进程共享 pid_namespace 内
* 首进程的 struct pid 。
*
* 这 3 个哈希链表的构建细节参考函数 attach_pid(), 每个进程通过
* struct task_struct::pids[PIDTYPE_*].node 挂接到 struct pid::tasks[PIDTYPE_*]
* 哈希链表,PIDTYPE_* 取值为 {PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID} 。
*/
struct hlist_head tasks[PIDTYPE_MAX];
...
/*
* numbers[] 的长度和其所属的 PID 命名空间的层级有关,其长度为 level + 1,
* 这由 struct upid 关联的 PID 命名空间 struct upid::ns 中,struct pid
* 分配缓存 struct pid_namespace::pid_cachep 决定。
* 更多细节见后面的代码分析。
*/
struct upid numbers[1];
};
struct pid_link {
struct hlist_node node; /* 用来将进程添加到所用 struct pid 的 tasks[PIDTYPE_*] 哈希链表 */
struct pid *pid;
};
/* include/linux/sched.h */
/* 进程管理对象 */
struct task_struct {
...
struct task_struct *group_leader; /* 【线程组】 leader 进程 */
...
struct nsproxy *nsproxy; /* 进程关联的各种命令空间,这里只关注 PID 命名空间 (pid_namespace) */
...
struct pid_link pids[PIDTYPE_MAX]; /* 进程的 PID, PGID, SID, TGID 管理数据 */
...
};
对上面这些数据结构的作用,择重做一个扼要介绍:
-
struct
pid_namespacePID 命名空间
。一方面,PID 命名空间
用来实现 PID 隔离,允许进程在不同的PID 命名空间
中有各自独立的 PID;另一方面,PID 命名空间
也实现了 PID 的层级结构。PID 命名空间
组织结构如下图:
上图中,数据标记的格式为:level.PID
:
. {0.1,0.2,0.3} 表示有 3 个进程,位于 `level 0 PID 命名空间`,
它们的 `PID` 分别为 {1,2,3}。
. {0.4 1.1, 0.5 1.2} 表示有 2 个进程,位于 `level 1 PID 命名空间`,
它们在 `level 0 PID 命名空间` 的 PID 分别为 {4,5};
它们在 `level 1 PID 命名空间` 的 PID 分别为 {1,2}。
. {0.6 1.3 2.1,0.7 1.4 2.2} 表示有 2 个进程,位于 `level 2 PID 命名空间`,
它们在 `level 0 PID 命名空间` 的 PID 分别为 {6,7};
它们在 `level 1 PID 命名空间` 的 PID 分别为 {3,4};
它们在 `level 2 PID 命名空间` 的 PID 分别为 {1,2}。
-
struct
upid
主要用来记录进程在某一PID 命名空间
中的 PID (struct upid::nr
),以及 PID 所在的PID 命名空间
(struct upid::ns
)。 -
struct
pid
主要用来记录进程所处PID 命名空间
层级(struct pid::level
),以及在所有PID 命名空间
层级中的 PID (struct pid::numbers[]
) 。
3. 进程 PID 的构建
3.1 第一个进程 PID 构建
Linux 系统中第一个进程
,init_task
,其 PID 是静态构建
的。细节如下:
/* init/init_task.c */
/* Initial task structure */
struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);
/* include/linux/init_task.h */
/*
* INIT_TASK is used to set up the first task table, touch at
* your own risk!. Base=0, limit=0x1fffff (=2MB)
*/
#define INIT_TASK(tsk) \
{
... \
.real_parent = &tsk, \
.parent = &tsk, \
... \
.group_leader = &tsk, \
... \
.nsproxy = &init_nsproxy, /* 进程 PID 命名空间管理数据 */ \
... \
/* 进程 PID 管理数据 */ \
.pids = { \
[PIDTYPE_PID] = INIT_PID_LINK(PIDTYPE_PID), \
[PIDTYPE_PGID] = INIT_PID_LINK(PIDTYPE_PGID), \
[PIDTYPE_SID] = INIT_PID_LINK(PIDTYPE_SID), \
}, \
... \
}
init_task
的 PID 命名空间管理数据:
/* kernel/nsproxy.c */
struct nsproxy init_nsproxy = {
...
.pid_ns_for_children = &init_pid_ns, /* init_task 的 PID 命名空间管理数据 */
...
};
/* kernel/pid.c */
struct pid_namespace init_pid_ns = { /* 系统第一个、位于 level 0 的 PID 命名空间对象 */
...
.level = 0, /* init_task 位于 level 0 PID 命名空间 */
.child_reaper = &init_task,
...
#ifdef CONFIG_PID_NS
.ns.ops = &pidns_operations,
#endif
};
init_task
的 PID 管理数据:
/* include/linux/init_task.h */
#define INIT_PID_LINK(type) \
{ \
.node = { \
.next = NULL, \
.pprev = NULL, \
}, \
.pid = &init_struct_pid, \
}
/* kernel/pid.c */
struct pid init_struct_pid = INIT_STRUCT_PID;
#define INIT_STRUCT_PID { \
.count = ATOMIC_INIT(1), \
.tasks = { \
{ .first = NULL }, \
{ .first = NULL }, \
{ .first = NULL }, \
}, \
.level = 0, /* init_task 处于 level 0 的 PID 命名空间 */ \
.numbers = { \
{ \
.nr = 0, /* init_task 的 PID, PGID, SID 均为 0 */ \
.ns = &init_pid_ns, /* init_task 所属的 PID 命名空间 */ \
.pid_chain = { .next = NULL, .pprev = NULL }, \
}, \
} \
}
从上面的代码可以了解到:
. init_task 处于 level 0 的 PID 命名空间
. init_task 的 PID,PGID,SID 均为 0
. init_task 位于 PID 命名空间 init_pid_ns 内
说了半天,还不知道 Linux 系统中第一个进程
,init_task
,到底是哪位。嗯,start_kernel()
熟悉吧?在 BOOT CPU
上运行的 start_kernel()
所在的执行序列,就是 init_task
。
3.2 第二个进程 PID 的构建过程
init_task
在 start_kernel()
中执行部分系统初始化工作后,将创建系统中第二个进程
来执行剩余的初始化工作:
start_kernel()
...
/*
* 全局 PID 哈希表空间分配,用来存储系统中
* 所有层级 PID 命名空间中所有 struct upid 。
*/
pidhash_init()
pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 0, 18,
HASH_EARLY | HASH_SMALL | HASH_ZERO, &pidhash_shift, NULL, 0, 4096);
...
/*
* . 设置默认允许的最大、最小 PID 值
* . 分配 level 0 PID 命名空间 进程 PID 管理位图
* . 创建 level 0 PID 命名空间 的 struct pid 分配缓存
*/
pidmap_init()
...
/* bump default and minimum pid_max based on number of cpus */
pid_max = min(pid_max_max, max_t(int, pid_max,
PIDS_PER_CPU_DEFAULT * num_possible_cpus()));
pid_max_min = max_t(int, pid_max_min,
PIDS_PER_CPU_MIN * num_possible_cpus());
pr_info("pid_max: default: %u minimum: %u\n", pid_max, pid_max_min);
/* 分配 level 0 PID 命名空间 进程 PID 管理位图 */
init_pid_ns.pidmap[0].page = kzalloc(PAGE_SIZE, GFP_KERNEL);
/* Reserve PID 0. We never call free_pidmap(0) */
/* 保留 level 0 PID 命名空间 进程 PID 0 */
set_bit(0, init_pid_ns.pidmap[0].page);
atomic_dec(&init_pid_ns.pidmap[0].nr_free);
/* 创建 level 0 PID 命名空间 的 struct pid 分配缓存 */
init_pid_ns.pid_cachep = KMEM_CACHE(pid,
SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT);
...
/* 创建系统中 第二个进程 来执行剩余的初始化工作 */
rest_init()
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
_do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL, 0);
/* kernel/fork.c */
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
...
long nr;
...
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
...
if (!IS_ERR(p)) {
...
struct pid *pid;
...
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid); /* 进程 在当前层级 PID 命名空间中 的 PID */
...
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr; /* 返回 进程 在当前层级 PID 命名空间 中 的 PID */
}
static __latent_entropy struct task_struct *copy_process(
unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace,
unsigned long tls,
int node)
{
int retval;
struct task_struct *p;
...
/* 分配进程结构体 task_struct,复制当前进程 @current 的信息到新进程 @p (包括 PID 数据) */
p = dup_task_struct(current, node);
...
/*
* 在设置了 CLONE_NEWNS,...,CLONE_NEWPID,... 等标记的情形,
* 按需新建各种 namespace (包括 PID 命名空间 pid_namespace) 。
*
* 创建 第二个进程时没有设置对应标志位,所以不会创建新的 PID 命名空间。
*/
retval = copy_namespaces(clone_flags, p);
...
/* 为新进程分配 PID 管理数据 */
if (pid != &init_struct_pid) {
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
...
}
...
/* ok, now we should be set up.. */
p->pid = pid_nr(pid); /* 记录进程在 level 0 PID 命名空间 中 的 PID */
if (clone_flags & CLONE_THREAD) { /* 线程组内 非 group leader 进程 */
...
/* 设置 线程组内 非 group leader 进程 的 group leader */
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else { /* 线程组的 group leader 进程 */
...
p->group_leader = p; /* 线程组内 group leader 进程 的 group leader 为自身 */
p->tgid = p->pid; /* 线程组 group leader 进程: tgid == pid */
}
...
if (likely(p->pid)) {
...
init_task_pid(p, PIDTYPE_PID, pid); /* 设置进程 @p 的 PID 信息 */
if (thread_group_leader(p)) { /* 如果是 线程组 group leader, */
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current)); /* 设置进程 @p 的 PGID 信息 */
init_task_pid(p, PIDTYPE_SID, task_session(current)); /* 设置进程 @p 的 SID 信息 */
...
attach_pid(p, PIDTYPE_PGID); /* 将进程 @p 添加到关联 struct pid 的 PGID 类型哈希链表 */
attach_pid(p, PIDTYPE_SID); /* 将进程 @p 添加到关联 struct pid 的 SID 类型哈希链表 */
...
} else {
...
}
attach_pid(p, PIDTYPE_PID); /* 将进程 @p 添加到关联 struct pid 的 PID 类型哈希链表 */
nr_threads++;
}
...
}
3.2.1 从当前进程复制进程 PID 信息
/* kernel/fork.c */
p = dup_task_struct(current, node);
struct task_struct *tsk;
...
tsk = alloc_task_struct_node(node); /* 分配进程结构体 task_struct */
...
/* !!! 复制旧进程的 task_struct 数据 @orig 到 新进程 @tsk (包括 PID 信息) */
err = arch_dup_task_struct(tsk, orig);
*dst = *src;
return 0;
...
3.2.2 创建每进程的 PID 管理数据 (struct pid
) 并初始化
通过接口 alloc_pid()
创建每进程的 PID 管理数据 (struct pid
) 并初始化:
/* kernel/pid.c */
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;
int retval = -ENOMEM;
pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); /* 分配 struct pid 对象 */
...
tmp = ns;
pid->level = ns->level;
/* 在每个层级的 pid_namespace 中, 分配一个 进程 PID */
for (i = ns->level; i >= 0; i--) {
nr = alloc_pidmap(tmp); /* 从当前层级 @i 的 pid_namespace 中分配一个空闲的 进程 PID */
...
pid->numbers[i].nr = nr; /* 记录在当前层级 @i 的 pid_namespace 中的 进程 PID */
pid->numbers[i].ns = tmp; /* 记录当前层级 @i 关联的 pid_namesapce 对象 */
tmp = tmp->parent; /* 进入父级 pid_namespace (@i - 1) */
}
...
/* 使用 @pid 的 PID,PGID,SID 类型 进程列表 初始为空 */
for (type = 0; type < PIDTYPE_MAX; ++type)
INIT_HLIST_HEAD(&pid->tasks[type]);
upid = pid->numbers + ns->level;
spin_lock_irq(&pidmap_lock);
...
/* 将每层级 pid_namespace 为进程分配的 PID, 插入到全局 PID 哈希表 @pid_hash */
for ( ; upid >= pid->numbers; --upid) {
hlist_add_head_rcu(&upid->pid_chain,
&pid_hash[pid_hashfn(upid->nr, upid->ns)]);
upid->ns->nr_hashed++;
}
spin_unlock_irq(&pidmap_lock);
return pid; /* 返回分配的 struct pid 对象 */
...
}
3.2.3 绑定进程和其相关的 PID 管理数据
通过接口 init_task_pid()
设定进程 PID、PGID、SID
管理数据,实现将 PID 管理数据 struct pid
绑定到进程;通过接口 attach_pid()
将进程添加到 PID 管理数据 struct pid
的 PID、PGID、SID
类型哈希链表。如此,实现了进程和 PID 管理数据 struct pid
的双向绑定。其中,PGID、SID
的绑定,仅针对线程组的 group leader
进程,非 group leader
的 PGID、SID
信息,通过 dup_task_struct()
间接或直接复制自其所在 PID 命名空间对象 struct pid_namespace
(如 init_pid_ns
) 。
init_task_pid(p, PIDTYPE_PID, pid); /* 设置进程 @p 的 PID 信息 */
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current)); /* 设置进程 @p 的 PGID 信息 */
init_task_pid(p, PIDTYPE_SID, task_session(current)); /* 设置进程 @p 的 SID 信息 */
static inline struct pid