Linux的直接I/O機制
對于傳統(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 )。
圖 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í)行完成了。
圖 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)存映射方式去訪問文件會獲得比較好的效率。
圖 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ù)的存取性能。
圖 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é)合在一起使用。
圖 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ù)原型如下所示:
- int open(const char *pathname, int oflag, … /*, mode_t mode * / ) ;
- |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
- |-------- 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)系圖
- sys_open()
- |-----do_sys_open()
- |---------getname()
- |---------get_unused_fd()
- |---------do_filp_open()
- |--------nameidata_to_filp()
- |----------__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)的代碼
- if (f->f_flags & O_DIRECT) {
- if (!f->f_mapping->a_ops ||
- ((!f->f_mapping->a_ops->direct_IO) &&
- (!f->f_mapping->a_ops->get_xip_page))) {
- fput(f);
- f = ERR_PTR(-EINVAL);
- }
- }
當(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)系圖
- sys_read()
- |-----vfs_read()
- |----generic_file_read()
- |----generic_file_aio_read()
- |--------- generic_file_direct_IO()
函數(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()
- ssize_t
- generic_file_read(struct file *filp,
- char __user *buf, size_t count, loff_t *ppos)
- {
- struct iovec local_iov = { .iov_base = buf, .iov_len = count };
- struct kiocb kiocb;
- ssize_t ret;
- init_sync_kiocb(&kiocb, filp);
- ret = __generic_file_aio_read(&kiocb, &local_iov, 1, ppos);
- if (-EIOCBQUEUED == ret)
- ret = wait_on_sync_kiocb(&kiocb);
- 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)的代碼
- if (filp->f_flags & O_DIRECT) {
- loff_t pos = *ppos, size;
- struct address_space *mapping;
- struct inode *inode;
- mapping = filp->f_mapping;
- inode = mapping->host;
- retval = 0;
- if (!count)
- goto out;
- size = i_size_read(inode);
- if (pos < size) {
- retval = generic_file_direct_IO(READ, iocb,
- iov, pos, nr_segs);
- if (retval > 0 && !is_sync_kiocb(iocb))
- retval = -EIOCBQUEUED;
- if (retval > 0)
- *ppos = pos + retval;
- }
- file_accessed(filp);
- 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()
- static ssize_t
- generic_file_direct_IO(int rw, struct kiocb *iocb, const struct iovec *iov,loff_t offset, unsigned long nr_segs)
- {
- struct file *file = iocb->ki_filp;
- struct address_space *mapping = file->f_mapping;
- ssize_t retval;
- size_t write_len = 0;
- if (rw == WRITE) {
- write_len = iov_length(iov, nr_segs);
- if (mapping_mapped(mapping))
- unmap_mapping_range(mapping, offset, write_len, 0);
- }
- retval = filemap_write_and_wait(mapping);
- if (retval == 0) {
- retval = mapping->a_ops->direct_IO(rw, iocb, iov,offset, nr_segs);
- if (rw == WRITE && mapping->nrpages) {
- pgoff_t end = (offset + write_len - 1)
- >> PAGE_CACHE_SHIFT;
- int err = invalidate_inode_pages2_range(mapping,
- offset >> PAGE_CACHE_SHIFT, end);
- if (err)
- retval = err;
- }
- }
- 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()
- ssize_t
- __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)
- {
- int seg;
- size_t size;
- unsigned long addr;
- unsigned blkbits = inode->i_blkbits;
- unsigned bdev_blkbits = 0;
- unsigned blocksize_mask = (1 << blkbits) - 1;
- ssize_t retval = -EINVAL;
- loff_t end = offset;
- struct dio *dio;
- int release_i_mutex = 0;
- int acquire_i_mutex = 0;
- if (rw & WRITE)
- rw = WRITE_SYNC;
- if (bdev)
- bdev_blkbits = blksize_bits(bdev_hardsect_size(bdev));
- if (offset & blocksize_mask) {
- if (bdev)
- blkbits = bdev_blkbits;
- blocksize_mask = (1 << blkbits) - 1;
- if (offset & blocksize_mask)
- goto out;
- }
- for (seg = 0; seg < nr_segs; seg++) {
- addr = (unsigned long)iov[seg].iov_base;
- size = iov[seg].iov_len;
- end += size;
- if ((addr & blocksize_mask) || (size & blocksize_mask)) {
- if (bdev)
- blkbits = bdev_blkbits;
- blocksize_mask = (1 << blkbits) - 1;
- if ((addr & blocksize_mask) || (size & blocksize_mask))
- goto out;
- }
- }
- dio = kmalloc(sizeof(*dio), GFP_KERNEL);
- retval = -ENOMEM;
- if (!dio)
- goto out;
- dio->lock_type = dio_lock_type;
- if (dio_lock_type != DIO_NO_LOCKING) {
- if (rw == READ && end > offset) {
- struct address_space *mapping;
- mapping = iocb->ki_filp->f_mapping;
- if (dio_lock_type != DIO_OWN_LOCKING) {
- mutex_lock(&inode->i_mutex);
- release_i_mutex = 1;
- }
- retval = filemap_write_and_wait_range(mapping, offset,end - 1);
- if (retval) {
- kfree(dio);
- goto out;
- }
- if (dio_lock_type == DIO_OWN_LOCKING){
- mutex_unlock(&inode->i_mutex);
- acquire_i_mutex = 1;
- }
- }
- if (dio_lock_type == DIO_LOCKING)
- down_read_non_owner(&inode->i_alloc_sem);
- }
- dio->is_async = !is_sync_kiocb(iocb) && !((rw & WRITE) &&(end > i_size_read(inode)));
- retval = direct_io_worker(rw, iocb, inode, iov, offset,nr_segs, blkbits, get_block, end_io, dio);
- if (rw == READ && dio_lock_type == DIO_LOCKING)
- release_i_mutex = 0;
- out:
- if (release_i_mutex)
- mutex_unlock(&inode->i_mutex);
- else if (acquire_i_mutex)
- mutex_lock(&inode->i_mutex);
- 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)系圖
- sys_write()
- |-----vfs_write()
- |----generic_file_write()
- |----generic_file_aio_read()
- |---- __generic_file_write_nolock()
- |-- __generic_file_aio_write_nolock
- |-- generic_file_direct_write()
- |-- generic_file_direct_IO()
函數(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)的代碼
- if (unlikely(file->f_flags & O_DIRECT)) {
- written = generic_file_direct_write(iocb, iov,&nr_segs, pos, ppos, count, ocount);
- if (written < 0 || written == count)
- goto out;
- pos += written;
- 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ù):
- void down_read(struct rw_semaphore *sem);
- void up_read(struct rw_semaphore *sem);
- void SetPageDirty(struct page *page);
- 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
【編輯推薦】