Linux内核原理之虚拟文件系统(下)

文章目录

    • 处理VFS对象
      • 文件系统操作
        • 注册文件系统
        • 装载和卸载
          • mount系统调用
          • umount系统调用
      • 文件操作过程
        • 查找inode
        • 打开文件
        • 读取和写入
    • 参考资料

处理VFS对象

文件系统操作

注册文件系统

文件系统注册到内核时,是编译为模块,或者持久编译到内核。fs/super.c中的register_filesystem函数用来向内核注册文件系统,该函数扫描文件系统结构组成的单链表,直至到达链表尾部然后添加新的元素或者找到所需的文件系统

装载和卸载

装载操作开始于超级块的读取,file_system_type中保存的read_super函数指针返回一个类型为super_block的对象,用来在内存中表示超级块

mount系统调用

入口点是sys_mount函数,在fs/namespace.c定义,代码流程如下图

Linux内核原理之虚拟文件系统(下)_第1张图片

首先将装载选项(类型、设备和选项)从用户空间复制到内核空间,内核将控制权转给do_mount函数,该函数调用path_lookup找到装载点所在的dentry项

do_mount充当一个多路分解器,将需要完成的工作委派给装载类型对应的各个函数,其中do_new_mount处理普通装载操作,它分为两部分:do_kern_mount和do_add_mount

Linux内核原理之虚拟文件系统(下)_第2张图片

  • do_kern_mount首先使用get_fs_type找到对应的file_system_type实例,并扫描已注册文件系统的链表,返回正确的项,然后调用特定于文件系统类型的get_sb函数读取相关的超级块,返回struct super_block的实例

  • do_add_mount首先处理一些必须的锁定操作,确保同一个文件系统不会重复装载到同一位置,然后主要工作是在graft_tree函数,新装载的文件系统通过attach_recursive_mnt函数添加到父文件系统的命名空间

    Linux内核原理之虚拟文件系统(下)_第3张图片

    • nameidata用于将vfsmount实例和denrty实例聚集起来,该结构保存了装载点的dentry实例和装载之前该目录对应的vfsmount实例

    • mnt_set_mount确保新的vfsmount实例中的mnt_parent成员指向父文件系统的vfsmount实例,以及mnt_mountpoint成员指向装载点在父文件系统中的dentry实例,旧的dentry实例d_mounted值加一

    Linux内核原理之虚拟文件系统(下)_第4张图片
    • 函数commit_tree将新的vfsmount实例添加到全局散列表以及父文件系统vfsmount实例中的子文件系统链表

      Linux内核原理之虚拟文件系统(下)_第5张图片
umount系统调用

umount系统调用的入口点是fs/namespace.c中的sys_umount,如下图

Linux内核原理之虚拟文件系统(下)_第6张图片

  • 首先,__user_walk找到装载点的dentry和vfsmount实例,主要的工作委派给do_umount函数
  • 如果定义了特定于超级块的umount_begin函数,则调用它。例如,网络文件系统在卸载前,需要终止与远程文件系统的通信
  • 如果装在的文件系统不再需要(通过使用计数判断),或者指定了MNT_DETACH来强制卸载文件系统,则调用umount_tree和release_mounts函数,前者将d_mounted减一,后者使用保存在mnt_mountpoints和mnt_parent中的数据,将环境恢复到文件系统装在之前的状态。同时被卸载的文件系统对应的数据结构,会从内核链表中移除

文件操作过程

查找inode

主要操作是根据给定的文件名查找inode,nameidata结构用于向查找函数传递参数,并保存查找结果,该结构定义如下

Linux内核原理之虚拟文件系统(下)_第7张图片

主要成员:

  • 查找完成后,dentry和mnt包含了找到的文件系统项的数据
  • last包含了查找的名称,包含字符串和散列值
  • flags保存了相关标志

内核使用path_lookup函数查找路径和文件名,该函数需要一个nameidata类型的指针,用作临时结果的“暂存器”

在这里插入图片描述

内核使用nameidata实例规定查找的起点,如果名称以/开头,使用当前根目录的dentry实例和vfsmount实例;否则,从当前进程的task_struct获得当前工作目录的数据

主要处理在link_path_walk函数,它调用__link_path_walk函数,该函数代码流程图如下

Linux内核原理之虚拟文件系统(下)_第8张图片

该函数由一个大的循环组成,逐分量处理文件或路径名(路径根据/被分解为多个分量),每个循环的主要逻辑为:

  • 将nameidata实例的mnt和dentry成员设置为根目录或工作目录对应的数据项
  • 对目录进行权限检查,判断进程是否允许进入该目录、
  • 路径名称是逐字母扫描,根据/将路径分为多个路径分量,每个循环处理一个路径分量;路径分量的每个字符传给partial_name_hash函数,用于计算散列和,将他保存到qstr实例中
  • 处理(.),直接跳过
  • 处理(. .),委派给follow_dotdot函数,当前目录为进程的根目录时,没有效果,否则,分两种情况:
    • 如果当前目录不是一个装载点的根目录时,将当前dentry对象的d_parent成员作为新的目录
    • 如果是已装载文件系统的根目录,利用保存在mnt_mountpoint和mnt_parent中的信息定义下一个dentry和vfsmount对象。follow_mount和lookup_mnt用于取得所需的信息
  • 如果路径分量是一个普通文件,需要区分两种情况进行处理,数据位于dentry缓存 或者 需要文件系统底层实现进行查找,函数do_lookup负责区分两种情况,返回所需的fentry实例
  • 最后一步:判断该分量是否为符号链接(方法:只有勇于符号链接的inode,其inode_operations中才包含lookup函数,否则为NULL)

最后一个分量对应的dentry作为函数link_path_walk的返回结果

打开文件

标准库的open函数用于打开文件,该函数使用同名的open系统调用,调用了fs/open.c中的sys_open函数,代码流程图如下

Linux内核原理之虚拟文件系统(下)_第9张图片

  • force_o_largefile检查是否应该不考虑用户层传递的标志
  • 接下来的主要处理是在do_sys_open函数
    • 调用get_unused_fd_flags查找未使用的文件描述符
    • 根据open参数中的文件路径名称,查找对应的inode,主要是在do_file_open函数
      • open_namei函数调用path_lookup函数查找inode并执行几个额外检查
      • nameidata_to_filp初始化预读结构,将新创建的file实例放置到超级块的sfiles链表中,并调用底层文件系统随影file_operations中的open函数
  • fd_install函数将file实例放置到进程task_struct结构的files->fd数组中
  • 最后控制权转到用户进程,返回进程描述符

读取和写入

文件打开之后,使用read和write系统调用进行读写,入口函数是sys_read和sys_write(都是在fs/read_write.c实现)

  • read

    函数需要三个参数:文件描述符、保存数据的缓冲区和指定读取字符数的长度参数,代码流程如下图

    • 根据文件描述符,fget_light函数(fs/file_table.c中)从task_struct结构中找到相关的file实例
    • file_pos_read找到文件当前的读写位置(返回file->f_pos的值)
    • vfs_read函数调用特定于文件的读取函数file->f_op->read,如果该函数没有实现,则调用一般的辅助函数sys_sync_read
    • file_pos_write函数记录文件内部新的读写位置

    注意:读取数据涉及到复杂的缓冲区和缓存系统,详见Linux内核原理之通用块设备层

    Linux内核原理之虚拟文件系统(下)_第10张图片

参考资料

  • Linux内核设计与实现
  • 深入Linux内核架构

你可能感兴趣的:(Linux内核)