
一、文件和目录
在日常开发或系统管理中,我们频繁地与文件和目录打交道。在Windows这类图形界面系统中,目录常以“文件夹”的直观形式展现,易于理解。而在工程领域,我们则更习惯称其为“目录”。
从直观感受上看,目录像一棵树,而文件是树上的叶子。虽然这个比喻在存储层面并不完全精确,但它为理解文件系统的基本架构提供了一个很好的起点。本文将深入探讨文件与目录在底层设计上的本质与区别。
二、本质:一切都是文件
你是否思考过,目录和文件在本质上有什么区别?在遵循“一切皆文件”理念的Linux系统中,它们的底层基础其实是相同的——都由inode(索引节点)和数据块构成。
当我们操作一个文件时,系统最终操作的对象就是这个文件的inode。inode之于文件,就如同身份证之于个人,它是文件在系统中的唯一标识。每当创建一个新文件,就会在磁盘的特定区域生成一个inode节点(正常情况下与文件一一对应)。每个inode拥有唯一的ID号,用于系统查找和操作。
那么,inode里究竟存储了哪些信息呢?它主要包含两大部分:文件的元数据和指向实际数据块的指针。
1. 元数据
inode中存储的元数据是文件的“身份信息”,主要包括:
- 文件编号:文件的唯一标识符。
- 文件类型:例如普通文件、目录、符号链接或设备文件等。
- 文件权限和所有者:包括所属用户、组以及读写执行权限。
- 时间戳:文件的创建时间、最后修改时间和最后访问时间。
- 文件大小:以字节为单位的文件数据总量。
- 链接数:指向该inode的硬链接数量。
- 其它属性:如扩展属性等。
如果你对内核实现感兴趣,可以参考以下inode结构体的定义:
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec64 __i_atime;
struct timespec64 __i_mtime;
struct timespec64 __i_ctime; /* use inode_*_ctime accessors! */
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
u8 i_blkbits;
enum rw_hint i_write_hint;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct rw_semaphore i_rwsem;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash;
struct list_head i_io_list; /* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
struct bdi_writeback *i_wb; /* the associated cgroup wb */
/* foreign inode detection, see wbc_detach_inode() */
int i_wb_frn_winner;
u16 i_wb_frn_avg_time;
u16 i_wb_frn_history;
#endif
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
struct list_head i_wb_list; /* backing dev writeback list */
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
atomic64_t i_version;
atomic64_t i_sequence; /* see futex */
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
#if defined(CONFIG_IMA) || defined(CONFIG_FILE_LOCKING)
atomic_t i_readcount; /* struct files open RO */
#endif
union {
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
void (*free_inode)(struct inode *);
};
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct fsnotify_mark_connector __rcu *i_fsnotify_marks;
#endif
#ifdef CONFIG_FS_ENCRYPTION
struct fscrypt_inode_info *i_crypt_info;
#endif
#ifdef CONFIG_FS_VERITY
struct fsverity_info *i_verity_info;
#endif
void *i_private; /* fs or device private pointer */
} __randomize_layout;
以下是Ext4文件系统中目录项的结构定义,展示了文件名如何与inode关联:
struct ext4_dir_entry_2 {
__le32 inode; /* Inode number */
__le16 rec_len; /* Directory entry length */
__u8 name_len; /* Name length */
__u8 file_type; /* See file type macros EXT4_FT_* below */
char name[EXT4_NAME_LEN]; /* File name */
};
2. 数据块的指针
指针是inode找到其“身体”(数据)的关键,主要分为三种类型:
- 直接指针:直接指向存储文件真实数据的数据块。通常一个inode会包含12个直接指针。
- 间接指针:可理解为二级指针,指向一个数据块,而这个数据块里存储着更多指向其他数据块的指针。这种设计主要用于支持大文件。
- 扩展指针:某些现代文件系统(如Ext4)使用扩展树来优化大文件存储,以减少间接指针的层级,提升访问效率。
这里有一个关键点容易被忽略:文件名并不存储在inode中。文件名实际上记录在其所属目录的数据块里。目录的数据块包含一系列目录项,每个目录项就是一个“文件名-inode编号”的映射表。
这就像图书馆的索引系统:你不会去翻遍每一个书架找书,而是先查目录卡片(文件名),卡片会告诉你书在哪个具体位置(inode编号),然后你就能快速找到它(数据块)。
另外需要了解的是,inode的数量是有限的,它决定了磁盘上能创建的文件/目录总数。例如,在Ext4文件系统中,默认每4KB空间分配一个inode(可通过mkfs.ext4 -i调整)。这意味着,如果系统存在海量小文件(如日志、缩略图),可能会先耗尽inode数量,即使磁盘空间还有剩余,也无法创建新文件。在Linux中,可以使用 ls -al、stat、df -i 等命令查看inode和文件详情。
三、区别
尽管目录和文件本质相同,但在具体实现和应用上存在显著差异:
-
inode类型不同
这是最根本的区别。通过stat命令查看时,普通文件的类型为S_IFREG,目录则为S_IFDIR。在ls -l的输出中,行首的第一个字符‘-’代表普通文件,而‘d’则代表目录。
-
数据内容不同
普通文件的数据块存储的是文件的实际内容(文本、二进制数据等)。而目录的数据块存储的是目录项列表,即前面提到的文件名到inode的映射表。
-
操作行为不同
由于其设计目的不同,允许的操作也不同。例如,文件可以使用read、write函数进行读写,而目录则不能。目录有专门的操作函数,如opendir、readdir、rmdir等。
-
链接处理不同
一般情况下,操作系统不允许为目录创建硬链接(除了固有的.和..),但文件可以创建硬链接。
简单总结:文件由inode和数据块组成,inode存元数据和数据块指针,数据块存真实内容;目录也由inode和数据块组成,但其数据块存的是目录项(文件名-inode映射表)。
四、相关例程
以下是从Linux内核中摘录的,关于操作inode创建文件和特殊设备节点的简化代码,有助于理解上述原理在代码层面是如何实现的:
// 创建普通文件:涉及inode创建、更新目录项(建立映射)以及绑定操作函数
static int ext4_create(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode, bool excl)
{
handle_t *handle;
struct inode *inode;
int err, credits, retries = 0;
err = dquot_initialize(dir);
if (err)
return err;
credits = (EXT4_DATA_TRANS_BLOCKS(dir->i_sb) +
EXT4_INDEX_EXTRA_TRANS_BLOCKS + 3);
retry:
inode = ext4_new_inode_start_handle(idmap, dir, mode, &dentry->d_name,
0, NULL, EXT4_HT_DIR, credits);
handle = ext4_journal_current_handle();
err = PTR_ERR(inode);
if (!IS_ERR(inode)) {
inode->i_op = &ext4_file_inode_operations;
inode->i_fop = &ext4_file_operations;
ext4_set_aops(inode);
err = ext4_add_nondir(handle, dentry, &inode);
if (!err)
ext4_fc_track_create(handle, dentry);
}
if (handle)
ext4_journal_stop(handle);
if (!IS_ERR_OR_NULL(inode))
iput(inode);
if (err == -ENOSPC && ext4_should_retry_alloc(dir->i_sb, &retries))
goto retry;
return err;
}
// 用于创建特殊文件节点,如字符设备、块设备、FIFO和Socket文件
static int ext4_mknod(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode, dev_t rdev)
{
handle_t *handle;
struct inode *inode;
int err, credits, retries = 0;
err = dquot_initialize(dir);
if (err)
return err;
credits = (EXT4_DATA_TRANS_BLOCKS(dir->i_sb) +
EXT4_INDEX_EXTRA_TRANS_BLOCKS + 3);
retry:
inode = ext4_new_inode_start_handle(idmap, dir, mode, &dentry->d_name,
0, NULL, EXT4_HT_DIR, credits);
handle = ext4_journal_current_handle();
err = PTR_ERR(inode);
if (!IS_ERR(inode)) {
init_special_inode(inode, inode->i_mode, rdev);
inode->i_op = &ext4_special_inode_operations;
err = ext4_add_nondir(handle, dentry, &inode);
if (!err)
ext4_fc_track_create(handle, dentry);
}
if (handle)
ext4_journal_stop(handle);
if (!IS_ERR_OR_NULL(inode))
iput(inode);
if (err == -ENOSPC && ext4_should_retry_alloc(dir->i_sb, &retries))
goto retry;
return err;
}
以上代码仅作原理示例,无需深入分析每一行。
五、总结
一个文件系统,其核心可以简化为三个部分:目录、inode和数据块。然而,看似简单的设计目标——“既要简单易用,又要快速高效,还要适配不断发展的硬件”——使得其底层实现变得极其复杂。这也正是文件系统种类繁多且持续演进的重要原因。深入理解inode和目录的基本原理,是掌握操作系统和进行系统级编程的坚实基础。