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.
Contents
Implementing the device
malloc declaration
Kernel modules have their own malloc types, which are defined as follows:
MALLOC_DECLARE(M_MYDEV); MALLOC_DEFINE(M_MYDEV, "mydev", "device description");
Then, you can use malloc(3) and free(3) as:
p = malloc(sizeof(struct MYDEV), M_FOO, M_WAITOK | M_ZERO); free(p, M_MYDEV);
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;
Declare 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()
It's good practice to keep an internal buffer. Below is a very simplified example. The buffer in this example is allocated and deallocated on module load and unload respectively (see sections below):
#define BUFSIZE (1 << 16) struct foo { char buf[BUFSIZE + 1]; size_t len; }; static struct foo *foo;
Data to be received or sent back is stored in uio and the copy from user to kernel memory is done through uiomove(9), defined in sys/uio.h:
static int mydev_read(struct cdev *dev, struct uio *uio, int ioflag) { size_t amnt; int v, error = 0; /* * Determine how many bytes we have to read. We'll either read the * remaining bytes (uio->uio_resid) or the number of bytes requested by * the caller. */ v = uio->uio_offset >= foo->len + 1 ? 0 : foo->len + 1 - uio->uio_offset; amnt = MIN(uio->uio_resid, v); /* Move the bytes from foo->buf to uio. */ if ((error = uiomove(foo->buf, amnt, uio)) != 0) { /* error handling */ } /* do stuff */ return (error); } static int mydev_write(struct cdev *dev, struct uio *uio, int ioflag) { size_t amnt; int error = 0; /* Do not allow random access. */ if (uio->uio_offset != 0 && (uio->uio_offset != foo->len)) return (EINVAL); /* We're not appending, reset length. */ else if (uio->uio_offset == 0) foo->len = 0; amnt = MIN(uio->uio_resid, (BUFSIZE - foo->len)); if ((error = uiomove(foo->buf + uio->uio_offset, amnt, uio)) != 0) { /* error handling */ } foo->len = uio->uio_offset; foo->buf[foo->len] = '\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:
_IO: No parameters.
_IOR: Copy out parameters. Read from device.
_IOW: Copy in paramters. Write to device.
_IOWR: Copy parameters in and out. Write to device and read the modified data back.
Each of those macros* takes 3 arguments:
- An arbitrary one-byte "class" identifier.
- A unique ID.
The parameter type (can be anything), which is used to calculate the parameter's size. The macro expands the type to sizeof(type).
* _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/types.h> #include <sys/param.h> #include <sys/conf.h> #include <sys/systm.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/malloc.h> #include <sys/uio.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"); foo = malloc(sizeof(foo_t), M_MYDEV, M_WAITOK | M_ZERO); foo->buf[0] = '\0'; foo->len = 0; break; case MOD_UNLOAD: /* FALLTHROUGH */ case MOD_SHUTDOWN: free(foo, M_MYDEV); 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).