pid即进程描述符在linux kernel中的分配和管理比较复杂。 本文分析了其相关数据结构以及函数。
代码基于v3.0.3,和pid相关的数据结构,函数定义:
include/linux/pid.h
include/linux/pid_namespace.h
kernel/pid.c kernel/pid_namespace.c
struct pid
struct pid
{
atomic_t count;
unsigned int level;
struct hlist_head tasks[pidtype_max];
struct rcu_head rcu;
struct upid numbers[1];
};
- count
是指向该数据结构的引用次数。 - level
是该pid在pid_namespace中处于第几层。当level=0时表示是global namespace,即最高层。pid_namespace这个数据结构将在后面进行解释。 - tasks[pidtype_max]
数组中每个元素都代表了不同的含义。pidtype_max表示pid所表示的类型的最大数。 该值定义在enum pid_type中
enum pid_type
{
pidtype_pid,
pidtype_pgid,
pidtype_sid,
pidtype_max
};
pidtype_pid代表进程描述符(pid) 。 pidtype_pgid代表一组进程描述符。 一组进程(process)可以组成一个群组,并且有一个组描述符。 这样的好处是如果有一个信号是针对这个组描述符,该群组内的所有进程都可以接受到。 pidtype_sid是对组描述符再做一个群组,形成一个session。这是更高一个层次的抽象。
tasks[i]指向的是一个哈希表。譬如说tasks[pidtype_pid]指向的是pid的哈希表。
-
rcu域
-
numbers[1]域
指向的是upid结构体。 numbers数组的本意是想表示不同的pid_namespace。 一个pid可以属于不同的namespace, numbers[0]表示global namespace,numbers[i]表示第i层namespace,i越大所在层级越低。目前该数组只有一个元素, 即global namespace。所以namepace的概念虽然引入了pid,但是并未真正使用,在未来的版本可能会用到。
接下来我们再看看upid这个数据结构
struct upid
struct upid {
int nr;
struct pid_namespace × ns;
struct hlist_node pid_chain;
};
pid结构体中的numbers域指向了upid结构体。linux内核将所有进程的upid都存放在一个哈希表中(pid_hash),以方便查找和统一管理。因此,pid结构体中的numbers[0]指向的upid instance存放在pid_hash里。通过pid_chain即哈希表的节点就能够找到该upid所在pid_hash中的位置。
- nr
是pid的值, 即 task_struct中 pid_t pid域的值。 - ns
指向该pid所处的namespace。
接下来再看看pid_namespace结构体
struct pid_namespace
介绍pid_namespace相关的数据结构前,我们来看看设计它们的本意是什么。 linux中增加namespace这个概念的目的是为了虚拟化和方便管理。 比如在不同的namespace中可以有pid相同的进程。 pid_namespace的结构是层次化的。而且在child namespace中的进程一定会有parent namespace的映射。这句话可能不太好理解。可以结合下面这张图
以上图为例子,此时pid_hash全局哈希表中此时会存放15个(9 3 3)upid的instance。
struct pid_namespace {
struct kref kref;
struct pidmap pidmap[pidmap_entries];
int last_pid;
struct task_struct *child_reaper;
struct kmem_cache *pid_cachep;
unsigned int level;
struct pid_namespace *parent;
};
- kref
表示指向pid_namespace的个数。 - pidmap
表示分配pid的位图。当需要分配一个新的pid时只需查找位图,找到bit为0的位置并置1,然后更新统计数据域(nr_free)
struct pidmap {
atomic_t nr_free;
void *page;
};
(1)nr_free表示还能分配的pid的数量。
(2)page指向的是存放pid的物理页。
所以pidmap[pidmap_entries]域表示该pid_namespace下pid已分配情况。
last_pid用于pidmap的分配。指向最后一个分配的pid的位置。(不是特别确定)
-
child_reaper
指向的是一个进程。 该进程的作用是当子进程结束时为其收尸(回收空间)。由于目前只支持global namespace,这里child_reaper就指向init_task。 -
pid_cachep
指向分配pid的slab的地址。 -
level
表示该namespace处于哪一层, 现在这里显然是0。 -
parent
指向该namespace的父亲namespace。 现在一定是null。
struct task_struct
linux内核通过task_struct来管理进程。在task_struct中,和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
指该进程的进程描述符。 后面会介绍在fork函数中如何对其进行赋值的。
对于用户态程序来说,调用getpid()函数其实返回的是tgid。想想是为什么?:) - tgid
指该进程的线程描述符。在linux内核中对线程并没有做特殊的处理,还是由task_struct来管理。所以从内核的角度看, 用户态的线程本质上还是一个进程。对于同一个进程(用户态角度)中不同的线程其tgid是相同的,但是pid各不相同。 主线程即group_leader(主线程会创建其他所有的子线程)。如果是单线程进程(用户态角度),它的pid等于tgid。 - group_leader
除了在多线程的模式下指向主线程,还有一个用处, 当一些进程组成一个群组时(pidtype_pgid), 该域指向该群组的leader。 - nsproxy
指向namespace相关的域。
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;
struct net *net_ns;
};
通过nsproxy域可以知道该task_struct属于哪个pid_namespace, 当然现在一定是global namespace。(已经讲了很多次了:))其他一些域也是namespace相关,这里就不展开解释了。
- pids[pidtype_max]
指向了和该task_struct相关的pid结构体。
struct pid_link
{
struct hlist_node node;
struct pid *pid;
};
在linux内核中如果想获得该task_struct所对应的pid可以调用task_pid()函数, 这个函数的实现非常简单
static inline struct pid *task_pid(struct task_struct *task)
{
return task->pids[pidtype_pid].pid;
}
fork函数中如何分配一个新的pid?
fork(), vfork()还有clone()函数最终都是通过调用do_fork()来进行工作。 分配新的pid是在copy_process()函数实现的。 do_fork()函数会调用copy_process(), 它们之间的关系我会在以后的文章中进行介绍。
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
...
if (pid != &init_struct_pid) {
retval = -enomem;
pid = alloc_pid(p->nsproxy->pid_ns);
if (!pid)
goto bad_fork_cleanup_io;
}
p->pid = pid_nr(pid);
p->tgid = p->pid;
if (clone_flags & clone_thread)
p->tgid = current->tgid;
...
}
我只将和pid分配的代码列出来了。
alloc_pid函数将分配一个新的pid struct。 简单的说该函数的功能是在pidmap上找到一个未用的pid bit,如若找不着,着说明已经没有可用的pid了,该namespace所在pid配给全部用完。 然后将其保存到pid_hash的哈希表里,然后再将pid结构体返回。
pid_nr函数
static inline pid_t pid_nr(struct pid *pid)
{
pid_t nr = 0;
if (pid)
nr = pid->numbers[0].nr;
return nr;
}
返回该pid所在global namespace的值。
后面几行代码用于区分进程和线程中tgid的值。