Ext2文件系统—文件打开关闭

1、文件打开

1.1 定义

     用户进程在能够读写一个文件之前必须要先打开这个文件。对文件的读写从概念上应该是一种进程与文件系统之间的一种“有连接”通信,所谓“打开文件”实质上就是在进程与文件之间建立起链接,而“打开文件号”就唯一的标识着这样一个连接,也称为文件描述符(file descriptors,简称fd)。不过,严格意义上的“连接”意味着一个独立的“上下文”,如果一个进程与某个目标之间重复建立起多个链接,则每个连接都应该是互相独立的。在文件系统的处理中,每当一个进程重复打开同一个文件时就建立起一个由file数据结构代表的独立上下文。通常,一个file数据结构,即一个读写文件的上下文都由一个“打开文件号(fd)”加以标识,但是通过系统调用dup()或dup()2却可以使同一个file结构对应到多个“打开文件号”。

1.2代码分析

1 sys_open()                                        

2 →→do_sys_open()                                   

    char *tmp = getname(filename);

         int fd = PTR_ERR(tmp);

         if (!IS_ERR(tmp)) {

                   fd =get_unused_fd_flags(flags);

                   if (fd >= 0) {

                            struct file *f =do_filp_open(dfd, tmp, flags, mode);

                            if (IS_ERR(f)) {

                                     put_unused_fd(fd);

                                     fd =PTR_ERR(f);

                            } else {

                                     fsnotify_open(f->f_path.dentry);

                                     fd_install(fd,f);

                            }

                   }

                   putname(tmp);

         }

         return fd;

参数filename 实际上是文件的路径名(绝对路径或相对路径);mode表示打开的模式,如“只读”等等;而flag则包含了许多标志位,用以表示打开模式以外的一些属性和要求。

3 →→→getname()                                           

getname从用户空间把文件的路径名拷贝到系统空间,使用的是字符串拷贝,前面已经分析过。

3 →→→get_unused_fd_flags()                               

4 →→→→alloc_fd()                                        

这个函数从当前进程的“打开文件表中”找到空闲的表项,该表项的下标即为“打开文件号”。然后do_sys_open会根据文件名通过file_open找到或创建一个“链接”,或者说读写该文件的上下文。文件读写的上下文是由file数据结构代表和描绘的。

因此要分析alloc_fd,就要先弄清楚与进程先关的文件结构以及它们之间的关系。

1、文件对象file

在Linux中,进程是通过文件描述符(file descriptors,简称fd)而不是文件名来访问文件的,文件描述符实际上是一个整数。Linux中规定每个进程能最多能同时使用NR_OPEN个文件描述符,这个值在fs.h中定义,为1024。

每个文件都有一个32位的数字来表示下一个读写的字节位置,这个数字叫做文件位置。每次打开一个文件,除非明确要求,否则文件位置都被置为0,即文件的开始处,此后的读或写操作都将从文件的开始处执行,但你可以通过执行系统调用LSEEK(随机存储)对这个文件位置进行修改。Linux中专门用了一个数据结构file来保存打开文件的文件位置,这个结构称为打开的文件描述(open file description)。这个数据结构的设置是煞费苦心的,因为它与进程的联系非常紧密,可以说这是VFS中一个比较难于理解的数据结构。

首先,为什么不把文件位置干脆存放在索引节点中,而要多此一举,设一个新的数据结构呢?我们知道,Linux中的文件是能够共享的,假如把文件位置存放在索引节点中,则如果有两个或更多个进程同时打开同一个文件时,它们将去访问同一个索引节点,于是一个进程的LSEEK操作将影响到另一个进程的读操作,这显然是不允许也是不可想象的。

另一个想法是既然进程是通过文件描述符访问文件的,为什么不用一个与文件描述符数组相平行的数组来保存每个打开文件的文件位置?这个想法也是不能实现的,原因就在于在生成一个新进程时,子进程要共享父进程的所有信息,包括文件描述符数组。我们知道,一个文件不仅可以被不同的进程分别打开,而且也可以被同一个进程先后多次打开。一个进程如果先后多次打开同一个文件,则每一次打开都要分配一个新的文件描述符,并且指向一个新的file结构,尽管它们都指向同一个索引节点,但是,如果一个子进程不和父进程共享同一个file结构,而是也如上面一样,分配一个新的file结构,会出现什么情况了?让我们来看一个例子:假设有一个输出重定位到某文件A的shell  script(shell脚本),我们知道,shell是作为一个进程运行的,当它生成第一个子进程时,将以0作为A的文件位置开始输出,假设输出了2K的数据,则现在文件位置为2K。然后,shell继续读取脚本,生成另一个子进程,它要共享shell的file结构,也就是共享文件位置,所以第二个进程的文件位置是2K,将接着第一个进程输出内容的后面输出。如果shell不和子进程共享文件位置,则第二个进程就有可能重写第一个进程的输出了,这显然不是希望得到的结果。

至此,已经可以看出设置file结构的原因所在了。file结构中主要保存了文件位置,此外,还把指向该文件索引节点的指针也放在其中。file结构形成一个双链表,称为系统打开文件表,其最大长度是NR_FILE,在fs.h中定义为8192。file结构定义如下:

struct file {

         union {

                   struct list_head        fu_list;     /*所有打开的文件形成一个链表*/

                   struct rcu_head      fu_rcuhead;

         } f_u;

         struct path                f_path;

#define f_dentry      f_path.dentry         /*指向相关目录项的指针*/

#define f_vfsmnt      f_path.mnt          /*指向VFS安装点的指针*/

         const struct file_operations    *f_op;    /*指向文件操作表的指针*/

         atomic_long_t          f_count;      /*使用该结构的进程数*/

         unsigned int             f_flags;       /*打开文件时所指定的标志*/

         mode_t                       f_mode;      /*文件的打开模式*/

         loff_t                           f_pos;       /*文件的当前位置*/

         struct fown_struct  f_owner;

         unsigned int              f_uid, f_gid;   /*用户的UID和GID*/

         struct address_space      *f_mapping;

         … …

};

数据结构中不但有指向文件的dentry结构的指针f_dentry,指向将文件所在设备安装在文件系统中的vfsmnt结构的指针,有共享计数f_count,还有一个在文件中的当前读写位置f_pos,这就是“上下文”。

每个文件对象总是包含在下列的一个双向循环链表之中:

“未使用”文件对象的链表。该链表既可以用做文件对象的内存高速缓存,又可以当作超级用户的备用存储器,也就是说,即使系统的动态内存用完,也允许超级用户打开文件。由于这些对象是未使用的,它们的f_count域是NULL,该链表首元素的地址存放在变量free_list中,内核必须确认该链表总是至少包含NR_RESERVED_FILES个对象,通常该值设为10。

“正在使用”文件对的象链表:该链表中的每个元素至少由一个进程使用,因此,各个元素的f_count域不会为NULL,该链表中第一个元素的地址存放在变量anon_list中。

  如果VFS需要分配一个新的文件对象,就调用函数get_empty_filp(  )。该函数检测“未使用”文件对象链表的元素个数是否多于NR_RESERVED_FILES,如果是,可以为新打开的文件使用其中的一个元素;如果没有,则退回到正常的内存分配。

2、用户打开文件表

每个进程用一个files_struct结构来记录文件描述符的使用情况,这个files_struct结构称为用户打开文件表,它是进程的私有数据。files_struct结构在include/linux/fdtable.h中定义如下:

structfiles_struct {

         atomic_t count;     /* 共享该表的进程数 */

         struct fdtable *fdt;

         struct fdtable fdtab;

         spinlock_t file_lock____cacheline_aligned_in_smp;  

         int next_fd;     /*已分配的文件描述符加1*/

         struct embedded_fd_setclose_on_exec_init;   /* 执行exec(  )时需要关闭的文件描述

符的初值集合*/

         struct embedded_fd_set open_fds_init;        /*文件描述符的初值集合*/

         struct file *fd_array[NR_OPEN_DEFAULT];   /* 文件对象指针的初始化数组*/

};

 

struct fdtable {

         unsigned int max_fds;      /*当前文件对象的最大数*/

         struct file ** fd;      /* 指向文件对象指针数组的指针 */

         fd_set *close_on_exec;    /*指向执行exec(  )时需要关闭的文件描述符*/

         fd_set *open_fds;      /*指向打开文件描述符的指针*/

         struct rcu_head rcu;

         struct fdtable *next;

};

在2.4内核中这两个数据结构(files_struct、fdtable)是放在一起的,都在files_struct中,2.6分开是为了更清晰的分类管理,但是都还是属于files_struct结构的。

fd域指向文件对象的指针数组。该数组的长度存放在max_fds域中。通常,fd域指向files_struct结构的fd_array域,该域包括32个文件对象指针。如果进程打开的文件数目多于32,内核就分配一个新的、更大的文件指针数组,并将其地址存放在fd域中;内核同时也更新max_fds域的值。对于在fd数组中有入口地址的每个文件来说,数组的索引就是文件描述符(filedescriptor)。结构中还有两个位图 close_on_exec_init和open_fd_init,这些位图大致对应着file结构指针数组的内容,但是比fd_array[]的大小要大的多。同时,偶有两个指针close_on_exec和open_fds最初时分别指向上述两个位图。通常,数组的第一个元素(索引为0)是进程的标准输入文件,数组的第二个元素(索引为1)是进程的标准输出文件,数组的第三个元素(索引为2)是进程的标准错误文件。请注意,借助于dup(  )、dup2(  )和 fcntl(  ) 系统调用,两个文件描述符就可以指向同一个打开的文件,也就是说,数组的两个元素可能指向同一个文件对象(一个file结构对应多个“文件打开号”(“文件描述符”),印证了最开始的叙述)。当用户使用shell结构(如2>&1)将标准错误文件重定向到标准输出文件上时,用户总能看到这一点。

当开始使用一个文件对象时调用内核提供的fget(  )函数。这个函数接收文件描述符fd作为参数,返回在current->files->fd[fd]中的地址,即对应文件对象的地址,如果没有任何文件与fd对应,则返回NULL。在第一种情况下,fget(  )使文件对象引用计数器f_count的值增1。当内核完成对文件对象的使用时,调用内核提供的fput(  ) 函数。该函数将文件对象的地址作为参数,并递减文件对象引用计数器f_count的值,另外,如果这个域变为NULL,该函数就调用文件操作的“释放”方法(如果已定义),释放相应的目录项对象,并递减对应索引节点对象的i_writeaccess域的值(如果该文件是写打开),最后,将该文件对象从“正在使用”链表移到“未使用”链表。

 

3、关于文件系统信息的fs_struct结构

struct fs_struct{

         atomic_t count;

         rwlock_t lock;

         int umask;

         struct path root, pwd;

};

count域表示共享同一fs_struct 表的进程数目。umask域由umask( )系统调用使用,用于为新创建的文件设置初始文件许可权。

fs_struct中的root、pwd是一个path结构,包含了dentry以及vfsmount两个指针。其中,root所指向的dentry结构代表着本进程所在的根目录,也就是在用户登录进入系统时所看到的根目录;pwd指向进程当前所在的目录。实际运行时,这两个个目录不一定都在同一个文件系统中。例如,进程的根目录通常是安装于“/”节点上的Ext2文件系统,而当前工作目录可能是安装于/msdos的一个DOS文件系统。因此,fs_struct结构中的rootmnt、 pwdmnt就是对那两个目录的安装点的描述。

4、主要数据结构之间的关系

用两幅图来表示:



有了前面的铺垫下面来分析alloc_fd,它的作用就是从fdtable为文件分配一个文件描述符fd。

         struct files_struct *files =current->files;

    … …

         fdt = files_fdtable(files);

         … …

         if (fd < files->next_fd)

                   fd = files->next_fd;

         if (fd < fdt->max_fds)

                   fd = find_next_zero_bit(fdt->open_fds->fds_bits,

                                                  fdt->max_fds, fd);

首先获得当前进程的files_struct结构files,和fdtable结构fdt。然后根据files_struct结构中的next_fd以及fdtable中的open_fds位图,找到一般情况下的下一个fd。

5 →→→→→expand_files()                              

    然后将files_struct结构files和得到的fd传给expand_files()函数,判断是否要扩大fdtable,并得到最终的fd。

         if (nr >= current->signal->rlim[RLIMIT_NOFILE].rlim_cur)

                   return -EMFILE;

         if (nr < fdt->max_fds)

                   return 0;

一个进程可以有多少个已打开文件只取决于该进程的task_struct结构中关于可用资源的限制(加亮部分)。若初步得到的fd号在这个限制内且没超过fdtable的最大fd号max_fds,则这个初步的fd就是最终fd,正常返回0,否则就需要扩大fd表fdtable。

6 →→→→→→expand_fdtable()                            

这是一个比较重要的函数,传给它的参数仍然是进程的files_struct结构files,和初步得到的fd。首先它会调用alloc_fdtable()创建新的fd表fdtable。

7 →→→→→→→alloc_fdtable()                              

这个函数就会根据传进来的fd号nr创建新的fdtable。它首先会根据一定规则(如2的指数等等)修改nr值,然后就开始分配各种空间。

fdt =kmalloc(sizeof(struct fdtable), GFP_KERNEL);

fdt->max_fds =nr

分配fdtable本身的空间,并设置新的max_fds值为nr。

data =alloc_fdmem(nr * sizeof(struct file *));

fdt->fd =(struct file **)data;

分配新的更大的文件对象指针数组(structfiles* 数组),相当于新的fd_array[],并将地址赋给fdt->fd。

data = alloc_fdmem(max_t(unsignedint, 2 * nr / BITS_PER_BYTE, L1_CACHE_BYTES));

         fdt->open_fds = (fd_set *)data;

         data += nr / BITS_PER_BYTE;

         fdt->close_on_exec = (fd_set *)data;

分配新的open_fds和close_on_exec位图,并分别用open_fds和close_on_exec两个指针指出。这两个位图其实是一片连续的空间,只不过通过两个不同的指针指向不同的位置。

返回到expand_fdtable (),现在有了新的fdtable,然后它又会得到旧的fdtable,于是调用copy_fdtable()进行内存拷贝。

7 →→→→→→→copy_fdtable()                              

这个内存拷贝函数思想很简单,就是把老的fdtable中的 文件对象指针数组、open_fds位图、close_on_exec位图分贝拷贝到新的fdtable的相应项中,并把各对应项多出的空间置0。

返回到expand_fdtable(),到此为止,创建新的fdtable()和赋值工作都完成了,最后通过rcu_assign_pointer(files->fdt, new_fdt);将files_struct结构files中的fdt指针指向这个新创建的fdtable用新的fdtable替换旧的fdtable,大功告成。

我们回过头来看几点。首先是files_struct结构中,files_struct和fdtable的关系。files_struct最重要的是存放两类东西,第一类就是指向fdtable的指针struct fdtable *fdt;第二类就是三个内存空间:文件对象指针数组fd_array[]、open_fds位图空间、close_on_exec位图空间。而要注意的是对于第二类的这三个空间都是初始时初始化的,它仅在开始时文件数目不多(小于32)时使用。一旦文件数目增大到需要创建新的fdtable时,这三个空间的内容就被复制到新分配的空间中,而原本最初时的这三个空间就不再使用,而是保留在files_struct中。其次就是在fdtable中最重要的就是三个指针structfile ** fd、fd_set *open_fds和fd_set*close_on_exec。一旦文件数目增大到需要创建新的fdtable,那么新分配的空间就由这三个指针相应指出。而原来files_struct中的第一类重要指针struct fdtable *fdt就指向这个新的fdtable。最后我们来分析这样设计的原因。进程刚开始创建时,fdtable中fdt里面的指针指向的是fd_array中的实体。随着进程打开文件的不断增大,
系统会在open操作时重新分配空间。对于文件表结构structfiles_struct,其成员变量fd_array为一个大小为NR_OPEN_DEFAULT的数组。这是一种常用的技巧。一般来说,进程的打开的文件不会太多,所以可以直接使用一个比较小的数组,这样可以提高效率避免二次分配,同时由于数组较小,并不会太浪费空间。当文件个数超过数组大小的时候,再重新申请内存。通过这种方法克服了早期Uinx只采用固定大学奥file结构指针数组而使每个进程可以同时打开文件数量受到限制的缺陷。

expand_fdtable()结束继而expand_files()结束,返回到alloc_fd()。得到这个最终为文件分配的fd,然后将next_fd=fd+1。最后在分配了空闲文件描述符号后,通过宏操作FD_SET()将open_fds所指向的位图中的相应位设成1,这个位图代表着已经在使用中的打开文件号。同时,还通过FD_CLR()将由指针close_on_exec所指向的位图中的相应位清0,表示如果当前进程通过exec()系统调用执行一个可执行的程序的话无需将这个文件关闭。这个位图的内容可以通过ioctl()系统调用来设置。

下面进入主体do_filp_open()。

3 →→→do_filp_open ()                                     

4 →→→→open_to_namei_flags()                         

这里的参数flags就是系统调用open()传递下来的,遵循open()界面上对flags的约定,但是do_filp_open函数将要调用的各种lookup函数却对这些标志位有着不同的约定,所以要在调用这些lookup函数之前先调用open_to_namei_flags对这些flags加以变换。函数的注释说的也很清楚,具体就是: 00,表示无写要求(只读),   变换成:01,表示要求读访问权。

   01,表示“只写”,           变换成:10,表示要求写访问权。

   10,表示“读和写”,         变换成:11,表示要求读和写访问权。

   11,特殊,                  变换成:00,表示无访问权限要求。

         acc_mode= MAY_OPEN | ACC_MODE(flag);

 

         /*O_TRUNC implies we need access checks for write permissions */

         if(flag & O_TRUNC)

                   acc_mode|= MAY_WRITE;

/* Allow the LSM permission hook todistinguish append  access from generalwrite access. */

         if(flag & O_APPEND)

                   acc_mode|= MAY_APPEND;

接下来的上述这段代码就是根据标志设置相应权限(flag->acc_mode)。这里需要注意的是一个ACC_MODE宏,定义是这样的:

#define ACC_MODE(x)("\004\002\006\006"[(x)&O_ACCMODE])

这个宏“”里面的就是一个四个元素的字符串,他就是基址,后面[]里面的求值表达式即为偏移量,他可以看成一个广义数组。所以就可以根据[]的偏移量索引基址所在地址的值。通过这种方式可以把需要使用的数字放到数组,通过数组索引完成取得对应的数字。具体可参考:

http://www.360doc.com/content/12/0502/16/9171956_208141193.shtml

接下来就是下面这段代码:

         if(!(flag & O_CREAT)) {

                   error= path_lookup_open(dfd, pathname, lookup_flags(flag),

                                                &nd, flag);

                   if(error)

                            returnERR_PTR(error);

                   gotook;

         }

这段代码意思很明了,就是先看最简单是,需要打开的文件是不是已经存在的,若是(不需要创建,O_CREAT标志为0),则就仅仅需要在文件系统中找到目标节点就行。

4 →→→→path_lookup_open ()                               

5 →→→→→__path_lookup_intent_open ()                     

它会先通过get_empty_filp获得一个空的struct file 结构,填充从sys_open传下来并且要返回的 struct nameidata 结构中structnameidata->intent.open相关字段,然后调用 do_path_lookup() 函数通过_link_path_walk()函数开始分解路径,并最终填充struct nameidata->mnt 与 struct nameidata->dentry 相关字段。最后将得到的structnameidata 结构nd返回(包含intent,包含file)到do_filp_open,进入ok逻辑。

如果不是上面这种简单的寻找,而是O_CREAT标志为1,需要创建文件。

4 →→→→path_lookup_create ()                              

这个函数的注释很重要。传递给它的lookup_flags参数是LOOKUP_PARENT。

/*

 * Create - we need to know the parent.

 */

/**

 * path_lookup_create - lookup a file path withopen + create intent

        *@dfd: the directory to use as base, or AT_FDCWD

        *@name: pointer to file name

        *@lookup_flags: lookup intent flags

        *@nd: pointer to nameidata

        *@open_flags: open intent flags

        *@create_mode: create intent flags

        */

它也是调用__path_lookup_intent_open,只不过调用的lookup_flags又加了一个LOOKUP_CREATE,也就是说最终通过do_path_lookup进行路径查找的lookup_flags=LOOKUP_PARENT+LOOKUP_CREATE+LOOKUP_OPEN。

既然path_lookup_open()和path_lookup_create()都是调用__path_lookup_intent_open,那他们有什么区别呢。区别有两点,第一点是create_mode。前者调用__path_lookup_intent_open时create_mode为0,后者则传递了create_mode给__path_lookup_intent_open,这就导致了在__path_lookup_intent_open中对struct nameidata->intent.open相关字段设置时,填充的建立标志位不一样,见如下代码:

struct file *filp = get_empty_filp()

         nd->intent.open.file= filp;

         nd->intent.open.flags= open_flags;

         nd->intent.open.create_mode= create_mode;

第二点就是传递的lookup_flags不一样。前者的lookup_flags是通过lookup_flags()函数处理的,是只让LOOKUP_FOLLOW和LOOKUP_DERECTORY有可能为1。而后者的关键是lookup_flags中有LOOKUP_PARENT,这就意味着path_lookup_create()寻找的不是目标节点本身,而是其父节点。实现细节是通过do_path_lookup路径解析最终到 __link_path_walk后在last_component:逻辑里会进入lookup_parent:逻辑,也就是说__link_path_walk()返回的nameidata结构nd是目标路径pathname的父目录的nd。但是要注意的是,虽然寻找的是目标节点的父节点,但是nameidata中的qstr结构last还是目标节点的节点名(路径最后一个分量名)和字符串长度,只不过没有去寻找目标节点的dentry结构(以及inode结构),详细可温故__link_path_walk()代码。

紧接着这小段代码是何解:

if (nd.last_type != LAST_NORM ||nd.last.name[nd.last.len])

                   gotoexit;

它的意思是这样的。在正常的路径名中,路径的终点是一个文件名,此时nameidata结构中的last_type由__link_path_walk()设置成LAST_NORM。但是也有可能路径的终点为“.”或“..”,也就是说路径的终点实际是一个目录,此时 __link_path_walk()将last_type设置成LAST_DOT或LAST_DOTDOT,那就应该视为出错返回。因为O_CREAT标志为1,若目标节点不存在就应该创建该节点,可是sys_open只能创建文件不能创建目录,目录要由另一个系统调用mkdir()来创建。同时目标名必须是以“\0”结尾的,这才是正常的文件名,否则说明在目标节点名最后还有一个作为分隔符的“/”字符,那么这还是个目录节点。

通过了这些检查,就说明真正找到了目标文件所在的目录的dentry结构,可以往下执行了。

4 →→→→lookup_hash ()                                   

5 →→→→→__lookup_hash ()                             

既然已经找到了目标文件的父目录dentry,并让指针dir指向了它,那么下一步就是通过lookup_hash()寻找目标文件的dentry结构了。我们知道在缓存中寻找目标dentry,是需要其父dentry的。这是因为dentry的哈希函数(dentry->d_op->d_hash(base, name),或直接d_hash())需要父目录dentry(base),并且哈希之后遍历哈希链时也需要比较两点:父dentry是否正确,文件名字符串是否一样(name),所以无论如何都需要目标dentry的父dentry,这也是为什么前面要先得到目标文件的父目录dentry。

知道了这些,__lookup_hash()就是做上述文字所说的事情。传递给它的参数name是nd->last,即目标文件本身的文件名;base是nd->path.dentry,即目标文件的父目录的dentry。它调用cached_lookup()先从dentry哈希表队列缓存中查找目标dentry(__d_lookup(),d_lookup()),若找到则直接返回,若没找到则调用d_alloc()创建一个新dentry,并填充部分相关字段(如name等),并返回。

这样从lookup_hash()返回的dentry,若是目标文件原来就存在的dentry,那么结构中的d_inode指针指向该文件的inode结构;若这个dentry是新创建的,则d_inode的指针为NULL,他还没有填充inode结构。

正常情况下从lookup_hash()返回的dentry应该是通过d_alloc()新创建的dentry,d_inode的指针为NULL。因为lookup_hash()是path_lookup_create()后面的函数, O_CREAT标志位为1,表示目标文件在此之前不存在,是需要创建的。否则若O_CREAT为0则进入的是path_lookup_open(),返回后就会直接进入ok逻辑,走不到lookup_hash()。

我们先看正常的文件不存在的逻辑。

4 →→→→mnt_want_write ()                             

这是判断设备是否具有写权限。传给它 的参数是nd.path.mnt,也即目标文件父目录所在的设备。只要VFS需要进行与“write”相关的操作,就要调用mnt_want_write()获取写权限,如果write access不再需要,就要通过mnt_drop_write()来释放“write lock”。具体可参考:

http://lwn.net/Articles/281157/

4 →→→→__open_namei_create ()                             

注意这个函数的注释

/*Be careful about ever adding anymore callers of this  function.  Its flags must be in the namei format, not whatget passed to sys_open(). */

这其实是照应了前面在do_filp_open()中调用open_to_namei_flags ()对flags进行变换的再次说明。现在的flags是“namei format~”。

5 →→→→→vfs_create ()                                    

首先注意传给它的参数:inode* 是目标文件父目录的inode,dentry *是目标文件部分填充的dentry,即lookup_hash()返回的dentry,d_inode为NULL,称为“negative”的dentry。nd依旧是do_filp_open()中的nd。

6 →→→→→→may_create ()                                    

主要是做权限检查,函数注释说的很清楚

/*     Checkwhether we can create an object with dentry child in directory

 *  dir.

 *  1. Wecan't do it if child already exists (open has special treatment for

 *    this case, but since we are inlined it's OK)

 *  2. Wecan't do it if dir is read-only (done in permission())

 *  3. Weshould have write and exec permissions on dir

 *  4. Wecan't do it if dir is immutable (done in permission())

 */

这其中有一个IS_DEADDIR()检查目标文件所在的目录是否实际上已被删除,定义如下:

#define IS_DEADDIR(inode)    ((inode)->i_flags & S_DEAD)

删除文件时要将所在目录inode结构中i_flags字段的S_DEAD标志位设成1。但是,如果当时其dentry结构和inode结构的共享计数不能递降到0则不能将这两个数据结构释放。所以,存在这样一种可能性,就是在当前进程通过path_walk()找到了目标文件所在目录(父目录)的dentry结构和inode结构之后,在试图进入临界区时进入了睡眠,而此时正在临界区中的另一个进程删除了这个目录。,所以要通过IS_DEADDIR()来检验。不过一旦进入临界区就不会再发生这种情况了。

mode &= S_IALLUGO;

mode |= S_IFREG;

error = security_inode_create(dir,dentry, mode);

从may_create()返回后这段代码的意思是这样。每个进程都有个“文件访问权限屏蔽”umask,记录在其fs_struct结构中(task_struct结构中的指针fs指向这个数据结构,前面图有说明)。这是一些对文件访问权限的屏蔽位,其格式与表示文件访问权限的mode相同。如果umask的某一位为1,则由此进程所创建的文件就把相应的访问权限“屏蔽”掉。例如:如果一个进程的umask为077,则由它所创建的文件只能由文件主使用,因为对同组人及其它用户的访问权限都给屏蔽掉了。进程的umask代代相传,但可以通过系统调用umask()加以改变。上述代码就是说明怎样根据调用参数mode和进程的umask确定所创建文件的访问模式。

经过安全性和访问权限的检查之后,就调用具体文件系统的inode_operations结构提供的创建文件函数创建文件。这个操作因具体文件系统而异,ext2文件系统函数为ext2_create()。

6 →→→→→→ext2_create ()                               

7 →→→→→→→ext2_new_inode ()                    

8 →→→→→→→→new_inode ()                            

new_inode()通过alloc_inode()分配一个空白的inode结构挂入内核中的inode_in_use队列。新分配的空白inode的i_nlink字段设置为1,表明目前只有一个目录项与这个inode结构相联系。

ei = EXT2_I(inode);

         sbi= EXT2_SB(sb);

         es= sbi->s_es;

回到ext2_new_inode()上述代码就是通过分配的inode获得struct ext2_inode_info、struct ext2_sb_info、struct ext2_super_block结构。

在分析ext2磁盘结构时说过,一般来说文件和其所在的目录存储在同一个块组中,这样才能提高效率。另一方面,文件内容和文件的索引节点也应该存在同一个块组。所以在创建文件系统时(格式化)已经注意到了每个块组在索引节点和记录块数量之间的比例(除了最后一个块组外,每个块组的索引节点数量和记录块数量是固定不变的!)。此外,根据统计,每一个块组中平均有多少个目录,也就是说每个目录平均有多少文件也有个大致比例。所以如果要创建的是文件,就应该首先考虑将它的索引节点分配在其所在目录所处的块组中如果要创建的是目录,则要考虑将来是否能将其数下的文件都容纳在同一块组中,所以应该找一个空闲索引节点的数量超过整个设备上的平均值这么一个块组,而不惜离开其父节点所在的块组“另起炉灶”。有了这个背景就可以理解下面代码的意思。

8 →→→→→→→→find_group_dir ()                          

这对于要创建的的是目录的情况。它的想法很简单,就是通过super_block遍历此超级块的所有块组,找到空闲inode数大于平均数的块组。其实最后返回的是空闲inode数最多的块组。

9 →→→→→→→→→ext2_count_free_inodes ()                      

这个函数是find_group_dir()内部的函数,作用是算出super_block所指代的设备中所有的空闲inode数目,进而算出每个块组的平均数avefreei(通过super_block可以知道此super_block的总块组数s_groups_count)。

10→→→→→→→→→→ext2_get_group_desc ()              

这个函数就是通过super_block和块组号得到此块组的组描述符。这个函数之前有分析过。然后从0到s_groups_count的遍历所有块组,把每个块组的空闲inode数目(desc->bg_free_inodes_count)加起来,就得此设备(super_block)到所有的空闲inode数。

10→→→→→→→→→→ext2_count_free ()                    

这个函数是个小插曲,在需要调试打印内核信息时会用到。但它的作用不仅限于此。函数代码如下:

#ifdef EXT2FS_DEBUG

static const int nibblemap[]= {4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1, 1, 0};

unsigned long ext2_count_free (structbuffer_head * map, unsigned int numchars)

{

         unsignedint i;

         unsignedlong sum = 0;

         if(!map)

                   return(0);

         for(i = 0; i < numchars; i++)

                   sum+= nibblemap[map->b_data[i] & 0xf] +

                            nibblemap[(map->b_data[i]>> 4) & 0xf];

         return(sum);

}

#endif /*  EXT2FS_DEBUG  */

它的作用很简单,每一次循环是计算一个字节中0的个数。整个函数的作用就是计算所给定numchars个字节中0的个数。说白了就是用在位图(bitmap)中计算空闲位(0)的个数。*map是buffer_head的指针,以前讲过,它指向的是一个逻辑块。一个块组的inode位图就是用一个逻辑块存放,所以这个*map就是指向一个块组的bitmap。之所以介绍它是因为它的算法思想比较有趣。

先给定一个全局数组nibblemap[],一共有16个元素。看似毫无规律,其实它就代表着0~15这16个数字的二进制表示中0的个数。所以它的每次循环都是把每个字节拆成低4位和高4位,然后分别把高低4位0的个数算出,然后相加就得到这个字节中0的个数。这种用数组来简化计算的思想很有意思。

8 →→→→→→→→find_group_orlov ()                    

它和find_group_dir()处理的情况是一样的,都是针对要创建的是目录的情况。不过find_group_dir()是老的处理方式(OLDALLOC),这种是新的方法,要复杂些。由于我比较懒就不具体分析了,看看注释就行。。。

/*

 * Orlov's allocator for directories.

* We always try to spread first-leveldirectories.

* If there are blockgroups with bothfree inodes and free blocks counts

 * not worse than average we return one withsmallest directory count.

 * Otherwise we simply return a random group.

* For the rest rules look so:

* It's OK to put directory into agroup unless

 * it has too many directories already(max_dirs) or

 * it has too few free inodes left (min_inodes)or

 * it has too few free blocks left (min_blocks)or

 * it's already running too large debt(max_debt).

 * Parent's group is preferred, if it doesn'tsatisfy these

 * conditions we search cyclically through therest. If none

 * of the groups look good we just look for agroup with more

 * free inodes than average (starting atparent's group).

* Debt is incremented each time weallocate a directory and decremented

 * when we allocate an inode, within 0--255.

 */

8 →→→→→→→→find_group_other ()                        

这是针对要创建的是文件的情况。首先获取文件父目录所在的块组描述符,然后分别检查这个块组的空闲inode(desc->bg_free_inodes_count)和空闲数据块(desc->bg_free_blocks_count)是否为0。如果都不为0,则直接返回这个块组号,也就是遵循前面说的规则。如果有一个为0,则通过下面两种方法获取另外一个块组。先是通过哈希的方法,如果失败则采用线性搜索的方法。

确定了将索引节点分配在哪一个块组以后,就要从该块组的索引节点位图中分配一个节点了,于是进入下面这个for循环。

for (i = 0; i <sbi->s_groups_count; i++) {

                   gdp= ext2_get_group_desc(sb, group, &bh2);

                   brelse(bitmap_bh);

                  bitmap_bh = read_inode_bitmap(sb,group);

                   if(!bitmap_bh) {

                            err= -EIO;

                            gotofail;

                   }

                   ino= 0;

repeat_in_this_group:

通过后面的分析可以知道for循环里的i并不是用作代表块组号。块组号已经通过前面获得,这里的i代表的是最多循环的次数,因此是从0开始一直到总的块组数(s_groups_count)。至于为什么要循环后面会讲。

for循环的前面代码中通过块组号group获得了块组描述符gdp,并进一步读取了此块组的索引节点位图(read_inode_bitmap(sb,group))。

8 →→→→→→→→ext2_find_next_zero_bit ()                   

这个函数就是在获取了块组的索引节点位图以后,从位图中找到以为仍然为0的位,也就是找到一个空闲的索引节点,并把索引节点号存放在ino中。

if (ino >=EXT2_INODES_PER_GROUP(sb)) {

                            /*

                             * Rare race: find_group_xx() decided thatthere were

                             * free inodes in this group, but by the timewe tried

                             * to allocate one, they're all gone.  This can also

                             * occur because the counters whichfind_group_orlov()

                             * uses are approximate.  So just go and search the

                             * next block group.

                             */

                            if(++group == sbi->s_groups_count)

                                     group= 0;

                            continue;

                   }

这段代码和注释就解释了为什么要用for循环。因为一般情况下ext2_find_next_zero_bit()是不会失败的,这是因为在之前获取块组号的时候该块组的的描述符已经告诉我们有空闲节点。但是也存在一些特殊情况(这种情况非常少,读注释!)会使ext2_find_next_zero_bit()失败,这时就需要寻找下一个块组,也就是回到for循环的开头(continue)。因此for循环里的i是代表的次数。

对于这个函数我要说一点题外话。看这个函数的定义时,发现有好几个相同的函数定义存放在不同的文件中。如图:


每个函数体进去看都不太一样,但又很有逻辑。于是问题就出来了,内核到底是调用的哪一个函数呢?在看linux内核代码的过程中经常遇到这样的情况。其实情况是这样的。每种函数定义是针对不同的处理器如arm、powerpc、X86等。它们的功能是一样的,只是针对不同的处理器实现细节和方法会有所不同。对于这样的函数不用每个都具体深入去看,弄清楚函数的作用即可。

ext2_find_next_zero_bit()成功之后就在位图中对索引节点位置位。

8 →→→→→→→→ext2_set_bit_atomic ()                      

9 →→→→→→→→→test_and_set_bit ()                    

这个函数一方面见位图中的某一位设成1,另一方面还检查这一位原来是否为1.如果是1,而且分配的ino已经是这个位图的最后一位,则又要到循环到下一个块组重新找(continue);如果是1,但是分配的ino不是这个位图最后以为,则还在这个块组中,从索引节点位图中再找一个(repeat:ext2_find_next_zero_bit())。

如果循环结束(遍历所有块组)还没找到,则宣告失败,否则则索引节点分配成功,此时要立即把该索引节点所在的索引节点位图记录块的缓冲区标志设成“脏”(mark_buffer_dirty(bitmap_bh))。

到此为止则获得了所分配的索引节点所在的块组号group和索引节点在本块组位图中的序号ino。下面这段代码很重要。

         ino += group *EXT2_INODES_PER_GROUP(sb) + 1;

         if(ino < EXT2_FIRST_INO(sb) || ino > le32_to_cpu(es->s_inodes_count)) {

                   ext2_error(sb, "ext2_new_inode",

                                "reserved inode or inode > inodescount - "

                                "block_group = %d,inode=%lu",group,

                                (unsigned long) ino);

                   err= -EIO;

                   gotofail;

         }

尤其是加亮部分的代码,曾一度困扰我。我原想ino已经获得了,这样一加一乘的那不是又废了。其实情况恰恰想法,要是不这么弄就错了。在这之前获得的ino是什么?它只是索引节点在本块组位图中的序号!索引节点号是在同一设备上保持唯一性的,所以同一个super_block中的所有块组中的索引节点号是逐次递加的(如2号块组的第一个索引节点号比1号块组的最后一个索引节点号大1)。因此要通过获得的块组号group和索引节点在本块组位图中的序号,来算出该索引节点在这个设备中的索引节点号!

算出了分配的索引节点在整个设备中的真正索引节点号后,还要对它作一些检查。比如Ext2文件系统可能保留最初的若干索引节点不用,这个索引节点号也不能大于超级块中的s_inodes_count,即各块组中索引节点的总和。

这之后剩下的就是对新建立的inode(new_inode()创建)结构的进一步填充。如对uid、gid的设置、MACtime的设置、mode设置、inode号填充等。里面还有这么一段代码:

le16_add_cpu(&gdp->bg_free_inodes_count,-1);

         if(S_ISDIR(mode)) {

                   if(sbi->s_debts[group] < 255)

                            sbi->s_debts[group]++;

                   le16_add_cpu(&gdp->bg_used_dirs_count,1);

         }else {

                   if(sbi->s_debts[group])

                            sbi->s_debts[group]--;

         }

这里第一句是把块组描述符中空闲inode数减1。if-else里面debts是ext2里面在对文件或目录决定块组号时所用的一个债务机制:分配目录时债务加1,分配inode时债务减1.这在find_group_orlov()函数注释中有说明。

至此为止ext2_new_inode()的操作就完成了。回到ext2_create()的代码中,接着就是设置新创贱inode结构中的inode_operations结构指针和file_operations结构指针,以及用于文件映射(至虚拟内存中)的address_space_operations结构指针。

至此新文件的索引节点已经分配完成,内核中的inode数据结构已经建立并设置好。由于新的inode已经通过mark_inode_dirty()设置成“脏”,并从哈希队列转移到了super_block中的s_dirty队列里,这样内核就会在适当的时机把这个inode结构的内容写回设备上的索引节点,因此可以认为文件本身的创建已经完成了。

但是,尽管如此,这个我文件还只是一个“孤岛”,同乡这个文件的路径还不存在。所以,回到ext2_create()中,下一步是要在该文件所在的目录中增加一个目录项,使新文件的文件名与其索引节点号挂上钩并出现在目录中,从而建立起通向这个文件的路径。这其实就是在《Ext2文件系统—路径名查找—3--ext2_lookup》一文中所说的路径查找本质过程:节点(目录文件)——>目录项——>节点(目录文件)中,对关键的中间链接——目录项,的建立过程。

7 →→→→→→→ext2_add_nondir ()                       

这个函数就是实现建立文件路径的,传给它的参数dentry是在vfs_create()时为目标文件生成的部分填充的dentry,inode也是前面创建的目标文件的inode。 

8 →→→→→→→→ext2_add_link ()                         

首先是获得后面需要用到的对象如:目标文件的父目录(struct inode *dir = dentry->d_parent->d_inode),目标文件的文件名(const char *name = dentry->d_name.name),目标文件的目录项长度(unsigned reclen = EXT2_DIR_REC_LEN(namelen)),以及目标文件父目录的数据缓存页(unsigned long npages = dir_pages(dir))。

接下来就是一个for循环,遍历目标文件父目录数据缓存页的每一页。

page = ext2_get_page(dir, n, 0);

lock_page(page);

kaddr = page_address(page);

dir_end = kaddr + ext2_last_byte(dir,n);

de = (ext2_dirent *)kaddr;

kaddr += PAGE_CACHE_SIZE - reclen;

for循环中的这段代码非常熟悉,它与《Ext2文件系统-路径查找-3》中的ext2_find_entry()的逻辑很像。就是先获取父目录数据缓存页的其中一页page,然后用虚拟内存操作(address_space)获得page的虚存地址起始kaddr和这页page的尾地址dir_end(ext2_last_byte: 如果剩余的长度大于一个页面,则返回一个页面大小.否则返回剩余空间大小)。最后将kaddr设置成 此页中能装下reclen大小的目录项的极限位置。

接下来的while循环就是在父目录文件的每一个page里面,一个一个的找目录项,看能否找到目标文件的目录项,或是能否找到一个较大的的目录项其空余空间可以插入这个新目标文件的目录项。

if (ext2_match (namelen, name, de))

         gotoout_unlock;

这个if语句就是说如果在父目录中找到了目标文件的目录项(文件名长度和文件名都匹配),则说明目标文件的目录项是存在的,不需要创建新的,便可释放page直接返回。

name_len =EXT2_DIR_REC_LEN(de->name_len);

rec_len =ext2_rec_len_from_disk(de->rec_len);

理解《Ext2文件系统-路径查找-3》一文中对ext2_dir_entry_2的分析就可以理解这段代码。EXT2_DIR_REC_LEN是得到文件名长度为name_len的目录项最少需要的目录项长度,存放在name_len中。而rec_len是此目录项实际的目录项长度。于是下面两个判断就精髓了。

if (!de->inode && rec_len>= reclen)

         gotogot_it;

de->inode=0表示此目录项中的节点被删除(《Ext2文件系统-路径查找-3》一文中说过,删除一个目录项,把ext2_dir_entry_2的inode域置为0并适当增加前一个有效目录项rec_len域的值就可以了),也就是说这个目录项是空闲的。而且这个目录项的实际长度rec_len又大于reclen(目标文件目录项需要的长度),于是这个空闲的目录项就足够存放新文件的目录项,于是进入got_it(找到存放新文件目录项的空间)。

if (rec_len >= name_len + reclen)

         gotogot_it;

而这个就是说虽然此目录项是有效的(de->inode即inode号不为0),但是它的节点中有空间剩余,这通常是由于它后面有节点被删除造成的。并且这个节点还满足它的实际目录项长度rec_len大于它本身所需要的最小长度name_len,并且多出的空间还能够插入新文件的目录项(rec_len - name_len > reclen)。这样的话也找到了能够存放新文件目录项的空间,进入got_it。

de = (ext2_dirent *) ((char *) de +rec_len);

最后如果这个目录项这两个条件都不满足,则加上rec_len,得到此page中的下一个目录项。这样依次遍历直至page中的最后一个目录项ext2_dir_entry_2。

而在while循环的一开始还有这么一段代码

if ((char *)de == dir_end) {

         /*We hit i_size */

         name_len= 0;

         rec_len= chunk_size;

         de->rec_len= ext2_rec_len_to_disk(chunk_size);

         de->inode= 0;

         gotogot_it;

}

由于dir_end的是每个page的尾地址,因此一般情况下修改后的kaddr是不会大于dir_end的(它是dir_end往前reclen个位置的地方)。但有一种情况除外,也就是循环到最后一页时剩余长度不够一个页面,ext2_last_byte返回的剩余空间小于页面大小,则此时kaddr会大于dir_end。所以一般情况下每一个page内的while循环都不会进入这个if语句,只有在父目录的最后一个page结束时才会进入这个if语句,说明到了父目录节点空间的末尾还没有找到合适的坑位给新增的子inode用,于是就应该从inode->i_size之后分配一个新的。

最后在got_it的逻辑中会对废弃的目录项(原inode号为0)的情况或者是结点空间有剩余即在空间中插入一个新的结点的情况又或者是新分配page的情况进行处理,真正的把目标文件的目录项插入到从父目录中找到的坑位中,并标记为脏刷回。由于这段涉及到文件的读写,细节可在分析了“文件读写”代码之后再来看。可参考:

https://groups.google.com/forum/?fromgroups=#!topic/zh-kernel/YnCC4TgG2eY

结束ext2_add_link()回到ext2_add_nondir(),已经把新文件的目录项存放在父目录文件中了,即中间链接与前面父目录的链接完成,接下来就是把新文件的dentry结构与inode结构也挂上钩。

8 →→→→→→→→d_instantiate ()                  

这个代码之前分析过。至此通向新文件的路径构建完成。

这样从vfs_create()返回到__open_namei_create()后,要创建的目标文件的inode和dentry都创建完毕,并且inode和dentry之间的关联以及它们和父目录的关联都建立好了,最后通过代码nd->path.dentry = path->dentry;把目标文件的dentry放入nd中(nd在传进到__open_namei_create()时存放的是父目录的dentry)。这样从 __open_namei_create()返回到do_filp_open()后,nd存放就是已经建立好路径的新文件dentry及其它信息。

4 →→→→nameidata_to_filp ()                        

5 →→→→→__dentry_open ()                       

这就是把之前创建的file结构(filp = nd->intent.open.file)的各相关字段填充(用前面返回的nd填充)后,返回这个file。

到此,从lookup_hash()返回的结构为文件不存在,即需要创建新文件的逻辑已经结束。对于文件存在时的逻辑,主要是一些安全性相关检查和对安装点以及符号链接情况的检查。代码不复杂,就不详细分析了。回到do_sys_open()。

3 →→→fd_install ()                                    

dentry_open()返回指向新建立的file数据结构的指针,并由do_filp_open()最终返回。每一次成功的open()系统调用就为目标文件建立起一个由file数据结构代表的上下文,而与该文件是否已经有其它的file结构无关。最后的这个fd_install()函数的作用是将新建立的file结构的指针“安装”到当前进程的file_struct结构中,确切的说是里面的已打开文件指针数组中,其位置下表fd已经在前面分配好了(rcu_assign_pointer(fdt->fd[fd], file);)。

 

2、文件关闭

sys_close()的代码比较少,主要是fput()等释放函数,详细可在《Linux dentry cache学习》中参考。

注意一点,代码的坐着在sys_close()的注释中讲述了在释放打开文件号之前先检查与其对应的file结构指针filp是否为0的重要性。一方面是因为在打开文件时分配打开文件号在前,而“安装”file结构指针在最后。另一方面,一个今晨挂载fork子进程时可以喧杂让子进程共享而不是继承它的资源,包括其files_struct结构。这样,如果两个进程共享同一个files_struct结构,其中一个进程正在打开文件,已经分配了打开文件号,但是尚未安装file结构指针,而另一个进程却在中途挤进来关闭这个“已打开文件”而释放了这个打开文件号,那就会造成问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值