說到進程,恐怕面試中最多見的問題就是線程和進程的關係了,那麼先說一下答案:在 Linux 系統中,進程和線程幾乎沒有區別。git
Linux 中的進程就是一個數據結構,看明白就能夠理解文件描述符、重定向、管道命令的底層工做原理,最後咱們從操做系統的角度看看爲何說線程和進程基本沒有區別。面試
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。算法
首先,抽象地來講,咱們的計算機就是這個東西:數組
這個大的矩形表示計算機的內存空間,其中的小矩形表明進程,左下角的圓形表示磁盤,右下角的圖形表示一些輸入輸出設備,好比鼠標鍵盤顯示器等等。另外,注意到內存空間被劃分爲了兩塊,上半部分表示用戶空間,下半部分表示內核空間。數據結構
用戶空間裝着用戶進程須要使用的資源,好比你在程序代碼裏開一個數組,這個數組確定存在用戶空間;內核空間存放內核進程須要加載的系統資源,這一些資源通常是不容許用戶訪問的。可是注意有的用戶進程會共享一些內核空間的資源,好比一些動態連接庫等等。多線程
咱們用 C 語言寫一個 hello 程序,編譯後獲得一個可執行文件,在命令行運行就能夠打印出一句 hello world,而後程序退出。在操做系統層面,就是新建了一個進程,這個進程將咱們編譯出來的可執行文件讀入內存空間,而後執行,最後退出。併發
你編譯好的那個可執行程序只是一個文件,不是進程,可執行文件必需要載入內存,包裝成一個進程才能真正跑起來。進程是要依靠操做系統建立的,每一個進程都有它的固有屬性,好比進程號(PID)、進程狀態、打開的文件等等,進程建立好以後,讀入你的程序,你的程序才被系統執行。app
那麼,操做系統是如何建立進程的呢?對於操做系統,進程就是一個數據結構,咱們直接來看 Linux 的源碼:socket
struct task_struct { // 進程狀態 long state; // 虛擬內存結構體 struct mm_struct *mm; // 進程號 pid_t pid; // 指向父進程的指針 struct task_struct __rcu *parent; // 子進程列表 struct list_head children; // 存放文件系統信息的指針 struct fs_struct *fs; // 一個數組,包含該進程打開的文件指針 struct files_struct *files; };
task_struct
就是 Linux 內核對於一個進程的描述,也能夠稱爲「進程描述符」。源碼比較複雜,我這裏就截取了一小部分比較常見的。ide
其中比較有意思的是mm
指針和files
指針。mm
指向的是進程的虛擬內存,也就是載入資源和可執行文件的地方;files
指針指向一個數組,這個數組裏裝着全部該進程打開的文件的指針。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。
先說files
,它是一個文件指針數組。通常來講,一個進程會從files[0]
讀取輸入,將輸出寫入files[1]
,將錯誤信息寫入files[2]
。
舉個例子,以咱們的角度 C 語言的printf
函數是向命令行打印字符,可是從進程的角度來看,就是向files[1]
寫入數據;同理,scanf
函數就是進程試圖從files[0]
這個文件中讀取數據。
每一個進程被建立時,files
的前三位被填入默認值,分別指向標準輸入流、標準輸出流、標準錯誤流。咱們常說的「文件描述符」就是指這個文件指針數組的索引,因此程序的文件描述符默認狀況下 0 是輸入,1 是輸出,2 是錯誤。
咱們能夠從新畫一幅圖:
對於通常的計算機,輸入流是鍵盤,輸出流是顯示器,錯誤流也是顯示器,因此如今這個進程和內核連了三根線。由於硬件都是由內核管理的,咱們的進程須要經過「系統調用」讓內核進程訪問硬件資源。
PS:不要忘了,Linux 中一切都被抽象成文件,設備也是文件,能夠進行讀和寫。
若是咱們寫的程序須要其餘資源,好比打開一個文件進行讀寫,這也很簡單,進行系統調用,讓內核把文件打開,這個文件就會被放到files
的第 4 個位置:
明白了這個原理,輸入重定向就很好理解了,程序想讀取數據的時候就會去files[0]
讀取,因此咱們只要把files[0]
指向一個文件,那麼程序就會從這個文件中讀取數據,而不是從鍵盤:
$ command < file.txt
同理,輸出重定向就是把files[1]
指向一個文件,那麼程序的輸出就不會寫入到顯示器,而是寫入到這個文件中:
$ command > file.txt
錯誤重定向也是同樣的,就再也不贅述。
管道符其實也是殊途同歸,把一個進程的輸出流和另外一個進程的輸入流接起一條「管道」,數據就在其中傳遞,不得不說這種設計思想真的很優美:
$ cmd1 | cmd2 | cmd3
到這裏,你可能也看出「Linux 中一切皆文件」設計思路的高明瞭,不論是設備、另外一個進程、socket 套接字仍是真正的文件,所有均可以讀寫,統一裝進一個簡單的files
數組,進程經過簡單的文件描述符訪問相應資源,具體細節交於操做系統,有效解耦,優美高效。
首先要明確的是,多進程和多線程都是併發,均可以提升處理器的利用效率,因此如今的關鍵是,多線程和多進程有啥區別。
爲何說 Linux 中線程和進程基本沒有區別呢,由於從 Linux 內核的角度來看,並無把線程和進程區別對待。
咱們知道系統調用fork()
能夠新建一個子進程,函數pthread()
能夠新建一個線程。但不管線程仍是進程,都是用task_struct
結構表示的,惟一的區別就是共享的數據區域不一樣。
換句話說,線程看起來跟進程沒有區別,只是線程的某些數據區域和其父進程是共享的,而子進程是拷貝副本,而不是共享。就好比說,mm
結構和files
結構在線程中都是共享的,我畫兩張圖你就明白了:
因此說,咱們的多線程程序要利用鎖機制,避免多個線程同時往同一區域寫入數據,不然可能形成數據錯亂。
那麼你可能問,既然進程和線程差很少,並且多進程數據不共享,即不存在數據錯亂的問題,爲何多線程的使用比多進程廣泛得多呢?
由於現實中數據共享的併發更廣泛呀,好比十我的同時從一個帳戶取十元,咱們但願的是這個共享帳戶的餘額正確減小一百元,而不是但願每人得到一個帳戶的拷貝,每一個拷貝帳戶減小十元。
固然,必需要說明的是,只有 Linux 系統將線程看作共享數據的進程,不對其作特殊看待,其餘的不少操做系統是對線程和進程區別對待的,線程有其特有的數據結構,我我的認爲不如 Linux 的這種設計簡潔,增長了系統的複雜度。
在 Linux 中新建線程和進程的效率都是很高的,對於新建進程時內存區域拷貝的問題,Linux 採用了 copy-on-write 的策略優化,也就是並不真正複製父進程的內存空間,而是等到須要寫操做時纔去複製。因此 Linux 中新建進程和新建線程都是很迅速的。