digestion of file system of minix 3.1.8

来源:互联网 发布:淘宝商城分销平台 编辑:程序博客网 时间:2024/04/24 16:50

文件系统

昨天考完了minix源码解读,今天把我的复习资料整合以后加上我自己的理解发出来,因为复习的时候参考了许多资料,所以文章中很多地方借鉴了其它朋友的东西,不能一一注明,还请见谅。

Minix文件系统的在磁盘中常驻的数据结构有引导块,超级块(部分),i节点位图,区段位图,i节点。而在只在内存中的数据结构有文件描述符,块高速缓存,超级块(部分)。

1. Minix文件系统的物理布局

MINIX文件系统是一个逻辑的、自包含的实体,它含有i-节点、目录和数据块。MINIX文件系统可以存储在任何块设备中。MINIX的文件系统都有相同的布局。如图所示:


                 

Minix文件系统在磁盘上的物理布局

引导块

引导块中包含有可执行代码。启动计算机时,硬件从引导设备将引导块读入内存,转而执行其代码。引导块代码开始操作系统本身的加载过程。一旦系统启动并加载成功之后,引导块不再使用。

超级块

超级块主要定义一些文件系统的参数。如节点数,区段数,最大文件长度等。Minix文件系统完整的超级块概念中除了在磁盘中的超级块外,还有一些数据结构只在内存中。

 struct minix_super_block {

  __u16 s_ninodes;      // i节点的总数

      __u16 s_nzones;       //16位数据块总数

      __u16 s_imap_blocks;  //inode表位图块数

      __u16 s_zmap_blocks;  //数据块位图块数

      __u16 s_firstdatazone;  //数据块的起点

      __u16 s_log_zone_size;  //数据块长

      __u32 s_max_size;     //最大文件字节长度

      __u16 s_magic;        //版本特征值(魔数)

      __u16 s_state;         //安装状态

      __u32 s_zones;        //32位数据块总数

unsigned shorts_block_size;  // 块大小(字节)

chars_disk_version;   //文件系统格式子版本号

//下列各项只有载入内存时才使用

struct inode*s_isup;  // 指向被挂载的文件系统根目录的i节点指针

struct inode*s_imount;  // 指向挂载到i节点的指针

unsigneds_inodes_per_block; //魔数的预先计算值

dev_ts_dev;   // 超级块所属的设备

ints_rd_only;   // 该位1表示只读

ints_native;    //该位1表示非字节交换

ints_version;   // 文件系统版本号,0意味着魔数异常

ints_ndzones;   // 直接区段/i节点

ints_nindirs;   // 间接区段/间接块

bit_ts_isearch;  // i节点位图中的第一个区段位

bit_ts_zsearch;  // 区段位图中的第一个空闲位

};

位图

i节点位图:i节点位图来记录空闲i-节点。

逻辑上,在创建文件时,文件系统必须在位图块中逐一地查找第一个空闲i-节点,把它分配给这个新创建的文件。然而,超级块在内存的拷贝中有一个域指向第一个空闲i-节点,因此不必进行查找,在该空闲i-节点分配使用后,就需要修改指针,使它指向下一个空闲i-节点。这样就实现了快速查找空闲的i节点。消除了很多顺序查找位图的麻烦。这种策略也使用与对区段位图的检索。

区段位图:磁盘存储区可以以区段为单位进行分配,而每个区段可以包含1、2、4、8个,或一般情况下,2n个磁盘块。区段位图按区段,记录空闲存储区。

 

i节点

i节点:Minix的i节点几乎与标准UNIX的节点一模一样。UNIX的节点采用一级寻址,二级寻址,三级寻址混合的寻址方式。Minix也一样,有7个直接寻址的指针,2个间接的寻址的指针。

Minix3的i-node访问时间、修改时间都和标准的UNIX一样,除了读操作外,其余的文件操作都会导致i-node的修改时间变化。

当一个文件被打开时,它的i-node将被找到同时装入到内存中的i-node表中,并且一直保留到该文件被关闭。在内存中的i-node表中有一些字段是磁盘中的i-node没有的。比如该i-node所在的设备。每个i-node还有一个计数器,如果同一个文件被多次打开,那么在内存中只保存一个i-node的副本。当计数器减到0时,该i-node将从内存中删除,如果在内存中曾被修改过,还要把它写入磁盘。

i-node的主要作用是给出文件数据块所在的位置。前7个区段号直接存在i-node中。在Minix发行版中,区段和块的大小都为1KB,所以小于7KB的文件可以不必使用间接块号。当文件的大小超过7KB时,需要使用间接区段。具体方案如图

在i-node中,Minix只用到了其中的一级间接块和二级间接块。如果块的大小和区段的大小都是1KB,区段号位32位,则一级间接块含有256项,可以表示256KB的存储区。二级间接块指向256个一级间接块,因此可以访问长达256个256KB长度的文件。如果块的大小为4KB,那么二级间接块将指向1024*1024个块,文件最大长度位4GB。

i-node中含有模式信息,给出了文件的类型和保护标志位、SETUID位、SETGID位,类型包括普通文件、目录、快设备文件、字符设备或者管道。i-node中的link字段记录了多少个目录项正在指向这个i-node。

inode

EXTERN struct inode {

//磁盘和内存中都有的

  mode_t i_mode;           /* file type, protection, etc. */

  nlink_t i_nlinks;             /* how many links to this file */

  uid_t i_uid;                     /*user id of the file's owner */

  gid_t i_gid;                     /*group number */

  off_t i_size;                    /*current file size in bytes */

  time_t i_atime;             /* time of last access (V2 only) */

  time_t i_mtime;           /* when was file data last changed */

  time_t i_ctime;             /* when was inode itself changed (V2 only)*/

  zone_t i_zone[V2_NR_TZONES]; /* zone numbersfor direct, ind, and dbl ind */

 

//只有内存中有的

  dev_t i_dev;                            /*which device is the inode on */

  ino_t i_num;                            /*inode number on its (minor) device */

  int i_count;                     /*# times inode used; 0 means slot is free */

  unsigned int i_ndzones;       /* # direct zones (Vx_NR_DZONES) */

  unsigned int i_nindirs; /* # indirect zones per indirect block */

  struct super_block *i_sp;    /* pointer to super block for inode's device*/

  char i_dirt;                     /*CLEAN or DIRTY */

  zone_t i_zsearch;                  /* where to start search for new zones */

 

  char i_mountpoint;               /* true if mounted on */

 

  char i_seek;                   /*set on LSEEK, cleared on READ/WRITE */

  char i_update;               /* the ATIME, CTIME, and MTIME bits are here */

 

  LIST_ENTRY(inode) i_hash;     /* hash list */

  TAILQ_ENTRY(inode) i_unused;  /* free and unused list */

 

}inode[NR_INODES];

2. Minix文件系统内存中的数据结构

块高速缓存

MINIX使用块高速缓存来改进文件系统性能。高速缓存用一个缓冲数组来实现,其中每个缓冲区由包含指针、计数器和标志的头以及用于存放磁盘块的体组成。所有未使用的缓冲区均使用双链表,按最近一次使用时间从近到远的顺序链接起来。形成一个LRU链。

为了迅速判断某一块是否在内存中,我们使用了哈希表。所有缓冲区,如果它所包含块的哈希代码为k,在哈希表中用第k项指向的单链表链接在一起。哈希函数提取块号低n位作为哈希代码,因此不同设备的块可以出现在同一哈希链之中。每个缓冲区都在其中某个链中。

在MINIX启动,初始化文件系统时,所有缓冲区均未使用,所以都在LRU双向链表中,并且也由系统预设全部在哈希表第0项指向的单链表中。这时,哈希表的其他项均为空指针。但是一旦系统启动完成,系统需要空白缓存块时,缓冲区将从0号链删除,放到其他链中。系统一段时间后,差不多所有的块都可能被使用过,并随机地分散在不同的哈希链中。

但块被返回到LRU链表中,此块仍被放在原来的哈希链表上,方便系统再次访问。

Minix文件系统的高速缓存

get_block:获取一个块;

put_block:返回之前用get_block请求的块;

alloc_zone:分配一个新区段;

free_zone:释放一个区段;

rw_block:在高速缓存和磁盘间传输一个块;

invalidate:清除用于某设备的所有高速缓存块;

flushall:刷新某设备上所有修改过的块;

rw_scatterred:在设备上读或写分散的块;

rm_lru:从LRU链表中删除一个块。

 

文件系统数据结构

vfs: fproc

EXTERN structfproc {

  unsigned fp_flags;

 

  mode_t fp_umask;                /* mask set by umask system call */

 

  struct vnode *fp_wd;           进程的工作目录/* working directory; NULL during reboot */

  struct vnode *fp_rd;             进程的根目录/* root directory; NULL during reboot */

 

  struct filp *fp_filp[OPEN_MAX]; filp是系统文件打开表表项的结构,这里fp_filp数组保存的是指向该进程打开的文件在系统文件打开表中的指针,在找一个进程打开文件的inode的时候,第一步就是通过该进程的fproc找到filp中的相对位置/* the file descriptor table */

 

  fd_set fp_filp_inuse;             该进程打开的文件描述符/* which fd's are in use? */

  uid_t fp_realuid;           /* real user id */

  uid_t fp_effuid;             /* effective user id */

  gid_t fp_realgid;           /* real group id */

  gid_t fp_effgid;             /* effective group id */

  int fp_ngroups;             /* number of supplemental groups */

  gid_t fp_sgroups[NGROUPS_MAX];/* supplementalgroups */

  dev_t fp_tty;                           /*major/minor of controlling tty */

  int fp_block_fd;             /* place to save fd if rd/wr can't finish */

  int fp_block_callnr;                /* blocked call if rd/wr can't finish */

  char *fp_buffer;            /* place to save buffer if rd/wr can't finish*/

  int fp_nbytes;            /* place tosave bytes if rd/wr can't finish */

  int fp_cum_io_partial;      /* partialbyte count if rd/wr can't finish */

  int fp_revived;               /* set to indicate process being revived */

  endpoint_t fp_task;              /* which task is proc suspended on */

  int fp_blocked_on;                 /* what is it blocked on */

 

  endpoint_t fp_ioproc;           /* proc no. in suspended-on i/o message */

  cp_grant_id_t fp_grant;      /* revoke this grant on unsuspend if >-1 */

 

  char fp_sesldr;              /* true if proc is a session leader */

  char fp_execced;          /* true if proc has exec()ced after fork */

  pid_t fp_pid;                            /*process id */

 

  fd_set fp_cloexec_set;         /* bit map for POSIX Table 6-2 FD_CLOEXEC */

  endpoint_t fp_endpoint;      /* kernel endpoint number of this process*/

}fproc[NR_PROCS];

 

vfs: filp

当一个文件被打开时,就会把一个文件描述符返回给用户进程,用于后续的read和write调用。Minix采用filp共享表记录整个系统所用的打开的文件描述符的部分相关信息。进程表中的文件描述符数组中包含了指向这个filp数组元素的指针。

 

filp是系统管理的所有文件打开表,这个结构中是每个表项中的内容,打开文件的进程不一样需要占有不同的表项

EXTERN structfilp {

  mode_t filp_mode;                系统打开模式/* RW bits, telling how file is opened */

  int filp_flags;                  /* flags from open and fcntl */

  int filp_state;                 /* state for crash recovery */

  int filp_count;                有多少文件描述符/* how many file descriptors share this slot?*/

/*  struct inode *filp_ino;*/  /* pointer to the inode */

 

  struct vnode *filp_vno;        指向拥有这个filp结构的打开文件的vnode结构

 

  u64_t filp_pos;              /* file position */

 

  /* the following fields are for select() andare owned by the generic

   * select() code (i.e., fd-type-specificselect() code can't touch these).

   */

  int filp_selectors;                  /* select()ingprocesses blocking on this fd */

  int filp_select_ops;                /* interested in these SEL_* operations */

  int filp_select_flags;    /* Select flags for the filp */

 

  /* following are for fd-type-specificselect() */

  int filp_pipe_select_ops;

} filp[NR_FILPS];

 

vfs:vmnt

EXTERN struct vmnt {

  intm_fs_e;                   实际文件系统,例如mfs,pfs/*FS process' kernel endpoint */

 dev_t m_dev;                  子文件系统的设备号/*device number */

  intm_flags;                  一些标志位/* mountflags */

 struct vnode *m_mounted_on;   父文件系统被挂载的的vnode号/* vnode onwhich the partition is mounted */

 struct vnode *m_root_node;    子文件系统的根目录vnode号/* rootvnode */

 char m_label[LABEL_MAX];         /*label of the file system process */

} vmnt[NR_MNTS];  //vmnt表

3. Minix文件系统的功能实现

服务器—客户模型

Minix文件系统作为一个服务器程序接受用户进程或其它服务器进程发来的请求消息,如果用户想要操作的数据块就在高速缓存中,则文件系统之间向系统任务发消息请求进行数据操作即可,如图所示。而如果数据不在高速缓存内,则文件系统先要想磁盘驱动程序发送消息请求调入高速缓存,当数据块调入后,再想系统任务发送消息进行数据操作,如图所示。


 

服务器主程序的实现

main.c

调用get_work函数检查是否有以前阻塞的进程被唤醒,如果有,这些进程的优先级将高于收到的消息。此时,应该去运行被唤醒的程序。只有当没有文件系统的内部操作时,get_work才去调用receive函数去接受消息。获取消息发送者的进程号和消息的消息号(who和call_nr)。

 

Main函数的总体框架:

PUBLIC int main()

{

fs_init();

 

while(TRUE){

    get_work(); //get_work接收信息设置全局变量who为调用者的进程表项号,把call_nr设置为即将执行的系统调用的编号。

    fp=&fproc[who];

    //先处理一下特殊的控制消息

    if(call_nr==SYS_SIG){

        ……………..

    }else if(call_nr==SYN_ALARM)

        ……………..

    }

    …………………………..

    //调用内部消息响应函数来处理消息

    else{

        ………………….

        error=(*call_vec[call_nr])();

        //将消息处理结果发送给用户程序

        reply(who,error);

}

return(ok);

}

 

系统调用

1. read

以系统调用read为例,我们先来看文件系统是如何一步一步将调用最终传递给底层系统的:



首先要知道的是在MINIX 3中

         1.设备靠设备文件描述

         2.设备文件是一种特设的i-node,他没有数据区,也没有子i-node。

         3.在设备文件中一定要有该设备的两个号:主编号和副编号(Major/Minor id)。主编号用于区分不同的设备,副编号用于向驱动传递参数。

 

调用过程:

         user:       read

                            _syscall函数

                            send_rec陷入内核,内核向VFS发送消息

         vfs:   main接收到消息

                            调用do_read()函数。

                            调用read_write(),首先调用get_filp(m_in.fd)获得当前文件的文件描述符f,这个f是保存在fproc结构中记录进程打开文件。在得到f之后,可以得到此文件对应的vnode* vp。vnode参见其数据结构。在这里要分析传递进来的文件描述符所描述文件的种类:即块设备或字符型设备描述文件还是普通文件。若为:

                                     管道文件:调用rw_pipe。

                                     字符型设备文件:不使用BufferCache技术,直接使用dev_io调用驱动程序;

                                     块设备型文件:调用req_breadwrite;

                                     普通文件:调用req_readwrite。

                            这些函数除了dev_io外,统统都要向具体的文件系统发送消息。向谁发呢,vp->v_bfs_e 中有可以处理这个文件的FS进程endpoint,这个值应当在打开文件时-即调用open时填充。对普通文件而言这个值就是mfs的进程号。

         mfs: main接收消息

                            调用fs_readwrite()。这里我们的第一项任务就是要通过传入的inode编号找到具体的inode结构。在mfs中inode结构被保存在了一个哈希表中。接下来,我们根据文件中读取偏移position,读取字节数量nrbytes和数据块的大小blocksize来找到一个个chunk,注意,这个chunk可能是一个block的数据,也可能是比一个block小的数据,但不可能大于一个block。我们先说如何实现的,再说为什么要这么实现。minix中这部分的代码是这样的:

                                     off= ((unsigned int) position) % block_size; //读取位置在block中的offset。注意文件是以块为单位存放的。

                                     chunk= min(nrbytes, block_size - off);    //每个chunk的大小是读取字节数和块大小-读取位置的offset中小的一个,想想这是为什么

                                     if(rw_flag == READING) {

                                               bytes_left= f_size - position;  //f_size是文件大小,bytes_left是文件中还有多少没有读的字节

                                               if(position >= f_size) break;    //读取偏移超过文件大小

                                               if(chunk > (unsigned int) bytes_left) chunk = bytes_left;        //这里是防止读的字节超过了文件中剩余的字节

                                     }

                                     ...

                                     nrbytes-= chunk;     //还没读完的字节数

                                     cum_io+= chunk;     //已经读完的字节数

                                     position+= (off_t) chunk;         //读取位置偏移

                           

                            调用rw_chunk函数。此函读取一个Chunk大小的数据到buffer中。MINIX 3 使用了buffercache的技术,即按LRU的方式对数据进行缓存。

                                     调用read_map,确定该文件的目前偏移在哪个block

                                     调用rahead

                                               调用get_block函数:

                                               此函数首先尝试从Buffer中读取

                                                        如果没有再调用rw_block从磁盘读取(同时读到BufferCache中)。

                                                                 rw_block调用block_dev_io对磁盘进行I/O操作。此函数根据dev编号查找数组driver_endpoint:这是一个按主设备编号排序的存储相应驱动程序进程号的,他应当在mount 一个系统时就应该确定好。在得到驱动程序的endpoint编号后向其发送请求读消息。

2. 路径名解析

vfs:eat_path()

                  advance()

                            lookup():

                                     req_lookup()//如果没有mount,一次解析到底。如果有mount,文件系统会在发生mount的目录的inode中发现这个挂载标志位被置上了(if (rip->i_mountpoint) return(EENTERMOUNT);),于是它停止继续解析。而是到挂载在这个目录的子文件系统的根目录inode上继续寻找,它怎么找到子文件系统的根目录inode呢。

                                     我们首先进入下面的循环,这是在有mount的情况下一次解析不成功才会进入的循环

                                     while(r == EENTERMOUNT || r == ELEAVEMOUNT || r == ESYMLINK) {

                                               //下面的for循环,实际上是搜索vmnt表,如果发现表中有一项父文件系统被挂载的目录的inode等于我们上次解析停止的地方的那个目录的res.inode_nr,我们就认为这个vmnt表项就是这个挂载的记录,然后我们取出vmnt表项中的另一个指针:指向子文件系统的根目录的m_root_node指针,然后停止搜索。

                                               for(vmp = &vmnt[0]; vmp != &vmnt[NR_MNTS]; ++vmp) {

                                                        if(vmp->m_dev != NO_DEV && vmp->m_mounted_on) {

                                                                 if(vmp->m_mounted_on->v_inode_nr == res.inode_nr &&

                                                                 vmp->m_mounted_on->v_fs_e== res.fs_e) {

                                                                 dir_vp= vmp->m_root_node;

                                                                 break;

                                                                 }

                                                        }

                                               //下述代码的意思是,我们设置子文件系统的对应的实际文件系统的endpoint和根目录的inode号,以便接下来在子文件系统中进行解析

                                               fs_e= dir_vp->v_fs_e;

                                               dir_ino= dir_vp->v_inode_nr;

                                               req_lookup()//继续解析子文件系统的路径名

                                               }       

                                     }

                           

                                               fs_sendrec()//vfs实际上没有进行路径名解析,它只是解决的mount节点要跳转的问题,实际的路径名解析是在mfs中进行的

mfs:fs_lookup

                   sys_safecopyfrom() //复制路径名,设置调用者id

                   parse_path()             //查找inode

                            这部分见课件minix5P34

                            advance()                  

                                     search_dir()//字符串匹配,搜索解析目录得到的字符串对应的文件是否在当前目录下

                                               read_map()查找当前目录的物理块号

                                               get_block()获得当前目录的缓存块

                                               //在缓存块中搜索匹配的目录项,找到的匹配目录项的inode存放在numb中

                                               put_block()//释放缓存块       

                                     get_inode()//如果找到了就获得指向这个文件mfs中inode的指针

3. OPEN

         do_open()(vfs/open.c)

                   fetch_name()   //获得路径名

                            //检查路径名长度

                            //通过消息或调用sys_datacopy()把路径名写入user_fullpath

                   common_open()

                            首先是调用get_fd获取新的文件描述符;

                                     查看fproc中文件描述符信息,是否有空余文件描述符

                                     获得空的filp slot

                            如果OPEN调用可以是以CREATE模式打开,即O_CREATE位被置上,那么我们要使用new_node()创建一个文件

                                     调用last_dir()查看目录正确性

                                               解析user_fullpath中的目录

                                     调用advance()将路径解析为vnode

                                     查看权限,调用forbidden()

                                     调用req_create()创建inode

                                     创建文件

                   2.否则使用eat_path函数进行路径解析。在filp这个描述系统打开文件的数组里加入这个文件的描述符,返回fd.

4.Mount

首先从理论上了解一下mount的机理

当我们键入mount /dev/c0d1p2/usr的时候,存放硬盘1的第2个分区上的文件系统将被挂载到根文件系统下的/usr/目录下。

挂载完成后,/usr目录的在内存中的inode结构中会置上一个标志位,表明/usr已经被挂载了。mount会在vmnt中添加一个表项记录本次mount。这个表项里有两个指针,分别指向父文件系统被挂载的的vnode号和子文件系统的根目录vnode号。

vfs:   do_mount()

                            mount_fs()

                                     req_mountpoint()

                                               fs_sendrec()

mfs: fs_mountpoint()       //如果可以mount,那么被mount的inode结构中i_mountpoint被置为真

 

0 0
原创粉丝点击