1. 程式人生 > >Linux Page cache和Block I/O layer

Linux Page cache和Block I/O layer

下面內容是來自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的策略: