文件读写过程

 : jank    :   : 859    : 2021-12-26 11:49  操作系统和网络

 文件读写过程

从用户程序读取磁盘文件整个链路是一个非常复杂的过程总体可以梳理为一个层次模型如下图所示

 

  image.png

 

1 用户层 IO引擎

主要是在应用程序中调用内核提供的系统调用函数

2 系统调用

2.1同步引擎

 

1sync

#include<unistd.h>
void sync(void)

返回值:若成功则返回0,若出错则返回-1,同时设置errno以指明错误.
函数说明:
    sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,他并不等待实际I/O操作结束.所以不要认为调用了sync函数,就觉得数据已安全的送到磁盘文件上,有可能会出现问题,但是sync函数是无法得知的.
    系统守候进程一般每隔一段时间调用一次sync函数,确保定期刷新内核的块缓存.UNIX系统中,系统守候进程update会周期性地(一般每个30秒)调用sync函数.命令sync(1)也调用sync函数.

 

 

2fsync

#include <unistd.h>

  int fsync(int fd);

sync函数不同,fsync函数只对由文件描符filedes指定的单一文件起作用,强制与描述字fildes相连文件的所有修改过的数据(包括核内I/O缓冲区中的数据)传送到外部永久介质,即刷新fildes给出的文件的所有信息,并且等待写磁盘操作结束,然后返回.调用 fsync()的进程将阻塞直到设备报告传送已经完成.这个fsync就安全点了.

fsync 对指定的文件句柄上所有变动的内容同步并阻塞等待IO落盘结束

fsync 通常至少有两次io写操作1修改文件内容 2修改文件描述信息metadata

优点可靠性高

     缺点效率低一次IO 平均要10ms所以多一次IO浪费的性能是巨大的

一个程序在写出数据之后,如果继续进行后续处理之前要求确保所写数据已写到磁盘,则应当调用fsync().例如,数据库应用通常会在调用write()保存关键交易数据的同时也调用fsync().这样更能保证数据的安全可靠.

fflush()fsync()的联系:

    内核I/O缓冲区是由操作系统管理的空间,而流缓冲区是由标准I/O库管理的用户空间.fflush()只刷新位于用户空间中的流缓冲区.fflush()返回后,只保证数据已不在流缓冲区中,并不保证它们一定被写到了磁盘.此时,从流缓冲区刷新的数据可能已被写至磁盘,也可能还待在内核I/O缓冲区中.要确保流I/O写出的数据已写至磁盘,那么在调用fflush()后还应当调用fsync().

 

3fdatasync

#include <unistd.h>

int fdatasync(int fd);

fdatasync 功能和fsync类似但是fdatasync 并不时刻都同步metadata信息,只有在特殊时刻才同步如size变化程序崩溃时没同步照样读不了文件内容),而访问时间(atime)/修改时间(mtime)是不需要每次都同步的

      通常像日志文件这种追加写的操作size肯定是一直再增加的所以这种情况metadata是需要每次都同步的但该如何发挥fdatasync的特性

      Berkeley DB这样处理日志文件的:

1.每个log文件固定为10MB大小,从1开始编号,名称格式为“log.%010d"

2.每次log文件创建时,先写文件的最后1page,将log文件扩展为10MB大小

3.log文件中追加记录时,由于文件的尺寸不发生变化,使用fdatasync可以大大优化写log的效率

4.如果一个log文件写满了,则新建一个log文件,也只有一次同步metadata的开销。

 

2.2异步引擎

1)libaio

linux AIO 则是 linux 内核原声支持的异步 IO 调用

libaio提供下面五个主要API函数
int io_setup(int maxevents, io_context_t *ctxp); //会创建一个所谓的"AIO上下文"(aio_context,后文也叫‘AIO context’)结构体到在内核中。aio_context是用以内核实现异步AIO的数据结构。它其实是一个无符号整形,位于头文件 /usr/include/linux/aio_abi.h

int io_destroy(io_context_t ctx); //io_destroy的作用是销毁这个上下文aio_context_t

int io_submit(io_context_t ctx, long nr, struct iocb *ios[]);//提交异步IO请求

int io_cancel(io_context_t ctx, struct iocb *iocb, struct io_event *evt);//取消异步io

int io_getevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout);//等待并获取异步IO请求的事件(也就是异步请求的处理结果)。对于每一个已经完成的IO请求(成功或失败),内核都会创建一个io_event结构。io_getevent()系统调用可以用来获取这一结构。

 

 

2)posixaio

 

POSIX AIO 是在用户控件模拟异步 IO 的功能,不需要内核支持

 

2.3磁盘映射

1mmap

       BIO(常规文件操作或传统IO)和mmap区别:

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

 

mmap优缺点

优点:(高性能,操作文件就像操作内存一下,适合对较大文件的读写)
对文件的读写操作跨国也页缓存,减少数据的拷贝次数,用内存读写取代IO流读写,提高了文件读写效率(Andorid加载.dex文件也通过使用此技术);
实现用户空间和内核空间的高效交互方式;
提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助磁盘空间协助操作,补充内存的不足。但是进一步会照成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好解决,需要用磁盘空间替代内存的时候,mmap都可以发挥其功效;

缺点:文件如果很小,比如小于4K的,比如60bytes,由于在内存当中的组织都是按页组织的,将文件调入到内存当中是一个页4K,相当于4096-60=4036bytes的内存空间浪费掉了;文件无法完成拓展,因为mmap到内存的时候,你所能操作的范围就已经确定了,无法增加文件长度。
使用场景:
对同一块区域频繁读写操作;
用户日志、数据上报等,微信开源mars框架中的xlog模块就是基于mmap特性实现;
跨进程同步的时候,mmap是个不错的选择,Android跨进程通信有自己独有的Binder机制,内部使用mmap实现;
Java层面使用:MappedByteBuffer已经封装好
C++代码实现:mmap

 

内存映射原理

进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

 

mmap相关函数

建立映射关系函数:
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
成功执行时,mmap()返回被映射区的指针地址。失败时,mmap()返回MAP_FAILED[其值为(void *)-1]
解除映射关系函数:
int munmap( void * addr, size_t len )
成功执行时,munmap()返回0。失败时,munmap返回-1error返回标志和mmap一致;
addr是调用mmap()时返回的地址,len是映射区的大小;
实时同步写入:
int msync( void *addr, size_t len, int flags )
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。
可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

mmap使用细节

①mmap映射区域大小必须是物理页大小(page_size)的倍数(32位系统中通常是4k字节),原因是内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位;
内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法的访问在当前文件大小以内又在内存映射区以内的那些字节。
映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。



 

3. VFS

VFSVirtual File System)虚拟文件系统是一种软件机制,更确切的说扮演着文件系统管理者的角色,VFS 是著名的类 Unix 系统中 “一切皆文件” 概念的基础大部分内核导出用户程序使用的函数都可以通过VFS定义的文件接口访问

 

VFS作用屏蔽下层具体文件系统操作的差异,为上层的操作提供一个统一的接口,对于应用程序来说,其访问的接口是完全一致的(例如openreadwrite等)。正是因为有了这个层次,Linux中允许众多不同的文件系统共存并且对文件的操作可以跨文件系统而执行。

 

LinuxVFS依靠四个主要的数据结构来描述其结构信息,分别为超级块Super Block、索引结点Inode、目录项Dentry和文件对象File

源码目录/usr/src/kernels/3.10.0-1127.19.1.el7.x86_64/
super block:超级块对象表示一个文件系统。它存储一个已安装的文件系统的控制信息,包括文件系统名称(比如Ext2)、文件系统的大小和状态、块设备的引用和元数据信息(比如空闲列表等等)。超级块是这些文件系统中最重要的数据结构,它 是来描述整个文件系统信息的,可以说是一个全局的数据结构。VFS超级块存在于内存中,它在文件系统安装时建立,并且在文件系统卸载时自动删除。同时需要注意的是对于每个具体的文件系统来说,也有各自的超级块,它们存放于磁盘。(源码.../include/linux/fs.h

很多具体文件系统中都有超级块结构,超级块是这些文件系统中最重要的数据结构,它 是来描述整个文件系统信息的,可以说是一个全局的数据结构。MinixExt2 等有超级块, VFS 也有超级块,为了避免与后面介绍的 Ext2 超级块发生混淆,这里用 VFS 超级块来表示。 VFS 超级块是各种具体文件系统在安装时建立的,并在这些文件系统卸载时自动删除,可见, VFS 超级块确实只存在于内存中,同时提到 VFS 超级块也应该说成是哪个具体文件系统的 VFS 超级块。

 

所有超级块对象(每个已安装的文件系统都有一个超级块)以双向环形链表的形式链 接在一起。链表中第一个元素和最后一个元素的地址分别存放在 super_blocks 变量的 s_list 域的 next prev 域中。

s_list 域的数据类型为 struct list_head,在超级块 的 s_dirty 域以及内核的其他很多地方都可以找到这样的数据类型;这种数据类型仅仅包 括指向链表中的前一个元素和后一个元素的指针。因此,超级块对象的 s_list 域包含指向链表中两个相邻超级块对象的指针。

 

 

     超级块方法

struct super_operations {
        //该函数在给定的超级块下创建并初始化一个新的索引节点对象
    struct inode *(*alloc_inode)(struct super_block *sb);
        //释放指定的索引结点 
 void (*destroy_inode)(struct inode *);
        //VFS在索引节点被修改时会调用此函数。
    void (*dirty_inode) (struct inode *, int flags);
        // 将指定的inode写回磁盘。
 int (*write_inode) (struct inode *, struct writeback_control *wbc);
        //删除索引节点。
 int (*drop_inode) (struct inode *);
        
 void (*evict_inode) (struct inode *);
        //用来释放超级块
 void (*put_super) (struct super_block *);
        //使文件系统的数据元素与磁盘上的文件系统同步,wait参数指定操作是否同步。
 int (*sync_fs)(struct super_block *sb, int wait);
 int (*freeze_fs) (struct super_block *);
 int (*unfreeze_fs) (struct super_block *);
        //获取文件系统状态。把文件系统相关的统计信息放在statfs
 int (*statfs) (struct dentry *, struct kstatfs *);
 int (*remount_fs) (struct super_block *, int *, char *);
 void (*umount_begin) (struct super_block *);
 
 int (*show_options)(struct seq_file *, struct dentry *);
 int (*show_devname)(struct seq_file *, struct dentry *);
 int (*show_path)(struct seq_file *, struct dentry *);
 int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
 ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
 ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
#endif
 int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
 long (*nr_cached_objects)(struct super_block *, int);
 long (*free_cached_objects)(struct super_block *, long, int);
};

 

 

Inodeinode的成员主要分为两类

1)描述文件的元数据例如:文件大小、设备标识符、用户标识符、用户组标识符等等。

2)保存实际文件内容的数据段或指向数据的指针

Inode分为两种:一种是VFSInode,一种是具体文件系统的Inode。前者在内存中,后者在磁盘中。所以每次其实是将磁盘中的Inode调进填充内存中的Inode,这样才是算使用了磁盘文件Inode。当创建一个文件的时候,就给文件分配了一个Inode。一个Inode只对应一个实际文件,一个文件也会只有一个Inode(源码.../include/linux/fs.h

每个文件都有一个 inode,每个 inode 有一个索引节点号 i_ino。在同一个文件系统 中,每个索引节点号都是唯一的,内核有时根据索引节点号的哈希值查找其 inode 结构。

 

 

 

 

 

Dentry:引入目录项对象的概念主要是出于方便查找文件的目的。不同于前面的两个对象,目录项对象没有对应的磁盘数据结构,只存在于内存中。

一个有效的 dentry 结构必定有一个 inode 结构,这是因为一个目录项要么代表着一个 文件,要么代表着一个目录,而目录实际上也是文件一个 inode 却可能对应着 不止一个 dentry 结构;也就是说,一个文件可以有不止一个文件名或路径名。这是因为一个 已经建立的文件可以被连接(link)到其他文件名。

一个路径的各个组成部分,不管是目录还是普通的文件,都是一个目录项对象。如,在路径/home/source/test.java中,目录 /, home, source和文件 test.java都对应一个目录项对象。VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的Inode,那么沿着目录项进行操作就可以找到最终的文件。(源码.../include/linux/dcache.h

 

 

 

 

File:文件对象描述的是进程已经打开的文件。因为一个文件可以被多个进程打开,所以一个文件可以存在多个文件对象。一个文件对应的文件对象可能不是唯一的,但是其对应的索引节点和目录项对象肯定是惟一的。(源码.../include/linux/fs.h

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

 

 

struct file {

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

struct dentry *f_dentry; /*指向相关目录项的指针*/

 struct vfsmount *f_vfsmnt; /*指向 VFS 安装点的指针*/

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

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

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

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

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

 unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; /*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及 预读的页面数*/

int f_owner; /* 通过信号进行异步 I/O 数据的传送*/

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

int f_error; /*网络写操作的错误码*/

 unsigned long f_version; /*版本号*/

 void *private_data; /* tty 驱动程序所需 */

 };

};

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

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

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

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

 

用户打开文件表

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

struct files_struct {

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

rwlock_t file_lock; /* 保护以下的所有域,以免在 tsk->alloc_lock 中的嵌套*/

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

int max_fdset; /*当前文件描述符的最大数*/

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

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

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

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

fd_set close_on_exec_init;/* 执行 exec( )时需要关闭的文件描述符的初 值 集合*/

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

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

};

fd 域指向文件对象的指针数组。该数组的长度存放在 max_fds 域中。通常,fd 域指向 files_struct 结构的 fd_array 域,该域包括 32 个文件对象指针。如果进程打开的文件数目 多于 32,内核就分配一个新的、更大的文件指针数组,并将其地址存放在 fd 域中;内核同 时也更新 max_fds 域的值。

对于在 fd 数组中有入口地址的每个文件来说,数组的索引就是文件描述符(file descriptor)。通常,数组的第 1 个元素(索引为 0)是进程的标准输入文件,数组的第 2 元素(索引为 1)是进程的标准输出文件,数组的第 3 个元素(索引为 2)是进程的标准错误 文件。请注意,借助于 dup()、dup2()和 fcntl() 系统调用,两个文件描述符 就可以指向同一个打开的文件,也就是说,数组的两个元素可能指向同一个文件对象。当用 户使用 shell 结构(如 2>&1)将标准错误文件重定向到标准输出文件上时,用户总能看到这 一点。

 open_fds 域包含 open_fds_init 域的地址,open_fds_init 域表示当前已打开文件的文 件描述符的位图。max_fdset 域存放位图中的位数。由于数据结构 fd_set 有 1024 位,通常 不需要扩大位图的大小。不过,如果确实需要,内核仍能动态增加位图的大小,这非常类似 文件对象的数组的情形。 当开始使用一个文件对象时调用内核提供的 fget()函数。这个函数接收文件描述符 fd 作为参数,返回在 current->files->fd[fd]中的地址,即对应文件对象的地址,如果没有任 何文件与 fd 对应,则返回 NULL。在第 1 种情况下,fget()使文件对象引用计数器 f_count 的值增 1。

当内核完成对文件对象的使用时,调用内核提供的 fput() 函数。该函数将文件对象的 地址作为参数,并递减文件对象引用计数器 f_count 的值,另外,如果这个域变为 NULL,该 函数就调用文件操作的“释放”方法(如果已定义),释放相应的目录项对象,并递减对应索 引节点对象的 i_writeaccess 域的值(如果该文件是写打开),最后,将该文件对象从“正在 使用”链表移到“未使用”链表。

 

文件系统信息的 fs_struct 结构

struct fs_struct {

int users;

spinlock_t lock;

seqcount_t seq;

int umask;

int in_exec;

struct path root, pwd;

};

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

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

 

 

 

 

 

主要数据结构间的关系

前面我们介绍了超级块对象、索引节点对象、文件对象及目录项对象的数据结构。

我们在此给出这些数据结构之间的联系。

超级块是对一个文件系统的描述;索引节点是对一个文件物理属性的描述;而目录项是

对一个文件逻辑属性的描述。除此之外,文件与进程之间的关系是由另外的数据结构来描述

的。一个进程所处的位置是由 fs_struct 来描述的,而一个进程(或用户)打开的文件是由

files_struct 来描述的,而整个系统所打开的文件是由 file 结构来描述。如图 8.4 给出了

这些数据结构之间的关系。

 

 

文件系统挂载:挂载是用户态发起的命令,就是我们知道的mount命令,该命令执行的时候需要指定文件系统的类型(本文假设Ext2)和文件系统数据的位置(也就是设备)。通过这些关键信息,VFS就可以完成Ext2文件系统的初始化,并将其关联到当前已经存在的文件系统中,也就是建立其图2所示的文件系统树。

当内核被编译时,就已经确定了可以支持哪些文件系统,这些文件系统在系统引导时, VFS 中进行注册。如果文件系统是作为内核可装载的模块,则在实际安装时进行注册,并 在模块卸载时注销。每个文件系统都有一个初始化例程,它的作用就是在 VFS 中进行注册, 即填写一个叫做 file_system_type (../include/linux/fs.h) 的数据结构,该结构包含了文件系统的名称以及一个指 向对应的 VFS 超级块读取例程的地址,所有已注册的文件系统的 file_system_type 结构形 成一个链表,为区别后面将要说到的已安装的文件系统形成的另一个链表,我们把这个链表 称为注册链表。图所示就是内核中的 file_system_type 链表,链表头由 file_systems 变量指定。

 

 

• name:文件系统的类型名,以字符串的形式出现。 • fs_flags:指明具体文件系统的一些特性

• read_super:这是各种文件系统读入其超级块的函数指针。因为不同的文件系统其超 级块不同,因此其读入函数也不同。

• owner:如果 file_system_type 所代表的文件系统是通过可安装模块实现的,则该指 针指向代表着具体模块的 module 结构。如果文件系统是静态地链接到内核,则这个域为 NULL。 实际上,你只需要把这个域置为 THIS_MODLUE (这是个一个宏),它就能自动地完成上述工 作。

• next:把所有的 file_system_type 结构链接成单项链表的链接指针,变量 file_systems 指向这个链表。这个链表是一个临界资源,受 file_systems_lock 自旋读写锁 的保护。

• fs_supers:这个域是 Linux 2.4.10 以后的内核版本中新增加的,这是一个双向链表。 链表中的元素是超级块结构。如前说述,每个文件系统都有一个超级块,但有些文件系统可 能被安装在不同的设备上,而且每个具体的设备都有一个超级块,这些超级块就形成一个双 向链表。

文件注册函数int register_filesystem(struct file_system_type * fs) (fs/super.c)

 

 

在挂载的过程中,最为重要的数据结构是vfsmount,它代表一个挂载点。其次是dentryinode,这两个都是对文件的表示,且都会缓存在哈希表中以提高查找的效率。

其中inode是对磁盘上文件的唯一表示,其中包含文件的元数据(管理数据)和文件数据等内容,但不含文件名称。而dentry则是为了Linux内核中查找文件方便虚拟出来的一个数据结构,其中包含文件名称、子目录(如果存在的话)和关联的inode等信息。

dentry结构体最为关键,其维护了内核中的文件目录树。其中里面比较重要的几个结构体分别是d_named_hashd_subdirs。其中d_name代表一个路径节点的名称(文件夹名称)、d_hash则用于构建哈希表,d_subdirs则是下级目录(或文件)的列表。这样,通过dentry就可以形成一个非常复杂的目录树。


 

文件处理流程:我们在访问一个文件之前首先要打开它(open)文件访问,然后进行文件的读写操作(read或者write)。

我们知道,在用户态打开一个文件是返回的是一个文件描述符,其实也就是一个整数值;同时,访问文件也是通过这个文件描述符进行的。那么操作系统是怎么通过这个整数值实现不同类型文件系统的访问呢?不同文件系统的差异其实就是inode中初始化的函数指针的差异。

Linux操作系统中,文件的打开必须要与进程(或者线程)关联,也就是说一个打开的文件必须隶属于某个进程。

linux内核当中一个进程通过task_struct结构体描述,而打开的文件则用file结构体描述,打开文件的过程也就是对file结构体的初始化的过程。在打开文件的过程中会将inode部分关键信息填充到file中,特别是文件操作的函数指针。在task_struct中保存着一个file类型的数组,而用户态的文件描述符其实就是数组的下标。这样通过文件描述符就可以很容易到找到file,然后通过其中的函数指针访问数据。

文件系统添加

Mount -t proc proc /tmp/

 

4 Page Cache

内存管理单元(英语:memory management unit,缩写为MMU),有时称作分页内存管理单元(英语:paged memory management unit,缩写为PMMU)。它是一种负责处理中央处理器CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址物理地址的转换(即虚拟内存管理)[1]内存保护、中央处理器高速缓存的控制,在较为简单的计算机体系结构中,负责总线仲裁以及存储体切换bank switching,尤其是在8的系统上)。

 

page 是内存管理分配的基本单位, Page Cache 由多个 page 构成。page 在操作系统中通常为 4KB 大小(32bits/64bits),而 Page Cache 的大小则为 4KB 的整数倍。

Cache层在内存中缓存了磁盘上的部分数据。用户进程启动read()系统调用后,内核会首先查看page cache里有没有用户要读取的文件内容,如果有(cache hit),那就直接读取,没有的话(cache miss缺页中断(英语:Page fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。通常情况下,用于处理此中断的程序是操作系统的一部分。如果操作系统判断此次访问是有效的,那么操作系统会尝试将相关的分页从硬盘上的虚拟内存文件中调入内存。而如果访问是不被允许的,那么操作系统通常会结束相关的进程

再启动I/O操作从磁盘上读取,然后放到page cache中,下次再访问这部分内容的时候,就又可以cache hit

 

Linux的实现中,文件Cache分为两个层面,一是Page Cache,另一个Buffer Cache,每一个Page Cache包含若干Buffer CachePage Cache主要用来作为文件系统上的文件数据的缓存来用,尤其是针对当进程对文件有read/write操作的时候。Buffer Cache则主要是设计用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用。

Page Cache = Buffers + Cached + SwapCached

 

相对于磁盘,内存的容量还是很有限的,所以通常没必要缓存整个文件,只需要当文件的某部分内容真正被访问到时,再将这部分内容调入内存缓存起来就可以了,这种方式叫做demand paging(按需调页),把对需求的满足延迟到最后一刻,很懒很实用。

 

内存管理系统和 VFS 只与 Page Cache 交互,内存管理系统负责维护每项 Page Cache 的分配和回收,同时在使用 memory map 方式访问时负责建立映射;VFS 负责 Page Cache 与用户空间的数据交换。而具体文件系统则一般只与 Buffer Cache 交互,它们负责在外围存储设备和 Buffer Cache 之间交换数据。读缓存以Page Cache为单位,每次读取若干个Page Cache,回写磁盘以Buffer Cache为单位,每次回写若干个Buffer Cache

       

 

Linux 内核中,文件的每个数据块最多只能对应一个 Page Cache 项,它通过两个数据结构来管理这些 Cache 项,一个是 radix tree,另一个是双向链表。Radix tree 是一种搜索树,Linux 内核利用这个数据结构来通过文件内偏移快速定位 Cache 项,图 4 radix tree的一个示意图,该 radix tree 的分叉为4(22),树高为4,用来快速定位8位文件内偏移。Linux(2.6.7) 内核中的分叉为 64(26),树高为 6(64位系统)或者 11(32位系统),用来快速定位 32 位或者 64 位偏移,radix tree 中的每一个叶子节点指向文件内相应偏移所对应的Cache项。

 

查看Page Cache的核心数据结构struct address_space就可以看到上述结构(略去了无关结构, incloud/linux/fs.h):

struct address_space  {

struct inode             *host;              /* owner: inode, block_device */

struct radix_tree_root      page_tree;         /* radix tree of all pages */

unsignedlong           nrpages;  /*number of total pages */

struct address_space       *assoc_mapping;      /* ditto */

......

} __attribute__((aligned(sizeof(long))));

下面是一个Radix Tree实例:

 

  另一个数据结构是双向链表,Linux内核为每一片物理内存区域(zone)维护active_listinactive_list两个双向 链表,这两个list主要用来实现物理内存的回收。这两个链表上除了文件Cache之外,还包括其它匿名(Anonymous)内存,如进程堆栈等。

 

 

回写与同步

Page cache毕竟是为了提高性能占用的物理内存,随着越来越多的磁盘数据被缓存到内存中,page cache也变得越来越大,如果一些重要的任务需要被page cache占用的内存,内核将回收page cache以支持这些需求。

elf文件为例,一个elf镜像文件通常由text(code)data组成,这两部分的属性是不同的,text是只读的,调入内存后不会被修改,page cache里的内容和磁盘上的文件内容始终是一致的,回收的时候只要将对应的所有PTEsP位和PFN0,直接丢弃就可以了, 不需要和磁盘文件同步,这种page cache被称为discardable的。

data是可读写的,当data对应的page被修改后,硬件会将PTE中的Dirty位置1Linux通过SetPageDirty(page)设置这个page对应的struct pageflagsPG_Dirty,而后将PTE中的Dirty位清0

在之后的某个时间点,这些修改过的page里的内容需要同步到外部的磁盘文件,这一过程就是page writeback,和硬件cachewriteback原理是一样的,区别仅在于CPUcache是由硬件维护一致性,而page cache需要由软件来维护一致性,这种page cache被称为syncable的。

触发条件

那什么时候才会触发pagewriteback呢?分下面几种情况:

从空间的层面,当系统中"dirty"的内存大于某个阈值时。该阈值以在dirtyable memory中的占比"dirty_background_ratio"(默认为10%),或者绝对的字节数"dirty_background_bytes"2.6.29内核引入)给出。如果两者同时设置的话,那么以"bytes"为更高优先级。

此外,还有"dirty_ratio"(默认为20%)和"dirty_bytes"[1],它们的意思是当"dirty"的内存达到这个数量(屋里太脏),进程自己都看不过去了,宁愿停下手头的write操作(被阻塞,同步),先去把这些"dirty"writeback了(把屋里打扫干净)。

而如果"dirty"的程度介于这个值和"background"的值之间(10% - 20%),就交给后面要介绍的专门负责writebackbackground线程去做就好了(专职的清洁工,异步)。

从时间的层面,即周期性的扫描(扫描间隔用"dirty_writeback_ interval"表示,以毫秒为单位),发现存在最近一次更新时间超过某个阈值的pages(该阈值用"dirty_expire_interval"表示, 以毫秒为单位)。

要想知道page的最近更新时间,最简单的方法当然是每个page维护一个timestamp,但这个开销太大了,而且全部扫描一次也会非常耗时,因此具体实现中不会以page为粒度,而是按照inode中记录的dirtying-time来算。

用户主动发起sync()/msync()/fsync()调用时。

可通过/proc/sys/vm文件夹查看或修改以上提到的几个参数:

 

centisecs0.01s,因此上图所示系统的的"dirty_writeback_interval"5s"dirty_expire_interval"30s

来对比下硬件cachewriteback机制。对于硬件cachewriteback会在两种情况触发:

内存有新的内容需要换入cache时,替换掉一个老的cache line。你说为什么page cache不也这样操作,而是要周期性的扫描呢?

替换掉一个cache lineCPU来说是很容易的,直接靠硬件电路完成,而替换page cache的操作本身也是需要消耗内存的(比如函数调用的堆栈开销),如果这个外部backing store是个网络上的设备,那么还需要先建立socket之类的,才能通过网络传输完成writeback,那这内存开销就更大了。所以啊,对于page cache,必须未雨绸缪,不能等内存都快耗光了才来writeback

x86为例,软件调用WBINVD指令刷新整个cache,调用CLFLUSH刷新指定的cache line(参考这篇文章),分别类似于page cachesync()msync()

 

Swap

Swap 机制指的是当物理内存不够用,内存管理单元(Memory Mangament UnitMMU)需要提供调度算法来回收相关内存空间,然后将清理出来的内存空间给当前内存申请方。

Swap 机制存在的本质原因是 Linux 系统提供了虚拟内存管理机制,每一个进程认为其独占内存空间,因此所有进程的内存空间之和远远大于物理内存。所有进程的内存空间之和超过物理内存的部分就需要交换到磁盘上。

操作系统以 page 为单位管理内存,当进程发现需要访问的数据不在内存时,操作系统可能会将数据以页的方式加载到内存中。上述过程被称为缺页中断,当操作系统发生缺页中断时,就会通过系统调用将 page 再次读到内存中。

但主内存的空间是有限的,当主内存中不包含可以使用的空间时,操作系统会从选择合适的物理内存页驱逐回磁盘,为新的内存页让出位置,选择待驱逐页的过程在操作系统中叫做页面替换(Page Replacement),替换操作又会触发 swap 机制。

如果物理内存足够大,那么可能不需要 Swap 机制,但是 Swap 在这种情况下还是有一定优势:对于有发生内存泄漏几率的应用程序(进程),Swap 交换分区更是重要,这可以确保内存泄露不至于导致物理内存不够用,最终导致系统崩溃。但内存泄露会引起频繁的 swap,此时非常影响操作系统的性能。

Linux 通过一个 swappiness 参数来控制 Swap 机制[2]:这个参数值可为 0-100,控制系统 swap 的优先级:

高数值:较高频率的 swap,进程不活跃时主动将其转换出物理内存。

低数值:较低频率的 swap,这可以确保交互式不因为内存空间频繁地交换到磁盘而提高响应延迟。

 

 

5 文件系统

文件系统是一种用于向用户提供底层数据访问的机制。它将设备中的空间划分为特定大小的块(或者称为),一般每块512字节。数据存储在这些块中,大小被修正为占用整数个块。由文件系统软件来负责将这些块组织为文件和目录,并记录哪些块被分配给了哪个文件,以及哪些块没有被使用。文件系统定义并实现了数据在存储介质(如硬盘等)上的存储方式和结构,以及其是如何被访问的,如索引、读取等。

 

一个文件系统一般使用块设备上一个独立的逻辑分区。

 

1.ext2文件系统。

ext2是为解决ext文件系统的缺陷而设计的可扩展的、高性能的文件系统,又被称为二级扩展文件系统。它是Linux文件系统中使用最多的类型,并且在速度和CPU利用率上较为突出。ext2存取文件的性能极好,并可以支持256字节的长文件名,是GNU/Linux系统中标准的文件系统。

2.ext3文件系统。

ext3ext2文件系统的日志版本,它在ext2文件系统中增加了日志的功能。ext3提供了3种日志模式:日志(journal)、顺序(ordered)和回写(writeback)。与ext2相比,ext3提供了更好的安全性以及向上向下的兼容性能。

3.ext4文件系统。

Linux kernel 2.6.28 开始正式支持新的文件系统 Ext4Ext4 Ext3 的改进版,修改了 Ext3 中部分重要的数据结构,而不仅仅像 Ext3 Ext2 那样,只是增加了一个日志功能而已。Ext4 可以提供更佳的性能和可靠性,还有更为丰富的功能:

Ext3 兼容。执行若干条命令,就能从 Ext3 在线迁移到Ext4,而无须重新格式化磁盘或重新安装系统。原有 Ext3数据结构照样保留,Ext4作用于新数据,当然,整个文件系统因此也就获得了 Ext4 所支持的更大容量。

更大的文件系统和更大的文件。较之 Ext3 目前所支持的最大 16TB 文件系统和最大 2TB 文件,Ext4分别支持 1EB1,048,576TB1EB=1024PB1PB=1024TB)的文件系统,以及 16TB 的文件。

无限数量的子目录。Ext3 目前只支持 32,000 个子目录,而Ext4支持无限数量的子目录。

ExtentsExt3 采用间接块映射,当操作大文件时,效率极其低下。比如一个 100MB 大小的文件,在 Ext3 中要建立 25,600 数据块(每个数据块大小为 4KB)的映射表。而Ext4引入了现代文件系统中流行的 extents 概念,每个 extent 为一组连续的数据块,上述文件则表示为“该文件数据保存在接下来的 25,600 个数据块中”,提高了不少效率。

多块分配。当写入数据到 Ext3 文件系统中时,Ext3 的数据块分配器每次只能分配一个 4KB 的块,写一个 100MB 文件就要调用 25,600 次数据块分配器,而Ext4的多块分配器multiblock allocator”(mballoc) 支持一次调用分配多个数据块。

延迟分配。Ext3 数据块分配策略是尽快分配,而Ext4和其它现代文件操作系统的策略是尽可能地延迟分配,直到文件在 cache 中写完才开始分配数据块并写入磁盘,这样就能优化整个文件的数据块分配,与前两种特性搭配起来可以显著提升性能。

快速 fsck。以前执行 fsck 第一步就会很慢,因为它要检查所有的 inode,现在Ext4给每个组的 inode 表中都添加了一份未使用 inode 的列表,今后 fsck Ext4 文件系统就可以跳过它们而只去检查那些在用的 inode 了。

 

日志校验。日志是最常用的部分,也极易导致磁盘硬件故障,而从损坏的日志中恢复数据会导致更多的数据损坏。Ext4的日志校验功能可以很方便地判断日志数据是否损坏,而且它将 Ext3 的两阶段日志机制合并成一个阶段,在增加安全性的同时提高了性能。

“无日志”(No Journaling)模式。日志总归有一些开销,Ext4允许关闭日志,以便某些有特殊需求的用户可以借此提升性能。

在线碎片整理。尽管延迟分配、多块分配和 extents 能有效减少文件系统碎片,但碎片还是不可避免会产生。Ext4支持在线碎片整理,并将提供 e4defrag 工具进行个别文件或整个文件系统的碎片整理。

inode 相关特性。Ext4支持更大的 inode,较之 Ext3 默认的 inode 大小 128 字节,Ext4 为了在 inode 中容纳更多的扩展属性(如纳秒时间戳 inode 版本),默认 inode 大小为 256 字节。Ext4还支持快速扩展属性(fast extended attributes)和 inode 保留(inodes reservation)。

持久预分配(Persistent preallocation)。P2P 软件为了保证下载文件有足够的空间存放,常常会预先创建一个与所下载文件大小相同的空文件,以免未来的数小时或数天之内磁盘空间不足导致下载失败。Ext4在文件系统层面实现了持久预分配并提供相应的 APIlibc 中的 posix_fallocate()),比应用软件自己实现更有效率。

默认启用 barrier。磁盘上配有内部缓存,以便重新调整批量数据的写操作顺序,优化写入性能,因此文件系统必须在日志数据写入磁盘之后才能写 commit 记录,若 commit 记录写入在先,而日志有可能损坏,那么就会影响数据完整性Ext4默认启用 barrier,只有当 barrier 之前的数据全部写入磁盘,才能写 barrier 之后的数据。(可通过 "mount -o barrier=0" 命令禁用该特性。)

 

4.xfs文件系统

XFS最早针对IRIX操作系统开发,是一个高性能的日志型文件系统,能够在断电以及操作系统崩溃的情况下保证文件系统数据的一致性。它是一个64位的文件系统,后来进行开源并且移植到了Linux操作系统中,目前CentOS 7XFS+LVM作为默认的文件系统。据官方所称,XFS对于大文件的读写性能较好。

性能问题对于大多数文件系统而言,都是一个头疼的事情,特别是在高并发大量小文件这种场景下,而XFS采用了一些好的思想比如引入分配组、B+树、extent等方法来提高性能

特性

数据完全性

采用XFS文件系统,当意想不到的宕机发生后,首先,由于文件系统开启了日志功能,所以你磁盘上的文件不再会意外宕机而遭到破坏了。不论目前文件系统上存储的文件与数据有多少,文件系统都可以根据所记录的日志在很短的时间内迅速恢复磁盘文件内容。

传输特性

XFS文件系统采用优化算法,日志记录对整体文件操作影响非常小。XFS查询与分配存储空间非常快。xfs文件系统能连续提供快速的反应时间。笔者曾经对XFSJFSExt3ReiserFS文件系统进行过测试,XFS文件文件系统的性能表现相当出众。

可扩展性

XFS 是一个全64-bit的文件系统,它可以支持上百万T字节的存储空间。对特大文件及小尺寸文件的支持都表现出众,支持特大数量的目录。最大可支持的文件大 小为263 = 9 x 1018 = 9 exabytes,最大文件系统尺寸为18 exabytesXFS使用高的表结构(B+),保证了文件系统可以快速搜索与快速空间分配。XFS能够持续提供高速操作,文件系统的性能不受目录中目录及文件数量的限制。

传输带宽

XFS 能以接近裸设备I/O的性能存储数据。在单个文件系统的测试中,其吞吐量最高可达7GB每秒,对单个文件的读写操作,其吞吐量可达4GB每秒。

容量

XFS是一个64位文件系统,最大支持 8exbibytes 1字节的单个文件系统,实际部署时取决于宿主操作系统的最大块限制。对于一个32Linux系统,文件和文件系统的大小会被限制在 16tebibytes

 

 

"XFS文件系统默认在挂载时启用“写入屏障”的支持。该特性会一个合适的时间冲刷下级存储设备的写回缓存,特别是在XFS做日志写入操作的时候。这个特性的初衷是保证文件系统的一致性,具体实现却因设备而异——不是所有的下级硬件都支持缓存冲刷请求。在带有电池供电缓存的硬件RAID控制器提供的逻辑设备上部署XFS文件系统时,这项特性可能导致明显的性能退化,因为文件系统的代码无法得知这种缓存是非易失性的。如果该控制器又实现了冲刷请求,数据将被不必要地频繁写入物理磁盘。为了防止这种问题,对于能够在断电或发生其它主机故障时保护缓存中数据的设备,应该以 nobarrier 选项挂载XFS文件系统。

 

 


通过fstransform可以实现无损的将一种文件系统转换成另外一种文件系统,比如ext4转换为xfs

6 通用块管理

通用块层是一个内核组件,它处理来自系统中的所有块设备发出的请求并最终发出I/O请求。该层隐藏了底层硬件块设备的特性,为块设备提供了一个通用的抽象视图。

通用块层的核心数据结构是一个称为BIO的描述符,它描述了块设备的IO操作。每个bio结构都包含一个磁盘存储区标识符(存储区中的起始扇区和扇区数目)和一个或多个描述符 与IO操作相关的内存区的段。biostruct bio 数据结构描述(include/linux/bio.h)

对于VFS和具体的文件系统来说,块(Block)是基本的数据传输单元,当内核访问文件的数据时,它首先从磁盘上读取一个块。但是对于磁盘来说,扇区是最小的可寻址单元,块设备无法对比它还小的单元进行寻址和操作。由于扇区是磁盘的最小可寻址单元,所以块不能比扇区还小,只能整数倍于扇区大小,即一个块对应磁盘上的一个或多个扇区。一般来说,块大小是2的整数倍,而且由于Page Cache层的最小单元是页(Page),所以块大小不能超过一页的长度。

大多情况下,数据的传输通过DMA方式。旧的磁盘控制器,仅仅支持简单的DMA操作:每次数据传输,只能传输磁盘上相邻的扇区,即数据在内存中也是连续的。这是因为如果传输非连续的扇区,会导致磁盘花费更多的时间在寻址操作上。而现在的磁盘控制器支持“分散/聚合”DMA操作,这种模式下,数据传输可以在多个非连续的内存区域中进行。为了利用“分散/聚合”DMA操作,块设备驱动必须能处理被称为段(segments)的数据单元。一个段就是一个内存页面或一个页面的部分,它包含磁盘上相邻扇区的数据。

通用块层是粘合所有上层和底层的部分,一个页的磁盘数据布局如下图所示:

 

 

当内核发现系统中一个新的磁盘时(在启动阶段,或将一个可移动介质插入到一个驱动器中时,或在运行期附加一个外置磁盘时),就调用alloc_disk()函数,该函数分配并初始化一个新的gendisk对象。如果新磁盘被分成了几个分区,那么alloc_disk还会分配并初始化一个适当的hd_struct类型的数组。然后,内核调用add_disk()函数将gendisk对象插入到通用块层的数据结构中。

提交请求
我们介绍一下当向通用块层提交一个IO操作请求时,内核所执行的步骤顺序。我们假定(因为上文提到一个IO,如果数据不相邻会被拆成多个请求)被请求的数据块在磁盘上是相邻的,并且内核已经知道了它们的物理位置。

 

第一步是执行bio_alloc函数分配一个新的bio描述符。然后通过设置一些字段值来初始化bio描述符(bi_sectori_sizei_bdevi_io_veci_rwi_end_io
一旦bio描述符被进行了适当的初始化,内核就调用generaic_make_request函数,该函数是通用块层的主要入口点。

 

获取与块设备相关的请求队列

调用blk_partition_remap()函数

至此,能用块层 IO调度程序以及设备驱动程序将忘记磁盘分区的存在,直接作用于整个磁盘。

调用q_make_request_fn方法将bio请求插入到请求队列中。




 

 

7 I/O 调度层

当通用块层把 IO 请求实际发出以后,并不一定会立即被执行。因为调度层会从全局出发,尽量让整体磁盘 IO 性能最大化。

I/O调度层的功能是管理块设备的请求队列。即接收通用块层发出的I/O请求,缓存请求并试图合并相邻的请求。并根据设置好的调度算法,回调驱动层提供的请求处理函数,以处理具体的I/O请求。

如果简单地以内核产生请求的次序直接将请求发给块设备的话,那么块设备性能肯定让人难以接受,因为磁盘寻址是整个计算机中最慢的操作之一。为了优化寻址操作,内核不会一旦接收到I/O请求后,就按照请求的次序发起块I/O请求。为此Linux实现了几种I/O调度算法,算法基本思想就是通过合并和排序I/O请求队列中的请求,以此大大降低所需的磁盘寻道时间,从而提高整体I/O性能。

对于机械硬盘来说,调度层会尽量让磁头类似电梯那样工作,先往一个方向走,到头再回来,这样整体效率会比较高一些。

常见的I/O调度算法包括Noop调度算法(No Operation)、CFQ(完全公正排队I/O调度算法)、DeadLine(截止时间调度算法)、AS预测调度算法等。

 

Noop算法:最简单的I/O调度算法。该算法仅适当合并用户请求,并不排序请求。新的请求通常被插在调度队列的开头或末尾,下一个要处理的请求总是队列中的第一个请求。这种算法是为不需要寻道的块设备设计的,如SSD。因为其他三个算法的优化是基于缩短寻道时间的,而SSD硬盘没有所谓的寻道时间且I/O响应时间非常短。

 

 

CFQ算法:算法的主要目标是在触发I/O请求的所有进程中确保磁盘I/O带宽的公平分配。算法使用许多个排序队列,存放了不同进程发出的请求。通过散列将同一个进程发出的请求插入同一个队列中。采用轮询方式扫描队列,从第一个非空队列开始,依次调度不同队列中特定个数(公平)的请求,然后将这些请求移动到调度队列的末尾。

 

 

Deadline算法:算法引入了两个排队队列分别包含读请求和写请求,两个最后期限队列包含相同的读和写请求。本质就是一个超时定时器,当请求被传给电梯算法时开始计时。一旦最后期限队列中的超时时间已到,就想请求移至调度队列末尾。Deadline算法避免了电梯调度策略(为了减少寻道时间,会优先处理与上一个请求相近的请求)带来的对某个请求忽略很长一段时间的可能。

 

 

AS算法:AS算法本质上依据局部性原理,预测进程发出的读请求与刚被调度的请求在磁盘上可能是“近邻”。算法统计每个进程I/O操作信息,当刚刚调度了由某个进程的一个读请求之后,算法马上检查排序队列中的下一个请求是否来自同一个进程。如果是,立即调度下一个请求。否则,查看关于该进程的统计信息,如果确定进程p可能很快发出另一个读请求,那么就延迟一小段时间。

 

 

对于固态硬盘来说,随机 IO 的问题已经被很大程度地解决了,所以可以直接使用最简单的 noop 调度器。

在许多的开源框架如KafkaHBase中,都通过追加写的方式来尽可能的将随机I/O转换为顺序I/O,以此来降低寻址时间和旋转延时,从而最大限度的提高IOPS

 

通过 dmesg | grep -i scheduler 查看 Linux 支持的调度算法。

 

8 硬盘驱动层

 

块设备同样支持以文件的形式被存取。当遇到打开块设备操作时,用来提供一套对

应的文件操作的机制与字符设备基本上是一样的。Linuxblkdevs向量中维护登记

了的块设备。与chrdevs向量一样,blkdevs向量使用设备的主设备号作为其索引。

向量的每一个入口仍是一个device_struct数据结构。与字符设备不同的是,这些数

据结构是属于块设备的。SCSI设备和IDE设备是其中两个例子。这些设备数据结构在

核心中登记并为核心提供对应于其设备的文件操作。对应于某类设备的设备驱动程

序提供实现这些接口的细节。例如,一个SCSI设备驱动程序必须为SCSI子系统提供

接口。SCSI子系统利用这些接口,提供给核心一个一致的文件接口。

 

除了文件操作接口,每个块设备还必须提供缓冲区接口。每一个块设备驱动程序在

一个blk_dev向量中添加其入口。blk_dev向量的每个元素是一个blk_dev_struct

据结构。向量的索引仍然是设备的主设备号。blk_dev_struct数据结构中含有一个

请求例程的地址和一个指向request"数据结构的指针。每一个“request"数据结

构代表了一个从缓冲区到驱动程序的读或写数据块的请求。

 

 

8.2 块设备的缓冲

 

 

每次一个缓冲区想要读或写一块数据从/到一个登记了的设备,它将插入一个"request"

据结构在blk_dev_struct中。由8.2所示,每一个申请含

有一个指向一个或多个buffer_head"的数据结构。每一个buffer_head是读或写一个块数

据的请求(译者注:请参阅Linux数据结构章节)Buffer_head结构是被缓冲区锁住的。因此

有可能存在一个进程正在等待对这个缓冲区操作的完成。每一个request"数据结构是从一

个静态的链表中(all_requests)分配而来。如果一个请求(request)被加在一个空的请求队列

上,设备驱动程序的请求函数(译者注:blk_dev_struc结构中函数

指针所指向的函数)将被立即调用来处理这个请求队列。否则,驱动程序将顺序地处

理请求队列中的所有请求。

 

一旦设备驱动程序完成一个请求,它必须从这个请求中移去每一个buffer_head结构,

将它们标志成为更新并释放对其的锁。对一个buffer_head锁的释放将唤醒所有在睡

眠中等待这个块操作完成的进程。一个例子是:当要解释一个文件名时,EXT2文件

系统必须从块设备中读取下一个EXT2目录项。这个进程将睡眠在那个含有目录项的

buffer_head上直到被设备驱动程序唤醒。这个request数据结构将被回收从而可以

被其他的块请求使用。

 

 

 

资料来源

https://zhuanlan.zhihu.com/p/371574406 

https://blog.csdn.net/iteye_10774/article/details/82606412

https://blog.csdn.net/u010006102/article/details/39672023 

https://zhuanlan.zhihu.com/p/71217136 

https://tech.meituan.com/2017/05/19/about-desk-io.html 

https://www.jianshu.com/p/ce43ec207ac5 

https://github.com/Spongecaptain/SimpleClearFileIO/blob/main/1.%20page%20cache.md 

https://www.jianshu.com/p/411963608ec9 

http://kerneltravel.net/book/ 

深入Linux内核架构Wolfgang Mauerer

深入了解计算机系统David R OHallaron & Randal E Bryant


   

备案编号:赣ICP备15011386号

联系方式:qq:1150662577    邮箱:1150662577@qq.com