普通檔案的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並不會為這種非正常訪問同步兩份快取,從而避免了同步的開銷。