第十六章--访问文件
生活随笔
收集整理的這篇文章主要介紹了
第十六章--访问文件
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
本章所涵蓋的主題即應(yīng)用于磁盤文件系統(tǒng)的普通文件,也應(yīng)用于塊設(shè)備文件;將這兩種文件系統(tǒng)都簡(jiǎn)單地統(tǒng)稱為“文件”。
??????? 訪問文件的模式有多種。我們?cè)诒菊驴紤]如下幾種情況:
??????? 規(guī)范模式:
????????規(guī)范模式下文件打開后,標(biāo)志O_SYNC與O_DIRECT清0,而且它的內(nèi)容是由系統(tǒng)調(diào)用read()和write()來存取。系統(tǒng)調(diào)用read()將阻塞調(diào)用進(jìn)程,直到數(shù)據(jù)被拷貝進(jìn)用戶態(tài)地址空間(內(nèi)核允許返回的字節(jié)數(shù)少于要求的字節(jié)數(shù))。但系統(tǒng)調(diào)用write()不同,它在數(shù)據(jù)被拷貝到頁高速緩存(延遲寫)后就馬上結(jié)束。
??????? 同步模式:
????????同步模式下文件打開后,標(biāo)志O_SYNC置1或稍后由系統(tǒng)調(diào)用fcntl()對(duì)其置1。這個(gè)標(biāo)志只影響寫操作(讀操作總是會(huì)阻塞),它將阻塞調(diào)用進(jìn)程,直到數(shù)據(jù)被有效地寫入磁盤。
??????? 內(nèi)存映射模式:
??????? 內(nèi)存映射模式下文件打開后,應(yīng)用程序發(fā)出系統(tǒng)調(diào)用mmap()將文件映射到內(nèi)存中。因此,文件就成為RAM中的一個(gè)字節(jié)數(shù)組,應(yīng)用程序就可以直接訪問數(shù)組元素,而不需要系統(tǒng)調(diào)用read()、write()或lseek()。
??????? 直接I/O模式:
??????? 直接I/O模式下文件打開后,標(biāo)志O_DIRECT置1。任何讀寫操作都將數(shù)據(jù)在用戶態(tài)地址空間與磁盤間直接傳送而不通過頁高速緩存。
??????? 異步模式:
??????? 異步模式下,文件的訪問可以有兩種方法,即通過一組POSIX API或Linux特有的系統(tǒng)調(diào)用來實(shí)現(xiàn)。所謂異步模式就是數(shù)據(jù)傳輸請(qǐng)求并不阻塞調(diào)用進(jìn)程,而是在后臺(tái)執(zhí)行,同時(shí)應(yīng)用程序繼續(xù)它的正常運(yùn)行。
一、讀寫文件
??????? 讀文件是基于頁的,內(nèi)核總是一次傳送幾個(gè)完整的數(shù)據(jù)頁。如果進(jìn)程發(fā)出read()系統(tǒng)調(diào)用來讀取一些字節(jié),而這些數(shù)據(jù)還不在RAM中,那么,內(nèi)核就要分配一個(gè)新頁框,并使用文件的適當(dāng)部分來填充這個(gè)頁,把該頁加入頁高速緩存,最后把所請(qǐng)求的字節(jié)拷貝到進(jìn)程地址空間中。對(duì)于大部分文件系統(tǒng)來說,從文件中讀取一個(gè)數(shù)據(jù)頁就等同于在磁盤上查找所請(qǐng)求的數(shù)據(jù)存放在哪些塊上。事實(shí)上,大多數(shù)磁盤文件系統(tǒng)的read方法是由名為generic_file_read()的通用函數(shù)實(shí)現(xiàn)的。
??????? 對(duì)基于磁盤的文件來說,寫操作的處理相當(dāng)復(fù)雜,因?yàn)槲募笮】梢愿淖?#xff0c;因此內(nèi)核可能會(huì)分配磁盤上的一些物理塊。當(dāng)然,這個(gè)過程到底如何實(shí)現(xiàn)要取決于文件系統(tǒng)的類型。不過,很多磁盤文件系統(tǒng)是通過通用函數(shù)generic_file_write()實(shí)現(xiàn)它們的write方法的。這樣的文件系統(tǒng)如Ext2、System V/Coherent/Xenix及Minix。另一方面,還有幾個(gè)文件系統(tǒng)(如日志文件系統(tǒng)和網(wǎng)絡(luò)文件系統(tǒng))通過自定義的函數(shù)實(shí)現(xiàn)它們的write方法。
1.1、從文件中讀取數(shù)據(jù)
1.1.1、普通文件的readpage方法
1.1.2、塊設(shè)備文件的readpage方法
1.2、文件的預(yù)讀
??????? 預(yù)讀(read-ahead)是一種技術(shù),這種技術(shù)在于在實(shí)際請(qǐng)求前讀普通文件或塊設(shè)備文件的幾個(gè)相鄰的數(shù)據(jù)頁。在大多數(shù)情況下,預(yù)讀能極大地提高磁盤的性能,因?yàn)轭A(yù)讀使磁盤控制器處理較少的命令,其中的每條命令都涉及一大組相鄰的扇區(qū)。此外,預(yù)讀還能提高系統(tǒng)的響應(yīng)能力。順序讀取文件的進(jìn)程通常不需要等待請(qǐng)求的數(shù)據(jù),因?yàn)檎?qǐng)求的數(shù)據(jù)已經(jīng)在RAM中了。
??????? 但是,預(yù)讀對(duì)于隨機(jī)訪問的文件是沒有用的;在這種情況下,預(yù)讀實(shí)際上是有害的,因?yàn)樗脽o用的信息浪費(fèi)了頁高速緩存的空間。因此,當(dāng)內(nèi)核確定出最近所進(jìn)行的I/O訪問與前一次I/O訪問不是順序的時(shí)候就減少或停止預(yù)讀。
??????? 文件的預(yù)讀需要更復(fù)雜的算法,這是由于以下幾個(gè)原因:
??????? * 由于數(shù)據(jù)是逐頁進(jìn)行讀取的,因此預(yù)讀算法不必考慮頁內(nèi)偏移量,只要考慮所訪問的頁在文件內(nèi)部的位置就可以了。
??????? * 只要進(jìn)程持續(xù)地順序訪問一個(gè)文件,預(yù)讀就會(huì)逐漸增加。
??????? * 當(dāng)前的訪問與上一次訪問不是順序的時(shí)(隨機(jī)訪問),預(yù)讀就會(huì)逐漸減少乃至禁止。
??????? * 當(dāng)一個(gè)進(jìn)程重復(fù)地訪問同一頁(即只使用文件的很小一部分)時(shí),或者當(dāng)幾乎所有的頁都已在頁高速緩存內(nèi)時(shí),預(yù)讀就必須停止。
??????? * 低級(jí)I/O設(shè)備驅(qū)動(dòng)程序必須在合適的時(shí)候激活,這樣當(dāng)將來進(jìn)程需要時(shí),頁已傳送完畢。
??????? 當(dāng)訪問給定文件時(shí),預(yù)讀算法使用兩個(gè)頁面集,各自對(duì)應(yīng)文件的一個(gè)連續(xù)區(qū)域。這兩個(gè)頁面集分別叫做當(dāng)前窗(current window)和預(yù)讀窗(ahead window)。
??????? 當(dāng)前窗內(nèi)的頁是進(jìn)程請(qǐng)求的頁和內(nèi)核預(yù)讀的頁,且位于頁高速緩存內(nèi)(當(dāng)前窗內(nèi)的頁不必是最新的,因?yàn)镮/O數(shù)據(jù)傳輸仍可能在運(yùn)行中)。當(dāng)前窗包含進(jìn)程順序訪問的最后一頁,且可能有內(nèi)核預(yù)讀但進(jìn)程未申請(qǐng)的頁。
??????? 預(yù)讀窗內(nèi)的頁緊接著當(dāng)前窗內(nèi)的頁,它們是內(nèi)核正在預(yù)讀的頁。預(yù)讀窗內(nèi)的頁都不是進(jìn)程請(qǐng)求的,但內(nèi)核假定進(jìn)程會(huì)遲早請(qǐng)求。
??????? 當(dāng)內(nèi)核認(rèn)為是順序訪問而且第一頁在當(dāng)前窗內(nèi)時(shí),它就檢查是否建立了預(yù)讀窗。如果沒有,內(nèi)核創(chuàng)建一個(gè)預(yù)讀窗并觸發(fā)相應(yīng)頁的讀操作。理想情況下,進(jìn)程繼續(xù)從當(dāng)前窗請(qǐng)求頁,同時(shí)預(yù)讀窗的頁則正在傳送。當(dāng)進(jìn)程請(qǐng)求的頁在預(yù)讀窗,那么預(yù)讀窗就成為當(dāng)前窗。
??????? 何時(shí)執(zhí)行預(yù)讀算法?這有下列幾種情形:
??????? * 當(dāng)內(nèi)核用用戶態(tài)請(qǐng)求來讀文件數(shù)據(jù)的頁時(shí)。這一事件觸發(fā)page_cache_readahead()函數(shù)的調(diào)用。
??????? * 當(dāng)內(nèi)核為文件內(nèi)存映射分配一頁時(shí)。
??????? * 當(dāng)用戶態(tài)應(yīng)用執(zhí)行readahead()系統(tǒng)調(diào)用時(shí),它會(huì)對(duì)某個(gè)文件描述符顯示觸發(fā)某預(yù)讀活動(dòng)。
??????? * 當(dāng)用戶態(tài)應(yīng)用使用POSIX_FADV_NOREUSE或POSIX_FADV_WILLNEED命令執(zhí)行posix_fadvise()系統(tǒng)調(diào)用時(shí),它會(huì)通知內(nèi)核,某個(gè)范圍的文件頁不久將要被訪問。
??????? * 當(dāng)用戶態(tài)應(yīng)用使用MADV_WILLNEED命令執(zhí)行madvise()系統(tǒng)調(diào)用時(shí),它會(huì)通知內(nèi)核,某個(gè)文件內(nèi)存映射區(qū)域中的給定范圍的文件頁不久將要被訪問。
1.2.1、page_cache_readahead()函數(shù)
????????page_cache_readahead()函數(shù)處理沒有被特殊系統(tǒng)調(diào)用顯示觸發(fā)的所有預(yù)讀操作。它填寫當(dāng)前窗和預(yù)讀窗,根據(jù)預(yù)讀命中數(shù)更新當(dāng)前窗和預(yù)讀窗的大小,也就是根據(jù)過去對(duì)文件訪問預(yù)讀策略的成功程度來調(diào)整。
1.2.2、handle_ra_miss()函數(shù)
1.3、寫入文件
??????? write()系統(tǒng)調(diào)用涉及把數(shù)據(jù)從調(diào)用進(jìn)程的用戶態(tài)地址空間中移動(dòng)到內(nèi)核數(shù)據(jù)結(jié)構(gòu)中,然后再移動(dòng)到磁盤上。文件對(duì)象的write方法允許每種文件類型都定義一個(gè)專用的寫操作。在Linux2.6中,每個(gè)磁盤文件系統(tǒng)的write方法都是一個(gè)過程,該過程主要標(biāo)識(shí)寫操作所涉及的磁盤塊,把數(shù)據(jù)從用戶態(tài)地址空間拷貝到頁高速緩存的某些頁中,然后把這些頁中的緩沖區(qū)標(biāo)記成臟。
1.3.1、普通文件的prepare_write和commit_write方法
1.3.2、塊設(shè)備文件的prepare_write和commit_write方法
1.4、將臟頁寫到磁盤
??????? 系統(tǒng)調(diào)用write()的作用就是修改頁高速緩存內(nèi)一些頁的內(nèi)容,如果頁高速緩存內(nèi)沒有所要的頁則分配并追加這些頁。某些情況下(例如文件帶O_SYNC標(biāo)志打開),I/O數(shù)據(jù)傳輸立即啟動(dòng)。但是通常I/O數(shù)據(jù)傳輸是延遲進(jìn)行的。
二、內(nèi)存映射
??????? 一個(gè)線性區(qū)可以和磁盤文件系統(tǒng)的普通文件的某一部分或者塊設(shè)備文件相關(guān)聯(lián)。這就意味著內(nèi)核把對(duì)區(qū)線性中頁內(nèi)某個(gè)字節(jié)的訪問轉(zhuǎn)換成對(duì)文件中相應(yīng)字節(jié)的操作。這種技術(shù)稱為內(nèi)存映射(memory mapping)。
??????? 有兩種類型的內(nèi)存映射:
??????? 共享型:在線性區(qū)頁上的任何寫操作都會(huì)修改磁盤上的文件;而且,如果進(jìn)程對(duì)共享映射中的一個(gè)頁進(jìn)行寫,那么這種修改對(duì)于其他映射了這同一文件的所有進(jìn)程來說都是可見的。
??????? 私有型:當(dāng)進(jìn)程創(chuàng)建的映射只是為讀文件,而不是寫文件時(shí)才會(huì)使用此種映射。出于這種目的,私有映射的效率要比共享映射的效率更高。但是對(duì)私有映射頁的任何寫操作都會(huì)使內(nèi)核停止映射該文件中的頁。因此,寫操作既不會(huì)改變磁盤上的文件,對(duì)訪問相同文件的其他進(jìn)程也不可見。但是私有內(nèi)存映射中還沒有被進(jìn)程改變的頁會(huì)因?yàn)槠渌M(jìn)程進(jìn)行的文件更新而更新。
??????? 作為一條通用規(guī)則,如果一個(gè)內(nèi)存映射是共享的,相應(yīng)的線性區(qū)就設(shè)置了VM_SHARED標(biāo)志;如果一個(gè)內(nèi)存映射是私有的,那么相應(yīng)的線性區(qū)就清除了VM_SHARED標(biāo)志。
2.1、內(nèi)存映射的數(shù)據(jù)結(jié)構(gòu)
??????? 內(nèi)存映射可以用下列數(shù)據(jù)結(jié)構(gòu)的組合來表示:
??????? * 與所映射的文件相關(guān)的索引節(jié)點(diǎn)對(duì)象。
??????? * 所映射文件的address_space對(duì)象。
??????? * 不同進(jìn)程對(duì)一個(gè)文件進(jìn)行不同映射所使用的文件對(duì)象。
??????? * 對(duì)文件進(jìn)行每一不同映射所使用的vm_area_struct描述符。
??????? * 對(duì)文件進(jìn)行映射的線性區(qū)所分配的每個(gè)頁框所對(duì)應(yīng)的頁描述符。
??????? 共享內(nèi)存映射的頁通常都包含在頁高速緩存中;私有內(nèi)存映射的頁只要還沒有被修改,也都包含在頁高速緩存中。當(dāng)進(jìn)程試圖修改一個(gè)私有內(nèi)存映射的頁時(shí),內(nèi)核就把該頁框進(jìn)行復(fù)制,并在進(jìn)程頁表中用復(fù)制的頁來替換原來的頁框,這是第八章中介紹的寫時(shí)復(fù)制機(jī)制的應(yīng)用之一。雖然原來的頁框還仍然在頁高速緩存中,但不再屬于這個(gè)內(nèi)存映射,這是由于被復(fù)制的頁框替換了原來的頁框。依次類推,這個(gè)復(fù)制的頁框不會(huì)被插入到頁高速緩存中,因?yàn)槠渲兴臄?shù)據(jù)不再是磁盤上表示那個(gè)文件的有效數(shù)據(jù)。
??????? 事實(shí)上,一個(gè)新建立的內(nèi)存映射就是一個(gè)不包含任何頁的線性區(qū)。當(dāng)進(jìn)程引用線性區(qū)中的一個(gè)地址時(shí),缺頁異常發(fā)生,缺頁異常中斷處理程序檢查線性區(qū)的nopage方法是否被定義。如果沒有定義nopage,則說明線性區(qū)不映射磁盤上的文件;否則,進(jìn)行映射,這個(gè)方法通過訪問塊設(shè)備處理讀取的頁。幾乎所有磁盤文件系統(tǒng)和塊設(shè)備文件都通過filemap_nopage()函數(shù)實(shí)現(xiàn)nopage方法。
2.2、創(chuàng)建內(nèi)存映射
??????? 要?jiǎng)?chuàng)建一個(gè)新的內(nèi)存映射,進(jìn)程就要發(fā)出一個(gè)mmap()系統(tǒng)調(diào)用,并向該函數(shù)傳遞以下參數(shù):
??????? * 文件描述符,標(biāo)識(shí)要映射的文件。
??????? * 文件內(nèi)的偏移量,指定要映射的文件部分的第一個(gè)字符。
??????? * 要映射的文件部分的長度。
??????? * 一組標(biāo)志。進(jìn)程必須顯示地設(shè)置MAP_SHARED標(biāo)志或MAP_PRIVATE標(biāo)志來指定所請(qǐng)求的內(nèi)存映射的種類。
??????? * 一組權(quán)限,指定對(duì)線性區(qū)進(jìn)行訪問的一種或者多種權(quán)限:讀訪問(PROT_READ)、寫訪問(PROT_WRITE)或執(zhí)行訪問(PROT_EXEC)。
??????? * 一個(gè)可選的線性地址,內(nèi)核把該地址作為新線性區(qū)應(yīng)該從哪里開始的一個(gè)線索。如果指定了MAP_FIXED標(biāo)志,且內(nèi)核不能從指定的線性地址開始分配新線性區(qū),那么這個(gè)系統(tǒng)調(diào)用失敗。
2.3、撤銷內(nèi)存映射
??????? 當(dāng)進(jìn)程準(zhǔn)備撤銷一個(gè)內(nèi)存映射時(shí),就調(diào)用munmap();該系統(tǒng)調(diào)用還可用于減少每種內(nèi)存區(qū)的大小。給它傳遞的參數(shù)如下:
??????? * 要?jiǎng)h除的線性地址區(qū)間中第一個(gè)單元的地址。
??????? * 要?jiǎng)h除的線性地址區(qū)間的長度。
??????? 該系統(tǒng)調(diào)用的sys_munmap()服務(wù)例程實(shí)際上是調(diào)用do_munmap()函數(shù)。注意,不需要將待撤銷可寫共享內(nèi)存映射中的頁刷新到磁盤。實(shí)際上,因?yàn)檫@些頁仍然在頁高速緩存內(nèi),因此繼續(xù)起磁盤高速緩存的作用。
2.4、內(nèi)存映射的請(qǐng)求調(diào)頁
??????? 處于效率的原因,內(nèi)存映射創(chuàng)建之后并沒有立即把頁框分配給它,而是盡可能向后推遲到不能再推遲----也就是說,當(dāng)進(jìn)程試圖對(duì)其中的一頁進(jìn)行尋址時(shí),就產(chǎn)生一個(gè)“缺頁”異常。
2.5、把內(nèi)存映射的臟頁刷新到磁盤
??????? 進(jìn)程可以使用msync()系統(tǒng)調(diào)用把屬于共享內(nèi)存映射的臟頁刷新到磁盤。這個(gè)系統(tǒng)調(diào)用所接收的參數(shù)為:一個(gè)線性地址區(qū)間的起始地址、區(qū)間的長度以及具有下列含義的一組標(biāo)志。
??????? MS_SYNC:要求這個(gè)系統(tǒng)調(diào)用掛起進(jìn)程,直到I/O操作完成為止。在這種方式中,調(diào)用進(jìn)程就可以假設(shè)當(dāng)系統(tǒng)調(diào)用完成時(shí),這個(gè)內(nèi)存映射中的所有頁都已經(jīng)被刷新到磁盤。
??????? MS_ASYNC(對(duì)MS_SYNC的補(bǔ)充):要求系統(tǒng)調(diào)用立即返回,而不用掛起調(diào)用進(jìn)程。
??????? MS_INVALIDATE:要求系統(tǒng)調(diào)用使同一文件的其他內(nèi)存映射無效(沒有真正實(shí)現(xiàn),因?yàn)樵贚inux中無用)。
2.6、非線性內(nèi)存映射
??????? 為了實(shí)現(xiàn)非線性映射,內(nèi)核使用了另外一些數(shù)據(jù)結(jié)構(gòu)。首先,線性區(qū)描述符的VM_NONLINEAR標(biāo)志用于表示線性區(qū)存在一個(gè)非線性映射。給定文件的所有非線性映射線性區(qū)描述符都存放在一個(gè)雙向循環(huán)鏈表,該鏈表根植于address_space對(duì)象的i_mmap_nonlinear字段。
三、直接I/O傳送
??????? Linux提供了繞過頁高速緩存的簡(jiǎn)單方法:直接I/O傳送。在每次I/O直接傳送中,內(nèi)核對(duì)磁盤控制器進(jìn)行編程,以便在自緩存的應(yīng)用程序的用戶態(tài)地址空間中的頁與磁盤之間直接傳送數(shù)據(jù)。
??????? 我們知道,任何數(shù)據(jù)傳送都是異步進(jìn)行的。當(dāng)數(shù)據(jù)傳送正在進(jìn)行時(shí),內(nèi)核可能切換當(dāng)前進(jìn)程,CPU可能返回到用戶態(tài),產(chǎn)生數(shù)據(jù)傳送的進(jìn)程的頁可能被交換出去,等等。這對(duì)于普通I/O數(shù)據(jù)傳送沒有什么影響,因?yàn)樗鼈兩婕按疟P高速緩存中的頁,磁盤高速緩存由內(nèi)核擁有,不能被換出去,并且對(duì)內(nèi)核態(tài)的所有進(jìn)程都是可見的。
??????? 另一方面,直接I/O傳送應(yīng)當(dāng)在給定進(jìn)程的用戶態(tài)地址空間的頁內(nèi)移動(dòng)數(shù)據(jù)。內(nèi)核必須當(dāng)心這些頁是由內(nèi)核態(tài)的任一進(jìn)程訪問的,當(dāng)數(shù)據(jù)傳送正在進(jìn)行時(shí)不能把它們交換出去。
四、異步I/O
??????? “異步”實(shí)際上就是:當(dāng)用戶態(tài)進(jìn)程調(diào)用庫函數(shù)讀寫文件時(shí),一旦讀寫操作進(jìn)入隊(duì)列函數(shù)就結(jié)束,甚至有可能真正的I/O數(shù)據(jù)傳輸還沒有開始。這樣調(diào)用進(jìn)程可以在數(shù)據(jù)正在傳輸時(shí)繼續(xù)自己的運(yùn)行。
4.1、Linux2.6中的異步I/O
4.1.1、異步I/O環(huán)境
??????? 基本上,一個(gè)異步I/O環(huán)境(簡(jiǎn)稱AIO環(huán)境)就是一組數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)用于跟蹤進(jìn)程請(qǐng)求的異步I/O操作的運(yùn)行情況。每個(gè)AIO環(huán)境與一個(gè)kioctx對(duì)象關(guān)聯(lián),它存放了與該環(huán)境有關(guān)的所有信息。一個(gè)應(yīng)用可以創(chuàng)建多個(gè)AIO環(huán)境。一個(gè)給定進(jìn)程的所有的kioctx描述符存放在一個(gè)單向鏈表中,該鏈表位于內(nèi)存描述符的ioctx_list字段。
??????? AIO環(huán)是用戶態(tài)進(jìn)程中地址空間的內(nèi)存緩沖區(qū),它也可以由內(nèi)核態(tài)的所有進(jìn)程訪問。kioctx對(duì)象中的ring_info.mmap_base和ring_info_mmap_size字段分別存放AIO環(huán)的用戶態(tài)起始地址和長度。ring_info.ring_pages字段則存放有一個(gè)數(shù)組指針,該數(shù)組存放所有含AIO環(huán)的頁框的描述符。
??????? AIO環(huán)實(shí)際上是一個(gè)環(huán)形緩沖區(qū),內(nèi)核用它來寫正運(yùn)行的異步I/O操作的完成報(bào)告。AIO環(huán)的第一個(gè)字節(jié)有一個(gè)首部(struct aio_ring 數(shù)據(jù)結(jié)構(gòu)),后面的所有字節(jié)是io_event數(shù)據(jù)結(jié)構(gòu),每個(gè)都表示一個(gè)已完成的異步I/O操作。因?yàn)锳IO環(huán)的頁映射至進(jìn)程的用戶態(tài)地址空間,應(yīng)用可以直接檢查正運(yùn)行的異步I/O操作的情況,從而避免使用相對(duì)較慢的系統(tǒng)調(diào)用。
4.1.2、提交異步I/O操作
??????? 訪問文件的模式有多種。我們?cè)诒菊驴紤]如下幾種情況:
??????? 規(guī)范模式:
????????規(guī)范模式下文件打開后,標(biāo)志O_SYNC與O_DIRECT清0,而且它的內(nèi)容是由系統(tǒng)調(diào)用read()和write()來存取。系統(tǒng)調(diào)用read()將阻塞調(diào)用進(jìn)程,直到數(shù)據(jù)被拷貝進(jìn)用戶態(tài)地址空間(內(nèi)核允許返回的字節(jié)數(shù)少于要求的字節(jié)數(shù))。但系統(tǒng)調(diào)用write()不同,它在數(shù)據(jù)被拷貝到頁高速緩存(延遲寫)后就馬上結(jié)束。
??????? 同步模式:
????????同步模式下文件打開后,標(biāo)志O_SYNC置1或稍后由系統(tǒng)調(diào)用fcntl()對(duì)其置1。這個(gè)標(biāo)志只影響寫操作(讀操作總是會(huì)阻塞),它將阻塞調(diào)用進(jìn)程,直到數(shù)據(jù)被有效地寫入磁盤。
??????? 內(nèi)存映射模式:
??????? 內(nèi)存映射模式下文件打開后,應(yīng)用程序發(fā)出系統(tǒng)調(diào)用mmap()將文件映射到內(nèi)存中。因此,文件就成為RAM中的一個(gè)字節(jié)數(shù)組,應(yīng)用程序就可以直接訪問數(shù)組元素,而不需要系統(tǒng)調(diào)用read()、write()或lseek()。
??????? 直接I/O模式:
??????? 直接I/O模式下文件打開后,標(biāo)志O_DIRECT置1。任何讀寫操作都將數(shù)據(jù)在用戶態(tài)地址空間與磁盤間直接傳送而不通過頁高速緩存。
??????? 異步模式:
??????? 異步模式下,文件的訪問可以有兩種方法,即通過一組POSIX API或Linux特有的系統(tǒng)調(diào)用來實(shí)現(xiàn)。所謂異步模式就是數(shù)據(jù)傳輸請(qǐng)求并不阻塞調(diào)用進(jìn)程,而是在后臺(tái)執(zhí)行,同時(shí)應(yīng)用程序繼續(xù)它的正常運(yùn)行。
一、讀寫文件
??????? 讀文件是基于頁的,內(nèi)核總是一次傳送幾個(gè)完整的數(shù)據(jù)頁。如果進(jìn)程發(fā)出read()系統(tǒng)調(diào)用來讀取一些字節(jié),而這些數(shù)據(jù)還不在RAM中,那么,內(nèi)核就要分配一個(gè)新頁框,并使用文件的適當(dāng)部分來填充這個(gè)頁,把該頁加入頁高速緩存,最后把所請(qǐng)求的字節(jié)拷貝到進(jìn)程地址空間中。對(duì)于大部分文件系統(tǒng)來說,從文件中讀取一個(gè)數(shù)據(jù)頁就等同于在磁盤上查找所請(qǐng)求的數(shù)據(jù)存放在哪些塊上。事實(shí)上,大多數(shù)磁盤文件系統(tǒng)的read方法是由名為generic_file_read()的通用函數(shù)實(shí)現(xiàn)的。
??????? 對(duì)基于磁盤的文件來說,寫操作的處理相當(dāng)復(fù)雜,因?yàn)槲募笮】梢愿淖?#xff0c;因此內(nèi)核可能會(huì)分配磁盤上的一些物理塊。當(dāng)然,這個(gè)過程到底如何實(shí)現(xiàn)要取決于文件系統(tǒng)的類型。不過,很多磁盤文件系統(tǒng)是通過通用函數(shù)generic_file_write()實(shí)現(xiàn)它們的write方法的。這樣的文件系統(tǒng)如Ext2、System V/Coherent/Xenix及Minix。另一方面,還有幾個(gè)文件系統(tǒng)(如日志文件系統(tǒng)和網(wǎng)絡(luò)文件系統(tǒng))通過自定義的函數(shù)實(shí)現(xiàn)它們的write方法。
1.1、從文件中讀取數(shù)據(jù)
1.1.1、普通文件的readpage方法
1.1.2、塊設(shè)備文件的readpage方法
1.2、文件的預(yù)讀
??????? 預(yù)讀(read-ahead)是一種技術(shù),這種技術(shù)在于在實(shí)際請(qǐng)求前讀普通文件或塊設(shè)備文件的幾個(gè)相鄰的數(shù)據(jù)頁。在大多數(shù)情況下,預(yù)讀能極大地提高磁盤的性能,因?yàn)轭A(yù)讀使磁盤控制器處理較少的命令,其中的每條命令都涉及一大組相鄰的扇區(qū)。此外,預(yù)讀還能提高系統(tǒng)的響應(yīng)能力。順序讀取文件的進(jìn)程通常不需要等待請(qǐng)求的數(shù)據(jù),因?yàn)檎?qǐng)求的數(shù)據(jù)已經(jīng)在RAM中了。
??????? 但是,預(yù)讀對(duì)于隨機(jī)訪問的文件是沒有用的;在這種情況下,預(yù)讀實(shí)際上是有害的,因?yàn)樗脽o用的信息浪費(fèi)了頁高速緩存的空間。因此,當(dāng)內(nèi)核確定出最近所進(jìn)行的I/O訪問與前一次I/O訪問不是順序的時(shí)候就減少或停止預(yù)讀。
??????? 文件的預(yù)讀需要更復(fù)雜的算法,這是由于以下幾個(gè)原因:
??????? * 由于數(shù)據(jù)是逐頁進(jìn)行讀取的,因此預(yù)讀算法不必考慮頁內(nèi)偏移量,只要考慮所訪問的頁在文件內(nèi)部的位置就可以了。
??????? * 只要進(jìn)程持續(xù)地順序訪問一個(gè)文件,預(yù)讀就會(huì)逐漸增加。
??????? * 當(dāng)前的訪問與上一次訪問不是順序的時(shí)(隨機(jī)訪問),預(yù)讀就會(huì)逐漸減少乃至禁止。
??????? * 當(dāng)一個(gè)進(jìn)程重復(fù)地訪問同一頁(即只使用文件的很小一部分)時(shí),或者當(dāng)幾乎所有的頁都已在頁高速緩存內(nèi)時(shí),預(yù)讀就必須停止。
??????? * 低級(jí)I/O設(shè)備驅(qū)動(dòng)程序必須在合適的時(shí)候激活,這樣當(dāng)將來進(jìn)程需要時(shí),頁已傳送完畢。
??????? 當(dāng)訪問給定文件時(shí),預(yù)讀算法使用兩個(gè)頁面集,各自對(duì)應(yīng)文件的一個(gè)連續(xù)區(qū)域。這兩個(gè)頁面集分別叫做當(dāng)前窗(current window)和預(yù)讀窗(ahead window)。
??????? 當(dāng)前窗內(nèi)的頁是進(jìn)程請(qǐng)求的頁和內(nèi)核預(yù)讀的頁,且位于頁高速緩存內(nèi)(當(dāng)前窗內(nèi)的頁不必是最新的,因?yàn)镮/O數(shù)據(jù)傳輸仍可能在運(yùn)行中)。當(dāng)前窗包含進(jìn)程順序訪問的最后一頁,且可能有內(nèi)核預(yù)讀但進(jìn)程未申請(qǐng)的頁。
??????? 預(yù)讀窗內(nèi)的頁緊接著當(dāng)前窗內(nèi)的頁,它們是內(nèi)核正在預(yù)讀的頁。預(yù)讀窗內(nèi)的頁都不是進(jìn)程請(qǐng)求的,但內(nèi)核假定進(jìn)程會(huì)遲早請(qǐng)求。
??????? 當(dāng)內(nèi)核認(rèn)為是順序訪問而且第一頁在當(dāng)前窗內(nèi)時(shí),它就檢查是否建立了預(yù)讀窗。如果沒有,內(nèi)核創(chuàng)建一個(gè)預(yù)讀窗并觸發(fā)相應(yīng)頁的讀操作。理想情況下,進(jìn)程繼續(xù)從當(dāng)前窗請(qǐng)求頁,同時(shí)預(yù)讀窗的頁則正在傳送。當(dāng)進(jìn)程請(qǐng)求的頁在預(yù)讀窗,那么預(yù)讀窗就成為當(dāng)前窗。
??????? 何時(shí)執(zhí)行預(yù)讀算法?這有下列幾種情形:
??????? * 當(dāng)內(nèi)核用用戶態(tài)請(qǐng)求來讀文件數(shù)據(jù)的頁時(shí)。這一事件觸發(fā)page_cache_readahead()函數(shù)的調(diào)用。
??????? * 當(dāng)內(nèi)核為文件內(nèi)存映射分配一頁時(shí)。
??????? * 當(dāng)用戶態(tài)應(yīng)用執(zhí)行readahead()系統(tǒng)調(diào)用時(shí),它會(huì)對(duì)某個(gè)文件描述符顯示觸發(fā)某預(yù)讀活動(dòng)。
??????? * 當(dāng)用戶態(tài)應(yīng)用使用POSIX_FADV_NOREUSE或POSIX_FADV_WILLNEED命令執(zhí)行posix_fadvise()系統(tǒng)調(diào)用時(shí),它會(huì)通知內(nèi)核,某個(gè)范圍的文件頁不久將要被訪問。
??????? * 當(dāng)用戶態(tài)應(yīng)用使用MADV_WILLNEED命令執(zhí)行madvise()系統(tǒng)調(diào)用時(shí),它會(huì)通知內(nèi)核,某個(gè)文件內(nèi)存映射區(qū)域中的給定范圍的文件頁不久將要被訪問。
1.2.1、page_cache_readahead()函數(shù)
????????page_cache_readahead()函數(shù)處理沒有被特殊系統(tǒng)調(diào)用顯示觸發(fā)的所有預(yù)讀操作。它填寫當(dāng)前窗和預(yù)讀窗,根據(jù)預(yù)讀命中數(shù)更新當(dāng)前窗和預(yù)讀窗的大小,也就是根據(jù)過去對(duì)文件訪問預(yù)讀策略的成功程度來調(diào)整。
1.2.2、handle_ra_miss()函數(shù)
1.3、寫入文件
??????? write()系統(tǒng)調(diào)用涉及把數(shù)據(jù)從調(diào)用進(jìn)程的用戶態(tài)地址空間中移動(dòng)到內(nèi)核數(shù)據(jù)結(jié)構(gòu)中,然后再移動(dòng)到磁盤上。文件對(duì)象的write方法允許每種文件類型都定義一個(gè)專用的寫操作。在Linux2.6中,每個(gè)磁盤文件系統(tǒng)的write方法都是一個(gè)過程,該過程主要標(biāo)識(shí)寫操作所涉及的磁盤塊,把數(shù)據(jù)從用戶態(tài)地址空間拷貝到頁高速緩存的某些頁中,然后把這些頁中的緩沖區(qū)標(biāo)記成臟。
1.3.1、普通文件的prepare_write和commit_write方法
1.3.2、塊設(shè)備文件的prepare_write和commit_write方法
1.4、將臟頁寫到磁盤
??????? 系統(tǒng)調(diào)用write()的作用就是修改頁高速緩存內(nèi)一些頁的內(nèi)容,如果頁高速緩存內(nèi)沒有所要的頁則分配并追加這些頁。某些情況下(例如文件帶O_SYNC標(biāo)志打開),I/O數(shù)據(jù)傳輸立即啟動(dòng)。但是通常I/O數(shù)據(jù)傳輸是延遲進(jìn)行的。
二、內(nèi)存映射
??????? 一個(gè)線性區(qū)可以和磁盤文件系統(tǒng)的普通文件的某一部分或者塊設(shè)備文件相關(guān)聯(lián)。這就意味著內(nèi)核把對(duì)區(qū)線性中頁內(nèi)某個(gè)字節(jié)的訪問轉(zhuǎn)換成對(duì)文件中相應(yīng)字節(jié)的操作。這種技術(shù)稱為內(nèi)存映射(memory mapping)。
??????? 有兩種類型的內(nèi)存映射:
??????? 共享型:在線性區(qū)頁上的任何寫操作都會(huì)修改磁盤上的文件;而且,如果進(jìn)程對(duì)共享映射中的一個(gè)頁進(jìn)行寫,那么這種修改對(duì)于其他映射了這同一文件的所有進(jìn)程來說都是可見的。
??????? 私有型:當(dāng)進(jìn)程創(chuàng)建的映射只是為讀文件,而不是寫文件時(shí)才會(huì)使用此種映射。出于這種目的,私有映射的效率要比共享映射的效率更高。但是對(duì)私有映射頁的任何寫操作都會(huì)使內(nèi)核停止映射該文件中的頁。因此,寫操作既不會(huì)改變磁盤上的文件,對(duì)訪問相同文件的其他進(jìn)程也不可見。但是私有內(nèi)存映射中還沒有被進(jìn)程改變的頁會(huì)因?yàn)槠渌M(jìn)程進(jìn)行的文件更新而更新。
??????? 作為一條通用規(guī)則,如果一個(gè)內(nèi)存映射是共享的,相應(yīng)的線性區(qū)就設(shè)置了VM_SHARED標(biāo)志;如果一個(gè)內(nèi)存映射是私有的,那么相應(yīng)的線性區(qū)就清除了VM_SHARED標(biāo)志。
2.1、內(nèi)存映射的數(shù)據(jù)結(jié)構(gòu)
??????? 內(nèi)存映射可以用下列數(shù)據(jù)結(jié)構(gòu)的組合來表示:
??????? * 與所映射的文件相關(guān)的索引節(jié)點(diǎn)對(duì)象。
??????? * 所映射文件的address_space對(duì)象。
??????? * 不同進(jìn)程對(duì)一個(gè)文件進(jìn)行不同映射所使用的文件對(duì)象。
??????? * 對(duì)文件進(jìn)行每一不同映射所使用的vm_area_struct描述符。
??????? * 對(duì)文件進(jìn)行映射的線性區(qū)所分配的每個(gè)頁框所對(duì)應(yīng)的頁描述符。
??????? 共享內(nèi)存映射的頁通常都包含在頁高速緩存中;私有內(nèi)存映射的頁只要還沒有被修改,也都包含在頁高速緩存中。當(dāng)進(jìn)程試圖修改一個(gè)私有內(nèi)存映射的頁時(shí),內(nèi)核就把該頁框進(jìn)行復(fù)制,并在進(jìn)程頁表中用復(fù)制的頁來替換原來的頁框,這是第八章中介紹的寫時(shí)復(fù)制機(jī)制的應(yīng)用之一。雖然原來的頁框還仍然在頁高速緩存中,但不再屬于這個(gè)內(nèi)存映射,這是由于被復(fù)制的頁框替換了原來的頁框。依次類推,這個(gè)復(fù)制的頁框不會(huì)被插入到頁高速緩存中,因?yàn)槠渲兴臄?shù)據(jù)不再是磁盤上表示那個(gè)文件的有效數(shù)據(jù)。
??????? 事實(shí)上,一個(gè)新建立的內(nèi)存映射就是一個(gè)不包含任何頁的線性區(qū)。當(dāng)進(jìn)程引用線性區(qū)中的一個(gè)地址時(shí),缺頁異常發(fā)生,缺頁異常中斷處理程序檢查線性區(qū)的nopage方法是否被定義。如果沒有定義nopage,則說明線性區(qū)不映射磁盤上的文件;否則,進(jìn)行映射,這個(gè)方法通過訪問塊設(shè)備處理讀取的頁。幾乎所有磁盤文件系統(tǒng)和塊設(shè)備文件都通過filemap_nopage()函數(shù)實(shí)現(xiàn)nopage方法。
2.2、創(chuàng)建內(nèi)存映射
??????? 要?jiǎng)?chuàng)建一個(gè)新的內(nèi)存映射,進(jìn)程就要發(fā)出一個(gè)mmap()系統(tǒng)調(diào)用,并向該函數(shù)傳遞以下參數(shù):
??????? * 文件描述符,標(biāo)識(shí)要映射的文件。
??????? * 文件內(nèi)的偏移量,指定要映射的文件部分的第一個(gè)字符。
??????? * 要映射的文件部分的長度。
??????? * 一組標(biāo)志。進(jìn)程必須顯示地設(shè)置MAP_SHARED標(biāo)志或MAP_PRIVATE標(biāo)志來指定所請(qǐng)求的內(nèi)存映射的種類。
??????? * 一組權(quán)限,指定對(duì)線性區(qū)進(jìn)行訪問的一種或者多種權(quán)限:讀訪問(PROT_READ)、寫訪問(PROT_WRITE)或執(zhí)行訪問(PROT_EXEC)。
??????? * 一個(gè)可選的線性地址,內(nèi)核把該地址作為新線性區(qū)應(yīng)該從哪里開始的一個(gè)線索。如果指定了MAP_FIXED標(biāo)志,且內(nèi)核不能從指定的線性地址開始分配新線性區(qū),那么這個(gè)系統(tǒng)調(diào)用失敗。
2.3、撤銷內(nèi)存映射
??????? 當(dāng)進(jìn)程準(zhǔn)備撤銷一個(gè)內(nèi)存映射時(shí),就調(diào)用munmap();該系統(tǒng)調(diào)用還可用于減少每種內(nèi)存區(qū)的大小。給它傳遞的參數(shù)如下:
??????? * 要?jiǎng)h除的線性地址區(qū)間中第一個(gè)單元的地址。
??????? * 要?jiǎng)h除的線性地址區(qū)間的長度。
??????? 該系統(tǒng)調(diào)用的sys_munmap()服務(wù)例程實(shí)際上是調(diào)用do_munmap()函數(shù)。注意,不需要將待撤銷可寫共享內(nèi)存映射中的頁刷新到磁盤。實(shí)際上,因?yàn)檫@些頁仍然在頁高速緩存內(nèi),因此繼續(xù)起磁盤高速緩存的作用。
2.4、內(nèi)存映射的請(qǐng)求調(diào)頁
??????? 處于效率的原因,內(nèi)存映射創(chuàng)建之后并沒有立即把頁框分配給它,而是盡可能向后推遲到不能再推遲----也就是說,當(dāng)進(jìn)程試圖對(duì)其中的一頁進(jìn)行尋址時(shí),就產(chǎn)生一個(gè)“缺頁”異常。
2.5、把內(nèi)存映射的臟頁刷新到磁盤
??????? 進(jìn)程可以使用msync()系統(tǒng)調(diào)用把屬于共享內(nèi)存映射的臟頁刷新到磁盤。這個(gè)系統(tǒng)調(diào)用所接收的參數(shù)為:一個(gè)線性地址區(qū)間的起始地址、區(qū)間的長度以及具有下列含義的一組標(biāo)志。
??????? MS_SYNC:要求這個(gè)系統(tǒng)調(diào)用掛起進(jìn)程,直到I/O操作完成為止。在這種方式中,調(diào)用進(jìn)程就可以假設(shè)當(dāng)系統(tǒng)調(diào)用完成時(shí),這個(gè)內(nèi)存映射中的所有頁都已經(jīng)被刷新到磁盤。
??????? MS_ASYNC(對(duì)MS_SYNC的補(bǔ)充):要求系統(tǒng)調(diào)用立即返回,而不用掛起調(diào)用進(jìn)程。
??????? MS_INVALIDATE:要求系統(tǒng)調(diào)用使同一文件的其他內(nèi)存映射無效(沒有真正實(shí)現(xiàn),因?yàn)樵贚inux中無用)。
2.6、非線性內(nèi)存映射
??????? 為了實(shí)現(xiàn)非線性映射,內(nèi)核使用了另外一些數(shù)據(jù)結(jié)構(gòu)。首先,線性區(qū)描述符的VM_NONLINEAR標(biāo)志用于表示線性區(qū)存在一個(gè)非線性映射。給定文件的所有非線性映射線性區(qū)描述符都存放在一個(gè)雙向循環(huán)鏈表,該鏈表根植于address_space對(duì)象的i_mmap_nonlinear字段。
三、直接I/O傳送
??????? Linux提供了繞過頁高速緩存的簡(jiǎn)單方法:直接I/O傳送。在每次I/O直接傳送中,內(nèi)核對(duì)磁盤控制器進(jìn)行編程,以便在自緩存的應(yīng)用程序的用戶態(tài)地址空間中的頁與磁盤之間直接傳送數(shù)據(jù)。
??????? 我們知道,任何數(shù)據(jù)傳送都是異步進(jìn)行的。當(dāng)數(shù)據(jù)傳送正在進(jìn)行時(shí),內(nèi)核可能切換當(dāng)前進(jìn)程,CPU可能返回到用戶態(tài),產(chǎn)生數(shù)據(jù)傳送的進(jìn)程的頁可能被交換出去,等等。這對(duì)于普通I/O數(shù)據(jù)傳送沒有什么影響,因?yàn)樗鼈兩婕按疟P高速緩存中的頁,磁盤高速緩存由內(nèi)核擁有,不能被換出去,并且對(duì)內(nèi)核態(tài)的所有進(jìn)程都是可見的。
??????? 另一方面,直接I/O傳送應(yīng)當(dāng)在給定進(jìn)程的用戶態(tài)地址空間的頁內(nèi)移動(dòng)數(shù)據(jù)。內(nèi)核必須當(dāng)心這些頁是由內(nèi)核態(tài)的任一進(jìn)程訪問的,當(dāng)數(shù)據(jù)傳送正在進(jìn)行時(shí)不能把它們交換出去。
四、異步I/O
??????? “異步”實(shí)際上就是:當(dāng)用戶態(tài)進(jìn)程調(diào)用庫函數(shù)讀寫文件時(shí),一旦讀寫操作進(jìn)入隊(duì)列函數(shù)就結(jié)束,甚至有可能真正的I/O數(shù)據(jù)傳輸還沒有開始。這樣調(diào)用進(jìn)程可以在數(shù)據(jù)正在傳輸時(shí)繼續(xù)自己的運(yùn)行。
4.1、Linux2.6中的異步I/O
4.1.1、異步I/O環(huán)境
??????? 基本上,一個(gè)異步I/O環(huán)境(簡(jiǎn)稱AIO環(huán)境)就是一組數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)用于跟蹤進(jìn)程請(qǐng)求的異步I/O操作的運(yùn)行情況。每個(gè)AIO環(huán)境與一個(gè)kioctx對(duì)象關(guān)聯(lián),它存放了與該環(huán)境有關(guān)的所有信息。一個(gè)應(yīng)用可以創(chuàng)建多個(gè)AIO環(huán)境。一個(gè)給定進(jìn)程的所有的kioctx描述符存放在一個(gè)單向鏈表中,該鏈表位于內(nèi)存描述符的ioctx_list字段。
??????? AIO環(huán)是用戶態(tài)進(jìn)程中地址空間的內(nèi)存緩沖區(qū),它也可以由內(nèi)核態(tài)的所有進(jìn)程訪問。kioctx對(duì)象中的ring_info.mmap_base和ring_info_mmap_size字段分別存放AIO環(huán)的用戶態(tài)起始地址和長度。ring_info.ring_pages字段則存放有一個(gè)數(shù)組指針,該數(shù)組存放所有含AIO環(huán)的頁框的描述符。
??????? AIO環(huán)實(shí)際上是一個(gè)環(huán)形緩沖區(qū),內(nèi)核用它來寫正運(yùn)行的異步I/O操作的完成報(bào)告。AIO環(huán)的第一個(gè)字節(jié)有一個(gè)首部(struct aio_ring 數(shù)據(jù)結(jié)構(gòu)),后面的所有字節(jié)是io_event數(shù)據(jù)結(jié)構(gòu),每個(gè)都表示一個(gè)已完成的異步I/O操作。因?yàn)锳IO環(huán)的頁映射至進(jìn)程的用戶態(tài)地址空間,應(yīng)用可以直接檢查正運(yùn)行的異步I/O操作的情況,從而避免使用相對(duì)較慢的系統(tǒng)調(diào)用。
4.1.2、提交異步I/O操作
總結(jié)
以上是生活随笔為你收集整理的第十六章--访问文件的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一个插件让你在 Redux 中写 pro
- 下一篇: 图像的遍历