前面几篇文章基本介绍清楚了LAYOUTGET和GETDEVICEINFO两个请求,这是客户端发起I/O操作前的准备工作,这篇文章中我们就以读操作为例讲讲pNFS中的I/O操作。pNFS中的读操作和没有采用pNFS机制时的读操作基本流程是一致的,只是部分步骤有差别,这篇文章主要关注差别之处。
static const struct nfs_pageio_ops filelayout_pg_read_ops = {
// 读数据前需要申请了layout信息 (LAYOUTGET, GETDEVICEINFO)
.pg_init = filelayout_pg_init_read,
// 检查两个I/O请求(缓存页)能否合并到一起.
.pg_test = filelayout_pg_test,
// 这是file layout中读数据的函数
.pg_doio = pnfs_generic_pg_readpages,
};
根据前面几篇文章的讲解,我们知道客户端在发起READ请求前需要先通过LAYOUTGET和GETDEVICEINFO获取文件的layout信息。按照RFC5661的规定,layout中只保存了文件中部分数据的信息,比如我想读取文件前4096字节的数据,那么我只需要获取前4096字节的layout就可以了,因此一个文件可以包含多个layout。在NFS客户端,这些layout保存在文件索引节点struct nfs_inode中。
struct nfs_inode {
......
struct pnfs_layout_hdr *layout; // 这是与layout相关的一个字段,保存了这个文件中所有layout的信息。
......
};
struct pnfs_layout_hdr中包含了一个链表,链表中链接了客户端获取的所有layout信息。
struct pnfs_layout_hdr {
......
struct list_head plh_segs; // 这是一个链表,链表中的数据结构是struct pnfs_layout_segment
...... // 每个pnfs_layout_segment表示一个layout.
};
对于file layout而言,pnfs_layout_segment包含在一个更大的数据结构struct nfs4_filelayout_segment中,这个结构才保存了所有的数据。
struct nfs4_filelayout_segment {
struct pnfs_layout_segment generic_hdr; // 这是一个通用字段,包含了layout中数据在文件中的范围.
u32 stripe_type; // 这是stripe类型 STRIPE_SPARSE 或者 STRIPE_DENSE
u32 commit_through_mds; // COMMIT请求是提交给MDS还是提交给DS.
u32 stripe_unit; // stripe unit 大小
u32 first_stripe_index; // first stripe index
u64 pattern_offset; // 偏移位置
// 这里保存了设备的信息,是GETDEVICEINFO请求的结果. 这里保存了所有相关的DS的信息.
// 包括stripe和ds class的对应关系.
struct nfs4_file_layout_dsaddr *dsaddr; /* Point to GETDEVINFO data */
unsigned int num_fh; // 文件句柄数量
struct nfs_fh **fh_array; // 这里保存的是文件句柄,不同的DS可能使用不同的文件句柄.
};
前面的文章中讲解过这个结构中每个字段的含义了。目前的代码实现中,客户端不允许只获取与读操作相关部分的layout,必须获取整个文件的layout,因此pnfs_layout_hdr中只包含一个pnfs_layout_segment结构。这只是代码实现问题,毕竟pnfs代码正在开发过程中,相信以后会根据读/写数据量获取文件部分内容的layout。
由于客户端可以向多个DS发起I/O请求,因此读操作中的第一件事情就是将请求的数据进行分组,不同DS中的数据不能分到同一个分组中,这个分组工作是在结构struct nfs_pageio_ops中的pg_test中完成的。没有使用pNFS机制时,pg_test是bool nfs_generic_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, struct nfs_page *req),这个函数包含三个参数,参数pgio中包含一个链表,这个链表中保存的是nfs_page结构,这些nfs_page中包含的数据在文件中是连续的,可以向服务器发送一个READ请求获取这些数据。如果链表中的数据超过了RPC的限制,则可能会发起多个READ请求。因为READ请求中只包含请求的数据在文件中的偏移量和请求的数据量两个数据,因此pgio中的数据必须连续。参数prev是这个链表中最后一个nfs_page结构,参数req表示一个新的nfs_page请求,这个函数主要在检查req和prev中的数据在文件中是否连续,如果连续返回true,表示这个新的nfs_page也可以添加到pgio中。当使用pNFS机制时,pg_test除了检查数据在文件中是否连续,还需要检查prev和req是否对应同一个DS,这个函数是filelayout_pg_test(),这个函数如下:
static bool
filelayout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev,
struct nfs_page *req)
{
u64 p_stripe, r_stripe;
u32 stripe_unit;
// 主要检查数据在文件中是否连续.
if (!pnfs_generic_pg_test(pgio, prev, req) ||
!nfs_generic_pg_test(pgio, prev, req))
return false;
// prev中数据在文件中的偏移量
p_stripe = (u64)req_offset(prev);
// req中数据在文件中的偏移量
r_stripe = (u64)req_offset(req);
// 这是pNFS中向每个DS发送的数据块大小.
stripe_unit = FILELAYOUT_LSEG(pgio->pg_lseg)->stripe_unit;
do_div(p_stripe, stripe_unit); // p_stripe = p_stripe / stripe_unit
do_div(r_stripe, stripe_unit); // r_stripe = r_stripe / stripe_unit
// 检查req和prev是否位于同一个stripe中,如果在同一个stripe中就可以一起请求数据.
return (p_stripe == r_stripe);
}
通过代码我们可以看到,req和prev中的数据不仅需要从同一个DS中请求,还需要保证属于同一个stripe,因为pNFS中按照stripe发起I/O请求。接下向下看,客户端会根据读操作中RPC报文长度限制,创建多个struct nfs_read_data结构,每个结构需要发起一个READ请求,从DS中请求一些数据。pNFS中,这些请求的处理函数是pnfs_do_multiple_reads(),对应于没有使用pNFS机制时的函数nfs_do_multiple_reads()。
参数desc: 这里包含了READ操作中的一些信息
参数head: 这是一个链表,链表中的数据结构是struct nfs_read_data,每个nfs_read_data需要发起一个READ请求。
static void
pnfs_do_multiple_reads(struct nfs_pageio_descriptor *desc, struct list_head *head)
{
struct nfs_read_data *data;
const struct rpc_call_ops *call_ops = desc->pg_rpc_callops;
// 这里面包含了layout的信息.
struct pnfs_layout_segment *lseg = desc->pg_lseg;
desc->pg_lseg = NULL;
while (!list_empty(head)) {
enum pnfs_try_status trypnfs;
// 取出第一个nfs_read_data结构.
data = list_first_entry(head, struct nfs_read_data, list);
list_del_init(&data->list);
// 处理这个请求 这个函数会向DS发起一个READ请求
trypnfs = pnfs_try_to_read_data(data, call_ops, lseg);
if (trypnfs == PNFS_NOT_ATTEMPTED)
pnfs_read_through_mds(desc, data);
}
// 释放一个pnfs_layout_segment结构
pnfs_put_lseg(lseg);
}
这个函数逻辑很简单,就是遍历链表中每个结点,依次执行pnfs_try_to_read_data()函数,这个函数会向DS请求数据。pnfs_try_to_read_data()很简单,代码如下:
static enum pnfs_try_status
pnfs_try_to_read_data(struct nfs_read_data *rdata,
const struct rpc_call_ops *call_ops,
struct pnfs_layout_segment *lseg)
{
struct nfs_pgio_header *hdr = rdata->header;
struct inode *inode = hdr->inode;
struct nfs_server *nfss = NFS_SERVER(inode);
enum pnfs_try_status trypnfs;
hdr->mds_ops = call_ops;
dprintk("%s: Reading ino:%lu %u@%llu\n",
__func__, inode->i_ino, rdata->args.count, rdata->args.offset);
// 这是实际请求数据的函数.
trypnfs = nfss->pnfs_curr_ld->read_pagelist(rdata);
if (trypnfs != PNFS_NOT_ATTEMPTED)
nfs_inc_stats(inode, NFSIOS_PNFS_READ);
dprintk("%s End (trypnfs:%d)\n", __func__, trypnfs);
return trypnfs;
}
这个函数直接调用了pNFS中的read_pagelist函数,在file layout中这个函数是static enum pnfs_try_status filelayout_read_pagelist(struct nfs_read_data *data),这是读操作的实际处理函数。filelayout_read_pagelist()主要需要完成三项工作:
(1)确定data中的数据从哪个DS获取,也就是说向哪个DS发送READ请求。因为到目前为止,我们只保证了data中的数据属于同一个stripe,但是还不确定应该从哪个DS获取数据。
(2)在NFS客户端和这个DS之间建立RPC通道。GETDEVICEINO请求只是获取了所有DS的地址信息,但是并没有在客户端和DS之间建立RPC连接,只有建立RPC连接后才能向DS发送READ请求。
(3)向DS发起READ请求,获取数据。
这个函数代码如下:
static enum pnfs_try_status
filelayout_read_pagelist(struct nfs_read_data *data)
{
struct nfs_pgio_header *hdr = data->header;
// 这是前面获取的layout信息,我们要请求的数据属于这个layout.
struct pnfs_layout_segment *lseg = hdr->lseg;
struct nfs4_pnfs_ds *ds;
// 我们请求的数据在文件中的偏移量
loff_t offset = data->args.offset;
u32 j, idx;
struct nfs_fh *fh;
int status;
dprintk("--> %s ino %lu pgbase %u req %Zu@%llu\n",
__func__, hdr->inode->i_ino,
data->args.pgbase, (size_t)data->args.count, offset);
/* Retrieve the correct rpc_client for the byte range */
// 根据文件偏移量计算stripe编号
j = nfs4_fl_calc_j_index(lseg, offset);
// 这个函数根据stripe编号计算DS组编号
idx = nfs4_fl_calc_ds_index(lseg, j);
// 因为DS组中所有DS完全等效,因此从这个DS组中随便挑选一个DS出来,
// 并且在NFS客户端和这个DS之间建立RPC通道.
ds = nfs4_fl_prepare_ds(lseg, idx);
if (!ds)
return PNFS_NOT_ATTEMPTED;
dprintk("%s USE DS: %s cl_count %d\n", __func__,
ds->ds_remotestr, atomic_read(&ds->ds_clp->cl_count));
/* No multipath support. Use first DS */
atomic_inc(&ds->ds_clp->cl_count);
// ds->ds_clp就是nfs4_fl_prepare_ds()中建立的RPC客户端,NFS客户端使用这个RPC客户端
// 向DS发送READ请求.
data->ds_clp = ds->ds_clp;
fh = nfs4_fl_select_ds_fh(lseg, j); // 挑选READ操作中使用的文件句柄.
if (fh)
data->args.fh = fh;
// 计算请求的数据在这个DS中的偏移量, spare和dense的处理方式不同.
data->args.offset = filelayout_get_dserver_offset(lseg, offset);
data->mds_offset = offset;
/* Perform an asynchronous read to ds */
// 向DS发起READ请求.
status = nfs_initiate_read(ds->ds_clp->cl_rpcclient, data,
&filelayout_read_call_ops, RPC_TASK_SOFTCONN);
BUG_ON(status != 0);
return PNFS_ATTEMPTED;
}
这个函数挑选DS的过程就是按照RFC5661中的规定计算的,可以参考前面讲解file layout的这篇文章:
http://blog.csdn.net/ycnian/article/details/8719051。nfs4_fl_prepare_ds()的作用是创建与DS通信的RPC客户端,这就是一个普通的创建RPC客户端的过程,也没有特别需要讲解的内容。有一点需要注意:nfs4_fl_prepare_ds()创建好RPC客户端后会马上向DS发起EXCHANGE_ID和CREATE_SESSION请求,建立与DS之间的状态。最后,filelayout_read_pagelist()就可以调用nfs_initiate_read()向DS发起READ请求了。