linux 文件系统基本概念

所有未说明版本的内核原理分析博客,都是基于linux-2.4.0版本源码分析的。

概念:

文件系统是是linux操作系统中非常重要的部分。这里我们主要介绍有关ext2的相关实现。为了让Linux支持不同文件系统,就要将各种不同的文件系统的操作和管理纳入到一个统一的框架中。让内核中的文件系统界面成为一条文件系统的总线,使得用户程序可以通过同一个文件系统操作界面,即同一组系统调用,对各种不同的文件系统(以及文件)进行操作。这样,就可以对用户程序隐藏掉对各种不同的文件系统的实现细节,为用户程序提供一个统一的、抽象的、虚拟的文件系统界面,这就是所谓的虚拟文件系统交换VFS( The Virtual File System (otherwise known as the Virtual Filesystem Switch) )。这个抽象的界面主要由一组标准的、抽象的稳健操作构成,以系统调用的形式提供于用户程序,比如之前的网络协议栈的博客中的发送 write,接收 read等等还有其他的。这样,用户程序就可以把文件都看成一致的、抽象的vfs文件,通过这些系统调用对文件进行操作,而无需关心具体的文件属于什么文件系统以及具体文件系统的设计和实现。其中比较主要的一个标准就是对file_operations数据结构的其中的一些有明确的定义的一些函数指针的实现。

基于linux2.4.0分析。

数据结构:

file_operations{}数据结构:

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 *);
};

每种文件系统都有自己的file_operations数据结构,结构中的属性基本都是函数指针,即跳转表,例如read就是指向具体文件系统用来实现读文件操作的入口函数。如果具体的文件系统不支持某种操作,其file_operations结构中的相应函数指针就是NULL。我们后面的博客会分析各种指针的用途。

每个进程通过open然后再到系统调用与具体的文件建立起连接,或者说建立起一个读写的“上下文”。这种连接是以一个file数据结构来代表的,其内部有个 file_operations结构的指针f_op。将file结构中的指针f_op设置成指向某个具体的file_operations结构,那么就指定了这个文件所属的文件系统,并且与具体文件系统提供的一组函数挂上了钩。

进程与文件的连接,即“已打开文件”,是进程的一项“财产”,归具体进程所有。代表着这种连接的file结构必定与代表着进程的task_struct数据结构存在联系。我们看看task_struct结构中与此有关的几行属性:

struct task_struct {
......
/* filesystem information */
	struct fs_struct *fs;
/* open file information */
	struct files_struct *files;
.......
};

这里的两个指针fs和files,一个指向fs_struct,是关于文件系统的信息;另一个指向files_struct,是关于已打开文件的信息。先看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和altroot。这些指针各自指向代表一个“目录项”的dentry数据结构,里面记录着文件的各项属性,如文件名、访问权限等。其中pwd指向进程当前的工作目录,root指向的dentry结构代表着本进程的“根目录”,那就是当用户登录进入系统时所看到的目录,至于altroot则为用户设置的替换根目录,后面还会讲到。这个三个目录可以在不同的文件系统中。

与具体已打开文件有关的信息在file结构中,而files_struct结构的主体就是一个file结构数组。每打开一个文件以后,进程就通过一个“打开文件号”fd来访问这个文件,而fd实际上就是相应file结构在数组中的下标。dentry与file的关系是一对多。每打开一次文件就会创建一个file结构对象。file结构中有个指针f_dentry,指向该文件的dentry数据结构。

 每个文件除了有一个dentry数据结构外,还有一个索引节点即inode结构,里面记录着文件在存储介质上的位置和分布信息。同时,dentry结构中有个inode结构指针d_inode指向相应的inode结构。dentry结构与inode的作用是不同的,一个文件可能有好几个名字,而通过不同的文件名访问同一个文件时权限也有可能不同。所以dentry结构所代表的逻辑意义上的文件,记录的是逻辑上的属性。而inode结构所代表的是物理意义上的文件,记录的是其物理上的属性。

inode和dentry结构中有两个函数跳转表结构inode_operations和dentry_operations。这两个数据结构只是在打开文件的过程中使用,后者仅在文件操作的“底层”使用(分配空间),具体定义如下:

struct inode_operations {
	int (*create) (struct inode *,struct dentry *,int);
	struct dentry * (*lookup) (struct inode *,struct dentry *);
	int (*link) (struct dentry *,struct inode *,struct dentry *);
	int (*unlink) (struct inode *,struct dentry *);
	int (*symlink) (struct inode *,struct dentry *,const char *);
	int (*mkdir) (struct inode *,struct dentry *,int);
	int (*rmdir) (struct inode *,struct dentry *);
	int (*mknod) (struct inode *,struct dentry *,int,int);
	int (*rename) (struct inode *, struct dentry *,
			struct inode *, struct dentry *);
	int (*readlink) (struct dentry *, char *,int);
	int (*follow_link) (struct dentry *, struct nameidata *);
	void (*truncate) (struct inode *);
	int (*permission) (struct inode *, int);
	int (*revalidate) (struct dentry *);
	int (*setattr) (struct dentry *, struct iattr *);
	int (*getattr) (struct dentry *, struct iattr *);
};

struct dentry_operations {
	int (*d_revalidate)(struct dentry *, int);
	int (*d_hash) (struct dentry *, struct qstr *);
	int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);//文件名对比函数
	int (*d_delete)(struct dentry *);//删除文件的入口
	void (*d_release)(struct dentry *);//关闭文件的入口
	void (*d_iput)(struct dentry *, struct inode *);
};

在后面的博客中会陆续介绍这些函数指针对应的实现函数及其原理。

接下来看一个文件系统逻辑结构图:

所谓inode,也就是索引节点(i节点)的意思。要访问一个文件,一定要通过它的索引才能知道这个文件是什么类型的文件、怎么组织的、文件中存储着多少数据、这些数据在什么地方以及其下层的驱动程序在哪里等。数据结构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;//i节点号,在同一个文件系统中唯一,用于计算哈希值的一部分
	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;
};

接下来看磁盘上面的ext2的inode结构


/*
 * Structure of an inode on the disk
 */
struct ext2_inode {
	__u16	i_mode;		/* File mode */
	__u16	i_uid;		/* Low 16 bits of Owner Uid */
	__u32	i_size;		/* Size in bytes */
	__u32	i_atime;	/* Access time */
	__u32	i_ctime;	/* Creation time */
	__u32	i_mtime;	/* Modification time */
	__u32	i_dtime;	/* Deletion Time */
	__u16	i_gid;		/* Low 16 bits of Group Id */
	__u16	i_links_count;	/* Links count */
	__u32	i_blocks;	/* Blocks count */
	__u32	i_flags;	/* File flags */
	union {
		struct {
			__u32  l_i_reserved1;
		} linux1;
		struct {
			__u32  h_i_translator;
		} hurd1;
		struct {
			__u32  m_i_reserved1;
		} masix1;
	} osd1;				/* OS dependent 1 */
	__u32	i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
	__u32	i_generation;	/* File version (for NFS) */
	__u32	i_file_acl;	/* File ACL */
	__u32	i_dir_acl;	/* Directory ACL */
	__u32	i_faddr;	/* Fragment address */
	union {
		struct {
			__u8	l_i_frag;	/* Fragment number */
			__u8	l_i_fsize;	/* Fragment size */
			__u16	i_pad1;
			__u16	l_i_uid_high;	/* these 2 fields    */
			__u16	l_i_gid_high;	/* were reserved2[0] */
			__u32	l_i_reserved2;
		} linux2;
		struct {
			__u8	h_i_frag;	/* Fragment number */
			__u8	h_i_fsize;	/* Fragment size */
			__u16	h_i_mode_high;
			__u16	h_i_uid_high;
			__u16	h_i_gid_high;
			__u32	h_i_author;
		} hurd2;
		struct {
			__u8	m_i_frag;	/* Fragment number */
			__u8	m_i_fsize;	/* Fragment size */
			__u16	m_pad1;
			__u32	m_i_reserved2[2];
		} masix2;
	} osd2;				/* OS dependent 2 */
};

我们在后面的博客中会把这些字段的意义进行讲解。虽然现在inode结构里面包含了关于文件的组织和管理信息,但是有个关键信息却不在其中,那就是文件名。显然我们需要一种机制,来根据文件名就可以在磁盘上找到该文件的索引节点,从而在内存中建立起代表该文件的inode结构。这就是文件系统的目录树。这棵倒立的树从系统的根节点出发向下延展,除最底层的叶子节点为文件之外,其他的中间节点都是目录。其实目录也是一个文件,这种文件的文件名就是目录名,也有索引节点,并且有数据部分。所不同的是,其数据部分只包含目录项。对于ext2文件系统来说,就是ext2_dir_entry,后面改成了ext2_dir_entry_2,对内容进行优化,因为文件名最大长度为255,之前的ext2_dir_entry结构中的name_len为unsigned short类型,新版本使用一个unsigned char来表示,腾出一个字节用作文件类型。定义如下:

struct ext2_dir_entry {
	__u32	inode;			/* Inode number */
	__u16	rec_len;		/* Directory entry length */
	__u16	name_len;		/* Name length */
	char	name[EXT2_NAME_LEN];	/* File name */
};

/*
 * The new version of the directory entry.  Since EXT2 structures are
 * stored in intel byte order, and the name_len field could never be
 * bigger than 255 chars, it's safe to reclaim the extra byte for the
 * file_type field.
 */
struct ext2_dir_entry_2 {
	__u32	inode;			/* Inode number */
	__u16	rec_len;		/* Directory entry length */
	__u8	name_len;		/* Name length */
	__u8	file_type;
	char	name[EXT2_NAME_LEN];	/* File name */
};

文件类型:有字符设备和块设备。

#define EXT2_FT_UNKNOWN		0
#define EXT2_FT_REG_FILE	1
#define EXT2_FT_DIR		2
#define EXT2_FT_CHRDEV		3
#define EXT2_FT_BLKDEV 		4
#define EXT2_FT_FIFO		5
#define EXT2_FT_SOCK		6
#define EXT2_FT_SYMLINK		7

#define EXT2_FT_MAX		8

注意ext2_dir_entry_2 中的rec_len,说明这个数据结构的长度不固定。因为节点的长度相差很大,固定按最大长度255分配空间会造成浪费(磁盘)。

磁盘上的ext2_inode与内存中的inode结构有很大不同,同样,目录项ext2_dir_entry_2与内存中的dentry结构,也有很大不同。

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 */
};

很明显,dentry结构中大部分成分都是动态生成的。就静态部分如文件名与磁盘上的ext2_dir_entry_2有很大不同。以后我们会结合代码实现来解释原因。这里inode与ext2_inode存在显著不同并不奇怪,因为dentry和inode是属于VFS层的数据结构,需要适配各种不同的文件系统;而ext2_dir_entry_2和ext2_inode则专门用于ext2文件系统而设计的,所以前置除了包含许多动态信息以外,还是对后者的一种抽象和扩充,并不是只是后者的映象。

现在还有个问题,我们要访问一个文件就得想访问一个目录(父目录),才能根据文件名从目录找到该文件的目录项,进而找到其i节点,可是目录本身也是文件,它本身的目录项又在上一级目录的数据部分即目录项中,那就成了 “先有鸡还是先有蛋”的问题,或者说递归了?在linux内核中,存在这样一个目录,它本身不需要通过这个方式来访问,而是通过固定的算法可以找到。这就是根设备的根目录。每一个文件系统,即格式化成某种文件系统的存储设备上的一个根目录,同时又都有一个超级块,根目录的位置以及文件系统的其他一些参数就记录在这个超级块中。超级块在设备上的逻辑位置都是固定的,例如在磁盘的上则一般为第二个逻辑块。所以不需要去其他地方查找了。同时,对于一个特定的文件系统,超级块的格式也是固定的,系统在初始化时要将一个存储设备作为整个系统的根设备,它的根目录就成为整个文件系统的总根,就是"/"。更确切的说,就是把根设备的根目录安装到文件系统的总根节点上。有了根设备,还可以把其他存储设备也安装到文件系统中空闲的目录节点上。所谓安装就是从一个存储设备上面读入超级块,在内存中建立起一个super_block结构。再进而将此设备上的根目录与文件系统中已经存在的一个空白目录挂钩,系统初始化时整个文件系统中只有一个空白目录,所以根设备的根目录就安装在这个节点上。这样,从根目录"/"开始,根据给定的全路径名就可以找到文件系统中的任何一个文件,而不论这个文件是在哪一个存储设备上,只要文件所在的存储设备已经安装就行了。

但是,每次都要提供一个全路径名,并且每次都要从根目录开始查找,既不方便也是一种浪费。所以系统也提供了从当前目录开始查找的手段。每一个进程在每一个时刻都一个当前工作路径pwd,用户可以改变这个目录,但是永远都有这么一个目录存在。这样,就可以只提供一个从pwd开始的相对路径名来查找一个文件。这就是前面看到过的fs_struct数据结构中为什么要有一个指针pwd的原因。这个指针总是指向本进程的当前工作目录的dentry结构,而进程的task_struct结构中的指针fs则总是指向一个有效的fs_struct结构。每当一个进程通过chdir系统调用进入一个目录,或者在login进入用户的原始目录("Home Directory")时,内核就使该进程的pwd指针指向这个目录在内存中的dentry结构。相对路径名还可以用"../"开头,表示先向上找到当前目录的父目录,再从那里开始查找。相应的,在dentry结构中也有个指针d_parent,指向其父目录的dentry结构。

如前所述,fs_struct结构中还有一个指针root,指向本进程的根目录"/"的dentry结构。前面讲过,"/"表示整个文件系统的总根,可这只是就一般而言,或者对早期的Unix系统而言。事实上,特权用户可以通过一个系统调用chroot将另一个目录设置成本进程的根目录。从此以后,这个进程以及由这个进程所fork的子进程就把这个目录当成了文件系统的根,遇到文件的全路径名时就从这个目录而不是真正的文件系统总根开始查找。例如,要是这个进程执行一个系统调用chdir("/");就会转到这个现在的根目录而不是真正的根目录。这种特殊的设计也是从实践需求引起的,最初是为了克服FTP,特别是匿名FTP的一个安全性问题。FTP的服务器进程是特权用户进程(daemon),当一个远程用户与FTP服务进程建立连接以后,就可以在远端发出诸如"cd /"、get  /etc/passwd之类的命令。显然,这给系统的安全性造成了一个潜在的缺口,现在有了进程自己的根目录,以及系统调用chroot,就可以让FTP服务进程把另一个目录当成它的根目录,从而当远程用户执行前面的get /etc/passwd 时就会得到 文件找不到之类的出错信息,从而保证了passwd口令文件的安全性。

而且fs_struct结构中还有一个指针altroot,指向本进程的替换根目录。当进程执行一个系统调用chroot("/")时,如果它有替换根目录,即指针altroot不为0,就会转入其替换根目录,否则才转入其视在根目录。这样就可以视具体情况而在两个根目录中切换,让用户在不同的情况下看到不同的根目录。

对于普通文件,文件系统层最终要通过磁盘或者其他存储设备的驱动程序从存储介质中读或写。就ext2文件系统而言,从磁盘文件的角度来看,对存储介质的访问可以涉及到四种不同的目标,那就是:

(1)文件中的数据,包括目录的内容,即目录项ext2_dir_entry_2数据结构。

(2)文件的组织与管理信息,即索引节点ext2_inode数据结构。

(3)磁盘的超级快。如果物理的磁盘被划分成若干分区,那就包括每个逻辑磁盘的超级块。

(4)引导块。

跳转表:

(1)文件操作跳转表,即file_operations数据结构:file结构中的指针f_op指向具体的file_operations结构,这是read、write等文件操作的跳转表,一种文件系统并不只限于一个file_operations结构,如ext2就有两个这样的数据结构,分别用于普通文件和目录文件。

(2)目录项操作函数表,即dentry_operations数据结构:dentry结构中的指针d_op指向具体的dentry_operations结构,这是内核中hash、compare等内部操作的跳转表。如果d_op为0表示按linux默认的方式办(ext2)。注意,这里说的是目录项,而不是目录,目录本身是一种特殊用途和具有特殊结构的文件。

(3)索引节点操作跳转表,即inode_operations数据结构:inode结构中的指针i_op指向具体的inode_operations数据结构。同样,一种文件系统不只限于一个inode_operations结构。

(4)超级块操作跳转表,即super_operations数据结构:super_block结构中的s_op指向具体的super_operations结构,这是read_inode、write_inode、delete_inode等内部操作的跳转表。

(5)超级块本身也因文件系统而异。

由此可见,file结构、dentry结构、inode结构、super_block结构以及关于超级块位置的约定都是VFS层。此外,inode还有一个指针i_fop,也是指向具体的file_operations结构,实际上file结构中的指针f_op只是inode结构中这个指针的一个副本,在打开文件的时候从目标文件的inode结构中复制到file结构中。

最后还要注意,虽然每个文件都有目录项和索引节点在磁盘上,但是只有在需要时才在内存中为之建立相应的dentry和inode数据结构。

Guess you like

Origin blog.csdn.net/guoguangwu/article/details/118863340