Programming Linux Character Driver

Linux Character Driver

Character Device Structures

Character device driver, block device driver and network device driver as the linux kernel three major driver devices, character devices mainly complete the byte read and write operations, common applications are mouse, keyboard, etc., the structure form is shown below:

1
2
3
4
5
6
7
8
struct cdev {
struct kobject kobj;
struct module *owner; // Belonging modules
struct file_operations *ops; // Character device operation method
struct list_head list;
dev_t dev; // Device
unsigned int count;
}
  • The dev_t in the cdev structure represents the 32-bit device number, 12 bits are the major device number and 20 bits are the minor device number. The major device number and minor device number can be obtained from the dev_t by macro definitions MAJOR(dev_t dev) and MINOR(dev_t dev). In addition, dev_t can be generated from the primary and secondary device numbers using the macro definition MKDEV(int major, int minor).
  • The Linux kernel provides a set of functions to operate on character device structures, which can be used to manipulate the cdev structure.
1
2
3
4
5
void cdev_init(struct cdev *, struct file_operations *);  // Used to initialize the members of cdev and establish the connection between cdev and file_operation
struct cdev *cdev_alloc(void); // Used to dynamically request a cdev memory
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned); // Add a cdev to the system to complete the registration of the character device. The call to cdev_add() usually occurs in the character device driver module load function
void cdev_del(struct cdev *); // Deleting a cdev completes the cancellation of the character device. The call to the cdev_del() function usually occurs in the uninstall function of the character device driver module
  • Call register_chrdev_region() or alloc_chrdev_region() function to request a device number from the system, and then call cdev_add() function to register the character device with the system.
1
2
int register_chrdev_region(dev_t from, unsigned count, const char *name);  // Known starting device number
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name); // Unknown starting device number
  • The member functions in the file_operations structure are eventually called by the kernel when the application makes Linux system calls such as open(), write(), read(), close(), etc.
    • llseek() modifies the current read/write location of a file and returns the new location, returning a negative value in case of error.
    • read() reads data from the device, returning the number of bytes read on success and a negative value on error. Corresponds to ssize_t read (int fd, voidbuf, size_t count) and size_t fread (voidptr, size_t size, size_t nmemb, FILE*stream) in user space applications.
    • write() sends data to the device and returns the number of bytes written on success. If this function is not implemented, the user will get the -EINVAL return value when making the write() system call. Corresponds to ssize_t write (int fd, constvoidbuf, size_t count) and size_t fwrite (const voidptr, size_t size, size_t nmemb, FILE*stream) in user space applications.
    • read() and write() indicate end-of-file (EOF) if they return 0.
    • unlocked_ioctl() provides implementation of device-related control commands (neither read nor write operations) and returns a non-negative value to the calling program when called successfully. It is similar to the user-space application calls int fcntl (int fd, int cmd, … /arg/) and intioctl (int d, int request, …) counterparts.
    • mmap() maps device memory into the virtual address space of the process. If the device driver does not implement this function, the user will get the -ENODEV return value when making the mmap() system call. This function is of particular interest for devices such as frame buffers, which are mapped into user space so that applications can access them directly without having to copy memory between the kernel and the application. It corresponds to the voidmmap (voidaddr, size_t length, int prot, int flags, int fd, off_t offset) function in user space applications.
  • The functions for loading and unloading the character device driver module are as follows
    • static int __init mydev_init(void)
    • static void __exit mydev_exit(void)
    • The application of device number and registration of cdev should be implemented in the load function of the character device driver module, while the release of device number and cancellation of cdev should be implemented in the unload function.

Character Structures Programming

Create a new dev folder and create mydev.c and the corresponding Makefile file in this directory

The mydev.c program is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define MYDEV_SIZE 0x1000
#define MEM_CLEAR 0x1
#define MYDEV_MAJOR 230

static int mydev_major = MYDEV_MAJOR; //Define the major device number
module_param(mydev_major, int, S_IRUGO); // module parameter passing

struct mydev_dev { // Define the mydevmen_dev structure
struct cdev cdev; // Character Structures
unsigned char mem[MYDEV_SIZE]; // Memory
};

struct mydev_dev * mydev_devp;// Declare the mydev structure object

// Read function of mydev device driver
static ssize_t mydev_read(struct file * filp, char __user * buf, size_t size, loff_t * ppos) {
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mydev_dev * dev = filp->private_data;

if (p >= MYDEV_SIZE) {
return 0;
}
if (count > MYDEV_SIZE - p) {
count = MYDEV_SIZE - p;
}
if (copy_to_user(buf, dev->mem + p, count)) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}
return ret;
}
// Write function for mydev device driver
static ssize_t mydev_write(struct file * filp, const char __user * buf, size_t size, loff_t * ppos) {
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mydev_dev * dev = filp->private_data;

if (p >= MYDEV_SIZE) {
return 0;
}
if (count > MYDEV_SIZE - p) {
count = MYDEV_SIZE - p;
}
if (copy_from_user(dev->mem + p, buf, count)) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}
return ret;
}
// Addressing functions
static loff_t mydev_llseek(struct file * filp, loff_t offset, int flag) {
loff_t ret = 0;
switch (flag) {
case 0: /* Seek from the beginning of the file */
if (offset< 0 || (unsigned int)offset > MYDEV_SIZE) {
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1: /* Seek from the current location of the file */
if ((filp->f_pos + offset) > MYDEV_SIZE || (filp->f_pos + offset) < 0) {
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = -EINVAL;
break;
}
return ret;
}

static long mydev_ioctl(struct file * filp, unsigned int cmd, unsigned long arg) {
struct mydev_dev * dev = filp->private_data;
switch (cmd) {
case MEM_CLEAR:
memset(dev->mem, 0, MYDEV_SIZE);
printk(KERN_INFO "mydev is set to zero\n");
break;
default:
return -EINVAL;
}
return 0;
}
// open function
static int mydev_open(struct inode * inode, struct file *filp) {
filp->private_data = mydev_devp;
return 0;
}
// release function
static int mydev_release(struct inode * inode, struct file * filp) {
return 0;
}

// Define the character structure method
static const struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.llseek = mydev_llseek,
.read = mydev_read,
.write = mydev_write,
.unlocked_ioctl = mydev_ioctl,
.open = mydev_open,
.release = mydev_release,
};

// Character device setup functions
static void mydev_setup_cdev(struct mydev_dev * dev, int index) {
int err, devno = MKDEV(mydev_major, index); // Get the device structure dev_t

cdev_init(&dev->cdev, &mydev_fops); // Initialization of character devices and character device handling methods
dev->cdev.owner = THIS_MODULE; // Initialize the module to which the character device belongs
err = cdev_add(&dev->cdev, devno, 1); // Add a character device
if (err) {
printk(KERN_NOTICE "Error %d adding mydev%d", err, index);
}
}
// Module initialization
static int __init mydev_init(void) {
int ret;
dev_t devno = MKDEV(mydev_major, 0); // Get the character device structure
if (mydev_major) {
ret = register_chrdev_region(devno, 1, "mydev"); // Register this cdev device, the second parameter is the number
} else {
ret = alloc_chrdev_region(&devno, 0, 1, "mydev"); // Request character device cdev space, the second parameter is the base, the third is the number
mydev_major = MAJOR(devno); // Get the major device number
}
if (ret < 0) {
return ret;
}
mydev_devp = kzalloc(sizeof(struct mydev_dev), GFP_KERNEL); // Allocate mydev structure internal memory
if (!mydev_devp) {
ret = -ENOMEM;
goto fail_malloc; //Jump if assignment fails
}
// The differenct between major and minor equipment
mydev_setup_cdev(mydev_devp, 0);
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}

module_init(mydev_init);
// Module uninstall function
static void __exit mydev_exit(void) {
cdev_del(&mydev_devp->cdev);
kfree(mydev_devp);
unregister_chrdev_region(MKDEV(mydev_major, 0), 1);
}
module_exit(mydev_exit);

MODULE_AUTHOR("AC");
MODULE_LICENSE("GPL v2");

The Makefile program is as follows:

1
2
3
4
5
6
7
8
mydev.o

KDIR := /home/test/test_kernel/linux-5.3.2
PWD :=$(shell pwd)
default:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean

Character device validation

  • Enter the make command in the mydev directory
  • First dmesg -c
  • Then insert the module as administrator and type insmod mydev.ko in the mydev directory
  • cat /proc/devices
  • Enter characters into this character device to create a device node: mknod /dev/mydev c 230 0 //230 0 is the primary and secondary device number of the device you created; write the string: echo “hello world!”>/dev/mydev; check the input information: cat /dev/mydev; check the read/write situation: dmesg mydev

Test Code

Create the mydevTest.c test file with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <fcntl.h>
#include <stdio.h>

int main(void) {
char s[] = "OvO\nTest,TeSt,Te5t,tesTte5T,tesT\n";
char buffer[80];
int fd=open("/dev/mydev", O_RDWR); // Open mydev device, fd returns a number greater than 2 then success, O_RDWR for permission to give
if (fd >= 0) {
write(fd, s, sizeof(s));
printf("---Write something to mydev---\n%d\n%s\n", fd, s);
close(fd);
} else {
printf("fd = %d, seems there are some error...\n", fd);
}
if (fd = open("/dev/mydev", O_RDWR) >= 0) {
read(fd, buffer, sizeof(buffer));
printf("---Read something from mydev---\n%d\n%s\n", fd, buffer);
} else {
printf("fd = %d, seems there are some error...\n", fd);
}
return 0;
}

Type gcc mydevTest.c a.out to generate a.out, then type . /a.out to run.

mydev: loading out-of-tree module taints kernel.
mydev: module verification failed: signature and/or required key missing - tainting kernel

are the very beginning of the Linux source code .config, which will be shown in this experiment but does not affect

If there is no CONFIG_MODULE_SIG=n statement in the Makefile file, then the hello.txt file will show

module verification failed: signature and/or required key missing - tainting kernel
i.e.: Module verification failed.

Author

ACce1er4t0r

Posted on

2022-03-17

Updated on

2023-04-22

Licensed under