# Linux Kernel Labs: Character device driver (Part 1)

# Character device drivers

Another week with the Linux kernel source! This week was tough with the topics and the assignments.

# Lectures

The lectures of these weeks were mostly about:

*   **Interrupts:** Events that can alter program execution and can be generated by hardware devices or the CPU.
    
*   **I/O access:** Operations that allow read/write functionality from/to device ports (=registers).
    
*   **Deferred work:** Postponing or scheduling for a later time the execution of a routine.
    

But let's focus on the labs, because yes, they were more interesting and I learned more from them (by practicing).

# Labs

## Character device drivers

[**Links**](https://linux-kernel-labs.github.io/refs/heads/master/so2/lab3-device-drivers.html)

A character device is a hardware or virtual computer component that sends and receives data sequentially, one **byte** or **character** at a time. The device driver is the kernel module that interacts with this device. Examples of character devices include the keyboard and mouse.

The hardware devices are accessed by the user through special device files. These files are grouped into the `/dev` directory, and system calls `open`, `read`, `write`, `close`, `lseek`, `mmap` etc. are redirected by the operating system to the device driver associated with the device. Usually we'll write user code (client code) that will allow interaction with them.

**NB**: In contrast to character devices, we have block devices where data volume is large and organized into **blocks**, and they also have their own API to interact with them. I've already read that there is a future lab dedicated to them.

As the character device driver supports many operations, let's see how to create a character device driver (=kernel module) and handle these operations.

Following are the common steps (the long way) to set up a character device driver named `my_char_dev`:

### Set the Major and Minor numbers

**Major** number identifies the device type (we can see it like the group/category of the device), while the **minor** number identifies the device itself. The **major number** can identify the `DISK` or `Serial Port` and the **minor** identifies the `first disk` or the `second serial port` on which the driver will work.

In the initialization function passed to `module_init` (refer to my article on [modules](https://tawaliou.com/linux-kernel-labs-kernel-module-and-kernel-api#kernel-modules)):

```c
#include <linux/cdev.h>

int register_chrdev_region(dev_t first, unsigned int count, char *name);
```

In our case:

```c
#define MY_MAJOR 42
#define MY_MINOR 0
#define MY_MINOR_COUNT 0
#define MY_DRIVER_NAME "my_char_dev"   

// in the initialization function
register_chrdev_region(MKDEV(MY_MAJOR, MY_MINOR), MY_MINOR_COUNT, MY_DRIVER_NAME);
```

The registration can fail and return an error, we need to check that and do the appropriate operations.

I recommend at the same time to unregister the device allocated in the exit so we will not forget to do it. In the exit function passed to `module_exit` (refer to my article on [modules](https://tawaliou.com/linux-kernel-labs-kernel-module-and-kernel-api#kernel-modules)):

```c
#include <linux/cdev.h>

void unregister_chrdev_region(dev_t first, unsigned int count);
```

In our case:

```c
// in the exit function
unregister_chrdev_region(MKDEV(MY_MAJOR,MY_MINOR),MY_MINOR_COUNT);
```

NB: If the number of minor devices exceeds `MY_MINOR_COUNT`, the registration will move to the next major.

### Initialize and add `cdev` structure to kernel

Here are the methods we will use to add and initialize our device to the kernel:

```c
void cdev_init(struct cdev *cdev, struct file_operations *fops);

int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
```

We need the `struct cdev`. In reality, our driver can manage some data related to each device, like holding some information per device. So we'll define a struct that will have `cdev` as an attribute with additional data:

```c
struct my_cdev_data {
    struct cdev cdev;
    int nb_used;
    pid_t related_pid; 
};

struct my_cdev_data my_dev;
```

**NB**: We can have more than one minor and we can do `struct my_cdev_data my_devs[MY_MINOR_COUNT];` to index all the minor devices.

Inside the initialization as always we'll initialize:

```c
// inside the initialization function
cdev_init(&my_dev.cdev, &my_fops);
cdev_add(&my_dev.cdev, MKDEV(MY_MAJOR, MY_MINOR), 1);
```

In the next section, we'll talk about `my_fops`.

In the exit function (when the module is unloaded), we will not forget to delete the device before unregistration:

```c
void cdev_del(struct cdev *dev);
```

So:

```c
// in the exit function
cdev_del(&my_dev.cdev);


unregister_chrdev_region(MKDEV(MY_MAJOR,MY_MINOR),MY_MINOR_COUNT);
```

### Define file operations with `struct file_operations`

Above, we said that we can do some actions (`syscalls`) like `open`, `read`, `write`, `close` and many more on the devices through our driver, right? Good.

`struct file_operations` will allow us to do that, because it defines some functions that we need to provide for each operation.  
Refer to this link [struct file\_operations](https://elixir.bootlin.com/linux/v7.1.5/source/include/linux/fs.h#L1926) to have an overview of these operations.  
Here I'll just show briefly some of them:

```c
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 (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
	...
	...
};
```

`release` is the equivalent of `close`.  
When we do a syscall from the user space to interact with our driver like:

```c
int fd = open("/dev/my_char_dev", O_RDONLY);
...
read(fd, buffer, size) 
...
```

Under the hood, the equivalent operation handlers defined in `file_operations` are called with the appropriate arguments.

In our case we can do something like:

```c
static int my_open (struct inode *inode, struct file *file)
{
 // pr_info("Device opened");
    return 0;
}

static int my_release (struct inode *inode, struct file *file)
{
    // pr_info("Device released");
    return 0;
}

static ssize_t my_read (struct file *file, char __user *buffer, size_t size, loff_t *offset)
{
    // pr_info("Data read");
    return size;
}

static ssize_t my_write (struct file *, const char __user *, size_t, loff_t *)
{
    // pr_info("Data write");
    return size;
}

struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
    .unlocked_ioctl = my_ioctl
};
```

In these functions, you define the operations and how things will be handled. But keep in mind:

*   Use `copy_from_user` and `copy_to_user` when you want to transfer data between the kernel and user space. For example on `char __user *buffer` (it's marked `__user`). So the kernel under the hood can do some security checks like availability and rights to access these memories.
    
*   `unlocked_ioctl`: We'll talk later in (the assignment) about this handler and ioctl (Input Output Control), which is really super fun and powerful to use. It allows the device to do custom operations, not only read or write.
    
*   In these functions, usually we'll need to manage synchronization to avoid concurrency pitfalls when the same resource (maybe `my_dev`) is accessed at the same time by two CPUs.
    

### `mknod` to create `/dev/my_char_dev`

After loading the driver in the kernel with `insmod`, we need to create the device. There are many ways to do it: programmatically or manually.  
For the programmatic way, check `device_create` and `class_create`.

Manually, to create an entry for our driver in `/dev`, we do:

```c
//sudo insmod my_char_dev.ko

sudo mknod /dev/my_char_dev c 42 0
```

where:

*   `c`: is for character device (`b` for block device)
    
*   `42` and `0`: are respectively the major and the minor
    

### Optional: Ports registration

As a character device can interact with hardware devices, there are some functions that allow reading/writing from/to the ports or registers of the hardware device. We can look for `inb` and `outb` in `#include <asm/io.h>`.

Too much? 😭😂, don't worry, in the next post, I'll present my second assignment: Kprobe-based tracer. In this assignment we did tracing on `kmalloc` and some kernel functions, and we used a new way to define device drivers in a shorter way with operations like `open`, `read`, `mknod`, handled under the hood (not by us) by using `struct miscdevice`.

* * *

Here is the complete code of our character device driver with some checks:

```c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Dev");
MODULE_DESCRIPTION("A minimal character device driver with a single minor device");

#define MY_MAJOR 42
#define MY_MINOR 0
#define MY_DRIVER_NAME "my_char_dev"

/* Single-instance device wrapper */
struct my_cdev_data {
    struct cdev cdev;
    int nb_used;
    pid_t related_pid;
};

/* Single device instance */
static struct my_cdev_data my_dev;

/* File operations */
static int my_open(struct inode *inode, struct file *file)
{
    pr_info("%s: Device opened\n", MY_DRIVER_NAME);
    return 0;
}

static int my_release(struct inode *inode, struct file *file)
{
    pr_info("%s: Device released\n", MY_DRIVER_NAME);
    return 0;
}

static ssize_t my_read(struct file *file, char __user *buffer, size_t size, loff_t *offset)
{
    pr_info("%s: Read operation triggered\n", MY_DRIVER_NAME);
    return 0; /* EOF */
}

static ssize_t my_write(struct file *file, const char __user *buffer, size_t size, loff_t *offset)
{
    pr_info("%s: Write operation triggered (%zu bytes)\n", MY_DRIVER_NAME, size);
    return size;
}

static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .read    = my_read,
    .write   = my_write,
    .release = my_release,
};

static int __init my_char_init(void)
{
    int ret;
    dev_t dev_num = MKDEV(MY_MAJOR, MY_MINOR);

    /* 1. Reserve 1 device number */
    ret = register_chrdev_region(dev_num, 1, MY_DRIVER_NAME);
    if (ret < 0) {
        pr_err("%s: Failed to register device number (err: %d)\n", MY_DRIVER_NAME, ret);
        return ret;
    }

    /* 2. Initialize and add the single cdev */
    cdev_init(&my_dev.cdev, &my_fops);
    my_dev.cdev.owner = THIS_MODULE;

    ret = cdev_add(&my_dev.cdev, dev_num, 1);
    if (ret < 0) {
        pr_err("%s: Failed to add cdev\n", MY_DRIVER_NAME);
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    pr_info("%s: Registered successfully (Major %d, Minor %d)\n",
            MY_DRIVER_NAME, MY_MAJOR, MY_MINOR);
    return 0;
}

static void __exit my_char_exit(void)
{
    /* Clean up cdev and unregister region */
    cdev_del(&my_dev.cdev);
    unregister_chrdev_region(MKDEV(MY_MAJOR, MY_MINOR), 1);

    pr_info("%s: Driver unloaded successfully\n", MY_DRIVER_NAME);
}

module_init(my_char_init);
module_exit(my_char_exit);
```

## **Next**

The next post will be about assignment 2 **Kprobe based tracer:** the task is to do an instrumentation (count the calls) of functions like `kmalloc`, `kfree`, `up`, `down_interruptible` in the Linux kernel by using `kretprobe`. I learnt more about device driver by using `miscdevice`, `ioctl` and `hashtable`

By the way, here's my GitHub repo for the assignments: [**kernel-labs-OS-2**](https://github.com/tawaliou/kernel-labs-OS-2)
