A Remembrance of Things Past: Writing Mass Storage Device Drivers

By Dmitriy Budko


When was the last time you used a RAM drive? Do you still remember what a RAM drive is?

On my first PC (an AT 286, 12 MHz, 2 MB RAM) a RAM disk was the only workable way to use more than 1 MB of memory while running under MS-DOS. Unlike creaky MS-DOS, a current OS like the BeOS usually uses free memory in the file system cache; in general, caching algorithms provide much better performance than a simple RAM disk. Recently though, I was overcome by inexplicable nostalgia for the good old days and I decided to write a driver for that "disk" of yesteryear.

So if you have memory to burn, you may want to try using some of your surplus as a RAM "disk." If nothing else you'll find out that a RAM disk driver is an excellent example of using a device driver not only as a hardware interface but also to emulate it! And if you develop a file system driver, a big RAM disk can speed up the testing.

I assume that you know the basics of BeOS device drivers, so I'll focus on features specific to mass storage devices (floppy drive, IDE disk, IEEE 1394 [FireWire] disk) and on some tricks related to the volatile nature of RAM.

The driver has to support all standard entry points. In addition it has to handle some mass storage device I/O control codes.

I tried to make the driver as simple as possible, so it supports only one RAM disk. Its dynamic configuration is almost nonexistent, and its emulation of the hard drive seek process is very primitive. This simplification let me forget about synchronization problems.

Here's the driver source code. You can find the original source, unaltered for or by e-mail transmission, at:

ftp://ftp.be.com/pub/samples/drivers/obsolete/ramdrive.zip

/****************** cut here ******************************/

#include <OS.h>
#include <Drivers.h>
#include <KernelExport.h>
#include <stdlib.h>

status_t vd_open(const char *name, uint32 flags,
                 void **cookie);
status_t vd_free (void *cookie);
status_t vd_close(void *cookie);
status_t vd_control(void *cookie, uint32 msg, void *buf,
                    size_t size);
status_t vd_read(void *cookie, off_t pos, void *buf,
                 size_t *count);
status_t vd_write(void *cookie, off_t pos, const void *buf,
                  size_t *count);

static void format_ram_drive(void* buf);
static uchar* create_ram_drive_area(size_t drive_size);
static status_t delete_ram_drive_area(void);
static void emulate_seek(off_t pos);

#define RAM_DRIVE_RELEASE_MEMORY  (B_DEVICE_OP_CODES_END+1)
#define RAM_DRIVE_EMULATE_SEEK    (B_DEVICE_OP_CODES_END+2)

#define RAM_DRIVE_SIZE        (8*1024*1024)
#define RAM_BLOCK_SIZE        512
#define MAX_SEEK_TIME         1000.0      /* microseconds */
#define PREFETCH_BUFFER_SIZE  (32*1024)

static const char* const
    ram_drive_area_name = "RAM drive area";
uchar icon_disk[B_LARGE_ICON * B_LARGE_ICON];
uchar icon_disk_mini[B_MINI_ICON * B_MINI_ICON];
int   emulate_seek_flag = FALSE;
uchar * ram = NULL;

static const char *vd_name[] = {
  "disk/virtual/ram_drive",
  NULL
};

device_hooks vd_devices = {
  vd_open,
  vd_close,
  vd_free,
  vd_control,
  vd_read,
  vd_write
};

status_t
init_driver(void)
{
  dprintf("vd driver: %s %s, init_driver()\n",
    __DATE__, __TIME__);
  ram = create_ram_drive_area(RAM_DRIVE_SIZE);
  if(ram == NULL)
    return B_ERROR;
  return B_NO_ERROR;
}

void
uninit_driver(void)
{
  dprintf("vd driver: uninit_driver()\n");
}

const char**
publish_devices()
{
  dprintf("vd driver: publish_devices()\n");
  return vd_name;
}

device_hooks*
find_device(const char* name)
{
  dprintf("vd driver: find_device()\n");
  return &vd_devices;
}

status_t
vd_open(const char *dname, uint32 flags, void **cookie)
{
  dprintf("vd driver: open(%s)\n", dname);
  return B_NO_ERROR;
}

status_t
vd_free (void *cookie)
{
  dprintf("vd driver: free()\n");
  return B_NO_ERROR;
}

status_t
vd_close(void *cookie)
{
  dprintf("vd driver: close()\n");
  return B_NO_ERROR;
}

status_t
vd_read(void *cookie, off_t pos, void *buf, size_t *count)
{
  size_t len;
  status_t ret = B_NO_ERROR;

  if(pos >= RAM_DRIVE_SIZE)
  {
    len = 0;
  }
  else
  {
    len = (pos + (*count) > RAM_DRIVE_SIZE) ?
            (RAM_DRIVE_SIZE - pos) : (*count);
    emulate_seek(pos);
    memcpy(buf, ram+pos, len);
  }
  *count = len;
  return ret;
}

status_t
vd_write(void *cookie, off_t pos, const void *buf,
         size_t *count)
{
  size_t len;
  status_t ret = B_NO_ERROR;

  if(pos >= RAM_DRIVE_SIZE)
  {
    len = 0;
  }
  else
  {
    len = (pos + (*count) > RAM_DRIVE_SIZE) ?
            (RAM_DRIVE_SIZE - pos) : (*count);
    emulate_seek(pos);
    memcpy(ram+pos, buf, len);
  }
  *count = len;
  return ret;
}

status_t
vd_control(void *cookie, uint32 ioctl, void *arg1,
           size_t len)
{
  device_geometry  *dinfo;

  dprintf("vd driver: control(%d)\n", ioctl);
  switch (ioctl)
  {

  /* generic mass storage device IO control codes */

  case B_GET_GEOMETRY:
    dinfo = (device_geometry *) arg1;

    dinfo->sectors_per_track =
      RAM_DRIVE_SIZE/RAM_BLOCK_SIZE;
    dinfo->cylinder_count  = 1;
    dinfo->head_count      = 1;

    dinfo->bytes_per_sector = RAM_BLOCK_SIZE;
    dinfo->removable = FALSE ;
    dinfo->read_only = FALSE;
    dinfo->device_type = B_DISK;
    dinfo->write_once = FALSE;

    return B_NO_ERROR;

  case B_FORMAT_DEVICE:
    format_ram_drive(ram);
    return B_NO_ERROR;

  case B_GET_DEVICE_SIZE:
    *(size_t*)arg1 = RAM_DRIVE_SIZE;
    return B_NO_ERROR;

  case B_GET_ICON:
    switch (((device_icon *)arg1)->icon_size)
    {
    case B_LARGE_ICON:
      memcpy(((device_icon *)arg1)->icon_data, icon_disk,
        B_LARGE_ICON * B_LARGE_ICON);
      break;

    case B_MINI_ICON:
      memcpy(((device_icon *)arg1)->icon_data,
        icon_disk_mini, B_MINI_ICON * B_MINI_ICON);
      break;

    default:
      return B_BAD_TYPE;
    }
    return B_NO_ERROR;

  /* device specific IO control codes */

  case RAM_DRIVE_RELEASE_MEMORY:
    return delete_ram_drive_area();

  case RAM_DRIVE_EMULATE_SEEK:
    emulate_seek_flag = *(int*)arg1;
    return B_NO_ERROR;

  default:
    return B_ERROR;
  }
}

static void format_ram_drive(void* buf)
{
  static const char format_str[16] = "RAM drive   ";
  uchar* ptr = (uchar*)buf;
  off_t i;

  dprintf("vd driver: format_ram_drive(%08x)\n", buf);
  for(i=0; i<RAM_DRIVE_SIZE/16; i++)
  {
    memcpy(ptr, format_str, 16);
    ptr += 16;
  }

}

static uchar*
create_ram_drive_area(size_t drive_size)
{
  void* addr;
  area_id area = find_area(ram_drive_area_name);

  if(area == B_NAME_NOT_FOUND)
  {
    area = create_area (ram_drive_area_name,
      &addr,
      B_ANY_KERNEL_ADDRESS,/*kernel team will own this area*/
      drive_size,
      B_LAZY_LOCK,
      B_READ_AREA | B_WRITE_AREA);

    if((area==B_ERROR) ||
       (area==B_NO_MEMORY) ||
       (area==B_BAD_VALUE))
      addr = NULL;
  }
  else
  {
    area_info info;

    get_area_info(area, &info);
    addr = info.address;
  }
  return (uchar*)addr;
}

static status_t
delete_ram_drive_area(void)
{
  area_id  area = find_area(ram_drive_area_name);

  if(area == B_NAME_NOT_FOUND)
    return B_ERROR;
  else
    return delete_area(area);
}

static void
emulate_seek(off_t pos)
{
  static off_t old_pos = 0;

  if(!emulate_seek_flag)
    return;

  if(abs(pos-old_pos)>PREFETCH_BUFFER_SIZE)
  {
    old_pos = pos;
    snooze((int)(rand()* MAX_SEEK_TIME)/RAND_MAX);
  }
}

/****************** cut here ******************************/

What happens is that init_driver() creates the kernel memory area that's used to store the data. The driver can't use malloc()/free() because when the driver is unloaded the memory (and all data) is lost. It's actually present somewhere, but there's no easy way to find it when the driver is loaded again.

So init_driver() calls create_ram_drive_area(), which tries to find the previously created memory area with the name "RAM drive area." If the search is unsuccessful the driver is loaded the first time. In that case create_ram_drive_area() creates the memory area. It uses a currently undocumented flag, B_ANY_KERNEL_ADDRESS. This flag gives ownership of this memory area to the kernel, so the area will not be deleted when the application that opened the driver quits.

It also uses the B_LAZY_LOCK flag so the driver doesn't consume RAM until the first time you use it. I could have used B_FULL_LOCK and put the memory allocation in vd_open(), but being lazy I didn't want to provide a critical section synchronization for it.

vd_open() can be called a few times simultaneously; init_driver cannot. publish_devices() creates the device file in /dev/disk/virtual/ram_drive. The "/virtual/ram_drive" part of the name is arbitrary, but "/dev/disk" is mandatory if you want to use a standard disk setup program -- like DriveSetup -- to configure the RAM disk.

vd_open(), vd_free(), and vd_close() do nothing and return success.

vd_read() and vd_write() check to see if the caller tries to read/write over the end of the disk. They adjust the requested length accordingly, then simply copy the data from/to the requested offset in the RAM area to/from the caller's buffer.

If emulate_seek_flag is set the driver calls emulate_seek(). emulate_seek() is a crude imitation of real hard drive mechanics and caching. The current implementation is not in a critical section so it is capable of multiple concurrent seeks. It would be great to have a real drive with an infinite number of head arms! Readers are welcome to create a more realistic model.

Still, with this function implemented, the RAM drive is better suited for testing a file system driver. It can block the caller's tread as a driver for a real drive would do.

vd_control() handles four generic I/O control codes for a mass storage device: B_GET_GEOMETRY, B_FORMAT_DEVICE, B_GET_DEVICE_SIZE, B_GET_ICON. The first three are mandatory; the last one is optional.

This driver has zero-initialized arrays for large and small icons. The real driver should provide more pleasing icons than black rectangles.

RAM_DRIVE_RELEASE_MEMORY deletes the allocated memory area and thus destroys all information on the RAM drive. RAM_DRIVE_EMULATE_SEEK sets or clears the emulate_seek_flag. A control panel application for the RAM drive could send such commands.

Now here's how to build and install the driver:

  1. Instruct the linker to produce an add-on image.
  2. Disable linking to the default shared system libraries.
  3. Export the driver's entry points -- for example, by exporting everything.
  4. Place a copy of the appropriate kernel file (kernel_joe, kernel_mac, or kernel_intel) in your project directory. Link against this file.
  5. Copy the compiled driver in the /boot/home/config/add-ons/kernel/drivers directory.
  6. Use your favorite disk format program or BeOS preferences/DriveSetup to partition and/or install BFS on the /dev/disk/virtual/ram_drive device.
That's it. You're all set!


Copyright ©1997 Be, Inc. Be is a registered trademark, and BeOS, BeBox, BeWare, GeekPort, the Be logo and the BeOS logo are trademarks of Be, Inc. All other trademarks mentioned are the property of their respective owners. Comments about this site? Please write us at webmaster@be.com.