一、文件和目录
在前面的分析中,对文件和链接以及重定向进行了初步的分析,已经初步了解了一些文件的基础知识。只要使用电脑的人,都离不开文件和盛放文件的"容器"------目录。在Windows告示这种视窗界面中,很好区分,目录就是可以看到的文件夹。大家可以把文件夹当成目录的一种别名,它容易为广大应用者理解和认知。而目录更倾向于工程领域一些,对普通人来说,相对有些专业。
从形式上看,大家可以把目录当作一棵树,而文件是树上的叶子即可。但是实际上这种描述并不准确,如果有数据库底层存储经验的开发者很容易明白其问题所在,但这并不影响普通开发者去理解底层的设计。下面将会对其进行详细的说明。
二、本质
虽然说看上去目录和文件有显著的不同,但在本质上,它们都是文件。这也符合前文提到的,在Linux中,一切皆文件的思想。既然本质都是文件,那它们一定是由inode(index node)和数据块组成的。下面对inode进行初步的分析。
在进行文件操作时,操作提文件句柄,而最终实际操作的对象就是这个inode,为什么叫node,大家应该明白,其实文件系统的底层组织就是树的数据结构。每当创建一个文件的时候,就会创建一个inode节点(正常情况下inode和文件是一一映射的,但在某些情况下,如硬链接等可能不是)并存储在硬件磁盘的指定区域。每个inode是有自己的唯一ID的,这个ID就是用来查找和操作这个文件的标识。在创建的inode中,主要存储着文件的元数据和实际存储数据的数据块的指针。具体包括:
- 元数据
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 */
};
- 数据块的指针
指针分为三种类型:
直接指针:好理解,指针指向的地址存储着真正的文件数据块。通常有12个直接指针
间接指针:可以理解成二级指针,即指向的指针的数据块中存储着更多的其它间接指针,主要用来支持大文件
扩展指针:特定的某些文件系统使用扩展树来优化大文件的存储,减少间接指针的层级。如Ext4中就采用了这种方式
大家所看到的文件名称则并没有存储在inode中,是不是有点意外?文件名是存储在目录的数据块中的。在目录的数据块中,存储了一组目录项,而每个目录项则是一个文件名与inode的映射。这这其实也很好理解,就像存储物品一样,总不打开一个个的抽屉去逐个查找,而是最好分门别类的放好,只在抽屉外的目录上找就可以了。文件系统也是这个样子,先通过目录名找到inode,再通过其映射到具体的文件的数据即可。不过大家需要注意的是,inode的数量不是无限的,它在每一个具体的环境中,是有着数量大小的限制的。说这个可能有人不清楚,其实就是inode的数量就是磁盘的真实的容量。如果不考虑严谨性,对用户来说,inode数量就是磁盘的空间大小。比如Ext4文件系统默认每4KB分配一个inode(当然是可以调整大小的,使用命令mkfs.ext4 -i 大小)。一个1T的硬盘一般是有大约不到3亿的inode节点。
这意味着,如果系统中存在着大量的小文件如缩略图、小日志等等,就有可能inode会先于磁盘空间耗尽,导致无法存储数据。
在Linux环境中,可以通过命令"ls -al"和"stat"、"df"或"du"等来查看上述具体的信息。
三、区别
虽然目录和文件本质是一样,但它们从具体的实现用应用上还是有很大区别的,主要表现在:
- inode的类型不同
这是最根本的不同,在通过stat命令查看时,可以发现,inode的类型在文件和目录时分别为:S_IFREG,S_IFDIR。说明一下,在命令的输出结果中"drwxrwxr-x",第一位的"d"表示是S_IFDIR,"-rw-rw-r--"中的第一位"-"表示S_IFREG。 - 数据内容不同
普通文件中数据块中存储的是文件的内容,如常见的文本或二进制等;而目录中数据块存储的是目录项,即前面提到的文件名和inode的映射表。可以使用"ls -i"或"df -i"等来查看 - 操作行为不同
这个就好理解了,由于其设计的目的不同,那么对目录的操作和文件的操作表现肯定大有区别。比如文件可以进行读写(read write函数)等操作,但目录不可以;目录有专门的操作方法,如函数opendir,rmdir等 - 链接的处理不同
一般情况下,目录不允许创建硬链接(.和...可以),但文件可以。
简单总结一下,文件一般是由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和数据块。但看上去简单的东西,实现起来却相当复杂。它既要满足简单方便,又要满足快速的操作,还要满足不断发展的硬件等等。当需要变成既要又要时,问题就会变得非常复杂,这也是为什么文件系统有不少种类并且在不断发展的一个重要原因。