天天看點

Linux核心設計與實作(3)---Process Management

1.程序

程序就是正在執行的程式代碼的實時結果,不僅包含可執行代碼,還包括其他資源,比如:打開的檔案,挂起的信号,核心内部資料結構,處理器狀态,一個或多個具有記憶體映射的記憶體位址空間及一個或多個執行線程,全局變量資料段等。

核心需要有效而透明的管理所有細節。

線程,每個線程擁有一個獨立的程式計數器,程序棧和一組寄存器。核心排程對象是線程而不是程序。

現代作業系統提供兩種虛拟機制:虛拟處理器和虛拟記憶體,線程之間可以共享虛拟記憶體,但每個都有各自的虛拟處理器。

Linux中,新程序是由fork()來實作的,fork()實際上由clone()系統調用實作,程式通過exit()系統調用退出執行,這個函數會終結程序并釋放其占用的資源,父程序可以通過wait4()查詢子程序是否終結。程序退出執行後被設定為僵死狀态,直到他父程序調用wait()或waitpid()。

2.程序描述符

核心把程序的清單存放在一個叫做任務隊列的雙向環形連結清單中,連結清單中每一項(task_struct類型)都稱為程序描述符。

程序描述符包括一個程序的具體所有資訊:打開的檔案,程序位址空間,挂起的信号,程序狀态等。在

中定義。

Linux通過slab配置設定器配置設定task_struct結構,這樣能達到對象複用和緩存目的。

Linux在棧底或棧頂建立一個新的結構struct thread_info來存放task_struct

此處)折疊或打開

  1. struct thread_info {
  2. *task;
  3. *exec_domain;
  4. ;
  5. ;
  6. ;
  7. int preempt_count;
  8. ;
  9. ;
  10. *sysenter_return;
  11. int uaccess_err;
  12. };

3.程序狀态

task_struct中的state域描述了程序的目前狀态,每個程序必處于以下5個狀态之一。

TASK_RUNNING—程序是可執行的,正在執行或者在運作隊列中等待執行

TASK_INTERRUPTIBLE—程序正在睡眠(阻塞),等待某個條件達成。該條件一旦到來就進入TASK_RUNNING狀态,可以接收信号而提前喚醒。

TASK_UNINTERRUPTIBLE—除了不能響應信号,與TASK_INTERRUPTIBLE一樣,這個狀态,程序必須在等待時不受幹擾或等待事件很快就會發生時出現。

__TASK_TRACED—被其他程序跟蹤的程序,比如通過ptrace對調試程式進行跟蹤

__TASK_STOPPED

—停止。程序沒有投入運作,也不能投入運作。這種情況一般發生在程序收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的時候,此外在調試期間接收到任何信号,都會使程序進入這種狀态

設定程序,set_task_state(task,state),必要的時候,它會設定記憶體屏蔽來強制其他處理器作重新排序(SMP系統中才有必要)

程序上下文:一個程式調用了系統調用,或觸發了某個異常,它就陷入了核心空間。此時,核心“代表程序執行”,并處于程序上下文中,這裡current宏是有效的;這個過程程序是可以被排程的。

中斷上下文:系統不代表程序執行,而是執行一個中斷處理函數;不能被排程。

4.程序家族樹

所有程序都是init程序的後代,核心在系統啟動的最後階段啟動init程序,該程序讀取系統初始化腳本并執行其他的相關程式。

每個程序都有自己的父程序,和零個或多個子程序,所有擁有同一個父程序的程序是兄弟程序。

此處)折疊或打開

  1. //通路父程序
  2. *my_parent = current->parent;
  3. //依次通路所有子程序
  4. *task;
  5. *list;
  6. (list, &current->children) {
  7. = list_entry(list, struct task_struct, sibling);
  8. /* task now points to one of current\'s children */
  9. }
  10. //周遊系統中所有程序
  11. (task->tasks.next, struct task_struct, tasks)
  12. (task->tasks.prev, struct task_struct, tasks)

5.程序建立

(1)許多作業系統都提供了産生程序的機制,首先在新的位址空間建立程序,讀入可執行檔案,最後開始執行。Unix吧這個步驟分解到兩個單獨的函數去執行,fork()和exec()。首先fork()通過拷貝目前程序建立一個子程序,其與父程序差別是PID,PPID,某些資源和統計量(如挂起信号,不用繼承),exec負責讀取可執行檔案并将其載入位址空間開始運作。

(2)寫時拷貝

是一種可以推遲甚至免除拷貝資料的技術,核心此時并不複制,而是與父程序共享一個拷貝。隻有在需要寫入時,才會複制資料。

fork()的實際開銷就是,複制父程序的頁表以及給子程序建立唯一的程序描述符。

(3)fork建立程序過程

fork(),vfork()和__clone()庫函數都根據各自需要的參數辨別去調用clone()->調用do_fork()->調用copy_process(),copy_process()完成如下過程

①調用dup_task_struct為新程序建立一個新的核心棧,thread_info結構和task_struct,這些值與目前程序的值相同,程序描述符也相同。

②檢查確定建立子程序後,目前使用者擁有的程序數沒有超出為其配置設定的資源限制

③程序描述符内的許多成員都被清零或初始化,以與父程序區分開來,統計資訊一般不繼承,task_struct中的大多數資料依然未修改。

④子程序狀态被設定為TASK_UNINTERRUPTIBLE,以保證它不會投入運作。

⑤copy_process調用copy_flags(),更新task_struct的flag成員。

⑥調用alloc_pid()為新程序配置設定一個有效的PID。

⑦根據傳遞給clone()的參數辨別,拷貝或共享打開的檔案,信号處理函數,程序位址空間等。

⑧最後copy_process做收尾工作,傳回一個指向子程序的指針。

傳回到do_fork(),如果copy_process()成功傳回,子程序被喚醒并投入運作,核心有意選擇子程序首先執行。(父程序先執行可能會向位址空間寫入)

(4)vfork()

除了不拷貝父程序頁表項外,vfork()系統調用與fork()功能相同,子程序作為父程序的一個單獨的線程在它的位址空間裡運作,父程序被阻塞,直到子程序推出或執行exec().

6.線程在Linux中的實作

(1)從核心角度來看,并沒有線程這個概念,Linux把所有線程都當作程序來實作,核心并沒有準備特别的排程算法或是定義特别的資料結構來表征線程,它僅僅被視為一個與其他程序共享某些資源的程序。每個線程都擁有唯一隸屬于自己的task_struct.

對于多個線程并行,Linux隻是為每個線程建立普通的task_struct的時候指定他們共享某些資源。

(2)建立線程

線程建立與普通程序建立類似,隻不過在調用clone()的時候需要會傳遞一些參數辨別來指明需要共享的資源

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

普通fork()

clone(SIGCHLD, 0);

vfork()

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

傳遞給clone()的參數标志據诶的那個了新建立程序的行為和父子程序之間共享資源種類。

(3)核心線程

核心經常需要在背景執行一些操作,這種任務可以通過核心線程來完成---獨立運作在核心空間的标準線程。它和普通線程的差別在于,核心線程沒有獨立的程序空間(指向位址空間的mm指針為NULL),隻在核心運作。跟普通線程一樣可以被排程,也可以被搶占。

核心線程隻能由其他核心線程建立,Linux是通過從kthread核心程序衍生出所有新的核心線程的。核心建立新核心線程方法:

此處)折疊或打開

  1. struct task_struct *kthread_create(int (*threadfn)(void *data),
  2.                  void *data,
  3.                  const char namefmt[],
  4.                  ...)

新的任務是有kthread程序調用clone()建立的。新程序将運作threafn函數,給其傳遞參數data,namefmt接受可變參數清單。

新建立的程序處于不可運作狀态,需要通過wake_up_process()明确的喚醒它,它不會主動運作。

建立一個程序并讓它運作起來,可以調用

此處)折疊或打開

  1. #define kthread_run(threadfn, data, namefmt, ...) \\
  2. ({ \\
  3.        struct task_struct *__k \\
  4.               = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \\
  5.        if (!IS_ERR(__k)) \\
  6.               wake_up_process(__k); \\
  7.        __k; \\
  8. })

實際上就是簡單的調用了kthread_create()和wake_up_process()。

核心線程啟動後就一直運作直到調用do_exit(),或者核心其他部分調用kthread_stop()退出。傳遞給kthread_stop()的參數是kthread_create()函數傳回的task_struct結構的位址。

int kthread_stop(struct task_struct *k)

7.程序終結

(1)一個程序終結時,核心必須釋放它占有的資源并把這告知其父程序。

顯示調用exit()(C編譯器會在main()函數的傳回點後面放置調用exit()的代碼),或者當程序接收到它既不能處理也不能忽略的信号或異常時,它還可能被動的終結。

不管以何種方式終結,大部分都要靠do_exit()來完成,它做以下工作:

①将task_struct中的标志成員設定為PF_EXITING。

②調用del_timer_sync()删除任一核心定時器,根據傳回結果,確定沒有定時器在排隊,也沒有定時器處理程式在運作。

③如果BSD的程序記賬功能開啟,do_exit()會調用act_update_integrals()來輸出記賬資訊。

④調用exit_mm()函數釋放程序占用的mm_struct,如果沒有别的程序使用它們(即該位址空間沒有被共享),就徹底釋放他們。

⑤調用sem__exit(),如果程序排隊等候IPC信号,它則離開隊列。

⑥調用exit_files()和exit_fs(),分别遞減檔案描述符,檔案系統資料的引用計數。如果某個引用計數為0,就代表沒有程序在使用相應的資源,此時可以釋放。

⑦把存放在task_struct的exit_code成員的任務推出代碼置為由exit()提供的推出代碼,或者去完成其他由核心機制規定的推出動作,退出代碼存放在這裡供父程序随時檢索。

⑧exit_notify()向父程序發信号,給子程序重新找養父,養父為線程組中的其他線程或者為init程序,并把程序狀态(task_struct的exit_state中)置為EXIT_ZOBIE.

⑨do_exit()調用schedule()切換到新的程序。處于EXIT_ZOBIE的程序永遠不會再被排程,do_exit()永不傳回。

至此,程序相關的所有資源都被釋放(假設是獨享),現在占用的資源就隻有核心棧,thread_info結構和task_struct結構,此時程序存在的唯一目的是向它的父程序提供資訊。

(2)删除程序描述符

調用do_exit()之後,線程已經僵死,但系統還保留有其程序描述符,這樣系統有辦法在子程序和終結後仍能獲得它的資訊。程序終結時所需的清理工作和删除程序描述符分開執行。

wait()函數族都是調用wait4()來實作的,它的标準動作是挂起調用它的程序,直到其中的一個子程序推出,此時函數會傳回孩子程序的PID,且調用該函數時提供的指針會包含子函數退出時的代碼。

當最終需要釋放程序描述符是,會調用release_task()。

①調用__exit_signal()à調用_unhash_process()à調用detach_pid()從pidhash上删除該程序,同時也要從任務隊列中删除該程序。

②_exit_signal()釋放目前僵屍程序所使用的剩餘資源,并進行最終統計和記錄。

③如果這個程序是線程組最後一個程序,并且領頭程序已經死掉,那麼release_task()就要通知僵死的領頭程序的父程序。

④release_task()調用put_task_struct()釋放程序核心棧和thread_info結構所占的頁,并釋放task_struct所占的slab高速緩存。

至此,程序描述符和所有程序獨享的資源就全部釋放掉了。

(3)孤兒程序

如果父程序在子程序之前退出,就必須為子程序找到新父親,以免程序永遠處于僵死狀态,耗費記憶體。解決方法是,給子程序在目前程序組找一個線程作為父親,如果不行,就讓init作為其父程序。

do_exit()會調用exit_notify(),該函數會調用forget_original_parent(),而後會調用find_new_reaper()來執行尋父過程。

此處)折疊或打開

  1. static struct task_struct *find_new_reaper(struct task_struct *father)
  2. {
  3.     struct pid_namespace *pid_ns = task_active_pid_ns(father);
  4.     struct task_struct *thread;
  5.     thread = father;
  6.     while_each_thread(father, thread) {
  7.         if (thread->flags & PF_EXITING)
  8.             continue;
  9.         if (unlikely(pid_ns->child_reaper == father))
  10.             pid_ns->child_reaper = thread;
  11.         return thread;
  12.     }
  13.     if (unlikely(pid_ns->child_reaper == father)) {
  14.         write_unlock_irq(&tasklist_lock);
  15.         if (unlikely(pid_ns == &init_pid_ns))
  16.             panic(\"Attempted to kill init!\");
  17.             zap_pid_ns_processes(pid_ns);
  18.             write_lock_irq(&tasklist_lock);
  19.             /*
  20.             * We can not clear ->child_reaper or leave it alone.
  21.             * There may by stealth EXIT_DEAD tasks on ->children,
  22.             * forget_original_parent() must move them somewhere.
  23.             */
  24.             pid_ns->child_reaper = init_pid_ns.child_reaper;
  25.         }
  26.     return pid_ns->child_reaper;
  27.     }
  28. //找到合适父程序後,隻要周遊所有子程序并為他們設定新的父程序
  29. reaper = find_new_reaper(father);
  30. list_for_each_entry_safe(p, n, &father->children, sibling) {
  31.     p->real_parent = reaper;
  32.     if (p->parent == father) {
  33.     BUG_ON(p->ptrace);
  34.     p->parent = p->real_parent;
  35.     }
  36.     reparent_thread(p, father);
  37. }
  38. //然後調用ptrace_exit_finish()同樣進行尋父過程,不過是給ptraced的子程序尋父
  39. void exit_ptrace(struct task_struct *tracer)
  40. {
  41.     struct task_struct *p, *n;
  42.     LIST_HEAD(ptrace_dead);
  43.     write_lock_irq(&tasklist_lock);
  44.     list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
  45.         if (__ptrace_detach(tracer, p))
  46.             list_add(&p->ptrace_entry, &ptrace_dead);
  47.      }
  48.     write_unlock_irq(&tasklist_lock);
  49.     BUG_ON(!list_empty(&tracer->ptraced));
  50.     list_for_each_entry_safe(p, n, &ptrace_dead, ptrace_entry) {
  51.         list_del_init(&p->ptrace_entry);
  52.         release_task(p);
  53.     }
  54. }

這段代碼周遊兩個連結清單:子程序連結清單和ptrace子程序連結清單。

在一個單獨的被ptrace跟蹤的子程序連結清單中搜尋相關的兄弟程序---用兩個相對較小的連結清單減輕了周遊所有系統程序的消耗。

一旦系統為程序成功找到和設定了新父程序,就不會再出現駐留僵死程序的危險,init程序會例行調用wait()來檢查其子程序,清除所有與其相關的僵死程序。

繼續閱讀