zoukankan      html  css  js  c++  java
  • 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内核精讲

  • 相关阅读:
    意见簿---在批评中成长
    团队冲刺——第六天
    团队冲刺——第五天
    团队冲刺--第四天
    小知识:引入properties时,遇到@Value("${xxxxx}")时,报注入失败的错误
    小知识:springCloudConfgServer启动时报警告:.c.s.e.MultipleJGitEnvironmentRepository : Could not fetch remote for master remote:
    小知识:springCloudConfig进行配置更新时报错:actuator/refresh报错 Get method not surport
    小知识:SpringCloudConfigServer从git获取配置信息报错:.c.s.e.MultipleJGitEnvironmentRepository : Error occured cloning to base directory.
    小知识:springCloudConfigServer连接git报错:.c.s.e.MultipleJGitEnvironmentRepository : Error occured cloning to base directory.
    小知识:最近发现http://maven.aliyun.com/nexus/content/groups/public的仓库,pom下载jar包失败了
  • 原文地址:https://www.cnblogs.com/theseventhson/p/15643658.html
Copyright © 2011-2022 走看看