Unix 文件系統概述
Unix的每個進程都有一個當前工作目錄。
為標識一個特定的文件,進程使用路徑名。如果路徑名第一個字符是斜槓,那麼這個路徑是絕對路徑,其起點是根目錄;如果第一項是目錄名或者文件名,那麼這個路徑就是相對路徑,其起點是進程的當前目錄。
硬鏈接的限制
1)不允許用戶給目錄創建硬鏈接,因為這可能把目錄的樹形結構變成環形結構。
2)只有在同一文件系統中的文件之間才能創建硬鏈接。此限制較大,因為現代Unix系統可能包含多種文件系統,這些文件系統位於不同的磁盤和/或分區,用戶也無法知道他們之間的物理劃分。
為了克服這些限制,引入了軟鏈接(符號鏈接),符號鏈接是短文件,包含有另一個文件的任意一個路徑名,可以指向位於任意一個文件系統的任意文件或者目錄,甚至一個不存在的文件。
文件類型
Unix 文件可以是以下類型之一:
1)普通文件
2)目錄
3)符號鏈接
4)面向塊的設備文件
5)面向字符的設備文件
6)管道(pipe)和命名管道(FIFO)
7)套接字(socket)
文件描述符和索引節點(inode)
Unix對文件的內容(Data)和描述文件的信息(Description)給出了清楚的區分。
除了設備文件和特殊文件系統文件外,每個文件都由字符序列組成,文件內容不包含任何控制信息,如文件的長度或者文件結束符(EOF)。
描述文件的信息(Description)都存儲在一個名為索引節點(inode)的數據結構中,每個文件都有自己的inode,文件系統用inode來標識文件。
inode在不同的Unix系統上實現有很大的差異,但是必須至少提供POSIX標准中指定的如下屬性:
1)文件類型
2)與文件相關聯的硬鏈接個數
3)以字節為單位的文件長度
4)設備標識符
5)在文件系統中標識文件的索引節點號(inode號)
6)文件擁有者的UID
7)文件的用戶組ID
8)三個時間戳:inode狀態改變時間,最後訪問時間,最後修改時間
9)訪問權限和文件模式
訪問權限
三種用戶:
文件所有者
同組用戶,不包括所有者
所有其他用戶
三種權限:讀、寫、執行
因此組合起來有9種訪問權限
文件模式
即三種附加的標記:
suid 進程執行一個文件時通常保持進程擁有者的UID,如果設置了該標識位,進程就獲得了該文件擁有者的UID(例如,sudo,x表示為s)。
sgid 如果設置該標志位,進程就獲得了該文件用戶組的ID。
sticky 設置了sticky標志位的可執行文件在程序執行結束後,依然將它保留在內存,普通文件該位會被忽略,如果是目錄設置了該位,那麼其他用戶是不能刪除該目錄下的文件和目錄的,即使其他用戶擁有該目錄的寫權限(如/tmp目錄,x表示為t)。
Unix內核概述
進程/內核模式
內核本身並不是一個進程,而是一個數據結構,可以理解為進程的管理者。
CPU可以運行在用戶態下,也可以運行在內核態下。
當一個程序運行在用戶態時,不能直接訪問內核數據結構或者內核的程序,通過使用system call來訪問內核。
進程是動態的實體,在系統內只有有限的生存期。創建、撤銷及同步的任務都委托給內核中的一組例程來完成。
除用戶進程之外,Unix系統還包括幾個所謂內核線程(kernel thread)的特權進程(被賦予了特殊權限的的進程),有以下特點:
1)以內核態運行在內核地址空間
2)不與用戶直接交互,因此不需要終端設備
3)通常在系統啟動時創建,然後一直處於活躍狀態直到系統關閉
Unix內核的工作內容:
1)系統調用
2)異常處理
3)中斷處理
4)內核線程執行
進程實現
每個進程由一個進程描述符(process descriptor)表示,這個描述符包含有關進程當前狀態的信息。
當內核暫停一個進程執行時,就把幾個相關CPU寄存器的內容保存在進程描述符中,包括:
1)程序計數器(PC)和棧指針(SP)寄存器
2)通用寄存器
3)浮點寄存器
4)包含CPU狀態信息的處理器控制寄存器(處理器狀態字,processor status word)
5)用來跟蹤進程對RAM訪問的內存管理寄存器
當內核決定恢復執行一個進程時,用進程描述符中合適的字段來裝載CPU寄存器。
當一個進程不在CPU上執行時,它正在等待某一事件,Unix內核可以區分很多等待狀態,這些等待狀態通常由進程描述符隊列實現。每個(可能為空)隊列對應一組等待特定事件的進程。
可重入內核
所有Unix內核都是可重入的,即若干個進程可以同時在內核態下執行。
內核控制路徑(kernel control path)表示內核處理系統調用、異常、中斷所執行的指令序列。
進程地址空間
每個進程運行在它的私有地址空間。
用戶態下運行的進程涉及到私有棧、數據區、代碼區。
內核態運行時,進程訪問內核的數據區,代碼區,但使用另外的私有棧。因為內核是可重入的,因此幾個內核控制路徑(每個都與不同的進程相關)可以輪流執行,這種情況下,每個內核控制路徑都使用它自己的私有內核棧。
盡管每個進程都有自己的私有地址空間,但是進程之間也有共享部分地址空間,一些情況下是顯示的指出(如mmap共享內存),另一些情況下,是由內核自動完成以節約內存(如相同程序的多個不同實例的指令只會在內存中裝載一次)。
同步和臨界區
可重入內核的實現需要利用同步機制,多個內核控制路徑對於內核數據結構存在競爭。
非搶占式內核,大多數傳統Unix內核是非搶占式的,進程在內核態時,不能被任意掛起,不能被另一個進程代替。在單處理器系統上效果可以,但是多處理器系統上,效率低下。(簡單,已不可取)
禁止中斷,單處理器系統上的另一種同步機制,進入一個臨界區之前禁止所有硬件中斷,離開時再重新啟用中斷。如果臨界區較大,那麼在一個相對較長的時間內持續禁止終端就可能使所有硬件活動處於凍結狀態。(簡單,已不可取)
信號量,可以看成是一個對象,由三個部分組成:
1)一個整數變量
2)一個等待進程的鏈表
3)兩個原子方法:down()和up()
每個要保護的數據結構都有它自己的信號量,其初始值為1。如果要使用該數據結構,就對其相應的信號量執行down(),如果當前值不是負數,則允許訪問這個數據結構,否則,將該內核控制路徑的進程加入這個信號量的鏈表並阻塞該進程。當另一個進程在那個信號量上執行up方法時,允許信號鏈表上的一個進程繼續執行,缺點,如果要修改數據結構所需時間較短,其檢查,插入隊列,掛起等過程比較耗時,效率低。
自旋鎖,與信號量是非常相似的,但是沒有等待進程的鏈表,與信號量對比,優勢在於,如果要修改的數據結構所需時間較短,比信號量更高效。當一個進程發現鎖被另外一個進程鎖著時,它就不停的“旋轉”,執行一個緊湊的循環指令直到鎖打開(占有某個處理器)。因此,在單處理器環境下,自旋鎖是無效的。
避免死鎖,死鎖情形會導致受影響的進程或者內核控制路徑完全處於凍結狀態。內核設計中,當所用內核信號量的數量較多時,死鎖就成為一個突出的問題,很難保證內核控制器在各種可能方式下的交錯執行不出現死鎖狀態。有幾種操作系統(包括Linux)通過按照規定的順序請求信號量來避免死鎖。
信號
Unix信號(signal)提供把系統事件報告給進程的一種機制,每種事件都有自己的信號編號。
POSIX標准定義了大約20種不同的信號,其中包括兩種是用戶自定義的。一般來說,進程可以以兩種不同的方式對接收到的信號做出反應:
1)忽略該信號
2)異步的執行一個指定過程(信號處理程序)
如果進程不指定選擇何種方式,內核就根據信號的編號執行一個默認操作,有五種可能的默認操作:
1)終止進程
2)將執行上下文進程地址空間的內容寫入一個文件(核心轉儲,core dump),並終止進程
3)忽略該信號
4)掛起進程
5)如果進程已經被暫停,則恢復它的執行
SIGKILL和SIGSTOP信號不能直接由進程處理,也不能由進程忽略。
進程間通信
Unix System V引入了在用戶態下其他種類的進程間通信機制,被很多Unix內核采用:信號量,消息隊列,共享內存。被統稱為System V IPC。
內核把它們作為IPC資源來實現,與文件一樣,IPC資源是持久不變的,進程創建者,進程擁有者,或者超級用戶必須顯示的釋放這些資源。
IPC裡的信號量與前面所述的信號量是不同的,IPC信號量只用在用戶態進程中。
POSIX標准定義了一中基於消息隊列的IPC機制,就是所謂的POSIX消息隊列,與System V IPC 消息隊列相似,但是提供的接口更簡單。
注意要區分:IPC信號量與信號量,System V IPC與POSIX IPC
進程管理
最基本的系統調用
fork()系統調用用來創建一個新進程(寫時復制Copy-On-Write,vfork的區別)
_exit()系統調用用來終止一個進程
exec()類系統調用用來裝入一個新程序(exec系列的區別)
僵死進程
父進程可以通過wait4()系統調用進行等待,直到其中的一個子進程結束,並且返回其進程標識符。waitpid()類似,只不過針對某單一子進程。
在父進程發出wait4()調用之前,已經運行完成的子進程會處於僵死進程狀態,內核會保存子進程的有關信息(就算子進程已經運行結束)。可以使用init進程來專門管理這些。
進程組和登陸會話
進程組(process group)是表示“作業(Job)”的抽象。
登陸會話(login session),非正式地說,包含在指定終端已經開始工作會話的那個進程的所有後代進程。
內存管理
虛擬內存(virtual memory),作為一種邏輯層,處於應用程序的內存請求與硬件內存管理單元(Memory Management Unit ,MMU)之間,有以下用途和優點:
1)若干個進程可以並發地執行
2)應用程序所需內存大於可用物理內存時也可以運行
3)程序只有部分代碼裝入內存時進程可以執行它
4)允許每個進程訪問可用物理內存的子集
5)進程可以共享庫函數或程序的一個單獨內存映射
6)程序是可重定位的,也就是說,可以把程序放在物理內存的任何地方
7)程序員可以編寫與機器無關的代碼,因為不必擔心有關物理內存的組織結構
當進程使用一個虛擬地址時,內核和MMU協同定位其在內存中的實際物理位置
隨機訪問存儲器(RAM)所有的Unix操作系統都將RAM劃分為兩部分,其中若干MB專門用於存放內核映射(也就是內核代碼和內核靜態數據結構),其余部分通常由虛擬內存系統來處理,可能用在以下三種方面:
1)滿足內核對緩沖區,描述符及其他動態內核數據的請求
2)滿足進程對一般內存區的請求及對文件內存映射的請求
3)借助高速緩存從磁盤及其他緩沖設備獲得較好的性能
由於RAM有限,因此在可用內存達到臨界阈值時,可以調用頁框(page-frame-reclaiming)回收算法釋放其他內存。
內核內存分配器(Kernel Memory Allocator, KMA),是一個子系統,它試圖滿足系統中所有部分對內存的請求。請求可能開自其他子系統,也會來自用戶程序,一個好的KMA應該具備以下特點:
1)必須快,最重要
2)必須把內存浪費減少到最少
3)必須努力減輕內存的碎片(fragmentation)問題
4)必須能與其他內存管理系統合作,以便借用和釋放頁框
已經提出的幾種KMA:
1)資源圖分配算法(allocator)
2)2的冪次方空閒鏈表
3)McKusick-Karels分配算法
4)伙伴(Buddy)系統
5)Mach的區域(Zone)分配算法
6)Dynix分配算法
7)Solaris的Slab分配算法
Linux的KMA在伙伴系統上采用Slab分配算法
進程虛擬地址空間處理,內核通常擁一組內存區描述符描述進程虛擬地址空間。所有現代Unix操作系統都采用請求調頁(demand paging)的內存分配策略。當進程訪問一個不存在的頁時,MMU產生一個異常,異常處理程序找到受影響的內存區,分配一個空閒的頁,並用適當的數據把它初始化。同理,進程調用malloc()或者brk()系統調用動態的請求內存時,內核僅僅修改進程的堆內存區的大小,只有試圖引用進程的虛擬內存地址而發生異常時,才給進程真正分配頁框。內核分配給一個進程的虛擬地址空間由以下內存區組成:
1)程序的可執行代碼(.text)
2)程序的初始化數據(.data)
3)程序的未初始化數據(.bss)
4)初始程序棧(.stack)
5)所需共享庫的可執行代碼和數據
6)堆(.heap)
高速緩存,與RAM的訪問時間相比,磁盤和其他塊設備的訪問太慢了。
設備驅動程序,內核通過設備驅動程序(device driver)與I/O設備交互。設備驅動程序包含在內核中,由控制一個或多個設備的數據結構和函數組成,這些設備包括硬盤、鍵盤、鼠標、監視器、網絡接口及連接到SCSI總線上的設備。通過特定的接口,每個驅動程序與內核中的其余部分相互作用,這種方式有以下優點:
1)可以把特定設備的代碼封裝在特定的模塊中
2)廠商可以在不了解內核源代碼而只知道接口規范的情況下,就能增加新的設備
3)內核以統一的方式對待所有的設備,並通過相同的接口訪問這些設備
4)可以把設備驅動程序寫成模塊,並動態地把它們裝進內核而不需要重新啟動系統,不再需要時,也可以動態的卸載下模塊,以減少存儲在RAM中的內核映像的大小。