Skip to content

值得您信賴的旅遊品牌 | 團體旅遊、自由行的專家‎

機場接送

Menu
  • 首頁
  • 旅遊天地
  • 裝潢設計
  • 環保清潔
  • 發燒車訊
Menu

Linux的文件系統及文件緩存知識點整理

Posted on 2021-02-192021-02-19 by admin

Linux的文件系统及文件缓存知识点整理

Linux的文件系統

文件系統的特點

  1. 文件系統要有嚴格的組織形式,使得文件能夠以塊為單位進行存儲。

  2. 文件系統中也要有索引區,用來方便查找一個文件分成的多個塊都存放在了什麼位置。

  3. 如果文件系統中有的文件是熱點文件,近期經常被讀取和寫入,文件系統應該有緩存層。

  4. 文件應該用文件夾的形式組織起來,方便管理和查詢。

  5. Linux內核要在自己的內存裏面維護一套數據結構,來保存哪些文件被哪些進程打開和使用。

    總體來說,文件系統的主要功能梳理如下:

ext系列的文件系統的格式

inode與塊的存儲

硬盤分成相同大小的單元,我們稱為塊(Block)。一塊的大小是扇區大小的整數倍,默認是4K。在格式化的時候,這個值是可以設定的。

一大塊硬盤被分成了一個個小的塊,用來存放文件的數據部分。這樣一來,如果我們像存放一個文件,就不用給他分配一塊連續的空間了。我們可以分散成一個個小塊進行存放。這樣就靈活得多,也比較容易添加、刪除和插入數據。

inode就是文件索引的意思,我們每個文件都會對應一個inode;一個文件夾就是一個文件,也對應一個inode。

inode數據結構如下:

struct ext4_inode {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size_lo;	/* Size in bytes */
	__le32	i_atime;	/* Access time */
	__le32	i_ctime;	/* Inode Change time */
	__le32	i_mtime;	/* Modification time */
	__le32	i_dtime;	/* Deletion Time */
	__le16	i_gid;		/* Low 16 bits of Group Id */
	__le16	i_links_count;	/* Links count */
	__le32	i_blocks_lo;	/* Blocks count */
	__le32	i_flags;	/* File flags */
......
	__le32	i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
	__le32	i_generation;	/* File version (for NFS) */
	__le32	i_file_acl_lo;	/* File ACL */
	__le32	i_size_high;
......
};

inode裏面有文件的讀寫權限i_mode,屬於哪個用戶i_uid,哪個組i_gid,大小是多少i_size_io,佔用多少個塊i_blocks_io,i_atime是access time,是最近一次訪問文件的時間;i_ctime是change time,是最近一次更改inode的時間;i_mtime是modify time,是最近一次更改文件的時間等。

所有的文件都是保存在i_block裏面。具體保存規則由EXT4_N_BLOCKS決定,EXT4_N_BLOCKS有如下的定義:

#define	EXT4_NDIR_BLOCKS		12
#define	EXT4_IND_BLOCK			EXT4_NDIR_BLOCKS
#define	EXT4_DIND_BLOCK			(EXT4_IND_BLOCK + 1)
#define	EXT4_TIND_BLOCK			(EXT4_DIND_BLOCK + 1)
#define	EXT4_N_BLOCKS			(EXT4_TIND_BLOCK + 1)

在ext2和ext3中,其中前12項直接保存了塊的位置,也就是說,我們可以通過i_block[0-11],直接得到保存文件內容的塊。

但是,如果一個文件比較大,12塊放不下。當我們用到i_block[12]的時候,就不能直接放數據塊的位置了,要不然i_block很快就會用完了。

那麼可以讓i_block[12]指向一個塊,這個塊裏面不放數據塊,而是放數據塊的位置,這個塊我們稱為間接塊。如果文件再大一些,i_block[13]會指向一個塊,我們可以用二次間接塊。二次間接塊裏面存放了間接塊的位置,間接塊裏面存放了數據塊的位置,數據塊裏面存放的是真正的數據。如果文件再大點,那麼i_block[14]同理。

這裏面有一個非常顯著的問題,對於大文件來講,我們要多次讀取硬盤才能找到相應的塊,這樣訪問速度就會比較慢。

為了解決這個問題,ext4做了一定的改變。它引入了一個新的概念,叫作Extents。比方說,一個文件大小為128M,如果使用4k大小的塊進行存儲,需要32k個塊。如果按照ext2或者ext3那樣散着放,數量太大了。但是Extents可以用於存放連續的塊,也就是說,我們可以把128M放在一個Extents裏面。這樣的話,對大文件的讀寫性能提高了,文件碎片也減少了。

Exents是一個樹狀結構:

每個節點都有一個頭,ext4_extent_header可以用來描述某個節點。

struct ext4_extent_header {
	__le16	eh_magic;	/* probably will support different formats */
	__le16	eh_entries;	/* number of valid entries */
	__le16	eh_max;		/* capacity of store in entries */
	__le16	eh_depth;	/* has tree real underlying blocks? */
	__le32	eh_generation;	/* generation of the tree */
};

eh_entries表示這個節點裏面有多少項。這裏的項分兩種,如果是恭弘=叶 恭弘子節點,這一項會直接指向硬盤上的連續塊的地址,我們稱為數據節點ext4_extent;如果是分支節點,這一項會指向下一層的分支節點或者恭弘=叶 恭弘子節點,我們稱為索引節點ext4_extent_idx。這兩種類型的項的大小都是12個byte。

/*
 * This is the extent on-disk structure.
 * It's used at the bottom of the tree.
 */
struct ext4_extent {
	__le32	ee_block;	/* first logical block extent covers */
	__le16	ee_len;		/* number of blocks covered by extent */
	__le16	ee_start_hi;	/* high 16 bits of physical block */
	__le32	ee_start_lo;	/* low 32 bits of physical block */
};
/*
 * This is index on-disk structure.
 * It's used at all the levels except the bottom.
 */
struct ext4_extent_idx {
	__le32	ei_block;	/* index covers logical blocks from 'block' */
	__le32	ei_leaf_lo;	/* pointer to the physical block of the next *
				 * level. leaf or next index could be there */
	__le16	ei_leaf_hi;	/* high 16 bits of physical block */
	__u16	ei_unused;
};

如果文件不大,inode裏面的i_block中,可以放得下一個ext4_extent_header和4項ext4_extent。所以這個時候,eh_depth為0,也即inode裏面的就是恭弘=叶 恭弘子節點,樹高度為0。

如果文件比較大,4個extent放不下,就要分裂成為一棵樹,eh_depth>0的節點就是索引節點,其中根節點深度最大,在inode中。最底層eh_depth=0的是恭弘=叶 恭弘子節點。

除了根節點,其他的節點都保存在一個塊4k裏面,4k扣除ext4_extent_header的12個byte,剩下的能夠放340項,每個extent最大能表示128MB的數據,340個extent會使你的表示的文件達到42.5GB。

inode位圖和塊位圖

inode的位圖大小為4k,每一位對應一個inode。如果是1,表示這個inode已經被用了;如果是0,則表示沒被用。block的位圖同理。

在Linux操作系統裏面,想要創建一個新文件,會調用open函數,並且參數會有O_CREAT。這表示當文件找不到的時候,我們就需要創建一個。那麼open函數的調用過程大致是:要打開一個文件,先要根據路徑找到文件夾。如果發現文件夾下面沒有這個文件,同時又設置了O_CREAT,就說明我們要在這個文件夾下面創建一個文件。

創建一個文件,那麼就需要創建一個inode,那麼就會從文件系統裏面讀取inode位圖,然後找到下一個為0的inode,就是空閑的inode。對於block位圖,在寫入文件的時候,也會有這個過程。

文件系統的格式

數據塊的位圖是放在一個塊裏面的,共4k。每位表示一個數據塊,共可以表示$4 * 1024 * 8 = 2{15}$個數據塊。如果每個數據塊也是按默認的4K,最大可以表示空間為$2{15} * 4 * 1024 = 2^{27}$個byte,也就是128M,那麼顯然是不夠的。

這個時候就需要用到塊組,數據結構為ext4_group_desc,這裏面對於一個塊組裡的inode位圖bg_inode_bitmap_lo、塊位圖bg_block_bitmap_lo、inode列表bg_inode_table_lo,都有相應的成員變量。

這樣一個個塊組,就基本構成了我們整個文件系統的結構。因為塊組有多個,塊組描述符也同樣組成一個列表,我們把這些稱為塊組描述符表。

我們還需要有一個數據結構,對整個文件系統的情況進行描述,這個就是超級塊ext4_super_block。裏面有整個文件系統一共有多少inode,s_inodes_count;一共有多少塊,s_blocks_count_lo,每個塊組有多少inode,s_inodes_per_group,每個塊組有多少塊,s_blocks_per_group等。這些都是這類的全局信息。

最終,整個文件系統格式就是下面這個樣子。

默認情況下,超級塊和塊組描述符表都有副本保存在每一個塊組裡面。防止這些數據丟失了,導致整個文件系統都打不開了。

由於如果每個塊組裡面都保存一份完整的塊組描述符表,一方面很浪費空間;另一個方面,由於一個塊組最大128M,而塊組描述符表裡面有多少項,這就限制了有多少個塊組,128M * 塊組的總數目是整個文件系統的大小,就被限制住了。

因此引入Meta Block Groups特性。

首先,塊組描述符表不會保存所有塊組的描述符了,而是將塊組分成多個組,我們稱為元塊組(Meta Block Group)。每個元塊組裡面的塊組描述符表僅僅包括自己的,一個元塊組包含64個塊組,這樣一個元塊組中的塊組描述符表最多64項。

我們假設一共有256個塊組,原來是一個整的塊組描述符表,裏面有256項,要備份就全備份,現在分成4個元塊組,每個元塊組裡面的塊組描述符表就只有64項了,這就小多了,而且四個元塊組自己備份自己的。

根據圖中,每一個元塊組包含64個塊組,塊組描述符表也是64項,備份三份,在元塊組的第一個,第二個和最後一個塊組的開始處。

如果開啟了sparse_super特性,超級塊和塊組描述符表的副本只會保存在塊組索引為0、3、5、7的整數冪里。所以上圖的超級塊只在索引為0、3、5、7等的整數冪里。

目錄的存儲格式

其實目錄本身也是個文件,也有inode。inode裏面也是指向一些塊。和普通文件不同的是,普通文件的塊裏面保存的是文件數據,而目錄文件的塊裏面保存的是目錄裏面一項一項的文件信息。這些信息我們稱為ext4_dir_entry。

在目錄文件的塊中,最簡單的保存格式是列表,每一項都會保存這個目錄的下一級的文件的文件名和對應的inode,通過這個inode,就能找到真正的文件。第一項是“.”,表示當前目錄,第二項是“…”,表示上一級目錄,接下來就是一項一項的文件名和inode。

如果在inode中設置EXT4_INDEX_FL標誌,那麼就表示根據索引查找文件。索引項會維護一個文件名的哈希值和數據塊的一個映射關係。

如果我們要查找一個目錄下面的文件名,可以通過名稱取哈希。如果哈希能夠匹配上,就說明這個文件的信息在相應的塊裏面。然後打開這個塊,如果裏面不再是索引,而是索引樹的恭弘=叶 恭弘子節點的話,那裡面還是ext4_dir_entry的列表,我們只要一項一項找文件名就行。通過索引樹,我們可以將一個目錄下面的N多的文件分散到很多的塊裏面,可以很快地進行查找。

Linux中的文件緩存

ext4文件系統層

對於ext4文件系統來講,內核定義了一個ext4_file_operations。

const struct file_operations ext4_file_operations = {
......
	.read_iter	= ext4_file_read_iter,
	.write_iter	= ext4_file_write_iter,
......
}

ext4_file_read_iter會調用generic_file_read_iter,ext4_file_write_iter會調用__generic_file_write_iter。

ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
......
    if (iocb->ki_flags & IOCB_DIRECT) {
......
        struct address_space *mapping = file->f_mapping;
......
        retval = mapping->a_ops->direct_IO(iocb, iter);
    }
......
    retval = generic_file_buffered_read(iocb, iter, retval);
}


ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
......
    if (iocb->ki_flags & IOCB_DIRECT) {
......
        written = generic_file_direct_write(iocb, from);
......
    } else {
......
		written = generic_perform_write(file, from, iocb->ki_pos);
......
    }
}

generic_file_read_iter和__generic_file_write_iter有相似的邏輯,就是要區分是否用緩存。因此,根據是否使用內存做緩存,我們可以把文件的I/O操作分為兩種類型。

第一種類型是緩存I/O。大多數文件系統的默認I/O操作都是緩存I/O。對於讀操作來講,操作系統會先檢查,內核的緩衝區有沒有需要的數據。如果已經緩存了,那就直接從緩存中返回;否則從磁盤中讀取,然後緩存在操作系統的緩存中。對於寫操作來講,操作系統會先將數據從用戶空間複製到內核空間的緩存中。這時對用戶程序來說,寫操作就已經完成。至於什麼時候再寫到磁盤中由操作系統決定,除非顯式地調用了sync同步命令。

第二種類型是直接IO,就是應用程序直接訪問磁盤數據,而不經過內核緩衝區,從而減少了在內核緩存和用戶程序之間數據複製。

如果在寫的邏輯__generic_file_write_iter裏面,發現設置了IOCB_DIRECT,則調用generic_file_direct_write,裏面同樣會調用address_space的direct_IO的函數,將數據直接寫入硬盤。

帶緩存的寫入操作

我們先來看帶緩存寫入的函數generic_perform_write。

ssize_t generic_perform_write(struct file *file,
				struct iov_iter *i, loff_t pos)
{
	struct address_space *mapping = file->f_mapping;
	const struct address_space_operations *a_ops = mapping->a_ops;
	do {
		struct page *page;
		unsigned long offset;	/* Offset into pagecache page */
		unsigned long bytes;	/* Bytes to write to page */
		status = a_ops->write_begin(file, mapping, pos, bytes, flags,
						&page, &fsdata);
		copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
		flush_dcache_page(page);
		status = a_ops->write_end(file, mapping, pos, bytes, copied,
						page, fsdata);
		pos += copied;
		written += copied;


		balance_dirty_pages_ratelimited(mapping);
	} while (iov_iter_count(i));
}

循環中主要做了這幾件事:

  • 對於每一頁,先調用address_space的write_begin做一些準備;
  • 調用iov_iter_copy_from_user_atomic,將寫入的內容從用戶態拷貝到內核態的頁中;
  • 調用address_space的write_end完成寫操作;
  • 調用balance_dirty_pages_ratelimited,看臟頁是否太多,需要寫回硬盤。所謂臟頁,就是寫入到緩存,但是還沒有寫入到硬盤的頁面。

對於第一步,調用的是ext4_write_begin來說,主要做兩件事:

第一做日誌相關的工作。

ext4是一種日誌文件系統,是為了防止突然斷電的時候的數據丟失,引入了日誌(Journal)模式。日誌文件系統比非日誌文件系統多了一個Journal區域。文件在ext4中分兩部分存儲,一部分是文件的元數據,另一部分是數據。元數據和數據的操作日誌Journal也是分開管理的。你可以在掛載ext4的時候,選擇Journal模式。這種模式在將數據寫入文件系統前,必須等待元數據和數據的日誌已經落盤才能發揮作用。這樣性能比較差,但是最安全。

另一種模式是order模式。這個模式不記錄數據的日誌,只記錄元數據的日誌,但是在寫元數據的日誌前,必須先確保數據已經落盤。這個折中,是默認模式。

還有一種模式是writeback,不記錄數據的日誌,僅記錄元數據的日誌,並且不保證數據比元數據先落盤。這個性能最好,但是最不安全。

第二調用grab_cache_page_write_begin來,得到應該寫入的緩存頁。

struct page *grab_cache_page_write_begin(struct address_space *mapping,
					pgoff_t index, unsigned flags)
{
	struct page *page;
	int fgp_flags = FGP_LOCK|FGP_WRITE|FGP_CREAT;
	page = pagecache_get_page(mapping, index, fgp_flags,
			mapping_gfp_mask(mapping));
	if (page)
		wait_for_stable_page(page);
	return page;
}

在內核中,緩存以頁為單位放在內存裏面,每一個打開的文件都有一個struct file結構,每個struct file結構都有一個struct address_space用於關聯文件和內存,就是在這個結構裏面,有一棵樹,用於保存所有與這個文件相關的的緩存頁。

對於第二步,調用iov_iter_copy_from_user_atomic。先將分配好的頁面調用kmap_atomic映射到內核裏面的一個虛擬地址,然後將用戶態的數據拷貝到內核態的頁面的虛擬地址中,調用kunmap_atomic把內核裏面的映射刪除。

size_t iov_iter_copy_from_user_atomic(struct page *page,
		struct iov_iter *i, unsigned long offset, size_t bytes)
{
	char *kaddr = kmap_atomic(page), *p = kaddr + offset;
	iterate_all_kinds(i, bytes, v,
		copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
		memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
				 v.bv_offset, v.bv_len),
		memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
	)
	kunmap_atomic(kaddr);
	return bytes;
}

第三步中,調用ext4_write_end完成寫入。這裏面會調用ext4_journal_stop完成日誌的寫入,會調用block_write_end->__block_commit_write->mark_buffer_dirty,將修改過的緩存標記為臟頁。可以看出,其實所謂的完成寫入,並沒有真正寫入硬盤,僅僅是寫入緩存后,標記為臟頁。

第四步,調用 balance_dirty_pages_ratelimited,是回寫臟頁。

/**
 * balance_dirty_pages_ratelimited - balance dirty memory state
 * @mapping: address_space which was dirtied
 *
 * Processes which are dirtying memory should call in here once for each page
 * which was newly dirtied.  The function will periodically check the system's
 * dirty state and will initiate writeback if needed.
  */
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
	struct inode *inode = mapping->host;
	struct backing_dev_info *bdi = inode_to_bdi(inode);
	struct bdi_writeback *wb = NULL;
	int ratelimit;
......
	if (unlikely(current->nr_dirtied >= ratelimit))
		balance_dirty_pages(mapping, wb, current->nr_dirtied);
......
}

在balance_dirty_pages_ratelimited裏面,發現臟頁的數目超過了規定的數目,就調用balance_dirty_pages->wb_start_background_writeback,啟動一個背後線程開始回寫。

另外還有幾種場景也會觸發回寫:

  • 用戶主動調用sync,將緩存刷到硬盤上去,最終會調用wakeup_flusher_threads,同步臟頁;
  • 當內存十分緊張,以至於無法分配頁面的時候,會調用free_more_memory,最終會調用wakeup_flusher_threads,釋放臟頁;
  • 臟頁已經更新了較長時間,時間上超過了設定時間,需要及時回寫,保持內存和磁盤上數據一致性。

帶緩存的讀操作

看帶緩存的讀,對應的是函數generic_file_buffered_read。

static ssize_t generic_file_buffered_read(struct kiocb *iocb,
		struct iov_iter *iter, ssize_t written)
{
	struct file *filp = iocb->ki_filp;
	struct address_space *mapping = filp->f_mapping;
	struct inode *inode = mapping->host;
	for (;;) {
		struct page *page;
		pgoff_t end_index;
		loff_t isize;
		page = find_get_page(mapping, index);
		if (!page) {
			if (iocb->ki_flags & IOCB_NOWAIT)
				goto would_block;
			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);
		}
		/*
		 * Ok, we have the page, and it's up-to-date, so
		 * now we can copy it to user space...
		 */
		ret = copy_page_to_iter(page, offset, nr, iter);
    }
}

在generic_file_buffered_read函數中,我們需要先找到page cache裏面是否有緩存頁。如果沒有找到,不但讀取這一頁,還要進行預讀,這需要在page_cache_sync_readahead函數中實現。預讀完了以後,再試一把查找緩存頁。

如果第一次找緩存頁就找到了,我們還是要判斷,是不是應該繼續預讀;如果需要,就調用page_cache_async_readahead發起一個異步預讀。

最後,copy_page_to_iter會將內容從內核緩存頁拷貝到用戶內存空間。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※推薦台中搬家公司優質服務,可到府估價

好站推薦

  • 健康醫療 減重知識專區
  • 婚紗世界 婚紗攝影寫真網
  • 成人話題 未滿18請勿進入
  • 流行時尚 時下流行愛美情報
  • 理財資訊 當舖借貸信用卡各式理財方法
  • 生活情報 各行各業情報資訊
  • 科技資訊 工業電子3C產品
  • 網路資訊 新奇趣味爆笑內容
  • 美食分享 全台各式名產 伴手禮
  • 裝潢設計 買屋賣屋裝修一羅框
  • 視覺設計 T恤、團體服、制服、polo衫

近期文章

  • 奧方地產11.23億元競得廣州增城1宗商住用地
  • 廣州高新投資20.18億元競得廣州黃埔區1宗商業用地
  • 廣州開發投資聯合體5.91億元競得廣州黃埔1宗商業用地
  • 廣東開新睿智生物1961萬元競得廣州1宗工業用地
  • 廣州130.87億元出讓7宗地塊 龍湖、香江控股、越秀各競得1宗

標籤

USB CONNECTOR  南投搬家公司費用 古典家具推薦 台中一中住宿 台中一中民宿 台中室內設計 台中室內設計師 台中室內設計推薦 台中搬家 台中電動車 台北網頁設計 台東伴手禮 台東名產 地板施工 大圖輸出 如何寫文案 婚禮錄影 宜蘭民宿 家具工廠推薦 家具訂製工廠推薦 家具訂製推薦 實木地板 復刻家具推薦 新竹婚宴會館 木地板 木質地板 柚木地板 桃園機場接送 桃園自助婚紗 沙發修理 沙發換皮 海島型木地板 牛軋糖 租車 網站設計 網頁設計 網頁設計公司 貨運 超耐磨木地板 銷售文案 隱形鐵窗 電動車 馬賽克拼貼 馬賽克磁磚 馬賽克磚

彙整

  • 2021 年 3 月
  • 2021 年 2 月
  • 2021 年 1 月
  • 2020 年 12 月
  • 2020 年 11 月
  • 2020 年 10 月
  • 2020 年 9 月
  • 2020 年 8 月
  • 2020 年 7 月
  • 2020 年 6 月
  • 2020 年 5 月
  • 2020 年 4 月
  • 2020 年 3 月
  • 2020 年 2 月
  • 2020 年 1 月
  • 2019 年 12 月
  • 2019 年 11 月
  • 2019 年 10 月
  • 2019 年 9 月
  • 2019 年 8 月
  • 2019 年 7 月
  • 2019 年 6 月
  • 2019 年 5 月
  • 2019 年 4 月
  • 2019 年 3 月
  • 2019 年 2 月
  • 2019 年 1 月
  • 2018 年 12 月
©2021 值得您信賴的旅遊品牌 | 團體旅遊、自由行的專家‎ | Built using WordPress and Responsive Blogily theme by Superb