自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

全方位剖析Linux操作系統(tǒng),太全了

系統(tǒng) Linux
UNIX 是一個(gè)交互式系統(tǒng),用于同時(shí)處理多進(jìn)程和多用戶同時(shí)在線。為什么要說 UNIX,那是因?yàn)?Linux 是由 UNIX 發(fā)展而來的,UNIX 是由程序員設(shè)計(jì),它的主要服務(wù)對(duì)象也是程序員。Linux 繼承了 UNIX 的設(shè)計(jì)目標(biāo)。

Linux 簡(jiǎn)介

UNIX 是一個(gè)交互式系統(tǒng),用于同時(shí)處理多進(jìn)程和多用戶同時(shí)在線。為什么要說 UNIX,那是因?yàn)?Linux 是由 UNIX 發(fā)展而來的,UNIX 是由程序員設(shè)計(jì),它的主要服務(wù)對(duì)象也是程序員。Linux 繼承了 UNIX 的設(shè)計(jì)目標(biāo)。從智能手機(jī)到汽車,超級(jí)計(jì)算機(jī)和家用電器,從家用臺(tái)式機(jī)到企業(yè)服務(wù)器,Linux 操作系統(tǒng)無處不在。

大多數(shù)程序員都喜歡讓系統(tǒng)盡量簡(jiǎn)單,優(yōu)雅并具有一致性。舉個(gè)例子,從最底層的角度來講,一個(gè)文件應(yīng)該只是一個(gè)字節(jié)集合。為了實(shí)現(xiàn)順序存取、隨機(jī)存取、按鍵存取、遠(yuǎn)程存取只能是妨礙你的工作。相同的,如果命令

ls A* 

意味著只列出以 A 為開頭的所有文件,那么命令

rm A* 

應(yīng)該會(huì)移除所有以 A 為開頭的文件而不是只刪除文件名是 A* 的文件。這個(gè)特性也是最小吃驚原則(principle of least surprise)

最小吃驚原則一半常用于用戶界面和軟件設(shè)計(jì)。它的原型是:該功能或者特征應(yīng)該符合用戶的預(yù)期,不應(yīng)該使用戶感到驚訝和震驚。

一些有經(jīng)驗(yàn)的程序員通常希望系統(tǒng)具有較強(qiáng)的功能性和靈活性。設(shè)計(jì) Linux 的一個(gè)基本目標(biāo)是每個(gè)應(yīng)用程序只做一件事情并把他做好。所以編譯器只負(fù)責(zé)編譯的工作,編譯器不會(huì)產(chǎn)生列表,因?yàn)橛衅渌麘?yīng)用比編譯器做的更好。

很多人都不喜歡冗余,為什么在 cp 就能描述清楚你想干什么時(shí)候還使用 copy?這完全是在浪費(fèi)寶貴的 hacking time。為了從文件中提取所有包含字符串 ard 的行,Linux 程序員應(yīng)該輸入

grep ard f 

Linux 接口

Linux 系統(tǒng)是一種金字塔模型的系統(tǒng),如下所示

全方位剖析 Linux 操作系統(tǒng),太全了

 

應(yīng)用程序發(fā)起系統(tǒng)調(diào)用把參數(shù)放在寄存器中(有時(shí)候放在棧中),并發(fā)出 trap 系統(tǒng)陷入指令切換用戶態(tài)至內(nèi)核態(tài)。因?yàn)椴荒苤苯釉?C 中編寫 trap 指令,因此 C 提供了一個(gè)庫(kù),庫(kù)中的函數(shù)對(duì)應(yīng)著系統(tǒng)調(diào)用。有些函數(shù)是使用匯編編寫的,但是能夠從 C 中調(diào)用。每個(gè)函數(shù)首先把參數(shù)放在合適的位置然后執(zhí)行系統(tǒng)調(diào)用指令。因此如果你想要執(zhí)行 read 系統(tǒng)調(diào)用的話,C 程序會(huì)調(diào)用 read 函數(shù)庫(kù)來執(zhí)行。這里順便提一下,是由 POSIX 指定的庫(kù)接口而不是系統(tǒng)調(diào)用接口。也就是說,POSIX 會(huì)告訴一個(gè)標(biāo)準(zhǔn)系統(tǒng)應(yīng)該提供哪些庫(kù)過程,它們的參數(shù)是什么,它們必須做什么以及它們必須返回什么結(jié)果。

除了操作系統(tǒng)和系統(tǒng)調(diào)用庫(kù)外,Linux 操作系統(tǒng)還要提供一些標(biāo)準(zhǔn)程序,比如文本編輯器、編譯器、文件操作工具等。直接和用戶打交道的是上面這些應(yīng)用程序。因此我們可以說 Linux 具有三種不同的接口:系統(tǒng)調(diào)用接口、庫(kù)函數(shù)接口和應(yīng)用程序接口

Linux 中的 GUI(Graphical User Interface) 和 UNIX 中的非常相似,這種 GUI 創(chuàng)建一個(gè)桌面環(huán)境,包括窗口、目標(biāo)和文件夾、工具欄和文件拖拽功能。一個(gè)完整的 GUI 還包括窗口管理器以及各種應(yīng)用程序。

全方位剖析 Linux 操作系統(tǒng),太全了

 

Linux 上的 GUI 由 X 窗口支持,主要組成部分是 X 服務(wù)器、控制鍵盤、鼠標(biāo)、顯示器等。當(dāng)在 Linux 上使用圖形界面時(shí),用戶可以通過鼠標(biāo)點(diǎn)擊運(yùn)行程序或者打開文件,通過拖拽將文件進(jìn)行復(fù)制等。

Linux 組成部分

事實(shí)上,Linux 操作系統(tǒng)可以由下面這幾部分構(gòu)成

  • 引導(dǎo)程序(Bootloader):引導(dǎo)程序是管理計(jì)算機(jī)啟動(dòng)過程的軟件,對(duì)于大多數(shù)用戶而言,只是彈出一個(gè)屏幕,但其實(shí)內(nèi)部操作系統(tǒng)做了很多事情
  • 內(nèi)核(Kernel):內(nèi)核是操作系統(tǒng)的核心,負(fù)責(zé)管理 CPU、內(nèi)存和外圍設(shè)備等。
  • 初始化系統(tǒng)(Init System):這是一個(gè)引導(dǎo)用戶空間并負(fù)責(zé)控制守護(hù)程序的子系統(tǒng)。一旦從引導(dǎo)加載程序移交了初始引導(dǎo),它就是用于管理引導(dǎo)過程的初始化系統(tǒng)。
  • 后臺(tái)進(jìn)程(Daemon):后臺(tái)進(jìn)程顧名思義就是在后臺(tái)運(yùn)行的程序,比如打印、聲音、調(diào)度等,它們可以在引導(dǎo)過程中啟動(dòng),也可以在登錄桌面后啟動(dòng)
  • 圖形服務(wù)器(Graphical server):這是在監(jiān)視器上顯示圖形的子系統(tǒng)。通常將其稱為 X 服務(wù)器或 X。
  • 桌面環(huán)境(Desktop environment):這是用戶與之實(shí)際交互的部分,有很多桌面環(huán)境可供選擇,每個(gè)桌面環(huán)境都包含內(nèi)置應(yīng)用程序,比如文件管理器、Web 瀏覽器、游戲等
  • 應(yīng)用程序(Applications):桌面環(huán)境不提供完整的應(yīng)用程序,就像 Windows 和 macOS 一樣,Linux 提供了成千上萬(wàn)個(gè)可以輕松找到并安裝的高質(zhì)量軟件。

Shell

盡管 Linux 應(yīng)用程序提供了 GUI ,但是大部分程序員仍偏好于使用命令行(command-line interface),稱為shell。用戶通常在 GUI 中啟動(dòng)一個(gè) shell 窗口然后就在 shell 窗口下進(jìn)行工作。

全方位剖析 Linux 操作系統(tǒng),太全了

 

shell 命令行使用速度快、功能更強(qiáng)大、而且易于擴(kuò)展、并且不會(huì)帶來肢體重復(fù)性勞損(RSI)

下面會(huì)介紹一些最簡(jiǎn)單的 bash shell。當(dāng) shell 啟動(dòng)時(shí),它首先進(jìn)行初始化,在屏幕上輸出一個(gè) 提示符(prompt),通常是一個(gè)百分號(hào)或者美元符號(hào),等待用戶輸入

全方位剖析 Linux 操作系統(tǒng),太全了

 

等用戶輸入一個(gè)命令后,shell 提取其中的第一個(gè)詞,這里的詞指的是被空格或制表符分隔開的一連串字符。假定這個(gè)詞是將要運(yùn)行程序的程序名,那么就會(huì)搜索這個(gè)程序,如果找到了這個(gè)程序就會(huì)運(yùn)行它。然后 shell 會(huì)將自己掛起直到程序運(yùn)行完畢,之后再嘗試讀入下一條指令。shell 也是一個(gè)普通的用戶程序。它的主要功能就是讀取用戶的輸入和顯示計(jì)算的輸出。shell 命令中可以包含參數(shù),它們作為字符串傳遞給所調(diào)用的程序。比如

cp src dest 

會(huì)調(diào)用 cp 應(yīng)用程序并包含兩個(gè)參數(shù) srcdest。這個(gè)程序會(huì)解釋第一個(gè)參數(shù)是一個(gè)已經(jīng)存在的文件名,然后創(chuàng)建一個(gè)該文件的副本,名稱為 dest。

并不是所有的參數(shù)都是文件名,比如下面

head -20 file 

第一個(gè)參數(shù) -20,會(huì)告訴 head 應(yīng)用程序打印文件的前 20 行,而不是默認(rèn)的 10 行??刂泼畈僮骰蛘咧付蛇x值的參數(shù)稱為標(biāo)志(flag),按照慣例標(biāo)志應(yīng)該使用 - 來表示。這個(gè)符號(hào)是必要的,比如

head 20 file 

是一個(gè)完全合法的命令,它會(huì)告訴 head 程序輸出文件名為 20 的文件的前 10 行,然后輸出文件名為 file 文件的前 10 行。Linux 操作系統(tǒng)可以接受一個(gè)或多個(gè)參數(shù)。

為了更容易的指定多個(gè)文件名,shell 支持 魔法字符(magic character),也被稱為通配符(wild cards)。比如,* 可以匹配一個(gè)或者多個(gè)可能的字符串

ls *.c 

告訴 ls 列舉出所有文件名以 .c 結(jié)束的文件。如果同時(shí)存在多個(gè)文件,則會(huì)在后面進(jìn)行并列。

另一個(gè)通配符是問號(hào),負(fù)責(zé)匹配任意一個(gè)字符。一組在中括號(hào)中的字符可以表示其中任意一個(gè),因此

ls [abc]* 

會(huì)列舉出所有以 a、b 或者 c 開頭的文件。

shell 應(yīng)用程序不一定通過終端進(jìn)行輸入和輸出。shell 啟動(dòng)時(shí),就會(huì)獲取 標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯(cuò)誤文件進(jìn)行訪問的能力。

標(biāo)準(zhǔn)輸出是從鍵盤輸入的,標(biāo)準(zhǔn)輸出或者標(biāo)準(zhǔn)錯(cuò)誤是輸出到顯示器的。許多 Linux 程序默認(rèn)是從標(biāo)準(zhǔn)輸入進(jìn)行輸入并從標(biāo)準(zhǔn)輸出進(jìn)行輸出。比如

sort	 

會(huì)調(diào)用 sort 程序,會(huì)從終端讀取數(shù)據(jù)(直到用戶輸入 ctrl-d 結(jié)束),根據(jù)字母順序進(jìn)行排序,然后將結(jié)果輸出到屏幕上。

通常還可以重定向標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出,重定向標(biāo)準(zhǔn)輸入使用 < 后面跟文件名。標(biāo)準(zhǔn)輸出可以通過一個(gè)大于號(hào) > 進(jìn)行重定向。允許一個(gè)命令中重定向標(biāo)準(zhǔn)輸入和輸出。例如命令

sort <in >out 

會(huì)使 sort 從文件 in 中得到輸入,并把結(jié)果輸出到 out 文件中。由于標(biāo)準(zhǔn)錯(cuò)誤沒有重定向,所以錯(cuò)誤信息會(huì)直接打印到屏幕上。從標(biāo)準(zhǔn)輸入讀入,對(duì)其進(jìn)行處理并將其寫入到標(biāo)準(zhǔn)輸出的程序稱為 過濾器。

考慮下面由三個(gè)分開的命令組成的指令

sort <in >temp;head -30 <temp;rm temp 

首先會(huì)調(diào)用 sort 應(yīng)用程序,從標(biāo)準(zhǔn)輸入 in 中進(jìn)行讀取,并通過標(biāo)準(zhǔn)輸出到 temp。當(dāng)程序運(yùn)行完畢后,shell 會(huì)運(yùn)行 head ,告訴它打印前 30 行,并在標(biāo)準(zhǔn)輸出(默認(rèn)為終端)上打印。最后,temp 臨時(shí)文件被刪除。輕輕的,你走了,你揮一揮衣袖,不帶走一片云彩。

命令行中的第一個(gè)程序通常會(huì)產(chǎn)生輸出,在上面的例子中,產(chǎn)生的輸出都不 temp 文件接收。然而,Linux 還提供了一個(gè)簡(jiǎn)單的命令來做這件事,例如下面

sort <in | head -30 

上面 | 稱為豎線符號(hào),它的意思是從 sort 應(yīng)用程序產(chǎn)生的排序輸出會(huì)直接作為輸入顯示,無需創(chuàng)建、使用和移除臨時(shí)文件。由管道符號(hào)連接的命令集合稱為管道(pipeline)。例如如下

grep cxuan *.c | sort | head -30 | tail -5 >f00 

對(duì)任意以 .t 結(jié)尾的文件中包含 cxuan 的行被寫到標(biāo)準(zhǔn)輸出中,然后進(jìn)行排序。這些內(nèi)容中的前 30 行被 head 出來并傳給 tail ,它又將最后 5 行傳遞給 foo。這個(gè)例子提供了一個(gè)管道將多個(gè)命令連接起來。

可以把一系列 shell 命令放在一個(gè)文件中,然后將此文件作為輸入來運(yùn)行。shell 會(huì)按照順序?qū)λ麄冞M(jìn)行處理,就像在鍵盤上鍵入命令一樣。包含 shell 命令的文件被稱為 shell 腳本(shell scripts)。

推薦一個(gè) shell 命令的學(xué)習(xí)網(wǎng)站:https://www.shellscript.sh/

shell 腳本其實(shí)也是一段程序,shell 腳本中可以對(duì)變量進(jìn)行賦值,也包含循環(huán)控制語(yǔ)句比如 if、for、while 等,shell 的設(shè)計(jì)目標(biāo)是讓其看起來和 C 相似(There is no doubt that C is father)。由于 shell 也是一個(gè)用戶程序,所以用戶可以選擇不同的 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 瀏覽器、多媒體播放器、圖片瀏覽器、辦公軟件和游戲程序等。

我們?cè)谏厦娴睦又幸呀?jīng)見過了幾個(gè) Linux 的應(yīng)用程序,比如 sort、cp、ls、head,下面我們?cè)賮碚J(rèn)識(shí)一下其他 Linux 的應(yīng)用程序。

我們先從幾個(gè)例子開始講起,比如

cp a b 

是將 a 復(fù)制一個(gè)副本為 b ,而

mv a b 

是將 a 移動(dòng)到 b ,但是刪除原文件。

上面這兩個(gè)命令有一些區(qū)別,cp 是將文件進(jìn)行復(fù)制,復(fù)制完成后會(huì)有兩個(gè)文件 a 和 b;而 mv 相當(dāng)于是文件的移動(dòng),移動(dòng)完成后就不再有 a 文件。cat 命令可以把多個(gè)文件內(nèi)容進(jìn)行連接。使用 rm 可以刪除文件;使用 chmod 可以允許所有者改變?cè)L問權(quán)限;文件目錄的的創(chuàng)建和刪除可以使用 mkdirrmdir 命令;使用 ls 可以查看目錄文件,ls 可以顯示很多屬性,比如大小、用戶、創(chuàng)建日期等;sort 決定文件的顯示順序

Linux 應(yīng)用程序還包括過濾器 grep,grep 從標(biāo)準(zhǔn)輸入或者一個(gè)或多個(gè)輸入文件中提取特定模式的行;sort 將輸入進(jìn)行排序并輸出到標(biāo)準(zhǔn)輸出;head 提取輸入的前幾行;tail 提取輸入的后面幾行;除此之外的過濾器還有 cutpaste,允許對(duì)文本行的剪切和復(fù)制;od 將輸入轉(zhuǎn)換為 ASCII ;tr 實(shí)現(xiàn)字符大小寫轉(zhuǎn)換;pr 為格式化打印輸出等。

程序編譯工具使用 gcc

make 命令用于自動(dòng)編譯,這是一個(gè)很強(qiáng)大的命令,它用于維護(hù)一個(gè)大的程序,往往這類程序的源碼由許多文件構(gòu)成。典型的,有一些是 header files 頭文件,源文件通常使用 include 指令包含這些文件,make 的作用就是跟蹤哪些文件屬于頭文件,然后安排自動(dòng)編譯的過程。

下面列出了 POSIX 的標(biāo)準(zhǔn)應(yīng)用程序

程序應(yīng)用ls列出目錄cp復(fù)制文件head顯示文件的前幾行make編譯文件生成二進(jìn)制文件cd切換目錄mkdir創(chuàng)建目錄chmod修改文件訪問權(quán)限ps列出文件進(jìn)程pr格式化打印rm刪除一個(gè)文件rmdir刪除文件目錄tail提取文件最后幾行tr字符集轉(zhuǎn)換grep分組cat將多個(gè)文件連續(xù)標(biāo)準(zhǔn)輸出od以八進(jìn)制顯示文件cut從文件中剪切paste從文件中粘貼

Linux 內(nèi)核結(jié)構(gòu)

在上面我們看到了 Linux 的整體結(jié)構(gòu),下面我們從整體的角度來看一下 Linux 的內(nèi)核結(jié)構(gòu)

全方位剖析 Linux 操作系統(tǒng),太全了

 

內(nèi)核直接坐落在硬件上,內(nèi)核的主要作用就是 I/O 交互、內(nèi)存管理和控制 CPU 訪問。上圖中還包括了 中斷調(diào)度器,中斷是與設(shè)備交互的主要方式。中斷出現(xiàn)時(shí)調(diào)度器就會(huì)發(fā)揮作用。這里的低級(jí)代碼停止正在運(yùn)行的進(jìn)程,將其狀態(tài)保存在內(nèi)核進(jìn)程結(jié)構(gòu)中,并啟動(dòng)驅(qū)動(dòng)程序。進(jìn)程調(diào)度也會(huì)發(fā)生在內(nèi)核完成一些操作并且啟動(dòng)用戶進(jìn)程的時(shí)候。圖中的調(diào)度器是 dispatcher。

注意這里的調(diào)度器是 dispatcher 而不是 scheduler,這兩者是有區(qū)別的

scheduler 和 dispatcher 都是和進(jìn)程調(diào)度相關(guān)的概念,不同的是 scheduler 會(huì)從幾個(gè)進(jìn)程中隨意選取一個(gè)進(jìn)程;而 dispatcher 會(huì)給 scheduler 選擇的進(jìn)程分配 CPU。

然后,我們把內(nèi)核系統(tǒng)分為三部分。

  • I/O 部分負(fù)責(zé)與設(shè)備進(jìn)行交互以及執(zhí)行網(wǎng)絡(luò)和存儲(chǔ) I/O 操作的所有內(nèi)核部分。

從圖中可以看出 I/O 層次的關(guān)系,最高層是一個(gè)虛擬文件系統(tǒng),也就是說不管文件是來自內(nèi)存還是磁盤中,都是經(jīng)過虛擬文件系統(tǒng)中的。從底層看,所有的驅(qū)動(dòng)都是字符驅(qū)動(dòng)或者塊設(shè)備驅(qū)動(dòng)。二者的主要區(qū)別就是是否允許隨機(jī)訪問。網(wǎng)絡(luò)驅(qū)動(dòng)設(shè)備并不是一種獨(dú)立的驅(qū)動(dòng)設(shè)備,它實(shí)際上是一種字符設(shè)備,不過網(wǎng)絡(luò)設(shè)備的處理方式和字符設(shè)備不同。

上面的設(shè)備驅(qū)動(dòng)程序中,每個(gè)設(shè)備類型的內(nèi)核代碼都不同。字符設(shè)備有兩種使用方式,有一鍵式的比如 vi 或者 emacs ,需要每一個(gè)鍵盤輸入。其他的比如 shell ,是需要輸入一行按回車鍵將字符串發(fā)送給程序進(jìn)行編輯。

網(wǎng)絡(luò)軟件通常是模塊化的,由不同的設(shè)備和協(xié)議來支持。大多數(shù) Linux 系統(tǒng)在內(nèi)核中包含一個(gè)完整的硬件路由器的功能,但是這個(gè)不能和外部路由器相比,路由器上面是協(xié)議棧,包括 TCP/IP 協(xié)議,協(xié)議棧上面是 socket 接口,socket 負(fù)責(zé)與外部進(jìn)行通信,充當(dāng)了門的作用。

磁盤驅(qū)動(dòng)上面是 I/O 調(diào)度器,它負(fù)責(zé)排序和分配磁盤讀寫操作,以盡可能減少磁頭的無用移動(dòng)。

  • I/O 右邊的是內(nèi)存部件,程序被裝載進(jìn)內(nèi)存,由 CPU 執(zhí)行,這里會(huì)涉及到虛擬內(nèi)存的部件,頁(yè)面的換入和換出是如何進(jìn)行的,壞頁(yè)面的替換和經(jīng)常使用的頁(yè)面會(huì)進(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)用會(huì)觸發(fā)一個(gè) 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)本身的接口,它對(duì)于創(chuàng)建進(jìn)程和線程,內(nèi)存分配,共享文件和 I/O 來說都很重要。

我們將從各個(gè)版本的共性出發(fā)來進(jìn)行探討。

基本概念

每個(gè)進(jìn)程都會(huì)運(yùn)行一段獨(dú)立的程序,并且在初始化的時(shí)候擁有一個(gè)獨(dú)立的控制線程。換句話說,每個(gè)進(jìn)程都會(huì)有一個(gè)自己的程序計(jì)數(shù)器,這個(gè)程序計(jì)數(shù)器用來記錄下一個(gè)需要被執(zhí)行的指令。Linux 允許進(jìn)程在運(yùn)行時(shí)創(chuàng)建額外的線程。

全方位剖析 Linux 操作系統(tǒng),太全了

 

Linux 是一個(gè)多道程序設(shè)計(jì)系統(tǒng),因此系統(tǒng)中存在彼此相互獨(dú)立的進(jìn)程同時(shí)運(yùn)行。此外,每個(gè)用戶都會(huì)同時(shí)有幾個(gè)活動(dòng)的進(jìn)程。因?yàn)槿绻且粋€(gè)大型系統(tǒng),可能有數(shù)百上千的進(jìn)程在同時(shí)運(yùn)行。

在某些用戶空間中,即使用戶退出登錄,仍然會(huì)有一些后臺(tái)進(jìn)程在運(yùn)行,這些進(jìn)程被稱為 守護(hù)進(jìn)程(daemon)。

Linux 中有一種特殊的守護(hù)進(jìn)程被稱為 計(jì)劃守護(hù)進(jìn)程(Cron daemon) ,計(jì)劃守護(hù)進(jìn)程可以每分鐘醒來一次檢查是否有工作要做,做完會(huì)繼續(xù)回到睡眠狀態(tài)等待下一次喚醒。

[[348940]]

 

Cron 是一個(gè)守護(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)程通過非常簡(jiǎn)單的方式來創(chuàng)建,fork 系統(tǒng)調(diào)用會(huì)創(chuàng)建一個(gè)源進(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)打開了某個(gè)文件,那么 fork 后,父進(jìn)程和子進(jìn)程仍然共享這個(gè)打開的文件。對(duì)共享文件的修改會(huì)對(duì)父進(jìn)程和子進(jìn)程同時(shí)可見。

那么該如何區(qū)分父進(jìn)程和子進(jìn)程呢?子進(jìn)程只是父進(jìn)程的拷貝,所以它們幾乎所有的情況都一樣,包括內(nèi)存映像、變量、寄存器等。區(qū)分的關(guān)鍵在于 fork 函數(shù)調(diào)用后的返回值,如果 fork 后返回一個(gè)非零值,這個(gè)非零值即是子進(jìn)程的 進(jìn)程標(biāo)識(shí)符(Process Identiier, PID),而會(huì)給子進(jìn)程返回一個(gè)零值,可以用下面代碼來進(jìn)行表示

pid = fork();    // 調(diào)用 fork 函數(shù)創(chuàng)建進(jìn)程 if(pid < 0){   error()				 // pid < 0,創(chuàng)建失敗 } else if(pid > 0){   parent_handle() // 父進(jìn)程代碼 } else {   child_handle()  // 子進(jìn)程代碼 } 

父進(jìn)程在 fork 后會(huì)得到子進(jìn)程的 PID,這個(gè) PID 即能代表這個(gè)子進(jìn)程的唯一標(biāo)識(shí)符也就是 PID。如果子進(jìn)程想要知道自己的 PID,可以調(diào)用 getpid 方法。當(dāng)子進(jìn)程結(jié)束運(yùn)行時(shí),父進(jìn)程會(huì)得到子進(jìn)程的 PID,因?yàn)橐粋€(gè)進(jìn)程會(huì) fork 很多子進(jìn)程,子進(jìn)程也會(huì) fork 子進(jìn)程,所以 PID 是非常重要的。我們把第一次調(diào)用 fork 后的進(jìn)程稱為 原始進(jìn)程,一個(gè)原始進(jìn)程可以生成一顆繼承樹

全方位剖析 Linux 操作系統(tǒng),太全了

 

Linux 進(jìn)程間通信

Linux 進(jìn)程間的通信機(jī)制通常被稱為 Internel-Process communication,IPC下面我們來說一說 Linux 進(jìn)程間通信的機(jī)制,大致來說,Linux 進(jìn)程間的通信機(jī)制可以分為 6 種

全方位剖析 Linux 操作系統(tǒng),太全了

 

下面我們分別對(duì)其進(jìn)行概述

信號(hào) signal

信號(hào)是 UNIX 系統(tǒng)最先開始使用的進(jìn)程間通信機(jī)制,因?yàn)?Linux 是繼承于 UNIX 的,所以 Linux 也支持信號(hào)機(jī)制,通過向一個(gè)或多個(gè)進(jìn)程發(fā)送異步事件信號(hào)來實(shí)現(xiàn),信號(hào)可以從鍵盤或者訪問不存在的位置等地方產(chǎn)生;信號(hào)通過 shell 將任務(wù)發(fā)送給子進(jìn)程。

你可以在 Linux 系統(tǒng)上輸入 kill -l 來列出系統(tǒng)使用的信號(hào),下面是我提供的一些信號(hào)

全方位剖析 Linux 操作系統(tǒng),太全了

 

進(jìn)程可以選擇忽略發(fā)送過來的信號(hào),但是有兩個(gè)是不能忽略的:SIGSTOPSIGKILL 信號(hào)。SIGSTOP 信號(hào)會(huì)通知當(dāng)前正在運(yùn)行的進(jìn)程執(zhí)行關(guān)閉操作,SIGKILL 信號(hào)會(huì)通知當(dāng)前進(jìn)程應(yīng)該被殺死。除此之外,進(jìn)程可以選擇它想要處理的信號(hào),進(jìn)程也可以選擇阻止信號(hào),如果不阻止,可以選擇自行處理,也可以選擇進(jìn)行內(nèi)核處理。如果選擇交給內(nèi)核進(jìn)行處理,那么就執(zhí)行默認(rèn)處理。

操作系統(tǒng)會(huì)中斷目標(biāo)程序的進(jìn)程來向其發(fā)送信號(hào)、在任何非原子指令中,執(zhí)行都可以中斷,如果進(jìn)程已經(jīng)注冊(cè)了新號(hào)處理程序,那么就執(zhí)行進(jìn)程,如果沒有注冊(cè),將采用默認(rèn)處理的方式。

例如:當(dāng)進(jìn)程收到 SIGFPE 浮點(diǎn)異常的信號(hào)后,默認(rèn)操作是對(duì)其進(jìn)行 dump(轉(zhuǎn)儲(chǔ))和退出。信號(hào)沒有優(yōu)先級(jí)的說法。如果同時(shí)為某個(gè)進(jìn)程產(chǎn)生了兩個(gè)信號(hào),則可以將它們呈現(xiàn)給進(jìn)程或者以任意的順序進(jìn)行處理。

下面我們就來看一下這些信號(hào)是干什么用的

  • SIGABRT 和 SIGIOT

SIGABRT 和 SIGIOT 信號(hào)發(fā)送給進(jìn)程,告訴其進(jìn)行終止,這個(gè) 信號(hào)通常在調(diào)用 C標(biāo)準(zhǔn)庫(kù)的abort()函數(shù)時(shí)由進(jìn)程本身啟動(dòng)

  • SIGALRM 、 SIGVTALRM、SIGPROF

當(dāng)設(shè)置的時(shí)鐘功能超時(shí)時(shí)會(huì)將 SIGALRM 、 SIGVTALRM、SIGPROF 發(fā)送給進(jìn)程。當(dāng)實(shí)際時(shí)間或時(shí)鐘時(shí)間超時(shí)時(shí),發(fā)送 SIGALRM。 當(dāng)進(jìn)程使用的 CPU 時(shí)間超時(shí)時(shí),將發(fā)送 SIGVTALRM。 當(dāng)進(jìn)程和系統(tǒng)代表進(jìn)程使用的CPU 時(shí)間超時(shí)時(shí),將發(fā)送 SIGPROF。

  • SIGBUS

SIGBUS 將造成總線中斷錯(cuò)誤時(shí)發(fā)送給進(jìn)程

  • SIGCHLD

當(dāng)子進(jìn)程終止、被中斷或者被中斷恢復(fù),將 SIGCHLD 發(fā)送給進(jìn)程。此信號(hào)的一種常見用法是指示操作系統(tǒng)在子進(jìn)程終止后清除其使用的資源。

  • SIGCONT

SIGCONT 信號(hào)指示操作系統(tǒng)繼續(xù)執(zhí)行先前由 SIGSTOP 或 SIGTSTP 信號(hào)暫停的進(jìn)程。該信號(hào)的一個(gè)重要用途是在 Unix shell 中的作業(yè)控制中。

  • SIGFPE

SIGFPE 信號(hào)在執(zhí)行錯(cuò)誤的算術(shù)運(yùn)算(例如除以零)時(shí)將被發(fā)送到進(jìn)程。

  • SIGUP

當(dāng) SIGUP 信號(hào)控制的終端關(guān)閉時(shí),會(huì)發(fā)送給進(jìn)程。許多守護(hù)程序?qū)⒅匦录虞d其配置文件并重新打開其日志文件,而不是在收到此信號(hào)時(shí)退出。

  • SIGILL

SIGILL 信號(hào)在嘗試執(zhí)行非法、格式錯(cuò)誤、未知或者特權(quán)指令時(shí)發(fā)出

  • SIGINT

當(dāng)用戶希望中斷進(jìn)程時(shí),操作系統(tǒng)會(huì)向進(jìn)程發(fā)送 SIGINT 信號(hào)。用戶輸入 ctrl - c 就是希望中斷進(jìn)程。

  • SIGKILL

SIGKILL 信號(hào)發(fā)送到進(jìn)程以使其馬上進(jìn)行終止。 與 SIGTERM 和 SIGINT 相比,這個(gè)信號(hào)無法捕獲和忽略執(zhí)行,并且進(jìn)程在接收到此信號(hào)后無法執(zhí)行任何清理操作,下面是一些例外情況

僵尸進(jìn)程無法殺死,因?yàn)榻┦M(jìn)程已經(jīng)死了,它在等待父進(jìn)程對(duì)其進(jìn)行捕獲

處于阻塞狀態(tài)的進(jìn)程只有再次喚醒后才會(huì)被 kill 掉

init 進(jìn)程是 Linux 的初始化進(jìn)程,這個(gè)進(jìn)程會(huì)忽略任何信號(hào)。

SIGKILL 通常是作為最后殺死進(jìn)程的信號(hào)、它通常作用于 SIGTERM 沒有響應(yīng)時(shí)發(fā)送給進(jìn)程。

  • SIGPIPE

SIGPIPE 嘗試寫入進(jìn)程管道時(shí)發(fā)現(xiàn)管道未連接無法寫入時(shí)發(fā)送到進(jìn)程

  • SIGPOLL

當(dāng)在明確監(jiān)視的文件描述符上發(fā)生事件時(shí),將發(fā)送 SIGPOLL 信號(hào)。

  • SIGRTMIN 至 SIGRTMAX

SIGRTMIN 至 SIGRTMAX 是實(shí)時(shí)信號(hào)

  • SIGQUIT

當(dāng)用戶請(qǐng)求退出進(jìn)程并執(zhí)行核心轉(zhuǎn)儲(chǔ)時(shí),SIGQUIT 信號(hào)將由其控制終端發(fā)送給進(jìn)程。

  • SIGSEGV

當(dāng) SIGSEGV 信號(hào)做出無效的虛擬內(nèi)存引用或分段錯(cuò)誤時(shí),即在執(zhí)行分段違規(guī)時(shí),將其發(fā)送到進(jìn)程。

  • SIGSTOP

SIGSTOP 指示操作系統(tǒng)終止以便以后進(jìn)行恢復(fù)時(shí)

  • SIGSYS

當(dāng) SIGSYS 信號(hào)將錯(cuò)誤參數(shù)傳遞給系統(tǒng)調(diào)用時(shí),該信號(hào)將發(fā)送到進(jìn)程。

  • SYSTERM

我們上面簡(jiǎn)單提到過了 SYSTERM 這個(gè)名詞,這個(gè)信號(hào)發(fā)送給進(jìn)程以請(qǐng)求終止。與 SIGKILL 信號(hào)不同,該信號(hào)可以被過程捕獲或忽略。這允許進(jìn)程執(zhí)行良好的終止,從而釋放資源并在適當(dāng)時(shí)保存狀態(tài)。 SIGINT 與SIGTERM 幾乎相同。

  • SIGTSIP

SIGTSTP 信號(hào)由其控制終端發(fā)送到進(jìn)程,以請(qǐng)求終端停止。

  • SIGTTIN 和 SIGTTOU

當(dāng) SIGTTIN 和SIGTTOU 信號(hào)分別在后臺(tái)嘗試從 tty 讀取或?qū)懭霑r(shí),信號(hào)將發(fā)送到該進(jìn)程。

  • SIGTRAP

在發(fā)生異?;蛘?trap 時(shí),將 SIGTRAP 信號(hào)發(fā)送到進(jìn)程

  • SIGURG

當(dāng)套接字具有可讀取的緊急或帶外數(shù)據(jù)時(shí),將 SIGURG 信號(hào)發(fā)送到進(jìn)程。

  • SIGUSR1 和 SIGUSR2

SIGUSR1 和 SIGUSR2 信號(hào)被發(fā)送到進(jìn)程以指示用戶定義的條件。

  • SIGXCPU

當(dāng) SIGXCPU 信號(hào)耗盡 CPU 的時(shí)間超過某個(gè)用戶可設(shè)置的預(yù)定值時(shí),將其發(fā)送到進(jìn)程

  • SIGXFSZ

當(dāng) SIGXFSZ 信號(hào)增長(zhǎng)超過最大允許大小的文件時(shí),該信號(hào)將發(fā)送到該進(jìn)程。

  • SIGWINCH

SIGWINCH 信號(hào)在其控制終端更改其大小(窗口更改)時(shí)發(fā)送給進(jìn)程。

管道 pipe

Linux 系統(tǒng)中的進(jìn)程可以通過建立管道 pipe 進(jìn)行通信。

在兩個(gè)進(jìn)程之間,可以建立一個(gè)通道,一個(gè)進(jìn)程向這個(gè)通道里寫入字節(jié)流,另一個(gè)進(jìn)程從這個(gè)管道中讀取字節(jié)流。管道是同步的,當(dāng)進(jìn)程嘗試從空管道讀取數(shù)據(jù)時(shí),該進(jìn)程會(huì)被阻塞,直到有可用數(shù)據(jù)為止。shell 中的管線 pipelines 就是用管道實(shí)現(xiàn)的,當(dāng) shell 發(fā)現(xiàn)輸出

sort <f | head 

它會(huì)創(chuàng)建兩個(gè)進(jìn)程,一個(gè)是 sort,一個(gè)是 head,sort,會(huì)在這兩個(gè)應(yīng)用程序之間建立一個(gè)管道使得 sort 進(jìn)程的標(biāo)準(zhǔn)輸出作為 head 程序的標(biāo)準(zhǔn)輸入。sort 進(jìn)程產(chǎn)生的輸出就不用寫到文件中了,如果管道滿了系統(tǒng)會(huì)停止 sort 以等待 head 讀出數(shù)據(jù)

全方位剖析 Linux 操作系統(tǒng),太全了

 

管道實(shí)際上就是 |,兩個(gè)應(yīng)用程序不知道有管道的存在,一切都是由 shell 管理和控制的。

共享內(nèi)存 shared memory

兩個(gè)進(jìn)程之間還可以通過共享內(nèi)存進(jìn)行進(jìn)程間通信,其中兩個(gè)或者多個(gè)進(jìn)程可以訪問公共內(nèi)存空間。兩個(gè)進(jìn)程的共享工作是通過共享內(nèi)存完成的,一個(gè)進(jìn)程所作的修改可以對(duì)另一個(gè)進(jìn)程可見(很像線程間的通信)。

全方位剖析 Linux 操作系統(tǒng),太全了

 

在使用共享內(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())
  • 對(duì)共享內(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ù)會(huì)丟失。相比之下,命名管道具有支持文件和獨(dú)特 API ,命名管道在文件系統(tǒng)中作為設(shè)備的專用文件存在。當(dāng)所有的進(jìn)程通信完成后,命名管道將保留在文件系統(tǒng)中以備后用。命名管道具有嚴(yán)格的 FIFO 行為

全方位剖析 Linux 操作系統(tǒng),太全了

 

寫入的第一個(gè)字節(jié)是讀取的第一個(gè)字節(jié),寫入的第二個(gè)字節(jié)是讀取的第二個(gè)字節(jié),依此類推。

消息隊(duì)列 Message Queue

一聽到消息隊(duì)列這個(gè)名詞你可能不知道是什么意思,消息隊(duì)列是用來描述內(nèi)核尋址空間內(nèi)的內(nèi)部鏈接列表??梢园磶追N不同的方式將消息按順序發(fā)送到隊(duì)列并從隊(duì)列中檢索消息。每個(gè)消息隊(duì)列由 IPC 標(biāo)識(shí)符唯一標(biāo)識(shí)。消息隊(duì)列有兩種模式,一種是嚴(yán)格模式, 嚴(yán)格模式就像是 FIFO 先入先出隊(duì)列似的,消息順序發(fā)送,順序讀取。還有一種模式是 非嚴(yán)格模式,消息的順序性不是非常重要。

套接字 Socket

還有一種管理兩個(gè)進(jìn)程間通信的是使用 socket,socket 提供端到端的雙相通信。一個(gè)套接字可以與一個(gè)或多個(gè)進(jìn)程關(guān)聯(lián)。就像管道有命令管道和未命名管道一樣,套接字也有兩種模式,套接字一般用于兩個(gè)進(jìn)程之間的網(wǎng)絡(luò)通信,網(wǎng)絡(luò)套接字需要來自諸如TCP(傳輸控制協(xié)議)或較低級(jí)別UDP(用戶數(shù)據(jù)報(bào)協(xié)議)等基礎(chǔ)協(xié)議的支持。

套接字有以下幾種分類

  • 順序包套接字(Sequential Packet Socket): 此類套接字為最大長(zhǎng)度固定的數(shù)據(jù)報(bào)提供可靠的連接。此連接是雙向的并且是順序的。
  • 數(shù)據(jù)報(bào)套接字(Datagram Socket):數(shù)據(jù)包套接字支持雙向數(shù)據(jù)流。數(shù)據(jù)包套接字接受消息的順序與發(fā)送者可能不同。
  • 流式套接字(Stream Socket):流套接字的工作方式類似于電話對(duì)話,提供雙向可靠的數(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)用通常在后臺(tái)靜默運(yùn)行,表示計(jì)算機(jī)程序向其操作系統(tǒng)內(nèi)核請(qǐng)求服務(wù)。

系統(tǒng)調(diào)用指令有很多,下面是一些與進(jìn)程管理相關(guān)的最主要的系統(tǒng)調(diào)用

fork

fork 調(diào)用用于創(chuàng)建一個(gè)與父進(jìn)程相同的子進(jìn)程,創(chuàng)建完進(jìn)程后的子進(jìn)程擁有和父進(jìn)程一樣的程序計(jì)數(shù)器、相同的 CPU 寄存器、相同的打開文件。

exec

exec 系統(tǒng)調(diào)用用于執(zhí)行駐留在活動(dòng)進(jìn)程中的文件,調(diào)用 exec 后,新的可執(zhí)行文件會(huì)替換先前的可執(zhí)行文件并獲得執(zhí)行。也就是說,調(diào)用 exec 后,會(huì)將舊文件或程序替換為新文件或執(zhí)行,然后執(zhí)行文件或程序。新的執(zhí)行程序被加載到相同的執(zhí)行空間中,因此進(jìn)程的 PID不會(huì)修改,因?yàn)槲覀?strong>沒有創(chuàng)建新進(jìn)程,只是替換舊進(jìn)程。但是進(jìn)程的數(shù)據(jù)、代碼、堆棧都已經(jīng)被修改。如果當(dāng)前要被替換的進(jìn)程包含多個(gè)線程,那么所有的線程將被終止,新的進(jìn)程映像被加載執(zhí)行。

這里需要解釋一下進(jìn)程映像(Process image) 的概念

什么是進(jìn)程映像呢?進(jìn)程映像是執(zhí)行程序時(shí)所需要的可執(zhí)行文件,通常會(huì)包括下面這些東西

  • 代碼段(codesegment/textsegment)

又稱文本段,用來存放指令,運(yùn)行代碼的一塊內(nèi)存空間

此空間大小在代碼運(yùn)行前就已經(jīng)確定

內(nèi)存空間一般屬于只讀,某些架構(gòu)的代碼也允許可寫

在代碼段中,也有可能包含一些只讀的常數(shù)變量,例如字符串常量等。

  • 數(shù)據(jù)段(datasegment)

可讀可寫

存儲(chǔ)初始化的全局變量和初始化的 static 變量

數(shù)據(jù)段中數(shù)據(jù)的生存期是隨程序持續(xù)性(隨進(jìn)程持續(xù)性)
隨進(jìn)程持續(xù)性:進(jìn)程創(chuàng)建就存在,進(jìn)程死亡就消失

  • bss 段(bsssegment):

可讀可寫

存儲(chǔ)未初始化的全局變量和未初始化的 static 變量

bss 段中的數(shù)據(jù)一般默認(rèn)為 0

  • Data 段

是可讀寫的,因?yàn)樽兞康闹悼梢栽谶\(yùn)行時(shí)更改。此段的大小也固定。

  • 棧(stack):

可讀可寫

存儲(chǔ)的是函數(shù)或代碼中的局部變量(非 static 變量)

棧的生存期隨代碼塊持續(xù)性,代碼塊運(yùn)行就給你分配空間,代碼塊結(jié)束,就自動(dòng)回收空間

  • 堆(heap):

可讀可寫

存儲(chǔ)的是程序運(yùn)行期間動(dòng)態(tài)分配的 malloc/realloc 的空間

堆的生存期隨進(jìn)程持續(xù)性,從 malloc/realloc 到 free 一直存在

下面是這些區(qū)域的構(gòu)成圖

全方位剖析 Linux 操作系統(tǒng),太全了

 

exec 系統(tǒng)調(diào)用是一些函數(shù)的集合,這些函數(shù)是

  • execl
  • execle
  • execlp
  • execv
  • execve
  • execvp

下面來看一下 exec 的工作原理

  1. 當(dāng)前進(jìn)程映像被替換為新的進(jìn)程映像
  2. 新的進(jìn)程映像是你做為 exec 傳遞的燦睡
  3. 結(jié)束當(dāng)前正在運(yùn)行的進(jìn)程
  4. 新的進(jìn)程映像有 PID,相同的環(huán)境和一些文件描述符(因?yàn)槲刺鎿Q進(jìn)程,只是替換了進(jìn)程映像)
  5. 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)用如下

系統(tǒng)調(diào)用指令描述pause掛起信號(hào)nice改變分時(shí)進(jìn)程的優(yōu)先級(jí)ptrace進(jìn)程跟蹤kill向進(jìn)程發(fā)送信號(hào)pipe創(chuàng)建管道m(xù)kfifo創(chuàng)建 fifo 的特殊文件(命名管道)sigaction設(shè)置對(duì)指定信號(hào)的處理方法msgctl消息控制操作semctl信號(hào)量控制

Linux 進(jìn)程和線程的實(shí)現(xiàn)

Linux 進(jìn)程

在 Linux 內(nèi)核結(jié)構(gòu)中,進(jìn)程會(huì)被表示為 任務(wù),通過結(jié)構(gòu)體 structure 來創(chuàng)建。不像其他的操作系統(tǒng)會(huì)區(qū)分進(jìn)程、輕量級(jí)進(jìn)程和線程,Linux 統(tǒng)一使用任務(wù)結(jié)構(gòu)來代表執(zhí)行上下文。因此,對(duì)于每個(gè)單線程進(jìn)程來說,單線程進(jìn)程將用一個(gè)任務(wù)結(jié)構(gòu)表示,對(duì)于多線程進(jìn)程來說,將為每一個(gè)用戶級(jí)線程分配一個(gè)任務(wù)結(jié)構(gòu)。Linux 內(nèi)核是多線程的,并且內(nèi)核級(jí)線程不與任何用戶級(jí)線程相關(guān)聯(lián)。

對(duì)于每個(gè)進(jìn)程來說,在內(nèi)存中都會(huì)有一個(gè) task_struct 進(jìn)程描述符與之對(duì)應(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)核會(huì)將所有進(jìn)程的任務(wù)結(jié)構(gòu)組成為一個(gè)雙向鏈表。PID 能夠直接被映射稱為進(jìn)程的任務(wù)結(jié)構(gòu)所在的地址,從而不需要遍歷雙向鏈表直接訪問。

我們上面提到了進(jìn)程描述符,這是一個(gè)非常重要的概念,我們上面還提到了進(jìn)程描述符是位于內(nèi)存中的,這里我們省略了一句話,那就是進(jìn)程描述符是存在用戶的任務(wù)結(jié)構(gòu)中,當(dāng)進(jìn)程位于內(nèi)存并開始運(yùn)行時(shí),進(jìn)程描述符才會(huì)被調(diào)入內(nèi)存。

進(jìn)程位于內(nèi)存被稱為 PIM(Process In Memory) ,這是馮諾伊曼體系架構(gòu)的一種體現(xiàn),加載到內(nèi)存中并執(zhí)行的程序稱為進(jìn)程。簡(jiǎn)單來說,一個(gè)進(jìn)程就是正在執(zhí)行的程序。

進(jìn)程描述符可以歸為下面這幾類

  • 調(diào)度參數(shù)(scheduling parameters):進(jìn)程優(yōu)先級(jí)、最近消耗 CPU 的時(shí)間、最近睡眠時(shí)間一起決定了下一個(gè)需要運(yùn)行的進(jìn)程
  • 內(nèi)存映像(memory image):我們上面說到,進(jìn)程映像是執(zhí)行程序時(shí)所需要的可執(zhí)行文件,它由數(shù)據(jù)和代碼組成。
  • 信號(hào)(signals):顯示哪些信號(hào)被捕獲、哪些信號(hào)被執(zhí)行
  • 寄存器:當(dāng)發(fā)生內(nèi)核陷入 (trap) 時(shí),寄存器的內(nèi)容會(huì)被保存下來。
  • 系統(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)用時(shí),文件描述符作為索引在文件描述符表中定位相關(guān)文件的 i-node 數(shù)據(jù)結(jié)構(gòu)
  • 統(tǒng)計(jì)數(shù)據(jù)(accounting):記錄用戶、進(jìn)程占用系統(tǒng) CPU 時(shí)間表的指針,一些操作系統(tǒng)還保存進(jìn)程最多占用的 CPU 時(shí)間、進(jìn)程擁有的最大堆??臻g、進(jìn)程可以消耗的頁(yè)面數(shù)等。
  • 內(nèi)核堆棧(kernel stack):進(jìn)程的內(nèi)核部分可以使用的固定堆棧
  • 其他: 當(dāng)前進(jìn)程狀態(tài)、事件等待時(shí)間、距離警報(bào)的超時(shí)時(shí)間、PID、父進(jìn)程的 PID 以及用戶標(biāo)識(shí)符等

有了上面這些信息,現(xiàn)在就很容易描述在 Linux 中是如何創(chuàng)建這些進(jìn)程的了,創(chuàng)建新流程實(shí)際上非常簡(jiǎn)單。為子進(jìn)程開辟一塊新的用戶空間的進(jìn)程描述符,然后從父進(jìn)程復(fù)制大量的內(nèi)容。為這個(gè)子進(jìn)程分配一個(gè) PID,設(shè)置其內(nèi)存映射,賦予它訪問父進(jìn)程文件的權(quán)限,注冊(cè)并啟動(dòng)。

當(dāng)執(zhí)行 fork 系統(tǒng)調(diào)用時(shí),調(diào)用進(jìn)程會(huì)陷入內(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

這個(gè)結(jié)構(gòu)中包含進(jìn)程描述符,進(jìn)程描述符位于固定的位置,使得 Linux 系統(tǒng)只需要很小的開銷就可以定位到一個(gè)運(yùn)行中進(jìn)程的數(shù)據(jù)結(jié)構(gòu)。

進(jìn)程描述符的主要內(nèi)容是根據(jù)父進(jìn)程的描述符來填充。Linux 操作系統(tǒng)會(huì)尋找一個(gè)可用的 PID,并且此 PID 沒有被任何進(jìn)程使用,更新進(jìn)程標(biāo)示符使其指向一個(gè)新的數(shù)據(jù)結(jié)構(gòu)即可。為了減少 hash table 的碰撞,進(jìn)程描述符會(huì)形成鏈表。它還將 task_struct 的字段設(shè)置為指向任務(wù)數(shù)組上相應(yīng)的上一個(gè)/下一個(gè)進(jìn)程。

task_struct : Linux 進(jìn)程描述符,內(nèi)部涉及到眾多 C++ 源碼,我們會(huì)在后面進(jìn)行講解。

從原則上來說,為子進(jìn)程開辟內(nèi)存區(qū)域并為子進(jìn)程分配數(shù)據(jù)段、堆棧段,并且對(duì)父進(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)程分配頁(yè)表,然后新分配的頁(yè)表指向父進(jìn)程的頁(yè)面,同時(shí)這些頁(yè)面是只讀的。當(dāng)進(jìn)程向這些頁(yè)面進(jìn)行寫入的時(shí)候,會(huì)開啟保護(hù)錯(cuò)誤。內(nèi)核發(fā)現(xiàn)寫入操作后,會(huì)為進(jìn)程分配一個(gè)副本,使得寫入時(shí)把數(shù)據(jù)復(fù)制到這個(gè)副本上,這個(gè)副本是共享的,這種方式稱為 寫入時(shí)復(fù)制(copy on write),這種方式避免了在同一塊內(nèi)存區(qū)域維護(hù)兩個(gè)副本的必要,節(jié)省內(nèi)存空間。

在子進(jìn)程開始運(yùn)行后,操作系統(tǒng)會(huì)調(diào)用 exec 系統(tǒng)調(diào)用,內(nèi)核會(huì)進(jìn)行查找驗(yàn)證可執(zhí)行文件,把參數(shù)和環(huán)境變量復(fù)制到內(nèi)核,釋放舊的地址空間。

現(xiàn)在新的地址空間需要被創(chuàng)建和填充。如果系統(tǒng)支持映射文件,就像 Unix 系統(tǒng)一樣,那么新的頁(yè)表就會(huì)創(chuàng)建,表明內(nèi)存中沒有任何頁(yè),除非所使用的頁(yè)面是堆棧頁(yè),其地址空間由磁盤上的可執(zhí)行文件支持。新進(jìn)程開始運(yùn)行時(shí),立刻會(huì)收到一個(gè)缺頁(yè)異常(page fault),這會(huì)使具有代碼的頁(yè)面加載進(jìn)入內(nèi)存。最后,參數(shù)和環(huán)境變量被復(fù)制到新的堆棧中,重置信號(hào),寄存器全部清零。新的命令開始運(yùn)行。

下面是一個(gè)示例,用戶輸出 ls,shell 會(huì)調(diào)用 fork 函數(shù)復(fù)制一個(gè)新進(jìn)程,shell 進(jìn)程會(huì)調(diào)用 exec 函數(shù)用可執(zhí)行文件 ls 的內(nèi)容覆蓋它的內(nèi)存。

全方位剖析 Linux 操作系統(tǒng),太全了

 

Linux 線程

現(xiàn)在我們來討論一下 Linux 中的線程,線程是輕量級(jí)的進(jìn)程,想必這句話你已經(jīng)聽過很多次了,輕量級(jí)體現(xiàn)在所有的進(jìn)程切換都需要清除所有的表、進(jìn)程間的共享信息也比較麻煩,一般來說通過管道或者共享內(nèi)存,如果是 fork 函數(shù)后的父子進(jìn)程則使用共享文件,然而線程切換不需要像進(jìn)程一樣具有昂貴的開銷,而且線程通信起來也更方便。線程分為兩種:用戶級(jí)線程和內(nèi)核級(jí)線程

用戶級(jí)線程

用戶級(jí)線程避免使用內(nèi)核,通常,每個(gè)線程會(huì)顯示調(diào)用開關(guān),發(fā)送信號(hào)或者執(zhí)行某種切換操作來放棄 CPU,同樣,計(jì)時(shí)器可以強(qiáng)制進(jìn)行開關(guān),用戶線程的切換速度通常比內(nèi)核線程快很多。在用戶級(jí)別實(shí)現(xiàn)線程會(huì)有一個(gè)問題,即單個(gè)線程可能會(huì)壟斷 CPU 時(shí)間片,導(dǎo)致其他線程無法執(zhí)行從而 餓死。如果執(zhí)行一個(gè) I/O 操作,那么 I/O 會(huì)阻塞,其他線程也無法運(yùn)行。

全方位剖析 Linux 操作系統(tǒng),太全了

 

一種解決方案是,一些用戶級(jí)的線程包解決了這個(gè)問題??梢允褂脮r(shí)鐘周期的監(jiān)視器來控制第一時(shí)間時(shí)間片獨(dú)占。然后,一些庫(kù)通過特殊的包裝來解決系統(tǒng)調(diào)用的 I/O 阻塞問題,或者可以為非阻塞 I/O 編寫任務(wù)。

內(nèi)核級(jí)線程

內(nèi)核級(jí)線程通常使用幾個(gè)進(jìn)程表在內(nèi)核中實(shí)現(xiàn),每個(gè)任務(wù)都會(huì)對(duì)應(yīng)一個(gè)進(jìn)程表。在這種情況下,內(nèi)核會(huì)在每個(gè)進(jìn)程的時(shí)間片內(nèi)調(diào)度每個(gè)線程。

全方位剖析 Linux 操作系統(tǒng),太全了

 

所有能夠阻塞的調(diào)用都會(huì)通過系統(tǒng)調(diào)用的方式來實(shí)現(xiàn),當(dāng)一個(gè)線程阻塞時(shí),內(nèi)核可以進(jìn)行選擇,是運(yùn)行在同一個(gè)進(jìn)程中的另一個(gè)線程(如果有就緒線程的話)還是運(yùn)行一個(gè)另一個(gè)進(jìn)程中的線程。

從用戶空間 -> 內(nèi)核空間 -> 用戶空間的開銷比較大,但是線程初始化的時(shí)間損耗可以忽略不計(jì)。這種實(shí)現(xiàn)的好處是由時(shí)鐘決定線程切換時(shí)間,因此不太可能將時(shí)間片與任務(wù)中的其他線程占用時(shí)間綁定到一起。同樣,I/O 阻塞也不是問題。

混合實(shí)現(xiàn)

結(jié)合用戶空間和內(nèi)核空間的優(yōu)點(diǎn),設(shè)計(jì)人員采用了一種內(nèi)核級(jí)線程的方式,然后將用戶級(jí)線程與某些或者全部?jī)?nèi)核線程多路復(fù)用起來

全方位剖析 Linux 操作系統(tǒng),太全了

 

在這種模型中,編程人員可以自由控制用戶線程和內(nèi)核線程的數(shù)量,具有很大的靈活度。采用這種方法,內(nèi)核只識(shí)別內(nèi)核級(jí)線程,并對(duì)其進(jìn)行調(diào)度。其中一些內(nèi)核級(jí)線程會(huì)被多個(gè)用戶級(jí)線程多路復(fù)用。

Linux 調(diào)度

下面我們來關(guān)注一下 Linux 系統(tǒng)的調(diào)度算法,首先需要認(rèn)識(shí)到,Linux 系統(tǒng)的線程是內(nèi)核線程,所以 Linux 系統(tǒng)是基于線程的,而不是基于進(jìn)程的。

為了進(jìn)行調(diào)度,Linux 系統(tǒng)將線程分為三類

  • 實(shí)時(shí)先入先出
  • 實(shí)時(shí)輪詢
  • 分時(shí)

實(shí)時(shí)先入先出線程具有最高優(yōu)先級(jí),它不會(huì)被其他線程所搶占,除非那是一個(gè)剛剛準(zhǔn)備好的,擁有更高優(yōu)先級(jí)的線程進(jìn)入。實(shí)時(shí)輪轉(zhuǎn)線程與實(shí)時(shí)先入先出線程基本相同,只是每個(gè)實(shí)時(shí)輪轉(zhuǎn)線程都有一個(gè)時(shí)間量,時(shí)間到了之后就可以被搶占。如果多個(gè)實(shí)時(shí)線程準(zhǔn)備完畢,那么每個(gè)線程運(yùn)行它時(shí)間量所規(guī)定的時(shí)間,然后插入到實(shí)時(shí)輪轉(zhuǎn)線程末尾。

注意這個(gè)實(shí)時(shí)只是相對(duì)的,無法做到絕對(duì)的實(shí)時(shí),因?yàn)榫€程的運(yùn)行時(shí)間無法確定。它們相對(duì)分時(shí)系統(tǒng)來說,更加具有實(shí)時(shí)性

Linux 系統(tǒng)會(huì)給每個(gè)線程分配一個(gè) nice 值,這個(gè)值代表了優(yōu)先級(jí)的概念。nice 值默認(rèn)值是 0 ,但是可以通過系統(tǒng)調(diào)用 nice 值來修改。修改值的范圍從 -20 - +19。nice 值決定了線程的靜態(tài)優(yōu)先級(jí)。一般系統(tǒng)管理員的 nice 值會(huì)比一般線程的優(yōu)先級(jí)高,它的范圍是 -20 - -1。

下面我們更詳細(xì)的討論一下 Linux 系統(tǒng)的兩個(gè)調(diào)度算法,它們的內(nèi)部與調(diào)度隊(duì)列(runqueue) 的設(shè)計(jì)很相似。運(yùn)行隊(duì)列有一個(gè)數(shù)據(jù)結(jié)構(gòu)用來監(jiān)視系統(tǒng)中所有可運(yùn)行的任務(wù)并選擇下一個(gè)可以運(yùn)行的任務(wù)。每個(gè)運(yùn)行隊(duì)列和系統(tǒng)中的每個(gè) CPU 有關(guān)。

Linux O(1) 調(diào)度器是歷史上很流行的一個(gè)調(diào)度器。這個(gè)名字的由來是因?yàn)樗軌蛟诔?shù)時(shí)間內(nèi)執(zhí)行任務(wù)調(diào)度。在 O(1) 調(diào)度器里,調(diào)度隊(duì)列被組織成兩個(gè)數(shù)組,一個(gè)是任務(wù)正在活動(dòng)的數(shù)組,一個(gè)是任務(wù)過期失效的數(shù)組。如下圖所示,每個(gè)數(shù)組都包含了 140 個(gè)鏈表頭,每個(gè)鏈表頭具有不同的優(yōu)先級(jí)。

全方位剖析 Linux 操作系統(tǒng),太全了

 

大致流程如下:

調(diào)度器從正在活動(dòng)數(shù)組中選擇一個(gè)優(yōu)先級(jí)最高的任務(wù)。如果這個(gè)任務(wù)的時(shí)間片過期失效了,就把它移動(dòng)到過期失效數(shù)組中。如果這個(gè)任務(wù)阻塞了,比如說正在等待 I/O 事件,那么在它的時(shí)間片過期失效之前,一旦 I/O 操作完成,那么這個(gè)任務(wù)將會(huì)繼續(xù)運(yùn)行,它將被放回到之前正在活動(dòng)的數(shù)組中,因?yàn)檫@個(gè)任務(wù)之前已經(jīng)消耗一部分 CPU 時(shí)間片,所以它將運(yùn)行剩下的時(shí)間片。當(dāng)這個(gè)任務(wù)運(yùn)行完它的時(shí)間片后,它就會(huì)被放到過期失效數(shù)組中。一旦正在活動(dòng)的任務(wù)數(shù)組中沒有其他任務(wù)后,調(diào)度器將會(huì)交換指針,使得正在活動(dòng)的數(shù)組變?yōu)檫^期失效數(shù)組,過期失效數(shù)組變?yōu)檎诨顒?dòng)的數(shù)組。使用這種方式可以保證每個(gè)優(yōu)先級(jí)的任務(wù)都能夠得到執(zhí)行,不會(huì)導(dǎo)致線程饑餓。

在這種調(diào)度方式中,不同優(yōu)先級(jí)的任務(wù)所得到 CPU 分配的時(shí)間片也是不同的,高優(yōu)先級(jí)進(jìn)程往往能得到較長(zhǎng)的時(shí)間片,低優(yōu)先級(jí)的任務(wù)得到較少的時(shí)間片。

這種方式為了保證能夠更好的提供服務(wù),通常會(huì)為 交互式進(jìn)程 賦予較高的優(yōu)先級(jí),交互式進(jìn)程就是用戶進(jìn)程。

Linux 系統(tǒng)不知道一個(gè)任務(wù)究竟是 I/O 密集型的還是 CPU 密集型的,它只是依賴于交互式的方式,Linux 系統(tǒng)會(huì)區(qū)分是靜態(tài)優(yōu)先級(jí) 還是 動(dòng)態(tài)優(yōu)先級(jí)。動(dòng)態(tài)優(yōu)先級(jí)是采用一種獎(jiǎng)勵(lì)機(jī)制來實(shí)現(xiàn)的。獎(jiǎng)勵(lì)機(jī)制有兩種方式:獎(jiǎng)勵(lì)交互式線程、懲罰占用 CPU 的線程。在 Linux O(1) 調(diào)度器中,最高的優(yōu)先級(jí)獎(jiǎng)勵(lì)是 -5,注意這個(gè)優(yōu)先級(jí)越低越容易被線程調(diào)度器接受,所以最高懲罰的優(yōu)先級(jí)是 +5。具體體現(xiàn)就是操作系統(tǒng)維護(hù)一個(gè)名為 sleep_avg 的變量,任務(wù)喚醒會(huì)增加 sleep_avg 變量的值,當(dāng)任務(wù)被搶占或者時(shí)間量過期會(huì)減少這個(gè)變量的值,反映在獎(jiǎng)勵(lì)機(jī)制上。

O(1) 調(diào)度算法是 2.6 內(nèi)核版本的調(diào)度器,最初引入這個(gè)調(diào)度算法的是不穩(wěn)定的 2.5 版本。早期的調(diào)度算法在多處理器環(huán)境中說明了通過訪問正在活動(dòng)數(shù)組就可以做出調(diào)度的決定。使調(diào)度可以在固定的時(shí)間 O(1) 完成。

O(1) 調(diào)度器使用了一種 啟發(fā)式 的方式,這是什么意思?

在計(jì)算機(jī)科學(xué)中,啟發(fā)式是一種當(dāng)傳統(tǒng)方式解決問題很慢時(shí)用來快速解決問題的方式,或者找到一個(gè)在傳統(tǒng)方法無法找到任何精確解的情況下找到近似解。

O(1) 使用啟發(fā)式的這種方式,會(huì)使任務(wù)的優(yōu)先級(jí)變得復(fù)雜并且不完善,從而導(dǎo)致在處理交互任務(wù)時(shí)性能很糟糕。

為了改進(jìn)這個(gè)缺點(diǎn),O(1) 調(diào)度器的開發(fā)者又提出了一個(gè)新的方案,即 公平調(diào)度器(Completely Fair Scheduler, CFS)。 CFS 的主要思想是使用一顆紅黑樹作為調(diào)度隊(duì)列。

數(shù)據(jù)結(jié)構(gòu)太重要了。

CFS 會(huì)根據(jù)任務(wù)在 CPU 上的運(yùn)行時(shí)間長(zhǎng)短而將其有序地排列在樹中,時(shí)間精確到納秒級(jí)。下面是 CFS 的構(gòu)造模型

全方位剖析 Linux 操作系統(tǒng),太全了

 

CFS 的調(diào)度過程如下:

CFS 算法總是優(yōu)先調(diào)度哪些使用 CPU 時(shí)間最少的任務(wù)。最小的任務(wù)一般都是在最左邊的位置。當(dāng)有一個(gè)新的任務(wù)需要運(yùn)行時(shí),CFS 會(huì)把這個(gè)任務(wù)和最左邊的數(shù)值進(jìn)行對(duì)比,如果此任務(wù)具有最小時(shí)間值,那么它將進(jìn)行運(yùn)行,否則它會(huì)進(jìn)行比較,找到合適的位置進(jìn)行插入。然后 CPU 運(yùn)行紅黑樹上當(dāng)前比較的最左邊的任務(wù)。

在紅黑樹中選擇一個(gè)節(jié)點(diǎn)來運(yùn)行的時(shí)間可以是常數(shù)時(shí)間,但是插入一個(gè)任務(wù)的時(shí)間是 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ù)被放入一個(gè)等待隊(duì)列中。等待隊(duì)列頭包含一個(gè)指向任務(wù)鏈表的指針和一個(gè)自旋鎖。自旋鎖對(duì)于并發(fā)處理場(chǎng)景下用處很大。

Linux 系統(tǒng)中的同步

下面來聊一下 Linux 中的同步機(jī)制。早期的 Linux 內(nèi)核只有一個(gè) 大內(nèi)核鎖(Big Kernel Lock,BKL) 。它阻止了不同處理器并發(fā)處理的能力。因此,需要引入一些粒度更細(xì)的鎖機(jī)制。

Linux 提供了若干不同類型的同步變量,這些變量既能夠在內(nèi)核中使用,也能夠在用戶應(yīng)用程序中使用。在地層中,Linux 通過使用 atomic_setatomic_read 這樣的操作為硬件支持的原子指令提供封裝。硬件提供內(nèi)存重排序,這是 Linux 屏障的機(jī)制。

具有高級(jí)別的同步像是自旋鎖的描述是這樣的,當(dāng)兩個(gè)進(jìn)程同時(shí)對(duì)資源進(jìn)行訪問,在一個(gè)進(jìn)程獲得資源后,另一個(gè)進(jìn)程不想被阻塞,所以它就會(huì)自旋,等待一會(huì)兒再對(duì)資源進(jìn)行訪問。Linux 也提供互斥量或信號(hào)量這樣的機(jī)制,也支持像是 mutex_tryLockmutex_tryWait 這樣的非阻塞調(diào)用。也支持中斷處理事務(wù),也可以通過動(dòng)態(tài)禁用和啟用相應(yīng)的中斷來實(shí)現(xiàn)。

Linux 啟動(dòng)

下面來聊一聊 Linux 是如何啟動(dòng)的。

當(dāng)計(jì)算機(jī)電源通電后,BIOS會(huì)進(jìn)行開機(jī)自檢(Power-On-Self-Test, POST),對(duì)硬件進(jìn)行檢測(cè)和初始化。因?yàn)椴僮飨到y(tǒng)的啟動(dòng)會(huì)使用到磁盤、屏幕、鍵盤、鼠標(biāo)等設(shè)備。下一步,磁盤中的第一個(gè)分區(qū),也被稱為 MBR(Master Boot Record) 主引導(dǎo)記錄,被讀入到一個(gè)固定的內(nèi)存區(qū)域并執(zhí)行。這個(gè)分區(qū)中有一個(gè)非常小的,只有 512 字節(jié)的程序。程序從磁盤中調(diào)入 boot 獨(dú)立程序,boot 程序?qū)⒆陨韽?fù)制到高位地址的內(nèi)存從而為操作系統(tǒng)釋放低位地址的內(nèi)存。

復(fù)制完成后,boot 程序讀取啟動(dòng)設(shè)備的根目錄。boot 程序要理解文件系統(tǒng)和目錄格式。然后 boot 程序被調(diào)入內(nèi)核,把控制權(quán)移交給內(nèi)核。直到這里,boot 完成了它的工作。系統(tǒng)內(nèi)核開始運(yùn)行。

內(nèi)核啟動(dòng)代碼是使用匯編語(yǔ)言完成的,主要包括創(chuàng)建內(nèi)核堆棧、識(shí)別 CPU 類型、計(jì)算內(nèi)存、禁用中斷、啟動(dòng)內(nèi)存管理單元等,然后調(diào)用 C 語(yǔ)言的 main 函數(shù)執(zhí)行操作系統(tǒng)部分。

這部分也會(huì)做很多事情,首先會(huì)分配一個(gè)消息緩沖區(qū)來存放調(diào)試出現(xiàn)的問題,調(diào)試信息會(huì)寫入緩沖區(qū)。如果調(diào)試出現(xiàn)錯(cuò)誤,這些信息可以通過診斷程序調(diào)出來。

然后操作系統(tǒng)會(huì)進(jìn)行自動(dòng)配置,檢測(cè)設(shè)備,加載配置文件,被檢測(cè)設(shè)備如果做出響應(yīng),就會(huì)被添加到已鏈接的設(shè)備表中,如果沒有相應(yīng),就歸為未連接直接忽略。

配置完所有硬件后,接下來要做的就是仔細(xì)手工處理進(jìn)程0,設(shè)置其堆棧,然后運(yùn)行它,執(zhí)行初始化、配置時(shí)鐘、掛載文件系統(tǒng)。創(chuàng)建 init 進(jìn)程(進(jìn)程 1 )守護(hù)進(jìn)程(進(jìn)程 2)

init 進(jìn)程會(huì)檢測(cè)它的標(biāo)志以確定它是否為單用戶還是多用戶服務(wù)。在前一種情況中,它會(huì)調(diào)用 fork 函數(shù)創(chuàng)建一個(gè) shell 進(jìn)程,并且等待這個(gè)進(jìn)程結(jié)束。后一種情況調(diào)用 fork 函數(shù)創(chuàng)建一個(gè)運(yùn)行系統(tǒng)初始化的 shell 腳本(即 /etc/rc)的進(jìn)程,這個(gè)進(jìn)程可以進(jìn)行文件系統(tǒng)一致性檢測(cè)、掛載文件系統(tǒng)、開啟守護(hù)進(jìn)程等。

然后 /etc/rc 這個(gè)進(jìn)程會(huì)從 /etc/ttys 中讀取數(shù)據(jù),/etc/ttys 列出了所有的終端和屬性。對(duì)于每一個(gè)啟用的終端,這個(gè)進(jìn)程調(diào)用 fork 函數(shù)創(chuàng)建一個(gè)自身的副本,進(jìn)行內(nèi)部處理并運(yùn)行一個(gè)名為 getty 的程序。

getty 程序會(huì)在終端上輸入

login: 

等待用戶輸入用戶名,在輸入用戶名后,getty 程序結(jié)束,登陸程序 /bin/login 開始運(yùn)行。login 程序需要輸入密碼,并與保存在 /etc/passwd 中的密碼進(jìn)行對(duì)比,如果輸入正確,login 程序以用戶 shell 程序替換自身,等待第一個(gè)命令。如果不正確,login 程序要求輸入另一個(gè)用戶名。

整個(gè)系統(tǒng)啟動(dòng)過程如下

全方位剖析 Linux 操作系統(tǒng),太全了

 

Linux 內(nèi)存管理

Linux 內(nèi)存管理模型非常直接明了,因?yàn)?Linux 的這種機(jī)制使其具有可移植性并且能夠在內(nèi)存管理單元相差不大的機(jī)器下實(shí)現(xiàn) Linux,下面我們就來認(rèn)識(shí)一下 Linux 內(nèi)存管理是如何實(shí)現(xiàn)的。

基本概念

每個(gè) Linux 進(jìn)程都會(huì)有地址空間,這些地址空間由三個(gè)段區(qū)域組成:text 段、data 段、stack 段。下面是進(jìn)程地址空間的示例。

全方位剖析 Linux 操作系統(tǒng),太全了

 

數(shù)據(jù)段(data segment) 包含了程序的變量、字符串、數(shù)組和其他數(shù)據(jù)的存儲(chǔ)。數(shù)據(jù)段分為兩部分,已經(jīng)初始化的數(shù)據(jù)和尚未初始化的數(shù)據(jù)。其中尚未初始化的數(shù)據(jù)就是我們說的 BSS。數(shù)據(jù)段部分的初始化需要編譯就期確定的常量以及程序啟動(dòng)就需要一個(gè)初始值的變量。所有 BSS 部分中的變量在加載后被初始化為 0 。

代碼段(Text segment) 不一樣,data segment 數(shù)據(jù)段可以改變。程序總是修改它的變量。而且,許多程序需要在執(zhí)行時(shí)動(dòng)態(tài)分配空間。Linux 允許數(shù)據(jù)段隨著內(nèi)存的分配和回收從而增大或者減小。為了分配內(nèi)存,程序可以增加數(shù)據(jù)段的大小。在 C 語(yǔ)言中有一套標(biāo)準(zhǔn)庫(kù) malloc 經(jīng)常用于分配內(nèi)存。進(jìn)程地址空間描述符包含動(dòng)態(tài)分配的內(nèi)存區(qū)域稱為 堆(heap)。

第三部分段是 棧段(stack segment)。在大部分機(jī)器上,棧段會(huì)在虛擬內(nèi)存地址頂部地址位置處,并向低位置處(向地址空間為 0 處)拓展。舉個(gè)例子來說,在 32 位 x86 架構(gòu)的機(jī)器上,棧開始于 0xC0000000,這是用戶模式下進(jìn)程允許可見的 3GB 虛擬地址限制。如果棧一直增大到超過棧段后,就會(huì)發(fā)生硬件故障并把頁(yè)面下降一個(gè)頁(yè)面。

當(dāng)程序啟動(dòng)時(shí),棧區(qū)域并不是空的,相反,它會(huì)包含所有的 shell 環(huán)境變量以及為了調(diào)用它而向 shell 輸入的命令行。舉個(gè)例子,當(dāng)你輸入

cp cxuan lx 

時(shí),cp 程序會(huì)運(yùn)行并在棧中帶著字符串 cp cxuan lx ,這樣就能夠找出源文件和目標(biāo)文件的名稱。

當(dāng)兩個(gè)用戶運(yùn)行在相同程序中,例如編輯器(editor),那么就會(huì)在內(nèi)存中保持編輯器程序代碼的兩個(gè)副本,但是這種方式并不高效。Linux 系統(tǒng)支持共享文本段作為替代。下面圖中我們會(huì)看到 A 和 B 兩個(gè)進(jìn)程,它們有著相同的文本區(qū)域。

全方位剖析 Linux 操作系統(tǒng),太全了

 

數(shù)據(jù)段和棧段只有在 fork 之后才會(huì)共享,共享也是共享未修改過的頁(yè)面。如果任何一個(gè)都需要變大但是沒有相鄰空間容納的話,也不會(huì)有問題,因?yàn)橄噜彽奶摂M頁(yè)面不必映射到相鄰的物理頁(yè)面上。

除了動(dòng)態(tài)分配更多的內(nèi)存,Linux 中的進(jìn)程可以通過內(nèi)存映射文件來訪問文件數(shù)據(jù)。這個(gè)特性可以使我們把一個(gè)文件映射到進(jìn)程空間的一部分而該文件就可以像位于內(nèi)存中的字節(jié)數(shù)組一樣被讀寫。把一個(gè)文件映射進(jìn)來使得隨機(jī)讀寫比使用 read 和 write 之類的 I/O 系統(tǒng)調(diào)用要容易得多。共享庫(kù)的訪問就是使用了這種機(jī)制。如下所示

全方位剖析 Linux 操作系統(tǒng),太全了

 

我們可以看到兩個(gè)相同文件會(huì)被映射到相同的物理地址上,但是它們屬于不同的地址空間。

映射文件的優(yōu)點(diǎn)是,兩個(gè)或多個(gè)進(jìn)程可以同時(shí)映射到同一文件中,任意一個(gè)進(jìn)程對(duì)文件的寫操作對(duì)其他文件可見。通過使用映射臨時(shí)文件的方式,可以為多線程共享內(nèi)存提供高帶寬,臨時(shí)文件在進(jìn)程退出后消失。但是實(shí)際上,并沒有兩個(gè)相同的地址空間,因?yàn)槊總€(gè)進(jìn)程維護(hù)的打開文件和信號(hào)不同。

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)用如下

系統(tǒng)調(diào)用描述s = brk(addr)改變數(shù)據(jù)段大小a = mmap(addr,len,prot,flags,fd,offset)進(jìn)行映射s = unmap(addr,len)取消映射

如果遇到錯(cuò)誤,那么 s 的返回值是 -1,a 和 addr 是內(nèi)存地址,len 表示的是長(zhǎng)度,prot 表示的是控制保護(hù)位,flags 是其他標(biāo)志位,fd 是文件描述符,offset 是文件偏移量。

brk 通過給出超過數(shù)據(jù)段之外的第一個(gè)字節(jié)地址來指定數(shù)據(jù)段的大小。如果新的值要比原來的大,那么數(shù)據(jù)區(qū)會(huì)變得越來越大,反之會(huì)越來越小。

mmapunmap 系統(tǒng)調(diào)用會(huì)控制映射文件。mmp 的第一個(gè)參數(shù) addr 決定了文件映射的地址。它必須是頁(yè)面大小的倍數(shù)。如果參數(shù)是 0,系統(tǒng)會(huì)分配地址并返回 a。第二個(gè)參數(shù)是長(zhǎng)度,它告訴了需要映射多少字節(jié)。它也是頁(yè)面大小的倍數(shù)。prot 決定了映射文件的保護(hù)位,保護(hù)位可以標(biāo)記為 可讀、可寫、可執(zhí)行或者這些的結(jié)合。第四個(gè)參數(shù) flags 能夠控制文件是私有的還是可讀的以及 addr 是必須的還是只是進(jìn)行提示。第五個(gè)參數(shù) fd 是要映射的文件描述符。只有打開的文件是可以被映射的,因此如果想要進(jìn)行文件映射,必須打開文件;最后一個(gè)參數(shù) offset 會(huì)指示文件從什么時(shí)候開始,并不一定每次都要從零開始。

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)。通過在多個(gè)競(jìng)爭(zhēng)的進(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)中的每個(gè)進(jìn)程都會(huì)有自己的虛擬地址空間。這些虛擬地址空間彼此完全分開,因此運(yùn)行一個(gè)應(yīng)用程序的進(jìn)程不會(huì)影響另一個(gè)。并且,硬件虛擬內(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)中的每個(gè)正在運(yùn)行的進(jìn)程公平分配系統(tǒng)的物理內(nèi)存。

共享虛擬內(nèi)存

盡管虛擬內(nèi)存讓進(jìn)程有自己的內(nèi)存空間,但是有的時(shí)候你是需要共享內(nèi)存的。例如幾個(gè)進(jìn)程同時(shí)在 shell 中運(yùn)行,這會(huì)涉及到 IPC 的進(jìn)程間通信問題,這個(gè)時(shí)候你需要的是共享內(nèi)存來進(jìn)行信息傳遞而不是通過拷貝每個(gè)進(jìn)程的副本獨(dú)立運(yùn)行。

 

責(zé)任編輯:龐桂玉 來源: Linux中國(guó)
相關(guān)推薦

2012-08-27 09:50:12

2009-01-11 10:11:22

Linux操作系統(tǒng)測(cè)試

2009-12-09 17:25:19

Linux操作系統(tǒng)

2009-12-23 10:38:08

Linux操作系統(tǒng)

2016-12-14 14:43:11

ButterknifeAndroid

2010-02-06 14:02:26

Android操作系統(tǒng)

2010-03-03 10:38:59

2017-12-30 09:45:50

Linux發(fā)行版面向孩子

2016-12-28 09:41:45

2020-12-29 16:39:01

Linux代碼命令

2010-02-06 14:44:26

Android操作系統(tǒng)

2010-08-04 14:15:44

nfs服務(wù)

2009-12-14 17:46:40

Linux桌面操作系統(tǒng)

2009-10-20 14:37:34

VB.NET文件操作

2010-04-29 14:08:38

Unix操作系統(tǒng)

2019-03-19 15:28:30

Linux 系統(tǒng) 數(shù)據(jù)

2014-09-10 09:54:43

2009-12-16 09:43:12

Linux操作系統(tǒng)

2009-12-22 13:44:33

Linux操作系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)