Making a character device kernel module
Disclaimer: this post is a WIP. Stay tuned!
The introduction on kernel modules focused primarily on how to integrate an out-of-tree driver into a modern Linux system, using only the bare miminum code to demonstrate a working module.
With the basic kernel development framework laid out, it’s now feasible to work on a more complex driver implementation. This post shall use more extensive kernel interfaces to implement character device driver, which will be used to demonstrate how to create an device file that can respond to typical file operations by a user process.
Writing the driver code
First, the individual header files and function signatures. There are many more headers to be included compared to the module from the previous post as there are numerous interfaces required for a driver to be able to interact with sysfs, procfs, and devfs:
Now some C macros will be defined for naming the various paths on the filesystem the driver will be registering to:
By convention, character devices are defined with the file_operations struct which implements common operations such as reading and writing. These struct members are implicitly-defined; to keep the driver implementation simple, only the following will be implemented:
A more complete driver could explicitly implement other struct members which in turn can be found by running the following command:
$ grep -Pzo "struct file_operations {(.|\n)*__randomize_layout;\n" /usr/src/kernels/$(uname -r)/include/linux/fs.h
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 *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
A character device must be able to open so that a program like cat
can attempt to read its contents. The following function checks if the device is open with a global variable representing open/close state, and will in turn respond depending on the state:
Devices and files generally should be checked to see if they’re opened to avoid issues with concurrent access. This driver will copy a string into msg
, when is then accessed when attempting to read the device:
The code for device_read()
may seem a bit complicted, but the gist is that the msg
string gets copied into userspace with put_user()
, and by convention its byte length will be returned to signify success unless it was zero bytes long.
Once the file contents have been read, it’s now time to release the device so that other programs may access it. Implementing this logic is very simple:
The character device is going be to read-only for now, so it should print an informative log message and return an error status:
Now it’s time to bring it all together. There’s some setup required for allowing the character device to tie into procfs, devfs, and sysfs once the kernel module is initialized:
The kernel module in turn must do some cleanup on shutdown: