NetBSD Documentation: Writing a pseudo device

Writing a pseudo device

Writing a pseudo device


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:

  1. 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.

  2. 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.

  3. 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.

  4. 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


  5. 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.

  6. 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


    • proc

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

  7. stop()

    Stop output on tty style device.

    • tty

      tty associated with the device

    • flags


  8. 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.

  9. 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:

  1. /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:

    device-major	skeleton	char	140	skeleton

Making config(1) aware of the new device

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.

Back to NetBSD Documentation: Kernel