Linux块设备驱动
块设备驱动
块设备驱动的引入
1. 简单字符驱动程序思想
当应用程序的 open,read,write 等函数要操作“硬件”时,自然引入了“驱动程序”的概念,最简单的方式是 APP 调用 open 时,驱动程序的“drv_open”函数被调用等等。
2. 块设备驱动程序思想
若块设备驱动程序也按照以上字符驱动程序的简单思想来写:
- 硬盘
磁盘的读写其实非常快,慢在机械结构读写装置的定位上面,从一个“磁头”的某“柱面”某“扇区”读到数据后(步骤 R0),跳到另一个“磁头”的某“柱面”的某“扇区”去写(步骤 W),接着再跳回原“磁头”相同柱面的下一个“扇区”去读(步骤R1)。慢就慢在读写扇区的跳转过程中。若按“字符设备”中的“opne” ,“read”,"write"方式,则总体效率在硬盘的读写上会非常低。
上面过程是R0 ->W->R1,这个步骤跳转 2 次。如何优化这个步骤,增加读写效率,步骤为: R0->R1->W。这个步骤跳转 1 次,这样效率就会高些。
总结:先不执行而是放入队列,优化后再执行(对硬盘有这种要求)。用“字符设备驱动”程序那样读写时就会在硬盘上跳来跳去,整体效率会非常低。所以有必要引入“优化过程”,就是读写先不执行,先放到某个“队列”中去。(调整顺序)
- Flash
对于Flash,是“块”里有一个一个的扇区,Flash一般是要先擦除再写,并且擦除是以块为单位的。假如现在要写“扇区0”和“扇区1”,若用字符设备驱动的读写方式来读写:
写扇区0的过程:
① 要写时,先把这整块读到一个 buf 中
② 然后修改 buf 中扇区 0 的数据
③ 这时再擦除整块
④ 再把修改过扇区 0 的数据的 buf 烧写到整块
写扇区1的过程:
① 要写时,先把这整块读到一个 buf 中
② 然后修改 buf 中扇区 1的数据
③ 这时再擦除整块
④ 再把修改过扇区 1的数据的 buf 烧写到整块
那么要修改多个扇区时,会擦除烧写多次,总体效率会很低
优化:
① 先不执行
② 合并操作后再执行
合并:合并后只需要一次。
a. 读出整块到 buf 中
b. 在 buf 中修改扇区 0 和扇区 1
c. 擦除
d. 烧写
综上,不论是Flash还是硬盘,块设备不能像字符设备那样提供读写函数,这样效率比较低。其读写优化的思想为:① 先把读写放入队列,先不执行。② 优化后再执行。
块设备驱动框架
1. 层次框架
对于一个普通文件 1.txt 的读写会转成对块设备的读写,即要读写哪个扇区。从文件的读写转成对扇区的读写,中间会涉及到“文件系统”。应用程序读写一个普通的文件,最终会转换成操作硬件,由“块设备驱动程序”来完成。
普通的文件如何转换成对扇区的读写,是由“文件系统”转换完成的,最终会调用到ll_rw_block这个通用的函数,ll_rw_block会把“读写”放入队列,调用队列的处理函数去优化(调整顺序、合并)再执行。(如何知道是ll_rw_block,可以看《LINUX内核源代码情景分析》)文件系统不是我们关心的重点,分析块设备驱动程序,从ll_rw_block这个函数开始分析。
2. 分析ll_rw_block
文件系统把一个普通文件的读写转换成块设备扇区的读写,最终会调用这个底层的函数ll_rw_block,作用为:
- 把“读写”放入队列
- 调用队列的处理函数
这个函数在内核“fs”目录下,该目录下有各种各样的文件系统以及很多通用的文件,该函数在buffer.c中,buffer.c是fs目录下的通用文件。
/*
* 参数1 - op:表示是读或是写
* 参数2 - op_flags:op的标志位
* 参数3 - nr:bhs数组的个数
* 参数4 - bhs[]:buffer_head类型的数组,表示数据传输的三要素(源,目的和长度)
*/
void ll_rw_block(int op, int op_flags, int nr, struct buffer_head *bhs[]);
/* struct buffer_head 结构代表一个要进行读或者写的数据块 */
struct buffer_head {
...
sector_t b_blocknr; /* 数据块号 */
size_t b_size; /* 数据块大小 */
char *b_data; /* 数据块内容 */
struct block_device *b_bdev; /* 数据块所属设备 */
...
};
/* ll_rw_block流程梳理 */
for (i = 0; i < nr; i++) //开始就是for循环
struct buffer_head *bh = bhs[i]; //bh 等于这个参数4数组的某一项
submit_bh(op, op_flags, bh); //提交buffer_head结构的bh
struct bio *bio; //使用buffer_head来构造bio(block input/output)
submit_bio(bio); //提交bio
generic_make_request(bio); //使用bio构造请求,把请求放入队列
struct request_queue *q = bdev_get_queue(bio->bi_bdev); //找到队列
ret = q->make_request_fn(q, bio); //调用队列中的make_request_fn函数
/* q->make_request_fn函数是什么 */
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
blk_init_queue_node(rfn, lock, NUMA_NO_NODE);
q = blk_init_allocated_queue(uninit_q, rfn, lock);
q->request_fn = rfn; //队列的处理函数为rfn
blk_queue_make_request(q, blk_queue_bio);
q->make_request_fn = blk_queue_bio; //make_request_fn的默认函数为“blk_queue_bio”
/* blk_queue_bio函数流程梳理 */
static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
/* 注:elv是电梯调度算法。以elv 算法把bio合并到请求队列 */
/* 电梯调度算法:假如1楼A要上,2楼B要下,三楼C要上,则电梯不会让A上了再让B下了最后再跑到3楼让C上,而是让电梯从1楼上到头,再下来,一次传送同样方向的人,在一个方向上合并起来运输 */
el_ret = elv_merge(q, &req, bio); //merge合并。这里先尝试合并。
init_request_from_bio(req, bio); //若合并不成功,就用bio构造请求
add_acct_request(q, req, where); //把请求放到队列中去
__blk_run_queue(q); //执行队列
__blk_run_queue_uncond(q);
q->request_fn(q); //其实执行队列的方式就是调用队列的处理函数(request_fn)
块设备驱动程序编写
参考:drivers\block\z2ram.c,使用内存模拟硬盘
LINUX 驱动程序中各种驱动都构造了一个结构体 ,块设备驱动也有一个结构体gendisk ,表示磁盘设备。
1.分配 gendisk 结构体,使用allock_disk函数
static struct gendisk *z2ram_gendisk;
z2ram_gendisk = alloc_disk(1);
alloc_disk(int minors)需要参数“minors”是指次设备号个数,即“分区个数+0”,0是指整个磁盘。当minors=1时,就是把整个磁盘当成一个分区,则不能再创建其他分区,如写成16,则最多可以创建15个分区。对于一个块设备,次设备为“0”时,表示整个磁盘。如“/dev/sda”。次设备号“1”、“2”等表示磁盘的第几个主分区,次设备号从“5”开始是“扩展分区”
2.设置gendisk结构体
① 分配/设置一个队列:request_queue,提供读写能力,使用blk_init_queue函数
static struct request_queue *z2_queue;
static void do_z2_request(struct request_queue *q)
{
struct request *req;
req = blk_fetch_request(q);
while (req) {
...
if (rq_data_dir(req) == READ)
memcpy(buffer, (char *)addr, size);
else
memcpy((char *)addr, buffer, size);
...
}
}
/*
* 参数1,执行处理队列的函数
* 参数2,一个自旋锁 static DEFINE_SPINLOCK(z2ram_lock);
*/
z2_queue = blk_init_queue(do_z2_request, &z2ram_lock);
/* 之前分析把“文件读写”转成“扇区读写”最终调用ll_rw_block,将“扇区的读写”会放入这个队列里面:
* 把“buffer_head”构造为“bio”,把“bio”放入队列,调用队列的“q->make_request_fn”,即blk_queue_bio函数
* 当我们初始化队列时,提供了一个默认的构造请求的函数blk_queue_bio。这个函数最终会执行到q->request_fn(q),即do_z2_request函数来处理读写
*/
② 设置gendisk其他信息。(提供磁盘属性:磁盘容量,扇区大小等)
/* 主设备号 */
z2ram_gendisk->major = Z2RAM_MAJOR;
/* 第一个次设备号是什么和块设备的名字 */
z2ram_gendisk->first_minor = 0;
sprintf(z2ram_gendisk->disk_name, "z2ram");
/* fops:操作函数,可以是空函数 */
z2ram_gendisk->fops = &z2_fops;
/* 设置队列,将z2_queue放到z2ram_gendisk里 */
z2ram_gendisk->queue = z2_queue;
/* 容量:设置容量时,时以扇区为单位 */
set_capacity(z2ram_gendisk, z2ram_size >> 9);
3.注册gendisk结构体,使用add_disk函数
/* 注册gendisk结构体 */
add_disk(z2ram_gendisk);
/* 注册块设备,为块设备分配Z2RAM_MAJOR主设备号,使其在cat /proc/device能看到该块设备 */
blk_register_region(MKDEV(Z2RAM_MAJOR, 0), Z2MINOR_COUNT, THIS_MODULE, z2_find, NULL, NULL);
MTD块设备驱动
MTD块设备驱动框架
块设备驱动可以分为ramblock、硬盘、emmc和MTD等
- 文件系统:知道怎么去优化读写 - ll_rw_block
- 块设备驱动:知道怎么去优化读写 - gendisk结构体
- MTD字符设备驱动:/dev/mtdchar.c
- MTD块设备驱动:/dev/mtdblock.c和/dev/mtd_blkdevs.c
- 硬件协议:知道发送什么去擦除、读写(发送什么指令)
- 硬件操作:知道怎么发送命令和地址(NAND控制器、spi通讯等)
对于不同的Flash,例如NOR、NAND和SPI_NOR,其硬件协议和硬件操作不同,但使用同一个MTD驱动框架,最终都是构造一个mtd_info结构体,然后调用mtd_device_parse_register函数去解析注册MTD设备,生成对应的字符设备和块设备。
struct mtd_info {
u_char type; /* MTD类型,包括MTD_NORFLASH,MTD_NANDFLASH等(可参考mtd-abi.h) */
uint32_t flags; /* MTD属性标志,MTD_WRITEABLE,MTD_NO_ERASE等(可参考mtd-abi.h) */
uint64_t size; /* MTD设备的大小 */
uint32_t erasesize; /* MTD设备的擦除单元大小 */
uint32_t writesize; /* 最小的可写单元的字节数,对nor是字节,对nand为一页 */
uint32_t writebufsize; /* MTD写缓冲区大小 */
uint32_t oobsize; /* OOB字节数 */
uint32_t oobavail; /* 可用的OOB字节数 */
...
const char *name; /* 名字 */
int index; /* 索引号 */
...
int (*_erase) (struct mtd_info *mtd, struct erase_info *instr); /* 擦除函数 */
int (*_read) (struct mtd_info *mtd, loff_t from, size_t len,
size_t *retlen, u_char *buf); /* 读函数 */
int (*_write) (struct mtd_info *mtd, loff_t to, size_t len,
size_t *retlen, const u_char *buf); /* 写函数 */
...
void *priv;
struct module *owner;
struct device dev;
};
分析mtd_device_parse_register
/*
* 参数1,mtd-mtd_info原始设备结构体
* 参数2,types-分区解析类型,可以是cmdlinepart和ofpart
* 参数3,parser_data-代解析的分区数据,由cmdline或者设备树传入
* 参数4,parts-分区信息(写死在代码中,不由cmdline或设备树决定)
* 参数5,nr_parts-分区个数
*/
int mtd_device_parse_register(struct mtd_info *mtd, const char * const *types,
struct mtd_part_parser_data *parser_data,
const struct mtd_partition *parts,
int nr_parts)
ret = parse_mtd_partitions(mtd, types, &parsed, parser_data); //解析分区
ret = mtd_add_device_partitions(mtd, &parsed); // 添加mtd设备(分区)
ret = add_mtd_device(mtd); // 添加mtd设备
struct mtd_notifier *not;
/* drivers/mtd/mtdchar.c中已注册了主设备号为90的mtd字符设备 */
/* #define MTD_DEVT(index) MKDEV(MTD_CHAR_MAJOR, (index)*2) */
mtd->dev.devt = MTD_DEVT(i);
error = device_register(&mtd->dev); // 创建dev/mtd*设备
device_create(&mtd_class, mtd->dev.parent, MTD_DEVT(i) + 1, NULL,"mtd%dro", i); //创建dev/mtd*ro设备
list_for_each_entry(not, &mtd_notifiers, list)
not->add(mtd); //遍历链表mtd_notifiers,调用not的add函数
/* mtd_notifiers在哪设置? 看drivers/mtd/mtdblock.c和drivers/mtd/mtd_blkdevs.c*/
/* mtdblock.c的入口函数 */
static int __init init_mtdblock(void)
register_mtd_blktrans(&mtdblock_tr);
list_add(&tr->list, &blktrans_majors); //tr即mtdblock_tr
register_mtd_user(&blktrans_notifier);
list_add(&new->list, &mtd_notifiers); //new即blktrans_notifier
/* not->add即blktrans_notifier的add函数blktrans_notify_add */
/* mtd_blkdevs.c中 */
static struct mtd_notifier blktrans_notifier = {
.add = blktrans_notify_add,
.remove = blktrans_notify_remove,
};
/* 分析blktrans_notify_add */
static void blktrans_notify_add(struct mtd_info *mtd)
struct mtd_blktrans_ops *tr;
list_for_each_entry(tr, &blktrans_majors, list)
tr->add_mtd(tr, mtd); //遍历链表blktrans_majors,调用tr的add_mtd函数
/* blktrans_majors在哪设置?同样看mtdblock.c的入口函数 */
/* tr->add_mtd即mtdblock_tr的add_mtd函数mtdblock_add_mtd */
/* mtdblock.c中 */
static struct mtd_blktrans_ops mtdblock_tr = {
.name = "mtdblock",
.major = MTD_BLOCK_MAJOR,
.part_bits = 0,
.blksize = 512,
.open = mtdblock_open,
.flush = mtdblock_flush,
.release = mtdblock_release,
.readsect = mtdblock_readsect,
.writesect = mtdblock_writesect,
.add_mtd = mtdblock_add_mtd,
.remove_dev = mtdblock_remove_dev,
.owner = THIS_MODULE,
};
/* 分析mtdblock_add_mtd */
static void mtdblock_add_mtd(struct mtd_blktrans_ops *tr, struct mtd_info *mtd)
add_mtd_blktrans_dev(&dev->mbd)
gd = alloc_disk(1 << tr->part_bits);
new->rq = blk_init_queue(mtd_blktrans_request, &new->queue_lock);//mtd_blktrans_request就是块设备最终的读写处理函数
gd->queue = new->rq;
device_add_disk(&new->mtd->dev, gd); /* 也就是add_disk */
/* add_disk实际也是调用device_add_disk */
static inline void add_disk(struct gendisk *disk)
{
device_add_disk(NULL, disk);
}
MTD字符设备读写
drivers/mtd/mtdchar.c
static const struct file_operations mtd_fops = {
.owner = THIS_MODULE,
.read = mtdchar_read,
.write = mtdchar_write,
.unlocked_ioctl = mtdchar_unlocked_ioctl,
.open = mtdchar_open,
.release = mtdchar_close,
...
};
static ssize_t mtdchar_read(struct file *file, char __user *buf, size_t count,loff_t *ppos)
ret = mtd_read(mtd, *ppos, len, &retlen, kbuf);
ret_code = mtd->_read(mtd, from, len, retlen, buf); //最终调用mtd_info结构体中的_read函数
static ssize_t mtdchar_write(struct file *file, const char __user *buf, size_t count,loff_t *ppos)
ret = mtd_write(mtd, *ppos, len, &retlen, kbuf);
mtd->_write(mtd, to, len, retlen, buf);//最终调用mtd_info结构体中的_write函数
static int mtdchar_ioctl(struct file *file, u_int cmd, u_long arg)
case MEMERASE64:
ret = mtd_erase(mtd, erase);
mtd->_erase(mtd, instr); //最终调用mtd_info结构体中的_erase函数
MTD块设备读写
drivers/mtd/mtd_blkdevs.c
最终调用mtd_blktrans_request处理函数
static void mtd_blktrans_request(struct request_queue *rq)
queue_work(dev->wq, &dev->work); //将dev->work添加到dev->wq工作队列中,并唤醒相应的线程处理函数
/* 这个工作队列是在哪里设置,工作又是在哪里初始化的? */
add_mtd_blktrans_dev(&dev->mbd)
new->wq = alloc_workqueue("%s%d", 0, 0, tr->name, new->mtd->index);
INIT_WORK(&new->work, mtd_blktrans_work);
/* mtd_blktrans_work处理函数 */
static void mtd_blktrans_work(struct work_struct *work)
do_blktrans_request(dev->tr, dev, req);
if (rq_data_dir(req) == READ)
tr->readsect(dev, block, buf)
else
tr->writesect(dev, block, buf)
/* tr在哪里定义 */
/* mtdblock.c中 */
static struct mtd_blktrans_ops mtdblock_tr = {
.name = "mtdblock",
.major = MTD_BLOCK_MAJOR,
.part_bits = 0,
.blksize = 512,
.open = mtdblock_open,
.flush = mtdblock_flush,
.release = mtdblock_release,
.readsect = mtdblock_readsect,
.writesect = mtdblock_writesect,
.add_mtd = mtdblock_add_mtd,
.remove_dev = mtdblock_remove_dev,
.owner = THIS_MODULE,
};
tr->readsect(dev, block, buf)
mtdblock_readsect
do_cached_read
ret = mtd_read(mtd, pos, size, &retlen, buf);
mtd->_read //最终调用mtd_info结构体中的_read函数
tr->writesect(dev, block, buf)
do_cached_write
erase_write
ret = mtd_erase(mtd, &erase);
mtd->_erase //最终调用mtd_info结构体中的_erase函数
ret = mtd_write(mtd, pos, len, &retlen, buf);
mtd->_write //最终调用mtd_info结构体中的_write函数
MTD_FLASH驱动的编写
关键:设置mtd_info结构体,使用mtd_device_parse_register注册mtd设备
SPI_NOR驱动程序编写
参考drivers\mtd\devices\m25p80.c
① 分配spi_nor结构体
② 设置spi_nor结构体
...
nor->read = m25p80_read;
nor->write = m25p80_write;
nor->write_reg = m25p80_write_reg;
nor->read_reg = m25p80_read_reg;
...
③ 硬件相关设置,在m25p80_write_reg等函数中使用spi的通讯方式
④ 使用spi_nor_scan函数,在里面设置mtd_info结构体,同时也涉及到spi_nor的硬件协议,发送什么指令进行什么操作,设置了读、写、擦、使能spi_nor等指令
int spi_nor_scan(struct spi_nor *nor, const char *name, enum read_mode mode)
{
struct mtd_info *mtd = &nor->mtd;
...
info = spi_nor_read_id(nor); //读flash的id,从内核维护的spi_nor_ids表格中获取具体型号的flash信息
...
/* 设置mtd_info结构体 */
mtd->type = MTD_NORFLASH;
mtd->writesize = 1;
mtd->flags = MTD_CAP_NORFLASH;
mtd->size = info->sector_size * info->n_sectors;
mtd->_erase = spi_nor_erase;
mtd->_read = spi_nor_read;
...
/* 设置操作指令 */
nor->erase_opcode = SPINOR_OP_SE;
nor->read_opcode = SPINOR_OP_READ;
nor->program_opcode = SPINOR_OP_PP;
...
}
⑤ 使用mtd_device_parse_register注册mtd设备
NOR驱动编写
参考drivers\mtd\maps\physmap.c
① 分配map_info结构体
② 设置物理基地址(phys), 大小(size), 位宽(bankwidth), 虚拟基地址(virt),并初始化
info->map[i].name = dev_name(&dev->dev);
info->map[i].phys = dev->resource[i].start;
info->map[i].size = resource_size(&dev->resource[i]);
info->map[i].bankwidth = physmap_data->width;
info->map[i].virt = devm_ioremap(&dev->dev, info->map[i].phys,
info->map[i].size);
simple_map_init(&info->map[i]);
③ 使用do_map_probe函数,返回mtd_info结构体
info->mtd[i] = do_map_probe(*probe_type, &info->map[i]);
//*probe_type表示使用不同的标准
static const char * const rom_probe_types[] = {
"cfi_probe", "jedec_probe", "qinfo_probe", "map_rom", NULL };
do_map_probe
ret = drv->probe(map);
cfi_probe
mtd_do_chip_probe(map, &cfi_chip_probe); //使用cfi标准从flash中读出相关信息
jedec_probe
mtd_do_chip_probe(map, &jedec_chip_probe); //从内核维护的jedec_table匹配对应型号的nor
④ 使用mtd_device_parse_register注册mtd设备
NAND驱动编写
参考drivers\mtd\maps\nand\hifmc100\hifmc100_os.c
① 分配nand_chip结构体
② 设置nand_chip结构体
③ 使用nand_scan函数设置mtd_info结构体,使用mtd_device_parse_register注册mtd设备
static int hisi_spi_nand_probe(struct platform_device *pltdev)
struct hifmc_host *host;
struct nand_chip *chip;
struct mtd_info *mtd;
/* 分配nand_chip结构体 */
host = devm_kzalloc(dev, len, GFP_KERNEL);
host->chip = chip = (struct nand_chip *)&host[1];
host->mtd = mtd = nand_to_mtd(chip);
/* 设置nand_chip结构体 */
result = hifmc100_spi_nand_init(chip);
/* 使用nand_scan函数设置mtd_info结构体 */
result = hifmc_nand_scan(mtd);
nand_scan(mtd, chip_num)
/* 使用mtd_device_parse_register注册mtd设备 */
result = mtd_device_register(mtd, NULL, 0);
mtd_device_parse_register(mtd, NULL, NULL, NULL, 0)