Linux 簡介
UNIX 是一個交互式系統(tǒng),用于同時處理多進(jìn)程和多用戶同時在線。為什么要說 UNIX,那是因?yàn)?Linux 是由 UNIX 發(fā)展而來的,UNIX 是由程序員設(shè)計(jì),它的主要服務(wù)對象也是程序員。Linux 繼承了 UNIX 的設(shè)計(jì)目標(biāo)。從智能手機(jī)到汽車,超級計(jì)算機(jī)和家用電器,從家用臺式機(jī)到企業(yè)服務(wù)器,Linux 操作系統(tǒng)無處不在。
大多數(shù)程序員都喜歡讓系統(tǒng)盡量簡單,優(yōu)雅并具有一致性。舉個例子,從最底層的角度來講,一個文件應(yīng)該只是一個字節(jié)集合。為了實(shí)現(xiàn)順序存取、隨機(jī)存取、按鍵存取、遠(yuǎn)程存取只能是妨礙你的工作。相同的,如果命令
lsA*
意味著只列出以 A 為開頭的所有文件,那么命令
rmA*
應(yīng)該會移除所有以 A 為開頭的文件而不是只刪除文件名是A*的文件。這個特性也是最小吃驚原則(principle of least surprise)
?最小吃驚原則一半常用于用戶界面和軟件設(shè)計(jì)。它的原型是:該功能或者特征應(yīng)該符合用戶的預(yù)期,不應(yīng)該使用戶感到驚訝和震驚。?
一些有經(jīng)驗(yàn)的程序員通常希望系統(tǒng)具有較強(qiáng)的功能性和靈活性。設(shè)計(jì) Linux 的一個基本目標(biāo)是每個應(yīng)用程序只做一件事情并把他做好。所以編譯器只負(fù)責(zé)編譯的工作,編譯器不會產(chǎn)生列表,因?yàn)橛衅渌麘?yīng)用比編譯器做的更好。
很多人都不喜歡冗余,為什么在 cp 就能描述清楚你想干什么時候還使用 copy?這完全是在浪費(fèi)寶貴的hacking time。為了從文件中提取所有包含字符串a(chǎn)rd的行,Linux 程序員應(yīng)該輸入
grepardf
Linux 接口
Linux 系統(tǒng)是一種金字塔模型的系統(tǒng),如下所示
應(yīng)用程序發(fā)起系統(tǒng)調(diào)用把參數(shù)放在寄存器中(有時候放在棧中),并發(fā)出trap系統(tǒng)陷入指令切換用戶態(tài)至內(nèi)核態(tài)。因?yàn)椴荒苤苯釉?C 中編寫 trap 指令,因此 C 提供了一個庫,庫中的函數(shù)對應(yīng)著系統(tǒng)調(diào)用。有些函數(shù)是使用匯編編寫的,但是能夠從 C 中調(diào)用。每個函數(shù)首先把參數(shù)放在合適的位置然后執(zhí)行系統(tǒng)調(diào)用指令。因此如果你想要執(zhí)行 read 系統(tǒng)調(diào)用的話,C 程序會調(diào)用 read 函數(shù)庫來執(zhí)行。這里順便提一下,是由 POSIX 指定的庫接口而不是系統(tǒng)調(diào)用接口。也就是說,POSIX 會告訴一個標(biāo)準(zhǔn)系統(tǒng)應(yīng)該提供哪些庫過程,它們的參數(shù)是什么,它們必須做什么以及它們必須返回什么結(jié)果。
除了操作系統(tǒng)和系統(tǒng)調(diào)用庫外,Linux 操作系統(tǒng)還要提供一些標(biāo)準(zhǔn)程序,比如文本編輯器、編譯器、文件操作工具等。直接和用戶打交道的是上面這些應(yīng)用程序。因此我們可以說 Linux 具有三種不同的接口:「系統(tǒng)調(diào)用接口、庫函數(shù)接口和應(yīng)用程序接口」
Linux 中的GUI(Graphical User Interface)和 UNIX 中的非常相似,這種 GUI 創(chuàng)建一個桌面環(huán)境,包括窗口、目標(biāo)和文件夾、工具欄和文件拖拽功能。一個完整的 GUI 還包括窗口管理器以及各種應(yīng)用程序。
Linux 上的 GUI 由 X 窗口支持,主要組成部分是 X 服務(wù)器、控制鍵盤、鼠標(biāo)、顯示器等。當(dāng)在 Linux 上使用圖形界面時,用戶可以通過鼠標(biāo)點(diǎn)擊運(yùn)行程序或者打開文件,通過拖拽將文件進(jìn)行復(fù)制等。
Linux 組成部分
事實(shí)上,Linux 操作系統(tǒng)可以由下面這幾部分構(gòu)成
引導(dǎo)程序(Bootloader):引導(dǎo)程序是管理計(jì)算機(jī)啟動過程的軟件,對于大多數(shù)用戶而言,只是彈出一個屏幕,但其實(shí)內(nèi)部操作系統(tǒng)做了很多事情
內(nèi)核(Kernel):內(nèi)核是操作系統(tǒng)的核心,負(fù)責(zé)管理 CPU、內(nèi)存和外圍設(shè)備等。
初始化系統(tǒng)(Init System):這是一個引導(dǎo)用戶空間并負(fù)責(zé)控制守護(hù)程序的子系統(tǒng)。一旦從引導(dǎo)加載程序移交了初始引導(dǎo),它就是用于管理引導(dǎo)過程的初始化系統(tǒng)。
后臺進(jìn)程(Daemon):后臺進(jìn)程顧名思義就是在后臺運(yùn)行的程序,比如打印、聲音、調(diào)度等,它們可以在引導(dǎo)過程中啟動,也可以在登錄桌面后啟動
圖形服務(wù)器(Graphical server):這是在監(jiān)視器上顯示圖形的子系統(tǒng)。通常將其稱為 X 服務(wù)器或 X。
桌面環(huán)境(Desktop environment):這是用戶與之實(shí)際交互的部分,有很多桌面環(huán)境可供選擇,每個桌面環(huán)境都包含內(nèi)置應(yīng)用程序,比如文件管理器、Web 瀏覽器、游戲等
應(yīng)用程序(Applications):桌面環(huán)境不提供完整的應(yīng)用程序,就像 Windows 和 macOS 一樣,Linux 提供了成千上萬個可以輕松找到并安裝的高質(zhì)量軟件。
Shell
盡管 Linux 應(yīng)用程序提供了 GUI ,但是大部分程序員仍偏好于使用命令行(command-line interface),稱為shell。用戶通常在 GUI 中啟動一個 shell 窗口然后就在 shell 窗口下進(jìn)行工作。
shell 命令行使用速度快、功能更強(qiáng)大、而且易于擴(kuò)展、并且不會帶來肢體重復(fù)性勞損(RSI)。
下面會介紹一些最簡單的 bash shell。當(dāng) shell 啟動時,它首先進(jìn)行初始化,在屏幕上輸出一個提示符(prompt),通常是一個百分號或者美元符號,等待用戶輸入
等用戶輸入一個命令后,shell 提取其中的第一個詞,這里的詞指的是被空格或制表符分隔開的一連串字符。假定這個詞是將要運(yùn)行程序的程序名,那么就會搜索這個程序,如果找到了這個程序就會運(yùn)行它。然后 shell 會將自己掛起直到程序運(yùn)行完畢,之后再嘗試讀入下一條指令。shell 也是一個普通的用戶程序。它的主要功能就是讀取用戶的輸入和顯示計(jì)算的輸出。shell 命令中可以包含參數(shù),它們作為字符串傳遞給所調(diào)用的程序。比如
cpsrcdest
會調(diào)用 cp 應(yīng)用程序并包含兩個參數(shù)src和dest。這個程序會解釋第一個參數(shù)是一個已經(jīng)存在的文件名,然后創(chuàng)建一個該文件的副本,名稱為 dest。
并不是所有的參數(shù)都是文件名,比如下面
head-20file
第一個參數(shù) -20,會告訴 head 應(yīng)用程序打印文件的前 20 行,而不是默認(rèn)的 10 行??刂泼畈僮骰蛘咧付蛇x值的參數(shù)稱為標(biāo)志(flag),按照慣例標(biāo)志應(yīng)該使用-來表示。這個符號是必要的,比如
head20file
是一個完全合法的命令,它會告訴 head 程序輸出文件名為 20 的文件的前 10 行,然后輸出文件名為 file 文件的前 10 行。Linux 操作系統(tǒng)可以接受一個或多個參數(shù)。
為了更容易的指定多個文件名,shell 支持魔法字符(magic character),也被稱為通配符(wild cards)。比如,*可以匹配一個或者多個可能的字符串
ls*.c
告訴 ls 列舉出所有文件名以.c結(jié)束的文件。如果同時存在多個文件,則會在后面進(jìn)行并列。
另一個通配符是問號,負(fù)責(zé)匹配任意一個字符。一組在中括號中的字符可以表示其中任意一個,因此
ls[abc]*
會列舉出所有以a、b或者c開頭的文件。
shell 應(yīng)用程序不一定通過終端進(jìn)行輸入和輸出。shell 啟動時,就會獲取「標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯誤」文件進(jìn)行訪問的能力。
標(biāo)準(zhǔn)輸出是從鍵盤輸入的,標(biāo)準(zhǔn)輸出或者標(biāo)準(zhǔn)錯誤是輸出到顯示器的。許多 Linux 程序默認(rèn)是從標(biāo)準(zhǔn)輸入進(jìn)行輸入并從標(biāo)準(zhǔn)輸出進(jìn)行輸出。比如
sort
會調(diào)用 sort 程序,會從終端讀取數(shù)據(jù)(直到用戶輸入 ctrl-d 結(jié)束),根據(jù)字母順序進(jìn)行排序,然后將結(jié)果輸出到屏幕上。
通常還可以重定向標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出,重定向標(biāo)準(zhǔn)輸入使用進(jìn)行重定向。允許一個命令中重定向標(biāo)準(zhǔn)輸入和輸出。例如命令
sort
會使 sort 從文件 in 中得到輸入,并把結(jié)果輸出到 out 文件中。由于標(biāo)準(zhǔn)錯誤沒有重定向,所以錯誤信息會直接打印到屏幕上。從標(biāo)準(zhǔn)輸入讀入,對其進(jìn)行處理并將其寫入到標(biāo)準(zhǔn)輸出的程序稱為過濾器。
考慮下面由三個分開的命令組成的指令
sort
首先會調(diào)用 sort 應(yīng)用程序,從標(biāo)準(zhǔn)輸入 in 中進(jìn)行讀取,并通過標(biāo)準(zhǔn)輸出到 temp。當(dāng)程序運(yùn)行完畢后,shell 會運(yùn)行 head ,告訴它打印前 30 行,并在標(biāo)準(zhǔn)輸出(默認(rèn)為終端)上打印。最后,temp 臨時文件被刪除?!篙p輕的,你走了,你揮一揮衣袖,不帶走一片云彩」。
命令行中的第一個程序通常會產(chǎn)生輸出,在上面的例子中,產(chǎn)生的輸出都不 temp 文件接收。然而,Linux 還提供了一個簡單的命令來做這件事,例如下面
sort
上面|稱為豎線符號,它的意思是從 sort 應(yīng)用程序產(chǎn)生的排序輸出會直接作為輸入顯示,無需創(chuàng)建、使用和移除臨時文件。由管道符號連接的命令集合稱為管道(pipeline)。例如如下
grepcxuan*.c|sort|head-30|tail-5>f00
對任意以.c結(jié)尾的文件中包含cxuan的行被寫到標(biāo)準(zhǔn)輸出中,然后進(jìn)行排序。這些內(nèi)容中的前 30 行被 head 出來并傳給 tail ,它又將最后 5 行傳遞給 foo。這個例子提供了一個管道將多個命令連接起來。
可以把一系列 shell 命令放在一個文件中,然后將此文件作為輸入來運(yùn)行。shell 會按照順序?qū)λ麄冞M(jìn)行處理,就像在鍵盤上鍵入命令一樣。包含 shell 命令的文件被稱為shell 腳本(shell scripts)。
?推薦一個 shell 命令的學(xué)習(xí)網(wǎng)站:https://www.shellscript.sh/?
shell 腳本其實(shí)也是一段程序,shell 腳本中可以對變量進(jìn)行賦值,也包含循環(huán)控制語句比如「if、for、while」等,shell 的設(shè)計(jì)目標(biāo)是讓其看起來和 C 相似(There is no doubt that C is father)。由于 shell 也是一個用戶程序,所以用戶可以選擇不同的 shell。
Linux 應(yīng)用程序
Linux 的命令行也就是 shell,它由大量標(biāo)準(zhǔn)應(yīng)用程序組成。這些應(yīng)用程序主要有下面六種
文件和目錄操作命令
過濾器
文本程序
系統(tǒng)管理
程序開發(fā)工具,例如編輯器和編譯器
其他
除了這些標(biāo)準(zhǔn)應(yīng)用程序外,還有其他應(yīng)用程序比如「Web 瀏覽器、多媒體播放器、圖片瀏覽器、辦公軟件和游戲程序等」。
我們在上面的例子中已經(jīng)見過了幾個 Linux 的應(yīng)用程序,比如 sort、cp、ls、head,下面我們再來認(rèn)識一下其他 Linux 的應(yīng)用程序。
我們先從幾個例子開始講起,比如
cpab
是將 a 復(fù)制一個副本為 b ,而
mvab
是將 a 移動到 b ,但是刪除原文件。
上面這兩個命令有一些區(qū)別,cp是將文件進(jìn)行復(fù)制,復(fù)制完成后會有兩個文件 a 和 b;而mv相當(dāng)于是文件的移動,移動完成后就不再有 a 文件。cat命令可以把多個文件內(nèi)容進(jìn)行連接。使用rm可以刪除文件;使用chmod可以允許所有者改變訪問權(quán)限;文件目錄的的創(chuàng)建和刪除可以使用mkdir和rmdir命令;使用ls可以查看目錄文件,ls 可以顯示很多屬性,比如大小、用戶、創(chuàng)建日期等;sort 決定文件的顯示順序
Linux 應(yīng)用程序還包括過濾器 grep,grep從標(biāo)準(zhǔn)輸入或者一個或多個輸入文件中提取特定模式的行;sort將輸入進(jìn)行排序并輸出到標(biāo)準(zhǔn)輸出;head提取輸入的前幾行;tail 提取輸入的后面幾行;除此之外的過濾器還有cut和paste,允許對文本行的剪切和復(fù)制;od將輸入轉(zhuǎn)換為 ASCII ;tr實(shí)現(xiàn)字符大小寫轉(zhuǎn)換;pr為格式化打印輸出等。
程序編譯工具使用gcc;
make命令用于自動編譯,這是一個很強(qiáng)大的命令,它用于維護(hù)一個大的程序,往往這類程序的源碼由許多文件構(gòu)成。典型的,有一些是header files 頭文件,源文件通常使用include指令包含這些文件,make 的作用就是跟蹤哪些文件屬于頭文件,然后安排自動編譯的過程。
下面列出了 POSIX 的標(biāo)準(zhǔn)應(yīng)用程序
Linux 內(nèi)核結(jié)構(gòu)
在上面我們看到了 Linux 的整體結(jié)構(gòu),下面我們從整體的角度來看一下 Linux 的內(nèi)核結(jié)構(gòu)
內(nèi)核直接坐落在硬件上,內(nèi)核的主要作用就是 I/O 交互、內(nèi)存管理和控制 CPU 訪問。上圖中還包括了中斷和調(diào)度器,中斷是與設(shè)備交互的主要方式。中斷出現(xiàn)時調(diào)度器就會發(fā)揮作用。這里的低級代碼停止正在運(yùn)行的進(jìn)程,將其狀態(tài)保存在內(nèi)核進(jìn)程結(jié)構(gòu)中,并啟動驅(qū)動程序。進(jìn)程調(diào)度也會發(fā)生在內(nèi)核完成一些操作并且啟動用戶進(jìn)程的時候。圖中的調(diào)度器是 dispatcher。
?注意這里的調(diào)度器是dispatcher而不是scheduler,這兩者是有區(qū)別的
scheduler 和 dispatcher 都是和進(jìn)程調(diào)度相關(guān)的概念,不同的是 scheduler 會從幾個進(jìn)程中隨意選取一個進(jìn)程;而 dispatcher 會給 scheduler 選擇的進(jìn)程分配 CPU。?
然后,我們把內(nèi)核系統(tǒng)分為三部分。
I/O 部分負(fù)責(zé)與設(shè)備進(jìn)行交互以及執(zhí)行網(wǎng)絡(luò)和存儲 I/O 操作的所有內(nèi)核部分。
從圖中可以看出 I/O 層次的關(guān)系,最高層是一個虛擬文件系統(tǒng),也就是說不管文件是來自內(nèi)存還是磁盤中,都是經(jīng)過虛擬文件系統(tǒng)中的。從底層看,所有的驅(qū)動都是字符驅(qū)動或者塊設(shè)備驅(qū)動。二者的主要區(qū)別就是是否允許隨機(jī)訪問。網(wǎng)絡(luò)驅(qū)動設(shè)備并不是一種獨(dú)立的驅(qū)動設(shè)備,它實(shí)際上是一種字符設(shè)備,不過網(wǎng)絡(luò)設(shè)備的處理方式和字符設(shè)備不同。
上面的設(shè)備驅(qū)動程序中,每個設(shè)備類型的內(nèi)核代碼都不同。字符設(shè)備有兩種使用方式,有一鍵式的比如 vi 或者 emacs ,需要每一個鍵盤輸入。其他的比如 shell ,是需要輸入一行按回車鍵將字符串發(fā)送給程序進(jìn)行編輯。
網(wǎng)絡(luò)軟件通常是模塊化的,由不同的設(shè)備和協(xié)議來支持。大多數(shù) Linux 系統(tǒng)在內(nèi)核中包含一個完整的硬件路由器的功能,但是這個不能和外部路由器相比,路由器上面是協(xié)議棧,包括 TCP/IP 協(xié)議,協(xié)議棧上面是 socket 接口,socket 負(fù)責(zé)與外部進(jìn)行通信,充當(dāng)了門的作用。
磁盤驅(qū)動上面是 I/O 調(diào)度器,它負(fù)責(zé)排序和分配磁盤讀寫操作,以盡可能減少磁頭的無用移動。
I/O 右邊的是內(nèi)存部件,程序被裝載進(jìn)內(nèi)存,由 CPU 執(zhí)行,這里會涉及到虛擬內(nèi)存的部件,頁面的換入和換出是如何進(jìn)行的,壞頁面的替換和經(jīng)常使用的頁面會進(jìn)行緩存。
進(jìn)程模塊負(fù)責(zé)進(jìn)程的創(chuàng)建和終止、進(jìn)程的調(diào)度、Linux 把進(jìn)程和線程看作是可運(yùn)行的實(shí)體,并使用統(tǒng)一的調(diào)度策略來進(jìn)行調(diào)度。
在內(nèi)核最頂層的是系統(tǒng)調(diào)用接口,所有的系統(tǒng)調(diào)用都是經(jīng)過這里,系統(tǒng)調(diào)用會觸發(fā)一個 trap,將系統(tǒng)從用戶態(tài)轉(zhuǎn)換為內(nèi)核態(tài),然后將控制權(quán)移交給上面的內(nèi)核部件。
Linux 進(jìn)程和線程
下面我們就深入理解一下 Linux 內(nèi)核來理解 Linux 的基本概念之進(jìn)程和線程。系統(tǒng)調(diào)用是操作系統(tǒng)本身的接口,它對于創(chuàng)建進(jìn)程和線程,內(nèi)存分配,共享文件和 I/O 來說都很重要。
我們將從各個版本的共性出發(fā)來進(jìn)行探討。
基本概念
每個進(jìn)程都會運(yùn)行一段獨(dú)立的程序,并且在初始化的時候擁有一個獨(dú)立的控制線程。換句話說,每個進(jìn)程都會有一個自己的程序計(jì)數(shù)器,這個程序計(jì)數(shù)器用來記錄下一個需要被執(zhí)行的指令。Linux 允許進(jìn)程在運(yùn)行時創(chuàng)建額外的線程。
Linux 是一個多道程序設(shè)計(jì)系統(tǒng),因此系統(tǒng)中存在彼此相互獨(dú)立的進(jìn)程同時運(yùn)行。此外,每個用戶都會同時有幾個活動的進(jìn)程。因?yàn)槿绻且粋€大型系統(tǒng),可能有數(shù)百上千的進(jìn)程在同時運(yùn)行。
在某些用戶空間中,即使用戶退出登錄,仍然會有一些后臺進(jìn)程在運(yùn)行,這些進(jìn)程被稱為守護(hù)進(jìn)程(daemon)。
Linux 中有一種特殊的守護(hù)進(jìn)程被稱為計(jì)劃守護(hù)進(jìn)程(Cron daemon),計(jì)劃守護(hù)進(jìn)程可以每分鐘醒來一次檢查是否有工作要做,做完會繼續(xù)回到睡眠狀態(tài)等待下一次喚醒。
?Cron 是一個守護(hù)程序,可以做任何你想做的事情,比如說你可以定期進(jìn)行系統(tǒng)維護(hù)、定期進(jìn)行系統(tǒng)備份等。在其他操作系統(tǒng)上也有類似的程序,比如 Mac OS X 上 Cron 守護(hù)程序被稱為launchd的守護(hù)進(jìn)程。在 Windows 上可以被稱為計(jì)劃任務(wù)(Task Scheduler)。?
在 Linux 系統(tǒng)中,進(jìn)程通過非常簡單的方式來創(chuàng)建,fork系統(tǒng)調(diào)用會創(chuàng)建一個源進(jìn)程的拷貝(副本)。調(diào)用 fork 函數(shù)的進(jìn)程被稱為父進(jìn)程(parent process),使用 fork 函數(shù)創(chuàng)建出來的進(jìn)程被稱為子進(jìn)程(child process)。父進(jìn)程和子進(jìn)程都有自己的內(nèi)存映像。如果在子進(jìn)程創(chuàng)建出來后,父進(jìn)程修改了一些變量等,那么子進(jìn)程是看不到這些變化的,也就是 fork 后,父進(jìn)程和子進(jìn)程相互獨(dú)立。
雖然父進(jìn)程和子進(jìn)程保持相互獨(dú)立,但是它們卻能夠共享相同的文件,如果在 fork 之前,父進(jìn)程已經(jīng)打開了某個文件,那么 fork 后,父進(jìn)程和子進(jìn)程仍然共享這個打開的文件。對共享文件的修改會對父進(jìn)程和子進(jìn)程同時可見。
那么該如何區(qū)分父進(jìn)程和子進(jìn)程呢?子進(jìn)程只是父進(jìn)程的拷貝,所以它們幾乎所有的情況都一樣,包括內(nèi)存映像、變量、寄存器等。區(qū)分的關(guān)鍵在于fork函數(shù)調(diào)用后的返回值,如果 fork 后返回一個非零值,這個非零值即是子進(jìn)程的進(jìn)程標(biāo)識符(Process Identiier, PID),而會給子進(jìn)程返回一個零值,可以用下面代碼來進(jìn)行表示
pid=fork();//調(diào)用fork函數(shù)創(chuàng)建進(jìn)程 if(pid0){ ??error()?????//?pid?0,創(chuàng)建失敗 } else?if(pid?>0){ parent_handle()//父進(jìn)程代碼 } else{ child_handle()//子進(jìn)程代碼 }
父進(jìn)程在 fork 后會得到子進(jìn)程的 PID,這個 PID 即能代表這個子進(jìn)程的唯一標(biāo)識符也就是 PID。如果子進(jìn)程想要知道自己的 PID,可以調(diào)用getpid方法。當(dāng)子進(jìn)程結(jié)束運(yùn)行時,父進(jìn)程會得到子進(jìn)程的 PID,因?yàn)橐粋€進(jìn)程會 fork 很多子進(jìn)程,子進(jìn)程也會 fork 子進(jìn)程,所以 PID 是非常重要的。我們把第一次調(diào)用 fork 后的進(jìn)程稱為原始進(jìn)程,一個原始進(jìn)程可以生成一顆繼承樹
Linux 進(jìn)程間通信
Linux 進(jìn)程間的通信機(jī)制通常被稱為Internel-Process communication,IPC下面我們來說一說 Linux 進(jìn)程間通信的機(jī)制,大致來說,Linux 進(jìn)程間的通信機(jī)制可以分為 6 種
下面我們分別對其進(jìn)行概述
信號 signal
信號是 UNIX 系統(tǒng)最先開始使用的進(jìn)程間通信機(jī)制,因?yàn)?Linux 是繼承于 UNIX 的,所以 Linux 也支持信號機(jī)制,通過向一個或多個進(jìn)程發(fā)送異步事件信號來實(shí)現(xiàn),信號可以從鍵盤或者訪問不存在的位置等地方產(chǎn)生;信號通過 shell 將任務(wù)發(fā)送給子進(jìn)程。
你可以在 Linux 系統(tǒng)上輸入kill -l來列出系統(tǒng)使用的信號,下面是我提供的一些信號
進(jìn)程可以選擇忽略發(fā)送過來的信號,但是有兩個是不能忽略的:SIGSTOP和SIGKILL信號。SIGSTOP 信號會通知當(dāng)前正在運(yùn)行的進(jìn)程執(zhí)行關(guān)閉操作,SIGKILL 信號會通知當(dāng)前進(jìn)程應(yīng)該被殺死。除此之外,進(jìn)程可以選擇它想要處理的信號,進(jìn)程也可以選擇阻止信號,如果不阻止,可以選擇自行處理,也可以選擇進(jìn)行內(nèi)核處理。如果選擇交給內(nèi)核進(jìn)行處理,那么就執(zhí)行默認(rèn)處理。
操作系統(tǒng)會中斷目標(biāo)程序的進(jìn)程來向其發(fā)送信號、在任何非原子指令中,執(zhí)行都可以中斷,如果進(jìn)程已經(jīng)注冊了新號處理程序,那么就執(zhí)行進(jìn)程,如果沒有注冊,將采用默認(rèn)處理的方式。
例如:當(dāng)進(jìn)程收到SIGFPE浮點(diǎn)異常的信號后,默認(rèn)操作是對其進(jìn)行dump(轉(zhuǎn)儲)和退出。信號沒有優(yōu)先級的說法。如果同時為某個進(jìn)程產(chǎn)生了兩個信號,則可以將它們呈現(xiàn)給進(jìn)程或者以任意的順序進(jìn)行處理。
下面我們就來看一下這些信號是干什么用的
SIGABRT 和 SIGIOT
SIGABRT 和 SIGIOT 信號發(fā)送給進(jìn)程,告訴其進(jìn)行終止,這個 信號通常在調(diào)用 C標(biāo)準(zhǔn)庫的abort()函數(shù)時由進(jìn)程本身啟動
SIGALRM 、 SIGVTALRM、SIGPROF
當(dāng)設(shè)置的時鐘功能超時時會將 SIGALRM 、 SIGVTALRM、SIGPROF 發(fā)送給進(jìn)程。當(dāng)實(shí)際時間或時鐘時間超時時,發(fā)送 SIGALRM。當(dāng)進(jìn)程使用的 CPU 時間超時時,將發(fā)送 SIGVTALRM。當(dāng)進(jìn)程和系統(tǒng)代表進(jìn)程使用的CPU 時間超時時,將發(fā)送 SIGPROF。
SIGBUS
SIGBUS 將造成總線中斷錯誤時發(fā)送給進(jìn)程
SIGCHLD
當(dāng)子進(jìn)程終止、被中斷或者被中斷恢復(fù),將 SIGCHLD 發(fā)送給進(jìn)程。此信號的一種常見用法是指示操作系統(tǒng)在子進(jìn)程終止后清除其使用的資源。
SIGCONT
SIGCONT 信號指示操作系統(tǒng)繼續(xù)執(zhí)行先前由 SIGSTOP 或 SIGTSTP 信號暫停的進(jìn)程。該信號的一個重要用途是在 Unix shell 中的作業(yè)控制中。
SIGFPE
SIGFPE 信號在執(zhí)行錯誤的算術(shù)運(yùn)算(例如除以零)時將被發(fā)送到進(jìn)程。
SIGUP
當(dāng) SIGUP 信號控制的終端關(guān)閉時,會發(fā)送給進(jìn)程。許多守護(hù)程序?qū)⒅匦录虞d其配置文件并重新打開其日志文件,而不是在收到此信號時退出。
SIGILL
SIGILL 信號在嘗試執(zhí)行非法、格式錯誤、未知或者特權(quán)指令時發(fā)出
SIGINT
當(dāng)用戶希望中斷進(jìn)程時,操作系統(tǒng)會向進(jìn)程發(fā)送 SIGINT 信號。用戶輸入 ctrl - c 就是希望中斷進(jìn)程。
SIGKILL
SIGKILL 信號發(fā)送到進(jìn)程以使其馬上進(jìn)行終止。與 SIGTERM 和 SIGINT 相比,這個信號無法捕獲和忽略執(zhí)行,并且進(jìn)程在接收到此信號后無法執(zhí)行任何清理操作,下面是一些例外情況
僵尸進(jìn)程無法殺死,因?yàn)榻┦M(jìn)程已經(jīng)死了,它在等待父進(jìn)程對其進(jìn)行捕獲
處于阻塞狀態(tài)的進(jìn)程只有再次喚醒后才會被 kill 掉
init進(jìn)程是 Linux 的初始化進(jìn)程,這個進(jìn)程會忽略任何信號。
SIGKILL 通常是作為最后殺死進(jìn)程的信號、它通常作用于 SIGTERM 沒有響應(yīng)時發(fā)送給進(jìn)程。
SIGPIPE
SIGPIPE 嘗試寫入進(jìn)程管道時發(fā)現(xiàn)管道未連接無法寫入時發(fā)送到進(jìn)程
SIGPOLL
當(dāng)在明確監(jiān)視的文件描述符上發(fā)生事件時,將發(fā)送 SIGPOLL 信號。
SIGRTMIN 至 SIGRTMAX
SIGRTMIN 至 SIGRTMAX 是實(shí)時信號
SIGQUIT
當(dāng)用戶請求退出進(jìn)程并執(zhí)行核心轉(zhuǎn)儲時,SIGQUIT 信號將由其控制終端發(fā)送給進(jìn)程。
SIGSEGV
當(dāng) SIGSEGV 信號做出無效的虛擬內(nèi)存引用或分段錯誤時,即在執(zhí)行分段違規(guī)時,將其發(fā)送到進(jìn)程。
SIGSTOP
SIGSTOP 指示操作系統(tǒng)終止以便以后進(jìn)行恢復(fù)時
SIGSYS
當(dāng) SIGSYS 信號將錯誤參數(shù)傳遞給系統(tǒng)調(diào)用時,該信號將發(fā)送到進(jìn)程。
SYSTERM
我們上面簡單提到過了 SYSTERM 這個名詞,這個信號發(fā)送給進(jìn)程以請求終止。與 SIGKILL 信號不同,該信號可以被過程捕獲或忽略。這允許進(jìn)程執(zhí)行良好的終止,從而釋放資源并在適當(dāng)時保存狀態(tài)。SIGINT 與SIGTERM 幾乎相同。
SIGTSIP
SIGTSTP 信號由其控制終端發(fā)送到進(jìn)程,以請求終端停止。
SIGTTIN 和 SIGTTOU
當(dāng) SIGTTIN 和SIGTTOU 信號分別在后臺嘗試從 tty 讀取或?qū)懭霑r,信號將發(fā)送到該進(jìn)程。
SIGTRAP
在發(fā)生異常或者 trap 時,將 SIGTRAP 信號發(fā)送到進(jìn)程
SIGURG
當(dāng)套接字具有可讀取的緊急或帶外數(shù)據(jù)時,將 SIGURG 信號發(fā)送到進(jìn)程。
SIGUSR1 和 SIGUSR2
SIGUSR1 和 SIGUSR2 信號被發(fā)送到進(jìn)程以指示用戶定義的條件。
SIGXCPU
當(dāng) SIGXCPU 信號耗盡 CPU 的時間超過某個用戶可設(shè)置的預(yù)定值時,將其發(fā)送到進(jìn)程
SIGXFSZ
當(dāng) SIGXFSZ 信號增長超過最大允許大小的文件時,該信號將發(fā)送到該進(jìn)程。
SIGWINCH
SIGWINCH 信號在其控制終端更改其大?。ù翱诟模r發(fā)送給進(jìn)程。
管道 pipe
Linux 系統(tǒng)中的進(jìn)程可以通過建立管道 pipe 進(jìn)行通信。
在兩個進(jìn)程之間,可以建立一個通道,一個進(jìn)程向這個通道里寫入字節(jié)流,另一個進(jìn)程從這個管道中讀取字節(jié)流。管道是同步的,當(dāng)進(jìn)程嘗試從空管道讀取數(shù)據(jù)時,該進(jìn)程會被阻塞,直到有可用數(shù)據(jù)為止。shell 中的管線 pipelines就是用管道實(shí)現(xiàn)的,當(dāng) shell 發(fā)現(xiàn)輸出
sort
它會創(chuàng)建兩個進(jìn)程,一個是 sort,一個是 head,sort,會在這兩個應(yīng)用程序之間建立一個管道使得 sort 進(jìn)程的標(biāo)準(zhǔn)輸出作為 head 程序的標(biāo)準(zhǔn)輸入。sort 進(jìn)程產(chǎn)生的輸出就不用寫到文件中了,如果管道滿了系統(tǒng)會停止 sort 以等待 head 讀出數(shù)據(jù)
管道實(shí)際上就是|,兩個應(yīng)用程序不知道有管道的存在,一切都是由 shell 管理和控制的。
共享內(nèi)存 shared memory
兩個進(jìn)程之間還可以通過共享內(nèi)存進(jìn)行進(jìn)程間通信,其中兩個或者多個進(jìn)程可以訪問公共內(nèi)存空間。兩個進(jìn)程的共享工作是通過共享內(nèi)存完成的,一個進(jìn)程所作的修改可以對另一個進(jìn)程可見(很像線程間的通信)。
在使用共享內(nèi)存前,需要經(jīng)過一系列的調(diào)用流程,流程如下
創(chuàng)建共享內(nèi)存段或者使用已創(chuàng)建的共享內(nèi)存段(shmget())
將進(jìn)程附加到已經(jīng)創(chuàng)建的內(nèi)存段中(shmat())
從已連接的共享內(nèi)存段分離進(jìn)程(shmdt())
對共享內(nèi)存段執(zhí)行控制操作(shmctl())
先入先出隊(duì)列 FIFO
先入先出隊(duì)列 FIFO 通常被稱為命名管道(Named Pipes),命名管道的工作方式與常規(guī)管道非常相似,但是確實(shí)有一些明顯的區(qū)別。未命名的管道沒有備份文件:操作系統(tǒng)負(fù)責(zé)維護(hù)內(nèi)存中的緩沖區(qū),用來將字節(jié)從寫入器傳輸?shù)阶x取器。一旦寫入或者輸出終止的話,緩沖區(qū)將被回收,傳輸?shù)臄?shù)據(jù)會丟失。相比之下,命名管道具有支持文件和獨(dú)特 API ,命名管道在文件系統(tǒng)中作為設(shè)備的專用文件存在。當(dāng)所有的進(jìn)程通信完成后,命名管道將保留在文件系統(tǒng)中以備后用。命名管道具有嚴(yán)格的 FIFO 行為
寫入的第一個字節(jié)是讀取的第一個字節(jié),寫入的第二個字節(jié)是讀取的第二個字節(jié),依此類推。
消息隊(duì)列 Message Queue
一聽到消息隊(duì)列這個名詞你可能不知道是什么意思,消息隊(duì)列是用來描述內(nèi)核尋址空間內(nèi)的內(nèi)部鏈接列表。可以按幾種不同的方式將消息按順序發(fā)送到隊(duì)列并從隊(duì)列中檢索消息。每個消息隊(duì)列由 IPC 標(biāo)識符唯一標(biāo)識。消息隊(duì)列有兩種模式,一種是嚴(yán)格模式, 嚴(yán)格模式就像是 FIFO 先入先出隊(duì)列似的,消息順序發(fā)送,順序讀取。還有一種模式是非嚴(yán)格模式,消息的順序性不是非常重要。
套接字 Socket
還有一種管理兩個進(jìn)程間通信的是使用socket,socket 提供端到端的雙相通信。一個套接字可以與一個或多個進(jìn)程關(guān)聯(lián)。就像管道有命令管道和未命名管道一樣,套接字也有兩種模式,套接字一般用于兩個進(jìn)程之間的網(wǎng)絡(luò)通信,網(wǎng)絡(luò)套接字需要來自諸如TCP(傳輸控制協(xié)議)或較低級別UDP(用戶數(shù)據(jù)報(bào)協(xié)議)等基礎(chǔ)協(xié)議的支持。
套接字有以下幾種分類
順序包套接字(Sequential Packet Socket):此類套接字為最大長度固定的數(shù)據(jù)報(bào)提供可靠的連接。此連接是雙向的并且是順序的。
數(shù)據(jù)報(bào)套接字(Datagram Socket):數(shù)據(jù)包套接字支持雙向數(shù)據(jù)流。數(shù)據(jù)包套接字接受消息的順序與發(fā)送者可能不同。
流式套接字(Stream Socket):流套接字的工作方式類似于電話對話,提供雙向可靠的數(shù)據(jù)流。
原始套接字(Raw Socket):可以使用原始套接字訪問基礎(chǔ)通信協(xié)議。
Linux 中進(jìn)程管理系統(tǒng)調(diào)用
現(xiàn)在關(guān)注一下 Linux 系統(tǒng)中與進(jìn)程管理相關(guān)的系統(tǒng)調(diào)用。在了解之前你需要先知道一下什么是系統(tǒng)調(diào)用。
操作系統(tǒng)為我們屏蔽了硬件和軟件的差異,它的最主要功能就是為用戶提供一種抽象,隱藏內(nèi)部實(shí)現(xiàn),讓用戶只關(guān)心在 GUI 圖形界面下如何使用即可。操作系統(tǒng)可以分為兩種模式
內(nèi)核態(tài):操作系統(tǒng)內(nèi)核使用的模式
用戶態(tài):用戶應(yīng)用程序所使用的模式
我們常說的上下文切換指的就是內(nèi)核態(tài)模式和用戶態(tài)模式的頻繁切換。而系統(tǒng)調(diào)用指的就是引起內(nèi)核態(tài)和用戶態(tài)切換的一種方式,系統(tǒng)調(diào)用通常在后臺靜默運(yùn)行,表示計(jì)算機(jī)程序向其操作系統(tǒng)內(nèi)核請求服務(wù)。
系統(tǒng)調(diào)用指令有很多,下面是一些與進(jìn)程管理相關(guān)的最主要的系統(tǒng)調(diào)用
fork
fork 調(diào)用用于創(chuàng)建一個與父進(jìn)程相同的子進(jìn)程,創(chuàng)建完進(jìn)程后的子進(jìn)程擁有和父進(jìn)程一樣的程序計(jì)數(shù)器、相同的 CPU 寄存器、相同的打開文件。
exec
exec 系統(tǒng)調(diào)用用于執(zhí)行駐留在活動進(jìn)程中的文件,調(diào)用 exec 后,新的可執(zhí)行文件會替換先前的可執(zhí)行文件并獲得執(zhí)行。也就是說,調(diào)用 exec 后,會將舊文件或程序替換為新文件或執(zhí)行,然后執(zhí)行文件或程序。新的執(zhí)行程序被加載到相同的執(zhí)行空間中,因此進(jìn)程的PID不會修改,因?yàn)槲覀儭笡]有創(chuàng)建新進(jìn)程,只是替換舊進(jìn)程」。但是進(jìn)程的數(shù)據(jù)、代碼、堆棧都已經(jīng)被修改。如果當(dāng)前要被替換的進(jìn)程包含多個線程,那么所有的線程將被終止,新的進(jìn)程映像被加載執(zhí)行。
這里需要解釋一下進(jìn)程映像(Process image)的概念
「什么是進(jìn)程映像呢」?進(jìn)程映像是執(zhí)行程序時所需要的可執(zhí)行文件,通常會包括下面這些東西
「代碼段(codesegment/textsegment)」
又稱文本段,用來存放指令,運(yùn)行代碼的一塊內(nèi)存空間
此空間大小在代碼運(yùn)行前就已經(jīng)確定
內(nèi)存空間一般屬于只讀,某些架構(gòu)的代碼也允許可寫
在代碼段中,也有可能包含一些只讀的常數(shù)變量,例如字符串常量等。
「數(shù)據(jù)段(datasegment)」
可讀可寫
存儲初始化的全局變量和初始化的 static 變量
數(shù)據(jù)段中數(shù)據(jù)的生存期是隨程序持續(xù)性(隨進(jìn)程持續(xù)性) 隨進(jìn)程持續(xù)性:進(jìn)程創(chuàng)建就存在,進(jìn)程死亡就消失
「bss 段(bsssegment):」
可讀可寫
存儲未初始化的全局變量和未初始化的 static 變量
bss 段中的數(shù)據(jù)一般默認(rèn)為 0
「Data 段」
是可讀寫的,因?yàn)樽兞康闹悼梢栽谶\(yùn)行時更改。此段的大小也固定。
「棧(stack):」
可讀可寫
存儲的是函數(shù)或代碼中的局部變量(非 static 變量)
棧的生存期隨代碼塊持續(xù)性,代碼塊運(yùn)行就給你分配空間,代碼塊結(jié)束,就自動回收空間
「堆(heap):」
可讀可寫
存儲的是程序運(yùn)行期間動態(tài)分配的 malloc/realloc 的空間
堆的生存期隨進(jìn)程持續(xù)性,從 malloc/realloc 到 free 一直存在
下面是這些區(qū)域的構(gòu)成圖
exec 系統(tǒng)調(diào)用是一些函數(shù)的集合,這些函數(shù)是
execl
execle
execlp
execv
execve
execvp
下面來看一下 exec 的工作原理
當(dāng)前進(jìn)程映像被替換為新的進(jìn)程映像
新的進(jìn)程映像是你做為 exec 傳遞的燦睡
結(jié)束當(dāng)前正在運(yùn)行的進(jìn)程
新的進(jìn)程映像有 PID,相同的環(huán)境和一些文件描述符(因?yàn)槲刺鎿Q進(jìn)程,只是替換了進(jìn)程映像)
CPU 狀態(tài)和虛擬內(nèi)存受到影響,當(dāng)前進(jìn)程映像的虛擬內(nèi)存映射被新進(jìn)程映像的虛擬內(nèi)存代替。
waitpid
等待子進(jìn)程結(jié)束或終止
exit
在許多計(jì)算機(jī)操作系統(tǒng)上,計(jì)算機(jī)進(jìn)程的終止是通過執(zhí)行exit系統(tǒng)調(diào)用命令執(zhí)行的。0 表示進(jìn)程能夠正常結(jié)束,其他值表示進(jìn)程以非正常的行為結(jié)束。
其他一些常見的系統(tǒng)調(diào)用如下
Linux 進(jìn)程和線程的實(shí)現(xiàn)
Linux 進(jìn)程
在 Linux 內(nèi)核結(jié)構(gòu)中,進(jìn)程會被表示為任務(wù),通過結(jié)構(gòu)體structure來創(chuàng)建。不像其他的操作系統(tǒng)會區(qū)分進(jìn)程、輕量級進(jìn)程和線程,Linux 統(tǒng)一使用任務(wù)結(jié)構(gòu)來代表執(zhí)行上下文。因此,對于每個單線程進(jìn)程來說,單線程進(jìn)程將用一個任務(wù)結(jié)構(gòu)表示,對于多線程進(jìn)程來說,將為每一個用戶級線程分配一個任務(wù)結(jié)構(gòu)。Linux 內(nèi)核是多線程的,并且內(nèi)核級線程不與任何用戶級線程相關(guān)聯(lián)。
對于每個進(jìn)程來說,在內(nèi)存中都會有一個task_struct進(jìn)程描述符與之對應(yīng)。進(jìn)程描述符包含了內(nèi)核管理進(jìn)程所有有用的信息,包括「調(diào)度參數(shù)、打開文件描述符等等」。進(jìn)程描述符從進(jìn)程創(chuàng)建開始就一直存在于內(nèi)核堆棧中。
Linux 和 Unix 一樣,都是通過PID來區(qū)分不同的進(jìn)程,內(nèi)核會將所有進(jìn)程的任務(wù)結(jié)構(gòu)組成為一個雙向鏈表。PID 能夠直接被映射稱為進(jìn)程的任務(wù)結(jié)構(gòu)所在的地址,從而不需要遍歷雙向鏈表直接訪問。
我們上面提到了進(jìn)程描述符,這是一個非常重要的概念,我們上面還提到了進(jìn)程描述符是位于內(nèi)存中的,這里我們省略了一句話,那就是進(jìn)程描述符是存在用戶的任務(wù)結(jié)構(gòu)中,當(dāng)進(jìn)程位于內(nèi)存并開始運(yùn)行時,進(jìn)程描述符才會被調(diào)入內(nèi)存。
?進(jìn)程位于內(nèi)存被稱為PIM(Process In Memory),這是馮諾伊曼體系架構(gòu)的一種體現(xiàn),加載到內(nèi)存中并執(zhí)行的程序稱為進(jìn)程。簡單來說,一個進(jìn)程就是正在執(zhí)行的程序。?
進(jìn)程描述符可以歸為下面這幾類
調(diào)度參數(shù)(scheduling parameters):進(jìn)程優(yōu)先級、最近消耗 CPU 的時間、最近睡眠時間一起決定了下一個需要運(yùn)行的進(jìn)程
內(nèi)存映像(memory image):我們上面說到,進(jìn)程映像是執(zhí)行程序時所需要的可執(zhí)行文件,它由數(shù)據(jù)和代碼組成。
信號(signals):顯示哪些信號被捕獲、哪些信號被執(zhí)行
寄存器:當(dāng)發(fā)生內(nèi)核陷入 (trap) 時,寄存器的內(nèi)容會被保存下來。
系統(tǒng)調(diào)用狀態(tài)(system call state):當(dāng)前系統(tǒng)調(diào)用的信息,包括參數(shù)和結(jié)果
文件描述符表(file descriptor table):有關(guān)文件描述符的系統(tǒng)被調(diào)用時,文件描述符作為索引在文件描述符表中定位相關(guān)文件的 i-node 數(shù)據(jù)結(jié)構(gòu)
統(tǒng)計(jì)數(shù)據(jù)(accounting):記錄用戶、進(jìn)程占用系統(tǒng) CPU 時間表的指針,一些操作系統(tǒng)還保存進(jìn)程最多占用的 CPU 時間、進(jìn)程擁有的最大堆??臻g、進(jìn)程可以消耗的頁面數(shù)等。
內(nèi)核堆棧(kernel stack):進(jìn)程的內(nèi)核部分可以使用的固定堆棧
其他:當(dāng)前進(jìn)程狀態(tài)、事件等待時間、距離警報(bào)的超時時間、PID、父進(jìn)程的 PID 以及用戶標(biāo)識符等
有了上面這些信息,現(xiàn)在就很容易描述在 Linux 中是如何創(chuàng)建這些進(jìn)程的了,創(chuàng)建新流程實(shí)際上非常簡單?!笧樽舆M(jìn)程開辟一塊新的用戶空間的進(jìn)程描述符,然后從父進(jìn)程復(fù)制大量的內(nèi)容。為這個子進(jìn)程分配一個 PID,設(shè)置其內(nèi)存映射,賦予它訪問父進(jìn)程文件的權(quán)限,注冊并啟動」。
當(dāng)執(zhí)行 fork 系統(tǒng)調(diào)用時,調(diào)用進(jìn)程會陷入內(nèi)核并創(chuàng)建一些和任務(wù)相關(guān)的數(shù)據(jù)結(jié)構(gòu),比如內(nèi)核堆棧(kernel stack)和thread_info結(jié)構(gòu)。
?關(guān)于 thread_info 結(jié)構(gòu)可以參考
https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch_2avr32_2include_2asm_2thread__info_8h_source.html?
這個結(jié)構(gòu)中包含進(jìn)程描述符,進(jìn)程描述符位于固定的位置,使得 Linux 系統(tǒng)只需要很小的開銷就可以定位到一個運(yùn)行中進(jìn)程的數(shù)據(jù)結(jié)構(gòu)。
進(jìn)程描述符的主要內(nèi)容是根據(jù)父進(jìn)程的描述符來填充。Linux 操作系統(tǒng)會尋找一個可用的 PID,并且此 PID 沒有被任何進(jìn)程使用,更新進(jìn)程標(biāo)示符使其指向一個新的數(shù)據(jù)結(jié)構(gòu)即可。為了減少 hash table 的碰撞,進(jìn)程描述符會形成鏈表。它還將 task_struct 的字段設(shè)置為指向任務(wù)數(shù)組上相應(yīng)的上一個/下一個進(jìn)程。
?task_struct :Linux 進(jìn)程描述符,內(nèi)部涉及到眾多 C++ 源碼,我們會在后面進(jìn)行講解。?
從原則上來說,為子進(jìn)程開辟內(nèi)存區(qū)域并為子進(jìn)程分配數(shù)據(jù)段、堆棧段,并且對父進(jìn)程的內(nèi)容進(jìn)行復(fù)制,但是實(shí)際上 fork 完成后,子進(jìn)程和父進(jìn)程沒有共享內(nèi)存,所以需要復(fù)制技術(shù)來實(shí)現(xiàn)同步,但是復(fù)制開銷比較大,因此 Linux 操作系統(tǒng)使用了一種欺騙方式。即為子進(jìn)程分配頁表,然后新分配的頁表指向父進(jìn)程的頁面,同時這些頁面是只讀的。當(dāng)進(jìn)程向這些頁面進(jìn)行寫入的時候,會開啟保護(hù)錯誤。內(nèi)核發(fā)現(xiàn)寫入操作后,會為進(jìn)程分配一個副本,使得寫入時把數(shù)據(jù)復(fù)制到這個副本上,這個副本是共享的,這種方式稱為寫入時復(fù)制(copy on write),這種方式避免了在同一塊內(nèi)存區(qū)域維護(hù)兩個副本的必要,節(jié)省內(nèi)存空間。
在子進(jìn)程開始運(yùn)行后,操作系統(tǒng)會調(diào)用 exec 系統(tǒng)調(diào)用,內(nèi)核會進(jìn)行查找驗(yàn)證可執(zhí)行文件,把參數(shù)和環(huán)境變量復(fù)制到內(nèi)核,釋放舊的地址空間。
現(xiàn)在新的地址空間需要被創(chuàng)建和填充。如果系統(tǒng)支持映射文件,就像 Unix 系統(tǒng)一樣,那么新的頁表就會創(chuàng)建,表明內(nèi)存中沒有任何頁,除非所使用的頁面是堆棧頁,其地址空間由磁盤上的可執(zhí)行文件支持。新進(jìn)程開始運(yùn)行時,立刻會收到一個缺頁異常(page fault),這會使具有代碼的頁面加載進(jìn)入內(nèi)存。最后,參數(shù)和環(huán)境變量被復(fù)制到新的堆棧中,重置信號,寄存器全部清零。新的命令開始運(yùn)行。
下面是一個示例,用戶輸出 ls,shell 會調(diào)用 fork 函數(shù)復(fù)制一個新進(jìn)程,shell 進(jìn)程會調(diào)用 exec 函數(shù)用可執(zhí)行文件 ls 的內(nèi)容覆蓋它的內(nèi)存。
Linux 線程
現(xiàn)在我們來討論一下 Linux 中的線程,線程是輕量級的進(jìn)程,想必這句話你已經(jīng)聽過很多次了,輕量級體現(xiàn)在所有的進(jìn)程切換都需要清除所有的表、進(jìn)程間的共享信息也比較麻煩,一般來說通過管道或者共享內(nèi)存,如果是 fork 函數(shù)后的父子進(jìn)程則使用共享文件,然而線程切換不需要像進(jìn)程一樣具有昂貴的開銷,而且線程通信起來也更方便。線程分為兩種:用戶級線程和內(nèi)核級線程
用戶級線程
用戶級線程避免使用內(nèi)核,通常,每個線程會顯示調(diào)用開關(guān),發(fā)送信號或者執(zhí)行某種切換操作來放棄 CPU,同樣,計(jì)時器可以強(qiáng)制進(jìn)行開關(guān),用戶線程的切換速度通常比內(nèi)核線程快很多。在用戶級別實(shí)現(xiàn)線程會有一個問題,即單個線程可能會壟斷 CPU 時間片,導(dǎo)致其他線程無法執(zhí)行從而餓死。如果執(zhí)行一個 I/O 操作,那么 I/O 會阻塞,其他線程也無法運(yùn)行。
一種解決方案是,一些用戶級的線程包解決了這個問題。可以使用時鐘周期的監(jiān)視器來控制第一時間時間片獨(dú)占。然后,一些庫通過特殊的包裝來解決系統(tǒng)調(diào)用的 I/O 阻塞問題,或者可以為非阻塞 I/O 編寫任務(wù)。
內(nèi)核級線程
內(nèi)核級線程通常使用幾個進(jìn)程表在內(nèi)核中實(shí)現(xiàn),每個任務(wù)都會對應(yīng)一個進(jìn)程表。在這種情況下,內(nèi)核會在每個進(jìn)程的時間片內(nèi)調(diào)度每個線程。
所有能夠阻塞的調(diào)用都會通過系統(tǒng)調(diào)用的方式來實(shí)現(xiàn),當(dāng)一個線程阻塞時,內(nèi)核可以進(jìn)行選擇,是運(yùn)行在同一個進(jìn)程中的另一個線程(如果有就緒線程的話)還是運(yùn)行一個另一個進(jìn)程中的線程。
從用戶空間 -> 內(nèi)核空間 -> 用戶空間的開銷比較大,但是線程初始化的時間損耗可以忽略不計(jì)。這種實(shí)現(xiàn)的好處是由時鐘決定線程切換時間,因此不太可能將時間片與任務(wù)中的其他線程占用時間綁定到一起。同樣,I/O 阻塞也不是問題。
混合實(shí)現(xiàn)
結(jié)合用戶空間和內(nèi)核空間的優(yōu)點(diǎn),設(shè)計(jì)人員采用了一種內(nèi)核級線程的方式,然后將用戶級線程與某些或者全部內(nèi)核線程多路復(fù)用起來
在這種模型中,編程人員可以自由控制用戶線程和內(nèi)核線程的數(shù)量,具有很大的靈活度。采用這種方法,內(nèi)核只識別內(nèi)核級線程,并對其進(jìn)行調(diào)度。其中一些內(nèi)核級線程會被多個用戶級線程多路復(fù)用。
Linux 調(diào)度
下面我們來關(guān)注一下 Linux 系統(tǒng)的調(diào)度算法,首先需要認(rèn)識到,Linux 系統(tǒng)的線程是內(nèi)核線程,所以 Linux 系統(tǒng)是基于線程的,而不是基于進(jìn)程的。
為了進(jìn)行調(diào)度,Linux 系統(tǒng)將線程分為三類
實(shí)時先入先出
實(shí)時輪詢
分時
實(shí)時先入先出線程具有最高優(yōu)先級,它不會被其他線程所搶占,除非那是一個剛剛準(zhǔn)備好的,擁有更高優(yōu)先級的線程進(jìn)入。實(shí)時輪轉(zhuǎn)線程與實(shí)時先入先出線程基本相同,只是每個實(shí)時輪轉(zhuǎn)線程都有一個時間量,時間到了之后就可以被搶占。如果多個實(shí)時線程準(zhǔn)備完畢,那么每個線程運(yùn)行它時間量所規(guī)定的時間,然后插入到實(shí)時輪轉(zhuǎn)線程末尾。
?注意這個實(shí)時只是相對的,無法做到絕對的實(shí)時,因?yàn)榫€程的運(yùn)行時間無法確定。它們相對分時系統(tǒng)來說,更加具有實(shí)時性?
Linux 系統(tǒng)會給每個線程分配一個nice值,這個值代表了優(yōu)先級的概念。nice 值默認(rèn)值是 0 ,但是可以通過系統(tǒng)調(diào)用 nice 值來修改。修改值的范圍從 -20 - +19。nice 值決定了線程的靜態(tài)優(yōu)先級。一般系統(tǒng)管理員的 nice 值會比一般線程的優(yōu)先級高,它的范圍是 -20 - -1。
下面我們更詳細(xì)的討論一下 Linux 系統(tǒng)的兩個調(diào)度算法,它們的內(nèi)部與調(diào)度隊(duì)列(runqueue)的設(shè)計(jì)很相似。運(yùn)行隊(duì)列有一個數(shù)據(jù)結(jié)構(gòu)用來監(jiān)視系統(tǒng)中所有可運(yùn)行的任務(wù)并選擇下一個可以運(yùn)行的任務(wù)。每個運(yùn)行隊(duì)列和系統(tǒng)中的每個 CPU 有關(guān)。
Linux O(1)調(diào)度器是歷史上很流行的一個調(diào)度器。這個名字的由來是因?yàn)樗軌蛟诔?shù)時間內(nèi)執(zhí)行任務(wù)調(diào)度。在 O(1) 調(diào)度器里,調(diào)度隊(duì)列被組織成兩個數(shù)組,一個是任務(wù)「正在活動」的數(shù)組,一個是任務(wù)「過期失效」的數(shù)組。如下圖所示,每個數(shù)組都包含了 140 個鏈表頭,每個鏈表頭具有不同的優(yōu)先級。
大致流程如下:
調(diào)度器從正在活動數(shù)組中選擇一個優(yōu)先級最高的任務(wù)。如果這個任務(wù)的時間片過期失效了,就把它移動到過期失效數(shù)組中。如果這個任務(wù)阻塞了,比如說正在等待 I/O 事件,那么在它的時間片過期失效之前,一旦 I/O 操作完成,那么這個任務(wù)將會繼續(xù)運(yùn)行,它將被放回到之前正在活動的數(shù)組中,因?yàn)檫@個任務(wù)之前已經(jīng)消耗一部分 CPU 時間片,所以它將運(yùn)行剩下的時間片。當(dāng)這個任務(wù)運(yùn)行完它的時間片后,它就會被放到過期失效數(shù)組中。一旦正在活動的任務(wù)數(shù)組中沒有其他任務(wù)后,調(diào)度器將會交換指針,使得正在活動的數(shù)組變?yōu)檫^期失效數(shù)組,過期失效數(shù)組變?yōu)檎诨顒拥臄?shù)組。使用這種方式可以保證每個優(yōu)先級的任務(wù)都能夠得到執(zhí)行,不會導(dǎo)致線程饑餓。
在這種調(diào)度方式中,不同優(yōu)先級的任務(wù)所得到 CPU 分配的時間片也是不同的,高優(yōu)先級進(jìn)程往往能得到較長的時間片,低優(yōu)先級的任務(wù)得到較少的時間片。
這種方式為了保證能夠更好的提供服務(wù),通常會為交互式進(jìn)程賦予較高的優(yōu)先級,交互式進(jìn)程就是用戶進(jìn)程。
Linux 系統(tǒng)不知道一個任務(wù)究竟是 I/O 密集型的還是 CPU 密集型的,它只是依賴于交互式的方式,Linux 系統(tǒng)會區(qū)分是靜態(tài)優(yōu)先級還是動態(tài)優(yōu)先級。動態(tài)優(yōu)先級是采用一種獎勵機(jī)制來實(shí)現(xiàn)的。獎勵機(jī)制有兩種方式:「獎勵交互式線程、懲罰占用 CPU 的線程」。在 Linux O(1) 調(diào)度器中,最高的優(yōu)先級獎勵是 -5,注意這個優(yōu)先級越低越容易被線程調(diào)度器接受,所以最高懲罰的優(yōu)先級是 +5。具體體現(xiàn)就是操作系統(tǒng)維護(hù)一個名為sleep_avg的變量,任務(wù)喚醒會增加 sleep_avg 變量的值,當(dāng)任務(wù)被搶占或者時間量過期會減少這個變量的值,反映在獎勵機(jī)制上。
?O(1) 調(diào)度算法是 2.6 內(nèi)核版本的調(diào)度器,最初引入這個調(diào)度算法的是不穩(wěn)定的 2.5 版本。早期的調(diào)度算法在多處理器環(huán)境中說明了通過訪問正在活動數(shù)組就可以做出調(diào)度的決定。使調(diào)度可以在固定的時間 O(1) 完成。?
O(1) 調(diào)度器使用了一種啟發(fā)式的方式,這是什么意思?
?在計(jì)算機(jī)科學(xué)中,啟發(fā)式是一種當(dāng)傳統(tǒng)方式解決問題很慢時用來快速解決問題的方式,或者找到一個在傳統(tǒng)方法無法找到任何精確解的情況下找到近似解。?
O(1) 使用啟發(fā)式的這種方式,會使任務(wù)的優(yōu)先級變得復(fù)雜并且不完善,從而導(dǎo)致在處理交互任務(wù)時性能很糟糕。
為了改進(jìn)這個缺點(diǎn),O(1) 調(diào)度器的開發(fā)者又提出了一個新的方案,即公平調(diào)度器(Completely Fair Scheduler, CFS)。CFS 的主要思想是使用一顆紅黑樹作為調(diào)度隊(duì)列。
?數(shù)據(jù)結(jié)構(gòu)太重要了。?
CFS 會根據(jù)任務(wù)在 CPU 上的運(yùn)行時間長短而將其有序地排列在樹中,時間精確到納秒級。下面是 CFS 的構(gòu)造模型
CFS 的調(diào)度過程如下:
CFS 算法總是優(yōu)先調(diào)度哪些使用 CPU 時間最少的任務(wù)。最小的任務(wù)一般都是在最左邊的位置。當(dāng)有一個新的任務(wù)需要運(yùn)行時,CFS 會把這個任務(wù)和最左邊的數(shù)值進(jìn)行對比,如果此任務(wù)具有最小時間值,那么它將進(jìn)行運(yùn)行,否則它會進(jìn)行比較,找到合適的位置進(jìn)行插入。然后 CPU 運(yùn)行紅黑樹上當(dāng)前比較的最左邊的任務(wù)。
在紅黑樹中選擇一個節(jié)點(diǎn)來運(yùn)行的時間可以是常數(shù)時間,但是插入一個任務(wù)的時間是O(loog(N)),其中 N 是系統(tǒng)中的任務(wù)數(shù)??紤]到當(dāng)前系統(tǒng)的負(fù)載水平,這是可以接受的。
調(diào)度器只需要考慮可運(yùn)行的任務(wù)即可。這些任務(wù)被放在適當(dāng)?shù)恼{(diào)度隊(duì)列中。不可運(yùn)行的任務(wù)和正在等待的各種 I/O 操作或內(nèi)核事件的任務(wù)被放入一個等待隊(duì)列中。等待隊(duì)列頭包含一個指向任務(wù)鏈表的指針和一個自旋鎖。自旋鎖對于并發(fā)處理場景下用處很大。
Linux 系統(tǒng)中的同步
下面來聊一下 Linux 中的同步機(jī)制。早期的 Linux 內(nèi)核只有一個大內(nèi)核鎖(Big Kernel Lock,BKL)。它阻止了不同處理器并發(fā)處理的能力。因此,需要引入一些粒度更細(xì)的鎖機(jī)制。
Linux 提供了若干不同類型的同步變量,這些變量既能夠在內(nèi)核中使用,也能夠在用戶應(yīng)用程序中使用。在地層中,Linux 通過使用atomic_set和atomic_read這樣的操作為硬件支持的原子指令提供封裝。硬件提供內(nèi)存重排序,這是 Linux 屏障的機(jī)制。
具有高級別的同步像是自旋鎖的描述是這樣的,當(dāng)兩個進(jìn)程同時對資源進(jìn)行訪問,在一個進(jìn)程獲得資源后,另一個進(jìn)程不想被阻塞,所以它就會自旋,等待一會兒再對資源進(jìn)行訪問。Linux 也提供互斥量或信號量這樣的機(jī)制,也支持像是mutex_tryLock和mutex_tryWait這樣的非阻塞調(diào)用。也支持中斷處理事務(wù),也可以通過動態(tài)禁用和啟用相應(yīng)的中斷來實(shí)現(xiàn)。
Linux 啟動
下面來聊一聊 Linux 是如何啟動的。
當(dāng)計(jì)算機(jī)電源通電后,BIOS會進(jìn)行開機(jī)自檢(Power-On-Self-Test, POST),對硬件進(jìn)行檢測和初始化。因?yàn)椴僮飨到y(tǒng)的啟動會使用到磁盤、屏幕、鍵盤、鼠標(biāo)等設(shè)備。下一步,磁盤中的第一個分區(qū),也被稱為MBR(Master Boot Record)主引導(dǎo)記錄,被讀入到一個固定的內(nèi)存區(qū)域并執(zhí)行。這個分區(qū)中有一個非常小的,只有 512 字節(jié)的程序。程序從磁盤中調(diào)入 boot 獨(dú)立程序,boot 程序?qū)⒆陨韽?fù)制到高位地址的內(nèi)存從而為操作系統(tǒng)釋放低位地址的內(nèi)存。
復(fù)制完成后,boot 程序讀取啟動設(shè)備的根目錄。boot 程序要理解文件系統(tǒng)和目錄格式。然后 boot 程序被調(diào)入內(nèi)核,把控制權(quán)移交給內(nèi)核。直到這里,boot 完成了它的工作。系統(tǒng)內(nèi)核開始運(yùn)行。
內(nèi)核啟動代碼是使用匯編語言完成的,主要包括創(chuàng)建內(nèi)核堆棧、識別 CPU 類型、計(jì)算內(nèi)存、禁用中斷、啟動內(nèi)存管理單元等,然后調(diào)用 C 語言的 main 函數(shù)執(zhí)行操作系統(tǒng)部分。
這部分也會做很多事情,首先會分配一個消息緩沖區(qū)來存放調(diào)試出現(xiàn)的問題,調(diào)試信息會寫入緩沖區(qū)。如果調(diào)試出現(xiàn)錯誤,這些信息可以通過診斷程序調(diào)出來。
然后操作系統(tǒng)會進(jìn)行自動配置,檢測設(shè)備,加載配置文件,被檢測設(shè)備如果做出響應(yīng),就會被添加到已鏈接的設(shè)備表中,如果沒有相應(yīng),就歸為未連接直接忽略。
配置完所有硬件后,接下來要做的就是仔細(xì)手工處理進(jìn)程0,設(shè)置其堆棧,然后運(yùn)行它,執(zhí)行初始化、配置時鐘、掛載文件系統(tǒng)。創(chuàng)建init 進(jìn)程(進(jìn)程 1 )和守護(hù)進(jìn)程(進(jìn)程 2)。
init 進(jìn)程會檢測它的標(biāo)志以確定它是否為單用戶還是多用戶服務(wù)。在前一種情況中,它會調(diào)用 fork 函數(shù)創(chuàng)建一個 shell 進(jìn)程,并且等待這個進(jìn)程結(jié)束。后一種情況調(diào)用 fork 函數(shù)創(chuàng)建一個運(yùn)行系統(tǒng)初始化的 shell 腳本(即 /etc/rc)的進(jìn)程,這個進(jìn)程可以進(jìn)行文件系統(tǒng)一致性檢測、掛載文件系統(tǒng)、開啟守護(hù)進(jìn)程等。
然后 /etc/rc 這個進(jìn)程會從 /etc/ttys 中讀取數(shù)據(jù),/etc/ttys 列出了所有的終端和屬性。對于每一個啟用的終端,這個進(jìn)程調(diào)用 fork 函數(shù)創(chuàng)建一個自身的副本,進(jìn)行內(nèi)部處理并運(yùn)行一個名為getty的程序。
getty 程序會在終端上輸入
login:
等待用戶輸入用戶名,在輸入用戶名后,getty 程序結(jié)束,登陸程序/bin/login開始運(yùn)行。login 程序需要輸入密碼,并與保存在/etc/passwd中的密碼進(jìn)行對比,如果輸入正確,login 程序以用戶 shell 程序替換自身,等待第一個命令。如果不正確,login 程序要求輸入另一個用戶名。
整個系統(tǒng)啟動過程如下
Linux 內(nèi)存管理
Linux 內(nèi)存管理模型非常直接明了,因?yàn)?Linux 的這種機(jī)制使其具有可移植性并且能夠在內(nèi)存管理單元相差不大的機(jī)器下實(shí)現(xiàn) Linux,下面我們就來認(rèn)識一下 Linux 內(nèi)存管理是如何實(shí)現(xiàn)的。
基本概念
每個 Linux 進(jìn)程都會有地址空間,這些地址空間由三個段區(qū)域組成:「text 段、data 段、stack 段」。下面是進(jìn)程地址空間的示例。
數(shù)據(jù)段(data segment)包含了程序的變量、字符串、數(shù)組和其他數(shù)據(jù)的存儲。數(shù)據(jù)段分為兩部分,已經(jīng)初始化的數(shù)據(jù)和尚未初始化的數(shù)據(jù)。其中尚未初始化的數(shù)據(jù)就是我們說的 BSS。數(shù)據(jù)段部分的初始化需要編譯就期確定的常量以及程序啟動就需要一個初始值的變量。所有 BSS 部分中的變量在加載后被初始化為 0 。
和代碼段(Text segment)不一樣,data segment 數(shù)據(jù)段可以改變。程序總是修改它的變量。而且,許多程序需要在執(zhí)行時動態(tài)分配空間。Linux 允許數(shù)據(jù)段隨著內(nèi)存的分配和回收從而增大或者減小。為了分配內(nèi)存,程序可以增加數(shù)據(jù)段的大小。在 C 語言中有一套標(biāo)準(zhǔn)庫malloc經(jīng)常用于分配內(nèi)存。進(jìn)程地址空間描述符包含動態(tài)分配的內(nèi)存區(qū)域稱為堆(heap)。
第三部分段是棧段(stack segment)。在大部分機(jī)器上,棧段會在虛擬內(nèi)存地址頂部地址位置處,并向低位置處(向地址空間為 0 處)拓展。舉個例子來說,在 32 位 x86 架構(gòu)的機(jī)器上,棧開始于0xC0000000,這是用戶模式下進(jìn)程允許可見的 3GB 虛擬地址限制。如果棧一直增大到超過棧段后,就會發(fā)生硬件故障并把頁面下降一個頁面。
當(dāng)程序啟動時,棧區(qū)域并不是空的,相反,它會包含所有的 shell 環(huán)境變量以及為了調(diào)用它而向 shell 輸入的命令行。舉個例子,當(dāng)你輸入
cpcxuanlx
時,cp 程序會運(yùn)行并在棧中帶著字符串cp cxuan lx,這樣就能夠找出源文件和目標(biāo)文件的名稱。
當(dāng)兩個用戶運(yùn)行在相同程序中,例如編輯器(editor),那么就會在內(nèi)存中保持編輯器程序代碼的兩個副本,但是這種方式并不高效。Linux 系統(tǒng)支持共享文本段作為替代。下面圖中我們會看到 A 和 B 兩個進(jìn)程,它們有著相同的文本區(qū)域。
數(shù)據(jù)段和棧段只有在 fork 之后才會共享,共享也是共享未修改過的頁面。如果任何一個都需要變大但是沒有相鄰空間容納的話,也不會有問題,因?yàn)橄噜彽奶摂M頁面不必映射到相鄰的物理頁面上。
除了動態(tài)分配更多的內(nèi)存,Linux 中的進(jìn)程可以通過內(nèi)存映射文件來訪問文件數(shù)據(jù)。這個特性可以使我們把一個文件映射到進(jìn)程空間的一部分而該文件就可以像位于內(nèi)存中的字節(jié)數(shù)組一樣被讀寫。把一個文件映射進(jìn)來使得隨機(jī)讀寫比使用 read 和 write 之類的 I/O 系統(tǒng)調(diào)用要容易得多。共享庫的訪問就是使用了這種機(jī)制。如下所示
我們可以看到兩個相同文件會被映射到相同的物理地址上,但是它們屬于不同的地址空間。
映射文件的優(yōu)點(diǎn)是,兩個或多個進(jìn)程可以同時映射到同一文件中,任意一個進(jìn)程對文件的寫操作對其他文件可見。通過使用映射臨時文件的方式,可以為多線程共享內(nèi)存提供高帶寬,臨時文件在進(jìn)程退出后消失。但是實(shí)際上,并沒有兩個相同的地址空間,因?yàn)槊總€進(jìn)程維護(hù)的打開文件和信號不同。
Linux 內(nèi)存管理系統(tǒng)調(diào)用
下面我們探討一下關(guān)于內(nèi)存管理的系統(tǒng)調(diào)用方式。事實(shí)上,POSIX 并沒有給內(nèi)存管理指定任何的系統(tǒng)調(diào)用。然而,Linux 卻有自己的內(nèi)存系統(tǒng)調(diào)用,主要系統(tǒng)調(diào)用如下
如果遇到錯誤,那么 s 的返回值是 -1,a 和 addr 是內(nèi)存地址,len 表示的是長度,prot 表示的是控制保護(hù)位,flags 是其他標(biāo)志位,fd 是文件描述符,offset 是文件偏移量。
brk通過給出超過數(shù)據(jù)段之外的第一個字節(jié)地址來指定數(shù)據(jù)段的大小。如果新的值要比原來的大,那么數(shù)據(jù)區(qū)會變得越來越大,反之會越來越小。
mmap和unmap系統(tǒng)調(diào)用會控制映射文件。mmp 的第一個參數(shù) addr 決定了文件映射的地址。它必須是頁面大小的倍數(shù)。如果參數(shù)是 0,系統(tǒng)會分配地址并返回 a。第二個參數(shù)是長度,它告訴了需要映射多少字節(jié)。它也是頁面大小的倍數(shù)。prot 決定了映射文件的保護(hù)位,保護(hù)位可以標(biāo)記為「可讀、可寫、可執(zhí)行或者這些的結(jié)合」。第四個參數(shù) flags 能夠控制文件是私有的還是可讀的以及 addr 是必須的還是只是進(jìn)行提示。第五個參數(shù) fd 是要映射的文件描述符。只有打開的文件是可以被映射的,因此如果想要進(jìn)行文件映射,必須打開文件;最后一個參數(shù) offset 會指示文件從什么時候開始,并不一定每次都要從零開始。
Linux 內(nèi)存管理實(shí)現(xiàn)
內(nèi)存管理系統(tǒng)是操作系統(tǒng)最重要的部分之一。從計(jì)算機(jī)早期開始,我們實(shí)際使用的內(nèi)存都要比系統(tǒng)中實(shí)際存在的內(nèi)存多。內(nèi)存分配策略克服了這一限制,并且其中最有名的就是虛擬內(nèi)存(virtual memory)。通過在多個競爭的進(jìn)程之間共享虛擬內(nèi)存,虛擬內(nèi)存得以讓系統(tǒng)有更多的內(nèi)存。虛擬內(nèi)存子系統(tǒng)主要包括下面這些概念。
「大地址空間」
操作系統(tǒng)使系統(tǒng)使用起來好像比實(shí)際的物理內(nèi)存要大很多,那是因?yàn)樘摂M內(nèi)存要比物理內(nèi)存大很多倍。
「保護(hù)」
系統(tǒng)中的每個進(jìn)程都會有自己的虛擬地址空間。這些虛擬地址空間彼此完全分開,因此運(yùn)行一個應(yīng)用程序的進(jìn)程不會影響另一個。并且,硬件虛擬內(nèi)存機(jī)制允許內(nèi)存保護(hù)關(guān)鍵內(nèi)存區(qū)域。
「內(nèi)存映射」
內(nèi)存映射用來向進(jìn)程地址空間映射圖像和數(shù)據(jù)文件。在內(nèi)存映射中,文件的內(nèi)容直接映射到進(jìn)程的虛擬空間中。
「公平的物理內(nèi)存分配」
內(nèi)存管理子系統(tǒng)允許系統(tǒng)中的每個正在運(yùn)行的進(jìn)程公平分配系統(tǒng)的物理內(nèi)存。
「共享虛擬內(nèi)存」
盡管虛擬內(nèi)存讓進(jìn)程有自己的內(nèi)存空間,但是有的時候你是需要共享內(nèi)存的。例如幾個進(jìn)程同時在 shell 中運(yùn)行,這會涉及到 IPC 的進(jìn)程間通信問題,這個時候你需要的是共享內(nèi)存來進(jìn)行信息傳遞而不是通過拷貝每個進(jìn)程的副本獨(dú)立運(yùn)行。
下面我們就正式探討一下什么是虛擬內(nèi)存
虛擬內(nèi)存的抽象模型
在考慮 Linux 用于支持虛擬內(nèi)存的方法之前,考慮一個不會被太多細(xì)節(jié)困擾的抽象模型是很有用的。
處理器在執(zhí)行指令時,會從內(nèi)存中讀取指令并將其解碼(decode),在指令解碼時會獲取某個位置的內(nèi)容并將他存到內(nèi)存中。然后處理器繼續(xù)執(zhí)行下一條指令。這樣,處理器總是在訪問存儲器以獲取指令和存儲數(shù)據(jù)。
在虛擬內(nèi)存系統(tǒng)中,所有的地址空間都是虛擬的而不是物理的。但是實(shí)際存儲和提取指令的是物理地址,所以需要讓處理器根據(jù)操作系統(tǒng)維護(hù)的一張表將虛擬地址轉(zhuǎn)換為物理地址。
為了簡單的完成轉(zhuǎn)換,虛擬地址和物理地址會被分為固定大小的塊,稱為頁(page)。這些頁有相同大小,如果頁面大小不一樣的話,那么操作系統(tǒng)將很難管理。Alpha AXP系統(tǒng)上的 Linux 使用 8 KB 頁面,而 Intel x86 系統(tǒng)上的 Linux 使用 4 KB 頁面。每個頁面都有一個唯一的編號,即頁面框架號(PFN)。
上面就是 Linux 內(nèi)存映射模型了,在這個頁模型中,虛擬地址由兩部分組成:「偏移量和虛擬頁框號」。每次處理器遇到虛擬地址時都會提取偏移量和虛擬頁框號。處理器必須將虛擬頁框號轉(zhuǎn)換為物理頁號,然后以正確的偏移量的位置訪問物理頁。
上圖中展示了兩個進(jìn)程 A 和 B 的虛擬地址空間,每個進(jìn)程都有自己的頁表。這些頁表將進(jìn)程中的虛擬頁映射到內(nèi)存中的物理頁中。頁表中每一項(xiàng)均包含
有效標(biāo)志(valid flag):表明此頁表?xiàng)l目是否有效
該條目描述的物理頁框號
訪問控制信息,頁面使用方式,是否可寫以及是否可以執(zhí)行代碼
要將處理器的虛擬地址映射為內(nèi)存的物理地址,首先需要計(jì)算虛擬地址的頁框號和偏移量。頁面大小為 2 的次冪,可以通過移位完成操作。
如果當(dāng)前進(jìn)程嘗試訪問虛擬地址,但是訪問不到的話,這種情況稱為缺頁異常,此時虛擬操作系統(tǒng)的錯誤地址和頁面錯誤的原因?qū)⑼ㄖ僮飨到y(tǒng)。
通過以這種方式將虛擬地址映射到物理地址,虛擬內(nèi)存可以以任何順序映射到系統(tǒng)的物理頁面。
按需分頁
由于物理內(nèi)存要比虛擬內(nèi)存少很多,因此操作系統(tǒng)需要注意盡量避免直接使用低效的物理內(nèi)存。節(jié)省物理內(nèi)存的一種方式是僅加載執(zhí)行程序當(dāng)前使用的頁面(這何嘗不是一種懶加載的思想呢?)。例如,可以運(yùn)行數(shù)據(jù)庫來查詢數(shù)據(jù)庫,在這種情況下,不是所有的數(shù)據(jù)都裝入內(nèi)存,只裝載需要檢查的數(shù)據(jù)。這種僅僅在需要時才將虛擬頁面加載進(jìn)內(nèi)中的技術(shù)稱為按需分頁。
交換
如果某個進(jìn)程需要將虛擬頁面?zhèn)魅雰?nèi)存,但是此時沒有可用的物理頁面,那么操作系統(tǒng)必須丟棄物理內(nèi)存中的另一個頁面來為該頁面騰出空間。
如果頁面已經(jīng)修改過,那么操作系統(tǒng)必須保留該頁面的內(nèi)容,以便以后可以訪問它。這種類型的頁面被稱為臟頁,當(dāng)將其從內(nèi)存中移除時,它會保存在稱為交換文件的特殊文件中。相對于處理器和物理內(nèi)存的速度,對交換文件的訪問非常慢,并且操作系統(tǒng)需要兼顧將頁面寫到磁盤的以及將它們保留在內(nèi)存中以便再次使用。
Linux 使用最近最少使用(LRU)頁面老化技術(shù)來公平的選擇可能會從系統(tǒng)中刪除的頁面,這個方案涉及系統(tǒng)中的每個頁面,頁面的年齡隨著訪問次數(shù)的變化而變化,如果某個頁面訪問次數(shù)多,那么該頁就表示越年輕,如果某個呃頁面訪問次數(shù)太少,那么該頁越容易被換出。
物理和虛擬尋址模式
大多數(shù)多功能處理器都支持物理地址模式和虛擬地址模式的概念。物理尋址模式不需要頁表,并且處理器不會在此模式下嘗試執(zhí)行任何地址轉(zhuǎn)換。Linux 內(nèi)核被鏈接在物理地址空間中運(yùn)行。
Alpha AXP 處理器沒有物理尋址模式。相反,它將內(nèi)存空間劃分為幾個區(qū)域,并將其中兩個指定為物理映射的地址。此內(nèi)核地址空間稱為 KSEG 地址空間,它包含從 0xfffffc0000000000 向上的所有地址。為了從 KSEG 中鏈接的代碼(按照定義,內(nèi)核代碼)執(zhí)行或訪問其中的數(shù)據(jù),該代碼必須在內(nèi)核模式下執(zhí)行。鏈接到 Alpha 上的 Linux內(nèi)核以從地址 0xfffffc0000310000 執(zhí)行。
訪問控制
頁面表的每一項(xiàng)還包含訪問控制信息,訪問控制信息主要檢查進(jìn)程是否應(yīng)該訪問內(nèi)存。
必要時需要對內(nèi)存進(jìn)行訪問限制。例如包含可執(zhí)行代碼的內(nèi)存,自然是只讀內(nèi)存;操作系統(tǒng)不應(yīng)允許進(jìn)程通過其可執(zhí)行代碼寫入數(shù)據(jù)。相比之下,包含數(shù)據(jù)的頁面可以被寫入,但是嘗試執(zhí)行該內(nèi)存的指令將失敗。大多數(shù)處理器至少具有兩種執(zhí)行模式:內(nèi)核態(tài)和用戶態(tài)。你不希望訪問用戶執(zhí)行內(nèi)核代碼或內(nèi)核數(shù)據(jù)結(jié)構(gòu),除非處理器以內(nèi)核模式運(yùn)行。
訪問控制信息被保存在上面的 Page Table Entry ,頁表項(xiàng)中,上面這幅圖是 Alpha AXP的 PTE。位字段具有以下含義
V
表示 valid ,是否有效位
FOR
讀取時故障,在嘗試讀取此頁面時出現(xiàn)故障
FOW
寫入時錯誤,在嘗試寫入時發(fā)生錯誤
FOE
執(zhí)行時發(fā)生錯誤,在嘗試執(zhí)行此頁面中的指令時,處理器都會報(bào)告頁面錯誤并將控制權(quán)傳遞給操作系統(tǒng),
ASM
地址空間匹配,當(dāng)操作系統(tǒng)希望清除轉(zhuǎn)換緩沖區(qū)中的某些條目時,將使用此選項(xiàng)。
GH
當(dāng)在使用單個轉(zhuǎn)換緩沖區(qū)條目而不是多個轉(zhuǎn)換緩沖區(qū)條目映射整個塊時使用的提示。
KRE
內(nèi)核模式運(yùn)行下的代碼可以讀取頁面
URE
用戶模式下的代碼可以讀取頁面
KWE
以內(nèi)核模式運(yùn)行的代碼可以寫入頁面
UWE
以用戶模式運(yùn)行的代碼可以寫入頁面
頁框號
對于設(shè)置了 V 位的 PTE,此字段包含此 PTE 的物理頁面幀號(頁面幀號)。對于無效的 PTE,如果此字段不為零,則包含有關(guān)頁面在交換文件中的位置的信息。
除此之外,Linux 還使用了兩個位
_PAGE_DIRTY
如果已設(shè)置,則需要將頁面寫出到交換文件中
_PAGE_ACCESSED
Linux 用來將頁面標(biāo)記為已訪問。
緩存
上面的虛擬內(nèi)存抽象模型可以用來實(shí)施,但是效率不會太高。操作系統(tǒng)和處理器設(shè)計(jì)人員都嘗試提高性能。但是除了提高處理器,內(nèi)存等的速度之外,最好的方法就是維護(hù)有用信息和數(shù)據(jù)的高速緩存,從而使某些操作更快。在 Linux 中,使用很多和內(nèi)存管理有關(guān)的緩沖區(qū),使用緩沖區(qū)來提高效率。
緩沖區(qū)緩存
緩沖區(qū)高速緩存包含塊設(shè)備驅(qū)動程序使用的數(shù)據(jù)緩沖區(qū)。
還記得什么是塊設(shè)備么?這里回顧下
塊設(shè)備是一個能存儲固定大小塊信息的設(shè)備,它支持「以固定大小的塊,扇區(qū)或群集讀取和(可選)寫入數(shù)據(jù)」。每個塊都有自己的物理地址。通常塊的大小在 512 - 65536 之間。所有傳輸?shù)男畔⒍紩赃B續(xù)的塊為單位。塊設(shè)備的基本特征是每個塊都較為對立,能夠獨(dú)立的進(jìn)行讀寫。常見的塊設(shè)備有「硬盤、藍(lán)光光盤、USB 盤」
與字符設(shè)備相比,塊設(shè)備通常需要較少的引腳。
緩沖區(qū)高速緩存通過設(shè)備標(biāo)識符和塊編號用于快速查找數(shù)據(jù)塊。如果可以在緩沖區(qū)高速緩存中找到數(shù)據(jù),則無需從物理塊設(shè)備中讀取數(shù)據(jù),這種訪問方式要快得多。
頁緩存
頁緩存用于加快對磁盤上圖像和數(shù)據(jù)的訪問
它用于一次一頁地緩存文件中的內(nèi)容,并且可以通過文件和文件中的偏移量進(jìn)行訪問。當(dāng)頁面從磁盤讀入內(nèi)存時,它們被緩存在頁面緩存中。
交換區(qū)緩存
僅僅已修改(臟頁)被保存在交換文件中
只要這些頁面在寫入交換文件后沒有修改,則下次交換該頁面時,無需將其寫入交換文件,因?yàn)樵擁撁嬉言诮粨Q文件中??梢灾苯觼G棄。在大量交換的系統(tǒng)中,這節(jié)省了許多不必要的和昂貴的磁盤操作。
硬件緩存
處理器中通常使用一種硬件緩存。頁表?xiàng)l目的緩存。在這種情況下,處理器并不總是直接讀取頁表,而是根據(jù)需要緩存頁的翻譯。這些是轉(zhuǎn)換后備緩沖區(qū)也被稱為TLB,包含來自系統(tǒng)中一個或多個進(jìn)程的頁表項(xiàng)的緩存副本。
引用虛擬地址后,處理器將嘗試查找匹配的 TLB 條目。如果找到,則可以將虛擬地址直接轉(zhuǎn)換為物理地址,并對數(shù)據(jù)執(zhí)行正確的操作。如果處理器找不到匹配的 TLB 條目, 它通過向操作系統(tǒng)發(fā)信號通知已發(fā)生 TLB 丟失獲得操作系統(tǒng)的支持和幫助。系統(tǒng)特定的機(jī)制用于將該異常傳遞給可以修復(fù)問題的操作系統(tǒng)代碼。操作系統(tǒng)為地址映射生成一個新的 TLB 條目。清除異常后,處理器將再次嘗試轉(zhuǎn)換虛擬地址。這次能夠執(zhí)行成功。
使用緩存也存在缺點(diǎn),為了節(jié)省精力,Linux 必須使用更多的時間和空間來維護(hù)這些緩存,并且如果緩存損壞,系統(tǒng)將會崩潰。
Linux 頁表
Linux 假定頁表分為三個級別。訪問的每個頁表都包含下一級頁表
圖中的 PDG 表示全局頁表,當(dāng)創(chuàng)建一個新的進(jìn)程時,都要為新進(jìn)程創(chuàng)建一個新的頁面目錄,即 PGD。
要將虛擬地址轉(zhuǎn)換為物理地址,處理器必須獲取每個級別字段的內(nèi)容,將其轉(zhuǎn)換為包含頁表的物理頁的偏移量,并讀取下一級頁表的頁框號。這樣重復(fù)三次,直到找到包含虛擬地址的物理頁面的頁框號為止。
Linux 運(yùn)行的每個平臺都必須提供翻譯宏,這些宏允許內(nèi)核遍歷特定進(jìn)程的頁表。這樣,內(nèi)核無需知道頁表?xiàng)l目的格式或它們的排列方式。
頁分配和取消分配
對系統(tǒng)中物理頁面有很多需求。例如,當(dāng)圖像加載到內(nèi)存中時,操作系統(tǒng)需要分配頁面。
系統(tǒng)中所有物理頁面均由mem_map數(shù)據(jù)結(jié)構(gòu)描述,這個數(shù)據(jù)結(jié)構(gòu)是mem_map_t的列表。它包括一些重要的屬性
count :這是頁面的用戶數(shù)計(jì)數(shù),當(dāng)頁面在多個進(jìn)程之間共享時,計(jì)數(shù)大于 1
age:這是描述頁面的年齡,用于確定頁面是否適合丟棄或交換
map_nr :這是此mem_map_t描述的物理頁框號。
頁面分配代碼使用free_area向量查找和釋放頁面,free_area 的每個元素都包含有關(guān)頁面塊的信息。
頁面分配
Linux 的頁面分配使用一種著名的伙伴算法來進(jìn)行頁面的分配和取消分配。頁面以 2 的冪為單位進(jìn)行塊分配。這就意味著它可以分配 1頁、2 頁、4頁等等,只要系統(tǒng)中有足夠可用的頁面來滿足需求就可以。判斷的標(biāo)準(zhǔn)是「nr_free_pages> min_free_pages」,如果滿足,就會在 free_area 中搜索所需大小的頁面塊完成分配。free_area 的每個元素都有該大小的塊的已分配頁面和空閑頁面塊的映射。
分配算法會搜索請求大小的頁面塊。如果沒有任何請求大小的頁面塊可用的話,會搜尋一個是請求大小二倍的頁面塊,然后重復(fù),直到一直搜尋完 free_area 找到一個頁面塊為止。如果找到的頁面塊要比請求的頁面塊大,就會對找到的頁面塊進(jìn)行細(xì)分,直到找到合適的大小塊為止。
因?yàn)槊總€塊都是 2 的次冪,所以拆分過程很容易,因?yàn)槟阒恍鑼K分成兩半即可??臻e塊在適當(dāng)?shù)年?duì)列中排隊(duì),分配的頁面塊返回給調(diào)用者。
如果請求一個 2 個頁的塊,則 4 頁的第一個塊(從第 4 頁的框架開始)將被分成兩個 2 頁的塊。第一個頁面(從第 4 頁的幀開始)將作為分配的頁面返回給調(diào)用方,第二個塊(從第 6 頁的頁面開始)將作為 2 頁的空閑塊排隊(duì)到 free_area 數(shù)組的元素 1 上。
頁面取消分配
上面的這種內(nèi)存方式最造成一種后果,那就是內(nèi)存的碎片化,會將較大的空閑頁面分成較小的頁面。頁面解除分配代碼會盡可能將頁面重新組合成為更大的空閑塊。每釋放一個頁面,都會檢查相同大小的相鄰的塊,以查看是否空閑。如果是,則將其與新釋放的頁面塊組合以形成下一個頁面大小塊的新的自由頁面塊。每次將兩個頁面塊重新組合為更大的空閑頁面塊時,頁面釋放代碼就會嘗試將該頁面塊重新組合為更大的空閑頁面。通過這種方式,可用頁面的塊將盡可能多地使用內(nèi)存。
例如上圖,如果要釋放第 1 頁的頁面,則將其與已經(jīng)空閑的第 0 頁頁面框架組合在一起,并作為大小為 2頁的空閑塊排隊(duì)到 free_area 的元素 1 中
內(nèi)存映射
內(nèi)核有兩種類型的內(nèi)存映射:共享型(shared)和私有型(private)。私有型是當(dāng)進(jìn)程為了只讀文件,而不寫文件時使用,這時,私有映射更加高效。但是,任何對私有映射頁的寫操作都會導(dǎo)致內(nèi)核停止映射該文件中的頁。所以,寫操作既不會改變磁盤上的文件,對訪問該文件的其它進(jìn)程也是不可見的。
按需分頁
一旦可執(zhí)行映像被內(nèi)存映射到虛擬內(nèi)存后,它就可以被執(zhí)行了。因?yàn)橹粚⒂诚竦拈_頭部分物理的拉入到內(nèi)存中,因此它將很快訪問物理內(nèi)存尚未存在的虛擬內(nèi)存區(qū)域。當(dāng)進(jìn)程訪問沒有有效頁表的虛擬地址時,操作系統(tǒng)會報(bào)告這項(xiàng)錯誤。
頁面錯誤描述頁面出錯的虛擬地址和引起的內(nèi)存訪問(RAM)類型。
Linux 必須找到代表發(fā)生頁面錯誤的內(nèi)存區(qū)域的 vm_area_struct 結(jié)構(gòu)。由于搜索 vm_area_struct 數(shù)據(jù)結(jié)構(gòu)對于有效處理頁面錯誤至關(guān)重要,因此它們以AVL(Adelson-Velskii和Landis)樹結(jié)構(gòu)鏈接在一起。如果引起故障的虛擬地址沒有vm_area_struct結(jié)構(gòu),則此進(jìn)程已經(jīng)訪問了非法地址,Linux 會向進(jìn)程發(fā)出SIGSEGV信號,如果進(jìn)程沒有用于該信號的處理程序,那么進(jìn)程將會終止。
然后,Linux 會針對此虛擬內(nèi)存區(qū)域所允許的訪問類型,檢查發(fā)生的頁面錯誤類型。如果該進(jìn)程以非法方式訪問內(nèi)存,例如寫入僅允許讀的區(qū)域,則還會發(fā)出內(nèi)存訪問錯誤信號。
現(xiàn)在,Linux 已確定頁面錯誤是合法的,因此必須對其進(jìn)行處理。
文件系統(tǒng)
在 Linux 中,最直觀、最可見的部分就是文件系統(tǒng)(file system)。下面我們就來一起探討一下關(guān)于 Linux 中國的文件系統(tǒng),系統(tǒng)調(diào)用以及文件系統(tǒng)實(shí)現(xiàn)背后的原理和思想。這些思想中有一些來源于 MULTICS,現(xiàn)在已經(jīng)被 Windows 等其他操作系統(tǒng)使用。Linux 的設(shè)計(jì)理念就是小的就是好的(Small is Beautiful)。雖然 Linux 只是使用了最簡單的機(jī)制和少量的系統(tǒng)調(diào)用,但是 Linux 卻提供了強(qiáng)大而優(yōu)雅的文件系統(tǒng)。
Linux 文件系統(tǒng)基本概念
Linux 在最初的設(shè)計(jì)是 MINIX1 文件系統(tǒng),它只支持 14 字節(jié)的文件名,它的最大文件只支持到 64 MB。在 MINIX 1 之后的文件系統(tǒng)是 ext 文件系統(tǒng)。ext 系統(tǒng)相較于 MINIX 1 來說,在支持字節(jié)大小和文件大小上均有很大提升,但是 ext 的速度仍沒有 MINIX 1 快,于是,ext 2 被開發(fā)出來,它能夠支持長文件名和大文件,而且具有比 MINIX 1 更好的性能。這使他成為 Linux 的主要文件系統(tǒng)。只不過 Linux 會使用VFS曾支持多種文件系統(tǒng)。在 Linux 鏈接時,用戶可以動態(tài)的將不同的文件系統(tǒng)掛載倒 VFS 上。
Linux 中的文件是一個任意長度的字節(jié)序列,Linux 中的文件可以包含任意信息,比如 ASCII 碼、二進(jìn)制文件和其他類型的文件是不加區(qū)分的。
為了方便起見,文件可以被組織在一個目錄中,目錄存儲成文件的形式在很大程度上可以作為文件處理。目錄可以有子目錄,這樣形成有層次的文件系統(tǒng),Linux 系統(tǒng)下面的根目錄是/,它通常包含了多個子目錄。字符/還用于對目錄名進(jìn)行區(qū)分,例如「/usr/cxuan」表示的就是根目錄下面的 usr 目錄,其中有一個叫做 cxuan 的子目錄。
下面我們介紹一下 Linux 系統(tǒng)根目錄下面的目錄名
/bin,它是重要的二進(jìn)制應(yīng)用程序,包含二進(jìn)制文件,系統(tǒng)的所有用戶使用的命令都在這里
/boot,啟動包含引導(dǎo)加載程序的相關(guān)文件
/dev,包含設(shè)備文件,終端文件,USB 或者連接到系統(tǒng)的任何設(shè)備
/etc,配置文件,啟動腳本等,包含所有程序所需要的配置文件,也包含了啟動/停止單個應(yīng)用程序的啟動和關(guān)閉 shell 腳本
/home,本地主要路徑,所有用戶用 home 目錄存儲個人信息
/lib,系統(tǒng)庫文件,包含支持位于 /bin 和 /sbin 下的二進(jìn)制庫文件
/lost+found,在根目錄下提供一個遺失+查找系統(tǒng),必須在 root 用戶下才能查看當(dāng)前目錄下的內(nèi)容
/media,掛載可移動介質(zhì)
/mnt,掛載文件系統(tǒng)
/opt,提供一個可選的應(yīng)用程序安裝目錄
/proc,特殊的動態(tài)目錄,用于維護(hù)系統(tǒng)信息和狀態(tài),包括當(dāng)前運(yùn)行中進(jìn)程信息
/root,root 用戶的主要目錄文件夾
/sbin,重要的二進(jìn)制系統(tǒng)文件
/tmp, 系統(tǒng)和用戶創(chuàng)建的臨時文件,系統(tǒng)重啟時,這個目錄下的文件都會被刪除
/usr,包含絕大多數(shù)用戶都能訪問的應(yīng)用程序和文件
/var,經(jīng)常變化的文件,諸如日志文件或數(shù)據(jù)庫等
在 Linux 中,有兩種路徑,一種是絕對路徑(absolute path),絕對路徑告訴你從根目錄下查找文件,絕對路徑的缺點(diǎn)是太長而且不太方便。還有一種是相對路徑(relative path),相對路徑所在的目錄也叫做工作目錄(working directory)。
如果/usr/local/books是工作目錄,那么 shell 命令
cpbooksbooks-replica
就表示的是相對路徑,而
cp/usr/local/books/books/usr/local/books/books-replica
則表示的是絕對路徑。
在 Linux 中經(jīng)常出現(xiàn)一個用戶使用另一個用戶的文件或者使用文件樹結(jié)構(gòu)中的文件。兩個用戶共享同一個文件,這個文件位于某個用戶的目錄結(jié)構(gòu)中,另一個用戶需要使用這個文件時,必須通過絕對路徑才能引用到他。如果絕對路徑很長,那么每次輸入起來會變的非常麻煩,所以 Linux 提供了一種鏈接(link)機(jī)制。
舉個例子,下面是一個使用鏈接之前的圖
以上所示,比如有兩個工作賬戶 jianshe 和 cxuan,jianshe 想要使用 cxuan 賬戶下的 A 目錄,那么它可能會輸入/usr/cxuan/A,這是一種未使用鏈接之后的圖。
使用鏈接后的示意如下
現(xiàn)在,jianshe 可以創(chuàng)建一個鏈接來使用 cxuan 下面的目錄了?!?/p>
當(dāng)一個目錄被創(chuàng)建出來后,有兩個目錄項(xiàng)也同時被創(chuàng)建出來,它們就是.和..,前者代表工作目錄自身,后者代表該目錄的父目錄,也就是該目錄所在的目錄。這樣一來,在 /usr/jianshe 中訪問 cxuan 中的目錄就是../cxuan/xxx
Linux 文件系統(tǒng)不區(qū)分磁盤的,這是什么意思呢?一般來說,一個磁盤中的文件系統(tǒng)相互之間保持獨(dú)立,如果一個文件系統(tǒng)目錄想要訪問另一個磁盤中的文件系統(tǒng),在 Windows 中你可以像下面這樣。
兩個文件系統(tǒng)分別在不同的磁盤中,彼此保持獨(dú)立。
而在 Linux 中,是支持掛載的,它允許一個磁盤掛在到另外一個磁盤上,那么上面的關(guān)系會變成下面這樣
掛在之后,兩個文件系統(tǒng)就不再需要關(guān)心文件系統(tǒng)在哪個磁盤上了,兩個文件系統(tǒng)彼此可見。
Linux 文件系統(tǒng)的另外一個特性是支持加鎖(locking)。在一些應(yīng)用中會出現(xiàn)兩個或者更多的進(jìn)程同時使用同一個文件的情況,這樣很可能會導(dǎo)致競爭條件(race condition)。一種解決方法是對其進(jìn)行加不同粒度的鎖,就是為了防止某一個進(jìn)程只修改某一行記錄從而導(dǎo)致整個文件都不能使用的情況。
POSIX 提供了一種靈活的、不同粒度級別的鎖機(jī)制,允許一個進(jìn)程使用一個不可分割的操作對一個字節(jié)或者整個文件進(jìn)行加鎖。加鎖機(jī)制要求嘗試加鎖的進(jìn)程指定其「要加鎖的文件,開始位置以及要加鎖的字節(jié)」
Linux 系統(tǒng)提供了兩種鎖:「共享鎖和互斥鎖」。如果文件的一部分已經(jīng)加上了共享鎖,那么再加排他鎖是不會成功的;如果文件系統(tǒng)的一部分已經(jīng)被加了互斥鎖,那么在互斥鎖解除之前的任何加鎖都不會成功。為了成功加鎖、請求加鎖的部分的所有字節(jié)都必須是可用的。
在加鎖階段,進(jìn)程需要設(shè)計(jì)好加鎖失敗后的情況,也就是判斷加鎖失敗后是否選擇阻塞,如果選擇阻塞式,那么當(dāng)已經(jīng)加鎖的進(jìn)程中的鎖被刪除時,這個進(jìn)程會解除阻塞并替換鎖。如果進(jìn)程選擇非阻塞式的,那么就不會替換這個鎖,會立刻從系統(tǒng)調(diào)用中返回,標(biāo)記狀態(tài)碼表示是否加鎖成功,然后進(jìn)程會選擇下一個時間再次嘗試。
加鎖區(qū)域是可以重疊的。下面我們演示了三種不同條件的加鎖區(qū)域。
如上圖所示,A 的共享鎖在第四字節(jié)到第八字節(jié)進(jìn)行加鎖
如上圖所示,進(jìn)程在 A 和 B 上同時加了共享鎖,其中 6 - 8 字節(jié)是重疊鎖
如上圖所示,進(jìn)程 A 和 B 和 C 同時加了共享鎖,那么第六字節(jié)和第七字節(jié)是共享鎖。
如果此時一個進(jìn)程嘗試在第 6 個字節(jié)處加鎖,此時會設(shè)置失敗并阻塞,由于該區(qū)域被 A B C 同時加鎖,那么只有等到 A B C 都釋放鎖后,進(jìn)程才能加鎖成功。
Linux 文件系統(tǒng)調(diào)用
許多系統(tǒng)調(diào)用都會和文件與文件系統(tǒng)有關(guān)。我們首先先看一下對單個文件的系統(tǒng)調(diào)用,然后再來看一下對整個目錄和文件的系統(tǒng)調(diào)用。
為了創(chuàng)建一個新的文件,會使用到creat方法,注意沒有e。
?這里說一個小插曲,曾經(jīng)有人問 UNIX 創(chuàng)始人 Ken Thompson,如果有機(jī)會重新寫 UNIX ,你會怎么辦,他回答自己要把 creat 改成 create ,哈哈哈哈。?
這個系統(tǒng)調(diào)用的兩個參數(shù)是文件名和保護(hù)模式
fd=creat("aaa",mode);
這段命令會創(chuàng)建一個名為 aaa 的文件,并根據(jù) mode 設(shè)置文件的保護(hù)位。這些位決定了哪個用戶可能訪問文件、如何訪問。
creat 系統(tǒng)調(diào)用不僅僅創(chuàng)建了一個名為 aaa 的文件,還會打開這個文件。為了允許后續(xù)的系統(tǒng)調(diào)用訪問這個文件,這個 creat 系統(tǒng)調(diào)用會返回一個非負(fù)整數(shù), 這個就叫做文件描述符(file descriptor),也就是上面的 fd。
如果在已經(jīng)存在的文件上調(diào)用了 creat 系統(tǒng)調(diào)用,那么該文件中的內(nèi)容會被清除,從 0 開始。通過設(shè)置合適的參數(shù),open系統(tǒng)調(diào)用也能夠創(chuàng)建文件。
下面讓我們看一看主要的系統(tǒng)調(diào)用,如下表所示
為了對一個文件進(jìn)行讀寫的前提是先需要打開文件,必須使用 creat 或者 open 打開,參數(shù)是打開文件的方式,是只讀、可讀寫還是只寫。open 系統(tǒng)調(diào)用也會返回文件描述符。打開文件后,需要使用close系統(tǒng)調(diào)用進(jìn)行關(guān)閉。close 和 open 返回的 fd 總是未被使用的最小數(shù)量。
?
什么是文件描述符?文件描述符就是一個數(shù)字,這個數(shù)字標(biāo)示了計(jì)算機(jī)操作系統(tǒng)中打開的文件。它描述了數(shù)據(jù)資源,以及訪問資源的方式。
?
當(dāng)程序要求打開一個文件時,內(nèi)核會進(jìn)行如下操作
授予訪問權(quán)限
在全局文件表(global file table)中創(chuàng)建一個條目(entry)
向軟件提供條目的位置
文件描述符由唯一的非負(fù)整數(shù)組成,系統(tǒng)上每個打開的文件至少存在一個文件描述符。文件描述符最初在 Unix 中使用,并且被包括 Linux,macOS 和 BSD 在內(nèi)的現(xiàn)代操作系統(tǒng)所使用。
當(dāng)一個進(jìn)程成功訪問一個打開的文件時,內(nèi)核會返回一個文件描述符,這個文件描述符指向全局文件表的 entry 項(xiàng)。這個文件表項(xiàng)包含文件的 inode 信息,字節(jié)位移,訪問限制等。例如下圖所示
默認(rèn)情況下,前三個文件描述符為STDIN(標(biāo)準(zhǔn)輸入)、STDOUT(標(biāo)準(zhǔn)輸出)、STDERR(標(biāo)準(zhǔn)錯誤)。
標(biāo)準(zhǔn)輸入的文件描述符是 0 ,在終端中,默認(rèn)為用戶的鍵盤輸入
標(biāo)準(zhǔn)輸出的文件描述符是 1 ,在終端中,默認(rèn)為用戶的屏幕
與錯誤有關(guān)的默認(rèn)數(shù)據(jù)流是 2,在終端中,默認(rèn)為用戶的屏幕。
在簡單聊了一下文件描述符后,我們繼續(xù)回到文件系統(tǒng)調(diào)用的探討。
在文件系統(tǒng)調(diào)用中,開銷最大的就是 read 和 write 了。read 和 write 都有三個參數(shù)
文件描述符:告訴需要對哪一個打開文件進(jìn)行讀取和寫入
緩沖區(qū)地址:告訴數(shù)據(jù)需要從哪里讀取和寫入哪里
統(tǒng)計(jì):告訴需要傳輸多少字節(jié)
這就是所有的參數(shù)了,這個設(shè)計(jì)非常簡單輕巧。
雖然幾乎所有程序都按順序讀取和寫入文件,但是某些程序需要能夠隨機(jī)訪問文件的任何部分。與每個文件相關(guān)聯(lián)的是一個指針,該指針指示文件中的當(dāng)前位置。順序讀?。ɑ?qū)懭耄r,它通常指向要讀?。▽懭耄┑南乱粋€字節(jié)。如果指針在讀取 1024 個字節(jié)之前位于 4096 的位置,則它將在成功讀取系統(tǒng)調(diào)用后自動移至 5120 的位置。
Lseek系統(tǒng)調(diào)用會更改指針位置的值,以便后續(xù)對 read 或 write 的調(diào)用可以在文件中的任何位置開始,甚至可以超出文件末尾。
?lseek = Lseek ,段首大寫。?
lseek 避免叫做 seek 的原因就是 seek 已經(jīng)在之前 16 位的計(jì)算機(jī)上用于搜素功能了。
Lseek有三個參數(shù):第一個是文件的文件描述符,第二個是文件的位置;第三個告訴文件位置是相對于文件的開頭,當(dāng)前位置還是文件的結(jié)尾
lseek(intfildes,off_toffset,intwhence);
lseek 的返回值是更改文件指針后文件中的絕對位置。lseek 是唯一從來不會造成真正磁盤查找的系統(tǒng)調(diào)用,它只是更新當(dāng)前的文件位置,這個文件位置就是內(nèi)存中的數(shù)字。
對于每個文件,Linux 都會跟蹤文件模式(常規(guī),目錄,特殊文件),大小,最后修改時間以及其他信息。程序能夠通過stat系統(tǒng)調(diào)用看到這些信息。第一個參數(shù)就是文件名,第二個是指向要放置請求信息結(jié)構(gòu)的指針。這些結(jié)構(gòu)的屬性如下圖所示。
fstat調(diào)用和stat相同,只有一點(diǎn)區(qū)別,fstat 可以對打開文件進(jìn)行操作,而 stat 只能對路徑進(jìn)行操作。
pipe文件系統(tǒng)調(diào)用被用來創(chuàng)建 shell 管道。它會創(chuàng)建一系列的偽文件,來緩沖和管道組件之間的數(shù)據(jù),并且返回讀取或者寫入緩沖區(qū)的文件描述符。在管道中,像是如下操作
sort
sort 進(jìn)程將會輸出到文件描述符1,也就是標(biāo)準(zhǔn)輸出,寫入管道中,而 head 進(jìn)程將從管道中讀入。在這種方式中,sort 只是從文件描述符 0 中讀取并寫入到文件描述符 1 (管道)中,甚至不知道它們已經(jīng)被重定向了。如果沒有重定向的話,sort 會自動的從鍵盤讀入并輸出到屏幕中。
最后一個系統(tǒng)調(diào)用是fcntl,它用來鎖定和解鎖文件,應(yīng)用共享鎖和互斥鎖,或者是執(zhí)行一些文件相關(guān)的其他操作。
現(xiàn)在我們來關(guān)心一下和整體目錄和文件系統(tǒng)相關(guān)的系統(tǒng)調(diào)用,而不是把精力放在單個的文件上,下面列出了這些系統(tǒng)調(diào)用,我們一起來看一下。
可以使用 mkdir 和 rmdir 創(chuàng)建和刪除目錄。但是需要注意,只有目錄為空時才可以刪除。
創(chuàng)建一個指向已有文件的鏈接時會創(chuàng)建一個目錄項(xiàng)(directory entry)。系統(tǒng)調(diào)用 link 來創(chuàng)建鏈接,oldpath 代表已有的路徑,newpath 代表需要鏈接的路徑,使用unlink可以刪除目錄項(xiàng)。當(dāng)文件的最后一個鏈接被刪除時,這個文件會被自動刪除。
使用chdir系統(tǒng)調(diào)用可以改變工作目錄。
最后四個系統(tǒng)調(diào)用是用于讀取目錄的。和普通文件類似,他們可以被打開、關(guān)閉和讀取。每次調(diào)用readdir都會以固定的格式返回一個目錄項(xiàng)。用戶不能對目錄執(zhí)行寫操作,但是可以使用 creat 或者 link 在文件夾中創(chuàng)建一個目錄,或使用 unlink 刪除一個目錄。用戶不能在目錄中查找某個特定文件,但是可以使用rewindir作用于一個打開的目錄,使他能在此從頭開始讀取。
Linux 文件系統(tǒng)的實(shí)現(xiàn)
下面我們主要討論一下虛擬文件系統(tǒng)(Virtual File System)。VFS 對高層進(jìn)程和應(yīng)用程序隱藏了 Linux 支持的所有文件系統(tǒng)的區(qū)別,以及文件系統(tǒng)是存儲在本地設(shè)備,還是需要通過網(wǎng)絡(luò)訪問遠(yuǎn)程設(shè)備。設(shè)備和其他特殊文件和 VFS 層相關(guān)聯(lián)。接下來,我們就會探討一下第一個 Linux 廣泛傳播的文件系統(tǒng):ext2。隨后,我們就會探討ext4文件系統(tǒng)所做的改進(jìn)。各種各樣的其他文件系統(tǒng)也正在使用中。所有 Linux 系統(tǒng)都可以處理多個磁盤分區(qū),每個磁盤分區(qū)上都有不同的文件系統(tǒng)。
Linux 虛擬文件系統(tǒng)
為了能夠使應(yīng)用程序能夠在不同類型的本地或者遠(yuǎn)程設(shè)備上的文件系統(tǒng)進(jìn)行交互,因?yàn)樵?Linux 當(dāng)中文件系統(tǒng)千奇百種,比較常見的有 EXT3、EXT4,還有基于內(nèi)存的 ramfs、tmpfs 和基于網(wǎng)絡(luò)的 nfs,和基于用戶態(tài)的 fuse,當(dāng)然 fuse 應(yīng)該不能完全的文件系統(tǒng),只能算是一個能把文件系統(tǒng)實(shí)現(xiàn)放到用戶態(tài)的模塊,滿足了內(nèi)核文件系統(tǒng)的接口,他們都是文件系統(tǒng)的一種實(shí)現(xiàn)。對于這些文件系統(tǒng),Linux 做了一層抽象就是VFS虛擬文件系統(tǒng),
下表總結(jié)了 VFS 支持的四個主要的文件系統(tǒng)結(jié)構(gòu)。
超級塊(superblock)包含了有關(guān)文件系統(tǒng)布局的重要信息,超級塊如果遭到破壞那么就會導(dǎo)致整個文件系統(tǒng)不可讀。
i-node索引節(jié)點(diǎn),包含了每一個文件的描述符。
?
在 Linux 中,目錄和設(shè)備也表示為文件,因?yàn)樗鼈兙哂袑?yīng)的 i-node
?
超級塊和索引塊所在的文件系統(tǒng)都在磁盤上有對應(yīng)的結(jié)構(gòu)。
為了便于某些目錄操作和路徑遍歷,比如 /usr/local/cxuan,VFS 支持一個dentry數(shù)據(jù)結(jié)構(gòu),該數(shù)據(jù)結(jié)構(gòu)代表著目錄項(xiàng)。這個 dentry 數(shù)據(jù)結(jié)構(gòu)有很多東西(http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec7.html)這個數(shù)據(jù)結(jié)構(gòu)由文件系統(tǒng)動態(tài)創(chuàng)建。
目錄項(xiàng)被緩存在dentry_cache緩存中。例如,緩存條目會緩存 /usr 、 /usr/local 等條目。如果多個進(jìn)程通過硬連接訪問相同的文件,他們的文件對象將指向此緩存中的相同條目。
最后,文件數(shù)據(jù)結(jié)構(gòu)是代表著打開的文件,也代表著內(nèi)存表示,它根據(jù) open 系統(tǒng)調(diào)用創(chuàng)建。它支持「read、write、sendfile、lock」和其他在我們之前描述的系統(tǒng)調(diào)用中。
在 VFS 下實(shí)現(xiàn)的實(shí)際文件系統(tǒng)不需要在內(nèi)部使用完全相同的抽象和操作。但是,它們必須在語義上實(shí)現(xiàn)與 VFS 對象指定的文件系統(tǒng)操作相同的文件系統(tǒng)操作。四個 VFS 對象中每個對象的操作數(shù)據(jù)結(jié)構(gòu)的元素都是指向基礎(chǔ)文件系統(tǒng)中功能的指針。
Linux Ext2 文件系統(tǒng)
現(xiàn)在我們一起看一下 Linux 中最流行的一個磁盤文件系統(tǒng),那就是ext2。Linux 的第一個版本用于MINIX1文件系統(tǒng),它的文件名大小被限制為最大 64 MB。MINIX 1 文件系統(tǒng)被永遠(yuǎn)的被它的擴(kuò)展系統(tǒng) ext 取代,因?yàn)?ext 允許更長的文件名和文件大小。由于 ext 的性能低下,ext 被其替代者 ext2 取代,ext2 目前仍在廣泛使用。
一個 ext2 Linux 磁盤分區(qū)包含了一個文件系統(tǒng),這個文件系統(tǒng)的布局如下所示
Boot 塊也就是第 0 塊不是讓 Linux 使用的,而是用來加載和引導(dǎo)計(jì)算機(jī)啟動代碼的。在塊 0 之后,磁盤分區(qū)被分成多個組,這些組與磁盤柱面邊界所處的位置無關(guān)。
第一個塊是超級塊(superblock)。它包含有關(guān)文件系統(tǒng)布局的信息,包括 i-node、磁盤塊數(shù)量和以及空閑磁盤塊列表的開始。下一個是組描述符(group descriptor),其中包含有關(guān)位圖的位置,組中空閑塊和 i-node 的數(shù)量以及組中的目錄數(shù)量的信息。這些信息很重要,因?yàn)?ext2 會在磁盤上均勻分布目錄。
圖中的兩個位圖用來記錄空閑塊和空閑 i-node,這是從 MINIX 1文件系統(tǒng)繼承的選擇,大多數(shù) UNIX 文件系統(tǒng)使用位圖而不是空閑列表。每個位圖的大小是一個塊。如果一個塊的大小是 1 KB,那么就限制了塊組的數(shù)量是 8192 個塊和 8192 個 i-node。塊的大小是一個嚴(yán)格的限制,塊組的數(shù)量不固定,在 4KB 的塊中,塊組的數(shù)量增大四倍。
在超級塊之后分布的是i-node它們自己,i-node 取值范圍是 1 - 某些最大值。每個 i-node 是 128 字節(jié)的long,這些字節(jié)恰好能夠描述一個文件。i-node 包含了統(tǒng)計(jì)信息(包含了stat系統(tǒng)調(diào)用能獲得的所有者信息,實(shí)際上 stat 就是從 i-node 中讀取信息的),以及足夠的信息來查找保存文件數(shù)據(jù)的所有磁盤塊。
在 i-node 之后的是數(shù)據(jù)塊(data blocks)。所有的文件和目錄都保存在這。如果一個文件或者目錄包含多個塊,那么這些塊在磁盤中的分布不一定是連續(xù)的,也有可能不連續(xù)。事實(shí)上,大文件塊可能會被拆分成很多小塊散布在整個磁盤上。
對應(yīng)于目錄的 i-node 分散在整個磁盤組上。如果有足夠的空間,ext2 會把普通文件組織到與父目錄相同的塊組中,而把同一塊上的數(shù)據(jù)文件組織成初始i-node節(jié)點(diǎn)。位圖用來快速確定新文件系統(tǒng)數(shù)據(jù)的分配位置。在分配新的文件塊時,ext2 也會給該文件預(yù)分配許多額外的數(shù)據(jù)塊,這樣可以減少將來向文件寫入數(shù)據(jù)時產(chǎn)生的文件碎片。這種策略在整個磁盤上實(shí)現(xiàn)了文件系統(tǒng)的負(fù)載,后續(xù)還有對文件碎片的排列和整理,而且性能也比較好。
為了達(dá)到訪問的目的,需要首先使用 Linux 系統(tǒng)調(diào)用,例如open,這個系統(tǒng)調(diào)用會確定打開文件的路徑。路徑分為兩種,相對路徑和絕對路徑。如果使用相對路徑,那么就會從當(dāng)前目錄開始查找,否則就會從根目錄進(jìn)行查找。
目錄文件的文件名最高不能超過 255 個字符,它的分配如下圖所示
每一個目錄都由整數(shù)個磁盤塊組成,這樣目錄就可以整體的寫入磁盤。在一個目錄中,文件和子目錄的目錄項(xiàng)都是未經(jīng)排序的,并且一個挨著一個。目錄項(xiàng)不能跨越磁盤塊,所以通常在每個磁盤塊的尾部會有部分未使用的字節(jié)。
上圖中每個目錄項(xiàng)都由四個固定長度的屬性和一個長度可變的屬性組成。第一個屬性是i-node節(jié)點(diǎn)數(shù)量,文件 first 的 i-node 編號是 19 ,文件 second 的編號是 42,目錄 third 的 i-node 編號是 88。緊隨其后的是rec_len域,表明目錄項(xiàng)大小是多少字節(jié),名稱后面會有一些擴(kuò)展,當(dāng)名字以未知長度填充時,這個域被用來尋找下一個目錄項(xiàng),直至最后的未使用。這也是圖中箭頭的含義。緊隨其后的是類型域:F 表示的是文件,D 表示的是目錄,最后是固定長度的文件名,上面的文件名的長度依次是 5、6、5,最后以文件名結(jié)束。
rec_len 域是如何擴(kuò)展的呢?如下圖所示
我們可以看到,中間的second被移除了,所以將其所在的域變?yōu)榈谝粋€目錄項(xiàng)的填充。當(dāng)然,這個填充可以作為后續(xù)的目錄項(xiàng)。
由于目錄是按照線性的順序進(jìn)行查找的,因此可能需要很長時間才能在大文件末尾找到目錄項(xiàng)。因此,系統(tǒng)會為近期的訪問目錄維護(hù)一個緩存。這個緩存用文件名來查找,如果緩存命中,那么就會避免線程搜索這樣昂貴的開銷。組成路徑的每個部分都在目錄緩存中保存一個dentry對象,并且通過 i-node 找到后續(xù)的路徑元素的目錄項(xiàng),直到找到真正的文件 i - node。
比如說要使用絕對路徑來尋找一個文件,我們暫定這個路徑是/usr/local/file,那么需要經(jīng)過如下幾個步驟:
首先,系統(tǒng)會確定根目錄,它通常使用 2 號 i -node ,也就是索引 2 節(jié)點(diǎn),因?yàn)樗饕?jié)點(diǎn) 1 是 ext2 /3/4 文件系統(tǒng)上的壞塊索引節(jié)點(diǎn)。系統(tǒng)會將一項(xiàng)放在 dentry 緩存中,以應(yīng)對將來對根目錄的查找。
然后,在根目錄中查找字符串usr,得到 /usr 目錄的 i - node 節(jié)點(diǎn)號。/usr 的 i - node 同樣也進(jìn)入 dentry 緩存。然后節(jié)點(diǎn)被取出,并從中解析出磁盤塊,這樣就可以讀取 /usr 目錄并查找字符串local了。一旦找到這個目錄項(xiàng),目錄/usr/local的 i - node 節(jié)點(diǎn)就可以從中獲得。有了 /usr/local 的 i - node 節(jié)點(diǎn)號,就可以讀取 i - node 并確定目錄所在的磁盤塊。最后,從 /usr/local 目錄查找 file 并確定其 i - node 節(jié)點(diǎn)呢號。
如果文件存在,那么系統(tǒng)會提取 i - node 節(jié)點(diǎn)號并把它作為索引在 i - node 節(jié)點(diǎn)表中定位相應(yīng)的 i - node 節(jié)點(diǎn)并裝入內(nèi)存。i - node 被存放在 i - node 節(jié)點(diǎn)表(i-node table)中,節(jié)點(diǎn)表是一個內(nèi)核數(shù)據(jù)結(jié)構(gòu),它會持有當(dāng)前打開文件和目錄的 i - node 節(jié)點(diǎn)號。下面是一些 Linux 文件系統(tǒng)支持的 i - node 數(shù)據(jù)結(jié)構(gòu)。
現(xiàn)在我們來一起探討一下文件讀取過程,還記得read函數(shù)是如何調(diào)用的嗎?
n=read(fd,buffer,nbytes);
當(dāng)內(nèi)核接管后,它會從這三個參數(shù)以及內(nèi)部表與用戶有關(guān)的信息開始。內(nèi)部表的其中一項(xiàng)是文件描述符數(shù)組。文件描述符數(shù)組用文件描述符作為索引并為每一個打開文件保存一個表項(xiàng)。
文件是和 i - node 節(jié)點(diǎn)號相關(guān)的。那么如何通過一個文件描述符找到文件對應(yīng)的 i - node 節(jié)點(diǎn)呢?
這里使用的一種設(shè)計(jì)思想是在文件描述符表和 i - node 節(jié)點(diǎn)表之間插入一個新的表,叫做打開文件描述符(open-file-description table)。文件的讀寫位置會在打開文件描述符表中存在,如下圖所示
我們使用 shell 、P1 和 P2 來描述一下父進(jìn)程、子進(jìn)程、子進(jìn)程的關(guān)系。Shell 首先生成 P1,P1 的數(shù)據(jù)結(jié)構(gòu)就是 Shell 的一個副本,因此兩者都指向相同的打開文件描述符的表項(xiàng)。當(dāng) P1 運(yùn)行完成后,Shell 的文件描述符仍會指向 P1 文件位置的打開文件描述。然后 Shell 生成了 P2,新的子進(jìn)程自動繼承文件的讀寫位置,甚至 P2 和 Shell 都不知道文件具體的讀寫位置。
上面描述的是父進(jìn)程和子進(jìn)程這兩個相關(guān)進(jìn)程,如果是一個不相關(guān)進(jìn)程打開文件時,它將得到自己的打開文件描述符表項(xiàng),以及自己的文件讀寫位置,這是我們需要的。
?因此,打開文件描述符相當(dāng)于是給相關(guān)進(jìn)程提供同一個讀寫位置,而給不相關(guān)進(jìn)程提供各自私有的位置。?
i - node 包含三個間接塊的磁盤地址,它們每個指向磁盤塊的地址所能夠存儲的大小不一樣。
Linux Ext4 文件系統(tǒng)
為了防止由于系統(tǒng)崩潰和電源故障造成的數(shù)據(jù)丟失,ext2 系統(tǒng)必須在每個數(shù)據(jù)塊創(chuàng)建之后立即將其寫入到磁盤上,磁盤磁頭尋道操作導(dǎo)致的延遲是無法讓人忍受的。為了增強(qiáng)文件系統(tǒng)的健壯性,Linux 依靠日志文件系統(tǒng),ext3 是一個日志文件系統(tǒng),它在 ext2 文件系統(tǒng)的基礎(chǔ)之上做了改進(jìn),ext4 也是 ext3 的改進(jìn),ext4 也是一個日志文件系統(tǒng)。ext4 改變了 ext3 的塊尋址方案,從而支持更大的文件和更大的文件系統(tǒng)大小。下面我們就來描述一下 ext4 文件系統(tǒng)的特性。
具有記錄的文件系統(tǒng)最基本的功能就是記錄日志,這個日志記錄了按照順序描述所有文件系統(tǒng)的操作。通過順序?qū)懗鑫募到y(tǒng)數(shù)據(jù)或元數(shù)據(jù)的更改,操作不受磁盤訪問期間磁盤頭移動的開銷。最終,這個變更會寫入并提交到合適的磁盤位置上。如果這個變更在提交到磁盤前文件系統(tǒng)宕機(jī)了,那么在重啟期間,系統(tǒng)會檢測到文件系統(tǒng)未正確卸載,那么就會遍歷日志并應(yīng)用日志的記錄來對文件系統(tǒng)進(jìn)行更改。
Ext4 文件系統(tǒng)被設(shè)計(jì)用來高度匹配 ext2 和 ext3 文件系統(tǒng)的,盡管 ext4 文件系統(tǒng)在內(nèi)核數(shù)據(jù)結(jié)構(gòu)和磁盤布局上都做了變更。盡管如此,一個文件系統(tǒng)能夠從 ext2 文件系統(tǒng)上卸載后成功的掛載到 ext4 文件系統(tǒng)上,并提供合適的日志記錄。
日志是作為循環(huán)緩沖區(qū)管理的文件。日志可以存儲在與主文件系統(tǒng)相同或者不同的設(shè)備上。日志記錄的讀寫操作會由單獨(dú)的JBD(Journaling Block Device)來扮演。
JBD 中有三個主要的數(shù)據(jù)結(jié)構(gòu),分別是「log record(日志記錄)、原子操作和事務(wù)」。一個日志記錄描述了一個低級別的文件系統(tǒng)操作,這個操作通常導(dǎo)致塊內(nèi)的變化。因?yàn)橄袷莣rite這種系統(tǒng)調(diào)用會包含多個地方的改動 --- i - node 節(jié)點(diǎn),現(xiàn)有的文件塊,新的文件塊和空閑列表等。相關(guān)的日志記錄會以原子性的方式分組。ext4 會通知系統(tǒng)調(diào)用進(jìn)程的開始和結(jié)束,以此使 JBD 能夠確保原子操作的記錄都能被應(yīng)用,或者一個也不被應(yīng)用。最后,主要從效率方面考慮,JBD 會視原子操作的集合為事務(wù)。一個事務(wù)中的日志記錄是連續(xù)存儲的。只有在所有的變更一起應(yīng)用到磁盤后,日志記錄才能夠被丟棄。
由于為每個磁盤寫出日志的開銷會很大,所以 ext4 可以配置為保留所有磁盤更改的日志,或者僅僅保留與文件系統(tǒng)元數(shù)據(jù)相關(guān)的日志更改。僅僅記錄元數(shù)據(jù)可以減少系統(tǒng)開銷,提升性能,但不能保證不會損壞文件數(shù)據(jù)。其他的幾個日志系統(tǒng)維護(hù)著一系列元數(shù)據(jù)操作的日志,例如 SGI 的 XFS。
/proc 文件系統(tǒng)
另外一個 Linux 文件系統(tǒng)是/proc(process) 文件系統(tǒng)
?它的主要思想來源于貝爾實(shí)驗(yàn)室開發(fā)的第 8 版的 UNIX,后來被 BSD 和 System V 采用。?
然而,Linux 在一些方面上對這個想法進(jìn)行了擴(kuò)充。它的基本概念是為系統(tǒng)中的每個進(jìn)程在/proc中創(chuàng)建一個目錄。目錄的名字就是進(jìn)程 PID,以十進(jìn)制數(shù)進(jìn)行表示。例如,/proc/1024就是一個進(jìn)程號為 1024 的目錄。在該目錄下是進(jìn)程信息相關(guān)的文件,比如進(jìn)程的命令行、環(huán)境變量和信號掩碼等。事實(shí)上,這些文件在磁盤上并不存在磁盤中。當(dāng)需要這些信息的時候,系統(tǒng)會按需從進(jìn)程中讀取,并以標(biāo)準(zhǔn)格式返回給用戶。
許多 Linux 擴(kuò)展與/proc中的其他文件和目錄有關(guān)。它們包含各種各樣的關(guān)于 CPU、磁盤分區(qū)、設(shè)備、中斷向量、內(nèi)核計(jì)數(shù)器、文件系統(tǒng)、已加載模塊等信息。非特權(quán)用戶可以讀取很多這樣的信息,于是就可以通過一種安全的方式了解系統(tǒng)情況。
NFS 網(wǎng)絡(luò)文件系統(tǒng)
從一開始,網(wǎng)絡(luò)就在 Linux 中扮演了很重要的作用。下面我們會探討一下NFS(Network File System)網(wǎng)絡(luò)文件系統(tǒng),它在現(xiàn)代 Linux 操作系統(tǒng)的作用是將不同計(jì)算機(jī)上的不同文件系統(tǒng)鏈接成一個邏輯整體。
NFS 架構(gòu)
NFS 最基本的思想是允許任意選定的一些客戶端和服務(wù)器共享一個公共文件系統(tǒng)。在許多情況下,所有的客戶端和服務(wù)器都會在同一個LAN(Local Area Network)局域網(wǎng)內(nèi)共享,但是這并不是必須的。也可能是下面這樣的情況:如果客戶端和服務(wù)器距離較遠(yuǎn),那么它們也可以在廣域網(wǎng)上運(yùn)行??蛻舳丝梢允欠?wù)器,服務(wù)器可以是客戶端,但是為了簡單起見,我們說的客戶端就是消費(fèi)服務(wù),而服務(wù)器就是提供服務(wù)的角度來聊。
每一個 NFS 服務(wù)都會導(dǎo)出一個或者多個目錄供遠(yuǎn)程客戶端訪問。當(dāng)一個目錄可用時,它的所有子目錄也可用。因此,通常整個目錄樹都會作為一個整體導(dǎo)出。服務(wù)器導(dǎo)出的目錄列表會用一個文件來維護(hù),這個文件是/etc/exports,當(dāng)服務(wù)器啟動后,這些目錄可以自動的被導(dǎo)出??蛻舳送ㄟ^掛載這些導(dǎo)出的目錄來訪問它們。當(dāng)一個客戶端掛載了一個遠(yuǎn)程目錄,這個目錄就成為客戶端目錄層次的一部分,如下圖所示。
在這個示例中,一號客戶機(jī)掛載到服務(wù)器的 bin 目錄下,因此它現(xiàn)在可以使用 shell 訪問 /bin/cat 或者其他任何一個目錄。同樣,客戶機(jī) 1 也可以掛載到 二號服務(wù)器上從而訪問 /usr/local/projects/proj1 或者其他目錄。二號客戶機(jī)同樣可以掛載到二號服務(wù)器上,訪問路徑是 /mnt/projects/proj2。
從上面可以看到,由于不同的客戶端將文件掛載到各自目錄樹的不同位置,同一個文件在不同的客戶端有不同的訪問路徑和不同的名字。掛載點(diǎn)一般通常在客戶端本地,服務(wù)器不知道任何一個掛載點(diǎn)的存在。
NFS 協(xié)議
由于 NFS 的協(xié)議之一是支持異構(gòu)系統(tǒng),客戶端和服務(wù)器可能在不同的硬件上運(yùn)行不同的操作系統(tǒng),因此有必要在服務(wù)器和客戶端之間進(jìn)行接口定義。這樣才能讓任何寫一個新客戶端能夠和現(xiàn)有的服務(wù)器一起正常工作,反之亦然。
NFS 就通過定義兩個客戶端 - 服務(wù)器協(xié)議從而實(shí)現(xiàn)了這個目標(biāo)。協(xié)議就是客戶端發(fā)送給服務(wù)器的一連串的請求,以及服務(wù)器發(fā)送回客戶端的相應(yīng)答復(fù)。
第一個 NFS 協(xié)議是處理掛載??蛻舳丝梢韵蚍?wù)器發(fā)送路徑名并且請求服務(wù)器是否能夠?qū)⒎?wù)器的目錄掛載到自己目錄層次上。因?yàn)榉?wù)器不關(guān)心掛載到哪里,因此請求不會包含掛載地址。如果路徑名是合法的并且指定的目錄已經(jīng)被導(dǎo)出,那么服務(wù)器會將文件句柄返回給客戶端。
?文件句柄包含唯一標(biāo)識文件系統(tǒng)類型,磁盤,目錄的i節(jié)點(diǎn)號和安全性信息的字段。?
隨后調(diào)用讀取和寫入已安裝目錄或其任何子目錄中的文件,都將使用文件句柄。
當(dāng) Linux 啟動時會在多用戶之前運(yùn)行 shell 腳本 /etc/rc ??梢詫燧d遠(yuǎn)程文件系統(tǒng)的命令寫入該腳本中,這樣就可以在允許用戶登陸之前自動掛載必要的遠(yuǎn)程文件系統(tǒng)。大部分 Linux 版本是支持自動掛載的。這個特性會支持將遠(yuǎn)程目錄和本地目錄進(jìn)行關(guān)聯(lián)。
相對于手動掛載到 /etc/rc 目錄下,自動掛載具有以下優(yōu)勢
如果列出的 /etc/rc 目錄下出現(xiàn)了某種故障,那么客戶端將無法啟動,或者啟動會很困難、延遲或者伴隨一些出錯信息,如果客戶根本不需要這個服務(wù)器,那么手動做了這些工作就白費(fèi)了。
允許客戶端并行的嘗試一組服務(wù)器,可以實(shí)現(xiàn)一定程度的容錯率,并且性能也可以得到提高。
另一方面,我們默認(rèn)在自動掛載時所有可選的文件系統(tǒng)都是相同的。由于 NFS 不提供對文件或目錄復(fù)制的支持,用戶需要自己確保這些所有的文件系統(tǒng)都是相同的。因此,大部分的自動掛載都只應(yīng)用于二進(jìn)制文件和很少改動的只讀的文件系統(tǒng)。
第二個 NFS 協(xié)議是為文件和目錄的訪問而設(shè)計(jì)的??蛻舳四軌蛲ㄟ^向服務(wù)器發(fā)送消息來操作目錄和讀寫文件??蛻舳艘部梢栽L問文件屬性,比如文件模式、大小、上次修改時間。NFS 支持大多數(shù)的 Linux 系統(tǒng)調(diào)用,但是 open 和 close 系統(tǒng)調(diào)用卻不支持。
?不支持 open 和 close 并不是一種疏忽,而是一種刻意的設(shè)計(jì),完全沒有必要在讀一個文件之前對其進(jìn)行打開,也沒有必要在讀完時對其進(jìn)行關(guān)閉。?
NFS 使用了標(biāo)準(zhǔn)的 UNIX 保護(hù)機(jī)制,使用rwx位來標(biāo)示所有者(owner)、組(groups)、其他用戶。最初,每個請求消息都會攜帶調(diào)用者的 groupId 和 userId,NFS 會對其進(jìn)行驗(yàn)證。事實(shí)上,它會信任客戶端不會發(fā)生欺騙行為??梢允褂霉€密碼來創(chuàng)建一個安全密鑰,在每次請求和應(yīng)答中使用它驗(yàn)證客戶端和服務(wù)器。
NFS 實(shí)現(xiàn)
即使客戶端和服務(wù)器的代碼實(shí)現(xiàn)是獨(dú)立于 NFS 協(xié)議的,大部分的 Linux 系統(tǒng)會使用一個下圖的三層實(shí)現(xiàn),頂層是系統(tǒng)調(diào)用層,系統(tǒng)調(diào)用層能夠處理 open 、 read 、 close 這類的系統(tǒng)調(diào)用。在解析和參數(shù)檢查結(jié)束后調(diào)用第二層,虛擬文件系統(tǒng) (VFS)層。
VFS 層的任務(wù)是維護(hù)一個表,每個已經(jīng)打開的文件都在表中有一個表項(xiàng)。VFS 層為每一個打開的文件維護(hù)著一個虛擬i節(jié)點(diǎn),簡稱為 v - node。v 節(jié)點(diǎn)用來說明文件是本地文件還是遠(yuǎn)程文件。如果是遠(yuǎn)程文件的話,那么 v - node 會提供足夠的信息使客戶端能夠訪問它們。對于本地文件,會記錄其所在的文件系統(tǒng)和文件的 i-node ,因?yàn)楝F(xiàn)代操作系統(tǒng)能夠支持多文件系統(tǒng)。雖然 VFS 是為了支持 NFS 而設(shè)計(jì)的,但是現(xiàn)代操作系統(tǒng)都會使用 VFS,而不管有沒有 NFS。
Linux IO
我們之前了解過了 Linux 的進(jìn)程和線程、Linux 內(nèi)存管理,那么下面我們就來認(rèn)識一下 Linux 中的 I/O 管理。
Linux 系統(tǒng)和其他 UNIX 系統(tǒng)一樣,IO 管理比較直接和簡潔。所有 IO 設(shè)備都被當(dāng)作文件,通過在系統(tǒng)內(nèi)部使用相同的 read 和 write 一樣進(jìn)行讀寫。
Linux IO 基本概念
Linux 中也有磁盤、打印機(jī)、網(wǎng)絡(luò)等 I/O 設(shè)備,Linux 把這些設(shè)備當(dāng)作一種特殊文件整合到文件系統(tǒng)中,一般通常位于/dev目錄下??梢允褂门c普通文件相同的方式來對待這些特殊文件。
特殊文件一般分為兩種:
塊特殊文件是一個能存儲固定大小塊信息的設(shè)備,它支持「以固定大小的塊,扇區(qū)或群集讀取和(可選)寫入數(shù)據(jù)」。每個塊都有自己的物理地址。通常塊的大小在 512 - 65536 之間。所有傳輸?shù)男畔⒍紩赃B續(xù)的塊為單位。塊設(shè)備的基本特征是每個塊都較為對立,能夠獨(dú)立的進(jìn)行讀寫。常見的塊設(shè)備有「硬盤、藍(lán)光光盤、USB 盤」與字符設(shè)備相比,塊設(shè)備通常需要較少的引腳。
塊特殊文件的缺點(diǎn)基于給定固態(tài)存儲器的塊設(shè)備比基于相同類型的存儲器的字節(jié)尋址要慢一些,因?yàn)楸仨氃趬K的開頭開始讀取或?qū)懭搿K?,要讀取該塊的任何部分,必須尋找到該塊的開始,讀取整個塊,如果不使用該塊,則將其丟棄。要寫入塊的一部分,必須尋找到塊的開始,將整個塊讀入內(nèi)存,修改數(shù)據(jù),再次尋找到塊的開頭處,然后將整個塊寫回設(shè)備。
另一類 I/O 設(shè)備是字符特殊文件。字符設(shè)備以字符為單位發(fā)送或接收一個字符流,而不考慮任何塊結(jié)構(gòu)。字符設(shè)備是不可尋址的,也沒有任何尋道操作。常見的字符設(shè)備有「打印機(jī)、網(wǎng)絡(luò)設(shè)備、鼠標(biāo)、以及大多數(shù)與磁盤不同的設(shè)備」。
每個設(shè)備特殊文件都會和設(shè)備驅(qū)動相關(guān)聯(lián)。每個驅(qū)動程序都通過一個主設(shè)備號來標(biāo)識。如果一個驅(qū)動支持多個設(shè)備的話,此時會在主設(shè)備的后面新加一個次設(shè)備號來標(biāo)識。主設(shè)備號和次設(shè)備號共同確定了唯一的驅(qū)動設(shè)備。
我們知道,在計(jì)算機(jī)系統(tǒng)中,CPU 并不直接和設(shè)備打交道,它們中間有一個叫作設(shè)備控制器(Device Control Unit)的組件,例如硬盤有磁盤控制器、USB 有 USB 控制器、顯示器有視頻控制器等。這些控制器就像代理商一樣,它們知道如何應(yīng)對硬盤、鼠標(biāo)、鍵盤、顯示器的行為。
絕大多數(shù)字符特殊文件都不能隨機(jī)訪問,因?yàn)樗麄冃枰褂煤蛪K特殊文件不同的方式來控制。比如,你在鍵盤上輸入了一些字符,但是你發(fā)現(xiàn)輸錯了一個,這時有一些人喜歡使用backspace來刪除,有人喜歡用del來刪除。為了中斷正在運(yùn)行的設(shè)備,一些系統(tǒng)使用ctrl-u來結(jié)束,但是現(xiàn)在一般使用ctrl-c來結(jié)束。
網(wǎng)絡(luò)
I/O 的另外一個概念是網(wǎng)絡(luò), 也是由 UNIX 引入,網(wǎng)絡(luò)中一個很關(guān)鍵的概念就是套接字(socket)。套接字允許用戶連接到網(wǎng)絡(luò),正如郵筒允許用戶連接到郵政系統(tǒng),套接字的示意圖如下
套接字的位置如上圖所示,套接字可以動態(tài)創(chuàng)建和銷毀。成功創(chuàng)建一個套接字后,系統(tǒng)會返回一個文件描述符(file descriptor),在后面的創(chuàng)建鏈接、讀數(shù)據(jù)、寫數(shù)據(jù)、解除連接時都需要使用到這個文件描述符。每個套接字都支持一種特定類型的網(wǎng)絡(luò)類型,在創(chuàng)建時指定。一般最常用的幾種
可靠的面向連接的字節(jié)流
可靠的面向連接的數(shù)據(jù)包
不可靠的數(shù)據(jù)包傳輸
可靠的面向連接的字節(jié)流會使用管道在兩臺機(jī)器之間建立連接。能夠保證字節(jié)從一臺機(jī)器按照順序到達(dá)另一臺機(jī)器,系統(tǒng)能夠保證所有字節(jié)都能到達(dá)。
除了數(shù)據(jù)包之間的分界之外,第二種類型和第一種類型是類似的。如果發(fā)送了 3 次寫操作,那么使用第一種方式的接受者會直接接收到所有字節(jié);第二種方式的接受者會分 3 次接受所有字節(jié)。除此之外,用戶還可以使用第三種即不可靠的數(shù)據(jù)包來傳輸,使用這種傳輸方式的優(yōu)點(diǎn)在于高性能,有的時候它比可靠性更加重要,比如在流媒體中,性能就尤其重要。
以上涉及兩種形式的傳輸協(xié)議,即TCP和UDP,TCP 是傳輸控制協(xié)議,它能夠傳輸可靠的字節(jié)流。UDP是用戶數(shù)據(jù)報(bào)協(xié)議,它只能夠傳輸不可靠的字節(jié)流。它們都屬于 TCP/IP 協(xié)議簇中的協(xié)議,下面是網(wǎng)絡(luò)協(xié)議分層
可以看到,TCP 、UDP 都位于網(wǎng)絡(luò)層上,可見它們都把 IP 協(xié)議 即互聯(lián)網(wǎng)協(xié)議作為基礎(chǔ)。
一旦套接字在源計(jì)算機(jī)和目的計(jì)算機(jī)建立成功,那么兩個計(jì)算機(jī)之間就可以建立一個鏈接。通信一方在本地套接字上使用listen系統(tǒng)調(diào)用,它就會創(chuàng)建一個緩沖區(qū),然后阻塞直到數(shù)據(jù)到來。另一方使用connect系統(tǒng)調(diào)用,如果另一方接受 connect 系統(tǒng)調(diào)用后,則系統(tǒng)會在兩個套接字之間建立連接。
socket 連接建立成功后就像是一個管道,一個進(jìn)程可以使用本地套接字的文件描述符從中讀寫數(shù)據(jù),當(dāng)連接不再需要的時候使用close系統(tǒng)調(diào)用來關(guān)閉。
Linux I/O 系統(tǒng)調(diào)用
Linux 系統(tǒng)中的每個 I/O 設(shè)備都有一個特殊文件(special file)與之關(guān)聯(lián),什么是特殊文件呢?
?在操作系統(tǒng)中,特殊文件是一種在文件系統(tǒng)中與硬件設(shè)備相關(guān)聯(lián)的文件。特殊文件也被稱為設(shè)備文件(device file)。特殊文件的目的是將設(shè)備作為文件系統(tǒng)中的文件進(jìn)行公開。特殊文件為硬件設(shè)備提供了借口,用于文件 I/O 的工具可以進(jìn)行訪問。因?yàn)樵O(shè)備有兩種類型,同樣特殊文件也有兩種,即字符特殊文件和塊特殊文件?
對于大部分 I/O 操作來說,只用合適的文件就可以完成,并不需要特殊的系統(tǒng)調(diào)用。然后,有時需要一些設(shè)備專用的處理。在 POSIX 之前,大多數(shù) UNIX 系統(tǒng)會有一個叫做ioctl的系統(tǒng)調(diào)用,它用于執(zhí)行大量的系統(tǒng)調(diào)用。隨著時間的發(fā)展,POSIX 對其進(jìn)行了整理,把 ioctl 的功能劃分為面向終端設(shè)備的獨(dú)立功能調(diào)用,現(xiàn)在已經(jīng)變成獨(dú)立的系統(tǒng)調(diào)用了。
下面是幾個管理終端的系統(tǒng)調(diào)用
Linux IO 實(shí)現(xiàn)
Linux 中的 IO 是通過一系列設(shè)備驅(qū)動實(shí)現(xiàn)的,每個設(shè)備類型對應(yīng)一個設(shè)備驅(qū)動。設(shè)備驅(qū)動為操作系統(tǒng)和硬件分別預(yù)留接口,通過設(shè)備驅(qū)動來屏蔽操作系統(tǒng)和硬件的差異。
當(dāng)用戶訪問一個特殊的文件時,由文件系統(tǒng)提供此特殊文件的主設(shè)備號和次設(shè)備號,并判斷它是一個塊特殊文件還是字符特殊文件。主設(shè)備號用于標(biāo)識字符設(shè)備還是塊設(shè)備,次設(shè)備號用于參數(shù)傳遞。
每個驅(qū)動程序都有兩部分:這兩部分都是屬于 Linux 內(nèi)核,也都運(yùn)行在內(nèi)核態(tài)下。上半部分運(yùn)行在調(diào)用者上下文并且與 Linux 其他部分交互。下半部分運(yùn)行在內(nèi)核上下文并且與設(shè)備進(jìn)行交互。驅(qū)動程序可以調(diào)用內(nèi)存分配、定時器管理、DMA 控制等內(nèi)核過程??杀徽{(diào)用的內(nèi)核功能都位于驅(qū)動程序 - 內(nèi)核接口的文檔中。
I/O 實(shí)現(xiàn)指的就是對字符設(shè)備和塊設(shè)備的實(shí)現(xiàn)
塊設(shè)備實(shí)現(xiàn)
系統(tǒng)中處理塊特殊文件 I/O 部分的目標(biāo)是為了使傳輸次數(shù)盡可能的小。為了實(shí)現(xiàn)這個目標(biāo),Linux 系統(tǒng)在磁盤驅(qū)動程序和文件系統(tǒng)之間設(shè)置了一個高速緩存(cache),如下圖所示
在 Linux 內(nèi)核 2.2 之前,Linux 系統(tǒng)維護(hù)著兩個緩存:頁面緩存(page cache)和緩沖區(qū)緩存(buffer cache),因此,存儲在一個磁盤塊中的文件可能會在兩個緩存中。2.2 版本以后 Linux 內(nèi)核只有一個統(tǒng)一的緩存一個通用數(shù)據(jù)塊層(generic block layer)把這些融合在一起,實(shí)現(xiàn)了磁盤、數(shù)據(jù)塊、緩沖區(qū)和數(shù)據(jù)頁之間必要的轉(zhuǎn)換。那么什么是通用數(shù)據(jù)塊層?
?通用數(shù)據(jù)塊層是一個內(nèi)核的組成部分,用于處理對系統(tǒng)中所有塊設(shè)備的請求。通用數(shù)據(jù)塊主要有以下幾個功能
將數(shù)據(jù)緩沖區(qū)放在內(nèi)存高位處,當(dāng) CPU 訪問數(shù)據(jù)時,頁面才會映射到內(nèi)核線性地址中,并且此后取消映射
實(shí)現(xiàn)零拷貝機(jī)制,磁盤數(shù)據(jù)可以直接放入用戶模式的地址空間,而無需先復(fù)制到內(nèi)核內(nèi)存中
管理磁盤卷,會把不同塊設(shè)備上的多個磁盤分區(qū)視為一個分區(qū)。
利用最新的磁盤控制器的高級功能,例如 DMA 等。?
cache 是提升性能的利器,不管以什么樣的目的需要一個數(shù)據(jù)塊,都會先從 cache 中查找,如果找到直接返回,避免一次磁盤訪問,能夠極大的提升系統(tǒng)性能。
如果頁面 cache 中沒有這個塊,操作系統(tǒng)就會把頁面從磁盤中調(diào)入內(nèi)存,然后讀入 cache 進(jìn)行緩存。
cache 除了支持讀操作外,也支持寫操作,一個程序要寫回一個塊,首先把它寫到 cache 中,而不是直接寫入到磁盤中,等到磁盤中緩存達(dá)到一定數(shù)量值時再被寫入到 cache 中。
Linux 系統(tǒng)中使用IO 調(diào)度器來保證減少磁頭的反復(fù)移動從而減少損失。I/O 調(diào)度器的作用是對塊設(shè)備的讀寫操作進(jìn)行排序,對讀寫請求進(jìn)行合并。Linux 有許多調(diào)度器的變體,從而滿足不同的工作需要。最基本的 Linux 調(diào)度器是基于傳統(tǒng)的Linux 電梯調(diào)度器(Linux elevator scheduler)。Linux 電梯調(diào)度器的主要工作流程就是按照磁盤扇區(qū)的地址排序并存儲在一個雙向鏈表中。新的請求將會以鏈表的形式插入。這種方法可以有效的防止磁頭重復(fù)移動。因?yàn)殡娞菡{(diào)度器會容易產(chǎn)生饑餓現(xiàn)象。因此,Linux 在原基礎(chǔ)上進(jìn)行了修改,維護(hù)了兩個鏈表,在最后日期(deadline)內(nèi)維護(hù)了排序后的讀寫操作。默認(rèn)的讀操作耗時 0.5s,默認(rèn)寫操作耗時 5s。如果在最后期限內(nèi)等待時間最長的鏈表沒有獲得服務(wù),那么它將優(yōu)先獲得服務(wù)。
字符設(shè)備實(shí)現(xiàn)
和字符設(shè)備的交互是比較簡單的。由于字符設(shè)備會產(chǎn)生并使用字符流、字節(jié)數(shù)據(jù),因此對隨機(jī)訪問的支持意義不大。一個例外是使用行規(guī)則(line disciplines)。一個行規(guī)可以和終端設(shè)備相關(guān)聯(lián),使用tty_struct結(jié)構(gòu)來表示,它表示與終端設(shè)備交換數(shù)據(jù)的解釋器,當(dāng)然這也屬于內(nèi)核的一部分。例如:行規(guī)可以對行進(jìn)行編輯,映射回車為換行等一系列其他操作。
?什么是行規(guī)則?
行規(guī)是某些類 UNIX 系統(tǒng)中的一層,終端子系統(tǒng)通常由三層組成:上層提供字符設(shè)備接口,下層硬件驅(qū)動程序與硬件或偽終端進(jìn)行交互,中層規(guī)則用于實(shí)現(xiàn)終端設(shè)備共有的行為。?
網(wǎng)絡(luò)設(shè)備實(shí)現(xiàn)
網(wǎng)絡(luò)設(shè)備的交互是不一樣的,雖然網(wǎng)絡(luò)設(shè)備(network devices)也會產(chǎn)生字符流,因?yàn)樗鼈兊漠惒?asynchronous)特性是他們不易與其他字符設(shè)備在同一接口下集成。網(wǎng)絡(luò)設(shè)備驅(qū)動程序會產(chǎn)生很多數(shù)據(jù)包,經(jīng)由網(wǎng)絡(luò)協(xié)議到達(dá)用戶應(yīng)用程序中。
Linux 中的模塊
UNIX 設(shè)備驅(qū)動程序是被靜態(tài)加載到內(nèi)核中的。因此,只要系統(tǒng)啟動后,設(shè)備驅(qū)動程序都會被加載到內(nèi)存中。隨著個人電腦 Linux 的出現(xiàn),這種靜態(tài)鏈接完成后會使用一段時間的模式被打破。相對于小型機(jī)上的 I/O 設(shè)備,PC 上可用的 I/O 設(shè)備有了數(shù)量級的增長。絕大多數(shù)用戶沒有能力去添加一個新的應(yīng)用程序、更新設(shè)備驅(qū)動、重新連接內(nèi)核,然后進(jìn)行安裝。
Linux 為了解決這個問題,引入了可加載(loadable module)機(jī)制。可加載是在系統(tǒng)運(yùn)行時添加到內(nèi)核中的代碼塊。
當(dāng)一個模塊被加載到內(nèi)核時,會發(fā)生下面幾件事情:第一,在加載的過程中,模塊會被動態(tài)的重新部署。第二,系統(tǒng)會檢查程序程序所需的資源是否可用。如果可用,則把這些資源標(biāo)記為正在使用。第三步,設(shè)置所需的中斷向量。第四,更新驅(qū)動轉(zhuǎn)換表使其能夠處理新的主設(shè)備類型。最后再來運(yùn)行設(shè)備驅(qū)動程序。
在完成上述工作后,驅(qū)動程序就會安裝完成,其他現(xiàn)代 UNIX 系統(tǒng)也支持可加載機(jī)制。
Linux 安全
Linux 作為 MINIX 和 UNIX 的衍生操作系統(tǒng),從一開始就是一個多用戶系統(tǒng)。這意味著 Linux 從早期開始就建立了安全和信息訪問控制機(jī)制。下面我們主要探討的就是 Linux 安全性的一些內(nèi)容
Linux 安全基本概念
一個 Linux 系統(tǒng)的用戶群里由一系列注冊用戶組成,他們每一個都有一個唯一的 UID (User ID)。一個 UID 是一個位于 0 到 65535 之間的整數(shù)。文件(進(jìn)程或者是其他資源)都標(biāo)記了它的所有者的 UID。默認(rèn)情況下,文件的所有者是創(chuàng)建文件的人,文件的所有者是創(chuàng)建文件的用戶。
用戶可以被分成許多組,每個組都會由一個 16 位的整數(shù)標(biāo)記,這個組叫做GID(組 ID)。給用戶分組是手動完成的,它由系統(tǒng)管理員執(zhí)行,分組就是在數(shù)據(jù)庫中添加一條記錄指明哪個用戶屬于哪個組。一個用戶可以屬于不同組。
Linux 中的基本安全機(jī)制比較容易理解,每個進(jìn)程都會記錄它所有者的 UID 和 GID。當(dāng)文件創(chuàng)建后,它會獲取創(chuàng)建進(jìn)程的 UID 和 GID。當(dāng)一個文件被創(chuàng)建時,它的 UID 和 GID 就會被標(biāo)記為進(jìn)程的 UID 和 GID。這個文件同時會獲取由該進(jìn)程決定的一些權(quán)限。這些權(quán)限會指定所有者、所有者所在組的其他用戶及其他用戶對文件具有什么樣的訪問權(quán)限。對于這三類用戶而言,潛在的訪問權(quán)限是「讀、寫和執(zhí)行」,分別由 r、w 和 x 標(biāo)記。當(dāng)然,執(zhí)行文件的權(quán)限僅當(dāng)文件時可逆二進(jìn)制程序時才有意義。試圖執(zhí)行一個擁有執(zhí)行權(quán)限的非可執(zhí)行文件,系統(tǒng)會報(bào)錯。
「Linux 用戶分為三種」
root(超級管理員),它的 UID 為 0,這個用戶有極大的權(quán)限,可以直接無視很多的限制 ,包括讀寫執(zhí)行的權(quán)限。
系統(tǒng)用戶,UID 為 1~499。
普通用戶,UID 范圍一般是 500~65534。這類用戶的權(quán)限會受到基本權(quán)限的限制,也會受到來自管理員的限制。不過要注意 nobody 這個特殊的帳號,UID 為 65534,這個用戶的權(quán)限會進(jìn)一步的受到限制,一般用于實(shí)現(xiàn)來賓帳號。
Linux 中的每類用戶由 3 個比特為來標(biāo)記,所以 9 個比特位就能夠表示所有的權(quán)限。
下面來看一下一些基本的用戶和權(quán)限例子
我們上面提到,UID 為 0 的是一個特殊用戶,稱為超級用戶(或者根用戶)。超級用戶能夠讀和寫系統(tǒng)中的任何文件,不管這個文件由誰所有,也不管這個文件的保護(hù)模式如何。UID 為 0 的進(jìn)程還具有少數(shù)調(diào)用受保護(hù)系統(tǒng)調(diào)用的權(quán)限,而普通用戶是不可能有這些功能的。通常情況下,只有系統(tǒng)管理員知道超級用戶的密碼。
在 Linux 系統(tǒng)下,目錄也是一種文件,并且具有和普通文件一樣的保護(hù)模式。不同的是,目錄的 x 比特位表示查找權(quán)限而不是執(zhí)行權(quán)限。因此,如果一個目錄的保護(hù)模式是rwxr-xr-x,那么它允許所有者讀、寫和查找目錄,而其他人只可以讀和查找,而不允許從中添加或者刪除目錄中的文件。
與 I/O 有關(guān)的特殊文件擁有和普通文件一樣的保護(hù)位。這種機(jī)制可以用來限制對 I/O 設(shè)備的訪問權(quán)限。舉個例子,打印機(jī)是特殊文件,它的目錄是/dev/lp,它可以被根用戶或者一個叫守護(hù)進(jìn)程的特殊用戶擁有,具有保護(hù)模式 rw-------,從而阻止其他所有人對打印機(jī)的訪問。畢竟每個人都使用打印機(jī)的話會發(fā)生混亂。
當(dāng)然,如果 /dev/lp 的保護(hù)模式是 rw-------,那就意味著其他任何人都不能使用打印機(jī)。
這個問題通過增加一個保護(hù)位SETUID到之前的 9 個比特位來解決。當(dāng)一個進(jìn)程的 SETUID 位打開,它的有效 UID將變成相應(yīng)可執(zhí)行文件的所有者 UID,而不是當(dāng)前使用該進(jìn)程的用戶的 UID。將訪問打印機(jī)的程序設(shè)置為守護(hù)進(jìn)程所有,同時打開 SETUID 位,這樣任何用戶都可以執(zhí)行此程序,而且擁有守護(hù)進(jìn)程的權(quán)限。
除了 SETUID 之外,還有一個 SETGID 位,SETGID 的工作原理和 SETUID 類似。但是這個位一般很不常用。
Linux 安全相關(guān)的系統(tǒng)調(diào)用
Linux 中關(guān)于安全的系統(tǒng)調(diào)用不是很多,只有幾個,如下列表所示
我們在日常開發(fā)中用到最多的就是chmod了,沒想到我們?nèi)粘i_發(fā)過程中也能用到系統(tǒng)調(diào)用啊,chmod 之前我們一直認(rèn)為是改變權(quán)限,現(xiàn)在專業(yè)一點(diǎn)是改變文件的保護(hù)模式。它的具體函數(shù)如下
s=chmod("路徑名","值");
例如
s=chmod("/usr/local/cxuan",777);
他就是會把/usr/local/cxuan這個路徑的保護(hù)模式改為 rwxrwxrwx,任何組和人都可以操作這個路徑。只有該文件的所有者和超級用戶才有權(quán)利更改保護(hù)模式。
access系統(tǒng)調(diào)用用來檢驗(yàn)實(shí)際的 UID 和 GID 對某文件是否擁有特定的權(quán)限。下面就是四個 getxxx 的系統(tǒng)調(diào)用,這些用來獲取 uid 和 gid 的。
?注意:其中的 chown、setuid 和 setgid 是超級用戶才能使用,用來改變所有者進(jìn)程的 UID 和 GID。?
Linux 安全實(shí)現(xiàn)
當(dāng)用戶登錄時,登錄程序,也被稱為login,會要求輸入用戶名和密碼。它會對密碼進(jìn)行哈希處理,然后在/etc/passwd中進(jìn)行查找,看看是否有匹配的項(xiàng)。使用哈希的原因是防止密碼在系統(tǒng)中以非加密的方式存在。如果密碼正確,登錄程序會在 /etc/passwd 中讀取用戶選擇的 shell 程序的名稱,有可能是bash,有可能是shell或者其他的csh或ksh。然后登錄程序使用 setuid 和 setgid 這兩個系統(tǒng)調(diào)用來把自己的 UID 和 GID 變?yōu)橛脩舻?UID 和 GID,然后它打開鍵盤作為標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸入的文件描述符是 0 ,屏幕作為標(biāo)準(zhǔn)輸出,文件描述符是 1 ,屏幕也作為標(biāo)準(zhǔn)錯誤輸出,文件描述符為 2。最后,執(zhí)行用戶選擇的 shell 程序,終止。
當(dāng)任何進(jìn)程想要打開一個文件,系統(tǒng)首先將文件的 i - node 所記錄的保護(hù)位與用戶有效 UID 和 有效 GID 進(jìn)行對比,來檢查訪問是否允許。如果訪問允許,就打開文件并返回文件描述符;否則不打開文件,返回 - 1。
Linux 安全模型和實(shí)現(xiàn)在本質(zhì)上與大多數(shù)傳統(tǒng)的 UNIX 系統(tǒng)相同。
后記
這篇文章從 Linux 進(jìn)程線程、內(nèi)存管理、文件系統(tǒng)、IO 管理和安全來為你呈現(xiàn)了一幅 Linux 藍(lán)圖,文中涉及大量的系統(tǒng)調(diào)用和解釋,是你了解 Linux 操作系統(tǒng)需要仔細(xì)研讀的一篇文章。
責(zé)任編輯:xj
原文標(biāo)題:對不起,學(xué)會這些 Linux 知識后,我有點(diǎn)飄
文章出處:【微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
程序
應(yīng)用
ls
列出目錄
cp
復(fù)制文件
head
顯示文件的前幾行
make
編譯文件生成二進(jìn)制文件
cd
切換目錄
mkdir
創(chuàng)建目錄
chmod
修改文件訪問權(quán)限
ps
列出文件進(jìn)程
pr
格式化打印
rm
刪除一個文件
rmdir
刪除文件目錄
tail
提取文件最后幾行
tr
字符集轉(zhuǎn)換
grep
分組
cat
將多個文件連續(xù)標(biāo)準(zhǔn)輸出
od
以八進(jìn)制顯示文件
cut
從文件中剪切
paste
從文件中粘貼
系統(tǒng)調(diào)用指令
描述
pause
掛起信號
nice
改變分時進(jìn)程的優(yōu)先級
ptrace
進(jìn)程跟蹤
kill
向進(jìn)程發(fā)送信號
pipe
創(chuàng)建管道
mkfifo
創(chuàng)建 fifo 的特殊文件(命名管道)
sigaction
設(shè)置對指定信號的處理方法
msgctl
消息控制操作
semctl
信號量控制
系統(tǒng)調(diào)用
描述
s = brk(addr)
改變數(shù)據(jù)段大小
a = mmap(addr,len,prot,flags,fd,offset)
進(jìn)行映射
s = unmap(addr,len)
取消映射
系統(tǒng)調(diào)用
描述
fd = creat(name,mode)
一種創(chuàng)建一個新文件的方式
fd = open(file, ...)
打開文件讀、寫或者讀寫
s = close(fd)
關(guān)閉一個打開的文件
n = read(fd, buffer, nbytes)
從文件中向緩存中讀入數(shù)據(jù)
n = write(fd, buffer, nbytes)
從緩存中向文件中寫入數(shù)據(jù)
position = lseek(fd, offset, whence)
移動文件指針
s = stat(name, &buf)
獲取文件信息
s = fstat(fd, &buf)
獲取文件信息
s = pipe(&fd[0])
創(chuàng)建一個管道
s = fcntl(fd,...)
文件加鎖等其他操作
存儲文件的設(shè)備
存儲文件的設(shè)備
i-node 編號
文件模式(包括保護(hù)位信息)
文件鏈接的數(shù)量
文件所有者標(biāo)識
文件所屬的組
文件大小(字節(jié))
創(chuàng)建時間
最后一個修改/訪問時間
系統(tǒng)調(diào)用
描述
s = mkdir(path,mode)
創(chuàng)建一個新的目錄
s = rmdir(path)
移除一個目錄
s = link(oldpath,newpath)
創(chuàng)建指向已有文件的鏈接
s = unlink(path)
取消文件的鏈接
s = chdir(path)
改變工作目錄
dir = opendir(path)
打開一個目錄讀取
s = closedir(dir)
關(guān)閉一個目錄
dirent = readdir(dir)
讀取一個目錄項(xiàng)
rewinddir(dir)
回轉(zhuǎn)目錄使其在此使用
對象
描述
超級塊
特定的文件系統(tǒng)
Dentry
目錄項(xiàng),路徑的一個組成部分
I-node
特定的文件
File
跟一個進(jìn)程相關(guān)聯(lián)的打開文件
屬性
字節(jié)
描述
Mode
2
文件屬性、保護(hù)位、setuid 和 setgid 位
Nlinks
2
指向 i - node 節(jié)點(diǎn)目錄項(xiàng)的數(shù)目
Uid
2
文件所有者的 UID
Gid
2
文件所有者的 GID
Size
4
文件字節(jié)大小
Addr
60
12 個磁盤塊以及后面 3 個間接塊的地址
Gen
1
每次重復(fù)使用 i - node 時增加的代號
Atime
4
最近訪問文件的時間
Mtime
4
最近修改文件的時間
Ctime
4
最近更改 i - node 的時間
系統(tǒng)調(diào)用
描述
tcgetattr
獲取屬性
tcsetattr
設(shè)置屬性
cfgetispeed
獲取輸入速率
cfgetospeed
獲取輸出速率
cfsetispeed
設(shè)置輸入速率
cfsetospeed
設(shè)置輸出速率
二進(jìn)制
標(biāo)記
準(zhǔn)許的文件訪問權(quán)限
111000000
rwx------
所有者可讀、寫和執(zhí)行
111111000
rwxrwx---
所有者和組可以讀、寫和執(zhí)行
111111111
rwxrwxrwx
所有人可以讀、寫和執(zhí)行
000000000
---------
任何人不擁有任何權(quán)限
000000111
------rwx
只有組以外的其他用戶擁有所有權(quán)
110100100
rw-r--r--
所有者可以讀和寫,其他人可以讀
110100100
rw-r-----
所有者可以讀和寫,組可以讀
系統(tǒng)調(diào)用
描述
chmod
改變文件的保護(hù)模式
access
使用真實(shí)的 UID 和 GID 測試訪問權(quán)限
chown
改變所有者和組
setuid
設(shè)置 UID
setgid
設(shè)置 GID
getuid
獲取真實(shí)的 UID
getgid
獲取真實(shí)的 GID
geteuid
獲取有效的 UID
getegid
獲取有效的 GID
-
Linux
+關(guān)注
關(guān)注
87文章
11215瀏覽量
208740 -
LINUX內(nèi)核
+關(guān)注
關(guān)注
1文章
316瀏覽量
21608
原文標(biāo)題:對不起,學(xué)會這些 Linux 知識后,我有點(diǎn)飄
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論