块设备驱动程序的引入和简单应用

本文介绍了块设备驱动程序的重要性及其实现原理,通过对比字符设备驱动程序,突出了块设备驱动程序在提高读写效率方面的优势,并详细展示了如何编写一个简单的块设备驱动程序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先说说为什么要引入块设备驱动程序。
以FLASH为例,如果对flash的读写采用字符设备驱动程序的那一套的话,会产生效率低下的问题。flash是以块为单位进行操作的,假如在flash一个块中,要完成对扇区0和扇区1的改写,如果按照字符设备的方式,需要完成以下步骤:
①读出整个块到buffer
②修改buffer的扇区0
③擦除整个块
④烧写整块
⑤读出整个块到buffer
⑥修改buffer的扇区1
⑦擦除整个块
⑧烧写整块
但如果我们引入一些优化:
①读出整个块到buffer
②修改buffer的扇区0和扇区1
③擦除整个块
④烧写整块

这样我们就能够大大提高效率。因此,块设备驱动程序的基本思想就出来了:先不执行读写,而是放入队列;优化之后再执行。

下面介绍一下块设备驱动程序的框架。

app:      open,read,write "1.txt"
---------------------------------------------  文件的读写
文件系统: vfat, ext2, ext3, yaffs2, jffs2      (把文件的读写转换为扇区的读写)
-----------------ll_rw_block-----------------  扇区的读写
                       1. 把"读写"放入队列
                       2. 调用队列的处理函数(优化/调顺序/合并)
            块设备驱动程序     
---------------------------------------------
硬件:        硬盘,flash

下面开始分析ll_rw_block(low level read/write block)

分析ll_rw_block
        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

接下来要写一个真正的驱动程序,分配一块内存,用内存来模拟块设备。
第一个程序:

static struct gendisk *ramblock_disk;
static request_queue_t *ramblock_queue;

static int major;

static DEFINE_SPINLOCK(ramblock_lock);//定义自选锁

static struct block_device_operations ramblock_fops = {
    .owner  = THIS_MODULE,
};

#define RAMBLOCK_SIZE (1024*1024)

static void do_ramblock_request(request_queue_t * q)
{
    static int cnt = 0;
    printk("do_ramblock_request %d\n", ++cnt);
}

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);//单位是扇区,在内核层面默认扇区是521字节

    /* 3. 注册 */
    add_disk(ramblock_disk);

    return 0;
}

static void ramblock_exit(void)
{
    unregister_blkdev(major, "ramblock");
    del_gendisk(ramblock_disk);
    put_disk(ramblock_disk);
    blk_cleanup_queue(ramblock_queue);
}

module_init(ramblock_init);
module_exit(ramblock_exit);

MODULE_LICENSE("GPL");

加载驱动之后发现:
ramblock:do_ramblock_request 1
然后就卡住了,无法继续操作。说明确实调用了do_ramblock_request这个函数,但由于我们没有对它进行进一步的处理,所以还需要完善。

static void do_ramblock_request(request_queue_t * q)
{
    static int cnt = 0;
    struct request *req;

    printk("do_ramblock_request %d\n", ++cnt);

    while ((req = elv_next_request(q)) != NULL) {
        end_request(req, 1);
    }
}

以电梯调度算法取出下一个请求,然后结束请求并返回1表示成功。实际上我们什么都没有做,但这样就不会卡住了。它会打印出如下结果:
ramblock:do_ramblock_request 1
unknown partition table
之所以提示不识别的分区表,是因为我们在实际上我们在do_ramblock_request里什么都没有做。我们只是简单粗暴的把请求拿出来,然后就简单把它当成完成了。
进一步优化:

static void do_ramblock_request(request_queue_t * q)
{
    static int 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)
        {
            memcpy(req->buffer, ramblock_buf+offset, len);
        }
        else
        {
            memcpy(ramblock_buf+offset, req->buffer, len);
        }       

        end_request(req, 1);
    }

其中memcpy(void *dest, const void *src, size_t count)。
测试:
在开发板上:
1. insmod ramblock.ko
2. 格式化: mkdosfs /dev/ramblock
3. 挂接: mount /dev/ramblock /tmp/
4. 读写文件: cd /tmp, 在里面vi文件,比如新建一个1.txt,里面的内容是“hello”
5. cd /; umount /tmp/,卸载设备
6. cat /dev/ramblock > /mnt/ramblock.bin,把文件的内容统统存到/mnt/ramblock.bin,相当于整个磁盘映象。
7. 在PC上查看ramblock.bin
sudo mount -o loop ramblock.bin /mnt,可以把一个普通文件当作块设备文件挂载,输入ls仍然可以看到1.txt。

接下来加入打印语句,可以看到一些有意思的事情:

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);
    }
}

还是按照上面的步骤测试,当挂载到/tmp之后,我们执行“cp /etc/inittab /tmp”:

do_ramblock_request write 6
do_ramblock_request write 7
do_ramblock_request write 8
再执行sync
do_ramblock_request write 9
do_ramblock_request write 10

其中,打印的时候会有很长的时间间隔,当输入sync时,会完成全部操作。sync是一个系统调用,是同步的意思。

从这里我们可以看出,块设备的读写不会交叉执行,而是会进行优化,放入队列。

接下来进行下一步的优化,实现分区:

static struct block_device_operations ramblock_fops = {
    .owner  = THIS_MODULE,
    .getgeo = ramblock_getgeo,
};


static int ramblock_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
    /* 容量=heads*cylinders*sectors*512 */
    geo->heads     = 2;//磁头的个数
    geo->cylinders = 32;//柱面的个数
    geo->sectors   = RAMBLOCK_SIZE/2/32/512;
    return 0;
}

要注意的是,实际上很多磁盘已经不是这种结构了,但为了兼容,必须设置虚拟的参数,这样才能借助分区工具。
加载驱动之后输入fdisk /dev/ramblock。然后输入n新建一个分区,输入p表示主分区,然后输入1表示第一个主分区。

Command (m for help): p

Disk /dev/ramblock: 1 MB, 1048576 bytes
2 heads, 32 sectors/track, 32 cylinders
Units = cylinders of 64 * 512 = 32768 bytes

        Device Boot      Start         End      Blocks  Id System

Command (m for help): n
Command action
   e   extended
   p   primary partition (1-4)
p
Partition number (1-4): 1
First cylinder (1-32, default 1): 1
Last cylinder or +size or +sizeM or +sizeK (1-32, default 32): 5
Command (m for help): w
The partition table has been altered!
 ramblock: ramblock1

这样第一个分区就好了。剩下的分区也这样操作,最后别忘记输入“w”把分区写到分区表中。最后可以看到:

Command (m for help): p

Disk /dev/ramblock: 1 MB, 1048576 bytes
2 heads, 32 sectors/track, 32 cylinders
Units = cylinders of 64 * 512 = 32768 bytes

        Device Boot      Start         End      Blocks  Id System
/dev/ramblock1               1           5         144  83 Linux
/dev/ramblock2               6          25         640  83 Linux
/dev/ramblock3              26          32         224  83 Linux

如果现在输入”ls /dev/ramblock* -l”,会看到:

brw-rw----    1 0        0        254,   0 Jan  1 00:45 /dev/ramblock
brw-rw----    1 0        0        254,   1 Jan  1 00:45 /dev/ramblock1
brw-rw----    1 0        0        254,   2 Jan  1 00:45 /dev/ramblock2
brw-rw----    1 0        0        254,   3 Jan  1 00:45 /dev/ramblock3

如果我们想分别格式化的时候就可以直接“fdisk /dev/ramblock2”,还可以直接挂载“mount /dev/ramblock1 /tmp/”。

驱动程序就写到这里,用内存模拟磁盘很简单,可以想象一下,如果块设备也能够像内存一样访问的话,块设备就会非常的简单,但可惜的是,现在还达不到这种水平,硬件的操作太复杂了。但块设备本身的框架并不复杂。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值