深入理解Lustre文件系统-第5篇 LDLM:锁管理者

Lustre锁管理者的基本思想来自于VAXDLM。在我们深入源码理解它如何工作之前,我们需要解释一些基本的概念。

5.1名字空间

我们首先涉及的概念是名字空间。当你请求锁时,你都在请求某个名字空间中的锁,每个Lustre服务都定义了一个名字空间。为了在一个实际的情景中讨论这个问题,假设你的Lustre文件系统有十个OST,从LDLM的观点看,有十个名字空间。另外,MDS和MGS各自有它们的名字空间。Lustre中的名字空间由结构体ldlm_namespace定义。在源码中,对于许多字段有适量的注释,所以我们只聚焦于其中不大显而易见的几个。

类型

字段

描述

ldlm_side_t

ns_client

这是否是一个客户端锁树?

struct list_head

*ns_has

在本名词空间中所有资源的哈希表

__u32

ns_refcount

在本名词空间中的资源数

struct list_head

ns_list_chain

所有名字空间的环状链表

struct list_head

ns_unused_list

未上锁的资源

int

ns_nr_unused

未使用的资源数

atomic_t

ns_locks

本名字空间中的锁数目

__u64

ns_resources

本名字空间中的资源数

ldlm_res_policy

ns_policy

请求一个意图锁时的回调函数

Figure 7: The fields of ldlmnamespace data structure.

对于ns_client还有一些需要注意的地方:每个客户端只需要访问一个名字空间中的一部分,而不是所有。所以,每个客户端都使用一个所谓的影子名字空间(shadow namespace)。ns_client标识着这个名字空间是一个仅供客户端使用的名字空间,并不完整(参看Figure 8)。ns_hash是一个在这名字空间中的所有资源的哈希表。注意,这只是一个指向链表的指针,而不是链表本身。

深入理解Lustre文件系统-第5篇 LDLM:锁管理者

5.2 资源

锁是用来保护资源的。在Lustre中,最常见的资源就是文件。本地锁就是指私有的只被本地实体所知的锁。对应的,全局锁就是对他人可见的锁。注意这里的“全局”可能是与其字面意义不符——它并不表明锁被广播至为所有实体所知。在Lustre中,它意味着你保存了一个锁的客户端复本,而服务器也可以通过客户端请求取得一个复本。

所有资源都定义在ldml_resource结构体中。关于这个结构体的一些要点在下面讨论。

struct ldlm_namespace *lr_namespace指回这个资源所属的名字空间。如果资源是一个对象ID(oid),那么它的名字空间是所对应的OST。如果它是fid,那么它的名字空间由MDS定义。

struct list_head lr_granted, lr_converting, lr_waiting这里定义了三个链表来对各种的锁和它们的状态进行归类,这些锁请求都对是当前的资源发出的。批准链表(granted list)是已经准许在该资源上使用的所有锁。等待链表(Waiting list)是正在请求该资源,但是由于冲突而需要等待所有锁。转换链表(Converting list)是处在转换状态所有锁。

struct ldlm_res_id lr_name资源的名字定义在一个结构体中,该结构体是4*64比特长的标识符。所有类型的资源,不管是fid还是oid,都要转换成这种格式,作为它们的一般性的资源名。

5.3 锁类型和模式

一个锁有其类型和模式。首先,我们探讨模式。存在六种锁模式,并且使用一个兼容性矩阵来标示两个锁是否兼容。六种模式如下:

  • EX独占模式(Exclusive mode)。在新文件创建前,MDS请求对父亲上EX锁。
  • PW保护写模式(Protective Write(正常写)mode)。当客户端从OST中请求写入,将分配一个PW模式的锁。
  • CW并行写模式(Concurrent Write mode)。如果客户端在文件打开操作期间请求写锁,MDS将批准该模式的锁。
  • CR并行读模式(Concurrent Read mode)。如果客户端执行路径查找,MDS将对中间(intermediate)路径部件批准一个CR模式inodebit类型的锁。
  • NL空模式(Null mode)。

相应的矩阵如Figure 9所示。

在Lustre中定义了四类锁,使用哪一类取决于客户端决定请求哪一类锁。从锁管理者中请求锁的部件是客户端,客户端可能是Lustre Lite、OSC或者MDC。这四类锁在下面给出。

extent锁用来保护OST数据,由ldlm_extent结构体定义。

flock用来协助用户空间请求flock,由ldlm_flock结构体定义。

索引节点比特锁(inode bit lock)用来保护元数据属性,由ldlm_inodebits结构体定义。

plain锁已被定义了,但是不被使用,可以忽略

深入理解Lustre文件系统-第5篇 LDLM:锁管理者

5.4 回调

与锁请求相关的另一个概念是回调函数。当创建锁时,客户端能提供三种回调:

阻塞型回调(blocking callback)。这种回调被调用的情况有两种。第一种是有客户端请求一个与当前锁相冲突的锁(即使请求发自同一个客户端),那么这个回调被调用,所以如果客户端为人友好,而这个锁又没用了,它可以释放锁,让他人使用。第二种情况是当锁被撤销时(在所有的引用都撤销了,而锁也被撤销之后)。

完成型回调(completion callback)。这种回调被调用也有两种可能。第一种,锁请求被批准。第二种,锁被转化了,例如转化为另一种模式的锁。

惊鸿一瞥型回调(glimpse callback)。这种回调用来提供某些基本性质的信息,而不用实际上释放锁。例如,由于只有OST知道文件对象的确切大小,OST可以提供一个取得文件大小信息的回调。然后,回调将与文件对象上的extent锁相关联,并由服务端在接收到客户端请求时调用。

5.5 意图

意图是在锁入队列(enqueue)时,提供的一些数据,用来表明在锁入队列过程中需要做的特殊处理,而数据本身则是做该处理的参数。名字空间可以为这个处理定义不同的意图处理函数(intent handler)。

意图的特性允许了调用namei,也允许了传递一些关于它们最终想实现什么的信息的前瞻(lookup)。意图的网络效应是减少了与MDS交互时的RPC数目。

意图的定义如下所示(inode.h):

struct intent {

unsigned int_opmask;

void * int_arg1;

void * int_arg2;

};

定义了六种意图操作(编码在int_opmask中),包括:取得属性、设置属性、插入或删除、打开、创建和读链接。

5.6 锁管理者

现在,让我们给出在Lustre中锁算法(由锁管理者完成)如何工作的一般性描述。进入Lustre分布式锁管理者(Lustre Distributed LockManager, LDLM)的入口,会根据Lustre版本的不同而改变,我们参考的是1.6分支的源码。LDLM所能提供的两种主要类型的服务是:第一种,是请求锁;第二种,是撤销锁。

5.6.1请求锁

1.对于锁服务客户端,不管是Lustre客户端、MDS还是OST,通常以调用ldlm_cli_enqueue()开始。该函数首先通过检查名字空间结构体中的ns_client标志来考查锁请求是否属于本地名字空间。记住,每个LDLM实例都定义了各自的名字空间。如果是本地的,这意味着我们不需要发送RPC来通信,只需跳转到7;否则,我们继续远程处理锁。

2. 如果锁请求是非本地的,那么我们需要向运行LDLM的服务器发送锁入队列RPC;这是在ldlm_cli_enqueue()里完成的。在服务器端,ldlm_handle_enqueue()首先解包锁请求,然后创建一个锁(ldlm_lock_create())。这个锁只是一个从原始锁请求中填充了一些字段的初始锁,暂未被批准。你可以通过如下方法判断一个锁是否被批准:

if(lock->req_mode == lock->granted_mode) { /* granted */

...

}

现在我们进入下一步,看看锁是否能被批准,而哪些锁又应当被批准。

3. ldlm_lock_enqueue()是批准锁的核心步骤。它检查是否设置了锁意图。如果没有设置意图,则检查锁的类型,并调用该类型定义的策略函数。策略函数将决定锁请求是否被批准。

如果设置了锁意图,跳到第6步。

4. 对于锁所请求的资源,需要检查如下两个条件:

  • 是否存在和已批准的锁之间的冲突?
  • 是否存在和等待锁之间的冲突?

如果两个判断的回答都是不,那么就没找到冲突,而锁可以被批准。调用完成AST(completionAST),返回被批准的锁,而我们就完事啦。否则,继续。

5. 对每个冲突的锁,我们需要调用阻塞AST(blocking AST)。阻塞AST检查到如果锁由服务器占有,则设置一个标志。如果锁由客户端占有,则需要发送一个阻塞RPC请求。不管哪种情况,在所有这些都完成后,将新的锁请求放入等待链接,然后返回状态为阻塞的锁。

稍后将给出更具细节的策略函数实现。

6. 如果锁意图已经设置了,那么所有需要做的事实调用意图处理函数。这里关键之处是LDLM并不解释意图。那些以意图来互相通信的实体才解释意图。例如,MDS需要注册它的意图处理函数,而OST需要注册它的意图处理函数。一般地,意图处理函数是通过调用ldlm_register_intent()来按名字空间注册的。LDLM负责调用它们。意图处理函数决定锁是否可以被批准。LDLM只是返回它们的裁决而已。

7. 对于本地锁请求,它进入一个不同的分支ldlm_cli_enqueue_local(),在这种情况下,它不需要再发送RPC了。它需要通过如上所述的两步:首先创建一个锁,然后调用ldlm_lock_enqueue()来检查这个锁是否可以被批准。如果锁被批准或者出现了错误,则对应地标记锁并立即返回。否则,锁请求被阻塞,需要等待它。

请注意,服务器(即MDS或者OST)可以通过直接调用ldlm_cli_enqueue_local()来初始化一个锁请求,因为他们知道锁必然是被本地LDLM服务器占有。

5.6.1撤销锁

锁通常是被非自愿地释放的,占有者会尽可能地长时间占有锁,直到:某人在请求一个与之冲突的锁,LDLM发出一个阻塞AST,从而引发LDLM客户端调用阻塞AST处理函数。现在,我们进入撤销过程。在撤销过程中,锁里有三个相关的计数器。可以列为:(1)活跃的读的数目,(2)活跃的写的数目,和(3)用户数目。

1. 撤销锁的入口是ldlm_cli_cancel()。这个函数首先检查活跃的读和活跃的写的总和是否为零。如果不是,则意味着在同一个客户端的另一个进程在使用该锁,所以,不做任何事情。最终,其他的客户端将释放锁,通过同样的源码路径,到达这个检查点,然后进入下一步。

2. 现在,在读和写数总和为零时,我们调用阻塞AST,其中设置了一个标志,表明锁被撤销了。

3. 检查锁时否处于本地名字空间中。如果不是,发送撤销RPC到客户端(Lustre客户端)。如果是,服务器端调用ldlm_handle_cancel()来撤销锁。

撤销操作实质上牵涉到将锁从所有身处的链表中移除(谨慎怀疑原文的of应为off——译者注),例如批准链表、等待链表等等。接着,将调用obd_cancel(),并不是来撤销锁,而只是撤销对读和写计数的引用。

4. 现在锁已经被撤销了,服务器能过重新处理所有等待该资源的锁。实际上,这与确认它们的锁请求是否可以被批准,所经过的逻辑是一样的。

5. 如果一个等待的锁请求可以被批准,则将它移至批准锁链表,然后调用完成AST。

5.6.2策略函数

正如我们在请求锁一节(4.6.1)所提及的,策略函数是用来根据锁类型,决定请求锁是否和已有请求冲突的函数。我们有四种锁类型:inodebits、extent、flock和plain。所以我们需要四个策略函数。它们的整体流程类似,只有一些微小变化。在此节中,我们给出对整体流程的整体描述。

1. 给出了两个参数、锁请求和一个first_enq标志。这个标识是用来标志我们是否第一次进行入队列请求。如果是第一次,需要处理请求链表,并在需要的时候发送阻塞AST。如果不是第一次,我们已经在先前发送了阻塞AST,所以不需要再发送了。

关于first_enq参数的另外一个重要的事情是:当它没有被设置时(这意味着我们正在再次处理已在等待链表中的锁),在我们在链表中找到自己的先前入口后,就停止处理。由于之后的锁时在稍后时间里入队的,所以可以安全地将他们忽略。

2. 策略函数调用了一个帮助函数来做实际工作,向它传递的参数包括锁链表、入队标志和RPC链表(要么是NULL要么是一个空链表)。第一次时,帮助函数以批准队列为参数被调用。对链表中的每个锁,执行如下的冲突检测:

(a) 通常以模式检查作为开始。如果两种锁是可兼容的(参考前面章节中的兼容矩阵),那么不要进一步的检查了,锁可以被批准。如果锁是不兼容的,我们才继续,锁类型不应当是plain类型的,因为plian锁并没有实际使用。

(b) 对于extent类型锁,如果间隔或者范围并未交叉,那么并不存在冲突。但是在分条大小的限制之内,LDLM经常尝试尽其所能地批准最大的extent,目的是减少客户端将来为要求更多的锁而产生的请求。

(c) 对于inodebits类型锁,如果请求的比特(Lustre里有一个64比特的空间,但是只用了3个)重叠了,那么存在冲突。

如果RPC链表为空,我们终止,并在发现第一个冲突之后立即返回。因为通过传递NULL,我们得到了所有调用者所需的信息。

如果PRC链表不为空,我们需要继续检查余下的锁,并将冲突锁加入到RPC链表中。

3. 如果没有发现冲突,那么锁被批准。否则,对每个RPC链表中的锁,我们调用阻塞AST。

4. 如果设置了首次入队列标志,那么回到第2步,但是此时,我们调用帮助函数时,锁链表则设置为等待链表,而不是批准链表。

5.7 使用情形

在这一节中,我们运用实例给出处理锁请求的高层概览。

MDS:一个客户端读

让我们假设客户端C1想要打开文件/d1/d2/foo.txt来读。在VFS路径查找过程中,将会调用Lustre特有的查找流程(参看Lustre Lite一节的细节)。第一个RPC请求是包含查找意图的锁入队列请求。为对d1上锁,这个请求发送到MDS。第二个RPC请求也是发送到MDS的,请求以inodebits锁锁住d2的,包含查找意图的锁入队列请求。返回的锁是一个inodebits锁,而它的资源可以用的d1和d2的fid表示。

这里需要注意的细节是,当我们请求锁时,对于我们请求的锁,我们一般需要资源ID。但是,在这种情况下,由于我们不知道d1的资源ID,我们实际对它的父亲“/”请求上锁,而不是d1本身。在意图中,我们指定它为一个查找意图,而查找的名字为d1。然后,当锁被返回时,锁是对d1上的。这个锁是(或者说,可能是)与客户端请求的锁有所不同的,客户端察觉了这种不同,并将旧的请求的锁替换为新的返回的锁。

第三个RPC请求是一个包含打开意图的lock_enqueue,但是并不请求对foo.txt上锁。这样,你能打开并读取一个文件,而不用从MDS上锁,因为其内容是由OST提供的。从OST请求锁有些不同,这将在稍后讨论。

换句话说,在打开过程中发生的事情是:我们发送了一个lock_request,亦即我们确实向LDLM服务器中请求了一个锁。但是在意图数据本身里,我们可能(也有可能不)设置一个特别的标志,如果我们实际上是对回收到的锁感兴趣。而意图处理函数决定(根据这个标识)是否返回这个锁。

如果foo.txt在先前已经存在了,那么将返回它的fid、索引节点上下文(拥有者、组、模式、创建时间、访问时间、修改时间、链接数等)和分条信息。

如果客户端C1以O_CREAT标志打开文件,而文件又不存在,那么第三个RPC请求将以包含打开和创建意图的方式发送,但是仍然不会有锁请求。现在在MDS端,为了在d2目录下创建一个foo.txt文件,MDS将要通过LDLM来对其父文件夹请求另外一个EX锁。注意这是一个和先前对d2上的CR锁相冲突的锁请求。在正常的情况下,第四个RPC请求(阻塞AST)将返回客户端C1或者其他占有了冲突锁的其他人,告知客户端某人正在请求一个冲突的锁,并向其请求一个锁撤销。MDS等待,直到得到一个客户端发来的撤销RPC后。只有等MDS得到先前请求的EX锁之后,才能继续。

如果客户端C1以LOV_DELAY标志打开文件,MDS如常创建文件,但是并不会分条,也不会分配对象。用户将调用ioctl调用,设置分条信息,然后MDS将填充EA结构体。

MDS:两个客户端

如果C1想要在文件夹/d1/d2下创建一个文件foo.txt,那么不会对foo.txt请求上锁,而是请求对d2上锁,该锁一个意图,意图中告知MDS“打开并创建”文件foo.txt,并有可能告知MDS返回对该文件的锁。

通过意图,得到了一个对foo.txt所上的CW锁。现在C2插入一脚,想要在/d1/d2下创建一个新文件夹d3。在此时,as far as the CW lock isconcerned(?),不会有任何的变化。

OST:两个客户端读和写

在MDS填充EA结构体后,客户端拿到了文件句柄和分条信息。客户端能够与OST进行交流了。假设有四个OST和两个客户端。客户端C1读取文件A,而第二个客户端C2写入文件A。我们进一步假设C1想要读取数据对象,A1、A2、A3和A4。

首先,客户端C1并行地向OST发送锁入队列请求1到4,请求已设置意图标志的读锁。这意味着,如果客户端C请求的任意一个对象,有阻塞请求,就不会尝试去取得锁,而是返回由称为lvb(Lustre ValueBlock)的数据结构所描述的信息(文件大小,修改时间等)(锁请求是惊鸿一瞥型,因为它设置了意图标志)。

如果不存在冲突,客户端将被批准对整个文件对象们上锁,锁的模式为PR。现在客户端占有了对四个OST文件对象的四个读锁。让我们进一步假设客户端仅想读取A1。由于它已经占有了锁,它可以接着读取之。

假设C2正在写入OST3。OST3不会返回一个PR锁,而是与C2联系,请求lvb信息。它可能以这种方式返回C1:“我没有处理你对A3上的锁,但是你看这是那个对象的状态”。

这仅是为了确定文件大小才做的,而我们需要知道文件大小,比如,我们正在尝试读取超出文件大小的部分。如果要读的内容刚好落在你已经占有了PR锁的对象上,那么不需要再发送更多的请求锁的RPC了。

现在如果客户端要读的内容在A3上,那么它别无选择,只好发送一个对OST3上锁的请求(此时不再包含意图)。在请求本身里,客户端客户端还应当讲清它请求对哪个extent上锁。如果它请求上锁的extent,不与C2占有的PW写锁相冲突(交叉读),那么读锁请求将被立即批准。

如果存在冲突,那么OST3将回复C1,告知锁请求不被批准;同时,它将向C2发送一个阻塞请求。从C2的观点看,有两种情况:

1. 如果write()系统调用已完成,即意味着所有的数据都已写入某个缓冲(也许还未写入磁盘),那么缓冲中将会设置脏标志。这种情况下,客户端将所有的脏缓冲刷新,这一操作有可能通过一个或者多个块写(bulk write)完成。然后,它通过向OST3发送取消RPC来释放锁(锁引用数现在是零)。

2. 如果write()仍然正在进行当中,那么系统调用未返回。此时,锁引用数非零,而阻塞AST将不会到达锁逻辑。只有在写系统调用结束后(这实际意味着两件事:第一,数据都保存在高速缓存中;第二,锁引用数减少到零),才能够进入上述步骤。

在OST3接到C2发来的取消请求后,它将向客户端C1发送一个完成AST,告知它的锁请求被批准了。

我们不再介绍两个客户端都读的情况,这种情况太普通,因为即使它们的extent交叉了,它们都能得到PR锁。

如果一个客户端并不愿意做好人,不愿意合作地释放它占有的锁,那么当向他发送一个阻塞AST的时候,将开启一个计时器。如果客户端不释放锁,并且在计时耗尽期间在该锁上没有正在进行的I/O,那么客户端将被驱逐。但是,如果存在正在进行的I/O,那么每个I/O RPC都会使得计时时间延长。

本文章欢迎转载,请保留原始博客链接http://blog.csdn.net/fsdev/article

你可能感兴趣的:(文件系统)