本文简要介绍了磁盘的基本构成;并从一个内存(ram)模拟一个块设备(disk)的简单驱动(ramdisk),来了解块设备驱动原理;最后如何对该块设备的简单使用。
一、磁盘基本构成
了解块设备原理,需要先了解磁盘的基本结构,如下为一个盘面,磁盘最小单位为扇区,在内核里通常默认512B,每个盘面有一道道环形,为磁道。
多个盘面一起构成一块磁盘,如下每个盘面上相同的磁道组成一个柱面,所以柱面数=当个盘面的磁道数,同时每个盘面对应一个磁头操作读写。
这样就能计算出磁盘总容量: 磁头(盘面)数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数(512)
块设备驱动与字符驱动不同在于,其数据不是立马写入设备,而是有个request_queue缓存了数据,对读和写进行分类,当积累了一定数量的写/读IO才统一去操作磁盘写入/读出,这样目的是提高磁盘读写效率,而不是每来一个读或者写IO,就做一次磁盘操作。
下面从一个ram(内存)模拟一个块设备的简单驱动,来了解块设备驱动原理。
二、ramdisk源码基本架构
先上源码,源码基于3.10.0-123编写。如下是最简易架构的ramdisk源码。创建一个4MB的内存作为ram盘。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/major.h>
#include <linux/blkdev.h>
#include <linux/bio.h>
#include <linux/highmem.h>
#include <linux/mutex.h>
#include <linux/radix-tree.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/sizes.h>
#include <linux/kernel.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/blkpg.h>
#define DEVICE_NAME "ramdisk_test"
#define ram_size SZ_4M
static DEFINE_SPINLOCK(ramdisk_lock);
struct gendisk *ramdisk;
static struct request_queue *disk_queue;
void *ram_addr;
static int ramdisk_getgeo(struct block_device *blk_dev, struct hd_geometry *geo)
{
geo->cylinders = 32; //磁道数、柱面数
geo->heads = 2; //磁盘面数
geo->sectors = ram_size/2/32/512; //单个磁道上的扇区数量
printk(KERN_INFO "capacity %d\n", get_capacity(blk_dev->bd_disk)); //get_capacity(blk_dev->bd_disk)=8292
return 0;
}
static struct kobject *ramdisk_probe(dev_t dev, int *part, void *data)
{
printk(KERN_INFO "%s called\n", __FUNCTION__);
*part = 0;
return get_disk(ramdisk);
}
/*
列队(disk_queue)中的request处理函数
*/
static void ramdisk_request(struct request_queue *q)
{
struct request *req;
printk(KERN_INFO "%s called\n", __FUNCTION__);
req = blk_fetch_request(q); //获取列队(disk_queue)里的request
while (req) {
unsigned long offset = blk_rq_pos(req) *512; //扇区起始地址(即偏移)
unsigned long len = blk_rq_cur_bytes(req); //request长度
int err = 0;
printk(KERN_INFO "%s offset:%d len %d\n", __FUNCTION__, offset, len);
if((u32)(offset+len) < ram_size)
{
if (rq_data_dir(req) == READ) //列队读请求
{
printk(KERN_INFO "%s read len %d\n", __FUNCTION__, len);
memcpy(req->buffer, (char *)(ram_addr + offset), len); //列队读(request中的buffer拷贝出来,写入ramdisk的扇区为offset开始的地方)
}
else
{
printk(KERN_INFO "%s write len %d\n", __FUNCTION__, len);
memcpy((char *)(ram_addr + offset), req->buffer, len); //列队写
}
}
else
{
printk(KERN_INFO "fail %s offset:%d len %d\n", __FUNCTION__, offset, len);
}
done:
if (!__blk_end_request_cur(req, err)) //end_request 结束
req = blk_fetch_request(q);
}
}
static const struct block_device_operations disk_fops =
{
.owner = THIS_MODULE,
.getgeo = ramdisk_getgeo,
//.open = ramdisk_open,
//.release = ramdisk_release,
};
int major;
static int __init ramdisk_init(void)
{
int ret;
printk(KERN_INFO "%s called\n", __FUNCTION__);
major = register_blkdev(0, DEVICE_NAME); //注册一个块设备,获取设备号
disk_queue = blk_init_queue(ramdisk_request, &ramdisk_lock); //初始化列队 request_queue,列队处理函数为ramdisk_request
if (!disk_queue)
goto out_queue;
ramdisk = alloc_disk(1); //分配一个gendisk(次设备号个数=分区数=1)
ramdisk->queue = disk_queue; //为gendisk赋值,绑定
ramdisk->major = major;
ramdisk->first_minor = 0; //该值0---次设备号个数,都是该块设备
ramdisk->fops = &disk_fops;
sprintf(ramdisk->disk_name, DEVICE_NAME);
set_capacity(ramdisk, ram_size/512); //设置块设备容量(单位:扇区)
add_disk(ramdisk); //添加gendisk
blk_register_region(MKDEV(major, 0), 1, THIS_MODULE, //注册设备号[0-1)
ramdisk_probe, NULL, NULL);
ram_addr = kzalloc(ram_size, GFP_KERNEL);
if(!ram_addr)
{
kfree(ram_addr);
goto out_queue;
}
printk(KERN_INFO "%s end\n", __FUNCTION__);
return 0;
out_queue:
put_disk(ramdisk);
out_disk:
unregister_blkdev(major, DEVICE_NAME);
err:
return -1;
}
static void __exit ramdisk_exit(void)
{
blk_unregister_region(MKDEV(major, 0), 1);
unregister_blkdev(major, DEVICE_NAME);
put_disk(ramdisk);
//del_gendisk(ramdisk);
blk_cleanup_queue(disk_queue);
kfree(ram_addr);
return;
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
2.1 源码分析:
一个块设备的框架主要实现流程如下:
- 注册块设备,获取设备号 register_blkdev
- 申请设置列队(主要是绑定列队处理函数) blk_init_queue
- 分配gendisk结构体并赋值(包括绑定列队,设备号,fop等) alloc_disk
- 设置盘容量(扇区个数) set_capacity
- 添加创建磁盘 add_disk
如果要创建一个4MB内存块作为该块设备,则需要:
- ram_addr = kzalloc(ram_size, GFP_KERNEL); //申请4MB内存
- 然后编写列队处理函数ramdisk_request,是实现数据读写的核心。
ramdisk_request处理函数实现流程:
- 内核根据电梯调度算法,获取request_queue列队中的quest。
- 获取quest的扇区所在地址(即偏移地址),request的长度
- 判断对列队是读还是写,对磁盘(ram)做相应的memcpy 操作(req->buffer与ram之间的拷贝)。
如上,可以看出request_queue列队实现读写的关键,该处理函数里,内核根据电梯调度算法,获取到request的IO,然后做批量读或批量写。
需要注意的是:函数里需要对request长度作下限制访问:if((u32)(offset+len) < ram_size),否则长度可能会超过4M,造成内存越界。
从调试的底层打印可以观察到,往块设备挂载路径下读写文件数据,只有少量的读写话并不会立马执行列队处理函数(除非执行了sync才会被立马执行),这个跟块设备的原理有关,这些少量数据会被先存到buffer,当块设备ramdisk_request时机成熟时,由电梯调度算法把request取出。
三、ramdisk的使用
- 加载驱动后,可以看看到块设备被创建(/dev/ramdisk_test)
[root@localhost mnt]# ls /dev/ramdisk_test -l
brw-rw----. 1 root disk 252, 0 Feb 2 05:47 /dev/ramdisk_test
- 给磁盘格式化文件系统后,会调用到ramdisk_getgeo几何函数,获取容量分配磁盘信息。包括:磁盘面数(geo->heads ),磁道/柱面数(geo->cylinders),单磁道的扇区数量(geo->sectors)。
[root@localhost ramdisk]# mkfs.ext4 /dev/ramdisk_test #格式化为ext4
mke2fs 1.42.9 (28-Dec-2013)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
Stride=0 blocks, Stripe width=0 blocks
1024 inodes, 4096 blocks
204 blocks (4.98%) reserved for the super user
First data block=1
Maximum filesystem blocks=4194304
1 block group
8192 blocks per group, 8192 fragments per group
1024 inodes per group
Allocating group tables: done
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
[root@localhost ramdisk]# mkfs.fat /dev/ramdisk_test #或格式化为fat
mkfs.fat 3.0.20 (12 Jun 2013)
注:如果没有加入ramdisk_getgeo函数,在格式时候有可能会报错(提示获取不到几何信息),报错如下:
[root@localhost ramdisk]# mkfs.fat /dev/ramdisk_test mkfs.fat 3.0.20 (12 Jun 2013) unable to get drive geometry, using default 255/63 ```
- fdisk可以查看ramdisk的容量,看出整个盘就一个分区,容量4MB。
[root@localhost mnt]# fdisk -l /dev/ramdisk_test
Disk /dev/ramdisk_test: 4 MB, 4194304 bytes, 8192 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x00000000
Device Boot Start End Blocks Id System
- 挂载并查看分区,后面就可以在/mnt/下存取文件了。
[root@localhost /]# mount /dev/ramdisk_test /mnt/
[root@localhost /]# mount |grep ramdisk
/dev/ramdisk_test on /mnt type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)
[root@localhost /]#