前言
块设备驱动程序是Liux块子系统中的最底层组件。它们从IO调度程序中获得请求,然后按要求处理这些请求。一个块设备驱动程序可能处理几个块设备。例如,IDE设备驱动程序可以处理几个IDE磁盘,其中的每个都是一个单独的块设备。而且,每个磁盘通常是被分区的,每个分区又可以被看作是一个逻辑块设备。
核心数据结构
block_device
block_device结构代表了内核中的一个块设备。它可以表示整个磁盘或一个特定的分区。当这个结构代表一个分区时,它的bd_contains成员指向包含这个分区的设备,bd_part成员指向设备的分区结构。当这个结构代表一个块设备时,bd_disk成员指向设备的gendisk结构。
1
struct block_device {
2
dev_t bd_dev; /* not a kdev_t - it’s a search key /
3
int bd_openers;
4
struct inode * bd_inode; / will die /
5
struct super_block * bd_super;
6
struct mutex bd_mutex; / open/close mutex /
7
struct list_head bd_inodes;
8
void * bd_claiming;
9
void * bd_holder;
10
int bd_holders;
11
bool bd_write_holder;
12
#ifdef CONFIG_SYSFS
13
struct list_head bd_holder_disks;
14
#endif
15
struct block_device * bd_contains;
16
unsigned bd_block_size;
17
struct hd_struct * bd_part;
18
/ number of times partitions within this device have been opened. /
19
unsigned bd_part_count;
20
int bd_invalidated;
21
struct gendisk * bd_disk;
22
struct request_queue * bd_queue;
23
struct list_head bd_list;
24
/
25
* Private data. You must have bd_claim’ed the block_device
26
* to use this. NOTE: bd_claim allows an owner to claim
27
* the same device multiple times, the owner must take special
28
* care to not mess up bd_private for that case.
29
/
30
unsigned long bd_private;
31
32
/ The counter of freeze processes /
33
int bd_fsfreeze_count;
34
/ Mutex for freeze */
35
struct mutex bd_fsfreeze_mutex;
36
};
bd_dev:设备号,用作搜索键而不是kdev_t类型。
bd_openers:设备打开者的计数器。
bd_inode:指向相关的inode结构体的指针(已弃用)。
bd_super:指向相关的super_block结构体的指针。
bd_mutex:用于保护设备的开启和关闭操作的互斥锁。
bd_inodes:一个链表头,用于保存使用此设备的inode结构体。
bd_claiming:指向声明该设备的指针。
bd_holder:指向持有该设备的指针。
bd_holders:持有该设备的计数器。
bd_write_holder:表示设备是否由写操作的持有者。
bd_holder_disks:一个链表头,用于保存持有该设备的磁盘。
bd_contains:指向包含该设备的block_device结构体的指针。
bd_block_size:设备的块大小。
bd_part:指向相关的hd_struct结构体的指针。
bd_part_count:该设备的分区被打开的次数。
bd_invalidated:表示设备是否已失效。
bd_disk:指向相关的gendisk结构体的指针。
bd_queue:指向相关的request_queue结构体的指针。
bd_list:一个链表头,用于将该设备添加到全局设备列表中。
bd_private:私有数据。使用该数据之前,必须使用bd_claim声明对设备的所有权。需要注意的是,bd_claim允许一个所有者多次声明相同的设备,所有者必须特别注意不要破坏针对该情况的bd_private。
bd_fsfreeze_count:冻结进程的计数器。
bd_fsfreeze_mutex:用于冻结操作的互斥锁。
register_blkdev
register_blkdev函数用于注册块设备,并将主设备号与设备名进行映射。它使用一个链表数组major_names来存储不同主设备号的映射关系,并使用互斥锁来确保并发访问的安全性。通过动态分配内存和链表操作,函数可以有效地管理和分配主设备号,并提供适当的错误处理。
1
int register_blkdev(unsigned int major, const char *name)
2
{
3
struct blk_major_name **n, p;
4
int index, ret = 0;
5
6
mutex_lock(&block_class_lock);
7
8
/ temporary */
9
if (major == 0) {
10
for (index = ARRAY_SIZE(major_names)-1; index > 0; index–) {
11
if (major_names[index] == NULL)
12
break;
13
}
14
15
if (index == 0) {
16
printk(“register_blkdev: failed to get major for %s\n”,
17
name);
18
ret = -EBUSY;
19
goto out;
20
}
21
major = index;
22
ret = major;
23
}
24
25
p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL);
26
if (p == NULL) {
27
ret = -ENOMEM;
28
goto out;
29
}
30
31
p->major = major;
32
strlcpy(p->name, name, sizeof(p->name));
33
p->next = NULL;
34
index = major_to_index(major);
35
36
for (n = &major_names[index]; *n; n = &(*n)->next) {
37
if ((*n)->major == major)
38
break;
39
}
40
if (!*n)
41
*n = p;
42
else
43
ret = -EBUSY;
44
45
if (ret < 0) {
46
printk(“register_blkdev: cannot get major %d for %s\n”,
47
major, name);
48
kfree§;
49
}
50
out:
51
mutex_unlock(&block_class_lock);
52
return ret;
53
}
使用互斥锁block_class_lock来锁定对块设备类的访问。这是为了确保在注册块设备时不会发生并发访问的问题。
如果传入的主设备号major为0,表示请求动态分配主设备号。
在上述条件块中,通过遍历major_names数组找到一个可用的主设备号。遍历是从数组末尾开始,找到第一个为NULL的元素位置,表示该位置的主设备号可用。将其赋值给major并将其作为返回值,表示成功分配的主设备号。
使用kmalloc函数动态分配一个struct blk_major_name结构体的内存,该结构体用于存储主设备号和设备名的映射关系。
将设备名name复制到struct blk_major_name结构体的name成员中。这里使用了strlcpy函数,确保不会发生缓冲区溢出。
将主设备号major转换为索引值index,用于在major_names数组中定位对应的链表。
在对应的链表中遍历,查找是否已经存在相同的主设备号major。
如果找到了相同的主设备号,表示已经被占用,将返回值ret设置为-EBUSY表示注册失败。否则,将新分配的struct blk_major_name结构体添加到链表的末尾。
如果ret小于0(即注册失败),则打印错误消息并释放分配的内存。
释放对块设备类的互斥锁,允许其他线程访问块设备类。
返回ret作为函数的结果,表示注册块设备的状态。如果成功,返回的是分配的主设备号;如果失败,返回的是相应的错误码。
blkdev_open
对于块设备文件的操作,通过block_dev伪文件系统来完成,open操作定义的函数为blkdev_open()
blkdev_open的主要任务有两个:1.获取设备的block_device信息。2.从gendisk中读取相关信息保存到block_device,同时建立数据结构之间的联系。
1
static int blkdev_open(struct inode * inode, struct file * filp)
2
{
3
struct block_device bdev;
4
5
/
6
* Preserve backwards compatibility and allow large file access
7
* even if userspace doesn’t ask for it explicitly. Some mkfs
8
* binary needs it. We might want to drop this workaround
9
* during an unstable branch.
10
*/
11
filp->f_flags |= O_LARGEFILE;
12
13
if (filp->f_flags & O_NDELAY)
14
filp->f_mode |= FMODE_NDELAY;
15
if (filp->f_flags & O_EXCL)
16
filp->f_mode |= FMODE_EXCL;
17
if ((filp->f_flags & O_ACCMODE) == 3)
18
filp->f_mode |= FMODE_WRITE_IOCTL;
19
20
bdev = bd_acquire(inode);
21
if (bdev == NULL)
22
return -ENOMEM;
23
24
filp->f_mapping = bdev->bd_inode->i_mapping;
25
26
return blkdev_get(bdev, filp->f_mode, filp);
27
}
设置文件标志O_LARGEFILE,以支持对大文件的访问。这是为了保持向后兼容性,在不显式要求的情况下允许对大文件的访问。某些mkfs二进制文件可能需要这个设置。
检查文件标志O_NDELAY是否被设置。如果是,则设置文件模式FMODE_NDELAY,表示以非阻塞模式打开文件。
检查文件标志O_EXCL是否被设置。如果是,则设置文件模式FMODE_EXCL,表示以独占模式打开文件。
检查文件访问模式是否为O_RDWR,即读写模式。如果是,则设置文件模式FMODE_WRITE_IOCTL,表示允许通过IOCTL进行写操作。
根据给定的inode获取对应的块设备block_device。bd_acquire函数负责获取块设备的引用计数,确保块设备在文件打开期间不会被卸载。
检查获取块设备是否失败。如果获取失败,则返回错误码-ENOMEM,表示内存不足。
将文件的映射关系设置为块设备的i_mapping。这是为了确保文件系统能够正确地将读写操作转发到块设备。
调用blkdev_get函数以确保块设备的引用计数递增,并执行必要的打开操作。函数将返回打开操作的结果。
blkdev_read_iter
blkdev_read_iter函数用于在块设备上执行读取操作。它首先获取块设备的大小和当前位置信息,然后检查是否已经达到或超出了块设备的大小。根据剩余可读取的字节数,调整目标迭代器的长度。最后,调用通用的文件读取函数generic_file_read_iter进行实际的读取操作,并返回读取操作的结果。
我们具体分析下generic_file_read_iter。generic_file_read_iter函数中的这部分代码用于执行通用文件的读取操作。它根据iocb中的标志判断是否进行直接IO读取,然后根据情况调用相应的函数进行读取操作,并更新位置信息和迭代器。在特定条件下,它会跳过剩余的读取操作,并更新文件的访问时间。最后,它返回读取操作的结果。
1
ssize_t blkdev_read_iter(struct kiocb *iocb, struct iov_iter *to)
2
{
3
struct file *file = iocb->ki_filp;
4
struct inode *bd_inode = file->f_mapping->host;
5
loff_t size = i_size_read(bd_inode);
6
loff_t pos = iocb->ki_pos;
7
8
if (pos >= size)
9
return 0;
10
11
size -= pos;
12
iov_iter_truncate(to, size);
13
return generic_file_read_iter(iocb, to);
14
}
15
16
ssize_t
17
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
18
{
19
struct file *file = iocb->ki_filp;
20
ssize_t retval = 0;
21
loff_t *ppos = &iocb->ki_pos;
22
loff_t pos = *ppos;
23
24
if (iocb->ki_flags & IOCB_DIRECT) {
25
struct address_space *mapping = file->f_mapping;
26
struct inode inode = mapping->host;
27
size_t count = iov_iter_count(iter);
28
loff_t size;
29
30
if (!count)
31
goto out; / skip atime */
32
size = i_size_read(inode);
33
retval = filemap_write_and_wait_range(mapping, pos,
34
pos + count - 1);
35
if (!retval) {
36
struct iov_iter data = *iter;
37
retval = mapping->a_ops->direct_IO(iocb, &data, pos);
38
}
39
40
if (retval > 0) {
41
ppos = pos + retval;
42
iov_iter_advance(iter, retval);
43
}
44
45
/
46
* Btrfs can have a short DIO read if we encounter
47
* compressed extents, so if there was an error, or if
48
* we’ve already read everything we wanted to, or if
49
* there was a short read because we hit EOF, go ahead
50
* and return. Otherwise fallthrough to buffered io for
51
* the rest of the read. Buffered reads will not work for
52
* DAX files, so don’t bother trying.
53
*/
54
if (retval < 0 || !iov_iter_count(iter) || *ppos >= size ||
55
IS_DAX(inode)) {
56
file_accessed(file);
57
goto out;
58
}
59
}
60
61
retval = do_generic_file_read(file, ppos, iter, retval);
62
out:
63
return retval;
64
}
获取与iocb(kiocb结构体)相关联的文件对象file。
loff_t *ppos = &iocb->ki_pos;:获取指向iocb中位置信息的指针ppos。
loff_t pos = *ppos;:将当前位置信息保存到变量pos中。
if (iocb->ki_flags & IOCB_DIRECT) { … }:检查iocb中的标志IOCB_DIRECT是否被设置。如果设置了该标志,表示执行直接IO(Direct I/O)操作。
在直接IO操作的条件块中,首先获取文件的地址空间mapping和对应的索引节点inode。然后获取读取操作的字节数count。如果字节数为0,则跳过访问时间更新(atime)的步骤。
调用filemap_write_and_wait_range函数,确保在进行直接IO读取之前,将文件中的数据写回存储设备并等待完成。该函数将返回写入操作的结果。
如果写入操作成功(retval为0),则复制iter到新的data迭代器,并调用文件地址空间的direct_IO操作进行直接IO读取。该函数将返回读取操作的结果。
如果读取操作返回的字节数retval大于0,表示读取成功,更新当前位置ppos和iter的偏移,并继续进行后续的读取操作。
接下来的条件块用于处理特定情况下的直接IO读取。例如,如果读取出现错误(retval小于0),或者已经读取完所有数据(iov_iter_count(iter)为0),或者已经读取到文件末尾(*ppos >= size),或者文件是DAX文件(IS_DAX(inode)),则跳过剩余的读取操作,并更新文件的访问时间。
如果上述条件均不满足,则调用do_generic_file_read函数执行通用文件读取操作。该函数将处理剩余的读取操作,并更新位置信息和迭代器。
最后,跳转到标签out处,并返回读取操作的结果retval。
blkdev_write_iter
blkdev_write_iter函数用于在块设备上执行写入操作。它首先检查块设备是否为只读模式,以及输入迭代器中是否有数据可写入。然后,根据当前位置和块设备的大小调整输入迭代器的长度。接着,通过调用通用的文件写入函数进行实际的写入操作,并返回写入操作的结果。如果写入成功,还会进行同步写入操作,确保数据真正写入块设备。最后,返回写入操作的结果。
1
ssize_t blkdev_write_iter(struct kiocb *iocb, struct iov_iter *from)
2
{
3
struct file *file = iocb->ki_filp;
4
struct inode *bd_inode = file->f_mapping->host;
5
loff_t size = i_size_read(bd_inode);
6
struct blk_plug plug;
7
ssize_t ret;
8
9
if (bdev_read_only(I_BDEV(bd_inode)))
10
return -EPERM;
11
12
if (!iov_iter_count(from))
13
return 0;
14
15
if (iocb->ki_pos >= size)
16
return -ENOSPC;
17
18
iov_iter_truncate(from, size - iocb->ki_pos);
19
20
blk_start_plug(&plug);
21
ret = __generic_file_write_iter(iocb, from);
22
if (ret > 0) {
23
ssize_t err;
24
err = generic_write_sync(file, iocb->ki_pos - ret, ret);
25
if (err < 0)
26
ret = err;
27
}
28
blk_finish_plug(&plug);
29
return ret;
30
}
struct file *file = iocb->ki_filp;:获取与iocb(kiocb结构体)相关联的文件对象file。
struct inode *bd_inode = file->f_mapping->host;:获取文件对象对应的块设备的索引节点bd_inode。
loff_t size = i_size_read(bd_inode);:获取块设备的大小(文件大小)。
if (bdev_read_only(I_BDEV(bd_inode))) return -EPERM;:检查块设备是否为只读模式。如果是,直接返回错误码-EPERM,表示无法进行写入操作。
if (!iov_iter_count(from)) return 0;:检查输入迭代器from中的字节数是否为0。如果为0,表示没有数据可写入,直接返回0,表示写入操作已完成。
if (iocb->ki_pos >= size) return -ENOSPC;:检查当前位置是否已经达到或超出了块设备的大小。如果是,返回错误码-ENOSPC,表示空间不足,无法进行写入操作。
iov_iter_truncate(from, size - iocb->ki_pos);:根据当前位置和块设备的大小,调整输入迭代器from的长度,确保只写入剩余可写入的字节数。
blk_start_plug(&plug);:启动块设备的批量操作,将后续的写入操作收集到一个批处理中。
ret = __generic_file_write_iter(iocb, from);:调用通用的文件写入函数__generic_file_write_iter,执行写入操作。将iocb和输入迭代器from传递给该函数进行写入操作,并返回写入操作的结果。
如果写入操作返回的字节数ret大于0,表示写入成功,执行以下代码块:
a. ssize_t err;:定义变量err用于保存同步写入操作的结果。
b. err = generic_write_sync(file, iocb->ki_pos - ret, ret);:调用通用的写入同步函数generic_write_sync,将写入的起始位置和字节数传递给该函数进行同步写入操作,并将结果保存到err中。
c. if (err < 0) ret = err;:如果同步写入操作返回的结果小于0,表示出现错误,将错误码保存到ret中。
blk_finish_plug(&plug);:结束块设备的批量操作。
返回写入操作的结果ret。
__generic_file_write_iter
__generic_file_write_iter函数用于实际执行文件写入操作。它会进行一系列的检查和操作,包括移除特权标志、更新修改时间戳和调用适当的子函数来处理直接IO或标准缓冲区写入。需要注意的是,对于O_SYNC写入,该函数不会处理数据同步的问题,需要调用者自行处理。这主要是因为希望避免在持有i_mutex时进行数据同步操作。
1
ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
2
{
3
struct file *file = iocb->ki_filp;
4
struct address_space * mapping = file->f_mapping;
5
struct inode inode = mapping->host;
6
ssize_t written = 0;
7
ssize_t err;
8
ssize_t status;
9
10
/ We can write back this queue in page reclaim /
11
current->backing_dev_info = inode_to_bdi(inode);
12
err = file_remove_privs(file);
13
if (err)
14
goto out;
15
16
err = file_update_time(file);
17
if (err)
18
goto out;
19
20
if (iocb->ki_flags & IOCB_DIRECT) {
21
loff_t pos, endbyte;
22
23
written = generic_file_direct_write(iocb, from, iocb->ki_pos);
24
/
25
* If the write stopped short of completing, fall back to
26
* buffered writes. Some filesystems do this for writes to
27
* holes, for example. For DAX files, a buffered write will
28
* not succeed (even if it did, DAX does not handle dirty
29
* page-cache pages correctly).
30
/
31
if (written < 0 || !iov_iter_count(from) || IS_DAX(inode))
32
goto out;
33
34
status = generic_perform_write(file, from, pos = iocb->ki_pos);
35
/
36
* If generic_perform_write() returned a synchronous error
37
* then we want to return the number of bytes which were
38
* direct-written, or the error code if that was zero. Note
39
* that this differs from normal direct-io semantics, which
40
* will return -EFOO even if some bytes were written.
41
/
42
if (unlikely(status < 0)) {
43
err = status;
44
goto out;
45
}
46
/
47
* We need to ensure that the page cache pages are written to
48
* disk and invalidated to preserve the expected O_DIRECT
49
* semantics.
50
/
51
endbyte = pos + status - 1;
52
err = filemap_write_and_wait_range(mapping, pos, endbyte);
53
if (err == 0) {
54
iocb->ki_pos = endbyte + 1;
55
written += status;
56
invalidate_mapping_pages(mapping,
57
pos >> PAGE_CACHE_SHIFT,
58
endbyte >> PAGE_CACHE_SHIFT);
59
} else {
60
/
61
* We don’t know how much we wrote, so just return
62
* the number of bytes which were direct-written
63
*/
64
}
65
} else {
66
written = generic_perform_write(file, from, iocb->ki_pos);
67
if (likely(written > 0))
68
iocb->ki_pos += written;
69
}
70
out:
71
current->backing_dev_info = NULL;
72
return written ? written : err;
73
}
struct file *file = iocb->ki_filp;:获取与iocb(kiocb结构体)相关联的文件对象file。
struct address_space *mapping = file->f_mapping;:获取文件对象对应的地址空间mapping。
struct inode *inode = mapping->host;:获取地址空间对应的索引节点inode。
current->backing_dev_info = inode_to_bdi(inode);:将当前进程的backing_dev_info字段设置为inode对应的块设备信息。
err = file_remove_privs(file);:移除文件对象的特权标志。这是为了确保写入操作不会以特权身份执行。
err = file_update_time(file);:更新文件的修改时间戳。
如果iocb->ki_flags中包含IOCB_DIRECT标志,表示执行直接IO(direct I/O),则执行以下代码块:
a. 定义变量pos和endbyte,用于记录写入的起始位置和结束位置。
b. written = generic_file_direct_write(iocb, from, iocb->ki_pos);:调用通用的直接写入函数generic_file_direct_write,执行直接IO操作,并返回已写入的字节数。
c. 如果写入操作未完成(written < 0)或输入迭代器中没有数据可写入(!iov_iter_count(from)),或者inode是DAX文件(IS_DAX(inode)为真),则跳转到out标签。
d. status = generic_perform_write(file, from, pos = iocb->ki_pos);:调用通用的执行写入操作的函数generic_perform_write,执行标准缓冲区写入操作,并将写入的起始位置保存到pos中,返回写入的状态码。
e. 如果status小于0,表示写入操作返回了同步错误,将错误码保存到err中,并跳转到out标签。
f. 计算写入操作的结束位置endbyte = pos + status - 1。
g. err = filemap_write_and_wait_range(mapping, pos, endbyte);:将页高速缓存中的数据写入磁盘,并等待写入操作完成。
h. 如果err为0,表示写入操作成功,更新iocb的位置iocb->ki_pos为endbyte + 1,累加已写入的字节数到written中,并使映射页无效。
i. 如果err不为0,表示写入操作出现错误,由于无法确定实际写入了多少字节,因此不做处理。
如果不是直接IO操作,则执行以下代码块:
a. written = generic_perform_write(file, from, iocb->ki_pos);:调用通用的执行写入操作的函数generic_perform_write,执行标准缓冲区写入操作,并返回已写入的字节数。
b. 如果已写入的字节数大于0,则更新iocb的位置iocb->ki_pos为当前位置加上已写入的字节数。
out:标签处的代码用于清理操作,将current->backing_dev_info字段重置为NULL。
返回已写入的字节数written,如果没有写入任何数据,则返回错误码err。
generic_write_sync
generic_write_sync函数用于根据文件的打开标志和文件的映射索引节点的同步属性,判断是否需要执行同步写入操作。如果满足同步条件,则调用vfs_fsync_range函数执行同步写入操作。
1
static inline int generic_write_sync(struct file *file, loff_t pos, loff_t count)
2
{
3
if (!(file->f_flags & O_DSYNC) && !IS_SYNC(file->f_mapping->host))
4
return 0;
5
return vfs_fsync_range(file, pos, pos + count - 1,
6
(file->f_flags & __O_SYNC) ? 0 : 1);
7
}
vfs_fsync_range
vfs_fsync_range函数用于将指定文件的指定范围内的数据和元数据同步到磁盘。它会检查文件是否定义了fsync函数,并根据参数决定是否写入元数据。在写入元数据之前,它会清除索引节点状态中的相应标志位,并将索引节点标记为已修改。最后,它调用文件的fsync函数来执行实际的同步操作。
1
int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync)
2
{
3
struct inode *inode = file->f_mapping->host;
4
5
if (!file->f_op->fsync)
6
return -EINVAL;
7
if (!datasync && (inode->i_state & I_DIRTY_TIME)) {
8
spin_lock(&inode->i_lock);
9
inode->i_state &= ~I_DIRTY_TIME;
10
spin_unlock(&inode->i_lock);
11
mark_inode_dirty_sync(inode);
12
}
13
return file->f_op->fsync(file, start, end, datasync);
14
}
struct inode *inode = file->f_mapping->host;:获取文件file对应的索引节点inode。
if (!file->f_op->fsync):检查文件的文件操作函数指针f_op中是否定义了fsync函数。如果未定义,则返回错误码-EINVAL。
if (!datasync && (inode->i_state & I_DIRTY_TIME)):如果不是仅执行数据同步,并且索引节点的状态中标志位I_DIRTY_TIME被设置。
spin_lock(&inode->i_lock);:获取索引节点的自旋锁,用于保护对索引节点状态的修改。
inode->i_state &= ~I_DIRTY_TIME;:清除索引节点状态中的I_DIRTY_TIME标志位,表示元数据已被写回。
spin_unlock(&inode->i_lock);:释放索引节点的自旋锁。
mark_inode_dirty_sync(inode);:将索引节点标记为已修改,需要同步到磁盘。
return file->f_op->fsync(file, start, end, datasync);:调用文件的fsync函数,将数据和元数据同步到磁盘。该函数由文件系统提供,并提供了特定文件系统的实现。