此篇文章主要會帶你介紹 Linux 作業系統,包括 Linux 本身、Linux 如何使用、以及系統呼叫和 Linux 是如何工作的。
Linux 簡介
UNIX 是一個互動式系統,用於同時處理多程式和多使用者同時線上。為什麼要說 UNIX,那是因為 Linux 是由 UNIX 發展而來的,UNIX 是由程式設計師設計,它的主要服務物件也是程式設計師。Linux 繼承了 UNIX 的設計目標。從智慧手機到汽車,超級計算機和家用電器,從家用桌上型電腦到企業伺服器,Linux 作業系統無處不在。
大多數程式設計師都喜歡讓系統儘量簡單,優雅並具有一致性。舉個例子,從最底層的角度來講,一個檔案應該只是一個位元組集合。為了實現順序存取、隨機存取、按鍵存取、遠端存取只能是妨礙你的工作。相同的,如果命令
ls A*
意味著只列出以 A 為開頭的所有檔案,那麼命令
rm A*
應該會移除所有以 A 為開頭的檔案而不是隻刪除檔名是 A*
的檔案。這個特性也是最小吃驚原則(principle of least surprise)
最小吃驚原則一半常用於使用者介面和軟體設計。它的原型是:該功能或者特徵應該符合使用者的預期,不應該使使用者感到驚訝和震驚。
一些有經驗的程式設計師通常希望系統具有較強的功能性和靈活性。設計 Linux 的一個基本目標是每個應用程式只做一件事情並把他做好。所以編譯器只負責編譯的工作,編譯器不會產生列表,因為有其他應用比編譯器做的更好。
很多人都不喜歡冗餘,為什麼在 cp 就能描述清楚你想幹什麼時候還使用 copy?這完全是在浪費寶貴的 hacking time
。為了從檔案中提取所有包含字串 ard
的行,Linux 程式設計師應該輸入
grep ard f
Linux 介面
Linux 系統是一種金字塔模型的系統,如下所示
應用程式發起系統呼叫把引數放在暫存器中(有時候放在棧中),併發出 trap
系統陷入指令切換使用者態至核心態。因為不能直接在 C 中編寫 trap 指令,因此 C 提供了一個庫,庫中的函式對應著系統呼叫。有些函式是使用匯編編寫的,但是能夠從 C 中呼叫。每個函式首先把引數放在合適的位置然後執行系統呼叫指令。因此如果你想要執行 read 系統呼叫的話,C 程式會呼叫 read 函式庫來執行。這裡順便提一下,是由 POSIX 指定的庫介面而不是系統呼叫介面。也就是說,POSIX 會告訴一個標準系統應該提供哪些庫過程,它們的引數是什麼,它們必須做什麼以及它們必須返回什麼結果。
除了作業系統和系統呼叫庫外,Linux 作業系統還要提供一些標準程式,比如文字編輯器、編譯器、檔案操作工具等。直接和使用者打交道的是上面這些應用程式。因此我們可以說 Linux 具有三種不同的介面:系統呼叫介面、庫函式介面和應用程式介面
Linux 中的 GUI(Graphical User Interface)
和 UNIX 中的非常相似,這種 GUI 建立一個桌面環境,包括視窗、目標和資料夾、工具欄和檔案拖拽功能。一個完整的 GUI 還包括視窗管理器以及各種應用程式。
Linux 上的 GUI 由 X 視窗支援,主要組成部分是 X 伺服器、控制鍵盤、滑鼠、顯示器等。當在 Linux 上使用圖形介面時,使用者可以通過滑鼠點選執行程式或者開啟檔案,通過拖拽將檔案進行復制等。
Linux 組成部分
事實上,Linux 作業系統可以由下面這幾部分構成
載入程式(Bootloader)
:載入程式是管理計算機啟動過程的軟體,對於大多數使用者而言,只是彈出一個螢幕,但其實內部作業系統做了很多事情核心(Kernel)
:核心是作業系統的核心,負責管理 CPU、記憶體和外圍裝置等。初始化系統(Init System)
:這是一個引導使用者空間並負責控制守護程式的子系統。一旦從引導載入程式移交了初始引導,它就是用於管理引導過程的初始化系統。後臺程式(Daemon)
:後臺程式顧名思義就是在後臺執行的程式,比如列印、聲音、排程等,它們可以在引導過程中啟動,也可以在登入桌面後啟動圖形伺服器(Graphical server)
:這是在監視器上顯示圖形的子系統。通常將其稱為 X 伺服器或 X。桌面環境(Desktop environment)
:這是使用者與之實際互動的部分,有很多桌面環境可供選擇,每個桌面環境都包含內建應用程式,比如檔案管理器、Web 瀏覽器、遊戲等應用程式(Applications)
:桌面環境不提供完整的應用程式,就像 Windows 和 macOS 一樣,Linux 提供了成千上萬個可以輕鬆找到並安裝的高質量軟體。
Shell
儘管 Linux 應用程式提供了 GUI ,但是大部分程式設計師仍偏好於使用命令列(command-line interface)
,稱為shell
。使用者通常在 GUI 中啟動一個 shell 視窗然後就在 shell 視窗下進行工作。
shell 命令列使用速度快、功能更強大、而且易於擴充套件、並且不會帶來肢體重複性勞損(RSI)
。
下面會介紹一些最簡單的 bash shell。當 shell 啟動時,它首先進行初始化,在螢幕上輸出一個 提示符(prompt)
,通常是一個百分號或者美元符號,等待使用者輸入
等使用者輸入一個命令後,shell 提取其中的第一個詞,這裡的詞指的是被空格或製表符分隔開的一連串字元。假定這個詞是將要執行程式的程式名,那麼就會搜尋這個程式,如果找到了這個程式就會執行它。然後 shell 會將自己掛起直到程式執行完畢,之後再嘗試讀入下一條指令。shell 也是一個普通的使用者程式。它的主要功能就是讀取使用者的輸入和顯示計算的輸出。shell 命令中可以包含引數,它們作為字串傳遞給所呼叫的程式。比如
cp src dest
會呼叫 cp 應用程式幷包含兩個引數 src
和 dest
。這個程式會解釋第一個引數是一個已經存在的檔名,然後建立一個該檔案的副本,名稱為 dest。
並不是所有的引數都是檔名,比如下面
head -20 file
第一個引數 -20,會告訴 head 應用程式列印檔案的前 20 行,而不是預設的 10 行。控制命令操作或者指定可選值的引數稱為標誌(flag)
,按照慣例標誌應該使用 -
來表示。這個符號是必要的,比如
head 20 file
是一個完全合法的命令,它會告訴 head 程式輸出檔名為 20 的檔案的前 10 行,然後輸出檔名為 file 檔案的前 10 行。Linux 作業系統可以接受一個或多個引數。
為了更容易的指定多個檔名,shell 支援 魔法字元(magic character)
,也被稱為萬用字元(wild cards)
。比如,*
可以匹配一個或者多個可能的字串
ls *.c
告訴 ls 列舉出所有檔名以 .c
結束的檔案。如果同時存在多個檔案,則會在後面進行並列。
另一個萬用字元是問號,負責匹配任意一個字元。一組在中括號中的字元可以表示其中任意一個,因此
ls [abc]*
會列舉出所有以 a
、b
或者 c
開頭的檔案。
shell 應用程式不一定通過終端進行輸入和輸出。shell 啟動時,就會獲取 標準輸入、標準輸出、標準錯誤檔案進行訪問的能力。
標準輸出是從鍵盤輸入的,標準輸出或者標準錯誤是輸出到顯示器的。許多 Linux 程式預設是從標準輸入進行輸入並從標準輸出進行輸出。比如
sort
會呼叫 sort 程式,會從終端讀取資料(直到使用者輸入 ctrl-d 結束),根據字母順序進行排序,然後將結果輸出到螢幕上。
通常還可以重定向標準輸入和標準輸出,重定向標準輸入使用 <
後面跟檔名。標準輸出可以通過一個大於號 >
進行重定向。允許一個命令中重定向標準輸入和輸出。例如命令
sort <in >out
會使 sort 從檔案 in 中得到輸入,並把結果輸出到 out 檔案中。由於標準錯誤沒有重定向,所以錯誤資訊會直接列印到螢幕上。從標準輸入讀入,對其進行處理並將其寫入到標準輸出的程式稱為 過濾器
。
考慮下面由三個分開的命令組成的指令
sort <in >temp;head -30 <temp;rm temp
首先會呼叫 sort 應用程式,從標準輸入 in 中進行讀取,並通過標準輸出到 temp。當程式執行完畢後,shell 會執行 head ,告訴它列印前 30 行,並在標準輸出(預設為終端)上列印。最後,temp 臨時檔案被刪除。輕輕的,你走了,你揮一揮衣袖,不帶走一片雲彩。
命令列中的第一個程式通常會產生輸出,在上面的例子中,產生的輸出都不 temp 檔案接收。然而,Linux 還提供了一個簡單的命令來做這件事,例如下面
sort <in | head -30
上面 |
稱為豎線符號,它的意思是從 sort 應用程式產生的排序輸出會直接作為輸入顯示,無需建立、使用和移除臨時檔案。由管道符號連線的命令集合稱為管道(pipeline)
。例如如下
grep cxuan *.c | sort | head -30 | tail -5 >f00
對任意以 .t
結尾的檔案中包含 cxuan
的行被寫到標準輸出中,然後進行排序。這些內容中的前 30 行被 head 出來並傳給 tail ,它又將最後 5 行傳遞給 foo。這個例子提供了一個管道將多個命令連線起來。
可以把一系列 shell 命令放在一個檔案中,然後將此檔案作為輸入來執行。shell 會按照順序對他們進行處理,就像在鍵盤上鍵入命令一樣。包含 shell 命令的檔案被稱為 shell 指令碼(shell scripts)
。
推薦一個 shell 命令的學習網站:https://www.shellscript.sh/
shell 指令碼其實也是一段程式,shell 指令碼中可以對變數進行賦值,也包含迴圈控制語句比如 if、for、while 等,shell 的設計目標是讓其看起來和 C 相似(There is no doubt that C is father)。由於 shell 也是一個使用者程式,所以使用者可以選擇不同的 shell。
Linux 應用程式
Linux 的命令列也就是 shell,它由大量標準應用程式組成。這些應用程式主要有下面六種
- 檔案和目錄操作命令
- 過濾器
- 文字程式
- 系統管理
- 程式開發工具,例如編輯器和編譯器
- 其他
除了這些標準應用程式外,還有其他應用程式比如 Web 瀏覽器、多媒體播放器、圖片瀏覽器、辦公軟體和遊戲程式等。
我們在上面的例子中已經見過了幾個 Linux 的應用程式,比如 sort、cp、ls、head,下面我們再來認識一下其他 Linux 的應用程式。
我們先從幾個例子開始講起,比如
cp a b
是將 a 複製一個副本為 b ,而
mv a b
是將 a 移動到 b ,但是刪除原檔案。
上面這兩個命令有一些區別,cp
是將檔案進行復制,複製完成後會有兩個檔案 a 和 b;而 mv
相當於是檔案的移動,移動完成後就不再有 a 檔案。cat
命令可以把多個檔案內容進行連線。使用 rm
可以刪除檔案;使用 chmod
可以允許所有者改變訪問許可權;檔案目錄的的建立和刪除可以使用 mkdir
和 rmdir
命令;使用 ls
可以檢視目錄檔案,ls 可以顯示很多屬性,比如大小、使用者、建立日期等;sort 決定檔案的顯示順序
Linux 應用程式還包括過濾器 grep,grep
從標準輸入或者一個或多個輸入檔案中提取特定模式的行;sort
將輸入進行排序並輸出到標準輸出;head
提取輸入的前幾行;tail 提取輸入的後面幾行;除此之外的過濾器還有 cut
和 paste
,允許對文字行的剪下和複製;od
將輸入轉換為 ASCII ;tr
實現字元大小寫轉換;pr
為格式化列印輸出等。
程式編譯工具使用 gcc
;
make
命令用於自動編譯,這是一個很強大的命令,它用於維護一個大的程式,往往這類程式的原始碼由許多檔案構成。典型的,有一些是 header files 標頭檔案
,原始檔通常使用 include
指令包含這些檔案,make 的作用就是跟蹤哪些檔案屬於標頭檔案,然後安排自動編譯的過程。
下面列出了 POSIX 的標準應用程式
程式 | 應用 |
---|---|
ls | 列出目錄 |
cp | 複製檔案 |
head | 顯示檔案的前幾行 |
make | 編譯檔案生成二進位制檔案 |
cd | 切換目錄 |
mkdir | 建立目錄 |
chmod | 修改檔案訪問許可權 |
ps | 列出檔案程式 |
pr | 格式化列印 |
rm | 刪除一個檔案 |
rmdir | 刪除檔案目錄 |
tail | 提取檔案最後幾行 |
tr | 字符集轉換 |
grep | 分組 |
cat | 將多個檔案連續標準輸出 |
od | 以八進位制顯示檔案 |
cut | 從檔案中剪下 |
paste | 從檔案中貼上 |
Linux 核心結構
在上面我們看到了 Linux 的整體結構,下面我們從整體的角度來看一下 Linux 的核心結構
核心直接坐落在硬體上,核心的主要作用就是 I/O 互動、記憶體管理和控制 CPU 訪問。上圖中還包括了 中斷
和 排程器
,中斷是與裝置互動的主要方式。中斷出現時排程器就會發揮作用。這裡的低階程式碼停止正在執行的程式,將其狀態儲存在核心程式結構中,並啟動驅動程式。程式排程也會發生在核心完成一些操作並且啟動使用者程式的時候。圖中的排程器是 dispatcher。
注意這裡的排程器是
dispatcher
而不是scheduler
,這兩者是有區別的scheduler 和 dispatcher 都是和程式排程相關的概念,不同的是 scheduler 會從幾個程式中隨意選取一個程式;而 dispatcher 會給 scheduler 選擇的程式分配 CPU。
然後,我們把核心系統分為三部分。
- I/O 部分負責與裝置進行互動以及執行網路和儲存 I/O 操作的所有核心部分。
從圖中可以看出 I/O 層次的關係,最高層是一個虛擬檔案系統
,也就是說不管檔案是來自記憶體還是磁碟中,都是經過虛擬檔案系統中的。從底層看,所有的驅動都是字元驅動或者塊裝置驅動。二者的主要區別就是是否允許隨機訪問。網路驅動裝置並不是一種獨立的驅動裝置,它實際上是一種字元裝置,不過網路裝置的處理方式和字元裝置不同。
上面的裝置驅動程式中,每個裝置型別的核心程式碼都不同。字元裝置有兩種使用方式,有一鍵式
的比如 vi 或者 emacs ,需要每一個鍵盤輸入。其他的比如 shell ,是需要輸入一行按Enter鍵將字串傳送給程式進行編輯。
網路軟體通常是模組化的,由不同的裝置和協議來支援。大多數 Linux 系統在核心中包含一個完整的硬體路由器的功能,但是這個不能和外部路由器相比,路由器上面是協議棧
,包括 TCP/IP 協議,協議棧上面是 socket 介面,socket 負責與外部進行通訊,充當了門的作用。
磁碟驅動上面是 I/O 排程器,它負責排序和分配磁碟讀寫操作,以儘可能減少磁頭的無用移動。
-
I/O 右邊的是記憶體部件,程式被裝載進記憶體,由 CPU 執行,這裡會涉及到虛擬記憶體的部件,頁面的換入和換出是如何進行的,壞頁面的替換和經常使用的頁面會進行快取。
-
程式模組負責程式的建立和終止、程式的排程、Linux 把程式和執行緒看作是可執行的實體,並使用統一的排程策略來進行排程。
在核心最頂層的是系統呼叫介面,所有的系統呼叫都是經過這裡,系統呼叫會觸發一個 trap,將系統從使用者態轉換為核心態,然後將控制權移交給上面的核心部件。