Chinaunix首页 | 论坛 | 博客
  • 博客访问: 541932
  • 博文数量: 64
  • 博客积分: 1591
  • 博客等级: 上尉
  • 技术积分: 736
  • 用 户 组: 普通用户
  • 注册时间: 2010-12-08 14:54
文章分类

全部博文(64)

文章存档

2011年(42)

2010年(22)

分类: 服务器与存储

2011-04-20 10:41:37

存储入门文章(8)--分级存储系统中文件访问重定向的设计与实现

名称:分级存储系统中文件访问重定向的设计与实现

出处:博士论文

作者:赵德铭

单位:大连海事大学

1.简介

数据分级存储之所以重要,是因为它既能最大限度地满足用户需求,又可使存储成本最小化。数据分级存储的优点具体表现在:

(1)减少总体存储成本不经常访问的数据驻留在较低成本的存储器中,可综合发挥磁盘驱动器的性能优势与磁带的成本优势。

(2)性能优化分级存储可使不同性价比的存储设备发挥最大的综合效益。

(3)改善数据可用性分级存储把很少使用的历史数据迁移到辅助存储器中,或归档到离线存储池中,这样就无需反复保存,减少了存储的时间;同时提高了在线数据的可用性,使磁盘的可用空间维持在系统要求的水平上。

(4)数据迁移对应用透明

进行分级存储后,数据移动到另外的存储器时,应用程序不需要改变,使数据迁移对应用透明。

2.研究的关键性问题

第一,分析现有的文件监控技术,找到最适合分级存储管理系统的文件监控方案.目前已有的文件监控机制多针对系统安全提出的,大多强调监控的实时性和精度,这种方式占用大量的系统资源,显然不适合分级存储系统。本文通过对已有机制的研究,各取所长,提出了一种适合分级存储特点的方案。

第二,获得访问操作信息后,对每条信息做记录,将记录传输给HSM管理端,以此为依据,通过分析文件的访问频率来调整文件存储位置(一级存储设备还是二级存储设备),即数据迁移和回迁。

第三,当访问某个文件时,首先判断这个文件是否迁移过,在文件里设置一个标识量,如果文件没被迁移,直接读取文件内容,若已经移动,则读取文件里所记录的文件存储路径,达到在对用户透明的情况下,更改文件实际存储位置,实现分级存储管理,以达到在降低成本的基础上,有效提高文件I/0访问效率。

3.传统文件监控的缺点

不论是Windows操作系统还是Limix操系统大多是基于系统提供的API接口函数或是接口处理好的结果,不能主动从操作系统内核获取我们需要的信息,因而有以下缺点。

(l)传统文件监控方法运行效率低,占用主机资源较高,同时反映时间较长,实时性差

(2)获取的信息有限,只能够得到系统API或者系统调用所提供的有限功能来获取用户需要的信息。

(3)不能够对文件进行有效的保护,也就是说如果入侵者得到了系统的管理员权限,那么他可以随意修改系统文件,甚至可以随意修改系统文件,甚至可以随意删除被监控文件。这些操作对时实监控的传统监控技术来说都是无能为力的。

4.应用dnotify机制实现文件监控

dnotify就是指directory notification,监视文件系统上一个目录中的况.dnotify机制是Linux一种比较早的文件监控机制,在最初的Linux系统中部分采用这种监控机制。这是指一个能实时地观察Linux文件系统的变化情况程序模块。能够实时的观察文件系统的变化情况,并做出及时的适当的反应.

Linux文档中,我们可以看到以下程序段:

#define _GNU_SOURCE     /* needed to get the defines */
#include       /* in glibc 2.2 this has the needed
      values defined */
#include 
#include 
#include 
      
static volatile int event_fd

// 信号处理例程     
static void handler(int sig, siginfo_t *si, void *data)
{
    event_fd = si->si_fd
}
  
int main(void)
{
    struct sigaction act
    int fd
    
    // 登记信号处理例程
    act.sa_sigaction = handler
    sigemptyset(&act.sa_mask)
    act.sa_flags = SA_SIGINFO
    sigaction(SIGRTMIN, &act, NULL)
   
    // 需要了解当前目录"."的情况
    fd = open(".", O_RDONLY)
    fcntl(fd, F_SETSIG, SIGRTMIN)
    fcntl(fd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_MULTISHOT)
    /* we will now be notified if any of the files
    in "." is modified or new files are created */
    while (1) {
      // 收到信号后,就会执行信号处理例程。
      // 而 pause() 也就结束了。
      pause()
      printf("Got event on fd=%d\n", event_fd)
    }
}


3.2dnotify工作原理

这里可以清晰的看到dnotify是如何工作的。

首先注册一个信号处理例程然后通知Kernel,之后观察fd上的DN_MODIFYDN_CREATEDN_MUTISHOT事件。Linux  Kernel收到这个请求后,把相应的fdinode给做上记号

然后Linux Kernel和用户应用程序就自顾自去处理各自的其他任务。等到inode上发生了相应的事件,Linux Kernel就把信号发给用户进程,于是开始执行信号处理例程,用户程序对文件系统上的变化也就可以及时的做出反应了。而在这整个过程中,系统以及用户程序的正常运行基本上未受到性能上的影响。这里还需要说明的是,dnotify并没有通过增加新的系统调用来完成它的功能,而是通过fcntl来完成任务的。增加一个系统调用,相对来说是一个很大的手术,而且如果设计不当,处理得不好的话,伤疤会一直留在那里,是Linux Kernel的开发者们所非常不愿意见到的事情。

其特点如下:

(l)对于想监视的每一个目录,用户都需要打开一个文件描述符,因此如果需要监视的目录较多,将导致打开许多文件描述符,

(2)dnotify是基于目录的,它只能得到目录变化事件,当然在目录内的文件的变化会影响到其所在目录从而引发目录变化事件,但是要想通过目录事件来得知哪个文件变化,需要缓存许多stat结构的数据。

(3)dnotify的接口非常不友好,它使用signal

5.应用inotify机制实施文件监控

开源社区提出用户态需要内核提供一些机制,以便用户态能够及时地得知内核或底层硬件设备发生了什么,从而能够更好地管理设备,给用户提供更好的服。inotify就是这种需求催生的。inotifykernel2.6.13后被引入。inotify是一种文件系统的变化通知机制,如文件增加、删除等事件可以立刻让用户态得知,该机制是著名的桌面搜索引擎项目beagle引入的,并在Gamin等项目中被应用。

Inotify是为替代dnotify而设计的,它克服了dnotify的缺陷,提供了更好用的,简洁而强大的文件变化通知机制:

(1)Inotify不需要对被监视的目标打开文件描述符,而且如果被监视目标在可移动介质上,那么在umount该介质上的文件系统后,被监视目标对应的watch将被自动删除,并且会产生一个umount事件。

(2)Inotify既可以监视文件,也可以监视目录。

(3)Inotify使用系统调用而非SIGIO来通知文件系统事件。

(4)Inotify使用文件描述符作为接口,因而可以使用通常的文件I/O操作selectpoll来监视文件系统的变化。

Inotify可以监视的文件系统事件包括:

IN_ACCESS,即文件被访问

IN_MODIFY,文件被write

IN_ATTRIB,文件属性被修改,如chmodchowntouch

IN_CLOSE_WRITE,可写文件被close

IN_CLOSE_NOWRITE,不可写文件被elose

IN_OPEN,文件被open

IN_MOVED_FROM,文件被移走,如mv

IN_MOVED_TO,文件被移来,如mvep

IN_CREATE,创建新文件

IN_DELETE,文件被删除,如rm

IN_DELETE_SELF,自删除,即一个可执行文件在执行时删除自己

IN_MOVE_SELF,自移动,即一个可执行文件在执行时移动自己

IN_UMOUNT,宿主文件系统被umount

IN_CLOSE,文件被关闭,等同于(INeeCLOSEesWRITE1INesCLOSEesNOWR

IN_MOVE,文件被移动,等同于(IN_MOVED_FROM | IN_MOVED_TO)

:上面所说的文件也包括目录。

在用户态,inotify通过三个系统调用和在返回的文件描述符上的文件I/O操作来使用,使用inotify的第一步是创建inotify实例:

int fd=inotify_init():

每一个inotify实例对应一个独立的排序的队列。

文件系统的变化事件被称做watches的一个对象管理,每一个watch个二元组(目标,事件掩码),目标可以是文件或目录,事件掩码表示应用希望关注的inotify事件,每一个位对应一个inotify事件。Watch对象通过watch描述符引用,watches通过文件或目录的路径名来添加。目录watches将返回在该目录下的所有文件上面发生的事件。

下面函数用于添加一个watch:

int  wd = inotify_add_watch(fdpathmask):

fdinotify_init()返回的文件描述符,path是被监视的目标路径名(即文件名或目录名)mask是事件掩码,在头文件Linux/inotify.h定义了每一位代表的事件。可以使用同样的方式来修改事件掩码,即改变希望被通知的inotify事件。Wdwatch描述符。

下面的函数用于删除一个watch:

int ret = inotify_rm_watch( fd , wd);

fdinotify_init()返回的文件描述符,wdinotify_add_watch()返回的watch描述符。Ret是函数的返回值。

文件事件用一个inotify_event结构表示,它通过由inotify_init()返回的文件描述符使用通常文件读取函数read来获得:

struet  inotify_event

{

_s32 wd:

/*watch deseriptor*/

_u32 mask:

/*wateh mask*/

_u32 cookie:

/*cookie to synehronize two events*/

_u32 len:

/*length(ineluding nulls) of name*/

char name[0];

/*stub for possible name*/

};

结构中的wd为被监视目标的watch描述符,mask为事件掩码,lenname字符串的长度,name为被监视目标的路径名,该结构的name字段为一个桩,它只是为了用户方面引用文件名,文件名是变长的,它实际紧跟在该结构的后面,文件名将被0填充以使下一个事件结构能够4字节对齐。注意,len也把填充字节数统计在内。

通过read调用可以一次获得多个事件,只要提供的buf足够大。

sizet len=read(fdbufBUF_LEN):

buf是一个inotify_event结构的数组指针,BUF_LEN指定要读取的总长度,buf大小至少要不小于BUF_LEN,该调用返回的事件数取决于BUF_LEN以及事件中文件名的长度。Len为实际读取的字节数,即获得的事件的总长度。

可以在函数inotify_init()返回的文件描述符fd上使用select()poll(),也可以在fd上使用ioctl命令FIONREAD来得到当前队列的长度.

close(fd)将删除所有添加到fd中的watch并做必要的清理。

int inotify_init(void):

int inotify_add_watch(int fdconst  char  *path__u32 mask);

int inotify_rm_watch(int fd, __u32 mask); 

内核实现机理

在内核中,每一个inotify实例对应一个inotify_deviee结构

struet inotify_deviee

{

wait_queue_head_t wq

/*wait queue for i/o*/

struet idr idr;

/*idr mapping wd>watch*/

struct semaphore sem;

/*protects this bad boy*/

struct list_head events;

/*list of queued events*/

struct  list_head watches;

/*list of watches*/

atomic_t count;

/*reference count*/

struct user_struct *user;

/*user who opened this dev*/

unsigned int event_count;

/*number of pending events*/

unsigned int max_events;

/*maximum number of events*/

u32 last_wd;

/*the last wd allocated*/

};

wq是等待队列,被read调用阻塞的进程将挂在该等待队列上,idr用于把watch描述符映射到对应的inotify_watchsem用于同步对该结构的访问,events为该inotify实例上发生的事件的列表,被该inotify实例监视的所有事件在发生后都将插入到这个列表,watches是给inotify实例监视的watch列表,inotify_add_watch将把新添加的watch插入到该列表,count是引用计数,user用于描述创建该inotify实例的用户,queue_size表示该inotify实例的事件队列的字节数,event_eountevents列表的事件数,max_events为最大允许的事件数,last_wd是上次分配的watch描述符。

每一个watch对应一个inotify_watch结构:

struct inotify_watch

{

struct list_head d_list; /*entry in inotify_device's list*/

struct list_head i_list; /*entry in inode's list*/

atomic_t count; /*reference count*/

struct inotify_device *dev;/*associated device*/

struct inode *inode; /*associated inode */

s32 wd; /*watch descriptor*/

u32 mask; /*event mask for this watch*/

}

d_1ist指向所有inotify_device组成的列表的,i_list指向所有被监视inode组成的列表,count是引用计数,dev指向该watch所在的inotify实例对应的inotify_deviee结构,inode指向该watch要监视的inodewd是分配给该watch的描述符,mask是该watch的事件掩码,表示它对哪些文件系统事件感兴趣。

结构inotify_device在用户态调用inotify_init()时创建,当关闭inotify_init()返回的文件描述符时将被释放。结构inotify_watch在用户态调用inotify_add_watch()创建,在用户态调用inotify_rm_watchclose(fd)时被释放。

无论是目录还是文件,在内核中都对应一个inode结构,inotify系统在inode结构中增加了两个字段:

#ifdef CONFIG_INOTIFY

struct list_head inotify_watches; /*watches on this inode*/

struct semaphore inotify_sme; /*protects the watches list*/

#endif

inotify_watches是在被监视目标上的watch列表,每当用户调用inotify_add_watch时,内核就为添加的watch创建一个inotify_watch结构,并把它插入到被监视目标对应的inodeinotify_watches列表。inotify_sem用于同步对inotify_watches列表的访问。当文件系统发生第一部分提到的事件之一时,相应的文件系统代码将显示调用fsnotify_e来把相应的事件报告给inotify系统,其中*号就是相应的事件名,目前实现包括:

fsnotify_move,文件从一个目录移动到另一个目录

fsnotify_nameremove,文件从目录中删除

fsnotify_inoderemove,自删除

fsnotify_create,创建新文件

fsnotify_mkdir,创建新目录

fsnotify_access,文件被读

fsnotify_modify,文件被写

fsnotify_open,文件被打开

fsnotify_close,文件被关闭

fsnotify_xattr,文件的扩展属性被修改

fsnotify_change,文件被修改或原数据被修改

有一个例外情况,就是inotify_umount_inodes,它会在文件系统被umount时调用来通知umount事件给inotify系统。

以上提到的通知函数最后都调用inotify_inode_queue_event (inotify_unmount_inodes直接调用inotify_dev_queue_event),该函数首先判断对应的inode是否被监视,这通过查看inotify_watches列表是否为空来实现,如果发现inode没有被监视,什么也不做,立刻返回,反之,遍历inotify_watches列表,看是否当前的文件操作事件被某个watoh监视,如果调用inotify_dev_queue_event,否则,返回.函数inotify_dev_queue_event首先判断该事件是否是上一个事件的重复,如果是就丢弃该事件并返回,否则,它判断是否inotify实例即inotify_deviee的事件队列是否溢出,如果溢出,产生一个溢出事件,否则产生一个当前的文件操作事件,这些事件通过kernel_event构建,kernel_event将创建一个inotify_kernel_event结构,然后把该结构插入到对应的inotify_devieeevents事件列表,然后唤醒等待在inotify_device结构中的wq指向的等待队列。想监视文件系统事件的用户态进程在inotify实例(inotifinit()返回的文件描述符)上调用read,没有事件时就挂在等待队列wq上。

6.利用tripwire进行文件监控

当前在文件监控领域最知名最成熟的产品莫过于tripwiretripwire是一有关数据和网络完整性保护的工具,主要检测和报告系统中任意文件被改动、添加、删除的详细情况,可以用于入侵检测、损失的评估和恢复、证据保存等用途。

7.应用linux内核可加载模块的文件监控方法

LKM(Loadable Kernel Module)

利用LKM拦截文件操作系统调用

要利用LKM实现对文件系统的实时监控,首先必须实时拦截对每个文件的读写、执行、修改属性等等操作。然后将拦截信息通过一定通信方式交与用户态下的一个守护进程进行处理,分析进程处理完文件操作的请求后向内核发送指令是否允许此次文件访问的通行。对文件操作相关系统调用的拦截可以利用替换系统调用函数指针来实现。

Linux系统(i386)中的系统调用是通过用户软件调用中断int0x80激发的,int0x80被执行后,内核获得cpu控制权,交由system_call程序处理。systeln_call的主要工作是:保存寄存器,判断系统调用编号合法性等工作,然后根据sys_call_table(系统调用表)和系统调用编号,找到系统调用处理程序,执行相应的系统调用。不难看出,找到sys_call_table中相应系统调用对应项,保存该项信息,用新的系统服务处理函数替换原来的函数。重要的是,新函数内部逻辑中,必须有个逻辑分支调用原来的系统服务函数,这样系统调用逻辑便完整了。在内核2.4.18以前sys_call_table是导出的,我们可直接利用这个指针找到相应的系统调用的处理函数,然后保存并进行替换。

Linux2.4.18内核以后,为了解决这个安全问题,sys_eall_table不能直接导出,但是由上面的介绍可以知道sys_call_table是由system__call进行调用的,而system_callint0x80软件中断,所有中断信息都保存在一张中断描述表IDT中,而这张表的地址是保存在IDTR寄存器里面的,所以整个截获过程如图:


具体实现的代码如下:

asm("sidt %0" : "=m" (idtr));

memcpy(&idt, (void*)idtr.base + 8 * 0x80, sizeof(idt));

sys_call_off = (idt.off2 << 16) | idt.off1;

memcpy(sc_asm, (void*)sys_call_off, 100);

for (sct=0, p=sc_asm; sct<100-1; sct++, p++)

{

if(!memcpy(p, "\xff\x14\x85", 3))

{

sct = *(unsigned *)(p+3);

sys_call_table = (long *)sct;

break;

}

}

最后可以得到系统调用表的地址sys_call_table,从而实现所有系统调用的拦截操作。

8.分级存储系统中文件监控的特点

分级存储系统主要是随着海量数据的出现导致存储成本不断上升而产生的一种将信息存入降格相对便宜的近线存储设备和离线存储设备的一种调度系统。通过分级存储系统的调度,使不在一级存储设备的信息也可以被用户在线访问到,以达到在降低存储成本的基础上扩充在线可访问信息量的目的。

分级存储中的文件监控不同于一般文件监控的要求,主要侧重于系统安全方面。分级存储系统侧重点在于对文件访问信息的监控,它主要关心的是哪些文件被用户访问、建立、修改、删除或者迁移。这里只要截获用户这些访问信息,将其发送到元数据管理模块,再由元数据管理模块对其进行处理。分析其使用频率决定是否迁移或者做其他处理。

文件监控就是元数据的采集来源,所以文件监控策略要适应元数据需求的要求。在此分级存储系统中,文件系统监测服务所维护的,提供给数据迁移服务使用的文件信息有如下几个特点:

(1)文件数目多,数据量大。由于现在数据存储的容量巨大。因此保存文件信息的数据结构必须支持高效的插入,删除和更新并且具有良好的扩展性(以满足随时可能变化的在线卷容量的需求)

(2)文件信息的更新具有并发性。当多个用户分别对在线卷上的文件进行操作时,会引发一系列的文件信息的更新请求。比如一个用户在在线卷上建立了一个新文件检测系统就需要生成关于这个新文件的信息;而同时另一个用户修改了己有的某个文件,引发了更新文件修改时间的请求。这些请求可能是先后发出的,也可能是同时的。这就要求数据结构能够支持文件信息更新的这种并发性。

(3)需要提供强大的查询功能。记录元数据信息的最终目的是为数据管理服务提供便利。每次执行归档迁移时,归档程序都要根据这些记录,从中找出那些符合归档条件的文件来。因此数据结构必须提供功能强大的查询接口,以满足归档程序的查询需求。

(4)文件信息必须可以永久保存。系统可能会因为一些意外断电或者是根据维护的需要重新启动。如果每次重新启动后,都需要通过扫描磁盘来重新初始化文件信息的话,对系统而言是一笔巨大的开销。为了降低这种开销,文件信息应当可以被保存在一些永久设备(如硬盘上),即使是断电或系统重启也不会丢失。而在服务重启的时候它们可以被重新读取出来。这要求数据结构支持序列化和反序列化。

由以上特点可以看出,由于分级存储系统对文件访问信息的实时性并不象数据安全方面那么高,而且所需要监控的信息量非常之大,分级存储系统的应用非常的广泛,不同用户可能在某些方面有起不同的要求,因此我们设计此分级存储系统时充分考虑了其广泛适用性。

9.实现相关技术的研究

Linux系统内核使用三种数据结构表示打开的文件:

(1)每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可以将其视为一个矢量,个描述符占用一项。与每个文件描述符相关联的是文件描述符标志和指向一个文件表项的指针。

(2)内核为所有打开文件维持一张文件表。每个文件表项包含:文件状态标志、当前文件偏移量和指向该文件i节点的表项的指针。

(3)每个打开的文件都有一个i节点结构。

Linux把传统UnixVnode分成了inodedentry,前者表示磁盘文件(真实的或虚拟出来的),后者表示一个目录项(介于进程的打开文件与inode之间)Linux文件系统中i节点是记录文件详细信息的数据结构,对文件的基本操作都是通过对i节点的访问来完成的。

每一个文件都有自己的i节点,每个i节点都有一个唯一的i节点号

10.文件重定向的方法

对文件的监控信息的获得,一方面是为元数据模块提供原始数据,另一方面为迁移文件的重定向访问提供依据。比如:当系统调用read函数时要截获该系统调用,通过开发人员编写的程序正确找到文件的所在位置,这样才能完成文件迁移对用户透明的效果,这是分级存储系统重要性能优点。本文中文件重定向就是指对该文件访问系统调用的重定向。目前,系统调用重定向主要有三种模式:

(1)普通的用户态应用程序

这个模型中系统调用服务程序是一个用户态运行的监控程序,用户程序的系统调用是经过系统调用捕获程序捕获,然后由系统调用转发程序转发给系统调用服务程序处理。系统调用转发程序可以在Linux下使用模块的方式动态载入到内核中,它创建一个系统调用请求队列,用来存放尚未处理的系统调用请求,在/dev目录下创建一个设备文件作为和系统调用服务程序交互的接口。每个系统调用重定向处理至少需要四次跨越用户态和内核态的边界。

(2)用户程序中的一段代码

这个模型中系统调用服务程序和用户程序处于一个进程的同一地址空间中,用户程序的系统调用经过系统调用捕获程序捕获,重新定向到系统调用服务程序中进行处理。因为这个系统调用服务程序和用户程序处于同一地址空间,所以处理完成后可以由系统调用服务程序直接返回。每个系统调用重定向处理至少需要两次跨越用户态和内核态的边界。

(3)利用ptrace进行系统调用重定向

利用UNIX操作系统的ptrace系统调用(请参考UNIX使用手册)最好的例子就是gdb调试器。这个模型,首先由系统调用服务程序派生出一个子进程,然后父进程调用,ait系统调用进行等待子进程加载用户程序。接着这个子进程使用PTRACE_SYSCALL为参数把自己设置成被跟踪状态,加载执行需要进行系统调用重定向的用户程序。父进程即系统调用服务程序被唤醒,利用PTRACE_SYSCALL为参数调用ptrace系统调用,然后等待子进程进行系统调用。如果子进程进行系统调用,那么就会被停止,父进程被唤醒,通过ptrace的其他功能就可以实现对子进程地址空间的内容和寄存器的内容进行修改,从而为对子进程的系统调用进行服务,子进程本身的系统调用加上父进程的ptrace系统调用,每个系统调用重定向处理至少需要四次跨越用户态和内核态的边界门。

阅读(3117) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~