天天看點

df與du輸出結果不同的原因研究QuestionKnowledge BackgroundConclusionReference

作者:嶺南

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的更改,是以他們仨會放在一起。

df與du輸出結果不同的原因研究QuestionKnowledge BackgroundConclusionReference

詳細内容參考

第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 e2tools

inode,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中的

e2ln

debugfs -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>           
  1. df是優先從/proc/self/mountinfo擷取挂載的裝置資訊的,如果不存在該檔案則是/proc/self/mounts。/proc/self/下存在三個以mount為字首的檔案,詳見 man page proc
  2. df對存儲空間的統計是以block的數量而非位元組為機關。
  3. KiB/kiB與KB/kB是不同的,前者是2的幂,後者是10的幂,即 Kibibit Kibibyte 的差別。

du

:estimate file space usage。它的原理是深度優先周遊目标檔案目錄下的所有檔案(非orphan inode),使用

stat()家族

擷取檔案資訊。

影響du輸出結果的因素有以下幾種:

  1. follow symbolic links?
  2. count sizes many times if hard linked?
  3. use apparent sizes rather than disk usage?
  4. 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,那麼存在這麼幾種可能性:

  1. 程序導緻的orphan inodes
    • 使用者态程序導緻,可使用$ lsof -a +L1檢視;
    • 系統調用acct(),無法在/proc中得知檔案的打開狀态。
  2. mount --bind導緻的重複計數
  3. 直接操作儲存設備建立hard link導緻的hard link計數異常
    • debugfs command # debugfs -w -R "link ..."
  4. ext4 mount option / feature
    • inline_data
  5. 因小檔案過多帶來的實際配置設定空間(block units)與實際檔案大小(apparent size)之間的差異
    • du參數--apparent-size
  6. 輸出的計量機關不同帶來的差異
    • 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           

Reference

  1. Linux doc: ext4 Data Structures and Algorithms
  2. man page: df
  3. man page: du
  4. linux-ext4 maillist: Phantom full ext4 root filesystems on 4.1 through 4.14 kernels

繼續閱讀