* Disclaimer: This exploratory topic is related to some graduate work I did while pursuing my Masters degree. I am by no means an expert in C programming nor Minix. With that said, there’s not a lot of information out there on Minix so I hope this work can be valuable to others.

minix

Intro

This article is a continuation of what is covered in the Programming Device Drivers in Minix post found on the official Minix 3 wiki. That post is an introduction to programming device drivers on Minix in C. Device drivers are, in short, software programs that control hardware devices. Minix, as described by the Minix homepage, is a free, open-source, operating system designed to be highly reliable, flexible and secure. Minix is certainly not the only example of a micro-kernel design for an operating system but it does serve as an introduction to the world of micro-kernels.

Minix Wiki Tutorial

Step 1 of course is to begin following the instructions detailed in the Programming Device Drivers in Minix tutorial. In that tutorial, you will create the driver directory, Makefile and driver file as well as setup the driver configuration file, start the driver service and create the character device. Rather than rehash all these steps (which are covered in great detail in that post), I will cover a few minor differences in my setup as well as a few slightly more advanced points.

Navigate to the Minix drivers directory /usr/src/minix/drivers/. Here, you can make a copy of the hello driver which was discussed in the Wiki post /usr/src/minix/drivers/examples/hello. In this folder you should see the Makefile, hello.c, header file and more (all of which was covered in the Minix wiki post). You do not need the .o, .h or .d files. You can rm them for now. From here, you can rename your hello.c file to whatever you want your device to be called (from here I will refer to this as [name]). Similarly, you will have to edit the contents of the Makefile, contents of the hello.c (now renamed to what you want) and the directory name of your driver replacing any instance of the word “hello” with your chosen name word [name].

After you’ve made these name-related changes, you can test the build by running…

make clean
make
make install

Unlike in the tutorial, rather than create a hello.conf in the driver’s directory itself, we instead put the configuration information in the more global /etc/system.conf directory/file. The configuration information for your driver can be the same as what was in the example ‘hello’ driver configuration. (In other words, you can copy exactly what is in this configuration file for the hello driver and put it at the end of the .conf file - but remember to change the name!).

Now, we can create the device file with the command…

mknod /dev/[name] c [major number] [minor number]

The major number and minor numbers are used by the system to associate the driver with the device. More information on these numbers can be read about here. Whats important is you choose a device number that is not already being used by an existing driver. You can see what device numbers are in used by running the command…

ls -l /dev | cut -d " " -f 11 | cut -d "," -f 1 | uniq | sort

The service can now be booted up by running…

service up /service/[name] -major [chosen major number]

The service can also be taken down with the command…

service down [name]

From here you should be able to read from the device driver. For example, issuing a cat /dev/[name] should let you read from the device. This is about where the Minix wiki tutorial finishes up.

Going a Bit Further…

The simple character device driver from the Minix tutorial includes function prototypes for opening, closing and reading from the device file. But what if you would like some additional functionality - say, writing to or controlling I/O to the file?

In the driver source there is an include file referencing <minix/chardriver.h> (this is a reference to /usr/include/minix/chardriver.h). This file contains the entry points for additional device dependent character driver function prototypes. This file includes not only open, close and read but also write, ioctl, cancel, select, intr, alarm and other. For now, we will cover adding both write and I/O control (ioctl) functionality to the driver.

Included below are the function prototypes covered by this tutorial (from /usr/include/minix/chardriver.h).

struct chardriver {
  int (*cdr_open)(devminor_t minor, int access, endpoint_t user_endpt);
  int (*cdr_close)(devminor_t minor);
  ssize_t (*cdr_read)(devminor_t minor, u64_t position, endpoint_t endpt, cp_grant_id_t grant, size_t size, int flags, cdev_id_t id);
  ssize_t (*cdr_write)(devminor_t minor, u64_t position, endpoint_t endpt, cp_grant_id_t grant, size_t size, int flags, cdev_id_t id);
  int (*cdr_ioctl)(devminor_t minor, unsigned long request, endpoint_t endpt, cp_grant_id_t grant, int flags, endpoint_t user_endpt, cdev_id_t id);

In your device driver code you will see these function prototypes initialized. You will notice of course that the write and ioctl functions are not included in the hello example driver. You must add these function declarations. This can be done by copying the declarations seen in the chardriver.h file described above into the driver source file. Note _*__ the name of the function should not be (*cdr_open) but rather a function name of your choice, such as _[name]_write.

Following the function prototype declarations, you will see some entry points into the driver, defined in struct chardriver. You will need to add some entry points for the other functions not defined in the original example hello driver. This includes the write and ioctl functions. Adding these to the chardriver struct can be seen below.

static struct chardriver [name]_tab =
{
    .cdr_open	= [name]_open,
    .cdr_close	= [name]_close,
    .cdr_read	= [name]_read,
    .cdr_write = [name]_write,
    .cdr_ioctl = [name]_ioctl,
};

read Function

As discussed in the Minix wiki post, the read function copies a string from the device driver program back to the calling user program by reading from the device file /dev/hello. This is done within the device driver primarily through the use of the sys_safecopyto function. The sys_safecopyto function reads size (which is a value passed to the read function as an argument) amount of bytes from the location pointed to by ptr and passes that back to the libc library read call which invoked it. In the code snippet below, whatever value is stored in int contents is passed back to the caller.

int contents;
...
static ssize_t [name]_read(devminor_t UNUSED(minor), u64_t position, endpoint_t endpt, cp_grant_id_t grant, size_t size, int UNUSED(flags), cdev_id_t UNUSED(id))
{
  int *ptr;
  int ret;
  ptr = contents;
  size = sizeof(ptr);
  if ((ret = sys_safecopyto(endpt, grant, 0, (vir_bytes) ptr, size)) != OK)
  {
    return ret;
  }
  return ret;
}

This read function is invoked from a user program such as the (simple) one included below. This program simply reads an integer out of the device file and displays it on the command line.

#include <stdio.h>
#inclue <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#define [NAME]_DEV "/dev/[name]"

int main (int argc, char *argv[]) {
  int data;
  fd = open([NAME]_DEV, O_RDWR);
  if (fd > 0)
    perror ("open"), exit(-1);
  if (read (fd, &data, sizeof(data)) < 0) {
    perror ("read")
    exit (1);
  }
  printf("%d\n", data)
  close (data);
  exit(0);
}

write Function

In the [name]_write function shown below, the reverse of sys_safecopyto is used - sys_safecopyfrom. This function is used to take data from the caller and write it into a value in the driver. The sys_safecopyfrom function writes size bytes from the calling libc library write function and writes it into the location pointed to by ptr. The [name]_write function code is shown below. In this case, an integer is being written from the calling function to the device file.

static ssize_t [name]_write(devminor_t UNUSED(minor), u64_t position, endpoint_t endpt, cp_grant_id_t grant, size_t size, int UNUSED(flags), cdev_id_t UNUSED(id))
{
  int ret;
  int value;
  int *ptr = &value;
  if ((ret = sys_safecopyfrom(endpt, grant, 0, (vir_bytes) ptr, size)) != OK)
  {
    return ret;
  }
  return sizeof(value);

An example of a C script which performs the libc library write call which calls the device driver write function and writes an integer to the device file is shown in the snippet below.

#include <stdio.h>
#inclue <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#define [NAME]_DEV "/dev/[name]"

int main (int argc, char *argv[]) {
  int data;
  fd = open([NAME]_DEV, O_RDWR);
  if (fd < 0)
    perror("open"), exit(-1);
  if (argc > 1)
  {
    data = atoi(argv[1]);
  }
  if (write (fd, &data, sizeof(data)) < 0) {
    perror ("write");
  }
  close(fd);
  exit(0);
}

ioctl function

Finally there is the ioctl function. ioctl is used to manipulate underlying device parameters of special files (i.e. device files).

To set up I/O control for this simple driver, we can create a header file (call it something like ioc_[name].h and place it in the /usr/include/sys/ directory). This file can later be referenced in your device driver file using #include <sys/ioc[name].h>_. In this file, you will define the different I/O control methods. For example, you could define three I/O control methods which control reading, writing and clearing a device file. This code is shown below…

#ifndef _S_I_[NAME]_H
#define _S_I_[NAME]_H
#include <minix/ioctl.h>
#define IOCREAD   _IOR('h', 3, u32_t)
#define IOCWRITE  _IOW('h', 4, u32_t)
#define IOCCLEAR  _IOW('h', 5, u32_t)
#endif

The file above has a single include, (<minix/ioctl.h) which contains only a single line which is another include, (<sys/ioccom.h>). ioccom.h contains the references for different types of I/O control definitions. The two definitions we will use (in our simple driver addition) are shown in the snippet below. It’s important to note that there are far more definitions which could be used in more advanced drivers.

#define _IOR(g,n,t)   _IOC(IOC_OUT,   (g), (n), sizeof(t))
#define _IOW(g,n,t)   _IOC(IOC_IN,   (g), (n), sizeof(t))

Now that we have the I/O header for our device driver, we can initialize the [name]_ioctl function in our driver file. The code snippet below is an example of how that would be done. The important function argument in the ioctl function is the unsigned long request parameter. This request value is the control value sent from the user program to the device driver which specifies which I/O control method is being invoked.

static int [name]_ioctl(dev_minor_t minor, unsigned long request, endpoint_t endpt, cp_grant_id_t grant, int flags, endpoint_t user_endpt, cdev_id_t id)
{  
  if(request==IOCREAD) {
    //Insert Functionality Here
  }
  if(request==IOCWRITE) {
    //Insert Functionality Here
  }
  if (request==IOCCLEAR) {
    //Insert Functionality Here
  }
  return -1
}

An example user program is provided in the snippet below… In this example, a single IOC call is made. It is up to you to figure out what you would like the different functionality to be!

#include <sys/types.h>
#include <sys/ioc_[name].h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#define [NAME]_DEV "/dev/[name]"

int main (int argc, char *argv[]) {
  int data;
  fd = open([NAME]_dev, O_RDWR);
  if (fd < 0)
    perror ("open"), exit(-1);
  if (ioctl (fd, [IOCREAD], &value) < 0)
    perror ("ioctl");
  close(fd);
  exit(0);
}

This was just a quick look at adding a bit more functionality to a device driver in Minix. Thanks for reading and feel free to contact me if there are any questions!