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.
Also mirrored on the FreeBSD Wiki.
Table of contents
- Implementing the device
- Creating and destroying the device
- Module declaration
- Makefile
- Running the module
- Testing
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(9) and free(9) as:
p = malloc(sizeof(foo), M_MYDEV, 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:
#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).