linux源码解读(五):文件系统——文件和目录的操作
对于普通用户,平时使用操作系统是肯定涉及到创建、更改、删除文件(比如mkdir、rmdir、rm、chmod、ln等);有些文件是高权限用户建的,低权限用户甚至都打不开,也删不掉;为了方便管理不同业务类型的文件,还需要在不同的逻辑分区建文件夹,分门别类各种文件;linux下用ls -l命令还可以查看文件的详细属性,这一系列的功能构师怎么实现的了?功能都在fs/namei.c文件中
1、(1)权限检查,核心就是依靠inode结构体中的i_mode成员变量了!这个变量是unsigned short类型,一共2byte=16bit长;linux用低9位表示当前用户权限、用户组权限、其他用户权限,用户平时用ls -l查到的权限就是靠这个字段得到的!举个例子:rwx------表示当前用户有读写执行权限,用户组没有任何权限,其他用户也没有任何权限,所有权限表示刚好使用9bit;
- i_mode节点右移3位,与上0007后得到用户组权限
- i_mode节点右移6位,与上0007后得到当前用户权限
- chmod改的就是i_mode这个字段
/* * permission() * * is used to check for read/write/execute permissions on a file. * I don't know if we should look at just the euid or both euid and * uid, but that should be easily changed. */ //// 检测文件访问权限 // 参数:inode - 文件的i节点指针;mask - 访问属性屏蔽码。 // 返回:访问许可返回1,否则返回0. static int permission(struct m_inode * inode,int mask) { int mode = inode->i_mode; /* special case: not even root can read/write a deleted file */ // 如果i节点有对应的设备,但该i节点的连接计数值等于0,表示该文件 // 已被删除,则返回。否则,如果进程的有效用户ID(euid)与i节点的 // 用户id相同,则取文件宿主的访问权限。否则如果与组id相同, // 则取组用户的访问权限。 if (inode->i_dev && !inode->i_nlinks) return 0; else if (current->euid==inode->i_uid) mode >>= 6; else if (current->egid==inode->i_gid) mode >>= 3; /* &0007:取最后3位 &mask:取传入参数的位 */ if (((mode & mask & 0007) == mask) || suser())/*要么是管理员,是超级用户*/ return 1; return 0; }
(2)因为是涉及到设备名、文件名、目录路径的比对,自然少不了字符串相关的操作。平时在3环做应用开发,码农都习惯于使用操作系统提供的库函数,比如strcmp、strcat等,但是现在还在内核,哪来的库函数直接调用了,只能自己动手重新写字符串的比较函数,如下:
* * ok, we cannot use strncmp, as the name is not in our data space. * Thus we'll have to use match. No big problem. Match also makes * some sanity tests. * * NOTE! unlike strncmp, match returns 1 for success, 0 for failure. */ //// 指定长度字符串比较函数 // 参数:len - 比较的字符串长度;name - 文件名指针;de - 目录项结构 // 返回:相同返回1,不同返回0. // 下面函数中的寄存器变了same被保存在eax寄存器中,以便高效访问。 static int match(int len,const char * name,struct dir_entry * de) { register int same ; // 首先判断函数参数的有效性。如果目录项指针空,或者目录项i节点等于0,或者 // 要比较的字符串长度超过文件名长度,则返回0.如果要比较的长度len小于NAME_LEN, // 但是目录项中文件名长度超过len,也返回0. if (!de || !de->inode || len > NAME_LEN) return 0; if (len < NAME_LEN && de->name[len]) return 0; // 然后使用嵌套汇编语句进行快速比较操作。他会在用户数据空间(fs段)执行字符串的比较 // 操作。%0 - eax(比较结果same);%1 - eax (eax初值0);%2 - esi(名字指针); // %3 - edi(目录项名指针);%4 - ecx(比较的字节长度值len). __asm__("cld\n\t" "fs ; repe ; cmpsb\n\t" "setz %%al" :"=a" (same) :"0" (0),"S" ((long) name),"D" ((long) de->name),"c" (len) ); return same; }
(3)还有在某个目录下查找名为xxx的文件,比如:"find /home -name test"命令,就是在home目录下查找名为test的文件,实现如下:
注意:函数的参数有两个双重指针,第二个双重指针明显是用来保存返回值的!
/* * find_entry() * * finds an entry in the specified directory with the wanted name. It * returns the cache buffer in which the entry was found, and the entry * itself (as a parameter - res_dir). It does NOT read the inode of the * entry - you'll have to do that yourself if you want to. * * This also takes care of the few special cases due to '..'-traversal * over a pseudo-root and a mount point. */ //// 查找指定目录和文件名的目录项。 find -name "xxx" /xxx/xxx // 参数:*dir - 指定目录i节点的指针;name - 文件名;namelen - 文件名长度; // 该函数在指定目录的数据(文件)中搜索指定文件名的目录项。并对指定文件名 // 是'..'的情况根据当前进行的相关设置进行特殊处理。关于函数参数传递指针的指针 // 作用,请参见seched.c中的注释。 // 返回:成功则函数高速缓冲区指针,并在*res_dir处返回的目录项结构指针。失败则 // 返回空指针NULL。 static struct buffer_head * find_entry(struct m_inode ** dir, const char * name, int namelen, struct dir_entry ** res_dir) { int entries; int block,i; struct buffer_head * bh; struct dir_entry * de; struct super_block * sb; // 同样,本函数一上来也需要对函数参数的有效性进行判断和验证。如果我们在前面 // 定义了符号常数NO_TRUNCATE,那么如果文件名长度超过最大长度NAME_LEN,则不予 // 处理。如果没有定义过NO_TRUNCATE,那么在文件名长度超过最大长度NAME_LEN时截短之。 #ifdef NO_TRUNCATE if (namelen > NAME_LEN) return NULL; #else if (namelen > NAME_LEN) namelen = NAME_LEN; #endif // 首先计算本目录中目录项项数entries(也即是当前目录中能存放的最大目录个数)。目录i节点i_size字段中含有本目录包含的数据 // 长度,因此其除以一个目录项的长度(16字节)即课得到该目录中目录项数。然后置空 // 返回目录项结构指针。如果长度等于0,则返回NULL,退出。 entries = (*dir)->i_size / (sizeof (struct dir_entry)); *res_dir = NULL; if (!namelen) return NULL; // 接下来我们对目录项文件名是'..'的情况进行特殊处理。如果当前进程指定的根i节点就是 // 函数参数指定的目录,则说明对于本进程来说,这个目录就是它伪根目录,即进程只能访问 // 该目录中的项而不能后退到其父目录中去。也即对于该进程本目录就如同是文件系统的根目录, // 因此我们需要将文件名修改为‘.’。 // 否则,如果该目录的i节点号等于ROOT_INO(1号)的话,说明确实是文件系统的根i节点。 // 则取文件系统的超级块。如果被安装到的i节点存在,则先放回原i节点,然后对被安装到 // 的i节点进行处理。于是我们让*dir指向该被安装到的i节点;并且该i节点的引用数加1. // 即针对这种情况,我们悄悄的进行了“偷梁换柱”工程。:-) /* check for '..', as we might have to do some "magic" for it */ if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') { /* '..' in a pseudo-root results in a faked '.' (just change namelen) */ if ((*dir) == current->root) namelen=1; else if ((*dir)->i_num == ROOT_INO) { /* '..' over a mount-point results in 'dir' being exchanged for the mounted directory-inode. NOTE! We set mounted, so that we can iput the new dir */ sb=get_super((*dir)->i_dev); if (sb->s_imount) { iput(*dir); (*dir)=sb->s_imount; (*dir)->i_count++; } } } // 现在我们开始正常操作,查找指定文件名的目录项在什么地方。因此我们需要读取目录的 // 数据,即取出目录i节点对应块设备数据区中的数据块(逻辑块)信息。这些逻辑块的块号 // 保存在i节点结构的i_zone[9]数组中。我们先取其中第一个块号。如果目录i节点指向的 // 第一个直接磁盘块好为0,则说明该目录竟然不含数据,这不正常。于是返回NULL退出, // 否则我们就从节点所在设备读取指定的目录项数据块。当然,如果不成功,则也返回NULL 退出。 if (!(block = (*dir)->i_zone[0])) return NULL; if (!(bh = bread((*dir)->i_dev,block))) return NULL; // 此时我们就在这个读取的目录i节点数据块中搜索匹配指定文件名的目录项。首先让de指向 // 缓冲块中的数据块部分。并在不超过目录中目录项数的条件下,循环执行搜索。其中i是目录中 // 的目录项索引号。在循环开始时初始化为0. i = 0; de = (struct dir_entry *) bh->b_data; while (i < entries) { // 如果当前目录项数据块已经搜索完,还没有找到匹配的目录项,则释放当前目录项数据块。 // 再读入目录的下一个逻辑块。若这块为空。则只要还没有搜索完目录中的所有目录项,就 // 跳过该块,继续读目录的下一逻辑块。若该块不空,就让de指向该数据块,然后在其中继续 // 搜索。其中DIR_ENTRIES_PER_BLOCK可得到当前搜索的目录项所在目录文件中的块号,而bmap() // 函数则课计算出在设备上对应的逻辑块号. if ((char *)de >= BLOCK_SIZE+bh->b_data) { brelse(bh); bh = NULL; if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) || !(bh = bread((*dir)->i_dev,block))) { i += DIR_ENTRIES_PER_BLOCK; continue; } de = (struct dir_entry *) bh->b_data; } // 如果找到匹配的目录项的话,则返回该目录项结构指针de和该目录项i节点指针*dir以及该目录项 // 数据块指针bh,并退出函数。否则继续在目录项数据块中比较下一个目录项。 if (match(namelen,name,de)) { *res_dir = de; return bh; } de++; i++; } // 如果指定目录中的所有目录项都搜索完后,还没有找到相应的目录项,则释放目录的数据块, // 最后返回NULL(失败)。 brelse(bh); return NULL; }
这个函数的开头就出现了一个新的结构体dir_entry,是这样定义的:结构体很简单,只有2个字段,分别是当前文件或目录中包含的inode个数,以及自己的名字;最后一个参数也是用这个结构体保存找到的文件名称和inode节点号数,通过inode节点号数从inode位图查看该inode是否被使用,也可以查找到该文件的inode节点在磁盘的block位置,进而找到文件元信息;
struct dir_entry { unsigned short inode; char name[NAME_LEN]; };
(4)既然能够查找文件,也就能新建文件或目录,linux的实现方式如下:
/* * add_entry() * * adds a file entry to the specified directory, using the same * semantics as find_entry(). It returns NULL if it failed. * * NOTE!! The inode part of 'de' is left at 0 - which means you * may not sleep between calling this and putting something into * the entry, as someone else might have used it while you slept. */ //// 根据指定的目录和文件名添加目录项 // 参数:dir - 指定目录的i节点;name - 文件名;namelen - 文件名长度; // 返回:高速缓冲区指针;res_dir - 返回的目录项结构指针。 static struct buffer_head * add_entry(struct m_inode * dir, const char * name, int namelen, struct dir_entry ** res_dir) { int block,i; struct buffer_head * bh; struct dir_entry * de; // 同样,本函数一上来也需要对函数参数的有效性进行判断和验证。 // 如果我们在前面定义了符号常数NO_TRUNCATE,那么如果文件名长 // 度超过最大长度NAME_LEN,则不予处理。如果没有定义过NO_TRUNCATE, // 那么在文件名长度超过最大长度NAME_LEN时截短之。 *res_dir = NULL; #ifdef NO_TRUNCATE if (namelen > NAME_LEN) return NULL; #else if (namelen > NAME_LEN) namelen = NAME_LEN; #endif // 现在我们开始操作,向指定目录中添加一个指定文件名的目录项。因此 // 我们需要先读取目录的数据,即取出目录i节点对应块数据区中的数据块 // 信息。这些逻辑块的块号保存在i节点结构i_zone[9]数组中。我们先取 // 其中第1个块号,如果目录i节点指向的第一个直接磁盘块号为0,则说明 // 该目录竟然不含数据,这不正常。于是返回NULL退出。否则我们就从节点 // 所在设备读取指定目录项数据块。当然,如果不成功,则也返回NULL退出。 // 如果参数提供的文件名长度等于0,则也返回NULL退出。 if (!namelen) return NULL; if (!(block = dir->i_zone[0]))/*目录文件存储的第一个磁盘逻辑块号,肯定不会是0(0是引导块)*/ return NULL; //目录数据必须存磁盘,不能只存内存,否则关机后就全丢了 if (!(bh = bread(dir->i_dev,block)))/*读取目录文件第一个逻辑块的数据到缓存区,里面存放的都是dir_entry,所以下面把b_data强转成dir_entry*/ return NULL; // 此时我们就在这个目录i节点数据块中循环查找最后未使用的空目录项。 // 首先让目录项结构指针de指向缓冲块中的数据块部分,即第一个目录项处。 // 其中i是目录中的目录项索引号,在循环开始时初始化为0. i = 0; de = (struct dir_entry *) bh->b_data; while (1) { // 如果当前目录项数据块已经搜索完毕,但还没有找到需要的空目录项, // 则释放当前目录项数据块,再读入目录的下一个逻辑块。如果对应的逻辑块。 // 如果对应的逻辑块不存在就创建一块。如果读取或创建操作失败则返回空。 // 如果此次读取的磁盘逻辑块数据返回的缓冲块数据为空,说明这块逻辑块 // 可能是因为不存在而新创建的空块,则把目录项索引值加上一块逻辑块所 // 能容纳的目录项数DIR_ENTRIES_PER_BLOCK,用以跳过该块并继续搜索。 // 否则说明新读入的块上有目录项数据,于是让目录项结构指针de指向该块 // 的缓冲块数据部分,然后在其中继续搜索。其中i/DIR_ENTRIES_PER_BLOCK可 // 计算得到当前搜索的目录项i所在目录文件中的块号,而create_block函数则可 // 读取或创建出在设备上对应的逻辑块。 if ((char *)de >= BLOCK_SIZE+bh->b_data) { brelse(bh); bh = NULL; block = create_block(dir,i/DIR_ENTRIES_PER_BLOCK); if (!block) return NULL; if (!(bh = bread(dir->i_dev,block))) { i += DIR_ENTRIES_PER_BLOCK; continue; } de = (struct dir_entry *) bh->b_data; } // 如果当前所操作的目录项序号i乘上目录结构大小所在长度值已经超过了该目录 // i节点信息所指出的目录数据长度值i_size,则说明整个目录文件数据中没有 // 由于删除文件留下的空目录项,因此我们只能把需要添加的新目录项附加到 // 目录文件数据的末端处。于是对该处目录项进行设置(置该目录项的i节点指针 // 为空),并更新该目录文件的长度值(加上一个目录项的长度),然后设置目录 // 的i节点已修改标志,再更新该目录的改变时间为当前时间。 if (i*sizeof(struct dir_entry) >= dir->i_size) { de->inode=0; dir->i_size = (i+1)*sizeof(struct dir_entry); dir->i_dirt = 1; dir->i_ctime = CURRENT_TIME; } // 若当前搜索的目录项de的i节点为空,则表示找到一个还未使用的空闲目录项 // 或是添加的新目录项。于是更新目录的修改时间为当前时间,并从用户数据区 // 复制文件名到该目录项的文件名字段,置含有本目录项的相应高速缓冲块已修改 // 标志。返回该目录项的指针以及该高速缓冲块的指针,退出。 if (!de->inode) { dir->i_mtime = CURRENT_TIME; for (i=0; i < NAME_LEN ; i++) de->name[i]=(i<namelen)?get_fs_byte(name+i):0; bh->b_dirt = 1; *res_dir = de; return bh; } de++; i++; } // 本函数执行不到这里。这也许是Linus在写这段代码时,先复制了上面的find_entry() // 函数的代码,而后修改成本函数的。:-) brelse(bh); return NULL; }
(5)截至目前,linux文件系统涉及到好多的结构体、缓存区、磁盘(主要是inode、buffer_head、dir_entry、block等),不熟悉的初学者看到这里估计都开始晕菜了,这里有个现成的图示(参考1),展示了各个结构体的关系:
整个磁盘文件数据读取流程如下:
- 先根据文件路径找到文件对应的inode节点。假设是个绝对路径,文件路径是/a/b/c.txt;系统初始化的时候我们已经拿到了根目录对应的inode(磁盘上第一个inode节点就是根目录所在的节点,从这里也可以看出,文件目录也必须保存在磁盘,而不仅仅是保存在内存,避免断电后丢失),把根目录文件的block内容读进来,是一系列的dir_entry结构体。然后逐个遍历,比较文件名是不是等于a,最后得到一个目录a对应的dir_entry;
- dir_entry结构体不仅保存了文件名,还保存了对应的inode号;根据inode号把a目录文件的内容也读取进来;以此类推,得到c对应的dir_entry
- 再根据c对应的dir_entry的inode号,从磁盘把inode的内容读进来,发现就是个普通文件;至此,找到了这个文件对应的inode节点,完成fd->file结构体->inode结构体的赋值
- 最后根据fd找到对应的inode节点,根据file结构体的pos字段;根据数据在文件中的偏移,可以算出应该取i_zone[9]字段的哪个索引,文件的前7块对应索引0-6,前7到7+512对应索引7等。得到索引后,读取i_zone数组在该索引的值,即我们要读取的数据在硬盘的数据块。然后把这个数据块从硬盘读取进来。返回给用户
- 整个流程总结:磁盘inode首节点->dir_entry->根据pathname查找目录或文件inode编号->从磁盘读取inode内容->分析i_zone得到文件内容的block编号->从磁盘读数据;整个思路流程和内存管理的CR3分页检索没有任何本质区别;
(6)linux常用的命令还有“cat /home/test.c”、“cd /home/jdk/java” 等目录相关的操作。通过前面的分析得知,操作文件或目录,本质就是读写其元信息,这些都存放在inode里面,所以想想办法得到inode,代码如下:
/* * get_dir() * * Getdir traverses the pathname until it hits the topmost directory. * It returns NULL on failure. */ //// 搜寻指定路径的目录(或文件名)的i节点。 // 参数:pathname - 路径名 // 返回:目录或文件的i节点指针。 static struct m_inode * get_dir(const char * pathname) { char c; const char * thisname; struct m_inode * inode; struct buffer_head * bh; int namelen,inr,idev; struct dir_entry * de; // 搜索操作会从当前任务结构中设置的根(或伪根)i节点或当前工作目录i节点 // 开始,因此首先需要判断进程的根i节点指针和当前工作目录i节点指针是否有效。 // 如果当前进程没有设定根i节点,或者该进程根i节点指向是一个空闲i节点(引用为0), // 则系统出错停机。如果进程的当前工作目录i节点指针为空,或者该当前工作目录 // 指向的i节点是一个空闲i节点,这也是系统有问题,停机。 if (!current->root || !current->root->i_count) panic("No root inode"); if (!current->pwd || !current->pwd->i_count) panic("No cwd inode"); // 如果用户指定的路径名的第1个字符是'/',则说明路径名是绝对路径名。则从 // 根i节点开始操作,否则第一个字符是其他字符,则表示给定的相对路径名。 // 应从进程的当前工作目录开始操作。则取进程当前工作目录的i节点。如果路径 // 名为空,则出错返回NULL退出。此时变量inode指向了正确的i节点 -- 进程的 // 根i节点或当前工作目录i节点之一。 if ((c=get_fs_byte(pathname))=='/') { inode = current->root; pathname++; } else if (c) inode = current->pwd; else return NULL; /* empty name is bad */ // 然后针对路径名中的各个目录名部分和文件名进行循环出路,首先把得到的i节点 // 引用计数增1,表示我们正在使用。在循环处理过程中,我们先要对当前正在处理 // 的目录名部分(或文件名)的i节点进行有效性判断,并且把变量thisname指向 // 当前正在处理的目录名部分(或文件名)。如果该i节点不是目录类型的i节点, // 或者没有可进入该目录的访问许可,则放回该i节点,并返回NULL退出。当然,刚 // 进入循环时,当前的i节点就是进程根i节点或者是当前工作目录的i节点。 inode->i_count++; while (1) { thisname = pathname; if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) { iput(inode); return NULL; } // 每次循环我们处理路径名中一个目录名(或文件名)部分。因此在每次循环中 // 我们都要从路径名字符串中分离出一个目录名(或文件名)。方法是从当前路径名 // 指针pathname开始处搜索检测字符,知道字符是一个结尾符(NULL)或者是一 // 个'/'字符。此时变量namelen正好是当前处理目录名部分的长度,而变量thisname // 正指向该目录名部分的开始处。此时如果字符是结尾符NULL,则表明以及你敢搜索 // 到路径名末尾,并已到达最后指定目录名或文件名,则返回该i节点指针退出。 // 注意!如果路径名中最后一个名称也是一个目录名,但其后面没有加上'/'字符, // 则函数不会返回该最后目录的i节点!例如:对于路径名/usr/src/linux,该函数 // 将只返回src/目录名的i节点。 for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++) /* nothing */ ; if (!c) return inode; // 在得到当前目录名部分(或文件名)后,我们调用查找目录项函数find_entry()在 // 当前处理的目录中寻找指定名称的目录项。如果没有找到,则返回该i节点,并返回 // NULL退出。然后在找到的目录项中取出其i节点号inr和设备号idev,释放包含该目录 // 项的高速缓冲块并放回该i节点。然后去节点号inr的i节点inode,并以该目录项为 // 当前目录继续循环处理路径名中的下一目录名部分(或文件名)。 if (!(bh = find_entry(&inode,thisname,namelen,&de))) { iput(inode); return NULL; } inr = de->inode; // 当前目录名部分的i节点号 idev = inode->i_dev; brelse(bh); iput(inode); if (!(inode = iget(idev,inr))) // 取i节点内容。 return NULL; } }
(7)查找目录的最高路径,比如:
- cd /home/test的最高路径是test(最后一个反斜杠后面是test):basename是test,namelen=4;
- cd /home/test/的最高路径是空的(最后一个反斜杠后面是空的):basename是null,namelen=0;
/* * dir_namei() * * dir_namei() returns the inode of the directory of the * specified name, and the name within that directory. */ // 参数:pathname - 目录路径名;namelen - 路径名长度;name - 返回的最顶层目录名。 // 返回:指定目录名最顶层目录的i节点指针和最顶层目录名称及长度。出错时返回NULL。 // 注意!!这里"最顶层目录"是指路径名中最靠近末端的目录。 static struct m_inode * dir_namei(const char * pathname, int * namelen, const char ** name) { char c; const char * basename; struct m_inode * dir; // 首先取得指定路径名最顶层目录的i节点。然后对路径名Pathname 进行搜索检测,查出 // 最后一个'/'字符后面的名字字符串,计算其长度,并且返回最顶层目录的i节点指针。 // 注意!如果路径名最后一个字符是斜杠字符'/',那么返回的目录名为空,并且长度为0. // 但返回的i节点指针仍然指向最后一个'/'字符钱目录名的i节点。 if (!(dir = get_dir(pathname))) return NULL; basename = pathname; while ((c=get_fs_byte(pathname++))) if (c=='/') basename=pathname; *namelen = pathname-basename-1; *name = basename; return dir; }
(8)这个可能是最有用的函数之一了:namei函数,用户传入路径,返回对应的inode节点
/* * 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. */ //// 取指定路径名的i节点。 // 参数:pathname - 路径名。 // 返回:对应的i节点。 struct m_inode * namei(const char * pathname) { const char * basename; int inr,dev,namelen; struct m_inode * dir; struct buffer_head * bh; struct dir_entry * de; // 首先查找指定路径的最顶层目录的目录名并得到其i节点,若不存在,则返回NULL退出。 // 如果返回的最顶层名字长度是0,则表示该路径名以一个目录名为最后一项。因此我们 // 已经找到对应目录的i节点,可以直接返回该i节点退出。 if (!(dir = dir_namei(pathname,&namelen,&basename))) return NULL; if (!namelen) /* special case: '/usr/' etc */ return dir; // 然后在返回的顶层目录中寻找指定文件名目录项的i节点。注意!因为如果最后也是一个 // 目录名,但其后没有加'/',则不会返回该目录的i节点!例如:/usr/src/linux,将只返回 // src/目录名的i节点。因为函数dir_namei()把不以'/'结束的最后一个名字当作一个文件名 // 来看待,所以这里需要单独对这种情况使用寻找目录项i节点函数find_entry()进行处理。 // 此时de中含有寻找到的目录项指针,而dir是包含该目录项的目录的i节点指针。 bh = find_entry(&dir,basename,namelen,&de); if (!bh) { iput(dir); return NULL; } // 接着取该目录项的i节点号和设备号,并释放包含该目录项的高速缓冲块并返回目录i节点。 // 然后取对应节点号的i节点,修改其被访问时间为当前时间,并置已修改标志。最后返回 // 该i节点指针。 inr = de->inode; dev = dir->i_dev;/*子目录设备号要和父目录一致*/ brelse(bh); iput(dir); dir=iget(dev,inr); if (dir) { dir->i_atime=CURRENT_TIME; dir->i_dirt=1; } return dir; }
namei没有做任何权限的判断,也只是查找现成的dir_entry,如果没有就返回null了,所以只能用在find -name这种命令;但实际用户使用时,还涉及到文件的权限校验,文件打开方式判断(只读?可读可写?)等,情况比find -name这种命令复杂很多,需要单独重新写个接口来实现这些需求,如下:相比namei,
- 检查了权限和打开模式;
- 如果没找到对饮的inode就新建inode,而不是直接返回null;“宏观”层面感受:用户调用open函数想打开一个文件,如果该文件不存在,就新建文件,并返回文件的handler!
/* * open_namei() * * namei for open - this is in fact almost the whole open-routine. */ //// 文件打开namei函数。 // 参数filename是文件名,flag是打开文件标志,他可取值:O_RDONLY(只读)、O_WRONLY(只写) // 或O_RDWR(读写),以及O_CREAT(创建)、O_EXCL(被创建文件必须不存在)、O_APPEND(在文件尾 // 添加数据)等其他一些标志的组合。如果本调用创建了一个新文件,则mode就用于指定文件的 // 许可属性。这些属性有S_IRWXU(文件宿主具有读、写和执行权限)、S_IRUSR(用户具有读文件 // 权限)、S_IRWXG(组成员具有读、写和执行权限)等等。对于新创建的文件,这些属性只应用于 // 将来对文件的访问,创建了只读文件的打开调用也将返回一个可读写的文件句柄。 // 返回:成功返回0,否则返回出错码;res_inode - 返回对应文件路径名的i节点指针。 int open_namei(const char * pathname, int flag, int mode, struct m_inode ** res_inode) { const char * basename; int inr,dev,namelen; struct m_inode * dir, *inode; struct buffer_head * bh; struct dir_entry * de; // 首先对函数参数进行合理的处理。如果文件访问模式标志是只读(0),但是文件截零标志 // O_TRUNC却置位了,则在文件打开标志中添加只写O_WRONLY。这样做的原因是由于截零标志 // O_TRUNC必须在文件可写情况下才有效。然后使用当前进程的文件访问许可屏蔽码,屏蔽掉 // 给定模式中的相应位,并添上对普通文件标志I_REGULAR。该标志将用于打开的文件不存在 // 而需要创建文件时,作为新文件的默认属性。 if ((flag & O_TRUNC) && !(flag & O_ACCMODE)) flag |= O_WRONLY; mode &= 0777 & ~current->umask; mode |= I_REGULAR; // 然后根据指定的路径名寻找对应的i节点,以及最顶端目录名及其长度。此时如果最顶端目录 // 名长度为0(例如'/usr/'这种路径名的情况),那么若操作不是读写、创建和文件长度截0, // 则表示是在打开一个目录名文件操作。于是直接返回该目录的i节点并返回0退出。否则说明 // 进程操作非法,于是放回该i节点,返回出错码。 if (!(dir = dir_namei(pathname,&namelen,&basename))) return -ENOENT; if (!namelen) { /* special case: '/usr/' etc */ if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) { *res_inode=dir; return 0; } iput(dir); return -EISDIR; } // 接着根据上面得到的最顶层目录名的i节点dir,在其中查找取得路径名字符串中最后的文件名 // 对应的目录项结构de,并同时得到该目录项所在的高速缓冲区指针。如果该高速缓冲指针为NULL, // 则表示没有找到对应文件名的目录项,因此只可能是创建文件操作。此时如果不是创建文件,则 // 放回该目录的i节点,返回出错号退出。如果用户在该目录没有写的权力,则放回该目录的i节点, // 返回出错号退出。 bh = find_entry(&dir,basename,namelen,&de); if (!bh) { if (!(flag & O_CREAT)) { iput(dir); return -ENOENT; } if (!permission(dir,MAY_WRITE)) { iput(dir); return -EACCES; } // 现在我们确定了是创建操作并且有写操作许可。因此我们就在目录i节点对设备上申请一个 // 新的i节点给路径名上指定的文件使用。若失败则放回目录的i节点,并返回没有空间出错码。 // 否则使用该新i节点,对其进行初始设置:置节点的用户id;对应节点访问模式;置已修改 // 标志。然后并在指定目录dir中添加一个新目录项。 inode = new_inode(dir->i_dev); if (!inode) { iput(dir); return -ENOSPC; } inode->i_uid = current->euid; inode->i_mode = mode; inode->i_dirt = 1; bh = add_entry(dir,basename,namelen,&de); // 如果返回的应该含有新目录项的高速缓冲区指针为NULL,则表示添加目录项操作失败。于是 // 将该新i节点的引用计数减1,放回该i节点与目录的i节点并返回出错码退出。否则说明添加 // 目录项操作成功。于是我们来设置该新目录的一些初始值:置i节点号为新申请的i节点的号 // 码;并置高速缓冲区已修改标志。然后释放该高速缓冲区,放回目录的i节点。返回新目录 // 项的i节点指针,并成功退出。 if (!bh) { inode->i_nlinks--; iput(inode); iput(dir); return -ENOSPC; } de->inode = inode->i_num; bh->b_dirt = 1; brelse(bh); iput(dir); *res_inode = inode; return 0; } // 若上面在目录中取文件名对应目录项结构的操作成功(即bh不为NULL),则说明指定打开的文件已 // 经存在。于是取出该目录项的i节点号和其所在设备号,并释放该高速缓冲区以及放回目录的i节点 // 如果此时堵在操作标志O_EXCL置位,但现在文件已经存在,则返回文件已存在出错码退出。 inr = de->inode; dev = dir->i_dev; brelse(bh); iput(dir); if (flag & O_EXCL) return -EEXIST; // 然后我们读取该目录项的i节点内容。若该i节点是一个目录i节点并且访问模式是只写或读写,或者 // 没有访问的许可权限,则放回该i节点,返回访问权限出错码退出。 if (!(inode=iget(dev,inr))) return -EACCES; if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) || !permission(inode,ACC_MODE(flag))) { iput(inode); return -EPERM; } // 接着我们更新该i节点的访问时间字段值为当前时间。如果设立了截0标志,则将该i节点的文件长度 // 截0.最后返回该目录项i节点的指针,并返回0(成功)。 inode->i_atime = CURRENT_TIME; if (flag & O_TRUNC) truncate(inode); *res_inode = inode; return 0; }
至此,不知道各读者有没有发现文件和目录相关操作的共性:全都围绕inode和dir_entry两个结构体各种操作!
- dir_entry有字符串数组,存放了目录或文件的字符,可以先根据字符串找到目标dir_entry
- 取出目标dir_entry的inode字段,这个字段标时了inode节点的偏移位置(或者说在磁盘上block的位置)
- 利用inode偏移从磁盘读取inode节点,这里面包含了文件的元信息,尤其时i_zone字段,根据这个进一步从磁盘读取文件数据
- 磁盘中的inode通过inode位图标记是否使用;dir_entry在inode根节点;内存中inode存放在inode_table数组!不论是在磁盘,还是在内存,本质上都是把inode或dir_entry结构体的实例集合起来统一管理(检索查找)!
两个结构体本质上都是用来做索引,dir_entry字段少,相当于简版的索引!inode字段多,相当于完整的索引!
(9)依次类推,mknod也是类似的操作(这居然还是个系统调用,级别相当的高):
//// 创建一个设备特殊文件或普通文件节点(node) // 该函数创建名称为filename,由mode和dev指定的文件系统节点(普通文件、设备特殊文件或命名管道) // 参数:filename - 路径名;mode - 指定使用许可以及所创建节点的类型;dev - 设备号。 // 返回:成功则返回0,否则返回出错码。 int sys_mknod(const char * filename, int mode, int dev) { const char * basename; int namelen; struct m_inode * dir, * inode; struct buffer_head * bh; struct dir_entry * de; // 首先检查操作许可和参数的有效性并取路径名中顶层目录的i节点。如果不是超级用户,则返回 // 访问许可出错码。如果找不到对应路径名中顶层目录的i节点,则返回出错码。如果最顶端的 // 文件名长度为0,则说明给出的路径名最后没有指定文件名,放回该目录i节点,返回出错码退出。 // 如果在该目录中没有写的权限,则放回该目录的i节点,返回访问许可出错码退出。如果不是超级 // 用户,则返回访问许可出错码。 if (!suser()) return -EPERM; if (!(dir = dir_namei(filename,&namelen,&basename))) return -ENOENT; if (!namelen) { iput(dir); return -ENOENT; } if (!permission(dir,MAY_WRITE)) { iput(dir); return -EPERM; } // 然后我们搜索一下路径名指定的文件是否已经存在。若已经存在则不能创建同名文件节点。 // 如果对应路径名上最后的文件名的目录项已经存在,则释放包含该目录项的缓冲区块并放回 // 目录的i节点,放回文件已存在的出错码退出。 bh = find_entry(&dir,basename,namelen,&de); if (bh) { brelse(bh); iput(dir); return -EEXIST; } // 否则我们就申请一个新的i节点,并设置该i节点的属性模式。如果要创建的是块设备文件或者是 // 字符设备文件,则令i节点的直接逻辑块指针0等于设备号。即对于设备文件来说,其i节点的 // i_zone[0]中存放的是该设备文件所定义设备的设备号。然后设置该i节点的修改时间、访问 // 时间为当前时间,并设置i节点已修改标志。 inode = new_inode(dir->i_dev); if (!inode) { iput(dir); return -ENOSPC; } inode->i_mode = mode; if (S_ISBLK(mode) || S_ISCHR(mode)) inode->i_zone[0] = dev; inode->i_mtime = inode->i_atime = CURRENT_TIME; inode->i_dirt = 1; // 接着为这个新的i节点在目录中新添加一个目录项。如果失败(包含该目录项的高速缓冲块指针为 // NULL),则放回目录的i节点,吧所申请的i节点引用连接计数复位,并放回该i节点,返回出错码退出。 bh = add_entry(dir,basename,namelen,&de); if (!bh) { iput(dir); inode->i_nlinks=0; iput(inode); return -ENOSPC; } // 现在添加目录项操作也成功了,于是我们来设置这个目录项内容。令该目录项的i节点字段于新i节点 // 号,并置高速缓冲区已修改标志,放回目录和新的i节点,释放高速缓冲区,最后返回0(成功)。 de->inode = inode->i_num;/*刚创建的inode和dir_entry做映射*/ bh->b_dirt = 1; iput(dir); iput(inode); brelse(bh); return 0; }
早期连创建目录都是系统调用,只能系统管理员创建的:
//// 创建一个目录 // 参数:pathname - 路径名;mode - 目录使用的权限属性。 // 返回:成功则返回0,否则返回出错码。 int sys_mkdir(const char * pathname, int mode) { const char * basename; int namelen; struct m_inode * dir, * inode; struct buffer_head * bh, *dir_block; struct dir_entry * de; // 首先检查操作许可和参数的有效性并取路径名中顶层目录的i节点。如果不是超级用户,则 // 放回访问许可出错码。如果找不到对应路径名中顶层目录的i节点,则返回出错码。如果最 // 顶端的文件名长度为0,则说明给出的路径名最后没有指定文件名,放回该目录i节点,返回 // 出错码退出。如果在该目录中没有写权限,则放回该目录的i节点,返回访问许可出错码退出。 // 如果不是超级用户,则返回访问许可出错码。 if (!suser()) return -EPERM; if (!(dir = dir_namei(pathname,&namelen,&basename))) return -ENOENT; if (!namelen) { iput(dir); return -ENOENT; } if (!permission(dir,MAY_WRITE)) { iput(dir); return -EPERM; } // 然后我们搜索一下路径名指定的目录名是否已经存在。若已经存在则不能创建同名目录节点。 // 如果对应路径名上最后的目录名的目录项已经存在,则释放包含该目录项的缓冲区块并放回 // 目录的i节点,返回文件已经存在的出错码退出。否则我们就申请一个新的i节点,并设置该i // 节点的属性模式:置该新i节点对应的文件长度为32字节(2个目录项的大小),置节点已修改 // 标志,以及节点的修改时间和访问时间,2个目录项分别用于‘.’和'..'目录。 bh = find_entry(&dir,basename,namelen,&de); if (bh) { brelse(bh); iput(dir); return -EEXIST; } inode = new_inode(dir->i_dev); if (!inode) { iput(dir); return -ENOSPC; } inode->i_size = 32;/*目录的inode节点size是32,这个是固定的*/ inode->i_dirt = 1; inode->i_mtime = inode->i_atime = CURRENT_TIME; // 接着为该新i节点申请一用于保存目录项数据的磁盘块,用于保存目录项结构信息。并令i节 // 点的第一个直接块指针等于该块号。如果申请失败则放回对应目录的i节点;复位新申请的i // 节点连接计数;放回该新的i节点,返回没有空间出错码退出。否则置该新的i节点已修改标志。 if (!(inode->i_zone[0]=new_block(inode->i_dev))) { iput(dir); inode->i_nlinks--; iput(inode); return -ENOSPC; } inode->i_dirt = 1; // 从设备上读取新申请的磁盘块(目的是吧对应块放到高速缓冲区中)。若出错,则放回对应 // 目录的i节点;释放申请的磁盘块;复位新申请的i节点连接计数;放回该新的i节点,返回没有 // 空间出错码退出。 if (!(dir_block=bread(inode->i_dev,inode->i_zone[0]))) { iput(dir); free_block(inode->i_dev,inode->i_zone[0]); inode->i_nlinks--; iput(inode); return -ERROR; } // 然后我们在缓冲块中建立起所创建目录文件中的2个默认的新目录项('.'和'..')结构数据。 // 首先令de指向存放目录项的数据块,然后置该目录项的i节点号字段等于新申请的i节点号, // 名字字段等于'.'。然后de指向下一个目录项结构,并在该结构中存放上级目录的i节点号 // 和名字'..'。然后设置该高速缓冲块 已修改标志,并释放该缓冲块。再初始化设置新i节点 // 的模式字段,并置该i节点已修改标志。 de = (struct dir_entry *) dir_block->b_data; de->inode=inode->i_num; strcpy(de->name,".");/*新创建的目录,用ls -al查询会发现有.和..这两个目录*/ de++; de->inode = dir->i_num; strcpy(de->name,".."); inode->i_nlinks = 2; dir_block->b_dirt = 1; brelse(dir_block); inode->i_mode = I_DIRECTORY | (mode & 0777 & ~current->umask); inode->i_dirt = 1; // 现在我们在指定目录中新添加一个目录项,用于存放新建目录的i节点号和目录名。如果 // 失败(包含该目录项的高速缓冲区指针为NULL),则放回目录的i节点;所申请的i节点引用 // 连接计数复位,并放回该i节点。返回出错码退出。 bh = add_entry(dir,basename,namelen,&de); if (!bh) { iput(dir); free_block(inode->i_dev,inode->i_zone[0]); inode->i_nlinks=0; iput(inode); return -ENOSPC; } // 最后令该新目录项的i节点字段等于新i节点号,并置高速缓冲块已修改标志,放回目录和 // 新的i节点,是否高速缓冲块,最后返回0(成功). de->inode = inode->i_num; bh->b_dirt = 1; dir->i_nlinks++; dir->i_dirt = 1; iput(dir); iput(inode); brelse(bh); return 0; }
(10)创建硬链接:本质就是新建该路径的dir_entry,然后和文件原inode映射绑定!
- 找到指定文件的inode
- 再指定文件的路径中创建新的dir_entry
- 新创建的dir_entry映射到原inode:de->inode = oldinode->i_num
//// 为文件建立一个文件名目录项 // 为一个已存在的文件创建一个新链接(也称为硬链接 - hard link) // 参数:oldname - 原路径名;newname - 新的路径名 // 返回:若成功则返回0,否则返回出错号。 int sys_link(const char * oldname, const char * newname) { struct dir_entry * de; struct m_inode * oldinode, * dir; struct buffer_head * bh; const char * basename; int namelen; // 首先对原文件名进行有效性验证,它应该存在并且不是一个目录名。所以我们先取得原文件 // 路径名对应的i节点oldname.若果为0,则表示出错,返回出错号。若果原路径名对应的是 // 一个目录名,则放回该i节点,也返回出错号。 oldinode=namei(oldname); if (!oldinode) return -ENOENT; if (S_ISDIR(oldinode->i_mode)) { iput(oldinode); return -EPERM; } // 然后查找新路径名的最顶层目录的i节点dir,并返回最后的文件名及其长度。如果目录的 // i节点没有找到,则放回原路径名的i节点,返回出错号。如果新路径名中不包括文件名, // 则放回原路径名i节点和新路径名目录的i节点,返回出错号。 dir = dir_namei(newname,&namelen,&basename); if (!dir) { iput(oldinode); return -EACCES; } if (!namelen) {//以反斜杠结尾,后面啥都没了,导致namelen=0; iput(oldinode); iput(dir); return -EPERM; } // 我们不能跨设备建立硬链接。因此如果新路径名顶层目录的设备号与原路径名的设备号不 // 一样,则放回新路径名目录的i节点和原路径名的i节点,返回出错号。另外,如果用户没 // 有在新目录中写的权限,则也不能建立连接,于是放回新路径名目录的i节点和原路径名 // 的i节点,返回出错号。 if (dir->i_dev != oldinode->i_dev) { iput(dir); iput(oldinode); return -EXDEV; } if (!permission(dir,MAY_WRITE)) { iput(dir); iput(oldinode); return -EACCES; } // 现在查询该新路径名是否已经存在,如果存在则也不能建立链接。于是释放包含该已存在 // 目录项的高速缓冲块,放回新路径名目录的i节点和原路径名的i节点,返回出错号。 bh = find_entry(&dir,basename,namelen,&de); if (bh) { brelse(bh); iput(dir); iput(oldinode); return -EEXIST; } // 现在所有条件都满足了,于是我们在新目录中添加一个目录项。若失败则放回该目录的 // i节点和原路径名的i节点,返回出错号。否则初始设置该目录项的i节点号等于原路径名的 // i节点号,并置包含该新添加目录项的缓冲块已修改标志,释放该缓冲块,放回目录的i节点。 bh = add_entry(dir,basename,namelen,&de); if (!bh) { iput(dir); iput(oldinode); return -ENOSPC; } de->inode = oldinode->i_num;/*老的inode对一个的block号给新建的dir_entry,借此建立映射*/ bh->b_dirt = 1; brelse(bh); iput(dir); // 再将原节点的硬链接计数加1,修改其改变时间为当前时间,并设置i节点已修改标志。最后 // 放回原路径名的i节点,并返回0(成功)。 oldinode->i_nlinks++; oldinode->i_ctime = CURRENT_TIME; oldinode->i_dirt = 1; iput(oldinode); return 0; }
总结:
- 目录本质上就是一系列dir_entry的集合!创建/修改目录就是创建/修改dir_entry,创建/修改文件就是创建/修改inode;
- 逆向或破解时掌握dir_entry和inode,就相当于掌握了所有的目录和文件;
参考:
1、https://zhuanlan.zhihu.com/p/76595175 深入浅出文件系统原理之文件读取(基于linux0.11)
2、https://www.bilibili.com/video/BV1tQ4y1d7mo?p=27 linux内核精讲