1.nfs_file_write
NFS文件系统中write操作过程和read操作过程类似,区别在于read操作从服务器读取数据到客户端,write操作将数据从客户端写到服务器中。NFS文件系统中write操作的处理函数是nfs_file_write()。
- ssize_t nfs_file_write(struct kiocb *iocb, const struct iovec *iov,
- unsigned long nr_segs, loff_t pos)
- {
- struct dentry * dentry = iocb->ki_filp->f_path.dentry;
- struct inode * inode = dentry->d_inode;
- unsigned long written = 0;
- ssize_t result;
- size_t count = iov_length(iov, nr_segs);
- struct nfs_inode *nfsi = NFS_I(inode);
-
-
- if (iocb->ki_filp->f_flags & O_DIRECT)
- return nfs_file_direct_write(iocb, iov, nr_segs, pos, true);
-
-
- dprintk("NFS: write(%s/%s, %lu@%Ld)\n",
- dentry->d_parent->d_name.name, dentry->d_name.name,
- (unsigned long) count, (long long) pos);
-
- result = -EBUSY;
- if (IS_SWAPFILE(inode))
- goto out_swapfile;
-
-
-
-
- if (iocb->ki_filp->f_flags & O_APPEND) {
-
-
- result = nfs_revalidate_file_size(inode, iocb->ki_filp);
- if (result)
- goto out;
- }
-
- result = count;
- if (!count)
- goto out;
-
-
-
- result = generic_file_aio_write(iocb, iov, nr_segs, pos);
- if (result > 0)
- written = result;
-
-
-
- if (result >= 0 && nfs_need_sync_write(iocb->ki_filp, inode)) {
- int err = vfs_fsync(iocb->ki_filp, 0);
- if (err < 0)
- result = err;
- }
-
-
- if (result > 0)
- nfs_add_stats(inode, NFSIOS_NORMALWRITTENBYTES, written);
- out:
- return result;
-
- out_swapfile:
- printk(KERN_INFO "NFS: attempt to write to active swap file!\n");
- goto out;
- }
这个函数的总体流程还是比较清晰的,分为下面5个步骤
步骤1:判断是否是直接IO,如果是直接IO则调用nfs_file_direct_write()处理。
步骤2:追加写时先更新文件长度。因为多个客户端可以挂载同一个文件系统,其他客户端可能已经修改了文件,追加写时需要先确定现在文件结尾的位置,从这个位置写入数据。更新文件长度是由nfs_revalidate_file_size()完成的,这个函数最终发起了GETATTR请求,不仅更新了文件长度,还更新了文件其他的属性信息。写操作中只有追加写之前更新了文件属性,如果不是追加写就没有更新文件属性。估计这样设计的初衷是本地可能已经修改了文件数据,但是还没有提交到服务器中。如果这时更新文件属性的话,以前修改的数据就丢失了。
步骤3:调用generic_file_aio_write()将数据写入缓存页中。这是VFS层的函数,调用了文件系统的write_begin和write_end函数,后面会分析这两个函数。
步骤4:将数据刷新到服务器中。步骤3只是将数据写入到了客户端的缓存页中,并没有传输到服务器中。步骤4中检查是否需要马上就数据传输到服务器中,如果需要马上传送则向服务器发起WRITE请求,否则由客户端缓存管理进程定期刷新脏的缓存页。
步骤5:更新统计信息,这就没什么说的了。
2.nfs_write_begin
generic_file_aio_write()的作用是将用户态缓冲区中的数据写入到内核态缓存页中,这是VFS层的通用函数,这个函数包含三个步骤:
步骤1:准备内核态缓存页,比如分配新的缓存页,这是由文件系统的write_begin函数完成的,NFS中这个函数是nfs_write_begin()。
步骤2:将数据从用户态缓冲区中拷贝到内核态缓存页中,这是由函数iov_iter_copy_from_user_atomic()实现的,这是一个通用函数,就不讲了。
步骤3:进行一些后续处理,比如将缓存页标记为脏,这是由文件系统的write_end函数完成的,NFS中这个函数是nfs_write_end()。
nfs_write_begin()流程如下:
参数file:这是文件对象指针,就是将数据写入到这个文件中
参数mapping:这是文件缓存的数据结构
参数pos:将要写入的数据在文件中的位置
参数len:要写入数据的长度
参数flags:一些标志位信息
参数pagep:这是一个输出参数,保存了缓存页指针
参数fsdata:这也是一个输出参数,由具体没有使用这个参数。
- static int nfs_write_begin(struct file *file, struct address_space *mapping,
- loff_t pos, unsigned len, unsigned flags,
- struct page **pagep, void **fsdata)
- {
- int ret;
- pgoff_t index = pos >> PAGE_CACHE_SHIFT;
- struct page *page;
- int once_thru = 0;
-
- dfprintk(PAGECACHE, "NFS: write_begin(%s/%s(%ld), %u@%lld)\n",
- file->f_path.dentry->d_parent->d_name.name,
- file->f_path.dentry->d_name.name,
- mapping->host->i_ino, len, (long long) pos);
-
- start:
-
-
-
-
-
-
- ret = wait_on_bit(&NFS_I(mapping->host)->flags, NFS_INO_FLUSHING,
- nfs_wait_bit_killable, TASK_KILLABLE);
- if (ret)
- return ret;
-
-
- page = grab_cache_page_write_begin(mapping, index, flags);
- if (!page)
- return -ENOMEM;
- *pagep = page;
-
-
- ret = nfs_flush_incompatible(file, page);
- if (ret) {
- unlock_page(page);
- page_cache_release(page);
- } else if (!once_thru &&
- nfs_want_read_modify_write(file, page, pos, len)) {
- once_thru = 1;
- ret = nfs_readpage(file, page);
- page_cache_release(page);
- if (!ret)
- goto start;
- }
- return ret;
- }
nfs_write_begin()分为下面几个步骤:
步骤1:检查标志位NFS_INO_FLUSHING,这个标志位表示客户端正在向服务器刷新数据,必须等待刷新操作完成后才能接着处理。
步骤2:查找缓存页,如果缓存页不存在就创建一个新的缓存页,这是通过grab_cache_page_write_begin()实现的。这是VFS层的通用函数,不讲了。
步骤3:处理缓存页不兼容的请求,这是由nfs_flush_incompatible()处理的。客户端中具有多个用户,可能不同的用户向同一个缓存页写入了数据,如用户1向缓存页前2048字节写入了数据,用户2向缓存页后2048字节写入了数据。需要先处理这个缓存页中的请求。后面会讲解这个函数。
步骤4:更新缓存页中的数据,这里涉及到读写顺序问题。目前存在两种顺序read-modify-write和modify-write-read。如果采用的是read-modify-write顺序,就先向服务器发送READ请求,将数据读到缓存页中。
nfs_flush_incompatible()代码如下
- int nfs_flush_incompatible(struct file *file, struct page *page)
- {
- struct nfs_open_context *ctx = nfs_file_open_context(file);
- struct nfs_page *req;
- int do_flush, status;
-
-
-
-
-
-
-
-
- do {
-
-
- req = nfs_page_find_request(page);
- if (req == NULL)
- return 0;
- do_flush = req->wb_page != page || req->wb_context != ctx ||
- req->wb_lock_context->lockowner != current->files ||
- req->wb_lock_context->pid != current->tgid;
- nfs_release_request(req);
- if (!do_flush)
- return 0;
- status = nfs_wb_page(page_file_mapping(page)->host, page);
- } while (status == 0);
- return status;
- }
nfs_flush_incompatible()会检查缓存页中是否有待处理的请求,如果有待处理的请求并且请求中用户信息和当前用户信息不一致,就调用nfs_wb_page()将缓存页中的数据刷新到服务器中。
nfs_want_read_modify_write()的作用是检查文件读写顺序。
- static int nfs_want_read_modify_write(struct file *file, struct page *page,
- loff_t pos, unsigned len)
- {
- unsigned int pglen = nfs_page_length(page);
- unsigned int offset = pos & (PAGE_CACHE_SIZE - 1);
- unsigned int end = offset + len;
-
-
- if ((file->f_mode & FMODE_READ) &&
-
- !PageUptodate(page) &&
-
- !PagePrivate(page) &&
-
- pglen &&
-
- (end < pglen || offset))
- return 1;
- return 0;
- }
3.nfs_write_end
NFS文件系统中,write_end是由nfs_write_end()实现的,这个函数的主要作用是为缓存页创建nfs_page结构,前面介绍读操作时已经讲解了nfs_page结构的作用了。
参数file:这是文件对象结构
参数mapping:这是文件缓存的数据结构
参数pos:数据在文件中的起始位置
参数len:用户请求写入的数据长度
参数copied:实际写入到内核态缓存页中数据的长度
参数page:这是缓存页
参数fsdata:这是write_begin中设置的一个参数,由具体的文件系统使用,NFS没有使用这个参数
- static int nfs_write_end(struct file *file, struct address_space *mapping,
- loff_t pos, unsigned len, unsigned copied,
- struct page *page, void *fsdata)
- {
- unsigned offset = pos & (PAGE_CACHE_SIZE - 1);
- int status;
-
- dfprintk(PAGECACHE, "NFS: write_end(%s/%s(%ld), %u@%lld)\n",
- file->f_path.dentry->d_parent->d_name.name,
- file->f_path.dentry->d_name.name,
- mapping->host->i_ino, len, (long long) pos);
-
-
-
-
-
-
- if (!PageUptodate(page)) {
- unsigned pglen = nfs_page_length(page);
- unsigned end = offset + len;
-
- if (pglen == 0) {
- zero_user_segments(page, 0, offset,
- end, PAGE_CACHE_SIZE);
- SetPageUptodate(page);
- } else if (end >= pglen) {
- zero_user_segment(page, end, PAGE_CACHE_SIZE);
- if (offset == 0)
- SetPageUptodate(page);
- } else
- zero_user_segment(page, pglen, PAGE_CACHE_SIZE);
- }
-
-
- status = nfs_updatepage(file, page, offset, copied);
-
- unlock_page(page);
- page_cache_release(page);
-
- if (status < 0)
- return status;
- NFS_I(mapping->host)->write_io += copied;
- return copied;
- }
这个函数主要做了两件事情:(1)将缓存页中数据未填充部分设置为0;(2)为缓存页设置nfs_page结构,发起WRITE请求时需要使用这个数据结构。
参数file:文件对象结构
参数page:这是待处理的缓存页
参数offset:这是有效数据在缓存页中的起始位置
参数count:这是缓存页中有效数据的长度
- int nfs_updatepage(struct file *file, struct page *page,
- unsigned int offset, unsigned int count)
- {
-
- struct nfs_open_context *ctx = nfs_file_open_context(file);
- struct inode *inode = page_file_mapping(page)->host;
- int status = 0;
-
- nfs_inc_stats(inode, NFSIOS_VFSUPDATEPAGE);
-
- dprintk("NFS: nfs_updatepage(%s/%s %d@%lld)\n",
- file->f_path.dentry->d_parent->d_name.name,
- file->f_path.dentry->d_name.name, count,
- (long long)(page_file_offset(page) + offset));
-
-
-
-
-
-
-
- if (nfs_write_pageuptodate(page, inode) &&
- inode->i_flock == NULL &&
- !(file->f_flags & O_DSYNC)) {
- count = max(count + offset, nfs_page_length(page));
- offset = 0;
- }
-
-
- status = nfs_writepage_setup(ctx, page, offset, count);
- if (status < 0)
-
-
- nfs_set_pageerror(page);
- else
-
- __set_page_dirty_nobuffers(page);
-
- dprintk("NFS: nfs_updatepage returns %d (isize %lld)\n",
- status, (long long)i_size_read(inode));
- return status;
- }
缓存页中的数据不一定全部是最新的。比如缓存页为4096字节,但是我们只写入了1024字节,这种情况下只需要将1024字节传输到服务器就可以了。nfs_updatepage()检查了缓存页中数据是否最新的。如果缓存页中所有数据都是最新的,那么客户端将整个缓存页中的数据都传输到服务器。按照程序中的注释,传输整个缓存页中的数据效率要高一些。这些检查是通过nfs_write_pageuptodate()实现的。
参数page:这是一个缓存页
参数inode:这是文件索引节点
- static bool nfs_write_pageuptodate(struct page *page, struct inode *inode)
- {
- if (nfs_have_delegated_attributes(inode))
- goto out;
- if (NFS_I(inode)->cache_validity & NFS_INO_REVAL_PAGECACHE)
- return false;
- out:
- return PageUptodate(page) != 0;
- }
但是这个函数中存在一个bug,nfs_write_pageuptodate()只检查了标志位NFS_INO_REVAL_PAGECACHE,没有检查标志位NFS_INO_INVALID_DATA,下面这种情况中就会出现问题。
- # mount localhost:/export /mnt/nfs/ -o vers=3
- # echo abc > /mnt/nfs/testfile; echo def >> /export/testfile; echo ghi >> /mnt/nfs/testfile
- # cat -v /export/testfile
- abc
- ^@^@^@^@ghi
上面这个例子中,客户端先向文件中写入数据,服务器端再追加数据,客户端再次写入数据。客户端第二次写入数据前会发起GETATTR调用或者文件属性。由于服务器端已经追加了数据,客户端会设置标志位NFS_INO_INVALID_DATA,但是nfs_write_pageuptodate()没有检查标志位NFS_INO_INVALID_DATA,因此返回true。客户端认为缓存页中所有数据都是最新的,因此将整个缓存页中的数据传输到服务器中,这就覆盖了文件中"def"三个字节。这个bug在后来的版本中修正了,讨论信息参见https://lkml.org/lkml/2013/1/16/395。
创建nfs_page结构的函数是nfs_writepage_setup()。
参数ctx:这里保存了用户信息
参数page:表示一个缓存页
参数offset:数据在缓存页中的偏移值
参数count:缓存页中的数据量
- static int nfs_writepage_setup(struct nfs_open_context *ctx, struct page *page,
- unsigned int offset, unsigned int count)
- {
- struct nfs_page *req;
-
-
-
- req = nfs_setup_write_request(ctx, page, offset, count);
- if (IS_ERR(req))
- return PTR_ERR(req);
-
-
- nfs_grow_file(page, offset, count);
-
- nfs_mark_uptodate(page, req->wb_pgbase, req->wb_bytes);
-
-
- nfs_mark_request_dirty(req);
- nfs_unlock_and_release_request(req);
- return 0;
- }
4.vfs_fsync
这个函数负责将数据传输到服务器中,如果是同步写,则马上调用vfs_fsync()将缓存页中的数据写入到服务器中。如果不是同步写,则不是马上刷新数据,而是定期刷新。这个函数会调用address_space_operations结构中的writepages,NFS中这个函数是nfs_writepages(),下面文章中讲解这个函数。