可以看到,以上的过程是十分复杂繁琐的,而且需要占用一块和主卷相同容量大小的卷。关键是需要停掉主机IO,这对应用会产生影响。为了解决这个难题,一种解决方案出现了,这就是快照技术。快照的基本思想是,抓取某一时间点,磁盘的所有数据,就像急冻一样,但是还不能真正冻住,主机的IO需要正常执行。这怎么可能呢?快照就将其变成了可能。快照是这样实现的,即先在某一时间点,对于这个时间点之后的所有上层的写IO,先将这个IO对应的块上的数据复制到一个新的卷中存放,并做好原卷中的这个块和新卷中块的对应关系记录,然后才进行上层IO的写入。这样,这一时间点上磁盘的数据,便被保存下来,就像做了急冻一样。这种方法也叫做copy on write,也就是在发生IO写之前,先将待更新块中原来的数据复制出来保存,然后再做新数据的写入,即写时复制。还有一种实现块快照的方法,叫做write redirect,当写IO到来的时候,将这个IO重定向到一个新的卷,而不是写原来的卷,并做好新卷上的块和原来卷所应该被写入块的映射记录。这样也同样保存下了这个时刻原来卷上的所有数据,同时不影响后续读写IO操作,因为保持了块映射关系。
不管是copy on write还是write redirect,只要上层有写IO,这个IO块就要占用新卷上的一个块(因为要保留原块的内容,不能被覆盖),如果上层将原卷上的所有数据块都写更新了,那么新卷的容量就需要和原卷的数据量同样大,甚至还大(预防新增数据写入),才可以。但是通常应用不会写覆盖面百分之百,做快照的时候,新卷的容量一般设置成原卷容量的30%就可以。
实际中一般都是用copy on write的方式做快照,因为write redirect方式,每次写IO都需要查一遍快映射表,速度慢,耗费资源大。
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都构造完成执行递减一次。