Linux块设备驱动——sbull

块设备驱动程序,linux内核中的一类驱动程序。通过传输固定大小的随机数据来访问设备。

初始化

注册块设备驱动

向内核注册设备驱动程序使用函数int register_blkdev(unsigned int major, const char *name);(linux/fs.h),参数是该设备使用的主设备号和名称(名字在/proc/devices中显示)。若传入的主设备号为0,内核将分配一个新的主设备号,并将该设备号返回给调用者;失败则返回负值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int __init sbull_init(void)
{
int i;
sbull_major = register_blkdev(sbull_major,"sbull"); // 注册块设备,第一个是设备号,0为动态
if(sbull_major <= 0){ //分配,第二个是设备名
printk(KERN_WARNING "sbull:unable to get major number\n");
return -EBUSY;
}
/* 为块核心数据结构 sbull_dev 分配空间*/
Devices = kmalloc(ndevices *sizeof(struct sbull_dev),GFP_KERNEL);
if(Devices == NULL)
goto out_unregister;
for(i = 0;i < ndevices;i++) /* 初始化 sbull_dev 核心数据结构 */
setup_device(Devices + i,i);
return 0;
out_unregister:
unregister_blkdev(sbull_major,"sbd");
return -ENOMEM;
}

对于register_blkdev的调用是可选的,该接口所做的事情是:如果需要的话分配一个动态的主设备号;在/proc/devices中创建一个入口项。

注册磁盘

内核使用gendisk结构(linux/genhd.h)来表示一个独立的磁盘设备。内核还使用该结构体表示分区。该结构中的许多成员必须由驱动程序进行初始化。

  • major
    int major;主设备号
  • first_minor
    int first_minor;一个驱动器至少使用一个次设备号,如果驱动器是可被分区的用户将要为每个可能的分区都分配一个次设备号。
  • minors
    int minors;常取16,一个完整的磁盘可以包含15个分区。
  • disk_name
    char disk_name[32];磁盘设备名字,该名字显示在/proc/partitionssysfs中。
  • fops
    struct block_device_operations *fops;块设备操作
  • queue
    struct request_queue *queue;设备I/O请求队列
  • flags
    int flags;驱动器状态标志。可移动介质将被设置为GENHD_FL_REMOVABLE;CD-ROM设备被设置为GENHD_FL_CD;若不希望在/proc/partitions中显示分区信息可设置为GENHD_FL_SUPPRESS_PARTITION_INFO
  • capacity
    sector_t capacity;以512字节为一个扇区,该驱动器可以包含的扇区数。驱动程序不能直接设置该成员,而要将扇区数传给set_capacity
  • private_data
    void *private_data;块设备驱动可以使用该成员保存指向其内部数据的指针

gendisk是一个动态分配的结构,驱动程序不能自己动态分配该结构,必须使用alloc_disk分配,使用del_gendisk回收。

  • alloc_disk
    struct gendisk *alloc_disk(int minors);参数minors是该磁盘使用的次设备号的数目。为了能正常工作,minors传入后就不能更改了。
  • add_disk
    void add_disk(struct gendisk *gd);使用alloc_disk分配的gendisk不能使磁盘对系统可用,还需要add_disk将磁盘设备激活,并随时准备调用它提供的方法。在驱动程序完全被初始化并且能够响应对磁盘的请求前,请不要调用add_disk
  • get_disk and put_disk
    gendisk是一个引用技术结构,get_diskput_disk函数负责处理引用计数。
  • del_gendisk
    void del_gendisk(struct gendisk *gd);调用del_gendisk会删除gendisk中的最终计数。当没有用户继续使用时,将真正删除该结构。此后,在系统中不会找到该设备信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
static void setup_device(struct sbull_dev *dev,int which)
{
memset(dev,0,sizeof(struct sbull_dev)); /* 初始化 dev 所指内容为0*/
dev->size = nsectors * hardsect_size;
dev->data = vmalloc(dev->size);
if(dev->data == NULL)
{
printk(KERN_NOTICE "vmalloc failure.\n");
return ;
}
spin_lock_init(&dev->lock); /* 初始化自旋锁*/
/* 在分配请求队列前要先初始化自旋锁*/
/* The timer which "invalidates the device给内核定时器初始化 "*/
init_timer(&dev->timer); /*初始化定时器,实际将结构中的list成员初始化为空*/
dev->timer.data = (unsigned long)dev; /*被用作function函数的调用参数*/
dev->timer.function = sbull_invalidate; /* 当定时器到期时,就执行function指定的函数*/

/*
* The I/O queue, depending on whether we are using our own
* make_request function or not.
*/
switch(request_mode)
{
case RM_NOQUEUE:
dev->queue = blk_alloc_queue(GFP_KERNEL); /* 分配“请求队列” */
if(dev->queue == NULL)
goto out_vfree;
blk_queue_make_request(dev->queue,sbull_make_request); /*绑定"制造请求"函数 */
break;
case RM_FULL:
dev->queue = blk_init_queue(sbull_full_request,&dev->lock); /*请求队列初始化*/
if(dev->queue == NULL)
goto out_vfree;
break;
case RM_SIMPLE:
dev->queue = blk_init_queue(sbull_request,&dev->lock); /*请求队列初始化*/
if(dev->queue == NULL)
goto out_vfree;
break;
default:
printk(KERN_NOTICE "Bad request mode %d,using simple\n",request_mode);
}
blk_queue_hardsect_size(dev->queue,hardsect_size); /* 硬件扇区尺寸设置 */
dev->queue->queuedata = dev;
dev->gd = alloc_disk(SBULL_MINORS); /* 动态分配 gendisk 结构体*/
if(!dev->gd)
{
printk(KERN_NOTICE "alloc_disk failure\n");
goto out_vfree;
}
/* 初始化 gendisk */
dev->gd->major = sbull_major; /* 主设备号 */
dev->gd->first_minor = which * SBULL_MINORS; /* 第一个次设备号 */
dev->gd->fops = &sbull_ops; /* 块设备操作结构体 */
dev->gd->queue = dev->queue; /* 请求队列 */
dev->gd->private_data = dev; /* 私有数据 */
snprintf(dev->gd->disk_name,32,"sbull%c",which + 'a');
/* 每个请求的大小都是扇区大小的整数倍,内核总是认为扇区大小是512字节,因此必须进行转换*/
set_capacity(dev->gd,nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));
add_disk(dev->gd); /* 完成以上初始化后,调用 add_disk 函数来注册这个磁盘设备 */
return ;

out_vfree:
if(dev->data)
vfree(dev->data); /* 释放用 vmalloc 申请的不连续空间*/
}

逆初始化

注销块设备驱动

register_blkdev对应的注销函数为int unregister_blkdev(unsigned int major, const char *name);,传入的参数与传递给register_blkdev的参数必须匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void sbull_exit(void)
{
int i;
for(i = 0; i < ndevices;i++)
{
struct sbull_dev *dev = Devices + i;
del_timer_sync(&dev->timer); /* 去掉 "介质移除" 定时器*/
if(dev->gd)
{
del_gendisk(dev->gd); /* 释放 gendisk 结构体*/
put_disk(dev->gd); /* 释放对 gendisk 的引用 */
}
if(dev->queue)
{
if(request_mode == RM_NOQUEUE)
blk_put_queue(dev->queue);
else
blk_cleanup_queue(dev->queue); // 清除请求队列
}
if(dev->data)
vfree(dev->data);
}
unregister_blkdev(sbull_major,"sbull");
kfree(Devices);
}

块设备操作

字符设备使用file_operations结构,块设备使用类似的结构block_device_operations(linux2.6在linux/fs.h中,linux4.4在linux/blkdev.h中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
long (*direct_access)(struct block_device *, sector_t, void __pmem **, unsigned long *pfn);
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 *);
/* 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;
const struct pr_ops *pr_ops;
};
  • open
    int (*open) (struct inode *inode, struct file *filp);当设备被打开时调用它。
  • release
    int (*release) (struct inode *inode, struct file *filp);当设备被关闭时调用它。
  • ioctl
    int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);实现ioctl系统调用的函数,块设备层会先截取大量的标准请求,因此大多数块设备的ioctl函数都十分短小。
  • media_changed
    int (*media_changed) (struct gendisk *gd);内核调用该函数检查用户是否更换了驱动器内的介质,如果更换返回一个非零值。该函数只适用于那些支持可移动介质。
  • revalidate_disk
    int (*revalidate_disk) (struct gendisk *gd);当介质被更换时被调用。
  • owner
    struct module *owner;指向拥有该结构的模块指针,通常被初始化为THIS_MODULE

open

1
2
3
4
5
6
7
8
9
10
11
12
static int sbull_open(struct inode *inode,struct file *filp)
{
struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;
del_timer_sync(&dev->timer); //去掉"介质移除"定时器
filp->private_data = dev;
spin_lock(&dev->lock);
if(!dev->users)
check_disk_change(inode->i_bdev);
dev->users++; // 使用计数加 1
spin_unlock(&dev->lock);
return 0;
}

release

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int sbull_release(struct inode *inode,struct file *filp)
{
struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;
spin_lock(&dev->lock);
dev->users--; // 使用计数减 1
if(!dev->users)
{
//30秒的定时器,如果这个时段内设备没有被打开则移除设备
dev->timer.expires = jiffies + INVALIDATE_DELAY;
add_timer(&dev->timer); //将定时器添加到定时器队列中
}
spin_unlock(&dev->lock);
return 0;
}

media_changed

open中调用check_disk_change函数触发media_changed检查介质是否被改变。如果介质改变则返回非零值。

1
2
3
4
5
int sbull_media_changed(struct gendisk *gd)
{
struct sbull_dev *dev = gd->private_data;
return dev->media_change;
}

revalidate_disk

介质改变后,内核会调用revalidate_disk,调用完成后,内核将重新读取设备的分区表。

1
2
3
4
5
6
7
8
9
10
int sbull_revalidate(struct gendisk *gd)
{
struct sbull_dev *dev = gd->private_data;
if(dev->media_change)
{
dev->media_change = 0;
memset(dev->data,0,dev->size);
}
return 0;
}

ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int sbull_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
{
long size;
struct hd_geometry geo;
struct sbull_dev *dev = filp->private_data; // 通过 file->private 获得设备结构体
switch(cmd)
{
case HDIO_GETGEO:
/*
* Get geometry: since we are a virtual device, we have to make
* up something plausible. So we claim 16 sectors, four heads,
* and calculate the corresponding number of cylinders. We set the
* start of data at sector four.
*/
size = dev->size *(hardsect_size/KERNEL_SECTOR_SIZE);
/* 获得几何信息 */
geo.cylinders = (size & ~0x3f) >> 6;
geo.heads = 4;
geo.sectors = 16;
geo.start = 4;
if(copy_to_user((void __user *)arg,&geo,sizeof(geo)))
return -EFAULT;
return 0;
}
return -ENOTTY; // 不知道的命令
}

内核对块设备的物理信息并不感兴趣,它只把设备看成是线性的扇区数组。但一些用户空间的应用程序依然需要查询磁盘的物理信息。特别是fdisk工具。

请求处理

块设备中没有字符设备中的read、write函数,那么块设备是如何处理I/O请求的呢?早在sbull_init中初始化queue时需要传入一个queue处理函数,待内核接收到磁盘I/O时,会调用queue处理函数来处理。

request queue

一个请求队列就是一个动态的数据结构,该结构必须由块设备的I/O子系统创建。

创建删除

  • 创建初始化
    request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);参数request是处理这个队列的函数指针,lock是控制访问队列权限的自旋锁。由于创建过程会分配内存,因此会有失败的可能,所以在使用队列前一定要检查返回值。
  • 删除
    void blk_cleanup_queue(request_queue_t *);调用该函数后,驱动程序将不会再得到这个队列中的请求,也不能再引用这个队列了。

队列元素

  • 从队列中获取请求
    struct request *elv_next_request(request_queue_t *queue);返回一个需要处理的请求指针,该指针由I/O调度器决定,如果没有请求需要处理返回NULL。该函数被调用后,请求依然保存在队列中,但是为其做了活动标记,该标记保证了当开始执行该请求时I/O调度器不再将该请求与其他请求合并。
  • 从队列中删除请求
    void blkdev_dequeue_request(struct request *req);将请求从队列中删除。
  • 请求返回队列
    void elv_requeue_request(request_queue_t *queue, struct request *req);将拿出队列的请求再返回给队列。当驱动需要同时处理同一队列中的多个请求时,一般多用blkdev_dequeue_requestelv_requeue_request

request

一个块请求队列可以包含那些实际并不向磁盘读写数据的请求,生产商信息、底层诊断操作、与特殊设备模式相关指令、介质写模式设定等。每个request结构都代表一个块设备的I/O请求,这个I/O请求可以通过对多个独立请求的合并而来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void sbull_request(request_queue_t *q)
{
struct request *req; //定义请求结构体
while((req = elv_next_request(q)) != NULL)//elv_next_request()获得队列中第一个未完成请求
{
struct sbull_dev *dev = req->rq_disk->private_data;
if(!blk_fs_request(req)) //判断是否为文件系统请求
{
printk(KERN_NOTICE "Skip non-fs request\n");
end_request(req,0); //通知请求处理失败,0为失败,1为成功
continue;
}
sbull_transfer(dev,req->sector,req->current_nr_sectors,req->buffer,rq_data_dir(req));
end_request(req,1);
}
}
  • elv_next_request
    用来获取队列中第一个未完成的请求,当没有请求需要处理时,返回NULL。请求被获取后并不从队列中删除。
  • blk_fs_request
    用来判断该请求是否是一个文件系统请求。
  • end_request
    void end_request(struct request *req, int succeeded);传递当前请求的指针和完成结果(0表示不成功,非0表示成功)。
  • req->sector
    在设备上开始扇区的索引号
  • req->buffer
    传输或者接收数据的缓冲区指针
  • rq_data_dir
    传输方向,返回0表示从设备读数据,非0表示向设备写入数据

如果多个请求都是对磁盘中相邻扇区进行操作,则内核将合并它们,内核不会合并在单独request结构中的读写操作,如果合并的结果会打破对请求队列的限制,则内核也不会对请求进行合并。

bio

一个request结构是作为一个bio结构的链表实现的,保证在执行请求的时候驱动程序能知道执行的位置。当内核以文件系统、虚拟内存子系统或者系统调用的形式决定从块I/O设备输入、输出块数据时,它将再结合一个bio结构,用来描述这个操作。该结构被传递给I/O代码,代码会把它合并到一个已存在的request结构中,或者根据需要再创建一个新的request结构。bio结构包含了驱动程序执行请求的全部信息,而不必与初始化这个请求的用户空间的进程相关联。

block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned int bi_flags; /* status, command, etc */
int bi_error;
unsigned long bi_rw; /* bottom bits READ/WRITE, top bits priority */
struct bvec_iter bi_iter;
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
atomic_t __bi_remaining;
bio_end_io_t *bi_end_io;
void *bi_private;
#ifdef CONFIG_BLK_CGROUP
/*
* Optional ioc and css associated with this bio. Put on bio
* release. Read comment on top of bio_associate_current().
*/
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
};
unsigned short bi_vcnt; /* how many bio_vec's */
/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/
unsigned short bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t __bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};

bio的核心是一个名为bi_io_vec的数组

1
2
3
4
5
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};

遍历request中的bio

1
2
3
4
5
6
7
8
9
10
11
static int sbull_xfer_request(struct sbull_dev *dev,struct request *req)
{
struct bio *bio;
int nsect = 0;
rq_for_each_bio(bio,req)//此宏遍历请求中的每个bio,传递用于sbull_xfer_bio()传输的指针
{
sbull_xfer_bio(dev,bio); //调用 bio 处理函数
nsect += bio->bi_size/KERNEL_SECTOR_SIZE; //传递的字节数/扇区大小等于扇区数
}
return nsect;
}

遍历bio中的segment

遍历bio中的segment,也就是遍历bio结构中的bi_io_vec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int sbull_xfer_bio(struct sbull_dev *dev,struct bio *bio)
{
int i;
struct bio_vec *bvec; //定义实际的 vec 列表
sector_t sector = bio->bi_sector; //定义要传输的第一个扇区
//下面的宏遍历bio的每一段,获得一个内核虚拟地址来存取缓冲
bio_for_each_segment(bvec,bio,i)
{
char *buffer = __bio_kmap_atomic(bio,i,KM_USER0);//通过kmap_atomic()函数获得返
//回bio的第i个缓冲区的虚拟地址
sbull_transfer(dev,
sector, // 开始扇区的索引号
bio_cur_sectors(bio), // 需要传输的扇区数
buffer, // 传输数据的缓冲区指针
bio_data_dir(bio)== WRITE); // 传输方向,0表述从设备读,非0从设备写
sector += bio_cur_sectors(bio); //返回扇区数
__bio_kunmap_atomic(bio,KM_USER0); //返回由 __bio_kmap_atomic()获得的内核虚拟地址
}
return 0;
}

参考&鸣谢

  • 《设备驱动程序》