Writing a pseudo device

Introduction

This document is meant to provide a guide to someone who
wants to start writing kernel drivers. The document covers
the writing of a simple pseudo-device driver. You will need
to be familiar with building kernels, makefiles and the
other arcana involved in installing a new kernel as these
are not covered by this document. Also not covered is
kernel programming itself - this is quite different to
programming at the user level in many ways. Having said all
that, this document will give you the process that is
required to get your code into and recognised by the
kernel.

Your code

The file pseudo_dev_skel.c gives the
framework for a pseudo-device and the file pseudo_dev_skel.h defines
the kernel function prototypes and the ioctl data structure
plus the ioctl number itself. Note that, unlike a normal
device driver, a pseudo-device does not have a probe routine
because this is not necessary. This simplifies life because
we do not need to deal with the autoconfig framework. The
skeleton file given is for a pseudo-device that supports the
open, close and ioctl calls. This is about the minimum
useful set of calls you can have in a real pseudo-device.
There are other calls to support read, write, mmap and other
device functions but they all follow the same pattern as
open, close and ioctl so they have been omitted for
clarity.

Probably the first important decision you need to make
is what you are going to call your new device. This needs
to be done up front as there are a lot of convenience macros
that generate kernel structures by prepending your device
name to the function call names, will help if you have an
idea of the config file entry you want to have. The config
file entry does not have to match the header file name. In
our skeleton driver we have decided to call the
pseudo-device “skeleton”, so we shall have a
config file entry called skeleton. This means that the
attach, open, close and ioctl function calls are named
skeletonattach, skeletonopen, skeletonclose and
skeletonioctl respectively. Another important decision is
what sort of device you are writing - either a character or
block device as this will affect how your code interacts
with the kernel and, of course, your code itself. The
decision of block vs character device depends a lot on the
underlying hardware the driver talks to, if the device the
driver talks to operates by reading and write data in fixed
chunks then a block device is a good choice, an example of
such a device is a hard disk which usually reads and writes
data in blocks of 512 byte sectors. If the hardware reads
and writes data one byte at a time then a character device
is normally the best choice, an example of such a device is
a serial line driver. Note that some drivers support both a
block mode and character mode of access to a device, in this
case the character mode is sometimes called the "raw" device
because it gives access to the hardware without the data
blocking abstractions operating on the access. For a
pseudo-device the choice is more flexible because there is
no underlying hardware to consider. The choice is driven by
the use the pseudo-device is going to be put to, a block
device may be useful if you are going to emulate a hard-disk
or similar. Our skeleton driver is to be a character
device.

Once the decisions have been made we can start cutting
code, before we do this though we need to decide where our
file should go. If you are writing a pseudo-device that
will be used by multiple architectures then the appropriate
place to put the driver code is in
/usr/src/sys/dev. If the pseudo-device
is specific to a particular architecture then put the driver
code under the architecture specific directory, for example
on the i386 architecture this would be
/usr/src/sys/arch/i386/i386. The
include file should go into
/usr/src/sys/sys for the architecture
independent device and in the include
directory under the architecture specific directory for an
architecture specific device, for example, on the i386
architecture this would be
/usr/src/sys/arch/i386/include. In
either case ensure that you update the relevant
Makefile so your include file is
installed. One thing you will note is the struct
skeleton_softc at the top of pseudo_dev_skel.c. You
must have a softc structure declared with the name of your
device with "_softc" appended and the first element of this
struct needs to be a struct device type, the
name of the entry is not important but it must be first as
the autoconfig system relies on the softc struct being
declared and that its
first element is a struct device.
There needs to be a softc
struct for each minor number a device handles. The softc
structure can hold more elements than just the struct device
if the minor devices require state information to be kept
about them.

The functions

The kernel interfaces to your device via a set of
function calls which will be called when a user level
programme accesses your device. A device need not support
all the calls, as we will see later, but at a minimum a
useful device needs to support an open and close on it.
Remember the function names need to be prepended with your
device name and are fully described in autoconf(9).
The functions are:

attach()

This function is called once when the kernel is
initialising. It is used to set up any variables that
are referenced in later calls or for allocating kernel
memory needed for buffers. The attach function is
passed one parameter which is the number of devices this
driver is expected to handle.

open()

As the name suggests, this function will be called
when a user level programme performs an open(2) call on
the device. At its simplest the open function may just
return success. More commonly, the open call will
validate the request and may allocate buffers or
initialise other driver state to support calls to the
other driver functions. The open call is passed the
following parameters:

dev

This is the device minor number the open is
being performed on.

flags

flags passed to the open call

mode

mode for open

proc

This is a pointer to the proc structure of the
process that has requested the open. It allows for
validation of credentials of the process.

close()

This closes an open device. Depending on the driver
this may be as simple as just returning success or it
could involve free'ing previously allocated memory
and/or updating driver state variables to indicate the
device is no longer open. The parameters for the close
function call are the same as those described for
open.

read()

Read data from your device. The parameters for the
function are:

dev

The minor number of the device.

uio

This is a pointer to a uio struct. The read
function will fill in the uio struct with the data
it wants to return to the user.

flags

flags

write()

Write data to your device. The parameters for the
write function are the same as those for a read function
- the only difference being that the uio structure
contains data to be written to the device.

ioctl()

Perform an ioctl on your device. The parameters for
the ioctl call are:

dev

The minor number of the device.

cmd

The ioctl command to be performed. The commands are
defined in a header file which both the kernel code
and the user level code reference. See the
sample
header for an example.

data

This is a pointer to the parameters passed in by
the user level code. What is in this parameter
depends on the implementation of the ioctl and also
on the actual ioctl command being issued.

flags

flags

proc

The proc structure that is associated with the user
level process making the ioctl request.

stop()

Stop output on tty style device.

tty

tty associated with the device

flags

flags

poll()

Checks the device for data that can be read from it.
The parameters are:

dev

The minor number of the device used.

events

The event(s) that the user level call is polling
for.

proc

The proc structure that is associated with the
user level process making the ioctl request.

mmap()

Supports the capability of mmap'ing a driver buffer
into a user level programme's memory space. The
parameters are:

dev

The minor device number of the device
used.

offset

The offset from the start of the buffer at which
to start the mmap.

prot

The type of mmap to perform, either read only,
write only or read write. The device driver need
not support all modes.

The function names your device driver supports must be
inserted into a struct cdevsw for a character
device and/or a struct bdevsw that has the name
of your module appended with either _cdevsw or
_bdevsw. For our sample pseudo-device this
structure would be called skeleton_cdevsw since
we decided that our pseudo-device would be a character
device only. Note that these structures have entries in
them for all the device interface functions but your device
may only implement a subset of these functions. Instead of
forcing everyone to implement stub functions for the unused
ones there are a set of pre-declared stubs prefixed with
either no (e.g. noread,
nowrite and so on) which will return
ENODEV when called or null
(e.g. nullread, nullwrite and so
on) which will return success, effectively providing a null
operation. For the functions in the cdevsw and/or bdevsw
that your driver does not support simply use one of the
predeclared stubs.

Making the kernel aware of the new device

Once you have done the coding of your pseudo-device it
is then time
to hook your code into the kernel so that it can be accessed. Note
that the process of hooking a pseudo-device into the kernel differs
a lot from that of a normal device. Since a pseudo-device is
either there or not the usual device probe and auto-configuration is
bypassed and entries made into kernel structures at the source
level instead of at run time. To make the kernel use your code you
have to modify these files:

/usr/src/sys/conf/majors or
/usr/src/sys/<arch>/conf/majors.<arch>

These files contain lists of device major numbers
for NetBSD. The file
/usr/src/sys/conf/majors contains
the major numbers for devices that are machine
independent, that is, available on all architectures
that NetBSD supports. If the device is only relevant to
a particular architecture then the file
/usr/src/sys/<arch>/conf/majors.<arch>
must be used where <arch> is replaced with the
architecture in question. These files contain entries
of the form

device-major prefix type number condition

The exact syntax for these lines is described in the
config(5) man page but for the purposes of our
example the components of the line are:

device-major

A keyword indicating this is a device major
number entry.

prefix

The prefix that will be applied to all the
driver functions when their names are automatically
generated. In our example this would be
skeleton.

type

The type of major device this is, it may be
either char or block. You
may specify both a char and block device by
repeating the type/number pair for both.

number

The major number for the device, choose the next
available number. Make a note of this number as you
will need it to make the device node in
/dev.

condition

The condition on which this device will be
included in the kernel. This should match the
pseudo-device entry you put in the conf file
(described below).

For our example skeleton pseudo device we want a
character device, and have decided that the driver is
machine specific to the i386 architecture. After making
these decisions we can edit the
/usr/src/sys/arch/i386/conf/majors.i386
file, we find that major number 140 is available so we
add the line:

To make config(1) aware of our new pseudo device we
need to edit the file in either
/usr/src/sys/conf/files (for
architecture independent devices) or
/usr/src/sys/arch/<arch>/conf/files.<arch>
where <arch> is the relevant architecture. This file
tells config what valid device names there are and which
files are associated with these devices. Firstly we look
for the section that defines the pseudo-devices. This
section has lines that start with defpseudo.
Since we have decided that our driver is to be an
architecture specific one we edit the
/usr/src/sys/arch/i386/conf/files.i386
file and once we have found the correct section we can add a
line like this:

defpseudo skeleton

Which tells config(1) we have a pseudo-device
called skeleton. Next we need to tell config(1) which
files are associated with the skeleton pseudo-device. In
this case we only have one file but a more complex
pseudo-device may have more files, simply add each file
required on a line in the same manner. For our example we
only need one line that looks like this:

file dev/skeleton.c skeleton needs-flag

The file on the line is a key word to say
we are defining a device to file association. The second
field is the location of the file relative to the root of
the kernel source tree (normally,
/usr/src/sys). The third field is the
name of the driver that this file is associated with, in our
case this is skeleton - our sample pseudo-device. The
fourth and last field is a control flag that tells
config(1) to write the skeleton.h
include file. Note that here the file is called
skeleton.c, if we were using the
example files here, we would have to either rename
pseudo_dev_skel.c to
skeleton.c or change this entry. Since
we said above that we are calling it skeleton, it would
probably be more suitable to call it
skeleton.c.

Adding the new device to the kernel config
file

Once config(1) has been told about the device,
adding it to the kernel config file is simple. To add our
skeleton device we add the line:

pseudo-device skeleton

To the kernel config file, note the name of the
pseudo-device matches the name given in the
defpseudo line in the previous section. New
defines can be added to the kernel makefile by using the
options kernel config file keyword, config
will build a makefile with the options named added as
-D command line options to the
cc command.

Allowing user level programmes access to the new
device

After building and installing a new kernel there is
one last thing that needs to be done to access the new
pseudo-device, this is to make a device node for talking
to the new device. The device node can be made on any
file system that allows you to access devices from it but,
by convention, device nodes are created in
/dev. To make the device node you
will need to use mknod(8) to create a device node
with the major number you noted in section 4.i. In our
case the mknod(8) command would look like
this:

#mknod /dev/skel c 140 0

Once this has been done you should be able to open
your new device and test it out. The file sample.c shows the skeleton pseudo
device in action. This file assumes you have followed the
instructions here and have created
/dev/skel, this device is opened and
a parameter structure passed to the device driver via an
ioctl call. To compile the sample code use this command
line:

$cc -o sample sample.c

Which will produce a binary called sample.
NOTE: you will have to have run
make includes in the directory you
copied pseudo_dev_skel.h to install
the header file into the system includes directory
otherwise the compiler will complain about a missing
include file. Once you have compiled the programme, run
it and then look at your kernel messages either on the
console screen or in
/var/log/messages, they should have a
message that looks like this:

May 17 20:32:57 siren /netbsd: Got number of 42 and string of Hello World

Which is a message printed by the skeleton ioctl
handler when it
receives a SKELTEST ioctl request; notice that the number and the
string printed are the ones we put into the param
structure in sample.c.