2.2 Linux文件系统

1、虚拟文件系统VFS

1.1、背景说明

1.1.1、层次结构

1

所谓块设备,就是在该设备上读写数据时,必须要以块为基本单位,而不能以随机的字节起始地址和长度。一般的磁盘块就是扇区,常见扇区大小是512字节。

上图是一个块设备操作在内核中涉及到的层次组织。其中分为四个大的层次:

  • 第一层:VFS。系统提供的虚拟文件系统,对所有的文件系统提供了一个统一的抽象视角,并且负责文件系统测层次结构组织。
  • 第二层:磁盘高速缓存。为了加快磁盘块设备的存储,内核对用到的磁盘数据在内存里做了相应缓存。
  • 第三层:文件系统层。根据每个文件系统自己的组织方法,将文件中的相对偏移地址,转换成磁盘里面实际的偏移地址。也可以直接使用裸的块设备。
  • 第四层:块设备层。该层次处理实际的磁盘操作。其又分为3个小层:通用块层、io调度层、块设备驱动层。
    本章描述的是第一层:VFS虚拟文件系统层。

1.1.2、vfs的基本元素super_block、inode、dentry、file

vfs的基本元素super_block、inode、dentry、file都是文件系统在内核中的四种基本数据结构,其表示的含义分别是:

  • super_block:表示文件系统的整体控制信息。在磁盘上有对应的数据,类似于fat文件系统中的DBR(DOS BOOT RECORD)。
  • inode:文件和文件夹的实际控制信息。在磁盘上有对应的数据,类似于fat文件系统中的目录项。每一个inode记录文件的开始位置、大小、权限、修改时间等等。
  • dentry:也是表示一个文件信息的,但是磁盘上无对应的数据。其作用其实和inode差不多,只不过是内核表示文件的一个辅助结构来实现linux文件系统的一些特性,比如多个文件系统的统一视角、层次关系表述、快速路径查找、灵活的mount。
  • file:进程open一个文件操作的描述符,多个file对应同一个文件。
  • vfsmount:表示一个文件系统的一个根节点。其中包含文件系统类型、文件系统super_block、文件系统挂载点。

1.1.2.1、super_block和inode表示的磁盘数据的层次结构

2

1.1.2.2、vfs层次关系的全景图

3

1.1.2.3、inode的缓存树结构

4

1.2、文件系统的初始化

1.2.1、rootfs(根文件系统)的初始化

1.2.1.1、init_rootfs()

安装rootfs文件系统的目的是提供一个初始安装点的空目录。其核心函数是init_rootfs()和init_mount_tree()。
函数在内核初始化时被调用,调用关系为start_kernel() -> vfs_caches_init() -> mnt_init() -> init_rootfs ()、init_mount_tree()。

5
6
7
8

1.2.1.2、init_mount_tree()

9
10
11

1.2.1.3、file_system_type->get_sb()(rootfs的实现)

Rootfs文件系统的type->getsb()函数调用的是rootfs_get_sb,所以接下来我们分析rootfs_get_sb的实现。

12
13
14
15
16
17
18
19
20
21
这里写图片描述

1.2.1.4、prepare_namespace ()

根文件系统的第二阶段是安装实际的根文件系统,这个根文件系统是通过“root”启动参数传入的。这部分的操作是在prepare_namespace()函数中实现的。

1.2.2、sysfs的初始化

1.2.2.1、sysfs_init()

函数在内核初始化时被调用,调用关系为start_kernel() -> vfs_caches_init() -> mnt_init()->sysfs_init()。

这里写图片描述
这里写图片描述
这里写图片描述

1.2.3、proc fs的初始化

1.2.3.1、proc_root_init ()

函数在内核初始化时被调用,调用关系为start_kernel() -> proc_root_init()。

26
这里写图片描述
这里写图片描述
这里写图片描述

1.2.4、bdev fs的初始化

1.2.4.1、bdev_cache_init()

函数在内核初始化时被调用,调用关系为start_kernel() -> vfs_caches_init() -> bdev_cache_init()。

这里写图片描述
31

1.2.5、ext2 fs的初始化

1.2.5.1、init_ext2_fs()

函数被声明成ext2驱动模块初始化函数,在模块初始化时被调用。

这里写图片描述
这里写图片描述
这里写图片描述

1.2.6、ramfs的初始化

1.2.6.1、init_ramfs_fs()

函数被声明成ramfs驱动模块初始化函数,在模块初始化时被调用。

这里写图片描述
36

1.2.7、devfs的初始化

1.2.7.1、init_devfs_fs()

函数被声明成devfs驱动模块初始化函数,在模块初始化时被调用。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
41

1.2.7.2、mount_devfs_fs()

函数在内核初始化时被调用,调用关系为init() -> prepare_namespace() -> mount_devfs_fs()。

这里写图片描述

1.2.8、devpts fs的初始化

1.2.8.1、init_devpts_fs()

函数被声明成devpts fs驱动模块初始化函数,在模块初始化时被调用。

这里写图片描述
这里写图片描述

1.2.9、pipefs的初始化

1.2.9.1、init_pipe_fs()

这里写图片描述
46

1.3、vfs相关系统调用的相关函数

1.3.1、mount()

1.3.1.1、sys_ mount()

mount()函数的系统调用服务函数为sys_ mount(),我们来分析sys_ mount()的实现过程。

这里写图片描述
这里写图片描述
这里写图片描述

1.3.1.2、path_lookup()

这里写图片描述
51
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
56
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
61
这里写图片描述
这里写图片描述
这里写图片描述

1.3.1.3、inode->i_op->lookup()(ext2文件夹的实现)

Real_lookup()函数调用的是父inode的lookup函数来查找实际的inode,并链接inode和新的dentry。我们通过ext2文件系统的inode lookup函数来分析,看看lookup函数具体都干了什么。

这里写图片描述
66
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
71

1.3.1.4、super_block->s_op->alloc_inode()(ext2的实现)

这里写图片描述
这里写图片描述

1.3.1.5、super_block-> s_op->read_inode()(ext2的实现)

ext2_lookup()函数调用read_inode()函数来填充实际的磁盘数据到inode数据结构当中去。

不仅如此,read_inode()函数还有非常重要的作用,几个非常重要的指针在这个函数中赋值:inode->i_op(inode操作函数指针)、inode->i_fop(默认file操作函数指针)、inode->i_mapping->a_ops(inode页 缓存的操作函数指针)。对特殊文件(字符设备节点文件、块设备节点文件、管道文件、sock文件)的特殊处理,也是在read_inode()函数中通过调用init_special_inode()函数完成的。

我们以ext2文件系统的read_inode()函数ext2_read_inode()为例,来说明read_inode()函数的实现。

这里写图片描述
这里写图片描述
76
这里写图片描述
这里写图片描述

1.3.1.6、init_special_inode()(字符/块设备、fifo/sock文件的read_inode实现)

init_special_inode()是read_inode()中对特殊文件(字符设备节点文件、块设备节点文件、管道文件、sock文件)的inode函数指针的初始化处理。

这里写图片描述

1.3.1.7、inode->i_op->lookup()(sysfs文件夹的实现)

sysfs file在创建的时候,只有sysfs_dirent结构,而没有inode和dentry结构。那sysfs file的inode和dentry结构是什么时候产生的呢?在路径查找时,调用inode->i_op->lookup()函数时才会分配实际的inode和dentry结构。

sysfs的lookup()函数为sysfs_lookup,我们看看其实现:

这里写图片描述
81
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
86

1.3.1.8、do_new_mount()

这里写图片描述

1.3.1.9、do_kern_mount()

这里写图片描述
这里写图片描述

1.3.1.10、file_system_type->get_sb()(sysfs的实现)

sysfs文件系统的file_system_type->get_sb()函数为sysfs_get_sb,我们看其具体的实现过程。

这里写图片描述
91
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
96

1.3.1.11、file_system_type->get_sb()(proc fs的实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
101
这里写图片描述
这里写图片描述

1.3.1.12、file_system_type->get_sb()(bdev fs的实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.1.13、file_system_type->get_sb()(ext2的实现)

在分配新的文件系统结构do_kern_mount()函数中,需要调用type->getsb()函数来分配文件系统的超级块,我们以ext2文件系统的超级块分配函数ext2_get_sb()为例,来说明getsb()函数的实现。

这里写图片描述
这里写图片描述
这里写图片描述
111
这里写图片描述
这里写图片描述
这里写图片描述

1.3.1.14、open_bdev_excl()

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
121
这里写图片描述
这里写图片描述
这里写图片描述

1.3.1.15、super_block->s_op->alloc_inode ()(bdev的实现)

这里写图片描述
这里写图片描述

1.3.1.16、do_add_mount()

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
131
这里写图片描述

1.3.2、open()

1.3.2.1、sys_open()

这里写图片描述
这里写图片描述

1.3.2.2、get_unused_fd()

这里写图片描述
这里写图片描述

1.3.2.3、do_filp_open()

这里写图片描述

1.3.2.4、open_namei()

这里写图片描述
这里写图片描述
这里写图片描述
141
这里写图片描述
这里写图片描述

1.3.2.5、nameidata_to_filp()

这里写图片描述
这里写图片描述
这里写图片描述

1.3.2.6、inode->i_fop->open()(ext2文件的实现)

这里写图片描述

1.3.2.7、inode->i_fop->open()(ext2文件夹的实现)

ext2文件夹inode的open函数没有实现
这里写图片描述

1.3.2.8、inode->i_fop->open()(字符设备文件的实现)

这里写图片描述
这里写图片描述
151

1.3.2.9、inode->i_fop->open()(块设备文件的实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
161
这里写图片描述
这里写图片描述

1.3.2.10、inode->i_fop->open()(sysfs attr文件的实现)

这里写图片描述
这里写图片描述

.3.2.11、inode->i_fop->open()(sysfs bin_attr文件的实现)

这里写图片描述
这里写图片描述

1.3.2.12、inode->i_fop->open()(sysfs文件夹的实现)

这里写图片描述
这里写图片描述

1.3.2.13、fd_install()

这里写图片描述

1.3.3、close()

1.3.4、read()

1.3.4.1、sys_read()

171
这里写图片描述
这里写图片描述
这里写图片描述

1.3.4.2、file->f_op->read()(ext2文件的实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.4.3、do_generic_file_read ()(普通读)

这里写图片描述
181
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.4.4、inode-> i_mapping->a_ops->readpage()(ext2 文件实现)

这里写图片描述
这里写图片描述
191
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.4.5、inode->i_mapping->a_ops->readpage()(块设备文件实现)

201
这里写图片描述
这里写图片描述

1.3.4.6、generic_file_direct_IO ()(directIO读)

这里写图片描述

1.3.4.7、inode->i_mapping->a_ops->direct_IO()(ext2 文件实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
211

1.3.4.8、file->f_op->aio_read()(ext2文件的实现)

aio_read()是异步文件读取方法,在启动异步操作以后函数会立即返回,不会等待实际的磁盘操作完成。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.4.9、file->f_op->read()(sysfs attr文件的实现)

sysfs本身就是内存文件系统,所以sysfs没有磁盘缓存,不涉及到对磁盘缓存的操作。

这里写图片描述
这里写图片描述

1.3.5、write()

1.3.5.1、sys_write()

这里写图片描述
这里写图片描述
221

1.3.5.2、file->f_op->write()(ext2文件的实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.5.3、generic_file_buffered_write()(普通写)

这里写图片描述
这里写图片描述
这里写图片描述
231
这里写图片描述
这里写图片描述
这里写图片描述

1.3.5.4、inode->i_mapping->a_ops->prepare_write()(ext2 文件实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
241

1.3.5.5、inode->i_mapping->a_ops->commit_write()(ext2 文件实现)

这里写图片描述
这里写图片描述
这里写图片描述

1.3.5.6、inode-> i_mapping->a_ops->writepage()(ext2 文件实现)

writepage函数将缓存中的数据刷新到磁盘当中,write函数并不会直接调用该函数。writepage函数会被内核的缓存刷新线程和同步函数sync、fsync所调用。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
251
这里写图片描述

1.3.5.7、inode-> i_mapping->a_ops->prepare_write()(块设备文件实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.5.8、inode-> i_mapping->a_ops->commit _write()(块设备文件实现)

这里写图片描述
这里写图片描述
这里写图片描述
261

1.3.5.9、inode-> i_mapping->a_ops-> writepage()(块设备文件实现)

这里写图片描述
这里写图片描述

1.3.5.10、generic_file_direct_write()(directIO写)

这里写图片描述

direct_IO函数的实现参考inode->i_mapping->a_ops->direct_IO()(ext2 文件实现)这一节的描述。

1.3.5.11、file->f_op->aio_ write()(ext2文件的实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.5.12、file->f_op->write()(sysfs attr文件的实现)

sysfs本身就是内存文件系统,所以sysfs没有磁盘缓存,不涉及到对磁盘缓存的操作。

这里写图片描述
这里写图片描述

1.3.6、ioctl()

1.3.6.1、sys_ioctl ()

这里写图片描述
271
这里写图片描述
这里写图片描述
这里写图片描述

1.3.6.2、file->f_op->ioctl()(ext2文件的实现)

这里写图片描述
这里写图片描述
这里写图片描述

1.3.6.3、file->f_op->ioctl()(字符设备文件的实现)

1.3.7、mmap()

正常的文件读写会经过以下过程:文件把用户态数据传递到磁盘缓存,磁盘缓存把数据传递到磁盘。而mmap文件映射,提供了一种方法,直接把磁盘缓存映射到用户内存空间,减少了一次内存拷贝,提高了操作效率。

1.3.7.1、sys_mmap2()

这里写图片描述
这里写图片描述
这里写图片描述
281
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.7.2、file->f_op->mmap()(ext2文件的实现)

f_op->mmap()函数并没有立即建起其vma所描述的用户地址和文件缓存之间的映射,而只是把vma->vm_ops函数操作赋值。在用户访问vma线性地址时,发生缺页异常,调用vma->vm_ops->nopage函数建立映射关系。

这里写图片描述
这里写图片描述

1.3.7.3、vma->vm_ops->nopage()(ext2文件的实现)

291
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.8、madvise()

使用mmap建立文件和用户空间的映射后,在读文件发生缺页时才会建立映射,在访问数据量很大时,会频繁发生缺页中断。为解决这一问题,可以在mmap以后,调用madvise函数建立起真正的映射,避免访问缺页才建立映射。

1.3.8.1、sys_madvise()

这里写图片描述
这里写图片描述

1.3.9、mknod()

创建设备文件节点,设备文件的类型有:表示对字符设备、块设备、FIFO管道、soket。

1.3.9.1、sys_mknod()

这里写图片描述
这里写图片描述
301
这里写图片描述
这里写图片描述

1.3.9.2、inode->i_op->mknod()(ext2文件夹的实现)

这里写图片描述
这里写图片描述
这里写图片描述

1.3.9.3、inode->i_op->create()(ext2文件夹的实现)

这里写图片描述
这里写图片描述

1.3.10、mkdir()

1.3.10.1、sys_mkdir()

这里写图片描述
这里写图片描述
311

1.3.10.2、inode->i_op->mkdir()(ext2文件夹的实现)

这里写图片描述
这里写图片描述
这里写图片描述

创建硬链接。

这里写图片描述
这里写图片描述
这里写图片描述

1.3.11.2、inode->i_op->link()(ext2文件夹的实现)

这里写图片描述
这里写图片描述

创建符号链接。

这里写图片描述
这里写图片描述
321

1.3.12.2、inode->i_op->symlink()(ext2文件夹的实现)

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.3.12.3、inode->i_op->follow_link()(ext2符号链接文件的实现)

符号路径的查找。在路径查找函数path_lookup()中,判断一个节点如果是符号链接,会调用inode->i_op->follow_link()进行解析。

Ext2符号链接文件在创建的时候我们看到其inode->i_op被赋值成了ext2_symlink_inode_operations。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
331
这里写图片描述
这里写图片描述
这里写图片描述

1.3.13、chroot()

更换当前进程的根路径。

1.3.13.1、sys_ chroot()

这里写图片描述
这里写图片描述

1.3.14、flock()

文件锁,多个进程操作同一文件互斥使用。

1.3.14.1、sys_ flock()

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
341
这里写图片描述

1.4、特殊文件系统的操作函数

1.4.1、sysfs

1.4.1.1、sysfs_create_dir()

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.4.1.2、sysfs_create_file()

351
这里写图片描述

上面看到sysfs file在创建的时候,只有sysfs_dirent结构,而没有inode和dentry结构。那sysfs file的inode和dentry结构是什么时候产生的呢?在路径查找时,调用inode->i_op->lookup()函数时才会分配实际的inode和dentry结构。

sysfs的lookup()函数为sysfs_lookup,我们看看其实现:

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.4.1.3、sysfs_create_bin_file()

这里写图片描述

361
这里写图片描述

1.4.1.5、sysfs_create_group()

这里写图片描述
这里写图片描述
这里写图片描述

1.4.2、Ramdisk和根文件系统

1.4.2.1、Init()

这里写图片描述
这里写图片描述
这里写图片描述

1.4.2.2、populate_rootfs()

这里写图片描述
这里写图片描述
371
这里写图片描述

1.4.2.3、prepare_namespace()

这里写图片描述
这里写图片描述
这里写图片描述

1.4.2.4initrd_load()

这里写图片描述
这里写图片描述
这里写图片描述

1.4.2.5、mount_root()

这里写图片描述
这里写图片描述
381

1.4.2.6、default_rootfs()

在2.6.16.54内核中,在加载cpio-initrd失败后,会调用prepare_namespace。prepare_namespace的第一句mount_devfs(),mount_devfs的作用mount devfs到 “/dev”路径,这个时候根目录是rootfs,但是这个时候的“/dev”路径是怎么产生的呢??

这个应该是2.6.16.54内核的一个bug,再更高的版本中,我们看到了default_rootfs来解决这个问题。default_rootfs中创建了rootfs的“/dev”。

这里写图片描述

2、块设备

2.1、背景说明

2.1.1、层次结构

这里写图片描述

所谓块设备,就是在该设备上读写数据时,必须要以块为基本单位,而不能以随机的字节起始地址和长度。一般的磁盘块就是扇区,常见扇区大小是512字节。

上图是一个块设备操作在内核中涉及到的层次组织。其中分为四个大的层次:

  • 第一层:VFS。系统提供的虚拟文件系统,对所有的文件系统提供了一个统一的抽象视角,并且负责文件系统测层次结构组织。
  • 第二层:磁盘高速缓存。为了加快磁盘块设备的存储,内核对用到的磁盘数据在内存里做了相应缓存。
  • 第三层:文件系统层。根据每个文件系统自己的组织方法,将文件中的相对偏移地址,转换成磁盘里面实际的偏移地址。也可以直接使用裸的块设备。
  • 第四层:块设备层。该层次处理实际的磁盘操作。其又分为3个小层:通用块层、io调度层、块设备驱动层。
    本章描述的是第四层:块设备层。

2.1.2、页、段、块、扇区概念

这里写图片描述

上一节描述了块设备操作的几个层次,在块设备的操作时涉及到几个长度单位页、段、块、扇区:

  • 1、页(page)。是磁盘高速缓存层使用的单位,磁盘高速缓存层分配缓存的基本分配单位为内存页。x86架构的一页为4k。
  • 2、段(segment)。一次性传输多个连续的块,就是一个段。
  • 3、块缓冲区(block buffer)。文件系统一般使用硬盘的单位是块,一般是扇区的2次幂大小。因为如果一个扇区一个块的话会消耗太多的分区表(FAT)空间。Fat文件系统中,这种单位也叫簇(cluster)。
  • 4、扇区(sector)。说磁盘是块设备,就是说磁盘的基本数据读写单位为扇区,而不能以随意字节偏移随意字节长度来读写。

2.1.3、bio、request概念

2.1.3.1、bio

bio是文件系统或者块设备对通用块层提交读写请求的数据结构。Bio代表了要传输一段内存到磁盘空间中去,其中磁盘空间要求连续,而内存空间不要求连续可以使用一个内存链表来表述。内存链表中的成员就是段。

这里写图片描述
这里写图片描述
这里写图片描述

2.1.3.2、request

上面的层次通过bio提交读写请求给块设备通用层,但是块设备的读写操作最耗时的是磁头移动即寻道所消耗的时间,如果每一次bio磁盘都立即响应的话磁盘的效率会很低,因为磁头需要根据bio中的地址随机移动。为了优化,io调度程序会将多个地址相近的bio组合到一起执行,这个组合就是request结构。

这就是说,bio请求并不是根据下发的顺序来执行的,另外不光要把磁头移动的开销降低,还要考虑到bio最长的响应时间,在规定时间内必须要去执行bio。

所以一个request结构就是多个bio结构的组合:

这里写图片描述
这里写图片描述
这里写图片描述

2.2、块设备的注册

2.2.1、分配块设备号register_blkdev()

该函数的目的很简单就是分配或者注册一个块设备的主设备号。

391
这里写图片描述

2.2.2、磁盘结构gendisk

块设备驱动最核心的数据结构,和磁盘相关数据就再gendisk数据结构中了。磁盘的一般操作函数blcok_device_operations、磁盘的读写请求队列都包含在gendisk里了。

磁盘和磁盘上所有的分区共享一个gendisk数据结构。

2.2.2.1、数据结构定义

这里写图片描述

2.2.2.2、块设备操作函数blcok_device_operations

和字符驱动的操作函数file_operations不同的是,blcok_device_operations中不包含读写函数。

这里写图片描述

2.2.2.3、块设备读写函数(队列处理函数)

Gendisk->queue成员包含了块设备的读写请求队列,上层对块设备的数据读写操作都是通过对队列下发请求来完成的。这一部分的详细描述请参考“请求队列”这一节的详细描述。

2.2.2.4、磁盘注册函数add_disk()

这里写图片描述
这里写图片描述
这里写图片描述

可以根据设备号将gendisk注册到dev_map中,同样也可以根据设备号在dev_map中找到相应的gendisk数据结构。这个查找函数是get_gendisk。

这里写图片描述
这里写图片描述

其实在add_disk之前还需要alloc_disk,在alloc_disk函数中设置了gendisk->kobj的kest为block_subsys。即gendisk在sysfs文件系统中的父文件夹为 “/sys/block”。

这里写图片描述
401
这里写图片描述

2.2.3、块设备操作符blcok_device

gendisk数据结构是给块设备驱动使用的,并且是一个磁盘和磁盘上所有的分区共享一个gendisk结构的。而在内核内部对块设备的引用,是通过使用block_device结构来实现的。每个磁盘和分区都有一个独立的block_device结构。

2.2.3.1、数据结构定义

这里写图片描述

2.2.3.2、bdev文件系统

每个block_device数据结构对应一个磁盘或者分区块设备,每个block_device都对应bdev文件系统中一个inode。

为什么需要弄出一个bdev文件系统的inode来呢,主要是为了共享块设备的磁盘缓存。磁盘上的多个文件是有多个inode结构的,但是同一个磁盘上的所有文件的的磁盘缓存inode->i_mapping会指向同一磁盘缓存,即block_device对应的bdev inode->i_data。其实现可以参考vfs一章open_bdev_excl()函数的实现分析。

2.3、请求队列

2.3.1、向块通用层下发请求generic_make_request()

文件系统向通用块层下发数据读写操作的函数就是generic_make_request,用户需要向提供bio结构的参数,用以描述读写操作类型、读写块设备和起始扇区和大小、内存的存储位置。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

2.3.2、IO调度层处理__make_request()

为了减少磁盘磁头的移动寻道时间,上层下发给通用块层的bio并不会立即被执行,IO调度程序会尝试合并地址连续的bio到一个request中,如果不能合并则新建一个request,并根据request中的扇区地址和读写超时时间等策略,对request进行排序,以优化磁盘寻道所消耗的时间。

Bio和队列q被提交给__make_request()函数,__make_request调用队列的调度策略函数q->elevator对bio进行合并尝试,或者分配一个新的request结构。最后调用用户注册的队列处理函数q->request_fn处理队列中的request,执行真正的磁盘读写操作。
一个request包含一个或者多个bio。

这里写图片描述
这里写图片描述
这里写图片描述
411
这里写图片描述
这里写图片描述

2.3.3、IO调度层策略函数q->elevator()

Linux 2.6中提供了四种不同类型的io调度算法或电梯算法,分别为“预期(Anticipatory)”算法、“最后期限(Deadline)”算法、“CFQ(Complete Fairness Queueing,完全公平队列)”算法、“Noop(No Operation)”算法。对算法的实现这里不做讨论,可以到相应文件中去看其详细实现:

这里写图片描述

2.3.3.1、策略注册函数elv_register()

这里写图片描述

2.3.3.2、启动参数指定策略elevator_setup()

这里写图片描述

2.3.3.3、策略初始化elevator_init()

这里写图片描述

2.3.4、块设备驱动注册的队列处理函数q->request_fn()

对于用户初始化请求队列时注册的用户队列处理函数,在ldd3的实例代码中,给出了3种实现代码:只处理request中的当前传输、处理request中所有的bio链表传输、没有request队列直接传输bio。

这里写图片描述

2.3.4.1、只处理request中的当前传输sbull_request()

这里写图片描述

2.3.4.2、处理request中所有的bio链表传输sbull_full_request()

这里写图片描述
421

2.3.4.3、没有request队列直接传输bio结构sbull_make_request()

这里写图片描述

2.3.5、队列初始化blk_init_queue()

这里写图片描述
这里写图片描述
这里写图片描述

2.4、实际磁盘块设备的注册

我们看看实际驱动中,IDE、SATA、SCSI磁盘是怎么注册其块设备的。

2.4.1、IDE磁盘块设备的创建

2.4.1.1、ide驱动的注册

在探测到ide设备时,会调用ide_disk_probe注册ide的磁盘块设备。

这里写图片描述
这里写图片描述
这里写图片描述

2.4.1.2、ide设备的注册

这里写图片描述
这里写图片描述
431
这里写图片描述
这里写图片描述

2.4.2、SCSI磁盘块设备的创建

SCSI的设备又分了几种:sr代表 scsi + rom,sd代表 scsi + disk,sg 代表 scsi + generic,st代表 scsi + tape。

2.4.2.1、sd驱动的注册

我们看其中sd的块驱动的注册。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

2.4.2.2、sd设备的注册

SCSI设备的注册过程也是先扫描设备再注册设备,扫描设备这一段暂时没法看清楚,不过可以找到SCSI设备初始化的函数scsi_sysfs_device_initialize(),从中可以看到设备的总线类型被设置为scsi_bus_type即“/sys/bus/scsi”。

441
这里写图片描述
这里写图片描述
这里写图片描述

2.4.3、SATA磁盘块设备的创建

在linux中,sata设备也被注册成scsi设备,所以sata使用的驱动也是scsi驱动。

ata_scsi_scan_host

2.4.3.1、sata_mv设备的注册

我们从一个Marvell SATA设备注册的例子,查看sata设备的注册。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

我们通过分析sata_mv的probe函数mv_init_one的调用关系, mv_init_one()->ata_device_add()->ata_scsi_scan_host()->scsi_scan_target ()->__scsi_scan_target ()->scsi_probe_and_add_lun ()->scsi_alloc_sdev ()。可以看到最终SATA设备调到了SCSI设备的初始化函数,把自己注册成了SCSI设备。

3、磁盘高速缓存

3.1、背景说明

3.1.1、层次结构

这里写图片描述

所谓块设备,就是在该设备上读写数据时,必须要以块为基本单位,而不能以随机的字节起始地址和长度。一般的磁盘块就是扇区,常见扇区大小是512字节。

上图是一个块设备操作在内核中涉及到的层次组织。其中分为四个大的层次:

  • 第一层:VFS。系统提供的虚拟文件系统,对所有的文件系统提供了一个统一的抽象视角,并且负责文件系统测层次结构组织。
  • 第二层:磁盘高速缓存。为了加快磁盘块设备的存储,内核对用到的磁盘数据在内存里做了相应缓存。
  • 第三层:文件系统层。根据每个文件系统自己的组织方法,将文件中的相对偏移地址,转换成磁盘里面实际的偏移地址。也可以直接使用裸的块设备。
  • 第四层:块设备层。该层次处理实际的磁盘操作。其又分为3个小层:通用块层、io调度层、块设备驱动层。
    本章描述的是第二层:磁盘高速缓存。

3.1.2、磁盘高速缓存

因为磁盘的读写速度是比较慢的,所以内核对磁盘访问使用内存来缓存,叫做磁盘缓存。

读磁盘数据时,如果缓存中已有最新数据则直接读取缓存,否则先更新磁盘数据到缓存再读取缓存;写磁盘数据是写入到磁盘缓存中,内核进程定期的刷新缓存到磁盘中。

磁盘缓存是隶属如inode的,每个inode拥有自己的磁盘缓存树。Inode可以是基于文件系统的文件、文件夹,也可以是直接对块设备操作的块设备文件,也可以是进程交换分区的交换数据。

以页(page)为单位的缓存,叫做页高速缓存。而文件系统是以块(block)为单位处理数据的,块缓存也叫块缓冲区。页缓存包含块缓存。

3.2、页高速缓存

每个inode拥有自己的磁盘缓存树,磁盘缓存树保存在inode的address_space对象inode->i_mapping中。

3.2.1、页缓存树的组织

radix_tree_root缓存树的的根节点,缓存树是由radix_tree_node类型的的节点来组成的,radix_tree_node包含有64个指针槽,它可以指向最底层的page,也可以指向中间层次的radix_tree_node节点。

缓存树是一个灵活层次的树,你可以根据总节点的大小来决定要建立多少的层次。

这里写图片描述
451

3.2.2、address_space定义

这里写图片描述

其中有两个成员需要重点关注,address_space->page_tree表示缓存树,address_space->a_ops表示缓存和磁盘交互数据的方法。

3.2.3、page定义

如果是以page为单位来当做磁盘缓存,page结构中需要存储数据的磁盘位置信息。

这里写图片描述
这里写图片描述

3.2.4、查找页函数find_get_page()

这里写图片描述
这里写图片描述

3.2.5、增加页函数add_to_page_cache()

这里写图片描述
这里写图片描述
这里写图片描述

3.2.6、更新页函数read_cache_page()

这里写图片描述
461

3.3、块缓冲区

前一节讨论的页磁盘缓存是以页为单位的,这个适应于直接通过块设备文件来访问磁盘的情况。而通过文件系统来访问磁盘是以块为单位的,块小于页,一个页缓存中的数据在磁盘上存储也许是不连续的,占用了多个不连续的磁盘块。

所以对于要使用块缓存区的情况,需要使用更多的数据结构来管理。

3.3.1、块缓冲区的组织

内核使用buffer_head数据结构来管理块缓存,一个page内多个块的buffer_head使用buffer_head->b_this_page链接成环形链表,page结构使用page->private指针指向这个buffer_head链表的首成员。

这里写图片描述

3.3.2、buffer_head定义

这里写图片描述

3.3.3、增加块缓存函数grow_buffers()

该函数处理的是,一个页缓冲区内的多个块缓存buffer_head在磁盘上也是连续的。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
471
这里写图片描述
这里写图片描述

3.3.4、查找块缓存函数__find_get_block()

这里写图片描述
这里写图片描述
这里写图片描述

3.3.5、查找或增加块缓存函数__getblk()

这里写图片描述
这里写图片描述

3.3.6、查找并更新块缓存函数__bread()

这里写图片描述
这里写图片描述
481
这里写图片描述
这里写图片描述

3.3.7、多个块缓存读写函数ll_rw_block()

这里写图片描述

3.4、缓冲区同步

为了减少对磁盘的访问,数据写入缓存区后并不会马上刷新到磁盘上,而是把缓存页标志置为PG_dirty,并为后续的读写直接访问缓存就可以了。

对于页缓存中包含块缓存的情况,只要一个块缓存的BH_Dirty标志被置位,整个页缓存的PG_dirty标志置位。

磁盘缓存区提供了延迟写特性,但是在以下情况下缓存需要刷新到磁盘当中:

  • 1、磁盘缓存占用的内存过多,需要减少缓存对内存的占用;
  • 2、页变成脏页的时间过长;
  • 3、手工调用sync()、fsync()或fdatasync()函数进行缓存刷新同步。
    前两种情况是由内核启动的缓存同步进程来完成的,第三种情况是由用户和程序主动调用完成的。

3.4.1、内核进程自动同步

3.4.1.1、pdflush内核线程

这里写图片描述
这里写图片描述
这里写图片描述

3.4.1.2、派发任务函数pdflush_operation()

内核调用pdflush_operation()给pdflush线程派发任务。

这里写图片描述

3.4.1.3、任务1:扫描搜索并刷新脏页background_writeout()

该任务会在几种情况下下发给pdflush线程:

  • 1、用户发出sync系统调用;
  • 2、grow_buffers函数分配新缓冲区时失败;
  • 3、页框回收函数调用free_more_memory或try_to_free_pages;

    这里写图片描述
    这里写图片描述
    491
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    501
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    511
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述

3.4.1.4、任务2:刷新超时脏页wb_kupdate()

该任务定时刷新超时的脏页。

这里写图片描述
这里写图片描述
这里写图片描述
521

3.4.2、手工同步

3.4.2.1、刷新系统所有的缓存sync()

这里写图片描述
这里写图片描述
这里写图片描述

3.4.2.2、刷新单个文件的缓存fsync()

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

3.5、文件映射

3.5.1、原理说明

正常的文件读写会经过以下过程:文件把用户态数据传递到磁盘缓存,磁盘缓存把数据传递到磁盘。而mmap文件映射,提供了一种方法,直接把磁盘缓存映射到用户内存空间,减少了一次内存拷贝,提高了操作效率。

Mmap映射以后用户虚拟地址和文件磁盘缓存之间的映射关系如下:

这里写图片描述

f_op->mmap()函数并没有立即建起其vma所描述的用户地址和文件缓存之间的映射,而只是把vma->vm_ops函数操作赋值。在用户访问vma线性地址时,发生缺页异常,调用vma->vm_ops->nopage函数建立映射关系。

4、参考资料

猜你喜欢

转载自blog.csdn.net/pwl999/article/details/78238321
2.2