1. 程式人生 > 實用技巧 >【轉載 labuladong】程序、執行緒

【轉載 labuladong】程序、執行緒

Linux的程序、執行緒、檔案描述符是什麼

轉自:https://github.com/ZeroPatrick/fucking-algorithm/blob/master/%E6%8A%80%E6%9C%AF/linux%E8%BF%9B%E7%A8%8B.md

說到程序,恐怕面試中最常見的問題就是執行緒和程序的關係了,那麼先說一下答案:在 Linux 系統中,程序和執行緒幾乎沒有區別。

Linux 中的程序就是一個數據結構,看明白就可以理解檔案描述符、重定向、管道命令的底層工作原理,最後我們從作業系統的角度看看為什麼說執行緒和程序基本沒有區別。

一、程序是什麼

那麼,作業系統是如何建立程序的呢?對於作業系統,程序就是一個數據結構,我們直接來看 Linux 的原始碼:

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 核心對於一個程序的描述,也可以稱為「程序描述符」。原始碼比較複雜,我這裡就截取了一小部分比較常見的。

其中比較有意思的是mm指標和files指標。mm指向的是程序的虛擬記憶體,也就是載入資源和可執行檔案的地方;files指標指向一個數組,這個數組裡裝著所有該程序開啟的檔案的指標。

三、執行緒是什麼

首先要明確的是,多程序和多執行緒都是併發,都可以提高處理器的利用效率,所以現在的關鍵是,多執行緒和多程序有啥區別。

為什麼說 Linux 中執行緒和程序基本沒有區別呢,因為從 Linux 核心的角度來看,並沒有把執行緒和程序區別對待。

我們知道系統呼叫fork()可以新建一個子程序,函式pthread()可以新建一個執行緒。但無論執行緒還是程序,都是用task_struct結構表示的,唯一的區別就是共享的資料區域不同。

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

所以說,我們的多執行緒程式要利用鎖機制,避免多個執行緒同時往同一區域寫入資料,否則可能造成資料錯亂。

那麼你可能問,既然程序和執行緒差不多,而且多程序資料不共享,即不存在資料錯亂的問題,為什麼多執行緒的使用比多程序普遍得多呢?

因為現實中資料共享的併發更普遍呀,比如十個人同時從一個賬戶取十元,我們希望的是這個共享賬戶的餘額正確減少一百元,而不是希望每人獲得一個賬戶的拷貝,每個拷貝賬戶減少十元。

當然,必須要說明的是,只有 Linux 系統將執行緒看做共享資料的程序,不對其做特殊看待,其他的很多作業系統是對執行緒和程序區別對待的,執行緒有其特有的資料結構,我個人認為不如 Linux 的這種設計簡潔,增加了系統的複雜度。

在 Linux 中新建執行緒和程序的效率都是很高的,對於新建程序時記憶體區域拷貝的問題,Linux 採用了 copy-on-write 的策略優化,也就是並不真正複製父程序的記憶體空間,而是等到需要寫操作時才去複製。所以 Linux 中新建程序和新建執行緒都是很迅速的。

二、檔案描述符是什麼

先說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陣列,程序通過簡單的檔案描述符訪問相應資源,具體細節交於作業系統,有效解耦,優美高效。