Enough introduction--it's time to actually create a basic device driver using FUSD!
This following sections will illustrate various techniques using example programs. To save space, interesting excerpts are shown instead of entire programs. However, the examples directory of the FUSD distribution contains all the examples in their entirety. They can actually be compiled and run on a system with the FUSD kernel module installed.
Where this text refers to example program line numbers, it refers to the line numbers printed alongside the excerpts in the manual--not the line numbers of the actual programs in the examples directory.
We saw an example of a simple driver, helloworld.c, in
Program 1 on page . Let's go
back and examine that program now in more detail.
The FUSD ball starts rolling when the fusd_register function is called, as shown on line 40. This function tells the FUSD kernel module:
If device registration is successful, fusd_register returns a
device handle--a small integer . On errors, it returns
-1 and sets the global variable errno appropriately. In
reality, the device handle you get is a plain old file descriptor,
as we'll see in Section 7.
Although Program 1 only calls this function once, it can be called multiple times if the FUSD driver is handling more than one device as we'll see in Program 4.
There is intentional similarity between fusd_register() and the kernel's device registration functions, such as devfs_register() and register_chrdev(). In many ways, FUSD's interface is meant to mirror the kernel interface as closely as possible.
The fusd_file_operations structure, defined in fusd.h, contains a list of callbacks that are used in response to different system calls executed on a file. It is similar to the kernel's file_operations structure, accepting callbacks for system calls such as open(), close(), read(), write(), and ioctl(). For the most part, the prototypes of FUSD file operation callbacks are the same as their kernel cousins, with one important exception. The first argument of FUSD callbacks is always a pointer to a fusd_file_info structure; it contains information that can be used to identify the file. This structure is used instead of the kernel's file and inode structures, and will be described in more detail later.
In lines 35-38 of Program 1, we create and initialize a fusd_file_operations structure. A GCC-specific C extension allows us to name structure fields explicitly in the initializer. This style may look strange, but it guards against errors in the future in case the order of fields in the structure ever changes. The 2.4 kernel series uses the same trick.
After calling fusd_register() on line 40, the example program calls fusd_run() on line 44. This function turns control over to the FUSD framework. fusd_run blocks the driver until one of the devices it registered needs to be serviced. Then, it calls the appropriate callback and blocks again until the next event.
Now, imagine that a user types cat /dev/hello-world. What happens? Recall first what the cat program itself does: opens a file, reads from it until it receives an EOF (printing whatever it reads to stdout), then closes it. cat works the same way regardless of what it's reading--be it a a FUSD device, a regular file, a serial port, or anything else. The strace program is a great way to see this in action; see Appendix A for details.
The first two callbacks that most drivers typically implement are open and close. Each of these two functions are passed just one argument--the fusd_file_info structure that describes the instance of the file being opened or closed. Use of the information in that structure will be covered in more detail in Section 5.
The semantics of an open callback's return value are exactly the same as inside the kernel:
If an open callback returns 0 (success), a driver is guaranteed to receive exactly one close callback for that file later. By the same token, the close callback will not be called if the open fails. Therefore, open callbacks that can return failure must be sure to deallocate any resources they might have allocated before returning a failure.
Let's return to our example in Program 1, which creates the /dev/hello-world device. If a user types cat /dev/hello-world, cat will will use the open(2) system call to open the file. FUSD will then proxy that system call to the driver and activate the callback that was registered as the open callback. Recall from line 36 of Program 1 that we registered do_open_or_close, which appears on line 8.
In helloworld.c, the open callback always returns 0, or success. However, in a real driver, something more interesting will probably happen--permissions checks, memory allocation for state-keeping, and so forth. The corresponding de-allocation of those resources should occur in the close callback, which is called when a user application calls close on their file descriptor. close callbacks are allowed to return error values, but this does not prevent the file from actually closing.
Returning to our cat /dev/hello-world example, what happens after the open is successful? Next, cat will try to use read(2), which will get proxied by FUSD to the function do_read on line 13. This function takes some additional arguments that we didn't see in the open and close callbacks:
The semantics of the return value are the same as if the callback were being written inside the kernel itself:
The first time a read is done on a device file, the user's file pointer (*offset) is 0. In the case of this first read, a greeting message of Hello, world! is copied back to the user, as seen on line 24. The user's file pointer is then advanced. The next read therefore fails the comparison at line 24, falling straight through to return 0, or EOF.
In this simple program, we also see an example of an error return on line 22: if the user tries to do a read smaller than the length of the greeting message, the read will fail with -EINVAL. (In an actual driver, it would normally not be an error for a user to provide a smaller read buffer than the size of the available data. The right way for drivers to handle this situation is to return partial data, then move *offset forward so that the remainder is returned on the next read(). We see an example of this in Program 2.)
Program 1 illustrated how a driver could return data to a client using the read callback. As you might expect, there is a corresponding write callback that allows the driver to receive data from a client. write takes four arguments, similar to the read callback:
The semantics of write's return value are the same as in a kernel callback:
Program 2, echo.c, is an example implementation of a device
(/dev/echo) that uses both read() and write()
callbacks. A client that tries to read() from this device will
get the contents of the most recent write(). For example:
% echo Hello there > /dev/echo % cat /dev/echo Hello there % echo Device drivers are fun > /dev/echo % cat /dev/echo Device drivers are fun |
The implementation of /dev/echo keeps a global variable, data, which serves as a cache for the data most recently written to the driver by a client program. The driver does not assume the data is null-terminated, so it also keeps track of the number of bytes of data available. (These two variables appear on lines 1-2.)
The driver's write callback first frees any data which might have been allocated by a previous call to write (lines 26-29). Next, on line 33, it attempts to allocate new memory for the new data arriving. If the allocation fails, -ENOMEM is returned to the client. If the allocation is successful, the driver copies the data into its local buffer and stores its length (lines 37-38). Finally, the driver tells the user that the entire buffer was consumed by returning a value equal to the number of bytes the user tried to write (user_length).
The read callback has some extra features that we did not see in Program 1's read() callback. The most important is that it allows the driver to read the available data incrementally, instead of requiring that the first read() executed by the client has enough space for all the data the driver has available. In other words, a client can do two 50-byte reads, and expect the same effect as if it had done a single 100-byte read.
This is implemented using *offset, the user's file pointer. If the user is trying to read past the amount of data we have available, the driver returns EOF (lines 8-9). Normally, this happens after the client has finished reading data. However, in this driver, it might happen on a client's first read if nothing has been written to the driver yet or if the most recent write's memory allocation failed.
If there is data to return, the driver computes the number of bytes that should be copied back to the client--the minimum of the number of bytes the user asked for, and the number of bytes of data that this client hasn't seen yet (line 12). This data is copied back to the user's buffer (line 15), and the user's file pointer is advanced accordingly (line 16). Finally, on line 19, the client is told how many bytes were copied to its buffer.
All devices registered by a driver are unregistered automatically when the program exits (or crashes). However, the fusd_unregister() function can be used to unregister a device without terminating the entire driver. fusd_unregister takes one argument: a device handle (i.e., the return value from fusd_register()).
A device can be unregistered at any time. Any client system calls that are pending when a device is unregistered will return immediately with an error. In this case, errno will be set to -EPIPE.