Linux 簡介
UNIX 是一個互動式系統,用于同時處理多行程和多用戶同時在線,為什么要說 UNIX,那是因為 Linux 是由 UNIX
發展而來的,UNIX 是由程式員設計,它的主要服務物件也是程式員,Linux 繼承了 UNIX的設計目標,從智能手機到汽車,超級計算機和家用電器,從家用臺式機到企業服務器,Linux 作業系統無處不在,
PS:如果你在學習C/C++的程序中遇到了問題,可以來加入小編的企鵝圈問小編哦~小編很熱情的(●’?’●)

大多數程式員都喜歡讓系統盡量簡單,優雅并具有一致性,舉個例子,從最底層的角度來講,一個檔案應該只是一個位元組集合,為了實作順序存取、隨機存取、按鍵存取、遠程存取只能是妨礙你的作業,相同的,如果命令
ls A*
意味著只列出以 A 為開頭的所有檔案,那么命令
rm A*
應該會移除所有以 A 為開頭的檔案而不是只洗掉檔案名是A*的檔案,這個特性也是最小吃驚原則(principle of least surprise)
最小吃驚原則一半常用于用戶界面和軟體設計,它的原型是:該功能或者特征應該符合用戶的預期,不應該使用戶感到驚訝和震驚,
一些有經驗的程式員通常希望系統具有較強的功能性和靈活性,設計 Linux 的一個基本目標是每個應用程式只做一件事情并把他做好,所以編譯器只負責編譯的作業,編譯器不會產生串列,因為有其他應用比編譯器做的更好,
很多人都不喜歡冗余,為什么在 cp 就能描述清楚你想干什么時候還使用 copy?這完全是在浪費寶貴的hacking time,為了從檔案中提取所有包含字串ard的行,Linux 程式員應該輸入
grep ard f
Linux 介面
Linux 系統是一種金字塔模型的系統,如下所示
應用程式發起系統呼叫把引數放在暫存器中(有時候放在堆疊中),并發出trap系統陷入指令切換用戶態至內核態,因為不能直接在 C 中撰寫 trap 指令,因此 C 提供了一個庫,庫中的函式對應著系統呼叫,有些函式是使用匯編撰寫的,但是能夠從 C 中呼叫,每個函式首先把引數放在合適的位置然后執行系統呼叫指令,因此如果你想要執行 read 系統呼叫的話,C 程式會呼叫 read 函式庫來執行,這里順便提一下,是由 POSIX 指定的庫介面而不是系統呼叫介面,也就是說,POSIX 會告訴一個標準系統應該提供哪些庫程序,它們的引數是什么,它們必須做什么以及它們必須回傳什么結果,
除了作業系統和系統呼叫庫外,Linux 作業系統還要提供一些標準程式,比如文本編輯器、編譯器、檔案操作工具等,直接和用戶打交道的是上面這些應用程式,因此我們可以說 Linux 具有三種不同的介面:系統呼叫介面、庫函式介面和應用程式介面
Linux 中的GUI(Graphical User Interface)和 UNIX 中的非常相似,這種 GUI 創建一個桌面環境,包括視窗、目標和檔案夾、工具列和檔案拖拽功能,一個完整的 GUI 還包括視窗管理器以及各種應用程式,
Linux 上的 GUI 由 X 視窗支持,主要組成部分是 X 服務器、控制鍵盤、滑鼠、顯示幕等,當在 Linux 上使用圖形界面時,用戶可以通過滑鼠點擊運行程式或者打開檔案,通過拖拽將檔案進行復制等,
Linux 組成部分
事實上,Linux 作業系統可以由下面這幾部分構成
引導程式(Bootloader):引導程式是管理計算機啟動程序的軟體,對于大多數用戶而言,只是彈出一個螢屏,但其實內部作業系統做了很多事情內核(Kernel):內核是作業系統的核心,負責管理 CPU、記憶體和外圍設備等,初始化系統(Init System):這是一個引導用戶空間并負責控制守護程式的子系統,一旦從引導加載程式移交了初始引導,它就是用于管理引導程序的初始化系統,后臺行程(Daemon):后臺行程顧名思義就是在后臺運行的程式,比如列印、聲音、調度等,它們可以在引導程序中啟動,也可以在登錄桌面后啟動圖形服務器(Graphical server):這是在監視器上顯示圖形的子系統,通常將其稱為 X 服務器或 X,桌面環境(Desktop environment):這是用戶與之實際互動的部分,有很多桌面環境可供選擇,每個桌面環境都包含內置應用程式,比如檔案管理器、Web 瀏覽器、游戲等應用程式(Applications):桌面環境不提供完整的應用程式,就像 Windows 和 macOS 一樣,Linux 提供了成千上萬個可以輕松找到并安裝的高質量軟體,
盡管 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 腳本其實也是一段程式,shell 腳本中可以對變數進行賦值,也包含回圈控制陳述句比如if、for、while等,shell 的設計目標是讓其看起來和 C 相似(There is no doubt that C is father),由于 shell 也是一個用戶程式,所以用戶可以選擇不同的 shell,
Linux 應用程式
Linux 的命令列也就是 shell,它由大量標準應用程式組成,這些應用程式主要有下面六種
檔案和目錄操作命令過濾器文本程式系統管理程式開發工具,例如編輯器和編譯器其他

除了這些標準應用程式外,還有其他應用程式比如Web 瀏覽器、多媒體播放器、圖片瀏覽器、辦公軟體和游戲程式等,
我們在上面的例子中已經見過了幾個 Linux 的應用程式,比如 sort、cp、ls、head,下面我們再來認識一下其他 Linux 的應用程式,
我們先從幾個例子開始講起,比如
cp a b
是將 a 復制一個副本為 b ,而
mv a b
是將 a 移動到 b ,但是洗掉原檔案,
上面這兩個命令有一些區別,cp是將檔案進行復制,復制完成后會有兩個檔案 a 和 b;而mv相當于是檔案的移動,移動完成后就不再有 a 檔案,cat命令可以把多個檔案內容進行連接,使用rm可以洗掉檔案;使用chmod可以允許所有者改變訪問權限;檔案目錄的的創建和洗掉可以使用mkdir和rmdir命令;使用ls可以查看目錄檔案,ls 可以顯示很多屬性,比如大小、用戶、創建日期等;sort 決定檔案的顯示順序
Linux 應用程式還包括過濾器 grep,grep從標準輸入或者一個或多個輸入檔案中提取特定模式的行;sort將輸入進行排序并輸出到標準輸出;head提取輸入的前幾行;tail 提取輸入的后面幾行;除此之外的過濾器還有cut和paste,允許對文本行的剪切和復制;od將輸入轉換為 ASCII ;tr實作字符大小寫轉換;pr為格式化列印輸出等,
程式編譯工具使用gcc;
make命令用于自動編譯,這是一個很強大的命令,它用于維護一個大的程式,往往這類程式的原始碼由許多檔案構成,典型的,有一些是header files 頭檔案,源檔案通常使用include指令包含這些檔案,make 的作用就是跟蹤哪些檔案屬于頭檔案,然后安排自動編譯的程序,
下面列出了 POSIX 的標準應用程式
程式
應用
ls
列出目錄
cp
復制檔案
head
顯示檔案的前幾行
make
編譯檔案生成二進制檔案
cd
切換目錄
mkdir
創建目錄
chmod
修改檔案訪問權限
ps
列出檔案行程
pr
格式化列印
rm
洗掉一個檔案
rmdir
洗掉檔案目錄
tail
提取檔案最后幾行
tr
字符集轉換
grep
分組
cat
將多個檔案連續標準輸出
od
以八進制顯示檔案
cut
從檔案中剪切
paste
從檔案中粘貼
Linux 內核結構
在上面我們看到了 Linux 的整體結構,下面我們從整體的角度來看一下 Linux 的內核結構

內核直接坐落在硬體上,內核的主要作用就是 I/O 互動、記憶體管理和控制 CPU 訪問,上圖中還包括了中斷和調度器,中斷是與設備互動的主要方式,中斷出現時調度器就會發揮作用,這里的低級代碼停止正在運行的行程,將其狀態保存在內核行程結構中,并啟動驅動程式,行程調度也會發生在內核完成一些操作并且啟動用戶行程的時候,
注意這里的調度器是dispatcher而不是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 允許行程在運行時創建額外的執行緒,
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 行程間通信
Linux 行程間的通信機制通常被稱為Internel-Process communication,IPC下面我們來說一說 Linux 行程間通信的機制,大致來說,Linux 行程間的通信機制可以分為 6 種
信號 signal

信號是 UNIX 系統最先開始使用的行程間通信機制,因為 Linux 是繼承于 UNIX 的,所以 Linux 也支持信號機制,通過向一個或多個行程發送異步事件信號來實作,信號可以從鍵盤或者訪問不存在的位置等地方產生;信號通過 shell 將任務發送給子行程,
行程可以選擇忽略發送過來的信號,但是有兩個是不能忽略的: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 發現輸出
sort <f | head
它會創建兩個行程,一個是 sort,一個是 head,sort,會在這兩個應用程式之間建立一個管道使得
sort 行程的標準輸出作為 head 程式的標準輸入,sort 行程產生的輸出就不用寫到檔案中了,如果管道滿了系統會停止 sort 以等待
head 讀出資料
管道實際上就是|,兩個應用程式不知道有管道的存在,一切都是由 shell 管理和控制的,
共享記憶體 shared memory
兩個行程之間還可以通過共享記憶體進行行程間通信,其中兩個或者多個行程可以訪問公共記憶體空間,兩個行程的共享作業是通過共享記憶體完成的,一個行程所作的修改可以對另一個行程可見(很像執行緒間的通信),
在使用共享記憶體前,需要經過一系列的呼叫流程,流程如下
創建共享記憶體段或者使用已創建的共享記憶體段(shmget())將行程附加到已經創建的記憶體段中(shmat())從已連接的共享記憶體段分離行程(shmdt())對共享記憶體段執行控制操作(shmctl())先入先出佇列 FIFO
先入先出佇列 FIFO 通常被稱為命名管道(Named Pipes),命名管道的作業方式與常規管道非常相似,但是確實有一些明顯的區別,未命名的管道沒有備份檔案:作業系統負責維護記憶體中的緩沖區,用來將位元組從寫入器傳輸到讀取器,一旦寫入或者輸出終止的話,緩沖區將被回收,傳輸的資料會丟失,相比之下,命名管道具有支持檔案和獨特 API ,命名管道在檔案系統中作為設備的專用檔案存在,當所有的行程通信完成后,命名管道將保留在檔案系統中以備后用,命名管道具有嚴格的 FIFO 行為
寫入的第一個位元組是讀取的第一個位元組,寫入的第二個位元組是讀取的第二個位元組,依此類推,
訊息佇列 Message Queue
一聽到訊息佇列這個名詞你可能不知道是什么意思,訊息佇列是用來描述內核尋址空間內的內部鏈接串列,可以按幾種不同的方式將訊息按順序發送到佇列并從佇列中檢索訊息,每個訊息佇列由 IPC 識別符號唯一標識,訊息佇列有兩種模式,一種是嚴格模式, 嚴格模式就像是 FIFO 先入先出佇列似的,訊息順序發送,順序讀取,還有一種模式是非嚴格模式,訊息的順序性不是非常重要,
套接字 Socket
還有一種管理兩個行程間通信的是使用socket,socket 提供端到端的雙相通信,一個套接字可以與一個或多個行程關聯,就像管道有命令管道和未命名管道一樣,套接字也有兩種模式,套接字一般用于兩個行程之間的網路通信,網路套接字需要來自諸如TCP(傳輸控制協議)或較低級別UDP(用戶資料報協議)等基礎協議的支持,
套接字有以下幾種分類
順序包套接字(Sequential Packet Socket): 此類套接字為最大長度固定的資料報提供可靠的連接,此連接是雙向的并且是順序的,資料報套接字(Datagram Socket):資料包套接字支持雙向資料流,資料包套接字接受訊息的順序與發送者可能不同,流式套接字(Stream Socket):流套接字的作業方式類似于電話對話,提供雙向可靠的資料流,原始套接字(Raw Socket): 可以使用原始套接字訪問基礎通信協議,Linux 中行程管理系統呼叫
現在關注一下 Linux 系統中與行程管理相關的系統呼叫,在了解之前你需要先知道一下什么是系統呼叫,
作業系統為我們屏蔽了硬體和軟體的差異,它的最主要功能就是為用戶提供一種抽象,隱藏內部實作,讓用戶只關心在 GUI 圖形界面下如何使用即可,作業系統可以分為兩種模式
內核態:作業系統內核使用的模式用戶態:用戶應用程式所使用的模式
我們常說的背景關系切換指的就是內核態模式和用戶態模式的頻繁切換,而系統呼叫指的就是引起內核態和用戶態切換的一種方式,系統呼叫通常在后臺靜默運行,表示計算機程式向其作業系統內核請求服務,
系統呼叫指令有很多,下面是一些與行程管理相關的最主要的系統呼叫
fork 呼叫用于創建一個與父行程相同的子行程,創建完行程后的子行程擁有和父行程一樣的程式計數器、相同的 CPU 暫存器、相同的打開檔案,
exec 系統呼叫用于執行駐留在活動行程中的檔案,呼叫 exec 后,新的可執行檔案會替換先前的可執行檔案并獲得執行,也就是說,呼叫 exec 后,會將舊檔案或程式替換為新檔案或執行,然后執行檔案或程式,新的執行程式被加載到相同的執行空間中,因此行程的PID不會修改,因為我們沒有創建新行程,只是替換舊行程,但是行程的資料、代碼、堆疊都已經被修改,如果當前要被替換的行程包含多個執行緒,那么所有的執行緒將被終止,新的行程映像被加載執行,
這里需要解釋一下行程映像(Process image)的概念
什么是行程映像呢?行程映像是執行程式時所需要的可執行檔案,通常會包括下面這些東西
代碼段(codesegment/textsegment)
又稱文本段,用來存放指令,運行代碼的一塊記憶體空間
此空間大小在代碼運行前就已經確定
記憶體空間一般屬于只讀,某些架構的代碼也允許可寫
在代碼段中,也有可能包含一些只讀的常數變數,例如字串常量等,
資料段(datasegment)
可讀可寫
存盤初始化的全域變數和初始化的 static 變數
資料段中資料的生存期是隨程式持續性(隨行程持續性) 隨行程持續性:行程創建就存在,行程死亡就消失
bss 段(bsssegment):
可讀可寫
存盤未初始化的全域變數和未初始化的 static 變數
bss 段中的資料一般默認為 0
Data 段
是可讀寫的,因為變數的值可以在運行時更改,此段的大小也固定,
堆疊(stack):
可讀可寫
存盤的是函式或代碼中的區域變數(非 static 變數)
堆疊的生存期隨代碼塊持續性,代碼塊運行就給你分配空間,代碼塊結束,就自動回收空間
堆(heap):
可讀可寫
存盤的是程式運行期間動態分配的 malloc/realloc 的空間
堆的生存期隨行程持續性,從 malloc/realloc 到 free 一直存在
下面是這些區域的構成圖
exec 系統呼叫是一些函式的集合,這些函式是
execlexecleexeclpexecvexecveexecvp
下面來看一下 exec 的作業原理
當前行程映像被替換為新的行程映像新的行程映像是你做為 exec 傳遞的燦睡結束當前正在運行的行程新的行程映像有 PID,相同的環境和一些檔案描述符(因為未替換行程,只是替換了行程映像)CPU 狀態和虛擬記憶體受到影響,當前行程映像的虛擬記憶體映射被新行程映像的虛擬記憶體代替,waitpid
等待子行程結束或終止
exit
在許多計算機作業系統上,計算機行程的終止是通過執行exit系統呼叫命令執行的,0 表示行程能夠正常結束,其他值表示行程以非正常的行為結束,
其他一些常見的系統呼叫如下
系統呼叫指令
描述
pause
掛起信號
nice
改變分時行程的優先級
ptrace
行程跟蹤
kill
向行程發送信號
pipe
創建管道
mkfifo
創建 fifo 的特殊檔案(命名管道)
sigaction
設定對指定信號的處理方法
msgctl
訊息控制操作
semctl
信號量控制
Linux 行程和執行緒的實作Linux 行程
在 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結構,
這個結構中包含行程描述符,行程描述符位于固定的位置,使得 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 執行緒
現在我們來討論一下 Linux 中的執行緒,執行緒是輕量級的行程,想必這句話你已經聽過很多次了,輕量級體現在所有的行程切換都需要清除所有的表、行程間的共享資訊也比較麻煩,一般來說通過管道或者共享記憶體,如果是 fork 函式后的父子行程則使用共享檔案,然而執行緒切換不需要像行程一樣具有昂貴的開銷,而且執行緒通信起來也更方便,執行緒分為兩種:用戶級執行緒和內核級執行緒

用戶級執行緒
用戶級執行緒避免使用內核,通常,每個執行緒會顯示呼叫開關,發送信號或者執行某種切換操作來放棄 CPU,同樣,計時器可以強制進行開關,用戶執行緒的切換速度通常比內核執行緒快很多,在用戶級別實作執行緒會有一個問題,即單個執行緒可能會壟斷 CPU 時間片,導致其他執行緒無法執行從而餓死,如果執行一個 I/O 操作,那么 I/O 會阻塞,其他執行緒也無法運行,
一種解決方案是,一些用戶級的執行緒包解決了這個問題,可以使用時鐘周期的監視器來控制第一時間時間片獨占,然后,一些庫通過特殊的包裝來解決系統呼叫的 I/O 阻塞問題,或者可以為非阻塞 I/O 撰寫任務,
內核級執行緒
內核級執行緒通常使用幾個行程表在內核中實作,每個任務都會對應一個行程表,在這種情況下,內核會在每個行程的時間片內調度每個執行緒,
所有能夠阻塞的呼叫都會通過系統呼叫的方式來實作,當一個執行緒阻塞時,內核可以進行選擇,是運行在同一個行程中的另一個執行緒(如果有就緒執行緒的話)還是運行一個另一個行程中的執行緒,
從用戶空間 -> 內核空間 -> 用戶空間的開銷比較大,但是執行緒初始化的時間損耗可以忽略不計,這種實作的好處是由時鐘決定執行緒切換時間,因此不太可能將時間片與任務中的其他執行緒占用時間系結到一起,同樣,I/O 阻塞也不是問題,
混合實作
結合用戶空間和內核空間的優點,設計人員采用了一種內核級執行緒的方式,然后將用戶級執行緒與某些或者全部內核執行緒多路復用起來
在這種模型中,編程人員可以自由控制用戶執行緒和內核執行緒的數量,具有很大的靈活度,采用這種方法,內核只識別內核級執行緒,并對其進行調度,其中一些內核級執行緒會被多個用戶級執行緒多路復用,
Linux 調度
下面我們來關注一下 Linux 系統的調度演算法,首先需要認識到,Linux 系統的執行緒是內核執行緒,所以 Linux 系統是基于執行緒的,而不是基于行程的,
為了進行調度,Linux 系統將執行緒分為三類
實時先入先出實時輪詢分時
實時先入先出執行緒具有最高優先級,它不會被其他執行緒所搶占,除非那是一個剛剛準備好的,擁有更高優先級的執行緒進入,實時輪轉執行緒與實時先入先出執行緒基本相同,只是每個實時輪轉執行緒都有一個時間量,時間到了之后就可以被搶占,如果多個實時執行緒準備完畢,那么每個執行緒運行它時間量所規定的時間,然后插入到實時輪轉執行緒末尾,
注意這個實時只是相對的,無法做到絕對的實時,因為執行緒的運行時間無法確定,它們相對分時系統來說,更加具有實時性
Linux 系統會給每個執行緒分配一個nice值,這個值代表了優先級的概念,nice 值默認值是 0 ,但是可以通過系統呼叫 nice 值來修改,修改值的范圍從 -20 - +19,nice 值決定了執行緒的靜態優先級,一般系統管理員的 nice 值會比一般執行緒的優先級高,它的范圍是 -20 - -1,
下面我們更詳細的討論一下 Linux 系統的兩個調度演算法,它們的內部與調度佇列(runqueue)的設計很相似,運行佇列有一個資料結構用來監視系統中所有可運行的任務并選擇下一個可以運行的任務,每個運行佇列和系統中的每個 CPU 有關,
Linux O(1)調度器是歷史上很流行的一個調度器,這個名字的由來是因為它能夠在常數時間內執行任務調度,在 O(1) 調度器里,調度佇列被組織成兩個陣列,一個是任務正在活動的陣列,一個是任務過期失效的陣列,
大致流程如下:
調度器從正在活動陣列中選擇一個優先級最高的任務,如果這個任務的時間片過期失效了,就把它移動到過期失效陣列中,如果這個任務阻塞了,比如說正在等待
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演算法總是優先調度哪些使用 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),
getty 程式會在終端上輸入
login:
等待用戶輸入用戶名,在輸入用戶名后,getty 程式結束,登陸程式/bin/login開始運行,login 程式需要輸入密碼,并與保存在/etc/passwd中的密碼進行對比,如果輸入正確,login 程式以用戶 shell 程式替換自身,等待第一個命令,如果不正確,login 程式要求輸入另一個用戶名,
Linux 記憶體管理
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 系統呼叫要容易得多,共享庫的訪問就是使用了這種機制,
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),
上面就是 Linux 記憶體映射模型了,在這個頁模型中,虛擬地址由兩部分組成:偏移量和虛擬頁框號,每次處理器遇到虛擬地址時都會提取偏移量和虛擬頁框號,處理器必須將虛擬頁框號轉換為物理頁號,然后以正確的偏移量的位置訪問物理頁,
上圖中展示了兩個行程 A 和 B 的虛擬地址空間,每個行程都有自己的頁表,這些頁表將行程中的虛擬頁映射到記憶體中的物理頁中,頁表中每一項均包含
有效標志(valid flag): 表明此頁表條目是否有效該條目描述的物理頁框號訪問控制資訊,頁面使用方式,是否可寫以及是否可以執行代碼
要將處理器的虛擬地址映射為記憶體的物理地址,首先需要計算虛擬地址的頁框號和偏移量,頁面大小為 2 的次冪,可以通過移位完成操作,
如果當前行程嘗試訪問虛擬地址,但是訪問不到的話,這種情況稱為缺頁例外,此時虛擬作業系統的錯誤地址和頁面錯誤的原因將通知作業系統,
通過以這種方式將虛擬地址映射到物理地址,虛擬記憶體可以以任何順序映射到系統的物理頁面,
按需分頁
由于物理記憶體要比虛擬記憶體少很多,因此作業系統需要注意盡量避免直接使用低效的物理記憶體,節省物理記憶體的一種方式是僅加載執行程式當前使用的頁面(這何嘗不是一種懶加載的思想呢?),例如,可以運行資料庫來查詢資料庫,在這種情況下,不是所有的資料都裝入記憶體,只裝載需要檢查的資料,這種僅僅在需要時才將虛擬頁面加載進內中的技術稱為按需分頁,
交換
如果某個行程需要將虛擬頁面傳入記憶體,但是此時沒有可用的物理頁面,那么作業系統必須丟棄物理記憶體中的另一個頁面來為該頁面騰出空間,
如果頁面已經修改過,那么作業系統必須保留該頁面的內容,以便以后可以訪問它,這種型別的頁面被稱為臟頁,當將其從記憶體中移除時,它會保存在稱為交換檔案的特殊檔案中,相對于處理器和物理記憶體的速度,對交換檔案的訪問非常慢,并且作業系統需要兼顧將頁面寫到磁盤的以及將它們保留在記憶體中以便再次使用,
Linux 使用最近最少使用(LRU)頁面老化技術來公平的選擇可能會從系統中洗掉的頁面,這個方案涉及系統中的每個頁面,頁面的年齡隨著訪問次數的變化而變化,如果某個頁面訪問次數多,那么該頁就表示越年輕,如果某個呃頁面訪問次數太少,那么該頁越容易被換出,
物理和虛擬尋址模式
大多數多功能處理器都支持物理地址模式和虛擬地址模式的概念,物理尋址模式不需要頁表,并且處理器不會在此模式下嘗試執行任何地址轉換, Linux 內核被鏈接在物理地址空間中運行,
訪問控制
頁面表的每一項還包含訪問控制資訊,訪問控制資訊主要檢查行程是否應該訪問記憶體,
必要時需要對記憶體進行訪問限制, 例如包含可執行代碼的記憶體,自然是只讀記憶體; 作業系統不應允許行程通過其可執行代碼寫入資料, 相比之下,包含資料的頁面可以被寫入,但是嘗試執行該記憶體的指令將失敗, 大多數處理器至少具有兩種執行模式:內核態和用戶態, 你不希望訪問用戶執行內核代碼或內核資料結構,除非處理器以內核模式運行,
訪問控制資訊被保存在上面的 Page Table Entry ,頁表項中,上面這幅圖是 Alpha AXP的 PTE,位欄位具有以下含義
V
表示 valid ,是否有效位
FOR
讀取時故障,在嘗試讀取此頁面時出現故障
FOW
寫入時錯誤,在嘗試寫入時發生錯誤
FOE
執行時發生錯誤,在嘗試執行此頁面中的指令時,處理器都會報告頁面錯誤并將控制權傳遞給作業系統,
ASM
地址空間匹配,當作業系統希望清除轉換緩沖區中的某些條目時,將使用此選項,
GH
當在使用單個轉換緩沖區條目而不是多個轉換緩沖區條目映射整個塊時使用的提示,
KRE
內核模式運行下的代碼可以讀取頁面
URE
用戶模式下的代碼可以讀取頁面
KWE
以內核模式運行的代碼可以寫入頁面
UWE
以用戶模式運行的代碼可以寫入頁面
頁框號
對于設定了 V 位的 PTE,此欄位包含此 PTE 的物理頁面幀號(頁面幀號),對于無效的 PTE,如果此欄位不為零,則包含有關頁面在交換檔案中的位置的資訊,
除此之外,Linux 還使用了兩個位
_PAGE_DIRTY
如果已設定,則需要將頁面寫出到交換檔案中
_PAGE_ACCESSED
Linux 用來將頁面標記為已訪問,
快取
上面的虛擬記憶體抽象模型可以用來實施,但是效率不會太高,作業系統和處理器設計人員都嘗試提高性能,
但是除了提高處理器,記憶體等的速度之外,最好的方法就是維護有用資訊和資料的高速快取,從而使某些操作更快,在 Linux中,使用很多和記憶體管理有關的緩沖區,使用緩沖區來提高效率,
緩沖區快取
緩沖區高速快取包含塊設備驅動程式使用的資料緩沖區,
還記得什么是塊設備么?這里回顧下
塊設備是一個能存盤固定大小塊資訊的設備,它支持以固定大小的塊,扇區或群集讀取和(可選)寫入資料,每個塊都有自己的物理地址,通常塊的大小在 512 - 65536 之間,所有傳輸的資訊都會以連續的塊為單位,塊設備的基本特征是每個塊都較為對立,能夠獨立的進行讀寫,常見的塊設備有硬碟、藍光光碟、USB 盤
與字符設備相比,塊設備通常需要較少的引腳,
緩沖區高速快取通過設備識別符號和塊編號用于快速查找資料塊, 如果可以在緩沖區高速快取中找到資料,則無需從物理塊設備中讀取資料,這種訪問方式要快得多,
頁快取
頁快取用于加快對磁盤上影像和資料的訪問
它用于一次一頁地快取檔案中的內容,并且可以通過檔案和檔案中的偏移量進行訪問,當頁面從磁盤讀入記憶體時,它們被快取在頁面快取中,
交換區快取
僅僅已修改(臟頁)被保存在交換檔案中
只要這些頁面在寫入交換檔案后沒有修改,則下次交換該頁面時,無需將其寫入交換檔案,因為該頁面已在交換檔案中, 可以直接丟棄, 在大量交換的系統中,這節省了許多不必要的和昂貴的磁盤操作,
硬體快取
處理器中通常使用一種硬體快取,頁表條目的快取,在這種情況下,處理器并不總是直接讀取頁表,而是根據需要快取頁的翻譯, 這些是轉換后備緩沖區也被稱為TLB,包含來自系統中一個或多個行程的頁表項的快取副本,
參考虛擬地址后,處理器將嘗試查找匹配的
TLB 條目, 如果找到,則可以將虛擬地址直接轉換為物理地址,并對資料執行正確的操作, 如果處理器找不到匹配的 TLB 條目,
它通過向作業系統發信號通知已發生 TLB 丟失獲得作業系統的支持和幫助,系統特定的機制用于將該例外傳遞給可以修復問題的作業系統代碼,
作業系統為地址映射生成一個新的 TLB 條目, 清除例外后,處理器將再次嘗試轉換虛擬地址,這次能夠執行成功,
使用快取也存在缺點,為了節省精力,Linux 必須使用更多的時間和空間來維護這些快取,并且如果快取損壞,系統將會崩潰,
Linux 頁表
Linux 假定頁表分為三個級別,訪問的每個頁表都包含下一級頁表
圖中的 PDG 表示全域頁表,當創建一個新的行程時,都要為新行程創建一個新的頁面目錄,即 PGD,
要將虛擬地址轉換為物理地址,處理器必須獲取每個級別欄位的內容,將其轉換為包含頁表的物理頁的偏移量,并讀取下一級頁表的頁框號,這樣重復三次,直到找到包含虛擬地址的物理頁面的頁框號為止,
Linux 運行的每個平臺都必須提供翻譯宏,這些宏允許內核遍歷特定行程的頁表,這樣,內核無需知道頁表條目的格式或它們的排列方式,
頁面分配
Linux 的頁面分配使用一種著名的伙伴演算法來進行頁面的分配和取消分配,頁面以 2 的冪為單位進行塊分配,這就意味著它可以分配 1頁、2 頁、4頁等等,只要系統中有足夠可用的頁面來滿足需求就可以,判斷的標準是nr_free_pages> min_free_pages,如果滿足,就會在 free_area 中搜索所需大小的頁面塊完成分配,free_area 的每個元素都有該大小的塊的已分配頁面和空閑頁面塊的映射,
分配演算法會搜索請求大小的頁面塊,如果沒有任何請求大小的頁面塊可用的話,會搜尋一個是請求大小二倍的頁面塊,然后重復,直到一直搜尋完
free_area 找到一個頁面塊為止,如果找到的頁面塊要比請求的頁面塊大,就會對找到的頁面塊進行細分,直到找到合適的大小塊為止,
因為每個塊都是 2 的次冪,所以拆分程序很容易,因為你只需將塊分成兩半即可,空閑塊在適當的佇列中排隊,分配的頁面塊回傳給呼叫者,
檔案系統
在 Linux 中,最直觀、最可見的部分就是檔案系統(file system),下面我們就來一起探討一下關于 Linux 中國的檔案系統,系統呼叫以及檔案系統實作背后的原理和思想,這些思想中有一些來源于 MULTICS,現在已經被 Windows 等其他作業系統使用,Linux 的設計理念就是小的就是好的(Small is Beautiful),雖然 Linux 只是使用了最簡單的機制和少量的系統呼叫,但是 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 系統提供了兩種鎖:共享鎖和互斥鎖,如果檔案的一部分已經加上了共享鎖,那么再加排他鎖是不會成功的;如果檔案系統的一部分已經被加了互斥鎖,那么在互斥鎖解除之前的任何加鎖都不會成功,為了成功加鎖、請求加鎖的部分的所有位元組都必須是可用的,
在加鎖階段,行程需要設計好加鎖失敗后的情況,也就是判斷加鎖失敗后是否選擇阻塞,如果選擇阻塞式,那么當已經加鎖的行程中的鎖被洗掉時,這個行程會解除阻塞并替換鎖,如果行程選擇非阻塞式的,那么就不會替換這個鎖,會立刻從系統呼叫中回傳,標記狀態碼表示是否加鎖成功,然后行程會選擇下一個時間再次嘗試,
如果此時一個行程嘗試在第 6 個位元組處加鎖,此時會設定失敗并阻塞,由于該區域被 A B C 同時加鎖,那么只有等到 A B C 都釋放鎖后,行程才能加鎖成功,
Linux 檔案系統呼叫
許多系統呼叫都會和檔案與檔案系統有關,我們首先先看一下對單個檔案的系統呼叫,然后再來看一下對整個目錄和檔案的系統呼叫,
為了創建一個新的檔案,會使用到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系統呼叫看到這些資訊,第一個引數就是檔案名,第二個是指向要放置請求資訊結構的指標,
Linux 中的模塊
UNIX 設備驅動程式是被靜態加載到內核中的,因此,只要系統啟動后,設備驅動程式都會被加載到記憶體中,隨著個人電腦 Linux 的出現,這種靜態鏈接完成后會使用一段時間的模式被打破,相對于小型機上的 I/O 設備,PC 上可用的 I/O 設備有了數量級的增長,絕大多數用戶沒有能力去添加一個新的應用程式、更新設備驅動、重新連接內核,然后進行安裝,
Linux 為了解決這個問題,引入了可加載(loadable module)機制,可加載是在系統運行時添加到內核中的代碼塊,
當一個模塊被加載到內核時,會發生下面幾件事情:第一,在加載的程序中,模塊會被動態的重新部署,第二,系統會檢查程式程式所需的資源是否可用,如果可用,則把這些資源標記為正在使用,第三步,設定所需的中斷向量,第四,更新驅動轉換表使其能夠處理新的主設備型別,最后再來運行設備驅動程式,
在完成上述作業后,驅動程式就會安裝完成,其他現代 UNIX 系統也支持可加載機制,
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/202398.html
標籤:Linux
上一篇:Hadoop集群搭建
下一篇:Linux入門--配置IP
