You're reading from Linux Device Driver Development Cookbook
Technical requirements
When we have to manage a peripheral, it's quite common to need to modify its internal configuration settings, or it may be useful to map it from the user space as if it was a memory buffer in which we can modify internal data just by referencing a pointer.
In this case, having the support of the lseek(), ioctl(), and mmap() system calls is fundamental. If, from the user space, the usage of these system calls is not tricky, within the kernel they require some attention by the driver developer, especially the mmap() system call, which involves the kernel Memory Management Unit (MMU).
Not only that one of the principal tasks a driver developer must pay attention to is the data exchanging mechanism with the user space...
Going up and down within a file with lseek()
Here we should remember that the prototypes of the read() and write() system calls were the following:
ssize_t (*read) (struct file *filp,
char __user *buf, size_t len, loff_t *ppos);
ssize_t (*write) (struct file *filp,
const char __user *buff, size_t len, loff_t *ppos);
When we tested our char driver using the program in the chapter_03/chrdev_test.c file, we noticed that we weren't able to reread written data unless we patched our file as follows:
--- a/chapter_03/chrdev_test.c
+++ b/chapter_03/chrdev_test.c
@@ -55,6 +55,16 @@ int main(int argc, char *argv[])
dump("data written are: ", buf, n);
}
+ close(fd);
+
+ ret = open(argv[1], O_RDWR);
+ if (ret < 0) {
+ perror("open");
+ exit(EXIT_FAILURE);
+ }
+ printf("file %s reopened\n", argv[1]);
+ fd = ret...
Using ioctl() for custom commands
In Chapter 3, Working with Char Drivers, we discussed the file abstraction and mentioned that a char driver is very similar to a usual file, from the user space point of view. However, it's not a file at all; it is used as a file but it belongs to a peripheral, and, usually, peripherals need to be configured to work correctly, due to the fact they may support different methods of operation.
Let's consider, for instance, a serial port; it looks like a file where we can (forever) read or write using both the read() and write() system calls, but to do so, in most cases, we must also set some communication parameters such as the baud rate, parity bit, and so on. Of course, these parameters can't be set with read() or write(), nor by using the open() system call (even if it can set some accessing modes as read or write only), so the...
Accessing I/O memory with mmap()
In the Getting access to I/O memory recipe in Chapter 6, Miscellaneous Kernel Internals, we saw how the MMU works and how we can get access to a memory-mapped peripheral. Within the kernel space, we must instruct the MMU in order to correctly translate a virtual address into a proper one, which must point to a well-defined physical address to which our peripheral belongs, otherwise, we can't control it!
On the other hand, in that section, we also used a userspace tool named devmem2, which can be used to get access to a physical address from the user space, using the mmap() system call. This system call is really interesting, because it allows us to do a lot of useful things, so let's start by taking a look at its man page (man 2 mmap):
NAME
mmap, munmap - map or unmap files or devices into memory
SYNOPSIS
#include <sys/mman.h>...
Locking with the process context
It's good to understand how to avoid race conditions in case more than one process tries to get access to our driver, or how to put to sleep a reading process (we talk about reading here, but the same thing also holds true for writing) in case our driver has no data to supply. The former case will be presented here, while the latter will be presented in the next section.
If we take a look at how read() and write() system calls have been implemented in our chrdev driver, we can easily notice that, if more than one process tries to do a read() call or even if one process attempts a read() call and another tries a write() call, a race condition will occur. This is because the ESPRESSObin's CPU is a multiprocessor composed of two cores and so it can effectively execute two processes at the same time.
However, even if our system had just one...
Waiting for I/O operations with poll() and select()
In a complex system such as a modern computer, it's quite common to have several useful peripherals to acquire information about the external environment and/or the system's status. Sometimes, we may use different processes to manage them but we may need to manage more than one peripheral at a time, but with just a single process.
In this scenario, we can imagine doing several read() system calls on each peripheral to acquire its data, but what happens if one peripheral is quite slow and it takes a lot of time to return its data? If we do the following, we may slow down all data acquisition (or even lock it if one peripheral doesn't receive new data):
fd1 = open("/dev/device1", ...);
fd2 = open("/dev/device2", ...);
fd3 = open("/dev/device3", ...);
while (1) {
read(fd1, buf1, size1...
Managing asynchronous notifications with fasync()
In the previous section, we considered the special case in which we can have a process that must manage more than one peripheral. In this situation, we can ask the kernel, which is the ready file descriptor, where to get data from or where to write data to using the poll() or select() system call. However, this is not the only solution. Another possibility is to use the fasync() method.
By using this method, we can ask the kernel to send a signal (usually SIGIO) whenever a new event has occurred on a file descriptor; the event, of course, is a ready-to-read or read-to-write event and the file descriptor is the one connected with our peripheral.
The fasync() method does not have a userspace counterpart due to the already presented methods in this book; there is no fasync() system call at all. We can use it indirectly by utilizing...