Character Devices, Major and Minor numbers

Based on granularity of access, there are two classes of devices:

  1. Character devices are accessed as a stream of bytes. Eg: Keyboards
  2. Block devices are accessed in blocks. For instance, hard disks transfer data in blocks of multiple bytes at a time.

The kernel uses major and minor numbers to identify the attached hardware devices. Major number usually tells us the type of device. Minor numbers are used to differentiate two or more devices with the same major number. Some minor numbers are reserved. The driver writer can choose to use a specific minor number for a device by reserving it, or allow the kernel to assign any free minor number. The meaning of major numbers and the list of reserved minor numbers can be found in Documentation/admin-guide/devices.txt.

ls /dev -l
# ....
# crw-------    241,0 root   27 Oct 15:25 ng0n1
# crw-rw-rw-      1,3 root   27 Oct 15:25 null
# crw-------    242,0 root   27 Oct 15:25 nvme0
# brw-rw----      3,0 root   27 Oct 15:25 nvme0n1
# brw-rw----      3,1 root   27 Oct 15:25 nvme0n1p1
# ....

The first character in the output of ls command tells us if it is a character (c) or a block (b) device. It also shows the major and minor numbers of the device files.

Misc Char device

Creating a character device involves choosing a major and minor number to use and registering the device with the kernel. For creating simple character device files which are not associated with any hardware or cannot be put in any other Major number categories, we can instead create a Miscellaneous character device.

Misc char devices makes device registration a bit easier. The functions for handling open and llseek syscalls on misc char devices are already defined. We only have to define functions for other syscalls that we want to support. Also, All misc devices share the same major number: 10 (defined as MISC_MAJOR).

Creating a Misc Char device

The structure and functions needed to create misc char devices are given below:

struct miscdevice  {
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
	struct device *parent;
	struct device *this_device;
	const struct attribute_group **groups;
	const char *nodename;
	umode_t mode;
};

extern int misc_register(struct miscdevice *misc);
extern void misc_deregister(struct miscdevice *misc);

To create a misc char device:

  1. Include include/linux/miscdevice.h file as it contains the structure and functions used for creating a misc char device.
  2. Initialize a miscdevice structure
  3. Register the miscdevice using the misc_register function in the initialization function of the kernel module
  4. Unregister the miscdevice using the misc_unregister funciton in the exit function

An example: Echo device

To better understand how to create a misc char device, we will look at a kernel module that implements an echo device.

  • When we write to the echo device file, it will store upto a page of data
  • When we read from the echo device file, it will return the data that was last written

I have given the source code of the module with explanations in between.

echo.c

Prelude

  • Include header files. The first three headers are required by all kernel modules.
  • Use module macros to add license, author and description
#include<linux/init.h>
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/fs.h>            // For struct file_operations
#include<linux/miscdevice.h>    // For struct misc device and register functions
#include<linux/uaccess.h>       // For permissions macros
#include<linux/slab.h>          // For kzalloc
#include<linux/string.h>        // For string helper functions
#include<linux/rwsem.h>         // For reader-writers lock

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Nihaal");
MODULE_DESCRIPTION("Misc char driver");

Buffer allocation and Locking

We need to allocate some space to store the data that is written to the device file. To do that, we first declare a static char pointer kernel_buffer.

Then in the initialization function of the module, we allocate memory using the kzalloc function. kzalloc function takes as input, the size of memory to allocate (in bytes) and the type of memory to allocate. It allocates the required memory and zero fills it. We use GFP_KERNEL which is used for kernel internal allocations, as the type of memory.

Any memory allocated dynamically should be freed when the module exits. So in the exit function, we use kfree to deallocate memory occupied by the buffer.

Multiple processes may try to read or write to the device file concurrently. To avoid an inconsistent state, we use a reader-writer lock to protect the buffer. The reader-writer lock allows multiple readers to read simultaneously, but when a process is writing to the buffer, no other process can read or write to it at the same time.

DECLARE_RWSEM macro declares a reader-writer’s lock. We can then use down_read, down_write functions to acquire the lock for reading or writing, and release it with up_read or up_write depending on which down function we used.

static char *kernel_buffer;
static DECLARE_RWSEM(echo_rwlock);

Read function

Here we define echo_read function which will be executed when a process reads from the device file. The read function should

  • Copy the requested amount (size bytes) of data into the user buffer (user_buffer),
  • Update the file offset (file_pos)
  • Return
    • the number of bytes copied to the user buffer, if the copy was successful
    • 0, if the End of File is reached
    • negative value, if there is an error

We cannot copy data from the kernel buffer to user buffer directly as they are on different address spaces. So we use copy_to_user function to copy data from the kernel address space to the user process address space.

ssize_t echo_read (struct file *filp, char __user * user_buffer, size_t size, loff_t * file_pos) {
	size_t len, ret;

	// Check if a page of data has been read already
	if ((*file_pos) >= PAGE_SIZE) {
		return 0;
	}

	// Acquire read lock
	down_read(&echo_rwlock);
	len = (size > PAGE_SIZE)? PAGE_SIZE: size;
	// Copy data to user buffer
	ret = copy_to_user(user_buffer, kernel_buffer, len);
	len = len - ret;
	*file_pos += len;
	// Release read lock
	up_read(&echo_rwlock);
	return len;
}

Write function

Here we define echo_write function which will be executed when a process writes to the device file. The write function should

  • Copy the requested amount (size bytes) of data from the user buffer (user_buffer),
  • Update the file offset (file_pos)
  • Return
    • the number of bytes left to be copied, if the copy was successful
    • negative value, if there is an error

Here again, since we cannot directly copy data between kernel and user address spaces, we use copy_from_user function.

ssize_t echo_write (struct file *filp, const char __user * user_buffer, size_t size, loff_t * file_pos) {
	size_t len, ret;

	// Check if a page of data has been written already
	if ((*file_pos) >= PAGE_SIZE) {
		return -ENOMEM;
	}

	// Acquire write lock
	down_write(&echo_rwlock);
	memset(kernel_buffer, 0, PAGE_SIZE);
	len = (size > PAGE_SIZE)? PAGE_SIZE: size;
	// Copy data to kernel buffer
	ret = copy_from_user(kernel_buffer, user_buffer, len);
	len = len - ret;
	*file_pos += len;
	// Release write lock
	up_write(&echo_rwlock);
	return len;
}

Creating the misc device

Now that we have defined functions for handling read and write system calls, we will first create a file_operations structure, setting the read and write members of the file_operations struct as the address of the functions we have defined.

Then we create the miscdevice structure, initializing some of its members:

  • minor is set to the minor number we want for our device. If we use MISC_DYNAMIC_MINOR for this member, the kernel will assign any minor number that is available. If we specify a number, the kernel will try to assign that minor number but device registration may fail if that minor number is already used by some other device.

  • name specifies the name of the device file

  • fops member is set as the file_operations structure we created

  • mode specifies the permissions for device file access. It would be more readable to use macros than to write the octal permission value. So we use permission macros defined in include/linux/stat.h and include/uapi/linux/stat.h.

    S_IRUGO denotes that User, Group and Others (UGO) have Read (R) permissions. Similarly S_IWUGO denotes that User, Group and Others (UGO) have Write (W) permissions. For the echo device, we want any user to have read and write access, so we use (S_IRUGO|S_IWUGO)

static struct file_operations echo_fops = {
	.read	= &echo_read,
	.write	= &echo_write,
};

static struct miscdevice echo_device = {
	.minor	= MISC_DYNAMIC_MINOR,
	.name	= "echo",
	.fops	= &echo_fops,
	.mode	= (S_IRUGO|S_IWUGO),
};

Registering the misc device

In the module’s initialization function, we first allocate space for the buffer. Then we register the miscdevice struct that we initialized earlier using the misc_register function. If the function returns 0, then the device has been created successfully.

In the exit function, we free the buffer and deregister the device using misc_deregister function.

static int __init echo_init(void)
{
	// Allocate a page sized buffer
	kernel_buffer = (char*) kzalloc(PAGE_SIZE, GFP_KERNEL);
	if (!kernel_buffer)
		return -ENOMEM;

	// Register misc char device
	if(misc_register(&echo_device)) {
      kfree(kernel_buffer);
		return -ENODEV;
	}
	return 0;
}

static void __exit echo_exit(void)
{
	// Free buffer
	kfree(kernel_buffer);
	// Unregister misc char device
	misc_deregister(&echo_device);
}

module_init(echo_init);
module_exit(echo_exit);

Makefile

obj-m := echo.o
all:
	make -C /usr/lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C /usr/lib/modules/$(shell uname -r)/build M=$(PWD) clean

Testing echo

  • Build and load the module

      make
      sudo insmod echo.ko
      ls /dev/echo
      # crw-rw-rw- 10,123 root  3 Nov 20:53 /dev/echo
    

    The echo device will show up at /dev/echo. Notice that the major number 10 tells us that it is a misc device.

  • Write to the device

      echo "Good Morning!" > /dev/echo
    
  • Read from the device. It will return what was last written to it.

      cat /dev/echo
      # Good Morning!
    
  • Unload the module

      sudo rmmod echo
    

References