disktest_rawio/
linux.rs

1// -*- coding: utf-8 -*-
2//
3// disktest - Storage tester
4//
5// Copyright 2020-2024 Michael Büsch <m@bues.ch>
6//
7// Licensed under the Apache License version 2.0
8// or the MIT license, at your option.
9// SPDX-License-Identifier: Apache-2.0 OR MIT
10//
11
12use super::{RawIoOsIntf, RawIoResult};
13use anyhow as ah;
14use libc::{c_int, off_t, POSIX_FADV_DONTNEED};
15use std::{
16    fs::{metadata, File, OpenOptions},
17    io::{Read, Seek, SeekFrom, Write},
18    os::unix::{fs::MetadataExt as _, io::AsRawFd as _},
19    path::{Path, PathBuf},
20};
21
22#[allow(clippy::unnecessary_cast)]
23const S_IFBLK: u32 = libc::S_IFBLK as u32;
24#[allow(clippy::unnecessary_cast)]
25const S_IFCHR: u32 = libc::S_IFCHR as u32;
26#[allow(clippy::unnecessary_cast)]
27const S_IFMT: u32 = libc::S_IFMT as u32;
28
29/// Raw device I/O for Linux OS.
30pub struct RawIoLinux {
31    path: PathBuf,
32    file: Option<File>,
33    read_mode: bool,
34    write_mode: bool,
35    is_blk: bool,
36    is_chr: bool,
37    sector_size: Option<u32>,
38}
39
40impl RawIoLinux {
41    fn read_disk_geometry(&mut self) -> ah::Result<()> {
42        if let Ok(meta) = metadata(&self.path) {
43            let mode_ifmt = meta.mode() & S_IFMT;
44            if mode_ifmt == S_IFBLK {
45                self.is_blk = true;
46            }
47            if mode_ifmt == S_IFCHR {
48                self.is_chr = true;
49            }
50        }
51
52        if self.is_blk {
53            let Some(file) = self.file.as_ref() else {
54                return Err(ah::format_err!("No file object"));
55            };
56
57            let mut sector_size: c_int = 0;
58            // SAFETY: The ioctl call is safe, because:
59            // - The raw file descriptor is valid. (Closing sets self.file to None).
60            // - sector_size points to a valid and initialized c_int.
61            // - The ioctl only fetches the sector size and has no other side effects.
62            let res = unsafe {
63                libc::ioctl(
64                    file.as_raw_fd(),
65                    libc::BLKPBSZGET, // get physical sector size.
66                    &mut sector_size as *mut c_int,
67                )
68            };
69            if res < 0 {
70                return Err(ah::format_err!(
71                    "Get device block size: ioctl(BLKPBSZGET) failed."
72                ));
73            }
74            if sector_size <= 0 {
75                return Err(ah::format_err!(
76                    "Get device block size: ioctl(BLKPBSZGET) invalid size."
77                ));
78            }
79
80            self.sector_size = Some(sector_size as u32);
81        } else {
82            self.sector_size = None;
83        }
84        Ok(())
85    }
86}
87
88impl RawIoOsIntf for RawIoLinux {
89    fn new(path: &Path, mut create: bool, read: bool, write: bool) -> ah::Result<Self> {
90        if path.starts_with("/dev/") {
91            // Do not create dev nodes by accident.
92            // This check is not meant to catch all possible cases,
93            // but only the common ones.
94            create = false;
95        }
96
97        let file = match OpenOptions::new()
98            .create(create)
99            .read(read)
100            .write(write)
101            .open(path)
102        {
103            Ok(f) => f,
104            Err(e) => {
105                return Err(ah::format_err!("Failed to open file {:?}: {}", path, e));
106            }
107        };
108
109        let mut self_ = Self {
110            path: path.into(),
111            file: Some(file),
112            read_mode: read,
113            write_mode: write,
114            is_blk: false,
115            is_chr: false,
116            sector_size: None,
117        };
118
119        if let Err(e) = self_.read_disk_geometry() {
120            let _ = self_.close();
121            return Err(e);
122        }
123
124        Ok(self_)
125    }
126
127    fn get_sector_size(&self) -> Option<u32> {
128        self.sector_size
129    }
130
131    fn drop_file_caches(&mut self, offset: u64, size: u64) -> ah::Result<()> {
132        let Some(file) = self.file.take() else {
133            return Ok(());
134        };
135
136        if self.is_chr {
137            // This is a character device.
138            // We're done. Don't flush.
139            return Ok(());
140        }
141
142        if self.write_mode {
143            // fsync()
144            if let Err(e) = file.sync_all() {
145                return Err(ah::format_err!("Failed to flush: {}", e));
146            }
147        }
148
149        // Try FADV_DONTNEED to drop caches.
150        //
151        // SAFETY: The ioctl call is safe, because:
152        // - The raw file descriptor is valid. (Closing sets self.file to None).
153        // - fadvise DONTNEED has no safety relevant side effects.
154        let ret = unsafe {
155            libc::posix_fadvise(
156                file.as_raw_fd(),
157                offset as off_t,
158                size as off_t,
159                POSIX_FADV_DONTNEED,
160            )
161        };
162
163        if ret == 0 {
164            // fadvise success.
165            Ok(())
166        } else {
167            // Try global drop_caches.
168
169            drop(file);
170
171            let proc_file = "/proc/sys/vm/drop_caches";
172            let proc_value = b"3\n";
173
174            match OpenOptions::new().write(true).open(proc_file) {
175                Ok(mut file) => match file.write_all(proc_value) {
176                    Ok(_) => Ok(()),
177                    Err(e) => Err(ah::format_err!("{}", e)),
178                },
179                Err(e) => Err(ah::format_err!("{}", e)),
180            }
181        }
182    }
183
184    fn close(&mut self) -> ah::Result<()> {
185        let Some(file) = self.file.take() else {
186            return Ok(());
187        };
188        if self.write_mode && !self.is_chr {
189            if let Err(e) = file.sync_all() {
190                return Err(ah::format_err!("Failed to flush: {}", e));
191            }
192        }
193        Ok(())
194    }
195
196    fn sync(&mut self) -> ah::Result<()> {
197        if self.write_mode && !self.is_chr {
198            let Some(file) = self.file.as_mut() else {
199                return Err(ah::format_err!("No file object"));
200            };
201            file.sync_all()?;
202        }
203        Ok(())
204    }
205
206    fn set_len(&mut self, size: u64) -> ah::Result<()> {
207        if !self.write_mode {
208            return Err(ah::format_err!("File is opened without write permission."));
209        }
210        if self.is_chr || self.is_blk {
211            return Err(ah::format_err!("Cannot set length of raw device."));
212        }
213        let Some(file) = self.file.as_mut() else {
214            return Err(ah::format_err!("No file object"));
215        };
216        Ok(file.set_len(size)?)
217    }
218
219    fn seek(&mut self, offset: u64) -> ah::Result<u64> {
220        let Some(file) = self.file.as_mut() else {
221            return Err(ah::format_err!("No file object"));
222        };
223        Ok(file.seek(SeekFrom::Start(offset))?)
224    }
225
226    fn read(&mut self, buffer: &mut [u8]) -> ah::Result<RawIoResult> {
227        if !self.read_mode {
228            return Err(ah::format_err!("File is opened without read permission."));
229        }
230        let Some(file) = self.file.as_mut() else {
231            return Err(ah::format_err!("No file object"));
232        };
233        match file.read(buffer) {
234            Ok(count) => Ok(RawIoResult::Ok(count)),
235            Err(e) => Err(ah::format_err!("Read error: {}", e)),
236        }
237    }
238
239    fn write(&mut self, buffer: &[u8]) -> ah::Result<RawIoResult> {
240        if !self.write_mode {
241            return Err(ah::format_err!("File is opened without write permission."));
242        }
243        let Some(file) = self.file.as_mut() else {
244            return Err(ah::format_err!("No file object"));
245        };
246        if let Err(e) = file.write_all(buffer) {
247            if let Some(err_code) = e.raw_os_error() {
248                if err_code == libc::ENOSPC {
249                    return Ok(RawIoResult::Enospc);
250                }
251            }
252            return Err(ah::format_err!("Write error: {}", e));
253        }
254        Ok(RawIoResult::Ok(buffer.len()))
255    }
256}
257
258impl Drop for RawIoLinux {
259    fn drop(&mut self) {
260        if let Err(e) = self.close() {
261            eprintln!("Warning: Failed to close device: {}", e);
262        }
263    }
264}
265
266// vim: ts=4 sw=4 expandtab