自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux的直接I/O機制

系統(tǒng) Linux
Linux 中提供了一種不經(jīng)過操作系統(tǒng)內(nèi)核的緩存文件訪問機制,這樣一種文件訪問機制對于那種將 I/O 緩存存放在用戶地址空間的應(yīng)用程序來說,是非常高效的。本文將基于 2.6.18 版本的內(nèi)核來討論 Linux 中直接 I/O 的技術(shù)的設(shè)計與實現(xiàn)。

對于傳統(tǒng)的操作系統(tǒng)來說,普通的 I/O 操作一般會被內(nèi)核緩存,這種 I/O 被稱作緩存 I/O。本文所介紹的文件訪問機制不經(jīng)過操作系統(tǒng)內(nèi)核的緩存,數(shù)據(jù)直接在磁盤和應(yīng)用程序地址空間進(jìn)行傳輸,所以該文件訪問的機制稱作為直接 I/O。Linux 中就提供了這樣一種文件訪問機制,對于那種將 I/O 緩存存放在用戶地址空間的應(yīng)用程序來說,直接 I/O 是一種非常高效的手段。本文將基于 2.6.18 版本的內(nèi)核來討論 Linux 中直接 I/O 的技術(shù)的設(shè)計與實現(xiàn)。

直接 I/O 的動機

在介紹直接 I/O 之前,這一小節(jié)先介紹一下為什么會出現(xiàn)直接 I/O 這種機制,即傳統(tǒng)的 I/O 操作存在哪些缺點。

什么是緩存 I/O (Buffered I/O)

緩存 I/O 又被稱作標(biāo)準(zhǔn) I/O,大多數(shù)文件系統(tǒng)的默認(rèn) I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統(tǒng)會將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache )中,也就是說,數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。緩存 I/O 有以下這些優(yōu)點:

·緩存 I/O 使用了操作系統(tǒng)內(nèi)核緩沖區(qū),在一定程度上分離了應(yīng)用程序空間和實際的物理設(shè)備。

·緩存 I/O 可以減少讀盤的次數(shù),從而提高性能。

當(dāng)應(yīng)用程序嘗試讀取某塊數(shù)據(jù)的時候,如果這塊數(shù)據(jù)已經(jīng)存放在了頁緩存中,那么這塊數(shù)據(jù)就可以立即返回給應(yīng)用程序,而不需要經(jīng)過實際的物理讀盤操作。當(dāng)然,如果數(shù)據(jù)在應(yīng)用程序讀取之前并未被存放在頁緩存中,那么就需要先將數(shù)據(jù)從磁盤讀到頁緩存中去。對于寫操作來說,應(yīng)用程序也會將數(shù)據(jù)先寫到頁緩存中去,數(shù)據(jù)是否被立即寫到磁盤上去取決于應(yīng)用程序所采用的寫操作機制:如果用戶采用的是同步寫機制( synchronous writes ), 那么數(shù)據(jù)會立即被寫回到磁盤上,應(yīng)用程序會一直等到數(shù)據(jù)被寫完為止;如果用戶采用的是延遲寫機制( deferred writes ),那么應(yīng)用程序就完全不需要等到數(shù)據(jù)全部被寫回到磁盤,數(shù)據(jù)只要被寫到頁緩存中去就可以了。在延遲寫機制的情況下,操作系統(tǒng)會定期地將放在頁緩存中的數(shù)據(jù)刷到磁盤上。與異步寫機制( asynchronous writes )不同的是,延遲寫機制在數(shù)據(jù)完全寫到磁盤上的時候不會通知應(yīng)用程序,而異步寫機制在數(shù)據(jù)完全寫到磁盤上的時候是會返回給應(yīng)用程序的。所以延遲寫機制本身是存在數(shù)據(jù)丟失的風(fēng)險的,而異步寫機制則不會有這方面的擔(dān)心。

緩存 I/O 的缺點

在緩存 I/O 機制中,DMA 方式可以將數(shù)據(jù)直接從磁盤讀到頁緩存中,或者將數(shù)據(jù)從頁緩存直接寫回到磁盤上,而不能直接在應(yīng)用程序地址空間和磁盤之間進(jìn)行數(shù)據(jù)傳輸,這樣的話,數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和頁緩存之間進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。

對于某些特殊的應(yīng)用程序來說,避開操作系統(tǒng)內(nèi)核緩沖區(qū)而直接在應(yīng)用程序地址空間和磁盤之間傳輸數(shù)據(jù)會比使用操作系統(tǒng)內(nèi)核緩沖區(qū)獲取更好的性能,下邊這一小節(jié)中提到的自緩存應(yīng)用程序就是其中的一種。

自緩存應(yīng)用程序( self-caching applications)

對于某些應(yīng)用程序來說,它會有它自己的數(shù)據(jù)緩存機制,比如,它會將數(shù)據(jù)緩存在應(yīng)用程序地址空間,這類應(yīng)用程序完全不需要使用操作系統(tǒng)內(nèi)核中的高速緩沖存儲器,這類應(yīng)用程序就被稱作是自緩存應(yīng)用程序( self-caching applications )。數(shù)據(jù)庫管理系統(tǒng)是這類應(yīng)用程序的一個代表。自緩存應(yīng)用程序傾向于使用數(shù)據(jù)的邏輯表達(dá)方式,而非物理表達(dá)方式;當(dāng)系統(tǒng)內(nèi)存較低的時候,自緩存應(yīng)用程序會讓這種數(shù)據(jù)的邏輯緩存被換出,而并非是磁盤上實際的數(shù)據(jù)被換出。自緩存應(yīng)用程序?qū)σ僮鞯臄?shù)據(jù)的語義了如指掌,所以它可以采用更加高效的緩存替換算法。自緩存應(yīng)用程序有可能會在多臺主機之間共享一塊內(nèi)存,那么自緩存應(yīng)用程序就需要提供一種能夠有效地將用戶地址空間的緩存數(shù)據(jù)置為無效的機制,從而確保應(yīng)用程序地址空間緩存數(shù)據(jù)的一致性。

對于自緩存應(yīng)用程序來說,緩存 I/O 明顯不是一個好的選擇。由此引出我們這篇文章著重要介紹的 Linux 中的直接 I/O 技術(shù)。Linux 中的直接 I/O 技術(shù)非常適用于自緩存這類應(yīng)用程序,該技術(shù)省略掉緩存 I/O 技術(shù)中操作系統(tǒng)內(nèi)核緩沖區(qū)的使用,數(shù)據(jù)直接在應(yīng)用程序地址空間和磁盤之間進(jìn)行傳輸,從而使得自緩存應(yīng)用程序可以省略掉復(fù)雜的系統(tǒng)級別的緩存結(jié)構(gòu),而執(zhí)行程序自己定義的數(shù)據(jù)讀寫管理,從而降低系統(tǒng)級別的管理對應(yīng)用程序訪問數(shù)據(jù)的影響。在下面一節(jié)中,我們會著重介紹 Linux 中提供的直接 I/O 機制的設(shè)計與實現(xiàn),該機制為自緩存應(yīng)用程序提供了很好的支持。#p#

Linux 2.6 中的直接 I/O 技術(shù)

Linux 2.6 中提供的幾種文件訪問方式

所有的 I/O 操作都是通過讀文件或者寫文件來完成的。在這里,我們把所有的外圍設(shè)備,包括鍵盤和顯示器,都看成是文件系統(tǒng)中的文件。訪問文件的方法多種多樣,這里列出下邊這幾種 Linux 2.6 中支持的文件訪問方式。

標(biāo)準(zhǔn)訪問文件的方式

在 Linux 中,這種訪問文件的方式是通過兩個系統(tǒng)調(diào)用實現(xiàn)的:read() 和 write()。當(dāng)應(yīng)用程序調(diào)用 read() 系統(tǒng)調(diào)用讀取一塊數(shù)據(jù)的時候,如果該塊數(shù)據(jù)已經(jīng)在內(nèi)存中了,那么就直接從內(nèi)存中讀出該數(shù)據(jù)并返回給應(yīng)用程序;如果該塊數(shù)據(jù)不在內(nèi)存中,那么數(shù)據(jù)會被從磁盤上讀到頁高緩存中去,然后再從頁緩存中拷貝到用戶地址空間中去。如果一個進(jìn)程讀取某個文件,那么其他進(jìn)程就都不可以讀取或者更改該文件;對于寫數(shù)據(jù)操作來說,當(dāng)一個進(jìn)程調(diào)用了 write() 系統(tǒng)調(diào)用往某個文件中寫數(shù)據(jù)的時候,數(shù)據(jù)會先從用戶地址空間拷貝到操作系統(tǒng)內(nèi)核地址空間的頁緩存中去,然后才被寫到磁盤上。但是對于這種標(biāo)準(zhǔn)的訪問文件的方式來說,在數(shù)據(jù)被寫到頁緩存中的時候,write() 系統(tǒng)調(diào)用就算執(zhí)行完成,并不會等數(shù)據(jù)完全寫入到磁盤上。Linux 在這里采用的是我們前邊提到的延遲寫機制( deferred writes )。

以標(biāo)準(zhǔn)的方式對文件進(jìn)行讀寫 

圖 1. 以標(biāo)準(zhǔn)的方式對文件進(jìn)行讀寫

同步訪問文件的方式

同步訪問文件的方式與上邊這種標(biāo)準(zhǔn)的訪問文件的方式比較類似,這兩種方法一個很關(guān)鍵的區(qū)別就是:同步訪問文件的時候,寫數(shù)據(jù)的操作是在數(shù)據(jù)完全被寫回磁盤上才算完成的;而標(biāo)準(zhǔn)訪問文件方式的寫數(shù)據(jù)操作是在數(shù)據(jù)被寫到頁高速緩沖存儲器中的時候就算執(zhí)行完成了。

數(shù)據(jù)同步寫回磁盤 

圖 2. 數(shù)據(jù)同步寫回磁盤

 

內(nèi)存映射方式

在很多操作系統(tǒng)包括 Linux 中,內(nèi)存區(qū)域( memory region )是可以跟一個普通的文件或者塊設(shè)備文件的某一個部分關(guān)聯(lián)起來的,若進(jìn)程要訪問內(nèi)存頁中某個字節(jié)的數(shù)據(jù),操作系統(tǒng)就會將訪問該內(nèi)存區(qū)域的操作轉(zhuǎn)換為相應(yīng)的訪問文件的某個字節(jié)的操作。Linux 中提供了系統(tǒng)調(diào)用 mmap() 來實現(xiàn)這種文件訪問方式。與標(biāo)準(zhǔn)的訪問文件的方式相比,內(nèi)存映射方式可以減少標(biāo)準(zhǔn)訪問文件方式中 read() 系統(tǒng)調(diào)用所帶來的數(shù)據(jù)拷貝操作,即減少數(shù)據(jù)在用戶地址空間和操作系統(tǒng)內(nèi)核地址空間之間的拷貝操作。映射通常適用于較大范圍,對于相同長度的數(shù)據(jù)來講,映射所帶來的開銷遠(yuǎn)遠(yuǎn)低于 CPU 拷貝所帶來的開銷。當(dāng)大量數(shù)據(jù)需要傳輸?shù)臅r候,采用內(nèi)存映射方式去訪問文件會獲得比較好的效率。

利用 mmap 代替 read 

圖 3. 利用 mmap 代替 read

 

直接 I/O 方式

凡是通過直接 I/O 方式進(jìn)行數(shù)據(jù)傳輸,數(shù)據(jù)均直接在用戶地址空間的緩沖區(qū)和磁盤之間直接進(jìn)行傳輸,完全不需要頁緩存的支持。操作系統(tǒng)層提供的緩存往往會使應(yīng)用程序在讀寫數(shù)據(jù)的時候獲得更好的性能,但是對于某些特殊的應(yīng)用程序,比如說數(shù)據(jù)庫管理系統(tǒng)這類應(yīng)用,他們更傾向于選擇他們自己的緩存機制,因為數(shù)據(jù)庫管理系統(tǒng)往往比操作系統(tǒng)更了解數(shù)據(jù)庫中存放的數(shù)據(jù),數(shù)據(jù)庫管理系統(tǒng)可以提供一種更加有效的緩存機制來提高數(shù)據(jù)庫中數(shù)據(jù)的存取性能。

數(shù)據(jù)傳輸不經(jīng)過操作系統(tǒng)內(nèi)核緩沖區(qū) 

圖 4. 數(shù)據(jù)傳輸不經(jīng)過操作系統(tǒng)內(nèi)核緩沖區(qū)

 

異步訪問文件的方式

Linux 異步 I/O 是 Linux 2.6 中的一個標(biāo)準(zhǔn)特性,其本質(zhì)思想就是進(jìn)程發(fā)出數(shù)據(jù)傳輸請求之后,進(jìn)程不會被阻塞,也不用等待任何操作完成,進(jìn)程可以在數(shù)據(jù)傳輸?shù)臅r候繼續(xù)執(zhí)行其他的操作。相對于同步訪問文件的方式來說,異步訪問文件的方式可以提高應(yīng)用程序的效率,并且提高系統(tǒng)資源利用率。直接 I/O 經(jīng)常會和異步訪問文件的方式結(jié)合在一起使用。

CPU 處理其他任務(wù)和 I/O 操作可以重疊執(zhí)行 

圖 5.CPU 處理其他任務(wù)和 I/O 操作可以重疊執(zhí)行

 #p#

在下邊這一小節(jié)中,我們會重點介紹 Linux 2.6 內(nèi)核中直接 I/O 的設(shè)計與實現(xiàn)。

Linux 2.6 中直接 I/O 的設(shè)計與實現(xiàn)

在塊設(shè)備或者網(wǎng)絡(luò)設(shè)備中執(zhí)行直接 I/O 完全不用擔(dān)心實現(xiàn)直接 I/O 的問題,Linux 2.6 操作系統(tǒng)內(nèi)核中高層代碼已經(jīng)設(shè)置和使用了直接 I/O,驅(qū)動程序級別的代碼甚至不需要知道已經(jīng)執(zhí)行了直接 I/O;但是對于字符設(shè)備來說,執(zhí)行直接 I/O 是不可行的,Linux 2.6 提供了函數(shù) get_user_pages() 用于實現(xiàn)直接 I/O。本小節(jié)會分別對這兩種情況進(jìn)行介紹。

內(nèi)核為塊設(shè)備執(zhí)行直接 I/O 提供的支持

要在塊設(shè)備中執(zhí)行直接 I/O,進(jìn)程必須在打開文件的時候設(shè)置對文件的訪問模式為 O_DIRECT,這樣就等于告訴操作系統(tǒng)進(jìn)程在接下來使用 read() 或者 write() 系統(tǒng)調(diào)用去讀寫文件的時候使用的是直接 I/O 方式,所傳輸?shù)臄?shù)據(jù)均不經(jīng)過操作系統(tǒng)內(nèi)核緩存空間。使用直接 I/O 讀寫數(shù)據(jù)必須要注意緩沖區(qū)對齊( buffer alignment )以及緩沖區(qū)的大小的問題,即對應(yīng) read() 以及 write() 系統(tǒng)調(diào)用的第二個和第三個參數(shù)。這里邊說的對齊指的是文件系統(tǒng)塊大小的對齊,緩沖區(qū)的大小也必須是該塊大小的整數(shù)倍。

這一節(jié)主要介紹三個函數(shù):open(),read() 以及 write()。Linux 中訪問文件具有多樣性,所以這三個函數(shù)對于處理不同的文件訪問方式定義了不同的處理方法,本文主要介紹其與直接 I/O 方式相關(guān)的函數(shù)與功能.首先,先來看 open() 系統(tǒng)調(diào)用,其函數(shù)原型如下所示:

 

  1. int open(const char *pathname, int oflag, … /*, mode_t mode * / ) ;  
  2. |-------10--------20--------30--------40--------50--------60--------70--------80--------9|  
  3. |-------- XML error:  The previous line is longer than the max of 90 characters ---------|  

以下列出了 Linux 2.6 內(nèi)核定義的系統(tǒng)調(diào)用 open() 所使用的標(biāo)識符宏定義:

表 1. open() 系統(tǒng)調(diào)用提供的標(biāo)識符  

標(biāo)識符名

 標(biāo)識符描述

O_RDONLY

以只讀的方式打開文件

O_WRONLY

以只寫的方式打開文件

O_RDWR

 

以讀寫的方式打開文件

O_CREAT

若文件不存在,則創(chuàng)建該文件

O_EXCL

以獨占模式打開文件;若同時設(shè)置 O_EXCL 和 O_CREATE, 那么若文件已經(jīng)存在,則打開操作會失敗

O_NOCTTY

若設(shè)置該描述符,則該文件不可以被當(dāng)成終端處理

O_TRUNC 

截斷文件,若文件存在,則刪除該文件

O_APPEND

若設(shè)置了該描述符,則在寫文件之前,文件指針會被設(shè)置到文件的底部

O_NONBLOCK

以非阻塞的方式打開文件

O_NELAY

同 O_NELAY,若同時設(shè)置 O_NELAY 和 O_NONBLOCK,O_NONBLOCK 優(yōu)先起作用

O_SYNC 該描述符會對普通文件的寫操作產(chǎn)生影響,若設(shè)置了該描述符,則對該文件的寫操作會等到數(shù)據(jù)被寫到磁盤上才算結(jié)束
 FASYNC

 若設(shè)置該描述符,則 I/O 事件通知是通過信號發(fā)出的

O_DIRECT 該描述符提供對直接 I/O 的支持
O_LARGEFILE  該描述符提供對超過 2GB 大文件的支持
O_DIRECTORY 該描述符表明所打開的文件必須是目錄,否則打開操作失敗
O_NOFOLLOW 若設(shè)置該描述符,則不解析路徑名尾部的符號鏈接

 當(dāng)應(yīng)用程序需要直接訪問文件而不經(jīng)過操作系統(tǒng)頁高速緩沖存儲器的時候,它打開文件的時候需要指定 O_DIRECT 標(biāo)識符。

操作系統(tǒng)內(nèi)核中處理 open() 系統(tǒng)調(diào)用的內(nèi)核函數(shù)是 sys_open(),sys_open() 會調(diào)用 do_sys_open() 去處理主要的打開操作。它主要做了三件事情:首先, 它調(diào)用 getname() 從進(jìn)程地址空間中讀取文件的路徑名;接著,do_sys_open() 調(diào)用 get_unused_fd() 從進(jìn)程的文件表中找到一個空閑的文件表指針,相應(yīng)的新文件描述符就存放在本地變量 fd 中;之后,函數(shù) do_filp_open() 會根據(jù)傳入的參數(shù)去執(zhí)行相應(yīng)的打開操作。清單 1 列出了操作系統(tǒng)內(nèi)核中處理 open() 系統(tǒng)調(diào)用的一個主要函數(shù)關(guān)系圖。

清單 1. 主要調(diào)用函數(shù)關(guān)系圖

  1. sys_open()  
  2.   |-----do_sys_open()  
  3.   |---------getname()  
  4.   |---------get_unused_fd()  
  5.   |---------do_filp_open()  
  6.   |--------nameidata_to_filp()  
  7.   |----------__dentry_open()  

函數(shù) do_flip_open() 在執(zhí)行的過程中會調(diào)用函數(shù) nameidata_to_filp(),而 nameidata_to_filp() 最終會調(diào)用 __dentry_open() 函數(shù),若進(jìn)程指定了 O_DIRECT 標(biāo)識符,則該函數(shù)會檢查直接 I./O 操作是否可以作用于該文件。清單 2 列出了 __dentry_open() 函數(shù)中與直接 I/O 操作相關(guān)的代碼。

清單 2. 函數(shù) dentry_open() 中與直接 I/O 相關(guān)的代碼

  1. if (f->f_flags & O_DIRECT) {  
  2.  
  3.          if (!f->f_mapping->a_ops ||  
  4.  
  5.             ((!f->f_mapping->a_ops->direct_IO) &&  
  6.  
  7.             (!f->f_mapping->a_ops->get_xip_page))) {  
  8.  
  9.                   fput(f);  
  10.  
  11.                   f = ERR_PTR(-EINVAL);  
  12.  
  13.           }  
  14.  
  15. }  
  16.  

當(dāng)文件打開時指定了 O_DIRECT 標(biāo)識符,那么操作系統(tǒng)就會知道接下來對文件的讀或者寫操作都是要使用直接 I/O 方式的。

下邊我們來看一下當(dāng)進(jìn)程通過 read() 系統(tǒng)調(diào)用讀取一個已經(jīng)設(shè)置了 O_DIRECT 標(biāo)識符的文件的時候,系統(tǒng)都做了哪些處理。 函數(shù) read() 的原型如下所示:

ssize_t read(int feledes, void *buff, size_t nbytes) ;

操作系統(tǒng)中處理 read() 函數(shù)的入口函數(shù)是 sys_read(),其主要的調(diào)用函數(shù)關(guān)系圖如下清單 3 所示:

清單 3. 主調(diào)用函數(shù)關(guān)系圖

  1. sys_read()  
  2.     |-----vfs_read()  
  3.           |----generic_file_read()  
  4.                |----generic_file_aio_read()  
  5.                     |--------- generic_file_direct_IO()  
  6.  

函數(shù) sys_read() 從進(jìn)程中獲取文件描述符以及文件當(dāng)前的操作位置后會調(diào)用 vfs_read() 函數(shù)去執(zhí)行具體的操作過程,而 vfs_read() 函數(shù)最終是調(diào)用了 file 結(jié)構(gòu)中的相關(guān)操作去完成文件的讀操作,即調(diào)用了 generic_file_read() 函數(shù),其代碼如下所示:

清單 4. 函數(shù) generic_file_read()

  1. ssize_t  
  2. generic_file_read(struct file *filp,  
  3. char __user *buf, size_t count, loff_t *ppos)  
  4. {  
  5.        struct iovec local_iov = { .iov_base = buf, .iov_len = count };  
  6.        struct kiocb kiocb;  
  7.        ssize_t ret;  
  8.        init_sync_kiocb(&kiocb, filp);  
  9.        ret = __generic_file_aio_read(&kiocb, &local_iov, 1, ppos);  
  10.        if (-EIOCBQUEUED == ret)  
  11.        ret = wait_on_sync_kiocb(&kiocb);  
  12.        return ret;  

函數(shù) generic_file_read() 初始化了 iovec 以及 kiocb 描述符。描述符 iovec 主要是用于存放兩個內(nèi)容:用來接收所讀取數(shù)據(jù)的用戶地址空間緩沖區(qū)的地址和緩沖區(qū)的大小;描述符 kiocb 用來跟蹤 I/O 操作的完成狀態(tài)。之后,函數(shù) generic_file_read() 凋用函數(shù) __generic_file_aio_read()。該函數(shù)檢查 iovec 中描述的用戶地址空間緩沖區(qū)是否可用,接著檢查訪問模式,若訪問模式描述符設(shè)置了 O_DIRECT,則執(zhí)行與直接 I/O 相關(guān)的代碼。函數(shù) __generic_file_aio_read() 中與直接 I/O 有關(guān)的代碼如下所示:

清單 5. 函數(shù) __generic_file_aio_read() 中與直接 I/O 有關(guān)的代碼

  1. if (filp->f_flags & O_DIRECT) {  
  2.           loff_t pos = *ppos, size;  
  3.           struct address_space *mapping;  
  4.           struct inode *inode;  
  5.           mapping = filp->f_mapping;  
  6.           inode = mapping->host;   
  7.           retval = 0;  
  8.           if (!count)  
  9.              goto out;  
  10.           size = i_size_read(inode);  
  11.           if (pos < size) {  
  12.              retval = generic_file_direct_IO(READ, iocb,  
  13. iov, pos, nr_segs);  
  14.           if (retval > 0 && !is_sync_kiocb(iocb))  
  15.              retval = -EIOCBQUEUED;  
  16.           if (retval > 0)  
  17.              *ppos = pos + retval;  
  18.           }  
  19. file_accessed(filp);  
  20. goto out;  

上邊的代碼段主要是檢查了文件指針的值,文件的大小以及所請求讀取的字節(jié)數(shù)目等,之后,該函數(shù)調(diào)用 generic_file_direct_io(),并將操作類型 READ,描述符 iocb,描述符 iovec,當(dāng)前文件指針的值以及在描述符 io_vec  中指定的用戶地址空間緩沖區(qū)的個數(shù)等值作為參數(shù)傳給它。當(dāng) generic_file_direct_io() 函數(shù)執(zhí)行完成,函數(shù) __generic_file_aio_read()會繼續(xù)執(zhí)行去完成后續(xù)操作:更新文件指針,設(shè)置訪問文件 i 節(jié)點的時間戳;這些操作全部執(zhí)行完成以后,函數(shù)返回。 函數(shù) generic_file_direct_IO() 會用到五個參數(shù),各參數(shù)的含義如下所示:

·rw:操作類型,可以是 READ 或者 WRITE

·iocb:指針,指向 kiocb 描述符

·iov:指針,指向 iovec 描述符數(shù)組

·offset:file 結(jié)構(gòu)偏移量

·nr_segs:iov 數(shù)組中 iovec 的個數(shù)

函數(shù) generic_file_direct_IO() 代碼如下所示:

清單 6. 函數(shù) generic_file_direct_IO()

  1. static ssize_t  
  2. generic_file_direct_IO(int rw, struct kiocb *iocb, const struct iovec *iov,loff_t offset, unsigned long nr_segs)  
  3. {  
  4.          struct file *file = iocb->ki_filp;  
  5.          struct address_space *mapping = file->f_mapping;  
  6.          ssize_t retval;  
  7.          size_t write_len = 0;  
  8.          if (rw == WRITE) {  
  9.                 write_len = iov_length(iov, nr_segs);  
  10.                 if (mapping_mapped(mapping))  
  11.                            unmap_mapping_range(mapping, offset, write_len, 0);  
  12.          }  
  13.          retval = filemap_write_and_wait(mapping);  
  14.          if (retval == 0) {  
  15.                     retval = mapping->a_ops->direct_IO(rw, iocb, iov,offset, nr_segs);  
  16.                     if (rw == WRITE && mapping->nrpages) {  
  17.                             pgoff_t end = (offset + write_len - 1)  
  18.                                         >> PAGE_CACHE_SHIFT;  
  19.                             int err = invalidate_inode_pages2_range(mapping,  
  20.                                 offset >> PAGE_CACHE_SHIFT, end);  
  21.                             if (err)  
  22.                                 retval = err;  
  23.                        }  
  24.  
  25.           }  
  26.           return retval;  

函數(shù) generic_file_direct_IO() 對 WRITE 操作類型進(jìn)行了一些特殊處理,這在下邊介紹 write() 系統(tǒng)調(diào)用的時候再做說明。除此之外,它主要是調(diào)用了 direct_IO 方法去執(zhí)行直接 I/O 的讀或者寫操作。在進(jìn)行直接  I/O  讀操作之前,先將頁緩存中的相關(guān)臟數(shù)據(jù)刷回到磁盤上去,這樣做可以確保從磁盤上讀到的是***的數(shù)據(jù)。這里的 direct_IO 方法最終會對應(yīng)到 __blockdev_direct_IO() 函數(shù)上去。__blockdev_direct_IO() 函數(shù)的代碼如下所示:

清單 7. 函數(shù) __blockdev_direct_IO()

  1. ssize_t  
  2. __blockdev_direct_IO(int rw, struct kiocb *iocb, struct inode *inode,struct block_device *bdev, const struct iovec *iov, loff_t offset,unsigned long nr_segs, get_block_t get_block, dio_iodone_t end_io,int dio_lock_type)  
  3. {  
  4.            int seg;  
  5.            size_t size;  
  6.            unsigned long addr;  
  7.            unsigned blkbits = inode->i_blkbits;  
  8.            unsigned bdev_blkbits = 0;  
  9.            unsigned blocksize_mask = (1 << blkbits) - 1;  
  10.            ssize_t retval = -EINVAL;  
  11.            loff_t end = offset;  
  12.            struct dio *dio;  
  13.            int release_i_mutex = 0;  
  14.            int acquire_i_mutex = 0;  
  15.            if (rw & WRITE)  
  16.                   rw = WRITE_SYNC;  
  17.            if (bdev)  
  18.                   bdev_blkbits = blksize_bits(bdev_hardsect_size(bdev));  
  19.            if (offset & blocksize_mask) {  
  20.                       if (bdev)  
  21.                           blkbits = bdev_blkbits;  
  22.                           blocksize_mask = (1 << blkbits) - 1;  
  23.                       if (offset & blocksize_mask)  
  24.                                  goto out;  
  25.  
  26.             }  
  27.            for (seg = 0; seg < nr_segs; seg++) {  
  28.                     addr = (unsigned long)iov[seg].iov_base;  
  29.                     size = iov[seg].iov_len;  
  30.                     end += size;  
  31.                     if ((addr & blocksize_mask) || (size & blocksize_mask)) {  
  32.                                if (bdev)  
  33.                                   blkbits = bdev_blkbits;  
  34.                                   blocksize_mask = (1 << blkbits) - 1;  
  35.                                if ((addr & blocksize_mask) || (size & blocksize_mask))  
  36.                                   goto out;  
  37.  
  38.                                  }  
  39.  
  40.                      }  
  41.             dio = kmalloc(sizeof(*dio), GFP_KERNEL);  
  42.             retval = -ENOMEM;  
  43.             if (!dio)  
  44.             goto out;  
  45.             dio->lock_type = dio_lock_type;  
  46.             if (dio_lock_type != DIO_NO_LOCKING) {  
  47.                    if (rw == READ && end > offset) {  
  48.                           struct address_space *mapping;  
  49.                           mapping = iocb->ki_filp->f_mapping;  
  50.                           if (dio_lock_type != DIO_OWN_LOCKING) {  
  51.                                  mutex_lock(&inode->i_mutex);  
  52.                                  release_i_mutex = 1;  
  53.                   }  
  54.  
  55.                   retval = filemap_write_and_wait_range(mapping, offset,end - 1);  
  56.                   if (retval) {  
  57.                         kfree(dio);  
  58.                         goto out;  
  59.  
  60.                   }  
  61.  
  62.                   if (dio_lock_type == DIO_OWN_LOCKING){        
  63.                   mutex_unlock(&inode->i_mutex);  
  64.                   acquire_i_mutex = 1;  
  65.                   }  
  66.             }  
  67.                   if (dio_lock_type == DIO_LOCKING)  
  68.                          down_read_non_owner(&inode->i_alloc_sem);  
  69.                   }  
  70.                   dio->is_async = !is_sync_kiocb(iocb) && !((rw & WRITE) &&(end > i_size_read(inode)));  
  71.                   retval = direct_io_worker(rw, iocb, inode, iov, offset,nr_segs, blkbits, get_block, end_io, dio);  
  72.                   if (rw == READ && dio_lock_type == DIO_LOCKING)  
  73.                   release_i_mutex = 0;  
  74. out:  
  75.  
  76.             if (release_i_mutex)  
  77.                       mutex_unlock(&inode->i_mutex);  
  78.             else if (acquire_i_mutex)  
  79.                       mutex_lock(&inode->i_mutex);  
  80.             return retval;  

該函數(shù)將要讀或者要寫的數(shù)據(jù)進(jìn)行拆分,并檢查緩沖區(qū)對齊的情況。本文在前邊介紹 open() 函數(shù)的時候指出,使用直接 I/O 讀寫數(shù)據(jù)的時候必須要注意緩沖區(qū)對齊的問題,從上邊的代碼可以看出,緩沖區(qū)對齊的檢查是在 __blockdev_direct_IO() 函數(shù)里邊進(jìn)行的。用戶地址空間的緩沖區(qū)可以通過 iov 數(shù)組中的 iovec 描述符確定。直接 I/O 的讀操作或者寫操作都是同步進(jìn)行的,也就是說,函數(shù) __blockdev_direct_IO() 會一直等到所有的 I/O 操作都結(jié)束才會返回,因此,一旦應(yīng)用程序 read() 系統(tǒng)調(diào)用返回,應(yīng)用程序就可以訪問用戶地址空間中含有相應(yīng)數(shù)據(jù)的緩沖區(qū)。但是,這種方法在應(yīng)用程序讀操作完成之前不能關(guān)閉應(yīng)用程序,這將會導(dǎo)致關(guān)閉應(yīng)用程序緩慢。

接下來我們看一下 write() 系統(tǒng)調(diào)用中與直接 I/O 相關(guān)的處理實現(xiàn)過程。函數(shù) write() 的原型如下所示:

ssize_t write(int filedes, const void * buff, size_t nbytes) ;

操作系統(tǒng)中處理 write() 系統(tǒng)調(diào)用的入口函數(shù)是 sys_write()。其主要的調(diào)用函數(shù)關(guān)系如下所示:

清單 8. 主調(diào)用函數(shù)關(guān)系圖

  1. sys_write()  
  2.           |-----vfs_write()  
  3.                |----generic_file_write()  
  4.                     |----generic_file_aio_read()  
  5.                          |---- __generic_file_write_nolock()  
  6.                                 |-- __generic_file_aio_write_nolock  
  7.                                      |-- generic_file_direct_write()  
  8.                                          |-- generic_file_direct_IO()  
  9.  

函數(shù) sys_write() 幾乎與 sys_read() 執(zhí)行相同的步驟,它從進(jìn)程中獲取文件描述符以及文件當(dāng)前的操作位置后即調(diào)用 vfs_write() 函數(shù)去執(zhí)行具體的操作過程,而 vfs_write() 函數(shù)最終是調(diào)用了 file 結(jié)構(gòu)中的相關(guān)操作完成文件的寫操作,即調(diào)用了 generic_file_write() 函數(shù)。在函數(shù) generic_file_write() 中, 函數(shù) generic_file_write_nolock() 最終調(diào)用 generic_file_aio_write_nolock() 函數(shù)去檢查 O_DIRECT 的設(shè)置,并且調(diào)用  generic_file_direct_write() 函數(shù)去執(zhí)行直接 I/O 寫操作。

函數(shù) generic_file_aio_write_nolock() 中與直接 I/O 相關(guān)的代碼如下所示:

清單 9. 函數(shù) generic_file_aio_write_nolock() 中與直接 I/O 相關(guān)的代碼

  1. if (unlikely(file->f_flags & O_DIRECT)) {  
  2.             written = generic_file_direct_write(iocb, iov,&nr_segs, pos, ppos, count, ocount);  
  3.             if (written < 0 || written == count)  
  4.                        goto out;  
  5.                        pos += written;  
  6.                        count -= written;  

從上邊代碼可以看出, generic_file_aio_write_nolock() 調(diào)用了 generic_file_direct_write() 函數(shù)去執(zhí)行直接 I/O 操作;而在 generic_file_direct_write() 函數(shù)中,跟讀操作過程類似,它最終也是調(diào)用了 generic_file_direct_IO() 函數(shù)去執(zhí)行直接 I/O 寫操作。與直接 I/O 讀操作不同的是,這次需要將操作類型 WRITE 作為參數(shù)傳給函數(shù) generic_file_direct_IO()。

前邊介紹了 generic_file_direct_IO() 的主體 direct_IO 方法:__blockdev_direct_IO()。函數(shù) generic_file_direct_IO() 對 WRITE 操作類型進(jìn)行了一些額外的處理。當(dāng)操作類型是 WRITE 的時候,若發(fā)現(xiàn)該使用直接 I/O 的文件已經(jīng)與其他一個或者多個進(jìn)程存在關(guān)聯(lián)的內(nèi)存映射,那么就調(diào)用 unmap_mapping_range() 函數(shù)去取消建立在該文件上的所有的內(nèi)存映射,并將頁緩存中相關(guān)的所有 dirty 位被置位的臟頁面刷回到磁盤上去。對于直接  I/O  寫操作來說,這樣做可以保證寫到磁盤上的數(shù)據(jù)是***的,否則,即將用直接  I/O  方式寫入到磁盤上的數(shù)據(jù)很可能會因為頁緩存中已經(jīng)存在的臟數(shù)據(jù)而失效。在直接  I/O  寫操作完成之后,在頁緩存中相關(guān)的臟數(shù)據(jù)就都已經(jīng)失效了,磁盤與頁緩存中的數(shù)據(jù)內(nèi)容必須保持同步。#p#

如何在字符設(shè)備中執(zhí)行直接 I/O

在字符設(shè)備中執(zhí)行直接 I/O 可能是有害的,只有在確定了設(shè)置緩沖 I/O 的開銷非常巨大的時候才建議使用直接 I/O。在 Linux 2.6 的內(nèi)核中,實現(xiàn)直接 I/O 的關(guān)鍵是函數(shù) get_user_pages() 函數(shù)。其函數(shù)原型如下所示:

int get_user_pages(struct task_struct *tsk,struct mm_struct *mm,unsigned long start,int len,int write,int force,struct page **pages,struct vm_area_struct **vmas);

該函數(shù)的參數(shù)含義如下所示:

·tsk:指向執(zhí)行映射的進(jìn)程的指針;該參數(shù)的主要用途是用來告訴操作系統(tǒng)內(nèi)核,映射頁面所產(chǎn)生的頁錯誤由誰來負(fù)責(zé),該參數(shù)幾乎總是 current。

·mm:指向被映射的用戶地址空間的內(nèi)存管理結(jié)構(gòu)的指針,該參數(shù)通常是 current->mm 。

·start: 需要映射的用戶地址空間的地址。

·len:頁內(nèi)緩沖區(qū)的長度。

·write:如果需要對所映射的頁面有寫權(quán)限,該參數(shù)的設(shè)置得是非零。

·force:該參數(shù)的設(shè)置通知 get_user_pages() 函數(shù)無需考慮對指定內(nèi)存頁的保護(hù),直接提供所請求的讀或者寫訪問。

·page:輸出參數(shù)。調(diào)用成功后,該參數(shù)中包含一個描述用戶空間頁面的 page 結(jié)構(gòu)的指針列表。

·vmas:輸出參數(shù)。若該參數(shù)非空,則該參數(shù)包含一個指向 vm_area_struct 結(jié)構(gòu)的指針,該 vm_area_struct 結(jié)構(gòu)包含了每一個所映射的頁面。

在使用 get_user_pages() 函數(shù)的時候,往往還需要配合使用以下這些函數(shù):

  1. void down_read(struct rw_semaphore *sem);  
  2. void up_read(struct rw_semaphore *sem);  
  3. void SetPageDirty(struct page *page);  
  4. void page_cache_release(struct page *page); 

首先,在使用 get_user_pages() 函數(shù)之前,需要先調(diào)用 down_read() 函數(shù)將 mmap 為獲得用戶地址空間的讀取者 / 寫入者信號量設(shè)置為讀模式;在調(diào)用完 get_user_pages() 函數(shù)之后,再調(diào)用配對函數(shù) up_read() 釋放信號量 sem。若 get_user_pages() 調(diào)用失敗,則返回錯誤代碼;若調(diào)用成功,則返回實際被映射的頁面數(shù),該數(shù)目有可能比請求的數(shù)量少。調(diào)用成功后所映射的用戶頁面被鎖在內(nèi)存中,調(diào)用者可以通過 page 結(jié)構(gòu)的指針去訪問這些用戶頁面。

直接 I/O 的調(diào)用者必須進(jìn)行善后工作,一旦直接 I/O 操作完成,用戶內(nèi)存頁面必須從頁緩存中釋放。在用戶內(nèi)存頁被釋放之前,如果這些頁面中的內(nèi)容改變了,那么調(diào)用者必須要通知操作系統(tǒng)內(nèi)核,否則虛擬存儲子系統(tǒng)會認(rèn)為這些頁面是干凈的,從而導(dǎo)致這些數(shù)據(jù)被修改了的頁面在被釋放之前無法被寫回到***存儲中去。因此,如果改變了頁中的數(shù)據(jù),那么就必須使用 SetPageDirty() 函數(shù)標(biāo)記出每個被改變的頁。對于 Linux 2.6.18.1,該宏定義在 /include/linux/page_flags.h 中。執(zhí)行該操作的代碼一般需要先檢查頁,以確保該頁不在內(nèi)存映射的保留區(qū)域內(nèi),因為這個區(qū)的頁是不會被交換出去的,其代碼如下所示:

if (!PageReserved(page)) 
SetPageDirty(page);

但是,由于用戶空間所映射的頁面通常不會被標(biāo)記為保留,所以上述代碼中的檢查并不是嚴(yán)格要求的。

最終,在直接 I/O 操作完成之后,不管頁面是否被改變,它們都必須從頁緩存中釋放,否則那些頁面永遠(yuǎn)都會存在在那里。函數(shù) page_cache_release() 就是用于釋放這些頁的。頁面被釋放之后,調(diào)用者就不能再次訪問它們。

關(guān)于如何在字符設(shè)備驅(qū)動程序中加入對直接 I/O 的支持,Linux 2.6.18.1 源代碼中 /drivers/scsi/st.c 給出了一個完整的例子。其中,函數(shù) sgl_map_user_pages()和 sgl_map_user_pages()幾乎涵蓋了本節(jié)中介紹的所有內(nèi)容。

直接 I/O 技術(shù)的特點

直接 I/O 的優(yōu)點

直接 I/O 最主要的優(yōu)點就是通過減少操作系統(tǒng)內(nèi)核緩沖區(qū)和應(yīng)用程序地址空間的數(shù)據(jù)拷貝次數(shù),降低了對文件讀取和寫入時所帶來的 CPU 的使用以及內(nèi)存帶寬的占用。這對于某些特殊的應(yīng)用程序,比如自緩存應(yīng)用程序來說,不失為一種好的選擇。如果要傳輸?shù)臄?shù)據(jù)量很大,使用直接 I/O 的方式進(jìn)行數(shù)據(jù)傳輸,而不需要操作系統(tǒng)內(nèi)核地址空間拷貝數(shù)據(jù)操作的參與,這將會大大提高性能。

直接 I/O 潛在可能存在的問題

直接 I/O 并不一定總能提供令人滿意的性能上的飛躍。設(shè)置直接 I/O 的開銷非常大,而直接 I/O 又不能提供緩存 I/O 的優(yōu)勢。緩存 I/O 的讀操作可以從高速緩沖存儲器中獲取數(shù)據(jù),而直接 I/O 的讀數(shù)據(jù)操作會造成磁盤的同步讀,這會帶來性能上的差異 , 并且導(dǎo)致進(jìn)程需要較長的時間才能執(zhí)行完;對于寫數(shù)據(jù)操作來說,使用直接 I/O 需要 write() 系統(tǒng)調(diào)用同步執(zhí)行,否則應(yīng)用程序?qū)恢朗裁磿r候才能夠再次使用它的 I/O 緩沖區(qū)。與直接 I/O 讀操作類似的是,直接 I/O 寫操作也會導(dǎo)致應(yīng)用程序關(guān)閉緩慢。所以,應(yīng)用程序使用直接 I/O 進(jìn)行數(shù)據(jù)傳輸?shù)臅r候通常會和使用異步 I/O 結(jié)合使用。

總結(jié)

Linux 中的直接 I/O 訪問文件方式可以減少 CPU 的使用率以及內(nèi)存帶寬的占用,但是直接 I/O 有時候也會對性能產(chǎn)生負(fù)面影響。所以在使用直接 I/O 之前一定要對應(yīng)用程序有一個很清醒的認(rèn)識,只有在確定了設(shè)置緩沖 I/O 的開銷非常巨大的情況下,才考慮使用直接 I/O。直接 I/O 經(jīng)常需要跟異步 I/O 結(jié)合起來使用,本文對異步 I/O 沒有作詳細(xì)介紹,有興趣的讀者可以參看 Linux 2.6 中相關(guān)的文檔介紹。

原文:http://www.ibm.com/developerworks/cn/linux/l-cn-directio/index.html?ca=drs

【編輯推薦】

  1. Unix標(biāo)準(zhǔn)輸入/輸出 (I/O) 流知識講解
  2. Linux Kernel IOPERM系統(tǒng)調(diào)用I/O端口訪問漏洞
  3. 技巧:部署I/O虛擬化技術(shù)的方法和時機
責(zé)任編輯:黃丹 來源: IBMDW
相關(guān)推薦

2019-06-14 09:34:59

Linux 系統(tǒng) 數(shù)據(jù)

2021-02-22 17:06:58

Linux改動代碼

2017-01-19 19:14:20

Linux重定向命令

2020-06-03 17:30:42

LinuxIO

2020-11-09 14:30:28

Linux多線程數(shù)據(jù)

2022-04-23 16:30:22

Linux磁盤性能

2017-03-25 21:33:33

Linux調(diào)度器

2023-05-08 00:06:45

Go語言機制

2019-09-12 08:58:26

LinuxUNIX操作系統(tǒng)

2010-06-25 09:47:29

Linux系統(tǒng)監(jiān)控

2019-02-25 08:40:28

Linux磁盤IO

2017-01-19 19:24:29

Linux重定向

2017-03-01 12:36:15

Linux驅(qū)動技術(shù)內(nèi)存

2018-11-05 11:20:54

緩沖IO

2020-12-01 07:08:23

Linux網(wǎng)絡(luò)I

2014-07-28 16:47:41

linux性能

2022-07-28 11:09:44

Linux優(yōu)化IO

2020-06-10 08:28:51

Kata容器I

2011-03-29 16:14:04

Cacti硬盤監(jiān)控

2013-05-28 10:08:41

IO輸出
點贊
收藏

51CTO技術(shù)棧公眾號