Introduction to Linux kernel Character Device Drivers
Written by Marcelo Schmitt
Written on , last modified onThis tutorial explains a few aspects of Linux character devices with a basic character device driver example. It is best enjoyed as a continuation of “Introduction to Linux kernel build configuration and modules” tutorial.
Command Summary
Skip this section if you have not completed this tutorial yet. This is a reference for those who have gone through this tutorial and want to remember specific commands.
cat /proc/devices
stat <file>
mknod <file_name> <type> <major_num> <minor_num>
dmesg -w
Introduction
In Linux, character devices facilitate sequential data exchange between user space and the system, handling dynamic data streams. They provide a flexible interface for interacting with hardware or software components.
Character device drivers define how these devices operate, managing data flow and access. They serve as an abstraction layer, enabling controlled and efficient communication between applications and the underlying system.
Introduction to Linux kernel character device drivers
This tutorial presents and explains a few key concepts related to Linux character devices, then presents an example Linux kernel character device driver that is built on top of the presented ideas.
Summary of the parts of this tutorial:
- Character devices
- Major and Minor Numbers
- File operations
- Bringing device IDs and file operations together
- A character device driver example
- Testing the
simple_char
driver
1) Character devices
A character device is an abstraction provided by Linux operating systems to
support devices that can be read from or written to with relatively small data
transfers which are usually byte size or few bytes size.
They are often abstracted as files in the file
system and accessed through conventional file access system calls.
Devices supported as character devices are serial ports, keyboards, mice, etc.
Example of character device files are /dev/ttyS0
, dev/input/mouse0
,
/dev/kmsg
, /dev/zero
.
2) Major and Minor Numbers
The files associated with character devices (like /dev/ttyS0
and
dev/input/mouse0
) are special types of files which allow users to interface
with devices from the user space. Under the hood of character device files are
the device drivers that handle the system calls for them.
The association between device files and devices is made with
a device ID that consists of two parts: a major and a minor number
[1].
The minor number is only used within the device driver to distinguish between
multiple devices of same type (such as the first and the second display) or to
switch between operation modes of a device. The major number can be requested
to be a specific number or allocated dynamically.
The device ID formed from the major/minor number combination is what
the kernel uses to choose which driver to run to support a particular device.
Inside the Linux kernel, the device ID is stored as a 32-bit unsigned integer
defined by the dev_t
type. The 12 most significant bits of a dev_t
variable
store the major device number while the 20 remaining bits store the minor
number.
In old versions of the Linux kernel, once a major number was registered for a driver, no other driver could use the same major number and all minor numbers associated with that major number were owned by the driver. Back then, one could state that a major number was unique for a driver. That might not hold always true nowadays since, in modern kernels, drivers allocate a range (not all) of minor numbers within the set of possible minor numbers. With that, it is now possible for more than one driver to have the same major number as long as their range of minor numbers do not intersect.
Within the Linux kernel, one can use the functions declared in
include/linux/fs.h
to allocate device major and minor numbers for a device.
For example, to dynamically allocate a device major number along with a range of
minor numbers one can call alloc_chrdev_region()
.
#include <linux/fs.h>
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
The macros MAJOR()
, MINOR()
, and MKDEV()
from include/linux/kdev_t.h
can be used to extract the major/minor number from a dev_t
variable and
to make a device ID from a pair of major and minor numbers, respectively.
At any moment, a list of character and block devices in the system can be
retrieved from the /proc/devices
file.
$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
6 lp
...
The numbers on the left are the major device numbers and the strings on the
right correspond to the registered names of the device drivers that handle the
respective device. Notice in the example above that the devices represented by
the files /dev/tty
, /dev/console
, and /dev/ptmx
share the same major
number. That is possible thanks to each driver only registering a subset of the
available minor numbers.
One can use stat
to get major and minor numbers for the device backing a file.
For example:
$ stat /boot/vmlinuz-6.5.0-5-amd64
File: /boot/vmlinuz-6.5.0-5-amd64
Size: 9127904 Blocks: 17832 IO Block: 4096 regular file
Device: 8,1 Inode: 4981079 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2024-01-04 10:51:19.000000000 -0300
Modify: 2023-11-29 04:45:07.000000000 -0300
Change: 2024-01-04 10:53:32.315343926 -0300
Birth: 2024-01-04 10:53:29.707343958 -0300
The output of stat shows that the Linux image (vmlinuz-6.5.0-5-amd64
) is
stored as a regular file and the device responsible for it has major number 8
and minor number 1 (/dev/sda1
).
To get major and minor numbers of character devices we must look at the Device type field.
$ stat /dev/ttyS0
File: /dev/ttyS0
Size: 0 Blocks: 0 IO Block: 4096 character special file
Device: 0,5 Inode: 94 Links: 1 Device type: 4,64
Access: (0660/crw-rw----) Uid: ( 0/ root) Gid: ( 20/ dialout)
Access: 2024-03-29 09:14:38.329879779 -0300
Modify: 2024-03-29 09:14:57.329879779 -0300
Change: 2024-03-29 09:14:38.329879779 -0300
Birth: 2024-03-29 09:14:32.192000051 -0300
So, that tty
character device has major number 4 and minor number 64.
We can manually create files to interface with character devices using mknod
[2].
A file that serves as interface for a device is often called a device node,
device file, or device special file [3].
3) File operations
Since character devices may interface with user space through files, character
device drivers may implement functions to handle file access system calls.
The set of those syscalls implemented by a device driver is set into a struct
file_operations
object.
There are several different file operations a character driver can
implement. In this tutorial we will focus on the most basic system calls for a
character device: open, close, read, write. All of those have
manual pages of same name. Whithin the Linux kernel, these operations are
respectively handled by open, release, read, and write functions
stored in struct file_operations
objects.
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 *);
<snipped>
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
<snipped>
}
See the definition of struct file_operations
in include/linux/fs.h
for a
complete list of entry points a driver can implement.
File read/write operations
Kernel code should not directly de-reference pointers to memory in user space
(i.e., we should not access the __user buffer with conventional C memory access
operations).
Kernel and user space memory addresses may be (and often are) in separate
address regions, offset by PAGE_OFFSET
physical memory addresses.
Because of that, the address of a user space pointer might not lead
to the memory address of user space data when de-referencing it from kernel
space. Furthermore, most modern systems provide virtualised memory address
for user space memory and memory paging, meaning that a user space
memory address may not be valid at a given moment either because
its page has not been allocated yet or it has been swapped out.
If you got interested about Linux memory management, watch Matt Porter’s
Introduction to Memory Management in Linux
after finishing this tutorial.
To implement the read and write entry points we will use memory access
functions from include/linux/uaccess.h
.
#include <linux/uaccess.h>
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
The semantics of the copy_from_user()
function are the same as memcpy()
.
It copies n bytes of data from the source (from) memory area to the
destination one (to). One important difference though is that the
return of copy_from_user()
is the number of bytes not successfully copied.
The copy_to_user()
is analogous to copy_from_user()
but with opposite
direction.
4) Bringing device IDs and file operations together
For a character device, we have major/minor numbers (combined into dev_t
) to
identify the driver to handle system calls for files/nodes linking to it. We
also have struct file_operations
to store the set of operations supported for
the character device. To tie together device IDs and file operations, the kernel
uses the struct cdev
structure (character device) defined in
include/linux/cdev.h
.
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
struct kobject kobj
is the embedded kobject struct of the cdev structure used
for reference counting.
struct module *owner
holds the owner of the resource within the kernel.
Later, we will use the character device module as resource owner.
const struct file_operations *ops
is the file operations structure discussed
earlier.
struct list_head list
is a double linked list for traversing character devices.
dev_t dev
is the device ID.
unsigned int count
holds the amount of minor numbers owned by the character
device.
The rudimentary functions for registering a cdev are:
#include <linux/cdev.h>
struct cdev *cdev_alloc(void)
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
void cdev_del(struct cdev *p)
struct cdev *cdev_alloc(void)
allocates and initializes a cdev struct object.
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
is
equivalent to cdev_alloc()
except it expects the caller to pass a pointer to
an already allocated cdev object and it also sets the file operations field for
it. If using cdev_init()
, the caller is responsible for freeing the cdev
object after the character device is removed from the system.
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
registers/adds the
character device to the system by creating an entry for it in the struct
kobj_map *cdev_map
array kept in fs/char_dev.c.
void cdev_del(struct cdev *p)
removes the character device from the system by
removing its entry from the cdev_map array. It also decrement the reference
count of the character device which will free the cdev struct if it was
allocated with cdev_alloc()
.
5) A character device driver example
Let’s create an example character device driver to see how it all works. Create
${IIO_TREE}/drivers/char/simple_char.c
in the Linux kernel sources and add the code for
it. Notice most of the ideas within it have been covered in previous sections.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h> /* for MAJOR */
#include <linux/cdev.h> /* for cdev */
#include <linux/fs.h> /* for chrdev functions */
#include <linux/slab.h> /* for malloc */
#include <linux/string.h> /* for strlen() */
#include <linux/uaccess.h> /* copy_to_user() */
struct cdev *s_cdev;
static dev_t dev_id;
#define S_BUFF_SIZE 4096
static char *s_buf;
#define MINOR_NUMS 1
static int simple_char_open(struct inode *inode, struct file *file)
{
pr_info("%s: %s\n", KBUILD_MODNAME, __func__);
return 0;
}
static ssize_t simple_char_read(struct file *file, char __user *buffer,
size_t count, loff_t *ppos)
{
int n_bytes;
pr_info("%s: %s about to read %ld bytes from buffer position %lld\n",
KBUILD_MODNAME, __func__, count, *ppos);
n_bytes = count - copy_to_user(buffer, s_buf + *ppos, count);
*ppos += n_bytes;
return n_bytes;
}
static ssize_t simple_char_write(struct file *file, const char __user *buffer,
size_t count, loff_t *ppos)
{
int n_bytes;
pr_info("%s: %s about to write %ld bytes to buffer position %lld\n",
KBUILD_MODNAME, __func__, count, *ppos);
n_bytes = count - copy_from_user(s_buf + *ppos, buffer, count);
return n_bytes;
}
static int simple_char_release(struct inode *inode, struct file *file)
{
pr_info("%s: %s\n", KBUILD_MODNAME, __func__);
return 0;
}
static const struct file_operations simple_char_fops = {
.owner = THIS_MODULE,
.open = simple_char_open,
.release = simple_char_release,
.read = simple_char_read,
.write = simple_char_write,
};
static int __init simple_char_init(void)
{
int ret;
pr_info("Initialize %s module.\n", KBUILD_MODNAME);
/* Allocate an internal buffer for reads and writes. */
s_buf = kmalloc(S_BUFF_SIZE, GFP_KERNEL);
if (!s_buf)
return -ENOMEM;
strcpy(s_buf, "This is data from simple_char buffer.");
/* Dynamically allocate character device device numbers. */
/* The name passed here will appear in /proc/devices. */
ret = alloc_chrdev_region(&dev_id, 0, MINOR_NUMS, "simple_char");
if (ret < 0)
return ret;
/* Allocate and initialize the character device cdev structure */
s_cdev = cdev_alloc();
s_cdev->ops = &simple_char_fops;
s_cdev->owner = simple_char_fops.owner;
/* Adds a mapping for the device ID into the system. */
return cdev_add(s_cdev, dev_id, MINOR_NUMS);
}
static void __exit simple_char_exit(void)
{
/*
* Undoes the device ID mapping and frees cdev struct, removing the
* character device from the system.
*/
cdev_del(s_cdev);
/* Unregisters (disassociate) the device numbers allocated. */
unregister_chrdev_region(dev_id, MINOR_NUMS);
kfree(s_buf);
pr_info("%s exiting.\n", KBUILD_MODNAME);
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_AUTHOR("A Linux kernel student <name.surname@usp.br>");
MODULE_DESCRIPTION("A simple character device driver example.");
MODULE_LICENSE("GPL");
Don’t forget to add a configuration symbol for the simple_char driver within
${IIO_TREE}/drivers/char/Kconfig
.
config SIMPLE_CHAR
tristate "Simple character driver example"
default m
help
This option enables a simple character driver that implements basic
file access operations.
Also add a build rule for it in ${IIO_TREE}/drivers/char/Makefile
.
obj-$(CONFIG_SIMPLE_CHAR) += simple_char.o
Configure and build the Linux kernel with the simple_char driver as explained in previous build and configuration tutorials.
If you get prompted by many config options after running make -C "$IIO_TREE"
Image.gz modules
, consider interrupting the kernel build and running make -C
"$TREE_IIO" olddefconfig
.
6) Testing the simple_char
driver
Install the simple_char driver in a Linux OS. If you have followed the previous
tutorials, you can install it to the arm64 virtual machine.
Load the driver module and inspect /proc/devices
to get what major number
the character device got. Then use that major number to create a file
to interface with your character device.
@VM
root@localhost:~# modprobe simple_char
root@localhost:~# cat /proc/devices | grep simp
511 simple_char
root@localhost:~# mknod simple_char_node c 511 0
root@localhost:~# stat simple_char_node
File: simple_char_node
Size: 0 Blocks: 0 IO Block: 4096 character special file
Device: 254,2 Inode: 2142 Links: 1 Device type: 511,0
Access: (0644/crw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2024-03-16 18:26:55.133581798 +0000
Modify: 2024-03-16 18:26:55.133581798 +0000
Change: 2024-03-16 18:26:55.133581798 +0000
Birth: 2024-03-16 18:26:55.133581798 +0000
Create ${LK_DEV_DIR}/read_prog.c
to test reading from our character device.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 256
/* Example inspired from open(2) */
int main(int argc, const char **argv)
{
char buf[BUF_SIZE];
int fd;
if (argc < 2)
return -22;
fd = open(argv[1], O_RDONLY);
read(fd, buf, BUF_SIZE);
printf("Read buffer: %s\n", buf);
close(fd);
}
Although it is advisable to handle possible errors that may happen when
accessing files, ${LK_DEV_DIR}/read_prog.c
was left with no error handling to keep the
example small and simple.
Compile the read test program for your Linux test machine.
If you have followed the previous tutorials, you can use the GCC from the cross
compiler toolchain you have for building the kernel to compile the test program.
Send the binary to the virtual machine and run it there.
@host
aarch64-linux-gnu-gcc read_prog.c -o read_prog
scp read_prog root@<VM-IP-ADDRESS>:~/
@VM
root@localhost:~# ./read_prog simple_char_node
Here is the ${LK_DEV_DIR}/write_prog.c
test program for writing to the character device.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define BUF_SIZE 256
int main(int argc, const char **argv)
{
char buf[BUF_SIZE];
int errsv;
int ret;
int fd;
if (argc < 2)
return -EINVAL;
sprintf(buf, "A new message for simple_char.");
fd = open(argv[1], O_RDWR);
ret = write(fd, buf, BUF_SIZE);
if (ret < 0) {
errsv = errno;
printf("Error: %d", errsv);
}
printf("wrote %d bytes to buffer\n", ret);
return close(fd);
}
Build and run the same way done for the read test program.
@host
aarch64-linux-gnu-gcc write_prog.c -o write_prog
scp write_prog root@<VM-IP-ADDRESS>:~/
@VM
root@localhost:~# ./write_prog simple_char_node
Proposed Exercises
-
Printing device numbers: Modify the simple_char driver to make it print major and minor device numbers on device open.
-
Device private data: Modify the simple_char driver to register more than one minor device number and make it keep separate buffers for each of them. Check the device minor number on device open and allocate a buffer for it if it doesn’t have one. Also, make the read/write operations run over the buffer for the particular major/minor device number pair. Access
f_inode
field ofstruct file
to get a pointer to the device inode then accessi_rdev
field ofstruct inode
to get the device ID (dev_t
) from which you can extract the device minor number. On module unload, free all allocated buffers. With that, we can think of each pair of major/minor numbers as a different character device and each of them will have their own data. Finally, create device nodes with different minor numbers to test you implementation.
Conclusion
This post presented a few key concepts related to Linux kernel character devices and provided a character device driver example that shows how the covered concepts apply in practice.
History
- V1: Release
- V2: Review for the Free Software Development course offered in 2025-1 at IME-USP
References
[1] . “makedev(3)”. URL: https://man.archlinux.org/man/makedev.3.en. ⤴
[2] David MacKenzie. “MKNOD(1)”. URL: https://man.archlinux.org/man/mknod.1. ⤴
[3] David MacKenzie. “device_node”. URL: https://wiki.debian.org/device_node. ⤴
comments powered by Disqus