在此首先对比一下之前经常打交道的字符型驱动设备。二者的区别主要可以概括为支不支持随机访问,字符驱动设备如键盘,执行读命令只能获取一个按照顺序的字符流,如“linux”,对这个字符流随机的访问是没有意义的;但是块驱动设备最大的不同是,它可以支持用户程序随机的访问,比如访问nandflash的某个扇区等操作。
写一个字符型驱动涉笔的大致套路就是,首先确定该字符型驱动设备的设备号,随后,在这个设备号下,建立一个file_operation结构体,逐个实现结构体中的系统调用函数,如read,write,open,ioctl等。最后一步是将这个结构体注册,可以理解为将这个结构体指针存放在该 数组[设备号] 的位置。讲究一点的话,可以将这些基本的内容融入一些框架之中,如输入输出子系统、device-bus-driver、framebuffer等。
在获取数据方面,常用的有:
1)查询方式,在用户空间中不断调用read函数查询是否有数据获取。
2)中断方式,数据准备好后内核中产生中断,异步IO的典型。
3)多路复用IO,如poll机制,epoll、select、poll函数,当数据没有准备好的时候,内核中该进程处于休眠状态,当数据准备好了,函数返回,通知用户读取数据。
4)信号量的方式,在没有数据的时候,用户空间该线程处于挂起状态,等待该信号量;数据准备完成,内核发出信号量同步两个进程,用户再去读取数据。
块驱动程序编写有什么不同?
块驱动程序操作慢在哪里?慢在负责两个扇区跳转的机械装置上。换句话说,读取一个扇区里面的内容很快,但是读取完之后去读另外一个扇区就很慢。所以,为了节约时间,常用的方法是合并和排序。因此,块驱动设备需要将读写命令插入队列,在统筹安排读写。因此,字符型驱动设备那一套就不行了。
块驱动设备框架
文件系统的作用是将文件的读写转化成扇区的读写,这其中还涉及到虚拟文件系统VFS,以将用户空间的命令转化为各种文件系统的命令,这里不是重点。
此处的重点是研究ll_rw_block函数的调用关系,这个函数实现了从请求插入队列到执行队列中请求的一系列操作。
void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
{
int i;
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
if (rw == SWRITE)
lock_buffer(bh);
else if (test_set_buffer_locked(bh))
continue;
if (rw == WRITE || rw == SWRITE) {
if (test_clear_buffer_dirty(bh)) {
bh->b_end_io = end_buffer_write_sync;
get_bh(bh);
submit_bh(WRITE, bh);
continue;
}
} else {
if (!buffer_uptodate(bh)) {
bh->b_end_io = end_buffer_read_sync;
get_bh(bh);
submit_bh(rw, bh);
continue;
}
}
unlock_buffer(bh);
}
}
这里首先说明个概念,就是缓冲区头buffer_head,它描述了内存中一块区域。这块区域相当于磁盘到内存的映射,可能包含一个或若干个块。从bh中,你可以知道这个内存与哪个块设备有关。
struct buffer_head {
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head *b_this_page;/* circular list of page's buffers */
struct page *b_page; /* the page this bh is mapped to */
sector_t b_blocknr; /* start block number */
size_t b_size; /* size of mapping */
char *b_data; /* pointer to data within the page */
struct block_device *b_bdev;
bh_end_io_t *b_end_io; /* I/O completion */
void *b_private; /* reserved for b_end_io */
struct list_head b_assoc_buffers; /* associated with another mapping */
struct address_space *b_assoc_map; /* mapping this buffer is
associated with */
atomic_t b_count; /* users using this buffer_head */
};
在2.6内核之前,这个结构体承担着相当重要的作用,在2.6之后的内核中,其管理块设备请求的作用被struct bio结构体所替代。原因是,这个bio结构体可以管理一个块设备的全部缓冲区,相比较前者bio更加轻量化。但是二者又是不可完全替代的,前者管理一个缓冲区的全部信息,而后者则管理一个块设备的io操作。
通过submit_bh(WRITE, bh);
完成缓冲区头bh到bio的转换。将bio赋值完成之后,调用submit_bio(rw, bio);
实现“submit a bio to the block device layer for I/O”。
这里有一个问题,在bio结构体中,明明可以一次性的管理多个缓冲区/缓冲区头,为什么bio结构体在实际编程的时候只是填充了一个缓冲区头?
答案在submit_bio(rw, bio);
中,调用generic_make_request(bio)函数:
“We only want one ->make_request_fn to be active at a time,
else stack usage with stacked devices could be a problem.”
多于一个bio有可能会引起栈的问题。在这个函数中用于向队列中添加请求__generic_make_request(bio);
。
在这个函数中执行:1.q = bdev_get_queue(bio->bi_bdev);
获取队列。
2.ret = q->make_request_fn(q, bio);
向队列中添加请求。
在q->make_request_fn(q, bio);
中发生了什么?
el_ret = elv_merge(q, &req, bio);
/*获取linus电梯参数:向前合并?向后合并?*/
switch (el_ret) {
...
}
init_request_from_bio(req, bio);
/*初始化命令:这个命令应该是请求队列中的成员*/
...
add_request(q, req);
/*将命令添加到请求队列*/
调用关系:
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
submit_bh(rw, bh);
struct bio *bio; // 使用bh来构造bio (block input/output)
submit_bio(rw, bio);
// 通用的构造请求: 使用bio来构造请求(request)
generic_make_request(bio);
__generic_make_request(bio);
request_queue_t *q = bdev_get_queue(bio->bi_bdev); // 找到队列
// 调用队列的"构造请求函数"
ret = q->make_request_fn(q, bio);
// 默认的函数是__make_request
__make_request
// 先尝试合并
elv_merge(q, &req, bio);
// 如果合并不成,使用bio构造请求
init_request_from_bio(req, bio);
// 把请求放入队列
add_request(q, req);
// 执行队列
__generic_unplug_device(q);
// 调用队列的"处理函数"
q->request_fn(q);
如何写一个块驱动程序:
怎么写块设备驱动程序呢?
1. 分配gendisk: alloc_disk
2. 设置
2.1 分配/设置队列: request_queue_t // 它提供读写能力
blk_init_queue
2.2 设置gendisk其他信息 // 它提供属性: 比如容量
3. 注册: add_disk
参考:
drivers\block\xd.c
drivers\block\z2ram.c
写一个虚拟块驱动设备
块驱动设备结构体gendisk :
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;
};
首先是初始化,其实可以理解为对块驱动设备结构体ramblock_disk 的填充和注册:
static int ramblock_init(void)
{
/* 1. 分配一个gendisk结构体 */
ramblock_disk = alloc_disk(16); /* 次设备号个数: 分区个数+1 */
/* 2. 设置 */
/* 2.1 分配/设置队列: 提供读写能力 */
ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock);
ramblock_disk->queue = ramblock_queue;
/* 2.2 设置其他属性: 比如容量 */
major = register_blkdev(0, "ramblock"); /* cat /proc/devices */
ramblock_disk->major = major;
ramblock_disk->first_minor = 0;
sprintf(ramblock_disk->disk_name, "ramblock");
ramblock_disk->fops = &ramblock_fops;
set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);
/* 3. 硬件相关操作 */
ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);
/* 4. 注册 */
add_disk(ramblock_disk);
return 0;
}
- 内核中默认一个扇区是512k;
- kzalloc()申请内存并把内存设置为0;
实现do_ramblock_request函数,当有IO请求下达不急于返回数据,先执行下面的函数。
static void do_ramblock_request(request_queue_t * q)
{
static int r_cnt = 0;
static int w_cnt = 0;
struct request *req;
//printk("do_ramblock_request %d\n", ++cnt);
while ((req = elv_next_request(q)) != NULL) {
/* 数据传输三要素: 源,目的,长度 */
/* 源/目的: */
unsigned long offset = req->sector * 512;
/* 目的/源: */
// req->buffer
/* 长度: */
unsigned long len = req->current_nr_sectors * 512;
if (rq_data_dir(req) == READ)
{
printk("do_ramblock_request read %d\n", ++r_cnt);
memcpy(req->buffer, ramblock_buf+offset, len);
}
else
{
printk("do_ramblock_request write %d\n", ++w_cnt);
memcpy(ramblock_buf+offset, req->buffer, len);
}
end_request(req, 1);
}
}
由于内核中已经实现了linus电梯法等IO调度算法,该函数进负责执行请求队列中的命令。由于在这里采用的是RAM模拟磁盘,所以并不涉及硬件操作,仅仅是判断读写,随后将一段内存复制到另一段内存中。
最后还要实现退出函数,主要是将之前申请的资源全部归还/注销。
static void ramblock_exit(void)
{
unregister_blkdev(major, "ramblock");
del_gendisk(ramblock_disk);
put_disk(ramblock_disk);
blk_cleanup_queue(ramblock_queue);
kfree(ramblock_buf);
}