1.基本说明
“open()在Linux内核的实现”系列文章将分析open系统调用在Linux内核中的实现过程。本系列文章分为六篇,每篇文章都描述 open()实现的一部分内容,与前后的系列文章保持相对独立。本文属于前序文章,集中说明后续文章涉及到的基本原理和基本数据结构,并且对整个分析过程进行Q&A。
本系列文章参考Linux内核源码版本为3.2.69。
2.数据结构
dentry结构
对于打开文件这个操作来说,它是通过路径名查找对应文件inode的过程,这里用户直面的是文件路径,而内核关注的inode。在文件路径和inode之间通过目录项(dentry)缓存进行关联,dentry缓存加快了vfs对文件的查找。所有的目录项通过散列表进行组织,这样可以快速对dentry进行查找;此外,内核将常用的dentry通过LRU算法进行组织,这样可以快速查到最近一段时间经常使用的dentry。
下面将对dentry中的部分字段进行说明。
d_inode:该字段指向目录项所关联的文件。如果该字段为空,则说明当前目录项指向的是一个并不存在的文件。
d_name:该字段表示目录项名称(并不是整个路径名),但它并不是单纯的字符串,而是将字符串文件名、字符串长度和散列值封装成qstr(quick string)结构,这样可以加速目录项的查找工作;
d_iname:当目录项名称长度小于DNAME_INLINE_LEN时,则该字符串名称则直接通过该字段进行存储;
d_parent:一个路径中的目录项形成层级结构。该字段指向当前目录项的父目录dentry实例;特别的,对于根目录项来说,这个字段指向自己;
d_subdirs:当前目录项如果代表目录,则该目录下的所有文件对应的dentry将形成d_subdirs链表(表头);
d_child:这个字段是父目录dentry中d_subdirs链表中的结点;
d_alias:一个文件可能有多个名称(硬链接),即多个dentry,则一个文件的所有目录项则形成一个链表,这个链表头位于该文件inode中的i_dentry字段,d_alias充当的该链表中的结点;
vfsmount结构
每个挂载在内核目录树中的文件系统都将对应一个vfsmount结构,下面将对该结构中的部分字段进行说明。假设设备/dev/sdc为ntfs文件系统,现需要将其挂载在文件系统为ext3的/home/edsionte/work下。因此,/home/edsionte/work可以被称为ntfs文件系统的挂载点,并且称ntfs文件系统与ext3文件系统形成父子文件文件系统关系。同时ntfs也可称为源文件系统,而ext3也可称为目的文件系统。
mnt_hash:内核将系统内所有已挂载的文件系统通过散列表的形式进行组织,每个vfsmount将处于其对应哈希值的冲突链表当中。mnt_hash字段则为具体冲突链表的元素。
mnt_mounts:如果当前文件系统下挂载了其他的子文件系统,那么这些子文件系统将通过自身vfsmount中的mnt_child字段组成一个链表,该链表头为父文件系统中的mnt_mounts字段。
mnt_child:当前文件系统将通过该字段与其他父文件系统下的子文件系统组成一个链表。
mnt_parent:该字段指向父文件系统对应的vfsmount结构。即指向ext3文件系统对应的vfsmount结构。
mnt_mountpoint:该字段表示源文件系统在目的文件系统中挂载点对应的dentry结构。/home/edsionte/work为挂载点,则该字段指向目录项work。
mnt_root:指向当前文件系统的根目录项。对于源文件系统ntfs来说,根目录项相对为/,但在整个系统目录树中,根目录项为work。
mnt_sb:每个文件系统都将对应一个super_block结构,该字段指向/dev/sdc设备上文件系统对应的超级块。
mnt_list:所有处于一个名字空间的文件系统通过mnt_list字段链接在一起,而该链表的表头为该名字空间结构中的list字段。
mnt_ns:该字段表示当前vfsmount所对应的名字空间结构。
nameidata结构
文件路径是由各级别的目录项组成的,因此路径的查找过程是对目录项的逐级查找。nameidata结构是路径查找过程中的核心数据结构,在每一级目录项查找过程中,它向查找函数输入参数,并且保存本次查找的结果,因此它是不断变化的。
下面对nameidata结构中的部分字段进行说明。
path:该字段用于保存当前目录项。该字段是path结构,该结构将目录项和该目录项所关联的vfsmount结构进行封装。
last:该字段为qstr结构,表示当前目录项的名称。
root:该字段为path结构,表示根目录。
last_type:表示当前目录项的类型。
inode:表示当前目录项对应的inode,它的取值来自于path.dentry.d_inode。
depth:表示符号链接当前的嵌套级别,最大不能超过MAX_NESTED_LINKS;
saved_names:该字符串数组表示符号链接每个嵌套级别的名称;
目录项的类型包括以下几种情况:
LAST_NORM:普通目录项;
LAST_ROOT:当前目录项为/;
LAST_DOT:当前目录项为.;
LAST_DOTDOT:当前目录项为..;
LAST_BIND:当前目录项为符号链接文件;
3.基本原理
rcu机制
写时拷贝(rcu,Read-Copy-Update)是Linux内核的一种锁机制,它是一种改良的rwlock(但并不能代替),适合读者多写者少的情景,可以保证读写者操作同时进行。
对于读者而言,rcu机制可以保证多个读者在不申请锁的情况下直接对临界区资源进行访问。对于写者而言,它之所以可以与读者同时访问共享资源,是因为在读者读取原始数据的同时它修改的是原始数据的备份。当所有读者都退出访问该共享资源时,写着将用修改后的新数据替换原始数据。同时,rcu中的回收机制将对原始数据进行回收。
与rwlock相比,在读多写少的情况下,rcu的效率会高很多。因为rcu所提供的拷贝技术使读写者可以同时访问共享资源,因此免去了读写者申请锁时所花费的开销。
由于rcu机制的自身特点,它所使用的上下文必须是不可睡眠的。因为,写者在替换原始数据之前会等待所有读者退出临界区,而此时如果读者处于阻塞状态,那么系统将进入死锁状态。
rcu-walk和ref-walk
内核中的路径查找提供两种模式:ref-walk和rcu-walk。前者是内核中传统的路径查找方式,而ref-walk是基于rcu所机制的一种路径查找模式。由于路径查找正好是一个读多写少的情景,基于rcu机制快速高效的特点,该模式可以高效的进行路径查找。不过,rcu-walk并不是万能的,如果路径查找过程中需要睡眠,那么必须将查找模式由rcu-walk切换到ref-walk。
4.总结
本篇对open()在内核实现中所涉及的数据结构和原理进行实现说明,并且针对open()实现过程的一些问题进行Q&A。可以在阅读open()内核源码之前阅读本文,也可在阅读之后再次阅读本文。
参考资料:
1.Linux源码3.2.69;
2.深入理解Linux内核:http://book.douban.com/subject/2287506/;
3.深入Linux内核架构:http://book.douban.com/subject/4843567/;
4.Linux内核探秘:http://book.douban.com/subject/25817503/;