Chapter 4
Chapter 4
Character Drivers
Typically, an operating system is designed to hide the underlying hardware details from the
user application. Applications do, however, require the ability to access data that is captured by
hardware peripherals, as well as the ability to drive peripherals with output. Since the peripheral
registers are accessible only by the Linux kernel, only the kernel is able to collect data streams as
they are captured by these peripherals.
Linux requires a mechanism to transfer data from kernel to user space. This transfer of data is
handled via device nodes, which are also known as virtual files. Device nodes exist within the
root filesystem, though they are not true files. When a user reads from a device node, the kernel
copies the data stream captured by the underlying driver into the application memory space.
When a user writes to a device node, the kernel copies the data stream provided by the application
into the data buffers of the driver, which are eventually output via the underlying hardware. These
virtual files can be "opened" and "read from" or "written to" by the user application using standard
system calls.
Each device has a unique driver that handles requests from user applications that are eventually
passed to the core. Linux supports three types of devices: character devices, block devices and
network devices. While the concept is the same, the difference in the drivers for each of these
devices is the manner in which the files are "opened" and "read from" or "written to". Character
devices are the most common devices, which are read and written directly without buffering,
for example, keyboards, monitors, printers and serial ports. Block devices can only be written to
and read from in multiples of the block size, typically 512 or 1024 bytes. They may be randomly
accessed, i.e., any block can be read or written no matter where it is on the device. A classic
example of a block device is a hard disk drive. Network devices are accessed via the BSD socket
interface and the networking subsystems.
Character devices are identified by a c in the first column of a listing, and block devices are
identified by a b. The access permissions, owner and group of the device are provided for each
device.
[ 69 ]
Character Drivers Chapter 4
From the point of view of an application, a character device is essentially a file. A process only
knows a /dev file path. The process opens the file by using the open() system call and performs
standard file operations like read() and write().
In order to achieve this, a character driver must implement the operations described in the file_
operations structure (declared in include/linux/fs.h in the kernel source tree) and register them. In
the file_operations structure shown below, you can see some of the most common operations for a
character driver:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
};
The Linux filesystem layer will ensure that these operations are called when a user space
application makes the corresponding system call (on the kernel side, the driver implements and
registers the callback operation).
[ 70 ]
Chapter 4 Character Drivers
The kernel driver will use the specific functions copy_from_user() and copy_to_user() to exchange
data with user space, as shown in the previous figure.
Both the read() and write() methods return a negative value if an error occurs. A return value
greater than or equal to 0, instead, tells the calling program how many bytes have been
successfully transferred. If some data is transferred correctly and then an error happens, the return
value must be the count of bytes successfully transferred, and the error does not get reported until
the next time the function is called. Implementing this convention requires, of course, that your
driver remember that the error has occurred so that it can return the error status in the future.
The return value for read() is interpreted by the calling application program:
1. If the value equals the count argument passed to the read system call, the requested
number of bytes has been transferred. This is the optimal case.
2. If the value is positive, but smaller than the count, then only part of the data has been
transferred. This may happen for a number of reasons, depending on the device. Most
often, the application program retries the read. For instance, if you read using the fread()
function, the library function reissues the system call until completion of the requested
data transfer. If the value is 0, end-of-file was reached.
3. A negative value means there was an error. The value specifies what the error was,
according to <linux/errno.h>. Typical values returned on error include -EINTR (interrupted
system call) or -EFAULT (bad address).
In Linux, every device is identified by two numbers: a major number and a minor number. These
numbers can be seen by invoking ls -l /dev on the host PC. Every device driver registers its major
number with the kernel and is completely responsible for managing its minor numbers. When
accessing a device file, the major number selects which device driver is being called to perform
the input/output operation. The major number is used by the kernel to identify the correct device
driver when the device is accessed. The role of the minor number is device dependent and is
handled internally within the driver. For instance, the Raspberry Pi has several hardware UART
ports. The same driver can be used to control all the UARTs, but each physical UART needs its
own device node, so the device nodes for these UARTs will all have the same major number, but
will have unique minor numbers.
[ 71 ]
Character Drivers Chapter 4
with the relevant major and minor device numbers for every possible device that might exist in the
world.
This is not the right approach to create device nodes nowadays, as you have to create a block or
character device file entry manually and associate it with the device, as shown in the Raspberry
Pi´s terminal command line below:
root@raspberrypi:/home/pi# mknod /dev/mydev c 202 108
Despite all this, you will develop your next driver using this static method, purely for educational
purposes, and you will see in the few next drivers a better way to create the device nodes by using
devtmpfs and the miscellaneous framework.
In this kernel module lab, you will interact with user space through an ioctl_test user application. You
will use open() and ioctl() system calls in your application, and you will write its corresponding driver´s
callback operations on the kernel side, providing a communication between user and kernel space.
In the first lab, you saw what a basic driver looks like. This driver didn’t do much except printing some
text during installation and removal. In the following lab, you will expand this driver to create a device
with a major and minor number. You will also create a user application to interact with the driver.
Finally, you will handle file operations in the driver to service requests from user space.
In the kernel, a character-type device is represented by struct cdev, a structure used to register it in the
system.
[ 72 ]
Chapter 4 Character Drivers
After assigning the identifiers, the character device will have to be initialized by using the cdev_
init() function and registered to the kernel by using the cdev_add() function. The cdev_init() and
cdev_add() functions will be called as many times as assigned device identifiers.
The following sequence registers and initializes a number (MY_MAX_MINORS) of devices:
#include <linux/fs.h>
#include <linux/cdev.h>
#define MY_MAJOR 42
#define MY_MAX_MINORS 5
struct my_device_data {
struct cdev cdev;
/* my data starts here */
[...]
};
[ 73 ]
Character Drivers Chapter 4
int init_module(void)
{
int i, err;
register_chrdev_region(MKDEV(MY_MAJOR, 0), MY_MAX_MINORS, "my_device_driver");
return 0;
}
The following code snippet deletes and unregisters the character devices:
void cleanup_module(void)
{
int i;
3. One of the first things your driver will need to do when setting up a char device is to
obtain one or more device identifiers (major and minor numbers) to work with. The
necessary function for this task is register_chrdev_region(), which is declared in include/linux/
fs.h in the kernel source tree. Add the following lines of code to the hello_init() function to
allocate the device numbers when the module is loaded. The MKDEV macro will combine
a major number and a minor number to a dev_t data type that is used to hold the first
device identifier.
[ 74 ]
Chapter 4 Character Drivers
4. Add the following line of code to the hello_exit() function to return the devices when the
module is removed:
unregister_chrdev_region(MKDEV(MY_MAJOR_NUM, 0), 1);
6. Implement each of the callback functions that are declared in the file_operations structure:
static int my_dev_open(struct inode *inode, struct file *file)
{
pr_info("my_dev_open() is called.\n");
return 0;
}
7. Add these file operation functionalities to your character device. The Linux kernel uses
struct cdev to represent character devices internally; therefore you will create a struct cdev
variable called my_dev and initialize it by using the cdev_init() function call, which takes
the variable my_dev and the structure my_dev_fops as parameters. Once the cdev structure
is setup, you will tell the kernel about it by using the cdev_add() function. You will call
cdev_init() and cdev_add() as many times as allocated character device identifiers (only once
in this driver).
[ 75 ]
Character Drivers Chapter 4
8. Add the following line of code to the hello_exit() function to delete the cdev structure:
cdev_del(&my_dev);
9. Once the kernel module has been dynamically loaded, the user needs to create a device
node to reference the driver. Linux provides the mknod utility for this purpose. The mknod
command has four parameters. The first parameter is the name of the device node that will
be created. The second parameter indicates whether the driver to which the device node
interfaces is a block driver or character driver. The last two parameters passed to mknod
are the major and minor numbers. Assigned major numbers are listed in the /proc/devices
file and can be viewed using the cat command. The created device node should be placed
in the /dev directory.
10. Create a new helloword_rpi3_char_driver.c file in the linux_5.4_rpi3_drivers folder, and write
the Listing 4-1 code on it. Add helloword_rpi3_char_driver.o to your Makefile obj-m variable.
11. In the linux_5.4_rpi3_drivers folder, you will create the apps folder, where you are going to
store most of the applications developed through this book.
~/linux_5.4_rpi3_drivers$ mkdir apps
12. In the apps folder, you will create an ioctl_test.c file, then write the Listing 4-3 code on it.
You will also create a Makefile (Listing 4-2) file in the apps folder to compile and deploy the
application.
13. Compile the helloworld_rpi3_char_driver.c driver and the ioctl_test.c application, and deploy
them to the Raspberry Pi:
~/linux_5.4_rpi3_drivers$ make
~/linux_5.4_rpi3_drivers$ make deploy
~/linux_5.4_rpi3_drivers/apps$ make
~/linux_5.4_rpi3_drivers/apps$ make deploy
[ 76 ]
Chapter 4 Character Drivers
static long my_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
pr_info("my_dev_ioctl() is called. cmd = %d, arg = %ld\n", cmd, arg);
return 0;
}
return 0;
}
[ 77 ]
Character Drivers Chapter 4
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alberto Liberal <[email protected]>");
MODULE_DESCRIPTION("This is a module that interacts with the ioctl system call");
app : ioctl_test.c
$(CC) -o $@ $^
clean :
rm ioctl_test
deploy : ioctl_test
scp $^ [email protected]:/home/pi
int main(void)
{
/* First, you need run "mknod /dev/mydev c 202 0" to create /dev/mydev */
if (my_dev < 0) {
perror("Fail to open device file: /dev/mydev.");
} else {
ioctl(my_dev, 100, 110); /* cmd = 100, arg = 110. */
close(my_dev);
}
return 0;
}
[ 78 ]
Chapter 4 Character Drivers
helloworld_rpi3_char_driver.ko demonstration
Load the module:
root@raspberrypi:/home/pi# insmod helloworld_rpi3_char_driver.ko
Hello world init
See the allocated "my_char_device". The device mydev is not created under /dev yet:
root@raspberrypi:/home/pi# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
[...]
202 my_char_device
[...]
root@raspberrypi:/home/pi# ls –l /dev/
Create mydev under /dev using mknod, and verify its creation:
root@raspberrypi:/home/pi# mknod /dev/mydev c 202 0
root@raspberrypi:/home/pi# ls -l /dev/mydev
crw-r--r-- 1 root root 202, 0 Apr 8 19:00 /dev/mydev
Run the application ioctl_test:
root@raspberrypi:/home/pi# ./ioctl_test
my_dev_open() is called.
my_dev_ioctl() is called. cmd = 100, arg = 110
my_dev_close() is called.
Remove the module:
root@raspberrypi:/home/pi# rmmod helloworld_rpi3_char_driver.ko
Hello world exit
[ 79 ]
Character Drivers Chapter 4
Device files are created by the kernel via the temporary devtmpfs filesystem. Any driver that wishes to
register a device node will use devtmpfs (via the core driver) to do it. When a devtmpfs instance is mounted
on /dev, the device node will initially be created with a fixed name, permissions and owner. These entries
can be both read from and written to. All device nodes are owned by root and have the default mode of
0600.
Shortly afterward, the kernel will send an uevent to udevd. Based on the rules specified in the files within the
/etc/udev/rules.d/, /lib/udev/rules.d/ and /run/udev/rules.d/ directories, udevd will create additional symlinks
to the device node, change its permissions, owner or group, or modify the internal udevd database entry
(name) for that object. The rules in these three directories are numbered, and all three directories are merged
together. If udevd can't find a rule for the device it is creating, it will leave the permissions and ownership at
whatever devtmpfs used initially.
The CONFIG_DEVTMPFS_MOUNT kernel configuration option makes the kernel mounts devtmpfs
automatically at boot time, except when booting on an initramfs.
A Linux driver creates the device nodes by using the following kernel APIs:
device_create() /* creates a device node in the /dev directory */
device_destroy() /* removes a device node in the /dev directory */
The main points that differ from your previous helloworld_rpi3_char_driver driver will now be
described:
1. Include the following header file to create the class and device files:
#include <linux/device.h> /* class_create(), device_create() */
[ 80 ]
Chapter 4 Character Drivers
2. Your driver will have a class name and a device name; hello_class is used as the class name
and mydev as the device name. This results in the creation of a device that appears on the
file system at /sys/class/hello_class/mydev. Add the following definitions for the device and
class names:
#define DEVICE_NAME "mydev"
#define CLASS_NAME "hello_class"
3. The hello_init() function is longer than the one written in the helloworld_rpi3_char_driver
driver. That is because it now automatically allocates a major number to the device by
using the function alloc_chrdev_region(), as well as registering the device class and creating
the device node.
static int __init hello_init(void)
{
dev_t dev_no;
int Major;
struct device* helloDevice;
/*
* Get the device identifiers using MKDEV. We are doing this
* for teaching purposes, as we only use one identifier in this
* driver and dev_no could be used as a parameter for cdev_add()
* and device_create() without needing to use the MKDEV macro
*/
return 0;
}
4. Create a new helloword_ rpi3_class_driver.c file in the linux_5.4_rpi3_drivers folder, and add
helloword_ rpi3_class_driver.o to your Makefile obj-m variable, then build and deploy the
module to the Raspberry Pi:
[ 81 ]
Character Drivers Chapter 4
~/linux_5.4_rpi3_drivers$ make
~/linux_5.4_rpi3_drivers$ make deploy
static long my_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
pr_info("my_dev_ioctl() is called. cmd = %d, arg = %ld\n", cmd, arg);
return 0;
}
[ 82 ]
Chapter 4 Character Drivers
return 0;
}
[ 83 ]
Character Drivers Chapter 4
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alberto Liberal <[email protected]>");
MODULE_DESCRIPTION("This is a module that interacts with the ioctl system call");
helloworld_rpi3_class_driver.ko demonstration
Reboot your Raspberry Pi to remove the mydev device that you created manually in the previous
lab:
root@raspberrypi:/home/pi# reboot
Load the module:
root@raspberrypi:/home/pi# insmod helloworld_rpi3_class_driver.ko
Hello world init
Allocated correctly with major number 238
device class registered correctly
The device is created correctly
Check that hello_class and mydev are created, and see the entries under mydev:
root@raspberrypi:/home/pi# ls /sys/class
bcm2708_vcio hello_class iscsi_session raw thermal
bcm2835-gpiomem hidraw iscsi_transport rc tty
bdi hwmon leds regulator uio
block i2c-adapter lirc rfkill vc
bluetooth i2c-dev mdio_bus rtc vchiq
bsg ieee80211 mem scsi_device vc-mem
devcoredump input misc scsi_disk vc-sm
dma iscsi_connection mmc_host scsi_host video4linux
dma_heap iscsi_endpoint net sound vtconsole
gpio iscsi_host power_supply spi_master watchdog
graphics iscsi_iface pwm spi_slave
root@raspberrypi:/home/pi# ls -l /dev/mydev
crw------- 1 root root 238, 0 Apr 8 18:56 /dev/mydev
root@raspberrypi:/home/pi# ls /sys/class/hello_class/mydev
dev power subsystem uevent
See the assigned mayor and minor numbers for the device mydev:
root@raspberrypi:/home/pi# cat /sys/class/hello_class/mydev/dev
238:0
Run the application ioctl_test:
root@raspberrypi:/home/pi# ./ioctl_test
my_dev_open() is called.
my_dev_ioctl() is called. cmd = 100, arg = 110
my_dev_close() is called.
Remove the module:
[ 84 ]
Chapter 4 Character Drivers
Where:
• minor is the minor number being registered.
• name is the name for this device, found in the /proc/misc file.
• fops is a pointer to the file_operations structure.
• parent is a pointer to a device structure that represents the hardware device exposed by this
driver.
The misc driver exports two functions, misc_register() and misc_deregister(), to register and
unregister their own minor number. These functions are declared in include/linux/miscdevice.h and
defined in drivers/char/misc.c in the kernel source tree:
int misc_register(struct miscdevice *misc);
int misc_deregister(struct miscdevice *misc);
[ 85 ]
Character Drivers Chapter 4
The misc_register() function registers a miscellaneous device with the kernel. If the minor number
is set to MISC_DYNAMIC_MINOR, a minor number is dynamically assigned and placed in the
minor field of the miscdevice structure. In other cases, the minor number requested is used.
The structure passed as an argument is linked into the kernel and may not be destroyed until it has
been unregistered. By default, an open() syscall to the device sets the file->private_data to point to
the structure. A zero is returned on success and a negative errno code for failure.
The typical code snippet for assigning a dynamic minor number is as follows:
static struct miscdevice my_dev;
int init_module(void)
{
my_dev.minor = MISC_DYNAMIC_MINOR;
my_dev.name = "my_device";
my_dev.fops = &my_fops;
misc_register(&my_dev);
pr_info("my: got minor %i\n", my_dev.minor);
return 0;
}
4. Create a new misc_ rpi3_driver.c file in the linux_5.4_rpi3_drivers folder, and add misc_ rpi3_
driver.o to your Makefile obj-m variable, then build and deploy the module to the Raspberry
Pi:
~/linux_5.4_rpi3_drivers$ make
[ 86 ]
Chapter 4 Character Drivers
static long my_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
pr_info("my_dev_ioctl() is called. cmd = %d, arg = %ld\n", cmd, arg);
return 0;
}
if (ret_val != 0) {
pr_err("could not register the misc device mydev");
return ret_val;
}
[ 87 ]
Character Drivers Chapter 4
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alberto Liberal <[email protected]>");
MODULE_DESCRIPTION("This is the helloworld_char_driver using misc framework");
misc_rpi3_driver.ko demonstration
Load the module:
root@raspberrypi:/home/pi# insmod misc_rpi3_driver.ko
Hello world init
mydev: got minor 60
Check that mydev is created under the misc class folder and under the /dev folder:
root@raspberrypi:/home/pi# ls /sys/class/misc
autofs cpu_dma_latency hw_random mydev vcsm-cma
cachefiles fuse loop-control rfkill watchdog
root@raspberrypi:/home/pi# ls -l /dev/mydev
crw------- 1 root root 10, 60 Apr 8 18:59 /dev/mydev
Run the application ioctl_test:
root@raspberrypi:/home/pi# ./ioctl_test
my_dev_open() is called.
my_dev_ioctl() is called. cmd = 100, arg = 110
my_dev_close() is called.
Remove the module:
root@raspberrypi:/home/pi# rmmod misc_rpi3_driver.ko
Hello world exit
[ 88 ]