【Linux驱动】块设备驱动介绍

本文详细介绍了Linux内核中块设备驱动的工作原理,包括块设备与字符设备的区别、块设备驱动的特点和框架。块设备驱动通过VFS、映射层、通用块层和I/O调度层进行数据交互,确保高效的数据读写。文中还提供了一个模拟磁盘的块设备驱动程序示例,展示了如何实现读写操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

块设备驱动的概念

块设备和字符设备的差异

  • 块和字符是两种不同的访问设备的策略
  • 同一个设备可以同时支持块和字符两种访问策略(理论上可以,实际不会这么干)
  • 设备本身的物理特性决定了哪一种访问策略更适合
  • 块设备本身驱动层支持缓冲区,而字符设备驱动层没有缓冲
  • 块设备驱动最适合存储设备

块设备驱动的特点

  • 块设备是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,它使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区;
  • 字符设备是一个顺序的数据流设备,对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据流。它不具备缓冲区,所以对这种设备的读写是实时的;
  • 传统的机械式块设备(如硬盘、DVD)虽然可以随机访问,但是连续访问效率更高,因此块设备驱动中有排序逻辑将用户的随机访问重新调整成尽量连续访问以提升效率;
  • Nand、SD卡等随机访问效率等同于顺序访问;

块设备驱动框架

一个进程在某个磁盘文件上发出一个 read() 系统调用,内核对进程请求回应的一般步骤:

1、read() 调用一个适当的 VFS 函数,将文件描述符和文件内的偏移量传递给它。
虚拟文件系统位于块设备处理体系结构的上层,提供一个通用的文件系统模型,Linux 支持的所有系统均采用该模型。
2、VFS函数确定所请求的数据是否已经存在,如有必要,它决定如何执行 read 操作。
有时候没有必要访问磁盘上的数据,因为内核将大多数最近从快速设备读出或写入其中的数据保存在 RAM 中。
3、假设内核从块设备读数据,那么它就必须确定数据的物理位置。因此,内核依赖映射层执行下面步骤: a.
内核确定该文件所在文件系统的块大小,并根据文件块的大小计算所请求数据的长度。
本质上,文件被看作拆分成许多块,因此内核确定请求数据所在的块号(文件开始位置的相对索引)。 b.
映射层调用一个具体文件系统的函数,它访问文件的磁盘节点,然后根据逻辑块号确定所请求数据在磁盘上的位置。
因为磁盘也被看作拆分成许多块,所以内核必须确定所请求数据的块对应的号。
由于一个文件可能存储子磁盘上的不连续块中,因此存放在磁盘索引节点中的数据结构将每个文件块号映射为一个逻辑块号。
4、现在内核可以对块设备发出读请求。内核利用通用块层启动 I/O 操作来传送所请求的数据。 一般,每个 I/O
操作只针对磁盘上一组连续操作的块。 由于请求的数据不必位于相邻的块中,所以通用层可能启动几次 I/O 操作。 每次 I/O 操作是由一个“块
I/O”结构描述符,它收集底层组件所需要的所有信息以满足所发出的请求。 5、通用块层下面的“I/O
调度程序”根据预先定义的内核策略将待处理的 I/O 数据传送请求进行归类。 调度程序的作用是把物理介质上相邻的数据请求聚集在一起。
6、最后,块设备驱动程序向磁盘控制器的硬件接口发出适当的命令,从而进行实际的数据传送。

在这里插入图片描述

  1. 虚拟文件系统(VFS):隐藏了各种硬件的具体细节,为用户操作不同的硬件提供了一个统一的接口(open、close、write和read的函数API)。其基于不同的文件系统格式,比如EXT,FAT等;
  2. 映射层(mapping layer):这一层主要用于确定文件系统的block size,然后计算所请求的数据包含多少个block。同时调用具体文件系统函数来访问文件的inode,确定所请求的数据在磁盘上面的逻辑地址。(由于一个文件可能存储子磁盘上的不连续块中,因此存放在磁盘索引节点中的数据结构将每个文件块号映射为一个逻辑块号);
  3. 通用块层:针对所有不同类型的块设备,封装出块设备操作的各种接口。该层屏蔽了不同硬件的差异,全都是软件抽象。且对上兼容不同的VFS;
  4. IO调度层:根据预先定义的内核策略(电梯算法)将待处理的 I/O 数据传送请求进行归类。调度程序的作用是把物理介质上相邻的数据请求聚集在一起。其涉及到如何接收用户请求并能最高效去访问硬件、磁盘中的数据(排序,排序成连续地址的操作、合并,读写操作进行分类以及合并);
  5. 块设备驱动层:真正硬件操作部分,驱动开发工程师完成,该层之上均是内核开发者完成的;

块设备相关的单位

  • 扇区(Sector):概念来自于早期磁盘,在硬盘、DVD中还有用,在Nand/SD中已经没意义了,扇区是块设备本身的特性,大小一般为512字节或512的整数倍,因为历史原因很多时候都向前兼容定义为512;
  • 块(block):扇区是硬件设备传输数据的基本单位,而块是 VFS 和文件系统传输数据的基本单位。大小为若干个扇区,常见有512B、1KB、4KB等;
  • 段(Section):概念来自于内核,是内核的内存管理中一个页或者部分页,由若干个连续为块组成;
  • 页(Page):概念来自于内核,是内核内存映射管理的基本单位。linux内核的页式内存映射名称来源于此;
    在这里插入图片描述
    块设备驱动案例(模拟磁盘)
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/wait.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/dma.h>


#define RAMBLOCK_SIZE (1024*1024)   			// 1MB,2048扇区

static struct gendisk *my_ramblock_disk;		// 磁盘设备的结构体
static struct request_queue *my_ramblock_queue;	// 等待队列
static DEFINE_SPINLOCK(my_ramblock_lock);
static int major;
static unsigned char *my_ramblock_buf;			// 虚拟块设备的内存指针


static void do_my_ramblock_request(struct request_queue *q)
{

	struct request *req;
	static int r_cnt = 0; 			//实验用,打印出驱动读与写的调度方法
	static int w_cnt = 0;
	
	req = blk_fetch_request(q);
	
	while (NULL != req)
	{
		unsigned long start = blk_rq_pos(req) *512;
		unsigned long len = blk_rq_cur_bytes(req);
		
		if(rq_data_dir(req) == READ)
		{
			// 读请求
			memcpy(req->buffer, my_ramblock_buf + start, len); 	//读操作,
			printk("do_my_ramblock-request read %d times\n", r_cnt++);
		}
		else
		{
			// 写请求
			memcpy( my_ramblock_buf+start, req->buffer, len); 	//写操作
			printk("do_my_ramblock request write %d times\n", w_cnt++);
		}

		if(!__blk_end_request_cur(req, 0)) 
		{
			req = blk_fetch_request(q);
		}
	}
}


static int blk_ioctl(struct block_device *dev, fmode_t no, unsigned cmd, unsigned long arg)
{
	return -ENOTTY;
}

static int blk_open (struct block_device *dev , fmode_t no)
{
	printk("blk mount succeed\n");
	return 0;
}
static int blk_release(struct gendisk *gd , fmode_t no)
{
	printk("blk umount succeed\n");
	return 0;
}

static const struct block_device_operations my_ramblock_fops =
{
	.owner 		= THIS_MODULE,
	.open 		= blk_open,
	.release 	= blk_release,
	.ioctl 		= blk_ioctl,
};

static int my_ramblock_init(void)
{
	major = register_blkdev(0, "my_ramblock");
	if (major < 0)
	{
		printk("fail to regiser my_ramblock\n");
		return -EBUSY;
	}
	
	// 实例化
	my_ramblock_disk = alloc_disk(1);		//次设备个数 ,分区个数 +1
	
	//分配设置请求队列,提供读写能力
	my_ramblock_queue = blk_init_queue(do_my_ramblock_request, &my_ramblock_lock);
	//设置硬盘属性 
	my_ramblock_disk->major = major;
	my_ramblock_disk->first_minor = 0;
	my_ramblock_disk->fops = &my_ramblock_fops;
	sprintf(my_ramblock_disk->disk_name, "my_ramblock");		// /dev/name
	my_ramblock_disk->queue = my_ramblock_queue;
	set_capacity(my_ramblock_disk, RAMBLOCK_SIZE / 512);
	/* 硬件相关操作 */
	my_ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);
	add_disk(my_ramblock_disk);				// 向驱动框架注册一个disk或者一个partation的接口
	
	return 0;
}


static void my_ramblock_exit(void)
{
	unregister_blkdev(major, "my_ramblock");
	del_gendisk(my_ramblock_disk);
	put_disk(my_ramblock_disk);
	blk_cleanup_queue(my_ramblock_queue);
	kfree(my_ramblock_buf); 
}

module_init(my_ramblock_init);
module_exit(my_ramblock_exit);

MODULE_LICENSE("GPL"); 

重点结构体
struct request // 对设备的每一次操作(譬如读或者写一个扇区)
struct request_queue // request队列
struct bio // 通用块层用bio来管理一个请求
struct gendisk // 表示一个磁盘设备或一个分区

如何证明块设备驱动真的工作了:
格式化:mkfs.ext2 /dev/my_ramblock
挂载: mount -t ext2 /dev/my_ramblcok /tmp

块设备驱动的编写,一般分一下几个部分:
1、块设备初始化
a、向内核注册块设备,register_blkdev,输入块设备名字,返回主设备号;
b、实例化(内存申请)一个struct gendisk类型的块设备数据结构,alloc_disk,disk用来表示一个块设备实体;
c、填充struct gendisk数据结构disk,主要是fops(对块设备各种操作的集合,mount时会调用open,umount时会调用release)、queue(用于接收调度层下发的请求,注册了真正操作读写硬件的函数);
d、向驱动框架注册一个disk或者一个partation的接口,add_disk;
2、需要实现真正操作读写硬件的函数,入参为struct request_queue *q。例子中通过memcpy模拟读写硬件(读请求,将内容(首地址+偏量)往上层buffer丢,写操作,将上层buffer拷贝到指定块设备地址空间中);
3、实现fops系列函数;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值