linux 進程管理-----pid哈希鏈表

轉自:http://blog.chinaunix.net/uid-24227137-id-3595819.html

爲了較快的從給定的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.  
  2.   50 typedef struct pidmap {
  3.   51 atomic_t nr_free;
  4.   52 void *page;
  5.   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.  
  2.  200 for (tmp = PIDTYPE_MAX; --tmp >= 0; )
  3.  201 if (tmp != type && find_pid(tmp, nr))
  4.  202 return;

爲什麼只搜索其他類型的hash鏈表而不處理我們剛剛將其刪pid的類型的hash鏈表呢?因爲對於剛剛刪除的進程描述符的類型的hash鏈表 我們應經用nr的值來表明是否需要清位圖位了。

如果nr=0那麼刪除的進程描述符所在的進程鏈表是非空的---不必刪除

如果進程鏈表是空的那麼nr=pid-->nr我們不考慮0號pid那麼這個nr一定爲非0就說明我這個類型的hash鏈表不用這個進程號了 你查查其他類型的hash鏈表是否還使用吧。

如果其他有一個在使用,OK 不刪返回。如果都不用,那麼我就清理門戶pidbitmap 相應位設置爲0了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章