《linux內核設計與實現》第三章

1.進程css

進程就是正在執行的程序代碼的實時結果,不只包含可執行代碼,還包括其餘資源,好比:打開的文件,掛起的信號,內核內部數據結構,處理器狀態,一個或多個具備內存映射的內存地址空間及一個或多個執行線程,全局變量數據段等。算法

內核須要有效而透明的管理全部細節。緩存

線程,每一個線程擁有一個獨立的程序計數器,進程棧和一組寄存器。內核調度對象是線程而不是進程。數據結構

現代操做系統提供兩種虛擬機制:虛擬處理器和虛擬內存,線程之間能夠共享虛擬內存,但每一個都有各自的虛擬處理器。dom

Linux中,新進程是由fork()來實現的,fork()實際上由clone()系統調用實現,程序經過exit()系統調用退出執行,這個函數會終結進程並釋放其佔用的資源,父進程能夠經過wait4()查詢子進程是否終結。進程退出執行後被設置爲僵死狀態,直到他父進程調用wait()或waitpid()。函數

2.進程描述符spa

內核把進程的列表存放在一個叫作任務隊列的雙向環形鏈表中,鏈表中每一項(task_struct類型)都稱爲進程描述符。操作系統

進程描述符包括一個進程的具體全部信息:打開的文件,進程地址空間,掛起的信號,進程狀態等。在中定義。線程

Linux經過slab分配器分配task_struct結構,這樣能達到對象複用和緩存目的。指針

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

 

  1. struct thread_info {
  2.     struct task_struct *task;
  3.     struct exec_domain *exec_domain;
  4.     __u32                 flags;
  5.     __u32                 status;
  6.     __u32                 cpu;
  7.     int preempt_count;
  8.     mm_segment_t          addr_limit;
  9.     struct restart_block  restart_block;
  10.     void *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. struct task_struct *my_parent = current->parent;
  3. //依次訪問全部子進程
  4. struct task_struct *task;
  5. struct list_head *list;
  6. list_for_each(list, &current->children) {
  7.     task = list_entry(list, struct task_struct, sibling);
  8.     /* task now points to one of current\'s children */
  9. }
  10. //遍歷系統中全部進程
  11. list_entry(task->tasks.next, struct task_struct, tasks)
  12. list_entry(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.     
  6.     thread = father;
  7.     while_each_thread(father, thread) {
  8.         if (thread->flags & PF_EXITING)
  9.             continue;
  10.         if (unlikely(pid_ns->child_reaper == father))
  11.             pid_ns->child_reaper = thread;
  12.         return thread;
  13.     }
  14.     if (unlikely(pid_ns->child_reaper == father)) {
  15.         write_unlock_irq(&tasklist_lock);
  16.         if (unlikely(pid_ns == &init_pid_ns))
  17.             panic(\"Attempted to kill init!\");
  18.             zap_pid_ns_processes(pid_ns);
  19.             write_lock_irq(&tasklist_lock);
  20.             /*
  21.             * We can not clear ->child_reaper or leave it alone.
  22.             * There may by stealth EXIT_DEAD tasks on ->children,
  23.             * forget_original_parent() must move them somewhere.
  24.             */
  25.             pid_ns->child_reaper = init_pid_ns.child_reaper;
  26.         }
  27.     return pid_ns->child_reaper;
  28.     }
  29. //找到合適父進程後,只要遍歷全部子進程併爲他們設置新的父進程
  30. reaper = find_new_reaper(father);
  31. list_for_each_entry_safe(p, n, &father->children, sibling) {
  32.     p->real_parent = reaper;
  33.     if (p->parent == father) {
  34.     BUG_ON(p->ptrace);
  35.     p->parent = p->real_parent;
  36.     }
  37.     reparent_thread(p, father);
  38. }
  39. //而後調用ptrace_exit_finish()一樣進行尋父過程,不過是給ptraced的子進程尋父
  40. void exit_ptrace(struct task_struct *tracer)
  41. {
  42.     struct task_struct *p, *n;
  43.     LIST_HEAD(ptrace_dead);
  44.     write_lock_irq(&tasklist_lock);
  45.     list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
  46.         if (__ptrace_detach(tracer, p))
  47.             list_add(&p->ptrace_entry, &ptrace_dead);
  48.      }
  49.     
  50.     write_unlock_irq(&tasklist_lock);
  51.     BUG_ON(!list_empty(&tracer->ptraced));
  52.     list_for_each_entry_safe(p, n, &ptrace_dead, ptrace_entry) {
  53.         list_del_init(&p->ptrace_entry);
  54.         release_task(p);
  55.     }
  56. }

這段代碼遍歷兩個鏈表:子進程鏈表和ptrace子進程鏈表。

在一個單獨的被ptrace跟蹤的子進程鏈表中搜索相關的兄弟進程---用兩個相對較小的鏈表減輕了遍歷全部系統進程的消耗。

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

相關文章
相關標籤/搜索