通过pid查找进程task_struct结构体
如何通过进程id查找进程的描述符task_struct?在实际的工作中,我的驱动中需要通过读取写进内存中的进程id值查找对应的进程是否还在运行,或者是已经退出。通过阅读内核代码,内核中已有相应的API函数来供我们使用。下面是我在使用过程中的一些笔记总结。
linux系统上运行的进程可能成百上千的,或者更多,如何能够快速的通过pid值反向的查找task_struct,内核的方法是通过Hash散列表的方式。下面结合3.16.7内核版本来分析pid hash表的实现。
void __init pidhash_init(void) { unsigned int i, pidhash_size;
pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 0, 18, HASH_EARLY | HASH_SMALL, &pidhash_shift, NULL, 0, 4096); pidhash_size = 1U << pidhash_shift;
for (i = 0; i < pidhash_size; i++) INIT_HLIST_HEAD(&pid_hash[i]); } |
pidhash_init函数在系统启动时由start_kernel函数调用。Pid hash表的大小由运行机器的总内存数决定,范围在16~4096个之间。在之前老的内核(如2.6.12.6)pid hash表包含四个表,分别针对enum pid_type的四种不同类型PID的情况。
进程创建时,是如何将task_struct与pid_hash散列表关联起来的?在copy_process函数中会判断参数pid指针是否等于init_struct_pid,如果不相等,就会调用alloc_pid函数分配一个新的struct pid类型的pid实例,并初始化。如果相等,说明是idle进程的创建。一般情况下,pid指针为空。下面看下alloc_pid函数的实现:
struct pid *alloc_pid(struct pid_namespace *ns) { pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
tmp = ns; pid->level = ns->level; for (i = ns->level; i >= 0; i--) { /*遍历pid namsespace*/ nr = alloc_pidmap(tmp);
pid->numbers[i].nr = nr; pid->numbers[i].ns = tmp; tmp = tmp->parent; }
for (type = 0; type < PIDTYPE_MAX; ++type) INIT_HLIST_HEAD(&pid->tasks[type]); /*初始化每个id类型的散列表头*/
upid = pid->numbers + ns->level; spin_lock_irq(&pidmap_lock); 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); |
参数ns参数是创建新进程时父进程task_struct描述符的命名空间指针nsproxy的pid namespace,一般情况下是指向init_pid_ns,如果在clone时候有指定CLONE_NEWPID标志,则会在copy_process中调用alloc_pid之前调用copy_namespaces创建一个新的pid namespace。
函数首先调用kmem_cache_alloc从slab中分配pid内存。上面讲到如果clone时有指定标志CLONE_NEWPID的情况下,会创建一个新的pid namespace,此新建的pid namespace的parent指向父进程的pid namespace,相应的level也加一。这个新建的进程对于新创建的pid namespace和父进程的pid namespace,以及更初始的pid namespace都是可见的
在第一个for循环中,从新创建PID namespace的level(此成员表示可以看到该进程的命名空间的数目)到0,给每个可见此进程的命名空间都分配一个局部的pid值。并将新分配的局部pid值nr赋给PID描述符的对应的命名空间numbers的nr字段,将pid namespace指针tmp赋给对应的命名空间numbers的ns字段。
struct pid结构体中成员tasks是一个数组,每个数组成员是一个散列表的表头,对应于pid_type枚举中的一个id类型。在第二个for循环中,初始化tasks数组对应的每个id类型的散列表头。
Pid结构体的成员numbers,虽然是一个长度为1的数组,但是因为它是在结构体的最后,可以根据pid所属的空间的个数分配更多的空间。numbers数组的大小由pid->level字段决定。Level值越大的命名空间级别越低,它能够被此pid的level值更小的命名空间看见,也就是说父命名空间能给看到子命名空间,反之子命名空间不能看见父命名空间。
代码这里首先将upid指向此pid描述符的级别最低的命名空间,然后在for循环中遍历每层的命名空间,以当前pid namespace的进程id值nr和命名空间指针ns为key值,调用pid_hashfn函数生成pid_hash全局数组中的bucket id值,并将此进程链入此bucket id所在的hash散列表中。
上面是alloc_pid函数分配一个新的pid描述符的大致过程,接着回到copy_process函数看看创建新进程的余下的和pid hash相关的代码。
copy_process函数代码片段:
if (likely(p->pid)) { init_task_pid(p, PIDTYPE_PID, pid); if (thread_group_leader(p)) { /*进程是线程组的组长*/ init_task_pid(p, PIDTYPE_PGID, task_pgrp(current)); init_task_pid(p, PIDTYPE_SID, task_session(current));
attach_pid(p, PIDTYPE_PGID); attach_pid(p, PIDTYPE_PGID); } else {
} attach_pid(p, PIDTYPE_PID); /*把PIDTYPE_PID类型的pid插入到相应的链表中*/ } |
如果前面alloc_pid创建新的pid成功或者本身没有指定CLONE_NEWPID标志,就会进入上述代码片段执行。前面讲到枚举类型pid_type的不同id类型,在我分析的新内核(3.16.7)中是三种:
PIDTYPE_PID 对应进程PID
PIDTYPE_PGID 进程组领头进程PID
PIDTYPE_SID 会话领头进程PID
首先调用函数init_task_pid将初始化新创建进程的类型PIDTYPE_PID的pid实例,如果此新创建的进程是线程组的组长,则初始化此进程的PIDTYPE_PGID类型和PIDTYPE_SID类型的实例,并且调用attach_pid函数将此进程进行Hash,加入到已经初始化好的pid_hash对应bucket的散列表中。
这样就建立起了一个双向链表的关系,进程可以通过task_struct的task->pids[type]的成员pid访问pid实例,pid实例也可以通过遍历tasks[type]链表来查找进程task_struct。
经过上面的分析,一个进程可以属于多个不同的命名空间,同一个进程在不同的命名空间中有不同的局部pid值,多个进程task_struct可以共用一个pid描述符。
更多推荐
所有评论(0)