第六章 APO文件目录系统

来源:互联网 发布:金方圆数控折弯机编程 编辑:程序博客网 时间:2024/05/16 09:10

 

            第六章  APO文件目录系统与磁盘文件管理类的实现

 

追求:简单啊!巨大啊!超光速啊!爽啊!

以下是初步方案:

      文件系统是对装文件的磁盘空间的文件布局、目录树组织的一种描述。有很多种文件系统,它们的空间接口都不一样。即使同一种文件系统,由于在不同的设备上,其空间接口也不一样。空间接口就是指文件系统空间与内存空间连结的方法集合。所以,文件系统类型.设备类型.空间接口.方法i.文件(参数),才能操作到具体的文件。所以,文件系统必须有一个超级块,也就是MFT(卷的根目录)。或简单说,操作一个文件,必须将它对应的树根也打开。MFT超级块有文件系统类型号、设备号、连到每一个文件的路径等等。不同的文件系统安装到APO系统中的一个子目录时,就加装了MFT。这样,其它文件系统也能在APO中使用;这跟unix/linux的VFS类似。

        为了追求速度的极致,APO不得不采取极端设计。APO的数据总线速度是512GBPS;而SATA3接口的固态硬盘的速度只是6GBPS,相对说就太慢了。如果固态硬盘接口和内部总线也采用256位数据总线,即使有存储芯片的限制;做到64GBPS应该不难,随机读取固态硬盘的一行数据速度可以做到256ME/S= 8GB/S。如果所有的文件系统中的文件名哈希值都统一对应映射到APO中的一个相应i节点;也就是说哈希节点就是i节点。APO卷有4G个i节点号,所以,最多有4G个文件或目录。这样,如果我们将所有的目录文件和包含MFT的32个元文件全部放在固态硬盘,需要多大空间?一个i节点是2W,需要4G*2W = 32GB的i节点空间。一个目录项是4E;那需要  4G*4E = 16GE = 512 GB 目录项空间。同或不同文件名的相同哈希值的项是极少的,相同哈希值的列表文件占空间不到64MB;其它元文件占的空间更少。那些小文件(文件内容占行数1到128行)全部集中在一个16GB的空间中,一个 XWJGL文件管理有将近16M个小文件。所以,固态硬盘有1TB的空间就够用了。除了以上的文件,其它文件的内容都是放在普通硬盘的空间中。固态硬盘有连续页、和连续数据块的2种管理模式;APO最大支持4G个数据块的空间(8PB)、32位的块号;连续页管理则需要空间4GY = 8MC = 16TB、32位的页号。

     当用户从控制台,发“列出一个目录内的所有文件”的消息;系统从该目录文件的内容读出一项项目录项时,当然是希望目录项能包含有全部相关的文件属性。而不是,一个一个inode去I/O;磁盘寻道;那会很慢。尽管,只是从2个文件(目录文件、MFT文件)中,读取信息;相对于从多个文件读取要快。但,如果只从一个目录文件读取信息,就不需来回磁盘寻道了。这个差别很大的,比如、目录内的文件数为10M个(1千万个),linux的i节点为128B;磁盘平均寻道时间为10ms,即使不算访问时间;就算1千万个来回寻道时间也要耗时:100K秒 ~= 28小时。如果,只是从一个大文件读取,那就快多了;寻道时间10ms,访问时间:10M * 128B / 160MB/s = 8s。APO是使用固态硬盘保存目录项,并且文件的属性都在目录项里,一个目录项4E;应用程序可以调用系统的戳穿方法(不用阻塞、不经过日志文件服务进程、直达硬件驱动程序),直接读入文件的所有目录项到流容器,速度64M个目录项/S;那么只需0.16s就可全部列出一个目录内的所有10M文件。那么,速度就会比linux快63万倍。

      当要删除一个目录时,如果目录内有10M个文件。那么,linux就需要从2个文件(目录文件、MFT文件)中,来回磁盘寻道了、一个一个inode去I/O读取信息、释放磁盘空间、释放i节点。我不清楚linux的释放时间是多少?但,漫长的过程,加上不会全是一个进程独占;加上进程切换等,估计耗时会翻3倍多、达到100小时以上。APO的目录项中就有文件内容的数据块指针,所以、释放磁盘空间就可在目录文件进行。释放文件中的一段连续数据空间,大约耗时20ns;就算平均一个文件有1.5个连续段,那也就是:30ns*10M = 0.3s,再加上读取目录项的0.16s;不到0.5s就可完成,速度就会比linux快70万倍。APO释放i节点?APO甚至不用删除目录项,只是在该目录文件的父目录文件的对应目录项清0;相应的i节点置空。

      当要按照10级的路径寻找和打开一个文件时,如果每级目录节点下都有1M个目录项;那么,linux从根目录开始一级一级往下寻找;每一级都要一个一个目录项比较,就算使用了文件名字长度及后来增加的文件类型,用轻型比较加快了速度;而不是每次都要比较“长的文件名字”。每次比较,就算0.1us;那也要10*1M*0.1us = 1s的时间,嗯、勉强满足使用要求。当每级目录节点下都有10M个目录项时,那就要晕倒了。我不知道linux为何不用文件名字哈希值来做比较?APO是直接得到i节点的,APO的i节点就是文件名字的哈希值。管你是有n级的路径,APO在ns级就可建立内存的v节点(包含文件的i节点和对应的目录项)。所以,比起linux要快个几十万倍,是很自然的。

     当没有路径时,要遍历整个目录树。咋办?有更好的方法吗?按树状目录查找已经是比不按顺序的查找要好的多了。还有好办法?嗯,B+树。B+树能够使资料保持有序,并拥有均匀的对数处理时间的插入和删除动作。B树的元素通常会自底向上插入,有别于多数自顶向下插入的二叉树。6级的B+树,可能7次i/o就能定位目标文件。当没有路径时,用文件名字的哈希值搜B+树,几次i/o就可定位到目标文件i节点号;这是不错。但B+树的索引节点指针需额外占用磁盘空间,大约一个文件需6W,如果有2G文件时就可观了,要多用12GW = 48GB的硬盘空间。B+树只适合小量文件的场合。B+树是自我分裂的,小量文件时,额外占用磁盘空间不大;不过每次多花点时间吧。那也是要花时间和多占空间啊,真是奇能淫技啊!改进了一些,过于复杂还是属于小聪明;相比APO的方法还是差远了。说真的、不是我想吹牛b;目前也不知道APO这种方法是否有缺陷。

      当我们查找名字第一位字符是x的所有文件时,哈希值没用了,B+树没用了,unix/linux技穷了,只能遍历整个目录树;磁头频繁移动,慢如乌龟了!APO也是只能是遍所有的目录文件。但APO的目录文件都是放在固态硬盘,无需磁盘寻道;而APO的多功能硬件模块可以在10ns比较4K个目录项;就算总共有1G个目录项,读入、装配4K个目录项需要耗时4K/64M/s = 64us,那么,总耗时16s。你可以试一下,WINDOWS、LINUX如果遍历1G个目录项需耗时多少?就算它们能一分钟扫描1万个目录项并作比较,也要耗时1G/0.01M/60s = 1700小时。

      那个EXT4文件系统,不过是预留一些就近i节点给目录文件,让同一个目录下的少部分文件i节点能靠在一起;稍微减少一些I/O操作。那些“叫兽”就鬼叫,搞得我都脸红。

      APO文件系统就是一个卷描述文档,提供了磁盘压缩、数据加密、磁盘配额(用户可以通过磁盘配额监视和限制磁盘空间的使用)、动态挂载其它文件系统、动态磁盘管理等功能,总共可有256T个64KE的块,所以APO文件系统最大支持到256T*64KE=16EE=512EB的磁盘空间。有最大64K - 256个卷,卷中的每一个数据块64KE,都有一个特定的序号,这个序号就叫做逻辑块号LJID,逻辑块号0指向卷中的第一个块,每个卷总共有4G块。最大支持256TE的磁盘空间。卷、或文件系统根目录可能连结在不同的设备上,所以打开一个文件,它对应的文件系统根目录也要打开。存放在块中的简单的字符排列数据称为流;每一个流都由起始块号和尺寸来描述。节点描述符也称为元数据,按节点ID从0开始的顺序的元数据流构成元数据文件MFT。最大文件2G*64KE = 128TE = 4PB。16位既是2字节2B或一个字符Z,2个字符为1字W;一行E为16个字符16Z或8个字8W,一个数据块是512KW或64KE或2MB或512页;一页Y是128E或4KB或2KZ/1KW,一个单元C是64K个数据块;一个卷J是64K个单元。

      流stream就是指很大的数据流、甚至无限的数据流;通常的文件数据、我们就看作是一个数据流。因为内存空间有限,不可能完全装入一个大的数据流;所以、通常是使用窗口,操作(读、写)数据流中的一部分;要操作另一部分时、则通过移动窗口、或移动流。而这种窗口、就称为数据流的位容器、简称流容器。本地内存的流容器有4种:连续行、连续扇区、连续页、连续数据块。磁盘空间的分配、释放管理有3种:连续页、连续数据块、连续单元。本地内存空间的分配、释放管理有3种:连续行、连续数据块、连续页。打开一个文件来操作,就是发送一条消息;必须给出流容器对象、文件位置、长度、文件和权限标志、消息类型3.消息值(open、close等)。当收到允许回复消息后,你就可以对流容器进行读、写操作了。不同的文件系统,只是对到日志文件的消息的加工方式不同,多了一道翻译工作,当然也就慢了些;磁盘设备驱动只是关联到日志文件。我们打开一个文件,无非是希望系统能把指定文件位置、长度的文件数据读入到流容器、或从流容器回写吧。Open一个文件、成功后,系统返回一个文件号;下次发消息,就要使用文件号了。这里只是简介,具体要往后看。

     一个i节点和一个相应目录项在内存表现就是v节点。对于硬连接、文件名不同、其i节点就不同,也就是有不同的v节点号;但在v节点内的指向文件内容的指针是相同的。对于软连接、同样是有不同的v节点号;但在v节点内的指向文件内容的指针却是指向被软连接的文件的父目录内容中的文件对应目录项。可能有许多进程、或线程同时或先后打开同一个v节点,它们的文件号、文件位置、流容器等都是各自的进程、或线程管理的。对于记录型文件,你可以要求打开文件的n个空记录,编写后添加、插入;不一定就是添加到文件尾部。对于流容器的操作,你要位、字节、字符、字、行操作都行;不就是赋值指令。

一、文件头(inode索引i节点描述符)

 

    inode包含文件的元信息,目录项则指出文件内容在磁盘中的布局。APO系统内部不使用文件名,而使用inode号码来识别文件。表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统据这个文件名对应的哈希值,从哈希顺序列表文件MFT中获取inode信息;从而找到文件内容数据所在的block,读出数据。MFT是一个数据表文件,表中每一项记录就是一个inode;按照inode号码(文件名字哈希值)排列。 MFT文件头、其inode索引节点号为0和卷的根目录文件放在一起,本体也放在一起,构成自归文件;MFT文件体就是inode索引节点列表了。

     考虑到本地内存空间有限;inode应尽可能小,APO的只是2W。如果系统支持最大打开16M个不同的文件,目录项与inode合为4E就需要64ME = 1M个数据块。当然,内存一开始、只是一个中模式位图变量数据块中的一个位图变量被使用,加上4个vnode连续数据块(即可装64K个i节点和目录项的内存v节点);以后,逐步增加。

文件头(i节点描述符)定义: 2W;节点树的节点数小于等于4G。

BU2W  inode{    // i节点描述符、文件名字哈希值。

union{ // 如果是目录文件 i_dirnum.31-28 = 1110

   BU32  i_dirnum;// 低16位、目录文件的第一个连续数据块数或页数。
// i_dirnum.31;  1、该i节点是有效的,0、该i节点是空的(无文件)。

// i_dirnum.30;  1、该i节点是唯一的,0、该i节点是有相同哈希值多项的。

// i_dirnum.29; 1、该i节点是目录文件,0、该i节点是非目录文件。

// i_dirnum.28; 1、该i节点是打开的,0、该i节点是未打开的。

//i_dirnum.27-16;  保留12位。

   BU32  i_mblockp;// 指向连续m个数据块或页的首指针。
};

union{ // 如果是非目录文件 i_dirnum.31-28= 1100.

   BU32  i_dirnum;// 低28位、文件的目录项在父目录文件表中的项数号。

   BU32  i_fnode; // 父目录文件i节点inode;父目录文件名字哈希值。

};

union{ // 如果是有相同哈希值 i_dirnum.31-28= 10x0.

   BU32  i_dirnum; // 保留28位。

   BU32  i_itemp;// 是tname相同文件名字哈希值列表文件中的项指针。

};

union{ // 如果该i节点是打开的i_dirnum.31-28 = 10x1

   BU32  i_dirnum;//低24位是内存v节点表中的项数;v节点号vd。

   BU32  i_itemp;// 是tname相同文件名字哈希值列表文件中的项指针。

};
union{ // 如果该i节点是打开的i_dirnum.31-28 = 11x1

   BU32  i_dirnum;//低24位是内存v节点表中的项数;v节点号vd。

   BU32  不变; // 原来的BU32  i_fnode; 或是BU32  i_mblockp; 

};
}

 

       一个目录下最多可以有256M个目录项。 Tname中的项是最多有256个列字段,每一个列字段是一个i节点格式;所以、每一项需要64E。相同哈希值(同名字)的情形是极少的,但APO还是给出允许64K的这种项;每项可以有最多256个相同哈希值的文件i节点描述;Tname动态生成文件可能需要64个数据块 = 128MB;还有一个64K位的位图变量在系统变量区域。


     APO的32个元文件和目录文件,都是放在固态硬盘;文件内容则放在普通硬盘;对文件名的搜索都是在us级。在i节点上,有父节点、目录项数变量;可以起到快速定位的作用。


     MFT和其他31个文件一起(共32个),组成所谓的“Metafiles”(元文件,也就是System files系统文件)。它们的id号就是文件名的哈希值,是固定的、唯一的。用户的文件(也包括目录)的MFT中的ID号也是取决于文件名字的哈希值,ID号就是哈希值。当某文件被删除时,与之对应的MFT记录ID号将被空出来;如果此时再次添加文件,并且、文件名字的哈希值与空出的ID号相同,那么、系统会填充这ID空位。如果,ID号非空、则需要申请Tname文件中的一项;并初始化该项、和ID号对应的i节点。无论簇的大小,文件头记录(i节点)大小都是2W。

        理论上$MFT在卷中的分配空间(占0.0008%),8GW = 1GE = 16K 数据块 = 32GB。$MFT在固态硬盘中会占用一块连续的空间,是最前面的连续16KC(块) = 1GE的;之后是其它的31个元文件内容。以下是元文件的列表。


项号      元文件     功能       
0          $MFT     主文件列表    最多32GB

1          $MFTMirr 主文件表的部分镜像   

2          $LJHU    垃圾回收表文件 

3          $

4          $tname   相同文件名字哈希值的列表文件。大约32MB

5          $lname   长文件名字的列表文件。大约16MB

6          $ACL     访问控制表文件 

7          $Secure  安全文件     

8          $LogFile 日志文件    

9          $BadClus 坏簇文件  

10         $UpCase  大写文件

11         $

12         $UJKWT   磁盘数据块式管理位图文件     512MB

13         $YUGL    磁盘页式管理位图文件         512MB

14         $DYGL    磁盘单位管理位图文件  16KB

15         $YLJU    硬连接计数文件

16         $XWJGL1  第一个小文件管理文件(1/8/32/128行)21.2GB

17—31     保留

 

    在APO中,因其前32个文件的重要性,对它们的MFT记录在文件区有一个备份。APO将文件作为属性、属性值的集合来处理。每个属性由单个的流(stream)组成,即简单的字符排列。严格的说,APO并不对件进行操作,而只对属性流进行读写。如果第一个MFT记录被破坏了,则APO就读出第二个记录找到MFT镜像文件,镜像文件的第一个记录和MFT的第一个记录完全相同。MFT和MFT镜像文件的位置记录在引导扇区中,引导扇区的一个副本放在逻辑磁盘的中间或末尾。

      在APO文件系统中,任何操作都可以被看成是一个“事件”。比如将一个文件从C盘复制到D盘,整个复制过程就是一个事件。事件日志一直监督着整个操作,当它在目标地——D盘发现了完整文件,就会记录下一个“已完成”的标记。假如复制中途断电,事件日志中就不会记录“已完成”,APO可以在来电后重新完成刚才的事件。事件日志的作用不在于它能挽回损失,而在于它监督所有事件,从而让系统永远知道完成了哪些任务,那些任务还没有完成,保证系统不会因为断电等突发事件发生紊乱,最大程度降低了破坏性。


    APO文件系统每次读写时,它都会检查扇区正确与否。当读取时发现错误,APO会报告这个错误;当向磁盘写文件时发现错误,APO将会十分智能地换一个完好位置存储数据,操作不会受到任何影响。在这两种情况下,APO都会在坏扇区上作标记,以防今后被使用。这种工作模式可以使磁盘错误可以较早地被发现,避免灾难性的事故发生。


访问控制表(Access ControlList,ACL)不是把一个文件的用户分为四类(根用户、文件主、同组用户、其他用户),而是对任何特定的用户或用户组,让每个文件与特定的存取权限相关联。


APO的数据大体上可分为4个部分

(1) Partition boot sector(引导扇区,又称BPB),此部分为所有磁盘格式都共有。

(2) Master File Table(主文件列表,MFT),它是对卷上所有文件头的记录。

(3) System files(系统文件),APO系统一共有32个系统文件。

(4) File area(数据区),留给用户的空间。

 

二、目录项结构:

           APO以一种特殊类型的文件实现了目录,这种文件的数据块包含了类型为APO_dir_entry的结构。每个目录项4E,由两部分组成:所包含文件的时间标识、数据块指针,以及该文件对应的inode号码等属性。数据块指针放在这里,目的是当删除一个大目录时,比如该目录下有一千万个文件;那么I/O只是对该目录文件,而无需针对一个个独立的文件i节点进行I/O,速度会相比linux快数十万倍。其实删除时,只是将该目录文件指向回收站中的一个目录文件项,而在该目录文件的父目录文件的对应目录项清0;相应的非哈希值相同i节点置空;在回收站的回收操作才开始释放资源。但最终释放资源时,如果每个目录项都有对应的文件内容的数据块指针、大小;那马上就可释放磁盘空间、和相应的inode号了。

BU4E APO_dir_entry{

  BU32 inode;  // 索引i节点号、文件名字哈希值。

  BU16 i_mode; // 描述文件的访问权限;文件的读、写、执行权限 

// i_mode.15-13  ftype; 文件类型: 0-符号连接文件,

// 1-普通文件, 2-块设备文件,3-字符设备文件,

// 4-文件系统根目录型文件。

// i_mode.12  ACL;  文件访问权限是否由ACL文件描述。

// i_mode.11-0 FWQX;文件访问权限rwx-rwx-rwx-rwx、owner–root–grp-oth

  BU16 i_flags;  // 文件标志。

// i_flags.15  MASK;      1、文件目录项在列表时为.隐藏

// i_flags.15  VISIBLE_FL; 1、文件内容可见。

// i_flags.13  SECRM_FL;    1、文件完全删除,不可恢复。

// i_flags.12  IMMUYABLE_FL;1、文件不可更改、删除。

// i_flags.11  NODUMP_FL;  1、文件不可生成“DUMP”文件。

// i_flags.10  NOATIME_FL; 1、文件不要打下时间标识。

// i_flags.9   APPEND_FL; 1、对文件的写访问只能是加在文件尾。

// i_flags.8   COMR_FL;  1、文件被压缩过。

// i_flags.7   DMUB_FL;  1、记录型文件是大模式管理,0、吸附式管理。

// i_flags.6   GIDINH_FL;1、新文件继承目录的组ID,0、继承进程的。

// i_flags.5   CHOWN_FL;1、拥有者可更改组ID,0、只能超级用户才可更改。

// i_flags.4   SYNC_FL; 1、同步更新。

// i_flags.3   LFN_FL;    1、长的文件名字。

// i_flags.2   FILE_MOD;  1、记录型文件,0、随机文件。

// i_flags.1-0 MEM_MOD; 文件连续存储模式:0行,1页,2数据块,3单位

}

  BU32 i_uid;     // 描述文件的拥有者标识。

  BU16 i_gid;     // 描述文件的用户组标识。

  BU16 i_count;  // 到本目录项的软连接计数器。

  BU16 i_ycount; // 硬连接的计数器指针。

  BU16 i_size;// 描述文件最后块或页的剩余行数或记录大小(小文件时就行大小)

  BU32 i_blocks;// 描述文件的数据块数或页数或记录数(小文件时就是0)。

  BU16 i_fop;   // 文件名长度和操作方法表指针号。
// i_fop.15-8  name_len; 高8位文件名字长度。
// i_fop.7-0  file_class_num; 低8位文件所属的类号。

  BU16 i_mblock; // 描述文件的第一个连续数据块数或连续页数m。

  BU32 i_mblockp;// 指向第一个连续m个数据块或页或单位的首指针。

  BU64 i_ctime;  // 索引节点最后改变的时间,单位n秒。

  BU64 i_mtime;  // 文件最后修改时间标识,单位n秒。

  BU64 i_crtime; // 文件的出生时间标识,单位nS。

  BU64 i_atime;  // 文件最后访问时间标识,单位n秒。

  BU16 name[32]; // 最大32个Unicode字符文件名字
// 或30个U字符文件名字 + 长名字项(指向256U字符)数。

}


         APO支持硬连接文件,硬连接文件i节点通常与源文件的i节点不同;即使删除源文件i节点、源文件的内容也不会消失,由硬连接指向。这类型的文件数不多;所以APO只支持最多64K个的硬连接文件。YLJU硬连接计数列表文件有一个64K位的位图变量、一个16位的空闲数变量、一个4KE、即64KZ的硬连接计数数组变量count[64k]。小模式管理,用户要建立一个对某个i节点的连接文件;那么,先查该i节点的目录项中的(i_ycount) = 0?是0,则需要分配一个连接计数位图序号;i_ycount = 位图序号,count.i_ycount = 2 ,COPY该i节点及目录项到新建连接文件的部分i节点项和目录项中去,完成。如果非0,count.i_ycount+,COPY该i节点及目录项到新建连接文件的部分i节点项和目录项中去,完成。当删除一个IMMUYABLE_FL = 0文件时,如count.i_ycount ≠ 0;那么,count.i_ycount-;如果结果为0,释放APO的i节点;调用i_fop类号,所指向的文件系统释放磁盘空间方法,释放文件系统的i节点、磁盘空间。如果APO_dir_entry.i_flags.SECRM_FL = 1;那么,该目录项清0;否则保留。如果,要知道所有到某个目录项的软连接、或到某个文件的硬连接;那只能搜索MFT文件了。

      有关文件权限这里就不多说了,这方面linux上是资料很多介绍。那么,用户进程是如何访问根权限的passwd文件?APO系统会提供一个专门的系统方法。该方法只能是访问属于用户的那一部分字段。其实,APO分为多级;司令只有一个root根;长老级即是root根组、只有200个成员;军团长级10个成员,每个管理100个大队;一个大队有60个小组长。4G个用户号中的高16位为0的用户号,就对应这些官吏。这样,就会有多级的不同权限的passwd文件。任一个用户,必定属于一个小组;登录时,只是检查在相应小组的passwd文件;就不必是根权限的passwd文件。当然,用户可以指定级别组登录;你要以根用户登录,只能指定是长老级组才行。


     文件内容的组织是比较复杂的,通常二进制文件是看作流stream。有一定结构的看作是记录文件;如MFT文件,一个记录是2W代表一个i节点。这种记录文件通常是不定的删除、或增加一条记录;所以它们的存储都伴随着管理位图。一种方法是管理位图独立,另一种方法是管理位图吸附。独立的就是我们说大模式管理了,吸附模式就是在每个内容数据块的头部开辟位图变量区。比如目录文件,刚开始只有一个文件时,我们分配16行,可装一个文件目录项和.、..目录项;空出一个。当增加到3个文件时,重新申请为32行,原来的删除;空出3个。当增加到200个文件时,重新申请为连续4页,在.目录项配256位管理位图;就可在256个目录项中增、或减了。当增加到2000个文件时,重新申请为连续4个数据块,在第一个数据块的头部配64K位管理位图变量(占256行、64个目录项);还有下一个连续数据块指针,位图变量的空闲数等;就可在64K个目录项中增、或减了。目录项到了1M,就类似链表结构了;所以,你要简单就吸附式;要速度就大模式管理。APO中的目录文件是采取动态生成的吸附模式。动态生成是操作系统的常态,而小模式管理是最常用的方式。


三、进程打开文件表

 

内存缓冲区、就是指一段内存空间;与位容器的意思是一样的,我更喜欢用后者。很多系统的磁盘I/O都使用自己的缓冲区;而这样一来,磁盘文件内容是先到磁盘缓冲区、之后,内核再拷贝缓冲区到用户进程空间的缓冲区,或回写、也是类似的过程。这很不合理、浪费时间、空间。应该使用多大的流容器才合适;磁盘设备驱动是不清楚的,只是按照固定的容器大小来安排;只有编写应用程序的编程员才清楚。所以,磁盘设备驱动使用的位容器大小应该是由应用程序来定才合理;比如、1PB大小的数据库文件,我们可以安排一个4GB大小的流容器来操控,只是将该流容器的文件描述符提交给磁盘设备驱动;通知磁盘设备驱动直接用该流容器作为缓冲区,你将容器灌满后回消息给我。这样才合理,文件流是直达用户空间,而无需多一道拷贝。用户进程操作完该流容器后,不外是给磁盘设备驱动发一个刷新消息就行了。速度、合理、简洁、节省内存空间是编程要素啊。

      在APO环境中,多个进程可同时读一个文件。为了使得每个进程都能够按自己的步调读文件,每个进程必须有自己的文件位置指针,这样才不会受到其他进程的影响。所以,我们需要一个进程打开文件表;表中的每一项对应一个文件描述符,表项有文件位置、流容器信息、文件状态标志等变量。即使在同一个进程中打开一个文件两次,也会得到两个具有独立文件位置的文件描述符。APO的进程打开文件表与UINX等还是有较大区别的;主要的在于APO的文件I/O、标准I/O方法库是合一的,即是只需一套方法;也不需要使用read、write等的方法;APO就是要简单、和实用。用户可以直接用文件描述符操作流容器,如R1 = fd.1.W;就是把相连流容器的字空间中的第一个字赋值给寄存器R1。 如fd.199.Z= 3; 就是把3赋值给相连流容器的字符空间中的第199个字符;效果与直接用Gi.A2...Aj.流容器名.199.Z;是一样的(假设流容器是Gi.A2...Ai下的成员变量)。前者,如果硬件不好实现的话、只能是编译成一条系统API调用指令setfd( fd.199.Z, 3 ); 那占据应用程序代码空间只是1W、但需要10多个ns啊。后者,编译器直接编译成一条变量赋值指令,只是3W、3ns。而后者的写法上也是可以缩短的,写成Gi..Aj.流容器名.199.Z; 把中间去掉,如果你觉得是不会有同名的时侯,你还可写成:流容器名.199.Z = 3; 流容器与文件号fd虽然是对应的,但编译器不知道fd的值啊;也可能会一个流容器对应多个文件号、但一个文件号只能对应一个流容器。所以,我们的应用程序还是操作自己声明的变量空间吧,文件号fd就用于系统API调用,从而获得、或设置文件属性等。

     当然、我们还可以对流容器做复杂的变换、或运算、或字符串处理、或格式化等等;这些都是ns指令级,无需像UNIX的低效率、每次I/O操作都要调用系统方法(read、write等)。我们也可以用fseek方法来改变文件的位置,再rflush(读磁盘冲洗流容器);或修改完流容器后、用wflush(将流容器刷新到磁盘)。

      APO的系统方法调用,并不是现代操作系统那样复杂和需要大的时间损耗,需要软中断远程调用;需要从用户空间陷入到内核空间,权限级别、堆栈和寄存器都要转换等等,完成后要经过复杂的运动才回到用户空间。其实、APO程序中调用系统方法和调用应用程序内的方法,基本上是没有什么差别的,系统方法还是使用用户堆栈、寄存器是使用公用的R0-R4、和系统的R24-R31(用户通常是R8-R23)。主要的区别是用户程序需要操作系统管理的变量时,只能调用系统的公有方法来进行;用户程序只能是行走在自己的时空中,而系统方法可以在宇宙中横行。系统方法可以将结果返回到R0、R24-R31寄存器中,用户程序一样可以读取R24-R31、R0;但不建议用户程序去使用R24-R31,因为系统中断程序可能会再次改变他们的值。系统中断只是打断用户进程、或线程,不会打断系统方法,对于系统方法是延时中断、直到系统方法完成回到用户程序才真正中断;所以、系统方法都是原子的。


     APO中的一个进程最多有64K个文件描述符,再大我觉得真的没必要;要知道,描述符也是要占用内存资源啊。至于如网络进程等,是否也使用文件描述符、甚至可达到1G个的文件描述符;还是使用别的方式、还在探讨中。一个进程最初只是256个描述符、和256个进程打开文件表项,不够再动态增长(每次增加256个描述符)。0、1、2号描述符通常在进程打开时对应输入、输出、错误标准流。与UINX不同,APO的进程打开表项、和系统文件打开表项是合一的;表项也包含了流容器的描述;打开一个文件总是和流容器关联的。

    文件描述符和打开文件表项之间,也可以使用硬连接、或符号连接;硬连接情形就类同UINX下的子进程的表项和父进程相同;软连接情形就类同UINX下的dup函数。我也很想完全兼容UNIX,但他们的库也搞得太啰嗦了。

Process_XMUB{  // 进程小模式属性表; 1.5KE + 32E 系统管理区域。
   BU256E dx_table{  // 对象头列表,成为当前进程时,基址在A0寄存器。
// 992个对象号,前面160个是只读;后面832个是可读、写。32位对象号的
// 高22位是标志,低10位才是对象号。
   BU2W [32] lf_tab;// 类方法表,0是本类、1-31是方法库DLL。
   BU2W [128] thread_lf_tab;//线程类方法表,128个线程组run()入口和长度。
   BU2W [64] dx_tab;// 静态对象表,
   BU2W [768] gx_tab;// 共享(动态)对象表。
   BU256 signal;     // 256个信号位图。
   BU256 [3]  gx_no_WT; // 768个公共动态共享对象号位图变量。
   BU1K  sblocked;  // 屏蔽码(对应信号、动态变量、对象位图)。
}
   BU64K  Thread_WT; // 64K个线程优先级位图。
   BU64K tblocked;  // 线程屏蔽码。
   BU64K Thread_RUN_WT;// 线程运行位图,Thread_WT BIC tblocked后的结果。
   BU1W  [256] FTEP;// 256个进程打开文件表项数组的首指针数组。
   BU256 [256] fd_WT;// 256个256位的位图变量数组;对应64K个文件描述符。
   BU1E  [256] FTE;// 进程最初的256个打开文件表项。0、1、2已经分配
}

 

     打开文件表项寻址:(Process_XMUB.FTEP.fd.15-8).fd.7-0.E,才能真正的指向文件号fd对应的打开文件表项行首址。这将由编译器和硬件完成,我们要操作流容器,必须是fd.某某;否则,编译器只是当成对象寻址。我们不应随意给fd赋值,只能通过open方法;否则,会因没有安装fd对应的文件表项(FTEP.i = 0)、而异常中断退出进程。( A0 + FTEP + fd.15-8)的内容为0、意味着对应的256个打开文件表项数组指针没有安装。前2项是固定的、已知的,硬件只是判定加上fd的高8位后的内容是否为0吧;即使非0、已经安装,硬件还需判断加上fd的低8位后、第0字内容是否为0。对于fd.5.W 编译器是编译成一条4W(4字)的指令,执行时间4ns。fd.5.W编译后的实际指令操作是:(((A0 + FTEP + fd.15-8).(fd.7-0)).1.W).5.W,即是fd对应的打开文件表项的第1字的内容才是流容器的首指针,这是一种3次间接的寻址。第一次是得到进程打开文件表项数组的首指针(A0 + FTEP +fd.15-8),第2次是首指针加上fd的低8位后、得到具体的打开文件表项指针,第3次是打开文件表项的第一字的指针以字空间为参考的偏移第5个字的内容;硬件要实现这点是很容易的。除了FTEP.0是打开进程时,就已经安装外;其它FTEP.i都是0。而(A0 +FTEP.0)的内容就指向Process_XMUB.FTE;FTE.0、.1、.2也是打开进程时,就已经安装;对应3个标准流。APO系统中每个进程都有三个预先定义并自动打开的流,它们是:stdin、stdout和stderr; 分别代表标准输入、标准输出以及错误输出。


打开文件表项介绍:
BU1E FTE{ // 1E的打开文件表项。file table entry 文件表项(FTE)

   BU32 vnode_p;  // v节点指针。

   BU32 fstream_p;  // 文件流容器本地内存空间指针。

   BU32  fstream_len;// 流容器对象的大小。单位E

   BU48  f_pos;  // 文件在磁盘中的当前位置。

   BU16  sflags; // 文件状态标志。

// sflags.15  FD_CLOEXEC;1、close_on_exec;调用相关exec时,关闭文件

// sflags.14  STREAM_LOCK;  1、锁住流容器,0、解锁。

// sflags.13  STREAM_ALLOW; 1、使用流容器,0、禁止使用。

// sflags.12  STREAM_T;  1、带头部的流容器,0、无头部。

// sflags.11  STREAM_MD; 流容器格式:1、全缓冲,0、行缓冲。

// sflags.10  STREAM_S; 1、流容器是用户定义,0、它方定义。

// sflags.9-8STREAM_MOD;流容器大小模式:0行,1扇区、2页,3数据块

// sflags.7   O_ASYNC;  异步I/O。

// sflags.6   O_RSYNC;  同步读、写。

// sflags.5   O_DSYNC;  等待写完成(仅数据)。

// sflags.4   O_SYNC;  等待写完成(数据和属性)。

// sflags.3   O_NONBLCOK; 非阻塞模式。

// sflags.2   O_APPEND; 1、对文件的写访问只能是加在文件尾。

// sflags.1   QNOTE_FL; 1、软连接标志, 0、非软连接。

// sflags.0   O_EOF;   1、报告文件结束,0、 否。

   BU16 STREAM_NUM;   // 16位流容器内的记录数。

   BU16 STREAM_AVA_LEN;// 流容器的尾部无效长度(单位E)。

   BU16  fd_count; // 软连接的引用计数。

   BU16 allow_err; // 低8位错误代号、高8位权限、许可标志。

// allow_err.15  FERR ; 1、操作文件出错指示,0、否。

// allow_err.14 root;  1、是根用户,0、否。

// allow_err.13  owner; 1、是用户拥有者,0、否。

// allow_err.12  grp;   1、是组用户,0、否。

// allow_err.11 Y_OK; 1、许可进程在目录中删除、或新建一个文件,0、否。

// allow_err.10 RD_OK; 1、许可进程读文件内容,0、否。

// allow_err.9   WR_OK; 1、许可进程写文件内容,0、否。

// allow_err.8   X_OK;  1、许可进程执行该文件,0、否。

   BU32 tmpfname; // 临时文件名字指针

}

        可能有2个进程使用同一个流容器,所以、进程可以锁住流容器、或作为查询另一个进程是否完成对流容器的操作;流容器可用于进程间的通信,只需通过消息传送文件号来建立流容器共享。文件是记录型时,我们通过查询方法能得到一些符合条件的记录放在流容器中;这就需要存放记录号的地方,带头部的流容器就可以实现了。当修改完这些记录后,我们需要发散回送磁盘;指明了带头部的流容器标志,磁盘驱动就会自动发散回送磁盘了。在数据库中增加、删除一条或多条记录等,都需要使用带头部的流容器。一些小型的文件(小于一个扇区16E),流容器可以为连续行大小(1E—15E);因为磁盘驱动使用连续扇区做单位,不匹配会导致磁盘缓冲区与流容器之间的COPY、造成性能损失;这是没法的。当流容器大于一个扇区时,请尽量使用连续扇区大小的流容器;这能提升性能。更大的流容器可以使用连续页(8个扇区/页)、或连续数据块(512页/数据块)做大小单位;可能会有些内存空间损失(不用的空间为0),但提高了速度也是值得的。其它标志是为了稍为兼容UNIX而设。

       软连接标志为1,说明本文件号的有效打开文件表项在第一个字的低16位为引用的fd文件号所指的打开文件表项。除第一个字变量vnode_p低半字、和标志外,本打开文件表项的其余变量无效。

         流容器通常是由用户定义,但也可能是它方定义的、如数据库服务进程来定义等;记录型文件通常是由数据库服务进程来管理。流容器头部可以设计为适合数据库应用,如记录的数据结构、对象、表等的定义放入流容器头部;在头部的顶端,通常是放记录号数组,目的是给磁盘驱动程序用于散射读、或写、经查询后的相关记录。查询表文件的记录通常是1W的哈希值、或1Z;对于查询表文件、我们是当作一个二进制流文件;所以、我们需要牵涉到多个文件流之间的联合操作。查询表文件的查询结果输出,可能就到达另一些记录型文件的流容器头部。所以,我们是需要声明一个保存文件号的数组变量;如果,你要打开上万个对应到同一个流容器的文件组;那APO是支持静态编译器打开的,你可以把它们放在同一个子目录下;你只要声明打开这子目录下的所有文件,剩下的就是编译器的事情了。APO的日志文件服务进程就有一些完全支持数据库服务的线程。APO的数据库服务支持多字段记录型表文件、单字段(可以是结构、表、文本对象等)型表文件、日志文件、大模式的记录删除、增加管理文件等等。用户进程通常是以消息、文件号(流容器)与日志文件服务进程交互。流容器的尾部无效长度是指、因为磁盘驱动是以扇区为单位来读写的,与数据记录不一定能完全匹配造成;用户是不能操作该区域的,否则、你的下一条数据记录会被改动。你可能声明了一个巨大的流容器来装一些记录,但流容器内的记录数最多只能是64K条。文件和流的指示标志字节、用于文件操作错误指示器、文件结束指示器等等。应注意,为了效率、记录或结构字段的大小是以行为单位的。


四、文件内存v节点、进程v节点


      当我们打开一个文件时,在本地内存建立一个v节点;v节点就是文件i节点与对应目录项的组合。目录项中的2E文件名字就不放进v节点内了,相应位置是放进i节点的一部分。每个进程可最多打开64K个文件、对应64K个文件号;相应8K个进程、系统打开的文件数最大512M个文件,这是理论上的;实际上很多进程的文件号使用量较少、有时候、多个文件号映射到同一个v节点。所以、系统只是最多可以打开16M个v节点,需要256个64K位的位图变量、刚好1个位图变量数据块;中模式管理。文件号fp就是对应64K位的位图的位序号;打开一个文件时,如果其i节点已经是打开的、即是指v节点是已经分配了;在该打开的i节点中有v节点号。file_vnode_tab + 4*vp 就是对应fp的v节点在本地内存的首指针vnode_p;我们只须为该文件分配文件号、打开文件表项、并初始化就行了。如果否、那么就要申请一个v节点号、并初始化i节点、v节点、打开文件表项FTE。Open一个文件是一件相对复杂的过程:装入临时v节点、权限检查、创建或删除、否分配文件号、分配v节点号、打开文件表项FTE、初始化、等等。

BU4E  file_vnode{ // 文件对应的本地内存v节点。

   BU32 vnode;    // 索引i节点号、文件名字哈希值。

   BU16 v_mode;   // 描述文件的访问权限;文件的读、写、执行权限

   BU16 v_flags;  // 文件标志。

   BU32 v_uid;     // 描述文件创建时的拥有者标识。

   BU16 v_gid;     // 描述文件创建时的用户组标识。

   BU16 v_count;  // 指向该目录项的软连接计数器。

   BU16 v_ycount; // 硬连接的计数器指针。

   BU16 v_size;   // 描述文件最后块或页的剩余行数(小文件时就是行大小)

   BU32 v_blocks; // 描述文件的数据块数或页数(小文件时就是0)。

   BU16 v_fop;    // 高8位文件名字长度和低8位文件所属的类号。

   BU16 v_mblocks;// 描述文件的第一个连续数据块数或连续页数。

   BU32 v_mvblock;// 指向第一个连续m个数据块或页或行的首指针。

   BU64 v_ctime;  // 索引节点最后改变的时间,单位n秒。

   BU64 v_mtime;  // 文件最后修改时间标识,单位n秒。

   BU64 v_crtime; // 文件的出生时间标识,单位nS。

   BU64 v_atime;  // 文件最后访问时间标识,单位n秒。

   BU32 v_finode; // 父目录文件i节点inode。父目录文件名字哈希值

   BU32 v_n;      // 文件的目录项在父目录文件表中的项数号。

   BU32 v_fvp;    // 父目录文件i节点的v节点号。

   BU16 v_yycount;// 本v节点的引用计数器。

   BU16 v_fdn;    // 指向本v节点的文件号数。

   BU32 [12] v_zubd; //暂时保留

}

 

进程v节点:8K*8E = 64KE 所有的8K个进程刚好占用1个数据块(块号32)。

BU8E  Process_vnode{// 进程v节点。

  BU4E  APO_dir_entry;// 进程i节点对应的目录项。

  BU1E  Process_inode{ // 进程i节点。

   BU32 v_finode; // 父目录文件i节点inode。父目录文件名字哈希值

   BU32 v_n;      // 文件的目录项在父目录文件表中的项数号。

   BU32 v_fvp;    // 父目录文件i节点的v节点号。

   BU32 MFT_fdv; // 进程所属MFT的i节点的打开v节点号

   BU32 pwd_fdv; // 进程当前工作目录的打开v节点号。

   BU32 DDRDV;  // 进程的共享DDR数据空间数据块地址

   BU16 yycount;// 本进程的引用计数器。

   BU16 DSstart;// 进程的数据段(变量属性表)的开始地址(块号)。

   BU16 pid; // 进程号;也是打开本进程文件的标识号( < 8K )。

   BU16 counter;//任务运行时间计数(滴答数),运行时间片,动态优先级值。

}

  BU1W [8]  p_uid;// 进程用户号数组。1E

  BU1Z [16] p_gid;// 进程组号数组。1E

  BU1E Process_task{// 进程控制块。

   BU32utime;   // 用户态运行时间(滴答数)。

   BU32alarm;   // 报警定时值(滴答数)。

   BU16 priority;// 优先级,开始时 counter = priority;越大运行越长。

   BU16 fs_n; // 进程打开的文件数。

   BU16 class_n;// 进程的类数。

   BU16 dx_n; // 进程的对象数。

   BU16 dtbl_n;  // 进程的动态变量数。

   BU16 Thread_n;//进程的线程数。

   BU3W 暂时保留;

}

}

        

0 0