块设备
1 块设备驱动程序框架
1.1 块设备加载过程
在块设备的模块加载函数中,需要完成一些重要的工作,这些工作涉及将在后面的内容中进行详解,本节的目的是为了给出一个整体的概念。块设备驱动加载模块中需要完成的工作如下图所示。
(1) 使用alloc_disk()函数分配通用磁盘gendisk结构。
(2) 通过register_blkdev()函数注册设备,该过程是一个可选的过程,也可以不用注册设备,驱动程序一样能够工作。
(3) 根据是否需要I/O调度,将分为两种情况,一种是使用请求队列进行数据传输,一种是不适用请求队列进行数据传输。
(4) 设置通用磁盘gendisk结构的成员变量。如给gendisk的major、fops、queue等赋初值。
(5) 使用add_disk()函数激活磁盘设备。当调用该函数后,就可以立即对磁盘设备进行操作,所有该函数的调用必须在所有准备工作就绪后。
下面的函数是一个不适用I/O队列的块设备加载模块,代码中涉及的重要概念将在后面讲解。这里,只需要对加载函数有个整体的了解即可。
static int __init xxx_blkdev_init(void)
{
int ret;
static struct gendisk *xxx_disk; // 通用磁盘结构
static struct request_queue *xxx_queue; // 请求队列
xxx_disk = alloc_disk(1); // 分配通用磁盘结构
if(!xxx_disk){
ret = -ENOMEM;
goto err_alloc_disk;
}
if(register_blkdev(xxx_MAJOR,"xxx")<0){ // 请求队列初始化
ret = -ENOMEM; // 请求队列初始化失败
goto err_init_queue;
}
strcpy(xxx_disk->disk_name, XXX_DISKNAME); // 设定设备名
xxx_disk->major = xxx_MAJOR; // 设备主设备号
xxx_disk->first_minor = 0; // 设置次设备号
xxx_disk->fops = &xxx_fops;
xxx_disk->queue = xxx_queue;
set_capacity(xxx_disk, xxx_BYTES>>9); // 设置设备容量
add_disk(xxx_disk);
return 0;
err_init_queue: // 队列初始化失败
unregister_blkdev(xxx_MJOR, "xxx");
err_alloc_disk: // 分配磁盘失败
put_disk(xxx_disk);
return ret;
}
1.2 块设备卸载过程
在块设备驱动的卸载模块中完成与模块加载函数相反的工作,如下图所示。
(1) 使用de;_gendisk()函数删除gendisk设备,并使用put_disk()函数删除对gendisk设备的引用。
(2) 使用blk_cleanup_queue()函数请求请求队列,并释放请求队列所占的资源。
(3) 如果在模块加载函数中使用resigter_blkdev()注册设备,那么需要在模块卸载函数中使用unregisgter_bkjdev()函数块设备,并释放对块设备的引用。
块设备驱动程序卸载函数的模板如下。
static void __exit xxx_blkdev_exit(void)
{
del_gendisk(xxx_disk); // 删除gendisk磁盘
put_disk(xxx_disk); // 删除gendisk磁盘引用
blk_cleanup_queue(xxx_queue); // 删除请求队列
unregister_blkdev(xxx_major, "xxx"); // 注销块设备
}
2 通用块
2.1 通用块层 – **通用块层是一个内核组件,它处理来自系统其他组件发出的块设备请求。换句话说,通用块层包含了块设备操作的一些通用函数和数据结构。下图是块设备加载函数中用到的一些重要数据结构,如通用磁盘结构gendisk、请求队列结构request_queue、请求结构request、块设备I/O操作结构bio和块设备操作结构blokc_device_operation等。这些结构将在下面详细介绍。**2.1 alloc_disk()函数对应的gendisk结构体
现实生活中有许多具体的物理块设备,例如磁盘、光盘等。不同的物理块设备其结构是不一样的,为了将这些块设备公用属性在内核中统一,一般称为通用磁盘。
(1) gendisk结构体
在Linux内核中,gendisk结构体可以表示一个磁盘,也可以表示一个分区。这个结构体的定义代码如下:
struct gendisk{
int major; // 设备主设备号
int first_minor; // 第一次设备号
int minors; // 磁盘可以进行分区的最大数目,如为1,则磁盘不能分区
char disk_name[DISK_NAME_LEN]; // 设备名称
struct disk_part_tbl *part_tbl;
struct hd_struct_part0;
struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
int flags;
struct devive *driverfs_dev;
struct kobject *slave_dir;
...
};
gendisk结构体的主要参数说明如下表所示。
Linux内核中提供了一组函数来操作gendisk结构体,这些函数如下:
- 分配gendisk
gendisk结构体是一个动态的结构,其成员是随系统状态不断变化的,所以不能静态地分配该结构,并对其成员赋值。对该结构的分配,应该使用内核提供的专用函数alloc_disk(),其原型如下:
struct gendisk *alloc_disk(int minors);
minors参数是这个磁盘使用的次设备数量,起始就是磁盘的分区数量,磁盘的分区一旦由alloc_disk()函数设定,就不能修改。alloc_disk()的例子如下:
struct gendisk *xxx_disk == alloc_disk(16); // 分配一个gendisk设备
if(xxx_disk==NULL)
goto error_alloc_disk;
该代码分配了一个通用磁盘xxx_disk,用参数16表示,这个磁盘可以有15个分区,其中0用来表示整块设备。
2. 设置gendisk的属性
使用alloc_disk()函数分配一个disk后,需要对该结构的一些成员进行设置,代码如下:
strcpy(xxx_disk->disk_name, XXX_DISKNAME); //设置设备名
xxx_disk->major = xxx_MAJOR;
xxx_disk->first_minot = 0;
xxx_disk->fops = &xxx_fops;
set_capacity(xxx_disk, xxx_BYTES>>9);// 设置设备容量,为了加快速度使用了位移9位的方法
需要注意的是set_capacity()函数,该函数用来设置磁盘的容量,但不是以字节为单位,而是以扇区为单位。为了将set_capacity()函数解释清除,这里以扇区分为两种,一种是为例设备的真实扇区,二是内核中的扇区。物理设备的真是扇区大小有512、1024、2048字节等,但不管真是扇区的大小是多少,内核中的扇区大小都被定义为512字节。set_capacity()函数是以512字节为单位的,所以set_capacity()函数的第2个参数是xxx_BYTES>>9,表示设备的字节容量除以512后得到的内核扇区数。
3. 激活gendisk
当使用alloc_disk()函数分配了gendisk通用磁盘,并设置了相关属性后,就可以调用add_disk()函数向系统激活这个磁盘设备了。add_disk()函数的原型如下:
void add_disk(struct gendisk *disk);
需要特别注意的是,一旦调用add_disk()函数,那么磁盘设备就开始工作了,所有关于gendisk的初始化必须在add_disk()函数之前。
4. 删除gendisk
当不再需要磁盘时,应该删除gendisk结构,可使用del_gendisk()函数完成这个功能,它和alloc_disk()函数是对应的。del_gendisk()函数的原型如下:
void del_gendisk(struct gendisk *disk);
5. 删除gendisk的引用计数
在调用del_gendisk()函数后,需要使用put_disk()函数减少gendisk的阴影计数,因为在add_disk()函数中增加了gendisk的引用计数。put_disk()函数的原型如下:
void put_disk(struct gendisk *disk);
2.2 块设备的注册和注销
为了使内核知道块设备的存在,需要使用块设备注册函数。在不适用块设备时,也㤇注销块设备。块设备的注册和注销如下所述。
1. 注销块设备函数register_blkdev()
与字符设备的register_chrdev()函数对应的是register_blkdev()函数。对于大多数块设备驱动来说,第一个工作就是向内核注册自己。但值得注意的是,在Linux2.6内核中,对register_blkdev()函数的调用完全是可选的,内核中的register_blkdev()函数的功能已经逐渐减少。在新内核中,一般只完成两件事情。
- 根据参数分配一个块设备好。
- 在/proc.devices中新增加一行数据,表示块设备的设备信息。
块设备的注册函数register_blkdev()的原型如下:
int register_blkdev(unsigned int major, const char *name);
register_blkdev()函数的第一个参数是设备需要申请的主设备号,如果传入的主设备号是0,那么内核将动态的分配一个主设备号给设备。第2个参数是块设备的名字,该名字将在/proc/devices文件中显示。register_blkdev()函数成功时,返回申请的设备号;函数失败时,将返回一个负的错误码。在未来的内核中,register_blkdev()函数可能会被去掉,但是目前大多数驱动程序仍然使用它,使用register_blkdev()函数的一个例子如下所示:
if(xxx_major = register_blkdev(xxx_MAJOR, "xxx") < 0) // 注册设备
{
ret = -EBUSY;
goto err_alloc_disk;
}
2. 注销块设备函数unregister_blkdev()
与register_blkdev()函数对应的是注销函数unregister_blkdev(),其函数原型如下:
int unregister_blkdev(unsigned int major, const char *name);
unregister_blkdev()函数的第一个参数是设备需要释放的主设备号,这个主设备是由register_blkdev()函数申请的。第二个参数是设备的设备名。当函数成功时返回0,失败时返回-EINVAL。
使用unregiser_blkdev()函数的一个例子如下:
unregister_blkdev(xxx_major, "VirtualDisk");
2.3 请求队列
简单地讲,一个块设备的请求队列就是包含块设备I/O请求的一个队列。这个队列使用链表线性的排列。请求队列中存储未完成的块设备I/O请求,并不是所有的I/O块请求都可以顺利地加入请求队列中。请求队列中定义了自己能处理的块设备请求限制。这些限制包括:请求的最大尺寸、一个请求能够包含的队理段数、硬盘扇区大小等。
请求队列提供了一些处理函数,使不同块设备可以使用不同的I/O调度器,甚至不是用I/O调度器。一个I/O调度器的作用,是以最大的性能来优化请求的顺序。大多数I/O调度器控制着所有的请求,根据请求执行的顺序和位置对其进行排序,使块设备能够以最快的数据将数据写入和读出。
请求队列使用request_queue结构体来描述,在include/linux/blkdev.h中定义了该结构体和其对应的操作函数。对于请求队列的这些了解是远远不够的,在后面用到请求队列时,将其进行详细解释。
2.4 设置gendisk属性中规定block_device_operations结构体
在块设备中有一个和字符设备中file_operations对应的结构体block_device_operationss。其也是一个对块设备操作的函数集合,定义代码如下:
struct block_device_operations{
int (*open)(struct block_device *, fmode_t);
int (*release)(struct gendisk *, fmode_t);
int (*locked_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl)(struct block_device*,fmode_t, unsigned, unsigned long);
int (*direct_access)(struct block_device *, sector_t, void **, unsigned long *);
int (*media_changed)(struct gendisk*);
int (*revalidate_disk)(struct gendisk *);
int (*getgeo)(struct block_device *, strcut hd_geometry *);
struct module *owner;
}
下面对这个结构体的主要成员进行分析。
1. 打开和释放函数
int (*open)(struct block_device *, fmode_t);
int (*release)(struct gendisk *, fmode_t);
  open()函数在设备打开时被调用,release()函数在设备关闭时被调用。这两个函数完成的功能与字符设备的打开和关闭函数相似。
2. I/O控制函数
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
  ioctl()函数实现了Linux的ioctl()系统调用,块设备的大多数标准请求已经有内核开发者实现了,驱动开发者需要实现的请求非常少,所以一般ioctl()函数也比较简单。
3. 介质改变函数
int (*media_changed)(struct gendisk *);
该函数会被内核调用来检查块设备是否改变,如果改变,则返回一个非0值,否则返回0值。这个函数仅对能够移动的块设备有效,不可以移动的块设备不需要实现这个函数。假设设备A用一个端口port1的第0位表示设备是否可用,该位为1时,表示设备被移除;该位为0时,表示设备仍然连接在主机上。那么介质改变函数可以写成如下形式:
int A_mdeia_changed(struct gendisk *gd)
{
struct A_dev *dev = gd->private_data;// 从gendisk的私有数据中得到A设备结构体
if(dev->port1&0x01 == 1)// A设备的端口1的第0位等于1,表示设备已经移除
{
return 1;// 1表示设备已经移除
}
else
{
return 0;// 0表示设备可用
}
}
4. 使介质有效函数
当介质改变时,系统会调用revalidate()函数。该函数对设备进行重新的设置,以使设备准备好。介质有效函数可以写成如下形式:
int xxx_revalidate(struct gendisk *gd)
{
struct xxx_dev *dev = gd->private_data;
... // 设备重新设置
return 0;
}
5. 获取驱动器信息的函数
int (*getgeo)(struct block_device *, struct hd_geometry *);
该函数根据驱动器的硬件信息填充一个hd_geometry结构体,hd_geometry结构包含了磁盘的磁头、扇区、柱面等信息。
6. 模块指针
struct moudle *owner;
几乎在所有的驱动程序中,该成员被初始化为THIS_MODULE,表示这个结构体属于目前运行的模块。