LINUX块设备驱动开发

2021/12/8 7:16:47

本文主要是介绍LINUX块设备驱动开发,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1. 块设备驱动程序简介

  • 块设备驱动程序提供了面向数据块的设备的访问
  • 块设备驱动一般以随机的方式传输数据,并且数据总是具有固定大小的块。
  • 典型的块设备是磁盘驱动器类设备。

块设备驱动程序简介

  • 块设备驱动接口相对复杂,字符设备接口简单。
  • 块设备驱动程序对整个系统的性能影响较大,速度和效率是设计块设备驱动要重点考虑的问题。
  • 系统中使用缓冲区与访问请求的优化管理(合并与重新排序)来提高系统性能。
  • 块设备驱动不可以通过节点直接访问。
  • 它需要通过文件系统挂载到目录下访问。当执行mount时则open。Umount时则release.

块设备和字符设备的区别

在这里插入图片描述

LINUX MTD 系统层次

在这里插入图片描述
在这里插入图片描述

2. 块设备的I/O操作特点

  • 块设备只能以块为单位接受输入和返回输出。字符设备则是以字节为单位。
  • 块设备对于I/O请求有对应的缓冲区。字符设备则直接读写。
  • 块设备可随机访问。字符设备只能顺序读写。

3. 块设备驱动的结构

3.1block_device_operations结构体

在块设备驱动中,有1个类似于字符设备驱动中file_operations结构体的block_device_operations结构体,是对块设备操作的集合:
struct block_device_operations
{
	int(*open)(struct inode *, struct file*);  //打开
	int(*release)(struct inode *, struct file*);  //释放
	int(*ioctl)(struct inode *, struct file *, unsigned, unsignedlong);  //ioctl
	int(*media_changed)(struct gendisk*);  //介质被改变
	int(*revalidate_disk)(struct gendisk*);  //使介质有效
	int(*getgeo)(struct block_device *, struct hd_geometry*);//填充驱动器信息
	struct module *owner; //模块拥有者
	……
};

分析block_device_operations结构体主要成员 :

1、打开和释放
int(*open)(struct inode *, struct file*);  //打开
int(*release)(struct inode *, struct file*);  //释放

2、I/O控制
int(*ioctl)(struct inode *, struct file *, unsigned cmd, unsigned long);
  
3、介质改变(适用于可移动的设备)
int(*media_changed)(struct gendisk*);

4、使介质有效
int(*revalidate_disk)(struct gendisk*);
   被调用来响应一个介质的改变,它给驱动一个机会来进行必要的工作一使新介质准备好。

5、获得驱动器信息
int(*getgeo)(struct block_device *, struct hd_geometry*);
    该函数根据驱动器的几何信息填充一个hd_geometry结构体, hd_geometry结构体包含磁头、扇区、柱面等信息。

6、模块指针
struct module *owner;
通常被初始化为THIS_MODULE

以上3-5这些是块设备的标准请求,由LINUX块设备层处理。

3.2gendisk结构体

在Linux内核中,使用gendisk(通用磁盘)结构体来表示1个独立的磁盘设备(或分区)

struct gendisk
{
	int major; /* 主设备号 */
	int first_minor;  /*第1个次设备号*/
	int minors; /* 最大的次设备数,如果不能分区,则为1*/
	char disk_name[32]; /* 设备名称 */
	struct block_device_operations *fops; /*块设备操作结构体*/
	struct request_queue *queue;  /*请求队列*/
	void *private_data;  /*私有数据*/
	sector_t capacity; /*扇区数,512字节为1个扇区*/
	…
}

Linux提供一组函数来操作gendisk:

1、分配gendisk
struct gendisk *alloc_disk(int minors);
参数minors是分区的数量

2、增加gendisk
void add_disk(struct gendisk *gd);
gendisk结构体被分配之后,系统还不能使用这磁盘,需要调用以上函数来注册这个磁盘设备。
特别要注意的是对add_disk()的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。

3、释放gendisk
当不再需要一个磁盘时,应当使用如下函数释放gendisk。
void del_gendisk(struct gendisk *gd);

4、设置gendisk的容量
void set_capacity(struct gendisk *disk,sector_t size);

不管物理设备的真实扇区大小是多少,内核与块设备驱动交互的扇区都是以512字节为单位。

## 3.3request 与bio结构体
Sbh(p280)
Sbh(285)
1、请求队列:struct request_queue.一个块设备对应一个请求队列。这个块设备的所有的IO请求都会加入到这个请求队列中。
2、请求: struct request。 一个等待进行的IO请求。
3、块IO:struct bio. 通常一个bio对应一个IO请求,而IO调度算法可将连续的bio合并成一个请求,所以一个请求可以包含多个bio.
4、bio_vec 数据块的操作。一个bio可以包含一个数据块或者几个数据块。可以通过下面方式对bio的成员bio_vec进行遍历:bio_for_each_segment(bvec, bio, i)

4. 块设备驱动的注册与注销

Int register_blkdev(unsigned int major,const char *name);

其中major为主设备号,如果是0,则动态分配,且返回值就是主设备号,注册失败则返回负值。 Major非0的整形数则静态注册。

int unregister_blkdev(unsigned int major,const char *name);

5. 块设备驱动的加载与卸载函数

加载函数要完成:
1、注册块设备驱动
2、分配、初始化请求队列,绑定请求队列和请求函数。
3、分配、初始化gendisk,给gendisk的major 、fops、 queue等成员赋值,最后添加gendisk.

卸载函数要完成:
1、清除请求队列。
2、删除gendisk和对gendisk的引用.
3、删除对块设备的引用,注销块设备驱动。

6. 块设备的打开与释放

简单的块设备驱动可以不提供open()和release()函数。
当一个节点引用一个块设备时, inode->i_bdev->bd_disk 包含一个指向关联 gendisk 结构体的指针。所以将gendisk的private_data赋给file的private_data, private_data同样最好是指向描述该设备的设备结构体xxx_dev的指针,如下页的代码清单:
open()和release()方法还应当设置驱动和硬件的状态,这些工作可能包括启停磁盘、加锁一个可移出设备和分配DMA缓冲等。

static int xxx_open(struct inode *inode, struct file *filp)
{
	struct xxx_dev *dev = inode->i_bdev->bd_disk->private_data;
	filp->private_data = dev; //赋值file的private_data
	...
	return 0;
}

7. 块设备驱动的ioctl函数

与字符设备驱动一样,块设备可以包含一个 ioctl()函数以提供对设备的I/O控制能力。实际上,高层的块设备层代码处理了绝大多数ioctl(),因此,具体的块设备驱动中通常不再需要实现很多ioctl命令,实现方法和字符设备的一样。

int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg) {
	switch (cmd) {
		case HDIO_GETGEO:
		  …… 
	}
}

8. 块设备驱动的I/O请求处理

块设备驱动的I/O请求处理有两种方式:
1)使用请求队列—驱动必须提供一个请求函数—用于不可随机访问的设备,如机械的磁盘
2)不使用请求队列----驱动必须提供一个制造请求函数—用于可随机访问的设备,如Ram盘 ,FLASH,SD卡等

8.1 使用请求队列,适用于机械的磁盘设备 ,

块设备驱动请求函数的原型为:

 int request(struct request_queue_t *queue );

这个函数不能由驱动自己调用,只有当内核认为是时候让驱动处理对设备的读写等操作时,它才会调用这个函数。

static int xxx_request(struct request_queue_t *queue )
{
	(1)获取队列中第一个未完成的请求
	(2)如果不是文件系统请求,直接清除 
	(3)处理这个请求,读写设备
	(4)清除此请求.
	(5)通知等待此请求的对象,此请求已经完成
}

## 8.2 不使用请求队列
驱动必须提供一个"制造请求"函数(注意:这不是请求函数哦),"制造请求"函数的原型为:

typedef int (make_request_fn) (request_queue_t* q, struct bio* bio);

第一个参数是“请求队列”,其所需操作的请求对应的块就是bio. bio表示一个或多个要传送的缓冲区.
此函数或直接进行传输,或将请求重定向给其他设备.
在处理完成之后,应使用bio_endio()通知处理结束.

void bio_endio(struct bio* bio, unsigned int byetes, int error);

bytes是已经传送的字节数。如不能完成请求,将错误码赋给error参数.此函数无论处理IO成功与否都返回0,如果返回非零值,则bio将再次被提交。

static int xxx_make_request(request_queue_t* q, struct bio* bio)
{
     1)获得这个bio请求在块设备内存中的起始位置 
      2)开始遍历这个bio中的每个bio_vec 并且映射其虚拟地址
      3)判断bio请求处理的方向 ,并根据方向进行读写
      4)处理完每一个bio_vec都应把kmap映射的地址取消掉 
      5)处理完后报告处理结束 
}

# 总结
块设备的I/O操作方式与字符设备存在较大的不同,因而引入了request_queue、request、bio等一系列数据结构。在整个块设备的I/O操作中,贯穿于始终的就是“请求”,字符设备的I/O操作则是直接进行不绕弯,块设备的I/O操作会排队和整合。驱动的任务是处理请求,对请求的排队和整合由I/O调度算法解决,因此,块设备驱动的核心就是请求处理函数或“制造请求”函数。尽管在块设备驱动中仍然存在block_device_operations结构体及其成员函数,但其不再包含读写一类的成员函数,而只是包含打开、释放及I/O控制等与具体读写无关的函数。块设备驱动的结构相当复杂的,但幸运的是,块设备不像字符设备那么包罗万象,它通常就是存储设备,而且驱动的主体已经由Linux内核提供,针对一个特定的硬件系统,驱动工程师所涉及到的工作往往只是编写少量的与硬件直接交互的代码。



这篇关于LINUX块设备驱动开发的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程