While working on LearnFS, one of the task I had was to determine the block device size in bytes . This information was required to properly calculate FS parameters such as the number of inodes, sizes of inode and data bitmaps etc.

One option was to use sysfs. File /sys/block/<device>/size contains the number of 512-bytes sectors, so by simply reading it and multiplying the value by 512 we can get the size in bytes.

Let’s try:

$ echo $(( 512 * $(cat /sys/block/sda/size )))
500107862016

And indeed my disk size is 500 gigs.

However I wanted to use some more direct method. Something like ioctl.

Os opposed to most Linux system calls which are designed to do one thing, ioctl is somewhat more generic. It accepts a request code which specifies what operation to carry out. The result of the call depends on the operation.

There are many available request codes, but for our tasks we’re interested in one called BLKGETSIZE64. It is defined in linux/fs.h (usually in /usr/include/linux/fs.h):

#define BLKGETSIZE64 _IOR(0x12,114,size_t)   /* return device size in bytes (u64 *arg) */

The _IOR macro is used to define ioctl operations which read some value from IO devices. The first parameter 0x12 is the type id, something like a group, similar ioctl operations share the same type id.

The second parameter 114 is the sequence number, which must be unique for every registered ioctl out there.

The third parameter specifies the size of the data going in or coming out (as in our case) of the system call.

To translate this into Rust we’ll rely on nix crate, specifically its ioctl module.

The ioctl_read! macro is used to generate the implementation of the ioctl function:


// Generate ioctl function
const BLKGETSIZE64_CODE: u8 = 0x12; // Defined in linux/fs.h
const BLKGETSIZE64_SEQ: u8 = 114;
ioctl_read!(ioctl_blkgetsize64, BLKGETSIZE64_CODE, BLKGETSIZE64_SEQ, u64);

This macro call generates an unsafe function ioctl_blkgetsize64 which we’re going to use to get the device size:

   let mut cap = 0u64;
   let cap_ptr = &mut cap as *mut u64;

   unsafe {
      ioctl_blkgetsize64(fd, cap_ptr)?;
   }

The full code looks like this:

#[macro_use]
extern crate nix;

use std::env;
use std::os::unix::io::AsRawFd;
use std::fs::OpenOptions;

// Generate ioctl function
const BLKGETSIZE64_CODE: u8 = 0x12; // Defined in linux/fs.h
const BLKGETSIZE64_SEQ: u8 = 114;
ioctl_read!(ioctl_blkgetsize64, BLKGETSIZE64_CODE, BLKGETSIZE64_SEQ, u64);

/// Determine device size
fn get_device_size(path: &str) -> u64 {
   let file = OpenOptions::new()
             .write(true)
             .open(path).unwrap();

   let fd = file.as_raw_fd();

   let mut cap = 0u64;
   let cap_ptr = &mut cap as *mut u64;

   unsafe {
      ioctl_blkgetsize64(fd, cap_ptr).unwrap();
   }
  
   return cap;
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let path = &args[1];

    let size = get_device_size(path);
    println!("Device size is {} bytes", size);
}

Let’s try to run it

sudo target/debug/blksize /dev/sda
Device size is 500107862016 bytes

Exactly what we’ve seen in sysfs output before!

Note: Of course be extra careful when testing anything on your main disk, it’s better to create a loop device (with losetup) and do all the experiments on it.