delegation是NFSv4中保持文件同步的一种机制。当NFS客户端持有delegation时就认为自己和服务器保持同步了。多个客户端可以持有同一个文件的delegation,比如多个客户端以只读方式打开了同一个文件,这种情况下服务器为每个客户端分发一个delegation。这篇文章中我们讲讲delegation的分发过程。服务器端delegation的数据结构如下:
struct nfs4_delegation {
// 这是一个nfs4_stid结构.
struct nfs4_stid dl_stid; /* must be first field */
// 同一个文件中的delegation构成了一个链表,链表头是nfs4_file->fi_delegations
// dl_perfile指向了链表中相邻的元素
struct list_head dl_perfile;
// 同一个NFS客户端中的delegation构成了一个链表,链表头是nfs4_client->cl_delegations
// dl_perclnt指向链表中相邻的元素
struct list_head dl_perclnt;
// 系统中定义了一个LRU链表del_recall_lru,
// nfsd_break_one_deleg()会将nfs4_delegation挂载到del_recall_lru中.
struct list_head dl_recall_lru; /* delegation recalled */
// delegation的引用计数
atomic_t dl_count; /* ref count */
// 这个delegation对应的文件
struct nfs4_file *dl_file;
// 为什么不添加一个字段呢
// struct nfs4_client *dl_clnt;
u32 dl_type; // delegation类型,读还是写,目前只支持写.
// 这是nfs4_delegation的过期时间,当把nfs4_delegation挂载到del_recall_lru中时,设置这个时间.
time_t dl_time;
struct knfsd_fh dl_fh; // 这是delegation对应文件的文件句柄
int dl_retries; // 这是CALLBACK请求的重试次数
struct nfsd4_callback dl_recall; // 这个结构和CALLBACK过程相关
};
nfs4_stid是nfs4_ol_stateid和nfs4_delegation共同使用的数据结构,这个数据的定义如下:
struct nfs4_stid {
#define NFS4_OPEN_STID 1 // 这是一个OPEN stateid
#define NFS4_LOCK_STID 2 // 这是一个LOCK stateid
#define NFS4_DELEG_STID 4 // 这是一个delegation stateid
/* For an open stateid kept around *only* to process close replays: */
#define NFS4_CLOSED_STID 8
unsigned char sc_type; // 这是type
stateid_t sc_stateid; // 这是stateid
struct nfs4_client *sc_client;
};
delegation是在OPEN请求中分发的,服务器端创建delegation的函数是nfs4_open_delegation(),这个函数的代码如下:
参数net:网络命令空间
参数fh:这是OPEN操作中打开文件的文件句柄
参数open:这里保存了OPEN操作相关的数据
参数stp:表示本次OPEN操作过程
nfsd4_open --> nfsd4_process_open2 --> nfs4_open_delegation
static void
nfs4_open_delegation(struct net *net, struct svc_fh *fh,
struct nfsd4_open *open, struct nfs4_ol_stateid *stp)
{
struct nfs4_delegation *dp;
// 找到nfs4_openowner结构
struct nfs4_openowner *oo = container_of(stp->st_stateowner, struct nfs4_openowner, oo_owner);
int cb_up;
int status = 0, flag = 0;
// 检查CALLBACK通道是否正常,因为delegation需要使用CALLBACK通道。
// oo->oo_owner.so_client的结构类型是nfs4_client,表示一个NFS客户端
cb_up = nfsd4_cb_channel_good(oo->oo_owner.so_client);
flag = NFS4_OPEN_DELEGATE_NONE; // 表示不需要创建delegation.
open->op_recall = 0;
switch (open->op_claim_type) {
case NFS4_OPEN_CLAIM_PREVIOUS:
if (!cb_up)
open->op_recall = 1;
flag = open->op_delegate_type;
if (flag == NFS4_OPEN_DELEGATE_NONE)
goto out;
break;
case NFS4_OPEN_CLAIM_NULL: // 正常打开一个文件时执行的是这个分支
/* Let's not give out any delegations till everyone's
* had the chance to reclaim theirs.... */
if (locks_in_grace(net))
goto out;
// 如果CALLBACK通道不连通,或者这是用户第一次执行OPEN操作,
// 就不能创建delegation了。
if (!cb_up || !(oo->oo_flags & NFS4_OO_CONFIRMED))
goto out;
// 根据OPEN请求的访问权限确定delegation的类型
if (open->op_share_access & NFS4_SHARE_ACCESS_WRITE)
flag = NFS4_OPEN_DELEGATE_WRITE; // 写类型的delegation
else
flag = NFS4_OPEN_DELEGATE_READ; // 读类型的delegation
break;
default:
goto out;
}
// 创建一个新的delegation.
dp = alloc_init_deleg(oo->oo_owner.so_client, stp, fh, flag);
if (dp == NULL)
goto out_no_deleg; // 分配内存失败了
// 设置新创建的delegation了. Linux中采用租借锁实现delegation,这个函数在创建租借锁.
status = nfs4_set_delegation(dp, flag); // 设置delegation
if (status)
goto out_free; // 设置delegation的过程可能失败.
// 需要将delegation的stateid返回给NFS客户端.
memcpy(&open->op_delegate_stateid, &dp->dl_stid.sc_stateid, sizeof(dp->dl_stid.sc_stateid));
dprintk("NFSD: delegation stateid=" STATEID_FMT "\n",
STATEID_VAL(&dp->dl_stid.sc_stateid));
out:
open->op_delegate_type = flag;
if (flag == NFS4_OPEN_DELEGATE_NONE) {
if (open->op_claim_type == NFS4_OPEN_CLAIM_PREVIOUS &&
open->op_delegate_type != NFS4_OPEN_DELEGATE_NONE)
dprintk("NFSD: WARNING: refusing delegation reclaim\n");
/* 4.1 client asking for a delegation? */
if (open->op_deleg_want)
nfsd4_open_deleg_none_ext(open, status);
}
return;
out_free:
nfs4_put_delegation(dp);
out_no_deleg:
flag = NFS4_OPEN_DELEGATE_NONE;
goto out;
}
delegation的创建过程分为四个步骤:
步骤1:检查反向通道是否连通
因为delegation需要使用反向通道,所以服务器创建delegation直线需要先检查反向通道是否连通(nfsd4_cb_channel_good),如果反向通道不连通就不能创建delegation了。但是也并不是只要反向通道连通就可以创建delegation。前面的文章讲过,用户第一次执行OPEN操作后需要执行OPEN_CONFIRM进行确认,这种情况下也不能创建delegation。
步骤2:确定delegation类型
根据OPEN请求中的访问权限确认delegation的类型。目前分为两种类型:NFS4_OPEN_DELEGATE_READ(读权限的delegation)和NFS4_OPEN_DELEGATE_WRITE(写权限的delegation)。
步骤3:创建一个nfs4_delegation结构
nfs4_delegation是表示delegation的数据结构,每个nfs4_delegation表示一个delegation,创建nfs4_delegation结构的函数是alloc_init_deleg()。
步骤4:设置步骤3中创建的delegation
Linux中采用租借锁实现delegation,同一个文件中所有的delegation共用同一个租借锁。如果这是第一次打开文件,则创建delegation时需要在文件上创建一个租借锁。当另一个客户端再次打开这个文件时就不需要创建租借锁了,只需要更新相应数据结构的计数就可以了。这是由函数nfs4_set_delegation实现的。
检查反向通道是否连通的函数是nfsd4_cb_channel_good(),对于NFSv4.0来说,只需要检查是否设置了标志位NFSD4_CB_UP就可以了,这个函数的代码如下:
nfsd4_open --> nfsd4_process_open2 --> nfs4_open_delegation --> nfsd4_cb_channel_good
static bool nfsd4_cb_channel_good(struct nfs4_client *clp)
{
if (clp->cl_cb_state == NFSD4_CB_UP)
return true;
/*
* In the sessions case, since we don't have to establish a
* separate connection for callbacks, we assume it's OK
* until we hear otherwise:
*/
return clp->cl_minorversion && clp->cl_cb_state == NFSD4_CB_UNKNOWN;
}
创建新的nfs4_delegation结构的函数是alloc_init_deleg(),这个函数的代码如下:
参数clp:这是一个NFS客户端的结构
参数stp:表示本次OPEN过程
参数current_fh:本次OPEN过程打开的文件
参数type:delegation的类型
nfsd4_open --> nfsd4_process_open2 --> nfs4_open_delegation --> alloc_init_deleg
static struct nfs4_delegation *
alloc_init_deleg(struct nfs4_client *clp, struct nfs4_ol_stateid *stp, struct svc_fh *current_fh, u32 type)
{
struct nfs4_delegation *dp; // 这是nfs4_delegation结构的指针
struct nfs4_file *fp = stp->st_file; // 这个stateid所属于的文件
dprintk("NFSD alloc_init_deleg\n");
/*
* Major work on the lease subsystem (for example, to support
* calbacks on stat) will be required before we can support
* write delegations properly.
*/
// Linux现在只支持读方式的delegation
if (type != NFS4_OPEN_DELEGATE_READ)
return NULL;
// delegation发生了冲突,不能在文件上添加新的delegation了.
if (fp->fi_had_conflict)
return NULL;
// 系统中delegation的数量超出了最大值,不能再创建了.
if (num_delegations > max_delegations)
return NULL;
// 分配一个nfs4_delegation结构
dp = delegstateid(nfs4_alloc_stid(clp, deleg_slab));
if (dp == NULL)
return dp; // 内存分配失败
init_stid(&dp->dl_stid, clp, NFS4_DELEG_STID); // 初始化这个delegation的stateid.
/*
* delegation seqid's are never incremented. The 4.1 special
* meaning of seqid 0 isn't meaningful, really, but let's avoid
* 0 anyway just for consistency and use 1:
*/
// delegation中的seqid不增长,这里只是为了避开0
dp->dl_stid.sc_stateid.si_generation = 1;
num_delegations++; // delegation分配成功,增加系统中delegation的数量.
INIT_LIST_HEAD(&dp->dl_perfile); // 同一个文件中的delegation构成了一个链表
INIT_LIST_HEAD(&dp->dl_perclnt); // 同一个NFS客户端的delegation构成了一个链表
// 用于将delegation添加到lru链表del_recall_lru中
INIT_LIST_HEAD(&dp->dl_recall_lru); // recall
get_nfs4_file(fp); // 增加nfs4_file结构的引用计数
dp->dl_file = fp; // 这是delegation执行的文件
dp->dl_type = type; // delegation的类型
fh_copy_shallow(&dp->dl_fh, ¤t_fh->fh_handle); // 这是文件的句柄
dp->dl_time = 0; // 最后更新时间
atomic_set(&dp->dl_count, 1); // 初始化引用计数为1
// 初始化这个工作结构,这个工作的处理函数是nfsd4_do_callback_rpc
// 当发起RECALL请求时调用这个函数.
INIT_WORK(&dp->dl_recall.cb_work, nfsd4_do_callback_rpc);
return dp;
}
关于alloc_init_deleg()有几点需要说明:
1.Linux系统只支持读方式的delegation,不支持写方式的delegation,因为几个用户同时写一个文件不好保持文件同步,因此当检测到delegation的类型是NFS4_OPEN_DELEGATE_READ后,函数直接推出了。
2.创建delegation前alloc_init_deleg检查了文件访问过程是否发生了冲突。如果发生了冲突就不能创建delegation了,检查条件是
if (fp->fi_had_conflict)
return NULL;
这是什么意思呢?假设客户端A先以只读方式打开了文件file1.txt,服务器为客户端A创建了delegation。现在客户端B同样以只读方式访问文件file.txt,按理说服务器应该为客户端B创建一个delegation。但是假设客户端C同时以写权限访问file1.txt,这种情况下文件访问过程就发生了冲突,服务器会将nfs4_file结构中的fi_had_conflict设置程true。显然,这种情况下就没必要给客户端B创建delegation了。
3.Linux定义了系统中delegation的最大数量,用max_delegations表示,同时用num_delegations表示系统中已经创建的delegation的最大数量。alloc_init_deleg检查了系统中delegation的数量
if (num_delegations > max_delegations)
return NULL;
但是这里有个问题。当num_delegations和max_delegations相等后,再加上新创建的这个delegation就超出了max_delegations的限制,正确的判断条件是
if (num_delegations >= max_delegations)
return NULL;
当然这是一个小bug,对系统不会造成任何影响。
设置delegation的函数是,这个函数的代码如下:
参数dp:这是步骤3中新创建的delegation
参数flag:这是delegation的类型
nfsd4_open --> nfsd4_process_open2 --> nfs4_open_delegation --> nfs4_set_delegation
static int nfs4_set_delegation(struct nfs4_delegation *dp, int flag)
{
struct nfs4_file *fp = dp->dl_file;
// 如果文件中没有锁结构,表示这是第一次打开文件,需要创建一个新的租借锁.
if (!fp->fi_lease)
return nfs4_setlease(dp, flag);
// 文件不是第一次打开,已经设置租借锁了
spin_lock(&recall_lock);
if (fp->fi_had_conflict) { // 访问方式发生了冲突
spin_unlock(&recall_lock);
return -EAGAIN;
}
// 由于文件中增加了一个delegation,需要增加租借锁的引用计数
atomic_inc(&fp->fi_delegees);
// 将新创建的delegation链接到该文件中.
// fp->fi_delegations是一个链表,保存了这个文件中所有的delegation.
list_add(&dp->dl_perfile, &fp->fi_delegations);
spin_unlock(&recall_lock);
// 将新创建的delegation链接到所属的客户端中.
// dp->dl_stid.sc_client->cl_delegations是一个链表,保存了这个客户端中所有的delegation.
list_add(&dp->dl_perclnt, &dp->dl_stid.sc_client->cl_delegations);
return 0;
}
如果文件是第一次打开,则需要创建一个租借锁,然后设置delegation,这是由函数nfs4_setlease()实现的。
参数dp:新创建的delegation
参数flag:这是文件的访问权限
nfsd4_open --> nfsd4_process_open2 --> nfs4_open_delegation --> nfs4_set_delegation --> nfs4_setlease
static int nfs4_setlease(struct nfs4_delegation *dp, int flag)
{
struct nfs4_file *fp = dp->dl_file; // 取出这个delegation对应的文件
struct file_lock *fl; // 这是文件锁的数据结构
int status;
// 分配一个文件锁结构,这是一个租借锁
fl = nfs4_alloc_init_lease(dp, flag);
if (!fl)
return -ENOMEM; // 分配内存过程失败了.
// 炸找一个只读权限的文件对象结构,因为Linux只支持读权限的delegation.
fl->fl_file = find_readable_file(fp); // 这是对应的文件
// 将这个租借锁添加到NFS客户端中
list_add(&dp->dl_perclnt, &dp->dl_stid.sc_client->cl_delegations);
// 设置租借锁,这是VFS层的函数,不讲了.
status = vfs_setlease(fl->fl_file, fl->fl_type, &fl);
if (status) { // 租借锁设置过程出错了.
list_del_init(&dp->dl_perclnt);
locks_free_lock(fl);
return -ENOMEM;
}
fp->fi_lease = fl; // 这是文件关联的租借锁
fp->fi_deleg_file = fl->fl_file; // 设置文件租借锁使用的文件对象结构.
get_file(fp->fi_deleg_file); // 增加文件对象引用计数
// 设置文件中delegation的数量,现在文件上只关联了一个delegation.
atomic_set(&fp->fi_delegees, 1);
// 将delegation添加到对应的文件中.
list_add(&dp->dl_perfile, &fp->fi_delegations);
return 0;
}