首页 > 代码库 > 块设备驱动程序设计

块设备驱动程序设计

一、块设备简介

1、块设备

块设备将数据存储在固定大小的块中,每个块的大小通常在512字节到32768字节之间。磁盘、SD卡都是常见的块设备。

2、块设备VS字符设备

# 块设备和字符设备最大的区别在于读写数据的基本单元不同。块设备读写数据的基本单元为块,例如磁盘通常为一个sector,而字符设备的基本单元为字节。

# 块设备能够随机访问,而字符设备则只能顺序访问。

块设备体系架构:


VFS是对各种具体文件系统的一种封装,为用户程序访问文件提供统一的接口。


Disk Cache

当用户发起文件访问请求的时候,首先会到Disk Cache中寻找文件是否被缓存了,如果在cache中,则直接从cache中读取。如果数据不再缓存中,就必须要到具体的文件系统读取数据了。

Mapping Layer

1、首先确定文件系统的block size,然后计算所请求的数据包含多少个block。

2、调用具体文件系统的函数来访问文件的inode,确定所请求的数据在磁盘上的逻辑块地址。

Generic Block Layer

Linux内核为块设备抽象了统一的模型,把块设备看作是由若干个扇区组成的数据空间。上层的读写请求在通用块层(Generic Block Layer)被构造成一个或多个bio结构。

I/O Scheduler Layer

I/O调度层负责将I/O操作进行排序,采用某种算法(如:电梯调度算法)来高效地处理操作。


电梯调度算法的基本原则:如果电梯现在朝上运动,如果当前楼层的上方和下方都有请求,则先响应所有上方的请求,然后才向下响应下方的请求;如果电梯向下运动,则刚好相反。

Block Device Driver

块设备驱动程序通过发送命令给磁盘控制器实现真正的数据传输。

二、块设备驱动程序设计

设备描述

Linux内核使用struct gendisk(定义于<linux/genhd.h>)来描述块设备。

struct gendisk
{
int major; //主设备号
int first_minor; //次设备号
int minors;
char disk_name[DISK_NAME_LEN]; //驱动名
struct block_device_operations *fops;
struct request_queue *queue; //请求队列
...... ...... ...... ......
int node_id;
}

设备注册

Linux内核使用add_disk函数向内核注册块设备驱动

void add_disk(struct gendisk *gd)

设备操作

字符设备通过file_operations结构来定义使它所支持的操作,块设备使用一个类似的结构:struct block_device_operations.

struct block_device_operations
{
int (*open)(struct block_device *, fmode_t);
int (*release)(struct gendisk *, fmode_t);
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
...... ....... ...... ....... ......................................
};

IO请求

在Linux内核中,使用struct request来表示等待处理的块设备I/O请求。

struct request
{
struct list_head queuelist; //链表结构
sector_t sector; //要操作的首个扇区
unsigned long nr_sectors; //要操作的扇区数目

struct bio *bio; //请求的bio结构体的链表
struct bio *biotail; //请求的bio结构体的链表尾
........................................................................................................................
}

请求队列

简单的讲,请求队列就是IO请求request所形成的队列,在Linux内核中struct request_queue描述。

内核提供了一系列函数用来操作请求队列:

struct request_queue *blk_init_queue(request_fn_proc *rfn,  spinlock_t *lock) //初始化请求队列,一般在块设备驱动的模块加载函数中调用。

void blk_cleanup_queue(request_queue_t *q) //清除请求队列,这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。

struct request *elv_next_request(request_queue_t *queue) //返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL。elv_next_request()不会清除请求,它仍然将这个请求保留在队列上,因此连续调用它2次,2次会返回同一个请求结构体。

void blkdev_dequeue_request(struct request *req) //从队列中删除1个请求。

 实例代码simple-blk.c是ramdisk的驱动,也就是基于内存的磁盘驱动

块设备驱动测试

1、insmod simple-blk.ko

2、ls /dev/simp_blkdev

3、mkfs.ext3 /dev/simp_blkdev

4、mkdir -p /mnt/blk

5、mount  /dev/simp_blkdev  /mnt/blk

6、cp /etc/init.d/* /mnt/blk

7、ls /mnt/blk

8、umount  /mnt/blk

9、ls /mnt/blk

数据访问流程

struct request_queue

{

.........

make_request_fn *make_request_fn;

.........

}

数据访问流程:


BIO

1个struct bio代表1次块设备I/O请求,IO调度器可将连续的bio合并成1个请求struct request.

struct bio

{

sector_t bi_sector; //要访问的第1个扇区

unsigned int bi_size; //以字节为单位所需传输的数据大小

struct bio_vec *bi_io_vec; //实际的vec列表

..........................................................................................................................................

}

struct bio_vec

{

struct page *bv_page; //页指针

unsigned int bv_len; //传输的数据长度

unsigned int bv_offset; //偏移量

}

_make_request

在_make_request函数中,使用了IO调度器(elevator)将多个bio的访问顺序进行优化,调整,合并为一个request,然后提交给用户指定的函数处理。但是对于ramdisk、U盘、记忆棒之类的设备,并不存在磁盘所面临的寻道时间(没有机械的磁头)。因此对这样的“块设备”而言,一个I/O调度器不但发挥不了作用,反而其本身将白白耗掉不少内存和CPU。

解决办法:驱动程序自己实现request_queue所需要的make_request_fn函数,不使用_make_request

请求队列

request_queue_t *blk_alloc_queue(int gfp_mask) //分配“请求队列”,对于FLASH、RAM盘等完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,应该使用上述函数分配1个“请求队列”。

void blk_queue_make_request(request_queue_t *q, make_request_fn * mfn)//绑定“请求队列”和“制造请求”函数。

三、SD卡驱动测试

见课件。

可参考:http://m.blog.csdn.net/blog/tangkai177/8532152