Skip to main content

blktrace_rs/
session.rs

1//! The tracing session: kernel setup, per-CPU reader threads, teardown.
2
3use std::{
4    ffi::CStr,
5    fs::File,
6    os::fd::{AsFd, AsRawFd},
7    path::Path,
8    sync::{
9        atomic::{AtomicBool, Ordering},
10        mpsc, Arc,
11    },
12    thread,
13    time::Duration,
14};
15
16use crate::debugfs::ensure_debugfs;
17use crate::error::BlktraceError;
18use crate::event::{read_event, BlkTraceEvent};
19use crate::uapi::{
20    blk_user_trace_setup, blktrace_cat_BLK_TC_READ, blktrace_cat_BLK_TC_WRITE, ioctl,
21};
22
23/// Size of each relay buffer the kernel allocates, per CPU.
24const BLK_TRACE_BUF_SIZE: u32 = 4 * 1024 * 1024;
25/// Number of relay buffers the kernel keeps spare, per CPU.
26const BLK_TRACE_BUF_COUNT: u32 = 16;
27/// How long a reader waits before re-checking a relay channel that is at
28/// EOF; the kernel appends records as the traced I/O happens.
29const RETRY_INTERVAL: Duration = Duration::from_millis(10);
30/// How long shutdown waits for the kernel to flush pending events into the
31/// relay channels before draining them.
32const FLUSH_WAIT: Duration = Duration::from_secs(1);
33
34/// Entry point for starting a trace.
35pub struct BlkTrace;
36
37impl BlkTrace {
38    /// Starts tracing the block device at `path`.
39    ///
40    /// Only read and write events are captured. Reads served from the page
41    /// cache never reach the block layer, so call
42    /// [`BlkTrace::drop_page_cache`] before the reads you want traced.
43    /// Stopping with [`BlkTraceSession::shutdown`] returns every event
44    /// recorded in between; dropping the session instead stops tracing but
45    /// discards the events.
46    pub fn monitor(path: impl AsRef<Path>) -> Result<BlkTraceSession, BlktraceError> {
47        BlkTraceSession::start(path.as_ref())
48    }
49
50    /// Drops the page cache; see the module docs on
51    /// [`BlkTrace::monitor`] for when this matters.
52    pub fn drop_page_cache() -> std::io::Result<()> {
53        crate::debugfs::drop_page_cache()
54    }
55}
56
57/// The outcome of a completed tracing session.
58pub struct BlkTraceReport {
59    /// All events captured, in arrival order across CPUs.
60    pub events: Vec<BlkTraceEvent>,
61    /// Events the kernel dropped because a relay buffer overflowed.
62    /// Non-zero means the recording is incomplete.
63    pub dropped: u64,
64}
65
66/// A live tracing session.
67///
68/// Created by [`BlkTrace::monitor`]. Call [`BlkTraceSession::shutdown`] to
69/// stop tracing and collect the events; dropping it stops the kernel tracer
70/// but discards the events.
71pub struct BlkTraceSession {
72    device: File,
73    block_name: String,
74    cancel: Arc<AtomicBool>,
75    // `start` builds the session as the spawns succeed, so a spawn failure
76    // after BLKTRACESTART unwinds into `Drop`, which cancels and joins the
77    // threads here before stopping the kernel tracer.
78    readers: Vec<thread::JoinHandle<Result<(), BlktraceError>>>,
79    // Option so `shutdown` can move the handle out: this type implements
80    // Drop (kernel STOP/TEARDOWN), so fields cannot be moved out directly.
81    collector: Option<thread::JoinHandle<Vec<BlkTraceEvent>>>,
82}
83
84impl BlkTraceSession {
85    fn start(path: &Path) -> Result<Self, BlktraceError> {
86        ensure_debugfs()?;
87
88        let device = File::open(path)?;
89        let fd = device.as_fd();
90
91        let mut setup_data = blk_user_trace_setup {
92            // Capture read and write events only
93            // See: https://github.com/sdsc/blktrace/blob/dd093eb1c48e0d86b835758b96a9886fb7773aa4/act_mask.c#L40
94            act_mask: (blktrace_cat_BLK_TC_READ | blktrace_cat_BLK_TC_WRITE) as u16,
95            buf_size: BLK_TRACE_BUF_SIZE,
96            buf_nr: BLK_TRACE_BUF_COUNT,
97            ..Default::default()
98        };
99        // SAFETY: fd is an open block device fd; setup_data is a valid,
100        // fully initialized blk_user_trace_setup.
101        unsafe { ioctl::blktrace_setup(fd.as_raw_fd(), &mut setup_data) }
102            .map_err(BlktraceError::Setup)?;
103
104        // The kernel returns the name of the block device it registered
105        // the tracer under; relay channels and counters hang off it.
106        // SAFETY: `name` is a fixed-size array field of the live
107        // `setup_data`, so the slice covers aligned, in-bounds bytes;
108        // missing NUL is a `from_bytes_until_nul` error, not UB.
109        let block_name = CStr::from_bytes_until_nul(unsafe {
110            std::slice::from_raw_parts(setup_data.name.as_ptr() as *const u8, setup_data.name.len())
111        })
112        .map_err(|_| BlktraceError::InvalidBlockName)?
113        .to_str()
114        .map_err(|_| BlktraceError::InvalidBlockName)?
115        .to_owned();
116
117        // SAFETY: fd holds a configured blktrace from the setup ioctl above.
118        unsafe { ioctl::blktrace_start(fd.as_raw_fd()) }.map_err(BlktraceError::Start)?;
119
120        let cancel = Arc::new(AtomicBool::new(false));
121        let (tx, rx) = mpsc::channel::<BlkTraceEvent>();
122        let cpu_count = num_cpus::get();
123        // The kernel tracer is live from here on; every fallible step below
124        // returns through this value's Drop, which cancels the readers,
125        // joins them and issues STOP/TEARDOWN.
126        let mut session = Self {
127            device,
128            block_name,
129            cancel: Arc::clone(&cancel),
130            readers: Vec::with_capacity(cpu_count),
131            collector: None,
132        };
133
134        for cpu in 0..cpu_count {
135            let tx = tx.clone();
136            let cancel = Arc::clone(&cancel);
137            let relay_channel =
138                format!("/sys/kernel/debug/block/{}/trace{cpu}", session.block_name);
139            let reader = thread::Builder::new()
140                .name(format!("blktrace-reader-{cpu}"))
141                .spawn(move || read_relay_channel(&relay_channel, &cancel, tx))?;
142            session.readers.push(reader);
143        }
144        // The collector finishes once every reader has dropped its sender.
145        drop(tx);
146
147        let collector = thread::Builder::new()
148            .name("blktrace-collector".to_owned())
149            .spawn(move || {
150                let mut events = Vec::new();
151                while let Ok(event) = rx.recv() {
152                    events.push(event);
153                }
154                events
155            })?;
156        session.collector = Some(collector);
157
158        Ok(session)
159    }
160
161    /// Stops tracing and returns everything that was recorded.
162    pub fn shutdown(mut self) -> Result<BlkTraceReport, BlktraceError> {
163        // Flush the device buffers first: events the kernel is still
164        // holding land in the relay channels while we wait.
165        // SAFETY: fd is the block device this session was started on.
166        unsafe { ioctl::blkflsbuf(self.device.as_raw_fd()) }
167            .map_err(BlktraceError::FlushBuffers)?;
168
169        thread::sleep(FLUSH_WAIT);
170
171        self.cancel.store(true, Ordering::Relaxed);
172
173        // shutdown consumes the session, so the collector is always there;
174        // the None arm exists only because take() needs an Option.
175        let events = match self.collector.take() {
176            Some(collector) => collector
177                .join()
178                .map_err(|_| BlktraceError::ThreadPanicked)?,
179            None => Vec::new(),
180        };
181
182        // The readers exit on the cancel flag above, which is also what
183        // lets the collector's drain end; joining keeps their panics and
184        // read errors visible here.
185        for reader in self.readers.drain(..) {
186            reader.join().map_err(|_| BlktraceError::ThreadPanicked)??;
187        }
188
189        let dropped = self.read_dropped()?;
190        if dropped > 0 {
191            tracing::warn!("Dropped {dropped} blktrace events, the recording is incomplete");
192        }
193
194        // Dropping `self` issues BLKTRACESTOP/BLKTRACETEARDOWN.
195        drop(self);
196        Ok(BlkTraceReport { events, dropped })
197    }
198
199    fn read_dropped(&self) -> Result<u64, BlktraceError> {
200        let path = format!("/sys/kernel/debug/block/{}/dropped", self.block_name);
201        let content = std::fs::read_to_string(path).map_err(BlktraceError::ReadDropped)?;
202        content
203            .trim()
204            .parse::<u64>()
205            .map_err(|_| BlktraceError::ParseDropped)
206    }
207}
208
209impl Drop for BlkTraceSession {
210    fn drop(&mut self) {
211        // Signal the readers even on the discard path: without this a
212        // dropped session would leave its reader threads spinning on the
213        // relay channels forever.
214        self.cancel.store(true, Ordering::Relaxed);
215
216        // Join the threads before touching the kernel tracer: a reader
217        // must not still be reading a relay channel when it is torn down,
218        // and this is what makes a failed `start` clean up fully. shutdown
219        // has drained both lists by the time it reaches here.
220        for reader in self.readers.drain(..) {
221            let _ = reader.join();
222        }
223        if let Some(collector) = self.collector.take() {
224            // Every reader sender is gone after the joins above, so the
225            // collector's drain has ended.
226            let _ = collector.join();
227        }
228
229        // SAFETY: fd is the block device this session was started on.
230        if let Err(e) = unsafe { ioctl::blktrace_stop(self.device.as_raw_fd()) } {
231            tracing::warn!("Failed to BLKTRACESTOP: {e}");
232        }
233        // SAFETY: same fd, and the tracer is stopped above.
234        if let Err(e) = unsafe { ioctl::blktrace_teardown(self.device.as_raw_fd()) } {
235            tracing::warn!("Failed to BLKTRACETEARDOWN: {e}");
236        }
237    }
238}
239
240/// Reads events off one CPU's relay channel until the session is cancelled.
241fn read_relay_channel(
242    relay_channel: &str,
243    cancel: &AtomicBool,
244    tx: mpsc::Sender<BlkTraceEvent>,
245) -> Result<(), BlktraceError> {
246    let mut file = File::open(relay_channel)?;
247    tracing::trace!(relay_channel, "Starting to read from trace file");
248
249    loop {
250        match read_event(&mut file) {
251            Ok(event) => {
252                // A send error means the collector is gone; nothing left
253                // to do but stop.
254                if tx.send(event).is_err() {
255                    return Ok(());
256                }
257            }
258            Err(BlktraceError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
259                if cancel.load(Ordering::Relaxed) {
260                    // The session is stopping: give the kernel one last
261                    // chance to hand over what it has flushed.
262                    thread::sleep(RETRY_INTERVAL);
263                    match read_event(&mut file) {
264                        Ok(event) => {
265                            let _ = tx.send(event);
266                        }
267                        Err(BlktraceError::Io(e))
268                            if e.kind() == std::io::ErrorKind::UnexpectedEof =>
269                        {
270                            tracing::trace!(relay_channel, "Cancelled now");
271                            return Ok(());
272                        }
273                        Err(e) => return Err(e),
274                    }
275                } else {
276                    // The channel is empty for now; the kernel appends
277                    // records as the traced I/O happens.
278                    thread::sleep(RETRY_INTERVAL);
279                }
280            }
281            Err(e) => return Err(e),
282        }
283    }
284}