rtems 架构支持:
• Mountable file systems 可挂载
• Hierarchical file system directory structure 分级
• POSIX compliant set of routines for the manipulation of files and directories posix兼容的文件目录操作

独立的文件和目录支持:

  1. Permissions for read, write and execute
  2. User ID
  3. Group ID
  4. Access time
  5. Modification time
  6. Creation time
    • Hard links to files and directories
    • Symbolic links to files and directories

它提供了类似unix的文件系统。posix的一些对文件和目录的操作都已经实现。rtems文件系统的概念有助于对未来设备的扩展和适应。也正是因为扩展原因,所以rtems文件系统具有可挂载性。

定义在rtems configuration中的设备都被注册为可挂载文件系统中的文件,访问设备驱动和设备可以通过传统的文件操作方式 ,除了IO manager中接口外,还有open(), read(), write(), lseek(), fstat() and ioctl() functions

rtems提供基于ram的文件系统In-Memory File System (IMFS),提供完全的posix文件系统能力
IMFS在其文件系统的每个已安装实例中为每个文件,设备和目录维护节点结构。 节点结构用于管理所有权,访问权限,访问时间,修改时间和创建时间。

术语简介:
磁盘块:块设备对磁盘的一个抽象,对于文件系统而言,磁盘就是一个一个连续的块,每个块通常大小为4KB,并且磁盘块有序编码,每个块对应有一个磁盘块号
文件逻辑块号:从逻辑上讲,一个文件可以看做一系列连续的数据块,每个数据块的大小和磁盘块大小相同;
从物理上讲,一个文件可以对应磁盘上若干个磁盘块,这些磁盘块可以在物理上不连续。
逻辑快号和磁盘块号的对应关系,很像虚拟地址和物理地址的关系:文件给你的感觉就是连续的数据,也就是连续的逻辑块,但实际上文件对应的磁盘块是可以不连续的,也就是不连续的物理块
inode:保存文件的元数据,元数据可以描述一个文件
很可惜inode里没有一下子就能想到的filename。关于filename下一节会讲到
一个inode最基本也最重要的信息就两个
用来标识inode的inode号,每个inode都不一样
用来指明这个文件对应着哪些磁盘块的信息——即逻辑块号和磁盘块号的对应关系

VFS的运行方式:

深入理解linux内核的第12章部分详细描述了VFS的执行方式
简单来说:
文件打开之后有一个file结构体,里面有个file_op,这个结构体每个文件系统都不一样,里面有read指令
用户使用read指令,然后内核开始调用sys_read,sys_read通过f_op中的数据结构,调用文件系统特定的read指令。
VFS要处理很多系统调用
image.png
这上面只是一部分,

从文件名到磁盘块:

extent:(inode的一部分?)

而ext4采用的办法是使用extent来保存<文件逻辑块号, 磁盘块号>的映射关系:一个extent对应一系列连续的块号,故可以想到,一个extent最基本的几个域有——文件逻辑块号,起始磁盘块号,块数量 ext4中一个inode可以直接存放4个extent 对于很大的文件,ext4采用extent_tree的方式,其本质同样是一种间接寻址的关系

inode存放文件元数据,但是并没有存放filename——那么ext4是如何把一个filename和一个inode绑定在一起呢?
也就是说存在一个filename和inode号之间的对应关系,而这个关系也是存放在一个文件里——目录文件。如根目录/就是一个文件,这个文件也对应一个inode,文件的数据就是根目录下的文件名和对应的inode号(dentry?不是dentry,dentry存在于内存中,是目录文件的缓存,这个数据结构是内存数据结构,用来存放一个文件路径(如/home)和其对应的数据信息(文件名和inode号的表))
VFS的四种数据结构,超级块,索引节点对象,目录项对象,文件对象FILE,都是内存数据结构,对于具体的文件系统比如Ext4,在挂载的时候根据其磁盘布局来构造出这四种数据结构(如何构造?这个应该是移植的重点)应用程序(如cat命令)均基于VFS提供的文件操作接口进行编程,每种具体的文件系统的实现,如ext4、fat32、ntfs等,需要对其特有的磁盘数据结构进行封装来实现VFS的这四种数据结构(**移植的时候,文件系统需要完成VFS的四种数据结构的封装,这个封装过程是不是使用VFS提供的API进行的?**)
ext2中的inode结构体:ext2_inode

  • 最重要的字段:i_block[EXT2_N_BLOCKS],存放磁盘块号
  • ext2_inode的大小为128字节,一个4KB的块可以存放32个inode

    1. struct ext2_inode{
    2. __le16 i_mode; //文件类型和访问权限
    3. __le16 i_uid; //拥有者id
    4. __le32 i_size; //文件大小,单位byte
    5. __le32 i_atime,i_ctime,i_mtime,i_dtime; //时间相关信息
    6. __le16 i_gid; //用户组id
    7. __le16 i_links_count; //硬链接计数器
    8. __le32 i_blocks; //文件数据块数
    9. __le32 i_flags; //标志
    10. union osd1; //操作系统相关信息
    11. __le32 i_block[EXT2_N_BLOCKS]; //指向数据块的指针,即磁盘块号
    12. __le32 i_generation; //文件版本,用于网络文件系统
    13. __le32 i_file_acl; //文件访问控制列表
    14. __le32 i_dir_acl; //目录访问控制列表
    15. __le32 i_faddr; //片地址(不懂)
    16. union osd2; //操作系统相关信息
    17. }

    i_block字段是一个有EXT2_N_BLOCKS个元素的数组,若EXT2_N_BLOCKS的默认值为15
    [0,11]为直接寻址,即i_block[0,11]直接存放磁盘块号
    [12,13,14]均为间接寻址,[12]指向一个存放磁盘块号的块,[13]为二级指针,[14]为三级指针
    若一个block的大小为4KB,直接寻址的范围为48KB,即只使用[0,11];若加上一次间接的[12],则为4.04MB;再加上二次间接的[13]则为4GB;再加上三次间接的[13]则约为4TB
    (和多级页表一模一样)、
    Ext4中的inode:
    ext4中就出现了extent,ext4_inode 定义于/fs/ext4/ext4.hext4_inode的大小为256字节,一个4KB的块可以保存16个inode,i_block字段和ext2中的一样
    字段i_block的大小为60个字节,即__le32 i_block[EXT4_N_BLOCKS]且EXT4_N_BLOCKS=15
    前12个字节为extent头,为extent的基本信息
    后48个字节可以保存4个extent节点,每个extent节点为12字节大小
    extent以树的形式组织,叶节点和非页节点的大小均12字节
    叶节点即直接保存了文件逻辑块号、起始磁盘块号、块数
    非叶节点同样具有文件逻辑块号,后面内容指向了一个磁盘块号,有两个字节未使用
    extent相关结构体定义于/fs/ext4/ext4_extents.h

磁盘布局:
引导块:磁盘的第一个块,ext2对其不管理,其余的块以块组的形式管理
image.png
每个块组有以下内容:

  • 超级块:1个块
  • 组描述符:n个块
  • 数据块bitmap:1个块
  • 索引节点bitmap:1个块
  • 索引节点表:n个块
  • 数据块:n个块

其中超级块和组描述符,对于所有块组均相同,且总是缓存在内存中
其余则用于描述本块组管理的inode块和数据块
Ext4中的磁盘数据结构:
超级块的源码:ext4_sb.h
组描述符ext4_group_desc

  1. /*
  2. * Structure of a blocks group descriptor
  3. */
  4. struct ext4_group_desc
  5. {
  6. __le32 bg_block_bitmap_lo; /* Blocks bitmap block */
  7. __le32 bg_inode_bitmap_lo; /* Inodes bitmap block */
  8. __le32 bg_inode_table_lo; /* Inodes table block */
  9. __le16 bg_free_blocks_count; /* Free blocks count */
  10. __le16 bg_free_inodes_count; /* Free inodes count */
  11. __le16 bg_used_dirs_count; /* Directories count */
  12. __le16 bg_flags; /* EXT4_BG_flags (INODE_UNINIT, etc) */
  13. __u32 bg_reserved[2]; /* Likely block/inode bitmap checksum */
  14. __le16 bg_itable_unused; /* Unused inodes count */
  15. __le16 bg_checksum; /* crc16(sb_uuid+group+desc) */
  16. __le32 bg_block_bitmap_hi; /* Blocks bitmap block MSB */
  17. __le32 bg_inode_bitmap_hi; /* Inodes bitmap block MSB */
  18. __le32 bg_inode_table_hi; /* Inodes table block MSB */
  19. __le16 bg_free_blocks_count_hi;/* Free blocks count MSB */
  20. __le16 bg_free_inodes_count_hi;/* Free inodes count MSB */
  21. __le16 bg_used_dirs_count_hi; /* Directories count MSB */
  22. __le16 bg_itable_unused_hi; /* Unused inodes count MSB */
  23. __u32 bg_reserved2[3];
  24. };

直接通过inode号找文件
找文件的inode号:

使用ls -i可以打印出相应的inode号

根据inode号打印出inode内容:
使用df命令找出文件所在的设备,找出当前路经所在的设备(eg:/dev/sda3,然后再使用ls -li xxxx命令
istat命令可以打印出某个设备上的某个inode信息

Group:320表示这个inode存在于块组320上 size:38表示文件的大小为38个字节Direct Blocks:10622354则是这个命令根据i_block解析出来的磁盘快号> 那么我们不直接使用这个块号,我们自己来算一算它,这就需要直接查看inode所在的磁盘数据块才能看到完整的信息 已知a.txt的文件inode为2629310,使用以上命令,得到下面的信息 ```bash $ sudo istat /dev/sda3 2629310 inode: 2629310 Allocated Group: 320//知道块组320 Generation Id: 2014416585 uid / gid: 1000 / 1000 mode: rrw-rw-r— Flags: Extents, size: 38//38个字节 num of links: 1

Inode Times: Accessed: 2017-06-26 21:13:12.184134514 (CST) File Modified: 2017-06-26 21:13:10.628122013 (CST) Inode Modified: 2017-06-26 21:13:10.628122013 (CST) File Created: 2017-06-26 10:18:07.690160310 (CST)

Direct Blocks://这个命令根据i_block解析出来的磁盘块号 //(如果不直接使用这个块号,自己来算一算它,这就需要直接查看inode所在的磁盘数据块才能看到完整的信息) 10622354

  1. 接下来fsstat列出文件系统的信息,找出320块组的信息
  2. ```bash
  3. $ sudo fsstat /dev/sda3 > dev.sda3.fsstat
  4. $ cat dev.sda3.fsstat | grep "Group: 320" -n
  5. 4560:Group: 320:
  6. $ cat dev.sda3.fsstat | head -n 4572 | tail -n 13
  7. Group: 320:
  8. Block Group Flags: [INODE_ZEROED]
  9. Inode Range: 2621441 - 2629632
  10. Block Range: 10485760 - 10518527
  11. Layout:
  12. Data bitmap: 10485760 - 10485760
  13. Inode bitmap: 10485776 - 10485776
  14. Inode Table: 10485792 - 10486303
  15. Data Blocks: 10493984 - 10518527
  16. Free Inodes: 8 (0%)
  17. Free Blocks: 23488 (71%)
  18. Total Directories: 534
  19. Stored Checksum: 0x03D2

磁盘块里面存储的内容:
blkcat命令可以读取一个磁盘块的内容

  1. $ sudo blkcat /dev/sda3 10486283 > dev.sda3.blk.10486283

使用十六进制编辑器hexedit打开,看到二进制的文件,然后根据block的布局,找出里面的信息
ext4 layout网站:
https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout

自己实现一个操作系统的步骤是什么:

https://www.cnblogs.com/wangzahngjun/p/5365310.html(基于VFS实现自己的文件系统)
这部分的步骤,主要是和内核交互,这部分应该是移植的重点,找出安装文件系统中需要和vfs打交道的部分,然后实现(深入理解linux内核里面第12章描述了文件系统如何安装)
首先要做的就是生产自己的super_block,即要重载内核的get_sb()函数,这个函数是在mount的时候调用的
一个具体的文件系统必须先向vfs注册,才能被使用。通过register_filesystem() ,可以将一个“文件系统类型”结构 file_system_type注册到内核中一个全局的链表file_systems 上。
文件系统注册的主要目的,就是让 VFS 创建该文件系统的“超级块”结构。
一个文件系统在内核中用struct file_system_type来表示:

  1. struct file_system_type {
  2. const char *name;
  3. int fs_flags;
  4. int (*get_sb) (struct file_system_type *, int,
  5. const char *, void *, struct vfsmount *);
  6. void (*kill_sb) (struct super_block *);
  7. struct module *owner;
  8. struct file_system_type * next;
  9. struct list_head fs_supers; /*超级块对象链表*/
  10. struct lock_class_key s_lock_key;
  11. struct lock_class_key s_umount_key;
  12. struct lock_class_key i_lock_key;
  13. struct lock_class_key i_mutex_key;
  14. struct lock_class_key i_mutex_dir_key;
  15. struct lock_class_key i_alloc_sem_key;
  16. };

这个结构中最关键的就是 get_sb() 这个函数指针,它就是用于创建并设置 super_block 的目的的。
因为安装一个文件系统的关键一步就是要为“被安装设备”创建和设置一个 super_block,而不同的具体的文件系统的 super_block 有自己特定的信息,因此要求具体的文件系统首先向内核注册,并提供 read_super() 的实现。

安装文件系统:
一个注册了的文件系统必须经过安装才能被VFS所接受。安装一个文件系统,必须指定一个目录作为安装点。一个设备可以同时被安装到多个目录上。 一个目录节点下可以同时安装多个设备。
“根安装点”、“根设备”和“根文件系统”
安装一个文件系统,除了需要“被安装设备”外,还要指定一个“安装点”。“安装点”是已经存在的一个目录节点。例如把 /dev/sda1 安装到 /mnt/win 下,那么 /mnt/win 就是“安装点”。 可是文件系统要先安装后使用。因此,要使用 /mnt/win 这个“安装点”,必然要求它所在文件系统已也经被安装。 也就是说,安装一个文件系统,需要另外一个文件系统已经被安装。

“安装”一个文件系统涉及“被安装设备”和“安装点”两个部分,安装的过程就是把“安装点”和“被安装设备”关联起来,这是通过一个“安装连接件”结构 vfsmount 来完成的。
vfsmount 将“安装点”dentry 和“被安装设备”的根目录节点 dentry 关联起来。
在安装文件系统时,内核的主要工作就是:(在移植工作中,由rtems完成)
1、 创建一个 vfsmount
2、 为“被安装设备”创建一个 super_block,并由具体的文件系统来设置这个 super_block。
3、 为被安装设备的根目录节点创建 dentry
4、 为被安装设备的根目录节点创建 inode, 并由 super_operations->read_inode() 来设置此 inode
5、 将 super_block 与“被安装设备“根目录节点 dentry 关联起来
6、 将 vfsmount 与“被安装设备”的根目录节点 dentry 关联起来
这个步骤由内核中的代码get_sb_single 完成

  1. int get_sb_single(struct file_system_type *fs_type,
  2. int flags, void *data,
  3. int (*fill_super)(struct super_block *, void *, int),
  4. struct vfsmount *mnt)
  5. {
  6. struct super_block *s;
  7. int error;
  8. s = sget(fs_type, compare_single, set_anon_super, NULL);
  9. if (IS_ERR(s))
  10. return PTR_ERR(s);
  11. if (!s->s_root) {
  12. s->s_flags = flags;
  13. error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
  14. if (error) {
  15. deactivate_locked_super(s);
  16. return error;
  17. }
  18. s->s_flags |= MS_ACTIVE;
  19. } else {
  20. do_remount_sb(s, flags, data, 0);
  21. }
  22. simple_set_mnt(mnt, s);
  23. return 0;
  24. }

打开文件的过程实际上就是将file,dentry,inode关联起来的过程,每打开一个文件,就创建一个file结构
image.png
具体实现的文件系统里面的函数结构:

  1. static struct inode *wzjfs_make_inode(struct super_block *sb, int mode)
  2. static int wzjfs_open(struct inode *inode, struct file *filp)
  3. static ssize_t wzjfs_read_file(struct file *filp, char *buf,size_t count, loff_t *offset)
  4. static ssize_t wzjfs_write_file(struct file *filp, const char *buf,size_t count, loff_t *offset)
  5. static struct file_operations wzjfs_file_ops= {
  6. .open = wzjfs_open,
  7. .read = wzjfs_read_file,
  8. .write = wzjfs_write_file,
  9. };
  10. static struct dentry *wzjfs_create_file (struct super_block *sb,struct dentry *dir, const char *name,atomic_t *counter)
  11. static struct dentry *wzjfs_create_dir (struct super_block *sb,struct dentry *parent, const char *name)
  12. static atomic_t counter, subcounter;
  13. static void wzjfs_create_files (struct super_block *sb, struct dentry *root)
  14. static struct super_operations wzjfs_s_ops
  15. static int wzjfs_fill_super (struct super_block *sb, void *data, int silent)
  16. static int wzjfs_get_super
  17. static struct file_system_type wzjfs_type = {
  18. .owner = THIS_MODULE,
  19. .name = "wzjfs",
  20. .get_sb = wzjfs_get_super,
  21. .kill_sb = kill_litter_super,
  22. };
  23. static int __init wzjfs_init(void)
  24. {
  25. struct file_system_type * tmp;
  26. printk("wzjfs_init ok\n");
  27. return register_filesystem(&wzjfs_type);
  28. }
  29. static void __exit wzjfs_exit(void)
  30. {
  31. unregister_filesystem(&wzjfs_type);
  32. printk("wzjfs_exit ok\n");
  33. }

VFS与实际文件系统的交互

通过struct找到磁盘inode节点对象:
一个进程打开的文件用struct file结构表示,这是VFS可访问的(file中的file_operations)。在file结构中可找到这个文件对应的dentry对象,如果两个进程打开同一个文件,那它们的file就指向同一个dentry。通过dentry就可以找到这个文件对应的inode对象,到了inode就与特定文件系统(如ext3/ext4)相关了,这个inode有读写文件的file_operations。那么,如果一个dentry是另一个dentry的硬链接,那这两个dentry就指向同一个inode对象。

VFS中的通用read/write调用实际文件系统的read/write:
在程序open()一个文件时,inode的file_operations就会被填到供VFS使用的file结构的file_operations,这样实际文件和VFS就建立了联系,就可以开始实际操作这个文件了。

通过address_space接触磁盘:
在open文件之后进行read/write时,读写操作(file_operations)并不是直接跟硬盘交互,而是会经过address_space。每个inode有自己的一个address_space(inode的i_mapping字段),address_space中的address_space_operations(如readpage/writepage/readpages/writepages等)才会跟磁盘打交道进行读写。

虚拟文件系统与Ext4文件系统

文件系统在内核中的读写过程
文件系统在内核中的读写过程是在 sys_write( ) 中定义的,其定义在 include/linux/syscalls.h 中:
asmlinkage long sys_write(unsigned int fd, const char __user *buf, 568 size_t count);
sys_write( )的具体实现在 fs/read_write.c 中:如下所示

  1. SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
  2. size_t, count)
  3. {
  4. struct fd f = fdget_pos(fd);
  5. ssize_t ret = -EBADF;
  6. if (f.file) {
  7. loff_t pos = file_pos_read(f.file);
  8. ret = vfs_write(f.file, buf, count, &pos);
  9. if (ret >= 0)
  10. file_pos_write(f.file, pos);
  11. fdput_pos(f);
  12. }
  13. return ret;
  14. }

sys_write过程分析:
1、根据打开文件号 fd找到该已打开文件file结构 struct fd f = fdget_pos(fd);<br />2、读取当前文件的读写位置 loff_t pos = file_pos_read(f.file);
3、写入(最重要的一部分 ) ret = vfs_write(f.file, buf, count, &pos);<br />4、根据读文件结果,更新文件读写位置 file_pos_write(f.file, pos);
sys_write( ) 的核心部分 vfs_write( )

  1. ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos){
  2. ssize_t ret;
  3. if (!(file->f_mode & FMODE_WRITE))
  4. return -EBADF;
  5. if (!(file->f_mode & FMODE_CAN_WRITE))
  6. return -EINVAL;
  7. if (unlikely(!access_ok(VERIFY_READ, buf, count)))
  8. return -EFAULT;
  9. ret = rw_verify_area(WRITE, file, pos, count);//合法性检查
  10. if (ret >= 0) {
  11. count = ret;
  12. file_start_write(file);
  13. if (file->f_op->write)
  14. ret = file->f_op->write(file, buf, count, pos);
  15. else if (file->f_op->aio_write)
  16. ret = do_sync_write(file, buf, count, pos);
  17. else
  18. ret = new_sync_write(file, buf, count, pos);
  19. if (ret > 0) {
  20. fsnotify_modify(file);
  21. add_wchar(current, ret);
  22. }
  23. inc_syscw(current);
  24. file_end_write(file);
  25. }
  26. return ret;
  27. }

首先函数在 rw_verify_area(WRITE, file, pos, count); 检查文件是否从当前位置 pos 开始的 count 字节是否对写操作加上了 “强制锁”,这是通过调用函数完成的。

通过合法性检查后,就调用具体文件系统 file_operations中 write 的方法。对于ext4文件系统,file_operations方法定义在 fs/ext4/file.c 中。从定义中可知 write 方法实现函数为 do_sync_write( )
ext4中file_operation的定义:

  1. const struct file_operations ext4_file_operations = {
  2. .llseek = generic_file_llseek,
  3. .read = do_sync_read,
  4. .write = do_sync_write,//这部分的代码在下面
  5. .aio_read = generic_file_aio_read,
  6. .aio_write = ext4_file_write,
  7. .unlocked_ioctl = ext4_ioctl,
  8. #ifdef CONFIG_COMPAT
  9. .compat_ioctl = ext4_compat_ioctl,
  10. #endif
  11. .mmap = ext4_file_mmap,
  12. .open = ext4_file_open,
  13. .release = ext4_release_file,
  14. .fsync = ext4_sync_file,
  15. .splice_read = generic_file_splice_read,
  16. .splice_write = generic_file_splice_write,
  17. };
  18. const struct inode_operations ext4_file_inode_operations = {
  19. .truncate = ext4_truncate,
  20. .setattr = ext4_setattr,
  21. .getattr = ext4_getattr,
  22. #ifdef CONFIG_EXT4_FS_XATTR
  23. .setxattr = generic_setxattr,
  24. .getxattr = generic_getxattr,
  25. .listxattr = ext4_listxattr,
  26. .removexattr = generic_removexattr,
  27. #endif
  28. .check_acl = ext4_check_acl,
  29. .fallocate = ext4_fallocate,
  30. .fiemap = ext4_fiemap,
  31. };

但是具体的实现do_sync_write( )的代码在fs/read_write.c里面,这个是个通用的文件,不是ext4特定的,所有文件系统的公用目录里面存在,因此这部分在rtems里面是否已经有实现的方式,在哪?

下面是do_sync_write( )的具体代码,也在fs/read_write.c中:

  1. ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
  2. {
  3. struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
  4. struct kiocb kiocb;
  5. ssize_t ret;
  6. init_sync_kiocb(&kiocb, filp);
  7. kiocb.ki_pos = *ppos;
  8. kiocb.ki_nbytes = len;
  9. ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
  10. if (-EIOCBQUEUED == ret)
  11. ret = wait_on_sync_kiocb(&kiocb);
  12. *ppos = kiocb.ki_pos;
  13. return ret;
  14. }
  15. EXPORT_SYMBOL(do_sync_write);

异步I/O允许用户空间来初始化操作而不必等待它们的完成,因此,一个应用程序可以在他的I/O处理进行中做其他的处理。

块和网络驱动在整个时间是完全异步的,因此只有字符驱动对于明确的异步I/O支持是候选的。实现异步I/O操作的file_operations方法,都使用I/O Control Block,其定义在 include/linux/aio.h中

定义了一个临时变量iov,这个变量记录了用户空间缓冲区地址buf和所要写的字节数len,用户空间的缓冲区地址buf是保存在iov中的。初始化异步I/O数据结构后,就用file_operations 中的aio_write方法。拓展到ext4文件中的时,该方法就是ext4_file_operations结构体中的ext4_file_write( )。

下面就具体到ext4的文件系统,这个函数也是aio_write( ) 的延展。

下面两个链接,讲linux里面文件系统结构的部分比较详细,各个结构之间的关系,在linux源码中的位置,以及读写过程在整个系统中的运行过程都很详细的介绍了
https://www.cnblogs.com/hanyan225/archive/2011/07/29/2120658.html
https://www.cnblogs.com/hanyan225/archive/2011/07/29/2121192.html