Linux Filesystem: Directory Entries, Inodes, Data Blocks

An inode is a unique number assigned to each Linux file and directory in a filesystem (except for hard links), it is used as an index (Index Node).

Inodes store metadata (attributes) about the files they refer to (it is like the "file’s identity card" without the name)
AND
Because the data of a file is actually stored into data blocks on a physical drive, serve as a reference to the disk blocks locations of the data they point to (via data block pointers).
Note that this information is not directly accessible to the user.

Thus, an inode is a data structure in a Unix-style filesystem that describes a filesystem object such as a file or a directory.

A block device is a storage device from which you can read/write data blocks.
You create partitions on it and then format it with a filesystem that dictates how the files are organized/managed.
Every filesystem needs to split up a partition into data blocks to store files and file parts.

A data block is the basic unit of data storage in a filesystem.
It is the smallest unit of data that can be read or written in a single operation.
In most filesystems, each data block has a fixed size, typically between 512 and 4096 bytes.
Today the default is usually 4096 bytes for storage I/O and filesystems.

With a default filesystem block size of 4096 bytes, if you have a data file of 3 bytes (logical size), it will take away 1 block (4096 bytes: physical size on storage device) from your disk’s capacity, since that is the smallest unit of the filesystem.
If you have a data file of 4097 bytes it will take 2 blocks.

NOTE:
'stat' command provides 'Size:' and 'Blocks:' informations
'Size:' is the data file' size in bytes (logical size)
'Blocks:' is the real disk usage in blocks of 512 bytes (physical size)

List size files from DIR
OUTPUT: <logical_size_bytes> <physical_size_bytes> <filepath>

$ LC_ALL=C find DIR -type f -exec stat -c '%s %b %B %n' {} + 2>/dev/null | awk '{ fname=""; for (i=4; i <= NF; i++) fname=fname $i " "; print $1" "($2*$3)" "fname }'

Linux ext filesystem uses a default 4096 bytes for a block size because that’s the default pagesize of CPUs, so there’s an easy mapping between memory-mapped files and disk blocks.
The hardware (specifically, the Memory Management Unit, which is part of the CPU) determines what page sizes are possible. It is the smallest unit of data for memory management in a virtual memory operating system. Almost all architectures support a 4kB page size. Modern architectures support larger pages (and a few also support smaller pages), but 4kB is a very widespread default.

Get the filesystem block size in bytes
(size used internally by kernel, it may be modified by filesystem driver on mount)
# blockdev --getbsz /dev/<device>

Get the system's page size
(number of bytes in a memory page, where "page" is a fixed-length block, the unit for memory allocation and file mapping)
$ getconf PAGE_SIZE
$ getconf PAGESIZE
Print inode's metadata for a specific file/dir using stat command
$ LC_ALL=C stat /path/to/file_or_dir
$ LC_ALL=C stat -c '%i %y %A %U %s %N' /path/to/file_or_dir | sed -e 's;[.][0-9]\{9\} +[0-9]\{4\};;g'

Get inode number(s) with ls -i
$ ls -i1 /path/to/file_or_dir
Get the number of blocks a file uses on disk, so you can calculate disk space really used per file (physical file size).
IMPORTANT:
By default 'ls', 'du' and 'df' commands use 1block=1024bytes which may differ from the filesystem unit. You can use --block-size option or set environment variables:
Display values are in units of the first available SIZE from --block-size, DF_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set).

$ du --block-size=4096 /path/to/file
$ ls -s --block-size=4096 /path/to/file

ls -l prints the data size in bytes (logical file size), which is less than the actual used space on disk.

Inodes Metadata

$ man inode

Inode number
Each file in a filesystem has a unique inode number (except for hard links).
Inode numbers are guaranteed to be unique only within a filesystem (i.e. the same inode numbers may be used by different filesystems, which is the reason that hard links may not cross filesystem boundaries).

Device where inode resides
Each inode (as well as the associated file) resides in a filesystem that is hosted on a device.
That device is identified by the combination of its major ID (which identifies the general class of device) and minor ID (which identifies a specific instance in the general class).

Device represented by this inode
If the current file (inode) represents a device, then the inode records the major and minor ID of that device.

Links count (number of hard links to the file)

User ID (of the owner of the file)

Group ID (of the file)

Mode: File Type + Permissions (read, write and execute permissions of the file for the owner, group and others)
The standard Unix file types are regular, directory, symbolic link, FIFO (named pipe), block device, character device, and socket as defined by POSIX.

File size (in bytes)
This field gives the size of the file (if it is a regular file) in bytes.
The size of a symbolic link is the length of the pathname it contains, without a terminating null byte.
Default size for a directory is usually one block size (4096 bytes on most ext4 filesystems).

Preferred block size for I/O operations (in bytes)
This field gives the "preferred" block size for efficient filesystem I/O operations.
(Writing to a file in smaller chunks may cause an inefficient read-modify-rewrite)

Number of blocks allocated to the file
This field indicates the number of blocks allocated to the file in 512-byte units

File creation (birth) timestamp (btime)
This is set on file creation and not changed subsequently.
The btime timestamp was not historically present on UNIX systems and is not currently supported by most Linux filesystems.

Last modification timestamp (mtime)
This is the file’s last modification timestamp. It is changed by file modifications (file’s content: data).
Moreover, the mtime timestamp of a directory is changed by the creation or deletion of files in that directory.
The mtime timestamp is not changed for changes in file’s name, owner, group, hard link count, or mode.

Last access timestamp (atime)
It is changed by file accesses.

Last status change timestamp (ctime)
It is changed by modifying file’s metadata information (i.e. file’s name, owner, group, link count, mode, etc.).

According to The POSIX standard an inode is a "file serial number", defined as a per-filesystem unique identifier for a file.
Combined together with the device ID of the device containing the file, they uniquely identify the file within the whole system
.

Two files can have the same inode, but only if they are part of different partitions (except for hard links).
Inodes are only unique on a partition level, not on the whole system.

Directory Entry

You may have noticed that inodes do not contain the file’s name.
The file’s name is not stored in the inode metadata but in its directory structure.
UNIX systems use a directory stream mapping system: directory entries contain the filenames and their inodes number.

From a user perspective a directory contains files, technically a directory is a structure used to locate other files/directories.
In most Unix filesystems, a directory is a mapping from filenames to inode numbers
.
There’s a separate table mapping inode numbers to inode data.

The header file dirent.h describes the format of a directory entry.

Format of a Directory Entry
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/dirent.h.html
https://www.gnu.org/software/libc/manual/html_node/Directory-Entries.html

In the glibc implementation, the dirent structure is defined as follows:

struct dirent {
   ino_t   d_ino;              /* Inode number */
   off_t   d_off;              /* Not an offset */
   unsigned short   d_reclen;  /* Length of this record */
   unsigned char   d_type;     /* Type of file; not supported by all filesystem types */
   char d_name[256];           /* Null-terminated filename */
};

The only fields in the dirent structure that are mandated by POSIX.1 are d_name and d_ino.
The other fields are unstandardized, and not present on all systems.

/* This is the data type of directory stream objects. */
typedef struct __dirstream DIR;

The DIR data type represents a directory stream.
You shouldn’t ever allocate objects of the struct dirent or DIR data types, since the directory access functions do that for you. Instead, you refer to these objects using the pointers returned by the functions.
Directory streams are a high-level interface.

The design of data block pointers is actually more complex than the schema illustrates below, it also depends on the filesystem. For ext filesystem an inode pointer structure is used to list the addresses of a file’s data blocks (around 15 direct/indirect data blocks pointers).

Filesystem

Linux uses filesystems to manage data stored on storage devices.
The filesystem manages a map (inodes table) to locate each file placed in the storage device.
The filesystem divides the partition into blocks: small contiguous areas.
The size of these blocks is defined during creation of the filesystem.

Before you can mount a drive partition, you must format it using a filesystem.

The default filesystem used by most Linux distributions is ext4.
ext4 filesystem provides journaling, which is a method of tracking data not yet written to the drive in a log file, called the journal. If the system fails before the data can be written to the drive, the journal data can be recovered and stored upon the next system boot.

After creating a partition, you need to create a filesystem (mkfs program is specifically dedicated for that)

#  LC_ALL=C mkfs -t ext4 /dev/<partition_id>

Some filesystems (ext4 included), allocate a limited number of inodes when created.
If the filesystem runs out of inode entries in the table, you cannot create any more files, even if there is still space available on the drive: that may happen with a multitude of very small files.
When a file is created on the partition or volume, a new entry in the inode table is created.
Using the -i option with the df command will show you the percentage of inodes used.
It is theoretically possible (although uncommon) that you could run out of available inodes while still having actual space left on your partition, so it’s worth keeping that in mind.

Report file system disk space usage

By blocks (most important)
$ LC_ALL=C df -Th --block-size=4096 -x tmpfs -x devtmpfs -x squashfs 2>/dev/null

By inodes
$ LC_ALL=C df -Ti -x tmpfs -x devtmpfs -x squashfs 2>/dev/null

Linux uses e2fsprogs package to provide utilities for working with ext filesystems