一、如何追踪
NFS客户端(client)和服务器(server)端的日志分别为:nfs_debug、nfsd_debug。它们的位置为:/proc/sys/sunrpc。这两个日志文件初始内容为:0,表示关闭,写入其他数字可以把它们打开。若不打开日志,运行dmesg命令之后,是无法看到在nfs源代码中输出的打印信息的。打开日志后,方可看到。
二、打开日志
- AS端
echo 32767 > /proc/sys/sunrpc/nfs_debug
执行dmesg
如图所示会出现一些打印信息。
- 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;
}
判断是否需要更新看三个条件:
- NFS_INO_INVALID_DATA:强制更新,只要设置了这个位,必须更新缓存中的信息。
- nfs_attribute_timeout:属性是否超时,inode包含两个字段:read_cache_jiffies和attrtimeo,前者是上次更新时间、后者是有效期,可以通过它俩判断是否超时。
- NFS_STALE:文件句柄是否过期。
需要更新就调用__nfs_revalidate_inode函数更新,__nfs_revalidate_inode通过发送GETATTR请求从服务器获取信息,然后调用nfs_refresh_inode函数更新。
<2>generic_file_aio_read(file.c)
上面已经说过,可以从缓存中读取数据,这里的缓存指的就是页缓存。nfs组织页缓存是用的radix树。如下图所示:
文件被分割成以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是链表中缓存页的数量。
这个函数中有一些数据结构,简要说明一下:
- nfs_page:每个缓存页对应一个nfs_page
- nfs_pageio_descriptor:缓存页链表
- nfs_open_context:与用户信息有关的数据结构
- nfs_readdesc:这个数据结构里有struct nfs_pageio_descriptor和struct nfs_open_context
nfs_readpages函数大概有七个步骤:
- 获取用户信息
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));
- 检查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。
- 发起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_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;
}
- 执行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
- 执行read_cache_pages把创建的nfs_page结构挂到radix树上
- 触发pnfs_read_done函数,调用_pnfs_return_layout
- 收尾工作,调用nfs_pageio_complete
整个过程的流程图大致如下: