Linux Page cache和Block I/O layer

来源:互联网 发布:linux jdk tar.gz下载 编辑:程序博客网 时间:2024/06/05 14:12

下面内容是来自LKD和ULK的读书笔记,见该书的LKD的《Chapter 16 The Page Cache and Page Writeback》和《Chapter 14 The Block I/O Layer》,以及ULK的《Chapter 18 The Ext2 and Ext3 Filesystems》, 由于LKD只是概述,因为可能会添加ULK中的内容。

先看《Chapter 16 The Page Cache and Page Writeback》

Linux实现了一个disk cache叫page cache,该cache的目标是通过把数据存储在Physical Memory中使disk I/O最小化。

page cache的大小是动态的,可以增大到消耗所有的free memory, 可以缩小来减轻内存压力。

这里主要介绍一个address_space对象,在inode对象中有这么一个field: struct address_space * i_mapping, 这是一个指向address_space对象的指针。显然一个文件对应一个inode,一个inode有一个address_space, 不同的进程打开相同的文件,文件的内容都是cache在这个address_space中的page tree中的,当不同的进程访问file的不同部分时,该部分就cache到page cache中,page cache中保存的是所有进程要打开的文件各个部分的总和。

在page cache中的page可以包含许多不连续的物理disk blocks。因此检查page cache来看看特定的数据是否在cache中就很困难,就是因为这种在page中的不连续的block布局。因此不可能在pagecache中只使用一个device name和block number来index 数据,不然这就是最简单的方法。

Linux page cache采用了一个新的对象来管理page和page I/O操作--即address_space:

struct address_space {
    struct inode *host; /* owning inode */ 用于找到该adress_space属于哪个inode,进而知道属于哪个file,以及该file的dentry。
    struct radix_tree_root page_tree; /* radix tree of all pages */ page基树,指向该树的根,可以根据该树找到所有的page。
    spinlock_t tree_lock; /* page_tree lock */
    unsigned int i_mmap_writable; /* VM_SHARED ma count */
    struct prio_tree_root i_mmap; /* list of all mappings */
    struct list_head i_mmap_nonlinear; /* VM_NONLINEAR ma list */
    spinlock_t i_mmap_lock; /* i_mmap lock */
    atomic_t truncate_count; /* truncate re count */
    unsigned long nrpages; /* total number of pages */ 该文件所有page的数量
    pgoff_t writeback_index; /* writeback start offset */
    struct address_space_operations *a_ops; /* operations table */ address_space的操作函数,很重要。
    unsigned long flags; /* gfp_mask and error flags */
    struct backing_dev_info *backing_dev_info; /* read-ahead information */
    spinlock_t private_lock; /* private lock */
    struct list_head private_list; /* private list */
    struct address_space *assoc_mapping; /* associated buffers */
};


下面介绍address_space operatios: struct address_space_operations *a_ops

struct address_space_operations {
    int (*writepage)(struct page *, struct writeback_control *);
    int (*readpage) (struct file *, struct page *);
    int (*sync_page) (struct page *);
    int (*writepages) (struct address_space *,struct writeback_control *);
    int (*set_page_dirty) (struct page *);
    int (*readpages) (struct file *, struct address_space *,struct list_head *, unsigned);
    int (*write_begin)(struct file *, struct address_space *mapping, \
            loff_t pos, unsigned len, unsigned flags,struct page **pagep, void **fsdata);
    int (*write_end)(struct file *, struct address_space *mapping,
            loff_t pos, unsigned len, unsigned copied,struct page *page, void *fsdata);
    sector_t (*bmap) (struct address_space *, sector_t);
    int (*invalidatepage) (struct page *, unsigned long);
    int (*releasepage) (struct page *, int);
    int (*direct_IO) (int, struct kiocb *, const struct iovec *,loff_t, unsigned long);
    int (*get_xip_mem) (struct address_space *, pgoff_t, int,void **, unsigned long *);
    int (*migratepage) (struct address_space *,struct page *, struct page *);
    int (*launder_page) (struct page *);
    int (*is_partially_uptodate) (struct page *,read_descriptor_t *,unsigned long);
    int (*error_remove_page) (struct address_space *,struct page *);
};

上面比较重要的: writepage, readpage, sync_page, direct_IO,之前文章都介绍到过,这里不再介绍,可以看ULK的chapter 16。

关于Radix Tress

因为kernel在发起任何page I/O之前,都要检查page cache中某个page是否存在,而且速度一定要快,要不然如果速度很慢的话,page cache就失去了意义,还不如直接从disk中读取。每一个address_space都有一个唯一的radix tree,一个radix tree是一个二叉树,二叉树加快了在page cache中搜索page的速度。


The Buffer Cache

单个的disk blocks也存在于page cache中,通过block I/O buffers。一个buffer是一个physical disk block在memory的代表。buffer用来将memory中的pages映射为disk blocks, 因此page cache也减少了block I/O操作中对disk 的access,通过cache disk blocks以及延后block I/O操作。这种caching被叫做buffer cache,尽管不是通过单独的cache实现的,而是page cache的一部分。

难道一个完整的page cache没有命中时,就发起访问硬盘的操作,然后给block layer发出请求,block在真正读硬盘时要首先到buffer cache中看看请求的block是否在内存中?????

Linux2.4中page cache和buffer cache是分开的,这造成了memory的浪费。Linux 2.6就只有一个disk cache了: page cache。但是Kernel仍然在memory中用buffers来代表disk中的block,因此,buffer描述了一个block到一个page的映射,这个page是在page cache中。

The Flusher Threads

page cache中write操作是被延后的,当page cache中的数据比disk中的数据新的时候,我们称为data dirty。Dirty page最终是需要协会到disk中的,有三种情况:

1. 当fress memory 缩小到小于一个指定的threshhold。

2. 当dirty data老于一个指定的threshhold时。

3. 当user process调用sync()和fsync()系统调用时。

Linux2.6中一群kernel线程,叫做flusher threads做上面三个工作。


Laptop mode: Lapton mode是一种很特别的page writeback 模式用来优化battery life,通过尽量不频繁的write back,尽量减少硬盘的转动来节省电源。


Block I/O layer还是比较精彩的,因为一般Block layer再往下可能就是某个block device的driver了,所以要搞清楚block I/O layer向下提供什么接口,这是很重要的。

首先说说,

下面的内容来自ULK《Chapter 14 Block Devices Drivers》

本章的“Block Devices Handling”解释了Linux block I/O子系统的架构,还介绍了该子系统的component: "The Generic Block Layer", "The I/O Scheduler","Block Device Drivers", "Opening a Block Device File"。

下面介绍Block I/O layer的架构:Block Devices Handing

下面这张图是介绍应用程序读写文件时的Linux存储协议栈所涉及到的组件。可以看出block I/O layer包括三部分:Generic Block Layer,I/O schduler layer,以及block device driver。本文后面仅仅了解下Generic block layer和I/O schduler layer的原理,重点关注下Block device driver, 因为可能会block device driver来写驱动,因为要多关注一下。


下面的内容来自LKD《Chapter 14 The Block I/O Layer》以及ULK《Chapter 14 Block Devices Drivers》,用来大体了解Generic Block Layer和I/O schduler layer

Block I/O layer还是比较精彩的,因为一般Block layer再往下可能就是某个block device的driver了,所以要搞清楚block I/O layer向下提供什么接口,这是很重要的。

首先说说block devices的定义, block devices是hardware devices,该devices可以被random访问固定大小chunk的data。固定大小chunk的data被叫做blocks。

Block devices中的术语:

1. sector: block device中最小的可寻址单元,必须是2的指数幂。I/O scheduler、block device drivers必须以sector管理data。

2. 软件最小的逻辑可寻址单元叫做block,filesystem只能以多个block的方式访问block devices。因此block必须是sector的整数倍。同时block不得大于page size。因此一个page可以容纳一个或者多于一个block。VFS、mapping layer、文件系统采用block来管理data。

3. Block Device Driver应该可以拷贝segments of data: 每个segment 是一个memory page或者 a memory page including a chunks of data that are physically adjacent on disk。

4. Generic block layer起承上启下的作用,因此知道sectors、blocks、segments、pages。

尽管有不同的chunks of data, 但它们共享相同的RAM,如下图14-2, 从这个图中可以理解上述概念。

上图中,upper kernel components看到这个page由4个block buffer组成,每个block buffer有1024个bytes。page中的最后三个blocks正在被block device driver传输,因此这三个block被塞到一个segment中,而Hard disk controller认为这个segment由6个512自己的sector组成。

Buffers以及Buffer Heads

当一个disk中的block存储在memory中时,比如说进行读或者将pending一个写时,block是存储在memory的buffer中。Memory中的buffer是和一个block精确相关联起来的。所以内核需要相关的控制信息来存储这种关联, 该控制信息叫做buffer head, 保存了kernel操作buffer的一切信息。

struct buffer_head {
    unsigned long b_state; /* buffer state flags */ 保存了这个buffer的状态,可以有很多值, 每个值都有相关的含义。
    struct buffer_head *b_this_page; /* list of page’s buffers */ buffer所在的page的下一个buffer
    struct page *b_page; /* associated page */ buffer所在的page
    sector_t b_blocknr; /* starting block number */ 相对于block device的起始位置的logical block number
    size_t b_size; /* size of mapping */ block的长度
    char *b_data; /* pointer to data within the page */ block的头在buffer page中的位置,block从b_data开始到b_data+b_size结束。
    struct block_device *b_bdev; /* associated block device */ 指向block device
    bh_end_io_t *b_end_io; /* I/O completion */
    void *b_private; /* reserved for b_end_io */
    struct list_head b_assoc_buffers; /* associated mappings */
    struct address_space *b_assoc_map; /* associated address space */
    atomic_t b_count; /* use count */
};

从上面的数据结构可以看出,buffer head的目的是描述on-disk block和in-memory的buffer之间的映射关系,比如描述了block所在的page的指针、block在page中的起始地址、block的大小,这就描述清楚了block在page中的位置,还有block在磁盘中的信息,比如block所在的block设备,以及相对于block device的起始位置的logical block number。

The bio Structure

bio结构体是上层提交给block I/O layer工作请求的接口数据结构, 是Generic block layer的核心数据结构:

struct bio {
    sector_t bi_sector; /* associated sector on disk */
    struct bio *bi_next; /* list of requests */
    struct block_device *bi_bdev; /* associated block device */
    unsigned long bi_flags; /* status and command flags */
    unsigned long bi_rw; /* read or write? */
    unsigned short bi_vcnt; /* number of bio_vecs off */
    unsigned short bi_idx; /* current index in bi_io_vec */
    unsigned short bi_phys_segments; /* number of segments */
    unsigned int bi_size; /* I/O count */
    unsigned int bi_seg_front_size; /* size of first segment */
    unsigned int bi_seg_back_size; /* size of last segment */
    unsigned int bi_max_vecs; /* maximum bio_vecs possible */
    unsigned int bi_comp_cpu; /* completion CPU */
    atomic_t bi_cnt; /* usage counter */
    struct bio_vec *bi_io_vec; /* bio_vec list */
    bio_end_io_t *bi_end_io; /* I/O completion method */
    void *bi_private; /* owner-private method */
    bio_destructor_t *bi_destructor; /* destructor method */
    struct bio_vec bi_inline_vecs[0]; /* inline bio vectors */
};

该数据结构最重要的是bi_io_vec,bi_vcnt,以及bi_idx, 如下图:

上图中bi_io_vec field指向一个数组,该数组元素为bio_vec结构体,结构体bio_vec用于描述一组相互独立的segments, 因此一个bio实际上描述了一组相互对立的segments,当然一个segment可以是一个page,也可以小于一个page,但是segments应该是block的倍数。显然bio描述的内存中的segments不是联系的,但是这些segments在disk(或者block device driver呈献给block layer的drive)中应该是连续的。因为该数据结构中描述的disk中的数据只有这么几个参数: 数据在disk上的第一个sector-->bi_sector, block device描述符,以及传输数据的总字节数。因此bio描述的是在disk连续的一块区间对应的不连续的一系列segments。

Request Queues: Request Queue由更高一层的code比如文件系统往其里面提交request,Request Queues由结构体request_queue代表,该request queue由一个个的单独的request组成,request由结构体struct request表示,而每一个request由一个以上的bio组成,因为一个请求可能包含多个相互之间不连续的disk blocks,所以一个request就包含了多个bio,因为一个bio只能请求一个连续的disk blocks。尽管bio描述的Disk中的blocks是连续的,但是在内存中的这些blocks不一定是连续的,因为bio可能包含几个segments, 内存中一个segment中包含的blocks是连续的,但是segments之间是不连续的。这样设计的目的很显然,disk喜欢连读的读写,而memory顺序和随机读写都没问题,如果给disk的读写请求不连续,那么disk花在寻道上的时间是可想而知的,所以要尽量让读写请求连续,磁盘就能发挥出最大的性能。下面的I/O schedulers通过sorting和merging的方法更加快了对disk的连续读写,因为disk的磁头是按照一条直线移动的。

I/O schedulers

因为磁盘的磁头寻道需要很长时间,类似于电梯,所以需要优化。这里主要用到了sorting(排序)和merging(合并)来大幅提高性能,这样Disk就像电梯一样运行,而不会浪费太多的时间在寻道上。I/O schduler主要通过管理block device的request queue来发挥作用, 其决定了请求队列中request的顺序,以及每一个request在什么时间dispatch给block device,通过减少磁头寻道来管理request在request queue中的顺序, 最终产生更大的全局throughput。这里主要介绍了4种电梯算法,不在赘述。

无论I/O schduler怎么merge和sort,最终在Request queue中的request都是由bio构成的,只是bio和request可能被I/O schduler修改了,如上图中文字所述。因此传给block device driver的请求依然是request queue中的request。

I/O schduler管理merge和sort request queue是以request为单位进行的,一个request是一个上层应用的请求对一个或者多个bio的封装,当发出请求后,该应用会pending在该request上,当DMA传输完成后,该request上pending的应用会被唤醒。当两个应用各自的request(即两个request) 合并成一个request后,这两个应用就pending在这个合并后的request上,当这个合并的request被DMA传输完成后,就唤醒pending在其上的两个应用。



下面的内容来自ULK《Chapter 14 Block Devices Drivers》,介绍block device driver,LKD中没有介绍block device driver。

Block device drivers是linux block subsystem中最底层的component,其从I/O scheduler得到requests, 然后去处理。

Block device drivers属于一种device driver model,因此block device drivers是结构体device_driver, 而disk是结构体device,而这些结构体太通用了,因此block I/O子系统必须对每个block device存储更多的信息。

Block devices

一个block device driver可能处理几个block devices,例如IDE device driver可以处理几个IDE硬盘,每一个硬盘都是一个单独的block device。而且每个disk都是分区的,每个分区都被看做一个逻辑block device。

每个block device都用block_device结构体描述。

struct block_device {  
    dev_t           bd_dev;   /*Major and minor numbers of the block device*/
    struct inode *      bd_inode;   /*Pointer to the inode of the file associated with the block device in the bdev filesystem*/
    struct super_block *    bd_super;  
    int         bd_openers;  /* counter of how many times the block device has been opened.*/
    struct mutex        bd_mutex; 
    struct list_head    bd_inodes;  
    void *          bd_holder;  
    int         bd_holders;  
#ifdef CONFIG_SYSFS  
    struct list_head    bd_holder_list;  
#endif  
    struct block_device *   bd_contains;  
    unsigned        bd_block_size;  
    struct hd_struct *  bd_part;  
    /* number of times partitions within this device have been opened. */  
    unsigned        bd_part_count;  
    int         bd_invalidated;  
    struct gendisk *    bd_disk;  
    struct list_head    bd_list;  
    /*
     * Private data.  You must have bd_claim'ed the block_device
     * to use this.  NOTE:  bd_claim allows an owner to claim
     * the same device multiple times, the owner must take special
     * care to not mess up bd_private for that case.
     */  
    unsigned long       bd_private;  
 
    /* The counter of freeze processes */  
    int         bd_fsfreeze_count;  
    /* Mutex for freeze */  
    struct mutex        bd_fsfreeze_mutex;  
}; 
所有的block device descriptors都插入到一个全局list中,list的head为变量all_bdevs,每个block device descriptor中的bd_list连入这个全局list中。

这里重点关注两部分: 1. ULK中的14.4.3 The Strategy Routine,用来描述如何将Request queue中的request发送给各个device的。其中介绍了DMA和Scatter-Gather DMA。

                                      2. ULK中的 14.4.4 The Interrupt Handler, 用来描述当DMA传输完成时,DMA如何引起中断,中断服务程序将某个request从dispatch queue中去除,并唤

                                           醒pending在该request上的所有进程。




下面聊聊EXT2和EXT3的格式有助于理解具体某个文件系统是如何根据file名字找到在磁盘中的位置的。

Ext2 Disk Data Structures

第一个block是作为boot block用的,用来标示是否有OS安装在这个分区。剩下的Ext2分区就分成了block groups,每一个block group的布局如下图所示:

根据上图可以看到,SuperBlock/DataBlockBitmap/InodeBitMap占用一个block, 而Group Descriptors/Inode Table/Data Blocks占用的block数目不确定。每个block group的大小都是一样的,并且是按照顺序存储的,因此kernel可以根据block group的index知道其开头位置。

Block groups减少文件碎片率,因为kernel尝试尽可能将文件数据放在一个block group中。每个block group包含如下信息:

1. 一份filesystem的superblock信息

2. 一份Block Group descriptor信息

3. A data block bitmap

4. An inode bitmap

5. A table of inodes

6. a chunk of data that belongs to a file; ie., data blocks.


Inode Table包括一系列连续的blocks,每一个inode table包括一个预先定义数量的inodes。每一个inode大小相同,都是128 bytes。当然这里的inode和inode对象是不同的,但是inode对象中的很多值都是从这个磁盘inode中提取的。下面重点介绍几个disk inode值:

i_size表示文件的真实有效长度(bytes单位), i_blocks表示分配给文件的blocks个数。二者不是完全一致的,i_size小于i_block*blocksize。

i_block是一个数组,数组成员是EXT2_N_BLOCKS(通常为15个)个指针,该指针指向分配给file的blocks。为了保存大文件采用一种indirection的策略:


0 0