跟我学C++中级篇——文件和目录

一、文件和目录

在前面的分析中,对文件和链接以及重定向进行了初步的分析,已经初步了解了一些文件的基础知识。只要使用电脑的人,都离不开文件和盛放文件的"容器"------目录。在Windows告示这种视窗界面中,很好区分,目录就是可以看到的文件夹。大家可以把文件夹当成目录的一种别名,它容易为广大应用者理解和认知。而目录更倾向于工程领域一些,对普通人来说,相对有些专业。

从形式上看,大家可以把目录当作一棵树,而文件是树上的叶子即可。但是实际上这种描述并不准确,如果有数据库底层存储经验的开发者很容易明白其问题所在,但这并不影响普通开发者去理解底层的设计。下面将会对其进行详细的说明。

二、本质

虽然说看上去目录和文件有显著的不同,但在本质上,它们都是文件。这也符合前文提到的,在Linux中,一切皆文件的思想。既然本质都是文件,那它们一定是由inode(index node)和数据块组成的。下面对inode进行初步的分析。

在进行文件操作时,操作提文件句柄,而最终实际操作的对象就是这个inode,为什么叫node,大家应该明白,其实文件系统的底层组织就是树的数据结构。每当创建一个文件的时候,就会创建一个inode节点(正常情况下inode和文件是一一映射的,但在某些情况下,如硬链接等可能不是)并存储在硬件磁盘的指定区域。每个inode是有自己的唯一ID的,这个ID就是用来查找和操作这个文件的标识。在创建的inode中,主要存储着文件的元数据和实际存储数据的数据块的指针。具体包括:

  1. 元数据
    inode节点中存储的元数据主要有:
    文件编号:文件的唯一标识;
    文件类型:普通文件、目录、符号链接或设备文件等;
    文件权限和所有者:分属的组或用户以及读写执行权限等;
    时间戳:包括文件的创建、修改和访问时间等;
    文件大小:一般是以字节为单位的文件总的数据量统计;
    链接数:指硬链接的数量;
    其它属性:如扩展属性等
    有兴趣的可以看一看内核中对其的定义:
c 复制代码
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中的目录结构定义:

c 复制代码
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 */
};
  1. 数据块的指针
    指针分为三种类型:
    直接指针:好理解,指针指向的地址存储着真正的文件数据块。通常有12个直接指针
    间接指针:可以理解成二级指针,即指向的指针的数据块中存储着更多的其它间接指针,主要用来支持大文件
    扩展指针:特定的某些文件系统使用扩展树来优化大文件的存储,减少间接指针的层级。如Ext4中就采用了这种方式

大家所看到的文件名称则并没有存储在inode中,是不是有点意外?文件名是存储在目录的数据块中的。在目录的数据块中,存储了一组目录项,而每个目录项则是一个文件名与inode的映射。这这其实也很好理解,就像存储物品一样,总不打开一个个的抽屉去逐个查找,而是最好分门别类的放好,只在抽屉外的目录上找就可以了。文件系统也是这个样子,先通过目录名找到inode,再通过其映射到具体的文件的数据即可。不过大家需要注意的是,inode的数量不是无限的,它在每一个具体的环境中,是有着数量大小的限制的。说这个可能有人不清楚,其实就是inode的数量就是磁盘的真实的容量。如果不考虑严谨性,对用户来说,inode数量就是磁盘的空间大小。比如Ext4文件系统默认每4KB分配一个inode(当然是可以调整大小的,使用命令mkfs.ext4 -i 大小)。一个1T的硬盘一般是有大约不到3亿的inode节点。

这意味着,如果系统中存在着大量的小文件如缩略图、小日志等等,就有可能inode会先于磁盘空间耗尽,导致无法存储数据。

在Linux环境中,可以通过命令"ls -al"和"stat"、"df"或"du"等来查看上述具体的信息。

三、区别

虽然目录和文件本质是一样,但它们从具体的实现用应用上还是有很大区别的,主要表现在:

  1. inode的类型不同
    这是最根本的不同,在通过stat命令查看时,可以发现,inode的类型在文件和目录时分别为:S_IFREG,S_IFDIR。说明一下,在命令的输出结果中"drwxrwxr-x",第一位的"d"表示是S_IFDIR,"-rw-rw-r--"中的第一位"-"表示S_IFREG。
  2. 数据内容不同
    普通文件中数据块中存储的是文件的内容,如常见的文本或二进制等;而目录中数据块存储的是目录项,即前面提到的文件名和inode的映射表。可以使用"ls -i"或"df -i"等来查看
  3. 操作行为不同
    这个就好理解了,由于其设计的目的不同,那么对目录的操作和文件的操作表现肯定大有区别。比如文件可以进行读写(read write函数)等操作,但目录不可以;目录有专门的操作方法,如函数opendir,rmdir等
  4. 链接的处理不同
    一般情况下,目录不允许创建硬链接(.和...可以),但文件可以。

简单总结一下,文件一般是由inode和数据块组成,inode存储元数据和数据块的指针,而数据块中存储了真正的文件内容;虽然目录也是inode和数据块组成,但其数据块中存储的是一组目录项,目录项中存储着文件和inode的映射表。

四、相关例程

下面给出内核中操作inode的代码:

c 复制代码
//创建文件: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和数据块。但看上去简单的东西,实现起来却相当复杂。它既要满足简单方便,又要满足快速的操作,还要满足不断发展的硬件等等。当需要变成既要又要时,问题就会变得非常复杂,这也是为什么文件系统有不少种类并且在不断发展的一个重要原因。

相关推荐
jojo_zjx1 小时前
GESP 23年9月2级 小杨的X字矩阵
c++
君生我老1 小时前
C++ list类容器常用操作
开发语言·c++
Pth_you1 小时前
Uptime Kuma安装/定时通知脚本
linux·运维·安全
leo03081 小时前
Ubuntu (NVIDIA Jetson) 开启 Wi-Fi 后系统高延迟、Ping 不通甚至硬死机排查全过程
linux·运维·ubuntu
济6171 小时前
linux 系统移植(第八期)----Linux 内核的获取、编译、顶层 Makefile 的简介-- Ubuntu20.04
linux
ouliten2 小时前
C++笔记:std::tuple
c++·笔记
Ha_To2 小时前
2026.1.16 Linux磁盘实验
linux·运维·服务器
2023自学中2 小时前
嵌入式系统中的非易失性存储设备
linux·嵌入式硬件
小龙报2 小时前
【初阶数据结构】解锁顺序表潜能:一站式实现高效通讯录系统
c语言·数据结构·c++·程序人生·算法·链表·visual studio