页次: 1
对于普通用户,平时使用操作系统是肯定涉及到创建、更改、删除文件(比如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内核精讲
离线
页次: 1