先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题
本系列文章是为记录在学习韦东山老师的嵌入式开发教程中的课程笔记,并整理一个比较详细的课堂笔记,方便一起学习的同学们参考。
如果还没有购买韦老师的教学视频,或者不知道去哪里购买的,我这里给大家一个小程序链接
一 what
Linux中I/O设备分为两类:字符设备和块设备。两种设备本身没有严格限制,但是,基于不同的功能进行了分类。
字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,键盘、串口、调制解调器都是典型的字符设备。
块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘、软盘、CD-ROM驱动器和闪存都是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。
二 why
看了上面字符设备和块设备的区别之后,我们思考这样一个问题,之前我们已经完成了字符设备驱动程序的开发,那么块设备驱动程序是否可以考虑采样同样的驱动框架方式呢?如果不行,我们应该如何实现块设备驱动开发?
在回答上面的问题之前,我们先看两个常用的实例
a. 读写flash
一般地,flash整个空间会被划分为多个块,每个块又会分为多个扇区,而我们读写flash都是以块为单位,如下图。同时这里有一个知识点,flash的写过程必须是先擦除后写入。
我们只想修改其中某一个扇区,考虑下面的场景
(1)先写扇区0
(2)然后写扇区1
因此写扇区0的操作步骤如下:
a. 读出整块到buffer
b. 修改buffer里扇区0
c. 擦除整块
d. 烧写整块
写扇区1的步骤如下:
a. 读出整块到buffer
b. 修改buffer里扇区1
c. 擦除整块
d. 烧写整块
显然我们为了写扇区0和写扇区1,分别进行了两次读整块,擦除整块,烧写整块的过程,而实际上对我们来说,只需要两次写/修改过程,至于两次读、擦除、烧写过程都是多余的,我们理论上来说只需要一次读,擦除,烧写就可以了。因此如果按照字符设备驱动程序方式来做,上面的过程就是多余的,因此我们需要采用新的驱动方案,优化读写的过程,优化的方案是
a. 先不执行,放入队列
b. 优化后执行,合并
b. 读写磁盘
磁盘一般可以分为磁头0,磁头1,磁头2,等等;每个磁头上有柱面,柱面又可以划分为块。
假设我们需要先读磁头0的某个块,然后再去写磁头1的某个块,再然后去读磁头0的另一个块。那么假设使用字符设备,上面的过程就会依序进行,读—>写—>读,从软件角度来说,是很快的,但是从磁盘的硬件结构来说,它有一个磁盘头的跳转,这个硬件跳转的过程相对于读写来说是很慢的,因此我们就不能简单按照读—>写—>读的方式进行。
我们可以优化一下这个顺序,改为 读—>读—>写,这样只需要磁盘头跳转一次,上面的过程需要跳转两次。
因此,块设备驱动程序的基本思想就是,先不执行读写I/O,而是先放入队列,优化后再执行。
块设备驱动程序的框架如下
app : open read write "1.txt"
----------------------------------------------------- 文件的读写
文件系统 : vfat, ext2, ext3, ext4, jfss2, yaffs2 (把文件的读写转换为扇区的读写)
------------------ll_rw_block------------------------ 扇区的读写
1. 把"读写"放入队列
2. 调用队列的处理函数(优化、调整顺序、合并)
块设备驱动程序
-----------------------------------------------------
硬件 : 硬盘, flash
我们可以分析ll_rw_block函数的调用过程,了解具体的实现步骤
分析 ll_rw_block
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
submit_bh(rw, bh);
}
submit_bh
struct bio *bio; // 使用bh来构造bio (block input output)
submit_bio(rw, bio);
submit_bio
generic_make_request(bio); //通用的构造请求,使用bio来构造请求request
generic_make_request
struct request_queue *q = bdev_get_queue(bio->bi_bdev); // 找到队列
q->make_request_fn(q, bio); //构造请求函数 默认函数是 __make_request
__make_request
三 how
那么我们需要如何写块设备驱动程序呢,步骤
1. 分配 gendisk : alloc_disk
2. 设置
2.1. 分配、设置队列: request_queue_t //提供读写能力
block_init_queue
2.2. 设置gendisk其他属性 //提供属性,比如容量
3. 注册: add_disk
下面我们以内存的读写模拟一个块设备驱动的读写过程,参考drivers\block\xd.c和drivers\block\z2ram.c
(1)初始化
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 设置gendisk其他属性,比如容量 */
major = register_blkdev(0, "ramblock"); /* cat /proc/device */
ramblock_disk->major = major;
ramblock_disk->first_minor = 0;
sprintf(ramblock_disk->disk_name, "ramblock");
ramblock_disk->fops = &ramdisk_fops;
set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);
/* 3. 硬件相关操作 */
ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);
/* 4. 注册 */
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);
kfree(ramblock_buf);
}
(2)request请求读写
static void do_ramblock_request (struct request_queue * q)
{
struct request *req;
static int cnt = 0;
//printk("do_ramblock_request %d\n", ++cnt);
req = blk_fetch_request(q);
//printk("aaaa\n");
while (req) {
/* 数据传输三要素 源、目的 、长度 */
/* 源/目的 */
unsigned long offset = blk_rq_pos(req) << 9;
/* 源/目的 */
// req->buffer
/* 长度 */
unsigned long len = blk_rq_cur_bytes(req);
//printk("bbbb\n");
if (rq_data_dir(req) == READ)
memcpy(req->buffer, ramblock_buf+offset, len);
else
memcpy(ramblock_buf+offset, req->buffer, len);
/* wrap up, 0 = success, -errno = fail */
if (!__blk_end_request_cur(req, 0))
req = blk_fetch_request(q);
}
}
完整代码如下
/* 参考
* drivers\block\xd.c
* drivers\block\z2ram.c
*/
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/wait.h>
#include <linux/blkdev.h>
#include <linux/mutex.h>
#include <linux/blkpg.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <linux/gfp.h>
#include <asm/uaccess.h>
#include <asm/dma.h>
#define RAMBLOCK_SIZE (1024*1024)
static struct gendisk *ramblock_disk;
static struct request_queue *ramblock_queue;
static DEFINE_SPINLOCK(ramblock_lock);
static int major;
static unsigned char *ramblock_buf;
static const struct block_device_operations ramdisk_fops = {
.owner = THIS_MODULE,
};
static void do_ramblock_request (struct request_queue * q)
{
struct request *req;
static int cnt = 0;
//printk("do_ramblock_request %d\n", ++cnt);
req = blk_fetch_request(q);
//printk("aaaa\n");
while (req) {
/* 数据传输三要素 源、目的 、长度 */
/* 源/目的 */
unsigned long offset = blk_rq_pos(req) << 9;
/* 源/目的 */
// req->buffer
/* 长度 */
unsigned long len = blk_rq_cur_bytes(req);
//printk("bbbb\n");
if (rq_data_dir(req) == READ)
memcpy(req->buffer, ramblock_buf+offset, len);
else
memcpy(ramblock_buf+offset, req->buffer, len);
/* wrap up, 0 = success, -errno = fail */
if (!__blk_end_request_cur(req, 0))
req = blk_fetch_request(q);
}
}
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 设置gendisk其他属性,比如容量 */
major = register_blkdev(0, "ramblock"); /* cat /proc/device */
ramblock_disk->major = major;
ramblock_disk->first_minor = 0;
sprintf(ramblock_disk->disk_name, "ramblock");
ramblock_disk->fops = &ramdisk_fops;
set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);
/* 3. 硬件相关操作 */
ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);
/* 4. 注册 */
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);
kfree(ramblock_buf);
}
module_init(ramblock_init);
module_exit(ramblock_exit);
MODULE_LICENSE("GPL");
四 test
在开发板上操作
1. insmode ramblock
这个时候会提示 ramblock: unknown partition table, 是正常的现象
2. 格式化: mkdosfs /dev/ramblock
3. 挂接: mount /dev/ramblock /tmp/
4. 读写文件: cd /tmp, 在里面vi文件
5. cd /; umount /tmp/
6. 重新挂接,看看上面读写的文件还是否存在: mount /dev/ramblock /tmp/; ls -al
7. cat /dev/ramblock > /mnt/ramblock.bin
8. 在PC上查看 ramblock.bin, 看里面有什么
sudo mount -o loop ramblock.bin /mnt
9. 在PC机上,sudo umount /mnt