Making a character device kernel module on FreeBSD

This article assumes advanced knowledge of C and a basic understanding of the FreeBSD kernel and programming environment. It is also meant to serve as a template/reference and not a complete implementation.

Sample code can be found here.

Implementing the device

cdevsw structure

The device's properties and methods are stored in a cdevsw (Character Device Switch) structure, defined in sys/conf.h. The fields we care about most of the time are the following:

struct cdevsw {
        int                     d_version;
        u_int                   d_flags;
        const char              *d_name;
        d_open_t                *d_open;
        d_fdopen_t              *d_fdopen;
        d_close_t               *d_close;
        d_read_t                *d_read;
        d_write_t               *d_write;
        d_ioctl_t               *d_ioctl;
        d_poll_t                *d_poll;
        d_mmap_t                *d_mmap;
        d_strategy_t            *d_strategy;
        dumper_t                *d_dump;
        d_kqfilter_t            *d_kqfilter;
        d_purge_t               *d_purge;
        d_mmap_single_t         *d_mmap_single;
        ...
};

All the *_t pointers are pointers to functions meant to be implemented by the driver. Not all functions have to be implemented however, but we usually do need to implement open(), close(), read(), write() and ioctl().

Declare the functions using some handy typedefs:

static d_open_t         mydev_open;
static d_close_t        mydev_close;
static d_read_t         mydev_read;
static d_write_t        mydev_write;
static d_ioctl_t        mydev_ioctl;

Or just declare them like normal functions:

static int      mydev_open(struct cdev *, int, int, struct thread *);
static int      mydev_close(struct cdev *, int, int, struct thread *);
static int      mydev_read(struct cdev *, struct uio *, int);
static int      mydev_write(struct cdev *, struct uio *, int);
static int      mydev_ioctl(struct cdev *, u_long, caddr_t, int, struct thread *);

Write the cdevsw structure:

static struct cdevsw mydev_cdevsw = {
        .d_name         = "mydev",
        .d_version      = D_VERSION,
        .d_flags        = D_TRACKCLOSE,
        .d_open         = mydev_open,
        .d_close        = mydev_close,
        .d_read         = mydev_read,
        .d_write        = mydev_write,
        .d_ioctl        = mydev_ioctl,
};

The D_TRACKCLOSE flag tells the kernel to track when the device closes so that it can close normally in case something goes wrong.

open() and close()

Those two functions are mainly used for resource allocation/deallocation and environment preparation:

static int
mydev_open(struct cdev *dev, int flags, int devtype, struct thread *td)
{
        int error = 0;

        /* do stuff */

        return (error);
}

static int
mydev_close(struct cdev *dev, int flags, int devtype, struct thread *td)
{
        int error = 0;

        /* do stuff */

        return (error);
}

read() and write()

Data to be received or sent back is stored in uio, defined in sys/uio.h:

static int
mydev_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        int error = 0;

        /* do stuff */

        return (error);
}

static int
mydev_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        int error = 0;

        /* do stuff */

        return (error);
}

ioctl()

To create an ioctl, you give it a name and #define it using one of the following _IO* macros defined in sys/ioccom.h:

Each of those macros* takes 3 arguments:

* _IO takes only the first 2 arguments (class and ID) since it doesn't use parameters.

We can now define a few ioctls that take foo_t as a parameter. This is usually done in a separate header file so that programs can use the ioctls:

#include <sys/ioccom.h>

typedef struct {
        int x;
        int y;
} foo_t;

#define MYDEVIOC_READ   _IOR('a', 1, foo_t)
#define MYDEVIOC_WRITE  _IOW('a', 2, foo_t)
#define MYDEVIOC_RDWR   _IOWR('a', 3, foo_t)

mydev_ioctl() is responsible for handling the ioctls we declared:

static int
mydev_ioctl(struct cdev *dev, u_long cmd, caddr_t addr, int flags,
    struct thread *td)
{
        foo_t *fp;
        int error = 0;

        switch (cmd) {
        case MYDEVIOC_READ:
                fp = (foo_t *)addr;
                /* do stuff */
                break;
        case MYDEVIOC_WRITE:
                fp = (foo_t *)addr;
                /* do stuff */
                break;
        case MYDEVIOC_RDWR:
                fp = (foo_t *)addr;
                /* do stuff */
                break;
        default:
                error = ENOTTY;
                break;
        }

        return (error);
}

Creating and destroying the device

Character devices are given a struct cdev handle upon creation, which we usually store as a global variable:

static struct cdev *mydev_cdev;

Devices are created with the make_dev() function, which is defined as:

struct cdev *
make_dev(struct cdevsw *cdevsw, int unit, uid_t uid, gid_t gid, int perms,
    const char *fmt, ...);

sys/conf.h has the definitions of all available flags.

Create the device:

mydev_cdev = make_dev(&mydev_cdevsw, 0, UID_ROOT, GID_WHEEL, 0666, "mydev");

When done, destroy the device:

destroy_dev(mydev_cdev);

Module declaration

Necessary includes:

#include <sys/param.h>
#include <sys/conf.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/module.h>

Implement the module's event handler. This function is called at module load and unload. Since we're dealing with a character device, it makes sense to create the device upon load and destroy it upon unload:

static int
mydev_modevent(module_t mod, int type, void *arg)
{
        int error = 0;

        switch (type) {
        case MOD_LOAD:
                mydev_cdev = make_dev(&mydev_cdevsw, 0, UID_ROOT, GID_WHEEL,
                    0666, "mydev");
                break;
        case MOD_UNLOAD: /* FALLTHROUGH */
        case MOD_SHUTDOWN:
                destroy_dev(mydev_cdev);
                break;
        default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

Lastly, declare the module. The first argument is the module's name, the second one is a pointer to the event handler and the last one is any data we want to supply the event handler with, i.e the arg argument in mydev_modevent():

DEV_MODULE(mydev, mydev_modevent, NULL);

Makefile

KMOD=   mydev
SRCS=   mydev.c

.include <bsd.kmod.mk>

Running the module

$ make
# kldload ./mydev.ko
...
# kldunload ./mydev.ko
$ make clean cleandepend

Testing

To test the module, load it, and create a simple program that opens the device, and makes a few calls to ioctl(2), read(2) and write(2).

CDevModule (last edited 2022-07-09T23:59:08+0000 by ChristosMargiolis)