linux2.6.11的內核中,為了方便管理linux的進程,主要建了5種linux鏈表。每個鏈表節點之間的互聯有兩種方式,一種是hash節點之間的互聯,通過hlist_node的數據結構來實現;另一種就是list_head類型的數據結構來互聯。看linux內核的人對這兩種類型的數據結構肯定是不會陌生的,因為它們在linux內核中無處不在。
1 進程直接的互連
通過任務描述符結構task_struct結構中的tasks成員來實現各個節點之間的互連,它是list_head類型。這個鏈表是一個循環的雙向鏈表,開始的時候只有init_task這一個進程,它是內核的第一個進程,它的初始化是通過靜態分配內存,"手動"(其它的進程初始化都是通過動態分配內存初始化的)進行的,每新建一個進程,就通過SET_LINKS宏將該進程的task_struct結構加入到這條雙向鏈表中,不過要注意的是如果一個進程新建一個線程(不包括主線程),也就是輕量級進程,它是不會加入該鏈表的。通過宏for_each_process可以從init_task開始遍歷所有的進程。
2 TASK_RUNNING狀態的進程鏈表
為了能讓調度程序在固定的時間內選出”最佳“可運行的進程,與隊列中可運行的進程數無關,建立了多個可運行進程鏈表,每個優先級對應一個,總共有140個。linux內核定義了一個prio_array_t類型的結構體來管理這140個鏈表。每個可運行的進程都在這140個鏈表中的一個,通過進程描述符結構中的run_list來實現,它也是一個list_head類型。enqueue_task是把進程描述符插入到某個可運行鏈表中,dequeue_task則從某個可運行鏈表中刪除該進程描述符。TASK_RUNNING狀態的prio_array_t類型的結構體是runqueue結構的arrays[1]成員。
3 進程間的關系
linux進程間的關系有兩種,一種是父進程與子進程間的父子關系,一種是進程同屬一個父進程的兄弟關系。linux中是通過進程描述符中的children和sibling來實現這種關系的,它們都是list_head類型的。children的next指向的是該進程最新的子進程,prev指向的是該該進程最老的子進程,sibling的next指向的是它父進程中比它更老的子進程,prev指向的是它父進程中比它更新的子進程。最新子進程的slibling.prev指向的是父進程,最老子進程的slibling.next也是指向父進程。這樣通過children和sibling實現了一個循環的雙向鏈表,該雙向鏈表以父進程描述符為頭節點。
進程間親屬關系
4 pidhash鏈表
為了通過pid找到進程的描述符,如果直接遍歷進程間互聯的鏈表來查找進程id為pid的進程描述符顯然是低效的,所以為了更為高效的查找,linux內核使用了4個hash散列表來加快查找,之所以使用4個散列表,是為了能根據不同的pid類型來查找進程描述符,它們分別是進程的pid,線程組領頭進程的pid,進程組領頭進程的pid,會話領頭進程的pid。每個類型的散列表中是通過宏pid_hashfn(x)來進行散列值的計算的。每個進程都可能同時處於這是個散列表中,所以在進程描述符中有一個類型為pid結構的pids成員,通過它可以將進程加入散列表中,pid結構中包含解決散列沖突的pid_chain成員,它是hlist_node類型的,還有一個是將相同pid鏈起來的pid_list,它是list_head類型。
pid散列表
5 等待隊列
linux把等待同一個事件發生或資源的進程都鏈接在一起形成一個帶頭節點的雙向鏈表。等待隊列的頭是用類型wait_queue_head_t描述,里面包含了list_head類型的task_list成員。等待隊列中節點的類型用wait_queue_t描述,該結構里有task_struct類型的指針task和list_head類型的task_list成員。為什么不像前面4個隊列中一樣,將list_head類型的task_list成員放到進程的描述符里來形成鏈表呢?原因是linux等待隊列太多了,每個事件,每個資源都可以形成一個等待隊列,一個進程還可以等待多個事件的發生,所以通過一個單獨的類型來形成隊列是需要的。linux通過sleep_on函數來將某個進程加入到某個等待隊列中和從等待隊列中刪除。調用sleep_on的進程都會主動讓出cpu進入等待狀態,可以通過wake_up來喚醒某個等待狀態的進程。