Linux 直接I/O 原理與實(shí)現(xiàn)
緩存I/O
一般來說,當(dāng)調(diào)用?open()?系統(tǒng)調(diào)用打開文件時(shí),如果不指定?O_DIRECT?標(biāo)志,那么就是使用緩存I/O來對文件進(jìn)行讀寫操作。我們先來看看?open()?系統(tǒng)調(diào)用的定義:
int open(const char *pathname, int flags, ... /*, mode_t mode */ );下面說明一下各個(gè)參數(shù)的作用:
pathname:指定要打開的文件路徑。flags:指定打開文件的標(biāo)志。mode:可選,指定打開文件的權(quán)限。
其中?flags?參數(shù)可選值如下表:
| 標(biāo)志 | 說明 |
|---|---|
| O_RDONLY | 以只讀的方式打開文件 |
| O_WRONLY | 以只寫的方式打開文件 |
| O_RDWR | 以讀寫的方式打開文件 |
| O_CREAT | 若文件不存在,則創(chuàng)建該文件 |
| O_EXCL | 以獨(dú)占模式打開文件;若同時(shí)設(shè)置 O_EXCL 和 O_CREATE, 那么若文件已經(jīng)存在,則打開操作會(huì)失敗 |
| O_NOCTTY | 若設(shè)置該描述符,則該文件不可以被當(dāng)成終端處理 |
| O_TRUNC | 截?cái)辔募?,若文件存在,則刪除該文件 |
| O_APPEND | 若設(shè)置了該描述符,則在寫文件之前,文件指針會(huì)被設(shè)置到文件的底部 |
| O_NONBLOCK | 以非阻塞的方式打開文件 |
| O_NELAY | 同 O_NELAY,若同時(shí)設(shè)置 O_NELAY 和 O_NONBLOCK,O_NONBLOCK 優(yōu)先起作用 |
| FASYNC | 若設(shè)置該描述符,則 I/O 事件通知是通過信號發(fā)出的 |
| O_SYNC | 該描述符會(huì)對普通文件的寫操作產(chǎn)生影響,若設(shè)置了該描述符,則對該文件的寫操作會(huì)等到數(shù)據(jù)被寫到磁盤上才算結(jié)束 |
| O_DIRECT | 該描述符提供對直接 I/O 的支持 |
| O_LARGEFILE | 該描述符提供對超過 2GB 大文件的支持 |
| O_DIRECTORY | 該描述符表明所打開的文件必須是目錄,否則打開操作失敗 |
| O_NOFOLLOW | 若設(shè)置該描述符,則不解析路徑名尾部的符號鏈接 |
flags?參數(shù)用于指定打開文件的標(biāo)志,比如指定?O_RDONLY,那么就只能以只讀方式對文件進(jìn)行讀寫。這些標(biāo)志都能通過?位或 (|)?操作來設(shè)置多個(gè)標(biāo)志如:
open("/path/to/file", O_RDONLY|O_APPEND|O_DIRECT);但?O_RDONLY、O_WRONLY?和?O_RDWR?這三個(gè)標(biāo)志是互斥的,也就是說這三個(gè)標(biāo)志不能同時(shí)設(shè)置,只能設(shè)置其中一個(gè)。
當(dāng)打開文件不指定?O_DIRECT?標(biāo)志時(shí),那么就默認(rèn)使用?緩存I/O?方式打開。我們可以通過下圖來了解?緩存I/O?處于文件系統(tǒng)的什么位置:

上圖中紅色框部分就是?緩存I/O?所在位置,位于?虛擬文件系統(tǒng)?與?真實(shí)文件系統(tǒng)?中間。
也就是說,當(dāng)虛擬文件系統(tǒng)讀文件時(shí),首先從緩存中查找要讀取的文件內(nèi)容是否存在緩存中,如果存在就直接從緩存中讀取。對文件進(jìn)行寫操作時(shí)也一樣,首先寫入到緩存中,然后由操作系統(tǒng)同步到塊設(shè)備(如磁盤)中。
緩存I/O 的優(yōu)缺點(diǎn)
緩存I/O?的引入是為了減少對塊設(shè)備的 I/O 操作,但是由于讀寫操作都先要經(jīng)過緩存,然后再從緩存復(fù)制到用戶空間,所以多了一次內(nèi)存復(fù)制操作。如下圖所示:

所以?緩存I/O?的優(yōu)點(diǎn)是減少對塊設(shè)備的 I/O 操作,而缺點(diǎn)就是需要多一次的內(nèi)存復(fù)制。另外,有些應(yīng)用程序需要自己管理 I/O 緩存的(如數(shù)據(jù)庫系統(tǒng)),那么就需要使用?直接I/O?了。
直接I/O
直接I/O?就是對用戶進(jìn)行的 I/O 操作直接與塊設(shè)備進(jìn)行交互,而不進(jìn)行緩存。
直接I/O?的優(yōu)點(diǎn)是:由于不對 I/O 數(shù)據(jù)塊進(jìn)行緩存,所以可以直接跟用戶數(shù)據(jù)進(jìn)行交互,減少一次內(nèi)存的拷貝。直接I/O?的缺點(diǎn)是:每次 I/O 操作都直接與塊設(shè)備進(jìn)行交互,增加了對塊設(shè)備的讀寫操作。
但由于應(yīng)用程序可以自行對數(shù)據(jù)塊進(jìn)行緩存,所以更加靈活,適合一些對 I/O 操作比較敏感的應(yīng)用,如數(shù)據(jù)庫系統(tǒng)。
直接I/O 實(shí)現(xiàn)
當(dāng)調(diào)用?open()?系統(tǒng)調(diào)用時(shí),在?flags?參數(shù)指定?O_DIRECT?標(biāo)志即可使用?直接I/O。我們從?虛擬文件系統(tǒng)?開始跟蹤 Linux 對?直接I/O?的處理過程。
當(dāng)調(diào)用?open()?系統(tǒng)調(diào)用時(shí),會(huì)觸發(fā)調(diào)用?sys_open()?系統(tǒng)調(diào)用,我們先來看看?sys_open()?函數(shù)的實(shí)現(xiàn):
asmlinkage long sys_open(const char *filename, int flags, int mode){char *tmp;int fd, error;...tmp = getname(filename); // 把文件名從用戶空間拷貝到內(nèi)核空間fd = PTR_ERR(tmp);if (!IS_ERR(tmp)) {fd = get_unused_fd(); // 申請一個(gè)還沒有使用的文件描述符if (fd >= 0) {// 根據(jù)文件路徑打開文件, 并獲取文件對象struct file *f = filp_open(tmp, flags, mode);error = PTR_ERR(f);if (IS_ERR(f))goto out_error;fd_install(fd, f); // 把文件對象與文件描述符關(guān)聯(lián)起來}out:putname(tmp);}return fd;...}
打開文件的整個(gè)流程比較復(fù)雜,但對我們分析?直接I/O?并沒有太大關(guān)系,之前在虛擬文件系統(tǒng)一章已經(jīng)分析過,這里就不再重復(fù)了,可以參考之前的文章:虛擬文件系統(tǒng)
我們主要關(guān)注的是,sys_open()?函數(shù)最后會(huì)調(diào)用?dentry_open()?把?flags?參數(shù)保存到文件對象的?f_flags?字段中,調(diào)用鏈:sys_open() -> filp_open() -> dentry_open():
struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags){struct file *f;...f = get_empty_filp();f->f_flags = flags;...}
也就是說,sys_open()?函數(shù)會(huì)打開文件,然后把?flags?參數(shù)保存到文件對象的?f_flgas?字段中。接下來,我們分析一下讀文件操作時(shí),是怎么對?直接I/O?進(jìn)行處理的。讀文件操作使用?read()?系統(tǒng)調(diào)用,而?read()?最終會(huì)調(diào)用內(nèi)核的?sys_read()?函數(shù),代碼如下:
asmlinkage ssize_t sys_read(unsigned int fd, char *buf, size_t count){ssize_t ret;struct file *file;file = fget(fd);if (file) {...if (!ret) {ssize_t (*read)(struct file *, char *, size_t, loff_t *);ret = -EINVAL;// ext2文件系統(tǒng)對應(yīng)的是: generic_file_read() 函數(shù)if (file->f_op && (read = file->f_op->read) != NULL)ret = read(file, buf, count, &file->f_pos);}...}return ret;}
由于?sys_read()?函數(shù)屬于虛擬文件系統(tǒng)范疇,所以其最終會(huì)調(diào)用真實(shí)文件系統(tǒng)的?file->f_op->read()?函數(shù),ext2文件系統(tǒng)?對應(yīng)的是?generic_file_read()?函數(shù),我們來分析下?generic_file_read()?函數(shù):
ssize_t generic_file_read(struct file *filp, char * buf, size_t count, loff_t *ppos){ssize_t retval;...if (filp->f_flags & O_DIRECT) // 如果標(biāo)記了使用直接IOgoto o_direct;...o_direct:{loff_t pos = *ppos, size;struct address_space *mapping = filp->f_dentry->d_inode->i_mapping;struct inode *inode = mapping->host;...size = inode->i_size;if (pos < size) {if (pos + count > size)count = size - pos;retval = generic_file_direct_IO(READ, filp, buf, count, pos);if (retval > 0)*ppos = pos + retval;}UPDATE_ATIME(filp->f_dentry->d_inode);goto out;}}
從上面代碼可以看出,如果在調(diào)用?open()?時(shí)指定了?O_DIRECT?標(biāo)志,那么?generic_file_read()?函數(shù)就會(huì)調(diào)用?generic_file_direct_IO()?函數(shù)對 I/O 操作進(jìn)行處理。由于?generic_file_direct_IO()?函數(shù)的實(shí)現(xiàn)曲折迂回,所以下面主要分析重要部分:
static ssize_t generic_file_direct_IO(int rw, struct file *filp, char *buf, size_t count, loff_t offset){...while (count > 0) {iosize = count;if (iosize > chunk_size)iosize = chunk_size;// 為用戶虛擬內(nèi)存空間申請物理內(nèi)存頁retval = map_user_kiobuf(rw, iobuf, (unsigned long)buf, iosize);if (retval)break;// ext2 文件系統(tǒng)對應(yīng) ext2_direct_IO() 函數(shù),// 而 ext2_direct_IO() 函數(shù)直接調(diào)用了 generic_direct_IO() 函數(shù)retval = mapping->a_ops->direct_IO(rw, inode, iobuf, (offset+progress) >> blocksize_bits, blocksize);...}...}
generic_file_direct_IO()?函數(shù)主要的處理有兩部分:
調(diào)用?
map_user_kiobuf()?函數(shù)為用戶虛擬內(nèi)存空間申請物理內(nèi)存頁。調(diào)用真實(shí)文件系統(tǒng)的?
direct_IO()?接口對?直接I/O?進(jìn)行處理。
map_user_kiobuf()?函數(shù)屬于內(nèi)存管理部分,可以參考之前的?內(nèi)存管理?相關(guān)的文章進(jìn)行分析,這里就不重復(fù)了。
generic_file_direct_IO()?函數(shù)最終會(huì)調(diào)用真實(shí)文件系統(tǒng)的?direct_IO()?接口,對于?ext2文件系統(tǒng),direct_IO()?接口對應(yīng)的是?ext2_direct_IO()?函數(shù),而?ext2_direct_IO()?函數(shù)只是簡單的封裝了?generic_direct_IO()?函數(shù),所以我們來分析下?generic_direct_IO()?函數(shù)的實(shí)現(xiàn):
int generic_direct_IO(int rw, struct inode *inode, struct kiobuf *iobuf,unsigned long blocknr, int blocksize, get_block_t *get_block){int i, nr_blocks, retval;unsigned long *blocks = iobuf->blocks;nr_blocks = iobuf->length / blocksize;// 獲取要讀取的數(shù)據(jù)塊號列表for (i = 0; i < nr_blocks; i++, blocknr++) {struct buffer_head bh;bh.b_state = 0;bh.b_dev = inode->i_dev;bh.b_size = blocksize;retval = get_block(inode, blocknr, &bh, rw == READ ? 0 : 1);...blocks[i] = bh.b_blocknr;}// 開始進(jìn)行I/O操作retval = brw_kiovec(rw, 1, &iobuf, inode->i_dev, iobuf->blocks, blocksize);out:return retval;}
generic_direct_IO()?函數(shù)的邏輯也比較簡單,首先調(diào)用?get_block()?獲取要讀取的數(shù)據(jù)塊號列表,然后調(diào)用?brw_kiovec()?函數(shù)進(jìn)行 I/O 操作。所以?brw_kiovec()?函數(shù)才是 I/O 操作的最終觸發(fā)點(diǎn)。我們繼續(xù)分析:
int brw_kiovec(int rw, int nr, struct kiobuf *iovec[],kdev_t dev, unsigned long b[], int size){...for (i = 0; i < nr; i++) {...for (pageind = 0; pageind < iobuf->nr_pages; pageind++) {map = iobuf->maplist[pageind];...while (length > 0) {blocknr = b[bufind++];...tmp = bhs[bhind++];tmp->b_size = size;set_bh_page(tmp, map, offset); // 設(shè)置保存I/O操作后的數(shù)據(jù)的內(nèi)存地址 (用戶空間的內(nèi)存)tmp->b_this_page = tmp;init_buffer(tmp, end_buffer_io_kiobuf, iobuf); // 設(shè)置完成I/O后的收尾工作回調(diào)函數(shù)為: end_buffer_io_kiobuf()tmp->b_dev = dev;tmp->b_blocknr = blocknr;tmp->b_state = (1 << BH_Mapped) | (1 << BH_Lock) | (1 << BH_Req);...submit_bh(rw, tmp); // 提交 I/O 操作 (通用塊I/O層)if (bhind >= KIO_MAX_SECTORS) {kiobuf_wait_for_io(iobuf);err = wait_kio(rw, bhind, bhs, size);...}skip_block:length -= size;offset += size;if (offset >= PAGE_SIZE) {offset = 0;break;}} /* End of block loop */} /* End of page loop */} /* End of iovec loop */...return err;}
brw_kiovec()?函數(shù)主要完成 3 個(gè)工作:
設(shè)置用于保存 I/O 操作后的數(shù)據(jù)的內(nèi)存地址 (用戶申請的內(nèi)存)。
設(shè)置 I/O 操作完成后的收尾回調(diào)函數(shù)為: end_buffer_io_kiobuf()。
提交 I/O 操作到通用塊層。
可以看出,對于 I/O 操作后的數(shù)據(jù)會(huì)直接保存到用戶空間的內(nèi)存,而沒有通過內(nèi)核緩存作為中轉(zhuǎn),從而達(dá)到?直接I/O?的目的。
