三、linux字符驱动详解
在上一节完成NFS开发环境的搭建后,本节将探讨Linux字符设备驱动的开发。字符设备驱动作为Linux内核的重要组成部分,主要负责管理与字符设备(如串口、键盘等)的交互,并为用户空间程序提供统一的读写操作接口。
驱动代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#define DEVICE_NAME "hello_chrdev"
#define BUFFER_SIZE 100
// 设备结构体
typedef struct {
char buffer[BUFFER_SIZE];
struct class *class;
struct device *device;
dev_t dev_num;
struct cdev cdev;
} HelloDevice;
static HelloDevice hello_dev;
// 打开设备
static int hello_open(struct inode *inode, struct file *filp) {
printk(KERN_INFO "Hello device opened\n");
return 0;
}
// 读取设备
static ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
size_t len = strlen(hello_dev.buffer);
if (*f_pos >= len) {
return 0;
}
if (count > len - *f_pos) {
count = len - *f_pos;
}
if (copy_to_user(buf, hello_dev.buffer + *f_pos, count)) {
return -EFAULT;
}
*f_pos += count;
return count;
}
// 写入设备
static ssize_t hello_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
if (count > BUFFER_SIZE - 1) {
count = BUFFER_SIZE - 1;
}
if (copy_from_user(hello_dev.buffer, buf, count)) {
return -EFAULT;
}
hello_dev.buffer[count] = '\0';
*f_pos += count;
return count;
}
// 关闭设备
static int hello_release(struct inode *inode, struct file *filp) {
printk(KERN_INFO "Hello device closed\n");
return 0;
}
// 文件操作结构体
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
};
// 模块初始化函数
static int __init hello_init(void) {
int ret;
// 分配设备号
ret = alloc_chrdev_region(&hello_dev.dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate character device number\n");
return ret;
}
// 创建类
hello_dev.class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(hello_dev.class)) {
unregister_chrdev_region(hello_dev.dev_num, 1);
printk(KERN_ERR "Failed to create class\n");
return PTR_ERR(hello_dev.class);
}
// 创建设备
hello_dev.device = device_create(hello_dev.class, NULL, hello_dev.dev_num, NULL, DEVICE_NAME);
if (IS_ERR(hello_dev.device)) {
class_destroy(hello_dev.class);
unregister_chrdev_region(hello_dev.dev_num, 1);
printk(KERN_ERR "Failed to create device\n");
return PTR_ERR(hello_dev.device);
}
// 初始化 cdev 结构体
cdev_init(&hello_dev.cdev, &hello_fops);
hello_dev.cdev.owner = THIS_MODULE;
// 添加字符设备到系统
ret = cdev_add(&hello_dev.cdev, hello_dev.dev_num, 1);
if (ret < 0) {
device_destroy(hello_dev.class, hello_dev.dev_num);
class_destroy(hello_dev.class);
unregister_chrdev_region(hello_dev.dev_num, 1);
printk(KERN_ERR "Failed to add character device\n");
return ret;
}
printk(KERN_INFO "Hello device initialized. Major: %d, Minor: %d\n", MAJOR(hello_dev.dev_num), MINOR(hello_dev.dev_num));
return 0;
}
// 模块卸载函数
static void __exit hello_exit(void) {
cdev_del(&hello_dev.cdev);
device_destroy(hello_dev.class, hello_dev.dev_num);
class_destroy(hello_dev.class);
unregister_chrdev_region(hello_dev.dev_num, 1);
printk(KERN_INFO "Hello device removed\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello world character device driver");
函数接口详解
1. 模块初始化与退出相关函数
alloc_chrdev_region
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
- 功能:动态分配一组连续的字符设备号。
- 参数
dev
:用于存储分配到的设备号。baseminor
:起始的次设备号。count
:要分配的设备号数量。name
:设备的名称,用于在/proc/devices
中显示。
- 返回值:成功返回 0,失败返回负数错误码。
class_create
struct class *class_create(struct module *owner, const char *name);
- 功能:在
/sys/class
目录下创建一个设备类。 - 参数
owner
:指向模块的指针,通常为THIS_MODULE
。name
:类的名称。
- 返回值:成功返回指向
struct class
的指针,失败返回错误指针。
device_create
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
- 功能:在
/sys/class/<class_name>
目录下创建设备节点,并在/dev
目录下创建对应的设备文件。 - 参数
class
:指向设备类的指针。parent
:父设备指针,通常为NULL
。devt
:设备号。drvdata
:设备驱动数据,通常为NULL
。fmt
:设备名称的格式化字符串。
- 返回值:成功返回指向
struct device
的指针,失败返回错误指针。
cdev_init
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
- 功能:初始化字符设备结构体
struct cdev
,并关联文件操作结构体struct file_operations
。 - 参数
cdev
:指向struct cdev
的指针。fops
:指向struct file_operations
的指针。
cdev_add
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
- 功能:将字符设备添加到内核中。
- 参数
p
:指向struct cdev
的指针。dev
:设备号。count
:设备数量。
- 返回值:成功返回 0,失败返回负数错误码。
module_init
和 module_exit
module_init(hello_init);
module_exit(hello_exit);
- 功能:分别指定模块加载和卸载时调用的函数。
2. 文件操作相关函数
-
在 Linux 内核中,
struct file_operations
结构体是字符设备驱动与用户空间进行交互的关键桥梁,其中open
、read
、write
和release
是比较常用的操作函数。struct file_operations` 结构体中相关成员介绍
open
函数int (*open) (struct inode *inode, struct file *filp);
- 功能:当用户空间使用
open()
系统调用打开设备文件时,内核会调用驱动中注册的open
函数。该函数通常用于执行设备的初始化操作,如分配资源、检查设备状态等。 - 参数
struct inode *inode
:指向文件对应的索引节点,包含了文件的元信息,如文件类型、权限等。struct file *filp
:指向文件对象,代表了一个打开的文件实例,包含了文件的当前状态、偏移量等信息。
- 返回值:成功时返回 0,失败时返回负数错误码。
read
函数ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
- 功能:当用户空间使用
read()
系统调用从设备文件读取数据时,内核会调用驱动中的read
函数。该函数负责将设备中的数据复制到用户空间的缓冲区。 - 参数
struct file *filp
:指向文件对象。char __user *buf
:用户空间的缓冲区指针,用于存储从设备读取的数据。size_t count
:用户请求读取的字节数。loff_t *f_pos
:文件的当前偏移量指针,可通过修改该指针来更新文件的读写位置。
- 返回值:成功时返回实际读取的字节数,返回 0 表示已到达文件末尾,失败时返回负数错误码。
write
函数ssize_t (*write) (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
- 功能:当用户空间使用
write()
系统调用向设备文件写入数据时,内核会调用驱动中的write
函数。该函数负责将用户空间缓冲区中的数据复制到设备中。 - 参数
struct file *filp
:指向文件对象。const char __user *buf
:用户空间的缓冲区指针,包含了要写入设备的数据。size_t count
:用户请求写入的字节数。loff_t *f_pos
:文件的当前偏移量指针。
- 返回值:成功时返回实际写入的字节数,失败时返回负数错误码。
release
函数int (*release) (struct inode *inode, struct file *filp);
- 功能:当用户空间使用
close()
系统调用关闭设备文件时,内核会调用驱动中的release
函数。该函数通常用于执行设备的清理操作,如释放资源、关闭设备等。 - 参数
struct inode *inode
:指向文件对应的索引节点。struct file *filp
:指向文件对象。
- 返回值:成功时返回 0,失败时返回负数错误码。
- 功能:当用户空间使用
3. 模块卸载相关函数
cdev_del
void cdev_del(struct cdev *p);
- 功能:从内核中移除字符设备。
- 参数
p
:指向struct cdev
的指针。
device_destroy
void device_destroy(struct class *class, dev_t devt);
- 功能:销毁
/sys/class/<class_name>
目录下的设备节点和/dev
目录下的设备文件。 - 参数
class
:指向设备类的指针。devt
:设备号。
class_destroy
void class_destroy(struct class *cls);
- 功能:销毁
/sys/class
目录下的设备类。 - 参数
cls
:指向struct class
的指针。
unregister_chrdev_region
void unregister_chrdev_region(dev_t from, unsigned count);
- 功能:释放之前分配的字符设备号。
- 参数
from
:起始的设备号。count
:要释放的设备号数量。
编译和测试
编写 Makefile
obj-m += helloworld.o
KDIR := linux-5.15.18/
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
将 linux-5.15.18/ 替换为实际的 Linux 5.15.18 内核源码路径。
编译驱动
在终端中执行 make
命令编译驱动模块。
测试驱动
在 QEMU 终端中:
- 使用
insmod helloworld.ko
加载驱动模块。 - 使用 echo “Hello World” > /dev/helloworld 向设备写入数据。
- 使用 cat /dev/helloworld 从设备读取数据。
- 使用
rmmod hello_chrdev.ko
卸载驱动模块。