Linux驱动 | 解读块设备驱动的重要概念

块设备驱动

块设备驱动比字符设备驱动要复杂得多,在 I/O操作上表现出极大的不同,

缓冲、I/O 调度、请求队列等都是与块设备驱动相关的概念。

字符设备与块设备I/O操作的不同:

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

2. 块设备对于I/O请求有对应的缓冲区,因此可调整顺序进行读写

3. 虽然块设备可以随机访问,但是顺序地组织块设备的访问可提高性能

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

尽管在块设备驱动中仍然存在 block_device_operations结构体及其成员函数,但其不再包含读写一类的成员函数,而只是包含打开、释放及 I/O控制等与具体读写无关的函数

 块设备驱动的结构相当复杂的,但幸运的是,块设备不像字符设备那么包罗万象,它通常就是存储设备,而且驱动的主体已经由 Linux内核提供,针对一个特定的硬件系统,驱动工程师所涉及的工作往往只是编写少量的与硬件直接交互的代码。

 

Linux块设备驱动结构

一、block_device_operations结构体

类似于file_operations

struct block_device_operations {

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

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

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

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;

};

主要的成员:

ioctl()

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

media_changed()

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

revalidate_disk()

使介质有效:被调用以响应一个介质改变,驱动进行必要的工作来使新介质准备好。

getgeo()

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

module

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

 

二、gendisk结构体

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

1. 定义

struct gendisk {

int major; /* major number of driver */

int first_minor;

int minors;                     /* maximum number of minors, =1 for

                                         * disks that can't be partitioned. */

char disk_name[32]; /* name of major driver */

struct hd_struct **part; /* [indexed by minor] */

int part_uevent_suppress;

struct block_device_operations *fops;

struct request_queue *queue;

void *private_data;

sector_t capacity;

 

int flags;

struct device *driverfs_dev;

struct kobject kobj;

struct kobject *holder_dir;

struct kobject *slave_dir;

 

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

struct work_struct async_notify;

};

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

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

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

capacity表示设备容量,以512Bytes为单位。

private_data用法与file->private_data类似。

 2. 操作gendisk的函数

(1)分配

struct gendisk *alloc_disk(int minors);

gendisk 结构体是一个动态分配的结构体,它需要特别的内核操作来初始化。minors参数是这个磁盘使用的次设备号的数量,一般也就是磁盘分区的数量,此后minors不能被修改。

(2)增加

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

void add_disk(struct gendisk *gd);

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

(3)释放

void del_gendisk(struct gendisk *gd);

(4)gendisk引用计数

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

(5)设置gendisk容量

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

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

虽然大多数块设备的扇区大小都是 512 字节,不过其他大小的扇区也很常见,比如,很多 CD-ROM 盘的扇区都是2KB。

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

 三、requestbio结构体

1.请求

使用request结构体来表示等待进行的I/O请求

struct request {

struct list_head queuelist;

struct list_head donelist;

 

request_queue_t *q;

 

unsigned int cmd_flags;

enum rq_cmd_type_bits cmd_type;

 

/* Maintain bio traversal state for part by part I/O submission.

 * hard_* are block layer internals, no driver should touch them!

 */

 

sector_t sector; /* next sector to submit */

sector_t hard_sector; /* next sector to complete */

unsigned long nr_sectors; /* no. of sectors left to submit */

unsigned long hard_nr_sectors; /* no. of sectors left to complete */

/* no. of sectors left to submit in the current segment */

unsigned int current_nr_sectors;

 

/* no. of sectors left to complete in the current segment */

unsigned int hard_cur_sectors;

 

struct bio *bio;

struct bio *biotail;

 

struct hlist_node hash; /* merge hash */

/*

 * The rb_node is only used inside the io scheduler, requests

 * are pruned when moved to the dispatch queue. So let the

 * completion_data share space with the rb_node.

 */

union {

struct rb_node rb_node; /* sort/lookup */

void *completion_data;

};

 

/*

 * two pointers are available for the IO schedulers, if they need

 * more they have to dynamically allocate it.

 */

void *elevator_private;

void *elevator_private2;

 

struct gendisk *rq_disk;

unsigned long start_time;

 

/* Number of scatter-gather DMA addr+len pairs after

 * physical address coalescing is performed.

 */

unsigned short nr_phys_segments;

 

/* Number of scatter-gather addr+len pairs after

 * physical and DMA remapping hardware coalescing is performed.

 * This is the number of scatter-gather entries the driver

 * will actually have to deal with after DMA mapping is done.

 */

unsigned short nr_hw_segments;

 

unsigned short ioprio;

 

void *special;

char *buffer;

 

int tag;

int errors;

 

int ref_count;

 

/*

 * when request is used as a packet command carrier

 */

unsigned int cmd_len;

unsigned char cmd[BLK_MAX_CDB];

 

unsigned int data_len;

unsigned int sense_len;

void *data;

void *sense;

 

unsigned int timeout;

int retries;

 

/*

 * completion callback.

 */

rq_end_io_fn *end_io;

void *end_io_data;

};

(1)标识未完成的扇区

sector_t hard_sector; //第一个尚未传输的扇区

unsigned long hard_nr_sectors; //是尚待完成的扇区数

unsigned int hard_cur_sectors; //当前 I/O操作中待完成的扇区数

(2)submit相关的成员

sector_t sector; /* next sector to submit */

unsigned long nr_sectors; /* no. of sectors left to submit */

unsigned int current_nr_sectors; /* no. of sectors left to complete in the current segment */

驱动中会经常与这 3 个成员打交道,这3个成员在内核和驱动交互中发挥着重大作用。它们以512字节大小为一个扇区,如果硬件的扇区大小不是512字节,则需要进行相应的调整。例如,如果硬件的扇区大小是2048字节,则在进行硬件操作之前,需要用4来除起始扇区号。

 (1)与(2)可认为是“副本”关系。

(3)bio

struct bio *bio;

bio 是这个请求中包含的 bio结构体的链表,驱动中不宜直接存取这个成员,而应该使用rq_for_each_bio()。

(4)buffer

char *buffer;

指向缓冲区的指针,数据应当被传送到或者来自这个缓冲区,这个指针是一个内

核虚拟地址,可被驱动直接引用。

(5)nr_phys_segment

unsigned short nr_phys_segments;

该值表示相邻的页被合并后,这个请求在物理内存中占据的段的数目。如果设备支持分散/聚集(SG,scatter/gather)操作,可依据此字段申请sizeof(scatterlist)* nr_phys_segments的内存,并使用下列函数进行 DMA映射:

int blk_rq_map_sg(request_queue_t) *q, struct request *req, struct scatterlist *sglist;

该函数与 dma_map_sg()类似,它返回scatterlist列表入口的数量。

(6)queue_list

struct list_head queuelist;

用于链接这个请求到请求队列的链表结构,blkdev_dequeue_request()可用于从队列中移除请求。

(7)从request中获得数据的传送方向

使用宏rq_data_dir(struct request *req);

0 返回值表示从设备中读,非 0返回值表示向设备写。

 

2.请求队列

一个块请求队列是一个块 I/O 请求的队列。

struct request_queue

{

/*

 * Together with queue_head for cacheline sharing

 */

struct list_head queue_head;

struct request *last_merge;

elevator_t *elevator;

 

/*

 * the queue request freelist, one for reads and one for writes

 */

struct request_list rq;

 

request_fn_proc *request_fn;

make_request_fn *make_request_fn;

prep_rq_fn *prep_rq_fn;

unplug_fn *unplug_fn;

merge_bvec_fn *merge_bvec_fn;

issue_flush_fn *issue_flush_fn;

prepare_flush_fn *prepare_flush_fn;

softirq_done_fn *softirq_done_fn;

 

/*

 * Dispatch queue sorting

 */

sector_t end_sector;

struct request *boundary_rq;

 

/*

 * Auto-unplugging state

 */

struct timer_list unplug_timer;

int unplug_thresh; /* After this many requests */

unsigned long unplug_delay; /* After this many jiffies */

struct work_struct unplug_work;

 

struct backing_dev_info backing_dev_info;

 

/*

 * The queue owner gets to use this for whatever they like.

 * ll_rw_blk doesn't touch it.

 */

void *queuedata;

 

/*

 * queue needs bounce pages for pages above this limit

 */

unsigned long bounce_pfn;

gfp_t bounce_gfp;

 

/*

 * various queue flags, see QUEUE_* below

 */

unsigned long queue_flags;

 

/*

 * protects queue structures from reentrancy. ->__queue_lock should

 * _never_ be used directly, it is queue private. always use

 * ->queue_lock.

 */

spinlock_t __queue_lock;

spinlock_t *queue_lock;

 

/*

 * queue kobject

 */

struct kobject kobj;

 

/*

 * queue settings

 */

unsigned long nr_requests; /* Max # of requests */

unsigned int nr_congestion_on;

unsigned int nr_congestion_off;

unsigned int nr_batching;

 

unsigned int max_sectors;

unsigned int 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;

 

struct blk_queue_tag *queue_tags;

 

unsigned int nr_sorted;

unsigned int in_flight;

 

/*

 * sg stuff

 */

unsigned int sg_timeout;

unsigned int sg_reserved_size;

int node;

#ifdef CONFIG_BLK_DEV_IO_TRACE

struct blk_trace *blk_trace;

#endif

/*

 * reserved for flush operations

 */

unsigned int ordered, next_ordered, ordseq;

int orderr, ordcolor;

struct request pre_flush_rq, bar_rq, post_flush_rq;

struct request *orig_bar_rq;

unsigned int bi_size;

 

struct mutex sysfs_lock;

};

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

请求队列还实现一个插入接口,这个接口允许使用多个 I/O调度器,I/O调度器(也称电梯)的工作是以最优性能的方式向驱动提交 I/O请求。大部分 I/O调度器累积批量的I/O请求,并将它们排列为递增(或递减)的块索引顺序后提交给驱动。

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

Linux 2.6 内核包含 4 个 I/O 调度器,它们分别是 No-op I/O scheduler、Anticipatory I/O scheduler、Deadline I/O scheduler 与 CFQ I/O scheduler。内核block目录中的noop-iosched.c、as-iosched.c、deadline-iosched.c和cfq-iosched.c文件分别实现了上述调度算法。可以通过给kernel添加启动参数,选择使用的IO调度算法,如:

kernel elevator=deadline


4 个 I/O 调度器各自的特点:

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

Anticipatory I/O scheduler 是当前内核中默认的 I/O调度器,它拥有非常好的性能。Anticipatory I/O scheduler的缺点是比较庞大与复杂,在一些特殊的情况下,特别是在数据吞吐量非常大的数据库系统中它会变得比较缓慢。

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

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


操作request_queue的函数

(1)初始化

request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);

该函数的第一个参数是请求处理函数的指针,第二个参数是控制访问队列权限的自旋锁,这个函数会发生内存分配的行为,它可能会失败,因此一定要检查它的返回值。这个函数一般在块设备驱动的模块加载函数中调用。

(2)清除

void blk_cleanup_queue(request_queue_t *q);

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

而 blk_put_queue()宏则定义为:

#define blk_put_queue(q) blk_cleanup_queue((q))

(3)分配

request_queue_t *blk_alloc_queue(int gfp_mask);

对于 Flash、RAM盘等完全随机访问的非机械设备,并不需要进行复杂的 I/O调度,应该使用上述函数分配一个“请求队列”,并使用如下函数来绑定请求队列和“制造请求”函数。

这种方式分配的“请求队列”实际上不包含任何请求。

void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn);

(4)提取请求

struct request *elv_next_request(request_queue_t *queue);

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

(5)去除请求

void blkdev_dequeue_request(struct request *req);

从队列中去除一个请求。如果驱动中同时从同一个队列中操作了多个请求,它必须以这样的方式将它们从队列中去除。

如果需要将一个已经出列的请求归还到队列中,可以调用:

void elv_requeue_request(request_queue_t *queue, struct request *req);

 6)启停请求队列

void blk_stop_queue(request_queue_t *queue);

void blk_start_queue(request_queue_t *queue);

如果块设备到达不能处理等候的命令的状态,应调用 blk_stop_queue()来告知块设备层。之后,请求函数将不被调用,除非再次调用blk_start_queue()将设备恢复到可处理请求的状态。

(7)参数设置

void blk_queue_max_sectors(request_queue_t *queue, unsigned short max);

void blk_queue_max_phys_segments(request_queue_t *queue, unsigned short max);

void blk_queue_max_hw_segments(request_queue_t *queue, unsigned short max);

void blk_queue_max_segment_size(request_queue_t *queue, unsigned int max);

blk_queue_max_sectors()描述任一请求可包含的最大扇区数,默认值为 255。

blk_queue_max_phys_segments() 和blk_queue_max_hw_segments()都控制一个请求中可包含的最大物理段(系统内存中不相邻的区),blk_queue_max_hw_segments()考虑了系统I/O内存管理单元的重映射,这两个参数缺省都是128。

blk_queue_max_segment_size 告知内核请求段的最大字节数,缺省值为 65,536。

(8)通告内核

void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);

该函数用于告知内核块设备执行DMA时可使用的最高物理地址dma_addr,如果一个请求包含超出这个限制的内存引用,系统将给这个操作分配一个“反弹”缓冲区。这种方式代价昂贵,应尽量避免。

可 以 给 dma_addr 参 数 提 供 任 何 可 能 的 值 或 使 用 预 先 定 义 的 宏 , 如BLK_BOUNCE_HIGH(对高端内存页使用反弹缓冲区)、BLK_BOUNCE_ISA(驱动只可在16MB的ISA区执行DMA)或者BLK_BOUCE_ANY(驱动可在任何地址执行DMA),缺省值是BLK_BOUNCE_HIGH。

 voidblk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);

如果我们正在驱动编写的设备无法处理跨越一个特殊大小内存边界的请求,应该使用这个函数来告知内核这个边界。例如,如果设备处理跨 4MB边界的请求有困难,应该传递一个0x3fffff掩码,缺省的掩码是0xffffffff(对应4GB边界)。

 voidblk_queue_dma_alignment(request_queue_t *queue, int mask);

告知内核块设备施加于 DMA 传送的内存对齐限制,所有请求都匹配这个对齐,缺省的屏蔽是 0x1ff,它导致所有的请求被对齐到512字节边界。

 void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);

该函数告知内核块设备硬件扇区的大小,所有由内核产生的请求都是这个大小的倍数并且被正确对界。但是,内核块设备层和驱动之间的通信还是以 512字节扇区为单位进行。

 3.I/O

通常一个 bio 对应一个I/O请求。I/O调度算法可将连续的bio合并成一个请求。所以,一个请求可以包含多个bio。

struct bio {

sector_t bi_sector; /* device address in 512 byte

   sectors */

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,

 * top bits priority

 */

 

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

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

 

/* Number of segments in this BIO after

 * physical address coalescing is performed.

 */

unsigned short bi_phys_segments;

 

/* Number of segments after physical and DMA remapping

 * hardware coalescing is performed.

 */

unsigned short bi_hw_segments;

 

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

 

/*

 * To keep track of the max hw size, we account for the

 * sizes of the first and last virtually mergeable segments

 * in this bio

 */

unsigned int bi_hw_front_size;

unsigned int bi_hw_back_size;

 

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

 

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

 

bio_end_io_t *bi_end_io;

atomic_t bi_cnt; /* pin count */

 

void *bi_private;

 

bio_destructor_t *bi_destructor; /* destructor */

};

(1)sector_t bi_sector;

标识这个 bio 要传送的第一个(512字节)扇区。

(2)unsigned int bi_size;

被传送的数据大小,以字节为单位,驱动中可以使用bio_sectors(bio)宏获得以扇区为单位的大小。

(3)unsigned long bi_flags;

一组描述 bio 的标志,如果这是一个写请求,最低有效位被置位,可以使用bio_data_dir(bio)宏来获得读写方向。

(4)unsigned short bio_phys_segments;

(5)unsigned short bio_hw_segments;

分别表示包含在这个 BIO 中要处理的不连续的物理内存段的数目和考虑 DMA 重映像后的不连续的内存段的数目。

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

struct bio_vec {

struct page *bv_page;

unsigned int bv_len;

unsigned int bv_offset;

};

我们不应该直接访问 bio 的 bio_vec 成员,而应该使用bio_for_each_segment()

来进行这项工作,可以用这个宏循环遍历整个 bio 中的每个段.

#define _ _bio_for_each_segment(bvl, bio, i, start_idx)  \

for (bvl = bio_iovec_idx((bio), (start_idx)), i = (start_idx); i < (bio)->bi_vcnt; bvl++, i++)

 

 #define bio_for_each_segment(bvl, bio, i) \

_ _bio_for_each_segment(bvl, bio, i, (bio)->bi_idx

操作bio的函数

(1)intbio_data_dir(struct bio *bio);

这个函数可用于获得数据传输的方向是 READ 还是 WRITE。

(2)struct page *bio_page(struct bio *bio) ;

这个函数可用于获得目前的页指针。

(3)intbio_offset(struct bio *bio) ;

这个函数返回操作对应的当前页内的偏移,通常块 I/O 操作本身就是页对齐的。

(4)intbio_cur_sectors(struct bio *bio) ;

这个函数返回当前 bio_vec 要传输的扇区数。

(5)char *bio_data(struct bio *bio) ;

这个函数返回数据缓冲区的内核虚拟地址。

(6)char *bvec_kmap_irq(struct bio_vec *bvec, unsigned long *flags) ;

这个函数返回一个内核虚拟地址,这个地址可用于存取被给定的 bio_vec 入口指向的数据缓冲区。它也会屏蔽中断并返回一个原子 kmap,因此,在bvec_kunmap_irq()被调用以前,驱动不应该睡眠。

(7)voidbvec_kunmap_irq(char *buffer, unsigned long *flags);

这个函数是 bvec_kmap_irq()函数的“反函数”,它撤销bvec_kmap_irq()创建的映射。

(8)char *bio_kmap_irq(struct bio *bio, unsigned long *flags);

这个函数是对 bvec_kmap_irq()的包装,它返回给定的bio的当前bio_vec入口的映射。

(9)char *_ _bio_kmap_atomic(struct bio *bio, int i, enum km_type type);

这个函数通过 kmap_atomic()获得返回给定bio的第i个缓冲区的虚拟地址。

(10)void_ _bio_kunmap_atomic(char *addr, enum km_type type);

这个函数返还由_ _bio_kmap_atomic()获得的内核虚拟地址。

(11) voidbio_get(struct bio *bio); //引用bio

void bio_put(struct bio *bio); //释放对bio的引用

实现对bio的引用计数。

 四、块设备驱动的注册与注销

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

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

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

注销函数是 unregister_blkdev(),其原型为:

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

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

 值得一提的是,在 Linux 2.6内核中,对register_blkdev()的调用完全是可选的register_blkdev()的功能已随时间正在减少,这个调用最多只完成两件事。

① 如果需要,分配一个动态主设备号。

② 在/proc/devices 中创建一个入口。

在将来的内核中,register_blkdev()可能会被去掉。但是目前的大部分驱动仍然调用它。

xxx_major = register_blkdev(xxx_major, "xxx");

if (xxx_major <= 0) //注册失败

{

printk(KERN_WARNING "xxx: unable to get major number\n");

return -EBUSY;

}

 模块的加载与卸载

一、加载

块设备驱动的加载函数通常完成以下工作:

(1)分配、初始化请求队列,绑定“请求队列”和“制造请求的函数”

(2)分配、初始化gendisk,给major, fops, queue等成员赋值,最后添加gendisk

(3) 注册块设备驱动

两个模板

(1) 使用blk_alloc_queue()分配队列,然后使用blk_queue_make_request()绑定“请求队列”和“制造请求的函数”

static int __init xxx_init(void)

{

/*分配gendisk*/

xxx_disks = alloc_disk(1);

if(!xxx_disks)

{

goto out;

}

/*注册块设备*/

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 = 0;

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以512B为单位

add_disk(xxx_disks); //添加gendisk

return 0;

out_queue:

unregister_blkdev(XXX_MAJOR, "xxx");

out:

put_disk(xxx_disks);

blk_cleanup_queue(xxx_queue);

return -ENOMEM;

} 

(2)使用blk_init_queue()初始化请求队列

(这里的“请求队列”实际不包含任何请求,适用于RAM盘这类可真正随机访问的块设备,后面会讨论它)

static int __init xxx_init(void)

{

xxx_disks = alloc_disk(1);

if(!xxx_disks)

{

goto out;

}

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

xxx_disk->major = XXX_MAJOR;

xxx_disk->first_minor = 0;

xxx_disk->fops = &xxx_op;

xxx_disk->queue = xxx_queue;

set_capacity(xxx_disks, xxx_size);

add_disk(xxx_disks);

return 0;

out_queue:

unregister_blkdev(XXX_MAJOR, "xxx");

out:

put_disk(xxx_disks);

blk_cleanup_queue(xxx_queue);

return -ENOMEM;

}

二、卸载

卸载函数通常完成以下工作:

(1)清除请求队列

(2)删除gendisk和对gendisk的引用

(3)删除对块设备的引用,注销块设备

模板:

static void __exit xxx_exit(void)

{

if(bdev)

{

invalidate_bdev(xxx_bdev, 1);

blkdev_put(xxx_bdev);

}

del_gendisk(xxx_disks);

put_disk(xxx_disks);

blk_cleanup_queue(xxx_queue[i]);

unregister_blkdev(XXX_MAJOR, "xxx");

}

块设备的打开与释放

块设备驱动的 open()和release()函数并非是必须的,一个简单的块设备驱动可以不提供open()和release()函数。

块设备驱动的 open()函数和其字符设备驱动的对等体非常类似,都以相关的inode和file结构体指针作为参数。当一个节点引用一个块设备时,inode->i_bdev->bd_disk包含一个指向关联gendisk结构体的指针。因此,类似于字符设备驱动,我们也可以将gendisk的private_data赋给file的private_data,private_data同样最好是指向描述该设备的设备结构体xxx_dev的指针。

 static int xxx_open(struct inode *inode, struct file *filp)

{

 struct xxx_dev *dev = inode->i_bdev->bd_disk->private_data;

 filp->private_data = dev; //赋值 file的private_data

.......

 return 0;

}

在一个处理真实的硬件设备的驱动中,open()和release()方法还应当设置驱动和硬件的状态,这些工作可能包括启停磁盘、加锁一个可移出设备和分配 DMA缓冲等。

块设备驱动的ioctl()函数

与字符设备驱动一样,块设备可以包含一个 ioctl()函数以提供对设备的I/O控制能力。实际上,高层的块设备层代码处理了绝大多数ioctl(),因此,具体的块设备驱动中通常不再需要实现很多ioctl命令。

模板:实现HDIO_GETGEO(获得磁盘的几何信息,CHS:Cylinder、Head、Sector/Track)

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

{

long size;

struct hd_geometry geo;

struct xxx_dev *dev = filp->private_data;

switch(cmd)

{

case HDIO_GETGEO:

size = dev->size * (hardsect_size / KERNEL_SECTOR_SIZE);

geo.cylinders = (size & ~0x3f) >> 6;

geo.heads = 4;

geo.sectors = 16;

geo.start = 4;

if(copy_to_user(void __user *)arg, &geo, sizeof(geo))

{

return -EFAULT;

}

return 0;

}

return -ENOTTY;

}

块设备驱动的I/O请求处理

一、使用请求队列

块设备驱动请求函数的原型为:

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, 0);

continue; //不是文件系统请求,跳出本次循环执行下一次

}

xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer, rq_data_dir(req));

end_request(req, 1);

}

}

 

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(KERNEL_NOTICE "Beyond-end write (%ld %ld)\n", offset, nbytes);

return;

}

if(write)

{

write_dev(offset, buffer, nbytes);

}

else

{

read_dev(offset, buffer, nbytes);

}

}

elc_next_request()获得队列中第一个未完成的请求;end_request()将请求从队列中剥离,其第二个参数的意义为:1-请求处理成功,0-请求处理失败。函数首先判断是否为文件系统请求,不是则直接清除。

end_request()源码如下:

void end_request(struct request *req, int uptodate)

{

if(!end_that_request_first(req, uptodate, req->hard_cur_sectors)

{

add_disk_randomness(req->rq_disk);

blkdev_derequeue_request(req);

end_that_requeue_last(req);

}

}

(1)end_that_request_first();

通告块设备层,设备已完成一个I/O请求的部分或全部扇区的传输。

参数count - 已完成传送的扇区数量。

返回值:指示该请求的所有扇区是否都被传送;0-扇区都被传送,且该请求已完成。

(2)add_disk_randomness();

使用块 I/O请求的定时来给系统的随机数池贡献熵,它不影响块设备驱动。但是,仅当磁盘的操作时间是真正随机的时候(大部分机械设备如此),才应该调用它。

(3)blkdev_derequeue_request;

从队列中清除请求。

(4)end_that_request_last();

end_that_requeue_last:通知所有正在等待这个请求完成的对象请求已经完成并回收这个请求结构体。


2. 一个较复杂的请求函数:进行3层遍历——遍历队列中的每个请求,遍历请求中的每个bio,遍历每个bio的每个段

static void xxx_full_request(request_queue_t *q)

{

struct request *request;

int sectors_xferred;

struct xxx_dev *dev = q->queuedata;

/*遍历每个请求*/

while((req = elv_next_request(q)) != NULL)

{

if(!blk_fs_request(req))

{

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

end_request(req, 0);

continue;

}

sectors_xferred = xxx_xfer_request(dev, req);

if(!end_that_request_first(req, 1, sectors_xferred))

{

blkdev_dequeue_request(req);

end_that_request_last(req);

}

}

}

 

/*处理请求*/

static int xxx_xfer_request(struct xxx_dev *dev, struct request *req)

{

struct bio *bio;

int nsect = 0;

/*遍历请求中的每个bio*/

rq_for_each_bio(bio, req)

{

xxx_xfer_bio(dev, bio);

nsect += bio->bio_size / KERN_SECTOR_SIZE;

}

return nsect;

}

 

/*处理bio*/

static int xxx_xfer_bio(struct xxx_dev *dev, struct bio *bio)

{

int i;

struct bio_vec *bvec;

sector_t sector = bio->bi-sector;

/*遍历每一段*/

bio_for_each_segment(bvec,bio, i)

{

char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);

xxx_fransfer(dev, sector, bio_cur_sectors(bio), buffer, bio_data_dir(bio) == WRITE);

sector += bio_cur_sectors(bio);

__bio_kunmap_atomic(bio, KM_USER0);

}

return 0;

}

二、不使用请求队列

使用请求队列对于一个机械的磁盘设备而言的确有助于提高系统的性能,但是对于许多块设备,如数码相机的存储卡、RAM盘等完全可真正随机访问的设备而言,无法从高级的请求队列逻辑中获益。

对于这些设备,块层支持“无队列”的操作模式,为了使用这个模式,驱动必须提供一个“制造请求”函数,而不是一个请求函数,“制造请求”函数的原型为:

typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);

上述函数的第一个参数仍然是“请求队列”,但是这个“请求队列”实际不包含任何请求。因此,“制造请求”函数的主要参数是bio结构体,这个bio结构体表示一个或多个要传送的缓冲区。“制造请求”函数或者直接进行传输,或者把请求重定向给其他设备。

“制造请求”函数例程

static int xxx_make_request(request_queue_t *q, struct bio *bio)

{

struct xxx_dev *dev = q->queuedata;

int status;

status = xxx_xfer_bio(dev, bio);

bio_endbio(bio, bio->bi_size, status);

return 0;

}

不管对应的 I/O 处理成功与否,“制造请求”函数都应该返回 0。如果“制造请求”函数返回一个非零值,bio 将被再次提交。

bio处理完成后,应使用bio_endio()函数通知处理结果:

void bio_endio(struct bio *bio, unsigned int bytes, int error);


你可能感兴趣的:(ARM-Linux,随笔)