Linux 以文件的形式对计算机中的数据和硬件资源进行管理,也就是彻底的一切皆文件,反映在 Linux 的文件类型上就是:普通文件、目录文件(也就是文件夹)、设备文件、链接文件、管道文件、套接字文件(数据通信的接口)等等。Linux 上的文件系统一般来说就是 EXT2/3/4。 EXT 是 延 伸 文 件 系 统 ( 英 语 : Extended file system,缩写为 ext 或 ext1),也译为扩展文件系统,一种文件系统,于 1992 年 4 月发表,是为 linux 核心所做的第一个文件系统。ext2既有超级块的速度又有非常小的cpu占用率,可用于硬盘和移动存储设备;ext3在ext2的基础上增加了日志功能,可以回溯;ext4是日志式的文件系统,容量可支持1EB,最大单个文件支持16TB,支持连续写入以减少文件碎片,现在是linux默认的文件系统。 除此之外,linux还支持文件系统xfs(rehl的默认文件系统,可以管理500TB的硬盘)、brtfs(针对固态硬盘做了优化)。还有windows支持的文件系统fat、ntfs等。这些不是今天介绍的主角,就不赘述了。
硬盘结构
先看一看硬盘的结构 扇区是硬盘上最小的物理存储单位,一个扇区512B。 柱面是系统分区的最小单位。由所有盘面上半径相同的磁道们组成,即柱面上所有磁道与主轴距离相等,目的是提高磁盘读写速度。磁头在同一时间处于同一半径上,为了提高存取速度,数据一般按照柱面的顺序来存储。
ext2
EXT2 第 二 代 扩 展 文 件 系 统 ( 英 语 : secondextended filesystem,缩写为 ext2),是 LINUX 内核所用的文件系统。它开始由 Rémy Card 设计,用以代替ext,于 1993 年 1 月加入 linux 核心支持之中。ext2 的经典实现为 LINUX 内核中的 ext2fs 文件系统驱动,最大可支持 2TB 的文件系统,至 linux 核心 2.6 版时,扩展到可支持 32TB。 ext2文件系统是 Linux 系统中的标准文件系统,是通过对 Minix 的文件系统进行扩展而得到的,其存取文件的性能极好。在ext2 文件系统中,文件由 inode(包含有文件的所有信息)进行唯一标识。一个文件可能对应多个文件名,只有在所有文件名都被删除后,该文件才会被删除。此外,同一文件在磁盘中存放和被打开时所对应的 inode是不同的,并由内核负责同步。 在 ext2 系统中,所有元数据结构的大小均基于“块”,而不是“扇区”。块的大小随文件系统的大小而有所不同。而一定数量的块又组成一个块组,每个块组的起始部分有多种多样的描述该块组各种属性的元数据结构。
相关数据结构
ext2系统的磁盘布局
超级块
超级块(Super Block)描述整个分区的文件系统信息,如inode/block的大小、总量、使用量、剩余量,以及文件系统的格式与相关信息。超级块在每个块组的开头都有一份拷贝(第一个块组必须有,后面的块组可以没有)。 为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作,就必须保证文件系统的super block信息在这种情况下也能正常访问。所以一个文件系统的super block会在多个block group中进行备份,这些super block区域的数据保持一致。
struct ext2_super_block {
__le32 s_inodes_count;
__le32 s_blocks_count;
__le32 s_r_blocks_count;
__le32 s_free_blocks_count;
__le32 s_free_inodes_count;
__le32 s_first_data_block;
__le32 s_log_block_size;
__le32 s_log_frag_size;
__le32 s_blocks_per_group;
__le32 s_frags_per_group;
__le32 s_inodes_per_group;
__le32 s_mtime;
__le32 s_wtime;
__le16 s_mnt_count;
__le16 s_max_mnt_count;
__le16 s_magic;
__le16 s_state;
__le16 s_errors;
__le16 s_minor_rev_level;
__le32 s_lastcheck;
__le32 s_checkinterval;
__le32 s_creator_os;
__le32 s_rev_level;
__le16 s_def_resuid;
__le16 s_def_resgid;
__le32 s_first_ino;
__le16 s_inode_size;
__le16 s_block_group_nr;
__le32 s_feature_compat;
__le32 s_feature_incompat;
__le32 s_feature_ro_compat;
__u8 s_uuid[16];
char s_volume_name[16];
char s_last_mounted[64];
__le32 s_algorithm_usage_bitmap;
__u8 s_prealloc_blocks;
__u8 s_prealloc_dir_blocks;
__u16 s_padding1;
__u8 s_journal_uuid[16];
__u32 s_journal_inum;
__u32 s_journal_dev;
__u32 s_last_orphan;
__u32 s_hash_seed[4];
__u8 s_def_hash_version;
__u8 s_reserved_char_pad;
__u16 s_reserved_word_pad;
__le32 s_default_mount_opts;
__le32 s_first_meta_bg;
__u32 s_reserved[190];
};
块组描述符
使用块组描述符用以描述一个块组的属性。
struct ext2_group_desc
{
__le32 bg_block_bitmap;
__le32 bg_inode_bitmap;
__le32 bg_inode_table;
__le16 bg_free_blocks_count;
__le16 bg_free_inodes_count;
__le16 bg_used_dirs_count;
__le16 bg_pad;
__le32 bg_reserved[3];
};
inode 表
inode 表用于跟踪定位每个文件,包括位置、大小等(但不包括文件名),一个块组只有一个 inode 表。
struct ext2_inode {
__le16 i_mode;
__le16 i_uid;
__le32 i_size;
__le32 i_atime;
__le32 i_ctime;
__le32 i_mtime;
__le32 i_dtime;
__le16 i_gid;
__le16 i_links_count;
__le32 i_blocks;
__le32 i_flags;
union {
struct {
__le32 l_i_reserved1;
} linux1;
struct {
__le32 h_i_translator;
} hurd1;
struct {
__le32 m_i_reserved1;
} masix1;
} osd1;
__le32 i_block[EXT2_N_BLOCKS];
__le32 i_generation;
__le32 i_file_acl;
__le32 i_dir_acl;
__le32 i_faddr;
union {
struct {
__u8 l_i_frag;
__u8 l_i_fsize;
__u16 i_pad1;
__le16 l_i_uid_high;
__le16 l_i_gid_high;
__u32 l_i_reserved2;
} linux2;
struct {
__u8 h_i_frag;
__u8 h_i_fsize;
__le16 h_i_mode_high;
__le16 h_i_uid_high;
__le16 h_i_gid_high;
__le32 h_i_author;
} hurd2;
struct {
__u8 m_i_frag;
__u8 m_i_fsize;
__u16 m_pad1;
__u32 m_i_reserved2[2];
} masix2;
} osd2;
};
目录结构
在 ext2 文件系统中,目录是作为文件存储的。
struct ext2_dir_entry {
__le32 inode;
__le16 rec_len;
__le16 name_len;
char name[];
};
struct ext2_dir_entry_2 {
__le32 inode;
__le16 rec_len;
__u8 name_len;
__u8 file_type;
char name[];
};
ext3
Ext3是Ext2的增强版,与Ext2兼容,其磁盘数据结构与Ext2基本相同,在Ext2的基础上重点强化了日志功能。这也是ext3的优势,ext2升级到ext3,不需要备份和恢复数据。 文件系统通常有两种块,包含元数据的块和包含普通文件数据的块,Ext2和Ext3中元数据是指超级块,块组描述符,索引节点,位图块等,不同文件系统使用不同的元数据。Ext3支持将元数据块和普通文件数据块都写入日志中,具体提供了三种不同大的日志模式。
- 日志(journal),文件系统的所有文件数据和元数据的改变都写入日志中。该模式减少了文件修改丢失,但是增加了额外的磁盘开销,是最安全和最慢的模式
- 顺序(ordered),只有文件系统的元数据的修改才写入日志,但是保证当元数据块和相关的文件数据块都需要写入磁盘时,文件数据块会比元数据块先写入磁盘,此时元数据块已经在日志中保存了一个副本。该模式是Linux的默认模式,可以减少普通文件修改的丢失,因为要维护元数据块和普通文件块的相关性和两者的磁盘写入顺序,相比写回模式有轻微的性能损耗。
- 写回(writeback),只有文件系统的元数据的修改才写入日志,对元数据块和普通文件数据块写入磁盘的顺序不做限制,由页高速缓存的脏页刷新机制决定。该模式是其他日志文件系统使用的方式,是最快的模式,但是当系统故障时存在文件损坏的风险。
当然,ext3还有缺陷,缺少动态的inode和树状的资料存放结构。
ext4
EXT4 是第四代扩展文件系统,是 Linux 系统下的日志文件系统,是 ext3 文件系统的后继版本。Ext4 产生原因是开发人员在 Ext3 中加入了新的高级功能,但在实现的过程出现了几个重要问题:
- 一些新功能违背向后兼容性
- 新功能使 Ext3 代码变得更加复杂并难以维护
- 新加入的更改使原来十分可靠的 Ext3 变得不可靠。
为了解决这些问题,ext4应运而生,其有以下特性:
- 更大的文件系统和更大的文件:Ext3-32TB 文件系统和2TB 文件。Ext4-1EB 文件系统和 16TB 文件 容量
- 更多的子目录数:Ext3 支持 32000 个子目录,Ext4 取消此限制
- 更多的块和 inode数量:ext3使用32 位空间记录块数量,ext4使用64 位空间记录块数量
- 多块分配:ext3一次只能分配一个4KB的块,ext4有多块分配器,可以一次性分配多个块,提升分配速度。
- 延迟分配:ext3的数据块分配是尽快分配,ext4是尽量延迟分配,直到文件在缓存中写完才分配数据块,写入到磁盘中。这样做可以优化整个文件数据块的分配,使性能得意提升。
- 日志校验功能:日志是文件系统最常用的基本数据结构。日志很容易受到破坏,从损坏的日志中恢复数据可能会导致更多的数据损坏。对于ext4,有了日志检验功能,可以判断日志的数据是否已损坏。
- 支持“无日志”模式:日志总会占用一定开销,ext4可以关闭日志功能,以便一些有特殊要求的用户借此来提升性能。
- 在线碎片整理:尽管延迟分配,多块分配可以有效减少文件碎片,但是不能完全避免。ext4 通过 e4defrag 提供支持在线碎片整理。
打开文件流程
打开文件的三个步骤:
- 需要在父目录的数据中查找文件对应的目录项,从目录项得到索引节点的编号,然后在内存中创建索引节点的副本。
- 需要分配文件的一个打开实例,即file 结构体,关联到文件的索引节点。
- 在进程的打开文件表中分配一个文件描述符,把文件描述符和打开实例的映射添加到进程的打开文件表中。
系统调用 open 和 openat 都把主要工作委托给函数do_sys_open , open 传 入 特 殊 的 文 件 描 述 符AT_FDCWD,表示“如果文件路径是相对路径,就解释为相对调用进程的当前工作目录"。
系统调用 open 和 openat 都把主要工作委托给函数do_sys_open , open 传 入 特 殊 的 文 件 描 述 符AT_FDCWD ,表示如果文件路径是相对路径,就解释为相对调用进程的当前工作目录。 结合流程图看代码
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
int fd = build_open_flags(flags, mode, &op);
struct filename *tmp;
if (fd)
return fd;
tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f);
}
}
putname(tmp);
return fd;
}
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(dfd, filename, flags, mode);
}
继续往下跟,先看build_open_flags
static inline int build_open_flags(int flags, umode_t mode, struct open_flags *op)
{
int lookup_flags = 0;
int acc_mode = ACC_MODE(flags);
flags &= VALID_OPEN_FLAGS;
if (flags & (O_CREAT | __O_TMPFILE))
op->mode = (mode & S_IALLUGO) | S_IFREG;
else
op->mode = 0;
flags &= ~FMODE_NONOTIFY & ~O_CLOEXEC;
if (flags & __O_SYNC)
flags |= O_DSYNC;
if (flags & __O_TMPFILE) {
if ((flags & O_TMPFILE_MASK) != O_TMPFILE)
return -EINVAL;
if (!(acc_mode & MAY_WRITE))
return -EINVAL;
} else if (flags & O_PATH) {
flags &= O_DIRECTORY | O_NOFOLLOW | O_PATH;
acc_mode = 0;
}
op->open_flag = flags;
if (flags & O_TRUNC)
acc_mode |= MAY_WRITE;
if (flags & O_APPEND)
acc_mode |= MAY_APPEND;
op->acc_mode = acc_mode;
op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN;
if (flags & O_CREAT) {
op->intent |= LOOKUP_CREATE;
if (flags & O_EXCL)
op->intent |= LOOKUP_EXCL;
}
if (flags & O_DIRECTORY)
lookup_flags |= LOOKUP_DIRECTORY;
if (!(flags & O_NOFOLLOW))
lookup_flags |= LOOKUP_FOLLOW;
op->lookup_flags = lookup_flags;
return 0;
}
get_unused_fd_flags
int __alloc_fd(struct files_struct *files,
unsigned start, unsigned end, unsigned flags)
{
unsigned int fd;
int error;
struct fdtable *fdt;
spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files);
fd = start;
if (fd < files->next_fd)
fd = files->next_fd;
if (fd < fdt->max_fds)
fd = find_next_fd(fdt, fd);
error = -EMFILE;
if (fd >= end)
goto out;
error = expand_files(files, fd);
if (error < 0)
goto out;
if (error)
goto repeat;
if (start <= files->next_fd)
files->next_fd = fd + 1;
__set_open_fd(fd, fdt);
if (flags & O_CLOEXEC)
__set_close_on_exec(fd, fdt);
else
__clear_close_on_exec(fd, fdt);
error = fd;
#if 1
if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL);
}
#endif
out:
spin_unlock(&files->file_lock);
return error;
}
static int alloc_fd(unsigned start, unsigned flags)
{
return __alloc_fd(current->files, start, rlimit(RLIMIT_NOFILE), flags);
}
int get_unused_fd_flags(unsigned flags)
{
return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags);
}
EXPORT_SYMBOL(get_unused_fd_flags);
do_filp_open
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
struct nameidata nd;
int flags = op->lookup_flags;
struct file *filp;
set_nameidata(&nd, dfd, pathname);
filp = path_openat(&nd, op, flags | LOOKUP_RCU);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(&nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
restore_nameidata();
return filp;
}
看一下nameidata结构体
#define EMBEDDED_LEVELS 2
struct nameidata {
struct path path;
struct qstr last;
struct path root;
struct inode *inode;
unsigned int flags;
unsigned seq, m_seq;
int last_type;
unsigned depth;
int total_link_count;
struct saved {
struct path link;
struct delayed_call done;
const char *name;
unsigned seq;
} *stack, internal[EMBEDDED_LEVELS];
struct filename *name;
struct nameidata *saved;
struct inode *link_inode;
unsigned root_seq;
int dfd;
};
上面的代码,提到了RCU查找方式和引用查找方式。
- rcu-walk方式:采用RCU锁的方式进行查找,并发查找过程中并不会因为等待spinlock而阻塞,因此速度相对更快。但是它并不能保证所有情况下都能查找成功。RCU锁:读者不需要获得任何锁就可访问RCU保护的临界区;写者在访问临界区时,写者“自己”将先拷贝一个临界区副本,然后对副本进行修改。此外,RCU还有相应的垃圾回收机制,在所有的读者访问结束时,会删除旧数据。RCU的适用场景是频繁地读取数据的场景,频繁写入的话就不要用,可以考虑宽限期RCU。
- ref-walk方式:指的是路径查找过程中,使用Spinlock并发使用或者修改目录项,来保证系统最终目录项内容的正确性。但是Spinlock因为会引发阻塞,所以效率会低于RCU-Walk。
关闭文件流程
进程可以使用系统调用 close 关闭文件。另外,在进程退出时,内核将会把进程打开的所有文件关闭。 流程图加代码
SYSCALL_DEFINE1(close, unsigned int, fd)
{
int retval = __close_fd(current->files, fd);
if (unlikely(retval == -ERESTARTSYS ||
retval == -ERESTARTNOINTR ||
retval == -ERESTARTNOHAND ||
retval == -ERESTART_RESTARTBLOCK))
retval = -EINTR;
return retval;
}
EXPORT_SYMBOL(sys_close);
__close_fd
int __close_fd(struct files_struct *files, unsigned fd)
{
struct file *file;
struct fdtable *fdt;
spin_lock(&files->file_lock);
fdt = files_fdtable(files);
if (fd >= fdt->max_fds)
goto out_unlock;
file = fdt->fd[fd];
if (!file)
goto out_unlock;
rcu_assign_pointer(fdt->fd[fd], NULL);
__clear_close_on_exec(fd, fdt);
__put_unused_fd(files, fd);
spin_unlock(&files->file_lock);
return filp_close(file, files);
out_unlock:
spin_unlock(&files->file_lock);
return -EBADF;
}
filp_close
int filp_close(struct file *filp, fl_owner_t id)
{
int retval = 0;
if (!file_count(filp)) {
printk(KERN_ERR "VFS: Close: file count is 0\n");
return 0;
}
if (filp->f_op->flush)
retval = filp->f_op->flush(filp, id);
if (likely(!(filp->f_mode & FMODE_PATH))) {
dnotify_flush(filp, id);
locks_remove_posix(filp, id);
}
fput(filp);
return retval;
}
fput最终会调用__fput
static void __fput(struct file *file)
{
struct dentry *dentry = file->f_path.dentry;
struct vfsmount *mnt = file->f_path.mnt;
struct inode *inode = file->f_inode;
might_sleep();
fsnotify_close(file);
eventpoll_release(file);
locks_remove_file(file);
if (unlikely(file->f_flags & FASYNC)) {
if (file->f_op->fasync)
file->f_op->fasync(-1, file, 0);
}
ima_file_free(file);
if (file->f_op->release)
file->f_op->release(inode, file);
security_file_free(file);
if (unlikely(S_ISCHR(inode->i_mode) && inode->i_cdev != NULL &&
!(file->f_mode & FMODE_PATH))) {
cdev_put(inode->i_cdev);
}
fops_put(file->f_op);
put_pid(file->f_owner.pid);
if ((file->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
i_readcount_dec(inode);
if (file->f_mode & FMODE_WRITER) {
put_write_access(inode);
__mnt_drop_write(mnt);
}
file->f_path.dentry = NULL;
file->f_path.mnt = NULL;
file->f_inode = NULL;
file_free(file);
dput(dentry);
mntput(mnt);
}
|