It’s known that btrfs behaves differently from other Linux filesystems. There are some fascinating aspects of how btrfs manages its internal structures and how common tools are not prepared to handle it.

This goal of this post is to demystify why ext4 can report the number of available inodes while btrfs always reports 0:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ file ext4.disk                                                               
ext4.disk: Linux rev 1.0 ext4 filesystem data, UUID=3f21312b-412a-4b1a-8561-5704eaf39d22 (extents) (64bit) (large files) (huge files)

$ mount ext4.disk /mnt
$ df -i /mnt    
Filesystem     Inodes IUsed  IFree IUse% Mounted on
/dev/loop0     327680    11 327669    1% /mnt

$ mount -l | grep sda2
/dev/sda2 on / type btrfs (rw,relatime,ssd,space_cache=v2,subvolid=266,subvol=/@/.snapshots/1/snapshot)

$ df -i /
Filesystem     Inodes IUsed IFree IUse% Mounted on
/dev/sda2           0     0     0     - /

Why btrfs always shows the number of available inodes as zero?

This aspect tells a lot about how btrfs manages its physical space.

Filesystems like ext4 allocate the entire disk on filesystem creation time, creating block groups all over the available space. This means that once the spaces for data and metadata are defined, they cannot be changed after the filesystem is in use, as there isn’t a way to extend them: they have fixed offsets. Let’s take a look how it works for ext4.

Ext4: block sizes and block groups

In filesystems, a block is a group of sectors (512 bytes) and it’s the smaller unit of data managed by the filesystem. The block size affects all other filesystem structures, specially in filesystems like ext4 for example. By the block size we can say how many inodes and how much space a ext4 filesystem can manage. Ext4 accepts block sizes of 1k, 2k, 4k and 64k.

A block group, as the name implies, is a collection of blocks, and many filesystems manage their spaces using block groups. Ext4 divides the entire disk into block groups when creating the filesystem and its size is defined by the block size. By default, ext4 uses blocks of 4k of size. Ext4 stores both data and metadata in a block group.

From now on we’ll make the calculations based in a block size of 4k (4096 bytes).

To track which blocks are used in a block group, ext4 reserves one block of the block group to store a bitmap. Each bit of the bitmap will track one block of the block group, meaning that we can map up to 128mb of space:

4096 bytes * 8bits: 32768 bits
32768 bits can map 32768 blocks of 4k
32768 * 4k: 134217728 bytes: 128Mb

Let’s take a look in how ext4 divides a 5G disk, using the default 4k block sizes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# create a 5G file to be used as disk
$ fallocate -l5g ext4.disk

# create the ext4 filesystem on it
$ mkfs.ext4 ext4.disk
mke2fs 1.43.8 (1-Jan-2018)
Discarding device blocks: done
Creating filesystem with 1310720 4k blocks and 327680 inodes
Filesystem UUID: e408e28f-f275-49c6-87e8-18104fe31ba4
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736

Allocating group tables: done
Writing inode tables: done
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done

# print some information about the created filesystem
$ dumpe2fs ext4.disk
dumpe2fs 1.43.8 (1-Jan-2018)
Filesystem volume name:   <none>
Last mounted on:          <not available>
Filesystem UUID:          e408e28f-f275-49c6-87e8-18104fe31ba4
Filesystem magic number:  0xEF53
Filesystem revision #:    1 (dynamic)
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags:         signed_directory_hash
Default mount options:    user_xattr acl
Filesystem state:         clean
Errors behavior:          Continue
Filesystem OS type:       Linux
Inode count:              327680
Block count:              1310720
Reserved block count:     65536
Free blocks:              1268642
Free inodes:              327669
First block:              0
Block size:               4096
Fragment size:            4096
Group descriptor size:    64
Reserved GDT blocks:      639
Blocks per group:         32768
Fragments per group:      32768
Inodes per group:         8192
Inode blocks per group:   512
Flex block group size:    16
Filesystem created:       Wed Aug  4 00:24:49 2021
...
First inode:              11
Inode size:               256
...

Group 0: (Blocks 0-32767) csum 0xcef6 [ITABLE_ZEROED]
  Primary superblock at 0, Group descriptors at 1-1
  Reserved GDT blocks at 2-640
  Block bitmap at 641 (+641)
  Inode bitmap at 657 (+657)
  Inode table at 673-1184 (+673)
  23897 free blocks, 8181 free inodes, 2 directories, 8181 unused inodes
  Free blocks: 8871-32767
  Free inodes: 12-8192
Group 1: (Blocks 32768-65535) csum 0x4873 [INODE_UNINIT, BLOCK_UNINIT, ITABLE_ZEROED]
  Backup superblock at 32768, Group descriptors at 32769-32769
  Reserved GDT blocks at 32770-33408
  Block bitmap at 642 (bg #0 + 642)
  Inode bitmap at 658 (bg #0 + 658)
  Inode table at 1185-1696 (bg #0 + 1185)
  32127 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
  Free blocks: 33409-65535
  Free inodes: 8193-16384
...
Group 39: (Blocks 1277952-1310719) csum 0x71eb [INODE_UNINIT, ITABLE_ZEROED]
  Block bitmap at 1048583 (bg #32 + 7)
  Inode bitmap at 1048591 (bg #32 + 15)
  Inode table at 1052176-1052687 (bg #32 + 3600)
  32768 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
  Free blocks: 1277952-1310719
  Free inodes: 319489-327680

The output of mkfs.ext4 was reduced because it’s too long. The output above gives a general idea about how the filesystem is organized. From now on this post will describe how these values are calculated, and why they were chosen by ext4.

As ext4 manages its spaces using block groups, and with 4k block sizes we can have a block group mapping up to 128Mb of space, mkfs.ext4 needed to create 40 block groups:

5G of space / 128mb block group size: 40 block groups

Ext4: inodes

As mentioned before ext4 uses a reserved block in a block group to track the used blocks. This is also true for inodes. There is a reserved block per block group used as an inode bitmap to track allocated inodes. By using the same math, the inode bitmap can track up to 32768 inodes.

Along with the inode bitmap, we also need to store the inode metadata (size, owner, file size, etc). There is a space in the block group to store the inode metadata, and it’s separated from the file’s data.

As each block group is allocated when creating the filesystem, and inode metadata has to have a separated space within the block group (called inode table) it needs to calculate the necessary space to store the metadata.

If a big amount of space is used to store inode metadata, the filesystem would be able to create more files, but the available space for file content (data) would be reduced. On the other hand, creating a small inode table allows the user to store more data, but with a reduced number of files. To address these limits, mkfs.ext4 uses a configuration called inode-ratio which defines the number of inodes proportional to the storage space. The default inode-ratio is 16k (described in mke2fs.conf file).

Using our 5G disk as before and the inode-ration, we can calculate the maximum number of inodes this filesystem can store:

5G of storage / 16k: 327680 inodes
327680 inodes / 40 block groups: 8192

These values match the output from mkfs.ext4 shown before.

The inode table needs to known how much space will be used to store the inode metadata for each inode in the filesystem. Ext4 uses 256 bytes as default inode size (also described in mke2fs.conf file), so for each block group it will use 2Mb of space for the inode table:

8192 inodes x 256 bytes per inode: 2Mb (2097152 bytes)

To compare the numbers, just mount the filesystem created before and use df to show the maximum number of inodes:

1
2
3
4
5
6
7
$ mount ext4.disk /tmp/ext4
$ df -i /tmp/ext4 
Filesystem     Inodes IUsed  IFree IUse% Mounted on
/dev/loop0     327680    11 327669    1% /tmp/ext4

$ ls -i /tmp/ext4
11 lost+found

Ext4 reserves the inode numbers from 0 to 10 for special purposes, and the first usable one is for the lost+found. This is a special purpose directory for the ext4 filesystem. All user files start from inode 12.

For more information about ext4 block group please check the official ext4 documentation here.

What about btrfs?

Btrfs allocates its structures dynamically. From block groups to internal structures and inodes, btrfs allocates them on demand.

Btrfs: block groups

Btrfs also uses block groups to manage the filesystem space, but each block group will store data OR metadata, not both. On filesystem creation time we can specify the block groups to be mixed, containing both data and metadata, but it’s not recommended.

When creating a filesystem btrfs creates a block group to store data, one to store metadata, and one system block group. A data block group (usually) takes 1G of size, while the metadata one can take 256Mb if the filesystem is smaller than 50G, and 1G if bigger. The system block group usually takes up to some megabytes. For small filesystems, the block group sizes cannot be more than 10% of the filesystem size, so it can smaller than 1G as stated before. All the remaining space is left there to be allocated when necessary.

Differently from ext4, btrfs allocates block groups on demand. If the workload is focused on data (bigger files), more data block groups will be allocated from the free space. In the same way, if the workload is creating more metadata (doing snapshots for example) more metadata block groups will be allocated.

Let’s see an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# create a 55G file to be used as disk
$ fallocate -l55g btrfs.disk

# Create the filesystem on top of it
$ mkfs.btrfs -f /storage/btrfs.disk 
btrfs-progs v5.10.1 
See http://btrfs.wiki.kernel.org for more information.

Label:              (null)
UUID:               8f9303a9-15cd-4709-848e-38f80b5b2985
Node size:          16384
Sector size:        4096
Filesystem size:    55.00GiB
Block group profiles:
  Data:             single            1.00GiB
  Metadata:         DUP               1.00GiB
  System:           DUP               8.00MiB
SSD detected:       no
Zoned device:       no
Incompat features:  extref, skinny-metadata
Runtime features:   
Checksum:           crc32c
Number of devices:  1
Devices:
   ID        SIZE  PATH
    1    55.00GiB  /storage/btrfs.disk

If your filesystem reports a different data block group as being of 8M, it’s because of this issue that was reported, but maybe not yet fixed. It’s important to understand that these numbers reflect the allocation strategy for single profile. For raid setups, these numbers can be different.

Also, the block group sizes doesn’t affect the number of maximum number of inodes that can be created, as we’ll see later.

We can inspect the block groups by using the btrfs tool:

1
2
3
4
5
6
7
8
$ btrfs inspect-internal dump-tree -t extent /storage/btrfs.disk | grep -A1 BLOCK_GROUP
        item 0 key (1078984704 BLOCK_GROUP_ITEM 1073741824) itemoff 16259 itemsize 24
                block group used 0 chunk_objectid 256 flags DATA
        item 1 key (2152726528 BLOCK_GROUP_ITEM 8388608) itemoff 16235 itemsize 24
                block group used 16384 chunk_objectid 256 flags SYSTEM|DUP
...
        item 4 key (2161115136 BLOCK_GROUP_ITEM 1073741824) itemoff 16145 itemsize 24
                block group used 114688 chunk_objectid 256 flags METADATA|DUP

The above command used the inspect-internal subcommand to dump the entire extent-tree. We can compare the block group sizes (the numbers after the BLOCK_GROUP_ITEM) in bytes that match we the previous mkfs.btrfs output. The profiles are also dumped and can be verified, being DUP for system and metadata.

When we write more files, or if a file occupies more than 1G of data, more data block groups are created (the flags field shows the type of the block group):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Creating a 2.5G file
$ dd if=/dev/urandom of=/mnt/testing/file.bin bs=1M count=2500
$ sync
# Check the new data block groups
$ btrfs inspect-internal dump-tree -t extent /storage/btrfs.disk| grep -A1 BLOCK_GROUP 
        item 0 key (1078984704 BLOCK_GROUP_ITEM 1073741824) itemoff 16259 itemsize 24
                block group used 940572672 chunk_objectid 256 flags DATA
..
        item 13 key (2152726528 BLOCK_GROUP_ITEM 8388608) itemoff 15619 itemsize 24
                block group used 16384 chunk_objectid 256 flags SYSTEM|DUP
        item 14 key (2161115136 BLOCK_GROUP_ITEM 1073741824) itemoff 15595 itemsize 24
                block group used 2899968 chunk_objectid 256 flags METADATA|DUP
..
        item 192 key (3234856960 BLOCK_GROUP_ITEM 1073741824) itemoff 9730 itemsize 24
                block group used 939524096 chunk_objectid 256 flags DATA
..
        item 201 key (4308598784 BLOCK_GROUP_ITEM 1073741824) itemoff 9282 itemsize 24
                block group used 742391808 chunk_objectid 256 flags DATA

We can see that new data block groups were created. If we remove the file, the used space is updated to reflect the file removal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ rm /mnt/testing/file.bin
$ sync
$ btrfs inspect-internal dump-tree -t extent /storage/btrfs.disk| grep -A1 BLOCK_GROUP
        item 1 key (1078984704 BLOCK_GROUP_ITEM 1073741824) itemoff 16206 itemsize 24
                block group used 1048576 chunk_objectid 256 flags DATA
..
        item 6 key (2152726528 BLOCK_GROUP_ITEM 8388608) itemoff 15990 itemsize 24
                block group used 16384 chunk_objectid 256 flags SYSTEM|DUP
        item 7 key (2161115136 BLOCK_GROUP_ITEM 1073741824) itemoff 15966 itemsize 24
                block group used 147456 chunk_objectid 256 flags METADATA|DUP
..
        item 17 key (3234856960 BLOCK_GROUP_ITEM 1073741824) itemoff 15645 itemsize 24
                block group used 0 chunk_objectid 256 flags DATA
        item 18 key (4308598784 BLOCK_GROUP_ITEM 1073741824) itemoff 15621 itemsize 24
                block group used 0 chunk_objectid 256 flags DATA

Take a look in the block group use field, they are now zeroed. The block groups are still allocated, but a balance can remove the non used ones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ btrfs balance start -dusage=0 /mnt/testing
Done, had to relocate 0 out of 3 chunks
$ btrfs inspect-internal dump-tree -textent /storage/btrfs.disk | grep -A 1 BLOCK_GROUP 
        item 0 key (1078984704 BLOCK_GROUP_ITEM 1073741824) itemoff 16259 itemsize 24
                block group used 524288 chunk_objectid 256 flags DATA
..
        item 3 key (2152726528 BLOCK_GROUP_ITEM 8388608) itemoff 16129 itemsize 24
                block group used 16384 chunk_objectid 256 flags SYSTEM|DUP
..
        item 5 key (2161115136 BLOCK_GROUP_ITEM 1073741824) itemoff 16072 itemsize 24
                block group used 147456 chunk_objectid 256 flags METADATA|DUP

It shows only one data block group.

Btrfs: inodes

Btrfs does not use fixes inode bitmaps for inode allocation. As stated before, btrfs allocates internal items to manage its metadata. Each item is addressed by three values that together compose a key. These values are described as objectid, type and offset.

This is the fs tree right after the filesystem is created:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ btrfs inspect-internal dump-tree -t fs /storage/btrfs.disk
btrfs-progs v5.16.1 
fs tree key (FS_TREE ROOT_ITEM 0) 
leaf 30474240 items 2 free space 16061 generation 5 owner FS_TREE
leaf 30474240 flags 0x1(WRITTEN) backref revision 1
fs uuid b62385e4-e0cc-497f-a220-29ae6465510d
chunk uuid b3e0fa4d-fddb-40b3-bd9c-349df8095b39
        item 0 key (256 INODE_ITEM 0) itemoff 16123 itemsize 160
                generation 3 transid 0 size 0 nbytes 16384
                block group 0 mode 40755 links 1 uid 0 gid 0 rdev 0
                sequence 0 flags 0x0(none)
                atime 1650811378.0 (2022-04-24 11:42:58)
                ctime 1650811378.0 (2022-04-24 11:42:58)
                mtime 1650811378.0 (2022-04-24 11:42:58)
                otime 1650811378.0 (2022-04-24 11:42:58)
        item 1 key (256 INODE_REF 256) itemoff 16111 itemsize 12
                index 0 namelen 2 name: ..

There are two items in the listing, and the two refer to the top level directory. The INODE_ITEM item contains data about the inode, owner, size and etc. Its key is always (inode_number INODE_ITEM 0), and 256 is the first inode number used in a filesystem tree.

The INODE_REF item maps an inode to its parent directory (inode_number INODE_REF parent_dir_inode). In this case it points to itself since the top level directory ancestor is itself.

By creating a file, we can see more structures being allocated:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ touch /mnt/testing/file.txt
$ sync
$ btrfs inspect-internal dump-tree -t fs /storage/btrfs.disk
...
        item 2 key (256 DIR_ITEM 3956591618) itemoff 16073 itemsize 38
                location key (257 INODE_ITEM 0) type FILE
                transid 9 data_len 0 name_len 8
                name: file.txt
        item 3 key (256 DIR_INDEX 2) itemoff 16035 itemsize 38
                location key (257 INODE_ITEM 0) type FILE
                transid 9 data_len 0 name_len 8
                name: file.txt
        item 4 key (257 INODE_ITEM 0) itemoff 15875 itemsize 160
                generation 9 transid 9 size 0 nbytes 0
                block group 0 mode 100644 links 1 uid 0 gid 0 rdev 0
                sequence 10 flags 0x0(none)
                atime 1650811865.463886516 (2022-04-24 11:51:05)
                ctime 1650811865.463886516 (2022-04-24 11:51:05)
                mtime 1650811865.463886516 (2022-04-24 11:51:05)
                otime 1650811865.463886516 (2022-04-24 11:51:05)
        item 5 key (257 INODE_REF 256) itemoff 15857 itemsize 18
                index 2 namelen 8 name: file.txt

A new INODE_ITEM was allocated for file.txt, using the inode number 257. The new INODE_REF item’s offset points to 256, which is the top level directory, as expected.

Two new items are also allocated: DIR_ITEM and DIR_INDEX. DIR_INDEX is used for directory listing, like readdir for example. Its key (parent_dir_inode DIR_INDEX pos) almost explains itself. The pos value says it’s the third file created in the directory, as values 1 and 2 are related to ‘.’ and ‘..’ respectively. DIR_ITEM is used for searching. Its offset value is the filename hashed, making it quick to find a file by name inside a directory.

As the file grows, more item are created:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ dd if=/dev/urandom of=/mnt/testing/file.txt bs=1M count=500
$ sync
$ btrfs inspect-internal dump-tree -t fs btrfs.disk
...
        item 6 key (257 EXTENT_DATA 0) itemoff 15804 itemsize 53
                generation 12 type 1 (regular)
                extent data disk byte 1104150528 nr 134217728
                extent data offset 0 nr 134217728 ram 134217728
                extent compression 0 (none)
        item 7 key (257 EXTENT_DATA 134217728) itemoff 15751 itemsize 53
                generation 12 type 1 (regular)
                extent data disk byte 1238368256 nr 134217728
                extent data offset 0 nr 134217728 ram 134217728
                extent compression 0 (none)
        item 8 key (257 EXTENT_DATA 268435456) itemoff 15698 itemsize 53
                generation 12 type 1 (regular)
                extent data disk byte 1372585984 nr 134217728
                extent data offset 0 nr 134217728 ram 134217728
                extent compression 0 (none)
        item 9 key (257 EXTENT_DATA 402653184) itemoff 15645 itemsize 53
                generation 12 type 1 (regular)
                extent data disk byte 1506803712 nr 90177536
                extent data offset 0 nr 90177536 ram 90177536
                extent compression 0 (none)
        item 10 key (257 EXTENT_DATA 492830720) itemoff 15592 itemsize 53
                generation 12 type 1 (regular)
                extent data disk byte 1596981248 nr 1048576
                extent data offset 0 nr 1048576 ram 1048576
                extent compression 0 (none)
        item 11 key (257 EXTENT_DATA 493879296) itemoff 15539 itemsize 53
                generation 12 type 1 (regular)
                extent data disk byte 1598029824 nr 30408704
                extent data offset 0 nr 30408704 ram 30408704
                extent compression 0 (none)
...

New EXTENT_DATA items are created to manage the data related to user files. The objectid of the EXTENT_DATA’s key informs to which inode the data is associated.

More data about btrfs item can be found in this document.

Why can’t btrfs show how many inodes it can hold?

Because it’s impossible to known beforehand.

The same metadata block group space is used to store INODE_ITEM, EXTENT_DATA and all other metadata items. So if a workload creates bigger files, it ends ups creating more EXTENT_DATA items, which can end up consuming a huge number of metadata block groups to manage extents.

On the other hand, if your workload ends up creating a huge number of small files, it would end up creating less EXTENT_DATA items, making it possible to store different items, including INODE_ITEMs, making it possible to create more files.

That’s the reason for the number of inodes cannot be checked by using df. The df tool uses the statfs system call to show the information about inodes. The system call populates the statfs struct with the values related to the filesystem being checked, and the f_files field contains the maximum number of inodes.

By looking at the kernel code, the function btrfs_statfs does not set buf->f_files, while in ext4_statfs we can see the information being get from the superblock.

Btrfs: Inodes and subvolumes

In btrfs, subvolumes store user data, and act like different filesystems, as each subvolume can be mounted separately. It can be compared to a disk partition, but having much more features, like creating snapshots. openSUSE and SUSE Linux Enterprise (SLE) uses subvolumes to divide the filesystem into logic structures, and also to manage snapshots after each upgrade:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ btrfs subvolume list /
[sudo] password for root: 
ID 256 gen 31 top level 5 path @
ID 257 gen 161195 top level 256 path @/var
ID 258 gen 161097 top level 256 path @/usr/local
ID 259 gen 64676 top level 256 path @/srv
ID 260 gen 149398 top level 256 path @/root
ID 261 gen 146119 top level 256 path @/opt
ID 262 gen 161195 top level 256 path @/home
ID 263 gen 150062 top level 256 path @/boot/grub2/x86_64-efi
ID 264 gen 27 top level 256 path @/boot/grub2/i386-pc
ID 265 gen 160386 top level 256 path @/.snapshots
ID 266 gen 161162 top level 265 path @/.snapshots/1/snapshot
ID 342 gen 110017 top level 265 path @/.snapshots/77/snapshot
ID 343 gen 110254 top level 265 path @/.snapshots/78/snapshot
ID 346 gen 112147 top level 265 path @/.snapshots/81/snapshot
ID 347 gen 112149 top level 265 path @/.snapshots/82/snapshot
ID 352 gen 113071 top level 265 path @/.snapshots/87/snapshot
ID 353 gen 113100 top level 257 path @/var/lib/machines
...

An interesting fact about subvolumes is that files always start from inode 257, and the same inode numbers are used in different subvolumes. Since each subvolume is a different tree inside btrfs, it’s not a problem for the end user. Let’s see an example of this happening:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ mount /storage/btrfs.disk /mnt/testing

$ btrfs subvolume create /mnt/testing/vol1
Create subvolume '/mnt/testing/vol1'

$ btrfs subvolume create /mnt/testing/vol2
Create subvolume '/mnt/testing/vol2'

# let's create directories and files inside each subvolume
$ touch /mnt/testing/vol1/file1
$ touch /mnt/testing/vol1/file2
$ mkdir /mnt/testing/vol2/dir1
$ touch /mnt/testing/vol2/filexx

# let's list the inodes of there files/direcotry
$ ls -iR /mnt/testing
/mnt/testing:
256 vol1  256 vol2

/mnt/testing/vol1:
257 file1  258 file2

/mnt/testing/vol2:
257 dir1  258 filexx

/mnt/testing/vol2/dir1:

From the output above we can see that both file1 and dir, and file2 and filexx have the same inode numbers.

Considerations

Interested readers may want to take a look into the ext4 kernel documentation , the mke2fs documentation and the configuration file used by mke2fs for the default filesystem settings.

Btrfs has a nice wiki page detailing how things work. Additional info can be get from the mkfs.btrfs utility man page and for the more curious readers, the btrfs-dev-docs details the inner parts of the filesystem.

The post got much bigger than I expected. The initial focus was to only mention about the kernel code setting the number of inodes in ex4, and the math involved to find the maximum number of inodes. While talking with some friends, it became more evident that more background would be more interesting. So that explains why block groups, subvolumes and everything in between was mentioned.

Thanks for reading!