通过hijack了解bio
背景
bio是block io,它是一个描述硬盘里面的位置与page cache的页对应关系的数据结构,每个bio对应的
硬盘里面一块连续的位置,每一块硬盘里面连续的位置,可能对应着page cache的多页,或者一页,
所以它里面会有一个bio_vec *bi_io_vec
的表。
而每个bio_vec都只能描述一个页内数据的连续的数据的偏移和长度:
通过bi_sectors和bi_size来描述硬盘中起始位置和长度。
一个bio的处理:一个bio通常通过submit_bio
来提交给设备队列,下面会经过bio聚合,转化成request,磁盘调度队列,块设备驱动,磁盘处理,对于bio来说大部分都是异步,所以处理结束通过bio->bi_end_io
来作为callback获取结果,外部的接口使用bio_endio。同步的读写是通过等待callback事件到来从而完成同步读写。
中间贴的只是代码片段,介绍怎么做的,不能直接运行的。获取全部的代码片段,请联系我
目标
进行块设备加密,磁盘落盘的数据是密文,而对于文件系统来说是明文.加密算法因为用过sm4,所以就选用了最简单的
sm4(ecb)
加解密。设计
submit_bio是写IO的起手势,所以通过hook submit_bio来进行写加密,保证提交给下面的数据是经过加密的就好了。而对于读IO可以通过在IO完成之后通知给文件系统之前进行解密,可以hook bio_endio来进行数据解密,确保page cache上的数据是经过解密后的明文。
写加密
bio只是管理的数据结构,数据仍然是存放在page cache中的,最终的结果要求是page cache中的所述句仍然是明文,提交给磁盘的数据是密文,所以一块数据是搞不定这件事了,我们需要重新申请一块内存来存放密文数据。 ``` long origin_submit_bio(int rw, struct bio *bio); void hook_submit_bio(int rw, struct bio *bio) { ret = try_crypt_bio(rw, bio); if (ret != 0) { origin_submit_bio(rw, bio); } return; } int bfx_bio_alloc_pages(struct bio *bio, gfp_t gfp) { int i; struct bio_vec *bv;
bio_for_each_segment(bv, bio, i) {
bv->bv_page = alloc_page(gfp);
if (!bv->bv_page) {
while (bv-- != bio->bi_io_vec + bio->bi_idx)
__free_page(bv->bv_page);
return -ENOMEM;
}
}
return 0; }
void bfx_crypt_callback(struct bio *bio, int error) { unsigned int i; struct bio_vec *bv; struct bio *origin = (struct bio *)bio->bi_private;
bio_for_each_segment_all(bv, bio, i) {
BUG_ON(!bv->bv_page);
__free_page(bv->bv_page);
bv->bv_page = NULL;
}
bio_endio(origin, error);
bio_put(bio); } struct bio* bfx_bio_clone(struct bio *origin) {
struct bio *new = NULL;
new = bio_clone(origin, GFP_NOIO);
if (!new)
return NULL;
if (bfx_bio_alloc_pages(new, GFP_NOIO)) {
bio_put(new);
return NULL;
}
new->bi_private = origin;
new->bi_end_io = bfx_crypt_callback;
return new; } int try_crypt_bio(int rw, struct bio *bio) {
sm4_key_t key;
int i, j;
char buffer[SM4_BLOCK_SIZE * 2 + 1];
struct bio_vec *bv;
struct bio *new;
if (!(rw & WRITE)) {
return -1;
}
new = bfx_bio_clone(bio);
if (!new) {
BUG();
return -ENOMEM;
}
new->bi_rw |= rw;
bio->bi_rw |= rw;
sm4_set_key1(&key, mykey, 16);
bio_for_each_segment(bv, bio, i) {
void *p1 = kmap(bv->bv_page);
void *p2 = kmap(new->bi_io_vec[i].bv_page);
/* Encrypt your bio data */
/*
for (j = bv->bv_offset; j < bv->bv_offset + bv->bv_len; j+= SM4_BLOCK_SIZE) {
sm4_do_crypt(key.rkey_enc, (u32 *)(p2 + j), (u32 *)(p1 + j));
}
*/
kunmap(bv->bv_page);
kunmap(new->bi_io_vec[i].bv_page);
}
generic_make_request(new);
return 0; } ``` ### 读解密 相对于写操作,读IO就比较简单了,从磁盘中读取到了密文数据到page cache中,我们只需要在文件系统看到这块page cache前解密就好,不需要倒腾内存,只需要原地解密。
遍历bio指向的内存数据:
void origin_bio_endio(struct bio *bio, int error);
void hook_bio_endio(struct bio *bio, int error)
{
try_decrypt_bio(bio, error);
origin_bio_endio(bio, error);
return;
}
void try_decrypt_bio(struct bio *bio, int error)
{
sm4_key_t key;
int i, j;
char buffer[SM4_BLOCK_SIZE];
char *ptr = NULL;
unsigned long flags;
struct bio_vec *bv;
if (bio_data_dir(bio) == WRITE)
return;
sm4_set_key1(&key, mykey, 16);
bio_for_each_segment(bv, bio, i) {
ptr = bvec_kmap_irq(bv, &flags);
for (j = 0; j < bv->bv_len; j+= SM4_BLOCK_SIZE) {
/* Decrypt ptr pointer buffer */
/*
sm4_do_crypt(key.rkey_dec, (u32 *)buffer, (u32 *)(ptr + j));
memcpy(ptr + j, buffer, SM4_BLOCK_SIZE);
*/
}
bvec_kunmap_irq(ptr, &flags);
}
}
后续改进
因为加解密完全使用cpu来进行,速度肯定比较慢,最终的结果就是IO速率大幅度下降.
一个选择就是使用有硬件加密卡支持的算法,提升速率;
另外一个选择就是启动一个线程,先将bio缓存下来慢慢加解密,为了提升体验,可以提升读IO的优先级,让用户优先看到数据,至于写放在后台慢慢整吧。
参考
https://lwn.net/Articles/26404/
www.eeworld.com.cn/mp/ymc/a52704.jspx
drivers/md/dm-crypt.c