基于Linux字符驱动总结

简介

字符设备驱动是用于管理字符设备的驱动程序,字符设备通常是通过顺序访问的,即一个字符一个字符地读取或写入。这类设备通常没有特定的寻址机制(区别于块驱动),设备的操作是直接通过文件描述符(如/dev下的文件)进行控制。

这里另外补充一下PCIE驱动:PCIE驱动不属于字符驱动或者块驱动,是一种单独的驱动类型,这类驱动主要用于高速场景下,相对更加复杂。

在Linux环境下开发驱动首先需要安装相关的依赖:

1
sudo apt install build-essenstial

驱动结构

这里设定一些前提条件:

  • 当载入驱动时,自动在/dev目录下生成相关的节点,无需手动载入;卸载驱动时同理自动卸载节点
  • 基于的内核版本高于5.x,某些函数会有一些区别
  • 这里不考虑Linux本身的安全机制,能够直接进行加载/卸载,无须进行签名
  • 这里只考虑三大功能,read/write/Ioctl

hello-world为例,驱动代码如下:

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h> // copy_to_user, copy_from_user
#include <linux/init.h>
#include <linux/device.h>
#include <linux/ioctl.h>

#define DEVICE_NAME "hello_chrdev"
#define HELLO_MSG_LEN 32

static char message[HELLO_MSG_LEN] = "Hello, World from Kernel!";
static int major_num;
static struct class *hello_class;
static struct cdev hello_cdev;
static dev_t dev_num;

#define HELLO_IOC_MAGIC 'k'
#define HELLO_IOCSMSG _IOW(HELLO_IOC_MAGIC, 1, char *)
#define HELLO_IOCGMSG _IOR(HELLO_IOC_MAGIC, 2, char *)
#define HELLO_IOC_MAXNR 2

static int hello_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "hello_chrdev: Device opened\n");
return 0;
}

static ssize_t hello_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
int bytes_read = 0;

if (*offset >= HELLO_MSG_LEN)
return 0;

bytes_read = HELLO_MSG_LEN - *offset;
if (len < bytes_read)
bytes_read = len;

if (copy_to_user(buf, message + *offset, bytes_read))
return -EFAULT;

*offset += bytes_read;
return bytes_read;
}

static ssize_t hello_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {
int bytes_written = 0;

if (*offset >= HELLO_MSG_LEN)
return 0;

if (len > HELLO_MSG_LEN - *offset)
len = HELLO_MSG_LEN - *offset;

if (copy_from_user(message + *offset, buf, len))
return -EFAULT;

*offset += len;
return len;
}

static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
char buffer[HELLO_MSG_LEN];

if (_IOC_TYPE(cmd) != HELLO_IOC_MAGIC) {
return -ENOTTY;
}

switch (cmd) {
case HELLO_IOCSMSG:
if (copy_from_user(buffer, (char __user *)arg, HELLO_MSG_LEN)) {
return -EFAULT;
}
strncpy(message, buffer, HELLO_MSG_LEN);
printk(KERN_INFO "hello_chrdev: Message updated to: %s\n", message);
break;

case HELLO_IOCGMSG:
if (copy_to_user((char __user *)arg, message, HELLO_MSG_LEN)) {
return -EFAULT;
}
printk(KERN_INFO "hello_chrdev: Message sent to user: %s\n", message);
break;

default:
return -ENOTTY;
}

return 0;
}

static int hello_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "hello_chrdev: Device closed\n");
return 0;
}

static struct file_operations fops = {
.owner = THIS_MODULE,
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
.unlocked_ioctl = hello_ioctl,
};

static int __init hello_init(void) {
int result;

result = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (result < 0) {
printk(KERN_ALERT "hello_chrdev: Failed to allocate a major number\n");
return result;
}

major_num = MAJOR(dev_num);
hello_class = class_create(DEVICE_NAME);
if (IS_ERR(hello_class)) {
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(hello_class);
}

cdev_init(&hello_cdev, &fops);
hello_cdev.owner = THIS_MODULE;
result = cdev_add(&hello_cdev, dev_num, 1);
if (result) {
class_destroy(hello_class);
unregister_chrdev_region(dev_num, 1);
return result;
}

device_create(hello_class, NULL, dev_num, NULL, DEVICE_NAME);
printk(KERN_INFO "hello_chrdev: Device registered with major number %d\n", major_num);
return 0;
}

static void __exit hello_exit(void) {
device_destroy(hello_class, dev_num);
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "hello_chrdev: Unregistered device\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver with ioctl functionality");

同时给出Makefile

1
2
3
4
5
6
7
8
9
10
obj-m := hello_chrdev.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
make -C $(KDIR) M=$(PWD) modules

clean:
make -C $(KDIR) M=$(PWD) clean

重点部分分析

init

这一部分是在调用insmod命令时运行的,主要功能是注册设备。挑几个概念着重讲一下:

  • major number
    在 Linux 内核中,Major Number 是一个 unsigned int,理论上可以支持 0 ~ 255(部分内核支持更大范围),但实际上 有一部分是被内核保留的。

    Major Number 范围 用途
    0 动态分配(由 register_chrdev(0, name, &fops) 生成)
    1 ~ 189 静态分配给系统设备(如 tty、block devices 等)
    190 ~ 238 未保留,通常可用于第三方驱动
    239 ~ 255 供私有使用(私有驱动)

    想要明确指定Major number 使用

    1
    2
    dev_t dev_num = MKDEV(240, 0);
    register_chrdev_region(dev_num, 1, "hello_chrdev");

    自动分配:

    1
    int major = register_chrdev(0, "hello_chrdev", &fops);
  • classdevice的关系:先创建class,表示大类,在创建device表示属于该类的一个设备

  • cdev是 Linux 驱动模型中的一个重要概念,它代表了一个 字符设备(Character Device) 的抽象。

    1
    2
    3
    4
    5
    6
    7
    8
    struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    dev_t dev; // 设备号 (major:minor)
    struct list_head list;
    unsigned int count;
    };

read/write

  • copy_from_user 从用户空间拷贝至内核空间
  • copy_to_user 从内核空间拷贝到用户空间

fops

把相关的函数入口放入到结构体中

1
2
3
4
5
6
7
8
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
.unlocked_ioctl = hello_ioctl,
};

这里unlocked_ioctl表示时操作无锁,需要自己加;ioctl本身是有锁的,可能会有性能的瓶颈

ioctl

ioctl(Input/Output Control)是一个系统调用,用于设备控制。在 Linux 中,ioctl 函数通常用于向设备发送特定的控制命令,或者配置设备的特性。这些操作不适合通过标准的 read 或 write 调用实现,因为它们通常涉及到设备的特殊功能或管理操作。

使用ioctl时,需要首先定义

1
2
3
4
5
6
7
8
#include <linux/ioctl.h>

#define HELLO_IOC_MAGIC 'k' // 定义一个 "magic" 数字,用来区分不同设备的命令

// 定义一个 ioctl 命令,用来设置消息
#define HELLO_IOCSMSG _IOW(HELLO_IOC_MAGIC, 1, char *) //表示写
#define HELLO_IOCGMSG _IOR(HELLO_IOC_MAGIC, 2, char *) //表示读
#define HELLO_IOC_MAXNR 2

在用户空间调用的时候,同样需要进行相同的定义。