struct device中的platform_data是我们自己在编写驱动程序的时候自己定义的设备结构体的指针。
因为之前不是很了解文件系统,所以要探究一下mtd来加深一下印象。
NAND FLASH每一页大小通常是512+16字节,16字节称为OOB区,通常在OOB区存放坏块标记,前面512字节的ECC校验码等。
FLASH的内部存储时MOSFET(金属半场效应管),里面有悬浮门,是真正存储数据的单元。
块是NandFlash的擦除操作的基本单元。
页是NandFlash的写入操作的基本单元。
在写数据之前,要先擦除,然后再写。
在加载完驱动之后,如果没有加入参数要求跳过坏块扫描的话,会主动扫描坏块,建立必要的BBT,以备后面坏块管理使用。
在一个块内,对每一页进行编程的话必须是顺序的,不能是随机的。
通常读取Nor速度比Nand稍快一些,而Nand写入速度快很多,NorFlash特点是芯片内执行(XIP execute in place),不需要初始化CPU取址模块就能直接从中把指令取出来。
NandFlash器件使用复杂的I/O来串行存取数据,8个引脚用来传送控制地址和数据信息。
MTD:memory technology device内存技术设备
MTD将文件系统与底层FLASH存储器进行了隔离
设备节点->MTD设备层->MTD原始设备层->硬件驱动层
MTD块设备(主设备号31) 字符设备(主设备号90)
spi_write_then_read();函数中,先分配spi_transfer 两个内存空间,一个用于写,一个用于读,然后调用spi_message_add_tail(),将spi_transfer的地址分别放入spi_message中,之后填充spi_transfer中的tx_buf和rx_buf,最后调用spi_sync();做i/o操作。该函数是__spi_async()的封装,里面最后还是调用了spi_master->transfer()之前spi驱动注册的回调函数。
如果是spi flash,对flash的操作就是对spi的读写操作。
在xxx_flash_probe()中主要还是对mtd_info结构体(MTD原始设备)的参数赋值,和回调函数的注册。
flash->mtd.type = MTD_NORFLASH; flash->mtd.writesize = 1; flash->mtd.flags = MTD_CAP_NORFLASH; flash->mtd.size = info->sector_size * info->n_sectors; flash->mtd.erase = m25p80_erase; flash->mtd.read = m25p80_read; /* sst flash chips use AAI word program */ if (JEDEC_MFR(info->jedec_id) == CFI_MFR_SST) flash->mtd.write = sst_write; else flash->mtd.write = m25p80_write; flash->mtd.dev.parent = &spi->dev; flash->sector_size = info->sector_size; flash->page_size = info->page_size;
在每次发送命令之前都需要进行写使能,然后发送命令的内容,最后再加上地址。
struct spi_transfer t[2]; struct spi_message m; t.tx_buf = flash->command; t.len = m25p_cmdsz(flash) + FAST_READ_DUMMY_BYTE; spi_message_add_tail(&t, &m); t.rx_buf = buf;t.len = len; spi_message_add_tail(&t, &m); flash->command = OPCODE_READ; flash->command = WR_CMD_TYPE; spi_sync(flash->spi, &m);
最后调用mtd_device_register().如果没有分区的话就调用add_mtd_partition(),否则就调用add_mtd_partitions().
这里有一个概念的区别:struct mtd_part和struct mtd_partition.mtd_partition就是每个子分区的分区信息,mtd_part是指一个分区的实例。真正主分区的信息还是由mtd_info获得.
分配一个分区是函数allocate_partition().
struct mtd_partition { char *name; /* identifier string */ uint64_t size; /* partition size */ uint64_t offset; /* offset within the master MTD space */ uint32_t mask_flags; /* master MTD flags to mask out for this partition */ struct nand_ecclayout *ecclayout; /* out of band layout for this partition (NAND only) */ };
struct mtd_part { struct mtd_info mtd; struct mtd_info *master; uint64_t offset; struct list_head list; };
/* set up the MTD object for this partition */ slave->mtd.type = master->type; slave->mtd.flags = master->flags & ~part->mask_flags; slave->mtd.size = part->size; slave->mtd.writesize = master->writesize; slave->mtd.writebufsize = master->writebufsize; slave->mtd.oobsize = master->oobsize; slave->mtd.oobavail = master->oobavail; slave->mtd.subpage_sft = master->subpage_sft; slave->mtd.name = name; slave->mtd.owner = master->owner; slave->mtd.backing_dev_info = master->backing_dev_info; slave->mtd.dev.parent = master->dev.parent; slave->mtd.read = part_read; slave->mtd.write = part_write;
分配成功后将返回的std_part添加到mtd_partitions中去。
最后调用add_mtd_device(),注册一个mtd设备。在add_mtd_device中会遍历的调用通知链mtd_notifiers中的所有注册函数。
在mtdchar.c中init_mtdchar();中相比一般字符设备注册地不同在于文件系统的注册register_filesystem,内核挂载kern_mount(),最终调用了vfs_kern_mount();
在该函数中,分配vfsmnt结构体,并且挂载文件系统mount_fs();它最终调用了file_system_type原本就已经注册号的mount回调函数。
static struct file_system_type mtd_inodefs_type = { .name = "mtd_inodefs", .mount = mtd_inodefs_mount, .kill_sb = kill_anon_super, };最终mtd_inodefs_mount();还是调用了mount_pseudo();//这是一个通用的挂载文件系统的函数(但是sockfs,pipefs,bdev是不能挂载的).
该函数主要是进行参数的赋值,关键的是三个数据结构struct super_block, struct inode, struct dentry;
s->s_flags = MS_NOUSER; s->s_maxbytes = MAX_LFS_FILESIZE; s->s_blocksize = PAGE_SIZE; s->s_blocksize_bits = PAGE_SHIFT; s->s_magic = magic; s->s_op = ops ? ops : &simple_super_operations;//以便之后new_inode()调用部分回调函数 s->s_time_gran = 1; root = new_inode(s); if (!root) goto Enomem; /* * since this is the first inode, make it number 1. New inodes created * after this must take care not to collide with it (by passing * max_reserved of 1 to iunique). */ root->i_ino = 1; root->i_mode = S_IFDIR | S_IRUSR | S_IWUSR; root->i_atime = root->i_mtime = root->i_ctime = CURRENT_TIME; dentry = d_alloc(NULL, &d_name); if (!dentry) { iput(root); goto Enomem; } dentry->d_sb = s; dentry->d_parent = dentry; d_instantiate(dentry, root);//关键是该函数,将inode信息填充到目录入口中。 s->s_root = dentry; s->s_d_op = dops; s->s_flags |= MS_ACTIVE; return dget(s->s_root);值得注意的是,几个file_operation还是要好好研究一下的
static const struct file_operations mtd_fops = { .owner = THIS_MODULE, .llseek = mtd_lseek, .read = mtd_read, .write = mtd_write, .unlocked_ioctl = mtd_unlocked_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = mtd_compat_ioctl, #endif .open = mtd_open, .release = mtd_close, .mmap = mtd_mmap, #ifndef CONFIG_MMU .get_unmapped_area = mtd_get_unmapped_area, #endif };在/mtd/mtdblock.c中初始化函数中调用了register_mtd_blktrans();最终还是调用了register_blkev();注册块设备。
static struct mtd_blktrans_ops mtdblock_tr = { .name = "mtdblock", .major = 31, .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, };
在在最后调用回调函数mtd_blktrans_ops->add_mtd();在该回调函数中调用了add_mtd_blktrans_dev();
调用alloc_disk();
block_device_operation:
static const struct block_device_operations mtd_blktrans_ops = { .owner = THIS_MODULE, .open = blktrans_open, .release = blktrans_release, .ioctl = blktrans_ioctl, .getgeo = blktrans_getgeo, };当给磁盘分区,在分区上创建文件系统,运行文件系统检查程序,或装载一个分区时,块设备驱动程序会调用open函数。
内核认为每个磁盘都是由512字节大小的扇区所组成的线性数组。所有的I/O请求都将定位在硬件扇区的开始位置,并且每个请求的大小都将是扇区大小的整数倍。
set_capacity();//设定扇区大小.
struct request代表了一个块设备的I/O执行请求.请求被表示为一系列段,每个段都对应内存中的一个缓冲区。如果多个请求都是对磁盘中相邻扇区进行操作,则内核将合并它们,但是内核不会合并在单独request结构中的读写操作。
从本质上讲,一个request结构是作为一个bio结构的链表实现的,bio结构是在底层对部分块设备I/O请求的描述。
bio结构的核心是bi_io_vec的数组
struct bio_vec { struct page *bv_page; unsigned int bv_len; unsigned int bv_offset; };当快设置I/O请求被转换到bio结构后,它将被单独的物理内存页所销毁。驱动程序做的所有工作就是根据这个结构体数组,使用每页传输数据。
blk_init_queue();分配请求队列,传入请求参数mtd_blktrans_request,负责块设备的读写请求(本质还是唤醒之后创建的内核线程wake_up_process())。当请求队列生成的时候,request函数就与该队列绑定在一起。
在分配好请求队列后,要创建一个内核线程mtd_blktrans_thread();
在该线程中首先通过blk_fetch_request来获得请求队列中的请求(最终还是调用__elv_next_request()来获取队列中第一个未完成的请求).
接下来最核心的还是调用do_blktrans_request() 来处理mtd块设备的请求(通过switch来执行读或者写请求)。读写操作都是调用之前注册号的回调函数(最终调用的是driver/mtd/devices/xxx_flash.c中之前已经注册好的与硬件相关的读写函数),同时读写成功之后就调用rq_flush_dcache_pages().
内核使用gendisk结构来表示一个独立的磁盘设备。一旦调用了add_disk,磁盘设备将被集火,并随时会调用它提供的方法。因此在驱动程序完全被初始化并且能够响应对磁盘的请求钱,不要调用add_disk.
对磁盘操作最昂贵的代价总是确定读写数据开始的位置,一旦位置确定了之后,实际用于读取或者写入数据的时间几乎无关紧要的。