Linux 块设备驱动 (一)

时间:2023-03-08 16:52:09

1、块设备的I/O操作特点

字符设备与块设备的区别:

  块设备只能以块为单位接受输入和返回输出,而字符设备则以字符为单位。

  块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无需缓冲区且直接被读写。

  字符设备只能被顺序读写,而块设备可以随机读写。 但是对于磁盘等机械设备而言,顺序的组织块设备的访问可以提高性能

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

Linux 块设备驱动 (一)

对于扇区1、10、3、2的请求被调整为对扇区1、2、3、10的请求。但是对于SD卡、RAMDISK等非机械设备来说则没必要调整

2.block_device_operations结构体

1、打开和释放

int (*open)(struct inode *inode,struct file *filp); int(*release)(struct inode *inode,struct file *filp); //与字符设备驱动类似,当设备打开和关闭的时候调用它们

2、io控制

 int (*ioctl)(struct inode *inode,struct file *filp,unsigned int cmd, unsigned long arg); //上述函数是ioctl()系统调用的实现,块设备包含大量的标准请求,这些标准请求由Linux块设备层处理,因此大部分块设备驱动的ioctl()函数相当短。 

3、介质改变

int (*media_changed)(struct gendisk *gd);
/*被内核调用来检查是否驱动器中的介质已经改变。如果是,则返回一个非0值。否则返回0.这个函数仅适用于支持可移动介质的驱动器,通常需要在驱动器中增加一个表示介质状态是否改变的标志变量,非可移动设备的驱动不需要实现这个方法。*/

4、使介质有效

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

5、获得驱动器信息

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

6、模块指针

struct module * owner;// 一个指向拥有这个结构体的模块的指针,它通常被初始化为THIS_MODULE.

3. gendisk分析

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

int major;/* 主设备号 */

int first_minor; /*第1个次设备号*/

int minors: //磁盘使用这些成员描述设备号。
       //最大的次设备数,如果不能分区,则为1,一个驱动器至少使用一个次设备号。
       //如果驱动器是可被分区的(大多数情况下),用户将要为每个可能的分区都分配一个次设备号。minors通常取16,他使得一个“完整的的磁盘“包含15个分区。 某些磁盘驱动程序设置每个设备可使用64个次设备号。 char disk_name[];
//设置磁盘设备名字的成员。该名字将显示在/proc/partitions和sysfs中。 struct block_device_operations *fops;
//设置前面所述的各种设备操作; struct request_queue *queue;
//内核使用该结构为设备管理i/o请求;在”请求过程“中详细进行论述。 int flags;
//用来描述驱动器状态的标志(很少使用)。如果用户设备包含了可移动介质,其将被设置为GENHD_FL_REMOVABLE。 sector_t capacity;
//以512字节为一个扇区时,该驱动器可包含的扇区数。 void *preivate_data;
//块设备驱动程序可能使用该成员保存指向其内部数据的指针。

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

fops为block_device_operations,即上节描述的块设备操作集合。queue是内核用来管理这个设备的 I/O请求队列的指针。

capacity表明设备的容量,以512个字节为单位。private_data可用于指向磁盘的任何私有数据,用法与字符设备驱动file结构体的private_data类似。

Linux内核提供了一组函数来操作gendisk,主要包括:

• 分配gendisk

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

struct gendisk *alloc_disk(int minors);

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

• 增加gendisk

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

void add_disk(struct gendisk *gd);

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

• 释放gendisk

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

void del_gendisk(struct gendisk *gd);

• gendisk引用计数

gendisk中包含1个kobject成员,因此,它是一个可被引用计数的结构体。通过get_disk()和put_disk()函数可用来操作引用计数,这个工作一般不需要驱动亲自做。通常对 del_gendisk()的调用会去掉gendisk的最终引用计数,但是这一点并不是一定的。

因此,在del_gendisk()被调用后,这个结构体可能继续存在。

• 设置gendisk容量

void set_capacity(struct gendisk *disk, sector_t size);

块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的大小是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次就传输多个扇区。虽然大多数块设

备的扇区大小都是512字节,不过其它大小的扇区也很常见,比如,很多CD-ROM盘的扇区都是2K大小。

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

4.块设备驱动模块加载和卸载函数模板

在块设备驱动的模块加载函数中通常需要完成如下工作:
① 分配、初始化请求队列,绑定请求队列和请求函数。

分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。
③ 注册块设备驱动。

使用blk_alloc_queue

static int __init xxx_init(void)
{
//分配gendisk
xxx_disks = alloc_disk();
if (!xxx_disks)
{
goto out;
} /*
块设备驱动注册
在2.6内核中,对 register_blkdev()的调用完全是可选的,register_blkdev()的功能已随时间正在减少,这个调用最多只完全2件事:
① 如果需要,分配一个动态主设备号。
② 在/proc/devices中创建一个入口。
在将来的内核中,register_blkdev()可能会被去掉。但是目前的大部分驱动仍然调用它。
*/
if (register_blkdev(XXX_MAJOR, "xxx"))
{
err = - EIO;
goto out;
} //“请求队列”分配
xxx_queue = blk_alloc_queue(GFP_KERNEL);
if (!xxx_queue)
{
goto out_queue;
} blk_queue_make_request(xxx_queue, &xxx_make_request); //绑定“制造请求”函数
blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬件扇区尺寸设置 //gendisk初始化
xxx_disks->major = XXX_MAJOR;
xxx_disks->first_minor = ;
xxx_disks->fops = &xxx_op;
xxx_disks->queue = xxx_queue;
sprintf(xxx_disks->disk_name, "xxx%d", i);
set_capacity(xxx_disks, xxx_size); //xxx_size以512bytes为单位
add_disk(xxx_disks); //添加gendisk return ;
out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
out: put_disk(xxx_disks);
blk_cleanup_queue(xxx_queue); return - ENOMEM;
}

使用blk_init_queue

static int __init xxx_init(void)
{
//块设备驱动注册
if (register_blkdev(XXX_MAJOR, "xxx"))
{
err = - EIO;
goto out;
} //请求队列初始化
xxx_queue = blk_init_queue(xxx_request, xxx_lock);
if (!xxx_queue)
{
goto out_queue;
} blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬件扇区尺寸设置 //gendisk初始化
xxx_disks->major = XXX_MAJOR;
xxx_disks->first_minor = ;
xxx_disks->fops = &xxx_op;
xxx_disks->queue = xxx_queue;
sprintf(xxx_disks->disk_name, "xxx%d", i);
set_capacity(xxx_disks, xxx_size *);
add_disk(xxx_disks); //添加gendisk return ;
out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
out: put_disk(xxx_disks);
blk_cleanup_queue(xxx_queue); return - ENOMEM;
}

每个块设备驱动程序的核心是它的请求函数。

实际的工作,如设备的启动,都是在这个函数中完成。

驱动程序的新能,是这个操作系统性能的重要组成部分,因此内核的块设备子系统在编写的时候就非常注意性能方面的问题。

块设备驱动模块卸载函数模板

① 清除请求队列。
② 删除gendisk和对gendisk的引用。
③ 删除对块设备的引用,注销块设备驱动。

static void __exit xxx_exit(void)
{
if (bdev)
{
invalidate_bdev(xxx_bdev, );
blkdev_put(xxx_bdev);
} del_gendisk(xxx_disks); //删除gendisk
put_disk(xxx_disks);
blk_cleanup_queue(xxx_queue[i]); //清除请求队列
unregister_blkdev(XXX_MAJOR, "xxx");
}

5.块设备驱动I/O

request函数介绍

原型:

void request(request_queue_t *queue);

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

请求函数可以在没有完成请求队列中的所有请求的情况下返回,甚至它1个请求不完成都可以返回。但是,对大部分设备而言,在请求函数中处理完所有请求后再返回通常是值得推荐的方法。

对request函数的调用是与用户空间进程中的动作是完全异步的,因此不能直接对用户空间进行访问。

块设备驱动请求函数例程:

static void xxx_request(request_queue_t *q)
{
struct request *req;
while ((req = elv_next_request(q)) != NULL)
{
struct xxx_dev *dev = req->rq_disk->private_data;
if (!blk_fs_request(req)) //不是文件系统请求
{
printk(KERN_NOTICE "Skip non-fs request/n");
end_request(req, );//通知请求处理失败
continue;
} xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer,
rq_data_dir(req)); //处理这个请求
end_request(req, ); //通知成功完成这个请求
}
} //完成具体的块设备I/O操作
static void xxx_transfer(struct xxx_dev *dev, unsigned long sector, unsigned
long nsect, char *buffer, int write)
{
unsigned long offset = sector * KERNEL_SECTOR_SIZE;
unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE; if ((offset + nbytes) > dev->size)
{
printk(KERN_NOTICE "Beyond-end write (%ld %ld)/n", offset, nbytes);
return ;
} if (write)
{
write_dev(offset, buffer, nbytes); //向设备些nbytes个字节的数据
}
else
{
read_dev(offset, buffer, nbytes); //从设备读nbytes个字节的数据
}
} void end_request(struct request *req, int uptodate)
{
/*
当设备已经完成1个I/O请求的部分或者全部扇区传输后,
end_that_request_first这个函数告知块设备层,块设备驱动已经完成count个扇区的传送
返回值为0表示所有的扇区已经被传送并且这个请求完成
*/
if (!end_that_request_first(req, uptodate, req->hard_cur_sectors))
{
/*
使用块 I/O 请求的定时来给系统的随机数池贡献熵,它不影响块设备驱动。
但是,仅当磁盘的操作时间是真正随机的时候(大部分机械设备如此),才应该调用它。
*/
add_disk_randomness (req->rq_disk);
blkdev_dequeue_request (req);//从队列中清除这个请求
end_that_request_last(req);//通知所有正在等待这个请求完成的对象请求已经完成并回收这个请求结构体。
}
}

请求队列

一个块设备请求队列是:包含块设备I/O请求的序列。

请求队列跟踪未完成的块设备的I/O请求,保存了描述设备所能处理的请求的参数:最大尺寸、在同一个请求中所包含的独立段的数目、硬件扇区的大小、对齐需求等。

请求队列实现了插件接口,以便可以使用多个I/O调度器。

I/O调度器积累了大量的请求,根据块索引号升序(或者降序)排列他们,并按照这个顺序向驱动程序发送请求。

磁头从一个磁盘的末尾移向另一个磁盘,如同单向电梯一样,直到每个请求都得到满足。

队列的创建与删除

一个请求队列就是一个动态的数据结构,该结构必须由块设备的I/O子系统创建。

request_queue_t *blk_init_queue(request_fn_proc *request,spinlock_t *lock) ;//该函数参数是处理这个队列的request指针和控制访问队列权限的自旋锁。

void blk_cleanup_queue(request_queue_t *);//删除队列

队列中的函数

返回队列中下一个要处理的请求

struct request *elv_next_request(request_queue_t *queue);

将请求从队列中删除

void blkdev_dequeue_request(struct request *req);

队列控制函数

驱动程序使用块设备层到处的一组函数去控制请求队列的操作。

void blk_stop_queue(request_queue_t *queue)

void blk_start_queue(request_queue_t *queue) //如果驱动程序进入不能处理更多命令的状态,就会调用blk_stop_queue以通知块设备层,以暂停调用request函数。当有能力处理更多请求时,需要调用blk_start_queue重新开始调用。

void blk_queue_bounce_limit(request_queue_t *queue,u64 dma_addr); //该函数告诉内核驱动程序执行DMA所使用的最高物理内存。如果一个请求包含了超越界限的内存引用,将使用回弹缓冲区(bounce buffer)进行处理。 

请求过程剖析

每个request结构都代表了一个块设备的I/O请求。

一个特定的请求可以分布在整个内存中,但多数是对相邻的扇区进行操作。

如果多个请求都是对磁盘中的相邻扇区进行操作,则内核将对他们进行合并。

从本质上讲,一个request结构是作为一个bio结构的链表实现的。

bio

bio结构 bio结构在<linux/bio.h>中定义,包含了驱动程序作者所要使用的诸多成员。

sector_t bi_sector; //该bio结构所要传输的第一个扇区(512字节)

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

unsigned long bi_flags; //bio中一系列的标志位;如果是写请求,最低有效位将被设置。

unsigned short bio_phys_segments;

unsigned short bio_hw_segments; //当DMA 映射完成时,它们分别表示bio中包含的物理段的数目和硬件所能操作的数目。

Linux 块设备驱动 (一)

request结构成员

sector_t hard_sector;

unsigned long hard_nr_sectors;

unsigned int hard_cur_sectors;// 用于跟踪那些驱动程序还未完成的扇区。还未传输的第一个扇区保存在hard_sector中,等待传输扇区的总数量保存在hard_nr_sectors中,当前bio中剩余的扇区数目包含在hard_cur_sectors中。

struct bio *bio;// 该请求的bio结构链表。

struct list_head queuelist;// 内核链表结构,用来把请求连接到请求队列中。

Linux 块设备驱动 (一)

屏障请求

在驱动程序收到请求前,块设备层重新组合了请求以提高I/O性能。出于同样的目的,驱动程序也可以重新组合请求。

但是一些应用程序的某些操作,要写在另外一些操作之前,比如关系数据库在执行一个关系数据库内容的会话前,日志信息要写到驱动器上。

2.6内核采用屏障(barrier)请求解决问题:如果一个请求被设置了REQ_HARDBARRER标志,那么其后请求被初始化前,它必须被写进驱动器。

不可重试请求

当第一次请求失败后,块设备驱动程序经常要重试请求。这样的性能使得系统更可靠,不会丢失数据。

但是,内核在某些情况下标记请求是不可重试的。这些请求如果在第一次执行失败后,要尽快抛弃。