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;
}