一、简介

Linux三大驱动类型包括字符驱动、块驱动和网络驱动。
块设备是针对存储设备的,比如 SD卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等;
块驱动和字符驱动的区别如下:
1、字符设备是以字节为单位进行数据传输的,不需要缓冲;
2、块设备只能以块为单位进行读写访问,块是linux虚拟文件系统(VFS)基本的数据传输单位,块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,根据回写机制条件将缓冲区中的数据写入块设备中;
3、块设备驱动不可以通过节点直接访问,它需要通过文件系统挂载到目录下访问。当执行mount时则调用open,执行umount命令则调用release。

二、编写块驱动的总过程

1、注册块设备驱动;
2、分配并初始化gendisk;
3、分配、初始化请求队列,绑定请求队列和请求函数;
4、给gendisk的major、disk_name、fops、queue等成员赋值;
5、设置设备容量(单位为扇区);
6、将gendisk添加到内核用于系统调用。

在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,在驱动中用request结构体描述。但对于一些磁盘设备而言请求的速度很慢,这时候内核就提供一种队列的机制把这些I/O请求添加到队列中(即:请求队列),在驱动中用request_queue结构体描述。在向块设备提交这些请求前内核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责提交 I/O 请求, 调度程序将磁盘资源分配给系统中所有挂起的块 I/O 请求,其工作是管理块设备的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求到设备。

由通用块层(Generic Block Layer)负责维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系。在通用块层中,通常用一个bio结构体来对应一个I/O请求。

Linux提供了一个gendisk数据结构体,用来表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行访问。在gendisk中有一个类似字符设备中file_operations的硬件操作结构指针,是block_device_operations结构体。

在这里插入图片描述
编写块设备驱动时,使用的一些单位介绍:
1、扇区(Sectors):任何块设备硬件对数据处理的基本单位。通常,1个扇区的大小为512字节。(对设备而言)

2、块 (Blocks):由Linux制定对内核或文件系统等数据处理的基本单位。通常,1个块由1个或多个扇区组成。(对Linux操作系统而言)

3、段(Segments):由若干个相邻的块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。

三、具体函数源码详解

块驱动相对于字符设备驱动,块驱动中是没有read、write函数,而是通过申请request,然后用bio结构体描述具体储存信息,这些结构体都是gendisk结构体的成员。

request结构体:
在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,在驱动中用request结构体描述。但对于一些磁盘设备而言请求的速度很慢,这时候内核就提供一种队列的机制把这些I/O请求添加到队列中(即:请求队列),在驱动中用request_queue结构体描述。在向块设备提交这些请求前内核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责提交I/O请求,调度程序将磁盘资源分配给系统中所有挂起的块I/O请求,其工作是管理块设备的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求到设备。

bio结构体:
由通用块层(Generic Block Layer)负责维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系。在通用块层中,通常用一个bio结构体来对应一个I/O请求。

gendisk数据结构体:
Linux提供了一个gendisk数据结构体,用来表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行访问。在gendisk中有一个类似字符设备中file_operations的硬件操作结构指针,是block_device_operations结构体。
3.1 block_device 结构体
内核使用 block_device 表示块设备,block_device 为一个结构体,定义在 include/linux/fs.h 文件中

struct block_device {
	dev_t bd_dev; 	/* not a kdev_t - it's a search key */
	int bd_openers;
	struct inode *bd_inode; 	/* will die */
	struct super_block *bd_super;
	struct mutex bd_mutex; 	/* open/close mutex */
	struct list_head bd_inodes;
	void * bd_claiming;
	void * bd_holder;
	int bd_holders;
	bool bd_write_holder;
#ifdef CONFIG_SYSFS
	struct list_head bd_holder_disks;
#endif
	struct block_device *bd_contains;
	unsigned bd_block_size;
	struct hd_struct *bd_part;
	/*number of times partitions within this device have been opened.*/
	unsigned bd_part_count;
	int bd_invalidated;
	/*  bd_disk成员变量为gendisk结构体指针类型,内核使用block_device来表示
	具体块设备对象(硬盘/分区),若是硬盘的话bd_disk就指向通用磁盘结构gendisk */
	struct gendisk *bd_disk;  
	struct request_queue *bd_queue;
	struct list_head bd_list;
	unsigned long bd_private;
	/* The counter of freeze processes */
	int bd_fsfreeze_count;
	/* Mutex for freeze */
	struct mutex bd_fsfreeze_mutex;
};

3.2 gendisk 结构体
linux 内核使用 gendisk 结构体来描述一个磁盘设备,定义在 include/linux/genhd.h 中

struct gendisk { 
	int major; 			/* 磁盘设备的主设备号 */ 
	int first_minor;	/* 磁盘的第一个次设备号 */
	int minors;  		/* 磁盘的次设备号数量,即磁盘的分区数量 */ 
	char disk_name[DISK_NAME_LEN]; 	/* name of major driver */ 
	char *(*devnode)(struct gendisk *gd, umode_t *mode); 
	unsigned int events; 	/* supported events */ 
	unsigned int async_events; 	/* async events, subset of all */ 
	/* 磁盘对应的分区表 */
	struct disk_part_tbl __rcu *part_tbl; 
	struct hd_struct part0; 
	const struct block_device_operations *fops; /* 块设备操作集 */
	struct request_queue *queue; /* 磁盘对应的请求队列 */
	void *private_data; /*私有数据*/
	int flags; 
	struct device *driverfs_dev; // FIXME: remove 
	struct kobject *slave_dir; 
	struct timer_rand_state *random; 
	atomic_t sync_io; /* RAID */ 
	struct disk_events *ev; 
#ifdef CONFIG_BLK_DEV_INTEGRITY 
	struct blk_integrity *integrity; 
#endif 
	int node_id;
};

3.3 block_device_operations结构体
块设备操作集,定义在 include/linux/blkdev.h 中

struct block_device_operations {
	int (*open) (struct block_device *, fmode_t);//打开
	void (*release) (struct gendisk *, fmode_t);//释放
	int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);//rw_page 函数用于读写指定的页
	int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);//ioctl 函数用于块设备的 I/O 控制
	int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	long (*direct_access)(struct block_device *, sector_t,
	void **, unsigned long *pfn, long size);
	unsigned int (*check_events) (struct gendisk *disk,
	unsigned int clearing);
	/* ->media_changed() is DEPRECATED, use ->check_events() instead */
	int (*media_changed) (struct gendisk *);//介质改变(适用于可移动的设备)
	void (*unlock_native_capacity) (struct gendisk *);
	int (*revalidate_disk) (struct gendisk *);//使介质有效	
	int (*getgeo)(struct block_device *, struct hd_geometry *);//getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息
	/* this callback is with swap_lock and sometimes page table lock held */
	void (*swap_slot_free_notify) (struct block_device *, unsigned long);
	struct module *owner;//模块指针
};

3.4 注册块设备:向内核注册新的块设备、申请设备号

int register_blkdev(unsigned int major, const char *name)
//major:主设备号
//name:块设备名字
//返回值:若major在1~255之间,表示自定义主设备号,则返回0表示注册成功,返回负值表示注册失败
//若major为0,表示由系统自动分配主设备号,成功则返回分配的主设备号,返回负值表示注册失败

3.5 分配并初始化一个 gendisk

struct gendisk *alloc_disk(int minors)
//minors:次设备号数量,即 gendisk 对应的分区数量
//返回值:成功,返回申请到的 gendisk,失败,返回 NULL

3.6 设置gendisk容量

void set_capacity(struct gendisk *disk, sector_t size)
//disk:要设置容量的 gendisk
//size:磁盘容量大小,注意这里是扇区数量
//块设备中最小的可寻址单元是扇区,扇区一般是512字节
//因此该函数设置的大小就是实际容量除以521得到的扇区数量

3.7 将gendisk添加到内核

void add_disk(struct gendisk *disk)
//disk:要添加到内核的 gendisk

四、块驱动源码编写

1、注册块设备驱动;
2、分配并初始化gendisk;
3、分配、初始化请求队列,绑定请求队列和请求函数;
4、给gendisk的major、disk_name、fops、queue等成员赋值;
5、设置设备容量(单位为扇区);
6、将gendisk添加到内核用于系统调用。

#include <linux/module.h> 
#include <linux/blkdev.h> 
#include <linux/hdreg.h> 
#include <linux/version.h>
 

static int Tiny4412_block_major=0;
static struct request_queue *tiny4412_blkdev_queue; 
static struct gendisk *tiny4412_blkdev_disk;
 
static unsigned long long tiny4412_blkdev_bytes=1024*1024*10;//10M--空间容量
#define TINY4412_BLKDEV_BYTES_1        (1024*1024*10)  /*设置块设备的大小*/
static unsigned char tiny4412_blkdev_data_1[TINY4412_BLKDEV_BYTES_1]; /*用于测试块设备的数组大小*/
 
 
/*
* Handle an I/O request.
* 实现扇区的读写
unsigned long sector:  当前扇区位置
unsigned long nsect :  扇区读写数量
char *buffer        :  读写的缓冲区指针
int write           :  是读还是写
*/
static void Tiny4412_block_dev_sector_read_write(unsigned long sector,unsigned long nsect, char *buffer, int write)
{
		/*块设备最小单位是一个扇区,一个扇区的字节数是512字节*/
		unsigned long offset = sector;  /*写入数据的位置*/
		unsigned long nbytes = nsect;   /*写入的长度*/
		if((offset + nbytes)>TINY4412_BLKDEV_BYTES_1)
		{
			printk("写超出范围,强制结束(%ld %ld)\n", offset, nbytes);
			return;
		}
		if(write) /*为真,表示是写*/
		memcpy(tiny4412_blkdev_data_1 + offset, buffer, nbytes);
		else      /*读操作*/
		memcpy(buffer,tiny4412_blkdev_data_1 + offset, nbytes);
}
 
 
/*“制造”请求函数*/
static int tiny4412_blkdev_make_request(struct request_queue *q, struct bio *bio) 
{ 
	int dir; 
	unsigned long long dsk_offset; 
	struct bio_vec *bvec; 
	int i; 
	void *iovec_mem;
	
	/*判断读写方向*/
	if(bio_data_dir(bio) == WRITE) dir = 1;
	else dir = 0;
	dsk_offset = bio->bi_sector << 9;
	bio_for_each_segment(bvec, bio, i) 
	{ 
		iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; 		
		
		Tiny4412_block_dev_sector_read_write(dsk_offset,bvec->bv_len,iovec_mem,dir);//起始位置,长度,源数据,方向
		
		kunmap(bvec->bv_page);
		dsk_offset += bvec->bv_len; 
	}
	bio_endio(bio, 0); 
	return 0;
}
 
static int tiny4412_blockdev_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
	/* 容量=heads*cylinders*sectors*512 
	 * 存储容量 = 磁头数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数
	 */
	geo->heads     = 2;  /*磁头(一般一个盘面有两个磁头,正面一个/反面一个)*/
	geo->cylinders = 32; /*柱面(一般一个盘面上有32个柱面)每个盘片32个磁道)*/
	geo->sectors   = TINY4412_BLKDEV_BYTES_1/2/32/512; /*扇区,一般每个磁道上有12个扇区,这里需要根据前面柱面和磁头进行计算,不能乱填*/
	return 0;
}
 
struct block_device_operations tiny4412_blkdev_fops = 
{ 
    .owner= THIS_MODULE, 
	 /*fdisk命令分区时需要调用该函数,用于读取磁头、柱面、扇区等信息*/
	.getgeo	= tiny4412_blockdev_getgeo,
};
 
static int __init tiny4412_blkdev_init(void) 
{ 
	/*动态分配请求队列*/
	tiny4412_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
	
	/*tiny4412_blkdev_queue:需要绑定的请求队列,tiny4412_blkdev_make_request:需要绑定的“制造”请求函数*/
	blk_queue_make_request(tiny4412_blkdev_queue,tiny4412_blkdev_make_request);
	
	/*动态分配次设备号结构*/
	/*每一个磁盘(分区)都是使用一个gendisk结构保存*/
	tiny4412_blkdev_disk = alloc_disk(64); 
	
	/*磁盘名称赋值*/
	strcpy(tiny4412_blkdev_disk->disk_name, "tiny4412_blkdev"); 
 
	/*注册一个块设备,自动分配主设备号*/
	Tiny4412_block_major = register_blkdev(0,"Tiny4412_block");
	printk("Tiny4412_block_major=%d\n",Tiny4412_block_major);
	
	tiny4412_blkdev_disk->major=Tiny4412_block_major; 	  /*主设备号*/
	tiny4412_blkdev_disk->first_minor = 0; 				  /*次设备号*/
	tiny4412_blkdev_disk->fops = &tiny4412_blkdev_fops;   /*文件操作结合*/
	tiny4412_blkdev_disk->queue = tiny4412_blkdev_queue;  /*处理数据请求的队列*/
	
	/*设置磁盘结构 capacity 的容量*/
	/*注意: 块设备的大小使用扇区作为单位设置,而扇区的大小默认是512字节。
	  cat /sys/block/xxxx/size 可以查看到设置的大小
	  把字节为单位的大小转换为以扇区为单位时,我们需要除以512,或者右移9位
	*/
	set_capacity(tiny4412_blkdev_disk,tiny4412_blkdev_bytes>>9); 	
	
	add_disk(tiny4412_blkdev_disk);//添加磁盘信息到内核
	return 0;
}
 
static void __exit tiny4412_blkdev_exit(void) 
{ 
	del_gendisk(tiny4412_blkdev_disk);//删除磁盘
	
	put_disk(tiny4412_blkdev_disk); //从内核减去gendisk
	
	blk_cleanup_queue(tiny4412_blkdev_queue);//清除队列

	unregister_blkdev(Tiny4412_block_major, "Tiny4412_block");/*注销块设备*/
	
}
 
module_init(tiny4412_blkdev_init); 
module_exit(tiny4412_blkdev_exit);
MODULE_LICENSE("GPL");

其他储存相关链接:

1、Flash闪存储存原理以及NAND flash、NOR flash总结

2、SSD硬盘SATA接口和M.2接口区别总结

Logo

更多推荐