NFS write过程1

1.nfs_file_write

NFS文件系统中write操作过程和read操作过程类似,区别在于read操作从服务器读取数据到客户端,write操作将数据从客户端写到服务器中。NFS文件系统中write操作的处理函数是nfs_file_write()。

[cpp]  view plain  copy
 print ?
  1. ssize_t nfs_file_write(struct kiocb *iocb, const struct iovec *iov,  
  2.                unsigned long nr_segs, loff_t pos)  
  3. {  
  4.     struct dentry * dentry = iocb->ki_filp->f_path.dentry;    // 找出文件的目录项结构  
  5.     struct inode * inode = dentry->d_inode;      // 这是文件的索引节点  
  6.     unsigned long written = 0;  
  7.     ssize_t result;  
  8.     size_t count = iov_length(iov, nr_segs);    // 计算用户态缓存的总长度  
  9.     struct nfs_inode *nfsi = NFS_I(inode);  
  10.   
  11.     // 步骤1  判断是否是直接IO  
  12.     if (iocb->ki_filp->f_flags & O_DIRECT)        // 这是直接写  
  13.         return nfs_file_direct_write(iocb, iov, nr_segs, pos, true);  
  14.   
  15.     // 下面就是缓存写了  
  16.     dprintk("NFS: write(%s/%s, %lu@%Ld)\n",  
  17.         dentry->d_parent->d_name.name, dentry->d_name.name,    // 父目录名称,文件名称  
  18.         (unsigned long) count, (long long) pos);    // 需要写的数据量,数据在文件中的位置  
  19.   
  20.     result = -EBUSY;  
  21.     if (IS_SWAPFILE(inode))  
  22.         goto out_swapfile;  
  23.     /* 
  24.      * O_APPEND implies that we must revalidate the file length. 
  25.      */  
  26.     // 步骤2  追加写时需要先更新文件长度  
  27.     if (iocb->ki_filp->f_flags & O_APPEND) {  // 追加写  
  28.         // 追加写操作之前需要先获取文件长度,因为文件长度已经发生了变化,  
  29.         // 需要重新确定文件长度。这个函数向服务器发起了GETATTR请求。  
  30.         result = nfs_revalidate_file_size(inode, iocb->ki_filp);  
  31.         if (result)  
  32.             goto out;   // 获取文件属性过程出错了  
  33.     }  
  34.   
  35.     result = count;  
  36.     if (!count) // 没有需要写的数据,不处理了,直接返回.  
  37.         goto out;  
  38.   
  39.     // 步骤3  这是缓存写的处理函数  
  40.     // 这就是通用的文件写操作方法,这个函数会调用write_begin和write_end。  
  41.     result = generic_file_aio_write(iocb, iov, nr_segs, pos);  
  42.     if (result > 0)  
  43.         written = result;   // 这是写入的数据量  
  44.   
  45.     /* Return error values for O_DSYNC and IS_SYNC() */  
  46.     // 步骤4  将数据刷新到服务器中  
  47.     if (result >= 0 && nfs_need_sync_write(iocb->ki_filp, inode)) {  
  48.         int err = vfs_fsync(iocb->ki_filp, 0);       // 同步数据,并且同步元数据  
  49.         if (err < 0)  
  50.             result = err;  
  51.     }  
  52.   
  53.     // 步骤5  更新统计信息  
  54.     if (result > 0)  
  55.         nfs_add_stats(inode, NFSIOS_NORMALWRITTENBYTES, written);  
  56. out:  
  57.     return result;  
  58.   
  59. out_swapfile:  
  60.     printk(KERN_INFO "NFS: attempt to write to active swap file!\n");  
  61.     goto out;  
  62. }  

这个函数的总体流程还是比较清晰的,分为下面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:这也是一个输出参数,由具体没有使用这个参数。

[cpp]  view plain  copy
 print ?
  1. static int nfs_write_begin(struct file *file, struct address_space *mapping,  
  2.                         loff_t pos, unsigned len, unsigned flags,  
  3.                         struct page **pagep, void **fsdata)  
  4. {  
  5.         int ret;  
  6.         pgoff_t index = pos >> PAGE_CACHE_SHIFT;        // 计算缓存页索引  
  7.         struct page *page;  
  8.         int once_thru = 0;  
  9.   
  10.         dfprintk(PAGECACHE, "NFS: write_begin(%s/%s(%ld), %u@%lld)\n",  
  11.                 file->f_path.dentry->d_parent->d_name.name,     // 父目录名称  
  12.                 file->f_path.dentry->d_name.name,               // 文件名称  
  13.                 mapping->host->i_ino, len, (long long) pos);    // 文件索引编号  长度 位置  
  14.   
  15. start:  
  16.         /* 
  17.          * Prevent starvation issues if someone is doing a consistency 
  18.          * sync-to-disk 
  19.          */  
  20.         // 等待这个标志位复位  NFS_INO_FLUSHING表示正在向服务器刷新数据,必须先等待,  
  21.         // 等待这个刷新操作完成。    步骤1  
  22.         ret = wait_on_bit(&NFS_I(mapping->host)->flags, NFS_INO_FLUSHING,  
  23.                         nfs_wait_bit_killable, TASK_KILLABLE);  
  24.         if (ret)  
  25.                 return ret;  
  26.   
  27.         // 查找指定索引的缓存页,如果不存在就创建一个缓存页,并加锁  mm/filemap.c    步骤2  
  28.         page = grab_cache_page_write_begin(mapping, index, flags);  
  29.         if (!page)  
  30.                 return -ENOMEM; // 分配缓存页失败  
  31.         *pagep = page;          // 这是分配的缓存页  
  32.   
  33.         // 如果不是新分配的缓存页,需要先处理这个缓存页中的请求.   步骤3  
  34.         ret = nfs_flush_incompatible(file, page);  
  35.         if (ret) {      // 处理失败,释放缓存页  
  36.                 unlock_page(page);  
  37.                 page_cache_release(page);  
  38.         } else if (!once_thru &&  
  39.                    nfs_want_read_modify_write(file, page, pos, len)) {  // 更新缓存页中的数据  
  40.                 once_thru = 1;  
  41.                 ret = nfs_readpage(file, page);  
  42.                 page_cache_release(page);  
  43.                 if (!ret)  
  44.                         goto start;  
  45.         }  
  46.         return ret;  
  47. }  
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()代码如下

[cpp]  view plain  copy
 print ?
  1. int nfs_flush_incompatible(struct file *file, struct page *page)  
  2. {  
  3.         struct nfs_open_context *ctx = nfs_file_open_context(file);     // 找到文件中的nfs_open_context结构  
  4.         struct nfs_page *req;  
  5.         int do_flush, status;  
  6.         /* 
  7.          * Look for a request corresponding to this page. If there 
  8.          * is one, and it belongs to another file, we flush it out 
  9.          * before we try to copy anything into the page. Do this 
  10.          * due to the lack of an ACCESS-type call in NFSv2. 
  11.          * Also do the same if we find a request from an existing 
  12.          * dropped page. 
  13.          */  
  14.         do {  
  15.                 // 查找缓存页中关联的请求  
  16.                 // 取出 page->private  这是nfs_page结构的指针,然后增加这个结构的引用计数.  
  17.                 req = nfs_page_find_request(page);  
  18.                 if (req == NULL)  
  19.                         return 0;       // 缓存页中没有关联数据请求,不需要处理直接退出就可以了。  
  20.                 do_flush = req->wb_page != page || req->wb_context != ctx ||  
  21.                         req->wb_lock_context->lockowner != current->files ||  
  22.                         req->wb_lock_context->pid != current->tgid;  
  23.                 nfs_release_request(req);       // 因为nfs_page_find_request()中增加了req的引用计数  
  24.                 if (!do_flush)  
  25.                         return 0;  
  26.                 status = nfs_wb_page(page_file_mapping(page)->host, page);      // 需要处理page中的请求  
  27.         } while (status == 0);  
  28.         return status;  
  29. }  
nfs_flush_incompatible()会检查缓存页中是否有待处理的请求,如果有待处理的请求并且请求中用户信息和当前用户信息不一致,就调用nfs_wb_page()将缓存页中的数据刷新到服务器中。

nfs_want_read_modify_write()的作用是检查文件读写顺序。

[cpp]  view plain  copy
 print ?
  1. static int nfs_want_read_modify_write(struct file *file, struct page *page,  
  2.                         loff_t pos, unsigned len)  
  3. {  
  4.         unsigned int pglen = nfs_page_length(page);     // 计算这个缓存页中的有效数据量  
  5.         unsigned int offset = pos & (PAGE_CACHE_SIZE - 1);      // 数据起始位置在缓存页中的偏移  
  6.         unsigned int end = offset + len;        // 这是数据结束位置的偏移  
  7.   
  8.         // offset是数据在缓存页中的起始位置,end是数据在缓存页中的结束位置.  
  9.         if ((file->f_mode & FMODE_READ) &&      /* open for read? */    // 以读权限打开  
  10.             // 没有设置标志位PG_uptodate(缓存页中数据不是最新的)  
  11.             !PageUptodate(page) &&              /* Uptodate? */           
  12.             // 没有设置标志位PG_private(缓存页没有关联nfs_page结构)  
  13.             !PagePrivate(page) &&               /* i/o request already? */        
  14.             // 缓存页中有有效数据  
  15.             pglen &&                            /* valid bytes of file? */  
  16.             // 这是一部分数据  
  17.             (end < pglen || offset))            /* replace all valid bytes? */    
  18.                 return 1;  
  19.         return 0;  
  20. }  


3.nfs_write_end

NFS文件系统中,write_end是由nfs_write_end()实现的,这个函数的主要作用是为缓存页创建nfs_page结构,前面介绍读操作时已经讲解了nfs_page结构的作用了。

参数file:这是文件对象结构

参数mapping:这是文件缓存的数据结构

参数pos:数据在文件中的起始位置

参数len:用户请求写入的数据长度

参数copied:实际写入到内核态缓存页中数据的长度

参数page:这是缓存页

参数fsdata:这是write_begin中设置的一个参数,由具体的文件系统使用,NFS没有使用这个参数

[cpp]  view plain  copy
 print ?
  1. static int nfs_write_end(struct file *file, struct address_space *mapping,  
  2.                         loff_t pos, unsigned len, unsigned copied,  
  3.                         struct page *page, void *fsdata)  
  4. {  
  5.         unsigned offset = pos & (PAGE_CACHE_SIZE - 1);  // 这是数据起始位置在缓存页中的偏移.  
  6.         int status;  
  7.   
  8.         dfprintk(PAGECACHE, "NFS: write_end(%s/%s(%ld), %u@%lld)\n",  
  9.                 file->f_path.dentry->d_parent->d_name.name,     // 父目录文件  
  10.                 file->f_path.dentry->d_name.name,               // 文件名称  
  11.                 mapping->host->i_ino, len, (long long) pos);    // 文件索引节点编号  数据长度  数据起始位置  
  12.   
  13.         /* 
  14.          * Zero any uninitialised parts of the page, and then mark the page 
  15.          * as up to date if it turns out that we're extending the file. 
  16.          */  
  17.         // 将数据位填充部分设置为0   步骤1  
  18.         if (!PageUptodate(page)) {      // 缓存页中的数据不是最新的  
  19.                 unsigned pglen = nfs_page_length(page);         // 计算缓存页中有效数据的长度  
  20.                 unsigned end = offset + len;    // 这是结束位置  
  21.   
  22.                 if (pglen == 0) {       // 缓存页中没有数据,整个缓存页填充为0.  
  23.                         zero_user_segments(page, 0, offset,  
  24.                                         end, PAGE_CACHE_SIZE);  // 全部初始化为0  
  25.                         SetPageUptodate(page);  // OK,缓存页中所有的数据都是最新的了,设置标志位PG_uptodate.  
  26.                 } else if (end >= pglen) {  
  27.                         zero_user_segment(page, end, PAGE_CACHE_SIZE);  // 将这部分内存初始化为0  
  28.                         if (offset == 0)  
  29.                                 SetPageUptodate(page);  // 现在所有数据是最新的了.  
  30.                 } else  
  31.                         zero_user_segment(page, pglen, PAGE_CACHE_SIZE);        // 将这部分数据初始化为0.  
  32.         }  
  33.   
  34.         // 这个函数只是设置了nfs_page结构,将缓存页面标记为脏.    步骤2  
  35.         status = nfs_updatepage(file, page, offset, copied);  
  36.   
  37.         unlock_page(page);  
  38.         page_cache_release(page);  
  39.   
  40.         if (status < 0)  
  41.                 return status;  
  42.         NFS_I(mapping->host)->write_io += copied;  
  43.         return copied;  
  44. }  
这个函数主要做了两件事情:(1)将缓存页中数据未填充部分设置为0;(2)为缓存页设置nfs_page结构,发起WRITE请求时需要使用这个数据结构。

参数file:文件对象结构

参数page:这是待处理的缓存页

参数offset:这是有效数据在缓存页中的起始位置

参数count:这是缓存页中有效数据的长度

[cpp]  view plain  copy
 print ?
  1. int nfs_updatepage(struct file *file, struct page *page,  
  2.                 unsigned int offset, unsigned int count)  
  3. {  
  4.         // 找到nfs_open_context结构,用户信息  
  5.         struct nfs_open_context *ctx = nfs_file_open_context(file);  
  6.         struct inode    *inode = page_file_mapping(page)->host; // 找到文件索引节点  
  7.         int             status = 0;  
  8.   
  9.         nfs_inc_stats(inode, NFSIOS_VFSUPDATEPAGE);  
  10.   
  11.         dprintk("NFS:       nfs_updatepage(%s/%s %d@%lld)\n",  
  12.                 file->f_path.dentry->d_parent->d_name.name,     // 父目录名称  
  13.                 file->f_path.dentry->d_name.name, count,        // 文件名称,缓存页中有效数据量  
  14.                 (long long)(page_file_offset(page) + offset));  // 数据在文件中的偏移量  
  15.   
  16.         /* If we're not using byte range locks, and we know the page 
  17.          * is up to date, it may be more efficient to extend the write 
  18.          * to cover the entire page in order to avoid fragmentation 
  19.          * inefficiencies. 
  20.          */  
  21.         // 步骤1   判断缓存页中是否所有数据都是最新的  
  22.         if (nfs_write_pageuptodate(page, inode) &&  
  23.                         inode->i_flock == NULL &&  
  24.                         !(file->f_flags & O_DSYNC)) {  
  25.                 count = max(count + offset, nfs_page_length(page));  
  26.                 offset = 0;  
  27.         }  
  28.   
  29.         // 步骤2   创建一个nfs_page结构  
  30.         status = nfs_writepage_setup(ctx, page, offset, count);   
  31.         if (status < 0)  
  32.                 // 数据传输过程发生I/O错误,设置标志位PG_error以及NFS_INO_INVALID_DATA  
  33.                 // 本地缓存中的数据标记为无效  
  34.                 nfs_set_pageerror(page);  
  35.         else  
  36.                 // 步骤3   将缓存页标记为脏  
  37.                 __set_page_dirty_nobuffers(page);  
  38.   
  39.         dprintk("NFS:       nfs_updatepage returns %d (isize %lld)\n",  
  40.                         status, (long long)i_size_read(inode));  
  41.         return status;  
  42. }  
缓存页中的数据不一定全部是最新的。比如缓存页为4096字节,但是我们只写入了1024字节,这种情况下只需要将1024字节传输到服务器就可以了。nfs_updatepage()检查了缓存页中数据是否最新的。如果缓存页中所有数据都是最新的,那么客户端将整个缓存页中的数据都传输到服务器。按照程序中的注释,传输整个缓存页中的数据效率要高一些。这些检查是通过nfs_write_pageuptodate()实现的。

参数page:这是一个缓存页

参数inode:这是文件索引节点

[cpp]  view plain  copy
 print ?
  1. static bool nfs_write_pageuptodate(struct page *page, struct inode *inode)  
  2. {  
  3.         if (nfs_have_delegated_attributes(inode))  
  4.                 goto out;  
  5.         if (NFS_I(inode)->cache_validity & NFS_INO_REVAL_PAGECACHE)  
  6.                 return false;  
  7. out:  
  8.         return PageUptodate(page) != 0;  
  9. }  

但是这个函数中存在一个bug,nfs_write_pageuptodate()只检查了标志位NFS_INO_REVAL_PAGECACHE,没有检查标志位NFS_INO_INVALID_DATA,下面这种情况中就会出现问题。

[cpp]  view plain  copy
 print ?
  1. # mount localhost:/export /mnt/nfs/ -o vers=3  
  2. # echo abc > /mnt/nfs/testfile; echo def >> /export/testfile; echo ghi >> /mnt/nfs/testfile  
  3. # cat -v /export/testfile  
  4. abc  
  5. ^@^@^@^@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:缓存页中的数据量

[cpp]  view plain  copy
 print ?
  1. static int nfs_writepage_setup(struct nfs_open_context *ctx, struct page *page,  
  2.                 unsigned int offset, unsigned int count)  
  3. {  
  4.         struct nfs_page *req;  
  5.   
  6.         // 创建一个新的nfs_page结构  
  7.         // page表示一个缓存页,offset是有效数据在page中的起始位置,count是有效数据的长度.  
  8.         req = nfs_setup_write_request(ctx, page, offset, count);  
  9.         if (IS_ERR(req))  
  10.                 return PTR_ERR(req);  
  11.         /* Update file length */  
  12.         // 当我们向文件中追加内容的时候,增加文件的长度.  这个函数只是修改了inode->i_size的值  
  13.         nfs_grow_file(page, offset, count);  
  14.         // 当缓存页中的数据全部更新后,设置缓存页中的标志位PG_uptodate.  
  15.         nfs_mark_uptodate(page, req->wb_pgbase, req->wb_bytes);  
  16.         // 将缓存页面标记为脏(PG_dirty),将radix树中的路径标记为脏(PAGECACHE_TAG_DIRTY)  
  17.         // 将文件索引节点标记为脏(I_DIRTY_PAGES)  
  18.         nfs_mark_request_dirty(req);  
  19.         nfs_unlock_and_release_request(req);  
  20.         return 0;  
  21. }  

4.vfs_fsync

这个函数负责将数据传输到服务器中,如果是同步写,则马上调用vfs_fsync()将缓存页中的数据写入到服务器中。如果不是同步写,则不是马上刷新数据,而是定期刷新。这个函数会调用address_space_operations结构中的writepages,NFS中这个函数是nfs_writepages(),下面文章中讲解这个函数。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值