5分鐘搞懂Linux中直接I/O原理
在介紹直接 I/O 之前,先來介紹下直接I/O這種機制產(chǎn)生的原因。畢竟已經(jīng)有了緩存I/O(Buffered I/O),那肯定能夠像到緩存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)用程序的地址空間。寫的過程就是數(shù)據(jù)流反方向。緩存 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)在頁緩存中,那就返回之。而不需要經(jīng)過硬盤的讀取操作了。如果這塊數(shù)據(jù)不在頁緩存中,就需要從硬盤中讀取數(shù)據(jù)到頁緩存。
對于寫操作:應(yīng)用程序會將數(shù)據(jù)先寫到頁緩存中,數(shù)據(jù)是否會被立即寫到磁盤,這取決于所采用的寫操作機制:
- 同步機制,數(shù)據(jù)會立即被寫到磁盤中,直到數(shù)據(jù)寫完,寫接口才返回;
- 延遲機制:寫接口立即返回,操作系統(tǒng)會定期地將頁緩存中的數(shù)據(jù)刷到硬盤。所以這個機制會存在丟失數(shù)據(jù)的風(fēng)險。想象下寫接口返回的時候,頁緩存的數(shù)據(jù)還沒刷到硬盤,正好斷電。對于應(yīng)用程序來說,認(rèn)為數(shù)據(jù)已經(jīng)在硬盤中。
緩存I/O的寫操作
緩存 I/O 的缺點
在緩存I/O的機制中,以寫操作為例,數(shù)據(jù)先從用戶態(tài)拷貝到內(nèi)核態(tài)中的頁緩存中,然后又會從頁緩存中寫到磁盤中,這些拷貝操作帶來的CPU以及內(nèi)存的開銷是非常大的。
對于某些特殊的應(yīng)用程序來說,能夠繞開內(nèi)核緩沖區(qū)能夠獲取更好的性能,這就是直接I/O出現(xiàn)的意義。
直接I/O寫操作
直接I/O 介紹
凡是通過直接I/O方式進行數(shù)據(jù)傳輸,數(shù)據(jù)直接從用戶態(tài)地址空間寫入到磁盤中,直接跳過內(nèi)核緩沖區(qū)。對于一些應(yīng)用程序,例如:數(shù)據(jù)庫。他們更傾向于自己的緩存機制,這樣可以提供更好的緩沖機制提高數(shù)據(jù)庫的讀寫性能。直接I/O寫操作如上圖所示。
直接I/O 設(shè)計與實現(xiàn)
要在塊設(shè)備中執(zhí)行直接 I/O,進程必須在打開文件的時候設(shè)置對文件的訪問模式為 O_DIRECT,這樣就等于告訴操作系統(tǒng)進程在接下來使用 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ù)倍。
下面主要介紹三個函數(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 * / ) ;
當(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() 從進程地址空間中讀取文件的路徑名;
- do_sys_open() 調(diào)用 get_unused_fd() 從進程的文件表中找到一個空閑的文件表指針,相應(yīng)的新文件描述符就存放在本地變量 fd 中;
- 函數(shù) do_filp_open() 會根據(jù)傳入的參數(shù)去執(zhí)行相應(yīng)的打開操作。
下面列出了操作系統(tǒng)內(nèi)核中處理 open() 系統(tǒng)調(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ù),若進程指定了 O_DIRECT 標(biāo)識符,則該函數(shù)會檢查直接 I./O 操作是否可以作用于該文件。下面列出了 __dentry_open() 函數(shù)中與直接 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)進程通過 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)系圖如下:
- sys_read()
- |-----vfs_read()
- |----generic_file_read()
- |----generic_file_aio_read()
- |--------- generic_file_direct_IO()
-
函數(shù) sys_read() 從進程中獲取文件描述符以及文件當(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ù),其代碼如下所示:
- 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)的代碼如下所示:
- 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() 代碼如下所示:
- 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 操作類型進行了一些特殊處理。除此之外,它主要是調(diào)用了 direct_IO 方法去執(zhí)行直接 I/O 的讀或者寫操作。在進行直接 I/O 讀操作之前,先將頁緩存中的相關(guān)臟數(shù)據(jù)刷回到磁盤上去,這樣做可以確保從磁盤上讀到的是***的數(shù)據(jù)。這里的 direct_IO 方法最終會對應(yīng)到 __blockdev_direct_IO() 函數(shù)上去。__blockdev_direct_IO() 函數(shù)的代碼如下所示:
- 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ù)進行拆分,并檢查緩沖區(qū)對齊的情況。本文在前邊介紹 open() 函數(shù)的時候指出,使用直接 I/O 讀寫數(shù)據(jù)的時候必須要注意緩沖區(qū)對齊的問題,從上邊的代碼可以看出,緩沖區(qū)對齊的檢查是在 __blockdev_direct_IO() 函數(shù)里邊進行的。用戶地址空間的緩沖區(qū)可以通過 iov 數(shù)組中的 iovec 描述符確定。直接 I/O 的讀操作或者寫操作都是同步進行的,也就是說,函數(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)用程序緩慢。
直接I/O 優(yōu)點
***的優(yōu)點就是減少操作系統(tǒng)緩沖區(qū)和用戶地址空間的拷貝次數(shù)。降低了CPU的開銷,和內(nèi)存帶寬。對于某些應(yīng)用程序來說簡直是福音,將會大大提高性能。
直接I/O 缺點
直接IO并不總能讓人如意。直接IO的開銷也很大,應(yīng)用程序沒有控制好讀寫,將會導(dǎo)致磁盤讀寫的效率低下。磁盤的讀寫是通過磁頭的切換到不同的磁道上讀取和寫入數(shù)據(jù),如果需要寫入數(shù)據(jù)在磁盤位置相隔比較遠,就會導(dǎo)致尋道的時間大大增加,寫入讀取的效率大大降低。
總結(jié)
直接IO方式確實能夠減少CPU的使用率以及內(nèi)存帶寬的占用,但是有時候也會造成性能的影響。所以在使用直接IO之前一定要清楚它的原理,只有在各項都清晰的情況下,才考慮使用。本人只是介紹了原理,如想深入,建議參考內(nèi)核相關(guān)文檔。