⑲tiny4412 Linux驱动开发之块设备驱动程序
之前说过了字符设备,网络设备(USB网卡),就差块设备没有说过了,本次就来介绍一下块设备,在Linux里有一种不严谨的说法叫:一切皆文件,实际上比如进程就不是文件,可它却是实实在在存在的东西,而且特别重要,但从这句话中还是可以看出文件是Linux里抽象出来的重要概念,在Linux里的设备主要分为:字符设备,块设备和网络设备,其中,前两种用文件IO操作,后边一种用socket操作,本次说的是块设备,和以往字符设备编程不同的是,在Linux中块设备没有read/write接口,因为市面上很多块设备是不支持按位和随机读写的,比如Nand Flash, HDD硬盘, 它们需要按页操作,当然,现在比较先进的NorFlash是可以按位读写的,但是单片容量太小了(目前国产厂商能做到256M),而且很贵,我是微电子出身,之前看过Nand Flash和NorFlash的原理,Nandflash需要两个逻辑门配合,NorFlash只需要1个就可以了,按说,硬件成本上应该减小好多,为什么反而贵?
反正因为历史原因,Linux上对于块设备不是按位操作的,它操作的最小单位是扇区,进行读写时,则使用块作为单位,说到这里我们来看一下Linux中的一些概念:
1,扇区
磁盘上的每个磁道被分为若干个弧段,这些弧段便是磁盘的扇区.磁盘驱动器再向磁盘读取和写入数据时,要以扇区为单位.至少出于两种原因,必须以扇区为单位进行读写:一是磁盘设备很难对单个字节进行定位;二是为了达到良好的性能,一次传送一组数据的效率比一次传送一个字节的效率要高.
在大多数磁盘设备中,扇区的大小一般是512~4096 byte.注意,即使程序只读取一个字节的数据,也应该传递一个扇区的数据.Linux系统中,扇区的大小历来都是512字节.内核模块中都是以512字节来定义扇区大小的.这就引起了一个问题,目前的很多块设备的扇区也有大于512字节的,Linux的解决方式是,内核依然使用512字节的扇区.例如光盘设备的扇区大小是4096字节,光驱读取一次将返回4096个字节,内核将这4096个字节看成8个连续的扇区.在内核看来,好像读取了8次设备一样.
2,块
扇区是硬件设备传送数据的最小单位,硬件一次传送一个扇区的数据到内存中.与扇区不同,块是虚拟文件系统传送数据的基本单位.在Linux系统中,块的大小必须是2的幂,而且不能超过一个页的大小.此外,块必须还是扇区大小的整数倍,所以一个块可以包含若干个扇区.在x86平台上,页的大小是4096字节,所以快的大小可以是512, 1024, 2048, 4096字节,Linux系统的块大小是可以配置的,默认情况下是1024字节.
3,段
一个段就是一个内存页或者内存页的一部分.例如页的大小是4096字节,块的大小是2个扇区,即1024字节,那么段的大小可以是1024, 2048, 3072, 4096字节.也就是段的大小只与块有关,而且是块的整数倍,且不超过1页.这是因为Linux内核一次读取磁盘的数据是一个块,而不是一个扇区.页中块的开始位置必须是块的整数倍偏移的位置,也就是0, 1024, 2048, 3072.一个大小为1024字节的段可以开始于页的如下位置:
4.扇区,块和段的关系
理解扇区,块和段的概念对驱动开发非常重要.扇区是由物理磁盘的机械特性决定;块缓冲区由内核代码决定;段由块缓冲区决定,是块缓存大小的整数倍,但不超过1页;这三者的关系如下图:
下面,我们来看一下块设备的框架:
用户空间对数据进行操作,首先会进入内核空间的虚拟文件系统层,其会调用真正的文件系统,比如EXT4,这个文件系统会调用自己内部实现的函数往下层调用,对页面缓存进行操作,页面缓存和驱动程序之间,增加了一层叫I/O调度,主要的目的就是提高块设备操作的效率,这一点,我们待会进行介绍,再往下走就是块设备的驱动程序了,也就是需要我们实现地方,后边会写一个简单的驱动程序进行示范,再往下就是实体块设备硬件了.
I/O调度器:
块设备会有寻到时间,即磁头从当前位置移动到目标磁盘区话费的时间.I/O调度器的主要目的是通过尽量减少寻到时间,来增加系统吞吐量.为此,I/O调度器维持一个排序过的请求队列,排序是将请求按相关磁盘扇区连续性进行排列.新的请求依据排序算法被插入适当位置.如果队列中已有的某个请求正好要访问一个邻近的磁盘扇区,那么新请求就与它合二为一,因为这些属性,I/O调度器的运行方式跟电梯类似:电梯只会在一个方向上安排各个请求,直到队列中的最后一个请求者得到服务.在2.4内核中就是用电梯式调度算法,这种算法在一定情况下适用,但是对于实际较复杂的情况则会很鸡肋,因为,相邻的有很多请求,都是后边"插队"进来的,本来来的很早的请求,因为这种插队机制,始终得不到执行权,就得挨饿,在新的内核中则有了4种新的调度器,分别为:限时式(deadline), 预测式(anticipatory), 完全公平排队(CFQ), 空操作(noop).默认的调度器是CFQ,这个在内核配置中可以修改,或者通过改动/sys/block/[disk]/queue/schedule的值来更改.我们通过下面的表格简单了解一下这几种调度器:
I/O调度器 |
说明 |
Linus电梯 | 标准的合并与排序I/O调度算法的直接实现 |
限时式 | 除了Linus电梯实现的之外,限时式调度器对每个请求还设置了溢出时间,以防止大量对同一磁盘区域是请求会"饿死"那些对远区方位的请求.并且,读操作被赋予比写操作更高的优先级,因为用户进程通常更多地会被阻塞,知道它们的读写请求完成.限时调度器因此确保每个I/O请求能在一定时限内得到服务,这对一些数据库应用很重要. |
预测式 | 预测式调度器与限时式调度器类似,不同的是在为读请求服务后,它会等待一个预定的时间以期待未来的请求,这种调度技术适合于工作站/交互式的负荷. |
完全公平调度队列 | 与Linus电梯类似,不同的是CFQ会为每个操作进程维护一个请求队列,而不是所有进程使用一个公共的队列.这确保了每个进程(或进程组)得到公平的I/O,防止一个进程"饿死"其他进程 |
空操作 | 空操作调度器不需要搜寻请求队列来找最优插入点,而只是简单地将新请求插入请求队列尾部,因此这种调度器对半导体存储介质是理想的,因为它们没有移动部件,没有寻道延时. |
从概念上来看,I/O调度类似于进程调度.I/O调度让进程觉得它拥有磁盘,而进程调度则让进程觉得它拥有CPU.
下面也不多废话了,直接上代码吧,本次是一个简单的块设备演示驱动程序,是用RAM来模拟硬盘的一个简单示例,其实,这种用RAM模拟硬盘的操作,是系统的特性,在Windows 7以上或者Linux上,都是把用不完的RAM空间当作硬盘的缓冲区来用的,比如说,你的笔记本有16G的内存,现在用了3G,那么剩下的13G内存几乎都被用来当作硬盘缓冲区,只有很小一部分RAM是真正空闲的.而被当作缓冲区的RAM空间,不会显示占用状态,而是会显示为free空间,这样做的好处就是:1.节约时间,RAM的存取速度是目前市面上的固态硬盘的好多倍;2.延长硬件使用,RAM的使用寿命是特别高的,跟CPU差不多,CPU的寿命有多长呢?我们拿英特尔的首款处理器4004为例,1977年,英特尔研发出处理器4004,这个CPU安装在美国的一个太空机器人,好像叫探索者,也是1977年发射到外太空的,现在是2018年,探索者还在正常运行,要知道外太空的条件是很极端的,拿月球来说动不动就零上100多度,零下两三百度,所以RAM使用寿命很长,而比如目前的固态硬盘,也就几万次的擦写.所以,这种用RAM做缓冲区的机制是比较好的,当然,前提是笔记本的RAM要有这种条件,有的电脑就2G内存,一般开点东西就会满载,这时则会采用硬盘模拟RAM的方式,扯远了,回到正题,下面贴代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/io.h>
#include <linux/hdreg.h>
#include <linux/genhd.h>
#include <linux/ioport.h>
#include <linux/mm.h>
#include <linux/errno.h>
#include <linux/timer.h>
#include <linux/wait.h>
#include <linux/delay.h>
#include <linux/bio.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/dma.h>
#define RAM_DISKNAME "virtual_mem"
#define BLOCKMEM_SIZE (1024 << 10)
static int VIRTUAL_MEM_MAJOR = -1; // 主设备号
static DEFINE_SPINLOCK(memspin_lock); // 初始化自旋锁
static struct gendisk *ram_disk; // 通用磁盘结构
static struct request_queue *ram_disk_queue; // 请求队列
static unsigned char *virtual_mem = NULL; // 虚拟磁盘地址
static struct block_device_operations ram_disk_fops = {
.owner = THIS_MODULE,
};
static void
do_memblock_request(struct request_queue *q)
{
struct request *req;
unsigned long offset, len;
req = blk_fetch_request(q);
while(req != NULL) {
offset = blk_rq_pos(req) << 9;
len = blk_rq_cur_sectors(req) << 9;
if(offset + len > BLOCKMEM_SIZE) {
printk("%s request more than mem space !\n", __func__);
continue;
}
switch(rq_data_dir(req))
{
case READ:
memcpy(req->buffer, virtual_mem + offset, len);
break;
case WRITE:
memcpy(virtual_mem + offset, req->buffer, len);
break;
default:
break;
}
if(!__blk_end_request_cur(req, 0))
req = blk_fetch_request(q);
}
}
static void __exit
exynos_virtual_mem_exit(void)
{
del_gendisk(ram_disk);
kfree(virtual_mem);
blk_cleanup_queue(ram_disk_queue);
unregister_blkdev(VIRTUAL_MEM_MAJOR, RAM_DISKNAME);
put_disk(ram_disk);
}
static int __init
exynos_virtual_mem_init(void)
{
int ret = -1;
printk("-----%s-----\n", __func__);
// 1,注册设备
VIRTUAL_MEM_MAJOR = register_blkdev(0, RAM_DISKNAME);
if(VIRTUAL_MEM_MAJOR < 0) {
printk("%s register_blkdev failed !\n", __func__);
ret = -EBUSY;
goto err1;
}
// 2,分配通用磁盘
ram_disk = alloc_disk(1);
if(!ram_disk) {
printk("%s alloc_disk failed !\n", __func__);
return -ENOMEM;
}
// 3,请求队列初始化
ram_disk_queue = blk_init_queue(do_memblock_request, &memspin_lock);
if(!ram_disk_queue) {
printk("%s blk_init_queue failed !\n", __func__);
ret = -ENOMEM;
goto err2;
}
// 4,设置逻辑块容量给请求队列
blk_queue_logical_block_size(ram_disk_queue, 512);
// 5,给ram_disk成员赋值
strcpy(ram_disk->disk_name, RAM_DISKNAME); // 设定设备名
ram_disk->major = VIRTUAL_MEM_MAJOR; // 设置主设备号
ram_disk->first_minor = 0; // 设置次设备号
ram_disk->fops = &ram_disk_fops; // 块设备操作函数
ram_disk->queue = ram_disk_queue; // 设置请求对列
set_capacity(ram_disk, BLOCKMEM_SIZE >> 9); // 设置设备容量
// 6,在RAM中申请一块空间当作虚拟磁盘
virtual_mem = kzalloc(BLOCKMEM_SIZE, GFP_KERNEL);
if(!virtual_mem) {
printk("%s kzalloc failed !\n", __func__);
ret = -ENOMEM;
goto err3;
}
// 7,**磁盘设备
add_disk(ram_disk);
return 0;
err3:
blk_cleanup_queue(ram_disk_queue);
err2:
unregister_blkdev(VIRTUAL_MEM_MAJOR, RAM_DISKNAME);
err1:
put_disk(ram_disk);
return ret;
}
module_init(exynos_virtual_mem_init);
module_exit(exynos_virtual_mem_exit);
MODULE_LICENSE("GPL v2");
对应的Makefile:
#指定内核源码路径
KERNEL_DIR = /home/george/1702/exynos/linux-3.5
#ָ指定当前路径
CUR_DIR = $(shell pwd)
#MYAPP = dht11_app
#MODULE = spi_flash
MODULE = virtual_mem
all:
make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
# arm-none-linux-gnueabi-gcc -o $(MYAPP) $(MYAPP).c
clean:
make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
$(RM) $(MYAPP)
install:
cp -raf *.ko $(MYAPP) /home/george/1702/exynos/filesystem/1702
#ָ指定当前项目编译的目标
obj-m = $(MODULE).o
如下测试:
[[email protected]]insmod 1702/virtual_mem.ko
[ 16.750000] -----exynos_virtual_mem_init-----
[[email protected]]ls -l /dev/virtual_mem
brw-rw---- 1 0 0 253, 0 Oct 22 2018 /dev/virtual_mem
[[email protected]]mkfs.ext2 /dev/virtual_mem
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
128 inodes, 1024 blocks
51 blocks (5%) reserved for the super user
First data block=1
Maximum filesystem blocks=262144
1 block groups
8192 blocks per group, 8192 fragments per group
128 inodes per group
[[email protected]]mount /dev/virtual_mem /mnt/virtemp/
[[email protected]]cp 1702/beep.ko /mnt/virtemp/
[[email protected]]df
Filesystem 1K-blocks Used Available Use% Mounted on
192.168.1.12:/home/george/1702/exynos/filesystem/
49185080 29979760 16706864 64% /
tmpfs 497320 0 497320 0% /tmp
/dev/virtual_mem 1003 100 852 11% /mnt/virtemp