修改bio完成加解密文件系统
背景
bio是block io,它是一个描述硬盘里面的位置与page cache的页对应关系的数据结构,每个bio对应的是硬盘里面一块或多块连续的位置,每一块硬盘里面连续的位置,可能对应着page cache的多页或者一页,bio中bio_vec *bi_io_vec
的表保存着内存和硬盘位置的关系。
而每个bio_vec都只能描述一个页内数据的连续的数据的偏移和长度,当一次IO长度大于一页的时候需要一个数组来表示他们的关系:
通过bv_offset和bv_len来描述硬盘中起始位置和长度。
一个bio的处理:一个bio通常通过submit_bio
来提交给设备队列,下面会经过bio聚合,转化成request,磁盘调度队列,块设备驱动,磁盘处理;对于bio来说大部分都是异步,这样提交完bio请求之后CPU就可以去完成其他工作,在bio完成后通过bio->bi_end_io
来作为callback获取结果,外部的接口使用bio_endio。同步方式的读写请求是通过等待callback事件到来从而完成同步读写。
实践操作bio进行加解密
下面通过对块设备数据加解密来实践一下bio的操作,磁盘中保存的数据是密文,但是page cache中保存的数据时明文,对于文件系统来说是透明的,通过这样hook bio的操作实际上就完成了一次软加解密,和ecryptfs比较相似完成一个简单的加密文件系统,这里加密算法选用最简单的sm4(ecb)
加解密。
设计
submit_bio是发起读写IO的起点,也是bio和request层的临界点,非常适合来进行做一些小动作,所以通过hook submit_bio来进行写加密,保证提交给下面的数据是经过加密的就好了。而对于读IO可以通过在IO完成之后通知给文件系统之前进行解密,可以hook bio_endio来进行数据解密,确保page cache上的数据是经过解密后的明文。
写加密
bio只是管理的数据结构,数据仍然是存放在page cache中的,最终的结果要求是page cache中的内容仍然是明文,提交给磁盘的数据是密文。写的内容是保存在page cache中的,我们不能直接修改它否则page cache中的内容就变成密文了,一块数据是搞不定这件事了,我们需要重新clone一个bio,也就是申请一块内存来存放密文数据,将clone后的bio提交给通用块设备。
long origin_submit_bio(int rw, struct bio *bio);
void hook_submit_bio(int rw, struct bio *bio) //hook submit_bio,先完成加密操作然后将加密后的bio提交给block层
{
ret = try_crypt_bio(rw, bio); //克隆bio并对其中的内容进行加密
if (ret != 0) {
origin_submit_bio(rw, bio); //对于不需要进行加密的bio通过原路径提交给block
}
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; //利用bi_private成员引用原始的bio
new->bi_end_io = bfx_crypt_callback; //新的clone bio操作完成后通知原始的bio
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) {
return -ENOMEM;
}
new->bi_rw |= rw;
bio->bi_rw |= rw;
sm4_set_key1(&key, mykey, 16);
bio_for_each_segment(bv, bio, i) { //遍历bio对其内容进行原地加密操作
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); //将新的bio而不是原始的bio提交给block层
return 0;
}
读解密
相对于写操作,读IO就比较简单了,从磁盘中读取到了密文数据到page cache中,我们只需要在文件系统看到这块page cache前解密好,不需要倒腾内存,只需要原地解密,这里我们利用bio完成之后的callback,在其中进行原地解密之后再通知原始的bio。
遍历bio指向的内存数据:
void origin_bio_endio(struct bio *bio, int error);
void hook_bio_endio(struct bio *bio, int error) //hook正常的bio callbak:bio_endio
{
try_decrypt_bio(bio, error); //将读bio中的内容原地解密
origin_bio_endio(bio, error); //不需要加密的bio通过正常路径通知完成
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发送给磁盘的速度下降,瓶颈是在cpu这,可以考虑加个专用的片子进行加解密操作。另一个因为加解密操作和原来的bio流程是串行的,一条流水线多加了一段肯定会多耗一点实践,这时候可以考虑异步的加解密,也就是将加解密操作放在一个专用线程中,先将bio缓存下来慢慢加解密,另外用户对于读速度比较敏感,可以让读操作串行,写操作放在后台线程慢慢整吧。
参考
https://lwn.net/Articles/26404/
www.eeworld.com.cn/mp/ymc/a52704.jspx
drivers/md/dm-crypt.c