Linux 内核源代码情景分析(三)(上):https://developer.aliyun.com/article/1597984
(7)ext2_inode
// include/linux/ext2_fs.h /* * Structure of an inode on the disk */ struct ext2_inode { __u16 i_mode; /* File mode */ __u16 i_uid; /* Low 16 bits of Owner Uid */ __u32 i_size; /* Size in bytes */ __u32 i_atime; /* Access time */ __u32 i_ctime; /* Creation time */ __u32 i_mtime; /* Modification time */ __u32 i_dtime; /* Deletion Time */ __u16 i_gid; /* Low 16 bits of Group Id */ __u16 i_links_count; /* Links count */ __u32 i_blocks; /* Blocks count */ __u32 i_flags; /* File flags */ union { struct { __u32 l_i_reserved1; } linux1; struct { __u32 h_i_translator; } hurd1; struct { __u32 m_i_reserved1; } masix1; } osd1; /* OS dependent 1 */ __u32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ __u32 i_generation; /* File version (for NFS) */ __u32 i_file_acl; /* File ACL */ __u32 i_dir_acl; /* Directory ACL */ __u32 i_faddr; /* Fragment address */ union { struct { __u8 l_i_frag; /* Fragment number */ __u8 l_i_fsize; /* Fragment size */ __u16 i_pad1; __u16 l_i_uid_high; /* these 2 fields */ __u16 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 */ __u16 h_i_mode_high; __u16 h_i_uid_high; __u16 h_i_gid_high; __u32 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; /* OS dependent 2 */ };
同样,读者以后还会不时地需要回过来看看这个结构的定义。这里我们暂不解释这个结构中的成分,而只是指出:除 Linux 外,FSF 还打算在它的其他两个操作系统中也采用 Ext2 文件系统,但是在具体使用上又略有不同,所以在这个结构中有两个 union,即 osd1 和 osd2,都要视实际运行的操作系统而作不同的解释。
虽然在 inode 结构(以及 ext2_inode 结构)中包含了关于文件的组织和管理的信息,但是还有一项关键性的信息,即文件名,却并不在其内。显然,我们需要一种机制,使得根据一个文件的文件名就可以在磁盘上找到该文件的索引节点,从而在内存中建立起代表该文件的 inode 结构。这种机制就是文件系统的目录树。这棵倒立的 “树” 从系统的 “根节点” ,即开始向下伸展,除最底层的 “叶” 节点为 “文件” 以外,其他的中间节点都是 “目录” 。其实,目录也是一种文件,是一种特殊的磁盘文件。这种文件的 “文件名” 就是目录名,也有索引节点,并且有数据部分。所不同的是,其数据部分的内容只包括 “目录项” 。对 Ext2 文件系统来说,这种 “目录项” 就是 ext2_dir_entry ,后来改成了ext2_dir_entry_2 数据结构(但保持兼容),它也是在 include/linux/ext2_fs.h 中定义的。
(8)ext2_dir_entry_2
// include/linux/ext2_fs.h /* * Structure of a directory entry */ #define EXT2_NAME_LEN 255 /* * The new version of the directory entry. Since EXT2 structures are * stored in intel byte order, and the name_len field could never be * bigger than 255 chars, it's safe to reclaim the extra byte for the * file_type field. */ struct ext2_dir_entry_2 { __u32 inode; /* Inode number */ __u16 rec_len; /* Directory entry length */ __u8 name_len; /* Name length */ __u8 file_type; char name[EXT2_NAME_LEN]; /* File name */ };
文件名(不包括路径部分)的最大长度为 255 个字符。老版本的 ext2_dir_entry 结构中 name_Ien 为无符号短整数,而在新版本的 ext2_dir_entry_2 中则改为 8位的无符号字符,腾出一半用作文件类型 file_type 。目前,已经定义的文件类型为:
// include/linux/ext2_fs.h /* * Ext2 directory file types. Only the low 3 bits are used. The * other bits are reserved for now. */ #define EXT2_FT_UNKNOWN 0 #define EXT2_FT_REG_FILE 1 #define EXT2_FT_DIR 2 #define EXT2_FT_CHRDEV 3 #define EXT2_FT_BLKDEV 4 #define EXT2_FT_FIFO 5 #define EXT2_FT_SOCK 6 #define EXT2_FT_SYMLINK 7 #define EXT2_FT_MAX 8
这里的 EXT2_FT_CHRDEV 和 EXT2_FT_BLKDEV 分别表示字符设备文件和块设备文件。我们在前面提到过的 “/proc” 下的特殊文件并不单独成为一类,而是作为常规文件,即 EXT2_FT_REG_FILE 出现在目录项中。至于最后怎样与真正的常规文件相区分,则读者在读完本章以后自会明白。
注意 ext2_dir_entry_2 结构中有个字段 rec_len,说明这个数据结构的长度并不是固定的。由于节点名的长度相差可以很大,固定按最大长度 255 分配空间会造成浪费,所以将这个数据结构的长度设计成可变的。当然,在这样的数据结构中其可变部分(这里是 name )必须放在最后。
磁盘上的 ext2_inode 数据结构在内存中的对应物为 inode 结构,但二者有很大不同;同样,目录项 ext2_dir_entry_2 在内存中的对应物是 dentry 结构,但是这二者也有很大不同。数据结构 dentry 是在 include/linux/dcache.h 中定义的:
(9)dentry
// include/linux/dcache.h #define DNAME_INLINE_LEN 16 struct dentry { atomic_t d_count; unsigned int d_flags; struct inode * d_inode; /* Where the name belongs to - NULL is negative */ struct dentry * d_parent; /* parent directory */ struct list_head d_vfsmnt; struct list_head d_hash; /* lookup hash list */ struct list_head d_lru; /* d_count = 0 LRU list */ struct list_head d_child; /* child of parent list */ struct list_head d_subdirs; /* our children */ struct list_head d_alias; /* inode alias list */ struct qstr d_name; unsigned long d_time; /* used by d_revalidate */ struct dentry_operations *d_op; struct super_block * d_sb; /* The root of the dentry tree */ unsigned long d_reftime; /* last time referenced */ void * d_fsdata; /* fs-specific data */ unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ };
很明显,dentry 结构中的大部分成分都是动态信息。就是静态部分如文件名也与磁盘上的 ext2_dir_entry_2 有很大的不同,相比之下几乎是面目全非。以后我们将结合代码解释其主要成分的用途。其实,dentry 与 ext2_dir_entry_2 之间以及 inode 与 ext2_inode 之间的这种显著不同并不奇怪,因为 dentry 和 inode 是属于 VFS 层的数据结构,需要适用于各种不同的文件系统;而 ext2_dir_entry_2 和 ext2_inode 则是专门针对 Ext2 文件系统而设计的,所以前者除包含了许多动态信息以外,还是对后者的一种抽象和扩充,并不只是后者的映象。
说到这里,读者可能会产生个疑问:要访问个文件就得先访问一个目录,才能根据文件名从目录中找到该文件的目录项,进而找到其 i 节点;可是目录本身也是文件,它本身的目录项又在另一个目录项中,这一来不是成了 “先有鸡还是先有蛋” 的问题,或者说递归了吗?这个圈子的出口在哪儿呢?我们不妨换一个方式来问这个问题,那就是:是否有这样一个目录,它本身的 “目录项” 不在其他目录中,而可以在一个固定的位置上或者通过一个固定的算法找到,并且从这个目录出发可以找到系统中的任何一个文件?答案是肯定的,这个目录就是系统的根目录 “/” ,或者说 “根设备” 上的根目录。每一个 “文件系统” ,即每一个格式化成某种文件系统的存储设备上都有一个根目录,同时又都有一个 “超级块” (super block),根目录的位置以及文件系统的其他一些参数就记录在超级块中。超级块在设备上的逻辑位置都是固定的,例如,在磁盘上总是在第二个逻辑块(第一个逻辑块为引导块), 所以不需要再从其他什么地方去 “查找” 。同时,对于一个特定的文件系统,超级块的格式也是固定的, 系统在初始化时要将一个存储设备(通常就是从中引导出操作系统的那个设备)作为整个系统的 “根设备” ,它的根目录就成为整个文件系统的 “总根” ,就是 “/” 。更确切地说,就是把根设备的根目录 “安装” 在文件系统的总根 “/” 节点上。有了根设备以后,还可以进而把其他存储设备也安装到文件系统中空闲的目录节点上。所谓 “安装” ,就是从一个存储设备上读入超级块,在内存中建立起一个 supe_block 结构。再进而将此设备上的根目录与文件系统中已经存在的一个空白目录挂上钩。系统初始化时整个文件系统只有一个空白目录 “/” ,所以根设备的根目录就安装在这个节点上。这样,从根目录 “/” 开始,根据给定的 “全路径名” 就可以找到文件系统中的任何一个文件,而不论这个文件是在哪一个存储设备上,只要文件所在的存储设备已经安装就行了。
但是,每次都要提供一个全路径名,并且每次都要从根目录开始查找,既不方便也是一种浪费。所以系统也提供了从 “当前目录” 开始查找的手段。每一个进程在每一时刻都有一个 “当前工作目录 “pwd” ,用户可以改变这个目录,但是永远都有这么个目录存在。这样,就可以只提供一个从 pwd 开始的 “相对路径名” 来查找一个文件。这就是前面看到过的 fs_struct 数据结构中为什么要有个指针 pwd 的原因。这个指针总是指向本进程的 “当前工作目录” 的 dentry 结构,而进程的 task_struct 结构中的指针 fs 则总是指向一个有效的 fs_struct 结构。每当一个进程通过 chdir() 系统调用进入一个目录, 或者在 login 进入用户的原始目录(“Home Directory”)时,内核就使该进程的 pwd 指针指向这个目录在内存中的 dentry 结构。相对路径名还可以用 “…/” 开头,表示先向上找到当前目录的父目录,再从那里开始查找。相应地,在 dentry 结构中也有个指针 d_parent,指向其父目录的 dentry 结构。
如前所述,fs_struct 结构中还有一个指针 root,指向本进程的根目录的 dentry 结构。前面讲过,表示整个文件系统的总根,可这只是就一般而言,或者是对早期的 Unix 系统而言。事实上, 特权用户可以通过一个系统调用 chroot() 将另一个目录设置成本进程的根目录。从此以后,这个进程以及由这个进程所 fork() 的子进程就把这个目录当成了文件系统的根,遇到文件的全路径名时就从这个目录而不是从真正的文件系统总根开始查找。例如,要是这个进程执行一个系统调用 chdir(“/”),就会转到这个 “现在” 的根目录而不是真正的根目录。这种特殊的设计也是从实践需求引起的,最初是为了克服 FTP,特别是匿名 FTP 的一个安全性问题。FTP 的服务进程(所谓 “守护神” daemon)是特权用户进程。当一个远程的用户与 FTP 服务进程建立起连接以后,就可以在远地发出诸如 “cd/"、 “get /etc/passwd” 之类的命令。显然,这给系统的安全性造成了一个潜在的缺口,现在有子进程自己的 “根目录” 以及系统调用 chroot() ,就可以让 FTP 服务进程把另一目录当成它的根目录,从而当远程用户要求 “get /etc/passwd” 时就会得到 “文件找不到” 之类的出错信息,从向保证了 passwd 口令文件的安全性。
而且,fs_struct 结构中还有一个指针 altroot ,指向本进程的 “替换根目录” 。当进程执行一个系统调用 chdir(“/”) 时,如果它有替换根目录,即指针 altroot 不为 0 ,就会转入其替换根目录,否则才转入其实在根目录。这样就可以视具体的情况而在两个 “根目录” 中切换,让用户在不同的情况下 “看到” 不同的根目录。
对于普通文件,文件系统层最终要通过磁盘或其他存储设备的驱动程序从存储介质上读或写。就 Ext2 文件系统而言,从磁盘文件的角度来看,对存储介质的访问可以涉及到四种不同的目标,那就是:
文件中的数据,包括目录的内容,即目录项 ext2_dir_entry_2 数据结构。
文件的组织与管理信息,即索引节点 ext2_inode 数据结构。
磁盘的超级块。如果物理的磁盘被划分成若干分区,那就包括每个 “逻辑磁盘” 的超级块。
引导块。
每个按 Ext2 格式经过格式化的磁盘(或逻辑盘)存储介质都相应地被划分成至少 4 个部分(图5.4)。 其中引导块永远是介质上的第一个记录块,超级块永远是介质上的第二个记录块,其他两部分的大小则取决于磁盘大小等参数,这些参数都存储在超级块中。
有的文件系统并没有索引节点这么一种数据结构,甚至没有这么一种概念。但是既然构成一个文件系统就必然存在着某种索引机制,从这种机制中就可以抽象出(或变换成)super_block 结构和 inode 结构中的公共信息。同时,super_block 结构也和 inode 结构一样包含着一个 union,对这一部分信息要根据具体的文件系统而加以不同的解释和使用。
从磁盘驱动程序的角度来看,则整个介质只是一个由若干记录块组成的一维阵列(记录块数组) 而已,所以这种设备称为 “块设备” 。当文件系统层要从磁盘上读出一个索引节点时,要根据索引节点号和超级块中提供的信息,计算出这个索引节点在磁盘上的哪一个记录块以及在此记录块中的相对位移。然后,通过磁盘驱动程序读入这个记录块后再根据索引节点在记录块中的相对位移找到这个节点。 如前所述,磁盘上的 “根目录” 是特殊的,其索引节点号保存在该磁盘的超级块中。从磁盘读一个特定文件的内容(数据)则要稍为麻烦一点。先要读入该文件的索引节点,然后根据索引节点中提供的信息将数据在文件中的位移换算成磁盘上的记录块号,再通过磁盘驱动程序从磁盘上读入。
相比之下,作为 “设备文件” 的磁盘则不存在(或看不见)这样的逻辑划分,而只是将磁盘看成一个巨大的线性存储空间(字节数组)。当从作为设备文件的磁盘读出时,只要将数据在此文件中的位移换算成磁盘上的记录块号,就可以通过磁盘驱动程序读入了。不过,在此之前也要先找到代表着这个设备文件的目录项和索引节点,才能把字符串形式的设备文件名转换成驱动程序所需要的设备号。
在前面我们曾把具体的文件系统比喻作 “接口卡” ,而把虚拟文件系统 VFS 比喻成一条插槽。因此,file 结构中的指针 f_op 就可以看作插槽中的一个触点,并且在 dentry、inode 等结构中都有着类似的触点。所以,如果把整个具体文件系统比喻成 “接口卡” 的话,那么这种接口下的 “插槽” 分成好几段,而 file 结构只是其中最主要的一段。有关的数据结构有:
文件操作跳转表,即 file_operations 数据结构:file 结构中的指针 f_op 指向具体的 file_operations 结构,这是 read() 、wirte() 等文件操作的跳转表。一种文件系统并不只限于一个file_operations 结构,如 Ext2 就有两个这样的数据结构,分别用于普通文件和目录文件。
目录项操作跳转表,即 dentry_operations 数据结构:dentry 结构中的指针 d_op 指向具体的 dentry_operations 数据结构,这是内核中 hash() 、compare() 等内部操作的跳转表。如果 d_op 为 0 则表示按 Linux 默认的(即 Ext2 )方式办。注意,这里说的是目录项,而不是目录,目录本身是一种特殊用途和具有特殊结构的文件。
索引节点操作跳转表,即 inode_operations 数据结构:inode 结构中的指针 i_op 指向具体的 inode_operations 数据结构,这是 mkdir() 、mknod() 等文件操作以及 lockup() 、permission() 等内部函数的跳转表。同样,一种文件系统也并不只限于一个 file_operations 结构。
超级块操作跳转表,即 super_operations 数据结构:super_block 结构中的指针 s_op 指向具体的 super_operations 数据结构,这是 read_inode() 、write_inode() 、delete_inode() 等内部操作的跳转表。
超级块本身也因文件系统而异。
由此可见,file 结构、dentry 结构、inode 结构、super_block 结构以及关于超级块位置的约定都属于 VFS 层。
此外,inode 结构中还有一个指针 i_fop,也指向具体的 file_operations 数据结构,实际上 file 结构中的指针 f_op 只是 inode 结构中这个指针的一个副本,在打开文件的时候从目标文件的 inode 结构中复制到 file 结构中。
最后还要指出,虽然每个文件都有目录项和索引节点在磁盘上,但是只有在需要时才在内存中为之建立起相应的 dentry 和 inode 数据结构。
2、从路径名到目标节点
本节先介绍几个函数的代码,主要是两个函数,即 path_init() 和 path_walk() 以及它们下面的一些低层函数。目的在于帮助读者加深对文件系统内部结构的理解,同时也为以后的代码阅读做些准备, 因为以这两个函数为入口的操作比较大,并且很重要,在本章后面几节中常常要用到。这两个函数通常都是连在一起调用的,二者合在一起就可以根据给定的文件路径名在内存中找到或建立代表着目标文件或目录的 dentry 结构和 inode 结构。在老一些的版本中,这一部分功能一直是通过一个叫 namei() (后来加了一个叫 lnamei() )的函数完成的,现在则有了新的实现。与 namei() 和 lnamei() 相对应,现在有一个函数 __user_walk() 将 path_init() 和 path_walk() “包装” 在一起。不过,内核代码中直接调用这两个函数的地方也有不少。本节涉及的代码基本上都在文件 fs/namei.c 中。
(1)__user_walk
先看 “外包装”,即 __user_walk() :
// fs/namei.c /* * namei() * * is used by most simple commands to get the inode of a specified name. * Open, link etc use their own routines, but this is enough for things * like 'chmod' etc. * * namei exists in two versions: namei/lnamei. The only difference is * that namei follows links, while lnamei does not. * SMP-safe */ // 其中调用参数name指向在用户空间中的路径名;flags的内容则是一些标志位,定义于文件 // include/linux/fs.h: // 最后一个参数 nd 是个结构指针,数据结构nameidata的定义也在 fs.h 中: int __user_walk(const char *name, unsigned flags, struct nameidata *nd) { char *tmp; int err; // 回到 __user_walk() ,先通过 getname() 在系统空间中分配一个页面,并从用户空间把文件名复制 // 到这个页面中。由于分配的是一个页面,所以整个路径名可以长达4K字节。同时,因为这块空间是动 // 态分配的,所以在使用完以后要通过 putname() 将其释放。代码中用到的 PTR_ERR 和 IS_ERR 都是 // inline 函数,均在 fs.h 中 tmp = getname(name); err = PTR_ERR(tmp); if (!IS_ERR(tmp)) { err = 0; if (path_init(tmp, flags, nd)) // 从 path_init() 成功返回时,nameidata 结构中的指针 dentry 指向路径搜索的起点,接着就是通过 // path_walk() 顺着路径名的指引进行搜索了。这个函数比较大,所以我们逐段地往下看(fs/namei.c) err = path_walk(tmp, nd); putname(tmp); } return err; }
⑴ LOOKUP flags
// include/linux/fs.h /* * The bitmask for a lookup event: * - follow links at the end * - require a directory * - ending slashes ok even for nonexistent files * - internal "there are more path compnents" flag */ #define LOOKUP_FOLLOW (1) #define LOOKUP_DIRECTORY (2) #define LOOKUP_CONTINUE (4) #define LOOKUP_POSITIVE (8) #define LOOKUP_PARENT (16) #define LOOKUP_NOALT (32)
这些标志位都是对怎样寻找目标的指示。例如,LOOKUP_DIRECTORY 表示要寻找的目标必须是个目录;而 LOOKUP_FOLLOW 表示如果找到的目标只是 “符号连接” 到其他文件或目录的一个目录项,则要顺着连接链一直找到终点。所谓 “连接” 是指一个 “节点” (目录项或文件)直接指向另一个节点,成为另一个节点的代表。注意, “符号连接” 与普通连接不同,普通的连接只能建立在同一个存储设备上,而 “符号连接” 可以是跨设备的;内核提供了两个不同的系统调用 link() 和 symlink() ,分别用于普通连接和 “符号连接” 的建立。由于 “符号连接” 可以是跨设备的,所以其终点有可能 “悬空” ,而普通连接的终点则必定是落实的。当路径中包含着 “符号连接” 时,对于是否继续顺着连接链往下搜索,则另有一些附加规定,对此,代码的作者在注释中加了说明(fs/namei.c)。
注释中谈到,如果在一个路径名内部的某个中间节点是符号连接,那就总是要跟随(follow);而在创建/删除/改名操作中如果路径名的最后一个节点是符号连接则不要跟随(读者不妨想想为什么?)。
至于其他一些标志位的用途,在阅读代码的过程中自会碰到。此处要提醒读者注意:并非所有标志位对所有文件系统都有意义。
⑵ nameidata
// include/linux/fs.h struct nameidata { struct dentry *dentry; struct vfsmount *mnt; struct qstr last; unsigned int flags; int last_type; };
这种数据结构是临时性的,只用来返回搜索的结果。成功返回时,其中的指针 dentry 指向所找到的 dentry 结构,而在该 dentry 结构中则有指针指向相应的 inode 结构。指针 mnt 则指向一个 vfsmount 数据结构,它记录着所属文件系统的安装信息,例如文件系统的安装点、文件系统的根节点等等。
⑶ PTR_ERR
// include/linux/fs.h /* * Kernel pointers have redundant information, so we can use a * scheme where we can return either an error code or a dentry * pointer with the same return value. * * This should be a per-architecture thing, to allow different * error and pointer decisions. */ static inline void *ERR_PTR(long error) { return (void *) error; } static inline long PTR_ERR(const void *ptr) { return (long) ptr; } static inline long IS_ERR(const void *ptr) { return (unsigned long)ptr > (unsigned long)-1000L; }
⑷ path_init
// fs/namei.c /* SMP-safe */ int path_init(const char *name, unsigned int flags, struct nameidata *nd) { nd->last_type = LAST_ROOT; /* if there are only slashes... */ nd->flags = flags; if (*name=='/') return walk_init_root(name,nd); read_lock(¤t->fs->lock); nd->mnt = mntget(current->fs->pwdmnt); nd->dentry = dget(current->fs->pwd); read_unlock(¤t->fs->lock); return 1; } // ====================================================================== // include/linux/fs.h /* * Type of the last component on LOOKUP_PARENT */ enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};
在搜索的过程中,这个字段(nd->last_type)的值会随路径名的当前搜索结果而变。例如:如果成功地找到了目标文件,那么这个字段的值就变成了 LAST_NORM;而如果最后停留在 “.” 上,则变成 LAST_DOT。
下面就取决于路径名是否以 “/” 开头了。
我们先看相对路径名,即不以 “/” 开头时的情况。以前讲过,进程的 task_struct 结构中有个指针 fs 指向一个 fs_struct 结构。在 fs_struct 结构中有个指针 pwd 指向进程的 “当前工作目录” 的 dentry 结构。相对路径是从当前工作目录开始的,所以将 nameidata 结构中的指针 dentry 也设置成指向这个当前工作目录的 dentry 结构,表示在虚拟的绝对路径中这个节点以及所有在此之前的节点都已经解决了。 同时,这个具体的 dentry 结构现在多了 一个 “用户” ,所以要调用 dget() 递增其共享计数。除此以外, fs_sturct 结构中还有个指针 pwdmnt 指向一个 vfsmount 结构。每当将一个存储设备(或称"文件系统") 安装到现有文件系统中的某个节点(空白目录)时,内核就要为之建立起一个 vfsmount 结构,这个结构中既包含着有关该设备(或者说 “子系统” )的信息,也包含了有关安装点的信息。系统中的每个文件系统,包括根设备上的文件系统,都要经过安装,所以 fs_sturct 结构中的指针 pwdmnt 总是指向一个 vfsmount 结构。详情可参阅后面 “文件系统的安装与拆卸” 一节。相应地,在 nameidata 结构中也有个指针 mnt ,要把它设置成指向同一个 vfsmount 结构。这样,对路径搜索的准备工作,即对 nameidata 结构的初始化就完成了。
可是,如果路径名是以 “/” 开头的绝对路径,那就要通过 walk_init_root() 从根节点开始查找 (fs/namei.c)。
⑸ walk_init_root
// fs/namei.c /* SMP-safe */ static inline int walk_init_root(const char *name, struct nameidata *nd) { read_lock(¤t->fs->lock); 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)) return 0; read_lock(¤t->fs->lock); } nd->mnt = mntget(current->fs->rootmnt); nd->dentry = dget(current->fs->root); read_unlock(¤t->fs->lock); return 1; }
如果当前进程并未通过 chroot() 系统调用设置自己的 “替换” 根目录,则代码中 if 语句里的 current->fs->altroot 为 0;所以把 nameidata 中的两个指针分别设置成指向当前进程的根目录的 dentry 结构及其所在设备的 vfsmount 结构。反之,如果已经设置了 “替换” 根目录,那就要看当初调用 path_init() 时参数 flags 中的标志位 LOOKUP_NOALT 是否为 1 了。通常这个标志位为 0,所以如果已经设置了 “替换” 根目录就会通过 __emul_lookup_dentry() 将 nameidata 结构中的指针设置成指向 “替换” 根目录。
这 “替换” 根目录到底是怎么回事呢?原来,在有些 Unix 变种(如Solaris等)中,可以在文件系统中(通常是在 “/usr” 下面)创建一棵子树,例如 “/usr/altroot/home/user1/…” 。然后,当用户调用 chroot() 设置其自己的根目录时,系统会自动将该进程的 fs_struct 结构中的 altroot 和 altrootmnt 两个指针设置成给定路径名在前述子树中的对应节点,那个对应节点就成了 “替换” 根目录。不过在 i386 处理器上的 linux 目前并不支持这种功能,所以这里 if 语句中的 current->fs->altroot 总是 NULL,因而不起作用。
⑹ path_walk
// fs/namei.c /* * Name resolution. * * This is the basic name resolution function, turning a pathname * into the final dentry. * * We expect 'base' to be positive and a directory. */ int path_walk(const char * name, struct nameidata *nd) { struct dentry *dentry; struct inode *inode; int err; unsigned int lookup_flags = nd->flags; // 如果路径名是以 '/' 开头的,就把它跳过去,因为在这种情况下nameidata结构中的指针dentry // 已经指向本进程的根目录了。注意,多个连续的 '/' 与一个 '/' 字符是等价的。如果路径名中仅仅含 // 有字符的话,那么其目标就是根目标,所以任务已经完成,可以返回了。不然,就继续搜索。 while (*name=='/') name++; if (!*name) goto return_base; inode = nd->dentry->d_inode; // 进程的task_struct结构中有个计数器link_count。在搜索过程中有可能碰到一个节点(目录项)只 // 是指向另一个节点的连接,此时就用这个计数器来对链的长度进行计数,这样,当链的长度达到某一 // 个值时就可以终止搜索而失败返回,以防陷入循环。另一方面,当顺着 “符号连接” 进入另一个设备 // 上的文件系统时,有可能会递归地调用 path_walk() 。所以,进入path_walk() 后,如果发现这个计数值 // 非 0,那就表示正在顺着 “符号连接” 递归调用 path_walk() 往前搜索的过程中,此时不管怎样都把 // LOOKUP_FOLLOW 标志位设成 1。这里还要指出,作为 path_walk() 起点的节点必定是一个目录,一 // 定有相应的索引节点存在,所以指针 inode 一定是有效的,而不可能是空指针。 if (current->link_count) lookup_flags = LOOKUP_FOLLOW; /* At this point we know we have a real path component. */ // 接下去是一个对路径中的节点所作的for循环,由于循环体较大,我们也只好分段来看。 for(;;) { unsigned long hash; // 循环体中的局部量this是个qstr数据结构,用来存放路径名中当前节点的杂凑值以及节点名的长 // 度,这个数据结构的定义在include/linux/dcache.h中: struct qstr this; unsigned int c; // 首先检查当前进程对当前节点的访问权限。函数 permission() 的代码与作用请参阅 “访问权限与文 // 件安全性” 一节。这里所检查的是对路径中各层目录(而不是目标文件)的访问权限。注意,对于中 // 间节点所需的权限为 “执行” 权,即 MAY_EXEC。如果权限不符,则 permission 返回一个出错代码, // 从而通过break语句结束循环,搜索就失败了。 err = permission(inode, MAY_EXEC); dentry = ERR_PTR(err); if (err) break; this.name = name; c = *(const unsigned char *)name; hash = init_name_hash(); // 回到代码中的第453一457行,这几行的作用就是逐个字符地计算出当前节点名的杂凑值,至于具 // 体的杂凑函数,我们就不关心了。 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? */ //(1)是 “\0” ,就是说当前节点已经是路径名中的最后一节,所以转入last_component。 if (!c) goto last_component; while (*++name == '/'); //(2)是个 '/' 字符,这里又有两种可能,第一种情况是当前节点实际上已是路径名中的最后一个 // 节点,只不过在此后面又多添了若干个字符。这种情况常常发生在用户界面上,特别是 // 在shell的命令行中,例如 “ls /usr/inclue/” ,这是允许的。但是当然最后的节点必须是个目 // 录,所以此时转到last_with_slashes。第二种情况就是当前节点为中间节点(包括起始节点), // 所以 '/' 字符(或者接连若干个 '/' 字符)后面还有其他字符。这种情况下就将其跳过, // 继续往下执行。 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. */ // 现在,要回过头来看当前节点了。记住,这个节点一定是中间节点或起始节点(否则就转到 // last_component 去了),这种节点一定是个目录。对于代表着文件的节点名来说,以 '.' 开头表示 // 这是一个隐藏的文件,而对于代表着目录的节点名则只有在两种情况下才是允许的。一种是节点名为 // '.' 表示当前目录,即不改变目录。另一种就是 ".." ,表示当前目录的父目录。 // // 就是说,如果当前节点名的第一个字符是则节点名的长度只能是 1 或者 2,并且当长度为 2 // 时第二个字符也必须是否则搜索就失败了(见475行和478行的break语句)。 // 如果当前节点名真的是那就要往上跑到当前已经到达的节点 nd->dentry 的父目录去。这是 // 由 follow_dotdot() 完成的 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 */ // 回到path_walk() 的代码中,注意 “case 2"的末尾没有break语句,所以会落入 // “case 1” 中通过 continue 语句回到for(;;)循环的开头,继续处理路径中的下一个节点名。 // 当然,多数情况下节点名都不是以开头的,就是说多数情况下总是顺着路径名逐层往下跑, // 而不是往上跑的。我们继续往下看对 “正常” 节点名的流程: case 1: continue; } /* * See if the low-level filesystem might want * to use its own hash.. */ // 有些文件系统通过dentry_operations结构中的指针d_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.. */ // 对当前节点的搜索是通过 cached_lookup() 和 real_lookup() 两个函数进行的。先通过 // cached_lookup() 在内存中寻找该节点业已建立的 dentry 结构。内核中有个杂凑去 // dentry_hashtable,是一个 list_head 指针数组,一旦在内存中建立起一个目录节点的 dentry 结构, // 就根据其节点名的杂凑值挂入杂凑表中的某个队列,需要寻找时则还是根据杂凑值从杂凑表着手。 // 当路径名中的某个节点变成 path_walk() 的当前节点时,位于其 “上游” 的所有节点必定都已经有 // dentry结构在内存中,而当前节点本身及其 “下游” 的节点则不一定。如果在内存中找不到当前节点 // 的dentry结构,那就要进一步通过 real_lookup() 到磁盘上通过其所在的目录寻找,找到后在内存 // 中为其建立起 dentry 结构并将之挂入杂凑表中的某个队列。 // // 内核中还有一个队列 dentry_unused,凡是已经没有用户,即共享计数为 0 的 dentry 结构就通过结 // 构中的另一个 list_head 挂入这个队列。这个队列是一个 LRU 队列,当需要回收已经不在使用中的 // dentry 结构的空间时,就从这个队列中找到已经空闲最久的 dentry 结构,再把这个结构从杂凑表 // 队列中脱链而加以释放。所以,dentry_unused是为缓冲存储而设置的辅助性的队列。不过,在一些 // 特殊的情况下, 可能会把一个还在使用中的 dentry 结构从杂凑表中脱链,迫使以后要访问这个节点 // 的进程重新根据磁盘上的内容另行构筑一个 dentry 结构,而已经脱链的那个数据结构则由最后 // 调用 dput() 使其共享计数变成 0 的进程负责将其释放。 // // 事实上,dentry 结构中有 6 个 list_head,即 d_vfsmnt、d_hash、d_lru、d_child、d_subdirs 和 // d_alias。 注意 list_head 既可以用来作为一个队列的头部,也可以用来将其所在的数据结构挂入到某个 // 队列中。 其中 d_vfsmnt 仅在该 dentry 结构为一安装点时才使用。一个dentry结构一经建立就通过其 // d_hash 挂入杂凑表 dentry_hashtable 中的某个队列里,当共享计数变成 0 时则通过 d_lru 挂入 // LRU 队列 dentry_unused 中。同时,dentry 结构通过d_child挂入在其父节点(上一层目录)的 // d_subdirs 队列中,同时又通过指针 d_parent 指向其父目录的dentry结构。而它自己各个子目录的 // dentry结构则在它本身的d_subdirs队列中。 // // 一个有效的detnry结构必定有一个相应的inode结构,这是因为一个目录项要么就代表着一个文 // 件,要么就代表着一个目录,而目录实际上也是文件。所以,只要是有效的dentry结构,则其指针 d_inode // 必定指向一个inode结构。可是,反过来一个inode却可能对应着不止一个dentry结构,也就是说,一个文件可 // 以有不止一个文件名(或路径名)。这是因为一个已经建立的文件可以被连接(link)到其他 // 文件名。所以,在inode结构中有个队列 i_dentry,凡是代表着这个文件的所有目录项都通过其dentry // 结构中的 d_alias 挂入相应inode结构中的i_dentry队列。此外,dentry结构中还有指针d_sb,指向其 // 所在设备的超级块的super_block数据结构,以及指针d_op,指向特定文件系统(指文件格式)的 // dentry_operations 结构。也许可以说,dentry结构是文件系统的核心数据结构,也是文件访问和为文件 // 访问而做的文件路径搜索操作的枢纽。 // // 下面是一个简要的总结: // 一 每个dentry结构都通过队列头d_hash链入杂凑表dentry_hashtable中的某个队列里。 // 一 共享计数为0的dentry结构都通过队列头d_lru链入LRU队列dentry_unused,在队列中等待 // 释放或者 “东山再起” 。 // 一 每个dentry结构都通过指针d_inode指向一个inode数据结构。但是多个dentry结构可以指向 // 同一个inode数据结构。 // 一 指向同一个inode数据结构的dentry结构都通过队列头d_alias链接在一起,都在该inode结 // 构的Ldentry队列中。 // 一 每个dentry结构都通过指针d_parent指向其父目录节点的dentry结构,并通过队列头d_child // 跟同一目录中的其他节点的dentry结构链接在一起,都在父目录节点的d_subdirs队列中。 // 一 每个dentry结构都通过指针d_sb指向一个super_block数据结构。 // 一 每个dentry结构都通过指针d_op指向一个dentry_operations数据结构。 // 一 每个dentry结构都有个队列头d_vfsmnt,用于文件系统的安装,详见 “文件系统的安装和拆卸” 。 // // 接下去我们看 cached_lookup() 的代码(namei.c) dentry = cached_lookup(nd->dentry, &this, LOOKUP_CONTINUE); // 如果所需的dentry结构不在杂凑表队列中或者已经无效,则返回NULL。那样,就要进一步通过 // real_lookup() 从父目录在磁盘上的内容中找到本节点的目录项,再根据其内容在内存中为之建立起一个 // dentry 结构(见 path_walk() 的 497 行)。下面就是 real_lookup() 的代码(见 namei.c): if (!dentry) { dentry = real_lookup(nd->dentry, &this, LOOKUP_CONTINUE); err = PTR_ERR(dentry); if (IS_ERR(dentry)) break; } /* Check mountpoints.. */ while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry)) ; err = -ENOENT; inode = 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(dentry, nd); dput(dentry); if (err) goto return_err; err = -ENOENT; inode = nd->dentry->d_inode; if (!inode) break; err = -ENOTDIR; if (!inode->i_op) break; } else { dput(nd->dentry); nd->dentry = dentry; } 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: 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_base; } 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; } dentry = cached_lookup(nd->dentry, &this, 0); if (!dentry) { dentry = real_lookup(nd->dentry, &this, 0); err = PTR_ERR(dentry); if (IS_ERR(dentry)) break; } while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry)) ; inode = dentry->d_inode; if ((lookup_flags & LOOKUP_FOLLOW) && inode && inode->i_op && inode->i_op->follow_link) { err = do_follow_link(dentry, nd); dput(dentry); if (err) goto return_err; inode = nd->dentry->d_inode; } else { dput(nd->dentry); nd->dentry = dentry; } err = -ENOENT; if (!inode) goto no_inode; if (lookup_flags & LOOKUP_DIRECTORY) { err = -ENOTDIR; if (!inode->i_op || !inode->i_op->lookup) break; } goto return_base; no_inode: err = -ENOENT; if (lookup_flags & (LOOKUP_POSITIVE|LOOKUP_DIRECTORY)) 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; return_base: return 0; out_dput: dput(dentry); break; } path_release(nd); return_err: return err; }
Ⓐ follow_dotdot
// fs/namei.c static inline void follow_dotdot(struct nameidata *nd) { while(1) { struct vfsmount *parent; struct dentry *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) { dentry = dget(nd->dentry->d_parent); spin_unlock(&dcache_lock); dput(nd->dentry); nd->dentry = dentry; break; } parent=nd->mnt->mnt_parent; if (parent == nd->mnt) { spin_unlock(&dcache_lock); break; } mntget(parent); dentry=dget(nd->mnt->mnt_mountpoint); spin_unlock(&dcache_lock); dput(nd->dentry); nd->dentry = dentry; mntput(nd->mnt); nd->mnt = parent; } }
但是这里又要分三种情况:
第一种情况,已到达节点 nd->dentry 就是本进程的根节点,这时不能再往上跑了,所以保持 nd->dentry 不变。
第二种情况,己到达节点 nd->dentry 与其父节点在同一个设备上。在这种情况下,既然己经到达的这个节点的 dentry 结构已经建立,则其父节点的 dentry 结构也必然已经建立在内存中,而且 dentry 结构中的指针 d_parent 就指向其父节点,所以往上跑一层是很简单的事情。
最后一种情况,已到达节点 nd->dentry 就是其所在设备上的根节点,往上跑一层就要跑到另一个设备上去了。如前所述,当将一个存储设备 “安装” 到另一个设备上的某个节点时,内核会分配和设置一个 vfsmount 结构,通过这个结构将两个设备以及两个节点联结起来(详见 “文件系统的安装与拆卸”)。所以,每个已经安装的存储设备(包括根设备)都有一个 vfsmount 结构,结构中有个指针 mnt_parent 指向其 “父设备” ,但是根设备的这个指针则指向其自己,因为它再没有 “父设备” 了,而另一个指针 mnt_mountpoint 则指向代表着安装点(一定是个目录)的 dentry 结构。从文件系统的角度来看,安装点与所安装设备的根目录是等价的。我们已经在当前设备的根目录中,所以从这里往上跑一层就是要跑到安装点的上一层目录中(而不是安装点本身)。
先检查当前的 vfsmount 结构是否代表着根设备,如果是的话,立即就通过399行的 break 语句结 束 while(1) 循环。这样,nameidata 结构中的 dentry 和 mnt 两个指针就维持不变。这种情况相当于在根目录中打入命令 “cd …” 或者 “cd usr/./…/…” 等等,读者不妨实验一下,看看结果如何。
反之,要是当前设备不是根设备,那就把 nameidata 结构中的两个指针分别设置成指向上层设备的 vfsmount 结构以及该设备上的安装点的上一层目录(dentry 结构),然后回到 while(1) 循环的开始处。 一般来说,安装点不会是一个设备上的根目录,所以这一次循环会将 nameidata 结构中的指针 dentry 指向安装点的父目录。可是,万一安装点真的就是上一层设备上的根目录(当然,必定是空的)呢?那也不要紧,只不过是再循环一次,再往上跑一层而已。
Linux 内核源代码情景分析(三)(下):https://developer.aliyun.com/article/1597995