1.正确理解块设备驱动的概念 ###############
1.1、块设备和字符设备的差异 @@@@@@
(1)块和字符是两种不同的访问设备的策略
(2)同一个设备可以同时支持块和字符两种访问策略
(3)设备本身的物理特性决定了哪一种访问策略更适合
(4)块设备本身驱动层支持缓冲区,而字符设备驱动层没有缓冲
(5)块设备驱动最适合存储设备
1.2、块设备驱动的特点 @@@@@@@@
(1)字符设备只能顺序访问(如串口发送数据顺序),而块设备可以随机访问(不连续块访问)
(2)传统的机械式块设备(如硬盘、DVD)虽然可以随机访问,但是连续访问效率更高,因此块设备驱动中有排序逻辑将用户的随机访问重新调整成尽量连续访问以提升效率
(3)Nand、SD卡等随机访问效率等同于顺序访问
1.3、块设备相关的几个单位 @@@@@@@
(1)扇区(Sector)
概念来自于早期磁盘,在硬盘、DVD中还有用,在Nand/SD中已经没意义了,扇区是块设备本身的特性,大小一般为512的整数倍,因为历史原因很多时候都向前兼容定义为512.
(2)块(block)
- 扇区是硬件设备传送数据的基本单位,硬件一次传送一个扇区到内存中。与扇区不同,块是虚拟文件系统传送数据的基本单位。
- Linux:块的大小 = 2的幂,而且,不能超过一个页的大小。此外,块还必须是扇区大小的整数倍,所以一个块可以包含若干个扇区。
- x86:页的大小是4096字节,所以块的大小可以是512、1024, 2048、 4096字节
下面的公式表示了块的取值范围:
扇区(512) <块<页(4096) && 块=n×扇区(n为整数)
Linux系统的块大小是可以配置的,默认情况下为1024字节。
(3)段(Section)
- 一个段就是一个内存页或者内存页的一部分。例如页的大小是4096个字节,块的大小为2个扇区,即1024个字节,那么段的大小可以是1024, 2048、 3072, 4096字节。
- 段的大小是块的整数倍,并且不超过一个页。因此Linux内核一次读取磁盘的数据是一个块,而不是一个扇区。
- 页中块的开始位置必须是块的整数倍偏移的位置,也就是0,1024, 2048, 3072,一个大小为1024字节的段可以开始于页的如下位置
(4)页(Page),概念来自于内核,是内核内存映射管理的基本单位。linux内核的页式内存映射名称来源于此。
总结:
- 扇区是由物理磁盘的机械特性决定
- 块缓冲区由内核代码决定
- 段由块缓冲区决定,是块缓存大小的倍数,但不超过一页
注意:块设备驱动和字符设备驱动不同,应用层对块设备驱动的访问一般不是直接操作设备文件(/dev/block/xxx,或者/dev/sdax),而是通过文件系统来简洁操作。(思考裸机阶段时刷机烧录SD卡时说过的对SD卡的2种访问:文件系统下访问和扇区级访问)
2.块设备驱动框架简介 #######
2.1、块设备驱动框图 @@@@@@
块设备的处理过程涉及内核中很多其他的模块。过程简述,如下:
(1)当一个用户程序要向磁盘写数据时,将发出一个write()系统调用给内核。
(2)内核将调用虚拟文件系统中一个适当的函数,将需要写入的文件描述符和文件内容指针传递给这个函数。
(3)内核需要确定写入磁盘的位置,通过映射层确定应该写到磁盘上的哪一个块。
(4)根据磁盘的文件格式调用不同文件格式的写入函数,将数据发送到通用块层。例如, Ext2文件的写入函数与Ext3文件的写入函数是不一样的。这些函数已由内核开发者实现,驱动开发者不需要重写这些函数。
(5)数据到达通用块层,就对块设备发出写请求。内核利用通用块层启动IO调度器,对数据进行排序。
(6)通用块层下面是"IO调度器”。调度器的作用是把物理上相邻的读写磁盘请求合并为一个请求,提高读写的效率。
(7)最后,块设备驱动程序向磁盘发送命令和数据,将数据写入磁盘。这样一次write()操作就完成了。
请求队列:
简单地讲,一个块设备的请求队列就是包含块设备I/O请求的一个队列。这个队列使用链表线性的排列。
请求队列中存储未完成的块设备10请求,并不是所有的I/O块请求,都可以顺利地加入请求队列中。
请求队列中定义了自己能处理的块设备请求限制。这些限制包括:请求的最大尺寸、一个请求能够包含的独立段数、硬盘扇区大小等。
请求队列还提供了一些处理函数,使不同块设备可以使用不同的I/O调度器,甚至不使用10调度其。
一个I/O调度器的作用,是以最大的性能来优化请求的顺序。
大多数IO调度器控制着所有的请求,根据请求执行的顺序和位置对其进行排序,使块设备能够以最快的数据将数据写入和读出。
请求队列使用request_queue结构体来描述,在中定义了该结构体和其相应的操作函数。对于请求队列的这些了解是远远不够的,在后面用到请求队列时,将对其进行详细解释。
2.2、重点结构体 @@@@@@
(1)struct request 对设备的每一次操作(譬如读或者写一个扇区)
(2)struct request_queue request队列
(3)struct bio 通用块层用bio来管理一个请求
(4)struct gendisk 表示一个磁盘设备或一个分区
struct gendisk {
/* major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
int major; /* 设备的主设备号 */
int first_minor; //该磁盘的第一给次设备号
int minors; /*磁盘的次设备数量,也就是分区数量, 如果等于1,表示没有分区 /
char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, mode_t *mode);
/* Array of pointers to partitions indexed by partno.
* Protected with matching bdev lock but stat and other
* non-critical accesses use RCU. Always access through
* helpers.
*/
struct disk_part_tbl *part_tbl; //磁盘分区的数组
struct hd_struct part0; //磁盘分区描述符
const struct block_device_operations *fops; //块设备擦操作函数指针
struct request_queue *queue; //链接磁盘请求队列的指针
void *private_data;
int flags;/*描述磁盘类型的标志。如果磁盘是一个可以移动的,例如软盘和光盘,那么需要设置*/
/*GENHD_FL_REMO-VABLE标志:如果磁盘被初始化为可以使用状态,那|么应该加上GENHD_FL_UP标志*/
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct work_struct async_notify;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};
函数:
分配gendisk
struct gendisk *alloc_disk(int minors) //分配动态gendisk
{
return alloc_disk_node(minors, -1);
}
添加磁盘到系统
void add_disk(struct gendisk *disk)
删除磁盘
void del_gendisk(struct gendisk *disk)
删除gendisk的引用计数
在调用del gendisk()函数后,需要使用put_disk()函数减少gendisk的引用计数,因为在add disk()函数中增加了gendisk的引用技术。
void put_disk(struct gendisk *disk)
{
if (disk)
kobject_put(&disk_to_dev(disk)->kobj);
}
注册块设备
与字符设备的register-chrdev()函数对应的是register blkdev()函数。
对于大多数块设备驱动程序来说,第一个工作就是向内核注册自己。
但是,在Linux 2.6内核中,对register blkdev()函数的调用完全是可选的,内核中的register blkdev()函数的功能已经逐渐减少。
在新内核中,一般只完成两件事情:
- 根据参数分配一个块设备号。
- 在/proc/devices中新增一行数据表示块设备的设备号信息。
int register_blkdev(unsigned int major, const char *name)
major = register_blkdev(0, "my_ramblock");
if (major < 0)
{
printk("fail to regiser my_ramblock\n");
return -EBUSY;
}
major:要申请的主设备号,如果为1表示要用系统分配的
name:设备名,这个名字会在/proc/devices出现
注销设备函数:
void unregister_blkdev(unsigned int major, const char *name)
3.块设备驱动案例分析1 #############
3.1、块设备驱动案例演示 @@@@@@@@
(1)驱动简单介绍
(2)编译
(3)模块安装
(4)查看信息
- cat /proc/devices
- cat /proc/partitions //查看磁盘分区
- ls /dev/
- lsmod
(5)挂载测试
3.2、块设备驱动简单分析 @@@@@@@@@
(1)如何证明块设备驱动真的工作了: 格式化、挂载
查看支持的文件系统:
- /proc/filesystems
- 当前被内核支持的文件系统类型列表文件
- /etc/filesystems
- 系统已经加载的文件系统
- ls /lib/modules/$(uname -r)/kernel/fs/
- 系统包含的文件系统驱动
格式化:mkfs.ext2 /dev/my_ramblcok
挂载: mount -t ext2 /dev/my_ramblcok /tmp
(2)注意各种打印信息
(3)体会块设备驱动的整体工作框架
4.块设备驱动案例分析 #######
4.1、源码分析 @@@@@@
(1)register_blkdev(kernel/block/genhd.c),内核提供的注册块设备驱动的注册接口,在块设备驱动框架中的地位,等同于register_chrdev在字符设备驱动框架中的地位。
(2)blk_init_queue 用来实例化产生一个等待队列,将来应用层对本块设备所做的所有的读写操作,都会生成一个request然后被加到这个等待队列中来。
(3)blk_init_queue函数接收2个参数,第一个是等待队列的回调函数,这个函数是驱动提供的用来处理等待队列中的request的函数(IO调度层通过电梯算法从等待队列中取出一个request,就会调用这个回调函数来处理这个请求),第二个参数是一个自旋锁,这个自旋锁是要求我们驱动提供给等待队列去使用的。
(4)blk_fetch_request函数是IO调度层提供的接口,作用是从request_queue中(按照电梯算法)取出一个(算法认为当前最应该去被执行的一个请求,是被算法排序、合并后的)请求,取出的请求其实就是当前硬件(块设备)最应该去执行的那个读写操作。