UNIX 是一個交互式系統,用於同時處理多進程和多用戶同時在線。爲何要說 UNIX,那是由於 Linux 是由 UNIX 發展而來的,UNIX 是由程序員設計,它的主要服務對象也是程序員。Linux 繼承了 UNIX 的設計目標。從智能手機到汽車,超級計算機和家用電器,從家用臺式機到企業服務器,Linux 操做系統無處不在。html
大多數程序員都喜歡讓系統儘可能簡單,優雅並具備一致性。舉個例子,從最底層的角度來說,一個文件應該只是一個字節集合。爲了實現順序存取、隨機存取、按鍵存取、遠程存取只能是妨礙你的工做。相同的,若是命令node
ls A*
意味着只列出以 A 爲開頭的全部文件,那麼命令linux
rm A*
應該會移除全部以 A 爲開頭的文件而不是隻刪除文件名是 A*
的文件。這個特性也是最小吃驚原則(principle of least surprise)
程序員
最小吃驚原則一半經常使用於用戶界面和軟件設計。它的原型是:該功能或者特徵應該符合用戶的預期,不該該使用戶感到驚訝和震驚。算法
一些有經驗的程序員一般但願系統具備較強的功能性和靈活性。設計 Linux 的一個基本目標是每一個應用程序只作一件事情並把他作好。因此編譯器只負責編譯的工做,編譯器不會產生列表,由於有其餘應用比編譯器作的更好。shell
不少人都不喜歡冗餘,爲何在 cp 就能描述清楚你想幹何時還使用 copy?這徹底是在浪費寶貴的 hacking time
。爲了從文件中提取全部包含字符串 ard
的行,Linux 程序員應該輸入數據庫
grep ard f
Linux 系統是一種金字塔模型的系統,以下所示編程
應用程序發起系統調用把參數放在寄存器中(有時候放在棧中),併發出 trap
系統陷入指令切換用戶態至內核態。由於不能直接在 C 中編寫 trap 指令,所以 C 提供了一個庫,庫中的函數對應着系統調用。有些函數是使用匯編編寫的,可是可以從 C 中調用。每一個函數首先把參數放在合適的位置而後執行系統調用指令。所以若是你想要執行 read 系統調用的話,C 程序會調用 read 函數庫來執行。這裏順便提一下,是由 POSIX 指定的庫接口而不是系統調用接口。也就是說,POSIX 會告訴一個標準系統應該提供哪些庫過程,它們的參數是什麼,它們必須作什麼以及它們必須返回什麼結果。數組
除了操做系統和系統調用庫外,Linux 操做系統還要提供一些標準程序,好比文本編輯器、編譯器、文件操做工具等。直接和用戶打交道的是上面這些應用程序。所以咱們能夠說 Linux 具備三種不一樣的接口:系統調用接口、庫函數接口和應用程序接口瀏覽器
Linux 中的 GUI(Graphical User Interface)
和 UNIX 中的很是類似,這種 GUI 建立一個桌面環境,包括窗口、目標和文件夾、工具欄和文件拖拽功能。一個完整的 GUI 還包括窗口管理器以及各類應用程序。
Linux 上的 GUI 由 X 窗口支持,主要組成部分是 X 服務器、控制鍵盤、鼠標、顯示器等。當在 Linux 上使用圖形界面時,用戶能夠經過鼠標點擊運行程序或者打開文件,經過拖拽將文件進行復制等。
事實上,Linux 操做系統能夠由下面這幾部分構成
引導程序(Bootloader)
:引導程序是管理計算機啓動過程的軟件,對於大多數用戶而言,只是彈出一個屏幕,但其實內部操做系統作了不少事情內核(Kernel)
:內核是操做系統的核心,負責管理 CPU、內存和外圍設備等。初始化系統(Init System)
:這是一個引導用戶空間並負責控制守護程序的子系統。一旦從引導加載程序移交了初始引導,它就是用於管理引導過程的初始化系統。後臺進程(Daemon)
:後臺進程顧名思義就是在後臺運行的程序,好比打印、聲音、調度等,它們能夠在引導過程當中啓動,也能夠在登陸桌面後啓動圖形服務器(Graphical server)
:這是在監視器上顯示圖形的子系統。一般將其稱爲 X 服務器或 X。桌面環境(Desktop environment)
:這是用戶與之實際交互的部分,有不少桌面環境可供選擇,每一個桌面環境都包含內置應用程序,好比文件管理器、Web 瀏覽器、遊戲等應用程序(Applications)
:桌面環境不提供完整的應用程序,就像 Windows 和 macOS 同樣,Linux 提供了成千上萬個能夠輕鬆找到並安裝的高質量軟件。儘管 Linux 應用程序提供了 GUI ,可是大部分程序員仍偏好於使用命令行(command-line interface)
,稱爲shell
。用戶一般在 GUI 中啓動一個 shell 窗口而後就在 shell 窗口下進行工做。
shell 命令行使用速度快、功能更強大、並且易於擴展、而且不會帶來肢體重複性勞損(RSI)
。
下面會介紹一些最簡單的 bash shell。當 shell 啓動時,它首先進行初始化,在屏幕上輸出一個 提示符(prompt)
,一般是一個百分號或者美圓符號,等待用戶輸入
等用戶輸入一個命令後,shell 提取其中的第一個詞,這裏的詞指的是被空格或製表符分隔開的一連串字符。假定這個詞是將要運行程序的程序名,那麼就會搜索這個程序,若是找到了這個程序就會運行它。而後 shell 會將本身掛起直到程序運行完畢,以後再嘗試讀入下一條指令。shell 也是一個普通的用戶程序。它的主要功能就是讀取用戶的輸入和顯示計算的輸出。shell 命令中能夠包含參數,它們做爲字符串傳遞給所調用的程序。好比
cp src dest
會調用 cp 應用程序幷包含兩個參數 src
和 dest
。這個程序會解釋第一個參數是一個已經存在的文件名,而後建立一個該文件的副本,名稱爲 dest。
並非全部的參數都是文件名,好比下面
head -20 file
第一個參數 -20,會告訴 head 應用程序打印文件的前 20 行,而不是默認的 10 行。控制命令操做或者指定可選值的參數稱爲標誌(flag)
,按照慣例標誌應該使用 -
來表示。這個符號是必要的,好比
head 20 file
是一個徹底合法的命令,它會告訴 head 程序輸出文件名爲 20 的文件的前 10 行,而後輸出文件名爲 file 文件的前 10 行。Linux 操做系統能夠接受一個或多個參數。
爲了更容易的指定多個文件名,shell 支持 魔法字符(magic character)
,也被稱爲通配符(wild cards)
。好比,*
能夠匹配一個或者多個可能的字符串
ls *.c
告訴 ls 列舉出全部文件名以 .c
結束的文件。若是同時存在多個文件,則會在後面進行並列。
另外一個通配符是問號,負責匹配任意一個字符。一組在中括號中的字符能夠表示其中任意一個,所以
ls [abc]*
會列舉出全部以 a
、b
或者 c
開頭的文件。
shell 應用程序不必定經過終端進行輸入和輸出。shell 啓動時,就會獲取 標準輸入、標準輸出、標準錯誤文件進行訪問的能力。
標準輸出是從鍵盤輸入的,標準輸出或者標準錯誤是輸出到顯示器的。許多 Linux 程序默認是從標準輸入進行輸入並從標準輸出進行輸出。好比
sort
會調用 sort 程序,會從終端讀取數據(直到用戶輸入 ctrl-d 結束),根據字母順序進行排序,而後將結果輸出到屏幕上。
一般還能夠重定向標準輸入和標準輸出,重定向標準輸入使用 <
後面跟文件名。標準輸出能夠經過一個大於號 >
進行重定向。容許一個命令中重定向標準輸入和輸出。例如命令
sort <in >out
會使 sort 從文件 in 中獲得輸入,並把結果輸出到 out 文件中。因爲標準錯誤沒有重定向,因此錯誤信息會直接打印到屏幕上。從標準輸入讀入,對其進行處理並將其寫入到標準輸出的程序稱爲 過濾器
。
考慮下面由三個分開的命令組成的指令
sort <in >temp;head -30 <temp;rm temp
首先會調用 sort 應用程序,從標準輸入 in 中進行讀取,並經過標準輸出到 temp。當程序運行完畢後,shell 會運行 head ,告訴它打印前 30 行,並在標準輸出(默認爲終端)上打印。最後,temp 臨時文件被刪除。輕輕的,你走了,你揮一揮衣袖,不帶走一片雲彩。
命令行中的第一個程序一般會產生輸出,在上面的例子中,產生的輸出都不 temp 文件接收。然而,Linux 還提供了一個簡單的命令來作這件事,例以下面
sort <in | head -30
上面 |
稱爲豎線符號,它的意思是從 sort 應用程序產生的排序輸出會直接做爲輸入顯示,無需建立、使用和移除臨時文件。由管道符號鏈接的命令集合稱爲管道(pipeline)
。例如以下
grep cxuan *.c | sort | head -30 | tail -5 >f00
對任意以 .t
結尾的文件中包含 cxuan
的行被寫到標準輸出中,而後進行排序。這些內容中的前 30 行被 head 出來並傳給 tail ,它又將最後 5 行傳遞給 foo。這個例子提供了一個管道將多個命令鏈接起來。
能夠把一系列 shell 命令放在一個文件中,而後將此文件做爲輸入來運行。shell 會按照順序對他們進行處理,就像在鍵盤上鍵入命令同樣。包含 shell 命令的文件被稱爲 shell 腳本(shell scripts)
。
推薦一個 shell 命令的學習網站:https://www.shellscript.sh/
shell 腳本其實也是一段程序,shell 腳本中能夠對變量進行賦值,也包含循環控制語句好比 if、for、while 等,shell 的設計目標是讓其看起來和 C 類似(There is no doubt that C is father)。因爲 shell 也是一個用戶程序,因此用戶能夠選擇不一樣的 shell。
Linux 的命令行也就是 shell,它由大量標準應用程序組成。這些應用程序主要有下面六種
除了這些標準應用程序外,還有其餘應用程序好比 Web 瀏覽器、多媒體播放器、圖片瀏覽器、辦公軟件和遊戲程序等。
咱們在上面的例子中已經見過了幾個 Linux 的應用程序,好比 sort、cp、ls、head,下面咱們再來認識一下其餘 Linux 的應用程序。
咱們先從幾個例子開始講起,好比
cp a b
是將 a 複製一個副本爲 b ,而
mv a b
是將 a 移動到 b ,可是刪除原文件。
上面這兩個命令有一些區別,cp
是將文件進行復制,複製完成後會有兩個文件 a 和 b;而 mv
至關因而文件的移動,移動完成後就再也不有 a 文件。cat
命令能夠把多個文件內容進行鏈接。使用 rm
能夠刪除文件;使用 chmod
能夠容許全部者改變訪問權限;文件目錄的的建立和刪除能夠使用 mkdir
和 rmdir
命令;使用 ls
能夠查看目錄文件,ls 能夠顯示不少屬性,好比大小、用戶、建立日期等;sort 決定文件的顯示順序
Linux 應用程序還包括過濾器 grep,grep
從標準輸入或者一個或多個輸入文件中提取特定模式的行;sort
將輸入進行排序並輸出到標準輸出;head
提取輸入的前幾行;tail 提取輸入的後面幾行;除此以外的過濾器還有 cut
和 paste
,容許對文本行的剪切和複製;od
將輸入轉換爲 ASCII ;tr
實現字符大小寫轉換;pr
爲格式化打印輸出等。
程序編譯工具使用 gcc
;
make
命令用於自動編譯,這是一個很強大的命令,它用於維護一個大的程序,每每這類程序的源碼由許多文件構成。典型的,有一些是 header files 頭文件
,源文件一般使用 include
指令包含這些文件,make 的做用就是跟蹤哪些文件屬於頭文件,而後安排自動編譯的過程。
下面列出了 POSIX 的標準應用程序
程序 | 應用 |
---|---|
ls | 列出目錄 |
cp | 複製文件 |
head | 顯示文件的前幾行 |
make | 編譯文件生成二進制文件 |
cd | 切換目錄 |
mkdir | 建立目錄 |
chmod | 修改文件訪問權限 |
ps | 列出文件進程 |
pr | 格式化打印 |
rm | 刪除一個文件 |
rmdir | 刪除文件目錄 |
tail | 提取文件最後幾行 |
tr | 字符集轉換 |
grep | 分組 |
cat | 將多個文件連續標準輸出 |
od | 以八進制顯示文件 |
cut | 從文件中剪切 |
paste | 從文件中粘貼 |
在上面咱們看到了 Linux 的總體結構,下面咱們從總體的角度來看一下 Linux 的內核結構
內核直接坐落在硬件上,內核的主要做用就是 I/O 交互、內存管理和控制 CPU 訪問。上圖中還包括了 中斷
和 調度器
,中斷是與設備交互的主要方式。中斷出現時調度器就會發揮做用。這裏的低級代碼中止正在運行的進程,將其狀態保存在內核進程結構中,並啓動驅動程序。進程調度也會發生在內核完成一些操做而且啓動用戶進程的時候。圖中的調度器是 dispatcher。
注意這裏的調度器是
dispatcher
而不是scheduler
,這二者是有區別的scheduler 和 dispatcher 都是和進程調度相關的概念,不一樣的是 scheduler 會從幾個進程中隨意選取一個進程;而 dispatcher 會給 scheduler 選擇的進程分配 CPU。
而後,咱們把內核系統分爲三部分。
從圖中能夠看出 I/O 層次的關係,最高層是一個虛擬文件系統
,也就是說無論文件是來自內存仍是磁盤中,都是通過虛擬文件系統中的。從底層看,全部的驅動都是字符驅動或者塊設備驅動。兩者的主要區別就是是否容許隨機訪問。網絡驅動設備並非一種獨立的驅動設備,它其實是一種字符設備,不過網絡設備的處理方式和字符設備不一樣。
上面的設備驅動程序中,每一個設備類型的內核代碼都不一樣。字符設備有兩種使用方式,有一鍵式
的好比 vi 或者 emacs ,須要每個鍵盤輸入。其餘的好比 shell ,是須要輸入一行按回車鍵將字符串發送給程序進行編輯。
網絡軟件一般是模塊化的,由不一樣的設備和協議來支持。大多數 Linux 系統在內核中包含一個完整的硬件路由器的功能,可是這個不能和外部路由器相比,路由器上面是協議棧
,包括 TCP/IP 協議,協議棧上面是 socket 接口,socket 負責與外部進行通訊,充當了門的做用。
磁盤驅動上面是 I/O 調度器,它負責排序和分配磁盤讀寫操做,以儘量減小磁頭的無用移動。
I/O 右邊的是內存部件,程序被裝載進內存,由 CPU 執行,這裏會涉及到虛擬內存的部件,頁面的換入和換出是如何進行的,壞頁面的替換和常用的頁面會進行緩存。
進程模塊負責進程的建立和終止、進程的調度、Linux 把進程和線程看做是可運行的實體,並使用統一的調度策略來進行調度。
在內核最頂層的是系統調用接口,全部的系統調用都是通過這裏,系統調用會觸發一個 trap,將系統從用戶態轉換爲內核態,而後將控制權移交給上面的內核部件。
下面咱們就深刻理解一下 Linux 內核來理解 Linux 的基本概念之進程和線程。系統調用是操做系統自己的接口,它對於建立進程和線程,內存分配,共享文件和 I/O 來講都很重要。
咱們將從各個版本的共性出發來進行探討。
每一個進程都會運行一段獨立的程序,而且在初始化的時候擁有一個獨立的控制線程。換句話說,每一個進程都會有一個本身的程序計數器,這個程序計數器用來記錄下一個須要被執行的指令。Linux 容許進程在運行時建立額外的線程。
Linux 是一個多道程序設計系統,所以系統中存在彼此相互獨立的進程同時運行。此外,每一個用戶都會同時有幾個活動的進程。由於若是是一個大型系統,可能有數百上千的進程在同時運行。
在某些用戶空間中,即便用戶退出登陸,仍然會有一些後臺進程在運行,這些進程被稱爲 守護進程(daemon)
。
Linux 中有一種特殊的守護進程被稱爲 計劃守護進程(Cron daemon)
,計劃守護進程能夠每分鐘醒來一次檢查是否有工做要作,作完會繼續回到睡眠狀態等待下一次喚醒。
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)
,而會給子進程返回一個零值,能夠用下面代碼來進行表示
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 後的進程稱爲 原始進程
,一個原始進程能夠生成一顆繼承樹
Linux 進程間的通訊機制一般被稱爲 Internel-Process communication,IPC
下面咱們來講一說 Linux 進程間通訊的機制,大體來講,Linux 進程間的通訊機制能夠分爲 6 種
下面咱們分別對其進行概述
信號是 UNIX 系統最早開始使用的進程間通訊機制,由於 Linux 是繼承於 UNIX 的,因此 Linux 也支持信號機制,經過向一個或多個進程發送異步事件信號
來實現,信號能夠從鍵盤或者訪問不存在的位置等地方產生;信號經過 shell 將任務發送給子進程。
你能夠在 Linux 系統上輸入 kill -l
來列出系統使用的信號,下面是我提供的一些信號
進程能夠選擇忽略發送過來的信號,可是有兩個是不能忽略的:SIGSTOP
和 SIGKILL
信號。SIGSTOP 信號會通知當前正在運行的進程執行關閉操做,SIGKILL 信號會通知當前進程應該被殺死。除此以外,進程能夠選擇它想要處理的信號,進程也能夠選擇阻止信號,若是不阻止,能夠選擇自行處理,也能夠選擇進行內核處理。若是選擇交給內核進行處理,那麼就執行默認處理。
操做系統會中斷目標程序的進程來向其發送信號、在任何非原子指令中,執行均可以中斷,若是進程已經註冊了新號處理程序,那麼就執行進程,若是沒有註冊,將採用默認處理的方式。
例如:當進程收到 SIGFPE
浮點異常的信號後,默認操做是對其進行 dump(轉儲)
和退出。信號沒有優先級的說法。若是同時爲某個進程產生了兩個信號,則能夠將它們呈現給進程或者以任意的順序進行處理。
下面咱們就來看一下這些信號是幹什麼用的
SIGABRT 和 SIGIOT 信號發送給進程,告訴其進行終止,這個 信號一般在調用 C標準庫的abort()
函數時由進程自己啓動
當設置的時鐘功能超時時會將 SIGALRM 、 SIGVTALRM、SIGPROF 發送給進程。當實際時間或時鐘時間超時時,發送 SIGALRM。 當進程使用的 CPU 時間超時時,將發送 SIGVTALRM。 當進程和系統表明進程使用的CPU 時間超時時,將發送 SIGPROF。
SIGBUS 將形成總線中斷
錯誤時發送給進程
當子進程終止、被中斷或者被中斷恢復,將 SIGCHLD 發送給進程。此信號的一種常見用法是指示操做系統在子進程終止後清除其使用的資源。
SIGCONT 信號指示操做系統繼續執行先前由 SIGSTOP 或 SIGTSTP 信號暫停的進程。該信號的一個重要用途是在 Unix shell 中的做業控制中。
SIGFPE 信號在執行錯誤的算術運算(例如除以零)時將被髮送到進程。
當 SIGUP 信號控制的終端關閉時,會發送給進程。許多守護程序將從新加載其配置文件並從新打開其日誌文件,而不是在收到此信號時退出。
SIGILL 信號在嘗試執行非法、格式錯誤、未知或者特權指令時發出
當用戶但願中斷進程時,操做系統會向進程發送 SIGINT 信號。用戶輸入 ctrl - c 就是但願中斷進程。
SIGKILL 信號發送到進程以使其立刻進行終止。 與 SIGTERM 和 SIGINT 相比,這個信號沒法捕獲和忽略執行,而且進程在接收到此信號後沒法執行任何清理操做,下面是一些例外狀況
殭屍進程沒法殺死,由於殭屍進程已經死了,它在等待父進程對其進行捕獲
處於阻塞狀態的進程只有再次喚醒後纔會被 kill 掉
init
進程是 Linux 的初始化進程,這個進程會忽略任何信號。
SIGKILL 一般是做爲最後殺死進程的信號、它一般做用於 SIGTERM 沒有響應時發送給進程。
SIGPIPE 嘗試寫入進程管道時發現管道未鏈接沒法寫入時發送到進程
當在明確監視的文件描述符上發生事件時,將發送 SIGPOLL 信號。
SIGRTMIN 至 SIGRTMAX 是實時信號
當用戶請求退出進程並執行核心轉儲時,SIGQUIT 信號將由其控制終端發送給進程。
當 SIGSEGV 信號作出無效的虛擬內存引用或分段錯誤時,即在執行分段違規時,將其發送到進程。
SIGSTOP 指示操做系統終止以便之後進行恢復時
當 SIGSYS 信號將錯誤參數傳遞給系統調用時,該信號將發送到進程。
咱們上面簡單提到過了 SYSTERM 這個名詞,這個信號發送給進程以請求終止。與 SIGKILL 信號不一樣,該信號能夠被過程捕獲或忽略。這容許進程執行良好的終止,從而釋放資源並在適當時保存狀態。 SIGINT 與SIGTERM 幾乎相同。
SIGTSTP 信號由其控制終端發送到進程,以請求終端中止。
當 SIGTTIN 和SIGTTOU 信號分別在後臺嘗試從 tty 讀取或寫入時,信號將發送到該進程。
在發生異常或者 trap 時,將 SIGTRAP 信號發送到進程
當套接字具備可讀取的緊急或帶外數據時,將 SIGURG 信號發送到進程。
SIGUSR1 和 SIGUSR2 信號被髮送到進程以指示用戶定義的條件。
當 SIGXCPU 信號耗盡 CPU 的時間超過某個用戶可設置的預約值時,將其發送到進程
當 SIGXFSZ 信號增加超過最大容許大小的文件時,該信號將發送到該進程。
SIGWINCH 信號在其控制終端更改其大小(窗口更改)時發送給進程。
Linux 系統中的進程能夠經過創建管道 pipe 進行通訊。
在兩個進程之間,能夠創建一個通道,一個進程向這個通道里寫入字節流,另外一個進程從這個管道中讀取字節流。管道是同步的,當進程嘗試從空管道讀取數據時,該進程會被阻塞,直到有可用數據爲止。shell 中的管線 pipelines
就是用管道實現的,當 shell 發現輸出
sort <f | head
它會建立兩個進程,一個是 sort,一個是 head,sort,會在這兩個應用程序之間創建一個管道使得 sort 進程的標準輸出做爲 head 程序的標準輸入。sort 進程產生的輸出就不用寫到文件中了,若是管道滿了系統會中止 sort 以等待 head 讀出數據
管道實際上就是 |
,兩個應用程序不知道有管道的存在,一切都是由 shell 管理和控制的。
兩個進程之間還能夠經過共享內存進行進程間通訊,其中兩個或者多個進程能夠訪問公共內存空間。兩個進程的共享工做是經過共享內存完成的,一個進程所做的修改能夠對另外一個進程可見(很像線程間的通訊)。
在使用共享內存前,須要通過一系列的調用流程,流程以下
(shmget())
(shmat())
(shmdt())
(shmctl())
先入先出隊列 FIFO 一般被稱爲 命名管道(Named Pipes)
,命名管道的工做方式與常規管道很是類似,可是確實有一些明顯的區別。未命名的管道沒有備份文件:操做系統負責維護內存中的緩衝區,用來將字節從寫入器傳輸到讀取器。一旦寫入或者輸出終止的話,緩衝區將被回收,傳輸的數據會丟失。相比之下,命名管道具備支持文件和獨特 API ,命名管道在文件系統中做爲設備的專用文件存在。當全部的進程通訊完成後,命名管道將保留在文件系統中以備後用。命名管道具備嚴格的 FIFO 行爲
寫入的第一個字節是讀取的第一個字節,寫入的第二個字節是讀取的第二個字節,依此類推。
一聽到消息隊列這個名詞你可能不知道是什麼意思,消息隊列是用來描述內核尋址空間內的內部連接列表。能夠按幾種不一樣的方式將消息按順序發送到隊列並從隊列中檢索消息。每一個消息隊列由 IPC 標識符惟一標識。消息隊列有兩種模式,一種是嚴格模式
, 嚴格模式就像是 FIFO 先入先出隊列似的,消息順序發送,順序讀取。還有一種模式是 非嚴格模式
,消息的順序性不是很是重要。
還有一種管理兩個進程間通訊的是使用 socket
,socket 提供端到端的雙相通訊。一個套接字能夠與一個或多個進程關聯。就像管道有命令管道和未命名管道同樣,套接字也有兩種模式,套接字通常用於兩個進程之間的網絡通訊,網絡套接字須要來自諸如TCP(傳輸控制協議)
或較低級別UDP(用戶數據報協議)
等基礎協議的支持。
套接字有如下幾種分類
順序包套接字(Sequential Packet Socket)
: 此類套接字爲最大長度固定的數據報提供可靠的鏈接。此鏈接是雙向的而且是順序的。數據報套接字(Datagram Socket)
:數據包套接字支持雙向數據流。數據包套接字接受消息的順序與發送者可能不一樣。流式套接字(Stream Socket)
:流套接字的工做方式相似於電話對話,提供雙向可靠的數據流。原始套接字(Raw Socket)
: 能夠使用原始套接字訪問基礎通訊協議。如今關注一下 Linux 系統中與進程管理相關的系統調用。在瞭解以前你須要先知道一下什麼是系統調用。
操做系統爲咱們屏蔽了硬件和軟件的差別,它的最主要功能就是爲用戶提供一種抽象,隱藏內部實現,讓用戶只關心在 GUI 圖形界面下如何使用便可。操做系統能夠分爲兩種模式
咱們常說的上下文切換
指的就是內核態模式和用戶態模式的頻繁切換。而系統調用
指的就是引發內核態和用戶態切換的一種方式,系統調用一般在後臺靜默運行,表示計算機程序向其操做系統內核請求服務。
系統調用指令有不少,下面是一些與進程管理相關的最主要的系統調用
fork 調用用於建立一個與父進程相同的子進程,建立完進程後的子進程擁有和父進程同樣的程序計數器、相同的 CPU 寄存器、相同的打開文件。
exec 系統調用用於執行駐留在活動進程中的文件,調用 exec 後,新的可執行文件會替換先前的可執行文件並得到執行。也就是說,調用 exec 後,會將舊文件或程序替換爲新文件或執行,而後執行文件或程序。新的執行程序被加載到相同的執行空間中,所以進程的 PID
不會修改,由於咱們沒有建立新進程,只是替換舊進程。可是進程的數據、代碼、堆棧都已經被修改。若是當前要被替換的進程包含多個線程,那麼全部的線程將被終止,新的進程映像被加載執行。
這裏須要解釋一下進程映像(Process image)
的概念
什麼是進程映像呢?進程映像是執行程序時所須要的可執行文件,一般會包括下面這些東西
又稱文本段,用來存放指令,運行代碼的一塊內存空間
此空間大小在代碼運行前就已經肯定
內存空間通常屬於只讀,某些架構的代碼也容許可寫
在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。
可讀可寫
存儲初始化的全局變量和初始化的 static 變量
數據段中數據的生存期是隨程序持續性(隨進程持續性)
隨進程持續性:進程建立就存在,進程死亡就消失
可讀可寫
存儲未初始化的全局變量和未初始化的 static 變量
bss 段中的數據通常默認爲 0
是可讀寫的,由於變量的值能夠在運行時更改。此段的大小也固定。
可讀可寫
存儲的是函數或代碼中的局部變量(非 static 變量)
棧的生存期隨代碼塊持續性,代碼塊運行就給你分配空間,代碼塊結束,就自動回收空間
可讀可寫
存儲的是程序運行期間動態分配的 malloc/realloc 的空間
堆的生存期隨進程持續性,從 malloc/realloc 到 free 一直存在
下面是這些區域的構成圖
exec 系統調用是一些函數的集合,這些函數是
下面來看一下 exec 的工做原理
等待子進程結束或終止
在許多計算機操做系統上,計算機進程的終止是經過執行 exit
系統調用命令執行的。0 表示進程可以正常結束,其餘值表示進程以非正常的行爲結束。
其餘一些常見的系統調用以下
系統調用指令 | 描述 |
---|---|
pause | 掛起信號 |
nice | 改變分時進程的優先級 |
ptrace | 進程跟蹤 |
kill | 向進程發送信號 |
pipe | 建立管道 |
mkfifo | 建立 fifo 的特殊文件(命名管道) |
sigaction | 設置對指定信號的處理方法 |
msgctl | 消息控制操做 |
semctl | 信號量控制 |
在 Linux 內核結構中,進程會被表示爲 任務
,經過結構體 structure
來建立。不像其餘的操做系統會區分進程、輕量級進程和線程,Linux 統一使用任務結構來表明執行上下文。所以,對於每一個單線程進程來講,單線程進程將用一個任務結構表示,對於多線程進程來講,將爲每個用戶級線程分配一個任務結構。Linux 內核是多線程的,而且內核級線程不與任何用戶級線程相關聯。
對於每一個進程來講,在內存中都會有一個 task_struct
進程描述符與之對應。進程描述符包含了內核管理進程全部有用的信息,包括 調度參數、打開文件描述符等等。進程描述符從進程建立開始就一直存在於內核堆棧中。
Linux 和 Unix 同樣,都是經過 PID
來區分不一樣的進程,內核會將全部進程的任務結構組成爲一個雙向鏈表。PID 可以直接被映射稱爲進程的任務結構所在的地址,從而不須要遍歷雙向鏈表直接訪問。
咱們上面提到了進程描述符,這是一個很是重要的概念,咱們上面還提到了進程描述符是位於內存中的,這裏咱們省略了一句話,那就是進程描述符是存在用戶的任務結構中,當進程位於內存並開始運行時,進程描述符纔會被調入內存。
進程位於內存
被稱爲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 結構能夠參考
這個結構中包含進程描述符,進程描述符位於固定的位置,使得 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 的內容覆蓋它的內存。
如今咱們來討論一下 Linux 中的線程,線程是輕量級的進程,想必這句話你已經聽過不少次了,輕量級
體如今全部的進程切換都須要清除全部的表、進程間的共享信息也比較麻煩,通常來講經過管道或者共享內存,若是是 fork 函數後的父子進程則使用共享文件,然而線程切換不須要像進程同樣具備昂貴的開銷,並且線程通訊起來也更方便。線程分爲兩種:用戶級線程和內核級線程
用戶級線程避免使用內核,一般,每一個線程會顯示調用開關,發送信號或者執行某種切換操做來放棄 CPU,一樣,計時器能夠強制進行開關,用戶線程的切換速度一般比內核線程快不少。在用戶級別實現線程會有一個問題,即單個線程可能會壟斷 CPU 時間片,致使其餘線程沒法執行從而 餓死
。若是執行一個 I/O 操做,那麼 I/O 會阻塞,其餘線程也沒法運行。
一種解決方案是,一些用戶級的線程包解決了這個問題。能夠使用時鐘週期的監視器來控制第一時間時間片獨佔。而後,一些庫經過特殊的包裝來解決系統調用的 I/O 阻塞問題,或者能夠爲非阻塞 I/O 編寫任務。
內核級線程一般使用幾個進程表在內核中實現,每一個任務都會對應一個進程表。在這種狀況下,內核會在每一個進程的時間片內調度每一個線程。
全部可以阻塞的調用都會經過系統調用的方式來實現,當一個線程阻塞時,內核能夠進行選擇,是運行在同一個進程中的另外一個線程(若是有就緒線程的話)仍是運行一個另外一個進程中的線程。
從用戶空間 -> 內核空間 -> 用戶空間的開銷比較大,可是線程初始化的時間損耗能夠忽略不計。這種實現的好處是由時鐘決定線程切換時間,所以不太可能將時間片與任務中的其餘線程佔用時間綁定到一塊兒。一樣,I/O 阻塞也不是問題。
結合用戶空間和內核空間的優勢,設計人員採用了一種內核級線程
的方式,而後將用戶級線程與某些或者所有內核線程多路複用起來
在這種模型中,編程人員能夠自由控制用戶線程和內核線程的數量,具備很大的靈活度。採用這種方法,內核只識別內核級線程,並對其進行調度。其中一些內核級線程會被多個用戶級線程多路複用。
下面咱們來關注一下 Linux 系統的調度算法,首先須要認識到,Linux 系統的線程是內核線程,因此 Linux 系統是基於線程的,而不是基於進程的。
爲了進行調度,Linux 系統將線程分爲三類
實時先入先出線程具備最高優先級,它不會被其餘線程所搶佔,除非那是一個剛剛準備好的,擁有更高優先級的線程進入。實時輪轉線程與實時先入先出線程基本相同,只是每一個實時輪轉線程都有一個時間量,時間到了以後就能夠被搶佔。若是多個實時線程準備完畢,那麼每一個線程運行它時間量所規定的時間,而後插入到實時輪轉線程末尾。
注意這個實時只是相對的,沒法作到絕對的實時,由於線程的運行時間沒法肯定。它們相對分時系統來講,更加具備實時性
Linux 系統會給每一個線程分配一個 nice
值,這個值表明了優先級的概念。nice 值默認值是 0 ,可是能夠經過系統調用 nice 值來修改。修改值的範圍從 -20 - +19。nice 值決定了線程的靜態優先級。通常系統管理員的 nice 值會比通常線程的優先級高,它的範圍是 -20 - -1。
下面咱們更詳細的討論一下 Linux 系統的兩個調度算法,它們的內部與調度隊列(runqueue)
的設計很類似。運行隊列有一個數據結構用來監視系統中全部可運行的任務並選擇下一個能夠運行的任務。每一個運行隊列和系統中的每一個 CPU 有關。
Linux O(1)
調度器是歷史上很流行的一個調度器。這個名字的由來是由於它可以在常數時間內執行任務調度。在 O(1) 調度器裏,調度隊列被組織成兩個數組,一個是任務正在活動的數組,一個是任務過時失效的數組。以下圖所示,每一個數組都包含了 140 個鏈表頭,每一個鏈表頭具備不一樣的優先級。
大體流程以下:
調度器從正在活動數組中選擇一個優先級最高的任務。若是這個任務的時間片過時失效了,就把它移動到過時失效數組中。若是這個任務阻塞了,好比說正在等待 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 的構造模型
CFS 的調度過程以下:
CFS 算法老是優先調度哪些使用 CPU 時間最少的任務。最小的任務通常都是在最左邊的位置。當有一個新的任務須要運行時,CFS 會把這個任務和最左邊的數值進行對比,若是此任務具備最小時間值,那麼它將進行運行,不然它會進行比較,找到合適的位置進行插入。而後 CPU 運行紅黑樹上當前比較的最左邊的任務。
在紅黑樹中選擇一個節點來運行的時間能夠是常數時間,可是插入一個任務的時間是 O(loog(N))
,其中 N 是系統中的任務數。考慮到當前系統的負載水平,這是能夠接受的。
調度器只須要考慮可運行的任務便可。這些任務被放在適當的調度隊列中。不可運行的任務和正在等待的各類 I/O 操做或內核事件的任務被放入一個等待隊列
中。等待隊列頭包含一個指向任務鏈表的指針和一個自旋鎖。自旋鎖對於併發處理場景下用處很大。
下面來聊一下 Linux 中的同步機制。早期的 Linux 內核只有一個 大內核鎖(Big Kernel Lock,BKL)
。它阻止了不一樣處理器併發處理的能力。所以,須要引入一些粒度更細的鎖機制。
Linux 提供了若干不一樣類型的同步變量,這些變量既可以在內核中使用,也可以在用戶應用程序中使用。在地層中,Linux 經過使用 atomic_set
和 atomic_read
這樣的操做爲硬件支持的原子指令提供封裝。硬件提供內存重排序,這是 Linux 屏障的機制。
具備高級別的同步像是自旋鎖的描述是這樣的,當兩個進程同時對資源進行訪問,在一個進程得到資源後,另外一個進程不想被阻塞,因此它就會自旋,等待一下子再對資源進行訪問。Linux 也提供互斥量或信號量這樣的機制,也支持像是 mutex_tryLock
和 mutex_tryWait
這樣的非阻塞調用。也支持中斷處理事務,也能夠經過動態禁用和啓用相應的中斷來實現。
下面來聊一聊 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 程序會在終端上輸入
login:
等待用戶輸入用戶名,在輸入用戶名後,getty 程序結束,登錄程序 /bin/login
開始運行。login 程序須要輸入密碼,並與保存在 /etc/passwd
中的密碼進行對比,若是輸入正確,login 程序以用戶 shell 程序替換自身,等待第一個命令。若是不正確,login 程序要求輸入另外一個用戶名。
整個系統啓動過程以下
Linux 內存管理模型很是直接明瞭,由於 Linux 的這種機制使其具備可移植性而且可以在內存管理單元相差不大的機器下實現 Linux,下面咱們就來認識一下 Linux 內存管理是如何實現的。
每一個 Linux 進程都會有地址空間,這些地址空間由三個段區域組成:text 段、data 段、stack 段。下面是進程地址空間的示例。
數據段(data segment)
包含了程序的變量、字符串、數組和其餘數據的存儲。數據段分爲兩部分,已經初始化的數據和還沒有初始化的數據。其中還沒有初始化的數據
就是咱們說的 BSS。數據段部分的初始化須要編譯就期肯定的常量以及程序啓動就須要一個初始值的變量。全部 BSS 部分中的變量在加載後被初始化爲 0 。
和 代碼段(Text segment)
不同,data segment 數據段能夠改變。程序老是修改它的變量。並且,許多程序須要在執行時動態分配空間。Linux 容許數據段隨着內存的分配和回收從而增大或者減少。爲了分配內存,程序能夠增長數據段的大小。在 C 語言中有一套標準庫 malloc
常常用於分配內存。進程地址空間描述符包含動態分配的內存區域稱爲 堆(heap)
。
第三部分段是 棧段(stack segment)
。在大部分機器上,棧段會在虛擬內存地址頂部地址位置處,並向低位置處(向地址空間爲 0 處)拓展。舉個例子來講,在 32 位 x86 架構的機器上,棧開始於 0xC0000000
,這是用戶模式下進程容許可見的 3GB 虛擬地址限制。若是棧一直增大到超過棧段後,就會發生硬件故障並把頁面降低一個頁面。
當程序啓動時,棧區域並非空的,相反,它會包含全部的 shell 環境變量以及爲了調用它而向 shell 輸入的命令行。舉個例子,當你輸入
cp cxuan lx
時,cp 程序會運行並在棧中帶着字符串 cp cxuan lx
,這樣就可以找出源文件和目標文件的名稱。
當兩個用戶運行在相同程序中,例如編輯器(editor)
,那麼就會在內存中保持編輯器程序代碼的兩個副本,可是這種方式並不高效。Linux 系統支持共享文本段做
爲替代。下面圖中咱們會看到 A 和 B 兩個進程,它們有着相同的文本區域。
數據段和棧段只有在 fork 以後纔會共享,共享也是共享未修改過的頁面。若是任何一個都須要變大可是沒有相鄰空間容納的話,也不會有問題,由於相鄰的虛擬頁面沒必要映射到相鄰的物理頁面上。
除了動態分配更多的內存,Linux 中的進程能夠經過內存映射文件
來訪問文件數據。這個特性能夠使咱們把一個文件映射到進程空間的一部分而該文件就能夠像位於內存中的字節數組同樣被讀寫。把一個文件映射進來使得隨機讀寫比使用 read 和 write 之類的 I/O 系統調用要容易得多。共享庫的訪問就是使用了這種機制。以下所示
咱們能夠看到兩個相同文件會被映射到相同的物理地址上,可是它們屬於不一樣的地址空間。
映射文件的優勢是,兩個或多個進程能夠同時映射到同一文件中,任意一個進程對文件的寫操做對其餘文件可見。經過使用映射臨時文件的方式,能夠爲多線程共享內存提供高帶寬
,臨時文件在進程退出後消失。可是實際上,並無兩個相同的地址空間,由於每一個進程維護的打開文件和信號不一樣。
下面咱們探討一下關於內存管理的系統調用方式。事實上,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 會指示文件從何時開始,並不必定每次都要從零開始。
內存管理系統是操做系統最重要的部分之一。從計算機早期開始,咱們實際使用的內存都要比系統中實際存在的內存多。內存分配策略
克服了這一限制,而且其中最有名的就是 虛擬內存(virtual memory)
。經過在多個競爭的進程之間共享虛擬內存,虛擬內存得以讓系統有更多的內存。虛擬內存子系統主要包括下面這些概念。
大地址空間
操做系統使系統使用起來好像比實際的物理內存要大不少,那是由於虛擬內存要比物理內存大不少倍。
保護
系統中的每一個進程都會有本身的虛擬地址空間。這些虛擬地址空間彼此徹底分開,所以運行一個應用程序的進程不會影響另外一個。而且,硬件虛擬內存機制容許內存保護關鍵內存區域。
內存映射
內存映射用來向進程地址空間映射圖像和數據文件。在內存映射中,文件的內容直接映射到進程的虛擬空間中。
公平的物理內存分配
內存管理子系統容許系統中的每一個正在運行的進程公平分配系統的物理內存。
共享虛擬內存
儘管虛擬內存讓進程有本身的內存空間,可是有的時候你是須要共享內存的。例如幾個進程同時在 shell 中運行,這會涉及到 IPC 的進程間通訊問題,這個時候你須要的是共享內存來進行信息傳遞而不是經過拷貝每一個進程的副本獨立運行。
下面咱們就正式探討一下什麼是 虛擬內存
在考慮 Linux 用於支持虛擬內存的方法以前,考慮一個不會被太多細節困擾的抽象模型是頗有用的。
處理器在執行指令時,會從內存中讀取指令並將其解碼(decode)
,在指令解碼時會獲取某個位置的內容並將他存到內存中。而後處理器繼續執行下一條指令。這樣,處理器老是在訪問存儲器以獲取指令和存儲數據。
在虛擬內存系統中,全部的地址空間都是虛擬的而不是物理的。可是實際存儲和提取指令的是物理地址,因此須要讓處理器根據操做系統維護的一張表將虛擬地址轉換爲物理地址。
爲了簡單的完成轉換,虛擬地址和物理地址會被分爲固定大小的塊,稱爲 頁(page)
。這些頁有相同大小,若是頁面大小不同的話,那麼操做系統將很難管理。Alpha AXP系統上的 Linux 使用 8 KB 頁面,而 Intel x86 系統上的 Linux 使用 4 KB 頁面。每一個頁面都有一個惟一的編號,即頁面框架號(PFN)
。
上面就是 Linux 內存映射模型了,在這個頁模型中,虛擬地址由兩部分組成:偏移量和虛擬頁框號。每次處理器遇到虛擬地址時都會提取偏移量和虛擬頁框號。處理器必須將虛擬頁框號轉換爲物理頁號,而後以正確的偏移量的位置訪問物理頁。
上圖中展現了兩個進程 A 和 B 的虛擬地址空間,每一個進程都有本身的頁表。這些頁表將進程中的虛擬頁映射到內存中的物理頁中。頁表中每一項均包含
有效標誌(valid flag)
: 代表此頁表條目是否有效要將處理器的虛擬地址映射爲內存的物理地址,首先須要計算虛擬地址的頁框號和偏移量。頁面大小爲 2 的次冪,能夠經過移位完成操做。
若是當前進程嘗試訪問虛擬地址,可是訪問不到的話,這種狀況稱爲 缺頁異常
,此時虛擬操做系統的錯誤地址和頁面錯誤的緣由將通知操做系統。
經過以這種方式將虛擬地址映射到物理地址,虛擬內存能夠以任何順序映射到系統的物理頁面。
因爲物理內存要比虛擬內存少不少,所以操做系統須要注意儘可能避免直接使用低效
的物理內存。節省物理內存的一種方式是僅加載執行程序當前使用的頁面(這未嘗不是一種懶加載的思想呢?)。例如,能夠運行數據庫來查詢數據庫,在這種狀況下,不是全部的數據都裝入內存,只裝載須要檢查的數據。這種僅僅在須要時纔將虛擬頁面加載進內中的技術稱爲按需分頁。
若是某個進程須要將虛擬頁面傳入內存,可是此時沒有可用的物理頁面,那麼操做系統必須丟棄物理內存中的另外一個頁面來爲該頁面騰出空間。
若是頁面已經修改過,那麼操做系統必須保留該頁面的內容,以便之後能夠訪問它。這種類型的頁面被稱爲髒頁,當將其從內存中移除時,它會保存在稱爲交換文件
的特殊文件中。相對於處理器和物理內存的速度,對交換文件的訪問很是慢,而且操做系統須要兼顧將頁面寫到磁盤的以及將它們保留在內存中以便再次使用。
Linux 使用最近最少使用(LRU)
頁面老化技術來公平的選擇可能會從系統中刪除的頁面,這個方案涉及系統中的每一個頁面,頁面的年齡隨着訪問次數的變化而變化,若是某個頁面訪問次數多,那麼該頁就表示越 年輕
,若是某個呃頁面訪問次數太少,那麼該頁越容易被換出
。
大多數多功能處理器都支持 物理地址
模式和虛擬地址
模式的概念。物理尋址模式不須要頁表,而且處理器不會在此模式下嘗試執行任何地址轉換。 Linux 內核被連接在物理地址空間中運行。
Alpha AXP 處理器沒有物理尋址模式。相反,它將內存空間劃分爲幾個區域,並將其中兩個指定爲物理映射的地址。此內核地址空間稱爲 KSEG 地址空間,它包含從 0xfffffc0000000000 向上的全部地址。爲了從 KSEG 中連接的代碼(按照定義,內核代碼)執行或訪問其中的數據,該代碼必須在內核模式下執行。連接到 Alpha 上的 Linux內核以從地址 0xfffffc0000310000 執行。
頁面表的每一項還包含訪問控制信息,訪問控制信息主要檢查進程是否應該訪問內存。
必要時須要對內存進行訪問限制
。 例如包含可執行代碼的內存,天然是隻讀內存; 操做系統不該容許進程經過其可執行代碼寫入數據。 相比之下,包含數據的頁面能夠被寫入,可是嘗試執行該內存的指令將失敗。 大多數處理器至少具備兩種執行模式:內核態和用戶態。 你不但願訪問用戶執行內核代碼或內核數據結構,除非處理器之內核模式運行。
訪問控制信息被保存在上面的 Page Table Entry ,頁表項中,上面這幅圖是 Alpha AXP的 PTE。位字段具備如下含義
表示 valid ,是否有效位
讀取時故障,在嘗試讀取此頁面時出現故障
寫入時錯誤,在嘗試寫入時發生錯誤
執行時發生錯誤,在嘗試執行此頁面中的指令時,處理器都會報告頁面錯誤並將控制權傳遞給操做系統,
地址空間匹配,當操做系統但願清除轉換緩衝區中的某些條目時,將使用此選項。
當在使用單個轉換緩衝區
條目而不是多個轉換緩衝區
條目映射整個塊時使用的提示。
內核模式運行下的代碼能夠讀取頁面
用戶模式下的代碼能夠讀取頁面
之內核模式運行的代碼能夠寫入頁面
以用戶模式運行的代碼能夠寫入頁面
對於設置了 V 位的 PTE,此字段包含此 PTE 的物理頁面幀號(頁面幀號)。對於無效的 PTE,若是此字段不爲零,則包含有關頁面在交換文件中的位置的信息。
除此以外,Linux 還使用了兩個位
若是已設置,則須要將頁面寫出到交換文件中
Linux 用來將頁面標記爲已訪問。
上面的虛擬內存抽象模型能夠用來實施,可是效率不會過高。操做系統和處理器設計人員都嘗試提升性能。 可是除了提升處理器,內存等的速度以外,最好的方法就是維護有用信息和數據的高速緩存,從而使某些操做更快。在 Linux 中,使用不少和內存管理有關的緩衝區,使用緩衝區來提升效率。
緩衝區高速緩存包含塊設備
驅動程序使用的數據緩衝區。
還記得什麼是塊設備麼?這裏回顧下
塊設備是一個能存儲固定大小塊
信息的設備,它支持以固定大小的塊,扇區或羣集讀取和(可選)寫入數據。每一個塊都有本身的物理地址
。一般塊的大小在 512 - 65536 之間。全部傳輸的信息都會以連續
的塊爲單位。塊設備的基本特徵是每一個塊都較爲對立,可以獨立的進行讀寫。常見的塊設備有 硬盤、藍光光盤、USB 盤
與字符設備相比,塊設備一般須要較少的引腳。
緩衝區高速緩存經過設備標識符
和塊編號用於快速查找數據塊。 若是能夠在緩衝區高速緩存中找到數據,則無需從物理塊設備中讀取數據,這種訪問方式要快得多。
頁緩存用於加快對磁盤上圖像和數據的訪問
它用於一次一頁地緩存文件中的內容,而且能夠經過文件和文件中的偏移量進行訪問。當頁面從磁盤讀入內存時,它們被緩存在頁面緩存中。
僅僅已修改(髒頁)被保存在交換文件中
只要這些頁面在寫入交換文件後沒有修改,則下次交換該頁面時,無需將其寫入交換文件,由於該頁面已在交換文件中。 能夠直接丟棄。 在大量交換的系統中,這節省了許多沒必要要的和昂貴的磁盤操做。
處理器中一般使用一種硬件緩存。頁表條目的緩存。在這種狀況下,處理器並不老是直接讀取頁表,而是根據須要緩存頁的翻譯。 這些是轉換後備緩衝區
也被稱爲 TLB
,包含來自系統中一個或多個進程的頁表項的緩存副本。
引用虛擬地址後,處理器將嘗試查找匹配的 TLB 條目。 若是找到,則能夠將虛擬地址直接轉換爲物理地址,並對數據執行正確的操做。 若是處理器找不到匹配的 TLB 條目, 它經過向操做系統發信號通知已發生 TLB 丟失得到操做系統的支持和幫助。系統特定的機制用於將該異常傳遞給能夠修復問題的操做系統代碼。 操做系統爲地址映射生成一個新的 TLB 條目。 清除異常後,處理器將再次嘗試轉換虛擬地址。此次可以執行成功。
使用緩存也存在缺點,爲了節省精力,Linux 必須使用更多的時間和空間來維護這些緩存,而且若是緩存損壞,系統將會崩潰。
Linux 假定頁表分爲三個級別。訪問的每一個頁表都包含下一級頁表
圖中的 PDG 表示全局頁表,當建立一個新的進程時,都要爲新進程建立一個新的頁面目錄,即 PGD。
要將虛擬地址轉換爲物理地址,處理器必須獲取每一個級別字段的內容,將其轉換爲包含頁表的物理頁的偏移量,並讀取下一級頁表的頁框號。這樣重複三次,直到找到包含虛擬地址的物理頁面的頁框號爲止。
Linux 運行的每一個平臺都必須提供翻譯宏,這些宏容許內核遍歷特定進程的頁表。這樣,內核無需知道頁表條目的格式或它們的排列方式。
對系統中物理頁面有不少需求。例如,當圖像加載到內存中時,操做系統須要分配頁面。
系統中全部物理頁面均由 mem_map
數據結構描述,這個數據結構是 mem_map_t
的列表。它包括一些重要的屬性
頁面分配代碼使用 free_area
向量查找和釋放頁面,free_area 的每一個元素都包含有關頁面塊的信息。
Linux 的頁面分配使用一種著名的夥伴算法來進行頁面的分配和取消分配。頁面以 2 的冪爲單位進行塊分配。這就意味着它能夠分配 1頁、2 頁、4頁等等,只要系統中有足夠可用的頁面來知足需求就能夠。判斷的標準是nr_free_pages> min_free_pages,若是知足,就會在 free_area 中搜索所需大小的頁面塊完成分配。free_area 的每一個元素都有該大小的塊的已分配頁面和空閒頁面塊的映射。
分配算法會搜索請求大小的頁面塊。若是沒有任何請求大小的頁面塊可用的話,會搜尋一個是請求大小二倍的頁面塊,而後重複,直到一直搜尋完 free_area 找到一個頁面塊爲止。若是找到的頁面塊要比請求的頁面塊大,就會對找到的頁面塊進行細分,直到找到合適的大小塊爲止。
由於每一個塊都是 2 的次冪,因此拆分過程很容易,由於你只需將塊分紅兩半便可。空閒塊在適當的隊列中排隊,分配的頁面塊返回給調用者。
若是請求一個 2 個頁的塊,則 4 頁的第一個塊(從第 4 頁的框架開始)將被分紅兩個 2 頁的塊。第一個頁面(從第 4 頁的幀開始)將做爲分配的頁面返回給調用方,第二個塊(從第 6 頁的頁面開始)將做爲 2 頁的空閒塊排隊到 free_area 數組的元素 1 上。
上面的這種內存方式最形成一種後果,那就是內存的碎片化,會將較大的空閒頁面分紅較小的頁面。頁面解除分配代碼會盡量將頁面從新組合成爲更大的空閒塊。每釋放一個頁面,都會檢查相同大小的相鄰的塊,以查看是否空閒。若是是,則將其與新釋放的頁面塊組合以造成下一個頁面大小塊的新的自由頁面塊。 每次將兩個頁面塊從新組合爲更大的空閒頁面塊時,頁面釋放代碼就會嘗試將該頁面塊從新組合爲更大的空閒頁面。 經過這種方式,可用頁面的塊將盡量多地使用內存。
例如上圖,若是要釋放第 1 頁的頁面,則將其與已經空閒的第 0 頁頁面框架組合在一塊兒,並做爲大小爲 2頁的空閒塊排隊到 free_area 的元素 1 中
內核有兩種類型的內存映射:共享型(shared)
和私有型(private)
。私有型是當進程爲了只讀文件,而不寫文件時使用,這時,私有映射更加高效。 可是,任何對私有映射頁的寫操做都會致使內核中止映射該文件中的頁。因此,寫操做既不會改變磁盤上的文件,對訪問該文件的其它進程也是不可見的。
一旦可執行映像被內存映射到虛擬內存後,它就能夠被執行了。由於只將映像的開頭部分物理的拉入到內存中,所以它將很快訪問物理內存還沒有存在的虛擬內存區域。當進程訪問沒有有效頁表的虛擬地址時,操做系統會報告這項錯誤。
頁面錯誤描述頁面出錯的虛擬地址和引發的內存訪問(RAM)類型。
Linux 必須找到表明發生頁面錯誤的內存區域的 vm_area_struct 結構。因爲搜索 vm_area_struct 數據結構對於有效處理頁面錯誤相當重要,所以它們以 AVL(Adelson-Velskii和Landis)
樹結構連接在一塊兒。若是引發故障的虛擬地址沒有 vm_area_struct
結構,則此進程已經訪問了非法地址,Linux 會向進程發出 SIGSEGV
信號,若是進程沒有用於該信號的處理程序,那麼進程將會終止。
而後,Linux 會針對此虛擬內存區域所容許的訪問類型,檢查發生的頁面錯誤類型。 若是該進程以非法方式訪問內存,例如寫入僅容許讀的區域,則還會發出內存訪問錯誤信號。
如今,Linux 已肯定頁面錯誤是合法的,所以必須對其進行處理。
在 Linux 中,最直觀、最可見的部分就是 文件系統(file system)
。下面咱們就來一塊兒探討一下關於 Linux 中國的文件系統,系統調用以及文件系統實現背後的原理和思想。這些思想中有一些來源於 MULTICS,如今已經被 Windows 等其餘操做系統使用。Linux 的設計理念就是 小的就是好的(Small is Beautiful)
。雖然 Linux 只是使用了最簡單的機制和少許的系統調用,可是 Linux 卻提供了強大而優雅的文件系統。
Linux 在最初的設計是 MINIX1 文件系統,它只支持 14 字節的文件名,它的最大文件只支持到 64 MB。在 MINIX 1 以後的文件系統是 ext 文件系統。ext 系統相較於 MINIX 1 來講,在支持字節大小和文件大小上均有很大提高,可是 ext 的速度仍沒有 MINIX 1 快,因而,ext 2 被開發出來,它可以支持長文件名和大文件,並且具備比 MINIX 1 更好的性能。這使他成爲 Linux 的主要文件系統。只不過 Linux 會使用 VFS
曾支持多種文件系統。在 Linux 連接時,用戶能夠動態的將不一樣的文件系統掛載倒 VFS 上。
Linux 中的文件是一個任意長度的字節序列,Linux 中的文件能夠包含任意信息,好比 ASCII 碼、二進制文件和其餘類型的文件是不加區分的。
爲了方便起見,文件能夠被組織在一個目錄中,目錄存儲成文件的形式在很大程度上能夠做爲文件處理。目錄能夠有子目錄,這樣造成有層次的文件系統,Linux 系統下面的根目錄是 /
,它一般包含了多個子目錄。字符 /
還用於對目錄名進行區分,例如 /usr/cxuan 表示的就是根目錄下面的 usr 目錄,其中有一個叫作 cxuan 的子目錄。
下面咱們介紹一下 Linux 系統根目錄下面的目錄名
/bin
,它是重要的二進制應用程序,包含二進制文件,系統的全部用戶使用的命令都在這裏/boot
,啓動包含引導加載程序的相關文件/dev
,包含設備文件,終端文件,USB 或者鏈接到系統的任何設備/etc
,配置文件,啓動腳本等,包含全部程序所須要的配置文件,也包含了啓動/中止單個應用程序的啓動和關閉 shell 腳本/home
,本地主要路徑,全部用戶用 home 目錄存儲我的信息/lib
,系統庫文件,包含支持位於 /bin 和 /sbin 下的二進制庫文件/lost+found
,在根目錄下提供一個遺失+查找系統,必須在 root 用戶下才能查看當前目錄下的內容/media
,掛載可移動介質/mnt
,掛載文件系統/opt
,提供一個可選的應用程序安裝目錄/proc
,特殊的動態目錄,用於維護系統信息和狀態,包括當前運行中進程信息/root
,root 用戶的主要目錄文件夾/sbin
,重要的二進制系統文件/tmp
, 系統和用戶建立的臨時文件,系統重啓時,這個目錄下的文件都會被刪除/usr
,包含絕大多數用戶都能訪問的應用程序和文件/var
,常常變化的文件,諸如日誌文件或數據庫等在 Linux 中,有兩種路徑,一種是 絕對路徑(absolute path)
,絕對路徑告訴你從根目錄下查找文件,絕對路徑的缺點是太長並且不太方便。還有一種是 相對路徑(relative path)
,相對路徑所在的目錄也叫作工做目錄(working directory)
。
若是 /usr/local/books
是工做目錄,那麼 shell 命令
cp books books-replica
就表示的是相對路徑,而
cp /usr/local/books/books /usr/local/books/books-replica
則表示的是絕對路徑。
在 Linux 中常常出現一個用戶使用另外一個用戶的文件或者使用文件樹結構中的文件。兩個用戶共享同一個文件,這個文件位於某個用戶的目錄結構中,另外一個用戶須要使用這個文件時,必須經過絕對路徑才能引用到他。若是絕對路徑很長,那麼每次輸入起來會變的很是麻煩,因此 Linux 提供了一種 連接(link)
機制。
舉個例子,下面是一個使用連接以前的圖
以上所示,好比有兩個工做帳戶 jianshe 和 cxuan,jianshe 想要使用 cxuan 帳戶下的 A 目錄,那麼它可能會輸入 /usr/cxuan/A
,這是一種未使用連接以後的圖。
使用連接後的示意以下
如今,jianshe 能夠建立一個連接來使用 cxuan 下面的目錄了。‘
當一個目錄被建立出來後,有兩個目錄項也同時被建立出來,它們就是 .
和 ..
,前者表明工做目錄自身,後者表明該目錄的父目錄,也就是該目錄所在的目錄。這樣一來,在 /usr/jianshe 中訪問 cxuan 中的目錄就是 ../cxuan/xxx
Linux 文件系統不區分磁盤的,這是什麼意思呢?通常來講,一個磁盤中的文件系統相互之間保持獨立,若是一個文件系統目錄想要訪問另外一個磁盤中的文件系統,在 Windows 中你能夠像下面這樣。
兩個文件系統分別在不一樣的磁盤中,彼此保持獨立。
而在 Linux 中,是支持掛載
的,它容許一個磁盤掛在到另一個磁盤上,那麼上面的關係會變成下面這樣
掛在以後,兩個文件系統就再也不須要關心文件系統在哪一個磁盤上了,兩個文件系統彼此可見。
Linux 文件系統的另一個特性是支持 加鎖(locking)
。在一些應用中會出現兩個或者更多的進程同時使用同一個文件的狀況,這樣極可能會致使競爭條件(race condition)
。一種解決方法是對其進行加不一樣粒度的鎖,就是爲了防止某一個進程只修改某一行記錄從而致使整個文件都不能使用的狀況。
POSIX 提供了一種靈活的、不一樣粒度級別的鎖機制,容許一個進程使用一個不可分割的操做對一個字節或者整個文件進行加鎖。加鎖機制要求嘗試加鎖的進程指定其 要加鎖的文件,開始位置以及要加鎖的字節
Linux 系統提供了兩種鎖:共享鎖和互斥鎖。若是文件的一部分已經加上了共享鎖,那麼再加排他鎖是不會成功的;若是文件系統的一部分已經被加了互斥鎖,那麼在互斥鎖解除以前的任何加鎖都不會成功。爲了成功加鎖、請求加鎖的部分的全部字節都必須是可用的。
在加鎖階段,進程須要設計好加鎖失敗後的狀況,也就是判斷加鎖失敗後是否選擇阻塞,若是選擇阻塞式,那麼當已經加鎖的進程中的鎖被刪除時,這個進程會解除阻塞並替換鎖。若是進程選擇非阻塞式的,那麼就不會替換這個鎖,會馬上從系統調用中返回,標記狀態碼錶示是否加鎖成功,而後進程會選擇下一個時間再次嘗試。
加鎖區域是能夠重疊的。下面咱們演示了三種不一樣條件的加鎖區域。
如上圖所示,A 的共享鎖在第四字節到第八字節進行加鎖
如上圖所示,進程在 A 和 B 上同時加了共享鎖,其中 6 - 8 字節是重疊鎖
如上圖所示,進程 A 和 B 和 C 同時加了共享鎖,那麼第六字節和第七字節是共享鎖。
若是此時一個進程嘗試在第 6 個字節處加鎖,此時會設置失敗並阻塞,因爲該區域被 A B C 同時加鎖,那麼只有等到 A B C 都釋放鎖後,進程才能加鎖成功。
許多系統調用都會和文件與文件系統有關。咱們首先先看一下對單個文件的系統調用,而後再來看一下對整個目錄和文件的系統調用。
爲了建立一個新的文件,會使用到 creat
方法,注意沒有 e
。
這裏說一個小插曲,曾經有人問 UNIX 創始人 Ken Thompson,若是有機會從新寫 UNIX ,你會怎麼辦,他回答本身要把 creat 改爲 create ,哈哈哈哈。
這個系統調用的兩個參數是文件名和保護模式
fd = creat("aaa",mode);
這段命令會建立一個名爲 aaa 的文件,並根據 mode 設置文件的保護位。這些位決定了哪一個用戶可能訪問文件、如何訪問。
creat 系統調用不只僅建立了一個名爲 aaa 的文件,還會打開這個文件。爲了容許後續的系統調用訪問這個文件,這個 creat 系統調用會返回一個 非負整數
, 這個就叫作 文件描述符(file descriptor)
,也就是上面的 fd。
若是在已經存在的文件上調用了 creat 系統調用,那麼該文件中的內容會被清除,從 0 開始。經過設置合適的參數,open
系統調用也可以建立文件。
下面讓咱們看一看主要的系統調用,以下表所示
系統調用 | 描述 |
---|---|
fd = creat(name,mode) | 一種建立一個新文件的方式 |
fd = open(file, ...) | 打開文件讀、寫或者讀寫 |
s = close(fd) | 關閉一個打開的文件 |
n = read(fd, buffer, nbytes) | 從文件中向緩存中讀入數據 |
n = write(fd, buffer, nbytes) | 從緩存中向文件中寫入數據 |
position = lseek(fd, offset, whence) | 移動文件指針 |
s = stat(name, &buf) | 獲取文件信息 |
s = fstat(fd, &buf) | 獲取文件信息 |
s = pipe(&fd[0]) | 建立一個管道 |
s = fcntl(fd,...) | 文件加鎖等其餘操做 |
爲了對一個文件進行讀寫的前提是先須要打開文件,必須使用 creat 或者 open 打開,參數是打開文件的方式,是隻讀、可讀寫仍是隻寫。open 系統調用也會返回文件描述符。打開文件後,須要使用 close
系統調用進行關閉。close 和 open 返回的 fd 老是未被使用的最小數量。
什麼是文件描述符?文件描述符就是一個數字,這個數字標示了計算機操做系統中打開的文件。它描述了數據資源,以及訪問資源的方式。
當程序要求打開一個文件時,內核會進行以下操做
全局文件表(global file table)
中建立一個條目(entry)
文件描述符由惟一的非負整數組成,系統上每一個打開的文件至少存在一個文件描述符。文件描述符最初在 Unix 中使用,而且被包括 Linux,macOS 和 BSD 在內的現代操做系統所使用。
當一個進程成功訪問一個打開的文件時,內核會返回一個文件描述符,這個文件描述符指向全局文件表的 entry 項。這個文件表項包含文件的 inode 信息,字節位移,訪問限制等。例以下圖所示
默認狀況下,前三個文件描述符爲 STDIN(標準輸入)
、STDOUT(標準輸出)
、STDERR(標準錯誤)
。
標準輸入的文件描述符是 0 ,在終端中,默認爲用戶的鍵盤輸入
標準輸出的文件描述符是 1 ,在終端中,默認爲用戶的屏幕
與錯誤有關的默認數據流是 2,在終端中,默認爲用戶的屏幕。
在簡單聊了一下文件描述符後,咱們繼續回到文件系統調用的探討。
在文件系統調用中,開銷最大的就是 read 和 write 了。read 和 write 都有三個參數
文件描述符
:告訴須要對哪個打開文件進行讀取和寫入緩衝區地址
:告訴數據須要從哪裏讀取和寫入哪裏統計
:告訴須要傳輸多少字節這就是全部的參數了,這個設計很是簡單輕巧。
雖然幾乎全部程序都按順序讀取和寫入文件,可是某些程序須要可以隨機訪問文件的任何部分。與每一個文件相關聯的是一個指針,該指針指示文件中的當前位置。順序讀取(或寫入)時,它一般指向要讀取(寫入)的下一個字節。若是指針在讀取 1024 個字節以前位於 4096 的位置,則它將在成功讀取系統調用後自動移至 5120 的位置。
Lseek
系統調用會更改指針位置的值,以便後續對 read 或 write 的調用能夠在文件中的任何位置開始,甚至能夠超出文件末尾。
lseek = Lseek ,段首大寫。
lseek 避免叫作 seek 的緣由就是 seek 已經在以前 16 位的計算機上用於搜素功能了。
Lseek
有三個參數:第一個是文件的文件描述符,第二個是文件的位置;第三個告訴文件位置是相對於文件的開頭,當前位置仍是文件的結尾
lseek(int fildes, off_t offset, int whence);
lseek 的返回值是更改文件指針後文件中的絕對位置。lseek 是惟一歷來不會形成真正磁盤查找的系統調用,它只是更新當前的文件位置,這個文件位置就是內存中的數字。
對於每一個文件,Linux 都會跟蹤文件模式(常規,目錄,特殊文件),大小,最後修改時間以及其餘信息。程序可以經過 stat
系統調用看到這些信息。第一個參數就是文件名,第二個是指向要放置請求信息結構的指針。這些結構的屬性以下圖所示。
存儲文件的設備 |
---|
存儲文件的設備 |
i-node 編號 |
文件模式(包括保護位信息) |
文件連接的數量 |
文件全部者標識 |
文件所屬的組 |
文件大小(字節) |
建立時間 |
最後一個修改/訪問時間 |
fstat
調用和 stat
相同,只有一點區別,fstat 能夠對打開文件進行操做,而 stat 只能對路徑進行操做。
pipe
文件系統調用被用來建立 shell 管道。它會建立一系列的僞文件
,來緩衝和管道組件之間的數據,而且返回讀取或者寫入緩衝區的文件描述符。在管道中,像是以下操做
sort <in | head –40
sort 進程將會輸出到文件描述符1,也就是標準輸出,寫入管道中,而 head 進程將從管道中讀入。在這種方式中,sort 只是從文件描述符 0 中讀取並寫入到文件描述符 1 (管道)中,甚至不知道它們已經被重定向了。若是沒有重定向的話,sort 會自動的從鍵盤讀入並輸出到屏幕中。
最後一個系統調用是 fcntl
,它用來鎖定和解鎖文件,應用共享鎖和互斥鎖,或者是執行一些文件相關的其餘操做。
如今咱們來關心一下和總體目錄和文件系統相關的系統調用,而不是把精力放在單個的文件上,下面列出了這些系統調用,咱們一塊兒來看一下。
系統調用 | 描述 |
---|---|
s = mkdir(path,mode) | 建立一個新的目錄 |
s = rmdir(path) | 移除一個目錄 |
s = link(oldpath,newpath) | 建立指向已有文件的連接 |
s = unlink(path) | 取消文件的連接 |
s = chdir(path) | 改變工做目錄 |
dir = opendir(path) | 打開一個目錄讀取 |
s = closedir(dir) | 關閉一個目錄 |
dirent = readdir(dir) | 讀取一個目錄項 |
rewinddir(dir) | 迴轉目錄使其在此使用 |
能夠使用 mkdir 和 rmdir 建立和刪除目錄。可是須要注意,只有目錄爲空時才能夠刪除。
建立一個指向已有文件的連接時會建立一個目錄項(directory entry)
。系統調用 link 來建立連接,oldpath 表明已有的路徑,newpath 表明須要連接的路徑,使用 unlink
能夠刪除目錄項。當文件的最後一個連接被刪除時,這個文件會被自動刪除。
使用 chdir
系統調用能夠改變工做目錄。
最後四個系統調用是用於讀取目錄的。和普通文件相似,他們能夠被打開、關閉和讀取。每次調用 readdir
都會以固定的格式返回一個目錄項。用戶不能對目錄執行寫操做,可是能夠使用 creat 或者 link 在文件夾中建立一個目錄,或使用 unlink 刪除一個目錄。用戶不能在目錄中查找某個特定文件,可是能夠使用 rewindir
做用於一個打開的目錄,使他能在此從頭開始讀取。
下面咱們主要討論一下 虛擬文件系統(Virtual File System)
。 VFS 對高層進程和應用程序隱藏了 Linux 支持的全部文件系統的區別,以及文件系統是存儲在本地設備,仍是須要經過網絡訪問遠程設備。設備和其餘特殊文件和 VFS 層相關聯。接下來,咱們就會探討一下第一個 Linux 普遍傳播的文件系統: ext2
。隨後,咱們就會探討 ext4
文件系統所作的改進。各類各樣的其餘文件系統也正在使用中。 全部 Linux 系統均可以處理多個磁盤分區,每一個磁盤分區上都有不一樣的文件系統。
爲了可以使應用程序可以在不一樣類型的本地或者遠程設備上的文件系統進行交互,由於在 Linux 當中文件系統千奇百種,比較常見的有 EXT三、EXT4,還有基於內存的 ramfs、tmpfs 和基於網絡的 nfs,和基於用戶態的 fuse,固然 fuse 應該不能徹底的文件系統,只能算是一個能把文件系統實現放到用戶態的模塊,知足了內核文件系統的接口,他們都是文件系統的一種實現。對於這些文件系統,Linux 作了一層抽象就是 VFS
虛擬文件系統,
下表總結了 VFS 支持的四個主要的文件系統結構。
對象 | 描述 |
---|---|
超級塊 | 特定的文件系統 |
Dentry | 目錄項,路徑的一個組成部分 |
I-node | 特定的文件 |
File | 跟一個進程相關聯的打開文件 |
超級塊(superblock)
包含了有關文件系統佈局的重要信息,超級塊若是遭到破壞那麼就會致使整個文件系統不可讀。
i-node
索引節點,包含了每個文件的描述符。
在 Linux 中,目錄和設備也表示爲文件,由於它們具備對應的 i-node
超級塊和索引塊所在的文件系統都在磁盤上有對應的結構。
爲了便於某些目錄操做和路徑遍歷,好比 /usr/local/cxuan,VFS 支持一個 dentry
數據結構,該數據結構表明着目錄項。這個 dentry 數據結構有不少東西(http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec7.html)這個數據結構由文件系統動態建立。
目錄項被緩存在 dentry_cache
緩存中。例如,緩存條目會緩存 /usr 、 /usr/local 等條目。若是多個進程經過硬鏈接訪問相同的文件,他們的文件對象將指向此緩存中的相同條目。
最後,文件數據結構是表明着打開的文件,也表明着內存表示,它根據 open 系統調用建立。它支持 read、write、sendfile、lock 和其餘在咱們以前描述的系統調用中。
在 VFS 下實現的實際文件系統不須要在內部使用徹底相同的抽象和操做。 可是,它們必須在語義上實現與 VFS 對象指定的文件系統操做相同的文件系統操做。 四個 VFS 對象中每一個對象的操做數據結構的元素都是指向基礎文件系統中功能的指針。
如今咱們一塊兒看一下 Linux 中最流行的一個磁盤文件系統,那就是 ext2
。Linux 的第一個版本用於 MINIX1
文件系統,它的文件名大小被限制爲最大 64 MB。MINIX 1 文件系統被永遠的被它的擴展系統 ext 取代,由於 ext 容許更長的文件名和文件大小。因爲 ext 的性能低下,ext 被其替代者 ext2 取代,ext2 目前仍在普遍使用。
一個 ext2 Linux 磁盤分區包含了一個文件系統,這個文件系統的佈局以下所示
Boot 塊也就是第 0 塊不是讓 Linux 使用的,而是用來加載和引導計算機啓動代碼的。在塊 0 以後,磁盤分區被分紅多個組,這些組與磁盤柱面邊界所處的位置無關。
第一個塊是 超級塊(superblock)
。它包含有關文件系統佈局的信息,包括 i-node、磁盤塊數量和以及空閒磁盤塊列表的開始。下一個是 組描述符(group descriptor)
,其中包含有關位圖的位置,組中空閒塊和 i-node 的數量以及組中的目錄數量的信息。這些信息很重要,由於 ext2 會在磁盤上均勻分佈目錄。
圖中的兩個位圖用來記錄空閒塊和空閒 i-node,這是從 MINIX 1文件系統繼承的選擇,大多數 UNIX 文件系統使用位圖而不是空閒列表。每一個位圖的大小是一個塊。若是一個塊的大小是 1 KB,那麼就限制了塊組的數量是 8192 個塊和 8192 個 i-node。塊的大小是一個嚴格的限制,塊組的數量不固定,在 4KB 的塊中,塊組的數量增大四倍。
在超級塊以後分佈的是 i-node
它們本身,i-node 取值範圍是 1 - 某些最大值。每一個 i-node 是 128 字節的 long
,這些字節剛好可以描述一個文件。i-node 包含了統計信息(包含了 stat
系統調用能得到的全部者信息,實際上 stat 就是從 i-node 中讀取信息的),以及足夠的信息來查找保存文件數據的全部磁盤塊。
在 i-node 以後的是 數據塊(data blocks)
。全部的文件和目錄都保存在這。若是一個文件或者目錄包含多個塊,那麼這些塊在磁盤中的分佈不必定是連續的,也有可能不連續。事實上,大文件塊可能會被拆分紅不少小塊散佈在整個磁盤上。
對應於目錄的 i-node 分散在整個磁盤組上。若是有足夠的空間,ext2 會把普通文件組織到與父目錄相同的塊組中,而把同一塊上的數據文件組織成初始 i-node
節點。位圖用來快速肯定新文件系統數據的分配位置。在分配新的文件塊時,ext2 也會給該文件預分配許多額外的數據塊,這樣能夠減小未來向文件寫入數據時產生的文件碎片。這種策略在整個磁盤上實現了文件系統的 負載
,後續還有對文件碎片的排列和整理,並且性能也比較好。
爲了達到訪問的目的,須要首先使用 Linux 系統調用,例如 open
,這個系統調用會肯定打開文件的路徑。路徑分爲兩種,相對路徑
和 絕對路徑
。若是使用相對路徑,那麼就會從當前目錄開始查找,不然就會從根目錄進行查找。
目錄文件的文件名最高不能超過 255 個字符,它的分配以下圖所示
每個目錄都由整數個磁盤塊組成,這樣目錄就能夠總體的寫入磁盤。在一個目錄中,文件和子目錄的目錄項都是未經排序的,而且一個挨着一個。目錄項不能跨越磁盤塊,因此一般在每一個磁盤塊的尾部會有部分未使用的字節。
上圖中每一個目錄項都由四個固定長度的屬性和一個長度可變的屬性組成。第一個屬性是 i-node
節點數量,文件 first 的 i-node 編號是 19 ,文件 second 的編號是 42,目錄 third 的 i-node 編號是 88。緊隨其後的是 rec_len
域,代表目錄項大小是多少字節,名稱後面會有一些擴展,當名字以未知長度填充時,這個域被用來尋找下一個目錄項,直至最後的未使用。這也是圖中箭頭的含義。緊隨其後的是 類型域
:F 表示的是文件,D 表示的是目錄,最後是固定長度的文件名,上面的文件名的長度依次是 五、六、5,最後以文件名結束。
rec_len 域是如何擴展的呢?以下圖所示
咱們能夠看到,中間的 second
被移除了,因此將其所在的域變爲第一個目錄項的填充。固然,這個填充能夠做爲後續的目錄項。
因爲目錄是按照線性的順序進行查找的,所以可能須要很長時間才能在大文件末尾找到目錄項。所以,系統會爲近期的訪問目錄維護一個緩存。這個緩存用文件名來查找,若是緩存命中,那麼就會避免線程搜索這樣昂貴的開銷。組成路徑的每一個部分都在目錄緩存中保存一個 dentry
對象,而且經過 i-node 找到後續的路徑元素的目錄項,直到找到真正的文件 i - node。
好比說要使用絕對路徑來尋找一個文件,咱們暫定這個路徑是 /usr/local/file
,那麼須要通過以下幾個步驟:
壞塊
索引節點。系統會將一項放在 dentry 緩存中,以應對未來對根目錄的查找。usr
,獲得 /usr 目錄的 i - node 節點號。/usr 的 i - node 一樣也進入 dentry 緩存。而後節點被取出,並從中解析出磁盤塊,這樣就能夠讀取 /usr 目錄並查找字符串 local
了。一旦找到這個目錄項,目錄 /usr/local
的 i - node 節點就能夠從中得到。有了 /usr/local 的 i - node 節點號,就能夠讀取 i - node 並肯定目錄所在的磁盤塊。最後,從 /usr/local 目錄查找 file 並肯定其 i - node 節點呢號。若是文件存在,那麼系統會提取 i - node 節點號並把它做爲索引在 i - node 節點表中定位相應的 i - node 節點並裝入內存。i - node 被存放在 i - node 節點表(i-node table)
中,節點表是一個內核數據結構,它會持有當前打開文件和目錄的 i - node 節點號。下面是一些 Linux 文件系統支持的 i - node 數據結構。
屬性 | 字節 | 描述 |
---|---|---|
Mode | 2 | 文件屬性、保護位、setuid 和 setgid 位 |
Nlinks | 2 | 指向 i - node 節點目錄項的數目 |
Uid | 2 | 文件全部者的 UID |
Gid | 2 | 文件全部者的 GID |
Size | 4 | 文件字節大小 |
Addr | 60 | 12 個磁盤塊以及後面 3 個間接塊的地址 |
Gen | 1 | 每次重複使用 i - node 時增長的代號 |
Atime | 4 | 最近訪問文件的時間 |
Mtime | 4 | 最近修改文件的時間 |
Ctime | 4 | 最近更改 i - node 的時間 |
如今咱們來一塊兒探討一下文件讀取過程,還記得 read
函數是如何調用的嗎?
n = read(fd,buffer,nbytes);
當內核接管後,它會從這三個參數以及內部表與用戶有關的信息開始。內部表的其中一項是文件描述符數組。文件描述符數組用文件描述符
做爲索引併爲每個打開文件保存一個表項。
文件是和 i - node 節點號相關的。那麼如何經過一個文件描述符找到文件對應的 i - node 節點呢?
這裏使用的一種設計思想是在文件描述符表和 i - node 節點表之間插入一個新的表,叫作 打開文件描述符(open-file-description table)
。文件的讀寫位置會在打開文件描述符表中存在,以下圖所示
咱們使用 shell 、P1 和 P2 來描述一下父進程、子進程、子進程的關係。Shell 首先生成 P1,P1 的數據結構就是 Shell 的一個副本,所以二者都指向相同的打開文件描述符的表項。當 P1 運行完成後,Shell 的文件描述符仍會指向 P1 文件位置的打開文件描述。而後 Shell 生成了 P2,新的子進程自動繼承文件的讀寫位置,甚至 P2 和 Shell 都不知道文件具體的讀寫位置。
上面描述的是父進程和子進程這兩個 相關
進程,若是是一個不相關進程打開文件時,它將獲得本身的打開文件描述符表項,以及本身的文件讀寫位置,這是咱們須要的。
所以,打開文件描述符至關因而給相關進程提供同一個讀寫位置,而給不相關進程提供各自私有的位置。
i - node 包含三個間接塊的磁盤地址,它們每一個指向磁盤塊的地址所可以存儲的大小不同。
爲了防止因爲系統崩潰和電源故障形成的數據丟失,ext2 系統必須在每一個數據塊建立以後當即將其寫入到磁盤上,磁盤磁頭尋道操做致使的延遲是沒法讓人忍受的。爲了加強文件系統的健壯性,Linux 依靠日誌文件系統
,ext3 是一個日誌文件系統,它在 ext2 文件系統的基礎之上作了改進,ext4 也是 ext3 的改進,ext4 也是一個日誌文件系統。ext4 改變了 ext3 的塊尋址方案,從而支持更大的文件和更大的文件系統大小。下面咱們就來描述一下 ext4 文件系統的特性。
具備記錄的文件系統最基本的功能就是記錄日誌
,這個日誌記錄了按照順序描述全部文件系統的操做。經過順序寫出文件系統數據或元數據的更改,操做不受磁盤訪問期間磁盤頭移動的開銷。最終,這個變動會寫入並提交到合適的磁盤位置上。若是這個變動在提交到磁盤前文件系統宕機了,那麼在重啓期間,系統會檢測到文件系統未正確卸載,那麼就會遍歷日誌並應用日誌的記錄來對文件系統進行更改。
Ext4 文件系統被設計用來高度匹配 ext2 和 ext3 文件系統的,儘管 ext4 文件系統在內核數據結構和磁盤佈局上都作了變動。儘管如此,一個文件系統可以從 ext2 文件系統上卸載後成功的掛載到 ext4 文件系統上,並提供合適的日誌記錄。
日誌是做爲循環緩衝區管理的文件。日誌能夠存儲在與主文件系統相同或者不一樣的設備上。日誌記錄的讀寫操做會由單獨的 JBD(Journaling Block Device)
來扮演。
JBD 中有三個主要的數據結構,分別是 log record(日誌記錄)、原子操做和事務。一個日誌記錄描述了一個低級別的文件系統操做,這個操做一般致使塊內的變化。由於像是 write
這種系統調用會包含多個地方的改動 --- i - node 節點,現有的文件塊,新的文件塊和空閒列表等。相關的日誌記錄會以原子性的方式分組。ext4 會通知系統調用進程的開始和結束,以此使 JBD 可以確保原子操做的記錄都能被應用,或者一個也不被應用。最後,主要從效率方面考慮,JBD 會視原子操做的集合爲事務。一個事務中的日誌記錄是連續存儲的。只有在全部的變動一塊兒應用到磁盤後,日誌記錄纔可以被丟棄。
因爲爲每一個磁盤寫出日誌的開銷會很大,因此 ext4 能夠配置爲保留全部磁盤更改的日誌,或者僅僅保留與文件系統元數據相關的日誌更改。僅僅記錄元數據能夠減小系統開銷,提高性能,但不能保證不會損壞文件數據。其餘的幾個日誌系統維護着一系列元數據操做的日誌,例如 SGI 的 XFS。
另一個 Linux 文件系統是 /proc
(process) 文件系統
它的主要思想來源於貝爾實驗室開發的第 8 版的 UNIX,後來被 BSD 和 System V 採用。
然而,Linux 在一些方面上對這個想法進行了擴充。它的基本概念是爲系統中的每一個進程在 /proc
中建立一個目錄。目錄的名字就是進程 PID,以十進制數進行表示。例如,/proc/1024
就是一個進程號爲 1024 的目錄。在該目錄下是進程信息相關的文件,好比進程的命令行、環境變量和信號掩碼等。事實上,這些文件在磁盤上並不存在磁盤中。當須要這些信息的時候,系統會按需從進程中讀取,並以標準格式返回給用戶。
許多 Linux 擴展與 /proc
中的其餘文件和目錄有關。它們包含各類各樣的關於 CPU、磁盤分區、設備、中斷向量、內核計數器、文件系統、已加載模塊等信息。非特權用戶能夠讀取不少這樣的信息,因而就能夠經過一種安全的方式瞭解系統狀況。
從一開始,網絡就在 Linux 中扮演了很重要的做用。下面咱們會探討一下 NFS(Network File System)
網絡文件系統,它在現代 Linux 操做系統的做用是將不一樣計算機上的不一樣文件系統連接成一個邏輯總體。
NFS 最基本的思想是容許任意選定的一些客戶端
和服務器
共享一個公共文件系統。在許多狀況下,全部的客戶端和服務器都會在同一個 LAN(Local Area Network)
局域網內共享,可是這並非必須的。也多是下面這樣的狀況:若是客戶端和服務器距離較遠,那麼它們也能夠在廣域網上運行。客戶端能夠是服務器,服務器能夠是客戶端,可是爲了簡單起見,咱們說的客戶端就是消費服務,而服務器就是提供服務的角度來聊。
每個 NFS 服務都會導出一個或者多個目錄供遠程客戶端訪問。當一個目錄可用時,它的全部子目錄也可用。所以,一般整個目錄樹都會做爲一個總體導出。服務器導出的目錄列表會用一個文件來維護,這個文件是 /etc/exports
,當服務器啓動後,這些目錄能夠自動的被導出。客戶端經過掛載這些導出的目錄來訪問它們。當一個客戶端掛載了一個遠程目錄,這個目錄就成爲客戶端目錄層次的一部分,以下圖所示。
在這個示例中,一號客戶機掛載到服務器的 bin 目錄下,所以它如今能夠使用 shell 訪問 /bin/cat 或者其餘任何一個目錄。一樣,客戶機 1 也能夠掛載到 二號服務器上從而訪問 /usr/local/projects/proj1 或者其餘目錄。二號客戶機一樣能夠掛載到二號服務器上,訪問路徑是 /mnt/projects/proj2。
從上面能夠看到,因爲不一樣的客戶端將文件掛載到各自目錄樹的不一樣位置,同一個文件在不一樣的客戶端有不一樣的訪問路徑和不一樣的名字。掛載點通常一般在客戶端本地,服務器不知道任何一個掛載點的存在。
因爲 NFS 的協議之一是支持 異構
系統,客戶端和服務器可能在不一樣的硬件上運行不一樣的操做系統,所以有必要在服務器和客戶端之間進行接口定義。這樣才能讓任何寫一個新客戶端可以和現有的服務器一塊兒正常工做,反之亦然。
NFS 就經過定義兩個客戶端 - 服務器協議從而實現了這個目標。協議就是客戶端發送給服務器的一連串的請求,以及服務器發送回客戶端的相應答覆。
第一個 NFS 協議是處理掛載。客戶端能夠向服務器發送路徑名而且請求服務器是否可以將服務器的目錄掛載到本身目錄層次上。由於服務器不關心掛載到哪裏,所以請求不會包含掛載地址。若是路徑名是合法的而且指定的目錄已經被導出,那麼服務器會將文件 句柄
返回給客戶端。
文件句柄包含惟一標識文件系統類型,磁盤,目錄的i節點號和安全性信息的字段。
隨後調用讀取和寫入已安裝目錄或其任何子目錄中的文件,都將使用文件句柄。
當 Linux 啓動時會在多用戶以前運行 shell 腳本 /etc/rc 。能夠將掛載遠程文件系統的命令寫入該腳本中,這樣就能夠在容許用戶登錄以前自動掛載必要的遠程文件系統。大部分 Linux 版本是支持自動掛載
的。這個特性會支持將遠程目錄和本地目錄進行關聯。
相對於手動掛載到 /etc/rc 目錄下,自動掛載具備如下優點
另外一方面,咱們默認在自動掛載時全部可選的文件系統都是相同的。因爲 NFS 不提供對文件或目錄複製的支持,用戶須要本身確保這些全部的文件系統都是相同的。所以,大部分的自動掛載都只應用於二進制文件和不多改動的只讀的文件系統。
第二個 NFS 協議是爲文件和目錄的訪問而設計的。客戶端可以經過向服務器發送消息來操做目錄和讀寫文件。客戶端也能夠訪問文件屬性,好比文件模式、大小、上次修改時間。NFS 支持大多數的 Linux 系統調用,可是 open 和 close 系統調用卻不支持。
不支持 open 和 close 並非一種疏忽,而是一種刻意的設計,徹底沒有必要在讀一個文件以前對其進行打開,也沒有必要在讀完時對其進行關閉。
NFS 使用了標準的 UNIX 保護機制,使用 rwx
位來標示全部者(owner)
、組(groups)
、其餘用戶
。最初,每一個請求消息都會攜帶調用者的 groupId 和 userId,NFS 會對其進行驗證。事實上,它會信任客戶端不會發生欺騙行爲。能夠使用公鑰密碼來建立一個安全密鑰,在每次請求和應答中使用它驗證客戶端和服務器。
即便客戶端和服務器的代碼實現是獨立於 NFS 協議的,大部分的 Linux 系統會使用一個下圖的三層實現,頂層是系統調用層,系統調用層可以處理 open 、 read 、 close 這類的系統調用。在解析和參數檢查結束後調用第二層,虛擬文件系統 (VFS)
層。
VFS 層的任務是維護一個表,每一個已經打開的文件都在表中有一個表項。VFS 層爲每個打開的文件維護着一個虛擬i節點
,簡稱爲 v - node。v 節點用來講明文件是本地文件仍是遠程文件。若是是遠程文件的話,那麼 v - node 會提供足夠的信息使客戶端可以訪問它們。對於本地文件,會記錄其所在的文件系統和文件的 i-node ,由於現代操做系統可以支持多文件系統。雖然 VFS 是爲了支持 NFS 而設計的,可是現代操做系統都會使用 VFS,而無論有沒有 NFS。
咱們以前瞭解過了 Linux 的進程和線程、Linux 內存管理,那麼下面咱們就來認識一下 Linux 中的 I/O 管理。
Linux 系統和其餘 UNIX 系統同樣,IO 管理比較直接和簡潔。全部 IO 設備都被看成文件
,經過在系統內部使用相同的 read 和 write 同樣進行讀寫。
Linux 中也有磁盤、打印機、網絡等 I/O 設備,Linux 把這些設備看成一種 特殊文件
整合到文件系統中,通常一般位於 /dev
目錄下。能夠使用與普通文件相同的方式來對待這些特殊文件。
特殊文件通常分爲兩種:
塊特殊文件是一個能存儲固定大小塊
信息的設備,它支持以固定大小的塊,扇區或羣集讀取和(可選)寫入數據。每一個塊都有本身的物理地址
。一般塊的大小在 512 - 65536 之間。全部傳輸的信息都會以連續
的塊爲單位。塊設備的基本特徵是每一個塊都較爲對立,可以獨立的進行讀寫。常見的塊設備有 硬盤、藍光光盤、USB 盤與字符設備相比,塊設備一般須要較少的引腳。
塊特殊文件的缺點基於給定固態存儲器的塊設備比基於相同類型的存儲器的字節尋址要慢一些,由於必須在塊的開頭開始讀取或寫入。因此,要讀取該塊的任何部分,必須尋找到該塊的開始,讀取整個塊,若是不使用該塊,則將其丟棄。要寫入塊的一部分,必須尋找到塊的開始,將整個塊讀入內存,修改數據,再次尋找到塊的開頭處,而後將整個塊寫回設備。
另外一類 I/O 設備是字符特殊文件
。字符設備以字符
爲單位發送或接收一個字符流,而不考慮任何塊結構。字符設備是不可尋址的,也沒有任何尋道操做。常見的字符設備有 打印機、網絡設備、鼠標、以及大多數與磁盤不一樣的設備。
每一個設備特殊文件都會和 設備驅動
相關聯。每一個驅動程序都經過一個 主設備號
來標識。若是一個驅動支持多個設備的話,此時會在主設備的後面新加一個 次設備號
來標識。主設備號和次設備號共同肯定了惟一的驅動設備。
咱們知道,在計算機系統中,CPU 並不直接和設備打交道,它們中間有一個叫做 設備控制器(Device Control Unit)
的組件,例如硬盤有磁盤控制器、USB 有 USB 控制器、顯示器有視頻控制器等。這些控制器就像代理商同樣,它們知道如何應對硬盤、鼠標、鍵盤、顯示器的行爲。
絕大多數字符特殊文件都不能隨機訪問,由於他們須要使用和塊特殊文件不一樣的方式來控制。好比,你在鍵盤上輸入了一些字符,可是你發現輸錯了一個,這時有一些人喜歡使用 backspace
來刪除,有人喜歡用 del
來刪除。爲了中斷正在運行的設備,一些系統使用 ctrl-u
來結束,可是如今通常使用 ctrl-c
來結束。
I/O 的另一個概念是網絡
, 也是由 UNIX 引入,網絡中一個很關鍵的概念就是 套接字(socket)
。套接字容許用戶鏈接到網絡,正如郵筒容許用戶鏈接到郵政系統,套接字的示意圖以下
套接字的位置如上圖所示,套接字能夠動態建立和銷燬。成功建立一個套接字後,系統會返回一個文件描述符(file descriptor)
,在後面的建立連接、讀數據、寫數據、解除鏈接時都須要使用到這個文件描述符。每一個套接字都支持一種特定類型的網絡類型,在建立時指定。通常最經常使用的幾種
可靠的面向鏈接的字節流會使用管道
在兩臺機器之間創建鏈接。可以保證字節從一臺機器按照順序到達另外一臺機器,系統可以保證全部字節都能到達。
除了數據包之間的分界以外,第二種類型和第一種類型是相似的。若是發送了 3 次寫操做,那麼使用第一種方式的接受者會直接接收到全部字節;第二種方式的接受者會分 3 次接受全部字節。除此以外,用戶還能夠使用第三種即不可靠的數據包來傳輸,使用這種傳輸方式的優勢在於高性能,有的時候它比可靠性更加劇要,好比在流媒體中,性能就尤爲重要。
以上涉及兩種形式的傳輸協議,即 TCP
和 UDP
,TCP 是 傳輸控制協議
,它可以傳輸可靠的字節流。UDP
是 用戶數據報協議
,它只可以傳輸不可靠的字節流。它們都屬於 TCP/IP 協議簇中的協議,下面是網絡協議分層
能夠看到,TCP 、UDP 都位於網絡層上,可見它們都把 IP 協議 即 互聯網協議
做爲基礎。
一旦套接字在源計算機和目的計算機創建成功,那麼兩個計算機之間就能夠創建一個連接。通訊一方在本地套接字上使用 listen
系統調用,它就會建立一個緩衝區,而後阻塞直到數據到來。另外一方使用 connect
系統調用,若是另外一方接受 connect 系統調用後,則系統會在兩個套接字之間創建鏈接。
socket 鏈接創建成功後就像是一個管道,一個進程能夠使用本地套接字的文件描述符從中讀寫數據,當鏈接再也不須要的時候使用 close
系統調用來關閉。
Linux 系統中的每一個 I/O 設備都有一個特殊文件(special file)
與之關聯,什麼是特殊文件呢?
在操做系統中,特殊文件是一種在文件系統中與硬件設備相關聯的文件。特殊文件也被稱爲
設備文件(device file)
。特殊文件的目的是將設備做爲文件系統中的文件進行公開。特殊文件爲硬件設備提供了藉口,用於文件 I/O 的工具能夠進行訪問。由於設備有兩種類型,一樣特殊文件也有兩種,即字符特殊文件和塊特殊文件
對於大部分 I/O 操做來講,只用合適的文件就能夠完成,並不須要特殊的系統調用。而後,有時須要一些設備專用的處理。在 POSIX 以前,大多數 UNIX 系統會有一個叫作 ioctl
的系統調用,它用於執行大量的系統調用。隨着時間的發展,POSIX 對其進行了整理,把 ioctl 的功能劃分爲面向終端設備的獨立功能調用,如今已經變成獨立的系統調用了。
下面是幾個管理終端的系統調用
系統調用 | 描述 |
---|---|
tcgetattr | 獲取屬性 |
tcsetattr | 設置屬性 |
cfgetispeed | 獲取輸入速率 |
cfgetospeed | 獲取輸出速率 |
cfsetispeed | 設置輸入速率 |
cfsetospeed | 設置輸出速率 |
Linux 中的 IO 是經過一系列設備驅動實現的,每一個設備類型對應一個設備驅動。設備驅動爲操做系統和硬件分別預留接口,經過設備驅動來屏蔽操做系統和硬件的差別。
當用戶訪問一個特殊的文件時,由文件系統提供此特殊文件的主設備號和次設備號,並判斷它是一個塊特殊文件仍是字符特殊文件。主設備號用於標識字符設備仍是塊設備,次設備號用於參數傳遞。
每一個驅動程序
都有兩部分:這兩部分都是屬於 Linux 內核,也都運行在內核態下。上半部分運行在調用者上下文而且與 Linux 其餘部分交互。下半部分運行在內核上下文而且與設備進行交互。驅動程序能夠調用內存分配、定時器管理、DMA 控制等內核過程。可被調用的內核功能都位於 驅動程序 - 內核接口
的文檔中。
I/O 實現指的就是對字符設備和塊設備的實現
系統中處理塊特殊文件 I/O 部分的目標是爲了使傳輸次數儘量的小。爲了實現這個目標,Linux 系統在磁盤驅動程序和文件系統之間設置了一個 高速緩存(cache)
,以下圖所示
在 Linux 內核 2.2 以前,Linux 系統維護着兩個緩存:頁面緩存(page cache)
和 緩衝區緩存(buffer cache)
,所以,存儲在一個磁盤塊中的文件可能會在兩個緩存中。2.2 版本之後 Linux 內核只有一個統一的緩存一個 通用數據塊層(generic block layer)
把這些融合在一塊兒,實現了磁盤、數據塊、緩衝區和數據頁之間必要的轉換。那麼什麼是通用數據塊層?
通用數據塊層是一個內核的組成部分,用於處理對系統中全部塊設備的請求。通用數據塊主要有如下幾個功能
將數據緩衝區放在內存高位處,當 CPU 訪問數據時,頁面纔會映射到內核線性地址中,而且此後取消映射
實現
零拷貝
機制,磁盤數據能夠直接放入用戶模式的地址空間,而無需先複製到內核內存中管理磁盤卷,會把不一樣塊設備上的多個磁盤分區視爲一個分區。
利用最新的磁盤控制器的高級功能,例如 DMA 等。
cache 是提高性能的利器,無論以什麼樣的目的須要一個數據塊,都會先從 cache 中查找,若是找到直接返回,避免一次磁盤訪問,可以極大的提高系統性能。
若是頁面 cache 中沒有這個塊,操做系統就會把頁面從磁盤中調入內存,而後讀入 cache 進行緩存。
cache 除了支持讀操做外,也支持寫操做,一個程序要寫回一個塊,首先把它寫到 cache 中,而不是直接寫入到磁盤中,等到磁盤中緩存達到必定數量值時再被寫入到 cache 中。
Linux 系統中使用 IO 調度器
來保證減小磁頭的反覆移動從而減小損失。I/O 調度器的做用是對塊設備的讀寫操做進行排序,對讀寫請求進行合併。Linux 有許多調度器的變體,從而知足不一樣的工做須要。最基本的 Linux 調度器是基於傳統的 Linux 電梯調度器(Linux elevator scheduler)
。Linux 電梯調度器的主要工做流程就是按照磁盤扇區的地址排序並存儲在一個雙向鏈表
中。新的請求將會以鏈表的形式插入。這種方法能夠有效的防止磁頭重複移動。由於電梯調度器會容易產生飢餓現象。所以,Linux 在原基礎上進行了修改,維護了兩個鏈表,在 最後日期(deadline)
內維護了排序後的讀寫操做。默認的讀操做耗時 0.5s,默認寫操做耗時 5s。若是在最後期限內等待時間最長的鏈表沒有得到服務,那麼它將優先得到服務。
和字符設備的交互是比較簡單的。因爲字符設備會產生並使用字符流、字節數據,所以對隨機訪問的支持意義不大。一個例外是使用 行規則(line disciplines)
。一個行規能夠和終端設備相關聯,使用 tty_struct
結構來表示,它表示與終端設備交換數據的解釋器,固然這也屬於內核的一部分。例如:行規能夠對行進行編輯,映射回車爲換行等一系列其餘操做。
什麼是行規則?
行規是某些類 UNIX 系統中的一層,終端子系統一般由三層組成:上層提供字符設備接口,下層硬件驅動程序與硬件或僞終端進行交互,中層規則用於實現終端設備共有的行爲。
網絡設備的交互是不同的,雖然 網絡設備(network devices)
也會產生字符流,由於它們的異步(asynchronous)
特性是他們不易與其餘字符設備在同一接口下集成。網絡設備驅動程序會產生不少數據包,經由網絡協議到達用戶應用程序中。
UNIX 設備驅動程序是被靜態加載
到內核中的。所以,只要系統啓動後,設備驅動程序都會被加載到內存中。隨着我的電腦 Linux 的出現,這種靜態連接完成後會使用一段時間的模式被打破。相對於小型機上的 I/O 設備,PC 上可用的 I/O 設備有了數量級的增加。絕大多數用戶沒有能力去添加一個新的應用程序、更新設備驅動、從新鏈接內核,而後進行安裝。
Linux 爲了解決這個問題,引入了 可加載(loadable module)
機制。可加載是在系統運行時添加到內核中的代碼塊。
當一個模塊被加載到內核時,會發生下面幾件事情:第一,在加載的過程當中,模塊會被動態的從新部署。第二,系統會檢查程序程序所需的資源是否可用。若是可用,則把這些資源標記爲正在使用。第三步,設置所需的中斷向量。第四,更新驅動轉換表使其可以處理新的主設備類型。最後再來運行設備驅動程序。
在完成上述工做後,驅動程序就會安裝完成,其餘現代 UNIX 系統也支持可加載機制。
Linux 做爲 MINIX 和 UNIX 的衍生操做系統,從一開始就是一個多用戶
系統。這意味着 Linux 從早期開始就創建了安全和信息訪問控制機制。下面咱們主要探討的就是 Linux 安全性的一些內容
一個 Linux 系統的用戶羣裏由一系列註冊用戶組成,他們每個都有一個惟一的 UID (User ID)。一個 UID 是一個位於 0 到 65535 之間的整數。文件(進程或者是其餘資源)都標記了它的全部者的 UID。默認狀況下,文件的全部者是建立文件的人,文件的全部者是建立文件的用戶。
用戶能夠被分紅許多組,每一個組都會由一個 16 位的整數標記,這個組叫作 GID(組 ID)
。給用戶分組是手動完成的,它由系統管理員執行,分組就是在數據庫中添加一條記錄指明哪一個用戶屬於哪一個組。一個用戶能夠屬於不一樣組。
Linux 中的基本安全機制比較容易理解,每一個進程都會記錄它全部者的 UID 和 GID。當文件建立後,它會獲取建立進程的 UID 和 GID。當一個文件被建立時,它的 UID 和 GID 就會被標記爲進程的 UID 和 GID。這個文件同時會獲取由該進程決定的一些權限。這些權限會指定全部者、全部者所在組的其餘用戶及其餘用戶對文件具備什麼樣的訪問權限。對於這三類用戶而言,潛在的訪問權限是 讀、寫和執行,分別由 r、w 和 x 標記。固然,執行文件的權限僅當文件時可逆二進制程序時纔有意義。試圖執行一個擁有執行權限的非可執行文件,系統會報錯。
Linux 用戶分爲三種
root(超級管理員)
,它的 UID 爲 0,這個用戶有極大的權限,能夠直接無視不少的限制 ,包括讀寫執行的權限。系統用戶
,UID 爲 1~499。普通用戶
,UID 範圍通常是 500~65534。這類用戶的權限會受到基本權限的限制,也會受到來自管理員的限制。不過要注意 nobody 這個特殊的賬號,UID 爲 65534,這個用戶的權限會進一步的受到限制,通常用於實現來賓賬號。Linux 中的每類用戶由 3 個比特爲來標記,因此 9 個比特位就可以表示全部的權限。
下面來看一下一些基本的用戶和權限例子
二進制 | 標記 | 准許的文件訪問權限 |
---|---|---|
111000000 | rwx------ | 全部者可讀、寫和執行 |
111111000 | rwxrwx--- | 全部者和組能夠讀、寫和執行 |
111111111 | rwxrwxrwx | 全部人能夠讀、寫和執行 |
000000000 | --------- | 任何人不擁有任何權限 |
000000111 | ------rwx | 只有組之外的其餘用戶擁有全部權 |
110100100 | rw-r--r-- | 全部者能夠讀和寫,其餘人能夠讀 |
110100100 | rw-r----- | 全部者能夠讀和寫,組能夠讀 |
咱們上面提到,UID 爲 0 的是一個特殊用戶,稱爲 超級用戶(或者根用戶)
。超級用戶可以讀和寫系統中的任何文件,無論這個文件由誰全部,也無論這個文件的保護模式如何。 UID 爲 0 的進程還具備少數調用受保護系統調用的權限,而普通用戶是不可能有這些功能的。一般狀況下,只有系統管理員知道超級用戶的密碼。
在 Linux 系統下,目錄也是一種文件,而且具備和普通文件同樣的保護模式。不一樣的是,目錄的 x 比特位表示查找權限而不是執行權限。所以,若是一個目錄的保護模式是 rwxr-xr-x
,那麼它容許全部者讀、寫和查找目錄,而其餘人只能夠讀和查找,而不容許從中添加或者刪除目錄中的文件。
與 I/O 有關的特殊文件擁有和普通文件同樣的保護位。這種機制能夠用來限制對 I/O 設備的訪問權限。舉個例子,打印機是特殊文件,它的目錄是 /dev/lp
,它能夠被根用戶或者一個叫守護進程的特殊用戶擁有,具備保護模式 rw-------,從而阻止其餘全部人對打印機的訪問。畢竟每一個人都使用打印機的話會發生混亂。
固然,若是 /dev/lp 的保護模式是 rw-------,那就意味着其餘任何人都不能使用打印機。
這個問題經過增長一個保護位 SETUID
到以前的 9 個比特位來解決。當一個進程的 SETUID 位打開,它的 有效 UID
將變成相應可執行文件的全部者 UID,而不是當前使用該進程的用戶的 UID。將訪問打印機的程序設置爲守護進程全部,同時打開 SETUID 位,這樣任何用戶均可以執行此程序,並且擁有守護進程的權限。
除了 SETUID 以外,還有一個 SETGID 位,SETGID 的工做原理和 SETUID 相似。可是這個位通常很不經常使用。
Linux 中關於安全的系統調用不是不少,只有幾個,以下列表所示
系統調用 | 描述 |
---|---|
chmod | 改變文件的保護模式 |
access | 使用真實的 UID 和 GID 測試訪問權限 |
chown | 改變全部者和組 |
setuid | 設置 UID |
setgid | 設置 GID |
getuid | 獲取真實的 UID |
getgid | 獲取真實的 GID |
geteuid | 獲取有效的 UID |
getegid | 獲取有效的 GID |
咱們在平常開發中用到最多的就是 chmod
了,沒想到咱們平常開發過程當中也能用到系統調用啊,chmod 以前咱們一直認爲是改變權限,如今專業一點是改變文件的保護模式。它的具體函數以下
s = chmod("路徑名","值");
例如
s = chmod("/usr/local/cxuan",777);
他就是會把 /usr/local/cxuan
這個路徑的保護模式改成 rwxrwxrwx,任何組和人均可以操做這個路徑。只有該文件的全部者和超級用戶纔有權利更改保護模式。
access
系統調用用來檢驗實際的 UID 和 GID 對某文件是否擁有特定的權限。下面就是四個 getxxx 的系統調用,這些用來獲取 uid 和 gid 的。
注意:其中的 chown、setuid 和 setgid 是超級用戶才能使用,用來改變全部者進程的 UID 和 GID。
當用戶登陸時,登陸程序,也被稱爲 login
,會要求輸入用戶名和密碼。它會對密碼進行哈希處理,而後在 /etc/passwd
中進行查找,看看是否有匹配的項。使用哈希的緣由是防止密碼在系統中以非加密的方式存在。若是密碼正確,登陸程序會在 /etc/passwd 中讀取用戶選擇的 shell 程序的名稱,有多是 bash
,有多是 shell
或者其餘的 csh
或 ksh
。而後登陸程序使用 setuid 和 setgid 這兩個系統調用來把本身的 UID 和 GID 變爲用戶的 UID 和 GID,而後它打開鍵盤做爲標準輸入、標準輸入的文件描述符是 0 ,屏幕做爲標準輸出,文件描述符是 1 ,屏幕也做爲標準錯誤輸出,文件描述符爲 2。最後,執行用戶選擇的 shell 程序,終止。
當任何進程想要打開一個文件,系統首先將文件的 i - node 所記錄的保護位與用戶有效 UID 和 有效 GID 進行對比,來檢查訪問是否容許。若是訪問容許,就打開文件並返回文件描述符;不然不打開文件,返回 - 1。
Linux 安全模型和實如今本質上與大多數傳統的 UNIX 系統相同。
關注公衆號 程序員cxuan 回覆 cxuan 領取優質資料。
我本身寫了六本 PDF ,很是硬核,連接以下
我本身寫了六本 PDF ,很是硬核,連接以下
我本身寫了六本 PDF ,很是硬核,連接以下