在操作系統中,文件系統的主要目的是「實現對文件的按名存取」。文件系統是操作系統用於明確存儲設備(常見的是磁碟,也有基於NAND Flash的固態硬碟)或分區上的文件的方法和數據結構;即在存儲設備上組織文件的方法。
操作系統中負責管理和存儲文件信息的軟體機構稱為文件管理系統,簡稱文件系統。文件系統指定命名文件的規則。這些規則包括文件名的字元數最大量,哪種字元可以使用,以及某些系統中文件名後綴可以有多長。文件系統還包括通過目錄結構找到文件的指定路徑的格式。
文件系統由三部分組成:文件系統的介面,對對象操縱和管理的軟體集合,對象及屬性,文件系統是軟體系統的一部分,它的存在使得應用可以方便的使用抽象命名的數據對象和大小可變的空間。
從系統角度來看,文件系統是對文件存儲設備的空間進行組織和分配,負責文件存儲並對存入的文件進行保護和檢索的系統。具體地說,它負責為用戶建立文件、修改文件、存取文件、轉存文件、將用戶不需要的文件從磁碟上刪除等。
從用戶角度看,文件系統實現了「按名存取」,只要知道文件名就可以存取文件,而不必考慮文件存儲在磁碟上什麼地方。
⑵ 如何實現一個文件系統
摘要:本文目的是分析在linux系統中如何實現新的文件系統。在介紹文件系統具體實現前先介紹文件系統的概念和作用,抽象出了文件系統概念模型。熟悉文件系統的內涵後,我們再近一步討論Linux系統中和文件系統的特殊風格和具體文件系統在Linux中組成結構,為讀者勾畫出Linux中文件系統工作的全景圖。最後,我們再通過Linux中最簡單的Romfs作實例分析實現文件系統的普遍步驟。(我們假定讀者已經對Linux文件系統初步了解)
什麼是文件系統
首先要談的概念就是什麼是文件系統,它的作用到底是什麼。
文件系統的概念雖然許多人都認為是再清晰不過的了,但其實我們往往在談論中或多或少地誇大或片縮小了它的實際概念(至少我時常混淆),或者說,有時借用了其它概念,有時說的又不夠全面。
比如在操作系統中,文件系統這個術語往往既被用來描述磁碟中的物理布局,比如有時我們說磁碟中的「文件系統」是EXT2或說把磁碟格式化成FAT32格式的「文件系統」等——這時所說的「文件系統」是指磁碟數據的物理布局格式;另外,文件系統也被用來描述內核中的邏輯文件結構,比如有時說的「文件系統」的介面或內核支持Ext2等「文件系統」——這時所說的文件系統都是內存中的數據組織結構而並非磁碟物理布局。還有些時候說「文件系統」負責管理用戶讀寫文件——這時所說的「文件系統」往往描述操作系統中的「文件管理系統」,也就是文件子系統。
雖然上面我們列舉了混用文件系統的概念的幾種情形,但是卻也不能說上述說法就是錯誤的,因為文件系統概念本身就囊括眾多概念,幾乎可以說在操作系統中自內存管理、系統調度到I/O系統、設備驅動等各個部分都和文件系統聯系密切,有些部分和文件系統甚至未必能明確劃分——所以不能只知道文件系統是系統中數據的存儲結構,一定要全面認識文件系統在操作系統中的角色,才能具備自己開發新文件系統的能力。
為了澄清文件系統的概念,必須先來看看文件系統在操作系統中處於何種角色,分析文件系統概念的內含外延。所以我們先拋開Linux文件系統的實例,而來看看操作系統中文件系統的普遍體系結構,從而增強對文件系統的理論認識。
下面以軟體組成的結構圖[1]的方式描述文件系統所涉及的內容。
我們針對各層做以簡要分析:
首先我們來分析最低層——設備驅動層,該層負責與外設——磁碟等——通訊。基於磁碟的文件系統都需要和存儲設備打交道,而系統操作外設離不開驅動程序。所以內核對文件的最後操作行為就是調用設備驅動程序完成從主存(內存)到輔存(磁碟)的數據傳輸。文件系統相關的多數設備都屬於塊設備,常見的塊設備驅動程序有磁碟驅動,光碟機驅動等,之所以稱它們為塊設備,一個原因是它們讀寫數據都是成塊進行的,但是更重要的原因是它們管理的數據能夠被隨機訪問——不需要向字元設備那樣必須順序訪問。
設備驅動層的上一層是物理I/O層,該層主要作為計算機外部環境和系統的介面,負責系統和磁碟交換數據塊。它要知道據塊在磁碟中存儲位置,也要知道文件數據塊在內存緩沖中的位置,另外它不需要了解數據或文件的具體結構。可以看到這層最主要的工作是標識別磁碟扇區和內存緩沖塊[2]之間的映射關系。
再上層是基礎I/O監督層,該層主要負責選擇文件 I/O需要的設備,調度磁碟請求等工作,另外分配I/O緩沖和磁碟空間也在該層完成。由於塊設備需要隨機訪問數據,而且對速度響應要求較高,所以操作系統不能向對字元設備那樣簡單、直接地發送讀寫請求,而必須對讀寫請求重新優化排序,以能節省磁碟定址時間,另外也必須對請求提交採取非同步調度(尤其寫操作)的方式進行。總而言之,內核對必須管理塊設備請求,而這項工作正是由該層負責的。
倒數第二層是邏輯I/O層,該層允許用戶和應用程序訪問記錄。它提供了通用的記錄(record)I/O操作,同時還維護基本文件數據。由於為了方便用戶操作和管理文件內容,文件內容往往被組織成記錄形式,所以操作系統為操作文件記錄提供了一個通用邏輯操作層。
和用戶最靠近的是訪問方法層,該層提供了一個從用戶空間到文件系統的標准介面,不同的訪問方法反映了不同的文件結構,也反映了不同的訪問數據和處理數據方法。這一層我們可以簡單地理解為文件系統給用戶提供的訪問介面——不同的文件格式(如順序存儲格式、索引存儲格式、索引順序存儲格式和哈希存儲格式等)對應不同的文件訪問方法。該層要負責將用戶對文件結構的操作轉化為對記錄的操作。
對比上面的層次圖我們再來分析一下數據流的處理過程,加深對文件系統的理解。
假如用戶或應用程序操作文件(創建/刪除),首先需要通過文件系統給用戶空間提供的訪問方法層進入文件系統,接著由使用邏輯I/O層對記錄進行給定操作,然後記錄將被轉化為文件塊,等待和磁碟交互。這里有兩點需要考慮——第一,磁碟管理(包括再磁碟空閑區分配文件和組織空閑區);第二,調度塊I/O請求——這些由基礎I/O監督層的工作。再下來文件塊被物理I/O層傳遞給磁碟驅動程序,最後磁碟驅動程序真正把數據寫入具體的扇區。至此文件操作完畢。
當然上面介紹的層次結構是理想情況下的理論抽象,實際文件系統並非一定要按照上面的層次或結構組織,它們往往簡化或合並了某些層的功能(比如Linux文件系統因為所有文件都被看作位元組流,所以不存在記錄,也就沒有必要實現邏輯I/O層,進而也不需要在記錄相關的處理)。但是大體上都需要經過類似處理。如果從處理對象上和系統獨立性上劃分,文件系統體系結構可以被分為兩大部分:——文件管理部分和操作系統I/O部分。文件管理系統負責操作內存中文件對象,並按文件的邏輯格式將對文件對象的操作轉化成對文件塊的操作;而操作系統I/O部分負責內存中的塊與物理磁碟中的數據交換。
數據表現形式再文件操作過程中也經歷了幾種變化:在用戶訪問文件系統看到的是位元組序列,而在位元組序列被寫入磁碟時看到的是內存中文件塊(在緩沖中),在最後將數據寫入磁碟扇區時看到的是磁碟數據塊[3]。
本文所說的實現文件系統主要針對最開始講到第二種情況——內核中的邏輯文件結構(但其它相關的文件管理系統和文件系統磁碟存儲格式也必須了解),我們用數據處理流圖來分析一下邏輯文件系統主要功能和在操作系統中所處的地位。
其中文件系統介面與物理布局管理是邏輯文件系統要負責的主要功能。
文件系統介面為用戶提供對文件系統的操作,比如open、close、read、write和訪問控制等,同時也負責處理文件的邏輯結構。
物理存儲布局管理,如同虛擬內存地址轉化為物理內存地址時,必須處理段頁結構一樣,邏輯文件結構必須轉化到物理磁碟中,所以也要處理物理分區和扇區的實際存儲位置,分配磁碟空間和內存中的緩沖也要在這里被處理。
所以說要實現文件系統就必須提供上面提到的兩種功能,缺一不可。
在了解了文件系統的功能後,我們針對Linux操作系統分析具體文件系統如何工作,進而掌握實現一個文件系統需要的步驟。
Linux 文件系統組成結構
Linux 文件系統的結構除了我們上面所提到的概念結構外,最主要有兩個特點,一個是文件系統抽象出了一個通用文件表示層——虛擬文件系統或稱做VFS。另外一個重要特點是它的文件系統支持動態安裝(或說掛載、登陸等),大多數文件系統都可以作為根文件系統的葉子接點被掛在到根文件目錄樹下的子目錄上。另外Linux系統在文件讀寫的I/O操作上也採取了一些先進技術和策略。
我們先從虛擬文件系統入手分析linux文件系統的特性,然後介紹有關文件系統的安裝、注冊和讀寫等概念。
虛擬文件系統
虛擬文件系統為用戶空間程序提供了文件系統介面。系統中所有文件系統不但依賴VFS共存,而且也依靠VFS系統協同工作。通過虛擬文件系統我們可以利用標準的UNIX文件系統調用對不同介質上的不同文件系統進行讀寫操作[4]。
虛擬文件系統的目的是為了屏蔽各種各樣不同文件系統的相異操作形式,使得異構的文件系統可以在統一的形式下,以標准化的方法訪問、操作。實現虛擬文件系統利用的主要思想是引入一個通用文件模型——該模型抽象出了文件系統的所有基本操作(該通用模型源於Unix風格的文件系統),比如讀、寫操作等。同時實際文件系統如果希望利用虛擬文件系統,既被虛擬文件系統支持,也必須將自身的諸如,「打開文件」、「讀寫文件」等操作行為以及「什麼是文件」,「什麼是目錄」等概念「修飾」成虛擬文件系統所要求的(定義的)形式,這樣才能夠被虛擬文件系統支持和使用。
我們可以借用面向對象的一些思想來理解虛擬文件系統,虛擬文件系統好比一個抽象類或介面,它定義(但不實現)了文件系統最常見的操作行為。而具體文件系統好比是具體類,它們是特定文件系統的實例。具體文件系統和虛擬文件系統的關系類似具體類繼承抽象類或實現介面。而在用戶看到或操作的都是抽象類或介面,但實際行為卻發生在具體文件系統實例上。至於如何將對虛擬文件系統的操作轉化到對具體文件系統的實例,就要通過注冊具體文件系統到系統,然後再安裝具體文件系統才能實現轉化,這點可以想像成面向對象中的多態概念。
我們個實舉例來說明具體文件系統如何通過虛擬文件系統協同工作。
例如:假設一個用戶輸入以下shell命令:
$ cp /hda/test1 /removable/test2
其中 /removable是MS-DOS磁碟的一個安裝點,而 /hda 是一個標準的第二擴展文件系統( Ext2)的目錄。cp命令不用了解test1或test2的具體文件系統,它所看到和操作的對象是VFS。cp首先要從ext3文件系統讀出test1文件,然後寫入MS-DOS文件系統中的test2。VFS會將找到ext3文件系統實例的讀方法,對test1文件進行讀取操作;然後找到MS-DOS(在Linux中稱VFAT)文件系統實例的寫方法,對test2文件進行寫入操作。可以看到 VFS是讀寫操作的統一界面,只要具體文件系統符合VFS所要求的介面,那麼就可以毫無障礙地透明通訊了。
Unix風格的文件系統
虛擬文件系統的通用模型源於Unix風格的文件系統,所謂Unix風格是指Unix傳統上文件系統傳統上使用了四種和文件系統相關的抽象概念:文件(file)、目錄項(dentry)、索引節點(inode)和安裝點(mount point)。
文件——在Unix中的文件都被看做是一有序位元組串,它們都有一個方便用戶或系統識別的名稱。另外典型的文件操作有讀、寫、創建和刪除等。
目錄項——不要和目錄概念搞混淆,在Linux中目錄被看作文件。而目錄項是文件路徑中的一部分。一個文件路徑的例子是「/home/wolfman/foo」——根目錄是/,目錄home,wolfman和文件foo都是目錄項。
索引節點——Unix系統將文件的相關信息(如訪問控制許可權、大小、擁有者、創建時間等等信息),有時被稱作文件的元數據(也就是說,數據的數據)被存儲在一個單獨的數據結構中,該結構被稱為索引節點(inode)。
安裝點——在Unix中,文件系統被安裝在一個特定的安裝點上,所有的已安裝文件系統都作為根文件系統樹中的葉子出現在系統中。
上述概念是Unix文件系統的邏輯數據結構,但相應的Unix文件系統(Ext2等)磁碟布局也實現了部分上述概念,比如文件信息(文件數據元)存儲在磁碟塊中的索引節點上。當文件被載如內存時,內核需要使用磁碟塊中的索引點來裝配內存中的索引接點。類似行為還有超級塊信息等。
對於非Unix風格文件系統,如FAT或NTFS,要想能被VFS支持,它們的文件系統代碼必須提供這些概念的虛擬形式。比如,即使一個文件系統不支持索引節點,它也必須在內存中裝配起索引節點結構體——如同本身固有一樣。或者,如果一個文件系統將目錄看作是一種特殊對象,那麼要想使用VFS,必須將目錄重新表示為文件形式。通常,這種轉換需要在使用現場引入一些特殊處理,使得非Unix文件系統能夠兼容Unix文件系統的使用規則和滿足VFS的需求。通過這些處理,非Unix文件系統便可以和VFS一同工作了,是性能上多少會受一些影響[5]。這點很重要,我們實現自己文件系統時必須提供(模擬)Unix風格文件系統的抽象概念。
Linux文件系統中使用的對象
Linux文件系統的對象就是指一些數據結構體,之所以稱它們是對象,是因為這些數據結構體不但包含了相關屬性而且還包含了操作自身結構的函數指針,這種將數據和方法進行封裝的思想和面向對象中對象概念一致,所以這里我們就稱它們是對象。
Linux文件系統使用大量對象,我們簡要分析以下VFS相關的對象,和除此還有和進程相關的一些其它對象。
VFS相關對象
這里我們不展開討論每個對象,僅僅是為了內容完整性,做作簡要說明。
VFS中包含有四個主要的對象類型,它們分別是:
超級塊對象,它代表特定的已安裝文件系統。
索引節點對象,它代表特定文件。
目錄項對象,它代表特定的目錄項。
文件對象,它代表和進程打開的文件。
每個主要對象中都包含一個操作對象,這些操作對象描述了內核針對主要對象可以使用的方法。最主要的幾種操作對象如下:
super_operations對象,其中包括內核針對特定文件系統所能調用的方法,比如read_inode()和sync_fs()方法等。
inode_operations對象,其中包括內核針對特定文件所能調用的方法,比如create()和link()方法等。
dentry_operations對象,其中包括內核針對特定目錄所能調用的方法,比如d_compare()和d_delete()方法等。
file對象,其中包括,進程針對已打開文件所能調用的方法,比如read()和write()方法等。
除了上述的四個主要對象外,VFS還包含了許多對象,比如每個注冊文件系統都是由file_system_type對象表示——描述了文件系統及其能力(如比如ext3或XFS);另外每一個安裝點也都利用vfsmount對象表示——包含了關於安裝點的信息,如位置和安裝標志等。
其它VFS對象
系統上的每一進程都有自己的打開文件,根文件系統,當前工作目錄,安裝點等等。另外還有幾個數據結構體將VFS層和文件的進程緊密聯系,它們分別是:file_struct 和fs_struct
file_struct結構體由進程描述符中的files項指向。所有包含進程的信息和它的文件描述符都包含在其中。第二個和進程相關的結構體是fs_struct。該結構由進程描述符的fs項指向。它包含文件系統和進程相關的信息。每種結構體的詳細信息不在這里說明了。
緩存對象
除了上述一些結構外,為了縮短文件操作響應時間,提高系統性能,Linux系統採用了許多緩存對象,例如目錄緩存、頁面緩存和緩沖緩存(已經歸入了頁面緩存),這里我們對緩存做簡單介紹。
頁高速緩存(cache)是 Linux內核實現的一種主要磁碟緩存。其目的是減少磁碟的I/O操作,具體的講是通過把磁碟中的數據緩存到物理內存中去,把對磁碟的I/O操作變為對物理內存的I/O操作。頁高速緩存是由RAM中的物理頁組成的,緩存中每一頁都對應著磁碟中的多個塊。每當內核開始執行一個頁I/O操作時(通常是對普通文件中頁大小的塊進行磁碟操作),首先會檢查需要的數據是否在高速緩存中,如果在,那麼內核就直接使用高速緩存中的數據,從而避免了訪問磁碟。
但我們知道文件系統只能以每次訪問數個塊的形式進行操作。內核執行所有磁碟操作都必須根據塊進行,一個塊包含一個或多個磁碟扇區。為此,內核提供了一個專門結構來管理緩沖buffer_head。緩沖頭[6]的目的是描述磁碟扇區和物理緩沖之間的映射關系和做I/O操作的容器。但是緩沖結構並非獨立存在,而是被包含在頁高速緩存中,而且一個頁高速緩存可以包含多個緩沖。我們將在文件後面的文件讀寫部分看到數據如何被從磁碟扇區讀入頁高速緩存中的緩沖中的。
文件系統的注冊和安裝
使用文件系統前必須對文件系統進行注冊和安裝,下面分別對這兩種行為做簡要介紹。
文件系統的注冊
VFS要想能將自己定義的介面映射到實際文件系統的專用方法上,必須能夠讓內核識別實際的文件系統,實際文件系統通過將代表自身屬性的文件類型對象(file_system_type)注冊(通過register_filesystem()函數)到內核,也就是掛到內核中的文件系統類型鏈表上,來達到使文件系統能被內核識別的目的。反過來內核也正是通過這條鏈表來跟蹤系統所支持的各種文件系統的。
我們簡要分析一下注冊步驟:
struct file_system_type {
const char *name; /*文件系統的名字*/
int fs_flags; /*文件系統類型標志*/
/*下面的函數用來從磁碟中讀取超級塊*/
struct super_block * (*read_super) (struct file_system_type *, int,
const char *, void *);
struct file_system_type * next; /*鏈表中下一個文件系統類型*/
struct list_head fs_supers; /*超級塊對象鏈表*/
};
其中最重要的一項是read_super()函數,它用來從磁碟上讀取超級塊,並且當文件系統被裝載時,在內存中組裝超級塊對象。要實現一個文件系統首先需要實現的結構體便是file_system_type結構體。
注冊文件系統只能保證文件系統能被系統識別,但此刻文件系統尚不能使用,因為它還沒有被安裝到特定的安裝點上。所以在使用文件系統前必須將文件系統安裝到安裝點上。
文件系統被實際安裝時,將在安裝點創建一個vfsmount結構體。該結構體用代表文件系統的實例——換句話說,代表一個安裝點。
vfsmount結構被定義在<linux/mount.h>中,下面是具體結構
―――――――――――――――――――――――――――――――――――――――
struct vfsmount
{
struct list_head mnt_hash; /*哈希表*/
struct vfsmount *mnt_parent; /*父文件系統*/
struct dentry *mnt_mountpoint; /*安裝點的目錄項對象*/
struct dentry *mnt_root; /*該文件系統的根目錄項對象*/
struct super_block *mnt_sb; /*該文件系統的超級塊*/
struct list_head mnt_mounts; /*子文件系統鏈表*/
struct list_head mnt_child; /*和父文件系統相關的子文件系統*/
atomic_t mnt_count; /*使用計數*/
int mnt_flags; /*安裝標志*/
char *mnt_devname; /*設備文件名字*/
struct list_head mnt_list; /*描述符鏈表*/
};
――――――――――――――――――――――――――――――――――――――
文件系統如果僅僅注冊,那麼還不能被用戶使用。要想使用它還必須將文件系統安裝到特定的安裝點後才能工作。下面我們接著介紹文件系統的安裝[7]過程。
安裝過程
用戶在用戶空間調用mount()命令——指定安裝點、安裝的設備、安裝類型等——安裝指定文件系統到指定目錄。mount()系統調用在內核中的實現函數為sys_mount(),該函數調用的主要常式是do_mount(),它會取得安裝點的目錄項對象,然後調用do_add_mount()常式。
do_add_mount()函數主要做的是首先使用do_kern_mount()函數創建一個安裝點,再使用graft_tree()將安裝點作為葉子與根目錄樹掛接起來。
整個安裝過程中最核心的函數就是do_kern_mount()了,為了創建一個新安裝點(vfsmount),該函數需要做一下幾件事情:
l 1 檢查安裝設備的權利,只有root許可權才有能力執行該操作。
l 2 Get_fs_type()在文件鏈表中取得相應文件系統類型(注冊時被填加到練表中)。
l 3 Alloc_vfsmnt()調用slab分配器為vfsmount結構體分配存儲空間,並把它的地址存放在mnt局部變數中。
l 4 初始化mnt->mnt_devname域
l 5 分配新的超級塊並初始化它。do_kern_mount( )檢查file_system_type描述符中的標志以決定如何進行如下操作:根據文件系統的標志位,選擇相應的方法讀取超級塊(比如對Ext2,romfs這類文件系統調用get_sb_dev();對於這種沒有實際設備的虛擬文件系統如 ramfs調用get_sb_nodev())——讀取超級塊最終要使用文件系統類型中的read_super方法。
安裝過程做的最主要工作是創建安裝點對象,掛接給定文件系統到根文件系統的指定接點下,然後初始化超級快對象,從而獲得文件系統基本信息和相關操作方法(比如讀取系統中某個inode的方法)。
總而言之,注冊過程是告之內核給定文件系統存在於系統內;而安裝是請求內核對給定文件系統進行支持,使文件系統真正可用。
轉載
⑶ Linux內核中有文件系統模塊, 那它和根文件系統什麼關系哦
linux內核是來linux的真實的操作系統源,所有的操作系統的相關功能都是由用戶介面程序傳遞到內核由內核來完成的。
linux文件系統是指linux操作系統對整個系統中的所有的數據、文件的管理的一種實現方式。
簡單點說:像windows它的文件系統是NTFS文件系統或者FAT文件系統,通過這種方式將windows的各種文件保存在磁碟上,用於存儲和訪問了。
而linux系統則使用ext這種文件系統來實現。
⑷ STM32 MCU啟動流程
你應該去學一下匯編,c文件或者h文件以及asm文件或者s文件都不會放到單片機裡面,setup.s文件裡面是內匯編代碼,他定容義了一些介面和異常處理方法,根據boot01的選擇,從內部flash啟動的所有過程在setup.s文件中可查,文件定義了最開始調用的函數和main函數的位置,以及中斷入口和中斷異常處理辦法。就這么多,別的都在.c文件中
⑸ 如何用C語言實現fat32文件系統
#include <stdio.h>
#include <stdlib.h> //為了使用exit()
int main()
{
char ch;
FILE* fp;
char fname[50]; //用於存放文件名
printf("輸入文件名:");
scanf("%s",fname);
fp=fopen(fname,"r"); //只供讀取
if(fp==NULL) //如果失敗了
{
printf("錯誤!");
exit(1); //中止程序
}
//getc()用於在打開文件中獲取一個字元
while((ch=getc(fp))!=EOF)
putchar(ch);
fclose(fp); //關閉文件
return 0;
}
注意!初學者往往會犯一個錯誤,即在輸入文件名時不加後綴名,請注意加上!
程序示例2[2]
#include <stdio.h>
FILE *stream, *stream2;
int main( void )
{
int numclosed;
// Open for read (will fail if file "crt_fopen.c" does not exist)
if( (stream = fopen( "crt_fopen.c", "r" )) == NULL ) // C4996
// Note: fopen is deprecated; consider using fopen_s instead
printf( "The file 'crt_fopen.c' was not opened\n" );
else
printf( "The file 'crt_fopen.c' was opened\n" );
// Open for write
if( (stream2 = fopen( "data2", "w+" )) == NULL ) // C4996
printf( "The file 'data2' was not opened\n" );
else
printf( "The file 'data2' was opened\n" );
// Close stream if it is not NULL
if( stream)
{
if ( fclose( stream ) )
{
printf( "The file 'crt_fopen.c' was not closed\n" );
}
}
// All other files are closed:
numclosed = _fcloseall( );
printf( "Number of files closed by _fcloseall: %u\n", numclosed );
}
⑹ 面試 linux 文件系統怎樣io到底層
前言:本文主要講解LinuxIO調度層的三種模式:cfp、deadline和noop,並給出各自的優化和適用場景建議。IO調度發生在Linux內核的IO調度層。這個層次是針對Linux的整體IO層次體系來說的。從read()或者write()系統調用的角度來說,Linux整體IO體系可以分為七層,它們分別是:VFS層:虛擬文件系統層。由於內核要跟多種文件系統打交道,而每一種文件系統所實現的數據結構和相關方法都可能不盡相同,所以,內核抽象了這一層,專門用來適配各種文件系統,並對外提供統一操作介面。文件系統層:不同的文件系統實現自己的操作過程,提供自己特有的特徵,具體不多說了,大家願意的話自己去看代碼即可。頁緩存層:負責真對page的緩存。通用塊層:由於絕大多數情況的io操作是跟塊設備打交道,所以Linux在此提供了一個類似vfs層的塊設備操作抽象層。下層對接各種不同屬性的塊設備,對上提供統一的BlockIO請求標准。IO調度層:因為絕大多數的塊設備都是類似磁碟這樣的設備,所以有必要根據這類設備的特點以及應用的不同特點來設置一些不同的調度演算法和隊列。以便在不同的應用環境下有針對性的提高磁碟的讀寫效率,這里就是大名鼎鼎的Linux電梯所起作用的地方。針對機械硬碟的各種調度方法就是在這實現的。塊設備驅動層:驅動層對外提供相對比較高級的設備操作介面,往往是C語言的,而下層對接設備本身的操作方法和規范。塊設備層:這層就是具體的物理設備了,定義了各種真對設備操作方法和規范。有一個已經整理好的[LinuxIO結構圖],非常經典,一圖勝千言:我們今天要研究的內容主要在IO調度這一層。它要解決的核心問題是,如何提高塊設備IO的整體性能?這一層也主要是針對機械硬碟結構而設計的。眾所周知,機械硬碟的存儲介質是磁碟,磁頭在碟片上移動進行磁軌定址,行為類似播放一張唱片。這種結構的特點是,順序訪問時吞吐量較高,但是如果一旦對碟片有隨機訪問,那麼大量的時間都會浪費在磁頭的移動上,這時候就會導致每次IO的響應時間變長,極大的降低IO的響應速度。磁頭在碟片上尋道的操作,類似電梯調度,實際上在最開始的時期,Linux把這個演算法命名為Linux電梯演算法,即:如果在尋道的過程中,能把順序路過的相關磁軌的數據請求都「順便」處理掉,那麼就可以在比較小影響響應速度的前提下,提高整體IO的吞吐量。這就是我們為什麼要設計IO調度演算法的原因。目前在內核中默認開啟了三種演算法/模式:noop,cfq和deadline。嚴格算應該是兩種:因為第一種叫做noop,就是空操作調度演算法,也就是沒有任何調度操作,並不對io請求進行排序,僅僅做適當的io合並的一個fifo隊列。目前內核中默認的調度演算法應該是cfq,叫做完全公平隊列調度。這個調度演算法人如其名,它試圖給所有進程提供一個完全公平的IO操作環境。註:請大家一定記住這個詞語,cfq,完全公平隊列調度,不然下文就沒法看了。cfq為每個進程創建一個同步IO調度隊列,並默認以時間片和請求數限定的方式分配IO資源,以此保證每個進程的IO資源佔用是公平的,cfq還實現了針對進程級別的優先順序調度,這個我們後面會詳細解釋。查看和修改IO調度演算法的方法是:cfq是通用伺服器比較好的IO調度演算法選擇,對桌面用戶也是比較好的選擇。但是對於很多IO壓力較大的場景就並不是很適應,尤其是IO壓力集中在某些進程上的場景。因為這種場景我們需要的滿足某個或者某幾個進程的IO響應速度,而不是讓所有的進程公平的使用IO,比如資料庫應用。deadline調度(最終期限調度)就是更適合上述場景的解決方案。deadline實現了四個隊列:其中兩個分別處理正常read和write,按扇區號排序,進行正常io的合並處理以提高吞吐量。因為IO請求可能會集中在某些磁碟位置,這樣會導致新來的請求一直被合並,可能會有其他磁碟位置的io請求被餓死。另外兩個處理超時read和write的隊列,按請求創建時間排序,如果有超時的請求出現,就放進這兩個隊列,調度演算法保證超時(達到最終期限時間)的隊列中的請求會優先被處理,防止請求被餓死。不久前,內核還是默認標配四種演算法,還有一種叫做as的演算法(Anticipatoryscheler),預測調度演算法。一個高大上的名字,搞得我一度認為Linux內核都會算命了。結果發現,無非是在基於deadline演算法做io調度的之前等一小會時間,如果這段時間內有可以合並的io請求到來,就可以合並處理,提高deadline調度的在順序讀寫情況下的數據吞吐量。其實這根本不是啥預測,我覺得不如叫撞大運調度演算法,當然這種策略在某些特定場景差效果不錯。但是在大多數場景下,這個調度不僅沒有提高吞吐量,還降低了響應速度,所以內核乾脆把它從默認配置里刪除了。畢竟Linux的宗旨是實用,而我們也就不再這個調度演算法上多費口舌了。1、cfq:完全公平隊列調度cfq是內核默認選擇的IO調度隊列,它在桌面應用場景以及大多數常見應用場景下都是很好的選擇。如何實現一個所謂的完全公平隊列(CompletelyFairQueueing)?首先我們要理解所謂的公平是對誰的公平?從操作系統的角度來說,產生操作行為的主體都是進程,所以這里的公平是針對每個進程而言的,我們要試圖讓進程可以公平的佔用IO資源。那麼如何讓進程公平的佔用IO資源?我們需要先理解什麼是IO資源。當我們衡量一個IO資源的時候,一般喜歡用的是兩個單位,一個是數據讀寫的帶寬,另一個是數據讀寫的IOPS。帶寬就是以時間為單位的讀寫數據量,比如,100Mbyte/s。而IOPS是以時間為單位的讀寫次數。在不同的讀寫情境下,這兩個單位的表現可能不一樣,但是可以確定的是,兩個單位的任何一個達到了性能上限,都會成為IO的瓶頸。從機械硬碟的結構考慮,如果讀寫是順序讀寫,那麼IO的表現是可以通過比較少的IOPS達到較大的帶寬,因為可以合並很多IO,也可以通過預讀等方式加速數據讀取效率。當IO的表現是偏向於隨機讀寫的時候,那麼IOPS就會變得更大,IO的請求的合並可能性下降,當每次io請求數據越少的時候,帶寬表現就會越低。從這里我們可以理解,針對進程的IO資源的主要表現形式有兩個:進程在單位時間內提交的IO請求個數和進程佔用IO的帶寬。其實無論哪個,都是跟進程分配的IO處理時間長度緊密相關的。有時業務可以在較少IOPS的情況下佔用較大帶寬,另外一些則可能在較大IOPS的情況下佔用較少帶寬,所以對進程佔用IO的時間進行調度才是相對最公平的。即,我不管你是IOPS高還是帶寬佔用高,到了時間咱就換下一個進程處理,你愛咋樣咋樣。所以,cfq就是試圖給所有進程分配等同的塊設備使用的時間片,進程在時間片內,可以將產生的IO請求提交給塊設備進行處理,時間片結束,進程的請求將排進它自己的隊列,等待下次調度的時候進行處理。這就是cfq的基本原理。當然,現實生活中不可能有真正的「公平」,常見的應用場景下,我們很肯能需要人為的對進程的IO佔用進行人為指定優先順序,這就像對進程的CPU佔用設置優先順序的概念一樣。所以,除了針對時間片進行公平隊列調度外,cfq還提供了優先順序支持。每個進程都可以設置一個IO優先順序,cfq會根據這個優先順序的設置情況作為調度時的重要參考因素。優先順序首先分成三大類:RT、BE、IDLE,它們分別是實時(RealTime)、最佳效果(BestTry)和閑置(Idle)三個類別,對每個類別的IO,cfq都使用不同的策略進行處理。另外,RT和BE類別中,分別又再劃分了8個子優先順序實現更細節的QOS需求,而IDLE只有一個子優先順序。另外,我們都知道內核默認對存儲的讀寫都是經過緩存(buffer/cache)的,在這種情況下,cfq是無法區分當前處理的請求是來自哪一個進程的。只有在進程使用同步方式(syncread或者syncwirte)或者直接IO(DirectIO)方式進行讀寫的時候,cfq才能區分出IO請求來自哪個進程。所以,除了針對每個進程實現的IO隊列以外,還實現了一個公共的隊列用來處理非同步請求。當前內核已經實現了針對IO資源的cgroup資源隔離,所以在以上體系的基礎上,cfq也實現了針對cgroup的調度支持。總的來說,cfq用了一系列的數據結構實現了以上所有復雜功能的支持,大家可以通過源代碼看到其相關實現,文件在源代碼目錄下的block/cfq-iosched.c。1.1cfq設計原理在此,我們對整體數據結構做一個簡要描述:首先,cfq通過一個叫做cfq_data的數據結構維護了整個調度器流程。在一個支持了cgroup功能的cfq中,全部進程被分成了若干個contralgroup進行管理。每個cgroup在cfq中都有一個cfq_group的結構進行描述,所有的cgroup都被作為一個調度對象放進一個紅黑樹中,並以vdisktime為key進行排序。vdisktime這個時間紀錄的是當前cgroup所佔用的io時間,每次對cgroup進行調度時,總是通過紅黑樹選擇當前vdisktime時間最少的cgroup進行處理,以保證所有cgroups之間的IO資源佔用「公平」。當然我們知道,cgroup是可以對blkio進行資源比例分配的,其作用原理就是,分配比例大的cgroup佔用vdisktime時間增長較慢,分配比例小的vdisktime時間增長較快,快慢與分配比例成正比。這樣就做到了不同的cgroup分配的IO比例不一樣,並且在cfq的角度看來依然是「公平「的。選擇好了需要處理的cgroup(cfq_group)之後,調度器需要決策選擇下一步的service_tree。service_tree這個數據結構對應的都是一系列的紅黑樹,主要目的是用來實現請求優先順序分類的,就是RT、BE、IDLE的分類。每一個cfq_group都維護了7個service_trees,其定義如下:其中service_tree_idle就是用來給IDLE類型的請求進行排隊用的紅黑樹。而上面二維數組,首先第一個維度針對RT和BE分別各實現了一個數組,每一個數組中都維護了三個紅黑樹,分別對應三種不同子類型的請求,分別是:SYNC、SYNC_NOIDLE以及ASYNC。我們可以認為SYNC相當於SYNC_IDLE並與SYNC_NOIDLE對應。idling是cfq在設計上為了盡量合並連續的IO請求以達到提高吞吐量的目的而加入的機制,我們可以理解為是一種「空轉」等待機制。空轉是指,當一個隊列處理一個請求結束後,會在發生調度之前空等一小會時間,如果下一個請求到來,則可以減少磁頭定址,繼續處理順序的IO請求。為了實現這個功能,cfq在service_tree這層數據結構這實現了SYNC隊列,如果請求是同步順序請求,就入隊這個servicetree,如果請求是同步隨機請求,則入隊SYNC_NOIDLE隊列,以判斷下一個請求是否是順序請求。所有的非同步寫操作請求將入隊ASYNC的servicetree,並且針對這個隊列沒有空轉等待機制。此外,cfq還對SSD這樣的硬碟有特殊調整,當cfq發現存儲設備是一個ssd硬碟這樣的隊列深度更大的設備時,所有針對單獨隊列的空轉都將不生效,所有的IO請求都將入隊SYNC_NOIDLE這個servicetree。每一個servicetree都對應了若干個cfq_queue隊列,每個cfq_queue隊列對應一個進程,這個我們後續再詳細說明。cfq_group還維護了一個在cgroup內部所有進程公用的非同步IO請求隊列,其結構如下:非同步請求也分成了RT、BE、IDLE這三類進行處理,每一類對應一個cfq_queue進行排隊。BE和RT也實現了優先順序的支持,每一個類型有IOPRIO_BE_NR這么多個優先順序,這個值定義為8,數組下標為0-7。我們目前分析的內核代碼版本為Linux4.4,可以看出,從cfq的角度來說,已經可以實現非同步IO的cgroup支持了,我們需要定義一下這里所謂非同步IO的含義,它僅僅表示從內存的buffer/cache中的數據同步到硬碟的IO請求,而不是aio(man7aio)或者linux的native非同步io以及lio機制,實際上這些所謂的「非同步」IO機制,在內核中都是同步實現的(本質上馮諾伊曼計算機沒有真正的「非同步」機制)。我們在上面已經說明過,由於進程正常情況下都是將數據先寫入buffer/cache,所以這種非同步IO都是統一由cfq_group中的async請求隊列處理的。那麼為什麼在上面的service_tree中還要實現和一個ASYNC的類型呢?這當然是為了支持區分進程的非同步IO並使之可以「完全公平」做准備嘍。實際上在最新的cgroupv2的blkio體系中,內核已經支持了針對bufferIO的cgroup限速支持,而以上這些可能容易混淆的一堆類型,都是在新的體系下需要用到的類型標記。新體系的復雜度更高了,功能也更加強大,但是大家先不要著急,正式的cgroupv2體系,在Linux4.5發布的時候會正式跟大家見面。我們繼續選擇service_tree的過程,三種優先順序類型的service_tree的選擇就是根據類型的優先順序來做選擇的,RT優先順序最高,BE其次,IDLE最低。就是說,RT里有,就會一直處理RT,RT沒了再處理BE。每個service_tree對應一個元素為cfq_queue排隊的紅黑樹,而每個cfq_queue就是內核為進程(線程)創建的請求隊列。每一個cfq_queue都會維護一個rb_key的變數,這個變數實際上就是這個隊列的IO服務時間(servicetime)。這里還是通過紅黑樹找到servicetime時間最短的那個cfq_queue進行服務,以保證「完全公平」。選擇好了cfq_queue之後,就要開始處理這個隊列里的IO請求了。這里的調度方式基本跟deadline類似。cfq_queue會對進入隊列的每一個請求進行兩次入隊,一個放進fifo中,另一個放進按訪問扇區順序作為key的紅黑樹中。默認從紅黑樹中取請求進行處理,當請求的延時時間達到deadline時,就從紅黑樹中取等待時間最長的進行處理,以保證請求不被餓死。這就是整個cfq的調度流程,當然其中還有很多細枝末節沒有交代,比如合並處理以及順序處理等等。1.2cfq的參數調整理解整個調度流程有助於我們決策如何調整cfq的相關參數。所有cfq的可調參數都可以在/sys/class/block/sda/queue/iosched/目錄下找到,當然,在你的系統上,請將sda替換為相應的磁碟名稱。我們來看一下都有什麼:這些參數部分是跟機械硬碟磁頭尋道方式有關的,如果其說明你看不懂,請先補充相關知識:back_seek_max:磁頭可以向後定址的最大范圍,默認值為16M。back_seek_penalty:向後定址的懲罰系數。這個值是跟向前定址進行比較的。以上兩個是為了防止磁頭尋道發生抖動而導致定址過慢而設置的。基本思路是這樣,一個io請求到來的時候,cfq會根據其定址位置預估一下其磁頭尋道成本。設置一個最大值back_seek_max,對於請求所訪問的扇區號在磁頭後方的請求,只要定址范圍沒有超過這個值,cfq會像向前定址的請求一樣處理它。再設置一個評估成本的系數back_seek_penalty,相對於磁頭向前定址,向後定址的距離為1/2(1/back_seek_penalty)時,cfq認為這兩個請求定址的代價是相同。這兩個參數實際上是cfq判斷請求合並處理的條件限制,凡事復合這個條件的請求,都會盡量在本次請求處理的時候一起合並處理。fifo_expire_async:設置非同步請求的超時時間。同步請求和非同步請求是區分不同隊列處理的,cfq在調度的時候一般情況都會優先處理同步請求,之後再處理非同步請求,除非非同步請求符合上述合並處理的條件限制范圍內。當本進程的隊列被調度時,cfq會優先檢查是否有非同步請求超時,就是超過fifo_expire_async參數的限制。如果有,則優先發送一個超時的請求,其餘請求仍然按照優先順序以及扇區編號大小來處理。fifo_expire_sync:這個參數跟上面的類似,區別是用來設置同步請求的超時時間。slice_idle:參數設置了一個等待時間。這讓cfq在切換cfq_queue或servicetree的時候等待一段時間,目的是提高機械硬碟的吞吐量。一般情況下,來自同一個cfq_queue或者servicetree的IO請求的定址局部性更好,所以這樣可以減少磁碟的定址次數。這個值在機械硬碟上默認為非零。當然在固態硬碟或者硬RAID設備上設置這個值為非零會降低存儲的效率,因為固態硬碟沒有磁頭定址這個概念,所以在這樣的設備上應該設置為0,關閉此功能。group_idle:這個參數也跟上一個參數類似,區別是當cfq要切換cfq_group的時候會等待一段時間。在cgroup的場景下,如果我們沿用slice_idle的方式,那麼空轉等待可能會在cgroup組內每個進程的cfq_queue切換時發生。這樣會如果這個進程一直有請求要處理的話,那麼直到這個cgroup的配額被耗盡,同組中的其它進程也可能無法被調度到。這樣會導致同組中的其它進程餓死而產生IO性能瓶頸。在這種情況下,我們可以將slice_idle=0而group_idle=8。這樣空轉等待就是以cgroup為單位進行的,而不是以cfq_queue的進程為單位進行,以防止上述問題產生。low_latency:這個是用來開啟或關閉cfq的低延時(lowlatency)模式的開關。當這個開關打開時,cfq將會根據target_latency的參數設置來對每一個進程的分片時間(slicetime)進行重新計算。這將有利於對吞吐量的公平(默認是對時間片分配的公平)。關閉這個參數(設置為0)將忽略target_latency的值。這將使系統中的進程完全按照時間片方式進行IO資源分配。這個開關默認是打開的。我們已經知道cfq設計上有「空轉」(idling)這個概念,目的是為了可以讓連續的讀寫操作盡可能多的合並處理,減少磁頭的定址操作以便增大吞吐量。如果有進程總是很快的進行順序讀寫,那麼它將因為cfq的空轉等待命中率很高而導致其它需要處理IO的進程響應速度下降,如果另一個需要調度的進程不會發出大量順序IO行為的話,系統中不同進程IO吞吐量的表現就會很不均衡。就比如,系統內存的cache中有很多臟頁要寫回時,桌面又要打開一個瀏覽器進行操作,這時臟頁寫回的後台行為就很可能會大量命中空轉時間,而導致瀏覽器的小量IO一直等待,讓用戶感覺瀏覽器運行響應速度變慢。這個low_latency主要是對這種情況進行優化的選項,當其打開時,系統會根據target_latency的配置對因為命中空轉而大量佔用IO吞吐量的進程進行限制,以達到不同進程IO佔用的吞吐量的相對均衡。這個開關比較合適在類似桌面應用的場景下打開。target_latency:當low_latency的值為開啟狀態時,cfq將根據這個值重新計算每個進程分配的IO時間片長度。quantum:這個參數用來設置每次從cfq_queue中處理多少個IO請求。在一個隊列處理事件周期中,超過這個數字的IO請求將不會被處理。這個參數只對同步的請求有效。slice_sync:當一個cfq_queue隊列被調度處理時,它可以被分配的處理總時間是通過這個值來作為一個計算參數指定的。公式為:time_slice=slice_sync+(slice_sync/5*(4-prio))。這個參數對同步請求有效。slice_async:這個值跟上一個類似,區別是對非同步請求有效。slice_async_rq:這個參數用來限制在一個slice的時間范圍內,一個隊列最多可以處理的非同步請求個數。請求被處理的最大個數還跟相關進程被設置的io優先順序有關。1.3cfq的IOPS模式我們已經知道,默認情況下cfq是以時間片方式支持的帶優先順序的調度來保證IO資源佔用的公平。高優先順序的進程將得到的時間片長度,而低優先順序的進程時間片相對較小。當我們的存儲是一個高速並且支持NCQ(原生指令隊列)的設備的時候,我們最好可以讓其可以從多個cfq隊列中處理多路的請求,以便提升NCQ的利用率。此時使用時間片的分配方式分配資源就顯得不合時宜了,因為基於時間片的分配,同一時刻最多能處理的請求隊列只有一個。這時,我們需要切換cfq的模式為IOPS模式。切換方式很簡單,就是將slice_idle=0即可。內核會自動檢測你的存儲設備是否支持NCQ,如果支持的話cfq會自動切換為IOPS模式。另外,在默認的基於優先順序的時間片方式下,我們可以使用ionice命令來調整進程的IO優先順序。進程默認分配的IO優先順序是根據進程的nice值計算而來的,計算方法可以在manionice中看到,這里不再廢話。2、deadline:最終期限調度deadline調度演算法相對cfq要簡單很多。其設計目標是:在保證請求按照設備扇區的順序進行訪問的同時,兼顧其它請求不被餓死,要在一個最終期限前被調度到。我們知道磁頭對磁碟的尋道是可以進行順序訪問和隨機訪問的,因為尋道延時時間的關系,順序訪問時IO的吞吐量更大,隨機訪問的吞吐量小。如果我們想為一個機械硬碟進行吞吐量優化的話,那麼就可以讓調度器按照盡量復合順序訪問的IO請求進行排序,之後請求以這樣的順序發送給硬碟,就可以使IO的吞吐量更大。但是這樣做也有另一個問題,就是如果此時出現了一個請求,它要訪問的磁軌離目前磁頭所在磁軌很遠,應用的請求又大量集中在目前磁軌附近。導致大量請求一直會被合並和插隊處理,而那個要訪問比較遠磁軌的請求將因為一直不能被調度而餓死。deadline就是這樣一種調度器,能在保證IO最大吞吐量的情況下,盡量使遠端請求在一個期限內被調度而不被餓死的調度器。