Linux驱动开发是嵌入式系统开发中的一个重要组成部分,它直接关系到硬件设备的功能实现和性能优化。在Linux系统中,驱动开发主要分为字符设备驱动、块设备驱动和网络设备驱动三大类。本文将重点介绍字符设备和块设备的基础知识,以及它们在驱动开发中的差异和开发流程。
一、字符设备与快设备的区别
1. 数据传输方式不同
字符设备:数据传输是以字符为单位进行的,通常不需要缓冲。例如键盘、鼠标等设备,它们的输入输出操作按字节顺序进行,适用于处理少量但频繁的I/O操作。
块设备:数据传输是以固定大小的数据块为单位进行的,这些数据块通常需要缓冲。例如硬盘、NAND Flash等存储设备,它们的读写操作以一定大小的数据块为单位,适用于处理大量数据的存储和传输。
2. 驱动程序结构不同
字符设备:由字符设备驱动程序控制,可以使用文件操作的接口进行操作。字符设备驱动的主要任务是实现设备的打开、关闭、读取和写入等基本功能。
块设备:由块设备驱动程序控制,同样可以使用文件操作的接口进行操作。块设备驱动的主要任务是实现对设备的读写请求管理、缓存管理和数据一致性保证等功能。
3. 典型应用场景
字符设备:适用于需要快速响应的设备,如串口通信设备、LED指示灯等。
块设备:适用于需要高效数据处理的设备,如磁盘驱动器、固态硬盘等。
二、字符设备驱动的开发流程
字符设备驱动的开发流程通常包括以下几个步骤:
//包含必要的头文件:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
1. 定义设备号和类
每个字符设备都需要一个唯一的主设备号来标识。在内核中可以通过register_chrdev函数注册字符设备,并分配一个主设备号。
int ret = register_chrdev(0, "mydevice", &my_fops);
if (ret < 0) {
printk(KERN_ALERT "mydevice failed to register");
return ret;
}
printk(KERN_INFO "mydevice registered ");
上述代码中,my_fops 是一个指向struct file_operations结构的指针,该结构定义了设备的各种操作函数,my_fops 结构体的定义 在下面的代码中。
2. 实现文件操作函数
字符设备驱动需要实现一系列的文件操作函数,包括但不限于打开、关闭、读取和写入等。这些函数通过填充 struct file_operations 结构体来实现。
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = device_open,
.release = device_release,
.read = device_read,
.write = device_write,
};
每个函数的具体实现根据设备的特性而定。例如,device_read函数可以这样实现:
ssize_t device_read(struct file *filp, char *buffer, size_t count, loff_t *f_pos) {
// 实现读取逻辑
printk(KERN_INFO "Device read operation");
return 0; // 返回读取的字节数
}
device_open 函数可以这样实现:
// 我们先定义一个结构体,open_count 用来记录 打开次数:
struct my_device {
int open_count;
};
int device_open(struct inode *inode, struct file *filp)
{
struct my_device *dev; //定义结构体指针 dev
dev= kmalloc(sizeof(struct my_char_device), GFP_KERNEL);
if (!dev) return -ENOMEM;
/* 将私有数据设置到文件私有数据中 */
filp->private_data = dev;
/* 初始化设备 */
if (device_init(dev)) {
return -ENODEV;
}
/* 更新设备的打开次数 */
atomic_inc(&dev->open_count);
return 0;
}
参数说明:
inode
:设备的索引节点,内核用它来表示一个打开的文件。
filp
:文件结构体指针,内核用它来表示一个打开的文件描述符。
3. 编写模块初始化和退出函数
模块初始化函数用于注册设备和分配资源,而模块退出函数则负责注销设备和释放资源。这两个函数分别使用module_init和module_exit宏进行标记。
4.编译和加载驱动模块
完成上述步骤后,需要将驱动程序编译成内核模块。可以使用Makefile来自动化编译过程,假设我们的代码文件为 my_chardev.c ,执行下面的 Makefile 后 会生成 my_chardev.ko文件。
obj-m+ = my_chardev.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
然后,通过insmod命令加载驱动模块,并通过mknod命令创建设备节点。
sudo insmod my_chardev.ko
sudo mknod /dev/mychardriver c <major> <minor>
其中,<major>为主设备号,<minor>为次设备号。加载成功后,可以通过cat或echo命令测试设备的功能。
三、快设备驱动的开发流程
快设备的开发流程与字符设备类似,但在具体实现细节上有所不同。以下是快设备驱动开发的主要步骤:
//包含必要的头文件:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/blkdev.h>
#include <linux/fs.h>
1. 定义设备结构和初始化函数
块设备驱动也需要定义一个设备结构体并初始化。
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
// 更多的文件操作函数按需添加
};
2. 实现文件操作函数
虽然快设备也是通过文件操作接口进行操作,但其内部实现涉及更多的数据管理和调度机制。例如,快设备的write
函数可能需要处理写缓存和数据同步等问题。
// 这里是对应的操作函数实现
static int my_open(struct inode *inode, struct file *filp) {
// 设备打开时的操作
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
// 设备关闭时的操作
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) {
// 设备读取操作
return 0;
}
static ssize_t my_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos) {
// 设备写入操作
return 0;
}
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
// 设备控制操作
return 0;
}
3. 编写模块初始化和退出函数
与字符设备类似,快设备驱动也需要编写模块初始化和退出函数,但需要额外处理设备的注册和注销,我们先定义 gendisk 结构体如下:
//gendisk 结构体在Linux块设备驱动中用于表示一个具体的磁盘设备或其分区.
static struct gendisk *example_disk;
结构体中包含的主要成员变量如下:
- int major:设备的主设备号。
- int first_minor:起始次设备号。
- int minors:次设备数量。
- char disk_name:块设备名称。
- struct hd_struct** part:分区表信息。
- struct block_device_operations* fops:块设备操作函数。
- struct request_queue* queue:请求队列,用于管理该设备的IO请求。
- void* private_data:私有数据。
- sector_t capacity:扇区数。
编写模块初始化代码:
static int __init example_init(void) {
// 分配一个新的gendisk结构体
example_disk = alloc_disk(1);
if (!example_disk) {
printk(KERN_ERR "无法分配gendisk结构体\n");
return -ENOMEM;
}
// 设置gendisk的属性
snprintf(example_disk->disk_name, DISK_NAME_LEN, "example");
example_disk->major = 123; // 主设备号
example_disk->first_minor = 0;
example_disk->fops = &my_fops ; // 与设备关联的文件操作
set_capacity(example_disk, 1UL << 32); // 设置设备容量为2的32次方扇区
// 注册块设备
add_disk(example_disk);
return 0;
}
编写模块注销代码:
static void __exit example_exit(void) {
del_gendisk(example_disk); // 注销gendisk
put_disk(example_disk); // 释放gendisk结构体
}
模块初始化和退出函数
module_init(example_init);
module_exit(example_exit);
MODULE_LICENSE("GPL"); //声明 LICENSE 模式
4. 编译和加载驱动模块
快设备驱动的编译和加载过程与字符设备类似,上述代码文件命名为 myblkdriver.c,然后写 Makefile 文件,执行只Makefile 文件后 后生成 myblkdriver.ko 文件, 执行下面的命令进行挂载和创建设备节点。
sudo insmod myblkdriver.ko
sudo mknod /dev/myblkdriver b <major> <minor>
加载成功后,可以通过相应的命令测试设备的功能。
四、总结
字符设备和快设备在Linux驱动开发中扮演着不同的角色,各自有其独特的特点和应用场景。字符设备适用于需要快速响应的小量数据传输场景,而快设备则适用于大量数据的高效存储和传输。无论是哪种类型的设备驱动开发,都需要遵循一定的流程和技术规范。通过本文的介绍,希望读者能够对Linux驱动开发有一个更深入的理解,并在实际操作中更加得心应手。
标签:struct,驱动,VS,Linux,全解,disk,my,example,设备 From: https://blog.csdn.net/m0_63998314/article/details/145198992