您的位置:首页 > 运维架构 > Linux

Linux块设备驱动

2015-08-04 10:55 447 查看

块设备是与字符设备并列的概念,这两类设备在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所示。

代码清单1 block_device_operations结构体

structblock_device_operations

{

int(*open)(struct inode *, struct file*); //打开

int(*release)(struct inode *, struct file*); //释放

int(*ioctl)(struct inode *, struct file *, unsigned, unsignedlong);//ioctl

long(*unlocked_ioctl)(struct file *, unsigned, unsigned long);

long(*compat_ioctl)(struct file *, unsigned, unsigned long);

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

int(*media_changed)(struct gendisk*); //介质被改变

int(*revalidate_disk)(struct gendisk*); //使介质有效

int(*getgeo)(struct block_device *, struct hd_geometry*);//填充驱动器信息

struct module *owner; //模块拥有者

};

下面对其主要的成员函数进行分析。

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, unsignedlong 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结构体,hd_geometry结构体包含磁头、扇区、柱面等信息。

6.模块指针

structmodule *owner;

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

2.2.gendisk结构体

在Linux内核中,使用gendisk(通用磁盘)结构体来表示1个独立的磁盘设备(或分区),这个结构体的定义如代码清单2所示。

代码清单2 gendisk结构体

structgendisk

{

int major; /*主设备号*/

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

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

char disk_name[32]; /*设备名称 */

struct hd_struct **part; /*磁盘上的分区信息
*/

struct block_device_operations *fops; /*块设备操作结构体*/

struct request_queue *queue; /*请求队列*/

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

sector_t capacity; /*扇区数,512字节为1个扇区*/

int flags;

char devfs_name[64];

int number;

struct device *driverfs_dev;

struct kobject kobj;

struct timer_rand_state *random;

int policy;

atomic_t sync_io; /* RAID */

unsigned long stamp;

int in_flight;

#ifdef CONFIG_SMP

struct disk_stats *dkstats;

#else

struct disk_stats dkstats;

#endif

};

major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一个主设备号,而次设备号则不同。fops为block_device_operations,即上节描述的块设备操作集合。queue是内核用来管理这个设备的I/O请求队列的指针。capacity表明设备的容量,以512个字节为单位。private_data可用于指向磁盘的任何私有数据,用法与字符设备驱动file结构体的private_data类似。

Linux内核提供了一组函数来操作gendisk,如下所示:

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

structgendisk *alloc_disk(int minors);

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

2.增加gendisk

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

voidadd_disk(struct gendisk *gd);

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

3.释放gendisk

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

voiddel_gendisk(struct gendisk *gd);

4.设置gendisk容量

voidset_capacity(struct gendisk *disk, sector_t size);

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

2.3.请求结构体request

在Linux块设备驱动中,使用request结构体来表征等待进行的I/O请求,这个结构体的定义如代码清单3所示。

代码清单3 request结构体

structrequest

{

struct list_head queuelist; /*链表结构*/

unsigned long flags; /* REQ_ */

sector_t sector; /*要传送输的下一个扇区 */

unsigned long nr_sectors; /*要传送的扇区数目*/

unsigned int current_nr_sectors; /*当前要传送的扇区数目*/

sector_t hard_sector; /*要完成的下一个扇区*/

unsigned long hard_nr_sectors; /*要被完成的扇区数目*/

unsigned int hard_cur_sectors; /*当前要被完成的扇区数目*/

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

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

void *elevator_private;

unsigned short ioprio;

int rq_status;

struct gendisk *rq_disk;

int errors;

unsigned long start_time;

unsigned short nr_phys_segments; /*请求在物理内存中占据的不连续的段的数目,scatter/gather列表的尺寸*/

unsigned short nr_hw_segments; /*与nr_phys_segments相同,但考虑了系统I/O
MMU的remap*/

int tag;

char *buffer; /*传送的缓冲,内核虚拟地址*/

int ref_count; /*引用计数*/

...

};

request结构体的主要成员包括:

sector_thard_sector;

unsignedlong hard_nr_sectors;

unsignedint hard_cur_sectors;

上述3个成员标识还未完成的扇区,hard_sector是第一个尚未传输的扇区,hard_nr_sectors是尚待完成的扇区数,hard_cur_sectors是当前I/O操作中待完成的扇区数。这些成员只用于内核块设备层,驱动不应当使用它们,如下所示:

sector_tsector;

unsignedlong nr_sectors;

unsignedint current_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。

代码清单4 request队列结构体

structrequest_queue

{

...

/*保护队列结构体的自旋锁 */

spinlock_t _ _queue_lock;

spinlock_t *queue_lock;

/*队列kobject */

struct kobject kobj;

/*队列设置 */

unsigned long nr_requests; /*最大的请求数量
*/

unsigned int nr_congestion_on;

unsigned int nr_congestion_off;

unsigned int nr_batching;

unsigned short max_sectors; /*最大的扇区数
*/

unsigned short max_hw_sectors;

unsigned short max_phys_segments; /*最大的段数
*/

unsigned short max_hw_segments;

unsigned short hardsect_size; /*硬件扇区尺寸
*/

unsigned int max_segment_size; /*最大的段尺寸
*/

unsigned long seg_boundary_mask; /*段边界掩码
*/

unsigned int dma_alignment; /* DMA传送的内存对齐限制
*/

struct blk_queue_tag *queue_tags;

atomic_t refcnt; /*引用计数 */

unsigned int in_flight;

unsigned int sg_timeout;

unsigned int sg_reserved_size;

int node;

struct list_head drain_list;

struct request *flush_rq;

unsigned char ordered;

};

请求队列跟踪等候的块I/O请求,它存储用于描述这个设备能够支持的请求的类型信息、它们的最大大小、多少不同的段可进入一个请求、硬件扇区大小、对齐要求等参数,其结果是:如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求。

请求队列还实现一个插入接口,这个接口允许使用多个I/O调度器,I/O调度器(也称电梯)的工作是以最优性能的方式向驱动提交I/O请求。大部分I/O调度器累积批量的I/O请求,并将它们排列为递增(或递减)的块索引顺序后提交给驱动。进行这些工作的原因在于,对于磁头而言,当给定顺序排列的请求时,可以使得磁盘顺序地从一头到另一头工作,非常像一个满载的电梯,在一个方向移动直到所有它的“请求”被满足。

另外,I/O调度器还负责合并邻近的请求,当一个新I/O请求被提交给调度器后,它会在队列里搜寻包含邻近扇区的请求。如果找到一个,并且如果结果的请求不是太大,调度器将合并这两个请求。

对磁盘等块设备进行I/O操作顺序的调度类似于电梯的原理,先服务完上楼的乘客,再服务下楼的乘客效率会更高,而顺序响应用户的请求则电梯会无序地忙乱。

Linux2.6内核包含4个I/O调度器,它们分别是Noop
I/O scheduler、Anticipatory I/O scheduler、Deadline I/Oscheduler与CFQ
I/O scheduler。

Noop I/O scheduler是一个简化的调度程序,它只作最基本的合并与排序。

Anticipatory I/O scheduler是当前内核中默认的I/O调度器,它拥有非常好的性能,在Linux2.5内核中它就相当引人注意。在与Linux2.4内核进行的对比测试中,在Linux2.4内核中多项以分钟为单位完成的任务,它则是以秒为单位来完成的,正因为如此它成为目前Linux2.6内核中默认的I/O调度器。Anticipatory
I/O scheduler的缺点是比较庞大与复杂,在一些特殊的情况下,特别是在数据吞吐量非常大的数据库系统中它会变得比较缓慢。

Deadline I/O scheduler是针对AnticipatoryI/O scheduler的缺点进行改善而来的,表现出的性能几乎与Anticipatory
I/Oscheduler一样好,但是比Anticipatory小巧。

CFQ I/O scheduler为系统内的所有任务分配相同的带宽,提供一个公平的工作环境,它比较适合桌面环境。事实上在测试中它也有不错的表现,mplayer、xmms等多媒体播放器与它配合的相当好,回放平滑,几乎没有因访问磁盘而出现的跳帧现象。

内核block目录中的noop-iosched.c、as-iosched.c、deadline-iosched.c和cfq-iosched.c文件分别实现了上述调度算法。

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); 2.6.30

blk_fetch_request() 2.6.31

上述函数用于返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL。elv_next_request()不会清除请求,它仍然将这个请求保留在队列上,但是标识它为活动的,这个标识将阻止I/O调度器合并其他的请求到已开始执行的请求。因为elv_next_request()不从队列里清除请求,因此连续调用它两次,两次会返回同一个请求结构体。

3.块设备驱动注册与注销

块设备驱动中的第一个工作通常是注册它们自己到内核,完成这个任务的函数是register_blkdev(),其原型为:

intregister_blkdev(unsigned int major, const char *name);

major参数是块设备要使用的主设备号,name为设备名,它会在cat
/proc/devices中被显示。如果major为0,内核会自动分配一个新的主设备号,register_blkdev()函数的返回值就是这个主设备号。如果register_blkdev()返回一个负值,表明发生了一个错误。

与register_blkdev()对应的注销函数是unregister_blkdev(),其原型为:

intunregister_blkdev(unsigned int major, const char *name);

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

值得一提的是,在Linux2.6内核中,对register_blkdev()的调用完全是可选的,register_blkdev()的功能已随时间正在减少,这个调用最多只完成两件事:①如果需要,分配一个动态主设备号。②在/proc/devices中创建一个入口。在将来的内核中,register_blkdev()可能会被去掉。但是目前的大部分驱动仍然调用它。

4.Linux块设备驱动的模块加载与卸载

在块设备驱动的模块加载函数中通常需要完成如下工作:①分配、初始化请求队列,绑定请求队列和请求函数。②分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。③注册块设备驱动register_blkdev()。

在块设备驱动的模块卸载函数中完成与模块加载函数相反的工作:①清除请求队列。②删除gendisk和对gendisk的引用。③删除对块设备的引用,注销块设备驱动。

最终代码:simp_blkdev.c

//为了便于讲解,一律没有使用宏

#include<linux/genhd.h>

#include<linux/fs.h>

#include<linux/blkdev.h>

#include<linux/types.h>

#include<linux/module.h>

unsignedchar array_data[16*1024*1024];//全局数组,表示本设备

structgendisk *my_gendisk;//全局变量,表示本设备

intmajor_num = -1;//全局变量,主设备号

structrequest_queue *my_request_queue = NULL;//全局变量,本设备关联的请求队列

staticDEFINE_SPINLOCK (lock);//全局变量,内核访问请求队列的自旋锁

/*对块设备的操作函数集合(函数指针集合)*/

structblock_device_operations my_operations =

{

.owner = THIS_MODULE,

};

staticvoid sbd_transfer(sector_t sector,unsigned long nsect, char *buffer, int write)

{

unsigned long offset = sector <<9;

unsigned long nbytes = nsect <<9;

if((offset + nbytes) > 16*1024*1024)

{

printk(KERN_NOTICE "sbd:Beyond-end write (%ld %ld)\n", offset, nbytes);

return;

}

if(write)

memcpy(array_data + offset,buffer, nbytes);

else

memcpy(buffer, array_data +offset, nbytes);

}

staticvoid my_process_on_request_queue(struct request_queue *q)

{

struct request *req;

req = blk_fetch_request(q);

while(req != NULL)

{

if(req->cmd_type !=REQ_TYPE_FS)

{

printk(KERN_NOTICE"Skip non-CMD request\n");

__blk_end_request_all(req,-EIO);

continue;

}

sbd_transfer(blk_rq_pos(req),blk_rq_cur_sectors(req),req->buffer, rq_data_dir(req));

if(!__blk_end_request_cur(req,0))

{

req =blk_fetch_request(q);

}

}

}

staticint __init my_init(void)

{

/*1、为代表本设备的结构体申请内存,这里的参数是本磁盘

使用的次设备号的数目,一经申请,这个数字就不可更改
*/

my_gendisk = alloc_disk(1);

if(NULL == my_gendisk)

{

printk(KERN_NOTICE"alloc gendisk failure\n");

return -ENOMEM;

}

/*2、申请主设备号,这个名字是出现在/proc/devices下显示的

(下面还有一个名字)事实证明的确如此251
simp_blkdev */

major_num = register_blkdev(0, "simp_blkdev");

if(major_num <= 0)

{

printk(KERN_WARNING "nomajor number\n");

/*申请主设备号失败的时候,驱动程序就无法正常

加载,要释放已经申请的资源,目前资源只有gendisk*/

goto major_alloc_error;

}

/*3、每个块设备关联了一个请求队列,我们这里申请一个,

需要注意的是:1)每个请求队列都对应一个”请求队列处

理函数“,这个是设备相关的。2)每次分配一个请求队列

的时候,必须提供一个自旋锁来控制对请求队列的访问,

我们先不考虑并发访问*/

//spin_lock_init (&lock);

my_request_queue = blk_init_queue(my_process_on_request_queue, &lock);

if(NULL == my_request_queue)

{

printk("request queueerror");

gotorequeue_alloc_error;

}

/*4、使用前面申请的资源为gendisk赋值*/

strcpy(my_gendisk->disk_name ,"simp_blkdev");/*本设备的名称,这个名称是出现在“/dev/”目录下的
*/

my_gendisk->major = major_num;

my_gendisk->first_minor = 0;

my_gendisk->queue =my_request_queue;

my_gendisk->fops =&my_operations;

/*设置块设备的大小,以扇区为单位,因为内核总是认为扇区大小是512B,所以用函数设定*/

set_capacity (my_gendisk,16*1024*2);

/*5、注册gendisk*/

add_disk (my_gendisk);

return 0;

requeue_alloc_error:

/*释放主设备号,和gendisk*/

unregister_blkdev(major_num, "simp_blkdev");

major_alloc_error:

/*释放gendisk*/

del_gendisk(my_gendisk);

return -ENOMEM;

}

staticvoid __exit my_exit(void)

{

blk_cleanup_queue (my_request_queue);

unregister_blkdev(major_num, "simp_blkdev");

del_gendisk(my_gendisk);

put_disk (my_gendisk);

}

module_init(my_init);

module_exit(my_exit);

MODULE_LICENSE("GPL");

Makefile

ifneq($(KERNELRELEASE),)

obj-m:=simp_blkdev.o

else

KERNELDIR ?= /usr/src/linux-headers-2.6.31-14-generic/

PWD := $(shell pwd)

default:

$(MAKE) -C $(KERNELDIR) M=$(PWD)modules

clean:

rm -rf *.o *~ core .depend .*.cmd *.ko*.mod.c

endif

编码到此结束,然后我们试试这个程序:

首先编译、加载模块:

#make

#insmodsimp_blkdev.ko

用lsmod查看,这里我们注意到,该模块的Used by为0,因为它既没有被其他模块使用,也没有被mount。

#lsmod

Module Size
Used by

simp_blkdev 16784008
0

如果当前系统支持udev,在调用add_disk()函数时即插即用机制会自动为我们在/dev目录下建立设备文件。

设备文件的名称为我们在gendisk.disk_name中设置的simp_blkdev,主、从设备号也是我们在程序中设定的251和0。

如果当前系统不支持udev,你需要自己用mknod /dev/simp_blkdev
b 251 0来创建设备文件了。

#ls-l /dev/simp_blkdev

brw-r-----1 root disk 72, 0 11-10 18:13 /dev/simp_blkdev

在块设备中创建文件系统,这里我们创建常用的ext3。当然,作为通用的块设备,创建其他类型的文件系统也没问题。

#mkfs.ext3/dev/simp_blkdev

mke2fs1.39 (29-May-2006)

Filesystemlabel=

OStype: Linux

Blocksize=1024 (log=0)

Fragmentsize=1024 (log=0)

4096inodes, 16384 blocks

819blocks (5.00%) reserved for the super user

Firstdata block=1

Maximumfilesystem blocks=16777216

2block groups

8192blocks per group, 8192 fragments per group

2048inodes per group

Superblockbackups stored on blocks:

8193

Writinginode tables: done

Creatingjournal (1024 blocks): done

Writingsuperblocks and filesystem accounting information: done

Thisfilesystem will be automatically checked every 38 mounts or

180days, whichever comes first. Use tune2fs-c or -i to override.

如果这是第一次使用,建议创建一个目录用来mount这个设备中的文件系统。

当然,这不是必需的。如果你对mount之类的用法很熟,你完全能够自己决定在这里干什么,甚至把这个设备mount成root。

#mkdir-p /mnt/temp1

把建立好文件系统的块设备mount到刚才建立的目录中

#mount/dev/simp_blkdev /mnt/temp1

看看现在的mount表

#mount

/dev/simp_blkdevon /mnt/temp1 type ext3 (rw)

看看现在的模块引用计数,从刚才的0变成1了,原因是我们mount了。

#lsmod

Module Size
Used by

simp_blkdev 16784008
1

看看文件系统的内容,有个mkfs时自动建立的lost+found目录。

#ls/mnt/temp1

lost+found

随便拷点东西进去

#cp/etc/init.d/* /mnt/temp1

再看看

#ls/mnt/temp1

acpid functions
irqbalance mdmpd rdisc
sendmail winbind

anacron gpm
kdump nfs readahead_early
setroubleshoot wpa_supplicant

apmd crond
haldaemon killall microcode_ctl nfslock
readahead_later

atd cups
halt krb524 multipathd
nscd restorecond

auditd hidd
kudzu netconsole ntpd
rhnsd smb

autofs dhcdbd
ip6tables lost+foundnetfs
pand rpcgssd sshd

bluetooth dund
ipmi netplugd pcscd rpcidmapd
syslog

conman firstboot
iptables mcstrans network
portmap rpcsvcgssd

cpuspeed frecord
irda psacct saslauthd
vncserver xinetd

现在这个块设备的使用情况是

#df

文件系统 1K-块 已用 可用 已用% 挂载点

/dev/simp_blkdev 15863
1440 13604 10% /mnt/temp1

查看程序的调度器信息:

#cat/sys/block/simp_blkdev/queue/scheduler

noopanticipatory deadline [cfq]

再全删了玩玩:

#rm-rf /mnt/temp1/*

看看删完了没有:

#ls/mnt/temp1

#

好了,大概玩够了,我们把文件系统umount掉:

#umount/mnt/temp1

模块的引用计数应该还原成0了吧:

#lsmod

Module Size
Used by

simp_blkdev 16784008
0

最后一步,移除模块:

#rmmodsimp_blkdev

blk_fetch_request函数说明:

Name:blk_fetch_request
— fetch a request from arequest queue

Synopsis:struct request
* fsfuncblk_fetch_request ( structrequest_queue * q);

Arguments:q— request
queue to fetch a request from

Description:Return
the request at the top of q. Therequest is started on return and LLD can start processing it immediately.

Return:Pointer to
the request at the top of q if available.Null otherwise.

Context:queue_lock
must be held.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: