虚拟文件子系统 - 带您探索 Linux核心


疯狂内核 带您探索 Linux 核心 掌握计算机底层软件必要知识 虚拟文件系统 陈云松 blog:http://blog.csdn.net/yunsongice email:yunsong_ice@163.com qq:85408821 目录 1 虚拟文件系统概述 ....................................................................................................................... 5 1.1 通用文件模型 .................................................................................................................... 7 1.2 VFS 所处理的系统调用 .................................................................................................... 9 2 虚拟文件系统架构 ..................................................................................................................... 11 2.1 VFS 对象数据结构 .......................................................................................................... 11 2.1.1 超级块对象 ........................................................................................................... 11 2.1.2 索引节点对象 ....................................................................................................... 15 2.1.3 文件对象 ............................................................................................................... 18 2.1.4 目录项对象 ........................................................................................................... 22 2.2 把 Linux 中的 VFS 对象串联起来 ................................................................................. 24 2.2.1 与进程相关的文件 ............................................................................................... 25 2.2.2 索引节点高速缓存 ............................................................................................... 29 2.2.3 目录项高速缓存 ................................................................................................... 30 2.2.4 VFS 对象的具体实现............................................................................................ 32 2.3 文件系统的注册与安装 .................................................................................................. 38 2.3.1 文件系统类型注册 ............................................................................................... 38 2.3.2 文件系统安装数据结构 ....................................................................................... 41 2.3.3 安装普通文件系统 ............................................................................................... 52 2.3.4 分配超级块对象 ................................................................................................... 58 2.3.5 安装根文件系统 ................................................................................................... 60 2.3.6 卸载文件系统 ....................................................................................................... 65 2.4 路径名的查找 .................................................................................................................. 66 2.4.1 查找路径名的一般流程 ....................................................................................... 67 2.4.2 父路径名查找 ....................................................................................................... 82 2.4.3 符号链接的查找 ................................................................................................... 84 2.5 VFS 系统调用的实现 ...................................................................................................... 88 2.5.1 open()系统调用 ..................................................................................................... 88 2.5.2 read()和 write()系统调用 ...................................................................................... 96 2.5.3 close()系统调用 ..................................................................................................... 97 3 第二扩展文件系统 ..................................................................................................................... 99 3.1 Ext2 磁盘数据结构 ........................................................................................................ 101 3.1.1 磁盘超级块 ......................................................................................................... 102 3.1.2 组描述符和位图 ................................................................................................. 105 3.1.3 磁盘索引节点表 ................................................................................................. 105 3.2 VFS 接口数据结构 ........................................................................................................ 110 3.2.1 Ext2 超级块对象 ................................................................................................ 110 3.2.2 Ext2 的索引节点对象 ........................................................................................ 121 3.2.3 创建 Ext2 文件系统 ........................................................................................... 124 3.2.4 Ext2 的方法总结 ................................................................................................. 126 3.3 Ext2 索引节点分配 ........................................................................................................ 129 3.3.1 创建索引节点 ..................................................................................................... 130 3.3.2 删除索引节点 ..................................................................................................... 143 3.4 Ext2 数据块分配 ............................................................................................................ 144 3.4.1 数据块寻址 ......................................................................................................... 145 3.4.2 文件的洞 ............................................................................................................. 147 3.4.3 分配数据块 ......................................................................................................... 148 4 页面高速缓存 ........................................................................................................................... 160 4.1 页高速缓存数据结构 .................................................................................................... 160 4.1.1 address_space 对象 .............................................................................................. 161 4.1.2 基树 ..................................................................................................................... 164 4.2 高速缓存底层处理函数 ................................................................................................ 166 4.2.1 查找页 ................................................................................................................. 166 4.2.2 增加页 ................................................................................................................. 168 4.2.3 删除页 ................................................................................................................. 173 4.3 文件系统与高速缓存 .................................................................................................... 175 4.3.1 缓冲头数据结构 ................................................................................................. 175 4.3.2 分配块设备缓冲区页 ......................................................................................... 178 4.3.3 释放块设备缓冲区页 ......................................................................................... 184 4.4 在页高速缓存中搜索块 ................................................................................................ 185 4.4.1 __find_get_block()函数 ....................................................................................... 185 4.4.2 __getblk()函数 ..................................................................................................... 188 4.4.3 __bread()函数 ...................................................................................................... 190 4.5 把脏页写入磁盘 ............................................................................................................ 191 4.5.1 pdflush 内核线程 ................................................................................................. 192 4.5.2 搜索要刷新的脏页 ............................................................................................. 193 4.5.3 回写陈旧的脏页 ................................................................................................. 196 5 文件读写................................................................................................................................... 199 5.1 系统调用 VFS 层的处理 .............................................................................................. 200 5.2 第二扩展文件系统 Ext2 层的处理 .............................................................................. 201 5.2.1 Ext2 的磁盘布局 ................................................................................................. 202 5.2.2 Ext2 的超级块对象 ............................................................................................. 206 5.2.3 Ext2 索引节点对象的创建 ................................................................................. 210 5.2.4 Ext2 索引节点对象的读取 ................................................................................. 218 5.2.5 Ext2 层读文件入口函数 ..................................................................................... 225 5.3 页高速缓存层的处理 .................................................................................................... 237 5.3.1 创建一个 bio 请求 .............................................................................................. 238 5.3.2 得到文件的逻辑块号 ......................................................................................... 244 5.3.3 普通文件的 readpage 方法 ................................................................................ 251 5.3.4 块设备文件的 readpage 方法 ............................................................................ 252 5.3.5 文件的预读 ......................................................................................................... 260 5.4 通用块层的处理 ............................................................................................................ 264 5.4.1 块设备的基础知识 ............................................................................................. 265 5.4.2 通用块层相关数据结构 ..................................................................................... 269 5.4.3 提交 I/O 传输请求 ............................................................................................. 271 5.4.4 请求队列描述符 ................................................................................................. 273 5.5 块设备 I/O 调度层的处理 ............................................................................................ 281 5.5.1 块设备的初始化 ................................................................................................. 284 5.5.2 建立块设备驱动环境 ......................................................................................... 288 5.5.3 关联 block_device 结构...................................................................................... 295 5.5.4 为设备建立请求队列 ......................................................................................... 306 5.5.5 块设备 I/O 调度程序 ......................................................................................... 311 5.5.6 真实的 I/O 调度层处理 ...................................................................................... 321 5.6 块设备驱动层的处理 .................................................................................................... 330 5.6.1 scsi 总线驱动的初始化 ....................................................................................... 330 5.6.2 scsi 设备驱动体系架构 ....................................................................................... 342 5.6.3 scsi 块设备驱动层处理 ....................................................................................... 347 5.6.4 scsi 命令的执行 ................................................................................................... 369 5.6.5 scsi 命令的第一次转变 ....................................................................................... 372 5.6.6 scsi 命令的第二次转变 ....................................................................................... 380 5.7 写文件............................................................................................................................ 384 5.7.1 generic file_write 函数 ........................................................................................ 384 5.7.2 普通文件的 prepare_write 方法 ......................................................................... 386 5.7.3 块设备文件的 prepare_write 方法 ..................................................................... 387 5.7.4 将脏页写到磁盘 ......................................................................................................... 388 6 直接 I/O 与异步 I/O ................................................................................................................. 391 6.1 直接 I/O ......................................................................................................................... 391 6.2 异步 I/O ......................................................................................................................... 393 6.2.1 Linux 2.6 中的异步 I/O ....................................................................................... 394 6.2.2 异步 I/O 环境 ..................................................................................................... 394 6.2.3 提交异步 I/O 操作 ............................................................................................. 395 1 虚拟文件系统概述 现在我们的主流价值观是社会和谐、世界和谐。同样, Linux 成功的关键因素之一是它具有 与其他操作系统和谐共存的能力。你能够透明地安装具有其他操作系统文件格式的磁盘或分 区,这些操作系统如 Windows、其他版本的 Unix,甚至像 Amiga 那样的市场占有率很低的 系统。通过所谓的虚拟文件系统概念, Linux 使用与其他 Unix 变体相同的方式设法支持多 种文件系统类型。 虚拟文件系统所隐含的思想是把表示很多不同种类文件系统的共同信息放入内核;其中有一 个字段或函数来支持 Linux 所支持的所有实际文件系统所提供的任何操作。对所调用的每个 读、写或其他函数,内核都能把它们替换成支持本地 Linux 文件系统、NTFS 文件系统,或 者文件所在的任何其他文件系统的实际函数。 虚拟文件系统( Virtual Filesystem)也可以称之为虚拟文件系统转换( Virtual Filesystem Switch, VFS),是一个内核软件层,用来处理与 Unix 标准文件系统相关的所有系统调用。其健壮性 表现在能为各种文件系统提供一个通用的接口。 例如,假设一个用户输入以下 shell 命令: $ cp /floppy/TEST /tmp/test 其中/floppy 是 MS-DOS 磁盘的一个安装点,而 /tmp 是一个标准的第二扩展文件系统( second Extended Filesystom, Ext2)的目录。正如图( a)所示, VFS 是用户的应用程序与文件系统 实现之间的抽象层。因此, cp 程序并不需要知道/floppy/TEST 和 /tmp/test 是什么文件系统 类型。相反, cp 程序直接与 VFS 交互,这是通过 Unix 程序设计人员都熟悉的普通系统调用 来进行的。cp 的执行代码如图( b)所示: VFS 支持的文件系统可以划分为三种主要类型: 磁盘文件系统 这些文件系统管理在本地磁盘分区中可用的存储空间或者其他可以起到磁盘作用的设备(比 如说一个 USB 闪存)。 VFS 支持的基于磁盘的某些著名文件系统还有: - Linux 使用的文件系统,如广泛使用的第二扩展文件系统( Ext2),新近的第三扩展文件系 统(Third Extended Filesystem,Ext3)及 Reiser 文件系统(ReiserFS) - Unix 家族的文件系统,如 sysv 文件系统( System V、Coherent、Xenix)、UFS(BSD、Solaris、 NEXTSTEP),MINIX 文件系统及 VERITAS VxFS(SCO UnixWare)。 - 微软公司的文件系统,如 MS-DOS、VFAT(Windows 95 及随后的版本)及 NTFS(Windows NT 以及随后的版本)。 - IS09660 CD-ROM 文件系统(以前的 High Sierra 文件系统)和通用磁盘格式( UDF)的 DVD 文件系统。 - 其他有专利权的文件系统,如 HPFS(IBM 公司的 OS/2)、HFS(苹果公司的 Macintosh)、 AFFS(Amiga 公司的快速文件系统)以及 ADFS(Acorn 公司的磁盘文件归档系统)。 - 起源于非 Linux 系统的其他日志文件系统,如 IBM 的 JFS 和 SGI 的 XFS。 网络文件系统 这些文件系统允许轻易地访问属于其他网络计算机的文件系统所包含的文件。虚拟文件系统 所支持的一些著名的网络文件系统有: NFS、Coda、AFS(Andrew 文件系统)、 CIFS(用于 Microsoft Windows 的通用网络文件系统)以及 NCP(Novell 公司的 NetWare Core Protocol)。 特殊文件系统 这些文件系统不管理本地或者远程磁盘空间。 /proc、/sys、/dev 等文件系统是特殊文件系统 的一个典型范例。 Unix 的目录建立了一棵根目录为 “/”的树。根目录包含在根文件系统( root filesystem)中, 在 Linux 中这个根文件系统通常就是 Ext2 或 Ext3 类型。其他所有的文件系统都可以被 “安 装”在根文件系统的子目中。当一个文件系统被安装在某一个目录上时,在父文件系统中的 目录内容不再是可访问的了,因为任何路径(包括安装点),都将引用已安装的文件系统。 但是,当被安装文件系统卸载时,原目录的内容又可再现。 所以,Unix 文件系统的一个重要特点就是可以由系统管理员用来隐藏文件,他们只需把一 个文件系统安装在要隐藏文件的目录中即可。 基于磁盘的文件系统通常存放在块设备中,如硬盘、软盘或者 CD-ROM。Linux VFS 的一个 有用特点是能够处理如 /dev/loop0 这样的虚拟块设备,这种设备可以用来安装普通文件所在的文件系统。作为一种可能的应用,用户可以保护自己的私有文件系统,因为可以通过把自 己文件系统的加密版本存放在一个普通文件中来实现。 第一个虚拟文件系统包含在 1986 年由 Sun 公司发布的 SunOS 操作系统中。从那时起,多数 UNIX 文件系统都包含 VFS。然而,Linux 的 VFS 支持最广泛的文件系统。 1.1 通用文件模型 VFS 所隐含的主要思想在于引入了一个通用的文件模型( common file model),这个模型能 够表示所有支持的文件系统。该模型严格反映传统 Unix 文件系统提供的文件模型。这并不 奇怪,因为 Linux 希望以最小的额外开销运行它的本地文件系统。不过,要实现每个具体的 文件系统,必须将其物理组织结构转换为虚拟文件系统的通用文件模型。 例如在通用文件模型中,每个目录被看作一个文件,可以包含若干文件和其他的子目录。但 是,存在几个非 Unix 的基于磁盘的文件系统,它们利用文件分配表( File Allocation Table, FAT)存放每个文件在目录树中的位置,在这些文件系统中,存放的是目录而不是文件。为 了符合 VFS 的通用文件模型,对上述基于 FAT 的文件系统的实现, Linux 必须在必要时能 够快速建立对应于目录的文件。这样的文件只作为内核内存的对象而存在。 从本质上说, Linux 内核不能对一个特定的函数进行硬编码来执行诸如 read()或 ioctl()这样的 操作,而是对每个操作都必须使用一个指针,指向要访问的具体文件系统的适当函数。 为了进一步说明这一概念,参见前面的那个图,其中显示了内核如何把 read()转换为专对 MS-DOS 文件系统的一个调用。应用程序对 read()的调用引起内核调用相应的 sys_read()服 务例程,这与其他系统调用完全类似。我们在本章后面会看到,文件在内核内存中是由一个 file 数据结构来表示的。这种数据结构中包含一个称为 f_op 的字段,该字段中包含一个指向 专对 MS-DOS 文件的函数指针,当然还包括读文件的函数。 sys_read()查找到指向该函数的指针,并调用它。这样一来,应用程序的 read()就被转化为相 对间接的调用: file->f_op->read(…); 与之类似,write()操作也会引发一个与输出文件相关的 Ext2 写函数的执行。简而言之,内 核负责把一组合适的指针分配给与每个打开文件相关的 file 变量,然后负责调用针对每个具 体文件系统的函数(由 f_op 字段指向)。 你可以把通用文件模型看作是面向对象的,在这里,对象是一个软件结构,其中既定义了数 据结构也定义了其上的操作方法。出于效率的考虑, Linux 的编码并未采用面向对像的程序 设计语言(比如 C++)。因此对象作为普通的 C 数据结构来实现,数据结构中指向函数的字 段就对应于对象的方法。 通用文件模型由下列对象类型组成: 超级块对象( superblock object):存放已安装文件系统的有关信息。对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块( filesystem control block)。 索引节点对象( inode object):存放关于具体文件的一般信息。对基于磁盘的文件系统,这 类对象通常对应于在磁盘上的文件控制块 (file control block)。每个索引节点对象都有一个索 引节点号,这个节点号唯一地标识文件系统中的文件。 文件对象(file object):存放打开文件与进程之间进行交互的有关信息。这类信息仅当进程 访问文件期间在于内核内存中。 目录项对象( dentry object):存放目录项(也就是文件的特定名称)与对应文件进行链接的 有关信息。每个磁盘文件系统都以自己特有的方式将该类信息存在磁盘上。 下图所示是一个简单的示例,说明进程怎样与文件进行交互。 三个不同进程已经打开同一个文件,其中两个进程使用同一个硬链接。在这种情况下,其中 的每个进程都使用自己的文件对象,但只需要两个目录项对象,每个硬链接对应一个目录项 对象。这两个目录项对象指向同一个索引节点对象,该索引节点对象标识超级块对象,以及 随后的普通磁盘文件。 VFS 除了能为所有文件系统的实现提供一个通用接口外,还具有另一个与系统性能相关的 重要作用,那就是一些文件相关数据结构的磁盘高速缓存。例如最近最常使用的目录项对象 被放在所谓目录项高速缓存( dentry cache)的磁盘高速缓存中,从而加速从文件路径名到 最后一个路径分量的索引节点的转换过程。 一般说来,磁盘高速缓存( disk cache)属于软件机制,它允许内核将原本存在磁盘上的某 些信息保存在 RAM 中,以便对这些数据的进一步访问能快速进行,而不必慢速访问磁盘本 身。 注意,磁盘高速缓存不同于硬件高速缓存或内存高速缓存,后两者都与磁盘或其他设备无关。 硬件高速缓存是一个快速静态 RAM,它加快了直接对内存,这样的慢速动态 RAM 的请求。 内存高速缓存是一种软件机制,引入它是为了绕过内核内存分配器( slab 分配器)。 除了目录项高速缓存和索引结点高速缓存之外, Linux 还使用其他磁盘高速缓存。其中最重 要的一种就是所谓的页高速缓存,我们将在本专题中中进行详细介绍。 1.2 VFS 所处理的系统调用 下表列出了 VFS 的系统调用,这些系统调用涉及文件系统、普通文件、目录文件以及符号 链接文件: 系统调用名 说明 mount( ) umount( ) umount2( ) 安装/卸载文件系统 sysfs( ) 获取文件系统信息 statfs( ) fstatfs( ) statfs64( ) fstatfs64( ) ustat( ) 获取文件系统统计信息 chroot( ) pivot_root( ) 更改根目录 chdir( ) fchdir( ) getcwd( ) 对当前目录进行操作 mkdir( ) rmdir( ) 创建/删除目录 getdents( ) getdents64( ) readdir( ) link( ) unlink( ) rename( ) lookup_dcookie( ) 对目录项进行操作 readlink( ) symlink( ) 对软链接进行操作 chown( ) fchown( ) lchown( ) chown16( ) fchown16( ) lchown16( ) 更改文件所有者性 chmod( ) fchmod( ) utime( ) 更改文件属性 stat( ) fstat( ) lstat( ) access( ) oldstat( ) oldfstat( ) oldlstat( ) stat64( ) lstat64( ) fstat64( ) 读取文件状态 open( ) close( ) creat( ) umask( ) 打开/关闭/创建文件 dup( ) dup2( ) fcntl( ) fcntl64( ) 对文件描述符进行操作 select( ) poll( ) 等待一组文件描述符上发生 的事件 系统调用名 说明 truncate( ) ftruncate( ) truncate64( ) ftruncate64( ) 更改文件长度 lseek( ) _llseek( ) 更改文件指针 read( ) write( ) readv( ) writev( ) sendfile( ) sendfile64( ) readahead( ) 进行文件 I/O 操作 io_setup( ) io_submit( ) io_getevents( ) io_cancel( ) io_destroy( ) 异步 I/O (允许多个读和写 请求) pread64( ) pwrite64( ) 搜索并访问文件 mmap( ) mmap2( ) munmap( ) madvise( ) mincore( ) remap_file_pages( ) 处理文件内存映射 fdatasync( ) fsync( ) sync( ) msync( ) 同步文件数据 flock( ) 处理文件锁 setxattr( ) lsetxattr( ) fsetxattr( ) getxattr( ) lgetxattr( ) fgetxattr( ) listxattr( ) llistxattr( ) flistxattr( ) removexattr( ) lremovexattr( ) fremovexattr( ) 处理文件扩展属性 另外还有少数几个由 VFS 处理的其他系统调用,诸如 ioperm()、ioctl()、pipe()和 mknod(), 涉及设备文件和管道文件,这些将在后续章节中讨论。最后一组由 VFS 处理的系统调用, 诸如 socket()、connect()和 bind()属于套接字系统调用,并用于实现网络功能。与表中列出的 系统调用还对应的一些内核服务例程,我们会在后面的章节中陆续进行讨论。 前面我们已经提到, VFS 是应用程序和具体文件系统之间的一层。不过,在某些情况下, 一个文件操作可能由 VFS 本身去执行,无需调用低层函数。例如,当某个进程关闭一个打 开的文件时,并不需要涉及磁盘上的相应文件,因此 VFS 只需释放对应的文件对象。类似 地,当系统调用 lseek()修改一个文件指针,而这个文件指针是打开文件与进程交互所涉及的 一个属性时, VFS 就只需修改对应的文件对象,而不必访问磁盘上的文件,因此,无需调 用具体文件系统的函数。从某种意义上说,可以把 VFS 看成“通用”文件系统,它在必要时 依赖某种具体文件系统。 2 虚拟文件系统架构 2.1 VFS 对象数据结构 前面讲了,为了使各种文件系统和谐相处,内核提供了一个通用的虚拟文件模型。这里,我 们就重点讨论组成这个通用文件模型的那些对象的数据结构。 每个 VFS 对象都存放在一个恰当的数据结构中,其中包括对象的属性和指向对象方法表的 指针。内核可以动态地修改对象的方法,因此可以为对象建立专用的行为。 所有的 VFS 对象都放在 include/linux/Fs.h 中。 2.1.1 超级块对象 超级块对象由 super_block 结构组成: struct super_block { struct list_head s_list; /* 指向超级块链表的指针 */ dev_t s_dev; /* 设备标识符 */ unsigned long s_blocksize; /* 以字节为单位的块大小 */ unsigned char s_blocksize_bits; /* 以位为单位的块大小 */ unsigned char s_dirt; /* 修改(脏)标志 */ unsigned long long s_maxbytes; /* 文件的最长长度 */ struct file_system_type *s_type; /* 文件系统类型 */ struct super_operations *s_op; /* 超级块方法 */ struct dquot_operations *dq_op; /* 磁盘限额处理方法 */ struct quotactl_ops *s_qcop; /* 磁盘限额管理方法 */ struct export_operations *s_export_op; /* 网络文件系统使用的输出操作 */ unsigned long s_flags; /* 安装标志 */ unsigned long s_magic; /* 文件系统的魔数 */ struct dentry *s_root; /* 文件系统根目录的目录项对象 */ struct rw_semaphore s_umount; /* 卸载所用的信号量 */ struct mutex s_lock; /* 超极块信号量 */ int s_count; /* 超级快引用计数器 */ int s_syncing; /* 表示对超级块的索引节点进行同步的标志 */ int s_need_sync_fs; /* 对超级块的已安装文件系统进行同步的标志 */ atomic_t s_active; /* 超级快次级引用计数器 */ void *s_security; /* 指向超级块安全数据结构的指针 */ struct xattr_handler **s_xattr; /* 指向超级块扩展属性结构的指针 */ struct list_head s_inodes; /* 所有索引节点的链表头 */ struct list_head s_dirty; /* 改进型索引节点的链表 */ struct list_head s_io; /* 等待被写入磁盘的索引节点的链表 */ struct hlist_head s_anon; /* 用于处理远程网络文件系统的匿名目录项的链表 */ struct list_head s_files; /* 文件对象的链表 */ struct block_device *s_bdev; /* 指向块设备驱动程序描述符的指针 */ struct list_head s_instances; /* 用于给定文件系统类型的超级块对象链表的指针 */ struct quota_info s_dquot; /* 磁盘限额的描述符 */ int s_frozen; /* 冻结文件系统时使用的标志(强制置于一致状态) */ wait_queue_head_t s_wait_unfrozen;/* 进程挂起的等待队列,直到文件系统被解冻 */ char s_id[32]; /* 包含超级块的块设备名称 */ void *s_fs_info; /* 指向特定文件系统的超级块信息的指针 */ /* * The next field is for VFS *only*. No filesystems have any business * even looking at it. You had been warned. */ struct mutex s_vfs_rename_mutex; /* 当 VFS 通过目录重命名文件时使用的信号量 */ /* Granularity of c/m/atime in ns. —— 时间戳的粒度(纳秒级) Cannot be worse than a second */ u32 s_time_gran; }; 所有超级块对象都以双向循环链表的形式链接在一起。链表中第一个元素用 super_blocks 变 量来表示,而超级块对象的 s_list 字段存放指向链表相邻元素的指针。 sb_lock 自旋锁保护链 表免受多处理器系统上的同时访问。 s_fs_info 字段指向属于具体文件系统的超级块信息;例如,假如超级块对象指的是 Ext2 文 件系统,该字段就指向 ext2_sb_info 数据结构,该结构包括磁盘分配位掩码和其他与 VFS 的通用文件模型无关的数据。 通常,为了效率起见,由 s_fs_info 字段所指向的数据被复制到内存。任何基于磁盘的文件 系统都需要访问和更改自己的磁盘分配位图,以便分配或释放磁盘块。 VFS 允许这些文件 系统直接对内存超级块的 s_fs_info 字段进行操作,而无需访问磁盘。 但是,这种方法带来一个新问题:有可能 VFS 超级块最终不再与磁盘上相应的超级块同步, 即读入内存的 s_fs_info 数据为脏。因此,有必要引入一个 s_dirt 标志来表示该超级块是否是 脏的——那磁盘上的数据是否必须要更新。 缺乏同步还会导致产生我们熟悉的一个问题:当一台机器的电源突然断开而用户来不及正常 关闭系统时,就会出现文件系统崩溃。我们将会在以后的章节中看到, Linux 是通过周期性 地将所有“脏”的超级块写回磁盘来减少该问题带来的危害。 与超级块关联的方法就是所谓的超级块操作。这些操作是由数据结构 super_operations 来描 述的,该结构的起始地址存放在超级块的 s_op 字段中: struct super_operations { /* 为索引节点对象分配空间,包括具体文件系统的数据所需要的空间。 */ struct inode *(*alloc_inode)(struct super_block *sb); /* 撤销索引节点对象,包括具体文件系统的数据。 */ void (*destroy_inode)(struct inode *); /* 用磁盘上的数据填充以参数传递过来的索引节点对象的字段; 索引节点对象的 i_ino 字段标识从磁盘上要读取的具体文件系统的索引节点。 */ void (*read_inode) (struct inode *); /* 当索引节点标记为修改(脏)时调用。 * 像 ReiserFS 和 Ext3 这样的文件系统用它来更新磁盘上的文件系统日志。 */ void (*dirty_inode) (struct inode *); /* 用通过传递参数指定的索引节点对象的内容更新一个文件系统的索引节点。 * 索引节点对象的 i_ino 字段标识所涉及磁盘上文件系统的索引节点。 * flag 参数表示 I/O 操作是否应当同步。 */ int (*write_inode) (struct inode *, int); /* 释放索引节点时调用(减少该节点引用计数器值)以执行具体文件系统操作。 */ void (*put_inode) (struct inode *); /* 在即将撤消索引节点时调用——也就是说, * 当最后一个用户释放该索引节点时; * 实现该方法的文件系统通常使用 generic_drop_inode()函数。 * 该函数从 VFS 数据结构中移走对索引节点的每一个引用, * 如果索引节点不再出现在任何目录中, * 则调用超级块方法 delete_inode 将它从文件系统中删除。 */ void (*drop_inode) (struct inode *); /* 在必须撤消索引节点时调用。删除内存中的 VFS 索引节点和磁盘上的文件数据及元 数据。*/ void (*delete_inode) (struct inode *); /* 释放通过传递的参数指定的超级块对象(因为相应的文件系统被卸载)。 */ void (*put_super) (struct super_block *); /* 用指定对象的内容更新文件系统的超级块。 */ void (*write_super) (struct super_block *); /* 在清除文件系统来更新磁盘上的具体文件系统数据结构时调用(由日志文件系统使 用)。 */ int (*sync_fs)(struct super_block *sb, int wait); /* 阻塞对文件系统的修改并用指定对象的内容更新超级块。 * 当文件系统被冻结时调用该方法,如,由逻辑卷管理器驱动程序( LVM)调用。*/ void (*write_super_lockfs) (struct super_block *); /* 取消由 write_super_lockfs()超级块方法实现的对文件系统更新的阻塞。 */ void (*unlockfs) (struct super_block *); /* 将文件系统的统计信息返回,填写在 buf 缓冲区中。*/ int (*statfs) (struct dentry *, struct kstatfs *); /* 用新的选项重新安装文件系统(当某个安装选项必须被修改时被调用)。 */ int (*remount_fs) (struct super_block *, int *, char *); /* 当撤消磁盘索引节点执行具体文件系统操作时调用。 */ void (*clear_inode) (struct inode *); /* 中断一个安装操作,因为相应的卸载操作已经开始(只在网络文件系统中使用)。 */ void (*umount_begin) (struct vfsmount *, int); /* 用来显示特定文件系统的选项。 */ int (*show_options)(struct seq_file *, struct vfsmount *); /* 用来显示特定文件系统的状态。 */ int (*show_stats)(struct seq_file *, struct vfsmount *); /* 限额系统使用该方法从文件中读取数据,该文件详细说明了所在文件系统的限制。 */ ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t); /* 限额系统使用该方法将数据写入文件中,该文件详细说明了所在文件系统的限制。 */ ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t); #ifndef __GENKSYMS__ //一般情况下用不到最后两个方法 int (*freeze_fs) (struct super_block *); int (*unfreeze_fs) (struct super_block *); #endif }; 每个具体的文件系统都可以定义自己的超级块操作。当 VFS 需要调用其中一个操作时,比 如 read_inode(),它执行下列操作: sb->s_op->read_inode(inode); 这里 sb 存放所涉及超级块对象的地址。 super_operations 表的 read_inode 字段存放这函数的 地址,因此,这一函数被直接调用。 super_operations 中的方法对所有可能的文件系统类型均是可用的。但是,只有其中的一个 子集应用到每个具体的文件系统;未实现的方法对应的字段置为 NULL。 注意,系统没有定义 get_super 方法来读超级块,那么,内核如何能够调用一个对象的方法 而从磁盘读出该对象?我们将在描述文件系统类型的另一个对象中找到等价的 get_sb 方法。 2.1.2 索引节点对象 文件系统处理文件所需要的所有信息都放在一个名为索引节点的数据结构中。文件名可以随 时更改,但是索引节点对文件是唯一的,并且随文件的存在而存在。内存中的索引节点对象 由一个 inode 数据结构组成: struct inode { struct hlist_node i_hash; /* 用于散列链表的指针 */ struct list_head i_list; /* 用于描述索引节点当前状态的链表的指针 */ struct list_head i_sb_list; /* 用于超级块的索引节点链表的指针 */ struct list_head i_dentry; /* 引用索引节点的目录项对象链表的头 */ unsigned long i_ino; /* 索引节点号 */ atomic_t i_count; /* 引用计数器 */ umode_t i_mode; /* 文件类型与访问权限 */ unsigned int i_nlink; /* 硬链接数目 */ uid_t i_uid; /* 所有者标识符 */ gid_t i_gid; /* 所有者组标识符 */ dev_t i_rdev; /* 实设备标识符 */ loff_t i_size; /* 文件的字节数 */ struct timespec i_atime; /* 上次访问文件的时间 */ struct timespec i_mtime; /* 上次写文件的时间 */ struct timespec i_ctime; /* 上次修改索引节点的时间 */ unsigned int i_blkbits; /* 块的位数 */ unsigned long i_version; /* 版本号(每次使用后自动递增) */ blkcnt_t i_blocks; /* 文件的块数 */ unsigned short i_bytes; /* 文件中最后一个块的字节数 */ spinlock_t i_lock; /* 保护索引节点一些字段的自旋锁: i_blocks, i_bytes, maybe i_size */ struct mutex i_mutex; /* 索引节点信号量 */ struct rw_semaphore i_alloc_sem; /* 在直接 I/O 文件操作中避免出现竞争条件的读 /写 信号量 */ struct inode_operations *i_op; /* 索引节点的操作 */ const struct file_operations *i_fop; /* 缺省文件操作: former->i_op->default_file_ops */ struct super_block *i_sb; /* 指向超级块对象的指针 */ struct file_lock *i_flock; /* 指向文件锁链表的指针 */ struct address_space *i_mapping; /* 指向缓存 address_space 对象的指针 */ struct address_space i_data; /* 嵌入在 inode 中的文件的 address_space 对象 */ #ifdef CONFIG_QUOTA struct dquot *i_dquot[MAXQUOTAS]; /* 索引节点磁盘限额 */ #endif struct list_head i_devices; /* 用于具体的字符或块设备索引节点链表的指针 */ union { struct pipe_inode_info *i_pipe; /* 如果文件是一个管道则使用它 */ struct block_device *i_bdev; /* 指向块设备驱动程序的指针 */ struct cdev *i_cdev; /* 指向字符设备驱动程序的指针 */ }; int i_cindex; /* 拥有一组次设备号的设备文件的索引 */ __u32 i_generation; /* 索引节点版本号(由某些文件系统使用) */ #ifdef CONFIG_DNOTIFY unsigned long i_dnotify_mask; /* 目录通知事件的位掩码 */ struct dnotify_struct *i_dnotify; /* 用于目录通知 */ #endif #ifdef CONFIG_INOTIFY struct list_head inotify_watches; /* watches on this inode */ struct mutex inotify_mutex; /* protects the watches list */ #endif unsigned long i_state; /* 索引节点的状态标志 */ unsigned long dirtied_when; /* 索引节点的弄脏时间(以节拍为单位) */ unsigned int i_flags; /* 文件系统的安装标志 */ atomic_t i_writecount; /* 用于写进程的引用计数器 */ void *i_security; /* 指向索引节点安全结构的指针 */ void *i_private; /* 指向私有数据的指针 */ #ifdef __NEED_I_SIZE_ORDERED seqcount_t i_size_seqcount;/* SMP 系统为 i_size 字段获取一致值时使用的顺序计数器 */ #endif }; 每个索引节点对象都会复制磁盘索引节点包含的一些数据,比如分配给文件的磁盘块数。如 果 i_state 字段的值等于 I_DIRTY_SYNC、I_DIRTY_DATASYNC 或 I_DIRTY_PAGES,那么 该索引节点就是“脏”的,也就是说,对应的磁盘索引节点必须被更新。 I_DIRTY 宏可以 用来立即检查这三个标志的值(详细内容参见后面)。 i_state 字段的其他值有 I_LOCK(涉及的索引节点对象处于 I/O 传送中)、 I_FREEING(索 引节点对象正在被释放)、 I_CLEAR(索引节点对象的内容不再有意义)以及 I_NEW(索引 节点对象已经分配但还没有用从磁盘索引节点读取来的数据填充)。 每个索引节点对象总是出现在下列的其中一个双向循环链表的某个链表中(所有情况下,指 向相邻元素的指针存放在 i_list 字段中): (1)有效未使用的索引节点链表。典型的如那些镜像有效的磁盘索引节点,且当前未被任 何进程使用。这些索引节点不为脏,且它们的 i_count 字段置为 0。链表中的首元素和尾元 素是由变量 inode_unused的 next 字段和 prev字段分别指向的。这个链表用作磁盘高速缓存。 (2)正在使用的索引节点链表,也就是那些镜像有效的磁盘索引节点,且当前被某些进程 使用。这些索引节点不为脏,但它们的 i_count 字段为正数。链表中的首元素和尾元素是由 变量 mode_in_use 指向的。 (3)脏索引节点的链表。链表中的首元素和尾元素是由相应超级块对象的 s_dirty 字段引用 的。 此外,每个索引节点对象也包含在每文件系统( per filesystem)的双向循环链表中,链表的 头存放在超级块对象的 s_inodes 字段中;索引节点对象的 i_sb_list 字段存放了指向链表相邻 元素的指针。 最后,索引节点对象也存放在一个称为 inode_hashtable 的散列表中。散列表加快了对索引节 点对象的搜索,前提是系统内核要知道索引节点号及文件所在文件系统对应的超级块对象的 地址。由于散列技术可能引发冲突,所以索引节点对象包含一个 i_hash 字段,该字段中包 含向前和向后的两个指针,分别指向散列到同一地址的前一个索引节点和后一个索引节点; 该字段因此创建了由这些索引节点组成的一个双向链表。 与索引节点对象关联的方法也叫索引节点操作。它们由 inode_operations 结构来描述,该结 构的地址存放在 i_op 字段中: struct inode_operations { /* 在某一目录下,为与目录项对象相关的普通文件创建一个新的磁盘索引节点。 */ int (*create) (struct inode *,struct dentry *,int, struct nameidata *); /* 为包含在一个目录项对象中的文件名对应的索引节点查找目录。 */ struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *); /* 创建一个新的名为 new_dentry 的硬链接,它指向 dir 目录下名为 old_dentry 的文件。 */ int (*link) (struct dentry *,struct inode *,struct dentry *new_dentry); /* 从一个目录中删除目录项对象所指定文件的硬链接。 */ int (*unlink) (struct inode *,struct dentry *); /* 在某个目录下,为与目录项对象相关的符号链接创建一个新的索引节点。 */ int (*symlink) (struct inode *,struct dentry *,const char *); /* 在某个目录下,为与目录项对象相关的目录创建一个新的索引节点。 */ int (*mkdir) (struct inode *,struct dentry *,int); /* 从一个目录删除子目录,子目录的名称包含在目录项对象中。 */ int (*rmdir) (struct inode *,struct dentry *); /* 在某个目录中,为与目录项对象相关的特定文件创建一个新的磁盘索引节点。 * 其中参数 mode 和 rdev 分别表示文件的类型和设备的主次设备号。 */ int (*mknod) (struct inode *,struct dentry *,int,dev_t); /* 将 old_dir 目录下由 old_entry 标识的文件移到 new_dir 目录下。 * 新文件名包含在 new_dentry 指向的目录项对象中。 */ int (*rename) (struct inode *old_dir, struct dentry *old_entry, struct inode *new_dir, struct dentry *new_dentry); /* 将目录项所指定的符号链接中对应的文件路径名拷贝到 buffer 所指定的用户态内存 区。 */ int (*readlink) (struct dentry *, char __user *,int); /* 解析索引节点对象所指定的符号链接;如果该符号链接是一个相对路径名, * 则从第二个参数所指定的目录开始进行查找。 */ void * (*follow_link) (struct dentry *, struct nameidata *); /* 释放由 follow_link 方法分配的用于解析符号链接的所有临时数据结构。 */ void (*put_link) (struct dentry *, struct nameidata *, void *); /* 修改与索引节点相关的文件长度。在调用该方法之前, * 必须将 inode 对象的 i_size 字段设置为需要的新长度值。 */ void (*truncate) (struct inode *); /* 检查是否允许对与索引节点所指的文件进行指定模式的访问。 */ int (*permission) (struct inode *, int, struct nameidata *); /* 在触及索引节点属性后通知一个“修改事件”。 */ int (*setattr) (struct dentry *, struct iattr *); /* 由一些文件系统用于读取索引节点属性。 */ int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *); /* 为索引节点设置“扩展属性”(扩展属性存放在任何索引节点之外的磁盘块中)。 */ int (*setxattr) (struct dentry *, const char *,const void *,size_t,int); /* 获取索引节点的扩展属性。 */ ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t); /* 获取扩展属性名称的整个链表。 */ ssize_t (*listxattr) (struct dentry *, char *, size_t); /* 删除索引节点的扩展属性。 */ int (*removexattr) (struct dentry *, const char *); void (*truncate_range)(struct inode *, loff_t, loff_t); #ifndef __GENKSYMS__ long (*fallocate)(struct inode *inode, int mode, loff_t offset, loff_t len); int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start, u64 len); #endif }; 上述列举的方法对所有可能的索引节点和文件系统类型都是可用的。不过,只有其中的一个 子集应用到某一特定的索引节点和文件系统,未实现的方法对应的字段被置为 NULL。 2.1.3 文件对象 文件对象描述进程怎样与一个打开的文件进行交互。文件对象是在文件被打开时创建的,由 一个 file 结构组成。注意,文件对象在磁盘上没有 对应的映像,因此 file 结构中没有设置“脏”字段来表示文件对象是否已被修改: struct file { /* * fu_list becomes invalid after file_free is called and queued via * fu_rcuhead for RCU freeing */ union { struct list_head fu_list; struct rcu_head fu_rcuhead; } f_u; /* 用于通用文件对象链表的指针 */ struct dentry *f_dentry; /* 与文件相关的目录项对象 */ struct vfsmount *f_vfsmnt; /* 含有该文件的已安装文件系统 */ const struct file_operations *f_op; /* 指向文件操作表的指针 */ atomic_t f_count; /* 文件对象的引用计数器 */ unsigned int f_flags; /* 当打开文件时所指定的标志 */ mode_t f_mode; /* 进程的访问模式 */ loff_t f_pos; /* 当前的文件位移量(文件指针) */ struct fown_struct f_owner; /* 通过信号进行 I/O 事件通知的数据 */ unsigned int f_uid, f_gid; /* 用户的 UID、GID */ struct file_ra_state f_ra; /* 文件预读状态 */ unsigned long f_version; /* 版本号,每次使用后自动递增 */ void *f_security; /* 指向文件对象的安全结构的指针 */ /* needed for tty driver, and maybe others 指向特定文件系统或设备驱动程序所需的数据的指针 */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct list_head f_ep_links; /* 文件的事件轮询等待者链表的头 */ spinlock_t f_ep_lock; /* 保护 f_ep_links 链表的自旋锁 */ #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; /* 指向文件地址空间对象的指针 */ }; 存放在文件对象中的主要信息是文件指针 f_pos,即文件中当前的位置,下一个操作将在该 位置发生。由于几个进程可能同时访问同一文件,因此文件指针必须存放在文件对象而不是 索引节点对象中。 文件对象通过一个名为 filp的 slab 高速缓存分配, filp描述符地址存放在 file_cachep变量中。 Linux 对分配的文件对象数目是有限制的,因此 files_stat 变量在其 max_files 字段中指定了 可分配文件对象的最大数目,也就是系统可同时访问的最大文件数。 在使用文件对象包含在由具体文件系统的超级块所确立的几个链表中。每个超级块对象把文 件对象链表的头存放在 s_files 字段中;因此,属于不同文件系统的文件对象就包含在不同 的链表中。链表中分别指向前一个元素和后一个元素的指针都存放在文件对象的 f_list 字段 中。 files_lock 自旋锁保护超级块的 s_files 链表免受多处理器系统上的同时访问。 文件对象的 f_count 字段是一个引用计数器:它记录使用文件对象的进程数(记住,以 CLONE_FILES 标志创建的轻量级进程共享打开文件表,因此它们可以使用相同的文件对 象)。当内核本身使用该文件对象时也要增加计数器的值——例如,把对象插入链表中或发 出 dup()系统调用时。 当 VFS 代表进程必须打开一个文件时,它调用 get_empty_filp()函数来分配一个新的文件对 象。该函数调用 kmem_cache_alloc()从 filp 高速缓存中获得一个空闲的文件对像,然后初始 化这个对象的字段,如下所示: f = kmem_cache_alloc(filp_cachep, GFP_KERNEL); percpu_counter_inc(&nr_files); memset(f, 0, sizeof(*f)); tsk = current; INIT_LIST_HEAD(&f->f_u.fu_list); atomic_set(&f->f_count, 1); rwlock_init(&f->f_owner.lock); f->f_uid = tsk->fsuid; f->f_gid = tsk->fsgid; 每个文件系统都有其自己的文件操作集合,执行诸如读写文件这样的操作。当内核将一个索 引节点从磁盘装入内存时,就会把指向这些文件操作的指针存放在 file_operations 结构中, 而该结构的地址存放在该索引节点对象的 i_fop 字段中。当进程打开这个文件时, VFS 就用 存放在索引节点中的这个地址初始化新文件对象的 fop 字段,使得对文件操作的后续调用能 够使用这些函数。如果需要, VFS 随后也可以通过在 f_op 字段存放一个新值而修改文件操 作的集合: struct file_operations { /* 指向一个模块的拥有者,该字段主要应用于那些有模块产生的文件系统 */ struct module *owner; /* 更新文件指针。 loff_t (*llseek) (struct file *, loff_t, int); /* 从文件的*offset 处开始读出 count 个字节;然后增加 *offset 的值(一般与文件指针对 应)。 */ ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); /* 启动一个异步 I/O 操作,从文件的 pos 处开始读出 len 个字节的数据并将它们放入 buf 中 * (引入它是为了支持 io_submit()系统调用)。 */ ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t); /* 从文件的*offset 处开始写入 count 个字节,然后增加 *offset 的值(一般与文件指针对 应)。 */ ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); /* 启动一个异步 I/O 操作,从 buf 中取 len 个字节写入文件 pos 处。 */ ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t); /* 返回一个目录的下一个目录项,返回值存人参数 dirent; * 参数 filldir 存放一个辅助函数的地址,该函数可以提取目录项的各个字段。 */ int (*readdir) (struct file *, void *, filldir_t); /* 检查是否在一个文件上有操作发生,如果没有则睡眠,直到该文件上有操作发生。 */ unsigned int (*poll) (struct file *, struct poll_table_struct *); /* 向一个基本硬件设备发送命令。该方法只适用于设备文件。 */ int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); /* 与 ioctl 方法类似,但是它不用获得大内核锁。 * 我们认为所有的设备驱动程序和文件系统都将使用这个新方法而不是 loctl 方法。 */ long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* 64 位的内核使用该方法执行 32 位的系统调用 ioctl()。 */ long (*compat_ioctl) (struct file *, unsigned int, unsigned long); /* 执行文件的内存映射,并将映射放入进程的地址空间。 */ int (*mmap) (struct file *, struct vm_area_struct *); /* 通过创建一个新的文件对象而打开一个文件,并把它链接到相应的索引节点对象。 */ int (*open) (struct inode *, struct file *); /* 当打开文件的引用被关闭时调用该方法。该方法的实际用途取决于文件系统。 */ int (*flush) (struct file *, fl_owner_t id); /* 释放文件对象。当打开文件的最后一个引用被关闭时(即文件对象 f_count 字段的值 变为 0 时)调用该方法。 */ int (*release) (struct inode *, struct file *); /* 将文件所缓存的全部数据写入磁盘。 */ int (*fsync) (struct file *, struct dentry *, int datasync); /* 启动一次异步 I/O 刷新操作。 */ int (*aio_fsync) (struct kiocb *, int datasync); /* 通过信号来启用或禁止 I/O 事件通告。 */ int (*fasync) (int, struct file *, int); /* 对 file 文件申请一个锁。 */ int (*lock) (struct file *, int, struct file_lock *); /* 从文件中读字节,并把结果放入 vector 描述的缓冲区中;缓冲区的个数由 count 指定。 */ ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); /* 把 vector 描述的缓冲区中的字节写人文件;缓冲区的个数由 count 指定。 */ ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); /* 把数据从 in_file 传送到 out_file(引人它是为了支持 sendfile()系统调用)。 */ ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *); /* 把数据从文件传送到页高速缓存的页;这个低层方法由 sendfile()和用于套接字的网 络代码使用。 */ ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /* 获得一个未用的地址范围来映射文件。 */ unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /* 当设置文件的状态标志( F_SETFL 命令)时, * fcntl()系统调用的服务例程调用该方法执行附加的检查。当前只适用于 NFS 网络文 件系统。 */ int (*check_flags)(int); /* 当建立一个目录更改通告( F_NOTIFY 命令)时, * 由 fcntl()系统调用的服务例程调用该方法。当前只适用于 CIFS(Common Internet File * system,公用互联网文件系统 * )网络文件系统。 */ int (*dir_notify)(struct file *filp, unsigned long arg); /* 用于定制 flock()系统调用的行为。官方 Linux 文件系统不使用该方法。 int (*flock) (struct file *, int, struct file_lock *); */ ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); }; 以上描述的方法对所有可能的文件类型都是可用的。不过,对于一个具体的文件类型,只使 用其中的一个子集;那些未实现的方法对应的字段被置为 NULL。 2.1.4 目录项对象 VFS 把每个目录看作由若干子目录和文件组成的一个普通文件。然而目录项不同,一旦目 录项被读人内存, VFS 就把它转换成基于 dentry 结构的一个目录项对象。对于进程查找的 路径名中的每个分量,内核都为其创建一个目录项对象;目录项对象将每个分量与其对应的 索引节点相联系。例如,在查找路名 /tmp/test 时,内核为根目录“ /“创建一个目录项对象, 为根目录下的 tmp 项创建一个第二级目录项对象,为 /tmp 目录下的 test 项创建一个第三级目 录项对象。 请注意,目录项对象在磁盘上并没有对应的映像,因此在 dentry 结构中不包含指出该对象 已被修改的字段。目录项对象存放在名为 dentry_cache 的 slab 分配器高速缓存中。因此,目 录项对象的创建和删除是通过调用 kmem_cache_alloc()和 kmem_cache_free()实现的。 //include/linux/Dcache.h struct dentry { atomic_t d_count; /* 目录项对象引用计数器 */ unsigned int d_flags; /* 目录项高速缓存标志 */ spinlock_t d_lock; /* 保护目录项对象的自旋锁 */ struct inode *d_inode; /* 与文件名关联的索引节点 */ /* * The next three fields are touched by __d_lookup. Place them here * so they all fit in a cache line. */ struct hlist_node d_hash; /* 指向散列表表项链表的指针 */ struct dentry *d_parent; /* 父目录的目录项对象 */ struct qstr d_name; /* 文件名 */ struct list_head d_lru; /* 用于未使用目录项链表的指针 */ /* * d_child and d_rcu can share memory */ union { struct list_head d_child; /* 对目录而言,用于同一父目录中的目录项链表的指针 */ struct rcu_head d_rcu; /* 回收目录项对象时,由 RCU 描述符使用 */ } d_u; struct list_head d_subdirs; /* 对目录而言,子目录项链表的头 */ struct list_head d_alias; /* 用于与同一索引节点(别名)相关的目录项链表的指针 */ unsigned long d_time; /* 由 d_revalidate 方法使用 */ struct dentry_operations *d_op; /* 目录项方法 */ struct super_block *d_sb; /* 文件的超级块对象 */ void *d_fsdata; /* 依赖于文件系统的数据 */ void *d_extra_attributes; /* TUX-specific data */ #ifdef CONFIG_PROFILING struct dcookie_struct *d_cookie; /* cookie,指向内核配置文件使用的数据结构的指针 */ #endif int d_mounted; /* 对目录而言,用于记录安装该目录项的文件系统 数的计数器 */ unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 存放短文件名的空间 */ }; 每个目录项对象可以处于以下四种状态之一: 空闲状态(free):处于该状态的目录项对象不包括有效的信息,且还没有被 VFS 使用。对 应的内存区由 slab 分配器进行处理。 未使用状态(unused):处于该状态的目录项对象当前还没有被内核使用。该对象的引用计 数器 d_count 的值为 0,但其 d_inode 字段仍然指向关联的索引节点。该目录项对象包含有 效的信息,但为了在必要时回收内存,它的内容可能被丢弃。 正在使用状态( in use):处于该状态的目录项对象当前正在被内核使用。该对象的引用计数 器 d_count 的值为正数,其 d_inode 字段指向关联的索引节点对象。该目录项对象包含有效 的信息,并且不能被丢弃。 负状态( negative):与目录项关联的索引节点不复存在,那是因为相应的磁盘索引节点已被删除,或者因为目录项对象是通过解析一个不存在文件的路径名创建的。目录项对象的 d_inode 字段被置为 NULL,但该对象仍然被保存在目录项高速缓存中,以便后续对同一文 件目录名的查找操作能够快速完成。术语“负状态”容易使人误解,因为根本不涉及任何负 值。 与目录项对象关联的方法称为目录项操作。这些方法由 dentry_operations 结构加以描述,该 结构的地址存放在目录项对象的 d_op 字段中。尽管一些文件系统定义了它们自己的目录项 方法,但是这些字段通常为 NULL,而 VFS 使用缺省函数代替这些方法: struct dentry_operations { /* 在把目录项对象转换为一个文件路径名之前,判定该目录项对象是否仍然有效。 * 缺省的 VFS 函数什么也不做,而网络文件系统可以指定自己的函数。 */ int (*d_revalidate)(struct dentry *, struct nameidata *); /* 生成一个散列值;这是用于目录项散列表的、特定干具体文件系统的散列函数。 * 参数 dentry 标识包含路径分量的目录。参数 name 指向一个结构, * 该结构包含要查找的路径名分量以及由散列函数生成的散列值。 */ int (*d_hash) (struct dentry *, struct qstr *); /* 比较两个文件名。 name1 应该属于 dir 所指的目录。 * 缺省的 VFS 函数是常用的字符串匹配函数。 * 不过,每个文件系统可用自己的方式实现这一方法。 * 例如,MS.DOS 文件系统不区分大写和小写字母。 */ int (*d_compare) (struct dentry *, struct qstr *, struct qstr *); /* 当对目录项对象的最后一个引用被删除( d_count 变为“0”)时, * 调用该方法。缺省的 VFS 函数什么也不做。 */ int (*d_delete)(struct dentry *); /* 当要释放一个目录项对象时(放入 slab 分配器),调用该方法。 * 缺省的 VFS 函数什么也不做。 */ void (*d_release)(struct dentry *); /* 当一个目录项对象变为“负”状态(即丢弃它的索引节点)时,调用该方法。 * 缺省的 VFS 函数调用 iput()释放索引节点对象。 */ void (*d_iput)(struct dentry *, struct inode *); }; 2.2 把 Linux 中的 VFS 对象串联起来 上一节讲到了内核提供的通用虚拟文件模型中四大数据结构极其操作,那么,为了使各种文 件系统和谐相处,这些对象又是怎么串联起来的呢。我们就重点讨论他们怎样与内核交互, 包括如何与进程打交道,以及介绍一些相关的缓存机制。 2.2.1 与进程相关的文件 首先,文件必须由进程打开,每个进程都有它自己当前的工作目录和它自己的根目录。这仅 仅是内核用来表示进程与文件系统相互作用所必须维护的数据中的两个例子。类型为 fs_struct 的整个数据结构就用于此目的,且每个进程描述符 task_struct 的 fs 字段就指向进程 的 fs_struct 结构: struct fs_struct { atomic_t count; rwlock_t lock; int umask; struct dentry * root, * pwd, * altroot; struct vfsmount * rootmnt, * pwdmnt, * altrootmnt; }; 其中: count:共享这个表的进程个数 lock:用于表中字段的读/写自旋锁 umask:当打开文件设置文件权限时所使用的位掩码 root:根目录的目录项 pwd:当前工作目录的目录项 altroot:模拟根目录的目录项(在 80x86 结构上始终为 NULL) rootmnt:根目录所安装的文件系统对象 pwdmnt:当前工作目录所安装的文件系统对象 altrootmnt:模拟根目录所安装的文件系统对象(在 80x86 结构上始终为 NULL) 第二个表表示进程当前打开的文件,表的地址存放于进程描述符 task_struct 的 files 字段。 该表的类型为 files_struct 结构: struct files_struct { atomic_t count; struct fdtable *fdt; struct fdtable fdtab; spinlock_t file_lock ____cacheline_aligned_in_smp; int next_fd; struct embedded_fd_set close_on_exec_init; struct embedded_fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; }; struct fdtable { unsigned int max_fds; int max_fdset; struct file ** fd; /* current fd array */ fd_set *close_on_exec; fd_set *open_fds; struct rcu_head rcu; struct files_struct *free_files; struct fdtable *next; }; #define NR_OPEN_DEFAULT BITS_PER_LONG #define BITS_PER_LONG 32 /* asm-i386 */ fdtable 结构嵌入在 files_struct 中,并且由它的 fdt 指向。 fdtable 结构的 fd 字段指向文件对象的指针数组。该数组的长度存放在 max_fds 字段中。通 常,fd 字段指向 files_struct 结构的 fd_array 字段,该字段包括 32 个文件对象指针。如果进 程打开的文件数目多于 32,内核就分配一个新的、更大的文件指针数组,并将其地址存放 在 fd 字段中,内核同时也更新 max_fds 字段的值,如图所示: 对于在 fd 数组中所有元素的每个文件来说,数组的索引就是文件描述符 (file descriptor)。通 常,数组的第一个元素(索引为 0)是进程的标准输入文件,数组的第二个元素(索引为 1) 是进程的标准输出文件,数组的第三个元素(索引为 2)是进程的标准错误文件。请注意, 借助于 dup()、dup2()和 fcntl()系统调用,两个文件描述符可以指向同一个打开的文件,也就 是说,数组的两个元素可能指向同一个文件对象。当用户使用 shell 结构(如 2>&1)将标准 错误文件重定向到标准输出文件上时,用户也能看到这一点。 进程不能使用多于 NR_OPEN(通常为 1 048 576)个文件描述符。内核也在进程描述符的 signal->rlim[RLIMIT_NOFILE]结构上强制动态限制文件描述符的最大数;这个值通常为 1024,但是如果进程具有超级用户特权,就可以增大这个值。 open_fds 字段最初包含 open_fds_init 字段的地址,open_fds_init 字段表示当前已打开文件的 文件描述符的位图。 max_fdset 字段存放位图中的位数。由于 fd_set 数据结构有 1024 位,所以通常不需要扩大位图的大小。不过,如果确有必要的话,内核仍能动态增加位图的大小, 这非常类似于文件对象的数组的情形。 当内核开始使用一个文件对象时,内核提供 fget()函数以供调用。这个函数接收文件描述符 fd 作为参数,返回在 current->files->fd [fd]中的地址,即对应文件对象的地址,如果没有任 何文件与 fd 对应,则返回 NULL。在第一种情况下,fget()使文件对象引用计数器 fcount 的 值增 1: struct file fastcall *fget(unsigned int fd) { struct file *file; struct files_struct *files = current->files; rcu_read_lock(); file = fcheck_files(files, fd); if (file) { if (!atomic_inc_not_zero(&file->f_count)) { /* File object ref couldn't be taken */ rcu_read_unlock(); return NULL; } } rcu_read_unlock(); return file; } static inline struct file * fcheck_files(struct files_struct *files, unsigned int fd) { struct file * file = NULL; /* 不考虑 RCU 机制,files_fdtable 宏就是返回 files->fdt */ struct fdtable *fdt = files_fdtable(files); if (fd < fdt->max_fds) file = rcu_dereference(fdt->fd[fd]); return file; } 当内核控制路径完成对文件对象的使用时,调用内核提供的 fput()函数。该函数将文件对象 的地址作为参数,并减少文件对象引用计数器 f_count 的值。另外,如果这个字段变为 0, 该函数就调用文件操作的 release 方法(如果已定义),减少索引节对象的 i_writecount 字段 的值(如果该文件是可写的),将文件对象从超级块链表中移走,释放文件对象给 slab 分配 器,最后减少相关的文件系统描述符的目录项对象的引用计数器的值: void fastcall fput(struct file *file) { if (atomic_dec_and_test(&file->f_count)) __fput(file); } void fastcall __fput(struct file *file) { struct dentry *dentry = file->f_dentry; struct vfsmount *mnt = file->f_vfsmnt; struct inode *inode = dentry->d_inode; might_sleep(); fsnotify_close(file); /* * The function eventpoll_release() should be the first called * in the file cleanup chain. */ eventpoll_release(file); locks_remove_flock(file); if (file->f_op && file->f_op->release) file->f_op->release(inode, file); security_file_free(file); if (unlikely(S_ISCHR(inode->i_mode) && inode->i_cdev != NULL)) cdev_put(inode->i_cdev); fops_put(file->f_op); if (file->f_mode & FMODE_WRITE) put_write_access(inode); file_kill(file); file->f_dentry = NULL; file->f_vfsmnt = NULL; file_free(file); dput(dentry); mntput(mnt); } fget_light()和 fget_light()函数是 fget()和 fput()的快速版本:内核要使用它们,前提是能够安 全地假设当前进程已经拥有文件对象,即进程先前已经增加了文件对象引用计数器的值。例 如,它们由接收一个文件描述符作为参数的系统调用服务例程使用,这是由于先前的 open() 系统调用已经增加了文件对象引用计数器的值。 2.2.2 索引节点高速缓存 VFS 用了一个高速缓存来加快对索引节点的访问,和我们以后将会谈到的页高速缓存不同 的一点是,每个缓冲区不用再分为两个部分了,因为 inode 结构中已经有了类似于块高速缓 存中缓冲区首部的域。索引节点高速缓存的实现代码全部在 fs/inode.c,这部分代码并没有 随着内核版本的变化做很多的修改。 每个索引节点可能处于哈希表中,也可能同时处于下列“类型”链表的一种中: � "in_use" - 有效的索引节点,即 i_count > 0 且 i_nlink > 0(参看前面的 inode 结构) � "dirty" - 类似于 "in_use" ,但还“脏” � "unused" - 有效的索引节点但还没使用,即 i_count = 0。 这几个链表定义如下: static LIST_HEAD(inode_in_use); static LIST_HEAD(inode_unused); static struct hlist_head *inode_hashtable; static LIST_HEAD(anon_hash_chain); /* for inodes with NULL i_sb */ 因此,索引节点高速缓存的结构概述如下: � 全局哈希表 inode_hashtable,其中哈希值是根据每个超级块指针的值和 32 位索引节点号 而得。对没有超级块的索引节点( inode->i_sb == NULL),则将其加入到 anon_hash_chain 链表的首部。我们通过 insert_inode_hash 函数将一个 inode 结构插入到这个散列表中。 � 正在使用的索引节点链表。全局变量 inode_in_use 指向该链表中的首元素和尾元素。一 般通过 new_inode 函数新分配的索引节点就加入到这个链表中。 � 未用索引节点链表。全局变量 inode_unused 的 next 域和 prev 域分别指向该链表中的首 元素和尾元素。 � 脏索引节点链表。由相应超级块的 s_dirty 域指向该链表中的首元素和尾元素。 � 对 inode 对象的缓存,定义如下: static kmem_cache_t * inode_cachep,这是一个 Slab 缓 存,用于分配和释放索引节点对象。 如上图所示,索引节点的 i_hash 域指向哈希表,i_list 指向 in_use、unused 或 dirty 某个链 表。所有这些链表都受单个自旋锁 inode_lock 的保护。索引节点高速缓存的初始化是由 inode_init()实现的,而这个函数是在系统启动时由 init/main.c 中的 start_kernel()函数调用的。 inode_init(unsigned long mempages)只有一个参数,表示索引节点高速缓存所使用的物理页面 数。因此,索引节点高速缓存可以根据可用物理内存的大小来进行配置,例如,如果物理内 存足够大的话,就可以创建一个大的哈希表。 索引节点状态的信息存放在数据结构 inodes_stat_t 中,在 linux/Fs.h 中定义如下: struct inodes_stat_t { int nr_inodes; int nr_unused; int dummy[5]; }; extern struct inodes_stat_t inodes_stat 用户程序可以通过 /proc/sys/fs/inode-nr 和 /proc/sys/fs/inode-state 获得索引节点高速缓存中 索引节点总数及未用索引节点数。 2.2.3 目录项高速缓存 由于从磁盘读入一个目录项并构造相应的目录项对象需要花费大量的时间,所以在完成对目 录项对象的操作后,可能后面还要使用它,因此,跟上面的索引节点一样,在内存中保留它 有重要的意义。 为了最大限度地提高处理这些目录项对象的效率, Linux 使用目录项高速缓存,它由两种类 型的数据结构组成: - 一个处于正在使用、未使用或负状态的目录项对象的集合。 - 一个散列表,从中能够快速获取与给定的文件名和目录名对应的目录项对象。同样,如果 访问的对象不在目录项高速缓存中,则散列函数返回一个空值。 如图所示: 所有“未使用”目录项对象都存放在一个“最近最少使用( Least Recently used,LRU)”的 双向链表中,该链表按照插入的时间排序。换句话说,最后释放的目录项对象放在链表的首 部,所以最近最少使用的目录项对象总是靠近链表的尾部。一旦目录项高速缓存的空间开始 变小,内核就从链表的尾部删除元素,使得最近最常使用的对象得以保留。 LRU 链表的首 元素和尾元素的地址存放在 list_head 类型的 dentry_unused 变量的 next 字段和 prev 字段中。 目录项对象的 d_lru 字段包含指向链表中相邻目录项的指针。 每个“正在使用”的目录项对象都被插入一个双向链表中,该链表由相应索引节点对象的 i_dentry 字段所指向(由于每个索引节点可能与若干硬链接关联,所以需要一个链表)。目 录项对象的 d_alias 字段存放链表中相邻元素的地址。这两个字段的类型都是 struct list_head。 当指向相应文件的最后一个硬链接被删除后,一个“正在使用“的目录项对象可能变成“负” 状态。在这种情况下,该目录项对象被移到“未使用”目录项对象组成的 LRU 链表中。每 当内核缩减目录项高速缓存时,“负”状态目录项对象就朝着 LRU 链表的尾部移动,这样一 来,这些对象就逐渐被释放。 散列表是由 dentry_hashtable 数组实现的。数组中的每个元素是一个指向链表的指针,这种链表就是把具有相同散列表值的目录项进行散列而形成的。该数组的长度取决于系统已安装 RAM 的数量;缺省值是每兆字节 RAM 包含 256 个元素。目录项对象的 d_hash 字段包含指 向具有相同散列值的链表中的相邻元素。散列函数产生的值是由目录的目录项对象及文件名 计算出来的。 dcache_lock 自旋锁保护目录项高速缓存数据结构免受多处理器系统上的同时访问。 d_lookup()函数在散列表中查找给定的父目录项对象和文件名;为了避免发生竞争,使用顺 序锁( seqlock)。__d_lookup()函数与之类似,不过它假定不会发生竞争,因此不使用顺序锁。 2.2.4 VFS 对象的具体实现 假设具体的文件系统是 ext2,那么,执行 mount -t ext2 /dev/sda2 /mnt/test 命令后,将会调用 do_mount()函数。该函数执行文件系统安装的实务操作,具体的代码分析请查看“文件系统 安装”章节,这里我们只做一个简单的介绍一下: do_mount()函数最后会走到 vfs_kern_mount 函数,该函数调用依赖具体文件系统的 get_sb 方法: static struct file_system_type ext2_fs_type = { .owner = THIS_MODULE, .name = "ext2", .get_sb = ext2_get_sb, .kill_sb = kill_block_super, .fs_flags = FS_REQUIRES_DEV | FS_HAS_FIEMAP, }; 以上是 ext2 具体文件系统的文件系统类型描述符,至于文件类型的定义,请看 “文件系统 注册”一节。我们看到, ext2 文件系统 get_sb 的具体方法是 ext2_get_sb,这个函数其实只 有一行代码: return get_sb_bdev(fs_type, flags, dev_name, data, ext2_fill_super, mnt); get_sb_bdev 函数打开传进来的设备文件名,也就是前面 mount 命令中的/dev/sda2;获得该 已注册文件系统的空闲超级快对象;然后调用当做参数传递进来的 ext2_fill_super 函数将 Ext2 磁盘上的超级块的一些信息读入内存中。具体的实现细节我们会在 “Ext2 的超级块对 象”中讨论,这里只把其中关键的一步提出来: sb->s_op = &ext2_sops; sb->s_export_op = &ext2_export_ops; sb->s_xattr = ext2_xattr_handlers; root = iget(sb, EXT2_ROOT_INO); sb->s_root = d_alloc_root(root); 于是,当 ext2_fill_super 函数返回后, /dev/sda2 对应的那个超级快的 s_op 字段就被赋予了以 下数据结构: static struct super_operations ext2_sops = { .alloc_inode = ext2_alloc_inode, .destroy_inode = ext2_destroy_inode, .read_inode = ext2_read_inode, .write_inode = ext2_write_inode, .put_inode = ext2_put_inode, .delete_inode = ext2_delete_inode, .put_super = ext2_put_super, .write_super = ext2_write_super, .statfs = ext2_statfs, .remount_fs = ext2_remount, .clear_inode = ext2_clear_inode, .show_options = ext2_show_options, #ifdef CONFIG_QUOTA .quota_read = ext2_quota_read, .quota_write = ext2_quota_write, #endif }; 当然,最后 get_sb_bdev 返回后,那个超级快就以 s_instances 为头加入了 ext2 文件系统对应 的那个 file_system_type 的 fs_supers 字段链表中。 为了帮助大家理解索引节点高速缓存如何帮助一个具体的文件系统工作的,我们再来研究一 下在打开 Ext2 文件系统的一个常规文件时,相应索引节点的作用。比如: fd = open("file", O_RDONLY); close(fd); open()系统调用是由 fs/open.c 中的 sys_open 函数实现的,而真正的工作是由 fs/open.c 中的 do_filp_open()函数完成的,do_filp_open()函数的具体实现只要依赖一种叫做 nameidata 的数 据结构。 这个数据结构是临时性的,其中,我们主要关注它的 dentry 和 mnt 域。dentry 结构,目录项 对象,我们已经在前面介绍过;而 vfsmount 结构记录着所属文件系统的安装信息,例如文 件系统的安装点、文件系统的根节点等,后面章节我们会详细探讨。 do_filp_open()的相关代码我们将会在“VFS 系统调用的实现”章节中详细分析,这里只讨 论它主要调用的两个函数: (1) open_namei():填充目标文件所在目录的 dentry 结构和所在文件系统的 vfsmount 结构, 并将信息保存在 nameidata 结构中。在 dentry 结构中 dentry->d_inode 就指向目标文件的索引 节点。这个函数比较复杂和庞大,在后面的章节会详细介绍。 (2) dentry_open():建立目标文件的一个“上下文”,即 file 数据结构,并让它与当前进程 的 task_strrct 结构挂上钩。同时,在这个函数中,调用了具体文件系统的打开函数,即f_op->open()。该函数返回指向新建立的 file 结构的指针,为了突出重点,这里也暂不详细 分析这个函数,后面的章节会讨论到的。 我们在前面看到 ext2_fill_super 函数中,当初始化超级快后,有一步是调用 iget 将索引节点 号为 EXT2_ROOT_INO(一般为 2)的索引节点分配给该 ext2 磁盘分区的根目录项。 static inline struct inode *iget(struct super_block *sb, unsigned long ino) { struct inode *inode = iget_locked(sb, ino); if (inode && (inode->i_state & I_NEW)) { sb->s_op->read_inode(inode); unlock_new_inode(inode); } return inode; } ext2_read_inode 就是 ext2 超级块的 s_op->read_inode 具体实现函数,该函数会调用 ext2_get_inode 函数,从一个页高速缓存中读入一个磁盘索引节点结构 ext2_inode,然后初 始化 VFS 的 inode。其中,最重要的初始化代码我们提取一段,如下: if (S_ISREG(inode->i_mode)) { /* 普通文件操作 */ inode->i_op = &ext2_file_inode_operations; if (ext2_use_xip(inode->i_sb)) { inode->i_mapping->a_ops = &ext2_aops_xip; inode->i_fop = &ext2_xip_file_operations; } else if (test_opt(inode->i_sb, NOBH)) { /* 不启动页高速缓存 */ inode->i_mapping->a_ops = &ext2_nobh_aops; inode->i_fop = &ext2_file_operations; } else { inode->i_mapping->a_ops = &ext2_aops; inode->i_fop = &ext2_file_operations; } } else if (S_ISDIR(inode->i_mode)) {/* 目录文件操作 */ inode->i_op = &ext2_dir_inode_operations; inode->i_fop = &ext2_dir_operations; if (test_opt(inode->i_sb, NOBH)) inode->i_mapping->a_ops = &ext2_nobh_aops; else inode->i_mapping->a_ops = &ext2_aops; } else if (S_ISLNK(inode->i_mode)) {/* 符号链接文件操作 */ if (ext2_inode_is_fast_symlink(inode)) inode->i_op = &ext2_fast_symlink_inode_operations; else { inode->i_op = &ext2_symlink_inode_operations; if (test_opt(inode->i_sb, NOBH)) inode->i_mapping->a_ops = &ext2_nobh_aops; else inode->i_mapping->a_ops = &ext2_aops; } } else {/* 其他特殊文件文件操作 */ inode->i_op = &ext2_special_inode_operations; if (raw_inode->i_block[0]) init_special_inode(inode, inode->i_mode, old_decode_dev(le32_to_cpu(raw_inode->i_block[0]))); else init_special_inode(inode, inode->i_mode, new_decode_dev(le32_to_cpu(raw_inode->i_block[1]))); } 当然,我们只关注最一般的情况咯,即普通文件在启用高速缓存时,并且不使用 xip 时的相 关操作: (1)普通文件索引节点操作: struct inode_operations ext2_file_inode_operations = { .truncate = ext2_truncate, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif .setattr = ext2_setattr, .permission = ext2_permission, .fiemap = ext2_fiemap, }; (2)普通文件操作 const struct file_operations ext2_file_operations = { .llseek = generic_file_llseek, .read = generic_file_read, .write = generic_file_write, .aio_read = generic_file_aio_read, .aio_write = generic_file_aio_write, .ioctl = ext2_ioctl, .mmap = generic_file_mmap, .open = generic_file_open, .release = ext2_release_file, .fsync = ext2_sync_file, .readv = generic_file_readv, .writev = generic_file_writev, .sendfile = generic_file_sendfile, .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, }; (3)普通文件页高速缓存操作: const struct address_space_operations ext2_aops = { .readpage = ext2_readpage, .readpages = ext2_readpages, .writepage = ext2_writepage, .sync_page = block_sync_page, .prepare_write = ext2_prepare_write, .commit_write = generic_commit_write, .bmap = ext2_bmap, .direct_IO = ext2_direct_IO, .writepages = ext2_writepages, .migratepage = buffer_migrate_page, }; 同样,在 open_namei()函数中,通过 path_lookup()跟对应的目录项高速缓存打交道获得父目 录项,而 path_lookup()又调用父索引节点的 inode_operations->lookup()方法,也就是我们的 ext2_lookup;该方法从磁盘找到并读入当前节点的目录项,然后通过 iget(sb, ino),根据索 引节点号从磁盘读入相应索引节点并在内存建立起相应的 inode 结构,这就到了我们讨论过 的索引节点高速缓存。 path_lookup 是 VFS 系统最重要的函数之一,我们会在“路径名查找” 一节详细讨论。 如果在访问模式标志中设置了 O_CREAT,则以 LOOKUP_PARENT、LOOKUP_OPEN 和 LOOKUP_CREATE 标志的设置开始查找操作。一旦 path_lookup()函数成功返回,则检查请 求的文件是否已存在。如果不存在,则调用父索引节点的 create 方法,也就是 ext2_create 分配一个新的磁盘索引节点。 当索引节点读入内存后,通过调用 d_add(dentry, inode),就将 dentry 结构和 inode 结构之间 的链接关系建立起来。两个数据结构之间的联系是双向的。一方面, dentry 结构中的指针 d_inode 指向 inode 结构,这是一对一的关系,因为一个目录项只对应着一个文件。反之则 不然,同一个文件可以有多个不同的文件名或路径(通过系统调用 link()建立,注意与符号 连接的区别,那是由 symlink()系统调用建立的),所以从 inode 结构到 dentry 结构的方向是 一对多的关系。因此, inode 结构的 i_dentry 是链表, dentry 结构通过其队列头部 d_alias 挂入相应 inode 结构的 i_dentry 队列中。 当 dentry_open 返回后,open 系统调用也就到了尾声了,这时候,几乎所有的 VFS 对象都串 联成了一个“小团队”。 最后,我们用一个大图描述 VFS 对象串联起来后的场景,并结束本节: 2.3 文件系统的注册与安装 前面主要把 VFS 对象,以及他们如何串联起来的内容介绍得差不多了。相信走大现在,大 家对大概的 VFS 工作机制有了相应的了解,接下来要做的事,就是把那些重要的细节描述 清楚。首先, Linux 内核支持很多不同的文件系统类型。本节,我们将讨论文件系统注册— —也就是通常在系统初始化期间并且在使用文件系统类型之前必须执行的基本操作。一旦文 件系统被注册,其特定的那些虚拟文件模型中的对象中的函数对内核就是可用的了,因此文 件系统类型可以安装在系统的目录树上。 接下来,我们再介绍一些特殊的文件系统类型,它们在 Linux 内核的内部设计中具有非常重 要的作用。 2.3.1 文件系统类型注册 通常,用户在为自己的系统编译内核时可以把 Linux 配置为能够识别所有需要的文件系统。 但是,文件系统的源代码实际上要么包含在内核映像中,要么作为一个模块被动态装入。 VFS 必须对代码目前已在内核中的所有文件系统的类型进行跟踪。这就是通过进行文件系 统类型注册来实现的。 每个注册的文件系统都用一个类型为 file_system_type 的对象来表示: struct file_system_type { const char *name; /* 文件系统名 */ int fs_flags; /* 文件系统类型标志 */ int (*get_sb) (struct file_system_type *, int, /* 读超级块的方法 */ const char *, void *, struct vfsmount *); void (*kill_sb) (struct super_block *); /* 删除超级块的方法 */ struct module *owner; /* 指向实现文件系统的模块的指针 */ struct file_system_type * next; /* 指向文件系统类型链表中下一个元素的指针 */ struct list_head fs_supers; /* 具有相同文件系统类型的超级块对象链表的头 */ struct lock_class_key s_lock_key; struct lock_class_key s_umount_key; }; 所有文件系统类型的对象都插入到一个单向链表中。由变量 file_systems 指向链表的第一个 元素,而结构中的 next 字段指向链表的下一个元素: static struct file_system_type *file_systems; fs_supers 字段表示给定类型的已安装文件系统所对应的超级块链表的头(第一个伪元素)。 链表元素的向后和向前链接存放在超级块对象的 s_instances 字段中。get_sb 字段指向依赖于 文件系统类型的函数,该函数分配一个新的超级块对象并初始化它(如果需要,可读磁盘,我们在上一节“把 Linux 中的 VFS 对象串联起来”有提到)。而 kill_sb 字段指向删除超级块 的函数。 fs_flags 字段存放如下几个标志: FS_REQUIRES_DEV:这种类型的任何文件系统必须位于物理磁盘设备上 FS_BINARY_MOUNTDATA:文件系统使用的二进制安装数据 FS_REVAL_DOT:始终在目录项高速缓存中使“ .”和“ ..”路径重新生效(针对网络文件 系统) FS_ODD_RENAME:“重命名”操作就是“移动” 在系统初始化期间,调用 register_filesystem()函数来注册 b 编译时指定的 b 每个文件系统(通 过 find_filesystem()函数找到 name);该函数把相应的 file_system_type 对象插入到文件系统 类型的链表中: int register_filesystem(struct file_system_type * fs) { int res = 0; struct file_system_type ** p; if (!fs) return -EINVAL; if (fs->next) return -EBUSY; INIT_LIST_HEAD(&fs->fs_supers); write_lock(&file_systems_lock); p = find_filesystem(fs->name); if (*p) res = -EBUSY; else *p = fs; write_unlock(&file_systems_lock); return res; } static struct file_system_type **find_filesystem(const char *name) { struct file_system_type **p; for (p=&file_systems; *p; p=&(*p)->next) if (strcmp((*p)->name,name) == 0) break; return p; } 另外,当实现文件系统的模块被装入时,也要调用 register_filesystem()函数。在这种情况下,当该模块被卸载时,对应的文件系统也可以被注销(调用 unregister_filesystem()函数)。 get_fs_type()函数(接收文件系统名作为它的参数)扫描已注册的文件系统链表以查找文件 系统类型的 name 字段,并返回指向相应的 file_system_type 对象(如果存在)的指针: struct file_system_type *get_fs_type(const char *name) { struct file_system_type *fs; read_lock(&file_systems_lock); fs = *(find_filesystem(name)); if (fs && !try_module_get(fs->owner)) fs = NULL; read_unlock(&file_systems_lock); if (!fs && (request_module("%s", name) == 0)) { read_lock(&file_systems_lock); fs = *(find_filesystem(name)); if (fs && !try_module_get(fs->owner)) fs = NULL; read_unlock(&file_systems_lock); } return fs; } 特殊文件系统 磁盘和网络文件系统能够使用户处理存放内核之外的信息,而特殊文件系统可以为系统序员 和管理员提供一种容易的方式来操作内核的数据结构并实现操作系统的特殊特征。我们先列 出 Linux 中所用的最常用的特殊文件系统;对于其中的每个文件系统,破折号隔出了它的安 装点和简短描述: bdev—— none——块设备 binfmt_misc—— any——其他可执行格式 devpts—— /dev/pts——伪终端支持(开放组织的 Unix98 标准) eventpollfs—— none——由有效事件轮询机制使用 futexfs—— none——由 futex(快速用户空间加锁)机制使用 pipefs—— none——管道 proc—— /proc——对内核数据结构的常规访问点 rootfs—— none——为启动阶段提供一个空的根目录 shm—— none—— IPC 共享线性区 mqueue—— any——实现 POSIX 消息队列时使用 sockfs—— none——套接字 sysfs—— /sys——对系统数据的常规访问点(一般是些驱动) tmpfs—— any——临时文件(如果不被交换出去就保持在 RAM 中) usbfs—— /proc/bus/usb—— USB 设备 注意,有几个文件系统没有固定的安装点(两个破折号之间的“ any”)。这些文件系统可以 由用户自由地安装和使用。此外,一些特殊文件系统根本没有安装点(两个破折号之间的 “none”)。它们不是用于与用户交互,但是内核可以用它们来很容易地重新使用 VFS 层的 某些代码。例如,有了 pipefs 特殊文件系统,就可以把管道和 FIFO 文件以相同的方式对待。 特殊文件系统不限于物理块设备。然而,内核给每个安装的特殊文件系统分配一个虚拟的块 设备,让其主设备号为 0 而次设备号具有任意值(每个特殊文件系统有不同的值)。 set_anon_super()函数用于初始化特殊文件系统的超级块;该函数本质上获得一个未使用的次 设备号 dev,然后用主设备号 0 和次设备号 dev 设置新超级块的 s_dev 字段。而另一个 kill_anon_super()函数移走特殊文件系统的超级块。 unnamed_dev_idr 变量包含指向一个辅助 结构(记录当前在用的次设备号)的指针。尽管有些内核设计者不喜欢虚拟块设备标识符, 但是这些标识符有助于内核以统一的方式处理特殊文件系统和普通文件系统。 2.3.2 文件系统安装数据结构 就像每个传统的 Unix 系统一样,Linux 也使用系统的根文件系统( system's root filesystem): 它由内核在引导阶段直接安装,并拥有系统初始化脚本以及最基本的系统程序。 其他文件系统要么由初始化脚本安装,要么由用户直接安装在已安装文件系统的目录上。作 为一个目录树,每个文件系统都拥有自己的根目录( root directory)。安装文件系统的这个目 录称之为安装点( mount point)。已安装文件系统属于安装点目录的一个子文件系统。例如, /proc 虚拟文件系统是系统的根文件系统的孩子(且系统的根文件系统是 /proc 的父亲)。已 安装文件系统的根目录隐藏了父文件系统的安装点目录原来的内容,而且父文件系统的整个 子树位于安装点之下。 文件系统的根目录有可能不同于进程的根目录:进程的根目录是与“ /”路径对应的目录。 缺省情况下,进程的根目录与系统的根文件系统的根目录一致(更准确地说是与进程的命名 空间中的根文件系统的根目录一致,这个是重点,我们下面要讨论),但是可以通过调用 chroot()系统调用改变进程的根目录。 命名空间 在传统的 Unix 系统中,只有一个已安装文件系统树:从系统的根文件系统开始,每个进程 通过指定合适的路径名可以访问已安装文件系统中的任何文件。从这个方面考虑, Linux 2.6 更加的精确:每个进程可拥有自己的已安装文件系统树— — 叫做进程的命名空间 (namespace)。 一般来说,整个系统的命名空间只有一个,被大多数进程共享,即位于系统的根文件系统且 被 init 进程使用的已安装文件系统树。不过,如果 clone()系统调用以 CLONE_NEWNS 标志 创建一个新进程,那么进程将获取一个新的命名空间。换句话说,如果父进程没有以 CLONE_NEWNS 标志创建这些子进程,命名空间将由随后的子进程继承。 当进程安装或卸载一个文件系统时,仅修改它的命名空间。因此,所做的修改对共享同一命 名空间的所有进程都是可见的,并且也只对它们可见。进程甚至可通过使用 Linux 特有的 pivot_root()系统调用来改变它的命名空间的根文件系统。 进程的命名空间由进程描述符的 namespace 字段指向的 namespace 结构描述: struct namespace { atomic_t count; /* 引用计数器(共享命名空间的进程数) */ struct vfsmount * root; /* 命名空间根目录的已安装文件系统描述符 */ struct list_head list; /* 所有已安装文件系统描述符 (vfsmount)链表的头 */ wait_queue_head_t poll; /* 命名空间等待队列 */ int event; /* 事件 */ }; list 字段是双向循环链表的头,该表聚集了属于命名空间的所有已安装文件系统。 root 字段 表示已安装文件系统,它是这个命名空间的已安装文件系统树的根。接下来我们将看到的, 已安装文件系统由 vfsmount 结构描述。 文件系统安装数据结构 在大多数传统的类 Unix 内核中,每个文件系统只能安装一次。假定存放在 /dev/fd0 磁盘上 的 Ext2 文件系统通过如下命令安装在目录 /flp 下: mount -t ext2 /dev/fd0 /flp 在用 umount 命令卸载该文件系统前,所有其他作用于 /dev/fd0 的安装命令都会失败。然而, Linux 有所不同:同一个文件系统被安装多次是可能的。当然,如果一个文件系统安装了 n 次,那么它的根目录就可通过 n 个安装点来访问。尽管同一文件系统可以通过不同的安装点 来访问,但是文件系统的的确确是唯一的。因此,不管一个文件系统被装了多少次,都仅有 一个超级块对象。 那么,安装的文件系统形成一个层次:一个文件系统的安装点可能成为第二个文件系统的目 录,而第二个文件系统又安装在第三个文件系统之上,等等。 把多个安装堆叠在一个单独的安装点上也是可能的。尽管已经使用先前安装下的文件和目录 的进程可以继续使用,但在同一安装点上的新安装隐藏前一个安装的文件系统。当最顶层的 安装被删除时,下一层的安装再一次变为可见的。 你可以想像,跟踪已安装的文件系统很快会变为一场恶梦。对于每个安装操作,内核必须在 内存中保存安装点和安装标志,以及要安装文件系统与其他已安装文件系统之间的关系。所 以,我们需要一个保留安装信息的数据结构来理清这些关系。这样的信息保存在已安装文件 系统描述符中;每个描述符是一个具有 vfsmount 类型的数据结构: struct vfsmount { struct list_head mnt_hash; /* 用于散列表链表的指针 */ struct vfsmount *mnt_parent; /* 指向父文件系统,这个文件系统安装在其上 */ struct dentry *mnt_mountpoint; /* 指向这个文件系统安装点目录的 dentry */ struct dentry *mnt_root; /* 指向这个文件系统根目录的 dentry */ struct super_block *mnt_sb; /* 指向这个文件系统的超级块对象 */ struct list_head mnt_mounts; /* 包含所有文件系统描述符链表的头 *(相对于这个文件系统) */ struct list_head mnt_child; /* 用于已安装文件系统链表 mnt_mounts 的指针 */ atomic_t mnt_count; /* 引用计数器(增加该值以禁止文件系统被卸载) */ int mnt_flags; /* 标志 */ int mnt_expiry_mark; /* 如果文件系统标记为到期,那么就设置该标为 true *(如果设置了该标志,并且没有任何人使用它, * 那么就可以自动卸载这个文件系统) */ char *mnt_devname; /* 设备文件名 如:/dev/dsk/hda1 */ struct list_head mnt_list; /* 已安装文件系统描述符的 namespace 链表的指针 */ struct list_head mnt_expire; /* 具体文件系统到期链表的指针 */ struct list_head mnt_share; /* circular list of shared mounts */ struct list_head mnt_slave_list;/* list of slave mounts */ struct list_head mnt_slave; /* slave list entry */ struct vfsmount *mnt_master; /* slave is on master->mnt_slave_list */ struct namespace *mnt_namespace; /* 指向安装了文件系统的进程命名空间的指针 */ int mnt_pinned; }; vfsmount 数据结构保存在几个双向循环链表中: - 由父文件系统 vfsmount 描述符的地址和安装点目录的目录项对象的地址索引的散列表。 散列表存放在 mount_hashtable 数组中,跟 inode 和 dentry 一样,vfsmount 的散列表大小取 决于系统中 RAM 的容量。表中每一项是具有同一散列值的所有描述符形成的双向循环链表 的头。描述符的 mnt_hash 字段包含指向链表中相邻元素的指针。 - 对于每一个命名空间,所有属于此命名空间的已安装的文件系统描述符形成了一个双向循 环链表。namespace 结构的 list 字段存放链表的头, vfsmount 描述符的 mnt_list 字段包含链 表中指向相邻元素的指针。 - 对于每一个已安装的文件系统,所有已安装的子文件系统形成了一个双向循环链表。每个 链表的头存放在已安装的文件系统描述符的 mnt_mounts 字段;此外,描述符的 mnt_child 字段存放指向链表中相邻元素的指针。 vfsmount_lock 自旋锁保护已安装文件系统对象的链表免受同时访问。 描述符的 mnt_flags 字段存放几个标志的值,用以指定如何处理已安装文件系统中的某些种 类的文件。这些标志可通过 mount 命令的选项进行设置,其标志如下所示: MNT_NOSUID:在已安装文件系统中禁止 setuid 和 setgid 标志 MNT_NODEV:在已安装文件系统中禁止访问设备文件 MNT_NOEXEC:在已安装文件系统中不允许程序执行 接下来介绍几个处理已安装文件系统描述符的常用的函数 (1)分配和初始化一个已安装文件系统描述符。 struct vfsmount *alloc_vfsmnt(const char *name) { struct vfsmount *mnt = kmem_cache_alloc(mnt_cache, GFP_KERNEL); if (mnt) { memset(mnt, 0, sizeof(struct vfsmount)); atomic_set(&mnt->mnt_count, 1); INIT_LIST_HEAD(&mnt->mnt_hash); INIT_LIST_HEAD(&mnt->mnt_child); INIT_LIST_HEAD(&mnt->mnt_mounts); INIT_LIST_HEAD(&mnt->mnt_list); INIT_LIST_HEAD(&mnt->mnt_expire); INIT_LIST_HEAD(&mnt->mnt_share); INIT_LIST_HEAD(&mnt->mnt_slave_list); INIT_LIST_HEAD(&mnt->mnt_slave); if (name) { int size = strlen(name) + 1; char *newname = kmalloc(size, GFP_KERNEL); if (newname) { memcpy(newname, name, size); mnt->mnt_devname = newname; } } } return mnt; } (2)释放由 mnt 指向的已安装文件系统描述符。 void free_vfsmnt(struct vfsmount *mnt) { kfree(mnt->mnt_devname); kmem_cache_free(mnt_cache, mnt); } (3)在散列表中查找一个描述符并返回它的地址(参数 mnt 表示一个已安装的文件系统, dentry 表示安装在这个已安装文件系统的子文件系统的安装点,函数返回该子文件系统的 vfsmount)。 struct vfsmount *lookup_mnt(struct vfsmount *mnt, struct dentry *dentry) { struct vfsmount *child_mnt; spin_lock(&vfsmount_lock); if ((child_mnt = __lookup_mnt(mnt, dentry, 1))) mntget(child_mnt); spin_unlock(&vfsmount_lock); return child_mnt; } struct vfsmount *__lookup_mnt(struct vfsmount *mnt, struct dentry *dentry, int dir) { struct list_head *head = mount_hashtable + hash(mnt, dentry); struct list_head *tmp = head; struct vfsmount *p, *found = NULL; for (;;) { tmp = dir ? tmp->next : tmp->prev; p = NULL; if (tmp == head) break; p = list_entry(tmp, struct vfsmount, mnt_hash); if (p->mnt_parent == mnt && p->mnt_mountpoint == dentry) { found = p; break; } } return found; } static inline struct vfsmount *mntget(struct vfsmount *mnt) { if (mnt) atomic_inc(&mnt->mnt_count); return mnt; } 内核区参数拷贝 理清了前面的那些数据结构后,我们现在可以来讨论安装一个文件系统时内核所要执行的操 作。我们首先考虑一个文件系统将被安装在一个已安装文件系统之上的情形(在这里我们把 这种新文件系统看作“普通的”)。 mount()系统调用被用来安装一个普通文件系统;它的服务例程 sys_mount()作用于以下参数: - 文件系统所在的设备文件的路径名,或者如果不需要的话就为 NULL(例如,当要安装的 文件系统是基于网络时) - 文件系统被安装其上的某个目录的目录路径名(安装点) - 文件系统的类型,必须是已注册文件系统的名字(查看上一篇章节) - 安装标志,这些标志如下所示 MS_RDONLY:文件只能被读 MS_NOSUID:禁止 setuid 和 setgid 标志 MS_NODEV:禁止访问设备文件 MS_NOEXEC:不允许程序执行 MS_SYNCHRONOUS:文件和目录上的写操作是即时的 MS_REMOUNT:重新安装改变了安装标志的文件系统 MS_MANDLOCK:允许强制加锁 MS_DIRSYNC:目录上的写操作是即时的 MS_NOATIME:不更新文件访问时间 MS_NODIRATIME:不更新目录访问时间 MS_BIND:创建一个“绑定安装”,这就使得一个文件或目录在系统目录树的另外一个点上 可以看得见( mount 命令的__bind 选项) MS_MOVE:自动把一个已安装文件系统移动到另一个安装点( mount 命令的__move 选项) MS_REC:为目录子树递归地创建“绑定安装” MS_VERBOSE:在安装出错时产生内核消息 - 指向一个与文件系统相关的数据结构的指针(也许为 NULL) asmlinkage long sys_mount(char __user * dev_name, char __user * dir_name, char __user * type, unsigned long flags, void __user * data) { int retval; unsigned long data_page; unsigned long type_page; unsigned long dev_page; char *dir_page; retval = copy_mount_options(type, &type_page); if (retval < 0) return retval; dir_page = getname(dir_name); retval = PTR_ERR(dir_page); if (IS_ERR(dir_page)) goto out1; retval = copy_mount_options(dev_name, &dev_page); if (retval < 0) goto out2; retval = copy_mount_options(data, &data_page); if (retval < 0) goto out3; lock_kernel(); retval = do_mount((char *)dev_page, dir_page, (char *)type_page, flags, (void *)data_page); unlock_kernel(); free_page(data_page); out3: free_page(dev_page); out2: putname(dir_page); out1: free_page(type_page); return retval; } sys_mount()函数把参数的值拷贝到临时内核缓冲区,也就是位于内核栈的我们这个函数的那 些局部变量们。怎么个拷贝法,我们这里好好讲一讲。注意,很多系统调用函数都有这个步骤,这里只讲一次,以后请各位举一反三。 首先,retval = copy_mount_options(type, &type_page),该函数把指向用户态数据区的一个 data 拷贝到内核堆栈 where 指向的那个单元开始处,出错返回 -ENOMEM: int copy_mount_options(const void __user * data, unsigned long *where) { int i; unsigned long page; unsigned long size; *where = 0; if (!data) return 0; if (!(page = __get_free_page(GFP_KERNEL))) return -ENOMEM; /* TASK_SIZE = PAGE_OFFSET * PAGE_OFFSET = 0xc0000000 * PAGE_SIZE = 12 */ /* copy_from_user cannot cross TASK_SIZE ! */ size = TASK_SIZE - (unsigned long)data; if (size > PAGE_SIZE) size = PAGE_SIZE; i = size - exact_copy_from_user((void *)page, data, size); if (!i) { free_page(page); return -EFAULT; } if (i != PAGE_SIZE) memset((char *)page + i, 0, PAGE_SIZE - i); *where = page; return 0; } 函数首先获得一个页,该页的首地址为 page。随后,检查 data 的地址是否在用户态,即它 产生的线性地址是否小于 0xc0000000。如果是,则 TASK_SIZE - data 肯定是大于 0 的,再 做 个 PAGE_SIZE 判 断 以 保 证 不 会 掉 到 TASK_SIZE 里 边 去 。 接 下 来 , 调 用 exact_copy_from_user(page, data, size)从 data 开始向 page 指向的地址位置拷贝 size 个字节: static long exact_copy_from_user(void *to, const void __user * from, unsigned long n) { char *t = to; const char __user *f = from; char c; if (!access_ok(VERIFY_READ, from, n)) return n; while (n) { if (__get_user(c, f)) { memset(t, 0, n); break; } *t++ = c; f++; n--; } return n; } exact_copy_from_user 首先做个参数传递验证,这个验证是通过 access_ok 宏来实现的: #define access_ok(type,addr,size) (likely(__range_ok(addr,size) == 0)) #define __range_ok(addr,size) ({ \ unsigned long flag,sum; \ __chk_user_ptr(addr); \ asm("addl %3,%1 ; sbbl %0,%0; cmpl %1,%4; sbbl $0,%0" \ :"=&r" (flag), "=r" (sum) \ :"1" (addr),"g" ((int)(size)),"rm" (current_thread_info()->addr_limit.seg)); \ flag; }) __chk_user_ptr 在 80x86 体系中是个空函数, access_ok 宏检查 addr 到 addr+size-1 之间的地 址区间,函数首先验证 addr+size(sum 内部变量,要检查的最高地址)是否大于 2^32-1; 这是因为 gcc 用 32 位数表示无符号长整数和指针( long),这就等价于对溢出条件的检查。 其次,函数还检查 addr+size 是否超过当期检查的 thread_info 结构的 addr_limit.seg 字段中存 放的值。在通常情况下,普通进程 addr_limit.seg 的值是 PAGE_OFFSET,也就是 0xc0000000; 内核线程的值是 0xffffffff。 回到 exact_copy_from_user 中,接下来用__get_user 宏再验证一下局部临时变量 c 是否在内 核区,并将 f 的内容拷给它: #define __get_user(x,ptr) \ __get_user_nocheck((x),(ptr),sizeof(*(ptr))) #define __get_user_nocheck(x,ptr,size) \ ({ \ long __gu_err; \ unsigned long __gu_val; \ __get_user_size(__gu_val,(ptr),(size),__gu_err,-EFAULT);\ (x) = (__typeof__(*(ptr)))__gu_val; \ __gu_err; \ }) #define __get_user_size(x,ptr,size,retval,errret) \ do { \ retval = 0; \ __chk_user_ptr(ptr); \ switch (size) { \ case 1: __get_user_asm(x,ptr,retval,"b","b","=q",errret);break; \ /* 拷贝一个字节 */ case 2: __get_user_asm(x,ptr,retval,"w","w","=r",errret);break; \ /* 拷贝一个字 */ case 4: __get_user_asm(x,ptr,retval,"l","","=r",errret);break; \ /* 拷贝一个双字 */ default: (x) = __get_user_bad(); \ } \ } while (0) #define __get_user_asm(x, addr, err, itype, rtype, ltype, errret) \ __asm__ __volatile__( \ "1: mov"itype" %2,%"rtype"1\n" \ "2:\n" \ ".section .fixup,\"ax\"\n" \ "3: movl %3,%0\n" \ " xor"itype" %"rtype"1,%"rtype"1\n" \ " jmp 2b\n" \ ".previous\n" \ ".section __ex_table,\"a\"\n" \ " .align 4\n" \ " .long 1b,3b\n" \ ".previous" \ : "=r"(err), ltype (x) \ : "m"(__m(addr)), "i"(errret), "0"(err)) 随后拷贝给参数 to 所指的位置: *t++ = c; f++; n--; 再回到 sys_mount,其次,dir_page = getname(dir_name),将安装目录名拷贝到 dir_page 内核 临时变量中。 getname 函数也是个很重要的函数,因为这个函数在拷贝文件名时用得很多: char * getname(const char __user * filename) { char *tmp, *result; result = ERR_PTR(-ENOMEM); tmp = __getname(); if (tmp) { int retval = do_getname(filename, tmp); result = tmp; if (retval < 0) { __putname(tmp); result = ERR_PTR(retval); } } audit_getname(result); return result; } #define __getname() kmem_cache_alloc(names_cachep, SLAB_KERNEL) static int do_getname(const char __user *filename, char *page) { int retval; unsigned long len = PATH_MAX; if (!segment_eq(get_fs(), KERNEL_DS)) { if ((unsigned long) filename >= TASK_SIZE) return -EFAULT; if (TASK_SIZE - (unsigned long) filename < PATH_MAX) len = TASK_SIZE - (unsigned long) filename; } retval = strncpy_from_user(page, filename, len); if (retval > 0) { if (retval < len) return 0; return -ENAMETOOLONG; } else if (!retval) retval = -ENOENT; return retval; } getname 函数主要调用 do_getname 完成文件名拷贝实务, do_getname 的实务函数主要是 strncpy_from_user: long strncpy_from_user(char *dst, const char __user *src, long count) { long res = -EFAULT; if (access_ok(VERIFY_READ, src, 1)) __do_strncpy_from_user(dst, src, count, res); return res; } #define __do_strncpy_from_user(dst,src,count,res) \ do { \ int __d0, __d1, __d2; \ might_sleep(); \ __asm__ __volatile__( \ " testl %1,%1\n" \ " jz 2f\n" \ "0: lodsb\n" \ " stosb\n" \ " testb %%al,%%al\n" \ " jz 1f\n" \ " decl %1\n" \ " jnz 0b\n" \ "1: subl %1,%0\n" \ "2:\n" \ ".section .fixup,\"ax\"\n" \ "3: movl %5,%0\n" \ " jmp 2b\n" \ ".previous\n" \ ".section __ex_table,\"a\"\n" \ " .align 4\n" \ " .long 0b,3b\n" \ ".previous" \ : "=d"(res), "=c"(count), "=&a" (__d0), "=&S" (__d1), \ "=&D" (__d2) \ : "i"(-EFAULT), "0"(count), "1"(count), "3"(src), "4"(dst) \ : "memory"); \ } while (0) sys_mount 函数之后的 copy_mount_options(dev_name, &dev_page)和 copy_mount_options(data, &data_page)就很好理解了吧。然后呢,获取大内核锁 lock_kernel(),并调用 do_mount()函数。 一旦 do_mount()返回,则这个服务例程释放大内核锁并释放临时内核缓冲区。 要知 do_mount()函数如何进行真正的安装处理工作,请接着往下看⋯⋯ 2.3.3 安装普通文件系统 在 sys_mount 中调用 do_mount()函数执行文件系统安装实务: retval = do_mount((char *)dev_page, dir_page, (char *)type_page, flags, (void *)data_page); do_mount()函数通过执行下列操作处理真正的安装操作: long do_mount(char *dev_name, char *dir_name, char *type_page, unsigned long flags, void *data_page) { struct nameidata nd; int retval = 0; int mnt_flags = 0; /* Discard magic */ if ((flags & MS_MGC_MSK) == MS_MGC_VAL) flags &= ~MS_MGC_MSK; /* Basic sanity checks */ if (!dir_name || !*dir_name || !memchr(dir_name, 0, PAGE_SIZE)) return -EINVAL; if (dev_name && !memchr(dev_name, 0, PAGE_SIZE)) return -EINVAL; if (data_page) ((char *)data_page)[PAGE_SIZE - 1] = 0; /* 如果已安装文件系统对象中的安装标志 MS_NOSUID、MS_NODEV、MS_NOATIME、 MS_NODIRATIME、MS_NODEV 或 MS_NOEXEC 中任一个被设置,则清除它们,并在已 安装文件系统对象中设置相应的标志( MNT_NOSUID、MNT_NODEV、MNT_NOEXEC、 MNT_NOATIME、MNT_NODIRATIME)。*/ if (flags & MS_NOSUID) mnt_flags |= MNT_NOSUID; if (flags & MS_NODEV) mnt_flags |= MNT_NODEV; if (flags & MS_NOEXEC) mnt_flags |= MNT_NOEXEC; if (flags & MS_NOATIME) mnt_flags |= MNT_NOATIME; if (flags & MS_NODIRATIME) mnt_flags |= MNT_NODIRATIME; flags &= ~(MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_ACTIVE | MS_NOATIME | MS_NODIRATIME); /* ... and get the mountpoint 调用 path_lookup()查找安装点的路径名; * 该函数把路径名查找的结果存放在 nameidata 类型的局部变量 nd 中。*/ retval = path_lookup(dir_name, LOOKUP_FOLLOW, &nd); if (retval) return retval; retval = security_sb_mount(dev_name, &nd, type_page, flags, data_page); if (retval) goto dput_out; /* 如果 MS_REMOUNT 标志被指定,其目的通常是改变超级块对象 s_flags 字段的安装 标志,以及已安装文件系统对象 mnt_flags 字段的安装文件系统标志。 do_remount()函数执行 这些改变。*/ if (flags & MS_REMOUNT) retval = do_remount(&nd, flags & ~MS_REMOUNT, mnt_flags, data_page); /* 否则,检查 MS_BIND 标志。如果它被指定,则用户要求在系统目录树的另一个安 装点上的文件或目录能够可见。 */ else if (flags & MS_BIND) retval = do_loopback(&nd, dev_name, flags & MS_REC); else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE)) retval = do_change_type(&nd, flags); /* 否则,检查 MS_MOVE 标志。如果它被指定,则用户要求改变已安装文件系统的安 装点。do_move_mount()函数原子地完成这一任务。 */ else if (flags & MS_MOVE) retval = do_move_mount(&nd, dev_name); /* 否则,调用 do_new_mount()。这是最普通的情况。当用户要求安装一个特殊文件系 统或存放在磁盘分区中的普通文件系统时,触发该函数。 */ else retval = do_new_mount(&nd, type_page, flags, mnt_flags, dev_name, data_page); dput_out: path_release(&nd); return retval; } 我们还是来看最普通的情况: static int do_new_mount(struct nameidata *nd, char *type, int flags, int mnt_flags, char *name, void *data) { struct vfsmount *mnt; if (!type || !memchr(type, 0, PAGE_SIZE)) return -EINVAL; /* we need capabilities... */ if (!capable(CAP_SYS_ADMIN)) return -EPERM; mnt = do_kern_mount(type, flags, name, data); if (IS_ERR(mnt)) return PTR_ERR(mnt); return do_add_mount(mnt, nd, mnt_flags, NULL); } do_new_mount 调用 do_kern_mount()函数,给它传递的参数为文件系统类型、安装标志以及 块设备名。 do_kern_mount()处理实际的安装操作并返回一个新安装文件系统描述符的地址: struct vfsmount * do_kern_mount(const char *fstype, int flags, const char *name, void *data) { struct file_system_type *type = get_fs_type(fstype); /* 我们熟悉的 get_fs_type 函数 */ struct vfsmount *mnt; if (!type) return ERR_PTR(-ENODEV); mnt = vfs_kern_mount(type, flags, name, data); put_filesystem(type); return mnt; } struct vfsmount * vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data) { struct vfsmount *mnt; char *secdata = NULL; int error; if (!type) return ERR_PTR(-ENODEV); error = -ENOMEM; mnt = alloc_vfsmnt(name); /* 呵呵,我们熟悉的 alloc_vfsmnt 函数 */ if (!mnt) goto out; if (data) { secdata = alloc_secdata(); if (!secdata) goto out_mnt; error = security_sb_copy_data(type, data, secdata); if (error) goto out_free_secdata; } /* 调用依赖于文件系统的 type->get_sb()函数分配,并初始化一个新的超级到 mnt->mnt_sb */ error = type->get_sb(type, flags, name, data, mnt); if (error < 0) goto out_free_secdata; error = security_sb_kern_mount(mnt->mnt_sb, secdata); if (error) goto out_sb; /* 将 mnt->mnt_root 字段初始化为与文件系统根目录对应的目录项对象的地址,并增加 该目录项对象的引用计数器值。 */ mnt->mnt_mountpoint = mnt->mnt_root; /* 用 mnt 中的值初始化 mnt->mnt_parent 字段(对于普通文件系统, * 当后面讲到的 graft_tree()把已安装文件系统的描述符插入到合适的链表中时, * 要把 mnt_parent 字段置为合适的值)。 */ mnt->mnt_parent = mnt; up_write(&mnt->mnt_sb->s_umount); free_secdata(secdata); return mnt; out_sb: dput(mnt->mnt_root); up_write(&mnt->mnt_sb->s_umount); deactivate_super(mnt->mnt_sb); out_free_secdata: free_secdata(secdata); out_mnt: free_vfsmnt(mnt); out: return ERR_PTR(error); } 然后,do_new_mount()函数调用 do_add_mount(): int do_add_mount(struct vfsmount *newmnt, struct nameidata *nd, int mnt_flags, struct list_head *fslist) { int err; down_write(&namespace_sem); /* Something was mounted here while we slept */ while (d_mountpoint(nd->dentry) && follow_down(&nd->mnt, &nd->dentry)) ; err = -EINVAL; if (!check_mnt(nd->mnt)) goto unlock; /* Refuse the same filesystem on the same mount point */ err = -EBUSY; if (nd->mnt->mnt_sb == newmnt->mnt_sb && nd->mnt->mnt_root == nd->dentry) goto unlock; err = -EINVAL; if (S_ISLNK(newmnt->mnt_root->d_inode->i_mode)) goto unlock; newmnt->mnt_flags = mnt_flags; if ((err = graft_tree(newmnt, nd))) goto unlock; if (fslist) { /* add to the specified expiration list */ spin_lock(&vfsmount_lock); list_add_tail(&newmnt->mnt_expire, fslist); spin_unlock(&vfsmount_lock); } up_write(&namespace_sem); return 0; unlock: up_write(&namespace_sem); mntput(newmnt); return err; } 其本质上执行下列操作: 1、获得当前进程的写信号量 namespace_sem,因为函数要更改 namespace 结构。 2、do_kern_mount()函数可能让当前进程睡眠;同时,另一个进程可能在完全相同的安装点 上安装文件系统或者甚至更改根文件系统( current->namespace->root)。验证在该安装点上 最近安装的文件系统是否仍指向当前的 namespace;如果不是,则释放读 /写信号量并返回一 个错误码。 3、如果要安装的文件系统已经被安装在由系统调用的参数所指定的安装点上,或该安装点 是一个符号链接,则释放读 /写信号量并返回一个错误码。 4、初始化由 do_kern_mount()分配的新安装文件系统对象的 mnt_flags 字段的标志。 5、调用 graft_tree()把新安装的文件系统对象插入到 namespace 链表、散列表中(在 graft_tree() 函数中调用 attach_recursive_mnt 函数实现) 6、父文件系统的子链表中。 7、释放 namespace_sem 读/写信号量并返回。 static int graft_tree(struct vfsmount *mnt, struct nameidata *nd) { int err; if (mnt->mnt_sb->s_flags & MS_NOUSER) return -EINVAL; if (S_ISDIR(nd->dentry->d_inode->i_mode) != S_ISDIR(mnt->mnt_root->d_inode->i_mode)) return -ENOTDIR; err = -ENOENT; mutex_lock(&nd->dentry->d_inode->i_mutex); if (IS_DEADDIR(nd->dentry->d_inode)) goto out_unlock; err = security_sb_check_sb(mnt, nd); if (err) goto out_unlock; err = -ENOENT; if (IS_ROOT(nd->dentry) || !d_unhashed(nd->dentry)) err = attach_recursive_mnt(mnt, nd, NULL); out_unlock: mutex_unlock(&nd->dentry->d_inode->i_mutex); if (!err) security_sb_post_addmount(mnt, nd); return err; } 回到 do_mount()函数,最后调用 path_release()终止安装点的路径名查找并返回。 2.3.4 分配超级块对象 文件系统对象的 get_sb 方法通常是由单行函数实现的。例如,在 Ext2 文件系统中该方法的 实现如下: //fs/ext2/Super.c static int ext2_get_sb(struct file_system_type *fs_type, int flags, const char *dev_name, void *data, struct vfsmount *mnt) { return get_sb_bdev(fs_type, flags, dev_name, data, ext2_fill_super, mnt); } get_sb_bdev() VFS 函数分配并初始化一个新的适合于磁盘文件系统的超级块;它接收 ext2_fill_super()函数的地址,该函数从 Ext2 磁盘分区读取磁盘超级块。 为了分配适合于特殊文件系统的超级块, VFS 也提供 get_sb_pseudo()函数,对于没有安装点 的特殊文件系统,例如 pipefs()、get_sb_single()函数等(对于具有唯一安装点的特殊文件系 统,例如. sysfs)以及 get_sb_nodev()函数(对于可以安装多次的特殊文件系统,例如 tmpfs; 参见下面)。 get_sb_bdev()函数位于/fs/Super.c,代码如下: int get_sb_bdev(struct file_system_type *fs_type, int flags, const char *dev_name, void *data, int (*fill_super)(struct super_block *, void *, int), struct vfsmount *mnt) { struct block_device *bdev; struct super_block *s; int error = 0; /* 调用 open_bdev_excl()打开设备文件名为 dev_name 的块设备。*/ bdev = open_bdev_excl(dev_name, flags, fs_type); if (IS_ERR(bdev)) return PTR_ERR(bdev); /* * once the super is inserted into the list by sget, s_umount * will protect the lockfs code from trying to start a snapshot * while we are mounting */ down(&bdev->bd_mount_sem); /* 调用 sget()搜索文件系统的超级块对象链表( type->fs_supers,参见前面的章节中“文 件系统安装数据结构”部分)。 如果找到一个与块设备相关的超级块,则返回它的地址。否 则,分配并初始化一个新的超级块对象,把它插入到文件系统链表和超级块全局链表中,并 返回其地址。 */ s = sget(fs_type, test_bdev_super, set_bdev_super, bdev); up(&bdev->bd_mount_sem); if (IS_ERR(s)) goto error_s; if (s->s_root) { /* 如果不是新的超级块(注意,是通过 s->s_root 是否为空来判断的) */ if ((flags ^ s->s_flags) & MS_RDONLY) { up_write(&s->s_umount); deactivate_super(s); error = -EBUSY; goto error_bdev; } close_bdev_excl(bdev); } else { char b[BDEVNAME_SIZE]; /* 把参数 flags 中的值拷贝到超级块的 s_flags 字段, * 并将 s_id、s_old_blocksize 以及 s_blocksize 字段设置为块设备的合适值。 */ s->s_flags = flags; strlcpy(s->s_id, bdevname(bdev, b), sizeof(s->s_id)); sb_set_blocksize(s, block_size(bdev)); /* 调用依赖文件系统的函数(例子中是 ext2_fill_super 函数,我们在讨论 ext2 的 时候会讲它)访问磁盘上的超级块信息,并填充新超级块对象的其他字段。 */ error = fill_super(s, data, flags & MS_SILENT ? 1 : 0); if (error) { up_write(&s->s_umount); deactivate_super(s); goto error; } s->s_flags |= MS_ACTIVE; bdev_uevent(bdev, KOBJ_MOUNT); } return simple_set_mnt(mnt, s); error_s: error = PTR_ERR(s); error_bdev: close_bdev_excl(bdev); error: return error; } 2.3.5 安装根文件系统 安装根文件系统是系统初始化的关键部分。这是一个相当复杂的过程,因为 Linux 内核允许 根文件系统存放在很多不同的地方,比如硬盘分区、软盘、通过 NFS 共享的远程文件系统,甚至保存在 ramdisk 中(RAM 中的虚拟块设备)。 为了使叙述变得简单,让我们假定根文件系统存放在硬盘分区(毕竟这是最常见的情行)。 当系统启动时,内核就要在变量 ROOT_DEV 中寻找包含根文件系统的磁盘主设备号: //init/Do_mounts.c dev_t ROOT_DEV; 当编译内核时,或者向最初的启动装入程序传递一个合适的“ root”选项时,根文件系统可 以 被指 定为/dev 目录下的一个设备文件。类似地,根文件系统的安装标志存放在 root_mountflags 变量中: //init/Do_mounts.c int root_mountflags = MS_RDONLY | MS_SILENT; 用户可以指定这些标志,或者通过对已编译的内核映像使用 rdev 外部程序,或者向最初的 启动装入程序传递一个合适的 rootflags 选项来达到。 安装根文件系统分两个阶段: (1)内核安装特殊 rootfs 文件系统,该文件系统仅提供一个作为初始安装点的空目录。 (2)内核在空目录上安装实际根文件系统。 为什么内核不怕麻烦,要在安装实际根文件系统之前安装 rootfs 文件系统呢?这是因为, rootfs 文件系统允许内核容易地改变实际根文件系统。实际上,在大多数情况下,系统初始 化是内核会逐个地安装和卸载几个根文件系统。例如,一个发布版的初始启动光盘可能把具 有一组最小驱动程序的内核装人 RAM 中,内核把存放在 ramdisk 中的一个最小的文件系统 作为根安装。接下来,在这个初始根文件系统中的程序探测系统的硬件(例如,它们判断硬 盘是否是 EIDE、SCSI 等等),装入所有必需的内核模块,并从物理块设备重新安装根文件 系统。 阶段 1:安装 rootfs 文件系统 第一阶段是由 init_rootfs()和 init_mount_tree()函数完成的,它们在系统初始化过程中执行。 init_rootfs()函数注册特殊文件系统类型 rootfs: static struct file_system_type rootfs_fs_type = { .name = "rootfs", .get_sb = rootfs_get_sb, .kill_sb = kill_litter_super, }; init_mount_tree()函数主要完成根文件系统的初始化: static void __init init_mount_tree(void) { struct vfsmount *mnt; struct namespace *namespace; struct task_struct *g, *p; mnt = do_kern_mount("rootfs", 0, "rootfs", NULL); if (IS_ERR(mnt)) panic("Can't create rootfs"); namespace = kmalloc(sizeof(*namespace), GFP_KERNEL); if (!namespace) panic("Can't allocate initial namespace"); atomic_set(&namespace->count, 1); INIT_LIST_HEAD(&namespace->list); init_waitqueue_head(&namespace->poll); namespace->event = 0; list_add(&mnt->mnt_list, &namespace->list); namespace->root = mnt; mnt->mnt_namespace = namespace; init_task.namespace = namespace; read_lock(&tasklist_lock); do_each_thread(g, p) { get_namespace(namespace); p->namespace = namespace; } while_each_thread(g, p); read_unlock(&tasklist_lock); set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root); set_fs_root(current->fs, namespace->root, namespace->root->mnt_root); } 看到了吧,init_mount_tree 首先调用 do_kern_mount()函数,把字符串“ rootfs”作为文件系 统类型参数传递给它,文件系统标志是 0,没有 data,并把该函数返回的新安装文件系统描 述符的地址保存在 mnt 局部变量中。正如前面介绍的, do_kern_mount()最终调用 rootfs 文件 系统的 get_sb 方法,也即 rootfs_get_sb()函数: static int rootfs_get_sb(struct file_system_type *fs_type, int flags, const char *dev_name, void *data, struct vfsmount *mnt) { return get_sb_nodev(fs_type, flags|MS_NOUSER, data, ramfs_fill_super, mnt); } get_sb_nodev()函数前面一件提到了,针对 rootfs 文件系统: 1、调用 sget()函数分配新的超级块,传递 set_anon_super()函数的地址作为参数。接下来,用合适的方式设置超级快的 s_dev 字段:主设备号为 O,次设备号不同于其他已安装的特殊 文件系统的次设备号。 2、将 flags 参数的值拷贝到超级块的 s_flags 字段中。 3、调用 ramfs_fill_super()函数分配索引节点对象和对应的目录项对象并填充超级块字段值。 由于 rootfs 是一种特殊文件系统,没有磁盘超级块,因此只需执行两个超级块操作: static int ramfs_fill_super(struct super_block * sb, void * data, int silent) { struct inode * inode; struct dentry * root; sb->s_maxbytes = MAX_LFS_FILESIZE; sb->s_blocksize = PAGE_CACHE_SIZE; sb->s_blocksize_bits = PAGE_CACHE_SHIFT; sb->s_magic = RAMFS_MAGIC; sb->s_op = &ramfs_ops; sb->s_time_gran = 1; inode = ramfs_get_inode(sb, S_IFDIR | 0755, 0); if (!inode) return -ENOMEM; root = d_alloc_root(inode); if (!root) { iput(inode); return -ENOMEM; } sb->s_root = root; return 0; } 4、返回新超级块的地址。 回到 init_mount_tree()函数,继续: 为进程 0 的命名空间分配一个 namespace 对象,并将它插入到由 do_kern_mount()函数返回 的已安装文件系统描述符中: namespace = kmalloc(sizeof(*namespace), GFP_KERNEL); list_add(&mnt->mnt_list, &namespace->list); namespace->root = mnt; mnt->mnt_namespace = init_task.namespace = namespace; 将系统中其他每个进程的 namespace 字段设置为 namespace 对象的地址;同时初始化引用计 数器 namespace->count(缺省情况下,所有的进程共享同一个初始 namespace)。 将进程 0 的根目录和当前工作目录设置为根文件系统。 set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root); set_fs_root(current->fs, namespace->root, namespace->root->mnt_root); 阶段 2:安装实际根文件系统 根文件系统安装操作的第二阶段是由内核在系统初始化即将结束时进行的。根据内核被编译 时所选择的选项,和内核装入程序所传递的启动选项,可以有几种方法安装实际根文件系统。 为了简单起见,我们只考虑磁盘文件系统的情况,它的设备文件名已通过“ root”启动参数 传递给内核。同时我们假定除了 rootfs 文件系统外,没有使用其他初始特殊文件系统。 内核主要是调用 prepare_namespace()函数执行安装实际根文件系统的操作: void __init prepare_namespace(void) { int is_floppy; if (root_delay) { printk(KERN_INFO "Waiting %dsec before mounting root device...\n", root_delay); ssleep(root_delay); } md_run_setup(); if (saved_root_name[0]) { /* 把 root_device_name 变量置为从启动参数“ root”中获取的设备文件名。 * 同样,把 ROOT_DEV 变量置为同一设备文件的主设备号和次设备号。 */ root_device_name = saved_root_name; if (!strncmp(root_device_name, "mtd", 3)) { /*调用 mount_block_root()函数,将最常用的块设备作为 rootfs 文件系统的子文件系统 */ mount_block_root(root_device_name, root_mountflags); goto out; } ROOT_DEV = name_to_dev_t(root_device_name); if (strncmp(root_device_name, "/dev/", 5) == 0) root_device_name += 5; } is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR; if (initrd_load()) goto out; if (is_floppy && rd_doload && rd_load_disk(0)) ROOT_DEV = Root_RAM0; mount_root(); out: /* 移动 rootfs 文件系统根目录上的已安装文件系统的安装点。 */ sys_mount(".", "/", NULL, MS_MOVE, NULL); sys_chroot("."); security_sb_post_mountroot(); } 注意,rootfs 特殊文件系统没有被卸载:它只是隐藏在基于磁盘的根文件系统下了。 2.3.6 卸载文件系统 umount()系统调用用来卸载一个文件系统。我们不去详细讨论它的代码了,比较简单。相应 的 sys_umount()服务例程作用于两个参数:文件名(多是安装点目录或是块设备文件名)和 一组标志。该函数执行下列操作: 1.调用 path_lookup()查找安装点路径名;该函数把返回的查找操作结果存放在 nameidata 类 型的局部变量 nd 中。 2.如果查找的最终目录不是文件系统的安装点,则设置 retval 返回码为-EINVAL 并跳到第 6 步。这种检查是通过验证 nd->mnt->mnt_root(它包含由 nd.dentry 指向的目录项对象地址) 进行的。 3.如果要卸载的文件系统还没有安装在命名空间中,则设置 retval 返回码为-EINVAL 并跳到 第 6 步(回想一下,某些特殊文件系统没有安装点)。这种检查是通过在 nd->mnt 上调用 check_mnt()函数进行的。 4.如果用户不具有卸载文件系统的特权,则设置 retval 返回码为-EPERM 并跳到第 6 步。 5.调用 do_umount(),传递给它的参数为 nd.mnt(已安装文件系统对象)和 flags(一组标志)。 该函数执行下列操作: a)从已安装文件系统对象的 mnt_sb 字段检索超级块对象 sb 的地址。 b)如果用户要求强制卸载操作,则调用 umount_begin 超级块操作中断任何正在进行的安装 操作。 c)如果要卸载的文件系统是根文件系统,且用户并不要求真正地把它卸载下来则调用 do_remount_sb()重新安装根文件系统为只读并终止。 d)为进行写操作而获取当前进程的 namespace->sem 读/写信号量和 vfsmount_lock 自旋锁。 e)如果已安装文件系统不包含任何子安装文件系统的安装点,或者用户要求强制卸载文件系 统,则调用 umount_tree()卸载文件系统(及其所有子文件系统) f)释放 vfsmount_lock 自旋锁和当前进程的 namespace->sem 读/写信号量。 6.减少相应文件系统根目录的目录项对象和已安装文件系统描述符的引用计数器值;这些计 数器值由 path_lookup()增加。 7.返回 retval 的值。 2.4 路径名的查找 当进程必须识别一个文件时,就把它的文件路径名传递给某个 VFS 系统调用,如 open()、 mkdir()、rename()或 stat()。我们这里要说明 VFS 如何实现路径名查找,也就是说如何从文 件路径名导出相应的索引节点。 执行这一任务的标准过程就是分析路径名并把它拆分成一个文件名序列。除了最后一个文件 名以外,所有的文件名都必定是目录。 如果路径名的第一个字符是“ /”,例如:/usr/share/system-configure-ext2/test.conf,那么这个 路径名是绝对路径,因此从 current->fs->root(进程的根目录)所标识的目录开始搜索。否 则,路径名是相对路径,因此从 currrent->fs->pwd(进程的当前目录)所标识的目录开始搜 索。 在对第一个目录的索引节点进行处理的过程中,代码要检查与第一个名字匹配的目录项,以 获得相应的索引节点。然后,从缓存或磁盘读出包含那个索引节点的目录文件,并检查与第 二个名字匹配的目录项,以获得相应的索引节点。对于包含在路径中的每个名字,这个过程 反复执行。 目录项高速缓存极大地加速了这一过程,因为它把最近最常使用的目录项对象保留在内存中。 正如我们以前看到的,每个这样的对象使特定目录中的一个文件名与它相应的索引节点相联 系。因此在很多情况下,路径名的分析可以避免从磁盘读取中间目录。 但是,事情并不像看起来那么简单,因为必须考虑如下的 Unix 和 VFS 文件系统的特点: - 对每个目录的访问权必须进行检查,以验证是否允许进程读取这一目录的内容。 - 文件名可能是与任意一个路径名对应的符号链接;在这种情况下,分析必须扩展到那个路 径名的所有分量。 - 符号链接可能导致循环引用;内核必须考虑这个可能性,并能在出现这种情况时将循环终 止。 - 文件名可能是一个已安装文件系统的安装点。这种情况必须检测到,这样,查找操作必须 延伸到新的文件系统。 - 路径名查找应该在发出系统调用的进程的命名空间中完成。由具有不同命名空间的两个进 程使用的相同路径名,可能指定了不同的文件。 路径名查找流程极其复杂,如果实在看不懂,还是用老方法,先理清相关的数据结构,然后 再在纸上画一画。 2.4.1 查找路径名的一般流程 路径名查找是由 path_lookup()函数执行的,它接收三个参数: name:指向要解析的文件路径名的指针。 flags:标志的值,表示将会怎样访问查找的文件。 nd:nameidata 数据结构的地址,这个结构存放了查找操作的结果。 当 path_lookup()返回时,结果参数 nd 指向的 nameidata 结构用与路径名查找操作有关的数据 来填充: struct nameidata { struct dentry *dentry; /* 目录项对象的地址 */ struct vfsmount *mnt; /* 已安装文件系统对象的地址 */ struct qstr last; /* 路径名的最后一个分量(当 LOOKUP_PARENT 标志被设置 时使用)*/ unsigned int flags; /* 查找标志 */ int last_type; /* 路径名最后一个分量的类型(当 LOOKUP_PARENT 标志被设置时使用)*/ unsigned depth; /* 符号链接嵌套的当前级别(参见下面);必须小于 6 */ char *saved_names[MAX_NESTED_LINKS + 1]; /* 与嵌套的符号链接关联的路径名 数组 */ /* Intent data 单个成员联合体,指定如何访问文件 */ union { struct open_intent open; } intent; }; dentry 和 mnt 字段分别指向所解析的最后一个路径分量的目录项对象(注意是 test.conf 而不 是 system-configure-ext2)和已安装文件系统对象。这两个字段“描述”由给定路径名表示 的文件。 由于 path_lookup()函数返回的 nameidata 结构中的目录项对象和已安装文件系统对象代表了 查找操作的结果,因此在 path_lookup()的调用者完成使用查找结果之前,这个两个对象都不 能被释放。因此, path_lookup()增加这两个对象引用计数器的值。如果调用者想释放这些对 象,则调用 path_release()函数,传递给它的参数就是 nameidata 结构的地址。 flags 字段存放查找操作中使用的某些标志的值,这些标志中的大部分可由调用者在 path_lookup()的 flags 参数中进行设置: LOOKUP_FOLLOW:如果最后一个分量是符号链接,则解释(追踪)它 LOOKUP_DIRECTORY:最后一个分量必须是目录 LOOKUP_CONTINUE:在路径名中还有文件名要检查 LOOKUP_PARENT:查找最后一个分量名所在的目录 LOOKUP_NOALT:不考虑模拟根目录(在 80x86 体系结构中没有用) LOOKUP_OPEN:试图打开一个文件 LOOKUP_CREATE:试图创建一个文件(如果不存在) LOOKUP_ACCESS:试图为一个文件检查用户的权限 下面,我们来详细查看 path_lookup()函数: int fastcall path_lookup(const char *name, unsigned int flags, struct nameidata *nd) { return do_path_lookup(AT_FDCWD, name, flags, nd); } static int fastcall do_path_lookup(int dfd, const char *name, unsigned int flags, struct nameidata *nd) { int retval = 0; int fput_needed; struct file *file; /* 首先,如下初始化 nd 参数的某些字段:*/ nd->last_type = LAST_ROOT; /* if there are only slashes... */ nd->flags = flags; nd->depth = 0; /* 如果路径名的第一个字符是“ /”,那么查找操作必须从当前根目录开始:获取相应 已安装文件对象 (current->fs->rootmnt)和目录项对象(current->fs->root)的地址,增加引用计数 器的值,并把它们的地址分别存放在 nd->mnt 和 nd->dentry 中。*/ if (*name=='/') { /* 为进行读操作而获取当前进程的 current->fs->lock 读/写信号量。 */ read_lock(¤t->fs->lock); /* 80x86 上 altroot 和 altrootmnt 总是 NULL,不会执行以下分支 */ if (current->fs->altroot && !(nd->flags & LOOKUP_NOALT)) { nd->mnt = mntget(current->fs->altrootmnt); nd->dentry = dget(current->fs->altroot); read_unlock(¤t->fs->lock); if (__emul_lookup_dentry(name,nd)) goto out; /* found in altroot */ read_lock(¤t->fs->lock); } nd->mnt = mntget(current->fs->rootmnt); /* 注意,这里增加了引用计数 */ nd->dentry = dget(current->fs->root); /* 注意,这里增加了引用计数 */ /* 释放当前进程的 current->fs->lock 读/写信号量。 */ read_unlock(¤t->fs->lock); } else if (dfd == AT_FDCWD) { /* 否则,如果路径名的第一个字符不是“ /”,则查找操作必须从当前工作目录 开始:获得相应已安装文件系统对象( current->fs->mt)和目录项对象(current->fs->pwd) 的地址,增加引用计数器的值,并把它们的地址分别存放在 nd->mnt 和 nd->dentry 中。 */ read_lock(¤t->fs->lock); nd->mnt = mntget(current->fs->pwdmnt); /* 注意,这里增加了引用计数 */ nd->dentry = dget(current->fs->pwd); /* 注意,这里增加了引用计数 */ read_unlock(¤t->fs->lock); } else { /* 我们看到在 path_lookup 中只用了 AT_FDCWD,所以也不会执行下面的分支。 */ struct dentry *dentry; file = fget_light(dfd, &fput_needed); retval = -EBADF; if (!file) goto out_fail; dentry = file->f_dentry; retval = -ENOTDIR; if (!S_ISDIR(dentry->d_inode->i_mode)) goto fput_fail; retval = file_permission(file, MAY_EXEC); if (retval) goto fput_fail; nd->mnt = mntget(file->f_vfsmnt); nd->dentry = dget(dentry); fput_light(file, fput_needed); } /* 把当前进程描述符中的 total_link_count 字段置为 0(参见后面的“符号链接的查找” 内容)。 */ current->total_link_count = 0; /* 调用 link_path_walk()函数处理真正进行的查找操作: */ retval = link_path_walk(name, nd); out: if (likely(retval == 0)) { if (unlikely(!audit_dummy_context() && nd && nd->dentry && nd->dentry->d_inode)) audit_inode(name, nd->dentry); } out_fail: return retval; fput_fail: fput_light(file, fput_needed); goto out_fail; } 我们现在准备描述路径名查找操作的核心,也就是 link_path_walk()函数。它接收的参数为 要解析的路径名指针 name 和拥有目录项信息和安装文件系统信息的 nameidata 数据结构的 地址 nd。 int fastcall link_path_walk(const char *name, struct nameidata *nd) { struct dentry *saved_dentry = nd->dentry; struct vfsmount *saved_mnt = nd->mnt; int result; …… result = __link_path_walk(name, nd); …… return result; } static fastcall int __link_path_walk(const char * name, struct nameidata *nd) { struct path next; struct inode *inode; int err, atomic; unsigned int lookup_flags = nd->flags; atomic = (lookup_flags & LOOKUP_ATOMIC); while (*name=='/') name++; if (!*name) goto return_reval; inode = nd->dentry->d_inode; if (nd->depth) lookup_flags = LOOKUP_FOLLOW | (nd->flags & LOOKUP_CONTINUE); /* At this point we know we have a real path component. */ for(;;) { unsigned long hash; struct qstr this; unsigned int c; nd->flags |= LOOKUP_CONTINUE; err = exec_permission_lite(inode, nd); if (err == -EAGAIN) err = vfs_permission(nd, MAY_EXEC); if (err) break; this.name = name; c = *(const unsigned char *)name; hash = init_name_hash(); do { name++; hash = partial_name_hash(c, hash); c = *(const unsigned char *)name; } while (c && (c != '/')); this.len = name - (const char *) this.name; this.hash = end_name_hash(hash); /* remove trailing slashes? */ if (!c) goto last_component; while (*++name == '/'); if (!*name) goto last_with_slashes; /* * "." and ".." are special - ".." especially so because it has * to be able to know about the current root directory and * parent relationships. */ if (this.name[0] == '.') switch (this.len) { default: break; case 2: if (this.name[1] != '.') break; follow_dotdot(nd); inode = nd->dentry->d_inode; /* fallthrough */ case 1: continue; } /* * See if the low-level filesystem might want * to use its own hash.. */ if (nd->dentry->d_op && nd->dentry->d_op->d_hash) { err = nd->dentry->d_op->d_hash(nd->dentry, &this); if (err < 0) break; } /* This does the actual lookups.. */ err = do_lookup(nd, &this, &next, atomic); if (err) break; err = -ENOENT; inode = next.dentry->d_inode; if (!inode) goto out_dput; err = -ENOTDIR; if (!inode->i_op) goto out_dput; if (inode->i_op->follow_link) { err = do_follow_link(&next, nd); if (err) goto return_err; err = -ENOENT; inode = nd->dentry->d_inode; if (!inode) break; err = -ENOTDIR; if (!inode->i_op) break; } else path_to_nameidata(&next, nd); err = -ENOTDIR; if (!inode->i_op->lookup) break; continue; /* here ends the main loop */ last_with_slashes: lookup_flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY; last_component: /* Clear LOOKUP_CONTINUE iff it was previously unset */ nd->flags &= lookup_flags | ~LOOKUP_CONTINUE; if (lookup_flags & LOOKUP_PARENT) goto lookup_parent; if (this.name[0] == '.') switch (this.len) { default: break; case 2: if (this.name[1] != '.') break; follow_dotdot(nd); inode = nd->dentry->d_inode; /* fallthrough */ case 1: goto return_reval; } if (nd->dentry->d_op && nd->dentry->d_op->d_hash) { err = nd->dentry->d_op->d_hash(nd->dentry, &this); if (err < 0) break; } err = do_lookup(nd, &this, &next, atomic); if (err) break; inode = next.dentry->d_inode; if ((lookup_flags & LOOKUP_FOLLOW) && inode && inode->i_op && inode->i_op->follow_link) { err = do_follow_link(&next, nd); if (err) goto return_err; inode = nd->dentry->d_inode; } else path_to_nameidata(&next, nd); err = -ENOENT; if (!inode) break; if (lookup_flags & LOOKUP_DIRECTORY) { err = -ENOTDIR; if (!inode->i_op || !inode->i_op->lookup) break; } goto return_base; lookup_parent: nd->last = this; nd->last_type = LAST_NORM; if (this.name[0] != '.') goto return_base; if (this.len == 1) nd->last_type = LAST_DOT; else if (this.len == 2 && this.name[1] == '.') nd->last_type = LAST_DOTDOT; else goto return_base; return_reval: /* * We bypassed the ordinary revalidation routines. * We may need to check the cached dentry for staleness. */ if (nd->dentry && nd->dentry->d_sb && (nd->dentry->d_sb->s_type->fs_flags & FS_REVAL_DOT)) { err = -ESTALE; /* Note: we do not d_invalidate() */ if (!nd->dentry->d_op->d_revalidate(nd->dentry, nd)) break; } return_base: return 0; out_dput: dput_path(&next, nd); break; } path_release(nd); return_err: return err; } 为了简单起见,我们首先描述当 LOOKUP_PARENT 未被设置且路径名不包含符号链接时, link_path_walk() 做些什么(非目录文件标准路径名查找)。接下来,我们讨论 LOOKUP_PARENT 被设置的情况:这种类型的查找在创建、删除或更名一个目录项时是需 要的,也就是在父目录名查找过程中是需要的。最后,我们阐明该函数如何解析符号链接。 当 LOOKUP_PARENT 标志被清零时,link_path_walk()执行下列步骤: 1. 用 nd->flags 和 nd->dentry->d_inode 分别初始化 lookup_flags 和 inode 局部变量。 unsigned int lookup_flags = nd->flags; inode = nd->dentry->d_inode; 2. 跳过路径名第一个分量前的任何斜杠( /)。 while (*name=='/') name++; 3. 如果剩余的路径名为空,则返回 0。在 nameidata 数据结构中, dentry 和 mnt 字段指向原 路径名最后一个所解析分量对应的对象。 4. 如果 nd 描述符中的 depth 字段的值为正(大于 0),则把 lookup_flags 局部变量置为 LOOKUP_FOLLOW 标志(这个跟符号链接查找相关)。 5. 执行一个循环,把 name 参数中传递的路径名分解为分量(中间的“ /”被当做文件名分 隔符对待);对于每个找到的分量,该函数: a) 检查存放到索引节点中的最近那个所解析分量的许可权是否允许执行(在 Unix 中,只 有目录是可执行的,它才可以被遍历)。执行 exec_permission_lite()函数,该函数检查存放在 索引节点 i_mode 字段的访问模式和运行进程的特权。在两种情况中,如果最近所解析分量 不允许执行,那么 link_path_walk()跳出循环并返回一个错误码: err = exec_permission_lite(inode, nd); if (err == -EAGAIN) err = vfs_permission(nd, MAY_EXEC); if (err) break; b) 考虑要解析的下一个分量。从它的名字,函数为目录项高速缓存散列表计算一个 32 位 的散列值: struct qstr this; this.name = name; c = *(const unsigned char *)name; hash = init_name_hash(); //就是返回 hash = 0 do { /* 这个 do 循环的目的就是计算 hash 值,仅此而已 */ name++; hash = partial_name_hash(c, hash); c = *(const unsigned char *)name; } while (c && (c != '/')); this.len = name - (const char *) this.name; this.hash = end_name_hash(hash); //就是返回 hash 注意,这里用到了目录项名字数据结构 qstr: struct qstr { unsigned int hash; unsigned int len; const unsigned char *name; }; 当前目录分量存放到了指向 qstr 结构的 this 内部变量中,即 this-name = "usr",而 name 的值 变成了/share/system-configure-ext2/test.conf。 其散列表的 32 位散列值如下计算: partial_name_hash(unsigned long c, unsigned long prevhash) { return (prevhash + (c << 4) + (c >> 4)) * 11; } c) 如果“/”终止了要解析的分量名,则跳过“ /”之后的任何尾部“ /”。 while (*++name == '/'); if (!*name) goto last_with_slashes; d) 如果要解析的分量是原路径名中的最后一个分量,则跳到第 6 步。 if (!c) goto last_component; e) 如果分量名是一个“ .”(单个圆点),则继续下一个分量(“ .”指的是当前目录,因此, 这个点在目录内没有什么效果)。如果分量名是“ ..”(两个圆点),则尝试回到父目录: if (this.name[0] == '.') switch (this.len) { default: break; case 2: if (this.name[1] != '.') break; follow_dotdot(nd); inode = nd->dentry->d_inode; /* fallthrough */ case 1: continue; } 这里面有个重要的 follow_dotdot(nd)函数: static __always_inline void follow_dotdot(struct nameidata *nd) { while(1) { struct vfsmount *parent; struct dentry *old = nd->dentry; read_lock(¤t->fs->lock); if (nd->dentry == current->fs->root && nd->mnt == current->fs->rootmnt) { read_unlock(¤t->fs->lock); break; } read_unlock(¤t->fs->lock); spin_lock(&dcache_lock); if (nd->dentry != nd->mnt->mnt_root) { nd->dentry = dget(nd->dentry->d_parent); spin_unlock(&dcache_lock); dput(old); break; } spin_unlock(&dcache_lock); spin_lock(&vfsmount_lock); parent = nd->mnt->mnt_parent; if (parent == nd->mnt) { spin_unlock(&vfsmount_lock); break; } mntget(parent); nd->dentry = dget(nd->mnt->mnt_mountpoint); spin_unlock(&vfsmount_lock); dput(old); mntput(nd->mnt); nd->mnt = parent; } follow_mount(&nd->mnt, &nd->dentry); } i. 如果最近解析的目录是进程的根目录( nd->dentry 等于 curren->fs->root,而 nd->mnt 等于 current->fs->rootmnt),那么再向上追踪是不允许的:在最近解析的分量上调用 follow_mount() (见下面),继续下一个分量。 ii. 如果最近解析的目录是 nd->mnt 文件系统的根目录( nd->dentry 等于 nd->mnt->mnt_root), 并且这个文件系统也没有被安装在其他文件系统之上( nd->mnt 等于 nd->mnt->mnt_parent), 那么 nd->mnt 文件系统通常就是命名空间的根文件系统:在这种情况下,再向上追踪是不可 能的,因此在最近解析的分量上调用 follow_mount()(参见下面),继续下一个分量。 iii. 如果最近解析的目录是 nd->mnt 文件系统的根目录,而这个文件系统被安装在其他文件 系统之上,那么就需要文件系统交换。因此,把 nd->dentry 置为 nd->mnt->mnt_mountpoint, 且把 nd->mnt 置为 nd->mnt->mnt_parent,然后重新开始第 5g 步(回想一下,几个文件系统 可以安装在同一个安装点上)。 iv. 如果最近解析的目录不是已安装文件系统的根目录,那么必须回到父目录:把 nd->dentry 置为 nd->dentry->d_parent,在父目录上调用 follow_mount(),继续下一个分量。 static void follow_mount(struct vfsmount **mnt, struct dentry **dentry) { while (d_mountpoint(*dentry)) { struct vfsmount *mounted = lookup_mnt(*mnt, *dentry); if (!mounted) break; dput(*dentry); mntput(*mnt); *mnt = mounted; *dentry = dget(mounted->mnt_root); } } follow_mount()函数检查 nd->dentry 是否是某文件系统的安装点( nd->dentry->d_mounted 的 值大于 0);如果是,则调用 lookup_mnt()搜索目录项高速缓存中已安装文件系统的根目录, 并把 nd->dentry 和 nd->mnt 更新为相应已安装文件系统的安装点和安装系统对象的地址;然 后重复整个操作(几个文件系统可以安装在同一个安装点上)。从本质上说,由于进程可能 从某个文件系统的目录开始路径名的查找,而该目录被另一个安装在其父目录上的文件系统 所隐藏,那么当需要回到父目录时,则调用 follow_mount()函数。 回到__link_path_walk 中,如果: f) 分量名既不是“ .”,也不是“ ..”,因此函数必须在目录项高速缓存中查找它。如果低级 文件系统有一个自定义的 d_hash 目录项方法,则调用它来修改已在第 5b 步计算出的散列值。 if (nd->dentry->d_op && nd->dentry->d_op->d_hash) { err = nd->dentry->d_op->d_hash(nd->dentry, &this); if (err < 0) break; } g) 调用 do_lookup (),得到与给定的父目录( nd->dentry)和文件名(要解析的路径名分 量&next 结果参数)相关的目录项对象,存放在结果参数 next 中: err = do_lookup(nd, &this, &next, atomic); 注意,这里又要介绍一个 path 数据结构,next 局部变量就是一个该结构: struct path { struct vfsmount *mnt; struct dentry *dentry; }; static int do_lookup(struct nameidata *nd, struct qstr *name, struct path *path, int atomic) { struct vfsmount *mnt = nd->mnt; struct dentry *dentry = __d_lookup(nd->dentry, name); if (!dentry) goto need_lookup; if (dentry->d_op && dentry->d_op->d_revalidate) goto need_revalidate; done: path->mnt = mnt; path->dentry = dentry; __follow_mount(path); return 0; need_lookup: if (atomic) return -EWOULDBLOCKIO; dentry = real_lookup(nd->dentry, name, nd); if (IS_ERR(dentry)) goto fail; goto done; need_revalidate: if (atomic) { dput(dentry); return -EWOULDBLOCKIO; } dentry = do_revalidate(dentry, nd); if (!dentry) goto need_lookup; if (IS_ERR(dentry)) goto fail; goto done; fail: return PTR_ERR(dentry); } 该函数本质上首先调用 __d_lookup()在目录项高速缓存中搜索分量的目录项对象: struct dentry * __d_lookup(struct dentry * parent, struct qstr * name) { unsigned int len = name->len; unsigned int hash = name->hash; const unsigned char *str = name->name; struct hlist_head *head = d_hash(parent,hash); struct dentry *found = NULL; struct hlist_node *node; struct dentry *dentry; rcu_read_lock(); hlist_for_each_entry_rcu(dentry, node, head, d_hash) { struct qstr *qstr; if (dentry->d_name.hash != hash) continue; if (dentry->d_parent != parent) continue; spin_lock(&dentry->d_lock); …… if (!d_unhashed(dentry)) { atomic_inc(&dentry->d_count); found = dentry; } spin_unlock(&dentry->d_lock); break; next: spin_unlock(&dentry->d_lock); } rcu_read_unlock(); return found; } 如果没有找到这样的目录项对象,则调用 real_lookup()。而 real_lookup()执行索引节点的 lookup 方法从磁盘读取目录,创建一个新的目录项对象并把它插入到目录项高速缓存中,然 后创建一个新的索引节点对象并把它插入到索引节点高速缓存中(在少数情况下,函数 real_lookup()可能发现所请求的索引节点已经在索引节点高速缓存中。路径名分量是最后一 个路径名而且不是指向一个目录,与路径名相应的文件有几个硬健接,并且最近通过与这个 路径名中被使用过的硬健接不同的硬链接访问过相应的文件)。在这一步结束时, next 局部 变量中的 dentry 和 mnt 字段将分别指向这次循环要解析的分量名的目录项对象和已安装文 件系统对象。 h) 调用 follow_mount()函数检查刚解析的分量( next.dentry)是否指向某个文件系统安装 点的一个目录( next.dentry->d_mounted 值大于 0)。follow_mount()更新 next.dentry 和 next.mnt 的值,以使它们指向由这个路径名分量所表示的目录上安装的最上层文件系统的目录项对象 和已安装文件系统对象(参见第 5f 步)。 i) 检查刚解析的分量是否指向一个符号链接(next.dentry->d_mode 具有一个自定义的 follow_link 方法)。我们将在后面的“符号链接的查找”章节中描述。 j) 把 nd->dentry 和 nd->mnt 分别置为 next.dentry 和 next.mnt,然后继续路径名的下一个分 量: path_to_nameidata(&next, nd); static inline void path_to_nameidata(struct path *path, struct nameidata *nd) { dput(nd->dentry); if (nd->mnt != path->mnt) mntput(nd->mnt); nd->mnt = path->mnt; nd->dentry = path->dentry; } k) 检查刚解析的分量是否指向一个目录( next.dentry->d_inode 具有一个自定义的 lookup 方法)。如果没有,返回一个错误码 -ENOTDIR,因为这个分量位于原路径名的中间,然后 continue 继续路径名的下一个分量: if (!inode->i_op->lookup) break; continue; 6 好啦,if (!c)的话,说明除了最后一个分量,原路径名的所有分量都被解析,即循环体的 qstr 结构的 this 指向最后一个分量( /test.conf),name 内部变量已经是 NULL 了,nd 指向的 dentry 对应 this 的前一个分量(/system-configure-ext2/test.conf),此时 goto last_component; 如果跳出循环的话,那么清除 nd->flags 中的 LOOKUP_CONTINUE 标志: nd->flags &= lookup_flags | ~LOOKUP_CONTINUE; 7 检查 lookup_flags 变量中 LOOKUP_PARENT 标志的值。下面假定这个标志被置为 0,并 把相反的情况推迟到 lookup_parent,下一章节介绍。 8 如果最后一个分量名是“ .”(单个圆点),则终止执行并返回值 0(无错误)。在 nd 指向的 nameidata 数据结构中,dentry 和 mnt 字段指向路径名中倒数第二个分量对应的对象 (/system-configure-ext2/test.conf,任何分量“.”在路径名中没有效果)。 9 如果最后一个分量名是“ ..”(两个圆点),则做跟第 5f 相似的工作,尝试回到父目录: a) 如果最后解析的目录是进程的根目录( nd->dentry 等于 current->fs->root,nd->mnt 等于 current->fs->rootmnt),则在倒数第二个分量上调用 follow_mount(),终止执行并返回值 0(无 错误)。 nd->dentry 和 nd->mnt 指向路径名的倒数第二个分量对应的对象,也就是进程的根 目录。 b) 如果最后解析的目录是 nd->mnt 文件系统的根目录( nd->dentry 等于 nd->mnt->mnt_root), 并且该文件系统没有被安装在另一个文件系统之上( nd->mnt 等于 nd->mnt->mnt_parent), 那么再向上搜索是不可能的,因此在倒数第二个分量上调用 follow_mount(),终止执行并返 回值 0(无错误)。 c) 如果最后解析的目录是 nd->mnt 文件系统的根目录,并且该文件系统被安装在其他文件系 统 之 上 , 那 么 把 nd->dentry 和 nd->mnt 分 别 置 为 nd->mnt->mnt_mountpoint 和 nd->mnt_mnt_parent,然后重新执行第 10 步。 d) 如果最后解析的目录不是已安装文件系统的根目录,则把 nd->dentry 置 为 nd->dentry->d_parent,在父目录上调用 follow_mount(),终止执行并返回值 0(无错误)。 nd->dentry 和 nd->mnt 指向前一个分量(即路径名倒数第二个分量)对应的对象。 10. 路径名的最后分量名既不是“ .”也不是“ ..”,因此,必须用 do_lookup 在高速缓存中查 找它。如果低级文件系统有自定义的 d_hash 目录项方法,则该函数调用它来修改在第 5c 步 已经计算出的散列值。 11. 检 查 在 lookup_flags 中 是 否 设 置 了 LOOKUP_FOLLOW 标 志 , 且 索 引节 点 对 象 next.dentry->d_inode 是否有一个自定义的 follow_link 方法。如果是,分量就是一个必须进 行解释的符号链接,这将在下一篇章节的“符号链接的查找”一节描述。 12. 要解析的分量不是一个符号链接或符号链接不该被解释。把 nd->mnt 和 nd->dentry 字段 分别置为 next.mnt 和 next.dentry 的值。最后的目录项对象就是整个查找操作的结果: path_to_nameidata(&next, nd); 13. 检查 nd->dentry->d_inode 是否为 NULL。这发生在没有索引节点与目录项对象关联时, 通常是因为路径名指向一个不存在的文件。在这种情况下,返回一个错误码 -ENOENT。 14. 路径名的最后一个分量有一个关联的索引节点。如果在 lookup_flags 中 设置了 LOOKUP_DIRECTORY 标志,则检查索引节点是否有一个自定义的 lookup 方法,也就是说 它是一个目录。如果没有,则返回一个错误码 -ENOTDIR。 if (lookup_flags & LOOKUP_DIRECTORY) { err = -ENOTDIR; if (!inode->i_op || !inode->i_op->lookup) break; } 15. 返回值 0(无错误)。 nd->dentry 和 nd->mnt 指向路径名的最后分量。 goto return_base; return_base: return 0; 2.4.2 父路径名查找 在很多情况下,查找操作的真正目的并不是路径名的最后一个分量,而是最后一个分量的前 一个分量。例如,当文件被创建时,最后一个分量表示还不存在的文件的文件名,而路径名 中的其余路径指定新链接必须插入的目录。因此,查找操作应当取回最后分量的前一个分量 的目录项对象。另举一个例子,我们执行 rm -f /foo/bar,这里路径名 /foo/bar 表示的文件 bar 拆分出来就包含从其目录 foo 中移去 bar。因此,内核真正的兴趣在于访问文件目录 foo 而 不是 bar。 当查找操作必须解析的是包含路径名最后一个分量的目录而不是最后一个分量本身时,就使 用 LOOKUP_PARENT 标志。 当 LOOKUP_PARENT 标志被设置时,link_path_walk()函数也在 nameidata 数据结构中建立 last 和 last_type 字段。last 字段存放路径名中的最后一个分量名,我们再来回忆一下 nameidate 的结构: struct nameidata { struct dentry *dentry; struct vfsmount *mnt; struct qstr last; unsigned int flags; int last_type; unsigned depth; char *saved_names[MAX_NESTED_LINKS + 1]; /* Intent data */ union { struct open_intent open; } intent; }; last_type 字段标识最后一个分量的类型;可以把它置为如下所示的值之一: LAST_NORM:最后一个分量是普通文件名 LAST_ROOT:最后一个分量是“/”(也就是整个路径名为“ /”) LAST_DOT:最后一个分量是“.” LAST_DOTDOT:最后一个分量是“..” LAST_BIND:最后一个分量是链接到特殊文件系统的符号链接 当整个路径名的查找操作开始时, LAST_ROOT 标志是由 path_lookup()设置的缺省值。如果 路径名正好是“ /”,则内核不改变 last_type 字段的初始值。 last_type 字段的其他值在 LOOKUP_PARENT 标志置位时由 link_path_walk()设置;在这种情 况下,函数执行上篇章节描述的步骤,直到第 7 步。不过,从第 7 步往后,即检查 lookup_flags 变量中 LOOKUP_PARENT 标志的值,如果被置位,则跳到 lookup_parent: lookup_parent: nd->last = this; nd->last_type = LAST_NORM; if (this.name[0] != '.') goto return_base; if (this.len == 1) nd->last_type = LAST_DOT; else if (this.len == 2 && this.name[1] == '.') nd->last_type = LAST_DOTDOT; else goto return_base; 1. 把 nd->last 置为最后一个分量名。 2. 把 nd->last_type 初始化为 LAST_NORM。 3. 如果最后一个分量名为“ .”(一个圆点),则把 nd->last_type 置为 LAST_DOT。 4. 如果最后一个分量名为“ ..”(两个圆点),则把 nd->last_type 置为 LAST_DOTDOT。 5. 通过返回值 0(无错误)终止。 你可以看到,最后一个分量根本就没有被解释。因此,当函数终止时, nameidata 数据结构 的 dentry 和 mnt 字段指向最后一个分量所在目录对应的对象。 2.4.3 符号链接的查找 回想一下,符号链接是一个普通文件,其中存放的是另一个文件的路径名。路径名可以包含 符号链接,且必须由内核来解析。 例如,如果 /foo/bar 是指向(包含路径名) ../dir 的一个符号链接,那么, /foo/bar/file 径名必 须由内核解析为对 /dir/file 文件的引用。在这个例子中,内核必须执行两个不同的查找操作。 第一个操作解析 /foo/bar:当内一核发现 bar 是一个符号链接名时,就必须提取它的内容并把 它解释为另一个路径名。第二个路径名操作从第一个操作所达到的目录开始,继续到符号链 接路径名的最后一个分量被解析。接下来,原来的查找操作从第二个操作所达到的目录项恢 复,且有了原目录名中紧随符号链接的分量。 对于更复杂的情景,含有符号链接的路径名可能包含其他的符号链接。你可能认为解析这类 符号链接的内核代码是相当难理解的,但并非如此;代码实际上是相当简单的,因为它是递 归的。 然而,难以驾驭的递归本质上是危险的。例如,假定一个符号链接指向自己。当然,解析含 有这样符号链接的路径名可能导致无休止的递归调用流,这又依次引发内核栈的溢出。当前 进程的描述符中的 link_count 字段用来避免这种问题:每次递归执行前增加这个字段的值, 执行之后减少其值。如果该字段的值达到 6,整个循环操作就以错误码结束。因此,符号链 接嵌套的层数不超过 5。 另外,当前进程的描述符中的 total_link_count 字段记录在原查找操作中有多少符号链接(甚 至非嵌套的)被跟踪。如果这个计数器的值到 40,则查找操作中止。没有这个计数器,怀 有恶意的用户就可能创建一个病态的路径名,让其中包含很多连续的符号链接,使内核在无休止的查找操作中冻结。 这就是代码基本工作的方式:一旦 link_path_walk()函数检索到与路径名分量相关的录项对 象,就检查相应的索引节点对象是否有自定义的 follow_link 方法(参见上一篇章节中的第 5k 步和第 11 步)。如果是,索引节点就是一个符号链接,在原路径名的查找操作进行之前 就必须先对这个符号链接进行解释。 在这种情况下, link_path_walk()函数调用 do_follow_link(),前者传递给后者的参数为符号链 接目录项对象的地址 dentry 和 nameidata 数据结构的地址 nd: static inline int do_follow_link(struct path *path, struct nameidata *nd) { int err = -ELOOP; if (current->link_count >= MAX_NESTED_LINKS) goto loop; if (current->total_link_count >= 40) goto loop; BUG_ON(nd->depth >= MAX_NESTED_LINKS); cond_resched(); err = security_inode_follow_link(path->dentry, nd); if (err) goto loop; current->link_count++; current->total_link_count++; nd->depth++; err = __do_follow_link(path, nd); current->link_count--; nd->depth--; return err; loop: dput_path(path, nd); path_release(nd); return err; } do_follow_link()依次执行下列步骤: 1. 检查 current->link_count 小于 MAX_NESTED_LINKS,一般来说是 5;否则,返回错误码 -ELOOP。 2. 检查 current->total_link_count 小于 40;否则,返回错误码 -ELOOP。 3. 如果当前进程需要,则调用 cond_resched()进行进程交换(设置当前进程描述符 thread_info 中的 TIF_NEED_RESCHED 标志)。 4. 递增 current->link_count、current->total_link_count 和 nd->depth 的值。 5. 更新与要解析的符号链接关联的索引节点的访问时间。 6. 调用与具体文件系统相关的函数来实现 follow_link 方法,给它传递的参数为 dentry 和 nd。 它读取存放在符号链接索引节点中的路径名,并把这个路径名保存在 nd->saved_names 数组 的合适项中。 7. 通过__do_follow_link 调用__vfs_follow_link()函数,给它传递的参数为地址 nd 和 nd->saved_names 数组中(参见下面)路径名的地址: static __always_inline int __do_follow_link(struct path *path, struct nameidata *nd) { int error; void *cookie; struct dentry *dentry = path->dentry; touch_atime(path->mnt, dentry); nd_set_link(nd, NULL); if (path->mnt != nd->mnt) { path_to_nameidata(path, nd); dget(dentry); } mntget(path->mnt); cookie = dentry->d_inode->i_op->follow_link(dentry, nd); error = PTR_ERR(cookie); if (!IS_ERR(cookie)) { char *s = nd_get_link(nd); error = 0; if (s) error = __vfs_follow_link(nd, s); if (dentry->d_inode->i_op->put_link) dentry->d_inode->i_op->put_link(dentry, nd, cookie); } dput(dentry); mntput(path->mnt); return error; } 8. 如果定义了索引节点对象的 put_link 方法,就执行它,释放由 follow_link 方法分配的临 时数据结构。 9. 减少 current->link_count 和 nd->depth 字段的值。 10. 返回由__vfs_follow_link()函数返回的错误码( 0 表示无错误): static __always_inline int __vfs_follow_link(struct nameidata *nd, const char *link) { int res = 0; char *name; if (IS_ERR(link)) goto fail; if (*link == '/') { path_release(nd); if (!walk_init_root(link, nd)) /* weird __emul_prefix() stuff did it */ goto out; } res = link_path_walk(link, nd); out: if (nd->depth || res || nd->last_type!=LAST_NORM) return res; /* * If it is an iterative symlinks resolution in open_namei() we * have to copy the last component. And all that crap because of * bloody create() on broken symlinks. Furrfu... */ name = __getname(); if (unlikely(!name)) { path_release(nd); return -ENOMEM; } strcpy(name, nd->last.name); nd->last.name = name; return 0; fail: path_release(nd); return PTR_ERR(link); } __vfs_follow_link()函数本质上依次执行下列操作: 1. 检查符号链接路径名的第一个字符是否是“ /”:在这种情况下,已经找到一个绝对路径名, 因此没有必要在内存中保留前一个路径的任何信息。如果是,对 nameidata 数据结构调用 path_release(),因此释放由前一个查找步骤产生的对象;然后,设置 nameidata 数据结构的 dentry 和 mnt 字段,以使它们指向当前进程的根目录。 2. 调用 link_path_walk()解析符号链的路径名,传递给它的参数为路径名和 nd。 3. 返回从 link_path_walk()取回的值。 当 do_follow_link()最后终止时,它把局部变量 next 的 dentry 字段设置为目录项对象的地址, 而这个地址由符号链接传递给原先就执行的 link_path_walk()。link_path_walk()函数然后进行 下一步。 2.5 VFS 系统调用的实现 至此,虚拟文件系统中主要的数据结构,以及最重要的 path_lookup()函数我们都讲完了,那 么就可以来讨论 VFS 的系统调用是如何实现的了。 当然,我们讨论所有的系统调用是不现实的,所以我们不打算对所有 VFS 系统调用的实现 进行讨论,这里只讲讲“开、读、写、关”四大 VFS 系统调用,而且是粗略的流程,请重 点关注 VFS 的数据结构怎样互相用。至于其中的细节,由于涉及到页缓存、回收页框、 mmap、 块设备驱动等,别急,后面的章节会来个详细分解的。 我们重新考虑一下在本单元开始所提到的例子,用户发出了一条 shell 命令:把 /floppy/TEST 中的 MS-DOS 文件拷贝到 /tmp/test 中的 Ext2 文件中。命令 shell 调用一个外部程序(如 cp), 我们假定 cp 执行下列代码片段: inf = open("/floppy/TEST", O_RDONLY, 0); outf = open("/tmp/test", O_WRONLY | O_CREAT | O_TRUNC, 0600); do { len = read(inf, buf, 4096); write(outf, buf, len); } while (len); close(outf); close(inf); 实际上,真正的 cp 程序的代码要更复杂些,因为它还必须检查由每个系统调用返回的可能 的出错码。在我们的例子中,我们只把注意力集中在拷贝操作的“正常”行为上。 2.5.1 open()系统调用 open()系统调用的服务例程为 sys_open()函数,该函数接收的参数为:要打开文件的路径名 filename,访问模式的一些标志 flags,以及如果该文件被创建所需要的许可权位掩码 mode。 如果该系统调用成功,就返回一个文件描述符,也就是指向文件对象的指针数组 current->files->fd 中分配给新文件的索引;否则,返回 -1。 在我们的例子中, open()被调用两次;第一次是为读( O_RDONLY 标志)而打开 /floppy/TEST,第二次是为写( O_WRONLY 标志)而打开 /tmp/test。如果/tmp/test 不存在,则该文件被创建 (O_CREAT 标志),文件主对该文件具有独占的读写访问权限(在第三个参数中的八进制 数 0600)。 相反,如果该文件已经存在,则从头开始重写它( O_TRUNC 标志)。下面列出了 open()系 统调用的所有标志: O_RDONLY:为读而打开 O_WRONLY:为写而打开 O_RDWR:为读和写而打开 O_CREAT:如果文件不存在,就创建它 O_EXCL:对于 O_CREAT 标志,如果文件已经存在,则失败 O_NOCTTY:从不把文件看作控制终端 O_TRUNC:截断文件(删除所有现有的内容) O_APPEND:总是在文件末尾写 O_NONBLOCK:没有系统调用在文件上阻塞 O_NDELAY:与 O_NONBLOCK 相同 O_SYNC:同步写(阻塞,直到物理写终止) FASYNC:通过信号发出 I/O 事件通告 O_DIRECT:直接 I/O 传送(不使用缓存) O_LARGEFILE:大型文件(长度大于 2GB) O_DIRECTORY:如果文件不是一个目录,则失败 O_NOFOLLOW:不解释路径名中尾部的符号链接 O_NOATIME:不更新索引节点的上次访问时间 下面来描述一下 sys_open()函数的操作。它执行如下操作: 1. 调用 getname()从进程地址空间读取该文件的路径名。 2. 调用 get_unused_fd()在 current->files->fd 中查找一个空的位置。相应的索引(新文件描述 符)存放在 fd 局部变量中。 int get_unused_fd(void) { struct files_struct * files = current->files; int fd, error; struct fdtable *fdt; error = -EMFILE; spin_lock(&files->file_lock); repeat: fdt = files_fdtable(files); fd = find_next_zero_bit(fdt->open_fds->fds_bits, fdt->max_fdset, files->next_fd); /* * N.B. For clone tasks sharing a files structure, this test * will limit the total number of files that can be opened. */ if (fd >= current->signal->rlim[RLIMIT_NOFILE].rlim_cur) goto out; /* Do we need to expand the fd array or fd set? */ error = expand_files(files, fd); if (error < 0) goto out; if (error) { /* * If we needed to expand the fs array we * might have blocked - try again. */ error = -EMFILE; goto repeat; } FD_SET(fd, fdt->open_fds); FD_CLR(fd, fdt->close_on_exec); files->next_fd = fd + 1; #if 1 /* Sanity check */ if (fdt->fd[fd] != NULL) { printk(KERN_WARNING "get_unused_fd: slot %d not NULL!\n", fd); fdt->fd[fd] = NULL; } #endif error = fd; out: spin_unlock(&files->file_lock); return error; } 3. 调用 do_filp_open()函数,传递给它的参数为路径名、访问模式标志以及许可权位掩码: static struct file *do_filp_open(int dfd, const char *filename, int flags, int mode) { int namei_flags, error; struct nameidata nd; /* 把访问模式标志拷贝到 namei_flags 标志中,但是,用特殊的格式对访问模式标志。 * O_RDONLY、O_WRONLY 和 O_RDWR 进行编码:如果文件访问需要读特权, * 那么只设置 namei_flags 标志的下标为 0 的位(最低位); * 类似地,如果文件访问需要写特权,就只设置下标为 1 的位。 * 注意,不可能在 open()系统调用中不指定文件访问的读或写特权; * 不过,这种情况在涉及符号链接的路径名查找中则是有意义的。 */ namei_flags = flags; if ((namei_flags+1) & O_ACCMODE) namei_flags++; /* 调用 open_namei(),传递给它的参数为 dfd(AT_FDCWD)、路径名、修改的访问模式标 志以及局部 nameidata 数据结构的地址。 */ error = open_namei(dfd, filename, namei_flags, mode, &nd); if (!error) return nameidata_to_filp(&nd, flags); return ERR_PTR(error); } 这个函数会调用到一个 open_namei,这个函数执行以下流程: 如果访问模式标志中没有设置 O_CREAT,则不设置 LOOKUP_PARENT 标志而设置 LOOKUP_OPEN 标志后开始查找操作。此外,只有 O_NOFOLLOW 被清零,才设置 LOOKUP_FOLLOW 标 志 , 而 只 有 设 置 了 O_ DIRECTORY 标 志 , 才 设 置 LOOKUP_DIRECTORY 标志。 如果在访问模式标志中设置了 O_CREAT,则以 LOOKUP_PARENT、LOOKUP_OPEN 和 LOOKUP_CREATE 标志的设置开始查找操作。一旦 path_lookup()函数成功返回,则检查请 求的文件是否已存在。如果不存在,则调用父索引节点的 create 方法分配一个新的磁盘索引 节点。 open_namei()函数也在查找操作确定的文件上执行几个安全检查。例如,该函数检查与已找 到的目录项对象关联的索引节点是否存在、它是否是一个普通文件,以及是否允许当前进程 根据访问模式标志访问它。如果文件也是为写打开的,则该函数检查文件是否被其他进程加 锁。 最后,open_namei 调用 path_lookup_open 或 path_lookup_create 把 filename 对应的目录项存 放在局部 nameidata 数据结构 nd 中,这两个函数都会调用到 __path_lookup_intent_open: static int __path_lookup_intent_open(int dfd, const char *name, unsigned int lookup_flags, struct nameidata *nd, int open_flags, int create_mode) { /* 分配一个 file 对象,并初始化一些字段,如权限等 */ struct file *filp = get_empty_filp(); int err; if (filp == NULL) return -ENFILE; nd->intent.open.file = filp; nd->intent.open.flags = open_flags; nd->intent.open.create_mode = create_mode; err = do_path_lookup(dfd, name, lookup_flags|LOOKUP_OPEN, nd); if (IS_ERR(nd->intent.open.file)) { if (err == 0) { err = PTR_ERR(nd->intent.open.file); path_release(nd); } } else if (err != 0) release_open_intent(nd); return err; } struct file *get_empty_filp(void) { struct task_struct *tsk; static int old_max; struct file * f; /* * Privileged users can go above max_files */ if (get_nr_files() >= files_stat.max_files && !capable(CAP_SYS_ADMIN)) { /* * percpu_counters are inaccurate. Do an expensive check before * we go and fail. */ if (percpu_counter_sum(&nr_files) >= files_stat.max_files) goto over; } f = kmem_cache_alloc(filp_cachep, GFP_KERNEL); if (f == NULL) goto fail; percpu_counter_inc(&nr_files); memset(f, 0, sizeof(*f)); if (security_file_alloc(f)) goto fail_sec; tsk = current; INIT_LIST_HEAD(&f->f_u.fu_list); atomic_set(&f->f_count, 1); rwlock_init(&f->f_owner.lock); f->f_uid = tsk->fsuid; f->f_gid = tsk->fsgid; eventpoll_init_file(f); /* f->f_version: 0 */ return f; over: /* Ran out of filps - report that */ if (get_nr_files() > old_max) { printk(KERN_INFO "VFS: file-max limit %d reached\n", get_max_files()); old_max = get_nr_files(); } goto fail; fail_sec: file_free(f); fail: return NULL; } 在调用 open_namei 后,do_filp_open 调用 nameidata_to_filp(&nd, flags)来将 nd 转换成 file 对 象: struct file *nameidata_to_filp(struct nameidata *nd, int flags) { struct file *filp; /* Pick up the filp from the open intent */ filp = nd->intent.open.file; /* Has the filesystem initialised the file for us? */ if (filp->f_dentry == NULL) filp = __dentry_open(nd->dentry, nd->mnt, flags, filp, NULL); else path_release(nd); return filp; } 注意,文件对象不需要在 *nameidata_to_filp 分 配 , 因 为 在 open_namei 中 , 执 行 到 __path_lookup_intent_open 之后已经分配了,由 nd->intent.open.file 指向。 nameidata_to_filp 会调用__dentry_open()函数,传递给它的参数为访问模式标志、目录项对 象的地址以及由查找操作确定的已安装文件系统对象: static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags, struct file *f, int (*open)(struct inode *, struct file *)) { struct inode *inode; int error; f->f_flags = flags; f->f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE; inode = dentry->d_inode; if (f->f_mode & FMODE_WRITE) { error = get_write_access(inode); if (error) goto cleanup_file; } f->f_mapping = inode->i_mapping; f->f_dentry = dentry; f->f_vfsmnt = mnt; f->f_pos = 0; f->f_op = fops_get(inode->i_fop); file_move(f, &inode->i_sb->s_files); if (!open && f->f_op) open = f->f_op->open; if (open) { error = open(inode, f); if (error) goto cleanup_all; } f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC); file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping); /* NB: we're sure to have correct a_ops only after f_op->open */ if (f->f_flags & O_DIRECT) { if (!f->f_mapping->a_ops || ((!f->f_mapping->a_ops->direct_IO) && (!f->f_mapping->a_ops->get_xip_page))) { fput(f); f = ERR_PTR(-EINVAL); } } return f; cleanup_all: fops_put(f->f_op); if (f->f_mode & FMODE_WRITE) put_write_access(inode); file_kill(f); f->f_dentry = NULL; f->f_vfsmnt = NULL; cleanup_file: put_filp(f); dput(dentry); mntput(mnt); return ERR_PTR(error); } 该函数依次执行下列操作: i. 根据传递给 open()系统调用的访问模式标志初始化文件对象的 f_flags 和 f_mode 字段。 ii. 根据作为参数传递来的目录项对象的地址和已安装文件系统对象的地址初始化文件对象 的 f_fentry 和 f_vfsmnt 字段。 iii. 重点步骤:把 f_op 字段设置为相应索引节点对象 i_fop 字段的内容。这就为进一步的文 件操作建立起所有的方法。 iv. 把文件对象插入到文件系统超级块的 s_files 字段所指向的打开文件的链表。 v. 如果文件操作的 open 方法被定义,则调用它。 vi. 调用 file_ra_state_init()初始化预读的数据结构(参见第十六章)。 vii. 如果O_DIRECT标志被设置,则检查直接 I/O操作是否可以作用于文件(参见第十六章)。 viii. 返回文件对象的地址。 最后*do_filp_open 返回文体对象的地址。 4. 回到 do_sys_open,把 current->files->fd[fd]置为由 dentry_open()返回的文件对象的地址: fsnotify_open(f->f_dentry); fd_install(fd, f); void fastcall fd_install(unsigned int fd, struct file * file) { struct files_struct *files = current->files; struct fdtable *fdt; spin_lock(&files->file_lock); fdt = files_fdtable(files); BUG_ON(fdt->fd[fd] != NULL); rcu_assign_pointer(fdt->fd[fd], file); spin_unlock(&files->file_lock); } 5. 返回 fd。 2.5.2 read()和 write()系统调用 让我们再回到 cp 例子的代码。open()系统调用返回两个文件描述符,分别存放在 inf 和 outf 变量中。然后,程序开始循环。在每次循环中, /floppy/TEST 文件的一部分被拷贝到本地缓 冲区( read ()系统调用)中,然后,这个本地缓冲区中的数据又被拷贝到 /tmp/test 文件( write() 系统调用)。 read()和 write()系统调用非常相似。它们都需要三个参数:一个文件描述符 fd,个内存区的 地址 buf(该缓冲区包含要传送的数据),以及一个数 count(指定应该传送多少字节)。当 然,read()把数据从文件传送到缓冲区,而 write()执行相反的操作。两个系统调用都返回所 成功传送的字节数,或者发送一个错误条件的信号并返回 -1。 返回值小于 count 并不意味着发生了错误。即使请求的字节没有都被传送,也总是允许内核 终止系统调用,因此用户应用程序必须检查返回值并重新发出系统调用(如果必要)。 一般会有这几种典型情况下返回小于 count 的值:当从管道或终端设备读取时,当读到文件 的末尾时,或者当系统调用被信号中断时。文件结束条件( EOF)很容易从 read()的空返回 值中判断出来。这个条件不会与因信号引起的异常终止混淆在一起,因为如果读取数据之前 read()被一个信号中断,则发生一个错误。 读或写操作总是发生在由当前文件指针所指定的文件偏移处(文件对象的 f_pos 字段)。两 个系统调用都通过把所传送的字节数加到文件指针上而更新文件指针。 简而言之, sys_read()(read()的服务例程)和 sys_write()(write())的服务例程)几乎都执行 相同的步骤: 1. 调用 fget_light()从 fd 获取当前进程相应文件对象的地址 file: struct file fastcall *fget_light(unsigned int fd, int *fput_needed) { struct file *file; struct files_struct *files = current->files; …… file = fcheck_files(files, fd); …… return file; } static inline struct file * fcheck_files(struct files_struct *files, unsigned int fd) { struct file * file = NULL; struct fdtable *fdt = files_fdtable(files); if (fd < fdt->max_fds) file = rcu_dereference(fdt->fd[fd]); return file; } 2. 如果 file->f_mode 中的标志不允许所请求的访问(读或写操作),则返回一个错误码 -EBADF。 3. 如果文件对象没有 read()或 aio_read()(write()或 aio_write())文件操作,则返回一个错误 码-EINVAL。 4. 调用 access_ok()粗略地检查 buf 和 count 参数。 5. 调用 rw_verify_area()对要访问的文件部分检查是否有冲突的强制锁。如果有,则返回一 个错误码,如果该锁已经被 F_SETLKW 命令请求,那么就挂起当前进程。 6. 调用 file->f_op->read 或 file->f_op->write 方法(如果已定义)来传送数据;否则,调用 file->f_op->aio_read 或 file->f_op->aio_write 方法。所有这些方法都返回实际传送的字节数。 另一方面的作用是,文件指针被适当地更新。 7. 调用 fput_light()释放文件对象。 8. 返回实际传送的字节数。 2.5.3 close()系统调用 在我们例子的代码中,循环结束发生在 read()系统调用返回 0 时,也就是说,发生在 /floppy/TEST 中所有自己被拷贝到/tmp/test 中时。然后,程序关闭打开的文件,这是因为拷 贝操作已经完成。 close()系统调用接收的参数为要关闭文件的文件描述符 fd。sys_close()服务例程执行下列操 作: 1. 获得存放在 current->files->fd[fd]中的文件对象 file 的地址;如果它为 NULL,则返回一个 出错码。 2. 把 current->files->fd[fd]置为 NULL。释放文件描述符 fd,这是通过清除 current->files 中 的 open_fds 和 close_on_exec 字段的相应位来进行的。 3. 调用 file_close(),该函数执行下列操作: a) 调用文件操作的 flush 方法(如果已定义)。 b) 释放文件上的任何强制锁。 c) 调用 fput()释放文件对象。 4. 返回 0 或一个出错码。出错码可由 flush 方法或文件中的前一个写操作错误产生。 3 第二扩展文件系统 前面对虚拟文件系统 VFS 的讨论就告一段落了, VFS 主要是提供一个系统调用接口,然后 将相关文件系统对象与具体的文件系统串联起来。从本章开始,我们将选择一个具体的文件 系统进行研究,这个文件系统就是第二扩展文件系统( Ext2)。为啥要特别学习这个东西呢? 因为 ext2 是 Linux 所固有的,事实上已在每个 Linux 系统中得以使用,因此我们自然要对 Ext2 进行讨论。此外, Ext2 在对现代文件系统的高性能支持方面也显示出很多良好的实践 性。固然,Linux 支持的其他文件系统有很多有趣的特点,但比较扩展文件系统是 Linux 中 的老大,头头,所以我们并不对其他文件系统一一讨论了。 类 Unix 操作系统使用多种文件系统。尽管所有这些文件系统都有少数 POSIX API(如 state()) 所需的共同的属性子集,但每种文件系统的实现方式是不同的。 Linux 的第一个版本是基于 MINIX 文件系统的。当 Linux 成熟时,引入了扩展文件系统 (Extended Filesystem, Ext FS),它包含了几个重要的扩展但提供的性能不令人满意。在 1994 年引入了第二扩展文件系统(Ext2);它除了包含几个新的特点外,还相当高性能和稳定, Ext2 及它的下一代文件系统 Ext3、e3fs 等已成为广泛使用的 Linux 文件系统。 Ext2 文件系统具有以下一般特点: 1、当创建 Ext2 文件系统时,系统管理员可以根据预期的文件平均长度来选择最佳的块大小 (从 1024B—— 4096B)。例如,当文件的平均长度小于几千字节时,块的大小为 1024B 是 最佳的,因为这会产生较少的内部碎片——也就是文件长度与存放块的磁盘分区有较少的不 匹配。另一方面,大的块对于大于几千字节的文件通常比较合合适,因为这样的磁盘传送较 少,因而减轻了系统的开销。 2、当创建 Ext2 文件系统时,系统管理员可以根据在给定大小的分区上预计存放的文件数来 选择给该分区分配多少个索引节点。这可以有效地利用磁盘的空间。 3、文件系统把磁盘块分为组。每组包含存放在相邻磁道上的数据块和索引节点。正是这种 结构,使得可以用较少的磁盘平均寻道时间对存放在一个单独块组中的文件并行访问。 4、在磁盘数据块被实际使用之前,文件系统就把这些块预分配给普通文件。因此当文件的 大小增加时,因为物理上相邻的几个块已被保留,这就减少了文件的碎片。 5、支持快速符号链接。如果符号链接表示一个短路径名(小于或等于 60 个字符),就把它 存放在索引节点中而不用通过由一个数据块进行转换。 此外,Ext2 还包含了一些使它既健壮又灵活的特点: 1、文件更新策略的谨慎实现将系统崩溃的影响减到最少。我们只举一个例子来体现这个优点:例如,当给文件创建一个硬链接时,首先增加磁盘索引节点中的硬链接计数器,然后把 这个新的名字加到合适的目录中。在这种方式下,如果在更新索引节点后而改变这个目录之 前出现一个硬件故障,这样即使索引节点的计数器产生错误,但目录是一致的。因此,尽管 删除文件时无法自动收回文件的数据块,但并不导致灾难性的后果。如果这种处理的顺序相 反(更新索引节点前改变目录),同样的硬件故障将会导致危险的不一致,删除原始的硬链 接就会从磁盘删除它的数据块,但新的目录项将指向一个不存在的索引节点。如果那个索引 节点号以后又被另外的文件所使用,那么向这个旧目录的写操作将毁坏这个新的文件。 2、在启动时支持对文件系统的状态进行自动的一致性检查。这种检查是由外部程序 e2fsck 完成的,这个外部程序不仅可以在系统崩溃之后被激活,也可以在一个预定义的文件系统安 装数(每次安装操作之后对计数器加 1)之后被激活,或者在自从最近检查以来所花的预定 义时间之后被激活。 3、支持不可变( immutable)的文件(不能修改、删除和更名)和仅追加( append-only)的 文件(只能把数据追加在文件尾)。 4、既与 Unix System V Release 4(SVR4)相兼容,也与新文件的用户组 ID 的 BSD 语义相 兼容。在 SVR4 中,新文件采用创建它的进程的用户组 ID;而在 BSD 中,新文件继承包含 它的目录的用户组 ID。Ext2 包含一个安装选项,由你指定采用哪种语义。 即使 Ext2 文件系统是如此成熟、稳定的程序,也还要考虑引入另外几个负面特性。目前, 一些负面特性已新的文件系统或外部补丁避免了。另外一些还仅仅处于计划阶段,但在一些 情况下,已经在 Ext2 的索引节点中为这些特性引入新的字段。最重要的一些特点如下: 块片(block fragmentation) 系统管理员对磁盘的访问通常选择较大的块,因为计算机应用程序常常处理大文件。因 此,在大块上存放小文件就会浪费很多磁盘空间。这个问题可以通过把几个文件存放在同一 块的不同片上来解决。 透明地处理压缩和加密文件 这些新的选项(创建一个文件时必须指定)将允许用户透明地在磁盘上存放压缩和(或) 加密的文件版本。 逻辑删除 一个 undelete 选项将允许用户在必要时很容易恢复以前已删除的文件内容。 日志 日志避免文件系统在被突然卸载(例如,作为系统崩溃的后果)时对其自动进行的耗时 检查。 实际上,这些特点没有一个正式地包含在 Ext2 文件系统中。有人可能说 Ext2 是这种成功的 牺牲品;直到几年前,它仍然是大多数 Linux 发布公司采用的首选文件系统,每天有成千上 万的用户在使用它,这些用户会对用其他文件系统来代替 Ext2 的任何企图产生质疑。 Ext2 中缺少的最突出的功能就是日志,日志是高可用服务器必需的功能。为了平顺过渡, 日志没有引入到 Ext2 文件系统;但是,完全与 Ext2 兼容的一种新文件系统 Ext3 文件系统 已经创建,这种文件系统提供了日志。不真正需要日志的用户可以继续使用良好而老式的 Ext2 文件系统,而其他用户可能采用这种新的文件系统。现在发行的大部分系统采用 Ext3 作为标准的文件系统。 3.1 Ext2 磁盘数据结构 要学习 ext2 文件系统,首先要学习 ext2 的磁盘设计,也就是说, ext2 将一个磁盘分区格式 化成一个什么样子。本篇章节就来介绍一下一个 ext2 文件系统的磁盘分区布局,重点关注 它的数据结构。 任何 Ext2 分区中的第一个块从不受 Ext2 文件系统的管理,因为这一块是为分区的引导扇区 所保留的。Ext2 分区的其余部分分成块组( block group),每个块组的分布图如图所示。正 如你从图中所看到的,一些数据结构正好可以放在一块中,而另一些可能需要更多的块。在 Ext2 文件系统中的所有块组大小相同并被顺序存放,因此,内核可以从块组的整数索引很 容易地得到磁盘中一个块组的位置: 由于内核尽可能地把属于同一个文件的数据块存放在同一块组中,所以块组减少了文,碎片。 块组中的每个块包含下列信息之一: - 文件系统的超级块的一个拷贝 - 一组块组描述符的拷贝 - 一个数据块位图 - 一个索引节点位图 - 一个索引节表 - 属干文件的一大块数据,即数据块 如果一个块中不包含任何有意义的信息,就说这个块是空闲的。从上图中可以看出,超级块与组描述符被复制到每个块组中。 其实呢,只有块组 0 中所包含超级块和组描述符才由内核使用,而其余的超级块和组描述符 都保持不变;事实上,内核甚至不考虑它们。当 e2fsck 程序对 Ext2 文件系统的状态执行一 致性检查时,就引用存放在块组 0 中的超级块和组描述符,然后把它们拷贝到其他所有的块 组中。如果出现数据损坏,并且块组 0 中的主超级块和主描述符变为无效,那么,系统管理 员就可以命令 e2fsck 引用存放在某个块组(除了第一个块组)中的超级块和组描述符的旧 拷贝。通常情况下,这些多余的拷贝所存放的信息足以让 e2fsck 把 Ext2 分区带回到一个一 致的状态。 那么有多少块组呢?这取决于分区的大小和块的大小。其主要限制在于块位图,因为块位图 必须存放在一个单独的块中。块位图用来标识一个组中块的占用和空闲状况。所以,每组中 至多可以有 8×b 个块,b 是以字节为单位的块大小。例如,一个块大学是 1024 Byte,那么, 一个块的位图就有 8192 个位,正好就对应 8192 个块。 因此,块组的总数大约是 c/8×b,这里:是指分区所包含的总块组数。 举例说明,让我们考虑一下 32GB 的 Ext2 分区,换算成 KB 就是 33554432,块的大小为 4KB。 在这种情况下,每个 4KB 的块位图描述 32KB 个数据块,即 128MB。因此,最多需要 33554432 / 4096 * 32 = 256 个块组。显然,块越大,块组数越小。 3.1.1 磁盘超级块 Ext2 在磁盘上的超级块存放在一个 ext2_super_block 结构中,它的字段在下面列出(为了确 保 Ext2 和 Ext3 文件系统之间的兼容性, ext2_super_block 数据结构包含了一些 Ext3 特有的 字段,我们省略号省了): struct ext2_super_block { __le32 s_inodes_count; /* 索引节点的总数 */ __le32 s_blocks_count; /* 块总数(所有的块) */ __le32 s_r_blocks_count; /* 保留的块数 */ __le32 s_free_blocks_count; /* 空闲块数 */ __le32 s_free_inodes_count; /* 空闲索引节点数 */ __le32 s_first_data_block; /* 第一个使用的块号(总为 1) */ __le32 s_log_block_size; /* 块的大小 */ __le32 s_log_frag_size; /* 片的大小 */ __le32 s_blocks_per_group; /* 每组中的块数 */ __le32 s_frags_per_group; /* 每组中的片数 */ __le32 s_inodes_per_group; /* 每组中的索引节点数 */ __le32 s_mtime; /* 最后一次安装操作的时间 */ __le32 s_wtime; /* 最后一次写操作的时间 */ __le16 s_mnt_count; /* 被执行安装操作的次数 */ __le16 s_max_mnt_count; /* 检查之前安装操作的次数 */ __le16 s_magic; /* 魔术签名 */ __le16 s_state; /* 文件系统状态标志 */ __le16 s_errors; /* 当检测到错误时的行为 */ __le16 s_minor_rev_level; /* 次版本号 */ __le32 s_lastcheck; /* 最后一次检查的时间 */ __le32 s_checkinterval; /* 两次检查之间的时间间隔 */ __le32 s_creator_os; /* 创建文件系统的操作系统 */ __le32 s_rev_level; /* 主版本号 */ __le16 s_def_resuid; /* 保留块的缺省 UID */ __le16 s_def_resgid; /* 保留块的缺省用户组 ID */ /* * These fields are for EXT2_DYNAMIC_REV superblocks only. * * Note: the difference between the compatible feature set and * the incompatible feature set is that if there is a bit set * in the incompatible feature set that the kernel doesn't * know about, it should refuse to mount the filesystem. * * e2fsck's requirements are more strict; if it doesn't know * about a feature in either the compatible or incompatible * feature set, it must abort and not try to meddle with * things it doesn't understand... */ __le32 s_first_ino; /* 第一个非保留的索引节点号 */ __le16 s_inode_size; /* 磁盘上索引节点结构的大小 */ __le16 s_block_group_nr; /* 这个超级块的块组号 */ __le32 s_feature_compat; /* 具有兼容特点的位图 */ __le32 s_feature_incompat; /* 具有非兼容特点的位图 */ __le32 s_feature_ro_compat; /* 只读兼容特点的位图 */ __u8 s_uuid[16]; /* 128 位文件系统标识符 */ char s_volume_name[16]; /* 卷名 */ char s_last_mounted[64]; /* 最后一个安装点的路径名 */ __le32 s_algorithm_usage_bitmap; /* 用于压缩 */ /* * Performance hints. Directory preallocation should only * happen if the EXT2_COMPAT_PREALLOC flag is on. */ __u8 s_prealloc_blocks; /* 预分配的块数 */ __u8 s_prealloc_dir_blocks; /* 为目录预分配的块数 */ __u16 s_padding1; /* 按字对齐 */ …… __u32 s_reserved[190]; /* 用 null 填充 1024 字节 */ }; __u8、__u16 及__u32 数据类型分别表示长度为 8、16 及 32 位的无符号数,而 __s8、__s16 及__s32 数据类型表示长度为 8、16 及 32 位的有符号数。为清晰地表示磁盘上字或双字中 字节的存放顺序,内核又使用了 __le16、__le32、__be16 和__be32 数据类型,前两种类型分 别表示字或双字的“小尾( little-endian )”排序方式(低阶字节在高位地址),而后两种类型 分别表示字或双字的“大尾( big-endian )”排序方式(高阶字节在高位地址): typedef __signed__ char __s8; typedef unsigned char __u8; typedef __signed__ short __s16; typedef unsigned short __u16; typedef __signed__ int __s32; typedef unsigned int __u32; typedef __signed__ long long __s64; typedef unsigned long long __u64; typedef __u16 __bitwise __le16; typedef __u16 __bitwise __be16; typedef __u32 __bitwise __le32; typedef __u32 __bitwise __be32; typedef __u64 __bitwise __le64; typedef __u64 __bitwise __be64; 有细心的朋友可以数一数这些 u 或者 le 后面的数字,然后加起来后除以 8,可以发现刚刚小 于 512。不错, 512B 正是最小的块大小,再小一点一个块就装不下 ext2_super_block 了。所 有我推断,为什么这里要用这些 u、s、或者 le 然后后面加数字的形式,就是方便开发者避 免超过一个块大小而看起来方便一点的原因。 s_inodes_count 字段存放索引节点的个数,而 s_blocks_count 字段存放 Ext2 文件系统的块的 个数。 s_log_block_size 字段以 2 的幂次方表示块的大小,用 1024 字节作为单位。因此, 0 表示 1024 字节的块,1 表示 2048 字节的块,如此等等。目前 s_log_frag_size 字段与 slog_block_size 字段相等,因为块片还没有实现。 s_blocks_per_group、s_frags_per_group 与 s_inodes_per_group 字段分别存放每个块组中的块 数、片数及索引节点数。 一些磁盘块保留给超级用户(或由 s_def_resuid 和 s_def_resgid 字段挑选给某一其他用户或 用户组)。即使当普通用户没有空闲块可用时,系统管理员也可以用这些块继续使用 Ext2 文 件系统。 s_mnt_count、s_max_mnt_count、s_lastcheck 及 s_checkinterval 字段使系统启动时自动地检查 Ext2 文件系统。在预定义的安装操作数完成之后,或自最后一次一致性检查以来预定义 的时间已经用完,这些字段就导致 e2fsck 执行(两种检查可以一起进行)。如果 Ext2 文件系 统还没有被全部卸载(例如系统崩溃以后),或内核在其中发现一些错误,则一致性检查在 启动时要强制进行。如果 Ext2 文件系统被安装或未被全部卸载,则 s_state 字段存放的值为 0;如果被正常卸载,则这个字段的值为 1;如果包含错误,则值为 2。 3.1.2 组描述符和位图 每个块组都有自己的组描述符,它是一个 ext2_group_desc 结构: struct ext2_group_desc { __le32 bg_block_bitmap; /* 块位图的块号 */ __le32 bg_inode_bitmap; /* 索引节点位图的块号 */ __le32 bg_inode_table; /* 第一个索引节点表块的块号 */ __le16 bg_free_blocks_count; /* 组中空闲块的个数 */ __le16 bg_free_inodes_count; /* 组中空闲索引节点的个数 */ __le16 bg_used_dirs_count; /* 组中目录的个数 */ __le16 bg_pad; /* 按字对齐 */ __le32 bg_reserved[3]; /* 用 null 填充 24 个字节 */ }; 注意,每个块组都有 n 个块组描述符的拷贝, n 等于块的个数,也就是刚才例子中的 256 个 块组。没看懂的再仔细回忆回忆前面的那个块图。 当分配新索引节点和数据块时,会用到 bg_free_blocks_count、bg_free_inodes_count、和 bg_used_dirs_count 字段。这些字段确定在最合适的块中给每个数据结构进行分配位图中位 的序列,其中值 0 表示对应的索引节点块或数据块是空闲的, 1 表示占用。因为每个位图必 须存放在一个单独的块中,又因为块的大小可以是 1024、2048 或 4096 字节,因此,一个单 独的位图描述 8192、16384 或 32768 个块的状态。 3.1.3 磁盘索引节点表 索引节点表由一连串连续的块组成,其中每一块包含索引节点的一个预定义号。索引点表第 一个块的块号存放在组描述符的 bg_inode_table 字段中。共有 n 个块,n 等于块组的数量( 256)。 所有索引节点的大小相同,即 128 字节。一个 1024 字节的块可以包含 8 个索引节点,一个 4096 字节的块可以包含 32 个索引节点。为了计算出索引节点表占用了多少块,用一个组中 的索引节点总数(存放在超级块的 s_inodes_per_group 字段中)除以每块的索引节点数。 每个 Ext2 索引节点为 ext2_inode 结构: struct ext2_inode { __le16 i_mode; /* 文件类型和访问权限 */ __le16 i_uid; /* 拥有者标识符 */ __le32 i_size; /* 以字节为单位的文件长度 */ __le32 i_atime; /* 最后一次访问文件的时间 */ __le32 i_ctime; /* 索引节点最后改变的时间 */ __le32 i_mtime; /* 文件内容最后改变的时间 */ __le32 i_dtime; /* 文件删除的时间 */ __le16 i_gid; /* 用户组标识符低 16 位 */ __le16 i_links_count; /* 硬链接计数器 */ __le32 i_blocks; /* 文件的数据块数 */ __le32 i_flags; /* 文件标志 */ union { struct { __le32 l_i_reserved1; } linux1; struct { __le32 h_i_translator; } hurd1; struct { __le32 m_i_reserved1; } masix1; } osd1; /* 特定的操作系统信息 1 */ __le32 i_block[EXT2_N_BLOCKS]; /* 指向数据块的指针 */ __le32 i_generation; /* 文件版本(当网络文件系统访问文件时) */ __le32 i_file_acl; /* 文件访问控制列表 */ __le32 i_dir_acl; /* 目录访问控制列表 */ __le32 i_faddr; /* 片的地址 */ union { struct { __u8 l_i_frag; /* Fragment number */ __u8 l_i_fsize; /* Fragment size */ __u16 i_pad1; __le16 l_i_uid_high; /* these 2 fields */ __le16 l_i_gid_high; /* were reserved2[0] */ __u32 l_i_reserved2; } linux2; struct { __u8 h_i_frag; /* Fragment number */ __u8 h_i_fsize; /* Fragment size */ __le16 h_i_mode_high; __le16 h_i_uid_high; __le16 h_i_gid_high; __le32 h_i_author; } hurd2; struct { __u8 m_i_frag; /* Fragment number */ __u8 m_i_fsize; /* Fragment size */ __u16 m_pad1; __u32 m_i_reserved2[2]; } masix2; } osd2; /* 特定的操作系统信息 2 */ }; 与 POSIX 规范相关的很多字段类似于 VFS 索引节点对象的相应字段,这已在“ VFS 文件系 统对象”一节中讨论过。其余的字段与 Ext2 的特殊实现相关,主要处理块的分配。 特别地,i_size 字段存放以字节为单位的文件的有效长度,而 i_blocks 字段存放已分配给文 件的数据块数(以 512 字节为单位)。 i_size 和 i_blocks 的值没有必然的联系。因为一个文件总是存放在整数块中,一个非空文件 至少接受一个数据块(因为还没实现片)且 i_size 可能小于 512 x i_blocks。另一方面,一个 文件可能包含有洞。在那种情况下, i_size 可能大于 512 x i_blocks。 i_block 字段是具有 EXT2_N_BLOCKS(通常是 15)个指针元素的一个数组,每个元素指向 分配给文件的数据块: #define EXT2_NDIR_BLOCKS 12 #define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS #define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1) #define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1) #define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1) 留给 i_size 字段的 32 位把文件的大小限制到 4GB。事实上,i size 字段的最高位没有使用, 因此,文件的最大长度限制为 2GB。然而,Ext2 文件系统包含一种“脏技巧”,允许像 AMD 的 Opteron 和 IBM 的 PowerPC G5 这样的 64 位体系结构使用大型文件。从本质上说,索引 节点的 i_dir_acl 字段(普通文件没有使用)表示 i_size 字段的 32 位扩展。因此,文件的大 小作为 64 位整数存放在索引节点中。 Ext2 文件系统的 64 位版本与 32 位版本在某种程度上 兼容,因为在 64 位体系结构上创建的 Ext2 文件系统可以安装在 32 位体系结构上,反之亦 然。但是,在 32 位体系结构上不能访问大型文件,除非以 O_LARGEFILE 标志打开文件(参 见“VFS 系统调用的实现”一节)。 回忆一下,VFS 模型要求每个文件有不同的索引节点号。在 Ext2 中,没有必要在磁盘上存 放文件的索引节点号与相应块号之间的转换,因为后者的值可以从块组号和它在索引节点表 中的相对位置而得出。例如,假设每个块组包含 4096 个索引节点,我们想知道索引节点 13021 在磁盘上的地址。在这种情况下,这个索引节点属于第三个块组,它的磁盘地址存放在相应 索引节点表的第 733 个表项中( 13021 - 4096 -4096)。所以,inode 数据结构的索引节点号 i_ino 是 Ext2 文件系统用来快速搜索磁盘上合适的索引节点描述符的一个非常关键的字段。 访问控制列表 很早以前访问控制列表就被建议用来改善 Unix 文件系统的保护机制。不是将文件的权限分 成三类:拥有者、组和其他,访问控制列表( access control list, ACL)可以与其他文件关联。 有了这种列表,用户可以为他的文件限定可以访问的用户(或用户组)名称以及相应的权限。 Linux 2.6 通过索引节点的增强属性完整实现 ACL。实际上,增强属性主要就是为了支持 ACL 才引入的。因此,能让你处理文件 ACL 的库函数 chacl()、setfacl()和 getfacl()是通过 setxattr() 和 getxattr()系统调用实现的。 不幸的是,在 POSIX 1003.1 系列标准内,定义安全增强属性的工作组所完成的成果从没有 正式成为新的 POSIX 标准。因此现在,不同的类 Unix 文件系统都支持 ACL,但不同的实 现之间有一些微小的差别。 那么,各种文件类型如何使用磁盘块呢? Ext2 所认可的文件类型(普通文件、管道文件等) 以不同的方式使用数据块。有些文件不存放数据,因此根本就不需要数据块。我们来讨论一 些文件类型的存储要求: 普通文件 普通文件是最常见的情况,我们要重点关注它。但普通文件只有在开始有数据时才需要数据 块。普通文件在刚创建时是空的,并不需要数据块;也可以用 truncate()或 open()系统调用清 空它。这两种情况是相同的,例如,当你发出一个包含字符串 > filename 的 shell 命令时, shell 创建一个空文件或截断一个现有文件。 目录 Ext2 以一种特殊的文件实现了目录,这种文件的数据块把文件名和相应的索引节点号存放 在一起。特别说明的是,这样的数据块包含了类型为 ext2_dir_entry_2 的结构: #define EXT2_NAME_LEN 255 struct ext2_dir_entry_2 { __le32 inode; /* 索引节点号 */ __le16 rec_len; /* 目录项长度 */ __u8 name_len; /* 文件名长度 */ __u8 file_type; /* 文件类型 */ char name[EXT2_NAME_LEN]; /* 文件名 */ }; 因为该结构最后一个 name 字段是最大为 EXT2_NAME_LEN(通常是 255)个字符的变长数 组,因此这个结构的长度是可变的。此外,因为效率的原因,目录项的长度总是 4 的倍数, 并在必要时用 null 字符( \0)填充文件名的末用的 name_len 字段存放实际的文件名长度(参 见下图)。 file_type 字段存放指定文件类型的值(见下表): 文件类型: 描述 0: 未知 1: 普通文件 2: 目录 3: 字符设备 4: 块设备 5: 命名管道 6: 套接字 7: 符号链接 rec_len 字段可以被解释为指定一个有效目录项的指针:它是偏移量,与目录项的起始地址 相加就得到下一个有效目录的起始地址。为了删除一个目录项,把它的 inode 字段置为 0 并 适当地增加前一个有效目录项 rec_len 字段的值就足够了。仔细看一下上边图中的 rec_len 字 段,你会发现 oldfile 项已被删除,因为 usr 的 rec_len 字段被置为 12+16(usr 和 oldfile 目录 项的长度)。 符号链接 如前所述,如果符号链接的路径名小于等于 60 个字符,就把它存放在索引节点的 i_blocks 字段,该字段是由 15 个 4 字节整数组成的数组,因此无需数据块。但是,如果路径名大于 60 个字符,就需要一个单独的数据块。 设备文件、管道和套接字 这些类型的文件不需要数据块。所有必要的信息都存放在索引节点中。 3.2 VFS 接口数据结构 前面我们详细讨论了将一个磁盘分区格式化成 ext2 文件系统后,一个分区的布局,重点介 绍了超级快、块组、位图和索引节点等内容。那么,内核如何跟这些 ext2 文件系统的对象 打交道呢?这个,才是我们研究存储的人应该重点关注的对象,这里,我们就来重点讨论。 3.2.1 Ext2 超级块对象 首先,当安装 Ext2 文件系统时(执行诸如 mount -t ext2 /dev/sda2 /mnt/test 的命令),存放在 Ext2 分区的磁盘数据结构中的大部分信息将被拷贝到 RAM 中,从而使内核避免了后来的很 多读操作。那么一些数据结构如何经常更新呢?让我们考虑一些基本的操作: 1、当一个新文件被创建时,必须减少磁盘中 Ext2 超级块中 s_free_inodes_count 字段的值和 相应的组描述符中 bg_free_inodes_count 字段的值。 2、如果内核给一个现有的文件追加一些数据,以使分配给它的数据块数因此也增加,那么 就必须修改 Ext2 超级块中 s_free_blocks_count 字段的值和组描述符中 bg_free_blocks_count 字段的值。 3、即使仅仅重写一个现有文件的部分内容,也要对 Ext2 超级块的 s_wtime 字段进行更新。 因为所有的 Ext2 磁盘数据结构都存放在 Ext2 磁盘分区的块中,因此,内核利用页高速缓存 来保持它们最新。下面我们就一项一项的来讲解内核如何跟他们打交道,咱们首先介绍 ext2 超级块对象。 在“VFS 对象数据结构”一节我们介绍过, VFS 超级块的 s_fs_info 字段指向一个文件系统 信息的数据结构: struct super_block { …… void *s_fs_info; /* Filesystem private info */ …… }; 对于 Ext2 文件系统,该字段指向 ext2_sb_info 类型的结构: struct ext2_sb_info { unsigned long s_frag_size; /* Size of a fragment in bytes */ unsigned long s_frags_per_block; /* Number of fragments per block */ unsigned long s_inodes_per_block; /* Number of inodes per block */ unsigned long s_frags_per_group; /* Number of fragments in a group */ unsigned long s_blocks_per_group; /* Number of blocks in a group */ unsigned long s_inodes_per_group; /* Number of inodes in a group */ unsigned long s_itb_per_group; /* Number of inode table blocks per group */ unsigned long s_gdb_count; /* Number of group descriptor blocks */ unsigned long s_desc_per_block; /* Number of group descriptors per block */ unsigned long s_groups_count; /* Number of groups in the fs */ struct buffer_head * s_sbh; /* Buffer containing the super block */ struct ext2_super_block * s_es; /* Pointer to the super block in the buffer */ struct buffer_head ** s_group_desc; unsigned long s_mount_opt; uid_t s_resuid; gid_t s_resgid; unsigned short s_mount_state; unsigned short s_pad; int s_addr_per_block_bits; int s_desc_per_block_bits; int s_inode_size; int s_first_ino; spinlock_t s_next_gen_lock; u32 s_next_generation; unsigned long s_dir_count; u8 *s_debts; struct percpu_counter s_freeblocks_counter; struct percpu_counter s_freeinodes_counter; struct percpu_counter s_dirs_counter; struct blockgroup_lock s_blockgroup_lock; }; 其含如下信息: - 磁盘超级块中的大部分字段 - s_sbh 指针,指向包含磁盘超级块的缓冲区的缓冲区首部 - s_es 指针,指向磁盘超级块所在的缓冲区 - 组描述符的个数 s_desc_per_block,可以放在一个块中 - s_group_desc 指针,指向一个缓冲区(包含组描述符的缓冲区)首部数组(只用一项就够 了) - 其他与安装状态、安装选项等有关的数据 下图表示的是与 Ext2 超级块和组描述符有关的缓冲区与缓冲区首部和 ext2_sb_info 数据结 构之间的关系。 要看懂上面那个图以及后面的文字,建议大家先去看一下“页高速缓存”的相关内容。当内 核安装 Ext2 文件系统时( mount 命令),它调用 ext2_fill_super()函数来为数据结构分配空间, 并写入从磁盘读取的数据: static int ext2_fill_super(struct super_block *sb, void *data, int silent) { struct buffer_head * bh; struct ext2_sb_info * sbi; struct ext2_super_block * es; struct inode *root; unsigned long block; unsigned long sb_block = get_sb_block(&data); unsigned long logic_sb_block; unsigned long offset = 0; unsigned long def_mount_opts; int blocksize = BLOCK_SIZE; int db_count; int i, j; __le32 features; sbi = kmalloc(sizeof(*sbi), GFP_KERNEL); if (!sbi) return -ENOMEM; sb->s_fs_info = sbi; memset(sbi, 0, sizeof(*sbi)); /* * See what the current blocksize for the device is, and * use that as the blocksize. Otherwise (or if the blocksize * is smaller than the default) use the default. * This is important for devices that have a hardware * sectorsize that is larger than the default. */ blocksize = sb_min_blocksize(sb, BLOCK_SIZE); if (!blocksize) { printk ("EXT2-fs: unable to set blocksize\n"); goto failed_sbi; } /* * If the superblock doesn't start on a hardware sector boundary, * calculate the offset. */ if (blocksize != BLOCK_SIZE) { logic_sb_block = (sb_block*BLOCK_SIZE) / blocksize; offset = (sb_block*BLOCK_SIZE) % blocksize; } else { logic_sb_block = sb_block; } if (!(bh = sb_bread(sb, logic_sb_block))) { printk ("EXT2-fs: unable to read superblock\n"); goto failed_sbi; } /* * Note: s_es must be initialized as soon as possible because * some ext2 macro-instructions depend on its value */ es = (struct ext2_super_block *) (((char *)bh->b_data) + offset); sbi->s_es = es; sb->s_magic = le16_to_cpu(es->s_magic); if (sb->s_magic != EXT2_SUPER_MAGIC) goto cantfind_ext2; /* Set defaults before we parse the mount options */ def_mount_opts = le32_to_cpu(es->s_default_mount_opts); if (def_mount_opts & EXT2_DEFM_DEBUG) set_opt(sbi->s_mount_opt, DEBUG); if (def_mount_opts & EXT2_DEFM_BSDGROUPS) set_opt(sbi->s_mount_opt, GRPID); if (def_mount_opts & EXT2_DEFM_UID16) set_opt(sbi->s_mount_opt, NO_UID32); if (def_mount_opts & EXT2_DEFM_XATTR_USER) set_opt(sbi->s_mount_opt, XATTR_USER); if (def_mount_opts & EXT2_DEFM_ACL) set_opt(sbi->s_mount_opt, POSIX_ACL); if (le16_to_cpu(sbi->s_es->s_errors) == EXT2_ERRORS_PANIC) set_opt(sbi->s_mount_opt, ERRORS_PANIC); else if (le16_to_cpu(sbi->s_es->s_errors) == EXT2_ERRORS_RO) set_opt(sbi->s_mount_opt, ERRORS_RO); sbi->s_resuid = le16_to_cpu(es->s_def_resuid); sbi->s_resgid = le16_to_cpu(es->s_def_resgid); if (!parse_options ((char *) data, sbi)) goto failed_mount; sb->s_flags = (sb->s_flags & ~MS_POSIXACL) | ((EXT2_SB(sb)->s_mount_opt & EXT2_MOUNT_POSIX_ACL) ? MS_POSIXACL : 0); ext2_xip_verify_sb(sb); /* see if bdev supports xip, unset EXT2_MOUNT_XIP if not */ if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV && (EXT2_HAS_COMPAT_FEATURE(sb, ~0U) || EXT2_HAS_RO_COMPAT_FEATURE(sb, ~0U) || EXT2_HAS_INCOMPAT_FEATURE(sb, ~0U))) printk("EXT2-fs warning: feature flags set on rev 0 fs, " "running e2fsck is recommended\n"); /* * Check feature flags regardless of the revision level, since we * previously didn't change the revision level when setting the flags, * so there is a chance incompat flags are set on a rev 0 filesystem. */ features = EXT2_HAS_INCOMPAT_FEATURE(sb, ~EXT2_FEATURE_INCOMPAT_SUPP); if (features) { printk("EXT2-fs: %s: couldn't mount because of " "unsupported optional features (%x).\n", sb->s_id, le32_to_cpu(features)); goto failed_mount; } if (!(sb->s_flags & MS_RDONLY) && (features = EXT2_HAS_RO_COMPAT_FEATURE(sb, ~EXT2_FEATURE_RO_COMPAT_SUPP))){ printk("EXT2-fs: %s: couldn't mount RDWR because of " "unsupported optional features (%x).\n", sb->s_id, le32_to_cpu(features)); goto failed_mount; } blocksize = BLOCK_SIZE << le32_to_cpu(sbi->s_es->s_log_block_size); if ((ext2_use_xip(sb)) && ((blocksize != PAGE_SIZE) || (sb->s_blocksize != blocksize))) { if (!silent) printk("XIP: Unsupported blocksize\n"); goto failed_mount; } /* If the blocksize doesn't match, re-read the thing.. */ if (sb->s_blocksize != blocksize) { brelse(bh); if (!sb_set_blocksize(sb, blocksize)) { printk(KERN_ERR "EXT2-fs: blocksize too small for device.\n"); goto failed_sbi; } logic_sb_block = (sb_block*BLOCK_SIZE) / blocksize; offset = (sb_block*BLOCK_SIZE) % blocksize; bh = sb_bread(sb, logic_sb_block); if(!bh) { printk("EXT2-fs: Couldn't read superblock on " "2nd try.\n"); goto failed_sbi; } es = (struct ext2_super_block *) (((char *)bh->b_data) + offset); sbi->s_es = es; if (es->s_magic != cpu_to_le16(EXT2_SUPER_MAGIC)) { printk ("EXT2-fs: Magic mismatch, very weird !\n"); goto failed_mount; } } sb->s_maxbytes = ext2_max_size(sb->s_blocksize_bits); if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV) { sbi->s_inode_size = EXT2_GOOD_OLD_INODE_SIZE; sbi->s_first_ino = EXT2_GOOD_OLD_FIRST_INO; } else { sbi->s_inode_size = le16_to_cpu(es->s_inode_size); sbi->s_first_ino = le32_to_cpu(es->s_first_ino); if ((sbi->s_inode_size < EXT2_GOOD_OLD_INODE_SIZE) || (sbi->s_inode_size & (sbi->s_inode_size - 1)) || (sbi->s_inode_size > blocksize)) { printk ("EXT2-fs: unsupported inode size: %d\n", sbi->s_inode_size); goto failed_mount; } } sbi->s_frag_size = EXT2_MIN_FRAG_SIZE << le32_to_cpu(es->s_log_frag_size); if (sbi->s_frag_size == 0) goto cantfind_ext2; sbi->s_frags_per_block = sb->s_blocksize / sbi->s_frag_size; sbi->s_blocks_per_group = le32_to_cpu(es->s_blocks_per_group); sbi->s_frags_per_group = le32_to_cpu(es->s_frags_per_group); sbi->s_inodes_per_group = le32_to_cpu(es->s_inodes_per_group); if (EXT2_INODE_SIZE(sb) == 0) goto cantfind_ext2; sbi->s_inodes_per_block = sb->s_blocksize / EXT2_INODE_SIZE(sb); if (sbi->s_inodes_per_block == 0 || sbi->s_inodes_per_group == 0) goto cantfind_ext2; sbi->s_itb_per_group = sbi->s_inodes_per_group / sbi->s_inodes_per_block; sbi->s_desc_per_block = sb->s_blocksize / sizeof (struct ext2_group_desc); sbi->s_sbh = bh; sbi->s_mount_state = le16_to_cpu(es->s_state); sbi->s_addr_per_block_bits = log2 (EXT2_ADDR_PER_BLOCK(sb)); sbi->s_desc_per_block_bits = log2 (EXT2_DESC_PER_BLOCK(sb)); if (sb->s_magic != EXT2_SUPER_MAGIC) goto cantfind_ext2; if (sb->s_blocksize != bh->b_size) { if (!silent) printk ("VFS: Unsupported blocksize on dev " "%s.\n", sb->s_id); goto failed_mount; } if (sb->s_blocksize != sbi->s_frag_size) { printk ("EXT2-fs: fragsize %lu != blocksize %lu (not supported yet)\n", sbi->s_frag_size, sb->s_blocksize); goto failed_mount; } if (sbi->s_blocks_per_group > sb->s_blocksize * 8) { printk ("EXT2-fs: #blocks per group too big: %lu\n", sbi->s_blocks_per_group); goto failed_mount; } if (sbi->s_frags_per_group > sb->s_blocksize * 8) { printk ("EXT2-fs: #fragments per group too big: %lu\n", sbi->s_frags_per_group); goto failed_mount; } if (sbi->s_inodes_per_group > sb->s_blocksize * 8) { printk ("EXT2-fs: #inodes per group too big: %lu\n", sbi->s_inodes_per_group); goto failed_mount; } if (EXT2_BLOCKS_PER_GROUP(sb) == 0) goto cantfind_ext2; sbi->s_groups_count = (le32_to_cpu(es->s_blocks_count) - le32_to_cpu(es->s_first_data_block) + EXT2_BLOCKS_PER_GROUP(sb) - 1) / EXT2_BLOCKS_PER_GROUP(sb); db_count = (sbi->s_groups_count + EXT2_DESC_PER_BLOCK(sb) - 1) / EXT2_DESC_PER_BLOCK(sb); sbi->s_group_desc = kmalloc (db_count * sizeof (struct buffer_head *), GFP_KERNEL); if (sbi->s_group_desc == NULL) { printk ("EXT2-fs: not enough memory\n"); goto failed_mount; } bgl_lock_init(&sbi->s_blockgroup_lock); sbi->s_debts = kmalloc(sbi->s_groups_count * sizeof(*sbi->s_debts), GFP_KERNEL); if (!sbi->s_debts) { printk ("EXT2-fs: not enough memory\n"); goto failed_mount_group_desc; } memset(sbi->s_debts, 0, sbi->s_groups_count * sizeof(*sbi->s_debts)); for (i = 0; i < db_count; i++) { block = descriptor_loc(sb, logic_sb_block, i); sbi->s_group_desc[i] = sb_bread(sb, block); if (!sbi->s_group_desc[i]) { for (j = 0; j < i; j++) brelse (sbi->s_group_desc[j]); printk ("EXT2-fs: unable to read group descriptors\n"); goto failed_mount_group_desc; } } if (!ext2_check_descriptors (sb)) { printk ("EXT2-fs: group descriptors corrupted!\n"); goto failed_mount2; } sbi->s_gdb_count = db_count; get_random_bytes(&sbi->s_next_generation, sizeof(u32)); spin_lock_init(&sbi->s_next_gen_lock); percpu_counter_init(&sbi->s_freeblocks_counter, ext2_count_free_blocks(sb)); percpu_counter_init(&sbi->s_freeinodes_counter, ext2_count_free_inodes(sb)); percpu_counter_init(&sbi->s_dirs_counter, ext2_count_dirs(sb)); /* * set up enough so that it can read an inode */ sb->s_op = &ext2_sops; sb->s_export_op = &ext2_export_ops; sb->s_xattr = ext2_xattr_handlers; root = iget(sb, EXT2_ROOT_INO); sb->s_root = d_alloc_root(root); if (!sb->s_root) { iput(root); printk(KERN_ERR "EXT2-fs: get root inode failed\n"); goto failed_mount3; } if (!S_ISDIR(root->i_mode) || !root->i_blocks || !root->i_size) { dput(sb->s_root); sb->s_root = NULL; printk(KERN_ERR "EXT2-fs: corrupt root inode, run e2fsck\n"); goto failed_mount3; } if (EXT2_HAS_COMPAT_FEATURE(sb, EXT3_FEATURE_COMPAT_HAS_JOURNAL)) ext2_warning(sb, __FUNCTION__, "mounting ext3 filesystem as ext2"); ext2_setup_super (sb, es, sb->s_flags & MS_RDONLY); return 0; cantfind_ext2: if (!silent) printk("VFS: Can't find an ext2 filesystem on dev %s.\n", sb->s_id); goto failed_mount; failed_mount3: percpu_counter_destroy(&sbi->s_freeblocks_counter); percpu_counter_destroy(&sbi->s_freeinodes_counter); percpu_counter_destroy(&sbi->s_dirs_counter); failed_mount2: for (i = 0; i < db_count; i++) brelse(sbi->s_group_desc[i]); failed_mount_group_desc: kfree(sbi->s_group_desc); kfree(sbi->s_debts); failed_mount: brelse(bh); failed_sbi: sb->s_fs_info = NULL; kfree(sbi); return -EINVAL; } 注意,要读懂上面的代码请好好理解页高速缓存相关的内容,因为 ext2 磁盘超级快 ext2_super_block 缓存于 bh->b_data 的某个位置,而对应的块号就是 1(0 号是引导块)。这 里是对该函数的一个简要说明,只强调缓冲区与描述符的内存分配: 1. 分配一个 ext2_sb_info 描述符,将其地址当作参数传递并存放在超级块 sb 的 s_fs_info 字 段: struct buffer_head * bh; struct ext2_sb_info * sbi; struct ext2_super_block * es; struct inode *root; unsigned long block; unsigned long sb_block = get_sb_block(&data); unsigned long logic_sb_block; unsigned long offset = 0; unsigned long def_mount_opts; int blocksize = BLOCK_SIZE; int db_count; int i, j; __le32 features; sbi = kmalloc(sizeof(*sbi), GFP_KERNEL); if (!sbi) return -ENOMEM; sb->s_fs_info = sbi; memset(sbi, 0, sizeof(*sbi)); 2. 调用__bread()在缓冲区页中分配一个缓冲区和缓冲区首部。然后从磁盘读入超级块存放 在缓冲区中。在“在页高速缓存中搜索块”一博我们讨论过,如果一个块已在页高速缓存的 缓冲区页而且是最新的,那么无需再分配。将缓冲区首部地址存放在 Ext2 超级块对象 sbi 的 s_sbh 字段: if (!(bh = sb_bread(sb, logic_sb_block))) { printk ("EXT2-fs: unable to read superblock\n"); goto failed_sbi; } //这里的 offset 其实就是 0。 es = (struct ext2_super_block *) (((char *)bh->b_data) + offset); sbi->s_es = es; …… 3. 分配一个数组用于存放缓冲区首部指针,每个组描述符一个,把该数组地址存放在 ext2_sb_info 的 s_group_desc 字段。 db_count = (sbi->s_groups_count + EXT2_DESC_PER_BLOCK(sb) - 1) / EXT2_DESC_PER_BLOCK(sb); sbi->s_group_desc = kmalloc (db_count * sizeof (struct buffer_head *), GFP_KERNEL); 4. 分配一个字节数组,每组一个字节,把它的地址存放在 ext2_sb_info 描述符的 s_debts 字 段(参见后面的“管理 ext2 磁盘空间”一节): sbi->s_debts = kmalloc(sbi->s_groups_count * sizeof(*sbi->s_debts), GFP_KERNEL); if (!sbi->s_debts) { printk ("EXT2-fs: not enough memory\n"); goto failed_mount_group_desc; } memset(sbi->s_debts, 0, sbi->s_groups_count * sizeof(*sbi->s_debts)); 5. 重复调用__bread()分配缓冲区,从磁盘读人包含 Ext2 组描述符的块。把缓冲区首部地址 存放在上一步得到的 s_group_desc 数组中: for (i = 0; i < db_count; i++) { block = descriptor_loc(sb, logic_sb_block, i); sbi->s_group_desc[i] = sb_bread(sb, block); if (!sbi->s_group_desc[i]) { for (j = 0; j < i; j++) brelse (sbi->s_group_desc[j]); printk ("EXT2-fs: unable to read group descriptors\n"); goto failed_mount_group_desc; } } 6. 最后安装好 sb->s_op,sb->s_export_op,超级块增强属性。因为准备为根目录分配一个索 引节点和目录项对象,必须把 s_op 字段为超级块建立好,从而能够从磁盘读入根索引节点 对象: sb->s_op = &ext2_sops; sb->s_export_op = &ext2_export_ops; sb->s_xattr = ext2_xattr_handlers; root = iget(sb, EXT2_ROOT_INO); sb->s_root = d_alloc_root(root); 注意,EXT2_ROOT_INO 是 2,也就是它的根节点位于第一个块组的第三个位置上。很显然, ext2_fill super()函数返回后,有很多关键的 ext2 磁盘数据结构的内容都保存在内存里了,例 如 ext2_super_block、第根索引节点等,只有当 Ext2 文件系统卸载时才会被释放。当内核必 须修改 Ext2 超级块的字段时,它只要把新值写入相应缓冲区内的相应位置然后将该缓冲区 标记为脏即可,有看页高速缓存事情就是这么简单! 3.2.2 Ext2 的索引节点对象 上一节我们详细分析了 VFS 如何将具体文件系统 ext2 的超级快对象缓存到内存中,主要是 利用了 ext2_sb_info 数据结构。ext2_fill_super 函数最后的时候,会将传入该函数的 super_block 类型的参数 sb 赋予以下的值: sb->s_op = &ext2_sops; 其中,ext2_sops 是将 super_block 的 super_operations 初始化以下方法的集合: static struct super_operations ext2_sops = { .alloc_inode = ext2_alloc_inode, .destroy_inode = ext2_destroy_inode, .read_inode = ext2_read_inode, .write_inode = ext2_write_inode, .put_inode = ext2_put_inode, .delete_inode = ext2_delete_inode, .put_super = ext2_put_super, .write_super = ext2_write_super, .statfs = ext2_statfs, .remount_fs = ext2_remount, .clear_inode = ext2_clear_inode, .show_options = ext2_show_options, #ifdef CONFIG_QUOTA .quota_read = ext2_quota_read, .quota_write = ext2_quota_write, #endif }; 本节,我们来讨论 VFS 如何将 ext2 的索引节点缓存到内存中。 在打开文件时,要执行路径名查找。对于不在目录项高速缓存内的路径名元素,会创建一个 新的目录项对象和索引节点对象(参见“标准路经名查找”章节)。当 VFS 访问一个 Ext2 磁盘索引节点时,它会创建一个 ext2_inode_info 类型的索引节点描述符: struct ext2_inode_info { __le32 i_data[15]; __u32 i_flags; __u32 i_faddr; __u8 i_frag_no; __u8 i_frag_size; __u16 i_state; __u32 i_file_acl; __u32 i_dir_acl; __u32 i_dtime; /* * i_block_group is the number of the block group which contains * this file's inode. Constant across the lifetime of the inode, * it is ued for making block allocation decisions - we try to * place a file's data blocks near its inode block, and new inodes * near to their parent directory's inode. */ __u32 i_block_group; /* * i_next_alloc_block is the logical (file-relative) number of the * most-recently-allocated block in this file. Yes, it is misnamed. * We use this for detecting linearly ascending allocation requests. */ __u32 i_next_alloc_block; /* * i_next_alloc_goal is the *physical* companion to i_next_alloc_block. * it the the physical block number of the block which was most-recently * allocated to this file. This give us the goal (target) for the next * allocation when we detect linearly ascending requests. */ __u32 i_next_alloc_goal; __u32 i_prealloc_block; __u32 i_prealloc_count; __u32 i_dir_start_lookup; #ifdef CONFIG_EXT2_FS_XATTR /* * Extended attributes can be read independently of the main file * data. Taking i_mutex even when reading would cause contention * between readers of EAs and writers of regular file data, so * instead we synchronize on xattr_sem when reading or changing * EAs. */ struct rw_semaphore xattr_sem; #endif #ifdef CONFIG_EXT2_FS_POSIX_ACL struct posix_acl *i_acl; struct posix_acl *i_default_acl; #endif rwlock_t i_meta_lock; struct inode vfs_inode; }; 该描述符包含下列信息: - 存放在 vfs mode 字段的整个 VFS 索引节点对象; - ext2 磁盘索引节点对象结构中的大部分字段(不保存在 VFS 索引节点中的那些字段); - 块组中索引节点对应的索引 i_block_group; - i_next_alloc_block 和 i_next_alloc_goal 分别存放着最近为文件分配的磁盘块的逻辑块号和 物理块号; - i_prealloc_block 和 i_prealloc_count 字段,用于数据块预分配 ; - xattr_sem 字段,一个读写信号量,允许增强属性与文件数据同时读入; - i_acl 和 i_default_acl 字段,指向文件的访问控制列表。 当处理 Ext2 文件时,alloc_inode 超级块方法是由 ext2_alloc_inode()函数实现: static struct inode *ext2_alloc_inode(struct super_block *sb) { struct ext2_inode_info *ei; ei = (struct ext2_inode_info *)kmem_cache_alloc(ext2_inode_cachep, SLAB_KERNEL); if (!ei) return NULL; #ifdef CONFIG_EXT2_FS_POSIX_ACL ei->i_acl = EXT2_ACL_NOT_CACHED; ei->i_default_acl = EXT2_ACL_NOT_CACHED; #endif ei->vfs_inode.i_version = 1; return &ei->vfs_inode; } 该函数的实现极其简单,它首先从 ext2_inode_cachep slab 分配器高速缓存得到一个 ext2_inode_info 数据结构,然后返回在这个 ext2_inode_info 数据结构中的索引节点对象的地 址 ei->vfs_inode。 我们看到,跟 ext2_sb_info 和 ext2_group_desc 不同,ext2_inode_info 根本没有对应的 buffer_head 字段,也就是说,它只是把 ext2_inode 的某些字段拷贝进来,对位于磁盘的 ext2_inode 进行动态缓存,下面,我们就把 ext2 数据结构的 VFS 映像做个总结: 类型 磁盘数据结构 内存数据结构 缓存模式 Superblock ext2_super_block ext2_sb_info 总是缓存 Group descriptor ext2_group_desc ext2_group_desc 总是缓存 Block bitmap Bit array in block Bit array in buffer 动态缓存 inode bitmap Bit array in block Bit array in buffer 动态缓存 inode ext2_inode ext2_inode_info 动态缓存 Data block Array of bytes VFS buffer 动态缓存 Free inode ext2_inode None 从不缓存 Free block Array of bytes None 从不缓存 3.2.3 创建 Ext2 文件系统 在磁盘上创建一个文件系统通常有两个阶段。第一步格式化磁盘,以使磁盘驱动程序可以将 块号转换成对应的磁道和扇区,从而可以读和写磁盘上的物理块。现在的硬磁盘已经由厂家 预先格式化,因此不需要重新格式化;在 Linux 上可以使用 superformat 或 fdformat 等实用 程序对软盘进行格式化。第二步才涉及创建文件系统,这意味着建立前面章节中详细描述的 那些磁盘文件系统数据结构。 Ext2 文件系统是由 C 程序 mke2fs 创建的。mke2fs 采用下列缺省选项,用户可以用命令行的标志修改这些选项: - 块大小: 1024 字节(小文件系统的缺省值) - 片大小:等于块的大小(因为块的分片还没有实现) - 所分配的索引节点个数:每 8192 字节的组分配一个索引节点 - 保留块的百分比: 5% mke2fs 程序执行下列操作: 1. 初始化超级块和组描述符。 2. 作为选择,检查分区是否包含有缺陷的块;如果有,就创建一个有缺陷块的链表。 3. 对于每个块组,保留存放超级块、组描述符、索引节点表及两个位图所需要的所有磁盘 块。 4. 把索引节点位图和每个块组的数据映射位图都初始化为 0。 5. 初始化每个块组的索引节点表。 6. 创建/root 目录。 7. 创建 lost+found 目录,由 e2fsck 使用这个目录把丢失和找到的缺陷块连接起来。 8. 在前两个已经创建的目录所在的块组中,更新块组中的索引节点位图和数据块位图。 9. 把有缺陷的块(如果存在)组织起来放在 lost+found 目录中。 让我们看一下 mke2fs 是如何以缺省选项初始化 Ext2 的 1.44 MB 软盘的。 软盘一旦被安装, VFS 就把它看作由 1412 个块组成的一个卷,每块大小为 1024 字节。为 了查看磁盘的内容,我们可以执行如下 Unix 命令: [root@localhost]# dd if=/dev/fdO bs=1k count=1440 | od -tx1 -Ax > /tmp/dump_hex 从而获得了 AMP 目录下的一个文件,这个文件包含十六进制的软盘内容的转储。 通过查看 dump_hex 文件我们可以看到,由于软盘有限的容量 1.44MB(约等于 1474560 个 字节),一共需要 1440 个块,那么一个单独的块组描述符就足够了。我们还注意到保留的块 数为 72(1440 块的 5%)。 软盘中的第一个块是引导块,第二个块是超级快 ext2_super_block,第三个块是组描述符 ext2_group_desc,第四个块是数据块的位图(所以 ext2_group_desc 中的 bg_block_bitmap 等 于 4),第五个块是索引节点的位图(所以 ext2_group_desc 中的 bg_inode_bitmap 等于 5)。 根据缺省选项,索引节点表必须为每 8192 个字节设置一个索引节点,也就是有 184 个索引 节点存放在紧挨着索引节点的位图那个块的后 23 个块中,也就是第 6~28 个块。 下表是对一个软盘执行 mke2fs 程序后的磁盘分布: 块 内容 0 引导块 1 超级块 2 仅包含一个组描述符的块 3 数据块位图 4 inode 位图 5-27 inode 表,其中:1 到 10 号 inode 保留(2 号 inode 是根目录的 inode);11 号 inode 是 lost+found;12-184 号 inode 空闲 28 根目录 (包括 .、..、和 lost+found) 29 lost+found 目录 (包括 . and ..) 30-40 预分配给 lost+found 目录的保留的块 41-1439 空闲块 3.2.4 Ext2 的方法总结 在“VFS 对象数据结构”一节中所描述的关于 VFS 的很多方法在 Ext2 都有相应的实现。因 为对所有的方法都进行讨论需要整整一本书,因此我们仅仅简单地回顾一下在 Ext2 中所实 现的方法。一旦你真正搞明白了磁盘和内存数据结构,你就应当能理解实现这些方法的 Ext2 函数的代码。 Ext2 超级块的操作 在“Ext2 的索引节点对象”我们已经分析了 Ext2 超级块的操作之一: ext2_alloc_inode,这 里在对 Ext2 超级块的操作做一个综合性的总结。很多 VFS 超级块操作在 Ext2 中都有具体 的实现,这些方法为 alloc_inode、destroy_inode、read_inode、write_inode、delete_inode、 put_super、write_super、statfs、remount_fs 和 clear_inode。超级块方法的地址存放在 ext2_sops 指针数组: static struct super_operations ext2_sops = { .alloc_inode = ext2_alloc_inode, .destroy_inode = ext2_destroy_inode, .read_inode = ext2_read_inode, .write_inode = ext2_write_inode, .put_inode = ext2_put_inode, .delete_inode = ext2_delete_inode, .put_super = ext2_put_super, .write_super = ext2_write_super, .statfs = ext2_statfs, .remount_fs = ext2_remount, .clear_inode = ext2_clear_inode, .show_options = ext2_show_options, #ifdef CONFIG_QUOTA .quota_read = ext2_quota_read, .quota_write = ext2_quota_write, #endif }; Ext2 索引节点的操作 一些 VFS 索引节点的操作在 Ext2 中都有具体的实现,这取决于索引节点所指的文件类型。 如果文件类型是普通文件,则对应 inode 的方法存放在 ext2_file_inode_operations 表中: struct inode_operations ext2_file_inode_operations = { .truncate = ext2_truncate, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif .setattr = ext2_setattr, .permission = ext2_permission, .fiemap = ext2_fiemap, }; 注意,普通文件的 inode 操作有很多没有实现 VFS 索引节点所规定的那些动作。如果文件 类型是目录,那么对应 inode 的方法存放在 ext2_dir_inode_operations 表中: struct inode_operations ext2_dir_inode_operations = { .create = ext2_create, .lookup = ext2_lookup, .link = ext2_link, .unlink = ext2_unlink, .symlink = ext2_symlink, .mkdir = ext2_mkdir, .rmdir = ext2_rmdir, .mknod = ext2_mknod, .rename = ext2_rename, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif .setattr = ext2_setattr, .permission = ext2_permission, }; 如果文件类型是符号链接,那么实际上是有两种符号链接的:快速符号链接(路径名全部存 放在索引节点内)与普通符号链接(较长的路径名)。因此,有两套索引节点操作,分别存 放在 ext2_fast_symlink_inode_operations 和 ext2_symlink_inode_operations 表中: struct inode_operations ext2_symlink_inode_operations = { .readlink = generic_readlink, .follow_link = page_follow_link_light, .put_link = page_put_link, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif }; struct inode_operations ext2_fast_symlink_inode_operations = { .readlink = generic_readlink, .follow_link = ext2_follow_link, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif }; 如果索引节点指的是一个字符设备文件、块设备文件或管道文件,那么这种索引节点的操作 部依赖于具体的文件系统,其分别位于 chrdev_inode_operations、blkdev_inode_operations 和 fifo_inode_operations 表中。这些数据结构在新的内核版本中已经淘汰了,我们就不去详细 描述他们了。 Ext2 的文件操作 针对 Ext2 文件系统特定的文件操作,其实现 VFS 文件对象的具体方法地址存放在 ext2_file_operations 表中: const struct file_operations ext2_file_operations = { .llseek = generic_file_llseek, .read = generic_file_read, .write = generic_file_write, .aio_read = generic_file_aio_read, .aio_write = generic_file_aio_write, .ioctl = ext2_ioctl, .mmap = generic_file_mmap, .open = generic_file_open, .release = ext2_release_file, .fsync = ext2_sync_file, .readv = generic_file_readv, .writev = generic_file_writev, .sendfile = generic_file_sendfile, .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, }; 我们看到,大部分方法都有 generic 字符,说明这些方法不仅只被 ext2 文件系统使用,而且 是个很通用的方法,如果你有兴趣去看看 ext3_file_operations 或 ext4_file_operations 你会发 现,他们也大量使用了这些通用的方法。 Ext2 最主要的读写文件方法的 read 和 write 方法分 别通过 generic_file_read 和 generic_file_write 函数实现。这两个函数我们会在以后的章节中 重点讨论他们。 3.3 Ext2 索引节点分配 文件在磁盘的存储不同于程序员所看到的文件,主要表现在两个方面:块可以分散在磁盘上 (尽管文件系统尽力保持块连续存放以提高访问速度),以及程序员看到的文件似乎比实际 的文件大,这是因为程序可以把洞引人文件(通过 lseek ()系统调用)。 从本篇章节开始,我们将介绍 Ext2 文件系统如何管理磁盘空间,也就是说,如何分配和释 放索引节点和数据块。有两个主要的问题必须考虑: (1)空间管理必须尽力避免文件碎片,也就是说,避免文件在物理上存放于几个小的、不 相邻的盘块上。文件碎片增加了对文件的连续读操作的平均时间,因为在读操作期间,磁头 必须频繁地重新定位。这个问题类似于在内存管理中的“伙伴系统算法”章节中所讨论的 RAM 的外部碎片问题。 (2)空间管理必须考虑效率,也就是说,内核应该能从文件的偏移量快速地导出 Ext2 分区上相应的逻辑块号。为了达到此目的,内核应该尽可能地限制对磁盘上寻址表的访问次数, 因为对该表的访问会极大地增加文件的平均访问时间。 3.3.1 创建索引节点 ext2_new_inode()函数创建 Ext2 磁盘的索引节点,返回相应的索引节点对象的地址(或失败 时为 NULL)。该函数谨慎地选择存放该新索引节点的块组;它将无联系的目录散放在不同 的组,而且同时把文件存放在父目录的同一组。为了平衡普通文件数与块组中的目录数, Ext2 为每一个块组引入“债( debt) ”参数。 ext2_new_inode 函数有两个参数:dir,所创建索引节点父目录对应的索引节点对象的地址, 新创建的索引节点必须插入到这个目录中,成为其中的一个目录项; mode,要创建的索引 节点的类型。后一个参数还包含一个 MS_SYNCHRONOUS 标志,该标志请求当前进程一直 挂起,直到索引节点被分配成功或失败。该函数代码如下: struct inode *ext2_new_inode(struct inode *dir, int mode) { struct super_block *sb; struct buffer_head *bitmap_bh = NULL; struct buffer_head *bh2; int group, i; ino_t ino = 0; struct inode * inode; struct ext2_group_desc *gdp; struct ext2_super_block *es; struct ext2_inode_info *ei; struct ext2_sb_info *sbi; int err; sb = dir->i_sb; inode = new_inode(sb); if (!inode) return ERR_PTR(-ENOMEM); ei = EXT2_I(inode); sbi = EXT2_SB(sb); es = sbi->s_es; if (S_ISDIR(mode)) { if (test_opt(sb, OLDALLOC)) group = find_group_dir(sb, dir); else group = find_group_orlov(sb, dir); } else group = find_group_other(sb, dir); if (group == -1) { err = -ENOSPC; goto fail; } for (i = 0; i < sbi->s_groups_count; i++) { gdp = ext2_get_group_desc(sb, group, &bh2); brelse(bitmap_bh); bitmap_bh = read_inode_bitmap(sb, group); if (!bitmap_bh) { err = -EIO; goto fail; } ino = 0; repeat_in_this_group: ino = ext2_find_next_zero_bit((unsigned long *)bitmap_bh->b_data, EXT2_INODES_PER_GROUP(sb), ino); if (ino >= EXT2_INODES_PER_GROUP(sb)) { /* * Rare race: find_group_xx() decided that there were * free inodes in this group, but by the time we tried * to allocate one, they're all gone. This can also * occur because the counters which find_group_orlov() * uses are approximate. So just go and search the * next block group. */ if (++group == sbi->s_groups_count) group = 0; continue; } if (ext2_set_bit_atomic(sb_bgl_lock(sbi, group), ino, bitmap_bh->b_data)) { /* we lost this inode */ if (++ino >= EXT2_INODES_PER_GROUP(sb)) { /* this group is exhausted, try next group */ if (++group == sbi->s_groups_count) group = 0; continue; } /* try to find free inode in the same group */ goto repeat_in_this_group; } goto got; } /* * Scanned all blockgroups. */ err = -ENOSPC; goto fail; got: mark_buffer_dirty(bitmap_bh); if (sb->s_flags & MS_SYNCHRONOUS) sync_dirty_buffer(bitmap_bh); brelse(bitmap_bh); ino += group * EXT2_INODES_PER_GROUP(sb) + 1; if (ino < EXT2_FIRST_INO(sb) || ino > le32_to_cpu(es->s_inodes_count)) { ext2_error (sb, "ext2_new_inode", "reserved inode or inode > inodes count - " "block_group = %d,inode=%lu", group, (unsigned long) ino); err = -EIO; goto fail; } percpu_counter_mod(&sbi->s_freeinodes_counter, -1); if (S_ISDIR(mode)) percpu_counter_inc(&sbi->s_dirs_counter); spin_lock(sb_bgl_lock(sbi, group)); gdp->bg_free_inodes_count = cpu_to_le16(le16_to_cpu(gdp->bg_free_inodes_count) - 1); if (S_ISDIR(mode)) { if (sbi->s_debts[group] < 255) sbi->s_debts[group]++; gdp->bg_used_dirs_count = cpu_to_le16(le16_to_cpu(gdp->bg_used_dirs_count) + 1); } else { if (sbi->s_debts[group]) sbi->s_debts[group]--; } spin_unlock(sb_bgl_lock(sbi, group)); sb->s_dirt = 1; mark_buffer_dirty(bh2); inode->i_uid = current->fsuid; if (test_opt (sb, GRPID)) inode->i_gid = dir->i_gid; else if (dir->i_mode & S_ISGID) { inode->i_gid = dir->i_gid; if (S_ISDIR(mode)) mode |= S_ISGID; } else inode->i_gid = current->fsgid; inode->i_mode = mode; inode->i_ino = ino; inode->i_blocks = 0; inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME_SEC; memset(ei->i_data, 0, sizeof(ei->i_data)); ei->i_flags = EXT2_I(dir)->i_flags & ~EXT2_BTREE_FL; if (S_ISLNK(mode)) ei->i_flags &= ~(EXT2_IMMUTABLE_FL|EXT2_APPEND_FL); /* dirsync is only applied to directories */ if (!S_ISDIR(mode)) ei->i_flags &= ~EXT2_DIRSYNC_FL; ei->i_faddr = 0; ei->i_frag_no = 0; ei->i_frag_size = 0; ei->i_file_acl = 0; ei->i_dir_acl = 0; ei->i_dtime = 0; ei->i_block_group = group; ei->i_next_alloc_block = 0; ei->i_next_alloc_goal = 0; ei->i_prealloc_block = 0; ei->i_prealloc_count = 0; ei->i_dir_start_lookup = 0; ei->i_state = EXT2_STATE_NEW; ext2_set_inode_flags(inode); spin_lock(&sbi->s_next_gen_lock); inode->i_generation = sbi->s_next_generation++; spin_unlock(&sbi->s_next_gen_lock); insert_inode_hash(inode); if (DQUOT_ALLOC_INODE(inode)) { err = -EDQUOT; goto fail_drop; } err = ext2_init_acl(inode, dir); if (err) goto fail_free_drop; err = ext2_init_security(inode,dir); if (err) goto fail_free_drop; mark_inode_dirty(inode); ext2_debug("allocating inode %lu\n", inode->i_ino); ext2_preread_inode(inode); return inode; fail_free_drop: DQUOT_FREE_INODE(inode); fail_drop: DQUOT_DROP(inode); inode->i_flags |= S_NOQUOTA; inode->i_nlink = 0; iput(inode); return ERR_PTR(err); fail: make_bad_inode(inode); iput(inode); return ERR_PTR(err); } 1.调用 new_inode()分配一个新的 VFS 索引节点对象,并把它的 i_sb 字段初始化为存放在 dir->i_sb 中的超级块地址。然后把它追加到正在用的索引节点链表与超级块链表中: struct inode *new_inode(struct super_block *sb) { /* 32 bits for compatability mode stat calls */ static unsigned int last_ino; struct inode * inode; spin_lock_prefetch(&inode_lock); inode = alloc_inode(sb); if (inode) { spin_lock(&inode_lock); inodes_stat.nr_inodes++; list_add(&inode->i_list, &inode_in_use); list_add(&inode->i_sb_list, &sb->s_inodes); inode->i_ino = ++last_ino; inode->i_state = 0; spin_unlock(&inode_lock); } return inode; } static struct inode *alloc_inode(struct super_block *sb) { static const struct address_space_operations empty_aops; static struct inode_operations empty_iops; static const struct file_operations empty_fops; struct inode *inode; if (sb->s_op->alloc_inode) inode = sb->s_op->alloc_inode(sb); else inode = (struct inode *) kmem_cache_alloc(inode_cachep, SLAB_KERNEL); if (inode) { struct address_space * const mapping = &inode->i_data; inode->i_sb = sb; inode->i_blkbits = sb->s_blocksize_bits; inode->i_flags = 0; atomic_set(&inode->i_count, 1); inode->i_op = &empty_iops; inode->i_fop = &empty_fops; inode->i_nlink = 1; atomic_set(&inode->i_writecount, 0); inode->i_size = 0; inode->i_blocks = 0; inode->i_bytes = 0; inode->i_generation = 0; #ifdef CONFIG_QUOTA memset(&inode->i_dquot, 0, sizeof(inode->i_dquot)); #endif inode->i_pipe = NULL; inode->i_bdev = NULL; inode->i_cdev = NULL; inode->i_rdev = 0; inode->i_security = NULL; inode->dirtied_when = 0; if (security_inode_alloc(inode)) { if (inode->i_sb->s_op->destroy_inode) inode->i_sb->s_op->destroy_inode(inode); else kmem_cache_free(inode_cachep, (inode)); return NULL; } mapping->a_ops = &empty_aops; mapping->host = inode; mapping->flags = 0; mapping_set_gfp_mask(mapping, GFP_HIGHUSER); mapping->assoc_mapping = NULL; mapping->backing_dev_info = &default_backing_dev_info; /* * If the block_device provides a backing_dev_info for client * inodes then use that. Otherwise the inode share the bdev's * backing_dev_info. */ if (sb->s_bdev) { struct backing_dev_info *bdi; bdi = sb->s_bdev->bd_inode_backing_dev_info; if (!bdi) bdi = sb->s_bdev->bd_inode->i_mapping->backing_dev_info; mapping->backing_dev_info = bdi; } inode->i_private = 0; inode->i_mapping = mapping; } return inode; } 还记得 sb->s_op->alloc_inode(sb)吧,对,就是章节“Ext2 的索引节点对象”提到的 ext2_alloc_inode 函数,我们就不再赘述了。注意, VFS 的 inode 对象是嵌入在 ext2_inode_info 描述符中的,当执行了 ext2_alloc_inode 函数以后,new_inode()函数内部就会有一个未初始 化的 inode 结构。随后,new_inode()函数将这个 inode 分别插入到以 inode_in_use 和 sb->s_inodes 为首的循环链表中。 2. 回到 ext2_new_inode 中,接下来: struct ext2_inode_info *ei = EXT2_I(inode); struct ext2_sb_info *sbi = EXT2_SB(sb); struct ext2_super_block *es = sbi->s_es; if (S_ISDIR(mode)) { if (test_opt(sb, OLDALLOC)) group = find_group_dir(sb, dir); else group = find_group_orlov(sb, dir); } else group = find_group_other(sb, dir); 我们看到,与分配的 inode 相关的磁盘索引节点映像、磁盘超级快映像和磁盘超级快对象分 别由内部变量 ei、sbi 和 es 指向。随后如果新的索引节点是一个目录,函数就调用 find_group_orlov(sb, dir)为目录找到一个合适的块组(安装 ext2 时,如果带一个参数 OLDALLOC,则会强制内核使用一种简单、老式的方式分配块组: find_group_dir(sb, dir))。 find_group_orlov 函数的实现比较简单,我们只是概要地介绍一下其中的相关代码,主要描 述其中的逻辑关系。为一个目录分配块组总的策略是,尽量把一个该目录的目录项对应的普 通文件的所以块放在同一个块组中。该函数执行如下试探法: a. 以文件系统根 root 为父目录的目录项应该分散在各个块组。这样,函数在这些块组中去 查找一个组,这个组中的空闲索引节点数的比例和空闲块数的比例比平均值高( avefreei = freei / ngroups,avefreeb = free_blocks / ngroups)。如果没有这样的组则跳到第 c 步。 b. 如果满足下列条件,嵌套目录(父目录不是文件系统根 root)就应被存放到父目录块组: - 该组没有包含太多的目录( desc->bg_used_dirs_count < best_ndir) - 该组有足够多的空闲索引节点( desc->bg_free_inodes_count < min_inodes) - 改组有足够多的空闲块( desc->bg_free_blocks_count) < min_blocks) - 该组有一点小“债”。(块组的债存放在 ext2_sb_info 描述符的 s_debts 字段所指向的计 数器数组中。每当一个新目录加入,债加 1;每当其他类型的文件加入,债减 1) 这个“债”有个最大值: max_debt = EXT2_BLOCKS_PER_GROUP(sb) / max(blocks_per_dir, BLOCK_COST),即每个组的块数与每个目录的块数之比值。注意,这里是表示目录文件所 对应的块数,不是目录项对应的普通文件,其最大值为 BLOCK_COST,即 256。 如果父目录组 parent_group 没有满足这些条件,那么选择第一个上述满足条件的组。如果没 有满足条件的组,则跳到第 c 步。 c. 这是一个“退一步”原则( fallback),当找不到合适的组时使用。函数从包含父目录的块 组开始选择第一个满足条件的块组,这个条件是:它的空闲索引节点数比每块组空闲索引节 点数的平均值大。 d. find_group_orlov 函数最后返回分配给该目录的块组号( group) 3. 如果新索引节点不是个目录,则调用 find_group_other(sb, dir),在有空闲索引节点的块组中给它分配一个。该函数从包含父目录的组开始往下找。具体逻辑如下: a. 从包含父目录 dir 的块组开始,执行快速的对数查找。这种算法要查找 log(n)个块组,这 里 n 是块组总数。该算法一直向前查找直到找到一个可用的块组,具体如下:如果我们把开 始的块组称为 i,那么,该算法要查找的块组为 i mod (n),i+1 mod (n),i+1+2 mod (n),i+1+2+4 mod (n),等等。大家是不是觉得这个东西是不是似曾相识呀,不错,就是伙伴系统算法的 思想: int parent_group = EXT2_I(parent)->i_block_group; int ngroups = EXT2_SB(sb)->s_groups_count; group = (group + parent->i_ino) % ngroups; for (i = 1; i < ngroups; i <<= 1) { group += i; if (group >= ngroups) group -= ngroups; desc = ext2_get_group_desc (sb, group, &bh); if (desc && le16_to_cpu(desc->bg_free_inodes_count) && le16_to_cpu(desc->bg_free_blocks_count)) goto found; } b.如果该算法没有找到含有空闲索引节点的块组,就从包含父目录 dir 的块组开始执行彻底 的线性查找: group = parent_group; for (i = 0; i < ngroups; i++) { if (++group >= ngroups) group = 0; desc = ext2_get_group_desc (sb, group, &bh); if (desc && le16_to_cpu(desc->bg_free_inodes_count)) goto found; } 4. 好啦,得到了所分配的块组号 group,回到 ext2_new_inode 函数中,下一步要做的事是设 置位图。于是调用 read_inode_bitmap()得到所选块组的索引节点位图,并从中寻找第一个空 闲位,这样就得到了第一个空闲磁盘索引节点号: static struct buffer_head * read_inode_bitmap(struct super_block * sb, unsigned long block_group) { struct ext2_group_desc *desc; struct buffer_head *bh = NULL; desc = ext2_get_group_desc(sb, block_group, NULL); if (!desc) goto error_out; bh = sb_bread(sb, le32_to_cpu(desc->bg_inode_bitmap)); if (!bh) ext2_error(sb, "read_inode_bitmap", "Cannot read inode bitmap - " "block_group = %lu, inode_bitmap = %u", block_group, le32_to_cpu(desc->bg_inode_bitmap)); error_out: return bh; } 我们看到,read_inode_bitmap 函数接收超级块和组号作为参数,获得超级快的块组描述符结 构,然后将其索引节点位图地址对应的那个块缓存到页高速缓存中。最后将该高速缓存描述 符 buffer_head 返回。 5. 接下来 ext2_new_inode 函数要做的事,是分配磁盘索引节点:把索引节点位图中的相应 位置位: ext2_set_bit_atomic(sb_bgl_lock(sbi, group), ino, bitmap_bh->b_data) 并把含有这个位图的页高速缓存标记为脏: mark_buffer_dirty(bitmap_bh); 此外,如果文件系统安装时指定了 MS_SYNCHRONOUS 标 志 , 则 调 用 sync_dirty_buffer(bitmap_bh)开始 I/O 写操作并等待,直到写操作终止。 brelse(bitmap_bh),递减 bitmap_bh 的引用数。 6. 减小 ext2_sb_info 数据结构中的 s_freeinodes_counter 字段;而且如果新索引节点是目录, 则增大 ext2_sb_info 数据结构的 s_dirs counter 字段。 percpu_counter_mod(&sbi->s_freeinodes_counter, -1); if (S_ISDIR(mode)) percpu_counter_inc(&sbi->s_dirs_counter); 7. 减小组描述符的 bg_free_inodes_count 字段。如果新的索引节点是一个目录,则增加 bg_used_dirs_count 字段,并把含有这个组描述符的缓冲区标记为脏: gdp = ext2_get_group_desc(sb, group, &bh2); gdp->bg_free_inodes_count = cpu_to_le16(le16_to_cpu(gdp->bg_free_inodes_count) - 1); if (S_ISDIR(mode)) { if (sbi->s_debts[group] < 255) sbi->s_debts[group]++; gdp->bg_used_dirs_count = cpu_to_le16(le16_to_cpu(gdp->bg_used_dirs_count) + 1); } else { if (sbi->s_debts[group]) sbi->s_debts[group]--; } sb->s_dirt = 1; mark_buffer_dirty(bh2); inode->i_uid = current->fsuid; 注意,依据索引节点指向的是普通文件或目录,相应增减超级块内 s_debts 数组中的组计数 器。 8. 初始化这个索引节点对象的字段。特别是,设置索引节点号 i_no,并把 xtime.tv_sec 的值 拷贝到 i_atime、i_mtime 及 i_ctime。把这个块组的索引赋给 ext2_inode_info 结构的 i_block_group 字段: ei->i_block_group = group; 9. 初始化这个索引节点对象的访问控制列表( ACL): err = ext2_init_acl(inode, dir); if (err) goto fail_free_drop; 10. 将新索引节点对象插入散列表 inode_hashtable(insert_inode_hash(inode),其数据结构详 情请参考《把 Linux 中的 VFS 对象串联起来》),调用 mark_inode_dirty()把该索引节点对象 移进超级块脏索引节点链表: static inline void insert_inode_hash(struct inode *inode) { __insert_inode_hash(inode, inode->i_ino); } void __insert_inode_hash(struct inode *inode, unsigned long hashval) { struct hlist_head *head = inode_hashtable + hash(inode->i_sb, hashval); spin_lock(&inode_lock); hlist_add_head(&inode->i_hash, head); spin_unlock(&inode_lock); } static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { struct hlist_node *first = h->first; n->next = first; if (first) first->pprev = &n->next; h->first = n; n->pprev = &h->first; } static inline void mark_inode_dirty(struct inode *inode) { __mark_inode_dirty(inode, I_DIRTY); } void __mark_inode_dirty(struct inode *inode, int flags) { struct super_block *sb = inode->i_sb; /* * Don't do this for I_DIRTY_PAGES - that doesn't actually * dirty the inode itself */ if (flags & (I_DIRTY_SYNC | I_DIRTY_DATASYNC)) { if (sb->s_op->dirty_inode) sb->s_op->dirty_inode(inode); } /* * make sure that changes are seen by all cpus before we test i_state * -- mikulas */ smp_mb(); /* avoid the locking if we can */ if ((inode->i_state & flags) == flags) return; if (unlikely(block_dump)) { struct dentry *dentry = NULL; const char *name = "?"; if (!list_empty(&inode->i_dentry)) { dentry = list_entry(inode->i_dentry.next, struct dentry, d_alias); if (dentry && dentry->d_name.name) name = (const char *) dentry->d_name.name; } if (inode->i_ino || strcmp(inode->i_sb->s_id, "bdev")) printk(KERN_DEBUG "%s(%d): dirtied inode %lu (%s) on %s\n", current->comm, current->pid, inode->i_ino, name, inode->i_sb->s_id); } spin_lock(&inode_lock); if ((inode->i_state & flags) != flags) { const int was_dirty = inode->i_state & I_DIRTY; inode->i_state |= flags; /* * If the inode is locked, just update its dirty state. * The unlocker will place the inode on the appropriate * superblock list, based upon its state. */ if (inode->i_state & I_LOCK) goto out; /* * Only add valid (hashed) inodes to the superblock's * dirty list. Add blockdev inodes as well. */ if (!S_ISBLK(inode->i_mode)) { if (hlist_unhashed(&inode->i_hash)) goto out; } if (inode->i_state & (I_FREEING|I_CLEAR)) goto out; /* * If the inode was already on s_dirty or s_io, don't * reposition it (that would break s_dirty time-ordering). */ if (!was_dirty) { inode->dirtied_when = jiffies; list_move(&inode->i_list, &sb->s_dirty); } } out: spin_unlock(&inode_lock); } 11. 调用 ext2_preread_inode()从磁盘读入包含该索引节点的块,将它存入页高速缓存。进行 这种预读是因为最近创建的索引节点可能会被很快写入。 static void ext2_preread_inode(struct inode *inode) { unsigned long block_group; unsigned long offset; unsigned long block; struct buffer_head *bh; struct ext2_group_desc * gdp; struct backing_dev_info *bdi; bdi = inode->i_mapping->backing_dev_info; if (bdi_read_congested(bdi)) return; if (bdi_write_congested(bdi)) return; block_group = (inode->i_ino - 1) / EXT2_INODES_PER_GROUP(inode->i_sb); gdp = ext2_get_group_desc(inode->i_sb, block_group, &bh); if (gdp == NULL) return; /* * Figure out the offset within the block group inode table */ offset = ((inode->i_ino - 1) % EXT2_INODES_PER_GROUP(inode->i_sb)) * EXT2_INODE_SIZE(inode->i_sb); block = le32_to_cpu(gdp->bg_inode_table) + (offset >> EXT2_BLOCK_SIZE_BITS(inode->i_sb)); sb_breadahead(inode->i_sb, block); } static inline void sb_breadahead(struct super_block *sb, sector_t block) { __breadahead(sb->s_bdev, block, sb->s_blocksize); } 12. 返回新索引节点对象 inode 的地址。 3.3.2 删除索引节点 内核调用 ext2_free_inode()函数删除一个磁盘索引节点,把磁盘索引节点表示为索引节点对 象,其地址作为参数来传递。内核在进行一系列的清除操作(包括清除内部数据结构和文件 中的数据)之后调用这个函数。具体来说,它在下列操作完成之后才执行:索引节点对象已 经从散列表中删除,指向这个索引节点的最后一个硬链接已经从适当的目录中删除,文件的长度截为 0 以回收它的所有数据块。 函数执行下列操作: 1. 调用 clear_inode(),它依次执行如下步骤: a.删除与索引节点关联的“间接”脏缓冲区。它们都存放在一个链表中,该链表的首部在 address_space 对象 inode->i_data 的 private_list 字段。 b.如果索引节点的 I_LOCK 标志置位,则说明索引节点中的某些缓冲区正处于 I/O 数据传送 中;于是,函数挂起当前进程,直到这些 1/O 数据传送结束。 c.调用超级块对象的 clear_inode 方法(如果已定义),但 Ext2 文件系统没定义这个方法。 d.如果索引节点指向一个设备文件,则从设备的索引节点链表中删除索引节对象,这个链表 要么在 cdev 字符设备描述符的 cdev 字段,要么在 block_device 块设备描述符的 bd_inodes 段。 e.把索引节点的状态置为 I_CLEAR(表示索引节点对象的内容不再有意义)。 2. 从每个块组的索引节点号和索引节点数计算包含这个磁盘索引节点的块组的索引。 3. 调用 read_inode_bitmap()得到索引节点位图。 4. 增加组描述符的 bg_free_inodes_count 字段。如果删除的索引节点是一个目录,那么也要 减小 bg_used_dirs_count 字段。把这个组描述符所在的缓冲区标记为脏。 5. 如果删除的索引节点是一个目录,就减小 ext2_sb_info 结构的 s_dirs_counter 字段,把超 级块的 s_dirt 标志置 1,并把它所在的缓冲区标记为脏。 6. 清除索引节点位图中这个磁盘索引节点对应的位,并把包含这个位图的缓冲区标记为脏。 此外,如果文件系统以 MS_SYNCHRONIZE 标志安装,则 sync_dirty_buffer()并等待,直到 在位图缓冲区上的写操作终止。 3.4 Ext2 数据块分配 跟索引节点一样,Ext2 也对磁盘数据块进行分配与释放。在详细分析相关代码之前,先引 出两个重要的预备,一个是数据块寻址,一个是文件的洞 3.4.1 数据块寻址 每个非空的普通文件都由一组数据块组成。这些块或者由文件内的相对位置(它们的文件块 号)来标识,或者由磁盘分区内的位置(它们的逻辑块号)来标识。 从文件内的偏移量 f 导出相应数据块的逻辑块号需要两个步骤: 1. 从偏移量 f 导出文件的块号,即在偏移量 f 处的字符所在的块索引。 2. 把文件的块号转化为相应的逻辑块号。 因为 Unix 文件不包含任何控制字符,因此,导出文件的第 f 个字符所在的文件块号当容易 的,只是用 f 除以文件系统块的大小,并取整即可。 例如,让我们假定块的大小为 4KB。如果 f 小于 4096,那么这个字符就在文件的第一数据 块中,其文件的块号为 O。如果 f 等于或大于 4096 而小于 8192,则这个字符就在文件块号 为 1 的数据块中,以此类推。 得到了文件的块号是第一步。但是,由于 Ext2 文件的数据块在磁盘上不必是相邻的,因此 把文件的块号转化为相应的逻辑块号可不是这么直截了当的了。因此, Ext2 文件系统必须 提供一种方法,用这种方法可以在磁盘上建立每个文件块号与相应逻辑块号之间的关系。在 索引节点内部部分实现了这种映射(回到了 AT&T Unix 的早期版本)。这种映射也涉及一些 包含额外指针的专用块,这些块用来处理大型文件的索引节点的扩展。 ext2 磁盘索引节点 ext2_inode 的 i_block 字段是一个有 EXT2_N_BLOCKS 个元素且包含逻辑 块号的数组。在下面的讨论中,我们假定 EXT2_N_BLOCKS 的默认值为 15(实际上到 2.6.18 这个值都一直是 15)。如图所示,这个数组表示一个大型数据结构的初始化部分。 正如从图中所看到的,数组的 15 个元素有 4 种不同的类型: - 最初的 12 个元素产生的逻辑块号与文件最初的 12 个块对应,即对应的文件块号从 0 - 11。 - 下标 12 中的元素包含一个块的逻辑块号(叫做间接块),这个块中存放着一个表示逻辑块 号的二级数组。这个数组的元素对应的文件块号从 12 到 b/4+11,这里 b 是文件系统的块 大小(每个逻辑块号占 4 个字节,因此我们在式子中用 4 作除数,如果块大小是 4096,则 该数组对应文件块号从 12 到 1035)。因此,内核为了查找指向一个块的指针必须先访问这 个元素,然后,在这个块中找到另一个指向最终块(包含文件内容)的指针。 - 下标 13 中的元素包含一个间接块的逻辑块号,而这个块包含逻辑块号的一个二级数组, 这个二级数组的数组项依次指向三级数组,这个三级数组存放的才是文件块号对应的逻辑块 号,范围从 b/4+12 到(b/4)^2+(b/4)+11。如果块大小是 4096,则范围是从 1036 到 1049611。 - 最后,下标 14 中的元素使用三级间接索引,第四级数组中存放的才是文件块号对应的逻 辑块号,范围从 (b/4)^2+(b/4)+12 到(b/4)^3+(b/4)^2+(b/4)+11。 在图中,块内的数字表示相应的文件块号。箭头(表示存放在数组元素中的逻辑块号)指示 了内核如何通过间接块找到包含文件实际内容的块。 注意这种机制是如何支持小文件的。如果文件需要的数据块小于 12,那么两次磁盘访问就 可以检索到任何数据:一次是读磁盘索引节点 i_block 数组的一个元素,另一次是读所需要 的数据块。对于大文件来说,可能需要三四次的磁盘访问才能找到需要的块。实际上,这是 一种最坏的估计,因为目录项、索引节点、页高速缓存都有助于极大地减少实际访问磁盘的 次数。 还要注意文件系统的块大小是如何影响寻址机制的,因为大的块允许 Ext2 把更多的逻辑块 号存放在一个单独的块中。例如,如果块的大小是 1024 字节,并且文件包含的数据最多为 268KB,那么,通过直接映射可以访问文件最初的 12KB 数据,通过简单的间接映射可以访 问剩的 13KB 到 268KB 的数据。大于 2GB 的大型文件通过指定 O_LARGEFILE 打开标志必 须在 32 位体系结构上进行打开。 3.4.2 文件的洞 文件的洞(file hole)是普通文件的一部分,它是一些空字符但没有存放在磁盘的任何数据 块中。洞是 Unix 文件一直存在的一个特点。例如,下列的 Unix 命令创建了第一个字节是洞 的文件: [root@localhost]# echo -n "X" | dd of=/tmp/hole bs=1024 seek=6 现在,/tmp/hole 有 6145 个字符(6144 个空字符加一个 X 字符),然而,这个文件在磁盘上 只占一个数据块。 引人文件的洞是为了避免磁盘空间的浪费。它们因此被广泛地用在数据库应用中,更一般地 说,用于在文件上进行散列的所有应用。 文件洞在 Ext2 中的实现是基于动态数据块的分配的:只有当进程需要向一个块写数据时, 才真正把这个块分配给文件。每个索引节点的 i_size 字段定义程序所看到的文件大小,包括 洞,而 i_blocks 字段存放分配给文件有效的数据块数(以 512 字节为单位)。 在前面 dd 命令的例子中,假定/tmp/hole 文件创建在块大小为 4096 的 Ext2 分区上。其相应 磁盘索引节点的 i_size 字段存放的数为 6145,而 i_blocks 字段存放的数为 8(因为每 4096 字节的逻辑块包含 8 个 512 字节的物理块)。 i_block 数组的第二个元素(对应块的文件块号 为 1)存放已分配块的逻辑块号,而数组中的其他元素都为空(参看下图)。 3.4.3 分配数据块 当内核要分配一个数据块来保存 Ext2 普通文件的数据时,就调用 ext2_get_block()函数。如 果块不存在,该函数就自动为文件分配块。请记住,每当内核在 Ext2 普通文件上执行读或 写操作时就调用这个函数;显然,这个函数只在页高速缓存内没有相应的块时才被调用。 ext2_get_block() 函数处理在刚才“数据块寻址”描述的数据结构,并在必要时调用 ext2_alloc_block()函数在 Ext2 分区真正搜索一个空闲块。如果需要,该函数还为间接寻址分 配相应的块(参见本节第一个图)。 为了减少文件的碎片, Ext2 文件系统尽力在已分配给文件的最后一个块附近找一个新块分 配给该文件。如果失败, Ext2 文件系统又在包含这个文件索引节点的块组中搜寻一个新的 块。如果还是失败,作为最后一个办法,可以从其他一个块组中获得空闲块。 Ext2 文件系统使用数据块的预分配策略。文件并不仅仅获得所需要的块,而是获得一组多 达 8 个邻接的块。 ext2_inode_info 结构的 i_prealloc_count 字段存放预分配给某一文件但还没 有使用的数据块的数量,而 i_prealloc_block字段存放下一次要使用的预分配块的逻辑块号。 当下列情况发生时,释放预分配而一直没有使用的块:当文件被关闭时,当文件被缩短时, 或者当一个写操作相对于引发块预分配的写操作不是顺序的时。 ext2_alloc_block()函数接收的参数为指向索引节点对象的指针、目标( goal)和存放错误码 的变量地址。目标是一个逻辑块号,表示新块的首选位置: static unsigned long ext2_alloc_block (struct inode * inode, unsigned long goal, int *err) { #ifdef EXT2FS_DEBUG static unsigned long alloc_hits, alloc_attempts; #endif unsigned long result; #ifdef EXT2_PREALLOCATE struct ext2_inode_info *ei = EXT2_I(inode); write_lock(&ei->i_meta_lock); if (ei->i_prealloc_count && (goal == ei->i_prealloc_block || goal + 1 == ei->i_prealloc_block)) { result = ei->i_prealloc_block++; ei->i_prealloc_count--; write_unlock(&ei->i_meta_lock); ext2_debug ("preallocation hit (%lu/%lu).\n", ++alloc_hits, ++alloc_attempts); } else { write_unlock(&ei->i_meta_lock); ext2_discard_prealloc (inode); ext2_debug ("preallocation miss (%lu/%lu).\n", alloc_hits, ++alloc_attempts); if (S_ISREG(inode->i_mode)) /* 如果是普通文件 */ result = ext2_new_block (inode, goal, &ei->i_prealloc_count, &ei->i_prealloc_block, err); else /* 如果是目录或符号链接 */ result = ext2_new_block(inode, goal, NULL, NULL, err); } #else result = ext2_new_block (inode, goal, 0, 0, err); #endif return result; } 代码很容易看懂,如果先前有预分配,则直接返回 ei->i_prealloc_block++,没有,则丢弃所 有剩余的预分配块 ext2_discard_prealloc(inode),并调用 ext2_new_block 函数分配一个块: unsigned long ext2_new_block(struct inode *inode, unsigned long goal, u32 *prealloc_count, u32 *prealloc_block, int *err) { struct buffer_head *bitmap_bh = NULL; struct buffer_head *gdp_bh; /* bh2 */ struct ext2_group_desc *desc; int group_no; /* i */ int ret_block; /* j */ int group_idx; /* k */ unsigned long target_block; /* tmp */ unsigned long block = 0; struct super_block *sb = inode->i_sb; struct ext2_sb_info *sbi = EXT2_SB(sb); struct ext2_super_block *es = sbi->s_es; unsigned group_size = EXT2_BLOCKS_PER_GROUP(sb); unsigned prealloc_goal = es->s_prealloc_blocks; unsigned group_alloc = 0, es_alloc, dq_alloc; int nr_scanned_groups; if (!prealloc_goal--) prealloc_goal = EXT2_DEFAULT_PREALLOC_BLOCKS - 1; if (!prealloc_count || *prealloc_count) prealloc_goal = 0; if (DQUOT_ALLOC_BLOCK(inode, 1)) { *err = -EDQUOT; goto out; } while (prealloc_goal && DQUOT_PREALLOC_BLOCK(inode, prealloc_goal)) prealloc_goal--; dq_alloc = prealloc_goal + 1; es_alloc = reserve_blocks(sb, dq_alloc); if (!es_alloc) { *err = -ENOSPC; goto out_dquot; } ext2_debug ("goal=%lu.\n", goal); if (goal < le32_to_cpu(es->s_first_data_block) || goal >= le32_to_cpu(es->s_blocks_count)) goal = le32_to_cpu(es->s_first_data_block); group_no = (goal - le32_to_cpu(es->s_first_data_block)) / group_size; desc = ext2_get_group_desc (sb, group_no, &gdp_bh); if (!desc) { /* * gdp_bh may still be uninitialised. But group_release_blocks * will not touch it because group_alloc is zero. */ goto io_error; } group_alloc = group_reserve_blocks(sbi, group_no, desc, gdp_bh, es_alloc); if (group_alloc) { ret_block = ((goal - le32_to_cpu(es->s_first_data_block)) % group_size); brelse(bitmap_bh); bitmap_bh = read_block_bitmap(sb, group_no); if (!bitmap_bh) goto io_error; ext2_debug("goal is at %d:%d.\n", group_no, ret_block); ret_block = grab_block(sb_bgl_lock(sbi, group_no), bitmap_bh->b_data, group_size, ret_block); if (ret_block >= 0) goto got_block; group_release_blocks(sb, group_no, desc, gdp_bh, group_alloc); group_alloc = 0; } ext2_debug ("Bit not found in block group %d.\n", group_no); /* * Now search the rest of the groups. We assume that * i and desc correctly point to the last group visited. */ nr_scanned_groups = 0; retry: for (group_idx = 0; !group_alloc && group_idx < sbi->s_groups_count; group_idx++) { group_no++; if (group_no >= sbi->s_groups_count) group_no = 0; desc = ext2_get_group_desc(sb, group_no, &gdp_bh); if (!desc) goto io_error; group_alloc = group_reserve_blocks(sbi, group_no, desc, gdp_bh, es_alloc); } if (!group_alloc) { *err = -ENOSPC; goto out_release; } brelse(bitmap_bh); bitmap_bh = read_block_bitmap(sb, group_no); if (!bitmap_bh) goto io_error; ret_block = grab_block(sb_bgl_lock(sbi, group_no), bitmap_bh->b_data, group_size, 0); if (ret_block < 0) { /* * If a free block counter is corrupted we can loop inifintely. * Detect that here. */ nr_scanned_groups++; if (nr_scanned_groups > 2 * sbi->s_groups_count) { ext2_error(sb, "ext2_new_block", "corrupted free blocks counters"); goto io_error; } /* * Someone else grabbed the last free block in this blockgroup * before us. Retry the scan. */ group_release_blocks(sb, group_no, desc, gdp_bh, group_alloc); group_alloc = 0; goto retry; } got_block: ext2_debug("using block group %d(%d)\n", group_no, desc->bg_free_blocks_count); target_block = ret_block + group_no * group_size + le32_to_cpu(es->s_first_data_block); if (target_block == le32_to_cpu(desc->bg_block_bitmap) || target_block == le32_to_cpu(desc->bg_inode_bitmap) || in_range(target_block, le32_to_cpu(desc->bg_inode_table), sbi->s_itb_per_group)) ext2_error (sb, "ext2_new_block", "Allocating block in system zone - " "block = %lu", target_block); if (target_block >= le32_to_cpu(es->s_blocks_count)) { ext2_error (sb, "ext2_new_block", "block(%d) >= blocks count(%d) - " "block_group = %d, es == %p ", ret_block, le32_to_cpu(es->s_blocks_count), group_no, es); goto io_error; } block = target_block; /* OK, we _had_ allocated something */ ext2_debug("found bit %d\n", ret_block); dq_alloc--; es_alloc--; group_alloc--; /* * Do block preallocation now if required. */ write_lock(&EXT2_I(inode)->i_meta_lock); if (group_alloc && !*prealloc_count) { unsigned n; for (n = 0; n < group_alloc && ++ret_block < group_size; n++) { if (ext2_set_bit_atomic(sb_bgl_lock(sbi, group_no), ret_block, (void*) bitmap_bh->b_data)) break; } *prealloc_block = block + 1; *prealloc_count = n; es_alloc -= n; dq_alloc -= n; group_alloc -= n; } write_unlock(&EXT2_I(inode)->i_meta_lock); mark_buffer_dirty(bitmap_bh); if (sb->s_flags & MS_SYNCHRONOUS) sync_dirty_buffer(bitmap_bh); ext2_debug ("allocating block %d. ", block); *err = 0; out_release: group_release_blocks(sb, group_no, desc, gdp_bh, group_alloc); release_blocks(sb, es_alloc); out_dquot: DQUOT_FREE_BLOCK(inode, dq_alloc); out: brelse(bitmap_bh); return block; io_error: *err = -EIO; goto out_release; } ext2_new_block()函数用下列策略在 Ext2 分区内搜寻一个空闲块: 1. 如果传递给 ext2_alloc_block()的首选块(目标块)是空闲的,就分配它。 2. 如果目标为忙,就检查首选块后的其余块之中是否有空闲的块。 3. 如果在首选块附近没有找到空闲块,就从包含目标的块组开始,查找所有的块组,对每 个块组有: a. 寻找至少有 8 个相邻空闲块的一个组块。 b. 如果没有找到这样的一组块,就寻找一个单独的空闲块。 下面我们就来详细分析这个函数,其接收的参数为: - inode:指向被分配块的文件的索引节点 - goal:由 ext2_alloc_block 传递过来的目标块号 - prealloc_count:指向打算预分配块的计数器的指针 - prealloc_block:指向预分配的第一个块的位置 - err:存放错误码的变量地址 只要找到一个空闲块,搜索就结束,返回该块的块号。在结束前, ext2_new_block()函数还 尽力在找到的空闲块附近的块中找 8 个空闲块进行预分配,并把磁盘索引节点的 i_prealloc_block 和 i_prealloc_count 字段置为适当的块位置及块数。函数执行以下步骤: 1. 首先初始化一些内部变量: struct buffer_head *bitmap_bh = NULL; struct buffer_head *gdp_bh; /* bh2 */ struct ext2_group_desc *desc; …… unsigned long block = 0; struct super_block *sb = inode->i_sb; struct ext2_sb_info *sbi = EXT2_SB(sb); struct ext2_super_block *es = sbi->s_es; unsigned group_size = EXT2_BLOCKS_PER_GROUP(sb); unsigned prealloc_goal = es->s_prealloc_blocks; unsigned group_alloc = 0, es_alloc, dq_alloc; 这些内部变量各有各的含义,其中, bitmap_bh 是位图的缓存; gdp_bh 是块组缓存;block 是当前搜索到的块号; sb 是 VFS 超级快结构,由 inode 的 i_sb 字段得出; sbi 是磁盘超级块 的内存对象描述符,由 VFS 超级快的 s_fs_info 字段得到;es 是磁盘超级快对象,由超级快 内存对象描述符的 s_es 字段得到; group_size 是磁盘块组的以块为单位的大小,由超级快的 s_blocks_per_group 字段得到; prealloc_goal 是已预分配的块数,由磁盘超级快对象的 s_prealloc_blocks 字段得到; group_alloc 表示同一个组分配块的数量。 2. 如果 prealloc_goal 减 1 为 0 了,则说明预分配的块已经用完,则: if (!prealloc_goal--) prealloc_goal = EXT2_DEFAULT_PREALLOC_BLOCKS - 1; if (!prealloc_count || *prealloc_count) prealloc_goal = 0; 其中 EXT2_DEFAULT_PREALLOC_BLOCKS 为 8,也就是需要重新预分配 8 个块。当然, 如果传递进来的参数 prealloc_count 为空或者是 0,则说明不是普通文件,或者没有启动预 分配机制,则 prealloc_goal 设置为 0 。 3. 对配额进行检查,分配的目标块超过了用户配额,则将出错对象 err 设置为-EDQUOT; 如果预分配超过了用户配额,则将预分配数量减至配额以内;检查完毕后,将预分配数量赋 给 dq_alloc 内部变量,再执行 reserve_blocks 函数检查预分配的块 dq_alloc 是否到达了保留 块,如果是则减去所处保留块的数量。如果得到的结果 es_alloc 为 0 了,则将出错对象 err 设置为-ENOSPC,表示 no space: if (DQUOT_ALLOC_BLOCK(inode, 1)) { *err = -EDQUOT; goto out; } while (prealloc_goal && DQUOT_PREALLOC_BLOCK(inode, prealloc_goal)) prealloc_goal--; dq_alloc = prealloc_goal + 1; es_alloc = reserve_blocks(sb, dq_alloc); if (!es_alloc) { *err = -ENOSPC; goto out_dquot; } 4. 如果目标块小于 1(es->s_first_data_block 总为 1,查看“ Ext2 磁盘数据结构”章节),或 者大于整个文件系统所有块的大小,则将 goal 设置为 1。 if (goal < le32_to_cpu(es->s_first_data_block) || goal >= le32_to_cpu(es->s_blocks_count)) goal = le32_to_cpu(es->s_first_data_block); 5. 得到 goal 所对应的块组号,并根据块组号获得对应的组描述符: group_no = (goal - le32_to_cpu(es->s_first_data_block)) / group_size; desc = ext2_get_group_desc (sb, group_no, &gdp_bh); 这里面的 ext2_get_group_desc 接收三个参数,sb 是 VFS 超级快,group_no 是块组号,&gdp_bh 是该块组对应的页高速缓存: struct ext2_group_desc * ext2_get_group_desc(struct super_block * sb, unsigned int block_group, struct buffer_head ** bh) { unsigned long group_desc; unsigned long offset; struct ext2_group_desc * desc; struct ext2_sb_info *sbi = EXT2_SB(sb); ⋯⋯ group_desc = block_group >> EXT2_DESC_PER_BLOCK_BITS(sb); offset = block_group & (EXT2_DESC_PER_BLOCK(sb) - 1); if (!sbi->s_group_desc[group_desc]) { ext2_error (sb, "ext2_get_group_desc", "Group descriptor not loaded - " "block_group = %d, group_desc = %lu, desc = %lu", block_group, group_desc, offset); return NULL; } desc = (struct ext2_group_desc *) sbi->s_group_desc[group_desc]->b_data; if (bh) *bh = sbi->s_group_desc[group_desc]; return desc + offset; } 函数里面的 group_desc 内部变量通过 block_group 参数获得组描述符数组的位置下标, ext2 磁盘组描述符 ext2_group_desc 缓存于 sbi->s_group_desc[group_desc]->b_data 的某个位置, 因为总是缓存的,见章节“ Ext2 的索引节点对象”最后那个表。 6. desc 指向了组描述符以后,事情就好办了。执行 group_reserve_blocks(sbi, group_no, desc, gdp_bh, es_alloc)查看 goal 对应的那个组内是否有连续 es_alloc 个空闲的块,当然,组内如 果一个空闲的块都没有,就返回 0。如果空闲的块小于 es_alloc,就返回可以分配的块数, 并修改组描述符的空闲块数,然后把组描述符对应的缓存标记为脏: static int group_reserve_blocks(struct ext2_sb_info *sbi, int group_no, struct ext2_group_desc *desc, struct buffer_head *bh, int count) { unsigned free_blocks; if (!desc->bg_free_blocks_count) return 0; spin_lock(sb_bgl_lock(sbi, group_no)); free_blocks = le16_to_cpu(desc->bg_free_blocks_count); if (free_blocks < count) count = free_blocks; desc->bg_free_blocks_count = cpu_to_le16(free_blocks - count); spin_unlock(sb_bgl_lock(sbi, group_no)); mark_buffer_dirty(bh); return count; } 7. 好了,group_alloc 局部变量就等于 group_reserve_blocks 的返回值。此时此刻, group_alloc 为 goal 期望的那个块可分配的数量。那么接下来就要做一个判断,如果 group_alloc 大于 0, 则说明首选块是空闲的,就分配它: if (group_alloc) { ret_block = ((goal - le32_to_cpu(es->s_first_data_block)) % group_size); bitmap_bh = read_block_bitmap(sb, group_no); if (!bitmap_bh) goto io_error; ret_block = grab_block(sb_bgl_lock(sbi, group_no), bitmap_bh->b_data, group_size, ret_block); if (ret_block >= 0) goto got_block; group_release_blocks(sb, group_no, desc, gdp_bh, group_alloc); group_alloc = 0; } read_block_bitmap 读取超级块 sb 对应块组 group_no 的那个位图,并把这个位图动态缓存到 bitmap_bh 页高速缓存中: static struct buffer_head * read_block_bitmap(struct super_block *sb, unsigned int block_group) { struct ext2_group_desc * desc; struct buffer_head * bh = NULL; desc = ext2_get_group_desc (sb, block_group, NULL); if (!desc) goto error_out; bh = sb_bread(sb, le32_to_cpu(desc->bg_block_bitmap)); if (!bh) ext2_error (sb, "read_block_bitmap", "Cannot read block bitmap - " "block_group = %d, block_bitmap = %u", block_group, le32_to_cpu(desc->bg_block_bitmap)); error_out: return bh; } 随后调用 grab_block(sb_bgl_lock(sbi, group_no), bitmap_bh->b_data, group_size, ret_block)检 查一下 goal 所对应的那个数据块位图对应的位是否为 0,如果是,则把它的块号赋给相对位 置 ret_block;如果不是,则 ext2_find_next_zero_bit在组内分配一个空闲块的块号到 ret_block。 当然,如果 ret_block 大于等于 0,则说明这个组内有空闲块,则跳到 got_block;如果小于 0, 就说明这个组的块已经分配完了,那么就把刚才给组描述符增加的那些值减回来,并把 group_alloc 重新设置为 0。 8. 如果 group_alloc 为 0,则说明组内没有 goal 期望的那个块可分配的数量那么多的块,就 到 retry 程序段,到其他组去找 es_alloc 个空闲块,具体代码跟前边一样。 9. 如果得到这个块了,那么 ret_block 就是这个连续块的第一个块的相对组头的位置,到 got_block 程序段,此时此刻,group_no 是该快所在的块组号,随后: target_block = ret_block + group_no * group_size + le32_to_cpu(es->s_first_data_block); target_block 就是要分配的实际逻辑块号(相对于目标块号)。 got_block 程序段随后对这个逻 辑块号进行一系列检查,包括这个块是否已经存放了索引节点、位图、组描述符等内容了。 当然,肯定不会出现这些问题的,因为这些都是系统 bug。 10. 设置位图(注意,是一个组内的预分配连续块都设置),并把 bitmap_bh 标记为脏。 write_lock(&EXT2_I(inode)->i_meta_lock); if (group_alloc && !*prealloc_count) { unsigned n; for (n = 0; n < group_alloc && ++ret_block < group_size; n++) { if (ext2_set_bit_atomic(sb_bgl_lock(sbi, group_no), ret_block, (void*) bitmap_bh->b_data)) break; } *prealloc_block = block + 1; *prealloc_count = n; es_alloc -= n; dq_alloc -= n; group_alloc -= n; } write_unlock(&EXT2_I(inode)->i_meta_lock); mark_buffer_dirty(bitmap_bh); 11. 最后返回这个物理块: brelse(bitmap_bh); block = target_block; return block 4 页面高速缓存 相信通过前面的虚拟文件系统 VFS 及一个具体的 Ext2 文件系统,大家对基本的 VFS 体系 有一个大致的掌握了吧。从本章开始,我们将讨论一些 VFS 底层的技术细节,磁盘高速缓 存就是其中一个重要的技术。磁盘高速缓存是一种软件机制,它允许系统把通常存放在磁盘 上的一些数据保留在 RAM 中,以便对那些数据的进一步访问而不用再访问磁盘。 因为对同一磁盘数据的反复访问频繁发生,所以磁盘高速缓存对系统性能至关重要。与磁盘 交互的用户态进程有权反复请求读或写同一磁盘数据。此外,不同的进程可能也需要在不同 的时间访问相同的磁盘数据。例如,你可以使用 cp 命令拷贝一个文本文件,然后调用你喜 欢的编辑器修改它。为了满足你的请求,命令 shell 将创建两个不同的进程,它们在不同的 时间访问同一个文件。 我们曾在前面的章节中提到过其他的磁盘高速缓存:目录项高速缓存和索引节点高速缓存, 前者存放的是描述文件系统路径名的目录项对象,而后者存放的是描述磁盘索引节点的索引 节点对象。不过要注意,目录项对象和索引结节点对象不只是存放一些磁盘块内容的缓冲区, 而是还加了一些内核感兴趣的其他信息,这些内容并不是从磁盘的某一个块上读取出来的; 由此而知,目录项高速缓存和索引节点高速缓存是特殊的磁盘高速缓存,但不是属于我们这 里讲的磁盘高速缓存概念的范围。 我们这里介绍的磁盘高速缓存其实是页高速缓存——一种对完整的数据页进行操作的磁盘 高速缓存,下面我们就从这个概念开始入手: 4.1 页高速缓存数据结构 页高速缓存( page cache)是 Linux 内核所使用的主要磁盘高速缓存。在绝大多数情况下, 内核在读写磁盘时都引用页高速缓存。新页被追加到页高速缓存以满足用户态进程的读请求。 如果页不在高速缓存中,新页就被加到高速缓存中,然后用从磁盘读出的数据填充它。如果 内存有足够的空闲空间,就让该页在高速缓存中长期保留,使其他进程再使用该页时不再访 问磁盘。 同样,在把一页数据写到块设备之前,内核首先检查对应的页是否已经在高速缓存中;如果 不在,就要先在其中增加一个新项,并用要写到磁盘中的数据填充该项。 I/O 数据的传送并 不是马上开始,而是要延迟几秒之后才对磁盘进行更新,从而使进程有机会对要写入磁盘的 数据做进一步的修改(换句话说,就是内核执行延迟的写操作)。 内核的代码和内核数据结构不必从磁盘读,也不必写入磁盘(如果要在关机后恢复系统的所 有状态——其实几乎不会出现这种情况,可以执行“挂起到磁盘”操作( hibernation),RAM 的全部内容保存到交换区,时此我们不做更多的讨论。),因此,页高速缓存中的页可能是下面的类型: - 含有普通文件数据的页。我们会在后面的章节描述内核如何处理它们的读、写和内存映射 操作。 - 含有目录的页。其实, Linux 采用与普通文件类似的方式操作目录文件。 - 含有直接从块设备文件(跳过文件系统层)读出的数据的页。内核处理这种页与处理含有 普通文件的页使用相同的函数集合。 - 含有用户态进程数据的页,但页中的数据已经被交换到磁盘。内核可能会强行在页高速缓 存中保留一些页面,而这些页面中的数据已经被写到交换区(可能是普通文件或磁盘分区)。 - 属于特殊文件系统文件的页,如共享内存的进程间通信( Interprocess Communication, IPC) 所使用的特殊文件系统 shm。 从上面我们可以得出结论,页高速缓存中的每个页所包含的数据肯定属于某个文件。这个文 件(或者更准确地说是文件的索引节点)就称为页的所有者( owner)。(即使含有换出数据 的页都属于同一个所有者,即使它们涉及不同的交换区。) 几乎所有的文件读和写操作都依赖于页高速缓存。只有在 O_DIRECT 标志被置位而进程打 开文件的情况下才会出现例外:此时, I/O 数据的传送绕过了页高速缓而使用了进程用户态 地址空间的缓冲区;少数数据库应用软件为了能采用自己的磁盘高速缓存算法而使用了 O_DIRECT 标志。 页高速缓存中的信息单位显然是一个完整的数据页。一个页中包含的磁盘块在物理上不一定 是相邻的,所以不能用设备号和块号来识别它,取而代之的是,通过页的所有者和所有者数 据中的索引(通常是一个索引节点和在相应文件中的偏移量)来识别页高速缓存中的页。 4.1.1 address_space 对象 页高速缓存的核心数据结构是 address_space 对象,它是一个嵌入在页所有者的索引节点对 象中的数据结构(页被换出可能会引起缺页异常,这些被换出的页拥有不在任何索引节点中 的公共 address_space 对象)。高速缓存中的许多页可能属于同一个所有者,从而可能被链接 到同一个 address_space 对象。该对象还在所有者的页和对这些页的操作之间建立起链接关 系。 每个页描述符都包括两个字段 mapping 和 index,把页链接到页高速缓存的。 mapping 字段 指向拥有页的索引节点的 address_space 对象,index 字段表示在所有者的地址空间中以页大 小为单位的偏移量,也就是在所有者的磁盘映像中页中数据的位置。在页高速缓存中查找页 时使用这两个字段。 值得庆幸的是,页高速缓存可以包含同一磁盘数据的多个副本。例如,可以用下述方式访问普通文件的同一 4KB 的数据块: - 读文件;因此,数据就包含在普通文件的索引节点所拥有的页中。 - 从文件所在的设备文件(磁盘分区)读取块。因此,数据就包含在块设备文件的主索引节 点所拥有的页中。 因此,两个不同 address_space 对象所引用的两个不同的页中出现了相同的磁盘数据。 address_space 对象包含如下所示: struct address_space { struct inode *host; /* 指向拥有该对象的索引节点的指针(如果存在) */ struct radix_tree_root page_tree; /* 表示拥有者页的基树( radix tree)的根 */ rwlock_t tree_lock; /* 保护基树的自旋锁 */ unsigned int i_mmap_writable;/* 地址空间中共享内存映射的个数 */ struct prio_tree_root i_mmap; /* radix 优先搜索树的根 */ struct list_head i_mmap_nonlinear;/* 地址空间中非线性内存区的链表 */ spinlock_t i_mmap_lock; /* 保护 radix 优先搜索树的自旋锁 */ unsigned int truncate_count; /* 截断文件时使用的顺序计数器 */ unsigned long nrpages; /* 所有者的页总数 */ pgoff_t writeback_index;/* 最后一次回写操作所作用的页的索引 */ const struct address_space_operations *a_ops; /* 对所有者页进行操作的方法 */ unsigned long flags; /* 错误位和内存分配器的标志 */ struct backing_dev_info *backing_dev_info; /* 指向拥有所有者数据的块设备的 backing_dev_info 的指针 */ spinlock_t private_lock; /* 通常是管理 private_list 链表时使用的自旋锁 */ struct list_head private_list; /* 通常是与索引节点相关的间接块的脏缓冲区的链表 */ struct address_space *assoc_mapping; /* 通常是指向间接块所在块设备的 address_space 对象的指针 */ } __attribute__((aligned(sizeof(long)))); 如果页高速缓存中页的所有者是一个文件, address_space 对象就嵌入在 VFS 索引节点对象 的 i_data 字段中。索引节点的 i_mapping 字段总是指向索引节点的数据页所有者的 address_space 对象。address_space 对象的 host 字段指向其所有者的索引节点对象。 因此,如果页属于一个文件(存放在 Ext3 文件系统中),那么页的所有者就是文件的索引节 点,而且相应的 address_space 对象存放在 VFS 索引节点对象的 i_data 字段中。索引节点的 i_mapping 字段指向同一个索引节点的 i_data 字段,而 address_space 对象的 host 字段也指向 这个索引节点。 不过,有些时候情况会更复杂。如果页中包含的数据来自块设备文件,即页含有存放着块设 备的“原始”数据,那么就把 address_space 对象嵌入到与该块设备相关的特殊文件系统 bdev 中文件的“主”索引节点中(块设备描述符的 bd_inode 字段引用这个索引节点)。因此,块 设备文件对应索引节点的 i_mapping 字段指向主索引节点中的 address_space 对象。相应地,address_space 对象的 host 字段指向主索引节点。这样,从块设备读取数据的所有页具有相 同的 address_space 对象,即使这些数据位于不同的块设备文件。 i_mmap, i_mmap_writable, i_mmap_nonlinear 和 i_mmap_lock 字段涉及内存映射和反映射, 我们将在后面的章节讨论这些主题。 backing_dev_info 字段指向 backing_dev_info 描述符,后者是对所有者的数据所在块设备进 行有关描述的数据结构。 private_list 字段是普通链表的首部,文件系统在实现其特定功能时可以随意使用。例如, Ext2 文件系统利用这个链表收集与索引节点相关的“间接”块的脏缓冲区。当刷新操作把索引节 点强行写入磁盘时,内核也同时刷新该链表中的所有缓冲区。此外,Ext2 文件系统在 assoc_mapping 字段中存放指向间接块所在块设备的 address_space 对 象 , 并 使 用 assoc_mapping->private_lock 自旋锁保护多处理器系统中的间接块链表。 address_space 对象的关键字段是 a_ops,它指向一个类型为 address_space_operations 的表,表 中定义了对所有者的页进行处理的各种方法: struct address_space_operations { /* 写操作(从页写到所有者的磁盘映像) */ int (*writepage)(struct page *page, struct writeback_control *wbc); /* 读操作(从所有者的磁盘映像读到页) */ int (*readpage)(struct file *, struct page *); /* 如果对所有者页进行的操作已准备好,则立刻开始 I/O 数据的传输 */ void (*sync_page)(struct page *); /* Write back some dirty pages from this mapping. */ /* 把指定数量的所有者脏页写回磁盘 */ int (*writepages)(struct address_space *, struct writeback_control *); /* Set a page dirty. Return true if this dirtied it */ /* 把所有者的页设置为脏页 */ int (*set_page_dirty)(struct page *page); /* 从磁盘中读所有者页的链表 */ int (*readpages)(struct file *filp, struct address_space *mapping, struct list_head *pages, unsigned nr_pages); /* * ext3 requires that a successful prepare_write() call be followed * by a commit_write() call - they must be balanced */ /* 为写操作做准备(由磁盘文件系统使用) */ int (*prepare_write)(struct file *, struct page *, unsigned, unsigned); /* 完成写操作(由磁盘文件系统使用) */ int (*commit_write)(struct file *, struct page *, unsigned, unsigned); /* Unfortunately this kludge is needed for FIBMAP. Don't use it */ /* 从文件块索引中获取逻辑块号 */ sector_t (*bmap)(struct address_space *, sector_t); /* 使所有者的页无效(截断文件时使用) */ void (*invalidatepage) (struct page *, unsigned long); /* 由日志文件系统使用以准备释放页 */ int (*releasepage) (struct page *, gfp_t); /* 所有者页的直接 I/O 传输(绕过页高速缓存) */ ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov, loff_t offset, unsigned long nr_segs); struct page* (*get_xip_page)(struct address_space *, sector_t, int); /* migrate the contents of a page to the specified target */ int (*migratepage) (struct address_space *, struct page *, struct page *); }; 其中最重要的方法是 readpage, writepage, prepare_write 和 commit_write。我们将在后面的章 节对它们进行讨论。在绝大多数情况下,这些方法把所有者的索引节点对象和访问物理设备 的低级驱动程序联系起来。例如,为普通文件的索引节点实现 readpage 方法的函数知道如 何确定文件页的对应块在物理磁盘设备上的位置。 4.1.2 基树 Linux 支持大到几个 TB 的文件。访问大文件时,页高速缓存中可能充满太多的文件页,以 至于顺序扫描这些页要消耗大量的时间。为了实现页高速缓存的高效查找, Linux2.6 采用了 大量的搜索树,其中每个 address_space 对象对应一棵搜索树。 address_space 对象的 page_tree 字段是基树(radix tree)的根,它包含指向所有者的页描述 符的指针。通过它,内核能够通过快速搜索操作来确定所需要的页是否在页高速缓存中。当 查找所需要的页时,内核把页索( page->index)引转换为基树中的路径,并快速找到页描述 符所(或应当)在的位置。如果找到,内核可以从基树获得页描述符,而且还可以很快确定 所找到的页是否是脏页(也就是应当被刷新到磁盘的页),以及其数据的 I/O 传送是否正在 进行。 基树的每个节点可以有多到 64 个指针指向其他节点或页描述符。底层节点存放指向页描述 符的指针(叶子节点),而上层的节点存放指向其他节点(孩子节点)的指针。每个节点由 radix_tree_node 数据结构表示,它包括三个字段: slots 是包括 64 个指针数组,count 是记录 节点中非空指针数量的计数器, tags 是二维的标志数组: struct radix_tree_node { unsigned int count; void *slots[RADIX_TREE_MAP_SIZE]; unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; }; #define RADIX_TREE_MAP_SIZE (1UL << RADIX_TREE_MAP_SHIFT) /* 64 */ #define RADIX_TREE_MAP_SHIFT 3 树根由 radix_tree_root 数据结构表示,他有三个字段: height 表示树的当前深度(不包括叶 子节点的层数), gfp_mask 指定为新节点请求内存时所用的标志, mode 指向与树中第一层 节点相应的数据结构 radix_tree_node(如果有的话): struct radix_tree_root { unsigned int height; gfp_t gfp_mask; struct radix_tree_node *rnode; }; 我们来看一个简单的例子。如果树中的索引都小于 63,那么树的深度就等于 1,因为可能存 在的 64 个叶子可以都存放在第一层的节点中 [如图( a)所示]。不过,如果与索引 131 相应 的新页的描述符肯定存放在页高速缓存中,那么树的深度就增加为 2,这样基树就可以查找 多达 4095 个索引[如图(b)所示]。 回顾一下分页系统是如何利用页表实现线性地址到物理地址转换的,从而理解如何实现页查 找。线性地址最高 20 位分成两个 10 位的字段:第一个字段是页目录中的偏移量,而第二个 字段是某个页目录项所指向的页表中的偏移量。 基树中使用类似的方法。页索引相当于线性地址,不过页索引中要考虑的字段的数量依赖于基树的深度。如果基树的深度为 1,就只能表示从 0~63 范围的索引,因此页索引的低 6 位 被解释为 slots 数组的下标,每个下标对应第一层的一个节点。如果基树的深度为 2,就可 以表示从 0~4095 范围的索引,页索引的低 12 位分成两个 6 位的字段,高位的字段用于表示 第一层节点数组的下标,而低位的字段用于表示第二层数组的下标。依此类推,如果深度等 于 6,页索引的最高两位(因为 page->index 是 32 位的)表示第一层节点数组的下标,接下 来的 6 位表示第二层节点数组的下标,这样一直到最低 6 位,它们表示第六层节点数组的下 标。 如果基树的最大索引小于应该增加的页的索引,那么内核相应地增加树的深度;基树的中间 节点依赖于页索引的值。 4.2 高速缓存底层处理函数 好了,在了解了页高速缓存相关的数据结构以后,我们来介绍一下基本的页高速缓存处理函 数: 对页高速缓存操作的基本高级函数有查找、增加和删除页。在以上函数的基础上还有另一个 函数确保高速缓存包含指定页的最新版本。 4.2.1 查找页 函数 find_get_page()接收的参数为指向 address_space 对象的指针和偏移量。它获取地址空间 的自旋锁,并调用 radix_tree_lookup()函数搜索拥有指定偏移量的基树的叶子节点: struct page * find_get_page(struct address_space *mapping, unsigned long offset) { struct page *page; read_lock_irq(&mapping->tree_lock); page = radix_tree_lookup(&mapping->page_tree, offset); if (page) page_cache_get(page); read_unlock_irq(&mapping->tree_lock); return page; } 函数 radix_tree_lookup 根据偏移量值中的位依次从树根开始并向下搜索,如上节所述。如果 遇到空指针,函数返回 NULL;否则,返回叶子节点的地址,也就是所需要的页描述符指针。 如果找到了所需要的页,find_get_page()函数就增加该页的使用计数器,释放自旋锁,并返 回该页的地址;否则,函数就释放自旋锁并返回 NULL: void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index) { void **slot; slot = __lookup_slot(root, index); return slot != NULL ? *slot : NULL; } static inline void **__lookup_slot(struct radix_tree_root *root, unsigned long index) { unsigned int height, shift; struct radix_tree_node **slot; height = root->height; if (index > radix_tree_maxindex(height)) return NULL; if (height == 0 && root->rnode) return (void **)&root->rnode; shift = (height-1) * RADIX_TREE_MAP_SHIFT; slot = &root->rnode; while (height > 0) { if (*slot == NULL) return NULL; slot = (struct radix_tree_node **) ((*slot)->slots + ((index >> shift) & RADIX_TREE_MAP_MASK)); shift -= RADIX_TREE_MAP_SHIFT; height--; } return (void **)slot; } 函数 find_get_pages()与 find_get_page()类似,但它实现在高速缓存中查找一组具有相邻索引 的页。它接收的参数是:指向 address_space 对象的指针、地址空间中相对于搜索起始位置 的偏移量、所检索到页的最大数量、指向由该函数赋值的页描述符数组的指针。 find_get_pages()依赖 radix_tree_gang_lookup()函数实现查找操作,radix_tree_gang_lookup() 函数为指针数组赋值并返回找到的页数。尽管由于一些页可能不在页高速缓存中而会出现空缺的页索引,但所返回的页还是递增的索引值。 还有另外几个函数实现页高速缓存上的查找操作。例如, find_lock_page() 函 数 与 find_get_page()类似,但它增加返回页的使用记数器,并调用 lock_page()设置 PG_locked 标 志,从而当函数返回时调用者能够以互斥的方式访问返回的页。随后,如果页已经被加锁, lock_Page()函数就阻塞当前进程。最后,它在 PG_locked 位置位时调用__wait_on_bit_lock() 函数。后面的函数把当前进程置为 TASK_UNINTERRUPTIBLE 状态,把进程描述符存入等 待队列,执行 address_space 对象的 sync_page 方法以取消文件所在块设备的请求队列,最后 调用 schedule()函数来挂起进程,直到把 PG_locked 标志清 0。内核使用 unlock_page()函数 对页进行解锁,并唤醒在等待队列上睡眠的进程。 函数 find_trylock_page()与 find_lock_page()类似,仅有一点不同,就是 find_trylock_page()从 不阻塞:如果被请求的页已经上锁,函数就返回错误码。最后要说明的是,函数 find_or_create_page()执行 find_lock_page();不过,如果找不到所请求的页,就分配一个新页 并把它插人页高速缓存。 4.2.2 增加页 函数 add_to_page_cache()把一个新页的描述符插入到页高速缓存。它接收的参数有:页描述 符的地址 page, address_space 对象的地址 mapping, 表示在地址空间内的页索引的值 offset 和为基树分配新节点时所使用的内存分配标志 gfp_mask。函数执行以下操作: int add_to_page_cache(struct page *page, struct address_space *mapping, pgoff_t offset, gfp_t gfp_mask) { int error = radix_tree_preload(gfp_mask & ~__GFP_HIGHMEM); if (error == 0) { write_lock_irq(&mapping->tree_lock); error = radix_tree_insert(&mapping->page_tree, offset, page); if (!error) { page_cache_get(page); SetPageLocked(page); page->mapping = mapping; page->index = offset; mapping->nrpages++; trace_add_to_page_cache(mapping, offset); __inc_zone_page_state(page, NR_FILE_PAGES); } write_unlock_irq(&mapping->tree_lock); radix_tree_preload_end(); } return error; } add_to_page_cache 首先调用 radix_tree_preload()函数,它禁用内核抢占,并把一些空的 radix_tree_node 结构赋给每 CPU 变量 radix_tree_preloads。radix_tree_node 结构的分配由 slab 分 配 器 高 速 缓 存 radix_tree_node_cachep 来 完 成 。 如 果 radix_tree_preload() 预 分 配 radix_tree_node 结构不成功,函数 add_to_page_cache()就终止并返回错误码 -ENOMEM。否 则,如果 radix_tree_preload()成功地完成预分配, add_to_page_cache()函数肯定不会因为缺乏 空闲内存或因为文件的大小达到了 64GB 而无法完成新页描述符的插入: int radix_tree_preload(gfp_t gfp_mask) { struct radix_tree_preload *rtp; struct radix_tree_node *node; int ret = -ENOMEM; preempt_disable(); rtp = &__get_cpu_var(radix_tree_preloads); while (rtp->nr < ARRAY_SIZE(rtp->nodes)) { preempt_enable(); node = kmem_cache_alloc(radix_tree_node_cachep, gfp_mask); if (node == NULL) goto out; preempt_disable(); rtp = &__get_cpu_var(radix_tree_preloads); if (rtp->nr < ARRAY_SIZE(rtp->nodes)) rtp->nodes[rtp->nr++] = node; else kmem_cache_free(radix_tree_node_cachep, node); } ret = 0; out: return ret; } add_to_page_cache 随后获取 mapping->tree_lock 自旋锁——注意, radix_tree_preload()函数已 经禁用了内核抢占。 调用 radix_tree_insert()在树中插入新节点,该函数执行下述操作: int radix_tree_insert(struct radix_tree_root *root, unsigned long index, void *item) { struct radix_tree_node *node = NULL, *slot; unsigned int height, shift; int offset; int error; /* Make sure the tree is high enough. */ if (index > radix_tree_maxindex(root->height)) { error = radix_tree_extend(root, index); if (error) return error; } slot = root->rnode; height = root->height; shift = (height-1) * RADIX_TREE_MAP_SHIFT; offset = 0; /* uninitialised var warning */ while (height > 0) { if (slot == NULL) { /* Have to add a child node. */ if (!(slot = radix_tree_node_alloc(root))) return -ENOMEM; if (node) { node->slots[offset] = slot; node->count++; } else root->rnode = slot; } /* Go a level down */ offset = (index >> shift) & RADIX_TREE_MAP_MASK; node = slot; slot = node->slots[offset]; shift -= RADIX_TREE_MAP_SHIFT; height--; } if (slot != NULL) return -EEXIST; if (node) { node->count++; node->slots[offset] = item; BUG_ON(tag_get(node, 0, offset)); BUG_ON(tag_get(node, 1, offset)); } else { root->rnode = item; BUG_ON(root_tag_get(root, 0)); BUG_ON(root_tag_get(root, 1)); } return 0; } 注意,radix_tree_insert 首先调用 radix_tree_maxindex()获得最大索引,该索引可能被插人具 有当前深度的基树;如果新页的索引不能用当前深度表示,就调用 radix_tree_extend()通过 增加适当数量的节点来增加树的深度: static int radix_tree_extend(struct radix_tree_root *root, unsigned long index) { struct radix_tree_node *node; unsigned int height; int tag; /* Figure out what the height should be. */ height = root->height + 1; while (index > radix_tree_maxindex(height)) height++; if (root->rnode == NULL) { root->height = height; goto out; } do { if (!(node = radix_tree_node_alloc(root))) return -ENOMEM; /* Increase the height. */ node->slots[0] = root->rnode; /* Propagate the aggregated tag info into the new root */ for (tag = 0; tag < RADIX_TREE_MAX_TAGS; tag++) { if (root_tag_get(root, tag)) tag_set(node, tag, 0); } node->count = 1; root->rnode = node; root->height++; } while (height > root->height); out: return 0; } 分配新节点是通过执行 radix_tree_node_alloc()函数实现的,该函数试图从 slab 分配器高速缓 存获得 radix_tree_node 结构,如果分配失败,就从存放在 radix_tree_preloads 中的预分配的 结构池中获得 radix_tree_node 结构: static struct radix_tree_node * radix_tree_node_alloc(struct radix_tree_root *root) { struct radix_tree_node *ret; gfp_t gfp_mask = root_gfp_mask(root); ret = kmem_cache_alloc(radix_tree_node_cachep, gfp_mask); if (ret == NULL && !(gfp_mask & __GFP_WAIT)) { struct radix_tree_preload *rtp; rtp = &__get_cpu_var(radix_tree_preloads); if (rtp->nr) { ret = rtp->nodes[rtp->nr - 1]; rtp->nodes[rtp->nr - 1] = NULL; rtp->nr--; } } return ret; } radix_tree_insert 然后根据页索引的偏移量,从根节点( mapping->page_tree)开始遍历树, 直到叶子节点。如果需要,就调用 radix_tree_node_alloc()分配新的中间节点。 radix_tree_insert最后把页描述符地址存放在对基树所遍历的最后节点的适当位置,并返回 0: node->count++; node->slots[offset] = item; 回到函数 add_to_page_cache(),在分配好页之后,增加页描述符的使用计数器 page->count。 由于页是新的,所以其内容无效:函数设置页框的 PG_locked 标志,以阻止其他的内核路径 并发访问该页。 用 mapping 和 offset 参数初始化 page->mapping 和 page->index。(重点!!!!) add_to_page_cache()函数最后递增在地址空间所缓存页的计数器( mapping->nrpages);释放 地址空间的自旋锁;并且调用 radix_tree_preload_end()重新启用内核抢占,返回 0(成功)。 4.2.3 删除页 函数 remove_from_page_cache()通过下述步骤从页高速缓存中删除页描述符: 1. 获取自旋锁 page->mapping->tree_lock 并关中断。 2. 调 用 radix_tree_delete() 函数从树中删除节点。该函数接收树根的地址 (page->mapping->page_tree)和要删除的页索引作为参数,并执行下述步骤: a) 如上节所述,根据页索引从根节点开始遍历树,直到到达叶子节点。遍历时,建立 radix_tree_path 结构的数组,描述从根到与要删除的页相应的叶子节点的路径构成。 b) 从最后一个节点(包含指向页描述符的指针)开始,对路径数组中的节点开始循环操作。 对每个节点,把指向下一个节点(或页描述符)位置数组的元素置为 NULL,并递减 count 字段。如果 count 变为 0,就从树中删除节点并把 radix_tree_node 结构释放给 slab 分配器高 速缓存。然后继续循环处理路径数组中的节点。否则,如果 count 不等于 0,继续执行下一 步。 c) 返回已经从树中删除的页描述符指针。 3. 把 page->mapping 字段置为 NULL。 4. 把所缓存页的 page->mapping->nrpages 计数器的值减 1。 5. 释放自旋锁 page->mapping->tree_lock,打开中断,函数终止。 更新页 函 数 read_cache_page() 确保高速缓存中包括最新版本的指定页。它的参数是指向 address_space 对象的指针 mapping,表示所请求页的偏移量的值 index,指向从磁盘读页数 据的函数的指针 filler(通常是实现地址空间 readpage 方法的函数)以及传递给 filler 函数的 指针 data(通常为 NULL),下面是对这个函数的简单说明: 1. 调用函数 find_get_page()检查页是否已经在页高速缓存中。 2. 如果页不在页高速缓存中,则执行下述子步骤: a) 调用 alloc_pages()分配一个新页框。 b) 调用 add_to_page_cache()在页高速缓存中插人相应的页描述符。 c) 调用 lru_cache_add()把页插人该管理区的非活动 LRU 链表中。 3. 此时,所请求的页已经在页高速缓存中了。调用 mark_page_accessed()函数记录页已经被 访问过的事实。 4. 如果页不是最新的( PG_uptodate 标志为 0),就调用 filler 函数从磁盘读该页。 5. 返回页描述符的地址。 基树的标记 前面我们曾强调,页高速缓存不仅允许内核快速获得含有块设备中指定数据的页,还允许内 核从高速缓存中快速获得给定状态的页。 例如,我们假设内核必须从高速缓存获得属于指定所有者的所有页和脏页(即其内容还没有 写回磁盘)。存放在页描述符中的 PG_dirty 标志表示页是否是脏的,但是,如果绝大多数页 都不是脏页,遍历整个基树以顺序访问所有叶子节点(页描述符)的操作就太慢了。 相反,为了能快速搜索脏页,基树中的每个中间节点都包含一个针对每个孩子节点(或叶子 节点)的脏标记,当有且只有至少有一个孩子节点的脏标记被置位时这个标记被设置。最底 层节点的脏标记通常是页描述符的 PG_dirty 标志的副本。通过这种方式,当内核遍历基树 搜索脏页时,就可以跳过脏标记为 0 的中间结点的所有子树:中间结点的脏标记为 0 说明其 子树中的所有页描述符都不是脏的。 同样的想法应用到了 PG_writeback 标志,该标志表示页正在被写回磁盘。这样,为基树的 每个结点引入两个页描述符的标志: PG_dirty 和 PG writeback。每个结点的 tags 字段中有两 个 64 位的数组来存放这两个标志。 tags[0](PAGECACHE TAG DIRTY)数组是脏标记,而 tags[l] (PAGECACHE TAG WRITEBACK)数组是写回标记。 设置页高速缓存中页的 PG_dirty 或 PG_writeback 标志时调用函数 radix_tree_tag_set(),它作 用于三个参数:基树的根、页的索引以及要设置的标记的类型( PAGECACHE TAG DIRTY 或 PAGECACHE TAG WRITEBACK)。函数从树根开始并向下搜索到与指定索引对应的叶子 结点;对于从根通往叶子路径上的每一个节点,函数利用指向路径中下一个结点的指针设置 标记。然后,函数返回页描述符的地址。结果是,从根结点到叶子结点的路径中的所有结点 都以适当的方式被加上了标记。 清除页高速缓存中页的 PG_dirty 或 PG_writeback 标志时调用函数 radix_tree_tag_clear(),它 的参数与函数 radix_tree_tag_set()的参数相同。函数从树根开始并向下到叶子结点,建立描 述路径的 radix_tree_path 结构的数组。然后,函数从叶子结点到根结点向后进行操作:清除 底层结点的标记,然后检查是否结点数组中所有标记都被清 0,如果是,函数把上层父结点 的相应标记清 0,并如此继续上述操作。最后,函数返回页描述符的地址。 从基树删除页描述符时,必须更新从根结点到叶子结点的路径中结点的相应标记。函数 radix_tree_delete()可以正确地完成这个工作(尽管我们在上一节没有提到这一点)。而函数 radix_tree_insert()不更新标记,因为插入基树的所有页描述符的 PG_dirty 和 PG_writeback 标 志都被认为是清零的。如果需要,内核可以随后调用函数 radix_tree_tag_set()。 函数 radix_tree_tagged()利用树的所有结点的标志数组来测试基树是否至少包括一个指定状态的页。函数通过执行下面的代码轻松地完成这一任务( root 是指向基树 radix_tree_root 结 构的指针,tag 是要测试的标记): for (idx = 0; idx < 2; idx++) { if (root->rnode->tags[tag][idx]) return 1; } return 0; 因为可能假设基树所有结点的标记都正确地更新过,所以 radix_tree_tagged()函数只需要检 查第一层的标记。使用该函数的一个例子是:确定一个包含脏页的索引节点是否要写回磁盘。 注意,函数在每次循环时要测试在无符号长整型的 32 个标志中,是否有被设置的标志。 函数 find_get_pages_tag()和 find_get_pages()类似,只有一点不同,就是前者返回的只是那些 用 tag 参数标记的页。该函数对快速找到一个索引节点的所有脏页是非常关键的。 4.3 文件系统与高速缓存 VFS(映射层)和各种文件系统以叫做“块”的逻辑单位组织磁盘数据。在 Linux 内核的旧 版本中,主要有两种不同的磁盘高速缓存:页高速缓存和缓冲区高速缓存,前者用来存放访 问磁盘文件内容时生成的磁盘数据页,后者把通过 VFS(管理磁盘文件系统)访问的块的 内容保留在内存中。 从 2.4.10 的稳定版本开始,缓冲区高速缓存其实就不存在了。事实上,由于效率的原因,不 再单独分配块缓冲区;相反,把它们存放在叫做“缓冲区页”的专门页中,而缓冲区页保存 在页高速缓存中。 缓冲区页在形式上就是与称做“缓冲区头”的附加描述符相关的数据页,其主要目的是快速 确定页中的一个块在磁盘中的地址。实际上,页高速缓存内的页中的一大块数据在磁盘上的 地址不一定是相邻的。 4.3.1 缓冲头数据结构 每个块缓冲区都有 buffer_head 类型的缓冲区头描述符。该描述符包含内核必须了解的、有 关如何处理块的所有信息。因此,在对所有块操作之前,内核检查缓冲区首部。缓冲区首部 的字段位于/include/linux/Buffer_head.h: struct buffer_head { unsigned long b_state; /* 缓冲区状态标志 */ struct buffer_head *b_this_page; /* 指向缓冲区页的链表中的下一个元素的指针 */ struct page *b_page; /* 指向拥有该块的缓冲区页的描述符的指针 */ sector_t b_blocknr; /* 与块设备相关的块号(起始逻辑块号) */ size_t b_size; /* 块大小 */ char *b_data; /* 块在缓冲区页内的位置 */ struct block_device *b_bdev; /* 指向块设备描述符的指针 */ bh_end_io_t *b_end_io; /* I/O 完成方法 */ void *b_private; /* 指向 I/O 完成方法数据的指针 */ struct list_head b_assoc_buffers; /* 为与某个索引节点相关的间接块的链表提供的指 针 */ atomic_t b_count; /* 块使用计数器 */ }; 缓冲区头的两个字段编码表示块的磁盘地址: b_bdev 字段表示包含块的块设备,通常是磁 盘或分区;而 b_blocknr 字段存放逻辑块号,即块在磁盘或分区中的编号。 b_data 字段表示块缓冲区在缓冲区页中的位置。实际上,这个位置的编号依赖于页是否在高 端内存。如果页在高端内存,则 b_data 字段存放的是块缓冲区相对于页的起始位置的偏移 量,否则,b_data 存放的就是是块缓冲区的线性地址。 b_state 字段可以存放几个标志。其中一些标志是通用的,我们在下面把它们列出来了。每 个文件系统还可以定义自己的私有缓冲区首部标志。 BH_Uptodate:缓冲区包含有效数据时被置位 BH_Dirty:如果缓冲区脏就置位(表示缓冲区中的数据必须写回块设备) BH_Lock:如果缓冲区加锁就置位,通常发生在缓冲区进行磁盘传输时 BH_Req:如果已经为初始化缓冲区而请求数据传输就置位 BH_Mapped:如果缓冲区被映射到磁盘就置位,即:如果相应的缓冲区首部的 b_bdev 和 b_blocknr 是有效的就置位 BH_New:如果相应的块刚被分配而还没有被访问过就置位 BH_Async_Read:如果在异步地读缓冲区就置位 BH_Async_Write:如果在异步地写缓冲区就置位 BH_Delay:如果还没有在磁盘上分配缓冲区就置位 BH_Boundary:如果两个相邻的块在其中一个提交之后不再相邻就置位 BH_Write_EIO:如果写块时出现 I/O 错误就置位 BH_Ordered:如果必须严格地把块写到在它之前提交的块的后面就置位(用于日志文件系 统) BH_Eopnotsupp:如果块设备的驱动程序不支持所请求的操作就置位 缓冲区头有它们自己的 slab 分配器高速缓存,其描述符 kmem_cache_s 存在变量 bh_cachep 中。alloc_buffer_head()和 free_buffer_head()函数分别用于获取和释放缓冲区首部。 struct buffer_head *alloc_buffer_head(gfp_t gfp_flags) { struct buffer_head *ret = kmem_cache_alloc(bh_cachep, gfp_flags); if (ret) { get_cpu_var(bh_accounting).nr++; recalc_bh_state(); put_cpu_var(bh_accounting); } return ret; } void free_buffer_head(struct buffer_head *bh) { BUG_ON(!list_empty(&bh->b_assoc_buffers)); kmem_cache_free(bh_cachep, bh); get_cpu_var(bh_accounting).nr--; recalc_bh_state(); put_cpu_var(bh_accounting); } 缓冲区首部的 b_count 字段是相应的块缓冲区的引用计数器。在每次对块缓冲区进行操作之 前递增计数器并在操作之后递减它。除了周期性地检查保存在页高速缓存中的块缓冲区之外, 当空闲内存变得很少时也要对它进行检查,只有引用计数器等于 0 的块缓冲区才可以被回收。 当内核控制路径希望访问块缓冲区时,应该先递增引用计数器。确定块在页高速缓存中的位 置的函数(__getblk(),后面会讲)自动完成这项工作,因此,高层函数通常不增加块缓冲 区的引用计数器。 当内核控制路径停止访问块缓冲区时,应该调用 __brelse()或__bforget()递减相应的引用计数 器。这两个函数之间的不同是 __bforget()还从间接块链表(缓冲区首部的 b_assoc_buffers 字 段)中删除块,并把该缓冲区标记为干净的,因此强制内核忽略对缓冲区所做的任何修改, 但实际上缓冲区依然必须被写回磁盘。 只要内核必须单独地访问一个块,就要涉及存放块缓冲区的缓冲区页,并检查相应的缓冲区 首部。下面是内核创建缓冲区页的两种普通情况: (1)当读或写的文件页在磁盘块中不相邻时。发生这种情况是因为文件系统为文件分配了 非连续的块,或因为文件有“洞”。 (2)当访问一个单独的磁盘块时(例如,当读超级块或索引节点块时)。 在第一种情况下,把缓冲区页的描述符插入普通文件的基树;保存好缓冲区首部,因为其中 存有重要的信息,即存有数据在磁盘中位置的块设备和逻辑块号。 在第二种情况下,把缓冲区页的描述符插人基树,树根是与块设备相关的特殊 bdev 文件系 统中索引节点的 address_space 对象。这种缓冲区页必须满足很强的约束条件,就是所有的 块缓冲区涉及的块必须是在块设备上相邻存放的。 第二种情况的一个应用实例是:如果虚拟文件系统要读大小为 1024 个字节的索引节块(包 含给定文件的索引节点)。内核并不是只分配一个单独的缓冲区,而是必须分配一个整页, 从而存放四个缓冲区;这些缓冲区将存放块设备上相邻的 4 块数据,其中包括所请求的索引 节点块。 这里我们将重点讨论第二种类型的缓冲区页,即所谓的块设备缓冲区页(有时简称为块设备 页),因为这是读写磁盘文件最多见的情况。 在一个缓冲区页内的所有块缓冲区大小必须相同,因此,在 80x86 体系结构上,根据块的大 小,一个缓冲区页可以包括多个缓冲区。 如果一个页作为缓冲区页使用,那么与它的块缓冲区相关的所有缓冲区首部都被收集在一个 单向循环链表中。缓冲区页描述符的 private 字段指向页中第一个块的缓冲区(由于 private 字段包含有效数据,而且页的 PG_private 标志被设置,因此,如果页中包含磁盘数据并且 设置了 PG_private 标志,该页就是一个缓冲区页。注意,尽管如此,其他与块 I/O 子系统无 关的内核组件也因为别的用途而使用 private 和 PG_private 字段);每个缓冲区首部存放在 b_this_page 字段中,该字段是指向链表中下一个缓冲区首部的指针。此外,每个缓冲区首 部还把缓冲区页描述符的地址存放在 b_page 中: 4.3.2 分配块设备缓冲区页 当内核发现指定块的缓冲区所在的页不在页高速缓存中时,就分配一个新的块设备缓冲区页。 特别是,对块的查找操作会由于下述原因而失败: (1)包含数据块的页不在块设备的基树中:这种情况下,必须把新页的描述符加到基树中。 (2)包含数据块的页在块设备的基树中,但这个页不是缓冲区页:在这种情况下,必须分 配新的缓冲区首部,并将它链接到所属的页,从而把它变成块设备缓冲区页。 (3)包含数据块的缓冲区页在块设备的基树中,但页中块的大小与所请求的块大小不相同: 这种情况下,必须释放旧的缓冲区首部,分配经过重新赋值的缓冲区首部并将它链接到所属 的页。 内核调用函数 grow_buffers()把块设备缓冲区页添加到页高速缓存中,该函数接收三个标识 块的参数: - block_device 描述符的地址 bdev。 - 逻辑块号 block(块在块设备中的位置)。 - 块大小 size。 下面我们就来分析这个重要的函数: static int grow_buffers(struct block_device *bdev, sector_t block, int size) { struct page *page; pgoff_t index; int sizebits; sizebits = -1; do { sizebits++; } while ((size << sizebits) < PAGE_SIZE); index = block >> sizebits; /* * Check for a block which wants to lie outside our maximum possible * pagecache index. (this comparison is done using sector_t types). */ if (unlikely(index != block >> sizebits)) { char b[BDEVNAME_SIZE]; printk(KERN_ERR "%s: requested out-of-range block %llu for " "device %s\n", __FUNCTION__, (unsigned long long)block, bdevname(bdev, b)); return -EIO; } block = index << sizebits; /* Create a page with the proper size buffers.. */ page = grow_dev_page(bdev, block, index, size); if (!page) return 0; unlock_page(page); page_cache_release(page); return 1; } 1. 计算数据页在所请求块的块设备中的偏移量 index,然后将 block 与 index 对齐。 比如,块大小是 512(都是以字节为单位), size << sizebits 就是 size * 2^sizebits,这个没问 题吧!那么 512*8=4096(PAGE_SIZE),所以跳出循环时 sizebits 是 3,那么 index = block >> sizebits,也就是最后计算出每个块 512 字节大小的块设备中的对应块 block 的块设备中的偏 移是 index = block / 8。然后将 block 与 index 对齐:block = index * 8 2. 如果需要,就调用 grow_dev_page()创建新的块设备缓冲区页。 static struct page * grow_dev_page(struct block_device *bdev, sector_t block, pgoff_t index, int size) { struct inode *inode = bdev->bd_inode; struct page *page; struct buffer_head *bh; page = find_or_create_page(inode->i_mapping, index, GFP_NOFS); if (!page) return NULL; BUG_ON(!PageLocked(page)); if (page_has_buffers(page)) { bh = page_buffers(page); if (bh->b_size == size) { init_page_buffers(page, bdev, block, size); return page; } if (!try_to_free_buffers(page)) goto failed; } /* * Allocate some buffers for this page */ bh = alloc_page_buffers(page, size, 0); if (!bh) goto failed; /* * Link the page to the buffers and initialise them. Take the * lock to be atomic wrt __find_get_block(), which does not * run under the page lock. */ spin_lock(&inode->i_mapping->private_lock); link_dev_buffers(page, bh); init_page_buffers(page, bdev, block, size); spin_unlock(&inode->i_mapping->private_lock); return page; failed: BUG(); unlock_page(page); page_cache_release(page); return NULL; } 该函数依次执行以下列子步骤: a. 调用函数 find_or_create_page(),传递给它的参数有:块设备的 address_space 对象 (bdev->bd_inode->i mapping)、页偏移 index 以及 GFP_NOFS 标志。正如在前面“页高速缓 存的处理函数”章节所描述的, find_or_create_page()在页高速缓存中(基树中)搜索需要的 页,如果需要,就把新的页插入高速缓存。 b. 此时,所请求的页已经在页高速缓存中,而且函数获得了它的描述符地址。函数检查它 的 PG_private 标志;如果为空,说明页还不是一个缓冲区页(没有相关的缓冲区首部),就 跳到第 e 步。 c. 页已经是缓冲区页。从页描述符的 private 字段获得第一个缓冲区首部的地址 bh,并检查 块大小 bh->size 是否等于所请求的块大小;如果大小相等,在页高速缓存中找到的页就是有 效的缓冲区页,因此跳到第 g 步。 d. 如果页中块的大小有错误,就调用 try_to_free_buffers()释放缓冲区页的上一个缓冲区首部, 并报错(goto failed)。 e. 调用函数 alloc_page_buffers()根据页中所请求的块大小分配缓冲区首部,并把它们插入由 b_this_page 字段实现的单向循环链表(注意那个 while 循环): struct buffer_head *alloc_page_buffers(struct page *page, unsigned long size, int retry) { struct buffer_head *bh, *head; long offset; try_again: head = NULL; offset = PAGE_SIZE; while ((offset -= size) >= 0) { bh = alloc_buffer_head(GFP_NOFS); if (!bh) goto no_grow; bh->b_bdev = NULL; bh->b_this_page = head; bh->b_blocknr = -1; head = bh; bh->b_state = 0; atomic_set(&bh->b_count, 0); bh->b_private = NULL; bh->b_size = size; /* Link the buffer to its page */ set_bh_page(bh, page, offset); init_buffer(bh, NULL, NULL); } return head; no_grow: …… } void set_bh_page(struct buffer_head *bh, struct page *page, unsigned long offset) { bh->b_page = page; BUG_ON(offset >= PAGE_SIZE); if (PageHighMem(page)) /* * This catches illegal uses and preserves the offset: */ bh->b_data = (char *)(0 + offset); else bh->b_data = page_address(page) + offset; } inline void init_buffer(struct buffer_head *bh, bh_end_io_t *handler, void *private) { bh->b_end_io = handler; bh->b_private = private; } 此外,函数 alloc_page_buffers 调用 set_bh_page 用页描述符的地址初始化缓冲区首部的 b_page 字段,用块缓冲区在页内的线性地址或偏移量初始化 b_data 字段。 回到 grow_dev_page: f. 调用 link_dev_buffers 把页的缓冲区头连成一个循环链表,在 page 结构的字段 private 中 存放第一个缓冲区首部的地址,把 PG_private 字段置位,并递增页的使用计数器(页中的 块缓冲区被算作一个页用户): static inline void link_dev_buffers(struct page *page, struct buffer_head *head) { struct buffer_head *bh, *tail; bh = head; do { tail = bh; bh = bh->b_this_page; } while (bh); tail->b_this_page = head; attach_page_buffers(page, head); } static inline void attach_page_buffers(struct page *page, struct buffer_head *head) { page_cache_get(page); /* 并递增页的使用计数器 */ SetPagePrivate(page); set_page_private(page, (unsigned long)head); } #define SetPagePrivate(page) set_bit(PG_private, &(page)->flags) #define set_page_private(page, v) ((page)->private = (v)) g. 调用 init_page_buffers()函数初始化连接到页的缓冲区首部的字段 b_bdev、b_blocknr 和 b_bstate。因为所有的块在磁盘上都是相邻的,因此逻辑块号是连续的,而且很容易从块得 出: static void init_page_buffers(struct page *page, struct block_device *bdev, sector_t block, int size) { struct buffer_head *head = page_buffers(page); struct buffer_head *bh = head; int uptodate = PageUptodate(page); do { if (!buffer_mapped(bh)) { init_buffer(bh, NULL, NULL); bh->b_bdev = bdev; bh->b_blocknr = block; if (uptodate) set_buffer_uptodate(bh); set_buffer_mapped(bh); } block++; bh = bh->b_this_page; } while (bh != head); } h. 返回页描述符地址。 4.3.3 释放块设备缓冲区页 当内核试图获得更多的空闲内存时,就释放块设备缓冲区页。显然,不可能释放有脏缓冲区 或上锁的缓冲区的页。内核调用函数 try_to_release_page()释放缓冲区页,该函数接收页描述 符 的 地 址 page ,并执行下述步骤(还可以对普通文件所拥有的缓冲区页调用 try_to_release_page 函数): 1. 如果设置了页的 PG_writeback 标志,则返回 0(因为正在把页写回磁盘,所以不可能释 放该页)。 2. 如果已经定义了块设备 address_space 对象的 releasepage 方法,就调用它(通常没有为块 设备定义的 releasepage 方法)。 3. 调用函数 try_to_free_buffers()并返回它的错误代码。 函数 try_to_free_buffers()依次扫描链接到缓冲区页的缓冲区首部,它本质上执行下列操作: 1. 检查页中所有缓冲区的缓冲区首部的标志。如果有些缓冲区首部的 BH_Dirty 或 BH_Locked 标志被置位,说明函数不可能释放这些缓冲区,所以函数终止并返回 0(失败)。 2. 如果缓冲区首部在间接缓冲区的链表中,该函数就从链表中删除它。 3. 清除页描述符的 PG_private 标记,把 private 字段设置为 NULL,并递减页的使用计数器。 4. 清除页的 PG_dirty 标记。 5. 反复调用 free_buffer_head(),以释放页的所有缓冲区首部。 6. 返回 1(成功)。 4.4 在页高速缓存中搜索块 当内核需要读或写一个单独的物理设备块时(例如一个超级块),必须检查所请求的块缓冲 区是否已经在页高速缓存中。在页高速缓存中搜索指定的块缓冲区(由块设备描述符的地址 bdev 和逻辑块号 nr 表示)的过程分成三个步骤: 1. 获取一个指针,让它指向包含指定块的块设备的 address_space 对 象 (bdev->bd_inode->i_mapping)。 2. 获得设备的块大小 (bdev->bd_block_size),并计算包含指定块的页索引。这需要在逻辑块 号上进行位移操作。例如,如果块的大小是 1024 字节,每个缓冲区页包含四个块缓冲区, 那么页的索引是 nr/4。 3. 在块设备的基树中搜索缓冲区页。获得页描述符之后,内核访问缓冲区首部,它描述了 页中块缓冲区的状态。 不过,实现的细节要更为复杂。为了提高系统性能,内核维持一个小磁盘高速缓存数组 bh_lrus(每个 CPU 对应一个数组元素),即所谓的最近最少使用( LRU)块高速缓存。每个 磁盘高速缓存有 8 个指针,指向被指定 CPU 最近访问过的缓冲区首部。对每个 CPU 数组的 元素排序,使指向最后被使用过的那个缓冲区首部的指针索引为 0。相同的缓冲区首部可能 出现在几个 CPU 数组中(但是同一个 CPU 数组中不会有相同的缓冲区首部)。在 LRU 块高 速缓存中每出现一次缓冲区首部,该缓冲区首部的使用计数器 b_count 就加 1。 4.4.1 __find_get_block()函数 函数__find_get_block()的参数有:block_device 描述符地址 bdev,块号 block 和块大小 size。 函数返回页高速缓存中的块缓冲区对应的缓冲区首部的地址;如果不存在指定的块,就返回 NULL: struct buffer_head * __find_get_block(struct block_device *bdev, sector_t block, int size) { struct buffer_head *bh = lookup_bh_lru(bdev, block, size); if (bh == NULL) { bh = __find_get_block_slow(bdev, block); if (bh) bh_lru_install(bh); } if (bh) touch_buffer(bh); return bh; } __find_get_block_slow 数本质上执行下面的操作: 1. 检查执行 CPU 的 LRU 块高速缓存数组中是否有一个缓冲区首部,其 b_bdev、b_blocknr 和 b_size 字段分别等于 bdev、block 和 size: static struct buffer_head * lookup_bh_lru(struct block_device *bdev, sector_t block, int size) { struct buffer_head *ret = NULL; struct bh_lru *lru; int i; check_irqs_on(); bh_lru_lock(); lru = &__get_cpu_var(bh_lrus); for (i = 0; i < BH_LRU_SIZE; i++) { struct buffer_head *bh = lru->bhs[i]; if (bh && bh->b_bdev == bdev && bh->b_blocknr == block && bh->b_size == size) { if (i) { while (i) { lru->bhs[i] = lru->bhs[i - 1]; i--; } lru->bhs[0] = bh; } get_bh(bh); ret = bh; break; } } bh_lru_unlock(); return ret; } 2. 如果缓冲区首部在 LRU 块高速缓存中,就刷新数组中的元素,以便让指针指在第一个位 置(索引为 0)刚找到的缓冲区首部,递增它的 b_count 字段,并跳转到第 8 步。 3. 如果缓冲区首部不在 LRU 块高速缓存中,就调用 __find_get_block_slow: static struct buffer_head * __find_get_block_slow(struct block_device *bdev, sector_t block) { struct inode *bd_inode = bdev->bd_inode; struct address_space *bd_mapping = bd_inode->i_mapping; struct buffer_head *ret = NULL; pgoff_t index; struct buffer_head *bh; struct buffer_head *head; struct page *page; int all_mapped = 1; index = block >> (PAGE_CACHE_SHIFT - bd_inode->i_blkbits); page = find_get_page(bd_mapping, index); if (!page) goto out; spin_lock(&bd_mapping->private_lock); if (!page_has_buffers(page)) goto out_unlock; head = page_buffers(page); bh = head; do { if (bh->b_blocknr == block) { ret = bh; get_bh(bh); goto out_unlock; } if (!buffer_mapped(bh)) all_mapped = 0; bh = bh->b_this_page; } while (bh != head); /* we might be here because some of the buffers on this page are * not mapped. This is due to various races between * file io on the block device and getblk. It gets dealt with * elsewhere, don't buffer_error if we had some unmapped buffers */ if (all_mapped) { printk("__find_get_block_slow() failed. " "block=%llu, b_blocknr=%llu\n", (unsigned long long)block, (unsigned long long)bh->b_blocknr); printk("b_state=0x%08lx, b_size=%zu\n", bh->b_state, bh->b_size); printk("device blocksize: %d\n", 1 << bd_inode->i_blkbits); } out_unlock: spin_unlock(&bd_mapping->private_lock); page_cache_release(page); out: return ret; } __find_get_block_slow 首先根据块号和块大小得到与块设备相关的页的索引: index = block >> (PAGE_SHIFT - bdev->bd_inode->i_blkbits) 4. 调用 find_get_page()确定存有所请求的块缓冲区的缓冲区页的描述符在页高速缓存中的 位置。该函数传递的参数有:指向块设备的 address_space 对 象 的 指 针 (bdev->bd_mode->i_mapping)和页索引。页索引用于确定存有所请求的块缓冲区的缓冲区 页的描述符在页高速缓存中的位置。如果高速缓存中没有这样的页,就返回 NULL(失败)。 具体内容请查看“页高速缓存处理函数”章节。 5. 此时,函数已经得到了缓冲区页描述符的地址:它扫描链接到缓冲区页的缓冲区首部链 表,查找逻辑块号等于 block 的块。 6. 递减页描述符的 count 字段(find_get_page 曾经递增它的值)。 7. 调用 bh_lru_install 把 LRU 块高速缓存中的所有元素向下移动一个位置,并把指向所请求 块的缓冲区首部的指针插入到第一个位置。如果一个缓冲区首部已经不在 LRU 块高速缓存 中,就递减它的引用计数器 b_count。 8. 如果需要,就调用 mark_page_accessed()把缓冲区页移至适当的 LRU 链表中。 9. 返回缓冲反首部指针。 4.4.2 __getblk()函数 古老的函数__getblk()现在的重要性也跟当年一样重要,即如果查找不到就分配一个缓冲区 头。__getblk()其与__find_get_block()接收相同的参数,也就是 block_device描述符的地址bdev、 块号 block 和块大小 size,并返回与缓冲区对应的缓冲区首部的地址。即使块根本不存在,该函数也不会失败, __getblk()会友好地分配块设备缓冲区页并返回将要描述块的缓冲区首 部 的 指 针 。 注 意 ,__getblk() 返回的块缓冲区不必存有有效数据— — 缓冲区首部的 BH_Uptodate 标志可能被清 0。 struct buffer_head * __getblk(struct block_device *bdev, sector_t block, int size) { struct buffer_head *bh = __find_get_block(bdev, block, size); might_sleep(); if (bh == NULL) bh = __getblk_slow(bdev, block, size); return bh; } 函数__getblk()本质上执行下面的步骤: 1. 调用__find_get_block()检查块是否已经在页高速缓存中。如果找到块,则函数返回其缓冲 区首部的地址。 2. 否则,调用 __getblk_slow,触发 grow_buffers()为所请求的页分配一个新的缓冲区页(参 见前面“把块存放在页高速缓存中”章节): static struct buffer_head * __getblk_slow(struct block_device *bdev, sector_t block, int size) { /* Size must be multiple of hard sectorsize */ if (unlikely(size & (bdev_hardsect_size(bdev)-1) || (size < 512 || size > PAGE_SIZE))) { printk(KERN_ERR "getblk(): invalid block size %d requested\n", size); printk(KERN_ERR "hardsect size: %d\n", bdev_hardsect_size(bdev)); dump_stack(); return NULL; } for (;;) { struct buffer_head * bh; int ret; bh = __find_get_block(bdev, block, size); if (bh) return bh; ret = grow_buffers(bdev, block, size); if (ret < 0) return NULL; if (ret == 0) free_more_memory(); } } 3. 如果 grow_buffers()分配这样的页时失败, __getblk()试图通过调用函数 free_more_memory() 回收一部分内存。 4. 跳转到第 1 步。 4.4.3 __bread()函数 函数__bread()接收与__getblk()相同的参数,即 block_device 描述符的地址 bdev、块号 block 和块大小 size,并返回与缓冲区对应的缓冲区首部的地址。与 __getblk()相反的是,如果需要 的话,在返回缓冲区首部之前函数 __bread()从磁盘读块,将分配到的一个空的 buffer_head 填满: struct buffer_head * __bread(struct block_device *bdev, sector_t block, int size) { struct buffer_head *bh = __getblk(bdev, block, size); if (likely(bh) && !buffer_uptodate(bh)) bh = __bread_slow(bh); return bh; } static struct buffer_head *__bread_slow(struct buffer_head *bh) { lock_buffer(bh); if (buffer_uptodate(bh)) { unlock_buffer(bh); return bh; } else { get_bh(bh); bh->b_end_io = end_buffer_read_sync; submit_bh(READ, bh); wait_on_buffer(bh); if (buffer_uptodate(bh)) return bh; } brelse(bh); return NULL; } 函数__bread()执行下述步骤: 1. 调用__getblk()在页高速缓存中查找与所请求的块相关的缓冲区页,并获得指向相应的缓 冲区首部的指针。 2. 如果块已经在页高速缓存中并包含有效数据( if(buffer_uptodate(bh))检查 BH_Uptodate 标 志被置位),就返回缓冲区首部的地址。 3. 否则,get_bh(bh)递增缓冲区首部的引用计数器。 4. 把 end_buffer_read_sync()的地址赋给 b_end_io 字段(参见下一章节)。 5. 调用 submit_bh()把缓冲区首部传送到通用块层(参见下一章节)。 6. 调用 wait_on_buffer()把当前进程插入等待队列,直到 I/O 操作完成,即直到缓冲区首部 的 BH_Lock 标志被清 0。 7. 返回缓冲区首部的地址。 4.5 把脏页写入磁盘 正如我们所了解的,内核不断用包含块设备数据的页填充页高速缓存。只要进程修改了数据, 相应的页就被标记为脏页,即把它的 PG_dirty 标志置位。 Unix 系统允许把脏缓冲区写入块设备的操作延迟执行,因为这种策略可以显著地提高系统 的性能。对高速缓存中的页的几次写操作可能只需对相应的磁盘块进行一次缓慢的物理更新 就可以满足。此外,写操作没有读操作那么紧迫,因为进程通常是不会由于延迟写而挂起, 而大部分情况都因为延迟读而挂起。正是由于延迟写,使得任一物理块设备平均为读请求提 供的服务将多于写请求。 一个脏页可能直到最后一刻(即直到系统关闭时)都一直逗留在主存中。然而,从延迟写策 略的局限性来看,它有两个主要的缺点: (1)如果发生了硬件错误或电源掉电的情况,那么就无法再获得 RAM 的内容,因此从系 统启动以来对文件进行的很多修改就丢失了。 (2)页高速缓存的大小(由此存放它所需的 RAM 的大小)就可能要很大——至少要与所 访问块设备的大小相同。 因此,在下列条件下把脏页刷新(写入)到磁盘: (1)页高速缓存变得太满,但还需要更多的页,或者脏页的数量已经太多。 (2)自从页变成脏页以来已过去太长时间。 (3)进程请求对块设备或者特定文件任何待定的变化都进行刷新。通过调用 sync()、fsync()或 fdatasync()系统调用来实现。 缓冲区页的引入使问题更加复杂。与每个缓冲区页相关的缓冲区首部使内核能够了解每个独 立块缓冲区的状态。如果至少有一个缓冲区首部的 BH_Dirty 标志被置位,就应该设置相应 缓冲区页的 PG_dirty 标志。当内核选择要刷新的缓冲区页时,它扫描相应的缓冲区首部, 并只把脏块的内容有效地写到磁盘。一旦内核把缓冲区的所有脏页刷新到磁盘,就把页的 PG_dirty 标记清 0。 4.5.1 pdflush 内核线程 早期版本的 Linux 使用 bdflush 内核线程系统地扫描页高速缓存以搜索要刷新的脏页,并且 使用另一个内核线程 kupdate 来保证所有的页不会“脏”太长的时间。 Linux 2.6 用一组通用 内核线程 pdflush 代替上述两个线程。 这些内核线程结构灵活,它们作用于两个参数:一个指向线程要执行的函数的指针和一个函 数要用的参数。系统中 pdflush 内核线程的数量是要动态调整的: pdflush 线程太少时就创建, 太多时就杀死。因为这些内核线程所执行的函数可以阻塞,所以创建多个而不是一个 pdflush 内核线程可以改善系统性能。 根据下面的原则控制 pdflush 线程的产生和消亡: - 必须有至少两个,最多八个 pdflush 内核线程。 - 如果到最近的 1s 期间没有空闲 pdflush,就应该创建新的 pdflush。 - 如果最近一次 pdflush 变为空闲的时间超过了 1s,就应该删除一个 pdflush。 所有的 pdflush 内核线程都有 pdflush_work 描述符。空闲 pdflush 内核线程的描述符都集中 在 pdflush_list 链表中;在多处理器系统中, pdflush_lock 自旋锁保护该链表不会被并发访问。 nr_pdflush_threads 变量(可以从文件 /proc/sys/vm/nr_pdflush_threads 中读出这个变童的值) 存放 pdflush 内核线程(空闲的或忙的)的总数。最后, last_empty_jifs 变量存放 pdflush 线 程的 pdflush_list 链表变为空的时间(以 jiffies 表示)。 类型 字段 说明 struct task_struct * who 指向内核线程描述符的指针 void(*)(unsigned long) fn 内核线程所执行的回调函数 unsigned long arg0 给回调函数的参数 struct list head list pdflush_list 链表的链接 unsigned long when_i_went_to_sleep 当内核线程可用时的时间(以 jiffies 表示) 所有 pdflush 内核线程都执行函数__pdflush,它本质上循环执行一直到内核线程死亡。我们 不妨假设 pdflush 内核线程是空闲的,而进程正在 TASK_INTERRUPTIBLE 状态睡眠。一但 内核线程被唤醒, __pdflush()就访问其 pdflush_work 描述符,并执行字段 fn 中的回调函数, 把 arg0 字段中的参数传递给该函数。当回调函数结束时 __pdflush()检查 last_empty_jifs 变量 的值:如果不存在空闲 pdflush 内核线程的时间已经超过 1s,而且 pdflush 内核线程的数量不到 8 个,函数 __pdflush()就创建另外一个内核线程。相反,如果 pdflush_list 链表中的最后 一项对应的 pdflush 内核线程空闲时间超过了 1s,而且系统中有两个以上的 pdflush 内核线 程,函数 __pdflush()就终止:就像在“内核线程”博文中所描述的,相应的内核线程执行 _exit() 系统调用,并因此而被撤消。否则,如果系统中 pdflush 内核线程不多于两个,__pdflush() 就把内核线程的 pdflush_work 描述符重新插入到 pdflush_list 链表中,并使内核线程睡眠。 pdflush_operation()函数用来激活空闲的 pdflush 内核线程。该函数作用于两个参参数:一个 指针 fn,指向必须执行的函数;以及参数 argO。函数执行下面的步骤: 1. 从 pdflush_list 链表中获取 pdf指针,它指向空闲 pdflush内核线程的 pdflush_work描述符。 如果链表为空,就返回 -1。如果链表中仅剩一个元素,就把 jiffies 的值赋给变量 last_empty_jifs。 2. 把参数 fn 和 arg0 分别赋给 pdf->fn 和 pdf->arg0。 3. 调用 wake_up_process()唤醒空闲的 pdflush 内核线程,即 pdf->who。 把哪些工作委托给 Pdflush 内核线程来完成呢?其中一些工作与脏数据的刷新相关。尤其是, pdflush 通常执行下面的回调函数之一: - background_writeout():系统地扫描页高速缓存以搜索要刷新的脏页。 - wb_kupdate():检查页高速缓存中是否有“脏”了很长时间的页。 4.5.2 搜索要刷新的脏页 所有的基树都可能有要刷新的脏页。为了得到所有这些页,就要彻底搜索与在磁盘上有映像 的索引节点相应的所有 address_space 对象。由于页高速缓存可能有大量的页,如果用一个 单独的执行流来扫描整个高速缓存,会令 CPU 和磁盘长时间繁忙。因此, Linux 使用一种 复杂的机制把对页高速缓存的扫描划分为几个执行流。 wakeup_bdflush()函数接收页高速缓存中应该刷新的脏页数量作为参数; 0 值表示高速缓存中 的所有脏页都应该写回磁盘。该函数调用 pdflush_operation()唤醒 pdflush 内核线程(参见上 一节),并委托它执行回调函数 background_writeout(),后者有效地从页高速缓存获得指定数 量的脏页,并把它们写回磁盘。 当内存不足或用户显式地请求刷新操作时执行 wakeup_bdflush()函数。特别是在下述情况下 会调用该函数: - 用户态进程发出 sync()系统调用 - grow_buffers()函数分配一个新缓冲区页时失败 - 页框回收算法调用 free_more_memory()或 try_to_free_pages() - menmpool_alloc()函数分配一个新的内存池元素时失败 此外,执行 background_writeout()回调函数的 pdflush 内核线程是由满足以下两个条件的进程 唤醒的:一是对页高速缓存中的页内容进行了修改,二是引起脏页部分增加到超过某个脏背 景阈值 (background threshold)。背景阈值通常设置为系统中所有页的 10%,不过可以通过修改文件/proc/sys/vm/dirty_background_ratio 来调整这个值。 background_writeout()函数依赖于作为双向通信设备的 writeback_control 结构:一方面,它告 诉辅助函数 writeback_modes()要做什么;另一方面,它保存写回磁盘的页的数量的统计值。 下面是这个结构最重要的字段: sync_mode:表示同步模式:WB_SYNC_ALL 表示如果遇到一个上锁的索引节点,必须等待 而不能略过它;WB_SYNC_HOLD 表示把上锁的索引节点放入稍后涉及的链表中; WB_SYNC_NONE 表示简单地略过上锁的索引节点。 bdi:如果不为空,就指向 backing_dev_info 结构。此时,只有属于基本块设备的脏页将会被 刷新。 older_than_this:如果不为空,就表示应该略过比指定值还新的索引节点。 nr_to_write:当前执行流中仍然要写的脏页的数量。 nonblocking:如果这个标志被置位,就不能阻塞进程。 background_writeout()函数只作用于一个参数 nr_pages,表示应该刷新到磁盘的最少页数。 它本质上执行下述步骤: 1. 从每 CPU 变量 page_state 中读当前页高速缓存中页和脏页的数量。如果脏页所占的比例 低于给定的阈值,而且已经至少有 nr_pages 页被刷新到磁盘,该函数就终止。这个阈值通 常大约是系统中总页数的 40%,可以通过写文件 /proc/sys/vm/dirty_ratio 来调整这个值。 2. 调用 writeback_inodes()尝试写 1024 个脏页(见下面)。 3. 检查有效写过的页的数量,并减少需要写的页的个数。 4. 如果已经写过的页少于 1024 页,或略过了一些页,则可能块设备的请求队列处于拥塞状 态:此时,background_writeout()函数使当前进程在特定的等待队列上睡眠 100ms,或使当 前进程睡眠到队列变得不拥塞。 5. 返回到第 1 步。 writeback_inodes()函数只作用于一个参数,就是指针 wbc,它指向 writeback_control 描述符。 该描述符的 nr_to_write 字段存有要刷新到磁盘的页数。函数返回时,该字段存有要刷新到 磁盘的剩余页数,如果一切顺利,则该字段的值被赋为 0。 我们假设 writeback_inodes()函数被调用的条件为:指针 wbc->bdi 和 wbc->older_than_this 被 置为 NULL,WB_SYNC_NONE 同步模式和 wbc->nonblocking 标志置位(这些值都由 background_writeout()函数设置)。函数 writeback_inodes()扫描在 super_blocks 变量中建立的 超级块链表。当遍历完整个链表或刷新的页数达到预期数量时,就停止扫描。对每个超级块sb,函数执行下述步骤: 1. 检查 sb->s_dirty 或 sb->s_io 链表是否为空:第一个链表集中了超级块的脏索引节点,而 第二个链表集中了等待被传输到磁盘的索引节点(见下面)。如果两个链表都为空,说明相 应文件系统的索引节点没有脏页,因此函数处理链表中的下一个超级块。 2. 此时,超级块有脏索引节点。对超级块 sb 调用 sync_sb_inodes(),该函数执行下面的操作: a) 把 sb->s_dirty 的所有索引节点插入 sb->s_io 指向的链表,并清空脏索引节点链表。 b) 从 sb->s_io 获得下一个索引节点的指针。如果该链表为空,就返回。 c) 如果 sync_sb_inodes()函数开始执行后,索引节点变为脏节点,就略过这个索引节点的脏 页并返回。注意, sb->s_io 链表中可能残留一些脏索引节点。 d) 如果当前进程是 pdflush内核线程,sync_sb_inodes()就检查运行在另一个 CPU上的 pdflush 内核线程是否已经试图刷新这个块设备文件的脏页。这是通过一个原子测试和对索引节点的 backing_dev_info 的 BDI_pdflush 标志的设置操作来完成的。本质上,它对同一个请求队列 上有多个 pdflush 内核线程是毫无意义的。 e) 把索引节点的引用计数器加 1。 f) 调用__writeback_single_inode()回写与所选择的索引节点相关的脏缓冲区: i. 如果索引节点被锁定,就把它移到脏索引节点链表中( inode->i_sb->s_dirty)并返回 0。(因 为我们假定 wbc->sync_mode 字段不等于 WB_SYNC_ALL,所以函数不会因为等待索引结 点解锁而阻塞。) ii. 使用索引节点地址空间的 writepages 方法,或者在没有这个方法的情况下使用 mpage_writepages()函数来写 wbc->nr_to_write 个脏页。该函数调用 find_get_pages_tag()函数 快速获得索引节点地址空间的所有脏页(参见本章前面“基树的标记”一节),细节将在下 一章描述。 iii. 如果索引节点是脏的,就用超级块的 write_inode 方法把索引节点写到磁盘。实现该方法 的函数通常依靠 submit_bh()来传输一个数据块。 iv. 检查索引节点的状态。如果索引节点还有脏页,就把索引节点移回 sb->s_dirty 链表,如 果索引节点引用计数器为 0,就把索引节点移到 mode_unused 链表中;否则就把索引节点移 到 inode_in_use 链表中。 v. 返回在第 2f(ii)步所调用的函数的错误代码。 g) 回到 sync_sb_modes()函数中。如果当前进程是 pdflush 内核线程,就把在第 2d 步设置的 BDI_pdflush 标志清 0。 h) 如果略过了刚处理的索引节点中的一些页,那么该索引节点包括锁定的缓冲区:把 sb->s_io 链表中的所有剩余索引节点移回到 sb->s_dirty 链表中,以后讲重新处理它们。 i) 把索引节点的引用计数器减 1。 j) 如果 wbc->nr_to_write 大于 0,则回到第 2b 步搜索同一个超级块的其他脏索引节点。否 则,sync_sb_inodes()函数终止。 3. 回到 writeback_inodes()函数中。如果 wbc->nr_to_write 大于 0,就跳转到 1 步,并继续处 理全局链表中的下一个超级块。否则,就返回。 4.5.3 回写陈旧的脏页 如前所述,内核试图避免当一些页很久没有被刷新时发生饥饿危险。因此,脏页在保留一定 时间后,内核就显式地开始进行 1/O 数据的传输,把脏页的内容写到磁盘。 回写陈旧脏页的工作委托给了被定期唤醒的 pdflush 内核线程。在内核初始化期间 page_writeback_init()函数建立 wb_timer 动态定时器,以便定时器的到期时间发生在 dirty_writeback_centisecs 文件中所规定的几百分之一秒之后(通常是 500 分之一秒,不过可 以 通 过 修 改 /proc/sys/vm/dirty_writeback_centisecs 文件调整这个值)。定时器函数 wb_timer_fn()本质上调用 pdflush_operation()函数,传递给它的参数是回调函数 wb_kupdate() 的地址。 wb_kupdate()函数遍历页高速缓存搜索陈旧的脏索引节点,它执行下面的步骤: 1. 调用 sync_supers()函数把脏的超级块写到磁盘中(参见下一节)。虽然这与页高速缓存中 的页刷新没有很密切的关系,但对 sync_supers()的调用确保了任何超级块脏的时间通常不会 超过 5s。 2. 把当前时间减 30s 所对应的值(用 jiffies 表示)的指针存放在 writeback_control 描述符的 older_than_this 字段中。允许一个页保持脏状态的最长时间是 30s。 3. 根据每 CPU 变量 page_state 确定当前在页高速缓存中脏页的大概数量。 4. 反复调用 writeback_inodes(),直到写入磁盘的页数等于上一步所确定的值,或直到把所 有保持脏状态时间超过 30s 的页都写到磁盘。如果在循环的过程中一些请求队列变得拥塞, 函数就可能去睡眠。 5. 用 mod_timer()重新启动 wb_timer 动态定时器:一旦从调用该函数开始经历过文件 dirty_writeback_centisecs 中规定的几百分之一秒时间后,定时器到期(或者如果本次执行的 时间太长,就从现在开始 1s 后到期)。 sync()、fsync()和 fdatasync()系统调用 我们下面来简要介绍用户应用程序把脏缓冲区刷新到磁盘会用到的三个系统调用: sync() 允许进程把所有的脏缓冲区刷新到磁盘。 fsync() 允许进程把属于特定打开文件的所有块刷新到磁盘。 fdatasync() 与 fsync()非常相似,但不刷新文件的索引节点块。 sync()系统调用 sync()系统调用的服务例程 sys_sync()调用一系列辅助函数: wakeup_bdflush(0); sync_inodes(0); sync_supers(); sync_filesystems(0); sync_filesystems(1); sync_inodes(1); 正如上一节所描述的, wakeup_bdflush()启动 pdflush 内核线程,把页高速缓存中的所有脏页 刷新到磁盘。 sync_inodes()函数扫描超级块的链表以搜索要刷新的脏索引节点;它作用于参数 wait,该参 数表示在执行完刷新之前函数是否必须等待。函数扫描当前已安装的所有文件系统的超级块; 对于每个包含脏索引节点的超级块, sync_inodes()首先调用 sync_sb_inodes()刷新相应的脏页, 然后调用 sync_blockdev()显式刷新该超级块所在块设备的脏缓冲区页。这一步之所以能完成 是因为许多磁盘文件系统的 write_inode 超级块方法仅仅把磁盘索引节点对应的块缓冲区标 记为“脏”;函数 sync_blockdev()确保把 sync_sb_inodes()所完成的更新有效地写到磁盘。 函数 sync_supers()把脏超级块写到磁盘,如果需要,也可以使用适当的 write_super 超级块操 作。最后,sync_filesystems()为所有可写的文件系统执行 sync_fs 超级块方法。该方法只不 过是提供给文件系统的一个“钩子”,在需要对每个同步执行一些特殊操作时使用,只有像 Ext3 这样的日志文件系统使用这个方法。 注意,sync_inodes()和 sync_filesystems()都是被调用两次,一次是参数 wait 等于 0 时,另一 次是 wait 等于 1 时。这样做的目的是:首先,它们把未上锁的索引节点快速刷新到磁盘;其次,它们等待所有上锁的索引节点被解锁,然后把它们逐个地写到磁盘。 fsync()和 fdatasync()系统调用 系统调用 fsync()强制内核把文件描述符参数 fd 所指定文件的所有脏缓冲区写到磁盘中(如 果需要,还包括存有索引节点的缓冲区)。相应的服务例程获得文件对象的地址,并随后调 用 fsync 方法。通常这个方法以调用函数 __writeback_single_inode()结束,该函数把与被选中 的索引节点相关的脏页和索引节点本身都写回磁盘 系统调用 fdatasync()与 fsync()非常相似,但是它只把包含文件数据而不是那些包含索引节点 信息的缓冲区写到磁盘。由于 Linux 2.6 没有提供专门的 fdatasync()文件方法,该系统调用 使用 fsync 方法,因此与 fsync()是相同的。 5 文件读写 访问基于磁盘的文件是一种复杂的活动,主要涉及 VFS 抽象层、磁盘高速缓存的使用和块 设备驱动的处理。我们希望通过介绍内核如何使用这些技术实现文件的读及写,来让大家对 Linux 内核的核心机制加深理解。 访问文件的模式有大概有以下几种: 1. 规范模式 规范模式是指以标志 O_SYNC 与 O_DIRECT 清 0 的方式调用 open 系统调用打开文件,而 且它的内容是由系统调用 read()和 write()来存取。系统调用 read()将阻塞调用进程,直到数 据被拷贝进用户态地址空间(内核允许返回的字节数少于要求的字节数)。但系统调用 write() 不同,它在数据被拷贝到页高速缓存(延迟写)后就马上结束。 2. 同步摸式 同步模式是指以标志 O_SYNC 置 1 的方式调用 open 系统调用打开文件,或稍后由系统调用 fcntl()其将其置 1。这个标志只影响写操作(读操作总是会阻塞的),它将阻塞调用进程,等 待着直到数据被有效地写入磁盘。这一点与规范模式不同,规范模式是写入缓存后理解返回。 3. 内存映射摸式 内存映射模式是指,以规范模式或同步模式调用 open 系统调用打开文件后,应用程序发出 系统调用 mmap()将文件映射到内存中。因此,文件就成为 RAM 中的一个字节数组,应用 程序就可以直接访问数组元素,而不需用系统调用 read()、write()或 lseek()等这些系统调用 了。 4. 直接 I/O 模式 直接 I/O 模式是指,以标志 O_DIRECT 置 1 的方式调用 open 系统调用打开文件。任何读写 操作都将数据在用户态地址空间与磁盘间直接传送而不通过页高速缓存(标志 0_SYNC 和 0_DIRECT 的值可以有四种组合)。 5 异步模式 异步模式下,文件的访问可以有两种方法,即通过一组 POSIX API 或 Linux 特有的系统调 用来实现。所谓异步模式就是数据传输请求并不阻塞调用进程,而是在后台执行,同时应用 程序继续它的正常运行。对这个感兴趣的同学可以去查查“ I/O 模型”的相关资料 5.1 系统调用 VFS 层的处理 用户要读写文件首先是通过 open、read 和 write 等系统调用接口来进入内核。 Linux 系统调 用接口( SCI,system call interface)的实现机制实际上是一个多路汇聚以及分解的过程,该 汇聚点就是 0x80 中断这个入口点( X86 系统结构)。也就是说,所有系统调用都从用户空 间中汇聚到 0x80 中断点,同时保存具体的系统调用号。当 0x80 中断处理程序运行时,将 根据系统调用号对不同的系统调用分别处理。 当调用发生时,用户程序通过 C 语言的库函数在保存 read 系统调用号以及参数后,陷入 0x80 中断。这时库函数工作结束。 Read 系统调用在用户空间中的处理也就完成了。 0x80 中断处理程序接管执行后,先检察其系统调用号,然后根据系统调用号查找系统调用 表,并从系统调用表中得到处理 read 系统调用的内核函数 sys_read ,最后传递参数并运行 sys_read 函数。至此,内核真正开始处理 read 系统调用(sys_read 是 read 系统调用的内 核入口): asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count) { struct file *file; ssize_t ret = -EBADF; int fput_needed; file = fget_light(fd, &fput_needed); if (file) { loff_t pos = file_pos_read(file); ret = vfs_read(file, buf, count, &pos); file_pos_write(file, pos); fput_light(file, fput_needed); } return ret; } 首先调用 fget_light()函数 :根据 fd 指定的索引,从当前进程描述符中取出相应的 file 对象。 我们不去详细分析这个函数了,至于 fd 和 file 的关系,请参考博客“把 Linux 中的 VFS 对 象串联起来” http://blog.csdn.net/yunsongice/archive/2010/06/21/5683859.aspx。 随后,根据得到的 file 结构,获得它的当前读写位置 file->f_pos 赋给内部变量 pos。然后调 用 vfs_read() 执行文件读取操作,而这个函数最终调用 file->f_op->read() 指向的函数,代码 如下: ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { …… if (!ret) { if (file->f_op->read) ret = file->f_op->read(file, buf, count, pos); else ret = do_sync_read(file, buf, count, pos); …… } } return ret; } 到此,虚拟文件系统层所做的处理就完成了,控制权交给了 ext2 文件系统层。在解析 ext2 文件系统层的操作之前,先让我们看一下 file 对象中 read 指针来源。 读数据之前,必须先打开文件。处理 open 系统调用的内核函数为 sys_open 。这个系统调 用的核心是调用 filp_open() ,后者又通过调用 open_namei() 函数取出和该文件相关的 dentry 和 inode (因为前提指明了文件已经存在,所以 dentry 和 inode 能够查找到,不用 创建),然后调用 dentry_open() 函数创建新的 file 对象,并用 dentry 和 inode 中的信息 初始化 file 对象(文件当前的读写位置在 file 对象中保存)。注意到 dentry_open() 中有一 条语句: f->f_op = fops_get(inode->i_fop); 这个赋值语句把和具体文件系统相关的,操作文件的函数指针集合赋给了 file 对象的 f_op 变量(这个指针集合是保存在 inode 对象中的),在接下来的 sys_read 函数中将会调用 file->f_op 中的成员 read 。 至于 inode 的 i_fop 又是什么时候被初始化的,我们在下一节探索第二扩展文件系统的时候, 会讲到。 5.2 第二扩展文件系统 Ext2 层的处理 当内核安装 Ext2 文件系统时,一般使用 mount 命令,如: mount -f ext2 /dev/sda2 /mnt/test, 则会调用 sys_mount 系统调用。而在 sys_mount 又是调用 do_mount()函数执行文件系统安装 实务,传入的参数就是 mount 命令给的设备名“ /dev/sda2”、挂载目录“ /mnt/test”和文件系 统类型“ ext2”。do_mount()函数又会 do_new_mount 函数,后者进一步调用 do_kern_mount() 函数,给它传递的参数为文件系统类型、安装标志以及块设备名。 do_kern_mount()函数得到 ext2 文 件 系 统 类 型 的 file_system_type 结 构 , 进 而 调 用 vfs_kern_mount 函 数 , 触 发 file_system_type 结构中的 get_sb 函数。 那么 ext2 文件系统是在什么时候注册的呢,这个 get_sb 函数又是什么呢?看到 fs/ext2/super.c 代码最后几行,系统初始化时,通过 module_init(init_ext2_fs)调用 init_ext2_fs 函数对 ext2 文件系统进行注册: static int __init init_ext2_fs(void) { ⋯⋯ err = register_filesystem(&ext2_fs_type); ⋯⋯ } 而 ext2_fs_type 在同一个文件中被定义为: static struct file_system_type ext2_fs_type = { .owner = THIS_MODULE, .name = "ext2", .get_sb = ext2_get_sb, .kill_sb = kill_block_super, .fs_flags = FS_REQUIRES_DEV, }; ext2 文件系统对象 file_system_type 结构中的 get_sb 方法就是 ext2_get_sb,来自同一文件, 它调用 get_sb_bdev,传进去的参数是文件系统类型、设备名和加载目录,还有一个最重要 的就是 ext2_fill_super 函数的地址。于是在这个函数体内,就会调用这个 ext2_fill_super 函 数。关于文件系统的安装我讲的比较简略,如果愿意相信探究的同学请访问博客“文件系统 安装 ”http://blog.csdn.net/yunsongice/archive/2010/06/21/5685067.aspx。 5.2.1 Ext2 的磁盘布局 对于 ext2 文件系统来说,硬盘分区首先被划分为一个个的块( block),一个 ext2 文件系 统上的每个块都是一样大小的,但是对于不同的 ext2 文件系统,块的大小可以有区别。 Linux ext2 文件系统对块的大小有一个严格的限制,必须大于 1024 字节,小于 4096 字节, 定义在 include/linux/ext2_fs.h 中: #define EXT2_MIN_BLOCK_SIZE 1024 #define EXT2_MAX_BLOCK_SIZE 4096 这个大小在创建 ext2 文件系统的时候被决定,它可以由系统管理员指定,也可以由文件系 统的创建程序根据硬盘分区的大小,自动选择一个较合理的值。但是,为了跟一个页面大小 对齐,也就是 4k 对齐,供我们选择的也就只有三种大小, 1024、2048 和 4096。尽量跟页面 对齐的目的是高效地使用页高速缓存,所以我们后面都会默认设置这些块的大小为 1024, 也就是说一页包含 4 个块。在磁盘中,块被聚在一起分成几个大的块组( block group)。每 个块组中有多少个块是固定的,那么这个固定大小到底是多少呢?别着急,请您继续往下学 习。 每个块组都相对应一个组描述符(group descriptor),这些组描述符被聚在一起放在硬盘分 区的开头部分,紧跟在超级块( super block)的后面。所谓超级块,我们下面还要讲到。在 这个组描述符当中有几个重要的块指针。我们这里所说的块指针,跟 C 语言指针概念不一 样。后者描述的是内存地址值,而前者就是指硬盘分区上的块的号数,比如,指针的值为 0,我们就说它是指向硬盘分区上的 block 0;指针的值为 1023,我们就说它是指向硬盘分区上 的 block 1023。我们注意到,一个硬盘分区上的块计数是从 0 开始的,并且这个计数对于 这个硬盘分区来说是全局性质的。 磁盘中的超级块在 C 语言中是这样描述的,来自 include/linux/ext2_fs.h: struct ext2_super_block { __le32 s_inodes_count; /* Inodes count */ __le32 s_blocks_count; /* Blocks count */ __le32 s_r_blocks_count; /* Reserved blocks count */ __le32 s_free_blocks_count; /* Free blocks count */ __le32 s_free_inodes_count; /* Free inodes count */ __le32 s_first_data_block; /* First Data Block */ __le32 s_log_block_size; /* Block size */ __le32 s_log_frag_size; /* Fragment size */ __le32 s_blocks_per_group; /* # Blocks per group */ __le32 s_frags_per_group; /* # Fragments per group */ __le32 s_inodes_per_group; /* # Inodes per group */ __le32 s_mtime; /* Mount time */ __le32 s_wtime; /* Write time */ __le16 s_mnt_count; /* Mount count */ __le16 s_max_mnt_count; /* Maximal mount count */ __le16 s_magic; /* Magic signature */ __le16 s_state; /* File system state */ __le16 s_errors; /* Behaviour when detecting errors */ __le16 s_minor_rev_level; /* minor revision level */ __le32 s_lastcheck; /* time of last check */ __le32 s_checkinterval; /* max. time between checks */ __le32 s_creator_os; /* OS */ __le32 s_rev_level; /* Revision level */ __le16 s_def_resuid; /* Default uid for reserved blocks */ __le16 s_def_resgid; /* Default gid for reserved blocks */ __le32 s_first_ino; /* First non-reserved inode */ __le16 s_inode_size; /* size of inode structure */ __le16 s_block_group_nr; /* block group # of this superblock */ __le32 s_feature_compat; /* compatible feature set */ __le32 s_feature_incompat; /* incompatible feature set */ __le32 s_feature_ro_compat; /* readonly-compatible feature set */ __u8 s_uuid[16]; /* 128-bit uuid for volume */ char s_volume_name[16]; /* volume name */ char s_last_mounted[64]; /* directory where last mounted */ __le32 s_algorithm_usage_bitmap; /* For compression */ __u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/ __u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */ __u16 s_padding1; __u8 s_journal_uuid[16]; /* uuid of journal superblock */ __u32 s_journal_inum; /* inode number of journal file */ __u32 s_journal_dev; /* device number of journal file */ __u32 s_last_orphan; /* start of list of inodes to delete */ __u32 s_hash_seed[4]; /* HTREE hash seed */ __u8 s_def_hash_version; /* Default hash version to use */ __u8 s_reserved_char_pad; __u16 s_reserved_word_pad; __le32 s_default_mount_opts; __le32 s_first_meta_bg; /* First metablock block group */ __u32 s_reserved[190]; /* Padding to the end of the block */ }; 看到上面 ext2 文件系统的超级块,每个块组开头(磁盘的开头第一个 byte 是 byte 0,存 放的是引导块 MBR)第一个块都存放其一个拷贝。 其中 __u32 是表示 unsigned 不带符号的 32 bits 的数据类型,其余类推。这是 Linux 内 核中所用到的数据类型,如果是开发用户空间( user-space)的程序,可以根据具体计算机 平台的情况,用 unsigned long 等等来代替。 其实,对应 Ext2 文件系统的磁盘布局来说,比超级块更重要的是组描述符。在每个块组中 紧接着超级块后若干个块中,存放着整个磁盘所有的组描述符,其 C 语言定义同样来自来 自 include/linux/ext2_fs.h: struct ext2_group_desc { __le32 bg_block_bitmap; /* Blocks bitmap block */ __le32 bg_inode_bitmap; /* Inodes bitmap block */ __le32 bg_inode_table; /* Inodes table block */ __le16 bg_free_blocks_count; /* Free blocks count */ __le16 bg_free_inodes_count; /* Free inodes count */ __le16 bg_used_dirs_count; /* Directories count */ __le16 bg_pad; __le32 bg_reserved[3]; }; 在块组的组描述符中,其中有一个块指针指向这个组描述符的块位图( block bitmap),块位 图中的每个 bit 表示一个 block,如果该 bit 为 0,表示该 block 中有数据,如果 bit 为 1, 则表示该 block 是空闲的。注意,这个块位图本身也正好只有一个 block 那么大小。假设 block 大小为 1024 bytes,那么 block bitmap 当中只能记载 8*1024 个 block 的情况(因 为一个 byte 等于 8 个 bits,而一个 bit 对应一个 block)。这也就是说,一个块组最多只能有 8*1024*1024 字节,即 8MB 这么大。 在块组的组描述符中另有一个 block 指针指向磁盘索引节点位图( inode bitmap),这个位图 同样也是正好有一个块那么大,里面的每一个 bit 相对应一个磁盘索引节点。磁盘索引节点 跟我们在 VFS 看到的那个 inode 有联系,但不是一个概念,关于 inode,我们下面还要进一 步讲到。 在块组的组描述符中还有一个重要的 block 指针,是指向所谓的磁盘索引节点表(inode table)。这个磁盘索引节点表就不止一个 block 那么大了。这个磁盘索引节点表就是这个块 组中所聚集到的全部磁盘索引节点表放在一起形成的。 一个磁盘索引节点表当中记载的最关键的信息,是这个磁盘索引节点表中的用户数据存放在 什么地方。一个磁盘索引节点表大体上相对应于文件系统中的一个文件,那么用户文件的内 容存放在什么地方,这就是一个 inode 要回答的问题。一个 inode 通过提供一系列的 block 指针,来回答这个问题。这些 block 指针指向的 block,里面就存放了用户文件的内容。 我们通过一个图,把以上内容组织起来,希望大家对 Ext2 文件系统的磁盘布局有个感性认 识,这样我们才好继续后面的内容: 5.2.2 Ext2 的超级块对象 当安装 Ext2 文件系统时(执行诸如 mount -t ext2 /dev/sda2 /mnt/test 的命令),存放在 Ext2 分区的磁盘数据结构中的大部分信息将被拷贝到 RAM 中,从而使内核避免了后来的很多读 操作。那么一些数据结构如何经常更新呢?因为所有的 Ext2 磁盘数据结构都存放在 Ext2 磁 盘分区的块中,因此,内核利用页高速缓存来保持它们最新。 前面谈到,安装 ext2 文件系统时,最终会调用 ext2_fill_super()函数来为数据结构分配空间, 并写入从磁盘读取的数据。该函数首先用 kmalloc 分配一个 ext2_sb_info 描述符,将其地址 赋给当做参数传递进来的超级块 super_block 的 s_fs_info 字段。 随后通过__bread()在缓冲区页中分配一个缓冲区和缓冲区首部,这个函数属于也高速缓存的 范围,详细介绍请到博客“在页高速缓存中搜索块” http://blog.csdn.net/yunsongice/archive/2010/08/30/5850807.aspx。 然后从磁盘读入超级块存放在缓冲区中。在“在页高速缓存中搜索块”一博我们讨论过,如 果一个块已在页高速缓存的缓冲区页而且是最新的,那么无需再分配。将缓冲区首部地址存 放在 Ext2 超级块对象 sbi 的 s_sbh 字段,之后再用 kmalloc 分配一个数组用于存放缓冲区首 部指针,每个组描述符一个,把该数组地址存放在 ext2_sb_info 的 s_group_desc 字段。这样, 就可以通过重复调用 __bread()分配缓冲区,从磁盘读人包含 Ext2 组描述符的块。把缓冲区 首部地址存放在上一步得到的 s_group_desc 数组中。 最后安装 sb->s_op,sb->s_export_op,超级块增强属性。因为准备为根目录分配一个索引节 点和目录项对象,必须把 s_op 字段为超级块建立好,从而能够从磁盘读入根索引节点对象: sb->s_op = &ext2_sops; sb->s_export_op = &ext2_export_ops; sb->s_xattr = ext2_xattr_handlers; root = iget(sb, EXT2_ROOT_INO); sb->s_root = d_alloc_root(root); 注意,EXT2_ROOT_INO 是 2,也就是它的根节点,位于第一个块组的第三个位置上。 sb->s_op 被赋值成了 ext2_sops 来自 fs/ext2/super.c: static struct super_operations ext2_sops = { .alloc_inode = ext2_alloc_inode, .destroy_inode = ext2_destroy_inode, .read_inode = ext2_read_inode, .write_inode = ext2_write_inode, .put_inode = ext2_put_inode, .delete_inode = ext2_delete_inode, .put_super = ext2_put_super, .write_super = ext2_write_super, .statfs = ext2_statfs, .remount_fs = ext2_remount, .clear_inode = ext2_clear_inode, .show_options = ext2_show_options, #ifdef CONFIG_QUOTA .quota_read = ext2_quota_read, .quota_write = ext2_quota_write, #endif }; 很显然,ext2_fill super()函数返回后,有很多关键的 ext2 磁盘数据结构的内容都保存在内存 里了,例如 ext2_super_block、块设备的索引节点等,只有当 Ext2 文件系统卸载时才会被释 放。当内核必须修改 Ext2 超级块的字段时,它只要把新值写入相应缓冲区内的相应位置然 后将该缓冲区标记为脏即可,有了页高速缓存事情就是这么简单! 以上内容仅简单介绍一下该函数的一些关键步骤。该函数的详细分析请查看博客“ Ext2 的 超级块对象” http://blog.csdn.net/yunsongice/archive/2010/08/17/5819323.aspx, 并结合我们这 里给出的数据结构图进行分析: 要弄清第二扩展文件系统 Ext2 层的处理,除了 ext2 超级快对象,还要弄懂一个 ext2 索引节点对象。我们在上一节看到 ext2_fill_super 函数中,当初始化超级快后,还有一步是调用 iget 将索引节点号为 EXT2_ROOT_INO(一般为 2)的索引节点分配给该 ext2 磁盘分区的加载 目录,也就是根目录项。 static inline struct inode *iget(struct super_block *sb, unsigned long ino) { struct inode *inode = iget_locked(sb, ino); if (inode && (inode->i_state & I_NEW)) { sb->s_op->read_inode(inode); unlock_new_inode(inode); } return inode; } 看到 ext2_sops 全局变量我们得知, ext2_read_inode 就是 ext2 超级块的 s_op->read_inode 具 体实现函数,该函数会调用 ext2_get_inode 函数,从一个页高速缓存中读入一个磁盘索引节 点结构 ext2_inode,然后初始化 VFS 的 inode。当然,这个根目录不是普通文件,而是一个 目录文件,所以我们后面章节再来深入研究,这里只介绍其中最重要的初始化代码段,如下: struct inode *ext2_read_inode(struct inode *ino) { ⋯⋯ if (S_ISREG(inode->i_mode)) { /* 普通文件操作 */ inode->i_op = &ext2_file_inode_operations; if (ext2_use_xip(inode->i_sb)) { inode->i_mapping->a_ops = &ext2_aops_xip; inode->i_fop = &ext2_xip_file_operations; } else if (test_opt(inode->i_sb, NOBH)) { /* 不启动页高速缓存 */ inode->i_mapping->a_ops = &ext2_nobh_aops; inode->i_fop = &ext2_file_operations; } else { inode->i_mapping->a_ops = &ext2_aops; inode->i_fop = &ext2_file_operations; } } else if (S_ISDIR(inode->i_mode)) {/* 目录文件操作 */ inode->i_op = &ext2_dir_inode_operations; inode->i_fop = &ext2_dir_operations; if (test_opt(inode->i_sb, NOBH)) inode->i_mapping->a_ops = &ext2_nobh_aops; else inode->i_mapping->a_ops = &ext2_aops; } else if (S_ISLNK(inode->i_mode)) {/* 符号链接文件操作 */ if (ext2_inode_is_fast_symlink(inode)) inode->i_op = &ext2_fast_symlink_inode_operations; else { inode->i_op = &ext2_symlink_inode_operations; if (test_opt(inode->i_sb, NOBH)) inode->i_mapping->a_ops = &ext2_nobh_aops; else inode->i_mapping->a_ops = &ext2_aops; } } else {/* 其他特殊文件文件操作 */ inode->i_op = &ext2_special_inode_operations; if (raw_inode->i_block[0]) init_special_inode(inode, inode->i_mode, old_decode_dev(le32_to_cpu(raw_inode->i_block[0]))); else init_special_inode(inode, inode->i_mode, new_decode_dev(le32_to_cpu(raw_inode->i_block[1]))); } ⋯⋯ } 我们看到根目录的 inode 结构的 i_op 、i_mapping 和 i_fop 字段被赋值了。先来看一下 i_fop 被初始化成了 ext2_file_operations 全局变量: struct file_operations ext2_file_operations = { .llseek = generic_file_llseek, .read = generic_file_read, .write = generic_file_write, .aio_read = generic_file_aio_read, .aio_write = generic_file_aio_write, .ioctl = ext2_ioctl, .mmap = generic_file_mmap, .open = generic_file_open, .release = ext2_release_file, .fsync = ext2_sync_file, .readv = generic_file_readv, .writev = generic_file_writev, .sendfile = generic_file_sendfile, }; i_op 也被初始化成了 ext2_file_inode_operations 全局变量: struct inode_operations ext2_file_inode_operations = { .truncate = ext2_truncate, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif .setattr = ext2_setattr, .permission = ext2_permission, .fiemap = ext2_fiemap, }; i_mapping->a_ops 也被初始化成了 ext2_aops 全局变量: const struct address_space_operations ext2_aops = { .readpage = ext2_readpage, .readpages = ext2_readpages, .writepage = ext2_writepage, .sync_page = block_sync_page, .prepare_write = ext2_prepare_write, .commit_write = generic_commit_write, .bmap = ext2_bmap, .direct_IO = ext2_direct_IO, .writepages = ext2_writepages, .migratepage = buffer_migrate_page, }; 5.2.3 Ext2 索引节点对象的创建 现在还是从一个普通文件的角度来分析上面的过程,比如说,当我门在根目录下调用 fd = open("file", O_CREAT)打开(创建)一个文件时,会启动 do_sys_open 系统调用,并根据路 径“file”去触发 do_filp_open 函数返回一个 file 结构。do_filp_open 主要调用的两个函数(详 细的过程请参考博客“ VFS 系统调用的实现” http://blog.csdn.net/yunsongice/archive/2010/06/22/5685130.aspx): (1)open_namei():填充目标文件所在目录(也就是根目录)的 dentry 结构和所在文件系 统的 vfsmount 结构,并将信息保存在 nameidata 结构中。在 dentry 结构中 dentry->d_inode 就指向目标文件的索引节点。 (2)dentry_open():建立目标文件的一个“上下文”,即 file 数据结构,并让它与当前进程 的 task_strrct 结构挂上钩。同时,在这个函数中,调用了具体文件系统的打开函数,即 f_op->open(),也就是前面看到的 generic_file_open,,该函数返回指向新建立的 file 结构的指 针。 注意,如果在访问模式标志中设置了 O_CREAT,比如我们这里,则以 LOOKUP_PARENT、 LOOKUP_OPEN 和 LOOKUP_CREATE 标志的设置开始查找操作。一旦 path_lookup()函数 成功返回,则检查请求的文件是否已存在。如果不存在,则 open_namei 会调用父索引节点 的 create 方法分配一个新的磁盘索引节点。这里父索引节点是一个目录的,所以其 i_op 方 法 不 是 上 面 提 到 的 ext2_file_inode_operations , 而 是 ext2_dir_inode_operations , 来 自 fs/ext2/namei.c: const struct inode_operations ext2_dir_inode_operations = { .create = ext2_create, .lookup = ext2_lookup, .link = ext2_link, .unlink = ext2_unlink, .symlink = ext2_symlink, .mkdir = ext2_mkdir, .rmdir = ext2_rmdir, .mknod = ext2_mknod, .rename = ext2_rename, #ifdef CONFIG_EXT2_FS_XATTR .setxattr = generic_setxattr, .getxattr = generic_getxattr, .listxattr = ext2_listxattr, .removexattr = generic_removexattr, #endif .setattr = ext2_setattr, .check_acl = ext2_check_acl, }; ext2_create 函数定义在同一个文件中,传给它的参数是根目录的 inode,: static int ext2_create (struct inode * dir, struct dentry * dentry, int mode, struct nameidata *nd) { struct inode *inode; dquot_initialize(dir); inode = ext2_new_inode(dir, mode); if (IS_ERR(inode)) return PTR_ERR(inode); inode->i_op = &ext2_file_inode_operations; if (ext2_use_xip(inode->i_sb)) { inode->i_mapping->a_ops = &ext2_aops_xip; inode->i_fop = &ext2_xip_file_operations; } else if (test_opt(inode->i_sb, NOBH)) { inode->i_mapping->a_ops = &ext2_nobh_aops; inode->i_fop = &ext2_file_operations; } else { inode->i_mapping->a_ops = &ext2_aops; inode->i_fop = &ext2_file_operations; } mark_inode_dirty(inode); return ext2_add_nondir(dentry, inode); } 这里面最重要的是 ext2_new_inode()函数,用于在父目录 dir 下创建 Ext2 磁盘的索引节点, 返回相应的索引节点对象的地址(或失败时为 NULL)。该函数谨慎地选择存放该新索引节 点的块组;它将无联系的目录散放在不同的组,而且同时把文件存放在父目录的同一组。为 了平衡普通文件数与块组中的目录数, Ext2 为每一个块组引入“债( debt) ”参数。 ext2_new_inode 函数有两个参数:dir,所创建索引节点父目录对应的索引节点对象的地址, 新创建的索引节点必须插入到这个目录中,成为其中的一个目录项; mode,要创建的索引 节点的类型。后一个参数还包含一个 MS_SYNCHRONOUS 标志,该标志请求当前进程一直 挂起,直到索引节点被分配成功或失败。该函数代码时分复杂,详细的分析内容请参考博客 “Ext2 索引节点分配” http://blog.csdn.net/yunsongice/archive/2010/08/18/5822472.aspx,这 里仅仅概要地讲解一下分配步骤。 ext2_new_inode函数首先调用 new_inode()通过 sb->s_op->alloc_inode函数分配一个新的 VFS 索引节点对象,并把它的 i_sb 字段初始化为存放在 dir->i_sb 中的超级块地址,然后把它追 加到正在用的索引节点链表与超级块链表中。 前面看到,superblock 的 s_op->alloc_inode 函数地址为 ext2_alloc_inode,该函数来自 fs/ext2/super.c: static struct inode *ext2_alloc_inode(struct super_block *sb) { struct ext2_inode_info *ei; ei = (struct ext2_inode_info *)kmem_cache_alloc(ext2_inode_cachep, GFP_KERNEL); if (!ei) return NULL; ei->i_block_alloc_info = NULL; ei->vfs_inode.i_version = 1; return &ei->vfs_inode; } 很简单,通过 ext2_inode_cachep 的 slab 分配器分配一个磁盘索引节点描述符 ext2_inode_info 而作为 VFS 的 inode 结构作为嵌入其内部的 vfs_inode 字段被返回。注意, VFS 的 inode 对 象是嵌入在 ext2_inode_info 描述符中的,当执行了 ext2_alloc_inode 函数以后,new_inode() 函数内部就会有一个未初始化的 inode 结构。随后,new_inode()函数将这个 inode 分别插入到以 inode_in_use 和 sb->s_inodes 为首的循环链表中。 因为 inode 分别嵌入到 ext2_inode_info 结构中的,所以内核提供 EXT2_I 宏来获得已分配 inode 的 ext2_inode_info 结构: static inline struct ext2_inode_info *EXT2_I(struct inode *inode) { return container_of(inode, struct ext2_inode_info, vfs_inode); } #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) struct ext2_inode_info *ei = EXT2_I(inode); 那么 super_block 和 ext2_sb_info 的关系是不是这样的呢?上一节我们看到,磁盘中的 ext2_super_block 结构总是被页高速缓存到内存中,由 ext2_sb_info 结构的 s_es 字段指向, 所以内核提供的 EXT2_SB 宏就能直接得到 ext2_sb_info 数据结构位于内存的地址。 static inline struct ext2_sb_info *EXT2_SB(struct super_block *sb) { return sb->s_fs_info; } struct ext2_sb_info *sbi = EXT2_SB(sb); struct ext2_super_block *es = sbi->s_es; 因此,ext2_new_inode 函数内部,与分配的 inode 相关的磁盘索引节点映像 ext2_inode_info、 磁盘超级快映像 ext2_sb_info 和磁盘超级快对象 ext2_super_block 分别由内部变量 ei、sbi 和 es 指向。 随后如果新的索引节点是一个目录,函数就调用 find_group_orlov(sb, dir)为目录找到一个合 适的块组(安装 ext2 时,如果带一个参数 OLDALLOC,则会强制内核使用一种简单、老式 的方式分配块组: find_group_dir(sb, dir))。 如果新索引节点不是个目录,则调用 find_group_other(sb, dir),在有空闲索引节点的块组中 给它分配一个。 该函数从包含父目录的块组开始往下找。具体逻辑如下: a. 从包含父目录 dir 的块组开始,执行快速的对数查找。这种算法要查找 log(n)个块组,这 里 n 是块组总数。该算法一直向前查找直到找到一个可用的块组,具体如下:如果我们 把开始的块组称为 i,那么,该算法要查找的块组号为 i mod (n),i+1 mod (n),i+1+2 mod (n),i+1+2+4 mod (n),等等。大家是不是觉得这个东西是不是似曾相识呀,不错,就是 伙伴系统算法的思想。 b. 如果该算法没有找到含有空闲索引节点的块组,就从包含父目录 dir 的块组开始执行彻 底的线性查找,总能找到一个含有空闲索引节点的块组,除非磁盘满了。 find_group_other 函数将要分配磁盘索引节点的块组号 group,回到 ext2_new_inode 函数中, 下一步要做的事是设置位图。于是调用 read_inode_bitmap()得到所选块组的索引节点位图, 并从中寻找第一个空闲位,这样就得到了第一个空闲磁盘索引节点号: static struct buffer_head * read_inode_bitmap(struct super_block * sb, unsigned long block_group) { struct ext2_group_desc *desc; struct buffer_head *bh = NULL; desc = ext2_get_group_desc(sb, block_group, NULL); if (!desc) goto error_out; bh = sb_bread(sb, le32_to_cpu(desc->bg_inode_bitmap)); if (!bh) ext2_error(sb, "read_inode_bitmap", "Cannot read inode bitmap - " "block_group = %lu, inode_bitmap = %u", block_group, le32_to_cpu(desc->bg_inode_bitmap)); error_out: return bh; } 我们看到,read_inode_bitmap 函数接收超级块和刚刚我们得到的组号作为参数,通过 ext2_get_group_desc 函数获得超级快的块组描述符结构: struct ext2_group_desc * ext2_get_group_desc(struct super_block * sb, unsigned int block_group, struct buffer_head ** bh) { unsigned long group_desc; unsigned long offset; struct ext2_group_desc * desc; struct ext2_sb_info *sbi = EXT2_SB(sb); if (block_group >= sbi->s_groups_count) { ext2_error (sb, "ext2_get_group_desc", "block_group >= groups_count - " "block_group = %d, groups_count = %lu", block_group, sbi->s_groups_count); return NULL; } group_desc = block_group >> EXT2_DESC_PER_BLOCK_BITS(sb); offset = block_group & (EXT2_DESC_PER_BLOCK(sb) - 1); if (!sbi->s_group_desc[group_desc]) { ext2_error (sb, "ext2_get_group_desc", "Group descriptor not loaded - " "block_group = %d, group_desc = %lu, desc = %lu", block_group, group_desc, offset); return NULL; } desc = (struct ext2_group_desc *) sbi->s_group_desc[group_desc]->b_data; if (bh) *bh = sbi->s_group_desc[group_desc]; return desc + offset; } 看前面那个图, Ext2 层对超级块对象总是缓存的,整个磁盘的所有 ext2_group_desc 都存放 在其 s_group_desc 数组字段对应的 buffer_head 对应的磁盘高速缓存中,即同样缓存在同一 个页面中的,这个函数直接返回块中对应 offset 的 ext2_group_desc 就行了,有缓存,很简 单! 得到了 ext2_group_desc 之后,read_inode_bitmap 函数将其索引节点位图地址对应的那个块 缓存到页高速缓存中,最后将该高速缓存描述符 buffer_head 返回。 接下来 ext2_new_inode 函数要做的事,是分配磁盘索引节点 ext2_inode_info:把索引节点位 图中的相应位置位,并把含有这个位图的页高速缓存标记为脏。此外,如果文件系统安装时 指定了 MS_SYNCHRONOUS 标志,则调用 sync_dirty_buffer(bitmap_bh)开始 I/O 写操作并 等待,直到写操作终止。 初始化这个 ext2_inode_info 的字段。特别是,设置嵌入在其内部的 inode 的索引节点号 i_no, 并把 xtime.tv_sec 的值拷贝到 i_atime、i_mtime 及 i_ctime。把这个块组的索引赋给 ext2_inode_info 结构的 i_block_group 字段;初始化这个 ext2_inode_info 的访问控制列表 (ACL);将新索引节点 inode 对象插入散列表 inode_hashtable(insert_inode_hash(inode), 调用 mark_inode_dirty()把该索引节点的磁盘索引节点对象移进超级块脏索引节点链表;调 用 ext2_preread_inode()从磁盘读入包含该索引节点的块,将它存入页高速缓存。千万要注意, 这里可不是文件预读,而是对磁盘索引节点进行缓存。进行这种所谓的“预读”是因为最近 创建的索引节点可能马上会被读写: static void ext2_preread_inode(struct inode *inode) { unsigned long block_group; unsigned long offset; unsigned long block; struct buffer_head *bh; struct ext2_group_desc * gdp; struct backing_dev_info *bdi; bdi = inode->i_mapping->backing_dev_info; if (bdi_read_congested(bdi)) return; if (bdi_write_congested(bdi)) return; block_group = (inode->i_ino - 1) / EXT2_INODES_PER_GROUP(inode->i_sb); gdp = ext2_get_group_desc(inode->i_sb, block_group, &bh); if (gdp == NULL) return; /* * Figure out the offset within the block group inode table */ offset = ((inode->i_ino - 1) % EXT2_INODES_PER_GROUP(inode->i_sb)) * EXT2_INODE_SIZE(inode->i_sb); block = le32_to_cpu(gdp->bg_inode_table) + (offset >> EXT2_BLOCK_SIZE_BITS(inode->i_sb)); sb_breadahead(inode->i_sb, block); } static inline void sb_breadahead(struct super_block *sb, sector_t block) { __breadahead(sb->s_bdev, block, sb->s_blocksize); } 我们看到,ext2_preread_inode 中首先获得 inode 对应的块组号 block_group,然后通过这个 块组号获得对应的 ext2_group_desc。得到组描述符后,就可以得到该组对应磁盘索引节点 表的磁盘首地址了,然后再根据 i_ino 得到该磁盘索引节点在表中的位置,赋给内部变量 block。 通过 sb_breadahead 读入该索引节点对应磁盘索引节点对象进缓存,最后 ext2_new_inode 返 回新索引节点对象 inode 的地址。这样,如果想得到 i_ino 对应的 ext2_inode,就可以通过块 设备的页高速缓存 page->private->b_data 获得。 当调用 ext2_new_inode 函数之后,新的 inode 和它对应的磁盘索引节点就建好了,并且缓存 到了内存中,其体系结构如图: 如图,VFS 索引节点号为 ino 的 inode,通过函数 ext2_preread_inode 计数就能得到它对应磁 盘索引节点 ext2_inode 所在的块号 block,然后把缓冲区页的描述符插人基树,树根是与块 设备相关的特殊 bdev 文件系统中索引节点的 address_space 对象。这种缓冲区页必须满足很 强的约束条件,就是所有的块缓冲区涉及的块必须是在块设备上相邻存放的,这也是为什么 ext2 磁盘布局中,所有磁盘索引节点必须连续存放在一个索引节点表中的原因。 还有一点要注意,如果一个页作为缓冲区页使用,那么与它的块缓冲区相关的所有缓冲区首 部都被收集在一个单向循环链表中。缓冲区页描述符的 private 字段指向页中第一个块的缓 冲区(由于 private 字段包含有效数据,而且页的 PG_private 标志被设置,因此,如果页中 包含磁盘数据并且设置了 PG_private 标志,该页就是一个缓冲区页。注意,尽管如此,其 他与块 I/O 子系统无关的内核组件也因为别的用途而使用 private 和 PG_private 字段);每个 缓冲区首部存放在 b_this_page 字段中,该字段是指向链表中下一个缓冲区首部的指针。此 外,每个缓冲区首部还把缓冲区页描述符的地址存放在 b_page 中。 至于__breadahead 的底层是怎么实现的,对于块设备的页高速缓存(注意与后面的普通文件 页高速缓存相区别)的详细内容,请访问博客“把块存放在页高速缓存中” http://blog.csdn.net/yunsongice/archive/2010/08/30/5850656.aspx。 5.2.4 Ext2 索引节点对象的读取 上一节我们提到了当 open("file", O_CREAT)创建一个文件时,其对应的 Ext2 磁盘索引节点 是如何建立的,又是如何与系统中的其他数据结构联系的。现在还是从一个普通文件的角度 来分析,当我门在根目录下调用 fd = open("file", O_RDONLY)打开一个已经存在文件时,同 样也会启动 do_sys_open 系统调用,并根据路径“ file”去触发 do_filp_open 函数返回一个 file 结构。 而这个时候, do_filp_open 调用 open_namei()函数就跟前面也差不多,即填充目标文件所在 目录(也就是根目录)的 dentry 结构和所在文件系统的 vfsmount 结构,并将信息保存在 nameidata 结构中。在 dentry 结构中 dentry->d_inode 就指向目标文件的索引节点。 open_namei 中,如果没有设置 O_CREAT 标志位,即所打开的文件是存在的,就会执行一步 最重要的步骤—— path_lookup_open 函数。path_lookup_open()实现文件的查找功能;要打开 的文件若不存在,也就是上一节的情况,则还需要有一个新建的过程,则调用 path_lookup_create(),后者和前者封装的是同一个实际的路径查找函数,只是参数不一样, 使它们在处理细节上有所偏差。 当是以新建文件的方式打开文件时,即设置了 O_CREAT 标志位时需要创建一个新的索引节 点,代表创建一个文件。上一节看到, open_namei 会调用父索引节点的 create 方法分配一个 新的磁盘索引节点,即在 vfs_create()里的一句核心语句 dir->i_op->create(dir, dentry, mode, nd) 调用 ext2 文件系统所提供的创建索引节点方法 ext2_create。 注意:这边的索引节点的概念,还只是位于内存之中,它和磁盘上的物理的索引节点的关系 就像位于内存中和位于磁盘中的文件一样。此时新建的索引节点还不能完全标志一个物理文 件的成功创建,只有当把索引节点回写到磁盘上才是一个物理文件的真正创建。想想我们以 新建的方式打开一个文件,对其读写但最终没有保存而关闭,则位于内存中的索引节点会经 历从新建到消失的过程,而磁盘却始终不知道有人曾经想过创建一个文件,这是因为索引节 点没有回写的缘故。 我们这节只关注磁盘索引节点的查找,不管是 path_lookup_open()还是 path_lookup_create() 最终都是调用 __path_lookup_intent_open()来实现查找文件的功能。它调用 do_path_lookup 函 数,如果路径名的第一个字符是“ /”,那么查找操作必须从当前根目录开始:获取相应已安 装文件对象(current->fs->rootmnt)和目录项对象(current->fs->root)的地址;增加引用计数器的 值,并把它们的地址分别存放在 nd->mnt 和 nd->dentry 中。 否则,如果路径名的第一个字符不是“ /”,则查找操作必须从当前工作目录开始:获得相应 已安装文件系统对象(current->fs->mt)和目录项对象(current->fs->pwd)的地址;增加引 用计数器的值,并把它们的地址分别存放在 nd->mnt 和 nd->dentry 中。 随后 do_path_lookup 函数调用 link_path_walk()函数处理真正进行的查找操作:它接收的参数为要解析的路径名指针 name 和拥有目录项信息和安装文件系统信息的 nameidata 数据结 构的地址 nd,逐层地将各个路径组成部分解析成目录项对象,如果此目录项对象在目录项 缓存中,则直接从缓存中获得。如果不在缓存中,则需从磁盘中读取该目录项所对应的索引 节点;这将引发 VFS 和实际的文件系统的一次交互。 __link_path_walk 函数完成上述过程主要是通过调用 do_lookup ()函数进而触发 __d_lookup() 函数在目录项高速缓存中搜索分量。传递给它的参数是目录项对象参数 parent,目的在于在 指定的父目录中查找名字为 name 的目录项。比如我们要访问 /usr/local/sbin/hello.c 文件,那 么就会搜索/、usr、local 和 sbin 分量,分别作为 usr、local、sbin 和 hello.c 的父目录,如果 usr、local、sbin 或 hello.c 不在目录项高速缓存中,即目录项高速缓存中没有一个 dentry 的 d_parent 字段指向__d_lookup的参数 parent,__d_lookup就会返回一个 NULL的 dentry结构。 如果该目录项在缓存中不存在,我们假设 /、usr、local 和 sbin 分量在目录项缓存中,而 hello.c 分量不在,则 do_lookup 函数调用__d_lookup()时会发现“ hello.c”的 dentry 不在目录项高速 缓存中,或者虽然在页目录项高速缓存中,但它 dentry 的 d_parent 字段并不是 sbin 对应的 dentry结构,就会返回一个 NULL的 dentry结构。那么 do_lookup ()函数就会触发real_lookup()。 而传递给 real_lookup()函数的参数是该被打开文件的父目录的目录项,即我们这里 sbin 分量 对应的 dentry 结构;还有“hello.c”对应的 qstr 结构。 real_lookup()函数执行 sbin 分量对应的 dentry 结构对应的索引节点的 lookup 方法从磁盘读取 目录,创建一个新的目录项对象并把它插入到目录项高速缓存中,然后创建一个新的索引节 点对象并把它插入到索引节点高速缓存中(在少数情况下,函数 real_lookup()可能发现所请 求的索引节点已经在索引节点高速缓存中。路径名分量是最后一个路径名而且不是指向一个 目录,与路径名相应的文件有几个硬健接,并且最近通过与这个路径名中被使用过的硬健接 不同的硬链接访问过相应的文件)。 在这一步结束时, __link_path_walk 函数中的 next 局部变量中的 dentry 和 mnt 字段将分别指 向这次循环要解析的分量名 hello.c 的目录项对象和已安装文件系统对象,然后返回 0。 所以,这里我们就从 real_lookup 函数开始,来探寻当要打开一个普通文件“ hello.c”时,它 的 Ext2 磁盘索引节点是如何被读到缓存中的。 446static struct dentry * real_lookup(struct dentry * parent, struct qstr * name, struct nameidata *nd) 447{ 448 struct dentry * result; 449 struct inode *dir = parent->d_inode; 450 451 mutex_lock(&dir->i_mutex); ⋯⋯ 466 result = d_lookup(parent, name); 467 if (!result) { 468 struct dentry * dentry = d_alloc(parent, name); 469 result = ERR_PTR(-ENOMEM); 470 if (dentry) { 471 result = dir->i_op->lookup(dir, dentry, nd); 472 if (result) 473 dput(dentry); 474 else 475 result = dentry; 476 } 477 mutex_unlock(&dir->i_mutex); 478 return result; 479 } 480 ⋯⋯ 493} 我们看到, 471 行 real_lookup()执行索引节点的 lookup 方法,传递给他的参数是 sbin 分量对 应的 inode 结构和通过 d_alloc()函数给“hello.c”文件分配的一个 dentry 空壳。这个方法是 什么呢?看到上一节的那个 ext2_dir_inode_operations,对应的 lookup 方法是 ext2_lookup, 来自 fs/ext2/namei.c: 55static struct dentry *ext2_lookup(struct inode * dir, struct dentry *dentry, struct nameidata *nd) 56{ 57 struct inode * inode; 58 ino_t ino; 59 60 if (dentry->d_name.len > EXT2_NAME_LEN) 61 return ERR_PTR(-ENAMETOOLONG); 62 63 ino = ext2_inode_by_name(dir, dentry); 64 inode = NULL; 65 if (ino) { 66 inode = iget(dir->i_sb, ino); 67 if (!inode) 68 return ERR_PTR(-EACCES); 69 } 70 return d_splice_alias(inode, dentry); 71} 63 行,通过 ext2_inode_by_name 得到 sbin 分量对应 inode 对应的索引节点号,该函数是通 过调用 ext2_find_entry 函数通过“ hello.c”文件目录项名和目录项名的长度与缓存在页高速 缓存的其父目录,也就是 sbin 分量的磁盘数据(目录文件的磁盘数据内容跟普通文件不一 样,其存放的就是整个目录内所有文件的 ext2_dir_entry_2 结构)进行比对,得到“ hello.c” 文件对应的磁盘目录项结构 ext2_dir_entry_2: struct ext2_dir_entry_2 { __le32 inode; /* Inode number */ __le16 rec_len; /* Directory entry length */ __u8 name_len; /* Name length */ __u8 file_type; char name[EXT2_NAME_LEN]; /* File name */ }; 然后通过 ext2_dir_entry_2 的 inode 字段获得“ hello.c”文件对应的索引节点号。 ext2_lookup 函数的 66 行,调用 iget 从磁盘中获得 ext2_inode,并更新 inode 结构。下面我们来重点关注 iget 函数,来自 include/linux/fs.h: 1604static inline struct inode *iget(struct super_block *sb, unsigned long ino) 1605{ 1606 struct inode *inode = iget_locked(sb, ino); 1607 1608 if (inode && (inode->i_state & I_NEW)) { 1609 sb->s_op->read_inode(inode); 1610 unlock_new_inode(inode); 1611 } 1612 1613 return inode; 1614} iget 函数 1606 行首先调用 iget_locked,通过“ hello.c”文件对应的索引节点号 ino 在索引节 点缓存的 inode_hashtable 中获得其对应 VFS 的 inode 结构。当然,如果 inode 不在缓存里, iget_locked 就会调用 get_new_inode_fast 去触发超级块的 s_op 字段的 alloc_inode 函数 ext2_alloc_inode 来为它分配一个嵌入了 VFS 的 inode 结构的 ext2_inode_info。 于是,我们得到了一个 VFS 的 inode“空壳”机器宿主结构 ext2_inode_info“空壳”,随后, 1609 行,重要的来了,调用 super_block 的 s_op 字段的 read_inode 方法,将这个 inode “空 壳”作为参数传递进去。在“ Ext2 的超级块对象”一节中我们得知, ext2 文件系统的超级 块方法被赋值成了全局变量 ext2_sops,它的 read_inode 方法是 ext2_read_inode 函数,来自 fs/ext2/inode.c: 1058void ext2_read_inode (struct inode * inode) 1059{ 1060 struct ext2_inode_info *ei = EXT2_I(inode); 1061 ino_t ino = inode->i_ino; 1062 struct buffer_head * bh; 1063 struct ext2_inode * raw_inode = ext2_get_inode(inode->i_sb, ino, &bh); 1064 int n; 1065 1066#ifdef CONFIG_EXT2_FS_POSIX_ACL 1067 ei->i_acl = EXT2_ACL_NOT_CACHED; 1068 ei->i_default_acl = EXT2_ACL_NOT_CACHED; 1069#endif 1070 if (IS_ERR(raw_inode)) 1071 goto bad_inode; 1072 1073 inode->i_mode = le16_to_cpu(raw_inode->i_mode); 1074 inode->i_uid = (uid_t)le16_to_cpu(raw_inode->i_uid_low); 1075 inode->i_gid = (gid_t)le16_to_cpu(raw_inode->i_gid_low); 1076 if (!(test_opt (inode->i_sb, NO_UID32))) { 1077 inode->i_uid |= le16_to_cpu(raw_inode->i_uid_high) << 16; 1078 inode->i_gid |= le16_to_cpu(raw_inode->i_gid_high) << 16; 1079 } 1080 inode->i_nlink = le16_to_cpu(raw_inode->i_links_count); 1081 inode->i_size = le32_to_cpu(raw_inode->i_size); 1082 inode->i_atime.tv_sec = le32_to_cpu(raw_inode->i_atime); 1083 inode->i_ctime.tv_sec = le32_to_cpu(raw_inode->i_ctime); 1084 inode->i_mtime.tv_sec = le32_to_cpu(raw_inode->i_mtime); 1085 inode->i_atime.tv_nsec = inode->i_mtime.tv_nsec = inode->i_ctime.tv_nsec = 0; 1086 ei->i_dtime = le32_to_cpu(raw_inode->i_dtime); 1087 /* We now have enough fields to check if the inode was active or not. 1088 * This is needed because nfsd might try to access dead inodes 1089 * the test is that same one that e2fsck uses 1090 * NeilBrown 1999oct15 1091 */ 1092 if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { 1093 /* this inode is deleted */ 1094 brelse (bh); 1095 goto bad_inode; 1096 } 1097 inode->i_blksize = PAGE_SIZE; /* This is the optimal IO size (for stat), not the fs block size */ 1098 inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); 1099 ei->i_flags = le32_to_cpu(raw_inode->i_flags); 1100 ei->i_faddr = le32_to_cpu(raw_inode->i_faddr); 1101 ei->i_frag_no = raw_inode->i_frag; 1102 ei->i_frag_size = raw_inode->i_fsize; 1103 ei->i_file_acl = le32_to_cpu(raw_inode->i_file_acl); 1104 ei->i_dir_acl = 0; 1105 if (S_ISREG(inode->i_mode)) 1106 inode->i_size |= ((__u64)le32_to_cpu(raw_inode->i_size_high)) << 32; 1107 else 1108 ei->i_dir_acl = le32_to_cpu(raw_inode->i_dir_acl); 1109 ei->i_dtime = 0; 1110 inode->i_generation = le32_to_cpu(raw_inode->i_generation); 1111 ei->i_state = 0; 1112 ei->i_next_alloc_block = 0; 1113 ei->i_next_alloc_goal = 0; 1114 ei->i_prealloc_count = 0; 1115 ei->i_block_group = (ino - 1) / EXT2_INODES_PER_GROUP(inode->i_sb); 1116 ei->i_dir_start_lookup = 0; 1117 1118 /* 1119 * NOTE! The in-memory inode i_data array is in little-endian order 1120 * even on big-endian machines: we do NOT byteswap the block numbers! 1121 */ 1122 for (n = 0; n < EXT2_N_BLOCKS; n++) 1123 ei->i_data[n] = raw_inode->i_block[n]; 1124 1125 if (S_ISREG(inode->i_mode)) { 1126 inode->i_op = &ext2_file_inode_operations; 1127 if (ext2_use_xip(inode->i_sb)) { 1128 inode->i_mapping->a_ops = &ext2_aops_xip; 1129 inode->i_fop = &ext2_xip_file_operations; 1130 } else if (test_opt(inode->i_sb, NOBH)) { 1131 inode->i_mapping->a_ops = &ext2_nobh_aops; 1132 inode->i_fop = &ext2_file_operations; 1133 } else { 1134 inode->i_mapping->a_ops = &ext2_aops; 1135 inode->i_fop = &ext2_file_operations; 1136 } 1137 } else if (S_ISDIR(inode->i_mode)) { 1138 inode->i_op = &ext2_dir_inode_operations; 1139 inode->i_fop = &ext2_dir_operations; 1140 if (test_opt(inode->i_sb, NOBH)) 1141 inode->i_mapping->a_ops = &ext2_nobh_aops; 1142 else 1143 inode->i_mapping->a_ops = &ext2_aops; 1144 } else if (S_ISLNK(inode->i_mode)) { 1145 if (ext2_inode_is_fast_symlink(inode)) 1146 inode->i_op = &ext2_fast_symlink_inode_operations; 1147 else { 1148 inode->i_op = &ext2_symlink_inode_operations; 1149 if (test_opt(inode->i_sb, NOBH)) 1150 inode->i_mapping->a_ops = &ext2_nobh_aops; 1151 else 1152 inode->i_mapping->a_ops = &ext2_aops; 1153 } 1154 } else { 1155 inode->i_op = &ext2_special_inode_operations; 1156 if (raw_inode->i_block[0]) 1157 init_special_inode(inode, inode->i_mode, 1158 old_decode_dev(le32_to_cpu(raw_inode->i_block[0]))); 1159 else 1160 init_special_inode(inode, inode->i_mode, 1161 new_decode_dev(le32_to_cpu(raw_inode->i_block[1]))); 1162 } 1163 brelse (bh); 1164 ext2_set_inode_flags(inode); 1165 return; 1166 1167bad_inode: 1168 make_bad_inode(inode); 1169 return; 1170} “hello.c”文件对应的 VFS 的 inode 结构,作为参数传递进 ext2_read_inode 时,还只是个空 壳,我们仅仅知道它的索引节点号,即 inode->i_ino,所以 1061 行获得这个索引节点号。 1060 行,通过 EXT2_I 宏得到另一个作为其宿主的 ext2_inode_info 空壳。 1063 行,调用 ext2_get_inode 函数,从一个页高速缓存中读入一个磁盘索引节点结构 ext2_inode,传递进去的参数是 super_block 结构、索引节点号和一个还未被初始化的高速缓 存头 buffer_head 结构。ext2_get_inode 函数同样来自 fs/ext2/inode.c: static struct ext2_inode *ext2_get_inode(struct super_block *sb, ino_t ino, struct buffer_head **p) { struct buffer_head * bh; unsigned long block_group; unsigned long block; unsigned long offset; struct ext2_group_desc * gdp; *p = NULL; ⋯⋯ block_group = (ino - 1) / EXT2_INODES_PER_GROUP(sb); gdp = ext2_get_group_desc(sb, block_group, &bh); ⋯⋯ offset = ((ino - 1) % EXT2_INODES_PER_GROUP(sb)) * EXT2_INODE_SIZE(sb); block = le32_to_cpu(gdp->bg_inode_table) + (offset >> EXT2_BLOCK_SIZE_BITS(sb)); if (!(bh = sb_bread(sb, block))) goto Eio; *p = bh; offset &= (EXT2_BLOCK_SIZE(sb) - 1); return (struct ext2_inode *) (bh->b_data + offset); ⋯⋯ } 我们只简单地介绍一下 ext2_get_inode 的执行流程。函数首先获得索引节点号 ino 对应的磁 盘索引节点 ext2_inode 所在块组的组号,存放到内部变量 block_group 中。然后通过前面介 绍的 ext2_get_group_desc 函数获得该块组描述符结构 ext2_group_desc,由内部指针变量 gdp 指向。接下来通过索引节点号得到该磁盘索引节点在该块组磁盘索引节点表的位置,这个值 又保存在内部变量 offset 中。有了这个位置,就可以通过一个计算,得到引节点号 ino 对应 的磁盘索引节点 ext2_inode 所在的块号 block。那么我们通过 sb_bread(sb, block)将这块缓存 到 sb 对应的块设备页高速缓存中。那么此时,通过 (struct ext2_inode *) (bh->b_data + offset) 就能从内存中得到对应的磁盘索引节点了。 回到 ext2_read_inode 中,从内存中读到的 ext2_inode 的值保存在内部变量 raw_inode 中,随 后 1073~1162 行代码通过 raw_inode 中从磁盘上读入的数据来初始化 VFS 的 inode 和其宿主 ext2_inode_info 结构。这里面最重要的位于磁盘索引节点中的“ hello.c”的块索引 i_block[n] 拷贝到 ext2_inode_info 结构的 i_data[n]中;然后把对应 VFS 的 inode 结构的 i_op、 i_mapping->a_ops 和 i_fop 赋值成相应的操作函数表。 当 VFS 的 inode 及其宿主 ext2_inode_info 结构初始化完毕后,就可以把 ext2_inode 对应的 高速缓存 brelse 掉以释放内存的空间,达到动态缓存的效果。至此,经过 fd = open("file", O_RDONLY)打开一个已经存在文件,我们举的例子是 /usr/local/sbin/hello.c 文件,它对应的 VFS 的 inode 及其宿主 ext2_inode_info 结构就准备好了,下一步就可以通过 read 和 write 系 统调用进行文件的读写了。 5.2.5 Ext2 层读文件入口函数 好了,我们知道了 Ext2 文件系统的磁盘布局,以及始终缓存的磁盘超级拷贝块结构 ext2_super_block 和动态缓存的已分配磁盘索引节点结构 ext2_inode 这些预备知识。接下来 就假设一个文件的 inode 已经分配好,并且包含该文件所有块号的对应宿主 ext2_inode_info 结构也在内存中初始化好了。那么如何读这个文件? 前面讲了,ext2 层,也就是第二扩展文件系统的入口函数 generic_file_read,下面我们就从 它开始,进入读文件操作的 Ext2 层: ssize_t generic_file_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct iovec local_iov = { .iov_base = buf, .iov_len = count }; struct kiocb kiocb; ssize_t ret; init_sync_kiocb(&kiocb, filp); ret = __generic_file_aio_read(&kiocb, &local_iov, 1, ppos); if (-EIOCBQUEUED == ret) ret = wait_on_sync_kiocb(&kiocb); return ret; } 我们看到,generic_file_read 调用函数__generic_file_aio_read,来自 mm/filemap.c: 1134ssize_t 1135__generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov, 1136 unsigned long nr_segs, loff_t *ppos) 1137{ 1138 struct file *filp = iocb->ki_filp; 1139 ssize_t retval; 1140 unsigned long seg; 1141 size_t count; 1142 1143 count = 0; 1144 for (seg = 0; seg < nr_segs; seg++) { 1145 const struct iovec *iv = &iov[seg]; 1146 1147 /* 1148 * If any segment has a negative length, or the cumulative 1149 * length ever wraps negative then return -EINVAL. 1150 */ 1151 count += iv->iov_len; 1152 if (unlikely((ssize_t)(count|iv->iov_len) < 0)) 1153 return -EINVAL; 1154 if (access_ok(VERIFY_WRITE, iv->iov_base, iv->iov_len)) 1155 continue; 1156 if (seg == 0) 1157 return -EFAULT; 1158 nr_segs = seg; 1159 count -= iv->iov_len; /* This segment is no good */ 1160 break; 1161 } 1162 1163 /* coalesce the iovecs and go direct-to-BIO for O_DIRECT */ 1164 if (filp->f_flags & O_DIRECT) { 1165 loff_t pos = *ppos, size; 1166 struct address_space *mapping; 1167 struct inode *inode; 1168 1169 mapping = filp->f_mapping; 1170 inode = mapping->host; 1171 retval = 0; 1172 if (!count) 1173 goto out; /* skip atime */ 1174 size = i_size_read(inode); 1175 if (pos < size) { 1176 retval = generic_file_direct_IO(READ, iocb, 1177 iov, pos, nr_segs); 1178 if (retval > 0 && !is_sync_kiocb(iocb)) 1179 retval = -EIOCBQUEUED; 1180 if (retval > 0) 1181 *ppos = pos + retval; 1182 } 1183 file_accessed(filp); 1184 goto out; 1185 } 1186 1187 retval = 0; 1188 if (count) { 1189 for (seg = 0; seg < nr_segs; seg++) { 1190 read_descriptor_t desc; 1191 1192 desc.written = 0; 1193 desc.arg.buf = iov[seg].iov_base; 1194 desc.count = iov[seg].iov_len; 1195 if (desc.count == 0) 1196 continue; 1197 desc.error = 0; 1198 do_generic_file_read(filp,ppos,&desc,file_read_actor); 1199 retval += desc.written; 1200 if (desc.error) { 1201 retval = retval ?: desc.error; 1202 break; 1203 } 1204 } 1205 } 1206out: 1207 return retval; 1208} 函数__generic_file_aio_read()是所有文件系统实现同步和异步读操作所使用的通用例程。该 函数接受四个参数: kiocb 描述符的地址 iocb、iovec 描述符数组的地址 iov、数组的长度和 存放文件当前指针的一个变量的地址 ppos。iovec 描述符数组被函数 generic_file_read()调用 时只有一个元素,该元素描述待接收数据的用户态缓冲区。 为什么只有一个元素呢? read()系统调用的一个叫做 readv()的变体允许应用程序定义多个用 户态缓冲区,从文件读出的数据分散存放在其中;__generic_file_aio_read()函数也实现这种 功能,只不过从文件读出的数据将只烤贝到一个用户态缓冲区,所以只有一个元素。不过, 可以想象,使用多个缓冲区虽然简单,但需要执行更多的步骤。 我们现在来说明函数__generic_file_aio_read()的操作。为简单起见,我们只针对最常见的情 形,即对页高速缓存文件的系统调用 read()所引发的同步操作。我们不讨论如何对错误和异 常的处理。 我们看到, 1154 行调用 access_ok()来检查 iovec 描述符所描述的用户态缓冲区是否有效。因 为起始地址和长度已经从 sys_read()系统调用得到,因此在使用前需要对它们进行检查。如 何检查呢?access_ok 宏实际上是__range_not_ok 宏: #define access_ok(type, addr, size) (likely(__range_not_ok(addr, size) == 0)) #define __range_not_ok(addr, size) \ ({ \ unsigned long flag, roksum; \ __chk_user_ptr(addr); \ asm("add %3,%1 ; sbb %0,%0 ; cmp %1,%4 ; sbb $0,%0" \ : "=&r" (flag), "=r" (roksum) \ : "1" (addr), "g" ((long)(size)), \ "rm" (current_thread_info()->addr_limit.seg)); \ flag; \ }) 如果参数无效,也就是检查 addr 到 addr+ size 的地址区间大于 current 进程的 thread_info 结 构的 addr_limit.seg 的值,则返回错误代码 -EFAULT。 随后 1189,其实传进来的参数 nr_segs 是 1,所以 1190 行只建立一个读操作描述符,也就是 一个 read_descriptor_t 类型的数据结构。该结构存放与单个用户态缓冲相关的文件读操作的 当前状态。 typedef struct { size_t written; //已经拷贝到用户态缓冲区的字节数 size_t count; //待传送的字节数 union { char __user *buf; void *data; } arg; //在用户态缓冲区中的当前位置 int error; //读操作的错误码( 0 表示无错误) } read_descriptor_t; __generic_file_aio_read 函数判断本次读请求的访问方式,如果是直接 I/O 模式( filp->f_flags 被设置了 O_DIRECT 标志,即不经过 cache)的方式,则调用 generic_file_direct_IO 函数; 不过我们最常用的是 page cache 的方式,则调用 1198 行的 do_generic_file_read 函数,传 送给它文件对象指针 filp、文件偏移量指针 ppos,刚分配的读操作描述符的地址和函数 file_read_actor()的地址: static inline void do_generic_file_read(struct file * filp, loff_t *ppos, read_descriptor_t * desc, read_actor_t actor) { do_generic_mapping_read(filp->f_mapping, &filp->f_ra, filp, ppos, desc, actor); } 函数 do_generic_file_read 仅仅是一个包装函数,把该文件的 file 结构的 address_space 字段 传给 do_generic_mapping_read 函数: 872void do_generic_mapping_read(struct address_space *mapping, 873 struct file_ra_state *_ra, 874 struct file *filp, 875 loff_t *ppos, 876 read_descriptor_t *desc, 877 read_actor_t actor) 878{ 879 struct inode *inode = mapping->host; 880 unsigned long index; 881 unsigned long end_index; 882 unsigned long offset; 883 unsigned long last_index; 884 unsigned long next_index; 885 unsigned long prev_index; 886 loff_t isize; 887 struct page *cached_page; 888 int error; 889 struct file_ra_state ra = *_ra; 890 891 cached_page = NULL; 892 index = *ppos >> PAGE_CACHE_SHIFT; 893 next_index = index; 894 prev_index = ra.prev_page; 895 last_index = (*ppos + desc->count + PAGE_CACHE_SIZE-1) >> PAGE_CACHE_SHIFT; 896 offset = *ppos & ~PAGE_CACHE_MASK; 897 898 isize = i_size_read(inode); 899 if (!isize) 900 goto out; 901 902 end_index = (isize - 1) >> PAGE_CACHE_SHIFT; 903 for (;;) { 904 struct page *page; 905 unsigned long nr, ret; 906 907 /* nr is the maximum number of bytes to copy from this page */ 908 nr = PAGE_CACHE_SIZE; 909 if (index >= end_index) { 910 if (index > end_index) 911 goto out; 912 nr = ((isize - 1) & ~PAGE_CACHE_MASK) + 1; 913 if (nr <= offset) { 914 goto out; 915 } 916 } 917 nr = nr - offset; 918 919 cond_resched(); 920 if (index == next_index) 921 next_index = page_cache_readahead(mapping, &ra, filp, 922 index, last_index - index); 923 924find_page: 925 page = find_get_page(mapping, index); 926 if (unlikely(page == NULL)) { 927 handle_ra_miss(mapping, &ra, index); 928 goto no_cached_page; 929 } 930 if (!PageUptodate(page)) 931 goto page_not_up_to_date; 932page_ok: 933 934 /* If users can be writing to this page using arbitrary 935 * virtual addresses, take care about potential aliasing 936 * before reading the page on the kernel side. 937 */ 938 if (mapping_writably_mapped(mapping)) 939 flush_dcache_page(page); 940 941 /* 942 * When (part of) the same page is read multiple times 943 * in succession, only mark it as accessed the first time. 944 */ 945 if (prev_index != index) 946 mark_page_accessed(page); 947 prev_index = index; 948 949 /* 950 * Ok, we have the page, and it's up-to-date, so 951 * now we can copy it to user space... 952 * 953 * The actor routine returns how many bytes were actually used.. 954 * NOTE! This may not be the same as how much of a user buffer 955 * we filled up (we may be padding etc), so we can only update 956 * "pos" here (the actor routine has to update the user buffer 957 * pointers and the remaining count). 958 */ 959 ret = actor(desc, page, offset, nr); 960 offset += ret; 961 index += offset >> PAGE_CACHE_SHIFT; 962 offset &= ~PAGE_CACHE_MASK; 963 964 page_cache_release(page); 965 if (ret == nr && desc->count) 966 continue; 967 goto out; 968 969page_not_up_to_date: 970 /* Get exclusive access to the page ... */ 971 lock_page(page); 972 973 /* Did it get unhashed before we got the lock? */ 974 if (!page->mapping) { 975 unlock_page(page); 976 page_cache_release(page); 977 continue; 978 } 979 980 /* Did somebody else fill it already? */ 981 if (PageUptodate(page)) { 982 unlock_page(page); 983 goto page_ok; 984 } 985 986readpage: 987 /* Start the actual read. The read will unlock the page. */ 988 error = mapping->a_ops->readpage(filp, page); 989 990 if (unlikely(error)) { 991 if (error == AOP_TRUNCATED_PAGE) { 992 page_cache_release(page); 993 goto find_page; 994 } 995 goto readpage_error; 996 } 997 998 if (!PageUptodate(page)) { 999 lock_page(page); 1000 if (!PageUptodate(page)) { 1001 if (page->mapping == NULL) { 1002 /* 1003 * invalidate_inode_pages got it 1004 */ 1005 unlock_page(page); 1006 page_cache_release(page); 1007 goto find_page; 1008 } 1009 unlock_page(page); 1010 error = -EIO; 1011 shrink_readahead_size_eio(filp, &ra); 1012 goto readpage_error; 1013 } 1014 unlock_page(page); 1015 } 1016 1017 /* 1018 * i_size must be checked after we have done ->readpage. 1019 * 1020 * Checking i_size after the readpage allows us to calculate 1021 * the correct value for "nr", which means the zero-filled 1022 * part of the page is not copied back to userspace (unless 1023 * another truncate extends the file - this is desired though). 1024 */ 1025 isize = i_size_read(inode); 1026 end_index = (isize - 1) >> PAGE_CACHE_SHIFT; 1027 if (unlikely(!isize || index > end_index)) { 1028 page_cache_release(page); 1029 goto out; 1030 } 1031 1032 /* nr is the maximum number of bytes to copy from this page */ 1033 nr = PAGE_CACHE_SIZE; 1034 if (index == end_index) { 1035 nr = ((isize - 1) & ~PAGE_CACHE_MASK) + 1; 1036 if (nr <= offset) { 1037 page_cache_release(page); 1038 goto out; 1039 } 1040 } 1041 nr = nr - offset; 1042 goto page_ok; 1043 1044readpage_error: 1045 /* UHHUH! A synchronous read error occurred. Report it */ 1046 desc->error = error; 1047 page_cache_release(page); 1048 goto out; 1049 1050no_cached_page: 1051 /* 1052 * Ok, it wasn't cached, so we need to create a new 1053 * page.. 1054 */ 1055 if (!cached_page) { 1056 cached_page = page_cache_alloc_cold(mapping); 1057 if (!cached_page) { 1058 desc->error = -ENOMEM; 1059 goto out; 1060 } 1061 } 1062 error = add_to_page_cache_lru(cached_page, mapping, 1063 index, GFP_KERNEL); 1064 if (error) { 1065 if (error == -EEXIST) 1066 goto find_page; 1067 desc->error = error; 1068 goto out; 1069 } 1070 page = cached_page; 1071 cached_page = NULL; 1072 goto readpage; 1073 } 1074 1075out: 1076 *_ra = ra; 1077 1078 *ppos = ((loff_t) index << PAGE_CACHE_SHIFT) + offset; 1079 if (cached_page) 1080 page_cache_release(cached_page); 1081 if (filp) 1082 file_accessed(filp); 1083} 要看懂这个函数的代码,首先要简单地介绍一下文件高速缓存的背景知识。前面我们在讲解 Ext2 超级块对象和索引节点对象的时候,涉及到了一点高速缓存的知识,其实页高速缓存 的类型分为两种,磁盘页高速缓存和文件页高速缓存。 磁盘页高速缓存是,把缓冲区页的描述符插人基树,树根是与块设备相关的特殊 bdev 文件 系统中索引节点的 address_space 对象。前面我们已经讲过了,这里再强调一下,这种缓冲 区页必须满足很强的约束条件,就是所有的块缓冲区涉及的块必须是在块设备上相邻存放的。 而文件页高速缓存却不同,是把缓冲区页的描述符插入普通文件的 inode 对应 address_space 的基树。文件数据被分割为一个个以 4k 字节,也就是一个页面大小为单元的数据块,这些 数据块(页)被组织成一个基树( radix 树)。 树中所有叶子节点为一个个页框结构( struct page),表示了用于缓存该文件的每一个页。在叶子层最左端的第一个页保存着该文件的前 4096 个字节,接下来的页保存着文件第二个 4096 个字节,严格地依次类推。 树中的所有中间节点为组织节点,指示某一地址上的数据所在的页。此树的层次可以从 0 层 到 6 层,所支持的文件大小从 0 字节到 16 T 个字节。树的根节点指针可以从和文件相关 的 address_space 对象(该对象保存在和文件关联的 inode 对象中)中取得(更多关于 page cache 的结构内容请参见博客“磁盘高速缓存” http://blog.csdn.net/yunsongice/archive/2010/08/23/5833154.aspx)。 言归正传,函数 do_generic_mapping_read 首先 879 行获得地址空间对象的所有者,即索引 节点对象,它将拥有填充了文件数据的页面。它的地址存放在 address_space 对象的 host 字 段中。千万要注意,如果所读文件是块设备文件,这也是一般情况下,我们读取 SCSI 磁盘上的文件,那么所有者就不是由 filp->f_dentry->d_inode 所指向的索引节点对象,而是 bdev 特殊文件系统中的索引节点对象。 892 行,把文件看作细分的数据页(每页 4096 字节),并从文件指针 *ppos 导出第一个请求 字节所在页的逻辑号,即地址空间中的页索引,并把它存放在 index 局部变量中。也把第一 个请求字节在页内的偏移量存放在 offset 局部变量中。 904 行开始一个循环来读入包含请求字节的所有页,要读数据的字节数存放在 read_descriptor_t 描述符的 count 字段中。在一次单独的循环期间,函数通过执行下列的子步 骤来传送一个数据页: 909~916 行,如果 index*4096+offset 超过存放在索引节点对象的 i_size 字段中的文件大小, 则从循环退出。 919 行调用 cond_resched()来检查当前进程的标志 TIF_NEED_RESCHED。 如果该标志置位,则调用函数 schedule()。如果有预读的页, 921 行读入这些页面。 注意到函数中还有一个内部变量 next_index , 在 函 数 内 等 于 index , 所 以 921 行 page_cache_readahead 函数肯定会执行,用来预读一些页面,取决于传递进来的 file 参数的 f_ra 字段。后面章节我们会详细讲解文件的预读。这里就略过。 函数中还有一个内部变量 last_index,这个 last_index 指向需要读的所有页面集合的最后一个 页框号(我们需要读的页面范围是 last_index - index)。那么接下来的工作就是从 index 到 last_index 一页一页的读入到用户空间。 925 行调用 find_get_page(),并传入指向 address_space 对象的指针及索引值作为参数;它将 查找页高速缓存以找到包含所请求数据的页描述符(如果有的话)。我们看到,根据文件当 前的读写位置,在页高速缓存中中找到缓存请求数据的 page。这个 page 的 index 的值是文 件当前读写位置 ppos 右移 PAGE_CACHE_SHIFT。这个宏等于 PAGE_SHIFT,也就是 12, 正好反映了, ppos 在磁盘中,距离文件开头所使用了页面个数。 如果 find_get_page()返回 NULL 指针,则所请求的页不在页高速缓存中。如果这样, 927 行 调用 handle_ra_miss()来调整预读系统的参数。然后走到 1050 行的 no_cached_page 标号下, 调用 page_cache_alloc_cold使用伙伴系统 alloc_pages 分配一个新页;调用 add_to_page_cache() 插入该新页的页描述符到 address_space 的基树 page_tree 中,对应位置为局部变量 index, 即对应磁盘地址空间中的页索引。记住该函数将新页的 PG_locked 标志置位。调用 add_to_page_cache_lru()将该新页描述符插入到 LRU 链表。然后跳到 readpage 开始读文件 数据。 如果 find_get_page()返回该页,说明页已经位于页高速缓存中。通过 930 行 PageUptodate 宏 检查标志 PG_uptodate:如果置位,则页所存数据是最新的,因此无需从磁盘读数据,就到 page_ok 标号。如果没有置位,说明页中的数据是无效的,因此必须从磁盘读取,则跳到 page_not_up_to_date 标号处,通过 971 行调用 lock_page()函数获取对页的互斥访问。 现在页已由当前进程锁定。然而,另一个进程也许会在上一步之前已从页高速缓存中删除该 页,那么,它就要检查页描述符的 mapping 字段是否为 NULL。在这中情形下,它将调用975 行的 unlock_page()来解锁页,976 行的 page_cache_release 宏减少它的引用计数 (find_get_page()所增加的计数),并跳回 924 行的 find_page 标号处来重读同一页。 如果函数已运行至 980 行,说明页已被锁定且在页高速缓存中。981 行再次检查标志 PG_uptodate,因为另一个内核控制路径可能已经完成上面的必要读操作。如果标志置位, 则调用 unlock_page()并跳至 page_ok 标号处来跳过读操做。 现在真正的 I/O 操作可以开始了, 988 行调用文件的 address_space 对象之 readpage 方法。传 递给它的是对应的待填充页描述符 page 和文件对象 file,函数会负责激活磁盘到页之间的 I/O 数据传输。我们稍后再讨论该函数对普通文件与块设备文件都会做些什么。 来到 998 行,如果标志 PG_uptodate 还没有置位,则它会等待直到调用 lock_page()函数后页 被有效读入。该页在前面被锁定,一旦读操作完成就被解锁。因此当前进程在 I/O 数据传输 完成时才停止睡眠。 1027 行,如果 index 超出文件包含的页数(该数是通过将 inode 对象的 i_size 字段的值除以 4096 得到的),那么它将减少页的引用计数器,并跳出循环。这种情况发生在这个正被本进 程读的文件同时有其他进程正在删减它的时候。 1033-1041 行将应被拷入用户态缓冲区的页中的字节数存放在局部变量 nr 中。这个值应该等 于页的大小( 4096 字节),除非 offset 非 0(这只发生在读请求书的首尾页时)或请求数据 不全在该文件中。 又回到 page_ok 标号,这回已经 prev_index 不等于 index 了,所以调用 mark_page_accessed() 将标志 PG_referenced 或 PG_active 置位,从而表示该页正被访问并且不应该被换出。如果 同一文件(或它的一部分)在 do_generic_file_read()的后续执行中要读几次,那么这个步骤 只在第一次读时执行。 现在到了把页中的数据拷贝到用户态缓冲区的时候了。为了这么做, do_generic_file_read() 的 959 行调用 file_read_actor()函数,该函数的地址作为参数传递。 file_read_actor()执行下列 步骤: i. 调用 kmap(),该函数为处于高端内存中的页建立永久的内核映射。 ii. 调用__copy_to_user(),该函数把页中的数据拷贝到用户态地址空间。注意,这个操作 在访问用户态地址空间时如果有缺页异常将会阻塞进程。 iii. 调用 kunmap()来释放页的任一永久内核映射。 iv. 更新 read_descriptor_t 描述符的 count、 written 和 buf 字段。 960 行,根据传入用户态缓冲区的有效字节数来更新局部变量 index 和 count。一般情况下, 如果页的最后一个字节已拷贝到用户态缓冲区,那么 index 的值加 1 而 offset 的值清 0;否 则,index 的值不变而 offset 的值被设为已拷贝到用户态缓冲区的字节数。 964 行减少页描述符的引用计数器。 965 行判断:如果 read_descriptor_t 描述符的 count 字段 不为 0,那么文件中还有其他数据要读,继续循环来读文件中的下一页数据。 至此,所有请求的或者说可以读到的数据已读完。到 out;标号,1076 行更新预读数据结构 filp->f_ra 来标记数据已被顺序从文件读入。 1078 行把 index*4096+offset 值赋给*ppos,从而 保存以后调用 read()和 write()进行顺序访问的位置。 最后调用 file_accessed(filp)把当前时间存放在文件的索引节点对象的 i_atime 字段中,并把 它标记为脏后返回。 到此,我们知道:当页上的数据不是最新的时候,该函数调用 mapping->a_ops->readpage 所 指向的函数(变量 mapping 为 inode 对象中的 address_space 对象),那么这个函数到底是什 么呢? 前面我们学习了, ext2_fill_super 函数初始化超级快后, ext2_read_inode 就是 ext2 超级块的 s_op->read_inode 具体实现函数。当需要建立一个文件的 inode 时 , 该 函 数 会 调 用 ext2_get_inode 函数,从一个页高速缓存中读入一个磁盘索引节点结构 ext2_inode,然后初 始化 VFS 的 inode。其中,索引节点 inode->i_mapping->a_ops 被初始化成了 ext2_aops 全局 变量。 其实,address_space 对象是嵌入在 inode 对象之中的,那么不难想象, address_space 对象 成员 a_ops 的初始化工作将会在初始化 inode 对象时进行。 ext2_aops 全局变量的 readpage 字段都是 ext2_readpage,最终调用 ext2_readpage 函数处理读数据请求。到此为止, ext2 文 件系统层的工作结束。 5.3 页高速缓存层的处理 从上文得知: ext2_readpage 函数是该层的入口点,传给它的参数是文件的 file,以及需要读 入页高速缓存的页面。这个页面不是别的,正是刚才 page_cache_alloc_cold 分配的那个空白 页面,其现在位于文件的基树中,基树中索引为 index: static int ext2_readpage(struct file *file, struct page *page) { return mpage_readpage(page, ext2_get_block); } 该函数调用 mpage_readpage 函数,传给他需要读入页高速缓存的页面的页描述符地址,以 及 ext2_get_block 函数地址作为参数。 需要封装函数是因为 mpage_readpage()函数接收的参数为待填充页的页描述符 page 及有助 于 mpage_readpage()找到正确块的函数的地址 get_block。封装函数依赖文件系统并因此能提 供适当的函数来得到块。 这个函数把相对于文件开始位置的逻辑块号转换为相对于磁盘分区 开始位置的逻辑块号。 当然,后一个参数依赖于普通文件所在文件系统的类型。在我们的例子中,这个参数就是ext2_get_block()函数的地址。所传递的 get_block 函数总是用缓冲区首部 buffer_head 来存放 有关重要信息,如块设备( b_dev 字段)、设备上请求数据的位置( b_blocknr 字段)和块状 态(b_state 字段)。 函数 mpage_readpage()在从磁盘读入一页时可选择两种不同的策略。如果包含请求数据的块 在磁盘上是连续的,那么函数就用单个 bio 描述符向通用块层发出读 I/O 操作;而如果不连 续,函数就对页上的每一块用不同的 bio 描述符来读。所以,依赖于文件系统的 get_block 函数的一个重要作用就是:确定文件中的下一块在磁盘上是否也是下一块。 5.3.1 创建一个 bio 请求 看到 mpage_readpage 函数的具体代码,来自 fs/mpage.c: 427int mpage_readpage(struct page *page, get_block_t get_block) 428{ 429 struct bio *bio = NULL; 430 sector_t last_block_in_bio = 0; 431 struct buffer_head map_bh; 432 unsigned long first_logical_block = 0; 433 434 clear_buffer_mapped(&map_bh); 435 bio = do_mpage_readpage(bio, page, 1, &last_block_in_bio, 436 &map_bh, &first_logical_block, get_block); 437 if (bio) 438 mpage_bio_submit(READ, bio); 439 return 0; 440} 该函数 434 行首先调用 clear_buffer_mapped 函数将一个类型为 buffer_head 的变量 map_bh 的 b_state和b_size 字段清零;随后调用函数 do_mpage_readpage 函数创建了一个 bio 请求, 该请求指明了要读取的数据块所在磁盘的位置、数据块的数量以及拷贝该数据的目标位置— —缓存区中 page 的信息。然后调用 mpage_bio_submit 函数处理请求。 这里简单介绍一下通用块层的一些基础知识。通用块层的核心数据结构是一个称为 bio 的描 述符,它描述了块设备的 I/O 操作。每个 bio 结构都包含一个磁盘存储区标识符(存储区中 的起始扇区号和扇区数目)和一个或多个描述与 I/O 操作相关的内存区的段。 bio 由 bio 数 据结构描述: struct bio { sector_t bi_sector; //块 I/O 操作的第一个磁盘扇区 struct bio *bi_next; //链接到请求队列中的下一个 bio struct block_device *bi_bdev;//指向块设备描述符的指针 unsigned long bi_flags; //bio 的状态标志 unsigned long bi_rw; //IO 操作标志,即这次 I.O 是读或写 unsigned short bi_vcnt; /* bio 的 bio_vec 数组中段的数目 */ unsigned short bi_idx; /* bio 的 bio_vec 数组中段的当前索引值 */ unsigned short bi_phys_segments; //合并之后 bio 中物理段的数目 unsigned short bi_hw_segments; //合并之后硬件段的数目 unsigned int bi_size; /* 需要传送的字节数 */ unsigned int bi_hw_front_size;// 硬件段合并算法使用 unsigned int bi_hw_back_size;// 硬件段合并算法使用 unsigned int bi_max_vecs; /* bio 的 bio vec 数组中允许的最大段数 */ struct bio_vec *bi_io_vec; /*指向 bio 的 bio_vec 数组中的段的指针 */ bio_end_io_t *bi_end_io; /* bio 的 I/O 操作结束时调用的方法 */ atomic_t bi_cnt; /* bio 的引用计数器 */ void *bi_private; //通用块层和块设备驱动程序的 I/O 完成方法使用的指针 bio_destructor_t *bi_destructor;//释放 bio 时调用的析构方法(通常是 bio_destructor() 方法)r }; 好了,知道 BIO 数据结构以后,我们就可以进入通用块层了。那么,为了启动一次磁盘访 问了,需要初始化一个什么样的 BIO,来看 do_mpage_readpage 的内容: 175static struct bio * 176do_mpage_readpage(struct bio *bio, struct page *page, unsigned nr_pages, 177 sector_t *last_block_in_bio, struct buffer_head *map_bh, 178 unsigned long *first_logical_block, get_block_t get_block) 179{ 180 struct inode *inode = page->mapping->host; 181 const unsigned blkbits = inode->i_blkbits; 182 const unsigned blocks_per_page = PAGE_CACHE_SIZE >> blkbits; 183 const unsigned blocksize = 1 << blkbits; 184 sector_t block_in_file; 185 sector_t last_block; 186 sector_t last_block_in_file; 187 sector_t blocks[MAX_BUF_PER_PAGE]; 188 unsigned page_block; 189 unsigned first_hole = blocks_per_page; 190 struct block_device *bdev = NULL; 191 int length; 192 int fully_mapped = 1; 193 unsigned nblocks; 194 unsigned relative_block; 195 196 if (page_has_buffers(page)) 197 goto confused; 198 199 block_in_file = (sector_t)page->index << (PAGE_CACHE_SHIFT - blkbits); 200 last_block = block_in_file + nr_pages * blocks_per_page; 201 last_block_in_file = (i_size_read(inode) + blocksize - 1) >> blkbits; 202 if (last_block > last_block_in_file) 203 last_block = last_block_in_file; 204 page_block = 0; 205 206 /* 207 * Map blocks using the result from the previous get_blocks call first. 208 */ 209 nblocks = map_bh->b_size >> blkbits; 210 if (buffer_mapped(map_bh) && block_in_file > *first_logical_block && 211 block_in_file < (*first_logical_block + nblocks)) { 212 unsigned map_offset = block_in_file - *first_logical_block; 213 unsigned last = nblocks - map_offset; 214 215 for (relative_block = 0; ; relative_block++) { 216 if (relative_block == last) { 217 clear_buffer_mapped(map_bh); 218 break; 219 } 220 if (page_block == blocks_per_page) 221 break; 222 blocks[page_block] = map_bh->b_blocknr + map_offset + 223 relative_block; 224 page_block++; 225 block_in_file++; 226 } 227 bdev = map_bh->b_bdev; 228 } 229 230 /* 231 * Then do more get_blocks calls until we are done with this page. 232 */ 233 map_bh->b_page = page; 234 while (page_block < blocks_per_page) { 235 map_bh->b_state = 0; 236 map_bh->b_size = 0; 237 238 if (block_in_file < last_block) { 239 map_bh->b_size = (last_block-block_in_file) << blkbits; 240 if (get_block(inode, block_in_file, map_bh, 0)) 241 goto confused; 242 *first_logical_block = block_in_file; 243 } 244 245 if (!buffer_mapped(map_bh)) { 246 fully_mapped = 0; 247 if (first_hole == blocks_per_page) 248 first_hole = page_block; 249 page_block++; 250 block_in_file++; 251 clear_buffer_mapped(map_bh); 252 continue; 253 } 254 255 /* some filesystems will copy data into the page during 256 * the get_block call, in which case we don't want to 257 * read it again. map_buffer_to_page copies the data 258 * we just collected from get_block into the page's buffers 259 * so readpage doesn't have to repeat the get_block call 260 */ 261 if (buffer_uptodate(map_bh)) { 262 map_buffer_to_page(page, map_bh, page_block); 263 goto confused; 264 } 265 266 if (first_hole != blocks_per_page) 267 goto confused; /* hole -> non-hole */ 268 269 /* Contiguous blocks? */ 270 if (page_block && blocks[page_block-1] != map_bh->b_blocknr-1) 271 goto confused; 272 nblocks = map_bh->b_size >> blkbits; 273 for (relative_block = 0; ; relative_block++) { 274 if (relative_block == nblocks) { 275 clear_buffer_mapped(map_bh); 276 break; 277 } else if (page_block == blocks_per_page) 278 break; 279 blocks[page_block] = map_bh->b_blocknr+relative_block; 280 page_block++; 281 block_in_file++; 282 } 283 bdev = map_bh->b_bdev; 284 } 285 286 if (first_hole != blocks_per_page) { 287 char *kaddr = kmap_atomic(page, KM_USER0); 288 memset(kaddr + (first_hole << blkbits), 0, 289 PAGE_CACHE_SIZE - (first_hole << blkbits)); 290 flush_dcache_page(page); 291 kunmap_atomic(kaddr, KM_USER0); 292 if (first_hole == 0) { 293 SetPageUptodate(page); 294 unlock_page(page); 295 goto out; 296 } 297 } else if (fully_mapped) { 298 SetPageMappedToDisk(page); 299 } 300 301 /* 302 * This page will go to BIO. Do we need to send this BIO off first? 303 */ 304 if (bio && (*last_block_in_bio != blocks[0] - 1)) 305 bio = mpage_bio_submit(READ, bio); 306 307alloc_new: 308 if (bio == NULL) { 309 bio = mpage_alloc(bdev, blocks[0] << (blkbits - 9), 310 min_t(int, nr_pages, bio_get_nr_vecs(bdev)), 311 GFP_KERNEL); 312 if (bio == NULL) 313 goto confused; 314 } 315 316 length = first_hole << blkbits; 317 if (bio_add_page(bio, page, length, 0) < length) { 318 bio = mpage_bio_submit(READ, bio); 319 goto alloc_new; 320 } 321 322 if (buffer_boundary(map_bh) || (first_hole != blocks_per_page)) 323 bio = mpage_bio_submit(READ, bio); 324 else 325 *last_block_in_bio = blocks[blocks_per_page - 1]; 326out: 327 return bio; 328 329confused: 330 if (bio) 331 bio = mpage_bio_submit(READ, bio); 332 if (!PageUptodate(page)) 333 block_read_full_page(page, get_block); 334 else 335 unlock_page(page); 336 goto out; 337} 首 先 , 180~182 行,得到每个页高速缓存的块存放能力。通过存放在 page->mapping->host->i_blkbits 索引节点字段,得到每一个页面应该存放多少个块,前面我 们设定的块大小是 1024 个字节,那么一个页就应该存放 4 个块,这也是为什么前面讲到设 置块大小的时候一定要跟页面对其的原因。 然后,196 行检查页描述符的 PG_private 字段: #define page_has_buffers(page) PagePrivate(page) #define page_private(page) ((page)->private) 如果置位,则该页是块设备页高速缓存的页,也就是该页与描述组成该页的块的缓冲区首部 链表相关。这意味着该页过去已从磁盘读入过,而且页中的块在磁盘上不是相邻的。跳到标 号 confused 处,用一次读一块的方式读该页。 然后 199 行计算出访问该页的所有块所需要的位置值,即页中的块数 blocks_per_page 及页 中第一块的文件块号,也就是相对于文件起始位置页中第一块的索引,存放在内部变量 block_in_file 中。 210~228行代码是处理如果 map_bh已经被映射(通过 buffer_mapped宏测试 map_bh的b_state 字段,这个宏是个 BUFFER_FNS 宏,在 include/linux/buffer_head.h 中),并且页中第一块的 文件块号位于传递进来的 first_logical_block 参数和一个 buffer_head 最后一个块之间,则对 未映射的部分进行处理。由于我们在 mpage_readpage 函数中将 map_bh 的 b_state 字段清零 了的,而且传递进来的 first_logical_block 为 0,所以略过这段代码。 5.3.2 得到文件的逻辑块号 继续走,233 行,设置 map_bh 的 b_page 字段为当前 page。随后进入循环,对于页中的每一 块,调用 ext2 文件系统的 get_block 函数,作为参数传递 page 的 inode、相对于文件起始位 置的块索引 block_in_file、map_bh 进去,最后返回相对于磁盘分区开始位置的逻辑块号, 即相对于磁盘或分区开始位置的块索引,存放在结果参数 map_bh 的 b_blocknr 字段中。所 以这里我们重点关注 get_block 的原型,ext2_get_block 函数,来自 fs/ext2/inode.c: 547int ext2_get_block(struct inode *inode, sector_t iblock, struct buffer_head *bh_result, int create) 548{ 549 int err = -EIO; 550 int offsets[4]; 551 Indirect chain[4]; 552 Indirect *partial; 553 unsigned long goal; 554 int left; 555 int boundary = 0; 556 int depth = ext2_block_to_path(inode, iblock, offsets, &boundary); 557 558 if (depth == 0) 559 goto out; 560 561reread: 562 partial = ext2_get_branch(inode, depth, offsets, chain, &err); 563 564 /* Simplest case - block found, no allocation needed */ 565 if (!partial) { 566got_it: 567 map_bh(bh_result, inode->i_sb, le32_to_cpu(chain[depth-1].key)); 568 if (boundary) 569 set_buffer_boundary(bh_result); 570 /* Clean up and exit */ 571 partial = chain+depth-1; /* the whole chain */ 572 goto cleanup; 573 } 574 575 /* Next simple case - plain lookup or failed read of indirect block */ 576 if (!create || err == -EIO) { 577cleanup: 578 while (partial > chain) { 579 brelse(partial->bh); 580 partial--; 581 } 582out: 583 return err; 584 } 585 586 /* 587 * Indirect block might be removed by truncate while we were 588 * reading it. Handling of that case (forget what we've got and 589 * reread) is taken out of the main path. 590 */ 591 if (err == -EAGAIN) 592 goto changed; 593 594 goal = 0; 595 if (ext2_find_goal(inode, iblock, chain, partial, &goal) < 0) 596 goto changed; 597 598 left = (chain + depth) - partial; 599 err = ext2_alloc_branch(inode, left, goal, 600 offsets+(partial-chain), partial); 601 if (err) 602 goto cleanup; 603 604 if (ext2_use_xip(inode->i_sb)) { 605 /* 606 * we need to clear the block 607 */ 608 err = ext2_clear_xip_target (inode, 609 le32_to_cpu(chain[depth-1].key)); 610 if (err) 611 goto cleanup; 612 } 613 614 if (ext2_splice_branch(inode, iblock, chain, partial, left) < 0) 615 goto changed; 616 617 set_buffer_new(bh_result); 618 goto got_it; 619 620changed: 621 while (partial > chain) { 622 brelse(partial->bh); 623 partial--; 624 } 625 goto reread; 626} ext2_get_block 函数代码比较复杂,怎么吃透它呢?首先,请大家访问博客“ Ext2 数据块分 配” http://blog.csdn.net/yunsongice/archive/2010/08/18/5822495.aspx 补充一下数据块寻址的 预备知识;然后我在网上找了一个 ext2_get_block 函数调用层次图,如下: ext2_get_block()将对文件系统的逻辑块号转换为对块设备的逻辑块号,这种转换关系是由 ext2_inode 结构中 i_block[]数组描述的。 i_block[]的前 12 项为直接块索引表,第 13 项为间 接索引块指针,第 14 项为二重索引块指针,第 15 项为三重索引块指针。当文件长度不超过 12 个块时(一个块是 1024 字节,12 个块就是 12k),可通过直接块索引表直接定位目标块, 当文件长度超过 12 块并且剩余的部分不超过间接索引块索引数量时,就在间接索引块中定 位目标块,依次类推。 ext2_get_block 函数功能是从相对于文件开始位置的块索引转换为相对于磁盘分区开始位置 的逻辑块号,若对应逻辑块被删除,则重新分配得到它间接块路径。系统是以块索引查找逻 辑块的。例如,要找到第 100 个逻辑块对应的逻辑块,因为 256+12>100>12,所以要用到 一次间接块,在一次间接块中查找第 88 项,此项内容就是对应的逻辑块的地址。 首先 556 行,ext2_block_to_path 得到 block_in_file 位于直接块还是 n 次间接块。该函数只返 回四个可能的值:如果是直接块中,则返回 1;如果是间接块则返回 2;如果是二次间接则 返回 3;如果是三次间接则返回 4。 static int ext2_block_to_path(struct inode *inode, long i_block, int offsets[4], int *boundary) { //每块地址数=块大小 / 每个指针大小即 32 位,一般为 1kbyte/32bit=256 int ptrs = EXT2_ADDR_PER_BLOCK(inode->i_sb); int ptrs_bits = EXT2_ADDR_PER_BLOCK_BITS(inode->i_sb); //直接块数 direct_blocks =12 const long direct_blocks = EXT2_NDIR_BLOCKS, //一次间接块数 indirect_blocks =256 (256k 字节) indirect_blocks = ptrs, //二次间接块数 double_blocks =256*256 (65536k 字节,64MB 字节) double_blocks = (1 << (ptrs_bits * 2)); int n = 0; if (i_block < 0) { ext2_warning (inode->i_sb, "ext2_block_to_path", "block < 0"); } else if (i_block < direct_blocks) { offsets[n++] = i_block;//直接块 } else if ( (i_block -= direct_blocks) < indirect_blocks) { offsets[n++] = EXT2_IND_BLOCK;//一次间接块 offsets[n++] = i_block; } else if ((i_block -= indirect_blocks) < double_blocks) { offsets[n++] = EXT2_DIND_BLOCK;//二次间接块 offsets[n++] = i_block >> ptrs_bits;//一次间接块 offsets[n++] = i_block & (ptrs - 1);//直接块 } else if (((i_block -= double_blocks) >> (ptrs_bits * 2)) < ptrs) { offsets[n++] = EXT2_TIND_BLOCK;//三次间接块 offsets[n++] = i_block >> (ptrs_bits * 2);//二次间接块 offsets[n++] = (i_block >> ptrs_bits) & (ptrs - 1); //一次间接块 offsets[n++] = i_block & (ptrs - 1); //直接块 } else { ext2_warning (inode->i_sb, "ext2_block_to_path", "block > big"); } if (boundary) *boundary = (i_block & (ptrs - 1)) == (final - 1); return n; } ext2_block_to_path 的 返 回 值 保 存 在 ext2_get_block 函 数 的 内 部 变量 depth 中 , 然 后 ext2_get_block 执行 562 行的 ext2_get_branch 函数,从逻辑块中读取数据到 chain 的 buffer中。 函数的参数说明如下: inode: 文件 VFS 的索引节点 depth: 间接块深度(如 1 —— 一次间接块指针 ) offsets: 间接逻辑块的指针数组 chain: 存储读取逻辑块的数据 err: 存储错误标识 函数 ext2_get_branch 的功能是填充 Indirect 结构的数组,如果运行正常,则返回 NULL。 typedef struct { u32 *p; 索引块中索引项的地址 u32 key; 索引块中索引项的值 struct buffer_head *bh; 索引块所在的缓冲区 } Indirect; static inline void add_chain(Indirect *p, struct buffer_head *bh, __le32 *v) { p->key = *(p->p = v); p->bh = bh; } static Indirect *ext2_get_branch(struct inode *inode, int depth, int *offsets, Indirect chain[4], int *err) { struct super_block *sb = inode->i_sb; Indirect *p = chain; struct buffer_head *bh; //初始化 chain add_chain (chain, NULL, EXT2_I(inode)->i_data + *offsets); //depth 为间接块深度,如三次间接块深度为 4 while (--depth) { bh = sb_bread(sb, le32_to_cpu(p->key)); …… //读出逻辑块数据到 chain 中,p 为 chain 数组的指针 add_chain(++p, bh, (__le32*)bh->b_data + *++offsets); if (!p->key) goto no_block; } return NULL; ⋯⋯ } 这个函数比较绕脑子,我们还是举个例子吧。假如 block_in_file 的磁盘索引节点逻辑块号较 大,比方说 16741216,其大于 256*256,小于 256*256*256,那么肯定是 3 次间接块。所以 在 ext2_block_to_path 函数中,offsets[0]=EXT2_TIND_BLOCK,也就是 15,作为三次间接 块存放二次间接块的逻辑块号; offsets[1]= 16741216 >> (8*2),等于 255,也就是作为二次 间接块存放一次间接块的逻辑块号; offsets[2]=16741216 >> 8 & (256 -1)等于 65395,作为一 次间接块存放直接寻址块的逻辑块号; offsets[3]=16741216 & (256 -1)等于 16741216,就是 直接块,也就是我们的 block_in_file 的逻辑块号。 那么进入 ext2_get_branch 函数以后,首先调用 add_chain 函数初始化作为参数传递进来的 Indirect 变量空数组 chain,有 4 个元素,仅初始化第一个 Indirect 元素的 p 字段为对应 ext2_inode_info 结构的 i_data + *offsets,就是 i_data[15]的值。随后进入 while (--depth)循环, depth 这里等于 4,首先通过 sb_bread 读取 i_data[15]对应的块到块设备页高速缓存中,然后 调用 add_chain 函数将 i_data[15]对应的块存放的第 255 项的内容,也就是第二次间接寻址的 逻辑块号加入到 chain[1]的 p 中。这样第二次、第三次循环后, chain[2]和 chain[3]就分别对 应一次间接块地址和直接地址的逻辑地址,就存放在 Indirect 结构的 p 字段中,而对应具体 的逻辑块号就存在他们的 key 字段中,同时保留对应缓冲块的 buffer_head 结构。眼睛看晕 了就看看我画的关系图: 如图,经过 ext2_block_to_path 和 ext2_get_branch 这么一折腾,ext2_get_block 中的内部变量 chain[4]就被赋上值了,其中 chain[3]的 key字段就存放了相对于文件开始位置的 block_in_file (也就是我们例子中的 16741216)对应的逻辑块号。而 partial 内部变量是 NULL。 随后跳到 ext2_get_block的 567 行,调用 map_bh函数将 bh_result这个 buffer_head的 b_bdev、 b_blocknr 和 b_size 字段分别设置为该文件超级块的设备, chain[3]的 key 字段( block_in_file 对应的逻辑块号)和块大小 1024。然后释放 brelse 刚才 chain 数组中用了临时存放块的高速 缓存并返回。 5.3.3 普通文件的 readpage 方法 回到 do_mpage_readpage 中,从 mpage_readpage 传进来的 buffer_head 类型的 map_bh 参数 的 b_bdev、b_blocknr 和 b_size 字段就被赋上值了,然后 245~271 行对这个 map_bh 进行一 系列的检查,检查可能发生的异常条件。具体有这几种情况:当一些块在磁盘上不相邻时, 或某块落如“文件洞”内时,或一个块缓冲区已经由 get_block 函数写入时。那么跳到 confused 标号处,用一次读一块的方式读该页。有关“文件的洞”的相关内容,请查阅 ULK-3 的相 关章节。 273-282 行,很重要,为内部变量 blocks[MAX_BUF_PER_PAGE]赋值。由于一页面包含 4 个块,所以 blocks[0]~ blocks[3]就存放着磁盘上是相邻连续 4 个块的逻辑块号。 page_block 内部变量也从 0 变成 3 了。 如果函数运行至 286 行,说明页中的所有块在磁盘上是相邻的。然而,它可能是文件中的最 后一页,因此页中的一些块可能在磁盘上没有映像。如果这样的话, 288 行将页中相应的块 缓冲区填上 0;如果不是这样, 298 行将页描述符的标志 PG_mappedtodisk 置位。 继续走,前面看到,我们传递进来的 bio 是 NULL 的,所以来到 308 行的条件分支。 309 行 调用调用 mpage_alloc 触发 bio_alloc()函数分配包含单一段的一个新 bio 描述符,并且分别用 块设备描述符地址和页中第一个块的逻辑块号 blocks[0]来初始化 bi_bdev 字段和 bi_sector 字段。这两个信息已在上面的 get_block 函数中得到。 通常,bio 结构是由 slab 分配器分配的,但是,当内存不足时,内核也会使用一个备用的 bio 小内存池。内核也为 bio_vec 结构分配内存池——毕竟,分配一个 bio 结构而不能分配其中 的段描述符也是没有什么意义的。 bio_alloc 就通过 bio_alloc_bioset 函数在 bs->bvec_pool 中 给他的 bio_vec 分配。相应地,bio_put()函数减少 bio 中引用计数器 (bi_cnt)的值,如果该值 等于 0,则释放 bio 结构以及相关的 bio_vec 结构。 这个 bio 的 bdev 来自 buffer_head 的 b_bdev 字段;bi_destructor 被设置为 bio_fs_destructor 函数,作为当 bio 上的 I/O 操作完成时所执行的完成程序的地址; bi_io_vec 通过 bvec_alloc_bs 函数初始化成数组,表示若干个 bio 待传输的段;同时 bi_flags 和 bi_max_vecs 字段也被设 置了;bi_sector 是 blocks[0]左移(blkbits - 9)位,得到该块对应磁盘扇区号,因为 BIO 更关心 的是扇区号而不是块号,后面谈通用块处理的时候会重点讨论它。另外 min_t(int, nr_pages, bio_get_nr_vecs(bdev))表示这个 bio 中的 iovec 个数,后面讲到通用块层的时候再来详细说这 些东西,很重要的。 最后,bio 初始化成功了,do_mpage_readpage 成功返回这个 bio,mpage_readpage 就调用 mpage_bio_submit(READ, bio)处理请求。 static struct bio *mpage_bio_submit(int rw, struct bio *bio) { bio->bi_end_io = mpage_end_io_read; if (rw == WRITE) bio->bi_end_io = mpage_end_io_write; submit_bio(rw, bio); return NULL; } 我们看到 mpage_bio_submit 函数将 bio 的 bi_end_io 字段赋值成 mpage_end_io_read 函数, 随后调用 submit_bio 函数处理该请求: void submit_bio(int rw, struct bio *bio) { int count = bio_sectors(bio); BIO_BUG_ON(!bio->bi_size); BIO_BUG_ON(!bio->bi_io_vec); bio->bi_rw |= rw; if (rw & WRITE) count_vm_events(PGPGOUT, count); else count_vm_events(PGPGIN, count); if (unlikely(block_dump)) { char b[BDEVNAME_SIZE]; printk(KERN_DEBUG "%s(%d): %s block %Lu on %s\n", current->comm, current->pid, (rw & WRITE) ? "WRITE" : "READ", (unsigned long long)bio->bi_sector, bdevname(bio->bi_bdev,b)); } generic_make_request(bio); } 函数最终将请求传递给函数 generic_make_request ,并由 generic_make_request 函数将请求 提交给通用块层处理,到此为止,页高速缓存层的处理结束。 5.3.4 块设备文件的 readpage 方法 看到 do_mpage_readpage 函数的 confused 标号处,如果页中含有的块在磁盘上不连续,则函 数跳到这里。如果页是最新的,调用 block_read_full_page 函数,这里牵涉到块设备文件的 readpage 方法。 在前面 ext2_read_inode 函数中,我们看到 init_special_inode 函数用来建立设备的索引节点。 该函数把索引节点对象的 i_rdev 字段初始化为设备文件的主设备号和次设备号,而把索引节点对象的 i_fop 字段设置为 def_blk_fops 或者 def_chr_fops 文件操作表的地址(根据设备 文件的类型);把索引节点的 i_data 对应的 a_ops 字段设置为 def_blk_aops(字符驱动不需要 页高速缓存)操作表地址。因此, open()系统调用的服务例程也调用 dentry_open()函数,后 者分配一个新的文件对象并把其 f_op 字段设置为 i_fop 中存放的地址,即再一次指向 def_blk_fops 或 def_chr_fops 的地址。正是这两个表的引入,才使得在设备文件上所发出的 任何系统调用都将激活设备驱动程序的函数而不是基本文件系统的函数。 我们这里是块设备文件,所以关注的是 def_blk_fops 全局变量,来自 fs/block_dev.c: const struct file_operations def_blk_fops = { .open = blkdev_open, .release = blkdev_close, .llseek = block_llseek, .read = generic_file_read, .write = blkdev_file_write, .aio_read = generic_file_aio_read, .aio_write = blkdev_file_aio_write, .mmap = generic_file_mmap, .fsync = block_fsync, .unlocked_ioctl = block_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = compat_blkdev_ioctl, #endif .readv = generic_file_readv, .writev = generic_file_write_nolock, .sendfile = generic_file_sendfile, .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, }; 块设备文件对应页高速缓存的函数来自 def_blk_aops 全局变量,也位于同一文件中: const struct address_space_operations def_blk_aops = { .readpage = blkdev_readpage, .writepage = blkdev_writepage, .sync_page = block_sync_page, .prepare_write = blkdev_prepare_write, .commit_write = blkdev_commit_write, .writepages = generic_writepages, .direct_IO = blkdev_direct_IO, }; 在 bdev 特殊文件系统中,块设备使用 address_space 对象,该对象存放在对应块设备索引节 点的 i_data 字段。不像普通文件(在 address_space 对象中它的 readpage 方法依赖于文件所属的文件系统的类型),块设备文件的 readpage 方法总是相同的。它是由 blkdev_readpage() 函数实现的,该函数调用 block_read_full_page(): int blkdev_readpage(struct file * file, struct * page page) { return block_read_full_page(page, blkdev_get_block); } 正如你看到的,这个函数又是一个封装函数,这里是 block_read_full_page()函数的封装函数。 block_read_full_page 函数,来自 fs/buffer.c: 2063int block_read_full_page(struct page *page, get_block_t *get_block) 2064{ 2065 struct inode *inode = page->mapping->host; 2066 sector_t iblock, lblock; 2067 struct buffer_head *bh, *head, *arr[MAX_BUF_PER_PAGE]; 2068 unsigned int blocksize; 2069 int nr, i; 2070 int fully_mapped = 1; 2071 2072 BUG_ON(!PageLocked(page)); 2073 blocksize = 1 << inode->i_blkbits; 2074 if (!page_has_buffers(page)) 2075 create_empty_buffers(page, blocksize, 0); 2076 head = page_buffers(page); 2077 2078 iblock = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits); 2079 lblock = (i_size_read(inode)+blocksize-1) >> inode->i_blkbits; 2080 bh = head; 2081 nr = 0; 2082 i = 0; 2083 2084 do { 2085 if (buffer_uptodate(bh)) 2086 continue; 2087 2088 if (!buffer_mapped(bh)) { 2089 int err = 0; 2090 2091 fully_mapped = 0; 2092 if (iblock < lblock) { 2093 WARN_ON(bh->b_size != blocksize); 2094 err = get_block(inode, iblock, bh, 0); 2095 if (err) 2096 SetPageError(page); 2097 } 2098 if (!buffer_mapped(bh)) { 2099 void *kaddr = kmap_atomic(page, KM_USER0); 2100 memset(kaddr + i * blocksize, 0, blocksize); 2101 flush_dcache_page(page); 2102 kunmap_atomic(kaddr, KM_USER0); 2103 if (!err) 2104 set_buffer_uptodate(bh); 2105 continue; 2106 } 2107 /* 2108 * get_block() might have updated the buffer 2109 * synchronously 2110 */ 2111 if (buffer_uptodate(bh)) 2112 continue; 2113 } 2114 arr[nr++] = bh; 2115 } while (i++, iblock++, (bh = bh->b_this_page) != head); 2116 2117 if (fully_mapped) 2118 SetPageMappedToDisk(page); 2119 2120 if (!nr) { 2121 /* 2122 * All buffers are uptodate - we can set the page uptodate 2123 * as well. But not if get_block() returned an error. 2124 */ 2125 if (!PageError(page)) 2126 SetPageUptodate(page); 2127 unlock_page(page); 2128 return 0; 2129 } 2130 2131 /* Stage two: lock the buffers */ 2132 for (i = 0; i < nr; i++) { 2133 bh = arr[i]; 2134 lock_buffer(bh); 2135 mark_buffer_async_read(bh); 2136 } 2137 2138 /* 2139 * Stage 3: start the IO. Check for uptodateness 2140 * inside the buffer lock in case another process reading 2141 * the underlying blockdev brought it uptodate (the sct fix). 2142 */ 2143 for (i = 0; i < nr; i++) { 2144 bh = arr[i]; 2145 if (buffer_uptodate(bh)) 2146 end_buffer_async_read(bh, 1); 2147 else 2148 submit_bh(READ, bh); 2149 } 2150 return 0; 2151} block_read_full_page 函数的第二个参数也指向一个函数,该函数把相对于文件开始处的文件 块号转换为相对于块设备开始处的逻辑块号。不过,对于块设备文件来说,这两个数是一致 的;因此,blkdev_get_block()函数就比较简单了,来自: 109blkdev_get_block(struct inode *inode, sector_t iblock, 110 struct buffer_head *bh, int create) 111{ 112 if (iblock >= max_block(I_BDEV(inode))) { 113 if (create) 114 return -EIO; 115 116 /* 117 * for reads, we're just trying to fill a partial page. 118 * return a hole, they will have to call get_block again 119 * before they can fill it, and they will get -EIO at that 120 * time 121 */ 122 return 0; 123 } 124 bh->b_bdev = I_BDEV(inode); 125 bh->b_blocknr = iblock; 126 set_buffer_mapped(bh); 127 return 0; 128} blkdev_get_block 函数 112 行检查页中第一个块的块号是否超过块设备的最后一块的索引值 (通过 max_block 函数将存放在 bdev->bd_inode->i_size 中的块设备大小除以存放在 bdev->bd_block_size 中的块大小得到该索引值; bdev 指向块设备描述符)。如果超过,那么 对于写操作它返-EIO,而对于读操作它返回 0。(超出块设备读也是不允许的,但不返回错 误代码,内核可以对块设备的最后数据试着发出读请求,而得到的缓冲区页只被部分映射)。 124 行设置缓冲区首部的 b_dev 字段为 b_dev。125 行设置缓冲区首部的 b_blocknr 字段为文件块号,它将被作为参数传给本函数。最后 126 行把缓冲区首部的 BH_Mapped 标志置位, 以表明缓冲区首部的 b_dev 和 b_blocknr 字段是有效的。 回到函数 block_read_full_page(),这个函数以一次读一块的方式读一页数据。正如我们已看 到的当读块设备文件或磁盘上块不相邻的普通文件时都使用该函数。它执行如下步骤: 首先 2074 行通过检查页描述符的标志 PG_private,如果置位,则说明该页与描述组成该页 的块的缓冲区首部链表相关;否则,调用 create_empty_buffers()来为该页所含所有块缓冲区 分配缓冲区首部。页中第一个缓冲区的缓冲区首部地址存放在 page->private 字段中。每个 缓冲区首部的 b_this_page 字段指向该页中下一个缓冲区的缓冲区首部: void create_empty_buffers(struct page *page, unsigned long blocksize, unsigned long b_state) { struct buffer_head *bh, *head, *tail; head = alloc_page_buffers(page, blocksize, 1); bh = head; do { bh->b_state |= b_state; tail = bh; bh = bh->b_this_page; } while (bh); tail->b_this_page = head; spin_lock(&page->mapping->private_lock); if (PageUptodate(page) || PageDirty(page)) { bh = head; do { if (PageDirty(page)) set_buffer_dirty(bh); if (PageUptodate(page)) set_buffer_uptodate(bh); bh = bh->b_this_page; } while (bh != head); } attach_page_buffers(page, head); spin_unlock(&page->mapping->private_lock); } 2078 行,从相对于页的文件偏移量( page->index 字段)计算出页中第一块的文件块号。 2084 行进入一个循环,对该页中每个缓冲区的缓冲区首部,执行如下子步骤: a) 2085 行如果标志 BH_Uptodate 置位,则跳过该缓冲区继续处理该页的下一个缓冲区。 b) 2088 行如果标志 BH_Mapped 未置位,并且该块未超出文件尾,则调用作为参数传递进来的 blkdev_get_block 函数。对于块设备文件,不同的是 blkdev_get_block 函数把文件块号当 作逻辑块号。不管是普通文件还是块设备文件,函数都将逻辑块号存放在相应缓冲区首部的 b_blocknr 字段中,并将标志 BH_Mapped 置位(访问普通文件时,如果一个数据块处于“文 件洞”中,get_block 函数就可能找不到这个块。此时,函数用 0 填充这个块缓冲区并设置 缓冲区首部的 BH_Uptodate 标志。)。 c) 2098 行再检查标志 BH_Uptodate,因为依赖于文件系统的 get_block 函数可能已触发块 I/O 操作而更新了缓冲区。如果 BH_Uptodate 置位,则继续处理该页的下一个缓冲区。 d) 2114 行将缓冲区首部的地址存放在局部数组 arr 中,继续该页的下一个缓冲区。 2117 行假如上一步中没遇到“文件洞”,则 2118 行将该页的标志 PG_mappedtodisk 置位。 现在局部变量 arr 中存放了一些缓冲区首部的地址,与其对应的缓冲区的内容不是最新的。 如果数组,即 2120 行判断 nr 为 0 为空,那么页中的所有缓冲区都是有效的,因此,该函数 设置页描述符的 PG_uptodate 标志,调用 unlock_page()对该页解锁并返回。 局部数组 arr 非空。对数组中的每个缓冲区首部, block_read_full_page()再执行下列子步骤: a) 2134 行将 BH_Lock 标志置位。该标志一旦置位,函数将一直等到该缓冲区释放。 b) 2135 行将缓冲区首部的 b_end_io 字段设为 end_buffer_async_read()函数的地址(见下面), 并将缓冲区首部的 BH_Async_Read 标志置位。 2143-2149 行对局部数组 arr 中的每个缓冲区首部调用 submit_bh(),将操作类型设为 READ。 就像我们在前面看到的那样,该函数触发了相应块的 I/O 数据传输。 函数 end_buffer_async_read()是缓冲区首部的完成方法。对块缓冲区的 I/O 数据传输一结束, 它 就执 行。 假定 没有 I/O 错误,函数将缓冲区首部的 BH_Uptodate 标 志 置 位而 将 BH_Async_Read 标志清 0。那么,函数就得到包含块缓冲区的缓冲区页描述符(它的地址存 放在缓冲区首部的 b_page 字段中),同时检查是否页中所有块是最新的;如果是,函数将该 页的 PG_uptodate 标志置位并调用 unlock_page(): static void end_buffer_async_read(struct buffer_head *bh, int uptodate) { unsigned long flags; struct buffer_head *first; struct buffer_head *tmp; struct page *page; int page_uptodate = 1; BUG_ON(!buffer_async_read(bh)); page = bh->b_page; if (uptodate) { set_buffer_uptodate(bh); } else { clear_buffer_uptodate(bh); if (printk_ratelimit()) buffer_io_error(bh); SetPageError(page); } /* * Be _very_ careful from here on. Bad things can happen if * two buffer heads end IO at almost the same time and both * decide that the page is now completely done. */ first = page_buffers(page); local_irq_save(flags); bit_spin_lock(BH_Uptodate_Lock, &first->b_state); clear_buffer_async_read(bh); unlock_buffer(bh); tmp = bh; do { if (!buffer_uptodate(tmp)) page_uptodate = 0; if (buffer_async_read(tmp)) { BUG_ON(!buffer_locked(tmp)); goto still_busy; } tmp = tmp->b_this_page; } while (tmp != bh); bit_spin_unlock(BH_Uptodate_Lock, &first->b_state); local_irq_restore(flags); /* * If none of the buffers had errors and they are all * uptodate then we can set the page uptodate. */ if (page_uptodate && !PageError(page)) SetPageUptodate(page); unlock_page(page); return; still_busy: bit_spin_unlock(BH_Uptodate_Lock, &first->b_state); local_irq_restore(flags); return; } 5.3.5 文件的预读 文件预读的内容我是把 ULK-3 一书中的内容全盘拷贝下来了。如果大家感兴趣可以根据源 代码深入了解一下。 很多磁盘的访问都是顺序的。普通文件以相邻扇区成组存放在磁盘上,因此很少移动磁头就 可以快速检索到文件。当程序读或拷贝一个文件时,它通常从第一个字节到最后一个字节顺 序地访问文件。因此,在处理进程对同一文件的一系列读请求时,可以从磁盘上很多相邻的 扇区读取。 预读(read-ahead)是一种技术,这种技术在于在实际请求前读普通文件或块设备文件的几 个相邻的数据页。在大多数情况下,预读能极大地提高磁盘的性能,因为预读使磁盘控制器 处理较少的命令,其中的每条命令都涉及一大组相邻的扇区。此外,预读还能提高系统的响 应能力。顺序读取文件的进程通常不需要等待请求的数据,因为请求的数据已经在 RAM 中 了。 但是,预读对于随机访问的文件是没有用的;在这种情况下,预读反而是有害的,因为它用 无用的信息浪费了页高速缓存的空间。因此,当内核确定出最近所进行的 I/O 访问与前一次 I/O 访问不是顺序的时就减少或停止预读。 文件的预读需要更复杂的算法,这是由于以下几个原因: l 由于数据是逐页进行读取的,因此预读算法不必考虑页内偏移量,只要考虑所访问的页 在文件内部的位置就可以了。 l 只要进程持续地顺序访问一个文件,预读就会逐渐增加。 l 当前的访问与上一次访问不是顺序的时(随机访问),预读就会逐渐减少乃至禁止。 l 当一个进程重复地访问同一页(即只使用文件的很小一部分)时,或者当几乎所有的页 都已在页高速缓存内时,预读就必须停止。 l 低级 I/O 设备驱动程序必须在合适的时候激活,这样当将来进程需要时,页已传输完毕。 如果请求的第一页紧跟上次访问所请求的最后一页,那么相对于上次的文件访问,内核把文 件的这次访问看作是顺序的。 当访问给定文件时,预读算法使用两个页面集,各自对应文件的一个连续区域。这两个页面 集分别叫做当前窗( current window)和预读窗(ahead window)。 当前窗内的页是进程请求的页和内核预读的页,且位于页高速缓存内(当前窗内的页不必是 最新的,因为 I/O 数据传输仍可能在运行中)。当前窗包含进程顺序访问的最后一页,且可 能有内核预读但进程未请求的页。 预读窗内的页紧接着当前窗内的页,它们是内核正在预读的页。预读窗内的页都不是进程请 求的,但内核假定进程会迟早请求。 当内核认为是顺序访问而且第一页在当前窗内时,它就检查是否建立了预读窗。如果没有, 内核创建一个预读窗并触发相应页的读操作。理想情况下,进程继续从当前窗请求页,同时 预读窗的页则正在传送。当进程请求的页在预读窗,那么预读窗就成为当前窗。 预读算法使用的主要数据结构是 file_ra_state 描述符,它的字段见表所示:每个文件对象在 它的 f_ra 字段中存放这样的一个描述符。 类型 字段 说明 unsigned long start 当前窗内第一页的索引 unsigned long size 当前窗内的页数(当临时禁止预读时为 -1,0 表示当前窗空) unsigned long flags 控制预读的一些标志 unsigned long cache_hit 连续高速缓存命中数(进程请求的页同时又在页高速缓存内) unsigned long prev_page 进程请求的最后一页的索引 unsigned long ahead_start 预读窗内第一页的索引 unsigned long ahead_size 预读窗的页数( 0 表示预读窗口空) unsigned long ra_pages 预读窗的最大页数( 0 表示预读窗永久禁止) unsigned long mmap_hit 预读命中计数器(用于内存映射文件) unsigned long mmap_miss 预读失败计数器(用干内存映射文件) 当一个文件被打开时,在它的 file_ra_state 描述符中,除了 prev_page 和 ra_pages 这两个字 段,其他的所有字段都置为 0。prev_page 字段用来存放进程在上一次读操作中所请求页的 最后一页的索引。它的初值是 -1。ra_pages 字段表示当前窗的最大页数,即对该文件允许的 最大预读量。该字段的初始值(缺省值)存放在该文件所在块设备的 backing_dev_info 描述 符中。一个应用可以修改一个打开文件的 ra_pages 字段从而调整预读算法;具体的实现方 法是调用 posix_fadvise()系统调用,并传给它命令 POSIX_FADV_NORMAL(设最大预读量 为缺省值,通常是 32 页)、 POSIX_FADV_SEQUENTIAL(设最大预读量为缺省值的两倍) 和 POSIX_FADV_RANDOM(最大预读量为 0,从而永久禁止预读)。 flags 字段内有两个重要的字段 RA_FLAG_MISS 和 RA_FLAG_INCACHE。如果已被预读的 页不在页高速缓存内(可能的原因是内核为了释放内存而加以收回了),则第一个标志置位, 这时候下一个要创建的预读窗大小将被缩小。当内核确定进程请求的最后 256 页都在页高速 缓存内时(连续高速缓存命中数存放在 ra->cache_hit 字段中),第二个标志置位,这时内核 认为所有的页都已在页高速缓存内,进而关闭预读。 如何时执行预读算法?这有下列几种情形: l 当内核用用户态请求来读文件数据的页时。这一事件触发 page_cach_readahead()函数的 调用。 l 当内核为文件内存映射分配一页时。 l 当用户态应用执行 readahead()系统调用时,它会对某个文件描述符显式触发某预读活动。 l 当用户态应用使用 POSIX_FADV_NOREUSE 或 POSIX_FADV_WILLNEED 命令执行 posix_fadvise()系统调用时,它会通知内核,某个范围的文件页不久将要被访问。 l 当用户态应用使用 MADV_WILLNEED 命令执行 madvise()系统调用时,它会通知内核, 某个文件内存映射区域中的给定范围的文件页不久将要被访问。 page_cache_readahead()函数 page_cache_readahead()函数处理没有被特殊系统调用显式触发的所有预读操作。它填写当前 窗和预读窗,根据预读命中数更新当前窗和预读窗的大小,也就是根据过去对文件访问预读 策略的成功程度来调整。 当内核必须满足对某个文件一页或多页的读请求时,函数就被调用,该函数有下面五个参数: mapping:描述页所有者的 address_space 对象指针 ra:包含该页的文件 file_ra_state 描述符指针 file:文件对象地址 offset:文件内页的偏移量 req_size:要完成当前读操作还需要读的页数(实际上,如果读操作要读的页数大于预读窗 的最大尺寸,就会多次调用 page_cache_readahead()函数。因此, req_size 参数可能比完成读 操作还需要读的页数小) 下图是 page_cache_readahead()的流程图。该函数基本上作用于 file_ra_state 描述符的字段, 因此,尽管流程图中的行为描述不很正规,你还是能很容易地确定函数执行的实际步骤。例 如,为了检查请求页是否与刚读的页相同,函数检查 ra->prev_page 字段的值和 offset 参数 的值是否一致(见前面的表)。 当进程第一次访问一个文件,并且其第一个请求页是文件中偏移量为 0 的页时,函数假定进 程要进行顺序访问。那么,函数从第一页创建一个新的当前窗。初始当前窗的长度(总是为 2 的幂)与进程第一个读操作所请求的页数有一定的联系。请求页数越大,当前窗越大,一 直到最大值,最大值存放在 ra->ra_pages 字段。反之,当进程第一次访问文件,但其第一个 请求页在文件中的偏移量不为 0 时,函数假定进程不是执行顺序读。那么,函数暂时禁止预读(ra->size 字段设为-1)。但是当预读暂时被禁止而函数又认为需要顺序访问时,将建立一 个新的当前窗。 如果预读窗不存在,一旦函数认为在当前窗内进程执行了顺序读,则预读窗将被建立。预读 窗总是从当前窗的最后一页开始。但它的长度与当前窗的长度相关:如果 RA_FLAG_MISS 标志置位,则预读窗长度是当前窗长度减 2,小于 4 时设为 4;否则,预读窗长度是当前窗 长度的 4 倍或 2 倍。如果进程继续顺序访问文件,最终预读窗成为新的当前窗,新的预读窗 被创建。这样,随着进程顺序地读文件,预读会大大地增强。 一旦函数认识到对文件的访问相对于上一次不是顺序的,当前窗与预读窗就被清空,预读被 暂时禁止。当进程的读操作相对于上一次文件访问为顺序时,预读将重新开始。 每次 page_cache_readahead()创建一个新窗,它就开始对所包含页的读操作。为了读一大组 页,函数 page_cache_readahead()调用 blockable_page_cache_readahead()。为减少内核开销, 后面这个函数采用下面灵活的方法: l 如果服务于块设备的请求队列是读拥塞的,就不进行读操作。 l 将要读的页与页高速缓存进行比较,如果该页已在页高速缓存内,跳过即可。 l 在从磁盘进行读之前,读请求所需的全部页框是一次性分配的。如果不能一次性得到全 部页框,预读操作就只在可以得到的页上进行。而且把预读推迟至所有页框都得到时再 进行并没有多大意义。 l 只要可能,通过使用多段 bio 描述符向通用块层发出读操作。这通过 address_space 对象 专用的 readpages 方法实现(假如已定义);如果没有定义,就通过反复调用 readpage 方法来实现。 readpage 方法在前面“从文件中读取数据”一节中对于单段情形有详细描 述,但稍作修改就可以很容易地将它用于多段情形。 handle_ra_miss()函数 在某些情况下,预读策略似乎不是十分有效,内核就必须修正预读参数。图中展示了两种情 形:请求页在当前窗或预读窗表明它已经被预先读入了;或者还没有,则调用 blockable_page_cache_readahead()来读入。在这两种情形下,函数 do_generic_file_read()应该 在第 4d 步中就在页高速缓存中找到了该页,如果没有,就表示该页框已被收回算法从高速 缓存中删除。在这种情形下, do_generic_file_read()调用 handle_ra_miss()函数,这个函数会 通过将 RA_FLAG_MISS 标志置位与 RA_FLAG_INCACHE 标志清 0 来调整预读算法。 5.4 通用块层的处理 在前面普通文件和块设备文件的 readpage 方法中介绍到,对于一个普通文件,要读取相对 于文件头的 ppos 处开始 size 个连续的字节,就必须计算成对应的页面缓存在内存中;如果 存在不连续的情况,如“文件的洞”,就调用块设备的 readpage 方法建立块设备页高速缓存 存放不来连续的块。 不管怎样,最终都将封装一个 bio 结构,并把请求传递给函数 generic_make_request ,并由 generic_make_request 函数将请求提交给通用块层处理。在进入通用块层之前,先把块设备 的一些预备知识清理了。 5.4.1 块设备的基础知识 块设备主要是指那些磁盘、 U 盘等一些用来存储的设备,他们的主要特点是, CPU 和总线 读写数据所花时间与磁盘硬件的速度不匹配。块设备的平均访问时间很长。每个操作都需要 几个毫秒才能完成,主要是因为磁盘控制器必须在磁盘表面将磁头移动到记录数据的确切位 置。但是,当磁头到达正确位置时,数据传送就可以稳定在每秒几十 MB 的速率。 块设备驱动程序上的每个操作都涉及很多内核组件,我们来看 ULK-3 的一幅经典的图: 我们前面所有的内容都在讲一个进程在某个磁盘文件上发出一个 read()系统调用——其实 write 请求本质上采用同样的方式。下面咱们就站在设备管理的高度对通用块层之前的步骤 进行一下总结和回顾: 1. read()系统调用的服务例程调用一个适当的 VFS 函数,也就是前面的 generic_file_read, 将文件描述符和文件内的偏移量传递给它。 2. generic_file_read 函数确定所请求的数据是否已经存在于页高速缓存中( address_space 对应的基树中)。有时候没有必要访问磁盘上的数据,因为内核将大多数最近从块设备读出 或写人其中的数据保存在页高速缓存中。 3. 我们假设内核需要从块设备读数据,那么它就必须确定数据的物理位置。为了做到这点, 内核依赖映射层( mapping layer),主要执行下面两步: a) 内核确定该文件所在文件系统的块大小,并根据文件块的大小计算所请求数据的长 度。本质上,文件被看作拆分成许多块,因此调用 do_generic_mapping_read 确定请求 数据所在的块号(文件开始位置的相对索引)。 b) 接下来,映射层调用一个具体文件系统的函数—— ext2_get_block,它访问缓存在内 存中的文件的磁盘索引节点,然后根据逻辑块号确定所请求数据在磁盘上的位置。 好了,现在我们已经走到这一步了,现在内核可以对块设备发出读请求。内核利用通用块层 (generic block lnyer)启动 I/O 操作函数 generic_make_request 来传送所请求的数据。一般而 言,每个 I/O 操作只针对磁盘上一组连续的块。由于请求的数据不必位于相邻的块中,所以 通用块层可能启动几次 I/O 操作。每次 I/O 操作是由一个“块 I/O”(简称“ bio”)结构描述, 它收集底层组件需要的所有信息以满足所发出的请求。 通用块层为所有的块设备提供了一个抽象视图,因而隐藏了硬件块设备间的差异性。几乎所 有的块设备都是磁盘,所以通用块层也提供了一些通用数据结构来描述“磁盘”或“磁盘分 区”。 通用块层下面的“ I/O 调度程序”根据预先定义的内核策略将待处理的 I/O 数据传送请求进 行归类。调度程序的作用是把物理介质上相邻的数据请求聚集在一起。我们将在后面的“ I/O 调度程序”一节中介绍调度程序。 最后,块设备驱动程序向磁盘控制器的硬件接口发送适当的命令,从而进行实际的数据传送。 我们将在后面的“块设备底层驱动程序”一节介绍通用块设备驱动程序的总体组织结构。 我们看到,块设备中的数据存储涉及了许多内核组件;每个组件采用不同长度的块来管理磁 盘数据: l 硬件块设备控制器采用称为“扇区”( sector)的固定长度的块来传送数据,这个长度对 于大多数磁盘都是 512 个字节,特别是 SCSI 硬盘、U 盘和光盘。因此, I/O 调度程序 和块设备驱动程序必须管理数据扇区。 l 虚拟文件系统、映射层和文件系统将磁盘数据存放在称为“块”( block)的逻辑单元中。 一个块对应文件系统中一个最小的磁盘存储单元。 l 块设备驱动程序应该能够处理数据的“段”( segment):一个段就是一个内存页或内存 页的一部分, 它们包含磁盘上物理相邻的数据块 。 l 磁盘高速缓存作用于磁盘数据的“页”( page)上,每页正好装在一个页框中。 通用块层将所有的上层和下层的组件组合在一起,因此它了解数据的扇区、块、段以及页。 即使有许多不同的数据块,它们通常也是共享相同的物理 RAM 单元。 我们看到上图就显示了一个页(说死了, 4096 字节)的构造。上层内核组件将页看成是由 4 个 1024 字节组成的块缓冲区。 块设备驱动程序正在传送页中的后 3 个块,因此这 3 块被插 人到涵盖了后 3072 字节的段中。 硬盘控制器将该段看成是由 6 个 512 字节的扇区组成。 从本节开始我们就要往块设备的下层内核组件走了:通用块层、 I/O 调度程序以及块设备驱 动程序,因此我们将注意力集中在扇区、块和段上。 扇区 为了达到可接受的性能,硬盘和类似的设备快速传送几个相邻字节的数据。块设备的每次数 据传送操作都作用于一组称为扇区的相邻字节。在下面的讨论中,我们假定字节按相邻的方 式记录在磁盘表面,这样一次搜索操作就可以访问到它们。尽管磁盘的物理构造很复杂,但 是硬盘控制器接收到的命令将磁盘看成一大组扇区。 在大部分磁盘设备中,扇区的大小是 512 字节,但是一些设备使用更大的扇区( 1024 和 2048 字节)。注意,应该把扇区作为数据传送的基本单元;不允许传送少于一个扇区的数据,尽 管大部分磁盘设备都可以同时传送几个相邻的扇区。 在 Linux 中,扇区大小按惯例都设为 512 字节;如果一个块设备使用更大的扇区,那么相应 的底层块设备驱动程序将做些必要的变换。因此,对存放在块设备中的一组数据是通过它们 在磁盘上的位置来标识,即其首个 512 字节扇区的下标以及扇区的数目。扇区的下标存放在 类型为 sector_t 的 32 位或 64 位的变量中。 块 扇区是硬件设备传送数据的基本单位,而块是 VFS 和文件系统传送数据的基本单位。 内核 访问一个文件的内容时,它必须首先从磁盘上读文件的磁盘索引节点所在的块。该块对应磁盘上一个或多个相邻的扇区,而 VFS 将其看成是一个单一的数据单元。 块设备的块大小不是唯一的。创建一个磁盘文件系统时,管理员可以选择合适的块大小。因 此,同一个磁盘上的几个分区可能使用不同的块大小。 每个块都需要自己的块缓冲区,它是内核用来存放块内容的 RAM 内存区。当内核从磁盘读 出一个块时,就用从硬件设备中所获得的值来填充相应的块缓冲区;同样,当内核向磁盘中 写入一个块时,就用相关块缓冲区的实际值来更新硬件设备上相应的一组相邻字节。块缓冲 区的大小通常要与相应块的大小相匹配。 缓冲区首部是一个与每个缓冲区相关的 buffer_head 类型的描述符。它包含内核处理缓冲区 需要了解的所有信息;因此,在对每个缓冲区进行操作之前,内核都要首先检查其缓冲区首 部。其中,b_page 字段存放的是块缓冲区所在页框的页描述符地址。如果页框位于高端内 存中,那么 b_data 字段存放页中块缓冲区的偏移量;否则, b_data 存放块缓冲区本身的起 始线性地址。 b_blocknr 字段存放的是逻辑块号(例如磁盘分区中的块索引)。最后, b_bdev 字段标识使用缓冲区首部的块设备。 段 磁盘的每个 I/O 操作的实质是在磁盘与一些 RAM 单元之间相互传送一些相邻扇区的内容。 大多数情况下,磁盘控制器直接采用 DMA 方式进行数据传送。 DMA 方式的特点是,磁盘 控制器就像一个外置 CPU 一样,块设备驱动程序只要向磁盘控制器发送一些适当的命令就 可以触发一次数据传送;一旦完成数据的传送,控制器就会发出一个中断通知块设备驱动程 序。 DMA 方式传送的是磁盘上相邻扇区的数据。这是一个物理约束:磁盘控制器允许 DMA 传 送不相邻的扇区数据,但是这种方式的传送速率很低,因为在磁盘表面上移动读 /写磁头是 相当慢的。 老式的磁盘控制器仅仅支持“简单的” DMA 传送方式:在这种传送方式中,磁盘必须与 RAM 中的连续内存单元相互传送数据。但是,新的磁盘控制器,也就是我们即将讲到的 SCSI 磁 盘控制器,支持所谓的分散 -聚集(scatter-gather)DMA 传送方式。此种方式中,磁盘可以 与一些非连续的内存区相互传送数据。 启动一次分散 -聚集 DMA 传送,块设备驱动程序需要向磁盘控制器发送: (1)要传送的起始磁盘扇区号和总的扇区数 (2)内存区的描述符链表,其中链表的每项包含一个地址和一个长度 磁盘控制器则负责整个数据传送;例如,在读操作中控制器从相邻磁盘扇区中获得数据,然 后将它们存放到不同的内存区中。 为了使用分散 -聚集 DMA 传送方式,块设备驱动程序必 须能够处理称为段的数据存储元。 一个段就是一个内存页或内存页中的一部分,它们包含一 些相邻磁盘扇区中的数据。因此,一次分散 -聚集 DMA 操作可能同时传送几个段。 注意,块设备驱动程序不需要知道块、块大小以及块缓冲区。因此,即使高层将段看成是由 几个块缓冲区组成的页,块设备驱动程序也不用对此给予关注。 如果,不同的段在 RAM中相应的页框正好是连续的并且在磁盘上相应的数据块也是相邻的, 那么通用块层可以合并它们。通过这种合并方式产生的更大的内存区就称为物理段。 然而,在多种体系结构上还允许使用另一个合并方式:通过使用一个专门的总线电路来处理 总线地址与物理地址间的映射。通过这种合并方式产生的内存区称为硬件段。由于我们将注 意力集中在 80 x 86 体系结构上,它在总线地址和物理地址之间不存在动态的映射,因此在 本章剩余部分我们假定硬件段总是对应物理段。 5.4.2 通用块层相关数据结构 好了,通用块层的一些比较重要的基础知识大家都知道了,下面我们着重来看 bio这个东西。 大家在前面“创建一个 bio 请求”一节中已经见过这个结构了,但是,由于它太重要了,所 以我们这里有必要对它进行进一步的介绍。 每个 bio 结构都包含一个磁盘存储区标识符(存储区中的起始扇区号和扇区数目)和一个或 多个描述与 I/O 操作相关的内存区的段。 bio 由 bio 数据结构描述: struct bio { sector_t bi_sector; //块 I/O 操作的第一个磁盘扇区 struct bio *bi_next; //链接到请求队列中的下一个 bio struct block_device *bi_bdev;//指向块设备描述符的指针 unsigned long bi_flags; //bio 的状态标志 unsigned long bi_rw; //IO 操作标志,即这次 I.O 是读或写 unsigned short bi_vcnt; /* bio 的 bio_vec 数组中段的数目 */ unsigned short bi_idx; /* bio 的 bio_vec 数组中段的当前索引值 */ unsigned short bi_phys_segments; //合并之后 bio 中物理段的数目 unsigned short bi_hw_segments; //合并之后硬件段的数目 unsigned int bi_size; /* 需要传送的字节数 */ unsigned int bi_hw_front_size;// 硬件段合并算法使用 unsigned int bi_hw_back_size;// 硬件段合并算法使用 unsigned int bi_max_vecs; /* bio 的 bio vec 数组中允许的最大段数 */ struct bio_vec *bi_io_vec; /*指向 bio 的 bio_vec 数组中的段的指针 */ bio_end_io_t *bi_end_io; /* bio 的 I/O 操作结束时调用的方法 */ atomic_t bi_cnt; /* bio 的引用计数器 */ void *bi_private; //通用块层和块设备驱动程序的 I/O 完成方法使用的指针 bio_destructor_t *bi_destructor;//释放 bio 时调用的析构方法(通常是 bio_destructor() 方法)r }; 在前面普通文件的 readpage 方法 do_mpage_readpage 中,bio 的 bi_sector 是该页第一个块位 于磁盘的逻辑块号 blocks[0]左移(blkbits - 9)位,得到该块对应磁盘扇区号。前面讲了,扇区 大小是 512 字节,一个块是 1024 字节,所以你可以猜到 blkbits - 9 肯定等于 1。不错,blkbits 就是 10,这样 1<<10=1024 才是一个块的大小。 另外,回忆一下,在 do_mpage_readpage 中 bio 的 bdev 来自 buffer_head 的 b_bdev 字段; bi_destructor 被设置为 bio_fs_destructor 函数,作为当 bio 上的 I/O 操作完成时所执行的完成 程序的地址; bi_io_vec 通过 bvec_alloc_bs 函数初始化成数组,表示若干个 bio 待传输的段; 同时bi_flags和bi_max_vecs字段也被设置了。另外 min_t(int, nr_pages, bio_get_nr_vecs(bdev)) 表示这个 bio 中的 iovec 个数。所以,这里我们就来好好说道说道这个 bio_vec 数组。 bio 中的每个段是由一个 bio_vec 数据结构描述的,bio 中的 bi_io_vec 字段指向 bio_vec 数据 结构的第一个元素, bi_vcnt 字段则存放了 bio_vec 数组中当前的元素个数。 struct bio_vec { struct page *bv_page; //指向段的页框对应页描述符的指针 unsigned int bv_len; //段的字节长度 unsigned int bv_offset; //页框中段数据的偏移量 }; 在 do_mpage_readpage 调用 mpage_alloc 分配一个 bio 时,传递给 mpage_alloc 的参数是自 buffer_head 的 b_bdev 字段,blocks[0]的扇区号,min_t(int, nr_pages, bio_get_nr_vecs(bdev)) 和等于 GFP_KERNEL 的 gfp_flags。 而这个 min_t(int, nr_pages, bio_get_nr_vecs(bdev))这么一长串是什么呢?大家可以回过头去 看看 do_mpage_readpage 函数:nr_pages 是 mpage_readpage 传递给他的,其值是 1;所以我 们不用去管 bio_get_nr_vecs 的值了,min_t(int, nr_pages, bio_get_nr_vecs(bdev))这么一长串就 是 1,表示 bio 就只有一个 iovec 结构。什么意思?其实 iovec 结构是代表一个段,用于启动 一次分散-聚集 DMA 传送,存放磁盘控制器所需的内存区的描述符链表的一项,包含一个 地址和一个长度。 所以,对于普通文件,假设没有遇到文件的洞,那么一个页面所包含的 4 个块总是连续的, mpage_alloc调用的 bio_alloc_bioset 函数就只分配一个 bio_vec结构。随后 do_mpage_readpage 通过调用 bio_add_page 将这个结构的 bv_page 指向对于的页描述符, bv_len 设置为 4 个块的 大小 4096 字节,bv_offset 为 0。bio_add_page 还有一些合并段的工作,即不同的段在 RAM 中相应的页框正好是连续的并且在磁盘上相应的数据块也是相邻的,那么就合并它们。这些 内容都比较绕脑子,有兴趣的同学可以去尝试深入分析一下。 我们这里就假设一个页中 4 个块都是连续的,所以在 bio_add_page 函数的最后一行,把 4 个块的大小 4096 赋值给 bio 的 bi_size 字段。 5.4.3 提交 I/O 传输请求 好了,bio 这个数据我们建立好了,随后调用 generic_make_request 函数。这个函数是通用 块层的入口点,该层只有这一个函数处理请求: 3020void generic_make_request(struct bio *bio) 3021{ 3022 request_queue_t *q; 3023 sector_t maxsector; 3024 int ret, nr_sectors = bio_sectors(bio); 3025 dev_t old_dev; 3026 3027 might_sleep(); 3028 /* Test device or partition size, when known. */ 3029 maxsector = bio->bi_bdev->bd_inode->i_size >> 9; 3030 if (maxsector) { 3031 sector_t sector = bio->bi_sector; 3032 3033 if (maxsector < nr_sectors || maxsector - nr_sectors < sector) { 3034 /* 3035 * This may well happen - the kernel calls bread() 3036 * without checking the size of the device, e.g., when 3037 * mounting a device. 3038 */ 3039 handle_bad_sector(bio); 3040 goto end_io; 3041 } 3042 } 3043 3044 /* 3045 * Resolve the mapping until finished. (drivers are 3046 * still free to implement/resolve their own stacking 3047 * by explicitly returning 0) 3048 * 3049 * NOTE: we don't repeat the blk_size check for each new device. 3050 * Stacking drivers are expected to know what they are doing. 3051 */ 3052 maxsector = -1; 3053 old_dev = 0; 3054 do { 3055 char b[BDEVNAME_SIZE]; 3056 3057 q = bdev_get_queue(bio->bi_bdev); 3058 if (!q) { 3059 printk(KERN_ERR 3060 "generic_make_request: Trying to access " 3061 "nonexistent block-device %s (%Lu)\n", 3062 bdevname(bio->bi_bdev, b), 3063 (long long) bio->bi_sector); 3064end_io: 3065 bio_endio(bio, bio->bi_size, -EIO); 3066 break; 3067 } 3068 3069 if (unlikely(bio_sectors(bio) > q->max_hw_sectors)) { 3070 printk("bio too big device %s (%u > %u)\n", 3071 bdevname(bio->bi_bdev, b), 3072 bio_sectors(bio), 3073 q->max_hw_sectors); 3074 goto end_io; 3075 } 3076 3077 if (unlikely(test_bit(QUEUE_FLAG_DEAD, &q->queue_flags))) 3078 goto end_io; 3079 3080 /* 3081 * If this device has partitions, remap block n 3082 * of partition p to block n+start(p) of the disk. 3083 */ 3084 blk_partition_remap(bio); 3085 3086 if (maxsector != -1) 3087 blk_add_trace_remap(q, bio, old_dev, bio->bi_sector, 3088 maxsector); 3089 3090 blk_add_trace_bio(q, bio, BLK_TA_QUEUE); 3091 3092 maxsector = bio->bi_sector; 3093 old_dev = bio->bi_bdev->bd_dev; 3094 3095 ret = q->make_request_fn(q, bio); 3096 } while (ret); 3097} 首 先 3033 行, 检 查 bio->bi_sector 没有超过块设备的扇区数。如果超过,则调用 handle_bad_sector(bio)函数将 bio->bi_flags 设置为 BIO_EOF 标志,然后打印一条内核错误信息。随后跳到 end_io 标号处执行 3065 行的 bio_endio()函数,并终止。 bio_endio()更新 bio 描述符中的 bi_size 和 bi_sector 值,然后调用 bio 的 bi_end_io 方法。 如果没有超过,那么进入一个循环,通过 3057 行的 bdev_get_queue 函数获取与块设备相关 的请求队列 q;其中地址存放在 bio 的块设备描述符 bi_bdev 的 bd_disk 字段所指向的 gendisk 结构中,而该块设备描述符则由 bio->bi_bdev 指向: static inline request_queue_t *bdev_get_queue(struct block_device *bdev) { return bdev->bd_disk->queue; } 其中地址存放在块设备描述符的 bd_disk 字段所指向的 gendisk 结构中,而该块设备描述符 则由 bio->bi_bdev 指向。 接 下来 3084 行 调用 blk_partition_remap() 函数检查块设备是否指的是一个磁盘分区 (bio->bi_bdev 不等于 bio->bi_dev->bd_contains)。如果是,则从 bio->bi_bdev 获取分区的 hd_struct 描述符,从而执行下面的子操作: a) 根据数据传送的方向,更新 hd_struct 描述符中的 read_sectors 和 reads 值或 write_sectors 和 writes 值。 b) 调整 bio->bi_sector 值使得把相对于分区的起始扇区号转变为相对于整有盘的扇区号。 c) 将 bio->bi_bdev 设置为整个磁盘的块设备描述符( bio->bd_contains)。 从现在开始,通用块层、 I/O 调度程序以及设备驱动程序将忘记磁盘分区的存在,直接作用 于整个磁盘。 最终,3095 行调用 q->make_request_fn 方法进入块设备的 I/O 调度层,将 bio 请求插入请求 队列 q 中。 5.4.4 请求队列描述符 make_request_fn 方法属于块设备 I/O 调度层的内容,要继续往下走,需要介绍一下通用块层 的体系架构,这里需要从磁盘和磁盘分区开始说起。磁盘是一个由通用块层处理的逻辑块设 备,是块设备驱动中最重要的一个概念。通常一个磁盘对应一个硬件块设备,例如硬盘、软 盘或光盘。但是,磁盘也可以是一个虚拟设备,可以建立在几个物理磁盘分区之上或一些 RAM 专用页中的内存区上。在任何情形中,借助通用块层提供的服务,上层内核组件可以 以同样的方式工作在所有的磁盘上。 磁盘是由 gendisk 对象描述的,其中各字段如下所示。 struct gendisk { int major; /* Major 磁盘主设备号 */ int first_minor; //与磁盘关联的第一个次设备号 int minors; /* 与磁盘关联的次设备号范围 */ char disk_name[32]; /* 磁盘的标准名称(通常是相应设备文件的规范名称) */ struct hd_struct **part; /* 磁盘的分区描述符数组 */ int part_uevent_suppress; struct block_device_operations *fops; //指向块设备操作表的指针 struct request_queue *queue; //指向磁盘请求队列的指针 void *private_data; //块设备驱动程序的私有数据 sector_t capacity; //磁盘内存区的大小(扇区数目) int flags; //描述磁盘类型的标志 struct device *driverfs_dev; //指向磁盘的硬件设备的 device 对象的指针 struct kobject kobj; //内嵌的 kobject 结构 struct kobject *holder_dir; struct kobject *slave_dir; struct timer_rand_state *random; //该指针指向的这个数据结构记录磁盘中断的定时; //由内核内置的随机数发生器使用 int policy; //如果磁盘是只读的,则置为 1(写操作禁止),否则为 0 atomic_t sync_io; /* 写入磁盘的扇区数计数器,仅为 RAID 使用 */ unsigned long stamp; //统计磁盘队列使用情况的时间戳 int in_flight; //正在进行的 I/O 操作数 #ifdef CONFIG_SMP struct disk_stats *dkstats; #else struct disk_stats dkstats; //统计每个 CPU 使用磁盘的情况 #endif }; flags 字段存放了关于磁盘的信息。其中最重要的标志是 GENHD_FL_UP:如果设置它,那 么磁盘将被初始化并可以使用。另一个相关的标志是 GENHD_FL_REMOVABLE,如果是诸 如软盘或光盘这样可移动的磁盘,那么就要设置该标志。 gendisk 对象的 fops 字段指向一个表 block_device_operations,该表为块设备的主要操作存放 了几个定制的方法: struct block_device_operations { int (*open) (struct inode *, struct file *); //打开块设备文件 int (*release) (struct inode *, struct file *); //关闭对块设备文件的最后一个引用 int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long); long (*unlocked_ioctl) (struct file *, unsigned, unsigned long); long (*compat_ioctl) (struct file *, unsigned, unsigned long); int (*direct_access) (struct block_device *, sector_t, unsigned long *); int (*media_changed) (struct gendisk *); int (*revalidate_disk) (struct gendisk *); //检查块设备是否持有有效数据 int (*getgeo)(struct block_device *, struct hd_geometry *); struct module *owner; }; 通常硬盘被划分成几个逻辑分区。每个块设备文件要么代表整个磁盘,要么代表磁盘中的某 一个分区。例如,一个主设备号为 3、次设备号为 0 的设备文件/dev/hda 代表的可能是一个 主 EIDE 磁盘;该磁盘中的前两个分区分别由设备文件 /dev/hda1 和/dev/hda2 代表,它们的 主设备号都是 3,而次设备号分别为 1 和 2。一般而言,磁盘中的分区是由连续的次设备号 来区分的。 如果将一个磁盘分成了几个分区,那么其分区表保存在 hd_struct 结构的数组中,该数组的 地址存放在 gendisk 对象的 part 字段中。通过磁盘内分区的相对索引对该数组进行索引。 hd_struct 描述符中的字段如下所示: struct hd_struct { sector_t start_sect; //磁盘中分区的起始扇区 sector_t nr_sects; //分区的长度(扇区数) struct kobject kobj; //内嵌的 kobject struct kobject *holder_dir; unsigned ios[2], sectors[2]; /* 对分区发出的读写操作次数和从分区读写的扇区数 */ int policy, partno; //磁盘中分区的相对索引 }; 当内核发现系统中一个新的磁盘时(在启动阶段,或将一个可移动介质插人一个驱动器中时, 或在运行期附加一个外置式磁盘时),就调用 alloc_disk()函数,该函数分配并初始化一个新 的 gendisk 对象,如果新磁盘被分成了几个分区,那么 alloc_disk()还会分配并初始化一个适 当的 hd_struct 类型的数组。然后,内核调用 add_disk ()函数将新的 gendisk 对象插入到通用 块层的数据结构中。 不管怎样,整个 gendisk- hd_struct 体系中,最重要的是请求队列结构。请求队列描述符是由 一个大的数据结构 request_queue 表示的,其字段如下表所示: struct request_queue { struct list_head queue_head; //待处理请求的链表 struct request *last_merge; //指向队列中首先可能合并的请求描述符 elevator_t *elevator; //指向 elevator 对象的指针(电梯算法) struct request_list rq; //为分配请求描述符所使用的数据结构 request_fn_proc *request_fn; //实现驱动程序的策略例程入口点的方法, //策略例程方法来处理请求队列中的下一个请求 merge_request_fn *back_merge_fn; //检查是否可能将 bio 合并到请求队列的最后一个请 求中的方法 merge_request_fn *front_merge_fn; //检查是否可能将 bio 合并到队列的第一个请求中的 方法 merge_requests_fn *merge_requests_fn; //试图合并请求队列中两个相邻请求的方法 make_request_fn *make_request_fn; //将一个新请求插入请求队列时调用的方法 prep_rq_fn *prep_rq_fn; //该方法把这个处理请求的命令发送给硬件设备 unplug_fn *unplug_fn; //去掉块设备的方法 merge_bvec_fn *merge_bvec_fn; //当增加一个新段时, //该方法返回可插人到某个已存在的 bio 结构中的字节数(通常未定义) activity_fn *activity_fn; //将某个请求加入请求队列时调用的方祛(通常未定义) issue_flush_fn *issue_flush_fn; //刷新请求队列时调用的方法 //(通过连续处理所有的请求清空队列) prepare_flush_fn *prepare_flush_fn; softirq_done_fn *softirq_done_fn; /* * Dispatch queue sorting */ sector_t end_sector; struct request *boundary_rq; /* * Auto-unplugging state */ struct timer_list unplug_timer; //插入设备时使用的动态定时器 int unplug_thresh; /* 如果请求队列中待处理请求数大于该值, 将立即去掉请求设备(缺省值是 4)*/ unsigned long unplug_delay; /* 去掉设备之前的时间延迟(缺省值是 3ms)*/ struct work_struct unplug_work; //去掉设备时使用的操作队列 struct backing_dev_info backing_dev_info; void *queuedata; //指向块设备驱动程序的私有数据的指针 void *activity_data; //activity_fn 方法使用的私有数据 unsigned long bounce_pfn; //在大于该页框号时必须使用缓冲区回弹 gfp_t bounce_gfp; //回弹缓冲区的内存分配标志 unsigned long queue_flags; //描述请求队列状态的标志 spinlock_t __queue_lock; //请求队列锁 spinlock_t *queue_lock; //指向请求队列锁的指针 struct kobject kobj; //请求队列的内嵌 kobject 结构 unsigned long nr_requests; /* 请求队列中允许的最大请求数 s */ unsigned int nr_congestion_on; //如果待处理请求数超出了该闭值, //则认为该队列是拥挤的 unsigned int nr_congestion_off;//如果待处理请求数在这个闭值的范围内, //则认为该队列是不拥挤的 unsigned int nr_batching; //即使队列已满, //仍可以由特殊进程“ batcher”提交的待处理请求的最大值(通常为 32) unsigned int max_sectors; //单个请求所能处理的最大扇区数(可调的) unsigned int max_hw_sectors; //单个请求所能处理的最大扇区数(硬约束) unsigned short max_phys_segments; //单个请求所能处理的最大物理段数 unsigned short max_hw_segments; //单个请求所能处理的最大硬段数(分散-聚集 DMA 操作中的最大不同内存区数) unsigned short hardsect_size; //扇区中以字节为单位的大小 unsigned int max_segment_size; //物理段的最大长度(以字节为单位) unsigned long seg_boundary_mask; //段合并的内存边界屏蔽字 unsigned int dma_alignment; //DMA 缓冲区的起始地址和长度的对齐位图(缺省值 是 511) struct blk_queue_tag *queue_tags; //空 闲 /忙标记的位图(用于带标记的请求) blk_queue_tag * unsigned int nr_sorted; //请求队列的引用计数器 unsigned int in_flight; //请求队列中待处理请求数 unsigned int sg_timeout; //用户定义的命令超时(仅由 SCSI 通用块设备使用) unsigned int sg_reserved_size; //基本上没有使用 int node; struct blk_trace *blk_trace; unsigned int ordered, next_ordered, ordseq; int orderr, ordcolor; struct request pre_flush_rq, bar_rq, post_flush_rq; struct request *orig_bar_rq; unsigned int bi_size; struct mutex sysfs_lock; }; 请求队列是一个双向链表,其元素就是请求描述符(也就是 request 数据结构,下面马上谈 到)。请求队列描述符中的 queue_head 字段存放链表的头(第一个伪元素),而请求描述符 中 queuelist 字段的指针把任一请求链接到链表的前一个和后一个元素之间。 队列链表中元素的排序方式对每个块设备驱动程序是特定的;然而, I/O 调度程序提供了几 种预先确定好的元素排序方式,牵涉到“ I/O 调度算法”的概念,后面会提到。 backing_dev_info 字段是一个 backing_dev_info 类型的小对象,它存放了关于基本的硬件块 设备的 I/O 数据流量的信息。例如,它保存了关于预读以及关于请求队列拥塞状态的信息。 每个块设备的待处理请求都是用一个请求描述符来表示的,请求描述符存放在如下所示的 request 数据结构中: struct request { struct list_head queuelist; /* 请求队列链表的指针 */ struct list_head donelist; unsigned long flags; /* 请求标志 */ sector_t sector; /*要传送的下一个扇区号 */ unsigned long nr_sectors; /* 整个请求中要传送的扇区数 */ /* no. of sectors left to submit in the current segment */ unsigned int current_nr_sectors; //当前 bio 的当前段中要传送的扇区数 sector_t hard_sector; /* 要传送的下一个扇区号 */ unsigned long hard_nr_sectors; /* 整个请求中要传送的扇区数(由通用块层更新) */ /* no. of sectors left to complete in the current segment */ unsigned int hard_cur_sectors; //当前 bio 的当前段中要传送的扇区数(由通用块层更新) struct bio *bio; //请求中第一个没有完成传送操作的 bio struct bio *biotail; //请求链表中末尾的 bio void *elevator_private; //指向 I/O 调度程序私有数据的指针 void *completion_data; int rq_status; /* 请求状态:实际上,或者是 RQ_ACTIVE,或者是 RQ_INACTIVE */ int errors; //用于记录当前传送中发生的 I/O 失败次数的计数器 struct gendisk *rq_disk; //请求所引用的磁盘描述符 unsigned long start_time; //请求的起始时间(用 jiffies 表示) unsigned short nr_phys_segments; //请求的物理段数 unsigned short nr_hw_segments; //请求的硬段数 unsigned short ioprio; int tag; //与请求相关的标记(只适合支持多次数据传送的硬件设备) int ref_count; //请求的引用计数器 request_queue_t *q; //指向包含请求的请求队列描述符的指针 struct request_list *rl; //指向 request_list 结构的指针 struct completion *waiting; //等待数据传送终止的 Completion 结构 void *special; //对硬件设备发出“特殊”命令的请求所使用的数据的指针 char *buffer; //指向当前数据传送的内存缓冲区的指针(如果缓冲区是高端内存区,则为 NULL) unsigned int cmd_len; //cmd 字段中命令的长度 unsigned char cmd[BLK_MAX_CDB]; //由请求队列的 prep_rq_fn 方法准备好的预先内置 命令所在的缓冲区后面还会详细谈到 unsigned int data_len; //通常,由 data 字段指向的缓冲区中数据的长度 unsigned int sense_len; //由 sense 字段指向的缓冲区的长度(如果 sense 是 NULL,则为 0) void *data; //设备驱动程序为了跟踪所传送的数据而使用的指针 void *sense; //指向输出 sense 命令的缓冲区的指针 unsigned int timeout; //请求的超时 int retries; rq_end_io_fn *end_io; void *end_io_data; }; 每个 request 请求包含一个或多个 bio 结构。最初,通用块层创建一个仅包含一个 bio 结构的 请求。然后, I/O 调度程序要么向初始的 bio 中增加一个新段,要么将另一个 bio 结构链接 到请求中,从而“扩展”该请求,原因是可能存在新数据与请求中已存在的数据物理相邻的 情况。请求描述符的 bio 字段指向请求中的第一个 bio 结构,而 biotail 字段则指向最后一个 bio 结构。rq_for_each_bio 宏执行一个循环,从而遍历一个请求(而不是请求队列)中的所 有 bio 结构。 请求描述符中的几个字段值可能是动态变化的。例如,一旦 bio 中引用的数据块全部传送完 毕,bio 字段立即更新从而指向请求链表中的下一个 bio。在此期间,另一个新的 bio 可能被 加人到请求链表的尾部,所以 biotail 的值也可能改变。 当磁盘数据块正在传送时,请求描述符的其它几个字段的值由 I/O 调度程序或设备驱动程序 修改。例如, nr_sectors 存放整个请求还需传送的扇区数, current_nr_sectors 存放当前 bio 结构中还需传送的扇区数。 flags 中存放了很多标志,如下表中所示。到目前为止,最重要的一个标志是 REQ_RW,它 确定数据传送的方向。 REQ_RW 数据传送的方向: READ(0)或 WRITE(1) REQ_FAILFAST 万一出错请求申明不再重试 I/O 操作 REQ_SOFTBARRIER 请求相当于 I/O 调度程序的屏障 REQ_HARDBARRIER 请求相当于 1/O 调度程序和设备驱动程序的屏障—应当在旧请求与 新请求之间处理该请求 REQ_CMD 包含一个标准的读或写 I/O 数据传送的请求 REQ_NOMERGE 不允许扩展或与其它请求合并的请求 REQ_STARTED 正处理的请求 REQ_DONTPREP 不调用请求队列中的 prep_rq_fn 方法预先准备把命令发选项发给硬件设 备 REQ_QUEUED 请求被标记——也就是说,与该请求相关的硬件设备可以同时管理很多未 完成数据的传送 REQ_PC 请求包含发送给硬件设备的直接命令 REQ_BLOCK_PC 与前一个标志功能相同,但发送的命令包含在 bio 结构中 REQ_SENSE 请求包含一个“ sense”请求命令(SCSI 和 ATAPI 设备使用) REQ_FAILED 当请求中的 sense 或 direct 命令的操作与预期的不一致时设置该标志 REQ_QUIET 万一 I/O 操作出错请求申明不产生内核消息 REQ_SPECIAL 请求包含对硬件设备的特殊命令(例如,重设驱动器) REQ_DRIVE_CMD 请求包含对 IDE 磁盘的特殊命令 REQ_DRIVE_TASK 请求包含对 IDE 磁盘的特殊命令 REQ_DRIVE_TASKFILE 请求包含对 IDE 磁盘的特殊命令 REQ_PREEMPT 请求取代位于请求队列前面的请求(仅对 IDE 磁盘而言) REQ_PM_SUSPEND 请求包含一个挂起硬件设备的电源管理命令 REQ_PM_RESUME 请求包含一个唤醒硬件设备的电源管理命令 REQ_PM_SHUTDOWN 请求包含一个切断硬件设备的电源管理命令 REQ_BAR_PREFLUSH 请求包含一个要发送给磁盘控制器的“刷新队列”命令 REQ_BAR_POSTFLUSH 请求包含一个已发送给磁盘控制器的“刷新队列”命令 介绍完了这些来自 ULK-3 上的基础知识,我们通过一个图把前面的知识串联起来: 前面提到 generic_make_request 调用 q->make_request_fn 方法将 bio 请求插入请求队列 q 中。 那么接下来的工作就交给 I/O 调度层了。 5.5 块设备 I/O 调度层的处理 下面进入块设备 I/O 调度层,来看看 q->make_request_fn 方法。不过这个方法的具体函数是 什么呢?别着急,要弄清这个问题还需要再补充一下块设备驱动的基础知识,不然就又走不 下去了。块设备驱动程序是 Linux 块子系统中的最底层组件。它们从 I/O 调度程序中获得请 求,然后按要求处理这些请求。 当然,块设备驱动程序是设备驱动程序模型的组成部分(也就是在 sysfs 中能够看到它)。因 此,每个块设备驱动程序对应一个 device_driver 类型的描述符;此外,设备驱动程序处理的 每个磁盘都与一个 device 类型的描述符相关联。但是,这些描述符没有什么特别的目的:只不过是为块 I/O 子系统必须在 sysfs 为系统中的每个块设备用来存放附加信息,后面等我 们遇到了他们了,再来详细分析。 我们真正应该关注的,是块设备本身。每个块设备都是由一个 block_device 结构的描述符来 表示,其字段如下: struct block_device { dev_t bd_dev; /* 块设备的主设备号和次设备号 */ struct inode * bd_inode; /* 指向 bdev 文件系统中块设备对应的索引节点的指针 */ int bd_openers; /* 计数器,统计块设备已经被打开了多少次 */ struct mutex bd_mutex; /* 保护块设备的打开和关闭的信号量 */ struct mutex bd_mount_mutex; /* 禁止在块设备上进行新安装的信号量 */ struct list_head bd_inodes; /* 已打开的块设备文件的索引节点链表的首部 */ void * bd_holder; /* 块设备描述符的当前所有者 */ int bd_holders; /* 计数器,统计对 bd_holder 字段多次设置的次数 */ #ifdef CONFIG_SYSFS struct list_head bd_holder_list; #endif struct block_device * bd_contains; /* 如果块设备是一个分区, 则指向整个磁盘的块设备描述符; 否则,指向该块设备描述符 */ unsigned bd_block_size; /* 块大小 */ struct hd_struct * bd_part; /* 指向分区描述符的指针 (如果该块设备不是一个分区,则为 NULL) */ /* number of times partitions within this device have been opened. */ unsigned bd_part_count; /* 计数器, 统计包含在块设备中的分区已经被打开了多少次 */ int bd_invalidated; /* 当需要读块设备的分区表时设置的标志 */ struct gendisk * bd_disk; /* 指向块设备中基本磁盘的 gendisk 结构的指针 */ struct list_head bd_list; /* 用于块设备描述符链表的指针 */ struct backing_dev_info *bd_inode_backing_dev_info; /*(通常为 NULL)*/ unsigned long bd_private; /* 指向块设备持有者的私有数据的指针 */ }; 所有的块设备描述符被插入一个全局链表中,链表首部是由变量 all_bdevs 表示的;链表链 接所用的指针位于块设备描述符的 bd_list 字段中。 如果块设备描述符对应一个磁盘分区,那么 bd_contains 字段指向与整个磁盘相关的块设备 描述符,而 bd_part 字段指向 hd_struct 分区描述符。否则,若块设备描述符对应整个磁盘, 那么 bd_contains 字段指向块设备描述符本身, bd_part_count 字段用于记录磁盘上的分区已 经被打开了多少次。 bd_holder 字段存放代表块设备持有者的线性地址。持有者并不是进行 I/O 数据传送的块设 备驱动程序;准确地说,它是一个内核组件,使用设备并拥有独一无二的特权(例如,它可以自由使用块设备描述符的 bd_private 字段)。典型地,块设备的持有者是安装在该设备上 的文件系统。当块设备文件被打开进行互斥访问时,另一个普遍的问题出现了:持有者就是 对应的文件对象。 bdclaim()函数将 bd_holder 字段设置为一个特定的地址;相反, bd_release()函数将该字段重 新设置为 NULL。然而,值得注意的是,同一个内核组件可以多次调用 bdclaim()函数,每 调用一次都增加 bd_holders 的值。为了释放块设备,内核组件必须调用 bd_release()函数 bd_holders 次。 ULK-3 上有一个很经典的图对一个整盘进行了描述,它说明了块设备描述符是如何被链接 到通用块层的其他重要数据结构上的。 那么,如何访问一个块设备呢?当内核接收一个打开块设备文件的请求时,必须首先确定该 设备文件是否已经是打开的。事实上,如果文件已经是打开的,内核就没有必要创建并初始 化一个新的块设备描述符;相反,内核应该更新这个已经存在的块设备描述符。然而,真正 的复杂性在于具有相同主设备号和次设备号但有不同路径名的块设备文件被 VFS 看作不同 的文件,但是它们实际上指向同一个块设备。因此,内核无法通过简单地在一个对象的索引 节点高速缓存中检查块设备文件的存在就确定相应的块设备已经在使用。 主、次设备号和相应的块设备描述符之间的关系是通过 bdev 特殊文件系统(挂载在 /dev 目 录的子目录中)来维护的。 每个块设备描述符都对应一个 bdev 特殊文件:块设备描述符的 bd_inode 字段指向相应的 bdev 索引节点;而该索引节点则将块设备的主、次设备号和相应 描述符的地址进行编码。 bdget()接收块设备的主设备号和次设备号作为其参数:在 bdev 文件系统中查寻相关的索引 节点;如果不存在这样的节点,那么就分配一个新索引节点和新块设备描述 block_device。 在任何情形下,函数都返回一个与给定主、次设备号对应的块设备描述符的地址。 一旦找到了块设备的描述符,那么内核通过检查 bd_openers 字段的值来确定块设备当前是 否在使用:如果值是正的,说明块设备已经在使用(可能通过不同的设备文件)。 同时内核也维护一个与已打开的块设备文件对应的索引节点对象的链表。该链表存放在块设备描述符 的 bd_inodes 字段中;索引节点对象的 i_devices 字段存放用于链接表中的前后元素的指针。 一个块设备驱动程序可能处理几个块设备。例如, IDE 设备驱动程序可以处理几个 IDE 磁 盘,其中的每个都是一个单独的块设备。而且,每个磁盘通常是被分区的,每个分区又可以 被看做是一个逻辑磁盘。很明显,块设备驱动程序必须处理在块设备对应的块设备文件上发 出所有的 VFS 系统调用。 5.5.1 块设备的初始化 q->make_request_fn 方法的具体实现函数,取决于具体的块设备。下面我们就通过当前最流 行的 SCSI 磁盘控制器来说明一个具体的块设备在 Linux 内核中是如何建立 I/O 调度和底层 驱动体系的。 Linux 是在系统初始化的时候执行 genhd_device_init()函数启动整个 Block 子系 统的: static int __init genhd_device_init(void) { ⋯⋯ bdev_map = kobj_map_init(base_probe, &block_class_lock); blk_dev_init(); register_blkdev(BLOCK_EXT_MAJOR, "blkext"); #ifndef CONFIG_SYSFS_DEPRECATED /* create top-level block dir */ block_depr = kobject_create_and_add("block", NULL); #endif return 0; } 该函数主要是调用来自 block/ll_rw_blk.c 中的 blk_dev_init()来对块设备进行初始化: int __init blk_dev_init(void) { BUILD_BUG_ON(__REQ_NR_BITS > 8 * sizeof(((struct request *)0)->cmd_flags)); kblockd_workqueue = create_workqueue("kblockd"); if (!kblockd_workqueue) panic("Failed to create kblockd\n"); request_cachep = kmem_cache_create("blkdev_requests", sizeof(struct request), 0, SLAB_PANIC, NULL); blk_requestq_cachep = kmem_cache_create("blkdev_queue", sizeof(struct request_queue), 0, SLAB_PANIC, NULL); return 0; } 首先第一个函数, create_workqueue("kblockd"),表示每个处理机给他分配一个工作队列。这 里返回值赋给了 kblocked_workqueue。接下来,使用两个 kmem_cache_create()分别为请求描 述符、请求队列描述符和 I/O 上下文建立两个 slab 分配器,他们分别是这里的 request 和 request_queue。 我们再回到 genhd_device_init()中来,很显然,这里还有两个函数我们并没有讲,一个是 kobj_map_init(),一个是 register_blkdev()。先说前者,kobj_map_init 函数,sysfs 体系里边的 东西,定义于 drivers/base/map.c: struct kobj_map { struct probe { struct probe *next; dev_t dev; unsigned long range; struct module *owner; kobj_probe_t *get; int (*lock)(dev_t, void *); void *data; } *probes[255]; struct mutex *lock; }; static struct kobj_map *bdev_map; 而 genhd_device_init()中的 kobj_map_init()函数是这样的: struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock){ struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL); struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL); int i; ⋯⋯ base->dev = 1; base->range = ~0; base->get = base_probe; for (i = 0; i < 255; i++) p->probes[i] = base; p->lock = lock; return p; } 看得出,申请了一个 struct kobj_map 的指针 p,然后最后返回的也是 p,即最后把一切都献 给了 bdev_kmap。而这里真正干的事情无非就是让 bdev_kmap->probes[]数组全都等于 base。 换言之,它们的 get 指针全都指向了这里传递进来的 base_probe 函数,这个函数主要是用来 将块设备的与 sysfs 中的 kobject 建立联系,我们这里就不再赘述了。 相比之下,register_blkdev()函数更容易理解,注册 Block 系统,来自 block/genhd.c: 55 int register_blkdev(unsigned int major, const char *name) 56 { 57 struct blk_major_name **n, *p; 58 int index, ret = 0; 59 60 mutex_lock(&block_subsys_lock); 61 62 /* temporary */ 63 if (major == 0) { 64 for (index = ARRAY_SIZE(major_names)-1; index > 0; index--) { 65 if (major_names[index] == NULL) 66 break; 67 } 68 69 if (index == 0) { 70 printk("register_blkdev: failed to get major for %s\n", 71 name); 72 ret = -EBUSY; 73 goto out; 74 } 75 major = index; 76 ret = major; 77 } 78 79 p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL); 80 if (p == NULL) { 81 ret = -ENOMEM; 82 goto out; 83 } 84 85 p->major = major; 86 strlcpy(p->name, name, sizeof(p->name)); 87 p->next = NULL; 88 index = major_to_index(major); 89 90 for (n = &major_names[index]; *n; n = &(*n)->next) { 91 if ((*n)->major == major) 92 break; 93 } 94 if (!*n) 95 *n = p; 96 else 97 ret = -EBUSY; 98 99 if (ret < 0) { 100 printk("register_blkdev: cannot get major %d for %s\n", 101 major, name); 102 kfree(p); 103 } 104 out: 105 mutex_unlock(&block_subsys_lock); 106 return ret; 107 } 如果是一个基于 scsi 的块设备,咱们是指定了主设备号了的。换言之, 这里的 major 是非零 值,而 struct blk_major_name 的定义也在 block/genhd.c 中: static struct blk_major_name { struct blk_major_name *next; int major; char name[16]; } *major_names[BLKDEV_MAJOR_HASH_SIZE]; 注意这里也是顺便定义了一个数组 major_names ,咱们这里也用到了。这其中 BLKDEV_MAJOR_HASH_SIZE 是 255。即数组 major_names[]有 255 个元素,换言之,咱 们定义了 255 个指针. 而 88 行这个内联函数同样来自 block/genhd.c: static inline int major_to_index(int major) { 36 return major % BLKDEV_MAJOR_HASH_SIZE; 37 } 比如咱们传递的 major 是 8,那么 major_to_index 就是 8。 不难理解,register_blkdev()这个函数做的事情就是,为这 255 个指针找到归属,即先在 79 行调用 kmalloc 申请一个 struct blk_major_name 结构体并且让 p 指向它,接下来为 p 赋值,而 n 将指向 major_names[index],比如 index 就是 8,那么 n 就指向 major_names[8],一开始 它肯定为空,所以直接执行 94 行并进而 95 行,于是就把赋好值的 p 的那个结构体赋给了 major_names[8],因此 major_names[8]就既有 major 也有 name 了,name 就是“sd”。 此时此刻,在 /dev/目录下的 sda,sdb 之类的文件,我们就可以通过 /proc/devices 看到这个块 设备驱动了。事实上, proc 文件系统中,deivices 文件的 Block devices 部分的内容也正是读 major_names 这个数组的内容,来显示出块设备驱动的名字的。 5.5.2 建立块设备驱动环境 上一节内容反映的是系统上电后, Linux 初始化块设备子系统并注册了相应的块设备驱动的 内容。而当我们把一块 300G 的硬盘插入总线的 SCSI 插槽中时,SCSI 设备对应的 probe 程 序就会调用 alloc_disk()函数为其分配 gendisk 结构。 那么 SCSI 设备对应的 probe 程序到底是什么呢?设备驱动的研究是有一定套路的,比如说 我们这里的 scsi 吧,必须在 drivers/scsi 目录下看它的 Kconfig 和 Makefile 文件。根据文件中 的内容可得知, SCSI 磁盘的驱动就只有一个文件,就是 drivers/scsi/sd.c。 看到这个文件的最后两行: module_init(init_sd); module_exit(exit_sd); 所以,scsi 磁盘驱动的故事就从 init_sd 开始: 1837 static int __init init_sd(void) 1838 { 1839 int majors = 0, i, err; 1840 1841 SCSI_LOG_HLQUEUE(3, printk("init_sd: sd driver entry point\n")); 1842 1843 for (i = 0; i < SD_MAJORS; i++) 1844 if (register_blkdev(sd_major(i), "sd") == 0) 1845 majors++; 1846 1847 if (!majors) 1848 return -ENODEV; 1849 1850 err = class_register(&sd_disk_class); 1851 if (err) 1852 goto err_out; 1853 1854 err = scsi_register_driver(&sd_template.gendrv); 1855 if (err) 1856 goto err_out_class; 1857 1858 return 0; 1859 1860 err_out_class: 1861 class_unregister(&sd_disk_class); 1862 err_out: 1863 for (i = 0; i < SD_MAJORS; i++) 1864 unregister_blkdev(sd_major(i), "sd"); 1865 return err; 1866 } 函数很简单,包括一串的注册注销函数。 其中 register_blkdev 我们见过了,在 dev 文件系统中注册每一个磁盘及其分区。这里出现的 宏 SD_MAJORS 实际上被定义为 16: #define SD_MAJORS 16 所以经过 16 次循环之后,我们看到这里叫 sd 的有 16 个。这些号码是怎么来的? Linux 中所 有的主设备号是被各种各样的设备所占据的。其中 8、65-71、136-143 这么 16 个号码就被 scsi disk 所霸占了,sd_major()函数的返回值就是这 16 个数字。每个主设备号可以带 256 个 次设备号。 class_register()函数将 scsi 磁盘注册到 sysfs 体系中,让 scsi_disk 作为/sys/class/的一个子目录。 感兴趣的可以去好好研究一下 Linux 的设备驱动模型,但是这里对我们分析文件读写的意义 不大,所以我们略过。 而 1854 行 scsi_register_driver 则是最重要的函数,作用是注册一个 scsi 设备驱动。在 Linux 设备模型中,对于每个设备驱动,有一个与之对应的 struct device_driver 结构体。而为了体 现各类设备驱动自身的特点,各个子系统可以定义自己的结构体,然后把 struct device_driver 包含进来如 C++中基类和扩展类一样。对于 scsi 子系统,这个基类就是 struct scsi_driver, 来自 include/scsi/scsi_driver.h: struct scsi_driver { struct module *owner; struct device_driver gendrv; int (*init_command)(struct scsi_cmnd *); void (*rescan)(struct device *); int (*issue_flush)(struct device *, sector_t *); int (*prepare_flush)(struct request_queue *, struct request *); }; 我们看到 device_driver 结构是嵌入在这个 scsi_driver 中的,来自 include/linux/device.h: struct device_driver { const char * name; struct bus_type * bus; struct completion unloaded; struct kobject kobj; struct klist klist_devices; struct klist_node knode_bus; struct module * owner; int (*probe) (struct device * dev); int (*remove) (struct device * dev); void (*shutdown) (struct device * dev); int (*suspend) (struct device * dev, pm_message_t state); int (*resume) (struct device * dev); }; 而咱们也自然定义了一个 scsi_driver 的结构体实例,它的名字叫做 sd_template: static struct scsi_driver sd_template = { .owner = THIS_MODULE, .gendrv = { .name = "sd", .probe = sd_probe, .remove = sd_remove, .shutdown = sd_shutdown, }, .rescan = sd_rescan, .init_command = sd_init_command, .issue_flush = sd_issue_flush, }; 这其中,gendrv 就是 struct device_driver 的结构体变量。咱们这么一注册,其直观效果就是: localhost:~ # ls /sys/bus/scsi/drivers/ sd 其间接效果就是,在系统的总线上, scsi 接口发现了又 scsi 磁盘,就调用 sd_probe 加载 scsi 的驱动。而与以上三个函数相反的就是 exit_sd()中的另外三个函数:scsi_unregister_driver、 class_unregister、unregister_blkdev。 Ok 了,这个初始化就这么简单地结束了。下一步我们就从 sd_probe 函数看起,某种意义来 说,读 sd_mod 的代码就算是对 scsi 子系统的入门。 static int sd_probe(struct device *dev) { struct scsi_device *sdp = to_scsi_device(dev); struct scsi_disk *sdkp; struct gendisk *gd; u32 index; int error; ⋯⋯ gd = alloc_disk(16); if (!gd) goto out_free; if (!idr_pre_get(&sd_index_idr, GFP_KERNEL)) goto out_put; ⋯⋯ gd->major = sd_major((index & 0xf0) >> 4); gd->first_minor = ((index & 0xf) << 4) | (index & 0xfff00); gd->minors = 16; gd->fops = &sd_fops; if (index < 26) { sprintf(gd->disk_name, "sd%c", 'a' + index % 26); } else if (index < (26 + 1) * 26) { sprintf(gd->disk_name, "sd%c%c", 'a' + index / 26 - 1,'a' + index % 26); } else { const unsigned int m1 = (index / 26 - 1) / 26 - 1; const unsigned int m2 = (index / 26 - 1) % 26; const unsigned int m3 = index % 26; sprintf(gd->disk_name, "sd%c%c%c", 'a' + m1, 'a' + m2, 'a' + m3); } gd->private_data = &sdkp->driver; gd->queue = sdkp->device->request_queue; sd_revalidate_disk(gd); gd->driverfs_dev = &sdp->sdev_gendev; gd->flags = GENHD_FL_DRIVERFS; if (sdp->removable) gd->flags |= GENHD_FL_REMOVABLE; dev_set_drvdata(dev, sdkp); add_disk(gd); sdev_printk(KERN_NOTICE, sdp, "Attached scsi %sdisk %s\n", sdp->removable ? "removable " : "", gd->disk_name); return 0; out_put: put_disk(gd); out_free: kfree(sdkp); out: return error; } 我们来看一下 alloc_disk()函数,在源代码 sd.c 中。咱们这里传递进来的参数是 16,表示次 设备号范围为 1~16。 struct gendisk *alloc_disk(int minors) { return alloc_disk_node(minors, -1); } struct gendisk *alloc_disk_node(int minors, int node_id) { struct gendisk *disk; disk = kmalloc_node(sizeof(struct gendisk), GFP_KERNEL, node_id); if (disk) { memset(disk, 0, sizeof(struct gendisk)); if (!init_disk_stats(disk)) { kfree(disk); return NULL; } if (minors > 1) { int size = (minors - 1) * sizeof(struct hd_struct *); disk->part = kmalloc_node(size, GFP_KERNEL, node_id); if (!disk->part) { kfree(disk); return NULL; } memset(disk->part, 0, size); } disk->minors = minors; kobj_set_kset_s(disk,block_subsys); kobject_init(&disk->kobj); rand_initialize_disk(disk); INIT_WORK(&disk->async_notify, media_change_notify_thread); } return disk; } 首先,我们做的事情就是申请了一个 struct gendisk 数据结构。毫无疑问,这个结构体是我 们 I/O 调度层中最重要的结构体之一,就是在这里被分配的。 因为 minors 我们给的是 16,于是 size 等于 15 个 sizeof(struct hd_struct *),而 part 我们看到 是 struct hd_struct 的二级指针,这里我们看到 kmalloc_node(),这个函数中的 node/node_id 这些概念指的是 NUMA 技术中的节点,对于咱们这些根本就不会接触 NUMA 的人来说 kmalloc_node()就等于 kmalloc(),因此这里做的就是申请内存并且初始化为 0。要说明的一 点是,part 就是 partition 的意思,日后它将扮演我们常说的分区的角色。 然后,disk->minors 设置为了 16。随后,kobj_set_kset_s(),block_subsys 是我们前面注册的 子系统,从数据结构来说,它的定义如下,来自 block/genhd.c: struct kset block_subsys; 其实也就是一个 struct kset。而这里的 kobj_set_kset_s 的作用就是让 disk 对应 kobject 的 kset 等于 block_subsys。也就是说让 kobject 找到它的 kset。(如果你还记得当初我们在我是 Sysfs 中分析的 kobject 和 kset 的那套理论的话,你不会不明白这里的意图。 )而 kobject_init()初始 化一个 kobject,这个函数通常就是出现在设置了 kobject 的 kset 之后。 正是因为有了这么一个定义,正是因为这里我们把“ block”给了 block_subsys 的 kobj 的 name 成员,所以当我们在 block 子系统初始化的时候调用 subsystem_register(&block_subsys)之后, 我们才会在/sys/目录下面看到“ block”子目录: [root@localhost~]# ls /sys/ block bus class devices firmware fs kernel module power 看到 rand_initialize_disk(disk)函数,初始化一个工作队列,这是一个很重要的数据结构,到 时候用到了再来看。至此, alloc_disk_node 就将返回,从而 alloc_disk 也就返回了。 当 sd_probe 程序调用 alloc_disk()函数为我们的 SCSI 硬盘分配了 gendisk 结构,紧接着会调 用 add_disk()来安装这个块设备: void add_disk(struct gendisk *disk){ disk->flags |= GENHD_FL_UP; blk_register_region(MKDEV(disk->major, disk->first_minor), disk->minors, NULL, exact_match, exact_lock, disk); register_disk(disk); blk_register_queue(disk); } 这个函数虽然只有四行代码,可是十分复杂,旗下三个函数,一个比一个难,我们接下来详 细分析。 头一个,blk_register_region,来自 block/genhd.c: void blk_register_region(dev_t dev, unsigned long range, struct module *module, struct kobject *(*probe)(dev_t, int *, void *), int (*lock)(dev_t, void *), void *data){ kobj_map(bdev_map, dev, range, module, probe, lock, data); } 虽然在这里我们完全看不出这么做的意义,或者说 blk_register_region 这个函数究竟有什么 价值现在完全体现不出来。但是其实这是 Linux 中实现的一种管理设备号的机制,这里利 用了传说中的哈希表(散列表)来管理设备号,哈希表的优点大家知道,便于查找。而我们 的目的是为了通过给定的一个设备号就能迅速得到它所对应的 kobject 指针,而对于块设备 来说,得到 kobject 是为了得到其对应的 gendisk。 事实上,内核提供 kobj_map_init()、kobj_map()以及 kobj_lookup()都是一个系列的,它们都 是为 Linux 设备号管理服务的。首先, kobj_map_init 提供的是一次性服务,它的使命是建立 了 bdev_map这个 struct kobj_map。然后 kobj_map()是每次在 blk_register_region中被调用的, 然而,调用 blk_register_region()的地方可真不少,而我们这个在 add_disk 中调用只是其中之 一,其它的比如 RAID 驱动那边,软驱驱动那边,都会有调用这个 blk_register_region 的需 求,而 kobj_lookup()发生在什么情况下呢?它提供的其实是售后服务。当块设备驱动完成了 初始化工作,当它在内核中站稳了脚跟,会有一个设备文件和它相对应,这个文件会出现在 /dev 目录下。在不久的将来,当 open 系统调用试图打开块设备文件的时候就会调用它,更 准确地说,sys_open 经由 filp_open 然后是 dentry_open(),最终会找到 blkdev_open, blkdev_open 会调用 do_open,do_open()会调用 get_gendisk(),要想明白这个理儿,得先看一 下 dev_t 这个结构。dev_t 实际上就是 u32,也即就是 32 个 bits。前面咱们看到的 MKDEV, MAJOR,都来自 include/linux/kdev_t.h: #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) 通过这几个宏,我们不难看出 dev_t 的意义了,32 个 bits,其中高 12 位被用来记录设备的 主设备号,低 20 位用来记录设备的次设备号。而 MKDEV 就是建立一个设备号。 ma 代表 主设备号,mi 代表次设备号,ma 左移 20 位再和 mi 相或,反过来, MAJOR 就是从 dev 中 取主设备号, MINOR 就是从 dev 中取次设备号。 当一个设备闯入 Linux 的内核时,首先它会有一个居住证号,这就是 dev_t,很显然,每个 人的居住证号不一样,它是唯一的。(为什么不说是身份证号?因为居住证意味着当设备离 开 Linux 系统的时候就可以销毁,所以它更能体现设备的流动性。 )建立一个设备文件的时 候,其设备号是确定的, 而我们每次建立一个文件都会建立一个结构体变量,它就是 struct inode,而 struct inode 拥有成员 dev_t i_dev,所以日后我们从 struct inode 就可以得到其设备 号 dev_t,而这里 kobj_map 这一系列函数使得我们可以从 dev_t 找到对应的 kobject,然后进 一步找到磁盘驱动,我们不可避免的需要访问磁盘对应的 gendisk 结构体指针,而 get_gendisk()就是在这时候粉墨登场的。咱们看到 get_gendisk()的两个参数, dev_t dev 和 int *part,前者就是设备号,而后者传递的是一个指针,这表示什么呢?这表示: 1. 如果这个设备号对应的是一个分区 ,那么 part 变量就用来保存分区的编号。 2. 如果这个设备号对应的是整个设备而不是某个分区 ,那么 part 就只要设置成 0 就行了。 那么得到 gendisk 的目的又是什么呢?我们注意到 struct gendisk 有一个成员,struct block_device_operations *fops,而这个指针才是用来真正执行操作的,每一个块设备驱动都 准备了这么一个结构体,比如咱们在 sd 中定义的那个: static struct block_device_operations sd_fops = { .owner = THIS_MODULE, .open = sd_open, .release = sd_release, .ioctl = sd_ioctl, .getgeo = sd_getgeo, #ifdef CONFIG_COMPAT .compat_ioctl = sd_compat_ioctl, #endif .media_changed = sd_media_changed, .revalidate_disk = sd_revalidate_disk, }; 正是因为有这种关系,我们才能一步一步从 sys_open 最终走到 sd_open,也才能从用户层一 步一步走到块设备驱动层。 5.5.3 关联 block_device 结构 接下来是 register_disk 函数,来自 fs/partitions/check.c: 473 /* Not exported, helper to add_disk(). */ 474 void register_disk(struct gendisk *disk) 475 { 476 struct block_device *bdev; 477 char *s; 478 int i; 479 struct hd_struct *p; 480 int err; 481 482 strlcpy(disk->kobj.name,disk->disk_name,KOBJ_NAME_LEN); 483 /* ewww... some of these buggers have / in name... */ 484 s = strchr(disk->kobj.name, '/'); 485 if (s) 486 *s = '!'; 487 if ((err = kobject_add(&disk->kobj))) 488 return; 489 err = disk_sysfs_symlinks(disk); 490 if (err) { 491 kobject_del(&disk->kobj); 492 return; 493 } 494 disk_sysfs_add_subdirs(disk); 495 496 /* No minors to use for partitions */ 497 if (disk->minors == 1) 498 goto exit; 499 500 /* No such device (e.g., media were just removed) */ 501 if (!get_capacity(disk)) 502 goto exit; 503 504 bdev = bdget_disk(disk, 0); 505 if (!bdev) 506 goto exit; 507 508 /* scan partition table, but suppress uevents */ 509 bdev->bd_invalidated = 1; 510 disk->part_uevent_suppress = 1; 511 err = blkdev_get(bdev, FMODE_READ, 0); 512 disk->part_uevent_suppress = 0; 513 if (err < 0) 514 goto exit; 515 blkdev_put(bdev); 516 517 exit: 518 /* announce disk after possible partitions are already created */ 519 kobject_uevent(&disk->kobj, KOBJ_ADD); 520 521 /* announce possible partitions */ 522 for (i = 1; i < disk->minors; i++) { 523 p = disk->part[i-1]; 524 if (!p || !p->nr_sects) 525 continue; 526 kobject_uevent(&p->kobj, KOBJ_ADD); 527 } 528 } 首先 487 行这个 kobject_add 的作用是很直观的,在 Sysfs 中为这块磁盘建一个子目录,例 如我们为的硬盘建立一个块设备驱动,则会在 /sys/block/目录中看到一个 sdf,要是把这个调 用 kobject_add 函数这行注释掉,肯定就看不到这个 sdf 目录。这里有两个问题: 第一,为什么 kobject_add 这么一调用,生成的这个子目录的名字就叫做“ sdf”,而不叫做 别的呢?其实在 sd_probe 中做过这么一件事情,通过精心计算得到 disk_name 的,而这个 disk_name 正是 struct gendisk 的一个成员,这里我们看到 482 行我们把 disk_name 给了 kobj.name,这就是为什么我们调用 kobject_add 添加一个 kobject 的时候,它的名字就是我们 当时的 disk_name。 第二,为什么生成的这个子目录是在 /sys/block 目录下面,而不是在别的位置呢?还记得在 alloc_disk_node 中我们申请 struct gendisk 的情景么?kobj_set_kset_s(disk,block_subsys)做的 就是让 disk 对应的 kobject 从属于 block_subsys 对应的 kobject 下面。这就是为什么我们现在 添加这个 kobject 的时候,它很自然的就会在 /sys/block 子目录下面建立文件。 继续走, disk_sysfs_symlinks 来自 fs/partitions/check.c,这个函数虽然不短,但是比较浅显易 懂。 static int disk_sysfs_symlinks(struct gendisk *disk){ struct device *target = get_device(disk->driverfs_dev); int err; char *disk_name = NULL; if (target) { disk_name = make_block_name(disk); if (!disk_name) { err = -ENOMEM; goto err_out; } err = sysfs_create_link(&disk->kobj, &target->kobj, "device"); if (err) goto err_out_disk_name; err = sysfs_create_link(&target->kobj, &disk->kobj, disk_name); if (err) goto err_out_dev_link; } err = sysfs_create_link(&disk->kobj, &block_subsys.kobj, "subsystem"); if (err) goto err_out_disk_name_lnk; kfree(disk_name); return 0; err_out_disk_name_lnk: if (target) { sysfs_remove_link(&target->kobj, disk_name); err_out_dev_link: sysfs_remove_link(&disk->kobj, "device"); err_out_disk_name: kfree(disk_name); err_out: put_device(target); } return err; } 我们用实际效果来解读这个函数。首先我们看正常工作的 U 盘会在/sys/block/sdf 下面有哪些 内容: [root@localhost ~]# ls /sys/block/sdf/ capability dev device holders queue range removable size slaves stat subsystem uevent 第一个 sysfs_create_link 创建的就是这里这个 device 这个软链接文件。我们来看它链接到哪 里去了: [root@localhost ~]# ls -l /sys/block/sdf/device lrwxrwxrwx 1 root root 0 Dec 13 07:09 /sys/block/sdf/device -> ../../devices/pci0000:00/0000:00:1d.7/usb4/4-4/4-4:1.0/host24/target24:0:0/24:0:0:0 第二个 sysfs_create_link 则从那边又建立一个反链接,又给链接回来了: [root@localhost~]# ls -l ⋯⋯ lrwxrwxrwx 1 root root 0 Dec 13 21:16 /sys/devices/pci0000:00/0000:00:1d.7/usb4/4-4/4-4:1.0/host24/target24:0:0/24:0:0:0/block:sdf -> ../../../../../../../../../block/sdf 于是这就等于你中有我我中有你,你那边有一个文件链接到了我这边,我这边有一个文件链接到了你那边。第三个 sysfs_create_link,生成的是/sys/block/sdf/subsystem 这个软链接文件。 [root@localhost ~]# ls -l /sys/block/sdf/subsystem lrwxrwxrwx 1 root root 0 Dec 13 07:09 /sys/block/sdf/subsystem -> ../../block 三个链接文件建立好之后,disk_sysfs_symlinks 也就结束了它的使命。接下来一个函数是 disk_sysfs_add_subdirs。同样来自 fs/partitions/check.c: static inline void disk_sysfs_add_subdirs(struct gendisk *disk){ struct kobject *k; k = kobject_get(&disk->kobj); disk->holder_dir = kobject_add_dir(k, "holders"); disk->slave_dir = kobject_add_dir(k, "slaves"); kobject_put(k); } 这个函数的意图太明显了,无非就是建立 holders 和 slaves 两个子目录。 504 行接着调用一个内联函数, bdget_disk,《Thinking in C++》告诉我们内联函数最好定义 在头文件中,所以这个函数来自 include/linux/genhd.h: static inline struct block_device *bdget_disk(struct gendisk *disk, int index){ return bdget(MKDEV(disk->major, disk->first_minor) + index); } bdget 来自 fs/block_dev.c: struct block_device *bdget(dev_t dev){ struct block_device *bdev; struct inode *inode; inode = iget5_locked(bd_mnt->mnt_sb, hash(dev), bdev_test, bdev_set, &dev); if (!inode) return NULL; bdev = &BDEV_I(inode)->bdev; if (inode->i_state & I_NEW) { bdev->bd_contains = NULL; bdev->bd_inode = inode; bdev->bd_block_size = (1 << inode->i_blkbits); bdev->bd_part_count = 0; bdev->bd_invalidated = 0; inode->i_mode = S_IFBLK; inode->i_rdev = dev; inode->i_bdev = bdev; inode->i_data.a_ops = &def_blk_aops; mapping_set_gfp_mask(&inode->i_data, GFP_USER); inode->i_data.backing_dev_info = &default_backing_dev_info; spin_lock(&bdev_lock); list_add(&bdev->bd_list, &all_bdevs); spin_unlock(&bdev_lock); unlock_new_inode(inode); } return bdev; } 这个函数是什么意思呢,还记得前面讲过的 struct block_device 数据结构,以及我们的老熟 人 struct inode 数据结构。不错,Linux 中每一个 Block 设备都由这么一个结构体变量表示, 这玩意儿因此被称作块设备描述符。 inode 我们不多讲,但是这里一个很重要的结构体是 struct bdev_inode: struct bdev_inode { struct block_device bdev; struct inode vfs_inode; }; bdev_inode 好像没出现过,用来干嘛呢?我们来看看 BDEV_I 函数,这个内联函数来自 fs/block_dev.c: static inline struct bdev_inode *BDEV_I(struct inode *inode){ return container_of(inode, struct bdev_inode, vfs_inode); } 很显然,从 inode 得到相应的 bdev_inode。于是这个&BDEV_I(inode)->bdev 表示的就是 inode 对应的 bdev_inode 的成员 struct block_device bdev。 但是 bdev 结构体变量是不会自动来到你的面前,需要的时候你要去申请才会有。 iget5_locked 就是干这件事情的,这个函数来自 fs/inode.c,跟我们前面接触到的 iget 类似。我们显然不 会去深入看它,只能告诉你,这个函数这么一执行,我们就既有 inode 又有 block_device 了, 而且对于第一次申请的 inode,其 i_state 成员是设置了 I_NEW 这个 flag 的,所以 bdget()函 数中,最后一段 if语句是要被执行的。这一段 if语句的作用就是初始化 inode结构体指针inode 以及 block_device 结构体指针 bdev。而函数最终返回的也正是 bdev。需要强调一下,bdev 正是从这一刻开始正式崭露头角的。 回到 register_disk()中,继续往下。下一个重量级的函数是 blkdev_get,来自 fs/block_dev.c: static int __blkdev_get(struct block_device *bdev, mode_t mode, unsigned flags, int for_part){ struct file fake_file = {}; struct dentry fake_dentry = {}; fake_file.f_mode = mode; fake_file.f_flags = flags; fake_file.f_path.dentry = &fake_dentry; fake_dentry.d_inode = bdev->bd_inode; return do_open(bdev, &fake_file, for_part); } int blkdev_get(struct block_device *bdev, mode_t mode, unsigned flags){ return __blkdev_get(bdev, mode, flags, 0); } 看到 blkdev_get 调用的是__blkdev_get,所以我们两个函数一块贴出来了。很显然,真正需 要看的却是 do_open,来自同一个文件,我们来详细讨论一下: 1110 static int do_open(struct block_device *bdev, struct file *file, int for_part) 1111 { 1112 struct module *owner = NULL; 1113 struct gendisk *disk; 1114 int ret = -ENXIO; 1115 int part; 1116 1117 file->f_mapping = bdev->bd_inode->i_mapping; 1118 lock_kernel(); 1119 disk = get_gendisk(bdev->bd_dev, &part); /* part 肯定为 0 */ 1120 if (!disk) { 1121 unlock_kernel(); 1122 bdput(bdev); 1123 return ret; 1124 } 1125 owner = disk->fops->owner; 1126 1127 mutex_lock_nested(&bdev->bd_mutex, for_part); 1128 if (!bdev->bd_openers) { 1129 bdev->bd_disk = disk; 1130 bdev->bd_contains = bdev; 1131 if (!part) { 1132 struct backing_dev_info *bdi; 1133 if (disk->fops->open) { 1134 ret = disk->fops->open(bdev->bd_inode, file); 1135 if (ret) 1136 goto out_first; 1137 } 1138 if (!bdev->bd_openers) { 1139 bd_set_size(bdev,(loff_t)get_capacity(disk)<<9); 1140 bdi = blk_get_backing_dev_info(bdev); 1141 if (bdi == NULL) 1142 bdi = &default_backing_dev_info; 1143 bdev->bd_inode->i_data.backing_dev_info = bdi; 1144 } 1145 if (bdev->bd_invalidated) 1146 rescan_partitions(disk, bdev); 1147 } else { 1148 struct hd_struct *p; 1149 struct block_device *whole; 1150 whole = bdget_disk(disk, 0); 1151 ret = -ENOMEM; 1152 if (!whole) 1153 goto out_first; 1154 BUG_ON(for_part); 1155 ret = __blkdev_get(whole, file->f_mode, file->f_flags, 1); 1156 if (ret) 1157 goto out_first; 1158 bdev->bd_contains = whole; 1159 p = disk->part[part - 1]; 1160 bdev->bd_inode->i_data.backing_dev_info = 1161 whole->bd_inode->i_data.backing_dev_info; 1162 if (!(disk->flags & GENHD_FL_UP) || !p || !p->nr_sects) { 1163 ret = -ENXIO; 1164 goto out_first; 1165 } 1166 kobject_get(&p->kobj); 1167 bdev->bd_part = p; 1168 bd_set_size(bdev, (loff_t) p->nr_sects << 9); 1169 } 1170 } else { 1171 put_disk(disk); 1172 module_put(owner); 1173 if (bdev->bd_contains == bdev) { 1174 if (bdev->bd_disk->fops->open) { 1175 ret = bdev->bd_disk->fops->open(bdev->bd_inode, file); 1176 if (ret) 1177 goto out; 1178 } 1179 if (bdev->bd_invalidated) 1180 rescan_partitions(bdev->bd_disk, bdev); 1181 } 1182 } 1183 bdev->bd_openers++; 1184 if (for_part) 1185 bdev->bd_part_count++; 1186 mutex_unlock(&bdev->bd_mutex); 1187 unlock_kernel(); 1188 return 0; 1189 1190 out_first: 1191 bdev->bd_disk = NULL; 1192 bdev->bd_inode->i_data.backing_dev_info = &default_backing_dev_info; 1193 if (bdev != bdev->bd_contains) 1194 __blkdev_put(bdev->bd_contains, 1); 1195 bdev->bd_contains = NULL; 1196 put_disk(disk); 1197 module_put(owner); 1198 out: 1199 mutex_unlock(&bdev->bd_mutex); 1200 unlock_kernel(); 1201 if (ret) 1202 bdput(bdev); 1203 return ret; 1204 } 一开始的时候, bd_openers是被初始化为了 0,所以1128这个if语句是要被执行的。 bd_openers 为 0 表示一个文件还没有被打开过。 一开始我们还没有涉及到分区的信息,所以一开始我们只有 sda 这个概念,而没有 sda1,sda2, sda3⋯这些概念。这时候我们调用 get_gendisk 得到的 part 一定是 0。所以 1131 行的 if 语句 也会执行。而 disk->fops->open 很明显,就是 sd_open。(因为我们在 sd_probe 中曾经设置了 gd->fops 等于&sd_fops) 但此时此刻我们执行 sd_open实际上是不做什么正经事儿的。顶多就是测试一下看看 sd_open 能不能执行,如果能执行,那么就返回 0。如果根本就不能执行,那就赶紧汇报错误。 接下来还有几个函数,主要做一些赋值,暂时不去看它,等到适当的时候需要看了再回来看。 而 1146 行这个 rescan_partitions()显然是我们要看的,首先我们在调用 blkdev_get 之前把 bd_invalidated 设置为了 1,所以这个函数这次一定会被执行。从这一刻开始分区信息闯入了 我们的生活。这个函数来自 fs/partitions/check.c: 530 int rescan_partitions(struct gendisk *disk, struct block_device *bdev) 531 { 532 struct parsed_partitions *state; 533 int p, res; 534 535 if (bdev->bd_part_count) 536 return -EBUSY; 537 res = invalidate_partition(disk, 0); 538 if (res) 539 return res; 540 bdev->bd_invalidated = 0; 541 for (p = 1; p < disk->minors; p++) 542 delete_partition(disk, p); 543 if (disk->fops->revalidate_disk) 544 disk->fops->revalidate_disk(disk); 545 if (!get_capacity(disk) || !(state = check_partition(disk, bdev))) 546 return 0; 547 if (IS_ERR(state)) /* I/O error reading the partition table */ 548 return -EIO; 549 for (p = 1; p < state->limit; p++) { 550 sector_t size = state->parts[p].size; 551 sector_t from = state->parts[p].from; 552 if (!size) 553 continue; 554 if (from + size > get_capacity(disk)) { 555 printk(" %s: p%d exceeds device capacity\n", 556 disk->disk_name, p); 557 } 558 add_partition(disk, p, from, size, state->parts[p].flags); 559 #ifdef CONFIG_BLK_DEV_MD 560 if (state->parts[p].flags & ADDPART_FLAG_RAID) 561 md_autodetect_dev(bdev->bd_dev+p); 562 #endif 563 } 564 kfree(state); 565 return 0; 566 } 这个函数执行过后,关于分区的信息我们就算都有了。关于分区,前面我们看到是用 struct hd_struct 这么个结构体来表示的,而 struct hd_struct 也正是 struct gendisk 的成员,并且是个 二级指针。接着, get_capacity()。没有比这个函数更简单的函数了。来自 include/linux/genhd.h: static inline sector_t get_capacity(struct gendisk *disk){ return disk->capacity; } 而 check_partition 就稍微复杂一些了,来自 fs/partitions/check.c,我们就不多讲了,这个函数 主要是利用 parsed_partitions 数据结构来记录分区信息的,并且调用 check_part 来专门指定 一个分区表格式,然后我们就来到了 add_partition,仍然是来自 fs/partitions/check.c: 371 void add_partition(struct gendisk *disk, int part, sector_t start, sector_t len, int flags) 372 { 373 struct hd_struct *p; 374 375 p = kmalloc(sizeof(*p), GFP_KERNEL); 376 if (!p) 377 return; 378 379 memset(p, 0, sizeof(*p)); 380 p->start_sect = start; 381 p->nr_sects = len; 382 p->partno = part; 383 p->policy = disk->policy; 384 385 if (isdigit(disk->kobj.name[strlen(disk->kobj.name)-1])) 386 snprintf(p->kobj.name,KOBJ_NAME_LEN,"%sp%d",disk->kobj.name,part); 387 else 388 snprintf(p->kobj.name,KOBJ_NAME_LEN,"%s%d",disk->kobj.name,part); 389 p->kobj.parent = &disk->kobj; 390 p->kobj.ktype = &ktype_part; 391 kobject_init(&p->kobj); 392 kobject_add(&p->kobj); 393 if (!disk->part_uevent_suppress) 394 kobject_uevent(&p->kobj, KOBJ_ADD); 395 sysfs_create_link(&p->kobj, &block_subsys.kobj, "subsystem"); 396 if (flags & ADDPART_FLAG_WHOLEDISK) { 397 static struct attribute addpartattr = { 398 .name = "whole_disk", 399 .mode = S_IRUSR | S_IRGRP | S_IROTH, 400 .owner = THIS_MODULE, 401 }; 402 403 sysfs_create_file(&p->kobj, &addpartattr); 404 } 405 partition_sysfs_add_subdir(p); 406 disk->part[part-1] = p; 407 } 有了之前的经验,现在再看这些 kobject 相关的,sysfs 相关的函数就容易多了。 389 行这个 p->kobj.parent = &disk->kobj 保证了我们接下来生成的东西在刚才的目录之下, 即 sda1、sda2、⋯ 在 sda 目录下。而 395 行 sysfs_create_link 的效果也很显然。而 partition_sysfs_add_subdir 也没什么好说的,添加了 holders 子目录。 最后,让我们记住这个函数做过的一件事情,对 p 的各个成员进行了赋值,而在函数的结尾 处把 disk->part[part-1]指向了 p。也就是说,从此以后, struct hd_struct 这个指针数组里就应 该有内容了,而不再是空的。 到这里,rescan_partitions()宣告结束,回到 do_open()中.1183 行,让 bd_openers 这个引用计 数增加 1,如果 for_part 有值,那么就让它对应的引用计数也加 1。然后 do_open 也就华丽 丽的结束了,像多米诺骨牌一样,__blkdev_get 和 blkdev_get 相继返回。blkdev_put 和 blkdev_get 做的事情基本相反,我们就不看了,只是需要注意,它把刚才增加上去的这两个 引用计数给减了回去。 最后,register_disk()中调用的最后一个函数就是 kobject_uevent(),这个函数就是通知用户空 间的进程 udevd,告诉它有事件发生了,如果你使用的发行版正确配置了 udev 的配置文件(详 见/etc/udev/目录下),那么其效果就是让 /dev 目录下面有了相应的设备文件。比如: [root@localhost tedkdb]# ls /dev/sda* /dev/sda /dev/sda10 /dev/sda12 /dev/sda14 /dev/sda2 /dev/sda5 /dev/sda7 /dev/sda9 /dev/sda1 /dev/sda11 /dev/sda13 /dev/sda15 /dev/sda3 /dev/sda6 /dev/sda8 至于为什么,你可以去阅读关于 udev 的知识,这是用户空间的程序,咱们就不多说了。 5.5.4 为设备建立请求队列 好啦,磁盘和分区建立好了, block_device 数据结构也关联起来了,回到 add_disk 中,我们 需要调用第三个函数了,也就是 blk_register_queue(disk),来建立请求队列与 bio 等数据结构 了,让我们来仔细分析。 4079 int blk_register_queue(struct gendisk *disk) 4080 { 4081 int ret; 4082 4083 request_queue_t *q = disk->queue; 4084 4085 if (!q || !q->request_fn) 4086 return -ENXIO; 4087 4088 q->kobj.parent = kobject_get(&disk->kobj); 4089 4090 ret = kobject_add(&q->kobj); 4091 if (ret < 0) 4092 return ret; 4093 4094 kobject_uevent(&q->kobj, KOBJ_ADD); 4095 4096 ret = elv_register_queue(q); 4097 if (ret) { 4098 kobject_uevent(&q->kobj, KOBJ_REMOVE); 4099 kobject_del(&q->kobj); 4100 return ret; 4101 } 4102 4103 return 0; 4104 } 首先,4090 行这个 kobject_add 很好解释,在/sys/block/sda/目录下面又多一个子目录而已, 但问题是,这个 q 究竟是什么?这里我们把 disk->queue 赋给了它,而 disk->queue 又是什么 呢?回过头去看 sd_probe(),当时我们有这么一句: gd->queue = sdkp->device->request_queue; 而 sdkp 是 struct scsi_disk 结构体指针,其 device 成员是 struct scsi_device 指针,那么这个 request_queue 则是 struct request_queue 结构体指针,表示的是一个请求队列。在 scsi 的 probe 函数,也就是 sd_probe 的被调用之前,核心层实际上已经为它们做了许多工作了。这里涉 及到 scsi 总线驱动的概念,我们将在“块设备驱动层的处理”一节中详细介绍这一过程,这 里只提一下,这个过程中会调用 scsi_probe_and_add_lun 为对应 scsi 磁盘申请一个 scsi_device 结构体变量, 为它的一些成员赋好了值,这其中就包括了这个请求队列。 准确地说,在 scsi 总线扫描的时候,每当探测到一个设备, scsi_probe_and_add_lun 函数会 通过 scsi_alloc_sdev 函数申请一个 scsi_device,scsi_alloc_sdev 会调用 scsi_alloc_queue 初始 化这个块设备的 request_queue。而这个函数涉及到很多 block 层提供的函数,所以我们不得 不从这里开始看起,来自 drivers/scsi/scsi_lib.c: 1569 struct request_queue *__scsi_alloc_queue(struct Scsi_Host *shost, 1570 request_fn_proc *request_fn) 1571 { 1572 struct request_queue *q; 1573 1574 q = blk_init_queue(request_fn, NULL); 1575 if (!q) 1576 return NULL; 1577 1578 blk_queue_max_hw_segments(q, shost->sg_tablesize); 1579 blk_queue_max_phys_segments(q, SCSI_MAX_PHYS_SEGMENTS); 1580 blk_queue_max_sectors(q, shost->max_sectors); 1581 blk_queue_bounce_limit(q, scsi_calculate_bounce_limit(shost)); 1582 blk_queue_segment_boundary(q, shost->dma_boundary); 1583 1584 if (!shost->use_clustering) 1585 clear_bit(QUEUE_FLAG_CLUSTER, &q->queue_flags); 1586 return q; 1587 } 1588 EXPORT_SYMBOL(__scsi_alloc_queue); 1589 1590 struct request_queue *scsi_alloc_queue(struct scsi_device *sdev) 1591 { 1592 struct request_queue *q; 1593 1594 q = __scsi_alloc_queue(sdev->host, scsi_request_fn); 1595 if (!q) 1596 return NULL; 1597 1598 blk_queue_prep_rq(q, scsi_prep_fn); 1599 blk_queue_issue_flush_fn(q, scsi_issue_flush_fn); 1600 blk_queue_softirq_done(q, scsi_softirq_done); 1601 return q; 1602 } 这两个函数因为调用关系所以一并贴了出来。我们首先要看的很自然就是 blk_init_queue(), 它来自 block/ll_rw_blk.c: 1893 request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock) 1894 { 1895 return blk_init_queue_node(rfn, lock, -1); 1896 } 1897 EXPORT_SYMBOL(blk_init_queue); 1898 1899 request_queue_t * 1900 blk_init_queue_node(request_fn_proc *rfn, spinlock_t *lock, int node_id) 1901 { 1902 request_queue_t *q = blk_alloc_queue_node(GFP_KERNEL, node_id); 1903 1904 if (!q) 1905 return NULL; 1906 1907 q->node = node_id; 1908 if (blk_init_free_list(q)) { 1909 kmem_cache_free(requestq_cachep, q); 1910 return NULL; 1911 } 1912 1913 /* 1914 * if caller didn't supply a lock, they get per-queue locking with 1915 * our embedded lock 1916 */ 1917 if (!lock) { 1918 spin_lock_init(&q->__queue_lock); 1919 lock = &q->__queue_lock; 1920 } 1921 1922 q->request_fn = rfn; 1923 q->prep_rq_fn = NULL; 1924 q->unplug_fn = generic_unplug_device; 1925 q->queue_flags = (1 << QUEUE_FLAG_CLUSTER); 1926 q->queue_lock = lock; 1927 1928 blk_queue_segment_boundary(q, 0xffffffff); 1929 1930 blk_queue_make_request(q, __make_request); 1931 blk_queue_max_segment_size(q, MAX_SEGMENT_SIZE); 1932 1933 blk_queue_max_hw_segments(q, MAX_HW_SEGMENTS); 1934 blk_queue_max_phys_segments(q, MAX_PHYS_SEGMENTS); 1935 1936 q->sg_reserved_size = INT_MAX; 1937 1938 /* 1939 * all done 1940 */ 1941 if (!elevator_init(q, NULL)) { 1942 blk_queue_congestion_threshold(q); 1943 return q; 1944 } 1945 1946 blk_put_queue(q); 1947 return NULL; 1948 } 别看这些函数都很可怕,正我们目前需要关注的其实只是其中的某几个而已。它们这个 blk_alloc_queue_node 和 elevator_init() 。 前 者 来 自 block/ll_rw_blk.c , 后 者 则 来 自 block/elevator.c: 1836 request_queue_t *blk_alloc_queue_node(gfp_t gfp_mask, int node_id) 1837 { 1838 request_queue_t *q; 1839 1840 q = kmem_cache_alloc_node(requestq_cachep, gfp_mask, node_id); 1841 if (!q) 1842 return NULL; 1843 1844 memset(q, 0, sizeof(*q)); 1845 init_timer(&q->unplug_timer); 1846 1847 snprintf(q->kobj.name, KOBJ_NAME_LEN, "%s", "queue"); 1848 q->kobj.ktype = &queue_ktype; 1849 kobject_init(&q->kobj); 1850 1851 q->backing_dev_info.unplug_io_fn = blk_backing_dev_unplug; 1852 q->backing_dev_info.unplug_io_data = q; 1853 1854 mutex_init(&q->sysfs_lock); 1855 1856 return q; 1857 } 还记得块设备初始化的那个 blk_dev_init 吧,当时我们调用 kmem_cache_create()申请了一个 slab 内存分配器 request_cachep,现在就该用它了。从这个分配器里申请了一个 struct request_queue_t 结构体的空间,给了指针 q,然后 1844 行初始化为 0。而 1847 行让 q 的 kobj.name 等于“queue”,这就是为什么今后我们在/sys/block/sda/目录下面能看到一个叫做 “queue”的目录。 而这个 queue 目录下面的内容是什么呢? [root@localhost ~]# ls /sys/block/sda/queue/ iosched max_hw_sectors_kb max_sectors_kb nr_requests read_ahead_kb scheduler 这几个文件从哪来的?注意 1848 行那个 queue_ktype: static struct kobj_type queue_ktype = { .sysfs_ops = &queue_sysfs_ops, .default_attrs = default_attrs, .release = blk_release_queue, }; 这些就是定义了一些属性, kobject 的属性。不过有一个东西例外,它就是 iosched,这不是 一个文件,这是一个目录: [root@localhost ~]# ls /sys/block/sdf/queue/iosched/ back_seek_max fifo_expire_async quantum slice_async_rq slice_sync back_seek_penalty fifo_expire_sync slice_async slice_idle 关于这个目录,我们需要分析 blk_init_queue_node 函数中的另一个函数, elevator_init()。要 弄清这个函数,需要学习块设备 I/O 调度层的核心—— I/O 调度算法。下面我们就一同进入。 5.5.5 块设备 I/O 调度程序 我们建立请求队列建的目录是,当向请求队列增加一条新的请求,即产生一个 request 数据 结构时,通用块层会调用 I/O 调度程序来确定该新 request 将在请求队列中的确切位置。 I/O 调度程序试图通过扇区将请求队列排序。如果顺序地从链表中提取要处理的请求,那么就会 明显减少磁头寻道的次数,因为磁头是按照直线的方式从内磁道移向外磁道(反之亦然), 而不是随意地从一个磁道跳跃到另一个磁道。 这就是著名的电梯算法,回想一下,电梯算法处理来自不同层的上下请求。电梯是往一个方 向移动的;当朝一个方向上的最后一个预定层到达时,电梯就会改变方向而开始向相反的方 向移动。因此,块设备 I/O 调度程序也被称为电梯算法( elevator)。 在重负载和磁盘操作频繁的情况下,固定数目的动态空闲内存将成为进程想要把新请求加入 请求队列 q 的瓶颈。为了解决这种问题,每个 request_queue 描述符包含一个 request_list 数 据结构: struct request_list { int count[2]; //两个计数器,分别用于记录分配给 READ 和 WRITE 请求的请求描述符数 int starved[2]; //两个标志,分别用于标记为读或写请求的分配是否失败 int elvpriv; // mempool_t *rq_pool; //一个指针,指向请求描述符的内存池 wait_queue_head_t wait[2]; //两个等待队列, //分别存放了为获得空闲的读和写请求描述符而睡眠的进程。 }; 我们可以通过 blk_get_request()函数从一个特定请求队列的内存池中获得一个空闲的请求描 述符;如果内存区不足并且内存池已经用完,则要么挂起当前进程,要么返回 NULL(如果 不能阻塞内核控制路径)。如果分配成功,则将请求队列的 request_list 数据结构的地址存放 在请求描述符的 r1 字段中。 blk_put_request()函数则释放一个请求描述符;如果该描述符的 引用计数器的值为 0,则将描述符归还回它原来所在的内存池。 每个请求队列都有一个允许处理的最大请求数。请求队列描述符的 nr_requests 字段存放了 每个数据传送方向所允许处理的最大请求数。缺省情况下,一个队列至多有 128 个待处理读 请求和 128 个待处理写请求。如果待处理的读(写)请求数超过了 nr_requests,那么通过设 置请求队列描述符 request_queue数据结构的queue_flags字段的QUEUE_FLAG_READFULL (QUEUE_FLAG_WRITEFULL)标志将该队列标记为已满,试图把请求加入到某个传送方 向的可阻塞进程被放置到 request_list 结构所对应的等待队列中睡眠。 一个填满的请求队列对系统性能有负面影响,因为它会强制许多进程去睡眠以等待 I/O 数据 传送的完成。因此,如果给定传送方向上的待处理请求数超过了存放在请求描述符的 nr_congestion_on 字段中的值(缺省值为 113),那么内核认为该队列是拥塞的,并试图降低 新请求的创建速率。当待处理请求数小于 nr_congestion_off 的值(缺省值为 111)时,拥塞的 请求队列才变为不拥塞。 blk_congestion_wait()函数挂起当前进程直到所有请求队列都变为不 拥塞或超时已到。 我们前面谈到,延迟激活块设备驱动程序有利于把相邻块的请求进行集中。这种延迟是通过 所谓的设备插入和设备拔出技术来实现的,其实,我们更愿意用激活-不激活来描述。在块 设备驱动程序被插入时,该驱动程序并不被激活,即使在驱动程序队列中有待处理的请求。 blk_plug_device()函数的功能是插入一个块设备——更准确地说,插入到某个块备驱动程序 处理的请求队列中。本质上,该函数接收一个请求队列描述符的地址 q 作为其参数。它设置 q->queue_flags 字段中的 QUEUE_FLAG_PLUGGED 位;然后,重新启动 q->unplug_timer 字段中的内嵌动态定时器。 blk_remove_plug()则拔去一个请求队列 q:清除 QUEUE_FLAG_PLUGGED 标志并取消 q->unplug_timer 动态定时器的执行。当“视线中”所有可合并的请求都被加入请求队列时, 内核就会显式地调用该函数。此外,如果请求队列中待处理的请求数超过过了请求队列描述 符的 unplug_thresh 字段中存放的值(缺省值为 4),那么 I/O 调度程序也会去掉该请求队列。 如果一个设备保持插入的时间间隔为 q->unplug_delay( 通 常 为 3ms ),那 么 说 明 blk_plug_device()函数激活的动态定时器的时间已用完,因此就会执行 blk_unplug_timeout() 函数。因而,唤醒内核线程 kblockd 所操作的工作队列 kblockd_workqueue。kblockd 执行 blk_unplug_work()函数,其地址存放在 q->unplug_work 结构中。接着,该函数会调用请求队 列 中 的 q->unplug_fn 方 法 , 通 常 该 方 法 是 由 generic_unplug_device() 函 数 实 现 的 。generic_unplug_device()函数的功能是拔出块设备:首先,检查请求队列是否仍然活跃;然后, 调用 blk_remove_plug()函数;最后,执行策略例程 request_fn 方法来开始处理请求队列中的 下一个请求。 总结一下就是: 1. blk_plug_device()负责戒严。 2. blk_remove_plug()负责解禁。 3. 但是戒严这东西吧,也是有时间限制的,所以在戒严的时候,设了一个定时器, unplug_timer,一旦时间到了就自动执行 blk_remove_plug 去解禁。 4. 而在解禁的时候就不要忘记把这个定时器给关掉 .(即 del_timer) 5. 解禁之后调用 request_fn()开始处理队列中的下一个请求,或者说车流开始恢复前行。 这样我们就算是明白这两个戒严与解禁的函数了。最后,题外话,关于 unplug 和 plug,我 觉得更贴切的单词是 activate 和 deactivate,或者说激活与冻结,或者简单的说,开与关。 编写这两个函数究竟是为了什么呢?不妨这样理解,假设你经常开车经过长安街,你会发现 经常有戒严的现象发生,比如某位领导人要出行,比如某位领导人要来访,而你可以把 blk_plug_device()想象成戒严,把 blk_remove_plug 想象成开放。车流要想行进,前提条件是 没有戒严,换言之,没有设卡,而 QUEUE_FLAG_PLUGGED 这个 flag 就相当于“卡”,设 了它队列就不能前进了,没有设才有可能前进。之所以需要设卡,是因为确实有这个需求, 有时候确实不想让队列前进,因为延迟激活块设备驱动程序有利于把相邻块的请求进行集中。 在重负载情况下,严格遵循扇区号顺序的 I/O 调度算法运行的并不是很好。因为,数据传送 的完成时间主要取决于磁盘上数据的物理位置。因此,如果设备驱动程序处理的请求位于队 列的首部(小扇区号),并且拥有小扇区号的新请求不断被加入队列中,那么队列末尾的请 求就很容易会饿死。因而 I/O 调度算法会非常复杂。 当前的 I/O 调度程序或电梯算法很多,也给了人们扩展电梯算法的多种方法。 Linux 2.6 中自 带了四种不同类型的电梯算法,分别为“预期( Anticipatory)”算法、“最后期限( Deadline)” 算法、“ CFQ (Complete Fairness Queueing,完全公平队列)”算法以及“ Noop (No Operation)” 算法。对大多数块设备而言,内核使用的缺省电梯算法可在引导时通过内核参数 elevator=进行再设置,其中 值可取下列任何一个: as、deadline、cfq 和 noop。 如果没有给定引导参数,那么内核缺省使用“预期” I/O 调度程序。总之,设备驱动程序可 以用任何一个调度程序取代缺省的电梯算法;设备驱动程序也可以自己定制 I/O 调度算法, 但是这种情况很少见。 此外,系统管理员可以在运行时为一个特定的块设备改变 I/O 调度程序。例如,为了改变第 一个 IDE 通道的主磁盘所使用的 I/O 调度程序,管理员可把一个电梯算法的名称写入 sysfs 特殊文件系统的 /sys/block/hda/queue/scheduler 文件中。 清求队列中使用的 I/O 调度算法是由一个 elevator_t 类型的 elevator 对象表示的;请求队列描 述符 request_queue 的 elevator 字段指向一个 elevator 对象。elevator 对象的 elevator_ops 包含 了几个方法,它们覆盖了 elevator 所有可能的操作:链接和断开 elevator,增加和合并队列 中的请求,从队列中删除请求,获得队列中下一个待处理的请求等等。 elevator 对象也存放了一个表的地址,表中包含了处理请求队列所需的所有信息。而且,每个请求描述符包含一 个 elevator_private 字段,该字段指向一个由 I/O 调度程序用来处理请求的附加数据结构。 现在我们从易到难简要地介绍一下四种 I/O 调度算法。注意,设计一个 I/O 调度程序与设计 一个 CPU 调度程序很相似:启发算法和采用的常量值是测试和基准外延量的结果。 一般而言,所有的算法都使用一个调度队列( dispatch queue),队列中包含的所有请求按照 设备驱动程序应当处理的顺序进行排序—也即设备驱动程序要处理的下一个请求通常是调 度队列中的第一个元素。调度队列实际上是由请求队列描述符的 queue_head 字段所确定的 请求队列。几乎所有的算法都使用另外的队列对请求进行分类和排序。它们允许设备驱动程 序将 bio 结构增加到已存在请求中,如果需要,还要合并两个“相邻的”请求。 “Noop”算法 这是最简单的 I/O 调度算法。它没有排序的队列:新的请求通常被插在调度队列的开头或 末尾,下一个要处理的请求总是队列中的第一个请求。 “CFQ”算法 "CFQ(完全公平队列)”算法的主要目标是在触发 I/O 请求的所有进程中确保磁盘 I/O 带宽 的公平分配。为了达到这个目标,算法使用许多个排序队列——缺省为 64——它们存放了 不同进程发出的请求。当算法处理一个请求时,内核调用一个散列函数将当前进程的线程组 标识符(通常,它对应其 PID;然后,算法将一个新的请求插人该队列的末尾。因此,同一 个进程发出的请求通常被插入相同的队列中。 为了再填充调度队列,算法本质上采用轮询方式扫描 I/O 输入队列,选择第一个非空队列, 然后将该队列中的一组请求移动到调度队列的末尾。 “最后期限”算法 除了调度队列外,“最后期限”算法还使用了四个队列。其中的两个排序队列分别包含读请 求和写请求,其中的请求是根据起始扇区号排序的。另外两个最后期限队列包含相同的读和 写请求,但这是根据它们的“最后期限”排序的。引人这些队列是为了避免请求饿死,由于 电梯策略优先处理与上一个所处理的请求最近的请求,因而就会对某个请求忽略很长一段时 间,这时就会发生这种情况。请求的最后期限本质上就是一个超时定时器,当请求被传给电 梯算法时开始计时。缺省情况下,读请求的超时时间是 500ms,写请求的超时时间是 5s— —读请求优先于写请求,因为读请求通常阻塞发出请求的进程。最后期限保证了调度程序照 顾等待很长一段时间的那个请求,即使它位于排序队列的末尾。 当算法要补充调度队列时,首先确定下一个请求的数据方向。如果同时要调度读和写两个请 求,算法会选择“读”方向,除非该“写”方向已经被放弃很多次了(为了避免写请求饿死)。 接下来,算法检查与被选择方向相关的最后期限队列:如果队列中的第一个请求的最后期限 已用完,那么算法将该请求移到调度队列的末尾。同时,它也会移动该过期的请求后面的一组来自排序队列的相同扇区号的请求。如果将要移动的请求在磁盘上物理相邻,那么这一批 队列的长度会很长,否则就很短。 最后,如果没有请求超时,算法对来自于排序队列的最后一个请求连带之后的一组相同扇区 的请求进行调度。当指针到达排序队列的末尾时,搜索又从头开始(“单方向算法”)。 “预期”算法 “预期”算法是 Linux 提供的最复杂的一种 1/O 调度算法。基本上,它是“最后期限”算法 的一个演变,借用了“最后期限”算法的基本机制:两个最后期限队列和两个排序队列; I/O 调度程序在读和写请求之间交互扫描排序队列,不过更倾向于读请求。扫描基本上是连续的, 除非有某个请求超时。读请求的缺省超时时间是 125ms,写请求的缺省超时时间是 250ms。 但是,该算法还遵循一些附加的启发式准则: 有些情况下,算法可能在排序队列当前位置之后选择一个请求,从而强制磁头从后搜索。这 种情况通常发生在这个请求之后的搜索距离小于在排序队列当前位置之后对该请求搜索距 离的一半时。 算法统计系统中每个进程触发的 I/O 操作的种类。当刚刚调度了由某个进程 p 发出的一个读 请求之后,算法马上检查排序队列中的下一个请求是否来自同一个进程 p。如果是,立即调 度下一个请求。否则,查看关于该进程 p 的统计信息:如果确定进程 p 可能很快发出另一个 读请求,那么就延迟一小段时间(缺省大约为 7ms)。因此,算法预测进程 p 发出的读请求 与刚被调度的请求在磁盘上可能是“近邻”。 好了,有这些来自 ULK-3 的基础知识后,来看 elevator_init 函数,来自 block/elevator.c: 220 int elevator_init(request_queue_t *q, char *name) 221 { 222 struct elevator_type *e = NULL; 223 struct elevator_queue *eq; 224 int ret = 0; 225 void *data; 226 227 INIT_LIST_HEAD(&q->queue_head); 228 q->last_merge = NULL; 229 q->end_sector = 0; 230 q->boundary_rq = NULL; 231 232 if (name && !(e = elevator_get(name))) 233 return -EINVAL; 234 235 if (!e && *chosen_elevator && !(e = elevator_get(chosen_elevator))) 236 printk("I/O scheduler %s not found\n", chosen_elevator); 237 238 if (!e && !(e = elevator_get(CONFIG_DEFAULT_IOSCHED))) { 239 printk("Default I/O scheduler not found, using no-op\n"); 240 e = elevator_get("noop"); 241 } 242 243 eq = elevator_alloc(q, e); 244 if (!eq) 245 return -ENOMEM; 246 247 data = elevator_init_queue(q, eq); 248 if (!data) { 249 kobject_put(&eq->kobj); 250 return -ENOMEM; 251 } 252 253 elevator_attach(q, eq, data); 254 return ret; 255 } 重点关注 elevator_alloc(): 179 static elevator_t *elevator_alloc(request_queue_t *q, struct elevator_type *e) 180 { 181 elevator_t *eq; 182 int i; 183 184 eq = kmalloc_node(sizeof(elevator_t), GFP_KERNEL, q->node); 185 if (unlikely(!eq)) 186 goto err; 187 188 memset(eq, 0, sizeof(*eq)); 189 eq->ops = &e->ops; 190 eq->elevator_type = e; 191 kobject_init(&eq->kobj); 192 snprintf(eq->kobj.name, KOBJ_NAME_LEN, "%s", "iosched"); 193 eq->kobj.ktype = &elv_ktype; 194 mutex_init(&eq->sysfs_lock); 195 196 eq->hash = kmalloc_node(sizeof(struct hlist_head) * ELV_HASH_ENTRIES, 197 GFP_KERNEL, q->node); 198 if (!eq->hash) 199 goto err; 200 201 for (i = 0; i < ELV_HASH_ENTRIES; i++) 202 INIT_HLIST_HEAD(&eq->hash[i]); 203 204 return eq; 205 err: 206 kfree(eq); 207 elevator_put(e); 208 return NULL; 209 } 无非就是申请一个 struct elevator_t 结构体变量的空间并且初始化为 0。 而真正引发我们兴趣的是 192行,很显然,就是因为这里把 eq的kobj的name设置为”iosched”, 才会让我们在 queue 目录下看到那个“iosched”子目录。 而这个子目录下那些乱七八糟的文件又来自哪里呢?正是下面这个 elv_register_queue()函数, 这个正是我们在 blk_register_queue()中调用的那个函数: 931 int elv_register_queue(struct request_queue *q) 932 { 933 elevator_t *e = q->elevator; 934 int error; 935 936 e->kobj.parent = &q->kobj; 937 938 error = kobject_add(&e->kobj); 939 if (!error) { 940 struct elv_fs_entry *attr = e->elevator_type->elevator_attrs; 941 if (attr) { 942 while (attr->attr.name) { 943 if (sysfs_create_file(&e->kobj, &attr->attr)) 944 break; 945 attr++; 946 } 947 } 948 kobject_uevent(&e->kobj, KOBJ_ADD); 949 } 950 return error; 951 } 936 行保证了, iosched 是出现在 queue 目录下而不是出现在别的地方,而 942 行这个 while 循环则是创建 iosched 目录下面那么多文件的。我们先来看这个 attr 到底是什么,这里它指 向了 e->elevator_type->elevator_attrs,而在刚才那个 elevator_alloc()函数中,190 行,我们看 到了 eq->elevator_type 被赋上了 e,回溯至 elevator_init(),我们来看 e 究竟是什么? 首先,当我们在 blk_init_queue_node()中调用 elevator_init 的时候,传递的第二个参数是 NULL, 即 name 指针是 NULL。 那么很明显,235行和238行这两个 if语句对于elevator_type 类型的e变量的取值至关重要。 它就是我们所谓的电梯算法。具体使用哪种算法我们可以在启动的时候通过内核参数 elevator 来指定,比如在我的 grub 配置文件中就这样设置过: ###Don't change this comment - YaST2 identifier: Original name: linux### title Linux kernel (hd0,0)/vmlinuz root=/dev/sda3 selinux=0 resume=/dev/sda2 splash=silent elevator=cfq showopts console=ttyS0,9600 console=tty0 initrd (hd0,0)/initrd 让 elevator=cfq,因此 cfq 算法将是我们的 IO 调度器所采用的算法。而另一方面我们也可以 单独的为某个设备指定它所采用的 IO 调度算法,这就通过修改在 /sys/block/sda/queue/目录 下面的 scheduler 文件。比如我们可以先看一下我的这块硬盘 : [root@localhost ~]# cat /sys/block/sda/queue/scheduler noop anticipatory deadline [cfq] 可以看到我们这里采用的是 cfq。现在暂时不去细说怎样使用这几种算法,我们接着刚才的 话题,还看 elevator_init()。 首先 chosen_elevator 是定义于 block/elevator.c 中的一个字符串. 160 static char chosen_elevator[16]; 这个字符串就是用来记录启动参数 elevator 的。如果没有设置,那就没有值。 而 CONFIG_DEFAULT_IOSCHED 是一个编译选项。它就是一字符串,在编译内核的时候设 置的,比如我的是 cfq。 119 CONFIG_DEFAULT_IOSCHED="cfq" 你当然也可以选择其它三个,看个人喜好了,喜欢哪个就选择哪个,总之这个字符串会传递 给 elevator_get 这个来自 block/elevator.c 的函数: 133 static struct elevator_type *elevator_get(const char *name) 134 { 135 struct elevator_type *e; 136 137 spin_lock(&elv_list_lock); 138 139 e = elevator_find(name); 140 if (e && !try_module_get(e->elevator_owner)) 141 e = NULL; 142 143 spin_unlock(&elv_list_lock); 144 145 return e; 146 } 这里 elevator_find()也来自同一个文件 . 112 static struct elevator_type *elevator_find(const char *name) 113 { 114 struct elevator_type *e; 115 struct list_head *entry; 116 117 list_for_each(entry, &elv_list) { 118 119 e = list_entry(entry, struct elevator_type, list); 120 121 if (!strcmp(e->elevator_name, name)) 122 return e; 123 } 124 125 return NULL; 126 } 这里我们要提一下 elv_list 链表,不管我们选择这四种算法中的哪一种,在正式登台演出之 前,都需要做一些初始化,系统初始化过程中的一项工作就是调用 elv_register()函数来注册 自己。而这个注册主要就是往 elv_list 这张链表里登记: int elv_register(struct elevator_type *e){ char *def = ""; spin_lock(&elv_list_lock); BUG_ON(elevator_find(e->elevator_name)); list_add_tail(&e->list, &elv_list); spin_unlock(&elv_list_lock); if (!strcmp(e->elevator_name, chosen_elevator) || (!*chosen_elevator && !strcmp(e->elevator_name, CONFIG_DEFAULT_IOSCHED))) def = " (default)"; printk(KERN_INFO "io scheduler %s registered%s\n", e->elevator_name, def); return 0; } 注意观察 list_add_tail 那一行,这个 elevator_type 结构体代表着一种电梯算法的类型,比如 对于 cfq,在 cfq-iosched.c 文件中,就定义了这么一个结构体变量 iosched_cfq: static struct elevator_type iosched_cfq = { .ops = { .elevator_merge_fn = cfq_merge, .elevator_merged_fn = cfq_merged_request, .elevator_merge_req_fn = cfq_merged_requests, .elevator_allow_merge_fn = cfq_allow_merge, .elevator_dispatch_fn = cfq_dispatch_requests, .elevator_add_req_fn = cfq_insert_request, .elevator_activate_req_fn = cfq_activate_request, .elevator_deactivate_req_fn = cfq_deactivate_request, .elevator_queue_empty_fn = cfq_queue_empty, .elevator_completed_req_fn = cfq_completed_request, .elevator_former_req_fn = elv_rb_former_request, .elevator_latter_req_fn = elv_rb_latter_request, .elevator_set_req_fn = cfq_set_request, .elevator_put_req_fn = cfq_put_request, .elevator_may_queue_fn = cfq_may_queue, .elevator_init_fn = cfq_init_queue, .elevator_exit_fn = cfq_exit_queue, .trim = cfq_free_io_context, }, .elevator_attrs = cfq_attrs, .elevator_name = "cfq", .elevator_owner = THIS_MODULE, }; 同样,我们可以找到,对于 noop,也有类似的变量。所以,我们就知道这个 e 到底是要得 到什么了,如果你什么都没设置,那么它只能选择最差的那个, noop。于是到现在我们终于 明 白 elv_register_queue() 中 那 个 e->elevator_type 是 啥 了 。 而 我 们 要 的 是 e->elevator_type->elevator_attrs。对于 cfq,很显然,它就是 cfq_attrs。在 block/cfq-iosched.c 中: static struct elv_fs_entry cfq_attrs[] = { CFQ_ATTR(quantum), CFQ_ATTR(fifo_expire_sync), CFQ_ATTR(fifo_expire_async), CFQ_ATTR(back_seek_max), CFQ_ATTR(back_seek_penalty), CFQ_ATTR(slice_sync), CFQ_ATTR(slice_async), CFQ_ATTR(slice_async_rq), CFQ_ATTR(slice_idle), __ATTR_NULL }; 所以,那个 while 循环的 sysfs_create_file 的功绩就是以上面这个数组的元素的名字建立一堆 的文件。而这正是我们在 /sys/block/sdf/queue/iosched/目录下面看到的那些文件。 至此,elv_register_queue 就算是结束了,从而 blk_register_queue()也就结束了,而 add_disk 这个函数也大功告成。这一刻开始,整个块设备工作的大舞台就已经搭好了。对于 sd 那边 来说,sd_probe 就是在结束 add_disk 之后结束的。 5.5.6 真实的 I/O 调度层处理 现在我们块设备也有了,队列也有了,要提交请求也就可以开始提交了。那就让我们回到 generic_make_request 来研究一下如何提交请求如何处理请求吧。我们看到,函数最后调用 q->make_request_fn(q, bio)。对 make_request_fn 函数的调用可以认为是 IO 调度层的入口, 该函数用于向请求队列中添加请求。该函数是在创建请求队列时指定的,代码如下 (blk_init_queue 函数中): q->request_fn = rfn; blk_queue_make_request(q, __make_request); 前面看到函数 blk_queue_make_request 将函数 __make_request 的地址赋予了请求队列 q 的 make_request_fn 成员: void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn) { q->nr_requests = BLKDEV_MAX_RQ; q->make_request_fn = mfn; ⋯⋯ 那么,__make_request 函数才是 IO 调度层的真实入口,来自 block/ll_rw_blk.c: 2846static int __make_request(request_queue_t *q, struct bio *bio) 2847{ 2848 struct request *req; 2849 int el_ret, rw, nr_sectors, cur_nr_sectors, barrier, err, sync; 2850 unsigned short prio; 2851 sector_t sector; 2852 2853 sector = bio->bi_sector; 2854 nr_sectors = bio_sectors(bio); 2855 cur_nr_sectors = bio_cur_sectors(bio); 2856 prio = bio_prio(bio); 2857 2858 rw = bio_data_dir(bio); 2859 sync = bio_sync(bio); 2860 2861 /* 2862 * low level driver can indicate that it wants pages above a 2863 * certain limit bounced to low memory (ie for highmem, or even 2864 * ISA dma in theory) 2865 */ 2866 blk_queue_bounce(q, &bio); 2867 2868 spin_lock_prefetch(q->queue_lock); 2869 2870 barrier = bio_barrier(bio); 2871 if (unlikely(barrier) && (q->next_ordered == QUEUE_ORDERED_NONE)) { 2872 err = -EOPNOTSUPP; 2873 goto end_io; 2874 } 2875 2876 spin_lock_irq(q->queue_lock); 2877 2878 if (unlikely(barrier) || elv_queue_empty(q)) 2879 goto get_rq; 2880 2881 el_ret = elv_merge(q, &req, bio); 2882 switch (el_ret) { 2883 case ELEVATOR_BACK_MERGE: 2884 BUG_ON(!rq_mergeable(req)); 2885 2886 if (!q->back_merge_fn(q, req, bio)) 2887 break; 2888 2889 blk_add_trace_bio(q, bio, BLK_TA_BACKMERGE); 2890 2891 req->biotail->bi_next = bio; 2892 req->biotail = bio; 2893 req->nr_sectors = req->hard_nr_sectors += nr_sectors; 2894 req->ioprio = ioprio_best(req->ioprio, prio); 2895 drive_stat_acct(req, nr_sectors, 0); 2896 if (!attempt_back_merge(q, req)) 2897 elv_merged_request(q, req); 2898 goto out; 2899 2900 case ELEVATOR_FRONT_MERGE: 2901 BUG_ON(!rq_mergeable(req)); 2902 2903 if (!q->front_merge_fn(q, req, bio)) 2904 break; 2905 2906 blk_add_trace_bio(q, bio, BLK_TA_FRONTMERGE); 2907 2908 bio->bi_next = req->bio; 2909 req->bio = bio; 2910 2911 /* 2912 * may not be valid. if the low level driver said 2913 * it didn't need a bounce buffer then it better 2914 * not touch req->buffer either... 2915 */ 2916 req->buffer = bio_data(bio); 2917 req->current_nr_sectors = cur_nr_sectors; 2918 req->hard_cur_sectors = cur_nr_sectors; 2919 req->sector = req->hard_sector = sector; 2920 req->nr_sectors = req->hard_nr_sectors += nr_sectors; 2921 req->ioprio = ioprio_best(req->ioprio, prio); 2922 drive_stat_acct(req, nr_sectors, 0); 2923 if (!attempt_front_merge(q, req)) 2924 elv_merged_request(q, req); 2925 goto out; 2926 2927 /* ELV_NO_MERGE: elevator says don't/can't merge. */ 2928 default: 2929 ; 2930 } 2931 2932get_rq: 2933 /* 2934 * Grab a free request. This is might sleep but can not fail. 2935 * Returns with the queue unlocked. 2936 */ 2937 req = get_request_wait(q, rw, bio); 2938 2939 /* 2940 * After dropping the lock and possibly sleeping here, our request 2941 * may now be mergeable after it had proven unmergeable (above). 2942 * We don't worry about that case for efficiency. It won't happen 2943 * often, and the elevators are able to handle it. 2944 */ 2945 init_request_from_bio(req, bio); 2946 2947 spin_lock_irq(q->queue_lock); 2948 if (elv_queue_empty(q)) 2949 blk_plug_device(q); 2950 add_request(q, req); 2951out: 2952 if (sync) 2953 __generic_unplug_device(q); 2954 2955 spin_unlock_irq(q->queue_lock); 2956 return 0; 2957 2958end_io: 2959 bio_endio(bio, nr_sectors << 9, err); 2960 return 0; 2961} __make_request 函数比较复杂,它接收 request_queue 类型的描述符 q 和一个 bio 结构的描 述符 bio 作为其参数,然后执行如下操作: 2853 内部变量 sector 赋值为 bio 的 bi_sector 字段,即将传送的 bio 的第一个扇区。 2854 行 通过 bio_sectors(bio)宏得到需要传送多少个连续的扇区,并赋值给内部变量 nr_sectors。 bio_sectors 宏来自 include/linux/bio.h #define bio_sectors(bio) ((bio)->bi_size >> 9) bio->bi_size 我们知道,在前面如果一个页中的 4 个块内容连续,则 do_mpage_readpage 通过 调用 bio_add_page 将 4 个块的大小 4096 赋值给 bio 的 bi_size 字段,那么 nr_sectors 就是 4096>>9,等于 8,表示这个 bio 一共 8 个扇区待传输。 2866 行,如果需要,调用 blk_cqueue_bounce()函数建立一个回弹缓冲区。如果回弹缓冲区 被建立,__make_request()函数将对该缓冲区而不是原先的 bio 结构进行操作。关于回弹缓冲 区的相关知识,请查阅相关资料,这里我们不做过多的介绍。 2878 行,调用 I/O 调度程序的 elv_queue_empty()函数检查请求队列中是否存在待处理请求 ——注意,调度队列可能是空的,但是 I/O 调度程序的其他队列可能包含待处理请求。如果 没有待处理请求,那么调用 blk_plug_device()函数插入请求队列,然后跳转到 2932 行的 get_rq 标号处。 如果插入的请求队列包含待处理请求,则走到 2881 行调用 I/O 调度程序的 elv_merge()函数 检查新的 bio 结构是否可以并入已存在的请求中。 该函数将返回三个可能值: 1. ELEVATOR_NO_MERGE:已经存在的请求中不能包含 bio 结构;这种情况下,跳转到 2932 行的 get_rq 标号处。 2. ELEVATOR_BACK_MERGE:bio 结构可作为末尾的 bio 而插入到某个请求 req 中;这种 情形下,函数调用 q->back_merge_fn 方法检查是否可以扩展该请求。如果不行,则跳转到 2932 行的 get_rq 标号处。否则,将 bio 描述符插入 req 链表的末尾并更新 req 的相应字段值。 然后,函数试图将该请求与其后面的请求合并(新的 bio 可能填充在两个请求之间)。 3. ELEVATOR_FRONT_MERGE:bio 结构可作为某个请求 req 的第一个 bio 被插入;这种情 形下,函数调用 q->front_merge_fn 方法检查是否可以扩展该请求。如果不行,则跳转到 2932 行的 get_rq 标号处。否则,将 bio 描述符插入 req 链表的首部并更新 req 的相应字段值。然 后,试图将该请求与其前面的请求合并。 不管是 ELEVATOR_BACK_MERGE 还是 ELEVATOR_FRONT_MERGE,说明 bio 已经被并 入存在的请求中,跳转到 2951 行 out 标号处终止函数。 下面来看 2932 行的 get_rq 标号处,bio 必须被插人到一个新的请求中。那么通过 get_request_wait 给我们这个分区的请求队列 q 分配一个新的请求描述符 request。如果没有 空闲的内存, get_request_wait 函数将调用 io_schedule 挂起当前进程,直到设置了 bio->bi_rw 中的 BIO_RW_AHEAD 标志,该标志表明这个 I/O 操作是一次预读;在这种情形下,调用 bio_endio()并终止:此时将不会执行数据传送。 然后调用 2945 行的 init_request_from_bio(req, bio)初始化请求描述符中的字段。主要有: a) 根据 bio 描述符的内容初始化各个字段,包括扇区数、当前 bio 以及当前段。 b) 设置 flags 字段中的 REQ_CMD 标志(说明这次 request 是一个标准的读或写操作)。 c) 如果第一个 bio 段的页框存放在低端内存,则将 buffer 字段设置为缓冲区的线性地址。 d) 将 rc_disk 字段设置为 bio->bi_bdev->bd_disk 的地址。 e) 将 bio 插入请求链表。 f) 将 start_time 字段设置为 jiffies 的值。 回到__make_request 的 2948-2949 行,再次调用 elv_queue_empty 检查一下请求队列中是否 存在待处理请求。如果没有待处理请求,那么调用 blk_plug_device()函数插入请求队列。不 管怎样,都会执行 2950 行的 add_request 函数: static inline void add_request(request_queue_t * q, struct request * req) { drive_stat_acct(req, req->nr_sectors, 1); if (q->activity_fn) q->activity_fn(q->activity_data, rq_data_dir(req)); /* * elevator indicated where it wants this request to be * inserted at elevator_merge time */ __elv_add_request(q, req, ELEVATOR_INSERT_SORT, 0); } add_request 函数本质上调用__elv_add_request 函数通过电梯算法把这个新的 request 插入到 对应 requesr_queue 合适的位置。在介绍 __elv_add_request 函数之前,我们先介绍几个宏, 来自 include/linux/elevator.h: 155 /* 156 * Insertion selection 157 */ 158 #define ELEVATOR_INSERT_FRONT 1 159 #define ELEVATOR_INSERT_BACK 2 160 #define ELEVATOR_INSERT_SORT 3 161 #define ELEVATOR_INSERT_REQUEUE 4 很明显,在 add_request 函数中传递进来的是 ELEVATOR_INSERT_SORT,表示从前面插入。 那么带着这个 where 我们进入下一个函数,即 __elv_add_request。来自 block/elevator.c: 646 void __elv_add_request(request_queue_t *q, struct request *rq, int where, 647 int plug) 648 { 649 if (q->ordcolor) 650 rq->cmd_flags |= REQ_ORDERED_COLOR; 651 652 if (rq->cmd_flags & (REQ_SOFTBARRIER | REQ_HARDBARRIER)) { 653 /* 654 * toggle ordered color 655 */ 656 if (blk_barrier_rq(rq)) 657 q->ordcolor ^= 1; 658 659 /* 660 * barriers implicitly indicate back insertion 661 */ 662 if (where == ELEVATOR_INSERT_SORT) 663 where = ELEVATOR_INSERT_BACK; 664 665 /* 666 * this request is scheduling boundary, update 667 * end_sector 668 */ 669 if (blk_fs_request(rq)) { 670 q->end_sector = rq_end_sector(rq); 671 q->boundary_rq = rq; 672 } 673 } else if (!(rq->cmd_flags & REQ_ELVPRIV) && where == ELEVATOR_INSERT_SORT) 674 where = ELEVATOR_INSERT_BACK; 675 676 if (plug) 677 blk_plug_device(q); 678 679 elv_insert(q, rq, where); 680 } 传入的参数 plug 等于 0,所以 blk_plug_device()不会被执行。很明显,一路走来我们根本没 有设置什么 REQ_SOFTBARRIER、REQ_HARDBARRIER 或 REQ_ELVPRIV 标识,所以前 面都和我们无关,直接跳到最后一行这个 elv_insert(): 548 void elv_insert(request_queue_t *q, struct request *rq, int where) 549 { 550 struct list_head *pos; 551 unsigned ordseq; 552 int unplug_it = 1; 553 554 blk_add_trace_rq(q, rq, BLK_TA_INSERT); 555 556 rq->q = q; 557 558 switch (where) { 559 case ELEVATOR_INSERT_FRONT: 560 rq->cmd_flags |= REQ_SOFTBARRIER; 561 562 list_add(&rq->queuelist, &q->queue_head); 563 break; 564 565 case ELEVATOR_INSERT_BACK: 566 rq->cmd_flags |= REQ_SOFTBARRIER; 567 elv_drain_elevator(q); 568 list_add_tail(&rq->queuelist, &q->queue_head); 569 /* 570 * We kick the queue here for the following reasons. 571 * - The elevator might have returned NULL previously 572 * to delay requests and returned them now. As the 573 * queue wasn't empty before this request, ll_rw_blk 574 * won't run the queue on return, resulting in hang. 575 * - Usually, back inserted requests won't be merged 576 * with anything. There's no point in delaying queue 577 * processing. 578 */ 579 blk_remove_plug(q); 580 q->request_fn(q); 581 break; 582 583 case ELEVATOR_INSERT_SORT: 584 BUG_ON(!blk_fs_request(rq)); 585 rq->cmd_flags |= REQ_SORTED; 586 q->nr_sorted++; 587 if (rq_mergeable(rq)) { 588 elv_rqhash_add(q, rq); 589 if (!q->last_merge) 590 q->last_merge = rq; 591 } 592 593 /* 594 * Some ioscheds (cfq) run q->request_fn directly, so 595 * rq cannot be accessed after calling 596 * elevator_add_req_fn. 597 */ 598 q->elevator->ops->elevator_add_req_fn(q, rq); 599 break; 600 601 case ELEVATOR_INSERT_REQUEUE: 602 /* 603 * If ordered flush isn't in progress, we do front 604 * insertion; otherwise, requests should be requeued 605 * in ordseq order. 606 */ 607 rq->cmd_flags |= REQ_SOFTBARRIER; 608 609 /* 610 * Most requeues happen because of a busy condition, 611 * don't force unplug of the queue for that case. 612 */ 613 unplug_it = 0; 614 615 if (q->ordseq == 0) { 616 list_add(&rq->queuelist, &q->queue_head); 617 break; 618 } 619 620 ordseq = blk_ordered_req_seq(rq); 621 622 list_for_each(pos, &q->queue_head) { 623 struct request *pos_rq = list_entry_rq(pos); 624 if (ordseq <= blk_ordered_req_seq(pos_rq)) 625 break; 626 } 627 628 list_add_tail(&rq->queuelist, pos); 629 break; 630 631 default: 632 printk(KERN_ERR "%s: bad insertion point %d\n", 633 __FUNCTION__, where); 634 BUG(); 635 } 636 637 if (unplug_it && blk_queue_plugged(q)) { 638 int nrq = q->rq.count[READ] + q->rq.count[WRITE] 639 - q->in_flight; 640 641 if (nrq >= q->unplug_thresh) 642 __generic_unplug_device(q); 643 } 644 } 由于我们是从前面插,所以我们执行 562 行这个 list_add,struct request 有一个成员 struct list_head queuelist,而 struct request_queue 有一个成员 struct list_head queue_head,所以我们 就把前者插入到后者所代表的这个队伍中来。然后咱们就返回了。 以上所有操作全部完成后,在终止之前,2952 行检查是否设置了 bio->bi_rw 中的 BIO_RW_SYNC 标志。 如果是,则对“请求队列”调用 generic_unplug_device()函数以卸载 设备驱动程序,并直接调用 q->request_fn(q),这个函数是什么,马上会看到。 如果在调用__make_request()函数之前请求队列不是空的,那么说明该请求队列要么已经被 拔掉过,要么很快将被拔掉——因为每个拥有待处理请求的插入请求队列 q 都有一个正在运 行的动态定时器 q->unplug_timer。另一方面,如果请求队列是空的,则 __make_request()函 数插入请求队列。或迟(最坏的情况是当拔出的定时器到期了)或最早(从 __make_request() 中退出时,如果设置了 bio 的 BIO_RW_SYNC 标志),该请求队列都会被拔掉。任何情形下, 块设备驱动程序的策略例程最后都将处理调度队列中的请求。 当 generic_make_request 执行完 scsi 磁盘设备对应请求队列的 q->make_request_fn 方法,也 就是刚才分析的 __make_request 以后,块设备的调度层就结束了。至于包含该 bio 的 request 放入到请求队列中后,何时被处理就由 IO 调度器的调度算法决定了。一旦该请求能够被处理,便调用请求队列中 request_fn 字段所指向的函数处理。这个成员的初始化也是在创建 请求队列时设置的: 1590 struct request_queue *scsi_alloc_queue(struct scsi_device *sdev) 1591 { 1592 struct request_queue *q; 1593 1594 q = __scsi_alloc_queue(sdev->host, scsi_request_fn); 1595 if (!q) 1596 return NULL; 1597 1598 blk_queue_prep_rq(q, scsi_prep_fn); 1599 blk_queue_issue_flush_fn(q, scsi_issue_flush_fn); 1600 blk_queue_softirq_done(q, scsi_softirq_done); 1601 return q; 1602 } 我们看到,给 scsi 设备创建 request_queue 的时候,是把 scsi_request_fn 作为他的 request_fn 字 段所指向的函数地址,所以这个 scsi_request_fn 就是 scsi 底层驱动的入口。 5.6 块设备驱动层的处理 当 generic_make_request 执行结束后,后面的工作依赖 I/O 调度程序或者是直接调用请求队 列的 request_fn 方法,也就是刚才看到的 scsi_request_fn 函数,取决于 bio->bi_rw 中的 BIO_RW_SYNC 标志。其实 I/O 调度程序最终也是调用的 request_fn 方法,传入参数为 q。 这个过程很复杂,下面我们就来详细分析一下整个块设备底层驱动程序的流程。 5.6.1 scsi 总线驱动的初始化 块设备底层驱动的核心是 scsi 总线层驱动,在总线层驱动之上为各种不同的 scsi 设备驱动, 在总线层驱动之下为 scsi host 驱动。其在内核中的位置如下图所示: 前面我们已经知道了上三层的工作,接下来大部分知识来自底下三层。 在 Linux 中 scsi 驱动基本分为三大层:top level,middle level 以及 lower level。top level 为具 体的 scsi 设备驱动,例如我们常用的磁盘设备驱动就在该层( Linux 中的实现为 sd.c),scsi disk 的驱动向上表现为块设备,因此,具有块设备的接口及一切属性,向下表现 scsi 设备,因为 scsi disk 基于 scsi 总线进行数据通信。top level 驱动与具体的 scsi 设备相关,所以该类驱动 往往由设备开发者提供,但是如果 scsi 设备为标准类设备,那么驱动可以通用。 middle level 实际上就是 scsi 总线层驱动,按照 scsi 协议进行设备枚举、数据传输、出错处 理。middle level 层的驱动与 scsi specification 相关,在一类操作系统平台上只需实现一次, 所以该类驱动往往由操作系统开发者提供。 lower level 为 scsi 控制器的驱动,该驱动与具体的硬件适配器相关,其需要与 scsi middle level 层进行接口,所以往往由提供适配器的硬件厂商完成驱动开发,只有硬件厂商才对自己定义 的 register file(寄存器堆)最清楚。当然,在 lower level 层可以做虚拟的 scsi host,所以该 层的驱动也不一定对硬件进行操作。 Linux 中,scsi 三层驱动模型如下图所示: 而前面提到的 scsi device 的数据结构就是在 scsi middle level 定义的,用于描述一个 scsi 的 具体功能单元,其在 scsi host 中通过 channel、id、lun 进行寻址。 首先,什么是 channel、id 和 lun。通常 SCSI 总线适配器作为 PCI 设备的形式存在,其在计 算机体系结构中的位置如下图所示: 在系统初始化时会扫描系统 PCI 总线,由于 scsi 端口适配器挂接在 pci 总线上,因此会被 pci 扫描软件扫描得到,并且生成一个 pci device(PDO)。然后扫描软件需要为该 pci device 加载 相应的驱动程序。 在 linux 系统中,系统初始化时会遍历 pci bus 上存在的所有驱动程序,检查是否有符合要求 的驱动程序存在,这里假设 scsi host 是 USB 或 marwell 中的设备,那么,如果存在 USB 或 marwell 提供的 scsi 端口驱动,就会被成功调用。加载 scsi 端口驱动时, pci 扫描程序会调用 对应 scsi 端口驱动提供的 probe 函数,该 probe 函数是 scsi 端口驱动程序在初始化驱动时注 册到 pci-driver 上的(Linux 的总线驱动都是采用的这种思路 )。 在 scsi host 具体的 probe 函数中会初始化 scsi host,注册中断处理函数,并且调用 scsi_host_alloc 函数生成一个 scsi host,然后添加到 scsi middle level,最后调用 scsi_scan_host 函数扫描 scsi 端口适配器所管理的所有 scsi 总线。 一个 scsi 端口适配器可能拥有多个 channel,每个 channel 拥有一条 scsi 总线。传统 scsi 总线是并行共享总线,现有的 SATA、SAS 等 P2P 接口在逻辑上可以理解成总线的一种特例,所 以 scsi middle level 驱动程序是通用的。由于一个 scsi host 可能存在多个 channel,因此依次 扫描每个 channel。按照 spec,传统 scsi bus 上最多可以连接 16 个 scsi target,因此,scsi 扫 描程序会依次探测 target。一个 scsi target 可以存在多种功能,每种功能称之为 LUN,对于 单功能设备(例如磁盘 ),其 LUN 通常为 0。 Scsi host 的扫描过程在系统初始化中进行,详细的代码我们就不去分析了,这里从网上摘录 了一段伪代码对其进行简单地描述: For (channel = 0; channel /* 对一个适配器的每个通道中的设备进行识别 */ ⋯ For (id=0; id /* 对一个通道中的每个 ID 对应设备进行识别 */ ⋯ For (lun=1; lun /* 对一个 ID 对应设备的每个 LUN 进行识别 */ ⋯ } } } 在计算机系统启动过程中,操作系统会扫描默认的 PCI 根节点,从而触发了 PCI 设备扫描 的过程,开始构建 PCI 设备树。 首先 scsi host 作为 PCI 设备会被 PCI 总线驱动层扫描到( PCI 设备的扫描采用配置信息读取 的方式),扫描到 scsi host 之后,操作系统开始加载 scsi host 的驱动,scsi host driver 就是上 面说所的 low level driver。scsi host driver 初始化 scsi 控制器,通过 PCI 配置空间的信息分配 硬件资源,注册中断服务。最后开始扫描通过 scsi 控制器扩展出来的下一级总线—— scsi bus。 scsi bus 的扫描通过 scsi middle level 提供的服务完成。 scsi host driver 可以调用 scsi middle level 提供的扫描算法完成 scsi 总线设备的扫描,扫描过程可以描述如下: a. 采用 scsi_add_host()函数为扫描出来的 scsi host 添加一个对象,注册到 scsi middle level。 b. 通过__scsi_add_device()函数循环扫描 scsi host,扫描过程采用了 scsi middle level 的 服务 scsi_probe_and_add_lun()。 c. 通过向 scsi device 发送 INQUIRY 命令获取 scsi 设备信息,得知 scsi 设备的 vendor id、 product id 以及设备类型等关键信息。至此,操作系统得知了 scsi 设备所具备的各种 能力,并且向 scsi middle level 注册了 scsi 设备对象—— scsi device。 d. 根据 scsi 设备的信息初始化 scsi device对象,并且通知内核去加载该设备的驱动程序。 如果被枚举的设备为 scsi disk,那么 scsi 磁盘的驱动程序将被加载,至此,一个 scsi 设备被枚举成功。 e. 循环(b)(c)(d)将 scsi 总线扫描完毕。结束 scsi host 的扫描工作。 通过上述扫描过程可以知道,在系统中可以采用如下方法对一个 scsi device 进行描述:host_id : channel_id : target_id : lun_id 其中,host_id是系统动态分配的,这与 PCI总线的扫描顺序相关,对于固定硬件的系统 host_id 扫描得到的结果不会改变,但是,如果动态添加一个 scsi host(PCI device),系统的 host_id 可能会发生变化,这一点需要注意。 最终,上述过程结束之后, scsi 的硬件逻辑可以采用如下的总线拓扑结构进行描述: scsi_device 就是对 lun 的抽象。下面对 scsi_device 中的重要域进行说明: 那么,什么又是 Scsi_Host 呢?在 scsi middle level 定义了 scsi 设备的数据结构,用于描述一 个 scsi 的具体功能单元,其在 scsi host 中通过 channel、id、lun 进行寻址。 通过上述描述可以知道 scsi_device 是对 lun 的抽象。下面对 scsi_device 中的重要域进行说明: struct scsi_device { struct Scsi_Host *host; /* 与 scsi device 相关的 scsi host */ struct request_queue *request_queue; /* 块设备接口的请求队列 */ unsigned int device_busy; /* 命令执行标记 */ struct list_head cmd_list; /* scsi_cmnd 队列 */ struct scsi_cmnd *current_cmnd; /* 当前执行的命令 */ unsigned int id, lun, channel; /* SCSI 设备的标识 */ void *hostdata; /* 通常指向 low-level driver 定义的 scsi device */ … } __attribute__((aligned(sizeof(unsigned long)))); 在 scsi 总线对一个 ID 对应设备的每个 LUN 进行识别的过程中, scsi middle level 会为每个 lun 生 成 一 个 scsi_device 结构,实现的核心函数为 scsi_probe_and_add_lun() , 来 自 drivers/scsi/scsi_scan.c: static int scsi_probe_and_add_lun(struct scsi_target *starget, uint lun, int *bflagsp, struct scsi_device **sdevp, int rescan, void *hostdata) { struct scsi_device *sdev; unsigned char *result; int bflags, res = SCSI_SCAN_NO_RESPONSE, result_len = 256; struct Scsi_Host *shost = dev_to_shost(starget->dev.parent); /* * The rescan flag is used as an optimization, the first scan of a * host adapter calls into here with rescan == 0. */ sdev = scsi_device_lookup_by_target(starget, lun); if (sdev) { if (rescan || sdev->sdev_state != SDEV_CREATED) { SCSI_LOG_SCAN_BUS(3, printk(KERN_INFO "scsi scan: device exists on %s\n", sdev->sdev_gendev.bus_id)); if (sdevp) *sdevp = sdev; else scsi_device_put(sdev); if (bflagsp) *bflagsp = scsi_get_device_flags(sdev, sdev->vendor, sdev->model); return SCSI_SCAN_LUN_PRESENT; } scsi_device_put(sdev); } else sdev = scsi_alloc_sdev(starget, lun, hostdata); if (!sdev) goto out; result = kmalloc(result_len, GFP_ATOMIC | ((shost->unchecked_isa_dma) ? __GFP_DMA : 0)); if (!result) goto out_free_sdev; if (scsi_probe_lun(sdev, result, result_len, &bflags)) goto out_free_result; if (bflagsp) *bflagsp = bflags; /* * result contains valid SCSI INQUIRY data. */ if (((result[0] >> 5) == 3) && !(bflags & BLIST_ATTACH_PQ3)) { /* * For a Peripheral qualifier 3 (011b), the SCSI * spec says: The device server is not capable of * supporting a physical device on this logical * unit. * * For disks, this implies that there is no * logical disk configured at sdev->lun, but there * is a target id responding. */ SCSI_LOG_SCAN_BUS(2, sdev_printk(KERN_INFO, sdev, "scsi scan:" " peripheral qualifier of 3, device not" " added\n")) if (lun == 0) { SCSI_LOG_SCAN_BUS(1, { unsigned char vend[9]; unsigned char mod[17]; sdev_printk(KERN_INFO, sdev, "scsi scan: consider passing scsi_mod." "dev_flags=%s:%s:0x240 or 0x800240\n", scsi_inq_str(vend, result, 8, 16), scsi_inq_str(mod, result, 16, 32)); }); } res = SCSI_SCAN_TARGET_PRESENT; goto out_free_result; } /* * Non-standard SCSI targets may set the PDT to 0x1f (unknown or * no device type) instead of using the Peripheral Qualifier to * indicate that no LUN is present. For example, USB UFI does this. */ if (starget->pdt_1f_for_no_lun && (result[0] & 0x1f) == 0x1f) { SCSI_LOG_SCAN_BUS(3, printk(KERN_INFO "scsi scan: peripheral device type" " of 31, no device added\n")); res = SCSI_SCAN_TARGET_PRESENT; goto out_free_result; } res = scsi_add_lun(sdev, result, &bflags); if (res == SCSI_SCAN_LUN_PRESENT) { if (bflags & BLIST_KEY) { sdev->lockable = 0; scsi_unlock_floptical(sdev, result); } } out_free_result: kfree(result); out_free_sdev: if (res == SCSI_SCAN_LUN_PRESENT) { if (sdevp) { if (scsi_device_get(sdev) == 0) { *sdevp = sdev; } else { __scsi_remove_device(sdev); res = SCSI_SCAN_NO_RESPONSE; } } } else scsi_destroy_sdev(sdev); out: return res; } 传给该函数的参数是 scsi_target 类型,表示 scsi 总线上的一个 scsi node。注意结合前面那个 图观察,每个 scsi target 可能拥有多个 lun,即多个 scsi devie,而这个。scsi target 数据结构 中的重要域定义如下: struct scsi_target { struct scsi_device *starget_sdev_user; /* 当前活动的 scsi device */ struct list_head siblings; struct list_head devices; /* scsi device 链表 */ struct device dev; unsigned int reap_ref; unsigned int channel; /* 当前 channel 号 */ unsigned int id; /* scsi target 的 ID 号 */ … } __attribute__((aligned(sizeof(unsigned long)))); 而 scsi_probe_and_add_lun 函数就是在对一个 ID 对应设备的 LUN 进行识别的过程中向这个 Scsi 节点增加这个 lun。函数内部还有一个 Scsi_Host 类型的内部变量 shost,表示对一个 scsi 适配器(很多地方又称为 scsi 总线控制器)。 在很多实际的系统中, scsi host 为一块基于 PCI 总线的 HBA 或者为一个 SCSI 控制器芯片。 每个 scsi host 可以存在多个 channel,一个 channel 实际扩展了一条 SCSI 总线。每个 channel 可以连接多个 scsi 节点,具体连接的数量与 scsi 总线带载能力有关。 scsi host 的重要域描述 如下: struct Scsi_Host { struct list_head __devices; /* scsi device 链表 */ struct list_head __targets; struct scsi_host_template *hostt; /* scsi host 操作接口方法 */ struct scsi_transport_template *transportt; /* scsi host transport 方法 */ unsigned int host_busy; /* scsi host 忙标记 */ unsigned int host_failed; /* commands that failed. */ unsigned int max_id; /* 最大的 scsi node 数量 */ unsigned int max_lun; /* 最大的 lun 数量 */ unsigned int max_channel; /* 最大的 channel 数量 */ unsigned char max_cmd_len; /* scsi 命令的长度 */ int this_id; /* scsi host 在总线的 id */ int can_queue; /* scsi cmd 是否可以 queue 在 host 标记 */ short cmd_per_lun; /* 每个 lun 可以 queue 多少 scsi cmd */ short unsigned int sg_tablesize; /* scatter-gather table 大小 */ short unsigned int max_sectors; …… }; 这里有一个重点字段,是 scsi_host_template ,翻译成 SCSI 总线端口样板,表示 scsi host 操作接口方法: struct scsi_host_template { /* scsi middle level 层驱动通过该函数将 scsi command 提交给 low level 层驱动,并且告 诉 low level 驱动完成 scsi 命令之后需要调用 done()函数 */ int (* queuecommand)(struct scsi_cmnd *, void (*done)(struct scsi_cmnd *)); ⋯⋯ /* scsi host 出错处理函数 */ int (* eh_abort_handler)(struct scsi_cmnd *); int (* eh_device_reset_handler)(struct scsi_cmnd *); int (* eh_bus_reset_handler)(struct scsi_cmnd *); int (* eh_host_reset_handler)(struct scsi_cmnd *); /* 更改 scsi 设备的队列深度 */ int (* change_queue_depth)(struct scsi_device *, int); int can_queue; /* scsi host 队列深度 */ int this_id; /* scsi host 的 ID 号 */ unsigned short sg_tablesize; /* scatter-gather table 的容量 */ short cmd_per_lun; /* 每个 lun 能够 queue 的命令数 */ unsigned emulated:1; /* 虚拟 scsi host flag */ }; 比如,USB 的 scsi_host_template 方法就定义如下: struct scsi_host_template usb_stor_host_template = { /* basic userland interface stuff */ .name = "usb-storage", .proc_name = "usb-storage", .proc_info = proc_info, .info = host_info, /* command interface -- queued only */ .queuecommand = queuecommand, /* error and abort handlers */ .eh_abort_handler = command_abort, .eh_device_reset_handler = device_reset, .eh_bus_reset_handler = bus_reset, /* queue commands only, only one command per LUN */ .can_queue = 1, .cmd_per_lun = 1, /* unknown initiator id */ .this_id = -1, .slave_alloc = slave_alloc, .slave_configure = slave_configure, /* lots of sg segments can be handled */ .sg_tablesize = SG_ALL, /* limit the total size of a transfer to 120 KB */ .max_sectors = 240, /* merge commands... this seems to help performance, but * periodically someone should test to see which setting is more * optimal. */ .use_clustering = 1, /* emulated HBA */ .emulated = 1, /* we do our own delay after a device or bus reset */ .skip_settle_delay = 1, /* sysfs device attributes */ .sdev_attrs = sysfs_device_attr_list, /* module management */ .module = THIS_MODULE }; 整个块设备驱动层的工作,其实就是一个东西:封装一个 scsi_cmnd 结构。在完成 scsi_cmnd 的封装后,SCSI 中间层通过调用 scsi_host_template 结构中定义的 queuecommand 函数将 SCSI 命令提交给 SCSI 底层驱动部分。 queuecommand 函数,是一个 SCSI 命令队列处理函数,在 SCSI 底层驱动中,定义了 queuecommand 函数的具体实现。因此, SCSI 中间层,调用 queuecommand 函数实际上就是 调用了底层驱动定义的 queuecommand 函数的处理实体,将 SCSI 命令提交给了各个厂家定 义的 SCSI 底层驱动进行处理。这个过程和通用块设备层调用 SCSI 中间层的处理函数进 行块请求处理的机制很相似,这也体现了 LINUX 内核代码具有很好的扩展性。 底层驱动接受到请求后,就要开始处理 SCSI 命令了,这一层和硬件关系紧密,所以这块 代码一般都是由各个厂家自己实现。基本流程可概括为:从底层驱动维护的队列中,取出一 个 SCSI 命令,封装成厂家自定义的请求格式,然后采用 DMA 或者其他方式,将请求提 交给 SCSI TARGET 端,由 SCSI TARGET 端对请求处理,并返回执行结果给 SCSI 底层 驱动层。 5.6.2 scsi 设备驱动体系架构 从这一层开始,整个文件读写的中心将由 request 转向 scsi 的命令结构 scsi_cmnd。那么这个 命令结构到底是怎么一回事呢,这还得从 SCSI 架构谈起。SCSI 实现了一种客户机 /服务器 风格的通信架构,发起者向目标设备发送命令请求。该目标处理此请求并向发起者返回响应。 发起者可以是托管计算机中的一个 SCSI 设备,而 SCSI 目标则可以是一个磁盘、光盘和 磁带设备或特殊设备(比如箱体设备)。 这里要提到一个概念—— Lower Level Device(LDD):在最低层的是一组驱动器,称为 SCSI 低层驱动器。它们是一些可与物理设备(比如 HBA)链接的特定驱动器。 LLD 提供了自 公共中间层到特定于设备的 HBA 的一种抽象。每个 LLD 都提供了到特定底层硬件的接口, 但所使用的到中间层的接口却是一组标准接口。 较低层包含大量代码,原因是它要负责处理各种不同的 SCSI 适配器类型。例如,Fibre Channel 协议包含了针对 Emulex 和 QLogic 的各种适配器的 LLD。面向 Adaptec 和 LSI 的 SAS 适配器的 LLD 也包括在内。 与 存 储 相 关 的 SCSI 命 令 一 般 是 在 SCSI Architecture Model (SAM)、SCSI Primary Commands (SPC) 和 SCSI Block Commands (SBC) 中定义的: l SAM:定义 SCSI 系统模型、SCSI 标准集的功能性分区,以及适用于所有 SCSI 实现 和实现标准的需求。 l SPC:定义:对所有 SCSI 设备模型通用的行为。 l SBC:定义命令集扩展,以方便操作 SCSI 直接访问块设备。 每个 SCSI 命令都由 Command Descriptor Block (CDB) 描述,它定义 SCSI 设备执行的操 作。SCSI 命令涉及到用于向 SCSI 设备传输数据(或从中输出数据)的数据命令,以及用 于设置 SCSI 设备的配置参数的非数据命令。 典型的 CDB 格式如下图所示: 位 7 位 6 位 5 位 4 位 3 位 2 位 1 位 0 字节 0 Operation code = 12h 字节 1 LUN Reserved EVPD 字节 2 Page code 字节 3 Reserved 字节 4 Allocation length 字节 5 Control 如果 EVPD 参数位(用于启用关键产品数据)为 0 并且 Page Code 参数字节为 0,那么 目标将返回标准命令数据(如 inquiry)。如果 EVPD 参数为 1,那么目标将返回对应 page code 字段的特定于供应商的数据。 scsi_cmnd 结构中的 cmnd[MAX_COMMAND_SIZE]数组就是这个 CDB 的内容。我们看到这 个数组虽然最大可有 16 个元素,每个元素 1 字节,但是我们仅仅用了其中 6 个元素,用来 存放 CDB,其中 cmnd[0]最为重要,对应 scsi 操作码。所有的 scsi 操作码都有定义,在 include/scsi/scsi.h 中: #define TEST_UNIT_READY 0x00 #define REZERO_UNIT 0x01 #define REQUEST_SENSE 0x03 #define FORMAT_UNIT 0x04 #define READ_BLOCK_LIMITS 0x05 #define REASSIGN_BLOCKS 0x07 #define INITIALIZE_ELEMENT_STATUS 0x07 #define READ_6 0x08 #define WRITE_6 0x0a #define SEEK_6 0x0b #define READ_REVERSE 0x0f #define WRITE_FILEMARKS 0x10 #define SPACE 0x11 #define INQUIRY 0x12 #define RECOVER_BUFFERED_DATA 0x14 #define MODE_SELECT 0x15 #define RESERVE 0x16 #define RELEASE 0x17 #define COPY 0x18 #define ERASE 0x19 #define MODE_SENSE 0x1a #define START_STOP 0x1b #define RECEIVE_DIAGNOSTIC 0x1c #define SEND_DIAGNOSTIC 0x1d #define ALLOW_MEDIUM_REMOVAL 0x1e #define SET_WINDOW 0x24 #define READ_CAPACITY 0x25 #define READ_10 0x28 #define WRITE_10 0x2a #define SEEK_10 0x2b #define POSITION_TO_ELEMENT 0x2b #define WRITE_VERIFY 0x2e #define VERIFY 0x2f #define SEARCH_HIGH 0x30 #define SEARCH_EQUAL 0x31 #define SEARCH_LOW 0x32 #define SET_LIMITS 0x33 #define PRE_FETCH 0x34 #define READ_POSITION 0x34 #define SYNCHRONIZE_CACHE 0x35 #define LOCK_UNLOCK_CACHE 0x36 #define READ_DEFECT_DATA 0x37 #define MEDIUM_SCAN 0x38 #define COMPARE 0x39 #define COPY_VERIFY 0x3a #define WRITE_BUFFER 0x3b #define READ_BUFFER 0x3c #define UPDATE_BLOCK 0x3d #define READ_LONG 0x3e #define WRITE_LONG 0x3f #define CHANGE_DEFINITION 0x40 #define WRITE_SAME 0x41 #define READ_TOC 0x43 #define LOG_SELECT 0x4c #define LOG_SENSE 0x4d #define MODE_SELECT_10 0x55 #define RESERVE_10 0x56 #define RELEASE_10 0x57 #define MODE_SENSE_10 0x5a #define PERSISTENT_RESERVE_IN 0x5e #define PERSISTENT_RESERVE_OUT 0x5f #define REPORT_LUNS 0xa0 #define MOVE_MEDIUM 0xa5 #define EXCHANGE_MEDIUM 0xa6 #define READ_12 0xa8 #define WRITE_12 0xaa #define WRITE_VERIFY_12 0xae #define SEARCH_HIGH_12 0xb0 #define SEARCH_EQUAL_12 0xb1 #define SEARCH_LOW_12 0xb2 #define READ_ELEMENT_STATUS 0xb8 #define SEND_VOLUME_TAG 0xb6 #define WRITE_LONG_2 0xea #define READ_16 0x88 #define WRITE_16 0x8a #define VERIFY_16 0x8f #define SERVICE_ACTION_IN 0x9e /* values for service action in */ #define SAI_READ_CAPACITY_16 0x10 /* Values for T10/04-262r7 */ #define ATA_16 0x85 /* 16-byte pass-thru */ #define ATA_12 0xa1 /* 12-byte pass-thru */ 我们列出最常使用的命令: 命令 描述 INQUIRY 请求目标设备的摘要信息 TEST_UNIT_READY 检测目标设备是否准备好进行传输 READ_6 从 SCSI 目标设备传输数据 WRITE_6 向 SCSI 目标设备传输数据 REQUEST_SENSE 请求最后一个命令的检测数据 READ_CAPACITY 获得存储容量信息 所有 SCSI 命令都要以操作代码的第一个字节为开端,以表明它所代表的操作。并且所有 SCSI 命令都要包含一个控制字节。这个字节通常是该命令的最后一个字节,用于表示与供 应商相关的信息等等。 scsi_cmnd 结构就完全是 SCSI 记录的抽象,不仅 cmnd 数组字段记录了命令描述块 (CDB); 还有用于感测数据缓存 (SENSE BUFFER)的 sense_buffer 字段,以及用于存放 IO 超时时间 等 SCSI 相关的信息和 SCSI 子系统处理命令需要的一些其他信息的字段,如回调函数等。 来自 include/scsi/scsi_cmnd.h: struct scsi_cmnd { struct scsi_device *device; struct list_head list; /* scsi_cmnd participates in queue lists */ struct list_head eh_entry; /* entry for the host eh_cmd_q */ int eh_eflags; /* Used by error handlr */ void (*done) (struct scsi_cmnd *); /* Mid-level done function */ unsigned long serial_number; unsigned long jiffies_at_alloc; int retries; int allowed; int timeout_per_command; unsigned char cmd_len; enum dma_data_direction sc_data_direction; /* These elements define the operation we are about to perform */ #define MAX_COMMAND_SIZE 16 unsigned char cmnd[MAX_COMMAND_SIZE]; unsigned request_bufflen; /* Actual request size */ struct timer_list eh_timeout; /* Used to time out the command. */ void *request_buffer; /* Actual requested buffer */ /* These elements define the operation we ultimately want to perform */ unsigned short use_sg; /* Number of pieces of scatter-gather */ unsigned short sglist_len; /* size of malloc'd scatter-gather list */ unsigned underflow; unsigned transfersize; int resid; struct request *request; #define SCSI_SENSE_BUFFERSIZE 96 unsigned char sense_buffer[SCSI_SENSE_BUFFERSIZE]; /* obtained by REQUEST SENSE when * CHECK CONDITION is received on original * command (auto-sense) */ void (*scsi_done) (struct scsi_cmnd *); struct scsi_pointer SCp; /* Scratchpad used by some host adapters */ unsigned char *host_scribble; int result; /* Status code from lower level driver */ unsigned char tag; /* SCSI-II queued command tag */ unsigned long pid; /* Process ID, starts at 0. Unique per host. */ }; bio_vex[bi_idx]bio request Scsi_device SCSI Top Level host request_queue sibings same_target_sibs cmd_list current_cmd devices targets hostt transportt SCSI Middle Level Scsi_Host Scsi_device Scsi_device Scsi_device⋯⋯ 共享同一Target的SCSI设备 scsi_cmnd device scsi_cmnd scsi_cmnd⋯⋯list request cmnd[MAX_COMMAND _SIZE] data_cmnd[MAX_CO MMAND_SIZE] queuelist request request⋯⋯ 共 享 同 一 端 口 的 设 备 bio biotail bi_next bi_io_vec bi_dev Page(全局共享缓存页面) bv_page bv_len bv_offset block bi_idx scsi_target starget_sdev_user device tgtt tgt_list_entry scst_tgt_template (* detect)() (* release)() (*xmit_response)() (*proc_info)() tgt_list scst_session sess_list scsi_target scsi_target scst_session scst_session tgt ⋯ sess_list_entry 5.6.3 scsi 块设备驱动层处理 好了,了解完必要的 scsi 设备驱动知识以后,我们就可以安心分析 scsi_request_fn 函数了。 大家回忆一下对,这个函数指针通过几次传递并最终在 blk_init_queue_node()中被赋予了 q->request_fn。所以这一层的重点就是这个 scsi_request_fn 函数。 在看 scsi_request_fn 之前,注意回忆一下 scsi_alloc_queue 函数的 1598 行至 1560 行还赋了三 个函数指针: 1590 struct request_queue *scsi_alloc_queue(struct scsi_device *sdev) 1591 { 1592 struct request_queue *q; 1593 1594 q = __scsi_alloc_queue(sdev->host, scsi_request_fn); 1595 if (!q) 1596 return NULL; 1597 1598 blk_queue_prep_rq(q, scsi_prep_fn); 1599 blk_queue_issue_flush_fn(q, scsi_issue_flush_fn); 1600 blk_queue_softirq_done(q, scsi_softirq_done); 1601 return q; 1602 } 143 void blk_queue_prep_rq(request_queue_t *q, prep_rq_fn *pfn) 144 { 145 q->prep_rq_fn = pfn; 146 } 313 void blk_queue_issue_flush_fn(request_queue_t *q, issue_flush_fn *iff) 314 { 315 q->issue_flush_fn = iff; 316 } 173 void blk_queue_softirq_done(request_queue_t *q, softirq_done_fn *fn) 174 { 175 q->softirq_done_fn = fn; 176 } 分别是把 scsi_prep_fn 赋给了 q->prep_rq_fn,把 scsi_issue_flush_fn 赋给了 q->issue_flush_fn, 把 scsi_softirq_done 赋给了 q->softirq_done_fn。尤其是 scsi_prep_fn 我们马上就会用到。 好,让我们继续前面的话题,重点关注 scsi_request_fn(): 1422 static void scsi_request_fn(struct request_queue *q) 1423 { 1424 struct scsi_device *sdev = q->queuedata; 1425 struct scsi_Host *shost; 1426 struct scsi_cmnd *cmd; 1427 struct request *req; 1428 1429 if (!sdev) { 1430 printk("scsi: killing requests for dead queue\n"); 1431 while ((req = elv_next_request(q)) != NULL) 1432 scsi_kill_request(req, q); 1433 return; 1434 } 1435 1436 if(!get_device(&sdev->sdev_gendev)) 1437 /* We must be tearing the block queue down already */ 1438 return; 1439 1440 /* 1441 * To start with, we keep looping until the queue is empty, or until 1442 * the host is no longer able to accept any more requests. 1443 */ 1444 shost = sdev->host; 1445 while (!blk_queue_plugged(q)) { 1446 int rtn; 1447 /* 1448 * get next queueable request. We do this early to make sure 1449 * that the request is fully prepared even if we cannot 1450 * accept it. 1451 */ 1452 req = elv_next_request(q); 1453 if (!req || !scsi_dev_queue_ready(q, sdev)) 1454 break; 1455 1456 if (unlikely(!scsi_device_online(sdev))) { 1457 sdev_printk(KERN_ERR, sdev, 1458 "rejecting I/O to offline device\n"); 1459 scsi_kill_request(req, q); 1460 continue; 1461 } 1462 1463 1464 /* 1465 * Remove the request from the request list. 1466 */ 1467 if (!(blk_queue_tagged(q) && !blk_queue_start_tag(q, req))) 1468 blkdev_dequeue_request(req); 1469 sdev->device_busy++; /* 说明命令正在执行中 */ 1470 1471 spin_unlock(q->queue_lock); 1472 cmd = req->special; 1473 if (unlikely(cmd == NULL)) { 1474 printk(KERN_CRIT "impossible request in %s.\n" 1475 "please mail a stack trace to " 1476 "linux-scsi@vger.kernel.org\n", 1477 __FUNCTION__); 1478 blk_dump_rq_flags(req, "foo"); 1479 BUG(); 1480 } 1481 spin_lock(shost->host_lock); 1482 1483 if (!scsi_host_queue_ready(q, shost, sdev)) 1484 goto not_ready; 1485 if (sdev->single_lun) { 1486 if (scsi_target(sdev)->starget_sdev_user && 1487 scsi_target(sdev)->starget_sdev_user != sdev) 1488 goto not_ready; 1489 scsi_target(sdev)->starget_sdev_user = sdev; 1490 } 1491 shost->host_busy++; 1492 1493 /* 1494 * XXX(hch): This is rather suboptimal, scsi_dispatch_cmd will 1495 * take the lock again. 1496 */ 1497 spin_unlock_irq(shost->host_lock); 1498 1499 /* 1500 * Finally, initialize any error handling parameters, and set up 1501 * the timers for timeouts. 1502 */ 1503 scsi_init_cmd_errh(cmd); 1504 1505 /* 1506 * Dispatch the command to the low-level driver. 1507 */ 1508 rtn = scsi_dispatch_cmd(cmd); 1509 spin_lock_irq(q->queue_lock); 1510 if(rtn) { 1511 /* we're refusing the command; because of 1512 * the way locks get dropped, we need to 1513 * check here if plugging is required */ 1514 if(sdev->device_busy == 0) 1515 blk_plug_device(q); 1516 1517 break; 1518 } 1519 } 1520 1521 goto out; 1522 1523 not_ready: 1524 spin_unlock_irq(shost->host_lock); 1525 1526 /* 1527 * lock q, handle tag, requeue req, and decrement device_busy. We 1528 * must return with queue_lock held. 1529 * 1530 * Decrementing device_busy without checking it is OK, as all such 1531 * cases (host limits or settings) should run the queue at some 1532 * later time. 1533 */ 1534 spin_lock_irq(q->queue_lock); 1535 blk_requeue_request(q, req); 1536 sdev->device_busy--; 1537 if(sdev->device_busy == 0) 1538 blk_plug_device(q); 1539 out: 1540 /* must be careful here...if we trigger the ->remove() function 1541 * we cannot be holding the q lock */ 1542 spin_unlock_irq(q->queue_lock); 1543 put_device(&sdev->sdev_gendev); 1544 spin_lock_irq(q->queue_lock); 1545 } scsi_request_fn 函 数 为 scsi 设备请求队列处理函数,前面看到该函数被注册到了 request_queue->request_fn 上。块设备请求的 bio 最终会 merge 到 request queue 中,然后通过 unplug_fn 函数调用 request_queue->request_fn,实现 scsi_reuqest_fn 函数的调用。 scsi_request_fn 函数实现了请求队列的处理,首先 1452-1468 行按照电梯算法从请求队列中 摘取一个 request,所以我们首先关注 1452 行的 elv_next_request(),来自 block/elevator.c: 712 struct request *elv_next_request(request_queue_t *q) 713 { 714 struct request *rq; 715 int ret; 716 717 while ((rq = __elv_next_request(q)) != NULL) { 718 if (!(rq->cmd_flags & REQ_STARTED)) { 719 /* 720 * This is the first time the device driver 721 * sees this request (possibly after 722 * requeueing). Notify IO scheduler. 723 */ 724 if (blk_sorted_rq(rq)) 725 elv_activate_rq(q, rq); 726 727 /* 728 * just mark as started even if we don't start 729 * it, a request that has been delayed should 730 * not be passed by new incoming requests 731 */ 732 rq->cmd_flags |= REQ_STARTED; 733 blk_add_trace_rq(q, rq, BLK_TA_ISSUE); 734 } 735 736 if (!q->boundary_rq || q->boundary_rq == rq) { 737 q->end_sector = rq_end_sector(rq); 738 q->boundary_rq = NULL; 739 } 740 741 if ((rq->cmd_flags & REQ_DONTPREP) || !q->prep_rq_fn) 742 break; 743 744 ret = q->prep_rq_fn(q, rq); 745 if (ret == BLKPREP_OK) { 746 break; 747 } else if (ret == BLKPREP_DEFER) { 748 /* 749 * the request may have been (partially) prepped. 750 * we need to keep this request in the front to 751 * avoid resource deadlock. REQ_STARTED will 752 * prevent other fs requests from passing this one. 753 */ 754 rq = NULL; 755 break; 756 } else if (ret == BLKPREP_KILL) { 757 int nr_bytes = rq->hard_nr_sectors << 9; 758 759 if (!nr_bytes) 760 nr_bytes = rq->data_len; 761 762 blkdev_dequeue_request(rq); 763 rq->cmd_flags |= REQ_QUIET; 764 end_that_request_chunk(rq, 0, nr_bytes); 765 end_that_request_last(rq, 0); 766 } else { 767 printk(KERN_ERR "%s: bad return=%d\n", __FUNCTION__, 768 ret); 769 break; 770 } 771 } 772 773 return rq; 774 } 它调用的__elv_next_request()仍然来自 block/elevator.c: 696 static inline struct request *__elv_next_request(request_queue_t *q) 697 { 698 struct request *rq; 699 700 while (1) { 701 while (!list_empty(&q->queue_head)) { 702 rq = list_entry_rq(q->queue_head.next); 703 if (blk_do_ordered(q, &rq)) 704 return rq; 705 } 706 707 if (!q->elevator->ops->elevator_dispatch_fn(q, 0)) 708 return NULL; 709 } 710 } 由于我们在 I/O 调度层中插入了一个 request,所以这里 q->queue_head 不可能为空。所以 702 行从中取出一个 request 来。然后是 blk_do_ordered(),来自 block/ll_rw_blk.c: 478 int blk_do_ordered(request_queue_t *q, struct request **rqp) 479 { 480 struct request *rq = *rqp; 481 int is_barrier = blk_fs_request(rq) && blk_barrier_rq(rq); 482 483 if (!q->ordseq) { 484 if (!is_barrier) 485 return 1; 486 487 if (q->next_ordered != QUEUE_ORDERED_NONE) { 488 *rqp = start_ordered(q, rq); 489 return 1; 490 } else { 491 /* 492 * This can happen when the queue switches to 493 * ORDERED_NONE while this request is on it. 494 */ 495 blkdev_dequeue_request(rq); 496 end_that_request_first(rq, -EOPNOTSUPP, 497 rq->hard_nr_sectors); 498 end_that_request_last(rq, -EOPNOTSUPP); 499 *rqp = NULL; 500 return 0; 501 } 502 } 503 504 /* 505 * Ordered sequence in progress 506 */ 507 508 /* Special requests are not subject to ordering rules. */ 509 if (!blk_fs_request(rq) && 510 rq != &q->pre_flush_rq && rq != &q->post_flush_rq) 511 return 1; 512 513 if (q->ordered & QUEUE_ORDERED_TAG) { 514 /* Ordered by tag. Blocking the next barrier is enough. */ 515 if (is_barrier && rq != &q->bar_rq) 516 *rqp = NULL; 517 } else { 518 /* Ordered by draining. Wait for turn. */ 519 WARN_ON(blk_ordered_req_seq(rq) < blk_ordered_cur_seq(q)); 520 if (blk_ordered_req_seq(rq) > blk_ordered_cur_seq(q)) 521 *rqp = NULL 522 } 523 524 return 1; 525 } 首先看一下 blk_fs_request, 528 #define blk_fs_request(rq) ((rq)->cmd_type == REQ_TYPE_FS) 很显然,咱们从来没有设置这个标识,所以不去管它。 所以在咱们这个上下文里, is_barrier 一定是 0。所以, blk_do_ordered 二话不说,直接返回 1。那么回到__elv_next_request 以后,703 行这个 if 条件是满足的,所以也就是返回 rq,下 面的那个 elevator_dispatch_fn 根本不会执行的。另一方面,我们从 __elv_next_request 返回, 回到 elv_next_request()的时候,只要 request queue 不是空的,那么返回值就是队列中最前边 的那个 request。 继续在 elv_next_request 中往下走,request 得到 了,cmd_flags 其实整个故事中设置 REQ_STARTED 的也就是这里, 732 行。所以在我们执行 732 行之前,这个 flag 是没有设置的。因此,if 条件是满足的。 而 blk_sorted_rq 又是一个宏,来自 include/linux/blkdev.h: 543 #define blk_sorted_rq(rq) ((rq)->cmd_flags & REQ_SORTED) 很显然,咱们也从来没有设置过这个 flag,所以这里不关我们的事。 当然了,对于 noop,即便执行下一个函数也没有意义,因为这个 elv_activate_rq()来自 block/elevator.c: 272 static void elv_activate_rq(request_queue_t *q, struct request *rq) 273 { 274 elevator_t *e = q->elevator; 275 276 if (e->ops->elevator_activate_req_fn) 277 e->ops->elevator_activate_req_fn(q, rq); 278 } 我们假设使用最简单的 noop 电梯算法,即根本就没有这个指针,所以不去管他。 这时候,我们设置 REQ_STARTED 这个 flag,最开始我们在 elevator_init()中,有这么一句: 230 q->boundary_rq = NULL; 于是 rq_end_sector 会被执行,这其实也只是一个很简单的宏: 172 #define rq_end_sector(rq) ((rq)->sector + (rq)->nr_sectors) 同时,boundary_rq 还是被置为 NULL。 回到 elv_next_request 中,接下来 744 行,由于我们把 prep_rq_fn 赋上了 scsi_prep_fn,所以 我们要看一下这个 scsi_prep_fn(),这个来自 drivers/scsi/scsi_lib.c 的函数: 1093static int scsi_prep_fn(struct request_queue *q, struct request *req) 1094{ 1095 struct scsi_device *sdev = q->queuedata; 1096 struct scsi_cmnd *cmd; 1097 int specials_only = 0; 1098 1099 /* 1100 * Just check to see if the device is online. If it isn't, we 1101 * refuse to process any commands. The device must be brought 1102 * online before trying any recovery commands 1103 */ 1104 if (unlikely(!scsi_device_online(sdev))) { 1105 sdev_printk(KERN_ERR, sdev, 1106 "rejecting I/O to offline device\n"); 1107 goto kill; 1108 } 1109 if (unlikely(sdev->sdev_state != SDEV_RUNNING)) { 1110 /* OK, we're not in a running state don't prep 1111 * user commands */ 1112 if (sdev->sdev_state == SDEV_DEL) { 1113 /* Device is fully deleted, no commands 1114 * at all allowed down */ 1115 sdev_printk(KERN_ERR, sdev, 1116 "rejecting I/O to dead device\n"); 1117 goto kill; 1118 } 1119 /* OK, we only allow special commands (i.e. not 1120 * user initiated ones */ 1121 specials_only = sdev->sdev_state; 1122 } 1123 1124 /* 1125 * Find the actual device driver associated with this command. 1126 * The SPECIAL requests are things like character device or 1127 * ioctls, which did not originate from ll_rw_blk. Note that 1128 * the special field is also used to indicate the cmd for 1129 * the remainder of a partially fulfilled request that can 1130 * come up when there is a medium error. We have to treat 1131 * these two cases differently. We differentiate by looking 1132 * at request->cmd, as this tells us the real story. 1133 */ 1134 if (req->flags & REQ_SPECIAL && req->special) { 1135 cmd = req->special; 1136 } else if (req->flags & (REQ_CMD | REQ_BLOCK_PC)) { 1137 1138 if(unlikely(specials_only) && !(req->flags & REQ_SPECIAL)) { 1139 if(specials_only == SDEV_QUIESCE || 1140 specials_only == SDEV_BLOCK) 1141 goto defer; 1142 1143 sdev_printk(KERN_ERR, sdev, 1144 "rejecting I/O to device being removed\n"); 1145 goto kill; 1146 } 1147 1148 1149 /* 1150 * Now try and find a command block that we can use. 1151 */ 1152 if (!req->special) { 1153 cmd = scsi_get_command(sdev, GFP_ATOMIC); 1154 if (unlikely(!cmd)) 1155 goto defer; 1156 } else 1157 cmd = req->special; 1158 1159 /* pull a tag out of the request if we have one */ 1160 cmd->tag = req->tag; 1161 } else { 1162 blk_dump_rq_flags(req, "SCSI bad req"); 1163 goto kill; 1164 } 1165 1166 /* note the overloading of req->special. When the tag 1167 * is active it always means cmd. If the tag goes 1168 * back for re-queueing, it may be reset */ 1169 req->special = cmd; 1170 cmd->request = req; 1171 1172 /* 1173 * FIXME: drop the lock here because the functions below 1174 * expect to be called without the queue lock held. Also, 1175 * previously, we dequeued the request before dropping the 1176 * lock. We hope REQ_STARTED prevents anything untoward from 1177 * happening now. 1178 */ 1179 if (req->flags & (REQ_CMD | REQ_BLOCK_PC)) { 1180 int ret; 1181 1182 /* 1183 * This will do a couple of things: 1184 * 1) Fill in the actual SCSI command. 1185 * 2) Fill in any other upper-level specific fields 1186 * (timeout). 1187 * 1188 * If this returns 0, it means that the request failed 1189 * (reading past end of disk, reading offline device, 1190 * etc). This won't actually talk to the device, but 1191 * some kinds of consistency checking may cause the 1192 * request to be rejected immediately. 1193 */ 1194 1195 /* 1196 * This sets up the scatter-gather table (allocating if 1197 * required). 1198 */ 1199 ret = scsi_init_io(cmd); 1200 switch(ret) { 1201 /* For BLKPREP_KILL/DEFER the cmd was released */ 1202 case BLKPREP_KILL: 1203 goto kill; 1204 case BLKPREP_DEFER: 1205 goto defer; 1206 } 1207 1208 /* 1209 * Initialize the actual SCSI command for this request. 1210 */ 1211 if (req->flags & REQ_BLOCK_PC) { 1212 scsi_setup_blk_pc_cmnd(cmd); 1213 } else if (req->rq_disk) { 1214 struct scsi_driver *drv; 1215 1216 drv = *(struct scsi_driver **)req->rq_disk->private_data; 1217 if (unlikely(!drv->init_command(cmd))) { 1218 scsi_release_buffers(cmd); 1219 scsi_put_command(cmd); 1220 goto kill; 1221 } 1222 } 1223 } 1224 1225 /* 1226 * The request is now prepped, no need to come back here 1227 */ 1228 req->flags |= REQ_DONTPREP; 1229 return BLKPREP_OK; 1230 1231 defer: 1232 /* If we defer, the elv_next_request() returns NULL, but the 1233 * queue must be restarted, so we plug here if no returning 1234 * command will automatically do that. */ 1235 if (sdev->device_busy == 0) 1236 blk_plug_device(q); 1237 return BLKPREP_DEFER; 1238 kill: 1239 req->errors = DID_NO_CONNECT << 16; 1240 return BLKPREP_KILL; 1241} 大家还记得我们前面使用__make_request 函 数 创 建 一 个 request 的 时 候 , 曾 经 通 过 init_request_from_bio(req, bio)初始化请求描述符中的字段。其中把 request 的设置 flags 字段 中的 REQ_CMD 标识,说明这次 request 是一个标准的读或写操作。注意,前面我们并没有 设置 REQ_BLOCK_PC 标识。 所以 scsi_prep_fn 函数首先会进入 1136 那个条件分支。 1138-1146 的代码是对该块设备状态 的一个检查,一般不会出什么问题。随后 1153 行调用 scsi_get_command 函数给我们这个 request 对应的 scsi_device 分配一个 scsi_cmnd 结构,其地址赋给函数内部变量 cmd 指针: struct scsi_cmnd *scsi_get_command(struct scsi_device *dev, gfp_t gfp_mask) { struct scsi_cmnd *cmd; /* Bail if we can't get a reference to the device */ if (!get_device(&dev->sdev_gendev)) return NULL; cmd = __scsi_get_command(dev->host, gfp_mask); if (likely(cmd != NULL)) { unsigned long flags; memset(cmd, 0, sizeof(*cmd)); cmd->device = dev; init_timer(&cmd->eh_timeout); INIT_LIST_HEAD(&cmd->list); spin_lock_irqsave(&dev->list_lock, flags); list_add_tail(&cmd->list, &dev->cmd_list); spin_unlock_irqrestore(&dev->list_lock, flags); cmd->jiffies_at_alloc = jiffies; } else put_device(&dev->sdev_gendev); return cmd; } static struct scsi_cmnd *__scsi_get_command(struct Scsi_Host *shost, gfp_t gfp_mask) { struct scsi_cmnd *cmd; cmd = kmem_cache_alloc(shost->cmd_pool->slab, gfp_mask | shost->cmd_pool->gfp_mask); if (unlikely(!cmd)) { unsigned long flags; spin_lock_irqsave(&shost->free_list_lock, flags); if (likely(!list_empty(&shost->free_list))) { cmd = list_entry(shost->free_list.next, struct scsi_cmnd, list); list_del_init(&cmd->list); } spin_unlock_irqrestore(&shost->free_list_lock, flags); } return cmd; } 看不懂这个分配函数的回去好好看一下“ scsi 设备驱动体系架构”最后那个图,我就不多费 口舌了。回到 scsi_prep_fn 中,1160 行把 reqest 的 tag 赋给这个全新的 scsi_cmnd 结构;然 后 1169、1170 行把这个 reqest 和 scsi_cmnd 联系起来。随后又进入 1179 行条件判断, 1199 行,调用 scsi_init_io 函数初始化这个 scsi_cmnd 结构: static int scsi_init_io(struct scsi_cmnd *cmd) { struct request *req = cmd->request; struct scatterlist *sgpnt; int count; /* * if this is a rq->data based REQ_BLOCK_PC, setup for a non-sg xfer */ if ((req->flags & REQ_BLOCK_PC) && !req->bio) { cmd->request_bufflen = req->data_len; cmd->request_buffer = req->data; req->buffer = req->data; cmd->use_sg = 0; return 0; } /* * we used to not use scatter-gather for single segment request, * but now we do (it makes highmem I/O easier to support without * kmapping pages) */ cmd->use_sg = req->nr_phys_segments; /* * if sg table allocation fails, requeue request later. */ sgpnt = scsi_alloc_sgtable(cmd, GFP_ATOMIC); if (unlikely(!sgpnt)) { scsi_unprep_request(req); return BLKPREP_DEFER; } cmd->request_buffer = (char *) sgpnt; cmd->request_bufflen = req->nr_sectors << 9; if (blk_pc_request(req)) cmd->request_bufflen = req->data_len; req->buffer = NULL; /* * Next, walk the list, and fill in the addresses and sizes of * each segment. */ count = blk_rq_map_sg(req->q, req, cmd->request_buffer); /* * mapped well, send it off */ if (likely(count <= cmd->use_sg)) { cmd->use_sg = count; return 0; } printk(KERN_ERR "Incorrect number of segments after building list\n"); printk(KERN_ERR "counted %d, received %d\n", count, cmd->use_sg); printk(KERN_ERR "req nr_sec %lu, cur_nr_sec %u\n", req->nr_sectors, req->current_nr_sectors); /* release the command and kill it */ scsi_release_buffers(cmd); scsi_put_command(cmd); return BLKPREP_KILL; } 一般情况下, scsi_init_io 返回 0,否则致命错误,导致 scsi_prep_fn 退出。继续走,由于我 们并没有设置 REQ_BLOCK_PC 标识,而且 req 的 rq_disk 是存在的,gendisk,忘了?那你 完了。所以 scsi_prep_fn 函数来到 1217 行,执行本函数中最重要的过程, drv->init_command。 这个 drv 是啥?来自 gendisk 的 private_data 字段。还记得 sd_probe 吗?我们在其中把它赋值 给了对应 scsi_disk 结构的 driver 字段,就是前面那个 sd_template 常量,别告诉我你又忘了。 如果真忘了,那就好好从头开始,从 scsi 磁盘驱动的初始化函数 init_sd 开始。 我们知道 sd_template 常量的 init_command 指针指向 sd_init_command 函数地址,所以下面 就来看看 sd_init_command 这个函数,十分重要,来自 drivers/scsi/sd.c: 366static int sd_init_command(struct scsi_cmnd * SCpnt) 367{ 368 struct scsi_device *sdp = SCpnt->device; 369 struct request *rq = SCpnt->request; 370 struct gendisk *disk = rq->rq_disk; 371 sector_t block = rq->sector; 372 unsigned int this_count = SCpnt->request_bufflen >> 9; 373 unsigned int timeout = sdp->timeout; 374 375 SCSI_LOG_HLQUEUE(1, printk("sd_init_command: disk=%s, block=%llu, " 376 "count=%d\n", disk->disk_name, 377 (unsigned long long)block, this_count)); 378 379 if (!sdp || !scsi_device_online(sdp) || 380 block + rq->nr_sectors > get_capacity(disk)) { 381 SCSI_LOG_HLQUEUE(2, printk("Finishing %ld sectors\n", 382 rq->nr_sectors)); 383 SCSI_LOG_HLQUEUE(2, printk("Retry with 0x%p\n", SCpnt)); 384 return 0; 385 } 386 387 if (sdp->changed) { 388 /* 389 * quietly refuse to do anything to a changed disc until 390 * the changed bit has been reset 391 */ 392 /* printk("SCSI disk has been changed. Prohibiting further I/O.\n"); */ 393 return 0; 394 } 395 SCSI_LOG_HLQUEUE(2, printk("%s : block=%llu\n", 396 disk->disk_name, (unsigned long long)block)); 397 398 /* 399 * If we have a 1K hardware sectorsize, prevent access to single 400 * 512 byte sectors. In theory we could handle this - in fact 401 * the scsi cdrom driver must be able to handle this because 402 * we typically use 1K blocksizes, and cdroms typically have 403 * 2K hardware sectorsizes. Of course, things are simpler 404 * with the cdrom, since it is read-only. For performance 405 * reasons, the filesystems should be able to handle this 406 * and not force the scsi disk driver to use bounce buffers 407 * for this. 408 */ 409 if (sdp->sector_size == 1024) { 410 if ((block & 1) || (rq->nr_sectors & 1)) { 411 printk(KERN_ERR "sd: Bad block number requested"); 412 return 0; 413 } else { 414 block = block >> 1; 415 this_count = this_count >> 1; 416 } 417 } 418 if (sdp->sector_size == 2048) { 419 if ((block & 3) || (rq->nr_sectors & 3)) { 420 printk(KERN_ERR "sd: Bad block number requested"); 421 return 0; 422 } else { 423 block = block >> 2; 424 this_count = this_count >> 2; 425 } 426 } 427 if (sdp->sector_size == 4096) { 428 if ((block & 7) || (rq->nr_sectors & 7)) { 429 printk(KERN_ERR "sd: Bad block number requested"); 430 return 0; 431 } else { 432 block = block >> 3; 433 this_count = this_count >> 3; 434 } 435 } 436 if (rq_data_dir(rq) == WRITE) { 437 if (!sdp->writeable) { 438 return 0; 439 } 440 SCpnt->cmnd[0] = WRITE_6; 441 SCpnt->sc_data_direction = DMA_TO_DEVICE; 442 } else if (rq_data_dir(rq) == READ) { 443 SCpnt->cmnd[0] = READ_6; 444 SCpnt->sc_data_direction = DMA_FROM_DEVICE; 445 } else { 446 printk(KERN_ERR "sd: Unknown command %lx\n", rq->flags); 447/* overkill panic("Unknown sd command %lx\n", rq->flags); */ 448 return 0; 449 } 450 451 SCSI_LOG_HLQUEUE(2, printk("%s : %s %d/%ld 512 byte blocks.\n", 452 disk->disk_name, (rq_data_dir(rq) == WRITE) ? 453 "writing" : "reading", this_count, rq->nr_sectors)); 454 455 SCpnt->cmnd[1] = 0; 456 457 if (block > 0xffffffff) { 458 SCpnt->cmnd[0] += READ_16 - READ_6; 459 SCpnt->cmnd[1] |= blk_fua_rq(rq) ? 0x8 : 0; 460 SCpnt->cmnd[2] = sizeof(block) > 4 ? (unsigned char) (block >> 56) & 0xff : 0; 461 SCpnt->cmnd[3] = sizeof(block) > 4 ? (unsigned char) (block >> 48) & 0xff : 0; 462 SCpnt->cmnd[4] = sizeof(block) > 4 ? (unsigned char) (block >> 40) & 0xff : 0; 463 SCpnt->cmnd[5] = sizeof(block) > 4 ? (unsigned char) (block >> 32) & 0xff : 0; 464 SCpnt->cmnd[6] = (unsigned char) (block >> 24) & 0xff; 465 SCpnt->cmnd[7] = (unsigned char) (block >> 16) & 0xff; 466 SCpnt->cmnd[8] = (unsigned char) (block >> 8) & 0xff; 467 SCpnt->cmnd[9] = (unsigned char) block & 0xff; 468 SCpnt->cmnd[10] = (unsigned char) (this_count >> 24) & 0xff; 469 SCpnt->cmnd[11] = (unsigned char) (this_count >> 16) & 0xff; 470 SCpnt->cmnd[12] = (unsigned char) (this_count >> 8) & 0xff; 471 SCpnt->cmnd[13] = (unsigned char) this_count & 0xff; 472 SCpnt->cmnd[14] = SCpnt->cmnd[15] = 0; 473 } else if ((this_count > 0xff) || (block > 0x1fffff) || 474 SCpnt->device->use_10_for_rw) { 475 if (this_count > 0xffff) 476 this_count = 0xffff; 477 478 SCpnt->cmnd[0] += READ_10 - READ_6; 479 SCpnt->cmnd[1] |= blk_fua_rq(rq) ? 0x8 : 0; 480 SCpnt->cmnd[2] = (unsigned char) (block >> 24) & 0xff; 481 SCpnt->cmnd[3] = (unsigned char) (block >> 16) & 0xff; 482 SCpnt->cmnd[4] = (unsigned char) (block >> 8) & 0xff; 483 SCpnt->cmnd[5] = (unsigned char) block & 0xff; 484 SCpnt->cmnd[6] = SCpnt->cmnd[9] = 0; 485 SCpnt->cmnd[7] = (unsigned char) (this_count >> 8) & 0xff; 486 SCpnt->cmnd[8] = (unsigned char) this_count & 0xff; 487 } else { 488 if (unlikely(blk_fua_rq(rq))) { 489 /* 490 * This happens only if this drive failed 491 * 10byte rw command with ILLEGAL_REQUEST 492 * during operation and thus turned off 493 * use_10_for_rw. 494 */ 495 printk(KERN_ERR "sd: FUA write on READ/WRITE(6) drive\n"); 496 return 0; 497 } 498 499 SCpnt->cmnd[1] |= (unsigned char) ((block >> 16) & 0x1f); 500 SCpnt->cmnd[2] = (unsigned char) ((block >> 8) & 0xff); 501 SCpnt->cmnd[3] = (unsigned char) block & 0xff; 502 SCpnt->cmnd[4] = (unsigned char) this_count; 503 SCpnt->cmnd[5] = 0; 504 } 505 SCpnt->request_bufflen = this_count * sdp->sector_size; 506 507 /* 508 * We shouldn't disconnect in the middle of a sector, so with a dumb 509 * host adapter, it's safe to assume that we can at least transfer 510 * this many bytes between each connect / disconnect. 511 */ 512 SCpnt->transfersize = sdp->sector_size; 513 SCpnt->underflow = this_count << 9; 514 SCpnt->allowed = SD_MAX_RETRIES; 515 SCpnt->timeout_per_command = timeout; 516 517 /* 518 * This is the completion routine we use. This is matched in terms 519 * of capability to this function. 520 */ 521 SCpnt->done = sd_rw_intr; 522 523 /* 524 * This indicates that the command is ready from our end to be 525 * queued. 526 */ 527 return 1; 528} 这个函数很重要,看似也很长,但是对照着前面 scsi 块设备驱动体系架构仔细看看,就会发 现其实代码虽多,但很好理解。 379~394 检查一下磁盘状态,正常的话就不进入相应的条件 分支。409~435 行,根据扇区大小对内部变量 block 和 this_count 进行调整,其中 block 表示 将要对磁盘读写的起始扇区号, this_count 表示将要读入 scsi_cmnd 对应的那个缓冲区的字 节数。这个缓冲区是通过前面 scsi_init_io 函数调用 scsi_alloc_sgtable 获得的,感兴趣的同学 可以深入研究一下。 继续走,436 行,通过 rq_data_dir 宏获得 request 的传输方向: #define rq_data_dir(rq) ((rq)->flags & 1) 如果是 WRITE 就把 scsi 命令设置成 WRITE_6,否则设置成 READ_6。457-478 是针对有些 磁盘的大扇区的处理,我们略过,然后 499-503 初始化 CDB 的其他字段,大家可以对照“ scsi 设备驱动体系架构”中 CDB 的格式来分析这些代码的意思。最后, sd_init_command 函数初 始化 scsi_cmnd 的其他字段,并返回到 scsi_prep_fn 函数中。由于 sd_init_command 返回的是 1,最终,正常的话, scsi_prep_fn 函数返回 BLKPREP_OK。prep 表示 prepare 的意思,用我 们的母语说就是准备的意思,最后 BLKPREP_OK 就说明准备好了,或者说准备就绪。而 scsi_prep_fn()也将返回这个值,返回之前还设置了 cmd_flags 中的 REQ_DONTPREP。(注意 elv_next_request()函数 741 行判断的就是设这个 flag。) 回到 elv_next_request()中,由于返回值是 BLKPREP_OK,所以 746 行我们就 break 了。换言 之,我们取到了一个 request,我们为之准备好了 scsi 命令,我们下一步就该是执行这个命 令了。所以我们不需要再在 elv_next_request()中滞留。我们终于回到了 scsi_request_fn(),结 束了 elv_next_request,又要看下一个,不只是一个,而是两个, 1467 行,一个宏加一个函 数,宏是 blk_queue_tagged,来自 include/linux/blkdev.h: #define blk_queue_tagged(q) test_bit(QUEUE_FLAG_QUEUED, &(q)->queue_flags) 而函数是 blk_queue_start_tag,来自 block/ll_rw_blk.c: 1122 int blk_queue_start_tag(request_queue_t *q, struct request *rq) 1123 { 1124 struct blk_queue_tag *bqt = q->queue_tags; 1125 int tag; 1126 1127 if (unlikely((rq->cmd_flags & REQ_QUEUED))) { 1128 printk(KERN_ERR 1129 "%s: request %p for device [%s] already tagged %d", 1130 __FUNCTION__, rq, 1131 rq->rq_disk ? rq->rq_disk->disk_name : "?", rq->tag); 1132 BUG(); 1133 } 1134 1135 /* 1136 * Protect against shared tag maps, as we may not have exclusive 1137 * access to the tag map. 1138 */ 1139 do { 1140 tag = find_first_zero_bit(bqt->tag_map, bqt->max_depth); 1141 if (tag >= bqt->max_depth) 1142 return 1; 1143 1144 } while (test_and_set_bit(tag, bqt->tag_map)); 1145 1146 rq->cmd_flags |= REQ_QUEUED; 1147 rq->tag = tag; 1148 bqt->tag_index[tag] = rq; 1149 blkdev_dequeue_request(rq); 1150 list_add(&rq->queuelist, &bqt->busy_list); 1151 bqt->busy++; 1152 return 0; 1153 } 对于我们大多数人来说,这两个函数的返回值都是 0。 也因此,下一个函数 blkdev_dequeue_request()就会被执行。来自 include/linux/blkdev.h: 725 static inline void blkdev_dequeue_request(struct request *req) 726 { 727 elv_dequeue_request(req->q, req); 728 } 而 elv_dequeue_request 来自 block/elevator.c: 778 void elv_dequeue_request(request_queue_t *q, struct request *rq) 779 { 780 BUG_ON(list_empty(&rq->queuelist)); 781 BUG_ON(ELV_ON_HASH(rq)); 782 783 list_del_init(&rq->queuelist); 784 785 /* 786 * the time frame between a request being removed from the lists 787 * and to it is freed is accounted as io that is in progress at 788 * the driver side. 789 */ 790 if (blk_account_rq(rq)) 791 q->in_flight++; 792 } 现在这个社会就是利用与被利用的关系,既然这个 request 已经没有了利用价值,我们已经 从它身上得到了我们想要的 scsi 命令,那么我们完全可以过河拆桥卸磨杀驴了。 list_del_init 把这个 request 从 request queue 队列里删除掉。 而下面这个 blk_account_rq 也是一个来自 include/linux/blkdev.h 的宏: 536 #define blk_account_rq(rq) (blk_rq_started(rq) && blk_fs_request(rq)) 很显然,至少第二个条件我们是不满足的。所以不用多说,结束这个 elv_dequeue_request。 现在是时候去执行 scsi 命令了,回到 scsi_request_fn 函数中 elv_next_request 执行完毕之后, req 的 special 就存放对 scsi 硬件设备发出“特殊”命令的请求所使用的数据的指针, 1472 行,把它赋给内部 scsi_cmnd 型变量 cmd。然后 1508 行调用 scsi_dispatch_cmd 函数执行这 个 cmd。 整个块设备驱动层的处理就结束了,我还是在网上找到一个图,正好可以总结上面的过程: 从前面分析可以看出,请求队列 queue 是 top level 与 middle level 之间的纽带。上层请求会 在请求队列中维护,处理函数的方法由上下各层提供。在请求队列的处理过程中,将普通的 块设备请求转换成标准的 scsi 命令,然后再通过 middle level 与 low level 之间的接口将请求 递交给 scsi host。 5.6.4 scsi 命令的执行 负责执行具体 scsi 命令的函数是 scsi_dispatch_cmd,来自 drivers/scsi/scsi.c: 468 int scsi_dispatch_cmd(struct scsi_cmnd *cmd) 469 { 470 struct Scsi_Host *host = cmd->device->host; 471 unsigned long flags = 0; 472 unsigned long timeout; 473 int rtn = 0; 474 475 /* check if the device is still usable */ 476 if (unlikely(cmd->device->sdev_state == SDEV_DEL)) { 477 /* in SDEV_DEL we error all commands. DID_NO_CONNECT 478 * returns an immediate error upwards, and signals 479 * that the device is no longer present */ 480 cmd->result = DID_NO_CONNECT << 16; 481 atomic_inc(&cmd->device->iorequest_cnt); 482 __scsi_done(cmd); 483 /* return 0 (because the command has been processed) */ 484 goto out; 485 } 486 487 /* Check to see if the scsi lld put this device into state SDEV_BLOCK. */ 488 if (unlikely(cmd->device->sdev_state == SDEV_BLOCK)) { 489 /* 490 * in SDEV_BLOCK, the command is just put back on the device 491 * queue. The suspend state has already blocked the queue so 492 * future requests should not occur until the device 493 * transitions out of the suspend state. 494 */ 495 scsi_queue_insert(cmd, SCSI_MLQUEUE_DEVICE_BUSY); 496 497 SCSI_LOG_MLQUEUE(3, printk("queuecommand : device blocked \n")); 498 499 /* 500 * NOTE: rtn is still zero here because we don't need the 501 * queue to be plugged on return (it's already stopped) 502 */ 503 goto out; 504 } 505 506 /* 507 * If SCSI-2 or lower, store the LUN value in cmnd. 508 */ 509 if (cmd->device->scsi_level <= SCSI_2 && 510 cmd->device->scsi_level != SCSI_UNKNOWN) { 511 cmd->cmnd[1] = (cmd->cmnd[1] & 0x1f) | 512 (cmd->device->lun << 5 & 0xe0); 513 } 514 515 /* 516 * We will wait MIN_RESET_DELAY clock ticks after the last reset so 517 * we can avoid the drive not being ready. 518 */ 519 timeout = host->last_reset + MIN_RESET_DELAY; 520 521 if (host->resetting && time_before(jiffies, timeout)) { 522 int ticks_remaining = timeout - jiffies; 523 /* 524 * NOTE: This may be executed from within an interrupt 525 * handler! This is bad, but for now, it'll do. The irq 526 * level of the interrupt handler has been masked out by the 527 * platform dependent interrupt handling code already, so the 528 * sti() here will not cause another call to the SCSI host's 529 * interrupt handler (assuming there is one irq-level per 530 * host). 531 */ 532 while (--ticks_remaining >= 0) 533 mdelay(1 + 999 / HZ); 534 host->resetting = 0; 535 } 536 537 /* 538 * AK: unlikely race here: for some reason the timer could 539 * expire before the serial number is set up below. 540 */ 541 scsi_add_timer(cmd, cmd->timeout_per_command, scsi_times_out); 542 543 scsi_log_send(cmd); 544 545 /* 546 * We will use a queued command if possible, otherwise we will 547 * emulate the queuing and calling of completion function ourselves. 548 */ 549 atomic_inc(&cmd->device->iorequest_cnt); 550 551 /* 552 * Before we queue this command, check if the command 553 * length exceeds what the host adapter can handle. 554 */ 555 if (CDB_SIZE(cmd) > cmd->device->host->max_cmd_len) { 556 SCSI_LOG_MLQUEUE(3, 557 printk("queuecommand : command too long.\n")); 558 cmd->result = (DID_ABORT << 16); 559 560 scsi_done(cmd); 561 goto out; 562 } 563 564 spin_lock_irqsave(host->host_lock, flags); 565 scsi_cmd_get_serial(host, cmd); 566 567 if (unlikely(host->shost_state == SHOST_DEL)) { 568 cmd->result = (DID_NO_CONNECT << 16); 569 scsi_done(cmd); 570 } else { 571 rtn = host->hostt->queuecommand(cmd, scsi_done); 572 } 573 spin_unlock_irqrestore(host->host_lock, flags); 574 if (rtn) { 575 if (scsi_delete_timer(cmd)) { 576 atomic_inc(&cmd->device->iodone_cnt); 577 scsi_queue_insert(cmd, 578 (rtn == SCSI_MLQUEUE_DEVICE_BUSY) ? 579 rtn : SCSI_MLQUEUE_HOST_BUSY); 580 } 581 SCSI_LOG_MLQUEUE(3, 582 printk("queuecommand : request rejected\n")); 583 } 584 585 out: 586 SCSI_LOG_MLQUEUE(3, printk("leaving scsi_dispatch_cmnd()\n")); 587 return rtn; 588 } scsi_dispatch_cmd 函数将一个 scsi 命令提交给底层 scsi host 驱动。在命令 dispatch 的过程中, middle level 会检查 scsi host 是否出于 busy 状态,是否还有空间存放新的 scsi command。如果所有条件都满足,那么会调用上下层之间的接口函数 queuecommand 函数转发请求。 queuecomand 函数的实现由 scsi host driver 完成。通常该函数的实现很简单,只需要将传下 来的 scsi 命令挂载到 host 的 scsi 命令队列中。由于 queuecommand 函数在持有 spinlock 的上 下文中运行,所以不宜做过多复杂的操作,否则很容易导致程序睡眠,从而使程序运行不稳 定。 一路走来的兄弟一定会一眼就看出这里我们最期待的一行代码就是 571那个queuecommand() 的调用。因为这之后我们就知道该发生什么了。比如对于 U 盘驱动来说,命令就从这里接 过去开始执行。而对于实际的 scsi 控制器,其对应的驱动中的 queuecommand 也会被调用, 剩下的事情我们就不用操心了。正常情况下 queuecommand 返 回 0 。 于 是 紧 接 着 scsi_dispatch_cmd 也返回 0。这样就算是执行了一条 scsi 命令了。 queuecomand 函数的实现由 scsi host driver 完成。通常该函数的实现很简单,只需要将传下 来的 scsi 命令 scsi_cmnd 挂载到 host 的 scsi 命令队列中。由于 queuecommand 函数在持有 spinlock 的上下文中运行,所以不宜做过多复杂的操作,否则很容易导致程序睡眠,从而使 程序运行不稳定。 而 scsi_request_fn() 是 否 结 束 还 得 看 while 循环的条件是否满足,而这就得看 blk_queue_plugged()的脸色了。那么我们从字面上来分析,什么叫 queue plugged?比如说, 北四环上上下班高峰期,许许多多的车辆排成一队又一队,但是可能半天都前进不了,这就 叫 plugged,或者说堵车,也叫塞车。为此咱们使用一个 flag 来标志堵车与否,来自 include/linux/blkdev.h: 523 #define blk_queue_plugged(q) test_bit(QUEUE_FLAG_PLUGGED, &(q)->queue_flags) 改变这个这个 flag 的函数有两个,一个是设置,一个是取消。负责设置的是 blk_plug_device, 负责取消的是 blk_remove_plug(),前面讲的理论知识,这里就用到了。 5.6.5 scsi 命令的第一次转变 前面介绍了读写文件时,是通过 sd_init_command 设置的 scsi_cmnd 命令结构,其实我们也 可以通过 scsi_execute_req 函数直接发送 scsi 命令,不过需要两次转变。 仍然以 scsi 磁盘举例,最初 scsi 这边发送的是 scsi 命令,可是从 block 走就得变成 request, 然而走到磁盘那边又得变回 scsi 命令,换言之,这整个过程 scsi 命令要变两次身。 比如,我们想获得磁盘的容量,就可以直接调用: cmd[0] = READ_CAPACITY; res = scsi_execute_req(sdp, cmd, DMA_NONE, NULL, 0, &sshdr, SD_TIMEOUT, SD_MAX_RETRIES); 首先让我们从 scsi 磁盘那边很常用的一个函数开始(比如 sd_ioctl),我们来看 scsi 命令是如 何在光天化日之下被偷梁换柱的变成了 request,这个函数就是 scsi_execute_req()。来自位于SCSI 中间层的 drivers/scsi/scsi_lib.c: 216 int scsi_execute_req(struct scsi_device *sdev, const unsigned char *cmd, 217 int data_direction, void *buffer, unsigned bufflen, 218 struct scsi_sense_hdr *sshdr, int timeout, int retries) 219 { 220 char *sense = NULL; 221 int result; 222 223 if (sshdr) { 224 sense = kzalloc(SCSI_SENSE_BUFFERSIZE, GFP_NOIO); 225 if (!sense) 226 return DRIVER_ERROR << 24; 227 } 228 result = scsi_execute(sdev, cmd, data_direction, buffer, bufflen, 229 sense, timeout, retries, 0); 230 if (sshdr) 231 scsi_normalize_sense(sense, SCSI_SENSE_BUFFERSIZE, sshdr); 232 233 kfree(sense); 234 return result; 235 } 这里面最需要关注的就是一个函数, scsi_execute(),来自同一个文件: 179 int scsi_execute(struct scsi_device *sdev, const unsigned char *cmd, 180 int data_direction, void *buffer, unsigned bufflen, 181 unsigned char *sense, int timeout, int retries, int flags) 182 { 183 struct request *req; 184 int write = (data_direction == DMA_TO_DEVICE); 185 int ret = DRIVER_ERROR << 24; 186 187 req = blk_get_request(sdev->request_queue, write, __GFP_WAIT); 188 189 if (bufflen && blk_rq_map_kern(sdev->request_queue, req, 190 buffer, bufflen, __GFP_WAIT)) 191 goto out; 192 193 req->cmd_len = COMMAND_SIZE(cmd[0]); 194 memcpy(req->cmd, cmd, req->cmd_len); 195 req->sense = sense; 196 req->sense_len = 0; 197 req->retries = retries; 198 req->timeout = timeout; 199 req->cmd_type = REQ_BLOCK_PC; 200 req->cmd_flags |= flags | REQ_QUIET | REQ_PREEMPT; 201 202 /* 203 * head injection *required* here otherwise quiesce won't work 204 */ 205 blk_execute_rq(req->q, NULL, req, 1); 206 207 ret = req->errors; 208 out: 209 blk_put_request(req); 210 211 return ret; 212 } 首先被调用的是 blk_get_request.来自 block/ll_rw_blk.c: 2215 struct request *blk_get_request(request_queue_t *q, int rw, gfp_t gfp_mask) 2216 { 2217 struct request *rq; 2218 2219 BUG_ON(rw != READ && rw != WRITE); 2220 2221 spin_lock_irq(q->queue_lock); 2222 if (gfp_mask & __GFP_WAIT) { 2223 rq = get_request_wait(q, rw, NULL); 2224 } else { 2225 rq = get_request(q, rw, NULL, gfp_mask); 2226 if (!rq) 2227 spin_unlock_irq(q->queue_lock); 2228 } 2229 /* q->queue_lock is unlocked at this point */ 2230 2231 return rq; 2232 } 注意到我们调用这个函数的时候,第二个参数确实是 __GFP_WAIT。所以 2223 行会被执行。 get_request_wait()来自同一个文件: 2173 static struct request *get_request_wait(request_queue_t *q, int rw_flags, 2174 struct bio *bio) 2175 { 2176 const int rw = rw_flags & 0x01; 2177 struct request *rq; 2178 2179 rq = get_request(q, rw_flags, bio, GFP_NOIO); 2180 while (!rq) { 2181 DEFINE_WAIT(wait); 2182 struct request_list *rl = &q->rq; 2183 2184 prepare_to_wait_exclusive(&rl->wait[rw], &wait, 2185 TASK_UNINTERRUPTIBLE); 2186 2187 rq = get_request(q, rw_flags, bio, GFP_NOIO); 2188 2189 if (!rq) { 2190 struct io_context *ioc; 2191 2192 blk_add_trace_generic(q, bio, rw, BLK_TA_SLEEPRQ); 2193 2194 __generic_unplug_device(q); 2195 spin_unlock_irq(q->queue_lock); 2196 io_schedule(); 2197 2198 /* 2199 * After sleeping, we become a "batching" process and 2200 * will be able to allocate at least one request, and 2201 * up to a big batch of them for a small period time. 2202 * See ioc_batching, ioc_set_batching 2203 */ 2204 ioc = current_io_context(GFP_NOIO, q->node); 2205 ioc_set_batching(q, ioc); 2206 2207 spin_lock_irq(q->queue_lock); 2208 } 2209 finish_wait(&rl->wait[rw], &wait); 2210 } 2211 2212 return rq; 2213 } 而真正被调用的又是 get_request(),仍然是来自同一个文件。 2068 static struct request *get_request(request_queue_t *q, int rw_flags, 2069 struct bio *bio, gfp_t gfp_mask) 2070 { 2071 struct request *rq = NULL; 2072 struct request_list *rl = &q->rq; 2073 struct io_context *ioc = NULL; 2074 const int rw = rw_flags & 0x01; 2075 int may_queue, priv; 2076 2077 may_queue = elv_may_queue(q, rw_flags); 2078 if (may_queue == ELV_MQUEUE_NO) 2079 goto rq_starved; 2080 2081 if (rl->count[rw]+1 >= queue_congestion_on_threshold(q)) { 2082 if (rl->count[rw]+1 >= q->nr_requests) { 2083 ioc = current_io_context(GFP_ATOMIC, q->node); 2084 /* 2085 * The queue will fill after this allocation, so set 2086 * it as full, and mark this process as "batching". 2087 * This process will be allowed to complete a batch of 2088 * requests, others will be blocked. 2089 */ 2090 if (!blk_queue_full(q, rw)) { 2091 ioc_set_batching(q, ioc); 2092 blk_set_queue_full(q, rw); 2093 } else { 2094 if (may_queue != ELV_MQUEUE_MUST 2095 && !ioc_batching(q, ioc)) { 2096 /* 2097 * The queue is full and the allocating 2098 * process is not a "batcher", and not 2099 * exempted by the IO scheduler 2100 */ 2101 goto out; 2102 } 2103 } 2104 } 2105 blk_set_queue_congested(q, rw); 2106 } 2107 2108 /* 2109 * Only allow batching queuers to allocate up to 50% over the defined 2110 * limit of requests, otherwise we could have thousands of requests 2111 * allocated with any setting of ->nr_requests 2112 */ 2113 if (rl->count[rw] >= (3 * q->nr_requests / 2)) 2114 goto out; 2115 2116 rl->count[rw]++; 2117 rl->starved[rw] = 0; 2118 2119 priv = !test_bit(QUEUE_FLAG_ELVSWITCH, &q->queue_flags); 2120 if (priv) 2121 rl->elvpriv++; 2122 2123 spin_unlock_irq(q->queue_lock); 2124 2125 rq = blk_alloc_request(q, rw_flags, priv, gfp_mask); 2126 if (unlikely(!rq)) { 2127 /* 2128 * Allocation failed presumably due to memory. Undo anything 2129 * we might have messed up. 2130 * 2131 * Allocating task should really be put onto the front of the 2132 * wait queue, but this is pretty rare. 2133 */ 2134 spin_lock_irq(q->queue_lock); 2135 freed_request(q, rw, priv); 2136 2137 /* 2138 * in the very unlikely event that allocation failed and no 2139 * requests for this direction was pending, mark us starved 2140 * so that freeing of a request in the other direction will 2141 * notice us. another possible fix would be to split the 2142 * rq mempool into READ and WRITE 2143 */ 2144 rq_starved: 2145 if (unlikely(rl->count[rw] == 0)) 2146 rl->starved[rw] = 1; 2147 2148 goto out; 2149 } 2150 2151 /* 2152 * ioc may be NULL here, and ioc_batching will be false. That's 2153 * OK, if the queue is under the request limit then requests need 2154 * not count toward the nr_batch_requests limit. There will always 2155 * be some limit enforced by BLK_BATCH_TIME. 2156 */ 2157 if (ioc_batching(q, ioc)) 2158 ioc->nr_batch_requests--; 2159 2160 rq_init(q, rq); 2161 2162 blk_add_trace_generic(q, bio, rw, BLK_TA_GETRQ); 2163 out: 2164 return rq; 2165 } 这个 elv_may_queue 来自 block/elevator.c: 848 int elv_may_queue(request_queue_t *q, int rw) 849 { 850 elevator_t *e = q->elevator; 851 852 if (e->ops->elevator_may_queue_fn) 853 return e->ops->elevator_may_queue_fn(q, rw); 854 855 return ELV_MQUEUE_MAY; 856 } 属于我们的那个 elevator_t结构体变量是当初我们在 elevator_init()中调用elevator_alloc()申请 的。它的 ops 显然是和具体我们采用了哪种电梯有关系的。这里我们为了简便起见,选择 “noop”,这种最简单最原始的机制。再一次贴出它的 elevator_type: 87 static struct elevator_type elevator_noop = { 88 .ops = { 89 .elevator_merge_req_fn = noop_merged_requests, 90 .elevator_dispatch_fn = noop_dispatch, 91 .elevator_add_req_fn = noop_add_request, 92 .elevator_queue_empty_fn = noop_queue_empty, 93 .elevator_former_req_fn = noop_former_request, 94 .elevator_latter_req_fn = noop_latter_request, 95 .elevator_init_fn = noop_init_queue, 96 .elevator_exit_fn = noop_exit_queue, 97 }, 98 .elevator_name = "noop", 99 .elevator_owner = THIS_MODULE, 100 }; 我们看到,对于我们选择的这种 noop 的电梯,elevator_may_queue_fn 根本就没有定义。所 以带着一个返回值 ELV_MQUEUE_MAY,我们返回到 get_request()中来。rl 又是什么呢? 2072 行我们让它指向了 q->rq,它就是 request_queue。 这里我们看到了 rq 其实是 struct request_list 结构体变量,在前面输入 /输出调度程序中见过 了。不过这些我们现在都不想看,我们想看的只有其中的几个函数,第一个是 2125 行blk_alloc_request(),来自 ll_rw_blk.c: 1970 static struct request * 1971 blk_alloc_request(request_queue_t *q, int rw, int priv, gfp_t gfp_mask) 1972 { 1973 struct request *rq = mempool_alloc(q->rq.rq_pool, gfp_mask); 1974 1975 if (!rq) 1976 return NULL; 1977 1978 /* 1979 * first three bits are identical in rq->cmd_flags and bio->bi_rw, 1980 * see bio.h and blkdev.h 1981 */ 1982 rq->cmd_flags = rw | REQ_ALLOCED; 1983 1984 if (priv) { 1985 if (unlikely(elv_set_request(q, rq, gfp_mask))) { 1986 mempool_free(rq, q->rq.rq_pool); 1987 return NULL; 1988 } 1989 rq->cmd_flags |= REQ_ELVPRIV; 1990 } 1991 1992 return rq; 1993 } 其它我们不懂没有关系,至少我们从 1972 行可以看出这里申请了一个 struct request 的结构 体指针,换句话说,此前,我们已经有了请求队列,但是没有实质性的元素,从这一刻起, 我们有了一个真正的 request。虽然现在还没有进入到队伍中去,但这只是早晚的事儿了。 请看下面: 下一个,get_request()的 2160 行的 rq_init(): 238 static void rq_init(request_queue_t *q, struct request *rq) 239 { 240 INIT_LIST_HEAD(&rq->queuelist); 241 INIT_LIST_HEAD(&rq->donelist); 242 243 rq->errors = 0; 244 rq->bio = rq->biotail = NULL; 245 INIT_HLIST_NODE(&rq->hash); 246 RB_CLEAR_NODE(&rq->rb_node); 247 rq->ioprio = 0; 248 rq->buffer = NULL; 249 rq->ref_count = 1; 250 rq->q = q; 251 rq->special = NULL; 252 rq->data_len = 0; 253 rq->data = NULL; 254 rq->nr_phys_segments = 0; 255 rq->sense = NULL; 256 rq->end_io = NULL; 257 rq->end_io_data = NULL; 258 rq->completion_data = NULL; 259 } 这个函数在干什么?很简单,就是对刚申请的 rq 进行初始化。 然后,get_request()就开开心心的返回了,正常情况下, get_request_wait()也会跟着返回,再 接着,blk_get_request()也就返回了。我们也带着申请好初始化好的 req 回到 scsi_execute() 中去,而接下来一段代码就是我们最关心的,对 req 的真正的赋值,比如 req->cmd_len, req->cmd 等等,就是这样被赋上的。换言之,我们的 scsi 命令就是这样被 request 拖下水的, 从此它们之间不再是以前那种“水留不住落花的漂泊,落花走不进水的世界”的关系,而是 沦落到了一荣俱荣一损俱损狼狈为奸的关系。 至此,完成了第一次变身,从 scsi 命令到 request 的变身。 5.6.6 scsi 命令的第二次转变 一旦这种关系建立好了以后,就可以开始执行请求了。来看 blk_execute_rq(),来自 block/ll_rw_blk.c: 2616 int blk_execute_rq(request_queue_t *q, struct gendisk *bd_disk, 2617 struct request *rq, int at_head) 2618 { 2619 DECLARE_COMPLETION_ONSTACK(wait); 2620 char sense[SCSI_SENSE_BUFFERSIZE]; 2621 int err = 0; 2622 2623 /* 2624 * we need an extra reference to the request, so we can look at 2625 * it after io completion 2626 */ 2627 rq->ref_count++; 2628 2629 if (!rq->sense) { 2630 memset(sense, 0, sizeof(sense)); 2631 rq->sense = sense; 2632 rq->sense_len = 0; 2633 } 2634 2635 rq->end_io_data = &wait; 2636 blk_execute_rq_nowait(q, bd_disk, rq, at_head, blk_end_sync_rq); 2637 wait_for_completion(&wait); 2638 2639 if (rq->errors) 2640 err = -EIO; 2641 2642 return err; 2643 } 抛去那些用于错误处理的代码,这个函数真正有意义的代码就是两行, blk_execute_rq_nowait 和 wait_for_completion。先看前者,来自 block/ll_rw_blk.c: 2588 void blk_execute_rq_nowait(request_queue_t *q, struct gendisk *bd_disk, 2589 struct request *rq, int at_head, 2590 rq_end_io_fn *done) 2591 { 2592 int where = at_head ? ELEVATOR_INSERT_FRONT : ELEVATOR_INSERT_BACK; 2593 2594 rq->rq_disk = bd_disk; 2595 rq->cmd_flags |= REQ_NOMERGE; 2596 rq->end_io = done; 2597 WARN_ON(irqs_disabled()); 2598 spin_lock_irq(q->queue_lock); 2599 __elv_add_request(q, rq, where, 1); 2600 __generic_unplug_device(q); 2601 spin_unlock_irq(q->queue_lock); 2602 } 首先 at_head 是表示往哪插。 而 where 用来记录 at_head 的值。在我们这个上下文中, at_head 是从 scsi_execute()中调用 blk_execute_rq 的时候传递下来的,当时我们设置的是 1 。 于 是 where 被 设 置 为 ELEVATOR_INSERT_FRONT。 回到 blk_execute_rq_nowait()中,下一个被调用的函数是 __generic_unplug_device,依然是来 自 block/ll_rw_blk.c: 1589 void __generic_unplug_device(request_queue_t *q) 1590 { 1591 if (unlikely(blk_queue_stopped(q))) 1592 return; 1593 1594 if (!blk_remove_plug(q)) 1595 return; 1596 1597 q->request_fn(q); 1598 } 其实最有看点的就是 1597 行调用这个 request_fn,struct request_queue 中的一个成员 request_fn_proc *request_fn,而至于 request_fn_proc,其实又是 typedef 的小伎俩,来自 include/linux/blkdev.h: 334 typedef void (request_fn_proc) (request_queue_t *q); 那么这个 request_fn 是多少呢?还记得当初那个 scsi 子系统中申请队列的函数了么?没错, 就是__scsi_alloc_queue(),设置成的 scsi_request_fn 函数。这个函数调用 elv_next_request 跟 我们前面看到的一样,只不过在执行 scsi_prep_fn 的时候,由于 request 的标识已经不是 按正路,我们会走到 1229 行这个 switch 语句,并且会根据 scsi 命令的类型而执行不同的函 数:scsi_setup_blk_pc_cmnd 或者 scsi_setup_fs_cmnd。那么我们 cmd_type 究竟是什么呢?跟 前面不同了,前面是 REQ_CMD,而我们这次是在 scsi_execute()中有这么一行: 199 req->cmd_type = REQ_BLOCK_PC; 所以,没什么好说的,我们会执行 scsi_setup_blk_pc_cmnd,来自 drivers/scsi/scsi_lib.c: 1090 static int scsi_setup_blk_pc_cmnd(struct scsi_device *sdev, struct request *req) 1091 { 1092 struct scsi_cmnd *cmd; 1093 1094 cmd = scsi_get_cmd_from_req(sdev, req); 1095 if (unlikely(!cmd)) 1096 return BLKPREP_DEFER; 1097 1098 /* 1099 * BLOCK_PC requests may transfer data, in which case they must 1100 * a bio attached to them. Or they might contain a SCSI command 1101 * that does not transfer data, in which case they may optionally 1102 * submit a request without an attached bio. 1103 */ 1104 if (req->bio) { 1105 int ret; 1106 1107 BUG_ON(!req->nr_phys_segments); 1108 1109 ret = scsi_init_io(cmd); 1110 if (unlikely(ret)) 1111 return ret; 1112 } else { 1113 BUG_ON(req->data_len); 1114 BUG_ON(req->data); 1115 1116 cmd->request_bufflen = 0; 1117 cmd->request_buffer = NULL; 1118 cmd->use_sg = 0; 1119 req->buffer = NULL; 1120 } 1121 1122 BUILD_BUG_ON(sizeof(req->cmd) > sizeof(cmd->cmnd)); 1123 memcpy(cmd->cmnd, req->cmd, sizeof(cmd->cmnd)); 1124 cmd->cmd_len = req->cmd_len; 1125 if (!req->data_len) 1126 cmd->sc_data_direction = DMA_NONE; 1127 else if (rq_data_dir(req) == WRITE) 1128 cmd->sc_data_direction = DMA_TO_DEVICE; 1129 else 1130 cmd->sc_data_direction = DMA_FROM_DEVICE; 1131 1132 cmd->transfersize = req->data_len; 1133 cmd->allowed = req->retries; 1134 cmd->timeout_per_command = req->timeout; 1135 cmd->done = scsi_blk_pc_done; 1136 return BLKPREP_OK; 1137 } 如果曾经的你还对 scsi cmd 是如何形成的颇有疑义的话,那么相信此刻,你应该会明白了吧, 尤其是当你在 usb-storage 那个故事中看到对它 sc_data_direction 的判断的时候,你不理解这 个值是如何设定的,那么此刻,这代码活生生的展现在你面前,想必已经揭开了你心中那谜 团吧。 最终,正常的话,函数返回 BLKPREP_OK。prep 表示 prepare 的意思,用我们的母语说就 是准备的意思,最后 BLKPREP_OK 就说明准备好了,或者说准备就绪。而 scsi_prep_fn() 也将返回这个值,返回之前还设置了 cmd_flags 中 的 REQ_DONTPREP 。( 注 意 elv_next_request()函数 741 行判断的就是设这个 flag。) 后面的工作就和前面“ scsi 块设备驱动层处理”的内容一样了。 5.7 写文件 回想一下, write()系统调用涉及把数据从调用进程的用户态地址空间中移动到内核数据结构 中,然后再移动到磁盘上。文件对象的 write 方法允许每种文件类型都定义一个专用的写操 作。在 Linux 2.6 中,每个磁盘文件系统的 write 方法都是一个过程,该过程主要标识写操作 所涉及的磁盘块,把数据从用户态地址空间拷贝到页高速缓存的某些页中,然后把这些页中 的缓冲区标记成脏。 5.7.1 generic file_write 函数 许多文件系统(包括 Ext2 或 JFS)通过 generic file_write()函数来实现文件对象的 write 方法。 它有如下参数: file:文件对象指针 buf:用户态地址空间中的地址,必须从这个地址获取要写入文件的字符 count:要写人的字符个数 ppos:存放文件偏移量的变量地址,必须从这个偏移量处开始写入 该函数执行以下操作: 1. 初始化 iovec 类型的一个局部变量,它包含用户态缓冲区的地址与长度(参见前面对 generic_file_read()函数的描述)。 2. 确定所写文件索引节点对象的地址 inode(file->f_mapping->host)和获得信号量 (inode->i_sem)。有了这个信号量,一次只能有一个进程对某个文件发出 write()系统调 用。 3. 调用宏 init_sync_kiocb 初始化 kiocb 类型的局部变量。就像前面从文件读取数据描述的 那样,该宏将 ki_key 字段设置为 KIOCB_SYNC_KEY 同步 I/O 操作)、 ki_filp 字段设置 为 filp、ki_obj 字段设置为 current。 4. 调用_generic_file_aio_write_nolock()函数(见下面)将涉及的页标记为脏,并传递相应 的参数:iovec 和 kiocb 类型的局部变量地址、用户态缓冲区的段数(这里只有一个) 和 ppos。 5. 释放 inode->i_sem 信号量。 6. 检查文件的 O_SYNC 标志、索引节点的 S_SYNC 标志及超级块的 MS_SYNCHRONOUS 标志。如果至少一个标志置位,则调用函数 sync_page_range()来强制内核高速缓存中第 4 步涉及的所有页刷新,阻塞当前进程直到 I/O 数据传输结束。然后依次地, sync_page_range()先执行 address_space 对象的 writepages 方法(如果有定义)或 mpage_writepages()函数来开始这些脏页的 I/O 传输(参见后面“将脏页写到磁盘”一节), 然后调用 generic_osync_inode()将索引节点和相关的缓冲区刷新到磁盘,最后调用 wait_on_page_bit()挂起当前进程一直到全部所刷新页的 PG_writeback 标志清 0。 7. 将__generic_file_aio_write_nolock()函数的返回值返回,通常是写入的有效字节数。 函数__generic_file_aio_write_nolock()接收四个参数: kiocb 描述符的地址 iocb、iovec 描述符 数 组 的 地 址 iov、该数组的长度以及存放文件当前指针的变量的地址 ppos。 当 被 generic_file_write()调用时,iovec 描述符数组只有一个元素,该元素描述待写数据的用户态 缓冲区(系统调用 write()的一个叫做 writev()的变体允许应用程序定义多个用户态缓冲区, 从中可以获取待写入文件的数据。函数 generic_file_aio_write_nolock()也具有这种功能。下 面我们假设将从一个用户缓冲区取数据,不过我们可以想象,使用多个缓冲区虽然简单,但 需要执行更多的步骤)。 我们现在来解释__generic_file_aiowrite_nolock()函数的行为。为简单起见,我们只讨论最常 见的情形,即对有页高速缓存的文件进行 write()系统调用的一般情况。我们在本章后面会讨 论该函数在其他情况下的行为。我们不讨论如何处理错误和异常条件。 该函数执行如下步骤: 1. 调用 access_ok()确定 iovec 描述符所描述的用户态缓冲区是有效的(起始地址和长度已 从服务例程 sys_write()得到,因此使用前必须对其检查)。如果参数无效,则返回错误 -EFAULT。 2. 确定待写文件( file->f_mapping->host)索引节点对象的地址 inode。记住:如果文件是一 个块设备文件,这就是一个 bdev 特殊文件系统的索引节点。 3. 将文件(file->f_mapping->backing_dev_info)的 backing_dev_info 描述符的地址设为 current->backing_dev_info。实际上,即使相应请求队列是拥塞的,这个设置也会允许当 前进程写回由 file->f_mapping 拥有的脏页。 4. 如果 file->flags 的 O_APPEND 标志置位而且文件是普通文件(非块设备文件),它将 *ppos 设为文件尾,从而新数据将都追加到文件的后面。 5. 对文件大小进行几次检查。比如,写操作不能把一个普通文件增大到超过每用户的上限 或文件系统的上限,每用户上限存放在 current->signal->rlim[RLIMIT_FSIZE],文件系 统上限存放在 inode->i_ob->s_maxbytes。另外,如果文件不是“大型文件”(当 file->f_flags 的 O_LARGEFILE 标志清 0 时),那么它的大小不能超出 2GB。如果没有设定所述限制, 它就减少待写字节数。 6. 如果设定,则将文件的 suid 标志清 0,而且如果是可执行文件的话就将 sgid 标志也清 0。 我们并不要用户能修改 setuid 文件。 7. 将当前时间存放在 inode->mtime 字段(文件写操作的最新时间)中,也存放在 inode->ctime 字段(修改索引节点的最新时间)中,而且将索引节点对象标记为脏。 8. 开始循环以更新写操作中涉及的所有文件页。在每次循环期间,执行下列子步骤: a) 调用 find_lock_page()在页高速缓存中搜索该页。如果函数找到了该页,则增加引 用计数并将 PG_locked 标志置位。 b) 如果该页不在页高速缓存中,则分配一个新页框并调用 add_to_page_cache()在页高 速缓存内插入此页。正如 “页高速缓存的处理函数”一节所述的那样,这个函数 也会增加引用计数并将 PG_locked 标志置位。另外函数还在内存管理区的非活动链 表中插入一页。 c) 调用索引节点( file->f_mapping)中 address_space 对象的 prepare_write 方法。对应 的函数会为该页分配和初始化缓冲区首部。我们在后面的章节中讨论该函数对于普 通文件和块设备文件做些什么。 d) 如果缓冲区在高端内存中,则建立用户态缓冲区的内核映射,然后它调用 __copy_from_user()把用户态缓冲区中的字符拷贝到页中,并且释放内核映射。 e) 调用索引节点( file->f_mapping)中 address_space 对象的 commit_write 方法。对应 的函数把基础缓冲区标记为脏,以便随后把它们写到磁盘。我们在后面两节讨论该 函数对于普通文件和块设备文件做些什么。 f) 调用 unlock_page()清 PG_locked 标志,并唤醒等待该页的任何进程。 g) 调用 mark_page_accessed()来为内存回收算法更新页状态。 h) 减少页引用计数来撤销第 8a 或 8b 步中的增加值。 i) 在这一步,还有另一页被标记为脏,它检查页高速缓存中脏页比例是否超过了一个 固定的阈值(通常为系统中页的 40%)。如果这样,则调用 writeback_inodes()来刷 新几十页到磁盘(参见 “搜索要刷新的脏页”一节)。 j) 调用 cond_resched()来检查当前进程的 TIF_NEED_RESCHED 标志。如果该标志置 位,则调用 schedule()函数。 9. 现在,在写操作中所涉及的文件的所有页都已处理。更新 *ppos 的值,让它正好指向最 后一个被写入的字符之后的位置。 10. 设置 current->backing_dev_info 为 NULL(参见第 3 步)。 11. 返回写入文件的有效字符数后结束。 5.7.2 普通文件的 prepare_write 方法 address_space 对象的 prepare_write 和 commit_write 方法专用于由 generic_file_write()实现的 通用写操作,这个函数适用于普通文件和块设备文件。对文件的受写操作影响的每一页,调 用一次这两个方法。 每个磁盘文件系统都定义了自己的 prepare_write 方法。与读操作类似,这个方法只不过是普 通函数的一个封装函数。例如, Ext2 文件系统通过下列函数实现 prepare_write 方法: int ext2_prepare_write(struct file *file, struct page *page, unsigned from, unsigned to) { return block_prepare_write(page, from, to, ext2_get_block); } 在前面“从文件读取数据”一节已经提到 ext2_get_block()函数;它把相对于文件的块号转 换为逻辑块号(表示数据在物理块设备上的位置)。 block_prepare_write()函数通过执行下列步骤为文件页的缓冲区和缓冲区首部做准备: 1. 检查某页是否是一个缓冲区页(如果是则 PG_Private 标志置位);如果该标志清 0,则 调用 create_empty_buffers()为页中所有的缓冲区分配缓冲区首部。 2. 对与页中包含的缓冲区对应的每个缓冲区首部,及受写操作影响的每个缓冲区首部,执 行下列操作: a) 如果 BH_New 标志置位,则将它清 0(参见下面)。 b) 如果 BH_New 标志已清 0,则函数执行下列子步骤: i. 调用依赖于文件系统的函数,该函数的地址 get_block 以参数形式传递过来。查看这个文件系统磁盘数据结构并查找缓冲区的逻辑块号(相对于磁盘分区的 起始位置而不是普通文件的起始位置)。与文件系统相关的函数把这个数存放 在对应缓冲区首部的 b_blocknr 字段,并设置它的 BH_Mapped 标志。与文件 系统相关的函数可能为文件分配一个新的物理块(例如,如果访问的块掉进普 通文件的一个“洞”中)。在这种情况下,就设置 BH_New 标志。 ii. 检查 BH_New 标志的值;如果它被置位,则调用 unmap_underlying_metadata() 来检查页高速缓存内的某个块设备缓冲区页是否包含指向磁盘同一块的一个 缓冲区(尽管可能性不大,但在一个用户直接向块设备文件写数据块时,还是 会出现这种情况,从而越过文件系统)。该函数实际上调用__find_get_lock() 在页高速缓存内查找一个旧块。如果找到一块,函数将 BH_Dirty 标志清 0 并 等待直到该缓冲区的 I/O 数据传输完毕。此外,如果写操作不对整个缓冲区进 行重写,则用 0 填充未写区域。然后考虑页中的下一个缓冲区。 c) 如果写操作不对整个缓冲区进行重写且它的 BH_Delay 和 BH_Uptodate 标志未置位 (也就是说,已在磁盘文件系统数据结构中分配了块,但是 RAM 中的缓冲区并没 有有效的数据映像),函数对该块调用 ll_rw_block()从磁盘读取它的内容。 3. 阻塞当前进程,直到在第 2c 步触发的所有读操作全部完成。 4. 返回 0。 一旦 prepare_write 方法返回,generic_file_write()函数就用存放在用户态地址间中的数据更新 页。接下来,调用 address_space 对象的 commit_write 方法。这个方法由 generic_commit_write() 函数实现,几乎适用于所有非日志型磁盘文件系统。 generic_commit_write()函数执行下列步骤: 1. 调用__block_commit_write()函数,然后依次执行如下步骤: a) 考虑页中受写操作影响的所有缓冲区;对于其中的每个缓冲区,将对应缓冲区首部 的 BH_Uptodate 和 BH_Dirty 标志置位。 b) 标记相应索引节点为脏,这需要将索引节点加入超级块脏的索引节点链表。 c) 如果缓冲区页中的所有缓冲区是最新的,则将 PG_uptodate 标志置位。 d) 将页的 PG_dirty 标志置位,并在基树中将页标记成脏。 2. 检查写操作是否将文件增大。如果增大,则更新文件索引节点对象的 i_size 字段。 3. 返回 0。 5.7.3 块设备文件的 prepare_write 方法 写入块设备文件的操作非常类似于对普通文件的相应操作。事实上,块设备文件的 address_space 对象的 prepare_write 方法通常是由下列函数实现的: int blkdev_prepare_write(struct file *file, struct page *page, unsigned from, unsigned to) { return block_prepare_write(page, from, to, blkdev_get_block); } 你可以看到,这个函数只不过是前一节讨论过的 block_prepare_write()函数的封装函数。当 然,唯一的差异是第二个参数,它是一个指向函数的指针,该函数必须把相对于文件开始处 的文件块号转换为相对于块设备开始处的逻辑块号。回想一下,对于块设备文件来说,这两 个数是一致的(参见前面对 blkdev_get_block()函数的讨论)。 用于块设备文件的 commit__write 方法是由下列简单的封装函数实现的: int blkdev_commit_write(struct file *file, struct page *page, unsigned from, unsigned to) { return block_commit_write(page, from, to); } 正如你所看到的,用于块设备的 commit_write 方法与用于普通文件的 commit_write 方法本 质上做同样的事情(我们在前一节描述了 block_commit_write)函数)。唯一的差异是这个 方法不检查写操作是否扩大了文件;你根本不可能在块设备文件的末尾追加字符来扩大它。 5.7.4 将脏页写到磁盘 系统调用 write()的作用就是修改页高速缓存内一些页的内容,如果页高速缓存内没有所要的 页则分配并追加这些页。某些情况下(例如文件带 O_SYNC 标志打开), I/O 数据传输立即 启动(参见本章前面 generic_file_write()函数的第 6 步)。但是通常 I/O 数据传输是延迟进行 的。 当内核要有效启动 I/O 数据传输时,就要调用文件 address_space 对象的 writepages 方法,它 在基树中寻找脏页,并把它们刷新到磁盘。例如 Ext2 文件系统通过下面的函数实现 writepages 方法: int ext2_writepages(struct address_space *mapping, struct writeback_control *wbc) { return mpage_writepages(mapping, wbc, ext2_get_block); } 你可以看到,该函数是通用 mpage_writepages()的一个简单的封装函数。事实上,若文件系 统没有定义 writepages 方法,内核则直接调用 mpage_writepages()并把 NULL 传给第三个参 数。ext2_get_block()函数在前面“从文件读取数据”一节中已讲到过,这是一个依赖于文件 系统的函数,它将文件块号转换成逻辑块号。 writeback_control 数据结构是一个描述符,它控制 writeback 写 回 操 作 如 何 执 行 。 mpage_writepages()函数执行下列步骤: 1. 如果请求队列写拥塞,但进程不希望阻塞,则不向磁盘写任何页就返回。 2. 确定文件的首页,如果 writeback_control 描述符给定一个文件内的初始位置,函数将把 它转换成页索引。否则,如果 writeback_control 描述符指定进程无需等待 I/O 数据传输 结束,它将 mapping->writeback_index 的值设为初始页索引(即从上一个写回操作的最 后一页开始扫描)。最后,如果进程必须等待 I/O 数据传输完毕,则从文件的第一页开 始扫描。 3. 调用 find_get_pages_tag()在页高速缓存中查找脏页描述符。 4. 对上一步得到的每个页描述符,执行如下步骤: a) 调用 lock_page()来锁定该页。 b) 确认页是有效的并在页高速缓存内(因为另一个内核控制路径可能已在第 3 步与第 4a 步间作用于该页)。 c) 检查页的 PG_writeback 标志。如果置位,表明页已被刷新到磁盘。如果进程必须 等待 I/O 数据传输完毕,则调用 wait_on_page_bit()在 PG_writeback 清 0 之前一直 阻塞当前进程;当函数结束时,以前运行的任何 writeback 操作都被终止。否则, 如果进程无需等待,它将检查 PG_dirty 标志:如果 PG_dirty 标志现已清 0,则正 在运行的写回操作将处理该页,将它解锁并跳回第 4a 步继续下一页。 d) 如果 get_block 的参数是 NULL(没有定义 writepages 方法),它将调用文件 address_space对象的 mapping->writepage方法将页刷新到磁盘。否则,如果 get_block 的参数不是 NULL,它就调用 mpage_writepage()函数。详见第 8 步。 5. 调用 cond_resched()来检查当前进程的 TIF_NEED_RESCHED 标志,如果该标志置位就 调用 schedule()函数。 6. 如果函数没有扫描完给定范围内的所有页,或者写到磁盘的有效页数小于 writeback_control 描述符中原先的给定值,那么跳回第 3 步。 7. 如果 writeback_control 描述符没有给定文件内的初始位置,它将最后一个扫描页的索引 值赋给 mapping->writeback_index 字段。 8. 如果在第 4d 步中调用了 mpage_writepage()函数,而且返回了 bio 描述符地址,那么调 用 mpage_bio_submit()(见下面)。 像 Ext2 这样的典型文件系统所实现的 writepage 方法是一个通用的 block_write_full_page() 函数的封装函数,并将依赖于文件系统的 get_block 函数的地址作为参数传给它。就像本章 前面“从文件读取数据”一节描述的 block_read_full_page()一样,block_write_full_page()函 数也依次执行:分配页缓冲区首部(如果还不在缓冲区页中),对每页调用 submit_bh()函数 来指定写操作。就块设备文件而言,就用 block_write_full_page()的封装函数 blkdev_writepage() 实现 writepage 方法。 许多非日志型文件系统依赖于 mpage_writepage()函数而不是自定义的 writepage 方法。这样 能改善性能,因为 mpage_writepage()函数进行 I/O 传输时,在同一个 bio 描迷符中聚集尽可 能多的页。这就使得块设备驱动程序能利用现代硬盘控制器的 DMA 分散-聚集能力。 长话短说, mpage_writepage()函数将检查:待写页包含的块在磁盘上是否不相邻,该页是否 包含文件洞,页上的某块是否没有脏或不是最新的。如果以上情况至少一条成立,函数就像 上面那样仍然用依赖于文件系统的 writepage 方法。否则,将页追加为 bio 描述符的一段。 bio 描述符的地址将作为参数被传给函数;如果该地址为 NULL,mpage_writepage()将初始 化 一 个 新 的 bio 描述符并将地址返回给调用函数,调用函数转而在未来调用 mpage_writepage()时再将该地址传回来。这样,同一个 bio 可以加载几个页。如果 bio 中某 页与上一个加载页不相邻, mpage_writepage()就调用 page_bio_submit()开始该 bio 的 I/O 数 据传输,并为该页分配一个新的 bio。 page_bio_submit()函数将 bio 的 bi_end_io 方法设为 mpage_end_io_write()的地址,然后调用 submit_bio()开始传输(参见第十五章“向涌用块层提交缓冲区首部”一节)。一旦数据传输 成功结束,完成函数 mpage_end_io_write()就唤醒那些等待页传输结束的进程,并清除 bio 描述符。 6 直接 I/O 与异步 I/O 我们已经看到,在 Linux 2.6 版本中,通过文件系统与通过引用基本块设备文件上的块,甚 至与通过建立文件内存映射访问一个普通文件之间没有什么本质的差异。但是,还是有一些 非常复杂的程序(自缓存的应用程序, self-caching application)更愿意具有控制 I/O 数据传送 机制的全部权力。例如,考虑高性能数据库服务器:它们大都实现了自己的高速缓存机制, 以充分挖掘对数据库独特的查询机制。对于这些类型的程序,内核页高速缓存毫无帮助;相 反,因为以下原因它可能是有害的: � 很多页框浪费在复制已在 RAM 中的磁盘数据上(在用户级磁盘高速缓存中)。 � 处理页高速缓存和预读的多余指令降低了 read()和 write()系统调用的执行效率,也降低 了与文件内存映射相关的分页操作。 � read()和 write()系统调用不是在磁盘和用户存储器之间直接传送数据,而是分两次传送: 在磁盘和内核缓冲区之间和在内核缓冲区与用户存储器之间。 因为必须通过中断和直接内存访问(DMA)处理块硬件设备,而且这只能在内核态完成, 因此,最终需要某种内核支持来实现自缓存的应用程序。 6.1 直接 I/O Linux 提供了绕过页高速缓存的简单方法:直接 I/O 传送。在每次 I/O 直接传送中,内核对 磁盘控制器进行编程,以便在自缓存的应用程序的用户态地址空间中的页与磁盘之间直接传 送数据。 我们知道,任何数据传送都是异步进行的。当数据传送正在进行时,内核可能切换当前进程, CPU 可能返回到用户态,产生数据传送的进程的页可能被交换出去,等等。这对于普通 I/O 数据传送没有什么影响,因为它们涉及磁盘高速缓存中的页,磁盘高速缓存由内核拥有,不 能被换出去,并且对内核态的所有进程都是可见的。 另一方面,直接 I/O 传送应当在给定进程的用户态地址空间的页内移动数据。内核必须当心 这些页是由内核态的任一进程访问的,当数据传送正在进行时不能把它们交换出去。让我们 看看这是如何实现的。 当自缓存的应用程序要直接访问文件时,它以 O_DIRECT 标志置位的方式打开文件。在运 行 open()系统调用时, dentry_open()函数检查打开文件的 address_space 对象是否有已实现的 direct_IO 方法,如果没有则返回错误码。对一个已打开的文件也可以由 fcntl()系统调用的 F_SETFL 命令把 O_DIRECT 置位。 让我们首先看第一种情况,这里自缓存应用程序对一个以 O_DIRECT 标志置位的方式打开 的文件调用 read()系统调用。在本章前面“从文件中读取数据”一节提到过,文件的 read 方法通常是由 generic_file_read()函数实现 的,它初 始化 iovec 和 kiocb 描述 符并调用 __generic_file_aio_read()。后面这个函数检查 iovec 描述符描述的用户态缓冲区是否有效,然 后检查文件的 O_DIRECT 标志是否置位。当被 read()调用时,该函数执行的代码段实际上等 效于下面的代码: if (filp->f_flags & O_DIRECT) { if (count == 0 || *ppos > filp->f_mapping->host->i_size) return 0; retval = generic_file_direct_IO(READ, iocb, iov, *ppos, 1); if (retval > 0) *ppos += retval; file_accessed(filp); return retval; } 函数检查文件指针的当前值、文件大小与请求的字符数,然后调用 generic_file_direct_IO() 函数,传给它 READ 操作类型、iocb 描述符、iovec 描述符、文件指针的当前值以及 io_vec 描述符中指定的用户态缓冲区号( 1 )。 当 generic_file_direct_IO() 结 束 时 , __generic_file_aio_read()更新文件指针,设置对文件索引节点的访问时间戳,然后返回。 对一个以 O_DIRECT 标志置位打开的文件调用 write()系统调用时,情况类似。在本章前面 “写入文件”一节讲到过,文件的 write 方法就是调用 generic_file_aio_write_nolock()。该函 数检查 O_DIRECT 标志是否置位,如果置位,则调用 generic_file_direct_IO()函数,而这次 限定的是 WRITE 操作类型。 generic_file_direct_IO()函数有以下参数: rw:操作类型:READ 或 WRITE iocb:kiocb 描述符指针 lov:iove 描述符数组指针 offset:文件偏移量 nr_segs:iov 数组中 iovec 描述符数 generic_file_direct_IO()函数的执行步骤如下: 1. 从 kiocb 描述符的 ki_filp 字段得到文件对象的地址 file,从 file->f_mapping 字段得到 address_space 对象的地址 mapping。 2. 如果操作类型为 WRITE,而且一个或多个进程已创建了与文件的某个部分关联的内存 映射,那么它调用 unmap_mapping_range()取消该文件所有页的内存映射。如果任何取 消映射的页所对应的页表项,其 Dirty 位置位,则该函数也确保它在页高速缓存内的相 应页被标记为脏。 3. 如 果 根 植 于 mapping 的 基 树 不 为 空 ( mapping->nrpages 大 于 0 ),则 调 用 filemap_fdatawrite()和 filemap_fdatawait()函数刷新所有脏页到磁盘,并等待 I/O 操作结 束。(即使自缓存应用程序是直接访问文件的,系统中还可能有通过页高速缓存访问文 件的其他应用程序。为了避免数据的丢失,在启动直接 I/O 传送之前,磁盘映像要与页 高速缓存进行同步)。 4. 调用 mapping 地址空间的 direct_IO 方法(参见下面的段落)。 5. 如果操作类型为 WRITE,则调用 invalidate_inode_pages2()扫描 mapping 基树中的所有 页并释放它们。该函数同时也清空指向这些页的用户态页表项。 大多数情况下, direct_IO 方法都是__blockdev_direct_IO()函数的封装函数。这个函数相当复 杂,它调用大量的辅助数据结构和函数,但是实际上它所执行的操作与本章所描述的操作一 样:对存放在相应块中要读或写的数据进行拆分,确定数据在磁盘上的位置,并添加一个或 多个用于描述要进行的 I/O 操作的 bio 描述符。当然,数据将被直接从 iov 数组中 iovec 描 述符确定的用户态缓冲区读写。调用 submit_io()函数将 bio 描述符提交给通用块层。通常情 况下,__blockdev_direct_IO()函数并不立即返回,而是等所有的直接 I/O 传送都已完成才返 回;因此,一旦 read()或 write()系统调用返回,自缓存应用程序就可以安全地访问含有文件 数据的缓冲区。 6.2 异步 I/O POSIX 1003.1 标准为异步方式访问文件定义了一套库函数(如表所示)。“异步”实际上就 是:当用户态进程调用库函数读写文件时,一旦读写操作进入队列函数就结束,甚至有可能 真正的 I/O 数据传输还没有开始。这样调用进程可以在数据正在传输时继续自己的运行。 函数 说明 aio_read( ) 从文件异步读数据 aio_write( ) 向文件异步写数据 aio_fsync( ) 请求刷新所有正在运行的异步 1/O 操作(不阻塞) aio_error( ) 获得正在运行的异步 I/O 操作的错误代码 aio_return( ) 获得一个已完成异步 1/O 操作的返回码 aio_cancel( ) 取消正在运行的异步 1/O 操作 aio_suspend( ) 挂起进程直到至少其中一个正在运行的异步 1/O 操作完成 使用异步 I/O 很简单,应用程序还是通过 open()系统调用打开文件,然后用描述请求操作的 信息填充 struct aiocb 类型的控制块。struct aiocb 控制块最常用的字段有 aio_fildes:文件的文件描述符(由 open()系统调用返回) aio_buf:文件数据的用户态缓冲区 aio_nbytes:待传输的字节数 aio_offset:读写操作在文件中的起始位置(与“同步”文件指针无关) 最后,应用程序将控制块地址传给 aio_read()或 aio_write()。一旦请求的 I/O 数据传输已由系 统库或内核送进队列,这两个函数就结束。应用程序稍后可以调用 aio_error()检查正在运行 的 I/O 操作的状态。如数据传输仍在进行当中,则返回 EINPROGRESS;如果成功完成,则 返回 0;如果失败,则返回一个错误码。 aio_return()函数返回已完成异步 I/O 操作的有效读 写字节数;或者如果失败,返回 -1。 6.2.1 Linux 2.6 中的异步 I/O 异步 I/O 可以由系统库实现而完全不需要内核支持。实际上 aio_read()或 aio_write()库函数克 隆当前进程,让子进程调用同步的 read()或 write()系统调用,然后父进程结束 aio_read()或 aio_write()函数并继续程序的执行。因此,它不用等待由子进程启动的同步操作完成。但是, 这个“穷人版” POSIX 函数比内核层实现的异步 I/O 要慢很多。 Linux 2.6 内核版运用一组系统调用实现异步 I/O。但在 Linux 2.6.11 中,这个功能还在实现 中,异步 I/O 只能用于打开 O_DIRECT 标志置位的文件。下表列出了异步 I/O 的系统调用: 系统调用 说明 io_setup( ) 为当前进程初始化一个异步环境 io_submit( ) 提交一个或多个异步 I/O 操作 io_getevents( ) 获得正在运行的异步 I/O 操作的完成状态 io_cancel( ) 取消一个正在运行的异步 I/O 操作 io_destroy( ) 删除当前进程的异步环境 6.2.2 异步 I/O 环境 如果一个用户态进程调用 io_submit()系统调用开始异步 I/O 操作,那么它必须预先创建一个 异步 I/O 环境。 基本上,一个异步 I/O 环境(简称 AIO 环境)就是一组数据结构,这个数据结构用于跟踪 进程请求的异步 I/O 操作的运行情况。每个 AIO 环境与一个 kioctx 对象关联,它存放了与 该环境有关的所有信息。一个应用可以创建多个 AIO 环境。一个给定进程的所有的 kioctx 描述符存放在一个单向链表中,该链表位于内存描述符的 ioctx_list 字段。 我们不再详细讨论 kioctx 对象。但是我们应当注意一个 kioctx 对象使用的重要的数据结构: AIO 环。 AIO 环是用户态进程中地址空间的内存缓冲区,它也可以由内核态的所有进程访问。 kioctx 对象中的 ring_info.mmap_base 和 ring_info.mmap_size 字段分别存放 AIO 环的用户态起始地 址和长度。 ring_info.ring_pages 字段则存放有一个数组指针,该数组存放所有含 AIO 环的页 框的描述符。 AIO 环实际上是一个环形缓冲区,内核用它来写正运行的异步 I/O 操作的完成报告。 AIO 环 的第一个字节有一个首部( struct aio_ring 数据结构),后面的所有字节是 io_event 数据结构, 每个都表示一个已完成的异步 I/O 操作。因为 AIO 环的页映射至进程的用户态地址空间, 应用可以直接检查正运行的异步 I/O 操作的情况,从而避免使用相对较慢的系统调用。 io_setup()系统调用为调用进程创建一个新的 AIO 环境。它有两个参数:正在运行的异步 I/O 操作的最大数目(这将确定 AIO 环的大小)和一个存放环境句柄的变量指针。这个句柄也 是 AIO 环的基地址。 sys_io_setup()服务例程实际上是调用 do_mmap()为进程分配一个存放 AIO 环的新匿名线性区,然后创建和初始化描述该 AIO 环境的 kioctx 对象。 相反地,io_destroy()系统调用删除 AIO 环境,还删除含有对应 AIO 环的匿名线性区。这个 系统调用阻塞当前进程直到所有正在运行的异步 I/O 操作结束。 6.2.3 提交异步 I/O 操作 为开始异步 I/O 操作,应用要调用 io_submit()系统调用。该系统调用有三个参数: ctx_id:由 io_setup()(标识 AIO 环境)返回的句柄 iocbpp:iocb 类型描述符的指针数组的地址,其中描述符的每项描述一个异步 I/O 操作 nr:iocbpp 指向的数组长度 iocb 数据结构与 POSIX aiocb 描述符有同样的字段 aio_fildes、aio_buf、aio_nbytes、aio_offset, 另外还有 aio_lio_opcode 字段存放请求操作的类型(典型地有: read, write 或 sync)。 sys_io_submit()服务例程执行下列步骤: 1. 验证 iocb 描述符数组的有效性。 2. 在内存描述符的 ioctx_list 字段所对应的链表中查找 ctx_id 句柄对应的 kioctx 对象。 3. 对数组中的每一个 iocb 描述符,执行下列子步骤: a) 获得 aio_fildes 字段中的文件描述符对应的文件对象地址。 b) 为该 I/O 操作分配和初始化一个新的 kiocb 描述符。 c) 检查 AIO 环中是否有空闲位置来存放操作的完成情况。 d) 根据操作类型设置 kiocb 描述符的 ki_retry 方法(见下面)。 e) 执行 aio_run_iocb()函数,它实际上调用 ki_retry 方法为相应的异步 I/O 操作启动数 据传输。如果 ki_retry 方法返回-EIOCBRETRY,则表示异步 I/O 操作已提交但还 没有完全成功:稍后在这个 kiocb 上,aio_run_iocb()函数会被再次调用(见下面); 否则,调用 aio_complete(),为异步 I/O 操作在 AIO 环境的环中追加完成事件。 如果异步 I/O 操作是一个读请求,那么对应 kiocb 描述符的 ki_retry 方法是由 aio_pread()实 现的。该函数实际上执行的是文件对象的 aio_read 方法,然后按照 aio_read 方法的返回值更 新 kiocb 描述符的 ki_buf 和 ki_left 字段。最后 aio_pread()返回从文件读入的有效字节数,或 者,如果函数确定请求的字节没有传输完,则返回 -EIOCBRETRY。对于大部分文件系统, 文件对象的 aio_read 方法就是调用__generic_file_aio_read()函数。假如文件的 O_DIRECT 标 志置位,函数就调用 generic_file_direct_IO()函数,这在上一节描述过。但在这种情况下, __blockdev_direct_IO()函数不是阻塞当前进程使之等待 I/O 数据传输完毕。而是立即返回。 因为异步 I/O 操作仍在运行, aio_run_iocb()会被再次调用,而这一次的调用者是 aio_wq 工作队列的 aio 内核线程。kiocb 描述符跟踪 I/O 数据传输的运行。终于所有数据传输完毕,将 完成结果追加到 AIO 环。 类似地,如果异步 I/O 操作是一个写请求,那么对应 kiocb 描述符的 ki_retry 方法是由 aio_pwrite()实现的。该函数实际上执行的是文件对象的 aio_write 方法,然后按照 aio_write 方法的返回值更新 kiocb 描述符的 ki_buf 和 ki_left 字段。最后 aio_pwrite()返回写入文件的 有效字节数,或者,如果函数确定请求的字节没有完全传输完,则返回 -EIOCBRETRY。对 于大部分文件系统,文件对象的 aio_write 方法就是调用 generic_file_aio_write_nolock()函数。 假如文件的 O_DIRECT 标志置位,跟上面一样,函数就调用 generic_file_direct_IO()函数。
还剩395页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 1 积分 [ 获取积分 ] 1 人已下载

下载pdf

pdf贡献者

dongAxis

贡献于2012-08-21

下载需要 1 积分 [ 获取积分 ]
下载pdf