1. 程式人生 > >全方位剖析 Linux 作業系統,太全了!!!

全方位剖析 Linux 作業系統,太全了!!!

## Linux 簡介 UNIX 是一個互動式系統,用於同時處理多程序和多使用者同時線上。為什麼要說 UNIX,那是因為 Linux 是由 UNIX 發展而來的,UNIX 是由程式設計師設計,它的主要服務物件也是程式設計師。Linux 繼承了 UNIX 的設計目標。從智慧手機到汽車,超級計算機和家用電器,從家用桌上型電腦到企業伺服器,Linux 作業系統無處不在。 大多數程式設計師都喜歡讓系統儘量簡單,優雅並具有一致性。舉個例子,從最底層的角度來講,一個檔案應該只是一個位元組集合。為了實現順序存取、隨機存取、按鍵存取、遠端存取只能是妨礙你的工作。相同的,如果命令 ```shell ls A* ``` 意味著只列出以 A 為開頭的所有檔案,那麼命令 ```shell rm A* ``` 應該會移除所有以 A 為開頭的檔案而不是隻刪除檔名是 `A*` 的檔案。這個特性也是`最小吃驚原則(principle of least surprise)` >最小吃驚原則一半常用於使用者介面和軟體設計。它的原型是:該功能或者特徵應該符合使用者的預期,不應該使使用者感到驚訝和震驚。 一些有經驗的程式設計師通常希望系統具有較強的功能性和靈活性。設計 Linux 的一個基本目標是每個應用程式只做一件事情並把他做好。所以編譯器只負責編譯的工作,編譯器不會產生列表,因為有其他應用比編譯器做的更好。 很多人都不喜歡冗餘,為什麼在 cp 就能描述清楚你想幹什麼時候還使用 copy?這完全是在浪費寶貴的 `hacking time`。為了從檔案中提取所有包含字串 `ard` 的行,Linux 程式設計師應該輸入 ```shell grep ard f ``` ### Linux 介面 Linux 系統是一種金字塔模型的系統,如下所示 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070233641-1312039505.png) 應用程式發起系統呼叫把引數放在暫存器中(有時候放在棧中),併發出 `trap` 系統陷入指令切換使用者態至核心態。因為不能直接在 C 中編寫 trap 指令,因此 C 提供了一個庫,庫中的函式對應著系統呼叫。有些函式是使用匯編編寫的,但是能夠從 C 中呼叫。每個函式首先把引數放在合適的位置然後執行系統呼叫指令。因此如果你想要執行 read 系統呼叫的話,C 程式會呼叫 read 函式庫來執行。這裡順便提一下,是由 POSIX 指定的庫介面而不是系統呼叫介面。也就是說,POSIX 會告訴一個標準系統應該提供哪些庫過程,它們的引數是什麼,它們必須做什麼以及它們必須返回什麼結果。 除了作業系統和系統呼叫庫外,Linux 作業系統還要提供一些標準程式,比如文字編輯器、編譯器、檔案操作工具等。直接和使用者打交道的是上面這些應用程式。因此我們可以說 Linux 具有三種不同的介面:**系統呼叫介面、庫函式介面和應用程式介面** Linux 中的 `GUI(Graphical User Interface)` 和 UNIX 中的非常相似,這種 GUI 建立一個桌面環境,包括視窗、目標和資料夾、工具欄和檔案拖拽功能。一個完整的 GUI 還包括視窗管理器以及各種應用程式。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070244927-1172503843.png) Linux 上的 GUI 由 X 視窗支援,主要組成部分是 X 伺服器、控制鍵盤、滑鼠、顯示器等。當在 Linux 上使用圖形介面時,使用者可以通過滑鼠點選執行程式或者開啟檔案,通過拖拽將檔案進行復制等。 ### Linux 組成部分 事實上,Linux 作業系統可以由下面這幾部分構成 * `載入程式(Bootloader)`:載入程式是管理計算機啟動過程的軟體,對於大多數使用者而言,只是彈出一個螢幕,但其實內部作業系統做了很多事情 * `核心(Kernel)`:核心是作業系統的核心,負責管理 CPU、記憶體和外圍裝置等。 * `初始化系統(Init System)`:這是一個引導使用者空間並負責控制守護程式的子系統。一旦從引導載入程式移交了初始引導,它就是用於管理引導過程的初始化系統。 * `後臺程序(Daemon)`:後臺程序顧名思義就是在後臺執行的程式,比如列印、聲音、排程等,它們可以在引導過程中啟動,也可以在登入桌面後啟動 * `圖形伺服器(Graphical server)`:這是在監視器上顯示圖形的子系統。通常將其稱為 X 伺服器或 X。 * `桌面環境(Desktop environment)`:這是使用者與之實際互動的部分,有很多桌面環境可供選擇,每個桌面環境都包含內建應用程式,比如檔案管理器、Web 瀏覽器、遊戲等 * `應用程式(Applications)`:桌面環境不提供完整的應用程式,就像 Windows 和 macOS 一樣,Linux 提供了成千上萬個可以輕鬆找到並安裝的高質量軟體。 ### Shell 儘管 Linux 應用程式提供了 GUI ,但是大部分程式設計師仍偏好於使用`命令列(command-line interface)`,稱為`shell`。使用者通常在 GUI 中啟動一個 shell 視窗然後就在 shell 視窗下進行工作。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070253705-671153863.png) shell 命令列使用速度快、功能更強大、而且易於擴充套件、並且不會帶來`肢體重複性勞損(RSI)`。 下面會介紹一些最簡單的 bash shell。當 shell 啟動時,它首先進行初始化,在螢幕上輸出一個 `提示符(prompt)`,通常是一個百分號或者美元符號,等待使用者輸入 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070300569-1672754686.png) 等使用者輸入一個命令後,shell 提取其中的第一個詞,這裡的詞指的是被空格或製表符分隔開的一連串字元。假定這個詞是將要執行程式的程式名,那麼就會搜尋這個程式,如果找到了這個程式就會執行它。然後 shell 會將自己掛起直到程式執行完畢,之後再嘗試讀入下一條指令。shell 也是一個普通的使用者程式。它的主要功能就是讀取使用者的輸入和顯示計算的輸出。shell 命令中可以包含引數,它們作為字串傳遞給所呼叫的程式。比如 ```shell cp src dest ``` 會呼叫 cp 應用程式幷包含兩個引數 `src` 和 `dest`。這個程式會解釋第一個引數是一個已經存在的檔名,然後建立一個該檔案的副本,名稱為 dest。 並不是所有的引數都是檔名,比如下面 ```shell head -20 file ``` 第一個引數 -20,會告訴 head 應用程式列印檔案的前 20 行,而不是預設的 10 行。控制命令操作或者指定可選值的引數稱為`標誌(flag)`,按照慣例標誌應該使用 `-` 來表示。這個符號是必要的,比如 ```shell head 20 file ``` 是一個完全合法的命令,它會告訴 head 程式輸出檔名為 20 的檔案的前 10 行,然後輸出檔名為 file 檔案的前 10 行。Linux 作業系統可以接受一個或多個引數。 為了更容易的指定多個檔名,shell 支援 `魔法字元(magic character)`,也被稱為`萬用字元(wild cards)`。比如,`*` 可以匹配一個或者多個可能的字串 ```shell ls *.c ``` 告訴 ls 列舉出所有檔名以 `.c` 結束的檔案。如果同時存在多個檔案,則會在後面進行並列。 另一個萬用字元是問號,負責匹配任意一個字元。一組在中括號中的字元可以表示其中任意一個,因此 ```shell ls [abc]* ``` 會列舉出所有以 `a`、`b` 或者 `c` 開頭的檔案。 shell 應用程式不一定通過終端進行輸入和輸出。shell 啟動時,就會獲取 **標準輸入、標準輸出、標準錯誤**檔案進行訪問的能力。 標準輸出是從鍵盤輸入的,標準輸出或者標準錯誤是輸出到顯示器的。許多 Linux 程式預設是從標準輸入進行輸入並從標準輸出進行輸出。比如 ```shell sort ``` 會呼叫 sort 程式,會從終端讀取資料(直到使用者輸入 ctrl-d 結束),根據字母順序進行排序,然後將結果輸出到螢幕上。 通常還可以重定向標準輸入和標準輸出,重定向標準輸入使用 `<` 後面跟檔名。標準輸出可以通過一個大於號 `>` 進行重定向。允許一個命令中重定向標準輸入和輸出。例如命令 ```shell sort out ``` 會使 sort 從檔案 in 中得到輸入,並把結果輸出到 out 檔案中。由於標準錯誤沒有重定向,所以錯誤資訊會直接列印到螢幕上。從標準輸入讀入,對其進行處理並將其寫入到標準輸出的程式稱為 `過濾器`。 考慮下面由三個分開的命令組成的指令 ```shell sort temp;head -30 f00 ``` 對任意以 `.t` 結尾的檔案中包含 `cxuan` 的行被寫到標準輸出中,然後進行排序。這些內容中的前 30 行被 head 出來並傳給 tail ,它又將最後 5 行傳遞給 foo。這個例子提供了一個管道將多個命令連線起來。 可以把一系列 shell 命令放在一個檔案中,然後將此檔案作為輸入來執行。shell 會按照順序對他們進行處理,就像在鍵盤上鍵入命令一樣。包含 shell 命令的檔案被稱為 `shell 指令碼(shell scripts)`。 >推薦一個 shell 命令的學習網站:https://www.shellscript.sh/ shell 指令碼其實也是一段程式,shell 指令碼中可以對變數進行賦值,也包含迴圈控制語句比如 **if、for、while** 等,shell 的設計目標是讓其看起來和 C 相似(There is no doubt that C is father)。由於 shell 也是一個使用者程式,所以使用者可以選擇不同的 shell。 ### Linux 應用程式 Linux 的命令列也就是 shell,它由大量標準應用程式組成。這些應用程式主要有下面六種 * 檔案和目錄操作命令 * 過濾器 * 文字程式 * 系統管理 * 程式開發工具,例如編輯器和編譯器 * 其他 除了這些標準應用程式外,還有其他應用程式比如 **Web 瀏覽器、多媒體播放器、圖片瀏覽器、辦公軟體和遊戲程式等**。 我們在上面的例子中已經見過了幾個 Linux 的應用程式,比如 sort、cp、ls、head,下面我們再來認識一下其他 Linux 的應用程式。 我們先從幾個例子開始講起,比如 ```shell cp a b ``` 是將 a 複製一個副本為 b ,而 ```shell mv a b ``` 是將 a 移動到 b ,但是刪除原檔案。 上面這兩個命令有一些區別,`cp` 是將檔案進行復制,複製完成後會有兩個檔案 a 和 b;而 `mv` 相當於是檔案的移動,移動完成後就不再有 a 檔案。`cat` 命令可以把多個檔案內容進行連線。使用 `rm` 可以刪除檔案;使用 `chmod` 可以允許所有者改變訪問許可權;檔案目錄的的建立和刪除可以使用 `mkdir` 和 `rmdir` 命令;使用 `ls` 可以檢視目錄檔案,ls 可以顯示很多屬性,比如大小、使用者、建立日期等;sort 決定檔案的顯示順序 Linux 應用程式還包括過濾器 grep,`grep` 從標準輸入或者一個或多個輸入檔案中提取特定模式的行;`sort` 將輸入進行排序並輸出到標準輸出;`head` 提取輸入的前幾行;tail 提取輸入的後面幾行;除此之外的過濾器還有 `cut` 和 `paste`,允許對文字行的剪下和複製;`od` 將輸入轉換為 ASCII ;`tr` 實現字元大小寫轉換;`pr` 為格式化列印輸出等。 程式編譯工具使用 `gcc `; `make` 命令用於自動編譯,這是一個很強大的命令,它用於維護一個大的程式,往往這類程式的原始碼由許多檔案構成。典型的,有一些是 `header files 標頭檔案`,原始檔通常使用 `include` 指令包含這些檔案,make 的作用就是跟蹤哪些檔案屬於標頭檔案,然後安排自動編譯的過程。 下面列出了 POSIX 的標準應用程式 | 程式 | 應用 | | ----- | ---------------------- | | ls | 列出目錄 | | cp | 複製檔案 | | head | 顯示檔案的前幾行 | | make | 編譯檔案生成二進位制檔案 | | cd | 切換目錄 | | mkdir | 建立目錄 | | chmod | 修改檔案訪問許可權 | | ps | 列出檔案程序 | | pr | 格式化列印 | | rm | 刪除一個檔案 | | rmdir | 刪除檔案目錄 | | tail | 提取檔案最後幾行 | | tr | 字符集轉換 | | grep | 分組 | | cat | 將多個檔案連續標準輸出 | | od | 以八進位制顯示檔案 | | cut | 從檔案中剪下 | | paste | 從檔案中貼上 | ### Linux 核心結構 在上面我們看到了 Linux 的整體結構,下面我們從整體的角度來看一下 Linux 的核心結構 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070321871-1490308607.png) 核心直接坐落在硬體上,核心的主要作用就是 I/O 互動、記憶體管理和控制 CPU 訪問。上圖中還包括了 `中斷` 和 `排程器`,中斷是與裝置互動的主要方式。中斷出現時排程器就會發揮作用。這裡的低階程式碼停止正在執行的程序,將其狀態儲存在核心程序結構中,並啟動驅動程式。程序排程也會發生在核心完成一些操作並且啟動使用者程序的時候。圖中的排程器是 dispatcher。 >注意這裡的排程器是 `dispatcher` 而不是 `scheduler`,這兩者是有區別的 > >scheduler 和 dispatcher 都是和程序排程相關的概念,不同的是 scheduler 會從幾個程序中隨意選取一個程序;而 dispatcher 會給 scheduler 選擇的程序分配 CPU。 然後,我們把核心系統分為三部分。 * I/O 部分負責與裝置進行互動以及執行網路和儲存 I/O 操作的所有核心部分。 從圖中可以看出 I/O 層次的關係,最高層是一個`虛擬檔案系統`,也就是說不管檔案是來自記憶體還是磁碟中,都是經過虛擬檔案系統中的。從底層看,所有的驅動都是字元驅動或者塊裝置驅動。二者的主要區別就是是否允許隨機訪問。網路驅動裝置並不是一種獨立的驅動裝置,它實際上是一種字元裝置,不過網路裝置的處理方式和字元裝置不同。 上面的裝置驅動程式中,每個裝置型別的核心程式碼都不同。字元裝置有兩種使用方式,有`一鍵式`的比如 vi 或者 emacs ,需要每一個鍵盤輸入。其他的比如 shell ,是需要輸入一行按回車鍵將字串傳送給程式進行編輯。 網路軟體通常是模組化的,由不同的裝置和協議來支援。大多數 Linux 系統在核心中包含一個完整的硬體路由器的功能,但是這個不能和外部路由器相比,路由器上面是`協議棧`,包括 TCP/IP 協議,協議棧上面是 socket 介面,socket 負責與外部進行通訊,充當了門的作用。 磁碟驅動上面是 I/O 排程器,它負責排序和分配磁碟讀寫操作,以儘可能減少磁頭的無用移動。 * I/O 右邊的是記憶體部件,程式被裝載進記憶體,由 CPU 執行,這裡會涉及到虛擬記憶體的部件,頁面的換入和換出是如何進行的,壞頁面的替換和經常使用的頁面會進行快取。 * 程序模組負責程序的建立和終止、程序的排程、Linux 把程序和執行緒看作是可執行的實體,並使用統一的排程策略來進行排程。 在核心最頂層的是系統呼叫介面,所有的系統呼叫都是經過這裡,系統呼叫會觸發一個 trap,將系統從使用者態轉換為核心態,然後將控制權移交給上面的核心部件。 ## Linux 程序和執行緒 下面我們就深入理解一下 Linux 核心來理解 Linux 的基本概念之程序和執行緒。系統呼叫是作業系統本身的介面,它對於建立程序和執行緒,記憶體分配,共享檔案和 I/O 來說都很重要。 我們將從各個版本的共性出發來進行探討。 ### 基本概念 每個程序都會執行一段獨立的程式,並且在初始化的時候擁有一個獨立的控制執行緒。換句話說,每個程序都會有一個自己的程式計數器,這個程式計數器用來記錄下一個需要被執行的指令。Linux 允許程序在執行時建立額外的執行緒。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070332421-1302383462.png) Linux 是一個多道程式設計系統,因此係統中存在彼此相互獨立的程序同時執行。此外,每個使用者都會同時有幾個活動的程序。因為如果是一個大型系統,可能有數百上千的程序在同時執行。 在某些使用者空間中,即使使用者退出登入,仍然會有一些後臺程序在執行,這些程序被稱為 `守護程序(daemon)`。 Linux 中有一種特殊的守護程序被稱為 `計劃守護程序(Cron daemon)` ,計劃守護程序可以每分鐘醒來一次檢查是否有工作要做,做完會繼續回到睡眠狀態等待下一次喚醒。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070340841-355969658.png) > Cron 是一個守護程式,可以做任何你想做的事情,比如說你可以定期進行系統維護、定期進行系統備份等。在其他作業系統上也有類似的程式,比如 Mac OS X 上 Cron 守護程式被稱為 `launchd` 的守護程序。在 Windows 上可以被稱為 `計劃任務(Task Scheduler)`。 在 Linux 系統中,程序通過非常簡單的方式來建立,`fork` 系統呼叫會建立一個源程序的`拷貝(副本)`。呼叫 fork 函式的程序被稱為 `父程序(parent process)`,使用 fork 函式創建出來的程序被稱為 `子程序(child process)`。父程序和子程序都有自己的記憶體映像。如果在子程序創建出來後,父程序修改了一些變數等,那麼子程序是看不到這些變化的,也就是 fork 後,父程序和子程序相互獨立。 雖然父程序和子程序保持相互獨立,但是它們卻能夠共享相同的檔案,如果在 fork 之前,父程序已經打開了某個檔案,那麼 fork 後,父程序和子程序仍然共享這個開啟的檔案。對共享檔案的修改會對父程序和子程序同時可見。 那麼該如何區分父程序和子程序呢?子程序只是父程序的拷貝,所以它們幾乎所有的情況都一樣,包括記憶體映像、變數、暫存器等。區分的關鍵在於 `fork ` 函式呼叫後的返回值,如果 fork 後返回一個非零值,這個非零值即是子程序的 `程序識別符號(Process Identiier, PID)`,而會給子程序返回一個零值,可以用下面程式碼來進行表示 ```c pid = fork(); // 呼叫 fork 函式建立程序 if(pid < 0){ error() // pid < 0,建立失敗 } else if(pid > 0){ parent_handle() // 父程序程式碼 } else { child_handle() // 子程序程式碼 } ``` 父程序在 fork 後會得到子程序的 PID,這個 PID 即能代表這個子程序的唯一識別符號也就是 PID。如果子程序想要知道自己的 PID,可以呼叫 `getpid` 方法。當子程序結束執行時,父程序會得到子程序的 PID,因為一個程序會 fork 很多子程序,子程序也會 fork 子程序,所以 PID 是非常重要的。我們把第一次呼叫 fork 後的程序稱為 `原始程序`,一個原始程序可以生成一顆繼承樹 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070355675-561579263.png) ### Linux 程序間通訊 Linux 程序間的通訊機制通常被稱為 `Internel-Process communication,IPC `下面我們來說一說 Linux 程序間通訊的機制,大致來說,Linux 程序間的通訊機制可以分為 6 種 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010071358913-1960224314.png) 下面我們分別對其進行概述 #### 訊號 signal 訊號是 UNIX 系統最先開始使用的程序間通訊機制,因為 Linux 是繼承於 UNIX 的,所以 Linux 也支援訊號機制,通過向一個或多個程序傳送`非同步事件訊號`來實現,訊號可以從鍵盤或者訪問不存在的位置等地方產生;訊號通過 shell 將任務傳送給子程序。 你可以在 Linux 系統上輸入 `kill -l` 來列出系統使用的訊號,下面是我提供的一些訊號 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070427949-1825126513.png) 程序可以選擇忽略傳送過來的訊號,但是有兩個是不能忽略的:`SIGSTOP` 和 `SIGKILL` 訊號。SIGSTOP 訊號會通知當前正在執行的程序執行關閉操作,SIGKILL 訊號會通知當前程序應該被殺死。除此之外,程序可以選擇它想要處理的訊號,程序也可以選擇阻止訊號,如果不阻止,可以選擇自行處理,也可以選擇進行核心處理。如果選擇交給核心進行處理,那麼就執行預設處理。 作業系統會中斷目標程式的程序來向其傳送訊號、在任何非原子指令中,執行都可以中斷,如果程序已經註冊了新號處理程式,那麼就執行程序,如果沒有註冊,將採用預設處理的方式。 例如:當程序收到 `SIGFPE` 浮點異常的訊號後,預設操作是對其進行 `dump(轉儲)`和退出。訊號沒有優先順序的說法。如果同時為某個程序產生了兩個訊號,則可以將它們呈現給程序或者以任意的順序進行處理。 下面我們就來看一下這些訊號是幹什麼用的 * SIGABRT 和 SIGIOT SIGABRT 和 SIGIOT 訊號傳送給程序,告訴其進行終止,這個 訊號通常在呼叫 C標準庫的`abort()`函式時由程序本身啟動 * SIGALRM 、 SIGVTALRM、SIGPROF 當設定的時鐘功能超時時會將 SIGALRM 、 SIGVTALRM、SIGPROF 傳送給程序。當實際時間或時鐘時間超時時,傳送 SIGALRM。 當程序使用的 CPU 時間超時時,將傳送 SIGVTALRM。 當程序和系統代表程序使用的CPU 時間超時時,將傳送 SIGPROF。 * SIGBUS SIGBUS 將造成`匯流排中斷`錯誤時傳送給程序 * SIGCHLD 當子程序終止、被中斷或者被中斷恢復,將 SIGCHLD 傳送給程序。此訊號的一種常見用法是指示作業系統在子程序終止後清除其使用的資源。 * SIGCONT SIGCONT 訊號指示作業系統繼續執行先前由 SIGSTOP 或 SIGTSTP 訊號暫停的程序。該訊號的一個重要用途是在 Unix shell 中的作業控制中。 * SIGFPE SIGFPE 訊號在執行錯誤的算術運算(例如除以零)時將被髮送到程序。 * SIGUP 當 SIGUP 訊號控制的終端關閉時,會發送給程序。許多守護程式將重新載入其配置檔案並重新開啟其日誌檔案,而不是在收到此訊號時退出。 * SIGILL SIGILL 訊號在嘗試執行非法、格式錯誤、未知或者特權指令時發出 * SIGINT 當用戶希望中斷程序時,作業系統會向程序傳送 SIGINT 訊號。使用者輸入 ctrl - c 就是希望中斷程序。 * SIGKILL SIGKILL 訊號傳送到程序以使其馬上進行終止。 與 SIGTERM 和 SIGINT 相比,這個訊號無法捕獲和忽略執行,並且程序在接收到此訊號後無法執行任何清理操作,下面是一些例外情況 殭屍程序無法殺死,因為殭屍程序已經死了,它在等待父程序對其進行捕獲 處於阻塞狀態的程序只有再次喚醒後才會被 kill 掉 `init` 程序是 Linux 的初始化程序,這個程序會忽略任何訊號。 SIGKILL 通常是作為最後殺死程序的訊號、它通常作用於 SIGTERM 沒有響應時傳送給程序。 * SIGPIPE SIGPIPE 嘗試寫入程序管道時發現管道未連線無法寫入時傳送到程序 * SIGPOLL 當在明確監視的檔案描述符上發生事件時,將傳送 SIGPOLL 訊號。 * SIGRTMIN 至 SIGRTMAX SIGRTMIN 至 SIGRTMAX 是`實時訊號` * SIGQUIT 當用戶請求退出程序並執行核心轉儲時,SIGQUIT 訊號將由其控制終端傳送給程序。 * SIGSEGV 當 SIGSEGV 訊號做出無效的虛擬記憶體引用或分段錯誤時,即在執行分段違規時,將其傳送到程序。 * SIGSTOP SIGSTOP 指示作業系統終止以便以後進行恢復時 * SIGSYS 當 SIGSYS 訊號將錯誤引數傳遞給系統呼叫時,該訊號將傳送到程序。 * SYSTERM 我們上面簡單提到過了 SYSTERM 這個名詞,這個訊號傳送給程序以請求終止。與 SIGKILL 訊號不同,該訊號可以被過程捕獲或忽略。這允許程序執行良好的終止,從而釋放資源並在適當時儲存狀態。 SIGINT 與SIGTERM 幾乎相同。 * SIGTSIP SIGTSTP 訊號由其控制終端傳送到程序,以請求終端停止。 * SIGTTIN 和 SIGTTOU 當 SIGTTIN 和SIGTTOU 訊號分別在後臺嘗試從 tty 讀取或寫入時,訊號將傳送到該程序。 * SIGTRAP 在發生異常或者 trap 時,將 SIGTRAP 訊號傳送到程序 * SIGURG 當套接字具有可讀取的緊急或帶外資料時,將 SIGURG 訊號傳送到程序。 * SIGUSR1 和 SIGUSR2 SIGUSR1 和 SIGUSR2 訊號被髮送到程序以指示使用者定義的條件。 * SIGXCPU 當 SIGXCPU 訊號耗盡 CPU 的時間超過某個使用者可設定的預定值時,將其傳送到程序 * SIGXFSZ 當 SIGXFSZ 訊號增長超過最大允許大小的檔案時,該訊號將傳送到該程序。 * SIGWINCH SIGWINCH 訊號在其控制終端更改其大小(視窗更改)時傳送給程序。 #### 管道 pipe Linux 系統中的程序可以通過建立管道 pipe 進行通訊。 在兩個程序之間,可以建立一個通道,一個程序向這個通道里寫入位元組流,另一個程序從這個管道中讀取位元組流。管道是同步的,當程序嘗試從空管道讀取資料時,該程序會被阻塞,直到有可用資料為止。shell 中的`管線 pipelines` 就是用管道實現的,當 shell 發現輸出 ```shell sort
`程序位於記憶體`被稱為 `PIM(Process In Memory)` ,這是馮諾伊曼體系架構的一種體現,載入到記憶體中並執行的程式稱為程序。簡單來說,一個程序就是正在執行的程式。 程序描述符可以歸為下面這幾類 * `排程引數(scheduling parameters)`:程序優先順序、最近消耗 CPU 的時間、最近睡眠時間一起決定了下一個需要執行的程序 * `記憶體映像(memory image)`:我們上面說到,程序映像是執行程式時所需要的可執行檔案,它由資料和程式碼組成。 * `訊號(signals)`:顯示哪些訊號被捕獲、哪些訊號被執行 * `暫存器`:當發生核心陷入 (trap) 時,暫存器的內容會被儲存下來。 * `系統呼叫狀態(system call state)`:當前系統呼叫的資訊,包括引數和結果 * `檔案描述符表(file descriptor table)`:有關檔案描述符的系統被呼叫時,檔案描述符作為索引在檔案描述符表中定位相關檔案的 i-node 資料結構 * `統計資料(accounting)`:記錄使用者、程序佔用系統 CPU 時間表的指標,一些作業系統還儲存程序最多佔用的 CPU 時間、程序擁有的最大堆疊空間、程序可以消耗的頁面數等。 * `核心堆疊(kernel stack)`:程序的核心部分可以使用的固定堆疊 * `其他`: 當前程序狀態、事件等待時間、距離警報的超時時間、PID、父程序的 PID 以及使用者識別符號等 有了上面這些資訊,現在就很容易描述在 Linux 中是如何建立這些程序的了,建立新流程實際上非常簡單。**為子程序開闢一塊新的使用者空間的程序描述符,然後從父程序複製大量的內容。為這個子程序分配一個 PID,設定其記憶體對映,賦予它訪問父程序檔案的許可權,註冊並啟動**。 當執行 fork 系統呼叫時,呼叫程序會陷入核心並建立一些和任務相關的資料結構,比如`核心堆疊(kernel stack)` 和 `thread_info` 結構。 >
關於 thread_info 結構可以參考 > >https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch_2avr32_2include_2asm_2thread__info_8h_source.html 這個結構中包含程序描述符,程序描述符位於固定的位置,使得 Linux 系統只需要很小的開銷就可以定位到一個執行中程序的資料結構。 程序描述符的主要內容是根據`父程序`的描述符來填充。Linux 作業系統會尋找一個可用的 PID,並且此 PID 沒有被任何程序使用,更新程序標示符使其指向一個新的資料結構即可。為了減少 hash table 的碰撞,程序描述符會形成`連結串列`。它還將 task_struct 的欄位設定為指向任務陣列上相應的上一個/下一個程序。 >
task_struct : Linux 程序描述符,內部涉及到眾多 C++ 原始碼,我們會在後面進行講解。 從原則上來說,為子程序開闢記憶體區域併為子程序分配資料段、堆疊段,並且對父程序的內容進行復制,但是實際上 fork 完成後,子程序和父程序沒有共享記憶體,所以需要複製技術來實現同步,但是複製開銷比較大,因此 Linux 作業系統使用了一種 `欺騙` 方式。即為子程序分配頁表,然後新分配的頁表指向父程序的頁面,同時這些頁面是隻讀的。當程序向這些頁面進行寫入的時候,會開啟保護錯誤。核心發現寫入操作後,會為程序分配一個副本,使得寫入時把資料複製到這個副本上,這個副本是共享的,這種方式稱為 `寫入時複製(copy on write)`,這種方式避免了在同一塊記憶體區域維護兩個副本的必要,節省記憶體空間。 在子程序開始執行後,作業系統會呼叫 exec 系統呼叫,核心會進行查詢驗證可執行檔案,把引數和環境變數複製到核心,釋放舊的地址空間。 現在新的地址空間需要被建立和填充。如果系統支援對映檔案,就像 Unix 系統一樣,那麼新的頁表就會建立,表明記憶體中沒有任何頁,除非所使用的頁面是堆疊頁,其地址空間由磁碟上的可執行檔案支援。新程序開始執行時,立刻會收到一個`缺頁異常(page fault)`,這會使具有程式碼的頁面載入進入記憶體。最後,引數和環境變數被複制到新的堆疊中,重置訊號,暫存器全部清零。新的命令開始執行。 下面是一個示例,使用者輸出 ls,shell 會呼叫 fork 函式複製一個新程序,shell 程序會呼叫 exec 函式用可執行檔案 ls 的內容覆蓋它的記憶體。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070537261-1218173422.png) #### Linux 執行緒 現在我們來討論一下 Linux 中的執行緒,執行緒是輕量級的程序,想必這句話你已經聽過很多次了,`輕量級`體現在所有的程序切換都需要清除所有的表、程序間的共享資訊也比較麻煩,一般來說通過管道或者共享記憶體,如果是 fork 函式後的父子程序則使用共享檔案,然而執行緒切換不需要像程序一樣具有昂貴的開銷,而且執行緒通訊起來也更方便。執行緒分為兩種:使用者級執行緒和核心級執行緒 #### 使用者級執行緒 使用者級執行緒避免使用核心,通常,每個執行緒會顯示呼叫開關,傳送訊號或者執行某種切換操作來放棄 CPU,同樣,計時器可以強制進行開關,使用者執行緒的切換速度通常比核心執行緒快很多。在使用者級別實現執行緒會有一個問題,即單個執行緒可能會壟斷 CPU 時間片,導致其他執行緒無法執行從而 `餓死`。如果執行一個 I/O 操作,那麼 I/O 會阻塞,其他執行緒也無法執行。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070547298-209131475.png) 一種解決方案是,一些使用者級的執行緒包解決了這個問題。可以使用時鐘週期的監視器來控制第一時間時間片獨佔。然後,一些庫通過特殊的包裝來解決系統呼叫的 I/O 阻塞問題,或者可以為非阻塞 I/O 編寫任務。 #### 核心級執行緒 核心級執行緒通常使用幾個程序表在核心中實現,每個任務都會對應一個程序表。在這種情況下,核心會在每個程序的時間片內排程每個執行緒。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070555156-403296964.png) 所有能夠阻塞的呼叫都會通過系統呼叫的方式來實現,當一個執行緒阻塞時,核心可以進行選擇,是執行在同一個程序中的另一個執行緒(如果有就緒執行緒的話)還是執行一個另一個程序中的執行緒。 從使用者空間 -> 核心空間 -> 使用者空間的開銷比較大,但是執行緒初始化的時間損耗可以忽略不計。這種實現的好處是由時鐘決定執行緒切換時間,因此不太可能將時間片與任務中的其他執行緒佔用時間繫結到一起。同樣,I/O 阻塞也不是問題。 #### 混合實現 結合使用者空間和核心空間的優點,設計人員採用了一種`核心級執行緒`的方式,然後將使用者級執行緒與某些或者全部核心執行緒多路複用起來 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070604256-1866299407.png) 在這種模型中,程式設計人員可以自由控制使用者執行緒和核心執行緒的數量,具有很大的靈活度。採用這種方法,核心只識別核心級執行緒,並對其進行排程。其中一些核心級執行緒會被多個使用者級執行緒多路複用。 ### Linux 排程 下面我們來關注一下 Linux 系統的排程演算法,首先需要認識到,Linux 系統的執行緒是核心執行緒,所以 Linux 系統是基於執行緒的,而不是基於程序的。 為了進行排程,Linux 系統將執行緒分為三類 * 實時先入先出 * 實時輪詢 * 分時 實時先入先出執行緒具有最高優先順序,它不會被其他執行緒所搶佔,除非那是一個剛剛準備好的,擁有更高優先順序的執行緒進入。實時輪轉執行緒與實時先入先出執行緒基本相同,只是每個實時輪轉執行緒都有一個時間量,時間到了之後就可以被搶佔。如果多個實時執行緒準備完畢,那麼每個執行緒執行它時間量所規定的時間,然後插入到實時輪轉執行緒末尾。 >注意這個實時只是相對的,無法做到絕對的實時,因為執行緒的執行時間無法確定。它們相對分時系統來說,更加具有實時性 Linux 系統會給每個執行緒分配一個 `nice` 值,這個值代表了優先順序的概念。nice 值預設值是 0 ,但是可以通過系統呼叫 nice 值來修改。修改值的範圍從 -20 - +19。nice 值決定了執行緒的靜態優先順序。一般系統管理員的 nice 值會比一般執行緒的優先順序高,它的範圍是 -20 - -1。 下面我們更詳細的討論一下 Linux 系統的兩個排程演算法,它們的內部與`排程佇列(runqueue)` 的設計很相似。執行佇列有一個數據結構用來監視系統中所有可執行的任務並選擇下一個可以執行的任務。每個執行佇列和系統中的每個 CPU 有關。 `Linux O(1)` 排程器是歷史上很流行的一個排程器。這個名字的由來是因為它能夠在常數時間內執行任務排程。在 O(1) 排程器裡,排程佇列被組織成兩個陣列,一個是任務**正在活動**的陣列,一個是任務**過期失效**的陣列。如下圖所示,每個陣列都包含了 140 個連結串列頭,每個連結串列頭具有不同的優先順序。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070614114-2008031150.png) 大致流程如下: 排程器從正在活動陣列中選擇一個優先順序最高的任務。如果這個任務的時間片過期失效了,就把它移動到過期失效陣列中。如果這個任務阻塞了,比如說正在等待 I/O 事件,那麼在它的時間片過期失效之前,一旦 I/O 操作完成,那麼這個任務將會繼續執行,它將被放回到之前正在活動的陣列中,因為這個任務之前已經消耗一部分 CPU 時間片,所以它將執行剩下的時間片。當這個任務執行完它的時間片後,它就會被放到過期失效陣列中。一旦正在活動的任務陣列中沒有其他任務後,排程器將會交換指標,使得正在活動的陣列變為過期失效陣列,過期失效陣列變為正在活動的陣列。使用這種方式可以保證每個優先順序的任務都能夠得到執行,不會導致執行緒飢餓。 在這種排程方式中,不同優先順序的任務所得到 CPU 分配的時間片也是不同的,高優先順序程序往往能得到較長的時間片,低優先順序的任務得到較少的時間片。 這種方式為了保證能夠更好的提供服務,通常會為 `互動式程序` 賦予較高的優先順序,互動式程序就是`使用者程序`。 Linux 系統不知道一個任務究竟是 I/O 密集型的還是 CPU 密集型的,它只是依賴於互動式的方式,Linux 系統會區分是`靜態優先順序` 還是 `動態優先順序`。動態優先順序是採用一種獎勵機制來實現的。獎勵機制有兩種方式:**獎勵互動式執行緒、懲罰佔用 CPU 的執行緒**。在 Linux O(1) 排程器中,最高的優先順序獎勵是 -5,注意這個優先順序越低越容易被執行緒排程器接受,所以最高懲罰的優先順序是 +5。具體體現就是作業系統維護一個名為 `sleep_avg` 的變數,任務喚醒會增加 sleep_avg 變數的值,當任務被搶佔或者時間量過期會減少這個變數的值,反映在獎勵機制上。 > O(1) 排程演算法是 2.6 核心版本的排程器,最初引入這個排程演算法的是不穩定的 2.5 版本。早期的排程演算法在多處理器環境中說明了通過訪問正在活動陣列就可以做出排程的決定。使排程可以在固定的時間 O(1) 完成。 O(1) 排程器使用了一種 `啟發式` 的方式,這是什麼意思? >在電腦科學中,啟發式是一種當傳統方式解決問題很慢時用來快速解決問題的方式,或者找到一個在傳統方法無法找到任何精確解的情況下找到近似解。 O(1) 使用啟發式的這種方式,會使任務的優先順序變得複雜並且不完善,從而導致在處理互動任務時效能很糟糕。 為了改進這個缺點,O(1) 排程器的開發者又提出了一個新的方案,即 `公平排程器(Completely Fair Scheduler, CFS)`。 CFS 的主要思想是使用一顆`紅黑樹`作為排程佇列。 >資料結構太重要了。 CFS 會根據任務在 CPU 上的執行時間長短而將其有序地排列在樹中,時間精確到納秒級。下面是 CFS 的構造模型 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070624179-1052046389.png) CFS 的排程過程如下: CFS 演算法總是優先排程哪些使用 CPU 時間最少的任務。最小的任務一般都是在最左邊的位置。當有一個新的任務需要執行時,CFS 會把這個任務和最左邊的數值進行對比,如果此任務具有最小時間值,那麼它將進行執行,否則它會進行比較,找到合適的位置進行插入。然後 CPU 執行紅黑樹上當前比較的最左邊的任務。 在紅黑樹中選擇一個節點來執行的時間可以是常數時間,但是插入一個任務的時間是 `O(loog(N))`,其中 N 是系統中的任務數。考慮到當前系統的負載水平,這是可以接受的。 排程器只需要考慮可執行的任務即可。這些任務被放在適當的排程佇列中。不可執行的任務和正在等待的各種 I/O 操作或核心事件的任務被放入一個`等待佇列`中。等待佇列頭包含一個指向任務連結串列的指標和一個自旋鎖。自旋鎖對於併發處理場景下用處很大。 #### Linux 系統中的同步 下面來聊一下 Linux 中的同步機制。早期的 Linux 核心只有一個 `大核心鎖(Big Kernel Lock,BKL)` 。它阻止了不同處理器併發處理的能力。因此,需要引入一些粒度更細的鎖機制。 Linux 提供了若干不同型別的同步變數,這些變數既能夠在核心中使用,也能夠在使用者應用程式中使用。在地層中,Linux 通過使用 `atomic_set` 和 `atomic_read` 這樣的操作為硬體支援的原子指令提供封裝。硬體提供記憶體重排序,這是 Linux 屏障的機制。 具有高級別的同步像是自旋鎖的描述是這樣的,當兩個程序同時對資源進行訪問,在一個程序獲得資源後,另一個程序不想被阻塞,所以它就會自旋,等待一會兒再對資源進行訪問。Linux 也提供互斥量或訊號量這樣的機制,也支援像是 `mutex_tryLock` 和 `mutex_tryWait` 這樣的非阻塞呼叫。也支援中斷處理事務,也可以通過動態禁用和啟用相應的中斷來實現。 ### Linux 啟動 下面來聊一聊 Linux 是如何啟動的。 當計算機電源通電後,`BIOS`會進行`開機自檢(Power-On-Self-Test, POST)`,對硬體進行檢測和初始化。因為作業系統的啟動會使用到磁碟、螢幕、鍵盤、滑鼠等裝置。下一步,磁碟中的第一個分割槽,也被稱為 `MBR(Master Boot Record)` 主引導記錄,被讀入到一個固定的記憶體區域並執行。這個分割槽中有一個非常小的,只有 512 位元組的程式。程式從磁碟中調入 boot 獨立程式,boot 程式將自身複製到高位地址的記憶體從而為作業系統釋放低位地址的記憶體。 複製完成後,boot 程式讀取啟動裝置的根目錄。boot 程式要理解檔案系統和目錄格式。然後 boot 程式被調入核心,把控制權移交給核心。直到這裡,boot 完成了它的工作。系統核心開始執行。 核心啟動程式碼是使用`組合語言`完成的,主要包括建立核心堆疊、識別 CPU 型別、計算記憶體、禁用中斷、啟動記憶體管理單元等,然後呼叫 C 語言的 main 函式執行作業系統部分。 這部分也會做很多事情,首先會分配一個訊息緩衝區來存放調試出現的問題,除錯資訊會寫入緩衝區。如果調試出現錯誤,這些資訊可以通過診斷程式調出來。 然後作業系統會進行自動配置,檢測裝置,載入配置檔案,被檢測裝置如果做出響應,就會被新增到已連結的裝置表中,如果沒有相應,就歸為未連線直接忽略。 配置完所有硬體後,接下來要做的就是仔細手工處理程序0,設定其堆疊,然後執行它,執行初始化、配置時鐘、掛載檔案系統。建立 `init 程序(程序 1 )` 和 `守護程序(程序 2)`。 init 程序會檢測它的標誌以確定它是否為單使用者還是多使用者服務。在前一種情況中,它會呼叫 fork 函式建立一個 shell 程序,並且等待這個程序結束。後一種情況呼叫 fork 函式建立一個執行系統初始化的 shell 指令碼(即 /etc/rc)的程序,這個程序可以進行檔案系統一致性檢測、掛載檔案系統、開啟守護程序等。 然後 /etc/rc 這個程序會從 /etc/ttys 中讀取資料,/etc/ttys 列出了所有的終端和屬性。對於每一個啟用的終端,這個程序呼叫 fork 函式建立一個自身的副本,進行內部處理並執行一個名為 `getty` 的程式。 getty 程式會在終端上輸入 ```shell login: ``` 等待使用者輸入使用者名稱,在輸入使用者名稱後,getty 程式結束,登陸程式 `/bin/login` 開始執行。login 程式需要輸入密碼,並與儲存在 `/etc/passwd` 中的密碼進行對比,如果輸入正確,login 程式以使用者 shell 程式替換自身,等待第一個命令。如果不正確,login 程式要求輸入另一個使用者名稱。 整個系統啟動過程如下 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070637056-1661326082.png) ## Linux 記憶體管理 Linux 記憶體管理模型非常直接明瞭,因為 Linux 的這種機制使其具有可移植性並且能夠在記憶體管理單元相差不大的機器下實現 Linux,下面我們就來認識一下 Linux 記憶體管理是如何實現的。 ### 基本概念 每個 Linux 程序都會有地址空間,這些地址空間由三個段區域組成:**text 段、data 段、stack 段**。下面是程序地址空間的示例。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070645204-504951634.png) `資料段(data segment)` 包含了程式的變數、字串、陣列和其他資料的儲存。資料段分為兩部分,已經初始化的資料和尚未初始化的資料。其中`尚未初始化的資料`就是我們說的 BSS。資料段部分的初始化需要編譯就期確定的常量以及程式啟動就需要一個初始值的變數。所有 BSS 部分中的變數在載入後被初始化為 0 。 和 `程式碼段(Text segment)` 不一樣,data segment 資料段可以改變。程式總是修改它的變數。而且,許多程式需要在執行時動態分配空間。Linux 允許資料段隨著記憶體的分配和回收從而增大或者減小。為了分配記憶體,程式可以增加資料段的大小。在 C 語言中有一套標準庫 `malloc` 經常用於分配記憶體。程序地址空間描述符包含動態分配的記憶體區域稱為 `堆(heap)`。 第三部分段是 `棧段(stack segment)`。在大部分機器上,棧段會在虛擬記憶體地址頂部地址位置處,並向低位置處(向地址空間為 0 處)拓展。舉個例子來說,在 32 位 x86 架構的機器上,棧開始於 `0xC0000000`,這是使用者模式下程序允許可見的 3GB 虛擬地址限制。如果棧一直增大到超過棧段後,就會發生硬體故障並把頁面下降一個頁面。 當程式啟動時,棧區域並不是空的,相反,它會包含所有的 shell 環境變數以及為了呼叫它而向 shell 輸入的命令列。舉個例子,當你輸入 ```shell cp cxuan lx ``` 時,cp 程式會執行並在棧中帶著字串 `cp cxuan lx` ,這樣就能夠找出原始檔和目標檔案的名稱。 當兩個使用者執行在相同程式中,例如`編輯器(editor)`,那麼就會在記憶體中保持編輯器程式程式碼的兩個副本,但是這種方式並不高效。Linux 系統支援`共享文字段作`為替代。下面圖中我們會看到 A 和 B 兩個程序,它們有著相同的文字區域。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070654221-1687164897.png) 資料段和棧段只有在 fork 之後才會共享,共享也是共享未修改過的頁面。如果任何一個都需要變大但是沒有相鄰空間容納的話,也不會有問題,因為相鄰的虛擬頁面不必對映到相鄰的物理頁面上。 除了動態分配更多的記憶體,Linux 中的程序可以通過`記憶體對映檔案`來訪問檔案資料。這個特性可以使我們把一個檔案對映到程序空間的一部分而該檔案就可以像位於記憶體中的位元組陣列一樣被讀寫。把一個檔案對映進來使得隨機讀寫比使用 read 和 write 之類的 I/O 系統呼叫要容易得多。共享庫的訪問就是使用了這種機制。如下所示 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070704619-494868919.png) 我們可以看到兩個相同檔案會被對映到相同的實體地址上,但是它們屬於不同的地址空間。 對映檔案的優點是,兩個或多個程序可以同時對映到同一檔案中,任意一個程序對檔案的寫操作對其他檔案可見。通過使用對映臨時檔案的方式,可以為多執行緒共享記憶體`提供高頻寬`,臨時檔案在程序退出後消失。但是實際上,並沒有兩個相同的地址空間,因為每個程序維護的開啟檔案和訊號不同。 ### Linux 記憶體管理系統呼叫 下面我們探討一下關於記憶體管理的系統呼叫方式。事實上,POSIX 並沒有給記憶體管理指定任何的系統呼叫。然而,Linux 卻有自己的記憶體系統呼叫,主要系統呼叫如下 | 系統呼叫 | 描述 | | --------------------------------------- | -------------- | | s = brk(addr) | 改變資料段大小 | | a = mmap(addr,len,prot,flags,fd,offset) | 進行對映 | | s = unmap(addr,len) | 取消對映 | 如果遇到錯誤,那麼 s 的返回值是 -1,a 和 addr 是記憶體地址,len 表示的是長度,prot 表示的是控制保護位,flags 是其他標誌位,fd 是檔案描述符,offset 是檔案偏移量。 `brk` 通過給出超過資料段之外的第一個位元組地址來指定資料段的大小。如果新的值要比原來的大,那麼資料區會變得越來越大,反之會越來越小。 `mmap` 和 `unmap` 系統呼叫會控制對映檔案。mmp 的第一個引數 addr 決定了檔案對映的地址。它必須是頁面大小的倍數。如果引數是 0,系統會分配地址並返回 a。第二個引數是長度,它告訴了需要對映多少位元組。它也是頁面大小的倍數。prot 決定了對映檔案的保護位,保護位可以標記為 **可讀、可寫、可執行或者這些的結合**。第四個引數 flags 能夠控制檔案是私有的還是可讀的以及 addr 是必須的還是隻是進行提示。第五個引數 fd 是要對映的檔案描述符。只有開啟的檔案是可以被對映的,因此如果想要進行檔案對映,必須開啟檔案;最後一個引數 offset 會指示檔案從什麼時候開始,並不一定每次都要從零開始。 ### Linux 記憶體管理實現 記憶體管理系統是作業系統最重要的部分之一。從計算機早期開始,我們實際使用的記憶體都要比系統中實際存在的記憶體多。`記憶體分配策略`克服了這一限制,並且其中最有名的就是 `虛擬記憶體(virtual memory)`。通過在多個競爭的程序之間共享虛擬記憶體,虛擬記憶體得以讓系統有更多的記憶體。虛擬記憶體子系統主要包括下面這些概念。 **大地址空間** 作業系統使系統使用起來好像比實際的實體記憶體要大很多,那是因為虛擬記憶體要比實體記憶體大很多倍。 **保護** 系統中的每個程序都會有自己的虛擬地址空間。這些虛擬地址空間彼此完全分開,因此執行一個應用程式的程序不會影響另一個。並且,硬體虛擬記憶體機制允許記憶體保護關鍵記憶體區域。 **記憶體對映** 記憶體對映用來向程序地址空間對映影象和資料檔案。在記憶體對映中,檔案的內容直接對映到程序的虛擬空間中。 **公平的實體記憶體分配** 記憶體管理子系統允許系統中的每個正在執行的程序公平分配系統的實體記憶體。 **共享虛擬記憶體** 儘管虛擬記憶體讓程序有自己的記憶體空間,但是有的時候你是需要共享記憶體的。例如幾個程序同時在 shell 中執行,這會涉及到 IPC 的程序間通訊問題,這個時候你需要的是共享記憶體來進行資訊傳遞而不是通過拷貝每個程序的副本獨立執行。 下面我們就正式探討一下什麼是 `虛擬記憶體` #### 虛擬記憶體的抽象模型 在考慮 Linux 用於支援虛擬記憶體的方法之前,考慮一個不會被太多細節困擾的抽象模型是很有用的。 處理器在執行指令時,會從記憶體中讀取指令並將其`解碼(decode)`,在指令解碼時會獲取某個位置的內容並將他存到記憶體中。然後處理器繼續執行下一條指令。這樣,處理器總是在訪問儲存器以獲取指令和儲存資料。 在虛擬記憶體系統中,所有的地址空間都是虛擬的而不是物理的。但是實際儲存和提取指令的是實體地址,所以需要讓處理器根據作業系統維護的一張表將虛擬地址轉換為實體地址。 為了簡單的完成轉換,虛擬地址和實體地址會被分為固定大小的塊,稱為 `頁(page)`。這些頁有相同大小,如果頁面大小不一樣的話,那麼作業系統將很難管理。Alpha AXP系統上的 Linux 使用 8 KB 頁面,而 Intel x86 系統上的 Linux 使用 4 KB 頁面。每個頁面都有一個唯一的編號,即`頁面框架號(PFN)`。 ![](https://img2020.cnblogs.com/blog/1515111/202010/1515111-20201010070716392-543120720.png) 上面就是 Linux 記憶體對映模型了,在這個頁模型中,虛擬地址由兩部分組成:**偏移量和虛擬頁框號**。每次處理器遇到虛擬地址時都會提取偏移量和虛擬頁框號。處理器必須將虛擬頁框號轉換為物理頁號,然後以正確的偏移量的位置訪問物理頁。 上圖中展示了兩個程序 A 和 B 的虛擬地址空間,每個程序都有自己的頁表。這些頁表將程序中的虛擬頁對映到記憶體中的物理頁中。頁表中每一項均包含 * `有效標誌(valid flag)`: 表明此頁表條目是否有效 * 該條目描述的物理頁框號 * 訪問控制資訊,頁面使用方式,是否可寫以及是否可以執行程式碼 要將處理器的虛擬地址對映為記憶體的實體地址,首先需要計算虛擬地址的頁框號和偏移量。頁面大小為 2 的次冪,可以通過移位完成操作。 如果當前程序嘗試訪問虛擬地址,但是訪問不到的話,這種情況稱為 `缺頁異常`,此時虛擬作業系統的錯誤地址和頁面錯誤的原因將通知作業系統。 通過以這種方式將虛擬地址對映到實體地址,虛擬記憶體可以以任何順序對映到系統的物理頁面。 #### 按需分頁 由於實體記憶體要比虛擬記憶體少很多,因此作業系統需要注意儘量避免直接使用`低效`的實體記憶體。節省實體記憶體的一種方式是僅載入執行程式當前使用的頁面(這何嘗不是一