实验七Linux块设备驱动.docx
- 文档编号:9419946
- 上传时间:2023-02-04
- 格式:DOCX
- 页数:20
- 大小:24.80KB
实验七Linux块设备驱动.docx
《实验七Linux块设备驱动.docx》由会员分享,可在线阅读,更多相关《实验七Linux块设备驱动.docx(20页珍藏版)》请在冰豆网上搜索。
实验七Linux块设备驱动
实验七:
Linux块设备驱动
块设备是与字符设备并列的概念,这两类设备在Linux中驱动的结构有较大差异,总体而言,块设备驱动比字符设备驱动要复杂得多,在I/O操作上表现出极大的不同,缓冲、I/O调度、请求队列等都是与块设备驱动相关的概念。
本章将详细讲解Linux块设备驱动的编程方法。
1.块设备的I/O操作特点
字符设备与块设备I/O操作的不同如下:
(1)块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位。
大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作。
(2)块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无须缓冲且被直接读写。
对于存储设备而言调整读写的顺序作用巨大,因为在读写连续的扇区比分离的扇区更快。
(3)字符设备只能被顺序读写,而块设备可以随机访问。
虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能。
而对SD卡、RamDisk(RamDisk是通过使用软件将RAM模拟当做硬盘来使用的一种技术)等块设备而言,不存在机械上的原因,进行这样的调整没有必要。
2.Linux块设备驱动结构
2.1.block_device_operations结构体
在块设备驱动中,有一个类似于字符设备驱动中file_operations结构体的block_device_operations结构体,它是对块设备操作的集合,定义如代码清单1所示。
代码清单1block_device_operations结构体
structblock_device_operations
{
int(*open)(structinode*,structfile*);//打开
int(*release)(structinode*,structfile*);//释放
int(*ioctl)(structinode*,structfile*,unsigned,unsignedlong);//ioctl
long(*unlocked_ioctl)(structfile*,unsigned,unsignedlong);
long(*compat_ioctl)(structfile*,unsigned,unsignedlong);
int(*direct_access)(structblock_device*,sector_t,unsignedlong*);
int(*media_changed)(structgendisk*);//介质被改变
int(*revalidate_disk)(structgendisk*);//使介质有效
int(*getgeo)(structblock_device*,structhd_geometry*);//填充驱动器信息
structmodule*owner;//模块拥有者
};
下面对其主要的成员函数进行分析。
1.打开和释放
int(*open)(structinode*inode,structfile*filp);
int(*release)(structinode*inode,structfile*filp);
与字符设备驱动类似,当设备被打开和关闭时将调用它们。
2.IO控制
int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg);
上述函数是ioctl()系统调用的实现,块设备包含大量的标准请求,这些标准请求由Linux块设备层处理,因此大部分块设备驱动的ioctl()函数相当短。
3.介质改变
int(*media_changed)(structgendisk*gd);
被内核调用来检查是否驱动器中的介质已经改变,如果是,则返回一个非0值,否则返回0。
这个函数仅适用于支持可移动介质的驱动器,通常需要在驱动中增加一个表示介质状态是否改变的标志变量,非可移动设备的驱动不需要实现这个方法。
4.使介质有效
int(*revalidate_disk)(structgendisk*gd);
revalidate_disk()被调用来响应一个介质改变,它给驱动一个机会来进行必要的工作以使新介质准备好。
5.获得驱动器信息
int(*getgeo)(structblock_device*,structhd_geometry*);
根据驱动器的几何信息填充一个hd_geometry结构体,hd_geometry结构体包含磁头、扇区、柱面等信息。
6.模块指针
structmodule*owner;
一个指向拥有这个结构体的模块的指针,它通常被初始化为THIS_MODULE。
2.2.gendisk结构体
在Linux内核中,使用gendisk(通用磁盘)结构体来表示1个独立的磁盘设备(或分区),这个结构体的定义如代码清单2所示。
代码清单2gendisk结构体
structgendisk
{
intmajor;/*主设备号*/
intfirst_minor;/*第1个次设备号*/
intminors;/*最大的次设备数,如果不能分区,则为1*/
chardisk_name[32];/*设备名称*/
structhd_struct**part;/*磁盘上的分区信息*/
structblock_device_operations*fops;/*块设备操作结构体*/
structrequest_queue*queue;/*请求队列*/
void*private_data;/*私有数据*/
sector_tcapacity;/*扇区数,512字节为1个扇区*/
intflags;
chardevfs_name[64];
intnumber;
structdevice*driverfs_dev;
structkobjectkobj;
structtimer_rand_state*random;
intpolicy;
atomic_tsync_io;/*RAID*/
unsignedlongstamp;
intin_flight;
#ifdefCONFIG_SMP
structdisk_stats*dkstats;
#else
structdisk_statsdkstats;
#endif
};
major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一个主设备号,而次设备号则不同。
fops为block_device_operations,即上节描述的块设备操作集合。
queue是内核用来管理这个设备的I/O请求队列的指针。
capacity表明设备的容量,以512个字节为单位。
private_data可用于指向磁盘的任何私有数据,用法与字符设备驱动file结构体的private_data类似。
Linux内核提供了一组函数来操作gendisk,如下所示:
1.分配gendiskgendisk结构体是一个动态分配的结构体,它需要特别的内核操作来初始化,驱动不能自己分配这个结构体,而应该使用下列函数来分配gendisk:
structgendisk*alloc_disk(intminors);
minors参数是这个磁盘使用的次设备号的数量,一般也就是磁盘分区的数量,此后minors不能被修改。
2.增加gendisk
gendisk结构体被分配之后,系统还不能使用这个磁盘,需要调用如下函数来注册这个磁盘设备。
voidadd_disk(structgendisk*gd);
特别要注意的是对add_disk()的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。
3.释放gendisk
当不再需要一个磁盘时,应当使用如下函数释放gendisk。
voiddel_gendisk(structgendisk*gd);
4.设置gendisk容量
voidset_capacity(structgendisk*disk,sector_tsize);
块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的大小是512字节。
扇区的大小是设备的物理属性,扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次就传输多个扇区。
虽然大多数块设备的扇区大小都是512字节,不过其他大小的扇区也很常见,比如,很多CD-ROM盘的扇区都是2KB。
不管物理设备的真实扇区大小是多少,内核与块设备驱动交互的扇区都以512字节为单位。
因此,set_capacity()函数也以512字节为单位。
2.3.请求结构体request
在Linux块设备驱动中,使用request结构体来表征等待进行的I/O请求,这个结构体的定义如代码清单3所示。
代码清单3request结构体
structrequest
{
structlist_headqueuelist;/*链表结构*/
unsignedlongflags;/*REQ_*/
sector_tsector;/*要传送输的下一个扇区*/
unsignedlongnr_sectors;/*要传送的扇区数目*/
unsignedintcurrent_nr_sectors;/*当前要传送的扇区数目*/
sector_thard_sector;/*要完成的下一个扇区*/
unsignedlonghard_nr_sectors;/*要被完成的扇区数目*/
unsignedinthard_cur_sectors;/*当前要被完成的扇区数目*/
structbio*bio;/*请求的bio结构体的链表*/
structbio*biotail;/*请求的bio结构体的链表尾*/
void*elevator_private;
unsignedshortioprio;
intrq_status;
structgendisk*rq_disk;
interrors;
unsignedlongstart_time;
unsignedshortnr_phys_segments;/*请求在物理内存中占据的不连续的段的数目,scatter/gather列表的尺寸*/
unsignedshortnr_hw_segments;/*与nr_phys_segments相同,但考虑了系统I/OMMU的remap*/
inttag;
char*buffer;/*传送的缓冲,内核虚拟地址*/
intref_count;/*引用计数*/
...
};
request结构体的主要成员包括:
sector_thard_sector;
unsignedlonghard_nr_sectors;
unsignedinthard_cur_sectors;
上述3个成员标识还未完成的扇区,hard_sector是第一个尚未传输的扇区,hard_nr_sectors是尚待完成的扇区数,hard_cur_sectors是当前I/O操作中待完成的扇区数。
这些成员只用于内核块设备层,驱动不应当使用它们,如下所示:
sector_tsector;
unsignedlongnr_sectors;
unsignedintcurrent_nr_sectors;
驱动中会经常与这3个成员打交道,这3个成员在内核和驱动交互中发挥着重大作用。
它们以512字节大小为一个扇区,如果硬件的扇区大小不是512字节,则需要进行相应的调整。
例如,如果硬件的扇区大小是2048字节,则在进行硬件操作之前,需要用4来除起始扇区号。
hard_sector、hard_nr_sectors、hard_cur_sectors与sector、nr_sectors、current_nr_sectors之间可认为是“副本”关系。
2.4.请求队列结构体request_queue
一个块请求队列是一个块I/O请求的队列,其定义如代码清单4。
代码清单4request队列结构体
structrequest_queue
{
...
/*保护队列结构体的自旋锁*/
spinlock_t__queue_lock;
spinlock_t*queue_lock;
/*队列kobject*/
structkobjectkobj;
/*队列设置*/
unsignedlongnr_requests;/*最大的请求数量*/
unsignedintnr_congestion_on;
unsignedintnr_congestion_off;
unsignedintnr_batching;
unsignedshortmax_sectors;/*最大的扇区数*/
unsignedshortmax_hw_sectors;
unsignedshortmax_phys_segments;/*最大的段数*/
unsignedshortmax_hw_segments;
unsignedshorthardsect_size;/*硬件扇区尺寸*/
unsignedintmax_segment_size;/*最大的段尺寸*/
unsignedlongseg_boundary_mask;/*段边界掩码*/
unsignedintdma_alignment;/*DMA传送的内存对齐限制*/
structblk_queue_tag*queue_tags;
atomic_trefcnt;/*引用计数*/
unsignedintin_flight;
unsignedintsg_timeout;
unsignedintsg_reserved_size;
intnode;
structlist_headdrain_list;
structrequest*flush_rq;
unsignedcharordered;
};
请求队列跟踪等候的块I/O请求,它存储用于描述这个设备能够支持的请求的类型信息、它们的最大大小、多少不同的段可进入一个请求、硬件扇区大小、对齐要求等参数,其结果是:
如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求。
请求队列还实现一个插入接口,这个接口允许使用多个I/O调度器,I/O调度器(也称电梯)的工作是以最优性能的方式向驱动提交I/O请求。
大部分I/O调度器累积批量的I/O请求,并将它们排列为递增(或递减)的块索引顺序后提交给驱动。
进行这些工作的原因在于,对于磁头而言,当给定顺序排列的请求时,可以使得磁盘顺序地从一头到另一头工作,非常像一个满载的电梯,在一个方向移动直到所有它的“请求”被满足。
另外,I/O调度器还负责合并邻近的请求,当一个新I/O请求被提交给调度器后,它会在队列里搜寻包含邻近扇区的请求。
如果找到一个,并且如果结果的请求不是太大,调度器将合并这两个请求。
对磁盘等块设备进行I/O操作顺序的调度类似于电梯的原理,先服务完上楼的乘客,再服务下楼的乘客效率会更高,而顺序响应用户的请求则电梯会无序地忙乱。
内核包含4个I/O调度器,它们分别是NoopI/Oscheduler、AnticipatoryI/Oscheduler、DeadlineI/Oscheduler与CFQI/Oscheduler。
NoopI/Oscheduler是一个简化的调度程序,它只作最基本的合并与排序。
AnticipatoryI/Oscheduler是当前内核中默认的I/O调度器,它拥有非常好的性能,在内核中它就相当引人注意。
在与内核进行的对比测试中,在内核中多项以分钟为单位完成的任务,它则是以秒为单位来完成的,正因为如此它成为目前内核中默认的I/O调度器。
AnticipatoryI/Oscheduler的缺点是比较庞大与复杂,在一些特殊的情况下,特别是在数据吞吐量非常大的数据库系统中它会变得比较缓慢。
DeadlineI/Oscheduler是针对AnticipatoryI/Oscheduler的缺点进行改善而来的,表现出的性能几乎与AnticipatoryI/Oscheduler一样好,但是比Anticipatory小巧。
CFQI/Oscheduler为系统内的所有任务分配相同的带宽,提供一个公平的工作环境,它比较适合桌面环境。
事实上在测试中它也有不错的表现,mplayer、xmms等多媒体播放器与它配合的相当好,回放平滑,几乎没有因访问磁盘而出现的跳帧现象。
内核block目录中的、、和文件分别实现了上述调度算法。
1.初始化请求队列。
request_queue_t*blk_init_queue(request_fn_proc*rfn,spinlock_t*lock);
第一个参数是请求处理函数的指针,第二个参数是控制访问队列权限的自旋锁,这个函数会发生内存分配的行为,它可能会失败,因此一定要检查它的返回值。
这个函数一般在块设备驱动的模块加载函数中调用。
2.清除请求队列。
voidblk_cleanup_queue(request_queue_t*q);
这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。
3.提取请求。
structrequest*elv_next_request(request_queue_t*queue);
blk_fetch_request()
上述函数用于返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL。
elv_next_request()不会清除请求,它仍然将这个请求保留在队列上,但是标识它为活动的,这个标识将阻止I/O调度器合并其他的请求到已开始执行的请求。
因为elv_next_request()不从队列里清除请求,因此连续调用它两次,两次会返回同一个请求结构体。
3.块设备驱动注册与注销
块设备驱动中的第一个工作通常是注册它们自己到内核,完成这个任务的函数是register_blkdev(),其原型为:
intregister_blkdev(unsignedintmajor,constchar*name);
major参数是块设备要使用的主设备号,name为设备名,它会在cat/proc/devices中被显示。
如果major为0,内核会自动分配一个新的主设备号,register_blkdev()函数的返回值就是这个主设备号。
如果register_blkdev()返回一个负值,表明发生了一个错误。
与register_blkdev()对应的注销函数是unregister_blkdev(),其原型为:
intunregister_blkdev(unsignedintmajor,constchar*name);
传递给unregister_blkdev()的参数必须与传递给register_blkdev()的参数匹配,否则这个函数返回-EINVAL。
值得一提的是,在内核中,对register_blkdev()的调用完全是可选的,register_blkdev()的功能已随时间正在减少,这个调用最多只完成两件事:
①如果需要,分配一个动态主设备号。
②在/proc/devices中创建一个入口。
在将来的内核中,register_blkdev()可能会被去掉。
但是目前的大部分驱动仍然调用它。
4.Linux块设备驱动的模块加载与卸载
在块设备驱动的模块加载函数中通常需要完成如下工作:
①分配、初始化请求队列,绑定请求队列和请求函数。
②分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。
③注册块设备驱动register_blkdev()。
在块设备驱动的模块卸载函数中完成与模块加载函数相反的工作:
①清除请求队列。
②删除gendisk和对gendisk的引用。
③删除对块设备的引用,注销块设备驱动。
最终代码:
simp_blkdev.c
//为了便于讲解,一律没有使用宏
#include
#include
#include
#include
#include
unsignedchararray_data[16*1024*1024];//全局数组,表示本设备
structgendisk*my_gendisk;//全局变量,表示本设备
intmajor_num=-1;//全局变量,主设备号
structrequest_queue*my_request_queue=NULL;//全局变量,本设备关联的请求队列
staticDEFINE_SPINLOCK(lock);//全局变量,内核访问请求队列的自旋锁
/*对块设备的操作函数集合(函数指针集合)*/
structblock_device_operationsmy_operations=
{
.owner=THIS_MODULE,
};
staticvoidsbd_transfer(sector_tsector,unsignedlongnsect,char*buffer,intwrite)
{
unsignedlongoffset=sector<<9;
unsignedlongnbytes=nsect<<9;
if((offset+nbytes)>16*1024*1024)
{
printk(KERN_NOTICE"sbd:
Beyond-endwrite(%ld%ld)\n",offset,nbytes);
return;
}
if(write)
memcpy(array_data+offset,buffer,nbytes);
else
memcpy(buffer,array_data+offset,nbytes);
}
staticvoidmy_process_on_request_queue(structrequest_queue*q)
{
structrequest*req;
req=blk_fetch_request(q);
while(req!
=NULL)
{
if(req->cmd_type!
=REQ_TYPE_FS)
{
printk(KERN_NOTICE"Skipnon-CMDrequest\n");
__blk_end_request_all(req,-EIO);
continue;
}
sbd_transfer(blk_rq_pos(
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 实验 Linux 设备 驱动