⑲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

如下测试:

[root@tiny4412]insmod 1702/virtual_mem.ko
[   16.750000] -----exynos_virtual_mem_init-----
[root@tiny4412]ls -l /dev/virtual_mem
brw-rw----    1 0        0         253,   0 Oct 22  2018 /dev/virtual_mem
[root@tiny4412]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
[root@tiny4412]mount /dev/virtual_mem /mnt/virtemp/
[root@tiny4412]cp 1702/beep.ko /mnt/virtemp/
[root@tiny4412]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

猜你喜欢

转载自blog.csdn.net/qq_23922117/article/details/83271584