ok,继前面概念篇之后,我们开始正式的探讨下Linux的文件系统。
文件系统是对一个存储设备上的数据和元数据进行组织的机制(教材式还是需要的),在前面的概念篇有说到,Linux支持大多数文件系统,可以预料到Linux文件系统接口实现为分层的体系结构,从而将用户接口层、文件系统实现和操作存储设备的驱动程序分隔开。
Linux源码(Linux/fs文件夹下)下会有Linux支持的各种文件系统的代码实现,每种文件系统之间肯定是存在差异的,应用层上层总不能为了支持每种文件系统,而单独的实现每种文件系统的接口吧,为此,Linux引入了VFS虚拟文件系统,这个抽象的界面主要由一组标准、抽象的统一的文件操作构成,以系统调用的形式提供给用户程序。
简单的说就是,虚拟文件系统对用户程序隐去了各种不同问价系统的实现细节,为用户程序提供了一个统一的、抽象的、虚拟的文件系统的界面。下层不同的文件系统则通过不同的程序来实现各种功能。
实际上这也是Unix设计哲学中的一个很重要的设计思想,在Linux内核网络协议栈中也是采用的这种思想,上层接口屏蔽下层的差异,实际上在C++语义中,结合虚函数机制,面向对象实现多态也是一个道理,在我们的平时的编程开发中也是可以借鉴的。
那么,其中是怎么实现的呢?
我们通过Linux kernel 代码来探讨(include/linux/fs.h)
这个虚拟文件系统的主题就是一个file_operations数据结构
/* * NOTE: * read, write, poll, fsync, readv, writev can be called * without the big kernel lock held in all filesystems. */
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
};
该结构当中成分全是函数指针,实际上是一个函数跳转表,细看这些函数指针,都是一些常规的文件操作函数,比如read就是指向具体文件系统用来实现读文件操作的入口函数。
以具体到某一种文件系统ext2为例,看代码(linux/fs/ext2/file.c)
/* * We have mostly NULL's here: the current defaults are ok for * the ext2 filesystem. */
struct file_operations ext2_file_operations = {
llseek: ext2_file_lseek,
read: generic_file_read,
write: generic_file_write,
ioctl: ext2_ioctl,
mmap: generic_file_mmap,
open: ext2_open_file,
release: ext2_release_file,
fsync: ext2_sync_file,
};
看到么,这就是下层具体文件系统的文件操作实现,如果具体的文件系统不支持某种操作,其file_operations结构中的相应函数指针就是NULL。
其对应的文件操作函数也会在该文件下实现,代码就不贴了。
至此,我们可以得出,每个文件系统独有自己的file_operations数据结构,为了统一化结构工VFS调用。
每个进程通过“打开文件”open()来与具体的文件建立连接,这种连接以file数据结构为代表,其结构中有一个file_operations结构指针f_op,指向具体的file_operations数据结构,就指定了这个文件所属的文件系统,并且与具体文件系统所提供的一组函数挂上钩。
struct task_struct {
.....
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
......
};
我们先看files_struct数据结构
struct files_struct {
atomic_t count;
rwlock_t file_lock;
int max_fds;
int max_fdset;
int next_fd;
struct file ** fd; /* current fd array */
fd_set *close_on_exec;
fd_set *open_fds;
fd_set close_on_exec_init;
fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];
};
再看file数据结构
struct file {
struct list_head f_list;
struct dentry *f_dentry;
struct vfsmount *f_vfsmnt;
struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
int f_error;
unsigned long f_version;
/* needed for tty driver, and maybe others */
void *private_data;
};
看到没,里面就有file_operations数据结构,进程就是这么与打开的文件建立关联的(回想前面网络部分的socket。sock、inode等等不也是这样么)。
顺便多说一句,与具体已打开文件有关的信息在file结构中,你可以大致从命名上可以得知(get一点,好的命名规范可以提高代码的可读性以及有利于代码的维护性)。
fs_struct是关于文件系统的信息。
struct fs_struct {
atomic_t count;
rwlock_t lock;
int umask;
struct dentry * root, * pwd, * altroot;
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};
其中的dentry是一个目录结构,你从其变量名可以看出,root(根目录),pwd(当前目录)
综合上面我们大致可以认知到Linux内核中对VFS与具体文件系统的关系划分可以用下图表示:
一言以蔽之,通过VFS屏蔽下层文件系统之间的差异。
回到files_struct,其中有一个file结构数组struct file * fd_array[NR_OPEN_DEFAULT];,每打开一个文件以后,进程就通过一个打开文件号fid来访问这个文件,而fid就是数组fd_array的下标,每个file结构中有个指针f_op,指向该文件所属文件系统的file_operations数据结构。
此外每个文件还有一个“目录项”即dentry数据结构和“索引节点”即inode数据结构,这是个很重要的数据结构,里面记录着文件在存储介质上的位置与分布等信息。
struct dentry {
atomic_t d_count;
unsigned int d_flags;
struct inode * d_inode; /* Where the name belongs to - NULL is negative */
struct dentry * d_parent; /* parent directory */
struct list_head d_vfsmnt;
struct list_head d_hash; /* lookup hash list */
struct list_head d_lru; /* d_count = 0 LRU list */
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
struct qstr d_name;
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op;
struct super_block * d_sb; /* The root of the dentry tree */
unsigned long d_reftime; /* last time referenced */
void * d_fsdata; /* fs-specific data */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */
};
已经有英文注释了,我就不赘释了。
再看看inode数据结构
struct inode {
struct list_head i_hash;
struct list_head i_list;
struct list_head i_dentry;
struct list_head i_dirty_buffers;
unsigned long i_ino;
atomic_t i_count;
kdev_t i_dev;
umode_t i_mode;
nlink_t i_nlink;
uid_t i_uid;
gid_t i_gid;
kdev_t i_rdev;
loff_t i_size;
time_t i_atime;
time_t i_mtime;
time_t i_ctime;
unsigned long i_blksize;
unsigned long i_blocks;
unsigned long i_version;
struct semaphore i_sem;
struct semaphore i_zombie;
struct inode_operations *i_op;
struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct super_block *i_sb;
wait_queue_head_t i_wait;
struct file_lock *i_flock;
struct address_space *i_mapping;
struct address_space i_data;
struct dquot *i_dquot[MAXQUOTAS];
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
unsigned long i_dnotify_mask; /* Directory notify events */
struct dnotify_struct *i_dnotify; /* for directory notifications */
unsigned long i_state;
unsigned int i_flags;
unsigned char i_sock;
atomic_t i_writecount;
unsigned int i_attr_flags;
__u32 i_generation;
union {
struct minix_inode_info minix_i;
struct ext2_inode_info ext2_i;
struct hpfs_inode_info hpfs_i;
struct ntfs_inode_info ntfs_i;
struct msdos_inode_info msdos_i;
struct umsdos_inode_info umsdos_i;
struct iso_inode_info isofs_i;
struct nfs_inode_info nfs_i;
struct sysv_inode_info sysv_i;
struct affs_inode_info affs_i;
struct ufs_inode_info ufs_i;
struct efs_inode_info efs_i;
struct romfs_inode_info romfs_i;
struct shmem_inode_info shmem_i;
struct coda_inode_info coda_i;
struct smb_inode_info smbfs_i;
struct hfs_inode_info hfs_i;
struct adfs_inode_info adfs_i;
struct qnx4_inode_info qnx4_i;
struct bfs_inode_info bfs_i;
struct udf_inode_info udf_i;
struct ncp_inode_info ncpfs_i;
struct proc_inode_info proc_i;
struct socket socket_i;
struct usbdev_inode_info usbdev_i;
void *generic_ip;
} u;
};
inode描述的是文件的物理属性,存在多个逻辑文件(目录项)指向同一个物理文件(索引节点)的情况。
然后,然后,Linux支持的具体的文件系统则在该数据结构的union中,当inode所代表的是哪种文件,u就用作哪种数据结构。
在Linux中目录也被作为文件看待,只是目录作为一种比较特殊的文件,其特殊之处在于文件的内容是该目录中文件和子目录的dentry的描述符。
除了file_operations数据结构外,还有其余与目录项相联系的dentry_operations数据结构和索引节点相联系的inode_operations数据结构,很显然这两个数据结构中的内容也都是一些函数指针,但是这些函数大多只是在打开文件的过程中使用。
1、inode用以描述“目录节点”(Linux把目录或普通文件,统一看成“目录节点”),它描述了一个目录节点物理上的属性,比如大小,uid、gid、创建时间、修改时间等等;
2、file_operations是“目录节点”提供的操作接口,包括open、lseek、read、write、mmap等操作的实现;
3、inode通过成员i_fop对应一个file_operations;
4、打开文件的过程就是寻找“目录节点”对应的inode的过程;
5、文件被打开后,inode和file_operations都已经在内存中建立,file_operations的指针也已经指向了具体文件系统提供的函数,此后文件的一些操作,都由这些函数来完成。
ok,有了前面的基础,现在我们着重来分析一下上面这个结构图(Markdown编辑器很喜欢把图片缩小…):
篇幅有限,关于格式化某种文件系统的设备上的逻辑划分,我们下篇再分析。