文件系統中對頁高速緩存的操作


本文從read函數入手,主要講述從頁緩沖,一直到具體的塊請求被提交給塊設備驅動程序的過程,以下是本文講述的一張概圖,也是對本文的一個概括,可以結合本圖,首先由一個從全局上有個清楚的認識,然后再去查看具體的代碼,當然本文只是從大體流程上對頁緩沖的處理流程進行分析,還有很多小的細節沒有搞清楚,后面還需要繼續研究。

1.具體文件系統

我們知道通用文件系統也就是虛擬文件系統,只是定義了一組接口,具體的實現是由具體文件系統來實現的。我們以ext2文件系統為例,來查看。

const struct file_operations ext2_file_operations = {
	.llseek		= generic_file_llseek,
	.read		= do_sync_read,
	.write		= do_sync_write,
	.aio_read	= generic_file_aio_read,
	.aio_write	= generic_file_aio_write,
	.unlocked_ioctl = ext2_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl	= ext2_compat_ioctl,
#endif
	.mmap		= generic_file_mmap,
	.open		= generic_file_open,
	.release	= ext2_release_file,
	.fsync		= simple_fsync,
	.splice_read	= generic_file_splice_read,
	.splice_write	= generic_file_splice_write,
};

1.1.可以看到ext2的read其實際上執行的是do_sync_read()函數。

ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
	struct iovec iov = { .iov_base = buf, .iov_len = len };
	struct kiocb kiocb;
	ssize_t ret;

	init_sync_kiocb(&kiocb, filp);
	kiocb.ki_pos = *ppos;
	kiocb.ki_left = len;

	for (;;) {
		ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos); //讀操作開始
		if (ret != -EIOCBRETRY)
			break;
		wait_on_retry_sync_kiocb(&kiocb);
	}

	if (-EIOCBQUEUED == ret)
		ret = wait_on_sync_kiocb(&kiocb);
	*ppos = kiocb.ki_pos;
	return ret;

1.相同的參數沿着上一層的讀函數傳遞下來,這些參數有:文件指針filp,指向內存緩沖區的指針buf(要讀取的內容將被保存在這個緩沖區中),讀入的字符數count以及從文件的哪個位置開始讀ppos.
2.這個函數一進來就對struct iovec進行初始化,由其初始化的代碼可知,該結構體包含用戶空間緩沖區的地址和長度,讀入的數據就被存入到了這個緩沖區。
3.接下來的struct kiocb,用來表示內核的I/O控制塊。它用來跟蹤正在運行的同步和異步的I/O操作。在這里用文件指針來初始化kiocb,即將正在進行的I/O操作與該文件對象關聯起來。
4.接下來就進入實際的讀操作。由上面的ext2_file_operation可知其調用的是generic_file_aio_read函數
注意
kiocb和iovec是Linux內核中協助異步I/O操作的兩個數據類型。當進程希望執行輸入輸出操作,但並不需要等一會兒就馬上得到操作結果時,異步I/O是非常合適的。內核I/O控制塊(kiocb)是輔助管理I/O向量所需要的結構,它幫助管理I/O向量如何異步的操作以及如何操作。

1.2.generic_file_aio_read函數

在該函數中,分為兩路:一路是當設置了O_DIRECT標志:

if (filp->f_flags & O_DIRECT) {
		loff_t size;
		struct address_space *mapping;
		struct inode *inode;

		mapping = filp->f_mapping;
		inode = mapping->host;
		if (!count)
			goto out; /* skip atime */
		size = i_size_read(inode);
		if (pos < size) {
			retval = filemap_write_and_wait_range(mapping, pos,
					pos + iov_length(iov, nr_segs) - 1);
			if (!retval) {
				retval = mapping->a_ops->direct_IO(READ, iocb,
							iov, pos, nr_segs);
			}
			if (retval > 0)
				*ppos = pos + retval;
			if (retval) {
				file_accessed(filp);
				goto out;
			}
		}

當設置了這個標志時,表示讀操作是直接I/O,其可以繞過頁緩沖,是某些設備非常有用的特性。大多數的文件I/O把我們的訪問路徑視為頁緩沖,它的效率很高。所以我們來看另一條路,即走頁緩沖的那一條路

for (seg = 0; seg < nr_segs; seg++) {
		read_descriptor_t desc;
//將iovec結構轉換成read_descriptor_t的結構
		desc.written = 0;
		desc.arg.buf = iov[seg].iov_base;
		desc.count = iov[seg].iov_len;
		if (desc.count == 0)
			continue;
		desc.error = 0;
		do_generic_file_read(filp, ppos, &desc, file_read_actor);
		retval += desc.written;
		if (desc.error) {
			retval = retval ?: desc.error;
			break;
		}
		if (desc.count > 0)
			break;
	}
out:
	return retval;

1.首先將iovec結構體轉換成read_descriptor_t的結構體,read_descriptor_t結構體記錄讀的狀態

typedef struct {
	size_t written;  //存放不斷變換着的已傳送的字節數
	size_t count;  //存放不斷變化着的未傳送的字節數
	union {
		char __user *buf;   //緩沖區的當前位置
		void *data;
	} arg;
	int error;   //讀操作期間遇到的任何的錯誤碼
} read_descriptor_t;

初始化完read_descriptor_t之后,進入read的內部do_generic_file_read()函數,由上面的代碼可知,do_generic_read函數執行完畢之后,會計算一系列的已讀字節數,最后返回給上層調用。

2.追蹤頁緩存

2.1.do_generic_file_read函數

static void do_generic_file_read(struct file *filp, loff_t *ppos,
		read_descriptor_t *desc, read_actor_t actor)
{
	struct address_space *mapping = filp->f_mapping;  //獲取頁高速緩存
	struct inode *inode = mapping->host;//獲取inode
	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
    . ............................
    .............................

在這個函數中首先通過 filp->f_mapping去獲取address_space,filp->f_ra是一個存放文件預讀狀態地址的結構。所以就把文件的讀取轉換成了對頁緩沖的讀取。

	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 + desc->count + PAGE_CACHE_SIZE-1) >> PAGE_CACHE_SHIFT;  //下次讀操作完成后的位置
	offset = *ppos & ~PAGE_CACHE_MASK;  //請求的第一個字節在頁內的偏移量

index為對應頁緩存中的頁號,而offset是對應的頁內偏移,接下來就是在address_space中根據index的頁號,找對應的頁。

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;
		}
		if (PageReadahead(page)) {
			page_cache_async_readahead(mapping,
					ra, filp, page,
					index, last_index - index);
		}
		if (!PageUptodate(page)) {
			if (inode->i_blkbits == PAGE_CACHE_SHIFT ||
					!mapping->a_ops->is_partially_uptodate)
				goto page_not_up_to_date;
			if (!trylock_page(page))
				goto page_not_up_to_date;
			if (!mapping->a_ops->is_partially_uptodate(page,
								desc, offset))
				goto page_not_up_to_date_locked;
			unlock_page(page);
		}

find_get_page()使用地址空間的基樹查找索引為index的頁。嘗試去找到第一個被請求的頁。如果這個頁不在頁緩存中,就跳轉到標號no_cached_page處,如果該頁不是最新的,就跳轉到標號page_not_up_to_date_locked處,如果在嘗試去獲取這個頁面的獨占權,即加鎖的時候,沒有獲取成功,則跳轉到page_not_up_to_date處。

page_ok:  //表示頁已經在頁高速緩存中了
		isize = i_size_read(inode);   //對應的文件的大小
		end_index = (isize - 1) >> PAGE_CACHE_SHIFT;   //最后的頁緩存序號
		if (unlikely(!isize || index > end_index)) {
			page_cache_release(page);
			goto out;
		}

		/* nr is the maximum number of bytes to copy from this page */
		nr = PAGE_CACHE_SIZE;
		if (index == end_index) {
			nr = ((isize - 1) & ~PAGE_CACHE_MASK) + 1;
			if (nr <= offset) {
				page_cache_release(page);
				goto out;
			}
		}
		nr = nr - offset;
.............
//對index和offset進行處理,目的是選擇下一個要獲取的頁。
		ret = actor(desc, page, offset, nr);
		offset += ret;
		index += offset >> PAGE_CACHE_SHIFT;
		offset &= ~PAGE_CACHE_MASK;
		prev_offset = offset;

		page_cache_release(page);  //釋放這個頁,數據已經從內核態拷貝到了用戶空間,
		if (ret == nr && desc->count)//nr表示需要拷貝的字節數,如果沒有拷貝完成,continue
			continue;
		goto out;

當頁面不在緩沖區中時,就要從文件系統中獲取數據

page_not_up_to_date_locked:
		/* Did it get truncated before we got the lock? */
  //有可能在鎖頁面的時候`有其它的進程將頁面移除了頁緩存區
         //在這種情況下:將page解鎖`並減少它的使用計數,重新循環```
         //重新進入循環后,在頁緩存區找不到對應的page.就會重新分配一個新的page
		if (!page->mapping) {
			unlock_page(page);
			page_cache_release(page);
			continue;
		}

		/* Did somebody else fill it already? */
         //在加鎖的時候,有其它的進程完成了從文件系統到具體頁面的映射?
         //在這種情況下,返回到page_ok.直接將頁面上的內容copy到用戶空間即可
		if (PageUptodate(page)) {
			unlock_page(page);
			goto page_ok;
		}

當該頁不是最新的時候,就再檢查一次;如果該頁現在是最新的,就立刻返回給標號page_ok處,(注釋中解釋了原因)否則,將去獲取該頁的獨占訪問;這將有可能導致睡眠,知道獲得對該頁的獨占訪問。獲的頁的訪問權限后,來看看這個頁是否企圖從頁緩存中刪除自己。(另一個進程可能會刪除它),如果是,再返回到for循環頂部前趕緊繼續向前。如果依然存在並且現在是最新的,就對頁解鎖並跳轉到標號page_ok處。接下來就要真正的開始讀取頁面了

readpage:
		/* Start the actual read. The read will unlock the page. */
		error = mapping->a_ops->readpage(filp, page);  //調用具體的readpage函數,在后面會分析。

		if (unlikely(error)) {   //如果發生了AOP_TRUNCATED_PAGE錯誤,則回到find_page重新進行獲取
			if (error == AOP_TRUNCATED_PAGE) {
				page_cache_release(page);
				goto find_page;
			}
			goto readpage_error;
		}
//如果PG_uptodata標志仍然末設置.就一直等待,一直到page不處於鎖定狀態
         //  在將文件系統的內容讀入page之前,page一直是處理Lock狀態的。一直到
         //讀取完成后,才會將頁面解鎖
		if (!PageUptodate(page)) {
			error = lock_page_killable(page);
			if (unlikely(error))
				goto readpage_error;
			if (!PageUptodate(page)) {
				if (page->mapping == NULL) {
					/*
					 * invalidate_inode_pages got it
					 */
					unlock_page(page);
					page_cache_release(page);
					goto find_page;
				}
				unlock_page(page);
				shrink_readahead_size_eio(filp, ra);
				error = -EIO;
				goto readpage_error;
			}
			unlock_page(page);
		}

		goto page_ok;  //讀取成功

在這里調用實際的讀page的操作mpping->a_ops->readpage()對該頁進行讀取。如果成功讀取了一個頁,檢查其是否是最新的,如果是最新的,則跳轉到標號page_ok處。如果發生了同步讀錯誤,就設置其error,並跳轉到readpage_error處。

no_cached_page:
		/*
		 * Ok, it wasn't cached, so we need to create a new
		 * page..
		 */
  //新分匹一個頁面
		page = page_cache_alloc_cold(mapping);
		if (!page) {
			desc->error = -ENOMEM;
			goto out;
		}
 //將分得的頁加到頁緩存區和LRU
		error = add_to_page_cache_lru(page, mapping,
						index, GFP_KERNEL);
//向緩存中添加頁時,如果因為頁已經存在而產生錯誤,就跳轉到find_Page處再試一次
		if (error) {
			page_cache_release(page);
			if (error == -EEXIST)
				goto find_page;
			desc->error = error;//如果不是已經存在的錯誤,而是其他的錯誤,則記錄該錯誤,並跳出for循環
			goto out;
		}
		goto readpage;//當成功的分配頁,並將頁加入頁緩存和LRU后,就讓指針page指向新頁,並挑戰到readpage,開始讀取。
	}

這里主要講述了當沒有改頁時時如何處理的。最后我們來看下do_generic_file_read函數的out

out:
	ra->prev_pos = prev_index;
	ra->prev_pos <<= PAGE_CACHE_SHIFT;
	ra->prev_pos |= prev_offset;

	*ppos = ((loff_t)index << PAGE_CACHE_SHIFT) + offset;  //計算實際的偏移量
	file_accessed(filp);//更新文件的最后一次訪問時間。
}

這個函數終於分析完了,它描述了頁緩存的核心,這使得Linux內核不用考慮底層文件系統的結構,用頁緩存就可以緩存各種各樣的頁。一次,頁緩存能夠同時容納來自MINX,EXT2等的頁。

3.readpage()函數

頁緩存維護着文件系統層之間的差異,每個特定的文件系統都需要維護自己的readpage函數,對於ext2文件系統而言

const struct address_space_operations ext2_aops = {
	.readpage		= ext2_readpage,
	.readpages		= ext2_readpages,
	.writepage		= ext2_writepage,
	.sync_page		= block_sync_page,
	.write_begin		= ext2_write_begin,
	.write_end		= generic_write_end,
	.bmap			= ext2_bmap,
	.direct_IO		= ext2_direct_IO,
	.writepages		= ext2_writepages,
	.migratepage		= buffer_migrate_page,
	.is_partially_uptodate	= block_is_partially_uptodate,
	.error_remove_page	= generic_error_remove_page,
};

在ext2_readpage函數中。調用mpage_readpage()

static int ext2_readpage(struct file *file, struct page *page)
{
	return mpage_readpage(page, ext2_get_block);
}

這個函數的第二個參數是一個回調函數ext2_get_block(),這個函數將文件起始的塊號轉換成文件系統的邏輯塊號,在這里要介紹一個結構體struct bio

struct bio {  
sector_t        bi_sector;//該bio結構所要傳輸的第一個(512字節)扇區:磁盤的位置  
struct bio        *bi_next;    //請求鏈表  
struct block_device    *bi_bdev;//相關的塊設備  
unsigned long        bi_flags//狀態和命令標志  
unsigned long        bi_rw; //讀寫  
unsigned short        bi_vcnt;//bio_vesc偏移的個數  
unsigned short        bi_idx;    //bi_io_vec的當前索引  
unsigned short        bi_phys_segments;//結合后的片段數目  
unsigned short        bi_hw_segments;//重映射后的片段數目  
unsigned int        bi_size;    //I/O計數  
unsigned int        bi_hw_front_size;//第一個可合並的段大小;  
unsigned int        bi_hw_back_size;//最后一個可合並的段大小  
unsigned int        bi_max_vecs;    //bio_vecs數目上限  
struct bio_vec        *bi_io_vec;    //bio_vec鏈表:內存的位置  
bio_end_io_t        *bi_end_io;//I/O完成方法  
atomic_t        bi_cnt; //使用計數  
void            *bi_private; //擁有者的私有方法  
bio_destructor_t    *bi_destructor;    //銷毀方法  
};  

對於這個結構體理解還不夠,其大概的意思就是bio結構記錄着與塊I/O相關的信息,既描述了磁盤的位置,又描述了內存的位置,是上層內核與下層驅動的連接紐帶,故當上層內核與下層的驅動層連接時,這個bio結構體就顯得很重要了。

int mpage_readpage(struct page *page, get_block_t get_block)
{
	struct bio *bio = NULL;
	sector_t last_block_in_bio = 0;
	struct buffer_head map_bh;
	unsigned long first_logical_block = 0;

	map_bh.b_state = 0;
	map_bh.b_size = 0;
	bio = do_mpage_readpage(bio, page, 1, &last_block_in_bio,
			&map_bh, &first_logical_block, get_block);
	if (bio)
		mpage_bio_submit(READ, bio);
	return 0;
}

do_mpage_readpage()函數完成的主要工作就是將address_space的邏輯頁轉換成由實際的頁和塊組成的bio結構體,bio結構記錄着與塊相關的信息。最后將新創建的bio發送給mpage_bio_submit()函數。

4.講到這里是時候對讀操作做個總結了

1.從open返回的文件描述符,得到索引節點。
2.文件系統層在內存的頁緩存中檢查和給定索引節點對應的一個或多個頁。
3.如果沒有找到所需的頁,文件系統層使用特定文件系統的驅動程序把所請求的文件轉換成特定設備上的I/O塊。
4.在頁緩存的address_space中為頁分配空間,通過struct bio,把新分配的頁與塊設備上的扇區對應起來。
通過上面的mpage_readpage只是把bio結構建立起來,此時頁中還是沒有數據的。這時,文件系統層需要塊設備的驅動程序來完成到設備的實際接口。這時由mpage_bio_submit()中的submit_bio()函數來完成的。(對於后面的還沒有仔細去看,還需要仔細去研究)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM