作者:嶺南
Question
标準GNU工具coreutils中有倆程式df / du,他們都可以檢視磁盤的使用情況。通常情況下他們的統計結果并不會相同,這是因為統計資訊來源的差異。是以問題來了:在ext4檔案系統下,有哪些可能的因素會帶來統計資訊的差異?
Knowledge Background
ext4 filesystem
physical structure overview
Unix-like 檔案系統,有file / dentry / inode / superblock的概念。在檔案系統這一層次,隻存在superblock與inode,前者儲存的是檔案系統的元資訊(metadata),後者是檔案的metadata;file與程序相關聯,記錄了程序打開檔案的上下文資訊;使用dentry建立的機制(dcache),提供了加速使用檔案名查找檔案方法。
磁盤與CD光牒的最小存儲機關是扇區(sector),作業系統每次I/O的首選長度(block size)稱為塊(block),檔案系統上最小的配置設定機關(fragment size)叫做fragment。傳統機械硬碟的機關扇區大小為512位元組,現代機械硬碟的扇區大小可以是4096位元組。Linux系統下,block size幾乎可以認為等于fragment size。[[2]](
https://en.wikipedia.org/wiki/Disk_sector)[[3]](https://unix.stackexchange.com/questions/463369/what-can-f-bsize-be-used-for-is-it-similar-to-st-blksize)[[4]](https://stackoverflow.com/questions/54823541/what-do-f-bsize-and-f-frsize-in-struct-statvfs-stand-for)inode(index node)作為檔案資料的索引資訊而存在,記錄了inode number、檔案元資訊、檔案資料塊的索引資訊。inode存儲在block中,預設大小是128位元組,是以一個block可以存儲多個inode,數個存儲inode的block組成inode table。
為了加速空閑block與inode的查找,設計了bmap與imap,它們采用位圖的方式辨別block或inode是否被使用。
inode table、bmap、imap過大也不利于查找,是以将一定數量的block劃分成塊組(block group)。每個block group都包含自己的metadata區域(存儲inode table、bmap、imap)與資料區域。
ext2 / ext3采用直接/間接尋址(Direct/Indirect Block Addressing)的方式索引data block,而ext4采用Extent Tree的方法,這減少了大檔案下metadata對空間的占用。
每個block group的元資訊使用GDT(group descriptor table)描述。考慮到未來檔案系統擴容的需要,出現了保留GDT(Reserved GDT)。
superblock的存在是為了記錄block數量與使用量等檔案系統的metadata,它存儲在0号block group中。為了防止superblock的損壞,在特定的block group中會儲存備份。修改GDT/Reserved GDT會導緻superblock的更改,是以他們仨會放在一起。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL0ITOkFGMwkjZwczYmdzYlZDOzMjN4YzY5EDZzMTMwM2Lc12bj5yYulWLuVXepxWYuIWdw1ycz9mL19Ga6dmbhhWLuNmLn1WatITY0F2Lc9CX6MHc0RHaiojIsJye.png)
詳細内容參考
第4章 ext檔案系統機制原理剖析,
man page: ext4 Linux doc: ext4 Data Structures and Algorithms Ext4 (and Ext2/Ext3) Wiki與
The Second Extended File System。
ext4檔案系統相關的指令行工具有:
e2fsprogs、
fuse2fs e2toolsinode,soft link(aka symbolic link) / hard link,and mount --bind
inode與檔案是一對一的關系,将inode與檔案關聯後使用inode number而非檔案名來識别檔案。
Linux下檔案的種類有七種,ls -l會看到具體的檔案類型:
$ ls -ail /dev/cdrom /etc/fstab
9837 lrwxrwxrwx 1 root root 3 Jul 17 11:06 /dev/cdrom -> sr0
1310722 -rw-r--r-- 1 root root 643 Jul 1 17:39 /etc/fstab
^ | |
| | +-- file size
| +---------------- link count
+-------------------------------- inode number
?: 未知檔案類型(some other file type)
-: 普通檔案(regular file)
d: 目錄(directory)
c: 字元裝置檔案(character special file)
b: 塊裝置檔案(block special file)
s: 套接字(socket)
p: 命名管道(FIFO, named pipe)
l: 軟連結(symbolic link, aka soft link)
排除未知檔案,隻有普通檔案、目錄與軟連結可能存在data block。
每個目錄檔案都有data block,存儲有該目錄下所有的檔案名,以及對應檔案的inode number、檔案類型。
Note: Linux doc: ext4 Data Structures and Algorithms, 4.1. Index Nodes 提到了使用inode number查找inode的算法,大意是:
inode table線性存儲struct ext4_inode,有固定大小sb.s_inode_size,因為block group的存在,先查找所屬group —— (inode_number - 1) / sb.s_inodes_per_group,在此group中對應的偏移量—— (inode_number - 1) % sb.s_inodes_per_group。不存在編号為0的inode。
對于非目錄檔案,硬連結的增加實際上是在目錄的data block中加一項記錄,同時
inode中的引用計數加一,這也是為什麼hard link無法跨檔案系統的原因(inode number可能沖突)。删除非唯一的硬連結過程與添加相反,隻有當inode的引用計數為0的時候,才将
inode加入orphan inode list,在沒有程序打開此檔案後會進入檔案的删除流程。
lsof -a +L1可以顯示目前的orphan inodes。它的原理是周遊/proc//fd/下的所有檔案描述符,因為orphan inode對應的檔案描述符有特殊的标志,是以可以枚舉出對應的檔案。
但若一個orphan inode的檔案描述符與核心線程相關聯,顯然lsof無法枚舉出來。
acct()系統可能導緻這種情況的發生(issue:
Phantom full ext4 root filesystems on 4.1 through 4.14 kernels)。
在kernel啟用編譯選項CONFIG_BSD_PROCESS_ACCT後,調用acct(filename)會開啟process accounting,之後在每個程序終止的時後kernel會将統計資訊
struct acct寫入filename。核心參數
/proc/sys/kernel/acct定義了accounting機制的行為。
對于目錄檔案,本身不存在硬連結的概念,ls -l顯示的link count指的是該目錄下一級檔案中所有目錄檔案的總數(包含"."與"..",是以即使是
空目錄link count的值也是2)。不過
mount --bind的作用與hard link一樣,隻不過link count不增加罷了,而且它可以跨檔案系統。
符号連結(symbolic link)又稱軟連結(soft link),它的作用是指向原檔案或目錄,存儲的是目标檔案路徑,隻有當目标檔案路徑字元串
大于60位元組的時候才會被配置設定一個data block,是以它的大小通常為0。
- link management
建立軟連結、硬連結,除了通過作業系統間接管理的方式,比如shell提供的[ln]與系統調用
symlink() link(),還可以直接操作存儲媒體,比如e2fsprogs中的
debugfs,e2tools中的
e2lndebugfs -w -R "link ( | )"可用于建立hard link,然而:
ln filespec dest_file
Create a link named dest_file which is a hard link to filespec. Note this does not ad‐
just the inode reference counts.
v1.44.5的debugfs -R "link ..."并不會帶來link count的變化,v0.0.16.4的e2ln也同樣如此(因為他們從讀取到寫入的邏輯幾乎是一緻的)。
ext4 file system mount option / feature
- bsddf | minixdf
ext4提供了挂在選項bsddf | minixdf(預設bsddf),它影響了statfs()擷取到的f_blocks(Total data blocks in filesystem)。
/**
* int ext4_statfs(struct dentry *dentry, struct kstatfs *buf)
*/
if (!test_opt(sb, MINIX_DF))
overhead = sbi->s_overhead;
buf->f_blocks = ext4_blocks_count(es) - EXT4_C2B(sbi, overhead);
e2fsprogs在resize/resize2fs.c中對overhead做出了解釋:
Overhead is the number of bookkeeping blocks per group. It
includes the superblock backup, the group descriptor
backups, the inode bitmap, the block bitmap, and the inode
table.
- has_journal
擁有has_journal feature的ext4會啟用日志功能,檔案系統的日志也會占用block,這些blocks在格式化分區的時候确定。statfs()系統調用在擷取資訊時并不一定會将journal blocks排除在外——挂載時啟用選項minixdf,f_blocks在計算時并不會減去journal blocks (這部分blocks屬于overhead的一部分)。# mkfs.ext4 -J size=journal-size 可在格式化分區的時候指定journal的大小為<journal-size>。
/**
* int ext4_statfs(struct dentry *dentry, struct kstatfs *buf)
*/
resv_blocks = EXT4_C2B(sbi, atomic64_read(&sbi->s_resv_clusters));
if (!test_opt(sb, MINIX_DF))
overhead = sbi->s_overhead;
buf->f_blocks = ext4_blocks_count(es) - EXT4_C2B(sbi, overhead);
bfree = percpu_counter_sum_positive(&sbi->s_freeclusters_counter) -
percpu_counter_sum_positive(&sbi->s_dirtyclusters_counter);
/* prevent underflow in case that few free space is available */
buf->f_bfree = EXT4_C2B(sbi, max_t(s64, bfree, 0));
buf->f_bavail = buf->f_bfree -
(ext4_r_blocks_count(es) + resv_blocks);
if (buf->f_bfree < (ext4_r_blocks_count(es) + resv_blocks))
buf->f_bavail = 0;
debugfs -R "stat <8>" 或# dumpe2fs | grep -i journal可以擷取到journal size:
# debugfs -R "stat <8>" /dev/loop2
debugfs 1.42.9 (28-Dec-2013)
Inode: 8 Type: regular Mode: 0600 Flags: 0x80000
Generation: 0 Version: 0x00000000:00000000
User: 0 Group: 0 Size: 134217728
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 262144
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5d3956d3:00000000 -- Thu Jul 25 03:14:27 2019
atime: 0x5d3956d3:00000000 -- Thu Jul 25 03:14:27 2019
mtime: 0x5d3956d3:00000000 -- Thu Jul 25 03:14:27 2019
crtime: 0x5d3956d3:00000000 -- Thu Jul 25 03:14:27 2019
Size of extra inode fields: 28
EXTENTS:
(0-32766):1081344-1114110, (32767):1114111
# dumpe2fs /dev/loop2 | grep -i journal
dumpe2fs 1.42.9 (28-Dec-2013)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Journal inode: 8
Journal backup: inode blocks
Journal features: journal_64bit
Journal size: 128M
Journal length: 32768
Journal sequence: 0x00000002
Journal start: 1
Note: <8>代表inode為8的檔案,它是一個 special inode ,屬于journal。
- inline data
Linux v3.8之後,ext4添加了一項feature:
。LWN.net:
Improving ext4: bigalloc, inline data, and metadata checksums提到:
大部分檔案系統下存在兩類inode:一類是存在與核心中、與檔案系統無關的inode(`struct inode),一類是在存儲媒體上儲存、檔案系統相關的inode(on-disk inode)。對第二類inode的維護意味着IO操作。
on-disk inode的大小在檔案系統建立後便确定,預設大小是256位元組,但實際上隻需要大約一半的空間,其餘空間常用來存儲檔案的額外屬性。
檔案的存儲需要配置設定額外的block。若存在太多的小檔案,則會造成大量block的浪費。如果使用了clustering (數個block組成一個更大的block cluster,檔案系統配置設定的最小機關是block cluster。feature: bigalloc),這種空間浪費會更加嚴重。
inline data的提出是便是為了解決這種存儲空間浪費的問題。啟用inline_data特性的ext4檔案系統,在檔案小于60位元組的時候不會被配置設定data block,資料将會存儲在inode中。
df
man page描述
:report file system disk space usage。coreutils中的df使用了glibc的
statvfs(),間接地調用系統調用
statfs()家族,資料來源于檔案系統的super block。
它的輸出,即--output的參數有以下幾種:
source: The source of the mount point, usually a device.
fstype: File system type.
itotal: Total number of inodes.
iused: Number of used inodes.
iavail: Number of available inodes.
ipcent: Percentage of IUSED divided by ITOTAL.
size: Total number of blocks.
used: Number of used blocks.
avail: Number of available blocks.
pcent: Percentage of USED divided by SIZE.
file: The file name if specified on the command line.
target: The mount point.
與空間大小有關輸出以block的數量計算,輸入的block大小從檔案系統的super block中擷取,輸出的大小可以通過參數-B / --block-size指定,預設1024位元組。
輸出資訊的數學表達式如下:
# about inode
<total inodes> = <statvfs.f_files>
<available inodes> = <statvfs.f_free>
<used inodes> = <total inodes> - <available inodes>
# about block count
<total blocks> = <statvfs.f_blocks> * <block size> / <output block size>
<available blocks> = <statvfs.f_bavail> * <block size> / <output block size>
<used blocks> = (<statvfs.f_blocks> - <statvfs.f_ffree>) * <block size> / <output block size>
- df是優先從/proc/self/mountinfo擷取挂載的裝置資訊的,如果不存在該檔案則是/proc/self/mounts。/proc/self/下存在三個以mount為字首的檔案,詳見 man page proc
- df對存儲空間的統計是以block的數量而非位元組為機關。
- KiB/kiB與KB/kB是不同的,前者是2的幂,後者是10的幂,即 Kibibit Kibibyte 的差別。
du
:estimate file space usage。它的原理是深度優先周遊目标檔案目錄下的所有檔案(非orphan inode),使用
stat()家族擷取檔案資訊。
影響du輸出結果的因素有以下幾種:
- follow symbolic links?
- count sizes many times if hard linked?
- use apparent sizes rather than disk usage?
- is output unit SIZE-byte blocks?
在實作上,是否周遊符号連結指向的檔案,
差別在與是否fstatat()的flag是否設定了AT_SYMLINK_NOFOLLOW。
檔案去重是基于hash的,
對硬連結的判斷則是觀察inode中硬連結的計數是否大于1,當然排除了檔案目錄的可能性。
因為stat()擷取的檔案大小是真實大小(以位元組為機關),并非配置設定的block units size,是以通過
向上取整擷取block units的方式
計算出block units size至于最後一個,隻是做了
機關的轉換Conclusion
假如檔案系統正常(即不存在bug、一緻性問題),作業系統、軟體也不存在bug,那麼存在這麼幾種可能性:
- 程序導緻的orphan inodes
- 使用者态程序導緻,可使用$ lsof -a +L1檢視;
- 系統調用acct(),無法在/proc中得知檔案的打開狀态。
- mount --bind導緻的重複計數
- 直接操作儲存設備建立hard link導緻的hard link計數異常
- debugfs command # debugfs -w -R "link ..."
- ext4 mount option / feature
- inline_data
- 因小檔案過多帶來的實際配置設定空間(block units)與實際檔案大小(apparent size)之間的差異
- du參數--apparent-size
- 輸出的計量機關不同帶來的差異
- du與df參數-B, --block-size=SIZE
若系統的狀态不正常,df / du統計資訊的巨大差異有可能是orphan inodes導緻的:即inode->i_nlink == 0 && inode->i_count != 0,同時不存在程序關聯了此inode。
為此,我開發了一個
診斷子產品,目前已內建到
diagnose-tools。一個使用案例如下:
$ insmod diagnose.ko
$ echo "vda1" > /proc/ali-linux/diagnose/fs/dump_orphan
$ cat /proc/ali-linux/diagnose/fs/dump_orphan
device "vda1" orphan list:
inode 262188 (ffff88003803f0e8): mode 40755, nlink 0, count 1
inode 262189 (ffff88003803d0e8): mode 40755, nlink 0, count 1
inode 262930 (ffff88003803c8e8): mode 40755, nlink 0, count 1
inode 262931 (ffff88003803c0e8): mode 40755, nlink 0, count 1
inode 262932 (ffff88003803d8e8): mode 40755, nlink 0, count 1
inode 262933 (ffff88003803fce8): mode 40755, nlink 0, count 1
inode 262934 (ffff88003c3e90e8): mode 40755, nlink 0, count 1
$ # or use script after insmoding module:
$ ali-diagnose report-dump-orphan vda1
device "vda1" orphan list:
inode 262188 (ffff88003803f0e8): mode 40755, nlink 0, count 1
inode 262189 (ffff88003803d0e8): mode 40755, nlink 0, count 1
inode 262930 (ffff88003803c8e8): mode 40755, nlink 0, count 1
inode 262931 (ffff88003803c0e8): mode 40755, nlink 0, count 1
inode 262932 (ffff88003803d8e8): mode 40755, nlink 0, count 1
inode 262933 (ffff88003803fce8): mode 40755, nlink 0, count 1
inode 262934 (ffff88003c3e90e8): mode 40755, nlink 0, count 1