一、字符设备驱动的特性
先搞懂它 "是什么样的驱动":
- 数据访问方式 :按字节流顺序读写(像水管流水,从头到尾依次读 / 写),典型设备:串口、键盘、LED、串口屏。
- 无缓存 / 低缓存:数据 "来了就处理",实时性强(比如键盘按一下就立刻响应),和 "块设备(硬盘)" 的 "先存缓存再批量处理" 区别明显。
- 设备文件映射 :用户空间通过设备文件(比如
/dev/ttyS0) 访问,内核通过struct file_operations把文件操作映射到硬件操作。
二、字符设备的 "创建 + 识别 + 注册" 步骤
内核要管理字符设备,得完成这 4 步(相当于给设备 "上户口"):
1. 准备 "设备号"(内核识别设备的身份证)
设备号分两部分:
- 主设备号:标识 "设备类型"(比如所有串口的主设备号可能是 4);
- 次设备号 :标识 "同类型下的具体设备"(比如
/dev/ttyS0次设备号是 0,/dev/ttyS1是 1)。
获取设备号有 2 种方式:
// 方式1:静态分配(需提前查系统未用的号,容易冲突)
dev_t dev_num = MKDEV(主设备号, 次设备号); // 比如 MKDEV(200, 0)
register_chrdev_region(dev_num, 1, "my_dev"); // 注册1个设备
// 方式2:动态分配(推荐,内核自动分配未用的号)
alloc_chrdev_region(&dev_num, 0, 1, "my_dev"); // 次设备号从0开始,分配1个
2. 初始化 "字符设备结构体(cdev)"
struct cdev 是内核管理字符设备的核心结构体,需要把它和你的驱动逻辑绑定:
struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops); // 绑定"操作函数集(my_fops就是file_operations)"
my_cdev.owner = THIS_MODULE; // 标记驱动所属模块,防止模块被意外卸载
3. 把设备 "注册到内核"
让内核知道这个设备存在:
cdev_add(&my_cdev, dev_num, 1); // 把cdev添加到内核的字符设备链表
4. 创建设备文件(用户空间访问的入口)
内核注册完设备后,用户空间还需要一个 "访问入口"------ 设备文件,有 2 种方式:
-
手动创建 :终端执行
mknod /dev/my_dev c 主设备号 次设备号(c表示字符设备); -
自动创建 :用
class_create+device_create(驱动加载时自动在/dev生成文件):// 1. 创建设备类(会在/sys/class下生成目录)
struct class *my_class = class_create(THIS_MODULE, "my_class");
// 2. 创建设备文件(自动在/dev下生成my_dev)
device_create(my_class, NULL, dev_num, NULL, "my_dev");
三、设备文件方法 + struct file_operations 详解
用户空间用open/read/write等系统调用访问设备文件时,内核会转发到 struct file_operations 里的对应函数 ------ 这是 "用户空间和硬件交互的桥梁"。
1. struct file_operations 核心成员(对应系统调用)
| 用户空间系统调用 | file_operations 成员函数 | 作用说明 |
|---|---|---|
open("/dev/my_dev") |
int (*open)(struct inode *inode, struct file *filp) |
打开设备时执行(比如初始化硬件、记录设备状态) |
read(fd, buf, size) |
ssize_t (*read)(struct file *filp, char __user *buf, size_t count, loff_t *pos) |
从设备读数据到用户空间(必须用copy_to_user传数据,不能直接写__user 指针) |
write(fd, buf, size) |
ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *pos) |
从用户空间写数据到设备(必须用copy_from_user读数据) |
close(fd) |
int (*release)(struct inode *inode, struct file *filp) |
关闭设备时执行(比如释放硬件资源、重置状态) |
lseek(fd, off, SEEK_SET) |
loff_t (*llseek)(struct file *filp, loff_t off, int whence) |
调整文件读写指针(字符设备可选实现) |
2. 写一个最小的 file_operations 示例
// 定义read函数:用户读时返回"hello char dev"
ssize_t my_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
char kernel_buf[] = "hello char dev";
int len = strlen(kernel_buf);
// 把内核数据复制到用户空间(必须用这个函数,保证安全)
if (copy_to_user(buf, kernel_buf, len)) {
return -EFAULT; // 复制失败返回错误
}
return len; // 返回实际读取的字节数
}
// 定义open函数:打开时打印日志
int my_dev_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "my_dev opened!\n");
return 0;
}
// 组装file_operations结构体
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_dev_open,
.read = my_dev_read,
};
四、完整流程串起来(驱动加载 + 用户测试)
-
驱动加载 :编译驱动为
.ko模块,执行insmod my_dev.ko,此时/dev/my_dev会自动创建; -
用户测试:写个 C 程序访问设备文件:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main() {
char buf[100];
int fd = open("/dev/my_dev", O_RDONLY); // 打开设备
read(fd, buf, 100); // 读设备数据
printf("read from dev: %s\n", buf); // 输出"hello char dev"
close(fd);
return 0;
}