1. 概念
什么是 PID ?
PID 是内核内部对进程的一个标识符,用来唯一标识一个进程(task)、进程组(process group)、会话(session)。PID 及其对应进程存储在一个哈希表中,方便依据 PID 快速访问进程 task_struct。
因此Linux 内核在设计管理ID的数据结构时,要充分考虑以下因素:
- 如何快速地根据进程的 task_struct、ID 类型、命名空间找到 PID ?
- 如何快速地根据 PID、命名空间、ID 类型找到对应进程的 task_struct ?
- 如何快速地给新进程在可见的命名空间内分配一个唯一的 PID ?
注:本文所附内核源码版本为 v4.11.6。
2. 数据结构
2.1 进程ID
内核中 PID 的定义include/linux/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];
};
-
count 是该数据结构被引用的次数。
-
level 是该 pid 在 pid_namespace 中所处层级。当 level=0 时表示是 global namespace,即最高层。
- tasks[i] 指向 PID 对应的 task_struct。PIDTYPE_MAX 是 pid 的类型数。一个或多个进程可以组成一个进程组,进程组ID(PGID)为进程组领导进程 (process group leader)的PID;一个或多个进程组可以组成一个会话,会话ID(SID)为会话领导进程(session leader)的PID。PIDTYPE_MAX定义在include/linux/pid.h中:
enum pid_type { PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID, PIDTYPE_MAX };
-
rcu 用于保证数据同步,具体机制另行讨论。
- numbers[1] 域是一个可扩展 upid 结构体。 一个 PID 可以属于不同的 namespace , numbers[0] 表示 global namespace,numbers[i] 表示第 i 层 namespace,i 越大所在层级越低。upid 结构体定义include/linux/pid.h如下:
struct upid { /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ int nr; struct pid_namespace *ns; struct hlist_node pid_chain; };
-
nr是pid的值, 即 task_struct 中 pid_t pid 域的值。
-
ns指向该 pid 所处的 namespace。
-
pid_chain 是 pid_hash 哈希表节点。linux内核将所有进程的upid都存放在一个哈希表(pid_hash)中,以方便查找和统一管理。通过 pid_chain 能够找到该 upid 所在 pid_hash 中的位置。
-
2.2 进程命名空间
再来看 pid 命名空间 pid_namespace 的定义(include/linux/pid_namespace.h)
struct pid_namespace {
struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES];
struct rcu_head rcu;
int last_pid;
unsigned int nr_hashed;
struct task_struct *child_reaper;
struct kmem_cache *pid_cachep;
unsigned int level;
struct pid_namespace *parent;
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct work_struct proc_work;
kgid_t pid_gid;
int hide_pid;
int reboot; /* group exit code if this pidns was rebooted */
struct ns_common ns;
};
-
kref 表示指向 pid_namespace 的个数。
- pidmap 结构体表示分配pid的位图,pidmap[PIDMAP_ENTRIES] 域存储了该 pid_namespace 下 pid 已分配情况。pidmap 结构体定义(include/linux/pid_namespace.h)如下:
struct pidmap { atomic_t nr_free; void *page; }; #define BITS_PER_PAGE (PAGE_SIZE * 8) #define BITS_PER_PAGE_MASK (BITS_PER_PAGE-1) #define PIDMAP_ENTRIES ((PID_MAX_LIMIT+BITS_PER_PAGE-1)/BITS_PER_PAGE) // include/linux/threads.h #define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000) /* * A maximum of 4 million PIDs should be enough for a while. * [NOTE: PID/TIDs are limited to 2^29 ~= 500+ million, see futex.h.] */ #define PID_MAX_LIMIT (CONFIG_BASE_SMALL ? PAGE_SIZE * 8 : \ (sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT))
-
nr_free 表示还能分配的 pid 的数量。
-
page 指向的是存放 pid 的物理页。
-
-
rcu 同样用于保证数据同步。
-
last_pid 是最后一个已分配的 pid。
-
nr_hashed 统计该命名空间已分配PID个数。
-
child_reaper指向的是一个进程。 该进程的作用是当子进程结束时为其收尸(回收空间)。global namespace 中child_reaper 指向 init_task。
-
pid_cachep 域指向分配 pid 的 slab 的地址。
-
level 表示该命名空间所处层级。
- parent 指向该命名空间的父命名空间。
2.3 进程描述符
进程描述符 task_struct 是 linux 内核中最重要的概念之一,其结构复杂(include/linux/sched.h),详情另行分析。其中,与 pid 有关的域如下:
struct task_struct {
···
pid_t pid;
pid_t tgid;
struct task_struct *group_leader;
struct pid_link pids[PIDTYPE_MAX];
struct nsproxy *nsproxy;
···
};
- pid 即是该进程的 id。使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值。
- tgid 是线程组 id。在一个进程中,如果以 CLONE_THREAD 标志来调用 clone 建立的进程就是该进程的一个线程,它们处于一个线程组。处于相同的线程组中的所有进程都有相同的 TGID;线程组组长的 TGID 与其 PID 相同;一个进程没有使用线程,则其 TGID 与 PID 也相同。
- group_leader 除了在多线程的模式下指向主线程外, 当一些进程组成一个群组时(PIDTYPE_PGID), 该域指向该群组的leader。
- pids[PIDTYPE_MAX] 指向了和该 task_struct 相关的 pid 结构体。其定义(include/linux/pid.h)如下:
struct pid_link { struct hlist_node node; struct pid *pid; };
- nsproxy指针指向namespace相关的域。其定义(include/linux/nsproxy.h)如下:
struct nsproxy { atomic_t count; struct uts_namespace *uts_ns; struct ipc_namespace *ipc_ns; struct mnt_namespace *mnt_ns; struct pid_namespace *pid_ns_for_children; struct net *net_ns; struct cgroup_namespace *cgroup_ns; };
- 与其它命名空间不同。此处 pid_ns_for_children 指向该进程的子进程会使用的 pid_namespace,该进程本身所属的 pid_namespace 可以通过 task_active_pid_ns 方法获得。
- nsproxy 被所有共享命名空间的 task_struct 共享,随命名空间的复制而复制。
2.4 数据结构关系
举例来说,在level 2 的某个命名空间上新建了一个进程,分配给它的 pid 为45,映射到 level 1 的命名空间,分配给它的 pid 为 134;再映射到 level 0 的命名空间,分配给它的 pid 为289,对于这样的例子,如下图所示为其表示:
结构图示例,图片来源郭海林的博客
3. 方法/函数
现在就可以解决本文开篇提出的问题了。
3.1 查询PID
-
获取与 task_struct 相关的 pid 结构体实例
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; }
-
获取与 task_struct 相关的 PID 命名空间
struct pid_namespace *task_active_pid_ns(struct task_struct *tsk) { return ns_of_pid(task_pid(tsk)); }
static inline struct pid_namespace *ns_of_pid(struct pid *pid) { struct pid_namespace *ns = NULL; if (pid) ns = pid->numbers[pid->level].ns; return ns; }
-
获取 pid 实例中的 PID
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; }
获取指定 ns 中的 PID时,需注意,由于 PID 命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能。因此,函数 中 需要确保当前命名空间的 level 小于等于产生局部 PID 的命名空间的 level。
此外,内核还封装有直接获取初始命名空间、当前命名空间对应 PID 的方法:
static inline pid_t pid_nr(struct pid *pid) { pid_t nr = 0; if (pid) nr = pid->numbers[0].nr; return nr; } pid_t pid_vnr(struct pid *pid) { return pid_nr_ns(pid, task_active_pid_ns(current)); }
3.2 查找 task_struct
-
获得 pid 实体。
根据 PID 以及指定命名空间计算在 pid_hash 数组中的索引,然后遍历散列表找到所要的 upid, 再根据内核的 container_of 机制找到 pid 实例。代码(kernel/pid.c)如下:
struct pid *find_pid_ns(int nr, struct pid_namespace *ns) { struct upid *pnr; hlist_for_each_entry_rcu(pnr, &pid_hash[pid_hashfn(nr, ns)], pid_chain) if (pnr->nr == nr && pnr->ns == ns) return container_of(pnr, struct pid, numbers[ns->level]); return NULL; }
由此,也可以根据当前命名空间下的局部 PID 获取对应的 pid实例:
struct pid *find_vpid(int nr) { return find_pid_ns(nr, task_active_pid_ns(current)); }
-
根据 pid 及 PID 类型获取 task_struct
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; }
3.3 分配PID
-
为新进程 task_struct 分配 pid 域
新的进程使用 alloc_pid 方法分配 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 结构体 pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); if (!pid) return ERR_PTR(retval); // 初始化进程在各级命名空间的 PID,直到全局命名空间(level 为0)为止 tmp = ns; pid->level = ns->level; for (i = ns->level; i >= 0; i--) { nr = alloc_pidmap(tmp); //分配一个局部PID if (nr < 0) { retval = nr; goto out_free; } pid->numbers[i].nr = nr; pid->numbers[i].ns = tmp; tmp = tmp->parent; } // 若为命名空间的初始进程 if (unlikely(is_child_reaper(pid))) { // 0 if (pid_ns_prepare_proc(ns)) goto out_free; } get_pid_ns(ns); atomic_set(&pid->count, 1); for (type = 0; type < PIDTYPE_MAX; ++type) INIT_HLIST_HEAD(&pid->tasks[type]); // // 初始化 pid->task[] 结构体,值为NULL upid = pid->numbers + ns->level; spin_lock_irq(&pidmap_lock); if (!(ns->nr_hashed & PIDNS_HASH_ADDING)) goto out_unlock; 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; out_unlock: spin_unlock_irq(&pidmap_lock); put_pid_ns(ns); out_free: while (++i <= ns->level) free_pidmap(pid->numbers + i); kmem_cache_free(ns->pid_cachep, pid); return ERR_PTR(retval); }
-
从指定命名空间中分配唯一PID
static int alloc_pidmap(struct pid_namespace *pid_ns) { int i, offset, max_scan, pid, last = pid_ns->last_pid; struct pidmap *map; pid = last + 1; // 默认最大值在 include/linux/threads.h 中定义为 (CONFIG_BASE_SMALL ? 0x1000 : 0x8000) if (pid >= pid_max) pid = RESERVED_PIDS; // RESERVED_PIDS = 300 offset = pid & BITS_PER_PAGE_MASK; map = &pid_ns->pidmap[pid/BITS_PER_PAGE]; /* * If last_pid points into the middle of the map->page we * want to scan this bitmap block twice, the second time * we start with offset == 0 (or RESERVED_PIDS). */ max_scan = DIV_ROUND_UP(pid_max, BITS_PER_PAGE) - !offset; for (i = 0; i <= max_scan; ++i) { if (unlikely(!map->page)) { void *page = kzalloc(PAGE_SIZE, GFP_KERNEL); /* * Free the page if someone raced with us * installing it: */ spin_lock_irq(&pidmap_lock); if (!map->page) { map->page = page; page = NULL; } spin_unlock_irq(&pidmap_lock); kfree(page); if (unlikely(!map->page)) return -ENOMEM; } if (likely(atomic_read(&map->nr_free))) { for ( ; ; ) { if (!test_and_set_bit(offset, map->page)) { atomic_dec(&map->nr_free); set_last_pid(pid_ns, last, pid); return pid; } offset = find_next_offset(map, offset); if (offset >= BITS_PER_PAGE) break; pid = mk_pid(pid_ns, map, offset); if (pid >= pid_max) break; } } if (map < &pid_ns->pidmap[(pid_max-1)/BITS_PER_PAGE]) { ++map; offset = 0; } else { map = &pid_ns->pidmap[0]; offset = RESERVED_PIDS; if (unlikely(last == offset)) break; } pid = mk_pid(pid_ns, map, offset); } return -EAGAIN; }
-
回收PID
static void free_pidmap(struct upid *upid) { int nr = upid->nr; struct pidmap *map = upid->ns->pidmap + nr / BITS_PER_PAGE; int offset = nr & BITS_PER_PAGE_MASK; clear_bit(offset, map->page); atomic_inc(&map->nr_free); }