3 releases
0.1.2 | Jan 2, 2020 |
---|---|
0.1.1 | Jan 2, 2020 |
0.1.0 | Jan 2, 2020 |
#1223 in Filesystem
17KB
225 lines
vblk
This crate provides an interface to create virtual block devices which call into your Rust code for read and write operations. This is similar to FUSE except that instead of exposing an entire filesystem, you only expose a single fixed-size file as a block device. This can be handy if you are interfacing with a remote block device but don't have any convenient means of mounting it for whatever reason, or for prototyping block device drivers.
This is achieved by (ab)using the Linux kernel's NBD driver rather than implementing a new generic block device driver; a basic NBD client is started in the Rust application serving NBD requests which are shuttled to and from the kernel driver through a Unix domain socket. Most of the time this is done through forking; in this crate a separate thread is started instead for hosting the kernel-side NBD server.
This crate only works on Linux 2.6+ and requires that the NBD kernel module be installed and loaded. It also requires root by default. Newer NBD features such as the FLUSH
and TRIM
commands may only be available in later Linux kernel versions; for older versions the Rust handlers for these commands will simply never be called.
Usage
Assuming you have the proper permissions and the NBD module is loaded, this example will mount a virtual block device on /dev/nbd0
which will read 0xDEADBEEF
across the entire block device. Since no write handler is specified, write requests to the block device will be denied. You can find and run this example in examples/deadbeef.rs
, and there is a ramdisk example showing how writes work in examples/ramdisk.rs
.
use std::io::Error;
use vblk::{mount, BlockDevice};
struct DeadbeefDevice;
impl BlockDevice for DeadbeefDevice {
fn read(&mut self, offset: u64, bytes: &mut [u8]) -> Result<(), Error> {
for (index, byte) in bytes.iter_mut().enumerate() {
*byte = match (index as u64 + offset) % 4 {
0 => 0xDE,
1 => 0xAD,
2 => 0xBE,
_ => 0xEF,
};
}
Ok(())
}
fn block_size(&self) -> u32 {
1024
}
fn blocks(&self) -> u64 {
4096 // 4MB device
}
}
fn main() {
unsafe { mount(&mut DeadbeefDevice, "/dev/nbd0", |_device| Ok(())).unwrap() };
}
Once it's running, you should be able to access and read the block device:
# xxd /dev/nbd0 | head -n 2
00000000: dead beef dead beef dead beef dead beef ................
00000010: dead beef dead beef dead beef dead beef ................
# blockdev --getsize64 /dev/nbd0
4194304
But attempting to write to the block device will fail as we haven't provided a write
handler, as observed below. Note that the oflag=dsync
parameter is important here, otherwise the writes will be cached and asynchronously written back by the kernel and you will find the write errors in dmesg
instead.
# dd oflag=dsync if=/dev/zero of=/dev/nbd0 bs=1024 count=4096
dd: error writing '/dev/nbd0': Input/output error
1+0 records in
0+0 records out
0 bytes copied, 0.000263995 s, 0.0 kB/s
Unmounting
The virtual block device will only get its unmount
handler called if the NBD connection is broken gracefully, so just sending Ctrl+C to your application will by default not do that. The callback you pass to the mount
function yields an owned Device
struct, which among other things has an unmount
method on it; the struct is intended to be stored somewhere and used whenever your application needs to shut down. You must not block in the callback
; the block device will not be mounted until it returns.
The examples show how to use this with the ctrlc
crate, by intercepting Ctrl+C and unmounting the block device, which will cause the mount
method to return gracefully.
Permissions
By default on most distributions only root (or users in the disk
group) can interface with the NBD kernel module and NBD block devices. I haven't tried this yet but it should be possible to configure the NBD device driver through e.g. udev
rules depending on your environment.
License
This software is provided under the MIT license.
Dependencies
~4.5MB
~96K SLoC