Linux Kernel文件系統寫I/O流程代碼分析(一)


Linux Kernel文件系統寫I/O流程代碼分析(一)

Linux VFS機制簡析(二)這篇博客上介紹了struct address_space_operations里底層文件系統需要實現的操作,實際編碼過程中發現不是那么清楚的知道這里面的函數具體是干啥,在什么時候調用。尤其是寫IO相關的操作,包括write_begin, write_end, writepage, writepages, direct_IO以及set_page_dirty等函數指針。
要搞清楚這些函數指針,就需要縱觀整個寫流程里這些函數指針的調用位置。因此本文重點分析和梳理了Linux文件系統寫I/O的代碼流程,以幫助實現底層文件系統的讀寫接口。

概覽

先放一張圖鎮貼,該流程圖沒有包括bdi_writeback回寫機制(將在下一篇中展示):
Linux Kernel文件系統寫I/O代碼流程

VFS流程

sys_write()

Glibc提供的write()函數調用由內核的write系統調用實現,對應的系統調用函數為sys_write()定義如下:

asmlinkage long sys_write(unsigned int fd, const char __user *buf,
			  size_t count);

sys_write()的實現在fs/read_write.c里:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
		size_t, count)
{
	struct fd f = fdget_pos(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos = file_pos_read(f.file);
		ret = vfs_write(f.file, buf, count, &pos);
		file_pos_write(f.file, pos);
		fdput_pos(f);
	}

	return ret;
}

該函數獲取struct fd引用計數和pos鎖定,獲取pos並主要通過調用vfs_write()實現數據寫入。

vfs_write()

vfs_write()函數定義如下:

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	ssize_t ret;

	if (!(file->f_mode & FMODE_WRITE))
		return -EBADF;
	if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
		return -EINVAL;
	if (unlikely(!access_ok(VERIFY_READ, buf, count)))
		return -EFAULT;

	ret = rw_verify_area(WRITE, file, pos, count);
	if (ret >= 0) {
		count = ret;
		file_start_write(file);
		if (file->f_op->write)
			ret = file->f_op->write(file, buf, count, pos);
		else
			ret = do_sync_write(file, buf, count, pos);
		if (ret > 0) {
			fsnotify_modify(file);
			add_wchar(current, ret);
		}
		inc_syscw(current);
		file_end_write(file);
	}

	return ret;
}

該函數首先調用rw_verify_area()檢查pos和count對應的區域是否可以寫入(如是否獲取寫鎖等)。然后如果底層文件系統指定了struct file_operations里的write()函數指針,則調用file->f_op->write()函數,否則直接調用VFS的通用寫入函數do_sync_write()。

do_sync_write()

VFS的do_sync_write()函數在底層文件系統沒有指定f_op->write()函數指針時默認調用,它也被很多底層系統直接指定為f_op->write()。其定義如下所示:

ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
	struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
	struct kiocb kiocb;
	ssize_t ret;

	init_sync_kiocb(&kiocb, filp);
	kiocb.ki_pos = *ppos;
	kiocb.ki_left = len;
	kiocb.ki_nbytes = len;

	ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
	if (-EIOCBQUEUED == ret)
		ret = wait_on_sync_kiocb(&kiocb);
	*ppos = kiocb.ki_pos;
	return ret;
}

通過時上面的代碼可知,該函數主要生成struct kiocb,將其提交給f_op->aio_write()函數,並等待該kiocb的完成。所以底層文件系統必須實現f_op->aio_write()函數指針。
底層文件系統大部分實現了自己的f_op->aio_write(),也有部分文件系統(如ext4, nfs等)直接指向了通用的寫入方法:generic_file_aio_write()。我們通過該函數代碼來分析寫入的大致流程。

generic_file_aio_write()

VFS(其實是mm模塊)提供了通用的aio_write()函數,其定義如下:

ssize_t generic_file_aio_write(struct kiocb *iocb, const struct iovec *iov,
		unsigned long nr_segs, loff_t pos)
{
	struct file *file = iocb->ki_filp;
	struct inode *inode = file->f_mapping->host;
	ssize_t ret;

	BUG_ON(iocb->ki_pos != pos);

	mutex_lock(&inode->i_mutex);
	ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);
	mutex_unlock(&inode->i_mutex);

	if (ret > 0) {
		ssize_t err;

		err = generic_write_sync(file, pos, ret);
		if (err < 0 && ret > 0)
			ret = err;
	}
	return ret;
}

該函數對inode加鎖之后,調用__generic_file_aio_write()函數將數據寫入。如果ret > 0即數據寫入成功,並且寫操作需要同步到磁盤(如設置了O_SYNC),則調用generic_write_sync(),這里面將調用f_op->fsync()函數指針將數據寫盤。

函數__generic_file_aio_write()的代碼略多,這里貼出主要的片段如下:

ssize_t __generic_file_aio_write(struct kiocb *iocb, const struct iovec *iov,
				 unsigned long nr_segs, loff_t *ppos)
{
	...
	if (io_is_direct(file)) {
		loff_t endbyte;
		ssize_t written_buffered;

		written = generic_file_direct_write(iocb, iov, &nr_segs, pos,
							ppos, count, ocount);
		...
	} else {
		written = generic_file_buffered_write(iocb, iov, nr_segs,
				pos, ppos, count, written);
	}
	...

從上面代碼可以看到,如果是Direct IO,則調用generic_file_direct_write(),不經過page cache直接寫入磁盤;如果不是Direct IO,則調用generic_file_buffered_write()寫入page cache。

Direct IO實現

generic_file_direct_write()

函數generic_file_direct_write()的主要代碼如下所示:

ssize_t
generic_file_direct_write(struct kiocb *iocb, const struct iovec *iov,
		unsigned long *nr_segs, loff_t pos, loff_t *ppos,
		size_t count, size_t ocount)
{
	...

	if (count != ocount)
		*nr_segs = iov_shorten((struct iovec *)iov, *nr_segs, count);

	write_len = iov_length(iov, *nr_segs);
	end = (pos + write_len - 1) >> PAGE_CACHE_SHIFT;

	written = filemap_write_and_wait_range(mapping, pos, pos + write_len - 1);
	if (written)
		goto out;

	if (mapping->nrpages) {
		written = invalidate_inode_pages2_range(mapping,
					pos >> PAGE_CACHE_SHIFT, end);
		if (written) {
			if (written == -EBUSY)
				return 0;
			goto out;
		}
	}

	written = mapping->a_ops->direct_IO(WRITE, iocb, iov, pos, *nr_segs);

	if (mapping->nrpages) {
		invalidate_inode_pages2_range(mapping,
					      pos >> PAGE_CACHE_SHIFT, end);
	}

	if (written > 0) {
		pos += written;
		if (pos > i_size_read(inode) && !S_ISBLK(inode->i_mode)) {
			i_size_write(inode, pos);
			mark_inode_dirty(inode);
		}
		*ppos = pos;
	}
out:
	return written;
}

由於是Direct IO,在寫入之前需要調用filemap_write_and_wait_range()將page cache里的對應臟數據刷盤,以保障正確的寫入順序。filemap_write_and_wait_range()函數最終通過調用do_writepages()函數將臟頁刷盤(參見后面)。
然后調用invalidate_inode_pages2_range()函數將要寫入的區域在page cache里失效,以保證讀操作必須經過磁盤讀到最新寫入的數據。在本次寫操作完成后再次調用invalidate_inode_pages2_range()函數將page cache失效,避免寫入磁盤的過程中有新的讀取操作將過期數據讀到了cache里。
最終通過調用a_ops->dierct_IO()將數據Direct IO方式寫入磁盤。a_ops即struct address_operations,由底層文件系統實現。

Buffered IO實現

generic_file_buffered_write()

函數generic_file_buffered_write()的主要代碼如下所示:

ssize_t
generic_file_buffered_write(struct kiocb *iocb, const struct iovec *iov,
		unsigned long nr_segs, loff_t pos, loff_t *ppos,
		size_t count, ssize_t written)
{
	struct file *file = iocb->ki_filp;
	ssize_t status;
	struct iov_iter i;

	iov_iter_init(&i, iov, nr_segs, count, written);
	status = generic_perform_write(file, &i, pos);

	if (likely(status >= 0)) {
		written += status;
		*ppos = pos + status;
  	}
	
	return written ? written : status;
}

該函數初始化一個struct iov_iter,然后主要通過調用generic_perform_write()函數寫入page cache。

generic_perform_write()

函數generic_perform_write()主要代碼如下所示:

static ssize_t generic_perform_write(struct file *file,
				struct iov_iter *i, loff_t pos)
{
	...

	if (segment_eq(get_fs(), KERNEL_DS))
		flags |= AOP_FLAG_UNINTERRUPTIBLE;

	do {
		...

		offset = (pos & (PAGE_CACHE_SIZE - 1));
		bytes = min_t(unsigned long, PAGE_CACHE_SIZE - offset,
						iov_iter_count(i));

again:
		if (unlikely(iov_iter_fault_in_readable(i, bytes))) {
			status = -EFAULT;
			break;
		}

		status = a_ops->write_begin(file, mapping, pos, bytes, flags,
						&page, &fsdata);
		if (unlikely(status))
			break;

		if (mapping_writably_mapped(mapping))
			flush_dcache_page(page);

		pagefault_disable();
		copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
		pagefault_enable();
		flush_dcache_page(page);

		mark_page_accessed(page);
		status = a_ops->write_end(file, mapping, pos, bytes, copied,
						page, fsdata);
		if (unlikely(status < 0))
			break;
		copied = status;

		cond_resched();

		iov_iter_advance(i, copied);
		if (unlikely(copied == 0)) {
			bytes = min_t(unsigned long, PAGE_CACHE_SIZE - offset,
						iov_iter_single_seg_count(i));
			goto again;
		}
		pos += copied;
		written += copied;

		balance_dirty_pages_ratelimited(mapping);
		if (fatal_signal_pending(current)) {
			status = -EINTR;
			break;
		}
	} while (iov_iter_count(i));

	return written ? written : status;
}

該函數包括如下幾個步驟:
1.通過調用a_ops->write_begin()進行數據寫入前的處理,由底層文件系統實現,主要處理需要申請額外的存儲空間,以及從后端存儲(磁盤或者網絡)讀取不在緩存里的page數據。該函數返回locked的page。
2.從用戶空間拷貝數據到步驟1返回的page里。訪問用戶態內存時可能觸發缺頁異常,為避免陷入缺頁異常處理從而導致重入和死鎖(如mmap文件系統的內存),拷貝之前,通過pagefault_disable()將缺頁異常處理關閉,當發生缺頁異常時不進行異常處理。
3.通過調用底層文件系統的a_ops->write_end()將page這是為dirty並unlock。
4.循環步驟1-3,直到所有iov都得到處理,每次循環只處理一個page里的數據。
5.調用balance_dirty_pages_ratelimited()平衡內存中的臟頁,需要時將臟頁刷盤。

后記

從上可知,對於Buffered IO,並不一定有將數據寫入磁盤的操作,這就是延遲寫技術。數據寫入內核的page cache緩存后,后續由bdi_writeback機制負責臟頁的數據刷盤回寫。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM