普通檔案的address space
檔案系統讀取檔案一般會使用do_generic_file_read(),mapping指向普通檔案的address space。如果一個檔案的某一塊不在page cache中,在find_get_page函式中會建立一個page,並將這個page根據index插入到這個普通檔案的address space中。這也是我們熟知的過程。
- static ssize_t do_generic_file_read(struct file *filp, loff_t *ppos,
 - struct iov_iter *iter, ssize_t written)
 - {
 - struct address_space *mapping = filp->f_mapping;
 - struct inode *inode = mapping->host;
 - struct file_ra_state *ra = &filp->f_ra;
 - pgoff_t index;
 - pgoff_t last_index;
 - pgoff_t prev_index;
 - unsigned long offset; /* offset into pagecache page */
 - unsigned int prev_offset;
 - int error = 0;
 - index = *ppos >> PAGE_CACHE_SHIFT;
 - prev_index = ra->prev_pos >> PAGE_CACHE_SHIFT;
 - prev_offset = ra->prev_pos & (PAGE_CACHE_SIZE–1);
 - last_index = (*ppos + iter->count + PAGE_CACHE_SIZE–1) >> PAGE_CACHE_SHIFT;
 - offset = *ppos & ~PAGE_CACHE_MASK;
 - for (;;) {
 - struct page *page;
 - pgoff_t end_index;
 - loff_t isize;
 - unsigned long nr, ret;
 - cond_resched();
 - find_page:
 - page = find_get_page(mapping, index);
 - if (!page) {
 - page_cache_sync_readahead(mapping,
 - ra, filp,
 - index, last_index – index);
 - page = find_get_page(mapping, index);
 - if (unlikely(page == NULL))
 - goto no_cached_page;
 - }
 - ……//此處省略約200行
 - }
 
塊裝置的address space
但是在讀取檔案系統元資料的時候,元資料對應的page會被加入到底層裸塊裝置的address space中。下麵程式碼的bdev_mapping指向塊裝置的address space,呼叫find_get_page_flags()後,一個新的page(如果page不在這個塊裝置的address space)就被建立並且插入到這個塊裝置的address space。
- static struct buffer_head *
 - __find_get_block_slow(struct block_device *bdev, sector_t block)
 - {
 - struct inode *bd_inode = bdev->bd_inode;
 - struct address_space *bd_mapping = bd_inode->i_mapping;
 - struct buffer_head *ret = NULL;
 - pgoff_t index;
 - struct buffer_head *bh;
 - struct buffer_head *head;
 - struct page *page;
 - int all_mapped = 1;
 - index = block >> (PAGE_CACHE_SHIFT – bd_inode->i_blkbits);
 - page = find_get_page_flags(bd_mapping, index, FGP_ACCESSED);
 - if (!page)
 - goto out;
 - ……//此處省略幾十行
 - }
 
兩份快取?
前面提到的情況是正常的操作流程,屬於普通檔案的page放在檔案的address space,元資料對應的page放在塊裝置的address space中,大家井水不犯河水,和平共處。但是世事難料,總有一些不按套路出牌的傢伙。檔案系統在塊裝置上歡快的跑著,如果有人繞過檔案系統,直接去操作塊裝置上屬於檔案的資料塊,這會出現什麼情況?如果這個資料塊已經在普通檔案的address space中,這次直接的資料塊修改能夠立馬體現到普通檔案的快取中嗎?
答案是直接修改塊裝置上塊會新建一個對應這個塊的page,並且這個page會被加到塊裝置的address space中。也就是同一個資料塊,在其所屬的普通檔案的address space中有一個對應的page。同時,在這個塊裝置的address space中也會有一個與其對應的page,所有的修改都更新到這個塊裝置address space中的page上。除非重新從磁碟上讀取這一塊的資料,否則普通檔案的檔案快取並不會感知這一修改。
實驗
口說無憑,實踐是檢驗真理的唯一標準。我在這裡準備了一個實驗,先將一個檔案的資料全部載入到page cache中,然後直接操作塊裝置修改這個檔案的資料塊,再讀取檔案的內容,看看有沒有被修改。
為了確認一個檔案的資料是否在page cache中,我先介紹一個有趣的工具—vmtouch,這個工具可以顯示出一個檔案有多少內容已經被載入到page cache。大家可以在github上獲取到它的原始碼,並自行編譯安裝
https://github.com/hoytech/vmtouch
現在開始我們的表演:
首先,我們找一個測試檔案,就拿我家目錄下的read.c來測試,這個檔案的內容就是一些凌亂的c程式碼。
➜ ~ cat read.c
- #include
 - #include
 - #include
 - #include
 - #include
 - char buf[4096] = {0};
 - int main(int argc, char *argv[])
 - {
 - int fd;
 - if (argc != 2) {
 - printf(“argument error.\n”);
 - return –1;
 - }
 - fd = open(argv[1], O_RDONLY);
 - if (fd < 0) {
 - perror(“open failed:”);
 - return –1;
 - }
 - read(fd, buf, 4096);
 - //read(fd, buf, 4096);
 - close(fd);
 - }
 - ➜ ~
 
接著執行vmtouch,看看這個檔案是否在page cache中了,由於這個檔案剛才被讀取過,所以檔案已經全部儲存在page cache中了。
- ➜ ~ vmtouch read.c
 - Files: 1
 - Directories: 0
 - Resident Pages: 1/1 4K/4K 100%
 - Elapsed: 0.000133 seconds
 - ➜ ~
 
        然後我透過debugfs找到read.c的資料塊,並且透過dd命令直接修改資料塊。
- Inode: 3945394 Type: regular Mode: 0644 Flags: 0x80000
 - Generation: 659328746 Version: 0x00000000:00000001
 - User: 0 Group: 0 Project: 0 Size: 386
 - File ACL: 0
 - Links: 1 Blockcount: 8
 - Fragment: Address: 0 Number: 0 Size: 0
 - ctime: 0x5ad2f108:60154d80 — Sun Apr 15 14:28:24 2018
 - atime: 0x5ad2f108:5db2f37c — Sun Apr 15 14:28:24 2018
 - mtime: 0x5ad2f108:5db2f37c — Sun Apr 15 14:28:24 2018
 - crtime: 0x5ad2f108:5db2f37c — Sun Apr 15 14:28:24 2018
 - Size of extra inode fields: 32
 - EXTENTS:
 - (0):2681460
 - ➜ ~ dd if=/dev/zero of=/dev/sda2 seek=2681460 bs=4096 count=1
 - 1+0 records in
 - 1+0 records out
 - 4096 bytes (4.1 kB, 4.0 KiB) copied, 0.000323738 s, 12.7 MB/s
 
修改已經完成,我們看看直接讀取這個檔案會怎麼樣。
- ➜ ~ cat read.c
 - #include
 - #include
 - #include
 - #include
 - #include
 - char buf[4096] = {0};
 - int main(int argc, char *argv[])
 - {
 - int fd;
 - if (argc != 2) {
 - printf(“argument error.\n”);
 - return –1;
 - }
 - fd = open(argv[1], O_RDONLY);
 - if (fd < 0) {
 - perror(“open failed:”);
 - return –1;
 - }
 - read(fd, buf, 4096);
 - //read(fd, buf, 4096);
 - close(fd);
 - }
 - ➜ ~ vmtouch read.c
 - Files: 1
 - Directories: 0
 - Resident Pages: 1/1 4K/4K 100%
 - Elapsed: 0.00013 seconds
 
檔案依然在page cache中,所以我們還是能夠讀取到檔案的內容。然而當我們drop cache以後,再讀取這個檔案,會發現檔案內容被清空。
- ➜ ~ vmtouch read.c
 - Files: 1
 - Directories: 0
 - Resident Pages: 1/1 4K/4K 100%
 - Elapsed: 0.00013 seconds
 - ➜ ~ echo 3 > /proc/sys/vm/drop_caches
 - ➜ ~ vmtouch read.c
 - Files: 1
 - Directories: 0
 - Resident Pages: 0/1 0/4K 0%
 - Elapsed: 0.000679 seconds
 - ➜ ~ cat read.c
 - ➜ ~
 
普通檔案的資料可以儲存在它的地址空間中,同時直接訪問塊裝置中此檔案的塊,也會將這個檔案的資料儲存在塊裝置的地址空間中。這兩份快取相互獨立,kernel並不會為這種非正常訪問同步兩份快取,從而避免了同步的開銷。
知識星球