简介
在深入translator api前,我们必须理解关于此api所在上下文的两个概念。
一个是文件系统api,主要通过分配表xlator_fops,来展露大多数文件系统功能。xlator_fops,就是组合linux vfs的file_operations, inode_operations, and super_operations三类操作在一个数组。要理解xlator如何工作,必须理解这些调用做什么,相互关系是什么,比如open/lookup/close,read/write,opendir/readdir,truncate,symlink等。
一个是xlator api的基础:是异步的并是基于回调的。意思是处理某个请求的代码必须分为两部分,一部分是在进入下一个xlator前的处理,一部分是待下一个xlator处理完成请求后。用另一种方式解说,就是你的分配函数(xlator_fops的前半部分)调用下一个xlator的分配函数,然后直接返回,并不阻塞。你的回调函数(后半部分)也许当你调用下一个xlator的分配函数时就很快被调用,或者后面某个时间从某个完全不同的线程中被调用(常常是网络传输的poll线程)。在两种情况下,回调都不能仅仅是从栈中取得上下文。Glusterfs确实提供了多种方式来在分配函数和回调函数间保持和传递上下文,但是不能仅仅依靠栈,需要我们编码处理一些事情。
分配表和默认函数
一个xlator的主要的分配表常常是fops(xlator动态库加载代码利用dlsym查找操作op名)。fops包含指向所有正常文件系统的操作的指针。仅仅需要填充在本xlator我们感兴趣的操作。任何其他的操作在运行时基于默认值来填充,并确定个回调函数,并直接把此请求发到下一个xlator。
默认函数和回调函数除了提供默认功能,也有其他目的。当我们需要添加某个新函数时到一个xlator时,我们可以拷贝和修改那些相同操作的默认函数等。这保证了正确的参数列表和合理的默认行为。每个xlator也许有另外的分配表,比如cbk表,此表用来管理inode和文件描述子的生命周期(看inode和file descriptor context部分)
STACK_WIND 和STACK_UNWIND宏
STACK_WIND and STACK_UNWIND是基于回调机制来实现的。这两个函数不作用于gdb里类似的调用栈,而是一个独立维护的栈,每个栈帧表示对xlators的调用。当某个fop从内核传递过来要调用时,这个调用就作为一个request请求,从FUSE xlator 经过DHT/AFR等到client xlator 直到server xlator 最后到posix xlator。 我们可以在用户空间fop入口点做任何需要的处理(这里应该就是fuse xlator),然后使用STACK_WIND沿着volume 文件设置的xlator 路径,传递此请求。
define STACK_WIND(frame, rfn, obj, fn, params …)
STACK_WIND (frame, default_setxattr_cbk, FIRST_CHILD(this), FIRST_CHILD(this)→fops→setxattr, loc, dict, flags, xdata);
-
frame:表示此请求的栈帧 stack frame。
-
rfn:当下一个xlator或者其他xlator完成了此请求(包括前半部分和后半部分)后,要调用的回调函数。
-
obj:要传递到的那个xlator对象,也即下一个xlator,不是调用STACK_WIND的本xlator。多个子的情况如何处理?
-
fn:要调用的具体的xlator的操作,来自下一个xlator的fops分配表。
-
params …任何其他的入口点相关的参数(比如针对此fop的inodes, file descriptors, offsets, data buffers等)如前叙,rfn回调函数也许在STACK_WIND的调用内部被调用,也或者后来在另一个环境中被调用。不调用下一个xlator就完成请求比如从cache中读数据,或者当本xlator层完成对req的处理,要从本层的回调中把请求传回上一个xlator时,我们使用STACK_UNWIND。实际上,我们最好使用 STACK_UNWIND_STRICT。这个函数允许我们确定请求的种类,比如是open还是close等。
#define STACK_UNWIND_STRICT(op, frame, params …)
STACK_UNWIND_STRICT (symlink, frame, op_ret, op_errno, inode, buf, preparent, postparent, xdata);
-
参数op:操作类型,是open/还是其他。用来检查额外参数是否匹配此fop。
-
params …:此请求的一些额外的参数
-
另外,在op和params直接,还有两个参数op_ret和op_errno.虽然没有在宏定义中出现。但在调用时还是要有的。
-
op_ret:fop的到目前的状态(读的字节数或者写的字节数,常常0表示fop成功,-1表示失败)
-
op_errno:标准错误码,在fop失败的情况下
-
-
具体fop相关的参数有其他特殊的参数,我们可以在params …中实现。但是我们也经常需要这两个参数
-
frame:当前请求的stack frame。
-
this:表示此xlator实例的xlator对象
-
回调函数跟分配函数类似,除了在这两个参数间有一个额外的参数cookie。是一个未定义数据类型的指针,由相应的STACK_WIND存储。默认情况下,这是一个由STACK_WIND生成的stack frame的指针,但是STACK_WIND_COOKIE可以允许我们确定一个不同的值。在这种情况下,这个处于rfn和obj直接的额外参数,可以从分配函数到其回调函数传递某些上下文。注意这个指针不能指向栈上的任何内容,因为当回调函数调用时,stack可能已经不存在了。
另一个需要注意的:STACK_UNWIND也许会导致整个调用栈退出,此时,最后的那个STACK_UNWIND调用将会释放所有的frames。因此,永远不要期望在调用STACK_UNWIND后,当前的frame内容还是完好的。
Per Request Context请求上下文私有变量
每个xlator 栈帧有一个local 指针,是用来保存xlator相关的上下文。这是最基本的机制,用来在分配函数和回调间保存上下文。因此我们应该习惯如下代码模式
/* in dispatch function */
local = (my_locals_t *)GF_CALLOC(1,sizeof(*local),...);
if (!local) {
/* STACK_UNWIND with ENOMEM eror */
}
/* fill in my_locals_t fields */
frame->local = local;
/* in callback */
local = frame->local;
要记住:每个帧frame的非NULL的local域当要毁栈时要用GF_FREE来释放,不用做其他的清理工作。如果local结构里包含指针或对其他对象的引用,需要我们自己进行这些资源的清理。在毁栈前,内存或者其他资源先清理是个好的习惯,为此就不能依靠GF_FREE来自动清理。最安全的方式是定义我们自己的xlator相关的destructor,在调用STACK_UNWIND前手动调用。
Inode and File Descriptor Context
大多数分配函数和回调以文件描述子或节点inode为参数。file descriptor (fd_t) or an inode (inode_t)。常常,xlator需要保存一些这些对象的一些关于此xlator的一些上下文,以此,在一个请求的完整生命周期内信息可以保持。例如,DHT xlator需要保存目录的layout map和某些inode的已知的位置。有一套函数来存储操作此类上下文。在每个函数中,第二个参数是此value相关的xlator对象的指针。value是64位无符号整形。
inode_ctx_put (inode, xlator, value) /* NB: put, not set */把value放到inode里?
inode_ctx_get (inode, xlator, &value)从inode中获取值到value。
inode_ctx_del (inode, xlator, &value)
fd_ctx_set (fd, xlator, value) /* NB: set, not put */
fd_ctx_get (fd, xlator, &value)
fd_ctx_del (fd, xlator, &value)
_del函数实际是破坏性get,先返回然后删除值。inode函数有两个value的形式,比如inode_ctx_put2,操作两个值。使用xlator指针作为key/index并不仅仅是为了好看。当要删除inode_t or fd_t时, 删除代码要浏览上下文槽(估计就是遍历所有xlator)。对于每个使用inode_t or fd_t的xlator的上下文槽,查看xlator的cbk表,调用其forget 或者release。如果上下文是个指针,需要手动释放资源。传递到分配函数和回调的inode_t or fd_t pointer参数,仅仅是个 borrowed reference。如果需要保证以后对象还在,需要调用inode_ref or fd_ref,来达到permanent reference。当不再需要引用时可以 inode_unref or fd_unref。
Inode and File Descriptor Context 词典和xlator选项
另一个常见的参数类型dict _t,是一种通用词典或者hash-map的数据结构,用来以字符串key/索引的方式保存任意值。例如,值可以是可变大小的有无符号的整形,字符串,二进制blob。字符串,二进制blob也许可以用GLusterfs函数free,也可以用glibc free。或者不用free。保存引用计数的值的dict_t* 和the *data_t 对象,在引用数为0的情况下释放资源。和inode和文件描述子一样,如果想在以后使用接受到dict_t的对象,需要使用add _ref and _unref来管理其生命周期。词典不仅仅用于分配函数和回调函数。也用于传递options到模块,比如xlator的init函数所用的options。事实上,目前的xlator的init函数主体大部分是来解释包含在词典里的options。要给xlator添加一个option,也需要向xlator的options 数组里添加一项。每个option可以是个boolean/整型/字符串/路径/xlator 名字/其他类型。如果是个字符串,可以确定一个正确的值的列表。解析后的options,加上其他xlator级别的信息,将存储在xlator_t structure结构(在大多数上下中用this表示)的private的成员变量中。
Child Enumeration and Fan Out 子xlator枚举和扇出
在一个xlator里的代码,经常需要枚举其子xlator。要不找到一个满足要求的子xlator的或者操作所有的子xlator。例如,对于DHT xlator,需要从所有的子xlator收集 hash-layout 映射,以确定文件应该放到哪个子xlator;对于 AFR xlator,需要从子xlator中提取同一文件将要执行的操作个数以便确定replication复制状态。代码范例如下: xlator_list_t *trav; xlator_t *xl;
for (trav = this->children; trav; trav = trav->next) {
xl = trav->xlator;
do_something(xl);
}
如果目的是扇出一个请求到所有的子xlator,则需要一些努力。在一个分配函数里最常用的方式是如下 local→call_count = priv→num_children;
for (trav = this->children; trav; trav = trav->next) {
xl = trav->xlator;
STACK_WIND(frame,my_callback,xl,xl->fops->whatever,...);
}
然后在回调函数中执行
LOCK(&frame->lock);
call_cnt = --local->call_count;
UNLOCK(&frame->lock);
/* Do whatever you do for every call */
if (!call_cnt) {
/* Do last-call processing. */
STACK_UNWIND(frame,op_ret,op_errno,...);
}
return 0;
在某些情况下,可以使用STACK_WIND_COOKIE,这样可以在回调中知道具体哪个调用返回了。可以查看AFR。
Call Frames and Call Stacks
所有的xlator 函数使用类似SEDA和AT&T的STREAMS的异步调用规范。需要的调用帧和调用栈数据结构功能上几乎与Windows NT里的I/O requst packets 和irp 栈相同。许多xlator函数类似与fuse对应的函数,但是修改了或者添加了其他参数。
在xlator api里这是最基本的数据结构,call_frame_t and call_stack_txlators环境可以看作是个线程库,每个请求有自己一个轻量级线程。这个线程有自己的栈,但是这些调用栈比c栈更加结构化,同时常常偏离栈的严格的FIFO语义。每个调用帧表示一个函数调用,函数调用也许是嵌套的。但是这些函数在c里实际上两个调用--初始部分执行并在进行任何嵌套调用前返回。收尾部分在嵌套的调用完成后执行。
-
root:指向内嵌在call_stack_t里的假帧,不像其他单独分配的frame。在stack里对所有的frame都相同。
-
parent指向调用此frame的那个frame。用c说就是调用函数 calling 函数。
-
next /prev指向下一个/前一个要完成的frame。注意不是要调用的那个帧,
-
local 私有数据,针对此帧调用的私有数据。是xlagor函数的本地变量,保存在此,可以在分配函数和回调间保持。
-
this 指向xlator的私有全局状态。
-
ret 指向调用者(上层xlator)的回调函数,当本xlator的req完成后执行。
-
ref_count and complete 调度器用来跟踪那个frame是active,哪个完成了,哪个需要恢复执行。
-
lock and cookie:
正常情况下,新frame push到栈后,next就跟parent就相同了。但是,一个frame(帧)可以分支。例如,stripe_writev请求的帧,在每个子xlator返回前生成一个新帧。假设有3个子xlator,即3个子卷。next就只与第一个新帧相同。
根据线程库模型,call_stack_t对应的就是线程控制块。
-
uid 和gid表示调用帧来自谁,用来鉴权和认证。
-
pid表示发起此请求的进程pid。
-
trans 指向请求相关的transport 结构。在服务器端用来实现基于节点的存取控制,或者用来沿着失败的连接跟踪请求。在client用作啥用?
-
frames:假帧,指向请求的第一个真正的frame。
-
op /type?
-
call_pool_t?
STACK_WIND and STACK_UNWIND等价与函数的调用和返回。在本xlator的请求的执行,分配表先执行,wind后就停止只ing了。当函数rfn回调函数执行时,此操作就开始恢复执行了。rfn也许被调用多次。(多个子卷)。
-
call stack
-
call stub