设备管理之块设备

2015-6-22 chenhui 设备驱动

在这里要注意,ARM 上一般用的是 MTD 设备,MTD 设备的块设备和 PC 块设备有点不同,比如分区表在 MTD 设备上是没有记录的,他是通过软件设置的,这一点一定要注意。


所谓的块设备,就是以数据块为单位来进行传输数据的一种设备,比如说硬盘就是一个非常典型的块设备。


内核的块设备子系统是非常复杂的,他涉及到了映射层、通用块层、IO调度层、设备驱动层。我不可能在一篇文章中把他们全部讲完,把他们讲完是一件非常恐怖的事情(没那个精力),所以说本文用了一种比较取巧的方法来讲解块设备:先给出一份最为简单的块设备驱动程序,然后我们再来分析这个驱动程序背后都做了些什么。

 

 

 

这是一个最为简单的使用内存实现的块设备驱动,我们下面来讲解他背后的机理。


在阅读源代码前,有必要先大概说下他的步骤。

  1. 创建一个磁盘描述符 gendisk,分配分区数组。
  2. 为 gendisk 创建请求队列。
  3. 申请设备号
  4. 扫描保存在磁盘上的分区表,得到所有分区信息
  5. 为所有的分区创建 hd_struct 对象,插入设备模型,存入分区数组
  6. 为磁盘和分区产生热拔插事件,使 /dev/ 目录下创建设备文件

再次提醒:上面这个步骤,其中扫描分区表,由于 ARM 使用 MTD 设备,所以不适用。关于 ARM 设备的分区,可阅读另一篇专门写 MTD 设备的文。


在该驱动被加载时,ramblock_init() 就会被执行,而他的第一句代码就是调用 alloc_disk() 分配了一个 struct gendisk 结构。那么这个 gendisk 又是干嘛的?实际上他代表一个磁盘。

 


 

major :磁盘的驱动的主设备号。

first_minor :第一个次设备号。

minors :磁盘的设备号范围大小(可以看做是分区数)。

disk_name :磁盘名。

part struct hd_struct 就是分区描述符。这里的 part 是个数组,这个数组指向磁盘下的分区。比如说 part[0] 得到的就是第二个分区的指针(为什么是第二个分区而不是第一个?因为第一个分区是磁盘本身)。


我们知道,Linux 把磁盘分成很多个分区来进行操作,有引导区、参数区、内核区、根文件系统区等。所以 gendisk 中也有一个分区数组。这个分区数组由 hd_struct 组成,hd_struct 即代表一个分区。


我们可以调用 alloc_disk() 来申请一个磁盘描述符。

 


minors :磁盘的设备号范围大小(可以看成是磁盘的分区数)。


所谓的设备号范围大小,我们知道,其实就是磁盘的分区数。磁盘下所有的分区都使用同一个主设备号,然后次设备号不同。

 

 

733~735 行,申请一个磁盘描述符

740 行,创建分区描述符数组。minors 必须大于 1,否则表示除了磁盘本身外没有额外分区。

749~754 行,初始化磁盘描述符。


至此,磁盘描述符 gendisk 以及分区数组都已经创建完毕(只是创建了分区数组,真正的分区对象还没创建,分区对象要等到扫描分区表时创建)。


在这里,分区也是一个单独的块设备,比如 /dev/mtdblock0 和 /dev/mtdblock1 这两个设备文件就是同一个磁盘内的两个不同分区。这点一定要了解


文首那份驱动调用 ramblock_init() 申请磁盘描述符后,他又调用 blk_init_queue() 分配了一个request_queue_t 结构


这个request_queue_t 我们称它为请求队列描述符。那么这个请求队列又是干嘛的?对于块设备而言,我们对他的每次读写访问都可以看成一个请求,有时候我们并不能瞬间完成这些请求,所以内核会把这些请求暂时保存在一个队列里,这个队列就是用request_queue_t 结构描述的请求队列。

 

 

queue :队列中待处理的请求所组成的链表。里面每个元素都是 struct request 结构。

request_fn :这个函数是程序员定义的,我们请求队列调用这个函数来处理请求。

queuedata :块设备驱动程序的私有数据。

kobj :请求队列内嵌kobject。可见,每一个请求队列也会在 sysfs 中生成对应目录。


我们可以使用 struct request 结构来描述请求队列中的请求。

 

 

queuelist :用于把请求链接到请求队列中。

q :指向包含请求的请求队列描述符指针。


然后我们再来看看blk_init_queue() 的实现。

 

 

rfn :请求队列的request_fn 字段,请求队列调用这个函数来处理请求。对于我们的驱动程序而言,他就是do_ramblock_request() 这个函数,而这个函数每执行一次就递增并打印一次数字(每执行一次表示处理一次请求)。

lock :请求队列的锁。

 

 

908~943 行,申请一个请求队列描述符并初始化他。


此时我们的磁盘、分区数组和请求队列都已经有了,所以驱动程序在 26 行把他们关联起来,并在 27 行为驱动程序分配一个主设备号。实际上块设备的主设备号分配和字符设备是差不多的,所以这里我们就不看了。然后我们再往下看,他最后调用add_disk() 把磁盘注册到了内核。实际上add_disk() 这个函数才是真正的重头函数,我们得好好分析分析他。

 

 

187 行,设置 GENHD_FL_UP 标志表示磁盘已被激活。

188 行,建立设备驱动程序和设备的主设备号之间的连接。实际上这个函数就是调用块设备(bdev_map) 版本的kobj_map(),这个函数在字符设备中已经讲过了,就不讲了。

190 行,注册设备驱动程序模型的gendiskkobject结构,并扫描磁盘中的分区表,对每个分区,初始化其hd_struct描述符。同时注册设备驱动程序模型中的分区。这个函数执行完之后,块设备就算是安装成功了。

191 行,注册请求队列描述符中内嵌的kobject结构。


这里最重要的是 register_disk 的调用。因为现在磁盘、分区数组、请求队列、设备号都已经有了,接下来要做的就是把他注册进去。

 

 

479 行,我们使用 struct block_device 结构来描述一个块设备。我们先来看看块设备描述符的定义。他比较重要。

 

 

Bd_dev :块设备的设备号。

bd_inode :指向bdev文件系统中块设备对应的文件索引结点的指针。

bd_contains :如果本对象是一个磁盘的块描述符,那么指向自己;如果本对象是一个分区的块描述符,那么他指向该分区所属的磁盘的块描述符。总之就是,无论如何都指向磁盘的块描述符。

bd_part :如果本块设备是分区,则指向分区描述符 hd_struct 。不是分区则为空。

bd_disk :如果是分区,指向所属磁盘的 gendisk;如果是磁盘,则指向自己。

bd_list :用于块设备描述符链表的指针。

bd_private :块设备持有者的私有数据指针。


472 行,我们使用 struct hd_struct 结构来描述一个分区

 


kobj :内嵌的kobject


下面开始正式讲解 register_disk。


485~490 行,设置磁盘的 kobj 的名,并把他加入设备模型

498~499 行,磁盘没有分区就直接返回,结束了。

501~502 行,如果磁盘的扇区数为 0,就表示设备已经被移除,退出。


503 行,到了这里就说明磁盘有分区在。


504~506 行,得到磁盘对应的块设备描述符,并为磁盘在 bdev 文件系统里创建其对应文件。

508 行,把他设为 1,说明块设备对应磁盘有分区,我们要扫描他。

510 行,扫描磁盘分区,建立磁盘与分区的关系,并将分区添加到系统中。

517 行,因为磁盘插入设备模型,我们要触发一次热拔插事件。

519~524 行,为所有分区都触发一次热拔插事件。

 

怎么注册磁盘呢?其实很简单,就是下面三件事:


  1. 加入设备模型
  2. 扫描分区
  3. 为磁盘和分区触发热拔插事件,由于在第二步已经得到所有分区并插入了设备模型,这会导致他们在 /dev/ 中产生一个设备文件,比如 /dev/mtdblock0...

下面开始扫描分区



 

bdev :块描述符。

mode :访问模式。

flags 0


如果 bdev 是一个磁盘,那么这个函数主要扫描磁盘下所有的分区并把他们加入设备模型

如果 bdev 是一个分区,他会为分区所属的磁盘调用一次 __blkdev_get(),然后再初始化分区

 

 

216 行,初始化文件对应的 inode 为分区的 inode

218 行,打开块设备。这个函数至关重要,内核会在这个函数里根据情况建立各个分区。比如说 ARM linux 常常会见到的 mtdblock0mtdblock 1mtdblock 2…就是在这里建立的。等分区建立完后,并为他们产生热拔插事件后,这个块设备也就正式可用了。


static int do_open(struct block_device *bdev, struct file *file)
{
	struct module *owner = NULL;
	struct gendisk *disk;
	int ret = -ENXIO;
	int part; 
	 
	
	//根据设备号找到对应的磁盘或分区, 如果是一个分区, 则索引存放在part, 否则part为0
	disk = get_gendisk(bdev->bd_dev, &part);
	if (!disk) {
		unlock_kernel();
		bdput(bdev);
		return ret;
	}
	
	owner = disk->fops->owner;
	down(&bdev->bd_sem);
 	
	// 设备未打开
	if (!bdev->bd_openers) { 
		
		bdev->bd_disk = disk;
		bdev->bd_contains = bdev;
		
		// 他是磁盘
		if (!part) { 
		
			struct backing_dev_info *bdi;
			
			// 若驱动定义了磁盘的打开方法,则调用他
			if (disk->fops->open) {
				ret = disk->fops->open(bdev->bd_inode, file);
				if (ret)
					goto out_first;
			}
			
			...
			
			// 读取分区表, 为每一个分区创建 hd_struct 并插入设备模型
			if (bdev->bd_invalidated)
				rescan_partitions(disk, bdev);
				
		} else { 
		 // 他是分区
			
			struct hd_struct *p;
			struct block_device *whole;
			
			// 得到磁盘的块设备
			whole = bdget_disk(disk, 0);
			
			...
			
			// 防止磁盘未扫描就直接为分区调用该函数?
			ret = blkdev_get(whole, file->f_mode, file->f_flags);
			if (ret)
				goto out_first;
			
			...
			
			// 关联分区和块设备描述符
			p = disk->part[part - 1];
			bdev->bd_part = p; 
			
			...
			
		}
	} else {
		//设备已经打开了,暂时无视

		...
	}

	bdev->bd_openers++;
	up(&bdev->bd_sem);
	unlock_kernel();
	return 0;

out_first:
	...
out:
	...
	return ret;
}



最后总结一下 do_open()。这个函数逻辑并不复杂。他其实就是打开一个块设备,如果打开的块设备之前已经被打开过了,那么就要看这个块设备是一个分区还是磁盘,如果是分区那么只要递增打开次数即可,如果是磁盘那么实际上就是重新扫描分区表并重新建立分区。如果块设备之前没有被打开过,那么无论块设备是分区还是磁盘,必然会扫描一次分区表并建立分区,然后再进行一些比较冗杂的初始化就结束了。


接下来我们再来看看,分区表的扫描和分区的建立。


int rescan_partitions(struct gendisk *disk, struct block_device *bdev)
{
	struct parsed_partitions *state;
	int p, res;

	...
	
	// 为防止分区重复挂载, 所以先卸载掉所有分区
	for (p = 1; p < disk->minors; p++)
		delete_partition(disk, p); 
	
	...

	// 检查分区表, 得到分区信息
	if (!get_capacity(disk) || !(state = check_partition(disk, bdev)))
		return 0;
	
	//遍历所有分区
	for (p = 1; p < state->limit; p++) {
		
		// 分区的开始扇区和扇区数, 如果扇区数为0, 则忽略当前分区
		sector_t size = state->parts[p].size;
		sector_t from = state->parts[p].from;  
		if (!size)
			continue;
		
		//将分区添加到系统中
		add_partition(disk, p, from, size);
		
	}

	kfree(state);
	return 0;
}


分区表的读取是通过 check_partition() 来实现的,不过这个分区表..在 ARM 上一般是没有的,ARM 上的分区,是写在 MTD 设备的驱动程序里的。


最后调用 add_parition() 创建一个分区并添加到系统。

 


void add_partition(struct gendisk *disk, int part, sector_t start, sector_t len)
{
	struct hd_struct *p;
	
	// 申请一个分区描述符
	p = kmalloc(sizeof(*p), GFP_KERNEL);
	if (!p)
		return;
	memset(p, 0, sizeof(*p)); 
	
	// 起始扇区、扇区数、分区号
	p->start_sect = start; 
	p->nr_sects = len;
	p->partno = part;
	
	devfs_mk_bdev(MKDEV(disk->major, disk->first_minor + part),
			S_IFBLK|S_IRUSR|S_IWUSR,
			"%s/part%d", disk->devfs_name, part); 
	
	// 为分区决定一个分区名,他一般是 磁盘名+分区号
	// 比如 mtdblock0...
	if (isdigit(disk->kobj.name[strlen(disk->kobj.name)-1]))
		snprintf(p->kobj.name,KOBJ_NAME_LEN,"%sp%d",disk->kobj.name,part);
	else
		snprintf(p->kobj.name,KOBJ_NAME_LEN,"%s%d",disk->kobj.name,part);
	
	// 把分区注册到设备模型内
	p->kobj.parent = &disk->kobj;
	p->kobj.ktype = &ktype_part;
	kobject_register(&p->kobj); 
	
	// 把分区放入分区数组
	disk->part[part-1] = p;
}



代码是比较简单的,其实就是为分区创建 hd_struct 结构体,用分区表内的信息初始化他后,把分区注册到设备模型,插入磁盘的分区数组内就完成了。


至此,我们已经把块设备的初始化全部讲解完了,实际上我们还有很多东西没有讲。比如说通用块层、IO调度层。实际上通用块层和 IO 调度层都是属于内容非常复杂的部分,完全可以用单独的一篇长文来讲解了








发表评论:

Copyright ©2015-2016 freehui All rights reserved