Linux 進程、線程、文件描述符的底層原理

說到進程,恐怕面試中最多見的問題就是線程和進程的關係了,那麼先說一下答案:在 Linux 系統中,進程和線程幾乎沒有區別git

Linux 中的進程就是一個數據結構,看明白就能夠理解文件描述符、重定向、管道命令的底層工做原理,最後咱們從操做系統的角度看看爲何說線程和進程基本沒有區別。面試

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。算法

1、進程是什麼

首先,抽象地來講,咱們的計算機就是這個東西:數組

c70d1d2e7574d7fe4c3cfe21493cac0b.jpeg

這個大的矩形表示計算機的內存空間,其中的小矩形表明進程,左下角的圓形表示磁盤,右下角的圖形表示一些輸入輸出設備,好比鼠標鍵盤顯示器等等。另外,注意到內存空間被劃分爲了兩塊,上半部分表示用戶空間,下半部分表示內核空間數據結構

用戶空間裝着用戶進程須要使用的資源,好比你在程序代碼裏開一個數組,這個數組確定存在用戶空間;內核空間存放內核進程須要加載的系統資源,這一些資源通常是不容許用戶訪問的。可是注意有的用戶進程會共享一些內核空間的資源,好比一些動態連接庫等等。多線程

咱們用 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的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。

2、文件描述符是什麼

先說files,它是一個文件指針數組。通常來講,一個進程會從files[0]讀取輸入,將輸出寫入files[1],將錯誤信息寫入files[2]

舉個例子,以咱們的角度 C 語言的printf函數是向命令行打印字符,可是從進程的角度來看,就是向files[1]寫入數據;同理,scanf函數就是進程試圖從files[0]這個文件中讀取數據。

每一個進程被建立時,files的前三位被填入默認值,分別指向標準輸入流、標準輸出流、標準錯誤流。咱們常說的「文件描述符」就是指這個文件指針數組的索引,因此程序的文件描述符默認狀況下 0 是輸入,1 是輸出,2 是錯誤。

咱們能夠從新畫一幅圖:

5254cefccb6d2992f8fd8818315d81a5.jpeg

對於通常的計算機,輸入流是鍵盤,輸出流是顯示器,錯誤流也是顯示器,因此如今這個進程和內核連了三根線。由於硬件都是由內核管理的,咱們的進程須要經過「系統調用」讓內核進程訪問硬件資源。

PS:不要忘了,Linux 中一切都被抽象成文件,設備也是文件,能夠進行讀和寫。

若是咱們寫的程序須要其餘資源,好比打開一個文件進行讀寫,這也很簡單,進行系統調用,讓內核把文件打開,這個文件就會被放到files的第 4 個位置:

24d65f4623ea73f21ccb6a7f0c7c2f65.jpg

明白了這個原理,輸入重定向就很好理解了,程序想讀取數據的時候就會去files[0]讀取,因此咱們只要把files[0]指向一個文件,那麼程序就會從這個文件中讀取數據,而不是從鍵盤:

$ command < file.txt

69861ea27cf9bbaffbdc2a24602f8ea3.jpeg

同理,輸出重定向就是把files[1]指向一個文件,那麼程序的輸出就不會寫入到顯示器,而是寫入到這個文件中:

$ command > file.txt

0bec983bc98ca50eab754c8a56d53821.jpeg

錯誤重定向也是同樣的,就再也不贅述。

管道符其實也是殊途同歸,把一個進程的輸出流和另外一個進程的輸入流接起一條「管道」,數據就在其中傳遞,不得不說這種設計思想真的很優美:

$ cmd1 | cmd2 | cmd3

fcd2dfa5dcb3a56ffe60c68f0b2cefcf.jpeg

到這裏,你可能也看出「Linux 中一切皆文件」設計思路的高明瞭,不論是設備、另外一個進程、socket 套接字仍是真正的文件,所有均可以讀寫,統一裝進一個簡單的files數組,進程經過簡單的文件描述符訪問相應資源,具體細節交於操做系統,有效解耦,優美高效。

3、線程是什麼

首先要明確的是,多進程和多線程都是併發,均可以提升處理器的利用效率,因此如今的關鍵是,多線程和多進程有啥區別。

爲何說 Linux 中線程和進程基本沒有區別呢,由於從 Linux 內核的角度來看,並無把線程和進程區別對待。

咱們知道系統調用fork()能夠新建一個子進程,函數pthread()能夠新建一個線程。但不管線程仍是進程,都是用task_struct結構表示的,惟一的區別就是共享的數據區域不一樣

換句話說,線程看起來跟進程沒有區別,只是線程的某些數據區域和其父進程是共享的,而子進程是拷貝副本,而不是共享。就好比說,mm結構和files結構在線程中都是共享的,我畫兩張圖你就明白了:

8224b48f19a85626ac9d670182b464d1.jpeg12d01af578ac636f3faa704833b57035.jpeg

因此說,咱們的多線程程序要利用鎖機制,避免多個線程同時往同一區域寫入數據,不然可能形成數據錯亂。

那麼你可能問,既然進程和線程差很少,並且多進程數據不共享,即不存在數據錯亂的問題,爲何多線程的使用比多進程廣泛得多呢

由於現實中數據共享的併發更廣泛呀,好比十我的同時從一個帳戶取十元,咱們但願的是這個共享帳戶的餘額正確減小一百元,而不是但願每人得到一個帳戶的拷貝,每一個拷貝帳戶減小十元。

固然,必需要說明的是,只有 Linux 系統將線程看作共享數據的進程,不對其作特殊看待,其餘的不少操做系統是對線程和進程區別對待的,線程有其特有的數據結構,我我的認爲不如 Linux 的這種設計簡潔,增長了系統的複雜度。

在 Linux 中新建線程和進程的效率都是很高的,對於新建進程時內存區域拷貝的問題,Linux 採用了 copy-on-write 的策略優化,也就是並不真正複製父進程的內存空間,而是等到須要寫操做時纔去複製。因此 Linux 中新建進程和新建線程都是很迅速的

相關文章
相關標籤/搜索