块设备驱动程序概念介绍

    系统中能够随机访问特定大小数据片的设备被称作块设备,这些数据片就称作块。最常见的块设备是硬盘。注意,它们都是以安装文件系统的方式使用的——这也是块设备通常的访问方式。
    另一种基本的设备类型是字符设备。字符设备按照字符流的方式被有序访问,像串口和键盘就属于字符设备。
    这两种设备的根本区别在于它们是否可以被随机访问。内核管理块设备比字符设备复杂的多,有一个专门的子系统来管理块设备和对块设备的请求,这一部分在内核中被称作块I/O层。
    我们在应用层对文件的读写访问,比如我们读写一个 1.txt,这个文件实际是存储在块设备上的,那么是谁完成了文件读写到具体块设备扇区的读写转换呢?是文件系统!我们内核中支持 vfat,ext2,ext3,yaffs2,jffs2 等非常多种文件系统,然而为了统一多种文件系统,内核抽象出了VFS虚拟文件系统,实际的文件系统向上为VFS提供统一的接口。比如一个 write 操作大体的操作流程如下图所示:

    整个块设备的框架:


一、基本概念

    块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,而最常见的大小是512字节。内核执行的所有磁盘操作都是按照块进行的,由于扇区是块设备的最小可寻址单元,所以块要比扇区2的整数倍,所以通常是 512 字节 、1k 、2k 。


    和磁盘相关的其它术语还有——簇、柱面、磁头等,这些术语都和具体的块设备相关,一般情况下用户空间的软件都用不到这些概念。


二、缓冲区和缓冲区头

    当一个块被调入内存时(也就是说,在读入后或等待写出时),它要存储在一个缓冲区中,每个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示。由于内核在处理数据时需要一些相关的控制信息,比如属于哪一个块设备、块对应于哪个缓冲区等,所以内一个缓冲区都有一个对应的描述符,该描述符用 buffer_head 结构体表示,被称作缓冲区头。

struct buffer_head {
	unsigned long b_state;		/* 缓冲区状态标志 */
	struct buffer_head *b_this_page;/* 页面中的缓冲区 */
	struct page *b_page;		/* 存储缓冲区的页面 */

	sector_t b_blocknr;		/* 逻辑块号 */
	size_t b_size;			/* 块大小 */
	char *b_data;			/* 页面中的缓冲区 */

	struct block_device *b_bdev;    <span style="font-family: Arial, Helvetica, sans-serif;">/* 所属的块设备 */</span>
	bh_end_io_t *b_end_io;		/* I/O 完成方法 */
 	void *b_private;		/* reserved for b_end_io */
	struct list_head b_assoc_buffers; /* associated with another mapping */
	struct address_space *b_assoc_map;	/* mapping this buffer is
						   associated with */
	atomic_t b_count;		/* 引用计数 */
};
    与缓冲区对应的磁盘物理块由 b_blockbr 索引,该值是 b_bdev 所指明的块设备中的逻辑块号。

    与缓冲区对应的内存物理页由 b_page 表示,另外,b_data 直接指向相应的块(它位于 b_page),块大小由 b_size 表示。

    缓冲区头的目的在于描述磁盘和物理内存缓冲区之间的映射关系。
    缓冲区头仅仅能描述一个缓冲区,当作为所有I/0操作的容器使用时,就会变成对多个 buffer_head 结构体进行操作,势必会造成不必要的负担和空间浪费,因此引入了一个灵活且轻量级的容器——bio 结构体。


三、bio 结构体

    每一个块 I/O 请求都通过一个 bio 结构体表示。每一个请求包含一个或者多个块,这些块存储在 bio_vec 结构体数组中,这些结构体描述了每一个片段在物理页面中的实际位置,并像向量一样被组织在一起。I/O 操作的第一个片段由 b_io_vec 结构体所指向,其他的片段在其后依次放置,共有 bi_vcnt 个片段。当块 I/O 层开始执行请求,需要使用各个片段时,bi_idx 会不断更新,从而总指向当前的片段。

struct bio {
//该bio结构所要传输的第一个(512字节)扇区:磁盘的位置
    sector_t bi_sector;
    struct bio *bi_next;     //请求链表
    struct block_device *bi_bdev;    //相关的块设备
    unsigned long bi_flags;    //状态和命令标志
    unsigned long bi_rw;     //读写
    unsigned short bi_vcnt;    //bio_vesc偏移的个数
    unsigned short bi_idx;     //bi_io_vec的当前索引
    unsigned short bi_phys_segments;    //结合后的片段数目
    unsigned short bi_hw_segments;    //重映射后的片段数目
    unsigned int bi_size; <span style="white-space:pre">	</span>      //I/O计数
    unsigned int bi_hw_front_size;    //第一个可合并的段大小;
    unsigned int bi_hw_back_size;    //最后一个可合并的段大小
    unsigned int bi_max_vecs;     //bio_vecs数目上限
    struct bio_vec *bi_io_vec;    //bio_vec链表:内存的位置
    bio_end_io_t *bi_end_io;//I/O完成方法
    atomic_t bi_cnt; //使用计数
    void *bi_private; //拥有者的私有方法
    bio_destructor_t *bi_destructor;//销毁方法
};

struct bio_vec {
	struct page	*bv_page;  // 这个缓冲区所在的物理页面
	unsigned int	bv_len;    // 这个缓冲区的大小
	unsigned int	bv_offset; // 缓冲区在页面的偏移
};

四、请求队列
    块设备将对它们的I/O操作请求保存在请求队列中,该队列由 request_queue 结构体表示。只要请求队列不为空,队列对应的块设备驱动程序就会从队列头获取请求,然后将其送入对应的块设备上去,请求队列中的每一项都是一个单独的请求,由 request结构体表示。
    队列中的请求由结构体 request 表示,因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个 bio 结构体组成(每个 bio 结构体本身就可以描述多个块)。

五、I/O 调度程序
    磁盘寻址是整个计算机中最慢的操作之一,每一次寻址——定位硬盘磁头到特定块上的某个位置。为了优化寻址操作,内核会在提交 I/O 请求之前,先执行名为合并与排序的预操作。在内核中负责提交 I/O 请求的子系统被称作 I/O 调度程序。
    I/O 调度程序通过两种方法来减少磁盘的寻址时间:合并与排序,合并指将两个或多个请求结合成一个新请求。如果之前队列中已经存在一个请求,它访问的磁盘扇区和当前请求访问的磁盘扇区相邻,比如同一个文件中早些时候被读取的数据区,那么这两个请求就可以合并成为一个单个对多个相邻磁盘扇区操作的新请求。和并请求可以明显减少对系统的开销和磁盘的寻址次数。
    如果队列中没有可以合并的扇区,此时就无法将当前请求与其他请求合并,当然,可以将其插入到请求队列的尾部。
    排序是指整个队列请求将按照扇区增长的方向有序排列,目的不仅是为了缩短单独一次的请求寻址时间,更重要优化在于通过保持磁头以直线方向移动,缩短了所有请求的磁盘寻址时间。该排列算法类似于电梯调度,所以 I/O 调度程序被称为电梯调度。

六、gendisk 结构
    内核使用gendisk结构来表示一个独立的磁盘设备。内核还使用gendisk结构表示分区,在此结构中,很多成员必须由驱动程序来进行初始化。
struct gendisk {
    int major; //主设备号
    int first_minor; //第一个从设备号
    int minors;
/* 描述被磁盘使用的设备号的成员.一个驱动器必须使用最少一个次编号.
 *如果你的驱动会是可分区的,但是(并且大部分应当是),你要分配一个次编号给每个可能 的分区.次编号的一个普通的值是 16, 
 *它允许"全磁盘"设备盒 15 个分区. 一些磁盘驱动使用 64 个次编号给每个设备.
 */
    char disk_name[32]; //应当被设置为磁盘驱动器名子的成员. 它出现在 /proc/partitions 和 sysfs.
    struct hd_struct **part; /* [indexed by minor] */
    struct block_device_operations *fops;// 设备操作集合.
    struct request_queue *queue;//被内核用来管理这个设备的 I/O 请求的结构;
    void *private_data;//块驱动可使用这个成员作为一个指向它们自己内部数据的指针.
    sector_t capacity;
// 这个驱动器的容量,以512-字节扇区来计.sector_t类型可以是64位宽.驱动不应当直接设置这个成员;相反,传递扇区数目给set_capacity.
    int flags;
// 一套标志(很少使用),描述驱动器的状态.如果你的设备有可移出的介质,
// 你应当设置GENHD_FL_REMOVABLE.CD-ROM驱动器可设置 GENHD_FL_CD. 
// 如果, 由于某些原因, 你不需要分区信息出现在 /proc/partitions, 设置 GENHD_FL_SUPPRESS_PARTITIONS_INFO.
    struct device *driverfs_dev; // FIXME: remove
    struct device dev;
    struct kobject *holder_dir;
    struct kobject *slave_dir;
    struct timer_rand_state *random;
    int policy;
    atomic_t sync_io; /* RAID */
    unsigned long stamp;
    int in_flight;
    #ifdef CONFIG_SMP
    struct disk_stats *dkstats;
    #else
    struct disk_stats dkstats;
    #endif
    struct work_struct async_notify;
};

七、内核 ll_rw_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);

















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值