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}