首页 > 代码库 > [转载] linux 进程管理-----pid哈希链表

[转载] linux 进程管理-----pid哈希链表

为了较快的从给定的pid值得到相应的宿主结构(进程描述符)指针,内核采用了pid哈希链表结构。
首先,以下的问题要理解:
1)为什么pid哈希链表只定义2048或者4096项(根据你的内存大小确定)?直接定义为pid最大值不是最好吗?
我们都知道,查找的最快方式就是数组了,可以在常数的时间内完成查找。假如我们的pid最大值为32768,那么我们完全可以定义一个struct task_struct* name[32768];进而可以最快速的从给定的pid值中找到其相应的宿主结构。也就是指向该pid进程的进程描述符指针。
然而,这确实一种不理智的方法。虽然进程PID的最大值为32768,但是,我们在实际应用中所能用的pid值要平均值要远远低于这个值。这样的话,会造成大量的物理空间浪费。假如我们用到了2768个pid,那么还有30000*4bytes的空间浪费。
2)特殊情况的需求
设想这样一种情况:假设内核必须回收一个给定pid值的线程组内的所有线程,(线程组内的所有线程的pid值都是相同的)在上述数组形式的定义中,是无法完成的。
而我们要做的是必须为这种形式的线程组(pid值相同)组织成一个链表。这个链表的节点,就是这个线程组内的全部线程。
而pid哈希链表能很好的完成上述的1)2)两点。
下面我们看一下内核对pid哈希链表的定义和初始化过程以及相关的操作。
static struct hlist_head *pid_hash[PIDTYPE_MAX];
系统在初始化期间手动的创建了一个含有PIDTYPE_MAX个元素的指针数组。
用于存放指向hash桶的指针。
并且在start_kernel期间调用pidhash_init函数对hash桶进行初始化。
303 void __init pidhash_init(void)
     /*  */
 304 {
 305         int i, j, pidhash_size;
 306         unsigned long megabytes = nr_kernel_pages >> (20 - PAGE_SHIFT);
 307 
 308         pidhash_shift = max(10, fls(megabytes * 4));
 309         pidhash_shift = min(12, pidhash_shift);
 310         pidhash_size = 1 << pidhash_shift;
 311 
 312         printk("PID hash table entries: %d (order: %d, %Zd bytes)\n",
 313                 pidhash_size, pidhash_shift,
 314                 PIDTYPE_MAX * pidhash_size * sizeof(struct hlist_head));
 315        #######以上为桶的大小设置和桶中含有的entry个数设置
     #######dmesg结果显示:PID hash table entries: 4096 (order: 12, 16384 bytes)
     #######pidhash_size(桶大小):4096*4bytes entries:4096
     #######也就是说桶中存有4096个指针,指针指向struct hlist_node结构 
 316         for (i = 0; i < PIDTYPE_MAX; i++) {
 317                 pid_hash[i] = alloc_bootmem(pidhash_size *
 318                                         sizeof(*(pid_hash[i])));
     #######在系统启动期间slab和alloc分配器都还没有建立好
     #######采用allocbootmem内存分配器对系统初始化期间的内存进行分配
 319                 if (!pid_hash[i])
 320                         panic("Could not alloc pidhash!\n");
 321                 for (j = 0; j < pidhash_size; j++)
 322                         INIT_HLIST_HEAD(&pid_hash[i][j]);
     #######将桶中指向struct hlist_node 结构的指针first初始化为NULL
 323         }
经过这个函数 pid哈希链表 被初始化完成。
 
与此同时,start_kernel期间也对pid位图进行了相应的初始化。利用pid位图对进程pid号进行管理是一种简单并且高效的形式。(bit:0代表这个pid可以使用bit:1代表这个pid已经被占用)

系统在初始化期间手动的为表示进程pid号的位图分配空间并且初始化。

     
  1.   50 typedef struct pidmap {
  2.   51 atomic_t nr_free;
  3.   52 void *page;
  4.   53 } pidmap_t;
  1.   55 static pidmap_t pidmap_array[PIDMAP_ENTRIES] =
  2.   56 { [ 0 ... PIDMAP_ENTRIES-1 ] = { ATOMIC_INIT(BITS_PER_PAGE), NULL } };

位图初始化期间:利用get_zeroed_page函数为位图提供一个物理上的页框,并且此页框被实现初始化为0.

这样,一个大小为4K的物理页框,可以表示的进程号个数为:4*1024*8=32768;

但是有一点我们要主要:由于进程号0的特殊性,我们事先将其设置为不可用,也就是将位图的0号位设置为1,并把相应表示目前可用的pid号的个数减1.

  1. 326 void __init pidmap_init(void)
  2.      /* */
  3.  327 {
  4.  328 int i;
  5.  329 
  6.  330 pidmap_array->page = (void *)get_zeroed_page(GFP_KERNEL);
  7.  331 set_bit(0, pidmap_array->page);
  8.  332 atomic_dec(&pidmap_array->nr_free);
  9.  333 
  10.  334 /*
  11.  335 * Allocate PID 0, and hash it via all PID types:
  12.  336 */
  13.  337 
  14.  338 for (i = 0; i < PIDTYPE_MAX; i++)
  15.  339 attach_pid(current, i, 0);
  16.  340 }

上述代码通过attach_pid函数将0号进程进行hash,将其hash到上面已经初始化好的hash链表桶中。

我们知道每个进程中都有:struct pid pids[PIDTYPE_MAX];而hash链表正是通过这个结构进行链接。

上述函数中,首先通过find_pid函数在指定类型和nr号的哈希链表中查找pid结构。如果找到则返回指向struct pid的指针 否则返回NULL。

接下来的代码根据find_pid的返回值,做出相应的动作。

1)pid为NULL,说明新attach的包含struct pid的进程描述符是哈希链表的第一个元素,将其插入头结点之后,并将struct pid的pid_list字段设置为NULL。struct list_head pid_list是由于连接具有相同nr号的进程描述符的连接件。

2)pid不为NULL,说明相应的哈希链表中已经存在了nr号为“nr”的进程描述符,我们只需将我们的进程描述符attach到相应的哈希链表的进程链表中。

 

通过上面的两个函数我们还可以得知:所有桶中(一般为4个)0号索引对应的哈希链表的第一个进程描述符元素都为0号进程的进程描述符。

而与attach_pid函数对应的操作为dettach_pid:将pid所在的进程描述符从对应的pid哈希链表中删除,这个函数的定义如下:

  1.  192 void fastcall detach_pid(task_t *task, enum pid_type type)
  2.      /* */
  3.  193 {
  4.  194 int tmp, nr;
  5.  195 
  6.  196 nr = __detach_pid(task, type);
  7.  197 if (!nr)
  8.  198 return;
  9.  199 
  10.  200 for (tmp = PIDTYPE_MAX; --tmp >= 0; )
  11.  201 if (tmp != type && find_pid(tmp, nr))
  12.  202 return;
  13.  203 
  14.  204 free_pidmap(nr);
  15.  205 }

核心函数为__detach_pid(task,type)

  1. 166 static fastcall int __detach_pid(task_t *task, enum pid_type type)
  2.      /* */
  3.  167 {
  4.  168 struct pid *pid, *pid_next;
  5.  169 int nr = 0;
  6.  170 
  7.  171 pid = &task->pids[type];
  8.  172 if (!hlist_unhashed(&pid->pid_chain)) {
  9.  173 
  10.  174 if (list_empty(&pid->pid_list)) {
  11.  175 nr = pid->nr;
  12.  176 hlist_del_rcu(&pid->pid_chain);
  13.  177 } else {
  14.  178 pid_next = list_entry(pid->pid_list.next,
  15.  179 struct pid, pid_list);
  16.  180 /* insert next pid from pid_list to hash */
  17.  181 hlist_replace_rcu(&pid->pid_chain,
  18.  182 &pid_next->pid_chain);
  19.  183 }
  20.  184 }
  21.  185 
  22.  186 list_del_rcu(&pid->pid_list);
  23.  187 pid->nr = 0;
  24.  188 
  25.  189 return nr;
  26.  190 }

函数首先判断task指向的进程是否已经被hash进pid哈希链表,注意有可能是哈希链表中的进程链表。

如果不是哈希链表的节点,直接将其从所在的进程链表中删除。此时返回nr=0,我们后面可以看到,detach_pid函数根据nr值来判断是否需要将pid位图中相应的pid位清0,以此来使继续可以被使用。

如果是hash链表中的节点,还要判断

1)是否此哈希链表节点所在的进程链表只有这个节点。如果是将nr设置为pid->nr 并将这个节点删除

2)如果哈希链表节点所在的进程链表还有别的节点。那么用下一个节点replace这个节点。注意此时并未设置nr的值。也就是说返回的nr=0

当从__datach_pid返回后,接下来的任务就是判断是否需要将pid对应的pid位图中相应的位清0

1)如果nr=0,不需要清0 函数直接返回。通过上面我们知道,函数在两种情况下会返回nr=0 :

  一,进程链表非空(此时暗指的含义就是这个pid还被其余的进程/线程使用着,不能在位图中将其释放; 

  二,删除的就是nr=0的进程描述符。这是也不需要在位图中将其释放;因为0号pid比较特殊是不允许其他进程使用的。

2)如果nr不等于0,还要判断在其他类型的哈希链表中是否存在这样的进程描述符,如果存在也表明其正在使用不能释放。

如果上述两步都没有return那么的确是需要将pid位图中想的pid表示位清0,以供系统使用。

上述的代码中有一处理解容易出现问题:

     
  1.  200 for (tmp = PIDTYPE_MAX; --tmp >= 0; )
  2.  201 if (tmp != type && find_pid(tmp, nr))
  3.  202 return;

为什么只搜索其他类型的hash链表而不处理我们刚刚将其删pid的类型的hash链表呢?因为对于刚刚删除的进程描述符的类型的hash链表 我们应经用nr的值来表明是否需要清位图位了。

如果nr=0那么删除的进程描述符所在的进程链表是非空的---不必删除

如果进程链表是空的那么nr=pid-->nr我们不考虑0号pid那么这个nr一定为非0就说明我这个类型的hash链表不用这个进程号了 你查查其他类型的hash链表是否还使用吧。

如果其他有一个在使用,OK 不删返回。如果都不用,那么我就清理门户pidbitmap 相应位设置为0了。

 

文章出处:http://blog.chinaunix.net/uid-25538637-id-271546.html

[转载] linux 进程管理-----pid哈希链表