use std::{
ffi::CStr,
fs::File,
os::fd::{AsFd, AsRawFd},
path::Path,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
thread,
time::Duration,
};
use crate::debugfs::ensure_debugfs;
use crate::error::BlktraceError;
use crate::event::{read_event, BlkTraceEvent};
use crate::uapi::{
blk_user_trace_setup, blktrace_cat_BLK_TC_READ, blktrace_cat_BLK_TC_WRITE, ioctl,
};
const BLK_TRACE_BUF_SIZE: u32 = 4 * 1024 * 1024;
const BLK_TRACE_BUF_COUNT: u32 = 16;
const RETRY_INTERVAL: Duration = Duration::from_millis(10);
const FLUSH_WAIT: Duration = Duration::from_secs(1);
pub struct BlkTrace;
impl BlkTrace {
pub fn monitor(path: impl AsRef<Path>) -> Result<BlkTraceSession, BlktraceError> {
BlkTraceSession::start(path.as_ref())
}
pub fn drop_page_cache() -> std::io::Result<()> {
crate::debugfs::drop_page_cache()
}
}
pub struct BlkTraceReport {
pub events: Vec<BlkTraceEvent>,
pub dropped: u64,
}
pub struct BlkTraceSession {
device: File,
block_name: String,
cancel: Arc<AtomicBool>,
readers: Vec<thread::JoinHandle<Result<(), BlktraceError>>>,
collector: Option<thread::JoinHandle<Vec<BlkTraceEvent>>>,
}
impl BlkTraceSession {
fn start(path: &Path) -> Result<Self, BlktraceError> {
ensure_debugfs()?;
let device = File::open(path)?;
let fd = device.as_fd();
let mut setup_data = blk_user_trace_setup {
act_mask: (blktrace_cat_BLK_TC_READ | blktrace_cat_BLK_TC_WRITE) as u16,
buf_size: BLK_TRACE_BUF_SIZE,
buf_nr: BLK_TRACE_BUF_COUNT,
..Default::default()
};
unsafe { ioctl::blktrace_setup(fd.as_raw_fd(), &mut setup_data) }
.map_err(BlktraceError::Setup)?;
let block_name = CStr::from_bytes_until_nul(unsafe {
std::slice::from_raw_parts(setup_data.name.as_ptr() as *const u8, setup_data.name.len())
})
.map_err(|_| BlktraceError::InvalidBlockName)?
.to_str()
.map_err(|_| BlktraceError::InvalidBlockName)?
.to_owned();
unsafe { ioctl::blktrace_start(fd.as_raw_fd()) }.map_err(BlktraceError::Start)?;
let cancel = Arc::new(AtomicBool::new(false));
let (tx, rx) = mpsc::channel::<BlkTraceEvent>();
let cpu_count = num_cpus::get();
let mut session = Self {
device,
block_name,
cancel: Arc::clone(&cancel),
readers: Vec::with_capacity(cpu_count),
collector: None,
};
for cpu in 0..cpu_count {
let tx = tx.clone();
let cancel = Arc::clone(&cancel);
let relay_channel =
format!("/sys/kernel/debug/block/{}/trace{cpu}", session.block_name);
let reader = thread::Builder::new()
.name(format!("blktrace-reader-{cpu}"))
.spawn(move || read_relay_channel(&relay_channel, &cancel, tx))?;
session.readers.push(reader);
}
drop(tx);
let collector = thread::Builder::new()
.name("blktrace-collector".to_owned())
.spawn(move || {
let mut events = Vec::new();
while let Ok(event) = rx.recv() {
events.push(event);
}
events
})?;
session.collector = Some(collector);
Ok(session)
}
pub fn shutdown(mut self) -> Result<BlkTraceReport, BlktraceError> {
unsafe { ioctl::blkflsbuf(self.device.as_raw_fd()) }
.map_err(BlktraceError::FlushBuffers)?;
thread::sleep(FLUSH_WAIT);
self.cancel.store(true, Ordering::Relaxed);
let events = match self.collector.take() {
Some(collector) => collector
.join()
.map_err(|_| BlktraceError::ThreadPanicked)?,
None => Vec::new(),
};
for reader in self.readers.drain(..) {
reader.join().map_err(|_| BlktraceError::ThreadPanicked)??;
}
let dropped = self.read_dropped()?;
if dropped > 0 {
tracing::warn!("Dropped {dropped} blktrace events, the recording is incomplete");
}
drop(self);
Ok(BlkTraceReport { events, dropped })
}
fn read_dropped(&self) -> Result<u64, BlktraceError> {
let path = format!("/sys/kernel/debug/block/{}/dropped", self.block_name);
let content = std::fs::read_to_string(path).map_err(BlktraceError::ReadDropped)?;
content
.trim()
.parse::<u64>()
.map_err(|_| BlktraceError::ParseDropped)
}
}
impl Drop for BlkTraceSession {
fn drop(&mut self) {
self.cancel.store(true, Ordering::Relaxed);
for reader in self.readers.drain(..) {
let _ = reader.join();
}
if let Some(collector) = self.collector.take() {
let _ = collector.join();
}
if let Err(e) = unsafe { ioctl::blktrace_stop(self.device.as_raw_fd()) } {
tracing::warn!("Failed to BLKTRACESTOP: {e}");
}
if let Err(e) = unsafe { ioctl::blktrace_teardown(self.device.as_raw_fd()) } {
tracing::warn!("Failed to BLKTRACETEARDOWN: {e}");
}
}
}
fn read_relay_channel(
relay_channel: &str,
cancel: &AtomicBool,
tx: mpsc::Sender<BlkTraceEvent>,
) -> Result<(), BlktraceError> {
let mut file = File::open(relay_channel)?;
tracing::trace!(relay_channel, "Starting to read from trace file");
loop {
match read_event(&mut file) {
Ok(event) => {
if tx.send(event).is_err() {
return Ok(());
}
}
Err(BlktraceError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
if cancel.load(Ordering::Relaxed) {
thread::sleep(RETRY_INTERVAL);
match read_event(&mut file) {
Ok(event) => {
let _ = tx.send(event);
}
Err(BlktraceError::Io(e))
if e.kind() == std::io::ErrorKind::UnexpectedEof =>
{
tracing::trace!(relay_channel, "Cancelled now");
return Ok(());
}
Err(e) => return Err(e),
}
} else {
thread::sleep(RETRY_INTERVAL);
}
}
Err(e) => return Err(e),
}
}
}