2008年(2)
分类: LINUX
2008-04-07 13:26:00
CDP是Linux内核中基于块的连续数据保护模块,这个模块在数据块级别提供连续数据保护能力。连续数据保护是一种备份和恢复技术,它持 续地捕获所有I/O请求,并且为在这些请求打上时间戳标志。它将数据变化以及时间戳保存下来,以便恢复到过去的任意时刻。因此,这个模块支持数据的任意时 刻映像。
对主机磁盘设备数据块的在各个时刻所作的写操作都被记录下来,被顺序保存到CDP仓库设备中,同时对应的元数据也被保存在CDP元数据设备中。元数据包括以下信息:
struct metadata {
int hrs, min, sec; 该数据块被写入主机磁盘设备的时间;
unsigned int bisize; 该数据块的以字节为单位的长度;
sector_t cdp_sector; CDP仓库设备中对应数据块的起始扇区编号;
sector_t host_sector; 该数据块在主机磁盘设备中的起始扇区编号;
};
下图反映了主机磁盘设备和CDP仓库设备之间的关系。CDP仓库设备中按时间顺序保存了对主机磁盘设备的数据修改。A为主机磁盘设备上的一个扇区,该扇区在9:00和9:05分别进行了修改,它在CDP仓库设备中对应的扇区分别为A1和A2。
下图反映了CDP仓库设备和CDP元数据设备之间的关系,它们以写入顺序一一对应。CDP仓库设备中的一个元数据对应CDP元数据设备中一个I/O请求,实际上可能是多个扇区。具体扇区数由元数据中的bisize指定,而起始扇区位置由cdp_sector指定。
CDP仓库设备
全局变量maddr保存了下一个I/O请求在CDP仓库设备上执行的地址(起始扇区编号)。maddr的初值被定义为宏START_METADATA(0)。
unsigned int maddr = START_METADATA;
当一个写请求到来时,对应数据被写到CDP仓库设备中,这时所作的操作如下:
这时,我们要将这里写入的CDP仓库设备的数据块编号记录下来以便构造对应的元数据。
CDP元数据设备
全局变量taddr保存了下一个I/O请求对应的元数据在CDP元数据设备中保存的地址(起始扇区编号)。 taddr的初值被定义为宏START_METADATA(0)。
unsigned int taddr = START_METADATA;
当一个写请求到来时,对应的元数据被记录在CDP元数据设备中。
为了简单起见,在元数据设备上,一个扇区(512字节)只保存一个元数据信息(只有32字节),这样浪费了大量的存储空间,但对元数据设备的处理却非常简单:
请求处理过程
请求处理过程是从make_request函数开始的。考虑到读请求的处理的相似性,甚至更为简单,我们这里只分析对写请求的处理过程。我 们首先获得当前的系统时间。之后,写请求bio结构(为说明方便,我们记为B)被分为三个写请求bio结构(分别为B0、B1和B2)。这三个bio结构 的作用是:
同其它块设备驱动程序的实现一样。我们从B克隆产生B0、B1和B2。然后重定向它们要处理的设备,即bi_bdev域。另外一个大的变动是重新设置了bi_end_io域,用于在I/O请求完成之后进行善后处理。
为了处理善后,还将B0、B1和B2的bi_private指向同一个cdp_bio1结构。从这个结构,我们要能够回到对B的处理。
struct cdp_bio {
struct bio *master_bio; 原来的bio,通过这个域我们可以从B0、B1、B2找到B
struct bio *bios[3]; 如果IO为WRITE,这个指针数组分别指向B0、B1、B2,为何需要这个域?
atomic_t remaining; 这里一个计数器,我们后面将解释。
unsigned long state; 在I/O完成方法中使用
};
善后工作的主要目的是:在B0、B1和B2都执行完成后,回去执行B,为此,我们需要一个“have we finished”计数器,这就是原子整型变量remaining。在构造B0、B1、B2时分别递增,同时在B0、B1和B2的I/O完成方法中递减, 最后根据该值是否递减到0,来判断B0、B1和B2是否都已经执行完毕。为了防止B0在构造后,在B1和B2构造之前就执行到B0的I/O完成方法,从而 使得remaining变成0,这种错误情况。我们没有将remaining的初值设置为0,而是设为1。并在B0、B1、B2都构造完成执行递减一次。
B0、B1、B2都执行完成之后,进行如下的处理:
另一个需要说明的是对B2的构造,这个bio结构需要处理的是元数据。时间戳已经在进入make_request时获得了保存,而对主机磁盘设备操作的起始扇区和长度从B中可以获得,对应的CDP仓库和CDP元数据的起始地址分别保存在全局变量maddr和taddr中。
数据恢复过程
我们可以将数据恢复到以前的任意时刻。CDP实现代码中提供了一个blk_ioctl函数,用户空间以GET_TIME为参数调用该函数,将主机磁盘设备中的数据恢复到指定的时间点。恢复的过程分为两步:
1. 顺序读取CDP元数据设备的所有扇区,构造一个从主机磁盘设备数据块到CDP仓库设备的(在这个时间点之前)更新数据块的映射。其结果保存在以mt_home为首的(映射表)链表中。
这里需要构造taddr个对CDP元数据设备的读请求,每个请求读取一个扇区。在这些请求的I/O完成方法中,从读到的数据中构造元数据,并递减计数器count。
如果元数据中的时间戳早于或等于指定的恢复时间点,则需要添加或修改mt_home链表的元数据结构。需要说明的是,这些项是以 host_sector为关键字索引的,因此添加或修改取决于前面是否出现对同一个host_sector的修改。我们以顺序方式读取的过程中,可以保证 host_sector(在指定的恢复时间点之前)的最新修改cdp_sector会出现在这个链表中。
由于计数器count为taddr,如果它递减为0,说明CDP元数据设备中的所有数据均已读出并处理,这时我们可以继续往后面执行。
2. 从CDP仓库设备中读取这些更新的数据块,构造以mt_bi_home为首的链表。
同上面的处理类似,我们需要为mt_home链表中的每一项构造对CDP仓库设备的读请求,每个请求在CDP仓库设备的起始编号取决于 cdp_sector域,长度则根据bisize而定。这个请求读出的数据需要被写入到主机磁盘设备中,为此我们在读请求I/O完成函数中,构造一个对应 的往主机磁盘设备的写请求bio,该写请求的起始编号取决于host_sector域,长度根据bisize而定,而要写入的数据是刚刚从CDP仓库设备 中读出的数据。另外,在读请求I/O完成函数中,还要递减一个计数器,当该计数器递减到0时,说明我们已经全部处理了mt_home链表中的项,这时得到 一个以mr_bio_home为首,每项中都指向一个bio结构的链表。
struct list_head mt_home; //BIO更新链表
struct most_recent_blocks { //BIO更新表项
struct bio *mrbio;
struct list_head list;
};
3. 将mt_bi_home链表的数据块都恢复到主机磁盘设备中。
这个操作相对比较简单,我们只需要在主机磁盘设备上执行mt_bi_home链表的每一个bio请求项即可。当然,我们要在这些请求项的I/O完成方法中做善后处理,即如果所有请求项都已经执行完毕,则释放mt_home链表和mt_bi_home链表。