Rust library for building linux ublk target device, which talks with
linux ublk driver
1 for exposing standard linux block device,
meantime all target IO logic can be moved to userspace.
Linux kernel 6.0 starts to support ublk covered by config option of CONFIG_BLK_DEV_UBLK.
Follows one totally working 2-queue ublk-null target which is built over libublk 0.1, and each queue depth is 64, and each IO's max buffer size is 512KB.
To use libublk
crate, first add this to your Cargo.toml
:
[dependencies]
libublk = "0.1"
Next we can start using libublk
crate.
The following is quick introduction for adding ublk-null block device, which
is against low level APIs.
use libublk::{ctrl::UblkCtrl, UBLK_DEV_F_ADD_DEV};
use libublk::io::{UblkDev, UblkIOCtx, UblkQueue};
use std::sync::Arc;
fn main() {
let nr_queues = 2; //two queues
//io depth: 64, max buf size: 512KB
let mut ctrl = UblkCtrl::new(-1, nr_queues, 64, 512 << 10, 0, UBLK_DEV_F_ADD_DEV).unwrap();
// target specific initialization by tgt_init closure, which is flexible
// for customizing target with captured environment
let tgt_init = |dev: &mut UblkDev| {
dev.set_default_params(250_u64 << 30);
Ok(serde_json::json!({}))
};
let ublk_dev =
Arc::new(UblkDev::new("null".to_string(), tgt_init, &mut ctrl).unwrap());
let mut threads = Vec::new();
for q in 0..nr_queues {
let dev = Arc::clone(&ublk_dev);
threads.push(std::thread::spawn(move || {
let mut queue = UblkQueue::new(q as u16, &dev).unwrap();
let ctx = queue.make_queue_ctx();
//IO handling closure(FnMut), we are driven by io_uring
//CQE, and this closure is called for every incoming CQE
//(IO command or target io completion)
let io_handler = move |io: &mut UblkIOCtx| {
let iod = ctx.get_iod(io.get_tag());
let bytes = unsafe { (*iod).nr_sectors << 9 } as i32;
io.complete_io(bytes);
Ok(0)
};
queue.wait_and_handle_io(io_handler);
}));
}
ctrl.start_dev(&ublk_dev).unwrap();
ctrl.dump();
for qh in threads {
qh.join().unwrap();
}
ctrl.stop_dev(&ublk_dev).unwrap();
}
The following ublk-null block device is built over high level APIs, which doesn't support IO closure of FnMut.
use libublk::io::{UblkDev, UblkIOCtx, UblkQueueCtx};
use libublk::{ctrl::UblkCtrl, UblkError};
fn main() {
let sess = libublk::UblkSessionBuilder::default()
.name("null")
.depth(64_u32)
.nr_queues(2_u32)
.dev_flags(UBLK_DEV_F_ADD_DEV)
.build()
.unwrap();
let tgt_init = |dev: &mut UblkDev| {
dev.set_default_params(250_u64 << 30);
Ok(serde_json::json!({}))
};
let wh = {
let (mut ctrl, dev) = sess.create_devices(tgt_init).unwrap();
let handle_io = move |ctx: &UblkQueueCtx, io: &mut UblkIOCtx| -> Result<i32, UblkError> {
let iod = ctx.get_iod(io.get_tag());
io.complete_io(unsafe { (*iod).nr_sectors << 9 } as i32);
Ok(0)
};
sess.run(&mut ctrl, &dev, handle_io, |dev_id| {
let mut d_ctrl = UblkCtrl::new_simple(dev_id, 0).unwrap();
d_ctrl.dump();
})
.unwrap()
};
wh.join().unwrap();
}
You can run the test of the library with the following command.
# cargo test
When running fio t/io_uring /dev/ublkb0
2, IOPS is basically same with
running same test over ublk device created by blktests miniublk
3, which
is written by pure C. And the ublk device is null, which has 2 queues, each
queue's depth is 64.
-
add one null ublk device
cargo run --example null -- add
-
del one null ublk device
cargo run --example null -- del [dev_id]
-
add one loop ublk device
cargo run --example loop -- add ${backing_file_path}
-
del one loop ublk device
cargo run --example loop -- del [dev_id]
This project is licensed under either of Apache License, Version 2.0 or MIT license at your option.
Any kinds of contributions are welcome!