How does rust use directIO?

The buffer I/O causes the system cache to be uncontrolled. In scenarios where resources are insufficient, directIO is a good choice. However, I encountered problems when using directIO.

Through libc's code, I can use it normally.

fn test_libc()  {
    let path = CString::new("/tmp/1.txt").unwrap();
    let flags = libc::O_DIRECT | libc::O_WRONLY | libc::O_CREAT;
    let mode: c_int = 0o644;
    let fd = unsafe { libc::open(path.as_ptr(), flags, mode) };
    use std::os::fd::FromRawFd;
    let mut file = unsafe {File::from_raw_fd(fd)};
    let buffer = vec![b'2'; 4096];
    file.write_all(&buffer).unwrap();
}

Using the standard library code, I ran into problems.

fn test_std()  {
    use std::os::unix::fs::OpenOptionsExt;
    let mut file = OpenOptions::new()
        .write(true)
        .create(true)
        .custom_flags(libc::O_DIRECT)
        .open("/tmp/1.txt").unwrap();
    let buffer = vec![b'1'; 4096];
    file.write_all(&buffer).unwrap();
}

Linux: 4.18.0

2 Likes

https://man7.org/linux/man-pages/man2/open.2.html

EINVAL The filesystem does not support the O_DIRECT flag. See
NOTES for more information.

And in NOTES there's a whole bunch of paragraphs on O_DIRECT, but the plausible ones seem like

Under Linux 2.4, transfer sizes, the alignment of the user
buffer, and the file offset must all be multiples of the logical
block size of the filesystem. Since Linux 2.6.0, alignment to
the logical block size of the underlying storage (typically 512
bytes) suffices. The logical block size can be determined using
the ioctl(2) BLKSSZGET operation or from the shell using the
command:

blockdev --getss

And

O_DIRECT support was added under Linux in kernel version 2.4.10.
Older Linux kernels simply ignore this flag. Some filesystems
may not implement the flag, in which case open() fails with the
error EINVAL if it is used.

If that panic line number is on the open(), I would guess you're hitting the latter, but then you should see the same thing earlier.

If it's the former paragraph, it would be on the later read() call itself, and it probably only works by accident on the first program. The reliable solution needs you to do an aligned allocation. There's no way to do this dynamically with a Vec in stable yet (the allocator_api nightly feature might provide a way in the future?), but for now you can either directly use alloc() (hopefully wrapped for safety) or create a buffer type with an #[align()] attribute

1 Like

This is a simple DirectIO I wrote, and I expected it to work, but it was somewhat unexpected. Case 1 works properly, but case 2 fails in the 512 scenario.


fn case1() {
    let path = "/tmp/1.txt";
    test(path, 512);
    println!("case1 success");
}

fn case2() {
    let path = "/tmp/1.txt";
    for i in 1..10240 {
        test(path, i);
    }
}

fn test(path: &str, len: usize) {
    let mut file = DirectIO::open(path).unwrap();
    let expect = vec![b'1'; len];
    file.write_all(&expect)
        .map_err(|e| {
            eprintln!("err len: {}", len);
            e
        })
        .unwrap();
    file.flush().unwrap();
    let mut file = File::open(path).unwrap();
    let mut actual = Vec::with_capacity(len);
    file.read_to_end(&mut actual).unwrap();
    assert_eq!(actual, expect);
    std::fs::remove_file(path).unwrap();
}

use libc::c_int;
use std::ffi::CString;
use std::io;
use std::io::Write;

const SIZE_OF_BLOCK: usize = 512;
const ALIGN_SIZE_OF_BLOCK: usize = !(SIZE_OF_BLOCK - 1);

pub struct DirectIO {
    fd: c_int,
    buf: [u8; SIZE_OF_BLOCK],
    n: usize,
}

impl Drop for DirectIO {
    fn drop(&mut self) {
        unsafe {
            let i = libc::close(self.fd);
            if i != 0 {
                panic!("{}", io::Error::last_os_error())
            }
        }
    }
}

impl DirectIO {
    pub fn open(path: &str) -> io::Result<Self> {
        let path = CString::new(path).unwrap();
        let flags = libc::O_DIRECT | libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC;
        let mode: c_int = 0o644;
        let fd = unsafe { libc::open(path.as_ptr(), flags, mode) };
        if fd == -1 {
            Err(io::Error::last_os_error())
        } else {
            Ok(Self {
                fd,
                buf: [0u8; SIZE_OF_BLOCK],
                n: 0,
            })
        }
    }

    fn write_direct(&self, buf: &[u8]) -> io::Result<usize> {
        if buf.is_empty() {
            return Ok(0);
        }
        let rt = unsafe { libc::write(self.fd, buf.as_ptr().cast(), buf.len()) };
        if rt >= 0 {
            Ok(rt as usize)
        } else {
            Err(io::Error::last_os_error())
        }
    }
}

impl Write for DirectIO {
    fn write(&mut self, mut buf: &[u8]) -> io::Result<usize> {
        let data_len = buf.len();
        if self.n != 0 {
            let end = self.n + buf.len();
            if end < self.buf.len() {
                self.buf[self.n..end].copy_from_slice(buf);
                self.n = end;
                return Ok(buf.len());
            } else {
                let r = self.buf.len() - self.n;
                self.buf[self.n..].copy_from_slice(&buf[..r]);
                let n = self.write_direct(&self.buf)?;
                assert_eq!(n, self.buf.len());
                self.n = 0;
                buf = &buf[r..];
            }
        }
        while buf.len() >= SIZE_OF_BLOCK {
            let r = buf.len() & ALIGN_SIZE_OF_BLOCK;
            let n = self.write_direct(&buf[..r])?;
            buf = &buf[n..];
        }
        if !buf.is_empty() {
            self.buf[0..buf.len()].copy_from_slice(buf);
            self.n = buf.len();
        }
        Ok(data_len)
    }

    fn flush(&mut self) -> io::Result<()> {
        let fd = self.fd;
        let flags: c_int = unsafe { libc::fcntl(fd, libc::F_GETFL) };
        if flags == -1 {
            return Err(std::io::Error::last_os_error());
        }
        let new_flags: c_int = flags & !libc::O_DIRECT;
        if unsafe { libc::fcntl(fd, libc::F_SETFL, new_flags) } == -1 {
            return Err(std::io::Error::last_os_error());
        }
        self.write_direct(&self.buf[..self.n]).unwrap();
        self.n = 0;
        if unsafe { libc::fcntl(fd, libc::F_SETFL, flags) } == -1 {
            return Err(std::io::Error::last_os_error());
        }
        Ok(())
    }
}

This simple code should have a bug, but I think it's aligned, but I don't know why it's still wrong, and it's possible to call test for a single call.

Note that for this:

pub struct DirectIO {
    fd: c_int,
    buf: [u8; SIZE_OF_BLOCK],
    n: usize,
}

that buf will be very unlikely to be aligned. You would need to do:

#[repr(C)] // lay out the items in order like C does
#[align(512)] // or whatever
pub struct DirectIO {
    buf: [u8; SIZE_OF_BLOCK], // must be first so it has the alignment of the type as a whole
    fd: c_int,
    n: usize,
}

Also, you probably don't want to unwrap() and therefore panic if you're trying to check each alignment...

1 Like

Thanks, unwrap is just written in the demo. I tried the writing you gave, but still the same problem.

#[repr(align(512))] // lay out the items in order like C does
pub struct DirectIO {
    buf: [u8; SIZE_OF_BLOCK],  // must be first so it has the alignment of the type as a whole
    fd: c_int,
    n: usize,
}

I think you need #[repr(C, align(512))] to tell rustc to not reorder the fields.

3 Likes

I replaced the implementation and solved the problem.

            let layout = Layout::from_size_align(size, 4096).unwrap();
            let data = unsafe { alloc::alloc::alloc(layout) as *mut u8 };

Another problem is how to perform write alignment in the flush method instead of using fcntl to change it to non-direct I/O.

impl Write for DirectIO {
    fn write(&mut self, mut buf: &[u8]) -> io::Result<usize> {}

    fn flush(&mut self) -> io::Result<()> {
        let fd = self.fd;
        let flags: c_int = unsafe { libc::fcntl(fd, libc::F_GETFL) };
        if flags == -1 {
            return Err(io::Error::last_os_error());
        }
        let new_flags: c_int = flags & !libc::O_DIRECT;
        if unsafe { libc::fcntl(fd, libc::F_SETFL, new_flags) } == -1 {
            return Err(io::Error::last_os_error());
        }
        let n = unsafe { libc::write(self.fd, self.buf.as_ptr().cast(), self.n as size_t) };
        if n < 0 {
            return Err(io::Error::last_os_error());
        }
        self.n = 0;
        if unsafe { libc::fcntl(fd, libc::F_SETFL, flags) } == -1 {
            return Err(io::Error::last_os_error());
        }
        Ok(())
    }
}

Mailing List Archive: open(O_DIRECT) on a tmpfs? (carbon60.com)

We can't open with O_DIRECT on tmpfs

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.