Linux内核版本:3.5.0

Linux驱动之块设备驱动​_块设备


Linux驱动之块设备驱动​_初始化_02


基本概念

MMC驱动源码:\drivers\mmc\host\sdhci-s3c.c

  • 块设备(blockdevice)

是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。

块设备是与字符设备并列的概念,这两类设备在 Linux 中驱动的结构有较大差异,总体而言, 块设备驱动比字符设备驱动要复杂得多,在 I/O 操作上表现出极大的不同,缓冲、 I/O 调度、请求队列等都是与块设备驱动相关的概念。


  • 字符设备(Character device)

是一个顺序的数据流设备,对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据流。他不具备缓冲区,所以对这种设备的读写是实时的。


  • 扇区(Sectors):任何块设备硬件对数据处理的基本单位。通常,1个扇区的大小为512byte。(对设备而言)

块 (Blocks):由Linux制定对内核或文件系统等数据处理的基本单位。通常,1个块由1个或多个扇区组成。(对Linux操作系统而言)


  • 段(Segments):由若干个相邻的块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。


  • 块设备的应用在Linux中是一个完整的子系统。

在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,在驱动中用request结构体描述。但对于一些磁盘设备而言请求的速度很慢,这时候内核就提供一种队列的机制把这些I/O请求添加到队列中(即:请求队列),在驱动中用request_queue结构体描述。在向块设备提交这些请求前内核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责提交 I/O 请求, 调度程序将磁盘资源分配给系统中所有挂起的块 I/O 请求,其工作是管理块设备的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求到设备。


由通用块层(Generic Block Layer)负责维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系。在通用块层中,通常用一个bio结构体来对应一个I/O请求。

Linux提供了一个gendisk数据结构体,用来表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行访问。在gendisk中有一个类似字符设备中file_operations的硬件操作结构指针,是block_device_operations结构体。


  • 相关数据结构

block_device: 描述一个分区或整个磁盘对内核的一个块设备实例

gendisk: 描述一个通用硬盘(generic hard disk)对象。

hd_struct: 描述分区应有的分区信息

bio: 描述块数据传送时怎样完成填充或读取块给driver

request: 描述向内核请求一个列表准备做队列处理。

request_queue: 描述内核申请request资源建立请求链表并填写BIO形成队列。


1.2 块设备相关的数据结构

1.2.1 块设备文件操作集合

/*块设备的文件操作集合接口 类似于字符设备的file_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);

int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);

int (*direct_access) (struct block_device *, sector_t,void **, unsigned long *);

unsigned int (*check_events) (struct gendisk *disk,unsigned int clearing);

int (*media_changed) (struct gendisk *);

void (*unlock_native_capacity) (struct gendisk *);

int (*revalidate_disk) (struct gendisk *);

int (*getgeo)(struct block_device *, struct hd_geometry *);

void (*swap_slot_free_notify) (struct block_device *, unsigned long);

struct module *owner;

};


1.2.2 接口函数解析

1.2.2.1 打开和释放

int (*open) (struct inode *inode, struct file *filp) ;

int (*release) (struct inode *inode, struct file *filp) ;

与字符设备驱动类似,当设备被打开和关闭时将调用它们。


1.2.2.2 IO控制

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd,unsigned long arg) ;

上述函数是 ioctl()系统调用的实现, 块设备包含大量的标准请求,这些标准请求由 Linux 块设备层处理, 因此大部分块设备驱动的 ioctl()函数相当短。


1.2.2.3 介质改变

int (*media_changed) (struct gendisk *gd) ;

被内核调用来检查是否驱动器中的介质已经改变,如果是,则返回一个非 0 值,否则返回 0。

这个函数仅适用于支持可移动介质的驱动器,通常需要在驱动中增加一个表示介质状态是否改变的标志变量,非可移动设备的驱动不需要实现这个方法。


1.2.2.4 使介质有效

int (*revalidate_disk) (struct gendisk *gd);

revalidate_disk()函数被调用来响应一个介质改变, 它给驱动一个机会来进行必要的工作以使新介质准备好。


1.2.2.5获得驱动器信息

int (*getgeo) (struct block_device *, struct hd_geometry *) ;

该函数根据驱动器的几何信息填充一个 hd_geometry 结构体, hd_geometry 结构体包含磁头、 扇区、 柱面等信息。

1.2.2.6模块指针

struct module *owner;

一个指向拥有这个结构体的模块的指针,它通常被初始化为 THIS_MODULE。

1.2.3 gendisk 通用磁盘结构体

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

struct gendisk {

主设备号*/

int first_minor;

int minors; /*最大的次设备号数量,如果设备不能分区,该值为1*/

char disk_name[DISK_NAME_LEN]; /*主设备名*/

char *(*devnode)(struct gendisk *gd, umode_t *mode);

unsigned int events;/* supported events */

unsigned int async_events;/* async events, subset of all */

struct disk_part_tbl __rcu *part_tbl;

struct hd_struct part0; /*分区信息,有minors个*/

const struct block_device_operations *fops; /*设备操作 */

struct request_queue *queue; /*设备管理I/O请求 */

void *private_data; /*私有数据*/

int flags;

struct device *driverfs_dev; // FIXME: remove

struct kobject *slave_dir;

struct timer_rand_state *random;

atomic_t sync_io;/* RAID */

struct disk_events *ev;

#ifdef CONFIG_BLK_DEV_INTEGRITY

struct blk_integrity *integrity;

#endif

int node_id;

};

major、 first_minor 和 minors 共同表征了磁盘的主、 次设备号,同一个磁盘的各个分区共享一个主设备号,而次设备号则不同。

fops 为 block_device_operations, 即上节描述的块设备操作集合。

queue 是内核用来管理这个设备的 I/O 请求队列的指针。

private_data 可用于指向磁盘的任何私有数据, 用法与字符设备驱动 file 结构体的 private_data 类似。


1.2.4 块设备的名称与数量

块设备的名称与数量*/

static struct blk_major_name {

struct blk_major_name *next;

int major; //主设备号

char name[16]; //名称最长16个字节

} *major_names[BLKDEV_MAJOR_HASH_SIZE]; /*最大255个设备*/


1.3 块设备相关的函数API

1.3.1 动态分配次设备号结构

gendisk 结构体是一个动态分配的结构体, 它需要特别的内核操作来初始化, 驱动不能自己分配这个结构体,而应该使用下列函数来分配 gendisk。

函数原型:

struct gendisk *alloc_disk(int minors)

{

return alloc_disk_node(minors, -1);

}


定义路径:\bloc\genhd.c

功能:minors 参数是这个磁盘使用的次设备号的数量, 一般也就是磁盘分区的数量, 此后 minors 不能被修改。


1.3.2 注册磁盘设备

gendisk 结构体被分配之后, 系统还不能使用这个磁盘, 需要调用如下函数来注册这个磁盘设备。

函数原型:

void add_disk(struct gendisk *disk)


注意:对 add_disk()的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。

函数功能:向内核注册磁盘的分区信息


1.3.3 释放磁盘设备

当不再需要一个磁盘时,应当使用如下函数释放 gendisk。

函数原型:

void del_gendisk(struct gendisk *disk)

函数功能:注销磁盘设备。



1.3.4 设置磁盘结构capacity的容量

函数原型:

tatic inline void set_capacity(struct gendisk *disk, sector_t size)

{

disk->part0.nr_sects = size;

}

块设备中最小的可寻址单元是扇区,扇区大小一般是 2 的整数倍, 最常见的大小是 512 字节。 扇区的大小是设备的物理属性, 扇区是所有块设备的基本单元, 块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次就传输多个扇区。虽然大多数块设备的扇区大小都是 512 字节,不过其他大小的扇区也很常见,比如,很多 CD-ROM 盘的扇区都是 2KB。不管物理设备的真实扇区大小是多少, 内核与块设备驱动交互的扇区都以 512 字节为单位。 因此, set_capacity()函数也以 512 字节为单位。


1.3.5 获取磁盘结构capacity的容量

函数原型:

static inline sector_t get_capacity(struct gendisk *disk)

{

return disk->part0.nr_sects;

}


1.3.6 注册块设备

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

参数:

major 参数是块设备要使用的主设备号

name为设备名,它会在/proc/devices中被显示。

如果major为0,内核会自动分配一个新的主设备号register_blkdev()函数的返回值就是这个主设备号。

如果返回负值,表明发生了一个错误。


1.3.7 注销块设备

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

参数:

传递给register_blkdev()的参数必须与传递给register_blkdev()的参数匹配,否则这个函数返回-EINVAL。


1.3.8 初始化请求队列

struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

{

return blk_init_queue_node(rfn, lock, -1);

}

标准的请求处理程序能排序请求,并合并相邻的请求,如果一个块设备希望使用标准的请求处理程序,那它必须调用函数blk_init_queue来初始化请求队列。当处理在队列上的请求时,必须持有队列自旋锁。

该函数的第1个参数是请求处理函数的指针,第2个参数是控制访问队列权限的自旋锁,这个函数会发生内存分配的行为,故它可能会失败,函数调用成功时,它返回指向初始化请求队列的指针,否则,返回NULL。这个函数一般在块设备驱动的模块加载函数中调用。


1.3.9 卸载请求队列

void blk_cleanup_queue(struct request_queue *q)

这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。


1.3.10 分配请求队列

struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

{

return blk_alloc_queue_node(gfp_mask, -1);

}


1.3.11 绑定请求队列

void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)

Linux驱动之块设备驱动​_块设备_03


1.3.12 获取请求队列

获取请求队列函数一般在请求队列函数中调用。

struct request *blk_fetch_request(struct request_queue *q)


2.6版本之前的内核中,块设备这一部分的代码有些过于简单并且冗余,2.5版本后的内核里这些代码被重新写过了。

以前,获取I/O请求队列中下一个请求的函数是:

struct request *elv_next_request(struct request_queue *queue);


这个函数返回经过I/O调度器优化过后的下一个请求。但是这个函数有一个特点,它会把这个请求保留在请求队列中,这样的话,如果两个elv_next_request()在非常短的时间间隔内被执行,函数就会返回同一个请求。当然,我们可以用blkdev_dequeue_request()来把一个请求从请求队列中移除,但是完全没必要这么做,因为一旦块设备驱动通知内核这个请求被完成了,内核同样会把这个请求从请求队列中移除。


把请求保留在请求队列中是以前的一个做法,那时候,内核每次只能处理一个请求————比如每次只操作一个扇区。这样做有一个缺点:就像上面讲的那样,内核不知道一个请求何时真正开始被处理,也就没有办法操作一些对处理时间有要求的请求。


况且,现在的处理器足够强大,使得内核能够同时处理多个请求,同时,也要求设备驱动要亲自移除这些请求并对它们保持追踪。所以,这种 process-on-queue模型要被抛弃,Tejun更改了代码,引入了新方法,具体可以查看:


新方法的想法就是在原来驱动的基础上增加“把请求从请求队列中移除”这么一个功能,也就是在合适的地方增加 blkdev_dequeue_request() 这么一个函数,有些地方的修改(比如IDE子系统)没有这么简单,但是大多数地方都是这样修改的。


更改之后,块设备驱动的一些旧的API随之就发生改动:函数elv_next_request(); 不再存在,替代它的是


struct request *blk_peek_request(struct request_queue *queue);


这个函数同样不移除请求,移除请求的函数是:


void blk_start_request(struct request *req);


它取代了blkdev_dequeue_request()。除了从请求队列中移除请求,blk_start_request() 同时启动一个定时器,用超时的办法防止这个请求没有响应。


大部分的情况下,我们只需要调用:


struct request *blk_fetch_request(struct request_queue *q);


这个函数包含了blk_peek_request()和 blk_start_request()。


另外,新的API也需要注意:尝试完成还在一个请求队列里面的请求会引起系统错误。


1.3.13 判断读写请求

/*判断读还是写*/

#define rq_data_dir(rq)((rq)->cmd_flags & 1)


示例:

switch(rq_data_dir(req))

{

case READ:

printk("读操作\n");

break;

case WRITE:

printk("写操作\n");

break;

}


1.3.14 请求完成当前块

bool __blk_end_request_cur(struct request *rq, int error)

{

return __blk_end_request(rq, error, blk_rq_cur_bytes(rq));

}

返回值:

0——完成这个请求

1——缓冲区仍然在等待这个请求


1.3.15 获取请求队列的数据信息

blk_rq_pos(): 当前扇区

blk_rq_bytes()全部请求字节大小

blk_rq_cur_bytes(): 当前请求字节大小

blk_rq_err_bytes(): bytes left till the next error boundary 错误字节

blk_rq_sectors(): 全部扇区

blk_rq_cur_sectors(): 当前扇区请求长度


1.3.16 创建文件系统

mkfs.ext2 /dev/tiny4412_block_a //格式化文件系统

核心结构介绍

核心结构与方法简述

  1. 核心结构
  2. gendisk是一个物理磁盘或分区在内核中的描述
  3. block_device_operations描述磁盘的操作方法集,block_device_operations之于gendisk,类似于file_operations之于cdev
  4. request_queue对象表示针对一个gendisk对象的所有请求的队列,是相应gendisk对象的一个域
  5. request表示经过IO调度之后的针对一个gendisk(磁盘)的一个"请求",是request_queue的一个节点。多个request构成了一个request_queue
  6. bio表示应用程序对一个gendisk(磁盘)原始的访问请求,一个bio由多个bio_vec,多个bio经过IO调度和合并之后可以形成一个request。
  7. bio_vec描述的应用层准备读写一个gendisk(磁盘)时需要使用的内存页page的一部分,即上文中的"段",多个bio_vec和bio_iter形成一个bio
  8. bvec_iter描述一个bio_vec中的一个sector信息

Linux驱动之块设备驱动​_块设备_04


  1. 核心方法
  2. set_capacity()设置gendisk对应的磁盘的物理参数
  3. blk_init_queue()分配+初始化+绑定一个有IO调度的gendisk的requst_queue,处理函数是**void (request_fn_proc) (struct request_queue *q);**类型
  4. blk_alloc_queue() 分配+初始化一个没有IO调度的gendisk的request_queue,
  5. blk_queue_make_request()绑定处理函数到一个没有IO调度的request_queue,处理函数函数是void (make_request_fn) (struct request_queue q, struct bio bio);类型
  6. __rq_for_each_bio()遍历一个request中的所有的bio
  7. bio_for_each_segment()遍历一个bio中所有的segment
  8. rq_for_each_segment()遍历一个request中的所有的bio中的所有的segment
    最后三个遍历算法都是用在request_queue绑定的处理函数中,这个函数负责对上层请求的处理。

1.3.18 bio结构

在通用块层中,bio用来描述单一的I/O请求,它记录了一次I/O操作所必需的相关信息,如用于I/O操作的数据缓存位置,I/O操作的块设备起始扇区,是读操作还是写操作等等。struct bio的定义如下

struct bio {

sector_t bi_sector;

struct bio *bi_next; /* request queue link */

struct block_device *bi_bdev;

unsigned long bi_flags; /* status, command, etc */

unsigned long bi_rw; /* bottom bits READ/WRITE,

unsigned short bi_vcnt; /* how many bio_vec's */

unsigned short bi_idx; /* current index into bvl_vec */

unsigned int bi_phys_segments;

unsigned int bi_size; /* residual I/O count */

unsigned int bi_seg_front_size;

unsigned int bi_seg_back_size;

unsigned int bi_max_vecs; /* max bvl_vecs we can hold */

unsigned int bi_comp_cpu; /* completion CPU */

atomic_t bi_cnt; /* pin count */

struct bio_vec *bi_io_vec; /* the actual vec list */

bio_end_io_t *bi_end_io;

void *bi_private;

struct bio_vec bi_inline_vecs[0];

};

bi_sector:该I/O操作的起始扇区号

bi_rw:指明了读写方向

bi_vcnt:该I/O操作中涉及到了多少个缓存向量,每个缓存向量由[page,offset,len]来描述

bi_idx:指示当前的缓存向量

bi_io_vec:缓存向量数组




1.4 编写块设备驱动流程

1.4.1 注册与注销步骤

  • 注册步骤

在块设备驱动的模块加载函数中通常需要完成如下工作:

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

在块设备驱动的模块卸载函数中通常需要与模块加载函数相反的工作:

  1. 清除请求队列。
  2. 删除gendisk和对gendisk的引用。
  3. 删除对块设备的引用,注销块设备驱动。
  • 总结:

块设备的I/O操作方式与字符设备存在较大的不同,因而引入了request_queue、request、bio等一系列数据结构。在整个块设备的I/O操作中,贯穿于始终的就是“请求”,字符设备的I/O操作则是直接进行不绕弯,块设备的I/O操作会排队和整合。驱动的任务是处理请求,对请求的排队和整合由I/O调度算法解决,因此,块设备驱动的核心就是请求处理函数或“制造请求”函数。

尽管在块设备驱动中仍然存在block_device_operations结构体及其成员函数,但其不再包含读写一类的成员函数,而只是包含打开、释放及I/O控制等与具体读写无关的函数。块设备驱动的结构相当复杂的,但幸运的是,块设备不像字符设备那么包罗万象,它通常就是存储设备,而且驱动的主体已经由Linux内核提供,针对一个特定的硬件系统,驱动工程师所涉及到的工作往往只是编写少量的与硬件直接交互的代码。


1.4.2 代码框架示例

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <asm/uaccess.h>

#include <linux/spinlock.h>

#include <linux/sched.h>

#include <linux/types.h>

#include <linux/fcntl.h>

#include <linux/hdreg.h>

#include <linux/genhd.h>

#include <linux/blkdev.h>



static char blk_dev_name[]="4412_blk_dev";

static int major; /*主设备号*/

static spinlock_t lock; /*自旋锁结构体*/

static struct gendisk *gd; /*磁盘的信息*/


/*块设备请求处理函数*/

static void blk_request_func(struct request_queue *q)

{

命令、mount命令等一系列命令会调该处理函数*/

请求队列!-->\n");

}


/*ioctl操作函数*/

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)

{

块设备open函数调用成功\n");

return 0;

}



static int blk_release(struct gendisk *gd , fmode_t no)

{

块设备release函数调用成功\n");

return 0;

}



/*块设备文件操作集合*/

struct block_device_operations blk_ops=

{

.owner = THIS_MODULE,

.open = blk_open,

.release = blk_release,

.ioctl = blk_ioctl,

};


static int __init block_module_init(void)

{

注册一个块设备,自动分配主设备号*/

major =register_blkdev(0, blk_dev_name);

if(major>0)

{

主设备号分配成功:%d\n",major);

}

else

{

return -EBUSY;

}


/*动态分配gendisk结构体*/

分配一个gendisk,分区是一个

初始化一个自旋锁


/*填充gendisk结构体*/

gd->major = major;

第一个次设备号

关联操作函数


初始化请求队列并关联到gendisk */

gd->queue = blk_init_queue(blk_request_func, &lock);


/*拼接设备节点的名称,固定最长为32字节 ,dev/目录可以看到*/

snprintf(gd->disk_name, 32, "tiny4412_block_%c", 'a');


/*设置块设备大小 512*32=16K ,cat /sys/block/xxxx/size 可以查看到 */

set_capacity(gd, 32);


/*向内核列表添加分区信息*/

add_disk(gd);

块设备注册成功!\n");

return 0;

}



static void __exit block_module_exit(void)

{

/*关闭请求队列*/

blk_cleanup_queue(gd->queue);


/*注销磁盘设备*/

del_gendisk(gd);


/*注销块设备*/

unregister_blkdev(major, blk_dev_name);


块设备注销成功!\n");

}


module_init(block_module_init);

module_exit(block_module_exit);

MODULE_LICENSE("GPL");



1.4.3 注册测试

[root@XiaoLong /code]# ls

app block_test.ko tiny4412_block_test.ko

[root@XiaoLong /code]# insmod tiny4412_block_test.ko

[ 472.600000] 主设备号分配成功:253

[ 472.615000] 块设备注册成功!

[root@XiaoLong /code]# ls /dev/tiny4412_block_a -l

brw-rw---- 1 root root 253, 0 May 7 02:02 /dev/tiny4412_block_a

[root@XiaoLong /code]# cat /sys/block/tiny4412_block_a/size

32

[root@XiaoLong /code]# dd if=./app of=/dev/tiny4412_block_a

[ 551.750000] 块设备open函数调用成功

[ 551.750000] 请求队列!-->