【文件管理】文件的写write()

来源:互联网 发布:淘宝 宝贝 排名 编辑:程序博客网 时间:2024/06/10 18:13

在打开了文件以后 ,或者说建立起进程与文件之间的连接之后,才能对文件进行读/写;为了提高效率,linux对文件的读写是带缓冲的;所谓缓冲,是指系统为最近刚读/写过的文件内容在内核中保留的一份副本,以便当再次需要已经在缓冲存储副本中的内容时,就不必再临时从设备里读了,需要写的时候可以先写到副本中,待系统较为空闲时再从副本写入;在对进程的系统中,由于同一个文件可能为多个进程所共享,缓冲的作用就更显著了;再综合考虑了各层的缓冲的特点,选择了在文件系统层来进行缓冲,主要有file结构,dentry结构,inode结构;

(1)首先是file结构,一个file结构代表目标文件的一个上下文,不同的进程可以在同一个文件上建立不同的上下文,就是同一个进程也可以通过打开同一个文件多次建立起多个上下文;如果在file结构设置一个缓冲队列,那么缓冲区的虽然贴近特定上下文的使用者,却不便于多个进程共享,甚至不便于同一个进程打开的不同上下文共享;

(2)dentry结构,它不属于一个上下文,也不属于某一个进程,可以为所有的进程和上下文共享,可是,dnetry结构与目标文件并不是一对一的关系;多个dentry代表着一个inode结构;

(3)在inode建立起缓冲区队列是在合适不过的了;首先,inode结构与文件是一对一的关系,即使一个文件有多个路径名,最后也是对应到一个inode结构;再说一个文件的内容是不能共享的,在同一时间内,设备上的每一个记录块只能属于至多一个文件,将载有同一个文件内容的缓冲区放在inode是很自然的事;在inode有一个i_mapping指针,它指向address_space结构,缓冲区队列就在这个数据结构中;


挂在缓冲区队列中是内存页面,也就是说文件的内容并不是以记录块为单位缓冲的,而是以页面为单位进行缓冲的;如果一个记录块1K字节,那么一个页面相当于4个记录块;这是为了将文件内容的缓冲和文件的内存映射相结合起来的一种设计;但是对于设备层来说,最自然的是想以记录块为单位的缓冲,因为设备的读写都是以记录块为单位的;预读机制可以解决这个问题;但是另一方面,如果无论是页面缓冲还是记录块缓冲,控制信息和附加信息(比如本身的page结构,缓冲区头部buffer_head)都是游离在这些页面之外的;在一个缓冲区页面中,在文件层通过一个page结构将它挂入缓冲区队列,并且同时又可以通过各个进程的页面映射表映射这些内存空间;而在设备层则又通过若干(通常是4个)buffer_head挂入其所在设备的缓冲区队列;


由于预读机制,file结构实际上是要维持两个上下文的,一个当前位置的f_pos代表真正的读写上下文,而另一个是预读的上下文,即f_reada,f_ramax等;预读机制,读分为同步的,异步的;而写由于kflushd,一定异步的;


首先来看写;

(1)在sys_write()中,在file是一个上下文,有文件的写的位置;首先根据fd找到该已打开文件的file结构;然后,要通过file中f_mode的模式是否是FMODE_WRITE,然后使用locks_verify_area()对f_pos和接下来的count字节对写操作加上强制锁(由内核控制的),然后对inode结构中i_flock一一比对;

(2)然后就是针对普通文件的写操作了,对于ext2,那就是ext2_file_operations,具体的就是generic_file_write();在inode中有个指针i_mapping,指向一个address_space结构(结构中的pages用来维持缓冲区页面队列,如果文件映射到某些进程的用户空间,指针i_mmap指向一串虚存区间,a_pos指向一个address_space_operations数据结构,这个结构的函数指针给出了缓冲页面与具体文件系统的设备层之间的关系和操作,例如怎样从具体的文件系统设备上读或写一个缓冲区页面等);打开参数是O_APPEND标志设为1,则表示对此文件的写操作只能填到末尾;此外对各个进程可以使用各种资源,包括文件大小,都是可以进加限制的;进程task_struct结构中数组rlim就规定了该进程使用各种资源的上限,其中以下标RLIMIT_FSIZE处,就表示对文件大小的限制,如果企图写入的位置超出了这个限制,那就要给这个进程发一个信号SIGXFSZ,并让系统调用返回出错代码-EFBIG;至此,只要待写的长度不为0,那就是一次有效的写操作,所以要在inode打上时间修改的标记,并将标志置成脏,表示其内容应写回设备的相应索引节点;对于remove_suid(),剥夺inode结构的uid和gid,将它们清0;然后写操作的主体是由一个while()实现的;循环的次数取决于写的长度和位置,在每一次循环中,只往一个缓冲区页面中写,并将当前位置的pos向前推进,而剩余未写的长度count则逐次减少,计算出了缓冲页面在目标文件的逻辑序号index以后,就通过__grab_cache_page找到该缓冲页面,如果找不到就分配,建立;

(2.1)在__grab_cache_page()中,首先是通过杂凑计算从页面杂凑队列表page_hash_table中找到所在或应该在的杂凑队列,是通过page_hash来计算的,然后找到对应的page结构;假如找不到的话,就要通过page_cache_alloc()分配一个空闲的页面,并通过add_to_page_cache_unique()将其链入相应的杂凑队列中;

(2.2)在开始写之前,还要做一些准备工作,这最终是由ext2_prepare_write()完成的,函数主体是block_prepare_write()完成的,函数指针get_block指向ext2_get_block(),这个函数的作用是为一个给定的页面中的记录块缓冲区页面做好写入准备;对于已存在的页面,这些缓冲区的buffer_head结构通过指针b_this_page指向同一个页面中的下一个buffer_head,而形成缓冲区页面page结构里的队列buffers;但是若果是新分配建立的页面,则要通过create_empty_buffers()为该页面准备好相应的buffer_head结构,并建立起这个队列;如果缓冲页面已经建立了对应的物理记录块的映射,如果对应的记录块不一致,就通过ll_rw_block()将设备上的记录块读到缓冲区中;但是缓冲页面是新的,就要使用get_block()即ext2_get_block()来完成一个从文件内的逻辑记录块号(前面计算出来的)到设备上记录块号相映射;


(2.3)在ext2_get_block()中,采用了(ext2_inode_info)i_data[]多重间址的方法(它的优点)(从文件内部块号到块设备上的块号的一一映射,(以文件内部块号为下标)就是直接寻址,),由此说明i_data[]并不是同一类型的,但是他们都代表着一个记录块,只不过这些记录块的用途不一样而已;内存inode(专注功能,比如pipe,缓冲机制等)和磁盘上的inode(专注设备本身)是功能不一样的;EXT2_NDIR_BLOCKS为12表示直接映射的记录块数;EXT2_IND_BLOCK也是12,用于i_data[]用于一次间接映射的元素下标;EXT2_DIND_BLOCK和EXT2_TIND_BLOCK分别对应二次和三次间接的元素下标;首先根据文件内部块号计算出这个记录块落在哪一个区间,要采用几重映射,这是由ext2_block_to_path完成的;

(2.3.1)在ext2_block_to_path()中,数组offest记录了每一层映射中使用的位移量,假如文件内块号为10,则不需要间接映射,一步就到位了,返回值为1,offest[0]置为10,假如文件内部块号为20,则offest[0]=12,offest[1]=8;如果返回0,就表示出错了,因为文件内块号和设备上块号至少也得映射一次,出错的原因可能是文件内部块号太大或为负值,或是冲突;

(2.3.2)进一步从磁盘上逐层读入用于间接映射的记录块,这是由ext2_get_branch()完成的;这个函数逐层将用于记录块号映射的记录块读入内存,并将缓冲区的指针保存在数组chain[]的相应元素中,即Indirect结构中,同时还要使Indirect结构中指针p指向本记录块号映射表相应的表项,并使字段key持有该表项的内容,其中bh指向间接映射的记录块;使用verify_chain()要检查映射链的有效性,实质上是检查各层映射表中有关内容是否改变了(本进程可能由于磁盘读记录块时,睡眠);

(2.4)ext2_get_block()中,要将前面的所得的结果填入到bh_result结构中,然后把映射过程中读入的缓冲区(用于间接映射)全部释放掉;首先从本文件的角度从目标记录块中提出一个建议块号,由ext2_find_goal来完成;

(2.4.1)在ext2_find_goal()中,在ext2_inode_info数据结构中设置两个字段,即i_next_alloc_block和i_next_alloc_goal,前者用来记录下一次要分配的文件内部号,后者用来记录下一次能分配的设备上的块号;但是有可能文件内逻辑块号是不连续的;这种情况发生在系统调用lseek()将已打开文件的当前读写位置推进到了超出文件末尾之后,这时候使用ext2_find_near来确定对设备上记录块号的建议值;

(2.5)ext2_get_block()中,设备上具体记录块的分配,包括目标记录块和可能需要的用于间接映射的中间记录块,以及映射的建立,都是由ext2_alloc_branch()完成的;

(2.5.1)在ext2_alloc_branch()中,参数num指向还有几层映射需要建立,实际上就是要分配几个记录块,指针branch指向前面的数组chain[]中从映射断裂的那一部分,offset则指向数组中相应的部分;使用的是ext2_alloc_block,除最底层的记录块,即目标记录块以外,其他的记录块都要通过getblock()为其在内存中分配缓冲区,并通过memset()将其缓冲区清成全0,然后在该缓冲区中建立起本层的映射,再把标志置成脏;

(2.6)对文件的写操作是分两步到位的,第一步是将内容写入缓冲区页面中,使缓冲页面成为脏页面链入到一个LRU队列中,把它提交给内核线程bdflush,第二步是由bdflush将已经变脏的页面写入文件所在的设备中;内核线程bdflush是个死循环,平时总是在睡眠的,每次唤醒时,就冲刷一次脏页面队列,然后再睡眠;为了提高效率,并不是只要有一个脏页面就唤醒bdflush的,而是要到积累到一定数量的脏页面;函数balance_dirty()就是检查是否已经积累太多的脏页面;如果积累太多就将bdflush唤醒;在refill_freelist()中,调用balance_diryt(),继而调用balance_dirty_state(),若返回0,表示脏页面不是很多,可以让bdflush异步地冲刷,返回-1,可分配页面的短缺程度;返回1,表示脏页面的数量已经很大;这时候将参数传给wakeup_bdflush()中,在wake_up_process()将目标进程唤醒,并通过reshedule_idle()比较目标进程和当前进程的综合权值,然后上面的返回值,看是否直接调用flush_dirty_buffers();refill_freelist()中还会看系统中页面的稀缺情况,看是否调用page_launder(),然后通过grow_buffers制造出一些缓冲区来;

(2.7)ext2_get_block()中,现在在设备上已经分配了所需的记录块,包括间接映射的中间记录块,但是原始映射开始断开的最高层上所分配的记录块号只是记在了Indirect结构中key字段,但是并未写入相应的映射表中;现在就要把树枝接到树上,同时还要对所属的inode结构中有关内容做一些调整,这些都是由ext2_splice_branch()完成的;

(3)最后返回到generic_file_write(),为写操作做好准备以后,从缓冲区到设备上的记录块的准备工作已经完成了;这样就可把缓冲待写的内容读过来了;,写入缓冲区页面后,还要将这些页面交给kflushd;kflushd即使不是当时写,最终也会通过address_space_operations结构中的函数指针commit_write()来写入;

0 0
原创粉丝点击