一、如何追踪

NFS客户端(client)和服务器(server)端的日志分别为:nfs_debug、nfsd_debug。它们的位置为:/proc/sys/sunrpc。这两个日志文件初始内容为:0,表示关闭,写入其他数字可以把它们打开。若不打开日志,运行dmesg命令之后,是无法看到在nfs源代码中输出的打印信息的。打开日志后,方可看到。

二、打开日志

  1. AS端
    echo 32767 > /proc/sys/sunrpc/nfs_debug

执行dmesg
pNFS读流程追踪
如图所示会出现一些打印信息。

  1. MDS端
    echo 32767 > /proc/sys/sunrpc/nfsd_debug

三、读流程

fs/nfs
<1>nfs_file_read(file.c)

static ssize_t nfs_file_read(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;
    ssize_t result;
    size_t count = iov_length(iov, nr_segs);
    if (iocb->ki_filp->f_flags & O_DIRECT)
            return nfs_file_direct_read(iocb, iov, nr_segs, pos);

    result = nfs_revalidate_mapping(inode, iocb->ki_filp->f_mapping);
    nfs_add_stats(inode, NFSIOS_NORMALREADBYTES, count);
    if (!result)
        result = generic_file_aio_read(iocb, iov, nr_segs, pos);
        
    return result;
}

read有两种方式,一种是直接读:nfs_file_direct_read;一种是从缓存中读:generic_file_aio_read。如果从缓存中读,因为多个客户端可以挂载在一个服务器上,如果别的客户端修改了文件,相应的缓存中的文件信息就需要修改,所以必须先检查是否需要更新缓存中的文件属性信息:nfs_revalidate_mapping。

我们看一下nfs_revalidate_mapping函数:

int nfs_revalidate_mapping(struct inode *inode, struct address_space *mapping)
{
      struct nfs_inode *nfsi = NFS_I(inode);
      int ret = 0;
      
      if ((nfsi->cache_validity & NFS_INO_REVAL_PAGECACHE)
           || nfs_attribute_timeout(inode) || NFS_STALE(inode))
      {
		           ret = __nfs_revalidate_inode(NFS_SERVER(inode), inode);
		           if (ret < 0)   goto out;
		      }
		      if (nfsi->cache_validity & NFS_INO_INVALID_DATA)
		           ret = nfs_invalidate_mapping(inode, mapping);
out:
      return ret;
}

判断是否需要更新看三个条件:

  1. NFS_INO_INVALID_DATA:强制更新,只要设置了这个位,必须更新缓存中的信息。
  2. nfs_attribute_timeout:属性是否超时,inode包含两个字段:read_cache_jiffies和attrtimeo,前者是上次更新时间、后者是有效期,可以通过它俩判断是否超时。
  3. NFS_STALE:文件句柄是否过期。

需要更新就调用__nfs_revalidate_inode函数更新,__nfs_revalidate_inode通过发送GETATTR请求从服务器获取信息,然后调用nfs_refresh_inode函数更新。

<2>generic_file_aio_read(file.c)
上面已经说过,可以从缓存中读取数据,这里的缓存指的就是页缓存。nfs组织页缓存是用的radix树。如下图所示:
pNFS读流程追踪
文件被分割成以page为单位的数据块,这些page被组织成一棵多叉树。如果页大小为4096B,叶子层最左端的第一个页就保存着文件的前4096B、第二个页保存着接下来的4096B,以此类推。中间节点node记录了某一地址上的数据在哪一个page上。

从页缓存中读数据的过程为:首先从radix树上获取相应的page,如果获取到了就直接将数据拷贝到用户空间;如果该page不存在,需调用nfs_readpages更新数据。

<3>nfs_readpages(file.c)

int nfs_readpages(struct file *filp, 
                   struct address_space *mapping,
                   struct list_head *pages, 
                   unsigned nr_pages)
{
    struct nfs_pageio_descriptor pgio;
    struct nfs_readdesc desc = {
                 .pgio = &pgio,
	    };
	    struct inode *inode = mapping->host;
	    struct nfs_server *server = NFS_SERVER(inode);
	    size_t rsize = server->rsize;
	    unsigned long npages;
	    int ret = -ESTALE;

    nfs_inc_stats(inode, NFSIOS_VFSREADPAGES);

    if (NFS_STALE(inode))
         goto out;

    if (filp == NULL) {
        desc.ctx = nfs_find_open_context(inode, NULL, FMODE_READ);
        if (desc.ctx == NULL)
              return -EBADF;
    } else
        desc.ctx = get_nfs_open_context(nfs_file_open_context(filp));
    
    ret = nfs_readpages_from_fscache(desc.ctx, inode, mapping,pages, &nr_pages);
    if (ret == 0)
         goto read_complete; 

#ifdef CONFIG_PNFS
    pnfs_pageio_init_read(&pgio, inode, desc.ctx, pages, &rsize);
#endif
    if (rsize < PAGE_CACHE_SIZE)
        nfs_pageio_init(&pgio, inode, nfs_pagein_multi, rsize, 0);
    else
        nfs_pageio_init(&pgio, inode, nfs_pagein_one, rsize, 0);

    ret = read_cache_pages(mapping, pages, readpage_async_filler, &desc);

    nfs_pageio_complete(&pgio);
    npages = (pgio.pg_bytes_written + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;
    nfs_add_stats(inode, NFSIOS_READPAGES, npages);
read_complete:
    put_nfs_open_context(desc.ctx);
out:
    return ret;
}

参数列表:pages是缓存页链表,nr_pages是链表中缓存页的数量。

这个函数中有一些数据结构,简要说明一下:

  1. nfs_page:每个缓存页对应一个nfs_page
  2. nfs_pageio_descriptor:缓存页链表
  3. nfs_open_context:与用户信息有关的数据结构
  4. nfs_readdesc:这个数据结构里有struct nfs_pageio_descriptor和struct nfs_open_context

nfs_readpages函数大概有七个步骤:

  1. 获取用户信息
if (filp == NULL) {
      desc.ctx = nfs_find_open_context(inode, NULL, FMODE_READ);
      if (desc.ctx == NULL)
           return -EBADF;
 } else
      desc.ctx = get_nfs_open_context(nfs_file_open_context(filp));
  1. 检查FS-Cache中的数据是否有效,如果有效就直接拿来填充page,不需要从服务器读;如果无效,就继续下面的步骤——将链表中的缓存页添加到redix树中,然后从服务器请求数据填充这些页
 ret = nfs_readpages_from_fscache(desc.ctx, inode, mapping,pages, &nr_pages);
 if (ret == 0)
     goto read_complete; 

ret=0表示FS-Cache中数据有效,调用put_nfs_open_context填充page。

  1. 发起LAYOUTGET请求
pnfs_pageio_init_read(&pgio, inode, desc.ctx, pages, &rsize);
pnfs_pageio_init_read(struct nfs_pageio_descriptor *pgio,
                      struct inode *inode,
                      struct nfs_open_context *ctx,
                      struct list_head *pages,
                      size_t *rsize)
{
   struct nfs_server *nfss = NFS_SERVER(inode);
   size_t count = 0;
   loff_t loff;
   int status = 0;

   pgio->pg_threshold = 0;
   pgio->pg_iswrite = 0;
   pgio->pg_boundary = 0;
   pgio->pg_test = NULL;

   if (!pnfs_enabled_sb(nfss))    //查看客户端是否设置了layout
       return;

   readahead_range(inode, pages, &loff, &count);

   if (count > 0 && !below_threshold(inode, count, 0)) {
        status = pnfs_update_layout(inode, ctx, count,loff, IOMODE_READ, NULL);
        dprintk("%s *rsize %Zd virt update returned %d\n",__func__, *rsize, status);
        if (status != 0)
            return;

        *rsize = NFS_SERVER(inode)->ds_rsize;
        pgio->pg_boundary = pnfs_getboundary(inode);
        if (pgio->pg_boundary)    pnfs_set_pg_test(inode, pgio);
    }
}

readhead_range:预读
pnfs_update_layout:发起请求
关键函数是pnfs_update_layout,下面着重介绍这个函数。

看这个函数之前,先要明白layout是如何存放的——layout对应数据结构pnfs_layout_segment,每个layout对应文件中的一段数据,由于用户可能申请了该文件的多段数据,因此pnfs_layout_segment会形成一个链表。这个链表保存在数据结构pnfs_layout_hdr中。又由于一个用户可能会读多个文件,因此pnfs_layout_hdr也会形成一个链表。这个链表保存在数据结构nfs_server中。即:
pNFS读流程追踪

下面具体看pnfs_update_layout函数的代码:

int pnfs_update_layout(struct inode *ino,
                       struct nfs_open_context *ctx,
                       u64 count,
                       loff_t pos,
                       enum pnfs_iomode iomode,
                       struct pnfs_layout_segment **lsegpp)
{
   struct nfs4_pnfs_layout_segment arg = {
         .iomode = iomode,
         .offset = pos,
         .length = count
   };
   struct nfs_inode *nfsi = NFS_I(ino);
   struct pnfs_layout_type *lo;
   struct pnfs_layout_segment *lseg = NULL;
   bool take_ref = (lsegpp != NULL);
   DEFINE_WAIT(__wait);
   int result = 0;

   lo = get_lock_alloc_layout(ino);    //创建layout
   if (IS_ERR(lo)) {                   //创建出错
       dprintk("%s ERROR: can't get pnfs_layout_type\n", __func__);
       result = PTR_ERR(lo);
       goto out;
    }

   lseg = pnfs_has_layout(lo, &arg, take_ref, !take_ref);    //检查是否存在layout
   if (lseg && !lseg->valid) {
      spin_unlock(&nfsi->lo_lock);         
      if (take_ref)   put_lseg(lseg);
      for (;;) 
      {
            prepare_to_wait(&nfsi->lo_waitq, &__wait,TASK_KILLABLE);
            spin_lock(&nfsi->lo_lock);      //上锁
            lseg = pnfs_has_layout(lo, &arg, take_ref, !take_ref);
            if (!lseg || lseg->valid)
                  break;
            dprintk("%s: invalid lseg %p ref %d\n", __func__,lseg, atomic_read(&lseg->kref.refcount)-1);
            if (take_ref)
                  put_lseg(lseg);
            if (signal_pending(current)) {
                  lseg = NULL;
                  result = -ERESTARTSYS;
                  break;
            }
            spin_unlock(&nfsi->lo_lock);
            schedule();
      }
      finish_wait(&nfsi->lo_waitq, &__wait);
      if (result)
         goto out_put;
  }

  if (lseg) {
      dprintk("%s: Using cached lseg %p for %[email protected]%llu iomode %d)\n",
                   __func__,
                   lseg,
                   arg.length,
                   arg.offset,
                   arg.iomode);

       goto out_put;
   }

   if (test_bit(NFS_INO_LAYOUT_FAILED, &nfsi->pnfs_layout_state)) {
        if (unlikely(nfsi->pnfs_layout_suspend &&get_seconds() >= nfsi->pnfs_layout_suspend)) {
                dprintk("%s: layout_get resumed\n", __func__);
                clear_bit(NFS_INO_LAYOUT_FAILED,&nfsi->pnfs_layout_state);
                nfsi->pnfs_layout_suspend = 0;
        } else {
                result = 1;
                goto out_put;
        }
    }

    drain_layoutreturns(lo);
    atomic_inc(&lo->lgetcount);
    spin_unlock(&nfsi->lo_lock);

    result = get_layout(ino, ctx, &arg, lsegpp, lo);
out:
    dprintk("%s end (err:%d) state 0x%lx lseg %p\n",__func__, result, nfsi->pnfs_layout_state, lseg);
    return result;
out_put:
    if (lsegpp)
        *lsegpp = lseg;
    put_unlock_current_layout(lo);
    goto out;
}

代码比较长,分块解释一下:
(1)if (lseg && !lseg->valid) {……}部分好像是和进程调度有关的,里面包含了很多linux内核的函数,简单介绍一下:
①spin_unlock():释放锁。与之相对应的是spin_lock()。出现资源竞争时会有两种解决方案,一种是挂起,一种是死等。spin_lock是强制死等的锁机制。
②prepare_to_wait(&nfsi->lo_waitq, &__wait,TASK_KILLABLE):把__wait加入到nfsi->lo_waitq队列,并把进程设置为TASK_KILLABLE状态。该状态是一种可终止的睡眠状态。
③automic_read():原子读,从内存中读取参数的值。
④signal_pending()检查当前进程是否有信号处理。返回0表示有。
⑤finish_wait():可能是结束当前进程工作的意思。

(2)test_bit():检查某一位是否为1,为1返回“T”、不为1返回“F”。这里是检查标志:NFS_INO_LAYOUT_FAILED,这个标志位的意思是获取layout是否失败。

(3)atomic_inc():增加可以发起layout请求的计数。

(4)get_layout():发起layout请求。这个函数比较重要,上代码:

static int get_layout(struct inode *ino,
                      struct nfs_open_context *ctx,
                      struct nfs4_pnfs_layout_segment *range,
                      struct pnfs_layout_segment **lsegpp,
                      struct pnfs_layout_type *lo)
{
    int status;
    struct nfs_server *server = NFS_SERVER(ino);       //找到nfs_server结构
    struct nfs4_pnfs_layoutget *lgp;                   //LAYOUTGET的参数和返回值

    lgp = kzalloc(sizeof(*lgp), GFP_KERNEL);           //为LAYOUTGET请求分配内存
    if (lgp == NULL) {                                 //分配失败
        pnfs_layout_release(lo, &lo->lgetcount, NULL);
        return -ENOMEM;
    }
    lgp->lo = lo;
    lgp->args.minlength = PAGE_CACHE_SIZE;             //最少请求的数据量
    lgp->args.maxcount = PNFS_LAYOUT_MAXSIZE;
    lgp->args.lseg.iomode = range->iomode;
    lgp->args.lseg.offset = range->offset;             //请求数据的偏移
    lgp->args.lseg.length = max(range->length, lgp->args.minlength);
    lgp->args.type = server->pnfs_curr_ld->id;         //layout类型
    lgp->args.inode = ino;                             //文件索引节点
    lgp->lsegpp = lsegpp;

    if (!memcmp(lo->stateid.data, &zero_stateid, NFS4_STATEID_SIZE)) {    //可能是数据不一致时执行操作使数据恢复一致性
        struct nfs_open_context *oldctx = ctx;

        if (!oldctx) {
            ctx = nfs_find_open_context(ino, NULL,
                   (range->iomode == IOMODE_READ) ?
                   FMODE_READ: FMODE_WRITE);
            BUG_ON(!ctx);
        }
        pnfs_layout_from_open_stateid(&lgp->args.stateid, ctx->state);
        if (!oldctx)
            put_nfs_open_context(ctx);         //减少nfs_open_context引用计数
    } else
       pnfs_get_layout_stateid(&lgp->args.stateid, lo);    //这个函数和pnfs_layout_from_stateid()的实现过程是一样的

	
    status = pnfs4_proc_layoutget(lgp);

    dprintk("<-- %s status %d\n", __func__, status);
    return status;
}

非关键函数的功能已经在代码中标出,重点是pnfs4_proc_layoutget函数,它是向服务器端发送layout的操作函数,代码如下:

int pnfs4_proc_layoutget(struct nfs4_pnfs_layoutget *lgp)
{
    struct inode *ino = lgp->args.inode;
    struct nfs_server *server = NFS_SERVER(ino);
    struct rpc_task *task;
    struct rpc_message msg = {
        .rpc_proc = &nfs4_procedures[NFSPROC4_CLNT_PNFS_LAYOUTGET],
        .rpc_argp = &lgp->args,
        .rpc_resp = &lgp->res,
    };
    struct rpc_task_setup task_setup_data = {
        .rpc_client = server->client,
        .rpc_message = &msg,
        .callback_ops = &nfs4_pnfs_layoutget_call_ops,
        .callback_data = lgp,
        .flags = RPC_TASK_ASYNC,
    };
    int status;

    lgp->res.layout.buf = (void *)__get_free_page(GFP_NOFS);
    if (lgp->res.layout.buf == NULL) {
        nfs4_pnfs_layoutget_release(lgp);
        status = -ENOMEM;
        goto out;
     }
    lgp->res.seq_res.sr_slotid = NFS4_MAX_SLOT_TABLE;
    task = rpc_run_task(&task_setup_data);          //发送rpc请求
    if (IS_ERR(task)) {
        status = PTR_ERR(task);
        goto out;
     }
     status = nfs4_wait_for_completion_rpc_task(task);
     if (status == 0) {
        status = lgp->status;
        if (status == 0)
              status = pnfs_layout_process(lgp);
      }
      rpc_put_task(task);
out:
      dprintk("<-- %s status=%d\n", __func__, status);
      return status;
}
  1. 执行nfs_pagein_multi或nfs_pagein_one,二者的区别是:nfs_pagein_multi是发起多个/一个read请求去填充一个page。以nfs_pagein_multi为例,调用过程为:nfs_pagein_multi–>nfs_read_rpcsetup–>pnfs_initiate_read–>pnfs_try_to_read_data
  2. 执行read_cache_pages把创建的nfs_page结构挂到radix树上
  3. 触发pnfs_read_done函数,调用_pnfs_return_layout
  4. 收尾工作,调用nfs_pageio_complete

整个过程的流程图大致如下:
pNFS读流程追踪

相关文章: