十六、Linux驱动之块设备驱动
1. 基本概念
块设备是Linux三大设备之一,其驱动模型主要针对磁盘,Flash等存储类设备,块设备(blockdevice)是一种具有一定结构的随机存取设备,对这种设备的读写是按块(所以叫块设备)进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。
1.1 块设备结构
块设备由Page->Segment->Block->Sector的层次结构组成。
Page就是内存映射的最小单位;Segment就是一个Page中我们要操作的一部分,由若干个相邻的块组成;Block是逻辑上的进行数据存取的最小单位,是文件系统的抽象,逻辑块的大小是在格式化的时候确定的, 一个Block最多仅能容纳一个文件(即不存在多个文件同一个block的情况)。如果一个文件比block小,他也会占用一个block,因而block中空余的空间会浪费掉。而一个大文件,可以占多个甚至数十个成百上千万的block。Linux内核要求 Block_Size = Sector_Size * (2的n次方),并且Block_Size <= 内存的Page_Size(页大小), 如ext2 fs的block缺省是4k。若block太大,则存取小文件时,有空间浪费的问题;若block太小,则硬盘的 Block 数目会大增,而造成inode在指向block的时候的一些搜寻时间的增加,又会造成大文件读写方面的效率较差,block是VFS和文件系统传送数据的基本单位。block对应磁盘上的一个或多个相邻的扇区,而VFS将其看成是一个单一的数据单元,块设备的block的大小不是唯一的,创建一个磁盘文件系统时,管理员可以选择合适的扇区的大小,同一个磁盘的几个分区可以使用不同的块大小。此外,对块设备文件的每次读或写操作是一种"原始"访问,因为它绕过了磁盘文件系统,内核通过使用最大的块(4096)执行该操作。Linux对内存中的block会被进一步划分为Sector,Sector是硬件设备传送数据的基本单位,这个Sector就是512byte,和物理设备上的概念不一样,如果实际的设备的sector不是512byte,而是4096byte(eg SSD),那么只需要将多个内核sector对应一个设备sector即可。
1.2 块设备与字符设备的区别
作为一种存储设备,和字符设备相比,块设备有以下几种不同:
字符设备 | 块设备 |
---|---|
1byte | 块,硬件块各有不同,但是内核都使用512byte描述 |
顺序访问 | 随机访问 |
没有缓存,实时操作 | 有缓存,不是实时操作 |
一般提供接口给应用层 | 块设备一般提供接口给文件系统 |
是被用户程序调用 | 由文件系统程序调用 |
2. 块设备模型
下图是Linux中的块设备模型示意图,应用层程序有两种方式访问一个块设备:/dev和文件系统挂载点,前者和字符设备一样,通常用于配置,后者就是我们mount之后通过文件系统直接访问一个块设备了。
1. read()系统调用最终会调用一个适当的VFS函数(read()-->sys_read()-->vfs_read()),将文件描述符fd和文件内的偏移量offset传递给它。
2. VFS会判断这个SCI的处理方式,如果访问的内容已经被缓存在RAM中(磁盘高速缓存机制),就直接访问,否则从磁盘中读取。
3. 为了从物理磁盘中读取,内核依赖映射层mapping layer,即上图中的磁盘文件系统。
3.1 确定该文件所在文件系统的块的大小,并根据文件块的大小计算所请求数据的长度。本质上,文件被拆成很多块,因此内核需要确定请求数据所在的块。
3.2 映射层调用一个具体的文件系统的函数,这个层的函数会访问文件的磁盘节点,然后根据逻辑块号确定所请求数据在磁盘上的位置。
4. 内核利用通用块层(generic block layer)启动IO操作来传达所请求的数据,通常,一个IO操作只针对磁盘上一组连续的块。
5. IO调度程序根据预先定义的内核策略将待处理的IO进行重排和合并。
6. 块设备驱动程序向磁盘控制器硬件接口发送适当的指令,进行实际的数据操作。
3. 块设备驱动调用过程
当我们要写一个很小的数据到txt文件某个位置时,由于块设备写的数据是按扇区为单位,但又不能破坏txt文件里其它位置,那么就引入了一个“缓存区”,将所有数据读到缓存区里,然后修改缓存数据,再将整个数据放入txt文件对应的某个扇区中,当我们对txt文件多次写入很小的数据的话,那么就会重复不断地对扇区读出,写入,这样会浪费很多时间在读/写硬盘上,所以内核提供了一个队列的机制,再没有关闭txt文件之前,会将读写请求进行优化,排序,合并等操作,从而提高访问硬盘的效率(IO调度)。
IO调度其实就是电梯算法。我们知道,磁盘是的读写是通过机械性的移动磁头来实现读写的,理论上磁盘设备满足块设备的随机读写的要求,但是出于节约磁盘,提高效率的考虑,我们希望当磁头处于某一个位置的时候,一起将最近需要写在附近的数据写入,而不是这写一下,那写一下然后再回来,IO调度就是将上层发下来的IO请求的顺序进行重新排序以及对多个请求进行合并,这样就可以实现上述的提高效率、节约磁盘的目的。这种解决问题的思路使用电梯算法,一个运行中的电梯,一个人20楼->1楼,另外一个人15->5楼,电梯不会先将第一个人送到1楼再去15楼接第二个人将其送到5楼,而是从20楼下来,到15楼的时候停下接人,到5楼将第二个放下,最后到达1楼,一句话,电梯算法最终服务的优先顺序并不按照按按钮的先后顺序。
当我们对一个*.txt写入数据时,文件系统会转换为对块设备上扇区的访问,也就是调用ll_rw_block()函数,从这个函数开始就进入了设备层。
3.1 ll_rw_block()函数
ll_rw_block()函数的部分代码如下(位于/fs/buffer.c):
/* * rw:读写标志位* nr:bhs[]长度* bhs[]:要读写的数据数组 */
void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
{int i; for (i = 0; i < nr; i++) {struct buffer_head *bh = bhs[i]; //获取nr个buffer_head... ...if (rw == WRITE || rw == SWRITE) {if (test_clear_buffer_dirty(bh)) {... ...submit_bh(WRITE, bh); //提交WRITE写标志的buffer_head continue;}}else {if (!buffer_uptodate(bh)) {... ...submit_bh(rw, bh); //提交其它标志的buffer_headcontinue;}}unlock_buffer(bh); }
}
其中buffer_head结构体定义如下:
struct buffer_head {unsigned long b_state; //缓冲区状态标志 struct buffer_head *b_this_page; //页面中的缓冲区 struct page *b_page; //存储缓冲区位于哪个页面sector_t b_blocknr; //逻辑块号size_t b_size; //块的大小char *b_data; //页面中的缓冲区struct block_device *b_bdev; //块设备,来表示一个独立的磁盘设备bh_end_io_t *b_end_io; //I/O完成方法void *b_private; //完成方法数据struct list_head b_assoc_buffers; //相关映射链表/* mapping this buffer is associated with */struct address_space *b_assoc_map; atomic_t b_count; //缓冲区使用计数
};
submit_bh()函数就是通过bh来构造bio,然后调用submit_bio()提交bio,submit_bio()函数如下:
void submit_bio(int rw, struct bio *bio)
{... ...generic_make_request(bio);
}
最终调用generic_make_request(),把bio数据提交到相应块设备的请求队列中,generic_make_request()函数主要是实现对bio的提交处理。
3.2 generic_make_request()函数
generic_make_request()函数如下:
void generic_make_request(struct bio *bio)
{if (current->bio_tail) { // current->bio_tail不为空,表示有bio正在提交*(current->bio_tail) = bio; //将当前的bio放到之前的bio->bi_next里面bio->bi_next = NULL; //更新bio->bi_next=0;current->bio_tail = &bio->bi_next; //然后将当前的bio->bi_next放到current->bio_tail里,使下次的bio就会放到当前bio->bi_next里面了return; }BUG_ON(bio->bi_next);do {current->bio_list = bio->bi_next;if (bio->bi_next == NULL)current->bio_tail = ¤t->bio_list;elsebio->bi_next = NULL;__generic_make_request(bio); //调用__generic_make_request()提交biobio = current->bio_list;} while (bio);current->bio_tail = NULL; /* deactivate */
}
__generic_make_request()首先由bio对应的block_device获取申请队列q,然后要检查对应的设备是不是分区,如果是分区的话要将扇区地址进行重新计算,最后调用q的成员函数make_request_fn完成bio的递交。__generic_make_request()函数如下:
static inline void __generic_make_request(struct bio *bio)
{request_queue_t *q; int ret; ... ...do{q = bdev_get_queue(bio->bi_bdev); //通过bio->bi_bdev获取申请队列q... ...ret = q->make_request_fn(q, bio); //提交申请队列q和bio}while (ret);
}
在内核中搜索make_request_fn,调用如下:
blk_init_queue_node()
blk_queue_make_request(q, __make_request)
make_request_fn
最终q->make_request_fn()执行的是__make_request()函数。
3.3 __make_request()函数
static int __make_request(request_queue_t *q, struct bio *bio)
{struct request *req; //块设备本身的队列... ...//将之前的申请队列q和传入的bio,通过排序,合并在本身的req队列中el_ret = elv_merge(q, &req, bio);... ...init_request_from_bio(req, bio); //合并失败,单独将bio放入req队列add_request(q, req); //单独将之前的申请队列q放入req队列... ...__generic_unplug_device(q); //执行申请队列的处理函数 }
上面的elv_merge()函数,就是之前所说的电梯算法函数。
3.4 总结
读写块设备在内核中的调用过程如下:
ll_rw_block() //进入内核中设备层,提交buff_head缓存区结构体
submit_bio() //用buff_head构造bio,提交bio
submit_bio() //把提交上来的bio提交的到相应块设备的请求队列中
generic_make_request() //对bio进行提交处理
__generic_make_request() //获取等待队列q,提交bio
__make_request() //合并q和bio,执行队列
elv_merge() //使用电梯算法合并q和bio
__generic_unplug_device() //执行队列
q->request_fn //调用队列处理函数
这个队列处理函数显然就是要我们驱动去实现的,参考内核自带的块设备驱动程序drivers/block/xd.c,使用队列如下:
static struct request_queue *xd_queue; //定义一个申请队列xd_queuexd_queue = blk_init_queue(do_xd_request, &xd_lock); //分配一个申请队列
队列处理函数如下:
static void do_xd_request (request_queue_t * q)
{struct request *req; if (xdc_busy)return;while ((req = elv_next_request(q)) != NULL) //(1)while获取申请队列中的需要处理的申请{int res = 0;... ...for (retry = 0; (retry < XD_RETRIES) && !res; retry++) res = xd_readwrite(rw, disk, req->buffer, block, count);//将获取申请req的buffer成员 读写到disk扇区中,当读写失败返回0,成功返回1end_request(req, res); //申请队列中的的申请已处理结束,当res=0,表示读写失败}
}
最终申请队列q调用驱动来对扇区读写。
4. 块设备驱动程序实现流程
分析内核块设备驱动drivers/block/xd.c如下:
static DEFINE_SPINLOCK(xd_lock); //定义一个自旋锁,用到申请队列中
static struct request_queue *xd_queue; //定义一个申请队列xd_queuestatic int __init xd_init(void) //入口函数
{if (register_blkdev(XT_DISK_MAJOR, "xd")) //1.创建一个块设备,保存在/proc/devices中goto out1;xd_queue = blk_init_queue(do_xd_request, &xd_lock); //2.分配一个申请队列,后面会赋给gendisk结构体的queue成员... ...for (i = 0; i < xd_drives; i++) { ... ...struct gendisk *disk = alloc_disk(64); //3.分配一个gendisk结构体, 64:次设备号个数,也称为分区个数/* 4.接下来设置gendisk结构体 */disk->major = XT_DISK_MAJOR; //设置主设备号disk->first_minor = i<<6; //设置次设备号disk->fops = &xd_fops; //设置块设备驱动的操作函数disk->queue = xd_queue; //设置queue申请队列,用于管理该设备IO申请队列... ...xd_gendisk[i] = disk;}... ...for (i = 0; i < xd_drives; i++)add_disk(xd_gendisk[i]); //5.注册gendisk结构体
}
4.1 重点数据结构gendisk
Linux内核使用gendisk对象描述一个系统的中的块设备,类似于Windows系统中的磁盘分区和物理磁盘的关系,OS眼中的磁盘都是逻辑磁盘,也就是一个磁盘分区,一个物理磁盘可以对应多个磁盘分区,在Linux中,这个gendisk就是用来描述一个逻辑磁盘,也就是一个磁盘分区。
struct gendisk {int major; /*设备主设备号*/int first_minor; /*起始次设备号*/int minors; /*次设备号的数量,也称为分区数量,如果改值为1,表示无法分区*/char disk_name[32]; /*设备名称*/struct hd_struct **part; /*分区表的信息*/int part_uevent_suppress;struct block_device_operations *fops; /*块设备操作集合 */struct request_queue *queue; /*申请队列,用于管理该设备IO申请队列的指针*/void *private_data; /*私有数据*/sector_t capacity; /*扇区数,512字节为1个扇区,描述设备容量*/....
};
4.2 块设备驱动程序流程
1. 创建一个块设备
2. 分配一个申请队列
3. 分配一个gendisk结构体
4. 设置gendisk结构体的成员
5. 注册gendisk结构体
5. 编写代码
接下来仿造内核块设备驱动drivers/block/xd.c编写自己的驱动程序。
5.1 代码框架
5.1.1 入口函数中
1. 使用register_blkdev()创建一个块设备
2. blk_init_queue()使用分配一个申请队列,并赋申请队列处理函数
3. 使用alloc_disk()分配一个gendisk结构体 4
4. 设置gendisk结构体的成员
4.1 设置成员参数(major、first_minor、disk_name、fops)
4.2 设置queue成员,等于之前分配的申请队列
4.3 通过set_capacity()设置capacity成员,等于扇区数
4.4 使用kzalloc()来获取缓存地址,用做扇区
4.5 使用add_disk()注册gendisk结构体
5.1.2 申请队列的处理函数中
1. while循环使用elv_next_request()获取申请队列中每个未处理的申请
2. 使用rq_data_dir()来获取每个申请的读写命令标志,为 0(READ)表示读, 为1(WRITE)表示写
3. 使用memcp()来读或者写扇区(缓存)
4. 使用end_request()来结束获取的每个申请
5.1.3 出口函数中
1. 使用put_disk()和del_gendisk()来注销,释放gendisk结构体
2. 使用kfree()释放磁盘扇区缓存
3. 使用blk_cleanup_queue()清除内存中的申请队列
4. 使用unregister_blkdev()卸载块设备
5.2 编写代码
驱动程序ramblock.c代码如下:
/*参考xd.c头文件*/
#include <linux/module.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>static struct gendisk *ramblock_disk;
static request_queue_t *ramblock_queue;
static DEFINE_SPINLOCK(ramblock_lock);
static int major;
#define RAMBLOCK_SIZE (1024*1024)
static unsigned char *ramblock_buf;static int fd_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{int drive = MINOR(bdev->bd_dev) & 3;/*容量=heads*cylinders*sectors*512字节*/geo->heads = 2; //磁头,有多少面,假设2geo->cylinders = 32; //柱面,有多少环,假设32geo->sectors = RAMBLOCK_SIZE/2/32/512; //按公式算出来扇区数return 0;
}static struct block_device_operations ramblock_fops = {.owner = THIS_MODULE,.getgeo = fd_getgeo,
};
static void do_ramblock_request (request_queue_t * q)
{struct request *req;while ((req = elv_next_request(q)) != NULL) { //以电梯调度算法执行下一请求/*数据传输3要素,源,目的,长度*//*源*/unsigned long offset = req->sector *512; //偏移值/*目的*///req->buffer/*长度*/unsigned long len = req->current_nr_sectors *512; //长度if(rq_data_dir(req)==READ) //如果是读数据,就把ramblock_buf+offset里的数据拷贝到req->buffer{memcpy(req->buffer,ramblock_buf+offset,len);}else{memcpy(ramblock_buf+offset,req->buffer,len);}end_request(req, 1); //结束获取的申请}
}static int ramblock_init(void)
{/* 1. 创建一个块设备 */major=register_blkdev(0, "ramblock");/* 2. 分配一个gendisk结构体 */ramblock_disk=alloc_disk(16); //不分区/* 3. 分配设置队列(用于填充gendisk) */ramblock_queue=blk_init_queue(do_ramblock_request, &ramblock_lock);/* 4. 设置gendisk结构体的成员 */ramblock_disk->major = major;ramblock_disk->first_minor = 0;sprintf(ramblock_disk->disk_name, "ramblock");ramblock_disk->fops = &ramblock_fops; ramblock_disk->queue = ramblock_queue;set_capacity(ramblock_disk, RAMBLOCK_SIZE/512);/* 5. 分配缓存地址,用做扇区 */ramblock_buf=kzalloc(RAMBLOCK_SIZE,GFP_KERNEL);/* 6. 注册gendisk结构体 */add_disk(ramblock_disk);return 0;
}static void ramblock_exit(void)
{del_gendisk(ramblock_disk);put_disk(ramblock_disk);kfree(ramblock_buf);blk_cleanup_queue(ramblock_queue);unregister_blkdev(major,"ramblock");
}
module_init(ramblock_init);
module_exit(ramblock_exit);
MODULE_LICENSE("GPL");
Makefile代码如下:
KERN_DIR = /work/system/linux-2.6.22.6 //内核目录all:make -C $(KERN_DIR) M=`pwd` modules clean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.orderobj-m += ramblock.o
6. 测试
内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10
1. 首先编译驱动程序。在驱动文件目录下执行:
make
2. 安装驱动,在开发板上执行:
insmod ramblock.ko
3. 将memblock块设备格式化为dos磁盘类型,在开发板上执行:
mkdosfs /dev/ramblock (使用韦东山老师的文件系统fs_mini_mdev/sbin中将mkdosfs拷贝到/first_fs/usr/sbin目录下就可以用mkdosfs工具了)
4. 挂载块设备到/tmp目录下,在开发板上执行:
mount /dev/ramblock /tmp/
5. 接下来在/tmp目录下创建编辑一个文件,最终都会保存在/dev/ memblock块设备里面。
cd /tmp
vi lzh.txt (随便输入内容保存)
cd /
umount /tmp/ (不能在/tmp目录里进行卸载。此时重新挂接,刚才新建的lzh.txt依然存在)
cat /dev/ramblock > /ramblock.bin(将/dev/ramblock磁盘内容存到bin文件里,也就是说刚才新建的lzh.txt会存入bin文件内)
6. 使用” -o loop”将ramblock.bin文件模拟成磁盘挂接到/mnt。ramblock.bin必须放在文件系统根目录里并在该目录执行该命令。注意:如果该磁盘没有格式化分区,或模拟磁盘的文件生成前没有格式化,会挂接不成功,提示“mount: you must specify the filesystem type”),如下:
sudo mount -o loop ramblock.bin /mnt
mnt目录下就有ramblock.bin文件里的内容了,如下:
7. 磁盘分区
7.1 重装驱动
7.2 fdisk /dev/ramblock (磁盘分区命令)
7.3 依次执行以下命令:
m (查看帮助)
n (添加分区)
p (主分区)
1 (第一个主分区)
1 (最外面的几个柱面作为第一个主分区开始,这里设为1)
5 (最外面的几个柱面作为第一个主分区结束,这里设为5)
重复7.2步骤再添加第二个主分区,完成后输入p可以查看分区,输入w将配置写入分区表(该磁盘里的第一个扇区))。
8. 查看效果,执行如下:
ls /dev/ramblock* -l
还可以分别进行磁盘格式化:
mkdosfs /dev/ramblock1
mkdosfs /dev/rambloc2
分别挂接磁盘:
mount /dev/ramblock1 /tmp/
mount /dev/ramblock2 /tmp/
十六、Linux驱动之块设备驱动相关推荐
- linux中流设备_[快速上手Linux设备驱动]之块设备驱动流程详解一
[快速上手Linux设备驱动]之块设备驱动流程详解一 walfred已经在[快速上手Linux设备驱动]之我看字符设备驱动一 文中详细讲解了linux下字符设备驱动,并紧接着用四篇文章描述了Linux ...
- Linux驱动开发|块设备驱动
块设备驱动 块设备驱动是 Linux 三大驱动类型之一,块设备驱动比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,下面介绍块设备驱动框架及使用 一.块设备介绍 块设备是针对存储设备的 ...
- linux内核的块设备驱动框架详解
1.块设备和字符设备的差异 (1)块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位.大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作; (2)块设备对于 I/O 请求 ...
- Linux驱动开发---块设备驱动
块设备驱动(Linux kernel 4.9.x) 主要结构 gendisk结构体:表示一个独立的磁盘设备(或分区) 1.1 定义如下: struct gendisk {/* major, first ...
- Linux块设备驱动总结
<Linux设备驱动程序>第十六章 块设备驱动程序读书笔记 简介 一个块设备驱动程序主要通过传输固定大小的随机数据来访问设备 Linux内核视块设备为与字符设备相异的基本设备类型 Linu ...
- Linux块设备驱动-MTD子系统
Linux块设备驱动 块设备驱动 块设备驱动的引入 1. 简单字符驱动程序思想 2. 块设备驱动程序思想 块设备驱动框架 1. 层次框架 2. 分析ll_rw_block 块设备驱动程序编写 1.分配 ...
- STM32MP157驱动开发——Linux块设备驱动
STM32MP157驱动开发--Linux块设备驱动 一.简介 二.驱动开发 1.使用请求队列的方式 2.测试① 3.不使用请求队列的方式 4.测试② 参考文章:[正点原子]I.MX6U嵌入式Linu ...
- 【linux驱动之字符设备驱动基础】
linux驱动之字符设备驱动基础 文章目录 linux驱动之字符设备驱动基础 前言 一.开启驱动学习之路 二.驱动预备知识 三.什么是驱动? 3.1 驱动概念 3.2 linux 体系架构 3.3 模 ...
- 块设备驱动、bio理解
别人写过的内容,我就不写了.贴一下大佬的博客,写的非常好: 块设备驱动实战基础篇一 (170行代码构建一个逻辑块设备驱动) 块设备驱动实战基础篇二 (继续完善170行过滤驱动代码至200行) 块设备驱 ...
最新文章
- 机器学习(Part I)机器学习的种类
- WPF DispatcherTimer(定时器应用) 无人触摸60s自动关闭窗口
- Linux内存管理 (2)页表的映射过程
- css样式图片、渐变、相关小知识
- Java这些多线程基础知识你会吗?
- MediaCodec的使用和若干问题处理
- 算法 c语言_C语言中10个经典的算法,学会它,利用它
- Android菜鸟的成长笔记(16)——Service简介
- linux informix数据库下载,informix数据库基础下载_informix数据库基础官方下载-太平洋下载中心...
- html5弹性盒子模型,推荐10款弹性盒子源码(收藏)
- [置顶] iOs LightBlue与cc2540 BLE开发板之间的通信实验 [原创, 多图]
- 电脑录屏软件哪个最好用?都是高效高清!
- 2023年北京师范大学应用统计考研上岸前辈备考经验指导
- C语言程序设计作业04
- 内网DNS报错:** server can‘t find ns1.aaa.com: SERVFAIL
- 大疆哪吒飞控naza-m等无法解锁的问题遥控无法启动电机不转解决疑难杂症。
- SAP CDS 开发和Fiori App生成学习笔记
- Prometheus 之 Alertmanager告警抑制与静默
- 使用ALT+数字小键盘在文本文件中输入特殊字符
- python性能分析工具