src/pxar/fuse.rs: introduce Context in order to add caching.

This patch introduces `Context` to hold the decoder, ino_offset and caches for
the attributes and the goodbye table.
By caching, certain callbacks can be handled without the need to read additional
data via the decoder, which improves performance.
The searching of the goodbye table is refactored as well, avoiding recursive
function calls in case of a hash collision.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
This commit is contained in:
Christian Ebner 2019-09-19 14:22:42 +02:00 committed by Dietmar Maurer
parent 1c93182371
commit 946b72a6b2

View File

@ -6,7 +6,7 @@ use std::collections::HashMap;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::ffi::{CStr, CString, OsStr}; use std::ffi::{CStr, CString, OsStr};
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::{BufReader, Read, Seek};
use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::Path; use std::path::Path;
use std::sync::Mutex; use std::sync::Mutex;
@ -17,7 +17,7 @@ use libc;
use libc::{c_char, c_int, c_void, size_t}; use libc::{c_char, c_int, c_void, size_t};
use super::decoder::Decoder; use super::decoder::Decoder;
use super::format_definition::{PxarEntry, PxarGoodbyeItem}; use super::format_definition::{PxarAttributes, PxarEntry, PxarGoodbyeItem};
/// Node ID of the root i-node /// Node ID of the root i-node
/// ///
@ -48,6 +48,8 @@ fn decoder_callback(_path: &Path) -> Result<(), Error> {
Ok(()) Ok(())
} }
type Inode = u64;
type Offset = u64;
/// FFI types for easier readability /// FFI types for easier readability
type Request = *mut c_void; type Request = *mut c_void;
type MutPtr = *mut c_void; type MutPtr = *mut c_void;
@ -86,6 +88,19 @@ struct FuseArgs {
allocated: c_int, allocated: c_int,
} }
/// Trait to create ReadSeek Decoder trait objects.
pub trait ReadSeek: Read + Seek {}
impl<T: Read + Seek> ReadSeek for T {}
/// `Context` for callback functions providing the decoder, caches and the
/// offset within the archive for the i-node given by the caller.
struct Context {
decoder: Decoder<Box<dyn ReadSeek>, fn(&Path) -> Result<(), Error>>,
goodbye_cache: Option<(Inode, Vec<(PxarGoodbyeItem, Offset, Offset)>)>,
attr_cache: Option<(Inode, PxarAttributes)>,
ino_offset: Offset,
}
/// `Session` stores a pointer to the session context and is used to mount the /// `Session` stores a pointer to the session context and is used to mount the
/// archive to the given mountpoint. /// archive to the given mountpoint.
pub struct Session { pub struct Session {
@ -186,19 +201,25 @@ impl Session {
// By storing the decoder as userdata of the session, each request may // By storing the decoder as userdata of the session, each request may
// access it. // access it.
let reader = BufReader::new(file); let reader: Box<dyn ReadSeek> = Box::new(BufReader::new(file));
let decoder = Decoder::new(reader, decoder_callback as fn(&Path) -> Result<(), Error>)?; let decoder = Decoder::new(reader, decoder_callback as fn(&Path) -> Result<(), Error>)?;
let session_decoder = Box::new(Mutex::new(decoder)); let ctx = Context {
decoder,
goodbye_cache: None,
attr_cache: None,
ino_offset: 0,
};
let session_ctx = Box::new(Mutex::new(ctx));
let session_ptr = unsafe { let session_ptr = unsafe {
fuse_session_new( fuse_session_new(
Some(&args), Some(&args),
Some(&oprs), Some(&oprs),
std::mem::size_of::<Operations>(), std::mem::size_of::<Operations>(),
// Ownership of session_decoder is passed to the session here. // Ownership of session_ctx is passed to the session here.
// It has to be reclaimed before dropping the session to free // It has to be reclaimed before dropping the session to free
// the decoder and close the underlying file. This is done inside // the `Context` and close the underlying file. This is done inside
// the destroy callback function. // the destroy callback function.
Box::into_raw(session_decoder) as ConstPtr, Box::into_raw(session_ctx) as ConstPtr,
) )
}; };
@ -273,33 +294,29 @@ impl Drop for Session {
} }
} }
/// Creates a context providing an exclusive mutable reference to the decoder. /// Creates a context providing an exclusive mutable reference to the `Context`.
/// ///
/// Each callback function needing access to the decoder can easily get an /// Each callback function needing access to the `Context` can easily get an
/// exclusive handle by running the code inside this context. /// exclusive handle by running the code inside this context.
/// Responses with error code can easily be generated by returning with the /// Responses with error code can easily be generated by returning with the
/// error code. /// error code.
/// The error code will be used to reply to libfuse. /// The error code will be used to reply to libfuse.
fn run_in_context<F>(req: Request, inode: u64, code: F) fn run_in_context<F>(req: Request, inode: u64, code: F)
where where
F: FnOnce( F: FnOnce(&mut Context) -> Result<(), i32>,
&mut Decoder<BufReader<File>, fn(&Path) -> Result<(), Error>>,
u64,
) -> Result<(), i32>,
{ {
let ptr = unsafe { let boxed_ctx = unsafe {
fuse_req_userdata(req) let ptr = fuse_req_userdata(req) as *mut Mutex<Context>;
as *mut Mutex<Decoder<BufReader<File>, fn(&Path) -> Result<(), Error>>> Box::from_raw(ptr)
}; };
let boxed_decoder = unsafe { Box::from_raw(ptr) }; let result = boxed_ctx
let result = boxed_decoder
.lock() .lock()
.map(|mut decoder| { .map(|mut ctx| {
let ino_offset = match inode { ctx.ino_offset = match inode {
FUSE_ROOT_ID => decoder.root_end_offset() - GOODBYE_ITEM_SIZE, FUSE_ROOT_ID => ctx.decoder.root_end_offset() - GOODBYE_ITEM_SIZE,
_ => inode, _ => inode,
}; };
code(&mut decoder, ino_offset) code(&mut ctx)
}) })
.unwrap_or(Err(libc::EIO)); .unwrap_or(Err(libc::EIO));
@ -309,8 +326,8 @@ where
} }
} }
// Release ownership of boxed decoder, do not drop it. // Release ownership of boxed context, do not drop it.
let _ = Box::into_raw(boxed_decoder); let _ = Box::into_raw(boxed_ctx);
} }
/// Return the correct offset for the item based on its `PxarEntry` mode /// Return the correct offset for the item based on its `PxarEntry` mode
@ -345,12 +362,10 @@ extern "C" fn init(_decoder: MutPtr) {
// Notting to do here for now // Notting to do here for now
} }
/// Cleanup the userdata created while creating the session, which is the decoder /// Cleanup the userdata created while creating the session, which is the `Context`
extern "C" fn destroy(decoder: MutPtr) { extern "C" fn destroy(ctx: MutPtr) {
// Get ownership of the decoder and drop it when Box goes out of scope. // Get ownership of the `Context` and drop it when Box goes out of scope.
unsafe { unsafe { Box::from_raw(ctx) };
Box::from_raw(decoder);
}
} }
/// FUSE entry for fuse_reply_entry in lookup callback /// FUSE entry for fuse_reply_entry in lookup callback
@ -363,8 +378,28 @@ struct EntryParam {
entry_timeout: f64, entry_timeout: f64,
} }
/// Update the goodbye table to the one corresponding to the i-node offset, both
/// given in the `Context`.
fn update_goodbye_cache(mut ctx: &mut Context) -> Result<(), i32> {
if let Some((off, _)) = &ctx.goodbye_cache {
if *off == ctx.ino_offset {
// Cache contains already the correct goodbye table
return Ok(());
}
}
// Inode did not match or cache is empty, need to update the cache
let gbt = ctx.decoder
.goodbye_table(None, ctx.ino_offset + GOODBYE_ITEM_SIZE)
.map_err(|_| libc::EIO)?;
ctx.goodbye_cache = Some((ctx.ino_offset, gbt));
Ok(())
}
/// Lookup the goodbye item identified by `filename` and its corresponding `hash` /// Lookup the goodbye item identified by `filename` and its corresponding `hash`
/// ///
/// Updates the goodbye table cache to contain the table for the directory given
/// by the i-node in the provided `Context`.
/// Search the first matching `hash` in the goodbye table, allowing for a fast /// Search the first matching `hash` in the goodbye table, allowing for a fast
/// comparison with the items. /// comparison with the items.
/// As there could be a hash collision, the found items filename is then compared /// As there could be a hash collision, the found items filename is then compared
@ -375,57 +410,60 @@ struct EntryParam {
/// The matching items archive offset, entry and payload size are returned. /// The matching items archive offset, entry and payload size are returned.
/// If there is no entry with matching `filename` and `hash` a `libc::ENOENT` is /// If there is no entry with matching `filename` and `hash` a `libc::ENOENT` is
/// returned. /// returned.
fn search_in_goodbye( fn find_goodbye_entry(
mut decoder: &mut Decoder<BufReader<File>, fn(&Path) -> Result<(), Error>>, mut ctx: &mut Context,
goodbye_table: &[(PxarGoodbyeItem, u64, u64)],
filename: &CStr, filename: &CStr,
hash: u64, hash: u64,
) -> Result<(u64, PxarEntry, u64), i32> { ) -> Result<(u64, PxarEntry, PxarAttributes, u64), i32> {
// Search for the first position with matching hash. update_goodbye_cache(&mut ctx)?;
let position = goodbye_table if let Some((_, gbt)) = &ctx.goodbye_cache {
.iter() let mut iterator = gbt.iter();
let mut position = 0;
loop {
// Search for the next position where the hash matches the one in the
// goodbye table.
position += iterator
.position(|(e, _, _)| e.hash == hash) .position(|(e, _, _)| e.hash == hash)
.ok_or(libc::ENOENT)?; .ok_or(libc::ENOENT)?;
let (_item, start, end) = &goodbye_table[position]; let (_item, start, end) = &gbt[position];
// At this point it is not clear if the item is a directory or not, this // At this point it is not clear if the item is a directory or not, this
// has to be decided based on the entry mode. // has to be decided based on the entry mode.
// `Decoder`s attributes function accepts both, offsets pointing to // `Decoder`s attributes function accepts both, offsets pointing to
// the start of an item (PXAR_FILENAME) or the GOODBYE_TAIL_MARKER in case // the start of an item (PXAR_FILENAME) or the GOODBYE_TAIL_MARKER in case
// of directories, so the use of start offset is fine for both cases. // of directories, so the use of start offset is fine for both cases.
let (entry_name, entry, _, payload_size) = decoder let (entry_name, entry, attr, payload_size) =
.attributes(*start) ctx.decoder.attributes(*start).map_err(|_| libc::EIO)?;
.map_err(|_| libc::EIO)?;
// Possible hash collision, need to check if the found entry is indeed // Possible hash collision, need to check if the found entry is indeed
// the filename to lookup. // the filename to lookup.
if entry_name.as_bytes() != filename.to_bytes() { if entry_name.as_bytes() == filename.to_bytes() {
// Hash collision, continue with the search for the next matching item.
search_in_goodbye(&mut decoder, &goodbye_table[position + 1..goodbye_table.len()], filename, hash)
} else {
// No hash collision, get the correct offset based on the entry mode.
let child_offset = find_offset(&entry, *start, *end); let child_offset = find_offset(&entry, *start, *end);
Ok((child_offset, entry, payload_size)) return Ok((child_offset, entry, attr, payload_size));
}
// Have to shift the position by one as next in iterator will be 0
position += 1;
} }
} }
/// Lookup `name` in the directory referenced by `parent` inode. Err(libc::ENOENT)
}
/// Lookup `name` in the directory referenced by `parent` i-node.
/// ///
/// Inserts also the child and parent file offset in the hashmap to quickly /// Inserts also the child and parent file offset in the hashmap to quickly
/// obtain the parent offset based on the child offset. /// obtain the parent offset based on the child offset.
/// Caches goodbye table of parent and attributes of child, if found.
extern "C" fn lookup(req: Request, parent: u64, name: StrPtr) { extern "C" fn lookup(req: Request, parent: u64, name: StrPtr) {
let filename = unsafe { CStr::from_ptr(name) }; let filename = unsafe { CStr::from_ptr(name) };
let hash = super::format_definition::compute_goodbye_hash(filename.to_bytes()); let hash = super::format_definition::compute_goodbye_hash(filename.to_bytes());
run_in_context(req, parent, |mut decoder, ino_offset| { run_in_context(req, parent, |mut ctx| {
let goodbye_table = decoder // find_ goodbye_entry() will also update the goodbye cache
.goodbye_table(None, ino_offset + GOODBYE_ITEM_SIZE) let (child_offset, entry, attr, payload_size) =
.map_err(|_| libc::EIO)?; find_goodbye_entry(&mut ctx, &filename, hash)?;
ctx.attr_cache = Some((child_offset, attr));
// Get the child item based on its hash and filename let child_inode = calculate_inode(child_offset, ctx.decoder.root_end_offset());
let (child_offset, entry, payload_size) =
search_in_goodbye(&mut decoder, &goodbye_table[..], &filename, hash)?;
let child_inode = calculate_inode(child_offset, decoder.root_end_offset());
let e = EntryParam { let e = EntryParam {
inode: child_inode, inode: child_inode,
@ -440,7 +478,7 @@ extern "C" fn lookup(req: Request, parent: u64, name: StrPtr) {
CHILD_PARENT CHILD_PARENT
.lock() .lock()
.map_err(|_| libc::EIO)? .map_err(|_| libc::EIO)?
.insert(child_offset, ino_offset); .insert(child_offset, ctx.ino_offset);
let _res = unsafe { fuse_reply_entry(req, Some(&e)) }; let _res = unsafe { fuse_reply_entry(req, Some(&e)) };
Ok(()) Ok(())
@ -470,8 +508,12 @@ fn stat(inode: u64, entry: &PxarEntry, payload_size: u64) -> Result<libc::stat,
} }
extern "C" fn getattr(req: Request, inode: u64, _fileinfo: MutPtr) { extern "C" fn getattr(req: Request, inode: u64, _fileinfo: MutPtr) {
run_in_context(req, inode, |decoder, ino_offset| { run_in_context(req, inode, |ctx| {
let (_, entry, _, payload_size) = decoder.attributes(ino_offset).map_err(|_| libc::EIO)?; let (_, entry, attr, payload_size) = ctx
.decoder
.attributes(ctx.ino_offset)
.map_err(|_| libc::EIO)?;
ctx.attr_cache = Some((ctx.ino_offset, attr));
let attr = stat(inode, &entry, payload_size)?; let attr = stat(inode, &entry, payload_size)?;
let _res = unsafe { let _res = unsafe {
// Since fs is read-only, the timeout can be max. // Since fs is read-only, the timeout can be max.
@ -484,13 +526,11 @@ extern "C" fn getattr(req: Request, inode: u64, _fileinfo: MutPtr) {
} }
extern "C" fn readlink(req: Request, inode: u64) { extern "C" fn readlink(req: Request, inode: u64) {
run_in_context(req, inode, |decoder, ino_offset| { run_in_context(req, inode, |ctx| {
let (target, _) = decoder let (target, _) = ctx
.read_link(ino_offset) .decoder
.map_err(|err| { .read_link(ctx.ino_offset)
println!("{}", err); .map_err(|_| libc::EIO)?;
libc::EIO
})?;
let link = CString::new(target.into_os_string().into_vec()).map_err(|_| libc::EIO)?; let link = CString::new(target.into_os_string().into_vec()).map_err(|_| libc::EIO)?;
let _ret = unsafe { fuse_reply_readlink(req, link.as_ptr()) }; let _ret = unsafe { fuse_reply_readlink(req, link.as_ptr()) };
@ -499,8 +539,8 @@ extern "C" fn readlink(req: Request, inode: u64) {
} }
extern "C" fn open(req: Request, inode: u64, fileinfo: MutPtr) { extern "C" fn open(req: Request, inode: u64, fileinfo: MutPtr) {
run_in_context(req, inode, |decoder, ino_offset| { run_in_context(req, inode, |ctx| {
decoder.open(ino_offset).map_err(|_| libc::ENOENT)?; ctx.decoder.open(ctx.ino_offset).map_err(|_| libc::ENOENT)?;
let _ret = unsafe { fuse_reply_open(req, fileinfo) }; let _ret = unsafe { fuse_reply_open(req, fileinfo) };
Ok(()) Ok(())
@ -508,9 +548,10 @@ extern "C" fn open(req: Request, inode: u64, fileinfo: MutPtr) {
} }
extern "C" fn read(req: Request, inode: u64, size: size_t, offset: c_int, _fileinfo: MutPtr) { extern "C" fn read(req: Request, inode: u64, size: size_t, offset: c_int, _fileinfo: MutPtr) {
run_in_context(req, inode, |decoder, ino_offset| { run_in_context(req, inode, |ctx| {
let mut data = decoder let mut data = ctx
.read(ino_offset, size, offset as u64) .decoder
.read(ctx.ino_offset, size, offset as u64)
.map_err(|_| libc::EIO)?; .map_err(|_| libc::EIO)?;
let _res = unsafe { let _res = unsafe {
@ -528,8 +569,11 @@ extern "C" fn read(req: Request, inode: u64, size: size_t, offset: c_int, _filei
/// This simply checks if the inode references a valid directory, no internal /// This simply checks if the inode references a valid directory, no internal
/// state identifies the directory as opened. /// state identifies the directory as opened.
extern "C" fn opendir(req: Request, inode: u64, fileinfo: MutPtr) { extern "C" fn opendir(req: Request, inode: u64, fileinfo: MutPtr) {
run_in_context(req, inode, |decoder, ino_offset| { run_in_context(req, inode, |ctx| {
let (_, entry, _, _) = decoder.attributes(ino_offset).map_err(|_| libc::EIO)?; let (_, entry, _, _) = ctx
.decoder
.attributes(ctx.ino_offset)
.map_err(|_| libc::EIO)?;
if (entry.mode as u32 & libc::S_IFMT) != libc::S_IFDIR { if (entry.mode as u32 & libc::S_IFMT) != libc::S_IFDIR {
return Err(libc::ENOENT); return Err(libc::ENOENT);
} }
@ -610,28 +654,35 @@ impl ReplyBuf {
} }
} }
/// Read and return the entries of the directory referenced by inode. /// Read and return the entries of the directory referenced by i-node.
/// ///
/// Replies to the request with the entries fitting into a buffer of length /// Replies to the request with the entries fitting into a buffer of length
/// `size`, as requested by the caller. /// `size`, as requested by the caller.
/// `offset` identifies the start index of entries to return. This is used on /// `offset` identifies the start index of entries to return. This is used on
/// repeated calls, occurring if not all entries fitted into the buffer. /// repeated calls, occurring if not all entries fitted into the buffer.
/// The goodbye table of the directory is cached in order to speedup repeated
/// calls occurring when not all entries fitted in the reply buffer.
extern "C" fn readdir(req: Request, inode: u64, size: size_t, offset: c_int, _fileinfo: MutPtr) { extern "C" fn readdir(req: Request, inode: u64, size: size_t, offset: c_int, _fileinfo: MutPtr) {
let offset = offset as usize; let offset = offset as usize;
run_in_context(req, inode, |decoder, ino_offset| { run_in_context(req, inode, |mut ctx| {
let gb_table = decoder.goodbye_table(None, ino_offset + GOODBYE_ITEM_SIZE).map_err(|_| libc::EIO)?; update_goodbye_cache(&mut ctx)?;
let gb_table = &ctx.goodbye_cache.as_ref().unwrap().1;
let n_entries = gb_table.len(); let n_entries = gb_table.len();
//let entries = decoder.list_dir(&dir).map_err(|_| libc::EIO)?;
let mut buf = ReplyBuf::new(req, size, offset); let mut buf = ReplyBuf::new(req, size, offset);
if offset < n_entries { if offset < n_entries {
for e in gb_table[offset..gb_table.len()].iter() { for e in gb_table[offset..gb_table.len()].iter() {
let entry = decoder.read_directory_entry(e.1, e.2).map_err(|_| libc::EIO)?; let entry = ctx
.decoder
.read_directory_entry(e.1, e.2)
.map_err(|_| libc::EIO)?;
let name = CString::new(entry.filename.as_bytes()).map_err(|_| libc::EIO)?; let name = CString::new(entry.filename.as_bytes()).map_err(|_| libc::EIO)?;
let (_, entry, _, payload_size) = decoder.attributes(e.1).map_err(|_| libc::EIO)?; let (_, entry, _, payload_size) =
ctx.decoder.attributes(e.1).map_err(|_| libc::EIO)?;
let item_offset = find_offset(&entry, e.1, e.2); let item_offset = find_offset(&entry, e.1, e.2);
let item_inode = calculate_inode(item_offset, decoder.root_end_offset()); let item_inode = calculate_inode(item_offset, ctx.decoder.root_end_offset());
let attr = stat(item_inode, &entry, payload_size).map_err(|_| libc::EIO)?; let attr = stat(item_inode, &entry, payload_size).map_err(|_| libc::EIO)?;
match buf.add_entry(&name, &attr) { match buf.add_entry(&name, &attr) {
Ok(ReplyBufState::Okay) => {} Ok(ReplyBufState::Okay) => {}
@ -643,7 +694,10 @@ extern "C" fn readdir(req: Request, inode: u64, size: size_t, offset: c_int, _fi
// Add current directory entry "." // Add current directory entry "."
if offset <= n_entries { if offset <= n_entries {
let (_, entry, _, payload_size) = decoder.attributes(ino_offset).map_err(|_| libc::EIO)?; let (_, entry, _, payload_size) = ctx
.decoder
.attributes(ctx.ino_offset)
.map_err(|_| libc::EIO)?;
// No need to calculate i-node for current dir, since it is given as parameter // No need to calculate i-node for current dir, since it is given as parameter
let attr = stat(inode, &entry, payload_size).map_err(|_| libc::EIO)?; let attr = stat(inode, &entry, payload_size).map_err(|_| libc::EIO)?;
let name = CString::new(".").unwrap(); let name = CString::new(".").unwrap();
@ -657,13 +711,14 @@ extern "C" fn readdir(req: Request, inode: u64, size: size_t, offset: c_int, _fi
// Add parent directory entry ".." // Add parent directory entry ".."
if offset <= n_entries + 1 { if offset <= n_entries + 1 {
let parent_off = if inode == FUSE_ROOT_ID { let parent_off = if inode == FUSE_ROOT_ID {
decoder.root_end_offset() - GOODBYE_ITEM_SIZE ctx.decoder.root_end_offset() - GOODBYE_ITEM_SIZE
} else { } else {
let guard = CHILD_PARENT.lock().map_err(|_| libc::EIO)?; let guard = CHILD_PARENT.lock().map_err(|_| libc::EIO)?;
*guard.get(&ino_offset).ok_or_else(|| libc::EIO)? *guard.get(&ctx.ino_offset).ok_or_else(|| libc::EIO)?
}; };
let (_, entry, _, payload_size) = decoder.attributes(parent_off).map_err(|_| libc::EIO)?; let (_, entry, _, payload_size) =
let item_inode = calculate_inode(parent_off, decoder.root_end_offset()); ctx.decoder.attributes(parent_off).map_err(|_| libc::EIO)?;
let item_inode = calculate_inode(parent_off, ctx.decoder.root_end_offset());
let attr = stat(item_inode, &entry, payload_size).map_err(|_| libc::EIO)?; let attr = stat(item_inode, &entry, payload_size).map_err(|_| libc::EIO)?;
let name = CString::new("..").unwrap(); let name = CString::new("..").unwrap();
match buf.add_entry(&name, &attr) { match buf.add_entry(&name, &attr) {