写文章的目的是想通过记录自己的学习过程,以便以后使用到相关的知识点可以回顾和参考。
.
linux 内核使用 block_device 表示块设备, block_device 为 一 个 结 构 体 , 定 义 在 include/linux/fs.h 文件中,结构体内容如下:
struct block_device { dev_t bd_dev; /* not a kdev_t - it's a search key */ int bd_openers; struct inode * bd_inode; /* will die */ struct super_block * bd_super; struct mutex bd_mutex; /* open/close mutex */ struct list_head bd_inodes; void * bd_claiming; void * bd_holder; int bd_holders; bool bd_write_holder; #ifdef CONFIG_SYSFS struct list_head bd_holder_disks; #endif struct block_device * bd_contains; unsigned bd_block_size; struct hd_struct * bd_part; /* number of times partitions within this device have been opened. */ unsigned bd_part_count; int bd_invalidated; struct gendisk * bd_disk; /* 描述一个磁盘的结构体gendisk */ struct request_queue * bd_queue; ........ };
对于 block_device 结构体,重点关注一下 bd_disk 成员变量,此成员变量为
gendisk 结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。
. 注册块设备
和字符设备驱动一样,需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:
int register_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:主设备号。
name:块设备名字。
返回值:如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。
. 注销块设备
如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,函数原型如下:
void unregister_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:要注销的块设备主设备号。
name:要注销的块设备名字。
返回值:无。
.
linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,定义在 include/linux/genhd.h中,内容如下:
struct gendisk { /* major, first_minor and minors are input parameters only, * don't use directly. Use disk_devt() and disk_max_parts(). */ /* 磁盘设备的主设备号 */ 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[DISK_NAME_LEN]; /* name of major driver */ char *(*devnode)(struct gendisk *gd, umode_t *mode); unsigned int events; /* supported events */ unsigned int async_events; /* async events, subset of all */ /* Array of pointers to partitions indexed by partno. * Protected with matching bdev lock but stat and other * non-critical accesses use RCU. Always access through * helpers. */ struct disk_part_tbl __rcu *part_tbl; /* 磁盘对应的分区表 */ struct hd_struct part0; const struct block_device_operations *fops; /* 块设备操作集 */ struct request_queue *queue; /* 磁盘对应的请求队列 */ void *private_data; /* 私有数据 */ ....... };
里面比较重要的成员,我已中文注释在代码中。编写块的设备驱动的时候需要分配并初始化一个 gendisk,linux 内核提供了一组 gendisk 操作函数,常用的 API 函数如下面这些。
. 申请 gendisk
使用 gendisk 之前要先申请,allo_disk 函数用于申请一个 gendisk,函数原型如下:
struct gendisk *alloc_disk(int minors)
函数参数和返回值含义如下:
minors:次设备号数量,也就是 gendisk 对应的分区数量。
返回值:成功:返回申请到的 gendisk,失败:NULL。
. 删除 gendisk
如果要删除 gendisk 的话可以使用函数 del_gendisk,函数原型如下:
void del_gendisk(struct gendisk *gp)
函数参数和返回值含义如下:
gp:要删除的 gendisk。
返回值:无。
. 将 gendisk 添加到内核
使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的gendisk 添加到内核中,add_disk 函数原型如下:
void add_disk(struct gendisk *disk)
函数参数和返回值含义如下:
disk:要添加到内核的 gendisk。
返回值:无。
. 设置 gendisk 容量
每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set_capacity,函数原型如下:
void set_capacity(struct gendisk *disk, sector_t size)
函数参数和返回值含义如下:
disk:要设置容量的 gendisk。
size:磁盘容量大小,注意这里是扇区数量。
返回值:无。
. 调整 gendisk 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道,get_disk 是增加 gendisk 的引用计数,put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:
truct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)
.
和字符设备的 file _operations 一样,块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h 中,结构体内容如下:
struct block_device_operations { int (*open) (struct block_device *, fmode_t); int (*release) (struct gendisk *, fmode_t); int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); int (*direct_access) (struct block_device *, sector_t, void **, unsigned long *); unsigned int (*check_events) (struct gendisk *disk, unsigned int clearing); /* ->media_changed() is DEPRECATED, use ->check_events() instead */ int (*media_changed) (struct gendisk *); void (*unlock_native_capacity) (struct gendisk *); int (*revalidate_disk) (struct gendisk *); int (*getgeo)(struct block_device *, struct hd_geometry *); /* this callback is with swap_lock and sometimes page table lock held */ void (*swap_slot_free_notify) (struct block_device *, unsigned long); struct module *owner; };
可以看出,block_device_operations 结构体里面的操作集函数和字符设备的 file_operations 操作集基本类似,不过块设备中的读写操作不再通过read,write操作函数来完成,因此在上面找不到这两个函数指针。除了open,release,ioctl外,里面有一个叫 getgeo 的也要了解下,接下来的驱动编写实践会用到,它用于获取磁盘信息,包括磁头、柱面和扇区等信息,其中有个参数
struct hd_geometry *
,它是 hd_geometry 结构体类型的指针,定义在 include/linux/hdreg.h 文件中,内容如下:
/* 描述一个磁盘的结构 */ struct hd_geometry { unsigned char heads; /* 磁头 */ unsigned char sectors; /* 一条磁道的扇区数 */ unsigned short cylinders; /* 柱面,即磁道数 */ unsigned long start; };
.
在上面讲到 block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引处理块设备驱动中非常重要的 request_queue、request 和 bio。
它们之间的关系如下图所示:
内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的 request(请求结构体),而 request 又包含了 bio,bio 包含的bvec_iter、bio_vec 结构体描述了该I/O请求的开始扇区、数据方向(读、写)、数据放入的页,即bio保存了读写相关数据。bio相关代码定义如下:
. bio结构体
struct bio { struct bio *bi_next; /* 请求队列的下一个 bio */ struct block_device *bi_bdev; /* 指向块设备 */ unsigned long bi_flags; /* bio 状态等信息 */ unsigned long bi_rw; /* I/O 操作,读或写 */ struct bvec_iter bi_iter; /* I/O 操作,读或写 */ ....... struct bio_vec *bi_io_vec; /* bio_vec 列表 */ struct bio_set *bi_pool; struct bio_vec bi_inline_vecs[0]; };
. bvec_iter结构体
struct bvec_iter { sector_t bi_sector; /* I/O 请求的设备起始扇区(512 字节) */ unsigned int bi_size; /* 剩余的 I/O 数量 */ unsigned int bi_idx; /* blv_vec 中当前索引 */ unsigned int bi_bvec_done; /* 当前 bvec 中已经处理完成的字节数 */ };
. bio_vec结构体
struct bio_vec { struct page *bv_page; /* 页 */ unsigned int bv_len; /* 长度 */ unsigned int bv_offset; /* 偏移 */ };
我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi_io_vec 指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度。bio、bvec_iter 以及 bio_vec 这三个结构体之间的关系如下图所示:
从上面知道了 request_queue、request 和 bio 这三者之间的关系,下面来看一下内核提供的涉及处理 request_queue、request 和 bio 的API函数。
1、初始化请求队列
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
函数参数和返回值含义如下:
rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数需要驱动编写人员自行实现。
lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。
返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。
2、清除请求队列
void blk_cleanup_queue(struct request_queue *q)
3、分配请求队列
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
对于 ramdisk 这种完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,可以直接 “踢开” I/O调度器,使用如下函数来绑定请求队列和 “制造请求” 函数(make_request_fn)。
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
通常 blk_cleanup_queue() 和 blk_queue_make_request() 都是结合起来使用的,逻辑一般是:
xxx_queue = blk_cleanup_queue(GFP_KERNEL);
blk_queue_make_request(xxx_queue, xxx_make_request_fn);
4、提取请求
struct request *blk_peek_request(struct request_queue *q)
上述函数用于返回下一个要处理的请求,如果没有请求则返回NULL。
5、启动请求
void blk_start_request(struct request *req)
使用 blk_peek_request 函数获取到下一个要处理的请求以后就要开始处理这个请求,使用上述函数启动请求。
6、一步到位处理请求
struct request *blk_fetch_request(struct request_queue *q)
我们可以考虑使用 blk_fetch_request() 函数完成请求的提取和启动工作。
7、遍历 bio 和片段
__rq_for_each_bio(_bio, rq) 遍历一个请求的所有 bio
#define __rq_for_each_bio(_bio, rq) \ if ((rq->bio)) \ for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
bio_for_each_segment 遍历一个 bio 的所有 bio_vec
#define bio_for_each_segment(bvl, bio, iter) \ __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
rq_for_each_segment 迭代遍历一个请求所有 bio 中的所有 bio_vec
#define rq_for_each_segment(bvl, _rq, _iter) \ __rq_for_each_bio(_iter.bio, _rq) \ bio_for_each_segment(bvl, _iter.bio, _iter.iter)
8、报告完成
void __blk_end_request_all(struct request *rq, int error)
void blk_end_request_all(struct request *rq, int error)
上述两个函数用于报告请求是否完成,error为0表示完成,小于0表示失败,__blk_end_request_all 需要在持有队列锁的场景下使用。
如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要使用 bio_endio 函数通知处理完成:
bvoid bio_endio(struct bio *bio, int error)
.
ramdisk 是一种模拟磁盘,其数据实际上存储在 RAM 中。它通过 kzalloc() 分配出来的内存空间来模拟出一个磁盘,以块设备的方式访问这片内存。
下面的驱动程序通过设置 request_mode 的值,实现 使用请求队列 或者 不使用请求队列 两种方式来访问ramdisk。
ramdisk.c驱动程序
#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <asm/mach/map.h> #include <asm/uaccess.h> #include <asm/io.h> #include <linux/blkdev.h> #include <linux/blkpg.h> #include <linux/bio.h> #include <linux/hdreg.h> #define RAMDISK_SIZE (2 * 1024 * 1024) #define RAMDISK_NAME "ramdisk" #define RAMDISK_MINORS (3) /* 3个分区 */ #define RAMDISK_QUEUE (1) /* 使用请求队列 */ #define RAMDISK_NOQUEUE (2) /* 不使用请求队列 */ const unsigned char request_mode = RAMDISK_QUEUE; /* 选择是否使用请求队列 */ /* ramdisk 设备结构体 */ struct ramdisk_dev{ int major; unsigned char *ramdiskbuf; /* ramdisk 内存空间,用于模拟块设备 */ spinlock_t lock; struct gendisk *gendisk; /* 描述一个磁盘设备, gendisk 结构体 */ struct request_queue *queue; /* 请求队列 */ }; struct ramdisk_dev ramdiskdev; /* ramdisk 设备 */ static int setup_device(struct ramdisk_dev *dev); /* 使用请求队列时的传输过程 */ static void ramdisk_transfer(struct ramdisk_dev *dev, struct request *req) { unsigned long start = blk_rq_pos(req) << 9; unsigned long len = blk_rq_cur_bytes(req); char *buffer = bio_data(req->bio); if(rq_data_dir(req) == READ) memcpy(buffer, dev->ramdiskbuf + start, len); else if(rq_data_dir(req) == WRITE) memcpy(dev->ramdiskbuf + start, buffer, len); } /* 请求处理函数,处理请求队列中每一个请求 */ static void ramdisk_request_fn(struct request_queue *q) { struct request *req; struct bio_vec *bvec; struct req_iterator iter; req = blk_fetch_request(q); #if 0 while(req != NULL){ struct ramdisk_dev *dev = req->rq_disk->private_data; ramdisk_transfer(dev, req); /* 针对请求做具体的传输处理 */ __blk_end_request_all(req, 0); req = blk_fetch_request(q); } #endif while(req != NULL){ struct ramdisk_dev *dev = req->rq_disk->private_data; rq_for_each_segment(bvec, req, iter){ /* 遍历一个请求所有 bio 中的所有 bio_vec */ ramdisk_transfer(dev, req); } __blk_end_request_all(req, 0); req = blk_fetch_request(q); } } /* * "制造请求"函数, * 不使用请求队列时使用 */ static void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio) { struct ramdisk_dev *dev = q->queuedata; int offset; struct bio_vec *bvec; unsigned long len = 0; int i = 0; offset = (bio->bi_sector) << 9; /* 处理 bio 中的所有 bio_vec */ bio_for_each_segment(bvec, bio, i){ char *ptr = __bio_kmap_atomic(bio, bio->bi_idx,i); len = bvec->bv_len; if(bio_data_dir(bio) == READ) memcpy(ptr, dev->ramdiskbuf + offset, len); else if(bio_data_dir(bio) == WRITE) memcpy(dev->ramdiskbuf + offset, ptr, len); offset += len; __bio_kunmap_atomic(ptr,i); } set_bit(BIO_UPTODATE, &bio->bi_flags); bio_endio(bio, 0); } static int ramdisk_open(struct block_device *bdev, fmode_t mode) { //struct ramdisk_dev *dev = bdev->bd_disk->private_data; printk(KERN_EMERG "ramdisk open!\r\n"); return 0; } static int ramdisk_release(struct gendisk *disk, fmode_t mode) { //struct ramdisk_dev *dev = disk->private_data; printk(KERN_EMERG "ramdisk release!\r\n"); return 0; } static int ramdisk_getgeo(struct block_device *bdev, struct hd_geometry *geo) { //struct ramdisk_dev *dev = bdev->bd_disk->private_data; geo->heads = 2; /* 磁头 */ geo->cylinders = 32; /* 柱面(即磁道数) */ geo->sectors = RAMDISK_SIZE / (2*32*512); /* 每条磁道的扇区数 */ return 0; } static struct block_device_operations ramdisk_fops = { .owner = THIS_MODULE, .open = ramdisk_open, .release = ramdisk_release, .getgeo = ramdisk_getgeo, }; /* 驱动入口函数 */ static int __init ramdisk_init(void) { int ret = 0; /* 申请 ramdisk 内存 */ ramdiskdev.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL); if(ramdiskdev.ramdiskbuf == NULL){ ret = -EINVAL; /* 申请内存空间失败 */ goto ram_fail; } /* 初始化自旋锁 */ spin_lock_init(&ramdiskdev.lock); /* 注册块设备 */ ramdiskdev.major = register_blkdev(0, RAMDISK_NAME); /* 自动分配主设备号 */ if(ramdiskdev.major < 0){ goto register_blkdev_fail; } printk(KERN_EMERG "major = %d\r\n",ramdiskdev.major); ret = setup_device(&ramdiskdev); if(ret != 0) return ret; return 0; register_blkdev_fail: kfree(ramdiskdev.ramdiskbuf); /* 释放内存 */ ram_fail: return ret; } /* 驱动出口函数 */ static void __exit ramdisk_exit(void) { put_disk(ramdiskdev.gendisk); /* 引用计数-1 */ del_gendisk(ramdiskdev.gendisk); /* 删除 gendisk */ blk_cleanup_queue(ramdiskdev.queue); /* 清除请求队列 */ unregister_blkdev(ramdiskdev.major, RAMDISK_NAME); /* 注销块设备 */ kfree(ramdiskdev.ramdiskbuf); /* 释放内存 */ } static int setup_device(struct ramdisk_dev *dev) { int ret = 0; switch(request_mode){ case RAMDISK_NOQUEUE: /* 不使用请求队列 */ dev->queue = blk_alloc_queue(GFP_KERNEL); if(dev->queue == NULL){ ret = -EINVAL; goto blk_queue_fail; } blk_queue_make_request(dev->queue, ramdisk_make_request_fn); /* 设置 "制造请求" 函数*/ break; default: /* 没有指定模式,则默认使用 请求队列 */ printk(KERN_EMERG "Bad request mode %d, using simple\r\n", request_mode); case RAMDISK_QUEUE: /* 使用请求队列 */ dev->queue = blk_init_queue(ramdisk_request_fn, &dev->lock); if (dev->queue == NULL){ ret = -EINVAL; goto blk_queue_fail; } break; } dev->queue->queuedata = dev; /* 私有数据 */ /* 分配并初始化gendisk */ dev->gendisk = alloc_disk(RAMDISK_MINORS); if (dev->gendisk == NULL) { printk(KERN_EMERG "alloc_disk fail\r\n"); ret = -EINVAL; /* 分配 gendisk 失败 */ goto gendisk_alloc_fail; } /* 初始化 gendisk ,并添加进内核 */ dev->gendisk->major = dev->major; dev->gendisk->first_minor = 0; dev->gendisk->fops = &ramdisk_fops; dev->gendisk->private_data = dev; /* 私有数据 */ dev->gendisk->queue = dev->queue; sprintf(dev->gendisk->disk_name, RAMDISK_NAME); set_capacity(dev->gendisk, RAMDISK_SIZE/512); /* 设置 gendisk 容量 */ add_disk(dev->gendisk); /* 将 gendisk 添加到内核 */ return 0; blk_queue_fail: unregister_blkdev(dev->major, RAMDISK_NAME); gendisk_alloc_fail: put_disk(dev->gendisk); /* 引用计数-1 */ kfree(dev->ramdiskbuf); /* 释放内存 */ return ret; } module_init(ramdisk_init); module_exit(ramdisk_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("xzj");
.
加载驱动 insmod
格式化 ramdisk-----mkfs.vfat /dev/ramdisk
查看所有硬盘及分区信息------fdisk -l
已经识别出 ramdisk 设备了,容量和磁盘信息跟上面驱动程序中的一样。
把磁盘挂载到 tmp目录------mount /dev/ramdisk /tmp
在 tmp 目录中随便新建一个.txt来测试 ramdisk 的访问是否正常,接着卸载。然后重新挂载到 mnt 目录,可以看到 mnt 目录里面有之前在 tmp 目录下创建的.txt,说明 ramdisk驱动正常工作。