直接 I/O 的動機linux
在介紹直接 I/O 以前,這一小節先介紹一下爲何會出現直接 I/O 這種機制,即傳統的 I/O 操做存在哪些缺點。算法
緩存 I/O 又被稱做標準 I/O,大多數文件系統的默認 I/O 操做都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操做系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。緩存 I/O 有如下這些優勢:數組
當應用程序嘗試讀取某塊數據的時候,若是這塊數據已經存放在了頁緩存中,那麼這塊數據就能夠當即返回給應用程序,而不須要通過實際的物理讀盤 操做。固然,若是數據在應用程序讀取以前並未被存放在頁緩存中,那麼就須要先將數據從磁盤讀到頁緩存中去。對於寫操做來講,應用程序也會將數據先寫到頁緩 存中去,數據是否被當即寫到磁盤上去取決於應用程序所採用的寫操做機制:若是用戶採用的是同步寫機制( synchronous writes ), 那麼數據會當即被寫回到磁盤上,應用程序會一直等到數據被寫完爲止;若是用戶採用的是延遲寫機制( deferred writes ),那麼應用程序就徹底不須要等到數據所有被寫回到磁盤,數據只要被寫到頁緩存中去就能夠了。在延遲寫機制的狀況下,操做系統會按期地將放在頁緩存中的數 據刷到磁盤上。與異步寫機制( asynchronous writes )不一樣的是,延遲寫機制在數據徹底寫到磁盤上的時候不會通知應用程序,而異步寫機制在數據徹底寫到磁盤上的時候是會返回給應用程序的。因此延遲寫機制自己 是存在數據丟失的風險的,而異步寫機制則不會有這方面的擔憂。緩存
在緩存 I/O 機制中,DMA 方式能夠將數據直接從磁盤讀到頁緩存中,或者將數據從頁緩存直接寫回到磁盤上,而不能直接在應用程序地址空間和磁盤之間進行數據傳輸,這樣的話,數據在傳 輸過程當中須要在應用程序地址空間和頁緩存之間進行屢次數據拷貝操做,這些數據拷貝操做所帶來的 CPU 以及內存開銷是很是大的。app
對於某些特殊的應用程序來講,避開操做系統內核緩衝區而直接在應用程序地址空間和磁盤之間傳輸數據會比使用操做系統內核緩衝區獲取更好的性能,下邊這一小節中提到的自緩存應用程序就是其中的一種。異步
自緩存應用程序( self-caching applications)
對於某些應用程序來講,它會有它本身的數據緩存機制,好比,它會將數據緩存在應用程序地址空間,這類應用程序徹底不須要使用操做系統內核中的 高速緩衝存儲器,這類應用程序就被稱做是自緩存應用程序( self-caching applications )。數據庫管理系統是這類應用程序的一個表明。自緩存應用程序傾向於使用數據的邏輯表達方式,而非物理表達方式;當系統內存較低的時候,自緩存應用程序會 讓這種數據的邏輯緩存被換出,而並不是是磁盤上實際的數據被換出。自緩存應用程序對要操做的數據的語義瞭如指掌,因此它能夠採用更加高效的緩存替換算法。自 緩存應用程序有可能會在多臺主機之間共享一塊內存,那麼自緩存應用程序就須要提供一種可以有效地將用戶地址空間的緩存數據置爲無效的機制,從而確保應用程 序地址空間緩存數據的一致性。
對於自緩存應用程序來講,緩存 I/O 明顯不是一個好的選擇。由此引出咱們這篇文章着重要介紹的 Linux 中的直接 I/O 技術。Linux 中的直接 I/O 技術很是適用於自緩存這類應用程序,該技術省略掉緩存 I/O 技術中操做系統內核緩衝區的使用,數據直接在應用程序地址空間和磁盤之間進行傳輸,從而使得自緩存應用程序能夠省略掉複雜的系統級別的緩存結構,而執行程 序本身定義的數據讀寫管理,從而下降系統級別的管理對應用程序訪問數據的影響。在下面一節中,咱們會着重介紹 Linux 中提供的直接 I/O 機制的設計與實現,該機制爲自緩存應用程序提供了很好的支持。
全部的 I/O 操做都是經過讀文件或者寫文件來完成的。在這裏,咱們把全部的外圍設備,包括鍵盤和顯示器,都當作是文件系統中的文件。訪問文件的方法多種多樣,這裏列出下邊這幾種 Linux 2.6 中支持的文件訪問方式。
在 Linux 中,這種訪問文件的方式是經過兩個系統調用實現的:read() 和 write()。當應用程序調用 read() 系統調用讀取一塊數據的時候,若是該塊數據已經在內存中了,那麼就直接從內存中讀出該數據並返回給應用程序;若是該塊數據不在內存中,那麼數據會被從磁盤 上讀到頁高緩存中去,而後再從頁緩存中拷貝到用戶地址空間中去。若是一個進程讀取某個文件,那麼其餘進程就都不能夠讀取或者更改該文件;對於寫數據操做來 說,當一個進程調用了 write() 系統調用往某個文件中寫數據的時候,數據會先從用戶地址空間拷貝到操做系統內核地址空間的頁緩存中去,而後才被寫到磁盤上。可是對於這種標準的訪問文件的 方式來講,在數據被寫到頁緩存中的時候,write() 系統調用就算執行完成,並不會等數據徹底寫入到磁盤上。Linux 在這裏採用的是咱們前邊提到的延遲寫機制( deferred writes )。
同步訪問文件的方式與上邊這種標準的訪問文件的方式比較相似,這兩種方法一個很關鍵的區別就是:同步訪問文件的時候,寫數據的操做是在數據徹底被寫回磁盤上纔算完成的;而標準訪問文件方式的寫數據操做是在數據被寫到頁高速緩衝存儲器中的時候就算執行完成了。
在不少操做系統包括 Linux 中,內存區域( memory region )是能夠跟一個普通的文件或者塊設備文件的某一個部分關聯起來的,若進程要訪問內存頁中某個字節的數據,操做系統就會將訪問該內存區域的操做轉換爲相應的 訪問文件的某個字節的操做。Linux 中提供了系統調用 mmap() 來實現這種文件訪問方式。與標準的訪問文件的方式相比,內存映射方式能夠減小標準訪問文件方式中 read() 系統調用所帶來的數據拷貝操做,即減小數據在用戶地址空間和操做系統內核地址空間之間的拷貝操做。映射一般適用於較大範圍,對於相同長度的數據來說,映射 所帶來的開銷遠遠低於 CPU 拷貝所帶來的開銷。當大量數據須要傳輸的時候,採用內存映射方式去訪問文件會得到比較好的效率。
凡是經過直接 I/O 方式進行數據傳輸,數據均直接在用戶地址空間的緩衝區和磁盤之間直接進行傳輸,徹底不須要頁緩存的支持。操做系統層提供的緩存每每會使應用程序在讀寫數據 的時候得到更好的性能,可是對於某些特殊的應用程序,好比說數據庫管理系統這類應用,他們更傾向於選擇他們本身的緩存機制,由於數據庫管理系統每每比操做 系統更瞭解數據庫中存放的數據,數據庫管理系統能夠提供一種更加有效的緩存機制來提升數據庫中數據的存取性能。
Linux 異步 I/O 是 Linux 2.6 中的一個標準特性,其本質思想就是進程發出數據傳輸請求以後,進程不會被阻塞,也不用等待任何操做完成,進程能夠在數據傳輸的時候繼續執行其餘的操做。相 對於同步訪問文件的方式來講,異步訪問文件的方式能夠提升應用程序的效率,而且提升系統資源利用率。直接 I/O 常常會和異步訪問文件的方式結合在一塊兒使用。
在下邊這一小節中,咱們會重點介紹 Linux 2.6 內核中直接 I/O 的設計與實現。
在塊設備或者網絡設備中執行直接 I/O 徹底不用擔憂實現直接 I/O 的問題,Linux 2.6 操做系統內核中高層代碼已經設置和使用了直接 I/O,驅動程序級別的代碼甚至不須要知道已經執行了直接 I/O;可是對於字符設備來講,執行直接 I/O 是不可行的,Linux 2.6 提供了函數 get_user_pages() 用於實現直接 I/O。本小節會分別對這兩種狀況進行介紹。
要在塊設備中執行直接 I/O,進程必須在打開文件的時候設置對文件的訪問模式爲 O_DIRECT,這樣就等於告訴操做系統進程在接下來使用 read() 或者 write() 系統調用去讀寫文件的時候使用的是直接 I/O 方式,所傳輸的數據均不通過操做系統內核緩存空間。使用直接 I/O 讀寫數據必需要注意緩衝區對齊( buffer alignment )以及緩衝區的大小的問題,即對應 read() 以及 write() 系統調用的第二個和第三個參數。這裏邊說的對齊指的是文件系統塊大小的對齊,緩衝區的大小也必須是該塊大小的整數倍。
這一節主要介紹三個函數:open(),read() 以及 write()。Linux 中訪問文件具備多樣性,因此這三個函數對於處理不一樣的文件訪問方式定義了不一樣的處理方法,本文主要介紹其與直接 I/O 方式相關的函數與功能.首先,先來看 open() 系統調用,其函數原型以下所示:
int open(const char *pathname, int oflag, … /*, mode_t mode * / ) ; |
如下列出了 Linux 2.6 內核定義的系統調用 open() 所使用的標識符宏定義:
標識符名 | 標識符描述 |
---|---|
O_RDONLY |
以只讀的方式打開文件 |
O_WRONLY |
以只寫的方式打開文件 |
O_RDWR |
以讀寫的方式打開文件 |
O_CREAT |
若文件不存在,則建立該文件 |
O_EXCL |
以獨佔模式打開文件;若同時設置 O_EXCL 和 O_CREATE, 那麼若文件已經存在,則打開操做會失敗 |
O_NOCTTY |
若設置該描述符,則該文件不能夠被當成終端處理 |
O_TRUNC |
截斷文件,若文件存在,則刪除該文件 |
O_APPEND |
若設置了該描述符,則在寫文件以前,文件指針會被設置到文件的底部 |
O_NONBLOCK |
以非阻塞的方式打開文件 |
O_NELAY |
同 O_NELAY,若同時設置 O_NELAY 和 O_NONBLOCK,O_NONBLOCK 優先起做用 |
O_SYNC |
該描述符會對普通文件的寫操做產生影響,若設置了該描述符,則對該文件的寫操做會等到數據被寫到磁盤上纔算結束 |
FASYNC |
若設置該描述符,則 I/O 事件通知是經過信號發出的 |
O_DIRECT |
該描述符提供對直接 I/O 的支持 |
O_LARGEFILE |
該描述符提供對超過 2GB 大文件的支持 |
O_DIRECTORY |
該描述符代表所打開的文件必須是目錄,不然打開操做失敗 |
O_NOFOLLOW |
若設置該描述符,則不解析路徑名尾部的符號連接 |
操做系統內核中處理 open() 系統調用的內核函數是 sys_open(),sys_open() 會調用 do_sys_open() 去處理主要的打開操做。它主要作了三件事情:首先, 它調用 getname() 從進程地址空間中讀取文件的路徑名;接着,do_sys_open() 調用 get_unused_fd() 從進程的文件表中找到一個空閒的文件表指針,相應的新文件描述符就存放在本地變量 fd 中;以後,函數 do_filp_open() 會根據傳入的參數去執行相應的打開操做。清單 1 列出了操做系統內核中處理 open() 系統調用的一個主要函數關係圖。
sys_open() |-----do_sys_open() |---------getname() |---------get_unused_fd() |---------do_filp_open() |--------nameidata_to_filp() |----------__dentry_open() |
函數 do_flip_open() 在執行的過程當中會調用函數 nameidata_to_filp(),而 nameidata_to_filp() 最終會調用 __dentry_open() 函數,若進程指定了 O_DIRECT 標識符,則該函數會檢查直接 I./O 操做是否能夠做用於該文件。清單 2 列出了 __dentry_open() 函數中與直接 I/O 操做相關的代碼。
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); } } |
當文件打開時指定了 O_DIRECT 標識符,那麼操做系統就會知道接下來對文件的讀或者寫操做都是要使用直接 I/O 方式的。
下邊咱們來看一下當進程經過 read() 系統調用讀取一個已經設置了 O_DIRECT 標識符的文件的時候,系統都作了哪些處理。 函數 read() 的原型以下所示:
ssize_t read(int feledes, void *buff, size_t nbytes) ; |
操做系統中處理 read() 函數的入口函數是 sys_read(),其主要的調用函數關係圖以下清單 3 所示:
sys_read() |-----vfs_read() |----generic_file_read() |----generic_file_aio_read() |--------- generic_file_direct_IO() |
函數 sys_read() 從進程中獲取文件描述符以及文件當前的操做位置後會調用 vfs_read() 函數去執行具體的操做過程,而 vfs_read() 函數最終是調用了 file 結構中的相關操做去完成文件的讀操做,即調用了 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; } |
函數 generic_file_read() 初始化了 iovec 以及 kiocb 描述符。描述符 iovec 主要是用於存放兩個內容:用來接收所讀取數據的用戶地址空間緩衝區的地址和緩衝區的大小;描述符 kiocb 用來跟蹤 I/O 操做的完成狀態。以後,函數 generic_file_read() 凋用函數 __generic_file_aio_read()。該函數檢查 iovec 中描述的用戶地址空間緩衝區是否可用,接着檢查訪問模式,若訪問模式描述符設置了 O_DIRECT,則執行與直接 I/O 相關的代碼。函數 __generic_file_aio_read() 中與直接 I/O 有關的代碼以下所示:
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; } |
上邊的代碼段主要是檢查了文件指針的值,文件的大小以及所請求讀取的字節數目等,以後,該函數調用 generic_file_direct_io(),並將操做類型 READ,描述符 iocb,描述符 iovec,當前文件指針的值以及在描述符 io_vec 中指定的用戶地址空間緩衝區的個數等值做爲參數傳給它。當 generic_file_direct_io() 函數執行完成,函數 __generic_file_aio_read()會繼續執行去完成後續操做:更新文件指針,設置訪問文件 i 節點的時間戳;這些操做所有執行完成之後,函數返回。 函數 generic_file_direct_IO() 會用到五個參數,各參數的含義以下所示:
函數 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; } |
函數 generic_file_direct_IO() 對 WRITE 操做類型進行了一些特殊處理,這在下邊介紹 write() 系統調用的時候再作說明。除此以外,它主要是調用了 direct_IO 方法去執行直接 I/O 的讀或者寫操做。在進行直接 I/O 讀操做以前,先將頁緩存中的相關髒數據刷回到磁盤上去,這樣作能夠確保從磁盤上讀到的是最新的數據。這裏的 direct_IO 方法最終會對應到 __blockdev_direct_IO() 函數上去。__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; } |
該函數將要讀或者要寫的數據進行拆分,並檢查緩衝區對齊的狀況。本文在前邊介紹 open() 函數的時候指出,使用直接 I/O 讀寫數據的時候必需要注意緩衝區對齊的問題,從上邊的代碼能夠看出,緩衝區對齊的檢查是在 __blockdev_direct_IO() 函數裏邊進行的。用戶地址空間的緩衝區能夠經過 iov 數組中的 iovec 描述符肯定。直接 I/O 的讀操做或者寫操做都是同步進行的,也就是說,函數 __blockdev_direct_IO() 會一直等到全部的 I/O 操做都結束纔會返回,所以,一旦應用程序 read() 系統調用返回,應用程序就能夠訪問用戶地址空間中含有相應數據的緩衝區。可是,這種方法在應用程序讀操做完成以前不能關閉應用程序,這將會致使關閉應用程 序緩慢。
接下來咱們看一下 write() 系統調用中與直接 I/O 相關的處理實現過程。函數 write() 的原型以下所示: ssize_t write(int filedes, const void * buff, size_t nbytes) ; 操做系統中處理 write() 系統調用的入口函數是 sys_write()。其主要的調用函數關係以下所示: |
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() |
函數 sys_write() 幾乎與 sys_read() 執行相同的步驟,它從進程中獲取文件描述符以及文件當前的操做位置後即調用 vfs_write() 函數去執行具體的操做過程,而 vfs_write() 函數最終是調用了 file 結構中的相關操做完成文件的寫操做,即調用了 generic_file_write() 函數。在函數 generic_file_write() 中, 函數 generic_file_write_nolock() 最終調用 generic_file_aio_write_nolock() 函數去檢查 O_DIRECT 的設置,而且調用 generic_file_direct_write() 函數去執行直接 I/O 寫操做。
函數 generic_file_aio_write_nolock() 中與直接 I/O 相關的代碼以下所示: |
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() 調用了 generic_file_direct_write() 函數去執行直接 I/O 操做;而在 generic_file_direct_write() 函數中,跟讀操做過程相似,它最終也是調用了 generic_file_direct_IO() 函數去執行直接 I/O 寫操做。與直接 I/O 讀操做不一樣的是,此次須要將操做類型 WRITE 做爲參數傳給函數 generic_file_direct_IO()。
前邊介紹了 generic_file_direct_IO() 的主體 direct_IO 方法:__blockdev_direct_IO()。函數 generic_file_direct_IO() 對 WRITE 操做類型進行了一些額外的處理。當操做類型是 WRITE 的時候,若發現該使用直接 I/O 的文件已經與其餘一個或者多個進程存在關聯的內存映射,那麼就調用 unmap_mapping_range() 函數去取消創建在該文件上的全部的內存映射,並將頁緩存中相關的全部 dirty 位被置位的髒頁面刷回到磁盤上去。對於直接 I/O 寫操做來講,這樣作能夠保證寫到磁盤上的數據是最新的,不然,即將用直接 I/O 方式寫入到磁盤上的數據極可能會由於頁緩存中已經存在的髒數據而失效。在直接 I/O 寫操做完成以後,在頁緩存中相關的髒數據就都已經失效了,磁盤與頁緩存中的數據內容必須保持同步。
在字符設備中執行直接 I/O 多是有害的,只有在肯定了設置緩衝 I/O 的開銷很是巨大的時候才建議使用直接 I/O。在 Linux 2.6 的內核中,實現直接 I/O 的關鍵是函數 get_user_pages() 函數。其函數原型以下所示:
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); |
該函數的參數含義以下所示:
在使用 get_user_pages() 函數的時候,每每還須要配合使用如下這些函數:
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() 函數以前,須要先調用 down_read() 函數將 mmap 爲得到用戶地址空間的讀取者 / 寫入者信號量設置爲讀模式;在調用完 get_user_pages() 函數以後,再調用配對函數 up_read() 釋放信號量 sem。若 get_user_pages() 調用失敗,則返回錯誤代碼;若調用成功,則返回實際被映射的頁面數,該數目有可能比請求的數量少。調用成功後所映射的用戶頁面被鎖在內存中,調用者能夠通 過 page 結構的指針去訪問這些用戶頁面。
直接 I/O 的調用者必須進行善後工做,一旦直接 I/O 操做完成,用戶內存頁面必須從頁緩存中釋放。在用戶內存頁被釋放以前,若是這些頁面中的內容改變了,那麼調用者必需要通知操做系統內核,不然虛擬存儲子系 統會認爲這些頁面是乾淨的,從而致使這些數據被修改了的頁面在被釋放以前沒法被寫回到永久存儲中去。所以,若是改變了頁中的數據,那麼就必須使用 SetPageDirty() 函數標記出每一個被改變的頁。對於 Linux 2.6.18.1,該宏定義在 /include/linux/page_flags.h 中。執行該操做的代碼通常須要先檢查頁,以確保該頁不在內存映射的保留區域內,由於這個區的頁是不會被交換出去的,其代碼以下所示:
if (!PageReserved(page)) SetPageDirty(page); |
可是,因爲用戶空間所映射的頁面一般不會被標記爲保留,因此上述代碼中的檢查並非嚴格要求的。
最終,在直接 I/O 操做完成以後,無論頁面是否被改變,它們都必須從頁緩存中釋放,不然那些頁面永遠都會存在在那裏。函數 page_cache_release() 就是用於釋放這些頁的。頁面被釋放以後,調用者就不能再次訪問它們。
關於如何在字符設備驅動程序中加入對直接 I/O 的支持,Linux 2.6.18.1 源代碼中 /drivers/scsi/st.c 給出了一個完整的例子。其中,函數 sgl_map_user_pages()和 sgl_map_user_pages()幾乎涵蓋了本節中介紹的全部內容。
直接 I/O 最主要的優勢就是經過減小操做系統內核緩衝區和應用程序地址空間的數據拷貝次數,下降了對文件讀取和寫入時所帶來的 CPU 的使用以及內存帶寬的佔用。這對於某些特殊的應用程序,好比自緩存應用程序來講,不失爲一種好的選擇。若是要傳輸的數據量很大,使用直接 I/O 的方式進行數據傳輸,而不須要操做系統內核地址空間拷貝數據操做的參與,這將會大大提升性能。
直接 I/O 並不必定總能提供使人滿意的性能上的飛躍。設置直接 I/O 的開銷很是大,而直接 I/O 又不能提供緩存 I/O 的優點。緩存 I/O 的讀操做能夠從高速緩衝存儲器中獲取數據,而直接 I/O 的讀數據操做會形成磁盤的同步讀,這會帶來性能上的差別 , 而且致使進程須要較長的時間才能執行完;對於寫數據操做來講,使用直接 I/O 須要 write() 系統調用同步執行,不然應用程序將會不知道何時纔可以再次使用它的 I/O 緩衝區。與直接 I/O 讀操做相似的是,直接 I/O 寫操做也會致使應用程序關閉緩慢。因此,應用程序使用直接 I/O 進行數據傳輸的時候一般會和使用異步 I/O 結合使用。
Linux 中的直接 I/O 訪問文件方式能夠減小 CPU 的使用率以及內存帶寬的佔用,可是直接 I/O 有時候也會對性能產生負面影響。因此在使用直接 I/O 以前必定要對應用程序有一個很清醒的認識,只有在肯定了設置緩衝 I/O 的開銷很是巨大的狀況下,才考慮使用直接 I/O。直接 I/O 常常須要跟異步 I/O 結合起來使用,本文對異步 I/O 沒有做詳細介紹,有興趣的讀者能夠參看 Linux 2.6 中相關的文檔介紹。
學習
討論
歡迎加入 My developerWorks 中文社區