Hi,各位朋友們,我們又見面了。本月個人工做和生活出現了一些變更,我如今也在進行積極的調整來適應變更,後續會作更多的努力來維持以前的學習和發文節奏。話很少說,今天咱們來聊一聊Linux內核中的進程和進程調度。以前學習操做系統的時候,雖然知道一些操做系統的基本設計思想,對於Windows和Linux的實際應用仍是不夠了解,尤爲是在進程調度這一相當重要的方面,不明白兩種操做系統的具體差別。Android又是基於Linux內核的,瞭解一點內核知識老是沒錯的,嘿嘿。本篇爲Linux內核初探系列第一篇,後續會陸續有其他方面的文章。html
固然,Linux內核博大精深,我也只是經過閱讀源碼和書籍管中窺豹。若是有什麼寫的不對的地方,歡迎各位朋友指正。本文基於Linux 2.6.25版,在這個版本中Linux已經加上了CFS調度策略,同時代碼量也不會很大,比較適合閱讀。源碼下載地址:mirrors.edge.kernel.org/pub/linux/k…node
可能有些朋友會以爲看這些內容沒啥用,那麼你能夠先看看這篇文章。Android 平臺 Native 代碼的崩潰捕獲機制及實現linux
閱讀本文大概須要三十分鐘,閱讀之後你會了解到:android
弱弱的求個點贊和關注,給小笨鳥一點寫做的動力。文章首發公衆號: Android笨鳥之旅。更多技術諮訊文章,敬請關注。c++
要給進程下一個明確的定義可能不是件容易的事情,不過通常來講都認爲進程是處於運行期的程序和相關資源的總稱,具有一些要素:git
這四條都是進程的必要條件。而進程一般含有一個或多個執行線程,線程是在進程中活動的對象,也是內核調度的基本對象。可能你們會有疑問,進程已經能夠獨立的擁有運行程序和資源了,已經能夠完成相關的任務了啊,爲何還要引進線程的概念,並把線程做爲內核調度的基本對象呢?windows
引入進程的緣由,是由於同一個進程,內部可能存在多個不一樣的task,這些task須要共享進程的數據,可是這些task操做的數據又有必定的獨立性,所以多個task並不須要按照時序執行,所以就產生了線程的概念。以office word寫文章爲例,office word程序的運行就是一個進程,可是進程只能把它運行起來,而word還要檢測你光標的移動,進行糾錯等相關功能,那麼光標移動和進行糾錯就是不一樣的線程,都須要CPU根據不一樣的策略來進行調度。所以,線程被引入並做爲內核調度的基本對象緩存
Linux系統對於線程實現很是特殊,他並不區分線程和進程,線程只是一種特殊的進程罷了。從上面四點要素來看,擁有前三點而缺第四點要素的就是線程,若是徹底沒有第四點的用戶空間,那就是系統線程
,若是是共享用戶空間,那就是用戶線程
。安全
由於線程只是特殊的進程,咱們會以進程知識爲主,最後講解Linux下線程與進程的區別。bash
內核把進程的列表存放在稱爲任務隊列
的雙向循環鏈表中。鏈表的每一項都是類型爲task_struct, 稱爲進程描述符
, 定義於<linux/sched.h>中。進程描述符包含一個進程的全部信息,包括的數據至關多,好比進程的狀態,打開的文件,掛起的信號,父子進程等,因此大小相對較大。
下面是部分比較重要的屬性的定義。
struct task_struct { volatile long state; // -1爲不可運行, 0爲可運行, >0爲已中斷 int lock_depth; // 鎖的深度 unsigned int policy; // 調度策略:通常有FIFO,RR,CFS pid_t pid; // 進程標識符,用來表明一個進程 struct task_struct *parent; // 父進程 struct list_head children; // 子進程 struct list_head sibling; // 兄弟進程 } 複製代碼
從2.6版本之後,Linux改用了slab分配器動態生成task_struct, 只須要在棧底(向下增加的棧)或棧頂(向上增加的棧)建立一個新的結構struct thread_info
(這裏是棧對象的尾端),你能夠把slab分配器認爲是一種分配和釋放數據結構的優化策略。經過預先分配和重複使用task_struct, 能夠避免動態分配和釋放帶來的資源消耗。也所以,進程建立迅速是Linux系統的一個重要特色。
slab分配器把不一樣對象類型劃分爲不一樣高速緩存組,好比一個高速緩存用於存放進程描述符task_struct,另一個存放索引節點對象inode。這些高速緩存又會被劃分爲slab,slab由一個或多個物理上連續的頁組成。當要申請數據結構的時候,好比咱們要申請一個task_struct,會先從半滿的slab(slabs_partial)中申請,若是沒有半滿的,就去空的slab(slabs_empty)中申請,直到把全部slab填滿(slabs_full)爲止。若是空slab也沒有了,那就要申請一個新的空slab。這種策略能減小數據結構頻繁申請和釋放的內存碎片,而且因爲有了緩存,分配和釋放迅速。
咱們繼續看thread_info
這個結構。定義於<asm/thread_info.h>
struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ unsigned long flags; /* low level flags */ __u32 cpu; int preempt_count; /* 0 => preemptable, <0 => BUG */ mm_segment_t addr_limit; /* thread address space */ struct restart_block restart_block; }; 複製代碼
在內核中,操做進程都須要得到進程描述符task_struct的指針,因此獲取task_struct的速度就顯得尤其重要,有的硬件體系會拿出專門寄存器來存放當前task_struct的指針,有些寄存器不富餘的體系就只能在棧的尾端建立thread_info結構,經過計算來間接查找。
咱們前面說過,進程描述符描述了進程的當前狀態,包括了進程的全部信息。進程描述符中的state字段就描述了進程的當前狀態。咱們能夠在<kernel/include/linux/sched.h>中找到進程狀態取值的定義。不一樣的系統版本可能會有差別,經常使用的幾種狀態有:
#define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define __TASK_STOPPED 4 #define __TASK_TRACED 8 /* in tsk->exit_state */ #define EXIT_ZOMBIE 16 #define TASK_DEAD 64 複製代碼
系統中每一個進程都必然處於7種進程狀態之一:
TASK_RUNNING(運行): 進程是可執行的,它可能正在執行,或者在運行隊列中等待執行。也就是說無論有沒有執行,只要它可執行,那就是處於TASK_RUNNING態。同一時刻可能有多個進程處於可執行態,他們被放在一個運行隊列中等待進程調度器調度。
TASK_INTERRUPTIBLE(可中斷):進程正被阻塞,等待某些條件達成。當這些條件達成後內核就會把進程狀態設置爲運行,處在此狀態的進程也會由於接收到信號而提早喚醒並隨時準備運行。咱們能夠經過ps命令查看,能夠看到系統其實大部分進程都在沉睡。
TASK_UNINTERRUPTIBLE(不可中斷):就算接收到信號也不會被喚醒或準備投入運行,較之可中斷狀態用得較少。不可中斷,指的並非CPU不響應外部硬件的中斷,而是指進程不響應異步信號。TASK_UNINTERRUPTIBLE狀態存在的意義就在於,內核的某些處理流程是不能被打斷的。好比內核跟硬件交互的時候,爲了不進程與設備交互的過程被打斷,形成設備陷入不可控的狀態,可能就須要這種狀態。
__TASK_TRACED: 被其餘進程跟蹤的進程,好比經過ptrace對調試程序進行跟蹤。我們平常開發中使用斷點,會發現進程停留在我們斷點所在的位置,這個時候進程就是__TASK_TRACED態。這種狀態下的進程只能等到調試進程經過ptrace系統調用執行PTRACE_CONT、PTRACE_DETACH等操做才能繼續恢復到TASK_RUNNING態。
__TASK_STOPPED: 進程中止執行;沒有投入運行也不能投入運行。一般發生在接受到SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU等信號的時候。這種暫停的狀態和__TASK_TRACED比較類似,向進程發送一個SIGCONT信號,可讓其從TASK_STOPPED狀態恢復到TASK_RUNNING狀態。
TASK_DEAD: 進程退出態,進程即將被銷燬。
EXIT_ZOMBIE/TASK_ZOMBIE:進程已經結束可是進程控制塊task_struct還沒註銷。這個狀態須要和上面的TASK_DEAD狀態一塊兒來看,進程在退出的過程當中處於TASK_DEAD態。在這個退出過程當中,進程佔有的全部資源將被回收,可是父進程極可能會關心這個進程的一些信息,因而攜帶這些信息的task_struct結構就沒有被銷燬。
狀態之間的切換關係如圖:
能夠看到狀態雖然有多種,可是變遷方向永遠是兩種:
也就是說,就算進程在TASK_INTERRUPTIBLE態被kill掉,他也須要先喚醒進入TASK_RUNNING態再響應kill信號進入TASK_DEAD態。
Linux系統中的進程存在一個明顯的繼承關係。全部的進程都是PID爲1的init進程的後代。內核會在系統啓動的最後階段啓動init進程,init進程再讀取系統的初始化腳本最終完成系統啓動的整個過程。
系統中的每一個進程都必有一個父進程,每一個進程也能夠有零個或多個子進程,固然所以每一個進程也會有多個兄弟進程。進程間的關係存放於進程描述符task_struct中。咱們在1.2節中講到了task_struct中有下面三個屬性:父進程,子進程和兄弟的list。
struct task_struct *parent; // 父進程 struct list_head children; // 子進程 struct list_head sibling; // 兄弟進程 複製代碼
由於這種繼承體系,咱們能夠經過指針的方式從任何一個進程出發查到任意指定的其它進程。
咱們都知道,Linux系統內核其實就是一種特殊的軟件程序,特殊在哪兒呢?控制計算機的硬件資源,例如協調CPU資源,分配內存資源,而且提供穩定的環境供應用程序運行。而應用程序是在內核的調度下完成本身的任務。
從系統設計的角度上來講,內核應該要有對系統全部資源和操做進行控制的能力,而應用程序訪問資源和進行各類操做都應該在系統的容許範圍內,這樣才能保證系統平穩安全運行。所以,Linux的涉及哲學之一就是:爲不一樣的操做賦予不一樣的執行等級,與系統相關的一些特別關鍵的操做必須由最高特權的程序來完成。對應的就是內核態和用戶態。運行於用戶態的進程能夠執行的操做和訪問的資源都會受到極大的限制,只能使用他們容許範圍內的資源和功能,而運行在內核態的進程則能夠執行任何操做而且在資源的使用上沒有限制。
從內存使用角度來看,兩種狀態分別有內核空間和用戶空間。每一個進程都有4G的虛擬尋址空間,這4G地址空間中,最高1G都是同樣的,即內核空間。只有剩餘的3G才歸進程本身使用。也就是說,這1G的空間是全部進程共享的。當進程運行在內核空間時就處於內核態,而進程運行在用戶空間時則處於用戶態。。同時,在這兩個空間中還分別有一個系統堆棧和用戶空間堆棧,進程運行在不一樣的態下就使用不一樣的堆棧。運行於內核空間時,CPU能夠執行任何指令。運行的代碼也不受任何的限制。進程運行在用戶地址空間時,那就要像大人管着的小孩,乖乖的了。各位看官,看到這裏對於咱們1.1節的基本要素是否是更理解了呢。
那可能你們會有問題了,爲啥進程須要有內核態呢,乖乖的運行於用戶態,管本身的一畝三分地很差嗎?實際上是不行的,由於進程的功能和內核息息相關(沒辦法,被限制的太死了),好比咱們常見的printf函數,雖然是應用程序發起,可是它須要進入內核態才能把數據寫到控制檯上。咱們也稱應用程序在內核空間運行
, 或者內核運行於進程上下文
,或者陷入內核空間
。這種交互方式是應用程序完成其工做的基本行爲方式。
那麼有哪些從用戶態進入到內核態的方式呢?通常有三種
系統調用
進入,好比咱們上面例子中printf就是調用write函數軟中斷
進入,常見的是進程忽然發生了異常。好比android中的應用crash發生之後,進程就會進入內核態調用中斷服務。硬件中斷
進入,一般是外部設備的中斷。當外圍設備完成用戶的請求操做後,會像CPU發出中斷信號,此時,CPU就會暫停執行下一條即將要執行的指令,轉而去執行中斷信號對應的處理程序,若是先前執行的指令是在用戶態下,則天然就發生從用戶態到內核態的轉換。好比網卡,鍵盤等,你一打字,進程就會陷入到內核態,是否是很奇妙。上面所說到的應用經過軟中斷和硬中斷進入內核態之後,都會去查找和調用相對應的中斷服務程序。Linux的中斷服務程序都不在進程上下文中執行,而是有一個進程無關的中斷上下文
中運行,保證中斷服務程序能第一時間響應和處理,而後快速退出。
因此進程,或者說CPU,在任何指定時間點上的活動必然爲三者之一:
本節先介紹了進程的基本要素,進程描述符的相關知識和進程不一樣狀態的切換,而後介紹了進程家族樹和進程的內核態和用戶態。你們看完第一節應該對進程的工做方式有初步的理解,咱們第二節會更近一步,從介紹進程建立引入,介紹Linux下進程和線程的聯繫和區別。給本身打打氣,我們繼續!
Linux的進程建立很特別,固然也是由於繼承了Unix的緣故。不少別的操做系統好比windows都提供了建立進程的機制,首先在新的地址空間裏建立進程,讀入可執行文件,而後開始執行,這些步驟多是經過一個方法完成的。可是Unix把上述步驟分到兩個單獨函數中去,合併使用和其餘系統的單一函數效果一致,這兩個函數是。
fork函數
:經過拷貝當前進程建立一個子進程,子進程和父進程的區別就只在於PID(進程id)和PPID(父進程id)和少許資源exec函數
:負責讀取可執行文件並載入地址空間開始運行。一般是指exec函數族。這種設計體現了Unix的設計哲學,如今來看也是比較符合單一職責原則的,畢竟一般狀況下父子進程須要作的事情(可執行文件)都是不一樣的。
前面咱們也提到過,Unix的一個特色就是建立和釋放進程至關迅速。其實在fork函數上也有體現。fork函數承擔的責任是是讓建立出來的子進程擁有父進程的全部資源。傳統的fork函數會把父類的全部資源複製給新資源,這種實現過於簡單和低效,由於不少狀況下這些拷貝會失去意義,好比這些數據並不共享,或者子進程用不上這些數據。Linux的fork函數使用了寫時拷貝來進行優化。Linux建立子進程的時候,內核並不複製父進程地址空間,而是讓子進程直接以只讀方式共享父進程空間數據,你們能夠想象爲一個指針指向了原來的地址,等到子進程須要寫這些數據的時候,數據纔會被拷貝到子進程。這樣就能夠推遲甚至免除拷貝數據了。
接下來咱們經過代碼層面來理解進程建立過程。
單純從代碼層次來看,Linux有兩種不一樣的函數來建立進程:fork函數,vfork函數。兩個函數都是從父進程拷貝出一個新進程,可是也有區別。下面是fork和vfork的定義。定義於<kernel/fork.c>中。本段代碼源於kernel 4.4版本。
//fork系統調用 SYSCALL_DEFINE0(fork) { return do_fork(SIGCHLD, 0, 0, NULL, NULL); } SYSCALL_DEFINE0(vfork) { return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL, 0); } long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { return _do_fork(clone_flags, stack_start, stack_size, parent_tidptr, child_tidptr, 0); } long _do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr, unsigned long tls) { // .... 省略大量代碼 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace, tls); // .... 省略大量代碼 } 複製代碼
咱們能夠看到fork和vfork最終都是經過調用do_fork來實現的,只不過傳入的參數值不一致。第一個參數中傳入了一些flag,而且這個flag最終被copy_process所使用。copy_process這個方法是真正的執行拷貝的地方,有興趣的同窗能夠研究研究。那麼這些flag有什麼含義呢?下面是linux中的flag定義(定義於<include/linux/sched.h>)。
參考這裏傳入的Flags的區別,咱們能夠進一步給出結論:
線程
在地址空間中運行,同時阻塞父進程
,直到子進程退出或者執行exec()。而且子進程不能像地址空間寫入。這一點在fork沒有支持寫時拷貝前是頗有意義的,可是因爲fork後來引入了寫時拷貝頁而且明確了子進程先執行,vfork的好處就只限於不拷貝頁表項了。建立線程和建立普通的進程相似,只不過須要在調用do_fork的時候須要傳遞不一樣的flag來指明須要共享的資源。使用pthread_create方法來進行建立,最終會調用到do_fork方法。傳入的flag爲
CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND
複製代碼
從上面的flag咱們能夠看出,建立出來的線程從內核層面來看其實也是一種特殊的進程,它跟父進程共享了打開的文件和文件系統信息,共享了地址空間和信號處理函數,這也跟咱們傳統印象中的線程和進程的關係是符合的。Linux的實現和windows之類的操做實現差別很大,假設一個進程有四個線程,在其它提供了專門線程支持的系統中,系統會在進程中增長一個包含指向該進程全部線程的指針的進程描述符,每一個線程再去描述本身獨佔的資源,可是linux就僅僅建立四個進程並分配四個普通的進程描述符,指定他們共享某些資源,這樣更爲簡單。
線程又分爲內核線程
和用戶線程
,內核線程是獨立運行於內核空間的標準進程,他們沒有獨立地址空間,歷來不會切換到用戶空間去。用戶線程就是我們所認知的普通線程了。
本節介紹了進程建立的相關知識,從進程建立的角度介紹了線程和進程的區別。接下來咱們要進入一個新的知識點,那就是CPU的調度策略,也就是咱們熟知的CPU運行時間分配。
本文介紹了進程和線程相關的知識,原本想把進程調度相關的內容也包括進來,可是限於篇幅過長拆成了兩篇文章,在此賣個關子,Linux內核的進程調度跟咱們以前操做系統課上學的區別很大,我的感受很優雅頗有意思。有興趣的同窗能夠繼續關注個人下一篇文章《Linux內核初探:進程調度》。
《Linux內核設計和實現》
我是Android笨鳥之旅,笨鳥也要有向上飛的心,我在這裏陪你一塊兒慢慢變強。期待你的關注